洋食の日記

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

近似最近傍探索ライブラリAnnoyのRuby bindingを作成した

はじめに

Annoy (Approximate Nearest Neighbors Oh Yeah) は、C++で書かれた近似最近傍探索ライブラリである。近似最近傍探索とは、その名のとおり、クエリに対して厳密ではなく近似的に近傍にあるものを探索する。高速に探索できるので、大規模データの画像の類似検索などに使われる。インデックスの木の構築では、データ数を二分割するような2点を選び、それらでできる超平面で分割することを繰り返す。k-means tree (vocabulary tree) をアレンジしたような手法になっている。Annoyでは、Python bindingが提供されていて、pipでインストールできる。そのため、Pythonライブラリなイメージがあるが、実際は一つのヘッダーファイルからなるシンプルなライブラリである。これを、native extensionsから叩くかたちでgemにした。

annoy-rb | RubyGems.org | your community gem host

使い方

インストールは、普通にgemコマンドでインストールできる。特別な外部ライブラリもPythonも必要ない。

$ gem install annoy-rb

APIは、AnnoyのPython bindingに合わせた。Python版を使ったことがあれば、すぐに使えると思う。

まずは、検索インデックスを作成する。AnnoyIndexをnewする際に、ベクトルの次元数と、距離基準を指定する(angularはコサイン距離で、他にユークリッド距離やマンハッタン距離などがある)。add_itemメソッドで、データの番号とRuby Arrayによるベクトルを登録していく。そして、buildメソッドで検索インデックスを作成する。作成したインデックスはsaveメソッドで保存できる。

require 'annoy'

n_features = 40 # 検索インデックスに追加するベクトルの次元数
t = Annoy::AnnoyIndex.new(n_features: n_features, metric: 'angular')

# ランダムな値からなるベクトルを1000件追加する.
1000.times do |i|
  v = Array.new(n_features) { rand }
  t.add_item(i, v)
end

# インデックスを構築する. この際, 構築する木の数を指定できる.
# 木の数は, 多いほど検索精度は上がるが遅くなる.
n_trees = 10
t.build(n_trees)

# インデックスを保存する.
t.save('foo.ann')

検索は、get_nns_by_itemとget_nns_by_vectorメソッドで行う。get_nns_by_itemは、検索インデックス中のベクトルを番号で指定し、それをクエリとして検索を行う。get_nns_by_vectorは、クエリとして任意のベクトルをRuby Arrayで与えて検索を行う。

require 'annoy'

# 作成した検索インデックスを読み込む.
n_features = 40
u = Annoy::AnnoyIndex.new(n_features: n_features, metric: 'angular')
u.load('foo.ann')

# アイテム番号0番に近い10件を検索する.
# 近傍とされるアイテムの番号がRuby Arrayで返る.
neighbor_ids = t.get_nns_by_item(0, 10)

# 任意のベクトルに近い10件を検索する.
# include_distancesにtrueを与えると, 距離も返す.
q = Array.new(n_features) { rand }
neighbor_ids, neighbor_dists = t.get_nns_by_vector(1, 10, include_distances: true)

おわりに

Annoyをbindingしたgemを作成した。シンプルにnative extensionsで繋いでるだけで(コードも300行程度)、今後大きくコードを改善する箇所はあまりないが、bugfixやAnnoy側のアップデートなどに対応するなど、ちょこちょこ面倒を見ていきたい。

github.com

Numo::OpenBLASでOpenBLASのダウンロードとビルドのタイミングを変えた

はじめに

Numo::OpenBLASで、インストール時のOpenBLASのダウンロードとビルドを、native extensionの作成時に行うようにして、version 0.2.0 としてリリースした。

numo-openblas | RubyGems.org | your community gem host

使い方

使い方に変更はない。Gemコマンドでインストールできる。インストール時に、OpenBLASをダウンロードしてビルドする。

$ gem install numo-openblas

使用方法はrequireするのみで、Numo::NArrayとNumo::Linalgもrequireされる。

require 'numo/openblas'

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

