CentOS7(x86_64) で Windows版 numo-narray (x86-mingw32/x64-mingw32)を build する場合の手順

numo-narray の Windows 版は linux でクロスコンパイルし、そのまま配布する fat gem の作成が可能なのですが、 ただ、その方法でハマったので対処方法のメモです。

具体的には numo-narray の Windows 向けビルドで rake-compiler.gem, rake-compiler-dock.gem を使うようになっているのですが、 docker コマンドの操作に root権限が必要なため、sudo 経由で rbenv を実行しないと docker コマンドの操作でdocker グループにユーザーを追加しておかないと、docker のステータスを認識できずに処理が止まります。

rake-compiler/rake-compiler-dock の詳細は下記参照。

主な作業

  • ruby 2.1 以上を rbenv でインストール (手順は割愛)
    • ここでハマったのですが、自分はホームディレクトリに rbenv を入れていたため sudo 実行時 rbenv コマンドを認識できませんでした。
    • 今回のケースはシステム側に rbenv をインストールした方がいいと思います。(未確認)
  • sudo の secure_path 変更

    $ sudo visudo

    • 修正前 Defaults secure_path = /sbin:/bin:/usr/sbin:/usr/bin
    • 修正後 Defaults secure_path = /sbin:/bin:/usr/sbin:/usr/bin:/usr/local/rbenv/bin:/usr/local/rbenv/shims:/home/ユーザー名/.rbenv/bin:/home/ユーザー名/.rbenv/shims
      • /home/ユーザー名/.rbenv/bin:/home/ユーザー名/.rbenv/shims の部分は、gem env コマンドで SHELL PATHを確認。
      • システム側に rbenv をインストールした場合は /home/ユーザー名/.rbenv/bin:/home/ユーザー名/.rbenv/shims 部分は不要なはず。
  • Docker インストール & グループ追加 & 起動 & 一旦ログアウトして反映。

$ sudo yum install docker
$ sudo groupadd docker
$ sudo gpasswd -a $USER docker
$ sudo systemctl start docker
$ exit
  • Docker のサービス確認。
$ sudo systemctl status docker.service 

Docker のサービスがactive なのを確認する。

  • Docker コマンド動作確認。
$ docker ps

sudo なしに実行してエラーしないことを確認。

  • gem インストール
$ gem install rake-compiler
$ gem install rake-compiler-dock
  • numo-narray を git clone
$ git clone https://github.com/ruby-numo/numo-narray.git
$ cd numo-narray

sudo 経由で rake コマンドが動作することを確認。

  • Windowsビルド 管理者権限でビルドを行います。(ここでハマっていた。) docker グループに入って入れば管理者権限は不要です。
$ rake build:windows

ビルドが完了し、pkg 配下に numo-narray-x.x.x.x-x86-mingw32.gem, numo-narray-x.x.x.x-x64-mingw32.gem が作られたら成功。

追記

先ほど、@sonots さんから、docker グループに追加すべきとコメント頂き、sudo は不要でしたので記事を修正しました。手順がスッキリしました。ありがとうございます。

numo-narray と benchmark_driver.gem

@watson1978 さんの blog で Mac 環境だと Linux 環境に比べて numo-narray が遅いというお話があったので調べてみました。

watson.hatenablog.com

今回の比較では、先日の Rubyアソシエーション開発助成成果報告会 で発表されていた

www.ruby.or.jp

@k0kubun さんの benchmark_driver.gem を使いました。

このベンチマークは、複数の Ruby バージョン間の比較や、同一 gem で複数のバージョン比較ができるという優れものです。

使い方は $ gem install benchmark_driver でインストールするだけです。
(rbenv を使用している場合、$ rbenv rehash で benchmark-driver コマンドを使用可能にする必要があります。)
グラフ画像を出力する場合は $ gem install benchmark_driver-output-gruff で追加の plugin をインストールします。 (要 RMagick)

確認環境

ベンチマーク内容

@watson1978 さんのベンチマーク結果をベースに inplace 演算と ブロードキャスト演算を加えて比較してみました。

$ cat numo_N_fp.yaml
contexts:
  - gems: { numo-narray: 0.9.1.2 }
    require: false
    prelude: require 'numo/narray'
  - gems: { numo-narray: 0.9.9.0 }
    require: false
    prelude: require 'numo/narray'

loop_count: 10000
prelude: |
  N = 100000
  xd = Numo::DFloat.new(N).seq
  yd = Numo::DFloat.new(N).seq
  xs = Numo::SFloat.new(N).seq
  ys = Numo::SFloat.new(N).seq
