はじめに
簡単なNumo::NArrayを利用した拡張ライブラリを作った。拡張ライブリの作成については、公式のドキュメントが充実しているので、挑戦してみた。
- Gems with Extensions - RubyGems Guides
- ruby/extension.ja.rdoc at trunk · ruby/ruby · GitHub
- numo-narray/api.ja.md at master · ruby-numo/numo-narray · GitHub
準備
まずは、bundlerでgemの雛形を作る。ここで、--extオプションをつけると、拡張ライブラリ用のディレクトリやファイルも作られるし、gemspecファイルもいい感じに作られる。しかし今回は、勉強のために、そのあたりも手作業で行う。
$ bundle gem foo Creating gem 'foo'... MIT License enabled in config Code of conduct enabled in config create foo/Gemfile create foo/lib/foo.rb create foo/lib/foo/version.rb create foo/foo.gemspec create foo/Rakefile ...(省略) $ cd foo
gemspecファイルを編集する。spec.extensionsを追加し、rutime_dependencyにNumo::NArrayを、development_dependencyにrake-compilerを追加した。また、TODOの文字があると、bundle installでコケるので、適宜、変更したり、コメントアウトしたりした。
$ vim foo.gemspec
...(省略) spec.summary = %q{T: Write a short summary, because RubyGems requires one.} ...(省略) spec.extensions = ['ext/foo/extconf.rb'] spec.add_runtime_dependency 'numo-narray', '~> 0.9.1' spec.add_development_dependency 'rake-compiler', '~> 1.0' ...(省略)
Numo::NArrayやrake-compilerといった、必要なライブラリをインストールする。
$ bundle install Fetching gem metadata from https://rubygems.org/.......... ...(省略)
Rakefileにrake-compilerの設定を書く。ひとまず、最低限を追記した。
$ vim Rakefile
...(省略) require 'rake/extensiontask' Rake::ExtensionTask.new('foo') ...(省略)
拡張ライブラリの設定ファイルを作る。このとき、Numo::NArrayのヘッダーファイルをincludeパスに含める必要がある。これは本家のNumo::FFTWのものを参考にした。
$ mkdir -p ext/foo/ $ vim ext/foo/extconf.rb
require 'mkmf' require 'numo/narray' $LOAD_PATH.each do |lp| if File.exist?(File.join(lp, 'numo/numo/narray.h')) $INCFLAGS = "-I#{lp}/numo #{$INCFLAGS}" break end end create_makefile('foo/foo')
拡張の作成
今回作るのは、与えられたベクトルの要素を二乗したベクトルを返す、簡単なメソッドを持つクラスである。Numo::NArrayを利用した拡張では、イテレータ関数を定義して、ndfunc_t構造体にイテレータ関数や入出力を定義し、na_ndloop関数に渡す、というのが一連の流れとなる。このあたりは、公式のドキュメントに詳しくある。また、Numo関連のライブラリのソースを読むのも勉強になる。
$ vim ext/foo/foo.c
#include <stddef.h> #include <ruby.h> #include <numo/narray.h> #include <numo/template.h> /** * イテレータ関数を定義する * 配列aが入力で、配列bが出力である */ static void iter_f_bar(na_loop_t* const lp) { size_t n = lp->n[0]; double* a = (double*)(lp->args[0].ptr + lp->args[0].iter[0].pos); double* b = (double*)(lp->args[1].ptr + lp->args[1].iter[0].pos); size_t i; for (i = 0; i < n; i++) b[i] = a[i] * a[i]; } /** * 与えれたNumo::DFloatのベクトルを二乗するメソッドを定義した関数 * 第二引数のnaryがメソッドに与えられたNumo::DFloatのベクトル * 処理としては入出力を定義しイテレータ関数とともにna_ndloopにわたす */ static VALUE f_bar(VALUE self, VALUE nary) { ndfunc_arg_in_t ain[1] = {{numo_cDFloat, 0}}; ndfunc_arg_out_t aout[1] = {{numo_cDFloat, 0}}; ndfunc_t ndf = { iter_f_bar, NDF_STRIDE_LOOP, 1, 1, ain, aout }; return na_ndloop(&ndf, 1, nary); } /* Init_xxxが最初に呼ばれる関数となる */ void Init_foo() { /** * 今回はFooモジュール以下にBarというクラスを用意した * そのBarクラスにNumo::DFloatなベクターの要素を二乗するbarメソッドを追加した */ VALUE mFoo, cBar; /* Fooモジュールを用意する */ mFoo = rb_define_module("Foo"); /* Fooモジュール以下にBarクラスを定義する */ cBar = rb_define_class_under(mFoo, "Bar", rb_cObject); /* Barクラスに引数を1つ受け取るメソッドbarを定義する */ rb_define_method(cBar, "bar", f_bar, 1); }
lib/foo.rbで拡張をrequireする。
$ vim lib/foo.rb
...(省略) require 'numo/narray' require 'foo/foo' ...(省略)
動作確認のためのspecを書く。
$ vim spec/bar_spec.rb
RSpec.describe Foo::Bar do let(:x) { Numo::DFloat[2, 3, 4, 5] } let(:y) { Numo::DFloat[4, 9, 16, 25] } it 'returns an array consisting of the squared of elements of the given array.' do bar = described_class.new expect(bar.bar(x)).to eq(y) end end
動作確認
rakeコマンドでコンパイルして、rspecで動作を確認する。
$ rake compile:foo mkdir -p tmp/x86_64-darwin18/foo/2.3.8 cd tmp/x86_64-darwin18/foo/2.3.8 /Users/atatsuma/.rbenv/versions/2.3.8/bin/ruby -I. ../../../../ext/foo/extconf.rb creating Makefile ...(省略) $ rspec spec/bar_spec.rb Foo::Bar returns an array consisting of the squared of elements of the given array. Finished in 0.00432 seconds (files took 0.47124 seconds to load) 1 example, 0 failures
無事にspecが通って、拡張によりNumo::DFloatのベクトルが二乗されていることがわかる。 実際にgemをインストールしてみての動作確認も行った。gemspecファイルのspec.filesにextディレクトリ以下のファイルが追加されている必要がある。bundleコマンドで自動的に作成されたgemspecファイルでは「git ls-files」した結果からspec.filesを作成している。そのため、extディレクトリ以下をgit addする必要がある。
$ git add ext/foo/extconf.rb $ git add ext/foo/foo.c $ rake install foo 0.1.0 built to pkg/foo-0.1.0.gem. foo (0.1.0) installed. $ irb
与えたNumo::DFloatのベクトルの要素を二乗した、新たなNumo::DFloatのベクトルが返ってきているのがわかる。
irb(main):001:0> require 'foo' => true irb(main):002:0> a=Foo::Bar.new => #<Foo::Bar:0x00007fd08885f560> irb(main):003:0> v=Numo::DFloat[10, 20, 30] => Numo::DFloat#shape=[3] [10, 20, 30] irb(main):004:0> a.bar(v) => Numo::DFloat#shape=[3] [100, 400, 900] irb(main):005:0>
おわりに
簡単なNumo::NArrayを使った拡張ライブラリを作ってみた。今回は、勉強のために、準備の部分を手作業で行ったが、bundle gemで--extオプションをつけるのが良いと思う。
Rubyの機械学習ライブラリRumaleの開発で、基本的なアルゴリズムは実装できたので、実行速度の向上を計画している。決定木などの行列・ベクトル演算で書けないアルゴリズムでは、どう工夫しても実行速度を上げられない部分があり、そのあたり、一部だけ拡張ライブラリ化することを考えている。scikit-learnもCythonで書かれているので、避けては通れない道なのかも。