简析CNN可视化方法——Grad-CAM

发布时间:2023-10-07 14:00

最近在应用中,发现Grad-CAM在验证神经网络知识表示有效性方面很好用,这篇文章总结以下该方法的原理以及实现代码。

一、介绍

卷积神经网络(CNN)和其他深度网络已经在多种计算机视觉任务中实现了前所未有的突破,从图像分类[24,16]到物体检测[15],语义分割[27],图像描述生成[43,6,12,21],以及最近的视觉问答[3,14,32,36]。虽然这些深度神经网络能够实现卓越的性能,但由于它们缺乏可分解性,不能转化为直观和易于理解的组件,因此它们很难被解释[26]。因此,当今天的智能系统出现故障时,如果没有任何警告或解释,它们就会失败得令人失望,用户盯着一个不连贯的输出,不知道为什么。

可解释性问题。为了建立对智能系统的信任,并将他们有意义地融入我们的日常生活中,很显然我们必须建立“透明”的模型来解释为什么它们这么预测。广义而言,这种透明度在人工智能(AI)演变的三个不同阶段非常有用。首先,当AI比人类弱得多并且还不能可靠地“部署”时(例如视觉问答[3]),透明度和解释的目标是识别失效模式[1,17],从而帮助研究人员集中精力在最富有成果的研究方向上。其次,当人工智能与人类平等并且可靠地“可部署”时(例如,在一组类别上训练了足够多的数据的图像分类[22]),目标是在用户中建立适当的信任和置信度。第三,当AI比人类强得多时(例如国际象棋或Go [39]),解释的目标是在机器教学中[20] - 即一台机器教人如何做出更好的决策。

在准确性和简单性或可解释性之间通常存在一种平衡。传统的基于规则或专家系统[18]是高度可解释的,但不是非常准确(或强大)。每个阶段都是手工设计的可分解管道被认为更具可解释性,因为每个单独的组件都假设了一个自然、直观的解释。通过使用深层模型,我们牺牲可解释模块来解释不可解释的模块,通过更好的抽象(更多层)和更紧密的集成(端到端训练)实现更高的性能。最近引入的深度残差网络(ResNets)[16]深度超过200层,并且在几项具有挑战性的任务中展现了最先进的性能。这种复杂性使得这些模型很难解释。因此,深层模型开始探索解释性和准确性之间的关系。

二、方法

以前的一些文章已经断言,CNN中的更深层次的表现可以捕捉到更高层次的视觉结构[5,31]。 此外,卷积特征保留了在全连接层中丢失的空间信息,因此我们可以猜想最后的卷积层在高级语义和详细空间信息之间具有最佳折衷。
这些图层中的神经元在图像中查找语义类特定的信息(比如对象部分)。Grad-CAM使用流入CNN最后一层卷积层的梯度信息来理解每个神经元对于目标决定的重要性。尽管我们的技术非常通用,并且可以用来可视化深层网络中的任何激活,但在这项工作中,我们专注于解释网络可能做出的决策。
Grad-CAM概述:给定一个图像和一个目标类(例如,\'虎猫’或任何其他类型的可微分输出)作为输入,我们将图像传播通过模型的CNN部分,然后通过特定任务的计算来获得该类别的原始分数。
对于所有类,除了所需的类(虎猫)的梯度设置为1,其余的梯度设置为零。然后将该信号反向传播到所关注的整形卷积特征图,其中我们结合起来计算粗糙的Grad-CAM定位(蓝色热力图),它表明了模型需要看哪里去做出精确决定。最后,我们将热力图与导向反向传播逐点相乘,获得高分辨率和特定概念的Guided Grad-CAM可视化。

具体流程如下:

1)求图像经过特征提取后最后一次卷积后得到的特征图(也就是VGG16 conv5_3的特征图(7x7x512))
2)512张featuremap在全连接层分类的权重肯定不同,利用反向传播求出每张特征图的权重。注意cam和Grad-cam的不同就在于求每张特征图权重的方式。其他流程都一样
3)用每张特征图乘以权重得到带权重的特征图(7x7x512),在第三维求均值得到7x7的map(np.mean(axis=-1)),relu激活,归一化处理(避免有些值不在0-255范围内)。
4) 将处理后的heatmap放缩到图像尺寸大小,便于与图像加权

