サンプル・プログラムの実行例
学習データにある期間中、@papa_pahoo は「井の頭恩賜公園でお花見する」とツイートはしていないが、他のアカウント比べると、この発言をする可能性が比較的高いというのが単純ベイズ分類機の判定結果である。
サンプル・プログラム
BayesClassifier.php | サンプル・プログラム |
pahooTwitterAPI.php | Twitter APIを利用するクラス pahooTwitterAPI。 使い方は「PHPでTwitterに投稿(ツイート)する」などを参照。include_path が通ったディレクトリに配置すること。 |
ベイズの定理
ベイズの定理は高校数学の範疇だが、ここで少し振り返っておこう。
左のベン図を見れば一目瞭然だが、Aが起きる確率、すなわちXもYもともに起きる確率は、\( P(X) \) と \( P(Y
同様にY側から考えると、
\[ P(X \cap Y) = P(Y) P(X
となり、次の等式が成立する。これがベイズの定理だ。
\[ \displaystyle P(X)P(Y
ベイジアン・フィルターを含めた機械学習では、\( P(Y
\[ \displaystyle P(Y
単純ベイズ分類機
あるテキスト \( text \) が与えられたとき、それがユーザー \( user \) のものである確率を \( P(text
\[ \displaystyle P(user
となる。
ここで、\( P(text) \) はどのユーザーにおいても一定値であるから、
\[ \displaystyle P(user
と表せる。
すなわち、\( P(user) \) と \( P(text
まず \( P(user) \) であるが、これは、
\[ \displaystyle P(user)=\frac{N(user)}{N(total)} \]
とあらわせる。ここで、\( N(total) \) は学習した全ツイート数、\( N(user) \) は当該ユーザーのツイート数である。
次に \( P(text
\[ \displaystyle P(text
とあらわせる。
bag-of-words を仮定する方式を、単純ベイズ分類機(ナイーブ・ベイジアン)と呼ぶ。
ここで \( P(word_i) \) はきわめて小さな値であり、ツイートに多くの単語が含まれていると、乗算の結果が限りなくゼロに近づき、コンピュータの演算処理の中でアンダーフローを起こす恐れがある。そこで、対数をとって、乗算を加算に変更する。
\[ \displaystyle \log P(user
判定(推定)では、複数の \( user \) に対して \( \log P(user
サンプル・プログラムの流れ
単純ベイズ分類機
BayesClassifier.php
915: // [3] 判定(単純ベイズ分類機) ==============================================
916: /**
917: * ユーザーの生起確率
918: * @param string $screen_name スクリーンネーム
919: * @return float 生起確率/FALSE
920: */
921: function userProb($screen_name) {
922: $sql_total = 'SELECT count(*) FROM ' . $this->table_tweets . ' WHERE 1';
923: $sql_count = 'SELECT count(*) FROM ' . $this->table_tweets . ' WHERE user_id=:user_id';
924:
925: list($userID, $dt) = $this->getUserID($screen_name);
926: $total = 0;
927: $count = 0;
928: if ($userID != FALSE) {
929: //合計
930: $stmt = $this->pdo->prepare($sql_total);
931: $stmt->execute();
932: $row = $stmt->fetch();
933: $total = $row[0];
934:
935: //生起回数
936: $stmt = $this->pdo->prepare($sql_count);
937: $stmt->bindValue(':user_id', $userID, PDO::PARAM_STR);
938: $stmt->execute();
939: $row = $stmt->fetch();
940: $count = $row[0];
941: }
942:
943: return ($total == 0) ? FALSE : ($count / $total);
944: }
945:
946: /**
947: * あるユーザーにおける単語の出現頻度
948: * @param string $screen_name スクリーンネーム
949: * @param string $word 単語
950: * @return float 出現頻度/FALSE
951: */
952: function wordProb($screen_name, $word) {
953: $sql_total = 'SELECT * FROM ' . $this->table_tweets . ' WHERE user_id=:user_id AND flag=1';
954: $sql_count = 'SELECT count FROM ' . $this->table_vectors . ' WHERE user_id=:user_id AND word=:word';
955:
956: list($userID, $dt) = $this->getUserID($screen_name);
957: $total = 0;
958: $count = 0;
959: if ($userID != FALSE) {
960: //合計
961: $stmt = $this->pdo->prepare($sql_total);
962: $stmt->bindValue(':user_id', $userID, PDO::PARAM_STR);
963: $stmt->execute();
964: $res = $stmt->fetchAll();
965: $total = 1;
966: foreach ($res as $val) $total++;
967:
968: //出現回数
969: $stmt = $this->pdo->prepare($sql_count);
970: $stmt->bindValue(':user_id', $userID, PDO::PARAM_STR);
971: $stmt->bindValue(':word', $word, PDO::PARAM_STR);
972: $stmt->execute();
973: $res = $stmt->fetchAll();
974: $count = 1; //ラプラススムージング
975: foreach ($res as $val) $count += $val['count'];
976: }
977:
978: return ($total == 0) ? NULL : ($count / $total);
979: }
980:
981: /**
982: * あるユーザーにおけるスコア計算
983: * @param string $screen_name スクリーンネーム
984: * @param string $text テキスト
985: * @return float スコア/NULL
986: */
987: function textProb($screen_name, $text) {
988: $words = array();
989:
990: //ユーザーの生起スコア
991: $score = $this->userProb($screen_name);
992: if ($score == FALSE) return NULL;
993: $score = log($score);
994:
995: //単語の出現スコア
996: $this->getWords($text, $words);
997: foreach ($words as $word) {
998: if (! $this->isword($word)) continue;
999: $ret = $this->wordProb($screen_name, $word[0]);
1000: if ($ret == FALSE) return NULL;
1001: $score += log($ret);
1002: }
1003:
1004: return $score;
1005: }
1006:
1007: /**
1008: * 単純ベイズ分類機
1009: * @param string $text 判定したいテキスト
1010: * @param array $users ユーザー別判定結果を格納する配列
1011: * @return なし
1012: */
1013: function resultsClassified($text, &$users) {
1014: $sql_select = 'SELECT * FROM ' . $this->table_users . ' WHERE active=1';
1015:
1016: $stmt = $this->pdo->prepare($sql_select);
1017: $stmt->execute();
1018: $key = 0;
1019: while ($row = $stmt->fetch()) {
1020: $score = $this->textProb($row['screen_name'], $text);
1021: foreach ($users as $key=>$user) {
1022: if ($user['screen_name'] == $row['screen_name']) {
1023: $users[$key]['score'] = $score;
1024: break;
1025: }
1026: }
1027: }
1028: }
1029:
1030:
1031: // End of Class ==============================================================
ここで問題がある。
判定したいテキストの含まれる単語のどれか1つでも、学習結果に含まれていないと、\( P(word_i) \) がゼロになってしまい、判定ができなくなる。これをゼロ頻度問題と呼ぶ。
ゼロ頻度問題は、単語の出現回数に1を加えるというラプラススムージングによって回避することにした。
参考サイト
- PHPで機械学習(その1):ツイートを取得しDB格納:ぱふぅ家のホームページ
- PHPで機械学習(その2):ツイート内容を学習:ぱふぅ家のホームページ
- PHPで機械学習(その3):単純ベイズ分類機:ぱふぅ家のホームページ
- PHPとデータベース:ぱふぅ家のホームページ
- PHPでTwitterに投稿(ツイート)する:ぱふぅ家のホームページ
- 機械学習とは:sas
- ベイズの定理:高校数学の美しい物語
- ベイズの定理のおさらい:T_NAKAの阿房ブログ
- 4 情報と確率(ベイズの定理):麻雀概論
- オッズとベイズの定理:てら58
今回は、学習させた結果をもとに、あたえられたテキストが、どのユーザーのツイートであるかを推定するプログラムを完成させる。
機械学習にはいくつかのアルゴリズムがあるが、ここでは実装が簡単な単純ベイズ分類器を用いることにする。学習方法は簡単で、ユーザー別の単語の出現回数をデータベースに登録していけばいい。
(2021年7月11日)PHP8対応,リファラ・チェック改良