PythonモジュールをC++で作ってみたが(Windows)

カーネルトリックを使ってグラム行列を計算してみた。Pythonのforループ(2重)を使って書いたところ500秒くらいかかっていたが、べた書きのC++3重ループで110秒くらいだった。約5倍速くなった。VisualStudioの最適化オプションをいじってみたが、Releaseで特に指定しないのが一番速かった。。。ノルム計算のところを組み込み関数に置き換えてループひとつなくせばもっと速くなる気もするけどそんな関数あったか? バカ素直にサンプル数Nに対してNxN回の計算をしたけど、上三角行列分だけ計算したらもっと速いはず。それでも55秒+コピー分?

scipy.spatial.distanceのpdistとどっちが速いかだが・・・。

pairwise_dists = squareform(pdist(X, 'euclidean')) #pairwise distance計算(上三角)の結果を行列に
K = scip.exp(-pairwise_dists ** 2 / s ** 2)

RBFをカーネルとしたグラム行列の計算だとsklearn.metrics.pairwise.rbf_kernelがあったか・・・。rbf_kernelに乱数から生成したベクトルの集合を食わせて見たら思った以上に速かった。。。今日(10/6)実際のデータ、7800個の312次元ベクトル、で試してみたら1.4秒くらい?(ハハッ

ここ数日C++でモジュールを作ることを考えていたが、提供されているPythonモジュールの関数使って良さげなPython独自の書き方にした方が速い?(ここまで調べてそれか・・・)。Matlabの場合を思い出すが、結局うまいことPythonでの計算が速くなる書き方になんとか書き換えるしかないかも。

行列の要素同士の積(アダマール積)使えばかけそうね。はー

PythonモジュールをC++で作る(Mac)

$ brew install boost-python --with-python3

で以下のディレクトリにインストールされる。

/usr/local/Cellar/boost/1.65.1/include/    #header
/usr/local/Cellar/boost-python/1.65.1/lib/ #.a, dylib

中身を確認したけど、*.a が静的ライブラリで、*.dylib が共有ライブラリ(Linuxだと*.so)、という感じに生成されていた。自前でBoost.Pythonをビルドする必要はないようだ。

$ g++ -fPIC -I/usr/local/Cellar/python3/3.6.2/Frameworks/Python.framework/Versions/3.6/include/python3.6m/ -c test_hoge.cpp
$ g++ -shared -I/usr/local/Cellar/python3/3.6.2/Frameworks/Python.framework/Versions/3.6/include/ -L/usr/local/Cellar/python3/3.6.2/Frameworks/Python.framework/Versions/3.6/lib/ -lpython3.6 -lboost_python3 -lboost_numpy3 test_hoge.o -o test_hoge.so
$ otool -L test_hoge.so
test_hoge.dylib:
	test_hoge.dylib (compatibility version 0.0.0, current version 0.0.0)
	/usr/local/opt/python3/Frameworks/Python.framework/Versions/3.6/Python (compatibility version 3.6.0, current version 3.6.0)
	/usr/local/opt/boost-python/lib/libboost_python3.dylib (compatibility version 0.0.0, current version 0.0.0)
	/usr/local/opt/boost-python/lib/libboost_numpy3.dylib (compatibility version 0.0.0, current version 0.0.0)
	/usr/lib/libc++.1.dylib (compatibility version 1.0.0, current version 307.4.0)
	/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1238.0.0)

> import test_hoge
> test_hoge.hello()
'hello, world'

できた。なお、*.dylibとか*.pydとしてモジュールを作成すると、なぜかimportできなかった。なんでさ? homebrewでインストールしたPython3だとなんでこうなるのか・・・。


Mac版のコンパイル・リンクの方法はLinuxでもなぞらえてできるだろうな。Linuxの場合はapt-get installか何かでサクッとできると思う。


