PHPで機械学習(その3):単純ベイズ分類機

(1/1)
PHPで機械学習(その2):ツイート内容を学習」の続きである。
今回は、学習させた結果をもとに、あたえられたテキストが、どのユーザーのツイートであるかを推定するプログラムを完成させる。

機械学習にはいくつかのアルゴリズムがあるが、ここでは実装が簡単な単純ベイズ分類器を用いることにする。学習方法は簡単で、ユーザー別の単語の出現回数をデータベースに登録していけばいい。

(2021年7月11日)PHP8対応,リファラ・チェック改良

目次

サンプル・プログラムの実行例

PHPで機械学習(その3):単純ベイズ分類機
今まで蓄積した学習データに対し、「井の頭恩賜公園でお花見する」の判定を求めると、@papa_pahoo の可能性が最も高いという結果が出る。
学習データにある期間中、@papa_pahoo は「井の頭恩賜公園でお花見する」とツイートはしていないが、他のアカウント比べると、この発言をする可能性が比較的高いというのが単純ベイズ分類機の判定結果である。

サンプル・プログラム

圧縮ファイルの内容
BayesClassifier.phpサンプル・プログラム
pahooTwitterAPI.phpTwitter APIを利用するクラス pahooTwitterAPI。
使い方は「PHPでTwitterに投稿(ツイート)する」などを参照。include_path が通ったディレクトリに配置すること。

ベイズの定理

電子メールのスパムを振り分けるフィルターとして、ベイジアン・フィルターというプログラムが使われることが多い。これは「ベイズの定理」を応用したプログラムで、スパムメールのパターンを学習させて、その振り分けを行っている。このことから、ベイズの定理が機械学習に有効であることが分かる。
ベイズの定理は高校数学の範疇だが、ここで少し振り返っておこう。
ベイズの定理
事象Xが起きる確率を \( P(X) \)、事象Xが起きたもとで事象Yが起きる確率 \( P(YX) \) と表す。\( P(YX) \) は条件付き確率と呼ばれる。
左のベン図を見れば一目瞭然だが、Aが起きる確率、すなわちXもYもともに起きる確率は、\( P(X) \) と \( P(YX) \) から求めることができる。
\[ \displaystyle P(A) = P(X \cap Y) = P(X) P(YX) \]
同様にY側から考えると、
\[ P(X \cap Y) = P(Y) P(XY) \]
となり、次の等式が成立する。これがベイズの定理だ。
\[ \displaystyle P(X)P(YX) = P(Y) P(XY) \]
ベイジアン・フィルターを含めた機械学習では、\( P(YX) \) を求めることになるので、式を次のように変形しておく。
\[ \displaystyle P(YX) = \frac{P(Y) P(XY)}{P(X)} \]

単純ベイズ分類機

未知のテキストを与えたとき、学習したツイートを使って、それがどのユーザーのものであるかを、ベイズの定理を使って判定(推定)するアルゴリズムを考える。

あるテキスト \( text \) が与えられたとき、それがユーザー \( user \) のものである確率を \( P(textuser) \) とする。ベイズの定理の \( P(X) \) が \( P(text) \)に、\( P(Y) \) が \( P(user) \)に置き換わったと考えればよい。
\[ \displaystyle P(usertext)=\frac{P(user) P(textuser)}{P(text)} \]
となる。
ここで、\( P(text) \) はどのユーザーにおいても一定値であるから、
\[ \displaystyle P(usertext) \propto P(user) P(textuser) \]
と表せる。
すなわち、\( P(user) \) と \( P(textuser) \) の2つの確率が計算できればよい。
まず \( P(user) \) であるが、これは、
\[ \displaystyle P(user)=\frac{N(user)}{N(total)} \]
とあらわせる。ここで、\( N(total) \) は学習した全ツイート数、\( N(user) \) は当該ユーザーのツイート数である。
次に \( P(textuser) \) であるが、テキスト \( text \) は単語の集合である。単語が文書内にどこに出てくるかは考慮しない(これを bag-of-words と呼ぶ)と仮定すると、
\[ \displaystyle P(textuser)=P(word_1 \wedge word_2 \wedge ...\wedge word_kuser)=\prod_{i} P(word_iuser) \]

とあらわせる。
bag-of-words を仮定する方式を、単純ベイズ分類機(ナイーブ・ベイジアン)と呼ぶ。

ここで \( P(word_i) \) はきわめて小さな値であり、ツイートに多くの単語が含まれていると、乗算の結果が限りなくゼロに近づき、コンピュータの演算処理の中でアンダーフローを起こす恐れがある。そこで、対数をとって、乗算を加算に変更する。
\[ \displaystyle \log P(usertext)=\log P(user) + \sum_{i} \log P(word_iuser) \]
判定(推定)では、複数の \( user \) に対して \( \log P(usertext) \) を計算し、その結果が最大になる \( user \) を求める。

サンプル・プログラムの流れ

PHPで機械学習(その3):単純ベイズ分類機

単純ベイズ分類機

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 ==============================================================

判定処理では、前述の単純ベイズ分類機で説明した数式をそのまま実装する。
すなわち、\( P(user) \) を計算するのはメソッド userProb、\( P(usertext) \) を計算するのはメソッド textProb である。また、\( P(word) \)はメソッド textProb によって計算する。

ここで問題がある。
判定したいテキストの含まれる単語のどれか1つでも、学習結果に含まれていないと、\( P(word_i) \) がゼロになってしまい、判定ができなくなる。これをゼロ頻度問題と呼ぶ。
ゼロ頻度問題は、単語の出現回数に1を加えるというラプラススムージングによって回避することにした。

参考サイト

(この項おわり)
header