跳到主要内容

数据截至 (上游 commit c187ef3271d5)

一个模型的完整诞生 — 从一张表到一个能用的模型

这一章讲一件事: 第 01 章那个「预测 → 比 → 调」的循环, 在真实项目里要被一整套工程件包起来——数据怎么变成张量、代码怎么组织、 模型怎么存、以及最关键的:怎么知道它是真学会了,而不是把答案背下来了。 书里明说:这一章重点不在回归,而在模型训练的所有「周边设施」1。 后面十三章的每一个实战,用的都是这一章搭好的这套架子。

1. 顶层全景:一条流水线,七道工序

先用一张图看清这一章在干什么。原料是一张 11000 行、19 列的表格 (Kaggle 上的「社交焦虑数据集」,每行一个人,目标是预测他 1–10 分的焦虑程度)2; 成品是一个能预测新数据的模型。中间七道工序:

表格(11000×19)
→ ① 类别列拆成 0/1 列 19 列变 31 列
→ ② 看数据(画图找关系) 睡眠越少,焦虑越高
→ ③ 数值缩放到同一量级 只拿训练集的统计量
→ ④ 搭模型 + 训练循环 从全手写,到全框架
→ ⑤ 分批喂数据 一次 512 行
→ ⑥ 切开训练集/验证集 80% / 20%
→ ⑦ 存下训练成果 只存权重,不存结构

图说:①②③ 管数据,④⑤ 管训练,⑥⑦ 管「信不信得过」。本章的主走查就是把这七步完整走一遍。

2. 数据先出场:类别列怎么变成数

模型只认数。这份数据里有好几列是文字:性别、职业、是否吸烟。 直觉做法是把类别编成号——黄色 = 1、绿色 = 2、红色 = 3。

在说这是不是个坑之前,先分个档 —— 书自己分了,而这一档决定了做法3: 类别有两种,一种本来就没有高低(喜欢的颜色、职业), 另一种本来就有高低(学历:小学 < 中学 < 大学)。

对后一种,编号恰恰是对的做法 —— 那个顺序是真的,编成 1、2、3 反而把它保住了。 坑只在前一种: 编号隐含着顺序,模型会当真——它会以为「绿色比黄色大 1、 红色是黄色的 3 倍」,而颜色之间根本没有这种关系,这套大小是凭空捏造的3

对没有顺序的那一种,正确的做法是把这一列拆成「每类一列」:只有黄/绿/红三种颜色, 就拆成三列,是哪种颜色就在哪列写 1、其余写 0。这种做法叫 one-hot 编码 (每行只有一个位置是「热」的 1)。书里还点了一个省列技巧:三列可以只留两列, 因为「前两列都是 0」本身就等于第三列4

代价也要知道:列数会变多。这份数据 one-hot 之后从 19 列变成 31 列5。 类别特别多的列(比如「城市」有几百个值)会让表格膨胀—— 这是这种编码的天然边界,第 06 章会给出另一条路。

3. 看数据与缩数据:两件上手前必做的事

先画关系图。 书里把「睡眠时长」和「焦虑程度」画成散点,趋势很直白: 睡眠越少,焦虑越高6。再进一步,把所有数值列两两之间的关联程度算出来画成热图, 一眼看出「睡眠」和「压力水平」跟焦虑最相关。

这里书里特意按了一次刹车:关联程度是 ±1 之间的一个数, 它只说明两个量同涨同跌,不说明谁导致谁7。这个提醒后面会反复用到。

再把各列缩到同一量级。 各列的单位天差地别:咖啡因一天几百毫克, 周运动时长却只有几小时。数值悬殊的列会让训练不稳—— 大数值会把第 01 章说的「调整信号」冲爆8。所以要把每列换成 「离均值几个标准差——标准差就是「这一列的数散开有多远」的尺子—— 全部变成均值为 0、标准差为 1 的数。

