洋食の日記

「だ・である」調ではなく「です・ます」調で書きはじめれば良かったなと後悔してる人のブログです

SVMKitにFactorization Machineによる分類器を追加した

はじめに

SVMKitの開発は、何かしら毎月バージョンアップしようという思いで進めており、無事に2月も0.2.4をリリースすることができた。

svmkit | RubyGems.org | your community gem host

SVMKitにFactorization Machine(FM)による分類器を追加した。FMは、一般的な線形分類器に、特徴ベクトル間の相互関係を(低次元な)潜在ベクトルの内積に因子分解することで捉える項を足したような形となる(と一文で説明するのは難しいので詳しくは元論文を参照してください。そんなに難しい論文ではないです)。 FMは、スパースな特徴ベクトルに効果的とされ、一般に推薦などに使われるが、普通に分類器として使うこともできる。 実装したのは、確率的勾配降下法(Stochastic Gradient Descent, SGD)による、Hinge lossなFMである。 SGDによる各パラメータの最適化部分は、論文に掲載されているものから改良して、Mini-Batchなものにした。

その他に、SVMKit 0.2.4では、評価尺度を計算するEvaluatorモジュールを追加した。これにより、評価尺度の計算を分離することができ、これまでAccuracyの計算のみだったが、Precision、Recall、F値を計算できるようにした(今後もLog-Lossなどを追加していく予定)。

使い方

まずSVMKitをインストールする。線形代数の計算で使用しているNumo::NArrayもインストールされる。

$ gem install svmkit

次に、データを用意する。今回は、LIBSVM DATAから、手書き数字のデータセットであるpendigitsをとってきた。

$ wget https://www.csie.ntu.edu.tw/~cjlin/libsvmtools/datasets/multiclass/pendigits

Factorization Machineの分類精度を、5交差検定でF値でみる場合、以下のようになる。

require 'svmkit'

# LIBSVM形式のデータを読み込む.
samples, labels = SVMKit::Dataset.load_libsvm_file('pendigits')

# FMを定義する.
# 各パラメータの意味はドキュメンを参照ください(http://www.rubydoc.info/gems/svmkit/0.2.4)
factm = SVMKit::PolynomialModel::FactorizationMachineClassifier.new(
    n_factors: 4, init_std: 0.001,
    reg_param_bias: 1.0, reg_param_weight: 1.0, reg_param_factor: 10000.0,
    max_iter: 1000, batch_size: 50, random_seed: 1)

# One-vs-restで多値分類器にする.
ovr_factm = SVMKit::Multiclass::OneVsRestClassifier.new(estimator: factm)

# 評価尺度はマクロ平均なF値で.
ev = SVMKit::EvaluationMeasure::FScore.new(average: 'macro')

# 5-交差検定で評価する.
kf = SVMKit::ModelSelection::StratifiedKFold.new(n_splits: 5, shuffle: true, random_seed: 1)
cv = SVMKit::ModelSelection::CrossValidation.new(estimator: ovr_factm, splitter: kf, evaluator: ev)
report = cv.perform(samples, labels)

# 結果を出力する.
mean_f_score = report[:test_score].inject(:+) / kf.n_splits
puts(sprintf("Mean F1-Score: %.1f%%", 100.0 * mean_f_score))

これを実行すると以下の様になる。

$ ruby svmkit_fm_cv.rb
Mean F1-Score: 0.886

ちなみに、線形SVMでは、F値が0.601だった。 個人的な知見だが、FMは因子分解なパラメータが、オーバーフィットしやすいように思うので、そこの正則化パラメータ(↑の例ではreg_param_factor)を線形項の正則化パラメータ(↑の例ではreg_param_biasやreg_param_weight)よりも、大きくしたほうが多くの場合で上手くいく。

おわりに

つまらないものですが、よろしくお願い致します(SVMKitって名前なのに、SVM関係なくなってきてる...)

SVMKitにK分割交差検証を追加した

はじめに

SVMKitで「LIBSVM相当のことができるように」と思い、K分割交差検証(K-fold cross validation)を追加した。一度、cross validationするためのデータを分割するクラスを追加した段階で「これでminimum viable productかな」と思って、0.2.2としてリリースした。その後、予想していたよりもサクッとcross validationが実装できたので、0.2.3としてリリースした。

