第七课标准答案
先把上一课的 9 个问题完整收掉,然后直接进入第 8 课。今天的主线会继续沿着同一条训练链往后走:
\[ \text{Gradient} \rightarrow \text{Learning Rate} \rightarrow \text{Optimizer} \rightarrow \text{Parameter Update} \]
1. 什么叫计算图?为什么 Forward 时要保留运算关系?
计算图可以理解成:
记录“一个结果是由哪些变量经过哪些运算得到的”这条关系链。
例如:
\[ z=wx \]\[ L=z^2 \]
可以画成:
w ──┐ × → z → square → L x ──┘Forward 时当然要先算:
\[ w,x\rightarrow z\rightarrow L \]
但训练还需要知道:
最终的 \(L\) 到底和前面的 \(w\) 有什么关系?
只有保留这条关系,Backward 才能从:
\[ L \]
一路反向找到:
\[ w \]
并计算:
\[ \frac{\partial L}{\partial w} \]
所以计算图不是为了“画图好看”,而是为了让自动微分知道:
梯度应该沿哪条计算路径往回传。
2. 为什么
\[ \frac{\partial L}{\partial w} = \frac{\partial L}{\partial z} \frac{\partial z}{\partial w} \]
?
因为 \(w\) 没有直接决定 \(L\)。
它先影响:
\[ z \]
而 \(z\) 再影响:
\[ L \]
也就是:
\[ w\rightarrow z\rightarrow L \]
所以 \(w\) 对 \(L\) 的影响,需要把中间两段影响连起来:
\[ w\rightarrow z \]
这一段:
\[ \frac{\partial z}{\partial w} \]
以及:
\[ z\rightarrow L \]
这一段:
\[ \frac{\partial L}{\partial z} \]
最终:
\[ \frac{\partial L}{\partial w} = \frac{\partial L}{\partial z} \frac{\partial z}{\partial w} \]
这就是 Chain Rule,链式法则。
3. 已知
\[ x=2,\qquad w=3 \]\[ z=wx,\qquad L=z^2 \]
求:
\[ \frac{\partial L}{\partial w} \]
先 Forward:
\[ z=3\times2=6 \]
然后:
\[ L=6^2=36 \]
现在反向。
因为:
\[ L=z^2 \]
所以:
\[ \frac{\partial L}{\partial z}=2z=12 \]
又因为:
\[ z=wx \]
所以:
\[ \frac{\partial z}{\partial w}=x=2 \]
于是:
\[ \frac{\partial L}{\partial w} = 12\times2 \]
得到:
\[ \boxed{24} \]
4. ReLU 在正数和负数区域的梯度分别是什么?
ReLU:
\[ ReLU(x)=\max(0,x) \]
所以:
\[ x>0 \]
时:
\[ ReLU(x)=x \]
导数:
\[ \frac{dReLU}{dx}=1 \]
而:
\[ x<0 \]
时:
\[ ReLU(x)=0 \]
导数:
\[ \frac{dReLU}{dx}=0 \]
所以:
\[ \boxed{ ReLU'(x)= \begin{cases} 0,&x<0\\ 1,&x>0 \end{cases} } \]
这意味着如果 Forward 时某个值小于 0,那么这一位置在 Backward 时梯度会被截断为 0。
5.loss.backward()后,是weight变了还是weight.grad有了结果?
是:
\[ \boxed{weight.grad} \]
有了结果。
例如:
loss.backward()以后:
layer.weight参数本身还没有因为这句话而更新。
但:
layer.weight.grad里面已经存着:
\[ \frac{\partial L}{\partial W} \]
真正修改 Weight 的是:
optimizer.step()所以必须记住:
\[ \boxed{ backward = 算梯度 } \]\[ \boxed{ step = 改参数 } \]
6. 为什么nn.Linear的 Weight 不需要手动设置requires_grad=True?
因为:
nn.Linear(...)内部的:
weight bias不是普通 Tensor,而是被 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 True7.nn.Parameter和普通 Tensor 最核心的区别是什么?
普通 Tensor:
x = torch.tensor(...)本质上只是数据。
而nn.Parameter可以理解成:
被
nn.Module识别、注册和管理的可训练 Tensor。
所以它可以被:
model.parameters()找到。
Optimizer 才能进一步拿到它:
optimizer = Adam(model.parameters())然后训练时更新它。
8. 为什么 Observation 一般不需要梯度,而模型 Parameter 需要?
因为我们训练的目标不是修改 Observation。
例如输入:
\[ o_t \]
它只是数据。
我们真正想改变的是:
\[ \theta \]
也就是:
\[ W,b,\ldots \]
所以训练关心:
\[ \frac{\partial L}{\partial W} \]\[ \frac{\partial L}{\partial b} \]
而通常不需要:
\[ \frac{\partial L}{\partial o_t} \]
因此 Robot Observation 通常作为普通 Tensor 使用,而 Parameter 要保存梯度。
9. 五行训练代码完整因果关系是什么?
optimizer.zero_grad()pred = model(obs)loss = loss_fn(pred, action)loss.backward()optimizer.step()完整关系是:
清空旧梯度 ↓ 当前参数做 Forward ↓ 得到预测 Action ↓ 和 Expert Action 比较 ↓ 得到 Loss ↓ Backward ↓ 沿计算图计算每个 Parameter 的 Gradient ↓ Optimizer读取 Gradient ↓ 修改 Parameter数学上就是:
\[ O \rightarrow \pi_\theta \rightarrow \hat A \]
然后:
\[ L(\hat A,A) \]
接着:
\[ \nabla_\theta L \]
最后:
\[ \theta_{\text{new}} = \theta_{\text{old}} +\text{某种更新量} \]
到底这个“更新量”怎么算,就是下一课。
VLA 系统学习第 8 课:Optimizer 到底在干什么?——从 Learning Rate 到 SGD、Momentum 和 Adam
上一课我们已经知道:
\[ loss.backward() \]
会得到:
\[ \nabla_\theta L \]
也就是每个参数对应的梯度。
但梯度只回答:
往哪个方向改,Loss 会变大或变小?
它还没有完整回答:
每次到底应该改多少?
这就是 Optimizer 要解决的问题。
最基础的更新公式:
\[ \theta_{\text{new}} = \theta_{\text{old}} - \eta\nabla_\theta L \]
这里新出现的:
\[ \eta \]
就是:
Learning Rate
学习率。
一、先只看一个参数,理解 Learning Rate
假设模型只有一个参数:
\[ w \]
当前:
\[ w=1 \]
Backward 算出:
\[ \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 \]
所以参数往正确方向增加了一点。
这里:
\[ \eta \]
控制的就是:
这一步迈多大。
二、Learning Rate 为什么不能随便设?
这可以用“下山”来理解。
假设 Loss 是山的高度。
我们的目标:
\[ \min_\theta L(\theta) \]
就是:
从山上走到最低处。
Gradient 告诉你:
哪边是上坡最快的方向。
因此负 Gradient:
\[ -\nabla L \]
告诉你:
哪边大致是下坡方向。
但是知道方向之后,还需要决定:
一步迈多大?
这就是 Learning Rate。
三、Learning Rate 太小会发生什么?
假设正确方向已经知道,但:
\[ \eta=0.0000001 \]
那么每一步:
\[ \Delta w = -\eta\frac{\partial L}{\partial w} \]
都非常小。
模型虽然在学习,但可能像:
1.000000 → 1.000002 → 1.000004 → 1.000006 → ...训练会非常慢。
所以可能出现:
Loss 在下降 但是下降得极其缓慢这通常意味着:
Learning Rate 可能过小,或者还有其他优化问题。
四、Learning Rate 太大又会怎样?
假设最低点在:
\[ w=3 \]
当前:
\[ w=1 \]
本来应该慢慢:
1 → 1.4 → 1.8 → 2.2 → 2.6 → 2.9 → 3但如果步子特别大:
1 → 5 → 0 → 7 → -2 → ...会不断跨过最低点。
于是 Loss 可能:
下降 ↓ 突然上升 ↓ 再下降 ↓ 剧烈振荡更严重时数值直接爆掉,可能看到:
loss = nan所以:
\[ \boxed{ Learning\ Rate太小 \rightarrow 学得太慢 } \]\[ \boxed{ Learning\ Rate太大 \rightarrow 震荡甚至发散 } \]
五、最基础的 Optimizer:Gradient Descent
最简单更新规则就是:
\[ \theta_{t+1} = \theta_t - \eta\nabla_\theta L \]
意思是:
- 算当前 Gradient;
- 乘 Learning Rate;
- 沿负梯度方向更新。
但真实训练通常不是用整个 Dataset 一次性算梯度,而是使用一个 Batch。
因此我们经常说:
Stochastic Gradient Descent
也就是:
SGD
PyTorch:
optimizer = torch.optim.SGD( model.parameters(), lr=0.01)这里:
model.parameters()告诉 Optimizer:
更新哪些参数。
而:
lr=0.01告诉它:
每次更新的基础步长有多大。
六、为什么叫 Stochastic?
理想情况下,可以用整个 Dataset 计算真正的平均梯度:
\[ \nabla_\theta L_{\text{all data}} \]
但假设 Dataset 有:
\[ 10^7 \]
个样本。
每更新一次参数都把全部数据算一遍,成本太高。
所以实际中通常每次只拿一个 Batch:
\[ B=32 \]
或者:
\[ B=64 \]
根据当前 Batch 估计:
\[ \nabla_\theta L \]
因此这个梯度带有一定随机性和噪声。
这就是 Stochastic 的来源。
实际上深度学习中常见的所谓 SGD,很多时候更准确地说是:
Mini-batch SGD
七、Batch 不同,Gradient 为什么也不同?
假设 Dataset 中有:
Batch 1: 一组比较简单的数据 Batch 2: 一组偏左的数据 Batch 3: 一组偏右的数据每个 Batch 对 Loss 的贡献不同。
所以算出来:
\[ \nabla_\theta L_1 \]\[ \nabla_\theta L_2 \]\[ \nabla_\theta L_3 \]
不一定完全相同。
于是参数更新可能:
这一批稍微往左改 ↓ 下一批稍微往右改 ↓ 再下一批又往另一个方向改这也是训练曲线不会像一条完美平滑直线下降的原因之一。
八、为什么纯 SGD 有时会抖?
想象一个狭长山谷。
最低点在谷底。
但是梯度每次都有一点不同。
可能出现:
左 → 右 → 左 → 右 → 左虽然整体正在向谷底走,但横向一直来回震荡。
这时可以引入:
Momentum
动量。
九、Momentum 的直觉是什么?
想象一个球从山坡上滚下来。
它不会每一步都完全只看当前坡度。
它还有:
之前运动留下来的惯性。
如果连续几步都在向同一个方向走,那么这个方向的速度会逐渐积累。
这就是 Momentum 的直觉。
十、Momentum 的基本数学思想
最基础的一种写法:
\[ v_t = \beta v_{t-1} + \nabla_\theta L_t \]
然后:
\[ \theta_{t+1} = \theta_t - \eta v_t \]
这里:
\[ v_t \]
可以理解为:
累积的更新趋势。
\[ \beta \]
控制:
之前的方向保留多少。
例如:
\[ \beta=0.9 \]
意味着过去的运动趋势会被较强保留。
所以如果过去连续很多次都:
往右是正确方向,
那么即使这一批数据梯度稍微抖一下,整体也不会马上掉头。
十一、Momentum 为什么能减少震荡?
假设横向梯度:
+1 -1 +1 -1不断来回变。
长期来看互相抵消。
而真正朝谷底的方向一直是:
-2 -2 -2 -2那么 Momentum 会逐渐积累稳定方向。
结果就是:
横向抖动减弱 ↓ 主方向速度增强 ↓ 更快走向低 Loss 区域所以 Momentum 主要解决:
SGD 更新方向容易受当前 Batch 噪声影响的问题。
十二、PyTorch 里的 SGD + Momentum
可以写:
optimizer = torch.optim.SGD( model.parameters(), lr=0.01, momentum=0.9)现在 Optimizer 不再只看:
\[ 当前Gradient \]
而是同时利用:
\[ 当前Gradient + 过去更新趋势 \]
十三、那为什么现代深度学习经常用 Adam?
接下来是你以后会极其频繁看到的:
torch.optim.Adam(...)甚至:
torch.optim.AdamW(...)先不讲 AdamW,先把 Adam 的核心直觉搞清楚。
SGD 有一个比较简单的问题:
所有参数基本共享同一个全局 Learning Rate。
但是神经网络不同参数的梯度尺度可能差别非常大。
例如:
\[ w_1.grad=0.0001 \]
而:
\[ w_2.grad=100 \]
如果都乘:
\[ \eta=0.01 \]
那么两个参数的实际更新幅度差异会非常巨大。
Adam 想做的事情之一就是:
根据每个参数自身过去的梯度情况,自适应地调整它的有效更新尺度。
十四、Adam 同时记住两件事
Adam 会维护两类统计量。
第一类:
梯度最近大概往哪个方向走。
可以写成一阶矩:
\[ m_t \]
第二类:
梯度最近大概有多大。
可以写成二阶矩:
\[ v_t \]
先不要被“矩”这个词吓到。
现在只需要理解:
\[ m_t \]
大致像:
带 Momentum 的平均方向。
而:
\[ v_t \]
大致反映:
梯度大小的历史信息。
十五、Adam 的一阶矩
核心思想类似:
\[ m_t = \beta_1m_{t-1} + (1-\beta_1)g_t \]
这里:
\[ g_t=\nabla_\theta L_t \]
也就是当前 Gradient。
如果:
\[ \beta_1=0.9 \]
那么:
\[ m_t \]
会保留过去的方向趋势。
所以这一部分和 Momentum 很像。
十六、Adam 的二阶矩
Adam 还会记录:
\[ v_t = \beta_2v_{t-1} + (1-\beta_2)g_t^2 \]
注意:
\[ g_t^2 \]
表示梯度平方。
因此 \(v_t\) 大致反映:
最近这个参数的梯度通常有多大。
如果某个参数梯度长期特别大,Adam 会相应缩小它的有效步长。
如果某个参数梯度长期比较小,Adam 可以相对给它更合适的更新尺度。
十七、Adam 最终怎么更新?
先忽略 bias correction 等细节,直觉上可以看成:
\[ \theta_{t+1} = \theta_t - \eta \frac{m_t}{\sqrt{v_t}+\epsilon} \]
这里:
\[ m_t \]
提供:
稳定的更新方向。
\[ \sqrt{v_t} \]
提供:
对梯度尺度的调节。
\[ \epsilon \]
是一个很小的数,用于数值稳定,避免除零等问题。
所以 Adam 的核心可以先记成:
\[ \boxed{ Momentum思想 + 每个参数自己的自适应步长 } \]
十八、为什么 Adam 经常比最基础 SGD 更容易直接用?
因为它对不同 Parameter 的梯度尺度进行了自适应调整。
现实网络里:
Vision Encoder Transformer MLP Action Head不同层的梯度尺度可能很不一样。
Adam 往往能让训练初期更容易稳定起来。
所以你以后会大量看到:
optimizer = torch.optim.Adam( model.parameters(), lr=1e-4)或者:
optimizer = torch.optim.AdamW(...)尤其在 Transformer、VLM、VLA 中非常常见。
十九、Adam 是不是就不需要 Learning Rate 了?
不是。
这是非常容易误解的地方。
即使 Adam 会自适应不同参数的更新尺度,仍然需要:
\[ \eta \]
也就是基础 Learning Rate。
例如:
optimizer = torch.optim.Adam( model.parameters(), lr=1e-4)这里:
\[ 10^{-4} \]
仍然非常重要。
Adam 不是:
自动把 Learning Rate 完全解决了。
而是:
在给定基础 Learning Rate 的情况下,对每个参数的更新进一步进行自适应调整。
二十、为什么不同 Optimizer 的step()行为不同?
现在你终于可以真正理解:
optimizer.step()并不是固定的一种数学操作。
如果 Optimizer 是 SGD:
torch.optim.SGD(...)它按照 SGD 的规则更新。
如果是 SGD + Momentum:
torch.optim.SGD(..., momentum=0.9)它会利用历史速度。
如果是 Adam:
torch.optim.Adam(...)它会利用一阶矩和二阶矩。
所以:
optimizer.step()只是一套统一接口。
真正采用什么更新公式,取决于:
optimizer这个对象具体是哪一种 Optimizer。
这和前面学习的面向对象思路又连起来了。
二十一、把代码真正看懂
现在:
optimizer = torch.optim.Adam( model.parameters(), lr=1e-3)可以完整翻译成:
创建一个 Adam Optimizer 对象,让它管理
model.parameters()中所有可训练参数,并使用基础学习率 \(10^{-3}\)。
训练:
optimizer.zero_grad()pred = model(obs)loss = loss_fn(pred, action)loss.backward()optimizer.step()现在最后一句真正意味着:
Adam 读取每个 Parameter 当前的
.grad,结合自己保存的历史梯度统计,根据 Adam 更新规则修改模型参数。
二十二、Optimizer 自己也有“状态”
这是一个很重要但很自然的新认识。
模型有参数:
W b而 Adam 自己也会保存:
m v step count ...这些东西不是模型本身的预测参数,但训练恢复时非常重要。
因此 Checkpoint 往往不仅保存:
model.state_dict()还会保存:
optimizer.state_dict()为什么?
因为如果你训练到第:
\[ 50000 \]
步突然停掉,
只加载 Model Parameter,Adam 过去积累的:
\[ m_t,\quad v_t \]
就全部丢了。
恢复训练可能和真正连续训练不完全一样。
所以以后看到:
checkpoint = { "model": model.state_dict(), "optimizer": optimizer.state_dict()}你应该知道:
一个保存模型本身,一个保存 Optimizer 的训练状态。
二十三、为什么训练 Loss 会抖动而不是一直下降?
现在我们已经能解释很多真实训练现象。
每个 Batch 不一样,所以:
\[ g_t \]
不一样。
参数更新:
\[ \theta_t\rightarrow\theta_{t+1} \]
之后,下一个 Batch 又可能比较难。
因此 Loss 可能:
0.80 ↓ 0.61 ↓ 0.65 ↓ 0.52 ↓ 0.55 ↓ 0.41虽然不是每一步都下降,但整体趋势下降。
这通常并不奇怪。
特别是 Mini-batch Training 中,Gradient 本身就是带噪声的估计。
所以判断训练是否正常不能只看:
某一步 Loss 有没有变大。
要看一段训练过程中的总体趋势。
二十四、Learning Rate 太大时你可能实际看到什么?
可能看到:
Loss突然剧烈震荡或者:
Loss持续增大甚至:
nan inf原因之一就是参数一步更新太大:
\[ \Delta\theta \]
过大。
于是:
当前参数还算合理 ↓ 一次step ↓ 参数跑到很差的区域 ↓ 输出变得极端 ↓ Loss变大 ↓ 梯度进一步异常最后训练数值崩溃。
当然nan不一定只由 Learning Rate 导致,但它是常见排查方向之一。
二十五、Learning Rate 太小时你可能看到什么?
例如:
Epoch 1 Loss = 0.800 Epoch 2 Loss = 0.799 Epoch 3 Loss = 0.798 Epoch 4 Loss = 0.797整个训练非常慢。
原因可能是:
\[ \Delta\theta = -\eta g \]
中:
\[ \eta \]
太小。
即使 Gradient 方向正确,参数每次也只动一点点。
二十六、Learning Rate 和 Batch Size 有没有关系?
有,但现在先建立直觉,不急着上复杂经验公式。
Batch Size 越小:
Gradient 通常噪声更大。
Batch Size 越大:
Gradient 通常更稳定,更接近整个 Dataset 的平均趋势。
例如:
\[ B=1 \]
每次只看一个样本。
这个样本如果很特殊,Gradient 就可能偏得比较厉害。
而:
\[ B=256 \]
把很多样本平均起来,梯度往往更稳定。
但 Batch 越大:
- 显存需求越高;
- 每个 Step 成本更高;
- 优化性质也会变化。
后面讲完整训练配置时再系统分析。
二十七、现在可以第一次理解 Training Configuration
以后看到训练配置:
batch_size: 64 learning_rate: 1e-4 optimizer: adam epochs: 100它们不是四个互不相关的参数。
而是在描述同一件事:
一次拿多少数据? → batch_size 算出梯度后基础步子多大? → learning_rate 用什么规则更新参数? → optimizer 整个Dataset反复学习多少轮? → epochs这就是训练系统。
二十八、把前几课全部接起来
现在我们已经从 Robot Dataset 一直走到真正参数更新。
完整链:
\[ \boxed{ Demonstration \rightarrow Dataset \rightarrow Batch \rightarrow Forward \rightarrow Predicted\ Action \rightarrow Loss \rightarrow Backward \rightarrow Gradient \rightarrow Optimizer \rightarrow Parameter\ Update } \]
Optimizer 这一段又可以展开:
\[ Gradient \rightarrow Learning\ Rate \rightarrow Update\ Rule \rightarrow \theta_{\text{new}} \]
如果是 SGD:
\[ \theta_{t+1} = \theta_t-\eta g_t \]
如果加入 Momentum:
\[ \text{Current Gradient} + \text{Previous Direction} \]
如果是 Adam:
\[ \text{Direction History} + \text{Gradient Scale History} + \text{Base Learning Rate} \]
共同决定下一步更新。
第八课自测
Gradient 和 Learning Rate 分别回答什么问题?
如果:
\[ w=2 \]
梯度:
\[ \frac{\partial L}{\partial w}=5 \]
学习率:
\[ \eta=0.1 \]
按照最基础 Gradient Descent,新的 \(w\) 是多少?
为什么 Learning Rate 太大可能导致 Loss 震荡甚至发散?
为什么 Learning Rate 太小会导致训练非常慢?
Mini-batch SGD 中为什么每一个 Batch 算出来的 Gradient 不一定相同?
Momentum 相比最基础 SGD 多使用了什么信息?
Adam 中:
\[ m_t \]
和:
\[ v_t \]
可以分别先怎样理解?
Adam 会自动消除 Learning Rate 这个超参数吗?为什么?
为什么恢复 Adam 训练时,除了
model.state_dict(),还经常需要恢复optimizer.state_dict()?现在解释:
optimizer.step()时,为什么不能简单说成“执行梯度下降”?
因为不同 Optimizer 的具体更新规则并不相同。