前回Go
でニューラルネットワークを作成して、コンパイル言語らしく良いパフォーマンスを出してくれた。
https://nnt339es.hatenablog.com/entry/2020/11/24/195430nnt339es.hatenablog.com
このままGo
に移行しても良いと思ったが、リファクタリングにはそれなりのコストがかかるため、Python のまま高速化できないか模索してみた。
- 没案
\*\* Cython
まず考えたのはCython
によるリファクタリングだ。
Cython
とはPython
で書いたものを C 言語に翻訳してコンパイルするための言語である。
元々Python
はC
による拡張が可能。公式ドキュメント。
ドキュメントの例にもあるようにPython.h
ヘッダを利用して、関数がPyObject
という形で引数を受け取って処理する、というような記法になる。
これを1つ1つ実装するのは大変なのでPython
コードからC
コードを生成してしまおうというのがCython
の試みだ。
詳しいことは別の記事にして、なぜ没だったか説明しよう。
結論から言うと、速くならなかったからだ。
通常、Python
の圧倒的に処理の遅い for 文の数値計算をCython
で書き直すと処理速度がぐっと上がるが、私のコードは計算部分はnumpy
に 100%任せているのでその恩恵を受けられなかった。むしろ関数呼び出しのオーバーヘッドが増えたのか、処理は遅くなった。
比較的少ないCython
についての文献を頑張って漁って実装した見返りがこれとは。。。
\*\* BLAS の更新
次のアプローチはnumpy
の処理速度を上げることで、BLAS 変えてコンパイルすることを検討した。HPC の講義中に導入できなかった BLAS
最速を目指したかったのでまずATLAS
のコンパイルを行ったが、大量のエラーに対して半日で心が折れてOpenBLAS
にした。
OpenBLAS
も全くコンパイルできず、諦めて windows 用のコンパイル済みバイナリをネットから拾ってきた。
OpenBLAS
を使ったnumpy
のコンパイルでまた四苦八苦。コンパイルできたと思ったら dll の置き場所がわからなくてギブアップ。
何も得られないまま数日無駄にした。BLAS うらめしや
numpy
が元々OpenBLAS
でコンパイルされていることは言わないお約束。
- C++でニューラルネットワークを実装
次に思いついたのが、ネットワークの部分だけC
で実装してPython
から呼び出すことだ。
結論から言うと、これは上手くいったが、それは長い長い道のりだった。
\*\* 線形代数ライブラリ Eigen
最初OpenMP
を使った線形演算関数を作っていたが、こんなの無駄だと気付きライブラリを探したところ、Eigen
というライブラリが紹介されていた。
API が直感的で可読性の高いコードが書けるとのことだが、使い勝手はnumpy
には及ばず。
導入がインクルードだけで良いことは評価できる。
要はBLAS
のラッパなので高速。
\*\* C++との闘い
Atcoder で使用していたため基礎はわかっていたが、コンストラクタの書き方は?ヘッダの書き方は?と計算処理以外の所でよく詰まった。
VSCode のインテリセンスが強力であることと、使用者が多いためコンパイルエラーが起こっても答えが Google に絶対あるというのが助かった。
\*\* Cython でラップ
Python
から C で実装したニューラルネットを使用するためにCython
でラップしてモジュールとしてコンパイルする。(Cython
に使った時間は無駄じゃなかった!)
C
コードをPython
っぽく書けると聞くと良さそうに思えるが、実際には悪い点だらけだ。
Cython
使用者がメチャクチャ少ないため、日本語文献はもちろん英語で聞いても Google 先生は的確に答えてはくれない。
使用者が少ないため、ノウハウがそこらへんに落ちていない。Cython
から C 関数へ配列ポインタの渡し方さえ私にはわからない。そのためstd::vector
で楽をしている。
Python
コードは確実にコンパイルできるが、どの程度C
らしい文法を使えるのかはっきりしていない。(Google 先生もあまり良い答えはくれない。)ポインタは使えるのか?アドレス演算子は?new は?delete は?
VScode でインテリセンスが効かない。。。作ったモジュールもインテリセンスなし。。。
nnt339es.hatenablog.com
Cython
についての記事はもう少し理解が進んだらまとめます。
ラッパーコードだけ載せるとこんな感じ。
> |python|
> from libcpp.vector cimport vector
> import numpy as np
> cimport numpy as np
> DTYPE = np.float64
> ctypedef np.float64_t DTYPE_t
# C++で実装したクラスの読み込み
cdef extern from "network.hpp":
cdef cppclass Network:
Network(vector[int] layer*structure, double learningRate)
vector[double] outputs(vector[double] input*)
void train(vector[vector[double]] inputs, vector[vector[double]] expecteds)
void trainForEpoch(vector[vector[double]] inputs, vector[vector[double]] expecteds, int epoch)
cdef class cNetwork:
cdef Network\* network
def **init**(self, vector[int] layer_structure: list[int], double learning_rate: float) -> None:
self.network = new Network(layer_structure, learning_rate)
def __dealloc__(self):
if self.network != NULL:
del *self.network
cpdef outputs(self, np.ndarray[DTYPE_t, ndim=1] input_: np.ndarray):
cdef vector[double] vin = input_
cdef vector[double] x = self.network.outputs(vin)
cdef np.ndarray[DTYPE_t, ndim=1] res = np.asarray(x, dtype=DTYPE)
return res
cpdef train(self, vector[vector[double]] inputs: list[np.ndarray], vector[vector[double]] expecteds: list[np.ndarray]):
self.network.train(inputs, expecteds)
cpdef trainForEpoch(self, vector[vector[double]] inputs: list[np.ndarray], vector[vector[double]] expecteds: list[np.ndarray], int epoch):
self.network.trainForEpoch(inputs, expecteds, epoch)
|