目录

  • 为什么神经网络要具有可解释性
  • 前言
  • 类激活图(CAM,class activation map),可视化
    • 1.1 CAM的工作原理
  • 2. 基于梯度的CAM(Grad-CAM)
    • pytorch 实现 Grad-CAM

为什么神经网络要具有可解释性

前言

神经网络往往被称为“黑盒”,Model学到的知识很难用人类可以理解的方式来提取和呈现。如何才能打开“黑盒”,一探究竟,因此有了神经网络的可解释性。目前,神经网络的可解释性主要有两大思路:

前处理:先数学理论证明,然后实验证明。
后处理:训练好的模型,通过可视化技术来理解模型的原理。
本文就一探可视化技术之CAM/Grad-CAM/Grad-CAM++。

类激活图(CAM,class activation map),可视化

Learning Deep Features for Discriminative Localization
论文地址

CAM是一张和原始图片等大小的图,指对输入图像生成类激活的热力图,可以理解为对预测输出的贡献分布,分数越高的地方表示原始图片对应区域对网络的响应越高、贡献越大,即**表示每个位置对该类别的重要程度。**如下图,颜色越深红的地方表示值越大,预测的类别分别“牙刷”和“伐树”。

CAM主要有以下作用:

有助于理解和分析神经网络的工作原理及决策过程,进而去更好地选择或设计网络。
利用可视化的信息引导网络更好的学习,例如可以利用CAM信息通过擦除裁剪的方式对数据进行增强。
利用CAM作为原始的种子,进行弱监督语义分割或弱监督定位。既然CAM能够cover到目标物体,所以可以仅利用分类标注来完成语义分割或目标检测任务,极大程度上降低了标注的工作量。

1.1 CAM的工作原理

卷积神经网络的卷积操作可以看做是滤波器对图片进行特征提取,通过滑动窗口的方式实现,因此特征图和输入图片存在空间上的对应关系。特征图的权重可以认为是被层层卷积核过滤后而保留的有效信息,**其值越大,表明特征越有效,对网络预测结果越重要。**一个深层的卷积神经网络,通过层层卷积操作,提取空间和语义信息。一般存在其他更难理解的层,例如分类的全连接层、softmax层等,很难以利用可视化的方式展示出来。所以,CAM的提取一般发生在卷积层,尤其是最后一层卷积。

CAM利用特征图权重叠加的原理获得热力图,公式如下:

其中,A 表示网络最后一层卷积层输出的大小,w 表示全连接层的权重大小,c 分类的类别。

1、假设输出为两个类别分别为猫和狗,最后一层卷积层有n个特征图 (最后一层卷积层特征图富含有最为丰富类别语义信息)。

2. 移除原始模型的全连接层,将最后一层卷积层的n个特征图做全局平均池化(GAP),得到n个神经元的全连接层,然后外接两个神经元的输出层重新训练(如上图所示)。

  1. 训练完后,输出层的两个神经元分别代表猫和狗的概率大小(实际上经过softmax才是概率)。假设我们要可视化某张图片中的猫是怎么进行识别的,取与猫对应神经元连接的n个权重,将这n个权重与最后一层卷积层的n个特征图进行相乘再相加,然后将得到的特征图进行上采样,得到与原始图像大小一致的图像,即为CAM。

**CAM的缺点:**需要修改网络结构并重新训练模型,导致在实际应用中非常不方便。

分别选用 resnet18、resnet50、densenet121 三种不同的模型,结合 hook 机制获取 CAM:

