TensorFlowで乱数シードを固定する

## ざっくり言うと

* TensorFlowでは`tensorflow.set_random_seed`関数、または乱数オペレータの`seed`引数で乱数シードを設定することができる。
* TensorFlowには「グラフレベル」、「オペレーションレベル」と呼ばれる2種類の乱数シードが存在する。
* グラフを作成した後、乱数オペレータを生成する前に乱数シードを設定する必要がある。

## 背景

TensorFlowで機械学習のコードを実装する時、多くの場合は重みの初期化などに疑似乱数（以下、単に「乱数」）を使います。
そのため、コードを実行し直す度に結果が変化してしまい、データやパラメータの変更による結果の変化なのか、乱数による結果の変化なのか区別できません。
乱数は「シード」（seed、種）と呼ばれる値に基づいて生成され、このシードは特に指定しない場合、ある程度変化する既定値（多くの場合は時刻やプロセスID）が使われます。
TensorFlowにも乱数シードを固定する方法が提供されていますが、その挙動に少し悩んだため、整理してみました。

TensorFlow以外にも、randomモジュール、NumPyパッケージにおける乱数シードの固定についても述べます。

## バージョン

本記事の内容は、以下のバージョンに基づいています。

* Python: v3.5.1
* NumPy: v1.11.0
* TensorFlow: v0.9.0rc0

## randomモジュール

Python標準の`random`モジュールでは、`random.seed`関数で乱数シードを設定することができます。
実験コードは以下の通りです。

```seed_random.py
import random
random.seed(0)
print("1:", random.random())
print("2:", random.random())
print("3:", random.random())
random.seed(0)
print("4:", random.random())
print("5:", random.random())
print("6:", random.random())
```

3回実行した場合の結果は以下の通りです。
乱数シードを設定する度、実行する度に同じ値が出力されています。

```
$ python3 seed_random.py
1: 0.8444218515250481
2: 0.7579544029403025
3: 0.420571580830845
4: 0.8444218515250481
5: 0.7579544029403025
6: 0.420571580830845
$ python3 seed_random.py
1: 0.8444218515250481
2: 0.7579544029403025
3: 0.420571580830845
4: 0.8444218515250481
5: 0.7579544029403025
6: 0.420571580830845
$ python3 seed_random.py
1: 0.8444218515250481
2: 0.7579544029403025
3: 0.420571580830845
4: 0.8444218515250481
5: 0.7579544029403025
6: 0.420571580830845
```

## NumPyパッケージ

NumPyパッケージでは、`numpy.random.seed`関数で乱数シードを設定することができます。
実験コードは以下の通りです。

```seed_numpy.py
import numpy as np
np.random.seed(0)
print("1:", np.random.rand(3))
print("2:", np.random.rand(3))
print("3:", np.random.rand(3))
np.random.seed(0)
print("4:", np.random.rand(3))
print("5:", np.random.rand(3))
print("6:", np.random.rand(3))
```

3回実行した場合の結果は以下の通りです。
乱数シードを設定する度、実行する度に同じ値が出力されています。

```
$ python3 seed_numpy.py
1: [ 0.5488135   0.71518937  0.60276338]
2: [ 0.54488318  0.4236548   0.64589411]
3: [ 0.43758721  0.891773    0.96366276]
4: [ 0.5488135   0.71518937  0.60276338]
5: [ 0.54488318  0.4236548   0.64589411]
6: [ 0.43758721  0.891773    0.96366276]
$ python3 seed_numpy.py
1: [ 0.5488135   0.71518937  0.60276338]
2: [ 0.54488318  0.4236548   0.64589411]
3: [ 0.43758721  0.891773    0.96366276]
4: [ 0.5488135   0.71518937  0.60276338]
5: [ 0.54488318  0.4236548   0.64589411]
6: [ 0.43758721  0.891773    0.96366276]
$ python3 seed_numpy.py
1: [ 0.5488135   0.71518937  0.60276338]
2: [ 0.54488318  0.4236548   0.64589411]
3: [ 0.43758721  0.891773    0.96366276]
4: [ 0.5488135   0.71518937  0.60276338]
5: [ 0.54488318  0.4236548   0.64589411]
6: [ 0.43758721  0.891773    0.96366276]
```

