梯度累积:loss为什么不能直接平均
梯度累积能把多个小批次的贡献合到一次参数更新,但“每个小批次的平均loss再除以累积步数”不总是等于大批次训练。对按token平均的目标,只要各小批次参与损失的token数不同,这种写法就会改变每个token的权重。
文章目录
关键是先确定优化目标的分母,再决定每次反向传播的缩放。下面用一个只有一个参数的精确例子,同时核对损失、梯度和一步更新,避免只看日志中的平均数。Read in English。
先明确你想平均什么
假设一个累积窗口包含K个小批次,第i批有nᵢ个有效token,逐token损失之和为Sᵢ。若目标是所有有效token等权,窗口损失应为L=(ΣSᵢ)/(Σnᵢ)。当每批先返回均值Lᵢ=Sᵢ/nᵢ时,应使用L=Σ(nᵢ/N)Lᵢ,其中N=Σnᵢ。
直接取ΣLᵢ/K,表示每个小批次等权;它通常不是逐token等权。只有各nᵢ相同等特殊条件下,两种权重才对应。偶然算出相同数值,也不能证明实现对任意长度分布都正确。
逐token等权本身是目标选择,不是所有任务唯一正确的方案。有些训练有意给每个样本相同权重,此时长度不同的样本可能需要另一套归一化。问题在于实现是否匹配事先声明的目标,而不是看到mean就一律改成sum。
一个参数就能看到梯度走向改变
让模型对每个有效项都预测同一个标量w,损失为ℓ=(w-y)²/2。A批只有一个目标0,B批有三个目标2;在w=0处,A的梯度和为0,B的梯度和为-6。全窗口梯度为-6/4=-1.5。
若先求每批平均梯度再平均,得到(0+(-2))/2=-1。此处完整窗口loss为1.5,而批均值平均为1;日志数值与梯度都可能一起变化。两个算法使用同一份数据、同一个参数,也都只更新一次,但更新幅度不同。学习率η=0.1时,正确逐项平均的一步从0走到0.15,批均值平均只走到0.1。
这里使用平方损失只是为了可以手算;论证依赖的是求和、常数分母与求导的线性关系,而不是语言模型特有结构。交叉熵的逐token求和同样需要匹配所声明的归一化。
八项检查:对照完整窗口而非日志
标准库Fraction可以让这个二次模型的数值精确。代码对比完整窗口梯度、加权小批次梯度和中心差分,并覆盖等长度、空批次与空窗口。未运行PyTorch、Transformers或真实模型训练。
# Scalar squared-loss teaching example; no training framework is run.
from fractions import Fraction as F
groups = [[F(0)], [F(2),F(2),F(2)]]
w = F(0)
def loss_sum(w, ys):
return sum((w-y)**2/2 for y in ys)
def grad_sum(w, ys):
return sum(w-y for y in ys)
def token_loss(w, batches):
n = sum(len(ys) for ys in batches)
if n==0:
raise ValueError("no valid items")
return sum(loss_sum(w,ys) for ys in batches)/n
def token_grad(w, batches):
n = sum(len(ys) for ys in batches)
if n==0:
raise ValueError("no valid items")
return sum(grad_sum(w,ys) for ys in batches)/n
n = sum(map(len,groups))
mean_of_means = sum(grad_sum(w,ys)/len(ys) for ys in groups)/len(groups)
weighted = sum(F(len(ys),n)*(grad_sum(w,ys)/len(ys)) for ys in groups)
global_g = token_grad(w,groups)
h = F(1,100)
finite_diff = (token_loss(w+h,groups)-token_loss(w-h,groups))/(2*h)
assert global_g == F(-3,2)
assert mean_of_means == -1
assert weighted == global_g
assert finite_diff == global_g and token_loss(w,groups) == F(3,2)
assert token_grad(w,[[F(0)],[F(2)]]) == -1 # equal counts
assert token_grad(w,[[],*groups]) == global_g
assert w-F(1,10)*global_g == F(3,20)
try:
token_grad(w,[[],[]])
except ValueError:
pass
else:
raise AssertionError("empty window accepted")
print("8 gradient checks passed; global=-3/2, mean-of-means=-1")
本地执行通过八项检查。中心差分对这个二次损失给出精确导数,不是一般非线性模型中任意步长都成立的结论。代码中的weighted与global_g一致,说明按nᵢ/N加权能恢复这个固定窗口目标;mean_of_means则暴露了另一个目标。
有效token数不是attention_mask总和
在因果语言模型中,标签通常要与预测位置错位对齐,损失还可能忽略padding、提示词或其他位置。因此分母应统计实际参与该次损失的目标项,而不是直接统计输入token、序列长度或原始attention_mask。
PyTorch 2.14交叉熵参考说明ignore_index与reduction的关系。本文推导默认没有额外类别权重,且按有效项等权;启用类别权重、样本权重或其他目标时,分母应遵循对应损失定义,不能只照搬非忽略项数量。
Hugging Face在2024年10月16日的修复说明就讨论了token级任务中“跨批损失总和除以有效token总数”与“平均每批均值”的区别。它是历史问题的解释材料,不表示今天所有版本的Trainer仍有同一个Bug,也不是建议安装两年前的开发分支。
反向传播前,把窗口分母弄清楚
单设备、固定窗口且没有其他自动缩放时,可以对每批损失和Sᵢ/N执行反向传播,累加后再更新一次;若接口只能提供正确的逐项均值,就用nᵢ/N乘回相应权重。前提是已知这个窗口的N,并确认内部没有再次缩放。N来自标签是否参与目标的计数,不应依赖当前参数w;若权重本身由可学习量决定,求导还要考虑另一层依赖。
若训练框架已经处理累积步数或有效项归一化,外层再除一次,就可能把梯度额外缩小。不要从参数名猜测行为:检查实际loss定义、返回值的归一化、反向路径以及优化器更新时机。
另一种实现会先累积未归一化梯度,再在更新前统一除以N。它要求你理解梯度缩放、裁剪和数值范围之间的顺序,不能只在已有复杂训练循环里随便插入一个除法。本文给的是目标等式,不是未经验证的通用训练器补丁。
窗口开始
固定目标与有效项分母
小批次计算
记录损失和与有效项数
累积梯度
避免框架外再次缩放
一次更新前
核对归一化与裁剪顺序
最后一个窗口、空批次和多设备
数据末尾的窗口可能只有两步,而配置的累积步数是四。若仍固定除以四,且框架没有补偿,更新尺度会再次偏离预期。窗口实际包含哪些项,比配置文件里的最大步数更直接。
某个小批次可能没有有效标签,它应贡献零损失和与零有效项,而不能在求均值时产生除零。若整个窗口都没有有效项,目标分母不存在,应按训练设计跳过或报告;不要把它记成一个正常的零loss更新。
多设备还要处理跨rank的有效项计数,以及梯度通信是求和还是平均。单设备的缩放式不能原样当作DDP公式;应对所用框架的通信与自动归一化规则建立单独对照。本例没有验证多卡实现。
数值接近,也未必是完整训练等价
梯度累积与一次大批次的比较,还依赖前向和目标可分解、窗口内参数不提前改变。若每个小批次都做optimizer.step,就已经不是这里讨论的一次窗口更新。
Dropout、依赖批次统计的层、梯度裁剪时机、混合精度与浮点累加顺序,也可能影响对照。为先定位归一化问题,可以在一个小的确定性目标上比较完整批梯度与累积梯度,再逐项恢复其他设置。
站内PPL计算文章也需要按有效token聚合,但训练这里多了一层后果:分母改变会进入反向传播,影响参数更新。修好显示的loss,并不自动修好训练梯度。
一份有用的回归记录应该包含什么
保留每批有效项数、损失和、窗口总项数、实际更新次数与选定参数的梯度对照。构造一组长短差异明显的批次,再构造等长度批次作为对照;后者单独通过,容易掩盖权重错误。
先比较同一初始化、同一窗口、同一优化器条件下的一步,而不是马上比完整训练后的指标。一步对齐以后,再验证真实数据与框架路径。这样发现差异时,可以定位是目标权重、前向随机性还是更新时机,而不是统称为“训练不稳定”。
如果这套推导和精确例子帮你找到了一次被批均值掩盖的梯度差异,欢迎通过文末支持区自愿支持。
参考与验证范围
参考Hugging Face历史修复说明和PyTorch 2.14交叉熵定义,于2026年10月17日核对。两幅图、平方损失构造和代码为原创教学内容;本地验证不包含模型微调或分布式运行。
支持
如果文中的解释或示例帮你解决了问题、节省了时间,欢迎自愿打赏支持本站。
二维码可点击放大。更多支持方式见支持页面。