最关键的一条工程纪律:均值和标准差只用训练集算, 再拿这两个数去缩验证集。 书里把这叫避免数据泄漏—— 如果你拿全部数据的统计量去缩,等于提前偷看了验证集长什么样, 评估结果会好看得不真实9。 leakage 这个词本书后面不再出现, 但这条纪律每个项目都该执行。

4. 从全手写到 nn.Module:把零件一件件交还给框架

第 01 章的走查是全手写的:自己乘矩阵、自己减梯度。这一章把它升级成框架写法, 升级路径本身就是教学安排——先看清每个零件,再把它交给框架

手写版的核心三行,和第 01 章一模一样:预测 = 矩阵乘法 + 偏置, 损失用 MSE,loss.backward() 算梯度,再手动 w -= lr × w.grad10

升级版把模型写成一个类,继承框架的 nn.Module,必须实现两个方法: __init__()(把要用的层建出来)和 forward()(规定数据怎么流过这些层)11。 写法固定成这样:

class LinearRegression(torch.nn.Module):
def __init__(self, input_size, output_size):
super().__init__()
self.linear = torch.nn.Linear(input_size, output_size) # 建层
def forward(self, x):
return self.linear(x) # 数据流

图说:类是「蓝图」——结构写一次;之后喂任何数据,它照同一套结构算。

跟着升级的还有两处。一是更新参数不再需要手写减法: optimizer.step() 一步搞定,optimizer.zero_grad() 一键清零—— 优化器换成 Adam(第 01 章说的自适应步长的那种)12。 二是书里在这里正式给了名字:超参数——训练开始前由人定死、 训练中不跟着数据变的那些设定,比如学习率、每批喂多少条、训练多少轮13。 它们的对面是参数:参数是训练自己学出来的,超参数是你拍板定的。

效果立竿见影:同样 100 轮,手写版一路缓慢下降,框架版在前 40 轮就陡降、 然后走平——自适应步长在前期敢迈大步14

5. 批量与 DataLoader:为什么要一小口一小口喂

到这一步,每轮都是把 11000 行一次全喂进去。真实项目里这常常做不到,也不该做。 书里给了三个改成分批喂的理由15:

  1. 装不下——大图、视频数据集根本塞不进内存;
  2. 算得快——一小份一小份地送,可以同时算多份(同时做多件事,行话叫并行);
  3. 学得反而好——小批量算出来的调整方向带点抖动, 这种抖动恰好能把模型从「假谷底」(第 01 章的局部最优)里晃出来。

每一小份叫一个批量(batch)——就是每次喂给模型的一小份数据。 批量大小本身也是个超参数。 书里的经验:从 2 的幂起步(16、32、64),图像任务常用 32 到 512, 语音和语言模型常用 8 到 256——但终究要试,没有万能值16

分批之后还有一堆琐事:每轮要不要打乱顺序、数据在后台预取…… 框架用两个类把它们收编:Dataset 负责「第 i 条数据是什么」 (必须写三个方法:初始化、总数、按序号取一条)17, DataLoader 负责「按批量、打乱、后台加载地」把 Dataset 里的数据送出来18。 收编的好处书里点得很准:换数据集时,训练循环一行都不用改19

6. 存模型:只存砖,不存房子

模型练完,总不能每次用都重练。存什么?书里用了一个很准的比方20:

训练好的模型 = 一栋盖好的房子
模型类 = 建筑蓝图(结构)
权重 = 一块一块的砖(数值)

搬家 = 把砖运走 + 到新地方照蓝图重砌

图说:存盘只存「砖」(权重数值),「蓝图」(模型类)本来就在你的代码里。

实现上,model.state_dict() 把模型里所有权重打包成一个有序字典 (键是层名,值是张量),torch.save(..., 'Model1.pth') 存成 .pth 文件21。 加载时反过来:先照蓝图建一个空模型,再 load_state_dict() 把砖填进去, 看到 <All keys matched successfully> 就是严丝合缝22

7. 切分与过拟合:怎么知道它不是背答案

这是全章最值钱的一节。先看现象:训练损失一路降到底,模型就好吗? 书里的比方一针见血——像一个只背答案的学生:旧题全对,换个问法就傻23

