第三课标准答案:Robot Dataset
1. 为什么 100 个 Episode 不代表只有 100 个训练样本?
因为:
Episode 是一次完整任务,而训练样本可以来自 Episode 内部的每个时间点。
例如:
\ 100\\text{ episodes} \\
每个 Episode 有:
\ 100\\text{ frames} \\
那么最简单情况下就可能有约:
\ 100\\times100=10000 \\
个:
\ (o_t,a_t) \\
训练对。
所以:
\ \\boxed{ Episode数量\\neq Sample数量 } \\
2. dataset[10] 和 dataloader 分别负责什么?
dataset[10]:
从 Dataset 中获取第 10 个 Sample。
它通常触发:
__getitem__(10)
例如得到:
{ "image": image, "state": state, "action": action}
而 DataLoader:
从 Dataset 中不断获取多个 Sample,并把它们组织成 Batch。
所以:
Dataset
→ 一个Sample怎么取
DataLoader
→ 多个Sample怎么组织、批量送入训练
3. 为什么
\ \[3,224,224 \]
会变成:
\ \[32,3,224,224 \]
?
因为:
\ \[3,224,224 \]
是一张 RGB 图片。
如果 DataLoader 一次取 32 个样本:
image1 [3,224,224]
image2 [3,224,224]
...
image32 [3,224,224]
把它们堆起来:
\ \[32,3,224,224 \]
最前面的:
\ 32 \\
就是 Batch Dimension。
4. 为什么 Training 有 Expert Action,但 Rollout 没有?
Training 使用的是提前收集好的专家 Dataset。
所以数据本身包含:
\ (o_t,a_t) \\
模型可以拿:
\ a_t \\
作为正确答案。
但 Rollout 时面对的是实时环境:
Environment
↓
Observation
↓
Policy
没有专家在旁边实时告诉模型:
这一刻正确动作应该是什么。
所以模型必须自己产生:
\ \\hat a_t \\
并真正执行它。
5. 为什么训练和推理的 normalization 要一致?
假设训练时:
\ x'= \\frac{x-\\mu}{\\sigma} \\
模型从来没有直接看到原始 \(x\)。
它看到的都是:
\ x' \\
如果推理时突然把原始数据:
\ x \\
直接送进去,那么模型面对的数据尺度发生变化。
例如训练阶段它习惯:
\ \[-1,1 \]
推理阶段却突然输入:
\ \[-180,180 \]
模型当然可能表现很差。
所以:
\ \\boxed{ \\text{训练时怎么处理输入} \\approx \\text{推理时怎么处理输入} } \\
6. 从机器人示范到 model(obs) 的完整过程
应该能够说出:
人类操作机器人
↓
记录很多Episode
↓
每个时间点记录Observation和Expert Action
↓
保存到磁盘
↓
Dataset读取数据
↓
__getitem__返回一个Sample
↓
DataLoader收集多个Sample
↓
组成Batch
↓
取出Observation
↓
送入model
所以:
pred_action = model(obs)
里的 obs
其实已经经过了很长的数据准备链。
四、VLA 系统学习第 4 课:一个 Batch 进入神经网络后,模型到底是怎么"学会"的?
现在我们已经成功走到了:
for batch in dataloader: obs = batch["observation"] action = batch["action"]
假设:
\ obs.shape=\[32,10 \]\ action.shape=\[32,7 \]
现在出现真正关键的问题:
这 32 个训练样本进入神经网络以后,模型为什么会发生变化?
也就是:
\ \\boxed{ Batch \\rightarrow Forward \\rightarrow Prediction \\rightarrow Loss \\rightarrow Backward \\rightarrow Optimizer \\rightarrow Parameters\\ Updated } \\
这条链,是 PyTorch 训练最核心的一条链。
1. 首先搞清楚:模型真正"学"的是什么?
看一个最简单模型:
model = nn.Linear(10, 7)
它表示:
\ \\mathbb R\^{10} \\rightarrow \\mathbb R\^7 \\
也就是:
10维Observation
↓
Linear
↓
7维Action
数学上:
\ \\hat a=xW+b \\
这里:
- \(x\):Observation;
- \(W\):权重;
- \(b\):偏置;
- \(\hat a\):模型预测的 Action。
真正训练过程中被修改的,不是:
\ x \\
也不是:
\ a \\
而是:
\ \\boxed{W,b} \\
这些统称:
\ \\theta \\
所以:
\ \\pi_\\theta \\
中的:
\ \\theta \\
就是模型里所有可以学习的参数。
2. 先看一个只有一个数字的模型
先不直接上矩阵。
假设模型特别简单:
\ \\hat y=wx \\
只有一个参数:
\ w \\
现在训练数据告诉我们:
\ x=2 \\
正确答案:
\ y=6 \\
假设初始:
\ w=1 \\
那么模型第一次预测:
\ \\hat y=wx \\
得到:
\ \\hat y=1\\times2=2 \\
但是正确答案是:
\ 6 \\
明显错了。
于是我们需要回答:
应该怎么修改 \(w\),才能让下一次预测更接近 6?
这就是训练问题的核心。
3. Forward:先让当前模型做一次预测
模型目前:
\ w=1 \\
输入:
\ x=2 \\
运行:
\ \\hat y=wx \\
得到:
\ \\hat y=2 \\
这个过程叫:
Forward
也就是:
从输入开始,按照神经网络当前参数一路向前计算,最终得到预测结果。
在 PyTorch:
pred = model(x)
就是在进行 Forward。
放回 BC:
\ \\hat A=\\pi_\\theta(O) \\
代码:
pred_action = model(obs)
就是:
用当前 Policy 对这一批 Observation 预测 Action。
4. Forward 以后为什么还不能训练?
因为现在只知道:
\ \\hat y=2 \\
模型还不知道:
2 到底好不好?
必须拿正确答案:
\ y=6 \\
进行比较。
于是需要:
Loss
例如使用平方误差:
\ L=(\\hat y-y)\^2 \\
代入:
\ L=(2-6)\^2 \\
所以:
\ L=16 \\
现在我们有了一个数字:
\ 16 \\
它告诉我们:
当前模型预测得有多差。
但是仍然没有解决:
\(w\) 应该往哪个方向改?
5. 真正核心问题:怎么知道参数应该变大还是变小?
现在:
\ \\hat y=wx \\
同时:
\ L=(wx-y)\^2 \\
这里 Loss 其实是参数 \(w\) 的函数:
\ L(w) \\
我们的目标就是:
\ \\boxed{\\text{让 }L(w)\\text{ 尽可能小}} \\
于是我们要问:
如果 \(w\) 稍微变大一点,Loss 会变大还是变小?
这就是:
梯度 Gradient
开始出现的地方。
6. 先不用微积分理解梯度
现在:
\ w=1 \\
Loss:
\ L=16 \\
试着把 \(w\) 调大一点:
\ w=1.1 \\
那么:
\ \\hat y=1.1\\times2=2.2 \\
Loss:
\ L=(2.2-6)\^2=14.44 \\
发现:
\ 16\\rightarrow14.44 \\
Loss 下降了。
说明:
当前这个位置,把 \(w\) 往大的方向调整是对的。
如果一直调:
\ w=3 \\
那么:
\ \\hat y=3\\times2=6 \\
Loss:
\ L=0 \\
于是训练就找到了一个非常好的参数。
问题是:
真正的神经网络有几百万甚至几十亿参数,不可能一个一个试。
所以我们需要一个数学工具直接告诉每个参数:
你应该增加还是减少,以及大概改多少。
这个东西就是:
\ \\frac{\\partial L}{\\partial w} \\
也就是 Loss 对参数的:
Gradient
7. Gradient 到底表达什么?
\ \\frac{\\partial L}{\\partial w} \\
初学阶段可以先读成:
如果 \(w\) 稍微变化一点,Loss 会朝哪个方向、变化多快?
例如:
\ \\frac{\\partial L}{\\partial w}\>0 \\
说明:
\(w\) 增大,会使 Loss 增大。
那我们为了让 Loss 下降,就应该让:
\ w \\
减小。
反过来:
\ \\frac{\\partial L}{\\partial w}\<0 \\
说明增加 \(w\) 会让 Loss 下降。
8. 为什么更新参数时有一个负号?
经典梯度下降:
\ w_{\\text{new}} = w_{\\text{old}} - \\eta \\frac{\\partial L}{\\partial w} \\
这里:
\ \\eta \\
叫:
Learning Rate
学习率。
核心就是那个负号:
\ - \\
因为梯度告诉你:
Loss 上升最快的方向。
我们想让 Loss 下降,所以反着走。
因此叫:
Gradient Descent
梯度下降。
9. Backward 到底干什么?
现在回到 PyTorch。
我们已经:
pred_action = model(obs)
得到预测。
然后:
loss = loss_fn(pred_action, action)
得到 Loss。
接下来:
loss.backward()
这里不是:
把数据倒着跑一遍。
它真正做的是:
利用反向传播算法,计算 Loss 对模型中所有可训练参数的梯度。
也就是计算很多:
\ \\frac{\\partial L}{\\partial \\theta_1}, \\frac{\\partial L}{\\partial \\theta_2}, \\ldots \\
假设网络里有:
1000000 个参数
PyTorch 会帮你计算:
第1个参数应该怎么变
第2个参数应该怎么变
...
第1000000个参数应该怎么变
严格来说,它计算的是这些参数对应的:
gradient
并把结果存在参数的:
parameter.grad
里面。
10. 一个非常容易混淆的问题:backward() 会修改参数吗?
不会。
这点一定要记住。
loss.backward()
只是:
\ \\boxed{\\text{计算梯度}} \\
例如模型当前参数:
\ w=1 \\
调用:
loss.backward()
以后:
\ w \\
本身仍然可能还是:
\ 1 \\
只是现在多得到:
\ w.grad \\
告诉我们它应该往哪个方向改。
真正改参数的是下一步。
11. optimizer.step() 才真正修改参数
例如:
optimizer.step()
Optimizer 会读取:
parameter.grad
再按照自己的更新规则修改参数。
最简单可以理解成:
\ \\theta_{\\text{new}} = \\theta_{\\text{old}} - \\eta\\nabla_\\theta L \\
所以:
backward()
→ 算应该怎么改
optimizer.step()
→ 真的去改
这两个职责一定要分开。
12. 为什么还需要 optimizer.zero_grad()?
现在 PyTorch 经典训练代码通常是:
optimizer.zero_grad()pred_action = model(obs)loss = loss_fn(pred_action, action)loss.backward()optimizer.step()
其中最奇怪的可能是:
optimizer.zero_grad()
为什么要清零?
因为 PyTorch 中梯度默认会:
累加。
假设第一次:
\ grad=2 \\
下一次又算出:
\ grad=3 \\
如果不清空,可能变成:
\ grad=5 \\
但普通训练通常希望:
当前 Batch 只根据当前 Batch 的梯度更新。
所以每一轮先:
optimizer.zero_grad()
把上一个 Batch 留下的梯度清掉。
13. 现在完整理解这五行
终于可以完整解释:
optimizer.zero_grad()pred_action = model(obs)loss = loss_fn(pred_action, action)loss.backward()optimizer.step()
它们不是五个需要死记的命令。
而是一条非常严密的数据链:
第一步
optimizer.zero_grad()
清除上一批数据留下的梯度。
第二步
pred_action = model(obs)
Forward:
\ O \\rightarrow \\pi_\\theta \\rightarrow \\hat A \\
用当前参数进行预测。
第三步
loss = loss_fn(pred_action, action)
比较:
\ \\hat A \\
和:
\ A \\
得到:
\ L \\
第四步
loss.backward()
计算:
\ \\nabla_\\theta L \\
也就是每个模型参数对应的梯度。
第五步
optimizer.step()
利用梯度真正更新:
\ \\theta \\
于是模型和刚才相比已经变了一点。
下一批数据进来的时候:
\ \\pi_{\\theta_{\\text{new}}} \\
已经不是刚才那个 Policy 了。
14. 把一个 Batch 真正串起来
假设:
\ obs.shape=\[32,10 \]\ action.shape=\[32,7 \]
进入:
pred_action = model(obs)
得到:
\ pred\\_action.shape=\[32,7 \]
也就是:
32个样本
每个样本预测7维Action
然后:
loss = loss_fn(pred_action, action)
比较:
\ \[32,7 \]
与:
\ \[32,7 \]
Loss 函数把这一批预测误差综合起来,最终通常得到一个标量:
\ L\\in\\mathbb R \\
例如:
loss = 0.032
然后:
loss.backward()
从这个标量 Loss 一路反向追踪:
Loss
↓
Predicted Action
↓
最后一层
↓
前面的隐藏层
↓
第一层
计算各层参数的 Gradient。
最后:
optimizer.step()
修改这些参数。
这就是:
\ \\boxed{ \[32,10 \rightarrow 32,7 \rightarrow Loss \rightarrow Gradient \rightarrow Parameter Update } \]
15. 为什么 Loss 最后通常是一个数?
因为模型可能一次预测:
\ 32\\times7=224 \\
个数字。
每个数字都有误差。
如果每个误差都单独存在,我们就很难用一句话描述:
当前模型到底有多差?
所以 Loss 函数通常把很多误差:
样本1的误差
样本2的误差
...
所有Action维度误差
汇总成一个标量:
\ L \\
这样才能明确做优化:
\ \\min_\\theta L \\
也就是:
找到一组模型参数,让总体 Loss 尽可能小。
16. Epoch 又是怎么来的?
假设 Dataset 一共有:
\ 3200 \\
个 Sample。
Batch Size:
\ 32 \\
那么大约有:
\ \\frac{3200}{32}=100 \\
个 Batch。
程序执行:
for batch in dataloader:
把这 100 个 Batch 都训练一遍。
这叫:
1 Epoch
也就是:
整个训练 Dataset 大体被模型看过一遍。
然后继续第二次:
Epoch 2
第三次:
Epoch 3
模型不断重复学习数据。
17. Step 和 Epoch 不要混淆
例如:
Dataset = 3200 samples
Batch Size = 32
那么:
\ 100\\text{ batches} \\
每执行一次:
optimizer.step()
通常称为一个:
Training Step
于是:
\ 1\\text{ Epoch}\\approx100\\text{ Steps} \\
如果训练:
\ 10\\text{ Epochs} \\
大约就是:
\ 1000\\text{ Steps} \\
当然实际情况还会受 drop_last、sampler 等影响,后面再讲。
现在理解基本关系即可。
18. 为什么模型不是一次就学会?
因为刚开始:
\ \\theta \\
通常是随机初始化,或者来自预训练模型。
第一次看到一个 Batch 后只进行一次小更新:
\ \\theta_0 \\rightarrow \\theta_1 \\
下一批:
\ \\theta_1 \\rightarrow \\theta_2 \\
再下一批:
\ \\theta_2 \\rightarrow \\theta_3 \\
经过很多 Step:
\ \\theta_0 \\rightarrow \\theta_1 \\rightarrow \\cdots \\rightarrow \\theta_N \\
模型才逐渐形成:
\ Observation\\rightarrow Action \\
之间的映射。
所以所谓:
"神经网络在学习"
从最底层看,并不神秘。
实际上就是:
\ \\boxed{ 不断计算误差 \\rightarrow 计算梯度 \\rightarrow 一点一点修改参数 } \\
19. 一段真正完整的最小 BC Training Loop
现在再来看代码,就能把前四课串起来:
for batch in dataloader: obs = batch["observation"] action = batch["action"] optimizer.zero_grad() pred_action = model(obs) loss = loss_fn(pred_action, action) loss.backward() optimizer.step()
完整调用逻辑:
Dataset
↓
__getitem__()
↓
Sample
↓
DataLoader
↓
Batch
↓
obs / expert action
↓
model(obs)
↓
Forward
↓
predicted action
↓
Loss
↓
Backward
↓
Gradient
↓
optimizer.step()
↓
Model Parameters Updated
这就是目前为止我们建立起来的第一条完整 Robot Learning 训练链。
20. 训练完以后发生了什么?
训练结束以后,模型内部已经保存了一组学到的参数:
\ \\theta\^\\\* \\
我们把它保存下来:
Checkpoint
之后推理时:
New Observation
↓
Model with θ*
↓
Predicted Action
这时:
- 不需要 Expert Action;
- 不需要计算 Loss;
- 不需要
backward(); - 不需要
optimizer.step()。
因为现在不是训练,而是在:
Inference / Rollout
所以:
Training
需要:
Forward + Loss + Backward + Update
而:
Inference
主要需要:
Forward
这是一个非常重要的区别。
第四课整章链路回看
到这里你应该能够连续说出:
\ \\boxed{ Robot\\ Demonstration \\rightarrow Dataset \\rightarrow DataLoader \\rightarrow Batch \\rightarrow Forward \\rightarrow Predicted\\ Action \\rightarrow Loss \\rightarrow Backward \\rightarrow Gradient \\rightarrow Optimizer \\rightarrow Parameter\\ Update } \\
其中:
\ \\theta \\
是模型真正学习的参数;
\ \\hat A=\\pi_\\theta(O) \\
是 Forward;
\ L(\\hat A,A) \\
衡量模型预测和专家动作之间的差距;
\ \\nabla_\\theta L \\
告诉我们参数应该如何改变;
loss.backward() 负责计算梯度;
optimizer.step() 才真正修改参数。
第四课自测
1. loss.backward() 和 optimizer.step() 的职责有什么区别?
2. 为什么每个 Batch 前通常要调用 optimizer.zero_grad()?
3. 模型真正"学到"的东西存在哪里?Observation、Loss 还是参数 \(\theta\)?
4. 如果:
\ obs:\[32,10 \]
经过 Policy 输出:
\ pred\\_action:\[32,7 \]
Expert Action 是:
\ action:\[32,7 \]
那么 Loss 最后通常是什么 Shape?
5. 一个 Dataset 有 6400 个 Sample,Batch Size 是 64。忽略特殊情况,一个 Epoch 大约有多少个 Training Step?
6. Training 和 Inference 为什么都需要 Forward,但只有 Training 需要 Backward?