洋食の日記

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

Rumaleのリッジ・線形回帰のソルバーでL-BFGS法が自動的に選択されるようにした

はじめに

Ruby機械学習ライブラリであるRumaleで、RidgeとLinearRegressionで、solver: 'auto' とした場合に、Numo::Linalgがロードされていない場合は、L-BFGS法をソルバーとするようにした。Numo::Linalgがロードされている場合は、従来どおり特異値分解を用いる方法を使う。これを、ver. 0.23.0としてリリースした。

rumale | RubyGems.org | your community gem host

使い方

Rumaleはgemコマンドでインストールできる。

$ gem install rumale

使い方は変わらない。RidgeとLinearRegressionのオブジェクトを生成する際に、ソルバーを指定するsolverパラメータを'auto'とすると、L-BFGS法が選択される。Numo::Linalgがロードされている場合は、特異値分解を用いる。

require 'rumale'

reg = Rumale::LinearModel::Ridge.new(solver: 'auto')
pp reg.params[:solver]
# > "lbfgs"

require 'numo/openblas'

reg = Rumale::LinearModel::Ridge.new(solver: 'auto')
pp reg.params[:solver]
# > "svd"

おわりに

これまで確率的勾配降下法(Stochastic Gradient Descent, SGD)が選択されていたが、SGDはパラメーターの設定方法が難しいようなので、L-BFGS法を選択するようにした。安定的に解を得たい場合は、Numo::Linalgをロードして、特異値分解を用いるのが良い。 Rumaleの開発は、これでしばらくはメンテナンスモードに入ろうと思っている。学習アルゴリズムは無数にあるので、足そうと思えばいくらでも足せるのだが、そうすると巨大ライブラリになってしまう。それよりも、自然言語処理ライブラリとか、OpenCVRubyバインディングだとか、機械学習と組み合わせて使うものを充実させた方が良いと考える。あと、APIリファレンスだけじゃなくて、User GuideとかTutorialとか、そういうドキュメントも書かないとな〜。

Rumaleのカーネル法まわりを便利にした

はじめに

Ruby機械学習ライブラリであるRumaleに、前処理としてカーネル行列を計算するクラス、カーネルリッジ回帰による分類器を追加した。また、Nystroemカーネル近似では、サポートするカーネル関数がRBFカーネルだけであったが、多項式カーネルやシグモイドカーネルを追加した。これを、ver. 0.22.5としてリリースした。

rumale | RubyGems.org | your community gem host

使い方

Rumaleはgemコマンドでインストールできる。カーネルリッジ回帰による分類器で、特異ベクトルを求める必要があるので、numo-openblasも一緒にインストールする。

$ gem install rumale numo-openblas

カーネルリッジ回帰による分類器は、あるラベルが付与されてない/されてるを {-1, 1} の目的変数に変換し(ラベルが3種類あって、あるサンプルに3番目のラベルが付与されているとすると、[-1, -1, 1]という目的変数によるベクトルになる)、これに対して回帰を行い、推論では推定値が最も大きな値に関係するラベルを返す。Rumaleでは、カーネル法による推定器に対しては、カーネル行列を与えるようになっている。scikit-learnなどでは、サンプルを与えることが多い。これと同じ使い方ができるように、前処理としてカーネル行列を計算するクラスを追加した。Pipelineでつないで使用する。

これらを用いて、分類を行う例を示す。データセットには、LIBSVM Dataからletterをダウンロードした。

$ wget https://www.csie.ntu.edu.tw/~cjlin/libsvmtools/datasets/multiclass/letter.scale.t
$ wget https://www.csie.ntu.edu.tw/~cjlin/libsvmtools/datasets/multiclass/letter.scale.tr
require 'numo/openblas'
require 'rumale'

# データを読み込む.
x_train, y_train = Rumale::Dataset.load_libsvm_file('letter.scale.tr')
x_test, y_test = Rumale::Dataset.load_libsvm_file('letter.scale.t')

# 与えられたサンプルからカーネル行列を計算する KernelCalculator と
# カーネルリッジ回帰による分類器の KernelRidgeClassifier を Pipeline でつなぐ.
classifier = Rumale::Pipeline::Pipeline.new(
  steps: {
    ker: Rumale::Preprocessing::KernelCalculator.new(kernel: 'poly', gamma: 1, degree: 3, coef: 1),
    krc: Rumale::KernelMachine::KernelRidgeClassifier.new(reg_param: 1)
  }
)

# 分類器を学習する.
classifier.fit(x_train, y_train)

# 正確度を出力する.
puts(format("Accuracy: %.3f", classifier.score(x_test, y_test)))

これを実行すると以下のようになる。ロジスティック回帰では、正確度は0.763だったので、カーネル法を用いることで精度が向上することがわかる。

Accuracy: 0.917

おわりに

自分のなかで、1週間ほどカーネル法リバイバルが起きて、あれこれと追加した。カーネル法は、ニューラルネットワークの隆盛で、影が薄くなってしまったが、小さいデータセットであるとか条件によっては有効であると考える。関連して、ガウス過程の手法を追加したいと考えているが、Rumale::GPとか別Gemかな〜と思っている。

