Pythonと機械学習

Pythonも機械学習も初心者ですが、頑張ってこのブログで勉強してこうと思います。

CythonでC++コードをラッピング

目次

はじめに

PythonC++に比べて圧倒的に実行速度が遅いのですが、それでもPythonを使いたいと思うのはなんででしょうかね?

コーディングのしやすさや、何よりサードパーティのモジュールが豊富にあることですかね。

これがあったらいいなって言うのが大抵ありますね。Jupyterもあるし。

C++の実行速度のままPythonで書ければ最高ですね。

というわけで、今回はC++で作成したコード(具体的にはクラス)をCythonでラップしてPythonから使える様にしてみようと思います。

C++テストコード

先ずは、C++でクラスを作ります。前々回Cythonでテストをした時にも作りましたが、正数の累積和を求めるメンバ関数を持ったクラスを作ってみたいと思います。

ヘッダーファイルはこんな感じです。クラスの宣言部そのままです。cpp_test_sum_.hという名前にしておきます。namespece (7行目です)をつけてやるのがコツです。

最初の#ifndefと最後の#endifは、もしCPPTESTSUM_Hが定義されていなかったら、2行目の#defineで定義してそれ以降の記述を実行します。

逆にもし1行目時点で、CPPTESTSUM_Hが定義されていたら、それ以降は#endifまで実行しません。

ヘッダーファイルの2重読み込みを防止するためのもので、C++でヘッダーファイルを書くときは慣例的に追加するみたいです。

メンバ関数の定義はファイルを分けておきます。cpp_test_sum_.cppという名前で保存します。

ヘッダーファイルcpp_test_sum_.h" "で囲って# includeしておきます。今いるフォルダから# includeするという意味です。

ちなみに、今回に限らず、C++でライブラリを作成するときは、ヘッダーとメンバ関数の定義は、ファイルを分けて作成するのが普通みたいです。

Cythonラッパーコード

C++コードをラッピングするCythonコードを書いていきましょう。

以下、C++コードをラッピングするCythonコードです。cpp_test_sum.pyxという名前で保存します。

注意点ですが、C++のコードはファイル名の後ろに_を付けてCythonコードとは別の名前にしておきます。

1行目のfrom libcpp.vector cimport vectorは、C++で使えるvectorをインポートしています。libcppの中には、C++で使えるオブジェクトが入っているみたいです。他に確認したのは、string等。必要に応じてインポートします。

3行目のcdef extern from "cpp_test_sum_.h" namespace "cpp_test_sum":は、C++のヘッダーのインポートをしています。namespaceC++ヘッダー内で定義している物を指定します。

4行目のcdef cppclass cppTestSum:は、C++コード内のクラスをCython内にプロトタイプ宣言しています。

5行目のcppTestSum(int n_iter) except +は、コンストラクターのプロトタイプ宣言です。except +を後ろに付けるといいみたいです。(理由は何かのエラーの回避みたいです。)

7行目vector[double] sumはメンバ変数sumの宣言です。vectorのタイプは[ ]で囲んで宣言します。C++< >から変えておく必要があります。

11行目~29行目までが、C++のクラスcppTestSumをCythonのクラスTestSumに置き換えている部分です。

C++のクラス名cppTestSumは前にcppを付けて、Cython内で定義するのクラスTestSumと名前が被らないようにしています。

12行目cdef cppTestSum* thisptrでは、C++クラスcppTestSumをCythonクラスTestSum内で使える変数としてthisptrという名前でポインターで宣言しています。

14行目、17行目は、所謂コンストラクターとデストラクターにあたる部分です。コンストラクターでは、C++クラスcppTestSumnewで生成してthisptrに入れており、デストラクターでは、delthisptrを解放しています。

20行目def calc_sum(self):は、メンバ関数の定義です。単にC++クラスcppTestSumメンバ関数returnで返しているだけです。

23行目~29行目がメンバ変数の定義です。propertyで定義して、__get____set__関数で呼び出し、代入をすることで、通常のパブリック変数の様に.で呼び出し、=で代入ができるようになります。

コンパイル用setup.py

以下がC++コードcpp_test_sum_.cppをラッピングしてCythonコードcpp_test_sum.pyxコンパイルするsetup.pyです。

7行目のname = "cpp_test_sum",と9行目のExtension("cpp_test_sum",ですが、Cythonコードと同じ名前にしておきます。

10行目のsources=["cpp_test_sum.pyx", "cpp_test_sum_.cpp"],は、Cythonコードと、C++コード名を入れておきます。

11行目のextra_compile_args=["-O3"],は、g++のコンパイルオプションを指定しています。コンパイルでは自動で高速化オプションの-Oを付けてくれるみたいですが、一応-O3オプションも付けておきます。

C++ヘッダーcpp_test_sum_.hとクラス定義コードcpp_test_sum_.cpp、またCythonラッピングコードcpp_test_sum.pyxsetup.pyを同じフォルダに入れて、以下のコマンドでコンパイルします。

python setup.py build_ext -i

コンパイルに成功すると、cpp_test_sum.pydができます。Pythonスクリプト内でモジュールとしてインポートできます。

実行スピード比較

早速実行スピードを、Python、Cython、C++ラッパーで比較してみましょう。

前々回Cythonでテストをした時に作った、Pythonモジュールpy_test_sum.pyとCythonモジュールcy_test_sum.pydを、作成したC++ラッパーモジュールcpp_test_sum.pydと同じフォルダに入れて以下の実行スピード比較スクリプトを実行してください。

以下実行結果です。

Python Sum: 4.9999995e+13. Elapsed time: 14.8093574652 sec.
Cython Sum: 4.9999995e+13. Elapsed time: 0.128290968028 sec.
C++    Sum: 4.9999995e+13. Elapsed time: 0.0189597421757 sec.

C++はやっぱりすごいですね。Pythonの781倍、Cythonの6.8倍も速いですね。

C++で書いたRRLのコードをCythonでラッピング

前回C++で書いたRRLのコードをCythonでラップしてPythonから読めるようにしたいと思います。

スクリプトは長いのでGistを使うのはやめてGithubにアップしました。

今回の記事の内容は、上記のリポジトリ04_cython_cppwraperの中に全部入れました。

C++のヘッダーtradingrrl_.hメンバ関数定義部tradingrrl_.cpp、Cythonラッピングコードtradingrrl.pyxコンパイルsetup.pyまた、RRLの学習実行スクリプトmain.py以外は全て2次的に生成されたものです。

ちなみに今回のテストコードは全てtestフォルダに入っています。

python setup.py build_ext -iコンパイル後、python main.pyで学習が開始されます。

以下、main.pyの実行結果です。

Epoch loop start. Initial sharp's ratio is -0.0135567.
Epoch: 100 / 10000. Shape's ratio: 0.0343914. Elapsed time: 0 sec.
Epoch: 200 / 10000. Shape's ratio: 0.0554093. Elapsed time: 0 sec.
Epoch: 300 / 10000. Shape's ratio: 0.0802484. Elapsed time: 0 sec.
...
...
Epoch: 9800 / 10000. Shape's ratio: 0.169652. Elapsed time: 15 sec.
Epoch: 9900 / 10000. Shape's ratio: 0.169963. Elapsed time: 15 sec.
Epoch: 10000 / 10000. Shape's ratio: 0.169771. Elapsed time: 15 sec.
Epoch loop end. Optimized sharp's ratio is 0.169771.
Epoch: 10000 / 10000. Shape's ratio: 0.169771. Elapsed time: 15 sec.

C++の実行速度を保ちつつ、Pythonで実行出来ました!感無量ですね!

参考にさせていただいたサイト