YOLOv11小白的进击之路(八)理解Grad-CAM可视化源码

最近在对改YOLOv11做可视化,主要利用的是Grad-CAM方法,所以花了点时间对其源码加以分析理解。

GradCAM类

类定义部分

class GradCAM:  
    def __init__(self,  
                 model,  
                 target_layers,  
                 reshape_transform=None,  
                 use_cuda=False):
  • class GradCAM::定义了一个名为GradCAM的类。
  • def __init__(self, ...)::初始化方法,创建类实例时调用。
    • model:要解释的深度学习模型。
    • target_layers:需要提取激活和梯度的目标层。
    • reshape_transform:可选的形状变换函数,用于调整激活和梯度的形状。
    • use_cuda:一个布尔值,指示是否使用CUDA加速。
        self.model = model.eval()  
        self.target_layers = target_layers  
        self.reshape_transform = reshape_transform  
        self.cuda = use_cuda
  • self.model = model.eval():将模型设置为评估模式。
  • self.target_layers = target_layers:保存目标层列表。
  • self.reshape_transform = reshape_transform:保存形状转换函数。
  • self.cuda = use_cuda:保存CUDA设置。
        if self.cuda:  
            self.model = model.cuda()
  • if self.cuda::如果选择使用CUDA,则将模型转移到GPU。
        self.activations_and_grads = ActivationsAndGradients(  
            self.model, target_layers, reshape_transform)
  • self.activations_and_grads = ActivationsAndGradients(...):创建ActivationsAndGradients类的实例,用于提取激活和梯度。

获取权重和损失

    @staticmethod  
    def get_cam_weights(grads):  
        return np.mean(grads, axis=(2, 3), keepdims=True)
  • @staticmethod:声明该方法为静态方法。
  • def get_cam_weights(grads)::计算每个通道的权重,返回梯度在空间维度上的均值。
    @staticmethod  
    def get_loss(output, target_category):  
        loss = 0  
        for i in range(len(target_category)):  
            loss = loss + output[i, target_category[i]]  
        return loss
  • def get_loss(output, target_category)::计算损失值。对于目标类别,返回模型输出的对应值之和。

获取CAM图像

    def get_cam_image(self, activations, grads):  
        weights = self.get_cam_weights(grads)  
        weighted_activations = weights * activations  
        cam = weighted_activations.sum(axis=1)  
   
        return cam
  • def get_cam_image(self, activations, grads)::计算CAM图像。
  • weights = self.get_cam_weights(grads):获取每层的权重。
  • weighted_activations = weights * activations:通过权重加权激活值。
  • cam = weighted_activations.sum(axis=1):在通道维度上求和以生成CAM图像。

获取目标宽高

    @staticmethod  
    def get_target_width_height(input_tensor):  
        width, height = input_tensor.size(-1), input_tensor.size(-2)  
        return width, height
  • def get_target_width_height(input_tensor)::获取输入张量的宽度和高度

计算每层的CAM

    def compute_cam_per_layer(self, input_tensor):  
        activations_list = [a.cpu().data.numpy()  
                            for a in self.activations_and_grads.activations]  
        grads_list = [g.cpu().data.numpy()  
                      for g in self.activations_and_grads.gradients]  
        target_size = self.get_target_width_height(input_tensor)
  • def compute_cam_per_layer(self, input_tensor)::为每个目标层计算CAM。
  • activations_list 和 grads_list:提取激活值和梯度的NumPy数组。
  • target_size = self.get_target_width_height(input_tensor):获取输入图像的目标尺寸。
        cam_per_target_layer = []  
        for layer_activations, layer_grads in zip(activations_list, grads_list):  
            cam = self.get_cam_image(layer_activations, layer_grads)  
            cam[cam < 0] = 0  
            scaled = self.scale_cam_image(cam, target_size)  
            cam_per_target_layer.append(scaled[:, None, :])  
   
        return cam_per_target_layer
  • cam_per_target_layer:存储每个层的CAM图像。
  • zip(activations_list, grads_list):同时遍历激活和梯度。
  • scaled = self.scale_cam_image(cam, target_size):缩放CAM图像到目标大小。
  • return cam_per_target_layer:返回所有目标层的CAM。

聚合多层的CAM

    def aggregate_multi_layers(self, cam_per_target_layer):  
        cam_per_target_layer = np.concatenate(cam_per_target_layer, axis=1)  
        cam_per_target_layer = np.maximum(cam_per_target_layer, 0)  
        result = np.mean(cam_per_target_layer, axis=1)  
        return self.scale_cam_image(result)
  • def aggregate_multi_layers(self, cam_per_target_layer)::聚合多层的CAM图像。
  • np.concatenate(cam_per_target_layer, axis=1):沿着通道维度合并。
  • result = np.mean(cam_per_target_layer, axis=1):计算平均值。

