VLA 系统学习第 7 课:loss.backward() 到底做了什么?——从计算图到反向传播

第六课标准答案

先把上一课的问题收掉,然后直接进入第七课。今天的主线会非常明确:

\ \\text{Forward} \\rightarrow \\text{Loss} \\rightarrow \\text{Chain Rule} \\rightarrow \\text{Backpropagation} \\rightarrow \\text{Gradient} \\rightarrow \\text{Parameter Update} \\


1. 为什么很多个 Linear 中间没有激活函数,最终仍然等价于一个 Linear?

假设两层 Linear:

\ h=W_1x+b_1 \\\ y=W_2h+b_2 \\

把第一式代入第二式:

\ y=W_2(W_1x+b_1)+b_2 \\

展开:

\ y=W_2W_1x+W_2b_1+b_2 \\

定义:

\ W'=W_2W_1 \\\ b'=W_2b_1+b_2 \\

于是:

\ y=W'x+b' \\

所以:

\ Linear\\rightarrow Linear \\

最终仍然只是一个新的线性映射。

堆很多层也一样。


2. ReLU 的数学定义是什么?

\ \\operatorname{ReLU}(x)=\\max(0,x) \\

也就是:

\ \\operatorname{ReLU}(x)= \\begin{cases} 0,\&x\\leq0\\\\ x,\&x\>0 \\end{cases} \\

负数变成 0,正数保持不变。


3. [-2,3,-0.5,7] 经过 ReLU 后是什么?

逐项计算:

\ -2\\rightarrow0 \\\ 3\\rightarrow3 \\\ -0.5\\rightarrow0 \\\ 7\\rightarrow7 \\

所以:

\ \\boxed{\[0,3,0,7} \]


4. 为什么 ReLU 不改变 Shape,却仍然很重要?

假设输入:

\ X:\[B,64 \]

ReLU 只是对里面每一个元素分别执行:

\ x\\rightarrow\\max(0,x) \\

所以元素数量没有变化:

\ \[B,64\rightarrowB,64 \]

但是数值关系发生了非线性变化。

于是:

\ Linear\\rightarrow ReLU\\rightarrow Linear \\

不能再简单压缩成一个 Linear。

因此 ReLU 虽然:

不改变 Tensor Shape,

却改变了:

网络能够表示什么类型的函数。


5. nn.ReLU() 有多少可训练参数?

\ \\boxed{0} \\

因为 ReLU 的规则固定:

\ x\\leq0\\rightarrow0 \\\ x\>0\\rightarrow x \\

它没有:

\ W \\

也没有:

\ b \\

因此没有 Parameter。


6. 为什么 Hidden Feature 通常不能直接解释为某个明确物理量?

因为 Hidden Feature 是网络为了降低最终 Loss 自己学出来的内部表示。

例如:

\ \[10 \rightarrow Linear \rightarrow 64 \]

这 64 个数字不是人工规定:

复制代码
第1维 = 距离
第2维 = 速度
第3维 = 是否抓取

而是训练过程中由:

\ W,b \\

共同形成的内部特征。

所以更准确地说:

Hidden Feature 是模型学习出的中间表示,而不是人为提前规定好物理意义的状态量。


7. Shape 怎么变化?

网络:

复制代码
nn.Linear(10, 64)nn.ReLU()nn.Linear(64, 7)

输入:

\ \[32,10 \]

第一层:

\ \[32,10 \rightarrow 32,64 \]

ReLU:

\ \[32,64 \rightarrow 32,64 \]

第二层:

\ \[32,64 \rightarrow 32,7 \]

完整:

\ \\boxed{ \[32,10 \rightarrow 32,64 \rightarrow 32,64 \rightarrow 32,7 } \]


8. 为什么 Linear → ReLU → Linear 更有表达能力?

因为如果没有 ReLU:

\ Linear\\rightarrow Linear \\

最终仍然是:

\ y=Wx+b \\

只能表达线性关系。

而加入:

\ ReLU \\

以后:

\ y=W_2\\operatorname{ReLU}(W_1x+b_1)+b_2 \\

中间出现了非线性操作。

模型可以根据不同输入区域产生不同响应。

于是才能逐渐表达复杂的:

\ Observation\\rightarrow Action \\

关系。