また、本 version から、OpenBLASのビルドオプションの一部が確認できるようになった。

> require 'numo/openblas'
=> true
> Numo::OpenBLAS::OPENBLAS_VERSION
=> " OpenBLAS 0.3.10 "
> Numo::OpenBLAS::OPENBLAS_CHAR_CORENAME
=> "HASWELL"
> Numo::OpenBLAS::OPENBLAS_NUM_CORES
=> 8

RubyGemsのhooks

前バージョンまでは、RubyGemsのpost_install hookを使って、インストール時のOpenBLASのダウンロードとビルドを行っていた。このRubyGemsのhooksが、動かないことがあるという話があり、自分でも体験したので、どうしたものかな〜と思っていた。また一方、別の話で、OpenBLASのビルド時に自動設定されるオプション(CPUの名前とかコア数とか)が、openblas_config.hに書かれるので、これを参照できないかな〜と思っていた。

この2つを解決するために、native extensionを導入することにした。native extensionをビルドする際に、OpenBLASのダウンロードとビルドを行うようにした(Makefileを作成するextconf.rbにpost_install hookに書いていたものを移植した)。native extensionのビルドは確実に動くので、いい感じになったと思う。ただし、使う側としては「native extensionのビルドがスゴイ重い!!」みたいに見えるようになった。

おわりに

numpyでも、pipでインストールするかcondaでインストールするかで、バックエンドライブラリが違うとかあって、線形代数ライブラリにとって、BLAS/LAPACKをどう使うかは永遠の課題なんだろうなと思う。

github.com

Rumaleからdeprecatedにしてた分類器などを削除した

はじめに

RumaleからFactorization MachineやNadamなど、deprecatedにしていたものを削除して version 0.20.0 としてリリースした。本 version で分類器などの追加はない。シンプルに削除だけでバージョンを切った。

rumale | RubyGems.org | your community gem host

徒然なるままに

Rumaleを良い意味で普通の機械学習ライブラリにするため、過去に個人的な興味で追加したものをdeprecatedにして、時間をおいて削除した。Factorization Machine(FM)は、Rumale(SVMKit)の初期からあるもので、派生アルゴリズムを追加するつもりだった。また、最近の確率的勾配降下法(Stochastic Gradient Descent, SGD)の発展を上手く利用して、FMを実装できないかという興味もあった。ただ、多くの包括的な機械学習ライブラリで、FMは外部ライブラリで拡張するものだったりするので、これにあわせることにした。同じくOptimizerも、Yellow Finなんかを実装したりもしたが、ここをいたずらに増やしてもな、というところで削除した。ちなみに、Rumaleでは、ロジスティック回帰やLassoなどは標準的なSGDで、多層パーセプトロンはAdamで学習するようになっている。Optimizerの削除は、これらには影響しない。FMは色んなバリエーションがあっておもしろいので、Rumale-FMとかそういう名前で、ライブラリを作ってみたい。

おわりに

Rumaleは、機械学習ライブラリとして、それなりにアルゴリズムが充実してきた。それをうけて、最近のRumaleの開発は、ユーティリティ的なモノの追加を進めている。やみくもに大きくしたくないのもある。また、Rumaleを中心として周辺ライブラリの作成もガンバらないとな、と思っている。

github.com

Windows10でRubyな開発環境を得るためにやったこと

はじめに

Windowsでの動作確認のため、その昔Windows 7Windows 8にし、そしてWindows 10にした、古いラップトップをひっぱりだして、Rubyの環境構築を行った。Windowsでの開発に関して、知識がゼロだったので、10回はインストールとアンインストールを繰り返したと思う。連休を返してほしい。

前提

Windowsで本格的に開発したいわけではなく、公開してるGemの動作確認をしたいだけだが、Githubからコードをとってきて軽微な修正もしたい。そしてエディタはvimかneovimを使いたい。WSLを使えるのが一番だが、GithubのIssueで報告されてるエラーは、本物のWindows10な環境で起こるみたいだ。

結論

全部MSYS2でまかなう。CygwinやRubyInstallerは使わなかった。

