注意
点击 这里 下载完整示例代码
介绍 || 张量 || 自动求导 || 构建模型 || TensorBoard 支持 || 训练模型 || 模型理解
Model Understanding with Captum¶
创建日期: 2021年11月30日 | 最后更新日期: 2024年1月19日 | 最后验证日期: 2024年11月5日
跟随下方的视频或在 youtube 上观看。下载笔记本和相应的文件 这里。
Captum (拉丁语中的“理解”)是基于PyTorch的开源可扩展模型可解释性库。
随着模型复杂性的增加以及由此带来的透明度缺失,模型可解释性方法变得越来越重要。模型理解既是研究领域的活跃方向,也是各行各业在机器学习应用中重点关注的领域。Captum提供了最先进的算法,包括集成梯度(Integrated Gradients),为研究人员和开发者提供了一种简便的方式来了解哪些特征对模型的输出有贡献。
完整的文档、API 参考以及特定主题的教学套件可在 captum.ai 网站上找到。
介绍¶
Captum在模型可解释性方面的方法是通过 归因。 Captum中有三种类型的归因:
特征归因旨在通过解释输入的哪些特征产生了特定输出来进行说明。例如,根据评论中的某些词语来解释一条电影评论是正面还是负面就是一个特征归因的例子。
层归因检查模型特定输入之后隐藏层的活动情况。在对输入图像进行卷积层空间映射输出的一个例子中,展示了层归因的应用。
神经元归因类似于层归因,但专注于单个神经元的活动。
在這個交互式笔记本中,我们将探讨特征归因和层归因。
每个三种归因类型中的每一个都有多个归因算法与之关联。许多归因算法可以分为两大类:
基于梯度的算法计算模型输出、层输出或神经元激活相对于输入的反向梯度。集成梯度(针对特征)、层梯度 * 激活和神经元传导都是基于梯度的算法。
基于扰动的算法检查模型、层或神经元在输入变化时输出的变化情况。输入扰动可以是有方向的或随机的。遮盖, 特征消除, 和 特征置换 都是基于扰动的算法。
我们将在此下面分别探讨这两种类型的算法。
特别是在涉及大型模型的情况下,以易于与正在检查的输入特征相关的方式可视化归因数据是非常有价值的。虽然您可以使用Matplotlib、Plotly或其他类似工具创建自己的可视化,但Captum提供了专门针对其归因的增强工具。
The
captum.attr.visualization模块(在下方导入为viz) 提供了用于可视化与图像相关的归因的帮助函数。Captum Insights 是 Captum 之上的一个易于使用的 API,提供了一个可视化控件,其中包含了针对图像、文本和任意模型类型的预设可视化。
这两个可视化工具集将在本笔记本中进行演示。前几个示例将集中于计算机视觉的应用场景,但 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')
在上面的图片中,你应该能看到集成梯度方法在图像中给出了猫所在位置最强的信号。
特征归因与遮盖¶
基于梯度的归因方法有助于理解模型,通过直接计算输出相对于输入的变化。 基于扰动的归因方法更直接地处理这个问题,通过引入输入的变化来测量对输出的影响。 遮挡就是其中一种方法。 它涉及替换输入图像的部分区域,并检查对输出信号的影响。
Below, 我们设置遮蔽归因。类似于配置卷积神经网络,您可以指定目标区域的大小,并确定单个测量之间的间距。我们将使用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)
)
再次强调,在图像中包含猫的区域具有更大的重要性。
Layer Attribution with 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")
我们将使用 interpolate() 基类中的方便方法 LayerAttribution 来放大此归因数据以与输入图像进行比较。
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(), ')')
…and 看起来我们的模型正确地识别了它们 - 但当然,我们想深入挖掘。为此我们将使用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插件。然后你可以选择归因方法及其参数,根据预测类别或预测准确性过滤模型响应,查看与关联概率一起的模型预测,并查看归因与原始图像的 heatmap。
visualizer.render()
脚本的总运行时间: ( 0 分钟 0.000 秒)