CNN 实战:MNIST
用 PyTorch 实现 SimpleCNN 并在 MNIST 上训练,与 MLP 对比参数量与精度。
从结构定义到训练实现
到上一节为止,CNN 的核心组件与组合范式已经定义完整: 卷积层负责局部特征提取、池化层负责空间下采样、全连接层负责类别决策; 感受野与特征层次共同刻画了 CNN 在图像建模中所内置的 归纳偏置 —— 即把"图像具有局部性、对小幅平移不变、特征可以由低到高分层组合" 这几条关于图像的先验知识,直接写进网络结构里, 使网络不必从原始数据中重新发现它们。
本节我们用 PyTorch 实现一个简化版 CNN—— SimpleCNN , 在 MNIST 上训练,并与 《MLP 处理图像的局限》 中训练过的 MLP 做 同口径对比 : 相同数据、相同训练轮数、相同学习率,考察在 参数量同量级 的条件下, CNN 是否在精度上具有显著优势。
SimpleCNN 的结构
本课要训练的 SimpleCNN 是一个 2 卷积层 + 2 全连接层 的小网络, 结构上完全遵循上一节给出的" [Conv → 激活 → Pool] × N → Flatten → [FC → 激活] × M → 输出 "范式:
关键设计选择
逐层张量形状
FC1 的输入维度 来源于 Pool2 输出展平后的总元素数 。 在写 nn.Module 时这个 1568 必须算对—— 否则 nn.Linear 在前向传播时会报形状不匹配。
下面的代码解读中, 右侧展示区会同步展示 SimpleCNN 的结构示意图 , 并随当前代码步骤 高亮对应的层 。图中把每一层的 输出张量形状 按等比例可视化: Conv / Pool 阶段的输出形状为 , 用一组堆叠的方块表示,单个方块的边长正比于空间尺寸 、堆叠的方块数等于通道数 ; Flatten 及之后的全连接层输出为一维向量,用一根竖条表示。 每组方块或竖条下方标注该张量的具体形状,产生它的层名标注在指向它的箭头上方, 颜色用于区分层的类型,当前步骤对应的层以强调色描边。
训练循环:直接复用 MLP 的代码骨架
模型结构换了, 训练流程几乎不变 :
- 数据加载 :与 MLP 一样,用 torchvision.datasets.MNIST + DataLoader ;唯一区别是 不需要再 flatten ,直接保留 4-D 张量形状 [B, 1, 28, 28] 喂给 SimpleCNN ;
- 损失函数 : nn.CrossEntropyLoss ;
- 优化器 : torch.optim.Adam (或 SGD),学习率 1e-3 ;
- 训练循环 :每个 epoch 内逐 batch 取数 → forward → loss → backward → step ,与 MLP 章节完全一致;
- 测试循环 : model.eval() + torch.no_grad() 下计算准确率。
训练 5 轮,准确率约 99%
用上面的训练脚本,在 MNIST 上跑 5 个 epoch , 测试集准确率通常落在 98.5% – 99.2% 区间。 作为参考: 《MLP 处理图像的局限》 中的 MLP 在相同设置下大约能跑到 97.0% – 97.5% 。
鼠标悬停在图上,可读出每个 epoch 上两个模型的具体数值。
与 MLP 同口径对比:参数量同量级,CNN 更准
相同数据集、相同训练轮数、相同学习率下,把两者的 关键指标 列出来对比:
两个观察
99% 之后,下一个问题
SimpleCNN 在 MNIST 上拿到 99% 的准确率,看起来已经"无可挑剔"。 但 准确率并不是评估分类模型的全部指标 —— 在某些场景下,它甚至会严重误导。
举一个极端例子: 某种罕见病的患病率约为 1%。 一个"永远预测健康"的模型,准确率立即就是 99% , 但它一个病人都没找出来, 临床上完全没用 。