RubyからPythonやJuliaのパッケージを呼び出せるvirtual_module
というgemを作っています。下記の例では、いくつかのコマンドのmanpageをドキュメントとして読み込むところまでをRubyで書いて、doc2vecの処理にかける部分はPythonを呼び出してgensimに任せるということをやっています。
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#class
やObject#methods
でオブジェクトの様々な状態の情報を取得することができますが、VirtualModuleもこれにならって似たようなメソッド(#vclass
と#vmethods
)を用意しています。なんとなく想像できるかもしれませんが、#vclass
はその値の型をバックグラウンドジョブに訪ねて返し、#vmethods
はそのオブジェクトについて利用可能なメソッドを返します。
今のところ用意している解説は以上ですが、もし更に他の例も見たい人がいればGitHubにいくつか他の例も置いてあるので参照可能です。実験的な実装なので大分使いづらいところも多いと思いますが、もし使いたい人がいたら使ってみた感想など教えてもらえると嬉しいです。