洋食の日記

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

インストール時にBLISをビルドしてNumo::Linalgのバックグラウンドライブラリに使用するGemを作成した

はじめに

Numo::OpenBLASのBLIS版を作成した。Numo::OpenBLASを作ったのが、だいたい1年前だった。

インストール時にOpenBLASをビルドしてNumo::Linalgのバックグラウンドライブラリに使用するGemを作成した - 洋食の日記

BLISは、最適化BLASの一種で、線形代数計算に対するモダンなAPIも提供している。条件によっては、OpenBLASよりも高速に計算できる様だ。

GitHub - flame/blis: BLAS-like Library Instantiation Software Framework

blis/Performance.md at master · flame/blis · GitHub

作ったきっかけは、Numo::OpenBLASに寄せられた「BLISをつかうオプションがあると良いんじゃない?」というIssueで、同じ層のNumo::OpenBLASにBLISを混ぜるのは文脈的にややこしいので、別のGemにした。最初は、Numo::Linalg::LoaderでBLISを読み込めるようにするPull Requestを作ろうと思ったが、各OSのパッケージでBLISのビルドのオプションにバラつきがあったので、Numo::OpenBLASのように、Gemのインストール時にダウンロードとビルドを行う専用のモノを用意することにした。

使い方

普通にgemコマンドでインストールできる。インストール時に、BLISとLAPACKをダウンロードしてビルドするので、ちょっと時間が必要になる。

$ gem install numo-blis

使用方法はNumo::BLISをrequireするのみで、内部的にNumo::NArrayとNumo::Linalgがrequireされて、ビルドしたBLISとLAPACKがバックグラウンドライブラリに使用される。

require 'numo/blis'

x = Numo::DFloat.new(5, 2).rand
c = x.transpose.dot(x)
eig_val, eig_vec = Numo::Linalg.eigh(c)

Numo::BLISとしては、これだけである。

Numo::BLISが行っていること

Numo::BLISがインストール時に行っていることを、手動でやってみる。BLIS・LAPACKのビルドと、Numo::Linalgでの読み込みである。

まず、BLISとLAPACKGithubからダウンロードする。

$ wget https://github.com/flame/blis/archive/refs/tags/0.8.1.tar.gz
$ tar xvzf 0.8.1.tar.gz
$ wget https://github.com/Reference-LAPACK/lapack/archive/refs/tags/v3.10.0.tar.gz
$ tar xvzf v3.10.0.tar.gz

BLISではconfigureでビルドオプションを指定する。helpオプションでどういうものがあるか確認する。

$ cd blis-0.8.1
$ ./configure --help
...
   -t MODEL, --enable-threading[=MODEL], --disable-threading

                 Enable threading in the library, using threading model
                 MODEL={openmp,pthreads,no}. If MODEL=no or
                 --disable-threading is specified, threading will be
                 disabled. The default is 'no'.
...
   --enable-cblas, --disable-cblas

                 Enable (disabled by default) building the CBLAS
                 compatibility layer. This automatically enables the
                 BLAS compatibility layer as well.
...

重要なのが、マルチスレッドとCBLAS互換に関するオプションがデフォルトで無効になっている点である。特に、CBLAS互換がないとNumo::Linalgでうまく動作してくれないので、これは有効にする必要がある。そんなわけで、configureは次のようになる。後ろについている「auto」は、CPUアーキテクチャを自動で検出してね、というモノである。

$ mkdir build; cd build
$ ../configure --enable-cblas --enable-threading=pthreads --prefix=インストール先ディレクトリ auto
...
$ make
$ make install

BLISのビルドができたので、次はLAPACKをビルドする。LAPACKではcmakeを使う。

$ cd lapack-3.10.0
$ mkdir build; cd build

BLASに先程ビルドしたBLISを指定する必要がある。CMakeのFindBLASを使っている様なので、BLAS_LIBRARIESでパスを指定すればよい。 その他のオプションは、CMakeLists.txtを見て確認した。Numo::LinalgはCインターフェースのLAPACKEを必要とするので、LAPACKEオプションをONにする。

