【PyCUDAでDeep Learningその1】PyCUDAから作るニューラルネットワーク
はじめに
PyCUDAを使ったニューラルネットワーク実装例を示します。Tensorflow, Chainer, KerasなどのDeep LearningライブラリからGPUを使う方法が一般的です。しかし、PyCUDAを使ったGPGPUプログラミングを経験することで、より理解が深まると思います。用いるニューラルネットワークは入力層を含めて3層パーセプトロンの簡易版です。また、MNIST手書き数字の学習および識別を行います。
対象者
- PyCUDAでDeep Learningに入門したい
- PyCUDAによるGPGPUに興味がある
- Deep Learningフレームワークを用いたくない
前提条件
- Python3
- Numpy
- PyCUDA実行環境
Mac使いでPyCUDAの実行環境がまだの人は以下の記事を参考にしてください。
- [PyCUDA環境構築 その1] MacにPython3とNumpyをインストール
- [PyCUDA環境構築 その2] Xcode7とXcode8をインストールして、切り替える方法
- [PyCUDA環境構築 その3] MacにCUDA8をインストール
- [PyCUDA環境構築 その4] PyCUDAをMacOS 10.12 Sierraにインストール
注意事項
ここで用いるニューラルネットワークはかなりテキトーなので、真面目に勉強したい人は他のサイトや書籍を参考にしてください。
- バイアスなし
- 活性化関数はReluのみ
- Softmaxすらない
ネットワークについて詳しくは数式なしでDeep Learning入門1 多層パーセプトロンをご覧ください。
実装
PyCUDA実装の要点
Numpyを用いた実装例は数式なしでDeep Learning入門1 多層パーセプトロンにmlp.pyとして示してあります。ここで示すPyCUDA版と比較してみてください。
PyCUDAでGPGPUプログラミングするときの要点は以下の通りです。
import pycuda.autoinit
linalg.init()
- NumpyのndarrayからPyCUDAのgpuarrayへの変換 一旦Numpyのndarrayを作ってから、gpuarrayに変換します。この際、データ変換と転送が発生します (CPU->GPU)。
w_hid = gpuarray.to_gpu(np.random.rand(n_hid, n_inp).astype(np.float32)/n_inp)
データタイプはfloat32を用いるのが無難です。逆変換 (GPU->CPU)は
w_hid_cpu = w_hid.get()
とするだけです。print()などで結果を表示したり、CPU側で計算したいときはこの処理が必要です。
y = linalg.dot(w, x)
はエラーとなります。
y = lialg.dot(w, x.reshape(2, 1))
とする必要があります。
PyCUDA版の実装例
変数名や関数名のpostfixに「gpu」がついているものはGPU用です。ただし、重みについては付けていません。少しくどくなるからです。
import numpy as np from sklearn.datasets import fetch_mldata from skcuda import linalg from pycuda import gpuarray # GPU初期化 import pycuda.autoinit linalg.init() # ネットワーク定数 n_inp = 784 n_hid = 1024 n_out = 10 # 学習定数 batch = 100 learn_rate = 0.01 update_rate = learn_rate/batch # 重み w_hid = gpuarray.to_gpu(np.random.rand(n_hid, n_inp).astype(np.float32)/n_inp) w_out = gpuarray.to_gpu(np.random.rand(n_out, n_hid).astype(np.float32)/n_hid) dw_hid = gpuarray.to_gpu(np.zeros(w_hid.shape).astype(np.float32)) dw_out = gpuarray.to_gpu(np.zeros(w_out.shape).astype(np.float32)) # MNIST n_train = 60000 mnist = fetch_mldata('MNIST original') data = mnist.data.astype(np.float32) target = mnist.target.astype(np.int32) x_train, x_test = np.split(data, [n_train]) d_train, d_test = np.split(target, [n_train]) n_test = d_test.size # 入力値を0~1に正規化 x_train /= 255 x_test /= 255 # 正解 (GPU) ans_gpu = [] for i in range(n_out): x = np.zeros(n_out).astype(np.float32) x[i] = 1 ans_gpu.append(gpuarray.to_gpu(x).reshape((n_out, 1))) # GPU転置行列 def T_gpu(x_gpu): return linalg.transpose(x_gpu) # GPU内積 def dot_gpu(a_gpu, b_gpu): return linalg.dot(a_gpu, b_gpu) # GPU外積 def outer_gpu(a_gpu, b_gpu): c_gpu = b_gpu.reshape(b_gpu.shape[1], b_gpu.shape[0]) return linalg.mdot(a_gpu, c_gpu) # 誤差微分 def diff_err_gpu(x_gpu, e_gpu): z_gpu = gpuarray.to_gpu(np.zeros(x_gpu.shape).astype(np.float32)) return gpuarray.if_positive(x_gpu > z_gpu, e_gpu, z_gpu) # relu関数 def relu_gpu(x_gpu): z_gpu = gpuarray.to_gpu(np.zeros(x_gpu.shape).astype(np.float32)) return gpuarray.if_positive(x_gpu > z_gpu, x_gpu, z_gpu) # 各層の出力を計算 def output_gpu(x): x_gpu = gpuarray.to_gpu(x).reshape((n_inp, 1)) hid_gpu = relu_gpu(dot_gpu(w_hid, x_gpu)) out_gpu = relu_gpu(dot_gpu(w_out, hid_gpu)) return x_gpu, hid_gpu, out_gpu # 学習していない数字を正しく識別できるか予想 def predict(): score = 0 for i in range(0, n_test): _, hid_gpu, out_gpu = output_gpu(x_test[i]) out = out_gpu.get().reshape(n_out) if d_test[i] == np.argmax(out): score += 1 print('test: {0}%'.format(score / 100)) # 学習用の数字で重みを更新 for epoch in range(1, 2): print('epoch: {0}'.format(epoch)) # ランダムな順序に学習 perm = np.random.permutation(n_train) # 100個単位で学習 for i in range(0, n_train, batch): x_batch = x_train[perm[i:i+batch]] d_batch = d_train[perm[i:i+batch]] # 学習データを100個みせる for j in range(batch): x_gpu, hid_gpu, out_gpu = output_gpu(x_batch[j]) # 誤差計算 e_out = ans_gpu[d_batch[j]] - out_gpu e_hid = T_gpu(dot_gpu(T_gpu(e_out), w_out)) e_hid = diff_err_gpu(hid_gpu, e_hid) dw_out += outer_gpu(e_out, hid_gpu) dw_hid += outer_gpu(e_hid, x_gpu) # 100個みせたので重み更新 w_out += dw_out * update_rate w_hid += dw_hid * update_rate dw_out *= 0.9 dw_hid *= 0.9 # 学習が進んでいるか1000個ごとに識別予想 if (i%1000) == 0: predict()
結果
基本的にはCPU版のmlp.pyの時と同等の結果が得られました。
epoch: 1 test: 9.8% test: 17.06% test: 10.2% test: 10.2% test: 24.61% test: 41.79% test: 42.53% test: 60.8% test: 59.03% test: 59.77% test: 62.69% ... epoch: 10 test: 96.89% test: 96.96% test: 96.87% test: 96.93% test: 96.87% test: 96.98% test: 96.98% test: 96.92% test: 96.97% test: 97.02% test: 97.05%
使用したマシンは以下の通りです。
- MacBook Pro, 2014 Mid
- MacOS Sierra 10.12.6
- Core i7 2.8GHz/16GBメモリ
- NVIDIA GeForce GT 750M 2GB GDDR5
- Xcode 7.3.1 & Command Line Tools
次にパフォーマンスについて調べてみます。
CPU v.s. GPU
ここでは、CPU版のmlp.pyとGPU版のmlp_gpu.pyを速度比較してみましょう。時間がかかるので、epoch数は1とします。
for epoch in range(1, 11):
を
for epoch in range(1, 2):
と変更してください。
また、最後のpredict()をコメントアウトして、学習時間だけを計測します。
#if (i%1000) == 0: #predict()
$ time python3 mlp.py ... $ time python3 mlp_gpu.py
で計測します。
1 epoch, 学習のみ
- CPU/mlp.py
n_hid=64, 0m5.363s n_hid=1024, 1m12.998s n_hid=4096, 4m17.055s n_hid=8192, 11m52.893s
n_hid=64, 1m38.650s n_hid=1024, 4m17.613s n_hid=4096, 5m6.054s n_hid=8192, 7m17.181s
ニューロン数が少ないとCPU演算の方が高速なことがわかります。むやみやたらとGPUを用いるとかえってパフォーマンスが落ちます。3層の単純なニューラルネットワークでは隠れ層のニューロンを多くしても今回の課題に対して良い結果をもたらしません。このようなケースではCPU演算で十分です。
入力データであるMNIST(具体的にはx_testとx_train)を事前にgpuarrayに変換して保持することで、多少のパフォーマンス改善が得られます。興味のある方は試してみてください。