## TensorFlowパッケージ

TensorFlowパッケージでは、`tensorflow.set_random_seed`関数、または乱数オペレータの`seed`引数で乱数シードを設定することができます。
TensorFlowには「グラフレベル」、「オペレーションレベル」と呼ばれる2種類の乱数シードが存在します。
また、設定の順序を気をつける必要があり、「グラフを作成した後」、「オペレータを生成する前」に乱数シードを設定する必要があります。

参考: [Constants, Sequences, and Random Values - tf.set_random_seed(seed)](https://www.tensorflow.org/versions/r0.9/api_docs/python/constant_op.html#set_random_seed)

### 想定通りに動作する例

実験コードは以下の通りです。

```seed_tensorflow1.py
import tensorflow as tf
with tf.Graph().as_default():
    tf.set_random_seed(0)
    rand_op1 = tf.random_normal([3])
    rand_op2 = tf.random_normal([3], seed=1)
    print("session1")
    with tf.Session() as sess1:
        print("op1-1:", sess1.run(rand_op1))
        print("op1-2:", sess1.run(rand_op1))
        print("op2-1:", sess1.run(rand_op2))
        print("op2-2:", sess1.run(rand_op2))
    print("session2")
    with tf.Session() as sess2:
        print("op1-1:", sess2.run(rand_op1))
        print("op1-2:", sess2.run(rand_op1))
        print("op2-1:", sess2.run(rand_op2))
        print("op2-2:", sess2.run(rand_op2))
```

3回実行した場合の結果は以下の通りです。

```
$ python3 seed_tensorflow1.py
session1
op1-1: [-1.40955448 -0.53668278 -0.56523788]
op1-2: [-1.07107699  0.4139019   2.29180121]
op2-1: [ 0.68034536  0.8777824  -0.64773595]
op2-2: [-1.21607268  0.95542693 -0.16866584]
session2
op1-1: [-1.40955448 -0.53668278 -0.56523788]
op1-2: [-1.07107699  0.4139019   2.29180121]
op2-1: [ 0.68034536  0.8777824  -0.64773595]
op2-2: [-1.21607268  0.95542693 -0.16866584]
$ python3 seed_tensorflow1.py
session1
op1-1: [-1.40955448 -0.53668278 -0.56523788]
op1-2: [-1.07107699  0.4139019   2.29180121]
op2-1: [ 0.68034536  0.8777824  -0.64773595]
op2-2: [-1.21607268  0.95542693 -0.16866584]
session2
op1-1: [-1.40955448 -0.53668278 -0.56523788]
op1-2: [-1.07107699  0.4139019   2.29180121]
op2-1: [ 0.68034536  0.8777824  -0.64773595]
op2-2: [-1.21607268  0.95542693 -0.16866584]
$ python3 seed_tensorflow1.py
session1
op1-1: [-1.40955448 -0.53668278 -0.56523788]
op1-2: [-1.07107699  0.4139019   2.29180121]
op2-1: [ 0.68034536  0.8777824  -0.64773595]
op2-2: [-1.21607268  0.95542693 -0.16866584]
session2
op1-1: [-1.40955448 -0.53668278 -0.56523788]
op1-2: [-1.07107699  0.4139019   2.29180121]
op2-1: [ 0.68034536  0.8777824  -0.64773595]
op2-2: [-1.21607268  0.95542693 -0.16866584]

```

### 期待通りに動作しない例1: グラフを作成する前に乱数シードを設定

グラフを作成する前に乱数シードを設定する例を示します。
実験コードは以下の通りです。

```seed_tensorflow2.py
import tensorflow as tf
tf.set_random_seed(0) # グラフを作成する前に乱数シードを設定
with tf.Graph().as_default():
    rand_op1 = tf.random_normal([3])
    rand_op2 = tf.random_normal([3], seed=1)
    with tf.Session() as sess1:
        print("op1-1:", sess1.run(rand_op1))
        print("op1-2:", sess1.run(rand_op1))
        print("op2-1:", sess1.run(rand_op2))
        print("op2-2:", sess1.run(rand_op2))
```

3回実行した場合の結果は以下の通りです。
オペレーションレベルで乱数シードを設定している`rand_op2`は3回とも同じ値が出力されていますが、`rand_op1`は3回とも異なる値が出力されています。

```
$ python3 seed_tensorflow2.py
op1-1: [ 0.22292495  0.67142487  0.80927771]
op1-2: [-1.11188841  0.57819426  0.19088539]
op2-1: [-0.81131822  1.48459876  0.06532937]
op2-2: [ 0.55171245 -0.13107552 -0.04481386]
$ python3 seed_tensorflow2.py
op1-1: [-0.96930581  1.48854125  0.52752507]
op1-2: [ 0.91646689  0.85830265 -0.18069462]
op2-1: [-0.81131822  1.48459876  0.06532937]
op2-2: [ 0.55171245 -0.13107552 -0.04481386]
$ python3 seed_tensorflow2.py
op1-1: [ 0.79890805  1.35702407 -0.12329593]
op1-2: [ 1.69212222  0.22590902 -0.73435217]
op2-1: [-0.81131822  1.48459876  0.06532937]
op2-2: [ 0.55171245 -0.13107552 -0.04481386]
```

### 期待通りに動作しない例2: 乱数オペレータの生成後に乱数シードを設定

乱数オペレータの生成後に乱数シードを設定する例を示します。
実験コードは以下の通りです。

```seed_tensorflow3.py
import tensorflow as tf
with tf.Graph().as_default():
    rand_op1 = tf.random_normal([3])
    rand_op2 = tf.random_normal([3], seed=1)
    tf.set_random_seed(0) # 乱数オペレータの生成後に乱数シードを設定
    with tf.Session() as sess:
        print("op1-1:", sess.run(rand_op1))
        print("op1-2:", sess.run(rand_op1))
        print("op2-1:", sess.run(rand_op2))
        print("op2-2:", sess.run(rand_op2))
```

3回実行した場合の結果は以下の通りです。
上記の例と同様、オペレーションレベルで乱数シードを設定している`rand_op2`は3回とも同じ値が出力されていますが、`rand_op1`は3回とも異なる値が出力されています。

```
$ python3 seed_tensorflow3.py
op1-1: [ 0.03802272  1.69988739 -0.59952497]
op1-2: [ 0.62614048  0.07537607 -1.19501412]
op2-1: [-0.81131822  1.48459876  0.06532937]
op2-2: [ 0.55171245 -0.13107552 -0.04481386]
$ python3 seed_tensorflow3.py
op1-1: [ 1.02020776  1.70896292  0.45571503]
op1-2: [-0.46230376 -1.22950804  0.51038951]
op2-1: [-0.81131822  1.48459876  0.06532937]
op2-2: [ 0.55171245 -0.13107552 -0.04481386]
$ python3 seed_tensorflow3.py
op1-1: [ 1.31134415 -1.12688231  0.1805287 ]
op1-2: [-0.57391566  0.94440365 -1.07441545]
op2-1: [-0.81131822  1.48459876  0.06532937]
op2-2: [ 0.55171245 -0.13107552 -0.04481386]
```

### その他

未確認情報ですが、GPU上で動かす場合は乱数シードを設定しても同じ値にはならないらしいです。

参考: [Mention that GPU reductions are nondeterministic in docs · Issue #2732 · tensorflow/tensorflow](https://github.com/tensorflow/tensorflow/issues/2732)

## 謝辞

TensorFlowに関する勉強会仲間である平井さんからとても有益な情報を頂きました。
ここに感謝致します。