@浙大疏锦行
一、核心概念
注意力热图的本质是注意力权重矩阵的可视化映射:
- 权重值越高 → 热图颜色越深(如红色),代表模型越关注该部分输入
- 权重值越低 → 热图颜色越浅(如蓝色),代表模型对该部分关注度低
在深度学习中,注意力热图主要分为两类:
- NLP 领域:基于 Transformer 的自注意力热图,展示 token 之间的关联权重
- CV 领域:基于 CAM/Grad-CAM 的特征注意力热图,展示图像区域对分类结果的贡献
二、CV - Grad-CAM 注意力热图可视化
在计算机视觉中,Grad-CAM 通过梯度加权激活图展示图像中对分类结果关键的区域,适用于 CNN 模型(如 ResNet、ViT)。
核心原理
- 计算目标类别对模型最后一个卷积层特征图的梯度
- 用梯度权重对特征图进行加权求和
- 将加权后的特征图上采样至原图尺寸,得到注意力热图
简化代码示例
import torch import torch.nn.functional as F import matplotlib.pyplot as plt from PIL import Image from torchvision import models, transforms # 1. 加载模型和图像预处理 model = models.resnet50(pretrained=True) model.eval() # 预处理:图像→张量 preprocess = transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) # 2. 加载并预处理图像 img = Image.open("cat.jpg").convert("RGB") img_tensor = preprocess(img).unsqueeze(0) # (1, 3, 224, 224) # 3. 注册钩子获取特征图和梯度 feature_maps = [] grads = [] def forward_hook(module, input, output): feature_maps.append(output) def backward_hook(module, grad_in, grad_out): grads.append(grad_out[0]) # 注册到最后一个卷积层 target_layer = model.layer4[-1] forward_handle = target_layer.register_forward_hook(forward_hook) backward_handle = target_layer.register_full_backward_hook(backward_hook) # 4. 前向传播+反向传播 preds = model(img_tensor) pred_class = preds.argmax(dim=1).item() model.zero_grad() preds[:, pred_class].backward() # 5. 计算Grad-CAM权重 fm = feature_maps[0].squeeze() # (2048, 7, 7) grad = grads[0].squeeze() # (2048, 7, 7) weights = F.adaptive_avg_pool2d(grad, (1, 1)).squeeze() # (2048,) cam = torch.sum(weights[:, None, None] * fm, dim=0) # (7, 7) cam = F.relu(cam) # 只保留正贡献 cam = F.interpolate( cam.unsqueeze(0).unsqueeze(0), size=img.size[::-1], mode="bilinear", align_corners=False ).squeeze() cam = cam / cam.max() # 归一化 # 6. 绘制热图叠加原图 plt.figure(figsize=(10, 10)) plt.imshow(img) plt.imshow(cam.numpy(), alpha=0.5, cmap="jet") plt.axis("off") plt.title(f"Grad-CAM Attention (Class: {pred_class})") plt.show() # 移除钩子 forward_handle.remove() backward_handle.remove()三、步骤总结
步骤 1:准备工作(模型 + 图像预处理)
核心目的:搭建可提取梯度和特征图的模型环境,将原始图像转为模型可处理的张量。
- 选择并加载 CNN 模型:优先选择包含卷积层 + 全连接层的模型(预训练 / 自定义均可),关键是保留最后一层卷积层(高层特征包含语义信息,可视化有意义;低层仅边缘纹理,无分类参考价值)。
- 图像预处理:严格遵循模型输入规范(尺寸、归一化参数),转为张量并增加批次维度(模型要求批量输入)。
- 模型设置:设为
eval()模式(关闭 Dropout、BatchNorm 等训练层),但不使用torch.no_grad()(因为需要计算梯度,no_grad()会禁用梯度传播)。
步骤 2:捕获最后一层卷积层的「特征图」和「梯度」(核心:Hook 函数)
核心目的:通过 Hook 函数获取两个关键数据 —— 最后卷积层的输出(特征图)、目标类别对该特征图的梯度(反映通道重要性)。这是 Grad-CAM 的核心基础,因为后续所有计算都依赖这两个数据,具体操作:
- 定义两个 Hook 函数:前向 Hook 捕获特征图,反向 Hook 捕获梯度;
- 定位最后一层卷积层:以 ResNet18 为例,最后一层卷积层是
model.layer4[-1].conv2; - 注册 Hook 并保存句柄:后续需移除 Hook,避免内存泄漏。
步骤 3:前向传播,获取目标类别
核心目的:通过前向传播得到模型的预测结果,确定要可视化的「目标类别」(可选择模型预测类别,也可自定义类别)。
- 执行前向传播,得到模型对输入图像的预测输出;
- 提取预测类别(
argmax获取概率最大的类别),作为后续反向传播的目标。
步骤 4:反向传播,计算目标类别的梯度
核心目的:针对目标类别执行反向传播,触发反向 Hook 函数,捕获该类别对最后卷积层特征图的梯度。关键细节:
- 清零现有梯度:用
model.zero_grad()避免梯度累积; - 仅对目标类别的输出值反向传播:不是对整个损失函数反向传播,而是直接对
pred_output[0, target_class]反向传播,这样得到的梯度仅反映该类别对特征图的贡献; - 反向传播后,
gradient变量会被 Hook 函数填充,得到目标类别对应的梯度。
步骤 5:计算梯度权重(全局平均池化 GAP)
核心目的:将每个通道的梯度压缩为一个标量权重,反映该通道对目标类别的「重要性」。为什么用全局平均池化(GAP)?因为最后卷积层的梯度形状是(1, C, H, W)(C = 通道数,H/W = 特征图尺寸),每个通道对应一个特征图,我们需要用一个标量来代表该通道的重要性,GAP 可以将每个通道的(H, W)梯度值平均为一个标量,具体操作:
- 对梯度在「空间维度」(H、W)上执行平均池化,保留通道维度;
- 得到的权重形状为
(1, C, 1, 1),每个值对应一个通道的重要性。
步骤 6:加权求和生成原始 CAM 图,ReLU 激活过滤负贡献
核心目的:用通道权重对特征图进行加权求和,生成原始 CAM 图,并过滤负贡献(仅保留对分类有帮助的区域)。具体操作:
- 加权求和:将通道权重与对应的特征图通道相乘,然后沿通道维度求和,消除通道维度,得到原始 CAM 图;
- ReLU 激活:Grad-CAM 的核心创新之一,只保留正贡献(值 > 0),负贡献对目标类别分类无帮助,直接过滤,避免干扰可视化结果;
- 原始 CAM 图的形状与最后卷积层特征图一致(如
(7, 7)),尺寸较小,需后续上采样。
步骤 7:上采样至原图尺寸,归一化处理
核心目的:将小尺寸的原始 CAM 图放大至与输入图像一致的尺寸,方便叠加可视化,并归一化到 0-1 区间,避免颜色失真。具体操作:
- 上采样:使用双线性插值(
bilinear),效果优于最近邻插值,能保持热图的平滑性; - 归一化:将 CAM 图的值压缩到 0-1 区间,消除数值范围差异对可视化的影响。
步骤 8:热图可视化,叠加原图展示结果
核心目的:将 CAM 图转换为彩色热图,并与原始图像叠加,直观展示模型的注意力区域。具体操作:
- 将 CAM 图转换为彩色热图:使用 OpenCV 的
applyColorMap,常用COLORMAP_JET(红 = 高权重,蓝 = 低权重); - 格式转换与归一化:将 OpenCV 的 BGR 格式转为 RGB 格式,与原始图像格式对齐;
- 叠加原图:设置透明度(如原图 0.7,热图 0.3),平衡原图和热图的可见性;
- 移除 Hook 句柄:释放内存,避免后续干扰。