备注
点击此处下载完整示例代码
简介 || 张量 || Autograd || 模型构建 || TensorBoard 支持 || 训练模型 || 模型理解
模型理解与 Captum ¶
创建于:2025 年 4 月 1 日 | 最后更新:2025 年 4 月 1 日 | 最后验证:2024 年 11 月 5 日
按照下面的视频或 YouTube 上的视频进行操作。在此处下载笔记本和相关文件。
Captum(拉丁语中的“理解”)是一个基于 PyTorch 的开源、可扩展的模型可解释性库。
随着模型复杂度的增加和随之而来的透明度降低,模型可解释性方法变得越来越重要。模型理解既是研究的热点,也是机器学习在各个行业应用中的关注点。Captum 提供了最先进的算法,包括集成梯度,为研究人员和开发者提供了一种简单的方法来理解哪些特征对模型的输出有贡献。
完整的文档、API 参考和一系列特定主题的教程可在 captum.ai 网站上找到。
简介
Captum 对模型可解释性的方法是归因。Captum 中有三种可用的归因方法:
特征归因试图用输入特征的术语来解释特定的输出。例如,用评论中的某些词来解释电影评论是正面还是负面,就是特征归因的一个例子。
层归因检查模型在特定输入后的隐藏层活动。以卷积层对输入图像的响应为例,检查空间映射的输出是层归因的一个例子。
神经归因类似于层归因,但专注于单个神经元的活动。
在这个交互式笔记本中,我们将探讨特征归因和层归因。
三种归因类型各自都有多个与之相关的归因算法。许多归因算法可以分为两大类:
基于梯度的算法计算模型输出、层输出或神经元激活相对于输入的逆向梯度。集成梯度(用于特征)、层梯度 * 激活和神经元电导都是基于梯度的算法。
基于扰动的算法检查模型、层或神经元在输入变化时的输出变化。输入扰动可以是定向的或随机的。遮挡、特征消除和特征置换都是基于扰动的算法。
下面我们将探讨这两种类型的算法。
尤其是在涉及大型模型的情况下,将归因数据以易于与正在检查的输入特征相关联的方式可视化可能非常有价值。虽然使用 Matplotlib、Plotly 或类似工具创建自己的可视化当然可行,但 Captum 提供了针对其归因的增强工具:
The
captum.attr.visualization
模块(以下导入为viz
)提供了与图像相关的归因可视化的有用函数。Captum Insights 是一个易于使用的 API,它构建在 Captum 之上,提供了一个可视化小部件,其中包含针对图像、文本和任意模型类型的现成可视化。
这两个可视化工具集将在本笔记本中演示。前几个示例将侧重于计算机视觉用例,但笔记本末尾的 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
在您设置的环境中重新启动这个笔记本,您就可以开始了!
一个第一个例子
首先,让我们从一个简单的、可视化的例子开始。我们将从一个在 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()
你获取这个交互式笔记本的地方也应该有一个 img
文件夹,其中包含一个文件 cat.jpg
。
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。
特征归因与集成梯度
特征归因将特定的输出归因于输入特征。它使用特定的输入——在这里,我们的测试图像——来生成每个输入特征相对于特定输出特征的相对重要性的映射。
集成梯度是 Captum 中可用的特征归因算法之一。集成梯度通过近似模型输出相对于输入的梯度的积分来为每个输入特征分配重要性分数。
在我们的案例中,我们将取输出向量中的一个特定元素——即表示模型对其所选类别的置信度的元素——并使用集成梯度来了解输入图像的哪些部分对这一输出做出了贡献。
一旦我们有了集成梯度的重要性映射,我们将使用 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')
在上面的图片中,你应该能看到集成梯度在图像中猫的位置给出了最强的信号。
隐藏特征归因
基于梯度的归因方法有助于从直接计算输出变化相对于输入的角度来理解模型。基于扰动的归因方法更直接地处理这个问题,通过向输入引入变化来测量对输出的影响。遮挡就是这样一种方法。它涉及替换输入图像的部分,并检查对输出信号的影响。
以下,我们设置了遮挡归因。与配置卷积神经网络类似,您可以指定目标区域的大小,以及步长长度以确定单个测量的间距。我们将通过 visualize_image_attr_multiple()
可视化我们的遮挡归因输出,展示正负归因的热图,以及通过正归因区域遮罩原始图像。这种遮罩提供了一个非常具有指导性的视图,展示了模型认为我们的猫照片中哪些区域最像猫。
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)
)
再次,我们看到图像中包含猫的区域被赋予了更大的重要性。
层级归因与层 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 适用于图像、文本和其他特征,帮助用户理解特征归因。它允许您可视化多个输入/输出对的归因,并提供图像、文本和任意数据的可视化工具。
在本笔记本的这一部分,我们将使用 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()
脚本总运行时间:(0 分钟 0.000 秒)