import numpy as np
from torchvision import models, transforms
import cv2
from PIL import Image
from torch.nn import functional as F# 定义预训练模型: resnet18、resnet50、densenet121
resnet18 = models.resnet18(pretrained=True)
resnet50 = models.resnet50(pretrained=True)
densenet121 = models.densenet121(pretrained=True)
resnet18.eval()
resnet50.eval()
densenet121.eval()# 图片数据转换
image_transform = transforms.Compose([# 将输入图片resize成统一尺寸transforms.Resize([224, 224]),# 将PIL Image或numpy.ndarray转换为tensor,并除255归一化到[0,1]之间transforms.ToTensor(),# 标准化处理-->转换为标准正太分布,使模型更容易收敛transforms.Normalize(mean=[0.485, 0.456, 0.406],std=[0.229, 0.224, 0.225])
])# =====注册hook start=====
feature_data = []def feature_hook(model, input, output):feature_data.append(output.data.numpy())resnet18._modules.get('layer4').register_forward_hook(feature_hook)
resnet50._modules.get('layer4').register_forward_hook(feature_hook)
densenet121._modules.get('features').register_forward_hook(feature_hook)
# =====注册hook end=====# 获取fc层的权重
fc_weights_resnet18 = resnet18._modules.get('fc').weight.data.numpy()
fc_weights_resnet50 = resnet50._modules.get('fc').weight.data.numpy()
fc_weights_densenet121 = densenet121._modules.get('classifier').weight.data.numpy()# 获取预测类别id
image = image_transform(Image.open("cat.jpg")).unsqueeze(0)
out_resnet18 = resnet18(image)
out_resnet50 = resnet50(image)
out_densenet121 = densenet121(image)
predict_classes_id_resnet18 = np.argmax(F.softmax(out_resnet18, dim=1).data.numpy())
predict_classes_id_resnet50 = np.argmax(F.softmax(out_resnet50, dim=1).data.numpy())
predict_classes_id_densenet121 = np.argmax(F.softmax(out_densenet121, dim=1).data.numpy())# =====获取CAM start=====
def makeCAM(feature, weights, classes_id):print(feature.shape, weights.shape, classes_id)# batchsize, C, h, wbz, nc, h, w = feature.shape# (512,) @ (512, 7*7) = (49,)cam = weights[classes_id].dot(feature.reshape(nc, h * w))cam = cam.reshape(h, w)  # (7, 7)# 归一化到[0, 1]之间cam = (cam - cam.min()) / (cam.max() - cam.min())# 转换为0~255的灰度图cam_gray = np.uint8(255 * cam)# 最后,上采样操作,与网络输入的尺寸一致,并返回return cv2.resize(cam_gray, (224, 224))cam_gray_resnet18 = makeCAM(feature_data[0], fc_weights_resnet18, predict_classes_id_resnet18)
cam_gray_resnet50 = makeCAM(feature_data[1], fc_weights_resnet50, predict_classes_id_resnet50)
cam_gray_densenet121 = makeCAM(feature_data[2], fc_weights_densenet121, predict_classes_id_densenet121)
# =====获取CAM start=====# =====叠加CAM和原图,并保存图片=====
# 1)读取原图
src_image = cv2.imread("cat.jpg")
h, w, _ = src_image.shape
# 2)cam转换成与原图大小一致的彩色度(cv2.COLORMAP_HSV为彩色图的其中一种类型)
cam_color_resnet18 = cv2.applyColorMap(cv2.resize(cam_gray_resnet18, (w, h)),cv2.COLORMAP_HSV)
cam_color_resnet50 = cv2.applyColorMap(cv2.resize(cam_gray_resnet50, (w, h)),cv2.COLORMAP_HSV)
cam_color_densenet121 = cv2.applyColorMap(cv2.resize(cam_gray_densenet121, (w, h)),cv2.COLORMAP_HSV)
# 3)合并cam和原图,并保存
cam_resnet18 = src_image * 0.5 + cam_color_resnet18 * 0.5
cam_resnet50 = src_image * 0.5 + cam_color_resnet50 * 0.5
cam_densenet121 = src_image * 0.5 + cam_color_densenet121 * 0.5
cam_hstack = np.hstack((src_image, cam_resnet18, cam_resnet50, cam_densenet121))
cv2.imwrite("cam_hstack.jpg", cam_hstack)
# 可视化
Image.open("cam_hstack.jpg").show()

最终的可视化效果如下图所示:

2. 基于梯度的CAM(Grad-CAM)

