阶段:S3-模型构建 | 预计耗时:60 分钟 | 前置:S2 全部验收通过 代码:本篇代码要你自己敲成
model-learn/s3_model_anatomy.py
学习目标
- 理解
nn.Module的两个槽位:__init__放积木(层)、forward写连线 - 逐层追踪 MNIST 小模型的形状变化链,能手推任意一层的输出 shape
- 会数参数量,并与
01_mnist_cnn.py的 206,922 对账
核心知识
1. __init__ 与 forward 的分工
class Net(nn.Module):
def __init__(self): # 备料:声明有哪些层(每层自带可训练参数)
super().__init__()
self.conv1 = nn.Conv2d(1, 16, kernel_size=3, padding=1)
...
def forward(self, x): # 连线:数据按什么顺序流过这些层
x = torch.relu(self.conv1(x))
...
print(model)直接打印这份"积木清单"sum(p.numel() for p in model.parameters())数出总参数量——跑实验前先数参数,模型大小直接决定你 CPU 训练多久
2. 常用积木速查
| 积木 | 作用 | 有参数吗 |
|---|---|---|
nn.Linear(in, out) |
全连接:矩阵乘法 | 有(W、b) |
nn.Conv2d(cin, cout, k) |
卷积:扫空间特征 | 有 |
nn.ReLU |
非线性"弯一下" | 无 |
nn.MaxPool2d(2) |
尺寸减半、留最强响应 | 无 |
nn.Flatten |
摊平成向量 | 无 |
nn.Dropout(p) |
训练时随机丢一部分(S4 的正则化药) | 无 |
nn.BatchNorm2d(c) |
稳定每层数值分布(S8 CIFAR 会用) | 有 |
3. 形状追踪(本实验模型,padding=1)
输入 (N,1,28,28)
→ conv1(pad=1) (N,16,28,28) 尺寸不变
→ MaxPool2d(2) (N,16,14,14)
→ conv2(pad=1) (N,32,14,14)
→ MaxPool2d(2) (N,32,7,7)
→ Flatten (N,1568) 32*7*7 —— Linear 的 in_features 就是这么算的
→ Linear(1568,128) → ReLU → Linear(128,10) (N,10)
推 shape 的规则:conv(k,p) 不改尺寸 iff p = (k-1)/2;pool(2) 尺寸减半。
实验任务
任务 1:重建并解剖(25 分钟)
把 01_mnist_cnn.py 的 SmallCNN 原样抄成 s3_model_anatomy.py,加三件事:
print(model) # 积木清单
print("参数总量:", sum(p.numel() for p in model.parameters())) # 对账 206,922
x = torch.randn(2, 1, 28, 28) # 逐层追踪
with torch.no_grad():
for name, layer in [("conv1", model.conv1), ("pool1", model.pool1)]:
x = layer(x); print(f"过 {name} 后: {tuple(x.shape)}")
# ……接着把 conv2 / pool2 / flatten / fc1 / fc2 全部追完
(层名以你机器上的 01_mnist_cnn.py 实际属性名为准。)
任务 2:故意输错形状(15 分钟)
把输入改成 (2, 28, 28)(少了通道维)跑一次,读 traceback——记下报错里最关键的一句。以后遇到 shape 报错,你该去哪里看?
任务 3:参数量手算(15 分钟)
- conv1: 1×16×3×3 + 16 = 160;conv2: 16×32×3×3 + 32 = 4640
- fc1: 1568×128 + 128;fc2: 128×10 + 10
- 手算总和 = ?(应与程序一致)——能算才能改,S3-02 消融实验就靠它
验收标准
衔接下一阶段
模型会解剖了 → S3-02 模型消融实验:拆掉积木看性能变化。