去年、オンライン機械学習本(クマ本)を読んで線形分類器を実装する記事を書いたり、それらのアルゴリズムをまとめてcl-online-learningというライブラリを作ってLispmeetupで紹介したりした。

その後放置していたのだが、最近になってもはや使わないようなアルゴリズムは削除したり、疎ベクトルへの対応や、学習器のCLOSオブジェクトを単なる構造体にするなどの大きな変更をした。このあたりで一度ちゃんと紹介記事を書いておこうかと思う。

cl-online-learningの特徴は、

インストール local-projectsディレクトリにソースを展開する。 cd ~/quicklisp/local-projects/ git clone https://github.com/masatoi/cl-online-learning.git あるいは、Roswellがインストールされているなら単に ros install masatoi/cl-online-learning

データの読み込み 1つのデータはラベル(+1/-1)と入力ベクトルのペア(cons)で、データのシーケンスがデータセットとなる。 libsvm datasetsの形式のファイルからデータセットを作るには、read-data関数が使える。とりあえずデータはlibsvm datasetsの二値分類データからa1aを使うことにする。 ( defpackage :clol-user ( :use :cl :cl-online-learning :cl-online-learning.utils :cl-online-learning.vector )) ( in-package :clol-user ) ( defparameter a1a-dim 123 ) ( defparameter a1a-train ( read-data "/path/to/a1a" a1a-dim )) ( defparameter a1a-test ( read-data "/path/to/a1a.t" a1a-dim )) ( car a1a-train )

モデル定義 学習器のモデルは単なる構造体で、make-系関数で生成できる。その際いずれもデータの次元数を必要とする。その他にAROWは1個、SCWは2個のメタパラメータを指定する必要がある。パーセプトロン、AROW、SCWのモデルをまとめて定義すると、 ( defparameter perceptron-learner ( make-perceptron a1a-dim )) ( defparameter arow-learner ( make-arow a1a-dim 10d0 )) ( defparameter scw-learner ( make-scw a1a-dim 0.9d0 0.1d0 ))

訓練 データ1個を学習するには各学習器のupdate関数を使う。AROWならarow-update関数。これにデータの入力ベクトルとラベルを与えることで、arow-learnerが破壊的に更新される。 ( arow-update arow-learner ( cdar a1a-train ) ( caar a1a-train )) これをデータセット全体に対して行うのがtrain関数である。 ( train arow-learner a1a-train )

予測 こうして学習したモデルを使って、ある入力ベクトルに対して予測を立てるには各学習器のpredict関数を使う。AROWならarow-predict関数。 ( arow-predict arow-learner ( cdar a1a-test )) 正解の値(caar a1a-test)が-1.0d0なのでここは外してしまっている。 これをテストデータ全体に対して行ない、正答率を返すのがtest関数である。 (test arow-learner a1a-test) ;; Accuracy: 84.44244%, Correct: 26140, Total: 30956 となって84%弱の精度が出ていることが分かる。

