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):
def mul(n):
print("from python")
print("received: " + str(n))
print("end from python")
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() {
Py_Initialize();
np::initialize();
auto main_ns = boost::python::import("__main__").attr("__dict__");
std::ifstream ifs("mat_numpy.py");
std::string script((std::istreambuf_iterator<char>(ifs)),
std::istreambuf_iterator<char>());
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;
}
}
boost::python::exec(script.c_str(), main_ns);
auto func = main_ns["mul"];
auto pyresult_numpy = func(A);
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());
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を入力する場合、
boost::python::exec(script.c_str(), main_ns);
auto func = main_ns["mul"];
auto pyresult_numpy = func(5);
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が動いているわで突き詰めて考えると闇です。