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

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 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 ...

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

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

(multi-)term-mode に dirtrack させる zsh の設定

TL;DR .zshrc に以下を書けば良い: # Enable dirtrack on (multi-)term-mode. if [[ " $TERM " = eterm * ]]; then chpwd() { printf '\032/%s\n' " $PWD " } fi 追記 (May 14, 2025): oh-my-zsh を使っていれば emacs プラグインが勝手にやってくれる: plugins = ( emacs ) 仔細 term-mode は Emacs 本体に付属する端末エミュレータである。基本的には Emacs 内でシェルを起動するために使うもので、古い shell-mode よりも端末に近い動きをするので便利なのだが、一つ問題がある。シェル内でディレクトリを移動しても Emacs バッファの PWD がそのままでは追従しない点だ。 こういう追従を Emacs では Directory Tracking (dirtrack) と呼んだりするが、 shell-mode や eshell ではデフォルトで提供しているのに term-mode だけそうではない。 要するにシェル内で cd してもバッファの PWD は開いた時点のもの (基本的には直前にアクティヴだったバッファの PWD を継承する) のままなので、移動したつもりで C-x C-f などをするとパスが違ってアレっとなることになる。 実は term-mode にも dirtrack 機能自体は存在しているのだが、これは シェルがディレクトリ移動を伴うコマンドを実行したときに特定のエスケープシーケンスを含んだ行を印字することで Emacs 側に通知するという仕組み になっている。 Emacs と同じく GNU プロジェクトの成果物である bash は Emacs 内での動作を検出すると自動的にこのような挙動を取るが、zsh は Emacs の事情なんか知ったことではないので手動で設定する必要がある。 まずもって「ディレクトリ移動のコマンドをフックする」必要がある訳だが、zsh の場合これは簡単で cd / pushd / popd のようなディレクトリ...

多分週刊チラシの裏 (Oct 19, 2020 - Feb 26, 2021)

週刊とは言ったが毎週刊とは言ってないという言い訳。 C++ のコンパイルを高速化する小技 ビルドシステムやツールを変更せずともコーディングだけで改善できるコンパイル時間短縮テクニック。 #include を減らす インライン化を明示的に避ける 関数オーバーロードの可視性を制限する 公開シンボルを減らす の 4 本。 歯医者で歯を治したら記憶能力を失った話 歯医者で簡単な治療を受けた日から後、記憶が 90 分しか保持できなくなった英国の軍人の話。まるで「博士の愛した数式」だが実話である。 DRPK で売られていた Sim City っぽいゲームのリバースエンジニアリング 平壌市内のアプリストア (物理) で売られていた Sim City 風ゲームがインストールに失敗してライセンス認証で止まってしまったのでなんとか動かせないものかとリバースエンジニアリングしてみた話。 日本にあっては DPRK のデジタル事情というと 3G セルラーが現役とか国内 Web サイトのリストがポスター一枚に収まるとか何故かコンピュータ将棋の古豪とかの断片的な情報が伝え聞かれる程度だが、近頃は Android タブレットでゲームなどもできるらしい。 国内のインフラ及びエコシステム事情に合わせて元々フリーミアム + アプリ内課金モデルだったものが買い切り 5,000 KPW (< 1 USD) になっているなど、我々が失った自由が我々よりも不自由な (はずだと我々が信じている) 国に残存しているのは皮肉だろうか。 typosquatting は単なる typo じゃ済まない typo を狙って人気のあるドメインやソフトウェアに類似した名前をつける手法 (typosquatting) は人を辟易させるのみならずセキュリティの脅威である。 IQT が 2017 年から 2020 年にかけて Python ライブラリの中央リポジトリである PyPI において行った調査で、メジャーなライブラリに名前を似せたマルウェアが 40 個確認されたとのこと。 その内 16 個が単純なスペルミス狙い (e.g., “urlib3” vs. “urllib3”) で、26 個は正当なパッケージと混同するような名前 (e.g., “nmap-python” vs. “pytho...