这里只大概了解类激活热力图的原理及实现。
卷积神经网络因为其在很多任务上效果很好但是其学到的内容和规则很难用人类理解的方式来呈现(相对于传统机器学习算法,例如决策树或者逻辑回归等),所以被很多人认为是“黑盒”。如果我们可以可视化:
1. 网络模型里面的中间层的激活结果;
2. 或者网络学到的过滤器是提取什么类型特征的;
3. 或者是图像中哪些位置的像素对输出有着强烈的影响,换句话说,输出对哪些位置的像素值比较敏感。
那么无疑对我们理解模型,或者分析模型为什么犯错有着很实际的用处。
所幸的是,2013年以来,随着深度学习的发展,也有一些很好的论文研究深度学习中模型可视化这一领域并取得了一些成果。例如,2014年ECCV上Zeiler的《Visualizing and Understanding Convolutional Networks》研究了第一个问题,此文也是可视化这一领域的开山之作;2012年NIPS上Alex Krizhevsky发表的AlexNet应该是最早可视化过滤器的文章;而针对第三个问题,2016年的**CAM** 《Learning deep features for discriminative localization》,2017年的**Grad-CAM** 《Grad-CAM:Visual Explanations from Deep Networks via Gradient-based Localization》和紧接着2018年的**Grad-CAM++** 《Grad-CAM++: Generalized Gradient-based Visual Explanations for Deep Convolutional Networks》这一系列的文章都对**类激活图**(**C**lass **A**ctivation **M**ap)这种可视化技术做了比较充分的研究。
CAM是 Class Activation Map 类别热力图
比如说,一个分类网络AlexNet,输入一个既包含着一只狗,又包含着一只猫的猫狗合影图片,它会输出一个1000维度的概率向量,其中有两个分量分别对应着图片分类为猫和图片分类为狗的概率。那么这两个概率,与图片中的哪些部分的关系更大,那些部分的关系更小呢?换句话讲,输出的概率的大小与输入图片中哪些区域的像素值更敏感呢?如果找出这个敏感度的一个热力图,越敏感的地方,温度越高,越不敏感的地方,温度越低,那就是类别热力图,如下图中c和i所示就分别时预测猫类别的CAM和预测狗类别的CAM,它很直观地告诉了我们模型是“看到了”猫的身体所以才认为图中有猫,“看到了”狗的脸才认为图中有狗。
代码实现:
# -*- coding: UTF-8 -*-
import torch
from torchvision import models
import torch.nn as nn
import torchvision.transforms as tfs
import numpy as np
import matplotlib.pyplot as plt
from PIL import Image
import cv2
class cal_cam(nn.Module):
def __init__(self, feature_layer="layer4"):
super(cal_cam, self).__init__()
self.model = models.resnet101(pretrained=False)
path = '../network/backbone/model/resnet101-5d3b4d8f.pth'
self.model.load_state_dict(torch.load(path))
self.device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
self.model.to(self.device)
# 要求梯度的层
self.feature_layer = feature_layer
# 记录梯度
self.gradient = []
# 记录输出的特征图
self.output = []
self.means = [0.485, 0.456, 0.406]
self.stds = [0.229, 0.224, 0.225]
self.trainsform = tfs.Compose([
tfs.ToTensor(),
tfs.Normalize(self.means, self.stds)
])
def save_grad(self, grad):
self.gradient.append(grad)
def get_grad(self):
return self.gradient[-1].cpu().data
def get_feature(self):
return self.output[-1][0]
def process_img(self, input):
input = self.trainsform(input)
input = input.unsqueeze(0)
return input
# 计算最后一个卷积层的梯度,输出梯度和最后一个卷积层的特征图
def getGrad(self, input_):
input_ = input_.to(self.device).requires_grad_(True)
num = 1
for name, module in self.model._modules.items():
if (num == 1):
input = module(input_)
num = num + 1
continue
# 是待提取特征图的层
if (name == self.feature_layer):
input = module(input)
input.register_hook(self.save_grad)
self.output.append([input])
# 马上要到全连接层了
elif (name == "avgpool"):
input = module(input)
input = input.reshape(input.shape[0], -1)
# 普通的层
else:
input = module(input)
# 到这里input就是最后全连接层的输出了
index = torch.max(input, dim=-1)[1]
one_hot = torch.zeros((1, input.shape[-1]), dtype=torch.float32)
one_hot[0][index] = 1
confidenct = one_hot * input.cpu()
confidenct = torch.sum(confidenct, dim=-1).requires_grad_(True)
# print(confidenct)
self.model.zero_grad()
# 反向传播获取梯度
confidenct.backward(retain_graph=True)
# 获取特征图的梯度
grad_val = self.get_grad()
feature = self.get_feature()
return grad_val, feature, input_.grad
# 计算CAM
def getCam(self, grad_val, feature):
# 对特征图的每个通道进行全局池化
alpha = torch.mean(grad_val, dim=(2, 3)).cpu()
feature = feature.cpu()
# 将池化后的结果和相应通道特征图相乘
cam = torch.zeros((feature.shape[2], feature.shape[3]), dtype=torch.float32)
for idx in range(alpha.shape[1]):
cam = cam + alpha[0][idx] * feature[0][idx]
# 进行ReLU操作
cam = np.maximum(cam.detach().numpy(), 0)
plt.imshow(cam)
plt.colorbar()
plt.savefig("cam.jpg")
# 将cam区域放大到输入图片大小
cam_ = cv2.resize(cam, (224, 224))
cam_ = cam_ - np.min(cam_)
cam_ = cam_ / np.max(cam_)
plt.imshow(cam_)
plt.savefig("cam_.jpg")
cam = torch.from_numpy(cam)
return cam, cam_
def show_img(self, cam_, img):
heatmap = cv2.applyColorMap(np.uint8(255 * cam_), cv2.COLORMAP_JET)
cam_img = 0.3 * heatmap + 0.7 * np.float32(img)
cv2.imwrite("img.jpg", cam_img)
def __call__(self, img_root):
img = Image.open(img_root)
img = img.resize((224, 224))
plt.imshow(img)
plt.savefig("airplane.jpg")
input = self.process_img(img)
grad_val, feature, input_grad = self.getGrad(input)
cam, cam_ = self.getCam(grad_val, feature)
self.show_img(cam_, img)
return cam
if __name__ == "__main__":
cam = cal_cam()
img_root = "image/2007_000033.jpg"
cam(img_root)
参考博客:https://zhuanlan.zhihu.com/p/83456743
代码参考:https://blog.csdn.net/haohulala/article/details/109558653
讲解详细:https://blog.csdn.net/csuyzt/article/details/102960853