🏠 总目录📚 本教程 17 · 实战项目 ← →
📑 本页目录(点开跳转)

17 / 一次完成一个小任务

先完成一个小闭环,
不必一次做完所有项目。

选择一个项目,留下可重复的基线、一次有依据的改动和比较结果。

先抓住一个具体结果

第一份产出:一张基线记录

写下数据切分、指标、最简单模型的分数和随机种子。后面的改进都和它比较;没有提升的实验,只要记录清楚,也有价值。

接下来,按需要选一项

你现在想看什么?

这些入口是选择,不是必须按顺序完成的任务。

可以停在这里

选择一个项目,留下可重复的基线、一次有依据的改动和比较结果。

需要更多细节时,继续看完整正文 →
继续探索

原有正文、图解和例子都在下方。按需跳转,不必一次读完。

17 · 实战项目

⏱ 三个项目,每个 1–2 天 | ⭐ 不做项目 = 没学过


🎯 一句话

三个递进项目:表格数据全流程 → 手写神经网络 → 图像迁移学习。做完你就有了完整的手感。

💡 全部不需要 GPU,笔记本就能跑。


🧠 做项目之前:三条能救你很多时间的规矩

规矩 为什么
每次只改一件事 ⭐ 同时改模型和特征,涨了你不知道是谁的功劳,掉了也不知道回滚哪个
每一步都记进表格 不记的话两小时后你会忘记"上一版是多少",然后重复实验
先跑通全流程再优化 哪怕 baseline 分数很烂。先有一条能从数据跑到提交的完整链路,比一个精雕细琢的半成品有用得多

💡 ADHD 友好的做法:每个 T 任务标了时间,做完一个就打勾并记一行分数。 卡住超过 25 分钟就跳到下一个 T,回头再补 —— 卡死在一个点上是最大的时间黑洞。


🥉 项目一:表格数据完整流程(1 天)

目标:在一个真实数据集上走完「数据 → 特征 → 模型 → 评估」,并证明自己没有自欺欺人。

练什么:第 4、5、16 章。

任务清单

  • 参考用时 · 30 分钟

    选数据集:sklearn 自带的 fetch_california_housing(回归)或 load_wine(分类)。 先看数据:形状、缺失、分布、标签平衡度
  • 参考用时 · 45 分钟

    搭 Pipeline(标准化 + 模型),跑一个最简 baseline(逻辑回归/线性回归), 记下 CV 分数 ± 标准差
  • 参考用时 · 45 分钟

    换 GBDT,对比。只改模型,不改别的
  • 参考用时 · 60 分钟

    特征工程:加 3–5 个构造特征(比值、分箱、交叉),每加一个跑一次 CV,记录变化
  • 参考用时 · 45 分钟

    调参:先调 max_depth/num_leaves,再调 learning_rate + n_estimators(第 4 章)
  • 参考用时 · 30 分钟

    ⭐ 诚实性检查(这一步才是这个项目的灵魂)

🔨 T2 的骨架(照抄就能开始)

from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import cross_val_score, KFold

cv = KFold(5, shuffle=True, random_state=0)

def score(model, X, y, name):
    s = cross_val_score(model, X, y, cv=cv, scoring="r2")   # 或 "roc_auc"
    print(f"{name:24s} {s.mean():.4f} ± {s.std():.4f}")
    return s.mean(), s.std()

score(make_pipeline(StandardScaler(), LinearRegression()), X, y, "baseline")

🎯 T6 诚实性检查清单

流程图

① 同一配置换 3 个随机种子跑→分数的波动有多大?
② 你的"提升"有没有超过这个波动?
· 提升 0.003,种子间波动 0.015→❌ 这不是提升,是噪声
③ 所有预处理都在 Pipeline 里吗?(标准化、填充、PCA、编码)
④ 有没有用到"预测时拿不到"的特征?(第 5 章的数据泄漏)
⑤ 你调参用的验证集,和你报告分数用的是同一个吗?→如果是,分数虚高 ⭐

🔑 第 ⑤ 条最隐蔽:你用 CV 调了 50 组参数,选了最好的,然后报告那个 CV 分数—— 这个分数已经被你"选"出来了,是乐观的。 正确做法:留一个从头到尾没碰过的测试集,最后只用一次。

✅ 通关标准

  • 有一张「baseline → +特征 → +调参」的分数演进表,每行带 ± 标准差
  • 能说出哪些改动的提升超过了噪声,哪些没有
  • Pipeline 里没有任何在 CV 外部做的预处理

🎁 加分

  • 画学习曲线,判断该加数据还是改模型(第 5 章)
  • 用 Permutation Importance 而非自带 importance 看特征贡献 (自带的对高基数特征有偏,第 16 章讲过)
  • 看一眼预测最差的 20 个样本——它们有共同点吗?那就是下一个特征的线索 ⭐

🥈 项目二:手写神经网络(1–2 天)

目标:不用 PyTorch,纯 numpy 实现一个多层网络并训练到收敛。

练什么:第 7、8、9、10 章 —— 这个项目做完,反向传播就再也忘不掉了。

