LoginSignup
55
41

More than 5 years have passed since last update.

ChainerでGPUを使うと毎回結果が変わる理由と対策

Last updated at Posted at 2017-01-23

本記事はChainerやTensorFlowでGPUを使うと毎回結果が変わる理由と対策 (まとめ)の詳細編であり、Chainerについて具体的な対策について検証します。

関連Issues(Chainer)

検証環境

Chainer v1.20.0
Python 3.5.2
CUDA 8.0
cuDNN 5.1
NVIDIA Tesla/GeForce いろいろ

まずは乱数種の指定

GPUの演算誤差だけでなく乱数を用いていることも演算結果が安定しない原因です。
そこで乱数種を指定して発生する乱数を固定します。

乱数種の指定対象

Chainerの場合、下記の3種類の乱数に配慮する必要があります。

  • Python
  • NumPy
  • CuPy

CuPyはPFN社謹製のGPU対応版NumPyと考えればよいでしょう。

参考実装

def set_random_seed(seed):
    # set Python random seed
    random.seed(seed)

    # set NumPy random seed
    np.random.seed(seed)

    # set Chainer(CuPy) random seed
    cp.random.seed(seed)

seedに同じ値を設定すれば発生する乱数値が安定します。

それではちょっとしたCNNで実験してみます

実験用データセットはみんな大好きMNIST。使用するモデルはCNNを用いている小さめのものを選びました。

テストに用いたモデル
class CNN(chainer.Chain):
    def __init__(self):
        super(CNN, self).__init__(
            conv1=L.Convolution2D(1, 32, 5, stride=1),
            conv2=L.Convolution2D(32, 64, 5, stride=1),
            l1=L.Linear(None, 512),
            l2=L.Linear(None, 10),
        )

    def __call__(self, x):
        x_4d = x.data.reshape(len(x.data), 1, 28, 28)
        h = F.relu(self.conv1(x_4d))
        h = F.max_pooling_2d(h, 2, stride=1)
        h = F.relu(self.conv2(h))
        h = F.max_pooling_2d(h, 2, stride=1)
        h = F.relu(self.l1(h))
        return self.l2(h)

ちなみにChainerのMNISTを用いたExampleはCNNになっていないため、TensorFlowのExampleであるDeep MNIST for Expertsに使われているモデル(Conv2層+FC1層)を使用しています。

まずCPUで5回実行しました

CPUによる実行結果(10Epoch目の結果だけを抽出)
$ python mnist_chainer.py
epoch       main/loss   validation/main/loss  main/accuracy  validation/main/accuracy  elapsed_time
10          0.00639321  0.0375724             0.998116       0.99164                   4494.79
10          0.00639321  0.0375724             0.998116       0.99164                   4487.52
10          0.00639321  0.0375724             0.998116       0.99164                   4498.98
10          0.00639321  0.0375724             0.998116       0.99164                   4493.16
10          0.00639321  0.0375724             0.998116       0.99164                   4492.28

期待通り5回とも同じ結果になっています。

次にGPUで5回実行しました

GPUによる実行結果(10Epoch目の結果だけを抽出)
$ python mnist_chainer.py
epoch       main/loss   validation/main/loss  main/accuracy  validation/main/accuracy  elapsed_time
10          0.00759941  0.0510452             0.997615       0.988754                  77.9142
10          0.00894238  0.0384938             0.997499       0.99164                   78.9828
10          0.00753692  0.0475569             0.997615       0.988256                  78.1221
10          0.00674504  0.0442383             0.998166       0.99164                   77.9893
10          0.00766461  0.0411011             0.997465       0.991143                  80.4075

やはりGPUでは結果が不安定(非決定的)になってしまいますね。

GPUを使用しても演算結果を安定(決定的に)させる方法

手っ取り早くて効果的なのは環境変数CHAINER_CUDNNに0を設定することです。

Chainerの環境変数についてはこちらを参照のこと。

