要旨
Algorithm::SVM
は極めて有用だけど API がなんか変なので注意が必要。
詳説
CPAN に Algorithm::SVM
1 というモジュールがあります。これ Support Vector Machine (SVM)2 を提供する LIBSVM3 という有名なライブラリの Perl バインディングなのですが、なんか API に癖があるので注意点を解説します。
まず使い方を簡単に紹介します:
use strict;
use warnings;
use Algorithm::SVM;
my @data_set;
while (<DATA>) {
chomp;
my ($label, $vector) = split /:\s+/, $_, 2;
my @vector = split /,\s+/, $vector;
my $data = Algorithm::SVM::DataSet->new(
DataSet => \@vector,
Label => $label,
);
push @data_set, $data;
}
# 本当はパラメータ調整が要るけど省略。全部デフォルトなのでガウスカーネル利用の C-SVC になる。
my $svm = Algorithm::SVM->new;
# 分類器を訓練する。
$svm->train(@data_set);
# ラベル1に分類されるべき未知のデータ。
my $test_data = Algorithm::SVM::DataSet->new(
DataSet => [ 4.6, 3.2, 1.4, 0.2 ],
# ラベルは未知なので仮に 0 とする。単に無視されるので -1 でも 65536 でも 42 でも良い。
Label => 0,
);
# 未知データを分類。1 が返るはず。
my $label = $svm->predict($test_data);
print "$label\n";
# Iris Data Set (http://archive.ics.uci.edu/ml/datasets/Iris) より一部抜粋の上形式を変更。
# <label>: <vector elm1>, <vector elm2>, ...
__DATA__
1: 5.1, 3.5, 1.4, 0.2
1: 4.9, 3.0, 1.4, 0.2
2: 7.0, 3.2, 4.7, 1.4
2: 6.4, 3.2, 4.5, 1.5
3: 6.3, 3.3, 6.0, 2.5
3: 5.8, 2.7, 5.1, 1.9
...
DataSet がデータセットじゃない
SVM で訓練・分類されるべき (正解ラベル付き) ベクトルを表現するために Algorithm::SVM::DataSet
というクラスが用意されていますが、このクラスが表現するのは1個のベクトルです。つまりデータセットじゃなくてデータです。Algorithm::SVM->train
メソッドで訓練するときにはデータセットとして Algorithm::SVM::DataSet
の配列を渡す必要があります。
分類時にもラベルが必要
未知データを分類する Algorithm::SVM->predict
メソッドのパラメータは Algorithm::SVM::DataSet
オブジェクトです。Algorithm::SVM::DataSet->new
は名前付きパラメータとしてベクトル (DataSet
) と正解ラベル (Label
) を取りますが、正解ラベルは必須パラメータです。
つまりラベルが未知のデータに対してもラベルを与えてやらなければなりません。割と意味不明ですが、predict
ではラベルは単に無視されるのでダミーのラベルを与えてやれば良いです。
疎ベクトルの与え方
Algorithm::SVM::DataSet->new
の DataSet
パラメータは ArrayRef を取ります。ところで問題によってはほとんどの成分が 0 である (i.e., 疎である) ようなベクトルを扱う場合があり、このような問題のデータを配列で表現するとメモリの無駄です。
例えば1万次元ベクトルの 1123 番目と 5813 番目の要素だけが 1 のようなベクトルを表現する場合、[ (0) x 1122, 1, (0) x 4689, 1, (0) x 4187 ]
という具合になってほとんど 0 です。もし HashRef で表現できるなら +{ 1123 => 1, 5813 => 1 }
といった感じになってより簡潔かつ省メモリです。
実際 LIBSVM の内部ではベクトルは連想リストとして表現されていて、ArrayRef でしか受けつけないのはバインディングのコンストラクタの都合です。疎ベクトルのつもりで 0 だらけの ArrayRef をコンストラクタに渡すと、値 0 の無駄なデータで連想リストが伸びて、メモリ使用量だけでなく計算量も増大します。
これを避けるためには成分をコンストラクタから与えず、Algorithm::SVM::DataSet->attribute
を使用します。このメソッドはベクトルの成分と値を併せて指定することで非零成分だけを連想リストに追加できます:
sub sparse_data {
my ($sparse_vector) = @_;
my $data = Algorithm::SVM::DataSet->new(Label => 0);
# 番号が若い成分を追加すると挿入ソートが走るので若い順に追加していく方が速い。
for my $index (sort { $a <=> $b } keys %$sparse_vector) {
$data->attribute($index => $sparse_vector->{$index});
}
return $data;
}
my $sparse_data = sparse_data(+{ 1123 => 1, 5813 => 1 });
宣伝
カーネル関数を使わない線形 SVM を利用したい場合、LIBLINEAR ベースの拙作 Algorithm::LibLinear4 の方が高速です。API もこっちの方が明解です (当社比)
コメント
コメントを投稿