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 が起きる確率を  mimetex 、事象X が起きたもとで事象Y が起きる確率  mimetex  と表す。 mimetex  は条件付き確率と呼ばれる。
左のベン図を見れば一目瞭然だが、A が起きる確率、すなわち X も Y もともに起きる確率は、 mimetex  と  mimetex  から求めることができる。


 mimetex 


同様に Y 側から考えると、


 mimetex 


となり、次の等式が成立する。これがベイズの定理だ。


 mimetex 


ベイジアン・フィルターを含めた機械学習では、 mimetex  を求めることになるので、式を次のように変形しておく。


 mimetex 

単純ベイズ分類機

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

あるテキスト  mimetex  が与えられたとき、それがユーザー  mimetex  のものである確率を  mimetex  とする。ベイズの定理の  mimetex  が  mimetex に、 mimetex  が  mimetex に置き換わったと考えればよい。


 mimetex 


となる。
ここで、 mimetex  はどのユーザーにおいても一定値であるから、


 mimetex 


と表せる。
すなわち、 mimetex  と  mimetex  の 2 つの確率が計算できればよい。

まず  mimetex  であるが、これは、


 mimetex 


とあらわせる。ここで、 mimetex  は学習した全ツイート数、 mimetex  は当該ユーザーのツイート数である。

次に  mimetex  であるが、テキスト  mimetex  は単語の集合である。単語が文書内にどこに出てくるかは考慮しない(これを bag-of-words と呼ぶ)と仮定すると、


 mimetex 


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

ここで  mimetex  はきわめて小さな値であり、ツイートに多くの単語が含まれていると、乗算の結果が限りなくゼロに近づき、コンピュータの演算処理の中でアンダーフローを起こす恐れがある。そこで、対数をとって、乗算を加算に変更する。


 mimetex 


判定(推定)では、複数の  mimetex  に対して  mimetex  を計算し、その結果が最大になる  mimetex  を求める。

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

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

単純ベイズ分類機

0915: // [3] 判定(単純ベイズ分類機) ==============================================
0916: /**
0917:  * ユーザーの生起確率
0918:  * @param   string $screen_name スクリーンネーム
0919:  * @return  float 生起確率/FALSE
0920: */
0921: function userProb($screen_name) {
0922:     $sql_total = 'SELECT count(*) FROM ' . $this->table_tweets . ' WHERE 1';
0923:     $sql_count = 'SELECT count(*) FROM ' . $this->table_tweets . ' WHERE user_id=:user_id';
0924: 
0925:     list($userID$dt) = $this->getUserID($screen_name);
0926:     $total = 0;
0927:     $count = 0;
0928:     if ($userID != FALSE) {
0929:         //合計
0930:         $stmt = $this->pdo->prepare($sql_total);
0931:         $stmt->execute();
0932:         $row = $stmt->fetch();
0933:         $total = $row[0];
0934: 
0935:         //生起回数
0936:         $stmt = $this->pdo->prepare($sql_count);
0937:         $stmt->bindValue(':user_id', $userIDPDO::PARAM_STR);
0938:         $stmt->execute();
0939:         $row = $stmt->fetch();
0940:         $count = $row[0];
0941:     }
0942: 
0943:     return ($total == 0) ? FALSE : ($count / $total);
0944: }
0945: 
0946: /**
0947:  * あるユーザーにおける単語の出現頻度
0948:  * @param   string $screen_name スクリーンネーム
0949:  * @param   string $word        単語
0950:  * @return  float 出現頻度/FALSE
0951: */
0952: function wordProb($screen_name$word) {
0953:     $sql_total = 'SELECT * FROM ' . $this->table_tweets . ' WHERE user_id=:user_id AND flag=1';
0954:     $sql_count  = 'SELECT count FROM ' . $this->table_vectors . ' WHERE user_id=:user_id AND word=:word';
0955: 
0956:     list($userID$dt) = $this->getUserID($screen_name);
0957:     $total = 0;
0958:     $count = 0;
0959:     if ($userID != FALSE) {
0960:         //合計
0961:         $stmt = $this->pdo->prepare($sql_total);
0962:         $stmt->bindValue(':user_id', $userIDPDO::PARAM_STR);
0963:         $stmt->execute();
0964:         $res = $stmt->fetchAll();
0965:         $total = 1;
0966:         foreach ($res as $val)      $total++;
0967: 
0968:         //出現回数
0969:         $stmt = $this->pdo->prepare($sql_count);
0970:         $stmt->bindValue(':user_id', $userIDPDO::PARAM_STR);
0971:         $stmt->bindValue(':word', $wordPDO::PARAM_STR);
0972:         $stmt->execute();
0973:         $res = $stmt->fetchAll();
0974:         $count = 1;      //ラプラススムージング
0975:         foreach ($res as $val)      $count += $val['count'];
0976:     }
0977: 
0978:     return ($total == 0) ? NULL : ($count / $total);
0979: }
0980: 
0981: /**
0982:  * あるユーザーにおけるスコア計算
0983:  * @param   string $screen_name スクリーンネーム
0984:  * @param   string $text        テキスト
0985:  * @return  float スコア/NULL
0986: */
0987: function textProb($screen_name$text) {
0988:     $words = array();
0989: 
0990:     //ユーザーの生起スコア
0991:     $score = $this->userProb($screen_name);
0992:     if ($score == FALSE)    return NULL;
0993:     $score = log($score);
0994: 
0995:     //単語の出現スコア
0996:     $this->getWords($text$words);
0997:     foreach ($words as $word) {
0998:         if (! $this->isword($word))   continue;
0999:         $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 ==============================================================

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

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

参考サイト

(この項おわり)
header