双分支的Grad_CAM实现
很久没有搞分类,突然需要实现双分支网络的Grad_CAM有点无从下手。以前参考过一些大佬的实现做过单分支,准备以那个为基础修改一下。
Grad_CAM实现要点:
- 选取要激活的特征图,使用hook得到正向传播后的特征图反向传播后的梯度图
- 将梯度图 ( s h a p e : [ C , H , W ] ) (shape:[C,H,W]) (shape:[C,H,W])全局池化得到 C × 1 × 1 C\times{1}\times{1} C×1×1的特征向量作为每个特征图的权重
- 每个维度的特征图乘以对应维度梯度权重,然后求和得到CAM图
register_hook函数是pytorch提供的获得特征图和梯度图的方法,因为一般情况下反向传播后为了节省显存,梯度图不会保存。
当然可能也有其他的方法获得特征图和梯度图。
双分支实现要点
-
选取某个模块有两个特征图的输出(具体情况具体对待)
-
在实践中直接使用register_backward_hook函数并没有获得两个输出,register_forward_hook倒是获得了两个特征图。
参考了文档和博客也没有发现问题,说的是输入输出可以是多元素的元组,但实际并没有。
最后找到了register_full_backward_hook方法成功解决问题、
注意点
第一次实现后,效果非常差,一时想不通是hook函数的问题,还是双分支的问题。最后灵光一现,输出没有过激活函数。(因为训练是交叉熵损失内置了这一内容,这个网络的作者没有加,加上长时间没碰分类问题也忽略了,困扰了好久)
代码实现(这里是小样本分类的网络,输入是支持集和查询集组成的episode)
我把每步维度标清楚,可以根据自己需求修改维度就行了
## import 自己需要的库
import cv2
import matplotlib.pyplot as plt
import numpy as np
import torch
## model+预训练权重
model = Model().cuda()
path=''
checkpoint = torch.load(path)
model.load_state_dict()
## 设置eval,固定BatchNormal和dropout等
model.eval()
## 准备存储传播过程的特征图和梯度图
grad_block=[]
feature_block=[]
## module选择的模块,grad_in:moudle的输入;grad_out:module的输出
def backward_hook(module,grad_in,grad_out):
grad_block.append(grad_out[0].detach())
grad_block.append(grad_out[1].detach())
def farward_hook(module,input,output):
feature_block.append(output[0])
feature_block.append(output[1])
## 选择自己网络中要勾选的模块(类似 output1,output2=self.out2(x))
##反向传播时自动执行backward_hook
model.out2.register_full_backward_hook(backward_hook)
##正向传播时自动执行farward_hook
model.out2.register_forward_hook(farward_hook)
def get_cam(x_train, x_test, y_train, y_test):
## 这里以小样本分类网络为例 查询图片1张,5way-1shot,batch_size=1
## x_train:[1,5,3,h,w]
## x_test :[1,1,3,h,w]
## y是标签,不是必需的
## 正向传播
## output :[1,1,5]
output = model(x_train, x_test, y_train, y_test)
## 网络中有激活可以注释这一行。
output=torch.softmax(output,dim=2)
## 得到概率最大对应输出的下标
cls_scores = output.view( output.shape[1]*output.shape[0],-1)
_, max_idx = torch.max(cls_scores.detach().cpu(), 1)
## 清空模型参数的梯度,不是必需
model.zero_grad()
## 获得概率最大下标对应的概率(ps:不是概率值,概率值torch.max就可以得到。一定是output中对应的值,这样才能保证求导)
class_loss = output[0, 0,max_idx[0]]
## 以这个类别的概率求导,得到影响概率值的梯度
class_loss.backward()
## 输入相同的话x_train和x_test一样即可
## 通用:保证一下形状传参即可:
## test_img:[h,w,3]
test_img=x_test[0][0]
test_img=test_img.cpu().detach().numpy()
test_img=test_img.transpose((1,2,0))
## train_img形状处理同test_img
train_img = x_train[0][max_idx[0]]
train_img = train_img.cpu().detach().numpy()
train_img = train_img.transpose((1, 2, 0))
## 保证grads_test:[C,H,W]
## 保证ftestmap_test:[C,H,W]
grads_test= grad_block[1][0][0][0].cpu().data.numpy().squeeze()
ftestmap_test = feature_block[1][0][0][0].cpu().data.numpy().squeeze()
## 传入原始图片,特征图,梯度图,开始计算cam
cam_test = cam_show_img(test_img, ftestmap_test, grads_test)
grads_train= grad_block[0][0][0][0].cpu().data.numpy().squeeze()
ftestmap_train = feature_block[0][0][0][0].cpu().data.numpy().squeeze()
cam_train = cam_show_img(train_img, ftestmap_train, grads_train)
return cam_train,cam_test
def cam_show_img(img, feature_map, grads):
## img: [H,W,3]
## feature_map:[C,H,W]
## grads_map:[C,H,W]
## cam:[H,W]
cam = np.zeros(feature_map.shape[1:], dtype=np.float32)
# grads:[C,H*W]
grads = grads.reshape([grads.shape[0], -1])
## 根据梯度图平局池化得到梯度向量[1,H*W]
weights = np.mean(grads, axis=1)
## 特征图对应维度加权然后所有维度求和
for i, w in enumerate(weights):
cam += w * feature_map[i, :, :]
## 去除负值(relu)并归一化,方便还原成0-255
## cam:[H,W]
cam = np.maximum(cam, 0)
cam = cam / cam.max()
## 将cam图resize成原图大小方便展示
cam = cv2.resize(cam, (img.shape[0],img.shape[1]))
## cam制作成3通道上色,cam:[H,W,3]
heatmap = cv2.applyColorMap(np.uint8(255 * cam), cv2.COLORMAP_JET)
## BGR->RGB
heatmap=cv2.cvtColor(heatmap,cv2.COLOR_BGR2RGB)
## 这里我的img是tensor还原的所以需要*255。建议找位置用cv2.imread重新读图片展示效果更好也不用*255了。
cam_img = 0.3* heatmap + 0.7 * img
## 第一个plot展示cam+img
ax1=plt.subplot(1,2,1)
plt.axis('off')
ax1.imshow(cam_img/255)
## 第二个plt展示cam
ax2 = plt.subplot(1,2, 2)
plt.axis('off')
ax2.imshow(heatmap/ 255)
plt.show()
return cam_img / 255
展示(来源:MiniImagnet数据集),这里原图用的是opencv重新读取(展示效果略好,不影响CAM结果)
查询集图片:
支持集最大概率对应图片,这里好像学到了奇怪的东西:
总结
小样本数据集较正常任务略复杂,具体实现具体情况而定,代码细节均已给出,至于支撑集为什么学到了奇怪的东西不太清楚(有可能是小样本本身的限制,图片也是随便抽的没看训练集和测试集)。
本篇博文主要提供一个多分支CAM图的实现思路,单独分支也是一般CAM图的实现。有什么问题可以评论区讨论。