Grad-CAM: Why did you say that? Visual Explanations from Deep Networks via Gradient-based Localization
论文地址
上文的局限性就是网络架构里必须有GAP层,但并不是所有模型都配GAP层的。而本文就是为克服该缺陷提出的,其基本思路是目标特征图的融合权重 w k c w^c_k wkc​可以表达为梯度。另外,因为热图关心的是对分类有正面影响的特征,所以加上了relu以移除负值。其实并不一定要是分类问题,只要是可求导的激活函数,在其他问题也一样使用Grad-CAM。


如上图,为了获得类别判别图Grad-CAM,记为高度为 h h h、宽度为 w w w、类别为 c c c。首先利用(softmax之前的,logits)计算c这个类的梯度,定义特征图的激活值为 A k A^k Ak。这些回流的梯度在宽度和高度维度(分别由i和j索引)上全局平均池化,以获得神经元重要性权重 α k c \alpha^c_k αkc​:

在计算 α k c \alpha^c_k αkc​ 的同时,相对于激活反向传播梯度,精确的计算等于权重矩阵和梯度相对于激活函数的连续矩阵乘积,直到将梯度传播到的最终卷积层为止。因此,该权重 α k c \alpha^c_k αkc​ 代表A下游的深层网络的部分线性化,并捕获了特征图k对于目标类别c的“重要性”。

我们前向传播激活图的加权组合,然后通过ReLU来获得:

请注意,这会导致生成与卷积特征图大小相同的粗略热图(在VGG和AlexNet网络的最后一个卷积层的情况下为14×14)。本文将ReLU应用于特征图的线性组合,因为仅对对关注类别有积极影响的特征感兴趣,即,应该增加强度以增加 y c y^c yc的像素。负像素可能属于图像中的其他类别。不出所料,如果没有此ReLU,定位上有时会突出显示更多内容,而不仅仅是所需的类,并且在定位方面表现较差。

举一个例子:

1.假设我们还是两个类别:猫和狗,我们要可视化猫这个类别的GradCAM,首先通过softmax得到猫的概率,然后对最后一层卷积层的所有特征图求偏导,得到大小与最后一层卷积层大小一致的偏导矩阵。
2.将这偏导特征图做GAP(全局平均池化),得到一个权重向量,向量长度就是特征图数量。
3.将权重向量与特征图对应相乘再相加,此时得到一个二维的矩阵,宽高与特征图一致。
4.将这个二维矩阵送入Relu过一遍,把负数变成0。
5.最后,进行上采样,得到GradCAM。

grad-CAM 和 CAM的区别?
1.CAM 只能用于最后一层特征图和输出之间是GAP的操作,grad-CAM可适用非GAP连接的网络结构;
2.CAM只能提取最后一层特征图的热力图,而gard-CAM可以提取任意一层;

目标类别score y c y^c yc是用通过softmax之后的还是之前的?

论文原文中目标类别score是指网络未经过softmax的得分,但是某些代码实现当中也使用了通过softmax后的。二者有无区别?下面我们通过公式推导一下。因为这两种做法仅仅相差一个softmax,所以对softmax的求导。假设softmax层有C个输出,记为: [ a 1 , a 2 , . . . a c , . . . a C ] [a^1,a^2,...a^c,...a^C] [a1,a2,...ac,...aC] 。则softmax输出为:


则输出 a c a_c ac​对 y c y_c yc​ 的偏导为,由于这里计算的是目标类别,所以仅需计算对应输出的偏导:

所以特征融合的权重则变成:

可以看出,二者的梯度差异就是softmax输出的多一项 a c ( 1 − a c ) a_c(1-a_c) ac​(1−ac​) ,而对于已经训练好的网络,该项为定值。后面特征层进行加权求和,再归一化后,该项 a c ( 1 − a c ) a_c(1-a_c) ac​(1−ac​) 会被消掉。所以通过softmax和不通过softmax在理论上完全一致。但是在实际应用中我发现,加上softmax后,权重 s o f t m a x − a k c softmax-a^c_k softmax−akc​ 会变得非常小,就是因为训练的充分好的网络,预测输出 a c a^c ac的值是非常接近1的,所以 a c ( 1 − a c ) a^c(1-a^c) ac(1−ac) 的值非常非常小,存在丢失精度的风险。所以目标类别score建议使用不经过softmax的值。

