読者です 読者をやめる 読者になる 読者になる

cocuh's note

type(あうとぷっと) -> 駄文

実験コードをCythonでゴリゴリ実装したら つらかった話

Python Cython 機械学習

でっかい実験コードをCythonで書いて死んだ話です。
記憶を便りに書いているので間違っていたことを書いてたら指摘してください。

tl;dr

なぜCythonを使った

理由はだいたいこんなかんじです。

  • 使いたいライブラリがC++だった
  • アルゴリズムが O(4N) でPythonなんか使ってられなかった
  • いままでjupyterで実験していた
    • データセット.npy(numpy)や.pickle(python)になってる
    • 全グラフ描画をmatplotlibでやってある

使いたいライブラリがC++でデータセット.npy.pickleで整備してあったので、
c++からboost.python叩くか・CythonでWrapper書いて叩くか、の二択でした。

これまでどおりjupyterから実験するならCythonでwrapperじゃろ、ということで使ったかんじです。
(ちなみにjupyter要素ここまでです。)

なにがつらかった

つらかったことはこんなかんじです。

  • Cython書きにくい
  • Cythonから使えるC++標準ライブラリが少ない
  • (Macだと Symbol not foundが出る)

Cython書きにくい

  • ->がない
    • config_ptr.load()みたいに.->みたいにつかえるためない
    • shared_ptrだと、.が使えない
  • template argumentに数字が使えない
    • Bitset<3>みたいなやつがだめ
  • 継承するごとにinterfaceをかかないといけない
  • auto がない
  • 人間に理解不能なエラーをgccが吐く
    • Cython(.pyx)を.cppにしてからshared object(.so)にするので、
    • Cython -> C++時にエラる(Cythonが、ここは読める
    • C++ -> so時にエラる(g++あたりが、よめない
    • メタプロの宿命

->がない

たとえば革命的なshared_ptrを使いたい時
(Cythonだとtemplateは[type]で指定します。)

from libcpp.memory cimport shared_ptr
from cython.operator cimport dereference

cdef class Converter:
    cdef shared_ptr[Config] config

    def __cinit__(self):
        self.config = shared_ptr[Config]() # <- nullptr
    
    def __init__(self, path):
        self.config = shared_ptr[Config](new Config())
        
        self.config->load(path) # エラー!
        self.config.load(path) # エラー!
        (*self.config).load(path) # なんでかエラー!!!
        dereference(self.config).load(path) # ok!

shared_ptrに(たしか)methodが生えてるので、それと区別するために->が使いたいのですが、 Cythonの言語仕様にはありません
dereferenceとかいう謎関数を噛ませてる必要があります。

ながいのでderefとかにしてますが、よく忘れてエラります。

from cython.operator cimport dereference as deref

継承するごとにinterfaceをかかないといけない

Cythonは外部にあるコードのinterfaceを記述しますが、継承がうまくうごいてません
なので使うインターフェイス 全部を型も含めて記述しないといけません

cdef extern from "hoge.h" namespace "ninja":
    cdef cppclass BaseNinja:
        BaseNinja()
        int call(Youjo)
        bool operator== (BaseNinja)
        bool operator== (HiwaiNinja)# ←書かないと叩けない!!!
    cdef cppclass HiwaiNinja:
        int call(Youjo) # ←書かないと叩けない!!!
        bool operator== (BaseNinja)# ←書かないと叩けない!!!
        bool operator== (HiwaiNinja)# ←書かないと叩けない!!!

こんな言語で大きいコード書けるわけがない

Cythonから使えるC++標準ライブラリが少ない

Cythonでimport libcppしてるのはここにあります。
基本的に 標準ライブラリはこのなかに有るものしか使えなくて 、 使いたいものは 自分でinterfaceを書く必要 があります。
(言語互換にする宿命かもしれないですけど)

実験中にboostのdynamic bitsetを叩く必要があったので、自分で.hppよみながらinterfaceを書いたりしました。

f:id:cocu_628496:20160131170302p:plain

github.com

どう解決した

Cythonの層を限りなく薄くしました

基本実装は全部C++にして、Pythonから扱いやすいdoubleとかintを出力する関数を C++ 書き、
Cythonはその関数を叩いてPythonオブジェクトに変換するだけにしました。

そうするとSAN値が減らなくなりました。

今回の知見

Cythonは、PythonからC++(or C)のオブジェクトを叩くというグルー言語としては非常に有用です。
しかし、グルー言語でガチ実装*1しようとすると私みたいに死にます

適材適所だなって実感しました。
ちなみに、CythonとC++で書いたコードで卒論はいい感じになりました。

*1:SAT solverを実装するとか