VLA 系统学习第 4 课:一个 Batch 进入神经网络后,模型到底是怎么“学会”的?

第三课标准答案: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?

相关推荐
IT古董1 小时前
《FDE前沿部署工程师实战教程》33 - Enterprise AI Observability:从Agent Trace到全链路智能运维
数据库·人工智能
Ada's1 小时前
【计算机基础系列】003:Python数据结构
开发语言·数据结构·python
xsd202411181 小时前
地网腐蚀AI识别算法全解析:从锈蚀图像到地下腐蚀快速定位
人工智能
oooost2 小时前
pytorch学习笔记2(transformer)
人工智能·机器学习
2601_962885722 小时前
如何用 Python 计算 TRIX 三重指数平滑均线指标?
开发语言·python
云杂项2 小时前
Exploring Model Inversion Attacks in the Black-box Setting(个人笔记)
人工智能
还卿一钵无情泪2 小时前
Unsloth 微调 构建自己的大模型 没有GPU也能微调
linux·开发语言·人工智能·python·大模型·nlp·unsloth
冬奇Lab3 小时前
LLM 驱动的自动化测试系列(06):移动端自动化(二)——DroidRun/Mobilerun 的角色级模型拆分
人工智能·测试
冬奇Lab3 小时前
一天一个开源项目(第230篇):HandRaw-Style —— 把 327 种手绘风格、165 种排版、36 种配色编号化,让 AI 画图不再每次都飘
人工智能·开源·资讯