当前位置: 代码迷 >> 综合 >> 模型解释 -- Guided-Backpropagation、CAM、Grad-CAM、Grad-CAM++ 及 pyTorch Hook
  详细解决方案

模型解释 -- Guided-Backpropagation、CAM、Grad-CAM、Grad-CAM++ 及 pyTorch Hook

热度:15   发布时间:2024-02-22 15:39:05.0

1 pyTorch Hook

本节内容引用自:https://zhuanlan.zhihu.com/p/75054200

首先贴一段维基百科中对钩子的定义:

钩子编程(hooking),也称作“挂钩”,是计算机程序设计术语,指通过拦截软件模块间的函数调用、消息传递、事件传递来修改或扩展操作系统、应用程序或其他软件组件的行为的各种技术。处理被拦截的函数调用、事件、消息的代码,被称为钩子(hook)。

Hook 是 PyTorch 中一个十分有用的特性。利用它,我们可以不必改变网络输入输出的结构,方便地获取、改变网络中间层变量的值和梯度。这个功能被广泛用于可视化神经网络中间层的 feature、gradient,从而诊断神经网络中可能出现的问题,分析网络有效性。本文将结合代码,由浅入深地介绍 pytorch 中 hook 的用法。文章分为三部分:

  1. Hook for Tensors :针对 Tensor 的 hook
  2. Hook for Modules:针对例如 nn.Conv2d nn.Linear等网络模块的 hook
  3. Guided Backpropagation:利用 Hook 实现的一段神经网络可视化代码

Hook for Tensors

上面的计算图中,x y w 为叶子节点,而 z 为中间变量

在 PyTorch 的计算图(computation graph)中,只有叶子结点(leaf nodes)的变量会保留梯度。而所有中间变量的梯度只被用于反向传播,一旦完成反向传播,中间变量的梯度就将自动释放,从而节约内存。如下面这段代码所示:

import torch

x = torch.Tensor([0, 1, 2, 3]).requires_grad_()
y = torch.Tensor([4, 5, 6, 7]).requires_grad_()
w = torch.Tensor([1, 2, 3, 4]).requires_grad_()
z = x+y
# z.retain_grad()

o = w.matmul(z)
o.backward()
# o.retain_grad()

print(‘x.requires_grad:’, x.requires_grad) # True
print(‘y.requires_grad:’, y.requires_grad) # True
print(‘z.requires_grad:’, z.requires_grad) # True
print(‘w.requires_grad:’, w.requires_grad) # True
print(‘o.requires_grad:’, o.requires_grad) # True

print(‘x.grad:’, x.grad) # tensor([1., 2., 3., 4.])
print(‘y.grad:’, y.grad) # tensor([1., 2., 3., 4.])
print(‘w.grad:’, w.grad) # tensor([ 4., 6., 8., 10.])
print(‘z.grad:’, z.grad) # None
print(‘o.grad:’, o.grad) # None

由于 z 和 o 为中间变量(并非直接指定数值的变量,而是由别的变量计算得到的变量),它们虽然 requires_grad 的参数都是 True,但是反向传播后,它们的梯度并没有保存下来,而是直接删除了,因此是 None。如果想在反向传播之后保留它们的梯度,则需要特殊指定:把上面代码中的z.retain_grad()o.retain_grad的注释去掉,可以得到它们对应的梯度,运行结果如下所示:

x.requires_grad: True
y.requires_grad: True
z.requires_grad: True
w.requires_grad: True
o.requires_grad: True
x.grad: tensor([1., 2., 3., 4.])
y.grad: tensor([1., 2., 3., 4.])
w.grad: tensor([ 4., 6., 8., 10.])
z.grad: tensor([1., 2., 3., 4.])
o.grad: tensor(1.)

但是,这种加 retain_grad() 的方案会增加内存占用,并不是个好办法,对此的一种替代方案,就是用 hook 保存中间变量的梯度。

对于中间变量z,hook 的使用方式为:z.register_hook(hook_fn),其中 hook_fn为一个用户自定义的函数,其签名为:

hook_fn(grad) -> Tensor or None

它的输入为变量 z 的梯度,输出为一个 Tensor 或者是 None (None 一般用于直接打印梯度)。反向传播时,梯度传播到变量 z,再继续向前传播之前,将会传入 hook_fn。如果 hook_fn的返回值是 None,那么梯度将不改变,继续向前传播,如果 hook_fn的返回值是 Tensor 类型,则该 Tensor 将取代 z 原有的梯度,向前传播。

