9
Help us understand the problem. What are the problem?

More than 5 years have passed since last update.

posted at

RubyからPythonやJuliaを呼び出す方法(実験的な実装)

RubyからPythonやJuliaのパッケージを呼び出せるvirtual_moduleというgemを作っています。下記の例では、いくつかのコマンドのmanpageをドキュメントとして読み込むところまでをRubyで書いて、doc2vecの処理にかける部分はPythonを呼び出してgensimに任せるということをやっています。

doc2vec.rb
require 'natto'
manpages={}
natto = Natto::MeCab.new
%w"ps ls cat cd top df du touch mkdir".each do |cmd|
  list = []
  natto.parse(`man #{cmd} | col -bx | cat`) do |n|
    list << n.surface
  end
  manpages[cmd] = list
end

require 'virtual_module'
py = VirtualModule.new(:methods=><<EOS, :python=>["gensim"])
class LabeledListSentence(object):
    def __init__(self, words_list, label_list):
        self.words_list = words_list
        self.label_list = label_list

    def __iter__(self):
        for i, words in enumerate(self.words_list):
            yield gensim.models.doc2vec.LabeledSentence(words, [self.label_list[i]])

EOS
model = py.gensim.models.doc2vec.Doc2Vec(py.LabeledListSentence(manpages.values, manpages.keys), min_count:0)
p model.docvecs.most_similar(["ps"]) # [["top", 0.5594387054443359], ["cat", 0.46929454803466797], ["df", 0.3900265693664551], ["mkdir", 0.38811227679252625], ["du", 0.23663029074668884], ["ls", 0.15436093509197235], ["cd", -0.1965409815311432], ["touch", -0.38958919048309326]]

これを使って自分のブログ(Sinatra製)にこのRubyで動くdoc2vecを使った関連記事抽出の機能を追加したのですがちょっと便利でした。僕以外に嬉しくなれる人がどれだけいるか分かりませんが、(かなり不自由ですが一応)scikit-learnも使えるようになりますし活用の仕方次第では面白いと思うので、想定する使い方などを書いていきます。

なお、doc2vec以外にscikit-learn使った例とかを個人のブログの方にまとめてあるので気になる人がいたらそちらもどうぞ。

REPLを使ったVirtualModuleの動作の簡単な紹介

ここではREPLを使ってVirtualModuleが内部的にどのように動いているのかを書いてみようと思います。下記が既にシステム上にインストールされていることを想定しています:

  • virtual_module gem (v0.3.0以上)
  • Pythonの実行環境

まずirbを立ち上げます。

debussy:~ remore$ irb -r virtual_module
irb(main):001:0> po = VirtualModule.new(:python=>["sklearn"=>"datasets"])
=> #<Module:0x007fb7e1aee818>

VirtualModule#newを呼び出すと、裏側でPython(またはJulia)のプロセスが起動します。バックグランドジョブが正しく起動し終わると、VirtualModuleは新しいModuleインスタンスを返却します。今後はこのModuleインスタンスを介して(≒このインスタンスがプロキシのように動作することで)バックグラウンドとの通信を行っていきます。ここでは簡便のために、これをプロキシオブジェクトと呼ぶことにします。

irb(main):002:0> py.int(2.3)
=> 2
irb(main):003:0> po.unknown_method(2.3)
RuntimeError: An error occurred while executing the command in python process: ,name 'unknown_method' is not defined

プロキシオブジェクトの動作は非常に単純です。上記の例では、プロキシオブジェクトはint(2.3)というメソッド呼び出しを受取、これをそのままバックグラウンドジョブに伝えます(この時、msgpackを使って値の変換を行います)。結果としてFixnum型の値である2がターミナルに出力されていますが、これはバックグラウンドジョブから返却されたものです。データの変換はmsgpackを使っているだけなので、相互に変換できる値もmsgpackの仕様に準じます。もしpo.unknown_method(2.3)の例のようにバックグラウンドジョブ側で未定義のメソッドが呼び出されると、エラーが表示されます。基本的には以上がVirtualModuleの動作の全てです。

これだけだと腑に落ちないところもあると思うので、もう少し書き加えます。

irb(main):004:0> po.datasets
=> #<Module:0x007ffd0906c030>
irb(main):005:0> po.datasets.load_iris(:_)
=> #<Module:0x007ffd09074500>
irb(main):006:0> po.datasets.load_iris(:_).vclass
=> "<class 'sklearn.datasets.base.Bunch'>"
irb(main):007:0> po.datasets.load_iris(:_).data[1].to_a
=> [4.9, 3.0, 1.4, 0.2]

