概要

なんだかナイーブな気持ちになったので、Mahoutのnaive bayesを使って心を落ち着けようとしてみた。

バージョンは0.7。コマンドの引数はバージョンによってけっこう違うので注意。

@CretedDate 2012/09/22
@Env Java7, Mahout0.7

テストデータを用意する

Wikipediaから野球サッカーF1の記事を各10個ずつ、計30記事を取ってきて、3つのジャンルでclassifyできるよう教育してみる。

コマンドから実行する場合、ディレクトリ名 = ラベルとして扱われる。ので、下記のようにカテゴリごとにディレクトリを分けてファイルを配置しておく。

|-- baseball
|   |-- buffaloes
|   |-- dragons
| (中略)
|   |-- swallows
|   `-- tigers
|-- f1
|   |-- cenna
|   |-- europeangp
| (中略)
|   |-- spaingp
|   `-- williams
`-- soccer
    |-- antlers
    |-- flugels
  (中略)
    |-- spulse
    `-- verdy

わかち書きはMeCabを利用。下記のようなシェルでディレクトリ配下のファイルをわかち書きし、元ファイルは消す。

for f in `ls`
do
  mecab -Owakati < $f > $f.wakati
done

seqdirectoryで文書をシーケンスファイルに

用意したファイルに対してseqdirectoryを実行する。用意したデータは仮にローカルの/tmp/wikipediaディレクトリ配下にいるものとする。

$ bin/mahout seqdirectory \
    -i file:///tmp/wikipedia \
    -o /tmp/wikipedia_seq

出来上がったファイルを見てみる。

$ bin/mahout seqdumper -i /tmp/wikipedia_seq -b 30

