使用 Captum 理解模型
你可以观看下方视频或 YouTube 上的对应内容跟随学习。在此处下载笔记本文件和相关文件。
Captum(拉丁语中意为“理解”)是一个基于 PyTorch 构建的开源、可扩展的模型可解释性库。
随着模型复杂度的提升以及由此带来的透明度缺失,模型可解释性方法变得越来越重要。模型理解既是活跃的研究领域,也是各行业机器学习实际应用中的重点关注方向。Captum 提供了最先进的算法(包括积分梯度 Integrated Gradients),让研究人员和开发者能够轻松理解哪些特征对模型输出有贡献。
完整的文档、API 参考和专题教程可在 captum.ai 网站查看。
引言
Captum 从归因(Attribution) 的角度实现模型可解释性。Captum 提供三种归因类型:
- 特征归因(Feature Attribution):基于生成输出的输入特征来解释特定的模型输出。例如,根据影评中的特定词汇解释该影评被判定为正面还是负面。
- 层归因(Layer Attribution):检查模型隐藏层在特定输入下的激活情况。例如,查看卷积层对输入图像的空间映射输出。
- 神经元归因(Neuron Attribution):与层归因类似,但聚焦于单个神经元的激活情况。
在本交互式笔记本中,我们将重点介绍特征归因和层归因。
每种归因类型都关联多种归因算法,这些算法主要分为两大类:
- 基于梯度的算法:计算模型输出、层输出或神经元激活相对于输入的反向梯度。积分梯度(针对特征)、Layer Gradient * Activation、Neuron Conductance 均属于此类算法。
- 基于扰动的算法:通过改变输入来观察模型、层或神经元输出的变化。输入扰动可以是定向的或随机的。遮挡(Occlusion)、特征消融(Feature Ablation)、特征置换(Feature Permutation)均属于此类算法。
下文将演示这两类算法的使用。
尤其是在处理大型模型时,将归因数据可视化以直观关联输入特征非常有价值。虽然你可以使用 Matplotlib、Plotly 等工具自行创建可视化,但 Captum 提供了针对其归因结果的增强型可视化工具:
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 文件夹中)。
# 模型期望输入 224x224 的 3 通道图像
transform = transforms.Compose([
transforms.Resize(224), # 调整尺寸
transforms.CenterCrop(224), # 中心裁剪
transforms.ToTensor() # 转为张量
])
# ImageNet 标准归一化
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) # 模型需要批量维度(添加虚拟批次维度)
# 加载标签映射
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_label, '(', prediction_score.squeeze().item(), ')')
我们已经确认 ResNet 识别出这张猫的图片确实是猫。但为什么模型认为这是猫呢?
答案就在 Captum 中。
基于积分梯度的特征归因
特征归因将特定输出归因于输入特征。它使用特定输入(此处为测试图像)生成一个映射,展示每个输入特征对特定输出特征的相对重要性。
积分梯度(Integrated Gradients)是 Captum 提供的特征归因算法之一。它通过近似模型输出相对于输入的梯度积分,为每个输入特征分配重要性分数。
在本例中,我们选取输出向量的特定元素(即模型对所选类别的置信度),使用积分梯度理解输入图像的哪些部分对该输出有贡献。
得到积分梯度的重要性图后,使用 Captum 的可视化工具展示该重要性图。Captum 的 visualize_image_attr() 函数提供多种自定义归因数据显示的选项,此处我们传入自定义的 Matplotlib 颜色映射。
运行 integrated_gradients.attribute() 通常需要 1-2 分钟。
# 用模型初始化归因算法
integrated_gradients = IntegratedGradients(model)
# 计算输出目标的归因
attributions_ig = integrated_gradients.attribute(input_img, target=pred_label_idx, n_steps=200)
# 显示原始图像作为对比
_ = viz.visualize_image_attr(None, np.transpose(transformed_img.squeeze().cpu().detach().numpy(), (1,2,0)),
method="original_image", title="原始图像")
# 自定义颜色映射
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='积分梯度归因')
在上述图像中,你会看到积分梯度在猫所在的图像区域给出了最强的信号。
基于遮挡的特征归因
基于梯度的归因方法通过直接计算输出相对于输入的变化来理解模型;而基于扰动的归因方法更直接——通过改变输入来测量对输出的影响。遮挡(Occlusion)就是这样一种方法,它通过替换输入图像的部分区域,观察对输出信号的影响。
下面配置遮挡归因。与配置卷积神经网络类似,你可以指定目标区域的大小和步长(用于确定单个测量点的间距)。使用 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=["原始图像", "正向归因", "负向归因", "掩码图像"],
fig_size=(18, 6)
)
同样,我们看到图像中猫所在的区域具有更高的重要性。
基于 Layer GradCAM 的层归因
层归因允许你将模型隐藏层的激活归因于输入特征。下面使用层归因算法分析模型中某个卷积层的激活情况。
GradCAM 计算目标输出相对于给定层的梯度,对每个输出通道(输出的第 2 维)求平均,然后将每个通道的平均梯度乘以层激活值,最后对所有通道求和。GradCAM 专为卷积网络设计;由于卷积层的激活通常与输入空间映射,GradCAM 归因结果常被上采样并用于掩码输入。
层归因的配置与输入归因类似,但除了模型外,还需指定要分析的模型隐藏层。调用 attribute() 时,指定感兴趣的目标类别。
# 初始化 Layer GradCAM,指定要分析的层
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=["原始图像", "正向归因融合图", "掩码图像"],
fig_size=(18, 6))
此类可视化能让你深入了解隐藏层如何响应输入。
使用 Captum Insights 可视化
Captum Insights 是构建在 Captum 之上的可解释性可视化组件,旨在简化模型理解。它支持图像、文本和其他特征的归因可视化,可查看多组输入/输出对的归因,并提供针对图像、文本和任意数据的可视化工具。
本节将使用 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) # 添加虚拟批次维度
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_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
# 基线函数:全零输入(可根据数据调整)
def baseline_func(input):
return input * 0
# 合并图像变换
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()
总结
- Captum 从归因角度实现模型可解释性,核心支持特征归因、层归因、神经元归因三类分析,涵盖基于梯度和基于扰动的多种算法;
- 积分梯度(Integrated Gradients)通过梯度积分量化输入特征重要性,遮挡(Occlusion)通过扰动输入观察输出变化,是理解模型决策的核心方法;
- Captum 提供丰富的可视化工具(
visualization模块、Captum Insights),可直观展示归因结果与输入特征的关联,助力模型行为分析。