Algorithm::SVMLight をインストールして使ってみようB!

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 さんです。楽しみですね!