评价此页

简介 || 张量 || 自动求导 || 构建模型 || TensorBoard 支持 || 训练模型 || 理解模型

使用 Captum 理解模型#

创建于:2021 年 11 月 30 日 | 最后更新:2024 年 1 月 19 日 | 最后验证:2024 年 11 月 05 日

请观看下面的视频教程,或在 YouTube 上观看。在此处 下载 Notebook 和相关文件

Captum(拉丁语意为“理解”)是一个基于 PyTorch 构建的开源、可扩展的模型可解释性库。

随着模型复杂性的增加以及由此带来的不透明性,模型可解释性方法变得越来越重要。模型理解既是一个活跃的研究领域,也是在各行业使用机器学习进行实际应用的一个重点领域。Captum 提供了包括 Integrated Gradients 在内的最先进算法,为研究人员和开发人员提供了一种简单的方法来理解哪些特征有助于模型的输出。

完整的文档、API 参考以及一系列关于特定主题的教程可在 captum.ai 网站上找到。

简介#

Captum 在模型可解释性方面的处理方式是基于“归因”。Captum 中有三种类型的归因:

  • 特征归因 旨在根据生成某个输出的输入特征来解释该输出。以电影评论中的某些词语来解释该评论是正面还是负面,就是特征归因的一个例子。

  • 层归因 检查模型在特定输入后的隐藏层活动。检查卷积层响应输入图像的空域映射输出是层归因的一个例子。

  • 神经元归因 类似于层归因,但侧重于单个神经元的活动。

在此交互式 Notebook 中,我们将探讨特征归因和层归因。

每种归因类型都有多个相关的归因算法。许多归因算法可分为两大类:

  • 基于梯度的算法 计算模型输出、层输出或神经元激活相对于输入的后向梯度。Integrated Gradients(用于特征)、Layer Gradient * ActivationNeuron Conductance 都是基于梯度的算法。

  • 基于扰动的算法 通过改变输入来测量对输出的影响,来检查模型、层或神经元的输出变化。输入扰动可以是定向的或随机的。OcclusionFeature AblationFeature Permutation 都是基于扰动的算法。

我们将在下面研究这两种类型的算法。

特别是对于大型模型,以易于关联到正在检查的输入特征的方式可视化归因数据可能很有价值。虽然使用 Matplotlib、Plotly 或类似工具创建自己的可视化当然是可能的,但 Captum 提供了针对其归因的增强工具。

  • captum.attr.visualization 模块(下面导入为 `viz`)提供了用于可视化与图像相关的归因的有用函数。

  • Captum Insights 是一个易于使用的 Captum 之上的 API,它提供了一个可视化小部件,为图像、文本和任意模型类型提供现成的可视化。

我们将在本 Notebook 中演示这两个可视化工具集。前几个示例将侧重于计算机视觉用例,但结尾的 Captum Insights 部分将演示在多模型、视觉问答模型中可视化归因。

安装#

在开始之前,您需要一个具有以下内容的 Python 环境:

  • Python 3.6 或更高版本

  • 对于 Captum Insights 示例,需要 Flask 1.1 或更高版本以及 Flask-Compress(推荐最新版本)

  • PyTorch 1.2 或更高版本(推荐最新版本)

  • TorchVision 0.6 或更高版本(推荐最新版本)

  • Captum(推荐最新版本)

  • Matplotlib 3.3.4 版本,因为 Captum 目前使用了一个 Matplotlib 函数,其参数在后续版本中已重命名

要在 Anaconda 或 pip 虚拟环境中安装 Captum,请使用下面适合您环境的命令

使用 conda

conda install pytorch torchvision captum flask-compress matplotlib=3.3.4 -c pytorch

使用 pip

pip install torch torchvision captum matplotlib==3.3.4 Flask-Compress

在您设置的环境中重启此 Notebook,即可开始!

第一个示例#