这种「把训练数据背下来」的状态,叫过拟合:训练数据上误差极小, 没见过的数据上一塌糊涂。

它的反面叫欠拟合:连训练数据都没学好,比如拿一条直线去硬套一条抛物线。

夹在中间的理想状态叫泛化:学到的是规律本身,所以对没见过的数据也准24

诊断办法是把数据切成两份(有时三份):训练集用来学, 验证集只用来考——每轮训练末尾,用验证集算一次损失(只算不调)25。 两条曲线一对比,病机立现:

损失 │ 训练 ─╲___
│ 验证 ─╲__╱── ← 验证损失掉头向上处 = 开始背答案了
└──────────────────→ 轮

图说:训练损失一直降不重要,验证损失何时开始回升才重要。 (怎么在这条曲线上自动刹车、连模型都自动存下最好的那一份,第 13 章讲。)

切多大?书里的经验值:两份切 80/20,三份切 70/15/15; 数据越少、模型越复杂,验证集越不能省26。还有一条变体叫交叉验证: 把数据切成 K 份(常取 5 或 10),轮流拿每一份当验证集、训 K 次取平均—— 更稳,但训练量翻 K 倍,本书因此没再用它27

最后把第 3 节那条纪律接上:切分之后,先用训练集算出缩放参数, 再拿它去缩验证集——顺序不能反28

8. 主走查:11000 行数据,走完七道工序

把第 1 节那张流程图落成真实数字(全部来自书里):

① 表格 11000×19 ──one-hot──→ 11000×31(多了 12 个 0/1 列)
② 拆掉目标列 ──→ X:(11000, 30),y:(11000, 1)[^29]
③ 标准化:每列 → 均值 0、标准差 1(统计量只来自训练集)
④ 全手写训练:100 轮,损失 19.94 → 1.57[^30]
⑤ 换框架写法(模型类 + Adam):前 40 轮陡降,然后走平
⑥ 切分:8800 行训练 / 2200 行验证,两个 DataLoader
⑦ 训练末尾存 state_dict → Model1.pth

图说:这份数据上,训练与验证两条曲线几乎重合——书明说这是这份数据的特例,别当成常态29

练出来的模型多好?用一个数说:R² = 0.6530。 R² 读作「目标的变化里,有多大比例能被输入特征解释」: 0 是完全解释不了,1 是全解释——但书里警告,1 在实践中几乎不可能, 真看到了反而要怀疑是过拟合;而 0.65 的意思是: 焦虑分数的波动,约三分之二能被睡眠、压力这些特征解释, 剩下的三分之一下落不明31它好不好取决于领域: 书里原话,有的领域 0.98 都算差,有的领域 0.4 已经很好。

9. 书里的立场与证据

  • 「小批量有助泛化」(第 5 节第 3 条)——这是深度学习社区的共识级经验, 书里只给直觉解释(抖动晃出假谷底),没给实验证据。信它,但别当成定理;
  • R² 的三条告诫(1 可疑、高不代表因果、好坏看领域)——都是统计学的标准告诫,可靠;
  • 批量大小、切分比例的经验值——作者明说是 best practice(经验法则), 不是推导出来的。和第 01 章学习率的经验值同一种性质:当成起点,不当成答案。

10. 边界与局限

  • 这一章的模型是线性回归——30 个特征各乘一个权重加总。 它能抓住「睡眠越少焦虑越高」这种单调关系,抓不住「过了某个坎突然恶化」这种弯; 弯的关系要靠第 03 章起带隐藏层的网络;
  • 交叉验证只讲了做法,本书后续所有项目都没用——小数据集上它其实更稳, 真做项目时值得捡回来;
  • 「数据泄漏」只讲了缩放这一种入口;实战中泄漏还有很多别的入口 (比如切分前去重、按时间排序的数据随机切),本书没展开;
  • R² 只适用于回归;分类任务要用另一套度量,正是第 03 章的全部内容。

11. 可带走的