VLA 系统学习第 7 课:loss.backward() 到底做了什么?------从计算图到反向传播

我们前面已经会写:

复制代码
pred_action = model(obs)loss = loss_fn(pred_action, action)optimizer.zero_grad()loss.backward()optimizer.step()

但现在真正没有讲透的是这一句:

复制代码
loss.backward()

PyTorch 到底凭什么知道:

复制代码
fc1.weight
fc1.bias
fc2.weight
fc2.bias

分别应该怎么修改?

它为什么可以从最后一个 Loss,一路找到前面每一个参数?

答案有两个核心概念:

\ \\boxed{\\text{计算图}} \\

和:

\ \\boxed{\\text{链式法则}} \\

反向传播 Backpropagation,本质上就是在计算图上使用链式法则,把 Loss 的梯度一层一层向前面的参数传回去。


一、先从一个只有三个运算的模型开始

先不用神经网络。

假设:

\ x=2 \\

模型有一个参数:

\ w \\

Forward:

\ z=wx \\

然后:

\ y=z\^2 \\

最后我们直接把:

\ L=y \\

看成 Loss。

整个 Forward:

\ x \\rightarrow z \\rightarrow y \\rightarrow L \\

假设:

\ w=3 \\

那么:

\ z=3\\times2=6 \\

然后:

\ y=6\^2=36 \\

所以:

\ L=36 \\


二、Forward 不只是算出了 36

这一点很重要。

我们表面上看到:

\ L=36 \\

但 PyTorch 在执行这些运算时,还需要知道:

36 是怎么来的?

因为它后面还要计算梯度。

所以逻辑关系是:

复制代码
w ─────┐
       × → z → square → y → L
x ─────┘

这就是一个最简单的:

Computational Graph

计算图。

它记录了:

哪些变量经过哪些运算,产生了哪些新变量。


三、为什么训练需要知道这张图?

因为我们最后真正想问的是:

\ \\frac{\\partial L}{\\partial w} \\

意思是:

如果参数 \(w\) 稍微变化一点,最终 Loss 会变化多少?

但:

\ w \\

并没有直接连接到:

\ L \\

中间隔着:

\ w \\rightarrow z \\rightarrow y \\rightarrow L \\

所以必须沿着这条路径,一层一层计算影响。

这就是链式法则出现的原因。


四、链式法则到底在说什么?

现在:

\ z=wx \\\ L=z\^2 \\

我们要求:

\ \\frac{\\partial L}{\\partial w} \\

但是 \(L\) 是先依赖 \(z\),而 \(z\) 再依赖 \(w\)。

所以:

\ \\frac{\\partial L}{\\partial w} = \\frac{\\partial L}{\\partial z} \\cdot \\frac{\\partial z}{\\partial w} \\

这就是最简单的:

Chain Rule

链式法则。

可以先用一句非常口语化的话理解:

\(w\) 对 \(L\) 的影响 = \(w\) 对中间变量的影响 × 中间变量对 Loss 的影响。


五、真正算一遍

因为:

\ L=z\^2 \\

所以:

\ \\frac{\\partial L}{\\partial z}=2z \\

当前:

\ z=6 \\

所以:

\ \\frac{\\partial L}{\\partial z}=12 \\

另一方面:

\ z=wx \\

因此:

\ \\frac{\\partial z}{\\partial w}=x \\

当前:

\ x=2 \\

所以:

\ \\frac{\\partial z}{\\partial w}=2 \\

于是:

\ \\frac{\\partial L}{\\partial w} = 12\\times2 \\

得到:

\ \\boxed{ \\frac{\\partial L}{\\partial w}=24 } \\

这个:

\ 24 \\

就是参数 \(w\) 当前的 Gradient。


六、为什么叫"反向传播"?

Forward 是:

\ w \\rightarrow z \\rightarrow L \\

也就是从输入和参数一路算到 Loss。

而计算梯度的时候:

\ L \\rightarrow z \\rightarrow w \\

方向反过来了。

所以:

复制代码
Forward:

w
↓
z
↓
L

而:

复制代码
Backward:

L
↓
z
↓
w

因此叫:

Backpropagation

也就是:

把 Loss 对各变量的影响沿计算图反方向传播回去。


七、现在加入真正的 Loss

前面为了简单用了:

