自己跑深度学习项目时,最怕遇到什么?不是说网络不收敛,而是同一个脚本昨天跑出92.5%的准确率,今天跑就变成91.8%,代码一行没动,结果却对不上。新手第一反应是怀疑环境被改了,或者数据读取出了bug,折腾半天最后发现,罪魁祸首就是随机数种子(random seed)没有固定。“随机撒种子”这件事,听上去就是一行代码的事,实际做起来暗坑非常多。
这篇文章我用自己踩过的坑,把随机数种子到底是什么、它怎么影响训练结果、怎么设置才算真正锁死、设完之后还不稳定又该怎么排查,一次性讲清楚。适合正在做机器学习实验、经常训练模型、或者写论文需要复现结果的朋友,也适合刚入门深度学习、对随机性还没概念的初学者。你不用全看懂,照着后面的代码抄,就能解决90%的可复现性问题。
1. 先搞懂一件事:为什么程序里的随机数不随机
1.1 伪随机的本质:一台能预测的“数字播种机”
很多初学者以为random函数每次调用都从宇宙里抓一个随机数回来,其实不是。计算机里的随机数生成器(PRNG,Pseudo-Random Number Generator)本质上是一个确定性算法,它吃进去一个初始状态,然后通过数学计算不断往外吐数字。这个初始状态就是种子(seed)。
我们拿最简单的线性同余生成器来举例:
python复制X_{n+1} = (a * X_n + c) % m
只要确定了X_0(也就是种子)、乘数a、增量c和模数m,后面整串数字就全部确定了。Python的random模块虽然用的是更复杂的梅森旋转算法,但原理完全一样——种子的作用就是给这台“数字播种机”定一个起点。
你可以把种子理解成一片土地的初始条件。同一片地,撒下同样的种子,在同等条件下长出来的庄稼肯定一样。程序里也一样,种子值相同,PRNG吐出的随机数序列就完全相同;种子不同,哪怕是连续的两个整数,吐出来的序列也会天差地别。
1.2 深度学习训练里的随机性到底从哪来
很多初学者以为随机数只和“生成随机样本”有关,实际上深度学习训练的每一个环节都在消费随机数:
- 模型参数的初始化:
torch.nn.Linear的权重初始值全靠随机数生成,种子不同,初始参数就不同。 - 训练集/验证集的划分:
train_test_split、random_split都依赖随机数决定哪些样本进训练集。 - DataLoader的batch顺序:每个epoch取样顺序不同,梯度更新轨迹就会不同。
- Dropout层:每次前向传播随机丢弃哪些神经元,直接改变模型输出。
- 数据增强:随机裁剪、翻转、色彩抖动,全都要靠随机数。
所以你会看到,哪怕只是一次简单的MLP训练,只要有一处随机源没锁住,两次训练的结果就可能差出一个百分点甚至更多。固定随机种子的本质,就是把所有这些随机源全部锁到同一个起点上,让整片“庄稼地”按照同一套节奏生长。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 五步把种子撒齐:主流框架的种子设置指南
2.1 Python、NumPy、PyTorch要各管各的
先给一个结论:只设一个库的seed是远远不够的。
Python的random、NumPy的numpy.random、PyTorch的torch.manual_seed各自维护一套独立的随机状态,互不干涉。你设了np.random.seed()但没设torch.manual_seed(),那么模型初始化和DataLoader的数据顺序仍然每次都不一样。
我目前项目里用的是一套统一的种子设置函数:
python复制import random
import numpy as np
import torch
def set_all_seeds(seed: int):
random.seed(seed) # Python 内置随机库
np.random.seed(seed) # NumPy 随机库
torch.manual_seed(seed) # PyTorch CPU/GPU 通用
torch.cuda.manual_seed_all(seed) # 如果用了多块 GPU,这句不能漏
# 下面两项涉及 cuDNN 的确定性设置,后面单独讲
torch.backends.cudnn.deterministic = True
torch.backends.cudnn.benchmark = False
调用的时候直接set_all_seeds(42)就行。42不是魔法,因为《银河系漫游指南》里说“生命、宇宙以及一切的答案是42”,程序员圈子里用42当默认种子已经成了一种习惯——它只是一个约定俗成的起点,并不代表42跑出来的效果最好。你完全可以用0、1、2024,只要全程统一就行。
