棚からパルチャギ

日々の雑記、ニュース拾いとか

2008. 02. 18

PHPでベイジアンフィルタ (実践編)
実践編です。
ベイジアンフィルタを使ったアプリケ-ションの流れは、大きく分けて以下の3段階になります。
  • カテゴリ(クラス)定義
  • パターン学習
  • 文書分類
単純ベイズ分類器(Naive Bayes classifier)ではクラス毎に単語の出現頻度を記憶して、その情報をもとに文書がそれぞれのクラスに属する確率を求めます。 SPAMフィルタなどでは「spam」と「nospam」のように2つのクラスだけで使用されることが多いです。多分。

パターン学習は、特定の文書(単語のセット)がどのクラスに所属するかを指定します。 これにより出現頻度のデータベース(コーパス)が更新されて、次回以降の分類精度を向上させることができます。 通常は、クラスを最初に設定して、以降は学習と分類を繰り返すような感じになると思います。


…ということで、クラスの定義から。
何故かNaiveBayesianStorageには、カテゴリ操作のインターフェースが提供されていないので、手作業で行います。 addCategory()とかdeleteCategory()とか追加してあげると便利なんですが。
mysql> insert into nb_categories (category_id) VALUES ('spam'); mysql> insert into nb_categories (category_id) VALUES ('nospam');


とりあえず一般的な例で、メールの文章を適当に登録してSPAM判定が行えるかどうか試してみます。 クラス毎にパターンを覚えさせる必要があるので、ここではSPAMメールの文章と、普通のメールの文章を各10件ずつ用意して登録します。 NaiveBayesian::train()で登録させるので、登録用のPHPスクリプトなんかを適当に。
$login = 'username'; // MySQL user $pass = 'password'; // MySQL password $db = 'nb'; $server = 'localhost'; include_once 'class.naivebayesian.php'; include_once 'class.naivebayesianstorage.php'; include_once 'class.mysql.php'; include_once 'class.naivebayesian_jp.php'; // NaiveBayesianJP $nbs = new NaiveBayesianStorage($login, $pass, $server, $db); $nb = new NaiveBayesianJP($nbs); $pattern_spam = array( '25歳人妻です旦那との夜の生活が3ヶ月に一度です。新婚の頃は…', '現在、当サイトにご登録されている女性会員様(約48万人)の…', '業界各方面から多大なる絶賛を得ています。過去には無かった体験が…', '人妻が大好き!または人妻に興味がある!という方だけ…', '私は市内の某私立高校に通う女子高生です。実は先日39歳になる母が…', ... ); foreach ($spams as $idx => $doc) { $cat = 'spam'; $docid = $cat .'_'. $idx; if ($nb->train($docid, $cat, $doc)) { $nb->updateProbabilities(); } } // 普通のメールの登録も同様に
NaiveBayesian::train() の $docid には文書に付ける一意のID(登録済みのIDと重複しなければなんでもいいです)を指定します。 $cat にはカテゴリ名、$docには文書を渡します。単語の抽出は自動的に行われます。


で、メインであるところの文書分類。
判定したい文章を渡して NaiveBayesian::categorize() すると各クラスに所属する確率が返る…はず。
$nbs = new NaiveBayesianStorage($login, $pass, $server, $db); $nb = new NaiveBayesianJP($nbs); $text = 'ドスケベな男女に朗報です△今すぐにエッチがしたい!近所で…'; $scores = $nb->categorize($text); while(list($cat,$score) = each($scores)) { echo "$cat : $score\n"; }
出力結果。ちゃんとSPAM判定してくれている模様。
ここで結果が間違っている場合は、上と同じやり方で NaiveBayesian::train() を使って学習させます。
spam : 0.99946305424632 nospam : 0.032765884645883


SPAMフィルタの場合は分類失敗よりも誤検出のほうが被害が大きいので、このときの確率が0.90以上の場合のみSPAM判定するみたいな分類をしたほうがいいかもしれません。 カテゴリが複数存在する場合は、一番確率が高いものを選択すれば良いと思います。


といった感じで、比較的簡単にベイジアンフィルタが使えそうな感じではあるんですが、たまに確率が1.0を超えたりする場合があって微妙に謎です。 どこかでやり方間違えてるのかなー。
spam : 6.1633758754325E-10 nospam : 1
こんなのとか、こんなのとか。
spam : 1 nospam : 9.547379900121E-13

学習パターン数が少ないと発生しやすいみたいなので、それなりのパターンを登録してあげる必要があるかもしれないですね。 とりあえず10件や20件では全然少ないので、1000件くらいのパターンは欲しいかも。
PHPでベイジアンフィルタ (準備編)
先日のデブサミ2008で、モバゲータウン(DeNA)が悪質な書き込みなんかを形態素解析+ベイジアンフィルタで抽出しているという話をしていたので、 SPAMフィルタ以外にも結構汎用的な文書分類に使えるものかと思い立ったので、ちょと調査。

