loss 忘了 mean,梯度大了 batch 倍
主线位置:训练主线的「loss → backward」:loss 应当先归约成一个数,再求导。
版本:PyTorch 2.15.0(CPU)、burn 0.22.1(Flex 设备加 autodiff)
flowchart TB ps["逐样本 loss 4x1"] -->|PyTorch backward| err["RuntimeError"] ps -->|burn backward| seed["种子是全 1"] seed --> sum["等于先 sum"] sum --> big["梯度是 mean 的 4 倍"] ps -->|".mean() 再 backward"| ok["和 PyTorch 一致"]
实验 crate pita-t 的 p12、编不过的例子 pita-ce 的 ce12,PyTorch 侧 p12_nonscalar.py。模型是 Linear(3, 1),权重 [0.5, -0.25, 0.75]、偏置 0.1(burn 的 Linear 权重存成 [in, out],写成 [[0.5], [-0.25], [0.75]]),输入 0.1·k - 0.5(k = 0..11,先在 f64 里算再舍入成 f32,两边逐位相同),batch 为 4。debug 构建。平台 M2 Max、macOS 27、rustc 1.99.0。
PyTorch 的写法
自己写 loss 时,先算逐样本的误差,最后 .mean()。忘了最后一步:
per_sample = (fc(x) - y) ** 2 # [4, 1], forgot .mean()
try:
per_sample.backward()
except RuntimeError as e:
print("per_sample.backward():", e)
loss = per_sample.mean()
loss.backward()
per_sample.backward(): grad can be implicitly created only for scalar outputs
mean loss 1.3181250095367432
weight.grad [0.10749998688697815, 0.04249998927116394, -0.02250000089406967]
bias.grad [-0.6499999761581421]
PyTorch 的 backward() 只接受标量,非标量要显式给 gradient。这一条报错替你抓住了漏写的 mean()。
搬到 burn 的现象
let per_sample = (fc.forward(x) - y).powi_scalar(2); // [4, 1], forgot .mean()
let grads = per_sample.backward();
per_sample dims [4, 1]
nomean weight.grad [0.42999995, 0.17000008, -0.089999914]
nomean bias.grad [-2.6]
没有报错。每个梯度都是 PyTorch 那组数的 4 倍(-0.65 × 4 = -2.6),4 就是 batch 大小。训练照样收敛,只是等效学习率被乘了 B,换一个 batch 大小,同样的学习率就是另一个训练。
原因
burn 的 backward 从一个和输出同形状的全 1 张量开始:Gradients::new 用 B::float_ones(root_tensor.shape(), ..) 给根节点登记梯度。对 [4, 1] 的输出,这等于先 sum() 再求导。Tensor::backward 的文档讲了图只能用一次、什么时候 panic,没有提输出必须是标量。两边的完整对照(包括 PyTorch 的 y.backward(v) 怎么写成 burn)见 backward 与梯度的「非标量输出」。
burn 自带的 loss 已经归约好了:MseLoss::forward 按 Reduction::Mean / Auto 返回 tensor.mean(),逐样本的版本叫 forward_no_reduction,对应 PyTorch 的 reduction="none"。这个坑出在自己拼的 loss 上,或者用了 forward_no_reduction 又忘了归约。
改法
把 .mean() 补上:
let grads = per_sample.mean().backward();
per_sample dims [4, 1]
mean weight.grad [0.10749999, 0.04250002, -0.022499979]
mean bias.grad [-0.65]
两边各写一份梯度的 JSON,compare.py 容差 1e-6:
weight n= 3 max|diff|=2.98e-08
bias n= 1 max|diff|=0.00e+00
worst=2.98e-08 tol=1e-06 OK
权重梯度最大差 2.98e-8,在 0.0425 那一项,是 8 个 ulp(-0.0225 那一项差 2.24e-8,12 个 ulp;第一项逐位相同):权重梯度是 4 个样本的乘积求和,两边求和的顺序和 mean 的 1/4 在哪一步乘进去不同,误差为什么不是 0 见怎么读这一卷。偏置梯度两边逐位相同。反向验证:拿没有 mean() 的那组梯度去比,同一条命令报 bias ... max|diff|=1.95e+00、MISMATCH,退出码 1。
怎么检查
在 backward 前面加一道关:一个只收 Tensor<1>、并断言形状是 [1] 的辅助函数。
/// Only accepts a loss that was reduced to one value.
fn backward_scalar(loss: Tensor<1>) -> Gradients {
assert_eq!(loss.dims(), [1], "loss must be reduced (mean/sum) before backward");
loss.backward()
}
逐样本 loss 是二维([B, 1])时,编译器直接拦下:
error[E0308]: mismatched types
--> src/bin/ce12.rs:14:32
|
14 | let _grads = backward_scalar(per_sample);
| --------------- ^^^^^^^^^^ expected `1`, found `2`
| |
| arguments to this function are incorrect
|
= note: expected struct `Tensor<1>`
found struct `Tensor<2>`
逐样本 loss 是一维([B],比如先 flatten 过)时,类型对得上,断言在运行期拦下:
thread 'main' (8835599) panicked at src/bin/p12.rs:9:3:
assertion `left == right` failed: loss must be reduced (mean/sum) before backward
left: [4]
right: [1]
sum() 和 mean() 都返回 [1],这道关分不出两者;用 sum() 时要自己确认学习率是按求和定的。
小结
- PyTorch 拒绝对非标量 backward;burn 对任何形状都 backward,等于先
sum()。 - loss 忘了
mean(),梯度是 batch 大小倍(实测 4 倍),不报错。 - 补上
mean()后和 PyTorch 差 2.98e-8(容差 1e-6)。 - 用只收
Tensor<1>、断言[1]的backward_scalar把关:二维的逐样本 loss 编译不过,一维的运行期 panic。