随机种子在Python中的实现
这一章,咱们来聊聊实际操作。
随机种子这个东西,说白了就是个「启动开关」。你把这个开关拧到同一个位置,后面跑出来的随机数序列就一模一样。我刚开始接触这玩意的时候,觉得它挺神秘的。后来踩过几次坑,才真正理解它的重要性。
Python生态里,搞随机性的库有好几个。每个库都有自己的种子管理系统。你得挨个设置,才能让整个实验变得可复现。
Python 标准库 random
这是最基础的随机数生成器。我平时写小脚本、做数据采样的时候经常用到它。
设置它的种子很简单:
import random
# 设置种子
random.seed(42)
# 生成随机数
print(random.random()) # 0.6394267984578837
print(random.randint(1, 100)) # 82
注意了,这个种子是全局的。你设一次,整个程序里所有 random 的调用都会受影响。
random.getstate() 和 random.setstate() 保存和恢复状态。这样不会污染全局种子。
我曾经在写自动化测试脚本时犯过一个错:测试用例里调了 random.seed(),结果把后面所有依赖随机性的逻辑全打乱了。排查了好久才发现问题。
NumPy 的随机种子
做数据科学和深度学习,NumPy 是绕不开的。它的随机数生成器跟标准库是两套独立系统。
NumPy 早期版本和现在的 API 有点不同。我建议你用新的方式:
import numpy as np
# 创建一个独立的随机数生成器(推荐方式)
rng = np.random.default_rng(seed=42)
# 生成随机数组
print(rng.random(3)) # [0.77395605 0.43887844 0.85859792]
print(rng.normal(0, 1, 3)) # 正态分布采样
老版本的做法是全局设置:
# 全局设置(不推荐,但很多旧代码在用)
np.random.seed(42)
print(np.random.rand(3))
np.random.seed() 是全局的,而且它会影响所有使用 old-style API 的调用。在多线程或分布式场景下,这种方式非常容易出问题。
我个人习惯用新的 default_rng 方式。每个生成器实例独立管理状态,互不干扰。这在写大型实验框架的时候特别有用。
PyTorch 的种子设置
搞深度学习的朋友对这个应该很熟悉。PyTorch 的随机性来源很多:参数初始化、数据加载、dropout、数据增强……
要确保可复现,你得把能设的种子全设上:
import torch
import random
import numpy as np
# 设置所有种子
random.seed(42)
np.random.seed(42)
torch.manual_seed(42)
# 如果使用 CUDA
if torch.cuda.is_available():
torch.cuda.manual_seed(42)
torch.cuda.manual_seed_all(42) # 多GPU
# 设置确定性算法(牺牲一些性能)
torch.backends.cudnn.deterministic = True
torch.backends.cudnn.benchmark = False
设置 deterministic = True 这一步很重要。我遇到过这种情况:种子明明设了,每次跑结果还是不一样。最后发现是 CuDNN 的卷积算法选择有随机性。
嗯,这里要注意 torch.cuda.manual_seed_all 这个函数。它是对所有 GPU 同时设种子。如果你单卡用 manual_seed 就行。
TensorFlow 的种子设置
TensorFlow 这边,种子设置入口不太一样。
import tensorflow as tf
import random
import numpy as np
# 设置基础种子
random.seed(42)
np.random.seed(42)
tf.random.set_seed(42)
# TF 2.x 的随机种子
tf.random.set_seed(42)
# 操作级别的种子(可选)
initializer = tf.keras.initializers.GlorotUniform(seed=42)
TensorFlow 有一个比较坑的地方:它的某些操作(比如 dropout 或 shuffle)默认不固定随机性。你得显式传 seed 参数。
我有个朋友,他训练一个模型,所有种子都设了,每次评估结果还是差几个点。排查了半天,发现是 tf.data.Dataset.shuffle() 没设 seed。
多库联合设置的最佳实践
做一个可复现的实验,你得像个强迫症患者一样把所有种子都管起来。
我一般会封装一个函数:
def set_all_seeds(seed=42):
"""统一设置所有随机种子"""
import random
import numpy as np
import torch
random.seed(seed)
np.random.seed(seed)
torch.manual_seed(seed)
if torch.cuda.is_available():
torch.cuda.manual_seed_all(seed)
torch.backends.cudnn.deterministic = True
torch.backends.cudnn.benchmark = False
# 如果用了 TensorFlow,再加上
# import tensorflow as tf
# tf.random.set_seed(seed)
# tf.compat.v1.set_random_seed(seed)
把这个函数放在实验入口的第一行调用。这样就能保证从头到尾的随机性都受控制。
DataLoader 里设置 worker_init_fn。
对比小结
不同库的种子设置方法,可以用这个表格快速对比:
| 库名 | 种子函数 | 作用域 | 备注 |
|---|---|---|---|
| random | random.seed(n) | 全局 | 最基础 |
| numpy (旧) | np.random.seed(n) | 全局 | 逐渐淘汰 |
| numpy (新) | np.random.default_rng(seed=n) | 实例独立 | 推荐使用 |
| PyTorch | torch.manual_seed(n) | 当前设备 | 需配合cudnn设置 |
| TensorFlow | tf.random.set_seed(n) | 全局图级别 | 操作级别也需设 |
每次写实验代码,我都会记得调用那个统一设置的函数。这已经成了我的肌肉记忆了。