您的位置:

深入理解并使用Grad-CAM

现如今,深度学习算法在许多领域都取得了巨大的成功,其中又以视觉领域最为突出。深度卷积神经网络具有强大的特征提取和模式识别能力,但模型的黑盒现象已经成为一个普遍的问题。因为很难理解它为何会得出特定的结果,以及它在图像中关注什么。

在这种情况下,研究人员提出了许多技术来解释卷积神经网络的工作,例如Grad-CAM(Gradient-weighted Class Activation Mapping)技术。Grad-CAM是一种可视化方法,它可以将卷积神经网络输出结果的可解释性可视化。它告诉我们卷积神经网络在哪里关注图像,以及这些区域如何帮助分类或回归任务。

一、Grad-CAM的原理

理解Grad-CAM的基本原理非常重要。Grad-CAM的核心思想是要找到一个能够反映网络输出概率的空间位置权重映射。具体而言,Grad-CAM的做法是将输出概率的梯度回传到卷积层上,并将卷积层的输出特征图和权重进行加权平均。通过这种方式,可以得到一个重要性分数,该分数与输出概率相关而能够反映图像区域的重要程度。

下面是Grad-CAM核心算法代码:

class GradCAM:
    def __init__(self, model, candidate_layers=None):
        self.model = model
        self.extractor = ModelOutputs(model, candidate_layers or model.outputs[0])
        
    def forward(self, input):
        return self.model(input)

    def __call__(self, input, index=None):
        features, output = self.extractor(input)

        if index is None:
            index = np.argmax(output.cpu().data.numpy())

        one_hot = np.zeros((1, output.size()[-1]), dtype=np.float32)
        one_hot[0][index] = 1

        one_hot = Variable(torch.from_numpy(one_hot), requires_grad=True)
        one_hot = torch.sum(one_hot.cuda() * output)

        self.model.zero_grad()
        one_hot.backward(retain_graph=True)

        grads_val = self.extractor.get_gradients()[-1].cpu().data.numpy()
        target = features[-1].cpu().data.numpy()[0, :]
        weights = np.mean(grads_val, axis=(2, 3))[0, :]
        cam = np.sum(target * weights[:, None, None], axis=0)
        cam = np.maximum(cam, 0)
        cam = cv2.resize(cam, (input.shape[3], input.shape[2]))
        cam = cam - np.min(cam)
        cam = cam / np.max(cam)
        return cam

其中的ModelOutputs类是一个包装类,它可以帮助我们同时获取卷积层和输出层。以下是ModelOutputs的代码:

class ModelOutputs:
    def __init__(self, model, candidate_layers):
        self.model = model
        self.gradients = None
        self.activation_maps = dict()

        for (name, module) in self.model.named_modules():
            if name in candidate_layers:
                module.register_backward_hook(self._get_gradients)
                module.register_forward_hook(self._get_activation(name))

    def _get_gradients(self, module, input_grad, output_grad):
        self.gradients = output_grad[0]

    def _get_activation(self, name):
        def hook(module, input, output):
            self.activation_maps[name] = output.detach()
        return hook

    def __call__(self, x):
        outputs = []
        for name, module in self.model.named_modules():
            x = module(x)
            if name in self.activation_maps:
                outputs.append(self.activation_maps[name])
        return outputs, x

该类中的_grads方法可以获取梯度。在我们使用Grad-CAM方法来实现可视化之前需要的基础就在这里。

二、Grad-CAM的优缺点

Grad-CAM具有多个优点。其中最重要的是,它是一个通用的可视化方法,可用于任何卷积神经网络架构。它并不需要重复训练或特殊的网络改造。它还不需要修改网络体系结构或模型体系结构,这意味着它可以很好地与其他机器学习工具一起使用。

此外,Grad-CAM并不难以实现。实际上,其是一个用于反向传播的标准技术。它只是使用了一些诸如箱形激活的技巧,以让输出分数和特征映射可用于可视化。它对于更复杂的架构和框架也很有效。

Grad-CAM的一个缺点是它假定模型完全是用卷积层和全连接层构建的。如果模型具有其他类型的层(例如循环或门层),那么该方法将不适用。此外,该方法局限于先前在模型中定义的卷积层或最终输出层。这意味着如果您想可视化网络中的其他层,您需要在代码中更改构建的层。