svmkit | RubyGems.org | your community gem host

使い方

データ分割クラスStratifiedKFold(もしくはKFold)と適当な分類器クラスを、CrossValidationクラスに渡して、performメソッドを実行すると交差検証が始まる形とした。performメソッドは、scikit-learnと同様に、実行時間やテストデータセットのスコアが配列で入ったHashを返す。

require 'svmkit'

# LIBSVM形式のデータを読み込む(LIBSVM Dataのpendigitsデータセット).
samples, labels = SVMKit::Dataset.load_libsvm_file('pendigits')

# カーネルSVMをOne-vs-Restで多値分類器にする.
kernel_svc =
  SVMKit::KernelMachine::KernelSVC.new(reg_param: 1.0, max_iter: 1000, random_seed: 1)
ovr_kernel_svc = SVMKit::Multiclass::OneVsRestClassifier.new(estimator: kernel_svc)

# StratifiedなK-fold分割を行うクラスを生成する(シャッフルして、各クラスで5分割する).
kf = SVMKit::ModelSelection::StratifiedKFold.new(n_splits: 5, shuffle: true, random_seed: 1)

# カーネルSVMの性能を交差検証で確認する.
cv = SVMKit::ModelSelection::CrossValidation.new(estimator: ovr_kernel_svc, splitter: kf)
kernel_mat = SVMKit::PairwiseMetric::rbf_kernel(samples, nil, 0.005)
report = cv.perform(kernel_mat, labels)

# 平均正確度を出力する.
mean_accuracy = report[:test_score].inject(:+) / kf.n_splits
puts(sprintf("Mean Accuracy: %.1f%%", 100.0 * mean_accuracy))

同じ様なことを、CrossValidationクラスではなく、データ分割のStratifiedKFoldクラスだけを使って行うと次のようになる。

require 'svmkit'

# LIBSVM形式のデータを読み込む(LIBSVM Dataのpendigitsデータセット).
samples, labels = SVMKit::Dataset.load_libsvm_file('pendigits')

# StratifiedなK-fold分割を行うクラスを生成する(シャッフルして、各クラスで5分割する).
kf = SVMKit::ModelSelection::StratifiedKFold.new(n_splits: 5, shuffle: true, random_seed: 1)

# K-fold cross validation法で分類精度を評価する.
scores = kf.split(samples, labels).map do |train_ids, test_ids|
  # 訓練データセットとテストデータセットに分ける.
  train_samples = samples[train_ids, true]
  train_labels = labels[train_ids]
  test_samples = samples[test_ids, true]
  test_labels = labels[test_ids]
  # 訓練データでカーネルSVMを学習する.
  kernel_matrix = SVMKit::PairwiseMetric::rbf_kernel(train_samples, nil, 0.005)
  base_classifier =
    SVMKit::KernelMachine::KernelSVC.new(reg_param: 1.0, max_iter: 1000, random_seed: 1)
  classifier = SVMKit::Multiclass::OneVsRestClassifier.new(estimator: base_classifier)
  classifier.fit(kernel_matrix, train_labels)
  # テストデータで学習したカーネルSVMの分類精度を評価する.
  kernel_matrix = SVMKit::PairwiseMetric::rbf_kernel(test_samples, train_samples, 0.005)
  classifier.score(kernel_matrix, test_labels)
end

# 平均正確度を出力する.
mean_accuracy = scores.inject(:+) / kf.n_splits
puts sprintf("Accuracy: %.1f%%", 100.0 * mean_accuracy)

これらを実行すると、以下のような5分割交差検証の分類精度が出力される。

$ ruby svmkit_validation.rb
Accuracy: 98.3%

おわりに

つまらないものですが、よろしくお願い致します。

Numo::Linalgのバックエンドライブラリを良い感じに読み込むライブラリを作った

はじめに

Numo::Linalgは、numpy.linalgに相当するもので、QR分解や特異値分解を行うメソッドを提供する。これら線形代数の機能は、いわゆるBLAS/LAPACK系のバックエンドライブラリの関数を呼び出すことで実現している。このバックエンドライブラリは、まだ開発途中(?)にあるため、.soファイルを読み込むようになっている。例えばmacOSでは、共有ライブラリの拡張子が.dylibになるため、バックエンドライブラリを指定する必要がある。これについては、詳細なドキュメントが先日公開された(感謝)。

