VLA 系统学习第 5 课:神经网络的参数到底是什么?——从 nn.Linear 真正理解 W、 b 和 Gradient

第四课标准答案

先把上一课最后六个问题收掉,然后直接沿着"参数 \(\theta\) 到底是什么"进入第五课。

1. loss.backward() 和 optimizer.step() 有什么区别?

最关键的区别是:

\ \\boxed{\\text{backward 负责算梯度,step 负责改参数}} \\

执行:

复制代码
loss.backward()

以后,模型参数本身还没有被更新。PyTorch 只是算出了:

\ \\frac{\\partial L}{\\partial \\theta} \\

并把每个参数对应的梯度保存下来。

随后:

复制代码
optimizer.step()

Optimizer 才利用这些梯度真正修改:

\ \\theta \\

所以是:

\ L \\rightarrow \\text{backward} \\rightarrow \\nabla_\\theta L \\rightarrow \\text{optimizer.step()} \\rightarrow \\theta_{\\text{new}} \\


2. 为什么每个 Batch 前通常要 optimizer.zero_grad()?

因为 PyTorch 默认会把梯度累加。

假设上一个 Batch 算出来:

\ grad=2 \\

当前 Batch 又得到:

\ grad=3 \\

如果不清零,可能得到:

\ grad=5 \\

普通训练通常希望当前更新只依据当前 Batch,所以先:

复制代码
optimizer.zero_grad()

把之前残留的梯度清掉。

然后重新:

复制代码
Forward
→ Loss
→ Backward
→ 当前Batch的Gradient
→ Update

3. 模型真正"学到"的东西存在哪里?

在模型参数:

\ \\boxed{\\theta} \\

里面。

Observation 是输入数据,Loss 是一个训练过程中产生的评价数值。

真正随着训练不断变化并最终被保存到 Checkpoint 的,是模型里的:

\ W,\\quad b,\\quad \\text{以及其他可训练参数} \\

因此:

\ \\theta=\\{W_1,b_1,W_2,b_2,\\ldots\\} \\

所谓"模型学会了",从最底层来看,就是:

模型内部的这些参数被调整到了更合适的数值。


4. Loss 通常是什么 Shape?

假设:

\ obs:\[32,10 \]

模型预测:

\ \\hat A:\[32,7 \]

专家 Action:

\ A:\[32,7 \]

虽然一共有:

\ 32\\times7=224 \\

个预测数值需要比较,但是常见 Loss 最后会把这些误差进行求和或求平均,得到一个:

\ \\boxed{\\text{scalar}} \\

也就是标量。

在 PyTorch 中经常看到:

复制代码
loss.shape

是:

复制代码
[]

或者把它打印出来就是:

复制代码
0.0317

5. 6400 个 Sample,Batch Size = 64,一个 Epoch 大约多少 Step?

\ \\frac{6400}{64}=100 \\

所以:

\ \\boxed{100\\text{ steps}} \\

也就是一个 Epoch 中大约会执行 100 次:

复制代码
optimizer.step()

6. 为什么 Training 和 Inference 都有 Forward,但只有 Training 需要 Backward?

因为两者都需要模型做预测:

\ o\\rightarrow\\pi_\\theta(o)\\rightarrow\\hat a \\

所以都需要 Forward。

但是 Training 还要问:

我的预测和正确答案差多少?参数应该怎么修改?

因此还需要:

\ \\hat a \\rightarrow Loss \\rightarrow Backward \\rightarrow Parameter Update \\

而 Inference 的参数已经训练好了,只需要:

\ Observation\\rightarrow Predicted\\ Action \\

不需要继续改模型。


VLA 系统学习第 5 课:神经网络的参数到底是什么?------从 nn.Linear 真正理解 \(W\)、\(b\) 和 Gradient

上一课最后,我们留下了一句话:

模型训练,就是不断修改参数 \(\theta\)。

但这句话其实仍然非常抽象。

究竟什么叫"参数"?

一个 nn.Linear(10, 64) 里面到底装了什么?

为什么输入:

\ \[B,10 \]

经过它以后会变成:

\ \[B,64 \]

?

optimizer.step() 到底在改哪个数字?

这一课我们就不再把"参数"当成一个抽象黑箱,而是真正把一层神经网络拆开。


一、先不要想"神经网络",只想一个最简单的公式

假设我们只有一个输入数字:

\ x \\

模型输出:

