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

1008fae7330f2f22299c8c1cc4ee5f32
357445f2d8e4df2ac6b9037917090e8a

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

image
image

Grad-CAM 到底在解释什么

Grad-CAM,全称是 Gradient-weighted Class Activation Mapping,中文可以理解为:
基于梯度加权的类别激活图。
它要解决的问题是:

对于一个已经训练好的 CNN 模型,当它把一张图判断为某个类别时,图像中的哪些区域对这个判断贡献最大?
例如模型看到一张图片,输出:

cup: 0.98

Grad-CAM 不满足于知道“模型认为这是杯子”,而是继续追问:

模型是根据图像中的哪些区域,才把它判断成 cup 的?

最终它会生成一张热力图。热力图越亮的区域,表示该区域对目标类别分数的贡献越大。

为什么要看最后一个卷积层

CNN 的卷积层有一个特点:越靠前,学到的特征越低级;越靠后,学到的特征越高级。
早期卷积层可能响应:

边缘、颜色、角点、纹理

中间层可能响应:

局部形状、重复纹理、简单部件

后期卷积层可能响应:

杯口、轮子、眼睛、耳朵、身体轮廓

Grad-CAM 通常选择最后一个卷积层,原因是它同时具备两个性质:

  1. 仍然保留一定空间位置,比如哪里有响应。
  2. 已经包含较高级语义信息,比如哪个区域像杯子、猫、车。
    全连接层虽然语义更强,但空间位置已经丢得差不多了;太早的卷积层虽然空间细节多,但语义太弱。
    所以最后卷积层是一个折中点。

先理解两个核心对象

特征图

设最后一个卷积层输出为:

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 的核心思想是:
用梯度衡量每张高级特征图对某个类别分数的重要性,再用这些重要性权重把特征图加权合成为一张空间热图。
这张热图表示:

为了支持“这是某个类别”的判断,模型主要依赖了图像中的哪些区域。

所以它能从“模型判断结果是什么”,推进到“模型大概是依据哪里做出这个判断的”。

整张图片属于哪个类别

image
它在做的判断取决于你运行代码时加载的 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()
posted @ 2026-04-24 10:10  asandstar  阅读(6)  评论(0)    收藏  举报