第七课标准答案

先把上一课的 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 \]

意思是:

  1. 算当前 Gradient;
  2. 乘 Learning Rate;
  3. 沿负梯度方向更新。

但真实训练通常不是用整个 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} \]

共同决定下一步更新。


第八课自测

  1. Gradient 和 Learning Rate 分别回答什么问题?

  2. 如果:

\[ w=2 \]

梯度:

\[ \frac{\partial L}{\partial w}=5 \]

学习率:

\[ \eta=0.1 \]

按照最基础 Gradient Descent,新的 \(w\) 是多少?

  1. 为什么 Learning Rate 太大可能导致 Loss 震荡甚至发散?

  2. 为什么 Learning Rate 太小会导致训练非常慢?

  3. Mini-batch SGD 中为什么每一个 Batch 算出来的 Gradient 不一定相同?

  4. Momentum 相比最基础 SGD 多使用了什么信息?

  5. Adam 中:

\[ m_t \]

和:

\[ v_t \]

可以分别先怎样理解?

  1. Adam 会自动消除 Learning Rate 这个超参数吗?为什么?

  2. 为什么恢复 Adam 训练时,除了 model.state_dict(),还经常需要恢复 optimizer.state_dict()?

  3. 现在解释:

optimizer.step()

时,为什么不能简单说成“执行梯度下降”?

因为不同 Optimizer 的具体更新规则并不相同。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