三、Grad-CAM的应用

1. 可视化图像分类结果

Grad-CAM最常见的应用是可视化图像分类结果。其方法非常简单,您只需要将Grad-CAM类与您的图像和分类器模型一起运行。下面是执行示例代码:

img = Image.open(image_path)

# 图像预处理
preprocessing = transforms.Compose([
    transforms.Resize((224,224)),
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])

img_tensor = preprocessing(img).unsqueeze(0).cuda()

# 获取Grad-CAM
grad_cam = GradCAM(model=model, candidate_layers=['layer4'])
output = model(img_tensor)

# 根据得到的概率分布,获得数字标签
pred_index = output.data.cpu().numpy().argmax()

# 获取Grad-CAM的热力图
cam = grad_cam(img_tensor)

# 将热力图(Grad-CAM)与原图像叠加
heatmap, result = visualize_cam(img_tensor.cpu().data.numpy()[0], cam)

# 显示结果
plt.figure(figsize=(10,10))
plt.subplot(2,1,1)
plt.imshow(heatmap)
plt.subplot(2,1,2)
plt.imshow(result)
plt.show()

2. 分析神经网络模型

分析神经网络的不同层:卷积层、池化层、批量标准化层(Batch Normalization)等在图像中的作用也是很有意义的。使用Grad-CAM可以很容易地以直观的方式分析每个层次的预测结果对输出的影响有哪些,并检查模型是否真正关注图像中的重要信息。

下面的代码演示了对特定卷积层进行可视化:

def get_cam(model, img_path, target_layer):
    """
    产生特定层的Grad-CAM
    :param model:
    :param img_path:
    :param target_layer: conv5_x, layer4, layer3, layer2, layer1
    """
    grad_cam = GradCAM(model=model, candidate_layers=[target_layer])
    img = Image.open(img_path)
    preprocessing = transforms.Compose([
        transforms.Resize((224,224)),
        transforms.ToTensor(),
        transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
    ])
    img_tensor = preprocessing(img).unsqueeze(0).cuda()
    target_index = None
    output = model(img_tensor)
    pred_class = output.argmax(dim=1, keepdim=True)
    # 如果有多个标签,则生成多个GradCAM
    if pred_class.size(0) > 1:
        for i in range(pred_class.size(0)):
            print(f'{i+1}-th categories with GradCAM:')
            # 注意GradCAM的标签需要int型,且此处要将标量变为int型,不能够用.item()方法
            cam = grad_cam(img_tensor, index=int(pred_class[i]))
            grad_img = cv2.resize(np.float32(img), (224,224))
            grad_img -= grad_img.min()
            grad_img /= grad_img.max()
            grad_map = torch.from_numpy(cam.transpose(2, 0, 1)).unsqueeze_(0)
            # 将GradCAM叠加到图像上
            show_cam_on_image(grad_img, grad_map.numpy()[0], f'Result{i+1}.jpg')
    else:
        # 获取Grad-CAM
        cam = grad_cam(img_tensor, index=target_index)
        grad_img = cv2.resize(np.float32(img), (224,224))
        grad_img -= grad_img.min()
        grad_img /= grad_img.max()
        grad_map = torch.from_numpy(cam.transpose(2, 0, 1)).unsqueeze_(0)
        # 将GradCAM叠加到图像上
        show_cam_on_image(grad_img, grad_map.numpy()[0], 'Result.jpg')

model = models.resnet50(pretrained=True).cuda()
_ = model.eval()

get_cam(model, image_path, "layer4")

四、结语

Grad-CAM是解释模型输出的强大工具,可以帮助我们理解卷积神经网络的特点、训练过程、优化以及如何通过调整超参数来提高模型的精度。

当将深度学习模型应用于实际问题时,人们通常要求精度和可解释性之间取得平衡。Grad-CAM作为一种可视化技术,为深度学习模型的可解释性和解释性提供了重要的信息。这种方法的优点是它易于实现,通用性强,可以应用于任何CNNs模型,缺点是存在局限性。