VLA 系统学习第 8 课:Optimizer 到底在干什么?——从 Learning Rate 到 SGD、Momentum 和 Adam
第七课标准答案
先把上一课的 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 True
7. 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 的具体更新规则并不相同。
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐


所有评论(0)