棚からパルチャギ

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

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

続きます。

2008. 01. 23

拡張マイリストFix
動画タイトルの取得に失敗する可能性があるので、XPathの記述をちょこっと修正しました。

今回は以下のような指摘をいただきまして…

するとあることに気がつきました。
ある動画に限ってタイトルがうまく表示されないようなのです。

実は前からGreasemonkeyスクリプトに興味があった私はこれはいい機会!と思ってソースを拝見してみました。

原因はどうやら、タイトル文字の取得方法にありそうです。

Andre's garden: ニコニコ動画拡張マイリストをHackしてみた
実はこの現象、自分の環境(*1)では再現しないのと、 //input[@name=\'m_title\'] というXPathも対応するノードが存在しないので修正コードが動作しなかったりと、微妙に謎だったりするのですが…。

一応件名取得は class="video" な<a>タグのテキストをFIRST_ORDERED_NODE_TYPEで先頭のものだけ取得しているので、 うp主コメントのリンクで誤爆することはないはずなんですが、実際videoクラスのaタグが複数存在することは想定外だったので、 確かにタイトル文字列の取得には問題ないとは言えなさそうです。 ということで、h1で囲まれたリンクのみを対象にしてみたので同様の誤爆は今後発生しないかと思われます。

既にタイトルが動画ID等になってしまっている方はお手数ですが修正版に更新してから、再度動画の登録をお願いします。 その際にどの動画で正しく取得できなかったのか教えていただけると嬉しいです。 ちなみに今回指摘のあったすばらしき新世界をヴァイオリンで弾いてみたは素晴らしい動画でした。 andreryuさん、ありがとうございます(感謝するところが違う><)

(*1) WinXP+Firefox(2.0.0.11)+Greasemonkey(0.7.20070607.0/0.7.20080121.0)

2008. 01. 05

