verilog書く人

自称ASIC設計者です。どなたかkaggle一緒に出ましょう。

C++からPythonを叩きつつ、boost.numpyを使ってC++とPython間でndarrayをやりとりする

C++メインで作られているシステムからchainerだったり、scikit-learnだったりを使って機械学習をしているPythonモジュールを呼び出しとデータをやりとりさせたいとします。

 

すると、C++の入力データ(n次元array)をnumpyに変換してPythonに渡し、Pythonからnumpyで返ってくるデータを解釈する必要があります。

 

 

はじめはC++からPythonにデータを引き渡すため、評判がいいprotobufを使おうとしました。

 

protobuf自体はイケてるのですが、

C++ array→protobuf→numpy

とまあ変換変換でなんか冗長で嫌です。

 

また、C++からは普通の二次元配列(double* num[]のような)を渡して、Python側でnumpyに変換する方法もありますが、C++側のarrayも単純な二次元配列と限らずEigenだったりCvmatだったりするので、やっぱり余分な変換があって冗長で嫌なのです。


boost.numpyあるんだし、C++の任意のarrayをboost.numpyに変換し、numpyのままダイレクトにPythonに渡してみようというお話です。

インストール

boost.pythonとboost.numpyがインストール済であることが前提です。

boost1.63はベータ版ですが、boost.numpyが同梱されているので楽です。

ただし以下の検証はboost1.62で行いました。

コード

Python側(mat_numpy.py):

# nはnumpy.ndarray
def mul(n):
    print("from python")
    print("received: " + str(n))
    print("end from python")

    # ndarrayのタプルを返す
    return (2 * n, 3* n)

本当は沢山処理があるんでしょうが、今回はこれだけ。

 C++側:

#include <iostream>
#include <string>
#include <fstream>
#include <streambuf>
#include <boost/python.hpp>
#include <boost/numpy.hpp>
#include <list>

#define MAX_X 100 //受け渡したい行列のサイズ。

namespace np = boost::numpy;

int main() {

    //Python、numpyモジュールの初期化
    Py_Initialize();
    np::initialize();

    //名前空間の確保
    auto main_ns = boost::python::import("__main__").attr("__dict__");

    //Pythonスクリプトの読み込み
    std::ifstream ifs("mat_numpy.py");
    std::string script((std::istreambuf_iterator<char>(ifs)),
                        std::istreambuf_iterator<char>());

    //100x100行列の準備
    boost::python::tuple shapeA = boost::python::make_tuple(MAX_X, MAX_X);
    np::ndarray A = np::zeros(shapeA, np::dtype::get_builtin<double>());
    for(int i=0; i != MAX_X; i++) {
        for(int j=0; j != MAX_X; j++) {
            A[i][j] = i+j;
        }
    }

    //mat_numpy.mulの実行
    boost::python::exec(script.c_str(), main_ns);
    auto func = main_ns["mul"];
    auto pyresult_numpy = func(A);

    //結果の受け取り
    //stl_input_iteratorを使ってタプル全要素を受け取る
    boost::python::stl_input_iterator<np::ndarray> begin(pyresult_numpy), end;
    std::list<np::ndarray> pyresult_list(begin, end);

    for(auto itr = pyresult_list.begin(); itr != pyresult_list.end(); ++itr) {
        double *p = reinterpret_cast<double *>((*itr).get_data());
        //ndarrayでは基本的にメモリは連続領域上に保持されるので、
//各要素には[]演算子を使ってアクセスできる
std::cout << p[0] << ',' << p[1] << ',' << p[2] << ',' << p[MAX_X * MAX_X - 1] << std::endl; } return 0; }

 

出力

from python
received: [[   0.    1.    2. ...,   97.   98.   99.]
 [   1.    2.    3. ...,   98.   99.  100.]
 [   2.    3.    4. ...,   99.  100.  101.]
 ...,
 [  97.   98.   99. ...,  194.  195.  196.]
 [  98.   99.  100. ...,  195.  196.  197.]
 [  99.  100.  101. ...,  196.  197.  198.]]
end from python
0,2,4,396
0,3,6,594

 

無事動いてます。

キモは

    auto pyresult_numpy = func(A);

の行で、この中でAはPyObjectとして解釈されてPythonに渡されます。

しかし、今はAはPyObjectの子クラスそのものなので、そのまま通るわけです。

パフォーマンス


ちゃんと測っていませんがC++での行列の初期化→Pythonへの受け渡し→Pythonでの計算→C++での受け渡しを通して、デバッグモードにて0.1secだったので、私の中では満足しました。
言語間の引渡しはポイント渡しでしかないのであまり時間はかかってない模様。

 

おまけ

func(A)ではオーバーロードができます。

これはfuncの実体である、mat_numpy.py内で定義されているmul(n)自体はnがndarrayに限らず、任意の掛け算が定義されているオブジェクトが入力されていれば、使えるからです。

たとえば行列の代わりにintを入力する場合、

 

    //mat_numpy.mulの実行
    boost::python::exec(script.c_str(), main_ns);
    auto func = main_ns["mul"];
    auto pyresult_numpy = func(5);

    //結果の受け取り
    //stl_input_iteratorを使ってタプル全要素を受け取る
    boost::python::stl_input_iterator<int> begin(pyresult_numpy), end;
    std::list<int> pyresult_list(begin, end);

    for(auto itr = pyresult_list.begin(); itr != pyresult_list.end(); ++itr) {
        std::cout << *itr << std::endl;
    }

 

のようにC++側のコードを変更すればいいです。

 

感想

コード間のインタフェースはデバッグが大変です。
それぞれのコードモジュールは単体でテストできるようにする。インタフェースはきっちりシンプルで使いまわしの効くラッパーを作ってあまりいじらないのがいいと思います。

あと、Python側の変更には再コンパイルが必要なかったりして、イイ感じです。


シンプルと言いつつ、C++コードの中でPythonが動いているわ、numpyの背面ではshared_objectとしてCが動いているわで突き詰めて考えると闇です。