単純ベイズ分類器と小一時間にらめっこした結果、スポンジ化した脳では理解するのに相当な時間を要すると判断したのでライブラリに頼ってみます。 仕事でも使えるようにとPHPで使えるものを探してみたんですが、これ(↓)しか見つかりませんでした。 CPANのAlgorithm::NaiveBayesを参考に作られたっぽいですね。

…とりあえず、日本語対応。
ライブラリ内に字句解析の処理があるんですが、英文字しか対応していないので日本語は通りません。 MeCabとかを使って単語抽出してあげる必要があるんですが、今回はYahoo!の日本語形態素解析サービスで誤魔化してみました。

PHP4なのをいいことに、NaiveBayesianのサブクラスとして_getTokens()をオーバーライド。 このメソッドで、キーが単語、値が出現回数の連想配列を返せばいいので、助詞・助動詞を除いた単語群を取得して配列にいれてあげます。 あと、日本語だと2文字の単語も多いので $min_token_length は 2 に変更。

$yahoo_appid は各自、Yahoo!で取得したアプリケーションIDを設定します。
class NaiveBayesianJP extends NaiveBayesian { var $min_token_length = 2; var $yahoo_maservice = 'http://api.jlp.yahoo.co.jp/MAService/V1/parse'; var $yahoo_appid = 'xxxxxx'; function _getTokens($string) { $tokens = array(); $result = $this->_yahooMA($string); if ($result && ($xml = simplexml_load_string($result))) { foreach ($xml->uniq_result->word_list->word as $word) { $token = (string)$word->surface; if ((mb_strlen($token) >= $this->min_token_length) && (mb_strlen($token) <= $this->max_token_length)) { $tokens[$token] = (int)$word->count; } } } return $tokens; } function _yahooMA($string) { $params = array( 'appid' => $this->yahoo_appid, 'sentence' => urlencode($string), 'results' => 'uniq', 'response' => 'surface', 'filter' => '1|2|3|4|5|6|7|8|9|10', ); $query_string = ''; foreach ($params as $key => $value) { $query_string .= ($query_string)? '&' : '?'; $query_string .= $key .'='. $value; } return file_get_contents($this->yahoo_maservice . $query_string); } }


あとは、データベースの準備。
デフォルトではMySQLの使用が前提なので、あらかじめインストールするなりして用意します。。 適当な名前でデータベースを作成して、ライブラリ同梱のmysql.sqlを実行。 元のデータベース名が「nb」なので、ここでも同じ名前にしておきます。
mysql> create database nb character set utf8 collate utf8_unicode_ci; mysql> use nb; mysql> source ~/phpnaivebayesian/mysql.sql;

ちなみにデータベースのエンコードをUTF-8にした場合、インデックスのキーサイズが1000bytesを超えてしまうので、以下のようなエラーが発生します。 すみません、先に言えよって感じですね。UTF-8だと1文字3bytes相当で計算するので mysql.sql 内の定義をすべて varchar(250) → varchar(160) くらいにすれば無問題です。
#1071 - Specified key was too long; max key length is 1000 bytes

続きます。
JavaScriptで「SL」
まさかブラウザ上でもこれが見られるなんてw

最近見てないけど、前の会社では開発鯖に仕込まれていたので、数か月に一度くらいはタイピングミスで走らせてしまってました。 「ls」のオプションによって、車両の数とか速度とかが変わった気がしますね。 JavaScriptでF5禁止だの右クリック禁止だのしているサイトも、このくらいユーモアがあればいいんですけどねー。(SL発車!)
ライフハック(笑)じゃなくて
非コミュとかウケミンとか今更どうこうできるようなものでもない気がするけど、 ライフハックとかじゃなくてもっと人生観とか生き方とかをドラスティックに変えるような何かはないかなあ。 このままだと、余命1ヶ月とか言われても、前日までくだらない生活を続けてしまいそうな気がする。
今日の浪費
狼と香辛料〈7〉Side Colors (電撃文庫)
なんというか、この作品の9割はホロの魅力だけで成り立ってると実感させられるようなニヤニヤ短編集。 本編では旅の道連れも増えてこういう掛け合いが少なくなりそうなのは、微妙に残念な感じです。 アニメ版は好評みたいだけど、ずるずると引き延ばしたりしないで綺麗に終わってくれるといいなあ。
ところで、先月発売の新刊は、アニメ放映開始の1月まで引っ張ればよかったのにと思っていたけど、ちゃんと用意してあったあたりは商売上手。


かんなぎ (4) (REX COMICS)
海イベントにスク水幼女とはなんというテコ入れ。 こちらもアニメ化発表てことで、スタッフ見る感じでは少し期待してもよさそうなんですが、 「ペンギン娘」といい手当たりしだいアニメ化している感が拭えない。。。今更かも。

ライトノベルは最近続きものしか買っていなかったけど、今月発売の新人作家作品は結構好評なのが多いですねー。 あと、『ミミズクと夜の王』を書いた紅玉いづき氏の新刊も出ているっぽいので、1作目が結構衝撃的だっただけに、どんな作品なのか楽しみです。 とりあえず、いろいろ買うのはあと1週間我慢…。