Rust in Everything
基于 burn 0.22.1

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。

💬 关联讨论

发起讨论
加载中...