全章那条流水线,一行写完: 11000×19 的表 → one-hot 成 31 列 → 拆出 X(11000,30) → 只用训练集统计量标准化 → 模型类 + Adam 练 100 轮(损失 19.94→1.57) → 8800/2200 切分验证 → R² 0.65 → state_dict 存成 .pth。

  1. 没有顺序的类别列不能编号,要 one-hot——编号会骗模型学出一套凭空捏造的大小关系; 本来就有顺序的那一种(学历这类)编号反而是对的,顺序是真的;
  2. 相关不等于因果,±1 的相关系数只说明同涨同跌;
  3. 标准化的统计量只能用训练集算——这是防数据泄漏的第一课;
  4. 先手写一遍训练循环,再交还给框架——__init__ 建层、forward 定数据怎么流;
  5. 超参数是人定的(学习率、批量大小、轮数),参数是学出来的,别混;
  6. 分批喂数据:装得下、算得快、还顺带防背题;从 2 的幂起步;
  7. Dataset 管「第 i 条是什么」,DataLoader 管「怎么送」——换数据不改训练循环;
  8. 存模型 = 只存权重(砖),结构在模型类(蓝图)里;
  9. 训练损失降不算数,验证损失不升才算数——过拟合就是背答案;
  10. R² = 0.65 读作「三分之二的波动可解释」;看到 R² ≈ 1 先怀疑,别庆祝。

12. 原文地图

主题原书章原文位置
本章定位(周边设施)Chapter 2text/04-ch02-chapter-2.txt:23(搜「less about regression」)
数据集Chapter 2text/04-ch02-chapter-2.txt:32(搜「more than 10,000 samples」) · text/04-ch02-chapter-2.txt:112(搜「anxiety.shape: (11000, 19)」)
类别分无序 / 有序两档Chapter 2text/04-ch02-chapter-2.txt:130(搜「unordered (e.g., favorite colors) and ordinal data」)
one-hot 与假顺序Chapter 2text/04-ch02-chapter-2.txt:179(搜「no implicit order」) · text/04-ch02-chapter-2.txt:183(搜「counted twice as much」) · text/04-ch02-chapter-2.txt:202(搜「(11000, 31)」)
EDA 与相关≠因果Chapter 2text/04-ch02-chapter-2.txt:232(搜「anxiety levels increase as hours of sleep decrease」) · text/04-ch02-chapter-2.txt:279(搜「this correlation is causal」)
标准化与数据泄漏Chapter 2text/04-ch02-chapter-2.txt:326(搜「avoid data leakage」)
手写训练循环Chapter 2text/04-ch02-chapter-2.txt:400(搜「blindfolded」) · text/04-ch02-chapter-2.txt:409(搜「a local minimum」)
模型类两方法、超参数框Chapter 2text/04-ch02-chapter-2.txt:633(搜「two methods: init() and forward」) · text/04-ch02-chapter-2.txt:610(搜「Hyperparameters are constants」)
Adam 与损失曲线Chapter 2text/04-ch02-chapter-2.txt:677(搜「Adam as the optimizer」) · text/04-ch02-chapter-2.txt:741(搜「level off around epoch 40」)
批量的三个理由与经验值Chapter 2text/04-ch02-chapter-2.txt:780(搜「fit completely into the RAM」) · text/04-ch02-chapter-2.txt:790(搜「better generalization」) · text/04-ch02-chapter-2.txt:800(搜「powers of 2」)
Dataset/DataLoaderChapter 2text/04-ch02-chapter-2.txt:952(搜「three methods」) · text/04-ch02-chapter-2.txt:922(搜「shuffling」) · text/04-ch02-chapter-2.txt:996(搜「slower CPU」)
存取模型Chapter 2text/04-ch02-chapter-2.txt:1082(搜「brick by brick」) · text/04-ch02-chapter-2.txt:1100(搜「state_dict」) · text/04-ch02-chapter-2.txt:1176(搜「All keys matched successfully」)
切分比例与交叉验证Chapter 2text/04-ch02-chapter-2.txt:1227(搜「80% training data and 20%」) · text/04-ch02-chapter-2.txt:1280(搜「folds are 5 or 10」)
过拟合学生比方Chapter 2text/04-ch02-chapter-2.txt:1310(搜「memorizes answers」)
scaler 顺序Chapter 2text/04-ch02-chapter-2.txt:1369(搜「scaler.transform(X_val)」)
两曲线重合是特例Chapter 2text/04-ch02-chapter-2.txt:1526(搜「almost congruent」)
R² 0.65 与告诫Chapter 2text/04-ch02-chapter-2.txt:577(搜「R-squared: 0.65」) · text/04-ch02-chapter-2.txt:562(搜「red flag」) · text/04-ch02-chapter-2.txt:565(搜「75% of the variance」)