Numo::Linalg - Selecting Backend Library

一方で、Numo::Linalgを使ったライブラリを作っていると「シンプルにrequire 'hoge'とかで上手いことできれば便利だな〜」という気持ちもあり、個人的にローダーを作っていた。元日だし、せっかくなので、gemとして公開することにした。

numo-linalg-autoloader | RubyGems.org | your community gem host

インストール

$ gem install numo-linalg-autoloader

使い方

requireするだけでよい。バックエンドライブラリを読み込むのに失敗するとRuntimeErrorだします。

require 'numo/linalg/autoloader'

仕組み

RbConfig::CONFIG['host_os']でOSを取ってきて、適切な拡張子を選択する。そして、Intel MKL, OpenBLAS, BLAS/LAPACKの順に、インストールされているだろうディレクトリを探していく。環境変数のLD_LIBRARY_PATHが設定されていれば、そこを優先的に探すようにしている。一通り必要なライブラリがそろったものを、バックエンドライブラリとして読み込む。

numo-linalg-autoloader/autoloader.rb at master · yoshoku/numo-linalg-autoloader · GitHub

ATLASは、UbuntuCentOSのパッケージでファイル名が異なったこと、macOSでは(例えばHomebrewなど)簡単にインストールする術がなかったのでサポートしなかった。OpenBLASも、UbuntuのパッケージではLAPACKのインターフェースを含まないなどの違いがあるので、ソースからビルドしてインストールすることをおすすめする。この他、macOSでは、Accelerate FrameworkでBLAS/LAPACKは提供されているが、LAPACKEは提供されていない(ので、Homebrewなどでインストールする必要があるが、うまみはあまりない)。Intel MKLかOpenBLASを使用することをおすすめする。

おわりに

私自身Numo::NArrayとNumo::Linalgのユーザーなので、上記の様なOSなど環境に依らないバックエンドライブラリを読み込む仕組みが、Numo::Linalgに組み込まれるまでサポートするつもりでいます。あと勝手に(Numo::Linalg::Autoloaderという)クラス生やして申し訳ありません 🙇🏻‍♂️ 🙇🏻‍♂️ 🙇🏻‍♂️

つまらないものですが、よろしくお願い致します。

SVMKitで使用する線形代数ライブラリをNumo::NArrayに移行した

SVMKitでは、線形代数ライブラリにNMatrixを使用していたが、パフォーマンスと将来性からNumo::NArrayに移行した。Numo::NArrayのメソッド等は、NMatrixともNumpyとも違うが、specを用意しておいて淡々と作業した。思い切った移行だけど、ユーザーも少ないので大丈夫と思いたい😅

つまらないものですが、よろしくお願い致します。

svmkit | RubyGems.org | your community gem host

vimのnativeなpackage管理でsyntasticをインストールした

はじめに

rubocopするのを忘れがちで、vimで(自動で)できてくれると嬉しいので、syntasticを入れることにした。使ってるvimのバージョンは8.0.1200なので、nativeなpackage管理を使ってインストールすることにした。

インストール

パッケージを入れるためのディレクトリを作って、そこにsyntasticをgit cloneするだけでよい。

$ mkdir -p ~/.vim/pack/plugins/start
$ cd ~/.vim/pack/plugins/start/
$ git clone --depth=1 https://github.com/vim-syntastic/syntastic.git

.vimrcは以下のとおり。設定については、公式のモノカッコイイ感じのモノを参考にすると良い。

" set statusline=%F とかがあって
set statusline+=%#warningmsg#
set statusline+=%{SyntasticStatuslineFlag()}
set statusline+=%*

let g:syntastic_always_populate_loc_list = 0
let g:syntastic_auto_loc_list = 0
let g:syntastic_check_on_open = 1
let g:syntastic_check_on_wq = 1
" rubocop動かしますよ〜
let g:syntastic_ruby_checkers = ['rubocop']

おわりに

ガリガリ書いてる段階で自動でrubocopが走るのが重い。もうちょっと考える。