第六课标准答案
先把上一课的问题收掉,然后直接进入第七课。今天的主线会非常明确:
\ \\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 } \\
第七课自测
-
什么叫计算图?为什么 Forward 时需要保留运算之间的关系?
-
如果:
\ z=wx \\\ L=z\^2 \\
为什么:
\ \\frac{\\partial L}{\\partial w} = \\frac{\\partial L}{\\partial z} \\frac{\\partial z}{\\partial w} \\
?
- 假设:
\ x=2,\\quad w=3 \\\ z=wx,\\quad L=z\^2 \\
求:
\ \\frac{\\partial L}{\\partial w} \\
- ReLU 在:
\ x\>0 \\
和:
\ x\<0 \\
区域的梯度分别是什么?
-
loss.backward()做完以后,是weight发生改变,还是weight.grad得到结果? -
nn.Linear中的 Weight 为什么通常不需要手工设置requires_grad=True? -
nn.Parameter和普通 Tensor 最核心的区别是什么? -
为什么机器人 Observation 一般不需要梯度,而模型 Parameter 需要?
-
请完整说出:
optimizer.zero_grad()pred = model(obs)loss = loss_fn(pred, action)loss.backward()optimizer.step()
这五行之间的因果关系。