本記事はChainerやTensorFlowでGPUを使うと毎回結果が変わる理由と対策 (まとめ)の詳細編であり、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と考えればよいでしょう。
###参考実装
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回実行しました
$ 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回実行しました
$ 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の環境変数についてはこちらを参照のこと。
$ 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を指定する必要があります。
###速度を気にしつつ再度実行します
$ 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が一番美しく解決されているようです。