Windows/Mac/LinuxPythonモジュールをC/C++で作れるようになったわけで、じゃあC++でバリバリ便利な関数を作りましょうということで。(全部C++で書くのはどうなん?(え

PythonモジュールをC++(Boost.Numpy)で作ってみる(Windows版)

ひとまず何か書いてコンパイルしてみる。プロジェクト名やソースコード名は適当でいい。ここ(http://tadaoyamaoka.hatenablog.com/entry/2017/05/25/234934)のコードを参考にして以下。ひとつのモジュールに複数の関数を定義することも可能。

#define BOOST_PYTHON_STATIC_LIB 
#define BOOST_NUMPY_STATIC_LIB
//上記2行でBoostライブラリを静的にリンクすることを明示
#include 
#include 
#include 
#include 

namespace p = boost::python;
namespace np = boost::python::numpy;

char const* hello() {
	return "hello, world";
}

char const* hello2() {
	return "hello, hell";
}

BOOST_PYTHON_MODULE(test_hoge) {
	Py_Initialize();
	np::initialize();
	p::def("hello", hello);
	p::def("hello2", hello2);
}

ビルドが正常にすめば、プロジェクト内の/64/Release ディレクトリにプロジェクト名.dllファイルが生成される。生成されたファイルの拡張子をdllからpydに変更し、名前もtest_hogeに変更してtest_hoge.pydとした上で、pythonの作業ディレクトリにコピー(この管理方法は雑)。Pythonでためしにimportして実行。

>import test_hoge
>test_hoge.hello()
'hello'

というわけでひとまず動いた。



Numpyのndarray型を引数にとって、計算結果もndarray型で返したいわけで、どうすればいいのかね。ひとまずBoostのチュートリアルを覗いてみる。

ndarray型でNxNのゼロ行列を生成する関数をtest_hogeモジュールに追加してみる。

np::ndarray new_zero1(unsigned int N) {
	p::tuple shape = p::make_tuple(N,N); //正方行列のサイズ指定
	np::dtype dtype = np::dtype::get_builtin(); //要素の組み込み型を指定
	return np::zeros(shape, dtype); // ndarrayをnumpy.hppの関数で生成
}


BOOST_PYTHON_MODULE(test_hoge) {
	~~
        ~~
	p::def("new_zero1", new_zero1);


ndarrayの扱いを始めるまえに、ndarray型について復習。listと違って、ndarray型は連続したメモリアドレスを持つ。すべての要素は同じ型で、テンソルでいうところの各モードの要素数はすべて同じ。異なるサイズのlistをlist化したもののように不ぞろいなサイズの配列にはできない。加えて、ndarray型は一度宣言してメモリ上に確保されると、サイズを変えるためには全部コピーなりで配列を確保しなおすらしい。


さて、これでndarray型多次元配列を宣言できたわけですが、要素にアクセスしたり線形演算したり、引数にndarray型をとるにはどうしたらよいのか? ここ(https://qiita.com/termoshtt/items/81eeb0467d9087958f7f)になにやら書いていることが役に立ちそうな感じ。

void mult_two(np::ndarray a) {
  int nd = a.get_nd();
  if (nd != 1)
    throw std::runtime_error("a must be 1-dimensional");
  size_t N = a.shape(0);
  if (a.get_dtype() != np::dtype::get_builtin())
    throw std::runtime_error("a must be float64 array");
  double *p = reinterpret_cast(a.get_data());
  std::transform(p, p + N, p, [](double x) { return 2 * x; });
  //transform()の第1,2引数は始点・終点位置、第3引数は計算結果の格納位置、3つ目が処理。
}

この関数は1次元ndarray型の配列を引数とし、要素の値を2倍にする。配列の次元、サイズ、要素の型はそれぞれget_nd(), shape(), get_dtype()で得られる。get_data()はraw pointerを返しているらしい。ndarray.hppを見ると以下のコメントがあった。というわけでreinterpret_castでdouble型ポインタに。最後の行ではpからp+Nまでの要素に対してラムダ式で書かれた2倍する処理を行っている。というわけで生ポインタとして扱ってデータアクセスができている。宣言されたndarray型の配列が連続したメモリアドレスを持つからできるわけね・・・。

/**
   *  @brief Return the array's raw data pointer.
   *
   *  This returns char so stride math works properly on it.  It's pretty much
   *  expected that the user will have to reinterpret_cast it.
   */


最後に、多次元のndarray型変数を宣言してアクセス・演算することを考える(あとちょっと)。先ほど参考にしたサイト(ここ(https://qiita.com/termoshtt/items/81eeb0467d9087958f7f)の最後の部分を参考にする。2次元ndarray型変数を受け取ってその要素を順番に表示する関数ぽい。インデックスi,jの要素にアクセスしたいときに、i,j方向のメモリアドレス上の移動量をstride[0], stride[1]として取得し、ポインタを動かしている。strideについてはここ(https://docs.scipy.org/doc/numpy/reference/generated/numpy.ndarray.strides.html#numpy.ndarray.strides)にいろいろ書いてある。

void print(np::ndarray a) {
  int nd = a.get_nd();
  if (nd != 2)
    throw std::runtime_error("a must be two-dimensional");
  if (a.get_dtype() != np::dtype::get_builtin())
    throw std::runtime_error("a must be float64 array");

  auto shape = a.get_shape();
  auto strides = a.get_strides();

  std::cout << "i j val\n";
  for (int i = 0; i < shape[0]; ++i) {
    for (int j = 0; j < shape[1]; ++j) {
      std::cout << i << " " << j << " "
                << *reinterpret_cast(a.get_data() + i * strides[0] + j * strides[1]) << std::endl;
    }
  }
}

ここでのi,jはそれぞれ行・列のインデックスに相当している。というわけで、最低限の数値計算はできそうな気配が出てきた。


numpyの2次元のndarray型を引数として受け取ったとき、列ベクトルを取り出したり、列ベクトル同士で線形演算するメソッドが何か用意されているとうれしいのだが・・・。Boost.NumPyのヘッダファイルを見てみるか・・・。


help関数を使うと、モジュール内の関数一覧と各外部仕様を表示できるっぽい。C++で作成したモジュールの場合、C++の戻り型、引数の型とかが表示されるのね。でも結局これだけじゃようわからんわな・・・。

$ help('test_hoge')

PythonモジュールをC++(Boost.Numpy)で作るための準備(Windows環境)

ひとまずWindows上で動かせるようにする。なぜなら、数値計算用のマシンがWindowsだから・・・。インストール済みのminicondaがPython2.xだったのでアンインストールの上、Anacondaをインストール。AnacondaでインストールしたPythonは3.6ということで、今までWindowsにインストールしていた3.5はアンインストールすることにした。

Boost.Numpyは1.63からBoostに同梱されるようになったので、新しいものをインストールすればよさげ。インストール後、C:\ProgramData\Anaconda3\Library\include\boost\python下にnumpyディレクトリがあることを確認。

$ conda install -c conda-forge boost=1.65.0

コンパイル時は以下のディレクトリを指定すればよい(?)。Anaconda3のpkgディレクトリをみるとvc14って文字列が見えたけど、VisualStudio15でないとうまくいかないパターンやろか。。。

include directory: C:\ProgramData\Anaconda3\include, C:\ProgramData\Anaconda3\Library\include
include direcotry: C:\ProgramData\Anaconda3\libs, C:\ProgramData\Anaconda3\Library\lib

はい、Visual Studio13(vc12)ではだめですねー、dll吐き出せませんーー。もう組み込みの開発は終了したからVS2015に移行してもよい気がするけど、Let's Noteに入れてあるVS2015で作業する。というわけで、Anacondaのインストールからやり直しorz。

んで、やり直してビルドするもエラー。libboost_numpy3-vc140-mt-s-1_64.libがないと怒られた。libboost_numpy3-vc140-mt-1_64.libはあるけど、libboost_numpy3-vc140-mt-s-1_64.libはない。MultithreadはあるのにMultithread static runtimeがない。Boostのビルドからやり直しってやつか。なお、ネット上のブログなりQiitaなりを見る限り、Macだと自前でビルドせずにうまくいってるっぽい。


公式からWindows用のzipファイルをダウンロードしてローカルで展開。VS2015 x64 Native tool cmdを管理者権限で開き、展開したディレクトリのルートで以下を実行。

$ bootstrap.bat

次に、project-config.jamとuser-config.jamを編集。msvcとpythonの箇所でパス指定。どっちかかたっぽでよかったぽい? 前者の中身は以下。

import option ; 
using msvc : 14.0 : "c:/Program Files (x86)/Microsoft Visual Studio 14.0/VC/bin/amd64/cl.exe"; 
option.set keep-going : false ; 

後者のpython部分は以下。

# Configure specific Python version.
using python : 3.6.2 : C:/ProgramData/Anaconda3 : C:/ProgramData/Anaconda3/include : C:/ProgramData/Anaconda3/libs ;

設定ファイルを変更したら以下を実行。今回はpythonだけビルドすればよい。

$ .\b2 toolset=msvc threading=multi variant=debug,release link=static runtime-link=static address-model=64 --stagedir=stage/x64 -j 6 --with-python

結果、以下のファイルが出力されていた。

libboost_numpy3-vc140-mt-s-1_65.lib
libboost_numpy3-vc140-mt-sgd-1_65.lib
libboost_python3-vc140-mt-s-1_65.lib
libboost_python3-vc140-mt-sgd-1_65.lib

Boostを自前でビルドしたので, VSのパスも変更。

include directory: C:\ProgramData\Anaconda3\include, C:\Users\hitoh\Documents\boost_1_65_0
include direcotry: C:\ProgramData\Anaconda3\libs, C:\Users\hitoh\Documents\boost_1_65_0\stage\x64\lib

VSの設定を、

Release x64
構成プロパティ→全般→構成の種類を.dllに

あとはビルドすればよい。


理論部分は原稿にまでできているのに、実時間で計算させるための実装に時間くっててしぶい気持ち。。。

PythonモジュールをC++で(作りたい)

というか、多重ループ遅すぎてちょっと変わった大規模計算ができない。研究を進めようにも話にならない。というわけでモジュールをC/C++で書く方法を調べるものの、古い記事を漁ってもおそらくアップデートで色々変わってしまっている気がする。。。しかも、数値計算をする以上はnumpyのndarrayを扱えないと困るわけで・・・。Boost.PythonやらBoost.NumPyを使えば良さげだが、C+11用のpybind11を使った方がよいのか判断に迷う。基本はPython3.xを使っているわけで、はてさて・・・。

PythonならWin/Linux(Ubuntu)/Macでプラットフォームに依存せずに作業できると思ったが、C++python用のモジュールを作るとなるといやはや・・・。macだったら適当にbrew install でなんとかなりそうだが、Boost.NumPyはビルドせなあかんみたい。


と思ったが、Anacondaから簡単に入れらるっぽい。Boost 1.63 から Boost.NumPyも同梱されるようになったようだ。もう明日研究室のPC(Win)で試せばいいかな。。。 Mac Book Proにhomebrewでboost-python-1.65をインストールはしてみたが、1. Boost.NumPyは入っているのか? 2. Python2.x or 3.x どっち対応なのか?。


確認したところ、以下Python2.xと3.x用のインストール。二つインストールすると競合するかもだが、3.xしか使わないのでおk。Pyenvでエラー起きてた報告もあるし、余計なことはしない方針で。

 $ brew install boost-python 
 $ brew install boost-python --with-python3

Mac Book Pro から Linx の VNCserverに接続

Linux側の設定が全部終わっている状態で、Mac Book Proの127.0.0.1:5901にトンネルを掘る。その上で、Finder→サーバーに接続→vnc://127.0.0.1:5901として接続。TigerVNCだとキーボード入力に問題がある。こちらでしばらく使う。

いままで使っていた通りに

$ brew cask install xquartz
$ brew install tiger-vnc

でいれてみたものの、リモート側の画面ロック解除のパスワード入力すらできず、ターミナルにエラーが・・・。

新しい環境ではServerAliveInterfvalをしてしないと接続がブチブチ切れる。

8/6 追記: ぶちぶち切れると思っていたのは、そのとき思いデータダウンロードしていたものの、Wifiルーター通信制限はじまってしまったことが原因みたい・・・。