pytorch 实现 Grad-CAM

这里只给出了 VGG 的实现方式,若想要进行修改,详细阅读模型复现源码进行修改,或者移步pytorch-cnn-visualizations,这里给出了比较多的可视化方法。

import torch
from torch.autograd import Variable
from torch.autograd import Function
from torchvision import models
from torchvision import utils
import cv2
import sys
import numpy as np
import argparseclass FeatureExtractor():""" Class for extracting activations and registering gradients from targetted intermediate layers """def __init__(self, model, target_layers):self.model = modelself.target_layers = target_layersself.gradients = []def save_gradient(self, grad):self.gradients.append(grad)def __call__(self, x):outputs = []self.gradients = []for name, module in self.model._modules.items():x = module(x)if name in self.target_layers:x.register_hook(self.save_gradient)outputs += [x]return outputs, xclass ModelOutputs():""" Class for making a forward pass, and getting:1. The network output.2. Activations from intermeddiate targetted layers.3. Gradients from intermeddiate targetted layers. """def __init__(self, model, target_layers):self.model = modelself.feature_extractor = FeatureExtractor(self.model.features, target_layers)def get_gradients(self):return self.feature_extractor.gradientsdef __call__(self, x):target_activations, output  = self.feature_extractor(x)output = output.view(output.size(0), -1)output = self.model.classifier(output)return target_activations, outputdef preprocess_image(img):means=[0.485, 0.456, 0.406]stds=[0.229, 0.224, 0.225]preprocessed_img = img.copy()[: , :, ::-1]for i in range(3):preprocessed_img[:, :, i] = preprocessed_img[:, :, i] - means[i]preprocessed_img[:, :, i] = preprocessed_img[:, :, i] / stds[i]preprocessed_img = \np.ascontiguousarray(np.transpose(preprocessed_img, (2, 0, 1)))preprocessed_img = torch.from_numpy(preprocessed_img)preprocessed_img.unsqueeze_(0)input = Variable(preprocessed_img, requires_grad = True)return inputdef show_cam_on_image(img, mask):heatmap = cv2.applyColorMap(np.uint8(255*mask), cv2.COLORMAP_JET)heatmap = np.float32(heatmap) / 255cam = heatmap + np.float32(img)cam = cam / np.max(cam)cv2.imwrite("../../images/cam01.jpg", np.uint8(255 * cam))class GradCam:def __init__(self, model, target_layer_names, use_cuda):self.model = modelself.model.eval()self.cuda = use_cudaif self.cuda:self.model = model.cuda()self.extractor = ModelOutputs(self.model, target_layer_names)def forward(self, input):return self.model(input) def __call__(self, input, index = None):if self.cuda:features, output = self.extractor(input.cuda())else:features, output = self.extractor(input)if index == None:index = np.argmax(output.cpu().data.numpy())one_hot = np.zeros((1, output.size()[-1]), dtype = np.float32)one_hot[0][index] = 1one_hot = Variable(torch.from_numpy(one_hot), requires_grad = True)if self.cuda:one_hot = torch.sum(one_hot.cuda() * output)else:one_hot = torch.sum(one_hot * output)self.model.features.zero_grad()self.model.classifier.zero_grad()#one_hot.backward(retain_variables=True)one_hot.backward()grads_val = self.extractor.get_gradients()[-1].cpu().data.numpy()target = features[-1]target = target.cpu().data.numpy()[0, :]weights = np.mean(grads_val, axis = (2, 3))[0, :]cam = np.zeros(target.shape[1 : ], dtype = np.float32)for i, w in enumerate(weights):cam += w * target[i, :, :]cam = np.maximum(cam, 0)cam = cv2.resize(cam, (224, 224))cam = cam - np.min(cam)cam = cam / np.max(cam)return camclass GuidedBackpropReLU(Function):def forward(self, input):positive_mask = (input > 0).type_as(input)output = torch.addcmul(torch.zeros(input.size()).type_as(input), input, positive_mask)self.save_for_backward(input, output)return outputdef backward(self, grad_output):input, output = self.saved_tensorsgrad_input = Nonepositive_mask_1 = (input > 0).type_as(grad_output)positive_mask_2 = (grad_output > 0).type_as(grad_output)grad_input = torch.addcmul(torch.zeros(input.size()).type_as(input), torch.addcmul(torch.zeros(input.size()).type_as(input), grad_output, positive_mask_1), positive_mask_2)return grad_inputclass GuidedBackpropReLUModel:def __init__(self, model, use_cuda):self.model = modelself.model.eval()self.cuda = use_cudaif self.cuda:self.model = model.cuda()# replace ReLU with GuidedBackpropReLUfor idx, module in self.model.features._modules.items():if module.__class__.__name__ == 'ReLU':self.model.features._modules[idx] = GuidedBackpropReLU()def forward(self, input):return self.model(input)def __call__(self, input, index = None):if self.cuda:output = self.forward(input.cuda())else:output = self.forward(input)if index == None:index = np.argmax(output.cpu().data.numpy())one_hot = np.zeros((1, output.size()[-1]), dtype = np.float32)one_hot[0][index] = 1one_hot = Variable(torch.from_numpy(one_hot), requires_grad = True)if self.cuda:one_hot = torch.sum(one_hot.cuda() * output)else:one_hot = torch.sum(one_hot * output)# self.model.features.zero_grad()# self.model.classifier.zero_grad()one_hot.backward()output = input.grad.cpu().data.numpy()output = output[0,:,:,:]return outputif __name__ == '__main__':""" python grad_cam.py <path_to_image>1. Loads an image with opencv.2. Preprocesses it for VGG19 and converts to a pytorch variable.3. Makes a forward pass to find the category index with the highest score,and computes intermediate activations.Makes the visualization. """image_path = "../../images/dog-cat.jpg"# Can work with any model, but it assumes that the model has a # feature method, and a classifier method,# as in the VGG models in torchvision.grad_cam = GradCam(model = models.vgg19(pretrained=True), \target_layer_names = ["35"], use_cuda=True)img = cv2.imread(image_path, 1)img = np.float32(cv2.resize(img, (224, 224))) / 255input = preprocess_image(img)# If None, returns the map for the highest scoring category.# Otherwise, targets the requested index.target_index = Nonemask = grad_cam(input, target_index)show_cam_on_image(img, mask)gb_model = GuidedBackpropReLUModel(model = models.vgg19(pretrained=True), use_cuda=True)gb = gb_model(input, index=target_index)utils.save_image(torch.from_numpy(gb), '../../images/gb.jpg')cam_mask = np.zeros(gb.shape)for i in range(0, gb.shape[0]):cam_mask[i, :, :] = maskcam_gb = np.multiply(cam_mask, gb)utils.save_image(torch.from_numpy(cam_gb), '../../images/cam_gb.jpg')

