Pythonのニューラルネット学習を高速化

前回Goニューラルネットワークを作成して、コンパイル言語らしく良いパフォーマンスを出してくれた。
https://nnt339es.hatenablog.com/entry/2020/11/24/195430nnt339es.hatenablog.com

このままGoに移行しても良いと思ったが、リファクタリングにはそれなりのコストがかかるため、Python のまま高速化できないか模索してみた。

  • 没案

\*\* Cython
まず考えたのはCythonによるリファクタリングだ。

CythonとはPythonで書いたものを C 言語に翻訳してコンパイルするための言語である。

元々PythonCによる拡張が可能。公式ドキュメント
ドキュメントの例にもあるように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で実装して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)

|