📑 本页目录(点开跳转)
挑战项目 C · 过拟合实验室
⏱ 5–7 天 | 难度 ★★★★★ | 前置:第 5、10、11 章
🎯 为什么选这个项目
前两个挑战是「造东西」,这个是「造故障」。
常规学习:跟着教程做对 → 遇到真实故障时两眼一抹黑
这个项目:故意做错 10 次 → 每种故障的"手感"刻进肌肉记忆
你要亲手制造并诊断:
💀 训练 loss 变 NaN
💀 loss 完全不动
💀 CV 分数 0.95,测试集 0.60
💀 换个种子结果差 10%
💀 验证 loss 比训练还低
💀 GPU 利用率只有 20%
... 共 10 种
然后为每一种写出【症状 → 诊断步骤 → 修复】
为什么这个能力最值钱:教程教你成功路径,但真实工作 80% 的时间在处理故障。 而故障诊断几乎无法从书上学——只能靠见过。这个项目让你在一周内"见过"十次。
🧪 十个必做实验
每个实验的流程都一样:制造故障 → 观察症状 → 用第 11 章流程诊断 → 修复 → 记录
A 组:训练不动(第 9、11 章)
-
[ ] 实验 1 · NaN 制造机 把 lr 调到 10,观察 loss 怎么爆。记录:第几步开始 NaN?梯度范数怎么变? 再试另外三种造 NaN 的方法:
log(0)、除以 0、数据里混inf -
[ ] 实验 2 · 梯度断流 故意在前向里写
x = x.detach(),或用.item()参与计算。 观察:loss 完全不动。练习:用打印梯度的方法定位到是哪一层断的 -
[ ] 实验 3 · 忘记 zero_grad 注释掉
optimizer.zero_grad(),看 loss 曲线变成什么样。这个症状要能一眼认出来 -
[ ] 实验 4 · 梯度消失 搭一个 20 层的全连接网络,全用 Sigmoid 激活。 打印每层的梯度范数 → 亲眼看到指数衰减。然后换 ReLU + 残差连接,对比
B 组:虚假的高分(第 5 章)⭐ 最重要的一组
-
[ ] 实验 5 · 预处理泄漏 用全量数据
fitscaler / 做特征选择,对比放进 Pipeline 的版本。 在纯噪声数据上做,差距会触目惊心(第 5 章那个 76.5% vs 47.5%) -
[ ] 实验 6 · 时间泄漏 造一个带时间戳的数据集,用随机切分 vs 按时间切分,对比 CV 分数和"上线"表现 (用最后 20% 的时间段模拟线上)
-
[ ] 实验 7 · 分组泄漏 造一个"同一用户多条记录"的数据集,用 KFold vs GroupKFold,看分数差多少
-
[ ] 实验 8 · 目标泄漏 往特征里偷偷加一个标签的衍生物(如
y + 微小噪声),观察: CV 分数飙到 0.99,但特征重要性会暴露它 —— 练习怎么用重要性排查泄漏
C 组:不可信的结论(第 5、11 章)
-
[ ] 实验 9 · 种子彩票 同一配置跑 20 个随机种子,画分数直方图。 记录极差。然后思考:你以前相信过的哪些"提升"其实在这个范围内?
-
[ ] 实验 10 · 测试集慢性污染 模拟"反复在测试集上调参":调 30 轮超参,每轮都看测试集分数,选最好的。 然后拿一个全新的测试集验证 —— 看之前那个"最好"掉多少
🔍 十种故障的「症状指纹」(做之前先别看,做完对照)
👀 点开对照(建议自己先做出来再看)
| # | 故障 | 一眼识别的特征 |
|---|---|---|
| 1 | 学习率过大 | loss 前几步先降后暴涨,然后 NaN。梯度范数指数增长 |
| 2 | 梯度断流 | loss 一动不动(不是降得慢,是逐位相同);p.grad 是 None 或全 0 |
| 3 | 忘 zero_grad |
loss 前期正常,随后越来越震荡;梯度范数随 step 线性增长(梯度在累加)⭐ |
| 4 | 梯度消失 | 后几层梯度正常,前几层小 3~6 个数量级;前几层的权重几乎不变 |
| 5 | 预处理泄漏 | CV 分数高但测试掉一大截;在纯噪声数据上仍能跑出 >60% ⭐ 这是决定性证据 |
| 6 | 时间泄漏 | 随机 CV 很高、按时间切分骤降;未来信息被用来预测过去 |
| 7 | 分组泄漏 | KFold 远高于 GroupKFold;同一实体的记录分散在训练和验证里 |
| 8 | 目标泄漏 | CV 突然飙到 0.99+;特征重要性里一个特征占 90% 以上 ⭐ |
| 9 | 种子彩票 | 同配置多次运行分数分布很宽;"提升"落在这个宽度里就不是提升 |
| 10 | 测试集污染 | 你选的"最优配置"在全新测试集上明显掉分 |
💡 注意 #3 和 #1 的区别:都是 loss 变糟,但 忘 zero_grad 是"梯度范数随 step 线性增长"(累加), lr 过大是"指数增长"(正反馈)。打印梯度范数就能分开。
🧪 每个实验的统一记录格式(六个数字)
import torch
def log_step(model, loss, opt):
gn = sum(p.grad.norm()**2 for p in model.parameters()
if p.grad is not None) ** 0.5
return {
"loss": loss.item(),
"grad_norm": gn.item(), # ⭐ 最有信息量的一个
"param_norm": sum(p.norm()**2 for p in model.parameters()) ** 0.5,
"lr": opt.param_groups[0]["lr"],
"n_dead": sum((p.grad == 0).all().item() # 有多少层完全没梯度
for p in model.parameters() if p.grad is not None),
"has_nan": bool(torch.isnan(loss)),
}
🔑
grad_norm是这个项目里最重要的一个数字。 十个故障里有五个能靠它一眼分辨(爆炸、消失、断流、累加、正常)。 养成每次训练都记它的习惯。
📓 产出:一份故障诊断手册
每个实验记录成这个格式:
## 故障 N:<名字>
### 症状(我观察到什么)
- loss 曲线:...
- 梯度范数:...
- 训练/验证差距:...
### 复现代码
```python
# 最小复现
诊断路径(我是怎么找到原因的)
- 先查了 X,排除
- 打印 Y,发现异常
- 定位到 Z
根因
修复
一眼识别的特征 ⭐ 最有价值的部分
「如果下次看到 ___,八成就是这个问题」
---
## 🧠 ADHD 任务切分
- [ ] **Day 1** 搭实验脚手架:一个能快速切换"故障开关"的训练脚本 + 统一的日志记录(第 11 章的六个数字)
- [ ] **Day 2** A 组实验 1–4(训练不动)
- [ ] **Day 3–4** B 组实验 5–8(虚假高分)⭐ 花最多时间在这
- [ ] **Day 5** C 组实验 9–10(不可信结论)
- [ ] **Day 6–7** 整理成诊断手册 + 写「我以前踩过但没意识到的坑」
### 脚手架建议
```python
BUGS = { # 故障开关,一行切换
"none": lambda cfg: cfg,
"huge_lr": lambda cfg: {**cfg, "lr": 10.0},
"no_zero_grad": lambda cfg: {**cfg, "zero_grad": False},
"leak_scaler": lambda cfg: {**cfg, "fit_scaler_on_all": True},
# ... 其余变体照这个形状加
}
def run(bug="none"):
cfg = BUGS[bug](DEFAULT_CFG)
log = train(cfg) # 返回每 epoch 的六个数字
plot(log, title=bug) # 存图
return log
✅ 通关标准
- 10 个故障全部复现成功,每个都有最小复现代码
- 每个故障有完整诊断路径(不是直接写答案,是记录你怎么找到的)
- 每个故障有「一眼识别的特征」 —— 这是手册的核心价值
- 实验 9 得出了你的噪声基准,并说明它如何改变你对"提升"的判断
- 实验 10 量化了测试集污染的代价
🏆 加分
- 让别人(或另一个 AI)随机注入一个故障,你只看日志来诊断 —— 盲测
- 把手册做成一棵决策树:「loss 是 NaN 吗?→ 是 → 查这三项…」
- 加实验 11:分布漂移 —— 训练集和测试集来自不同分布时会怎样
💭 做完之后
你会发现自己看别人的训练日志时,能一眼看出不对劲。 这个能力没法从教程学,只能靠"见过"。你用一周换来了通常要一年才能积累的故障直觉。
🔗 这和智能体工程挑战项目 B的精神一致: 优化的前提是能正确测量;而能正确测量的前提,是知道测量会怎么骗你。
✅ 检查点
- 这个项目和前两个挑战的根本区别是什么?为什么这个能力最值钱?
- 「忘了 zero_grad」和「学习率过大」都会让 loss 变糟,怎么一眼区分?
- 为什么说「在纯噪声数据上仍能跑出 >60%」是预处理泄漏的决定性证据?
- 目标泄漏在特征重要性上会留下什么痕迹?
grad_norm为什么是这个项目里最重要的数字?- 实验 9(种子彩票)的产出是什么?它怎么改变你对"提升"的判断?
- 实验 10 在模拟什么真实工作中的错误?
- B 组为什么最重要?
👀 答案
- 前两个是造东西,这个是造故障。最值钱是因为:教程只教成功路径,但真实工作 80% 时间在处理故障,而故障诊断几乎无法从书上学,只能靠见过——这个项目让你一周内见过十次。
- 看梯度范数的增长方式:忘 zero_grad 是线性增长(梯度在累加);lr 过大是指数增长(正反馈)。
- 因为纯噪声数据的真实可分性是 50%(就是随机)。任何显著高于 50% 的分数只可能来自泄漏——没有别的解释。这是唯一能"证明"泄漏存在的实验设计。
- 特征重要性里一个特征占 90% 以上,且 CV 突然飙到 0.99+。这是排查泄漏最快的手段。
- 因为十个故障里有五个能靠它一眼分辨:爆炸(指数增长)、消失(前层小几个数量级)、断流(为 0 或 None)、累加(线性增长)、正常。
- 产出是你的噪声基准(同配置 20 个种子的分数极差)。之后任何"提升"如果落在这个宽度里,就不是提升,是随机波动。
- 模拟反复在测试集上调参——每轮都看测试集分数并据此选择,测试集就慢慢变成了"第二个验证集",报告的分数是乐观的。这是真实工作中最普遍也最隐蔽的错误。
- 因为 A 组和 C 组的故障你能看见(loss 不动、NaN),而 B 组的故障看起来像成功——分数很高,你会高高兴兴上线,然后崩。看不见的错误才是最贵的。
🛑 可以停在这里
⚡ 走神救援
故意造 10 种故障再诊断,把手感刻进肌肉记忆(前两个挑战是造东西,这个是造故障——故障诊断无法从书上学,只能靠见过)。A组训练不动(NaN/梯度断流/忘zero_grad/梯度消失);B组虚假高分(最重要——因为A、C组的故障你看得见,B组的故障看起来像成功):预处理泄漏/时间泄漏/分组泄漏/目标泄漏;C组不可信结论:种子彩票(跑20个种子得噪声基准,之后落在这个宽度里的"提升"都不算)、测试集慢性污染。⭐ grad_norm 是最重要的数字,十个故障有五个靠它一眼分辨:爆炸=指数增长、忘zero_grad=线性增长(在累加)、断流=0或None、消失=前层小几个数量级。纯噪声数据上跑出>60% 是泄漏的决定性证据(真实可分性只有50%);目标泄漏的痕迹=一个特征占重要性90%以上。产出=一份诊断手册,核心是每个故障的「一眼识别特征」。加分:让别人随机注入故障做盲测。
🎓 三个挑战都做完之后
- A(手搓框架):向下 —— 看清框架黑箱
- B(四种解法对决):向外 —— 选型判断力
- C(过拟合实验室):向内 —— 故障诊断直觉
这三个 + 第 17 章的三个基础项目,是一份能证明"基本功扎实"的作品集。
回 首页 看进度,或去全景导论第 16 章选下一个方向。