$ cmake -DLAPACKE=ON -DBUILD_SHARED_LIBS=ON -DCMAKE_INSTALL_PREFIX='インストール先ディレクトリ' -DBLAS_LIBRARIES='先程ビルドしたBLISをlibblas.soとかまで指定する' ../
...
$ make
$ make install

あとはこれらをNumo::Linalgで読み込むと、使えるようになる。

require 'fiddle'
require 'numo/linalg/linalg'

Fiddle.dlopen('/hoge/fuga/lib/libblis.so')
Fiddle.dlopen('/hoge/fuga/lib/liblapacke.so')
Numo::Linalg::Blas.dlopen('/hoge/fuga/lib/libblis.so')
Numo::Linalg::Lapack.dlopen('/hoge/fuga/lib/liblapacke.so') 

ここで、注意が必要なのが、BLISは環境変数でスレッド数を指定しないといけないことである。指定しないとシングルスレッドになってしまう。

BLIS_NUM_THREADS=8 ruby hoge.rb

Numo::BLISでは、ENV['BLIS_NUM_THREADS']で確認していて、これがnilであれば、Etc.nprocessorsをセットしている。

おわりに

Numo::OpenBLASをベースにすれば良いので、BLISのビルド方法を調べて、一日でガッと作った。未だ腱鞘炎に悩まされていて、大した開発ができてないのだが、久しぶりにgemを作って、良いストレス解消になった。

RBSでメソッドをオーバーロードするときは三点リーダーを書く

はじめに

タイトルのとおり。RBSによる型注釈が提供されているメソッドをmonkey patchして、その型注釈をpatch.rbsに書きたい時がある。Rubyのノリで書くと、重複定義エラーになる。

ディレクトリ構成

以降の文章は、次のようなディレクトリ構成で、

|- lib
|   `- hoge.rb
|- sig
|   |- patch.rbs
|   `- hoge.rbs
|- Gemfile
`- Steepfile   

bundle exec steep check が動くような感じで、ファイルを用意している。

# Gemfile
source 'https://rubygems.org'

gem 'rbs', '~> 1.2'
gem 'steep', '~> 0.44'
# Steepfile
target :lib do
  signature "sig"

  check "lib"
end

たとえば

Javascriptみたいに実数と文字列の足し算をRubyでもしたいとする(本当はしたくない)。

> 3.1 + '4'
"3.14"

そのために、Floatクラスをmonkey patchする。

# lib/hoge.rb

class Float < Numeric
  alias_method :_org_add, :+

  def + (b)
    return self.to_s + b if b.is_a?(String)
    _org_add b
  end

  private :_org_add
end

これで実数と文字列の足し算ができる。

$ irb
irb(main):001:0> require_relative 'lib/hoge.rb'
=> true
irb(main):002:0> 3.1 + '4'
=> "3.14"
irb(main):003:0> 3.1 + 4
=> 7.1

型注釈を書く

以上のmonkey patchに対してRBSを書くとすると、まず、以下のようなものが思いつく。

# sig/patch.rbs

class Float < Numeric
  alias _org_add +

  def +: (Complex) -> Complex
       | (Numeric) -> Float
       | (String) -> String
end

これで bundle exec steep check すると、重複定義エラーになる。 Floatがcore/float.rbshttps://github.com/ruby/rbs/blob/master/core/float.rbs)で定義されているためである。

(前後省略)

gems/rbs-1.2.1/core/float.rbs:40:2: [error] Non-overloading method definition of `+` in `::Float` cannot be duplicated
│ Diagnostic ID: RBS::DuplicatedMethodDefinition
│
└   def +: (Complex) -> Complex

解決策

三点リーダーを書いて、オーバーロードであることを示す。

# sig/patch.rbs

class Float < Numeric
  alias _org_add +

  def +: (Complex) -> Complex
       | (Numeric) -> Float
       | (String) -> String
       | ... # オーバーロードであると示す
end

これで重複定義エラーがでなくなる。

$ bundle exec steep check
# Type checking files:

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

No type error detected. 🫖

おわりに

Rubyはmonkey patchが容易なので、よく出くわすエラーだと思う。公式の文法ドキュメントを見て考えたが、実はガイドの方に書いてありました: rbs/sigs.md at master · ruby/rbs · GitHub

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

はじめに

タイトルのとおり。色々あって仕事でのタイプ量が急増したために、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