项目地址:
cnn-mnist-playground:基于 PyTorch 的 CNN 手写数字识别与可视化实验项目 - AtomGit
前面几篇文章里,我已经在同一个cnn-mnist-playground项目中完成了 CNN 手写数字识别、Conv1 / Conv2 Feature Map 可视化,以及 CNN 与 MLP 的对比实验。
做到这里以后,我又有了一个新的问题:
模型虽然能把数字识别出来,但它在做出这个分类结果时,到底更关注图像中的哪些区域?
所以这一次,我继续使用码道 Agent,在原项目基础上加入一个轻量级 ResNet,并实现 Grad-CAM 可视化,让分类结果背后的空间响应能够直接显示出来。
一、继续在原项目上加入 ResNet
这次我没有重新开一个仓库,而是继续在原来的项目上增量开发。
现在整个实验路线已经变成:
CNN 手写数字识别
→ Feature Map 可视化
→ CNN vs MLP
→ ResNet
→ Grad-CAM
这样做的好处是,输入任务始终都是 MNIST,只改变网络结构和分析方式,更方便观察不同方法之间的差异。
这一次用的也不是直接照搬 torchvision 的原版 ResNet18,而是根据 MNIST 的28×28单通道输入设计了一个轻量 ResNet。
模型输入为1×28×28,最终输出 10 个类别,对应数字 0~9。网络中使用了多个残差块,并逐步把空间尺寸从28×28降到14×14,最后降到7×7,再通过 Global Average Pooling 和全连接层完成分类。
最终模型真实参数量为:
174,970
二、本地真实训练,测试准确率 99.14%
代码完成以后,我还是按照之前的方式,不让 Agent 去假设训练结果,而是在自己的 PyTorch 环境中进行真实训练。
训练命令:
python -m scripts.train_resnet --epochs 5训练完成后,模型权重保存到:
app/weights/mnist_resnet.pth最终 ResNet 的真实测试集准确率为:
99.14%
而上一篇 CNN vs MLP 实验中,MLP 的真实测试准确率为:
97.92%
在当前这套 MNIST 实验配置下,轻量 ResNet 得到了更高的测试准确率。当然,这只能说明当前实验结果,不能简单得出“ResNet 在所有情况下都一定优于 MLP”的结论。
三、Grad-CAM 到底在做什么?
Grad-CAM 的全称是:
Gradient-weighted Class Activation Mapping
它想解决的问题并不是“模型预测了什么”,而是:
对于当前这个类别,卷积特征图中的哪些空间区域对输出贡献相对更大?
它的大致过程是先进行一次正常的前向传播,得到分类结果,然后针对目标类别进行反向传播,计算目标类别得分对最后卷积层 Feature Map 的梯度。
这些梯度经过空间平均后,可以得到每个通道对应的权重,再利用这些权重对 Feature Map 加权求和,经过 ReLU 和归一化以后,就得到了最终的 Grad-CAM。
这次项目选择的 Target Layer 是:
stage3[-1].conv2也就是最后一个残差阶段中最后一个 BasicBlock 的第二个卷积层。
为了获取真实的 Feature Map 和 Gradient,我在实现中使用了 PyTorch Hook:
target_layer.register_forward_hook( self._save_activation)target_layer.register_full_backward_hook( self._save_gradient)也就是说,页面上看到的热力图不是随机生成的图片,而是真正根据当前输入、当前模型和当前目标类别实时计算出来的。
四、最终页面效果
前端新增了一个独立的:ResNet + Grad-CAM 可视化区域。
页面中可以直接看到:
模型名称MnistResNet,输入尺寸1×28×28,参数量174,970,测试准确率99.14%,以及 Grad-CAM Target Layerstage3[-1].conv2。
手写一个数字以后,点击“生成 Grad-CAM”,页面就会同时显示预测类别、置信度、Target Class、原始输入、Grad-CAM 热力图和 Overlay 叠加图。
整个流程已经完整打通:
手写数字
→ ResNet 推理
→ 得到预测类别
→ 反向传播
→ Grad-CAM
→ Heatmap
→ Overlay
五、真实案例:数字 0
第一个测试是手写数字0。
最终结果:
Prediction:0 Confidence:95.18% Target Class:0 Target Layer:stage3[-1].conv2从热力图中可以看到,高响应区域主要分布在数字笔画及其内部邻近区域。
Overlay 把热力图直接叠加到了原始输入上,这样比单独看一张 Heatmap 更容易观察空间响应与数字结构之间的位置关系。
不过这里需要特别注意:
Grad-CAM 的高响应区域不能简单理解成“模型真正看到了这里”。
更加准确的理解应该是:在当前输入和当前目标类别下,这些空间区域对类别输出的贡献相对更大。
六、再测试数字 8 和 9
随后我又分别手写了数字8和9。
数字8的结果为:
Prediction:8 Confidence:99.75%从当前结果来看,数字 8 的高响应区域主要集中在中上部以及两个环的连接附近。
数字9的结果为:
Prediction:9 Confidence:99.91%数字 9 的高响应区域则更多集中在上方闭环以及中部附近。
三个真实案例分别为:数字 0,置信度 95.18%;数字 8,置信度 99.75%;数字 9,置信度 99.91%。
不同输入产生的 Grad-CAM 空间分布明显不同,这也是这次实验最直观的地方。
七、真正运行以后发现了一个 Bug:7×7 对不上 28×28
这次项目在真实运行 pytest 的时候,还发现了一个很典型的问题。
最开始执行:
pytest tests/test_resnet.py tests/test_gradcam.py -vResNet 本身的测试基本正常,但 Grad-CAM 出现了多个失败。
继续看 Traceback 后发现,实际上核心问题只有一个:
GradCAMError: cam 与 base_image 形状不一致: (7, 7) vs (28, 28)原因也比较直接。
原始 MNIST 图片是:
28×28但是经过 ResNet 的多次下采样后,最后卷积层已经变成:
7×7所以从这个卷积层得到的原始 Grad-CAM 自然也是7×7。
而 Overlay 需要把热力图和原始图像逐像素叠加,7×7和28×28显然不能直接进行计算。
八、解决方法:先进行空间尺寸对齐
这个问题不能简单地通过删除 shape 检查解决。
因为即使不报错,两个不同尺寸的数组依然无法正常进行 Overlay。
最后采用的处理流程是:
Grad-CAM 7×7 ↓ Bilinear Resize ↓ 28×28 ↓ 与原始输入 Overlay ↓ 28×28 ↓ 显示时放大 ↓ 224×224核心代码类似:
if cam.shape != base.shape: cam = np.asarray( Image.fromarray(cam, mode="F").resize( ( int(base.shape[1]), int(base.shape[0]) ), resample=Image.BILINEAR, ), dtype=np.float32, )这样原始 Grad-CAM 依然来自真实的7×7特征空间,只是在进行 Overlay 前,把它插值到和原始 MNIST 图像一致的28×28。
最后放大到224×224只是为了让网页显示更加清楚。
九、修复以后:100 个专项测试全部通过
修复尺寸问题以后,我重新运行:
pytest tests/test_resnet.py tests/test_gradcam.py -v最终得到:
100 passed, 2 warnings in 3.94s这说明 ResNet 和 Grad-CAM 相关的模型结构、API、Heatmap、Overlay、空白输入以及异常处理等测试都已经正常。
十、完整回归测试:192 Passed
因为这一次是在原来的项目上继续增加功能,所以仅仅保证 Grad-CAM 能运行还不够。
还要确认原来的 CNN、MLP、Feature Map、CNN vs MLP 和 API 没有因为新功能被破坏。
于是最后重新运行整个测试集:
pytest -v最终结果:
192 passed, 2 warnings in 6.84s到这里,ResNet + Grad-CAM 的整个功能链条才算真正完成。
相比“页面看起来能运行”,我觉得这种完整回归测试更重要,因为它可以确认新增功能没有破坏前面的实验。
总结
这一次在原来的 MNIST 实验平台上继续加入了轻量 ResNet 和 Grad-CAM。
最终得到的真实结果是:
ResNet 参数量174,970,测试准确率99.14%;数字 0 的预测置信度95.18%,数字 8 为99.75%,数字 9 为99.91%;ResNet + Grad-CAM 专项测试100 passed,完整项目回归测试192 passed。
相比单纯看到一个分类结果,我觉得 Grad-CAM 更有意思的地方是:
我们终于可以观察当前分类结果对应的空间响应主要分布在哪里。
当然,Grad-CAM 仍然不是完整的模型因果解释,它更适合作为一种辅助观察神经网络决策过程的方法。
现在这条实验路线已经做到:
CNN
→ Feature Map
→ CNN vs MLP
→ ResNet
→ Grad-CAM
下一步准备继续进入目标检测,开始做YOLO。
项目地址:
cnn-mnist-playground:基于 PyTorch 的 CNN 手写数字识别与可视化实验项目 - AtomGit