洋食の日記

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

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)

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