下面的示例代码中 hook_fn 不改变梯度值,仅仅是打印梯度:

import torch

x = torch.Tensor([0, 1, 2, 3]).requires_grad_()
y = torch.Tensor([4, 5, 6, 7]).requires_grad_()
w = torch.Tensor([1, 2, 3, 4]).requires_grad_()
z = x+y

# ===================
def hook_fn(grad):
print(grad)

z.register_hook(hook_fn)
# ===================

o = w.matmul(z)

print(’=Start backprop=’)
o.backward()
print(’=End backprop=’)

print(‘x.grad:’, x.grad)
print(‘y.grad:’, y.grad)
print(‘w.grad:’, w.grad)
print(‘z.grad:’, z.grad)

运行结果如下:

=Start backprop=
tensor([1., 2., 3., 4.])
=End backprop=
x.grad: tensor([1., 2., 3., 4.])
y.grad: tensor([1., 2., 3., 4.])
w.grad: tensor([ 4., 6., 8., 10.])
z.grad: None

我们发现,z 绑定了hook_fn后,梯度反向传播时将会打印出 oz 的偏导,和上文中 z.retain_grad()方法得到的 z 的偏导一致。

接下来可以试一下,在 hook_fn 中改变梯度值,看看会有什么结果。

import torch

x = torch.Tensor([0, 1, 2, 3]).requires_grad_()
y = torch.Tensor([4, 5, 6, 7]).requires_grad_()
w = torch.Tensor([1, 2, 3, 4]).requires_grad_()
z = x + y

# ===================
def hook_fn(grad):
g = 2 * grad
print(g)
return g

z.register_hook(hook_fn)
# ===================

o = w.matmul(z)

print(’=Start backprop=’)
o.backward()
print(’=End backprop=’)

print(‘x.grad:’, x.grad)
print(‘y.grad:’, y.grad)
print(‘w.grad:’, w.grad)
print(‘z.grad:’, z.grad)

运行结果如下:

=Start backprop=
tensor([2., 4., 6., 8.])
=End backprop=
x.grad: tensor([2., 4., 6., 8.])
y.grad: tensor([2., 4., 6., 8.])
w.grad: tensor([ 4., 6., 8., 10.])
z.grad: None

发现 z 的梯度变为两倍后,受其影响,xy的梯度也都变成了原来的两倍。

在实际代码中,为了方便,也可以用 lambda 表达式来代替函数,简写为如下形式:

import torch

x = torch.Tensor([0, 1, 2, 3]).requires_grad_()
y = torch.Tensor([4, 5, 6, 7]).requires_grad_()
w = torch.Tensor([1, 2, 3, 4]).requires_grad_()
z = x + y

# ===================
z.register_hook(lambda x: 2*x)
z.register_hook(lambda x: print(x))
# ===================

o = w.matmul(z)

print(’=Start backprop=’)
o.backward()
print(’=End backprop=’)

print(‘x.grad:’, x.grad)
print(‘y.grad:’, y.grad)
print(‘w.grad:’, w.grad)
print(‘z.grad:’, z.grad)

运行结果和上面的代码相同,我们发现一个变量可以绑定多个 hook_fn,反向传播时,它们按绑定顺序依次执行。例如上面的代码中,第一个绑定的 hook_fnz的梯度乘以2,第二个绑定的 hook_fn打印z的梯度。因此反向传播时,也是按照这个顺序执行的,打印出来的 z的梯度值,是其原本梯度值的两倍。

至此,针对对 Tensor 的 hook 就介绍完了。然而它的使用场景一般不多,最常用的 hook 是针对神经网络模块的。

Hook for Modules

网络模块 module 不像上一节中的 Tensor,拥有显式的变量名可以直接访问,而是被封装在神经网络中间。我们通常只能获得网络整体的输入和输出,对于夹在网络中间的模块,我们不但很难得知它输入/输出的梯度,甚至连它输入输出的数值都无法获得。除非设计网络时,在 forward 函数的返回值中包含中间 module 的输出,或者用很麻烦的办法,把网络按照 module 的名称拆分再组合,让中间层提取的 feature 暴露出来。

为了解决这个麻烦,PyTorch 设计了两种 hook:register_forward_hookregister_backward_hook,分别用来获取正/反向传播时,中间层模块输入和输出的 feature/gradient,大大降低了获取模型内部信息流的难度。

