Movatterモバイル変換


[0]ホーム

URL:


LoginSignup
56

Go to list of users who liked

50

Share on X(Twitter)

Share on Facebook

Add to Hatena Bookmark

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

PyCall Ruby版 Tips

Last updated atPosted at 2018-01-29

pycallrb_logo.png    pycall公式ロゴ

はじめに

PyCallは mrkn さんが開発されている、Python のライブラリを Ruby や Julia で利用するためのブリッジライブラリです。

https://github.com/mrkn/pycall.rb/

PyCallの解説やインストール方法は mrkn さん始め諸賢のドキュメントが多く有りますので、ここでは私がRuby版PyCallを使ってきてのTipsを書いていきます。(2018年2月時点の最新版 PyCall 1.0.3 に基づく)

Tips

Python ライブラリのロード

Python のライブラリを使う場合、次の2行を書いておく

require'pycall/import'includePyCall::Import

Python のライブラリは次のようにロードする

# Pythonだと、 import matplotlibpyimport:matplotlibpyimport'matplotlib'# 上ではシンボルを使ったが、文字列でも同じ# Python だと import matplotlib.pyplot as pltpyimport'matplotlib.pyplot',as: :pltplt=PyCall.import_module('matplotlib.pyplot')# 上はこのような記法でも同じ# Python だと from janome.tokenizer import Tokenizerpyfrom'janome.tokenizer',import: :Tokenizer# Python だと from keras.layers import Dense, Dropoutpyfrom'keras.layers',import:['Dense','Dropout']

全般的に気をつけること

  • Pythonから返ってきたオブジェクトの型には気をつけましょう。 - PythonのいろいろなライブラリからRubyに戻されるオブジェクトには、一見数値やArrayやHashのように思えるものが有りますが、実は違っている場合が有ります。
# RubyのArrayといくつかのArray風のオブジェクトの違いを調べますr_array=[0.0,1.0,2.0]# RubyのArray(配列)を作るp_list=PyCall.eval("[0.0, 1.0, 2.0]")# Python のリストを作るrequire'numpy'# numpy は配列を扱うPythonの定番ライブラリnp_array=Numpy.array([0.0,1.0,2.0])# numpy のarrayを作るpyfrom('collections',import: :deque)# dequeはappendやpopを高速に行えるリスト風のコンテナp_deque=deque.([0.0,1.0,2.0])# deque を作る# 上で作ったArray風のオブジェクトが [] でアクセスできることを確認します。またクラスを調べますpr_array[1],r_array.class# -> 1.0, Arraypp_list[1],p_list.class# -> 1.0, <class 'list'>pnp_array[1],np_array.class# -> 1.0, <class 'numpy.ndarray'>pp_deque[1],p_deque.class# -> 1.0, Object# Array風のオブジェクトで each が使えるかどうかの確認r_array.each{|x|px}# OKp_list.each{|x|px}# OKnp_array.each{|x|px}# OKp_deque.each{|x|px}# エラー! PyCall::List.(p_deque)とすればリストにキャストできる。でも計算コストを考えると高速性がウリのdequeをリストにする意味はないだろう# Array風のオブジェクトで delete_if が使えるかどうかの確認r_array.delete_if{|x|x>1.5}# OKp_list.delete_if{|x|x>1.5}# エラー! - p_list.to_a とすればArrayにキャストできるnp_array.delete_if{|x|x>1.5}# エラー! - PyCall::List.(np_array).to_aとすればArrayにキャストできるp_deque.delete_if{|x|x>1.5}# エラー! - PyCall::List.(p_deque).to_aとすればArrayにキャストできる
  • Pythonオブジェクトに渡す引数の()の前にはピリオドを入れますが、よく使うオブジェクトでは不要です。
    ("よく使う" というのはPythonの基本的なオブジェクトやnumpy のようにPyCall側でラッパーメソッドが用意されているもの、という意味です)
    ピリオドが抜けた場合、"ArgumentError" か "NoMethodError" が返ってきます。
# 引数の()の前にピリオドを入れる例pyfrom'collections',import: :dequep_deque=deque.([0.0,1.0,2.0,3.0])# deque と ( の間に . を入れる# 引数の()の前にピリオドを入れない例require'numpy'a=Numpy.array([1,2,3],dtype=np.float32)# array と ( の間に . を入れない

Numpy の使用

numpyは PyCall と同じ mrkn さんの作成によるgemがあります。 gem install numpy でインストールしましょう。
Rubyプログラムから使う時は、

require'numpy'np=Numpya=np.array([1,2,3],dtype=np.float32)

などとして使用します。 pyimport :numpy, as: :np とするより良いです。

# pyimport で numpy をロードする時の落とし穴pyimport:numpy,as: :np# これでも良いように思えるが...a=np.array([1,2],dtype=np.float32)b=a[0]+a[1]pb# 3.0 が表示される。当たり前じゃないかpb.class# Object と表示される。 float じゃないんだp"bは3だよ"ifb==3.0# "bは3だよ" と表示される。当たり前じゃないかp"bは3だよ"ifb==5.0# "bは3だよ" と表示される!? why!?  (b==5.0) は false じゃなくFalseだから。Rubyはfalseとnil以外は真です