首先,让我们看一个简单的视觉示例。我们将从一个在 ImageNet 数据集上预训练的 ResNet 模型开始。我们将获取一个测试输入,并使用不同的特征归因算法来检查输入图像如何影响输出,并为一些测试图像查看此输入归因图的有益可视化。

首先,导入一些库

import torch
import torch.nn.functional as F
import torchvision.transforms as transforms
import torchvision.models as models

import captum
from captum.attr import IntegratedGradients, Occlusion, LayerGradCam, LayerAttribution
from captum.attr import visualization as viz

import os, sys
import json

import numpy as np
from PIL import Image
import matplotlib.pyplot as plt
from matplotlib.colors import LinearSegmentedColormap

现在我们将使用 TorchVision 模型库下载一个预训练的 ResNet。由于我们不进行训练,暂时将其置于评估模式。

model = models.resnet18(weights='IMAGENET1K_V1')
model = model.eval()

您获取此交互式 Notebook 的地方应该还有一个包含 `cat.jpg` 文件的 `img` 文件夹。

test_img = Image.open('img/cat.jpg')
test_img_data = np.asarray(test_img)
plt.imshow(test_img_data)
plt.show()

我们的 ResNet 模型在 ImageNet 数据集上进行了训练,并期望图像具有特定大小,并且通道数据被归一化到特定值范围。我们还将获取模型识别的类别的人类可读标签列表——这些也应该在 `img` 文件夹中。

# model expects 224x224 3-color image
transform = transforms.Compose([
 transforms.Resize(224),
 transforms.CenterCrop(224),
 transforms.ToTensor()
])

# standard ImageNet normalization
transform_normalize = transforms.Normalize(
     mean=[0.485, 0.456, 0.406],
     std=[0.229, 0.224, 0.225]
 )

transformed_img = transform(test_img)
input_img = transform_normalize(transformed_img)
input_img = input_img.unsqueeze(0) # the model requires a dummy batch dimension

labels_path = 'img/imagenet_class_index.json'
with open(labels_path) as json_data:
    idx_to_labels = json.load(json_data)

现在,我们可以提出这个问题:我们的模型认为这张图片是什么?

output = model(input_img)
output = F.softmax(output, dim=1)
prediction_score, pred_label_idx = torch.topk(output, 1)
pred_label_idx.squeeze_()
predicted_label = idx_to_labels[str(pred_label_idx.item())][1]
print('Predicted:', predicted_label, '(', prediction_score.squeeze().item(), ')')

我们已经确认 ResNet 认为我们的猫的图片确实是一只猫。但为什么模型认为这是一张猫的图片呢?

要回答这个问题,我们转向 Captum。

使用 Integrated Gradients 的特征归因#

特征归因将特定输出归因于输入特征。它使用特定输入——这里是我们的测试图像——来生成一个图,显示每个输入特征对特定输出特征的相对重要性。

Integrated Gradients 是 Captum 中可用的特征归因算法之一。Integrated Gradients 通过近似模型输出相对于输入的梯度积分来为每个输入特征分配重要性分数。

在我们的例子中,我们将选取输出向量的一个特定元素——即表示模型对其选择类别信心的元素——并使用 Integrated Gradients 来了解输入图像的哪些部分对该输出起作用。

一旦我们有了来自 Integrated Gradients 的重要性图,我们将使用 Captum 中的可视化工具来提供重要性图的有用表示。Captum 的 `visualize_image_attr()` 函数提供了多种选项来自定义显示您的归因数据。在这里,我们传入了一个自定义的 Matplotlib 颜色映射。

运行带有 `integrated_gradients.attribute()` 调用的单元格通常需要一到两分钟。

# Initialize the attribution algorithm with the model
integrated_gradients = IntegratedGradients(model)

# Ask the algorithm to attribute our output target to
attributions_ig = integrated_gradients.attribute(input_img, target=pred_label_idx, n_steps=200)

# Show the original image for comparison
_ = viz.visualize_image_attr(None, np.transpose(transformed_img.squeeze().cpu().detach().numpy(), (1,2,0)),
                      method="original_image", title="Original Image")

