洋食の日記

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

SVMKitのSVM系分類器に事後確率を推定するメソッドを追加した

はじめに

SVMKitのSupport Vector MachineSVM)系の分類器であるSVCKernelSVCに、ロジスティック回帰と同様に事後確率を推定するpredict_probaメソッドを追加した。

svmkit | RubyGems.org | your community gem host

これにあわせて、Log-Lossを評価するクラスを追加した。そして、それを良い感じに動かすために、ラベル情報に整数を割り当ててベクトルにするLabelEncoderや、ラベル情報からOne-hot-vectorを生成するOneHotEncoderを追加した。

使い方

事後確率の推定には、SVMの識別関数の値をもとに、シグモイド関数のモデルを当てはめる手法を用いている(詳しくはdocumentのreferenceにある論文を参照)。SVMKitでは、Scikit-Learnにならって、コンストラクタにprobablity引数を追加したので、これをtrueにすると事後確率のためのモデルを計算する。

require 'svmkit'

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

# 線形SVMを定義する.事後確率の推定のためprobability引数にtrueを指定する.
svc = SVMKit::LinearModel::SVC.new(reg_param: 1.0, max_iter: 1000, probability: true, random_seed: 1) 

# 評価手法にLogarithmic Lossを用いる.
ev = SVMKit::EvaluationMeasure::LogLoss.new

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

# 結果を出力する
mean_logloss = report[:test_score].inject(:+) / kf.n_splits
puts("Mean log-loss: %.3f" % mean_logloss) 

これを実行すると、平均Log-Lossが出力される。

Mean log-loss: 0.293

マルチクラスなLog-Lossを計算するために、いわゆるLabelEncoderOneHotEncoderが必要になったので、これも追加した。

おわりに

0.2系も0.2.9にまで達したので、分類器がらみの実装はひとまず置いて、回帰を実装していこうと思う。リファクタリングできる箇所も多いが(基本的に確率的勾配降下法を用いていて共通化できるコードが多い)それもグッと我慢して、分類以外のタスクにも使えるようにすることを優先しよう、という思う。

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

SVMKitのLogistic Regressionの勾配計算に間違いがあったので修正した

はじめに

SVMKitのSVMKit::LinearModel::LogisticRegressionの勾配計算に間違いがあったので修正して0.2.8としてリリースした。

svmkit | RubyGems.org | your community gem host

0.2.8では、与えられたパラメータのチェックなどの細かな部分を補うことを予定していた。そのチェックを兼ねて実データで実験していて間違いに気づいた。

反省

SVMKitでは、基本的に各クラスにspecを用意していて、coverallsでcoverageも100%となっている。学習アルゴリズムの確認には、toydataとも言えるシンプルな合成データを用いていて、LogisticRegressionのspecでも、正しく分類できていた。今回、偶然に試した、手書き数字画像データの分類で、LogisticRegressionを用いた場合のAccuracyが、SVMKit::LinearModel::SVCのAccuracyと比較して30%も低かったことで気づくことができた。今後は、アルゴリズムを追加した場合は、実データでの交差検定で動作を確認していきたい。

おわりに

0.2.8では、予定通り、パラメータのチェックもくわえた。例えば、サンプルはNumo::DFloatの行列、ラベルはNumo::Int32のベクトルで与えないとTypeErrorをraiseする。

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

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周年おめでとうございます!!