GradCAM原理及代码

前言

利用深度学习方法对数据进行分析时,在得到结果的同时还需要对算法进行可解释性分析。在分类任务中,GradCAM比较常用(特别是基于卷积神经网络CNN的分类任务)。它通过生成热力图来展示模型在做出特定类别预测时关注的区域,从而提供模型决策过程的可视化解释。

一、GradCAM原理

GradCAM是CAM(Class Activation Mapping)的改进版本。 CAM需要网络具有特定的结构,如全局平均池化层,而GradCAM更通用,适用于大多数CNN模型。GradCAM不需要更改网络结构或重新训练就能实现更多CNN模型的可视化。
GradCAM原理简图
以CNN网络为例,将图像输入CNN,中间得到多个维度的卷积特征图AAA,得到的图像特征可以用于1)图像分类;2)图像描述(image captioning);3)视觉问答。也就是说,对于这三类任务,都可以用GradCAM可视化网络的决策依据。下面以图像分类任务为例,特征图AAA经过多层全连接层(FC Layers)得到对多个类别的预测分数。
对于感兴趣的类别ccc,GradCAM的计算公式如下:
LGrad−CAMc=ReLU(∑kαkcAk)L_{Grad-CAM}^c = ReLU(\sum_k \alpha_k^c A^k)LGradCAMc=ReLU(kαkcAk)
AkA^kAk就是上面提到的特征图,αkc\alpha_k^cαkc表示AkA^kAk层的权重,这里表示的是根据特征图的“重要程度”加权求和,再经过一个ReLUReLUReLU激活函数得到一个初步的热力图。所以,根据这个公式可以看出,关键在于求解特征图的权重,下面是权重求解的公式:
αkc=1Z∑i∑j∂yc∂Aijk\alpha_k^c = \frac{1}{Z}\sum_i\sum_j\frac{\partial y^c}{\partial A_{ij}^k}αkc=Z1ijAijkyc
也就是使用图像的预测分数ycy^cyc反传求解对于AAA的梯度图,对应AAA的第kkk维特征图,对应的梯度图为∂yc/∂Ak\partial y^c/\partial A^kyc/Ak。为了进一步得到权重,这里采用的是全局平均池化的方法,也就是第kkk维特征图对应的梯度图所有元素求平均,得到αkc\alpha_k^cαkc

二、GradCAM的实现

从前面的原理可以看出,要实现GradCAM的关键在于得到中间层的特征图以及反传的梯度图。

调用GradCAM绘制热力图部分

import torch
from torch import nn

##首先要对自己的模型进行训练,得到训练好的模型
class ViT(nn.Sequential):
	def __init__(self, emb_size=40, depth=6, n_classes=4, **kwargs):
		super().__init__(
		# ...the model
		)

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = ViT()
model.load_state_dict(torch.load('XXXX.pth', map_location=device))
target_layers = [model[1]]
cam = GradCAM(model=model, target_layer=target_layers, use_cuda=False, reshape_reshape_transform)

GradCAM主函数

class GradCAM:
	def __init__(self,
				model,
				target_layers,
				reshape_transform=None
				use_cuda=False):
		self.model = model.eval()#将模型参数固定,不更新参数
		self.target_layers = target_layers
		self.reshape_transform = reshape_transform
		self.cuda = use_cuda
		if self.cuda:
			self.model = model.cuda()
		self.activations_and_grads = ActivationsAndGradients(
			self.model, target_layers, reshape_transform)#ActivationsAndGradients为获取特征图及其梯度的核心函数
	def __call__(self, input_tensor, target_category=None):
		if self.cuda:
			input_tensor = input_tensor.cuda()
		#此处进行了正向传播,钩子函数会自动保存激活值
		output = self.activations_and_grads(input_tensor)
		if isinstance(target_category, int):
			target_category = [target_category]*input_tensor.size(0)
		
		if target_category is None:
			target_category = np.argmax(output.cpu().data.numpy(), axis=-1)
			print(f"category id:{target_category}")
		
		self.model.zero_grad()
		loss = self.get_loss(output, target_category)
		print("the loss is ", loss)
		#此处进行了反向传播,钩子函数会自动保存梯度信息
		loss.backward(retrain_graph=True)
		
		cam_per_layer = self.compute_cam_per_layer(input_tensor)
		return self.aggregate_multi_layers(cam_per_layer)
	def __del__(self):
		self.activations_and_grads.release()
	def __enter__(self):
		return self
	def __exit__(self, exc_type, exc_value, exc_tb):
		self.activations_and_grads.release()
		if instance(exc_value, IndexError):
			print(f"An expection occurred in CAM with block: {exc_type}, message {exc_value}")
			return True

