在 TensorFlow.org 上查看 | 在 Google Colab 中运行 | 在 GitHub 上查看 | 下载笔记本 | 查看 TF Hub 模型 |
本教程演示了如何实现 **集成梯度 (IG)**,这是一种在论文 深度网络的公理属性 中介绍的 可解释 AI 技术。IG 旨在解释模型预测与其特征之间的关系。它有许多用例,包括了解特征重要性、识别数据偏差和调试模型性能。
IG 由于其广泛的适用性(例如图像、文本、结构化数据)、易于实现、理论依据以及相对于其他方法的计算效率而成为一种流行的可解释性技术,这些方法允许它扩展到大型网络和特征空间,例如图像。
在本教程中,您将逐步了解 IG 的实现,以了解图像分类器的像素特征重要性。例如,请考虑这张 图像,它是一艘喷射水柱的消防船。您会将此图像分类为消防船,并可能突出显示构成船体和水炮的像素,因为它们对您的决策很重要。您的模型稍后在本教程中也将把此图像分类为消防船;但是,它在解释其决策时是否会突出显示相同的像素作为重要像素?
在下方的标题为“IG 属性掩码”和“原始 + IG 掩码叠加”的图像中,您可以看到您的模型反而突出显示(以紫色显示)构成船体水炮和水柱的像素,因为它们比船体本身对决策更重要。您的模型将如何推广到新的消防船?没有水柱的消防船呢?继续阅读以了解更多关于 IG 的工作原理以及如何将 IG 应用于您的模型,以更好地了解其预测与其底层特征之间的关系。
设置
import matplotlib.pylab as plt
import numpy as np
import tensorflow as tf
import tensorflow_hub as hub
从 TF-Hub 下载预训练的图像分类器
IG 可以应用于任何可微模型。本着原始论文的精神,您将使用相同模型(Inception V1)的预训练版本,您将从 TensorFlow Hub 下载它。
model = tf.keras.Sequential([
hub.KerasLayer(
name='inception_v1',
handle='https://tfhub.dev/google/imagenet/inception_v1/classification/4',
trainable=False),
])
model.build([None, 224, 224, 3])
model.summary()
从模块页面,您需要记住有关 Inception V1 的以下内容
输入:模型的预期输入形状为 (None, 224, 224, 3)
。这是一个 dtype 为 float32 的密集 4D 张量,形状为 (batch_size, height, width, RGB channels)
,其元素是归一化为 [0, 1] 范围内的像素的 RGB 颜色值。第一个元素是 None
,表示模型可以接受任何整数批次大小。
输出:一个 tf.Tensor
,其 logits 形状为 (batch_size, 1001)
。每一行代表模型对 ImageNet 中 1,001 个类别的预测分数。对于模型的最高预测类别索引,您可以使用 tf.math.argmax(predictions, axis=-1)
。此外,您还可以使用 tf.nn.softmax(predictions, axis=-1)
将模型的 logits 输出转换为所有类别的预测概率,以量化模型的不确定性并探索类似的预测类别以进行调试。
def load_imagenet_labels(file_path):
labels_file = tf.keras.utils.get_file('ImageNetLabels.txt', file_path)
with open(labels_file) as reader:
f = reader.read()
labels = f.splitlines()
return np.array(labels)
imagenet_labels = load_imagenet_labels('https://storage.googleapis.com/download.tensorflow.org/data/ImageNetLabels.txt')
使用 tf.image
加载和预处理图像
您将使用来自 维基百科共享资源 的两张图像来说明 IG:一张 消防船 和一张 大熊猫。
def read_image(file_name):
image = tf.io.read_file(file_name)
image = tf.io.decode_jpeg(image, channels=3)
image = tf.image.convert_image_dtype(image, tf.float32)
image = tf.image.resize_with_pad(image, target_height=224, target_width=224)
return image
img_url = {
'Fireboat': 'http://storage.googleapis.com/download.tensorflow.org/example_images/San_Francisco_fireboat_showing_off.jpg',
'Giant Panda': 'http://storage.googleapis.com/download.tensorflow.org/example_images/Giant_Panda_2.jpeg',
}
img_paths = {name: tf.keras.utils.get_file(name, url) for (name, url) in img_url.items()}
img_name_tensors = {name: read_image(img_path) for (name, img_path) in img_paths.items()}
plt.figure(figsize=(8, 8))
for n, (name, img_tensors) in enumerate(img_name_tensors.items()):
ax = plt.subplot(1, 2, n+1)
ax.imshow(img_tensors)
ax.set_title(name)
ax.axis('off')
plt.tight_layout()
对图像进行分类
首先对这些图像进行分类,并显示前 3 个最自信的预测。以下是一个实用程序函数,用于检索前 k 个预测标签和概率。
def top_k_predictions(img, k=3):
image_batch = tf.expand_dims(img, 0)
predictions = model(image_batch)
probs = tf.nn.softmax(predictions, axis=-1)
top_probs, top_idxs = tf.math.top_k(input=probs, k=k)
top_labels = imagenet_labels[tuple(top_idxs)]
return top_labels, top_probs[0]
for (name, img_tensor) in img_name_tensors.items():
plt.imshow(img_tensor)
plt.title(name, fontweight='bold')
plt.axis('off')
plt.show()
pred_label, pred_prob = top_k_predictions(img_tensor)
for label, prob in zip(pred_label, pred_prob):
print(f'{label}: {prob:0.1%}')
计算积分梯度
您的模型 Inception V1 是一个学习函数,它描述了输入特征空间(图像像素值)与由 ImageNet 类概率值(介于 0 和 1 之间)定义的输出空间之间的映射。神经网络的早期可解释性方法使用梯度分配特征重要性分数,这些梯度告诉您哪些像素在您模型的预测函数的给定点处具有最陡峭的局部值。但是,梯度仅描述模型预测函数相对于像素值的局部变化,而没有完全描述整个模型预测函数。当您的模型完全“学习”单个像素的范围与正确 ImageNet 类别之间的关系时,该像素的梯度将饱和,这意味着变得越来越小,甚至变为零。考虑以下简单的模型函数
def f(x):
"""A simplified model function."""
return tf.where(x < 0.8, x, 0.8)
def interpolated_path(x):
"""A straight line path."""
return tf.zeros_like(x)
x = tf.linspace(start=0.0, stop=1.0, num=6)
y = f(x)
左侧:您模型的像素
x
的梯度在 0.0 到 0.8 之间为正,但在 0.8 到 1.0 之间变为 0.0。像素x
对将您的模型推向真实类别的 80% 预测概率显然具有重大影响。像素x
的重要性很小或不连续是否有意义?右侧:IG 背后的直觉是累积像素
x
的局部梯度,并将其重要性作为分数来衡量它对模型的整体输出类别概率的增加或减少。您可以将 IG 分解为 3 个部分并计算它- 在特征空间中,沿着从 0(基线或起点)到 1(输入像素的值)的直线进行小步长插值
- 在模型预测相对于每个步骤的预测之间计算每个步骤的梯度
- 通过累积(累积平均值)这些局部梯度来近似基线和输入之间的积分。
为了加强这种直觉,您将通过将 IG 应用于下面的示例“消防船”图像来逐步了解这 3 个部分。
建立基线
基线是作为计算特征重要性的起点使用的输入图像。直观地说,您可以将基线的解释作用视为代表每个像素不存在对“消防船”预测的影响,以对比它在输入图像中存在时每个像素对“消防船”预测的影响。因此,基线的选择在解释和可视化像素特征重要性方面起着核心作用。有关基线选择的更多讨论,请参阅本教程底部“下一步”部分中的资源。在这里,您将使用一个像素值全为零的黑色图像。
您可以尝试的其他选择包括全白图像或随机图像,您可以使用 tf.random.uniform(shape=(224,224,3), minval=0.0, maxval=1.0)
创建。
baseline = tf.zeros(shape=(224,224,3))
plt.imshow(baseline)
plt.title("Baseline")
plt.axis('off')
plt.show()
将公式分解为代码
积分梯度的公式如下
\(IntegratedGradients_{i}(x) ::= (x_{i} - x'_{i})\times\int_{\alpha=0}^1\frac{\partial F(x'+\alpha \times (x - x'))}{\partial x_i}{d\alpha}\)
其中
\(_{i}\) = 特征
\(x\) = 输入
\(x'\) = 基线
\(\alpha\) = 用于扰动特征的插值常数
在实践中,计算定积分并不总是数值上可行的,并且可能在计算上很昂贵,因此您计算以下数值近似值
\(IntegratedGrads^{approx}_{i}(x)::=(x_{i}-x'_{i})\times\sum_{k=1}^{m}\frac{\partial F(x' + \frac{k}{m}\times(x - x'))}{\partial x_{i} } \times \frac{1}{m}\)
其中
\(_{i}\) = 特征(单个像素)
\(x\) = 输入(图像张量)
\(x'\) = 基线(图像张量)
\(k\) = 缩放的特征扰动常数
\(m\) = 积分的黎曼和近似中的步数
\((x_{i}-x'_{i})\) = 基线差值项。这是必要的,以缩放积分梯度并使其保持在原始图像的范围内。从基线图像到输入的路径位于像素空间中。由于使用 IG,您是在一条直线上进行积分(线性变换),因此这最终与插值图像函数相对于 \(\alpha\) 的导数的积分项大致等效,并且具有足够的步数。积分将每个像素的梯度乘以路径上像素的变化量进行累加。将此积分实现为从一个图像到另一个图像的均匀步长更简单,将 \(x := (x' + \alpha(x-x'))\) 代入。因此,变量的变化给出 \(dx = (x-x')d\alpha\)。\((x-x')\) 项是常数,并从积分中分解出来。
插值图像
\(IntegratedGrads^{approx}_{i}(x)::=(x_{i}-x'_{i})\times\sum_{k=1}^{m}\frac{\overbrace{\partial F(\overbrace{x' + \frac{k}{m}\times(x - x')}^\text{在 k 个间隔处插值 m 个图像})}^\text{计算梯度} }{\partial x_{i} } \times \frac{1}{m}\)
首先,您将生成基线图像和原始图像之间的 线性插值。您可以将插值图像视为特征空间中基线和输入之间的小步长,由原始方程中的 \(\alpha\) 表示。
m_steps=50
alphas = tf.linspace(start=0.0, stop=1.0, num=m_steps+1) # Generate m_steps intervals for integral_approximation() below.
def interpolate_images(baseline,
image,
alphas):
alphas_x = alphas[:, tf.newaxis, tf.newaxis, tf.newaxis]
baseline_x = tf.expand_dims(baseline, axis=0)
input_x = tf.expand_dims(image, axis=0)
delta = input_x - baseline_x
images = baseline_x + alphas_x * delta
return images
使用上面的函数在黑色基线图像和示例“消防船”图像之间以 alpha 间隔沿着线性路径生成插值图像。
interpolated_images = interpolate_images(
baseline=baseline,
image=img_name_tensors['Fireboat'],
alphas=alphas)
可视化插值图像。注意:另一种看待 \(\alpha\) 常数的方法是,它始终增加每个插值图像的强度。
fig = plt.figure(figsize=(20, 20))
i = 0
for alpha, image in zip(alphas[0::10], interpolated_images[0::10]):
i += 1
plt.subplot(1, len(alphas[0::10]), i)
plt.title(f'alpha: {alpha:.1f}')
plt.imshow(image)
plt.axis('off')
plt.tight_layout();
计算梯度
本节解释如何计算梯度以衡量特征变化与模型预测变化之间的关系。在图像的情况下,梯度告诉我们哪些像素对模型的预测类别概率具有最强的影响。
\(IntegratedGrads^{approx}_{i}(x)::=(x_{i}-x'_{i})\times\sum_{k=1}^{m}\frac{\overbrace{\partial F(\text{插值图像})}^\text{计算梯度} }{\partial x_{i} } \times \frac{1}{m}\)
其中
\(F()\) = 您的模型的预测函数
\(\frac{\partial{F} }{\partial{x_i} }\) = 模型 F 的预测函数相对于每个特征 \(x_i\) 的梯度(偏导数 \(\partial\) 的向量)
TensorFlow 使用 tf.GradientTape
使您能够轻松地计算梯度。
def compute_gradients(images, target_class_idx):
with tf.GradientTape() as tape:
tape.watch(images)
logits = model(images)
probs = tf.nn.softmax(logits, axis=-1)[:, target_class_idx]
return tape.gradient(probs, images)
计算沿插值路径的每个图像相对于正确输出的梯度。回想一下,您的模型返回一个形状为 (1, 1001)
的 Tensor
,其中包含 logits,您可以将其转换为每个类别的预测概率。您需要将正确的 ImageNet 目标类别索引传递给图像的 compute_gradients
函数。
path_gradients = compute_gradients(
images=interpolated_images,
target_class_idx=555)
注意 (n_interpolated_images, img_height, img_width, RGB)
的输出形状,它为我们提供了沿插值路径的每个图像的每个像素的梯度。您可以将这些梯度视为衡量模型预测在特征空间中每个小步长的变化。
print(path_gradients.shape)
可视化梯度饱和
回想一下,您上面计算的梯度描述了模型对“消防船”的预测概率的局部变化,并且可能饱和。
这些概念使用您上面计算的梯度在下面的 2 个图中可视化。
pred = model(interpolated_images)
pred_proba = tf.nn.softmax(pred, axis=-1)[:, 555]
左侧:此图显示了模型对“消防船”类别的置信度如何在所有 alpha 中变化。请注意,梯度或线的斜率在 0.6 到 1.0 之间很大程度上变平或饱和,然后在最终的“消防船”预测概率约为 40% 时稳定下来。
右侧:右侧图更直接地显示了 alpha 上的平均梯度幅度。请注意,这些值如何急剧接近甚至短暂地低于零。实际上,您的模型从较低 alpha 值的梯度中“学习”最多,然后饱和。直观地说,您可以将其视为您的模型已经学习了像素(例如水炮)以做出正确的预测,将这些像素梯度发送到零,但仍然非常不确定,并且专注于虚假的桥梁或水柱像素,因为 alpha 值接近原始输入图像。
为了确保这些重要的水炮像素反映为对“消防船”预测的重要性,您将在下面继续学习如何累积这些梯度,以准确地近似每个像素对“消防船”预测概率的影响。
累积梯度(积分近似)
对于 IG,您可以通过多种不同的方式计算积分的数值近似值,这些方法在不同函数上的精度和收敛性方面存在不同的权衡。一种流行的方法类别称为 黎曼和。在这里,您将使用梯形法则(您可以在本教程末尾找到探索不同近似方法的额外代码)。
\(IntegratedGrads^{approx}_{i}(x)::=(x_{i}-x'_{i})\times \overbrace{\sum_{k=1}^{m} }^\text{求和 m 个局部梯度}\text{gradients(插值图像)} \times \overbrace{\frac{1}{m} }^\text{除以 m 步}\)
从等式中,您可以看到您正在对 m
个梯度求和并除以 m
步。您可以将这两个操作一起实现为第 3 部分的m
个插值预测和输入图像的局部梯度的平均值。
def integral_approximation(gradients):
# riemann_trapezoidal
grads = (gradients[:-1] + gradients[1:]) / tf.constant(2.0)
integrated_gradients = tf.math.reduce_mean(grads, axis=0)
return integrated_gradients
integral_approximation
函数获取目标类预测概率相对于基线图像和原始图像之间插值图像的梯度。
ig = integral_approximation(
gradients=path_gradients)
您可以确认对 m
个插值图像的梯度进行平均会返回一个与原始“大熊猫”图像形状相同的集成梯度张量。
print(ig.shape)
将所有内容整合在一起
现在,您将把前面 3 个一般部分组合成一个 IntegratedGradients
函数,并使用 @tf.function 装饰器将其编译成高性能的可调用 TensorFlow 图。这在下面 5 个较小的步骤中实现
\(IntegratedGrads^{approx}_{i}(x)::=\overbrace{(x_{i}-x'_{i})}^\text{5.}\times \overbrace{\sum_{k=1}^{m} }^\text{4.} \frac{\partial \overbrace{F(\overbrace{x' + \overbrace{\frac{k}{m} }^\text{1.}\times(x - x'))}^\text{2.} }^\text{3.} }{\partial x_{i} } \times \overbrace{\frac{1}{m} }^\text{4.}\)
生成 alpha \(\alpha\)
生成插值图像 = \((x' + \frac{k}{m}\times(x - x'))\)
计算模型 \(F\) 输出预测相对于输入特征的梯度 = \(\frac{\partial F(\text{插值路径输入})}{\partial x_{i} }\)
通过对梯度求平均值进行积分近似 = \(\sum_{k=1}^m \text{梯度} \times \frac{1}{m}\)
根据原始图像缩放集成梯度 = \((x_{i}-x'_{i}) \times \text{集成梯度}\)。此步骤之所以必要,是为了确保在多个插值图像上累积的归因值具有相同的单位,并忠实地表示原始图像上的像素重要性。
def integrated_gradients(baseline,
image,
target_class_idx,
m_steps=50,
batch_size=32):
# Generate alphas.
alphas = tf.linspace(start=0.0, stop=1.0, num=m_steps+1)
# Collect gradients.
gradient_batches = []
# Iterate alphas range and batch computation for speed, memory efficiency, and scaling to larger m_steps.
for alpha in tf.range(0, len(alphas), batch_size):
from_ = alpha
to = tf.minimum(from_ + batch_size, len(alphas))
alpha_batch = alphas[from_:to]
gradient_batch = one_batch(baseline, image, alpha_batch, target_class_idx)
gradient_batches.append(gradient_batch)
# Concatenate path gradients together row-wise into single tensor.
total_gradients = tf.concat(gradient_batches, axis=0)
# Integral approximation through averaging gradients.
avg_gradients = integral_approximation(gradients=total_gradients)
# Scale integrated gradients with respect to input.
integrated_gradients = (image - baseline) * avg_gradients
return integrated_gradients
@tf.function
def one_batch(baseline, image, alpha_batch, target_class_idx):
# Generate interpolated inputs between baseline and input.
interpolated_path_input_batch = interpolate_images(baseline=baseline,
image=image,
alphas=alpha_batch)
# Compute gradients between model outputs and interpolated inputs.
gradient_batch = compute_gradients(images=interpolated_path_input_batch,
target_class_idx=target_class_idx)
return gradient_batch
ig_attributions = integrated_gradients(baseline=baseline,
image=img_name_tensors['Fireboat'],
target_class_idx=555,
m_steps=240)
同样,您可以检查 IG 特征归因是否与输入“消防艇”图像具有相同的形状。
print(ig_attributions.shape)
该论文建议根据示例将步骤数范围设置为 20 到 300 之间(尽管在实践中,这可能在 1,000 多个范围内以准确地近似积分)。您可以在本教程末尾的“下一步”资源中找到用于检查适当步骤数的额外代码。
可视化归因
您已准备好可视化归因,并将它们叠加在原始图像上。下面的代码对集成梯度在颜色通道上的绝对值求和,以生成归因掩码。这种绘图方法捕获了像素对模型预测的相对影响。
查看“消防艇”图像上的归因,您可以看到模型将水炮和喷嘴识别为有助于其正确预测的因素。
_ = plot_img_attributions(image=img_name_tensors['Fireboat'],
baseline=baseline,
target_class_idx=555,
m_steps=240,
cmap=plt.cm.inferno,
overlay_alpha=0.4)
在“大熊猫”图像上,归因突出了熊猫脸部的纹理、鼻子和毛皮。
_ = plot_img_attributions(image=img_name_tensors['Giant Panda'],
baseline=baseline,
target_class_idx=389,
m_steps=55,
cmap=plt.cm.viridis,
overlay_alpha=0.5)
用途和局限性
用例
- 在部署模型之前使用集成梯度等技术可以帮助您对模型的工作原理和原因形成直觉。这种技术突出显示的特征是否与您的直觉相符?如果不是,这可能表明您的模型或数据集中存在错误,或者过度拟合。
局限性
集成梯度技术提供单个示例上的特征重要性。但是,它不提供整个数据集的全局特征重要性。
集成梯度技术提供单个特征重要性,但它不解释特征交互和组合。
下一步
本教程介绍了集成梯度的基本实现。作为下一步,您可以使用此笔记本自己尝试使用不同的模型和图像来使用此技术。
对于感兴趣的读者,本教程有一个更长的版本(其中包括用于计算不同基线、计算积分近似值以及确定足够步骤数的代码),您可以在 此处 找到它。
为了加深您的理解,请查看论文 深度网络的公理归因 和 Github 存储库,其中包含 TensorFlow 以前版本中的实现。您还可以探索 distill.pub 上的特征归因以及不同基线的影响。
有兴趣将 IG 整合到您的生产机器学习工作流程中以进行特征重要性、模型错误分析和数据倾斜监控吗?查看 Google Cloud 的 可解释 AI 产品,该产品支持 IG 归因。Google AI PAIR 研究小组还开源了 What-if 工具,可用于模型调试,包括可视化 IG 特征归因。