スキップしてメイン コンテンツに移動

LIBLINEAR 2.41 で One-class SVM が使えるようになったので Perl から触ってみよう

改訂 (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 の「シリコンバレー」に出てきた「ホットドッグ」と「ホットドッグ以外」を識別するアプリが典型。「ホットドッグ以外」の方は犬でも神でも一つの指輪でも何でも含まれるのがミソ。

二値分類の場合正反両者のデータを集める必要があるのに対して、一値分類の学習器は正例データのみしか要求しない (ものが多い。) 主な用途は外れ値検出で、もちろんホットドッグやホットドッグ様のものを検出したりもできる。

使い方

手順自体は他の二値ないし多値分類問題と同じです。つまり、

  1. 訓練パラメータを決めて
  2. 訓練データセットで訓練して
  3. テストデータセットで確度を検証して
  4. 十分良くなったらモデルを保存する

といういつもの流れ。

訓練パラメータ

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” (最新) と表示してもらうことができました:

Interfaces to LIBLINEAR

これは公式バインディングを除けば php-liblinear に次いで二例目です。php-liblinear は厳密に言えばバインディングではなく別個にインストールされた LIBLINEAR の train / predict コマンドを使用してテキスト分類を行うアプリケーション・ツールキットなので、ライブラリとして LIBLINEAR 自体の機能を提供する非公式バインディングは A::LL が現時点で唯一のものです。

コメント

このブログの人気の投稿

京大テキストコーパスのパーサを書いた

要旨 CaboCha やなんかの出力形式であるところの京大テキストコーパス形式のパーサモジュールを Perl で書いたので紹介します。 Github Tarball on Github Ppages これを使うと例えば CaboCha の出力した係り受け関係を Perl のオブジェクトグラフとして取得できます。 使用例 単なる文節区切りの例。 #!/usr/bin/env perl use v5.18; use utf8; use IPC::Open3; use Parse::KyotoUniversityTextCorpus; use Parse::KyotoUniversityTextCorpus::MorphemeParser::MeCab; use Symbol qw//; my ($in, $out, $err); my $pid; BEGIN { ($in, $out, $err) = (Symbol::gensym, Symbol::gensym, Symbol::gensym); $pid = open3($in, $out, $err, cabocha => '-f1'); } END { close $out; close $err; waitpid $pid => 0 if defined $pid; } binmode STDOUT, ':encoding(utf8)'; binmode $in, ':encoding(utf8)'; binmode $out, ':encoding(utf8)'; my $parser = Parse::KyotoUniversityTextCorpus->new( morpheme_parser => Parse::KyotoUniversityTextCorpus::MorphemeParser::MeCab->new, ); say $in '星から出るのに、その子は渡り鳥を使ったんだと思う。'; say $in '出る日の朝、自分の星の片付けをした。'; close $in; my $sentence...

Perl 7 より先に Perl 5.34 が出るぞという話

Perl 5 の次期バージョンとして一部後方互換でない変更 (主に間接オブジェクト記法の削除とベストプラクティスのデフォルトでの有効化) を含んだメジャーバージョンアップである Perl 7 がアナウンスされたのは昨年の 6 月 のことだったが、その前に Perl 5 の次期周期リリースである Perl 5.34 が 5 月にリリース予定 である。 現在開発版は Perl 5.33.8 がリリースされておりユーザから見える変更は凍結、4 月下旬の 5.33.9 で全コードが凍結され 5 月下旬に 5.34.0 としてリリース予定とのこと。 そういうわけで事前に新機能の予習をしておく。 8進数数値リテラルの新構文 見た瞬間「マジかよ」と口に出た。これまで Perl はプレフィクス 0 がついた数値リテラルを8進数と見做してきたが、プレフィクスに 0o (zero, small o) も使えるようになる。 もちろんこれは2進数リテラルの 0b や 16進数リテラルの 0x との一貫性のためである。リテラルと同じ解釈で文字列を数値に変換する組み込み関数 oct も` 新構文を解するようになる。 昨今無数の言語に取り入れられているリテラル記法ではあるが、この記法の問題は o (small o) と 0 (zero) の区別が難しいことで、より悪いことに大文字も合法である: 0O755 Try / Catch 構文 Perl 5 のリリース以来 30 年ほど待たれた実験的「新機能」である。 Perl 5 における例外処理が特別な構文でなかったのは予約語を増やさない配慮だったはずだが、TryCatch とか Try::Tiny のようなモジュールが氾濫して当初の意図が無意味になったというのもあるかも知れない。 use feature qw/ try / ; no warnings qw/ experimental::try / ; try { failable_operation(); } catch ( $e ) { recover_from_error( $e ); } Raku (former Perl 6) だと CATCH (大文字なことに注意) ブロックが自分の宣言されたスコープ内で投げられた例外を捕らえる...

C の時間操作関数は tm 構造体の BSD 拡張を無視するという話

久しぶりに C++ (as better C) で真面目なプログラムを書いていて引っかかったので備忘録。 「拡張なんだから標準関数の挙動に影響するわけねえだろ」という常識人は読む必要はない。 要旨 time_t の表現は環境依存 サポートしている時刻は UTC とプロセスグローバルなシステム時刻 (local time) のみで、任意のタイムゾーン間の時刻変換を行う標準的な方法はない BSD / GNU libc は tm 構造体にタイムゾーン情報を含むが、tm -> time_t の変換 ( timegm / mktime ) においてその情報は無視される 事前知識 C 標準ライブラリにおいて時刻の操作に関係するものは time.h (C++ では ctime) ヘッダに定義されている。ここで時刻を表現するデータ型は2つある: time_t と tm である。time_t が第一義的な型であり、それを人間が扱い易いように分解した副次的な構造体が tm という関係になっている。なので標準ライブラリには現在時刻を time_t として取得する関数 ( time_t time(time_t *) ) が先ずあり、そこから time_t と tm を相互に変換する関数が定義されている。 ここで time_t の定義は処理系依存である。C / C++ 標準はそれが算術型であることを求めているのみで (C11 からは実数型に厳格化された)、その実体は任意である。POSIX においては UNIX epoch (1970-01-01T00:00:00Z) からのうるう秒を除いた経過秒数であることが保証されており Linux や BSD の子孫も同様だが、この事実に依存するのは移植性のある方法ではない。 一方で tm は構造体であり、最低限必要なデータメンバが規定されている: int tm_year : 1900 年からの年数 int tm_mon : 月 (0-based; 即ち [0, 11]) int tm_mday : 月初からの日数 (1-based) int tm_hour : 時 (Military clock; 即ち [0, 23]) int tm_min : 分 int tm_sec : 秒 (うるう秒を含み得るので [0...

去る6月に Perl 5.32.0 がリリースされたので差分を把握するために perldelta を読んだ件

要旨 Perl 5 メジャーバージョンアップの季節がやって来たのでまともな Perl プログラマの嗜みとして perldelta を読んだ。 今回は有り体に言えばルーティン的なリリースで、言語コアの拡張は他言語にも見られる構文が実験的に入ったくらいで大きな変化はない。新機能は RegExp の拡充が主である。 比較的重要と思われる変更点を抜粋する。 新機能 isa 演算子 実験的機能。Python とか Java における isinstance とか instanceof 。 これまでも UNIVERSAL::isa があったが、これはメソッドなのでレシーバにオブジェクトでもクラスでもない値 (i.e., 未定義値 / bless されていないリファレンス) を置くと実行時エラーが起きるのが問題だった: package Foo { use Moo; } package Bar { use Moo; extends ' Foo ' ; } package Baz { use Moo; } use feature qw/ say / ; sub do_something_with_foo_or_return_undef { my ( $foo ) = @_ ; # Returns safely if the argument isn't an expected instance, in mind. return unless $foo -> isa ( ' Foo ' ); ...; } # OK. do_something_with_foo(Bar->new); # |undef| is expected in mind, but actually error will be thrown. do_something_with_foo( undef ); これを避けるために今までは Scalar::Util::blessed を併用したりしていたわけだが、 isa 演算子は左辺が何であっても意味のある値を返すのでよりシンプルになる: # True +( bless +{} ...

Perl のサブルーチンシグネチャ早見表

Perl のサブルーチン引数といえば実引数への参照を保持する特殊配列 @_ を手続き的に分解するのが長らくの伝統だった。これはシェルの特殊変数 $@ に由来する意味論で、おそらく JavaScript の arguments 変数にも影響を与えている。 すべての Perl サブルーチンはプロトタイプ宣言がない限りリスト演算子なので、この流儀は一種合理的でもあるのだが、実用的にそれで良いかというとまったくそうではないという問題があった; 結局大多数のサブルーチンは定数個の引数を取るので、それを参照する形式的パラメータが宣言できる方が都合が良いのである。 そういうわけで実験的に導入されたサブルーチンシグネチャ機能により形式的パラメータが宣言できるようになったのは Perl 5.20 からである。その後 Perl 5.28 において出現位置がサブルーチン属性の後に移動したことを除けば Perl 5.34 リリース前夜の今まで基本的に変わっておらず、未だに実験的機能のままである。 おまじない シグネチャは前方互換性を持たない (構文的にプロトタイプと衝突している) 実験的機能なのでデフォルトでは無効になっている。 そのため明示的にプラグマで利用を宣言しなければならない: use feature qw/signatures/; no warnings qw/experimental::signatures/; どの途みんな say 関数のために使うので feature プラグマは問題ないだろう。実験的機能を断りなしに使うと怒られるので、 no warnings で確信犯であることをアピールする必要がある。 これでプラグマのスコープにおいてサブルーチンシグネチャ (と :prototype 属性; 後述) が利用可能になり、 従来のプロトタイプ構文が無効になる。 使い方 対訳を載せておく。シグネチャの方は実行時に引数チェックを行うので厳密には等価でないことに注意: # Old School use feature qw/signatures/ 1 sub f { my ($x) = @_; ... } sub f($x) { ... } 2 sub f { my ($x, undef, $y) = @_...