gradcam在yolo中的应用

CAM技术在分类可视化中应用非常多,一直以来想用cam去可视化化yolo的检测结果,这里把我尝试的过程记录下来,一起学习。

一、gradCam

gradCam 是什么,这里我不再介绍,这里直接说我是怎么应用的。

首先,需要使用pytorch的hook,将P3 P4 P5层的特征和 梯度 提取出来,具体代码可以问AI,这里我使用的是

1、设置提取特征和梯度的hook

python 复制代码
class YOLOFeatureHook:
    def __init__(self, model_path, device="cuda:0"):
        self.device = device
        self.model = YOLO(model_path)
        self.model.to(device)
        self.model.model.eval()
        self.features = {"P3": None, "P4": None, "P5": None}
        self.gradients = {"P3": None, "P4": None, "P5": None}
        self.hooks = []
        self._register_hooks()

    def _register_hooks(self):
        layers = self.model.model.model
        if len(layers) <= 21:
            raise RuntimeError(f"YOLO模型层数不足21层,当前层数: {len(layers)}")
        self.hooks.append(layers[15].register_forward_hook(self._hook_p3))
        self.hooks.append(layers[18].register_forward_hook(self._hook_p4))
        self.hooks.append(layers[21].register_forward_hook(self._hook_p5))
        print("Hook注册完成: P3->15, P4->18, P5->21")

    def _hook_p3(self, module, inputs, output):
        self.features["P3"] = output
        if output.requires_grad:
            output.register_hook(lambda grad: self._save_gradient("P3", grad))

    def _hook_p4(self, module, inputs, output):
        self.features["P4"] = output
        if output.requires_grad:
            output.register_hook(lambda grad: self._save_gradient("P4", grad))

    def _hook_p5(self, module, inputs, output):
        self.features["P5"] = output
        if output.requires_grad:
            output.register_hook(lambda grad: self._save_gradient("P5", grad))

    def _save_gradient(self, name, grad):
        self.gradients[name] = grad

这里使用hook,在前向过程中,只能提取P3 P4 P5的特征,要想提取梯度,需要反向传播;

2、特征和梯度

  • P3 P4 P5特征的提取,在forward过程中就将值提取保存了;
  • 这里取梯度的时候,是特定类别的最大分数值的梯度,比如我想要看class_id =0 , 的分数最大的那个目标框,例如coco类中,在nms之前的 模型forward 的输出是 84x 8400 , 84中 前80 是每个类别,80~84, 是目标框的预测,如果是class_id =0, 就是在 840,; 也就是 8400大小中找到最大的分数,然后对这个分数直接求 反向传播就是,我们想要的梯度。
python 复制代码
  def grad_cam(self, image, class_id, imgsz=640, save_dir="grad_cam"):
        self._clear()

        tensor, original_img = self._preprocess(image, imgsz)
        tensor.requires_grad_(True)

        self.model.model.zero_grad(set_to_none=True)

        # ======================================================
        # 一次Forward
        # ======================================================
        output = self.model.model(tensor)

        prediction = output[0] if isinstance(output, tuple) else output

        print("\nRaw prediction shape:", prediction.shape)

        if prediction.ndim != 3:
            raise RuntimeError(f"YOLO prediction shape异常: {prediction.shape}")

        nc = prediction.shape[1] - 4

        if class_id < 0 or class_id >= nc:
            raise ValueError(f"class_id={class_id}错误,类别数量={nc}")

        # ======================================================
        # 找指定类别最高分candidate
        # ======================================================
        class_scores = prediction[0, 4 + class_id, :]
        max_score, max_index = torch.max(class_scores, dim=0)

        max_score.backward()
        class_cams = {}
        gradient_cams = {}

        for name in ["P3", "P4", "P5"]:
            feature = self.features[name]
            gradient = self.gradients[name]

            if feature is None:
                raise RuntimeError(f"{name} feature为空")

            if gradient is None:
                raise RuntimeError(f"{name} gradient为空")

            weights = gradient.mean(dim=(2, 3), keepdim=True)
            class_cam = (weights * feature).sum(dim=1, keepdim=True)
            class_cam = self._normalize(class_cam)
            class_cams[name] = class_cam.detach()

3、score cam

Class Score CAM

CAM(x,y)=ReLU(∑kαkFk(x,y))CAM(x,y) = ReLU(\sum_k{\alpha_k F_k(x,y)})CAM(x,y)=ReLU(k∑αkFk(x,y))

其中:

αk=1HW∑xy∂S∂F(x,y)\alpha_k = \frac{1}{HW} \sum_{xy}{\frac{\partial S}{\partial F(x,y)}}αk=HW1xy∑∂F(x,y)∂S

python 复制代码
#score cam 可视化
            weights = gradient.mean(dim=(2, 3), keepdim=True)
            class_cam = (weights * feature).sum(dim=1, keepdim=True)
            class_cam = self._normalize(class_cam)
            class_cams[name] = class_cam.detach()