ActivationsAndGradients功能函数

class ActivationsAndGradients:
	def __init__(self, model, target_layers, reshape_transform):
		self.model = model
		self.gradients = []
		self.activations = []
		self.reshape_transform = reshape_transform
		self.handles = []
		for target_layer in target_layers:
			self.handles.append(
				target_layer.register_forward_hook(
					self.save_activation))
			if hasattr(target_layer, "register_full_backward_hook"):
				self.handles.append(
					target_layer.register_full_backward_hook(
							self.save_gradient))
			else:
				self.handles.append(
					target_layer.register_backward_hook(
						self.save_gradient))

	def save_activation(self, module, input, output):
		activation = output
		if self.reshape_transform is not None:
			activation = self.reshape_transform(activation)
		self.activations.append(activation.cpu().detach())
	def save_gradient(self, module, grad_input, grad_output):
		grad = grad_output[0]
		if self.reshape_transform is not None:
			grad = self.reshape_transform(grad)
		self.gradients = [grad.cpu().detach()] + self.gradients
	def __call__(self, x):
		self.gradients = []
		self.activations = []
		return self.model(x)
	def relaease(self):
		for handle in self.handles:
			handle.remove()

这里用到了钩子(hook),当钩子被注册到指定的网络层上时,每次正向/反向传播时会被自动调用,以下面这句代码为例:

self.handles.append(target_layer.register_forward_hook(self.save_activation))

self.handles存储所有注册的钩子的句柄(handle),指定的网络层为target_layer,register_forward_hook为target_layer的一个方法,用于注册一个在该层正向传播过程中会被调用的钩子函数。self.save_activation为该钩子函数,当正向传播到这一层时,这个函数会被调用,activation存储进self.activations。梯度计算是反向的,所以在存储梯度时反着放进列表中。

compute_cam_per_layer功能函数

@staticmethod
def get_cam_weights(grads):
	return np.mean(grads, axis=(2,3), keep_dims=True)
	#这里输入的grads是一个四维数组
	#第一个维度表示图像个数,第二个维度表示特征图的个数,最后两个维度分别表示特征图的行和列
	#axis=(2,3)表示沿最后两个维度求平均,最后得到的形状为(a,b,1,1)
def get_cam_image(self, activations, grads):
	weights = self.get_cam_weights(grads)#得到每个特征图的权重
 	weighted_activations = weights*activations#对每个特征图进行加权
 	#假设activations的形状为(a,b,w,h),加权之后得到的形状为(a,b,w,h)
 	cam = weighted_activations.sum(axis=1)#对加权后的特征图进行了求和,变成形状(a,1,w,h)
 	return cam
 	
#这个函数将cam图像的各像素值归一化到0和1之间,同时将尺度变成和输入图像尺度一样
@staticmethod
def scale_cam_image(cam, target_size=None):
	result = []
	for img in cam:
		img = img - np.min(img)#减去最小值,相当于将最小值变成0
		img = img / (1e-7 + np.max(img))#除以最大值,相当于将最大值变成1,这里加上1e-7是为了防止除法出错
		if target_size is not None:
			img = cv2.resize(img, target_size)
		result.append(img)
	result = np.float32(result)
	return result
#这个函数目的是得到输入张量的最后两维,也就是特征图的高和宽
@staticmethod
def get_target_width_height(input_tensor):
	width, height = input_tensor.size(-1), input_tensor.size(-2)
	return width, height

def compute_cam_per_layer(self, input_tensor):
	activations_list = [a.cpu().data.numpy()
						for a in self.activations_and_grads.activations]
	grads_list = [g.cpu().data.numpy()
				for g in self.activations_and_grads.gradients]
	cam_per_target_layer = []
	target_size = self.get_target_width_height(input_tensor)#目标的形状就是输入张量的形状
	cam_per_target_layer = []

	for layer_activations, layer_grads in zip(activations_list, grads_list):
		cam = self.get_cam_image(layer_activations, layer_grads)#每一个感兴趣的层都计算一个cam_image
		cam[cam<0] = 0#相当于进行了ReLU激活函数
		scaled = self.scale_cam_image(cam, target_size)
		cam_per_target_layer.append(scaled[:,None,:])#相当于增加了一个维度
	return cam_per_target_layer
		
def aggregate_multi_layers(self, cam_per_target_layer):
	cam_per_target_layer = np.concatenate(cam_per_target_layer, axis=1)
	cam_per_target_layer = np.maximum(cam_per_target_layer, 0)#和0比较,如果比0大就保持原值,如果比0小,就取0
	result = np.mean(cam_per_target_layer, axis=1)#取所有感兴趣layer的cam的平均值
	return self.scale_cam_image(result)		
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值