洋食の日記

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

SVMKitの二値分類器にOne-vs-the-restを内包した

はじめに

SVMKitのSVMKit::LinearModel::SVCなどの二値分類器で、多値のラベルを与えれた場合に、自動的にOne-vs-the-rest法で多値分類器化するように修正を加えた。

svmkit | RubyGems.org | your community gem host

これにより、SVMKit::Multiclass::OneVsRestClassifierを使うことなく、多値分類が可能となる。あわせて、SVCとLogisticRegressionの実装を修正した。Numo::Narrayに不慣れなときに実装したものだったので、今回の修正でわずかだかパフォーマンスも向上した。

使い方

LinearModel::SVCの他、LinearModel::LogisticRegression、KernelMachine::KernelSVC、PolynomialModel::FactorizationMachineClassifierで、明示的な多値分類器化が必要なくなる。

require 'svmkit'

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

# 線形SVM分類器を定義する.
svc = SVMKit::LinearModel::SVC.new

# 以前は、明示的に多値分類器化する必要があったが、これがなくなる.
# ovr_svc = SVMKit::Multiclass::OneVsRestClassifier.new(estimator: svc)

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

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

おわりに

0.2系では細かい修正を行い、0.3系から回帰かクラスタリングの実装をしていく予定にしている。

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

SVMKitに決定木とランダム森の分類器を追加した

はじめに

SVMKitに決定木(Decision Tree)とランダム森(Random Forest)による分類器を実装し、バージョンを0.2.6とした。

svmkit | RubyGems.org | your community gem host

決定木は、CARTをベースにした二分木によるものを実装した。Scikit-learnにならって、いわゆる木の剪定(pruning)は実装せず、木の深さなど木の成長に関するパラメータをあらかじめ指定して、過学習を防ぐアプローチ(事前剪定,pre-pruning)を採用した。このパラメータに関しては、「Pythonではじめる機械学習」を参考にして、木の最大の深さ(max_depth)、葉の最大数(max_leaf_nodes)、葉がもつサンプル数の最小値(min_samples_leaf)を実装した。

Pythonではじめる機械学習 ―scikit-learnで学ぶ特徴量エンジニアリングと機械学習の基礎

ランダム森は、決定木が実装できれば簡単に実装できる。ブートストラップサンプルしたデータで決定木を作成することを複数回おこない、分類の際は、これら複数の決定木で多数決をとる。決定木を成長させる際に、全ての特徴量を使わずに、ランダムに選んだ一部の特徴量のみを使用する。決定木は、学習データの変化に影響を受けやすいため、複数のもので多数決をとることで安定的な分類器を実現できる。また、ランダムに選択した一部の特徴量のみを使うため、互いに異なった決定木が作成される(※ランダムな特徴量の選択をしない場合、決定木によるバギングと同様となる)。決定木やランダム森のアルゴリズムの詳細については、「はじめてのパターン認識」がわかりやすい。

はじめてのパターン認識

使い方

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

$ gem install svmkit

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

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

決定木の分類精度を、5交差検定でF値で確認する場合、以下のようになる。

require 'svmkit'

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

# 決定木を定義する.
# 木の深さ(max_depth)などがパラメータとしてあるが、指定しなければ、
# 学習データに合わせて成長する(※過学習する恐れがあることに注意).
dt = SVMKit::Tree::DecisionTreeClassifier.new(random_seed: 1)

# 評価尺度はマクロ平均な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: dt, splitter: kf, evaluator: ev)
report = cv.perform(samples, labels)

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

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

$ ruby svmkit_dtree_cv.rb
Mean F1-Score: 0.953

さらに、ランダム森の場合は、以下の様になる(一部コメントを省略)。

require 'svmkit'

samples, labels = SVMKit::Dataset.load_libsvm_file('pendigits')

# ランダム森を定義する.
# 決定木と同様のパラメータの他に、作成する決定木の数(n_estimators)と、ランダムに選択する特徴数(max_features)を指定できる.
# 指定しない場合、Scikit-learnと同様に、n_estimators=10、max_features=Math.sqrt(n_features)となる.
# これらパラメータ(とくにmax_features)は最適値を探った方が良い.
rf = SVMKit::Ensemble::RandomForestClassifier.new(random_seed: 1)

