Grad-CAM的详细介绍和Pytorch代码实现_天天通讯
Grad-CAM(Gradient-weightedClassActivationMapping)是一种可视化深度神经网络中哪些部分对于预测结果贡献最
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的实现,我将使用来自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()
首先我们看看这个模型的架构。就像前面提到的,我们需要识别最后一个卷积层,特别是它的激活函数。这一层表示模型学习到的最复杂的特征,它最有能力帮助我们理解模型的行为,下面是我们这个演示模型的代码:
import torch import torch.nn as nn import torch.nn.functional as F # hyperparameters nc = 3 # number of channels nf = 64 # number of features to begin with dropout = 0.2 device = torch.device("cuda" if torch.cuda.is_available() else "cpu") # setup a resnet block and its forward function class ResNetBlock(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() if stride != 1 or in_channels != out_channels: self.shortcut = nn.Sequential( nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=stride, bias=False), nn.BatchNorm2d(out_channels) ) def forward(self, x): out = F.relu(self.bn1(self.conv1(x))) out = self.bn2(self.conv2(out)) out += self.shortcut(x) out = F.relu(out) return out # setup the final model structure class XRayClassifier(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(), ) def forward(self, input): output = self.resnet_blocks(input.to(device)) output = self.classifier(output) return output
模型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 def backward_hook(module, grad_input, grad_output): global gradients # 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. def forward_hook(module, args, output): global activations # 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。
from PIL import Image img_path = "/your/image/path/" image = Image.open(img_path).convert("RGB")
为了进行推理,我们还需要对其进行预处理:
from torchvision import transforms from torchvision.transforms import ToTensor 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()
钩子函数的返回如下:
Forward hook running... Activations size: torch.Size([1, 1024, 8, 8]) Backward hook running... Gradients size: torch.Size([1, 1024, 8, 8])
得到了梯度和激活变量后就可以生成热图:
计算Grad-CAM为了计算Grad-CAM,我们将原始论文公式进行一些简单的修改:
pooled_gradients = torch.mean(gradients[0], dim=[0, 2, 3])
import torch.nn.functional as F import matplotlib.pyplot as plt # weight the channels by corresponding gradients for i in range(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())
结果如下:
得到的激活包含1024个特征映射,这些特征映射捕获输入图像的不同方面,每个方面的空间分辨率为8x8。通过钩子获得的梯度表示每个特征映射对最终预测的重要性。通过计算梯度和激活的元素积可以获得突出显示图像最相关部分的特征映射的加权和。通过计算加权特征图的全局平均值,可以得到一个单一的热图,该热图表明图像中对模型预测最重要的区域。这就是Grad-CAM,它提供了模型决策过程的可视化解释,可以帮助我们解释和调试模型的行为。
但是这个图能代表什么呢?我们将他与图片进行整合就能更加清晰的可视化了。
结合原始图像和热图下面的代码将原始图像和我们生成的热图进行整合显示:
from torchvision.transforms.functional import to_pil_image from matplotlib import colormaps import numpy as np import PIL # 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()
这样看是不是就理解多了。由于它是一个正常的x射线结果,所以并没有什么需要特殊说明的。
再看这个例子,这个结果中被标注的是肺炎。Grad-CAM能准确显示出医生为确定是否患有肺炎而必须检查的胸部x光片区域。也就是说我们的模型的确学到了一些东西(红色区域再肺部附近)
删除钩子要从模型中删除钩子,只需要在返回句柄中调用remove()方法。
backward_hook.remove() forward_hook.remove()总结
这篇文章可以帮助你理清Grad-CAM 是如何工作的,以及如何用Pytorch实现它。因为Pytorch包含了强大的钩子函数,所以我们可以在任何模型中使用本文的代码。
关键词:
Grad-CAM(Gradient-weightedClassActivationMapping)是一种可视化深度神经网络中哪些部分对于预测结果贡献最
据美团数据显示,14日火车票订单量已达今年春运峰值2倍。凭借烧烤出圈的淄博再成顶流,火车票搜索增幅位居
近日,话题 月薪1万的同事一年存下11万 冲上热搜前排,讨论度突破4 5亿。有人表示每个月仅留800多元生活太
1、中山的邮政编码是528400 中山市是中国4个不设市辖区的地级市之一。2、前身为1152年设立的香山县;1925年
中国消费者报南宁讯(记者顾艳伟)近日,广西壮族自治区百色市市场监管部门根据群众举报,通过蹲守布控,成
4月19日消息,编程美国当地时间4月18日,特斯拉将javascriptModelY长续航版售价下调9 1%至49990美元,Model
4月18日,2023年第三届北京森林城市艺术节暨第五届将府公园森林影像艺术季正式开幕。以“融合·共生”为主
“谷雨春光晓,山川黛色青。”今天开始是谷雨节气。常年这一节气广州雨量又上了一个台阶,今年谷雨节气也将
4月19日,广西柳州市柳北区“物业+养老”服务驿站启动仪式在保利大江郡小区举行。全区养老服务机构和设施达
原标题:补齐数字乡村建设短板曹诚平近日,中央网信办、国家乡村振兴局等部门联合印发《2023年数字乡村发展
昆明信息港讯(昆明日报记者郭曼)高校和科研院所拥有不少专利技术,却无法实现产业化;中小企业有能力在市
有报道称小米旗下POCOF5Pro型号为“23013PC75G”的手机最近在Geekbench认证页面上亮相。据页面显示,POCOF5
通过查巫术的这件案子张汤受到刘彻的重用提拔为廷尉,在这之后张汤负责的一件比较大的案子就是淮南王造反的
据悉,2023中原农谷预制菜产业发展论坛汇聚了海内外400多位业界嘉宾,来自美国、泰国等国外农业领域的专家
今天来聊聊关于乐器的种类翻译,乐器的种类的文章,现在就为大家来简单介绍下乐器的种类翻译,乐器的种类,
4月20日,A股上市公司聚和材料发布2022年全年业绩报告。其中,净利润3 91亿元,同比增长58 53%。根据同花顺
该乐伎服饰装束与众不同,位居显要,为乐队领乐。
黑龙江印发规划优化生态系统碳汇资源空间布局-4月18日,《黑龙江省生态系统增汇规划(2021—2030年)》日前
格隆汇4月20日丨4月19日,主题为“艺境·腾龙”的艺龙酒店2周年盛典暨投资品鉴会在苏州举行。会上,艺龙酒
别克安吉星可以免费使用五年时长,以前别克安吉星系统只能免费使用一年,但现在进行调整,变成了五年时长,
同花顺金融研究中心4月19日讯,有投资者向怡亚通提问,请问董秘,公司极高的负债率和极低的毛利率以及净利
浙江省玉环市清港镇垟根村文旦种植基地,无人驾驶的“小火车”沿着轨道往山下运输文旦。近年来,玉环市农业
1、食材洗净,扇贝提前2小时放在淡盐水内吐沙子,然后用刷子把壳上的残留砂砾去除,黄蚬子用淡盐水浸泡1小
瑞纳智能(301129)04月20日在投资者关系平台上答复了投资者关心的问题。
你们好,最近小未来发现有诸多的小伙伴们对于男士领带打法视频单手,男士领带打法视频教程这个问题都颇为感
Copyright 2015-2023 华夏酒业网 版权所有 备案号:琼ICP备2022009675号-37 联系邮箱:435 227 67@qq.com