该步最重要的是relu激活(relu只保留大于0的值),relu后只保留该类别有用的特征。正数认为是该类别有用的特征,负数是其他类别的特征(或无用特征)。如下图,假设某类别最后加权后为0.8965,类别值越大则是该类别的概率就越高,那么属于该类别的特征既为wx值大于0的特征。小于0的特征可能是其他类的特征。通俗理解,假如图像中出现一个猫头,那么该特征在猫类别中为正特征,在狗类别中为负特征,要增加猫的置信度,降低狗的置信度。

关键代码

class CamExtractor():
    \"\"\"
        Extracts cam features from the model
    \"\"\"
    def __init__(self, model, target_layer):
        self.model = model
        self.target_layer = target_layer
        self.gradients = None

    def save_gradient(self, grad):
        self.gradients = grad

    def forward_pass_on_convolutions(self, x):
        \"\"\"
            Does a forward pass on convolutions, hooks the function at given layer
        \"\"\"
        conv_output = None
        for module_pos, module in self.model.features._modules.items():
            x = module(x)  # Forward
            if int(module_pos) == self.target_layer:
                x.register_hook(self.save_gradient)
                conv_output = x  # Save the convolution output on that layer
        return conv_output, x

    def forward_pass(self, x):
        \"\"\"
            Does a full forward pass on the model
        \"\"\"
        # Forward pass on the convolutions
        conv_output, x = self.forward_pass_on_convolutions(x)
        x = x.view(x.size(0), -1)  # Flatten
        # Forward pass on the classifier
        x = self.model.classifier(x)
        return conv_output, x


class GradCam():
    \"\"\"
        Produces class activation map
    \"\"\"
    def __init__(self, model, target_layer):
        self.model = model
        self.model.eval()
        # Define extractor
        self.extractor = CamExtractor(self.model, target_layer)

    def generate_cam(self, input_image, target_class=None):
        # Full forward pass
        # conv_output is the output of convolutions at specified layer
        # model_output is the final output of the model (1, 1000)
        conv_output, model_output = self.extractor.forward_pass(input_image)
        if target_class is None:
            target_class = np.argmax(model_output.data.numpy())
        # Target for backprop
        one_hot_output = torch.FloatTensor(1, model_output.size()[-1]).zero_()
        one_hot_output[0][target_class] = 1
        # Zero grads
        self.model.features.zero_grad()
        self.model.classifier.zero_grad()
        # Backward pass with specified target
        model_output.backward(gradient=one_hot_output, retain_graph=True)
        # Get hooked gradients
        guided_gradients = self.extractor.gradients.data.numpy()[0]
        # Get convolution outputs
        target = conv_output.data.numpy()[0]
        # Get weights from gradients
        weights = np.mean(guided_gradients, axis=(1, 2))  # Take averages for each gradient
        # Create empty numpy array for cam
        cam = np.ones(target.shape[1:], dtype=np.float32)
        # Multiply each weight with its conv output and then, sum
        for i, w in enumerate(weights):
            cam += w * target[i, :, :]
        cam = np.maximum(cam, 0)
        cam = (cam - np.min(cam)) / (np.max(cam) - np.min(cam))  # Normalize between 0-1
        cam = np.uint8(cam * 255)  # Scale between 0-255 to visualize
        cam = np.uint8(Image.fromarray(cam).resize((input_image.shape[2],
                       input_image.shape[3]), Image.ANTIALIAS))/255
        # ^ I am extremely unhappy with this line. Originally resizing was done in cv2 which
        # supports resizing numpy matrices with antialiasing, however,
        # when I moved the repository to PIL, this option was out of the window.
        # So, in order to use resizing with ANTIALIAS feature of PIL,
        # I briefly convert matrix to PIL image and then back.
        # If there is a more beautiful way, do not hesitate to send a PR.

        # You can also use the code below instead of the code line above, suggested by @ ptschandl
        # from scipy.ndimage.interpolation import zoom
        # cam = zoom(cam, np.array(input_image[0].shape[1:])/np.array(cam.shape))
        return cam

参考文献:

[1]、https://blog.csdn.net/stu_sun/article/details/80628406
[2]、https://arxiv.org/abs/1610.02391
[3]、https://github.com/utkuozbulak/pytorch-cnn-visualizations#convolutional-neural-network-filter-visualization

ItVuer - 免责声明 - 关于我们 - 联系我们

本网站信息来源于互联网,如有侵权请联系:561261067@qq.com

桂ICP备16001015号