\ L=z\^2 \\

现在换成更像 BC 的结构:

\ \\hat y=wx \\

然后正确答案:

\ y \\

平方误差:

\ L=(\\hat y-y)\^2 \\

假设:

\ x=2 \\\ w=1 \\\ y=6 \\

那么 Forward:

\ \\hat y=1\\times2=2 \\

Loss:

\ L=(2-6)\^2=16 \\

现在计算:

\ \\frac{\\partial L}{\\partial w} \\

链条是:

\ w \\rightarrow \\hat y \\rightarrow L \\

所以:

\ \\frac{\\partial L}{\\partial w} = \\frac{\\partial L}{\\partial \\hat y} \\cdot \\frac{\\partial \\hat y}{\\partial w} \\

因为:

\ L=(\\hat y-y)\^2 \\

所以:

\ \\frac{\\partial L}{\\partial \\hat y} = 2(\\hat y-y) \\

代入:

\ 2(2-6)=-8 \\

而:

\ \\hat y=wx \\

所以:

\ \\frac{\\partial \\hat y}{\\partial w}=x=2 \\

最终:

\ \\frac{\\partial L}{\\partial w} = -8\\times2 \\

所以:

\ \\boxed{-16} \\


八、这个负号意味着什么?

得到:

\ \\frac{\\partial L}{\\partial w}=-16 \\

说明在当前位置:

增大 \(w\),会让 Loss 下降。

梯度下降:

\ w_{\\text{new}} = w_{\\text{old}} - \\eta \\frac{\\partial L}{\\partial w} \\

假设:

\ \\eta=0.01 \\

那么:

\ w_{\\text{new}} = 1-0.01\\times(-16) \\

所以:

\ w_{\\text{new}}=1.16 \\

参数:

\ 1\\rightarrow1.16 \\

确实增大了。

新的预测:

\ \\hat y=1.16\\times2=2.32 \\

已经比原来的:

\ 2 \\

更接近正确答案:

\ 6 \\

所以 Backward 算出来的 Gradient 确实给出了合理的更新方向。


九、神经网络只是把这条链变长了

现在来看我们真正的 BC 网络:

复制代码
self.fc1 = nn.Linear(10, 64)self.relu = nn.ReLU()self.fc2 = nn.Linear(64, 7)

Forward:

复制代码
x = self.fc1(obs)x = self.relu(x)pred_action = self.fc2(x)

再:

复制代码
loss = loss_fn(pred_action, action)

计算图可以抽象成:

复制代码
Observation
    ↓
 W1,b1
    ↓
Linear 1
    ↓
 h1
    ↓
 ReLU
    ↓
 h2
    ↓
 W2,b2
    ↓
Linear 2
    ↓
Predicted Action
    ↓
   Loss

真正训练时,我们想知道:

\ \\frac{\\partial L}{\\partial W_2} \\

以及:

\ \\frac{\\partial L}{\\partial b_2} \\

甚至前面的:

\ \\frac{\\partial L}{\\partial W_1} \\\ \\frac{\\partial L}{\\partial b_1} \\

所以 PyTorch 从 Loss 开始,沿着这张计算图反向计算。


十、先看最后一层为什么最容易

假设最后一层:

\ \\hat A=H W_2\^T+b_2 \\

然后:

\ L=L(\\hat A,A) \\

那么 \(W_2\) 离 Loss 很近:

复制代码
W2
↓
Predicted Action
↓
Loss

所以:

\ \\frac{\\partial L}{\\partial W_2} \\

只需要经过很短的链。

但:

\ W_1 \\

离 Loss 更远:

复制代码
W1
↓
fc1
↓
ReLU
↓
fc2
↓
Predicted Action
↓
Loss

于是它需要把中间每一段的影响都乘起来。

这就是:

深层网络的 Backpropagation 本质上是很多次 Chain Rule。


十一、ReLU 在反向传播时做什么?

Forward 时:

\ y=\\operatorname{ReLU}(x) \\

定义:

\ y= \\begin{cases} 0,\&x\\leq0\\\\ x,\&x\>0 \\end{cases} \\

那么它的导数可以先理解为:

\ \\frac{dy}{dx} = \\begin{cases} 0,\&x\<0\\\\ 1,\&x\>0 \\end{cases} \\