環境変数を指定した、GPUによる実行結果(10Epoch目の結果だけを抽出)
$ export CHAINER_CUDNN=0
$ python mnist_chainer.py
epoch       main/loss   validation/main/loss  main/accuracy  validation/main/accuracy  elapsed_time
10          0.00971611  0.0498366             0.997232       0.98955                   175.727
10          0.00971611  0.0498366             0.997232       0.98955                   174.934
10          0.00971611  0.0498366             0.997232       0.98955                   175.237
10          0.00971611  0.0498366             0.997232       0.98955                   175.662
10          0.00971611  0.0498366             0.997232       0.98955                   174.704

あっさり解決しました。演算結果が不安定(非決定的)になる原因はcuDNNだったようです。

しかし、演算結果は安定したものの演算時間が約2.2倍になってしまいました。

演算性能をほとんど低下させずに安定(決定)的な演算を行う方法

個人的には過去の試験結果との比較を行うような試験はそれほど高頻度で行うわけではないため、演算時間が2.2倍程度なら十分許容出来ます。

しかし、ケースによっては出来るだけ処理性能を落とさずに安定(決定)的な結果を得たいこともあるでしょう。

その場合、実装に若干の修正が必要になりますが、下記のようなパラメータ追加で対応可能です。

性能低下を抑えて安定(決定)的な演算結果を得られるよう修正
class CNN(chainer.Chain):
    def __init__(self):
        super(CNN, self).__init__(
            conv1=L.Convolution2D(1, 32, 5, stride=1, deterministic=True),
            conv2=L.Convolution2D(32, 64, 5, stride=1, deterministic=True),
            l1=L.Linear(None, 512),
            l2=L.Linear(None, 10),
        )

    def __call__(self, x):
        x_4d = x.data.reshape(len(x.data), 1, 28, 28)
        h = F.relu(self.conv1(x_4d), use_cudnn=False)
        h = F.max_pooling_2d(h, 2, stride=1, use_cudnn=False)
        h = F.relu(self.conv2(h), use_cudnn=False)
        h = F.max_pooling_2d(h, 2, stride=1, use_cudnn=False)
        h = F.relu(self.l1(h), use_cudnn=False)
        return self.l2(h)

記述のポイントは下記の3点です。

  • L.Convolution2Dにdeterministic=Trueを指定
  • F.max_pooling_2dにuse_cudnn=Falseを指定
  • F.reluにuse_cudnn=Falseを指定

Convolution2DではcuDNNを使用するもののdeterministic(決定的)なアルゴリズムを使用するよう指定しています。(本記事冒頭のIssuesにある通り、v1.18.0にて追加されました)

max_pooling_2dとreluではdeterministicの指定が出来ないためcuDNNを使用しないよう指定しています。
(max_pooling_2dでcuDNNを使用すると結果が非決定的になります)

尚、この実装の場合は、環境変数CHAINER_CUDNNは指定しない、または1を指定する必要があります。

速度を気にしつつ再度実行します

GPUによる実行結果(10Epoch目の結果だけを抽出)
$ export CHAINER_CUDNN=1
$ python mnist_chainer_deterministic.py
epoch       main/loss   validation/main/loss  main/accuracy  validation/main/accuracy  elapsed_time
10          0.00820688  0.0408237             0.997649       0.991242                  80.8627
10          0.00820688  0.0408237             0.997649       0.991242                  80.2272
10          0.00820688  0.0408237             0.997649       0.991242                  80.7561
10          0.00820688  0.0408237             0.997649       0.991242                  80.8799
10          0.00820688  0.0408237             0.997649       0.991242                  80.7954

期待通り、ほとんど演算性能を落とさずに安定(決定)的な演算結果を得ることが出来ました。

上記実行例では僅か2.5%の速度低下で安定(決定)的な演算結果を得られました。

結論

ChainerでGPU演算を用いて安定(決定)的な演算を行いたい場合は下記2つの方法がある。

  • 環境変数CHAINER_CUDNNに0を設定しcuDNNを無効にする
  • LinkやFunction系APIの引数でuse_cudnn=Falseやdeterministic=Trueを個別指定する

余談

TensorFlow, Torch, Caffeなど他のフレームワークでもこの問題に取り組んでいるようですが、Chainerが一番美しく解決されているようです。

55
41
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
55
41