benchmark:
  '[100000] + [100000] (fp64)'        : xd         + yd
  '[100000] + [100000] (fp32)'        : xs         + ys
  '[100000].inplace + [100000] (fp64)': xd.inplace + yd
  '[100000].inplace + [100000] (fp32)': xs.inplace + ys
  '[100000] + 1 (fp64)'               : xd         + 1
  '[100000] + 1 (fp32)'               : xs         + 1
  '[100000].inplace + 1 (fp64)'       : xd.inplace + 1
  '[100000].inplace + 1 (fp32)'       : xs.inplace + 1
  • inplace 演算は指定した変数のメモリ領域を書き換える形で演算するため、結果的に省メモリになります。0.9.9.0(省メモリによる高速化版)で省メモリ化しています。
  • ブロードキャスト演算は指定した変数を全要素に対して同じ計算(上記の場合は+1)をします。0.9.9.0(省メモリによる高速化版)で省メモリ化しています。

上記を下記のように実行します。

  • $ benchmark-driver numo_N_fp.yaml --output markdown (Markdown 出力)
  • $ benchmark-driver numo_N_fp.yaml --output gruff (PNG出力)

CentOS / macOS の結果 (グラフ)

CentOS (左) / macOS (右) の結果

  • 1秒間あたりの処理数 (i/s)なので、数値が大きい方が速いことを意味します。

  • 非inplace の場合、Linux と比較してMac は 3〜4倍遅い

  • 通常演算(非inplace & 非broadcast)から inplaceやbroadcastに変更した場合、Linuxは1.5〜2倍速くなりますが、Macは4〜11倍速くなりLinuxより速い
  • サンプルソースを作成してアセンブラ出力を確認したところ、Macコンパイラ(Clang)が自動ベクトル(SIMD)化している。
    • Linux の場合、DFloat → SFloat or inplace & broadcast 改善版(0.9.9.0) で、省メモリになりキャッシュに乗ることで高速化。
    • Mac の場合、inplace & broadcast 改善版(0.9.9.0) で、省メモリになりキャッシュに乗りSIMDが効果を発揮することで劇的に高速化。
      • SIMDでは変数をまとめて演算するため、DFloat(8Byte)×2個=16Byte → SFloat(4Byte)×4個=16Byte となりメモリサイズは同一。(省メモリにはならない。)

CentOS 7 の結果 (数値) (※グラフ化とは別に再測定実施。)

numo-narray 0.9.1.2 numo-narray 0.9.9.0
[100000] + [100000] (fp64) 7.150k 7.040k
[100000] + [100000] (fp32) 12.768k 12.692k
[100000].inplace + [100000] (fp64) 12.161k 11.572k
[100000].inplace + [100000] (fp32) 17.069k 22.032k
[100000] + 1 (fp64) 7.936k 8.045k
[100000] + 1 (fp32) 9.273k 12.697k
[100000].inplace + 1 (fp64) 11.570k 14.160k
[100000].inplace + 1 (fp32) 12.875k 27.566k

macOS Sierra の結果 (数値) (※グラフ化とは別に再測定実施。)

numo-narray 0.9.1.2 numo-narray 0.9.9.0 (master)
[100000] + [100000] (fp64) 1.897k 1.829k
[100000] + [100000] (fp32) 3.867k 3.753k
[100000].inplace + [100000] (fp64) 12.431k 15.041k
[100000].inplace + [100000] (fp32) 17.005k 29.455k
[100000] + 1 (fp64) 2.093k 2.126k
[100000] + 1 (fp32) 3.664k 4.101k
[100000].inplace + 1 (fp64) 13.843k 23.250k
[100000].inplace + 1 (fp32) 14.338k 44.210k

以上より、

  • Numo::NArrayが Mac で遅いケースがあるが自動ベクトル(SIMD)化とキャッシュの乗り方に起因する現象のように思われる。
  • 可能なら inplace を使いましょう。

というところでしょうか。

四則演算のベンチマーク (おまけ)

ついでに、RubyKaigi 2018 LTベンチマーク結果を、非 inplace のケースも含めてbenchmark_driver.gemベンチマーク化してみました。

ベンチマーク内容

$ cat broadcast_fp32.yaml
contexts:
  - gems: { numo-narray: 0.9.1.2 }
    require: false
    prelude: require 'numo/narray'
  - gems: { numo-narray: 0.9.9.0 }
    require: false
    prelude: require 'numo/narray'

loop_count: 5000
prelude: |
  x = Numo::SFloat.ones([1000,784])
  y = Numo::SFloat.ones([1000,784])
  z = Numo::SFloat.ones([1000,1])