任务清单

  • 参考用时 · 60 分钟

    实现前向:Linear → ReLU → Linear → Softmax,能跑通形状
  • 参考用时 · 90 分钟

    实现反向传播(第 8 章的模板扩展到多层 + 多分类)
  • 参考用时 · 45 分钟

    ⭐ 梯度检验:数值梯度 vs 解析梯度,相对误差必须 < 1e-6。不通过就别往下走
  • 参考用时 · 60 分钟

    加 mini-batch 训练循环 + 交叉熵损失, 在 load_digits(8×8 手写数字,sklearn 自带)上训练
  • 参考用时 · 60 分钟

    加两个优化器:SGD+Momentum 和 Adam,对比收敛曲线
  • 参考用时 · 45 分钟

    加正则:L2 和 Dropout,观察训练/验证 loss 差距的变化
  • 参考用时 · 30 分钟

    和 sklearn 的 MLPClassifier 对比精度,验证自己实现是对的

🔑 关键提示

# Softmax + 交叉熵的梯度(和第8章的 sigmoid 版一样干净)
import numpy as np
def softmax(z):
    z = z - z.max(axis=1, keepdims=True)      # ⭐ 数值稳定,防 exp 溢出
    e = np.exp(z)
    return e / e.sum(axis=1, keepdims=True)

# 前向:probs = softmax(logits)
# 反向:dlogits = (probs - y_onehot) / n      ← 就这么简单

# ReLU 的反向
dz = da * (z > 0)

⚠️ 形状对不上的自救表(这个项目 80% 的时间会耗在这)

信息关系

记住一条:【梯度的形状 = 它对应的那个量的形状】
W: (in, out)→dW 必须是 (in, out)
b: (out,)→db 必须是 (out,)
a: (batch, in)→da 必须是 (batch, in)
出错时打印出来对照:
print(name, "grad", g.shape, "param", p.shape)
现象 多半是
dW 形状反了 忘了转置:dW = a.T @ dz(不是 dz @ a.T)
db 是二维的 忘了对 batch 求和:db = dz.sum(axis=0)
loss 是 NaN ①学习率太大 ②softmax 没减最大值 ③log(0) → 加 1e-12 ⭐
loss 完全不动 ①忘了更新参数 ②学习率太小 ③梯度全是 0(ReLU 全死,初始化太小)
训练集精度只有 10% 就是随机猜(10 类)→ 大概率是标签和预测的对应错位了

🎯 该看到的数字(对照用)

结果对照

digits 数据集(1797 张 8×8),架构 64→64→10:
梯度检验相对误差 ~1e-8 ~ 1e-9 ✅(参考实现实测 1.49e-08)
SGD 收敛 ~200 epoch
Adam 收敛 ~50 epoch ← 明显更快 ⭐
测试精度 95% ~ 97%

✅ 通关标准

  • 梯度检验通过(相对误差 < 1e-6)—— 这是唯一的硬标准
  • 在 digits 上测试精度 > 95%
  • Adam 的收敛曲线明显快于 SGD
  • 加 Dropout 后训练/验证 gap 缩小

🔗 想更进一步 → 挑战 A 手搓 mini-torch, 那里要求你实现通用的自动微分,而不是写死的四层网络。 🔗 数学不放心 → 数学原理第 12 章 有完整推导。


🥇 项目三:图像迁移学习(1–2 天)

目标:用预训练模型做一个小型图像分类,体验「不从零训练」的威力。

练什么:第 12、15 章。

任务清单

  • 参考用时 · 45 分钟

    数据:torchvision.datasets.CIFAR10(自动下载,170MB)或自己找 3–5 类各 100 张图
  • 参考用时 · 30 分钟

    ⭐ 先跑第 0 步:10 张图能过拟合吗?(第 11 章)
  • 参考用时 · 60 分钟

    基线 A:从零训练一个小 CNN,记录精度和耗时
  • 参考用时 · 60 分钟

    基线 B:ResNet18 预训练 + 冻结主干只训分类头
  • 参考用时 · 60 分钟

    基线 C:ResNet18 预训练 + 全部微调(lr 小 10 倍)
  • 参考用时 · 45 分钟

    加数据增强(翻转、随机裁剪),看 B 和 C 各涨多少
  • 参考用时 · 30 分钟

    三个方案的对比表:精度 / 训练时间 / 数据需求

🔨 B 和 C 的关键代码差异(就这几行)

import torchvision, torch.nn as nn

m = torchvision.models.resnet18(weights="IMAGENET1K_V1")
m.fc = nn.Linear(m.fc.in_features, n_classes)     # 换分类头(两个方案都要)

# ---- 方案 B:冻结主干 ----
for p in m.parameters():
    p.requires_grad = False
for p in m.fc.parameters():
    p.requires_grad = True
opt = torch.optim.AdamW(m.fc.parameters(), lr=1e-3)

