Task02 视觉感知-04-具身场景的计算机视觉、3D重建/02-抓取注意力热图.md


04-具身场景的计算机视觉、3D重建/02-抓取注意力热图.md


Grad-CAM 到底在解释什么
Grad-CAM,全称是 Gradient-weighted Class Activation Mapping,中文可以理解为:
基于梯度加权的类别激活图。
它要解决的问题是:
对于一个已经训练好的 CNN 模型,当它把一张图判断为某个类别时,图像中的哪些区域对这个判断贡献最大?
例如模型看到一张图片,输出:
cup: 0.98
Grad-CAM 不满足于知道“模型认为这是杯子”,而是继续追问:
模型是根据图像中的哪些区域,才把它判断成 cup 的?
最终它会生成一张热力图。热力图越亮的区域,表示该区域对目标类别分数的贡献越大。
为什么要看最后一个卷积层
CNN 的卷积层有一个特点:越靠前,学到的特征越低级;越靠后,学到的特征越高级。
早期卷积层可能响应:
边缘、颜色、角点、纹理
中间层可能响应:
局部形状、重复纹理、简单部件
后期卷积层可能响应:
杯口、轮子、眼睛、耳朵、身体轮廓
Grad-CAM 通常选择最后一个卷积层,原因是它同时具备两个性质:
- 仍然保留一定空间位置,比如哪里有响应。
- 已经包含较高级语义信息,比如哪个区域像杯子、猫、车。
全连接层虽然语义更强,但空间位置已经丢得差不多了;太早的卷积层虽然空间细节多,但语义太弱。
所以最后卷积层是一个折中点。
先理解两个核心对象
特征图
设最后一个卷积层输出为:
A¹, A², A³, ..., Aᵏ
每个 Aᵏ 都是一张二维特征图。
可以理解为:
第 k 个特征探测器在图像各个位置的响应强度
比如某个通道可能对圆形边缘敏感,另一个通道可能对杯柄样式敏感。
类别分数
设模型对目标类别 c 的输出分数为:
yᶜ
比如:
yᶜ = cup 类别的分数
Grad-CAM 的目标,就是解释:
哪些特征图、哪些空间区域,让 yᶜ 变大?
Grad-CAM 的核心公式
Grad-CAM 的第一步是计算梯度:
∂yᶜ / ∂Aᵏᵢⱼ
意思是:
目标类别 c 的分数 yᶜ,
对第 k 张特征图中位置 (i, j) 的激活值 Aᵏᵢⱼ 的敏感程度。
换成人话:
如果这个位置的特征响应稍微变一下,cup 分数会变多少?
如果变化很大,说明这个位置对 cup 判断重要。
为什么要对梯度求平均
Grad-CAM 会对每张特征图的所有空间位置的梯度求平均,得到一个权重:
αᶜₖ = average(∂yᶜ / ∂Aᵏᵢⱼ)
这个 αᶜₖ 表示:
第 k 张特征图对类别 c 的整体重要性
也就是说:
- 如果
αᶜₖ大,说明第 k 张特征图对cup分数贡献大。 - 如果
αᶜₖ小,说明它对cup分数不太重要。 - 如果
αᶜₖ为负,说明它可能抑制cup这个类别。
怎么生成热力图
有了每张特征图的重要性权重后,把所有特征图加权求和:
Lᶜ = ReLU(Σₖ αᶜₖ Aᵏ)
这就是 Grad-CAM 的核心输出。
含义是:
每张特征图 Aᵏ 乘以它对类别 c 的重要性 αᶜₖ,
然后全部加起来,得到类别 c 的空间响应图。
最后加一个 ReLU,是因为通常只关心正向支持该类别的区域。
也就是说,只保留那些会增强 cup 判断的区域,而过滤掉对 cup 起抑制作用的区域。
用一个完整例子串起来
假设输入是一张杯子的图片,模型输出:
cup: 0.98
Grad-CAM 的过程如下。
第一步:正向传播
图片经过 CNN,得到最后卷积层特征图,同时得到 cup 类别分数。
第二步:选定解释目标
选择:
目标类别 = cup
接下来只解释为什么模型认为它是 cup,而不是解释全部类别。
第三步:反向传播
计算:
cup 分数对最后卷积层特征图的梯度
也就是问模型:
最后卷积层里的哪些响应,会影响 cup 分数?
第四步:得到通道权重
对每个通道的梯度做全局平均,得到每张特征图的重要性。
第五步:加权叠加特征图
把重要特征图放大,不重要特征图压低,然后求和。
第六步:生成热图并叠加原图
把低分辨率热图插值放大到原图大小,再叠加到原图上。
最终看到:
杯口、杯身、杯柄区域较亮
背景区域较暗
这说明模型判断 cup 时主要依赖了杯子的关键视觉区域。
博客式比喻
可以把 CNN 想象成一个检查员团队。
每个特征图是一个检查员:
- 有的检查员专门找圆形
- 有的检查员专门找边缘
- 有的检查员专门找纹理
- 有的检查员专门找类似杯柄的结构
模型最后说:
我认为这是杯子
Grad-CAM 就像在开复盘会,问:
刚才这个“杯子”判断,哪些检查员的意见最关键?
他们主要是在图像的哪些地方发现了证据?
如果杯柄检查员和杯口检查员贡献最大,而且它们的响应都集中在杯子区域,那么热图就会亮在杯子上。
如果关键检查员其实盯着背景文字或桌面纹理,那热图就会亮在错误区域。
它为什么叫 Class Activation Mapping
因为它不是生成一张普通热图,而是生成一张针对某个类别的激活图。
同一张图片,可以对不同类别生成不同 Grad-CAM。
比如模型同时输出:
cup: 0.80
bottle: 0.15
person: 0.03
可以分别问:
为什么是 cup?
为什么有一点像 bottle?
为什么不是 person?
对应会得到不同的热图。
这就是 Class Activation 的意思:
热图是和某个具体类别绑定的,不是通用注意力图。
和注意力机制的区别
很多人会把 Grad-CAM 热图叫作“注意力图”,但严格说它不是 Transformer 里的 self-attention。
Grad-CAM 是一种事后解释方法:
模型已经做完预测,
再用梯度分析哪些区域支持这个预测。
而 Transformer attention 是模型内部计算的一部分:
模型在前向传播时本来就会计算 token 之间的注意力权重。
所以更准确的说法是:
Grad-CAM 是类别判别区域热图,不等同于注意力机制本身。
和 YOLO 热图的区别
04-具身场景的计算机视觉、3D重建/02-抓取注意力热图.md里代码大致是:
检测框 → 填充置信度 → 高斯模糊 → 显示热图
它说明的是:
检测框在哪里,热区就在哪里。
Grad-CAM 是:
类别分数 → 反向传播梯度 → 加权特征图 → 得到热图
它说明的是:
哪些区域真正推动了这个类别分数。
所以前者是结果可视化,后者是模型解释。
最简公式版
设:
Aᵏ = 最后卷积层第 k 个特征图
yᶜ = 类别 c 的预测分数
先算每张特征图的重要性:
αᶜₖ = GlobalAveragePooling(∂yᶜ / ∂Aᵏ)
再生成热图:
Lᶜ = ReLU(Σₖ αᶜₖ Aᵏ)
解释为:
对类别 c 越重要的特征图,权重越大;
这些特征图在哪些空间位置激活强,热图哪里就亮。
最后一段话总结
Grad-CAM 的核心思想是:
用梯度衡量每张高级特征图对某个类别分数的重要性,再用这些重要性权重把特征图加权合成为一张空间热图。
这张热图表示:
为了支持“这是某个类别”的判断,模型主要依赖了图像中的哪些区域。
所以它能从“模型判断结果是什么”,推进到“模型大概是依据哪里做出这个判断的”。
整张图片属于哪个类别