やったこと

まず、MSYS2のインストーラをダウンロードしてきて、ウィザードにしたがって何も考えずインストールする。

MSYS2

スタートメニューには3種類のMSYS2が用意されるが、64bitなMinGWでターミナルを起動する。そして、必要なパッケージをpacmanでインストールする。neovimはなかった。

$ pacman -S mingw-w64-x86_64-toolchain # これでgccとかが一通りはいる
$ pacman -S mingw-w64-x86_64-ruby
$ pacman -S vim tmux git

ここで、pacman -S rubyでインストールしたRubyでは、native extensionを含むGemで、ビルドに失敗することがあった。注意したい。

さらに、Numo::LinalgとかMagroのために、関連するライブラリをインストールする。

$ pacman -S mingw-w64-x86_64-openblas mingw-w64-x86_64-lapack
$ pacman -S mingw-w64-x86_64-libpng mingw-w64-x86_64-libjpeg

ちなみに、ターミナルもカスタマイズした。アイコンを右クリックでオプションがでる。半透明にしたり、フォントを変えたりできる。フォントはCicaにした。ビルドせずに、GithubのReleaseからzipをダウンロードした。

github.com

これで、簡単なコード修正も含む、Rubyな環境がWindows10に用意できた。

おわりに

文字に起こしてみると、大したことしてなかった。ここにたどり着くまでに、試行錯誤を繰り返して、貴重な休日を溶かしたのに。書き忘れていたが、まず最初に、Windows Updateで再起動を繰り返す、というのがある。

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

はじめに

Numo::OpenBLASという、タイトルのとおりのものを作成した。

numo-openblas | RubyGems.org | your community gem host

使い方

Gemコマンドでインストールできる。このとき、OpenBLASをダウンロードしてビルドする。

$ gem install numo-openblas

使用方法はrequireするのみで、Numo::NArrayとNumo::Linalgもrequireされる。

require 'numo/openblas'

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

作った理由

Numo::Linalgでは、バックグラウンドライブラリのBLAS/LAPACKを選択することができる。しかし、この設定を間違うと、ライブラリを読み込めず失敗する。これに対して、以前、Autoloaderというものを作成した。これは、/usr/local/libなど、BLAS/LAPACKがいそうなディレクトリを探して、バックグラウンドライブラリとして設定する。OpenBLASをインストールしてある場合、だいたいが、これでうまくいくが、パッケージシステムによって、OpenBLASのビルドオプションが異なるという問題がある。OpenBLASはビルド時のオプションで、純粋にBLASAPIだけを提供するようにできる。これでビルドされた場合、Numo::Linalgのバックエンドライブラリとしては、OpenBLASの他にLAPACKも必要となる。このあたりどうなっているかを、利用者は把握しておく必要がある。また、Autoloaderは、実行速度のために、当たりをつけて検索しているので、ライブラリを見つけられないこともある。

対策法

上記のバックエンドライブラリの問題に対して、Gemのインストール時に、OpenBLASのダウンロードとビルドを行い、それをバックエンドライブラリとしてNumo::Linalgをロードすることにした。いまのところ、Rubyがインストールできたなら、OpenBLASをビルドできるであろう(gccとかなにかしらあるだろう)と想定している。

終わりに

Pythonのnumpyは、Anacondaのサーバーに各種プラットフォームでビルドされたOpenBLASがある。これをダウンロードすることも考えたがやめた。Rubyのデータ分析・機械学習が広まって、一定のコミュニティが形成された後に、Numo::Linalg用として提供されるのが理想かなと思った。また、OpenBLASは、CPUにあわせてビルドを行う。利用者の環境でビルドしたほうが、本来のパフォーマンスが出せるのではないかと思った。もろもろいい感じにしたDocker Imageを用意すれば良いんだろうけど、pip install numpy みたいな感じで、Numo::NArrayとNumo::Linalgをgemコマンドからインストールして、サクッと使いたかった(OpenBLASのビルドに時間がかかるけど...)。

github.com