\ y \\

最简单的关系可以写成:

\ y=wx+b \\

这里真正决定模型行为的是:

\ w \\

和:

\ b \\

例如:

\ w=2,\\qquad b=1 \\

输入:

\ x=3 \\

那么:

\ y=2\\times3+1=7 \\

如果把参数改成:

\ w=5,\\qquad b=-2 \\

相同的输入:

\ x=3 \\

就得到:

\ y=5\\times3-2=13 \\

你看,输入完全没变。

真正改变模型输出的,是:

\ w,b \\

所以这两个东西就是:

Parameters

模型参数。

训练就是不断调整这些参数。


二、为什么需要 \(b\)?

先只考虑:

\ y=wx \\

如果:

\ x=0 \\

那么无论 \(w\) 是多少:

\ y=0 \\

但现实中我们可能希望:

\ x=0 \\

时模型输出:

\ y=3 \\

这时就可以加入:

\ b \\

变成:

\ y=wx+b \\

于是:

\ x=0 \\

时:

\ y=b \\

所以 \(b\) 可以理解成:

在输入经过权重变化之后,再给输出增加一个可以学习的平移量。

\(W\) 通常叫:

Weight,权重

\(b\) 叫:

Bias,偏置


三、从一个输入扩展到多个输入

机器人 Observation 当然不可能只有一个数字。

假设 Observation 有三个维度:

\ x= \[x_1,x_2,x_3 \]

现在我们想输出一个数字。

可以写成:

\ y= x_1w_1+x_2w_2+x_3w_3+b \\

比如:

\ x=\[2,1,3 \]

权重:

\ w=\[0.5,-1,2 \]

偏置:

\ b=0.2 \\

那么:

\ y= 2\\times0.5 + 1\\times(-1) + 3\\times2 + 0.2 \\

所以:

\ y=1-1+6+0.2=6.2 \\

这里其实发生了一件非常重要的事情:

模型让不同输入维度具有不同的重要程度。

因为三个权重:

\ 0.5,-1,2 \\

不同。

第三个输入的影响就比第一个更强。


四、但机器人 Action 也不止一个数字

现在假设:

\ x=\[x_1,x_2,x_3 \]

我们不想只输出一个数,而是输出两个:

\ y=\[y_1,y_2 \]

那么第一份输出可以有自己的一套权重:

\ y_1= x_1w_{11} +x_2w_{12} +x_3w_{13} +b_1 \\

第二份输出也有自己的一套权重:

\ y_2= x_1w_{21} +x_2w_{22} +x_3w_{23} +b_2 \\

现在已经有:

\ 6 \\

个 Weight 和:

\ 2 \\

个 Bias。

这时候一个一个写就很麻烦,所以使用矩阵。


五、Linear Layer 本质上就是矩阵乘法

把权重组织成:

\ W= \\begin{bmatrix} w_{11}\&w_{12}\&w_{13}\\\\ w_{21}\&w_{22}\&w_{23} \\end{bmatrix} \\

那么:

\ W\\in\\mathbb R\^{2\\times3} \\

输入:

\ x\\in\\mathbb R\^3 \\

Bias:

\ b\\in\\mathbb R\^2 \\

就可以写成:

\ y=Wx+b \\

于是:

\ \\mathbb R\^3 \\rightarrow \\mathbb R\^2 \\

这就是 Linear Layer 最核心的东西。


六、对应到 PyTorch 的 nn.Linear

现在看:

复制代码
layer = nn.Linear(3, 2)

这里:

复制代码
3 = in_features
2 = out_features

也就是说:

\ 3\\text{维输入} \\rightarrow 2\\text{维输出} \\

PyTorch 内部会创建:

\ W \\

和:

\ b \\

其中:

复制代码
layer.weight.shape

是:

\ \[2,3 \]

而:

复制代码
layer.bias.shape

是:

\ \[2 \]

这非常重要。

PyTorch 的 nn.Linear(in_features, out_features) 中:

\ \\boxed{ weight.shape= \[out\\_features,in\\_features } \]

所以:

复制代码
nn.Linear(10, 64)

里面:

\ W:\[64,10 \]\ b:\[64 \]


七、为什么有时候教材又写 \(Y=XW+b\)?

这个地方以后非常容易把你绕晕,所以现在就把它说明白。

数学教材里可能写:

\ Y=XW+b \\

如果:

\ X:\[B,10 \]

希望输出:

\ Y:\[B,64 \]

那么这里的 \(W\) 应该是:

\ W:\[10,64 \]

因为:

\ \[B,10\times10,64 = B,64 \]

但是 PyTorch 实际存储:

复制代码
layer.weight

是:

\ \[64,10 \]

所以 PyTorch 计算概念上更接近:

\ Y=XW\^T+b \\

因为:

\ W\^T:\[10,64 \]

于是:

\ \[B,10\times10,64 = B,64 \]

所以以后必须区分两件事:

\ \\boxed{\\text{数学公式中的矩阵写法}} \\

和:

\ \\boxed{\\text{PyTorch实际保存Weight的Shape}} \\

如果代码是:

复制代码
nn.Linear(10, 64)

你看到:

复制代码
weight.shape

应该想到:

\ \[64,10 \]


八、现在终于解释 [B,10] → [B,64]

假设:

复制代码
layer = nn.Linear(10, 64)

输入:

\ X:\[B,10 \]

其中每一行代表一个 Sample:

\ X= \\begin{bmatrix} ---x_1---\\\\ ---x_2---\\\\ \\vdots\\\\ ---x_B--- \\end{bmatrix} \\

每一个:

\ x_i \\

都有 10 个数字。

Linear Layer 有:

\ 64 \\

组输出权重。

所以对于一个 Sample:

\ \[10 \]

会计算出:

\ \[64 \]

个新数字。

于是整个 Batch:

\ \[B,10 \]

变成:

\ \[B,64 \]

注意:

\ B \\

没有变。

因为 Linear 并没有把 32 个 Sample 混在一起。

它只是对这 32 个 Sample 使用同一套参数分别计算。

所以:

\ \[32,10 \rightarrow 32,64 \]

这里真正变化的是:

\ 10\\rightarrow64 \\

而不是:

\ 32 \\


九、这 64 个数字是什么?

这是一个很关键的问题。

假设原 Observation 是:

\ \[10 \]

经过:

复制代码
nn.Linear(10, 64)

得到:

\ \[64 \]

这 64 个数字通常不再具有:

第 1 个就是关节 1,第 2 个就是关节 2......

这样的直接物理含义。

它们可以理解成:

神经网络根据原始输入学习出来的一组内部特征表示。

通常称为:

Feature / Hidden Representation

所以网络:

复制代码
Observation
[10]
↓
Linear
↓
Hidden Feature
[64]

实际上是在把原始状态映射到一个新的特征空间。


十、为什么不能直接 10 → 7?

当然可以。

例如:

复制代码
nn.Linear(10, 7)

直接:

\ Observation \\rightarrow Action \\

完全可以训练。

但它本质上只是:

\ y=Wx+b \\

属于一个线性映射。

而现实中的 Observation → Action 关系往往非常复杂。

例如:

当物体在左边且夹爪打开时这样移动;
当物体在右边且夹爪已经闭合时又是另一种行为。

这种复杂关系通常不能只靠一个简单线性关系表达。

因此神经网络通常会增加隐藏层和:

Nonlinearity

例如:

复制代码
self.net = nn.Sequential(    nn.Linear(10, 64),    nn.ReLU(),    nn.Linear(64, 7))

于是数据流:

\ \[10 \rightarrow 64 \rightarrow 64 \rightarrow 7 \]

其中:

复制代码
nn.ReLU()

不会改变这里的 Shape:

\ \[64\rightarrow64 \]

但它引入了非线性。

ReLU 的细节我们现在不岔出去,后面真正需要理解神经网络表达能力时再系统展开。


十一、这个小网络到底有多少 Parameters?

现在来看:

复制代码
nn.Linear(10, 64)

Weight:

\ 64\\times10=640 \\

个参数。

Bias:

\ 64 \\

个参数。

所以第一层共有:

\ 640+64=704 \\

个参数。

第二层:

复制代码
nn.Linear(64, 7)

Weight:

\ 7\\times64=448 \\

Bias:

\ 7 \\

所以:

\ 448+7=455 \\

整个网络总参数数:

\ 704+455=1159 \\

ReLU 没有可训练 Weight 和 Bias,所以:

\ 0 \\

个可训练参数。

因此这个极小的 BC Policy 就已经有:

\ \\boxed{1159} \\

个可以被训练修改的数字。

所谓:

\ \\theta \\

