機械学習におけるランダムシードの研究

## はじめに

乱数生成のことを考えていたら、気になって眠れなくなってしまったのでまとめてみました。


## まずは結論から

機械学習のコードでは、こんな感じの関数を最初に実行することで再現性を持たせることが多い。

```Python:seal_seed.py
def fix_seed(seed):
    # random
    random.seed(seed)
    # Numpy
    np.random.seed(seed)
    # Pytorch
    torch.manual_seed(seed)
    torch.cuda.manual_seed_all(seed)
    torch.backends.cudnn.deterministic = True
    # Tensorflow
    tf.random.set_seed(seed)

SEED = 42
fix_seed(SEED)
```

本当にこれで大丈夫か？と気になるところだが、seedの固定はこれで大丈夫。
ただし、random_seedとRandomStateの違いやGPU周りで注意点があるので、少し説明する。

## Pythonの組み込みモジュールのランダムシード
[random --- 擬似乱数を生成する — Python 3.8.3 ドキュメント](https://docs.python.org/ja/3/library/random.html)

`random.seed(seed)`

デフォルトでは現在のシステム時刻が使用されるが、OSによってはOS固有の乱数発生源が用意されている。

[メルセンヌ・ツイスタ](https://ja.wikipedia.org/wiki/%E3%83%A1%E3%83%AB%E3%82%BB%E3%83%B3%E3%83%8C%E3%83%BB%E3%83%84%E3%82%A4%E3%82%B9%E3%82%BF)という擬似乱数生成器が使われている。

## Numpyのシード固定

Numpyの乱数生成は実行するたびに異なるシードが使われるので注意が必要。

```python
import numpy as np
np.random.seed(42)
# 一回目
print(np.random.randint(0, 1000, 10))
# -> [102 435 860 270 106  71 700  20 614 121]

# 二回目
print(np.random.randint(0, 1000, 10))
# -> [466 214 330 458  87 372  99 871 663 130]
```

固定したければ、その都度シードを設定する。

```python
import numpy as np
np.random.seed(42)
# 一回目
print(np.random.randint(0, 1000, 10))
# -> [102 435 860 270 106  71 700  20 614 121]

# 二回目
np.random.seed(42)
print(np.random.randint(0, 1000, 10))
# -> [102 435 860 270 106  71 700  20 614 121]
```

環境やOSが変わっても最初に固定したseedが同じであれば、そのあとの出力は同じものが出力されるようだ。

実験の再現性を保つだけなら、前述のように冒頭でseedだけ固定すれば問題なさそうだ。

## Numpyを利用したライブラリ

`np.random.seed(42)`で基本的には大丈夫だが、外部モジュールでもシード固定している場合は注意が必要。外部モジュール内で`np.random.seed(43)`のように上書きしてしまうと、呼び出した方のseedも上書きされてしまう。

OptunaやPandasなどのライブラリではそれを考慮して`numpy.random.RandomState`で乱数生成クラスを改めて用意している。


```python
np.random.seed(42)
'''
なんかの処理
'''
df.sample(frac=0.5, replace=True, random_state=43)
```

引数に`random_state=43`と含めることでpandasのシードを固定している。

これで、先頭で固定したnumpyのseedが43に上書きされることはない。



```python
s = pd.Series(np.arange(100))
np.random.seed(42)
# 初回は42で実行される
print(s.sample(n=3)) # -> (83, 53, 70)
# 二回目は別の乱数シードが適用される
print(s.sample(n=3)) # -> (79, 37, 65)

print(s.sample(n=3, random_state=42)) # -> (83, 53, 70)
print(s.sample(n=3, random_state=42)) # -> (83, 53, 70)
```

さらにNumpyと同様に、二回目以降はseedが固定されていないので注意。変数に保存しておくか、その都度random_stateの値を設定する。


jupyterノートブックを順次実行して行って、最終的に呼び出し回数が同じであれば、最初に一回`np.random.seed(42)`としておけば再現性は保たれる。

ただ、後述のようにGPUを利用している場合は微妙に再現性が保たれない可能性もあるので、注意が必要。

## Scikit-learnのシード固定

Scikit-learnのtrain_test_split関数などで`random_state`を指定することができるが、Scikit-learn全体で固定する方法は提供されていない。

```Python
X_train, X_test, Y_train, Y_test = train_test_split(X, Y, random_state=SEED)
```


[How to set the global random_state in Scikit Learn | Bartosz Mikulski](https://www.mikulskibartosz.name/how-to-set-the-global-random_state-in-scikit-learn/)

上記リンクによると、Numpyのランダムシードを固定すれば大丈夫とのことだが、二回目以降はsplitを実行するたびに結果が変わってしまうので注意。


## Optunaのシード固定

[How can I obtain reproducible optimization results?](https://optuna.readthedocs.io/en/stable/faq.html?highlight=seed#how-can-i-obtain-reproducible-optimization-results)

```Python
sampler = TPESampler(seed=SEED)  # Make the sampler behave in a deterministic way.
study = optuna.create_study(sampler=sampler)
study.optimize(objective)
```

Optuna内では別のRandomStateインスタンスが用意されるのでseedを指定することが可能。内部ではRandomStateを利用している。

## LightGBMでのシード固定


Cross-Validationを使う場合は

```Python
lgb.cv(lgbm_params,
       lgb_train,
       early_stopping_rounds=10,
       nfold=5,
       shuffle=True,
       seed=42,
       callbacks=callbacks,
       )
```

のように設定できる。
マニュアルには
>Seed used to generate the folds (passed to numpy.random.seed)

と書いてあるので「あ！これはseedが書き換えられてしまうやつか？」と思うかもしれないが、ソースコードをみると
`randidx = np.random.RandomState(seed).permutation(num_data)`
となっていたので大丈夫そうだ。


また、Scikit-learn APIを使う場合は

```Python
clf = lgb.LGBMClassifier(random_state=42)
```

のように設定できる。


マニュアルによると、設定しない場合はC++のデフォルトシードが使われると記載してある。

> If None, default seeds in C++ code are used.

C++のデフォルトシードがどうなっているのか気になりだすとキリがないのでここら辺でやめておく。

## PyTorchのシード固定

[Reproducibility — PyTorch 1.5.0 documentation](https://pytorch.org/docs/stable/notes/randomness.html)

```Python
torch.manual_seed(seed)
# cuDNN用
torch.backends.cudnn.deterministic = True
torch.backends.cudnn.benchmark = False
```

`torch.cuda.manual_seed_all(seed)`というメソッドがあるが、最新のPytorchでは`torch.manual_seed(seed)`だけで十分。

また、マニュアルにはこう書いてある。

>Deterministic operation may have a negative single-run performance impact, depending on the composition of your model. Due to different underlying operations, **which may be slower**, the processing speed (e.g. the number of batches trained per second) may be lower than when the model functions nondeterministically. However, even though single-run speed may be slower, depending on your application determinism may save time by facilitating experimentation, debugging, and regression testing.

GPUの処理を決定的（Deterministic）にすると、処理速度が落ちる可能性があるので注意。
※ただし、デバッグやテストなど全体工程を考えると”結局は時間の短縮につながる”とも書いてある

再現性を問わない場合 ＆ ネットワークの構造（計算グラフ）が変化しない場合は
`torch.backends.cudnn.benchmark = True`
にすると高速化できる

## TensorFlowのシード固定

基本的には下記のようにシードを固定する

`tf.random.set_seed(seed)`

ただし、下記のようにオペレーションレベルでseedの値を指定することもできる

`tf.random.uniform([1], seed=1)`

## DeepLearningのフレームワークとGPUのシード固定

正直なところ、TensorflowのGPU周りについてはあまり情報が見つからなかった。GPUと乱数生成は結構根深い問題がありそうだ。ソフトウェアとハードウェアでは全然違うだろうし。

[NVIDIA/tensorflow-determinism: Tracking, debugging, and patching non-determinism in TensorFlow](https://github.com/NVIDIA/tensorflow-determinism)


Pytorchの方でも速度低下のリスクがあるように、再現性とGPUの処理性能の間にはトレードオフがあると思ったほうがいい。

GPU内部では高速化のためにFP16やINT8などデータ型を変換することもあるため、丸め誤差なども無視できないのかもしれない。再現性を保つために考えなければならないことはいろいろありそうだ。

## Seed=42はどこから来たのか

The Hitchhiker’s Guide to the Galaxy. という小説に出てくるスーパーコンピュータDeepThoughtがはじき出した「[生命、宇宙、そして万物についての究極の疑問の答え](https://ja.wikipedia.org/wiki/%E7%94%9F%E5%91%BD%E3%80%81%E5%AE%87%E5%AE%99%E3%80%81%E3%81%9D%E3%81%97%E3%81%A6%E4%B8%87%E7%89%A9%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6%E3%81%AE%E7%A9%B6%E6%A5%B5%E3%81%AE%E7%96%91%E5%95%8F%E3%81%AE%E7%AD%94%E3%81%88)」が42ということらしい。

[What is it about the random seed "4242"? | Kaggle](https://www.kaggle.com/questions-and-answers/34361)

Kaggleではコードの~~コピペ~~再利用が頻繁に行われるので、誰かがジョークで使ったseed=42という部分がよく使われるようになったんだとかなんとか。

今となってはseedの値を変えて学習させたモデルの予測をアンサンブルすることもある。


## まとめ

- numpy関連の乱数生成は実行するたびにseedが変わるので油断しない。
    - 特に実行するたびに乱数生成する場合や、何回呼ばれるかわからないメソッドなどは明示的にseedを設定しないと再現性が保たれない。
- 外部ライブラリを利用する場合は呼び出すたびにrandom_stateを設定する。
     - 自分でモジュールを作る場合などはnumpyのシードを上書きしないように、改めてRandomStateを用意する
- GPU周りの乱数生成は結構ややこしい。処理速度と再現性（というより精度？）はトレードオフの関係

簡単な実験コードはこちら
 [machine_leraning_experiments/random_seed_experiment.ipynb at master · si1242/machine_leraning_experiments](https://github.com/si1242/machine_leraning_experiments/blob/master/random_seed_experiment.ipynb)