# ---- 方案 C:全微调(分层学习率)----⭐
opt = torch.optim.AdamW([
    {"params": [p for n, p in m.named_parameters() if not n.startswith("fc")],
     "lr": 1e-4},                                  # 主干:小 10 倍
    {"params": m.fc.parameters(), "lr": 1e-3},     # 新头:大
])

⚠️ 三个必踩的坑:

坑 后果 解法
忘了用预训练的归一化参数 精度莫名低 5~10% 必须用 ImageNet 的 mean=[0.485,0.456,0.406], std=[0.229,0.224,0.225] ⭐
冻结主干时忘了 bn.eval() BN 的 running stats 被新数据带跑 冻结时整个主干 .eval()
全微调用了和从零训练一样的学习率 把预训练权重直接冲垮,效果比 B 还差 主干 lr 小 10 倍 ⭐

🎯 该看到的现象

数据量决定该冻结,还是该全微调

三种训练策略在两种数据量下的准确率
训练数据从零训练小 CNN冻结主干全微调
CIFAR-10 全量(50,000 张)约 70%约 75%约 93%:最优
只用 100 张训练图约 25%:几乎崩了约 65%:最优约 60%:数据太少,容易过拟合

🔑 这三组数字回答了"什么时候冻结、什么时候全微调": 数据少 → 冻结(可训参数少,不容易过拟合); 数据多 → 全微调(能让特征适配你的领域)。 分界大概在每类几百张。

✅ 通关标准

  • 迁移学习(B/C)明显优于从零训练(A),且能解释为什么
  • 数据增强带来可测量的提升
  • 能说出「数据量多少时该冻结、多少时该全微调」

🎁 加分

  • 故意只用 100 张训练图,看三个方案的差距会不会拉大 (这才是迁移学习真正的价值,也是上表最有意思的一行)
  • 可视化第一层卷积核,看它学到了什么(边缘、颜色块)
  • 画混淆矩阵,看哪两类最容易混——通常是语义上真的相似的那两类

📝 项目 README 模板

# 项目名
## 问题定义(数据、任务、指标)
## 结果
| 方案 | CV 分数 | 测试分数 | 备注 |
|------|---------|----------|------|
| baseline | 0.812 ± 0.021 | 0.805 | |
| +特征工程 | 0.847 ± 0.018 | 0.841 | 提升超过噪声 ✅ |
| +调参 | 0.853 ± 0.019 | 0.845 | 提升在噪声内 ⚠️ |
## 关键决策与依据
## 踩过的坑  ← 最加分

💡 "踩过的坑"那一节别省。面试时它比分数有说服力得多—— 分数说明你会调,坑说明你懂原理。


✅ 检查点

  1. 做项目的三条规矩是什么?为什么"先跑通全流程"比"精雕细琢半成品"重要?
  2. 诚实性检查里最隐蔽的一条是哪个?正确做法是什么?
  3. 手写神经网络时,梯度形状怎么快速判断对不对?
  4. loss 变成 NaN 的三个常见原因?
  5. loss 完全不动的三个常见原因?
  6. 迁移学习为什么必须用 ImageNet 的归一化参数?
  7. 全微调时主干为什么要用更小的学习率?
  8. 数据只有 100 张时,冻结和全微调哪个更好?为什么?
👀 答案
  1. ①每次只改一件事(否则不知道是谁的功劳)②每步记进表格 ③先跑通全流程。因为一条完整链路能立刻告诉你瓶颈在哪,半成品连"到底哪一环最弱"都看不出来。
  2. 用来调参的验证集和报告分数的是同一个 —— 你从 50 组参数里选了最好的,那个分数已经被"选"出来了,是乐观的。正确做法:留一个从头到尾没碰过的测试集,最后只用一次。
  3. 梯度的形状 = 它对应的那个量的形状。W 是 (in,out) → dW 必须是 (in,out);b 是 (out,) → db 必须是 (out,)。
  4. ①学习率太大 ②softmax 没减最大值导致 exp 溢出 ③log(0),要加 1e-12。
  5. ①忘了更新参数 ②学习率太小 ③梯度全为 0(ReLU 全死 / 初始化太小)。
  6. 因为预训练权重是在那个归一化下学到的。输入分布一变,所有层的激活分布都偏了,精度会莫名低 5~10%。
  7. 因为预训练权重已经很好了,大学习率会直接把它冲垮,效果甚至不如只训分类头。分层学习率:主干 1e-4,新头 1e-3。
  8. 冻结更好(约 65% vs 60%)。因为可训练参数少得多,100 张数据不足以支撑全微调,会过拟合。分界大概在每类几百张。

🛑 完成了?

三个高难度挑战在等你:


⚡ 走神救援

先记住这几件事

  • 先跑通完整流程,再优化;每次只改一项并记下结果。
  • 表格项目把预处理放进 Pipeline,保留未参与调参的测试集,并检查种子波动。
  • 手写网络先做梯度检验;排查时核对形状、梯度和参数更新。
  • 迁移学习比较冻结与微调,检查预处理是否匹配;把实验条件和踩坑过程写进报告。
打卡记录保存在你的浏览器里,首页能看到总进度