小样本分类双分支CAM

双分支的Grad_CAM实现

很久没有搞分类,突然需要实现双分支网络的Grad_CAM有点无从下手。以前参考过一些大佬的实现做过单分支,准备以那个为基础修改一下。

Grad_CAM实现要点:

  1. 选取要激活的特征图,使用hook得到正向传播后的特征图反向传播后的梯度图
  2. 将梯度图 ( 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的特征向量作为每个特征图的权重
  3. 每个维度的特征图乘以对应维度梯度权重,然后求和得到CAM图

register_hook函数是pytorch提供的获得特征图和梯度图的方法,因为一般情况下反向传播后为了节省显存,梯度图不会保存。

当然可能也有其他的方法获得特征图和梯度图。

双分支实现要点

  1. 选取某个模块有两个特征图的输出(具体情况具体对待)

  2. 在实践中直接使用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图的实现。有什么问题可以评论区讨论。

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值