在 \(x=0\) 处严格来说不可导,深度学习框架会采用一个约定值;当前阶段不用纠结。

所以 Forward 时如果:

\ x=-3 \\

ReLU 输出:

\ 0 \\

Backward 时:

\ \\frac{dy}{dx}=0 \\

意味着:

经过这一位置的梯度会被截断。

如果:

\ x=4 \\

那么:

\ \\frac{dy}{dx}=1 \\

梯度可以继续往前传。

所以 ReLU 不仅影响 Forward,也会影响 Backward。


十二、用一个极小网络把全过程跑完

假设:

\ x=2 \\

第一层:

\ z=wx+b \\

为了简单:

\ w=1,\\qquad b=0 \\

所以:

\ z=2 \\

经过 ReLU:

\ h=\\operatorname{ReLU}(2)=2 \\

第二层先简化成:

\ \\hat y=vh \\

假设:

\ v=2 \\

那么:

\ \\hat y=4 \\

正确答案:

\ y=6 \\

Loss:

\ L=(4-6)\^2=4 \\

Forward 完成。

现在 Backward。


第一步:Loss 对预测的梯度

\ L=(\\hat y-y)\^2 \\

所以:

\ \\frac{\\partial L}{\\partial \\hat y} = 2(\\hat y-y) \\

得到:

\ 2(4-6)=-4 \\


第二步:求 \(v\) 的梯度

因为:

\ \\hat y=vh \\

所以:

\ \\frac{\\partial \\hat y}{\\partial v}=h=2 \\

于是:

\ \\frac{\\partial L}{\\partial v} = \\frac{\\partial L}{\\partial\\hat y} \\frac{\\partial\\hat y}{\\partial v} \\

得到:

\ -4\\times2=-8 \\

因此:

\ \\boxed{ \\frac{\\partial L}{\\partial v}=-8 } \\


第三步:梯度继续传到 \(h\)

因为:

\ \\hat y=vh \\

所以:

\ \\frac{\\partial \\hat y}{\\partial h}=v=2 \\

于是:

\ \\frac{\\partial L}{\\partial h} = -4\\times2=-8 \\

注意:

这一步不是在更新 \(h\)。

而是在问:

Loss 对中间变量 \(h\) 有多敏感?

这个梯度还要继续往前传。


第四步:经过 ReLU

由于 Forward 时:

\ z=2\>0 \\

所以 ReLU 在这个位置的导数:

\ \\frac{\\partial h}{\\partial z}=1 \\

于是:

\ \\frac{\\partial L}{\\partial z} = \\frac{\\partial L}{\\partial h} \\frac{\\partial h}{\\partial z} \\

得到:

\ -8\\times1=-8 \\


第五步:算第一层 \(w\) 的梯度

因为:

\ z=wx+b \\

所以:

\ \\frac{\\partial z}{\\partial w}=x=2 \\

因此:

\ \\frac{\\partial L}{\\partial w} = \\frac{\\partial L}{\\partial z} \\frac{\\partial z}{\\partial w} \\

得到:

\ -8\\times2=-16 \\

所以:

\ \\boxed{ \\frac{\\partial L}{\\partial w}=-16 } \\

整条 Backward 链:

\ L \\rightarrow \\hat y \\rightarrow h \\rightarrow z \\rightarrow w \\


十三、这就是 loss.backward() 自动替你做的事情

刚才我们手算了:

\ \\frac{\\partial L}{\\partial v} \\

和:

\ \\frac{\\partial L}{\\partial w} \\

真实神经网络可能有:

\ 10\^6 \\

甚至更多参数。

人不可能手算。

所以 PyTorch 会在 Forward 时建立计算关系,然后调用:

复制代码
loss.backward()

时自动执行:

\ \\boxed{ \\text{Automatic Differentiation} } \\

也就是自动微分。

最终把每个 Parameter 的梯度放进:

复制代码
param.grad

十四、来看真正的 PyTorch 代码

复制代码
import torchx = torch.tensor([2.0])w = torch.tensor([1.0], requires_grad=True)y = torch.tensor([6.0])pred = w * xloss = (pred - y) ** 2loss.backward()print(w.grad)

理论上:

\ \\frac{\\partial L}{\\partial w}=-16 \\

