機械学習やシミュレーションでは、同じコードを実行しているのに結果が毎回変わることがあります。
原因の一つが、Python・NumPy・PyTorchで別々の乱数生成器が使われていることです。
そこで、各ライブラリのseedをまとめて固定する関数を作ります。
実行環境
- Python 3
- NumPy
- PyTorch
pip install numpy torch
seedをまとめて固定する関数
import random
import numpy as np
import torch
def seed_everything(
seed: int = 42,
deterministic: bool = False,
) -> np.random.Generator:
"""Python・NumPy・PyTorchの乱数seedを固定する。"""
# Python標準ライブラリ
random.seed(seed)
# NumPyの従来API
np.random.seed(seed)
# PyTorch
torch.manual_seed(seed)
# CUDAを使用する場合
if torch.cuda.is_available():
torch.cuda.manual_seed(seed)
torch.cuda.manual_seed_all(seed)
# 再現性をさらに優先する場合
if deterministic:
torch.use_deterministic_algorithms(True, warn_only=True)
if torch.backends.cudnn.is_available():
torch.backends.cudnn.benchmark = False
torch.backends.cudnn.deterministic = True
# NumPyの新しいGenerator API
return np.random.default_rng(seed)
使用例です。
def generate_values(seed: int) -> dict[str, float]:
rng = seed_everything(seed)
return {
"Python": random.random(),
"NumPy従来API": np.random.rand(),
"NumPy Generator": rng.random(),
"PyTorch": torch.rand(1).item(),
}
first = generate_values(42)
second = generate_values(42)
print(first)
print(second)
print(first == second)
実行結果の例です。
{'Python': 0.6394267984578837, ...}
{'Python': 0.6394267984578837, ...}
True
同じseedを指定したため、2回とも同じ乱数列が生成されます。
np.random.seed()とdefault_rng()の両方を使う理由
NumPyには、大きく分けて2種類の乱数生成方法があります。
従来の書き方です。
np.random.seed(42)
value = np.random.rand()
現在推奨されているGeneratorを使う書き方です。
rng = np.random.default_rng(42)
value = rng.random()
既存コードや外部ライブラリが従来APIを使っている可能性があるため、今回は両方に対応しています。
新しくコードを書く場合は、乱数生成器を明示的に渡せるdefault_rng()のほうが扱いやすいです。
def create_noise(
rng: np.random.Generator,
size: int,
) -> np.ndarray:
return rng.normal(size=size)
rng = np.random.default_rng(42)
noise = create_noise(rng, size=5)
print(noise)
完全に同じ結果になるとは限らない
seedを固定しても、次の条件が変わると結果が一致しない場合があります。
- Pythonやライブラリのバージョン
- CPUとGPUの違い
- GPUの種類
- 並列処理の実行順序
- 非決定的なアルゴリズムの使用
そのため、実験結果を保存するときはseedだけでなく、ライブラリのバージョンや実行環境も記録しておくと安全です。
import platform
print("Python:", platform.python_version())
print("NumPy:", np.__version__)
print("PyTorch:", torch.__version__)
print("CUDA:", torch.version.cuda)
まとめ
複数のライブラリを使う実験では、一つのseedだけを設定したつもりでも、別の乱数生成器が固定されていないことがあります。
最初にseed_everything()を呼び出す形にしておけば、seedの設定漏れを減らせます。
rng = seed_everything(42)
小規模な検証コードでも、あとから結果を再確認できるよう、seedを明示しておくことをおすすめします。