它在做的判断取决于你运行代码时加载的 ViT 分类模型。
一般情况下,如果用的是:
ViTForImageClassification.from_pretrained("google/vit-base-patch16-224")
那它在做的是 ImageNet 图像分类判断。
也就是判断整张图最像 ImageNet 里的哪个类别,例如:
robot arm / machinery / cup / coffee mug / lab equipment ...
但要注意:这张 attention map 本身没有告诉最终分类结果是什么。它只显示:
模型在形成最终分类结果时,哪些图像 patch 被 CLS token 更多关注
要知道它到底判断成了什么类别,需要看代码里的预测输出,例如:
logits = outputs.logits
predicted_class_id = logits.argmax(-1).item()
print(model.config.id2label[predicted_class_id])
所以答案是:
它在做“整张图片属于哪个类别”的判断。
不是在判断:
哪里是杯子
哪里是机械臂
哪里可以抓取
是否抓稳了杯子
如果想判断“杯子在哪里”,需要目标检测模型,比如 YOLO。
如果想判断“模型为什么认为这是某类”,更适合用 Grad-CAM。
import torch
import numpy as np
import matplotlib.pyplot as plt
from PIL import Image
from transformers import ViTImageProcessor, ViTForImageClassification
import cv2
image_path = "cup.png"
image = Image.open(image_path).convert("RGB")
processor = ViTImageProcessor.from_pretrained("google/vit-base-patch16-224")
model = ViTForImageClassification.from_pretrained("google/vit-base-patch16-224")
model.eval()
inputs = processor(images=image, return_tensors="pt")
with torch.no_grad():
outputs = model(**inputs, output_attentions=True)
# attentions 是每一层的 attention
attentions = outputs.attentions
# 取最后一层 attention
last_attn = attentions[-1] # [batch, heads, tokens, tokens]
# 对所有 head 求平均
attn_mean = last_attn.mean(dim=1) # [batch, tokens, tokens]
# 取 CLS token 对所有 image patch 的注意力
cls_attn = attn_mean[0, 0, 1:] # 去掉 CLS 自己,剩下 patch tokens
# ViT-base-patch16-224 是 14x14 patch
attn_map = cls_attn.reshape(14, 14).cpu().numpy()
# resize 到原图大小
img_np = np.array(image)
h, w = img_np.shape[:2]
attn_map = cv2.resize(attn_map, (w, h))
# 归一化
attn_map = (attn_map - attn_map.min()) / (attn_map.max() - attn_map.min() + 1e-8)
# 可视化叠加
plt.imshow(img_np)
plt.imshow(attn_map, cmap="inferno", alpha=0.5)
plt.axis("off")
plt.title("ViT Attention Map")
plt.show()

浙公网安备 33010602011771号