benchmark:
  '[1000,784]         + [1000,784]': x         + y
  '[1000,784].inplace + [1000,784]': x.inplace + y
  '[1000,784]         + [1000,1]  ': x         + z
  '[1000,784].inplace + [1000,1]  ': x.inplace + z
  '[1000,784]         + 1         ': x         + 1
  '[1000,784].inplace + 1         ': x.inplace + 1
  '[1000,784]         - [1000,784]': x         - y
  '[1000,784].inplace - [1000,784]': x.inplace - y
  '[1000,784]         - [1000,1]  ': x         - z
  '[1000,784].inplace - [1000,1]  ': x.inplace - z
  '[1000,784]         - 1         ': x         - 1
  '[1000,784].inplace - 1         ': x.inplace - 1
  '[1000,784]         * [1000,784]': x         * y
  '[1000,784].inplace * [1000,784]': x.inplace * y
  '[1000,784]         * [1000,1]  ': x         * z
  '[1000,784].inplace * [1000,1]  ': x.inplace * z
  '[1000,784]         * 1         ': x         * 1
  '[1000,784].inplace * 1         ': x.inplace * 1
  '[1000,784]         / [1000,784]': x         / y
  '[1000,784].inplace / [1000,784]': x.inplace / y
  '[1000,784]         / [1000,1]  ': x         / z
  '[1000,784].inplace / [1000,1]  ': x.inplace / z
  '[1000,784]         / 1         ': x         / 1
  '[1000,784].inplace / 1         ': x.inplace / 1

確認結果

CentOS (左) / macOS (右) の四則演算結果

0.9.9.0(inplace 演算と ブロードキャスト演算時の省メモリによる高速化対応版)の改善効果がよくわかります。 同一gem で複数のバージョン比較ができる benchmark_driver.gem は便利ですね。

#RubyKaigi 2018 RubyData Workshop LTで「Red Data Tools -Red Chainer-」というタイトルで発表しました。

RubyKaigi 2018 の 二日目の RubyData Workshop (2) Red Data Tools Lightning Talks - RubyKaigi 2018で「Red Data Tools -Red Chainer-」というタイトルで、自分が現在参加しているRed Data Tools の生活発表を行いました。

資料はこちらになります。

最初になぜ、Red Data Tools プロジェクトに参加して Red Chainer の開発をしているのかの経緯を説明しました。

次に Red Data Tools と Red Chainer の概要と、自分の取り組み内容を説明しました。

具体的に私が何の機能を開発したのか、Red Chainer のリリース機能と合わせて紹介しました。

また、Red Chainer の開発で Numo::NArray にも改善できる点があることに気づいたので、 Numo::NArray に対して取り組んだ内容を紹介しました。

※ RubyKaigi 2018 LT でお話しした内容は下記をご覧ください。

naitoh.hatenablog.com

Red Chainer の現在の実装状況を、移植元の Chainer 2.0 の機能と比較することで、どの機能が使えてどの機能の実装が足りないかを説明しました。 まだまだ未実装の部分が多いので、未実装だけど使いたい機能があれば、その部分から開発に参加してみるのはどうでしょうか。

移植元の Chainer はどんどんバージョンアップしているので、機能差が開いて追いつけない状況なので、開発に参加頂ける方を募集中です。

最後に

Ruby での DATA処理、またはその開発に興味を持って頂ける人が増えることを期待しています。

#RubyKaigi 2018 LTで「Improve Red Chainer and Numo::NArray performance」というタイトルで発表しました。

naitoh.hatenablog.com で記載した通り、RubyKaigi 2018 LT で発表させて頂きました。

内容は、今自分が py2rb.py を使用して取り組んでいる Red Chainer のポーティング作業中に気づいた、 Red ChainerNumo::NArray に対する処理速度改善のための取り組みです。

具体的には、下記になります。

  • Red Chainer の行列計算(Numo::NArray)の精度を DFloat→SFloat に変更することで mnist 1epoc の学習処理を 1.73倍速くした。
  • Numo::NArrayの inplace と BroadCast 計算の処理速度を最大3.8倍速くした。
  • 残りの下記2点のボトルネックの修正が完了すれば、Red Chainer は python Chainer 2.0 と比較してmnist 1epoc の学習処理が 16%遅いところまで処理速度が改善されること。 (CPU比較)
    • 「転置行列に対する dot 計算 "x.dot(w.transpose)"が遅い」=> view に対する dot が遅いので、dup で配列のコピーを作る"x.dot(w.dup.transpose)"ことで暫定対処可能。
    • 平方根"Numo::NMath.sqrt "が遅い」=> SIMD演算を使用することで対処可能 (暫定実装で確認済)。

最後、時間切れでお伝えしきれませんでしたが、Red Chainer の処理速度の改善が進んできているので、興味がある方はぜひ Red Chainerをさわってみて、RubyDeep Learning を始めてみるきっかけになればと考えています。

なお、OpenBlas を用いた Red Chainer の環境は下記のようにして構築しました。(CentOS7 x86_64環境) ご参考までに。

  • numpy を使って、OSにインストールされている OpenBlas のライブラリパスを確認。
$ python
Python 2.7.5 (default, Aug  4 2017, 00:39:18) 
[GCC 4.8.5 20150623 (Red Hat 4.8.5-16)] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import numpy.distutils.system_info as sysinfo
>>> sysinfo.get_info('atlas')
/usr/lib64/python2.7/site-packages/numpy/distutils/system_info.py:624: UserWarning: 
*********************************************************************
    Could not find lapack library within the ATLAS installation.