可解释性神经网络(可视化):CAM/Grad-CAM pytorch相关代码相关推荐

  1. MobileNetV3 论文理解,以及tensorflow、pytorch相关代码

    MobileNetV3论文理解,以及tensorflow+pytorch代码 MobileNetV3相关 论文地址 Block结构变化 算法内部微结构变化 网络整体结构 网络性能 Tensorflow ...

  2. 数据分析可视化常用图介绍以及相关代码实现(箱型图、Q-Q图、Kde图、线性回归图、热力图)

    文章目录 前言 一.箱型图是什么? 1-1.箱型图介绍 1-2.箱型图的作用 1-3.实战 二.Q-Q图是什么? 2-1.Q-Q图(分位数-分位数图:quantile-quantile plot)介绍 ...

  3. 机器学习可解释性(二) —— 类激活映射(CAM)

    # 机器学习可解释性(二)--类激活映射(CAM) 文章目录 1.序言 2.方法介绍 2.1 CAM 2.2 GradCAM 2.3 GradAM++ 2.4 LayerCAM 3.算法实现 3.1 ...

  4. pytorch visualizer 深度神经网络可视化工具

    深度神经网络可视化工具 1. visdom 1.1 通用操作 1.1.1 创建/关闭窗口.查询窗口状态 1.1.2 更新窗口 update_window_opts 1.1.3 不同的update模式 ...

  5. GradCAM神经网络可视化解释(原理和实现)

    GradCAM是经典的特征图可视化工具,在CV任务中,能用于分析CNN学到了什么东西.先看一张图: 这就是GradCAM做出的效果,它直观地表示出咱们模型认为图片是Dog的是依据哪些地方. GradC ...

  6. “看得见的”卷积神经网络(图文并茂+代码解读)(卷积神经网络可视化)

    这篇博客主要是想和大家分享一下我学习卷积神经网络可视化之后的总结和心得.学习完卷积神经网络的大致流程之后,会感觉到它和其他深度学习网络一样,像个"黑盒子".我们只知道它有几层,每层 ...

  7. PyTorch Autograd(backward grad 等PyTorch核心)

    文章目录 绪论 1. PyTorch基础 2. 人工神经网络和反向传播 3. 动态计算图(dynamic computational graph) 4. 反向函数(Backward()) 5. 数学: ...

  8. 模型可解释性-树结构可视化

    在算法建模过程中,我们一般会用测试集的准确率与召回率衡量一个模型的好坏.但在和客户的实际沟通时,单单抛出一个数字就想要客户信任我们,那肯定是不够的,这就要求我们摆出规则,解释模型.但不是所有的模型都是 ...

  9. github可视化工具_【神经网络可视化01】——用Netron实现可视化

    版权声明:小博主水平有限,博文仅代表个人观点,希望大家多多指导. 参考: 1.神经网络可视化(一)--Netron - 云+社区 - 腾讯云 神经网络可视化(一)--Netron - 云+社区 - 腾 ...