缩放CAM图像

    @staticmethod  
    def scale_cam_image(cam, target_size=None):  
        result = []  
        for img in cam:  
            img = img - np.min(img)  
            img = img / (1e-7 + np.max(img))  
            if target_size is not None:  
                img = cv2.resize(img, target_size)  
            result.append(img)  
        result = np.float32(result)  
   
        return result
  • def scale_cam_image(cam, target_size=None)::缩放CAM图像到目标大小。
  • 对每张图像归一化处理,并根据需要调整大小。

调用方法

    def __call__(self, input_tensor, target_category=None):  
        if self.cuda:  
            input_tensor = input_tensor.cuda()  
   
        output = self.activations_and_grads(input_tensor)

        if isinstance(target_category, int):  
            target_category = [target_category] * input_tensor.size(0)
  • def __call__(self, input_tensor, target_category=None):使得该类的实例可以像函数一样调用。
  • if self.cuda:如果使用CUDA,将输入张量转移到GPU。
  • output = self.activations_and_grads(input_tensor):通过模型进行前向传播
  • if isinstance(target_category, int)::如果目标类别是单个整数,复制为与输入大小相同的列表。
        if target_category is None:  
            target_category = np.argmax(output.cpu().data.numpy(), axis=-1)  
            print(f"category id: {target_category}")

        self.model.zero_grad()  
        loss = self.get_loss(output, target_category)  
        loss.backward(retain_graph=True)
  • if target_category is None::如果没有指定目标类别,获取输出中最大概率的类别。
  • self.model.zero_grad():清除之前的梯度。
  • loss = self.get_loss(output, target_category):计算当前的损失。
  • loss.backward(retain_graph=True):反向传播以计算梯度。

计算CAM并返回

        cam_per_layer = self.compute_cam_per_layer(input_tensor)  
        return self.aggregate_multi_layers(cam_per_layer)
  • cam_per_layer = self.compute_cam_per_layer(input_tensor):计算每层的CAM。
  • return self.aggregate_multi_layers(cam_per_layer):返回聚合后的CAM结果。

清理和上下文管理

    def __del__(self):  
        self.activations_and_grads.release()

    def __enter__(self):  
        return self  
   
    def __exit__(self, exc_type, exc_value, exc_tb):  
        self.activations_and_grads.release()  
        if isinstance(exc_value, IndexError):  
            print(  
                f"An exception occurred in CAM with block: {exc_type}. Message: {exc_value}")  
            return True
  • def __del__(self)::类被销毁时,释放钩子。
  • __enter__ 和 __exit__:实现上下文管理器,方便使用with语句时自动管理资源的释放。

ActivationsAndGradients类

这个类的主要作用是通过注册钩子来提取神经网络中目标层的激活值和梯度,方便后续的可视化分析(在前向传播时保存激活,在反向传播时保存梯度,提供了一种比较便捷的清理机制)。

函数定义部分

def show_cam_on_image(img: np.ndarray,  
                      mask: np.ndarray,  
                      use_rgb: bool = False,  
                      colormap: int = cv2.COLORMAP_JET) -> np.ndarray:
  • def show_cam_on_image(...)::定义一个名为show_cam_on_image的函数。
  • img: np.ndarray:输入图像,格式为NumPy数组(可为RGB或BGR)。
  • mask: np.ndarray:CAM图像,热力图的基础。
  • use_rgb: bool = False:布尔值参数,指定输入图像的格式是RGB还是BGR;默认为False(表示BGR)。
  • colormap: int = cv2.COLORMAP_JET:OpenCV的colormap,默认为cv2.COLORMAP_JET

热力图生成与颜色空间转换

heatmap = cv2.applyColorMap(np.uint8(255 * mask), colormap)

if use_rgb:  
    heatmap = cv2.cvtColor(heatmap, cv2.COLOR_BGR2RGB)

heatmap = np.float32(heatmap) / 255
  • cv2.applyColorMap(...):将CAM掩膜(mask)转换为热力图。mask经过255倍缩放并转换为无符号8位整数(np.uint8),为了在应用颜色映射时获得适当的值。
  • if use_rgb::如果输入图像的格式是RGB,则将热力图从BGR转换为RGB。
  • np.float32(heatmap) / 255:将热力图转换为浮点数格式,并归一化到[0, 1]范围。

输入图像验证

if np.max(img) > 1:  
    raise Exception(  
        "The input image should be np.float32 in the range [0, 1]")
  • if np.max(img) > 1::检查输入图像的最大值是否大于1,若是则抛出异常。确保图像在[0, 1]范围内,以便后续的叠加不会产生不期望的结果。

 叠加和归一化

cam = heatmap + img  
cam = cam / np.max(cam)

