给出一些相关的前置知识,我们从对象真正是怎么产生的开始。
一、先严格区分:定义类,不等于创建对象
看:
class BCPolicy(nn.Module): ...这句话是在:
定义一个新的类
BCPolicy。
它相当于告诉 Python:
以后我要有一种叫做
BCPolicy的对象,这种对象应该按照这里定义的规则工作。
但此时还没有真正创建一个 BCPolicy 模型实例。
也就是说:
class BCPolicy(nn.Module):不是:
创建一个模型。
而只是:
定义“模型这一类东西应该长什么样”。
真正创建对象是在:
model = BCPolicy()这里才产生了一个真正的:
BCPolicy 对象并把这个对象的引用保存到:
model里面。
所以一定区分:
class BCPolicy(nn.Module):是:
类定义
而:
model = BCPolicy()才是:
创建实例对象
二、(nn.Module)到底是什么意思?
看:
class BCPolicy(nn.Module):括号里的:
nn.Module不是函数参数。
它表示:
BCPolicy继承nn.Module。
所以关系是:
nn.Module ↑ BCPolicy可以把:
nn.Module叫做:
父类 / 基类
把:
BCPolicy叫:
子类 / 派生类
意思是:
BCPolicy 在自己定义新功能的同时,可以继承
nn.Module提供的能力。
三、为什么 PyTorch 模型一般都继承nn.Module?
因为:
nn.Module给神经网络对象准备好了大量基础设施。
例如后面我们会不断使用:
model.parameters()model.to(device)model.train()model.eval()model.state_dict()model.load_state_dict(...)包括:
model(obs)这种调用方式背后的很多机制,也来自nn.Module。
所以你可以暂时理解:
nn.Module = PyTorch 神经网络模块的基础骨架而我们写:
class BCPolicy(nn.Module):相当于:
我要在 PyTorch 已经提供好的神经网络骨架上,定义我自己的 BC Policy。
四、现在看第二行:def __init__(self):
def __init__(self):它只是:
定义这个类的初始化方法。
注意又是“定义”。
这一行本身也没有创建对象。
真正什么时候执行?
当后面:
model = BCPolicy()创建 BCPolicy 对象时,它会参与对象初始化过程。
先用一个普通 Python 类看:
class Person: def __init__(self, name): self.name = name然后:
p = Person("Tom")最终:
p.name就是:
Tom所以:
__init__()的主要作用可以先理解成:
当对象创建时,初始化这个对象内部需要保存的状态。
五、但是这里要比“自动调用 init”再严谨一点
很多教程会简单说:
创建对象的时候 Python 自动调用
__init__()。
作为初学理解没问题,但我们现在可以稍微严谨一点。
你写:
model = BCPolicy()大致经历:
BCPolicy() ↓ 创建一个新的 BCPolicy 实例 ↓ 初始化这个实例 ↓ 返回实例 ↓ model 指向这个实例其中 Python 对象创建机制还涉及:
__new__()和:
__init__()可以粗略看成:
__new__() 负责“创建对象” __init__() 负责“初始化已经创建出来的对象”当前阶段不用深入__new__()。
你只需要避免以后说成:
__init__()创建了对象。
更准确的是:
BCPolicy()触发对象创建流程,__init__()负责初始化这个已经产生的实例。
六、self到底是谁?
现在:
class BCPolicy(nn.Module): def __init__(self): ...假设后面:
model = BCPolicy()那么在这次初始化过程中:
self指向的就是:
这次刚创建出来的那个
BCPolicy实例。
也就是最终由:
model引用的那个对象。
所以如果写:
self.fc = nn.Linear(10, 7)意思不是:
给 BCPolicy 这个类统一创建一个 fc。
而是:
给当前这个具体的 BCPolicy 对象保存一个名为
fc的成员。
例如:
model1 = BCPolicy()model2 = BCPolicy()这是两个不同对象。
概念上:
BCPolicy 类 │ ├── model1 对象 │ └── 自己的 fc │ └── model2 对象 └── 自己的 fc这点和“类定义”和“真正对象”一定不要混在一起。
七、最关键的一行:super().__init__()到底在干什么?
现在看:
class BCPolicy(nn.Module): def __init__(self): super().__init__()前面已经知道:
BCPolicy继承:
nn.Module那么BCPolicy自己定义了:
__init__()就有一个问题:
父类
nn.Module自己原本的初始化工作怎么办?
于是使用:
super().__init__()它可以先理解成:
调用父类这一套初始化逻辑。
对于这里:
super()会按照 Python 的继承关系,继续寻找父类中的初始化方法。
所以概念上:
super().__init__()就是:
让 nn.Module 先把 PyTorch Module 应该准备的内部结构初始化好然后我们再继续定义自己模型的层。
例如:
class BCPolicy(nn.Module): def __init__(self): super().__init__() self.fc1 = nn.Linear(10, 64) self.relu = nn.ReLU() self.fc2 = nn.Linear(64, 7)逻辑就是:
创建 BCPolicy 对象 ↓ 进入 BCPolicy.__init__() ↓ 先调用父类 nn.Module 的初始化逻辑 ↓ PyTorch Module 基础设施准备好 ↓ 再把 fc1 注册进去 ↓ 再把 relu 注册进去 ↓ 再把 fc2 注册进去八、super()不是“创建父类对象”
这个地方也非常重要。
不要理解成:
super().__init__()创建了一个独立的:
nn.Module 对象不是。
当前仍然只有我们这个:
BCPolicy 实例比如:
model = BCPolicy()我们最终使用的就是这一个model对象。
super().__init__()是:
在这个同一个对象上,执行继承链中父类需要完成的初始化逻辑。
你可以先画成:
model │ └── BCPolicy 实例 │ ├── 具有 nn.Module 提供的能力 │ └── 具有 BCPolicy 自己增加的结构而不是:
model ├── 一个 BCPolicy └── 一个单独 nn.Module后者是错误理解。
九、nn.Module.__init__()到底帮我们初始化了什么?
不用现在去背 PyTorch 源码字段,但概念上非常重要。
nn.Module需要管理:
- Parameters;
- 子 Module;
- Buffers;
- forward/backward hooks;
- 模型状态;
state_dict();.to(device)等 Module 机制。
例如你后来写:
self.fc1 = nn.Linear(10, 64)PyTorch 不只是把:
fc1当成一个普通 Python 属性。
因为:
nn.Linear自己也是:
nn.Module所以它能够被当前 BCPolicy 注册成:
子 Module。
这样后面执行:
model.parameters()PyTorch 才能够递归找到:
model ↓ fc1 ↓ fc1.weight fc1.bias model ↓ fc2 ↓ fc2.weight fc2.bias于是:
model.parameters()才能把整个模型内部需要学习的参数找出来。
这就是为什么:
super().__init__()不是为了“写法好看”。
它实际上是在建立 PyTorch Module 的基础设施。
十、所以这三行应该怎样完整翻译?
现在重新看:
class BCPolicy(nn.Module): def __init__(self): super().__init__()可以逐行翻译。
第一行:
class BCPolicy(nn.Module):定义一个名为
BCPolicy的新类,它继承 PyTorch 的nn.Module。
第二行:
def __init__(self):定义 BCPolicy 实例创建后需要执行的初始化逻辑;
self表示当前这个具体实例。
第三行:
super().__init__()在当前这个实例上先执行父类
nn.Module的初始化逻辑,使这个对象具备完整的 PyTorch Module 基础设施。
然后通常继续:
self.fc1 = nn.Linear(10, 64)给当前 BCPolicy 实例注册一个 Linear 子模块。
这才是完整理解。
十一、接下来重点讲__call__():为什么model(obs)能工作?
先完全离开 PyTorch,看普通 Python。
假设:
class Test: def __call__(self, x): return x * 2创建对象:
obj = Test()注意:
obj是一个对象,不是普通函数。
但现在可以写:
result = obj(5)得到:
10为什么?
因为:
obj(5)这种:
对象(...)语法会触发对象对应类的:
__call__()概念上可以理解:
obj(5)触发:
obj.__call__(5)因此:
对象后面加括号 ↓ __call__()这和上一课:
dataset[10] ↓ __getitem__(10)属于同一套 Python 特殊方法机制。
十二、现在回到 PyTorch
我们的类:
class BCPolicy(nn.Module):继承:
nn.Module而nn.Module为 Module 对象提供了“可以被调用”的机制。
于是:
model = BCPolicy()得到一个 BCPolicy 实例以后,可以写:
pred_action = model(obs)这里:
model(obs)会进入nn.Module提供的调用流程。
最终再执行我们自己定义的:
def forward(self, obs):十三、那为什么我们不自己定义__call__()?
这是 PyTorch 初学中很关键的一点。
你可能会想:
既然:
model(obs)调用__call__(),
那我为什么不写:
class BCPolicy(nn.Module): def __call__(self, obs): ...而非要写:
def forward(self, obs):因为nn.Module已经替我们实现了 Module 的调用机制。
它不只是简单:
__call__() ↓ forward()中间还需要为 PyTorch 处理一些 Module 级机制,例如各种 hook 等。
因此我们通常应该做的是:
实现
forward(),而不是自己覆盖nn.Module.__call__()。
也就是把职责分开:
nn.Module 负责模型怎么被调用 我们 负责 forward 到底怎么计算这是一种非常重要的框架设计思想。
十四、所以这条链可以写得更严谨
之前我们写:
model(obs) ↓ nn.Module 的 __call__() ↓ PyTorch 相关 Module 调用逻辑 ↓ forward(obs) ↓ 返回结果这个理解是对的。
为了避免你以后误解,再加上对象关系:
model = BCPolicy() │ └── model 是 BCPolicy 的一个实例 │ └── BCPolicy 继承 nn.Module │ └── 得到了 Module 的调用机制 model(obs) ↓ 触发 Module 对象的调用流程 ↓ 进入 PyTorch Module 的 __call__ 相关逻辑 ↓ 最终调用 model.forward(obs) ↓ 得到输出十五、为什么推荐model(obs),而不是直接model.forward(obs)?
因为:
model.forward(obs)是直接调用你自己实现的forward()。
而:
model(obs)走的是:
nn.Module 调用机制 ↓ forward这样 PyTorch 在forward()前后需要处理的 Module 机制才能正常参与。
因此平时应该:
output = model(x)而不是把:
model.forward(x)当成标准调用方式。
十六、把__getitem__()和__call__()放在一起,你就容易记住了
现在你已经遇到两个非常典型的 Python 特殊方法。
Dataset
你写:
sample = dataset[10]背后:
[] ↓ __getitem__()所以:
dataset[10]触发取数据逻辑。
Model
你写:
pred = model(obs)背后:
() ↓ __call__() ↓ forward()所以:
model(obs)最终进行网络 Forward。
因此:
dataset[index] ↓ __getitem__() ↓ 得到数据 model(input) ↓ __call__() ↓ forward() ↓ 得到预测这两条链以后会每天见到。
十七、把类创建、Dataset、Model 整体串起来
现在看代码:
class BCPolicy(nn.Module): def __init__(self): super().__init__() self.net = nn.Sequential( nn.Linear(10, 64), nn.ReLU(), nn.Linear(64, 7) ) def forward(self, obs): return self.net(obs)第一阶段只是:
定义 BCPolicy 类还没有真正的模型对象。
然后:
model = BCPolicy()才:
创建 BCPolicy 实例 ↓ 执行初始化过程 ↓ BCPolicy.__init__() ↓ super().__init__() ↓ nn.Module 基础结构初始化 ↓ 创建并注册 self.net ↓ model 对象准备完成之后训练:
pred_action = model(obs)发生:
model(obs) ↓ Module 调用机制 ↓ forward(obs) ↓ self.net(obs) ↓ Linear ↓ ReLU ↓ Linear ↓ pred_action现在整个对象链就连起来了。
十八、第五课标准答案
1.nn.Linear(10,64)的 Weight 和 Bias Shape
PyTorch 中:
\[ weight.shape=[64,10] \]\[ bias.shape=[64] \]
记住:
\[ nn.Linear(in,out) \]
其 Weight:
\[ [out,in] \]
2. 为什么[B,10] → [B,64],但 \(B\) 不变?
因为这一层对 Batch 中的每个样本使用同一套 Linear 参数。
每个:
\[ [10] \]
变成:
\[ [64] \]
所以:
\[ [B,10]\rightarrow[B,64] \]
Batch 中仍然是原来的 \(B\) 个样本。
3.nn.Linear(10,64)有多少参数?
Weight:
\[ 64\times10=640 \]
Bias:
\[ 64 \]
总数:
\[ 640+64=\boxed{704} \]
4.nn.Linear(64,7)有多少参数?
Weight:
\[ 7\times64=448 \]
Bias:
\[ 7 \]
总数:
\[ 448+7=\boxed{455} \]
5. 如果 \(W:[64,10]\),那么W.grad的 Shape?
也是:
\[ \boxed{[64,10]} \]
因为每一个 Weight:
\[ w_{ij} \]
都需要对应一个梯度:
\[ \frac{\partial L}{\partial w_{ij}} \]
因此 Weight Matrix 和它的 Gradient Matrix Shape 相同。
6. 为什么要把model.parameters()给 Optimizer?
因为 Optimizer 必须知道:
哪些东西是允许它更新的模型参数。
例如:
fc1.weight fc1.bias fc2.weight fc2.bias于是:
optimizer = Adam(model.parameters())相当于告诉 Optimizer:
训练的时候,请更新这些参数。
7.model(obs)为什么会最终调用forward()?
因为:
BCPolicy继承:
nn.Module获得了 Module 的调用机制。
所以:
model(obs) ↓ Module 的 __call__ 相关流程 ↓ forward(obs) ↓ 返回预测我们负责定义:
forward()PyTorch 负责外面的调用框架。
8. \(\theta\) 到底是什么?
\[ \theta \]
表示:
模型所有可训练参数的集合。
例如:
\[ \theta= \{ W_1,b_1,W_2,b_2 \} \]
更复杂的模型中可能有成千上万组参数。
训练本质上就是不断改变:
\[ \theta \]
让:
\[ \pi_\theta(o) \]
产生越来越正确的输出。
VLA 系统学习第 6 课:为什么只有 Linear 不够?——ReLU、非线性和隐藏层究竟做了什么
现在我们已经知道一个 Linear Layer 的本质:
\[ y=Wx+b \]
于是一个非常自然的问题出现了:
那我多堆几层 Linear 不就很强了吗?
比如:
nn.Linear(10, 64)nn.Linear(64, 128)nn.Linear(128, 64)nn.Linear(64, 7)看起来已经四层了。
是不是就已经拥有非常强的表达能力?
答案非常关键:
如果中间没有非线性激活函数,那么无论堆多少个 Linear,本质上仍然只相当于一个 Linear。
这就是为什么神经网络里必须出现:
ReLU这样的东西。
一、先从两个 Linear 连起来看
假设第一层:
\[ h=W_1x+b_1 \]
第二层:
\[ y=W_2h+b_2 \]
把第一层代入第二层:
\[ y=W_2(W_1x+b_1)+b_2 \]
展开:
\[ y=W_2W_1x+W_2b_1+b_2 \]
现在定义:
\[ W'=W_2W_1 \]
再定义:
\[ b'=W_2b_1+b_2 \]
那么:
\[ y=W'x+b' \]
你会发现:
两层 Linear 又被合并成了一个新的 Linear。
所以:
\[ Linear\rightarrow Linear \]
本质上仍然是:
\[ Linear \]
二、堆十层也没有解决问题
三层:
\[ x \rightarrow W_1x+b_1 \rightarrow W_2h_1+b_2 \rightarrow W_3h_2+b_3 \]
最后照样可以整理成:
\[ y=W'x+b' \]
因此:
\[ \boxed{ Linear \rightarrow Linear \rightarrow Linear \rightarrow \cdots } \]
如果中间没有非线性操作,再多层也只是一个复杂写法的线性映射。
所以问题不是:
层数还不够。
而是:
模型内部缺少非线性。
三、为什么机器人 Policy 明显需要非线性?
想一个非常简单的动作规则。
假设 Observation 只有一个值:
\[ x \]
表示:
物体在机器人左右方向上的位置。
规定:
- \(x<0\):物体在左边;
- \(x>0\):物体在右边。
假设策略希望:
物体在左边 → 向左运动 物体在右边 → 向右运动有些简单关系 Linear 可以表达。
但现实策略往往是:
如果物体在左边 而且夹爪是打开的 而且末端还比较远 → 靠近物体 如果物体在左边 而且夹爪已经靠近 → 关闭夹爪 如果已经抓住 → 向上抬 如果已经到目标位置 → 松开也就是说同一个输入维度对 Action 的影响,会随着其他状态变化而变化。
这种关系不是简单:
\[ y=Wx+b \]
就能轻松描述的。
我们需要模型能够形成:
不同区域使用不同的行为规律。
于是引入:
Nonlinearity
非线性。
四、ReLU 是什么?
最常见的激活函数之一:
\[ \operatorname{ReLU}(x) = \max(0,x) \]
意思非常简单:
如果:
\[ x>0 \]
输出:
\[ x \]
如果:
\[ x\leq0 \]
输出:
\[ 0 \]
例如:
\[ ReLU(-3)=0 \]\[ ReLU(-0.5)=0 \]\[ ReLU(0)=0 \]\[ ReLU(2)=2 \]\[ ReLU(8)=8 \]
五、它到底改变了什么?
假设 Linear 输出:
\[ h= [-2,\;3,\;-0.5,\;7] \]
经过 ReLU:
\[ ReLU(h) = [0,\;3,\;0,\;7] \]
可以理解成:
所有负值被截断为 0,正值保留。
所以:
Linear ↓ 产生一组特征 ↓ ReLU ↓ 根据数值所在区域改变这些特征这一步虽然公式看起来简单,却破坏了“所有层都可以合并成一个 Linear”的性质。
六、为什么加了 ReLU 后不能再简单合并?
没有 ReLU:
\[ y=W_2(W_1x+b_1)+b_2 \]
可以直接展开。
但加入 ReLU:
\[ h=ReLU(W_1x+b_1) \]
然后:
\[ y=W_2h+b_2 \]
也就是:
\[ y= W_2ReLU(W_1x+b_1)+b_2 \]
现在:
\[ ReLU(\cdot) \]
夹在中间。
你无法再简单写成:
\[ y=W'x+b' \]
因为 ReLU 会根据输入不同,决定:
哪些值保留,哪些值直接变成 0。
这就是非线性真正带来的变化。
七、隐藏层为什么叫 Hidden Layer?
看:
self.net = nn.Sequential( nn.Linear(10, 64), nn.ReLU(), nn.Linear(64, 7))输入:
\[ x:[10] \]
经过第一层:
\[ h:[64] \]
最后:
\[ a:[7] \]
这里:
\[ h \]
既不是原始 Observation,也不是最终 Action。
它是网络内部生成的中间表示。
所以叫:
Hidden Representation
对应这一层叫:
Hidden Layer
数据链:
\[ Observation \rightarrow Hidden\ Feature \rightarrow Action \]
八、Hidden Feature 到底是什么?
假设 Observation 有:
\[ 10 \]
个原始数字。
经过:
nn.Linear(10,64)以后变成:
\[ 64 \]
个隐藏特征。
这些隐藏特征通常没有简单固定的人类解释。
不能机械地说:
hidden[0] = 距离 hidden[1] = 角度 hidden[2] = 是否应该抓模型自己通过训练决定这些内部表示应该如何组织。
更准确地说:
Hidden Representation 是模型为了完成最终预测任务而学习出来的内部特征空间。
九、现在看完整 Shape
假设:
\[ obs:[32,10] \]
第一层:
nn.Linear(10,64)得到:
\[ [32,64] \]
再经过:
nn.ReLU()Shape 不变:
\[ [32,64] \]
然后:
nn.Linear(64,7)得到:
\[ [32,7] \]
所以完整数据流:
\[ [32,10] \rightarrow [32,64] \rightarrow [32,64] \rightarrow [32,7] \]
对应:
Observation ↓ Linear ↓ Hidden Feature ↓ ReLU ↓ Nonlinear Hidden Feature ↓ Linear ↓ Predicted Action十、为什么 ReLU 没有 Parameters?
看:
nn.Linear(10,64)里面有:
\[ W,b \]
所以有可训练参数。
而:
nn.ReLU()只是执行固定规则:
\[ x\leq0\rightarrow0 \]\[ x>0\rightarrow x \]
它没有需要训练的:
\[ W \]
或者:
\[ b \]
所以:
\[ \boxed{ ReLU\ Parameters=0 } \]
但是:
没有参数,不代表它不重要。
恰恰相反,它对神经网络表达复杂函数非常关键。
十一、用代码完整看一次
class BCPolicy(nn.Module): def __init__(self): super().__init__() self.fc1 = nn.Linear(10, 64) self.relu = nn.ReLU() self.fc2 = nn.Linear(64, 7) def forward(self, obs): x = self.fc1(obs) x = self.relu(x) action = self.fc2(x) return action现在这段代码应该能够连续读。
输入:
obsShape:
\[ [B,10] \]
先:
x = self.fc1(obs)发生:
\[ [B,10] \rightarrow [B,64] \]
然后:
x = self.relu(x)数值发生非线性变换,但 Shape:
\[ [B,64] \rightarrow[B,64] \]
最后:
action = self.fc2(x)发生:
\[ [B,64] \rightarrow[B,7] \]
最终:
\[ action:[B,7] \]
十二、它又怎么训练?
仍然没有改变我们前面的主链。
pred_action = model(obs)loss = loss_fn(pred_action, action)optimizer.zero_grad()loss.backward()optimizer.step()只是现在:
model(obs)内部不再是一个 Linear。
而是:
\[ Observation \rightarrow Linear \rightarrow ReLU \rightarrow Linear \rightarrow Action \]
Backward 会沿整条计算链往回计算:
Loss ↓ fc2 ↓ ReLU ↓ fc1最终得到:
\[ \frac{\partial L}{\partial W_2}, \frac{\partial L}{\partial b_2}, \frac{\partial L}{\partial W_1}, \frac{\partial L}{\partial b_1} \]
然后 Optimizer 更新这些参数。
十三、为什么这和后面的 VLA 有关系?
因为后面模型会变得非常复杂:
Image ↓ Vision Encoder Language ↓ Language Encoder Robot State ↓ State Encoder ↓ Fusion ↓ Transformer ↓ Action Head ↓ Action但是无论名字多复杂,里面仍然充满:
- Linear;
- Matrix Multiplication;
- Nonlinear Activation;
- Parameters;
- Forward;
- Gradient;
- Backward。
所以现在真正理解:
\[ Wx+b \]
以及:
\[ Linear\rightarrow ReLU \]
不是在偏离 VLA。
而是在学习以后所有复杂网络共同使用的底层语言。
第六课链路回看
这一课最重要的逻辑只有一条:
\[ Linear \rightarrow Linear \rightarrow Linear \]
如果没有非线性,本质仍然可以压缩成:
\[ Linear \]
因此加入:
\[ ReLU \]
以后:
\[ Linear \rightarrow ReLU \rightarrow Linear \]
模型才拥有表达复杂非线性映射的基础能力。
于是 BC Policy:
\[ Observation \rightarrow Hidden\ Representation \rightarrow Action \]
不再只是一个简单的线性变换。
第六课自测
为什么两层 Linear 中间如果没有激活函数,最终仍然等价于一个 Linear?
ReLU 的数学定义是什么?
输入:
\[ [-2,3,-0.5,7] \]
经过 ReLU 后是什么?
- 为什么 ReLU 不改变:
\[ [B,64] \]
的 Shape,却仍然非常重要?
nn.ReLU()有多少个可训练参数?为什么 Hidden Feature 通常不能直接解释成“某个明确物理量”?
对于:
nn.Linear(10,64)nn.ReLU()nn.Linear(64,7)如果输入:
\[ [32,10] \]
请完整写出每一步 Shape。
- 最后一个核心问题:为什么
Linear → ReLU → Linear比单纯Linear → Linear更有能力表达复杂的 Observation → Action 关系?