register forward hook

register_forward_hook的作用是获取前向传播过程中,各个网络模块的输入和输出。对于模块 module,其使用方式为:module.register_forward_hook(hook_fn) 。其中 hook_fn的签名为:

hook_fn(module, input, output) -> None

它的输入变量分别为:模块,模块的输入,模块的输出,和对 Tensor 的 hook 不同,forward hook 不返回任何值,也就是说不能用它来修改输入或者输出的值(注意:从 pytorch 1.2.0 开始,forward hook 也有返回值了,可以修改网络模块的输出),但借助这个 hook,我们可以方便地用预训练的神经网络提取特征,而不用改变预训练网络的结构。下面提供一段示例代码:

import torch
from torch import nn# 首先我们定义一个模型
class Model(nn.Module):def __init__(self):super(Model, self).__init__()self.fc1 = nn.Linear(3, 4)self.relu1 = nn.ReLU()self.fc2 = nn.Linear(4, 1)self.initialize()# 为了方便验证,我们将指定特殊的weight和biasdef initialize(self):with torch.no_grad():self.fc1.weight = torch.nn.Parameter(torch.Tensor([[1., 2., 3.],[-4., -5., -6.],[7., 8., 9.],[-10., -11., -12.]]))self.fc1.bias = torch.nn.Parameter(torch.Tensor([1.0, 2.0, 3.0, 4.0]))self.fc2.weight = torch.nn.Parameter(torch.Tensor([[1.0, 2.0, 3.0, 4.0]]))self.fc2.bias = torch.nn.Parameter(torch.Tensor([1.0]))def forward(self, x):o = self.fc1(x)o = self.relu1(o)o = self.fc2(o)return o# 全局变量,用于存储中间层的 feature
total_feat_out = []
total_feat_in = []# 定义 forward hook function
def hook_fn_forward(module, input, output):print(module) # 用于区分模块print('input', input) # 首先打印出来print('output', output)total_feat_out.append(output) # 然后分别存入全局 list 中total_feat_in.append(input)model = Model()modules = model.named_children() # 
for name, module in modules:module.register_forward_hook(hook_fn_forward)# 注意下面代码中 x 的维度,对于linear module,输入一定是大于等于二维的
# (第一维是 batch size)。在 forward hook 中看不出来,但是 backward hook 中,
# 得到的梯度完全不对。
# 有一篇 hook 的教程就是这里出了错,作者还强行解释x = torch.Tensor([[1.0, 1.0, 1.0]]).requires_grad_() 
o = model(x)
o.backward()print('==========Saved inputs and outputs==========')
for idx in range(len(total_feat_in)):print('input: ', total_feat_in[idx])print('output: ', total_feat_out[idx])

运行结果为:

Linear(in_features=3, out_features=4, bias=True)
input (tensor([[1., 1., 1.]], requires_grad=True),)
output tensor([[ 7., -13., 27., -29.]], grad_fn=<AddmmBackward>)
ReLU()
input (tensor([[ 7., -13., 27., -29.]], grad_fn=<AddmmBackward>),)
output tensor([[ 7., 0., 27., 0.]], grad_fn=<ThresholdBackward0>)
Linear(in_features=4, out_features=1, bias=True)
input (tensor([[ 7., 0., 27., 0.]], grad_fn=<ThresholdBackward0>),)
output tensor([[89.]], grad_fn=<AddmmBackward>)
Saved inputs and outputs
input: (tensor([[1., 1., 1.]], requires_grad=True),)
output: tensor([[ 7., -13., 27., -29.]], grad_fn=<AddmmBackward>)
input: (tensor([[ 7., -13., 27., -29.]], grad_fn=<AddmmBackward>),)
output: tensor([[ 7., 0., 27., 0.]], grad_fn=<ThresholdBackward0>)
input: (tensor([[ 7., 0., 27., 0.]], grad_fn=<ThresholdBackward0>),)
output: tensor([[89.]], grad_fn=<AddmmBackward>)

读者可以用笔验证一下,这里限于篇幅,就不做验证了。

register backward hook

register_forward_hook相似,register_backward_hook 的作用是获取神经网络反向传播过程中,各个模块输入端和输出端的梯度值。对于模块 module,其使用方式为:module.register_backward_hook(hook_fn) 。其中hook_fn的函数签名为:

hook_fn(module, grad_input, grad_output) -> Tensor or None

