当试图理解模型预测背后的原因时,局部每个样本的特征重要性可以是一个有价值的工具。这种方法使你能够将分析集中在输入数据的较小部分,从而更加有针对性地理解对模型输出贡献的关键特征。然而,通常仍然不清楚模型使用了哪些模式来识别出重要特征。这个问题可以通过回顾更多的预测解释来在一定程度上规避,针对目标样本进行分析,目的是战略性地辨别预测的实际原因,这将在本章后续实践中介绍。然而,这种方法仍然受限于你必须验证模型的样本数量,而且有时仍然很难具体确定模型使用的模式。
深度神经网络(DNNs)学习从低级到高级的特征,帮助预测层识别正确的标签。当我们使用基于局部特征重要性的解释方法时,我们无法确切知道哪些低级、中级或高级的模式对输入数据的重要性做出了贡献。对于图像来说,这些特征可能从简单的形状等低级特征,到人体轮廓等中级特征,再到逐渐构建成完整人脸或日常物体的组合模式。对于文本来说,这些特征可能从表示词义的词嵌入等低级特征,到代表文本意义的句子嵌入等中级特征,再到我们熟悉的高级特征,如话题和情感。当然,这些仅仅是我们认为神经网络学习的模式的理论假设。
在本章中,我们将探讨一种方法,帮助澄清深度神经网络学习的特征中的所有模糊性,那就是通过输入优化直接可视化神经网络检测到的模式。通过直接可视化学习到的模式,并结合激活的筛选,我们可以揭示深度神经网络做出预测的实际原因。具体来说,本章将讨论以下主题:
- 解释神经元
- 寻找需要解释的神经元
- 解释学习到的图像模式
- 发现反事实解释策略
技术要求
本章包括 Python 编程语言的实践实现。为了完成它,你需要在电脑上安装以下库:
- torchvision
- torch
- torch-lucent==0.1.8
- matplotlib==3.3.0
- captum
- pillow
- numpy
代码文件可以在 GitHub 上找到:链接。
解释神经元
神经网络(NN)层中的神经元产生的特征将被后续层使用。所产生的特征或激活值只是一个指示,表示所学模式在输入数据中的显著性。但你是否曾想过这些模式是什么?解码神经网络实际学习到的模式可以进一步提高透明度,从而帮助实现第11章《解释神经网络预测》中"探索预测解释的价值"部分提到的目标。
数据由许多复杂的模式组成,这些模式被组合成单个样本。传统上,为了辨识神经元在检测什么,必须评估大量的输入数据,并将其与其他数据进行比较,从而让人类做出定性结论,这既费时又难以做到精准。这种方法允许我们通过视觉直观地找出导致高激活值的实际模式,而不受其他高度相关模式的干扰。
更正式地说,通过优化进行特征可视化在以下使用场景中非常有用:
- 理解与混淆标签相关的模式,无需领域专家的帮助: 这种情况在现实世界的音频数据中尤为常见,标签的声音往往与大量噪声混合在一起。 这种情况在图像数据中也可能发生。 获取真实数据来验证神经网络学习到的假设并不直接或可能,尤其是当基于梯度的特征归因技术无法在现有数据上证明时。
神经元解释技术的核心是神经网络输入优化,这是一个修改神经网络输入数据的过程,使其在选择的神经元上产生高激活值。记住,在训练过程中,我们优化神经网络的权重,以减少损失值。在这个技术中,我们随机初始化一个输入,并优化该输入数据,使其在选择的神经元上产生高激活值,从而有效地将输入数据视为神经网络的权重。梯度可以自然地计算到输入数据阶段,这使得在应用学习率后可以根据计算得到的梯度更新输入数据。该技术还允许你联合优化多个神经元,使它们产生高激活,并获得显示两个不同神经元如何共存的图像。
图12.1 展示了在卷积神经网络(CNN)中从低级到中级到高级模式的概念,并通过优化图像输入数据的示例,使用了在ImageNet上预训练的efficientnet-b0模型中的随机低级、中级和高级卷积滤波器。
如果你查看高级别的滤波器模式,第一个经过优化的图像看起来有些像花朵,第二个图像则像是叶子的图案。然而,这种技术的一个主要警告是,得到的优化输入数据可能无法代表与神经元相关的所有真实生活中模式的变化。即使是动态的输入数据变量,例如图像,它可以被优化为以不同的方式呈现模式,得到的优化输入仍然可能错过一些模式的表现。应对这一警告的一个好方法是,首先获得初始优化的输入数据变体,然后执行后续优化,并确保优化后的输入数据不同于初始变体。这可以通过联合优化一个额外的组件——初始优化输入数据和当前正在优化的输入数据之间的负余弦相似度来实现。这个技术有助于生成多样化的输入数据示例。但在你优化输入数据并尝试解释一个神经元之前,你需要一个选择最佳神经元进行优化输入数据的策略,接下来会讨论这一部分内容。
找到要解释的神经元
在今天的最先进架构中,神经元的数量达到数百万或数十亿,因此不可能解释每一个神经元,坦率地说,这也是浪费时间。选择要解释的神经元应该取决于你的目标。以下列表展示了一些不同的目标和选择合适神经元的相关方法:
- 找出某个预测标签或类别模式的样子:在这种情况下,你应该简单地选择与目标标签或类别的预测相关的神经元。通常这样做是为了了解模型是否很好地捕捉到了该类别的期望模式,或者是否学到了不相关的特征。在多标签的场景中,这也很有用,在这些场景中多个标签总是一起存在,而你希望解耦标签,从而更好地理解与单一标签相关的输入模式。
- 想要理解为什么数据集中某个特定标签能被预测,或者一般情况下所有标签的潜在原因:在这种情况下,你应该选择从全局神经元重要性得分中提取的潜在中间层中最具影响力的神经元。全局神经元重要性可以通过聚合集成梯度方法的结果(在第11章《解释神经网络预测》中介绍)来获得,并应用于你验证数据集中的所有神经元。然后,你可以对所有神经元的权重进行排名,并选择前几个神经元。
- 在基于显著性的方法上找出预测的具体原因:在这种情况下,你应该选择具有最高激活值和最高重要性得分的神经元。一个神经元激活值很高并不意味着它对某个特定预测很重要,此外,一个重要的神经元也不一定是激活的。结合集成梯度的权重值和激活值来选择最重要的神经元将有助于确保选择你关心的神经元。此外,如果你基于初始输入数据的显著性图有一个重点区域,你还可以进一步筛选出更多神经元,只选择那些影响该重点区域的神经元。
- 理解多个标签或类别之间的交互:在多个标签或类别之间关系重要的场景中,你可以选择捕捉这些交互的神经元。确定在多个标签或类别一起预测时激活值高且重要性得分高的神经元。分析这些神经元有助于你理解模型如何捕捉不同标签或类别之间的关系,并可能揭示改进的潜力。
- 调查模型对对抗攻击的鲁棒性:在这种情况下,你应该选择对输入数据中的对抗扰动敏感的神经元。你可以生成对抗样本,关于如何操作可以参考第14章《分析对抗性能》,然后使用集成梯度等技术计算神经元的重要性得分。通过可视化最受对抗扰动影响的神经元,你可以深入了解模型的脆弱性,并探索潜在的防御措施。
- 探索学习到的特征的层次结构:在这种情况下,你应该选择来自神经网络不同层的神经元,理解模型是如何学习层次化特征的。选择来自较早层的神经元以研究低级特征,选择较深层的神经元以研究高级特征。你还可以选择多个神经元共同优化输入数据以实现高激活,从而理解多个神经元学习到的模式如何在同一输入数据中共存。可视化这些神经元有助于你理解模型如何构建越来越复杂的特征。这有助于你深入了解模型的学习过程并识别潜在的改进领域。
- 分析模型在不同数据集上的泛化能力:为了了解模型在新数据上的泛化效果,你应该选择在不同数据集上始终重要的神经元。使用集成梯度等技术计算不同数据集上的神经元重要性得分,并识别在所有数据集上都保持高重要性的神经元。通过可视化这些神经元,你可以深入了解模型的泛化能力,并识别潜在的改进领域。
现在我们已经建立了选择解释神经元的方法,让我们开始实际探索如何解释带有图像输入数据的神经元吧!
解释学习到的图像模式
解释处理图像数据的神经网络(NN)开启了一种全新的解释范式,即能够可视化一个神经元到底在检测什么。在音频输入数据的情况下,解释神经网络将使我们能够听到神经元检测到的内容,类似于我们如何在图像数据中可视化模式!根据目标选择你想要理解的神经元,并通过对图像数据进行迭代优化来激活该神经元,进而可视化它检测到的模式。
然而,实际上基于神经元优化图像数据时存在一个问题,即结果图像通常会产生高频模式,这些模式被认为是噪声、不易解释且缺乏美感。高频模式是指像素强度较高且变化快速的像素,这主要是因为像素的取值范围通常没有约束,而像素单独来看并不是我们关注的语义单元。对结果图像进行放大可能会使其更易解释,但由于需要进行人工评估和额外的工作,解释效果会降低。
这个问题可以通过以下技术有效缓解:
-
频率惩罚——例如技术如下:
- 在优化过程中使用双边滤波器随机模糊图像,双边滤波器不仅能够模糊图像,还能保留边缘模式。
- 在优化过程中保守地惩罚相邻像素之间的变化。
- 图像增强。
-
图像预处理——例如技术如下:
- 数据去相关化。
- 快速傅里叶变换。
接下来,我们将通过一个实践教程,使用在ImageNet数据集上预训练的121层DenseNet模型来进行操作。
解释图像输入数据的预测与集成梯度
在本节中,我们将探索如何使用集成梯度方法解释来自卷积神经网络(CNN)模型的预测,该模型接受图像输入数据,并提供有关模型为何做出该预测的一些见解。在这个教程中,我们将发现从预测解释中缺失的答案,这将为我们解释CNN模型打下基础。我们将按以下步骤进行:
首先,我们将使用lucent库进行此教程,该库提供了通过优化进行特征可视化的神经网络解释方法。此外,我们还将使用torch库来加载DenseNet模型,并使用captum库来应用集成梯度方法。让我们从导入所有必要的库开始:
import glob
import numpy as np
import torch
import torch.nn as nn
import torchvision.transforms as transforms
from PIL import Image
from captum.attr import IntegratedGradients
from captum.attr import NoiseTunnel
from captum.attr import visualization as viz
from lucent.optvis import render, param, objectives
from lucent.optvis.objectives import diversity
接下来,我们将定义我们将使用的预训练DenseNet模型的模型类:
class CNN(nn.Module):
def __init__(self, num_classes, model='resnet50'):
super(CNN, self).__init__()
self.num_classes = num_classes
self.chosen_model = model
if self.chosen_model=='densenet121':
self.model = models.densenet121(pretrained=True)
self.classifier = nn.Sequential(
nn.Dropout(p=0.1),
nn.Linear(self.model.classifier.in_features, 256, bias=False),
nn.ReLU(),
nn.BatchNorm1d(256),
nn.Linear(256, 128, bias=False),
nn.ReLU(),
nn.BatchNorm1d(128),
nn.Linear(128, self.num_classes, bias=False),
nn.BatchNorm1d(self.num_classes),
)
self.model.classifier = self.classifier
model_parameters = filter(lambda p: p.requires_grad, self.model.parameters())
params = sum([np.prod(p.size()) for p in model_parameters])
def forward(self, x):
return self.model(x)
我们将使用定义的模型类加载在名为HAM10000的数据集上预训练的权重,该数据集包含七种不同的皮肤病变类别:
checkpoint = torch.load('0.8228 checkpoint.pt', map_location=torch.device('cpu'))
model = checkpoint['model']
预训练模型的第七个预测层的索引是用于预测黑色素瘤,这是一种皮肤癌。让我们看一下ISIC-2017数据集中的几个黑色素瘤样本,看看预训练模型在预测这些图像时到底关注了什么。我们将使用captum库中的集成梯度方法。首先,让我们定义需要的预处理逻辑,转换一个numpy图像数组为torch张量,将图像调整为预训练的图像大小,并进行归一化:
resize_transform = transforms.Compose([
transforms.ToTensor(),
transforms.Resize((224, 224)),
])
norm_transform = transforms.Normalize(
mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225])
均值和标准差值直接来源于ImageNet数据集,且在对HAM10000数据集进行预训练之前,模型是在ImageNet上预训练的。此外,224的维度也是从ImageNet预训练设置中采用的。由于我们需要单独获取调整大小后的中间结果,我们将调整大小和归一化逻辑分开定义。
接下来,我们将使用glob库加载提供的数据集文件夹中的所有黑色素瘤图像:
all_melanoma_images = glob.glob("lesions_augmented_data/Data/test/Melanoma/*.jpg")
我们将使用captum库实现的集成梯度和噪声隧道方法来平滑掉结果的归因噪声。让我们定义执行这些组件所需的实例,并定义我们感兴趣的黑色素瘤目标类的预测类索引:
integrated_gradients = IntegratedGradients(model)
noise_tunnel_applyer = NoiseTunnel(integrated_gradients)
prediction_label_index = 6
现在我们可以循环处理前六张图像,应用预处理,应用captum的集成梯度方法,最后可视化原始图像和得到的输入重要性热图:
for melanoma_image in all_melanoma_images[:6]:
pil_image = Image.open(melanoma_image)
img = np.array(pil_image)
transformed_image = resize_transform(img)
input = norm_transform(transformed_image).unsqueeze(0)
attributions_ig_nt = noise_tunnel_applyer.attribute(
input, nt_samples=10, nt_type='smoothgrad_sq',
target=prediction_label_index)
_ = viz.visualize_image_attr_multiple(
np.transpose(attributions_ig_nt.squeeze().cpu().detach().numpy(), (1,2,0)),
np.transpose(transformed_image.squeeze().cpu().detach().numpy(), (1,2,0)),
["original_image", "heat_map"],
["all", "positive"],
cmap='turbo',
show_colorbar=True)
这将展示如图12.2所示的可视化结果:
模型似乎主要关注前五个例子中的较暗斑点,但模型仍然考虑了周围的皮肤,尽管关注度较低。这可能表明模型在一定程度上依赖周围的皮肤来预测黑色素瘤。但这也引发了一个问题:模型是通过识别皮肤的黑暗程度来预测黑色素瘤,还是识别某种潜在模式,或者是两者兼有?对于最后一个例子,模型似乎表现得有些混乱,并没有真正聚焦于暗斑。这可能意味着皮肤上存在与黑色素瘤相关的模式,这些模式不一定是颜色较暗的。通过这些例子,还有一些问题无法得到解答,具体如下:
- 模型是否依赖皮肤的颜色来预测黑色素瘤?还是它实际上是依赖于某种模式?
- 模型究竟在检测什么样的模式?
为了解答这些问题,我们将使用lucent库可视化模型学习到的模式,以更自信地预测黑色素瘤。
实际可视化神经元与图像输入数据
在本节中,我们将继续上一篇教程,进一步探讨如何通过优化技术实践地可视化神经元与图像输入数据,深入了解CNN模型学习到的模式和行为。这个过程涉及选择要解释的神经元、为这些神经元优化图像数据,并应用正则化技术生成可视化的可解释模式。通过可视化模型学习到的模式,我们可以更好地理解模型的预测,并回答通过传统特征重要性方法可能无法显现的问题。
通过按照本教程中列出的步骤,你可以可视化深度神经网络学习到的模式,深入理解模型的预测以及贡献这些预测的特征。这有助于解答关于模型是否依赖某些特征的问题,例如皮肤的颜色或黑色素瘤的形状,并提供有关模型模式和行为的有价值的见解。让我们开始吧:
首先,定义必要的变量。我们想要可视化黑色素瘤类别的图像模式,它位于第六个预测层的索引位置,因此我们必须定义我们希望优化的参数,如下所示:
param_to_optimize = "classifier_8:6"
接下来,对于第一次迭代,我们将使用组合模式生成网络(CPPN)方法来初始化并生成输入图像,而不是直接生成随机的图像输入。请注意,这是一种专门用于图像的技术,并且已被证明能够生成更加美观的图像。在lucent中,CPPN由几个卷积层组成,其组合激活函数包括元素级的正切函数、平方、除法和连接操作。这意味着,我们不是直接优化图像,而是优化生成主网络输入图像的CPPN卷积网络的参数。反向传播可以一直执行到生成的输入图像,直到CPPN网络的第一层。初始输入图像是一个固定图像,图像中心是一个圆形区域,圆心的值接近零,向圆的边缘逐渐增大。然而,使用CPPN时,通常需要较低的学习率才能正确收敛。让我们定义一个CPPN配置,设置图像大小为224,并使用较低学习率的Adam优化器:
cppn_param_f = lambda: param.cppn(224)
cppn_opt = lambda params: torch.optim.Adam(params, 4e-3)
最后,利用定义好的变量,并使用配置了GPU的模型来可视化黑色素瘤部分的预测层捕捉到的模式:
model.cuda()
images = render.render_vis(
model, param_to_optimize, cppn_param_f, cppn_opt,
thresholds=np.linspace(0, 10000, 100, dtype=int).tolist(),
verbose=True,
show_inline=True
)
其中,thresholds列表控制优化步骤的数量,并确定哪些中间步骤的优化图像需要被可视化。此外,render_vis方法中的一个隐藏组件是transforms组件。transforms组件通过添加最小的增强,如填充、抖动、随机缩放和随机旋转,来减少图像优化过程中的随机噪声。上述代码的结果如图12.3所示:
通过这个过程,我们成功生成了一幅看起来像是真正黑色素瘤的图像。
接下来,让我们来查看这张图像的黑色素瘤概率。我们可以通过定义执行推断所需的预处理方法来实现:
inference_transform = transforms.Compose([
transforms.ToTensor(),
transforms.Resize((224, 224)),
transforms.Normalize(mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225]),
])
在这里,我们通过禁用梯度计算来预测最终优化图像的皮肤病变类别:
def inference_lesion_model(img):
transformed_image = inference_transform(img).unsqueeze(0)
with torch.no_grad():
output = torch.nn.functional.softmax(model(transformed_image), dim=1)
return output
output = inference_lesion_model(images[-1].squeeze())
这会得到100%的概率,预测这张图像为黑色素瘤!
然而,单独通过这张图像并不能显现出所有种类的图像都能被识别为黑色素瘤。有些类别可以通过一张图像充分表示,但有些标签则不能通过一张图片来代表。以背景类为例:不可能将每一个背景视觉元素都放入一张图像中。基于这个结果,有一些具体问题可能会有帮助,具体如下:
- 皮肤的颜色是否重要?
- 黑色素瘤的形状是否重要,因为最终生成的图像似乎有类似的黑色素瘤模式?
- 黑色素瘤斑块的颜色是否重要?因为绿色斑块和红色斑块有相似的模式。
这时,之前提到的用于确保多样性的损失函数可以提供更多的洞察力。现在,让我们利用多样性目标与原始黑色素瘤预测层索引目标,优化一批四个输入图像。CPPN的批处理功能在lucent中不支持,只能支持基本的输入图像初始化参数模块:
obj = objectives.channel("classifier_8", 6) - 1e2 * diversity("input")
batch_param_f = lambda: param.image(224, fft=True, decorrelate=True, batch=4)
batch_images = render.render_vis(
model, obj,
batch_param_f,
thresholds=np.linspace(0, 500, 50, dtype=int).tolist(),
show_inline=True,
verbose=True,
)
关于图像初始化方法的两点补充说明如下:
fft代表快速傅里叶变换(Fast Fourier Transform)decorrelate应用奇异值分解(SVD)到图像输入
这两种技术在研究中被认可为能够加速收敛,减少高频图像,并生成更美观的图像。
结果如图12.4所示:
这些图像看起来非常奇特。非黑色部分的颜色可能是在模拟皮肤。让我们查看这些图像的黑色素瘤类别概率,以便验证:
outputs = []
for img_idx in range(4):
img = batch_images[-1][img_idx]
output = inference_lesion_model(img)
outputs.append((output[0][6], output.argmax()))
这将得到以下数组:
[(tensor(0.9946, device='cuda:0'), tensor(6, device='cuda:0')), (tensor(0.9899, device='cuda:0'), tensor(6, device='cuda:0')), (tensor(0.7015, device='cuda:0'), tensor(6, device='cuda:0')), (tensor(0.9939, device='cuda:0'), tensor(6, device='cuda:0'))]
它们都对黑色素瘤的概率较高!从中可以得出以下结论:
- 模型并不太依赖皮肤颜色来检测黑色素瘤。皮肤颜色所能提供的最多也许是大约3%的概率提升。
- 模型主要依赖底层的低级模式来检测黑色素瘤。
- 模型并不太依赖黑色素瘤斑块的颜色。第一次生成的图像和真实图像中的黑色素瘤是红色的,而批量生成图像中的黑色素瘤斑块是黑色的。
- 模型能够从所用的真实图像中检测到更小的黑色素瘤信号。
这些结果充分展示了每种洞察技术之间的互补性。我们将以一些关于通过优化技术可视化神经元模式的有用笔记结束这一话题:
- 有些问题比其他问题更难以收敛,甚至有些根本无法收敛。准备好进行多次实验,看看你是否能得到一个能在你选择的神经元、通道或整个层上高度激活的输入。你甚至可以选择多个神经元,看看它们是如何相互作用的!
- 损失函数可能会变得非常负,输入收敛得越多,负值就越大。这是好事,因为损失是定义为结果激活值的负值。
- 正则化技术是通过优化生成合理输入的关键。
- 使用真实数据和多样化优化数据来理解你的模型学习到的检测模式。一组优化后的数据通常无法代表一个神经元可以检测到的所有模式范围。
- 在本教程中,我们使用了最终的分类层,这使得找到对所选神经元高度激活的样本变得更容易。如果选择的是中间层神经元,确保找到激活值最高的数据集。
- 针对基于PyTorch模型的lucent库和基于TensorFlow模型的lucid库,虽然它们专注于图像可视化,但也可以适应其他输入变量类型,如文本。然而,在如何为其他变量类型找到良好的正则化技术以实现更快的收敛方面,研究仍然较少。
总体来说,通过优化技术可视化神经元可以为机器学习(ML)模型的模式和行为提供有价值的洞察,但这需要实验和对所用输入及正则化技术的仔细考虑。作为额外内容,了解如何执行预测解释和神经网络解释后,我们将发现一种有用的方法来使解释变得更加有效,这种方法被称为反事实解释。
探索反事实解释策略
反事实解释或推理是一种通过考虑替代情境和反事实场景或“如果……”情况来理解和解释事物的一般方法。在预测解释的背景下,它涉及识别输入数据的变化,这些变化会导致不同的结果。理想情况下,应识别出最小的变化。在神经网络解释的背景下,它涉及可视化目标标签或中间潜在特征的相反情况。这种方法之所以有意义,是因为它与人类自然解释事件和评估因果关系的方式非常契合,这最终使我们能够更好地理解模型的决策过程。
人类倾向于从因果关系的角度思考,我们经常探索替代可能性来理解事件或决策。例如,在试图理解为什么做出某个决策时,我们可能会问类似的问题:“如果我们选择了不同的选项,会发生什么?”或者“是什么因素导致了这个结果?”这种推理帮助我们识别出影响决策的关键因素,并让我们从经验中学习。机器学习模型的反事实解释遵循类似的思维过程。通过呈现出本应导致不同预测的替代输入实例,反事实解释帮助我们理解输入数据中哪些特征在模型决策过程中最为关键。这种解释方式使得用户能够更直观地理解模型的推理过程,也有助于提高用户对模型预测的信任。
反事实推理补充了特征重要性和神经元可视化技术。通过结合这些方法,我们可以更全面地理解模型是如何做出决策的。这反过来可以帮助用户更好地评估模型的可靠性,并根据模型的预测做出更明智的决策。
总结
神经网络解释是一种模型理解过程,区别于解释模型做出的预测。当你有揭示特定预测标签或类别模式外观的目标时,或者希望深入了解数据集中某个标签的预测因素,或所有标签的一般预测因素时,神经网络解释将会非常有用。此外,它还可以帮助你详细分析预测背后的原因。
在实际应用中,解释神经网络时可能会遇到一些困难,因此不要害怕尝试调整本章介绍的参数和组件,以帮助你更好地理解你的神经网络。
在下一章中,我们将探索你可以从数据和模型中获得的另一种洞察,那就是偏见与公平性问题。