default_cmap = LinearSegmentedColormap.from_list('custom blue',
                                                 [(0, '#ffffff'),
                                                  (0.25, '#0000ff'),
                                                  (1, '#0000ff')], N=256)

_ = viz.visualize_image_attr(np.transpose(attributions_ig.squeeze().cpu().detach().numpy(), (1,2,0)),
                             np.transpose(transformed_img.squeeze().cpu().detach().numpy(), (1,2,0)),
                             method='heat_map',
                             cmap=default_cmap,
                             show_colorbar=True,
                             sign='positive',
                             title='Integrated Gradients')

在上面的图像中,您应该看到 Integrated Gradients 在图像中猫的位置附近给出了最强的信号。

使用 Occlusion 的特征归因#

基于梯度的归因方法通过直接计算输出相对于输入的改变来帮助理解模型。基于扰动的归因方法通过引入输入的变化来测量对输出的影响,更直接地处理这个问题。 Occlusion 是这样一种方法。它涉及替换输入图像的某些部分,并检查对输出信号的影响。

下面,我们设置 Occlusion 归因。与配置卷积神经网络类似,您可以指定目标区域的大小以及步长来确定各个测量值的间距。我们将使用 `visualize_image_attr_multiple()` 可视化我们的 Occlusion 归因的输出,显示每个区域的正面和负面归因的热图,并通过将原始图像与正面归因区域进行掩码。掩码提供了一个非常有启发性的视图,展示了我们的猫照片的哪些区域被模型认为是“猫样”的。

occlusion = Occlusion(model)

attributions_occ = occlusion.attribute(input_img,
                                       target=pred_label_idx,
                                       strides=(3, 8, 8),
                                       sliding_window_shapes=(3,15, 15),
                                       baselines=0)


_ = viz.visualize_image_attr_multiple(np.transpose(attributions_occ.squeeze().cpu().detach().numpy(), (1,2,0)),
                                      np.transpose(transformed_img.squeeze().cpu().detach().numpy(), (1,2,0)),
                                      ["original_image", "heat_map", "heat_map", "masked_image"],
                                      ["all", "positive", "negative", "positive"],
                                      show_colorbar=True,
                                      titles=["Original", "Positive Attribution", "Negative Attribution", "Masked"],
                                      fig_size=(18, 6)
                                     )

同样,我们看到图像中包含猫的区域被赋予了更高的重要性。

使用 Layer GradCAM 的层归因#

层归因允许您将模型中隐藏层的活动归因于输入特征。下面,我们将使用层归因算法来检查我们模型中一个卷积层的活动。

GradCAM 计算目标输出相对于给定层的梯度,为每个输出通道(输出的第 2 维)求平均,并将每个通道的平均梯度乘以层激活。结果按所有通道求和。GradCAM 是为卷积神经网络设计的;由于卷积层的活动通常在空间上映射到输入,因此 GradCAM 归因通常会被上采样并用于掩盖输入。

层归因的设置类似于输入归因,只是除了模型之外,您还必须指定模型中您希望检查的隐藏层。如上所述,当我们调用 `attribute()` 时,我们会指定感兴趣的目标类别。

layer_gradcam = LayerGradCam(model, model.layer3[1].conv2)
attributions_lgc = layer_gradcam.attribute(input_img, target=pred_label_idx)

_ = viz.visualize_image_attr(attributions_lgc[0].cpu().permute(1,2,0).detach().numpy(),
                             sign="all",
                             title="Layer 3 Block 1 Conv 2")

我们将使用 `LayerAttribution` 基类中的便利方法 `interpolate()` 来上采样此归因数据,以便与输入图像进行比较。

upsamp_attr_lgc = LayerAttribution.interpolate(attributions_lgc, input_img.shape[2:])

print(attributions_lgc.shape)
print(upsamp_attr_lgc.shape)
print(input_img.shape)

