洋食の日記

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

機械学習ライブラリSVMKitにカーネルSVMを追加した

はじめに

Pure Ruby機械学習ライブラリSVMKitにカーネルSVMを追加しました。カーネルSVMは、Pure Rubyでは速度的にツラいものがあるかな?と思っていたが、機械学習ライブラリとしては実装されているべきものなので追加した。※それ以前にLogistic Regressionを追加したけどブログに書くのを失念した...

svmkit | RubyGems.org | your community gem host

インストール

行列・ベクトルをあつかうのでNMatrixに依存する。

$ gem install nmatrix svmkit

使い方

Pythonのscikit-learnライクを意識してきたけど、今回はちょっと違うものにしてみた。 入力データとして、特徴ベクトルを与えるのが機械学習ライブラリでは一般的だが、SVMKitではカーネル行列を与える形にした。libsvmでいうところのprecomputed kernelという形式となる。 これは、世にある全てのカーネル関数を実装するのは難しいことと、ライブラリ利用者が適切なカーネルを選択できるように、という思いから。RBFカーネルやシグモイドカーネルは実装してある。

libsvm dataのサイトにあるpendigitsデータセットを読み込んで、分類するコードは以下の通り。 まずは、分類機の訓練から。

require 'svmkit'
require 'libsvmloader'

# libsvm形式の訓練データセットを読み込む。
samples, labels = LibSVMLoader::load_libsvm_file('pendigits', stype: :dense)

# RBFカーネル行列を計算する。※パラメータγは0.005とした。
kernel_matrix = SVMKit::PairwiseMetric::rbf_kernel(samples, nil, 0.005)

# カーネルSVMを用意する。
base_classifier =
  SVMKit::KernelMachine::KernelSVC.new(reg_param: 1.0, max_iter: 1000, random_seed: 1)

# one-vs-restで多値分類器とする。
classifier = SVMKit::Multiclass::OneVsRestClassifier.new(estimator: base_classifier)

# 分類器を訓練する。
classifier.fit(kernel_matrix, labels)

# 分類器を保存する。
File.open('trained_classifier.dat', 'wb') { |f| f.write(Marshal.dump(classifier)) }

そして、訓練済みの分類器を読み込んで、テストデータを分類するコード。

require 'svmkit'
require 'libsvmloader'

# libsvm形式のテストデータセットを読み込む。
samples, labels = LibSVMLoader::load_libsvm_file('pendigits.t', stype: :dense)

# カーネル行列を計算するために、訓練データセットも読み込む。※ラベル情報は不要
tr_samples, = LibSVMLoader::load_libsvm_file('pendigits', stype: :dense)

# 訓練済みの分類器を読み込む。
classifier = Marshal.load(File.binread('trained_classifier.dat'))

# テストデータ-訓練データ間でRBFカーネル行列を計算する。
kernel_matrix = SVMKit::PairwiseMetric::rbf_kernel(samples, tr_samples, 0.005)

# テストデータのラベルを推定し、分類結果のAccuracyを出力する。
puts(sprintf("Accuracy: %.1f%%", 100.0 * classifier.score(kernel_matrix, labels)))

結果としてAccuracyは97.6%となった。

トイデータでの実験

訓練データがこれで、

f:id:yoshoku:20171021152814p:plain

テストデータがこれ。

f:id:yoshoku:20171021152829p:plain

テストデータのラベルを、SVMKitのカーネルSVMで学習して推定すると、

f:id:yoshoku:20171021152844p:plain

こうじゃ!!うまく非線形データを分類できている。

おわりに

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

Pure RubyなSupport Vector Machineのgemを公開した

はじめに

RubyPythonのscikit-learnに相当するライブラリがない様なので、作ってみることにした。ひとまず、Support Vector MachineSVM)による多値分類が実装できたので、gemとして公開することにした。今後、他の機械学習アルゴリズムも追加していく。

svmkit | RubyGems.org | your community gem host

インストール

行列・ベクトルをあつかうのでNMatrixに依存する。

$ gem install svmkit

使い方

インターフェースは、scikit-learnライクにした。libsvm dataのサイトにあるpendigitsデータセットを読み込んで、分類するコードは以下の通り。 まずは、分類機の訓練から。

require 'svmkit'
require 'libsvmloader'