所以会得到类似:

复制代码
tensor([-16.])

这里最关键的是:

复制代码
requires_grad=True

意思是:

请 PyTorch 跟踪这个 Tensor 参与的计算,因为后面我要对它求梯度。


十五、那为什么平时定义 nn.Linear 不需要自己写 requires_grad=True?

因为:

复制代码
nn.Linear(10, 64)

内部的:

复制代码
weight
bias

本身就是被 PyTorch 注册为可训练 Parameter 的对象。

它们默认就是训练需要跟踪梯度的参数。

所以:

复制代码
for name, param in model.named_parameters():    print(name, param.requires_grad)

通常会看到:

复制代码
fc1.weight True
fc1.bias   True
fc2.weight True
fc2.bias   True

因此我们不用手工给每个 Weight 设置:

复制代码
requires_grad=True

十六、Parameter 和普通 Tensor 有什么区别?

这是这里自然会遇到的新概念。

普通 Tensor:

复制代码
x = torch.tensor(...)

只是数据。

而:

复制代码
nn.Parameter

可以理解成:

被 nn.Module 注册并管理的可训练 Tensor。

例如 Linear 里面的:

复制代码
weight

实际上属于 Parameter。

所以模型:

复制代码
model.parameters()

能够找到它。

Optimizer:

复制代码
Adam(model.parameters())

也能够更新它。

这也是为什么我们前面强调:

复制代码
self.fc1 = nn.Linear(...)

不是简单保存一个普通属性那么简单。

PyTorch 会把其中的 Module 和 Parameter 纳入模型管理体系。


十七、为什么 Observation 不需要 Gradient?

训练的时候:

复制代码
Observation
↓
Model
↓
Prediction
↓
Loss

我们的目标是:

修改模型参数。

并不是修改 Dataset 中的 Observation。

所以我们需要:

\ \\frac{\\partial L}{\\partial W} \\

但通常不关心:

\ \\frac{\\partial L}{\\partial Observation} \\

因此普通训练数据一般不需要:

复制代码
requires_grad=True

我们主要让模型 Parameter 保留梯度。


十八、为什么 optimizer.zero_grad() 现在更容易理解了?

假设第一次:

复制代码
loss.backward()

算出:

\ W.grad=G_1 \\

下一批数据又调用:

复制代码
loss.backward()

PyTorch 默认会做梯度累积:

\ W.grad=G_1+G_2 \\

如果普通训练希望只使用当前 Batch 的:

\ G_2 \\

那么在第二次 Backward 前就要:

复制代码
optimizer.zero_grad()

变成:

\ W.grad=0 \\

然后:

\ loss.backward() \\

得到:

\ W.grad=G_2 \\

所以现在这句不应该再靠死记。


十九、为什么计算图通常是"动态"的?

PyTorch 常被称为动态图框架。

先用直觉理解:

你每次运行:

复制代码
pred = model(obs)

PyTorch 根据这一次真正执行过的运算建立计算关系。

比如代码中出现:

复制代码
if condition:    x = self.fc1(x)else:    x = self.fc2(x)

这次程序到底执行哪个分支,就会形成对应的计算图。

所以可以理解成:

Forward 的同时,PyTorch 记录这一次到底经历了哪些运算。

然后:

复制代码
loss.backward()

再沿本次计算路径反向求梯度。

这个特点以后读复杂模型很重要,但当前记住概念即可。


二十、为什么 loss.backward() 通常从一个标量 Loss 开始?

假设:

\ loss=0.031 \\

只有一个数字。

那么问题非常清楚:

每一个 Parameter 对这个最终单一目标有多大影响?

也就是:

\ \\frac{\\partial L}{\\partial\\theta_i} \\

所以训练通常会把整个 Batch 中大量误差先 reduction 成一个 scalar Loss。

然后从这个 scalar 开始反向传播。

这就是前面我们说:

\ Loss.shape=\[ \]

非常常见的原因之一。


二十一、放回 Behavior Cloning

现在重新看:

\ \\hat A=\\pi_\\theta(O) \\

然后:

\ L=L(\\hat A,A) \\

Forward:

复制代码
Observation
↓
Policy Parameters θ
↓
Predicted Action
↓
Loss

Backward:

