思路
获取想要解释的那一层的特征图,然后根据特征图梯度计算出权重值,加在原图上面。
Demo
加上类激活(cam)
可以看到,cam将模型认为有利于分类的特征标注了出来。
下面以ResNet50为例:
Trick:
使用
for i in model._modules.items():
可以获得模型名称和对应层。
# coding: utf-8
import os
import cv2
import numpy as np
from PIL import Image
import matplotlib.pyplot as pltimport torch
import torch.autograd as autograd
import torchvision.transforms as transformsimport torchvision.models as models# 训练过的模型路径
#resume_path = r"D:\TJU\GBDB\set113\cross_validation\test1\epoch_0257_checkpoint.pth.tar"
# 输入图像路径
single_img_path = r'bicycle.jpg'
# 绘制的热力图存储路径
save_path = r'heatmap/bicycle_layer4.jpg'# 网络层的层名列表, 需要根据实际使用网络进行修改
layers_names = ['conv1', 'bn1', 'relu', 'maxpool', 'layer1', 'layer2', 'layer3', 'layer4', 'avgpool']
# 指定层名
out_layer_name = "layer4"features_grad = 0# 为了读取模型中间参数变量的梯度而定义的辅助函数
def extract(g):global features_gradfeatures_grad = gdef draw_CAM(model, img_path, save_path, transform=None, visual_heatmap=False, out_layer=None):"""绘制 Class Activation Map:param model: 加载好权重的Pytorch model:param img_path: 测试图片路径:param save_path: CAM结果保存路径:param transform: 输入图像预处理方法:param visual_heatmap: 是否可视化原始heatmap(调用matplotlib):return:"""# 读取图像并预处理global layer2img = Image.open(img_path).convert('RGB')if transform:img = transform(img)img = img.unsqueeze(0) # (1, 3, 448, 448)# model转为eval模式model.eval()# 获取模型层的字典layers_dict = {layers_names[i]: None for i in range(len(layers_names))}for name,module in model._modules.items():#print(i, (name, module))layers_dict[name] = module# 遍历模型的每一层, 获得指定层的输出特征图# features: 指定层输出的特征图, features_flatten: 为继续完成前端传播而设置的变量features = imgstart_flatten = Falsefeatures_flatten = Nonefor name, layer in layers_dict.items():if name != out_layer and start_flatten is False: # 指定层之前features = layer(features)elif name == out_layer and start_flatten is False: # 指定层features = layer(features)start_flatten = Trueelse: # 指定层之后if name == "fc":breakif features_flatten is None:features_flatten = layer(features)else:features_flatten = layer(features_flatten)#print(features_flatten.shape)features_flatten = torch.flatten(features_flatten, 1)#print(features_flatten.shape)output = model.fc(features_flatten)# 预测得分最高的那一类对应的输出scorepred = torch.argmax(output, 1).item()pred_class = output[:, pred]# 求中间变量features的梯度# 方法1# features.register_hook(extract)# pred_class.backward()# 方法2features_grad = autograd.grad(pred_class, features, allow_unused=True)[0]grads = features_grad # 获取梯度pooled_grads = torch.nn.functional.adaptive_avg_pool2d(grads, (1, 1))# 此处batch size默认为1,所以去掉了第0维(batch size维)pooled_grads = pooled_grads[0]features = features[0]print("pooled_grads:", pooled_grads.shape)print("features:", features.shape)# features.shape[0]是指定层feature的通道数for i in range(features.shape[0]):features[i, ...] *= pooled_grads[i, ...]# 计算heatmapheatmap = features.detach().cpu().numpy()heatmap = np.mean(heatmap, axis=0)heatmap = np.maximum(heatmap, 0)heatmap /= np.max(heatmap)# 可视化原始热力图if visual_heatmap:plt.matshow(heatmap)plt.show()img = cv2.imread(img_path) # 用cv2加载原始图像heatmap = cv2.resize(heatmap, (img.shape[1], img.shape[0])) # 将热力图的大小调整为与原始图像相同heatmap = np.uint8(255 * heatmap) # 将热力图转换为RGB格式heatmap = cv2.applyColorMap(heatmap, cv2.COLORMAP_JET) # 将热力图应用于原始图像superimposed_img = heatmap * 0.7 + img # 这里的0.4是热力图强度因子cv2.imwrite(save_path, superimposed_img) # 将图像保存到硬盘if __name__ == '__main__':model = models.resnet50(pretrained=True)#model.eval()transform = transforms.Compose([transforms.Resize(448),transforms.ToTensor(),transforms.Normalize([0.5, 0.5, 0.5], [0.5, 0.5, 0.5])])# 构建模型并加载预训练参数#seresnet50 = FineTuneSEResnet50(num_class=113).cuda()#checkpoint = torch.load(resume_path)#seresnet50.load_state_dict(checkpoint['state_dict'])draw_CAM(model, single_img_path, save_path, transform=transform, visual_heatmap=True, out_layer=out_layer_name)