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

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

##関連Issues(Chainer)
https://github.com/pfnet/chainer/pull/1321

##検証環境
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と考えればよいでしょう。

###参考実装

```python:
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を用いている小さめのものを選びました。

```python:テストに用いたモデル
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回実行しました

```console: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回実行しました

```console: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の環境変数については[こちら](http://docs.chainer.org/en/stable/reference/environment.html)を参照のこと。

```console:環境変数を指定した、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倍程度なら十分許容出来ます。

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

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

```python:性能低下を抑えて安定(決定)的な演算結果を得られるよう修正
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を指定する必要があります。

###速度を気にしつつ再度実行します
```console: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が一番美しく解決されているようです。