ニコニコ動画に拡張マイリスト(ry
よしよし動画とニコニコ生放送対応。最近の流れに全然追いついてないw
ニコニコ側の仕様が固まってきたので、いちいち追加しないでも対応できるようにそのうち書き換えます。 正月休み(無職だけど)は、これのネットワーク対応するつもりが、ちょっと遊びすぎてしまった。。。

(追記) そもそも仕様変更で動画タイプの画像が表示されてなかったので、とりあえずグラビア動画も含めて共通化されたっぽいURL等に修正。すっきり。 アメーバビジョンは動画が見つからないので未検証です。 あと、キャッシュでスクリプトが更新されないことが多かったので、.htaccessで無効化しました。

2007. 11. 18

Android (エミュレータ小ネタ)
Androidのエミュレータは、小さなディスプレイとボタンが並んだ無骨なデザインで、 少しがっかりした人もいるかもしれませんが(実機ではないですけど)、 エミュレータのスキンは全部で4種類あるので、別のデザインに変更することが可能です。

> emulator -skin HVGA-P
起動時の引数に、上記のようにスキン名を指定すると、任意のスキンを適用してエミュレータが起動します。 スキンファイルは <android_sdk(インストールパス)>/tools/lib/images/skins に置かれています。 必要な画像を用意してレイアウトを定義すれば、自作スキンで動かすこともできそうですね。 各スキンのデザインはこんな感じ(↓)。


左から順に…
・HVGA-L
・HVGA-P
・QVGA-L (デフォルト)
・QVGA-P



もう一つ。
デフォルトの壁紙は、オーロラっぽいのとNice boat.っぽいものの2種類しか用意されていないんですが、 どうにも味気ないので自分好みの壁紙に変更する方法。 実機じゃないのであまり意味はないですけど、開発中のモチベーション維持にw

画像(ここではimage.jpg)を用意して、エミュレータが起動している状態で、コマンドラインから以下のコマンドを実行します。 エミュレータを再起動すると壁紙が変更されています。元に戻す場合は、デフォルトの壁紙を選択しなおしてください。
> adb push image.jpg /data/misc/wallpaper

画像形式はJPG/GIF/PNG/BMPあたりなら認識してくれるようです。 サイズは解像度に合わせて自動的に拡大/縮小されますが、横長の壁紙を縦画面のインターフェースで使用すると、 縦に伸ばされてしょんぼりするので、解像度にあったサイズで用意しておくのがよいと思います。
スキンの解像度は、320x240/480x320の縦または横画面。 バッテリー残量や時計表示で上部に20dot分確保されるので、その分を差し引く必要があります。
Android (プリプロセッサ)
携帯Javaの世界では、とにかくファイルサイズを縮小する必要があるので、 プリプロセッサを使って定数やメソッドをマクロで展開したりすることも多かったと思います。 そうでなくても、#ifdef とか enum(これは言語仕様だけど) 使いたいよねー、 ということでC/C++付属のものや私製のプリプロセッサを仲介させる場合は、build.xmlに以下のような記述を追加します。
<!-- Execute preprocessor --> <property name="preprocess.exec" value="C:/Development/Tools/preprocessor.exe" /> <target name="preprocess"> <apply executable="${preprocess.exec}" dest="${srcdir}" parallel="false"> <srcfile/> <targetfile/> <fileset dir="${srcdir}" includes="**/*.j"/> <mapper type="glob" from="*.j" to="*.java"/> </apply> </target>

<!-- Compile this project's .java files into .class files. --> <target name="compile" depends="dirs, resource-src, aidl, preprocess"> ... </target>
preprocessターゲットを新規に追加して、compileターゲットの依存関係に追加します。 ここでは、ソースコードディレクトリに存在する*.jファイルを、プリプロセッサを介して*.javaファイルに変換しています。

赤字のプリプロセッサのファイルパスは使用するプログラムに置き換えてください。 iアプリのときはpp_incというプリプロセッサを使っていたんですが、 Java5以降のアノテーション(@overrideとか)に対応していないので、変数やメソッド名の短縮命令と間違われて正しく処理できなくなってしまいました。残念。 前の会社で社内ツールとして使っていたプリプロセッサは、携帯アプリ開発に特化していたのもあって、 非常に高機能で便利だったんですが、あれをオープンソースとかで公開してくれないかしら。

2007. 11. 17

Android (携帯アプリ互換編)
[android.swf]
iアプリでの定石(なのかも微妙だけど)だと、アプリケーション本体であるIApplicationと描画用のCanvasを継承したクラスを1つずつ用意して、 Canvasのメソッド内でメインループを回すのが一般的だと思います。 メインループはwhile()で回して、System.getCurrentMiils()を見ながらFPSを調整、キー情報はループ毎にポーリングして、 描画はCanvasから取得したGraphicsにlock/unlockして描画みたいな。

AndroidではIApplocationに相当するものがActivity。 描画はCanvasそのものに描くのではなくて、View#OnDraw()で引数として渡されるCanvasに描画。 メインループは、Handlerで定期的に内部処理と描画の更新を行うというのが作法(?)みたいですね。

速度や正確さを求めると結局はwhile()で回すことになるのかもしれないけど、 携帯アプリみたいにほぼシングルスレッドで動作するわけでもなさそうなので、とりあえずは奇麗な方法で。 キー情報はonKeyイベントが発生するまで状態を取得する手段が無いっぽいので、 イベントハンドラでキー状態を保持するようにKeyPadStateクラスを作成しました。 とりあえずメインループとキー情報が取れて、描画対象が1枚だけということになれば携帯アプリと同じような感覚で作れるかなー、 ということで三角形が動くだけのサンプルでも置いておきます。
描画関係だと、あとはスプライトとして使っていたImageはDrawableで代用。 背景なんかは従来通りにBitmapで保持して、BitmapDrawableで描画すればなんとかなりそう。 ダブルバッファリングはonDraw()の最中に表示スクリーンが再描画されることはないので、特に意識する必要はなさそう。 シーン管理を書けばゲームっぽいものなら作れる気がしますね。

あと必要そうなのは、データの読み書きと通信処理、サウンド機能。OpenGLとか3D関連は苦手。 端末機能は当面使わないので保留。公式のサンプルコードはじっくり読むとかなり参考になるので、 ドキュメントと合わせてもう少し勉強します。
Android (デバッグ編)
DDMS(Dalvik Debug Monitor Service) Toolという素敵っぽいツールが付属しているんですが、 いまいち使い方が把握できていないので、とりあえずは原始的にprintデバッグ的な。 Eclipseと統合していれば、この辺もコンソールに表示されたりするんだろうなー、とか思いつつ。

Androidのデバッグ出力はSystem.out.println()ではなく、android.util.Logを使います。 staticなメソッドがあるので、そのままこんな感じで(↓)呼び出します。
private static final String TAG = "APPNAME";

Log.e(TAG, "This is ERROR message."); Log.w(TAG, "This is WARNING message."); Log.i(TAG, "This is INFO message."); Log.d(TAG, "This is DEBUG message."); Log.v(TAG, "This is VERBOSE message.");
「Log.」に続く一文字がエラーレベルを示します。「e」ならば「ERROR」。 ERRORが最も深刻度が高く、順にWARNING→INFO→…と下がっていきます。 第1引数はログメッセージにつけるタグ名で、ログをフィルタリングする際に使用するので、 アプリケーションの名前などを設定しておくと良いと思います。

吐き出されたログメッセージは、adb logcat で閲覧できます。
> adb logcat APPNAME:D
そのまま表示すると大量に流れてしまうので、オプションでタグ名とエラーレベル(優先度)を指定してフィルタリングします。 上記の例では、タグが「APPNAME」でエラーレベルがDEBUG以上のメッセージのみを表示します。 タグ名は「*」にすると、全てのタグを対象にすることができます。
> adb logcat *:W
エラーレベル(優先度)に指定できる文字列は以下の通り。調べるまでもないかもですが…。
> adb logcat --help V Verbose D Debug I Info W Warn E Error F Fatal
Android (インストール編・補足)
書き忘れていたので、補足です。
Androidのアプリケーションはパッケージ単位で認識されているようなので、 たとえば com.example というパッケージでは1つのアプリケーションしか登録することができません。多分。 なので、com.example.Foo と com.example.Bar というアプリケーションを同時にインストールすることはできず、 どちらか一方だけが有効になります。

パッケージ名はアプリケーション名まで含めて com.example.foo.Foo のように命名したほうが良さそうですね。 …って、これJavaでは常識だったりしますか???


あとは、アプリケーションの削除について。 エミュレータのインターフェース上ではアプリケーションを削除することができないので、ここでもadb(Android Debug Bridge)を使用します。
1. エミュレータを起動する
2. adb shellでエミュレータ(が動いている土台のqemu?)にシェルログインする
> adb shell
3. data/app に移動する
# cd data/app
4. インストールした.apkファイルが置いてあるので削除する
# ls ls -rw-r--r-- system system 1325833 2007-11-11 20:59 ApiDemos.apk -rw-rw-rw- root root 118915 2007-11-16 06:19 LunarLander.apk # rm LunarLander.apk

  [ 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 ]