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

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 が現時点で唯一のものです。

コメント

このブログの人気の投稿

Perl の新 class 構文を使ってみる

Perl 5 のオブジェクト指向機能は基本的には Python の影響を受けたものだが、データを名前空間 (package) に bless する機構だけで Perl 4 以来の名前空間とサブルーチンをそのままクラスとメソッドに転換し第一級のオブジェクト指向システムとした言語設計は驚嘆に価する。 実際この言語のオブジェクトシステムは動的型付言語のオブジェクト指向プログラミングに要求されるおよそあらゆる機能を暗にサポートしており、CPAN には Moose を筆頭とした屋下屋オブジェクトシステムが複数存在しているがその多くは Pure Perl ライブラリである。つまり「やろうと思えば全部手書きで実現できる」わけである。 そういうわけで Perl のオブジェクト指向プログラミングサポートは機能面では (静的型検査の不在という現代的には極めて重大な欠如を除けば) 申し分ないのだが、しかし Moose その他の存在が示しているように一つ明らかな欠点がある。記述の冗長さだ。 コンストラクタを含むあらゆるメソッドは第一引数としてレシーバを受ける単なるサブルーチンとして明示的に書く必要があるし、オブジェクトのインスタンス変数 (a.k.a. プロパティ / データメンバ) は bless されたデータに直接的ないし間接的に プログラマ定義の方法 で格納されるためアクセス手段は実装依存である。これはカプセル化の観点からは望ましい性質だが、他者の書いたクラスを継承するときに問題となる。ある日データ表現を変更した親クラスがリリースされると突然自分の書いた子クラスが実行時エラーを起こすようになるわけだ。 そうならないためにはインスタンス変数へのアクセスに (protected な) アクセサを使う必要があるのだが、そのためには親クラスが明示的にそれらを提供している必要があるし、そもそも Perl にはメソッドのアクセス修飾子というものがないので完全な制御を与えるならばオブジェクトの内部状態がすべて public になってしまう。 そのような事情もあり、特にパフォーマンスが問題にならないようなアプリケーションコードでは Moose のようなリッチな語彙を提供するオブジェクトシステムを使うことが 公式のチュートリアルでも推奨 されてきた。Perl コアのオブジェクトシステムの改良は...

Perl 5 to 6 - コンテキスト

2011-02-27: コメント欄で既に改訂された仕様の指摘がありました ので一部補足しました。 id:uasi に感謝します。 これはMoritz Lenz氏のWebサイト Perlgeek.de で公開されているブログ記事 "Perl 5 to 6" Lesson 06 - Contexts の日本語訳です。 原文は Creative Commons Attribution 3.0 Germany に基づいて公開されています。 本エントリには Creative Commons Attribution 3.0 Unported を適用します。 Original text: Copyright© 2008-2010 Moritz Lenz Japanese translation: Copyright© 2011 SATOH Koichi NAME "Perl 5 to 6" Lesson 06 - コンテキスト SYNOPSIS my @a = <a b c> my $x = @a; say $x[2]; # c say (~2).WHAT # Str() say +@a; # 3 if @a < 10 { say "short array"; } DESCRIPTION 次のように書いたとき、 $x = @a Perl5では $x は @a より少ない情報—— @a の要素数だけ——しか持ちません。 すべての情報を保存しておくためには明示的にリファレンスを取る必要があります: $x = \@a Perl6ではこれらは反対になります: デフォルトでは何も失うことなく、スカラ変数は配列を単に格納します。 これは一般要素コンテキスト(Perl5で scalar と呼ばれていたもの)及びより特化された数値、整数、文字列コンテキストの導入によって可能となりました。無効コンテキストとリストコンテキストは変更されていません。 特別な構文でコンテキストを強制できます。 構文 コンテキスト ~stuff 文字列 ?stuff 真理値 +stuff ...

去る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 5 to 6 - 正規表現(またの名をルール)

これはMoritz Lenz氏のWebサイト Perlgeek.de で公開されているブログ記事 "Perl 5 to 6" Lesson 07 - Regexes (also called "rules") の日本語訳です。 原文は Creative Commons Attribution 3.0 Germany に基づいて公開されています。 本エントリには Creative Commons Attribution 3.0 Unported を適用します。 Original text: Copyright© 2008-2010 Moritz Lenz Japanese translation: Copyright© 2011 SATOH Koichi NAME "Perl 5 to 6" Lesson 07 - 正規表現(またの名をルール) SYNOPSIS grammar URL { token TOP { <schema> '://' [<ip> | <hostname> ] [ ':' <port>]? '/' <path>? } token byte { (\d**{1..3}) <?{ $0 < 256 }> } token ip { <byte> [\. <byte> ] ** 3 } token schema { \w+ } token hostname { (\w+) ( \. \w+ )* } token port { \d+ } token path { <[ a..z A..Z 0..9 \-_.!~*'():@&=+$,/ ]>+ } } my $match = URL.parse('http:/...