Grad cam

参考我导的讲解

论文小结

总的公式如下:对于某一类C,比如Cat

其中:

  • A代表某个特征层,在论文中一般指的是最后一个卷积层输出的特征层
  • K代表特征层A中第k个通道(channel)
  • c代表类别c
  • $A^k$代表特征层A中通道k的数据
  • $\alpha^c_k$ 代表$A^k$对应的权重

最后输出的是一个特征层宽高的矩阵。

计算权重

后面那一项其实是第c类对权重的梯度,可以通过反向传播得到,本质就是对反向传播的矩阵进行一个求平均操作,输出K维的向量作为权重。

根据上式计算Mat

转换为图片

这时已经得到了0~1的灰度图mask,如果想放大则直接使用插值得到结果图

cv2.resize(mat,targetsize)

转rgb热力图

灰度图只能显示提取到的特征,不能很好显示注意力的位置,可以修改为热力图来显示。

# 将灰度转热力三通道
heat_map=cv2.applyColorMap(mask,colormap=colormap)
heat_map=heat_map/255.
image=image+heat_map
image=image/np.max(image)

代码

class GradCam:
def __init__(self,model_name:list,target_size=None):
self.fh=[]
self.bh=[]
self.target_size=target_size
for i in model_name:
i.register_forward_hook(self.forward_hook)
i.register_backward_hook(self.backward_hook)

def forward_hook(self,module,input,output):
self.fh.append(output.cpu().detach().numpy())

def backward_hook(self,module, grad_in, grad_out):
self.bh = [grad_out[0].cpu().detach().numpy()] + self.bh

def get_rgb_image(self,image,mask,colormap: int = cv2.COLORMAP_JET,rgb_or_bgr=False,use_heatmap=True):
if use_heatmap==True:
mask = np.uint8(mask * 255)
heat_map=cv2.applyColorMap(mask,colormap=colormap)
if rgb_or_bgr==True:
heat_map=cv2.cvtColor(heat_map,cv2.COLOR_BGR2RGB)
heat_map=heat_map/255.
image=image+heat_map
image=image/np.max(image)
else:
mask=np.expand_dims(mask,axis=2)
image=image*mask

return image

def cal_cam(self,image,colormap: int = cv2.COLORMAP_JET,rgb_or_bgr=False,use_heatmap=True):
'''
获得各层的 mask和image
:param image: 输入的图片 0~1格式
:param colormap: 热力图colormap格式
:param rgb_or_bgr: 输入图片是否为rgb格式HWC
:param use_heatmap: 输出热力图或蒙版图
:return: images,masks
'''
self.Image=[]
self.masks=[]
for a, a_ in zip(self.fh, self.bh):
# 1,C,W,H -> CWH
a=np.squeeze(a)
a_=np.squeeze(a_)
# C,1,1
alpha = np.mean(a_, axis=(1,2),keepdims=True)
# CWH-> WH
mat = np.sum(alpha*a,axis=0)
# relu
mat[mat<0]=0
# 转换到0-1
mat=(mat-np.min(mat))/(np.max(mat)+1e-7)
if self.target_size!=None:
# targetsize W,H
mask=cv2.resize(mat,self.target_size)
else:
mask=mat
image=self.get_rgb_image(image,mask,colormap=colormap,rgb_or_bgr=False,use_heatmap=use_heatmap)
self.masks.append(mask)
self.Image.append(image)
return self.Image,self.masks
# 使用方式
cam=GradCam(feature_name,target_size)
images,_=cam.cal_cam(raw_image/255.)
cv2.imshow(images[0])
cv2.waitKey()
cv2.destroyAllWindows()

Guide backpropagation

论文小结

​ 总的来说就是根据正反向传播通过relu激活函数的特性进行一个可视化:正向传播中被成功激活的才认为是有效信息,反向传播也同理,所以我们通过正向传播获得激活参数的位置,与反向传播权重进行relu后相乘获得新的反向传播权重权重并回传。

​ 注意这个是要走一遍所有反向传播过程,只是在relu层进行重新回传梯度这个操作,所以最后输出是第一个网络层的梯度(这里需要设置图片允许求梯度,否则第一个会是None),维度与输入图片相同,梯度的输出含义则是图片的边缘信息。

论文里实现部分讲的很难理解,我是根据代码进一步理解的。

我尝试了直接输出第一个网络层(上图)的梯度与GBP(下图)进行对比,只筛选被激活部分还是挺能说明一些东西的。

代码

代码实现时有几点要注意的顺便总结一下:

  • permute,transpose,view:

    view:将图片全部展开为一维,然后按照给定的维度进行拼接,会改变原始的矩阵信息。

    transpose和permute,不会改变原始矩阵信息,用法也相同,只是一个是numpy一个是tensor。

    比如permute(2,1,0),代表原本第2个维到到第一个位置,第1个元素到第二个位置第0维到第三个位置。

  • 直接输出的图片太暗:

    通过debug可以发现最后输出并不是0~1,所以我们需要把他进行一个0~1的转换,然后由于我们只关心高频边缘信息,所以把0~1直接的全部置0,只保留原始为1的数。这里转换还要注意一点,为了增强对比度,我们将均值设置在0.5左右,标准差设置在0.1,上图为min-max归一化,中图为mean-std归一化,下图在中图基础上还设置了方差和均值。

  • 由于会改变反向传播的梯度,不能在训练的时候用也不能和grad-cam同时用(这里的同时是指只进行一次前后向传播)

  • 输出gbp.image不存在:需要给前向传播的那张图片设置允许求梯度requiregrad()

class GuideBackPropagation:
def __init__(self,model_name:list):
self.fh=[]
self.model=model

# 所有relu层都注册
for name, module in model.features.named_children():
if isinstance(module, nn.ReLU):
module.register_forward_hook(self.forward_hook)
module.register_backward_hook(self.backward_hook)
# 第一个卷积层位置

for i in model_name:
i.register_backward_hook(self.first_backward_hook)

def normalize(self,I):
# 归一化梯度map,先归一化到 mean=0 std=1
norm = (I - I.mean()) / I.std()
# 把 std 重置为 0.1,让梯度map中的数值尽可能接近 0
norm = norm * 0.1
# 均值加 0.5,保证大部分的梯度值为正
norm = norm + 0.5
# 把 0,1 以外的梯度值分别设置为 0 和 1
norm = norm.clip(0, 1)
return norm

def forward_hook(self,module,input,output):
self.fh.append(output.cpu().detach().numpy())

def first_backward_hook(self,module,grad_in,grad_out):
# 获取第一个卷积层反向传播的权重
# BCHW-> CHW ->h,w,c
self.image=grad_in[0].cpu().detach()[0]
self.image=self.image.permute(1,2,0).numpy()
self.image=self.normalize(self.image)
pass
def backward_hook(self,module, grad_in, grad_out):
a=self.fh.pop()
a[a>0]=1
# 反向传播 relu
new_grad=torch.clamp(grad_out[0],min=0.0)
# rule 是返回一个参数,返回修改后的梯度
return (new_grad*a,)
# 使用
gbp=GuideBackPropagation(feature_name)
res=model(image.require_grad_())
res[0,0].backward()
image=gbp.image