改訂 (Sep 15, 2020): 必要のない手順を含んでいたのでサンプルコードと記述を修正しました。
CPAN に Algorithm::LibLinear 0.22 がリリースされました (しました。) 高速な線形 SVM およびロジスティック回帰による複数の機械学習アルゴリズムを実装したライブラリである LIBLINEAR への Perl バインディングです。
利用している LIBLINEAR のバージョンが LIBLINEAR 2.30 から LIBLINEAR 2.41 に上がったことで新しいソルバが追加され、One-class SVM (OC-SVM) による一値分類が利用可能になっています (しました。)
OC-SVM って何
一値分類を SVM でやること。
一値分類って何
ある値が学習したクラスに含まれるか否かを決定する問題。 HBO の「シリコンバレー」に出てきた「ホットドッグ」と「ホットドッグ以外」を識別するアプリが典型。「ホットドッグ以外」の方は犬でも神でも一つの指輪でも何でも含まれるのがミソ。
二値分類の場合正反両者のデータを集める必要があるのに対して、一値分類の学習器は正例データのみしか要求しない (ものが多い。) 主な用途は外れ値検出で、もちろんホットドッグやホットドッグ様のものを検出したりもできる。
使い方
手順自体は他の二値ないし多値分類問題と同じです。つまり、
- 訓練パラメータを決めて
- 訓練データセットで訓練して
- テストデータセットで確度を検証して
- 十分良くなったらモデルを保存する
といういつもの流れ。
訓練パラメータ
use 5.032;
use Algorithm::LibLinear;
my $learner = Algorithm::LibLinear->new(
epsilon => 0.01,
nu => 0.75,
solver => 'ONECLASS_SVM',
);
solver => 'ONECLASS_SVM'
が一値分類用のソルバです。LIBLINEAR の train コマンドで言うところの -s 21
。 OC-SVM の良いところは (ハイパー) パラメータが少ないことで、2個しかありません。epsilon は収束判定に使う指標で、nu は外れ値の見込の割合です。
訓練
use Algorithm::LibLinear::DataSet;
my $data_set = Algorithm::LibLinear::DataSet->load(fh => \*DATA);
my $model = $learner->train(data_set => $data_set);
# a9a training data.
# cf. https://www.csie.ntu.edu.tw/~cjlin/libsvmtools/datasets/binary.html#a9a
__DATA__
-1 3:1 11:1 14:1 19:1 39:1 42:1 55:1 64:1 67:1 73:1 75:1 76:1 80:1 83:1
-1 5:1 7:1 14:1 19:1 39:1 40:1 51:1 63:1 67:1 73:1 74:1 76:1 78:1 83:1
-1 3:1 6:1 17:1 22:1 36:1 41:1 53:1 64:1 67:1 73:1 74:1 76:1 80:1 83:1
-1 5:1 6:1 17:1 21:1 35:1 40:1 53:1 63:1 71:1 73:1 74:1 76:1 80:1 83:1
-1 2:1 6:1 18:1 19:1 39:1 40:1 52:1 61:1 71:1 72:1 74:1 76:1 80:1 95:1
-1 3:1 6:1 18:1 29:1 39:1 40:1 51:1 61:1 67:1 72:1 74:1 76:1 80:1 83:1
-1 4:1 6:1 16:1 26:1 35:1 45:1 49:1 64:1 71:1 72:1 74:1 76:1 78:1 101:1
+1 5:1 7:1 17:1 22:1 36:1 40:1 51:1 63:1 67:1 73:1 74:1 76:1 81:1 83:1
...
確度の検証
やるだけ。Algorithm::LibLinear::Model#predict が返すラベルは訓練データセットの値に関係なく 1 / -1 になります。
my $num_corrects = 0;
my $test_data_set = Algorithm::LibLinear->load(fh => \*DATA);
for my $data ($test_data_set->as_arrayref->@*) {
my $predicted_label = $model->predict(feature => $data->{feature});
++$num_corrects if $data->{label} == $predicted_label;
}
my $test_data_set_size = $test_data_set->size;
say "$num_corrects / $test_data_set_size = ", $num_corrects / $test_data_set_size;
# a9a test data.
# cf. https://www.csie.ntu.edu.tw/~cjlin/libsvmtools/datasets/binary.html#a9a
__DATA__
-1 1:1 6:1 17:1 21:1 35:1 42:1 54:1 62:1 71:1 73:1 74:1 76:1 80:1 83:1
-1 3:1 6:1 14:1 22:1 36:1 40:1 56:1 63:1 67:1 73:1 74:1 76:1 82:1 83:1
+1 2:1 10:1 18:1 24:1 38:1 40:1 59:1 63:1 67:1 73:1 74:1 76:1 80:1 83:1
+1 4:1 6:1 16:1 20:1 37:1 40:1 54:1 63:1 71:1 73:1 75:1 76:1 80:1 83:1
-1 1:1 14:1 20:1 37:1 42:1 62:1 67:1 72:1 74:1 76:1 78:1 83:1
-1 3:1 6:1 17:1 31:1 35:1 42:1 49:1 64:1 67:1 73:1 74:1 76:1 78:1 83:1
-1 2:1 17:1 22:1 36:1 42:1 66:1 71:1 73:1 74:1 76:1 80:1 83:1
+1 5:1 7:1 14:1 23:1 39:1 40:1 52:1 63:1 67:1 73:1 75:1 76:1 78:1 83:1
...
モデルの保存
ラベルの対応も何もないので単に LIBLINEAR モデルを保存すれば十分です。
$model->save(filename => 'path/to/model');
# 復元
$model = Algorithm::LibLinear::Model->load(filename => 'path/to/model');
コード例
ここまで説明されても分からなかったらコピペして使おう:
#!/usr/bin/env perl
use 5.032;
use Algorithm::LibLinear;
use Algorithm::LibLinear::DataSet;
# Expects a9a or similar binary classification data set.
# cf. https://www.csie.ntu.edu.tw/~cjlin/libsvmtools/datasets/binary.html#a9a
my $training_data_file = shift or die "Usage: $0 <training-data-file>";
my $test_data_file = "${training_data_file}.t";
sub negative_rate {
my ($data_set) = @_;
my $num_negatives = grep { $_->{label} == -1 } $data_set->as_arrayref->@*;
$num_negatives / $data_set->size;
}
sub train {
my ($data_file) = @_;
my $training_data =
Algorithm::LibLinear::DataSet->load(filename => $data_file);
my $negative_rate = negative_rate($training_data);
# One-class SVM solver assumes that all the given data are positive instances
# so labels are ignored. We need to filter out negative ones since binary data
# set like a9a contains both.
my $learner = Algorithm::LibLinear->new(
nu => $negative_rate,
solver => 'ONECLASS_SVM',
);
$learner->train(data_set => $training_data);
}
my $model = train($training_data_file);
my $test_data =
Algorithm::LibLinear::DataSet->load(filename => $test_data_file);
my $num_corrects = 0;
for my $data ($test_data->as_arrayref->@*) {
my $predicted_label = $model->predict(feature => $data->{feature});
++$num_corrects if $predicted_label == $data->{label};
}
printf
"Correct: %d/%d; Accuracy: %f%%\n",
$num_corrects,
$test_data->size,
+($num_corrects / $test_data->size * 100);
終わったので自慢
C ライブラリである LIBLINEAR には複数の言語バインディングがありますが、国立台湾大学の開発チームから公式に提供されているのは MATLAB / Octave / Python の三言語版で残りは有志による実装です。
Perl 版である A::LL はそれらの内で最も暇なメンテナを持つ積極的にメンテナンスされている実装であり、LIBLINEAR の各リリース後一ヶ月以内に追従してきました。今回も OC-SVM 機能をサポートした既知の非公式バインディングは A::LL が最初です。
そういう経緯も含めて開発チームのリーダーである Chih-Jen Lin 教授に依頼したところ、公式サイトの紹介中で対応バージョンを “The latest” (最新) と表示してもらうことができました:
これは公式バインディングを除けば php-liblinear に次いで二例目です。php-liblinear は厳密に言えばバインディングではなく別個にインストールされた LIBLINEAR の train / predict コマンドを使用してテキスト分類を行うアプリケーション・ツールキットなので、ライブラリとして LIBLINEAR 自体の機能を提供する非公式バインディングは A::LL が現時点で唯一のものです。
コメント
コメントを投稿