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:最强梯度是否位于目标内?