_ = viz.visualize_image_attr_multiple(upsamp_attr_lgc[0].cpu().permute(1,2,0).detach().numpy(),
                                      transformed_img.permute(1,2,0).numpy(),
                                      ["original_image","blended_heat_map","masked_image"],
                                      ["all","positive","positive"],
                                      show_colorbar=True,
                                      titles=["Original", "Positive Attribution", "Masked"],
                                      fig_size=(18, 6))

像这样的可视化可以让你对隐藏层如何响应输入产生新的见解。

使用 Captum Insights 进行可视化#

Captum Insights 是一个构建在 Captum 之上的可解释性可视化小部件,用于促进模型理解。Captum Insights 适用于图像、文本和其他特征,以帮助用户理解特征归因。它允许您可视化多个输入/输出对的归因,并为图像、文本和任意数据提供可视化工具。

在本 Notebook 的这一部分,我们将使用 Captum Insights 来可视化多个图像分类推理。

首先,让我们收集一些图像,看看模型是如何识别它们的。为了多样性,我们将使用我们的猫、一个茶壶和一个三叶虫化石。

imgs = ['img/cat.jpg', 'img/teapot.jpg', 'img/trilobite.jpg']

for img in imgs:
    img = Image.open(img)
    transformed_img = transform(img)
    input_img = transform_normalize(transformed_img)
    input_img = input_img.unsqueeze(0) # the model requires a dummy batch dimension

    output = model(input_img)
    output = F.softmax(output, dim=1)
    prediction_score, pred_label_idx = torch.topk(output, 1)
    pred_label_idx.squeeze_()
    predicted_label = idx_to_labels[str(pred_label_idx.item())][1]
    print('Predicted:', predicted_label, '/', pred_label_idx.item(), ' (', prediction_score.squeeze().item(), ')')

……看来我们的模型能够正确识别它们——但当然,我们想深入了解。为此,我们将使用 Captum Insights 小部件,我们用下面导入的 `AttributionVisualizer` 对象进行配置。`AttributionVisualizer` 需要数据批次,所以我们将引入 Captum 的 `Batch` 辅助类。我们将专门查看图像,所以我们还将导入 `ImageFeature`。

我们使用以下参数配置 `AttributionVisualizer`:

  • 要检查的模型数组(在本例中只有一个)

  • 一个评分函数,允许 Captum Insights 从模型中提取 top-k 预测

  • 一个有序的、人类可读的模型训练类别列表

  • 要查找的特征列表——在本例中是一个 `ImageFeature`

  • 一个数据集,这是一个返回输入和标签批次的迭代对象——就像您用于训练一样

from captum.insights import AttributionVisualizer, Batch
from captum.insights.attr_vis.features import ImageFeature

# Baseline is all-zeros input - this may differ depending on your data
def baseline_func(input):
    return input * 0

# merging our image transforms from above
def full_img_transform(input):
    i = Image.open(input)
    i = transform(i)
    i = transform_normalize(i)
    i = i.unsqueeze(0)
    return i


input_imgs = torch.cat(list(map(lambda i: full_img_transform(i), imgs)), 0)

visualizer = AttributionVisualizer(
    models=[model],
    score_func=lambda o: torch.nn.functional.softmax(o, 1),
    classes=list(map(lambda k: idx_to_labels[k][1], idx_to_labels.keys())),
    features=[
        ImageFeature(
            "Photo",
            baseline_transforms=[baseline_func],
            input_transforms=[],
        )
    ],
    dataset=[Batch(input_imgs, labels=[282,849,69])]
)

请注意,运行上面的单元格花费的时间并不长,不像我们之前的归因。这是因为 Captum Insights 允许您在可视化小部件中配置不同的归因算法,之后它将计算并显示归因。那个过程将需要几分钟。

运行下面的单元格将渲染 Captum Insights 小部件。然后,您可以选择归因方法及其参数,根据预测类别或预测正确性过滤模型响应,查看带有相关概率的模型预测,并查看归因与原始图像的对比热图。

visualizer.render()