github.com

Rumaleに投票によるアンサンブル法を追加した

はじめに

Ruby機械学習ライブラリであるRumaleに、投票(voting)によるアンサンブル法を利用した分類器・回帰分析を追加して、ver. 0.22.4としてリリースした。

https://rubygems.org/gems/rumale/versions/0.22.4

使い方

Rumaleはgemコマンドでインストールできる。

$ gem install rumale

投票は、複数の推定器の推定結果を、多数決によって結合し、最終的な推定結果を得る。回帰分析を例に示す。まずは、線形回帰とランダム森による回帰分析を試す。データセットにはLIBSVM Dataのabaloneを用いた。

$ wget https://www.csie.ntu.edu.tw/~cjlin/libsvmtools/datasets/regression/abalone_scale
require 'rumale'

# データセットを読み込む.
x, y = Rumale::Dataset.load_libsvm_file('abalone_scale')

# 訓練とテストに分割する.
x_train, x_test, y_train, y_test = Rumale::ModelSelection.train_test_split(x, y, test_size: 0.2, random_seed: 1)

# ランダム森を学習する.
reg = Rumale::Ensemble::RandomForestRegressor.new(random_seed: 1)
reg.fit(x_train, y_train)

# 決定係数 (1に近づくほどよい) により回帰の精度を評価する.
puts(format("R2-Score: %.4f", reg.score(x_test, y_test)))

# 同様に線形回帰を用いる.
reg = Rumale::LinearModel::LinearRegression.new(solver: 'lbfgs', random_seed: 1)
reg.fit(x_train, y_train)
puts(format("R2-Score: %.4f", reg.score(x_test, y_test)))

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

$ ruby regression.rb
R2-Score: 0.5209
R2-Score: 0.5353

意外なことに線形回帰のほうがスコアが高い。この二つを投票により組み合わせる。

# 新たに投票を加える. 組み合わせる推定器はestimatorsパラメータにHashで与える.
reg = Rumale::Ensemble::VotingRegressor.new(
  estimators: {
    rnd: Rumale::Ensemble::RandomForestRegressor.new(random_seed: 1),
    lin: Rumale::LinearModel::LinearRegression.new(solver: 'lbfgs', random_seed: 1)
  }
)
reg.fit(x_train, y_train)
puts('---')
puts(format("R2-Score: %.4f", reg.score(x_test, y_test)))

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

$ ruby regression.rb
R2-Score: 0.5209
R2-Score: 0.5353
---
R2-Score: 0.5664

組み合わせることで、スコアが大きく向上していることがわかる。 weightsパラメータで重みを与えることによる重み付き投票や、分類器(VotingClassifier)では、クラス確率によるソフト投票にも対応している。

Class: Rumale::Ensemble::VotingRegressor — Documentation by YARD 0.9.26

おわりに

投票による推定器の結合は、直感的にはたくさん推定器を用意すれば上手くいく気がするが、実際には、同じ問題を解いていることから推定器間に相関があり限界がある。しかし、ロジスティック回帰と決定木など、異なる仕組みの手法を組み合わせると効果が得られることがある。スタッキングと同様に、性能に伸び悩んでいる場合に試してみると良いと思う。投票(に限らずアンサンブル手法全般)については以下の書籍が詳しい。Rumaleのアンサンブル手法の実装でも参考にしている。

Rumaleに非負最小二乗法による回帰を追加した

はじめに

Ruby機械学習ライブラリであるRumaleに、非負最小二乗法(Non-nagtive Least Squares, NNLS)による線形回帰を追加して、ver. 0.22.3としてリリースした。

https://rubygems.org/gems/rumale/versions/0.22.3

使い方

Rumaleはgemコマンドでインストールできる。

$ gem install rumale

非負最小二乗法は、その名の通り、回帰係数の値が負にならないという制限がついた回帰手法である。 与えられた説明変数や目的変数が、負の値を含まない場合に有効であったりする。 普通の線形回帰で当てはまりが悪い場合に、検討してみると良い。 簡単なサンプルプログラムを、以下に示す。

require 'rumale'

rng = Random.new(1)
n_samples = 200
n_features = 100

# ランダムなデータを用意する.
x = Rumale::Utils.rand_normal([n_samples, n_features], rng)

# 非負の係数を用意する (本来は未知).
coef = Rumale::Utils.rand_normal([n_features, 1], rng)
coef[coef.lt(0)] = 0.0

# ノイズを加えつつダミーの目的変数を用意する.
noise = Rumale::Utils.rand_normal([n_samples, 1], rng)
y = x.dot(coef) + noise

# 訓練とテストに分割する.
x_train, x_test, y_train, y_test = Rumale::ModelSelection.train_test_split(x, y, test_size: 0.4, random_seed: 1)

# 非負最小二乗法を訓練し, 決定係数を計算する.
nnls = Rumale::LinearModel::NNLS.new(reg_param: 1e-4, random_seed: 1).fit(x_train, y_train)
puts(format("NNLS R2-Score: %.4f", nnls.score(x_test, y_test)))

