梯度累积 从不等长微批到一次正确的优化器更新
沿定义、计算与实验边界阅读,分开模型原理、教学验证和实际系统效果。
章节速览7 节 · 展开选择
反向传播已经说明梯度会累加,但“多做几次backward再step”还不够完整。若最后一个微批较短,或者一个微批包含更多有效token,简单把每个微批损失除以同一个累积次数,可能悄悄改变训练目标。
本篇只讨论一个清楚的问题:希望得到某批记录的平均损失梯度时,怎样把它拆成微批计算,最后只更新一次参数? 我们先固定目标分母,再推导权重,用完整的小程序核对算术。程序是标量回归的教学模型,不运行深度网络,也不声称测量显存、训练速度或模型收益。
把分母写进记录,再分配各微批的权重。微批个数相同,不代表每个微批包含的信息单位一样多。
先收齐本窗口的贡献,再做属于一次更新的裁剪、step和清零;不要在中途改变参数或缩放口径。
累积的对象是什么
在PyTorch中,反向传播会把叶子参数的梯度累加到.grad中,而不是每次自动覆盖;官方清零教程说明了这一行为,见文献1。这是一项存储规则,不是“无论怎样拆批都与大批次完全等价”的保证。
设本次有效批有N条记录,拆成K个不重叠微批B₁至B_K,第k个有n_k条,且Σn_k=N。所有微批的梯度必须针对同一个参数值θ计算。先假定每条记录的损失可以独立相加,不含依赖本批其他记录的运算。
L_k(θ) = (1/n_k) Σᵢ∈B_k ℓᵢ(θ)
N与n_k为正计数,微批不重叠且覆盖同一批记录;所有项在相同θ处求值。损失、参数与梯度的单位取决于模型;本篇标量数值按无量纲合成数据处理。
- N|目标分母
- 本次更新对应的有效单位总数。标量回归例子是一条记录一个单位,不能自动推广成任意张量的第一维长度。
- n_k|微批分母
- 若微批返回自己的均值,应知道它实际除以什么。掩码、忽略标签或样本权重会改变有效分母。
- K|计算分块数
- 只描述分成几次计算。只有各n_k相同、目标口径相同等条件满足时,1/K才是正确的均值权重。
由求和与求导的线性性,完整梯度可以按微批均值加权。若所有n_k相等,n_k/N=1/K;不相等时,就不能继续套用同一个1/K。
= (1/N) Σₖ ∇[Σᵢ∈B_k ℓᵢ(θ)]
第一行合成微批均值,第二行先累加各微批损失和。两条路径应使用同一个目标分母,并在窗口内保持θ不变。若每条记录带权,分母应按实际目标改写,不能机械替换为记录数。
三条记录,错误平均在哪里出现
取无偏置线性模型预测wx,单条损失为ℓ=½(wx−y)²,梯度为x(wx−y)。这里特意使用½,方便手算;它不是torch.nn.MSELoss默认损失的原样表达。官方MSELoss默认均值对输出中的所有元素取平均且没有这个½,使用时需核对缩放,见文献2。
令w=1,三条记录为(1,0)、(2,0)、(3,0)。单条梯度依次为1、4、9,因此完整批次平均梯度是14/3。拆成前两条一个微批、最后一条一个微批,就得到下表。
| 微批 | 记录数n_k | 梯度和 | 微批均值梯度 | 正确权重 |
|---|---|---|---|---|
| (1,0)、(2,0) | 2 | 5 | 2.5 | 2/3 |
| (3,0) | 1 | 9 | 9 | 1/3 |
正确结果为(2/3)×2.5+(1/3)×9=14/3≈4.666667。若直接平均两个微批均值,得到(2.5+9)/2=5.75,第三条记录相对于前两条中的每一条被赋予了双倍权重。
这不是浮点精度导致的小差异,而是优化目标改变了。若学习率为0.1,普通SGD的一次正确更新是w_new=1−0.1×14/3=8/15≈0.533333;用错误均值则得到0.425。
自检1:两个微批各自都用了mean,为什么它们的均值不能再直接平均?
因为两个微批的分母分别为2和1。再各乘1/2会让较短微批中的每条记录占更大权重。应按2/3与1/3合成,或先累加梯度和再统一除以3。
逐微批更新为什么是另一条训练轨迹
仍用这三条记录,若第一微批就更新,w先从1变成1−0.1×2.5=0.75。第二微批的梯度因此变成9×0.75=6.75,再更新得到0.075。它不是固定w=1时各微批贡献的和,也没有理由等于0.533333。
参数何时改变,是累积定义的一部分。AdamW等有状态优化器还维护内部状态,增加step次数会同时改变状态更新、衰减应用与调度计数。不要只数已经处理多少样本,就把不同更新频率的训练称为完全相同。
θ_new = θ_old − ηg
η为学习率,本式仅写普通SGD,无动量或权重衰减。每个微批均使用θ_old;不能把中途更新后的梯度混入同一个固定参数梯度的等式。
一个能直接运行的标准库模型
下面的程序显式传入微批列表,固定w计算每个微批的均值梯度,再按实际记录数加权。空窗口、空微批和非有限输入被拒绝;输入及中间计算须在float可表达范围内。它没有张量自动微分、GPU或分布式通信,不是训练框架的替代实现。
1 | from math import fsum, isfinite |
最后一个窗口、掩码与分母契约
如果原计划每四个微批更新一次,数据尾部只剩两个,应先决定尾部处理策略。若仍希望优化这两批实际记录的平均损失,就用尾部实际总数归一化;继续除以四,会缩小这一更新的梯度。也可以明确丢弃尾部,但这会改变本轮使用的数据,不能不记录。
带掩码的序列任务还要问,loss的mean是否只覆盖有效token。若微批A有100个有效token、微批B有20个,且目标是120个有效token的均值,权重应为100/120和20/120;若目标本来是“每条序列先平均,再平均序列”,它又是另一项目标。两种目标都可定义,关键是不能混用分母。
清零属于窗口边界:每次新窗口开始前,不应留着上个窗口的历史梯度;窗口内部又不能在还没收齐贡献时清空。调度器与日志也应注明按微批、有效单位还是优化器更新计步。一个step的含义含糊时,仅报告“训练了一千步”不足以复现实验。
裁剪、AMP和BatchNorm的额外边界
若需要全窗口梯度裁剪,应在合成之后按该窗口目标执行。裁剪是非线性操作,通常不能与累加交换。例如两个等权微批的标量梯度为10和−8,阈值为1:分别裁剪再平均得到0,先平均得到1再裁剪仍为1。这两种操作实现了不同规则。
AMP还有缩放状态:PyTorch文档要求同一有效批内累积时保持缩放因子一致,反缩放应等相关梯度累积完成后进行;相应检查与缩放更新按有效批粒度处理,见文献3。这里不把普通float模型冒称为AMP验证,也不展开分布式通信平均的实现细节。
即便分母完全正确,BatchNorm的微批统计也未必等于完整批统计。官方文档说明其训练时使用批内统计并维护运行估计,见文献4。随机层、跨样本损失或批间状态改变,都可能使拆批后实际计算的函数不同。A2只保证同一可加目标在同一参数处的线性合成,不自动保证整段训练轨迹、随机数使用或浮点舍入逐位一致。
flowchart LR A[写目标与有效分母] --> B[固定参数计算各微批贡献] B --> C[按实际分母合成] C --> D[完成本窗口所需数值处理] D --> E[一次优化器更新] E --> F[清理梯度并进入下一窗口]
自检2:微批期间提前step一次,下一微批的梯度还能放进同一个固定参数等式吗?
不能。下一微批通常在新参数处求梯度,已经不属于A2右侧的同一个θ。它可以是一种训练方案,但应按逐微批更新描述。
自检3:分母权重正确,是否就能保证BatchNorm与大批次完全相同?
不能。训练时批内统计依赖实际微批中的其他记录;拆批可能改变前向计算与运行统计。应先检查模型是否满足本篇可加、固定函数的假设。
自检4:计划累积四批,尾部只有两批,为什么不总是继续除以四?
如果目标是尾部实际有效单位的均值,应按这两批的实际总分母归一化。继续按四个等长微批缩放,会得到较小梯度。若刻意采用这种规则,应明确记录其目标与步长含义。
flowchart TD A[拆批结果与预期不一致] --> B[先查每个mean的分母] B --> C[再查参数是否在中途变化] C --> D[核对清零与优化器计步] D --> E[检查裁剪缩放与批依赖算子] E --> F[最后区分数值误差与目标变化]
参考文献与验证范围
- PyTorch:Zeroing out gradients。 官方教程。核对.grad累加与清零行为;本篇微批权重由目标求和独立推导。
- PyTorch 2.15:MSELoss。 官方API文档。用于区分输出元素均值与记录数,以及本文额外½因子;程序不是此API的复刻。
- PyTorch 2.15:Automatic Mixed Precision examples,Gradient accumulation。 有效批与缩放规则。本文没有运行AMP或GPU训练,只核对规则并提供标准库算术模型。返回A2。
- PyTorch 2.15:BatchNorm1d。 官方API文档。用于说明批内统计与运行状态边界;完整批等价需要另行检查。