Grad-CAM的详细介绍和Pytorch代码实现

这篇具有很好参考价值的文章主要介绍了Grad-CAM的详细介绍和Pytorch代码实现。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

Grad-CAM (Gradient-weighted Class Activation Mapping) 是一种可视化深度神经网络中哪些部分对于预测结果贡献最大的技术。它能够定位到特定的图像区域,从而使得神经网络的决策过程更加可解释和可视化。

Grad-CAM 的基本思想是,在神经网络中,最后一个卷积层的输出特征图对于分类结果的影响最大,因此我们可以通过对最后一个卷积层的梯度进行全局平均池化来计算每个通道的权重。这些权重可以用来加权特征图,生成一个 Class Activation Map (CAM),其中每个像素都代表了该像素区域对于分类结果的重要性。

相比于传统的 CAM 方法,Grad-CAM 能够处理任意种类的神经网络,因为它不需要修改网络结构或使用特定的层结构。此外,Grad-CAM 还可以用于对特征的可视化,以及对网络中的一些特定层或单元进行分析。

在Pytorch中,我们可以使用钩子 (hook) 技术,在网络中注册前向钩子和反向钩子。前向钩子用于记录目标层的输出特征图,反向钩子用于记录目标层的梯度。在本篇文章中,我们将详细介绍如何在Pytorch中实现Grad-CAM。

Grad-CAM的详细介绍和Pytorch代码实现

加载并查看预训练的模型

为了演示Grad-CAM的实现,我将使用来自Kaggle的胸部x射线数据集和我制作的一个预训练分类器,该分类器能够将x射线分类为是否患有肺炎。

 model_path="your/model/path/"
 
 # instantiate your model
 model=XRayClassifier() 
 
 # load your model. Here we're loading on CPU since we're not going to do 
 # large amounts of inference
 model.load_state_dict(torch.load(model_path, map_location=torch.device('cpu'))) 
 
 # put it in evaluation mode for inference
 model.eval()

首先我们看看这个模型的架构。就像前面提到的,我们需要识别最后一个卷积层,特别是它的激活函数。这一层表示模型学习到的最复杂的特征,它最有能力帮助我们理解模型的行为,下面是我们这个演示模型的代码:

 importtorch
 importtorch.nnasnn
 importtorch.nn.functionalasF
 
 # hyperparameters
 nc=3# number of channels
 nf=64# number of features to begin with
 dropout=0.2
 device=torch.device('cuda'iftorch.cuda.is_available() else'cpu')
 
 # setup a resnet block and its forward function
 classResNetBlock(nn.Module):
     def__init__(self, in_channels, out_channels, stride=1):
         super(ResNetBlock, self).__init__()
         self.conv1=nn.Conv2d(in_channels, out_channels, kernel_size=3, stride=stride, padding=1, bias=False)
         self.bn1=nn.BatchNorm2d(out_channels)
         self.conv2=nn.Conv2d(out_channels, out_channels, kernel_size=3, stride=1, padding=1, bias=False)
         self.bn2=nn.BatchNorm2d(out_channels)
         
         self.shortcut=nn.Sequential()
         ifstride!=1orin_channels!=out_channels:
             self.shortcut=nn.Sequential(
                 nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=stride, bias=False),
                 nn.BatchNorm2d(out_channels)
             )
         
     defforward(self, x):
         out=F.relu(self.bn1(self.conv1(x)))
         out=self.bn2(self.conv2(out))
         out+=self.shortcut(x)
         out=F.relu(out)
         returnout
 
 # setup the final model structure
 classXRayClassifier(nn.Module):
     def__init__(self, nc=nc, nf=nf, dropout=dropout):
         super(XRayClassifier, self).__init__()
 
         self.resnet_blocks=nn.Sequential(
             ResNetBlock(nc,   nf,    stride=2), # (B, C, H, W) -> (B, NF, H/2, W/2), i.e., (64,64,128,128)
             ResNetBlock(nf,   nf*2,  stride=2), # (64,128,64,64)
             ResNetBlock(nf*2, nf*4,  stride=2), # (64,256,32,32)
             ResNetBlock(nf*4, nf*8,  stride=2), # (64,512,16,16)
             ResNetBlock(nf*8, nf*16, stride=2), # (64,1024,8,8)
         )
 
         self.classifier=nn.Sequential(
             nn.Conv2d(nf*16, 1, 8, 1, 0, bias=False),
             nn.Dropout(p=dropout),
             nn.Sigmoid(),
         )
 
     defforward(self, input):
         output=self.resnet_blocks(input.to(device))
         output=self.classifier(output)
         returnoutput