score cam具体是怎么操作的,gradient梯度是一个 比如 192x80x80的大小,score cam 计算192个平面中每个平面的平均值,得到192x1的大小,然后在将这192x1和 P3的特征 192x80x80 进行后,得到192x80x80的维度,在通道channl上求和,得到1x80x80,对这个1x80x80的向量进行relu操作后,再进行归一化,得到的就是P3特征层的score cam。

它回答的是:哪些空间位置上的 feature activation , 与整个类别 score的正向贡献比较大

4、Gradient-only

Gradient-only

G(X,Y)=∣∂S∂F(x,y)∣G(X,Y) = |\frac{\partial S}{\partial F(x,y)}|G(X,Y)=∣∂F(x,y)∂S∣

S是分数最大的候选框的 class score;F是P3/P4/P5 特征;G是我们看到的gradient heatmap

python 复制代码
# 梯度可视化
            gradient_cam = gradient.mean(dim=1, keepdim=True)
            gradient_cam = torch.abs(gradient_cam)
            gradient_cam = self._normalize(gradient_cam)
            gradient_cams[name] = gradient_cam.detach()

我们来看下gradient-only的操作,得到P3特征层的梯度,其实可以理解为,特征流到这里后的大小,直接对通道进行求平均,得到1x80x80,然后求绝对值后,直接进行归一化。

和 score cam对比,可以发现,没有和feature相乘的操作,这个相乘有点像特征的加权和,但是这个权重就是当前的梯度。

所以Gradient-only

告诉我们:这个位置的特征发生变化,会不会明显影响整个calss score?

当我使用Gradient-only 可视化的时候,梯度的响应基本都在目标内,但是使用score cam 可视化的时候,响应比较乱,可能是因为 受特征P3 P4 P5本身响应的影响。这个点暂时先不深究,先使用gradient-cam 进行分析。

5、Gradient-cam 分析

我用了几个指标,目的是 将梯度集中程度 + 梯度是否落在目标上量化出来。

5.1 localization

localization:目标框内的平均梯度密度 / 全图平均梯度密度,例如bbox面积比例 0.05,box_grad_ratio = 0.45, localization = 0.45 / 0.05 = 9, 9是对 bbox 占图像面积的 5%, 但是占整个梯度能量的 45%这个信息的量化。

python 复制代码
#目标框面积占图像面积的比例
box_area_ratio = box_area / image_area; 

#box_grad_ratio 整个图像梯度能量中,有多少比例位于目标框内部
total_energy = g_original.sum().item()
box_energy = g_original[y1i:y2i, x1i:x2i].sum().item()
box_grad_ratio = box_energy / total_energy

#localization
localization = box_grad_ratio / box_area_ratio

5.2 top1_energy

这里的top1 不是 top-1个像素,而是梯度最大的top 1% 像素,占整个图像总梯度能量的多少,例如top1_energy = 0.05, 全图梯度最大的1%的梯度值占总能量的5%。

python 复制代码
flat = g_original.flatten()
k1 = int(flat.numel() * 0.01)
top1_values, top1_indices = torch.topk(flat, k1)
top1_energy_ratio = top1_values.sum().item() / total_energy

5.3 top1_box_ratio

top1_box_ratio表示梯度最大top1 1% 像素中,落到目标框中的能量占总能量的多少?

python 复制代码
 top1_inside = ((top1_x >= fx1) & (top1_x < fx2) & (top1_y >= fy1) & (top1_y < fy2)).float().mean().item()
 top5_inside = ((top5_x >= fx1) & (top5_x < fx2) & (top5_y >= fy1) & (top5_y < fy2)).float().mean().item()
 top1_energy_ratio = top1_values.sum().item() / total_energy
 top5_energy_ratio = top5_values.sum().item() / total_energy

总结:
localization : 梯度是否集中在目标区域?
topX_energy :梯度是否集中在少数区域中?
topX_box_ratio:最强梯度是否位于目标内?

相关推荐
亚川楼宇自控系统数据中心厂家29 分钟前
金融数据中心:能碳一体化管理的技术逻辑
人工智能·金融
代码方舟36 分钟前
零信任架构实战:基于天远车辆过户详版查询构建自动化车辆估值网关
运维·人工智能·架构·自动化
沈管家AI数字员工38 分钟前
集团多组织AI怎么上?多子公司统一AI管理的架构与路径
人工智能·架构
美好世界38 分钟前
OpenCode 的 Agent Runtime:从本地状态到 Agent Loop
人工智能
leoZ23140 分钟前
AI+前端提效-09 AI赋能前端测试:单元测试、E2E测试自动生成,提升覆盖率
前端·人工智能·opencv·目标检测·数据挖掘·单元测试·语音识别
算了吧95691 小时前
从“黑箱”到“透明”:答序科技如何用“诊断型”技术架构重构品牌AI可见度
人工智能·科技·架构
2601_949950631 小时前
考研英语资料太散?用小程序把真题、词汇和错题放到一起
人工智能·学习·考研·小程序·刷题·小程序推荐
ShineWinsu1 小时前
对于OpenClaw:核心命令以及飞书钉钉等渠道的解析
人工智能·钉钉·飞书
李可以量化1 小时前
Redis Client 从了解到精通(六):redis-py 服务控制与状态监控辅助函数详解
python