最新文章

  1. 机器学习 | 数据从哪里找?手把手教你构建数据集
  2. 基于图文界面的蓝牙扫描工具btscanner
  3. Python Numpy中返回下标操作函数-节约时间的利器
  4. CentOS7更换镜像源
  5. Snort 网络***检测系统(二)之Snort 介绍
  6. body onload 控制窗口大小 html,HTML5 对各个标签的定义与规定:body的介绍
  7. 08 comet反向ajax
  8. bootstrap 树形表格渲染慢_layUI之树状表格异步加载组件treetableAsync.js(基于treetable.js)...
  9. 《WinForm开发系列之控件篇》Item3 BindingSource (暂无)
  10. 我是一个CPU:这个世界慢!死!了!
  11. PHP 正则表达式资料
  12. 95-36-032-ChannelHandler-SimpleChannelInboundHandler
  13. 18 TaskScheduler任务调度器抽象基类——Live555源码阅读(一)任务调度相关类
  14. 计算机技术中的常见概念
  15. 如何在矩池云GPU云中安装MATLAB R2016b软件
  16. .Net Core Linux centos7行—.net core json 配置文件
  17. .NET基础示例系列之十五:操作Excel
  18. 三年级计算机第一学期期末试题,三年级上册信息技术期末考试试卷(清华版)
  19. Ubantu 查看显卡相关信息
  20. Android LiveData初识

热门文章

  1. 机器学习中你不得不知道的数学符号表示
  2. 基于华为端口安全的网络实验题
  3. TCP三次握手和syn攻击
  4. 单/三相电表参数电压电流采集器中相电压和线电压的概念与区别
  5. 2022.08.31 特赞面试
  6. mysql取出另外一张表的数据_mysql从一张表中取出数据插入到另一张表
  7. 一定要爱你用计算机怎么弹,怎么用计算器弹奏音乐下山 用计算器弹奏流行歌曲...
  8. java输出数组的最大值_JAVA 键盘输入数组,输出数组内容和最大值、最小值(示例代码)...
  9. [办公自动化]PDF大小不一如何调整
  10. PHP父母互助,40|静待花开:我们的第一次正面管教线上父母互助活动