洋食の日記

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

ひどい腱鞘炎になったので型注釈を書いている

はじめに

タイトルのとおり。色々あって仕事でのタイプ量が急増したために、4月末頃からヒドい腱鞘炎に悩まされている。手首から先の曲がる箇所は全て曲げると痛い。朝起きてしばらくは、指が痛みで曲げられないほどで、仕事では、サポーターで手首から先を固めてタイプしている。整形外科でもらった塗り薬を辛抱に塗り続けているが、おそらく無職になって1ヶ月とか完全に休むほうが効果あるんだろうな...

当然ながら、プライベートの開発は、以前と同じペースでできない状態にある。この記事も少しずつ書いた。趣味の格ゲーも禁じ、休日はダラダラと過ごしていたのだが、それだと生活が無気力な感じになってしまい、精神面で良くないと考え、無理のない範囲でできることをやることにした。

RBSは、Rubyコードの型注釈を記述するもので、Ruby3で話題になったが、Gemをインストールすれば2.6や2.7でも利用できる。RBSは、rbsコマンドによりスケルトンコードを自動生成できるので、バリバリタイプする必要はない。そんなわけで、自分が作成しているGemのRBSを、少しずつ書いていくことにした。

RBSとSteepの準備

SteepはRBSをもとに型検査をおこなう。これらをインストールする。

$ gem install rbs steep

Steepの設定ファイルであるSteepfileを用意する。以下であれば、sigディレクトリ以下にあるRBSファイルをもとに、libディレクトリ以下のrbファイルを検査するよ、という意味になる。

$ steep init
$ vim Steepfile
target :lib do
  signature "sig"

  check "lib"
  
  # ...
end

RBSファイルの自動生成

rbsコマンドで、ベースとなるRBSファイルを自動生成する。普通のRubyコードであれば以下で、だいたいいい感じのものができる。

$ rbs prototype rb lib/hoge.rb > sig/hoge.rbs

native extensionsを使っているのであれば、runtimeを使うとよい。例えば、hoge gemのHogeクラスがnative extensionsを利用しているとしたら、以下のようになる。

$ rbs prototype runtime --require 'hoge' Hoge > sig/hoge.rbs

RBSが書かれいてるGemは、まだまだ少ない。依存しているGemのRBSがない場合は、patch.rbsに必要なものを書くとよい。例えば、Rumaleをはじめ、私が公開しているGemの多くが、Numo::NArrayに依存しているが、以下のようにしてpatch.rbsを用意している(実際はGemで使用しているメソッドだけを抜き出した簡易的なものを用意している)。

$ rbs prototype runtime --require 'numo/narray' Numo::NArray > sig/patch.rbs

これでSteepで型検査ができる。

$ steep check
# Type checking files:

............................................................

No type error detected. 🧋

あとは、RBSが型を推定できず、untypedとしたものを、いい感じに補っていくだけで型注釈ができあがる。

おわりに

少しずつ書いてるうちに、Rumale系のGem(Rumale, Rumale::SVM, Rumale::Torch)をのぞいて、型注釈を用意することができた。無理に型注釈をつけようとせず、untypedを許容する心のゆとりを持つことが、コツな気がする。

RumaleのRBSを書くのが、この作業のラスボスになるが、量が多いのと、コードの修正も予想されることから、時間がかかりそうだ。しかし、Pythonであれば、例えばNumpyやThinc(spaCyで利用されている深層学習ライブラリ)などが型注釈に対応しているので、Ruby機械学習ライブラリとしてなんとか対応したい(Typed Ruby Machine Learningというキャッチフレーズだけ思いついている)。

なによりも、健康な体を取り戻すことが大事で...近況報告でした。

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