尧图精选

fastbook 实战:使用 CAM 与 Grad-CAM 解释 CNN 决策——基于 PyTorch Hooks 的类激活图实现

🕒 发布时间:2026/10/1 7:52:35 📁 来源:尧图网络
教程深度学习机器学习【免费下载链接】fastbookThe fastai book, published as Jupyter Notebooks项目地址https://gitcode.com/gh_mirrors/fa/fastbook点击查看免费下载导读本文基于 fastai 官方教材《Deep Learning for Coders with fastai PyTorch》第十八章系统讲解如何从零实现类激活图Class Activation Map, CAM与梯度 CAMGrad-CAM让深度学习模型开口说话直观呈现 CNN 究竟依据图像中的哪些区域做出了分类决策。你将学会利用 PyTorch 的 hook 机制注入前向/反向计算、通过torch.einsum聚合特征与权重并把热力图叠加到原图上完成可视化——这套方法同样适用于分析假阳性样本、定位训练数据缺口是模型可解释性方向的实用入门。对应仓库文件translations/cn/18_CAM.md本文依据的中文译文与 18_CAM.ipynb可运行的完整 Notebook。为什么需要 CAM从黑盒到解释器训练好的 CNN 往往被视为黑盒——我们知道它输出了什么却不清楚它为什么这么做。类激活图CAM由周博磊Bolei Zhou等人在论文Learning Deep Features for Discriminative Localization中提出它使用最后一个卷积层的输出恰好位于平均池化层之前与模型预测结果生成一张覆盖在原图上的热力图告诉我们模型做出某个决定时看了图像的哪些位置。其数学直觉非常朴素在最终卷积层的每个空间位置上滤波器数量与最后一个线性层的输入维度一致。因此我们可以把特征图的激活值与最终线性层的权重做点积得到特征图上每个位置对最终决策的贡献分数——分数高的区域就是模型判定为猫或狗的关键证据。从仓库代码结构可以印证这一点在 fastai 的cnn_learner/vision_learner构建的模型中learn.model[0]是 CNN 主干bodylearn.model[1]是头部其中learn.model[1][-1]即最后一层线性层其权重矩阵正是 CAM 计算所需的分类权重。前置准备训练一个猫狗分类器在实现 CAM 之前我们需要一个已经训练好的模型。原文使用与第一章相同的宠物数据集Oxford PETS训练二分类模型——类别是狗False与猫True标签由文件名首字母是否大写判定is_cat。path untar_data(URLs.PETS)/images def is_cat(x): return x[0].isupper() dls ImageDataLoaders.from_name_func( path, get_image_files(path), valid_pct0.2, seed21, label_funcis_cat, item_tfmsResize(224)) learn cnn_learner(dls, resnet34, metricserror_rate) learn.fine_tune(1)轮次训练损失验证损失错误率时间00.1419870.0188230.00744200:16轮次训练损失验证损失错误率时间---------------00.0509340.0153660.00676600:21仅微调 1 轮验证集错误率就已降到 0.7% 以下。值得注意的是原文代码中的cnn_learner在本书后续章节的 Notebook 实现中已更新为vision_learner见 18_CAM.ipynb二者在该场景下用法等价仓库 environment.yml 要求python3.6、pytorch1.6requirements.txt 要求fastai2.0.0运行环境以此为准。PyTorch Hook向计算图内部注入代码要获得 CAM就必须在推理过程中拦截模型内部层的激活值。PyTorch 提供了hook机制它是 fastai 回调在 PyTorch 层面的等价物。区别在于fastai 的Learner回调把代码注入训练循环而hook 把代码直接注入前向计算与反向传播本身。前向 hookforward hook附加到任意层在该层计算出输出时触发。回调函数接收三个参数——模块m、模块输入i、模块输出o可以做任何你想做的事。后向 hookbackward hook在反向传播过程中触发PyTorch 会把梯度作为参数传入方便我们存储各层的梯度。原文还提示fastai 提供了更方便的HookCallback类封装了这一过程读者可以查阅 fastai 文档加深理解。最小的 Hook 实现CAM 需要保存最后一个卷积层的激活。我们把 hook 函数放进一个类里让它拥有可访问的状态并只保存输出的副本detach().clone()切断计算图且防止后续内存被覆盖class Hook(): def hook_func(self, m, i, o): self.stored o.detach().clone()实例化并把 hook 挂到 CNN 主干body的最后一层hook_output Hook() hook learn.model[0].register_forward_hook(hook_output.hook_func)源码层面register_forward_hook是 PyTorchnn.Module的内置 APIhook 注册后会在每次前向时调用hook_func。使用完毕务必调用hook.remove()移除否则可能造成内存泄漏。现在取一张猫图过一遍模型并取出存储的激活img PILImage.create(images/chapter1_cat_example.jpg) x, first(dls.test_dl([img])) with torch.no_grad(): output learn.model.eval()(x) act hook_output.stored[0]这里有两个关键操作需要解释eval()把模型切到推理模式关闭 dropout / BatchNorm 的统计更新保证激活值反映的是训练收敛后的行为torch.no_grad()推理阶段不需要计算梯度关闭自动求导能显著降低内存占用并加速。先确认预测结果与类别顺序F.softmax(output, dim-1) # tensor([[7.3566e-07, 1.0000e00]], devicecuda:0) dls.vocab # (#2) [False,True]fastai 会自动对类别排序索引0对应False狗索引1对应True猫。模型几乎 100% 确信这是一张猫图。用 torch.einsum 计算类激活图现在我们有分类权重矩阵learn.model[1][-1].weight形状为2 类 × 激活通道数最后一层卷积激活act形状为批次 × 通道 × 行 × 列。将两者做点积即可得到每个类别在每个空间位置的决策分数。原文用自定义einsum一步完成x形状为[1, 3, 224, 224]即单张 224×224 的 RGB 图像x.shape # torch.Size([1, 3, 224, 224]) cam_map torch.einsum(ck,kij-cij, learn.model[1][-1].weight, act) cam_map.shape # torch.Size([2, 7, 7])einsum的语义直观c遍历类别、k遍历通道、i,j遍历空间位置。对批次中的每张图、每个类别得到一张 7×7 的分数图——这正是 ResNet-34 最后一个卷积层下采样后的空间分辨率它告诉我们激活高/低的位置。把热力图叠加到原图decode 与 TensorImage由于DataLoader对输入x做了归一化直接显示会得到一张不可见的图像因此需要decode还原同时由于索引操作会丢失类型信息需要显式转换为TensorImage书中撰写时 PyTorch 的行为读者阅读时可能已修复x_dec TensorImage(dls.train.decode((x,))[0][0]) _,ax plt.subplots() x_dec.show(ctxax) ax.imshow(cam_map[1].detach().cpu(), alpha0.6, extent(0,224,224,0), interpolationbilinear, cmapmagma);上图中亮黄色对应高激活、紫色对应低激活。可以看到头部与前爪是模型判定这是猫的两个主要依据区域——这正是 CAM 作为解释工具的价值模型关注的位置与人类直觉高度一致。热力图绘制要点alpha0.6控制叠加透明度extent(0,224,224,0)让 7×7 的激活图缩放对齐到 224×224 的原图坐标系interpolationbilinear做平滑插值cmapmagma选用感知均匀且色盲友好的色带。上下文管理器让 Hook 自动注册与清理每次手动hook.remove()容易遗漏。更好的做法是把Hook实现为上下文管理器进入with块时注册 hook__enter__退出时自动移除__exit__——这与with open(...) as f:自动关闭文件的机制完全相同class Hook(): def __init__(self, m): self.hook m.register_forward_hook(self.hook_func) def hook_func(self, m, i, o): self.stored o.detach().clone() def __enter__(self, *args): return self def __exit__(self, *args): self.hook.remove()使用起来安全且简洁with Hook(learn.model[0]) as hook: with torch.no_grad(): output learn.model.eval()(x.cuda()) act hook.storedfastai 官方库已内置此Hook类及若干辅助类HookBwd等读者可直接复用。从 CAM 到 Grad-CAM突破最后一层的限制普通 CAM 存在硬伤它只能用于最后一个卷积层——因为拿到特征后必须乘以最后的权重矩阵而这一权重只对最后层有意义对网络内部层无能为力。2016 年 Selvaraju 等人提出的Grad-CAM: Why Did You Say That?给出解法用目标类最终激活的梯度替代权重。回忆反向传播的结论最后一层是线性层时该层输出对输入的梯度恰好等于权重矩阵。但对更深层的内部层梯度不再等于权重必须显式计算。问题在于PyTorch 在反向传播中会计算每一层的梯度但默认不存储除requires_gradTrue的张量。解决办法就是注册一个后向 hook让 PyTorch 把梯度作为参数传给回调函数并保存下来class HookBwd(): def __init__(self, m): self.hook m.register_backward_hook(self.hook_func) def hook_func(self, m, gi, go): self.stored go[0].detach().clone() def __enter__(self, *args): return self def __exit__(self, *args): self.hook.remove()为什么不能直接 output.backward()output是形状1 张图 × 2 类的秩 2 张量而梯度只对标量通常是损失有意义。解法是选取单张图、单个类对output[0, cls]这个标量求导cls 1 # True - 猫 with HookBwd(learn.model[0]) as hookg: with Hook(learn.model[0]) as hook: output learn.model.eval()(x.cuda()) act hook.stored output[0,cls].backward() grad hookg.storedGrad-CAM 权重特征图上的梯度平均Grad-CAM 的类别权重定义为该层梯度在特征图空间维上的均值再与激活逐通道相乘求和w grad[0].mean(dim[1,2], keepdimTrue) cam_map (w * act[0]).sum(0)keepdimTrue至关重要若去掉它mean会直接压缩掉空间维度w * act[0]因形状不匹配而无法广播最终求和结果也会丢失空间坐标——这也是原文进一步研究中要求读者亲手验证的问题。Grad-CAM 的杀手锏任意中间层Grad-CAM 的新颖之处在于可以在任何层使用。下面把它应用到 ResNet 倒数第二个残差组learn.model[0][-2]的输出上流程与上面完全一致只是 hook 挂载点变了with HookBwd(learn.model[0][-2]) as hookg: with Hook(learn.model[0][-2]) as hook: output learn.model.eval()(x.cuda()) act hook.stored output[0,cls].backward() grad hookg.storedw grad[0].mean(dim[1,2], keepdimTrue) cam_map (w * act[0]).sum(0)绘制该中间层的激活图_,ax plt.subplots() x_dec.show(ctxax) ax.imshow(cam_map.detach().cpu(), alpha0.6, extent(0,224,224,0), interpolationbilinear, cmapmagma);对比三张图可以直观感受越深的层语义越抽象、空间分辨率越低但激活区域越能对应到可解释的物体部件如头部、口鼻、前爪。这也是 Grad-CAM 被广泛应用于弱监督目标定位、医学影像分析等领域的理论基础。结论与实践建议模型解释是一个活跃的研究方向本章只触及了可能性的一角。类激活图向我们展示对于一个给定预测图像的哪些区域贡献最大。这直接服务于两类工程场景分析假阳性若模型把狗误判为猫叠加 CAM 即可定位到误导模型的区域例如背景中的球、模糊的边缘进而定位数据问题发现训练数据缺口如果热力图始终集中在非语义区域如边框、水印说明训练集中该类样本的判别性特征代表性不足需要补充数据。运行本文全部代码需按仓库 environment.yml 安装环境Python ≥ 3.6、PyTorch ≥ 1.6、fastai ≥ 2.0.0并在 GPUx.cuda()相关调用或 CPU 环境下执行 18_CAM.ipynb本文中的训练损失/错误率表格为该章节在 GPU 上运行的示例输出具体数值会因环境与随机种子而略有差异。章节自测Questionnaire为检验理解请尝试回答以下问题原文收录PyTorch 中的 hook 是什么CAM 使用哪个层的输出为什么 CAM 需要一个 hook查看ActivationStats类的源码看看它如何使用 hooksfastai 回调体系中的典型实现。编写一个 hook用于存储模型中给定层的激活先不要偷看上面的实现。为什么在获取激活之前要调用eval为什么使用no_grad使用torch.einsum计算模型主体最后激活的每个位置的狗或猫得分。如何检查类别的顺序索引 → 类别的对应关系显示输入图像时为什么要decode什么是上下文管理器需要定义哪些特殊方法为什么普通 CAM 不能用于网络的内部层为了执行 Grad-CAM为什么需要在反向传播中注册 hook当output是每个图像每个类别输出激活的秩 2 张量时为什么不能直接调用output.backward()进一步研究尝试移除keepdim观察发生了什么查阅 PyTorch 文档中该参数的意义并解释为什么本章需要它。仿照本章创建一个面向 NLP 的 Notebook用同样的思路找出电影评论中哪些词对情感判断最重要即词级 CAM的初步探索。赞分享教程深度学习机器学习【免费下载链接】fastbookThe fastai book, published as Jupyter Notebooks项目地址https://gitcode.com/gh_mirrors/fa/fastbook点击查看免费下载相关推荐如何轻松掌握PyTorch Grad-CAM从数学原理到视觉解释实战指南如何轻松掌握PyTorch Grad CAM从数学原理到视觉解释实战指南 PyTorch Grad CAM是一个强大的计算机视觉解释工具能够可视化深度学习模人工智能AI 可解释性计算机视觉深度学习PySlowFast模型解释性工具Grad-CAM与Score-CAM实现PySlowFast模型解释性工具Grad CAM与Score CAM实现 引言视频理解中的模型可解释性挑战 你是否曾困惑于视频分类模型为何做出特定预测人工智能计算机视觉深度学习预训练pytorch-cnn-visualizations实战指南从安装到Grad-CAM热力图生成pytorch cnn visualizations实战指南从安装到Grad CAM热力图生成 你是否曾好奇AI如何看见图像当深度学习模型对一张图片进行人工智能深度学习计算机视觉AI 可解释性上一篇StartOS备份与恢复完整指南确保你的自托管数据永不丢失下一篇突破存储瓶颈Pig平台分布式存储集成实战指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
上一篇/下一篇内容由系统自动关联 返回资讯列表 →