はじめに
乱数生成のことを考えていたら、気になって眠れなくなってしまったのでまとめてみました。
まずは結論から
機械学習のコードでは、こんな感じの関数を最初に実行することで再現性を持たせることが多い。
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 ドキュメント
random.seed(seed)
デフォルトでは現在のシステム時刻が使用されるが、OSによってはOS固有の乱数発生源が用意されている。
メルセンヌ・ツイスタという擬似乱数生成器が使われている。
Numpyのシード固定
Numpyの乱数生成は実行するたびに異なるシードが使われるので注意が必要。
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]
固定したければ、その都度シードを設定する。
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
で乱数生成クラスを改めて用意している。
np.random.seed(42)
'''
なんかの処理
'''
df.sample(frac=0.5, replace=True, random_state=43)
引数にrandom_state=43
と含めることでpandasのシードを固定している。
これで、先頭で固定したnumpyのseedが43に上書きされることはない。
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全体で固定する方法は提供されていない。
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
上記リンクによると、Numpyのランダムシードを固定すれば大丈夫とのことだが、二回目以降はsplitを実行するたびに結果が変わってしまうので注意。
Optunaのシード固定
How can I obtain reproducible optimization results?
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を使う場合は
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を使う場合は
clf = lgb.LGBMClassifier(random_state=42)
のように設定できる。
マニュアルによると、設定しない場合はC++のデフォルトシードが使われると記載してある。
If None, default seeds in C++ code are used.
C++のデフォルトシードがどうなっているのか気になりだすとキリがないのでここら辺でやめておく。
PyTorchのシード固定
Reproducibility — PyTorch 1.5.0 documentation
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
Pytorchの方でも速度低下のリスクがあるように、再現性とGPUの処理性能の間にはトレードオフがあると思ったほうがいい。
GPU内部では高速化のためにFP16やINT8などデータ型を変換することもあるため、丸め誤差なども無視できないのかもしれない。再現性を保つために考えなければならないことはいろいろありそうだ。
Seed=42はどこから来たのか
The Hitchhiker’s Guide to the Galaxy. という小説に出てくるスーパーコンピュータDeepThoughtがはじき出した「生命、宇宙、そして万物についての究極の疑問の答え」が42ということらしい。
What is it about the random seed "4242"? | Kaggle
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