它的输入变量分别为:模块,模块输入端的梯度,模块输出端的梯度。需要注意的是,这里的输入端输出端,是站在前向传播的角度的,而不是反向传播的角度。例如线性模块:o=W*x+b,其输入端为 W,x 和 b,输出端为 o。

如果模块有多个输入或者输出的话,grad_inputgrad_output可以是 tuple 类型。对于线性模块:o=W*x+b ,它的输入端包括了W、x 和 b 三部分,因此 grad_input 就是一个包含三个元素的 tuple。

这里注意和 forward hook 的不同:

  1. 在 forward hook 中,input 是 x,而不包括 W 和 b。
  2. 返回 Tensor 或者 None,backward hook 函数不能直接改变它的输入变量,但是可以返回新的 grad_input,反向传播到它上一个模块。

Talk is cheap,下面看示例代码:

import torch
from torch import nnclass Model(nn.Module):def __init__(self):super(Model, self).__init__()self.fc1 = nn.Linear(3, 4)self.relu1 = nn.ReLU()self.fc2 = nn.Linear(4, 1)self.initialize()def initialize(self):with torch.no_grad():self.fc1.weight = torch.nn.Parameter(torch.Tensor([[1., 2., 3.],[-4., -5., -6.],[7., 8., 9.],[-10., -11., -12.]]))self.fc1.bias = torch.nn.Parameter(torch.Tensor([1.0, 2.0, 3.0, 4.0]))self.fc2.weight = torch.nn.Parameter(torch.Tensor([[1.0, 2.0, 3.0, 4.0]]))self.fc2.bias = torch.nn.Parameter(torch.Tensor([1.0]))def forward(self, x):o = self.fc1(x)o = self.relu1(o)o = self.fc2(o)return ototal_grad_out = []
total_grad_in = []def hook_fn_backward(module, grad_input, grad_output):print(module) # 为了区分模块# 为了符合反向传播的顺序,我们先打印 grad_outputprint('grad_output', grad_output) # 再打印 grad_inputprint('grad_input', grad_input)# 保存到全局变量total_grad_in.append(grad_input)total_grad_out.append(grad_output)model = Model()modules = model.named_children()
for name, module in modules:module.register_backward_hook(hook_fn_backward)# 这里的 requires_grad 很重要,如果不加,backward hook
# 执行到第一层,对 x 的导数将为 None,某英文博客作者这里疏忽了
# 此外再强调一遍 x 的维度,一定不能写成 torch.Tensor([1.0, 1.0, 1.0]).requires_grad_()
# 否则 backward hook 会出问题。
x = torch.Tensor([[1.0, 1.0, 1.0]]).requires_grad_()
o = model(x)
o.backward()print('==========Saved inputs and outputs==========')
for idx in range(len(total_grad_in)):print('grad output: ', total_grad_out[idx])print('grad input: ', total_grad_in[idx])

读者可以自己用笔算一遍,验证正确性。需要注意的是,对线性模块,其grad_input 是一个三元组,排列顺序分别为:对 bias 的导数,对输入 x 的导数,对权重 W 的导数。

注意事项

register_backward_hook只能操作简单模块,而不能操作包含多个子模块的复杂模块。 如果对复杂模块用了 backward hook,那么我们只能得到该模块最后一次简单操作的梯度信息。对于上面的代码稍作修改,不再遍历各个子模块,而是把 model 整体绑在一个 hook_fn_backward上:

model = Model()
model.register_backward_hook(hook_fn_backward) 

输出结果如下:

Model((fc1): Linear(in_features=3, out_features=4, bias=True)(relu1): ReLU()(fc2): Linear(in_features=4, out_features=1, bias=True)
)
grad_output (tensor([[1.]]),)
grad_input (tensor([1.]), tensor([[1., 2., 3., 4.]]), tensor([[ 7.],[ 0.],[27.],[ 0.]]))
==========Saved inputs and outputs==========
grad output:  (tensor([[1.]]),)
grad input:  (tensor([1.]), tensor([[1., 2., 3., 4.]]), tensor([[ 7.],[ 0.],[27.],[ 0.]]))

我们发现,程序只输出了 fc2 的梯度信息。