*********************************************************************

  self.calc_info()
{'libraries': ['tatlas'], 'library_dirs': ['/usr/lib64/atlas'], 'define_macros': [('ATLAS_WITHOUT_LAPACK', None)], 'language': 'c', 'include_dirs': ['/usr/include']}
>>> sysinfo.get_info('openblas')
{'libraries': ['openblas', 'openblas'], 'library_dirs': ['/usr/lib64'], 'define_macros': [('HAVE_CBLAS', None)], 'language': 'c'}
>>> sysinfo.get_info('lapack')
{'libraries': ['lapack', 'lapack'], 'library_dirs': ['/usr/lib64'], 'language': 'f77'}
>>> sysinfo.get_info('mkl')
{}
>>> 
  • OpenBlas が入っていなければ、インストールしてください。rpm コマンでも OpenBlas のパスがわかります。
# yum install openblas-devel.x86_64
# rpm -ql openblas-devel.x86_64
 (略)
/usr/lib64/libopenblas.so
  • numo-narray と numo-linalg をインストール (nomo-linalg のインストール時に、上記で確認した OpenBlas のパスを指定。)
$ rvm 2.5.1
$ gem install numo-narray
$ gem install numo-linalg -- --with-openblas-dir=/usr/lib64/  --with-atlas-dir=/usr/lib64/   --with-lapack-lib=/usr/lib64/
$ ruby  -r numo/linalg -e "p Numo::Linalg::Loader.libs"
["libopenblaso.so", "liblapacke.so"]
  • red-chainer をインストール、git clone、minist 実行。
$ gem install red-chainer
$ git clone https://github.com/red-data-tools/red-chainer.git
$ cd red-chainer
$ ruby  -r numo/linalg/use/openblas examples/mnist.rb --epoch 1
epoch       main/loss   validation/main/loss  main/accuracy  validation/main/accuracy  elapsed_time
1           0.193477                          0.94225                                  28.5615       

※ Red Chainer に転地行列のdotのdupと、Numo::NArrayに平方根に対するSIMD処理の実装(あと、inplace の最適化)を反映して計測した mnist の結果です。

RubyKaigi 2018 LT で発表させて頂きます。

RubyKaigi 2018 LT に出していた CFP が accept されましたので、 Red Chainer と Numo::NArray のパフォーマンス改善の取り組み (下記の二つ)について、 どんな事をしたか、どれくらい改善されたのかを python Chainer と numpy の結果と比較しながら、お話させて頂く予定です。

LT は初日 (5/31) になります。

Red Chainer の紹介は @hatappi さんが同じ日にメインセッションの Deep Learning Programming on Ruby - RubyKaigi 2018 でお話しされますので、そちらをご覧ください。

受理された CFP はこんな感じです。ご参考までに。

## Improve Red Chainer and Numo::NArray performance

## Abstract

I would like to talk my progress of daily contribution to Ruby world.
I'm planing to talk the performance improvement to Red Chainer and Numo::NArray(used by Red Chainer) gem.

I hope that machine learning will be comfortable to use with Ruby.

## Details

I would like to talk my progress of daily contribution to Ruby world.
Last year, I talked about "How to get on the shoulder of the giant for machine learning" at RejectKaigi 2017.
I'm planing to talk the performance improvement to Red Chainer and Numo::NArray(used by Red Chainer) gem as following with 5 minutes at RubyKaigi 2018 LT.

## Red Chainer 

- https://github.com/red-data-tools/red-chainer/pull/48

When my pull request will be merged, red-chainer gem performance will improve as follows.

* examples/mnist.rb 1 epoc  -> 73 % faster

## Numo::NArray

- https://github.com/ruby-numo/numo-narray/pull/94

With my pull request, the performance of Numo::NArray gem has been improved as follows.

* x.inplace + y or - y or * y  -> 10-15 % faster
* x.inplace + 1 or - 1 or  * 1  -> 2.1 times faster
* x.inplace / y or / 1 -> 3.2-3.8 times faster

Thanks.

PythonのコードをPython ASTベースでRubyに変換を行う py2rb.py 0.1.0 をリリースしました。

去年の8月に RejectKaigi 2017 にて発表した、「PythonのコードをPython ASTベースでRubyに変換を行う py2rb.py」が、初版公開できるレベルになったので、python のパッケージとして PyPIで公開しました。

pypi.python.org

github.com


py2rb.py は、Python機械学習関連のライブラリ、特に Chainer を Ruby に移植するためにPythonからRubyへのトランスコンパイラを作成している位置づけになります。

py2rb.py 開発の経緯の詳細は、下記 blog をご覧ください。

naitoh.hatenablog.com

