读完这篇,你能做什么
复跑CPU线性分类得到同配置一致结果,并完成四条件三种子实验;Java计算主效应与交互差值。
学习理由、准备要求与知识点
为什么学它
只报告最好的一次,分不清改善来自算法还是随机波动。固定种子也不能替代环境记录或独立验证。
先读这些内容
知识点清单
- 随机源与确定性
- 代码数据配置身份
- 训练和评估模式
- 全因子消融
- 配对种子与方差
- 交互效应
- 检查点恢复状态
- 开发集与保留集
运行前先确认论文机制按文中版本说明;Java 17 完整算法,Python 3.14 与 PyTorch 2.9.1+cpu 的小型 CPU 实验。未执行 GPU、多机或大规模模型训练。
01先弄懂它是什么
重新运行实验,能不能得到同样结论,不能只靠把随机种子写成同一个数。数据顺序、预处理、初始化、优化器状态、框架和具体运算实现都会影响结果。还要先说清复现目标:是在同一机器同一版本重复运行,换机器后得到相近统计结果,还是用独立实现验证结论。每种目标需要不同证据,完成其中一种,不能自动保证另外两种。
随机种子只控制对应随机数发生器的序列。Python、NumPy 和 PyTorch 可能分别使用自己的发生器,数据加载进程和设备也可能引入其他随机来源。确定性算子设置则是限制运算采用什么实现,与设种子做的事不同。即使都设置好,也不能据此保证跨版本、跨平台,或 CPU 与 GPU 的输出每一位都完全一样。
消融实验通过改变组件来检验贡献,但“删除A后下降”只是在其他配置固定时的条件效应。A和B可能互相配合,因此分别删除并不能完整解释交互。四条件设计包含都关、只开A、只开B和都开,配上相同种子集合,比只展示完整系统和一个弱基线更能定位改进来源。
文中这几个词,先认一认
- 实验身份
- 能追溯一次实验的整套信息,包括代码、数据、配置、运行环境和随机种子。
- 主效应
- 例如看组件 A 的作用:先分别算 B 关闭、开启时,加上 A 让分数改变多少,再把这两个差值平均。本篇把这个数叫 A 的主效应,它仍取决于选用的组件状态、数据和评分方法。
- 交互项
- 先看 B 关闭时,加上 A 让分数改变多少,再看 B 开启时同样操作带来多少变化。本篇用后一个差值减前一个差值,检查 A 的效果会不会随 B 改变;它的数值缩放以正文公式为准。
02看看它怎么工作
把四个条件分数记为y00、y10、y01、y11,下标表示A和B开关。A主效应定义为((y10-y00)+(y11-y01))/2,B类似;本文交互项定义为y11-y10-y01+y00。它不同于使用正负一编码回归时的系数缩放,因此报告公式比只写“interaction”更清楚。
Java对每个种子先算上述三个量,再跨种子平均,并用在线方差算法给交互均值的标准误。一个种子无法估计样本方差,返回空值;标准误表达种子层的变动,不是测试题覆盖的全部不确定性。重复种子或多次重放同一确定结果不应假装提供新的独立样本。
Python用六个固定二维训练点训练线性分类器,因素A是按训练数据标准差缩放,B是SGD动量;每条件跑三个种子,并在四个另设点上报告准确率。先对同种子同配置运行两次比较权重和损失,再做完整十二次实验。数据过小,只能验收训练与记录流程,不能从结果推荐普遍有效配置。
先跑通一个最小例子
共享PyTorch CPU环境执行experiment.py:先重放同配置,再跑四条件×三种子,每次35步线性分类训练。
展开收起代码 · experiment.py
长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。
import hashlib
import json
import random
import numpy as np
import torch
torch.set_num_threads(1)
torch.use_deterministic_algorithms(True)
x=torch.tensor([[-2.,-3.],[-1.,0.],[0.,-2.],[1.,1.],[2.,0.],[0.,3.]],dtype=torch.float64)
y=torch.tensor([0,0,0,1,1,1])
test_x=torch.tensor([[-1.,-1.],[1.,2.],[-2.,1.],[2.,-1.]],dtype=torch.float64)
test_y=torch.tensor([0,1,0,1])
def run(seed,scale,momentum):
random.seed(seed);np.random.seed(seed);torch.manual_seed(seed)
model=torch.nn.Linear(2,2,dtype=torch.float64)
divisor=x.std(0).clamp_min(1e-6) if scale else torch.ones(2,dtype=torch.float64)
train_x=x/divisor
optimizer=torch.optim.SGD(model.parameters(),lr=.15,momentum=.9 if momentum else 0.)
initial=torch.nn.functional.cross_entropy(model(train_x),y).item()
for _ in range(35):
optimizer.zero_grad();loss=torch.nn.functional.cross_entropy(model(train_x),y)
loss.backward();optimizer.step() # 全批次固定次序,避免额外的数据加载随机源。
final=torch.nn.functional.cross_entropy(model(train_x),y).item()
with torch.no_grad(): accuracy=(model(test_x/divisor).argmax(-1)==test_y).double().mean().item()
weights=torch.cat([p.detach().flatten() for p in model.parameters()])
return initial,final,accuracy,weights
a=run(46,True,True);b=run(46,True,True)
assert torch.equal(a[3],b[3])
assert a[:3]==b[:3]
records=[]
for seed in (46,47,48):
for scale,momentum in ((False,False),(True,False),(False,True),(True,True)):
initial,final,accuracy,weights=run(seed,scale,momentum)
assert final<initial
assert torch.isfinite(weights).all() and 0<=accuracy<=1
records.append(dict(seed=seed,scale=scale,momentum=momentum,train_loss=final,test_accuracy=accuracy))
assert len(records)==12
assert all(sum(r["scale"]==s and r["momentum"]==m for r in records)==3 for s in (False,True) for m in (False,True))
digest=hashlib.sha256(json.dumps({"x":x.tolist(),"y":y.tolist()},sort_keys=True).encode()).hexdigest()
print(json.dumps({"torch":torch.__version__,"numpy":np.__version__,"data_sha256":digest,"runs":records},sort_keys=True))
print("PASS 6 check groups; repeatable only within this tested CPU configuration")
核对运行结果输出torch、numpy、数据SHA256和12次指标;PASS 6 check groups,只保证本次CPU配置下的检查。
正在加载流程图…
查看流程图源码(Mermaid)
flowchart TD
N0["冻结代码数据和实验问题"]
N1["预声明条件种子与主指标"]
N2["执行训练并保存逐次结果"]
N3["计算配对主效应与交互"]
N4["在独立保留任务确认结论"]
N0 --> N1
N1 --> N2
N2 --> N3
N3 --> N4按顺序看,每一步做了什么
- 冻结代码数据和实验问题
- 预声明条件种子与主指标
- 执行训练并保存逐次结果
- 计算配对主效应与交互
- 在独立保留任务确认结论
03对照实现代码
PyTorch 2.9文档;实现v2.9.1 · torch.manual_seed / torch.use_deterministic_algorithms
原创配置示例,参考PyTorch2.9文档与v2.9.1实际函数;实验不验证GPU确定性。
import random
import numpy as np
import torch
def configure(seed):
random.seed(seed) # Python标准库的随机源单独设置。
np.random.seed(seed) # 只设置传统全局NumPy随机源,独立Generator还需单独管理。
torch.manual_seed(seed) # 设置PyTorch随机源,不等于所有算子自动确定。
torch.use_deterministic_algorithms(True) # 无可用确定实现的已知算子可报错,而非自动消除所有环境差异。
官方复现说明明确指出完全一致结果不保证跨发行版、提交或不同平台保持;相同种子也可能在CPU与GPU得到不同结果。本文将验收限制为所测试CPU配置内的重复运行,版本号进入输出,避免把一次权重相同扩大成长期跨设备承诺。
固定v2.9.1的torch.__init__.py中use_deterministic_algorithms把设置传到底层确定性开关,文档列出受影响算子和报错情形。它不是自动捕获任意第三方随机源的万能开关,外部库、用户生成器和数据加载逻辑仍需管理。本文训练只用少量CPU线性算子。
NumPy的新Generator对象可以独立持有状态,np.random.seed并不重置所有实例。真实实验应记录并保存实际使用的发生器;从检查点继续训练还需模型、优化器、调度器、混合精度缩放器及数据位置等状态,单独载入权重不等于恢复原轨迹。
研究记录指南要求让读者理解数据划分、超参数选择、计算资源与结果变动。本文把数据摘要和逐次指标一起输出,未实现大型实验管理平台;其作用是建立可核对最小记录,而不是让一个哈希代替数据来源说明、许可或完整代码归档。
代码先调用谁,再调用谁
- 配置实际使用的随机数发生器
- 选择确定性执行策略
- 建立模型和优化器
- 保存运行身份与逐次指标
04动手写一个例子
配对种子的二因素效应汇总
每行一个种子,四列依次为00、10、01、11分数;返回A、B主效应和交互差值均值,至少两种子时给交互均值标准误。
- 输入 1
[10,12,13,15]- 输出 1
A=2,B=3,交互0
- 输入 2
两种子交互分别0与2- 输出 2
均值1,标准误1
先在种子内部作差能抵消共同平移,例如一个种子所有条件都高十分,交互差值不会改变。这与先随意匹配不同种子结果不同;数据输入的配对语义必须来自实验设计,算法无法自动判断四列是否真的来自同一随机条件。
在线更新均值和M2,处理第t行后meanI为此前交互项的均值,M2为它们相对当前均值的平方偏差和。t大于一时样本方差为M2/(t-1),均值标准误再除t后开方。浮点舍入可能产生极小负数,因此对M2作非负截断后计算根号。
每行固定四个数且只更新常数个标量,N个种子总时间O(N)、额外空间O(1)。这不是显著性检验,也没有凭标准误自动输出优劣。输入有限范围防止平方项溢出;单行缺少方差信息,明确返回OptionalDouble.empty,不能用零表示“完全稳定”。
为什么这样写是对的处理t行后,三个均值对应前t个配对效应,M2是交互项围绕当前均值的平方偏差和。
每种子固定四列、常数次运算,统计状态不随种子数增长。
保存为 Main.java,运行 javac --release 17 -encoding UTF-8 Main.java,再运行 java Main。程序包含固定输入自测,断言失败时会抛出错误。
展开收起代码 · Main.java · 完整可运行程序
长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。
import java.util.*;
public class Main {
static void near(double expected,double actual) {
if (!Double.isFinite(actual) || Math.abs(expected-actual)>1e-9*Math.max(1,Math.abs(expected)))
throw new AssertionError(expected+" != "+actual);
}
static void check(boolean ok) { if(!ok) throw new AssertionError(); }
static void bad(Runnable r) {
try { r.run(); } catch(IllegalArgumentException e) { return; }
throw new AssertionError("invalid input accepted");
}
record Effects(double a,double b,double interaction,OptionalDouble standardError) {}
static Effects effects(double[][] rows) {
if(rows==null||rows.length==0) throw new IllegalArgumentException("seeds");
double meanA=0,meanB=0,meanI=0,m2=0;int count=0;
for(double[] r:rows) {
if(r==null||r.length!=4) throw new IllegalArgumentException("four conditions");
for(double x:r) if(!Double.isFinite(x)||Math.abs(x)>1e100) throw new IllegalArgumentException("score");
double a=((r[1]-r[0])+(r[3]-r[2]))/2; // 顺序为00、10、01、11,A效应在B两种状态取平均。
double b=((r[2]-r[0])+(r[3]-r[1]))/2;
double interaction=(r[3]-r[2])-(r[1]-r[0]); // 差中之差,不能与正负一编码回归系数混用。
count++;meanA+=(a-meanA)/count;meanB+=(b-meanB)/count;
double delta=interaction-meanI;meanI+=delta/count;m2+=delta*(interaction-meanI); // 在线维护跨种子交互项样本方差。
}
OptionalDouble se=count<2?OptionalDouble.empty():OptionalDouble.of(Math.sqrt(Math.max(0,m2)/(count-1)/count));
return new Effects(meanA,meanB,meanI,se);
}
public static void main(String[] args) {
Effects a=effects(new double[][]{{10,12,13,15}});near(2,a.a());near(3,a.b());near(0,a.interaction());check(a.standardError().isEmpty());
Effects b=effects(new double[][]{{0,0,0,4}});near(2,b.a());near(2,b.b());near(4,b.interaction());
Effects c=effects(new double[][]{{10,12,13,17},{20,22,23,27}});near(2,c.interaction());near(0,c.standardError().orElseThrow());
Effects d=effects(new double[][]{{0,0,0,0},{0,0,0,2}});near(1,d.interaction());near(1,d.standardError().orElseThrow());
Effects e=effects(new double[][]{{4,3,2,1}});near(-1,e.a());near(-2,e.b());
bad(()->effects(new double[][]{}));
bad(()->effects(new double[][]{{1,2,3}}));
bad(()->effects(new double[][]{{1,2,3,Double.NaN}}));
System.out.println("PASS 8 cases");
}
}
这些特殊情况也要试一试
- 单种子标准误不可计算
- 共同平移不改变交互
- 负效应合法
- 缺列
NaN和空数据拒绝
进阶选读 · 可恢复训练与多机复现实验
场景训练持续多天,重启或更换设备后需解释结果偏差。
做法保存全部训练状态和数据游标,冻结依赖与代码;分开验证不中断与恢复轨迹,再在目标设备用多种子比较统计稳定性。
复杂度状态存储随模型、优化器和数据采样状态增长,恢复I/O与分布式同步需实测;本篇O(N)汇总不描述训练成本。
好处与代价更强确定性可能牺牲性能,逐位一致与统计复现是不同验收目标;多机部分仅作为后续实验。
05项目里怎么用
实验前写清假设、主指标、因素范围和停止规则,避免观察到结果后不断改口径。开发集用于选择学习率和训练步数,最终保留集不参与这种选择;如果测试结果反复影响改动,应承认它已被用于开发,并安排新的确认任务。固定随机种子不能防止这种信息泄漏。
消融应尽量保持预算公平。删除一个组件可能同时减少参数、训练时间和输入信息,结果变化不能只归因于组件名称;必要时加上参数量或计算匹配的对照。报告所有预声明种子的结果,失败运行也记录原因,不将训练崩溃静默删除后只汇总成功种子。
长期记录应包含可运行入口、依赖版本、数据划分、配置、种子、资源耗时及产物校验值,并保存简短结论与未解决问题。本文十二次微型CPU训练已给出完整逐次输出,但未检验GPU算子、数据并行、真实Agent环境或大模型恢复;这些必须作为单独阶段进行验证。
容易踩的坑
- 只设torch种子忽略其他随机源
- 确定性开关被当跨版本保证
- 挑最佳种子不报告失败
- 消融同时改变多个条件
- 保留集反复用于选择配置
几种做法,怎么选
表格有四列,窄屏可左右滑动查看代价和适用条件。
| 方案 | 优势 | 代价与限制 | 适用条件 |
|---|---|---|---|
| 单种子调试 | 便宜且便于定位 | 不能估计随机波动 | 实现检查阶段 |
| 多种子配对消融 | 能观察条件与交互 | 训练成本成倍增加 | 方法对比与稳定性 |
| 跨环境复现 | 检验迁移和独立实现 | 需要更完整记录和资源 | 准备发表或长期维护 |
06检验是否读懂了
先自己想、自己写,再展开答案核对。场景题是假设的练习情境;遇到还没学过的内容,可以做完基础后再回来。
先用自己的话解释
01同种子为何仍可能不同?
展开参考答案与推导
框架、设备、算子和其他随机源可能变化,种子只约束相应发生器。
02交互项为正意味什么?
展开参考答案与推导
按本文差中之差定义,A开启时的增益在B开启条件下更大,不等于总体一定有用。
03一个种子标准误能写零吗?
展开参考答案与推导
不能,缺少样本方差信息,应标记不可估计。
换个情况,还会处理吗
01只载入模型后训练路径不同?
展开参考答案与推导
检查优化器、调度器、随机状态和数据位置是否恢复。
02完整方法只在一个种子领先?
展开参考答案与推导
展示所有预定种子,检查波动和交互,不将最好一次当平均收益。
03开确定性后速度降低?
展开参考答案与推导
说明目标是重复性还是吞吐,记录性能影响并分别验证,不能直接关掉后仍称同等复现。
写代码试一试
01计算交互。
- 输入 / 条件
00=0,10=0,01=0,11=4- 输出 / 预期
交互4,主效应各2
提示 差中之差
展开参考答案与推导
先比较B开关下的A效应,再相减。
02验证种子内共同偏移。
- 输入 / 条件
四条件均加10- 输出 / 预期
所有效应不变
提示 成对差分
展开参考答案与推导
常数在每个差中抵消,测试原值与偏移值。
03保存实验身份。
- 输入 / 条件
代码数据配置及版本- 输出 / 预期
可重建运行记录
提示 哈希加原始说明
展开参考答案与推导
记录摘要同时保存可解释配置和数据划分,哈希不能替代内容。
读完之后,独立完成
课后作业:自己做一遍
本篇共 12 道作业,先完成 9 道基础与综合任务,再按前置知识挑战进阶题。其中包含 2 道思维拓展,试着换个条件,看看原来的办法是否还管用。不附参考答案。
复跑CPU线性分类得到同配置一致结果,并完成四条件三种子实验;Java计算主效应与交互差值。
本篇重点加练
配对种子、交互效应与保留集纪律共同影响归因,需要检验选择过程和不完整实验。
- 配对种子与方差
- 交互效应
- 开发集与保留集
重点内容会多练几次。先说清楚怎么做,再试试空数据、出错或条件改变时会发生什么。
先自己做,卡住时记下问题,再回来查。问答题写清“为什么、什么时候适用”;动手题留下输入、输出和运行步骤,方便下次重做。
基础必做
- 作业 01原理问答
为一个 CPU 随机训练实验列出随机源、代码数据配置身份及 train
/eval状态;区分可重复运行、数值近似复现和结论复现。- 提交内容
- 复现清单与验收层次
- 怎样算完成
- 身份信息足以重建输入
- 一致性标准可检验
练到的知识点随机源与确定性 · 代码数据配置身份 · 训练和评估模式
- 作业 02代码推演
因素 A、B 各有开关,给出三个种子下四种配置的自建观测表,计算主效应与交互量并说明配对身份。
- 提交内容
- 全因子表与计算记录
- 怎样算完成
- 四条件齐全
- 种子配对不打乱
练到的知识点全因子消融 · 配对种子与方差 · 交互效应
- 作业 03原理问答
四条件消融各自使用不同种子,完整方法又挑最佳种子。写出该数据还能回答与不能回答的问题,并设计缺失运行补齐计划。
- 提交内容
- 可识别性说明与运行矩阵
- 怎样算完成
- 配对关系可追踪
- 补实验不依赖结果优劣
练到的知识点配对种子与方差 · 交互效应
综合应用
- 作业 04编程实践
实现 CPU 实验登记与恢复工具,保存配置摘要、种子、模型和优化器状态;中断后重跑一个条件并对照其原始记录。
- 提交内容
- 程序与恢复审计
- 怎样算完成
- 实验身份不可静默覆盖
- 恢复状态范围明确
练到的知识点代码数据配置身份 · 配对种子与方差 · 检查点恢复状态
- 作业 05排错实验
分别漏设数据生成种子、忘记 eval、只恢复模型权重,记录三种故障对实验结果的影响,并设计检查器定位。
- 提交内容
- 故障注入与诊断记录
- 怎样算完成
- 每次只改变一个因素
- 训练和评估随机性分开
练到的知识点随机源与确定性 · 训练和评估模式 · 检查点恢复状态
- 作业 06面试场景
完整方法在两个种子领先、另外三个落后。制定报告规则与后续实验问题,避免事后只保留最好配置。
- 提交内容
- 完整报告与后续协议
- 怎样算完成
- 所有运行都有去向
- 开发探索与保留检验分开
练到的知识点配对种子与方差 · 交互效应 · 开发集与保留集
- 作业 07编程实践
CPU 生成含共同种子扰动和真实交互项的四条件数据,比较保留配对与打乱配对后的估计和波动;公开生成参数。
- 提交内容
- 模拟代码与两种分析
- 怎样算完成
- 真效应和噪声分开
- 配对改变是唯一分析差异
练到的知识点配对种子与方差 · 交互效应
- 作业 08设计评审
保留测试集被用于选择提示、模型和最佳断点之后,还能作何种报告?设计新的确认性数据与旧结果的标签规则。
- 提交内容
- 实验分层及结果命名规范
- 怎样算完成
- 已使用信息不被重新称盲测
- 新验证独立性可说明
练到的知识点开发集与保留集
- 作业 09排错实验
一个种子运行失败被自动重试直到成功,日志只保留最后一次。构造 CPU 演练并修改登记规范,保留所有尝试与预定失败处理政策。
- 提交内容
- 重试偏差演练及账本
- 怎样算完成
- 逻辑运行和尝试编号分开
- 失败不会静默消失
练到的知识点配对种子与方差 · 开发集与保留集
进阶挑战 · 掌握前置后选做
- 作业 10设计评审
制定两个因素的确认性消融实验,预定种子数、主要比较、失败运行处理与保留任务;先用模拟数据检验分析脚本。
- 提交内容
- 预注册协议和分析脚本
- 怎样算完成
- 交互效应被考虑
- 模拟演练不冒充真实结论
练到的知识点随机源与确定性 · 全因子消融 · 交互效应 · 开发集与保留集
思维拓展
先写出你的猜想,再用例子或实验验证。答案可以不同,但要说清依据、适用情况,以及还有什么没弄懂。
- 作业 11思维拓展 · 条件变化
两个组件在开发集表现互补,但保留分布改变后交互项反向,是否意味着实验错误?设计分布改变的可控案例并提出区分机制变化与实现错误的证据。
- 提交内容
- 双分布案例与诊断计划
- 怎样算完成
- 各分布目标明确
- 交互结论限定适用范围
练到的知识点交互效应 · 开发集与保留集
- 作业 12思维拓展 · 跨域迁移
迁移到跨团队复现:不同团队无法使用相同硬件和随机算法时,应该优先复现逐位结果还是结论?制定分层验收而不预设所有差异都可接受。
- 提交内容
- 复现层级及拒绝标准
- 怎样算完成
- 数值与结论容差独立
- 设备差异有记录和边界
练到的知识点配对种子与方差 · 开发集与保留集
完成后,再检验一次
给每题标记“独立完成 / 查阅后完成 / 尚未完成”。查过资料或没做出的题,记下卡在哪里。隔一天换一组输入再试,能解释结果和出错原因,再把这个知识点标为掌握。
继续阅读
先看能帮你完成当前练习的部分。例子跑通后,再读更深入的解释。
- 必读参考文档英文
PyTorch2.9复现指南 (新标签页)↗
必读随机源、确定性及跨平台限制。
- 实践参考文档英文
PyTorch v2.9.1设置实现 (新标签页)↗
对照
use_deterministic_algorithms及底层开关。 - 深入参考文档英文
Deep RL that Matters v3 (新标签页)↗
理解种子、实现差异和报告方式如何影响结论。
- 深入参考文档英文
NeurIPS论文检查清单 (新标签页)↗
实践记录数据、代码、超参数与实验资源。