实际上就是这 1159 个数字的集合。


十二、model.parameters() 里面究竟是什么?

假设模型:

复制代码
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)

创建:

复制代码
model = BCPolicy()

然后:

复制代码
model.parameters()

就是在获取模型中注册的可训练参数。

这里主要就是:

复制代码
第一层 weight
第一层 bias
第二层 weight
第二层 bias

更直观的方法是:

复制代码
for name, param in model.named_parameters():    print(name, param.shape)

可能看到类似:

复制代码
net.0.weight   [64, 10]
net.0.bias     [64]
net.2.weight   [7, 64]
net.2.bias     [7]

这就是:

\ \\theta \\

第一次真正从数学符号变成代码里的东西。


十三、Optimizer 为什么需要 model.parameters()?

现在看:

复制代码
optimizer = torch.optim.Adam(    model.parameters(),    lr=1e-3)

你现在应该能理解:

复制代码
model.parameters()

不是一句无意义的固定语法。

它实际上是在告诉 Optimizer:

这些参数是你以后允许修改的对象。

也就是:

复制代码
W1
b1
W2
b2

训练时:

复制代码
loss.backward()

计算这些参数的 Gradient。

然后:

复制代码
optimizer.step()

修改这些参数。

所以链条真正变成:

\ \\boxed{ model.parameters() \\rightarrow Gradient \\rightarrow Optimizer \\rightarrow Updated\\ Parameters } \\


十四、Gradient 和 Weight 是一一对应的

假设:

复制代码
layer.weight.shape

是:

\ \[64,10 \]

那么训练以后:

复制代码
layer.weight.grad.shape

也是:

\ \[64,10 \]

为什么?

因为每一个 Weight:

\ w_{ij} \\

都需要知道:

我对最终 Loss 产生了什么影响?

所以每一个:

\ w_{ij} \\

都有对应的:

\ \\frac{\\partial L}{\\partial w_{ij}} \\

因此 Weight Matrix:

\ W \\

对应一个同 Shape 的 Gradient Matrix:

\ \\nabla_WL \\

如果:

\ W:\[64,10 \]

那么:

\ \\nabla_WL:\[64,10 \]

同样:

\ b:\[64 \]

那么:

\ \\nabla_bL:\[64 \]


十五、真正理解 optimizer.step() 在做什么

假设某一个 Weight 当前是:

\ w_{23}=0.41 \\

Backward 算出:

\ \\frac{\\partial L}{\\partial w_{23}}=0.8 \\

假设最简单的梯度下降学习率:

\ \\eta=0.01 \\

那么:

\ w_{23}\^{new} = 0.41-0.01\\times0.8 \\

所以:

\ w_{23}\^{new}=0.402 \\

另一个参数可能是:

\ w_{45}=-0.12 \\

梯度:

\ \\frac{\\partial L}{\\partial w_{45}}=-0.5 \\

更新:

\ w_{45}\^{new} = -0.12-0.01\\times(-0.5) \\

得到:

\ w_{45}\^{new}=-0.115 \\

也就是说:

有的 Weight 变大,有的 Weight 变小。

不是整个模型统一加一个数字。

而是:

\ \\boxed{ 每一个参数都有自己的Gradient } \\

因此一次 optimizer.step() 可能同时修改几十万、几百万甚至几十亿个不同的数字。


十六、为什么一个参数改变会影响最终 Action?

现在把整个网络写出来:

\ O \\rightarrow W_1,b_1 \\rightarrow Hidden \\rightarrow W_2,b_2 \\rightarrow \\hat A \\

如果:

\ W_1 \\

改变了一点,

中间 Hidden Feature 就会改变。

Hidden 改变以后:

\ \\hat A \\

也会改变。

所以训练实际上是在不断寻找:

\ W_1,b_1,W_2,b_2 \\

这些参数的组合,让:

\ \\hat A \\

越来越接近:

\ A \\

于是:

\ \\min_\\theta L(\\pi_\\theta(O),A) \\

现在这条公式应该比上一课具体得多了。

这里:

\ \\theta \\

不是什么神秘符号。

它真的就是:

\ \\theta= \\{W_1,b_1,W_2,b_2,\\ldots\\} \\


十七、顺着前面"魔术方法"再理解一次 model(obs)

前面我们学过:

复制代码
dataset[10]

会触发:

复制代码
__getitem__()

现在还有一个很像的情况。