# リッジ回帰を訓練し, 決定係数を計算する.
ridge = Rumale::LinearModel::Ridge.new(solver: 'lbfgs', reg_param: 1e-4, random_seed: 1).fit(x_train, y_train)
puts(format("Ridge R2-Score: %.4f", ridge.score(x_test, y_test)))

これを実行すると、以下の様になる。 決定係数が1に近いほど、回帰の当てはまりが良いといえる。 非負最小二乗法のほうが、リッジ回帰よりも決定係数の値が大きい。 わざと、回帰係数が非負のものを用意したので、当たり前なのだが、 非負値の制限を加えたほうが、当てはまりが良い場合がある。

$ ruby nnls.rb
NNLS R2-Score: 0.9478
Ridge R2-Score: 0.8602

また、非負最小二乗法による回帰は、スタッキングによる回帰において、 最終段階のメタ推定器に使用するのが有効であるという研究もある。

Breiman, L., "Stacked Regressions," Machine Learning, 24, pp. 49-64, 1996.

おわりに

非負最小二乗法による線形回帰を追加した。非負最小二乗法は、回帰係数の値域に制限を加えた回帰(Bounded-Variable Least-Squares)の一種といえる。Rumaleでは、非負最小二乗法を、制限付きの最適化ができるL-BFGS-Bで実装した。L-BFGS-Bのgemを作ったことで、実装できるアルゴリズムが広がった。その他、ver. 0.22.3では、native extensionsに、ガーベージコレクションによるsegmentation faultが起きる恐れのある箇所があり、それに対処したりした。

github.com

Rumaleにスタッキングによる分類器と回帰分析を追加した

はじめに

Ruby機械学習ライブラリであるRumaleに、スタッキング(stacking / stacked generalization)を用いた分類器と回帰分析を追加して、ver. 0.22.2としてリリースした。

rumale | RubyGems.org | your community gem host

使い方

Rumaleはgemコマンドでインストールできる。

$ gem install rumale

今回は高速化のためにparallelとnumo-openblasもインストールする(必須ではない)。

$ gem install parallel numo-openblas

スタッキングは、アンサンブル手法の一種で、一般的に学習器を二段階構成でつなげる。複数の学習器(first-level learner)を訓練し、それらの推定結果を特徴量として、最終的な推定をおこなう学習器(second-level learner / meta-leaner)を訓練する。これを実装しようとすると、割と大変というか、コードが冗長なものになる。そこで、専用のものを作った(scikit-learnでも ver. 0.22 で追加された)。

回帰分析を例に使ってみる。まずは、ランダム森による回帰分析を試す。データセットにはLIBSVM Dataのabaloneを用いた。

$ wget https://www.csie.ntu.edu.tw/~cjlin/libsvmtools/datasets/regression/abalone_scale
require 'numo/openblas'
require 'parallel'
require 'rumale'

# データセットを読み込む.
x, y = Rumale::Dataset.load_libsvm_file('abalone_scale')

# 訓練とテストに分割する.
x_train, x_test, y_train, y_test = Rumale::ModelSelection.train_test_split(x, y, test_size: 0.2, random_seed: 1)

# ランダム森を学習する.
reg = Rumale::Ensemble::RandomForestRegressor.new(n_jobs: -1, random_seed: 1)
reg.fit(x_train, y_train)

# 決定係数 (1に近づくほどよい) により回帰の精度を評価する.
puts reg.score(x_test, y_test)

これを実行すると、以下のようになる。

$ ruby regression.rb
R2-Score: 0.5224

これがスタッキングにより、どれだけスコアが向上するか試してみる。ランダム森による回帰を、以下のスタッキングに置き換える。一段目の学習機の種類やハイパーパラメータは適当なものである。

# ランダム森のところをスタッキングに置き換える.
# estimatorsにHashで特徴抽出に使う一段目の推定器を指定する.
# meta_estimatorに実際に推定結果を返す二段目の推定器を指定する.
reg = Rumale::Ensemble::StackingRegressor.new(
  estimators: {
    rnd: Rumale::Ensemble::RandomForestRegressor.new(n_jobs: -1, random_seed: 1),
    ext: Rumale::Ensemble::ExtraTreesRegressor.new(n_jobs: -1, random_seed: 1),
    grd: Rumale::Ensemble::GradientBoostingRegressor.new(n_jobs: -1, random_seed: 1),
    rdg: Rumale::LinearModel::LinearRegression.new
  },
  meta_estimator: Rumale::LinearModel::Ridge.new(reg_param: 10),
  random_seed: 1
)

これを実行すると、以下のようになる。ランダム森単体のときよりも、スコアが向上していることがわかる。

$ ruby regression.rb
R2-Score: 0.5751

さらにもう一歩スコアを伸ばしたい場合に、スタッキングを試してみるのは、有効であると考える。

おわりに

スタッキングはKaggleでよく使われ、アンサンブル手法として有名になった。研究自体は90年代初頭からあり、回帰分析では、二段目のメタ学習器の係数に、非負の制限をつけたほうが良いという知見もあったりする。次のバージョンで、非負最小二乗法による回帰を実装しても良いかなとか思ったり。

github.com