
第四课标准答案先把上一课最后六个问题收掉然后直接沿着“参数 \(\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 算出来\[ grad2 \]当前 Batch 又得到\[ grad3 \]如果不清零可能得到\[ grad5 \]普通训练通常希望当前更新只依据当前 Batch所以先optimizer.zero_grad()把之前残留的梯度清掉。然后重新Forward → Loss → Backward → 当前Batch的Gradient → Update3. 模型真正“学到”的东西存在哪里在模型参数\[ \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\times7224 \]个预测数值需要比较但是常见 Loss 最后会把这些误差进行求和或求平均得到一个\[ \boxed{\text{scalar}} \]也就是标量。在 PyTorch 中经常看到loss.shape是[]或者把它打印出来就是0.03175. 6400 个 SampleBatch 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 \]最简单的关系可以写成\[ ywxb \]这里真正决定模型行为的是\[ w \]和\[ b \]例如\[ w2,\qquad b1 \]输入\[ x3 \]那么\[ y2\times317 \]如果把参数改成\[ w5,\qquad b-2 \]相同的输入\[ x3 \]就得到\[ y5\times3-213 \]你看输入完全没变。真正改变模型输出的是\[ w,b \]所以这两个东西就是Parameters模型参数。训练就是不断调整这些参数。二、为什么需要 \(b\)先只考虑\[ ywx \]如果\[ x0 \]那么无论 \(w\) 是多少\[ y0 \]但现实中我们可能希望\[ x0 \]时模型输出\[ y3 \]这时就可以加入\[ b \]变成\[ ywxb \]于是\[ x0 \]时\[ yb \]所以 \(b\) 可以理解成在输入经过权重变化之后再给输出增加一个可以学习的平移量。\(W\) 通常叫Weight权重\(b\) 叫Bias偏置三、从一个输入扩展到多个输入机器人 Observation 当然不可能只有一个数字。假设 Observation 有三个维度\[ x [x_1,x_2,x_3] \]现在我们想输出一个数字。可以写成\[ y x_1w_1x_2w_2x_3w_3b \]比如\[ x[2,1,3] \]权重\[ w[0.5,-1,2] \]偏置\[ b0.2 \]那么\[ y 2\times0.5 1\times(-1) 3\times2 0.2 \]所以\[ y1-160.26.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 \]就可以写成\[ yWxb \]于是\[ \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] \]七、为什么有时候教材又写 \(YXWb\)这个地方以后非常容易把你绕晕所以现在就把它说明白。数学教材里可能写\[ YXWb \]如果\[ X:[B,10] \]希望输出\[ Y:[B,64] \]那么这里的 \(W\) 应该是\[ W:[10,64] \]因为\[ [B,10]\times[10,64] [B,64] \]但是 PyTorch 实际存储layer.weight是\[ [64,10] \]所以 PyTorch 计算概念上更接近\[ YXW^Tb \]因为\[ W^T:[10,64] \]于是\[ [B,10]\times[10,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 \]完全可以训练。但它本质上只是\[ yWxb \]属于一个线性映射。而现实中的 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]\rightarrow[64] \]但它引入了非线性。ReLU 的细节我们现在不岔出去后面真正需要理解神经网络表达能力时再系统展开。十一、这个小网络到底有多少 Parameters现在来看nn.Linear(10, 64)Weight\[ 64\times10640 \]个参数。Bias\[ 64 \]个参数。所以第一层共有\[ 64064704 \]个参数。第二层nn.Linear(64, 7)Weight\[ 7\times64448 \]Bias\[ 7 \]所以\[ 4487455 \]整个网络总参数数\[ 7044551159 \]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(), lr1e-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 \]假设最简单的梯度下降学习率\[ \eta0.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 的关系。第五课自测nn.Linear(10, 64)中PyTorch 的weight.shape和bias.shape分别是多少为什么\[ [B,10] \]经过nn.Linear(10,64)后变成\[ [B,64] \]但 \(B\) 不发生变化nn.Linear(10,64)一共有多少个可训练参数一个nn.Linear(64, 7)又有多少参数如果\[ W:[64,10] \]那么\[ W.grad \]的 Shape 应该是多少为什么model.parameters()为什么必须传给 Optimizermodel(obs)为什么最终会调用我们定义的forward()现在请用自己的话解释\[ \theta \]在神经网络里到底是什么。