本文转载于⚡哥的知乎专栏,大佬写的真是清晰易懂,菜鸡的我默默留存,好好学习!🙇‍♂️

觉得不错的,大家记得帮电哥点赞👍+收藏⭐呀!

原理

① 要做猫狗的二分类任务,网络的分类器是输出为两个神经元的全连接层,两个神经元的输出分别为$z=\left[z_{c}, z_{d}\right]$,其中猫的概率为$p_c$,狗的概率为$p_d$,且$\left[p_{c}, p_{d}\right]=\operatorname{softmax}(z)$。
② 要可视化猫这个类别的GradCAM,通过$z_c$对CNN最后一层的所有特征图$A_{i, j}^{k}$求偏导$G_{i, j}^{k}=\frac{\partial z_{c}}{\partial A_{i, j}^{k}}$,其中$A_{i, j}^{k}$表示特征图$A$ shape=(1, C, H, W)的第k通道的(i, j)坐标点,最终的偏导特征图$G$ shape=(1, C, H, W)。

  • 取最后一层的原因:

    • GradCAM可以用来可视化任何的激活特征图,但论文的主要目的是要解释神经网络得到决策的可能原因。
    • 最后一层特征图有丰富的highlevel语义信息和详细的空间信息,而全连接层完全丢失了空间信息。
  • 求偏导的意义:

    • 偏导表示输出关于输入的变化率,也就是特征图上变化一个单位,得到的输出变化多少单位。可以反映出输出$z_c$关于$A_{i, j}^{k}$的敏感程度,如果梯度大,则非常敏感,表示该位置更有可能就是猫类。

③ 对偏导特征图$G$做全局平均池化GAP,得到一个$C$个元素的权重向量$\alpha=\left[\alpha_{1}, \cdots, \alpha_{k}, \cdots, \alpha_{C}\right]$,$C$是$G$的通道数,$\alpha_{k}=\frac{1}{H W} \sum_{i}^{H} \sum_{j}^{W} G_{i, j}^{k}$。$\alpha_k$表示的是猫类相对于最后一层特征图的第k个通道的平均的敏感程度。

④ 将权重向量$\alpha$与特征图$A$对应通道做线性加权,得到一个二维的激活图$\operatorname{Grad CAM}=\operatorname{ReLU}\left(\sum_{k} \alpha_{k} A^{k}\right)$。

  • ReLU的作用是要求得到激活图里的值和猫类正相关的热力图,而负数部分表示属于狗类,可以表示为对预测为猫类起到抑制作用,因此用ReLU过滤掉这些起抑制作用的部分。
  • 最后将激活图上采样到输入图片大小。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
import cv2
import os
import numpy as np
from PIL import Image
import torch
import torch.nn as nn
import torch.nn.functional as F
import torchvision
import torchvision.transforms as transforms
import matplotlib.pyplot as plt
from torchvision.models.resnet import resnet18

class GradCAM:
def __init__(self, model: nn.Module, target_layer: str, size=(224, 224), mean=None, std=None) -> None:
self.model = model
self.model.eval()

# register hook
# 可以自己指定层名,没必要一定通过target_layer传递参数
# self.model.layer4
# self.model.layer4[1].register_forward_hook(self.__forward_hook)
# self.model.layer4[1].register_backward_hook(self.__backward_hook)
getattr(self.model, target_layer).register_forward_hook(self.__forward_hook)
getattr(self.model, target_layer).register_backward_hook(self.__backward_hook)

self.size = size
self.origin_size = None
self.mean, self.std = [0.485, 0.456, 0.406], [0.229, 0.224, 0.225]
if mean and std:
self.mean, self.std = mean, std

self.grads = []
self.fmaps = []

def forward(self, img_arr: np.ndarray, label=None, show=True, write=False):
img_input = self.__img_preprocess(img_arr.copy())

# forward
output = self.model(img_input)
idx = np.argmax(output.cpu().data.numpy())

# backward
self.model.zero_grad()
loss = self.__compute_loss(output, label)

loss.backward()

# generate CAM
grads_val = self.grads[0].cpu().data.numpy().squeeze()
fmap = self.fmaps[0].cpu().data.numpy().squeeze()
cam = self.__compute_cam(fmap, grads_val)

# show
cam_show = cv2.resize(cam, self.origin_size)
img_show = img_arr.astype(np.float32) / 255
self.__show_cam_on_image(img_show, cam_show, if_show=show, if_write=write)

self.fmaps.clear()
self.grads.clear()

def __img_transform(self, img_arr: np.ndarray, transform: torchvision.transforms) -> torch.Tensor:
img = img_arr.copy() # [H, W, C]
img = Image.fromarray(np.uint8(img))
img = transform(img).unsqueeze(0) # [N,C,H,W]
return img

def __img_preprocess(self, img_in: np.ndarray) -> torch.Tensor:
self.origin_size = (img_in.shape[1], img_in.shape[0]) # [H, W, C]
img = img_in.copy()
img = cv2.resize(img, self.size)
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
transform = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize(self.mean, self.std)
])
img_tensor = self.__img_transform(img, transform)
return img_tensor

def __backward_hook(self, module, grad_in, grad_out):
self.grads.append(grad_out[0].detach())

def __forward_hook(self, module, input, output):
self.fmaps.append(output)

def __compute_loss(self, logit, index=None):
if not index:
index = np.argmax(logit.cpu().data.numpy())
else:
index = np.array(index)

index = index[np.newaxis, np.newaxis]
index = torch.from_numpy(index)
one_hot = torch.zeros(1, 1000).scatter_(1, index, 1)
one_hot.requires_grad = True
loss = torch.sum(one_hot * logit)
return loss

def __compute_cam(self, feature_map, grads):
"""
feature_map: np.array [C, H, W]
grads: np.array, [C, H, W]
return: np.array, [H, W]
"""
cam = np.zeros(feature_map.shape[1:], dtype=np.float32)
alpha = np.mean(grads, axis=(1, 2)) # GAP
for k, ak in enumerate(alpha):
cam += ak * feature_map[k] # linear combination

cam = np.maximum(cam, 0) # relu
cam = cv2.resize(cam, self.size)
cam = (cam - np.min(cam)) / np.max(cam)
return cam


def __show_cam_on_image(self, img: np.ndarray, mask: np.ndarray, if_show=True, if_write=False):
heatmap = cv2.applyColorMap(np.uint8(255 * mask), cv2.COLORMAP_JET)
heatmap = np.float32(heatmap) / 255
cam = heatmap + np.float32(img)
cam = cam / np.max(cam)
cam = np.uint8(255 * cam)
if if_write:
cv2.imwrite("camcam.jpg", cam)
if if_show:
# 要显示RGB的图片,如果是BGR的 热力图是反过来的
plt.imshow(cam[:, :, ::-1])
plt.show()

# 调用函数
img = cv2.imread('test.jpg', 1)
net = resnet18(pretrained=True)

grad_cam = GradCAM(net, 'layer4', (224, 224), [0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
grad_cam.forward(img, show=True, write=False)