拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

ResNet 到底在看哪里?我用码道做了一个 Grad-CAM 可视化实验

ResNet 到底在看哪里?我用码道做了一个 Grad-CAM 可视化实验

项目地址:

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 -v

ResNet 本身的测试基本正常,但 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

返回列表