Key: /f1/fduct.wakati: Value: 近年 の F 1 マシン は エンジン 開発 が 凍結 さ 
Key: /f1/shumacher.wakati: Value: ミハエル ・ シューマッハ ( Michael Schuma
Key: /f1/williams.wakati: Value: 1970 年代 に 誕生 し た コンス トラクター として
Key: /f1/europeangp.wakati: Value: 2007 年 は 、 予選 で ルイス ・ ハミルトン が 
  ・
  ・

こんな感じでファイル名をKeyに、テキストをValueにしたシーケンスファイルが出来上がっている。

ここのKeyのf1のところ(1つ目のディレクトリ名)がラベルとして使用される。

seq2sparseする

seqdirectoryで作ったファイルを、seq2sparseを使ってVectorに変換する。

わかち書きは済んでいるので、AnalyzerにはWhitespaceAnalyzer(空白を区切り文字としてtokenizeする)を指定する。

$ bin/mahout seq2sparse \
    -i /tmp/wikipedia_seq \
    -o /tmp/wikipedia_sparse \
    -a org.apache.lucene.analysis.WhitespaceAnalyzer

これでTF/IDFとかも加味されたVectorファイルができた。

trainしてみる

Vectorデータができたので、とりあえずtrainを実行してみる。

$ bin/mahout trainnb \
    -i /tmp/wikipedia_sparse/tfidf-vectors/part-r-00000 \
    -o /tmp/wikipedia_model \
    -li /tmp/wikipedia_labelIndex

-cを指定するとcomplementary naive bayes(情報に偏りがあっても精度が出やすいヤツ)で実行してくれる。-elを指定すると-liで指定したパスにlabelIndexを生成してくれる。

labelIndexは正解ラベルの一覧みたいなもの。試しに生成されたlabelIndexを見てみる。

$ bin/mahout seqdumper -i /tmp/wikipedia_labelIndex

Key: baseball: Value: 0
Key: f1: Value: 1
Key: soccer: Value: 2

ディレクトリ名に指定した、baseball、f1、soccerがラベルとして採用されている。

ちなみにラベルを決めている部分のコードは下記。

String theLabel = ((Pair<?,?>) label).getFirst().toString().split("/")[1];

見ての通り、/でsplitして1番目を取っているので、スラッシュが0個のKeyを渡すとおそらく落ちる。

とにもかくにも、これで-oで指定したパス配下にモデルファイルが作成された。

testnbしてみる

どの程度の精度が出ているかtestnbで確認する。

とりあえずtrainに使ったデータでそのまま使って実行してみる。

$ bin/mahout testnb \
  -i /tmp/wikipedia_sparse/tfidf-vectors \
  -o /tmp/wikipedia_test \
  -m /tmp/wikipedia_model \
  -l /tmp/wikipedia_labelIndex \
  -ow -c

こんな結果が表示される。

Correctly Classified Instances          :         30	       100%
Incorrectly Classified Instances        :          0	         0%
Total Classified Instances              :         30

splitしてから実行してみる

学習したデータでそのまま評価したら、そりゃいい成績が出るに決まってるので、用意したデータを2つに分けて、片方でtrain、片方でtestしてみる。

splitコマンドを使えばデータを複数に分けられる。試しに--randomSelectionPct30%に指定してみる。

$ bin/mahout split \
  -i /tmp/wikipedia_sparse/tfidf-vectors \
  --trainingOutput /tmp/wikipedia_train_data \
  --testOutput /tmp/wikipedia_test_data \
  --randomSelectionPct 30 \
  --method sequential \
  -ow -seq

出来上がったテストデータの数を見てみる。

$ bin/mahout seqdumper -i /tmp/wikipedia_test_data --count

Count: 8

8件がテストデータ側に分配されていた。

全30件の30%だから期待値は9。けど、あくまでランダムなので実行するたびに数は変化する。母数が少ないとけっこう誤差が出る。一般的にMahoutで扱うレベルのレコード数ならだいたい期待値通りになるだろうけど。

22件のデータでtrainして、8件のデータでテストしてみる。

$ bin/mahout trainnb \
    -i /tmp/wikipedia_train_data \
    -o /tmp/wikipedia_model \
    -li /tmp/wikipedia_labelIndex \
    -ow -c -el

$ bin/mahout testnb \
  -i /tmp/wikipedia_test_data \
  -o /tmp/wikipedia_test \
  -m /tmp/wikipedia_model \
  -l /tmp/wikipedia_labelIndex \
  -ow -c

結果

Correctly Classified Instances          :          7	      87.5%
Incorrectly Classified Instances        :          1	      12.5%
Total Classified Instances              :          8

1つ不正解。

生成したmodelを使ってみる

最後に、出来上がったmodelを使って、さらっとclassifyしてみる。

trainの時にmodelの出力先に指定したパスに、naiveBayesModel.binというファイルが出来ている。これはシーケンスファイルではなくバイナリファイルらしい。

具体的には、最初にfloatが1つ入っていて、その後にベクターが続くバイナリファイルっぽい。

この子をNaiveBayesModel.materializeで読み込んで、判定させてみる。

例として、「サッカー」「ゴール」「ナビスコ」の3つの単語が入っただけの文書をでっち上げてclassifyする。当然、サッカーのカテゴリが選ばれるはず。

まずは単語をIDにしないといけないので、seq2sparseの時に生成されたditionaryファイルをHDFSから持ってくる。

$ bin/mahout seqdumper -i /tmp/wikipedia_sparse/dictionary.file-0 -o dic

「サッカー」「ゴール」「ナビスコ」の各IDを(コードで自動でぶつけるのも面倒だったので)手動で確認して、Vectorを生成する。

ホントはfrequencyも加味した方が良さそうな気がするけど(やっぱり面倒なので)パス。

Configuration conf = new Configuration();
// カレントディレクトリにnaiveBayesModel.binがあること
NaiveBayesModel model = NaiveBayesModel.materialize(new Path("."), conf);
ComplementaryNaiveBayesClassifier classifier = new ComplementaryNaiveBayesClassifier(model);

// サッカー:303, ゴール:302, ナビスコ:366
Vector vec = new RandomAccessSparseVector(1343);
vec.setQuick(303, 1.0);
vec.setQuick(302, 1.0);
vec.setQuick(366, 1.0);

// 0:baseball, 1:f1, 2:soccer
Vector result = classifier.classifyFull(vec);
for (int i = 0; i < result.size(); i++)
    System.out.println(i + ", " + result.get(i));

結果

0, 20.55879584431586
1, 22.075497860871426
2, 24.71672003638534

予想通り2(soccer)が一番スコアが高くなった。