ev = SVMKit::EvaluationMeasure::FScore.new(average: 'macro')

kf = SVMKit::ModelSelection::StratifiedKFold.new(n_splits: 5, shuffle: true, random_seed: 1)
cv = SVMKit::ModelSelection::CrossValidation.new(estimator: rf, splitter: kf, evaluator: ev)
report = cv.perform(samples, labels)

mean_f_score = report[:test_score].inject(:+) / kf.n_splits
puts(sprintf("Mean F1-Score: %.3f", mean_f_score))

これを実行すると以下の様になる。決定木よりもF値が改善されていることがわかる。

$ ruby svmkit_rforest_cv.rb
Mean F1-Score: 0.967

おわりに

決定木は、アルゴリズムとして線形代数的な計算をあまり含まず、Numo::NArrayを使用することによる高速化の恩恵が少ないため、少し実装を避ける気持ちがあった(今回の実装も、煩雑なコードになってしまい、動作も高速なものではない)。しかし、決定木とランダム森を実装したことで、代表的な分類器をそろえることができた。今後のSVMKitの開発は、K-meansクラスタリングや主成分分析などの、教師なし学習の実装に入る前に、全体的なリファクタリングを考えている。ひとまず代表的な分類器を実装することを優先したので、パラメータの値チェックなどが実装されていない。また、パフォーマンス的なことも一旦は無視している。ドキュメントはそろえる様に心がけているのですが...

File: README — Documentation for svmkit (0.2.6)

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

SVMKitに単純ベイズ分類器を追加した

はじめに

Ruby 25周年の記念日に何かSVMKitをバージョンアップしたくて準備を進めていた。無事に単純ベイズ(Naive Bayes, NB)分類器を追加した状態でバージョンアップできた。Scikit-learnにならってGaussian、Multinomial、Bernoulliの各種分布のアルゴリズムを追加した。この他の変更点は、Factorization Machineによる分類器に、ロス関数を選択するパラメータを追加して、hingeとlogisticを選べるようにした。

svmkit | RubyGems.org | your community gem host

使い方

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

$ gem install svmkit

次に、データを用意する。今回は、単純ベイズということから、LIBSVM DATAより、文書データセットであるnews20を取得した。 TFにより重み付けされた文書ベクトルのデータセットとなる。

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

Multinomialな単純ベイズの分類精度を、5交差検定でF値でみる場合、以下のようになる。

require 'svmkit'

# LIBSVM形式のnews20データを読み込む.
# ※やや大きいファイルなため全体的に遅め.
samples, labels = SVMKit::Dataset.load_libsvm_file('news20.t')

# Multinomialな単純ベイズを定義する.
# 平滑化パラメータは基本的には1.0で良い.
mnb = SVMKit::NaiveBayes::MultinomialNB.new(smoothing_param: 1.0)

# 評価尺度はマクロ平均な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: mnb, 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_nb_cv.rb
Mean F1-Score: 73.0%

ちなみに、線形SVMでは、以下のようになる(コメントは一部省略)。 TFによる文書ベクトルの文書分類では、Multinomialな単純ベイズが有効であることがわかる。

require 'svmkit'

samples, labels = SVMKit::Dataset.load_libsvm_file('news20.t')

# 線形SVMによる分類器を作成し,それをone-vs-restで多値分類器にする.
svc = SVMKit::LinearModel::SVC.new(reg_param: 1.0, max_iter: 1000, random_seed: 1)
ovr_svc = SVMKit::Multiclass::OneVsRestClassifier.new(estimator: svc)

ev = SVMKit::EvaluationMeasure::FScore.new(average: 'macro')

kf = SVMKit::ModelSelection::StratifiedKFold.new(n_splits: 5, shuffle: true, random_seed: 1)
cv = SVMKit::ModelSelection::CrossValidation.new(estimator: ovr_svc, 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_svc_cv.rb
Mean F1-Score: 69.1%

おわりに

Ruby 25周年おめでとうございます!!

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%

おわりに

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