我们定义:

复制代码
class BCPolicy(nn.Module):    def forward(self, obs):        ...

但训练时并不通常写:

复制代码
model.forward(obs)

而是:

复制代码
model(obs)

这是因为:

复制代码
BCPolicy

继承了:

复制代码
nn.Module

而 nn.Module 对:

复制代码
__call__()

进行了处理。

所以可以先把调用链理解成:

复制代码
model(obs)
↓
nn.Module 的 __call__()
↓
执行 PyTorch 在前后需要处理的一些逻辑
↓
调用我们定义的 forward(obs)
↓
返回结果

所以:

复制代码
pred_action = model(obs)

最终确实会进入:

复制代码
def forward(self, obs):

这和前面的:

复制代码
dataset[index]
→ __getitem__()

属于同一种 Python 思想:

特定语法触发对象定义好的特殊行为。

因此以后看到:

复制代码
model(x)

不要理解成:

model 是一个普通函数。

更准确地说:

model 是 nn.Module 对象,这个对象可以被调用,而调用最终会进入 forward()。


十八、现在把第五课和前四课彻底接起来

我们的完整链已经从真实数据走到了具体 Weight:

\ \\boxed{ Demonstration \\rightarrow Dataset \\rightarrow Batch \\rightarrow Observation \\rightarrow Linear(W_1,b_1) \\rightarrow Hidden\\ Feature \\rightarrow Linear(W_2,b_2) \\rightarrow Predicted\\ Action } \\

然后:

\ \\hat A \\rightarrow L(\\hat A,A) \\rightarrow Backward \\

得到:

\ \\nabla_{W_1}L, \\nabla_{b_1}L, \\nabla_{W_2}L, \\nabla_{b_2}L \\

再:

\ optimizer.step() \\

于是:

\ W_1,b_1,W_2,b_2 \\

发生一点变化。

下一批数据进来时,模型已经不是原来的模型。

重复成千上万次以后:

\ \\theta_0 \\rightarrow \\theta_1 \\rightarrow \\theta_2 \\rightarrow \\cdots \\rightarrow \\theta\^\* \\

这就是目前最底层意义上的:

模型从 Demonstration 中学到了 Observation → Action 的关系。


第五课自测

  1. nn.Linear(10, 64) 中,PyTorch 的 weight.shape 和 bias.shape 分别是多少?

  2. 为什么:

\ \[B,10 \]

经过 nn.Linear(10,64) 后变成:

\ \[B,64 \]

但 \(B\) 不发生变化?

  1. nn.Linear(10,64) 一共有多少个可训练参数?

  2. 一个:

    nn.Linear(64, 7)

又有多少参数?

  1. 如果:

\ W:\[64,10 \]

那么:

\ W.grad \\

的 Shape 应该是多少?为什么?

  1. model.parameters() 为什么必须传给 Optimizer?

  2. model(obs) 为什么最终会调用我们定义的 forward()?

  3. 现在请用自己的话解释:

\ \\theta \\

在神经网络里到底是什么。

相关推荐
goujunwe1 小时前
电商品牌 GEO:让 AI 在消费者对比产品时,采信你的品牌信息
大数据·人工智能
XZ-0700011 小时前
week7-文本
python
Rocky Ding*1 小时前
Emu3大模型深度解析:下一Token预测如何统一图像、视频与理解,以及它尚未解决的问题
论文阅读·人工智能·深度学习·机器学习·aigc·ai-native·emu3
IT大白鼠1 小时前
彭大帅的AI运维助手跨出 Linux:全面接管 Windows 服务器
运维·服务器·人工智能
Tingmanyi1 小时前
YOLOv13改进策略【卷积层篇】| RFAConv 感受野注意力卷积,每个位置都值得被单独加权
深度学习·算法·yolo·目标检测·计算机视觉
聪明蛋子哟1 小时前
大模型工程化全景实战:打通Prompt、RAG、Tool Calling与跨系统集成的任督二脉
人工智能·prompt
Web3&Basketball1 小时前
给Agent上线前加一道自动化稳定性闸门
深度学习·大模型·ai技术
Java后端的Ai之路1 小时前
01-React基础教程
开发语言·前端·python·react.js·前端框架
阿明副业观察1 小时前
动漫AI视频创作工具在哪找到的?2026年最新动漫AI视频平台与软件指南
人工智能·ai作画·aigc·音视频·ai写作