复制代码
Loss
↓
Action Head
↓
Hidden Layers
↓
Earlier Layers
↓
Every trainable Parameter

得到:

\ \\nabla_\\theta L \\

Optimizer 再:

\ \\theta \\leftarrow \\theta-\\eta\\nabla_\\theta L \\

于是下一次:

\ \\pi_\\theta \\

产生的 Action 就会略有变化。

这就是:

Demonstration 为什么最终能够改变 Policy 行为。


二十二、再往后看 VLA,你会发现本质没有变

以后网络可能变成:

复制代码
Image
↓
Vision Encoder
      \
       Fusion
      /
Language Encoder

Robot State Encoder
       ↓
Transformer
       ↓
Action Head
       ↓
Action
       ↓
Loss

Backward 会从:

\ Loss \\

沿着所有参与计算并且允许训练的 Parameter 往回传播。

如果某个部分被冻结:

复制代码
Vision Encoder
Frozen

那么它的参数就不会按照普通训练方式被更新。

这就是以后你看到:

复制代码
freeze vision encoder

真正应该想到的事情:

Forward 仍然可能经过它,但是这些参数不参与正常的训练更新。

这个概念以后讲 VLA 冻结/微调时会再展开。


第七课整章回看

现在 loss.backward() 已经不应该再是魔法。

完整逻辑是:

\ \\boxed{ Forward \\rightarrow 建立计算关系 \\rightarrow 得到 Loss \\rightarrow Backward \\rightarrow 沿计算图反向使用 Chain Rule \\rightarrow 计算每个 Parameter 的 Gradient } \\

例如:

\ \\frac{\\partial L}{\\partial W_1}, \\quad \\frac{\\partial L}{\\partial b_1}, \\quad \\frac{\\partial L}{\\partial W_2}, \\quad \\frac{\\partial L}{\\partial b_2} \\

然后:

复制代码
optimizer.step()

才真正更新:

\ W_1,b_1,W_2,b_2 \\

于是:

\ \\boxed{ Backward\\neq Update } \\

而是:

\ \\boxed{ Backward=Calculate\\ Gradients } \\


第七课自测

  1. 什么叫计算图?为什么 Forward 时需要保留运算之间的关系?

  2. 如果:

\ z=wx \\\ L=z\^2 \\

为什么:

\ \\frac{\\partial L}{\\partial w} = \\frac{\\partial L}{\\partial z} \\frac{\\partial z}{\\partial w} \\

?

  1. 假设:

\ x=2,\\quad w=3 \\\ z=wx,\\quad L=z\^2 \\

求:

\ \\frac{\\partial L}{\\partial w} \\

  1. ReLU 在:

\ x\>0 \\

和:

\ x\<0 \\

区域的梯度分别是什么?

  1. loss.backward() 做完以后,是 weight 发生改变,还是 weight.grad 得到结果?

  2. nn.Linear 中的 Weight 为什么通常不需要手工设置 requires_grad=True?

  3. nn.Parameter 和普通 Tensor 最核心的区别是什么?

  4. 为什么机器人 Observation 一般不需要梯度,而模型 Parameter 需要?

  5. 请完整说出:

    optimizer.zero_grad()pred = model(obs)loss = loss_fn(pred, action)loss.backward()optimizer.step()

这五行之间的因果关系。

相关推荐
serdes211 小时前
56G PAM4 SerDes RX MMCDR Mueller–Müller 误差检测与数据对齐设计
人工智能
倔强的石头1061 小时前
DeepSeek系列_国产大模型的技术创新解析
人工智能·大模型
企业数字化笔记1 小时前
AI工具的文件和参数怎么设计?上传校验、配置版本与可复现任务
前端·人工智能
seconp1 小时前
AI 时代怎么做计算机毕业设计?
人工智能·毕业设计·软件工程·课程设计·毕设
外收内放1 小时前
Python基础语法练习题(53-54)
python·学习
loulanyue_1 小时前
智能成为“商品”之后:读吴泳铭 2026 云栖演讲的六个取舍
人工智能
鬼手点金1 小时前
opencode-性能优化建议
java·人工智能·git·自动化·nanogpt
聪明蛋子哟1 小时前
Python与Java双栈实战:手撸一个支持RAG与Tool Calling的高性能Agent框架
java·开发语言·python