RubyKaigi 2017 に CFP を出したところ残念ながら Reject されてしまい、この内容を発表する場が無いかなと考えていたところ、
RejectKaigi 2017 に発表の機会を頂きましたので、お話させて頂きました。
スライドは下記に公開してますので、ご覧ください。
www.slideshare.net
お話した内容は
Ruby で機械学習をしたいと思った場合、現状では不可能なため、下記の三つの内容について Ruby コミュニティで取り組みが行われており、
この中の現実的な解が「巨人 (Python) の肩に乗る方法」を実現しつつある PyCall になります。
PyCall の詳細は過去の記事をご覧ください。
『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
- Grumpy(Go running Python)
- Goに変換して実行。実行(非移植)が目的のためメソッドがインライン展開されるため、機械に優しく人に優しくなコードになるため断念
- py2js.py
- Python AST(Abstract Syntax Tree: 抽象構文木)ベースでJavaScriptに正確な変換をする、テストコードの仕組みも充実しておりかなりよい
- py2cpp
- HOLY
ここで、py2js.py をベースに Python から Ruby に変換するツールをまず開発すればいいのではという結論になりました。
py2rb.py
というわけで、 py2rb.py という Python ASTベースで Ruby に変換するツールを作っています。
- Python ASTベースで1行単位にRubyへ翻訳
- ドメインが同じライブラリ間でPython=>Rubyに置き換え
- numpy => Numo::NArray (Chainer で使われている 116 メソッド のうち 8メソッドを仮実装)
- unittest => test::unit (Chainer で使われている 33 メソッド のうち 4メソッドを仮実装)
このように単純なソースコード変換だけでなく、下回りのライブラリのドメインが同じ場合、それに置き換えることで品質をある程度確保した移植を行うことを考えています。
主な特徴として、下記を備えています。
py2rb.py を用いた移植作業の流れ下記を想定しており、このうち 2, 3の作業を支援できることを想定しています。
- 移植対象を決定 (移植可能か見極め)
- プロダクションコードを1行単位で移植
- テストコードを1行単位で移植 (テストコードが無いツールの移植はお勧めしません)
- テストをパスするようプロダクションコード修正 (機能が動くことを担保するために必要な作業)
- 必要に応じてリファクタリング (コードのメンテナンス性向上や、性能を確保するために必要な作業)
py2rb.py 開発方針として下記を考えています。
- あくまで移植の支援ツール と割り切る (Python と Ruby で言語思想の違いからどうしても非互換部分が避けられないため)
- 移植対象が使用していない機能は保留
- ライブラリ部分の変換定義を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の差の影響
Python の関数は Ruby と異なりObject なのですが、変換が難しい例として、全く同じに見える foo() が実は中身によって解釈が変わる場合があります。
これは正規表現では判断は無理でASTなら判断可能です。ただし、変数に代入されるともはやASTでも判断は無理です。
なので、変換可能な範囲で変換する形に割り切っています。
他に、py2rb.py で割り切った点として
- 多重継承は未サポート: これは根本的に無理なので、移植対象選択時、多重継承の有無を事前確認することを想定。
- クラス内クラス
- 変数のスコープ範囲の違い
- デコレータ : 可能な範囲のみ。
があります。そもそも、
という差があるのでなかなか難しいものがあります。
py2rb.py で気をつけた点として
今後の予定
- ドックフーディングとしてChainerの移植にTry.
- 標準ライブラリ/unittest/numpy のYAML変換定義の実装完了(Chainerの対象範囲)
- RubyでDeep Learning(本当にやりたい事)
という形で締めくくったのですが、質疑応答で Red Data Toolのプロジェクトで、すでにChainerの移植作業が始まっているという情報を頂きました。
https://t.co/9OuQDJLwWe #rejectkaigi2017
— masayoshi takahashi (@takahashim) 2017年8月19日
私の手法は Python のコードの1行単位の移植になるため、Rubyらしいコードではなく Python ぽいコードになるため、最初から Ruby らしく書いた方がいいのではないかという指摘を頂きました。
また、手で作業を行なった場合、11年と見積もった作業も、一人ではなく複数人でやれば現実的な時間になるのではというお話も頂きました。
今の作業を保留して、そちらに Join すべきか、今の作業そのままやるべきか、まだ決めかねていますが、同じ目的の人がいるんだなと心強くなりました。