主题
第 3 章 让小网络学会预测
自动求导引擎不会自己知道什么叫 “预测正确”。我们还需要定义模型、目标和衡量误差的办法。三者接好以后,训练就是反复运行程序、求梯度、改参数。先用四条记录看清这个闭环,比直接面对成千上万行数据容易检查。
3.1 神经元:带参数的加权求和
输入向量
权重决定不同输入对结果的影响,偏置负责平移阈值。tanh 把输出限制在 -1 到 1 之间,并引入非线性。如果所有层都只有仿射变换,那么
沿用课程的单神经元示例:
若把输入也当作待求导变量,
第二个权重的梯度为零,因为这一条记录的第二个输入为零。这不表示权重永远没有作用,换一条记录它就可能收到梯度。课程神经元实例
3.2 从一个神经元到一层,再到 MLP
一层包含多个神经元:每个神经元都接收相同输入,但有各自独立的参数。输入长度为 3,一层有 4 个神经元,输出就是长度 4 的列表。再把它交给下一层,逐层继续。
MLP 是 multilayer perceptron,即多层感知机。本书 MLP([3,4,4,1]) 的参数数量可以直接数出来。第一层每个神经元有 3 个权重、1 个偏置,共
python
class Neuron:
def __call__(self, x):
if len(x) != len(self.w):
raise ValueError('Input width mismatch')
return sum((w*v for w, v in zip(self.w, x)), self.b).tanh()输入长度检查是本书添加的防错。zip 会在较短的列表结束时停止,不检查长度的话,输错维度可能悄悄丢掉部分输入。另一个小设计是每层始终返回列表,包括只有一个神经元时。课程用单元素时返回标量的简便接口;它在结尾方便,但若中间层只有一个神经元,需要谨慎处理下层仍期待序列的问题。
parameters() 将每层、每个神经元的权重和偏置摊平成一个列表。优化器不需要知道某个参数属于哪一层,只需把它的数值按梯度改动。它们必须是原有 Value 的引用,不能新建一套相同数值的对象,否则前向图和更新对象会脱节。
3.3 用一个数表达 “预测得多差”
本实验使用课程后半 notebook 的四条三维输入与目标
平方使正负误差都产生非负惩罚,并让较大误差受罚更多。对一个预测值的导数是
这时可以画出完整损失图,理解参数为何连接许多路径。但图很快会变大。课程的 Graphviz 工具先遍历可达节点,再为每个数值画矩形、运算画小节点,并显示 data 与 grad。它是调试显示,不参与求导。实际大模型通常观察层级形状和统计量,而不是打印每次标量加法。
平方误差 “之和” 与 “均值” 相差样本数。若把这里改为均值,同一个学习率下梯度会缩小四倍。比较训练代码时,先核对损失的归约方式,再判断学习率是否 “太小”。
3.4 四步循环,每一步都能出错
python
for step in range(100):
loss = sum((model(x)[0] - y)**2 for x, y in zip(xs, ys))
loss.backward()
for parameter in model.parameters():
parameter.data -= 0.05 * parameter.grad本书引擎会在 backward 内清空可达梯度,所以上面不另外清零;使用课程引擎时,应在 loss.backward() 前循环执行 p.grad=0.0。每步都重新前向构图,因为参数已经改变,旧图上的数值和导数不再代表新的模型。
先做前向,得出当前预测和损失;再清空上次累积的梯度;然后反向算新梯度;最后更新参数。这四步次序不要靠记口号,要检查各步读写什么。更新发生在反向之前,会让一些简化引擎读到错误的参数值。清零发生在反向之后,会把刚算好的梯度抹掉。忘了清零,则旧批次与新批次的贡献混在一起,训练算法已经变了。
在随机初始化上,不宜让同层神经元的入边、出边以及优化器状态都保持完全相同的置换对称;在这个条件下,它们会收到相同梯度,并一直相同。仅仅入边权重相同还不足以下这个结论,因为不同的出边权重可能传回不同梯度。偏置统一为零没有这个问题,因为不同的随机权重已经打破了神经元之间的对称。本实验使用 random.Random(42) 生成均匀分布的权重,偏置为零。
3.5 真实小实验与它没有证明的事
运行 python examples/part1/scalar_engine.py,本书环境 Python 3.12.14 的实际记录如下:
| 检查项 | 结果 |
|---|---|
| 参数数 | 41 |
| 更新前平方误差之和 | 5.278844 |
| 100 次更新后平方误差之和 | 0.010930 |
| 四条预测 | 0.9653,-0.9375,-0.9562,0.9376 |
这些数字证明,这个具体实现可以把四条训练数据拟合得更好。它没有证明模型学到了可推广规律,因为没有拿新数据测试。输出层用了 tanh,有限输入时输出不会恰好等于 ±1;为了越来越接近这些目标,预激活可能需要更大的绝对值,同时梯度越来越小。后面分类任务用 logits 和交叉熵,会有更合适的输出解释。
如果损失没有下降,先固定一小批数据反复训练。连少量样本都拟合不了,优先排查标签、参数是否收到梯度、参数更新是否实际发生和学习率。不要立即加深网络掩盖代码错误。
3.6 notebook 与完整 micrograd 库有什么不同
课程后半 notebook 使用 tanh 神经元和四点平方误差演示。独立 micrograd 库 则提供 Module 基类、zero_grad、隐藏层 ReLU 与线性输出层。它的 demo notebook 使用双月形二维数据和最大间隔损失:
这里
demo 还用规则网格采样二维平面,将每个网格点的分数正负画成决策边界。那张图回答模型在哪里预测哪类;训练准确率只是数对了多少条,两者不能互相代替。本书没有声称复现该 demo 的双月形训练结果,前面的数值只来自明确记录的四点实验。
练习与解答
补充题一:把最后一层改成两个神经元,参数数会是多少?前两层仍为 36,最后一层变成
补充题二:若入边、出边、偏置和优化器状态都使同层神经元完全对称,训练会自动让它们分工吗?在确定性计算下不会,两个神经元会接收相同梯度。写一段程序比较它们每一步的权重,是比 “随机性很重要” 更具体的检查。
补充题三:故意删掉参数更新,再跑 100 步。每次构图和求导都可以正常完成,但损失不应改善。这个反例区分了 “能求导” 与 “真的在学习”。
来源与下一步
本章完整覆盖 第一讲后半 notebook 的神经元、层、MLP、参数收集、四条数据、训练与预测单元;另读取固定版本 micrograd 的 engine.py、nn.py、测试和 demo,解释实现差异。接下来仍然是 “模型、损失、梯度、更新” 这个闭环,但数据变成字符,输出变成一组概率,标量列表则换成张量。