Footnotes

  1. 出处:「Chapter 2 Creating Your First PyTorch Model」第 23 段(text/04-ch02-chapter-2.txt:23,搜「less about regression」)。原文:这一章较少关乎回归本身,更多关乎模型训练的所有周边设施。

  2. 出处:「Chapter 2」第 32 段(text/04-ch02-chapter-2.txt:32,搜「more than 10,000 samples」)与第 112 段(text/04-ch02-chapter-2.txt:112,搜「anxiety.shape: (11000, 19)」)。

  3. 出处:「Chapter 2」第 183 段(text/04-ch02-chapter-2.txt:183,搜「counted twice as much」)与第 120-131 段(text/04-ch02-chapter-2.txt:130,搜「unordered (e.g., favorite colors) and ordinal data」)。原文两处:编 1/2/3 会让算法以为绿色是黄色的两倍、红色是黄色的三倍,这没有道理;而在这之前,原文把类别数据又分成两类 —— 无序的(例子给的是「喜欢的颜色」)和有天然顺序的(例子给的是学历、学位)。原书只在无序那一类上讲了 one-hot,对有序那一类没有再说做法。 2

  4. 出处:「Chapter 2」第 176-184 段(text/04-ch02-chapter-2.txt:179,搜「no implicit order」)。pd.get_dummies(drop_first=True) 就是「省一列」的开关。

  5. 出处:「Chapter 2」第 202 段(text/04-ch02-chapter-2.txt:202,搜「(11000, 31)」)。

  6. 出处:「Chapter 2」第 232 段(text/04-ch02-chapter-2.txt:232,搜「anxiety levels increase as hours of sleep decrease」)。

  7. 出处:「Chapter 2」第 279 段(text/04-ch02-chapter-2.txt:279,搜「this correlation is causal」)。原文:只能说明有相关,不能据此判定因果。

  8. 出处:「Chapter 2」第 315-320 段(text/04-ch02-chapter-2.txt:315,搜「Data scaling」)。原文:大数值会让反向传播中的梯度「爆炸」,小数值会让梯度消失,都会让训练不稳。

  9. 出处:「Chapter 2」第 326 段(text/04-ch02-chapter-2.txt:326,搜「avoid data leakage」)。原文:缩放参数(均值和标准差)只应在训练数据上计算,再应用到验证集和测试集。

  10. 出处:「Chapter 2」第 425-463 段(text/04-ch02-chapter-2.txt:425,搜「The loop always runs through the same steps」)。七步:前向、损失、backward、更新、清零、记录、打印。

  11. 出处:「Chapter 2」第 633 段(text/04-ch02-chapter-2.txt:633,搜「two methods: init() and forward」)。

  12. 出处:「Chapter 2」第 677 段(text/04-ch02-chapter-2.txt:677,搜「Adam as the optimizer」)与第 692-694 段(optimizer.step() / optimizer.zero_grad())。

  13. 出处:「Chapter 2」第 610 段(text/04-ch02-chapter-2.txt:610,搜「Hyperparameters are constants」)。原文框:超参数是程序运行中不变的常量,训练前由开发者依据优化流程或最佳实践手动定义;最常用的三个是学习率、批量大小、轮数。

  14. 出处:「Chapter 2」第 741 段(text/04-ch02-chapter-2.txt:741,搜「level off around epoch 40」)。

  15. 出处:「Chapter 2」第 780 段(text/04-ch02-chapter-2.txt:780,搜「fit completely into the RAM」)与第 790 段(text/04-ch02-chapter-2.txt:790,搜「better generalization」)。原文第三条:中小批量带来的梯度更强波动,能把模型带出局部最小,提升泛化。

  16. 出处:「Chapter 2」第 800 段(text/04-ch02-chapter-2.txt:800,搜「powers of 2」)与第 807 段(text/04-ch02-chapter-2.txt:807,搜「batch sizes between 32」)。语音处理 8-256,transformer 模型因自身占内存多而偏好小批量。

  17. 出处:「Chapter 2」第 952 段(text/04-ch02-chapter-2.txt:952,搜「three methods」)。三个方法:__init____len____getitem__

  18. 出处:「Chapter 2」第 922 段(text/04-ch02-chapter-2.txt:922,搜「shuffling」)与第 996 段(text/04-ch02-chapter-2.txt:996,搜「slower CPU」)。后一处讲了「数据加载在 CPU、计算在 GPU,CPU 可能成瓶颈」——所以 DataLoader 要后台预取。

  19. 出处:「Chapter 2」第 1071-1072 段(text/04-ch02-chapter-2.txt:1071,搜「any changes to the training loop」)。

  20. 出处:「Chapter 2」第 1082 段(text/04-ch02-chapter-2.txt:1082,搜「brick by brick」)。原文:把房子拆成一块块砖,再写一份施工说明;蓝图就是模型类,砖就是权重。

  21. 出处:「Chapter 2」第 1100 段(text/04-ch02-chapter-2.txt:1100,搜「state_dict」)。惯例扩展名 .pt 或 .pth。

  22. 出处:「Chapter 2」第 1176 段(text/04-ch02-chapter-2.txt:1176,搜「All keys matched successfully」)。书里还对比了加载前后的权重直方图:加载前权重都挤在零附近,加载后铺开、偏置接近 4。

  23. 出处:「Chapter 2」第 1310 段(text/04-ch02-chapter-2.txt:1310,搜「memorizes answers」)。

  24. 出处:「Chapter 2」第 1287-1313 段(text/04-ch02-chapter-2.txt:1289,搜「learns the training data too precisely」)。过拟合的定义:模型把训练数据学得太精确,连噪声和个别特性也一并背下。

  25. 出处:「Chapter 2」第 1206-1216 段(text/04-ch02-chapter-2.txt:1206,搜「Training dataset」)。三份时,测试集只在最后用来做一次无偏评估。

  26. 出处:「Chapter 2」第 1227 段(text/04-ch02-chapter-2.txt:1227,搜「80% training data and 20%」)。影响因素:数据量、模型复杂度、时序数据不可随机切、类别不平衡。

  27. 出处:「Chapter 2」第 1280 段(text/04-ch02-chapter-2.txt:1280,搜「folds are 5 or 10」)与第 1282-1285 段。原文:计算开销显著增加,所以本书后面不再用,但你该知道它。

  28. 出处:「Chapter 2」第 1369 段(text/04-ch02-chapter-2.txt:1369,搜「scaler.transform(X_val」)。fit_transform 用在训练集,transform 用在验证集——这个顺序就是第 3 节那条纪律的代码形态。

  29. 出处:「Chapter 2」第 1526 段(text/04-ch02-chapter-2.txt:1526,搜「almost congruent」)。

  30. 出处:「Chapter 2」第 577 段(text/04-ch02-chapter-2.txt:577,搜「R-squared: 0.65」)。

  31. 出处:「Chapter 2」第 562 段(text/04-ch02-chapter-2.txt:562,搜「red flag」)与第 565 段(text/04-ch02-chapter-2.txt:565,搜「75% of the variance」)。原文:R² 接近 1 要当红旗看,可能是过拟合;0.75 读作 75% 的方差被解释;高 R² 不代表因果。