autograd、优化器与过拟合:训练这台机器的自动化与自检
这一章讲三件事: 手工求导怎么被 PyTorch 自动化(autograd);「怎么更新参数」怎么被抽象成可插拔的优化器;以及深度学习中最重要的分辨力——训练损失下降不等于学会,以及怎么用一个举动看穿它。 位置:第 04 章给了机制的「手动挡」,这一章换成自动挡,并装上仪表盘。
1. 为什么要自动化:手工推导的天花板
第 04 章我们手工推了梯度:模型两行、损失两行,求导链一目了然。但那是两个参数的玩具。真实网络有几百万个参数、几十层复合函数,手工推一遍导数——书里的原话是:「不是特别有趣,也不快」1。
更根本的是:只要模型里每个零件都是可微的,那么「损失对每个参数的导数」在数学上永远存在、形式永远一样——沿链式法则回乘。 一件永远机械的事,就不该由人做。这就是 autograd 存在的理由。
2. autograd:张量会记家谱
打开开关只要一个参数2:
params = torch.tensor([1.0, 0.0], requires_grad=True)
requires_grad=True 告诉 PyTorch:凡是祖先里有这个张量的后续张量,都把「自己是怎么算出来的」记下来。 前向计算照常进行,但暗地里,每步运算都在一张计算图(computation graph:把「谁由谁经过什么运算算出来」画成的一张有向图)上多记一个节点2。
主走查还是那支温度计,但换成自动版3:
① t_p = model(t_u, w, b) # 前向:照常算出预测,暗处记下「×w」「+b」两个节点
② loss = loss_fn(t_p, t_c) # 再记「−t_c」「²」「mean」三个节点
③ loss.backward() # 一行:从 loss 沿图反向走,链式法则一路乘回叶子
④ params.grad → tensor([4517.2969, 82.6000])
图说:第 ④ 步的两个数,和第 04 章手工推公式算出来的那两个一模一样。
手工两小时,机器一行。
backward()(反向传播)就是从损失出发、沿计算图逆着走,把每一步的局部导数按链式法则连乘,最后把「损失对某个叶子参数的变化率」写到那个参数的 .grad 属性上。从此,换模型不再意味着重推公式——改成神经网络、改成卷积、改成 Transformer,backward 的写法一个字不变。 这是 PyTorch 全部魔法的内核。
全库最有名的坑:梯度是累加,不是覆盖
书里用 大写 WARNING 标记的坑4:
每次
backward(),梯度是加到.grad上,不是覆盖。 第二轮训练时如果不清零,新一轮的梯度会叠在上一轮的上面,更新量立刻错掉。
所以训练循环里永远有一行 params.grad.zero_()(或优化器替你做的 zero_grad())。为什么不自动清?因为有些高级用法故意要跨多步累加梯度(比如显存放不下大批量时,分几小批累加再统一更新)——PyTorch 选择了灵活性,把清零的责任留给你4。
配套的闸:no_grad
反过来,有些时候你不想记图:比如用验证集评估时,只算前向、不求导,记图纯属浪费内存。with torch.no_grad(): 块里的计算不建图5。第 04 章的「更新参数」那步也要包在里面——改叶子参数这件事本身不该进图。
3. 优化器:把「怎么更新」换成可插拔零件
现在循环里还剩一段手工代码:params -= learning_rate * params.grad。「按梯度反方向挪」只是所有更新策略里最简单的一种;PyTorch 把它抽象成优化器(optimizer):构造时把参数列表交给它,之后每轮只要三行6:
optimizer.zero_grad() # 清旧梯度
loss.backward() # 算新梯度
optimizer.step() # 读 .grad,按自己的策略更新参数
SGD(随机梯度下降,Stochastic Gradient Descent)就是我们手写的那个策略;「随机」指的是梯度不是用全部数据算的,而是每次随机抽一小批(minibatch,小批量)来估计——算法本身和全量版一模一样,只是输入的梯度带噪声7。这个噪声不是缺陷:第 07 章会看到,小批量带来的抖动反而帮优化跳出局部的小坑。
另一个是 Adam:给每个参数各维护一个自适应的学习率,梯度大的参数步子自动调小。书里演示了一个震撼的对比:把第 04 章那个会发散的场景原样交给 Adam——输入不归一化、学习率开到 1e-1,它眼都不眨,2000 轮收敛到同样的答案8。
判断(我们的,不是书里的): 「先 SGD 后 Adam」是教学顺序,不是工程建议。真实项目里今天多数人直接 Adam 起步(书里在 part 2 用 SGD+momentum 是出于可控性,见第 13 章);该带走的是「优化器可换、循环不变」这个结构,不是某个具体选择。 如果错,会错在: 如果读者做的是对收敛行为极敏感的研究复现,SGD 系与 Adam 系的泛化(在没见过的新数据上也表现好,而不只是背下训练数据)差异是真实存在的;但对入门和多数应用,差异不影响本章结论。
4. 过拟合:它把数据背下来了
开普勒的第 ④ 步,现代版
第 04 章埋了条线:开普勒留了一半数据不参与拟合,专供验证。现在收线。为什么要留?因为一个足够灵活的模型,可以把训练数据的每个点都穿得严丝合缝,却在点与点之间胡乱扭动——损失只奖励「点上穿得过」,管不到点外的行为9。
训练点: × × × × ×
|~~~~~|~~~~~~|~~~~~~|~~~~~| ← 灵活的模型:损失≈0,但波浪形穿过
|_____|______|______|_____| ← 简单模型(直线):损失略高,但点间行为正常
图说:两个模型在训练点上几乎一样好;差别全在没有数据的地方。
这种现象叫过拟合(overfitting):模型在训练集上越来越好,在没见过的数据上越来越差——它把训练样本连同噪声一起背了下来,而没有学到背后的规律9。它的反义词是泛化(generalization):在没见过的数据上照样表现好——这才是学习的真正目标,因为我们训模型从来是为了用在明天的数据上。
怎么看见它:训练/验证两条曲线
做法就是开普勒那步:打乱数据,80% 训练、20% 验证;训练循环里同时算两个损失,但只对训练损失调 backward——验证集永远不产生梯度10:
Epoch 1 Training 66.58 Validation 142.39
Epoch 500 Training 7.15 Validation 9.13
Epoch 3000 Training 3.01 Validation 3.58
图说:两条曲线一起降、验证略高 —— 这是健康的样子。下一段的四格表会给它一个名字。
书里给了一张四格图,值得背下来当诊断手册10:
| 曲线形态 | 诊断 |
|---|---|
| A:两条都不降 | 没学到:模型太简单(欠拟合——连训练数据都还没学会,和「背下来了」正相反),或数据里根本没有可用信息——拿气压计读数预测温度,换什么模型都没用 |
| B:训练降、验证升 | 过拟合确诊,该停手了 |
| C:两条几乎重合地降 | 理想,还有余量 |
| D:都降、验证略高 | 正常,继续 |
书里的温度计跑出来正是 D:训练 3.01、验证 3.58,同期同向10。
还有一道防线:测试集与泄漏
验证集也不是终极裁判:你拿它调了十轮结构、挑了八次学习率,它就被「用旧了」——你的决策已经间接适应了它。所以还有第三份测试集:全程封存,模型定稿后才开封用一次,作为无偏的最终评估11。而比用旧更隐蔽的事故是数据泄漏(data leakage):训练数据里混进了「考试答案」——比如同一病人的扫描同时进了训练集和验证集——评估结果会虚假地高。泄漏一经发现,之前所有指标(衡量成绩的那些数)都要作废11。
5. 作者的判断与证据
| 说法 | 性质 |
|---|---|
backward() 自动版与手工版结果一致(4517.2969, 82.6000) | 有据,两组数字书里并列打印3 |
| 梯度累加是坑也是特性 | 有据+设计说明,书里给了「故意跨步累加」的正当场景4 |
| Adam 对未归一化输入鲁棒(条件变了也不垮)的演示 | 有据,但只是这个小玩具上;别把这里的结论推广成「Adam 不需要归一化」8 |
| 「验证集用旧了也会过拟合」 | 领域共识,书里以直陈句给出,无单独实验11 |
| 「训练/验证损失怎么分叉怎么读」四格图 | 经验法则,全书后续章节(尤其第 14 章)会拿它反复实战检验10 |
6. 边界与局限
- 计算图按「前向一次」临时搭建、backward 时消费掉——这意味着每次迭代(一轮一轮的训练步)的图可以不一样(if/循环随数据变也行),这是 PyTorch 的灵活;但也意味着图本身有开销,大模型上不可忽略5。
- 本章的优化器对比是玩具规模;真实项目里学习率调度(训练途中按日程调整学习率的做法)、batch 大小与优化器的相互作用要到第 13、14 章才有战场感。
- 「欠拟合/过拟合」二分忽略了第三种常见失败:数据本身没信息(气压计例子)——两条曲线都躺平,别只会调模型。
7. 可带走的
- autograd = 张量记家谱 +
backward()沿图回乘;换模型不改训练代码。 - 梯度是累加的:每轮先
zero_grad(),忘了就是 bug。 no_grad包住一切不求导的计算:验证、推理、参数更新。- 优化器三行式:zero_grad → backward → step;策略可换,循环不动。
- SGD 的 S 来自小批量抽样;Adam 给每个参数自适应步长,对尺度不敏感。
- 训练损失降 ≠ 学会;训练降+验证升 = 过拟合,停。
- 欠拟合 = 模型太简单或数据没信息——先分清是哪一种再动手。
- 测试集只用一次;数据泄漏一旦坐实,既往指标全部作废。
8. 原文地图
| 主题 | 原书章 | 原文位置 |
|---|---|---|
| 手工推导不可扩展 | ch5 | text/13-ch05-5-the-mechanics-of-learning.txt:845(搜「millions of parameters」) |
| requires_grad 与计算图 | ch5 | text/13-ch05-5-the-mechanics-of-learning.txt:880(搜「requires_grad=True」) · :910(搜「computation graph」) |
| 梯度累加 WARNING | ch5 | text/13-ch05-5-the-mechanics-of-learning.txt:917(搜「accumulate, not store」) · :919(搜「Calling backward」) |
| autograd 版训练循环 | ch5 | text/13-ch05-5-the-mechanics-of-learning.txt:934(搜「params.grad.zero_」) |
| no_grad 与更新细节 | ch5 | text/13-ch05-5-the-mechanics-of-learning.txt:956(搜「no_grad」) |
| 优化器抽象 | ch5 | text/13-ch05-5-the-mechanics-of-learning.txt:1010(搜「optim submodule」) · :1049(搜「zero_grad and step」) |
| SGD 的 stochastic | ch5 | text/13-ch05-5-the-mechanics-of-learning.txt:1059(搜「stochastic」) |
| Adam 演示 | ch5 | text/13-ch05-5-the-mechanics-of-learning.txt:1152(搜「Adam」) · :1157(搜「won’t even blink」) |
| 过拟合定义与四格图 | ch5 | text/13-ch05-5-the-mechanics-of-learning.txt:1206(搜「overfitting」) · :1239(搜「underfitting」) · :1375(搜「case C is ideal」) |
| 训练/验证切分与结果 | ch5 | text/13-ch05-5-the-mechanics-of-learning.txt:1291(搜「randperm」) · :1356(搜「Validation loss 142.3890」) |
| 测试集与数据泄漏 | ch5 | text/13-ch05-5-the-mechanics-of-learning.txt:1399(搜「test set」) · :1406(搜「data leakage」) |
| 两张计算图互不干扰 | ch5 | text/13-ch05-5-the-mechanics-of-learning.txt:1433(搜「separate computation graph」) |