特徴
  • Python ASTベースで1行単位にRubyへ翻訳
    1. Python => Ruby (メソッド,クラス,変数) : decorator や yield 等は未サポート
    2. import 文を解釈し、ローカルのモジュールファイルを参照&一括変換
    3. Python標準ライブラリ=>Ruby標準ライブラリ : (Chainer で使われている 73 モジュール のうち 10モジュールを仮実装)
  • ドメインが同じライブラリ間でPython=>Rubyに置き換え
    1. numpy => Numo::NArray (Chainer で使われている 116 メソッド のうち 31メソッドを仮実装)
    2. unittest => test::unit (Chainer で使われている 33 メソッド のうち 4メソッドを仮実装)
実行方法
$ py2rb
Usage: py2rb.py [options] filename.py
    or py2rb.py [-w [-f]] [-(r|b)] [-v] filename.py
    or py2rb.py -p foo/bar/ -m [-w [-f]] [-(r|b)] [-v] foo/bar/filename.py
    or py2rb -l lib_store_directory/ [-f]

Usage のように変換対象の Python のファイル名を指定して実行すると、変換結果を標準出力に表示します。
(以前は利用しているローカルのモジュール名を個別に指定する必要がありましたが、0.1.0では import 文を解釈しローカルのモジュールファイルを自動的に参照します。)
"-w" オプションをつけると、filename.py の変換結果を filename.rb に拡張子を変えた形式で保存します。
すでに filename.rb が存在すると処理を Skip するので、上書き保存をしたい場合は "-f" オプションをあわせて指定します。
"-m" オプションをつけると参照したローカルのモジュールファイルもあわせて一括変換します。

実際に変換すると下記のようになります。

Python サンプルコード:
$ git clone git://github.com/naitoh/py2rb.git
$ cat py2rb/tests/strings/zipstring.py

s1 = "hello"
s2 = "world"
s3 = "abcd"

s4 = list(zip(s1,s2,s3))

for item in s4:
    print("----")
    for val in item:
        print(val)

Python実行例:

$ python py2rb/tests/strings/zipstring.py
----
h
w
a
----
e
o
b
----
l
r
c
----
l
l
d

変換例:
$ py2rb -r py2rb/tests/strings/zipstring.py

# frozen_string_literal: true

require 'module'

using EnumerableEx
using PythonZipEx
using PythonPrintEx
using PythonIsBoolEx
using PythonIndexEx
using PythonFindEx
using PythonSplitEx
using PythonStripEx
using PythonStringCountEx
using PythonRemoveEx
using PythonMethodEx

s1 = "hello"
s2 = "world"
s3 = "abcd"
s4 = zip_p(s1, s2, s3).to_a
for item in s4
  print("----")
  for val in item
    print(val)
  end
end

なお、ここで Python の zip メソッドを Rubyでは独自に定義した zip_p メソッドに置き換えています。
PythonRuby で互換性のないメソッドなどの場合、互換用ライブラリを使用することで互換性を確保しています。
"using PythonZipEx" をコメントアウトすれば互換用ライブラリのメソッドは個別に無効になるので、処理の置き換えなどを実施後に該当処理が不要になれば using 行を削除するだけで済みます。

先ほどは、標準出力に表示されましたが、"-w" をつけることで、ファイル保存されます。

$ py2rb -r py2rb/tests/strings/zipstring.py -w
Try  : py2rb/tests/strings/zipstring.py -> py2rb/tests/strings/zipstring.rb : [OK]

"-l" で互換用ライブラリを指定ディレクトリに保存します。今回はカレントディレクトリに保存します。

$ py2rb -l .
OK :  ./module.rb file was stored.

Ruby の -I オプションでカレントディレクトリをライブラリパスを追加し、実行時に module.rb を呼び出し可能にして実行します。

Ruby実行例:

$ ruby -I . py2rb/tests/strings/zipstring.rb
----
h
w
a
----
e
o
b
----
l
r
c
----
l
l
d

Python の実行結果と同じになりました。

大規模なソースコードの変換の例として、chainer の変換例は次のようになります。

$ git clone git://github.com/chainer/chainer.git
$ cat chainer/chainer/variable.py

import collections
import copy
import heapq
import traceback
import warnings
import weakref

import numpy

import chainer
from chainer.backends import cuda
from chainer import initializers
from chainer.initializers import constant
from chainer.utils import argument

def _check_grad_type(func, x, gx):
    if x.data is None or gx is None:
        # ``x.data is None`` implies that the data array is not retained
        return
    if not isinstance(gx, type(x.data)):
        msg = ('Type of data and grad mismatch\n%s != %s' %
               (type(x.data), type(gx)))
        typ = TypeError
    elif gx.dtype != x.data.dtype:
        msg = ('Dtype of data and grad mismatch\n%s != %s' %
               (x.data.dtype, gx.dtype))
        typ = TypeError
    elif gx.shape != x.data.shape:
        msg = ('Shape of data and grad mismatch\n%s != %s' %
               (x.data.shape, gx.shape))
        typ = ValueError
    else:
        return

    detail = ''
    if func:
        detail = 'Function `{0}` ({1}) has a bug.\n'.format(
            type(func)._impl_name, func.label)
        stack = func.stack
        if stack:
            detail += 'Stacktrace of the function is below:\n'
            for line in traceback.format_list(func.stack):
                detail += line
        detail += '''
Please report this error to the issue tracker with the stack trace,
the information of your environment, and your script:
https://github.com/chainer/chainer/issues/new.
'''.format(type(func).__name__, func.label)

    raise typ(detail + msg)
