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

17 · 实战项目

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


🎯 一句话

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

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


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

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

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


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

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

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

任务清单

🔨 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 分数—— 这个分数已经被你"选"出来了,是乐观的。 正确做法:留一个从头到尾没碰过的测试集,最后只用一次。

✅ 通关标准

🎁 加分


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

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

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

任务清单

🔑 关键提示

# 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%

✅ 通关标准

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


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

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

练什么:第 12、15 章。

任务清单

🔨 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 倍 ⭐

🎯 该看到的现象

   CIFAR10 全量(50000 张):
   A 从零小CNN     ~70%
   B 冻结主干      ~75%      ← 领先不多
   C 全微调        ~93%   ⭐

   只用 100 张训练图:
   A 从零小CNN     ~25%      ← 几乎崩了
   B 冻结主干      ~65%   ⭐  ← 差距拉开到 40 个点!
   C 全微调        ~60%      ← 反而不如 B(数据太少,微调会过拟合)

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

✅ 通关标准

🎁 加分


📝 项目 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 张数据不足以支撑全微调,会过拟合。分界大概在每类几百张。

🛑 完成了?

三个高难度挑战在等你:


走神救援

三规矩:每次只改一件事、每步记表格、先跑通全流程再优化(卡住超25分钟就跳过回头补)。三项目:①表格全流程(Pipeline+CV+特征工程+调参,灵魂是诚实性检查:换3个种子看波动,提升有没有超过噪声;⭐最隐蔽的坑是"调参的验证集=报告分数的验证集",必须留一个从没碰过的测试集)②纯numpy手写神经网络(硬标准=梯度检验<1e-6;80%时间耗在形状上——记住"梯度形状=对应量的形状";NaN=学习率大/softmax没减最大值/log(0);不动=没更新/lr太小/ReLU全死。参考数字:Adam 50轮 vs SGD 200轮,精度95-97%)③图像迁移学习(必须用ImageNet的归一化参数否则掉5-10%;全微调主干lr要小10倍否则冲垮预训练权重;⭐关键实验:只用100张图时冻结(65%) > 全微调(60%) > 从零(25%)——数据少就冻结,数据多才全微调)。README里"踩过的坑"最加分。

打卡记录保存在你的浏览器里,首页能看到总进度