除此之外,有人还总结(吐槽)了 backward hook 在全连接层和卷积层表现不一致的地方(Feedback about PyTorch register_backward_hook · Issue #12331 · pytorch/pytorch)

  1. 形状
    1. 在卷积层中,weight 的梯度和 weight 的形状相同
    2. 在全连接层中,weight 的梯度的形状是 weight 形状的转秩(观察上文中代码的输出可以验证)
  2. grad_input tuple 中各梯度的顺序
    1. 在卷积层中,bias 的梯度位于tuple 的末尾:grad_input = (对feature的导数,对权重 W 的导数,对 bias 的导数)
    2. 在全连接层中,bias 的梯度位于 tuple 的开头:grad_input=(对 bias 的导数,对 feature 的导数,对 W 的导数)
  3. 当 batchsize>1时,对 bias 的梯度处理不同
    1. 在卷积层,对 bias 的梯度为整个 batch 的数据在 bias 上的梯度之和:grad_input = (对feature的导数,对权重 W 的导数,对 bias 的导数)
    2. 在全连接层,对 bias 的梯度是分开的,bach 中每条数据,对应一个 bias 的梯度:grad_input = ((data1 对 bias 的导数,data2 对 bias 的导数 ...),对 feature 的导数,对 W 的导数)

2 反卷积和Guided-Backpropagation

2.1 反卷积

引用自:https://zhuanlan.zhihu.com/p/140896660

反卷积最早出现在ZFNet论文中,将前向运算的结果反向卷积映射到图像空间,进行了特征的可视化操作。

一般的卷积网络基本构成块是卷积层 + ReLU激活函数+最大池化,反卷积网络的实现过程就是这三个过程进行逆向操作。

1.反最大池化
最大池化即在一个特征块中只保留了最大响应,忽略了其余响应。需要反最大池化时,则记录原最大响应值的索引位置,反池化的时候,将传递回来的值填到最大值所在的索引位置,其余位置补0.
在这里插入图片描述2.反ReLU
前向计算的时候执行了ReLU操作,反向变化时为了保证一致性,也进行了ReLU操作,这次ReLU相当于对输出中大于0的值进行了保留。

3.反卷积操作
以滑动窗的方式进行卷积运算效率很低,主流的实现思路都是im2col+矩阵乘法进行实现。im2col就是把各滑动窗覆盖的图像块拉成一列组成矩阵,把卷积核转换成一个行向量和矩阵进行相乘操作进行实现,如下图所示:

在这里插入图片描述
在这里插入图片描述
反卷积操作就是根据输出得到输入,其实现是将卷积核转置后和输出进行矩阵乘,然后通过col2img得到和输入图像大小一致的矩阵。所以反卷积称为是转置卷积更加合适。
在这里插入图片描述在这里插入图片描述

2.2 Guided-Backpropagation

引用自:https://zhuanlan.zhihu.com/p/142654696

除了反卷积之外,还可以通过反向传播中输出相对于输入的梯度值大小判定哪些区域最大化激活了一个特征。

反向传播和反卷积最大的区别在于,数据通过ReLU时的处理方式不同。反向传播中,输入大于0的位置的梯度得以保留;反卷积中,输出大于0的位置的信息得以保留。Guided-Backpropagation则是反向传播和反卷积的结合,只保留输入和输出都大于0的位置的梯度。三种方式的对比如下图所示:
在这里插入图片描述Guided-Backpropagation是在普通反向传播的基础上阻止了小于0的梯度的反传,输出相对于输入的梯度小于0,说明两者是负相关的,说明梯度小于0部分对应的输入阻止了得到正确的输出。那么反传的时候不传输这部分的信息,就有助于我们找到有助于得到正确输出的输入部分

代码实现,引用自:https://zhuanlan.zhihu.com/p/75054200:

import torch
from torch import nnclass Guided_backprop():def __init__(self, model):self.model = modelself.image_reconstruction = Noneself.activation_maps = []self.model.eval()self.register_hooks()def register_hooks(self):def first_layer_hook_fn(module, grad_in, grad_out):# 在全局变量中保存输入图片的梯度,该梯度由第一层卷积层# 反向传播得到,因此该函数需绑定第一个 Conv2d Layerself.image_reconstruction = grad_in[0]def forward_hook_fn(module, input, output):# 在全局变量中保存 ReLU 层的前向传播输出# 用于将来做 guided backpropagationself.activation_maps.append(output)def backward_hook_fn(module, grad_in, grad_out):# ReLU 层反向传播时,用其正向传播的输出作为 guide# 反向传播和正向传播相反,先从后面传起grad = self.activation_maps.pop() # ReLU 正向传播的输出要么大于0,要么等于0,# 大于 0 的部分,梯度为1,# 等于0的部分,梯度还是 0grad[grad > 0] = 1 # grad_out[0] 表示 feature 的梯度,只保留大于 0 的部分positive_grad_out = torch.clamp(grad_out[0], min=0.0)# 创建新的输入端梯度new_grad_in = positive_grad_out * grad# ReLU 不含 parameter,输入端梯度是一个只有一个元素的 tuplereturn (new_grad_in,)# 获取 module,这里只针对 alexnet,如果是别的,则需修改modules = list(self.model.features.named_children())# 遍历所有 module,对 ReLU 注册 forward hook 和 backward hookfor name, module in modules:if isinstance(module, nn.ReLU):module.register_forward_hook(forward_hook_fn)module.register_backward_hook(backward_hook_fn)# 对第1层卷积层注册 hookfirst_layer = modules[0][1]first_layer.register_backward_hook(first_layer_hook_fn)def visualize(self, input_image, target_class):# 获取输出,之前注册的 forward hook 开始起作用model_output = self.model(input_image)self.model.zero_grad()pred_class = model_output.argmax().item()# 生成目标类 one-hot 向量,作为反向传播的起点grad_target_map = torch.zeros(model_output.shape,dtype=torch.float)if target_class is not None:grad_target_map[0][target_class] = 1else:grad_target_map[0][pred_class] = 1# 反向传播,之前注册的 backward hook 开始起作用 model_output.backward(grad_target_map)# 得到 target class 对输入图片的梯度,转换成图片格式result = self.image_reconstruction.data[0].permute(1,2,0)return result.numpy()def normalize(I):# 归一化梯度map,先归一化到 mean=0 std=1norm = (I-I.mean())/I.std()# 把 std 重置为 0.1,让梯度map中的数值尽可能接近 0norm = norm * 0.1# 均值加 0.5,保证大部分的梯度值为正norm = norm + 0.5# 把 0,1 以外的梯度值分别设置为 0 和 1norm = norm.clip(0, 1)return normif __name__=='__main__':from torchvision import models, transformsfrom PIL import Imageimport matplotlib.pyplot as pltimage_path = './cat.png'I = Image.open(image_path).convert('RGB')means = [0.485, 0.456, 0.406]stds = [0.229, 0.224, 0.225]size = 224transform = transforms.Compose([transforms.Resize(size),transforms.CenterCrop(size),transforms.ToTensor(),transforms.Normalize(means, stds)])tensor = transform(I).unsqueeze(0).requires_grad_()model = models.alexnet(pretrained=True)guided_bp = Guided_backprop(model)result = guided_bp.visualize(tensor, None)result = normalize(result)plt.imshow(result)plt.show()print('END')

输入图像:
在这里插入图片描述输出结果:
在这里插入图片描述

Guided-Backpropagation的缺点是对target class不敏感,设置不同的target class,最终得到的梯度图可能差异不大。因此后续又发展除了Grad-CAM和Grad-CAM++等方法。

3 CAM

参考:https://blog.csdn.net/dhaiuda/article/details/102937760?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-3.nonecase&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-3.nonecase#GradCAM_69

CAM,Classification Activation Mapping。目的在于定位出图像上的哪些区域帮助CNN完成了图像的分类。

普通的CNN网络,一般分成特征提取和特征分类两大模块,CAM的实现为对输出的特征进行GAP(Global Average Pooling),得到数量等于输出channel数的一个向量。后面再接一个全连接层,输入神经元数量等于feature的channel数,输出神经元的数量等于数据集的类别数。这样的网络训练完成图像的分类任务后,全连接层的权重向量的含义就表示各个channel的feature对于目标分类的贡献度。那么就可以权重值乘以对应feature,全部相加之后上采样到输入图像的大小,叠加到输入图像上定位出图像的当前分类主要取决于哪一区域。整个过程如下图所示:
在这里插入图片描述
CAM的缺点在于,对于原有网络结构未在最后使用GAP的模型,需要更改网络结构重新进行模型训练才能得到CAM图。因此实际应用中工作量较大。更加推荐使用Grad-CAM和Grad-CAM++获取CAM图。

4 Grad-CAM

论文:https://ramprs.github.io/static/docs/IJCV_Grad-CAM.pdf

Grad-CAM以及下面要介绍的Grad-CAM++都和CAM的思路一致,权重系数乘以feature map,相加后上采样,叠加到原始图像上,得到CAM图。不同点在于权重系数的获取思路不一致。

Grad-CAM中权重系数的计算公式为:
在这里插入图片描述在这里插入图片描述
Grad-CAM的缺点是对于密集的多目标定位不够准确

5 Grad-CAM++

参考:https://zhuanlan.zhihu.com/p/46200853

推导过程:
定义YcY^cYc表示图像被预测为第c类的概率值;AijkA_{ij}^kAijk?表示第k个特征图(i,j)(i,j)(i,j)位置的值。
在这里插入图片描述

定义wkcw_k^cwkc?表示第k个特征图对于图像被分为第c类的贡献。grad-cam++中的wkcw_k^cwkc?为:
在这里插入图片描述

公式(5)带入公式(1)中得到:
在这里插入图片描述
因为relu只是一个阈值,可以忽略。

求导有:
在这里插入图片描述
整理得到:

在这里插入图片描述
公式(9)中要求二阶导数和三阶导数,不易于实现,下面进行简化。

原则上,YcY^cYc可以说任意预测的分数,唯一的要求是必须是光滑函数。由于这个原因,不像Grad-CAM(把倒数第二层表示作为分数YcY^cYc),我们把倒数第二层分数传入一个指数函数,因为它可微。如果我们把倒数第二层分数传入指数函数,网络的最后一层卷积和倒数第二层只有线性或ReLU激活,计算高阶倒数很容易了。定义ScS^cSc为倒数第二层的分数,有:
在这里插入图片描述
在这里插入图片描述
对于relu函数,有:
在这里插入图片描述
把式子(14)代入(12)中,有:
在这里插入图片描述至此,只需要一阶导数,就得到了αijkc\alpha_{ij}^{kc}αijkc?,也就有了wkcw_k^cwkc?

有了wkcw_k^cwkc?之后,显著图即为:
在这里插入图片描述

6 对比及实现

放一张CAM、Grad-CAM、Grad-CAM++的对比图:
在这里插入图片描述实现:

#coding=utf-8import torch
import torch.nn as nn
import torchvision
import cv2
from PIL import Image
import torchvision.transforms as transform
import matplotlib.pyplot as plt
import numpy as np
import jsonclass Guided_backProp():def __init__(self,model):self.model = model#self.activations = []self.input_grad = Noneself.add_hook()# def forward_hook(self,module,input,output):# self.activations.append(output)def backward_hook(self,module,grad_in,grad_out):return (torch.clamp(grad_in[0],0.),)def firstlayer_backward_hook(self,module,grad_in,grad_out):#对卷积层,grad_input = (对输入feature的导数,对权重 W 的导数,对 bias 的导数)#对全连接层,grad_input=(对 bias 的导数,对输入feature 的导数,对 W 的导数) self.input_grad = grad_in[0]def add_hook(self):modules = list(self.model.named_children())for name,layer in modules:if isinstance(layer,nn.ReLU):# layer.register_forward_hook(self.forward_hook)layer.register_backward_hook(self.backward_hook)first_layer = modules[0][1]first_layer.register_backward_hook(self.firstlayer_backward_hook)def __call__(self,inputs,index,class_names):self.model.eval()pred = self.model(Img_trans)self.model.zero_grad()pred = torch.nn.functional.softmax(pred)if index == None:val,index = torch.max(pred,dim=1)print('预测结果:%s' %class_names[str(index.item())])one_hot = torch.zeros(pred.shape)one_hot[0][index] = 1#backwardpred.backward(one_hot)return self.input_grad[0].permute(1,2,0).numpy(),Falseclass GradCAM():def __init__(self,model,module_name,layer_name):self.model = modelself.model.eval()self.module_name = module_nameself.layer_name = layer_nameself.features = Noneself.grad = Noneself.add_hook()def forward_hook(self,layer,input,output):self.features = outputdef backward_hook(self,layer,grad_in,grad_out):self.grad = grad_in[0]def add_hook(self):for name_module,module in self.model.named_children():if name_module == self.module_name:for name_layer,layer in module.named_children():if name_layer == self.layer_name:layer.register_forward_hook(self.forward_hook)layer.register_backward_hook(self.backward_hook)def __call__(self,input,index,class_names):self.model.eval()self.model.zero_grad()pred = self.model(input)if index == None:val,index = torch.max(pred,dim=1)print('预测结果:%s' %class_names[str(index.item())])one_hot = torch.zeros(pred.shape)one_hot[0][index] = 1pred.backward(one_hot)w_c = torch.mean(self.grad.view(self.grad.shape[0],self.grad.shape[1],-1),dim=2)output = w_c[0].view(-1,1,1) * self.features[0]output = torch.sum(output,dim=0)return output.detach().numpy(),Trueclass GradCAMPlusPlus(GradCAM):def __call__(self,input,index,class_names):self.model.eval()self.model.zero_grad()pred = self.model(input)if index == None:val,index = torch.max(pred,dim=1)print('预测结果:%s' %class_names[str(index.item())])one_hot = torch.zeros(pred.shape)one_hot[0][index] = 1pred.backward(one_hot)#grad = torch.clamp(self.grad,min=0.)alpha_num = self.grad.pow(2)alpha_denom = 2 * alpha_num + self.features.mul(self.grad.pow(3)).view(self.features.shape[0],self.features.shape[1],-1).sum(dim=2).view(self.features.shape[0],self.features.shape[1],1,1)alpha = alpha_num / (alpha_denom+1e-8)weights = (alpha * grad).view(alpha.shape[0],alpha.shape[1],-1).sum(dim=2)saliency_map = (weights.view(weights.shape[0],weights.shape[1],1,1) * self.features).sum(dim=1)output = torch.clamp(saliency_map,0.)return output[0].detach().numpy(),Truedef Show_Res(Img,input_grad,bAdd):if bAdd:input_grad = (input_grad - input_grad.min()) / input_grad.max()image = np.array(Img).astype(np.float) / 255.rez_img = cv2.resize(input_grad,(Img.size[0],Img.size[1]))rez_img = (rez_img * 255).astype(np.uint8)heatmap = cv2.applyColorMap(rez_img, cv2.COLORMAP_JET).astype(np.float)heatmap /= 255.result = heatmap + imageresult = result / result.max()else:#归一化input_gradinput_grad = (input_grad - input_grad.mean()) / input_grad.std()input_grad *= 0.1input_grad += 0.5input_grad = cv2.resize(input_grad,(Img.size[0],Img.size[1]))result = input_grad.clip(0,1)return resultif __name__ == "__main__":#读取一张图像,预处理Img = Image.open('./DogAndCat.jpg').convert('RGB')#DogAndCat.jpgmean=[0.485, 0.456, 0.406]std=(0.229, 0.224, 0.225)transfer = transform.Compose([transform.Resize((224,224)),transform.ToTensor(),transform.Normalize(mean,std)])Img_trans = transfer(Img).unsqueeze(0).requires_grad_()#class-nameswith open('./labels.json') as fp:class_names = json.load(fp)#加载一个预训练模型model = torchvision.models.resnet34(pretrained = True)#gbname = 'gb.jpg'gb = Guided_backProp(model)output,bAdd = gb(Img_trans,None,class_names)GBResImg = Show_Res(Img,output,bAdd)cv2.imwrite(name,(GBResImg * 255).astype(np.uint8))#grad-camname = 'grad-cam.jpg'cam = GradCAM(model,'layer4','2')output,bAdd = cam(Img_trans,None,class_names)GradCamResImg = Show_Res(Img,output,bAdd)cv2.imwrite(name,(GradCamResImg * 255).astype(np.uint8))#grad-cam++name = 'grad-cam++.jpg'cam = GradCAMPlusPlus(model,'layer4','2')output,bAdd = cam(Img_trans,None,class_names)GradCamPPResImg = Show_Res(Img,output,bAdd) cv2.imwrite(name,(GradCamPPResImg* 255).astype(np.uint8))#showResImg = np.hstack((GBResImg,GradCamResImg,GradCamPPResImg))cv2.imshow('Res',ResImg)cv2.waitKey()

结果:
在这里插入图片描述
更多对比结果:
在这里插入图片描述
密集目标下,Grad-CAM++ 略由于 Grad-CAM:

Grad-CAM++ :
在这里插入图片描述Grad-CAM:
在这里插入图片描述

参考:
https://zhuanlan.zhihu.com/p/37223341
https://zhuanlan.zhihu.com/p/75054200
https://zhuanlan.zhihu.com/p/140896660
https://zhuanlan.zhihu.com/p/142654696
https://zhuanlan.zhihu.com/p/46200853
https://github.com/yizt/Grad-CAM.pytorch
https://github.com/jacobgil/pytorch-grad-cam
https://blog.csdn.net/tj253750329/article/details/100518630