numpy の : は 代わりに範囲演算子 .. / ... を使います

# 配列へのアクセス例m=np.arange(0,100).reshape(10,10)#まずnumpyの配列を作ります=>array([[0,1,2,3,4,5,6,7,8,9],[10,11,12,13,14,15,16,17,18,19],[20,21,22,23,24,25,26,27,28,29],...[90,91,92,93,94,95,96,97,98,99]])# 特定の行、列へのアクセスm[2,1]=>21# m が RubyのArrayだったら [20, 21, ... 28, 29] となる# 最後の行へのアクセス pythonだと m[-1,:] と書くところm[-1]=>array([90,91,92,93,94,95,96,97,98,99])# 最後の列へのアクセス pythonだと m[:,-1] と書くところm[0..-1,-1]=>array([9,19,29,39,49,59,69,79,89,99])# 部分へのアクセス python だと m[5:7,2:4] と書くところm[5...7,2...4]=>array([[52,53],[62,63]])# python のスライスを使うこともできます# 6行目から最後の行までの偶数行の、3列目から5列目までを選択する例sl=PyCall::Slice.(6,-1,2)# 6行目から最後の行まで2行ごとに行を選択するためのスライスを作成m[sl,3..5]=>array([[63,64,65],[83,84,85]])

Pythonモジュールを読むディレクトリの指定

PythonがデフォルトでサーチしないディレクトリにPythonモジュールが書かれたファイルを置く場合、次のような感じで置いたディレクトリを指定する

PyCall.sys.path.append(__dir__)# Rubyプログラムと同じディレクトリにある場合hoge=PyCall.import_module('hoge')PyCall.sys.path.append('/home/boo/foo')# ディレクトリを指定する場合woo=PyCall.import_module('woo')

Ruby のクラスの中で PyCall を使う

クラスの中で、あるメソッドに "pyimport :tensorflow, as: :tf" のように書いても "tf" は別のメソッドの名前空間には存在しない。
次の2つの例のいずれかが使える

classBoomodulePyextendPyCall::Importpyimport:tensorflow,as: :tfend...tf=Py.tf# 記法1 クラスの中で"tf"のインスタンスをたくさん作るならこの記法が見やすい?...endclassFoo...tf=PyCall.import_module(:tensorflow)# 記法2...end

Ruby プログラムの中に Python の関数を作る

2種の記法例で、引数を倍にする関数を示します

#記法 1PyCall.exec(<<PYTHON)def double(object):  return object * 2PYTHONa=5pPyCall.eval("double(#{a})")# -> 10c="'!!'"pPyCall.eval("double(#{c})")# -> "!!!!"#記法 2double=PyCall.eval(<<PYTHON)  lambda object: object *2PYTHONa=5pdouble.(a)# -> 10c='!!'pdouble.(c)# -> "!!!!"

Ruby プログラムの中に Python のクラスを作る

次のサイコロのクラスを参考にしてください。PyCall.exec(script) はPythonのscript を実行します。
でもRuby プログラムの中に大きなクラスを作るのはデバッグが面倒なのでおすすめしません。大きなPythonのクラスを作る時は別ファイルに作って、Pythonでデバッグするのが良いと思います。

PyCall.exec(<<PYTHON)import randomclass Die():  def __init__(self,x):            # サイコロを作るよ    if x == 0:      self.val = self.roll()       # 0 が与えられたら最初の目はランダムに決める    else:      self.val = x                 # 最初の目を指定された値にする  def roll(self):    self.val = random.randint(1,6)  # サイコロを転がすよ    return self.val  def look(self):    return self.val               # サイコロの目を確認するよ  def change(self, x):    self.val = x                  # サイコロの目を指定された値にするよ    return self.valPYTHONdie=PyCall.eval('Die').(4)# PyCall.eval('Die').new(4) とも書けるdie.look# -> 4die.roll# -> 1i=5die.change(i)# -> 5

set

Python のsetオブジェクトに対する演算子、 &, |, -, ^ , > をRubyでも使うことができる

# Python の set オブジェクトを作るset1=PyCall.eval("{1,2,3,4}")set2=PyCall.eval("{0,2,4,6}")# set オブジェクトの演算例set1&set2# -> {2, 4}set1|set2# -> {0, 1, 2, 3, 4, 6}set1-set2# -> {1, 3}set1^set2# -> {0, 1, 3, 6}set1>set2# -> false

TensorFlow Eager Execution

TensorFlow の バージョン1.5からデフォルトで使えるようになったEager Executionは python コードの冒頭に次のようなおまじないを書きます。

# Eager Execution を使う python のコード冒頭部importtensorflowastfimporttensorflow.contrib.eagerastfetfe.enable_eager_execution()

私には理屈がわからないのですが、これを単純にpyimportに置き換えてもrubyでは動きません。次のように書く必要があるようです。