マルチクラス分類 データの読み込み (MNIST) マルチクラス分類ではデータのラベルが+1/-1ではなく、0以上の整数になる。例えばlibsvm datasetsからMNISTのデータを落としてきて読み込んでみる。読み込みはread-data関数にmulticlass-pキーワードオプションをつけて呼び出す。 ( defparameter mnist-dim 780 ) ( defparameter mnist-train ( read-data "/home/wiz/tmp/mnist.scale" mnist-dim :multiclass-p t )) ( defparameter mnist-test ( read-data "/home/wiz/tmp/mnist.scale.t" mnist-dim :multiclass-p t )) ( dolist ( datum mnist-train ) ( incf ( car datum ))) ( dolist ( datum mnist-test ) ( incf ( car datum ))) ( car mnist-train ) モデル定義 マルチクラス分類は二値分類器の組み合わせで実現する。組み合せ方には色々あるが、cl-online-learningではone-vs-oneとone-vs-restを用意している。一般にone-vs-oneの方が精度が高いが、クラス数の二乗に比例する二値分類器が必要になる。一方のone-vs-restはクラス数に比例する。 例えばone-vs-oneで、二値分類器としてAROWを用いる場合の定義はこうなる。 ( defparameter mnist-arow ( make-one-vs-one mnist-dim 10 ' arow 10d0 )) この構造体に対しても二値分類のときと同じくone-vs-one-update、one-vs-one-predict関数でデータを一つずつ処理できるし、train、test関数でデータセットをまとめて処理できる。 訓練、予測 データセットを8周訓練する時間を計測し、テストを行うコードは以下のようになる。 ( time ( loop repeat 8 do ( train mnist-arow mnist-train ))) ( test mnist-arow mnist-test ) liblinearの場合 高速な線形分類器とされるliblinearで同じデータを学習してみる。 wiz@prime:~/tmp$ time liblinear-train -q mnist.scale mnist.model real 2m26.804s user 2m26.668s sys 0m0.312s wiz@prime:~/tmp$ liblinear-predict mnist.scale.t mnist.model mnist.out Accuracy = 91.69% (9169/10000) こちらはデータの読み込みなども含めた時間なのでフェアな比較ではないが、大まかにいってcl-online-learningの方が大幅に速いといえる。また精度もcl-online-learning(AROW + one-vs-one)の方が良い。ちなみにliblinearのマルチクラス分類はone-vs-restを使っているらしい。

疎なデータの分類 a1aのデータを見ると気付くのは、ほとんどの要素が0の疎（スパース）なデータであるということだ。例えば「単語が文書に出現する回数」のような特徴量は高次元かつスパースになる。これをそのまま扱うと空間計算量も時間計算量も膨れ上がってしまうので、このようなデータではデータの次元数の長さのベクタを用意するのではなく、非零値のインデックスと値のペアだけを保持しておけばいい。 cl-online-learning.vectorパッケージに定義されているsparse-vector構造体がそれで、インデックスのベクタと値のベクタをスロットに持つ。 ( make-sparse-vector ( make-array 3 :element-type ' fixnum :initial-contents '( 3 5 10 )) ( make-array 3 :element-type ' double-float :initial-contents '( 10d0 20d0 30d0 ))) 疎ベクトルの形でデータセットを読み込むにはread-data関数にsparse-pキーワードオプションをつけて呼び出す。試しに、1355191次元という超高次元のデータセットnews20.binaryを読み込んでみる。1つのデータはラベルとsparse-vector構造体のペアになっていることが分かる。 ( defparameter news20.binary-dim 1355191 ) ( defparameter news20.binary ( read-data "/home/wiz/datasets/news20.binary" news20.binary-dim :sparse-p t )) ( car news20.binary ) データ中の非零値の数をヒストグラムにしてみるとこうなる。 ( ql:quickload :clgplot ) ( clgp:plot-histogram ( mapcar ( lambda ( d ) ( clol.vector::sparse-vector-length ( cdr d ))) news20.binary ) 200 :x-range '( 0 3000 ))

1355191次元といってもほとんどのデータが2000次元以下なので疎なデータであることが分かる。 これを学習するためには、二値分類器としてarowの代わりにsparse-arowを使う。同様にパーセプトロンやSCWにもスパース版がある。 ( defparameter news20.binary.arow ( make-sparse-arow news20.binary-dim 10d0 )) ( time ( loop repeat 20 do ( train news20.binary.arow news20.binary ))) ( test news20.binary.arow news20.binary ) AROW++の場合 同じことをC++によるAROW実装のAROW++を使ってやってみる。 wiz@prime:~/datasets$ arow_learn -i 20 news20.binary news20.binary.model.arow Number of features: 1355191 Number of examples: 19996 Number of updates: 37643 Done! Time: 9.0135 sec. wiz@prime:~/datasets$ arow_test news20.binary news20.binary.model.arow Accuracy 99.915% (19979/19996) (Answer, Predict): (t,p):9986 (t,n):9993 (f,p):4 (f,n):13 Done! Time: 2.2762 sec. liblinearの場合 wiz@prime:~/datasets$ time liblinear-train -q news20.binary news20.binary.model real 0m2.800s user 0m2.772s sys 0m0.265s wiz@prime:~/datasets$ liblinear-predict news20.binary news20.binary.model news20.binary.out Accuracy = 99.875% (19971/19996) なおAROW++もliblinearもベクトルの内部表現は疎ベクトルでやっている模様。