资讯详情

PyTorch随机种子详解:从torch.manual_seed到实验可复现

📅 2026/10/20 5:09:45 | 华诺云谱 👁 阅读
PyTorch随机种子详解:从torch.manual_seed到实验可复现
先说个我上周刚经历的事。我在复现一篇论文的实验结果数据预处理、模型结构、超参数这些全都核对过一遍跑出来的指标却和论文里差了将近两个点。折腾了两天才发现问题就出在一行代码上torch.manual_seed(42)。加上这一行重新训练指标立刻对上了。这种经历我相信不少人都有。深度学习训练过程中充满了随机性从模型权重初始化到数据打乱顺序再到dropout层的掩码生成每一步都在掷骰子。如果这些骰子不受控制你的实验就变成了一场没有剧本的即兴演出。这次不借助标题里这个不起眼的函数把随机种子这件事彻底讲清楚它是什么、为什么重要、怎么用、以及为什么你明明设置了种子实验结果还是不一样。这篇文章覆盖的内容对两类人特别有价值一类是初学PyTorch、正在做课程作业或者入门项目的同学你可能会发现昨天跑出来的结果今天复现不了那这篇文章就是为你写的另一类是在做论文复现、模型对比实验的从业者需要把实验的可重复性做实这里面有很多光看官方文档看不出来的细节。1. 随机种子到底解决了什么问题1.1 训练过程中的随机性从哪里来很多人对随机的理解停留在表面觉得不就是模型初始化时随机一下嘛。实际上一次标准训练的随机性来源比你想象的多得多。最开始是模型参数的初始化。全连接层、卷积层、Embedding层的权重要么用均匀分布初始化要么用正态分布初始化这些都是从某个概率分布中采样出来的。同一份代码、同一个模型权重初始值不同训练轨迹就会完全不同最终的收敛点可能差很远。然后是数据加载阶段。训练集在每次epoch中都要重新打乱顺序shuffle这个shuffle本身就是对样本索引序列的一次随机重排。即使数据集完全一样打乱顺序不同每个batch里装的是哪几个样本就不同梯度更新的轨迹自然也不同。接着是训练过程中的随机操作。最常见的就是dropout它按一定概率随机屏蔽神经元这个屏蔽模式是逐样本、逐层生成的。还有Batch Normalization在训练模式下用batch内的随机样本做统计这些都会引入随机性。这些随机性本身不是坏事。随机初始化能打破对称性数据shuffle能让SGD的梯度估计更稳健dropout本来就是一种正则化手段。问题在于如果你不控制这些随机源同一个训练脚本跑两遍得到的不是同一个模型而是同一套超参数训练出来的两个不同模型。做对比实验时你没控制随机性那你比较的到底是方案A和方案B的真实差异还是随机噪声导致的差异说不清楚。1.2 一个典型场景实验为什么复现不了我见过不少团队做过这样的操作跑完一份基线隔了一周想复现结果指标对不上或者两个人在同一份代码上各自跑实验一个收敛一个不收敛开始怀疑数据、怀疑模型、怀疑对方改动了什么。其实很多时候原因特别朴素——就是随机种子没固定。举个例子某个图像分类任务你用同一个超参数配置跑三次不设种子三次的准确率分别是82.3%、83.1%、81.8%。波动范围可能超过1个百分点。如果你的方案A改良后是83.5%方案B改良后是82.9%这之间的差距只有0.6个点还在随机波动范围内。这时候你根本无法判断方案B到底是真有效还是运气好。把随机种子固定下来之后同样的配置跑出来的结果应该是完全一致的CPU环境下几乎完全一致GPU环境下也会有办法尽力逼近一致。这样做的意义在于实验变量可控对比才有意义。你改了方案A性能涨了是因为改动本身带来的还是因为跑了不同随机序列固定种子之后这个干扰就被清除掉了。有人可能会反问那是不是说固定种子会让实验结果变得不真实我的看法是固定种子不是让你只汇报某一次运气好的结果而是让每次对比实验在相同起点上进行。至于想验证模型的稳定性那是另一个话题后面我会专门讲多seed实验的做法。2. torch.manual_seed() 的基本用法2.1 函数定义与参数解析torch.manual_seed(seed)是PyTorch提供的最基础随机种子设置函数作用是为CPU上的随机数生成器设定种子。import torch # 设置随机种子 torch.manual_seed(42)参数seed需要传入一个整数。这个整数本身没有魔法它只是伪随机数生成器的起点。PyTorch内部维护着一个伪随机数生成器PRNG它按照某种算法产生一系列看起来随机的数。只要生成器的算法和初始种子确定后面产生的整个随机序列就是完全确定的。打个比方伪随机数生成器就像一本很厚的随机数手册种子决定了从第几页开始翻。同一个种子翻到的内容是固定的往后的内容也是固定的。不同种子相当于从不同页开始翻看到的就是另一套序列。一个容易忽略的细节是torch.manual_seed()的返回值是一个torch.Generator对象。在需要显式控制某个操作时你可以把这个对象保存下来。gen torch.manual_seed(42) # gen 是一个 torch.Generator 实例通常简单调用就够了但如果你需要在同一个种子下构造多个随机操作或者想手动控制某个具体模块的随机源这个返回值就有用了。2.2 到底要种几个种子这是新手最容易掉进去的坑。你写了torch.manual_seed(42)信心满满地开始训练结果下次运行还是对不上。很可能是因为你只管住了PyTorch自己的随机源但你的代码里还有另外两套随机系统在自由发挥。一套是NumPy的随机数生成器。做数据增强、数据预处理时很多人会直接调np.random。例如图像随机裁剪、随机翻转、加噪声这些操作内部用的都是NumPy的随机数。如果不管住NumPytorch.manual_seed(42)管不到它。另一套是Python标准库的random模块。有时候你写代码会用random.choice、random.sample这类函数它们用的是Python自己的随机数生成器和PyTorch、NumPy都是独立的也需要单独设置。所以在实际项目中一个完整的种子设置流程至少是这样的import random import numpy as np import torch seed 42 random.seed(seed) np.random.seed(seed) torch.manual_seed(seed)至于CUDA相关的部分如果你的代码只在CPU上跑那到此就够了。如果要用GPU还得加上GPU侧的种子设置我放在下一节一起讲。2.3 种子值选择42到底有什么魔力为什么大家在示例代码里都用42这其实是个文化梗来自《银河系漫游指南》说生命、宇宙以及任何事情的终极答案是42。因为这本小说在程序员圈子里流传太广42就成了各种示例代码里的默认魔术数。它并没有什么数学上的特殊优势你用0、1、1234、2024都行。不过种子选择上有几条实用经验。第一不要频繁更换种子。同一个实验配置如果你今天用42明天用123后天用2024那三次实验之间根本没有可比性。设计实验时定下一个seed整个实验周期都用它。第二避免用0。倒不是0有什么问题而是很多框架、很多示例代码默认种子就是0如果你用0做研究有时会不小心和别人的默认行为撞车反而给自己的实验引入困惑。比如有些代码内部偷偷调用了np.random.seed(0)你自己的种子就被覆盖了。第三做多seed实验时优先选择互相之间差异较大的随机种子比如42、777、2024、1314这种。差异较大的种子通常能覆盖到更多样的初始化空间统计均值时更有代表性。3. 完整可复现实验的种子配置方案3.1 数据加载阶段的种子控制如果你只控制了PyTorch全局随机源在简单数据集上用DataLoader(shuffleTrue)训练顺序是可以复现的。但一旦DataLoader开了多进程num_workers 0情况就复杂了。PyTorch的每个DataLoader worker是一个子进程它有自己的随机状态。就算你在主进程调用了torch.manual_seed(42)worker进程内部的NumPy和Pythonrandom也可能处于一个未固定的随机状态导致数据增强结果不可复现。解决办法是给DataLoader传一个worker_init_fn回调函数。def worker_init_fn(worker_id): worker_seed torch.initial_seed() % 2**32 np.random.seed(worker_seed) random.seed(worker_seed) # 创建 DataLoader 时传入 dataloader DataLoader( dataset, shuffleTrue, num_workers4, worker_init_fnworker_init_fn )torch.initial_seed()返回的是当前进程这里就是worker进程的初始种子PyTorch会基于主进程的种子和worker的编号生成一个独立种子给每个worker。拿到这个种子后再去给NumPy和Pythonrandom设置就能保证每个worker在每次运行时的一致性。如果你连数据集里的shuffle顺序都想精确控制还可以手动传入一个Generator对象给DataLoaderg torch.Generator() g.manual_seed(42) dataloader DataLoader(dataset, shuffleTrue, generatorg)注意这个generator参数控制的是数据索引打乱的随机源。用这种方式即使你在外层调了其他种子函数只要Generator里的manual_seed固定了数据顺序就是固定的不容易被全局状态干扰。3.2 模型初始化与训练过程的确定性模型初始化用的随机数来自PyTorch全局随机源所以主进程的torch.manual_seed(42)已经能管住。但这里有一个很容易被忽略的点如果你在同一个脚本里多次创建模型每次创建时都会从当前的全局随机状态中采样。如果你中途调用过其他随机操作比如数据增强再创建模型权重初始值就变了。为了把模型初始化和其他随机操作隔离我习惯的做法是在导入所有库后立刻设置种子然后先创建模型再做其他随机操作。# 代码最开头部分 torch.manual_seed(42) model MyModel()如果在训练循环的后面还需要重置随机状态可以用torch.random.get_rng_state()和torch.random.set_rng_state()手动保存和恢复。这在某些需要精确复现特定中间状态的调试场景中很有用。还有一个和训练确定性密切相关的开关是cudnn。GPU上的卷积计算有很多种实现算法torch.backends.cudnn.benchmark默认是False此时PyTorch使用默认的卷积算法。如果某个库或者某段代码把它设成了TruePyTorch会在运行时对不同的卷积算法做基准测试选最快的。这个选快的过程本身不固定且不同算法对浮点累加顺序有差异最终结果就会有细微不同。为了保证可复现推荐做两件事torch.backends.cudnn.deterministic True torch.backends.cudnn.benchmark FalsedeterministicTrue会让PyTorch优先选择确定性的卷积算法。代价是可能牺牲一点性能但这个开销在大多数研究场景下是可以接受的。3.3 一个可以直接抄的种子工具函数把上面所有机制汇总到一个函数里放到项目的公共代码部分以后每个脚本开头调用一下就行。import os import random import numpy as np import torch def seed_everything(seed42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed(seed) # 当前GPU torch.cuda.manual_seed_all(seed) # 所有GPU torch.backends.cudnn.deterministic True torch.backends.cudnn.benchmark False os.environ[PYTHONHASHSEED] str(seed)有一个细节我必须特别说明os.environ[PYTHONHASHSEED] str(seed)这一行放在代码里其实是心有余而力不足的设置。PYTHONHASHSEED是Python解释器启动时读取的环境变量用来控制字符串哈希的随机化。解释器已经跑起来了你在脚本里再设置它通常不会改变当前进程的哈希行为。真的想管住字符串哈希需要在启动命令层面设置PYTHONHASHSEED0 python train.py这个变量影响的是字符串哈希顺序。如果你的代码里大量使用字符串作为字典键或者依赖集合、字典的迭代顺序哈希随机化会导致每次运行顺序不稳定从而影响数据处理顺序。不过大部分PyTorch训练脚本不依赖这种顺序如果你确实需要极致可复现再把这条加上。4. 设置了种子结果仍然不同排查实录4.1 常见原因一NumPy 和 Python random 没管住这绝对是我见过的最频繁的种子失效原因。你写了torch.manual_seed(42)然后数据增强代码里用的是np.random.uniform或者random.randrange。每次运行数据增强的结果都不一样模型输入都不一样结果当然对不上。排查方法很简单在代码里全局搜索所有随机相关调用。np.random开头的函数包括np.random.seed、np.random.rand、np.random.randint等random.开头的函数包括random.seed、random.choice、random.sample、random.random等还有第三方库里间接调用这些随机函数的情况比如某些数据增强库内部用NumPy这种情况下你需要看该库是否暴露了设置种子的接口实用技巧在训练脚本开头调用seed_everything()后你可以先跑一次完整流程打印出第一个batch的样本索引或第一个样本增强后的张量值然后重新启动脚本再打印一次两次相同基本说明随机源控制住了。4.2 常见原因二DataLoader 多进程的坑前面讲了worker_init_fn的作用这里再说一个我实际踩过的更隐蔽的坑。即使你设置了worker_init_fn在PyTorch某些版本中如果shuffleTrue并且你又给DataLoader传了generator两个随机源都参与的情况下一旦变量传递顺序有一点混乱就会出现部分复现、部分不复现的诡异现象。我的建议是DataLoader的随机源控制尽量只走一条路。要么用全局种子加worker_init_fn要么用自定义Generator加worker_init_fn不要混用太多层级的随机控制逻辑。保持简单出问题时更好排查。另外num_workers0和num_workers0的随机行为本身就有差异。你在调试环境下用num_workers0跑通了换到多进程训练时随机性就不一样了这不是你的代码写错了而是多进程下的随机状态本来就重新分配了。遇到复现问题时先在同一套环境配置下对比不要跨配置对比。4.3 常见原因三GPU 运算的不确定性这个是很多人的知识盲区。就算你在CPU上所有随机源都设置好了程序也声明了确定性算法GPU上某些操作仍然可能不完全确定。原因在于GPU的并行计算中大量使用了原子操作。多个线程同时执行加法或散射操作时数据完成累加的顺序取决于硬件调度而这种调度并不保证每次一致。微小的浮点累加顺序差异经过多层传播后模型参数就会出现细微偏差。这种偏差通常很小但在训练过程中会被放大。PyTorch从1.10版本左右开始提供torch.use_deterministic_algorithms(True)接口比仅设置cudnn.deterministic更严格。不过它是个双刃剑一方面它会更严格地检查并阻止非确定性操作另一方面如果模型或者数据增广中调用了不受支持的算子程序可能会直接报错告诉你哪里不能用确定性模式。torch.use_deterministic_algorithms(True)在实际研究中我的折中方案是先把模型在CPU小数据量上验证逻辑再在GPU上做正式训练。GPU上如果发现结果不完全一致先看差多少。如果是浮点精度级别的微小差异比如1e-7量级且不影响最终指标那就接受它如果指标级波动那一定是随机源的问题回到前两节排查。4.4 常见原因四cudnn benchmark 带来的波动前面提到的torch.backends.cudnn.benchmark True它本质上是让你在运行速度和结果一致性之间做一个权衡。开启benchmark后PyTorch会对同一卷积层的多种算法做实际运行测试选择最快的。问题是测试结果可能受硬件状态、内存布局、当前负载影响不一定每次选到同一个算法。你可能会问算法不同结果不也就是细微差异吗对单次计算差异很小但如果这个算法选择发生在很多层日积月累下来训练曲线就可能出现可见的偏离。更麻烦的是它会造成时好时坏的假象让你误判模型改动是否有效。在做对比实验、需要严格复现的场景下benchmarkFalse几乎没有争议。只有当你做产品部署、追求极致吞吐、完全不在乎逐次一致性时才考虑打开它。这一点建议直接写进你的实验规范里。5. 种子管理的一些实战心得5.1 对比实验的正确做法固定种子之后不要急着说这样实验就严谨了。我见过不少团队把固定一个种子当作唯一要求最后得出的结论依然不靠谱。原因是一个种子只代表一条随机的训练轨迹你无法判断算法改进是普遍有效还是靠运气在这个特定种子上占了便宜。更稳妥的做法是在每个实验配置下跑3到5个不同种子报告均值加减标准差。比如方案A在3个种子下的准确率是82.5、83.0、82.8方案B是84.1、83.9、84.2。这样你不仅能看到均值提升还能顺便评估稳定性比单种子对比有说服力得多。有一种更实用的小技巧叫种子配对对比方案A和方案B用同一组种子分别跑。因为两边的初始化、数据顺序、dropout模式在同一个种子下是完全一致的A和B之间的差异会被放大暴露出来更容易看出真实改进效果。相当于把随机的共模噪声减掉了。5.2 随机种子搜索固定 vs 随机做超参搜索时一个常见的困惑是应该固定一个种子让所有超参实验对齐还是每个实验随机种子我的经验是分阶段处理。前期快速筛选超参时固定一个种子保持所有实验的随机轨迹一致这时候变量只有超参本身能快速缩小范围。到了后期确定最优超参组合时再用3到5个种子验证稳定性和泛化性。千万不要在筛选阶段就随机种子不然你筛选到的所谓最优超参往往只是恰好在该种子下表现好的一组超参换个种子就垮了。另一个相关问题是seed averaging多模型融合。把同一个实验跑多个种子得到多个模型然后对预测结果做平均常常能稳定提升性能这在参加比赛时特别常见。但你要理解它和单一模型训练的本质区别它不是在优化模型结构而是在不同随机起点上做了集成。如果你做研究需要解释模型行为忘掉这个技巧如果追求最终指标可以考虑。5.3 保存与加载模型时的种子状态最后分享一个稍微进阶一点的细节。有些复现场景要求你不仅能从头复现训练还要求能从某个中间断点继续训练并保持轨迹一致。这时候单单保存模型的state_dict是不够的因为优化器的状态、学习率调度器的状态、数据加载器的迭代位置甚至随机数生成器的内部状态都会影响训练轨迹。如果你需要深度复现训练过程可以在checkpoint里额外保存这样一些东西checkpoint { model: model.state_dict(), optimizer: optimizer.state_dict(), rng_state: torch.get_rng_state(), np_rng_state: np.random.get_state(), random_state: random.getstate(), epoch: epoch }加载时把这些状态一并恢复。但说句实话我自己在绝大多数实验里不会做这么重的事。原因很简单完整的训练轨迹精确重建在实际研究中需求不高大家关注的是模型最终效果和关键节点表现。反而是轻量级的固定种子方案基本覆盖了99%的场景。只有当你需要严格复现一篇论文的整个训练日志时才值得把随机状态也纳入checkpoint管理。结尾说了这么多落到实操上我自己的习惯其实很简单每个训练脚本的第一段固定代码就是seed_everything(42)然后全项目统一用一个种子工具模块不搞特殊化。做了这个动作之后最直观的感受就是同样一份代码跑出来的结果终于咬得住了。这一行代码带来的确定性比任何复杂日志监控都更能快速帮你定位问题——训练反常时你首先排除的是随机性这个变量剩下的怀疑空间就小多了。最后再补一个我从实践中得来的启发种子管理看起来是别人踩过的坑但真正让你记忆深刻的一定是自己亲手踩一次。你完全可以在接下来的一次实验里故意不设种子连续跑两次对比一下差异然后再加上种子再跑两次看看这个函数到底改变了什么。有了这种直觉你对随机种子在整个深度学习训练中的地位就再也不会轻视了。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