洋食の日記

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

Numo::NArrayを扱ったRubyの拡張ライブラリの作りかた

はじめに

簡単なNumo::NArrayを利用した拡張ライブラリを作った。拡張ライブリの作成については、公式のドキュメントが充実しているので、挑戦してみた。

準備

まずは、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で書かれているので、避けては通れない道なのかも。

github.com