原文出处:原文链接
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 さんです。楽しみですね!