(略)

$ py2rb -p chainer chainer/chainer/variable.py

module Chainer
  module Variable
    require 'weakref'
    require_relative 'backends/cuda'
    include Chainer::Backends
    require_relative 'chainer'
    include Chainer
    require_relative 'initializers/constant'
    include Chainer::Initializers
    require_relative 'utils/argument'
    include Chainer::Utils
    def _check_grad_type(func, x, gx)
      if is_bool((x.data === nil)||(gx === nil))
        return
      end
      if is_bool(!(gx.is_a? (x.data).class))
        msg = "Type of data and grad mismatch
%s != %s" % [(x.data).class, (gx).class]
        typ = ArgumentError
      else
        if gx.dtype != x.data.dtype
          msg = "Dtype of data and grad mismatch
%s != %s" % [x.data.dtype, gx.dtype]
          typ = ArgumentError
        else
          if gx.shape != x.data.shape
            msg = "Shape of data and grad mismatch
%s != %s" % [x.data.shape, gx.shape]
            typ = TypeError
          else
            return
          end
        end
      end
      detail = ""
      if is_bool(func)
        detail = "Function `{0}` ({1}) has a bug.
".format((func).class._impl_name, func.label)
        stack = func.stack
        if is_bool(stack)
          detail += "Stacktrace of the function is below:
"
          for line in (func.stack).to_a
            detail += line
          end
        end
        detail += "
Please report this error to the issue tracker with the stack trace,
the information of your environment, and your script:
https://github.com/chainer/chainer/issues/new.
".format((func).class.__name__, func.label)
      end
      raise typ, (detail)+(msg)
    end
(略)

ここまでの規模になると、ソースコード修正が必要になりますが、大部分の機械的な置き換えが行われた後なので悩む箇所や手間が削減されます。

実際にPython からの移植を行う場合は、"-m" オプションをつけると "-p" のパス配下のファイルで指定されたpython ファイル (下記の場合は chainer/chainer/variable.py) から参照されているファイルを一括変換するので便利です。

$ py2rb -p chainer chainer/chainer/variable.py -m -w
Try  : chainer/chainer/functions/math/fmod.py -> chainer/chainer/functions/math/fmod.rb : [OK]
Try  : chainer/chainer/functions/array/expand_dims.py -> chainer/chainer/functions/array/expand_dims.rb : [OK]
Try  : chainer/chainer/functions/pooling/average_pooling_nd.py -> chainer/chainer/functions/pooling/average_pooling_nd.rb : [OK]
Warning : syntax not supported (<_ast.ExtSlice object at 0x102df11d0>)
Warning : ExtSlice not supported (path[]) in Assign(ast.Subscript)
Warning : syntax not supported (<_ast.ExtSlice object at 0x102df11d0>)
Warning : syntax not supported (<_ast.ExtSlice object at 0x102dd35f8>)
Warning : syntax not supported (<_ast.ExtSlice object at 0x102ddef28>)
()

変換処理で未サポートなどにより変換ができない部分は Warning が表示され、該当箇所以外の変換を続行します。

現在、Red Data Tool プロジェクトで行われている Chainer の Ruby移植版 Red Chainer の開発に Join し、ドッグフーディングも兼ねて私の作成した部分については py2rb.py を使いながら (py2rb.py の機能不足点も強化しながら)開発を進めています。

github.com

今後はRed Chainer の成長にあわせて py2rb.py の機能カバレッジや完成度も向上していければと考えています。

興味ある方は使ってみてください。

RejectKaigi 2017で『Rubyで機械学習を行うための「巨人の肩に乗る」別の方法』という発表をしました

RubyKaigi 2017 に CFP を出したところ残念ながら Reject されてしまい、この内容を発表する場が無いかなと考えていたところ、
RejectKaigi 2017 に発表の機会を頂きましたので、お話させて頂きました。

スライドは下記に公開してますので、ご覧ください。

www.slideshare.net

お話した内容は

Ruby機械学習をしたいと思った場合、現状では不可能なため、下記の三つの内容について Ruby コミュニティで取り組みが行われており、

  1. 既存のgemをなんとかすること
  2. Rubyのための仕組みをつくっていくこと
  3. 巨人 (Python) の肩に乗る方法

この中の現実的な解が「巨人 (Python) の肩に乗る方法」を実現しつつある PyCall になります。

PyCall の詳細は過去の記事をご覧ください。

www.s-itoc.jp

getnews.jp

Ruby機械学習を行うための「巨人の肩に乗る」別の方法』