# libsvm形式の訓練データセットを読み込む。
samples, labels = LibSVMLoader.load_libsvm_file('pendigits', stype: :dense)

# 特徴量の値を[0,1]の範囲に正規化する。
normalizer = SVMKit::Preprocessing::MinMaxScaler.new
normalized = normalizer.fit_transform(samples)

# RBFカーネル空間に(カーネル近似法で)射影する。
transformer = SVMKit::KernelApproximation::RBF.new(gamma: 2.0, n_components: 1024, random_seed: 1)
transformed = transformer.fit_transform(normalized)

# ベースとなる2値分類のSVMを定義して、
base_classifier =
  SVMKit::LinearModel::PegasosSVC.new(penalty: 1.0, max_iter: 50, batch_size: 20, random_seed: 1)
# それを、One-vs.-Rest法で多値化する。
classifier = SVMKit::Multiclass::OneVsRestClassifier.new(estimator: base_classifier)
# そして、分類器を学習する。
classifier.fit(transformed, labels)

# 各種モデルを保存する。
File.open('trained_normalizer.dat', 'wb') { |f| f.write(Marshal.dump(normalizer)) }
File.open('trained_transformer.dat', 'wb') { |f| f.write(Marshal.dump(transformer)) }
File.open('trained_classifier.dat', 'wb') { |f| f.write(Marshal.dump(classifier)) }

そして、訓練済みの分類器を読み込んで、テストデータを分類するコード。

require 'svmkit'
require 'libsvmloader'

# libsvm形式のテストデータセットを読み込む。
samples, labels = LibSVMLoader.load_libsvm_file('pendigits.t', stype: :dense)

# 各種モデルを読み込む。
normalizer = Marshal.load(File.binread('trained_normalizer.dat'))
transformer = Marshal.load(File.binread('trained_transformer.dat'))
classifier = Marshal.load(File.binread('trained_classifier.dat'))

# 正規化とカーネル空間への射影を行う。
normalized = normalizer.transform(samples)
transformed = transformer.transform(normalized)

# テストデータセットのラベルを推定する。
# predicted_labels = classifier.predict(transformed, labels)

# Accuracyを出力する。
puts(sprintf("Accuracy: %.1f%%", 100.0 * classifier.score(transformed, labels)))

実装よもやま話

  • SVMアルゴリズムには、確率的勾配降下法によるPegasosを選択した。 事前に、他のアルゴリズムも試したが、Pure Rubyでそれなりの速度で動くのがPegasosだった。 同じく速度の問題で、直接的なカーネル法による非線形化はあきらめて、ランダムプロジェクションによるカーネル近似を実装した。
  • SVMの多値分類器化には、ひとまず、One-vs.-Rest(OvR)を選択した。scikit-learnでは、OvRなどを内包して、multiclassモジュールを意識的に使わなくて良いようになっている。SVMKitでは、明示的に使う方向とした。
  • 学習済みのモデルの保存・読込には、Marshalによるシリアライズを使用することにした。 これは、scikit-learnでは、joblib.dumpを使用することに対応させてである。Marshal.loadを使うことに関しては、rubocop先輩サーセンといった感じ。

おわりに

今後は、scikit-learn内のベーシックな手法を実装していきたい。ある程度、中身が充実してきたら、gem名を変えると思う。次は、決定木系のアルゴリズムの実装かなぁ。つまらないものですが、よろしくお願い致します。

rb-libsvmとlibsvmloaderを使ったRubyでのカーネル非線形SVMによる分類

はじめに

RubyLIBSVMバインドであるrb-libsvmと、libsvm形式のデータセットを読み書きするlibsvmloaderで、カーネル非線形SVMで分類する例です。カーネル非線形と明記するのは、libsvmの姉妹品であるliblinearが線形SVMなため。

準備

まずLIBSVMそのものをインストールする必要がある。macでhomebrewなら次のとおり。

$ brew install libsvm

次にGemをインストールする。libsvmloaderがnmatrixに依存するので、一応nmatrixつけてみました。SciRubyにようこそ的なメッセージがでるのが良いよね〜。

source 'https://rubygems.org'

gem 'nmatrix'
gem 'rb-libsvm'
gem 'libsvmloader'
$ bundle install