実際msgpackで変換できない値が利用されてるとどう動作するかについては、こちらの例を参照してください。この例では初めにプロキシオブジェクト(ここでいうローカル変数のpo)が#datasetsメソッドの呼び出しに対して新しいプロキシオブジェクト(#<Module:0x007ffd0906c030>)を返していますが、その後の#load_iris(:_)も別のプロキシオブジェクト(#<Module:0x007ffd09074500>)を返しています。datasetsはPython上でmodule型のオブジェクトですし、load_iris(:_)は'sklearn.datasets.base.Bunch'クラスのインスタンスということでどちらもmsgpackを経由して変換不能なため、Moduleインスタンスが生成されています。このようにmspgackが変換できないような呼び出しに対しては、VirtualModuleには値の実体は渡されることがなく、その値へのポインタのみが渡されるような内部動作をしています。

irb(main):008:0> po.datasets.vclass
=> "<type 'module'>"
irb(main):009:0> iris = po.datasets.load_iris(:_)
=> #<Module:0x007ffd09057568>
irb(main):010:0> iris.target.vclass
=> "<type 'numpy.ndarray'>"
irb(main):011:0> iris.target.vmethods
=> ["T", "__abs__", "__add__", "__and__", "__array__", "__array_finalize__", "__array_interface__", "__array_prepare__", "__array_priority__", "__array_struct__", "__array_wrap__", "__class__", "__contains__", "__copy__", "__deepcopy__", "__delattr__", "__delitem__", "__delslice__", "__div__", "__divmod__", "__doc__", "__eq__", "__float__", "__floordiv__", "__format__", "__ge__", "__getattribute__", "__getitem__", "__getslice__", "__gt__", "__hash__", "__hex__", "__iadd__", "__iand__", "__idiv__", "__ifloordiv__", "__ilshift__", "__imod__", "__imul__", "__index__", "__init__", "__int__", "__invert__", "__ior__", "__ipow__", "__irshift__", "__isub__", "__iter__", "__itruediv__", "__ixor__", "__le__", "__len__", "__long__", "__lshift__", "__lt__", "__mod__", "__mul__", "__ne__", "__neg__", "__new__", "__nonzero__", "__oct__", "__or__", "__pos__", "__pow__", "__radd__", "__rand__", "__rdiv__", "__rdivmod__", "__reduce__", "__reduce_ex__", "__repr__", "__rfloordiv__", "__rlshift__", "__rmod__", "__rmul__", "__ror__", "__rpow__", "__rrshift__", "__rshift__", "__rsub__", "__rtruediv__", "__rxor__", "__setattr__", "__setitem__", "__setslice__", "__setstate__", "__sizeof__", "__str__", "__sub__", "__subclasshook__", "__truediv__", "__xor__", "all", "any", "argmax", "argmin", "argpartition", "argsort", "astype", "base", "byteswap", "choose", "clip", "compress", "conj", "conjugate", "copy", "ctypes", "cumprod", "cumsum", "data", "diagonal", "dot", "dtype", "dump", "dumps", "fill", "flags", "flat", "flatten", "getfield", "imag", "item", "itemset", "itemsize", "max", "mean", "min", "nbytes", "ndim", "newbyteorder", "nonzero", "partition", "prod", "ptp", "put", "ravel", "real", "repeat", "reshape", "resize", "round", "searchsorted", "setfield", "setflags", "shape", "size", "sort", "squeeze", "std", "strides", "sum", "swapaxes", "take", "tobytes", "tofile", "tolist", "tostring", "trace", "transpose", "var", "view"]
irb(main):012:0> iris.target.to_a
=> [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2]

RubyではObject#classObject#methodsでオブジェクトの様々な状態の情報を取得することができますが、VirtualModuleもこれにならって似たようなメソッド(#vclass#vmethods)を用意しています。なんとなく想像できるかもしれませんが、#vclassはその値の型をバックグラウンドジョブに訪ねて返し、#vmethodsはそのオブジェクトについて利用可能なメソッドを返します。

今のところ用意している解説は以上ですが、もし更に他の例も見たい人がいればGitHubにいくつか他の例も置いてあるので参照可能です。実験的な実装なので大分使いづらいところも多いと思いますが、もし使いたい人がいたら使ってみた感想など教えてもらえると嬉しいです。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
9
Help us understand the problem. What are the problem?