return np.uint8(255 * cam)
  • cam = heatmap + img:将热力图叠加到原始图像上,形成最终的可视化效果。
  • cam = cam / np.max(cam):对叠加后的图像进行归一化,以确保所有值在[0, 1]之间。
  • return np.uint8(255 * cam):将归一化的图像转换回[0, 255]范围,并返回结果。

其他函数

show_cam_on_image函数的主要作用是根据给定的热力图掩膜将其叠加在给定图像上,生成一个可视化的热图,使得模型的注意力区域清晰可见。换句话说,通过使用OpenCV的颜色映射功能,热力图被有效地转换为直观的格式。

center_crop_img函数则是对输入图像进行中心裁剪和大小调整,以确保输出图像的尺寸与指定的size相匹配。这个功能虽然比较基础,但是在处理深度学习模型输入时非常有用,能够保持图像内容的中心区域,同时确保图像尺寸一致。

调用流程

  1. 输入图像处理:用户提供原始图像并将其作为输入传递给GradCAM实例,这时候通常会通过center_crop_img进行预处理,就是为了确保图像大小一致。

  2. 正向传播

    GradCAM调用activations_and_grads(input_tensor),这实际上触发了ActivationsAndGradients实例的__call__方法,进行正向传播来得到模型的输出。
  3. 损失计算与反向传播

    在得到输出后,GradCAM根据输出和目标类别计算损失,然后反向传播以获取相应层的梯度。
  4. CAM计算

    使用提取的激活值和梯度信息,GradCAM计算CAM图像。
  5. 图像可视化

    GradCAM生成的CAM结果会作为掩膜传递给show_cam_on_image函数,结合原始图像叠加热力图生成最后的可视化结果。

最后

欢迎一起交流探讨 ~ 砥砺奋进,共赴山海!

文章推荐YOLOv11小白的进击之路(二)从YOLO类-DetectionModel类出发看YOLO代码运行逻辑..._ch = self.yaml["ch"] = self.yaml.get("ch", ch)-CSDN博客

YOLOv11小白的进击之路(七)训练输出日志解读以及训练OOM报错解决办法_yolo 命令行训练如何产生日志-CSDN博客

### 实现 YOLO 模型中的 Grad-CAM 可视化 为了在 YOLO 模型中实现 Grad-CAM (Gradient-weighted Class Activation Mapping) 可视化,需遵循特定流程并解决可能出现的问题。首先,安装必要的库 `grad-cam` 是必不可少的操作,在此期间可能会遇到依赖项冲突或其他兼容性问题[^1]。 #### 安装所需库 确保环境配置正确无误之后,通过 pip 命令来安装 grad-cam 库: ```bash pip install grad-cam ``` #### 准备工作 对于不同版本的 YOLO 模型(如 YOLOv5 或 YOLOv8),具体操作会有所差异。以 YOLOv5 为例,可以参考开源项目 yolov5-gradcam 提供的方法来进行可视化[Yolov5 GradCAM 工具][^2]。该项目不仅提供了详细的文档说明,还包含了完整的代码实例用于展示如何利用 Grad-CAM 技术分析网络层特征图的重要性分布情况。 #### 编写代码实现 Grad-CAM 下面是一个简单的 Python 脚本片段,展示了如何针对给定图像执行 Grad-CAM 并生成热力图: ```python from pytorch_grad_cam import GradCAM, HiResCAM, FullGrad from pytorch_grad_cam.utils.model_targets import ClassifierOutputTarget from pytorch_grad_cam.utils.image import show_cam_on_image import torch import cv2 import numpy as np from models.experimental import attempt_load from utils.general import non_max_suppression, scale_coords from utils.torch_utils import select_device def load_model(weights_path='yolov5s.pt'): device = select_device() model = attempt_load(weights_path, map_location=device) return model.eval().to(device) model = load_model() target_layers = [model.model[-2]] # 对于YOLOv5,通常选择倒数第二层作为目标层 cam = GradCAM(model=model, target_layers=target_layers) input_tensor = ... # 加载输入张量 targets = None # 如果不需要指定类别,则设置为None;否则定义ClassifierOutputTarget对象列表 grayscale_cam = cam(input_tensor=input_tensor, targets=targets)[0] rgb_img = ... # 将原始图片转换成RGB格式 visualization = show_cam_on_image(rgb_img / 255., grayscale_cam, use_rgb=True) cv2.imshow('Grad CAM', visualization[:, :, ::-1]) # 显示结果 cv2.waitKey(0) ``` 上述脚本实现了加载预训练好的 YOLOv5 模型,并对其内部某一层应用 Grad-CAM 方法获取相应类别的激活映射。注意这里选择了模型的最后一卷积层之前的那一层作为目标层,这通常是推荐的做法因为该位置能够较好地反映高层语义信息。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值