sein's blog

ディープラーニング, 人工知能, 自然言語処理 ~ Deep Learning, AI, NLP ~

【PyCUDAでDeep Learningその1】PyCUDAから作るニューラルネットワーク

はじめに

PyCUDAを使ったニューラルネットワーク実装例を示します。Tensorflow, Chainer, KerasなどのDeep LearningライブラリからGPUを使う方法が一般的です。しかし、PyCUDAを使ったGPGPUプログラミングを経験することで、より理解が深まると思います。用いるニューラルネットワークは入力層を含めて3層パーセプトロンの簡易版です。また、MNIST手書き数字の学習および識別を行います。

f:id:kanemura:20170818121344p:plain

対象者

前提条件

  • Python3
  • Numpy
  • PyCUDA実行環境

Mac使いでPyCUDAの実行環境がまだの人は以下の記事を参考にしてください。

Windows/Linuxの人はググれば結構ヒットします。

注意事項

ここで用いるニューラルネットワークはかなりテキトーなので、真面目に勉強したい人は他のサイトや書籍を参考にしてください。

  • バイアスなし
  • 活性化関数はReluのみ
  • Softmaxすらない

ネットワークについて詳しくは数式なしでDeep Learning入門1 多層パーセプトロンをご覧ください。

実装

PyCUDA実装の要点

Numpyを用いた実装例は数式なしでDeep Learning入門1 多層パーセプトロンmlp.pyとして示してあります。ここで示すPyCUDA版と比較してみてください。

PyCUDAでGPGPUプログラミングするときの要点は以下の通りです。

  • GPU関連の初期化 まずGPUを初期化して使える状態にします。
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側で計算したいときはこの処理が必要です。

  • 内積外積を計算する際はreshapeする必要があります。例えば、w.shapeが(3, 2)、x.shapeが(2, 0)とします。
y = linalg.dot(w, x)

はエラーとなります。

y = lialg.dot(w, x.reshape(2, 1))

とする必要があります。

PyCUDA版の実装例

変数名や関数名のpostfixに「gpu」がついているものはGPU用です。ただし、重みについては付けていません。少しくどくなるからです。

mlp_gpu.py

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%

使用したマシンは以下の通りです。

次にパフォーマンスについて調べてみます。

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, 学習のみ

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に変換して保持することで、多少のパフォーマンス改善が得られます。興味のある方は試してみてください。

おすすめ書籍