模型结构写对之后,训练仍可能失败。损失不降、突然变成 NaN、梯度接近零,背后不一定是同一个原因。我们先学习怎样观察,而不是遇到问题就同时改十个参数。
激活尺度怎样影响梯度
例如 tanh 的输出被限制在 −1 到 1。输入绝对值很大时,输出接近边界,导数接近零。若许多隐藏单元长期处于这种饱和区域,误差信号很难穿过它们回到前面的参数。
输入尺度、权重初始化、层数和激活函数都可能影响这种现象。梯度很小也可能来自任务本身或模型已经接近局部平坦区,所以不能只凭一张直方图下结论。
给训练加三个观察点
在固定的一批数据上,记录隐藏层激活的均值和标准差、各层参数的梯度范数、更新量相对参数量的大小。这些记录帮助你回答:问题从哪个位置开始?更新到底太小还是太大?
# h 是某层的激活;在 loss.backward() 之后检查梯度。
print("activation", h.detach().mean().item())
print("activation std", h.detach().std().item())
for name, parameter in model.named_parameters():
if parameter.grad is not None:
print(name, parameter.grad.detach().norm().item())
代码片段要放入已有训练程序,其中 h 和 model 来自你的模型。记录多步趋势比只截取单步更有价值。检查时还要确认数据里没有非法值、标签没有越界。
归一化并不是万能修复
BatchNorm 在训练时利用当前批次统计,在评估时通常使用累计统计。因此 train() 和 eval() 会改变相关层的行为。Transformer 常见的 LayerNorm 则按不同维度归一化,不应把两者的统计方式混为一谈。
加入归一化后,仍要检查初始化、学习率、损失计算和数据划分。不要为了让曲线看起来平滑,就把验证损失混入训练曲线,或直接删除表现较差的批次。
怎样做一个有解释力的对照
保留同一数据顺序、模型结构与随机种子,只改变初始化尺度。记录改变前后的激活统计、梯度范数和留出损失。若实验有随机波动,可以用多个种子重复,并报告范围。
这一课的产出是一份诊断记录。下一课手动推导梯度,让你知道观察到的数值来自哪一步计算;随后再进入分词和 GPT,而不是把不稳定的训练循环一路复制下去。