模型3通道接收256x256的图片。它期望输入为[batch size, 3,256,256]。每个ResNet块以一个ReLU激活函数结束。对于我们的目标,我们需要选择最后一个ResNet块。

 XRayClassifier(
   (resnet_blocks): Sequential(
     (0): ResNetBlock(
       (conv1): Conv2d(3, 64, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
       (bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
       (conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
       (bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
       (shortcut): Sequential(
         (0): Conv2d(3, 64, kernel_size=(1, 1), stride=(2, 2), bias=False)
         (1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
       )
     )
     (1): ResNetBlock(
       (conv1): Conv2d(64, 128, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
       (bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
       (conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
       (bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
       (shortcut): Sequential(
         (0): Conv2d(64, 128, kernel_size=(1, 1), stride=(2, 2), bias=False)
         (1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
       )
     )
     (2): ResNetBlock(
       (conv1): Conv2d(128, 256, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
       (bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
       (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
       (bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
       (shortcut): Sequential(
         (0): Conv2d(128, 256, kernel_size=(1, 1), stride=(2, 2), bias=False)
         (1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
       )
     )
     (3): ResNetBlock(
       (conv1): Conv2d(256, 512, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
       (bn1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
       (conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
       (bn2): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
       (shortcut): Sequential(
         (0): Conv2d(256, 512, kernel_size=(1, 1), stride=(2, 2), bias=False)
         (1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
       )
     )
     (4): ResNetBlock(
       (conv1): Conv2d(512, 1024, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
       (bn1): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
       (conv2): Conv2d(1024, 1024, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
       (bn2): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
       (shortcut): Sequential(
         (0): Conv2d(512, 1024, kernel_size=(1, 1), stride=(2, 2), bias=False)
         (1): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
       )
     )
   )
   (classifier): Sequential(
     (0): Conv2d(1024, 1, kernel_size=(8, 8), stride=(1, 1), bias=False)
     (1): Dropout(p=0.2, inplace=False)
     (2): Sigmoid()
   )
 )

在Pytorch中,我们可以很容易地使用模型的属性进行选择。

 model.resnet_blocks[-1]
 #ResNetBlock(
 #  (conv1): Conv2d(512, 1024, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
 #  (bn1): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
 #  (conv2): Conv2d(1024, 1024, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
 #  (bn2): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
 #  (shortcut): Sequential(
 #    (0): Conv2d(512, 1024, kernel_size=(1, 1), stride=(2, 2), bias=False)
 #    (1): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
 #  )
 #)

Pytorch的钩子函数

Pytorch有许多钩子函数,这些函数可以处理在向前或后向传播期间流经模型的信息。我们可以使用它来检查中间梯度值,更改特定层的输出。

在这里,我们这里将关注两个方法:

register_full_backward_hook(hook, prepend=False)

该方法在模块上注册了一个后向传播的钩子,当调用backward()方法时,钩子函数将会运行。后向钩子函数接收模块本身的输入、相对于层的输入的梯度和相对于层的输出的梯度

 hook(module, grad_input, grad_output) -> tuple(Tensor) or None

它返回一个torch.utils.hooks.RemovableHandle,可以使用这个返回值来删除钩子。我们在后面会讨论这个问题。

*register_forward_hook(hook, , prepend=False, with_kwargs=False)

这与前一个非常相似,它在前向传播中后运行,这个函数的参数略有不同。它可以让你访问层的输出:

 hook(module, args, output) -> None or modified output

它的返回也是torch.utils.hooks.RemovableHandle

向模型添加钩子函数

为了计算Grad-CAM,我们需要定义后向和前向钩子函数。这里的目标是关于最后一个卷积层的输出的梯度,需要它的激活,即层的激活函数的输出。钩子函数会在推理和向后传播期间为我们提取这些值。

 # defines two global scope variables to store our gradients and activations
 gradients=None
 activations=None
 
 defbackward_hook(module, grad_input, grad_output):
   globalgradients# refers to the variable in the global scope
   print('Backward hook running...')
   gradients=grad_output
   # In this case, we expect it to be torch.Size([batch size, 1024, 8, 8])
   print(f'Gradients size: {gradients[0].size()}') 
   # We need the 0 index because the tensor containing the gradients comes
   # inside a one element tuple.
 
 defforward_hook(module, args, output):
   globalactivations# refers to the variable in the global scope
   print('Forward hook running...')
   activations=output
   # In this case, we expect it to be torch.Size([batch size, 1024, 8, 8])
   print(f'Activations size: {activations.size()}')

在定义了钩子函数和存储激活和梯度的变量之后,就可以在感兴趣的层中注册钩子,注册的代码如下:

 backward_hook=model.resnet_blocks[-1].register_full_backward_hook(backward_hook, prepend=False)
 forward_hook=model.resnet_blocks[-1].register_forward_hook(forward_hook, prepend=False)

检索需要的梯度和激活

现在已经为模型设置了钩子函数,让我们加载一个图像,计算gradcam。

 fromPILimportImage
 
 img_path="/your/image/path/"
 image=Image.open(img_path).convert('RGB')

Grad-CAM的详细介绍和Pytorch代码实现

为了进行推理,我们还需要对其进行预处理:

 fromtorchvisionimporttransforms
 fromtorchvision.transformsimportToTensor
 
 image_size=256
 transform=transforms.Compose([
                                transforms.Resize(image_size, antialias=True),
                                transforms.CenterCrop(image_size),
                                transforms.ToTensor(),
                                transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)),
                            ])
 
 img_tensor=transform(image) # stores the tensor that represents the image

现在就可以进行前向传播了:

 model(img_tensor.unsqueeze(0)).backward()

钩子函数的返回如下:

 Forwardhookrunning...
 Activationssize: torch.Size([1, 1024, 8, 8])
 Backwardhookrunning...
 Gradientssize: torch.Size([1, 1024, 8, 8])

得到了梯度和激活变量后就可以生成热图:

计算Grad-CAM

为了计算Grad-CAM,我们将原始论文公式进行一些简单的修改:

Grad-CAM的详细介绍和Pytorch代码实现

 pooled_gradients=torch.mean(gradients[0], dim=[0, 2, 3])

Grad-CAM的详细介绍和Pytorch代码实现

 importtorch.nn.functionalasF
 importmatplotlib.pyplotasplt
 
 # weight the channels by corresponding gradients
 foriinrange(activations.size()[1]):
     activations[:, i, :, :] *=pooled_gradients[i]
 
 # average the channels of the activations
 heatmap=torch.mean(activations, dim=1).squeeze()
 
 # relu on top of the heatmap
 heatmap=F.relu(heatmap)
 
 # normalize the heatmap
 heatmap/=torch.max(heatmap)
 
 # draw the heatmap
 plt.matshow(heatmap.detach())

结果如下:

Grad-CAM的详细介绍和Pytorch代码实现

得到的激活包含1024个特征映射,这些特征映射捕获输入图像的不同方面,每个方面的空间分辨率为8x8。通过钩子获得的梯度表示每个特征映射对最终预测的重要性。通过计算梯度和激活的元素积可以获得突出显示图像最相关部分的特征映射的加权和。通过计算加权特征图的全局平均值,可以得到一个单一的热图,该热图表明图像中对模型预测最重要的区域。这就是Grad-CAM,它提供了模型决策过程的可视化解释,可以帮助我们解释和调试模型的行为。

但是这个图能代表什么呢?我们将他与图片进行整合就能更加清晰的可视化了。

结合原始图像和热图

下面的代码将原始图像和我们生成的热图进行整合显示:

 fromtorchvision.transforms.functionalimportto_pil_image
 frommatplotlibimportcolormaps
 importnumpyasnp
 importPIL
 
 # Create a figure and plot the first image
 fig, ax=plt.subplots()
 ax.axis('off') # removes the axis markers
 
 # First plot the original image
 ax.imshow(to_pil_image(img_tensor, mode='RGB'))
 
 # Resize the heatmap to the same size as the input image and defines
 # a resample algorithm for increasing image resolution
 # we need heatmap.detach() because it can't be converted to numpy array while
 # requiring gradients
 overlay=to_pil_image(heatmap.detach(), mode='F')
                       .resize((256,256), resample=PIL.Image.BICUBIC)
 
 # Apply any colormap you want
 cmap=colormaps['jet']
 overlay= (255*cmap(np.asarray(overlay) **2)[:, :, :3]).astype(np.uint8)
 
 # Plot the heatmap on the same axes, 
 # but with alpha < 1 (this defines the transparency of the heatmap)
 ax.imshow(overlay, alpha=0.4, interpolation='nearest', extent=extent)
 
 # Show the plot
 plt.show()

Grad-CAM的详细介绍和Pytorch代码实现

这样看是不是就理解多了。由于它是一个正常的x射线结果,所以并没有什么需要特殊说明的。

Grad-CAM的详细介绍和Pytorch代码实现

再看这个例子,这个结果中被标注的是肺炎。Grad-CAM能准确显示出医生为确定是否患有肺炎而必须检查的胸部x光片区域。也就是说我们的模型的确学到了一些东西(红色区域再肺部附近)

删除钩子

要从模型中删除钩子,只需要在返回句柄中调用remove()方法。

 backward_hook.remove()
 forward_hook.remove()

总结

这篇文章可以帮助你理清Grad-CAM 是如何工作的,以及如何用Pytorch实现它。因为Pytorch包含了强大的钩子函数,所以我们可以在任何模型中使用本文的代码。

https://avoid.overfit.cn/post/59ce70fd73cc4110acd4016e992b50ea

作者:Vinícius Almeida文章来源地址https://www.toymoban.com/news/detail-418452.html

到了这里,关于Grad-CAM的详细介绍和Pytorch代码实现的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 《python深度学习》笔记(二十):神经网络的解释方法之CAM、Grad-CAM、Grad-CAM++、LayerCAM

    原理 优点 缺点 GAP 将多维特征映射降维为一个固定长度的特征向量 ①减少了模型的参数量;②保留更多的空间位置信息;③可并行计算,计算效率高;④具有一定程度的不变性 ①可能导致信息的损失;②忽略不同尺度的空间信息 CAM 利用最后一个卷积层的特征图×权重(用

    2024年02月05日
    浏览(27)
  • Grad-CAM简介

    论文名称:Grad-CAM: Visual Explanations from Deep Networks via Gradient-based Localization 论文下载地址:https://arxiv.org/abs/1610.02391 推荐代码(Pytorch):https://github.com/jacobgil/pytorch-grad-cam bilibili视频讲解:https://b23.tv/1kccjmb 对于常用的深度学习网络(例如CNN),普遍认为是个黑盒可解释性并不

    2024年02月02日
    浏览(34)
  • grad-CAM用于自己的语义分割网络【亲测】

    参考链接:https://github.com/jacobgil/pytorch-grad-cam  我遇到了 如下错误 解决方法: 将base_cam.py的第81行修改为:  就不报错了! 拿下!    

    2024年02月02日
    浏览(34)
  • [ 可视化 ] 经典网络模型 —— Grad-CAM 详解与复现

    🤵 Author :Horizon Max ✨ 编程技巧篇 :各种操作小结 🎇 机器视觉篇 :会变魔术 OpenCV 💥 深度学习篇 :简单入门 PyTorch 🏆 神经网络篇 :经典网络模型 💻 算法篇 :再忙也别忘了 LeetCode 随着神经网路模型的不断发展,深度模型通过使用 更抽象 (增加网络层数)和 更紧密

    2024年02月02日
    浏览(28)
  • yolov5热力图可视化grad-cam踩坑经验分享

    最近在做热力图的可视化,网上搜了很多的资料,但是大部分都是需要在原网络结构上进行修改,非常的不方便。最后在网上找到一位博主分享的即插即用的模块,觉得效果还可以,但是中间有些细节,需要注意。 原博文地址: https://blog.csdn.net/qq_37706472/article/details/12871460

    2024年02月04日
    浏览(31)
  • CAM实现的流程--基于Pytorch实现

    可视化CNN的工具, CAM解释网络特征变化,CAM使得弱监督学习发展成为可能,可以慢慢减少对人工标注的依赖,能降低网络训练的成本。通过可视化,就像往黑箱子里打了一个手电筒,让人们可以尝试去理解网络。 在不同空间位置处存在这些视觉图案的加权线性和, 通过简单

    2024年02月11日
    浏览(24)
  • 文本识别CRNN模型介绍以及pytorch代码实现

    提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 文本识别是图像领域的一个常见任务,场景文字识别OCR任务中,需要先检测出图像中文字位置,再对检测出的文字进行识别,文本介绍的CRNN模型可用于后者, 对检测出的文字进行识别。 An End-to-End Tra

    2024年02月07日
    浏览(30)
  • 【PyTorch 实战2:UNet 分割模型】10min揭秘 UNet 分割网络如何工作以及pytorch代码实现(详细代码实现)

      U-Net,自2015年诞生以来,便以其卓越的性能在生物医学图像分割领域崭露头角。作为FCN的一种变体,U-Net凭借其Encoder-Decoder的精巧结构,不仅在医学图像分析中大放异彩,更在卫星图像分割、工业瑕疵检测等多个领域展现出强大的应用能力。UNet是一种常用于图像分割的卷

    2024年04月28日
    浏览(32)
  • CNN经典网络模型(三):VGGNet简介及代码实现(PyTorch超详细注释版)

    一、开发背景 二、网络结构 三、模型特点 四、代码实现 1. model.py 2. train.py 3. predict.py 4. spilit_data.py 五、参考内容 VGGNet在2014年由牛津大学计算机视觉组VGG (Visual Geometry Group) 提出,斩获该年ImageNet竞赛中Localization Task (定位任务) 第一名和Classification Task(分类任务)第二名(第一

    2024年02月04日
    浏览(29)
  • CNN经典网络模型(二):AlexNet简介及代码实现(PyTorch超详细注释版)

    一、开发背景 二、网络结构 三、模型特点 四、代码实现 1. model.py 2. train.py 3. predict.py 4. spilit_data.py 五、参考内容 AlexNet由Hinton和他的学生Alex Krizhevsky设计,模型名字来源于论文第一作者的姓名Alex。该模型以很大的优势获得了2012年ISLVRC竞赛的冠军网络,分类准确率由传统的

    2024年04月12日
    浏览(29)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包