最後にサンプルで使うデータセットlibsvmのサイトからダウンロードする。MNISTが有名だけど、大きいので、同じ手書き文字認識データセットのpendigitsを使う。

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

コード

「1. 訓練データセットで、SVMを学習する。2. テストデータセットを読み込んで、ラベルを推定する。 3. Accuracyを計算して出力する。」この一連の流れを一つにまとめた。

require 'libsvmloader'
require 'libsvm'

# 訓練データを読み込む
samples, labels = LibSVMLoader.load_libsvm_file('pendigits')

# 訓練データをrb-libsvmの特徴ベクトル形式に変換する
examples = samples.each_row.map { |v| Libsvm::Node.features(v.to_a) }

# カーネル非線形SVMのパラメータを設定する
params = Libsvm::SvmParameter.new.tap do |p|
  p.cache_size = 1000                      # キャッシュメモリサイズ [MB]
  p.svm_type = Libsvm::SvmType::C_SVC      # SVM分類器
  p.kernel_type = Libsvm::KernelType::RBF  # RBFカーネル
  p.gamma = 0.0001                         # RBFカーネルのパラメータ
  p.c = 1.0                                # SVMの正則化パラメータ
  p.eps = 0.001                            # SVMの最適化を終了する閾値
end

# カーネル非線形SVMを訓練する
problem = Libsvm::Problem.new
problem.set_examples(labels.to_flat_a, examples)
model = Libsvm::Model.train(problem, params)

# テストデータを読み込む
samples, labels = LibSVMLoader.load_libsvm_file('pendigits.t')

# テストデータのラベルを推定する
preds = samples.each_row.map { |v| model.predict(Libsvm::Node.features(v.to_a)) }

# Accuracyを出力する
n_hits = preds.map.with_index { |l, n| 1 if l == labels[n] }.compact.sum
n_samples = labels.size
accuracy = 100.0 * (n_hits / n_samples.to_f)
puts(format('Accuracy = %.4f%% (%d/%d)', accuracy, n_hits, n_samples))

これを実行すると、pendigitsデータセットの学習と分類が行われて、バーンとAccuracyが表示される。

$ ruby hoge.rb
Accuracy = 98.2847% (3438/3498)

おわりに

RubyにはLIBSVMをバインドしたGemが他にもある。それぞれに使い方が異なるので、チームやプロジェクトにあったものを選ぶのが良いと思う。Pythonのscikit-learnmみたいな、デファクトスタンダード機械学習ライブラリはRubyにはない(2017/09/16現在)。みんな自由にやってる感じ。でも、それはそれで良い雰囲気だと思う。そんなわけで、個人的にscikit-learnインスパイアなRubyのベーシックな機械学習ライブラリを作ることを考えている(特に野心はなく単純な興味から)。深層学習は「流行ってるから、もう誰かライブラリ作ってるでしょ〜」といった気持ち。

ホントにスゴイ人気!!

LibSVMLoaderのゼロベクトルが読み込めないバグを修正した

タイトルのとおりです。LibSVMLoader(ver. 0.1.0)は、ゼロベクトルが含まれているとコケることがわかりました。修正してアップしておきました。申し訳ありません。

libsvmloader | RubyGems.org | your community gem host

specまわりの修正を除いた本体の修正はわずか1行です。バグって恐い。

RandSVDで乱数のシードを固定できるように修正した

RandSVDをマイナー(タイニー?)バージョンアップした。RandSVD(というよりも乱択アルゴリズム特異値分解)では、ランダム行列との積により行列を小さくする処理を含んでいる。なので、乱数のシードを固定できるようにしたほうが親切だと考えた。 ※ NMatrixに、numpy.random.seedとかみたいにグローバルに固定できる仕組みがあると、勝手に思い込んでいたのがそもそもの間違いでして…

require 'randsvd'

mat = NMatrix.rand [5, 5]
nb_singular_values = 2
nb_power_iterations = 3
random_seed = 1

u, s, vt = RandSVD.gesvd(mat, nb_singular_values, nb_power_iterations, random_seed)

単純にメソッド引数を後ろに増やしました。最初からキーワード引数とかなにかで、柔軟に引数増やせるように設計すれば良かったと反省…

randsvd | RubyGems.org | your community gem host