はじめに
Rumaleでは決定木系のアルゴリズムの高速化と追加を進めている。ついに人気のGradient Tree Boosting(Gradient Boosting MachineやGradient Boosted Regression Treeなどとも呼ばれる)を実装して、ver. 0.9.2としてリリースした。
rumale | RubyGems.org | your community gem host
Gradient Tree Boosting (GTB)は、PythonではXGBoost、LightGBM、CatBoostなどで有名なアルゴリズムである。
GTBを実装しようと思ったのは、Scikit-Learnでも実装されているのと、ver. 0.2.1でLightGBMを参考にした実装であるHistGradientBoostingClassifier/Regressorが追加されるのを知って、Rubyで実装してみてみるか〜という気持ちになったのもあって。
使い方
Rumaleはgemコマンドでインストールできる。Numo::NArrayに依存している。
$ gem install rumale
データセットの読み込みでred-datasetsを使いたいので、これもインストールする。
$ gem install red-datasets-numo-narray
LIBSVM Dataのcpusamllデータセット(データ数 8,192、次元数12)を用いた回帰を試してみる。
require 'rumale'
require 'datasets'
require 'datasets-numo-narray'
datasets = Datasets::LIBSVM.new('cpusmall').to_narray
values = Numo::DFloat.cast(datasets[true, 0])
samples = Numo::DFloat.cast(datasets[true, 1..-1])
ss = Rumale::ModelSelection::ShuffleSplit.new(n_splits: 1, test_size: 0.1, random_seed: 1)
train_ids, test_ids = ss.split(samples, values).first
train_s = samples[train_ids, true]
train_v = values[train_ids]
test_s = samples[test_ids, true]
test_v = values[test_ids]
est = Rumale::Ensemble::GradientBoostingRegressor.new(
n_estimators: 100,
learning_rate: 0.1,
reg_lambda: 0.001,
subsample: 0.8,
max_depth: 4,
max_features: 8,
random_seed: 1
)
est.fit(train_s, train_v)
puts("GTB R2-Score: %.4f" % est.score(test_s, test_v))
est = Rumale::Ensemble::RandomForestRegressor.new(
n_estimators: 100,
max_depth: 4,
max_features: 8,
random_seed: 1
)
est.fit(train_s, train_v)
puts("RF R2-Score: %.4f" % est.score(test_s, test_v))
これを実行すると以下のようになる。GTBの方が良い値を得られている。
GTB R2-Score: 0.9703
RF R2-Score: 0.9433
分類器のRumale::Ensemble::GradientBoostingClassifierも、同様のパラメータと手順で利用できる。
特徴量の離散化
LightGBMなどでは、特徴量を離散値に変換すること(離散化)で高速な計算を実現している。決定木では特徴量の値(特徴ベクトルをuniqして残る値)が、木を分割する際の閾値の候補となる。離散化することで候補値が少なくなると、分割の評価計算の回数が少なくなるので、そのぶん速くなる。この離散化込みでアルゴリズムを考え、全体的に高速化しているものもあるが、Rumaleでは、離散化したくない場合もあると思い、アルゴリズムとは別で、特徴量を離散化する BinDiscretizer クラスを用意した。実行例は以下のようになる。[-1, 1]な実数値を4段階に離散化している。
irb(main):001:0> require 'rumale'
=> true
irb(main):002:0> t = Rumale::Preprocessing::BinDiscretizer.new(n_bins: 4)
=>
irb(main):003:0> x=Numo::DFloat.new(5, 3).rand - 0.5
=> Numo::DFloat
[[-0.438246, -0.126933, 0.294815],
[-0.298958, -0.383959, -0.155968],
[0.039948, 0.237815, -0.334911],
[-0.449117, -0.391935, -0.431292],
[0.404121, -0.0213559, -0.157031]]
irb(main):004:0> t.fit_transform(x)
=> Numo::DFloat
[[0, 1, 3],
[0, 0, 1],
[2, 3, 0],
[0, 0, 0],
[3, 2, 1]]
離散化することでGTB高速になるかを以下のコードで確認した。
require 'rumale'
require 'datasets'
require 'datasets-numo-narray'
require 'benchmark'
datasets = Datasets::LIBSVM.new('cpusmall').to_narray
values = Numo::DFloat.cast(datasets[true, 0])
samples = Numo::DFloat.cast(datasets[true, 1..-1])
ss = Rumale::ModelSelection::ShuffleSplit.new(n_splits: 1, test_size: 0.1, random_seed: 1)
train_ids, test_ids = ss.split(samples, values).first
train_s = samples[train_ids, true]
train_v = values[train_ids]
test_s = samples[test_ids, true]
test_v = values[test_ids]
est = Rumale::Ensemble::GradientBoostingRegressor.new(
n_estimators: 100,
learning_rate: 0.1,
reg_lambda: 0.001,
subsample: 0.8,
max_depth: 4,
max_features: 8,
random_seed: 1
)
Benchmark.bm 10 do |r|
r.report 'non-transform' do
est.fit(train_s, train_v)
puts(" (R2-Score: %.4f)" % est.score(test_s, test_v))
end
r.report 'discretized' do
t = Rumale::Preprocessing::BinDiscretizer.new(n_bins: 4)
dis_train_s = t.fit_transform(train_s)
dis_test_s = t.transform(test_s)
est.fit(dis_train_s, train_v)
puts(" (R2-Score: %.4f)" % est.score(dis_test_s, test_v))
end
end
実行結果は以下のとおり。高速化されるが、予測精度は落ちるようだ。このあたりは、データセットの大きさや特徴量の次元数、パラメータの兼ね合いで変わってくると思われる。
user system total real
non-transform (R2-Score: 0.9703)
8.030000 0.810000 8.840000 ( 8.866683)
discretized (R2-Score: 0.9203)
6.330000 0.920000 7.250000 ( 7.275490)
おわりに
Rumaleではベーシックなアルゴリズムの追加を進めてきたが、これでちょっとRumaleもモダンになったな?今後は、もう少し決定木関連の実装を進めつつ、教師なし学習のアルゴリズムを増やしたいと考えている。
github.com