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

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

##関連Issues(TensorFlow)
https://github.com/tensorflow/tensorflow/issues/2652
https://github.com/tensorflow/tensorflow/issues/2732

##検証環境
TensorFlow r0.12
Python 3.5.2
CUDA 8.0
cuDNN 5.1
NVIDIA Tesla/GeForce いろいろ

##まずは乱数種の指定

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

###乱数種の指定対象
TensorFlowの場合、下記の3種類の乱数に配慮する必要があります。

- Python
- NumPy
- TensorFlow

###参考実装

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

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

    # set TensorFlow random seed
    tf.set_random_seed(seed)
```
seedに同じ値を設定すれば発生する乱数値が安定します。

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

実験用データセットはみんな大好きMNIST。
モデルはTensorFlowのExampleであるDeep MNIST for Expertsのモデル(Conv2層+FC1層)を使用しています。

```python:実験に使用したモデル(DeepMNISTforExperts相当)
w_conv1 = tf.Variable(tf.truncated_normal([5, 5, 1, 32]))
b_conv1 = tf.Variable(tf.truncated_normal([32]))
w_conv2 = tf.Variable(tf.truncated_normal([5, 5, 32, 64]))
b_conv2 = tf.Variable(tf.truncated_normal([64]))
w_fc1 = tf.Variable(tf.truncated_normal([(28 // 4) * (28 // 4) * 64, 512]))
b_fc1 = tf.Variable(tf.truncated_normal([512]))
w_out = tf.Variable(tf.truncated_normal([512, 10]))
b_out = tf.Variable(tf.truncated_normal([10]))

x_4d = tf.reshape(x_in, [-1, 28, 28, 1])

conv1 = tf.nn.relu(tf.add(tf.nn.conv2d(x_4d, w_conv1, strides=[1, 1, 1, 1]), b_conv1))
pool1 = tf.nn.max_pool(conv1, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1])

conv2 = tf.nn.relu(tf.add(tf.nn.conv2d(pool1, w_conv2, strides=[1, 1, 1, 1]), b_conv2))
pool2 = tf.nn.max_pool(conv2, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1])

flat = tf.reshape(pool2, [-1, (28 // 4) * (28 // 4) * 64, 512])
h_fc1 = tf.nn.relu(tf.add(tf.matmul(flat, w_fc1), b_fc1))
h_fc1 = tf.nn.dropout(fc1, dropout_keep_prob)
```

※下記の出力結果はDeep MNIST for Expertsの結果出力フォーマットとは異なります。

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

```console:CPUによる実行結果(1000Step目の結果だけを抽出)
$ python deep_mnist_for_expert.py

Step 1000 Validation error: 1.5600%
Step 1000 Validation error: 1.5600%
Step 1000 Validation error: 1.5600%
Step 1000 Validation error: 1.5600%
Step 1000 Validation error: 1.5600%
```
乱数種を指定したため、期待通り5回とも同じ結果になっています。

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

```console:GPUによる実行結果(1000Step目の結果だけを抽出)
$ python deep_mnist_for_expert.py

Step 1000 Validation error: 1.4800%
Step 1000 Validation error: 1.5000%
Step 1000 Validation error: 1.5800%
Step 1000 Validation error: 1.4600%
Step 1000 Validation error: 1.4300%
```
乱数種を指定しても、GPUでは結果が不安定(非決定的)になってしまいますね。

##Issuesに挙げられているcuDNNを無効にしてGPUで実行

cuDNNを無効にするためにはconv2dの引数にuse_cudnn_on_gpu=Falseを追加します。

```python:修正箇所
conv1 = tf.nn.relu(tf.add(tf.nn.conv2d(x_4d, w_conv1, strides=[1, 1, 1, 1], use_cudnn_on_gpu=False), b_conv1))

conv2 = tf.nn.relu(tf.add(tf.nn.conv2d(pool1, w_conv2, strides=[1, 1, 1, 1], use_cudnn_on_gpu=False), b_conv2))
```

```console:環境変数を指定した、GPUによる実行結果
$ python deep_mnist_for_expert.py

UnimplementedError (see above for traceback): Conv2D for GPU is not currently supported without cudnn
```
なんと今のところGPU環境ではcuDNN無しでの実行をサポートしていないとのこと。

改めて[API Document](https://www.tensorflow.org/api_docs/python/nn/convolution#conv2d)を参照しましたが、そんなことは書かれていませんよね...

##しかも原因はcuDNNだけではないようです

IssuesではcuDNNだけではなくEigenも原因の一つとして挙げられています。
そこで単純な行列演算でも実験しました。

```python:squared_sum.py
import numpy as np
import tensorflow as tf

t = tf.Variable(np.arange(0.0, 1.0, 1e-6))

sess = tf.Session()
sess.run(tf.global_variables_initializer())

for _ in range(10):
    print("{:.15f}".format(sess.run(tf.reduce_sum(t ** 2))))
```

DNNどころかNNですらない単純な行列を用いて二乗和を求めます。
また乱数も使用していません。安定(決定的)な結果を期待しますが...

```console:まずはCPUによる実行結果
$ python squared_sum.py
333332.833333499496803
333332.833333499496803
333332.833333499496803
333332.833333499496803
333332.833333499496803
333332.833333499496803
333332.833333499496803
333332.833333499496803
333332.833333499496803
333332.833333499496803
```
こちらはCPU演算なので結果が安定的なのは期待通りです。

```console:次にGPUによる実行結果
$ python squared_sum.py
333332.833333499729633
333332.833333500020672
333332.833333499846049
333332.833333499962464
333332.833333499846049
333332.833333499962464
333332.833333499962464
333332.833333500078879
333332.833333499962464
333332.833333499787841
```
残念ながら単純な行列演算でも値が不安定になってしまいました。
cuDNNだけが原因では無いと考えて良いでしょう。

##結論
残念ですがTensorFlowの場合、現状では打つ手が無さそうです
行列演算でも不安定(非決定的)になるのは、上記issuesに記載されている通り、Eigenの仕様かもしれませんね。(未確認)

どうしても安定(決定的)な結果が欲しければCPUで演算するしか無さそうです。

ちなみに良く知られたDeepLearningフレームワークの状況を比較すると下記のようになるようです。

- **Caffe** [TensorFlowのIssue](https://github.com/tensorflow/tensorflow/issues/2732)によればGPUでも決定的(Deterministic)な演算が可能になった
- **Torch** [TorchのIssue](https://github.com/soumith/cudnn.torch/issues/270)によれば、不完全ながら決定的な演算モードが実装されている
- **Chainer** 決定的な演算が可能
- **TensorFlow** 不安定(非決定的)な演算のみ対応

TensorFlowは最後発ですので今後は対応されるかもしれませんね。