Algorithm::SVMLight をインストールして使ってみよう
overlast(さとうとしのり)です。
僕は普段、自然言語処理技術を活用する仕事に従事しています。
Perl は「アイディアが浮かんでからコードを実行するまでの早さ」や「急で無茶な仕様変更への対応のしやすさ」などが好きで使っています。最初に Perl で実装して、後日速度が求められるようになったら、遅い部分だけ C / C++ で書き直すことが多いです。
Perl の CPAN モジュールの Author の方にはいつもお世話になっております。本当にいつもどうもありがとうございます。
さて今回は、教師あり学習を用いる識別手法の一つである Support Vector Machine(以下では SVM と略す) の実装の一つである SVMlight のための Perl モジュールの一つである「Algorithm::SVMLight」のインストール方法をご紹介します。
Support Vector Machine(SVM)はどんなことをしてくれるの?
Support Vector Machine はパターン認識のための手法です。
SVM を分かりやすく説明するのは、思わず放棄したくなるほど困難なことなのですが、頑張ってみます。
たとえば、床にばらまかれた沢山のボールがあるとします。
ボールが落ちている位置に基づいて、すべてのボールを 2 つのバケツのどちらかに分けようと思います。
ボールの分け方には様々な方法があると思うのですが、
SVM による分け方は、ボール分けるときにロープを一直線に床に置いて、
ボールがロープより右にあるか左にあるかでバケツを分けるような分け方です。
大変にいい加減な図ですが、こんな図を思い浮かべてください。
o o o o -------------- o o o o
ロープの置き方は、なんでも良い、というわけではありません。
ボールを分けたときに、ロープの左右にあるボールとロープの距離の合計が最大となるようなロープの置き方をします。
ボールの位置が変わったときでも、このロープの置き方のルールを適用すれば、ボールは迷わずに 2 つのバケツに分けることができます。
ところで、もしもボールが明らかに 2 色にだけ別れているなら、以下のように分かれていても大丈夫な気分がしますね。
o o o o -------------- x x x x
では、ボールがこんな感じでバラまかれていたら、どこにロープを置くのでしょうか。
x o o o x x x o
なんというか、どうやってロープを置いても、まっすぐに置く限りは上手く2つのバケツに分けられなさそうですよね。
でも、もしも上の図が実は2次元の図ではなく3次元の図だったらどうでしょうか。
図を回転してあげたら、ロープをまっすぐ引いてボールを綺麗に分けられるような位置を見つけられるかもしれません。
SVM のすごいところは、これらのボールを何とか分けられるような空間にボールを写像して、なんとか線を引いてしまいます。
x | | o | oo | xx | x | | o
で、たとえば、こんな感じで線を引いてしまうのです。SVM にちょっと興味が出てきましたか?
ほんのりと SVM のことが分かってもらえれば、この説明は成功です。
SVM についてちゃんと知りたい方は、キチンと別の文献を読んで理解をし直してください。
今回ご紹介するモジュール「Algorithm::SVMLight」
今回、ご紹介する「Algorithm::SVMLight」は CPAN にある SVM 向けの Perl モジュールのうち、一番ちゃんと動きそうだから選びました。
でも、多少試行錯誤しないとインストールできなかったのでネタとして丁度良かったです。
インストールできなくて諦めてしまう人も多いかと思いますので、この記事を読んでガンバってみてください。
「Algorithm::SVMLight」を使うと何が嬉しいのか
SVMlightをPerlから扱えると何が嬉しいのかというと、インスタンスの読み込み、学習の実行、モデルの書き出し・読み込み、分類結果の取得などの動作を、Perl で書いたアプリケーションの任意の位置で実行できる点にあるのかな、と思います。
分類対象のデータを素性エンコーディングして、即、SVMlight で分類しようと思うようなときには、SVMlight が Perl から扱えると嬉しいです。分類結果を出力したあと、改めて素性エンコードする前のデータに結果に適用しようとすると、面倒くさいことが多いです。
Algorithm::SVMLight の作者である Ken Williams は、このモジュールにファイルからの分類対象データの読み込み処理を書いていません。「分類対象のデータに関しては Perl で扱え!」ということですかね。。。
ちなみに、学習データを SVM の学習用の素性にエンコードする処理に関しては SVMlight とは無関係に書けます。
でも、このエンコーディング処理は複雑になりがちなので、もろもろ柔軟な Perl はかなり重宝します。
SVM light と、Algorithm::SVMLight のインストール
SVMlightの最新のソースコードは以下のURLからダウンロードできます。
今回、利用したソースコードは以下から取得しました。
その後は、以下のようにしてインストールしました。適時 sudo してください。
% wget http://search.cpan.org/CPAN/authors/id/K/KW/KWILLIAMS/Algorithm-SVMLight-0.09.tar.gz % tar xfvz Algorithm-SVMLight-0.09.tar.gz % mkdir ./svm_light % cd ./svm_light % wget http://download.joachims.org/svm_light/current/svm_light.tar.gz % tar xfvz svm_light.tar.gz % patch -p1 < ../Algorithm-SVMLight-0.09/SVMLight.patch % make all % mkdir /usr/local/bin/svm_light/ % cp ./svm_learn /usr/local/bin/svm_light/ % cp ./svm_classify /usr/local/bin/svm_light/ % mkdir /usr/local/include/svm_light/ % cp ./svm_learn.h /usr/local/include/svm_light/ % cp ./svm_common.h /usr/local/include/svm_light/ % cp ./libsvmlight.a /usr/local/lib % cp ./libsvmlight.so /usr/local/lib % ldconfig % cd ../Algorithm-SVMLight-0.09/
バイナリファイルの名前を変えてコピーしているのは、変更前のファイル名が TinySVM と同じだったからです。
でも、このままだと Algorithm::SVMLight のコンパイル中に、SVMlight のヘッダファイルが見つからなくてエラーが出てしまいました。
仕方がないので、エディタで Algorithm-SVMLight-0.09/lib/Algorithm/SVMLight.c の 30・31 行目を編集し
#include "svm_common.h" #include "svm_learn.h"
に、ヘッダの絶対パスを追記して、
#include "/usr/local/include/svm_light/svm_common.h" #include "/usr/local/include/svm_light/svm_learn.h"
にしました。
あとは、以下を実行するだけでした。
% perl Makefile.PL % perl Build % perl Build test % perl Build install
これで SVMlight のインストールが終わり、Perl スクリプトからは Algorithm::SVMLight が使えます。
SVMlight の素性エンコード
一番面倒なのが、データを素性形式にエンコード部分です。
素性の文字列表現と番号を対応づけるコードは、一回書くと使い回しが効いて楽です。
例えば以下のように実行できるエンコーダーを書いてしまって、
% perl feature_encoder.pl "入力の学習データファイルのパス" "出力の素性エンコード済みデータファイルのパス"
その後で、素性の作り方を工夫してみるのはどうでしょうか。
feature_encoder.plの例
#!/usr/bin/perl use strict; use warnings; use utf8; use Encode; use TokyoCabinet; use MeCab; # MeCabオブジェクト my @mecab_opt = (); my $mecab = new MeCab::Tagger(join " ", @mecab_opt); my $inputdata = $ARGV[0]; my $outputdata = $ARGV[1]; my ($in, $out); # TokyoCabinetの初期化 my $tchdb_file_path = $FindBin::Bin."/../feature_num.tch"; my $hdb = TokyoCabinet::HDB->new(); $hdb->tune(2000000); $hdb->open($tchdb_file_path, $hdb->OWRITER | $hdb->OCREAT | $hdb->OREADER); # 素性番号カウンタ my $gloval_counter = 1; # 素性番号カウンタの値をHDBから取り出すためのキー my $gkey = "GLOBALCOUNTER"; # 素性番号カウンタの値を取得 my $tmp_gloval_counter = $hdb->get($gkey); if (defined $tmp_gloval_counter) { $gloval_counter = $tmp_gloval_counter; } else { # 取得できなかったら初期値「1」を登録 $hdb->put($gkey, 1); $gloval_counter = 1; } open ($in, "< $inputdata"); open ($out, ">> $outputdata"); # 素性の書き出し while(my $line = <$in>){ chomp $line; next unless ($line); # MeCabの結果を取得する my @mecab_arr = @{get_mecab_result_arr($line)}; next unless (@mecab_arr); my $count = 0; # ラベル my $label = 0; # 出力用に素性番号を突っ込む配列 my @feature_arr = (); # 素性の材料を得る my $entry = $mecab_arr[$i]; my $key = $entry->[0]; my $pos = $entry->[1]; my $keypos = "$key:-:$pos"; # 素性番号の取得と登録 my @keyarr = ($key, $pos, $keypos); foreach my $k (@keyarr) { # 素性番号を取得してみる my $tmp_feature_num = $hdb->get($k); my $feature_num = 0; if (defined $tmp_feature_num) { # 取得できたら、そのまま出力用の配列に突っ込む push @feature_arr, "$tmp_feature_num"; } else { # 取得できなかったら、素性番号カウンタの値を取得 $feature_num = $gloval_counter; # カウンタの値を、キーに対する素性番号にして登録 $hdb->put($k, $feature_num); # 素性番号カウンタ++ $gloval_counter++; # 素性番号カウンタのバックアップ $hdb->put($gkey, $gloval_counter); push @feature_arr, "$feature_num"; } } # ソート、ユニークする。 @feature_arr = sort {$a < = > $b} @feature_arr; my $x = '-'; my @uniq_arr = grep( $_ ne $x && ($x = $_), @feature_arr); # この例では、最後に全ての素性に一様な重みをつけている my $features = join ":0.1 ", @uniq_arr; my $entry = "$label $features:0.1\n"; print $out $entry; } close ($out); close ($in); $hdb->close(); # 1行のテキストを受け取り、MeCabでparseしたあと、結果を配列に入れて返す。 sub get_mecab_result_arr { my ($line) = @_; my $parsed = $mecab->parse($line); $parsed = decode_utf8($parsed) unless utf8::is_utf8($parsed); my @pos_arr = split('\n', $parsed); my @result = (); if(@pos_arr){ my $i = 0; foreach my $pos (@pos_arr){ my @info_arr = split(/\t/, $pos); my @mecab_arr = split(/\,/, $info_arr[1]); my @mec = ($info_arr[0], @mecab_arr); $result[$i] = \@mec; $i++; } } return \@result; }
このファイルの中には TokyoCabinetを使った素性番号管理と、MeCab を使った形態素解析の処理が含まれています。
TokyoCabinetのHDBに、現在の素性番号の最大値を格納してあるので、追加も楽にできます。
SVMlight の素性エンコード時の注意点
SVMlight に素性エンコードしたインスタンスを読み込ませるには、以下のような注意が必要です。
- インスタンス中の素性番号は昇順に並べること
- 良い例:-1 10:0.1 20:0.2 30:0.3
- 悪い例:-1 10:0.1 40:0.4 30:0.3
- インスタンス中の素性番号はユニークにすること
- 良い例:-1 10:0.1 20:0.2 30:0.3
- 悪い例:-1 10:0.1 20:0.2 20:0.3
- 学習データ以外の、分類対象のデータをエンコードする場合にもラベルを付与する
Algorithm::SVMLight を使ったモデル構築
Algorithm::SVMLight を使うと、モデルの構築は例えば以下のように書けます。
% perl ./make_model.pl "入力のインスタンスファイルのパス" "出力のモデルファイルのパス"
素性にエンコード済みなインスタンスファイルを用意できれば、上記を実行してあげるとモデルが得られます。
make_model.pl の例
#!/usr/bin/perl use strict; use warnings; use utf8; # オブジェクト作成 use Algorithm::SVMLight; my $svm = new Algorithm::SVMLight; # 入出力ファイルのパス my $inputdata = $ARGV[0]; my $outputdata = $ARGV[1]; # インスタンスの読み込み $svm->read_instances($inputdata); # 学習開始 $svm->train(); # モデルの書き出し $svm->write_model($outputdata);
Algorithm::SVMLight があれば、モデル構築以外の処理も Perl で手軽に書けます。
まとめ
今回は SVMLight の Perl モジュールである Algorithm::SVMLight をインストールしました。
SVM は使いどころを間違えなければ大変に便利です。SVM を扱った学術論文は多数あるので、そちらもご覧下さい。
さてさて、明日は pixiv のエンジニアである kamipo さんです。楽しみですね!