とは

  • Ruby から PyCall を経由してPython の機能をを呼び出す」

という方法とは異なり、

  • Pythonのライブラリを Ruby に移植」

することで、Python のノウハウ、知見を Ruby で活用したい
そのためにはどうすれば良いか

というお話になります。

今回、移植の対象としてChainerを選択しました、理由は

  • tensorflow.rb はすでに移植作業が行われているため、対象外。PyCallでまだ動作していないChainerがいいのではないか
  • (GPU対応を保留にすれば)主な依存性が numpy に絞られる Chainer について、numpy 部分の機能を Numo::NArray に置き換える形で移植を行えば移植のハードルが比較的低い

になります。

移植の現実性

について検討したところ、Chainer 2.0 : 合計 86.3 Ks (本体 49.8 Ks/テストコード 36.4 Kstep) とかなりの規模になり、自分が過去に行なったRBPDF(TCPDFからの PHP => Ruby移植。Redmine のPDF出力で使用されている gem です。)が約15 Ksで移植作業に約2年かかったことから、86.3 Ks/(15 Ks/2年)=11年必要となり、仮に完成したとしても時代遅れになります。

そのため、自動で Python のコードを Ruby に変換するツールを探したところ、5つのトランスレータが見つかりました。

  • https://bitbucket.org/snej/py2rb/src/tip/py2.rb
    • 正規表現でパースしてRubyに置換する。精度甘めのため、小規模ソースコード量ならよいが、大規模だと修正が多すぎるため断念
  • Grumpy(Go running Python)
    • Goに変換して実行。実行(非移植)が目的のためメソッドがインライン展開されるため、機械に優しく人に優しくなコードになるため断念
  • py2js.py
    • Python AST(Abstract Syntax Tree: 抽象構文木)ベースでJavaScriptに正確な変換をする、テストコードの仕組みも充実しておりかなりよい
  • py2cpp
    • Python ASTベースでC++に変換、結構変換してくれる
  • HOLY
    • Python ASTベースでRubyに変換し、今回の目的に合致したのですが、POCが目的であり実装範囲が狭くそのままでは使えない。

ここで、py2js.py をベースに Python から Ruby に変換するツールをまず開発すればいいのではという結論になりました。

py2rb.py

というわけで、 py2rb.py という Python ASTベースで Ruby に変換するツールを作っています。

github.com

  • Python ASTベースで1行単位にRubyへ翻訳
    1. Python => Ruby (メソッド,クラス,変数) : 大分動作
    2. Python標準ライブラリ=>Ruby標準ライブラリ : (Chainer で使われている 73 モジュール のうち 8モジュールを仮実装)
  • ドメインが同じライブラリ間でPython=>Rubyに置き換え
    1. numpy => Numo::NArray (Chainer で使われている 116 メソッド のうち 8メソッドを仮実装)
    2. unittest => test::unit (Chainer で使われている 33 メソッド のうち 4メソッドを仮実装)

このように単純なソースコード変換だけでなく、下回りのライブラリのドメインが同じ場合、それに置き換えることで品質をある程度確保した移植を行うことを考えています。

主な特徴として、下記を備えています。

  • Python 3.5 => Ruby 2.4 変換
  • クラス継承
  • モジュール呼び出し(import => require)
  • PythonRuby のメソッドのキーワード引数差異吸収

py2rb.py を用いた移植作業の流れ下記を想定しており、このうち 2, 3の作業を支援できることを想定しています。

  1. 移植対象を決定 (移植可能か見極め)
  2. プロダクションコードを1行単位で移植
  3. テストコードを1行単位で移植 (テストコードが無いツールの移植はお勧めしません)
  4. テストをパスするようプロダクションコード修正 (機能が動くことを担保するために必要な作業)
  5. 必要に応じてリファクタリング (コードのメンテナンス性向上や、性能を確保するために必要な作業)

py2rb.py 開発方針として下記を考えています。

  • あくまで移植の支援ツール と割り切る (PythonRuby で言語思想の違いからどうしても非互換部分が避けられないため)
  • 移植対象が使用していない機能は保留
  • ライブラリ部分の変換定義をYAMLで指定 (変換ツール内部に手を入れなくても、ライブラリ部の変換定義を追加可能にするため)
  • Rubyの黒魔術でPythonとの互換性を向上 (互換性確保のため py-builtins.rb を用意)

これにより、私以外が py2rb.py を用いて Python のライブラリ・ツールをRuby に移植したいと考えた場合でも、ライブラリ変換定義のYAML定義を追加する作業を行い、上記 1, 4, 5 の作業行うことで移植作業が可能になると考えています。

実行方法

以下のように変換対象の filename と、filename から import 指定されているモジュール名を指定します。 (モジュールとして指定したソースコードは意図せずファイルを上書きする事を避けるため、変換は行いません。モジュールを個別に filename に指定して変換することを想定しています。)
※ ここでモジュール名の指定漏れがある場合、変換精度が落ちます。