# Eager Execution を使う ruby のコード冒頭部pyimport:tensorflow,as: :tftf.contrib.eager.enable_eager_execution()

余談になりますが、 Eager Execution を使うとグラフを動的に変えることができるし、irbやpryのデバッグ中にtensorflow オブジェクトの中身を簡単に確認できてよさげです。

#  Eager Execution でtensorflow オブジェクトを操作する例x=[[1.0,-1.0],[-1.0,1.0]]y=[[0.25,0.5],[0.75,1.0]]# xとyはrubyのArraytm=tf.nn.tanh(tf.matmul(x,y))# rubyのArrayを何も考えずにtensorflowの計算に使えるptm# 結果は随時確認できる# <tf.Tensor: id=8, shape=(2, 2), dtype=float32, numpy=array([[-0.46211717,-0.46211717],[ 0.46211717,  0.46211717]], dtype=float32)>numpy_m=tm.numpy()# numpy へのキャストも簡単

with

PyCall.with() を使う

# Python の io.open と with を使って、ファイルの各行を空行が出てくるまで読む例a=[]# ファイルの行を格納するArrayfile_name='hoge.txt'PyCall.with(io.open(file_name,"r")){|fp|# fp は Pythonのファイルポインタですwhile(line=fp.readline())!=""# readline()はPythonの io::readline ですa<<lineend}# with を使ってるのでファイルをcloseしなくて良い

キーワード引数

キーワードで引数をわたす Pyrhon のメソッドには、ハッシュで引数を渡す

# Python では w2v.Word2Vec(st, size=200, sg=1) と書くところw2v.Word2Vec.(st,{size:200,sg:1})

キャスト 型変換

  • Python のオブジェクトから Ruby のオブジェクトへ

    • タプルから: to_a で RubyのArray(配列)が作られる
    • リストから: to_a で RubyのArray(配列)が作られる
    • 辞書(dict)から: to_h で Rubyのハッシュが作られる
  • Ruby のオブジェクトを Pythonの タプル、リスト、辞書へ

    • タプルへ: PyCall.eval("tuple(hoge)") もしくは PyCall::Tuple.(hoge)
    • リストへ: PyCall.eval("list(hoge)") もしくは PyCall::List.(hoge)
    • 辞書(dict)へ: PyCall.eval("dict(hoge)") もしくは PyCall::Dict.(hoge)
# numpy オブジェクトを Python の リストにする例PyCall::List.(np_array)# numpy オブジェクトを Ruby の Array にする例PyCall::List.(np_array).to_a
  • Ruby から Python のオブジェクトを作る
PyCall::List.([0,1,2])# Ruby の Array から Pythonのリストを作るPyCall::Tuple.([0,1,2])# Array から タプルを作るPyCall::Dict.({zero:0,one:1,two:2})# ハッシュから 辞書(dict)を作る# Python の Slice オブジェクトの作成と使用例sl=PyCall::Slice.(1,3,nil)# slice(1,3,None) ができる。 nilは省略可np.array([0,1,2,3,4],dtype=np.float32)[sl]# -> array([1., 2.], dtype=float32)

辞書(dict)にイテレータを使う

each は辞書で使える。使えないイテレータは to_h して使う

python_dict.to_h.delete_if{|k,v|v>1.5}

タプルにイテレータを使う

タプルに each や inject や zip みたいなRubyのArrayのメソッドが使いたい!って時は、to_a でArrayにする

python_tuple.to_a.each{|x|px}

タプルの戻り値

Python のオブジェクトがタプルで戻り値を返す場合、to_a で配列に変換して受け取る

a,b=pythonObj.tuple_return_method.(arg).to_a

内包表記を使う

PyCall.eval() を使う

size=3x=PyCall.eval("[ i*2 for i in range(#{size}) ]")# -> [0,2,4]# ここで x は to_a しないとRubyのArrayではなくPythonのリストであることに注意

Ruby と Python を結びつけるその他のアプローチ

なぜPyCall

tensorflow を使うため初めて Python に触れ、tensorflow みたいなフレームワークには Python がぴったりだよな、と思いつつ Python でいろいろプログラムを書いてみるものの、 Ruby のほうが書いてて楽しいし、慣れてるせいもあって効率も良い。機械学習のプログラムを書くと言っても、tensorflow のグラフまわりのコードは全体の1割か2割位で、データを収集したり加工するコードのほうが遥かに多いんだ、ということが判って私は機械学習のプログラムを基本は Ruby で書いて tensorflow まわりのコードだけ Python で書くことにしました。でもその場合 Ruby と Python での間でデータを どのようにやり取りするかが問題だよな。 という経緯で PyCall を使ってみたのですがなかなか具合が良いです。

謝辞

素晴らしいライブラリをご提供くださっている mrkn さんに改めて感謝の意を表します

56

Go to list of users who liked

50
1

Go to list of comments

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
56

Go to list of users who liked

50

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?


[8]ページ先頭

©2009-2025 Movatter.jp