跳到主要内容

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. 可带走的

  1. autograd = 张量记家谱 + backward() 沿图回乘;换模型不改训练代码。
  2. 梯度是累加的:每轮先 zero_grad(),忘了就是 bug。
  3. no_grad 包住一切不求导的计算:验证、推理、参数更新。
  4. 优化器三行式:zero_grad → backward → step;策略可换,循环不动。
  5. SGD 的 S 来自小批量抽样;Adam 给每个参数自适应步长,对尺度不敏感。
  6. 训练损失降 ≠ 学会;训练降+验证升 = 过拟合,停
  7. 欠拟合 = 模型太简单或数据没信息——先分清是哪一种再动手。
  8. 测试集只用一次;数据泄漏一旦坐实,既往指标全部作废。

8. 原文地图

主题原书章原文位置
手工推导不可扩展ch5text/13-ch05-5-the-mechanics-of-learning.txt:845(搜「millions of parameters」)
requires_grad 与计算图ch5text/13-ch05-5-the-mechanics-of-learning.txt:880(搜「requires_grad=True」) · :910(搜「computation graph」)
梯度累加 WARNINGch5text/13-ch05-5-the-mechanics-of-learning.txt:917(搜「accumulate, not store」) · :919(搜「Calling backward」)
autograd 版训练循环ch5text/13-ch05-5-the-mechanics-of-learning.txt:934(搜「params.grad.zero_」)
no_grad 与更新细节ch5text/13-ch05-5-the-mechanics-of-learning.txt:956(搜「no_grad」)
优化器抽象ch5text/13-ch05-5-the-mechanics-of-learning.txt:1010(搜「optim submodule」) · :1049(搜「zero_grad and step」)
SGD 的 stochasticch5text/13-ch05-5-the-mechanics-of-learning.txt:1059(搜「stochastic」)
Adam 演示ch5text/13-ch05-5-the-mechanics-of-learning.txt:1152(搜「Adam」) · :1157(搜「won’t even blink」)
过拟合定义与四格图ch5text/13-ch05-5-the-mechanics-of-learning.txt:1206(搜「overfitting」) · :1239(搜「underfitting」) · :1375(搜「case C is ideal」)
训练/验证切分与结果ch5text/13-ch05-5-the-mechanics-of-learning.txt:1291(搜「randperm」) · :1356(搜「Validation loss 142.3890」)
测试集与数据泄漏ch5text/13-ch05-5-the-mechanics-of-learning.txt:1399(搜「test set」) · :1406(搜「data leakage」)
两张计算图互不干扰ch5text/13-ch05-5-the-mechanics-of-learning.txt:1433(搜「separate computation graph」)

Footnotes

  1. 出处:「5 The mechanics of learning」第 845 段(text/13-ch05-5-the-mechanics-of-learning.txt:845,搜「millions of parameters」)。原文:哪怕模型有几百万参数,只要可微,求梯度就是「把导数的解析式写出来再算一遍」——但写那个式子「不怎么有趣,也不快」。

  2. 出处:「5 The mechanics of learning」第 880 段(text/13-ch05-5-the-mechanics-of-learning.txt:880,搜「requires_grad=True」)与第 910 段(:910,搜「computation graph」)。requires_grad 让 PyTorch 追踪「params 的所有后代张量」;前向时创建 autograd 计算图,backward 时反向遍历。 2

  3. 出处:「5 The mechanics of learning」第 900 段(text/13-ch05-5-the-mechanics-of-learning.txt:900,搜「loss.backward」)与第 905 段(:905,搜「4517.2969」)。autograd 算出的梯度 tensor([4517.2969, 82.6000]) 与第 04 章手工推导完全一致。 2

  4. 出处:「5 The mechanics of learning」第 917 段(text/13-ch05-5-the-mechanics-of-learning.txt:917,搜「accumulate, not store」)与第 919 段(:919,搜「Calling backward」)。原文加粗警告:「调用 backward 会让导数在叶子节点上累加;用完必须显式清零」;注记解释为何默认不清:有些模型故意跨迭代累加梯度。 2 3

  5. 出处:「5 The mechanics of learning」第 956 段(text/13-ch05-5-the-mechanics-of-learning.txt:956,搜「no_grad」)与第 1454 段(:1454,搜「building the graph in the first place」)。不 backward 就不该建图;torch.no_grad 关掉追踪,大模型上省内存省时间。 2

  6. 出处:「5 The mechanics of learning」第 1010 段(text/13-ch05-5-the-mechanics-of-learning.txt:1010,搜「optim submodule」)与第 1049 段(:1049,搜「zero_grad and step」)。优化器持有参数引用;每个优化器暴露 zero_grad 与 step 两个方法。

  7. 出处:「5 The mechanics of learning」第 1059 段(text/13-ch05-5-the-mechanics-of-learning.txt:1059,搜「stochastic」)。原文:SGD 之名来自梯度通常在随机抽出的小批量上估计;优化器本身不知道你喂的是全量还是小批量,算法完全一样。

  8. 出处:「5 The mechanics of learning」第 1152 段(text/13-ch05-5-the-mechanics-of-learning.txt:1152,搜「Adam」)与第 1157 段(:1157,搜「won’t even blink」)。Adam 自适应学习率、对参数尺度不敏感:用原始未归一化的 t_u、学习率 1e-1,2000 轮收敛到 w≈0.537、b≈−17.30。 2

  9. 出处:「5 The mechanics of learning」第 1206 段(text/13-ch05-5-the-mechanics-of-learning.txt:1206,搜「overfitting」)与第 1253 段(:1253,搜「meandering its way through the data points」)。原文:高适应力模型会把损失在数据点上压到极低,「但我们没法保证它在数据点之间表现好」——这种现象叫过拟合。 2

  10. 出处:「5 The mechanics of learning」第 1291 段(text/13-ch05-5-the-mechanics-of-learning.txt:1291,搜「randperm」)、第 1331 段(:1331,搜「no val_loss.backward」)与第 1375 段(:1375,搜「case C is ideal」)。温度计 80/20 切分,3000 轮后训练 3.01/验证 3.58;四格图(原书图 5.14)A=没学到、B=过拟合、C=理想(ideal)、D=可接受(acceptable)。 2 3 4

  11. 出处:「5 The mechanics of learning」第 1399 段(text/13-ch05-5-the-mechanics-of-learning.txt:1399,搜「test set」)与第 1406 段(:1406,搜「data leakage」)。反复用验证集做决策也会对它过拟合;测试集只在模型完全定型后用一次;泄漏会让评估「乐观得不真实」。 2 3