$ ./py2rb.py
Usage: py2rb.py [options] filename [module filename [module filename [..]]]

実際に変換すると下記のようになります。

$ cat chainer/chainer/variable.py

def _check_grad_type(func, x, gx):
    def make_message(message):
        if func:
            detail = 'Function `{0}` ({1}) has a bug.\n'.format(
                type(func).__name__, func.label)
            stack = func.stack
            if stack:
                detail += 'Stacktrace of the function is below:\n'
                for line in traceback.format_list(func._stack):
                    detail += line
            detail += ''' (略)'''.format(type(func).__name__, func.label)
        else:
            detail = ''
        detail += message
        return detail

$./py2rb.py --include-require chainer/chainer/variable.py chainer/chainer/utils/argument.py

def _check_grad_type(func, x, gx)
    make_message = lambda do |message|
        if is_bool(func)
            detail = "Function `{0}` ({1}) has a bug.\n".format(type(func).__name__, func.label)
            stack = func.stack
            if is_bool(stack)
                detail += "Stacktrace of the function is below:\n"
                for line in (func._stack).to_a
                    detail += line
                end
            end
            detail += "(略)".format(type(func).__name__, func.label)
        else
            detail = ""
        end
        detail += message
        return detail
    end
  • pythonクロージャー (def 内 def) をlambdaに変換することで、 def make_message の外部スコープのfuncを参照可能にしています。
  • is_bool()を用意し、Pythonとの差 (Rubyでは 0, '', [], {} は True、nil(None), false は Falseだが、Python は全て False)を吸収します。

※ まだ変換定義の実装が不足しているため、未変換で Python のコードがそのままの部分が残っている例になります。

また、テストケースとしてサンプル変換を一括実行する形式でテストを行なっています。

./run_tests.py -a

およそ 200個のテストケースを用意して変換動作を確認しています。

テストケース期待値は、

  • python のコードがrubyのコードに変換される (変換後のrubyのコードを期待値として持つ)事

ではなく

  • pythonのコードの実行結果と変換したrubyのコードの実行結果が同一になること

で、意図した変換になることを確認しています。

PythonRubyの差の影響

Python の関数は Ruby と異なりObject なのですが、変換が難しい例として、全く同じに見える foo() が実は中身によって解釈が変わる場合があります。

  1. foo() : fooが関数の場合、メソッド実行
  2. foo() : fooがクラスの場合、Rubyのnew()
  3. foo() : fooがインスタンスの場合、Rubyのfoo.call()

これは正規表現では判断は無理でASTなら判断可能です。ただし、変数に代入されるともはやASTでも判断は無理です。
なので、変換可能な範囲で変換する形に割り切っています。

他に、py2rb.py で割り切った点として

  • 多重継承は未サポート: これは根本的に無理なので、移植対象選択時、多重継承の有無を事前確認することを想定。
  • クラス内クラス
  • 変数のスコープ範囲の違い
  • デコレータ : 可能な範囲のみ。

があります。そもそも、

  • Python はストリーム : メソッド定義時にコンテキストを保持
  • Ruby はオブジェクト(型) : クラス内ではコンテキスト非保持

という差があるのでなかなか難しいものがあります。

py2rb.py で気をつけた点として

  • 動く事: 動かないとどこが問題なのか探す(デバッグ)する必要があり、不要なロスの時間は避けたい。(互換性を保つための余分な定義は不要と判断後に削除すればよい。)
  • (移植が目的なので)ソースコードをなるべく綺麗な形で変換する事。
  • 非互換を吸収する為、ソースコードが汚くなりそうな場合はPython&Ruby互換用ライブラリとして実装。(大半が互換の範囲なので、不要と判断したら using を外せばよい。)
Ruby機械学習を行うためのまとめ
  1. 既存のgemをなんとかすること
  2. Rubyのための仕組みをつくっていくこと
    • PythonからRubyへ変換ツール(py2rb.py)を用意
  3. 巨人(Python)の肩に乗ること
    • Pythonのツールを移植する事でPythonでの知見を活用する
今後の予定
  • ドックフーディングとしてChainerの移植にTry.
  • 標準ライブラリ/unittest/numpy のYAML変換定義の実装完了(Chainerの対象範囲)
  • RubyDeep Learning(本当にやりたい事)

という形で締めくくったのですが、質疑応答で Red Data Toolのプロジェクトで、すでにChainerの移植作業が始まっているという情報を頂きました。

私の手法は Python のコードの1行単位の移植になるため、Rubyらしいコードではなく Python ぽいコードになるため、最初から Ruby らしく書いた方がいいのではないかという指摘を頂きました。

また、手で作業を行なった場合、11年と見積もった作業も、一人ではなく複数人でやれば現実的な時間になるのではというお話も頂きました。
今の作業を保留して、そちらに Join すべきか、今の作業そのままやるべきか、まだ決めかねていますが、同じ目的の人がいるんだなと心強くなりました。