网上关于类激活图Cam以及梯度类激活图的讲解很多,但都不是非常全面,这里我就全面的介绍一下两者的原理,并讲解代码实现过程,最后通过一个实例进行演示。
CAM: CAM
Grad-CAM:Grad-CAM
类激活图cam(class activation map)通过可视化的热力图将模型认为最显著的结果显示出来,因此可用于解释模型预测的结果。卷积神经网络的最后一层卷积层包含了最丰富的空间和语义信息,于是Cam充分利用了最后一层卷积的特征,并将后面的全连接层和softmax层替换成了GAP层(全局平均池化),用特征图所有像素的均值代替整个特征图的值。每个特征图
A
k
A^k
Ak都有一个对应的权重
w
k
c
w_k^c
wkc,与GAP后的特征图求加权和就能得到相应类别C的类激活图,同时也能得到对应的预测得分
Y
c
Y^c
Yc(softmax之前)。
C
a
m
c
=
∑
k
w
k
c
×
A
k
Cam^c=\sum\limits_k w_k^c\times A^k
Camc=k∑wkc×Ak
可以看到,其实Cam实现并不难,但有个很大的缺陷,由于添加了GAP改变了网络模型,与原始训练好的模型不同。所以还需要对改变的模型进行训练得到相应的权重,大大限制了应用场景。
于是就有了Grad-CAM,它使用梯度的全局平均来计算权重,不需要修改模型,自然也不需要重新训练。经过严格的数学推导,权重为
我们只关心那些有正向作用的像素点,所以加了ReLU。
从公式中可以看出,我们只要对类别C的预测得分进行反向传播得到梯度,并求全局平均就能计算出相应的权重,因此不需要修改模型。
下面我们结合代码进行讲解,GitHub上的GradCam非常多,我挑了其中一个,讲解其中关键的代码。
由于需要计算前向传播的特征和反向传播的梯度,所以介绍前需要先了解hook函数,可以不改变主体情况下,提取网络中间的输出。这里我就不详细介绍了,详情可以看这个博客
hook函数和CAM类激活图
此代码添加前向传播的特征和反向传播的梯度
def _register_hook(self):
for (name, module) in self.net.named_modules():
if name == self.layer_name:
self.handlers.append(module.register_forward_hook(self._get_features_hook))
self.handlers.append(module.register_backward_hook(self._get_grads_hook))
在计算完GradCam后,需要释放hook,否则计算会越来越慢。
def remove_handlers(self):
for handle in self.handlers:
handle.remove()
这里就是计算类激活图的关键代码了,结合上述公式可以很快的理解。
def __call__(self, inputs, index):
"""
:param inputs: [1,3,H,W]
:param index: class id
:return:
"""
self.net.zero_grad()
#模型预测得分
output = self.net(inputs) # [1,num_classes]
#取最大得分对应的索引作为类别
if index is None:
index = np.argmax(output.cpu().data.numpy())
target = output[0][index]
#预测得分反向传播
target.backward()
#梯度全局平均求权重
gradient = self.gradient[0].cpu().data.numpy() # [C,H,W]
weight = np.mean(gradient, axis=(1, 2)) # [C]
feature = self.feature[0].cpu().data.numpy() # [C,H,W]
#特征与对应权重加权和并ReLU
cam = feature * weight[:, np.newaxis, np.newaxis] # [C,H,W]
cam = np.sum(cam, axis=0) # [H,W]
cam = np.maximum(cam, 0) # ReLU
# 数值归一化
cam -= np.min(cam)
cam /= np.max(cam)
# resize to 224*224
cam = cv2.resize(cam, (224, 224))
return cam