可解释性人工智能-全-
可解释性人工智能(全)
原文:Interpretable AI
译者:飞龙
前置内容
前言
我有幸在过去十年左右的时间里从事数据和机器学习工作。我的背景是机器学习,我的博士研究集中在将机器学习应用于无线网络。我在顶级会议和期刊上发表了关于强化学习、凸优化以及将经典机器学习技术应用于 5G 蜂窝网络的论文(mng.bz/zQR6)。
在完成我的博士学业后,我开始在工业界作为数据科学家和机器学习工程师工作,并在多个行业(如制造、零售和金融)为客户部署复杂的 AI 解决方案方面积累了经验。正是在这段时间里,我意识到了可解释人工智能的重要性,并开始深入研究。我还开始在实际场景中实施和部署可解释性技术,以便数据科学家、商业利益相关者和专家能够更深入地理解机器学习模型。
我撰写了一篇关于可解释人工智能和提出构建稳健、可解释人工智能系统的原则性方法的博客文章(mng.bz/0wnE)。这篇文章意外地收到了来自数据科学家、研究人员和来自各行各业实践者的巨大反响。我还在多个 AI 和机器学习会议上就这个主题进行了演讲。通过将我的内容公之于众并在领先会议上发言,我学到了以下内容:
-
对这个主题感兴趣的不止我一个人。
-
我能够更好地理解社区对哪些具体主题感兴趣。
这些经验教训导致了你现在正在阅读的这本书的诞生。你可以找到一些资源来帮助你了解可解释人工智能的最新动态,比如综述论文、博客文章和一本书,但没有单一的资源或书籍涵盖了所有对人工智能从业者有价值的重要可解释性技术。也没有关于如何实施这些尖端技术的实用指南。本书旨在填补这一空白,首先为这个活跃的研究领域提供一个结构,并涵盖广泛的可解释性技术。在这本书中,我们将探讨具体的现实世界案例,并了解如何使用最先进的技术构建复杂模型并进行解释。
我坚信,随着复杂机器学习模型在现实世界的部署,理解它们至关重要。缺乏深入理解可能导致模型传播偏见,我们在刑事司法、政治、零售、面部识别和语言理解中都看到了这方面的例子。所有这些都对信任产生了有害影响,根据我的经验,这也是公司抵制人工智能部署的主要原因之一。我很高兴你也意识到这种深入理解的重要性,并希望你能从这本书中学到很多。
致谢
写一本书比我想象的要难得多,这需要大量的工作——真的!如果没有我父母 Krishnan 和 Lakshmi Thampi、我的妻子 Shruti Menon 和我的兄弟 Arun Thampi 的支持和理解,这一切都不可能实现。我的父母让我走上了终身学习的道路,并始终给予我追逐梦想的力量。我也永远感激我的妻子,她在我写这本书的艰难旅程中一直支持我,耐心地倾听我的想法,审阅我的草稿,并相信我能够完成它。我的兄弟也值得我衷心的感谢,因为他总是支持我!
接下来,我想感谢 Manning 团队:Brian Sawyer,他阅读了我的博客文章,并建议这可能是一本好书;我的编辑 Matthew Spaur、Lesley Trites 和 Kostas Passadis,因为他们与我合作,提供高质量的反馈,并在事情变得艰难时保持耐心;以及 Marjan Bace,因为他批准了这个整个项目。还要感谢所有其他与我在这本书的生产和推广工作中合作的 Manning 团队成员:Deirdre Hiam,我的生产编辑;Pamela Hunt,我的校对编辑;以及 Melody Dolab,我的页面校对员。
我还想感谢那些在书的发展过程中花时间阅读我的手稿并在各个阶段提供宝贵反馈的审稿人:Al Rahimi、Alain Couniot、Alejandro Bellogin Kouki、Ariel Gamiño、Craig E. Pfeifer、Djordje Vukelic、Domingo Salazar、Dr. Kanishka Tyagi、Izhar Haq、James J. Byleckie、Jonathan Wood、Kai Gellien、Kim Falk Jorgensen、Marc Paradis、Oliver Korten、Pablo Roccatagliata、Patrick Goetz、Patrick Regan、Raymond Cheung、Richard Vaughan、Sergio Govoni、Shashank Polasa Venkata、Sriram Macharla、Stefano Ongarello、Teresa Fontanella De Santis、Tiklu Ganguly、Vidhya Vinay、Vijayant Singh、Vishwesh Ravi Shrimali 和 Vittal Damaraju。特别感谢 James Byleckie 和 Vishwesh Ravi Shrimali,技术校对员,在书进入生产前仔细审查了代码。
关于这本书
可解释人工智能旨在帮助您实现复杂机器学习模型的最先进可解释技术,并构建公平可解释的人工智能系统。可解释性是研究的热点话题,但只有少数资源和实用指南涵盖了所有对现实世界从业者有价值的重要技术。本书旨在填补这一空白。
应该阅读这本书的人
可解释人工智能是为那些对深入了解模型工作原理以及如何构建公平无偏模型感兴趣的数据科学家和工程师而设计的。本书对希望了解驱动人工智能系统的模型以确保公平性并保护企业用户和品牌的企业架构师和业务利益相关者也应有帮助。
本书是如何组织的:一个路线图
本书分为四个部分,共涵盖九章内容。
第一部分带您进入可解释人工智能的世界:
-
第一章介绍了不同类型的 AI 系统,定义了可解释性及其重要性,讨论了白盒和黑盒模型,并解释了如何构建可解释人工智能系统。
-
第二章介绍了白盒模型及其解释方法,具体关注线性回归、决策树和广义加性模型(GAMs)。
第二部分专注于黑盒模型,并理解模型如何处理输入并得出最终预测:
-
第三章介绍了一类称为树集成(tree ensembles)的黑盒模型,以及如何使用全局范围内的后验模型无关方法来解释它们,例如部分依赖图(PDPs)和特征交互图。
-
第四章介绍了深度神经网络及其解释方法,使用局部范围内的后验模型无关方法来解释它们,例如局部可解释模型无关解释(LIME)、SHapley 加性解释(SHAP)和锚点。
-
第五章介绍了卷积神经网络,以及如何使用显著性图来可视化模型关注的重点,具体关注的技术包括梯度、引导反向传播、梯度加权类激活映射(Grad-CAM)、引导 Grad-CAM 和光滑梯度(SmoothGrad)。
第三部分继续关注黑盒模型,但转向理解它们学习到的特征或表示:
-
第六章介绍了卷积神经网络,以及如何剖析它们以理解神经网络中间或隐藏层学习到的数据表示。
-
第七章介绍了语言模型,以及如何使用主成分分析(PCA)和 t 分布随机邻域嵌入(t-SNE)等技术可视化它们学习到的高维表示。
第四部分专注于公平性和偏差,为可解释人工智能铺平道路:
-
第八章涵盖了公平的多种定义以及检查模型是否存在偏差的方法。它还讨论了减轻偏差的技术以及使用数据表来标准化记录数据集的方法,这将有助于提高与 AI 系统的利益相关者和用户的透明度和问责制。
-
第九章通过理解如何构建这样的系统为可解释人工智能铺平道路,同时也涵盖了使用反事实示例的对比解释。
关于代码
这本书包含了许多源代码示例。在大多数情况下,源代码以fixed-width font like this这样的固定宽度字体格式化,以将其与普通文本区分开来。
在许多情况下,原始源代码已被重新格式化;我们已添加换行符并重新处理缩进来适应书中的可用页面空间。在极少数情况下,即使这样也不够,列表中还包括了行续续标记(➥)。此外,当代码在文本中描述时,源代码中的注释通常已从列表中删除。代码注释伴随着许多列表,突出显示重要概念。
您可以从这本书的在线版本 liveBook 中获取可执行的代码片段,网址为livebook.manning.com/book/interpretable-ai。书中示例的完整代码可在 Manning 网站www.manning.com/books/interpretable-ai和 GitHubmng.bz/KBdZ上下载。
liveBook 讨论论坛
购买《可解释人工智能》包括免费访问 Manning 的在线阅读平台 liveBook。使用 liveBook 的独特讨论功能,您可以在全球范围内或针对特定章节或段落附加评论。为自己做笔记、提问和回答技术问题以及从作者和其他用户那里获得帮助都非常简单。要访问论坛,请访问livebook.manning.com/book/interpretable-ai/discussion。您还可以在livebook.manning.com/discussion了解更多关于 Manning 论坛和行为准则的信息。
曼宁对读者的承诺是提供一个场所,让读者之间以及读者与作者之间可以进行有意义的对话。这并不是对作者参与特定数量活动的承诺,作者对论坛的贡献仍然是自愿的(且未付费)。我们建议您尝试向作者提出一些挑战性的问题,以免他的兴趣转移!只要这本书有售,论坛和以前讨论的存档将可通过出版社的网站访问。
关于作者

阿贾伊·桑皮在机器学习方面有着坚实的背景。他的博士研究专注于信号处理和机器学习。他在强化学习、凸优化以及将经典机器学习技术应用于 5G 蜂窝网络等主题的领先会议和期刊上发表了论文。阿贾伊目前是一家大型科技公司的高级机器学习工程师,主要专注于负责任的 AI 和公平性。在过去,阿贾伊曾在微软担任首席数据科学家,负责为多个行业如制造业、零售业和金融业等客户部署复杂的 AI 解决方案。
关于封面插图
《可解释人工智能》封面上的图像是“Mourcy 的橙商”,或“橙商”,取自雅克·格拉塞·德·圣索沃尔的收藏,该收藏于 1797 年出版。每一幅插图都是手工精细绘制和着色的。
在那些日子里,仅凭人们的服饰就能轻易地识别出他们居住的地方以及他们的职业或社会地位。曼宁通过基于几个世纪前丰富多样的地域文化的书封面,庆祝计算机行业的创新精神和主动性,这些文化通过如这一系列图片的图片被重新带回生活。
第一部分:可解释性基础
本部分将带你进入可解释人工智能的世界。在第一章中,你将了解不同类型的 AI 系统、可解释性及其重要性、白盒模型和黑盒模型,以及如何构建可解释的人工智能系统。
在第二章中,你将了解使白盒模型天生透明和黑盒模型天生不透明的特性。你将学习如何解释简单的白盒模型,例如线性回归和决策树,然后转换方向,专注于广义加性模型(GAMs)。你还将了解赋予 GAMs 高预测力的特性以及如何解释它们。GAMs 具有非常高的预测力,并且也非常易于解释,因此使用 GAMs 可以获得更多的价值。
1 引言
本章涵盖
-
不同的机器学习系统类型
-
机器学习系统的构建方式
-
可解释性的定义及其重要性
-
如何构建可解释的机器学习系统
-
本书涵盖的可解释性技术总结
欢迎来到这本书!我非常高兴您开始这段探索可解释人工智能世界的旅程,并期待成为您的向导。仅在过去的五年里,我们就见证了人工智能(AI)领域的重大突破,尤其是在图像识别、自然语言理解和围棋等棋类游戏等领域。随着人工智能在医疗保健和金融等行业增强关键的人类决策,构建稳健且无偏见的机器学习模型来驱动这些人工智能系统变得越来越重要。在这本书中,我希望为您提供关于可解释人工智能系统和如何构建它们的实用指南。通过一个具体的例子,本章将解释为什么可解释性很重要,并为本书的其余部分奠定基础。
1.1 Diagnostics+ AI——一个示例人工智能系统
现在,让我们看看一个名为 Diagnostics+ 的医疗中心的具体例子,该中心提供诊断不同类型疾病的服务。为 Diagnostics+ 工作的医生分析血涂片样本并提供他们的诊断,这些诊断可以是阳性或阴性。Diagnostics+ 的当前状态如图 1.1 所示。

图 1.1 Diagnostics+ 当前状态
当前状态的问题在于医生正在手动分析血涂片样本。由于资源有限,因此诊断需要相当长的时间。Diagnostics+ 希望使用人工智能自动化此过程,并诊断更多的血样,以便患者能够更快地获得正确的治疗。这种未来状态在图 1.2 中展示。

图 1.2 Diagnostics+ 的未来状态
Diagnostics+ AI 的目标是使用血涂片样本的图像以及其他患者元数据来提供诊断——阳性、阴性或中性,并带有置信度度量。Diagnostics+ 还希望医生参与审查诊断,特别是更困难的案例,从而使人工智能系统能够从错误中学习。
1.2 机器学习系统类型
我们可以使用三类机器学习系统来驱动 Diagnostics+ AI:监督学习、无监督学习和强化学习。
1.2.1 数据表示
让我们先看看如何表示机器学习系统可以理解的数据。对于 Diagnostics+,我们知道有血涂片样本的历史数据,以图像和患者元数据的形式存在。
我们如何最好地表示图像数据?这如图 1.3 所示。假设血涂片样本的图像是一个 256 × 256 像素的彩色图像,由三个主通道组成:红色(R)、绿色(G)和蓝色(B)。我们可以将这个 RGB 图像以数学形式表示为三个像素值矩阵,每个通道一个,每个大小为 256 × 256。这三个二维矩阵可以组合成一个 256 × 256 × 3 的大小多维矩阵来表示 RGB 图像。一般来说,表示图像的矩阵维度具有以下形式:{垂直像素数} × {水平像素数} × {通道数}。

图 1.3 血涂片样本图像表示
现在,我们如何最好地表示患者元数据?假设元数据包括患者标识符(ID)、年龄、性别和最终诊断等信息。元数据可以表示为结构化表格,如图 1.4 所示,有 N 列和 M 行。我们可以轻松地将元数据的表格表示转换为 M × N 维度的矩阵。在图 1.4 中,你可以看到,患者 ID、性别和诊断列是分类的,必须编码为整数。例如,患者 ID “AAABBCC” 编码为整数 0,性别 “M”(男性)编码为整数 0,诊断 “Positive” 编码为整数 1。

图 1.4 表格形式的患者元数据表示
1.2.2 监督学习
监督学习的目标是根据示例输入输出对学习从输入到输出的映射。它需要标记的训练数据,其中输入(也称为特征)有一个相应的标签(也称为目标)。这些数据是如何表示的?输入特征通常使用多维数组数据结构表示,或者从数学上表示为矩阵 X。输出或目标表示为单维数组数据结构,或者从数学上表示为向量 y。矩阵 X 的维度通常是 m × n,其中 m 代表示例或标记数据的数量,n 代表特征的数量。向量 y 的维度通常是 m × 1,其中 m 再次代表示例或标签的数量。目标是学习一个函数 f,它将输入特征 X 映射到目标 y。这如图 1.5 所示。

图 1.5 监督学习说明
在图 1.5 中,你可以看到,通过监督学习,你正在学习一个函数 f,它接受多个以 X 表示的输入特征,并提供一个与已知标签或值匹配的输出,表示为目标变量 y。图的下半部分显示了一个示例,其中提供了一个标记数据集,通过监督学习,你正在学习如何将输入特征映射到输出。
函数 f 是一个多元函数——它将多个输入变量或特征映射到一个目标。以下是无监督学习问题的两个广泛类别:
-
回归—目标向量 y 是连续的。例如,预测美国某地点的房价(以美元计)是一个回归类型的学习问题。
-
分类—目标变量 y 是离散且有界的。例如,预测一封电子邮件是否为垃圾邮件是一个分类学习问题。
1.2.3 无监督学习
在无监督学习中,目标是学习数据的最佳表示,以最好地描述它。没有标记的数据,目标是学习从原始数据中的一些未知模式。输入特征表示为矩阵 X,系统学习一个函数 f,它将 X 映射到输入数据的模式或表示。这如图 1.6 所示。无监督学习的一个例子是聚类,其目标是形成具有相似属性或特征的数据点组或聚类。这显示在图的下半部分。未标记的数据包含两个特征,数据点在二维空间中显示。没有已知的标签,无监督学习系统的目标是学习数据中存在的潜在模式。在这个示例中,系统学习如何根据数据点之间的接近度或相似性将原始数据点映射到聚类中。这些聚类事先是未知的,因为数据集未标记,因此学习是完全无监督的。

图 1.6 无监督学习示意图
1.2.4 强化学习
强化学习由一个通过与环境交互来学习的代理组成,如图 1.7 所示。学习代理在环境中采取一个动作,并根据动作的质量获得奖励或惩罚。根据采取的动作,代理从一个状态移动到另一个状态。代理的整体目标是通过对从输入状态到动作映射的策略函数 f 进行学习,以最大化累积奖励。强化学习的例子包括机器人吸尘器学习最佳清洁路径以清洁家庭,以及人工代理学习如何玩象棋和国际象棋等棋类游戏。

图 1.7 强化学习示意图
图 1.7 的下半部分展示了强化学习系统。该系统由一个机器人(代理)在一个迷宫(环境)中组成。学习代理的目标是确定最佳的动作集,以便它能从当前位置移动到终点线(最终状态),终点线由绿色星号指示。代理可以采取四种动作之一:向左、向右、向上或向下。
1.2.5 用于诊断+ AI 的机器学习系统
既然你已经知道了三种主要的机器学习系统类型,那么对于诊断+ AI,哪种系统最适用?鉴于数据集已标记,并且你可以从历史数据中知道对患者的诊断以及血液样本的诊断,可以用来驱动诊断+ AI 的机器学习系统是 监督学习。
这是哪种监督学习问题?监督学习问题的目标是诊断,可以是阳性也可以是阴性。因为目标是离散且有界的,所以这是一个 分类 类型的学习问题。
本书的主要关注点
本书主要关注存在标记数据的 监督学习 系统。我将教你如何实现回归和分类类型问题的可解释性技术。尽管本书没有明确涵盖无监督学习或强化学习系统,但本书中学到的技术可以扩展到它们。
1.3 构建诊断+ AI
既然我们已经确定诊断+ AI 将是一个监督学习系统,我们如何着手构建它?典型的过程包括三个主要阶段:
-
学习
-
测试
-
部署
在学习阶段,如图 1.8 所示,我们处于开发环境,在那里我们使用数据的两个子集,称为训练集和开发集。正如其名所示,训练集用于训练机器学习模型,学习从输入特征 X(在这种情况下,血液样本的图像和元数据)到目标 y(在这种情况下,诊断)的映射函数 f。一旦我们训练了模型,我们就使用开发集进行验证,并根据该开发集上的性能调整模型。调整模型包括确定模型的最佳参数,称为 超参数,以获得最佳性能。这是一个相当迭代的过程,我们继续这样做,直到模型达到可接受的性能水平。

图 1.8 构建 AI 系统流程——学习阶段
在测试阶段,如图 1.9 所示,我们现在切换到测试环境,在那里我们使用数据的一个子集,称为 测试集,它与训练集不同。目标是获得模型准确性的无偏评估。利益相关者和专家(在这种情况下,医生)将在此阶段评估系统的功能以及模型在测试集上的性能。这种额外的测试,称为用户验收测试(UAT),是任何软件系统开发的最终阶段。如果性能不可接受,那么我们就回到第 1 阶段去训练一个更好的模型。如果性能可接受,那么我们就进入第 3 阶段,即部署。

图 1.9 构建 AI 系统流程——测试阶段
最后,在部署阶段,我们现在将学习到的模型部署到生产系统中,模型现在将暴露于它之前未见过的新的数据。整个过程如图 1.10 所示。在 Diagnostics+ AI 的情况下,这些数据将是模型将用于预测诊断结果为阳性或阴性的新血液样本和患者信息,并附带置信度度量。然后,这些信息被专家(医生)消费,进而被最终用户(患者)消费。

图 1.10 构建 AI 系统的过程——完整
1.4 Diagnostics+ AI 的差距
图 1.10 显示了 Diagnostics+ AI 系统的一些主要差距。这个 AI 系统无法防止一些常见问题,这些问题会导致部署的模型在生产环境中表现不符合预期。这些问题可能会对诊断中心的业务产生有害影响。常见问题如下:
-
数据泄露
-
偏差
-
监管不合规
-
概念漂移
1.4.1 数据泄露
当训练、开发和测试集中的特征无意中泄露了在生产环境中模型评分新数据时不会出现的信息时,就会发生数据泄露。对于 Diagnostics+,假设我们使用医生关于诊断的笔记作为模型的特征或输入。在用测试集评估模型时,我们可能会得到夸大的性能结果,从而欺骗自己认为我们已经构建了一个优秀的模型。医生的笔记可能包含关于最终诊断的信息,这将泄露关于目标变量的信息。如果这个问题没有及早被发现,一旦模型部署到生产环境中,可能会造成灾难性的后果——模型在医生有机会审查诊断并添加笔记之前就已经进行了评分。因此,模型要么在生产中崩溃(因为缺少特征),要么开始做出糟糕的诊断。
数据泄露的一个经典案例研究是 2008 年的 KDD Cup 挑战赛(www.kdd.org/kdd-cup/view/kdd-cup-2008)。这个基于真实数据的机器学习竞赛的目标是根据 X 光图像检测乳腺癌细胞是良性还是恶性。一项研究(kdd.org/exploration_files/KDDCup08-P1.pdf)表明,在这次竞赛的测试集中得分最高的团队使用了一个名为“患者 ID”的特征,这是医院为患者生成的标识符。结果发现,一些医院使用患者 ID 来表示患者在入院时的病情严重程度,因此泄露了关于目标变量的信息。
1.4.2 偏差
偏差是指机器学习模型做出不公平的预测,这种预测偏向于某个人或群体,而损害了另一个人或群体。这种不公平的预测可能是由于数据或模型本身造成的。可能存在采样偏差,即用于训练的数据样本与总体之间存在系统性差异。模型所捕捉到的系统性社会偏差也可能存在于数据中。训练好的模型也可能存在缺陷——即使有相反的证据,它也可能有一些强烈的先入之见。对于 Diagnostics+ AI 的情况,如果存在采样偏差,例如,模型可能对某一群体做出更准确的预测,但不能很好地推广到整个总体。这远远不是理想的,因为诊断中心希望新的 AI 系统能够为所有患者使用,无论他们属于哪个群体。
机器偏差的一个经典案例研究是美国法院使用的 COMPAS AI 系统,用于预测未来的罪犯。这项研究由 ProPublica 进行。(网页包含分析数据和数据集的链接。)ProPublica 获得了 2013 年和 2014 年在佛罗里达州一个县被捕的 7,000 人的 COMPAS 评分。使用这些评分,他们发现无法准确预测再犯率(即被定罪的人再次犯罪的比例)——预测会犯暴力犯罪的人中只有 20%实际上犯了罪。更重要的是,他们发现了模型中严重的种族偏见。
1.4.3 规章制度不遵守
《通用数据保护条例》(GDPR;[gdpr.eu/](https://gdpr.eu/))是欧洲议会于 2016 年通过的一套全面法规,涉及外国公司如何收集、存储和处理数据。该法规包含第 17 条([https://gdpr-info.eu/art-17-gdpr/](https://gdpr-info.eu/art-17-gdpr/))——“被遗忘权”,个人可以要求收集其数据的公司删除其所有个人数据。该法规还包含第 22 条([https://gdpr-info.eu/art-22-gdpr/](https://gdpr-info.eu/art-22-gdpr/)),个人可以挑战基于其个人数据做出的算法或 AI 系统的决策。该法规强调了提供解释或说明为什么算法做出了特定决策的需要。当前的 Diagnostics+ AI 系统不符合这两套法规。在这本书中,我们更关注第 22 条,因为关于如何遵守第 17 条有很多在线资源。
1.4.4 概念漂移
当生产环境中的数据属性或分布与用于训练和评估模型的原始数据相比发生变化时,就会发生概念漂移。对于 Diagnostics+ AI 来说,如果出现新的患者或疾病档案,而这些档案没有包含在原始数据中,就可能会发生这种情况。当发生概念漂移时,我们会观察到机器学习模型在生产环境中的性能随时间下降。当前的 Diagnostics+ AI 系统并没有妥善处理概念漂移问题。
1.5 构建鲁棒 Diagnostics+ AI 系统
我们如何解决 1.4 节中提到的所有差距,并构建一个鲁棒的 Diagnostics+ AI 系统?我们需要调整这个过程。首先,如图 1.11 所示,我们在测试阶段之后、部署之前添加一个模型理解阶段。

图 1.11 构建鲁棒 AI 系统的过程——理解阶段
这个新的 理解 阶段的目的在于回答重要的“如何”问题——模型是如何对一个特定的血液样本给出阳性诊断的?这涉及到解释模型的重要特征以及它们是如何相互作用的,解释模型学习到的模式,理解盲点,检查数据中的偏差,并确保这些偏差不会被模型传播。这个理解阶段应确保 AI 系统免受 1.4.1 节和 1.4.2 节中提到的数据泄露和偏差问题的影响。
第二个变化是在部署之后添加一个 解释 阶段,如图 1.12 所示。解释阶段的目的在于解释模型是如何在生产环境中对新的数据进行预测的。对新数据的预测解释使我们能够(如果需要的话)将信息暴露给挑战部署模型决策的系统专家用户。另一个目的是提供一个可读的解释,以便它可以被暴露给更广泛的 AI 系统最终用户。通过包括解释步骤,我们将能够解决 1.4.3 节中提到的监管不合规问题。

图 1.12 构建鲁棒 AI 的过程——解释阶段
最后,为了解决 1.4.4 节中提到的概念漂移问题,我们需要在生产环境中添加一个 监控 阶段。这个完整的过程如图 1.13 所示。监控阶段的目的在于跟踪生产环境中数据的分布以及部署模型的性能。如果数据分布或模型性能出现任何变化,我们需要回到学习阶段,并将生产环境中的新数据纳入以重新训练模型。
本书的主要关注点
本书主要关注理解和解释阶段中的 解释 步骤。我打算教你各种可解释性技术,你可以应用这些技术来回答重要的“如何”问题,并解决数据泄露、偏差和监管不合规问题。尽管可解释性和监控是过程中的重要步骤,但它们不是本书的主要焦点。区分可解释性和可解释性也很重要。这将在以下部分中讨论。

图 1.13 构建稳健 AI 系统的过程——完整
1.6 可解释性 vs. 可解释性
可解释性 和 可解释性 有时被互换使用,但区分这两个术语很重要。
可解释性 主要是关于理解 AI 系统内部的因果关系。这是我们在给定输入的情况下,可以持续估计模型将预测什么,理解模型是如何得出预测的,理解预测如何随着输入或算法参数的变化而变化,以及最后理解模型何时犯错的程度。可解释性主要是由构建、部署或使用 AI 系统的专家可辨别的,这些技术是帮助我们达到可解释性的基石。
另一方面,可解释性 超出了可解释性的范畴,因为它帮助我们以人类可读的形式理解模型是如何以及为什么做出预测的。它用人类术语解释系统的内部机制,目的是达到更广泛的受众。可解释性需要可解释性作为基石,并参考其他领域和领域,如人机交互(HCI)、法律和伦理。在本书中,我将更多地关注可解释性,而不是可解释性。在可解释性本身就有很多要涵盖的内容,但它应该为你提供一个坚实的基础,以便你能够后来构建一个可解释的 AI 系统。
当你考虑可解释性时,你应该意识到四种不同的角色。他们是构建 AI 系统的 数据科学家 或 工程师,希望为他们的业务部署 AI 系统的 商业利益相关者,AI 系统的 最终用户,以及最后是监控或审计 AI 系统健康状况的 专家 或 监管者。请注意,可解释性对这四个角色意味着不同的事情,如下所述:
-
对于一个 数据科学家 或 工程师 来说,这意味着更深入地理解模型如何做出特定的预测,哪些特征是重要的,以及如何通过分析模型表现不佳的案例来调试问题。这种理解有助于数据科学家构建更稳健的模型。
-
对于 商业利益相关者 来说,这意味着理解模型如何做出决策,以确保公平性并保护企业的用户和品牌。
-
对于最终用户来说,这意味着理解模型是如何做出决策的,并在模型出错时允许进行有意义的挑战。
-
对于专家或监管者来说,这意味着审计模型和 AI 系统,并追踪决策路径,尤其是在事情出错的时候。
1.6.1 可解释性技术类型
图 1.14 总结了各种可解释性技术的类型。内禀可解释性技术是与结构简单的机器学习模型相关的,也称为白盒模型。白盒模型本质上是透明的,解释模型的内部结构是直接的。对于这类模型,可解释性是直接呈现的。“后验”可解释性技术通常在模型训练之后应用,用于解释和理解模型预测中某些输入的重要性。后验可解释性技术适用于白盒和黑盒模型,即那些本质上不透明的模型。

图 1.14 可解释性技术类型
可解释性技术也可以是模型特定的或模型无关的。模型特定的可解释性技术,正如其名所示,只能应用于某些类型的模型。内禀可解释性技术本质上就是模型特定的,因为这种技术是与所使用的模型的特定结构紧密相连的。然而,模型无关的可解释性技术并不依赖于所使用的特定模型类型。由于它们独立于模型的内部结构,因此可以应用于任何模型。后验可解释性技术本质上大多是模型无关的。
可解释性技术也可以在范围上是局部的或全局的。局部可解释性技术旨在对特定实例或示例的模型预测有更好的理解。另一方面,全局可解释性技术旨在对模型整体有更好的理解——输入特征对模型预测的总体影响。我们在本书中涵盖了所有这些类型的技巧。现在让我们看看你将具体学到什么。
1.7 我在这本书中将学到什么?
图 1.15 展示了本书中将学习到的所有可解释性技术地图。在解释监督学习模型时,区分白盒模型和黑盒模型非常重要。白盒模型的例子包括线性回归、逻辑回归、决策树和广义加性模型(GAMs)。黑盒模型的例子包括树集成,如随机森林和提升树,以及神经网络。白盒模型比黑盒模型更容易解释。另一方面,黑盒模型的预测能力比白盒模型高得多。因此,我们需要在预测能力和可解释性之间做出权衡。了解我们可以应用白盒模型和黑盒模型的场景非常重要。
在第二章中,你将了解使白盒模型本质上是透明的和黑盒模型本质上是模糊的特性。你将学习如何解释简单的白盒模型,例如线性回归和决策树,然后我们将转换方向,专注于广义加性模型(GAMs)。GAMs 具有高度的预测能力,并且也是高度可解释的,因此它们比 GAMs 提供了更多的价值。你将了解赋予 GAMs 力量的属性以及如何解释它们。在撰写本文时,关于 GAMs 的实用资源不多,难以对模型内部结构和如何解释它们有一个好的理解。为了填补这一空白,我们在第二章中投入了大量关注 GAMs。其余章节则专注于黑盒模型。
我们可以通过两种方式来解释黑盒模型。一种方式是解释模型处理过程,即理解模型如何处理输入并得出最终预测。第三章到第五章专注于解释模型处理过程。另一种方式是解释模型表示,这仅适用于深度神经网络。第六章和第七章专注于通过解释模型表示来理解神经网络学习到的特征或模式。目标是理解神经网络学习到的特征或模式。

图 1.15 本书涵盖的可解释性技术地图
在第三章中,我们关注一类称为树集成(tree ensembles)的黑盒模型。你将了解它们的特征以及它们为什么被称为“黑盒”。你还将学习如何使用全局范围内的模型无关的后验方法来解释它们。我们将特别关注局部依赖性图(PDPs)、个体条件期望(ICE)图和特征交互图。
在第四章中,我们关注深度神经网络,特别是纯全连接神经网络。你将了解使这些模型成为黑盒的特性,以及如何使用局部范围内的模型无关的后验方法来解释它们。你将特别学习到局部可解释模型无关解释(LIME)、SHapley Additive exPlanations(SHAP)和锚点等技术。
在第五章中,我们专注于卷积神经网络,这是一种主要用于视觉任务(如图像分类和目标检测)的更高级的架构形式。你将学习如何使用显著性图来可视化模型关注的对象。你还将学习梯度、指导反向传播(简称 backprop)、梯度加权类激活映射(grad-CAM)、指导 grad-CAM 和光滑梯度(SmoothGrad)等技术。
在第六章和第七章中,我们专注于卷积神经网络和用于语言理解的神经网络。你将学习如何剖析神经网络,并理解神经网络中中间或隐藏层学习到的数据表示。你还将学习如何使用主成分分析(PCA)和 t 分布随机邻域嵌入(t-SNE)等技术来可视化模型学习到的高维表示。
本书以构建公平且无偏见的模型以及学习构建可解释人工智能系统所需的知识点结束。在第八章中,你将了解公平性的各种定义以及如何检查你的模型是否存在偏见。你还将学习使用中和技术来减轻偏见的技术。我们讨论了使用数据表来标准化记录数据集的方法,这有助于提高与系统利益相关者和用户的透明度和问责制。在第九章中,我们通过教授如何构建这样的系统为可解释人工智能铺平道路,你还将了解使用反事实示例进行对比解释。到本书结束时,你将拥有各种可解释性技术在你的工具箱中。当涉及到模型理解时,遗憾的是没有一劳永逸的解决方案。没有一种可解释性技术适用于所有场景。因此,你需要通过应用多种可解释性技术,从几个不同的角度来审视模型。在这本书中,我将帮助你识别适合特定场景的正确工具。
1.7.1 我在这本书中将使用哪些工具?
在本书中,我们将使用 Python 编程语言实现模型和可解释性技术。我选择 Python 的主要原因是因为大多数最先进的可解释性技术都是在这个语言中创建和积极开发的。图 1.16 展示了本书中使用的工具概览。对于数据表示,我们将使用 Python 数据结构和常见的数据科学库,如 Pandas 和 NumPy。为了实现白盒模型,我们将使用 Scikit-Learn 库进行简单的线性回归和决策树,以及 pyGAM 用于 GAM。对于黑盒模型,我们将使用 Scikit-Learn 进行树集成,以及 PyTorch 或 TensorFlow 进行神经网络。对于用于理解模型处理的可解释性技术,我们将使用 Matplotlib 库进行可视化,以及实现 PDP、LIME、SHAP、anchors、gradients、guided backprop、grad-CAM 和 SmoothGrad 等技术的开源库。为了解释模型表示,我们将使用实现 NetDissect 和 tSNE 的工具,并使用 Matplotlib 库进行可视化。最后,为了减轻偏差,我们将使用 PyTorch 和 TensorFlow 实现偏差中和技术,以及 GANs 进行对抗性去偏差。

图 1.16 本书使用的工具概览
1.7.2 在阅读本书之前我需要了解什么?
本书主要面向有 Python 编程经验的科学家和工程师。对常见 Python 数据科学库(如 NumPy、Pandas、Matplotlib 和 Scikit-Learn)的基本了解会有所帮助,尽管这不是必需的。本书将向您展示如何使用这些库加载数据和表示数据,但不会深入探讨它们,因为这超出了本书的范围。
读者必须熟悉线性代数,特别是向量和矩阵,以及它们的运算,如点积、矩阵乘法、转置和求逆。读者还必须具备概率论和统计学的基础知识,特别是关于随机变量、基本的离散和连续概率分布、条件概率和贝叶斯定理的主题。还期望读者具备微积分的基本知识,特别是单变量和多变量函数及其导数(梯度)和偏导数。尽管本书不会过多关注模型可解释性背后的数学,但期望对构建机器学习模型感兴趣的数据科学家和工程师具备这些基本数学基础。
对机器学习的基本知识或实际训练机器学习模型的实践经验是一个加分项,尽管这不是硬性要求。本书不会深入探讨机器学习,因为许多资源和书籍已经很好地涵盖了这一主题。然而,本书将为你提供特定机器学习模型的基本理解,并展示如何训练和评估它们。主要关注与可解释性相关的理论,以及你如何在训练后实施解释模型的技术。
摘要
-
存在三种广泛的机器学习系统类型:监督学习、无监督学习和强化学习。本书专注于监督学习系统的可解释性技术,这些系统包括回归和分类类型的问题。
-
在构建 AI 系统时,将可解释性、模型理解和监控添加到流程中非常重要。如果不这样做,你可能会遇到灾难性的后果,如数据泄露、偏差、概念漂移和普遍缺乏信任。此外,随着 GDPR 的实施,我们在 AI 流程中包含可解释性有法律上的原因。
-
理解可解释性和可解释性之间的区别很重要。
-
可解释性是我们能够一致地估计模型将预测什么、理解模型如何得出预测以及理解模型何时犯错的程度。可解释性技术是帮助你达到可解释性的基石。
-
可解释性超越了可解释性,因为它帮助我们以人类可读的形式理解模型是如何以及为什么得出预测的。它利用可解释性技术,并参考其他领域和领域,如人机交互(HCI)、法律和伦理。
-
你需要意识到不同的人在使用或构建 AI 系统时的不同角色,因为可解释性对不同的人意味着不同的事情。
-
可解释性技术可以是内在的或事后确定的,模型特定的或模型无关的,局部的或全局的。
-
本质上透明的模型被称为白盒模型,而本质上不透明的模型被称为黑盒模型。白盒模型更容易解释,但通常比黑盒模型的预测能力低。
-
黑盒模型提供了两种广泛的可解释性技术:一种专注于解释模型处理,另一种专注于解释模型学习到的表示。
2 白盒模型
本章涵盖
-
使白盒模型天生透明和可解释的特征
-
如何解释简单的白盒模型,如线性回归和决策树
-
广义加性模型(GAMs)是什么以及赋予它们高预测能力和高可解释性的特性
-
如何实现和解释 GAMs
-
黑盒模型是什么以及使它们天生不透明的特征
要构建一个可解释的人工智能系统,我们必须了解我们可以用来驱动人工智能系统的不同类型模型以及我们可以应用来解释它们的技巧。在本章中,我将介绍三种关键的白盒模型——线性回归、决策树和广义加性模型(GAMs),它们天生透明。您将学习它们如何实现、何时可以应用以及如何解释。我还简要介绍了黑盒模型。您将学习它们何时可以应用以及使它们难以解释的特征。本章重点在于解释白盒模型,而本书的其余部分将致力于解释复杂的黑盒模型。
在第一章中,您学习了如何构建一个健壮、可解释的人工智能系统。该过程在图 2.1 中再次展示。第二章以及本书的其余部分的主要重点将在于实现可解释性技巧,以更好地理解涵盖白盒和黑盒模型的人工智能模型。相关的模块在图 2.1 中被突出显示。我们将在模型开发和测试期间应用这些可解释性技巧。我们还将了解模型训练和测试,特别是实现方面。由于模型学习、测试和理解阶段相当迭代,因此同时涵盖这三个阶段很重要。对于已经熟悉模型训练和测试的读者,可以自由跳过那些部分,直接进入可解释性部分。
在生产中应用可解释性技巧时,我们还需要考虑构建一个生成解释的系统,为您的系统最终用户提供人类可读的解释。然而,可解释性超出了本书的范围,本书将专注于模型开发和测试期间的可解释性。

图 2.1 构建健壮人工智能系统的过程,主要关注解释
2.1 白盒模型
白盒模型天生透明,使它们透明的特征是
-
用于机器学习的算法易于理解,我们可以清楚地解释输入特征是如何转换为输出或目标变量的。
-
我们可以识别出预测目标变量最重要的特征,并且这些特征是可理解的。
白盒模型的例子包括线性回归、逻辑回归、决策树和广义加性模型(GAMs)。表 2.1 显示了这些模型可以应用到的机器学习任务。
表 2.1 白盒模型到机器学习任务的映射
| 白盒模型 | 机器学习任务(s) |
|---|---|
| 线性回归 | 回归 |
| 逻辑回归 | 分类 |
| 决策树 | 回归和分类 |
| 广义加性模型(GAMs) | 回归和分类 |
在本章中,我们重点关注线性回归、决策树和广义加性模型(GAMs)。在图 2.2 中,我将这些技术绘制在了一个二维平面上,其中可解释性在x轴上,预测能力在y轴上。当你从左到右移动这个平面时,模型从低可解释性状态过渡到高可解释性状态。当你从底部向上移动这个平面时,模型从低预测能力状态过渡到高预测能力状态。线性回归和决策树高度可解释,但预测能力较低到中等。另一方面,广义加性模型(GAMs)具有高预测能力,并且也是高度可解释的。该图还以灰色和斜体显示了黑盒模型。我们将在第 2.6 节中介绍这些。

图 2.2 可解释性对预测能力平面的白盒模型
我们首先从解释更简单的线性回归和决策树模型开始,然后深入到广义加性模型(GAMs)的世界。对于这些白盒模型中的每一个,我们学习算法是如何工作的以及使它们本质上可解释的特征。对于白盒模型,理解算法的细节非常重要,因为它将帮助我们解释输入特征是如何转换为最终模型输出或预测的。它还将帮助我们量化每个输入特征的重要性。你将首先学习如何在 Python 中训练和评估本书中的所有模型,然后再深入研究可解释性。如前所述,由于模型学习、测试和理解阶段是迭代的,因此这三个阶段一起考虑非常重要。
2.2 诊断+—糖尿病进展
让我们通过一个具体的例子来探讨白盒模型。回忆一下第一章中的 Diagnostics+ AI 例子。Diagnostics+中心现在希望确定在基线测量后一年内他们的患者的糖尿病进展情况,如图 2.3 所示。中心已经指派你,作为一位新晋数据科学家,为 Diagnostics+ AI 构建一个模型,以预测一年后的糖尿病进展。医生将使用这个预测来确定他们的患者的适当治疗方案。为了赢得医生对模型的信心,不仅要提供准确的预测,还要能够展示模型是如何得出这个预测的。你将如何开始这项任务?

图 2.3 Diagnostics+ AI 用于糖尿病
首先,让我们看看有哪些数据可用。Diagnostics+中心收集了大约 440 名患者的数据,这些数据包括患者的元数据,如年龄、性别、体重指数(BMI)和血压(BP)。还对这些患者进行了血液检查,并收集了以下六个测量值:
-
低密度脂蛋白(坏胆固醇)
-
高密度脂蛋白(好胆固醇)
-
总胆固醇
-
甲状腺刺激激素
-
低眼压性青光眼
-
空腹血糖
数据还包含了所有患者在基线测量后一年内的空腹血糖水平。这是模型的目标。你将如何将这个问题表述为一个机器学习问题?因为提供了标记数据,其中你被给出了 10 个输入特征和一个你必须预测的目标变量,你可以将这个问题表述为一个监督学习问题。目标变量是实值或连续的,因此这是一个回归任务。目标是学习一个函数 f,它将帮助根据输入特征 x 预测目标变量 y。
现在,让我们在 Python 中加载数据,并探索输入特征与彼此以及目标变量的相关性。如果输入特征与目标变量高度相关,那么我们可以使用它们来训练一个模型进行预测。然而,如果它们与目标变量不相关,那么我们需要进一步探索以确定数据中是否存在一些噪声。数据可以在 Python 中如下加载:
from sklearn.datasets import load_diabetes ①
diabetes = load_diabetes() ②
X, y = diabetes[‘data’], diabetes[‘target’] ③
① 导入 scikit-learn 函数以加载公开的糖尿病数据集
② 加载糖尿病数据集
③ 提取特征和目标变量
现在,我们将创建一个 Pandas DataFrame,它是一个包含所有特征和目标变量的二维数据结构。Scikit-Learn 提供的糖尿病数据集包含的特征名称不易理解。六个血液样本测量值分别命名为 s1、s2、s3、s4、s5 和 s6,这使得我们难以理解每个特征测量的是什么。然而,文档提供了这种映射,我们使用它将列重命名为更易理解的形式,如下所示:
feature_rename = {'age': 'Age', ①
'sex': 'Sex', ①
'bmi': 'BMI', ①
'bp': 'BP', ①
's1': 'Total Cholesterol', ①
's2': 'LDL', ①
's3': 'HDL', ①
's4': 'Thyroid', ①
's5': 'Glaucoma', ①
's6': 'Glucose'} ①
df_data = pd.DataFrame(X, ②
columns=diabetes['feature_names']) ③
df_data.rename(columns=feature_rename, inplace=True) ④
df_data['target'] = y ⑤
① 将 Scikit-Learn 提供的特征名称映射到更易读的形式
② 将所有特征(x)加载到 DataFrame 中
③ 使用 Scikit-Learn 特征名称作为列名称
④ 将 Scikit-Learn 的特征名称重命名为更易读的形式
⑤ 将目标变量(y)作为一个单独的列包含
现在,让我们计算列之间的成对相关性,以便我们可以确定每个输入特征与其他输入特征和目标变量的相关性。这可以在 Pandas 中轻松完成,如下所示:
corr = df_data.corr()
默认情况下,pandas 中的corr()函数计算皮尔逊或标准相关系数。这个系数衡量两个变量之间的线性相关性,其值介于+1 和-1 之间。如果系数的绝对值大于 0.7,这意味着它具有非常高的相关性。如果系数的绝对值介于 0.5 和 0.7 之间,则表示中等程度的高相关性。如果系数的绝对值介于 0.3 和 0.5 之间,则表示低相关性,而系数的绝对值小于 0.3 则意味着几乎没有相关性。现在我们可以在 Python 中如下绘制相关矩阵:
import matplotlib.pyplot as plt ①
import seaborn as sns ①
sns.set(style=’whitegrid’) ①
sns.set_palette(‘bright’) ①
f, ax = plt.subplots(figsize=(10, 10)) ②
sns.heatmap( ③
corr, ③
vmin=-1, vmax=1, center=0, ③
cmap="PiYG", ③
square=True, ③
ax=ax ③
) ③
ax.set_xticklabels( ④
ax.get_xticklabels(), ④
rotation=90, ④
horizontalalignment='right' ④
); ④
① 导入 Matplotlib 和 Seaborn 以绘制相关矩阵
② 使用预定义的大小初始化 Matplotlib 图表
③ 使用 Seaborn 绘制相关系数的热图
④ 将 x 轴上的标签旋转 90 度
生成的图表显示在图 2.4 中。让我们首先关注图中的最后一行或最后一列。这显示了每个输入与目标变量之间的相关性。我们可以看到七个特征——BMI、血压、总胆固醇、高密度脂蛋白、甲状腺、青光眼和葡萄糖——与目标变量具有中等程度到高度的相关性。我们还可以观察到良好的胆固醇(HDL)也与糖尿病的进展呈负相关。这意味着 HDL 值越高,患者一年后的空腹血糖水平就越低。这些特征似乎在预测疾病进展方面具有很好的信号,我们可以继续使用它们来训练模型。作为练习,观察每个特征之间是如何相互关联的。例如,总胆固醇似乎与坏胆固醇 LDL 高度相关。当我们开始在第 2.3.1 节中解释线性回归模型时,我们将回到这一点。

图 2.4 糖尿病数据集中特征与目标变量的相关性图
2.3 线性回归
线性回归是您可以训练的最简单的回归任务模型之一。在线性回归中,函数f表示为所有输入特征的线性组合,如图 2.5 所示。已知变量用灰色表示,目标是表示目标变量为输入的线性组合。未知变量是学习算法必须学习的权重。

图 2.5 将疾病进展表示为输入的线性组合
在一般情况下,线性回归的函数f可以用以下数学公式表示,其中n是特征的总数:

线性回归学习算法的目标是确定权重,以准确预测训练集中所有患者的目标变量。我们可以应用以下技术:
-
梯度下降
-
闭合形式解(例如,牛顿方程)
梯度下降法通常被应用,因为它能够很好地扩展到大量特征和训练示例。其基本思想是更新权重,使得预测目标变量与实际目标变量之间的平方误差最小化。
梯度下降算法的目的是在整个训练集的所有示例中,最小化预测目标变量与实际目标变量之间的平方误差或平方差。该算法保证找到最优的权重集,并且因为算法最小化平方误差,所以它被称为基于最小二乘法。可以使用 Python 中的 Scikit-Learn 包轻松训练线性回归模型。下面的代码展示了训练模型的代码。请注意,这里使用的是 Scikit-Learn 提供的开放型糖尿病数据集,并且该数据集已经标准化,所有输入特征都具有零均值和单位方差。特征标准化是在许多机器学习模型(如线性回归、逻辑回归以及基于神经网络的更复杂模型)中广泛使用的预处理形式。它允许驱动这些模型的机器学习算法更快地收敛到最优解:
from sklearn.model_selection import train_test_split ①
from sklearn.linear_model import LinearRegression ②
import numpy as np ③
X_train, X_test, y_train, y_test = train_test_split(X, y, ④
test_size=0.2, ④
random_state=42) ④
lr_model = LinearRegression() ⑤
lr_model.fit(X_train, y_train) ⑥
y_pred = lr_model.predict(X_test) ⑦
mae = np.mean(np.abs(y_test - y_pred)) ⑧
① 导入 scikit-learn 函数以将数据分为训练集和测试集
② 导入 scikit-learn 的线性回归类
③ 导入 numpy 库以评估模型的性能
④ 将数据分为训练集和测试集,其中 80%的数据用于训练,20%的数据用于测试,并确保使用 random_state 参数设置随机数生成器的种子,以保证训练集和测试集的分割一致性
⑤ 初始化基于最小二乘法的线性回归模型
⑥ 通过在训练集上拟合来学习模型的权重
⑦ 使用学习到的权重来预测测试集中患者的疾病进展
⑧ 使用平均绝对误差(MAE)指标评估模型性能
训练的线性回归模型的性能可以通过将预测值与测试集中的实际值进行比较来量化。我们可以使用多个指标,例如均方根误差(RMSE)、平均绝对误差(MAE)和平均绝对百分比误差(MAPE)。这些指标中的每一个都有其优缺点,并且使用多个指标来衡量模型的好坏有助于量化性能。MAE 和 RMSE 与目标变量具有相同的单位,并且在这方面易于理解。然而,使用这两个指标很难理解误差的大小。例如,一个 10 的误差可能一开始看起来很小,但如果你要比较的实际值是 100,那么这个误差相对于那个值来说就不小了。这就是 MAPE 在这里很有用,因为它以百分比(%)的形式表达误差,有助于理解这些相对差异。测量模型好坏的话题很重要,但超出了本书的范围。你可以在网上找到很多资源。我已经写了一篇综合的两部分博客文章(mng.bz/ZzNP)来涵盖这个主题。
之前训练的线性回归模型使用 MAE 指标进行了评估,其性能被确定为 42.8。但是,这个性能好吗?为了检查一个模型的性能是否良好,我们需要将其与基线进行比较。对于 Diagnostics+,医生们一直在使用一个基线模型,该模型预测所有患者糖尿病进展的中位数。这个基线模型的 MAE 被确定为 62.2。如果我们现在将这个基线与线性回归模型进行比较,我们会注意到 MAE 下降了 19.4,这是一个相当好的改进。我们现在已经训练了一个不错的模型,但它并没有告诉我们模型是如何得出预测的,以及哪些输入特征是最重要的。我将在下一节中介绍这一点。
2.3.1 解释线性回归
在前面的章节中,我们在模型开发期间训练了一个线性回归模型,然后在测试期间使用 MAE 指标评估了模型性能。作为构建 Diagnostics+ AI 的数据科学家,你现在将这些结果与医生们分享,他们对性能表示满意。但是,还有一些不足之处。医生们对模型如何得出最终预测没有清晰的理解。解释梯度下降算法并不能帮助理解这一点,因为在这个例子中你处理的是一个相当大的特征空间——总共 10 个输入特征。在 10 维空间中可视化算法如何收敛到最终预测是不可能的。一般来说,描述和解释机器学习算法的能力并不能保证其可解释性。那么,最佳的解释模型的方法是什么呢?
对于线性回归,因为最终的预测只是输入特征的加权求和,我们只需要关注学习到的权重。这就是为什么线性回归是一个白盒模型。权重告诉我们什么?如果一个特征的权重是正的,那么输入的正变化将导致输出的正变化成比例增加,输入的负变化将导致输出的负变化成比例增加。同样,如果权重是负的,输入的正变化将导致输出的负变化成比例增加,输入的负变化将导致输出的正变化成比例增加。这种在图 2.6 中显示的学到的函数被称为线性、单调函数。

图 2.6 线性、单调函数的表示
我们也可以通过查看对应权重的绝对值来观察一个特征在预测目标变量中的影响或重要性。权重的绝对值越大,其重要性就越高。图 2.7 显示了 10 个特征按重要性降序排列的权重。

图 2.7 糖尿病线性回归模型的特征重要性
最重要的特征是总胆固醇测量值。其权重具有较大的负值。这意味着胆固醇水平的任何增加都会对预测糖尿病进展产生较大的负面影响。这可能是因为总胆固醇也包含了好的胆固醇类型。
如果我们现在查看坏胆固醇,或 LDL,特征,它具有较大的正权重,并且也是预测糖尿病进展的第四个最重要的特征。这意味着 LDL 胆固醇水平的任何增加都会对预测一年后糖尿病的进展产生较大的正影响。好的胆固醇,或 HDL,特征具有较小的正权重,并且是第三个最不重要的特征。为什么是这样?回想一下我们在 2.2 节中进行的探索性分析,我们在图 2.4 中绘制了相关矩阵。如果我们观察总胆固醇、LDL 和 HDL 之间的相关性,我们会看到总胆固醇和 LDL 之间有非常高的相关性,总胆固醇和 HDL 之间有中等程度的高相关性。由于这种相关性,模型认为 HDL 特征是冗余的。
看起来,对于该患者的基线葡萄糖测量对预测一年后糖尿病进展的影响非常小。如果我们再次回到图 2.4 所示的关联图,我们可以看到葡萄糖测量与基线青光眼测量(模型中第二重要的特征)高度相关,并且与总胆固醇(模型中最重要的特征)高度相关。因此,模型将葡萄糖视为一个冗余特征,因为大部分信号都来自总胆固醇和青光眼特征。
如果一个输入特征与一个或多个其他特征高度相关,则称它们为多重共线性。多重共线性可能会损害基于最小二乘法的线性回归模型的性能。假设我们使用两个特征,x[1]和x[2],来预测目标变量y。在线性回归模型中,我们实际上是在估计每个特征的权重,这些权重将有助于预测目标变量,从而最小化平方误差。使用最小二乘法,特征x[1]的权重,或x[1]对目标变量y的影响,是通过保持x[2]不变来估计的。同样,x[2]的权重是通过保持x[1]不变来估计的。如果x[1]和x[2]是共线的,那么它们会一起变化,这就使得准确估计它们对目标变量的影响变得非常困难。其中一个特征对于模型来说变得完全冗余。我们之前在糖尿病模型中看到了共线性对模型的影响,其中像 HDL 和葡萄糖这样的特征与目标变量高度相关,但在最终模型中的重要性非常低。可以通过移除模型中的冗余特征来解决多重共线性问题。作为一个练习,我强烈建议你尝试这样做,看看你是否能提高线性回归模型的性能。
在训练机器学习模型的过程中,首先探索数据并确定特征之间以及它们与目标变量之间的相关性非常重要。多重共线性问题必须在模型训练之前早期发现,但如果被忽略,解释模型将有助于揭示这些问题。图 2.7 所示的图表可以使用以下代码片段在 Python 中生成:
import numpy as np ①
import matplotlib.pyplot as plt ②
import seaborn as sns ②
sns.set(style=’whitegrid’) ②
sns.set_palette(‘bright’) ②
weights = lr_model.coef_ ③
feature_importance_idx = np.argsort(np.abs(weights))[::-1] ④
feature_importance = [feature_names[idx].upper() for idx in ⑤
feature_importance_idx] ⑤
feature_importance_values = [weights[idx] for idx in ⑤
feature_importance_idx] ⑤
f, ax = plt.subplots(figsize=(10, 8)) ⑥
sns.barplot(x=feature_importance_values, y=feature_importance, ax=ax) ⑥
ax.grid(True) ⑥
ax.set_xlabel('Feature Weights') ⑥
ax.set_ylabel('Features') ⑥
① 导入 numpy 以优化方式对向量进行操作
② 导入 matplotlib 和 seaborn 以绘制特征重要性
③ 从之前训练的线性回归模型中通过 coef_ 参数获取权重
④ 按重要性降序排序权重并获取它们的索引
⑤ 使用有序索引获取特征名称和相应的权重值
⑥ 生成图 2.7 所示的图表
2.3.2 线性回归的局限性
在上一节中,我们看到了解释线性回归模型是多么容易。它非常透明,易于理解。然而,它的预测能力较差,尤其是在输入特征与目标之间的关系是非线性的情况下。考虑图 2.8 中显示的示例。

图 2.8 非线性数据集的说明
如果我们将线性回归模型拟合到这个数据集,我们会得到一个直线线性拟合,如图 2.9 所示。如图所示,该模型没有正确拟合数据,也没有捕捉到非线性关系。线性回归的这个局限性被称为欠拟合,并且模型被认为具有高偏差。在接下来的几节中,我们将看到如何通过使用具有更高预测能力的更复杂模型来克服这个问题。

图 2.9 过拟合问题(高偏差)
2.4 决策树
决策树是一种优秀的机器学习算法,可以用来建模复杂的非线性关系。它可以应用于回归和分类任务。它比线性回归具有相对更高的预测能力,并且易于解释。决策树背后的基本思想是在数据中找到最佳分割,以最好地预测输出或目标变量。在图 2.10 中,我通过仅考虑两个特征,BMI 和年龄,来展示这一点。决策树将数据集分为总共五个组,三个年龄组和两个 BMI 组。

图 2.10 决策树分割策略
在确定最佳分割时,通常应用的算法是分类和回归树(CART)算法。该算法首先选择一个特征和该特征的阈值。基于该特征和阈值,算法将数据集分割成以下两个子集:
-
子集 1,其中特征的值小于或等于阈值
-
子集 2,其中特征的值大于阈值
算法选择特征和阈值,以最小化成本函数或标准。对于回归任务,这个标准通常是均方误差(MSE),而对于分类任务,通常是基尼不纯度或熵。然后算法继续递归地分割数据,直到标准进一步降低或达到最大深度。图 2.10 中的分割策略在图 2.11 中显示为一个二叉树。

图 2.11 决策树数据分割以二叉树形式可视化
可以使用 Python 中的 Scikit-Learn 包训练决策树模型,如下所示。学习开放糖尿病数据集并将其分割为训练集和测试集的代码与第 2.3 节中用于线性回归的代码相同,因此这里不再重复:
from sklearn.tree import DecisionTreeRegressor ①
dt_model = DecisionTreeRegressor(max_depth=None, random_state=42) ②
dt_model.fit(X_train, y_train) ③
y_pred = dt_model.predict(X_test) ④
mae = np.mean(np.abs(y_test - y_pred)) ⑤
① 导入 scikit-learn 的决策树回归器类
② 初始化决策树回归器。设置 random_state 非常重要,以确保可以得到一致且可重复的结果。
③ 训练决策树模型
④ 使用训练好的决策树模型预测测试集中患者的疾病进展
⑤ 使用平均绝对误差(MAE)指标评估模型性能
这里训练的决策树模型使用 MAE 指标进行评估,性能确定为 54.7。如果我们调整max_depth超参数并将其设置为 3,我们可以进一步提高 MAE 性能到 48.6。然而,这种性能比第 2.2 节中训练的回归模型要差。我将在第 2.4.2 节中讨论这种差异的原因,但首先,让我们在下一节中看看如何解释决策树。
用于分类任务的决策树
如本节所述,决策树也可以用于分类任务。在 CART 算法中,Gini 不纯度或熵被用作成本函数。在 Scikit-Learn 中,你可以轻松地训练一个决策树分类器,如下所示:
from sklearn.tree import DecisionTreeClassifier
dt_model = DecisionTreeClassifier(criterion=’gini’, max_depth=None)
dt_model.fit(X_train, y_train)
DecisionTreeClassifier中的criterion参数可以用来指定 CART 算法的成本函数。默认情况下,它设置为gini,但可以更改为entropy。
2.4.1 解释决策树
决策树擅长建模输入和输出之间的非线性关系。通过在特征间找到数据分割,模型倾向于学习一个本质上非线性的函数。这个函数可以是单调的,其中输入的变化导致输出以相同方向的变化,或者非单调的,其中输入的变化可能导致输出以任何方向和不同的速率变化。这如图 2.12 所示。

图 2.12 非线性、单调和非单调函数的表示
我们如何解释这样一个学习到的非线性函数?如前所述,决策树可以被视为一系列串联的 if-else 条件,其中每个条件将数据分成两部分。这样的模型可以很容易地可视化为一个二叉树,如图 2.11 所示。对于为糖尿病训练的决策树模型,二叉树的可视化如图 2.13 所示。树可以解释如下。
从树的根节点开始,检查标准化后的 BMI 是否小于等于 0。如果是,则进入树的左侧部分。如果不是,则进入树的右侧部分。因为我们是从树的根节点开始的,这个节点代表了 100%的数据。这就是为什么“样本”等于 100%的原因。此外,如果我们把max_depth设置为 0 并预测疾病进展,那么我们会使用数据中所有样本的平均值,即 153.7,在树中表示为value。通过预测 153.7,我们会得到一个均方误差(MSE)为 6076.4。
如果标准化后的 BMI <= 0,则我们进入树的左侧部分并检查标准化后的青光眼是否 <= 0。如果 BMI <= 0,我们将处理大约 59%的数据,MSE 将从父节点的 6076.4 减少到 3612.7。我们可以重复此过程,直到达到树的叶节点。如果我们查看最右侧的叶节点,这对应以下条件:如果 BMI > 0 且 BMI > 0.1 且 LDL > 0,则对 2.3%的数据预测 225.8,导致 MSE 为 2757.9。
请注意,图 2.13 中决策树的max_depth被设置为 3。随着max_depth的增加或输入特征数量的增加,此树的复杂性将增加。

图 2.13 糖尿病决策树模型可视化
图 2.13 中的可视化可以使用以下代码片段在 Python 中生成:
from sklearn.externals.six import StringIO ①
from IPython.display import Image ①
from sklearn.tree import export_graphviz ①
import pydotplus ①
diabetes_dt_dot_data = StringIO() ②
export_graphviz(dt_model,
out_file=diabetes_dt_dot_data,
filled=False, rounded=True,
feature_names=feature_names,
proportion=True,
precision=1,
special_characters=True) ③
dt_graph = pydotplus.graph_from_dot_data(diabetes_dt_dot_data.getvalue()) ④
Image(dt_graph.create_png()) ⑤
① 导入所有必要的库以生成和可视化二叉树
② 初始化一个字符串缓冲区以存储 DOT 格式的二叉树/图
③ 将决策树模型导出为 DOT 格式的二叉树
④ 使用 DOT 格式字符串生成二叉树的图像
⑤ 使用 Image 类可视化二叉树
由于决策树学习输入特征与目标之间的非线性关系,很难理解每个输入的变化对输出的影响。这不如线性回归直观。然而,我们可以在全局层面上计算每个特征在预测目标时的相对重要性。为了计算特征重要性,我们首先需要计算二叉树中节点的权重。节点的权重是通过该节点在树中的概率加权,计算该节点成本函数或纯度度量的减少。这将在下面的数学公式中展示:

然后,我们可以通过将具有该特征分割的节点的重要性求和并除以树中所有节点的重要性来计算特征重要性。这将在下面的数学公式中展示。决策树的特征重要性介于 0 和 1 之间,其中更高的值表示更大的重要性:

在 Python 中,可以从 Scikit-Learn 决策树模型中获取特征重要性,并按如下方式绘制:
weights = dt_model.feature_importances_ ①
feature_importance_idx = np.argsort(np.abs(weights))[::-1] ②
feature_importance = [feature_names[idx].upper() for idx in ③
feature_importance_idx] ③
feature_importance_values = [weights[idx] for idx in ③
feature_importance_idx] ③
f, ax = plt.subplots(figsize=(10, 8)) ④
sns.barplot(x=feature_importance_values, y=feature_importance, ax=ax) ④
ax.grid(True) ④
ax.set_xlabel('Feature Weights') ④
ax.set_ylabel('Features') ④
① 从训练好的决策树模型中获取特征重要性
② 按重要性降序排序特征权重的索引
③ 按重要性降序获取特征名称和特征权重
④ 生成图 2.14 所示的图表
按照重要性降序排列的特征及其对应权重在图 2.14 中显示。如图所示,重要特征的顺序与线性回归不同。最重要的特征是 BMI,占整体模型重要性的约 42%。青光眼测量是下一个最重要的特征,占模型重要性的约 15%。这些重要性值有助于确定哪些特征在预测目标变量时具有最大的信号。决策树算法对多重共线性问题具有免疫力,因为它选择与目标高度相关的特征,并且最能减少成本函数或纯度。作为一名数据科学家,可视化学习到的决策树(如图 2.13 所示)非常重要,因为这有助于你理解模型是如何得出最终预测的。你可以通过设置max_depth超参数或通过修剪输入到模型中的特征数量来降低树的复杂性。你可以通过可视化全局特征重要性(如图 2.14 所示)来确定要修剪的特征。

图 2.14 决策树对糖尿病特征的重要性
2.4.2 决策树的局限性
决策树非常灵活,因为它们可以应用于回归和分类任务,并且它们还具有建模非线性关系的能力。然而,该算法容易受到过拟合问题的影响,并且模型被认为具有高方差。
当模型拟合训练数据几乎完美时,就会发生过拟合问题,因此它对之前未见过的数据(如测试集)的泛化能力不好。这如图 2.15 所示。当模型过拟合时,你会在训练集上注意到非常好的性能,但在测试集上表现较差。这可以解释为什么在糖尿病数据集上训练的决策树模型的表现不如线性回归模型。

图 2.15 过拟合问题(高方差)
通过调整决策树中的某些超参数,如max_depth和叶节点所需的最小样本数,可以克服过拟合问题。如图 2.13 中决策树模型的可视化所示,一个叶节点只占样本的 0.8%。这意味着这个节点的预测仅基于大约三个患者的数据。通过将所需的最小样本数增加到 5 或 10,我们可以提高模型在测试集上的性能。
2.5 广义加性模型(GAMs)
Diagnostics+ 和医生们对迄今为止构建的两个模型相当满意,但性能并不那么好。通过解释模型,我们也发现了某些不足。线性回归模型似乎无法处理彼此高度相关的特征,例如总胆固醇、LDL 和 HDL。决策树模型的表现不如线性回归,并且似乎在训练数据上过度拟合。
让我们来看看糖尿病数据中的一个特定特征。图 2.16 展示了年龄与目标变量之间非线性关系的虚构示例,其中两个变量都已归一化。你将如何最好地建模这种关系而不过度拟合?一个可能的方法是扩展线性回归模型,其中目标变量被建模为特征集的n次多项式。这种回归形式称为多项式回归。


以下方程展示了不同次数的多项式回归。在这些方程中,我们只考虑一个特征x[1]来建模目标变量y。一次多项式与线性回归相同。对于二次多项式,我们会添加一个额外的特征,即x[1]的平方。对于三次多项式,我们会添加两个额外的特征——一个是x[1]的平方,另一个是x[1]的立方:

可以使用与线性回归相同的算法获得多项式回归模型的权重,即使用梯度下降的最小二乘法。图 2.17 中显示了三个多项式各自学习到的最佳拟合。我们可以看到,三次多项式比二次和一次多项式更好地拟合原始数据。我们可以像解释线性回归模型一样解释多项式回归模型,因为模型本质上是由包括高次特征在内的特征进行线性组合的。

图 2.17 展示了用于建模非线性关系的多项式回归
然而,多项式回归也有一些局限性。随着特征数量或特征空间维度的增加,模型的复杂性也会增加。因此,它倾向于在数据上过度拟合。此外,在多项式中确定每个特征的次数也很困难,尤其是在高维特征空间中。
那么,哪种模型可以应用于克服所有这些限制,并且也是可解释的?欢迎广义加性模型(GAMs)!GAMs 是具有中等至高预测能力和高度可解释性的模型。非线性关系通过为每个特征使用平滑函数并将它们全部相加来建模,如下方程所示:

在这个方程式中,每个特征都有其关联的平滑函数,该函数最好地建模了该特征与目标之间的关系。你可以选择许多类型的平滑函数,但一种广泛使用的平滑函数被称为 回归样条,因为它既实用又计算高效。本书将重点关注回归样条。现在,让我们深入到使用回归样条的 GAMs 世界吧!
2.5.1 回归样条
回归样条表示为基函数的加权和。这在下一个方程式中以数学形式展示。在这个方程式中,f[j] 是一个函数,它建模了特征 x[j] 与目标变量之间的关系。这个函数表示为基函数的加权和,其中权重表示为 w[k],基函数表示为 b[k]。在 GAMs 的上下文中,函数 f[j] 被称为平滑函数。

现在,什么是基函数?基函数是一组变换,可以用来捕捉一般形状或非线性关系。对于回归样条,正如其名称所暗示的,样条被用作基函数。样条是一个具有 n – 1 个导数的 n 阶多项式。使用插图来理解样条将更容易。图 2.18 显示了不同阶数的样条。左上角的图表显示了最简单的 0 阶样条,由此可以生成更高阶的样条。正如你可以从左上角的图表中看到,六个样条被放置在一个网格上。想法是将数据的分布分割成部分,并在每一部分上拟合一个样条。因此,在这个插图中,数据被分割成六个部分,我们正在将每一部分建模为 0 阶样条。

图 2.18 0 阶、1 阶、2 阶和 3 阶样条的示意图
一个 1 阶样条,如右上角图表所示,可以通过将 0 阶样条与其自身卷积生成。卷积是一种数学运算,它接受两个函数并创建一个第三函数,该函数表示第一个函数与第二个函数延迟副本的相关性。当我们对一个函数与其自身卷积时,我们实际上是在查看该函数与其自身延迟副本的相关性。Christopher Olah 有一篇关于卷积的很好的博客文章(mng.bz/5Kdq)。通过将 0 阶样条与其自身卷积,我们得到一个 1 阶样条,它是三角形的,并且具有连续的 0 阶导数。
如果我们现在将一个一次样条与其自身卷积,我们将得到一个二次样条,如图中左下角的图所示。这个二次样条有一个一阶导数。同样,我们可以通过卷积一个二次样条来得到一个三次样条,它有一个二阶导数。一般来说,一个n次样条有一个n – 1阶导数。在极限情况下,当n趋向于无穷大时,我们将获得一个具有高斯分布形状的样条。在实践中,使用三次样条,或称为三次样条,因为它可以捕捉到大多数一般形状。
如前所述,在图 2.18 中,我们将数据的分布分成了六个部分,并在网格上放置了六个样条。在早期的数学方程中,部分数或样条的数量用变量K表示。回归样条背后的想法是学习每个样条的权重,这样你就可以在每个部分中建模数据的分布。网格中部分数或样条的数量K也称为自由度。一般来说,如果我们在这K个样条上放置网格,我们将有K + 3个分割点,也称为节点。
现在我们来聚焦于三次样条,如图 2.19 所示。我们可以看到有六个样条,或者说六个自由度,导致有九个分割点或节点。

图 2.19 样条和节点的示意图
为了捕捉一个一般形状,我们需要对样条进行加权求和。在这里我们将使用三次样条。在图 2.20 中,我们使用相同的六个样条叠加来创建九个节点。对于左边的图,我为所有六个样条设置了相同的权重。正如你可以想象的那样,如果我们对所有六个样条进行等权重求和,我们将得到一条水平直线。这是对原始数据拟合不良的说明。然而,对于右边的图,我取了六个样条的不等权重求和,生成一个完美拟合原始数据的形状。这展示了回归样条和 GAMs 的强大功能。通过增加样条的数量或将数据分成更多部分,我们可以建模更复杂的非线性关系。在基于回归样条的 GAMs 中,我们分别对每个特征与目标变量的非线性关系进行建模,然后将它们全部加起来以得出最终的预测。

图 2.20 用于建模非线性关系的样条
在图 2.20 中,权重是通过试错法确定的,以最好地描述原始数据。但是,如何算法性地确定回归样条的最佳权重,以捕捉特征与目标之间的关系呢?回想一下本节开头提到的,回归样条是基函数或样条的有权总和。这本质上是一个线性回归问题,你可以使用最小二乘法和梯度下降法来学习权重。然而,我们需要指定结点的数量,或者说自由度。我们可以将其视为超参数,并使用称为 交叉验证 的技术来确定它。使用交叉验证,我们会移除一部分数据,并在剩余数据上拟合一个具有预定的结点数量的回归样条。然后,在这个保留的集合上评估这个回归样条。最佳的结点数量是导致在保留集合上性能最佳的数量。
在 GAMs 中,通过增加样条或自由度的数量,很容易过度拟合。如果样条的数量很高,得到的平滑函数(样条的有权总和)会非常“扭曲”——它开始拟合数据中的噪声。我们如何控制这种扭曲或防止过度拟合呢?我们可以使用一种称为 正则化 的技术。在正则化中,我们会在最小二乘成本函数中添加一个量化扭曲的项。然后,我们可以通过取函数的二阶导数的平方的积分来量化平滑函数的扭曲程度。然后,使用一个超参数(也称为正则化参数)λ 来调整扭曲的强度。λ 的值越高,对扭曲的惩罚就越重。我们可以使用与确定其他超参数相同的方式,通过交叉验证来确定 λ。
GAMs 概述
GAM 是一种强大的模型,其中目标变量被表示为一系列平滑函数的总和,这些平滑函数代表了每个特征与目标之间的关系。我们可以使用平滑函数来捕捉任何非线性关系。这在此处用数学公式表示:
y = w[0] + f[1](x[1]) + f[2](x[2]) +...+ f[n](x[n])
这是一个白盒模型——我们可以很容易地看到每个特征是如何通过平滑函数转换为输出的。表示平滑函数的一种常见方式是使用回归样条。回归样条表示为基函数的简单加权求和。GAMs 中广泛使用的基函数是三次样条。通过增加样条的数量或自由度,我们可以将数据的分布划分为小部分,并逐部分建模。这样,我们可以捕捉非常复杂的非线性关系。学习算法本质上必须确定回归样条的权重。我们可以像线性回归一样使用最小二乘法和梯度下降法来做这件事。我们可以使用交叉验证技术来确定样条的数量。随着样条数量的增加,GAMs 倾向于在数据上过拟合。我们可以通过使用正则化技术来防止这种情况。使用正则化参数λ,我们可以控制曲线的波动程度。较高的λ确保函数更加平滑。参数λ也可以通过交叉验证来确定。
GAMs 也可以用于建模变量之间的交互。GA2M,如数学上所示,是一种建模成对交互的 GAM:

在主题专家(SMEs)——Diagnostics+示例中的医生——的帮助下,你可以确定需要建模哪些特征交互。你也可以查看特征之间的相关性,以了解需要一起建模哪些特征。
在 Python 中,你可以使用一个名为 pyGAM 的包来构建和训练 GAMs。它受到了 R 中流行的 mgcv 包中 GAM 实现的启发。你可以使用 pip 包在你的 Python 环境中安装 pyGAM,如下所示:
pip install pygam
2.5.2 GAM 用于 Diagnostics+糖尿病
现在让我们回到 Diagnostics+示例,使用所有 10 个特征来训练一个 GAM 以预测糖尿病进展。请注意,患者的性别是一个分类或离散特征。使用平滑函数来模拟这个特征是没有意义的。我们可以在 GAM 中将此类分类特征视为因子项。我们可以使用 pyGAM 包如下训练 GAM。与决策树一样,我不会重复加载糖尿病数据集并将其拆分为训练集和测试集的代码。请参阅第 2.2 节以获取该代码片段:
from pygam import LinearGAM ①
from pygam import s ②
from pygam import f ③
# Load data using the code snippet in Section 2.2
gam = LinearGAM(s(0) + ④
f(1) + ⑤
s(2) + ⑥
s(3) + ⑦
s(4) + ⑧
s(5) + ⑨
s(6) + ⑩
s(7) + ⑪
s(8) + ⑫
s(9), ⑬
n_splines=35) ⑭
gam.gridsearch(X_train, y_train) ⑮
y_pred = gam.predict(X_test) ⑯
mae = np.mean(np.abs(y_test - y_pred)) ⑰
① 从 pygam 导入 LinearGAM 类,可用于训练回归任务的 GAM
② 导入用于数值特征的平滑项函数
③ 导入用于分类特征的因子项函数
④ 年龄特征的立方样条项
⑤ 性别特征的因子项,这是一个分类特征
⑥ 体质指数(BMI)特征的立方样条项
⑦ 血压(BP)特征的立方样条项
⑧ 总胆固醇特征的立方样条项
⑨ 低密度脂蛋白(LDL)特征的立方样条项
⑩ HDL 特征的立方样条项
⑪ Thyroid 特征的立方样条项
⑫ Glaucoma 特征的立方样条项
⑬ Glucose 特征的立方样条项
⑭ 每个特征要使用的最大样条数量
⑮ 使用网格搜索进行训练和交叉验证,以确定每个特征的样条数量、正则化参数 lambda 以及回归样条的优化权重
⑯ 使用训练好的 GAM 模型在测试上进行预测
⑰ 使用 MAE 指标评估模型在测试集上的性能
现在是检验真伪的时刻!GAM 的表现如何?GAM 的 MAE 性能为 41.4,与线性回归和决策树模型相比,这是一个相当不错的改进。所有三个模型性能的比较总结在表 2.2 中。我还包括了 Diagnostics+和医生们一直在使用的基线模型的表现,他们在所有患者中查看糖尿病进展的中位数。所有模型都与基线进行比较,以显示模型为医生带来的改进程度。看起来 GAM 在所有性能指标上都是最好的模型。
表 2.2 线性回归、决策树和 GAM 相对于 Diagnostics+基线的性能比较
| MAE | RMSE | MAPE | |
|---|---|---|---|
| 基线 | 62.2 | 74.7 | 51.6 |
| 线性回归 | 42.8 (–19.4) | 53.8 (–20.9) | 37.5 (–14.1) |
| 决策树 | 48.6 (–13.6) | 60.5 (–14.2) | 44.4 (–7.2) |
| GAM | 41.4 (–20.8) | 52.2 (–22.5) | 35.7 (–15.9) |
我们现在已经看到了 GAM 的预测能力。通过建模特征交互,特别是胆固醇特征之间的交互,以及与其他可能高度相关的特征(如 BMI)的交互,我们有可能进一步提高性能。作为一个练习,我鼓励你尝试使用 GAM 建模特征交互。
GAM 是白盒模型,可以很容易地解释。在下一节中,我们将看到如何解释 GAM。
GAM 分类任务
GAM 也可以通过使用逻辑链接函数来训练二元分类器,其中响应y可以是 0 或 1。在 pyGAM 包中,你可以使用逻辑 GAM 来解决二元分类问题,如下所示:
from pygam import LogisticGAM
gam = LogisticGAM()
gam.gridsearch(X_train, y_train)
2.5.3 解释 GAM
虽然每个平滑函数都是通过基函数的线性组合获得的,但每个特征的最终平滑函数是非线性的,因此我们不能像线性回归那样解释权重。然而,我们可以通过部分依赖或部分效应图轻松地可视化每个特征对目标的影响。部分依赖通过边缘化其他特征来观察每个特征的影响。它非常易于解释,因为我们可以看到每个特征值对目标变量的平均影响。我们可以看到目标对特征的反应是线性、非线性、单调还是非单调。图 2.21 显示了每个病人特征对目标变量的影响。它们周围的 95%置信区间也已绘制。这将帮助我们确定模型对样本量较小的数据点的敏感性。
现在我们来看看图 2.21 中的几个特性,即 BMI 和 BP。BMI 对目标变量的影响显示在左下角的图表中。在x轴上,我们看到 BMI 的标准化值,而在y轴上,我们看到 BMI 对病人糖尿病进展的影响。我们看到随着 BMI 的增加,对糖尿病进展的影响也增加。右下角的图表显示了类似的趋势,BP 越高,对糖尿病进展的影响也越大。如果我们观察 95%置信区间线(图 2.21 中的虚线),我们会看到 BMI 和 BP 的上下端附近的置信区间更宽。这是因为在这个值范围内的病人样本较少,导致对这些特征在该范围内的效果的认知存在更高的不确定性。

图 2.21 每个病人特征对目标变量的影响
生成图 2.21 的代码如下:
grid_locs1 = [(0, 0), (0, 1), ①
(1, 0), (1, 1)] ①
fig, ax = plt.subplots(2, 2, figsize=(10, 8)) ②
for i, feature in enumerate(feature_names[:4]): ③
gl = grid_locs1[i] ④
XX = gam.generate_X_grid(term=i) ⑤
ax[gl[0], gl[1]].plot(XX[:, i], gam.partial_dependence(term=i, X=XX)) ⑥
ax[gl[0], gl[1]].plot(XX[:, i], gam.partial_dependence(term=i, X=XX,
➥ width=.95)[1], c='r', ls='--') ⑦
ax[gl[0], gl[1]].set_xlabel('%s' % feature) ⑧
ax[gl[0], gl[1]].set_ylabel('f ( %s )' % feature) ⑧
① 四个图表在 2x2 Matplotlib 网格中的位置
② 创建一个 2x2 的 Matplotlib 图表网格
③ 遍历四个病人元数据特征
④ 获取特征在 2x2 网格中的位置
⑤ 生成特征值的部分依赖,目标对其他特征进行边缘化
⑥ 以实线绘制部分依赖值
⑦ 以虚线绘制部分依赖值的 95%置信区间
⑧ 为 x 轴和 y 轴添加标签
图 2.22 显示了六个血液检测指标对目标的影响。作为一个练习,观察总胆固醇、LDL、HDL 和青光眼等特征对糖尿病进展的影响。你能说些什么关于更高 LDL 值(或坏胆固醇)对目标变量的影响?为什么更高总胆固醇对目标变量的影响较小?为了回答这些问题,让我们看看一些胆固醇值非常高的病人案例。以下代码片段将帮助您聚焦这些病人:
print(df_data[(df_data['Total Cholesterol'] > 0.15) &
(df_data['LDL'] > 0.19)])
如果你执行此代码,你将看到 442 个病人中只有一个病人的总胆固醇读数大于 0.15,并且低密度脂蛋白读数大于 0.19。这位病人在一年后的空腹血糖水平(目标变量)似乎为 84,处于正常范围内。这可以解释为什么在图 2.22 中,我们看到了总胆固醇对目标变量在大于 0.15 的范围内的非常显著的负影响。总胆固醇的负影响似乎大于坏的低密度脂蛋白胆固醇对目标的正面影响。这些值范围内的置信区间似乎要宽得多。模型可能对这个异常病人的记录过度拟合,因此,我们不应过分解读这些影响。通过观察这些影响,我们可以识别出模型对预测有信心和存在高度不确定性的案例或值范围。对于高度不确定的案例,我们可以回到诊断中心收集更多病人数据,以便我们有代表性的样本。

图 2.22 每个血液检测测量对目标变量的影响
生成图 2.22 的代码如下:
grid_locs2 = [(0, 0), (0, 1), ①
(1, 0), (1, 1), ①
(2, 0), (2, 1)] ①
fig2, ax2 = plt.subplots(3, 2, figsize=(12, 12)) ②
for i, feature in enumerate(feature_names[4:]): ③
idx = i + 4 ④
gl = grid_locs2[i] ④
XX = gam.generate_X_grid(term=idx) ⑤
ax2[gl[0], gl[1]].plot(XX[:, idx], gam.partial_dependence(term=idx,
➥ X=XX)) ⑥
ax2[gl[0], gl[1]].plot(XX[:, idx], gam.partial_dependence(term=idx, X=XX,
➥ width=.95)[1], c='r', ls='--') ⑦
ax2[gl[0], gl[1]].set_xlabel('%s' % feature) ⑧
ax2[gl[0], gl[1]].set_ylabel('f ( %s )' % feature) ⑧
① 六个图表在 3 × 2 Matplotlib 网格中的位置
② 创建一个 3 × 2 网格的 Matplotlib 图表
③ 遍历六个血液检测测量特征
④ 获取特征在 3 × 2 网格中的位置
⑤ 生成特征值与目标变量对其他特征网格的边际化相关的部分依赖图
⑥ 以实线绘制部分依赖值
⑦ 以虚线绘制部分依赖值周围的 95% 置信区间
⑧ 为 x 轴和 y 轴添加标签
通过图 2.21 和 2.22,我们可以更深入地了解每个特征值对目标的边际效应。部分依赖图对于调试模型中的任何问题都很有用。通过绘制部分依赖值周围的 95% 置信区间,我们还可以看到样本量较小的数据点。如果一个特征值在样本量较小的情况下对目标有显著影响,那么可能存在过度拟合问题。我们还可以可视化平滑函数的波动性,以确定模型是否拟合了数据中的噪声。我们可以通过增加正则化参数的值来解决这些过度拟合问题。这些部分依赖图也可以与 SME(在这种情况下是医生)共享,以进行验证,这将有助于赢得他们的信任。
2.5.4 GAMs 的局限性
到目前为止,我们已经看到了 GAMs 在预测能力和可解释性方面的优势。GAMs 有过度拟合的倾向,尽管可以通过正则化来克服。然而,你需要注意以下其他局限性:
-
GAMs 对训练集中范围之外的特性值敏感,并且当暴露于异常值时往往会失去预测能力。
-
对于关键任务,GAMs 有时可能预测能力有限,在这种情况下,你可能需要考虑更强大的黑盒模型。
2.6 展望黑盒模型
黑盒模型是具有极高预测能力的模型,通常应用于模型性能(如准确度)极为重要的任务。然而,它们本质上是不可透见的,使它们不可透见的特征包括以下内容:
-
机器学习过程很复杂,你无法轻易理解输入特征是如何转换为输出或目标变量的。
-
你无法轻易识别出预测目标变量最重要的特征。
黑盒模型的例子包括随机森林和梯度提升树等树集成模型,深度神经网络(DNNs),卷积神经网络(CNNs)和循环神经网络(RNNs)。表 2.3 显示了这些模型通常应用的机器学习任务。
表 2.3 黑盒模型到机器学习任务的映射
| 黑盒模型 | 机器学习任务 |
|---|---|
| 树集成(随机森林、梯度提升树) | 回归和分类 |
| 深度神经网络(DNNs) | 回归和分类 |
| 卷积神经网络(CNNs) | 图像分类、目标检测 |
| 循环神经网络(RNNs) | 序列建模、语言理解 |
我现在已经在与 2.1 节中介绍的预测能力与可解释性平面相同的黑盒模型中进行了绘制,如图 2.23 所示。

图 2.23 黑盒模型在可解释性与预测能力平面上的分布
黑盒模型因为具有高预测能力但低可解释性而被聚类在平面的左上角。对于关键任务,重要的是不要通过应用白盒模型来牺牲模型性能(如准确度)以换取可解释性。我们需要应用黑盒模型来完成这些任务,并且需要找到解释它们的方法。我们可以以多种方式解释黑盒模型,而这本书剩余章节的主要焦点。在下一章中,我们将特别关注树集成以及如何使用全局、模型无关的技术来解释它们。
摘要
-
白盒模型本质上是透明的。机器学习过程易于理解,你可以清楚地解释输入特征是如何转换为输出的。使用白盒模型,你可以识别出最重要的特征,而这些特征是可理解的。
-
线性回归是最简单的白盒模型之一,其中目标变量被建模为输入特征的线性组合。你可以使用最小二乘法和梯度下降法确定权重。
-
我们可以使用 Scikit-Learn 包中的
LinearRegression类在 Python 中实现线性回归。你可以通过检查系数或学习到的权重来解释模型。权重也可以用来确定每个特征的重要性。然而,线性回归却存在多重共线性和高偏差的问题。 -
决策树是一种稍微高级一点的白盒模型,可以用于回归和分类任务。你可以通过将数据分割到所有特征上以最小化成本函数来预测目标变量。你已经学习了 CART 算法来学习分割。
-
使用 Scikit-Learn 中的
DecisionTreeRegressor类,可以在 Python 中实现回归任务的决策树。你可以使用 Scikit-Learn 中的DecisionTreeClassifier类实现分类任务的决策树。你可以通过将 CART 学习到的决策树可视化成二叉树来解释它。Scikit-Learn 的实现还为你计算了特征重要性。决策树可以用来建模非线性关系,但往往容易过度拟合。 -
GAMs 是一种强大的白盒模型,其中目标变量被表示为一系列平滑函数的总和,这些平滑函数代表了每个特征与目标之间的关系。你知道回归样条和三次样条广泛用于表示平滑函数。
-
回归样条和广义加性模型(GAMs)可以使用 Python 中的 pyGAM 包实现。我们可以使用
LinearGAM类进行回归任务,使用LogisticGAM类进行分类任务。你可以通过绘制每个特征对目标变量的部分依赖性来解释 GAM。GAMs 有过度拟合的倾向,但这个问题可以通过正则化来缓解。 -
黑盒模型是具有极高预测能力的模型,通常应用于模型性能(如准确率)极其重要的任务。然而,它们本质上是透明的。机器学习过程很复杂,你无法轻易理解输入特征是如何转换成输出或目标变量的。因此,你无法轻易识别出预测目标变量最重要的特征。
第二部分. 模型处理解释
这一部分的书籍专注于黑盒模型,并理解模型如何处理输入并得出最终预测。
在第三章中,你将学习一类称为树集成(tree ensembles)的黑盒模型。你将了解它们的特性和使它们成为黑盒的原因。你还将学习如何使用全局范围内的后验模型无关方法来解释它们,例如部分依赖图(PDPs)和特征交互图。
在第四章中,你将学习关于深度神经网络的内容,特别是纯全连接神经网络。你将了解使这些模型成为黑盒的特性,以及如何使用局部范围内的后验模型无关方法来解释它们,例如局部可解释模型无关解释(LIME)、SHapley 加性解释(SHAP)和锚点。
在第五章中,你将学习关于卷积神经网络的内容,这是一种主要用于视觉任务的高级架构形式,例如图像分类和目标检测。你将学习如何使用显著性图来可视化模型关注的重点。你还将了解梯度、指导反向传播(简称backprop)、梯度加权类激活映射(Grad-CAM)、指导 Grad-CAM 和光滑梯度(SmoothGrad)等技术。
3 模型无关方法:全局可解释性
本章涵盖
-
模型无关方法和全局可解释性的特点
-
如何实现树集成,特别是随机森林——一个黑盒模型
-
如何解释随机森林模型
-
如何使用称为部分依赖图(PDPs)的模型无关方法来解释黑盒模型
-
如何通过观察特征交互来揭示偏差
在上一章中,我们看到了两种不同的机器学习模型——白盒模型和黑盒模型,并将大部分注意力集中在如何解释白盒模型上。黑盒模型具有很高的预测能力,正如其名称所示,很难解释。在本章中,我们将专注于解释黑盒模型,并具体介绍那些模型无关和(全局)范围内的技术。回想一下第一章,模型无关的可解释性技术不依赖于所使用的特定模型类型。由于它们独立于模型的内部结构,因此可以应用于任何模型。此外,全局范围内的可解释性技术可以帮助我们理解整个模型。我们还将专注于树集成,特别是随机森林。尽管我们专注于随机森林,但你可以在本章中学到的模型无关技术应用于任何模型。在下一章中,我们将转向更复杂的黑盒模型,如神经网络。在第四章中,你还将了解局部范围内的模型无关技术,如 LIME、SHAP 和锚点。
第三章的结构与第二章相似。我们将从一个具体的例子开始。在本章中,我们将暂时放下 Diagnostics+,专注于与教育相关的问题。我选择这个问题是因为数据集有一些有趣的特点,我们可以通过本章中你将学习的可解释性技术来揭示这个数据集中的某些问题。与第二章一样,本章的主要重点是实现可解释性技术,以更好地理解黑盒模型(特别是树集成)。我们将在模型开发和测试过程中应用这些可解释性技术。你还将了解模型训练和测试,特别是实施方面。由于模型学习、测试和理解阶段是迭代的,因此涵盖这三个阶段是很重要的。对于已经熟悉树集成训练和测试的读者,可以自由跳过这些部分,直接进入可解释性部分。
3.1 高中生成绩预测器
让我们从一个具体的例子开始。我们将从诊断+和医疗保健行业切换到教育行业。美国一个学校区的区主管向您寻求帮助,以解决她的数据科学问题。区主管希望了解学生在三个关键学科领域——数学、阅读和写作——的表现,以确定不同学校的资金需求,并确保每个学生都能在《每个学生成功法案》(ESSA)的框架下取得成功。
区主管特别希望得到帮助,预测她辖区的高中生在数学、阅读和写作科目中的成绩。成绩可以是 A、B、C 或 F。根据这些信息,您会如何将这个问题表述为一个机器学习问题?因为模型的目的是预测成绩,这可以是四个离散值之一,所以这个问题可以表述为一个分类问题。在数据方面,区主管收集了她辖区代表不同学校和背景的 1,000 名学生的数据。以下五个数据点为每个学生收集:
-
性别
-
种族
-
父母的教育水平
-
学生购买的午餐类型
-
测试准备水平
基于这些数据,因此您需要训练三个独立的分类器,每个分类器对应一个学科领域。这如图 3.1 所示。

图 3.1 学校区主管所需的学生表现模型示意图
受保护属性和公平性
受保护属性是与个人相关的、与社会偏见有关的属性,包括性别、年龄、种族、民族、性取向等。美国和欧洲等某些地区的法律禁止基于这些受保护属性歧视个人,特别是在住房、就业和信贷贷款等领域。在构建可能使用这些受保护属性作为特征的机器学习模型时,了解这些法律框架和非歧视法律非常重要。我们希望确保机器学习模型不会嵌入偏见,并基于受保护属性歧视某些个人。在本章中,我们的数据集包含一些受保护属性,我们主要将其用作模型的特征,以学习如何通过可解释性技术揭示模型与偏见相关的问题。我们将在第八章更深入地介绍受保护属性的法律框架和各种公平性标准。此外,本章中使用的数据集是虚构的,并不反映学校区实际的学生表现。种族/民族特征也已匿名化。
3.1.1 探索性数据分析
我们在这里处理的是一个新数据集,所以在训练模型之前,让我们首先了解不同的特征及其可能的值。数据集包含五个特征:学生的性别、他们的民族、父母的教育水平、他们购买的午餐类型以及他们的测试准备水平。所有这些特征都是分类特征,其中可能的值是离散且有限的。对于每个学生有三个目标变量:数学成绩、阅读成绩和写作成绩。成绩可以是 A、B、C 或 F。
有两个性别类别——男性和女性,学生的女性人口(52%)略高于男性人口(48%)。现在让我们关注另外两个特征——学生的民族和父母的教育水平。图 3.2 显示了这些特征的各个类别以及属于这些类别的学生的比例。人口中有五个群体或民族,C 组和 D 组是最具代表性的,占学生人口的约 58%。有六个不同的父母教育水平。按顺序排列,它们是某些高中、(认可)高中、某些大学、副学士学位、学士学位和硕士学位。看起来有更多学生的父母教育水平较低。大约 82%的学生父母的教育水平是高中或大学水平或副学士学位。只有 18%的学生父母拥有学士学位或硕士学位。

图 3.2 特征“民族”和“父母教育水平”的值和比例
接下来是剩余的两个特征——购买的午餐类型和测试准备水平。大多数人(大约 65%)购买标准午餐,其余的人购买免费/减价午餐。在测试准备方面,只有 36%的学生完成了他们的测试准备,而对于剩余的学生,要么没有完成,要么未知。
现在我们来看一下在三个学科领域中取得 A、B、C 或 F 成绩的学生比例。这显示在图 3.3 中。我们可以看到,大多数学生(48-50%)获得 B 级成绩,而极少数学生(3-4%)获得 F 级成绩。大约 18-25%的学生获得 A 级成绩,在整个三个学科领域中,22-28%的学生获得 C 级成绩。值得注意的是,在我们训练模型之前,数据相当不平衡。这为什么很重要,我们如何处理不平衡数据?在分类问题中,当我们给定类别存在不成比例的例子或数据点时,我们说数据是不平衡的。这一点很重要,因为大多数机器学习算法在各个类别的样本比例大致相同的情况下表现最佳。大多数算法旨在最小化错误或最大化准确度,这些算法倾向于自然地偏向多数类。我们可以通过几种方式处理不平衡的类别,包括以下常见方法:
-
在测试和评估模型时,使用正确的性能指标。
-
重新采样训练数据,使得多数类要么被欠采样,要么少数类被过采样。
你将在第 3.2 节中了解更多关于这些方法的信息。

图 3.3 三个学科领域中成绩目标变量的值和比例
让我们更深入地剖析一下数据。以下见解将在第 3.4 节中对我们解释和验证模型学到了什么很有用。当学生的父母教育水平最低和最高时,学生通常表现如何?让我们比较父母教育水平最低(即高中)的学生与父母教育水平最高(即硕士学位)的学生的成绩分布。图 3.4 显示了所有三个学科领域的这种比较。

图 3.4 比较父母拥有高中教育与硕士学位的学生成绩分布百分比
让我们关注那些父母有高中教育背景的学生。在所有三个学科领域,从总体上看,获得 A 级的学生比总体人口中更多,而获得 F 级的学生比总体人口中更少。例如,在数学学科领域,只有 10%的父母有高中教育背景的学生获得 A 级,而总体人口(如图 3.3 所示)中大约有 20%的学生获得 A 级。现在让我们关注那些父母有硕士学位的学生。从总体上看,与总体人口相比,获得 A 级的学生更多,没有学生获得 F 级。例如,在数学学科领域,大约有 30%的父母有硕士学位的学生获得 A 级。如果我们现在比较图 3.4 中的两个条形图,我们可以看到,当父母在所有三个学科领域拥有更高教育水平时,获得更高等级(A 或 B)的学生数量会显著增加。
那么,种族因素如何呢?属于最代表性群体(C 组)的学生与属于最少代表性群体(A 组)的学生在表现上有什么不同?从图 3.2 中,我们知道最代表性群体是 C 组,最少代表性群体是 A 组。图 3.5 比较了属于 C 组的学生与属于 A 组的学生成绩分布。
从总体上看,C 组的学生似乎比 A 组的学生表现更好——似乎有更大比例的学生获得了更高的成绩(A 或 B),而获得较低成绩(C 或 F)的学生比例较小。如前所述,本节中的见解将在第 3.4 节中派上用场,当我们解释和验证模型学到了什么时。

图 3.5 比较属于种族群体 A 和 C 的学生成绩分布百分比
3.2 树集成
在第二章中,你学习了决策树,这是一种建模非线性关系的强大方法。决策树是白盒模型,易于解释。然而,我们注意到更复杂的决策树存在过拟合的问题,即模型过度拟合数据中的噪声或方差。为了克服过拟合的问题,我们可以通过剪枝来降低决策树的复杂性,包括深度和叶节点所需的最小样本数。然而,这会导致预测能力降低。
通过结合多个决策树,我们可以在不牺牲预测能力的情况下避免过拟合问题。这是树集成背后的原理。我们可以以下两种广泛的方式组合或集成决策树:
-
Bagging——在训练数据的独立随机子集上并行训练多个决策树。我们可以使用这些独立的决策树进行预测,并通过取平均值将它们组合起来,得出最终的预测。随机森林是使用 Bagging 技术的一种树集成。除了在数据的随机子集上训练独立的决策树外,随机森林算法还随机抽取特征子集来分割数据。
-
Boosting——与 Bagging 类似,Boosting 技术也训练多个决策树,但顺序不同。第一个决策树通常是一个浅层树,并在训练集上训练。第二个决策树的目标是从第一个决策树的错误中学习,并进一步提高性能。使用这种技术,我们将多个决策树串联起来,它们迭代地尝试优化和减少前一个决策树所犯的错误。自适应提升和梯度提升是两种常见的 Boosting 算法。
在本章中,我们将重点关注集成学习技术,特别是随机森林算法,如图 3.6 所示。首先,我们从训练数据中随机抽取子集,并在这些子集上分别训练独立的决策树。然后,每个决策树在特征随机子集上进行分割。我们通过在所有决策树中进行多数投票来获得最终的预测。正如您所看到的,随机森林模型比决策树复杂得多。随着集成中树的数量增加,复杂性也随之增加。此外,由于每个决策树都从数据特征中抽取随机子集进行分割,因此很难可视化和解释所有决策树中特征的分割情况。这使得随机森林成为一个黑盒模型,难以解释。能够解释算法并不意味着在这种情况下具有可解释性。

图 3.6 随机森林算法的示意图
为了完整性,让我们也讨论一下自适应提升和梯度提升算法的工作原理。自适应提升,通常简称为AdaBoost,如图 3.7 所示。该算法的工作原理如下。首先,我们使用所有训练数据训练一个决策树。在第一个决策树中,每个数据点都被赋予相同的权重。一旦第一个决策树训练完成,通过计算每个数据点的加权误差总和来计算树的误差率。然后,我们使用这个加权误差率来确定决策树的权重。如果树的误差率较高,则给树赋予较低的权重,因为其预测能力较低。如果误差率较低,则给树赋予较高的权重,因为它具有更高的预测能力。然后,我们使用第一个决策树的权重来确定第二个决策树的每个数据点的权重。被错误分类的数据点将被赋予更高的权重,以便第二个决策树尝试降低误差率。我们按顺序重复此过程,直到达到训练期间设置的树的数量。在所有树都训练完成后,我们通过加权多数投票来得出最终预测。由于权重较高的决策树具有更高的预测能力,它在最终预测中具有更大的影响力。

图 3.7 AdaBoost 算法的示意图
梯度提升算法的工作原理略有不同,如图 3.8 所示。与 AdaBoost 一样,第一个决策树是在所有训练数据上训练的,但与 AdaBoost 不同,没有与数据点关联的权重。在训练第一个决策树后,我们计算一个残差误差指标,即实际目标和预测目标之间的差异。然后,我们训练第二个决策树来预测第一个决策树造成的残差误差。因此,与 AdaBoost 中更新每个数据点的权重不同,梯度提升直接预测残差误差。每个树的目标是修正前一个树的错误。这个过程按顺序重复,直到达到训练期间设置的树的数量。在所有树都训练完成后,我们通过求和所有树的预测来得出最终预测。
如前所述,我们将重点关注随机森林算法,但用于训练、评估和解释算法的方法也可以扩展到提升技术。

图 3.8 梯度提升算法的示意图
3.2.1 训练随机森林
现在让我们训练我们的随机森林模型来预测高中生的表现。以下代码片段显示了在训练模型之前如何准备数据。请注意,当将数据分为训练集和测试集时,20%的数据用于测试。其余数据用于训练和验证。此外,我们对数学目标变量进行分层抽样,以确保训练集和测试集的分数分布相同。您也可以使用阅读和写作分数轻松创建类似的分割:
import pandas as pd
from sklearn.preprocessing import LabelEncoder
# Load the data
df = pd.read_csv('data/StudentsPerformance.csv') ①
# First, encode the input features
gender_le = LabelEncoder() ②
race_le = LabelEncoder() ②
parent_le = LabelEncoder() ②
lunch_le = LabelEncoder() ②
test_prep_le = LabelEncoder() ②
df['gender_le'] = gender_le.fit_transform(df['gender']) ③
df['race_le'] = race_le.fit_transform(df['race/ethnicity']) ③
df['parent_le'] = parent_le.fit_transform(df['parental level of education']) ③
df['lunch_le'] = lunch_le.fit_transform(df['lunch']) ③
df['test_prep_le'] = test_prep_le.fit_transform(df['test preparation ③
➥ course']); ③
# Next, encode the target variables
math_grade_le = LabelEncoder() ④
reading_grade_le = LabelEncoder() ④
writing_grade_le = LabelEncoder() ④
df['math_grade_le'] = math_grade_le.fit_transform(df['math grade']) ⑤
df['reading_grade_le'] = reading_grade_le.fit_transform(df['reading grade']) ⑤
df['writing_grade_le'] = writing_grade_le.fit_transform(df['writing grade']) ⑤
# Creating training/val/test sets
df_train_val, df_test = train_test_split(df, test_size=0.2, ⑥
stratify=df['math_grade_le'], #F shuffle=True, random_state=42) ⑥
feature_cols = ['gender_le', 'race_le',
'parent_le', 'lunch_le',[CA] 'test_prep_le']
X_train_val = df_train_val[feature_cols] ⑦
X_test = df_test[feature_cols] ⑦
y_math_train_val = df_train_val['math_grade_le'] ⑧
y_reading_train_val = df_train_val['reading_grade_le'] ⑧
y_writing_train_val = df_train_val['writing_grade_le'] ⑧
y_math_test = df_test['math_grade_le'] ⑧
y_reading_test = df_test['reading_grade_le'] ⑧
y_writing_test = df_test['writing_grade_le'] ⑧
① 将数据加载到 Pandas DataFrame 中
② 因为输入特征是文本和分类的,我们需要将它们编码成数值。
③ 将输入特征拟合并转换为数值
④ 初始化目标变量的 LabelEncoders,因为字母成绩需要转换为数值
⑤ 将目标变量拟合并转换为数值
⑥ 将数据分割为训练/验证集和测试集
⑦ 提取训练/验证集和测试集的特征矩阵
⑧ 提取训练/验证集和测试集中的数学、阅读和写作的目标向量
一旦您准备好了数据,现在就可以训练数学、阅读和写作的三个随机森林模型,如下代码示例所示。请注意,我们可以使用交叉验证来确定随机森林分类器的最佳参数。此外,请注意,随机森林算法首先随机选取训练数据的子集来训练每个决策树,对于每个决策树,模型随机选取特征子集来分割数据。对于算法中的这两个随机元素,设置随机数生成器的种子(使用random_state参数)非常重要。如果没有设置这个种子,您将无法获得可重复和一致的结果。首先,让我们使用一个辅助函数来创建一个具有预定义参数的随机森林模型:
from sklearn.ensemble import RandomForestClassifier
def create_random_forest_model(n_estimators, ①
max_depth=10, ②
criterion='gini', ③
random_state=42, ④
n_jobs=4): ⑤
return RandomForestClassifier(n_estimators=n_estimators,
max_depth=max_depth,
criterion=criterion,
random_state=random_state,
n_jobs=n_jobs)
① 设置随机森林中的决策树数量
② 决策树的最大深度参数
③ 使用基尼不纯度作为优化每个决策树的代价函数
④ 为了可重复性,设置随机数生成器的种子
⑤ 将 n_jobs 设置为并行训练单个决策树,使用您计算机上所有可用的核心
现在让我们使用这个辅助函数来初始化和训练数学、阅读和写作三个随机森林模型,如下所示:
math_model = create_random_forest_model(50) ①
math_model.fit(X_train_val, y_math_train_val) ②
y_math_model_test = math_model.predict(X_test) ③
reading_model = create_random_forest_model(25) ④
reading_model.fit(X_train_val, y_reading_train_val) ④
y_reading_model_test = reading_model.predict(X_test) ④
writing_model = create_random_forest_model(40) ⑤
writing_model.fit(X_train_val, y_writing_train_val) ⑤
y_writing_model_test = writing_model.predict(X_test) ⑤
① 使用 50 个决策树初始化数学模型的随机森林分类器
② 使用数学成绩作为目标,在训练数据上拟合数学学生表现分类器
③ 使用预训练模型预测测试集中所有学生的数学成绩
④ 使用 25 个决策树初始化和训练随机森林分类器以预测阅读成绩
⑤ 使用 40 个决策树初始化和训练随机森林分类器以预测写作成绩
现在我们已经训练了数学、阅读和写作的三个随机森林模型,让我们评估它们,并将它们与一个总是预测所有科目大多数成绩(在这种情况下,为 B)的基线模型进行比较。通常用于分类问题的指标是准确率。然而,这个指标在类别不平衡的情况下并不适用。在我们的案例中,我们已经看到学生成绩相当不平衡,如图 3.3 所示。例如,如果 98%的学生在数学中获得 B 级成绩,你可以通过总是预测所有学生的 B 级成绩来欺骗自己构建一个具有 98%准确率的高度准确模型。为了衡量模型在所有类别上的性能,我们可以使用更好的指标,如精确度、召回率和 F1。精确度是一个衡量预测类别准确比例的指标。召回率是一个衡量模型准确预测的实际类别比例的指标。精确度和召回率的公式如下:


完美分类器的精确度和召回率均为 1,因为假阳性和假阴性的数量将为 0。但在实践中,这两个指标是相互矛盾的——你总是在假阳性和假阴性之间做出权衡。随着你减少假阳性并提高精确度,成本将增加假阴性和降低召回率。为了在精确度和召回率之间找到正确的平衡,我们可以将这两个指标结合成一个称为 F1 的分数。F1 分数是精确度和召回率的调和平均值,如下所示:

表 3.1 显示了所有三个模型的性能。它们与每个科目的基线进行比较,以查看新模型提供了多少性能改进。校长使用的一个合理的基线是预测每个科目的大多数成绩(在这种情况下,为 B)。
表 3.1 数学、阅读和写作模型的性能
| 精确度(%) | 召回率(%) | F1 分数(%) | |
|---|---|---|---|
| 数学基线 | 23 | 49 | 32 |
| 数学模型 | 39 | 41 | 39 |
| 阅读基线 | 24 | 49 | 32 |
| 阅读模型 | 39 | 43 | 41 |
| 写作基线 | 18 | 43 | 25 |
| 写作模型 | 44 | 45 | 41 |
在性能方面,我们可以看到,在精确度和 F1 方面,数学和阅读随机森林模型优于基线模型。然而,在召回率方面,基线数学和阅读模型在随机森林模型中表现更好。因为基线模型总是预测多数类,所以它总是正确地预测所有多数类。但是,精确度指标和 F1 为我们提供了衡量所有预测准确性的更好指标。写作主题领域的随机森林模型在所有三个指标上都优于基线模型。校长对这种性能提升感到满意,但现在想了解模型是如何得出预测的。在第 3.3 节和第 3.4 节中,我们将看到如何解释随机森林模型。
训练 AdaBoost 和梯度提升树
我们可以使用 Scikit-Learn 提供的AdaBoostClassifier类来训练 AdaBoost 分类器。在 Python 中初始化 AdaBoost 分类器如下:
from sklearn.ensemble import AdaBoostClassifier
math_adaboost_model = AdaBoostClassifier(n_estimators=50)
我们使用 Scikit-Learn 提供的GradientBoostingClassifier类来训练梯度提升树分类器,如下所示:
from sklearn.ensemble import GradientBoostingClassifier
math_gbt_model = GradientBoostingClassifier(n_estimators=50)
我们以与随机森林分类器相同的方式进行模型训练。梯度提升树的变体(如 CatBoost 和 XGBoost)更快且可扩展。作为练习,尝试为所有三个主题领域训练 AdaBoost 和梯度提升分类器,并将你的结果与随机森林模型进行比较。
3.3 随机森林的解释
随机森林是由多个决策树组成的集成,因此我们可以通过平均所有决策树中每个特征的归一化特征重要性来查看每个特征的全球相对重要性。在第二章中,我们看到了如何计算决策树的特征重要性。以下是一个给定决策树t的示例:

为了计算相对重要性,我们需要通过除以所有特征重要性值的总和来归一化之前显示的特征重要性,如下所示:

现在,你可以轻松地通过平均所有决策树中该特征的归一化特征重要性来计算随机森林中每个特征的全球相对重要性,如下所示。请注意,特征重要性在 AdaBoost 和梯度提升树中是按相同方式计算的:

在 Python 中,我们可以从 Scikit-Learn 的随机森林模型中获取特征重要性,并如下所示进行绘图:
math_fi = math_model.feature_importances_ * 100 ①
reading_fi = reading_model.feature_importances_ * 100 ②
writing_fi = writing_model.feature_importances_ * 100 ③
feature_names = ['Gender', 'Ethnicity', 'Parent Level of Education',
➥ 'Lunch', 'Test Preparation'] ④
# Code below plots the relative feature importance
# of the math, reading and writing random forest models
fig, ax = plt.subplots()
index = np.arange(len(feature_names))
bar_width = 0.2
opacity = 0.9
error_config = {'ecolor': '0.3'}
ax.bar(index, math_fi, bar_width,
alpha=opacity, color='r',
label='Math Grade Model')
ax.bar(index + bar_width, reading_fi, bar_width,
alpha=opacity, color='g',
label='Reading Grade Model')
ax.bar(index + bar_width * 2, writing_fi, bar_width,
alpha=opacity, color='b',
label='Writing Grade Model')
ax.set_xlabel('')
ax.set_ylabel('Feature Importance (%)')
ax.set_xticks(index + bar_width)
ax.set_xticklabels(feature_names)
for tick in ax.get_xticklabels():
tick.set_rotation(90)
ax.legend(loc='center left', bbox_to_anchor=(1, 0.5))
ax.grid(True);
① 获取数学随机森林模型的特征重要性
② 获取阅读随机森林模型的特征重要性
③ 获取写作随机森林模型的特征重要性
④ 初始化特征名称列表
特征及其重要性值显示在图 3.9 中。如图所示,对于三个主题来说,最重要的两个特征是父母的受教育程度和学生的种族。这是有用的信息,但它并没有告诉我们关于成绩如何受不同教育水平的影响,以及种族和教育如何相互作用的任何信息。

图 3.9 随机森林模型的特征重要性
此外,我们可以轻松地计算和可视化树集成特征的重要性,但当我们将目光转向神经网络和更复杂的黑盒模型时,这会变得非常困难,这一点在第四章中会更加明显。因此,我们需要考虑对黑盒模型类型无差别的可解释性技术。以下章节将介绍这些模型无关的方法。
3.4 模型无关方法:全局可解释性
到目前为止,我们一直在关注特定模型或依赖的可解释性技术。对于白盒模型,我们看到了如何使用最小二乘法学习到的权重来解释线性回归模型。我们通过将决策树可视化为一棵二叉树来解释它们,其中每个节点使用 CART 算法确定的特征来分割数据。我们还能够可视化特征的全局重要性,其计算对模型是特定的。我们通过可视化单个特征对目标变量的平均基函数效应,然后对其他特征进行边缘化来解释 GAMs。这些可视化被称为部分依赖图或部分效应图。
对于像树集成这样的黑盒模型,我们可以计算特征的全球相对重要性,但不能将这种计算扩展到其他黑盒模型,如神经网络。为了更好地解释黑盒模型,我们现在将探索可以应用于任何类型模型的模型无关方法。在本章中,我们还将关注全局范围内的可解释性技术。全局可解释性技术旨在更好地理解模型的整体,即特征对目标变量的全局影响。一个全局可解释的模型无关方法是部分依赖图(PDPs)。你将在下一节中看到如何将你在第二章中学到的用于 GAMs 的部分依赖图扩展到像随机森林这样的黑盒模型。我们将正式定义 PDPs,并了解如何将 PDPs 扩展以可视化任意两个特征之间的交互,以验证模型是否捕捉到了它们之间的任何依赖关系。
模型无关的可解释性技术也可以是局部的。我们可以使用这些技术来解释给定局部实例或预测的模型。例如,LIME、SHAP 和锚定技术是模型无关且局部的,你将在第四章中了解更多关于它们的内容。
3.4.1 偏依赖图
正如我们在第二章中看到的 GAMs 一样,偏依赖图(PDPs)背后的思想是展示不同特征值对模型预测的边缘或平均效应。设 f 为模型学习到的函数。对于高中学生预测问题,设 f[math]、f[reading] 和 f[writing] 分别为为数学、阅读和写作学科领域训练的随机森林模型学习到的函数。对于每个学科,该函数返回给定输入特征下获得一定等级的概率。现在让我们专注于数学随机森林模型以便于理解。你可以轻松地将你现在学到的理论扩展到其他学科领域。
假设对于数学随机森林模型,我们想了解不同的父母教育水平对预测给定等级有什么影响。为了实现这一点,我们需要做以下几步:
-
使用与数据集中相同的值来设置其余特征。
-
通过将父母教育水平设置为所有数据点的感兴趣值来创建一个人工数据集——如果你对查看高中教育对学生成绩的平均效应感兴趣,那么将父母教育水平设置为所有数据点的高中教育。
-
运行模型,并获取这个人工集中所有数据点的预测。
-
取预测的平均值以确定该父母教育水平的整体平均效应。
更正式地说,如果我们想绘制特征 S 的偏依赖图,我们就在其他特征(表示为集合 C)上求边缘,将特征 S 设置为感兴趣的值,然后观察数学模型对特征 S 的平均影响,假设集合 C 中所有特征的值都是已知的。让我们来看一个具体的例子。假设我们想了解父母教育水平对学生数学成绩的边缘效应。在这种情况下,特征 S 是父母教育水平,其余的特征表示为 C。为了了解,比如说,高中教育水平的影响,我们将特征 S 设置为对应高中教育的值(感兴趣的值),并取数学模型输出的平均值,假设我们知道其余特征的值。数学上,这可以通过以下方程表示:

在这个方程中,特征 S 的偏函数是通过计算学习函数 f[math] 的平均值得到的,假设训练集中所有示例的特征 C 的值都是已知的,表示为 n。
需要注意的是,如果特征 S 与集合 C 中的特征相关联,则 PDP(部分依赖图)不可信。为什么是这样呢?为了确定特征 S 的给定值的平均效应,我们正在创建一个人工数据集,其中我们使用集合 C 中所有其他特征的原始特征值,但将特征 S 的值更改为感兴趣的值。如果特征 S 与集合 C 中的任何特征高度相关,我们可能会创建一个极不可能的人工数据集。让我们看一个具体的例子。假设我们感兴趣的是了解学生家长的高中教育水平对其成绩的平均影响。我们将设置训练集中所有实例的家长教育水平为高中。现在,如果家长教育水平与种族高度相关,我们知道给定种族的家长教育水平,那么我们可能会遇到一个不太可能的情况,即属于某个种族的家长只有高中教育。因此,我们正在创建一个与原始训练数据分布不匹配的人工数据集。由于模型尚未接触到该数据分布,该模型的预测可能会非常不准确,导致不可信的 PDP。我们将在第 3.4.2 节中回到这个限制。
现在我们来看看如何实现 PDP。在 Python 中,您可以使用 Scikit-Learn 提供的实现,但这将限制您使用梯度提升回归器或分类器。Python 中更好的实现是 PDPBox,由 Jiangchun Lee 开发。您可以通过以下方式安装此库:
pip install pdpbox
现在我们来看看 PDP 的实际应用。我们首先关注最重要的特征,即您在第 3.3 节中学到的家长教育水平(见图 3.9)。我们可以查看不同教育水平对预测数学成绩 A、B、C 和 F 的影响如下:
from pdpbox import pdp ①
feature_cols = ['gender_le', 'race_le', 'parent_le', 'lunch_le',
➥ 'test_prep_le'] ②
pdp_education = pdp.pdp_isolate(model=math_model, ③
dataset=df, ④
model_features=feature_cols,
feature='parent_le') ⑤
ple_xticklabels = ['High School', ⑥
'Some High School', ⑥
'Some College', ⑥
"Associate\'s Degree", ⑥
"Bachelor\'s Degree", ⑥
"Master\'s Degree"] ⑥
# Parameters for the PDP Plot
plot_params = {
# plot title and subtitle
'title': 'PDP for Parent Level Educations - Math Grade',
'subtitle': 'Parent Level Education (Legend): \n%s' % (parent_title),
'title_fontsize': 15,
'subtitle_fontsize': 12,
# color for contour line
'contour_color': 'white',
'font_family': 'Arial',
# matplotlib color map for interact plot
'cmap': 'viridis',
# fill alpha for interact plot
'inter_fill_alpha': 0.8,
# fontsize for interact plot text
'inter_fontsize': 9,
}
# Plot PDP of parent level of education in matplotlib
fig, axes = pdp.pdp_plot(pdp_isolate_out=pdp_education,
feature_name='Parent Level Education',
center=True, x_quantile=False, ncols=2,
plot_lines=False, frac_to_plot=100,
plot_params=plot_params, figsize=(18, 25))
axes['pdp_ax'][0].set_xlabel('Parent Level Education')
axes['pdp_ax'][1].set_xlabel('Parent Level Education')
axes['pdp_ax'][2].set_xlabel('Parent Level Education')
axes['pdp_ax'][3].set_xlabel('Parent Level Education')
axes['pdp_ax'][0].set_title('Grade A')
axes['pdp_ax'][1].set_title('Grade B')
axes['pdp_ax'][2].set_title('Grade C')
axes['pdp_ax'][3].set_title('Grade F')
axes['pdp_ax'][0].set_xticks(parent_codes)
axes['pdp_ax'][1].set_xticks(parent_codes)
axes['pdp_ax'][2].set_xticks(parent_codes)
axes['pdp_ax'][3].set_xticks(parent_codes)
axes['pdp_ax'][0].set_xticklabels(ple_xticklabels)
axes['pdp_ax'][1].set_xticklabels(ple_xticklabels)
axes['pdp_ax'][2].set_xticklabels(ple_xticklabels)
axes['pdp_ax'][3].set_xticklabels(ple_xticklabels)
for tick in axes['pdp_ax'][0].get_xticklabels():
tick.set_rotation(45)
for tick in axes['pdp_ax'][1].get_xticklabels():
tick.set_rotation(45)
for tick in axes['pdp_ax'][2].get_xticklabels():
tick.set_rotation(45)
for tick in axes['pdp_ax'][3].get_xticklabels():
tick.set_rotation(45)
① 从 PDPBox 导入 PDP 函数
② 仅提取标签编码的特征列
③ 通过传递学习到的数学随机森林模型,为每个教育水平获取部分依赖函数
④ 使用预加载数据集
⑤ 除了家长教育水平之外,对其他所有特征进行边缘化
⑥ 从最低教育水平开始初始化 xticks 的标签,直到最高水平
该代码片段生成的图示如图 3.10 所示。家长教育水平的部分依赖图分别显示在每个等级——A、B、C 和 F 上。部分依赖函数的值域在 0 到 1 之间,因为该分类器的学习数学模型 f unction 是一个范围在 0 到 1 之间的概率度量。现在让我们放大几个等级来分析家长教育水平对学生成绩的影响。

图 3.10 各种家长教育水平特征对数学成绩 A、B、C 和 F 的 PDP
在图 3.11 中,我们放大了数学成绩 A 的 PDP。我们在 3.1.1 节中看到,当父母拥有硕士学位时,数学成绩 A 的学生比例高于父母拥有高中文凭时(见图 3.4)。随机森林模型是否学习了这种模式?从图 3.11 中我们可以看到,随着父母受教育程度的提高,获得成绩 A 的影响也在增加。对于受过高中教育的父母,预测数学成绩 A 的影响可以忽略不计——接近 0。这意味着高中教育对模型没有任何影响,并且在预测成绩 A 时,除了父母的受教育程度之外的其他特征开始发挥作用。然而,当父母拥有硕士学位时,我们可以看到大约+0.2 的高正影响。这意味着平均而言,硕士学位将学生获得成绩 A 的概率提高了大约 0.2。

图 3.11 解释数学成绩 A 的父母的受教育程度 PDP
在图 3.12 中,我们放大了数学成绩 F 的 PDP。我们可以注意到成绩 F 呈下降趋势——父母受教育程度越高,对预测成绩 F 的负面影响越大。我们可以看到,父母拥有硕士学位的学生在预测成绩 F 时平均有大约-0.05 的负面影响。这意味着拥有硕士学位的父母降低了学生获得成绩 F 的可能性,因此增加了学生获得成绩 A 的可能性。这个见解很棒,仅通过查看特征重要性是无法实现的。该系统的最终用户(即校长)将对她所使用的模型有更多的信任。

图 3.12 解释数学成绩 F 的父母的受教育程度 PDP
作为一项练习,我鼓励你将数学成绩和父母受教育程度的 PDP 代码扩展到其他学科领域——阅读和写作。你可以检查在 3.1.1 节中观察到的模式是否被随机森林模型学习(见图 3.4)。你还可以将代码扩展到其他特征。作为练习,选择第二重要的特征,即学生的种族或民族,并生成该特征的 PDP。
3.4.2 特征交互
我们可以将 PDPs 扩展以理解特征交互。回到 3.4.1 节中的方程,我们现在将关注集合 S 中的两个特征,并对其他特征进行边缘化。让我们检查两个最重要的特征——父母的受教育程度和学生的种族——在预测数学成绩 A、B、C 和 F 之间的交互作用。使用 PDPBox,我们可以轻松地可视化成对的特征交互,如下面的代码片段所示:
pdp_race_parent = pdp.pdp_interact(model=math_model, ①
dataset=df, ②
model_features=feature_cols, ③
features=['race_le', 'parent_le']) ④
# Parameters for the Feature Interaction plot
plot_params = {
# plot title and subtitle
'title': 'PDP Interaction - Math Grade',
'subtitle': 'Race/Ethnicity (Legend): \n%s\nParent Level of Education
➥ (Legend): \n%s' % (race_title, parent_title),
'title_fontsize': 15,
'subtitle_fontsize': 12,
# color for contour line
'contour_color': 'white',
'font_family': 'Arial',
# matplotlib color map for interact plot
'cmap': 'viridis',
# fill alpha for interact plot
'inter_fill_alpha': 0.8,
# fontsize for interact plot text
'inter_fontsize': 9,
}
# Plot feature interaction in matplotlib
fig, axes = pdp.pdp_interact_plot(pdp_race_parent, [CA]['Race/Ethnicity',
➥ 'Parent Level of Education'],
plot_type='grid', plot_pdp=True,
➥ plot_params=plot_params)
axes['pdp_inter_ax'][0]['_pdp_x_ax'].set_xlabel('Race/Ethnicity (Grade A)')
axes['pdp_inter_ax'][1]['_pdp_x_ax'].set_xlabel('Race/Ethnicity (Grade B)')
axes['pdp_inter_ax'][2]['_pdp_x_ax'].set_xlabel('Race/Ethnicity (Grade C)')
axes['pdp_inter_ax'][3]['_pdp_x_ax'].set_xlabel('Race/Ethnicity (Grade F)')
axes['pdp_inter_ax'][0]['_pdp_x_ax'].grid(False)
① 获取学习数学随机森林模型中两个特征之间的特征交互
② 使用预加载的数据集
③ 设置特征列名称
④ 获取特征交互的特定期表
由该代码生成的图示显示在图 3.13 中。共生成了四个图,每个图对应一个等级。特征交互在二维网格中可视化,其中六个家长教育水平特征位于y轴上,五个族裔特征位于x轴上。我将放大查看等级 A,进一步分解和解释此图。

图 3.13 家长教育水平和族裔对所有数学等级 A、B、C 和 F 的交互
图 3.14 显示了数学等级 A 的特征交互图。家长教育水平位于y轴上,学生的匿名族裔位于x轴上。从y轴的底部到顶部,家长教育水平从高中一直增加到硕士学位。高中教育用 0 表示,硕士学位用 5 表示。x轴显示了五个不同的族裔群体——A、B、C、D 和 E。族裔群体 A 用 0 表示,群体 B 用 1 表示,群体 C 用 2 表示,依此类推。每个单元格中的数字表示给定家长教育水平和学生族裔对获得等级 A 的影响。

图 3.14 放大查看数学等级 A 并分解特征交互图
例如,最底部行和最左侧列的单元格表示属于族裔群体 A 且家长拥有高中教育背景的学生在获得等级 A 上的平均影响。请注意网格中每个单元格的数值——数字越低表示影响越低,数字越高表示在预测等级 A 上的影响越高。
现在,让我们专注于族裔群体 A,它是网格中最左侧的列,如图 3.15 所示。您可以看到,随着家长教育水平的提高,预测等级 A 的影响也增加。这很有道理,因为它表明家长教育水平对等级的影响大于族裔。这一点也由图 3.9 中显示的特征重要性图得到验证。因此,模型已经很好地学习了这种模式。

图 3.15 通过对族裔群体 A 进行条件预测等级 A 的影响
但族裔群体 C,即图 3.16 中突出显示的第三列,发生了什么?看起来一个家长拥有高中学位的学生在预测等级 A 时比一个家长拥有硕士学位的学生有更高的积极影响(比较突出列的最底部单元格与最顶部单元格)。看起来一个家长拥有副学士学位的学生在预测等级 A 时比其他任何教育水平都有更高的积极影响(参见突出列中从顶部数起的第三个单元格)。

图 3.16 在种族组 C 的条件下预测 A 等级的影响
这有点令人担忧,因为它可能暴露以下一个或多个问题:
-
父母教育水平可能与种族特征相关,因此导致不可信的特征交互图。
-
数据集没有正确地代表人口,特别是种族组 C。这被称为抽样偏差。
-
模型是有偏见的,并且没有很好地学习父母教育水平与种族之间的交互。
-
数据集揭示了社会中系统性的偏差。
第一个问题揭示了 PDPs 的限制,我们将在下一段中讨论这个限制。第二个问题可以通过收集更多代表人口的代表性数据来解决。你将在第八章中了解其他形式的偏差以及如何减轻它们。第三个问题可以通过添加或工程更多特征,或者通过训练一个更好、更复杂的模型来解决。最后一个问题要难得多,需要更好的政策和法律,这超出了本书的范围。
为了检查第一个问题是否存在,让我们看看父母教育水平与种族之间的相关性。我们在第二章中介绍了如何计算和可视化相关性矩阵。我们使用皮尔逊相关系数来量化该问题中特征之间的相关性。这个系数只能用于数值特征,而不能用于分类特征。因为我们在这个例子中处理的是分类特征,所以我们必须使用不同的指标。我们可以使用Cramer's V 统计量,因为它衡量的是两个分类变量之间的关联。这个统计量可以在 0 到 1 之间,其中 0 表示没有相关性/关联,1 表示最大相关性/关联。以下辅助函数可以用来计算这个统计量:
import scipy.stats as ss
def cramers_corrected_stat(confusion_matrix):
""" Calculate Cramers V statistic for categorial-categorial association.
uses correction from Bergsma and Wicher,
Journal of the Korean Statistical Society 42 (2013): 323-328
"""
chi2 = ss.chi2_contingency(confusion_matrix)[0]
n = confusion_matrix.sum().sum()
phi2 = chi2/n
r,k = confusion_matrix.shape
phi2corr = max(0, phi2 - ((k-1)*(r-1))/(n-1))
rcorr = r - ((r-1)**2)/(n-1)
kcorr = k - ((k-1)**2)/(n-1)
return np.sqrt(phi2corr / min( (kcorr-1), (rcorr-1)))
我们可以如下计算父母教育水平与种族之间的相关性:
confusion_matrix = pd.crosstab(df['parental level of education'],
df['race/ethnicity'])
print(cramers_corrected_stat(confusion_matrix))
通过执行这些代码行,我们可以看到父母教育水平与种族之间的相关性或关联为0.0486。这相当低,因此我们可以排除特征交互图或 PDP 不可信的问题。
我们在图 3.5 中看到,属于 C 组的学生的表现总体上优于属于 A 组的学生的表现。可能的情况是模型已经学会了这种模式。我们可以通过查看图 3.14、3.15 和 3.16 中最上面的图例来验证它。如果学生属于 C 组,对预测 A 等级有+0.153 的积极影响,这比学生属于 A 组时的+0.125 的影响要大。现在让我们来看看图 3.17 中显示的 A 族和 C 族之间父母教育水平的分布差异。

图 3.17 总体人口与种族群体 A 和 C 的家长教育水平分布比较
在图 3.17 中,我们可以看到,属于种族群体 A 的学生家长比总体人口和属于群体 C 的学生家长更有可能拥有高中或某些高中教育。看起来群体 C 的学生家长拥有副学士学位的比例也比总体人口和群体 A 高。分布的差异非常明显。我们不确定数据集是否准确地代表了总体人口和每个种族群体。作为数据科学家,向利益相关者(例如本例中的校长)强调这个问题,并确保数据集是合法的,且没有抽样偏差,这一点非常重要。
从本节中可以吸取的重要观点是,可解释性技术,尤其是 PDPs 和特征交互,是暴露模型或数据潜在问题的强大工具,在模型部署到生产之前。本节中的所有见解仅通过查看特征重要性是无法实现的。作为练习,我鼓励您使用 PDPBox 包在其他黑盒模型上,例如梯度提升树。
累积局部效应(ALE)
在本章中,我们已经看到,如果特征之间存在相关性,基于它们的 PDPs(部分依赖图)和特征交互图就不可信。一种无偏且克服 PDPs 局限性的可解释性技术是累积局部效应(ALE)。这项技术由 Daniel W. Apley 和 Jingyu Zhu 于 2016 年提出。在撰写本文时,ALE 仅在 R 编程语言中得到实现。Python 实现仍在进行中,并且目前尚不支持分类特征。由于 ALE 的实现还不够成熟,我们将在本书的后续版本中更深入地介绍这项技术。
在下一章中,我们将进入黑盒神经网络的世界。这可能看起来是一个相当大的跳跃,因为神经网络本质上是复杂的,因此需要更复杂的可解释性技术来理解它们。我们将特别关注局部范围内的模型无关技术,例如 LIME、SHAP 和锚点。
摘要
-
模型无关的可解释性技术不依赖于所使用的特定模型类型。由于它们独立于模型的内部结构,因此可以应用于任何模型。
-
范围全局的可解释性技术将帮助我们理解整个模型。
-
为了克服过拟合的问题,我们可以以两种广泛的方式结合或集成决策树:袋装和提升。
-
使用袋装技术,我们在训练数据的独立随机子集上并行训练多个决策树。我们使用这些单独的决策树进行预测,并通过取平均值来得出最终的预测。随机森林是一种使用袋装技术的树集成。
-
与袋装技术类似,提升技术也训练多个决策树,但顺序不同。第一个决策树通常是浅层树,并在训练集上训练。第二个决策树的目的是从第一个决策树的错误中学习,并进一步提高性能。使用这种技术,我们将多个决策树串联起来,它们迭代地尝试优化和减少前一个决策树所犯的错误。自适应提升和梯度提升是两种常见的提升算法。
-
我们可以使用 Scikit-Learn 包提供的
RandomForestClassifier类在 Python 中为分类任务训练随机森林模型。这种实现还将帮助您轻松计算特征的全球相对重要性。 -
我们可以通过使用 Scikit-Learn 的
AdaBoostClassifier和GradientBoostingClassifier类分别训练 Scikit-Learn 的自适应提升和梯度提升分类器。梯度提升树的变体更快且可扩展,例如CatBoost和XGBoost。 -
对于树集成,我们可以计算特征的全球相对重要性,但不能将这种计算扩展到其他黑盒模型,如神经网络。
-
部分依赖图(PDP)是一种全局、模型无关的可解释技术,我们可以用它来理解不同特征值对模型预测的边缘或平均效应。如果特征之间存在相关性,则 PDPs 不可信。我们可以使用
PDPBoxPython 包来实现 PDPs。 -
PDPs 可以扩展以理解特征交互。PDPs 和特征交互图可以用来揭示可能的问题,如采样偏差和模型偏差。
4 模型无关方法:局部可解释性
本章涵盖了
-
深度神经网络的特性
-
如何实现本质上是黑盒模型的深度神经网络
-
基于扰动的局部范围模型无关方法,如 LIME、SHAP 和锚点
-
如何使用 LIME、SHAP 和锚点来解释深度神经网络
-
LIME、SHAP 和锚点的优缺点
在上一章中,我们探讨了树集成,特别是随机森林模型,并学习了如何使用全局范围内的模型无关方法来解释它们,例如部分依赖图(PDPs)和特征交互图。我们看到 PDPs 是理解单个特征值如何在全球范围内影响最终模型预测的绝佳方式。我们还能够通过特征交互图看到特征如何相互作用,以及它们如何被用来揭示潜在的偏差等问题。PDPs 易于理解且直观,但它们的最大缺点是假设特征之间是相互独立的。此外,使用特征交互图无法可视化高阶特征交互。
在本章中,我们将探讨黑盒神经网络,特别是深度神经网络(DNNs)。这些模型本质上是复杂的,需要更高级的解释技术来理解它们。我们将特别关注局部可解释模型无关解释(LIME)、SHapley Additive exPlanations(SHAP)和锚点等技术。与 PDPs 和特征交互图不同,这些技术是局部的。这意味着我们可以使用它们来解释单个实例或预测。
我们将遵循与前面章节相似的结构。我们从一个具体的例子开始,目标是构建一个用于乳腺癌诊断的模型。我们将探索这个新的数据集,并学习如何在 PyTorch 中训练和评估深度神经网络(DNNs)。然后我们将学习如何解释它们。值得重申的是,尽管本章的主要重点是解释 DNNs,我们还将涵盖 DNNs 的基本概念以及如何训练和测试它们。由于学习、测试和理解阶段是迭代的,因此同时涵盖这三个方面非常重要。我们还将在前面的部分中介绍一些关键见解和概念,这些在模型解释过程中将非常有用。对于已经熟悉 DNNs 以及如何训练和测试它们的读者,可以自由跳过前面的部分,直接跳到第 4.4 节,其中我们将涵盖模型可解释性。
4.1 诊断+ AI:乳腺癌诊断
让我们来看一个具体的例子。我们将回到第一章和第二章中介绍的 Diagnostics+。中心希望扩展其 AI 能力以诊断乳腺癌,并已将约 570 名患者的乳腺肿块细针穿刺图像进行了数字化。从这些数字化图像中计算了描述图像中存在的细胞核特征的特性。对于每个细胞核,以下 10 个特征被用来描述其特征:
-
半径
-
纹理
-
周长
-
面积
-
平滑度
-
紧凑度
-
凹凸性
-
凹点
-
对称性
-
分形维度
对于患者图像中存在的所有核,计算每个这些 10 个特征的均值、标准误差以及最大或最差值。因此,每位患者总共拥有 30 个特征。给定这些输入特征,AI 系统的目标是预测细胞是良性还是恶性,并为医生提供信心分数以帮助他们诊断。这总结在图 4.1 中。

图 4.1 乳腺癌诊断的 Diagnostics+ AI
根据这些信息,你将如何将这个问题表述为一个机器学习问题?因为模型的目的是预测给定的乳腺肿块是良性还是恶性,我们可以将这个问题表述为一个二元分类问题。
4.2 探索性数据分析
让我们现在尝试更好地理解这个数据集。探索性数据分析是模型开发过程中的一个重要步骤。我们将特别关注数据的体积、目标类别的分布,以及像细胞的面积、半径和周长这样的特征是否可以用来区分良性和恶性病例。我们将使用本节中获得的许多见解来确定应该使用哪些特征进行模型训练,应该使用哪些指标进行模型评估,以及如何验证我们将在本章后面介绍的技术的模型解释。
数据集包含 569 个患者案例和总共 30 个特征。所有特征都是连续的。图 4.2 显示了良性病例和恶性病例的案例比例。在 569 个案例中,357 个(大约 62.7%)是良性的,212 个(大约 37.3%)是恶性的。这表明数据集是倾斜的,或者说是不平衡的。正如我们在第三章中看到的,当我们对于一个给定类别的例子或数据点存在不成比例的数量时,我们说数据是不平衡的。大多数机器学习算法在每个类别的样本比例大致相同的情况下表现最佳。这是因为大多数算法旨在最小化错误或最大化准确性,这些算法倾向于自然地偏向多数类。为了总结,当处理不平衡数据集时,你应该注意以下两点:
-
在测试和评估模型时,使用正确的性能指标(如精确度、召回率和 F1)。
-
重新采样训练数据,使得多数类要么被欠采样,要么少数类被过采样。

图 4.2 良性和恶性病例的分布
我们将在 4.3.2 节中进一步讨论这个问题。现在,让我们看看细胞的面积、半径和周长的分布,看看良性和恶性病例之间是否有任何显著差异。图 4.3 显示了平均细胞面积和最差或最大细胞面积的分布。

图 4.3 比较良性和恶性病例的细胞面积分布
在图 4.3 中,我们可以看到,如果平均细胞面积大于 750,那么病例更有可能是恶性的而不是良性的。同样,如果最差或最大细胞面积大于 1,000,那么病例更有可能是恶性的。仅通过观察与细胞面积相关的两个特征,似乎恶性与良性病例之间有良好的但较弱地分离。
那么细胞的半径和周长呢?图 4.4 和图 4.5 分别显示了半径和周长的分布。我们可以看到良性和恶性病例之间有类似的分离。例如,平均半径大于 15 的病例比良性病例更有可能是恶性的。同样,周长最差或最大的细胞周长为 100 的病例更有可能是恶性的。

图 4.4 比较良性和恶性病例的细胞半径分布
本分析的目的在于了解特征在预测目标变量(即给定病例是良性还是恶性)方面的好坏。通过观察图 4.3、4.4 和 4.5 中的分布,我们可以看到我们考虑的六个特征中,良性和恶性病例之间有很好的分离信号。我们还将利用这些见解来验证本章后面通过 LIME、SHAP 和锚点获得的解释。

图 4.5 比较良性和恶性病例的细胞周长分布
最后,我们来探讨每个输入特征与彼此以及目标变量之间的相关性。我们知道输入特征是连续的,但目标变量是离散的二进制变量。在数据集中,恶性病例被编码为 0,良性病例被编码为 1。由于输入特征和目标都是数值型,我们可以使用皮尔逊相关系数,或称标准相关系数,来衡量相关性。正如我们在第二章中看到的,皮尔逊相关系数衡量两个变量之间的线性相关性,其值介于+1 和-1 之间。如果系数的绝对值大于 0.7,这意味着高度相关。如果系数的绝对值介于 0.5 和 0.7 之间,这意味着中等高度相关。如果系数的绝对值介于 0.3 和 0.5 之间,这意味着低度相关,而系数的绝对值小于 0.3 则意味着几乎没有相关性。你可以使用 Pandas 提供的corr()函数轻松计算成对的相关性。作为练习,请重新使用第 2.2 节中学到的代码来计算并绘制相关矩阵。加载数据集的代码可以在第 4.3.1 节中找到。乳腺癌数据集的结果图示如图 4.6 所示。

图 4.6 输入特征与目标变量的相关性图
在图 4.6 中,我们首先关注最后一列,它显示了所有输入特征与目标变量的相关性。我们可以看到,像平均细胞面积、半径和周长这样的特征与目标类别高度相关。然而,相关系数是负的,这意味着特征值越大,目标变量的值就越小。这是有道理的,因为目标类别对于恶性类别有较小的值(即,0),而对于良性类别有较高的值(即,1)。正如我们在图 4.3、4.4 和 4.5 中看到的,这些特征的值越大,病例为恶性的可能性就越大。我们还可以看到,相当多的特征彼此之间高度相关。例如,平均细胞半径、面积和周长等特征与最差细胞半径、面积和周长高度相关。正如我们在第二章中讨论的,彼此相关的特征被称为多重共线性,或冗余。处理多重共线性的一种方法是从模型中移除冗余特征。我们将在下一节中进一步讨论这个问题。
4.3 深度神经网络
人工神经网络(ANN)是一个旨在松散地模拟生物大脑的系统。它属于被称为深度学习的一类广泛的机器学习方法。基于 ANN 的深度学习的核心思想是从更简单的概念或特征构建复杂的概念或表示。ANN 通过将输入映射到输出学习一个复杂函数,它由许多简单的函数组成。在本章中,我们将重点关注由多层单元(或神经元)组成的 ANN,这些单元彼此完全互联。这些也被称为深度神经网络(DNN)、全连接神经网络(FCNN)或多层感知器(MLP)。在随后的章节中,我们将介绍卷积神经网络(CNN)和循环神经网络(RNN),这些是用于复杂计算机视觉和语言理解任务的更高级神经网络结构。
图 4.7 展示了由三种类型的层组成的一个简单 ANN:输入层、隐藏层和输出层。输入层充当数据的输入。输入层中的单元数量等于数据集中特征的数量。在图 4.7 中,我们仅考虑来自乳腺癌数据集的两个特征,即平均细胞半径和平均细胞面积。这就是为什么输入层中存在两个单元的原因。

图 4.7 人工神经网络的示意图
输入层随后连接到第一隐藏层中的所有单元。隐藏层根据其单元使用的激活函数对输入进行转换。在图 4.7 中,函数f用于表示隐藏层中所有单元的激活函数。同一层的单元通过边与另一层的单元连接。每条边都关联着一个权重,它定义了连接的单元之间的连接强度。请注意,偏置项也通过权重为 1 的边连接到隐藏层中的每个单元。在通过激活函数转换之前,对输入和偏置项进行加权求和。如果存在多个隐藏层,则称人工神经网络(ANN)为“深度”。因此,具有两个或更多隐藏层的 ANN 被称为深度神经网络(DNN)。
最终隐藏层的单元随后连接到输出层的单元。在图 4.7 中,输出层中存在一个单元,因为对于乳腺癌检测任务,我们有二元输出,即给定的细胞要么是恶性的,要么是良性的。输出层中的单元也有一个激活函数g,它将输入转换为输出预测。创建神经网络的一个挑战是确定神经网络的架构——网络应该有多深(隐藏层的数量)和多宽(每层的单元数量)。我们将在 4.4 节中简要讨论如何确定和解释神经网络的架构,并在后续章节中更详细地介绍,当我们查看卷积神经网络(CNNs)和循环神经网络(RNNs)时。
现在我们来看看输入数据是如何在通过人工神经网络(ANN)时转换为输出的。这被称为前向传播,如图 4.8 所示。输入数据被输入到输入层的单元中。两个特征输入单元的值表示为x1和x2。然后这些值通过隐藏层在网络中向前传播。在隐藏层的每个单元中,计算输入的加权和并通过激活函数传递。在图 4.8 中,隐藏层中的第一个单元计算输入x1和x2以及偏置项b1的加权和,以获得预激活值a1。然后这个值通过激活函数f得到f(a1)。在隐藏层的第二个单元中也发生类似的操作。请注意,隐藏层中的两个单元使用相同的激活函数。我们将在本节稍后更深入地讨论激活函数。

图 4.8 人工神经网络中的前向传播示意图
一旦我们计算了隐藏层单元的输出,这些输出随后被作为输入传递到下一层的单元。在下一幅图中,隐藏层中两个单元的输出被作为输入传递到输出层的一个单元。就像之前一样,首先确定输入的加权和以及偏置项,以获得预激活值a3。然后这个值通过激活函数g得到单元的输出作为g(a3)。这个最终单元的输出旨在作为目标变量y的估计,表示为ŷ。网络中所有边的权重在开始时都会随机初始化。
现在学习算法的目标是确定边的权重,或者单元之间连接的强度,使得输出预测尽可能接近目标变量的实际值。你是如何学习这些权重的呢?我们将应用在第二章中学到的相同技术来确定线性回归模型的权重——梯度下降。最优的权重集是那些最小化损失或损失函数的权重集。对于回归问题,一个常见的损失函数是预测输出和实际输出之间的平方误差或平方差。对于二元分类问题,一个常见的损失函数是对数损失或二元交叉熵(BCE)损失函数。
平方误差损失函数及其相对于预测输出的导数在以下方程中展示:


接下来展示了对数损失或 BCE 损失函数及其相对于预测输出的导数:


当损失函数的梯度为 0 或接近 0 时,称损失函数处于最小值(全局或局部)。对于线性回归或逻辑回归类型的问题,我们可以很容易地确定权重,因为权重的数量等于输入特征的数目(加上一个额外的偏置项)。另一方面,对于深度神经网络(DNN),权重的数量取决于网络的结构。随着我们向网络添加更多的单元和层,权重的数量可能会迅速增加。直接应用梯度下降算法在计算上是不切实际的。在 DNN 中确定这些权重的有效算法是反向传播。
之前看到的简单 ANN 结构的反向传播算法在图 4.9 中进行了说明。一旦我们评估了网络的前向传播后的输出,下一步就是计算损失函数或损失函数相对于预测输出的梯度。然后以相反的顺序访问节点,并传播一个错误信号,我们可以使用这个信号来计算网络中所有边的权重相对于梯度的值。让我们一步一步地通过从右到左解析图 4.9 来了解这个过程。

图 4.9 ANN 中错误信号反向传播的示意图
我们首先计算成本函数相对于预测输出变量的梯度。这如图 4.9 中的J’所示。然后,这个梯度通过输出层的单元以相反的方向传递。在输出层中,激活函数g的局部梯度被存储,这表示为g’。在正向传播过程中评估的预激活值a3也被存储。这些值用于计算单元的输出误差信号,表示为e1。如图 4.9 所示,计算值是损失函数的梯度乘以激活函数的局部梯度。使用微积分的术语,我们在这里应用链式法则来计算损失函数相对于输出层单元输入的梯度。这个误差信号e1随后传播到隐藏层的两个单元。然后重复这个过程来计算隐藏单元的输出误差信号。一旦误差信号通过网络传播并且我们到达输入层,我们可以通过将反向传播过程中通过它的误差信号乘以正向传播过程中通过它的值来计算梯度,这个梯度相对于每个边权重。多个在线资源和书籍对反向传播和数学概念进行了深入的解释。因此,我们将在本章中不深入探讨这些概念。
激活函数是神经网络中的一个重要特性。它决定了神经元是否应该被激活以及激活的程度。激活函数的特性是它可导(即存在一阶导数)且单调(即要么完全非递减,要么非递增)。在神经网络中常用的激活函数包括 Sigmoid 函数、双曲正切(tanh)函数和修正线性单元(ReLU),这些函数在表 4.1 中定义。
Sigmoid 激活函数通常用于分类器,因为函数的输出范围在 0 到 1 之间。在本章关于乳腺癌检测问题的讨论中,我们将使用 Sigmoid 函数作为输出层的激活函数g。双曲正切函数与 Sigmoid 函数具有相似的性质,但输出范围在-1 到 1 之间。Sigmoid 和双曲正切激活函数都存在梯度消失的问题。这是因为对于这两个函数,当输入值非常大或非常小时,梯度为 0(也称为饱和),如表 4.1 所示。
表 4.1 神经网络中常用的激活函数
| 激活函数 | 描述 |
|---|---|
| Sigmoid | Sigmoid 函数的定义如下: 函数的输出范围在 0 到 1 之间。它是可导的,并且如图所示是单调的。 |
| 双曲正切(tanh) | 双曲正切函数定义为如下: 函数的输出范围从 -1 到 1。它也是可微和单调的,如图所示。 |
| 线性整流单元(ReLU) | ReLU 函数定义为如下: 函数的输出范围从 0 到无穷大(取决于输入 x 的值)。它是可微和单调的,如图所示。![]() |
ReLUs 是神经网络中最广泛使用的激活函数,因为它们很好地处理了梯度消失问题。我们可以看到,当输入为负时,ReLU 的值为 0。这意味着如果具有 ReLU 激活函数的神经元的输入为负,那么该神经元的输出为 0,因此没有激活。只有具有非负输入的神经元才会被激活。因为不是所有神经元同时被激活,ReLU 激活函数在计算上更有效率。在实践中,为了简单起见,神经网络隐藏层的所有单元都使用相同的激活函数。
4.3.1 数据准备
现在让我们训练一个用于乳腺癌检测问题的深度神经网络(DNN)。我们将使用 PyTorch 来构建和训练网络。PyTorch 是一个库,它简化了在 Python 中构建神经网络的过程。PyTorch 因其易用性而受到研究人员和行业机器学习实践者的青睐。我们也可以使用其他库,如 TensorFlow 和 Keras 来构建神经网络,但在这本书中我们将专注于 PyTorch。因为这个库是 Pythonic 的,所以对于已经熟悉 Python 的数据科学家和工程师来说,使用它将更容易。要了解更多关于 PyTorch 的信息,请参阅附录 B。
在训练深度神经网络(DNN)之前的第一步是准备数据。以下代码展示了如何加载数据——将其分为训练集、验证集和测试集,然后将它们转换为网络 PyTorch 实现的输入:
import numpy as np ①
from sklearn.datasets import load_breast_cancer ②
from sklearn.model_selection import train_test_split ③
import torch ④
from torch.autograd import Variable ④
data = load_breast_cancer() ⑤
X = data['data'] ⑤
y = data['target'] ⑤
X_train, X_val, y_train, y_val = train_test_split(X, y, test_size=0.3,
➥ random_state=24) ⑥
X_val, X_test, y_val, y_test = train_test_split(X_val, y_val, test_size=0.5,
➥ random_state=24) ⑦
X_train = Variable(torch.from_numpy(X_train)) ⑧
X_val = Variable(torch.from_numpy(X_val)) ⑧
y_train = Variable(torch.from_numpy(y_train)) ⑧
y_val = Variable(torch.from_numpy(y_val)) ⑧
X_test = Variable(torch.from_numpy(X_test)) ⑧
y_test = Variable(torch.from_numpy(y_test)) ⑧
① 导入 NumPy,用于将数据集加载为向量和矩阵
② 导入 Scikit-Learn 中可用的乳腺癌数据集
③ 导入 Scikit-Learn 中可用的 train_test_split 函数
④ 导入 PyTorch 和 Variable 数据结构,用于将输入数据集存储为张量
⑤ 加载乳腺癌数据集并提取特征和目标
⑥ 将数据分为训练集和验证/测试集
⑦ 将验证/测试集分为两个相等的集合:验证集和测试集
⑧ 将训练集、验证集和测试集初始化为 PyTorch 张量
注意,70% 的数据用于训练,15% 用于验证,剩余的 15% 作为保留的测试集。现在让我们检查目标变量的分布,如图 4.10 所示,在三个集中是否相似。我们可以看到,在所有三个集中,大约 60-62% 的情况是良性的(目标变量 = 1),38-40% 的情况是恶性的(目标变量 = 0)。

图 4.10 训练集、验证集和测试集中目标变量的分布
4.3.2 训练和评估 DNN
现在我们已经准备好了数据,下一步是定义 DNN。我们将创建一个类,其中可以传递层数和单元数作为属性,如下所示:
class Model(torch.nn.Sequential): ①
def __init__(self, layer_dims): ②
super(Model, self).__init__() ③
for idx, dim in enumerate(layer_dims): ④
if (idx < len(layer_dims) - 1): ⑤
module = torch.nn.Linear(dim, layer_dims[idx + 1]) ⑤
self.add_module("linear" + str(idx), module) ⑤
else: ⑥
self.add_module("sig" + str(idx), torch.nn.Sigmoid()) ⑥
if (idx < len(layer_dims) - 2): ⑦
self.add_module("relu" + str(idx), torch.nn.ReLU()) ⑦
① 创建一个从 PyTorch Sequential 类继承的 Model 类
② 将每层的层数和单元数作为数组传递给构造函数
③ 初始化 PyTorch Sequential 超类
④ 对于数组中的每个元素,提取该层的索引和单元数量
⑤ 创建一个包含所有线性单元直到最终输出层的层模块
⑥ 对输出层的单元使用 sigmoid 激活函数
⑦ 对隐藏层中的所有单元使用 ReLU 激活函数
注意,DNN Model 类继承自 PyTorch Sequential 类,该类以初始化的顺序排列层模块。对于输入层和隐藏层,使用 Linear 单元来计算该单元所有输入的加权和。对于隐藏层,我们使用 ReLU 激活函数。最终的输出层由一个单元组成,我们使用 sigmoid 激活函数。sigmoid 激活函数的输出是一个介于 0 和 1 之间的分数。这个输出作为分类任务中正类概率度量的代理。在这种情况下,正类是良性。现在我们有了 Model 类,让我们按照以下方式初始化它:
dim_in = X_train.shape[1] ①
dim_out = 1 ②
layer_dims = [dim_in, 20, 10, 5, dim_out] ③
model = Model(layer_dims) ④
① 输入层的单元数量等于训练集中特征的数量。
② 输出层的单元数量为 1,因为我们处理的是一个二元分类问题。
③ 初始化层维度数组以定义 DNN 的结构
④ 使用预定义的结构初始化 DNN 模型
如果你使用命令 print(model) 打印模型,你会得到以下输出,它总结了 DNN 的结构:
Model(
(linear0): Linear(in_features=30, out_features=20, bias=True)
(relu0): ReLU()
(linear1): Linear(in_features=20, out_features=10, bias=True)
(relu1): ReLU()
(linear2): Linear(in_features=10, out_features=5, bias=True)
(relu2): ReLU()
(linear3): Linear(in_features=5, out_features=1, bias=True)
(sig4): Sigmoid()
)
在这个输出中,您可以看到 DNN 由一个输入层、三个隐藏层和一个输出层组成。输入层包含 30 个单元,因为数据集包含 30 个输入特征。第一个隐藏层包含 20 个单元,第二个隐藏层包含 10 个单元,第三个隐藏层包含 5 个单元。隐藏层中的所有单元都使用 ReLU 激活函数。最后,输出层由一个具有 sigmoid 激活函数的单个单元组成。对于这个数据集,输入层和输出层的单元数必须分别为 30 和 1,因为特征数为 30,并且二分类任务只需要一个输出。然而,您可以根据哪种结构给出最佳性能来调整隐藏层的数量和每个隐藏层的单元数。您可以使用验证集来确定这些超参数。
模型就绪后,现在让我们定义损失函数和优化器,它们将用于在反向传播期间确定权重,如下所示:
criterion = torch.nn.BCELoss(reduction='sum') ①
optimizer = torch.optim.Adam(model.parameters(), lr=0.001) ②
① 将二元交叉熵(BCE)损失初始化为优化的标准
② 使用学习率为 0.001 的 Adam 优化器在反向传播期间确定权重
如前所述,BCE 损失被用作二分类问题的优化标准。我们在这里也使用 Adam 优化器,并使用预定义的初始学习率来确定反向传播期间的边权重。Adam 优化器是一种自适应确定梯度下降算法学习率的技巧。您可以在以下博客文章中找到有关 Adam 优化技术的更多详细信息:mng.bz/zQzX。最后,按照以下方式训练模型:
num_epochs = 300 ①
for epoch in range(num_epochs):
y_pred = model(X_train.float()) ②
loss = criterion(y_pred, y_train.view(-1, 1).float()) ③
optimizer.zero_grad() ④
loss.backward() ⑤
optimizer.step() ⑥
① 将时代数初始化为 300
在每个时代,获取训练集的 DNN 输出
③ 计算训练集的 BCE 损失
④ 在反向传播之前将梯度置零
⑤ 计算每个参数/边权重的梯度
⑥ 根据当前梯度更新权重
注意,我们正在对模型进行 300 个时代的训练。一个时代是一个超参数,它定义了我们在神经网络的前向和反向方向中传播整个训练集的次数。在每个时代,我们首先通过将训练集通过网络的前向方向传播来获取 DNN 的输出。然后,我们计算每个参数或边权重的梯度,并在反向传播期间更新权重。请注意,在每个时代开始反向传播之前,梯度被设置为 0,因为 PyTorch 默认在反向传递期间累积梯度。如果我们不将梯度设置为 0,权重将不会正确更新。
下一步是使用测试集来评估模型性能。因为这是一个分类问题,我们将使用与第三章中用于学生成绩预测问题的相同指标。我们将使用的指标是精确率、召回率和 F1 分数。我们将比较训练好的深度神经网络(DNN)模型与一个合理的基线模型的性能。如第 4.2 节所示,数据集中大多数案例都是良性的。因此,我们将考虑一个总是预测良性的基线模型。这并不理想,因为我们将会错误地预测所有恶性案例。在现实情况下,基线模型通常是由人类或专家或企业正在使用的现有模型做出的预测。对于这个例子,不幸的是,我们没有获取到这些信息,因此我们将比较模型与总是预测良性的基线。
表 4.2 显示了用于基准测试模型的三项关键性能指标——精确率、召回率和 F1 分数。如果我们看召回率指标,基线模型比深度神经网络(DNN)做得更好。这是预期的,因为基线模型总是预测正类,因此会正确地预测所有正类案例。然而,对于负类,基线模型的召回率将是 0。尽管如此,深度神经网络(DNN)模型的整体表现仍然优于基线,实现了 98.1%的精确率(比基线高 35.4%)和 96.2%的 F1 分数(比基线高 19.1%)。
作为一项练习,我强烈建议你调整模型的超参数,看看你是否能提高这个模型的性能。你可以通过改变网络结构中的隐藏层数量和每层的单元数,以及用于训练的 epoch 数来调整网络结构。在第 4.2 节(图 4.6)中,我们也看到一些输入特征彼此之间高度相关。通过移除一些冗余特征,模型的性能可以进一步提高。作为另一项练习,进行特征选择,确定最大化模型性能的最佳特征子集。
表 4.2 基线模型与深度神经网络(DNN)模型性能比较
| 精确率(%) | 召回率(%) | F1 分数(%) | |
|---|---|---|---|
| 基线模型 1 | 62.7 | 100 | 77.1 |
| 深度神经网络(DNN)模型 | 98.1 (+35.4) | 94.4 (–5.6) | 96.2 (+19.1) |
由于深度神经网络(DNN)模型的性能优于基线,现在让我们来解释它,并了解这个黑盒模型是如何得出最终预测的。
4.4 解释深度神经网络(DNN)
如前节所述,要使用深度神经网络(DNN)进行预测,我们需要将数据通过多个层传递,每一层由多个单元组成。每一层的输入数据会根据单元使用的权重和激活函数进行非线性转换。一个单一的预测可能涉及大量的数学运算,这取决于神经网络的架构。对于前节中用于乳腺癌检测的相对简单的架构,一个单一的预测大约涉及 890 次数学运算,这是基于训练参数或权重的数量,如下所示:
+----------------+------------+
| Modules | Parameters |
+----------------+------------+
| linear0.weight | 600 |
| linear0.bias | 20 |
| linear1.weight | 200 |
| linear1.bias | 10 |
| linear2.weight | 50 |
| linear2.bias | 5 |
| linear3.weight | 5 |
| linear3.bias | 1 |
+----------------+------------+
Total Trainable Parameters: 891
随着我们添加更多的隐藏层和每隐藏层的单元,这个例子可以非常容易地扩展到数百万次运算。这就是为什么 DNN 被称为黑盒——它变得非常难以理解每一层执行了什么转换以及模型是如何得出最终预测的。我们将在后面的章节中看到,对于像 CNN 和 RNN 这样更复杂的结构,这变得更加困难。
我们可以解释 DNN 的一种方式是查看连接到输入层单元的权重或边的强度。这可以被视为确定输入特征对输出预测的整体影响的代理。然而,它不会给我们一个像我们在前几章中看到的白盒模型和树集成那样准确的特征重要性度量。主要原因是因为神经网络在隐藏层学习输入的表示。初始输入特征被转换成中间特征和概念。因此,这些输入特征的重要性不仅仅由连接到输入层单元的边来决定。那么我们如何解释 DNN 呢?
我们有多种方式来解释 DNN。我们可以使用在前一章中学到的模型无关方法,这些方法具有全局范围。我们学习了 PDPs(Partial Dependence Plots)和特征交互图——模型无关的技术,意味着它们是可以与任何机器学习模型一起工作的可解释性技术。它们在范围上也是全局的,因为它们查看模型对最终预测的整体影响。PDPs 和特征交互图易于使用且直观,它们是揭示特定特征值如何影响模型输出的优秀工具。我们还学习了如何使用它们来揭示潜在问题,如数据和模型偏差。我们可以非常容易地将这些技术应用到为乳腺癌检测训练的 DNN 模型上。然而,为了使 PDPs 和特征交互图起作用,模型的输入特征必须是独立的,我们在第 4.2 节中看到它们并不是。
在接下来的章节中,我们将学习更多高级的模型无关技术,特别是关注 LIME、SHAP 和锚点。这些可解释技术是局部的,也就是说,它们专注于解释特定的实例或示例。在后面的章节中,我们将学习特征归因方法,旨在量化每个输入特征对最终预测的贡献,并学习如何剖析神经网络以及可视化中间隐藏层和单元学习到的特征。
4.5 LIME
LIME,即局部可解释模型无关解释的缩写,由 Marco Tulio Ribeiro 及其团队于 2016 年提出。让我们分解这项技术。在上一个章节中,我们训练了一个 DNN,它使用 30 个特征学习如何区分良性病例和恶性病例。让我们通过将特征空间折叠成 2-D 空间来简化这一点,如图 4.11 所示。该图展示了 DNN 学习到的复杂决策函数,其中模型将良性病例与恶性病例分开。决策边界在图 4.11 中被故意夸张,以说明一个全局上难以解释但可能通过 LIME 等技术局部解释的复杂函数。

图 4.11 DNN(或任何黑盒模型)学习到的复杂决策边界的 2-D 示意图,用于区分良性病例和恶性病例
LIME 首先选择一个示例进行解释。这如图 4.12 所示,我们选择了一个恶性病例进行解释。目标是尽可能频繁地探测模型,以解释模型是如何对所选示例做出预测的。你可以通过扰动数据集来获取该新数据集的模型预测。

图 4.12 使用 LIME 选择的实例进行解释的示意图
我们如何创建这个新的扰动数据集?给定训练数据,我们计算每个特征的关键摘要统计量。对于数值或连续特征,我们计算均值和标准差。对于分类特征,我们计算每个值的频率。然后我们根据这些摘要统计量创建一个新的数据集。对于数值特征,我们根据该特征的均值和标准差从高斯分布中采样数据。对于分类特征,我们根据频率分布或概率质量函数进行采样。一旦我们创建了数据集,我们就通过获取它们的预测来探测模型,如图 4.13 所示。所选实例以大加号表示。在扰动数据集上的恶性和良性预测分别以小加号和圆圈表示。

图 4.13 生成的或扰动的数据集及其相应的模型预测示意图
一旦我们创建了扰动数据集并获得了它们的模型预测,我们将根据这些新样本与所选实例的接近程度对它们进行加权,通过查看在特征方面类似的案例来解释所选实例。这种解释的局部性通过这种加权来捕捉——因此,LIME 缩写中的“局部”。图 4.14 显示了给定的扰动样本,它们接近所选实例并赋予更高的权重。

图 4.14 解释了与所选实例紧密相邻的加权实例的示意图
那么,我们如何根据样本与所选实例的接近程度来加权样本?在原始论文中,作者使用了指数核函数。指数核函数接受两个参数作为输入:
-
扰动样本与所选实例的距离——对于乳腺癌数据集(或一般表格数据),我们使用欧几里得距离来测量扰动样本在特征空间中与所选实例的距离。欧几里得距离也用于图像。对于文本,使用余弦距离度量。
-
核宽度——这是一个可以调整的超参数。如果宽度较小,只有接近所选实例的样本会影响解释。然而,如果宽度较大,则距离较远的样本也可以影响解释。这是一个重要的超参数,我们将在稍后更深入地研究其对解释的影响。默认情况下,核宽度设置为 0.75 × √特征数量。因此,对于具有 30 个输入特征的模型,默认核宽度为 4.1。核宽度的值可以从零到无穷大。
使用指数核函数,距离所选实例较近的样本在距离方面将具有更大的权重,而距离较远的样本权重较小。
最后一步是拟合一个易于在加权样本上解释的白色盒模型。在 LIME 中,使用线性回归,正如我们在第二章中看到的,我们可以使用线性回归模型的权重来解释所选实例的特征重要性——因此,LIME 缩写中的“可解释”。我们得到一个局部忠诚的解释,因为我们拟合了一个线性代理模型,LIME 对 DNN 或黑盒模型是完全无知的——因此,LIME 缩写中的“模型无关”。图 4.15 展示了用于解释所选实例的线性代理模型(由虚线灰色线表示),该模型忠实于所选实例附近和周围的区域。

图 4.15 展示了用于使用周围加权样本解释所选实例的线性模型
现在,让我们动手看看 LIME 在之前训练的乳腺癌诊断 DNN 模型上的实际应用。首先,使用 pip 安装 LIME 库,如下所示:
pip install lime
安装后,第一步是初始化一个 LIME 解释器对象。因为数据集是表格式的,所以我们使用 LimeTabularExplainer 类。其他解释器类有 LimeImageExplainer 用于解释使用图像作为输入的模型,以及 LimeTextExplainer 用于文本。我们将在下一章中使用 LimeImageExplainer 类来处理图像:
import lime ①
import lime.lime_tabular ①
explainer = lime.lime_tabular.LimeTabularExplainer(X_train.numpy(), ②
feature_names=data.feature_names, ③
class_names=data.target_names, ④
discretize_continuous=True) ⑤
① 导入库和相关模块
② 使用训练数据集初始化解释器
③ 提供特征名称
④ 提供目标类名(良性/恶性)
⑤ 将连续变量离散化以降低计算复杂度
现在让我们选择两个案例进行解释——一个是良性,一个是恶性。在这里我们将使用测试集,选择第一个良性案例和恶性案例,如下面的代码所示:
benign_idx = np.where(y_test.numpy() == 1)[0][0]
malignant_idx = np.where(y_test.numpy() == 0)[0][0]
我们需要创建一个辅助函数来提供对扰动数据集的 DNN 模型预测,如下所示:
def prob(data):
return model.forward(Variable(torch.from_numpy(data)).float()).\
detach().\
numpy().\
reshape(-1, 1)
我们还需要创建另一个函数来在 Matplotlib 中绘制 LIME 解释。我们可以使用库来创建这个图,但它不允许自定义。这就是我们创建这个辅助函数的原因,这样我们就可以添加标题和标签,更改颜色,甚至使用 LIME 解释创建我们自己的图表:
def lime_exp_as_pyplot(exp, label=0, figsize=(8,5)):
exp_list = exp.as_list(label=label)
fig, ax = plt.subplots(figsize=figsize)
vals = [x[1] for x in exp_list]
names = [x[0] for x in exp_list]
vals.reverse()
names.reverse()
colors = ['green' if x > 0 else 'red' for x in vals]
pos = np.arange(len(exp_list)) + .5
ax.barh(pos, vals, align='center', color=colors)
plt.yticks(pos, names)
return fig, ax
现在让我们解释第一个良性案例。如下所示,我们将选定的良性案例传递给 LIME 解释器:
bc1_lime = explainer.explain_instance(X_test.numpy()[benign_idx], ①
prob, ②
num_features=5, ③
top_labels=1) ④
f, ax = lime_exp_as_pyplot(bc1_lime) ⑤
① 将选定的良性案例的特征传递给函数
② 传递提供扰动数据集预测的辅助函数
③ 限制线性代理模型的特征数量为 5
④ 最高标签或正类是 1。
⑤ 使用辅助函数绘制 LIME 解释
注意,我们正在将线性代理模型的特征数量限制为 5。LIME 默认使用岭回归模型作为代理模型。岭回归是线性回归模型的一个变体,它通过正则化允许变量选择或参数消除。通过使用高正则化参数,我们可以创建稀疏模型,仅选择几个顶级特征进行预测。我们可以使用低正则化参数以获得更少的稀疏性。图 4.16 显示了良性案例的 LIME 解释结果。
对于使用 LIME 进行解释的良性案例,DNN 模型预测它为良性,概率为 0.99,或信心度为 99%。为了理解它是如何得出这个预测的,图 4.16 显示了线性代理模型的前五个最重要的特征及其相应的权重或重要性。看起来最重要的特征是最差区域,具有较大的正值权重。根据 LIME,模型预测良性是因为最差区域值在 511 和 683.95 之间。LIME 是如何得到这个值域的?这是基于线性代理模型使用的加权扰动数据集的标准差。现在,这个解释合理吗?为了验证这一点,我们必须回到我们在 4.2 节中进行的探索性数据分析。我们在图 4.3 中看到,当最差或最大的细胞面积小于 700 时,良性案例比恶性案例多得多。

图 4.16 深度神经网络模型预测为良性的第一个良性案例的 LIME 解释,置信度为 99%
如果我们现在查看 LIME 确定的第二个最重要的特征,我们可以看到,如果平均面积在 415.63 和 544.05 之间,案例为良性的可能性就大得多。这一点进一步得到了我们在图 4.3 中的观察的验证。我们也可以对第三个最重要的特征——平均周长——做出类似的观察。你可能已经在图 4.16 的标题中观察到了核宽度和一个分数。我们稍后会讨论这一点。
现在我们来查看测试集中第一个恶性案例,并使用 LIME 进行解释。我们可以使用之前相同的代码,但需要记住使用malignant_idx从测试集中选择正确的特征值。作为一个练习,我鼓励你自己去尝试。得到的 LIME 解释如图 4.17 所示。最重要的两个特征与良性案例相同,但值域不同。此外,最重要的特征(最差细胞面积)的权重也是负值。这很有道理,因为我们预期该特征会对模型的输出产生负面影响。深度神经网络(DNN)被训练来预测正类的概率,在这种情况下,是良性。因此,如果案例是恶性的,我们期望模型的输出尽可能低;也就是说,案例为良性的概率必须尽可能低。

图 4.17 深度神经网络模型预测为恶性的第一个恶性案例的 LIME 解释,置信度为 100%
对于这个恶性案例,DNN 预测该案例为良性的概率为 0。这意味着模型 100%确信该案例为恶性。现在让我们检查特征值范围。我们可以看到,模型预测为恶性,因为最坏或最大的细胞面积大于 683.95 但小于 1030.75。这在探索性分析中是有意义的,因为我们观察到在该范围内恶性案例比良性案例多(见图 4.3)。对于其他特征,我们可以做出类似的观察。
内核宽度的影响需要指出的是,内核宽度是 LIME 的一个重要超参数。选择合适的内核宽度非常重要,并且会影响解释的质量。我们不能为所有希望解释的实例选择相同的内核宽度。宽度的选择会影响 LIME 考虑的加权扰动样本,用于线性代理模型。如果我们选择较大的内核宽度,距离选定的实例较远的样本将影响线性代理模型。这可能不是我们想要的,因为我们希望代理模型尽可能忠实地反映原始的黑盒模型。默认情况下,LIME 库使用一个内核宽度,它是特征数量的平方根乘以 0.75 的因子。因此,如果kernel_width = None,则使用默认值。可能的情况是,相同的内核宽度可能不适用于所有需要使用 LIME 解释的实例。为了评估解释的质量,LIME 提供了一个解释,或称为保真度分数。该参数称为score,用于结果 LIME 解释。分数越高,意味着 LIME 使用的线性模型是黑盒模型的良好近似。内核宽度和 LIME 保真度分数在图 4.16 和图 4.17 的标题中显示。
现在我们通过观察另一个良性案例来分析内核宽度的影响。我们在此选择了测试集的第二种情况,如下所示:
benign_idx2 = np.where(y_test.numpy() == 1)[0][1]
我们之前创建的 LIME 解释器使用了默认值,即 0.75 乘以特征数量的平方根。这计算出的内核宽度为 4,因为数据集中的特征数量为 30。我们还将创建另一个 LIME 解释器,其初始化内核宽度为 1,以观察对解释的影响。以下代码显示了如何创建内核宽度为 1 的 LIME 解释器:
explainer_kw1 = lime.lime_tabular.LimeTabularExplainer(X_train.numpy(),
feature_names=data.feature_names,
class_names=data.target_names,
kernel_width=1, ①
discretize_continuous=True)
① 将kernel_width参数设置为 1。
使用默认内核宽度和内核宽度为 1 的第二个良性案例的 LIME 解释结果分别显示在图 4.18(a)和图 4.18(b)中。

图 4.18a 默认内核宽度的良性案例 2 的 LIME 解释

图 4.18b 内核宽度为 1 的良性案例 2 的 LIME 解释
首先,让我们将默认 LIME 解释的第二良性案例与之前展示的第一个案例进行比较。最重要的特征是相同的。然而,我们可以看到特征的值域是不同的。对于第二个良性案例,我们看到模型预测良性是因为最坏细胞的面积小于 511,而第一个案例的面积介于 511 和 683.95 之间。这仍然是一个有效的预测,因为当最坏面积小于 511 时,有更多的案例是良性的。第二个良性案例的默认 LIME 解释的保真度分数也更高。这意味着 LIME 中的线性模型在这个案例中比第一个案例更接近 DNN 模型。
如果我们现在切换到图 4.18(b),我们可以看到如果我们使用较小的核宽度,解释会有多么不同。最上面的特征仍然是相同的,但我们看到不同的特征以及它们的一个小得多的值域,因为小的核宽度将线性代理模型集中在与所选实例非常接近的扰动案例上。对于第二个良性案例,哪个核宽度更好?我们可以看到,核宽度为 1 的保真度分数仅为 0.27,而默认值为 0.22。因此,在这个案例中,核宽度为 1 更好。作为一个练习,我强烈建议你增加第二个案例的核宽度,看看你是否可以达到更高的保真度分数,并分析结果 LIME 图。我还建议你调整第一个案例的核宽度超参数,看看你是否可以得到一个更好的解释,这个解释与 DNN 的忠实度更高。
图 4.19(a)和图 4.19(b)展示了第二个恶性案例的两个核宽度的 LIME 解释——一个是默认宽度,另一个是宽度为 1。作为一个练习,比较这些解释与第一个恶性案例,看看哪个核宽度给出了更高质量的解释。

图 4.19a 默认核宽度的 LIME 解释的恶性案例 2

图 4.19b 核宽度为 1 的 LIME 解释的恶性案例 2
LIME 是一个解释黑盒模型的优秀工具。它是模型无关的,可以与不同类型的模型一起工作。LIME 还可以与不同类型的数据一起工作——表格数据、图像和文本。我们已经在本节中看到了它使用表格数据的实际应用。我们将在后面的章节中探索图像和文本数据,你可以在库文档中找到示例(github.com/marcotcr/lime)。这是一个广泛使用的库,拥有许多活跃的贡献者。
然而,LIME 解释的质量在很大程度上取决于核宽度的选择,这是用于加权扰动样本的核函数的输入。它是一个重要的超参数,我们已看到对于不同例子,我们选择的宽度可能不同。我们可以使用库提供的保真度分数来确定正确的宽度,但选择正确的核宽度仍然是不明确的。LIME 的另一个限制是,扰动数据集是通过从高斯分布中进行采样创建的,并且它忽略了特征之间的相关性。因此,扰动数据集可能不具有与原始训练数据相同的特征。
4.6 SHAP
SHAP,即 SHapley Additive exPlanations 的缩写,由 Scott M. Lundberg 和 Su-In Lee 于 2017 年提出。它统一了 LIME(以及线性代理模型)和博弈论的思想,并在解释的准确性方面提供了比 LIME 更多的数学保证。Shapley 值是博弈论中的一个概念,它量化了合作游戏中玩家联盟的影响。现在让我们看看我们所说的合作游戏、游戏的玩家以及玩家联盟的含义。在模型可解释性的背景下,合作游戏是模型及其做出的预测。输入到模型中的特征相当于玩家,而玩家联盟是相互作用的特征集,以得出最终预测。因此,Shapley 值可以用来量化特征(即玩家)及其相互作用(即玩家联盟)对模型预测(即合作游戏)的影响。让我们通过查看图 4.20 中所示的具体示例来分解 SHAP 可解释性技术。
SHAP 背后的思想与 LIME 背后的思想相当相似。第一步是选择一个实例进行解释。在图 4.20 中,选择的实例显示为索引 0 的第一行。由于 SHAP 使用博弈论概念,选择的实例由所有特征的联盟组成。当所有特征都被选中,或“打开”时,它由包含所有特征的所有 1s 的向量表示。图 4.20 的第一列显示了联盟向量作为表格。对于选择的实例,联盟向量由所有 1s 组成,因此当我们把该向量转换到特征空间时,我们选择该实例的所有实际特征值。这个特征向量在图 4.20 的第二列中作为表格显示。

图 4.20 创建 SHAP 扰动数据集的说明
一旦我们选择了要解释的实例,下一步就是创建扰动数据集。这个过程与 LIME 相同,但与 LIME 不同,SHAP 的思路是生成一系列联盟向量,其中特征是随机“开启”或“关闭”。如果一个特征被开启,其在联盟向量中的值为 1。如果一个特征被关闭,其在联盟向量中的值为 0。我们知道如何在特征空间中表示开启时的特征——我们只需从已选择的实例中选取实际值。然而,如果特征被关闭,我们从训练集中随机选取该特征的一个值。
创建扰动数据集后,下一步是根据其与所选实例的接近程度对数据集进行加权。这又与 LIME 相似,但与 LIME 不同,SHAP 使用SHAP 核来确定扰动数据集中样本的权重,而不是指数核函数。SHAP 核函数会给包含非常低或非常高数量特征的联盟更高的权重。接下来的步骤与 LIME 相同,即在加权数据集上拟合线性模型,并返回线性模型的系数或权重作为所选实例的解释。这些系数或权重被称为Shapley 值。
现在让我们看看 SHAP 在之前训练的乳腺癌诊断模型上的实际应用。SHAP 的作者在 GitHub 上创建了一个 Python 库。我们可以使用 pip 安装此库,如下所示:
pip install shap
我们将使用之前在 LIME 部分介绍的名为prob的相同辅助函数来提供对扰动数据集的 DNN 模型预测。你现在可以创建扰动数据集并初始化 SHAP 解释器,如下所示:
import shap
shap.initjs() ①
shap_explainer = shap.KernelExplainer(prob, ②
X_train.numpy(),
link="logit") ③
① 初始化 JavaScript 以进行交互式可视化
② 使用 prob 辅助函数获取 DNN 预测
③ 使用对数连接函数,因为 DNN 是一个分类器
注意,对数连接函数用于线性代理模型,因为我们处理的是一个二元分类器,它为正类输出概率估计。对于回归问题,你可以将link参数切换到identity。接下来,按照以下方式获取测试集中所有数据的 SHAP 值:
shap_values = shap_explainer.shap_values(X_test.numpy())
你现在可以像这里所示的那样,以 Matplotlib 图的形式获取第一个良性案例的 SHAP 解释:
plot = shap.force_plot(shap_explainer.expected_value[0],
shap_values[0][benign_idx,:],
X_test.numpy()[benign_idx,:],
feature_names=data['feature_names'],
link="logit")
结果图如图 4.21 所示。回想一下,对于第一个良性案例,DNN 模型预测其良性概率为 0.99 或置信度为 99%。

图 4.21 对良性案例 1 的 SHAP 解释,其中 DNN 模型预测良性概率为 0.99(或 99%的置信度)
SHAP 库提供了更美观的可视化效果,您可以看到每个特征值是如何推动基础预测值上升或下降的。在图 4.21 中,您可以看到基础值大约在 0.63。这代表的是正类率,即良性病例的比例。当我们探索 4.2 节中的数据时,我们观察到在数据集中,大约 63%的病例是良性的。SHAP 可视化的理念是观察特征值是如何将基础预测概率从 0.63 推高到 0.99 的。特征的影响通过条形长度来表示。从图中我们可以看出,最差细胞面积和平均细胞面积特征具有最大的 Shapley 值,这推动了基础预测值的最大变化。下一个最重要的特征是最差细胞周长。
图 4.22 展示了 DNN 模型预测为 0.99(或置信度为 99%)的第二个良性病例的 SHAP 解释

图 4.22 展示了 DNN 模型预测为 0.99(或置信度为 99%)的第二个良性病例的 SHAP 解释
我们可以看到,这里最重要的两个特征是最差细胞面积和平均细胞面积。因为最差面积和平均面积相当低,分别为 424.8 和 346.4,足以将基础预测推高到 0.99。作为一个练习,修改前面展示的代码来解释两个恶性病例。结果图示在图 4.23 和 4.24 中。

图 4.23 展示了 DNN 模型预测为 0(或恶性,置信度为 100%)的恶性病例 1 的 SHAP 解释
对于第一个恶性病例,模型预测其概率为 0。在图 4.23 中,我们可以看到特征值是如何将基础预测概率推低到 0 的。看起来对最终预测影响最大的特征是最差细胞面积、平均细胞面积和周长。
对于第二个恶性病例,模型同样预测其概率为 0。我们可以看到,最有影响力的特征再次是最差细胞面积。因为该值相当大——大于 1417——足以将基础预测概率推低到 0,如图 4.24 所示。

图 4.24 展示了 DNN 模型预测为 0(或恶性,置信度为 100%)的恶性病例 2 的 SHAP 解释
SHAP 是另一个用于解释黑盒模型的优秀工具。像 LIME 一样,它是模型无关的,并且它使用博弈论的概念来量化特征对单个实例模型预测的影响。它比 LIME 提供了更多关于解释准确性的数学保证。该库还提供了关于特征影响的出色可视化,展示了特征值如何将基线预测推高或推低到最终预测。然而,基于 SHAP 内核计算 Shapley 值的计算量是密集的。计算复杂度随着输入特征数量的指数增长。
4.7 锚点
锚点是一种局部范围内的模型无关解释技术。它由 LIME 的相同创造者在 2018 年提出。它通过提供高精度规则或谓词来改进 LIME,这些规则说明了模型如何得出预测,并通过全球范围量化这些规则的范围。让我们来分解一下。
在这项技术中,模型解释以锚点的形式生成。一个 锚点 实质上是一组 if 条件,或 谓词,它包含我们想要解释的选定的实例。这如图 4.25 中的框所示。图中的锚点可以解释为两个 if 条件,其中二维特征空间中的两个特征被下限和上限所限制,从而在选定的实例周围形成一个边界框。算法的第一个目标是形成包含选定实例且针对目标预测的高精度锚点。精度 是锚点质量的度量,定义为具有与选定实例相同目标预测的扰动样本数与锚点内总样本数之比。算法的一个重要超参数是 精度阈值。

图 4.25 锚点的示意图
一旦算法生成了一组高精度锚点,下一步就是量化每个锚点的范围。锚点的范围通过一个称为 覆盖率 的指标来量化。覆盖率指标衡量锚点(或谓词集)出现在其他样本或特征空间其他部分中的概率。有了这个指标,我们可以了解锚点的解释在全球范围内是如何适用的。算法的目标是选择覆盖率最高的锚点。
确定所有满足精度阈值和覆盖率要求的谓词是一项计算密集型任务。算法的作者在构建谓词或规则时采用了自底向上的方法。算法从一个空的规则集开始,并在每次迭代中,算法增量地构建一个满足精度阈值和覆盖率要求的锚点,并将其添加到集合中。为了估计锚点的精度,作者将此问题表述为一个多臂老丨虎丨机问题,并特别使用了 KL-LUCB 算法来识别精度最高的规则。
现在让我们使用锚点来解释乳腺癌 DNN 模型。论文的作者在 GitHub 上创建了一个 Python 库。您可以使用 pip 安装此库,如下所示:
pip install anchors_exp
如同我们使用 LIME 和 SHAP 所做的那样,现在让我们创建乳腺癌数据集的锚点表格解释器,如下所示:
from anchor import anchor_tabular ①
anchor_explainer = anchor_tabular.AnchorTabularExplainer(
data.target_names, ②
data.feature_names, ③
X_train.numpy(),
categorical_names={}) ④
anchor_explainer.fit(X_train.numpy(), ⑤
y_train.numpy(), ⑤
X_val.numpy(), ⑤
y_val.numpy()) ⑤
① 从库中导入 anchor_tabular 模块
② 设置目标标签名称
③ 设置数据集的特征名称
④ 如果有,提供分类特征名称
⑤ 将锚点解释器拟合到训练集和验证集
我们需要为锚点创建一个不同的辅助函数,该函数提供 DNN 预测作为离散标签而不是概率。此辅助函数如下所示:
def pred(data):
pred = model.forward( ①
Variable(torch.from_numpy(data)).float()).\ ①
detach().numpy().reshape(-1) > 0.5 ①
return np.array([1 if p == True else 0 for p in pred]) ①
① 如果输出概率大于 0.5,则预测为 1,否则为 0
现在让我们使用锚点来解释第一个良性案例。以下代码展示了如何解释实例,提取谓词或规则,以及获得解释的精度和覆盖率:
exp = anchor_explainer.explain_instance(X_test.numpy()[benign_idx], ①
pred, ②
threshold=0.95) ③
print('Prediction: ',
➥ anchor_explainer.class_names[pred(X_test.numpy()[benign_idx])][0]) ④
print('Anchor: %s' % (' AND '.join(exp.names()))) ⑤
print('Precision: %.3f' % exp.precision()) ⑥
print('Coverage: %.3f' % exp.coverage()) ⑦
① 将选定的实例作为第一个参数传递
② 提供提供模型标签预测的辅助函数
③ 设置精度阈值
④ 打印模型做出的标签预测
⑤ 打印规则或谓词
⑥ 打印锚点的精度
⑦ 打印锚点的覆盖率
注意,精度阈值设置为 0.95。规则或谓词以字符串列表的形式获得,并使用 AND 子句连接。代码的输出结果如下所示:
Prediction: benign
Anchor: worst area <= 683.95 AND mean radius <= 13.27
Precision: 1.000
Coverage: 0.443
您可以看到模型正确预测了良性,具有最高精度的解释或锚点由两个规则或谓词组成。如果最坏区域小于或等于 683.95,并且平均半径小于或等于 13.27,则模型在所选实例周围的区域预测良性 100%。在覆盖率方面,此锚点表现相当不错,覆盖率为 44.3%。这意味着该规则适用于全球相当多的良性案例。您还可以使用以下代码行获得此解释的 HTML 可视化,如图 4.26 所示:
exp.save_to_file('anchors_benign_case1_interpretation.html')
目前,锚点库不提供 Matplotlib 可视化。

图 4.26 良性案例 1 的锚点解释,精度为 100%,覆盖率为 44.3%
作为练习,将此代码扩展到其他良性及恶性案例。第二良性案例的结果可视化如图 4.27 所示。你可以看到模型正确预测了良性,并且锚定算法提出了两个规则,精确度为 1:如果最坏细胞面积小于或等于 683.95,且最坏细胞半径小于或等于 12.98,模型 100%预测为良性。然而,此锚定的覆盖率比第一个良性案例低 20.9%。这意味着第二个良性案例的解释比第一个案例更加局部。

图 4.27 展示了精确度为 100%、覆盖率为 20.9%的良性案例 2 的锚定解释
第一个恶性案例的锚定解释如图 4.28 所示。模型正确预测其为恶性,解释由两个规则或谓词组成,精确度为 1。规则如下:如果最坏细胞面积大于 683.95 且平均细胞半径小于或等于 544.05,模型 100%预测为恶性。然而,锚定的覆盖率非常低,仅为 1.2%。因此,解释非常局部,实际上并不适用于许多其他恶性案例。

图 4.28 展示了锚定对恶性案例 1 的解释,其中精确度为 100%,覆盖率为 1.1%
最后,第二个恶性案例的锚定解释如图 4.29 所示。模型再次正确预测其为恶性,解释由一个精确度为 1 的规则组成。规则如下:如果最坏细胞面积大于 1030.75,模型 100%预测为恶性。此锚定的覆盖率比第一个案例好得多,为 27.1%。这是有道理的,因为如果我们回到第 4.2 节中进行的探索性分析,并仔细观察图 4.3,我们会看到许多恶性案例的最坏细胞面积大于 1030。

图 4.29 展示了精确度为 100%、覆盖率为 27.1%的恶性案例 2 的锚定解释
锚定是一种强大的模型无关解释技术,因为它们提供了一组高精度规则、谓词或人类可读的 if 条件作为解释。这项技术还让我们对规则的覆盖范围或范围有了一定的感觉,即规则在全局范围内适用性如何。然而,Python 库仍在开发中,并且不如 LIME 或 SHAP 那样活跃。
在接下来的章节中,我们将更深入地探讨神经网络的世界,并了解更复杂的结构,如 CNN 和 RNN。我们还将学习如何在神经网络上执行特征归因,以及如何剖析它们以更好地理解网络学到了什么。
摘要
-
人工神经网络(ANN)是一个旨在松散地模拟生物大脑的系统。它属于被称为深度学习的一类广泛的机器学习方法。基于 ANN 的深度学习的核心思想是从更简单的概念或特征构建复杂的概念或表示。
-
具有两个或更多隐藏层的 ANN 被称为深度神经网络(DNN)。
-
确定深度神经网络(DNN)中权重的有效算法是反向传播。
-
激活函数是神经网络中的一个重要特征。它决定了神经元是否应该被激活以及激活的程度。激活函数的性质是它是可微的且单调的。
-
ReLUs 是神经网络中最广泛使用的激活函数,因为它们很好地处理了梯度消失问题。它们在计算上也更高效。
-
我们可以用多种方式解释神经网络。我们可以使用全局范围内的模型无关方法,例如 PDPs。在本章中,我们学习了更多基于扰动的模型无关技术,如 LIME、SHAP 和锚点。这些可解释性技术是局部的,意味着它们专注于解释特定实例或示例。
-
LIME 代表局部可解释的模型无关解释。它基于选择一个示例,随机扰动它,根据其与所选实例的接近程度对扰动样本进行加权,并在加权样本上拟合一个更简单的白盒模型。
-
LIME 解释的质量在很大程度上取决于核宽度的选择,这是用于加权的扰动样本的核函数的输入。它是一个重要的超参数,我们已看到宽度可能因我们选择的解释示例而异。我们可以使用库提供的保真度分数来确定正确的宽度,但选择正确的核宽度仍然是不确定的。
-
LIME 的另一个缺点是,通过从高斯分布中进行采样来创建扰动数据集,并且它忽略了特征之间的相关性。因此,扰动数据集可能不具有原始训练数据的相同特征。
-
SHAP 代表 SHapley Additive exPlanations。与 LIME 一样,它是模型无关的,并且它使用博弈论的概念来量化特征对单个实例模型预测的影响。理论上,SHAP 在解释的准确性方面比 LIME 提供了更多的数学保证。
-
SHAP 库提供了关于特征影响的出色可视化,显示了特征值如何将基线预测推高或推低到最终预测。
-
然而,基于 SHAP 核计算 Shapley 值是计算密集型的。计算复杂度随着输入特征数量的指数增长。
-
Anchors 是一种改进 LIME 的技术,它通过提供一组高精度规则、谓词或人类可读的 if 条件作为解释。该技术还让我们对规则的覆盖范围或范围有了一种感觉,即规则在全球范围内适用性的如何。然而,Python 库仍然处于开发中,并且不如 LIME 或 SHAP 活跃。
5 显著性映射
本章涵盖了
-
使卷积神经网络本质上成为黑盒的特征
-
如何实现用于图像分类任务的卷积神经网络
-
如何使用显著性映射技术(如标准反向传播、引导反向传播、引导 Grad-CAM 和 SmoothGrad)来解释卷积神经网络
-
这些显著性映射技术的优缺点以及如何对它们进行合理性检查
在前一章中,我们探讨了深度神经网络,并学习了如何使用局部范围内的模型无关方法来解释它们。我们具体学习了三种技术:LIME、SHAP 和锚点。在本章中,我们将专注于卷积神经网络(CNNs),这是一种更复杂的神经网络架构,主要用于视觉任务,如图像分类、图像分割、目标检测和面部识别。我们将学习如何将前一章中学到的技术应用到 CNNs 上。此外,我们还将关注显著性映射,这是一种局部、模型相关和事后解释的技术。显著性映射是解释 CNNs 的一个很好的工具,因为它帮助我们可视化模型的重要或显著特征。我们将具体介绍标准反向传播、引导反向传播、集成梯度、SmoothGrad、Grad-CAM 和引导 Grad-CAM 等技术。
本章的结构与前面章节类似。我们将从一个具体的例子开始,这个例子将扩展第四章中的乳腺癌诊断示例。我们将探索这个包含图像的新数据集,并学习如何在 PyTorch 中训练和评估卷积神经网络(CNNs),以及如何解释它们。值得重申的是,尽管本章的主要重点是使用显著性映射来解释 CNNs,但我们也会涵盖模型训练和测试。我们还会在前面的部分中提炼出一些关键见解,这些见解在模型解释过程中将非常有用。对于已经熟悉 CNNs 训练和测试的读者,可以自由跳过前面的部分,直接跳到第 5.4 节,该节涵盖了模型可解释性。
5.1 诊断+ AI:浸润性导管癌检测
浸润性导管癌(IDC)是乳腺癌最常见的类型。在本章中,我们将扩展前一章中的乳腺癌诊断示例,以检测 IDC。Diagnostics+的病理学家目前对病人进行活检,他们移除小块组织样本并在显微镜下分析,以确定病人是否有 IDC。病理学家将整个组织样本分割成小块,并确定每个小块是否为 IDC 阳性或阴性。通过在组织中界定 IDC 的确切区域,病理学家确定癌症的侵略性或进展程度,以及为病人分配哪个等级。

图 5.1 Diagnostics+ AI 用于检测浸润性导管癌(IDC)
Diagnostics+ 希望扩展我们在第四章中构建的 AI 系统的功能,以自动评估组织样本图像。目标是让 AI 系统确定组织样本中的每个切片是 IDC 阳性还是阴性,并为其分配一个置信度度量。这如图 5.1 所示。通过使用这个 AI 系统,Diagnostics+ 可以自动化预处理步骤,即描绘组织中的 IDC 区域,以便病理学家可以轻松地为它分配一个等级,以确定癌症的侵袭性。考虑到这些信息,你将如何将这个问题表述为一个机器学习问题?因为模型的目的是预测给定的图像或切片是 IDC 阳性还是阴性,我们可以将这个问题表述为一个 二元分类 问题。这个表述与第四章类似,但分类器的输入是图像,而不是结构化的表格数据。
5.2 探索性数据分析
现在我们来更好地理解这个新的图像数据集。本节中获得的许多见解将有助于我们进行模型训练、评估和解释。在这个数据集中,我们有来自 279 名患者的组织样本和 277,524 个组织切片图像。原始数据集从 Kaggle(mng.bz/0wBl)获得,并经过预处理以提取与这些图像相关的元数据。预处理笔记本和预处理后的数据集可以在与本书相关的 GitHub 仓库(mng.bz/KBdZ)中找到。
在图 5.2 中,我们可以看到 IDC 阳性与阴性切片的分布。在 277,524 个切片中,大约 70%是 IDC 阴性,30%是 IDC 阳性。因此,数据集高度不平衡。为了回顾,在处理不平衡数据集时,我们需要注意以下两点:
-
在测试和评估模型时,使用正确的性能指标(如精确度、召回率和 F1)。
-
重新采样训练数据,使得多数类要么被欠采样,要么少数类被过采样。

图 5.2 IDC 阳性与阴性切片的分布
让我们看看几个随机图像块的样本。通过可视化这些图像,我们可以看到 IDC 阳性图像块和阴性图像块是否有某些明显的特征。这将在我们后来解释模型时有所帮助。图 5.3 显示了四个 IDC 阳性图像块的随机样本,图 5.4 显示了四个 IDC 阴性图像块的随机样本。每个图像块的尺寸是 50 × 50 像素。我们可以观察到 IDC 阳性图像块有更多的深染细胞。深染的密度也更高。通常使用较深的颜色来染色细胞核。对于 IDC 阴性样本,另一方面,浅染的密度更高。浅色通常用于突出细胞质和细胞外结缔组织。因此,我们可以直观地说,如果一个图像块有高密度的深染或细胞核,那么它更有可能是 IDC 阳性。另一方面,如果一个图像块有高密度的浅染和非常低的细胞核密度,那么它更有可能是 IDC 阴性。

图 5.3 随机 IDC 阳性图像块的可视化

图 5.4 随机 IDC 阴性图像块的可视化
现在,让我们可视化一个患者或组织样本的所有图像块以及 IDC 阳性区域。图 5.5 为一位患者展示了这一点。左边的图显示了组织样本的所有图像块拼接在一起。右边的图显示了相同的图像,但以较深的色调突出显示 IDC 阳性图像块。这证实了我们之前的观察,即如果一个图像块有非常高的深染密度,那么它更有可能是 IDC 阳性。当我们需要解释我们将为 IDC 检测训练的 CNN 时,我们将回到这个可视化。

图 5.5 组织样本和 IDC 阳性图像块的可视化
在下一节中,我们将准备数据并训练一个卷积神经网络(CNN)。这个 CNN 将被用来将每张图像或图像块分类为 IDC 阳性或阴性。由于数据集相当不平衡,我们需要使用诸如精确度、召回率和 F1 等指标来评估 CNN。
5.3 卷积神经网络
卷积神经网络(CNN)是一种常用于视觉任务(如图像分类、目标检测和图像分割)的神经网络架构。为什么 CNN 用于视觉任务而不是全连接深度神经网络(DNN)?全连接 DNN 无法很好地捕捉图像中的像素依赖性,因为图像在输入神经网络之前需要被展平成 1 维结构。另一方面,CNN 利用图像的多维结构,很好地捕捉图像中的像素依赖性或空间依赖性。CNN 还具有平移不变性,这意味着它们擅长检测图像中的形状,无论这些形状出现在图像的哪个位置。此外,CNN 架构还可以更有效地训练以适应输入数据集,因为网络中的权重被重复使用。图 5.6 展示了用于二值图像分类的 CNN 架构示意图。

图 5.6 CNN 用于图像分类的示意图
图 5.6 中的架构由一系列称为“卷积和池化层”的层组成。这两种类型层的组合称为“特征学习层”。特征学习层的目的是从输入图像中提取层次化特征。前几层将提取低级特征,如边缘、颜色和梯度。通过添加更多的卷积和池化层,架构学习高级特征,使我们能够更好地理解数据集中图像的特征。我们将在本节稍后更深入地介绍卷积和池化层。
在特征学习层之后是神经元或单元的层,这些层是完全连接的,就像我们在第四章中看到的 DNN 架构一样。这些全连接层的目的是执行分类。全连接层的输入是卷积和池化层学习的高级特征,输出是对分类任务的概率度量。由于我们在第四章中介绍了 DNN 的工作原理,我们现在将主要关注卷积和池化层。
在第一章中,我们看到了如何表示图像以便 CNN 可以轻松处理,如图 5.7 所示。在这个例子中,组织片的图像是一个 50 × 50 像素大小的彩色图像,由三个主要通道组成:红色(R)、绿色(G)和蓝色(B)。这个 RGB 图像可以用数学形式表示为三个像素值矩阵,每个通道一个,大小为 50 × 50。

图 5.7 如何表示 50 × 50 像素的组织片图像
现在我们来看一下卷积层是如何处理以像素值矩阵表示的图像的。这一层由一个核或滤波器组成,并与输入图像进行卷积,以获得称为特征图像的图像表示。让我们一步一步地分析它。图 5.8 展示了卷积层中执行操作的简化示意图。在图中,图像被表示为一个 3×3 维度的矩阵,而核或滤波器被表示为一个 2×2 维度的矩阵。核从图像的左上角开始,向右移动直到处理完整个图像的宽度。然后核向下移动,并从图像的左侧重新开始,重复这一过程直到处理完整个图像。每次核的移动被称为步长。对于核来说,步长长度是一个重要的超参数。如果步长长度为 1,核在每次步长中移动一步。图 5.8 展示了步长长度为 1 的核。如图所示,核从图像的左上角开始,需要执行三个步长来处理整个图像。

图 5.8 展示了卷积层如何从输入图像创建特征图的示例
在每次步长中,被处理的图像部分与核进行卷积。正如我们在第二章中讨论 GAMs 时所见,卷积操作本质上是一个点积。对与核处理的图像部分进行逐元素乘积,然后求和。在图 5.8 中,我们可以看到所有步长的这种表示。例如,对于步长 0,通过核处理的图像部分由虚线框突出显示。这个图像与核进行点积得到的结果是 3,这个值被放置在特征图矩阵的左上角。在步长 1 中,我们向右移动一步并再次执行卷积操作。卷积后得到的结果是 7,这个值被放置在特征图矩阵的右上角。这个过程一直重复,直到处理完整个图像。在卷积操作结束时,我们得到一个 2×2 大小的特征图矩阵,它旨在捕获输入图像的高级特征表示。核或滤波器内的数字被称为权重。请注意,在图 5.8 中,卷积层使用了相同的权重。这种权重共享使得 CNN 的训练比 DNNs 更加高效。
学习算法的目标是确定卷积层内核或过滤器中的权重。这是在反向传播过程中完成的。特征图矩阵的大小由几个超参数决定——输入图像的大小、内核的大小、步长长度以及另一个称为填充的超参数。填充是指在执行卷积操作之前添加到图像中的像素数。在图 5.8 中,使用了 0 填充,即不向图像添加额外的像素。如果将填充设置为 1,则在图像周围添加一个像素边界,其中边界中的所有像素值都设置为 0,如图 5.9 所示。添加填充会增加特征图的大小,并允许更准确地表示图像。在实践中,卷积层由多个过滤器或内核组成。过滤器的数量是我们必须在训练之前指定的另一个超参数。

图 5.9 填充的示意图
CNN 中的卷积层通常后面跟着一个池化层。池化层的作用是进一步降低特征图的维度,以减少模型训练过程中所需的计算能力。一个常见的池化层是最大池化。就像在卷积层中一样,池化层也由一个过滤器组成。最大池化过滤器返回该过滤器覆盖的所有值的最大值,如图 5.10 所示。

图 5.10 最大池化的示意图
在过去十年中,CNN 在各种任务中取得了快速进展,例如图像识别、目标检测和图像分割。这得益于大量标注数据(ImageNet [www.image-net.org/]、CIFAR-10 和 CIFAR-100 [www.cs.toronto.edu/~kriz/cifar.html] 等为其一)和计算能力的提升,其中深度学习模型正在利用图形处理单元(GPU)的优势。图 5.11 展示了过去十年 CNN 研究进展,特别是在使用 ImageNet 数据集进行图像分类任务中。ImageNet 数据集是一个大型标注图像数据库,通常用于图像分类和目标检测任务。它包含超过一百万张图像,组织在一个包含超过 20,000 个标注类别的分层结构中。图 5.11 是从 Papers with Code (mng.bz/9K8o) 获得的,这是一个包含顶级(SoTA)机器学习技术的有用仓库。在性能方面的一个主要突破发生在 2013 年,使用了 AlexNet 架构。当前的顶级 CNN 基于名为残差网络(ResNet)的架构。其中一些 SoTA 架构已经在 PyTorch 和 Keras 等深度学习框架中实现。我们将在下一节中看到如何使用它们,在那里我们将训练一个用于 IDC 检测任务的 CNN。我们将特别关注 ResNet 架构,因为它是最广泛使用的架构之一。

图 5.11 在 ImageNet 数据集上进行图像分类的顶级 CNN 架构(来源:mng.bz/9K8o)
5.3.1 数据准备
在本节中,我们将为模型训练准备数据。数据准备与之前章节略有不同,因为我们处理的是图像而不是结构化的表格数据。请注意,这里使用的是预处理后的数据集。用于预处理的代码和预处理后的数据集可以在与本书相关的 GitHub 仓库(mng.bz/KBdZ)中找到。首先,让我们准备训练集、验证集和测试集。重要的是我们不要通过补丁来分割数据,而是使用患者 ID。这可以防止训练集、验证集和测试集之间的数据泄露。如果我们随机通过补丁分割数据集,一个患者的补丁可能会出现在所有三个集中,因此可能会泄露一些关于患者的信息。以下代码片段展示了如何通过患者 ID 来分割数据集:
df_data = pd.read_csv('data/chapter_05_idc.csv') ①
patient_ids = df_data.patient_id.unique() ②
train_ids, val_test_ids = train_test_split(patient_ids, ③
test_size=0.4, ③
random_state=24) ③
val_ids, test_ids = train_test_split(val_test_ids, ④
test_size=0.5, ④
random_state=24) ④
df_train =
➥ df_data[df_data['patient_id'].isin(train_ids)].reset_index(drop=True) ⑤
df_val = df_data[df_data['patient_id'].isin(val_ids)].reset_index(drop=True) ⑥
df_test =
➥ df_data[df_data['patient_id'].isin(test_ids)].reset_index(drop=True) ⑦
① 将数据加载到 Pandas DataFrame 中
② 从数据中提取所有唯一的患者 ID
③ 将数据分割为训练集和验证/测试集
④ 将验证/测试集分割为单独的验证集和测试集
⑤ 从训练集中的患者 ID 提取所有补丁
⑥ 提取验证集中患者 ID 的所有补丁
⑦ 提取测试集中患者 ID 的所有补丁
注意,60% 的患者位于训练集中,20% 位于验证集中,剩余的 20% 位于测试集中。现在让我们检查目标变量的分布是否在三个集中相似,如图 5.12 所示。我们可以看到,在所有三个集中,大约 25-30% 的补丁是 IDC 阳性,而 70-75% 是 IDC 阴性。

图 5.12 训练集、验证集和测试集中的目标变量分布
现在让我们创建一个自定义类,以便轻松加载补丁的图像及其相应的标签。PyTorch 提供了一个名为 Dataset 的类用于此目的。在本章中,我们将扩展此类以用于 IDC 数据集。有关 Dataset 类以及 PyTorch 的更多详细信息,请参阅附录 A。以下代码示例展示了如何进行:
from torch.utils.data import Dataset ①
class PatchDataset(Dataset): ②
def __init__(self, df_data, images_dir, transform=None): ③
super().__init__() ③
self.data = list(df_data.itertuples(name='Patch', index=False)) ③
self.images_dir = images_dir ③
self.transform = transform ③
def __len__(self): ④
return len(self.data) ④
def __getitem__(self, index): ⑤
image_id, label = self.data[index].image_id, self.data[index].target ⑥
image = Image.open(os.path.join(self.images_dir, image_id)) ⑦
image = image.convert('RGB') ⑦
if self.transform is not None: ⑧
image = self.transform(image) ⑧
return image, label ⑨
① 加载 PyTorch 提供的数据集类
② 为图像补丁创建一个新的数据集类,该类扩展了 PyTorch 类
③ 一个构造函数,用于初始化补丁列表,以及包含图像和任何图像转换器的目录
④ 覆盖了 __len__ 方法以返回数据集中图像补丁的数量
⑤ 覆盖了 __getitem__ 方法以从数据集的位置索引返回图像和标签
⑥ 从数据集中提取图像 ID 和标签
⑦ 打开图像并将其转换为 RGB
⑧ 如果定义了转换,则应用于图像
⑨ 返回图像和标签
现在让我们定义一个函数来转换补丁的图像。常见的图像转换,如裁剪、翻转、旋转和调整大小,在 torchvision 包中得到实现。完整的转换列表可以在 mng.bz/jy6p 找到。以下代码片段展示了在训练集中的图像上执行了五个转换。作为一个数据增强步骤,第二个和第三个转换随机地围绕水平和垂直轴翻转图像。作为一个练习,为验证集和测试集创建转换。请注意,在验证集和测试集上,您不需要通过水平或垂直翻转图像来增强数据。您可以命名转换 trans_val 和 trans_test:
import torchvision.transforms as transforms ①
trans_train = transforms.Compose([ ②
transforms.Resize((50, 50)), ③
transforms.RandomHorizontalFlip(), ④
transforms.RandomVerticalFlip(), ⑤
transforms.ToTensor(), ⑥
transforms.Normalize(mean=[0.5, 0.5, 0.5], ⑦
std=[0.5, 0.5, 0.5])]) ⑦
① 导入 PyTorch 提供的转换模块
② 使用 Compose 类将多个转换组合在一起
③ 第一个转换将图像调整大小到 50 × 50 像素。
④ 第二个转换围绕水平轴翻转图像。
⑤ 第三个转换围绕垂直轴翻转图像。
⑥ 第四个转换将图像转换为 NumPy 数组。
⑦ 第五个转换对图像进行归一化。
在数据集类和转换就绪后,我们现在可以初始化数据集和加载器。以下代码片段展示了如何为训练集初始化它。PyTorch 提供的 DataLoader 类允许您批量数据、打乱数据,并使用多进程工作器并行加载:
from torch.utils.data import DataLoader ①
dataset_train = PatchDataset(df_data=df_train, ②
images_dir=all_images_dir, ②
transform=trans_train) ②
batch_size = 64 ③
loader_train = DataLoader(dataset=dataset_train, ④
batch_size=batch_size, ④
shuffle=True, ④
num_workers=0) ④
① 使用 PyTorch 中的 DataLoader 类以批量迭代数据
② 创建训练集中切片的数据集
③ 以 64 批次的形式加载切片的图像和标签
④ 创建训练集的数据加载器
作为练习,我鼓励您为验证集和测试集创建类似的数据集和加载器,分别命名为 dataset_val 和 dataset_test。这些练习的解决方案可以在与本书相关的 GitHub 仓库中找到 (mng.bz/KBdZ)。
5.3.2 训练和评估 CNN
在数据集和加载器就绪后,我们现在可以创建 CNN 模型。我们将使用 PyTorch 中的 torchvision 包实现的 ResNet 架构。使用 torchvision (mng.bz/jy6p),您还可以初始化其他最先进的架构,例如 AlexNet、VGG、Inception 和 ResNeXt。您还可以通过将预训练标志设置为 true 来加载带有预训练权重的这些模型架构。如果设置为 true,该包将返回在 ImageNet 数据集上预训练的模型。对于本章中的 IDC 检测示例,我们将不使用预训练模型,因为它会随机初始化模型权重,并且模型将使用包含组织切片图像的新数据集从头开始训练。作为一个练习,我鼓励您将 pretrained 参数设置为 True 以初始化通过在 ImageNet 数据集上训练获得的权重。
我们还需要将全连接层连接到 CNN 以执行二进制分类任务。我们可以使用以下代码片段来初始化 CNN:
# Hyper parameters
num_classes = 2 ①
# Device configuration
device = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu') ②
# Use the ResNet architecture for the CNN
model = torchvision.models.resnet18(pretrained=False) ③
num_features = model.fc.in_features ③
# Create the fully connected layers for classification
model.fc = nn.Sequential( ④
nn.Linear(num_features, 512),
nn.ReLU(),
nn.BatchNorm1d(512),
nn.Dropout(0.5),
nn.Linear(512, 256),
nn.ReLU(),
nn.BatchNorm1d(256),
nn.Dropout(0.5),
nn.Linear(256, num_classes))
model = model.to(device) ⑤
① 设置数据集中的类别数量,在本例中为二进制
② 如果可用 CUDA,则使用 GPU 设备;否则,将设备设置为 CPU
③ 初始化 ResNet 模型并从模型中提取特征数量
④ 将全连接层连接到 ResNet 模型以进行分类
⑤ 将模型转移到设备上
注意,默认情况下模型是在 CPU 上加载的。为了加快处理速度,您可以将模型加载到 GPU 上。所有流行的深度学习框架,包括 PyTorch,都使用 CUDA(代表计算统一设备架构)在 GPU 上执行通用计算。CUDA 是由 NVIDIA 构建的平台,它提供了直接访问 GPU 的 API。
我们可以使用以下代码片段来训练模型。注意,在这个例子中,模型训练了五个周期。使用 IDC 数据集训练这个复杂模型在 CPU 上的训练时间大约是 17 小时。如果在一个 GPU 实例上执行,训练时间会更短。你还可以通过增加周期数或延长训练时间来提高模型性能:
# Hyper parameters
num_epochs = 5
learning_rate = 0.002
# Criterion or loss function
criterion = nn.CrossEntropyLoss()
# Optimizer for CNN
optimizer = torch.optim.Adamax(model.parameters(), lr=learning_rate)
for epoch in range(num_epochs):
model.train()
for idx, (inputs, labels) in enumerate(loader_train):
inputs = inputs.to(device, dtype=torch.float)
labels = labels.to(device, dtype=torch.long)
# zero the parameter gradients
optimizer.zero_grad()
with torch.set_grad_enabled(True):
outputs = model(inputs)
_, preds = torch.max(outputs, 1)
loss = criterion(outputs, labels)
# backpropagation
loss.backward()
optimizer.step()
现在我们来看看这个模型在测试集上的表现如何。正如我们在前面的章节中所做的那样,我们将模型性能与一个合理的基线进行比较。我们在 5.2 节中看到,目标类别高度不平衡(见图 5.2),其中 IDC 阴性是多数类别。基线模型的一个选择是始终预测多数类别,即始终预测组织片是 IDC 阴性。然而,这种基线并不合理,因为在医疗保健领域,特别是癌症诊断中,假阴性的成本远大于假阳性。一个更合理的策略是偏向于假阳性——始终预测给定的组织片是 IDC 阳性。尽管这种策略并不理想,但它至少确保了所有阳性病例都被正确识别。在现实情况下,基线模型通常是人为或专家(在这种情况下,由专家病理学家进行的评估)或企业正在使用的现有模型的预测。不幸的是,对于这个例子,我们无法访问这些信息,因此使用始终预测 IDC 阳性的基线模型。
表 5.1 展示了用于基准测试模型的三个关键性能指标:精确率、召回率和 F1 分数。精确率衡量预测类别中准确的比率。召回率衡量模型准确预测的实际类别的比率。F1 分数是精确率和召回率的调和平均值。请参阅第三章以获取这些指标的更详细解释。
如果我们查看表 5.1 中的召回率指标,基线模型比 CNN 模型表现更好。这是预期的,因为基线模型始终预测 IDC 阳性。然而,总的来说,CNN 模型比基线模型表现要好得多,实现了 74.4% 的精确率(比基线高 45.8%)和 74.2% 的 F1 分数(比基线高 29.7%)。作为一个练习,我鼓励你调整模型,通过增加周期数或改变 CNN 架构来延长训练时间,以实现更高的性能:
表 5.1 基线模型与 CNN 模型的性能比较
| 精确率 (%) | 召回率 (%) | F1 分数 (%) | |
|---|---|---|---|
| 基线模型 | 28.6 | 100 | 44.5 |
| CNN 模型(ResNet) | 74.4 (+45.8) | 74.1 (–25.9) | 74.2 (+29.7) |
随着 CNN 模型比基线模型表现更好,现在让我们来解释它,并了解这个黑盒模型是如何得出最终预测的。
5.4 解释 CNN
如前节所述,使用卷积神经网络(CNN)进行预测时,图像会经过多个卷积和池化层进行特征学习,随后通过多个全连接的深度神经网络层进行分类。对于用于 IDC 检测的 ResNet 模型,训练过程中学习的总参数数量为11,572,546。网络中正在执行数百万个复杂操作,因此理解模型如何得出最终预测变得极其困难。这正是 CNN 成为黑盒的原因。
5.4.1 概率景观
在前一章中,我们了解到一种解释深度神经网络(DNN)的方法是通过可视化边缘权重的强度。通过这种技术,我们可以从高层次上看到输入特征对最终模型预测的影响。然而,这种方法不能应用于 CNN,因为可视化卷积层中的核(或过滤器)及其对学习到的中间特征和最终模型输出的影响并非易事。不过,我们可以可视化 CNN 的概率景观。什么是概率景观?在二元分类的上下文中使用 CNN,我们实际上得到了目标类的概率度量。在 IDC 检测的情况下,我们从 CNN 中得到给定输入块是 IDC 阳性的概率。对于组织中的所有块,我们可以绘制分类器的输出概率并将其可视化为一个热图。将此热图叠加到图像上可以给我们指示热点区域,其中 CNN 检测到高度可能的 IDC 阳性区域。这就是概率景观。

图 5.13 整个组织样本的 ResNet 模型的概率景观
图 5.13 显示了三个图表。最左侧的图表是患者 12930 的所有块的可视化。中间的图表突出显示了基于从专家病理学家收集的地面真实标签的 IDC 阳性块。这两个图表与我们在 5.2 节中看到的图表类似(见图 5.5)。最右侧的图表显示了用于检测 IDC 的 ResNet 模型的概率景观。颜色越亮,给定块是 IDC 阳性的概率就越大。通过与地面真实情况进行比较,我们可以看到模型预测和地面真实之间存在良好的重叠。然而,也有一些假阳性,模型突出显示的区域并不一定是 IDC 阳性。图 5.13 的实现可以在与本书相关的 GitHub 仓库(mng.bz/KBdZ)中找到。您可以加载 5.3.2 节中训练的模型,直接进入模型可解释性。
可视化概率景观是验证模型输出的好方法。通过与真实标签进行比较,我们可以看到模型在哪些情况下出错,并相应地调整模型。这也是在将模型部署到生产环境中后可视化并监控模型输出的好方法。然而,概率景观并没有告诉我们模型是如何到达预测的。
5.4.2 LIME
解释卷积神经网络(CNN)的一种方法是通过使用我们在上一章中学到的任何一种模型无关的可解释性技术。让我们具体看看如何将LIME可解释性技术应用于图像和 CNN。为了回顾,LIME 技术是一种局部范围内的模型无关技术。在一个表格数据集上,该技术的工作方式如下:
-
选择一个例子进行解释。
-
通过从表格数据集中特征的均值和标准差进行高斯分布采样来创建扰动数据集。
-
将扰动数据集通过黑盒模型运行,并获取预测结果。
-
根据样本与所选例子的接近程度对样本进行加权,其中接近所选例子的样本被赋予更高的权重。正如我们在第四章中看到的,一个称为核宽度的超参数用于对样本进行加权。如果核宽度小,只有接近所选实例的样本才会影响解释。
-
最后,在加权样本上拟合一个易于解释的白盒模型。对于 LIME,使用线性回归。
线性回归模型的权重可以用来确定所选例子的特征重要性。通过使用一个局部忠实于我们希望解释的例子的代理模型,我们可以获得解释。现在,我们如何将 LIME 应用于图像?与表格数据类似,我们首先需要选择一个我们希望解释的图像。接下来,我们必须创建一个扰动数据集。我们不能像处理表格数据那样扰动数据集,通过从高斯分布中进行采样。相反,我们在图像中随机打开和关闭像素。这计算量很大,因为为了生成一个局部忠实的解释,我们必须生成大量的样本来运行模型。此外,像素可能在空间上相关,多个像素可能对同一个目标类别做出贡献。因此,我们将图像分割成多个部分,也称为超像素,并随机打开和关闭超像素,如图 5.14 所示。

图 5.14 如何为 LIME 创建扰动图像的说明
我们可以从底部到顶部阅读图 5.14。其思想是通过将多个像素分组到超像素中来分割原始图像。在这个示例中,我们使用了一个简单的分割算法,将原始图像分割成四个不重叠的矩形段。一旦你用超像素形成了分割图像,你可以通过打开和关闭随机超像素来创建扰动图像。默认情况下,LIME 实现使用 quickshift (mng.bz/W7mw) 分割算法。一旦创建了扰动数据集,其余的技术与表格数据相同。线性代理模型的权重将给我们一个关于特征或超像素对所选输入图像最终模型预测影响的直观认识。我们通过将图像分割成超像素,因为我们试图将相关的像素分组在一起,并观察对最终预测的影响。
现在我们来看如何实现之前训练的 ResNet 模型的 LIME。我们首先需要将之前使用的单个 PyTorch 转换器分成两个。第一个将输入的 Python Imaging Library (PIL) 图像转换为 50 × 50 的张量,第二个将对其进行归一化。下面的第一个转换是 LIME 图像分割算法所必需的:
trans_pil = transforms.Compose([transforms.Resize((50, 50)),]) ①
trans_pre = transforms.Compose([transforms.ToTensor(),
transforms.Normalize(mean=[0.5, 0.5, 0.5], ②
std=[0.5, 0.5, 0.5])]) ②
① 将输入图像调整大小为 50 × 50 像素,这是图像分割算法所需的第一个转换
② 第二个转换,用于归一化转换后的 50 × 50 输入图像
接下来,我们需要两个辅助函数——一个用于将图像文件加载为 PIL 图像,另一个用于使用模型对扰动数据集进行预测。下面的代码片段显示了这些函数,其中 get_image 是加载 PIL 图像的函数,batch_predict 是将扰动 images 通过 model 运行的函数。我们还创建了一个部分函数,它预先设置了 ResNet 模型参数,该参数是我们之前章节中训练的:
def get_image(images_dir, image_id): ①
image = Image.open(os.path.join(images_dir, image_id)) ②
image = image.convert('RGB') ②
return image ③
def batch_predict(images, model): ④
def sigmoid(x): ⑤
return 1\. / (1 + np.exp(-x)) ⑤
batch = torch.stack(tuple(trans_pre(i) for i in images), dim=0) ⑥
outputs = model(batch) ⑦
proba = outputs.detach().cpu().numpy().astype(np.float) ⑧
return sigmoid(proba) ⑨
from functools import partial ⑩
batch_predict_with_model = partial(batch_predict, model=model) ⑩
① 一个辅助函数,用于将输入 RGB 图像读取到内存中
② 打开图像并将其转换为 RGB
③ 返回图像
④ 一个辅助函数,用于在扰动数据集中的图像上进行预测
⑤ 一个用于计算输入参数的 sigmoid 函数
⑥ 将输入图像的所有转换张量堆叠在一起
⑦ 通过模型运行以获得所有图像的输出
⑧ 断开输出张量并将其转换为 NumPy 数组
⑨ 通过通过 sigmoid 函数传递,将预测作为概率返回
⑩ 一个部分函数,用于使用预训练的 ResNet 模型执行批量预测
注意,在这段代码中,我们定义了一个名为batch_predict_with_model的部分函数。Python 中的部分函数允许我们在函数中设置一定数量的参数并生成一个新的函数。我们正在使用batch_predict函数,并使用之前训练的 ResNet 模型设置model参数。你可以用任何其他你希望使用 LIME 进行解释的模型替换它。
因为 LIME 是一种局部可解释技术,我们需要挑选一个示例进行解释。对于 ResNet 模型,我们将挑选两个补丁进行解释——一个是 IDC 阴性,另一个是 IDC 阳性——来自测试集,如图所示:
non_idc_idx = 142 ①
idc_idx = 41291 ②
non_idc_image = get_image(all_images_dir, ③
df_test.iloc[non_idc_idx, :]['image_id']) ③
idc_image = get_image(all_images_dir, ④
df_test.iloc[idc_idx, :]['image_id']) ④
① 一个 ID 为 142 的 IDC 阴性示例
② 一个 ID 为 41291 的 IDC 阳性示例
③ 加载 IDC 阴性示例的 PIL 图像
④ 加载 IDC 阳性示例的 PIL 图像
现在我们将初始化 LIME 解释器,并使用它来解释我们挑选的两个示例。以下代码片段展示了如何获取 IDC 阴性示例的 LIME 解释。作为练习,获取 IDC 阳性示例的 LIME 解释并将其赋值给名为idc_exp的变量:
from lime import lime_image ①
explainer = lime_image.LimeImageExplainer() ②
non_idc_exp = explainer.explain_instance(np.array(trans_pil(non_idc_image)), ③
batch_predict_with_model, ④
num_samples=1000) ⑤
① 从 LIME 库中导入 lime_image 模块
② 初始化 LIME 图像解释器
③ 首先将 IDC 阴性图像进行分割转换
④ 将部分函数传递给 ResNet 模型,在扰动数据集上进行预测
⑤ 对分割图像进行扰动以创建 1,000 个样本
使用前一段代码中显示的 LIME 解释变量,获取包含解释的 RGB 图像和 2D 掩码。作为练习,获取 IDC 阳性示例的掩码 LIME 图像,并将其命名为i_img_boundary。为此,你需要完成之前的练习,首先获取 IDC 阳性示例的 LIME 解释。这些练习的解决方案可以在与本书相关的 GitHub 仓库(mng.bz/KBdZ)中找到:
from skimage.segmentation import mark_boundaries ①
ni_tmp, ni_mask = non_idc_exp.get_image_and_mask(non_idc_exp.top_labels[0],
positive_only=False,
num_features=20,
hide_rest=True) ②
ni_img_boundary = mark_boundaries(ni_tmp/255.0, ni_mask) ③
① 从 skimage 库中导入 mark_boundaries 函数以绘制分割图像
② 获取 IDC 阴性示例的掩码 LIME 图像
③ 使用 mark_boundaries 函数绘制掩码图像
我们现在可以使用以下代码可视化 IDC 阳性和阴性补丁的 LIME 解释:
non_idc_conf = 100 - df_test_with_preds.iloc[non_idc_idx]['proba'] * 100 ①
idc_conf = df_test_with_preds.iloc[idc_idx]['proba'] * 100 ②
non_idc_image = df_test.iloc[non_idc_idx]['image_id'] ③
idc_image = df_test.iloc[idc_idx]['image_id'] ④
non_idc_patient = df_test.iloc[non_idc_idx]['patient_id'] ⑤
idc_patient = df_test.iloc[idc_idx]['patient_id'] ⑥
f, ax = plt.subplots(2, 2, figsize=(10, 10)) ⑦
# Plot the original image of the IDC negative patch
ax[0][0].imshow(Image.fromarray(imread(os.path.join(all_images_dir,
➥ non_idc_image)))) ⑧
ax[0][0].axis('off') ⑧
ax[0][0].set_title('Patch Image (IDC Negative)\nPatient Id: %d' % ⑧
➥ non_idc_patient) ⑧
# Plot the LIME explanation for the IDC negative patch
ax[0][1].imshow(ni_img_boundary) ⑨
ax[0][1].axis('off') ⑨
ax[0][1].set_title('LIME Explanation (IDC Negative)\nModel Confidence: ⑨
➥ %.1f%%' % non_idc_conf) ⑨
# Plot the original image of the IDC positive patch
ax[1][0].imshow(Image.fromarray(imread(os.path.join(all_images_dir,
➥ idc_image)))) ⑩
ax[1][0].axis('off') ⑩
ax[1][0].set_title('Patch Image (IDC Positive)\nPatient Id: %d' % ⑩
➥ idc_patient) ⑩
# Plot the LIME explanation for the IDC positive patch
ax[1][1].imshow(i_img_boundary) ⑪
ax[1][1].axis('off') ⑪
ax[1][1].set_title('LIME Explanation (IDC Positive)\nModel Confidence: ⑪
➥ %.1f%%' % idc_conf); ⑪
① 获取 IDC 阴性补丁的模型置信度
② 获取 IDC 阳性补丁的模型置信度
③ 获取 IDC 阴性补丁的图像
④ 获取 IDC 阳性补丁的图像
⑤ 获取 IDC 阴性补丁的患者 ID
⑥ 获取 IDC 阳性补丁的患者 ID
⑦ 创建一个 2×2 的图形来绘制原始图像和 LIME 解释
⑧ 在左上角单元格中绘制 IDC 阴性补丁的原始图像
⑨ 在右上角单元格中绘制 IDC 阴性补丁的 LIME 解释
⑩ 在左下角单元格中绘制 IDC 阳性补丁的原始图像
⑪ 在右下角单元格中绘制 IDC 阳性补丁的 LIME 解释
图 5.15 显示了生成的可视化结果。图中已标注,左上角图像是 IDC 阴性区域的原始图像。右上角图像是 IDC 阴性区域的 LIME 解释。我们可以看到模型预测该区域为 IDC 阴性,置信度为 82%。从原始图像中,我们可以看到较浅染色质的密度较高,这与我们在第 5.2 节(图 5.4)中看到的模式相匹配。较浅染色质通常用于突出细胞质和细胞外结缔组织。如果我们查看 LIME 解释,我们可以看到分割算法突出了两个超像素,分割边界将高密度较浅染色质与其他图像部分分开。对预测产生正面影响的区域或超像素用红色(较深的色调)表示,这标注为分割图像的左侧。对预测产生负面影响的区域或超像素用绿色(较浅的色调)表示,这标注为分割图像的右侧。因此,LIME 解释似乎正确地突出了密集的较浅染色质,并作为预测 IDC 阴性具有高置信度的贡献因素。

图 5.15 展示了 LIME 对 IDC 阴性区域和 IDC 阳性区域的解释。
图 5.15 左下角的图像是 IDC 阳性区域的原始图像。该区域的 LIME 解释显示在图的右下角。我们可以从原始图像中看到较深染色质的密度要高得多,这与我们在第 5.2 节(图 5.3)中看到的模式相匹配。如果我们现在查看 LIME 解释,我们可以看到分割算法将整个图像视为超像素,整个超像素对预测 IDC 阳性具有高置信度产生正面影响。尽管这种解释在高级别上是合理的,因为整个图像由高密度的较深染色质组成,但它并没有给我们提供任何关于哪些特定像素影响模型预测的额外信息。
这让我们想到了 LIME 的一些缺点。正如我们在第四章和本节中看到的,LIME 是一种非常出色的可解释性技术,因为它对模型没有依赖性,可以应用于任何复杂的模型。然而,它也有一些缺点。LIME 解释的质量很大程度上取决于核宽度的选择。正如我们在第四章中看到的,这是一个重要的超参数,相同的核宽度可能不适用于我们希望解释的所有示例。LIME 解释也可能不稳定,因为它们依赖于扰动数据集的采样方式。解释还依赖于我们使用的特定分割算法。正如我们在图 5.15 中看到的,分割算法将整个图像视为一个超级像素。LIME 的计算复杂度也较高,这取决于需要打开或关闭的像素或超级像素的数量。
5.4.3 可视化归因方法
现在,让我们退一步,从更广泛的可解释性方法类别的角度来审视 LIME,这类方法被称为可视化归因方法。可视化归因方法用于将重要性归因于影响 CNN 做出的预测的图像部分。以下列出了三种广泛的可视化归因方法类别,并在图 5.16 中展示:
-
干扰
-
梯度
-
激活

图 5.16 可视化归因方法的类型
类似于 LIME 和 SHAP 的可解释性技术是基于扰动的方 法。正如我们在第四章和前一节中看到的,其想法是扰动输入并探测其对 CNN 做出的预测的影响。这些技术是模型无关的、事后分析和局部可解释性技术。然而,基于扰动的方 法在计算上效率不高,因为每次扰动都需要我们在复杂的 CNN 模型上执行前向传递。这些技术也可能低估特征的重要性,其中特征是基于对原始图像进行的分割的部分。
基于梯度的方法 用于可视化目标类别相对于输入图像的梯度。想法是选择一个示例或图像进行解释。然后,我们将此图像通过 CNN 的正向方向运行以获得输出预测。我们应用反向传播算法来计算输出类别相对于输入图像的梯度。梯度是一个很好的重要性度量,因为它告诉我们哪些像素需要改变才能影响模型输出。如果梯度的幅度很大,那么对像素值的微小改变将导致输入的大幅变化。因此,具有大梯度度量的像素被认为对模型来说是最重要的,或是最显著的。基于梯度的方法有时也被称为 反向传播方法,因为反向传播算法用于确定特征重要性。它们也被称为 显著性图,因为可以获得一个显著或重要的特征图。流行的基于梯度的方法包括 vanilla backpropagation、引导反向传播、积分梯度 和 SmoothGrad,这些将在 5.5–5.7 节中介绍。这些技术在范围上是局部的,也是事后的。然而,它们并不是完全模型无关的,并且是弱模型依赖的。与基于扰动的方
基于激活的方法 会查看最终卷积层的特征图或激活,并根据目标类别对这些特征图的梯度进行加权。特征图的权重作为输入特征重要性的代理。这种技术被称为梯度加权类激活映射 (Grad-CAM)。因为我们关注的是最终卷积层中特征图的重要性,所以 Grad-CAM 提供了一个 粗粒度 的激活图。为了获得更 细粒度 的激活图,我们可以结合 Grad-CAM 和引导反向传播——这种技术被称为 引导 Grad-CAM。我们将在 5.8 节中更详细地了解 Grad-CAM 和引导 Grad-CAM 的工作原理。基于激活的方法也是弱模型依赖、事后和局部可解释的技术。
5.5 梯度下降法
在本节中,我们将学习一种基于梯度的归因方法,称为 vanilla backpropagation。vanilla backpropagation 由 Karen Simonyan 等人在 2014 年提出,该技术如图 5.17 所示。

图 5.17 梯度下降法的示意图
第一步是选择一个图像或示例进行解释。因为我们正在查看解释单个实例,所以这种可解释性技术的范围是局部的。第二步是在 CNN 上执行前向传播以获得输出类别预测。一旦获得了输出类别,下一步就是获得输出相对于倒数第二层的梯度并执行反向传播——我们在第四章中学习了这一点——以最终获得输出类别相对于输入图像中像素的梯度。输入像素或特征的梯度被用作重要指标。像素的梯度越大,该像素对模型预测输出类的重要性就越大。其背后的直觉是,如果给定像素的梯度幅度很大,那么像素值的微小变化将对模型预测产生更大的影响。
纯反向传播和其他基于梯度的方法已在 PyTorch 中由 Utku Ozbulak 实现,并在本 GitHub 仓库中开源:mng.bz/8l8B。然而,这些实现不能直接应用于 ResNet 架构或基于 ResNet 的架构,因此我在本书中对其进行了修改,以便它们可以应用于这些更高级的架构。下面的代码片段实现了纯反向传播技术作为一个 Python 类:
# Code below adapted from: http://mng.bz/8l8B
class VanillaBackprop():
"""
Produces gradients generated with vanilla back propagation from the
➥ image
"""
def __init__(self, model, features): ①
self.model = model ②
self.gradients = None ③
# Put model in evaluation mode
self.model.eval() ④
# Set feature layers
self.features = features ⑤
# Hook the first layer to get the gradient
self.hook_layers() ⑥
def hook_layers(self): ⑦
def hook_function(module, grad_in, grad_out): ⑧
self.grad_in = grad_in ⑨
self.grad_out = grad_out ⑩
self.gradients = grad_in[0] ⑪
# Register hook to the first layer
first_layer = list(self.features._modules.items())[0][1] ⑫
first_layer.register_backward_hook(hook_function) ⑬
def generate_gradients(self, input_image, target_class): ⑭
# Forward
model_output = self.model(input_image) ⑮
# Zero grads
self.model.zero_grad() ⑯
# Target for backprop
one_hot_output = torch.FloatTensor(1, model_output.size()[-1]).zero_() ⑰
one_hot_output[0][target_class] = 1 ⑰
# Backward pass
model_output.backward(gradient=one_hot_output) ⑱
gradients_as_arr = self.gradients.data.numpy()[0] ⑲
return gradients_as_arr ⑲
① 一个用于纯反向传播的构造函数,它接受模型和特征层的起始部分
② 初始化模型对象
③ 将梯度对象初始化为 None
④ 将模型设置为评估模式
⑤ 设置指向模型中特征层起始部分的 features 对象
⑥ 钩住层以便可以计算输出相对于输入像素的梯度
⑦ 一个用于钩住第一层以获取梯度的函数
⑧ 用于在反向传播过程中处理输入和输出梯度的辅助函数
⑨ 将 grad_in 对象设置为从上一层获得的梯度
⑩ 将 grad_out 对象设置为从当前层获得的梯度
⑪ 获得当前层相对于特征图像素的梯度
⑫ 获得第一个特征层
⑬ 注册反向钩子函数以获得输出类别相对于输入像素的梯度
⑭ 一个执行反向传播以获得梯度的函数
⑮ 通过在模型中前向传播图像来获得模型输出
⑯ 在反向传播之前将梯度重置为 0
⑰ 创建一个将目标类别设置为 1 的一热编码张量
⑱ 执行反向传播
⑲ 通过钩子函数返回获得的梯度对象
注意,ResNet 模型和其他架构(如 Inception v3 和 ResNeXt)的特征层可以在父模型中找到,并且它们不是以 VGG16 和 AlexNet 架构中的层次结构存储,在 VGG16 和 AlexNet 架构中,特征层存储在模型的features键中。你可以通过以下方式初始化 VGG16 模型来测试这一点,并打印其结构:
vgg16 = torchvision.models.vgg16()
print(vgg16)
下一个打印语句的输出如下所示。输出被截断,目的是展示特征层是如何存储在features键中的。如果你将上一段代码中的vgg16替换为alexnet,你将得到类似的输出:
VGG(
(features): Sequential(
(0): Conv2d(3, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(1): ReLU(inplace=True)
(2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(3): ReLU(inplace=True)
(4): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
(5): Conv2d(64, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(6): ReLU(inplace=True)
...
(output clipped)
Utku Ozbulak 的实现期望架构具有与 VGG16 和 AlexNet 相同的层次结构。另一方面,在先前的标准反向传播实现中,特征层被显式地传递给构造函数,以便它可以用于更复杂的架构。现在,你可以如下实例化这个类来为 ResNet 模型使用:
vbp = VanillaBackprop(model=model, features=model)
现在,我们将创建一个辅助函数来获取输出相对于输入的梯度,如下所示:
def get_grads(gradient_method, dataset, idx): ①
image, label = dataset[idx] ②
X = image.reshape(1, ③
image.shape[0], ③
image.shape[1], ③
image.shape[2]) ③
X_var = Variable(X, requires_grad=True) ④
grads = gradient_method.generate_gradients(X_var, label) ⑤
return grads ⑥
① get_grads函数接收基于梯度的方法、数据集和要解释的示例索引。
② 在索引 idx 处获取图像和标签
③ 将图像重塑为可以通过模型运行
④ 创建一个 PyTorch 变量,其中 requires_grad 为 True,以通过反向传播获取梯度
⑤ 使用generate_gradients函数获取梯度
⑥ 返回相对于输入像素的梯度
我们将使用与第 5.4.2 节中 LIME 技术相同的两个示例——一个 IDC 负片和一个 IDC 正片。现在,我们可以使用标准的反向传播技术来获取梯度,如下所示:
non_idc_vanilla_grads = get_grads(vbp, dataset_test, non_idc_idx)
idc_vanilla_grads = get_ grads(vbp, dataset_test, idc_idx)
注意,测试数据集是我们第 5.3.1 节中初始化的PatchDataset。这里显示的梯度数组将与输入图像具有相同的维度。输入图像的维度为 3 × 50 × 50,其中包含三个通道(红色、绿色、蓝色),图像的高度和宽度均为 50 像素。生成的梯度也将具有相同的维度,可以将其可视化为彩色图像。然而,为了便于可视化,我们将梯度图像转换为灰度图。我们可以使用以下辅助函数将彩色图像转换为灰度图:
# Code below from: http://mng.bz/8l8B
def convert_to_grayscale(im_as_arr):
"""
Converts 3d image to grayscale
Args:
im_as_arr (numpy arr): RGB image with shape (D,W,H)
returns:
grayscale_im (numpy_arr): Grayscale image with shape (1,W,D)
"""
grayscale_im = np.sum(np.abs(im_as_arr), axis=0)
im_max = np.percentile(grayscale_im, 99)
im_min = np.min(grayscale_im)
grayscale_im = (np.clip((grayscale_im - im_min) / (im_max - im_min), 0, 1))
grayscale_im = np.expand_dims(grayscale_im, axis=0)
return grayscale_im
现在我们已经通过标准反向传播获得了梯度,我们可以像可视化 LIME 解释一样可视化它们。作为一个练习,我鼓励你将第 5.4.2 节中的可视化代码扩展,用梯度的灰度表示替换 LIME 解释。结果图如图 5.18 所示。

图 5.18 使用标准反向传播的显著性图
让我们先关注 IDC-阴性区域。该区域的原始图像显示在左上角。通过传统反向传播获得的梯度灰度表示显示在右上角。我们可以看到图像中有各种灰度的像素。较大的梯度具有更高的灰度强度或呈现为白色。这是一种很好的可视化 CNN 关注哪些像素来预测图像是 IDC 阴性且置信度为 82% 的方法。显著的或重要的像素对应于原始图像中密集的较亮污点。因为梯度在像素级别显示,所以这比 LIME 的解释要细粒度得多,LIME 只关注超级像素。显著性图是作为数据科学家或工程师调试 CNN 的好方法,也有助于专家病理学家了解 CNN 关注图像的哪些部分。
现在我们来看 IDC-阳性区域。原始图像显示在左下角,通过传统反向传播获得的解释显示在右下角。我们可以看到很多像素被点亮,这对应于输入图像中的较暗的污点或细胞核。这种解释比 LIME 的解释要好得多,在 LIME 中,整个图像被视为一个超级像素。
5.6 引导反向传播
引导反向传播 是由 J. T. Springenberg 等人在 2015 年提出的一种基于梯度的属性方法。它与传统的反向传播类似,唯一的区别在于它处理梯度通过修正线性单元(ReLU)的方式。正如我们在第四章中看到的,ReLU 是一个非线性激活函数,它将负输入值裁剪为零。引导反向传播技术会将梯度置零到 ReLU 中,如果梯度是负的,或者在正向传播过程中 ReLU 的输入是负的。引导反向传播背后的思想是只关注对模型预测产生积极影响的输入特征。
引导反向传播技术也已实现在 PyTorch 的 mng.bz/8l8B 仓库中,但在这本书中已经进行了适配,以便它可以应用于更复杂的架构,如 ResNet,其中包含具有 ReLU 的嵌套层。以下代码片段显示了改进的实现:
# Code below adapted from: http://mng.bz/8l8B
from torch.nn import ReLU, Sequential ①
class GuidedBackprop():
"""
Produces gradients generated with guided back propagation from the
➥ given image
"""
def __init__(self, model, features): ②
self.model = model ②
self.gradients = None ②
self.features = features ②
self.forward_relu_outputs = [] ②
# Put model in evaluation mode ②
self.model.eval() ②
self.update_relus() ②
self.hook_layers() ②
def hook_layers(self): ③
def hook_function(module, grad_in, grad_out): ③
self.gradients = grad_in[0] ③
# Register hook to the first layer ③
first_layer = list(self.features._modules.items())[0][1] ③
first_layer.register_backward_hook(hook_function) ③
def update_relus(self): ④
"""
Updates relu activation functions so that
1- stores output in forward pass
2- imputes zero for gradient values that are less than zero
"""
def relu_backward_hook_function(module, grad_in, grad_out): ⑤
"""
If there is a negative gradient, change it to zero
"""
# Get last forward output
corresponding_forward_output = self.forward_relu_outputs[-1] ⑥
corresponding_forward_output[corresponding_forward_output > 0] = 1 ⑦
modified_grad_out = corresponding_forward_output *
➥ torch.clamp(grad_in[0], min=0.0) ⑧
del self.forward_relu_outputs[-1] # ⑨
return (modified_grad_out,) ⑩
def relu_forward_hook_function(module, ten_in, ten_out): ⑪
"""
Store results of forward pass
"""
self.forward_relu_outputs.append(ten_out)
# Loop through layers, hook up ReLUs
for pos, module in self.features._modules.items(): ⑫
if isinstance(module, ReLU): ⑬
module.register_backward_hook(relu_backward_hook_function)
module.register_forward_hook(relu_forward_hook_function)
elif isinstance(module, Sequential): ⑭
for sub_pos, sub_module in module._modules.items(): ⑭
if isinstance(sub_module, ReLU): ⑮
sub_module.register_backward_hook(relu_backward_hook_function)
sub_module.register_forward_hook(relu_forward_hook_function)
elif isinstance(sub_module, torchvision.models.resnet.BasicBlock): ⑯
for subsub_pos, subsub_module in ⑯
➥ sub_module._modules.items(): ⑯
if isinstance(subsub_module, ReLU): ⑰
subsub_module.register_backward_hook(relu_backward_hook_function)
subsub_module.register_forward_hook(relu_forward_hook_function)
def generate_gradients(self, input_image, target_class): ⑱
# Forward pass ⑱
model_output = self.model(input_image) ⑱
# Zero gradients ⑱
self.model.zero_grad() ⑱
# Target for backprop ⑱
one_hot_output = torch.FloatTensor(1, model_output.size()[-1]).zero_()1*
one_hot_output[0][target_class] = 1 ⑱
# Backward pass ⑱
model_output.backward(gradient=one_hot_output) ⑱
gradients_as_arr = self.gradients.data.numpy()[0] ⑱
return gradients_as_arr ⑱
① 导入 ReLU 激活函数和 Sequential 容器
② 引导反向传播的构造函数类似于传统反向传播,只是在反向传播期间增加了一个更新 ReLU 的函数调用。
③ 一个用于钩子第一层以获取梯度的函数,类似于传统反向传播
④ 一个用于更新 ReLU 的函数
⑤ 一个用于对梯度值小于 0 的值进行零值插补的函数
⑥ 获取正向传播中 ReLU 的输出
⑦ 设置标识变量,其中正值设置为 1
⑧ 用 0 插补负梯度值
⑨ 移除最后的正向输出
⑩ 返回修改后的梯度
⑪ 一个辅助函数,用于在正向传播期间存储 ReLU 的输出
⑫ 遍历所有特征层
⑬ 如果模块是 ReLU,则注册钩子函数以在正向传播期间获取值并在反向传播期间裁剪梯度
⑭ 如果模块是 Sequential 容器,则遍历其子模块
⑮ 如果子模块是 ReLU,则像之前一样注册钩子函数
⑯ 如果子模块是 BasicBlock,则遍历其子模块
⑰ 如果子子模块是 ReLU,则像之前一样注册钩子函数
⑱ 一个执行反向传播以获取梯度的函数,类似于普通反向传播
我们可以使用这种修改后的实现来处理最多具有三个嵌套层和 ReLU 的模型架构。现在让我们如下实例化 ResNet 模型的引导反向传播类:
gbp = GuidedBackprop(model= model,
features=model)
我们现在可以使用在 5.5 节中定义的相同get_gradients辅助函数来获取梯度。作为一个练习,获取两个示例的梯度,将它们转换为灰度,然后可视化。结果图如图 5.19 所示。

图 5.19 使用引导反向传播的显著性图
使用引导反向传播的解释似乎表明模型关注了 IDC 阴性片和 IDC 阳性片中的更多像素。对于 IDC 阴性片,像素似乎对应于高密度浅色斑点的区域,而对于 IDC 阳性片,像素似乎对应于高密度深色斑点的区域。使用普通反向传播和引导反向传播的解释似乎都是合理的,但我们应该使用哪一个?我们将在 5.9 节中讨论这个问题。
5.7 其他基于梯度的方法
普通和引导反向传播方法都低估了在表现出饱和度的模型中特征的重要性。这意味着什么?让我们看看 2017 年 Avanti Shrikumar 等人发表的一篇论文中的简单例子,该论文可在arxiv.org/pdf/1704.02685;Learning找到。图 5.20 展示了一个表现出输出信号饱和的简单网络。网络接受两个输入,x1和x2。箭头或边上的数字是用于与它连接的输入单元相乘的权重。网络的最终输出(或输出信号)y可以评估如下:
y=1+max(0,1- (x1+x2))
如果x1 + x2大于 1,则输出信号y在 1 处饱和。我们可以看到,当输入之和大于 1 时,输出相对于输入的梯度为零。在这种情况下,普通反向传播和引导反向传播都低估了两个输入特征的重要性,因为梯度为 0。

图 5.20 展示了一个表现出输出信号饱和的简单网络的示意图
为了克服饱和问题,最近提出了两种基于梯度的方法,称为集成梯度 (arxiv.org/pdf/1703.01365.pdf) 和 SmoothGrad (arxiv.org/pdf/1706.03825.pdf)。集成梯度是由 Mukund Sundararajan 等人在 2017 年提出的。对于给定的输入图像,集成梯度将梯度积分,随着输入像素从起始值(例如,所有为零)缩放到实际值。SmoothGrad 也是由 Daniel Smilkov 等人在 2017 年提出的。SmoothGrad 在输入图像的副本上添加像素级的高斯噪声,然后通过传统的反向传播计算得到的梯度进行平均。这两种技术都需要在多个样本上积分/平均,类似于基于扰动的技术,从而增加了计算复杂度。因此得到的解释也不一定可靠,这就是为什么我们不明确地在本书中涵盖它们。我们将在 5.9 节中进一步讨论它们。对于感兴趣的人,您可以使用存储库中的 PyTorch 实现这些技术进行实验,存储库地址为 mng.bz/8l8B。
5.8 Grad-CAM 和 guided Grad-CAM
我们现在将关注基于激活的方法。Grad-CAM 是由 R. R. Selvaraju 等人在 2017 年提出的,是一种基于激活的归因方法,它利用了通过卷积层学习到的特征。Grad-CAM 查看 CNN 中最终卷积层学习的特征图,并通过计算输出相对于特征图中像素的梯度来获得该特征图的重要性。因为我们查看的是最终卷积层的特征图,所以 Grad-CAM 生成的激活图是粗略的。Grad-CAM 技术也在 mng.bz/8l8B 存储库中实现,并进行了以下调整,以便它可以应用于任何 CNN 架构。首先,我们将定义一个名为 CamExtractor 的类,以获取最终卷积层的输出或特征图以及分类器或全连接层的输出:
# Code below adapted from: http://mng.bz/8l8B
class CamExtractor():
"""
Extracts cam features from the model
"""
def __init__(self, model, features, fc, fc_layer, target_layer): ①
self.model = model ②
self.features = features ③
self.fc = fc ④
self.fc_layer = fc_layer ⑤
self.target_layer = target_layer ⑥
self.gradients = None ⑦
def save_gradient(self, grad): ⑧
self.gradients = grad ⑧
def forward_pass_on_convolutions(self, x): ⑨
"""
Does a forward pass on convolutions,
➥ hooks the function at given layer
"""
conv_output = None ⑩
for module_pos, module in self.features._modules.items(): ⑪
if module_pos == self.fc_layer: ⑫
break ⑫
x = module(x) ⑬
if module_pos == self.target_layer: ⑭
x.register_hook(self.save_gradient) ⑭
conv_output = x ⑭
return conv_output, x ⑮
def forward_pass(self, x): ⑯
"""
Does a full forward pass on the model
"""
# Forward pass on the convolutions
conv_output, x = self.forward_pass_on_convolutions(x) ⑰
x = x.view(x.size(0), -1) ⑱
# Forward pass on the classifier
x = self.fc(x) ⑲
return conv_output, x ⑳
① CamExtractor 构造函数,接受五个输入参数
② 第一个参数设置 CNN 模型对象。
③ 第二个参数设置 CNN 中特征层的起始位置。
④ 第三个参数设置 CNN 中全连接层的起始位置。
⑤ 第四个参数是全连接层的名称。
⑥ 第五个参数是目标层或最终卷积层的名称。
⑦ 初始化梯度对象为 None
⑧ 保存梯度的方法
⑨ 一种进行前向传播并获取最终卷积层的输出以及注册钩子函数以获取该层输出相对于该层的梯度的方法
⑩ 将最终卷积层的输出初始化为 None
⑪ 遍历 CNN 特征层的所有模块
⑫ 一旦模块名称与全连接层名称匹配,则中断
⑬ 使用来自前一层的输入获取模块的输出
⑭ 如果模块名称与最终卷积层名称匹配,则在反向传播期间注册钩子以获取相对于此层的输出梯度
⑮ 返回最终卷积层的特征图和全连接层的输入
⑯ 在模型上执行前向传递的方法
⑰ 获取最终卷积层的特征图和全连接层的输入
⑱ 将输入展平到全连接层
⑲ 通过全连接层传递以获取分类器输出
⑳ 返回最终卷积层的特征图和分类器的输出
代码片段中显示的CamExtractor类接受以下五个输入参数:
-
model—用于图像分类的 CNN 模型 -
features—在 CNN 中表示特征层开始的层 -
fc—在用于分类的 CNN 中表示全连接层开始的层 -
fc_layer—模型对象中全连接层的名称 -
target_layer—模型对象中最终卷积层的名称
正如我们在 vanilla 反向传播和引导反向传播中看到的,模型对象被命名为model,表示特征层开始的层是同一个对象。model对象中表示全连接层开始的层是model.fc。模型对象中全连接层的名称是fc,模型中最终卷积层的名称是layer4。我们现在定义GradCam类来生成类激活图,如下所示:
# Code below adapted from: http://mng.bz/8l8B
class GradCam():
"""
Produces class activation map
"""
def __init__(self, model, features, fc, fc_layer, target_layer): ①
self.model = model ②
self.features = features ②
self.fc = fc ②
self.fc_layer = fc_layer ②
self.model.eval() ③
self.extractor = CamExtractor(self.model, ④
self.features, ④
self.fc, ④
self.fc_layer, ④
target_layer) ④
def generate_cam(self, input_image, target_class=None): ⑤
conv_output, model_output = self.extractor.forward_pass(input_image) ⑥
if target_class is None: ⑦
target_class = np.argmax(model_output.data.numpy()) ⑦
one_hot_output = torch.FloatTensor(1, model_output.size()[-1]).zero_() ⑧
one_hot_output[0][target_class] = 1 ⑧
self.features.zero_grad() ⑨
self.fc.zero_grad() ⑨
model_output.backward(gradient=one_hot_output, retain_graph=True) ⑩
guided_gradients = self.extractor.gradients.data.numpy()[0] ⑪
target = conv_output.data.numpy()[0] ⑫
weights = np.mean(guided_gradients, axis=(1, 2)) ⑫
cam = np.ones(target.shape[1:], dtype=np.float32) ⑫
for i, w in enumerate(weights): ⑫
cam += w * target[i, :, :] ⑫
cam = np.maximum(cam, 0) ⑬
cam = (cam - np.min(cam)) / (np.max(cam) - np.min(cam)) ⑭
cam = np.uint8(cam * 255) ⑮
cam = np.uint8(Image.fromarray(cam).resize((input_image.shape[2],
input_image.shape[3]), Image.ANTIALIAS))/255 ⑯
return cam ⑰
① GradCam 的构造函数接受与 CamExtractor 相同的五个参数
② 设置适当的对象
③ 将模型设置为评估模式
④ 初始化 CamExtractor 对象
⑤ 给定输入图像和目标类别生成 CAM 的函数
⑥ 使用提取器从最终卷积层和分类器的输出中获取特征图
⑦ 如果未指定目标类别,则根据模型预测获取输出类别
⑧ 将目标类别转换为 one-hot 编码的张量
⑨ 在反向传播之前重置梯度
⑩ 执行反向传播
⑪ 获取输出类别相对于特征图的梯度
⑫ 通过对特征图加权梯度来获取 CAM
⑬ 剪裁 CAM 并移除负值
⑭ 在 0 和 1 之间归一化 CAM
⑮ 将 CAM 缩放到 0-255 以可视化为灰度图像
⑯ 放大 CAM 并插值到与输入图像相同的维度
⑰ 返回 CAM
您可以按照以下方式初始化 Grad-CAM 对象。作为一个练习,我鼓励您为之前使用的两个示例创建激活图。这个练习的解决方案可以在与本书相关的 GitHub 仓库中找到 (mng.bz/KBdZ):
grad_cam = GradCam(resnet18_model,
features=resnet18_model,
fc=resnet18_model.fc,
fc_layer='fc',
target_layer='layer4')
图 5.21 包含了生成的 Grad-CAM 激活图。从图中我们可以看到,激活图显示了最终卷积层特征图的重要性,并且相当粗糙。灰色或白色的区域显示了模型预测中高度重要的区域。

图 5.21 使用 Grad-CAM 的激活图
为了获得更精细的激活图,我们可以使用引导 Grad-CAM 技术。引导 Grad-CAM 技术是由与 Grad-CAM 同样的作者于 2017 年提出的,本质上结合了 Grad-CAM 和引导反向传播技术。引导 Grad-CAM 生成的最终激活图是 Grad-CAM 生成的激活图和引导反向传播生成的显著性图的逐元素点积。这在上面的函数中实现:
# Code below from: http://mng.bz/8l8B
def guided_grad_cam(grad_cam_mask, guided_backprop_mask):
"""
Guided grad cam is just pointwise multiplication of cam mask and
guided backprop mask
Args:
grad_cam_mask (np_arr): Class activation map mask
guided_backprop_mask (np_arr):Guided backprop mask
"""
cam_gb = np.multiply(grad_cam_mask, guided_backprop_mask)
return cam_gb
此函数接受从 Grad-CAM 和引导反向传播获得的灰度掩码,并返回它们的逐元素乘积。图 5.22 展示了引导 Grad-CAM 为两个感兴趣的示例生成的激活图。我们可以看到,可视化比引导反向传播更干净,并突出了与 IDC 负面和正面补丁一致的区域。

图 5.22 使用引导 Grad-CAM 的激活图
5.9 我应该使用哪种归因方法?
现在我们已经拥有了这些技术,我们应该应用哪些技术呢?换句话说,哪些技术能产生可靠的解释?通过使用几个示例对解释进行视觉检查,我们发现所有的显著性技术都为像素提供了一种重要性度量。通过视觉评估,我们发现这些重要性度量是合理的。然而,仅依靠视觉或定性评估可能会产生误导。
一篇由 Julius Adebayo 等人于 2018 年发表的论文,可在 mng.bz/Exjj 获取,对本章讨论的显著性方法进行了全面的定量评估。以下进行了两种广泛的测试:
-
模型参数随机化测试—检查通过随机化模型的权重是否会对显著性图产生影响,我们预期模型会做出随机或垃圾预测。如果显著性方法在训练模型和随机模型上的输出相同,那么我们可以说显著性图对模型参数不敏感。因此,显著性图对调试模型来说可能不可靠。
-
数据随机化测试—检查通过在训练数据中随机化标签是否会对显著性图产生影响。当我们在一个目标标签被随机化的训练数据集副本上训练相同的模型架构时,我们预期显著性方法的结果也会对其敏感。如果通过随机化标签,显著性图没有受到影响,那么该方法不依赖于原始训练集中存在的输入图像和标签。因此,显著性图对于理解输入输出关系不可靠。
论文提供了一些可以在实践中使用的合理性检查,以确定显著性方法输出的可靠性。合理性检查的结果总结在表 5.2 中。
表 5.2 对视觉归因方法进行的合理性检查结果
| 归因方法 | 模型参数随机化测试 | 数据随机化测试 |
|---|---|---|
| 纯量反向传播 | 通过 | 通过 |
| 指导反向传播 | 失败 | 失败 |
| 集成梯度 | 失败 | 失败 |
| SmoothGrad | 失败 | 通过 |
| Grad-CAM | 通过 | 通过 |
| 指导 Grad-CAM | 失败 | 失败 |
我们可以看到,通过两个测试的方法是纯量反向传播和 Grad-CAM。它们产生的显著性和激活图对模型和数据生成过程敏感。因此,它们可以用来可靠地调试模型,并理解输入图像和目标标签之间的关系。其他技术提供了令人信服的图像来解释模型预测,并且从定性评估来看似乎是可接受的。然而,它们对模型和标签随机化是不变的,因此不足以用于模型调试和理解输入输出关系。这些合理性检查的重要信息是要意识到确认偏差。仅仅从定性上使解释有意义是不够的;它还必须通过合理性检查,才能更好地理解模型和输入输出关系。论文中提出的两个测试可以应用于实践中其他可解释技术的检查。
在下一章中,我们将学习如何进一步剖析网络,并理解神经网络学习的高级概念。我们将不会关注像素级的重要性,而是学习那些提供概念级重要性的技术。这些技术已被证明对模型和数据生成过程敏感,因此通过本节讨论的合理性检查。
摘要
-
卷积神经网络(CNN)是一种常用于视觉任务(如图像分类、目标检测和图像分割)的神经网络架构。
-
完全连接的深度神经网络(DNN)在图像中捕捉像素依赖性方面并不很好,因此不能训练以理解图像中的边缘、颜色和梯度等特征。另一方面,CNN 在图像中很好地捕捉像素依赖性或空间依赖性。我们还可以更有效地训练 CNN 架构以适应输入数据集,因为我们可以在网络中重用权重。
-
CNN 架构通常由一系列卷积和池化层组成,称为特征学习层。这些层的目的是从输入图像中提取层次特征。在特征学习卷积层之后是神经元或单元层,它们是完全连接的,这些完全连接层的目的是执行分类。完全连接层的输入是由卷积和池化层学习的高级特征,输出是对分类任务的概率度量。
-
各种最先进的卷积神经网络(CNN)架构,如 AlexNet、VGG、ResNet、Inception 和 ResNeXT,已在流行的深度学习库中实现,例如 PyTorch 和 Keras。在 PyTorch 中,你可以使用 torchvision 包来初始化这些架构。
-
在 CNN 中,随着图像经过数百万个复杂的操作,理解模型如何到达最终预测变得极其困难。这就是 CNN 成为黑盒的原因。
-
我们可以使用视觉归因方法来解释 CNN。这些方法用于将重要性归因于影响 CNN 预测的图像部分。
-
可用的视觉归因方法分为三大类:扰动、梯度和激活。
-
基于扰动的方法的理念是扰动输入并探测其对 CNN 预测的影响。LIME 和 SHAP 等技术是扰动方法。然而,这些技术计算效率低下,因为每次扰动都需要我们对复杂的 CNN 模型执行前向传递。这些技术还可能低估特征的重要性。
-
我们可以使用基于梯度的方法来可视化输入图像相对于目标类的梯度。具有大梯度度量的像素被认为对模型来说是最重要的,或称为显著的。基于梯度的方法有时也称为反向传播方法——反向传播算法用于确定特征重要性和显著性图,因为可以获得显著或重要特征的映射。流行的基于梯度的方法包括标准反向传播、引导反向传播、集成梯度和 SmoothGrad。
-
基于激活的方法查看最终卷积层中的特征图或激活,并根据目标类别对这些特征图的梯度进行加权。特征图的权重作为输入特征重要性的代理。这种技术被称为梯度加权类激活映射(Grad-CAM)。
-
Grad-CAM 提供了一个粗粒度的激活图。为了获得更细粒度的激活图,我们可以结合 Grad-CAM 和 guided backpropagation——这种技术被称为 guided Grad-CAM。
-
通过模型参数随机化和数据随机化测试的视觉归因方法包括 vanilla backpropagation 和 Grad-CAM。因此,它们产生的显著性和激活图在调试模型和更好地理解输入输出关系方面更加可靠。
第三部分. 解释模型表示
本书本部分继续关注黑盒模型,但特别关注理解它们学习到的哪些特征或表示。
在第六章和第七章中,你将学习关于卷积神经网络和用于语言理解的神经网络。你将学习如何剖析神经网络,并理解神经网络中中间层或隐藏层学习到的数据表示。你还将学习如何使用主成分分析(PCA)和 t 分布随机邻域嵌入(t-SNE)等技术来可视化模型学习到的高维表示。
6 理解层和单元
本章涵盖
-
剖析黑盒卷积神经网络以理解层和单元学习到的特征或概念
-
运行网络剖析框架
-
量化卷积神经网络中层和单元的可解释性以及如何可视化它们
-
网络剖析框架的优势和劣势
在第 3、4 和 5 章中,我们关注了黑盒模型以及如何使用各种技术如部分依赖图(PDPs)、LIME、SHAP、锚点和显著性图来解释它们。在第五章中,我们特别关注了卷积神经网络(CNNs)和视觉归因方法,如梯度图和激活图,这些方法突出了模型关注的显著特征。所有这些技术都集中在通过降低其复杂性来解释黑盒模型内部发生的复杂处理和操作。例如,PDPs 是模型无关的,显示了特征值对模型预测的边际或平均全局影响。LIME、SHAP 和锚点等技术的模型无关性也体现在它们创建了一个与原始黑盒模型行为相似但更简单、更容易解释的代理模型。视觉归因方法和显著性图对模型有轻微的依赖性,有助于突出对模型来说显著或重要的输入部分。
在本章和下一章中,我们关注解释深度神经网络学习到的表示或特征。本章特别关注用于视觉任务(如图像分类、目标检测和图像分割)的 CNNs。CNN 内部发生的操作数量被组织成层和单元。通过解释模型表示,我们旨在理解通过这些层和单元流动的数据的角色和结构。你将在本章中具体了解网络剖析框架。这个框架将使我们对 CNN 学习到的特征和高级概念有更深入的了解。它还将帮助我们从通常定性评估的显著性图等可视化过渡到更定量的解释。
我们首先将介绍 ImageNet 和 Places 数据集以及相关的图像分类任务。然后,我们将快速回顾卷积神经网络(CNNs)和视觉归因方法,重点关注这些方法的局限性。这是为了展示网络剖析框架的优势。本章的剩余部分将专注于这个框架以及我们如何利用它来理解 CNNs 学习到的表示。
6.1 视觉理解
在本章中,我们将专注于训练一个智能体或智能系统来识别现实世界中的物体、地点和场景的任务。该系统的任务是执行多类分类。为了训练这样的智能体,我们需要访问大量标记数据。ImageNet 数据集(www.image-net.org/)是为了识别物体而创建的。它是在 WordNet 的基础上构建的一个大规模图像本体。WordNet 是一个英语名词、动词、形容词和副词的词汇数据库,这些词汇被组织成一组同义词,也称为 synsets。ImageNet 也遵循类似的结构,其中图像根据层次同义词或类别分组。图 6.1 展示了这种结构的示例。在这个例子中,动物图像被组织成三个类别。最高级别的类别包括哺乳动物的图像。下一级包括食肉动物的图像,然后是最终级别,包括狗的图像。完整的 ImageNet 数据库包含超过 1400 万张图像,分为 27 个高级类别。同义词或子类别的数量从 51 到 3822 不等。当涉及到构建图像分类器时,ImageNet 是最常用的数据集之一。

图 6.1 ImageNet 数据集中同义词或类别的示意图
对于识别地点和场景的任务,我们将使用 Places 数据集(places2.csail.mit.edu/)。了解物体在现实世界中出现的地点、场景或上下文是构建像自动驾驶汽车这样的智能系统的重要方面,该系统试图在城市中导航。Places 数据集将图像组织成不同级别的场景类别。图 6.2 展示了说明数据集语义结构的示例。该示例显示了一个高级场景类别,称为户外。在这个类别下有三个子类别,分别是大教堂、建筑和体育场。总共有超过 1000 万张图像被组织成 400 个独特的场景类别。使用这个数据集,我们可以训练一个模型来学习各种地点和场景识别任务的特征。

图 6.2 Places 数据集中类别的示意图
随着 ImageNet 和 Places 数据集的加入,我们现在已经准备好训练智能系统。幸运的是,我们可以使用在 ImageNet 和 Places 数据集上预训练的各种最先进的 CNN 架构模型。这将节省我们从头开始训练模型的努力、时间和金钱。在下一节中,我们将看到如何利用这些预训练模型。我们还将回顾 CNN 以及我们迄今为止学到的技术,以解释这些模型的输出。
6.2 卷积神经网络:回顾
在本节中,我们快速回顾了我们在第五章中学到的 CNN。图 6.3 展示了一个 CNN 架构,可以用于将 ImageNet 数据集中的图像分类为狗或不是狗。

图 6.3 卷积神经网络 (CNN) 的示意图
该架构由一系列称为卷积和池化层的层组成,随后是一组全连接层。卷积和池化层合称为 特征学习层。这些层从输入图像中提取层次特征。前几层提取低级特征,如边缘、颜色和梯度。后续层学习高级特征。全连接层用于分类。特征学习层中学习的特征被作为输入馈送到全连接层。最终输出是输入图像是狗的概率度量。
在上一章中,我们也学习了如何使用 PyTorch 中的 torchvision 包初始化最先进的 CNN 架构。我们特别关注了深度为 18 层的 ResNet 架构,称为 ResNet-18。我们将在本章中也继续使用这个架构。在 PyTorch 中可以按照以下方式初始化 ResNet-18 模型:
import torchvision ①
model = torchvision.models.resnet18(pretrained=False) ②
① 导入 torchvision 包
② 使用随机权重初始化 ResNet 模型
通过将 pretrained 参数设置为 False,我们使用随机权重初始化 ResNet 模型。要初始化在 ImageNet 数据集上预训练的模型,我们必须将 pretrained 参数设置为 True。这将在下面展示:
imagenet_model = torchvision.models.resnet18(pretrained=True) ①
① 初始化在 ImageNet 数据集上预训练的 ResNet 模型
其他 CNN 架构,如 AlexNet、VGG、Inception 和 ResNeXT,也可以使用 torchvision 初始化。所有支持的架构都可以在 pytorch.org/vision/stable/models.html 找到。
对于 Places 数据集,你可以在 github.com/CSAILVision/places365 找到各种架构的预训练 PyTorch 模型。你可以从 mng.bz/GGmA 下载用于 ResNet-18 架构的预训练 PyTorch 模型。由于文件大小超过 40 MB,我鼓励你本地下载。一旦下载完成,你可以按照以下方式加载在 Places 数据集上预训练的模型:
import torch ①
places_model_file = “resnet18_places365.pth.tar” ②
if torch.cuda.is_available(): ③
places_model = torch.load(places_model_file) ③
else: ③
places_model = torch.load(places_model_file, map_location=torch.device(‘cpu’)) ③
① 导入 PyTorch 库
② 将此变量设置为预训练 ResNet 模型下载的完整路径
③ 加载在 Places 数据集上预训练的 ResNet 模型
我们还学习了各种可以用来解释 CNN 的视觉归因方法,如图 6.4 所示。存在三种视觉归因方法的广泛类别:扰动、梯度和激活。像 LIME 和 SHAP 这样的技术是基于扰动的。这些模型无关、事后和局部可解释的技术使用与复杂 CNN 行为相似但更容易解释的代理模型。这些技术突出了图像中对模型预测重要的部分或超像素。这些技术非常好,可以应用于任何复杂模型。基于梯度和基于激活的方法是事后和局部可解释的技术。然而,它们对模型有轻微的依赖性,并且只突出了输入图像中显著或对模型重要的很小一部分。对于像 vanilla backpropagation、guided backpropagation、integrated gradients 和 SmoothGrad 这样的基于梯度的方法,我们通过计算目标类别相对于输入图像的梯度来获得图像中的显著像素。对于像 Grad-CAM 和 guided Grad-CAM 这样的基于激活的方法,最终卷积层的激活是基于目标类别相对于激活或特征图的梯度进行加权的。

图 6.4 视觉归因方法回顾
图 6.4 中展示的所有视觉归因方法都突出了最终模型预测中重要的像素或超像素。我们通常对这些方法生成的可视化进行定性评估,因此解释是主观的。此外,这些技术并没有给我们提供关于 CNN 中特征学习层和单元所学习的低级和高级概念或特征的信息。在下一节中,我们将了解网络剖析框架。这个框架将帮助我们剖析 CNN,并提出更多定量解释。我们还将能够理解 CNN 中的特征学习层学习到哪些人类可理解的概念。
6.3 网络剖析框架
网络剖析框架由麻省理工学院的周博磊等研究人员在 2018 年提出(参见arxiv.org/pdf/1711.05611.pdf)。该框架旨在回答的基本问题如下:
-
CNN 是如何分解理解图像的任务的?
-
CNN 是否识别出任何人类可理解的特征或概念?
该框架通过在 CNN 的卷积层中找到与有意义的、预定义的语义概念匹配的单元来回答这些问题。这些单元的可解释性通过测量单元响应与预定义概念的对齐程度来量化。以这种方式剖析网络是有趣的,因为它使得深度神经网络不那么神秘。网络剖析框架包括以下三个关键步骤,如图 6.5 所示:
-
首先,定义一组广泛的有意义的概念,这些概念可以用来剖析网络。
-
然后,通过寻找对那些预定义概念做出响应的单元来探测网络。
-
最后,测量这些单元对这些概念的质量或可解释性。

图 6.5 网络剖析框架
我们将在接下来的小节中更详细地分解这些步骤。
6.3.1 概念定义
网络剖析框架的第一步也是最重要的一步是数据收集。数据必须由用不同抽象级别的概念逐像素标记的图像组成。在第 6.1 节中介绍的 ImageNet 和 Places 数据集可以用来训练模型以检测现实世界中的物体和场景。对于网络剖析的目的,我们需要另一个独立的、包含标记概念的独立数据集。我们不会使用这个数据集进行模型训练,而是用它来探测网络,以了解特征学习层学到了哪些高级概念。
为了分解使用 ImageNet 和 Places 等数据集训练以检测现实世界对象和场景的模型,周、博雷等结合了五个不同的数据集,创建了一个独立的、称为 Broden 的高层次概念标注数据集。Broden 代表 broadly 和 densely 标注数据集。Broden 统一了以下五个数据集:ADE (mng.bz/zQD6/)、Open-Surfaces (mng.bz/0wJE)、PASCAL-Context (mng.bz/9KEq)、PASCAL-Part (mng.bz/jyD8) 和可描述的纹理数据集 (mng.bz/W7Xl)。这些数据集包含广泛概念类别的标注图像,从低级概念类别如颜色、纹理和材料到更高级的概念类别如部分、物体和场景。图 6.6 提供了一个带有各种概念的图像的插图。在 Broden 数据集中,为图像中的每个概念创建了一个分割图像。如果我们以图 6.6 中的树对象为例,包含树的边界框内的像素有一个标签 1,而边界框外、不包含树的像素有一个标签 0。概念需要在像素级别进行标注。所有五个数据集的标签在 Broden 数据集中统一。具有相似同义词的概念也被合并。Broden 包含超过 1,000 个视觉概念。

图 6.6 一个带有标注概念的图像的插图
由于创建带有标注概念的数据集是网络分解框架中的关键步骤,让我们退一步看看如何创建一个新的数据集。我们将特别关注我们可以用于此目的的工具和遵循的方法,以获得一致、高质量标注的概念。
我们可以使用各种工具来标注图像。LabelMe (mng.bz/8lE5) 和 Make Sense (www.makesense.ai/) 是免费的基于网络的图像标注工具。在 LabelMe 中,我们可以轻松创建账户,上传图像,并对它们进行标注。通过共享功能,我们还可以协作创建标注。然而,在 LabelMe 上上传的图像被认为是公开的。Make Sense 是一个非常类似的工具,但它不允许您与他人协作和共享标注。该工具也不保存标注项目的状态。因此,如果您在 Make Sense 中开始一个项目,该项目中图像的标注必须一次性完成。该工具不允许您保存状态并从上次离开的地方继续标注。LabelMe 和 Make Sense 都支持多种标签类型,如矩形、线条、点和多边形。这两个工具主要被使用数据集旨在公开的研究人员使用。
对于企业、商业或更私人的需求,您可以托管自己的标注服务。计算机视觉标注工具(CVAT;github.com/openvinotoolkit/cvat)和视觉对象标记工具(VoTT;github.com/microsoft/VoTT)是免费的、开源的 Web 服务,您可以将它们部署在自己的 Web 服务器上。如果您不想处理托管自己的标注服务的麻烦,您也可以使用如 LabelBox (labelbox.com/))、Amazon SageMaker Ground Truth (aws.amazon.com/sagemaker/groundtruth/)或 Azure Machine Learning 提供的标注服务(mng.bz/ExgX)或 Google Cloud(mng.bz/Nx9v))等托管服务。如果您没有一组可以为您标注图像的标注者团队,您也可以通过 Amazon Mechanical Turk (www.mturk.com/)进行众包标注并获取标签。
重要的是要有良好的标注方法,以确保高质量的、一致的标签。标注任务的协议必须明确指定,以便标注者知道所有概念的完整列表,并对它们有明确的定义。然而,通过这个过程获得的标签可能会相当嘈杂,尤其是如果它们是众包的。为了确保标签的一致性,随机抽取图像的一个子集,并让同一组标注者进行标注。通过这样做,您现在可以通过查看以下三种类型的错误来量化标签的一致性,这些错误在mng.bz/DxaA中详细说明,该链接介绍了 ADE20K 数据集:
-
分割质量—这种错误量化了概念分割的精确度。同一个概念可能被不同的标注者或同一个标注者以不同的方式分割。
-
概念命名—概念命名的差异可能出现在同一个标注者或不同的标注者给同一个像素分配不同的概念名称的情况下。
-
分割数量—某些图像可能包含比其他图像更多的标注概念。您可以通过查看多个标注者对某个图像中概念数量的方差来量化这种错误。
我们可以通过增加标注者的数量来规避分割质量和数量的错误,以便我们可以达成共识,或者通过让更有经验的标注者对图像进行标注。我们可以通过有一个明确定义的标注协议和精确的术语来避免概念命名的错误。如前所述,创建带有标注概念的数据库是网络剖析框架中最重要的一步。这也是最耗时和成本最高的步骤。我们将在以下章节中通过可解释性的视角来了解这个数据库的价值。
6.3.2 网络探测
一旦你有了视觉概念的标记数据集,下一步就是探测预训练神经网络,以了解网络对这些概念的反应。让我们首先看看这在一个简单的深度神经网络中是如何工作的。图 6.7 展示了深度神经网络的简化表示,其中单元的数量从输入层到输出层逐渐减少。网络中间层学习到输入数据的表示,这被表示为R。为了更好地理解网络,我们希望通过量化表示R如何映射到我们关心的给定查询概念Q来探测网络。从表示R到查询概念Q的映射称为计算模型,在图中表示为f。现在,让我们在 CNN 的背景下定义R、Q和f。

图 6.7 探测深度神经网络中的概念
图 6.8 展示了探测网络第 4 层的 CNN。在图中,我们使用狗的图像探测网络,确定预训练 CNN 中第 4 层的单元学习到了哪些概念(如颜色和物体)。因此,第一步是将狗的图像通过 CNN 进行前向传播。CNN 的权重被冻结,不需要训练或反向传播。接下来,我们选择一个卷积层进行探测(在这种情况下,第 4 层)。然后,我们从该层获得前向传播后的输出特征图或激活图。一般来说,随着你深入 CNN,激活图的大小会减小。因此,为了将激活图与输入图像中的标记概念进行比较,我们必须将低分辨率激活图上采样或缩放,使其与输入图像具有相同的分辨率。这形成了 CNN 中第 4 层卷积层的表示R。对标记概念数据集中的所有图像重复此过程,并存储所有图像的激活图。我们也可以对 CNN 中的其他层重复此过程。

图 6.8 探测卷积神经网络中的第 4 层概念
现在,我们如何解释这些表示R中包含的高级概念?换句话说,我们如何将这些表示R与查询概念Q进行映射?这需要我们确定一个计算模型f,将R映射到Q。此外,你如何上采样或缩放低分辨率激活图,使其与输入图像具有相同的分辨率?这已在图 6.9 中分解。

图 6.9 上采样和如何将表示 R 映射到查询概念 Q 的示意图
在图 6.9 中,我们可以看到输入图像 i 通过 CNN 的前向传播。为了说明目的,假设我们特别关注卷积层 l 中的单元 k。这个卷积层的输出表示为低分辨率激活图 A[i]。网络剖析框架随后对激活图进行上采样或调整大小,使其与输入图像 i 的分辨率相同。这如图 6.9 中的输入图像分辨率激活图 S[i] 所示。框架中使用的是双线性插值算法。双线性插值将线性插值扩展到二维平面。它根据周围像素中的已知值估计调整大小后的图像中新未知像素的值。估计或插值值位于原始激活图中每个单元响应的中心。
一旦你有了图像分辨率激活图 S[i],框架随后通过执行简单的阈值化将此表示映射到给定的查询概念 Q[c]。阈值化是在单元级别进行的,以便可以将卷积层中每个单元的响应与查询概念进行比较。在图 6.9 中,查询概念 Q[c] 是原始标记概念数据集中棕色颜色的分割图像。阈值化后的单元 k 的二进制单元分割图显示为 M[k]。计算模型 f 使用的阈值是 T[k],其中
M[k] = S[i] ≥ T[k]
二进制单元分割图 M[k] 强调了所有激活值超过阈值 T[k] 的区域。阈值 T[k] 取决于我们在 CNN 中探测的单元。我们如何计算这个阈值?框架会查看单元激活在标记概念数据集所有图像中的分布。设 a[k] 为给定输入图像 i 中低分辨率激活图 A[i] 中一个单元的激活值。一旦你得到了所有图像中 a[k] 的分布,阈值 T[k] 就被计算为最高分位数水平,使得
ℙ(a[k] > T[k]) = 0.005
T[k] 衡量的是 0.005 分位数水平。换句话说,标记概念数据集中所有图像中所有单元激活 (a[k]) 的 0.5% 大于 T[k]。一旦我们将 CNN 学到的表示映射到二进制单元分割图,下一步就是量化该分割图与所有查询概念 Q[c] 的对齐程度。这将在以下小节中详细说明。
6.3.3 量化对齐
在你已经探测了网络并获得了表示层中所有单元的二值单元分割图之后,框架中的最后一步是对分割图与数据集中所有查询概念的对齐进行量化。图 6.10 展示了如何量化给定二值单元分割图 M[k] 和查询概念 Q[c] 的对齐。对齐是通过交并比(IoU)分数来衡量的。IoU 是衡量一个单元检测给定概念准确性的有用指标。它衡量二值单元分割图与查询概念的像素级分割图像的重叠程度。IoU 分数越高,准确性越好。如果二值分割图与概念完美重叠,我们获得完美的 IoU 分数为 1。

图 6.10 用概念量化对齐
给定二值分割图 M[k] 和查询概念 Q[c] 的 IoU 值是单元 k 检测概念 c 的准确性。它通过衡量单元 k 检测概念 c 的能力来量化单元 k 的可解释性。在网络分解框架中,使用 0.04 的 IoU 阈值,如果一个单元 k 的 IoU 分数大于 0.04,则认为该单元 k 是概念 c 的检测器。0.04 这个值是框架的作者任意选择的,在他们提供的论文中,arxiv.org/pdf/1711.05611.pdf,作者通过人工评估表明,解释的质量对 IoU 阈值不敏感。为了量化卷积层的可解释性,框架计算与单元对齐的独特概念的数目,即独特概念检测器的数量。有了对网络分解框架如何工作的理解,让我们在下一节中看看它是如何实际应用的。
6.4 解释层和单元
在本节中,我们通过解释在 ImageNet 和 Places 数据集上预训练的 CNN 模型中的层和单元来测试网络剖析框架。如 6.2 节所述,我们将重点关注 ResNet-18 架构,但网络剖析框架可以应用于任何 CNN 模型。我们在 6.2 节中看到了如何加载在 ImageNet 和 Places 数据集上预训练的 ResNet-18 模型。论文的作者创建了一个名为 NetDissect 的库(github.com/CSAILVision/NetDissect),该库实现了这个框架。这个库支持 PyTorch 和 Caffe 深度学习框架。然而,我们将使用一个改进的实现,称为 NetDissect-Lite(github.com/CSAILVision/NetDissect-Lite),它比原始实现更轻更快。这个库是用 PyTorch 和 Python 3.6 编写的。我们需要对库进行一些小的修改以支持 Python 的后续版本(3.7 及以上),我们将在下一小节中讨论这个问题。
我们可以使用以下命令从 GitHub 将 NetDissect-Lite 库克隆到我们的本地存储库中:
git clone https://github.com/CSAILVision/NetDissect-Lite
这个库也被添加到与本书相关的存储库中,作为一个 Git 子模块。如果你已经从 GitHub 克隆了本书的存储库,那么你可以通过在本地克隆存储库的目录中运行以下命令来拉取子模块:
git submodule update --init –recursive
一旦你克隆了 NetDissect-Lite 存储库,请本地切换到该目录。然后,运行以下命令下载 Broden 数据集。Broden 数据集需要超过 1GB 的存储空间。请注意数据集下载的路径,因为我们稍后会用到它:
$>./script/dlbroden.sh
你也可以通过在 NetDissect-Lite 目录中运行以下命令来下载在 Places 数据集上预训练的 ResNet-18 模型。同样,请注意模型下载的路径,因为我们稍后会用到它:
$>./script/dlzoo_example.sh
6.4.1 运行网络剖析
在本节中,我们将学习如何使用 NetDissect-Lite 库,通过使用 Broden 数据集中的标记概念来探测在 ImageNet 和 Places 数据集上预训练的 ResNet-18 模型。我们可以通过 NetDissect-Lite 库根目录下的 settings.py 文件来配置库。我们不会涵盖所有设置,因为对于大多数设置,我们将使用库提供的默认值。因此,我们将重点关注关键设置,这些设置总结在表 6.1 中。
表 6.1 NetDissect-Lite 库的设置
| 设置 | 描述 | 可能的值 |
|---|---|---|
GPU |
这是一个布尔设置,可以用来在 GPU 上加载模型并运行网络剖析。 | 可能的值是 True 和 False。如果设置为 True,则使用 GPU。 |
MODEL |
这是一个字符串设置,用于设置预训练模型的模型架构。 | 可能的值有resnetlB、alexnet、resnetS0、densenet161等。在本节中,我们将设置此值为resnetlB。 |
DATASET |
这是一个字符串设置,让库知道使用了哪个数据集来训练 CNN 模型。 | 可能的值有imagenet和places 365。在本节中,我们将使用这两个值来比较层和单元的可解释性。 |
CATEGORIES |
这是一个字符串列表设置,定义了标记概念数据集中的高级类别。 | 对于 Broden 数据集,列表可以包含以下值:object、part、scene、material、texture和color。在本节中,我们将删除material概念,并查看其他五个类别。 |
OUTPUT_FOLDER |
这是一个字符串设置,为库提供标记概念数据集的路径。 | 此设置的默认值是./script/dlbroden.sh脚本下载 Broden 数据集的路径。 |
FEATURE_NAMES |
这是一个字符串列表设置,让库知道在 CNN 中要探测哪些特征学习层。 | 对于 Resnet18 模型,列表可以包含以下值:layer1、layer2、layer3和/或layer4。在本章中,我们将使用所有四个值来比较所有四个特征学习层中单元的可解释性。 |
MODEL_FILE |
这是一个字符串设置,用于向库提供预训练模型的路径。 | 对于在 Places 数据集上预训练的 Resnet18 模型,将此设置的值设置为script/dlzoo_example.sh脚本下载模型的路径。对于在 ImageNet 数据集上预训练的模型,将值设置为None。这将让库知道从torchvision包中加载模型。 |
MODEL_PARALLEL |
这是一个布尔设置,用于让库知道模型是否在多 GPU 上训练。 | 可能的值是True和False。 |
在运行网络剖析框架之前,请确保 settings.py 文件已更新为正确的设置。为了探测在 ImageNet 数据集上预训练的 ResNet-18 模型中的所有特征学习层,我们在 settings.py 文件中将关键设置设置为以下值:
GPU = False
MODEL = 'resnet18'
DATASET = 'imagenet'
QUANTILE = 0.005
SCORE_THRESHOLD = 0.04
TOPN = 10
CATAGORIES = ["object", "part","scene","texture","color"]
OUTPUT_FOLDER = "result/pytorch_" + MODEL + "_" + DATASET
DATA_DIRECTORY = '/data/dataset/broden1_227'
IMG_SIZE = 227
NUM_CLASSES = 1000
FEATURE_NAMES = ['layer2', 'layer3', 'layer4']
MODEL_FILE = None
MODEL_PARALLEL = False
确保将DATA_DIRECTORY设置设置为 Broden 数据集下载的路径。此外,如果您想使用 GPU 进行更快的处理,请将GPU设置设置为True。如前所述,该库提供了一些子设置。这些设置在之前的代码中并未明确设置,您可以使用它们的默认值。
要探测在 Places 数据集上预训练的 ResNet-18 模型中的所有特征学习层,我们只需更新以下设置。其余的设置与 ImageNet 数据集的设置相同。确保 MODEL_FILE 设置设置为下载预训练的 ResNet-18 模型的路径:
DATASET = 'places365'
NUM_CLASSES = 365
MODEL_FILE = '/models/zoo/resnet18_places365.pth.tar'
MODEL_PARALLEL = True
一旦我们设置了设置值,我们现在就可以初始化并运行框架了。运行以下代码行以探测网络并从特征学习层提取激活图:
import settings ①
from loader.model_loader import loadmodel ②
from feature_operation import hook_feature, FeatureOperator ③
fo = FeatureOperator() ④
model = loadmodel(hook_feature) ⑤
features, maxfeatures = fo.feature_extraction(model=model) ⑥
① 从 settings.py 文件导入所有设置
② 从 loader/model_loader 模块导入 loadmodel f 函数
③ 从特征操作模块导入 hook_feature 函数和 FeatureOperator 类
④ 初始化 FeatureOperator 对象
⑤ 加载模型并将 hook_feature 函数传递给添加特征学习层的钩子
⑥ 在模型上运行特征提取以从特征学习层获取激活图
loadmodel 函数根据 MODEL 设置加载模型。模型的加载方式与我们看到的第 6.2 节中的方式相同。该函数还根据 FEATURE_NAMES 设置为每个特征学习层添加钩子。这些钩子由 FeatureOperator 对象用于从这些层中提取激活图。FeatureOperator 类是实现网络剖析框架中的步骤 2 和 3 的主要类。在前面的代码片段中,我们正在运行步骤 2 的一部分,使用 feature_extraction 函数从特征学习层提取低分辨率激活图。此函数从 Broden 数据集中加载图像,通过模型前向传播它们,使用钩子提取激活图,然后将其保存到名为 feature_size.npy 的文件中。该文件保存在 settings.py 中设置的 OUTPUT_FOLDER 路径。feature_extraction 函数还返回两个变量:features 和 maxfeatures。features 变量包含所有特征学习层和输入图像的激活图。maxfeatures 变量存储每个图像的最大值激活,我们将在生成摘要结果时使用它。
一旦我们提取了低分辨率激活图,我们可以运行以下代码行来计算特征学习层中所有单元的阈值 T[k](0.005 分位数水平),上采样低分辨率激活图并生成二值单元分割图,计算 IoU 分数,并最终生成结果摘要:
from visualize.report import generate_html_summary ①
for layer_id, layer in enumerate(settings.FEATURE_NAMES): ②
# Calculate the thresholds T_k
thresholds = fo.quantile_threshold(features[layer_id],
➥ savepath=f"quantile_{layer}.npy") ③
# Up-sample and calculate the IoU scores
tally_result = fo.tally(features[layer_id],thresholds,
➥ savepath=f"tally_{layer}.csv") ④
# Generate a summary of the results
generate_html_summary(fo.data, layer, ⑤
tally_result=tally_result, ⑤
maxfeature=maxfeatures[layer_id], ⑤
features=features[layer_id], ⑤
thresholds=thresholds) ⑤
① 从可视化/报告模块导入 generate_html_summary 函数
② 遍历每个特征学习层
③ 计算特征学习层中所有单元的 0.005 分位数水平
④ 在上采样并生成二值单元分割图后计算 IoU 分数
⑤ 以 HTML 格式生成结果摘要
在此代码片段中,我们正在遍历 FEATURE_NAMES 中的每个特征学习层,并执行以下操作:
-
使用
FeatureOperator类中的quantile_threshold函数,计算每个特征学习层中所有单元的 0.005 分位数水平(T[k])。这些分位数水平或阈值保存在OUTPUT_FOLDER路径下的每个层的文件中(称为 quantile_{layer}.csv)。该函数还返回阈值作为 NumPy 数组。 -
使用
FeatureOperator类中的tally函数,将每个特征学习层的低分辨率激活图上采样到与输入图像相同的分辨率。tally函数还会根据每个单元的上采样激活图和计算出的阈值生成基于二进制单元分割图。该函数最终计算 IoU 分数并测量二进制单元分割图与 Broden 数据集中分割概念的匹配度。每个高级概念的聚合 IoU 分数都保存在 OUTPUT_FOLDER 路径下的一个文件中(称为 tally_{layer}.csv),每个层一个文件。这些结果也作为字典对象返回。 -
最后,使用
generate_html_summary函数创建结果的 HTML 形式摘要。
在下一节中,我们将探索库生成的结果摘要,并可视化特征学习层中单元学习到的概念。
在自定义数据集上运行网络分解
理解 Broden 数据集文件夹的结构非常重要,这样我们才能为我们的自定义数据集和概念模仿它。在高级别上,该文件夹包含以下文件和文件夹:
-
images(文件夹)—包含所有 JPEG 或 PNG 格式的图像。该文件夹应包含以{filename}.jpg 格式存储的原始图像以及每个概念的分割图像,格式为{filename}_{concept}.jpg。
-
index.csv—包含数据集中所有图像的列表,以及有关标记概念的详细信息。第一列是图像文件名,包含图像的相对路径。然后是包含图像高度和宽度以及分割高度和宽度维度的列。然后是每个概念的列,包含该概念的分割图像的相对路径。
-
category.csv—列出所有概念类别,随后是一些关于概念的摘要统计信息。第一列是概念名称,然后是属于该概念类别的标签数量以及具有该标记概念的图像频率。
-
label.csv—列出所有标签及其对应的每个标签所属的概念,随后是一些关于标签的摘要统计。第一列是标签编号(或标识符),然后是标签名称和它所属的类别。摘要统计包括具有该标签的图像数量频率、像素部分或图像的覆盖范围,以及具有该标签的图像总数。
-
c_{concept}.csv—每个概念类别一个文件,包含所有标签、图像频率和覆盖细节。
你使用自己的标记概念创建的新数据集应遵循 Broden 数据集相同的结构,以确保与网络剖析框架兼容。一旦你将数据集结构化如前所述,你就可以在 settings.py 中更新以下设置:
-
DATA_DIRECTORY—指向存储你的自定义数据集的目录。 -
CATEGORIES—列出你自定义数据集中的所有概念类别,即在 category.csv 文件中。 -
IMG_SIZE—图像文件夹中图像的维度。维度应与 index.csv 文件中的维度匹配。
这些设置将确保库加载新的自定义概念数据集。如果你在不同于 ImageNet 或 Places 的数据集上有一个自己的预训练模型,你还需要更新以下设置:
-
DATASET—设置为模型已训练的数据集名称。 -
NUM_CLASSES—设置为模型可能输出的类别或标签数量。 -
FEATURE_NAMES—列出你自定义预训练模型中的特征层名称。 -
MODEL_FILE—包含你的预训练模型在 PyTorch 中的完整路径。 -
MODEL_PARALLEL—如果你的自定义模型是在多 GPU 上训练的,此设置必须为True。
6.4.2 概念检测器
我们现在将分析运行网络剖析框架后的结果。我们首先将关注 ResNet-18 模型中的最终卷积层(即第 4 层),并查看该层中独特概念检测器的数量。独特检测器的数量是网络可解释性的度量,衡量该特征学习层中单元学习的独特概念数量。独特检测器的数量越多,训练的网络在检测人类可理解的概念方面就越多样化。
让我们先看看网络剖析框架结果文件夹的输出结构。OUTPUT_FOLDER 设置提供了结果文件夹的路径。我们在上一节中看到了保存在该文件夹中的相关文件。现在,让我们处理 tally_layer4.csv 来计算 ResNet-18 模型第 4 层的独特检测器的数量以及这些独特检测器覆盖的单元比例。以下函数可以用来计算相关统计。该函数接受以下关键字参数:
-
network_names—需要计算唯一检测器数量的模型列表。在本章中,我们只关注 ResNet-18 模型,因此这个关键字参数是一个只包含一个元素的列表—resnet18。 -
datasets—这个参数是一个列表,包含了模型预训练的数据集。在本章中,我们关注的是imagenet和places365。 -
results_dir—存储每个预训练模型结果的父目录。 -
categories—需要计算唯一检测器数量的所有概念类别列表。 -
iou_thres—这是 IoU 得分的阈值,我们根据这个阈值将一个单元视为一个概念检测器。正如我们在 6.3.3 节中看到的,这个阈值的默认值是 0.04。 -
layer—我们感兴趣的特性学习层。在这种情况下,我们关注的是最终层,即第 4 层。
import os ①
import pandas as pd ①
from collections import OrderedDict ①
def compute_unique_detectors(**kwargs): ②
network_names = kwargs.get("network_names",
["resnet18"]) ③
datasets = kwargs.get("datasets",
["imagenet", "places365"]) ④
results_dir = kwargs.get("results_dir", "result") ⑤
categories = kwargs.get("categories",
["object",
"scene",
"part",
"texture",
"color"]) ⑥
iou_thres = kwargs.get("iou_thres",
0.04) ⑦
layer = kwargs.get("layer", "layer4") ⑧
ud_data = [] ⑨
for network_name in network_names: ⑩
for dataset in datasets: ⑪
result_file = os.path.join(results_dir,
f"pytorch_{network_name}_{dataset}/tally_{layer}.csv") ⑫
df_result = pd.read_csv(result_file) ⑫
ud = OrderedDict() ⑬
ud["network_name"] = network_name ⑬
ud["dataset"] = dataset ⑬
ud["num_units"] = len(df_result) ⑬
num_ud = 0 ⑭
for category in categories: ⑮
df_cat = df_result[df_result["category"] ==
➥ category].reset_index(drop=True) ⑯
df_unique_detectors = df_cat[df_cat[f"{category}-iou"] >
➥ iou_thres].reset_index(drop=True) ⑰
ud[f"num_ud_{category}"] = len(df_unique_detectors) ⑱
ud[f"num_ud_{category}_pc"] = len(df_unique_detectors) /
➥ ud["num_units"] * 100 ⑲
num_ud += len(df_unique_detectors) ⑳
ud["num_ud"] = num_ud ㉑
ud["num_ud_pc"] = ud["num_ud"] / ud["num_units"] * 100 ㉑
ud_data.append(ud) ㉒
df_ud = pd.DataFrame(ud_data) ㉓
return df_ud ㉔
① 导入函数所需的模块
② 计算唯一检测器数量的函数。它接受一组关键字参数。
③ network_names关键字参数是我们需要计算唯一检测器数量的模型列表
④ 模型预训练的数据集列表
⑤ 指向网络剖析框架为每个模型保存结果的父目录
⑥ 所有感兴趣的概念类别列表
⑦ 测量单元是否为概念检测器的 IoU 阈值;默认设置为 0.04
⑧ layer参数默认设置为 ResNet-18 模型的最终层。
⑨ 初始化一个空列表来存储唯一检测器数量的结果
⑩ 遍历每个网络或模型
⑪ 遍历模型预训练的每个数据集
⑫ 将 tally_{layer}.csv 文件加载为 Pandas DataFrame
⑬ 初始化一个 OrderedDict 数据结构来存储给定网络和数据的检测结果
⑭ 将唯一检测器的数量初始化为 0
⑮ 遍历每个概念类别
⑯ 获取该概念类别的结果
⑰ 过滤 IoU 得分大于阈值的单元
⑱ 结果 DataFrame 中的行数是该概念类别唯一检测器的数量。
⑲ 计算检测该概念类别的单元占总单元数量的比例
⑳ 增加唯一检测器数量的计数
㉑ 将唯一检测器结果存储在 OrderedDict 数据结构中
㉒ 将结果追加到列表中
㉓ 将结果列表转换为 Pandas DataFrame
㉔ 返回 DataFrame
我们可以通过运行以下代码行来获取在 ImageNet 和 Places 上预训练的 ResNet-18 模型最终特征学习层的唯一检测器数量。请注意,该函数没有提供任何关键字参数,因为参数的默认值将计算 ImageNet 和 Places 上预训练的 ResNet-18 模型最终特征学习层的统计数据:
df_ud = compute_unique_detectors()
如果我们想要计算,比如说,第三特征学习层的统计数据,我们可以按照以下方式调用函数:
df_ud = compute_unique_detectors(layer="layer3")
一旦我们获得了独特检测器的数量作为 Pandas DataFrame,我们使用以下函数来绘制结果:
def plot_unique_detectors(df_ud, **kwargs): ①
categories = kwargs.get("categories",
["object",
"scene",
"part",
"texture",
"color"]) ②
num_ud_cols = [f"num_ud_{c}" for c in categories] ③
num_ud_pc_cols = [f"num_ud_{c}_pc" for c in categories] ④
num_ud_col_rename = {} ⑤
num_ud_pc_col_rename = {} ⑤
for c in categories: ⑤
num_ud_col_rename[f"num_ud_{c}"] = c.capitalize() ⑤
num_ud_pc_col_rename[f"num_ud_{c}_pc"] = c.capitalize() ⑤
df_ud["network_dataset"] = df_ud.apply(lambda x: x["network_name"] + "_" ⑥
➥ + x["dataset"], axis=1) ⑥
df_ud_num = df_ud.set_index("network_dataset")[num_ud_cols] ⑥
df_ud_num_pc = df_ud.set_index("network_dataset")[num_ud_pc_cols] ⑥
df_ud_num = df_ud_num.rename(columns=num_ud_col_rename) ⑦
df_ud_num_pc = df_ud_num_pc.rename(columns=num_ud_pc_col_rename) ⑦
f, ax = plt.subplots(2, 1, figsize=(8, 10)) ⑧
df_ud_num.plot(kind='bar', stacked=True, ax=ax[0]) ⑨
ax[0].legend(loc='center left', bbox_to_anchor=(1, 0.5)) ⑨
ax[0].set_ylabel("Number of Unique Detectors") ⑨
ax[0].set_xlabel("") ⑨
ax[0].set_xticklabels(ax[0].get_xticklabels(), rotation=0) ⑨
df_ud_num_pc.plot(kind='bar', stacked=True, ax=ax[1]) ⑩
ax[1].get_legend().remove() ⑩
ax[1].set_ylabel("Proportion of Unique Detectors (%)") ⑩
ax[1].set_xlabel("") ⑩
ax[1].set_xticklabels(ax[1].get_xticklabels(), rotation=0) ⑩
return f, ax ⑪
① 绘制独特检测器的数量;接受 compute_unique_detectors 函数返回的 DataFrame 和某些关键字参数
② 我们感兴趣的所有概念类别的列表
③ 包含每个概念类别独特检测器数量的列名列表
④ 包含每个概念类别独特检测器比例的列名列表
⑤ 将列名重命名为大写概念类别名称的字典
⑥ 按网络名称和数据集索引 DataFrame
⑦ 将列名重命名为大写概念类别名称
⑧ 创建一个具有两个子图行的 Matplotlib 图
⑨ 在第一个子图中,将独特检测器的数量以堆叠条形图的形式可视化
⑩ 在第二个子图中,将独特检测器的比例以堆叠条形图的形式可视化
⑪ 返回 Matplotlib 图和坐标轴
按如下方式绘制独特检测器和比例。结果图如图 6.11 所示:
f, ax = plot_unique_detectors(df_ud)
图 6.11 的最上面一行显示了在 ImageNet 和 Places 数据集上预训练的两个 ResNet-18 模型在最终特征学习层中的独特检测器的绝对数量。最下面一行显示了作为最终层中单元总数比例的计数。ResNet-18 的最终特征学习层中的单元总数为 512。我们可以看到,ImageNet 模型有 302 个独特检测器,这大约占总单元的 59%。另一方面,Places 模型有 435 个独特检测器,这大约占总单元的 85%。总的来说,看起来在 Places 数据集上训练的模型比 ImageNet 拥有更多样化的概念检测器集。地点通常由多个场景组成。这就是为什么我们在 Places 数据集上训练的模型中比在 ImageNet 数据集中看到更多的场景检测器。ImageNet 数据集包含更多的物体。这就是为什么我们在 ImageNet 模型上看到更多的物体检测器。我们还可以观察到在最终特征学习层中比低级概念(如颜色、纹理和部分)出现更多的更高级概念,如物体和场景。

图 6.11 独特检测器的数量——ImageNet 与 Places 的比较
现在我们将扩展compute_unique_detectors函数,以计算 ResNet-18 模型中所有特征学习层的独特检测器。这样我们可以观察到网络中所有层学习到的概念。作为一个练习,我鼓励你更新这个函数,并使用一个表示特征学习层列表的layers关键字参数。还要添加一个嵌套循环来遍历所有层,计算每层的独特检测器数量。这个练习的解决方案可以在与本书相关的 GitHub 存储库中找到,网址为mng.bz/KBdZ。
一旦你获得了包含所有层独特检测器数量的 DataFrame,你可以使用以下辅助函数将统计数据绘制成折线图:
def plot_ud_layers(df_ud_layer): ①
def plot_ud_layers_dataset(df_ud_layer_dataset, ax): ②③
object_uds = df_ud_layer_dataset["num_ud_object_pc"].values ③
scene_uds = df_ud_layer_dataset["num_ud_scene_pc"].values ③
part_uds = df_ud_layer_dataset["num_ud_part_pc"].values ③
texture_uds = df_ud_layer_dataset["num_ud_texture_pc"].values ③
color_uds = df_ud_layer_dataset["num_ud_color_pc"].values ③
ax.plot(object_uds, '^-', label="object") ④
ax.plot(scene_uds, 's-', label="scene") ④
ax.plot(part_uds, 'o-', label="part") ④
ax.plot(texture_uds, '*-', label="texture") ④
ax.plot(color_uds, 'v-', label="color") ④
ax.legend() ⑤
ax.set_xticks([0, 1, 2, 3]) ⑥
ax.set_xticklabels(["Layer 1", "Layer 2", "Layer 3", "Layer 4"]) ⑥
ax.set_ylabel("Proportion of Unique Detectors (%)") ⑦
df_ud_layer_r18_p365 = df_ud_layer[(df_ud_layer["network_name"] ==
➥ "resnet18") &
(df_ud_layer["dataset"] ==
➥ "places365")].reset_index(drop=True) ⑧
df_ud_layer_r18_imgnet = df_ud_layer[(df_ud_layer["network_name"] ==
➥ "resnet18") &
(df_ud_layer["dataset"] ==
➥ "imagenet")].reset_index(drop=True) ⑨
f, ax = plt.subplots(2, 1, figsize=(8, 10)) ⑩
plot_ud_layers_dataset(df_ud_layer_r18_imgnet, ax[0]) ⑪
ax[0].set_title("resnet18_imagenet") ⑪
plot_ud_layers_dataset(df_ud_layer_r18_p365, ax[1]) ⑫
ax[1].set_title("resnet18_places365") ⑫
return f, ax ⑬
① 绘制网络中所有层的独特检测器的比例
② 绘制在给定数据集上预训练的网络中所有层的独特检测器比例
③ 提取所有概念类别的统计数据
④ 将统计数据绘制成折线图
⑤ 显示图例
⑥ 标记网络中所有层的 x 轴刻度
⑦ 标记 y 轴
⑧ 从源 DataFrame 中筛选出在 Places 数据集上预训练的网络的过滤器行
⑨ 从源 DataFrame 中筛选出在 ImageNet 数据集上预训练的网络的过滤器行
⑩ 创建一个具有两个子图行的 Matplotlib 图
⑪ 绘制 ImageNet 数据集第一子图行中所有层的统计数据
⑫ 绘制 Places 数据集第二子图行中所有层的统计数据
⑬ 返回图和轴
我们可以通过运行以下代码行获得图 6.12 中的图表:
f, ax = plot_ud_layers(df_ud_layer)

图 6.12 层数中独特检测器的数量——ImageNet 与 Places
图 6.12 的顶部行显示了在 ImageNet 数据集上预训练的 ResNet-18 模型中所有层的独特检测器的比例。底部行显示了在 Places 数据集上训练的模型的相同统计数据。我们可以看到,对于这两个模型,低级概念类别如颜色和纹理在较低的特征学习层中出现,而高级概念类别如部分、物体和场景在较高或较深的层中出现。这意味着在较深的层中学习了更多的高级概念。我们可以看到,随着层深度的增加,网络的表征能力也在增加。较深的层有更多的能力学习复杂的视觉概念,如物体和场景。在下一节中,我们将通过查看网络中每个单元学习到的特定标签和概念来进一步剖析网络。
6.4.3 训练任务的概念检测器
在上一节中,我们可视化了所有高级概念类别中独特检测器的数量。现在让我们深入挖掘,并可视化 Broden 数据集中每个概念或标签的独特检测器数量。我们将关注最终的特征学习层以及按独特检测器数量排名前三的概念类别:纹理、物体和场景。
我们需要将 6.4.2 节中开发的compute_unique_detectors函数扩展到按概念或标签计算统计数据。作为一个练习,我强烈建议您这样做,因为它将使您更好地理解 NetDissect-Lite 库生成的 tally_{layer}.csv 文件的格式。您可以传递一个新的关键字参数,让函数知道是否按概念类别或按概念或标签聚合。对于按概念或标签聚合,您需要按category和label分组,并计算类别 IoU 分数大于阈值的单元数量。这个练习的解决方案可以在与本书相关的 GitHub 存储库中找到。调用新函数并将结果存储在名为df_cat_label_ud的 DataFrame 中。
我们首先将查看纹理概念类别。使用以下代码片段提取纹理概念类别的 DataFrames:
df_r18_imgnet_texture = df_cat_label_ud[(df_cat_label_ud["network_name"] ==
➥ "resnet18") &
(df_cat_label_ud["dataset"] == "imagenet") &
(df_cat_label_ud["category"] == "texture")].\
sort_values(by="unit", ascending=False).reset_index(drop=True) ①
df_r18_p365_texture = df_cat_label_ud[(df_cat_label_ud["network_name"] ==
➥ "resnet18") &
(df_cat_label_ud["dataset"] == "places365") &
(df_cat_label_ud["category"] == "texture")].\
sort_values(by="unit", ascending=False).reset_index(drop=True) ②
① 从在 ImageNet 上预训练的 ResNet-18 模型中提取统计数据,并按 IoU 分数降序排列
② 从在 Places 上预训练的 ResNet-18 模型中提取统计数据,并按 IoU 分数降序排列
您现在可以使用以下代码可视化各种纹理概念的独特检测器数量。结果图如图 6.13 所示:
import seaborn as sns ①
f, ax = plt.subplots(1, 2, figsize=(16, 10)) ②
sns.barplot(x="unit", y="label", data=df_r18_imgnet_object,
➥ ax=ax[0]) ③
ax[0].set_title(f"resnet18_imagenet : {len(df_r18_ ③
➥ imgnet_object)} objects") ③
ax[0].set_xlabel("Number of Unique Detectors") ③
ax[0].set_ylabel("") ③
sns.barplot(x="unit", y="label", data=df_r18_ ④
➥ p365_object, ax=ax[1]) ④
ax[1].set_title(f"resnet18_places365 : {len ④
➥ (df_r18_p365_object)} objects") ④
ax[1].set_xlabel("Number of Unique Detectors") ④
ax[1].set_ylabel(""); ④
① 导入 Seaborn 库
② 创建一个 Matplotlib 图,包含两个子图列
③ 绘制 ImageNet 模型学习到的所有纹理概念的独特检测器数量
④ 绘制 Places 模型学习到的所有纹理概念的独特检测器数量
在上一节(见图 6.11)中,我们观察到,对于纹理概念类别,ImageNet 模型在最终层的独特检测器数量比在 Places 数据集上训练的模型更多。但是,这个层中单元学习到的概念有多多样?图 6.13 旨在回答这个问题。我们可以看到,ImageNet 模型涵盖了 27 个纹理概念,而 Places 模型涵盖了 21 个。ImageNet 模型的前三个纹理概念占 19 个独特检测器。它们是条纹、波浪和螺旋。另一方面,Places 模型的前三个纹理概念占 10 个独特检测器。它们是交织、棋盘和层状。尽管 Places 模型在最终层中学习的纹理数量较少,但我们看到在较低的特征学习层中,这个模型的独特检测器比例更高(如图 6.12 所示)。

图 6.13 唯一纹理检测器数量——ImageNet 与 Places
现在,让我们可视化各种对象和场景概念的唯一检测器数量。作为一个练习,将用于纹理概念类别的代码扩展到对象和场景。对象概念类别的结果图显示在图 6.14 中。因为基于 Places 数据集训练的模型在最终层检测到很多场景,所以场景概念类别的可视化被分成两个单独的图表。图 6.15 显示了 ImageNet 模型的场景检测器数量,图 6.16 显示了 Places 模型的场景检测器数量。

图 6.14 唯一对象检测器数量——ImageNet 与 Places
让我们先看看图 6.14。在前一节中,我们观察到 ImageNet 模型在高级对象类别中有更高比例的唯一检测器,因为 ImageNet 数据集中包含更多的对象。然而,如果我们观察学习到的概念有多样化,我们可以看到在 Places 数据集上训练的模型中出现了更多的对象。Places 模型检测到 45 个对象,而 ImageNet 模型在最终特征学习层中检测到的对象是 36 个。ImageNet 模型检测到的顶级对象是狗,占 25 个唯一检测器——ImageNet 数据集中有大量标记的狗的图像。Places 模型检测到的顶级对象是飞机,占 11 个唯一检测器。

图 6.15 唯一场景检测器数量——ImageNet 数据集
如果我们现在比较图 6.15 和图 6.16,我们可以看到在 Places 数据集上训练的模型能够识别比 ImageNet 模型(119 个对 30 个)更多样化的场景集。这是预期的,因为 Places 数据集包含大量的标记地点,这些地点通常由许多场景组成。注意,在图 6.16 中,尽管在 Places 数据集上训练的模型总共能够识别 119 个场景,但图中只显示了前 40 个场景,以便更容易阅读图表。

图 6.16 唯一场景检测器数量——Places 数据集
通过深入挖掘并可视化每个概念的唯一检测器数量,我们可以确保用于训练模型的数据库足够多样化,并且对感兴趣的概念有良好的覆盖。我们还可以使用这些可视化来了解单元在每个神经网络层的关注点是什么。
迁移学习
迁移学习是一种技术,其中针对特定任务训练的模型被用作另一个任务的起点。例如,假设我们有一个在 ImageNet 数据集上训练的模型,该模型在检测对象方面表现良好。我们希望将此模型用作起点来检测地点和场景。为此,我们可以加载 ImageNet 模型的权重,并在训练和微调到 Places 数据集之前使用这些权重作为起点。迁移学习背后的基本思想是,在一个领域学习到的特征可以在另一个领域重用,前提是这两个领域之间有一些重叠。当一个领域的预训练网络在另一个领域的任务上进行训练时,通常训练时间更快,并且会产生更准确的结果。
网络分解框架的作者在其论文中分析了单元的可解释性在迁移学习过程中的演变,该论文可在arxiv.org/pdf/1711.05611.pdf找到。他们使用了在 ImageNet 数据集上预训练并微调到 Places 数据集的 AlexNet 模型。作者观察到,对于在 ImageNet 上预训练但微调到 Places 的模型,独特检测器的数量有所增加。最初检测狗的单元演变为其他对象和场景,如马、牛和瀑布。Places 数据集中的许多地方都包含这些动物和场景。如果预训练在 Places 上的模型微调到 ImageNet 数据集,作者观察到独特检测器的数量有所下降。对于 Places-to-ImageNet 网络,许多单元演变为狗检测器,因为 ImageNet 中狗的标记数据比例要高得多。
6.4.4 可视化概念检测器
在前面的章节中,我们通过查看每个概念类别和个别概念的独特检测器数量来量化 CNN 中每个特征学习层中单元的可解释性。在本节中,我们将通过 NetDissect 库可视化特征学习层中每个单元生成的二值单元分割图。该库将在原始图像上叠加二值单元分割图并为我们生成 JPG 文件。如果我们想可视化原始图像中单元针对特定概念关注的特定像素,这将非常有用。
由于空间有限,我们无法可视化所有单元和模型生成的所有图像。因此,我们将重点关注在 Places 数据集上预训练的模型,以及某些概念的最大激活图像的特定单元。以下函数可以用来获取由库生成的二值分割图像:
import matplotlib.image as mpimg ①
def get_image_and_stats(**kwargs): ②
network_name = kwargs.get("network_name", ③
"resnet18") ③
dataset = kwargs.get("dataset", ③
"places365") ③
results_dir = kwargs.get("results_dir", "result") ③
layer = kwargs.get("layer", "layer4") ③
unit = kwargs.get("unit", "0000") ③
result_file = os.path.join(results_dir,
f"pytorch_{network_name}_{dataset}/tally_{layer}.csv") ④
df_result = pd.read_csv(result_file) ④
image_dir = os.path.join(results_dir,
f"pytorch_{network_name}_{dataset}/html/image") ⑤
image_file = os.path.join(image_dir, ⑤
f"{layer}-{unit}.jpg") ⑤
img = mpimg.imread(image_file) ⑤
df_unit_stats = df_result[df_result["unit"] == int(unit)+1] ⑥
stats = None ⑦
if len(df_unit_stats) > 0: ⑧
stats = { ⑧
"category": df_unit_stats["category"].tolist()[0], ⑧
"label": df_unit_stats["label"].tolist()[0], ⑧
"iou": df_unit_stats["score"].tolist()[0] ⑧
} ⑧
return img, stats ⑨
① 导入 Matplotlib 提供的 mpimg 模块以加载和显示图像
② 获取给定单元在图像上叠加的二值单元分割图及其相关统计数据
获取网络名称、数据集、结果目录、特征学习层和感兴趣的单元
将计数字段文件加载为 Pandas DataFrame
加载叠加在原始图像上的二进制单元分割图
过滤 DataFrame 以获取感兴趣层和单元的统计数据
初始化统计数据变量为 None
如果感兴趣层和单元有结果,从 DataFrame 中提取类别、标签和 IoU,并将它们存储在字典中
返回感兴趣层和单元的图像和相关统计数据
我们现在将重点关注单元 247 和 39,它们检测飞机对象。我们在上一节中看到(见图 6.14),飞机对象在 Places 模型中的所有对象中具有最独特的检测器。这些单元按零索引,并由 NetDissect 库保存为四位字符串。因此,我们需要将字符串“0246”和“0038”分别作为单元 247 和 39 的单元关键字参数传递给get_image_and_stats函数。以下代码片段将获取图像和相关统计数据,并在 Matplotlib 中可视化。结果图如图 6.17 所示:
img_247, stats_247 = get_image_and_stats(unit="0246") ①
img_39, stats_39 = get_image_and_stats(unit="0038") ②
f, ax = plt.subplots(2, 1, figsize=(15, 4)) ③
ax[0].imshow(img_247, interpolation='nearest') ④
ax[0].grid(False) ④
ax[0].axis(False) ④
ax[0].set_title(f"Unit: 247, Label: {stats_247['label']}, Category: ④
➥ {stats_247['category']}, IoU: {stats_247['iou']:.2f}", ④
fontsize=16) ④
ax[1].imshow(img_39, interpolation='nearest') ⑤
ax[1].grid(False) ⑤
ax[1].axis(False) ⑤
ax[1].set_title(f"Unit: 39, Label: {stats_39['label']}, Category: ⑤
➥ {stats_39['category']}, IoU: {stats_39['iou']:.2f}", ⑤
fontsize=16); ⑤
从单元 247 提取图像和统计数据
从单元 39 提取图像和统计数据
创建一个具有两个子图行的 Matplotlib 图像
在顶部行显示单元 247 的图像并在标题中显示统计数据
在底部行显示单元 39 的图像并在标题中显示统计数据

图 6.17 对象概念检测器——飞机的可视化
图 6.17 顶部行显示了为单元 247 的 10 个最大激活 Broden 图像生成的分割。平均 IoU 为 0.19。由于二进制单元分割图叠加在原始图像上,激活的像素是那些满足S[i] >= T[k]的像素。未激活的像素显示为黑色。从图像中我们可以看到,该单元专注于飞机,而不是任何其他随机对象。图 6.17 底部行显示了为单元 39 的 10 个最大激活 Broden 图像生成的分割。平均 IoU 为 0.06,并且在这种情况下较低。从图像中我们可以看到,该单元在飞机上以及一般概念(如鸟类、飞行、天空和蓝色)上激活。
图 6.18 显示了为三个对象生成的二值分割图像,分别是火车(顶部行)、公共汽车(中间行)和轨道(底部行)。具体的单元分别是 168、386 和 218。对于火车概念检测器,我们可以看到激活的像素突出显示引擎和铁路轨道。在这种情况下,平均 IoU 很高,为 0.27。对于公共汽车概念检测器,激活的像素似乎突出显示公共汽车和像任何大型窗户和相对平坦前端的车辆这样的通用概念。在这种情况下,平均 IoU 为 0.24。轨道概念检测器很有趣。激活的像素似乎突出显示具有两条平行轨道的图像,包括铁路轨道、保龄球道和寿司传送带。在这种情况下,平均 IoU 为 0.06。

图 6.18 物体概念检测器可视化——火车、公共汽车和轨道
最后,图 6.19 显示了训练集中未直接表示的场景的分割图像。我们特别关注单元 379 和 370,分别突出显示高速公路和托儿所场景。顶部行显示高速公路场景,底部行显示托儿所场景。我们可以看到,在 Places 数据集上训练的模型正在很好地学习这些高级场景概念。

图 6.19 场景概念检测器可视化——高速公路和托儿所
6.4.5 网络解剖的局限性
网络解剖框架是一个伟大的工具,帮助我们打开黑盒神经网络。它通过提出可量化的解释来克服视觉归因方法的局限性。我们可以通过可视化每个单元在特征学习层中学习的特征或概念,来了解 CNN 如何分解识别图像的任务。然而,网络解剖框架具有以下局限性,如框架原作者在原始论文中所强调的:
-
该框架需要像素级别的概念标记数据集。这是框架中最关键的一步,可能非常耗时且成本高昂。此外,数据集中未表达的概念在解释单元时不会显示出来,即使网络已经学习了它们。
-
该框架无法识别代表一个概念的多个单元的组合。
-
单元的可解释性通过“唯一检测器数量”指标来量化。该指标倾向于更大、更深的网络,这些网络具有学习更多高级概念的能力。
神经网络的解剖分析是一个活跃的研究领域,研究社区正在探索许多有希望的道路,例如自动识别概念和使用概念分数来识别对神经网络的对抗攻击。
摘要
-
我们在上一章中学到的视觉归因方法存在一些局限性。它们通常通过定性评估,并且相当主观。这些技术并没有给我们提供关于卷积神经网络(CNN)中特征学习层和单元所学习的低级和高级概念或特征的信息。
-
本章讨论的网络剖析框架通过提出更定量的解释来克服视觉归因方法的局限性。通过使用该框架,我们还将能够理解 CNN 的特征学习层学习了哪些人类可理解的概念。
-
该框架包括三个关键步骤:概念定义、网络探测和对齐测量。概念定义步骤是最关键的步骤,因为它要求我们收集一个像素级别的概念标记数据集。网络探测步骤是关于在网络中找到对那些预定义概念做出响应的单元。最后,对齐测量步骤量化了单元激活与那些概念之间的对齐程度。
-
我们学习了如何使用 NetDissect 库在 PyTorch 模型上运行网络剖析框架,这些模型是在 ImageNet 和 Places 数据集上训练的。我们使用了 Broden 数据集来定义概念。
-
我们学习了如何通过使用“独特检测器数量”指标来量化单元的可解释性,并可视化不同概念类别和个别概念单元的可解释性。
-
我们还学习了如何可视化由库生成的二值单元分割图像,以查看单元对特定概念关注的像素。
-
网络剖析框架是一个帮助我们打开黑盒神经网络的优秀工具。然而,它也有一些局限性。创建一个概念标记数据集可能非常耗时且成本高昂。该框架无法识别代表一个概念的单元组。独特的检测器数量指标倾向于更大的、更深的网络,这些网络有学习更多高级概念的能力。
7 理解语义相似性
本章涵盖
-
学习捕获语义意义的密集词表示
-
使用 PCA 和 t-SNE 等降维技术可视化高维词嵌入的语义相似性
-
PCA 和 t-SNE 的优缺点
-
对 PCA 和 t-SNE 生成的可视化进行定性和定量验证
在上一章中,我们将关注点从解释黑盒模型内部发生的复杂处理和操作转向解释模型学习到的表示或特征。我们特别研究了网络剖析框架来了解卷积神经网络(CNN)中的特征学习层学习到了哪些概念。该框架包括三个关键步骤:概念定义、网络探测和对齐测量。概念定义步骤主要涉及数据收集,特别是收集像素级别的概念标注数据集。这是最耗时且最关键的步骤。下一步是探测网络,确定 CNN 中哪些单元对预定义的概念做出响应。最后一步是量化单元响应与概念的匹配程度。该框架通过提出以人类可理解的概念形式存在的定量解释,克服了视觉归因方法的局限性。
在本章中,我们将继续探讨解释深度神经网络学习到的表示的主题,但将关注点转向自然语言处理(NLP)。NLP 是机器学习的一个子领域,它处理自然语言。到目前为止,我们一直在处理图像或以数值特征表格形式输入。在 NLP 中,我们将处理文本形式的输入。我们将特别关注如何以密集和语义上有意义的形式表示文本,以及如何解释那些意义相似——即那些具有语义相似性的——由这些表示学习到的单词。
我们将首先介绍一个分析电影评论情感的具体例子。然后,我们将了解神经词嵌入,这是深度学习中的一个有趣分支,广泛用于以语义上有意义的形式表示文本。这些词表示可以用作预测情感的模型的输入。本章的其余部分将专注于从词表示中解释和可视化语义相似性。我们将特别学习线性和非线性降维技术,如主成分分析(PCA)和 t 分布随机邻域嵌入(t-SNE)。
7.1 情感分析
在本章中,我们接受了一个名为“互联网电影库”的电影网站的委托,确定电影评论的情感。目标是确定一个评论是否与积极或消极的情绪相关联。如图 7.1 所示,我们有两个电影和每个电影的几条评论。这两个电影的评分仅用于说明目的。基于每条评论中的单词或单词序列,我们想要确定评论表达的是积极情绪或观点,还是消极情绪。

图 7.1 电影评论的情感分析
目标是构建一个 AI 系统,给定一个评论作为输入,判断该评论是否传达了积极或消极的情绪。有了这个信息,我们可以将问题表述为一个二元分类问题。这将会类似于我们在第四章和第五章中看到的二元分类器,但与处理具有数值特征的表格数据或图像不同,我们处理的是一系列单词,如图 7.2 所示。模型的输入是一系列代表评论的单词,输出是一个分数,表示评论情感为积极的概率。

图 7.2 情感二分类器
图 7.2 中的情感分析模型被表示为一个黑盒。我们将在 7.3.4 节中详细介绍模型的具体情况。在我们深入构建模型的方法之前,我们想要回答以下两个关键问题:
-
我们如何将一个单词表示成模型可以处理的形式?
-
我们如何对一系列单词进行建模并基于此构建分类器?
本书的主要重点是回答第一个问题。我们将学习可以用来以密集和语义上有意义的形式表示单词的深度学习模型,以及如何解释它们。一旦我们找到了表示单词的好方法,回答第二个问题——如何构建处理一系列单词的模型——就会变得更加直接。尽管这并不是本书的主要焦点,但我们将简要介绍序列建模以及如何使用我们在前几章中学到的技术来解释这类模型。在我们深入单词表示之前,让我们首先探索电影评论数据集,弄清楚为什么我们需要一个好的单词表示来构建情感分类器。
7.2 探索性数据分析
在本节中,我们将探索电影评论数据集,并确定我们是否可以构建任何数值特征来训练一个更简单的逻辑回归或基于树的模型。主要目标是确定是否需要提出具有语义意义的词表示以及建模词序列。我们将使用 PyTorch 提供的torchtext包来加载和处理数据集。torchtext包与torchvision类似,因为它提供了各种数据处理实用工具、流行数据集和 NLP 模型。我们可以使用 pip 安装此包,如下所示:
$> pip install torchtext
除了torchtext,我们还将安装 spaCy,这是一个流行的 NLP 库,我们将用它来进行字符串分词。分词是将文本字符串分割成离散组件或标记的过程,例如单词和标点符号。一种简单的分词方法是按空格分割文本字符串,但这种方法没有考虑到标点符号。spaCy库提供了在多种语言中分词字符串的更复杂的方法。在本章中,我们将专注于英语语言,因此使用名为en_core_web_sm的模型进行字符串分词。spaCy 库和模型可以按照以下方式安装:
$> pip install spacy
$> python -m spacy download en_core_web_sm
在所有库都就绪后,我们现在可以按照以下方式加载电影评论数据集:
import torch ①
from torchtext.legacy import data, datasets ①
TEXT = data.Field(tokenize='spacy', ②
tokenizer_language='en_core_web_sm') ②
LABEL = data.LabelField(dtype=torch.float) ③
train_data, test_data = datasets.IMDB.splits(TEXT, LABEL) ④
① 导入 PyTorch 和来自 torchtext 的相关实用工具
② 使用电影评论文本的标记器初始化 Field 类
③ 初始化 LabelField 类以将情感标签加载为浮点数
④ 加载电影评论数据集并将其分为训练集和测试集
让我们现在看看这个数据集的一些关键汇总统计数据,例如训练集和测试集中的评论数量、正面和负面评论的比例以及每条评论中的单词数量,这些数据总结在表 7.1 中。为了节省空间,我们不会展示相应的源代码,但你可以从与本书相关的 GitHub 仓库中获取它。
表 7.1 电影评论数据集的关键统计数据
| 统计信息 | 训练集 | 测试集 |
|---|---|---|
| 评论数量 | 25,000 | 25,000 |
| 正面评论比例 | 50% | 50% |
| 负面评论比例 | 50% | 50% |
| 正面评论中的单词数量 | 最小值 | 14 |
| 中位数 | 202 | 198 |
| 最大值 | 2789 | 2640 |
| 负面评论中的单词数量 | 最小值 | 11 |
| 中位数 | 203 | 203 |
| 最大值 | 1827 | 1290 |
从表 7.1 中,我们可以观察到训练集和测试集的电影评论数量相等——每个集都有 25,000 条。两个集合中的正面和负面评论都平均分配。我们还可以观察到,在训练集和测试集中,评论中单词数量的汇总统计相似。我们可以看到正面和负面评论之间的一些差异,特别是每条评论的最小和最大单词数。除了理解数据集之外,查看这些关键汇总统计的原因是为了确定我们是否可以构建某些数值特征,并构建一个简单的逻辑回归或基于树的分类器来进行情感分析。
沿着这个思路,让我们看看每条评论中单词数量的分布,比较正面和负面情感。正面和负面评论的单词数量是否有差异?如果有,负面评论通常比正面评论长还是短?我们可以通过查看图 7.3 来回答这些问题。您可以在与本书相关的 GitHub 仓库中找到生成此图表的源代码。

图 7.3 每条评论中单词数量的分布——正面与负面
在图 7.3 中,我们可以看到在单词数量方面,正面和负面评论之间没有明显的差异。因此,仅通过查看单词数量并不能准确预测评论是正面还是负面。

图 7.4 正面评论的词云
关于单词的频率或出现次数呢?是否有某些单词在正面或负面评论中更常见?图 7.4 显示了正面评论中所有常见单词的词云。词云是在一些数据清理之后生成的,其中去除了非常常见的单词,如a、the、is、at、which和on(也称为停用词)以及标点符号。您可以在与本书相关的 GitHub 仓库中访问用于移除所有停用词并清理数据的代码。在词云中,单词越大,它在评论中出现的频率就越高。我们可以看到,对于正面评论,最频繁出现的单词是像film、movie、one和character这样的单词。我们还看到传达积极情绪的单词,如love、great、good和wonderful。

图 7.5 负面评论的词云
图 7.5 展示了负面评论的词云。乍一看,我们确实看到了一些与正面评论中相同的词语——例如电影、电影、一个和角色——这些词语也常见于负面评论中。我们还可以看到一些表达负面情绪的词语,例如糟糕、不幸地、差劲和愚蠢。如果我们比较图 7.4 和图 7.5,正面评论和负面评论在词语数量上并没有明显的差异。然而,我们可以通过进一步使用人类知识和启发式方法清理数据集来从这一词语计数特征中找到更多的信号。例如,我们可以移除一些中性的词语,如电影、电影、一个和角色,仅举几例。正如你所想象的那样,这种使用一些语言背景知识(例如识别中性词语)和启发式方法进行特征工程的方法是相当耗时且不一定容易扩展到其他语言的。我们需要一种更好的方法来表示语言中的词语,这将是下一节的重点。
7.3 神经词语嵌入
在上一节中,我们看到了提出数值特征以训练情感分析模型是多么困难。现在,我们将学习如何以数值形式表示词语,尽可能多地编码其意义。然后我们可以使用这些词语表示来训练情感分析模型。在我们深入之前,让我们先澄清一下术语。编码语义意义的词语的密集表示称为词语嵌入、词语向量或分布式表示。由神经网络学习的词语表示或词语嵌入称为神经词语嵌入。在本章中,我们将重点关注神经词语嵌入。
我们还需要了解一些更多的 NLP 术语。我们将使用术语语料库来指代我们将要处理的文本体。对于电影评论示例,语料库将是数据集中所有的电影评论。我们将使用术语词汇表来指代文本语料库中的词语。
7.3.1 单热编码
现在,让我们看看一种表示词语的简单方法,这表明了词语嵌入的需求。这个练习突出了需要提出更复杂的方法来以密集、语义有意义的形式表示词语。假设我们有一个包含词汇表中的V个词语的文本语料库。词汇表大小V通常相当大。让我们看看图 7.6 所示的例子。在图中,我们可以看到语料库中的词语列在左边的表中。从表中我们可以看到,语料库由超过 10,000 个词语组成。语料库中的每个词语在表中都被分配了一个索引。

图 7.6 单热编码向量的示意图
在语料库中表示单词的一种天真方法是用一个大小等于词汇量大小 V 的向量,其中向量的每个条目对应于语料库中的一个单词。在图 7.6 中,我们可以看到短语“movie is a masterpiece.”中单词的表示。单词 this 的天真表示由一个向量组成,其中其他每个单词的条目都是 0,而单词 this 的位置或索引处的值是 1。同样,对于句子中的其他单词,我们可以看到一个全为零的向量,除了单词所在索引处的值为 1。这种表示方式被称为 独热编码。
如图中所示,独热编码使用了一种极端稀疏的单词表示,其中向量大部分为零,只有一个 1。它不编码任何关于单词的语义信息。使用这种表示方法很难识别经常一起出现或意义相似的单词。向量的尺寸也很大。我们需要一个与词汇量一样大的向量来表示单词。处理这样的向量在计算和存储方面将极其低效。
注意,图 7.6 中的表示确实是一种非常天真表示。我们有方法通过去除停用词来改进表示。这应该会减少用于表示每个单词的独热编码单词向量的尺寸。另一种选择是使用 词袋模型 (BoW)。BoW 模型本质上将每个单词映射到一个数字,该数字表示它在语料库中出现的频率。在 BoW 表示中,停用词通常具有更大的数值,因为它们在语言中频繁出现。我们可以选择删除这些停用词,或者我们可以使用另一种称为 词频逆文档频率 (TF-IDF) 的表示方法。TF-IDF 模型本质上将每个单词在评论语料库中出现的频率与包含该单词的评论数量成反比。这种模型是过滤停用词的好方法,因为它们将与较低的数值相关联。数值较低是因为这些词在评论中频繁出现。BoW 和 TF-IDF 都是表示单词的高效方法,但它们仍然不编码关于单词的语义信息。
7.3.2 Word2Vec
我们可以通过使用Word2Vec(即 Word to Vector)嵌入来克服 one-hot 编码和其他更有效的表示方法(如 BoW 和 TF-IDF)的局限性。Word2Vec 背后的关键思想是观察单词在上下文中的情况。我们可以通过观察通常一起出现的单词来编码意义。让我们来看一个例子并给出一些符号。在图 7.7 中,我们可以看到之前相同的短语,“电影是一部杰作。”该图还显示了一个窗口大小等于 3 的上下文,即由三个标记或单词组成:电影、是和一个。窗口大小等于上下文中的标记或单词数量。我们用w[t]表示上下文中的中心词,用w[t–1]表示紧靠其左边的单词,用w[t+1]表示紧靠其右边的单词。中心词左右两侧的单词也称为周围词或上下文词。

图 7.7 展示了上下文、窗口大小、周围单词和中心词的示例
我们可以使用两种关键的神经网络架构来生成 Word2Vec 嵌入:连续词袋(CBOW)和 skip-gram,如图 7.8 所示。

图 7.8 展示了窗口大小为 3 的 CBOW 和 skip-gram 神经词嵌入模型
如图中所示,CBOW 架构背后的思想是根据周围或上下文单词预测中心词。其背后的神经网络架构是一个包含输入层、隐藏层和输出层的全连接神经网络。另一方面,skip-gram 架构是根据中心词预测周围或上下文单词。其背后的神经网络架构与 CBOW 类似。CBOW 和 skip-gram 模型在尝试预测邻近单词或通常一起出现的单词方面也是相似的。但它们在某些方面有所不同。skip-gram 模型已被证明在少量数据下表现良好,并且能够很好地表示出现频率较低的单词。另一方面,CBOW 模型训练速度更快,并且已被证明能够为出现频率较高的单词提供更好的表示。两种模型的训练过程是等效的。因此,为了简化问题,让我们关注其中之一,并更详细地研究 skip-gram 的训练过程。
跳字图词语嵌入训练的第一步是构建一个训练数据集。给定文本语料库,我们的想法是构建一个数据集,其中包含中心词语作为输入,相应的周围或上下文词语作为输出。在生成数据集之前,我们需要知道上下文窗口大小,因为窗口大小是训练过程中的一个重要超参数。让我们保持与早期模型相同的窗口大小 3,并查看一个具体示例,如图 7.9 所示。在图中,我们使用与之前相同的示例句子。我们将上下文窗口设置在文本的开始处(如图中所示为上下文 1)并识别中心词语和周围词语。然后我们构建一个训练数据表,其中包含中心词语作为输入,周围词语作为输出。在上下文 1 的表中,词语is与两个相邻词语movie和a相关联。

图 7.9 跳字图模型训练数据准备
然后,我们通过将窗口向右滑动一个词语,如图 7.9 中的上下文 2 所示,继续这个过程。我们将为新的中心词语和周围词语添加另一个条目到训练数据表中。我们对语料库中的所有文本重复此过程。一旦我们有了包含输入和输出词语的训练数据集,我们就可以准备训练跳字图神经网络了。我们可以通过将问题重新表述为二分类问题来进一步简化训练过程:不是预测给定中心词语的周围词语,而是预测给定词语对是否为邻居。如果两个词语在上下文中出现,则它们是邻居。我们可以使用图 7.9 中生成的训练数据表来为这个新的二分类公式提供正标签。这如图 7.10 的上半部分所示,其中输入和输出(周围或上下文)词语的表被转换为一个具有正标签(即,标签=1)的词语对表。正标签表示这两个词语是邻居。

图 7.10 带负采样的训练数据准备
我们如何确定负标签,即不是邻居的词语对?我们可以通过一个称为负采样的过程来完成这项工作。对于图 7.9 中的训练数据表中的每个词语,我们从词汇表中随机采样一个新词语。窗口大小的选择很重要。如果与词汇表中的词语数量相比,窗口大小相对较小,则随机采样将确保所选词语出现在输入词语上下文之外的可能性很小。这如图 7.10 的下半部分所示。对于每个输入词语和随机词语的对,我们分配一个负标签(即,标签=0)。这些对应于不是邻居的词语对。
一旦我们有了新二分类公式的训练数据集,我们就可以准备训练跳字模型了。我们将使用这种新公式的神经网络模型称为跳字模型与负采样。输入单词将被表示为一组 one-hot 编码向量。尽管模型被训练来决定两个单词是否是邻居,但训练过程的最终目标是学习单词的神经词嵌入或密集表示。这是架构中隐藏层的目的。对于隐藏层,我们需要初始化图 7.11 中显示的两个矩阵:一个嵌入矩阵和一个上下文矩阵。嵌入矩阵由词汇表中的每个单词一行组成。列数对应于用于表示单词的单词嵌入或单词向量的大小。这如图 7.11 中的 N 所示。

图 7.11 跳字模型(Skip-gram)与负采样训练
在训练之前,我们还需要确定另一个超参数,即嵌入大小。嵌入大小的选择决定了我们希望表示有多密集。它还决定了在表示中捕获了多少语义信息。上下文矩阵的大小与嵌入矩阵相同。这两个矩阵都使用随机值初始化。这些矩阵中的值是神经网络中的参数,我们旨在使用我们在图 7.10 中生成的训练数据集来学习这些参数。
现在我们来更详细地看看学习过程。图 7.11 显示了两个矩阵以及在这些矩阵上执行行内点积操作。行内点积本质上衡量了两个单词对之间的相似性。如果我们然后将得到的向量通过 sigmoid 函数,我们将得到一个介于 0 和 1 之间的相似性或概率度量。然后我们可以将这些分数与训练数据中单词对的真正标签进行比较,并相应地更新参数。参数可以通过反向传播来更新,正如我们在第四章和第五章中学到的。
一旦学习过程完成,我们可以丢弃上下文矩阵,并使用嵌入矩阵作为单词到其对应的神经词嵌入的映射。我们可以通过以下方式获得映射:嵌入矩阵中的每一行都是词汇表中给定单词的表示。例如,矩阵中的第一行对应于单词 w[1] 的表示。第二行是单词 w[2] 的表示,依此类推。
7.3.3 GloVe 嵌入
跳过-负采样模型是生成单词密集表示的绝佳方式,该表示捕捉了在局部上下文中出现的单词对之间的相似性。然而,该模型在识别停用词方面做得并不好。像is、a、the和this这样的停用词将被标记为与像masterpiece这样的单词相似,因为它们在局部上下文中一起出现。我们可以通过查看单词的全局统计信息来识别这样的停用词,即单词对在整个文本语料库中出现的频率。全局向量(也称为GloVe)模型是跳过-负采样的改进,它捕捉了全局和局部统计信息。在接下来的工作中,我们将使用预训练的 GloVe 单词嵌入。
我们将不会使用电影评论数据集从头开始训练 GloVe 单词嵌入,而是将使用在更大文本语料库上预训练的预训练 GloVe 嵌入。用于训练单词嵌入的常见文本语料库是维基百科。我们有以下两种加载维基百科语料库上预训练的 GloVe 嵌入的方法:
-
使用 PyTorch 提供的
torchtext包 -
使用
gensim,这是一个常用的开源 Python 库,用于自然语言处理
使用torchtext加载 GloVe 嵌入的第一种方法,如果我们必须训练另一个下游模型,例如情感分类,该模型将使用这些嵌入作为 PyTorch 中的特征时是有用的。使用gensim加载 GloVe 嵌入的第二种方法对于分析单词嵌入是有用的,因为许多实用函数都是现成的。我们将使用前一种方法来训练情感分类器,后一种方法来解释单词嵌入。我们可以按以下方式使用torchtext加载单词嵌入:
import torchtext.vocab ①
glove = torchtext.vocab.GloVe(name='6B', ②
dim=100) ③
① 从 torchtext 导入词汇模块
② 使用在维基百科上预训练的包含六十亿个单词的模型初始化 GloVe 类
③ 加载大小为 100 的 GloVe 嵌入
注意,已经加载了在维基百科语料库上预训练的包含六十亿个单词的 GloVe 嵌入。预训练模型的嵌入大小为 100。
如果您尚未在您的机器上安装gensim,可以通过运行以下命令来完成:
$> pip install -–upgrade gensim
然后,我们可以按以下方式加载 GloVe 嵌入:
from gensim.models import KeyedVectors ①
from gensim.scripts.glove2word2vec import glove2word2vec ①
from gensim.test.utils import datapath, get_tmpfile ①
path_to_glove = 'data/glove.6B/glove.6B.100d.txt' ②
glove_file = datapath(path_to_glove) ③
word2vec_glove_file = get_tmpfile(glove_file) ③
model = KeyedVectors.load_word2vec_format(word2vec_glove_file) ③
① 从 gensim 导入相关模块和类
② 初始化预训练 GloVe 嵌入文件的路径
③ 初始化 GloVe 嵌入
注意,使用gensim时,我们需要下载预训练的 GloVe 嵌入文件。您可以从 GloVe 项目网站([nlp.stanford.edu/projects/glove/](https://nlp.stanford.edu/projects/glove/))下载在维基百科上预训练的包含六十亿个单词且嵌入大小为 100 的嵌入。
7.3.4 情感分析模型
在 7.1 节中,我们提出了以下两个关键问题,用于构建情感分析模型:
-
我们如何以模型可以处理的形式表示一个单词?
-
我们如何对单词序列进行建模并基于此构建分类器?
我们已经在上一节通过学习神经词嵌入回答了第一个问题。本章的关键重点是词嵌入及其解释。为了完整性,我们通过提供一个如何将单词序列建模以构建情感分类器的高级概述来回答第二个问题。
情感分类器的高级架构如图 7.12 的上半部分所示。它由两个链式连接的神经网络架构组成。第一个神经网络被称为循环神经网络(RNN),第二个神经网络是完全连接的神经网络,我们在第四章中学习过。让我们更详细地看看 RNNs,如图 7.12 的下半部分所示。

图 7.12 使用循环神经网络(RNNs)进行序列建模和情感分析
RNNs 通常用于分析序列,如单词序列,如在情感分析问题中,或时间序列分析,如天气预报。对于情感分析问题,RNN 逐个处理单词序列,并为每个单词生成一个隐藏状态,这是先前输入的表示。单词通过在前一节中学习的神经词嵌入表示输入到 RNN 中。一旦所有单词都已输入到 RNN 中,最终的隐藏状态就用于训练用于情感分类的前馈神经网络。这里我们省略了很多细节,因为这不是本章和本书的主要关注点。关于 RNNs 和语言模型的更多学习资源是斯坦福大学提供的 NLP 与深度学习在线课程(web.stanford.edu/class/cs224n/)。
Transformer 网络
自然语言处理(NLP)领域的一个最近突破是transformer 网络,由谷歌研究团队在 2017 年提出的开创性论文“Attention Is All You Need”(arxiv.org/abs/1706.03762)中提出。与 RNNs 类似,transformer 网络或 trans-formers 用于建模序列数据。正如我们在 7.3.4 节中看到的,RNNs 按顺序逐个处理输入的词。在处理下一个词之前,需要当前词的输出,即隐藏状态。这使得训练过程难以并行化,因此训练 RNNs 相当耗时。Transformers 通过采用注意力机制克服了这一限制,并且不需要我们按顺序提供词的输入。直观地说,注意力机制类似于卷积神经网络(CNNs)中基于卷积的方法,其中序列中更接近的词之间的交互在较低层建模,而序列中较远的词之间的交互在较高层建模。所有词同时被输入到网络中,同时包含它们相对和绝对位置的信息。
在这里我们省略了很多细节——要公正地讨论这个话题,需要整整一章的内容,但遗憾的是,这超出了本书的范围。关于学习更多关于 transformers 的内容,包括视频讲座和讲义,可以参考斯坦福大学提供的在线课程《深度学习与 NLP》(web.stanford.edu/class/cs224n/)。transformer 网络架构的发展包括双向编码器表示(BERT)和生成预训练 transformer(GPT)等系统。transformers 学习到的预训练词嵌入可以通过 Hugging Face 提供的流行开源库在 PyTorch 中加载(huggingface.co/transformers/))。在后续章节中,你将学习到的用于理解 GloVE 词嵌入学习的语义相似性的可解释技术,也可以扩展到由 transformer 网络学习的嵌入。这些技术是模型无关的。
7.4 解释语义相似性
在上一节中,你学习了如何获取使用神经词嵌入编码语义意义的密集表示的词的表示。现在我们将专注于理解和解释从这些学习到的词嵌入中得到的语义相似性。你将学习如何衡量语义相似性,以及如何将高维词嵌入在二维空间中可视化相似性。
在我们开始测量和解释语义相似度之前,第一步是确定一些意义多变且细微的词汇,我们对它们与其他可能相似的词汇之间的语义相似度有很好的理解。这与第六章网络分解框架中的概念定义步骤类似,在那里我们需要对我们要具体测量和解释的内容有良好的人类理解。在神经词嵌入的语义相似度背景下,我们需要对词汇的理解或分类法,以验证神经词嵌入是否正确地学习了语义意义。
我们将查看两组不同的词汇来解释语义相似度。第一组词汇不一定与电影评论或情感分类问题相关。然而,这些词汇旨在验证某些词汇细微差别是否被词嵌入所捕捉。第一组(称为组 1)的词汇如下:
-
篮球
-
勒布朗
-
罗纳尔多
-
Facebook
-
媒体
这些词汇之间的意义或联系可以从图 7.13 中所示的分类法中获得。在图中,该组中的词汇被突出显示。我们可以看到,在体育类别中,我们有篮球和足球/足球赛。体育类别也有个性——勒布朗和罗纳尔多属于体育和个性类别。体育个性和他们各自的运动之间也存在联系。例如,勒布朗与运动篮球相关联,而罗纳尔多与运动足球/足球赛相关联。此外,在媒体类别中,我们有不同类型的媒体,如电视、广播和互联网。在互联网类别中,有像Facebook和Google这样的公司。图 7.13 作为词汇之间如何关联的地图,我们可以使用它来解释词嵌入中的语义意义。

图 7.13 组 1 中词汇的分类
第二组(称为组 2)的词汇与电影评论相关。我们将查看以下电影集,以了解它们是如何相关的:
-
教父
-
好家伙
-
蝙蝠侠
-
复仇者联盟
第二组词汇的分类法如图 7.14 所示。

图 7.14 组 2 中词汇的分类
该组中的电影被突出显示。我们根据电影的类型和拍摄地点对电影进行了分类。像教父和好家伙这样的电影属于黑帮类型,它们都在纽约拍摄。像蝙蝠侠和复仇者联盟这样的电影是超级英雄电影。蝙蝠侠以哥谭为基地,这是一个基于纽约的虚构地点。值得注意的是,这些词汇的细微差别和意义是语言和上下文相关的,因此在我们开始解释语义意义之前,我们需要对此有良好的理解。
7.4.1 测量相似度
现在我们已经获得了感兴趣的单词,我们如何量化它们之间的相似度?我们特别感兴趣的是测量单词或词嵌入之间的表示相似度。为了便于可视化,让我们首先考虑一个大小为 2 的简单词嵌入示例。假设我们有两个单词,篮球和足球,在这个词嵌入空间中,如图 7.15 所示。这两个单词在图中分别表示为向量 W[1]和 W[2]。

图 7.15 展示了在 2-D 空间中测量词嵌入之间相似性的示意图
测量词向量 W[1]和 W[2]之间相似度的一种方法是在 2-D 嵌入空间中观察它们有多接近。相似度度量应该具有以下属性:如果词向量彼此接近,则它们更相似。如果它们相距更远,则它们不太相似。具有这种属性的良好度量是两个向量之间角度的余弦值—cos(θ)。这种测量称为余弦相似度。给定词向量 W[1]和 W[2]的余弦相似度的数学公式如下:

这实际上是两个向量欧几里得范数(或大小)的乘积除以词向量点积。
使用 gensim,我们可以轻松地获取与给定单词最相似的单词,如下所示。在第 7.3.3 节中,我们看到了如何使用 gensim 加载 GloVe 词嵌入。一旦嵌入被初始化,我们可以使用以下代码获取第一组单词的前五个最相似单词:
words = ['basketball', 'lebron', 'ronaldo', 'facebook', 'media'] ①
topn = 5 ②
sim_words_scores = [] ③
for word in words: ④
sim_words = model.most_similar(word, topn=topn) ⑤
print(f"Words similar to: {word}") ⑥
for sim_word in sim_words: ⑥
sim_words_scores.append((word, sim_word[0], sim_word[1])) ⑥
print(f"\t{sim_word[0]} ({sim_word[1]:.2f})") ⑥
① 初始化一个数组,包含第一组单词
② 我们对前五个最相似的单词感兴趣。
③ 初始化一个数组来存储最相似的单词
④ 遍历每个单词
⑤ 从 gensim 模型中获取前五个最相似单词
⑥ 将相似单词存储在数组中并打印结果
本代码的输出总结在表 7.2 中。表的最上面一行包含该集合中的单词。表中的每一列显示与该列最上面一行单词相似的五个单词。相似单词的余弦相似度度量也以括号形式显示。从表中我们可以看出,GloVe 词嵌入确实学习了在意义上语义相似的单词。例如,第一列显示了所有与篮球相似的单词,它们都是体育项目。第二列显示了所有与勒布朗相似的单词,它们都是打篮球的体育人物。第三列显示了所有踢足球或足球的体育人物。第四列显示了与Facebook相似的公司,即互联网或基于网络的社交媒体公司。最后一列显示了所有与媒体相似的单词。作为练习,对第二组单词进行类似的分析。解决方案可以在与本书相关的 GitHub 存储库中找到。
表 7.2 集合 1 中单词的前五个相似单词
| 篮球 | 勒布朗 | 罗纳尔多 | 媒体 | |
|---|---|---|---|---|
| 足球 (0.86) | 杜威恩 (0.79) | 罗纳尔迪尼奥 (0.86) | 推特 (0.92) | 新闻 (0.77) |
| 冰球 (0.8) | 沙奎尔 (0.75) | 里瓦尔多 (0.85) | MySpace (0.9) | 新闻 (0.75) |
| 足球 (0.8) | 波什 (0.72) | 贝克汉姆 (0.84) | YouTube (0.81) | 电视 (0.75) |
| NBA (0.78) | 奥尼尔 (0.68) | 克里斯蒂亚诺 (0.84) | 谷歌 (0.75) | 电视 (0.73) |
| 棒球 (0.76) | 卡梅隆 (0.68) | 罗比尼奥 (0.82) | 网络 (0.74) | 互联网 (0.72) |
现在我们也可视化第一组单词之间的余弦相似度。以下代码显示了如何计算单词对的余弦相似度以及如何可视化它们:
from sklearn.metrics.pairwise import cosine_similarity ①
import pandas as pd ②
import matplotlib.pyplot as plt ③
import seaborn as sns ③
words = ['basketball', 'lebron', 'ronaldo', 'facebook', 'media'] ④
word_pairs = [(a, b) for idx, a in enumerate(words) for b
➥ in words[idx + 1:]] ⑤
cosine_sim_word_pairs = [] ⑥
for word_pair in tqdm(word_pairs): ⑥
cos_sim = cosine_similarity([model[word_pair[0]]], ⑥
[model[word_pair[1]]])[0][0] ⑥
cosine_sim_word_pairs.append([str(word_pair), "glove", ⑥
➥ cos_sim]) ⑥
df_sim = pd.DataFrame(cosine_sim_word_pairs, ⑦
columns=['Word Pairs', ⑦
'Embedding', ⑦
'Cosine Similarity']) ⑦
f, ax = plt.subplots() ⑧
sns.barplot(x="Word Pairs", y="Cosine Similarity", ⑧
data=df_sim[df_sim['Embedding'] == 'glove'], ⑧
ax=ax) ⑧
plt.xticks(rotation=90); ⑧
① 导入 Pandas 以在 DataFrame 中存储单词对的余弦相似度
② 从 Scikit-Learn 导入余弦相似度辅助函数
③ 导入可视化相关的库
④ 初始化第一组单词
⑤ 根据初始化的单词集创建单词对数组
⑥ 计算单词对的余弦相似度并将其存储在数组中
⑦ 创建包含结果的 DataFrame
⑧ 使用 DataFrame 绘制条形图
结果图显示在图 7.16 中。我们可以从图中观察到,篮球和勒布朗之间的相似度比与其他任何单词都要高。此外,单词篮球与罗纳尔多的相似度比与Facebook和媒体的相似度更高,因为我们从图 7.13 中的分类法中知道篮球和罗纳尔多与体育类别相关联。使用分类法,我们还可以对其他单词对进行类似的观察。例如,单词Facebook与单词媒体的相似度比与其他任何单词都要高,因为 Facebook 是一家社交媒体公司。

图 7.16 集合 1 中 GloVe 嵌入的单词对余弦相似度可视化
作为练习,编写代码以可视化第二组电影对之间的余弦相似度。你可以从与本书相关的 GitHub 仓库中获取源代码,结果图如图 7.17 所示。我们可以从图中观察到,两部黑帮电影《教父》和《好家伙》彼此之间比它们与超级英雄电影《蝙蝠侠》和《复仇者联盟》更相似。同样,超级英雄电影彼此之间比它们与黑帮电影更接近。我们还可以看到,《教父》和《好家伙》与《蝙蝠侠》比与《复仇者联盟》更相似。这可能是因为电影基于相互连接的位置,正如我们在图 7.14 中的分类法中确立的那样。

图 7.17 展示了第二组中各种电影对 GloVe 嵌入的余弦相似度可视化
现在我们有了测量词嵌入之间相似性的方法,即使用余弦相似度度量。使用特定的单词集及其对应的分类法进行评估,我们还验证了 100 维的 GloVe 词嵌入能够很好地捕捉单词的语义意义。现在让我们看看如何将词嵌入可视化在 2 维空间中,类似于图 7.15 中所示,同时不失任何语义意义。这将是下一两个章节的重点。你将特别学习两种技术:主成分分析(PCA)和 t 分布随机邻域嵌入(t-SNE)。
7.4.2 主成分分析(PCA)
主成分分析(PCA)是一种强大的技术,用于降低数据集的维度。因为我们处理的是 100 维的词嵌入,我们希望将维度降低到 2,这样我们就可以轻松地可视化数据集。我们希望在降低维度的同时,尽可能多地捕捉到变化或语义信息。让我们通过一个简单的例子来看看 PCA 的实际应用。为了说明,我们将查看大小为 2 的词嵌入,并看看我们如何使用 PCA 将维度从 2 降低到 1。图 7.18 显示了四个词放置在二维平面上。为了便于可视化,我们假设嵌入大小为 2。目标是可视化词嵌入在一维上——在一条 1 维线上。我们可以看到,单词 1 和 2(“Doctor”和“Nurse”)在二维空间中彼此更接近,因此语义上相似。单词 3 和 4(“Athletics”和“Athlete”)在语义上也是相似的。然而,单词对 1 和 2 与单词对 3 和 4 相比更远,因为它们在语义上不相似。

图 7.18 在大小为 2 的嵌入空间中四个词的示意图
PCA 的第一步是取所有维度的词的平均值,并从词嵌入中减去平均值。这如图 7.19 所示,其中平均值由一个大十字表示。这种转换的目的是将词围绕平均值中心化,即把数据的平均值放在原点。通过在平均值上中心化词嵌入,我们仍然保留了词在 2-D 空间中的距离,因此也保留了它们的语义意义。

图 7.19 计算平均值并将词围绕平均值中心化的说明
因为我们对在一条线上可视化词嵌入感兴趣,PCA 的下一步就是通过词嵌入拟合一条线。最佳拟合线是使每个词与线之间的垂直距离最小化的线。换句话说,目标是使词与线之间的投影距离最小化或最大化原点到每个词在直线上的投影的距离。最大化原点到投影的距离将确保尽可能多地保留数据的变化。这如图 7.20 所示。最佳拟合线也称为主成分。我们只对在 1-D 中可视化词感兴趣,所以只有一个主成分。

图 7.20 主成分的说明
最后一步是将每个词投影到主成分上。这将作为我们在 1-D 中可视化词嵌入的方式,如图 7.21 所示。

图 7.21 将词嵌入投影到主成分的说明
现在我们已经对 PCA 的工作原理有了直观的了解,让我们将这项技术扩展到多个维度。让我们用矩阵X来表示所有的词嵌入,其中行数等于词汇表中的词的数量,列数等于嵌入大小。让我们用n来表示嵌入大小。目标是减少词的维度到大小k,在可视化的目的中通常是 2 或 3。
如通过可视化示例所见,第一步是将数据中心化到平均值。这由以下方程表示,其中平均值从嵌入矩阵X中减去。均值中心数据由矩阵U表示:
U = X – X̄
下一步是计算矩阵U的协方差。这通过下一个方程表示,其中协方差矩阵由矩阵V表示。计算矩阵U的协方差的目的在于估计均值中心化数据中每个嵌入维度的方差:
V = U^T**U
一旦你有了方差的估计,下一步就是通过解以下特征方程来计算矩阵V的特征值和特征向量。通过求解λ,我们可以得到方程的根,这将给我们特征值。注意,在下一个方程中,“det”代表行列式,而矩阵I是单位矩阵。一旦我们有了特征值,我们就可以获得相应的特征向量:
det(V – λI) = 0
特征向量基本上给我们主成分。特征值的幅度给我们一个估计,即每个主成分捕获的变异量。然后我们应该按特征值降序排序向量,并选择前k个主成分来投影我们的数据。前k个主成分将尽可能多地捕获数据中的变异。让我们用具有前k个主成分(或特征向量)的矩阵W来表示这个矩阵。最后一步是通过应用以下方程将原始单词嵌入从n-维空间投影到k-维空间:
Y = W^T**X
现在我们来看看 PCA 在 GloVe 单词嵌入上的实际应用。第一步是准备数据,其中我们提取了我们感兴趣可视化的单词的单词嵌入。这将在下一个代码片段中展示,其中我们提取了集合 1 中的单词及其对应的五个最相似单词的单词嵌入:
viz_words = [sim_word_score[1] for sim_word_score in ①
➥ sim_words_scores] ①
main_words = [sim_word_score[0] for sim_word_score in ①
➥ sim_words_scores] ①
word_vectors = [] ②
for word in tqdm(viz_words): ②
word_vectors.append(model[word]) ②
word_vectors = np.array(word_vectors) ②
① 创建包含主要单词和相似单词的列表以进行可视化
② 提取用于可视化的单词嵌入
一旦我们准备好了数据,我们就可以运行 PCA,并在低维空间中获取单词嵌入的投影。为了便于可视化,我们将主成分的数量设置为 2。我们可以使用 Scikit-Learn 库提供的 PCA 实现。以下代码展示了如何获取主成分并将数据投影到它们上:
from sklearn.decomposition import PCA ①
pca_2d = PCA(n_components=2, ②
random_state=24).fit(word_vectors) ③
pca_wv_2d = pca_2d.transform(word_vectors) ④
pca_kwv_2d = {} ⑤
for idx, word in enumerate(viz_words): ⑤
pca_kwv_2d[word] = pca_wv_2d[idx] ⑤
① 从 Scikit-Learn 导入 PCA 类
② 使用两个主成分初始化 PCA 类
③ 设置随机状态并获得单词向量的最佳拟合
④ 将单词向量投影到主成分上
⑤ 创建一个将每个单词映射到其 PCA 单词嵌入的字典
一旦我们在 2 维空间中有了单词嵌入的投影,我们就可以很容易地使用 Matplotlib 和 Seaborn 库进行可视化,如下所示:
df_pca_2d = pd.DataFrame(pca_wv_2d, columns=['y', 'x']) ①
df_pca_2d['text'] = viz_words ①
df_pca_2d['word'] = main_words ①
f, ax = plt.subplots(figsize=(10, 8)) ②
sns.scatterplot(data=df_pca_2d, ②
x="x", y="y", ②
hue="word", style="word", s=50, ax=ax) ②
ax.legend() ③
for i, row in df_pca_2d.iterrows(): ③
ax.text(row['x']+.05, row['y']-0.02, str(row['text']), ③
size=size) ③
① 创建一个 DataFrame,包含每个单词的 2 维 PCA 坐标
② 创建散点图
③ 为散点图添加图例和注释
结果图显示在图 7.22 中。集合 1 中的主要单词在图例中显示,它们最相似的五个单词使用对应于每个单词的符号进行说明。例如,“Basketball”用圆圈表示,而“Media”用菱形表示。让我们花点时间来欣赏 PCA 技术的输出。现在我们能够将原始的 100 维单词嵌入可视化到二维空间中!但是,PCA 表示是否仍然保留了在 100 维中捕获的语义意义呢?在图 7.22 中,我们可以看到与主要单词相似的单词聚集在一起,除了单词“lebron”。一些篮球人物如“bosh”、“dwyane”和“carmelo”与足球人物比与其篮球同伴更近。

图 7.22 使用 PCA 可视化集合 1 中 GloVe 单词嵌入的语义相似性
这是预料之中的,因为我们可能无法在仅仅两个维度中捕捉到原始数据集中的所有变化。我们可以通过运行以下代码行来轻松检查这一点:
print(pca_2d.explained_variance_ratio_)
此代码输出每个主成分中捕获的变化百分比。如果我们把它们加起来,我们得到大约 49%。这意味着通过将单词嵌入投影到仅两个主成分上,我们能够捕捉到数据中 49%的变化。作为一个练习,尝试用三个主成分训练 PCA,看看数据中的大部分方差是否可以被捕捉。如果是这样,尝试将嵌入可视化到三维空间中,看看二维中观察到的問題是否得到解决。
虽然 PCA 是一种强大的技术,但它确实存在一些主要的缺点。它假设数据集或单词嵌入可以线性建模。对于我们处理的大多数数据集来说,这可能并不成立。在下一节中,我们将学习一个更强大且更受欢迎的技术,称为 t-SNE,它可以推广到非线性结构。
7.4.3 t 分布随机邻域嵌入(t-SNE)
t-SNE 属于被称为流形学习的机器学习技术广泛类别,其目标是学习从高维数据中提取低维的非线性结构。这项技术是可视化高维数据中最受欢迎的选择之一。让我们通过一个简单的二维数据集来观察它的实际应用,该数据集的目标是将数据可视化到一维。在图 7.23 中,我们看到了左侧 2-D 空间中四个单词的熟悉示例。第一步是为所有单词对构建一个相似性表。这个相似性表将给我们一个相似度的度量,或者在高维嵌入空间中单词对成为邻居的概率。另一种看待它的方法是计算高维嵌入空间中单词的联合概率分布。我们将在稍后展示如何从数学上完成这个操作。

图 7.23 在高维空间中构建单词嵌入的相似性表
下一步是将所有单词随机放置在一条线上,因为我们感兴趣的是在 1-D 空间中可视化单词嵌入。这如图 7.24 左侧所示。一旦我们将单词随机放置在线上,我们应该为在该 1-D 空间中随机表示的单词构建一个相似性表。这如图 7.24 右侧所示。表中与高维联合概率分布不同的条目被突出显示。我们将在稍后看到如何数学地计算这个低维空间的联合概率分布。

图 7.24 随机放置单词在低维空间及其对应的相似性表
最后一步是 t-SNE 学习过程,如图 7.25 所示。我们必须将随机低维表示和更高维表示的联合概率分布输入到学习算法中。学习算法的目标是更新低维表示,使得两个概率分布相似。这将给我们一个低维可视化,它保留了来自高维空间中的概率分布或相似性。

图 7.25 t-SNE 学习算法
现在我们从数学的角度来分析。第一步是构建一个相似性表,或者说是联合概率分布,用于更高维嵌入空间中的单词。对于每个单词,我们可以投影一个以该单词为中心的高斯分布,使得离它更近的单词有更高的概率,而离它更远的单词有更低的概率。这在上面的方程中显示,该方程计算单词 x[j] 接近 x[i] 的概率。分子是以单词 x[i] 为中心的高斯分布,标准差为 σ。标准差 σ 是 t-SNE 的超参数,我们将在稍后看到如何设置这个超参数。分母是一个归一化因子,以确保不同密度的单词簇的概率范围相似:

使用这个方程,我们存在一个风险,即单词 x[j] 成为单词 x[i] 邻居的概率与单词 x[i] 成为 x[j] 邻居的概率不同,因为这两个条件概率来自不同的分布。为了确保相似度度量是可交换的,我们将计算两个单词 x[i] 和 x[j] 成为邻居的最终概率如下:

一旦你计算了高维嵌入空间的联合概率分布,下一步是将单词随机放置在低维空间中。然后,我们应该使用以下方程计算低维表示的联合概率分布。该方程本质上计算了两个单词在低维表示中作为 y[i]和 y[j]的概率是邻居的概率:

注意,低维表示使用了不同的分布。方程中的分子本质上是一个 t 分布,因此得名 t-SNE。图 7.26 显示了高斯分布和 t 分布之间的差异。我们可以看到,t 分布的右侧(极端值概率分数不可忽略)比高斯分布有更重的尾部。我们正在利用 t 分布的这一特性来确保在更高维度的空间中可能适度分散的点在低维空间中不会聚集在一起。
一旦我们有了高维和低维表示的联合分布,最后一步是训练一个算法来更新低维表示,使得两个分布相似。可以通过量化两个分布之间的差距来完成此优化。我们可以使用 Kullback–Leibler(KL)散度指标来完成此目的。

图 7.26 高斯分布与 t 分布的比较
KL 散度是衡量两个分布之间的熵或差异的度量。值越高,差异越大。更精确地说,KL 散度可以从相同分布的 0 到差异极大的分布的无限大。KL 散度指标可以按以下方式计算:

学习算法的目标是确定低维表示的分布,以使 KL 散度指标最小化。我们可以通过应用梯度下降和迭代更新低维表示来完成此优化。整个 t-SNE 算法已在 Scikit-Learn 库中实现。
在跳入代码之前,有一个细节我们略过了。注意,在计算更高维表示的联合概率分布时,我们对每个词拟合了一个以σ为标准差的 Gaussian 分布。这个标准差是 t-SNE 的一个重要超参数,被称为“困惑度”,它是每个词有多少个近邻的大致估计。正如我们稍后将会看到的,困惑度的选择将极大地改变词嵌入的可视化,因此它是一个重要的超参数。我们可以使用以下代码在 GloVE 词嵌入上训练 t-SNE。我们使用集合 1 中的单词及其相关的最相似的前五个单词:
from sklearn.manifold import TSNE ①
perplexity = 10 ②
learning_rate = 20 ②
iteration = 1000 ②
tsne_2d = TSNE(n_components=2, ③
random_state=24, ③
perplexity=perplexity, ③
learning_rate=learning_rate, ③
n_iter=iteration).fit(word_vectors) ③
tnse_wv_2d = tsne_2d.fit_transform(word_vectors) ④
tsne_kwv_2d = {} ⑤
for idx, word in enumerate(viz_words): ⑤
tsne_kwv_2d[word] = tnse_wv_2d[idx] ⑤
① 从 Scikit-Learn 导入 TSNE 类
② 初始化 t-SNE 超参数
③ 初始化 TSNE 类并使用词向量训练模型
④ 在 2-D 空间中获取 t-SNE 词嵌入
⑤ 将每个词映射到其 t-SNE 嵌入
注意,我们将困惑度设置为 10。我们可以重用前节中 PCA 的代码来可视化低维 t-SNE 嵌入。结果图如图 7.27 所示。

图 7.27 使用 t-SNE 可视化集合 1 中单词的 GloVe 词嵌入的语义相似性
图 7.27 所示的可视化在定性上看起来比 PCA 更好。我们确实看到篮球人物被聚集在一起,并且与足球人物集群明显不同。这仍然是一种定性评估,我们将在下一节中看到如何定量地验证这些可视化。
让我们看看当我们将困惑度设置为一个大值时会发生什么,比如说 100。作为一个练习,使用困惑度为 100 重新训练 t-SNE 模型,并可视化结果词嵌入。你可以在与本书相关的 GitHub 仓库中看到代码。结果图如图 7.28 所示。

图 7.28 高困惑度下的 t-SNE 可视化
我们可以看到单词以随机顺序聚集,所有的单词似乎都大致放置在一个圆圈中。t-SNE 算法的作者建议将困惑度设置为 5 到 50 之间。指导原则是对于密集的数据集,其中在更高维空间中有密集的单词集群,应使用更高的困惑度值。
7.4.4 验证语义相似性可视化
我们已经学习了两种可视化高维词嵌入的技术,即 PCA 和 t-SNE。我们对每种可视化进行了定性评估,但有没有一种方法可以定量验证它们呢?为了定量验证这些图,我们可以测量低维表示中词对之间的余弦相似度,并将其与高维表示进行比较。我们已经在 7.4.1 节中对此进行了操作(见图 7.16)。作为一个练习,将 7.4.1 节中的代码扩展到也可视化由 PCA 和两种 t-SNE 模型(困惑度=10 和困惑度=100)生成的嵌入。结果图如图 7.29 所示。您可以在与本书相关的 GitHub 存储库中查看解决方案。

图 7.29 验证语义相似性的可视化
我们可以看到,PCA 表示与原始 GloVe 表示不一致。例如,在 PCA 表示中,basketball和lebron的相似度低于basketball和facebook。然而,我们可以看到,困惑度为 10 的 t-SNE 学习的表示保留了原始 GloVe 嵌入捕获的大部分相似性。困惑度为 100 的 t-SNE 显示了所有具有相似意义的词对,并且在三种表示中显然是最差的。这种验证在规模上比定性评估 PCA 和 t-SNE 为所有感兴趣的词语生成的 2-D 可视化要容易得多。
摘要
-
在本章中,我们专注于自然语言处理(NLP)领域,特别是关于如何以捕捉语义意义的形式表示词语的话题。我们还学习了如何使用 PCA 和 t-SNE 等降维技术从这些词语表示中解释和可视化语义相似性。
-
表示词语的一种天真方式是使用独热编码。然而,这种表示是稀疏的,计算效率低下,并且不编码任何语义意义。
-
编码语义意义的词语的密集表示被称为词嵌入、词向量或分布式表示。由神经网络学习的表示或词嵌入被称为神经词嵌入。
-
可以使用连续词袋(CBOW)、跳字模型(skip-gram)和全局向量(GloVe)等神经网络架构来学习词语的密集表示。
-
在解释和可视化神经词嵌入中的语义相似性的背景下,我们需要一个词语的理解或分类法来验证神经词嵌入是否正确地学习了语义意义。
-
我们可以使用余弦相似度度量来衡量语义相似性。该度量具有一个属性,即彼此更接近的词嵌入比彼此更远的词嵌入具有更高的分数。
-
我们可以使用主成分分析(PCA)和 t 分布随机邻域嵌入(t-SNE)等降维技术,在低维空间中可视化高维词嵌入。
-
虽然主成分分析(PCA)是一种强大的技术,但它确实存在一个主要的缺点:它假设数据集或词嵌入可以线性建模。对于我们处理的大多数数据集来说,这可能并不成立。
-
t-SNE 属于机器学习技术中广泛分类的流形学习,其目标是学习从高维数据中提取低维的非线性结构。这项技术是可视化高维数据中最受欢迎的选择之一。
-
我们可以通过计算不同词对之间的余弦相似度,并检查相似度是否与原始高维表示一致,来定量验证 PCA 和 t-SNE 生成的可视化。
第四部分:公平性与偏差
干得好,你已经读到这本书的这一部分了!你现在工具箱中有各种可解释性技术,你应该已经准备好构建健壮的 AI 系统了!本部分最后将重点关注公平性与偏差,为可解释 AI 铺平道路。
在第八章中,你将学习关于公平性的各种定义以及如何检查你的模型是否存在偏差。你还将了解减轻偏差的技术以及使用数据表来标准化记录数据集的方法,这些方法有助于提高与 AI 系统的利益相关者和用户的透明度和问责制。
在第九章中,我们将通过讨论如何构建这样的系统来为可解释 AI 铺平道路,你还将学习如何使用反事实示例进行对比解释。
8 公平性与减轻偏差
本章涵盖
-
识别数据集中偏差的来源
-
使用各种公平性概念验证机器学习模型的公平性
-
将可解释性技术应用于识别机器学习模型中歧视的来源
-
使用预处理技术减轻偏差
-
使用数据表记录数据集以改善透明度和问责制,并确保符合法规
到目前为止,你已经学到了很多,并将许多可解释性技术添加到你的工具箱中,从你可以用来解释模型处理的技术(第二章至第五章)到解释机器学习模型学习到的表示的技术(第六章和第七章)。现在,我们将使用其中一些技术来解决构建由机器学习模型驱动的系统时遇到的一个重要问题,即解决偏差问题。这个问题有多个原因很重要。我们必须构建不歧视个人或系统用户的系统。如果企业使用 AI 进行决策,例如为用户提供机会或某些服务质量或信息,那么有偏见的决策可能会通过损害企业的声誉或对客户信任产生负面影响而给企业带来巨大的成本。某些地区,如美国和欧洲,有法律禁止基于受保护属性(如性别、种族、民族、性取向等)歧视个人。一些受监管的行业,如金融服务、教育、住房、就业、信贷和医疗保健,禁止或限制在决策中使用受保护属性,并且 AI 系统需要提供某些公平性保证。
在我们深入探讨偏差和公平性问题之前,让我们回顾一下我们用来构建一个解决常见问题(如图 8.1 所示的数据泄露、偏差、法规不合规和概念漂移)的健壮 AI 系统的过程。学习和测试阶段是在线完成的,都是基于历史标记数据训练模型、评估它以及使用各种可解释性技术来理解它。一旦模型部署,它就会上线并开始对实时数据进行预测。该模型也会被监控以确保没有概念漂移,这发生在生产环境中的数据分布与开发和测试环境中的数据分布不同时。还有一个反馈循环,其中新数据被添加到历史训练数据集中,以进行持续的训练、评估和部署。

图 8.1 回顾如何构建一个健壮的 AI 系统
这个系统中可能存在哪些偏差来源?一个来源,如图 8.2 所示,是历史训练数据集,其中可能在标签过程中或采样或数据收集过程中存在偏差。另一个来源是模型本身,算法可能更倾向于某些个人或群体而不是其他人。如果模型在本身存在偏差的数据集上训练,那么偏差会进一步放大。偏差的另一个来源是生产环境反馈到开发和测试环境中的反馈循环。如果初始数据集或模型存在偏差,那么在生产环境中部署的模型将继续做出有偏差的预测。如果基于这些预测的数据被反馈作为训练数据,那么这些偏差将进一步得到加强和放大。

图 8.2 AI 系统中的偏差来源
可解释性在偏差和公平性问题中如何定位?如图 8.2 所示,我们可以在训练和测试过程中使用可解释性技术来揭露历史数据集或模型的问题。我们已经在第三章中看到了这一点,在高中学生成绩预测问题中,使用部分依赖图(PDPs)揭露了种族偏差。一旦模型部署,我们可以使用可解释性技术来确保模型预测继续保持公平。
在本章中,我们将通过预测成人收入的另一个具体例子,更深入地探讨偏差和公平性的问题。我们将为各种公平性概念给出正式的定义,并使用它们来判断模型是否存在偏差。然后,我们将使用可解释性技术来衡量和揭露公平性问题。我们还将讨论减轻偏差的技术。最后,我们将探讨使用数据表来标准化记录数据集的方法,这将有助于提高 AI 系统利益相关者和用户的透明度和问责制。
8.1 成人收入预测
为了具体说明公平性问题,让我们来看一个具体的例子。你被人口普查局委托构建一个模型来预测美国成人的收入。预测问题如图 8.3 所示。

图 8.3 人口普查局的收入预测器
如图 8.3 所示,我们得到了用于收入预测的各种输入,例如教育水平、职业、年龄、性别、种族、资本收益等。我们的任务是构建一个收入预测器,它以矩形框的形式接受这些输入,并输出一个“是”或“否”的答案来回答“成人每年收入是否超过 50,000 美元?”这个问题。因此,这个问题可以表述为一个二元分类问题,因为我们感兴趣的是二元答案:是或否。我们将把答案“是”视为正标签,将答案“否”视为负标签。
我们得到了来自人口普查局的历史数据集,包含 30,940 名成年人。输入特征总结在表 8.1 中。从表中,我们可以看到连续和分类变量的混合。我们在这本书中处理的大多数数据集都由连续特征组成,其特征值是实数。我们已经在第三章中看到了如何处理分类特征。为了回顾,分类特征是值离散且有限的特征。我们需要将它们编码成数值,我们也在第三章中看到了如何使用标签编码器来完成这一操作。
表 8.1 收入预测输入特征
| 特征名称 | 描述 | 类型 | 是否为受保护属性? |
|---|---|---|---|
| age | 成年人年龄 | 连续 | 是 |
| workclass | 工作类别 | 分类 | 否 |
| fnlwgt | 人口普查局分配的最终权重 | 连续 | 否 |
| education | 教育水平 | 分类 | 否 |
| marital-status | 婚姻状况 | 分类 | 否 |
| occupation | 职业 | 分类 | 否 |
| gender | 男性或女性 | 分类 | 是 |
| race | 白人或黑人 | 分类 | 是 |
| capital-gain | 资本收益 | 连续 | 否 |
| capital-loss | 资本损失 | 连续 | 否 |
| hours-per-week | 每周工作小时数 | 连续 | 否 |
| native-country | 原籍国 | 分类 | 否 |
此外,表 8.1 还显示了给定特征是否为受保护属性。受保护属性是指根据许多国家广泛共享的立法,不能用于歧视个人的属性。例如,在美国,1964 年的民权法保护个人免受基于性别、种族、年龄、肤色、信仰、国籍、性取向和宗教等属性的歧视。在英国,具有相同属性的个人根据 2010 年的平等法受到保护免受歧视。
在这个数据集中,我们处理三个受保护属性:年龄、性别和种族。年龄是一个连续特征,而性别和种族是分类的。在本章中,我们将主要关注性别和种族,但我们将学习如何将公平概念和技术扩展到像年龄这样的连续受保护属性。至于性别和种族,在这个数据集中,我们处理两种性别,男性和女性,以及两种种族,白人和黑人。遗憾的是,我们无法包括更多性别或种族群体,因为它们在这个数据集中没有得到适当的代表。
最后,这个数据集的目标变量是二元的,其中 1 表示成年人每年收入超过 50,000 美元,0 表示年薪低于或等于 50,000 美元。现在让我们探索这个数据集,特别是关注整体薪资分布以及两个感兴趣的受保护属性:性别和种族的分布。
8.1.1 探索性数据分析
图 8.4 显示了美国人口普查局提供的数据集中 30,940 名成年人的薪资、性别和种族的整体分布。我们可以看到,数据集确实存在倾斜或偏见。大约 75%的人口收入低于或等于 50K 美元,其余人收入超过 50K 美元。在性别方面,男性成年人在这个数据集中比女性成年人更为代表,大约 65%的人口是男性。同样,在种族方面,我们也看到对白人成年人的偏见,大约 90%的成年人在数据集中是白人。请注意,您可以在与本书相关的 GitHub 存储库中找到用于探索性数据分析的源代码。

图 8.4 薪资、性别和种族分布
现在让我们来看看不同受保护性别和种族群体的薪资分布,以确定是否存在任何偏见。这如图 8.5 所示。如果我们看性别,我们可以看到,比女性成年人,男性成年人的比例更高,他们的收入超过 50K 美元。对于种族,我们也可以做出相同的观察,其中白人成年人的比例高于黑人成年人,他们的收入超过 50K 美元。

图 8.5 薪资与性别、薪资与种族的分布
最后,让我们来看看这个数据集中两种种族的性别代表性,如图 8.6 所示。我们可以看到,在黑人成年人中,男性和女性的比例相当,大约为 50%。另一方面,对于白人成年人来说,白人男性成年人的代表性比白人女性更高。这种分析有助于确定薪资偏见的主要原因。因为 70%的白人成年人是男性,所以白人成年人的薪资偏见可能更好地解释为性别保护群体,其中数据集中的男性用户可能比女性用户赚得更多。另一方面,对于黑人成年人来说,因为男性和女性的比例相当,所以黑人成年人的主要偏见来源可能是种族本身。

图 8.6 性别与种族分布
在我们继续构建模型之前,了解数据集中这些偏差的根本原因非常重要。我们不确定数据集是如何收集的,因此无法确定根本原因。然而,我们可以假设偏见来源可能是以下几种:
-
样本偏差,其中数据集未能正确代表真实人口。
-
标签偏见,其中可能在记录人口中各个群体的薪资信息的方式中存在偏见。
-
社会系统性偏见。如果存在系统性偏见,那么这种偏见将在数据集中得到反映。
如我们已在第三章中讨论过的,第一个问题可以通过收集更多代表人群的数据来解决。在本章中,我们还将学习如何使用数据表来正确记录数据收集过程,以提高透明度和问责制。这些数据表还可以用来确定数据集中偏差的根本原因。通过改进数据收集过程可以修复标签偏差。在本章中,我们还将学习另一种纠正标签偏差的技术。最后一个问题更难解决,需要更好的政策和法律,这超出了本书的范围。
8.1.2 预测模型
从我们的探索性分析中,我们发现数据集中存在一些偏差,不幸的是,其根本原因尚不清楚。为了衡量模型的公平性,我们现在将构建一个预测成人收入的模型。我们将为此目的使用随机森林模型。正如你在第三章中学到的,随机森林是一种结合决策树的方法,具体使用的是袋装技术。这个模型的示意图如图 8.7 所示。训练数据以表格或矩阵形式输入到随机森林模型中。请注意,分类特征被编码为数值。使用随机森林,我们可以在训练数据的独立随机子集上并行训练多个决策树。预测是通过这些单个决策树来进行的,并将它们全部结合起来得出最终预测。通常使用多数投票作为将单个决策树的预测组合成最终预测的方法。

图 8.7 成人收入预测的随机森林模型示意图
作为练习,编写代码以在成人收入数据集上训练随机森林模型。你可以参考第三章中的代码示例。请注意,你可以使用 Scikit-Learn 提供的LabelEncoder类将分类特征编码为数值。此外,你也可以尝试使用 Scikit-Learn 提供的RandomForestClassifier类来初始化和训练模型。你可以在与本书相关的 GitHub 仓库中找到这个练习的解决方案。
在本章剩余部分,我们将使用一个使用 10 个估计器或决策树训练的随机森林模型,每个决策树的最大深度为 20。该模型的表现总结在表 8.2 中。我们将考虑四个模型评估指标,即准确率、精确率、召回率和 F1 值。这些指标在第三章中已介绍,并在前几章中反复使用。我们还将考虑一个基线模型,该模型总是预测多数类为 0,即成人的收入总是小于或等于$50K。我们将随机森林模型的表现与这个基线进行比较。我们可以看到,在多个指标上,随机森林模型优于基线,实现了约 86%的准确率(比基线高 10%),约 85%的精确率(比基线高 27%),约 86%的召回率(比基线高 10%),以及约 85%的 F1 值(比基线高 19%)。
表 8.2 收入预测随机森林模型的表现
| 准确率 (%) | 精确率 (%) | 召回率 (%) | F1 (%) | |
|---|---|---|---|---|
| 基准 | 76.1 | 57.9 | 76.1 | 65.8 |
| 随机森林 | 85.8 | 85.3 | 85.8 | 85.4 |
让我们现在以几种方式解释随机森林模型。首先,让我们看看随机森林模型认为的输入特征的重要性。这将帮助我们理解一些受保护群体特征的重要性,如图 8.8 所示。您可以在与本书相关的 GitHub 存储库中查看用于生成此图的源代码。我们可以看到,年龄(一个受保护群体)是最重要的特征,其次是资本收益。然而,种族和性别似乎重要性较低。这可能是因为种族和性别被编码在其他一些特征中。我们可以通过查看特征之间的相关性来检查这一点。我们还可以使用局部相关图(PDPs),正如我们在第三章中看到的,来理解种族和性别可能如何与其他一些特征相互作用。

图 8.8 随机森林模型学习到的特征的重要性
接下来,我们可以使用 SHAP 技术来确定模型如何做出单个预测。正如我们在第四章中学到的,SHAP 是一种模型无关的局部可解释技术,它使用博弈论概念来量化特征对单个模型预测的影响。图 8.9 显示了年薪超过$50K 的成年人的 SHAP 解释。请注意,这个数据点并未用于训练。我们可以看到每个特征值如何将模型预测从基值推动到 0.73 分(即成年人有 73%的可能性年薪超过$50K)。对于这个实例,最重要的特征值按降序排列为资本收益、教育水平和每周工作时间。

图 8.9 对于年薪超过$50K 的单个预测的 SHAP 解释
我们将在第 8.3 节中再次讨论 SHAP 和依赖图在公平性背景下的应用。我们还将讨论如何使用本书中学到的其他可解释性技术,如网络分解和 t-SNE。但在那之前,让我们了解各种公平性的概念。
8.2 公平性概念
在上一节中,我们训练了一个随机森林模型来进行薪资预测。该模型的目标是为每个成年人确定一个二元结果:他们是否每年收入超过 50K 美元。但这些预测对于性别和种族等不同受保护群体是否公平呢?为了正式定义各种公平性的概念,让我们看看模型做出的预测以及为公平性所需的相关测量。图 8.10 展示了模型在二维平面上的预测示意图。随机森林模型将二维平面分为两部分,将正预测(在右侧)与负预测(在左侧)分开。20 位成年人的实际标签也投影到了这个二维平面上。请注意,实际标签在二维平面上的位置无关紧要。重要的是标签是否落在左侧(模型预测为负,即 0)或右侧(模型预测为正,即 1)。实际正标签以圆圈表示,实际负标签以三角形表示。

图 8.10 模型预测和与公平性概念相关的测量示意图
根据图 8.10 中的示意图,我们现在可以定义以下基本测量:
-
实际正标签—数据集中真实标签为正的数据点。在图 8.10 中,数据集中每年收入超过 50K 美元的成年人被表示为圆圈。如果我们计算圆圈的数量,实际正标签的数量等于 12。
-
实际负标签—数据集中真实标签为负的数据点。在图 8.10 中,数据集中每年收入低于或等于 50K 美元的成年人被表示为三角形。因此,实际负标签的数量为 8。
-
预测正标签—模型预测为正结果的数据点。在图 8.10 中,落在二维平面右侧的数据点具有正预测。该区域有 10 个数据点。因此,预测正标签的测量值为 10。
-
预测负标签—模型预测为负结果的数据点。在图 8.10 中,这些是落在二维平面左侧的点。预测负标签的测量值也是 10。
-
真阳性—在图 8.10 中,真阳性是落在 2 维平面右半部分的圆圈。它们实际上是模型预测为正,并且实际标签也是正的数据点。共有八个这样的圆圈,因此真阳性的数量是 8。我们也可以从混淆矩阵中获取这个信息,其中真阳性是模型预测为 1 且实际标签也是 1 的情况。
-
真阴性—另一方面,真阴性是落在 2 维平面左半部分的三角形。它们是模型预测为负,并且实际标签也是负的数据点。在图 8.10 中,我们可以看到真阴性的数量是 6。从混淆矩阵中,这些是模型预测为 0 且实际标签也是 0 的情况。
-
假阳性—在图 8.10 的平面右半部分,假阳性是三角形。它们是模型预测为正,但实际标签为负的数据点。从图中可以看出,假阳性的数量是 2。从混淆矩阵中,这些是模型预测为 1 但实际标签为 0 的情况。
-
假阴性—假阴性是落在 2 维平面左半部分的圆圈。它们实际上是模型预测为负,但实际标签为正的数据点。由于图 8.10 的左半部分有四个圆圈,因此假阴性的数量是 4。从混淆矩阵中,这些是模型预测为 0 但实际标签为 1 的情况。
在这些基本测量到位之后,我们现在定义各种公平性的概念。
8.2.1 人口统计学平等
我们将要考虑的第一个公平性概念被称为人口统计学平等。人口统计学平等的概念有时也被称为独立性、统计平等,在法律上称为不同影响。它主张对于模型来说,不同受保护群体的正预测率是平等的。让我们看看图 8.11 中的示例。在图中,20 个成年人——正如我们在图 8.10 中看到的——被分为两组,A 和 B,每组对应一个受保护的性别群体。A 组由男性成年人组成,在 2 维平面上有 10 个数据点。B 组由女性成年人组成,在 2 维平面上也有 10 个数据点。

图 8.11 两个受保护性别群体的人口统计学平等示意图
基于图 8.11 的插图,我们现在可以计算之前描述的基本测量值。对于男性成年人,有六个实际阳性,四个实际阴性,五个预测阳性,和五个预测阴性。对于女性成年人,我们可以看到实际阳性/阴性以及预测阳性/阴性与男性成年人相同。男性和女性成年人的阳性率是模型预测为阳性的每个群体中成年人的比例。从图 8.11 中我们可以看到,男性和女性用户的阳性率相同——都是 50%。因此,我们可以断言,这两组之间存在人口统计学平等。
让我们从实际的角度来看这个问题。假设模型预测被用来分配一种稀缺资源,比如说,住房贷款。再假设收入超过 5 万美元的成年人更有可能负担得起房子并偿还贷款。如果像住房贷款申请这样的决策是基于模型预测来做的,那么贷款将只发放给收入超过 5 万美元的成年人,那么人口统计学平等将确保贷款以相同的比率发放给男性和女性成年人。人口统计学平等断言,模型以相同的可能性预测男性和女性成年人的工资都超过 5 万美元。
现在我们更正式地定义人口统计学平等,并使用这个定义来检查我们的随机森林模型是否公平。让我们用ŷ表示模型预测,用z表示受保护群体变量。性别受保护群体变量z可以有两个可能的值:0 代表女性成年人,1 代表男性成年人。对于种族受保护群体,变量z也可以有两个可能的值:0 代表黑人成年人,1 代表白人成年人。人口统计学平等要求模型预测一个受保护群体为阳性的概率与模型预测另一个受保护群体为阳性的概率相似或相等。如果它们的比率在阈值τ[1]和τ[2]之间,则概率度量是相似的,这些阈值通常是 0.8 和 1.2。阈值设置为 0.8 和 1.2 是为了紧密遵循法律文献中关于不同影响的 80%规则,如下一个方程所示。如果比率是 1,则概率度量是相等的:

现在,我们如何使用这个定义来处理一个分类但具有多个值的受保护群体特征?在这个例子中,我们只考虑了两种种族:白色和黑色。如果数据集中有更多种族怎么办?请注意,个人可能是多种族的,他们可能认同多个种族。我们将把它们视为一个单独的种族,以确保不会对认同多个种族的个人产生歧视。在这种情况下,如果有超过两个种族,我们将为每个种族定义人口比例率指标,并采用一对一的策略,其中z = 0代表感兴趣的种族,z = 1代表所有其他种族。请注意,多种族的个人可能属于多个群体。然后我们需要确保与所有其他种族相比,每个种族的人口比例率相似。对于一个连续的受保护群体特征,比如年龄呢?在这种情况下,我们需要将连续特征划分为离散组,然后应用一对一策略。
现在定义已经就绪,让我们看看随机森林模型是否公平。以下代码片段使用人口比例概念评估模型:
male_indices_test = X_test[X_test['gender'] == 1].index.values ①
female_indices_test = X_test[X_test['gender'] == 0].index.values ②
white_indices_test = X_test[X_test['race'] == 1].index.values ③
black_indices_test = X_test[X_test['race'] == 0].index.values ④
y_score = adult_model.predict_proba(X_test) ⑤
y_score_male_test = y_score[male_indices_test, :] ⑥
y_score_female_test = y_score[female_indices_test, :] ⑥
y_score_white_test = y_score[white_indices_test, :] ⑦
y_score_black_test = y_score[black_indices_test, :] ⑦
dem_par_gender_ratio = np.mean(y_score_female_test
➥ [:, 1]) / np.mean(y_score_male_test[:, 1]) ⑧
dem_par_race_ratio = np.mean(y_score_black_test
➥ [:, 1]) / np.mean(y_score_white_test[:, 1]) ⑨
① 加载测试集中编码性别为 1 的男性成年人的索引
② 加载测试集中编码性别为 0 的女性成年人的索引
③ 加载测试集中编码种族为 1 的白色成年人的索引
④ 加载测试集中编码种族为 0 的黑色成年人的索引
⑤ 获取测试集中所有成年人的模型预测
⑥ 获取两个性别群体的模型预测
⑦ 获取两个种族群体的模型预测
⑧ 计算两个性别群体的人口比例率
⑨ 计算两个种族群体的人口比例率
注意,在这个代码片段中,我们使用的是标签编码的数据集和第 8.1.2 节中训练的模型。标签编码的输入特征存储在X_test数据框中,随机森林模型命名为adult_model。请注意,您可以从与本书相关的 GitHub 存储库中获取数据准备和模型训练的代码。我们计算人口比例率作为预测一个群体(女性/黑色成年人)的阳性类的平均概率分数与对应群体(男性/白色成年人)的比率。一旦我们计算了性别和种族群体的人口比例率,我们可以使用以下代码片段绘制该指标:
def plot_bar(values, labels, ax, color='b'): ①
bar_width = 0.35 ①
opacity = 0.9 ①
index = np.arange(len(values)) ①
ax.bar(index, values, bar_width, ①
alpha=opacity, ①
color=color) ①
ax.set_xticks(index) ①
ax.set_xticklabels(labels) ①
ax.grid(True); ①
threshold_1 = 0.8 ②
threshold_2 = 1.2 ②
f, ax = plt.subplots() ③
plot_bar([dem_par_gender_ratio, dem_par_race_ratio], ④
['Gender', 'Race'], ④
ax=ax, color='r') ④
ax.set_ylabel('Demographic Parity Ratio') ⑤
ax.set_ylim([0, 1.5]) ⑥
ax.plot([-0.5, 1.5], ⑦
[threshold_1, threshold_1], "k--", ⑦
linewidth=3.0) ⑦
ax.plot([-0.5, 1.5], ⑧
[threshold_2, threshold_2], "k--", ⑧
label='Threshold', ⑧
linewidth=3.0) ⑧
ax.legend(); ⑨
① 调用辅助函数 plot_bar 来绘制条形图
② 设置人口比例率阈值
③ 初始化一个 Matplotlib 图表
④ 绘制性别和种族的人口比例率条形图
⑤ 设置 y 轴的标签
⑥ 将 y 轴的值限制在-0.5 到 1.5 之间
⑦ 绘制阈值为 threshold_1 的水平线
⑧ 绘制阈值为 threshold_2 的水平线
显示图表的图例
结果图表显示在图 8.12 中。我们可以看到,性别和种族的人口比例比分别为 0.38 和 0.45。它们不在阈值范围内,因此,基于人口比例的随机森林模型对两个受保护群体来说都是不公平的。我们将在第 8.4 节中看到如何减轻偏差并训练一个公平的模型。

图 8.12 性别和种族的人口比例比
8.2.2 机会平等和概率
人口比例平等的概念对于我们需要确保所有受保护群体在人口中的处理平等的场景是有用的。它确保少数群体与多数群体以相同的方式被对待。在某些场景中,我们可能想要考虑所有受保护群体的实际标签的分布。例如,如果我们对就业机会感兴趣,一组个人可能比其他群体更感兴趣并且更有资格从事某些工作。在这种情况下,我们可能不希望确保平等,因为我们可能希望确保将就业机会给予那些对它更感兴趣并且更有资格的个人群体。在这种情况下,我们可以使用机会平等和概率公平的概念。
让我们回到用于人口比例的插图,以建立我们的直觉。在图 8.13 中,A 组(男性)和 B 组(女性)的 20 名成年人的分离与我们在图 8.11 中看到的人口比例相同。对于机会平等和概率,我们感兴趣的测量是考虑到每个受保护群体的实际标签的分布。这些测量在图 8.13 中作为真正例率和假正例率来计算。真正例率衡量的是实际正例被预测为正例的概率,其计算为真正例数与真正例数和假负例数之和的比率。换句话说,真正例率衡量的是模型正确识别的实际真例的百分比,也称为召回率。对于 A 组(男性),真正例率约为 66.7%,而对于 B 组(女性),真正例率为 50%。当组间真正例率存在平等时,我们说存在机会平等。因为图 8.13 中展示的玩具示例中真正例率不匹配,我们可以说不存在性别受保护群体的机会平等。

图 8.13 两个受保护性别群体机会平等和概率的示意图
概率均等扩展了机会均等定义,引入了另一个对称的测量指标,称为假阳性率。假阳性率衡量的是实际负事件被预测为正事件的可能性。它被计算为假阳性数量与假阳性数量和真阴性数量之和的比率。我们可以断言,当受保护群体之间在真阳性率和假阳性率上存在均衡时,概率均等存在。在图 8.13 中展示的玩具示例中,A 组和 B 组之间的真阳性率不存在均衡,因此我们无法说概率均等存在。此外,两组之间的假阳性率也不匹配。
我们可以使用以下方程更正式地定义机会均等和概率均等。第一个方程本质上计算了两组之间真阳性率的差异。第二个方程计算了两组之间假阳性率的差异。当差异等于或接近 0 时,存在均衡。这一概念与人口均衡概念不同,因为它考虑了实际标签的分布——对于真阳性率为正,对于假阳性率为负。此外,人口均衡概念比较概率时作为比率而不是加法,紧密遵循法律文献中的“80%规则”:
ℙ(ŷ = 1|z = 0|y = 1) – ℙ(ŷ = 1|z = 1|y = 1)
ℙ(ŷ = 1|z = 0|y = 0) – ℙ(ŷ = 1|z = 1|y = 0)
现在,让我们使用这个概念来看看我们的随机森林模型是否公平。我们可以使用接收者操作特征(ROC)曲线来比较真阳性率和假阳性率。ROC 曲线本质上是在真阳性率与假阳性率之间绘制曲线。对于机会均等和概率均等,我们可以使用曲线下面积(AUC)作为性能的汇总度量,以便轻松比较每个受保护群体的模型性能。我们可以查看组间 AUC 的差异,以了解模型的公平性。下面的代码片段展示了如何计算真/假阳性率和 AUC:
from sklearn.metrics import roc_curve, auc ①
def compute_roc_auc(y_test, y_score): ②
fpr = dict() ③
tpr = dict() ③
roc_auc = dict() ③
for i in [1]:
fpr[i], tpr[i], _ = roc_curve(y_test, ④
➥ y_score[:, i]) ④
roc_auc[i] = auc(fpr[i], tpr[i]) ④
return fpr, tpr, roc_auc ⑤
fpr_male, tpr_male, roc_auc_male = compute_roc_auc(y_male_test, ⑥
y_pred_proba_male_test) ⑥
fpr_female, tpr_female, roc_auc_female = compute_roc_auc(y_female_test, ⑦
d_proba_female_test) ⑦
fpr_white, tpr_white, roc_auc_white = compute_roc_auc(y_white_test, ⑧
y_pred_proba_white_tes ⑧
fpr_black, tpr_black, roc_auc_black = compute_roc_auc(y_black_test, ⑨
y_pred_proba_black_test)
① 从 Scikit-Learn 导入 roc_curve 和 auc 辅助函数。
② 定义一个辅助函数来计算每个受保护群体的 ROC 和 AUC。
③ 定义用于存储数据集中每个类别的真/假阳性率和 AUC 的字典。
④ 对于实际标签,计算真/假阳性率和 AUC,并将它们存储在字典中。
⑤ 将字典返回给函数的调用者。
⑥ 使用辅助函数计算成年男性的指标。
⑦ 使用辅助函数计算成年女性的指标。
⑧ 使用辅助函数计算成年白人的指标。
⑨ 使用辅助函数计算成年黑人的指标。
一旦为每个受保护群体计算了指标,我们可以使用以下代码片段来绘制 ROC 曲线:
lw = 1.5 ①
f, ax = plt.subplots(1, 2, figsize=(15, 5)) ②
ax[0].plot(fpr_male[1], tpr_male[1], ③
linestyle='-', color='b', ③
lw=lw, ③
label='Male (Area = %0.2f)' % roc_auc_male[1]) ③
ax[0].plot(fpr_female[1], tpr_female[1], ④
linestyle='--', color='g', ④
lw=lw, ④
label='Female (Area = %0.2f)' % roc_auc_female[1]) ④
ax[1].plot(fpr_white[1], tpr_white[1], ⑤
linestyle='-', color='c', ⑤
lw=lw, ⑤
label='White (Area = %0.2f)' % roc_auc_white[1]) ⑤
ax[1].plot(fpr_black[1], tpr_black[1], ⑥
linestyle='--', color='r', ⑥
lw=lw, ⑥
label='Black (Area = %0.2f)' % roc_auc_black[1]) ⑥
ax[0].legend() ⑦
ax[1].legend() ⑦
ax[0].set_ylabel('True Positive Rate') ⑦
ax[0].set_xlabel('False Positive Rate') ⑦
ax[1].set_ylabel('True Positive Rate') ⑦
ax[1].set_xlabel('False Positive Rate') ⑦
ax[0].set_title('ROC Curve (Gender)') ⑦
ax[1].set_title('ROC Curve (Race)') ⑦
① 设置线图的线宽
② 初始化一个由一排两列组成的 Matplotlib 图表
③ 在第一列中,绘制了男性成人的 ROC 曲线
④ 在第一列中,绘制了女性成人的 ROC 曲线
⑤ 在第二列中,绘制了白人成人的 ROC 曲线
⑥ 在第二列中,绘制了黑人成人的 ROC 曲线
⑦ 注释并标记了图表
结果图表显示在图 8.14 中。图表的第一列比较了两个性别组(男性和女性)的 ROC 曲线。第二列比较了两个种族组(白人和黑人)的 ROC 曲线。两个图表的曲线下面积在图例中显示。我们可以看到,男性成人的 AUC 为 0.89,女性成人的 AUC 为 0.92。差异大约有 3%偏向女性成人。另一方面,白人成人的 AUC 为 0.9,黑人成人的 AUC 为 0.92。差异大约有 2%偏向黑人成人。不幸的是,与人口平衡不同,法律或研究界没有关于考虑模型公平性时应使用哪些阈值的指南。在本章中,如果差异在统计上显著,我们将使用机会平等和赔率的概念将模型视为不公平。我们将在 8.3.1 节中通过置信区间查看这些差异是否显著。

图 8.14 性别和种族的接收器操作特征(ROC)曲线
8.2.3 其他公平性概念
最常用的公平性概念是人口平衡和机会平等/赔率。但是,为了提醒,让我们也看看以下其他公平性概念:
-
预测质量平等—不同组之间的预测质量没有差异。预测质量可以是模型的准确度或任何其他性能指标,如 F1。
-
治疗平等—模型对各组平等对待,其中假阳性率存在平衡。假阳性率被量化为假阴性数与假阳性数的比率。
-
通过无意识实现的公平性—可以通过不明确使用受保护属性作为预测特征来实现公平性。在一个理想的世界里,模型使用的其他特征与受保护属性不相关,但这几乎总是不成立。因此,通过无意识实现公平性不能保证。我们将在 8.4.1 节中看到这一点。
-
反事实公平性—如果一个模型在反事实世界中,如果该个体属于另一个受保护群体,它仍然做出相同的预测,那么该模型对该个体是公平的。
我们可以将所有公平性概念分为两类——组公平性和个体公平性。组公平性确保模型对不同受保护群体是公平的。对于成人收入数据集,受保护群体包括性别、种族和年龄。另一方面,个体公平性确保模型对相似个体做出相似的预测。对于成人收入数据集,个体可以根据其教育水平、原籍国或每周工作时间等因素相似,仅举几个例子。表 8.3 显示了不同公平性概念的所属类别。
表 8.3 组合和个体公平性概念
| 公平性概念 | 描述 | 类别 |
|---|---|---|
| 人口统计平等 | 不同受保护群体在正预测率上的平等 | 组 |
| 机会和赔率的平等 | 不同受保护群体在真阳性率和假阳性率上的平等 | 组 |
| 预测质量平等 | 不同受保护群体在预测质量上的平等 | 组 |
| 处理平等 | 不同受保护群体在错误预测率上的平等 | 组 |
| 通过无意识实现公平性 | 通过不明确将受保护属性作为预测特征来实现公平性 | 个体 |
| 反事实公平性 | 在反事实世界中,如果该个体属于另一个受保护群体,则对个体的预测相似 | 个体 |
8.3 可解释性和公平性
在本节中,我们将学习如何使用可解释性技术来检测模型导致的歧视来源。歧视的来源可以大致分为以下两组:
-
通过输入特征进行歧视——可以追溯到输入特征的公平性问题。
-
通过表示进行歧视——难以追溯到输入特征的公平性问题,尤其是对于处理图像和文本等输入的深度学习模型。在这种情况下,我们可以将歧视的来源追溯到模型学习到的深度表示。
8.3.1 通过输入特征进行歧视
让我们首先通过输入特征来观察歧视。当我们查看第 8.2 节中的各种公平概念时,我们通过处理模型输出看到,随机森林模型在人口统计学差异和机会平等/概率公平的公平性度量上是不公平的。我们如何通过追溯到输入来解释这些公平性度量?我们可以利用 SHAP 来完成这个目的。正如我们在第四章和第 8.1.2 节中看到的,SHAP 将模型输出分解为每个输入的 Shapley 值。这些 Shapley 值与模型输出具有相同的单位——如果我们对所有特征求和 Shapley 值,我们将得到一个与模型输出相匹配的值,该值衡量预测正结果的概率。我们在第 8.1.2 节中看到了这个说明。因为输入特征的 Shapley 值之和等于模型输出,所以我们可以将保护群体之间模型输出(以及,相应地,公平性度量)的差异归因于每个输入的 Shapley 值的差异。这就是如何将任何歧视或公平性问题追溯到输入的方法。
让我们通过代码来观察这一过程。以下代码片段定义了一个辅助函数,用于生成保护群体之间的 SHAP 差异,并可用于生成模型输出中由输入引起的差异的可视化:
def generate_shap_group_diff(df_X, ①
y, ②
shap_values, ③
notion='demographic_parity', ④
protected_group='gender', ⑤
trace_to_input=False): ⑥
if notion not in ['demographic_parity',
➥ 'equality_of_opportunity']: ⑦
return None ⑦
if protected_group not in ['gender', 'race']: ⑦
return None ⑦
if notion == 'demographic_parity': ⑧
flabel = 'Demographic parity difference' ⑧
if notion == 'equality_of_opportunity': ⑨
flabel = 'Equality of opportunity difference' ⑨
positive_label_indices = np.where(y == 1)[0] ⑨
df_X = df_X.iloc[np.where(y == 1)[0]] ⑨
shap_values = shap_values[np.where(y == 1)[0],:] ⑨
if protected_group == 'gender': ⑩
pg_label = 'men v/s women' ⑩
mask = df_X['gender'].values == 1 ⑩
if protected_group == 'race': ⑪
pg_label = 'white v/s black' ⑪
mask = df_X['race'].values == 1 ⑪
glabel = f"{flabel}\nof model output for {pg_label}" ⑫
xmin = -0.8 ⑬
xmax = 0.8 ⑬
if trace_to_input: ⑭
shap.group_difference_plot(shap_values, ⑭
mask, ⑭
df_X.columns, ⑭
xmin=xmin, ⑭
xmax=xmax, ⑭
xlabel=glabel, ⑭
show=False) ⑭
else: ⑮
shap.group_difference_plot(shap_values.sum(1), ⑮
mask, ⑮
xmin=xmin, ⑮
xmax=xmax, ⑮
xlabel=glabel, ⑮
show=False) ⑮
① 生成 SHAP 群体差异图的辅助函数,该函数接受六个输入。输入 1:特征值的 DataFrame
② 输入 2:目标值的向量
③ 输入 3:为输入特征生成的 SHAP 值
④ 输入 4:公平概念可以是人口统计学差异或机会平等/概率公平
⑤ 输入 5:可以是性别或种族的保护群体
⑥ 输入 6:标志,指示是否将歧视的来源追溯到输入
⑦ 对于不支持公平概念和保护群体,返回 None
⑧ 设置人口统计学差异概念的标签
⑨ 设置标签并仅处理机会平等概念的实际情况
⑩ 设置性别保护群体的标签和掩码
⑪ 设置种族保护群体的标签和掩码
⑫ 设置可视化标签
⑬ 限制可视化到 xmin 和 xmax
⑭ 当 trace_to_input 设置为 True 时创建可视化
⑮ 当 trace_to_input 设置为 False 时创建可视化
我们首先将使用辅助函数来检查模型输出中性别保护群体的人口统计学差异,如下代码示例所示。请注意,我们只关注测试集中的模型预测。shap_values变量包含数据集中所有输入和成人的 Shapley 值。我们是在第 8.1.2 节中生成的,你可以在与本书相关的 GitHub 仓库中找到源代码:
test_indices = X_test.index.values ①
generate_shap_group_diff(X_test, ②
y_test, ②
shap_values[1][test_indices,:], ②
notion='demographic_parity', ②
protected_group='gender', ②
trace_to_input=False) ②
① 提取测试集中输入的索引
② 调用辅助函数以适当的输入生成 SHAP 图
结果可视化如图 8.15 所示。请注意,差异可以是正的也可以是负的。如果差异为正,则模型偏向于男性成年人,如果差异为负,则模型偏向于女性成年人。从图 8.15 中我们可以看到,随机森林模型在预测正例(即薪水>50K)时对男性成年人的预测更多,因此偏向于男性成年人。

图 8.15 模型输出中性别的人口统计学差异
为了确定导致男性和女性成年人之间人口统计学差异的原因,我们可以使用以下代码片段将其追溯到输入特征:
generate_shap_group_diff(X_test, ①
y_test, ①
shap_values[1][test_indices,:], ①
notion='demographic_parity', ①
protected_group='gender', ①
trace_to_input=True) ①
① 使用与之前相同的输入调用辅助函数,但将trace_to_input设置为 True
结果图如图 8.16 所示。我们可以看到,偏差主要来自三个特征:关系、性别和婚姻状况。通过确定导致模型违反人口统计学公平性概念的特性,我们可以更仔细地查看数据,了解这些特征的偏差原因,正如我们在第 8.1.1 节中讨论的那样。

图 8.16 追踪到输入的性别人口统计学差异
作为练习,使用辅助函数来确定机会公平性度量中的差异是否存在,并将其追溯到输入。您可以将notion输入参数设置为equality_of_opportunity,这样函数将仅查看模型输出和 Shapley 值之间的差异,针对数据集中的实际正例。
图 8.17 显示了模型输出的结果可视化。我们可以看到,男性和女性成年人在真正例率之间的差异在统计上具有显著性,当模型在预测正例时偏向于男性成年人。因此,我们可以说,该模型在机会公平性概念下是不公平的。您可以通过将trace_to_input参数设置为 True 来追踪偏差到输入。

图 8.17 性别的机会公平性差异
8.3.2 通过表示进行歧视
在某些情况下,很难将歧视问题或公平性度量中的差异追溯到输入。例如,如果输入是图像或文本,那么将公平性度量中的差异追溯到像素值或词表示中的值将很困难。在这种情况下,一个更好的选择是检测模型学习到的表示中的任何偏差。让我们看看一个简单的例子,其目标是训练一个模型来检测图像中是否包含医生。假设我们已经训练了一个卷积神经网络(CNN),该网络预测图像中是否包含医生。如果我们想检查这个模型是否对任何受保护的群体(如性别)存在偏见,我们可以利用我们在第六章中学到的网络剖析框架来确定模型是否学习到了任何与受保护属性相关的特定概念。高级过程如图 8.18 所示。

图 8.18 使用网络剖析框架检查学习到的表示中的偏差的高级说明
在图 8.18 中,我们关注性别受保护属性。第一步是定义一个性别特定概念的字典。图中显示了一个例子,其中图像在像素级别上被标记为各种性别概念,如男性、女性和非二元。下一步是探测预训练网络,然后量化 CNN 中每个单元和层与性别特定概念的匹配度。一旦我们量化了匹配度,我们就可以检查每个性别概念有多少独特的检测器。如果某个性别有更多的独特检测器,那么我们可以说模型似乎学习到了一个对该性别有偏见的表示。
现在我们来看一个例子,其中模型的输入是文本形式。我们在第七章学习了如何得出密集和分布式的词表示,这些表示传达语义意义。我们如何检查模型学习到的表示是否对受保护的群体存在偏见?如果我们看医生这个例子,单词doctor是中性的还是对任何性别有偏见?我们可以通过使用我们在第七章学习的 t-distributed stochastic neighbor embedding(t-SNE)技术来检查这一点。然而,我们首先需要制定一个单词分类法,以便我们知道哪些单词是中性的,哪些单词与特定性别相关,如male或female。一旦我们有了分类法,我们就可以使用 t-SNE 来可视化单词doctor在语料库中与其他单词的接近程度。如果单词doctor与其他中性的单词(如hospital或healthcare)更接近,那么模型为doctor学习到的表示就没有偏见。另一方面,如果单词doctor与性别特定的单词(如man或woman)更接近,那么表示就是有偏见的。
8.4 缓解偏差
我们有以下三种广泛的方法来减轻模型中的偏差:
-
预处理—我们在训练模型之前应用这些方法,目的是减轻训练数据集中的偏差。
-
内处理—我们在模型训练期间应用这些方法。公平概念被明确或隐含地纳入学习算法中,使得模型不仅优化性能(如准确率),还优化公平性。
-
后处理—我们在模型训练后对模型的预测应用这些方法。模型预测经过校准,以确保满足公平约束。
在本节中,我们将关注两种预处理方法。
8.4.1 无意识公平
一种常见的预处理方法是移除模型中的任何受保护特征。在某些受监管的行业,如住房、就业和信贷,法律禁止将任何受保护特征作为决策模型输入。对于我们为成人收入预测训练的随机森林模型,让我们尝试移除两个感兴趣的受保护特征——性别和种族——并查看模型是否使用机会均等/概率均等概念公平。作为一个练习,从随机森林模型中移除标签编码的性别和种族特征,并使用之前相同的超参数重新训练模型。查看与此书相关的 GitHub 仓库中的解决方案。
重新训练的模型在 ROC 曲线上的性能展示在图 8.19 中。正如我们在 8.2.2 节中看到的,ROC 曲线被用来绘制真正例率与假正例率的关系,我们可以使用从该 ROC 曲线获得的 AUC 来检查模型在机会均等和概率均等概念下是否公平。对于之前使用性别和种族作为输入特征的随机森林模型,性别组之间的 AUC 差异为 3%,种族组之间的差异为 2%。通过使用无意识公平,种族组之间的差异减少到 1%,但性别组之间的差异没有变化。因此,无意识公平并不提供任何公平保证。其他特征可能与这些受保护群体高度相关,并可能作为性别和种族的代理。此外,我们还观察到所有组的模型性能都有所下降,与之前的随机森林模型相比,AUC 有所降低。正如我们之前提到的,某些受监管行业要求我们依法使用无意识公平。即使模型不能保证公平,法律也要求这些行业在模型中不使用任何受保护特征。

图 8.19 性别和种族的 ROC 曲线:无意识公平
8.4.2 通过重新加权纠正标签偏差
Heinrich Jiang 和 Ofir Nachum 在 2019 年提出了一种另一种预处理技术,该技术提供了公平性保证。在作者发布的、可在arxiv.org/abs/1901.04966找到的研究论文中,他们提供了可能出现在训练数据集中的偏差的数学公式。他们假设偏差可能出现在数据集中的观察标签中(也称为标签偏差),并且可以通过迭代重新加权训练数据集中的数据点来纠正,而不改变观察到的标签。他们为各种公平性概念提供了该算法的理论保证,如人口统计的平等性和机会/概率的平等。您可以参考原始论文以了解更多关于数学公式和证明的信息。在本节中,我们将概述该算法,并使用作者在mng.bz/Ygjj提供的实现。
通过重新加权纠正偏差的算法依赖于一个关键假设。数据集中观察到的标签基于一个未知的真实且无偏的标签集,这个标签集是未知的。观察到的数据集由于标签者或引入偏差的过程而存在偏差。关键假设是这种偏差的来源是无意的,并且可能由于无意识或固有的偏差。基于这个假设,论文的作者从数学上证明了通过重新加权观察到的、有偏差的数据集中的特征,可以构建一个无偏分类器,该分类器可能是在未知的、无偏的数据集上通过重新加权训练的。这如图 8.20 所示。

图 8.20 重新加权方法中标签偏差来源的潜在假设
标签偏差重新加权算法是迭代的,总结如图 8.21 所示。假设数据集中有K个受保护组,N个特征。对于成人收入数据集,考虑的受保护组数量是四个(两个性别组和两个种族组)。数据集包含 14 个特征。在运行算法之前,我们需要为每个受保护组初始化系数,其值为 0。我们还需要为每个特征初始化权重,其值为 1。
系数和权重初始化完成后,下一步是使用这些权重训练一个模型。由于本章我们考虑的是随机森林模型,因此这一步训练的模型将与第 8.1.2 节中训练的模型相同。下一步是为每个 K 个受保护群体计算该模型的公平性违规。公平性违规取决于我们感兴趣的具体概念。如果公平性概念是人口平衡,那么受保护群体的公平性违规是模型的整体平均正面率与该特定受保护群体的平均正面率之间的差异。如果公平性概念是机会平等,那么我们需要考虑整体平均真正阳性率和特定受保护群体的平均真正阳性率之间的差异。一旦我们计算了公平性违规,下一步是更新每个受保护群体的系数。算法的目标是最小化公平性违规,因此我们通过从其中减去公平性违规来更新系数。最后一步是使用受保护群体的系数更新每个特征的权重。更新权重的公式如图 8.21 所示,您可以在该算法作者发表的原论文中找到该公式的推导过程。本段中的步骤随后会重复 T 次,其中 T 是一个超参数,表示我们希望算法运行的迭代次数。

图 8.21 标签偏差重加权算法
现在,让我们将此算法应用于之前训练的成人收入数据集。但在运行算法之前,我们首先需要使用以下代码片段准备数据。思路是将标签编码的性别和种族特征转换为独热编码特征,其中有一个列对应于四个受保护群体(男性、女性、白人和黑人成年人)中的一个,并使用 0 或 1 的值来表示成年人是否属于该特定受保护群体:
from functools import partial ①
def prepare_data_for_label_bias(df_X, protected_features, ②
protected_encoded_map): ②
df_X_copy = df_X.copy(deep=True) ③
def map_feature(row, feature_name, feature_encoded): ④
if row[feature_name] == feature_encoded: ④
return 1 ④
return 0 ④
colname_func_map = {} ⑤
for feature_name in protected_features: ⑤
protected_encoded_fv = protected_encoded_map ⑤
➥ [feature_name] ⑤
for feature_value in protected_encoded_fv: ⑤
colname = f"{feature_name}_{feature_value}" ⑤
colname_func_map[colname] = partial ⑤
➥ (map_feature, ⑤
feature_name=feature_name, ⑤
feature_encoded=protected_encoded_fv[feature_value]) ⑤
for colname in colname_func_map: ⑤
df_X_copy[colname] = df_X_copy.apply ⑤
➥ (colname_func_map[colname], ⑤
axis=1) ⑤
df_X_copy = df_X_copy.drop(columns=protected_features) ⑥
return df_X_copy ⑦
① 导入 Python functool 库提供的部分函数
② 为标签偏差重加权算法准备数据集的辅助函数
③ 创建原始 DataFrame 的副本并对副本进行修改
④ 将每个特征映射到其编码值的辅助函数
⑤ 遍历所有受保护特征,并为每个群体创建具有相应二进制编码值的单独列
⑥ 从 DataFrame 的副本中删除原始受保护特征列
⑦ 返回带有新列的 DataFrame 副本
然后,您可以使用此辅助函数如下准备数据集。请注意,在调用辅助函数之前,为每个受保护群体创建了一个映射到其对应标签编码值的映射:
protected_features = ['gender', 'race'] ①
protected_encoded_map = { ②
'gender': { ②
'male': 1, ②
'female': 0 ②
}, ②
'race': { ②
'white': 1, ②
'black': 0 ②
} ②
} ②
df_X_lb = prepare_data_for_label_bias(df_X, ③
protected_features, ③
protected_encoded_map) ③
X_train_lb = df_X_lb.iloc[X_train.index] ④
X_test_lb = df_X_lb.iloc[X_test.index] ④
PROTECTED_GROUPS = ['gender_male', 'gender_female', 'race_white', 'race_black'] ⑤
protected_train = [np.array(X_train_lb[g]) for g ⑤
➥ in PROTECTED_GROUPS] ⑤
protected_test = [np.array(X_test_lb[g]) for g ⑤
➥ in PROTECTED_GROUPS] ⑤
① 需要处理的受保护特征列表
② 将每个受保护群体映射到其相应的标签编码值
③ 调用辅助函数以准备数据集用于标签偏差重加权算法
④ 将新的数据集分为训练集和测试集
⑤ 提取受保护群体的列
一旦你准备好了数据集,你就可以轻松地将它插入到标签偏差重加权算法中。你可以在论文作者发布的 GitHub 仓库(mng.bz/Ygjj)中找到这个算法的源代码。为了节省空间,我们不会在本节中重复该代码。作为一个练习,运行该算法并确定训练数据集中每个数据点的权重。一旦确定了权重,你可以使用以下代码片段重新训练无偏的随机森林模型:
model_lb = create_random_forest_model(10, max_depth=20) ①
model_lb.fit(X_train_lb, ②
y_train, ②
weights) ②
① 使用第三章中学习的辅助函数创建随机森林模型
② 调用 fit 方法并传入准备好的数据集和标签偏差重加权算法获得的权重
重新训练的模型在 ROC 曲线上的性能如图 8.22 所示。我们可以看到,性别组和种族组之间的 AUC 差异都是 1%。因此,这个模型在机会平等和概率方面比之前训练的以性别和种族为特征的随机森林模型以及没有这些特征的模型更加公平。

图 8.22 纠正标签偏差后的性别和种族 ROC 曲线
8.5 数据集的数据表
在第 8.1.1 节探索成人收入数据集时,我们注意到一些受保护群体(如女性和黑人成年人)没有得到适当的代表,这些群体的标签存在偏见。我们确定了几个偏见来源,即抽样偏差和标签偏差,但我们无法确定偏差的根本原因。主要原因在于这个数据集的数据收集过程是未知的。在 2020 年,谷歌和微软的研究员 Timnit Gebru 和其他研究人员发表的一篇论文中,提出了一种标准化的流程来记录数据集。这个想法是让数据创建者制定一份数据表,回答关于数据集动机、组成、数据收集过程和用途的关键问题。以下是一些关键问题的概述,但更深入的研究可以在原始研究论文arxiv.org/pdf/1803.09010.pdf中找到:
-
动机
-
数据集创建的目的是什么?这个问题的目的是了解数据集是否针对特定任务或解决特定差距或需求。
-
谁创建了数据集?目标是确定数据集的所有者,这可能是一个个人、团队、公司、组织或机构。
-
谁资助了数据集的创建?目标是了解数据集是否与研究拨款或其他资金来源相关联。
-
-
组成
-
数据集代表什么?目标是了解数据是否代表文档、照片、视频、人物、国家或其他任何表示。
-
数据集中有多少个示例?这个问题是显而易见的,旨在了解数据集的大小,即数据点或示例的数量。
-
数据集是否包含所有可能的示例,或者它是一个更大数据集或总体的样本?目标是了解数据集是否是从更大数据集或总体中抽取的样本。这将帮助我们检查是否存在任何抽样偏差。
-
数据集是否已标记?目标是检查数据集是原始的还是标记的。
-
数据集是否依赖于外部来源?目标是确定数据集是否存在任何外部来源或依赖,例如网站、Twitter 上的推文或任何其他数据集。
-
-
收集过程
-
数据是如何获取的?这个问题有助于我们了解数据收集过程。
-
如果适用,使用了什么抽样策略?这是组成部分中抽样问题的扩展,帮助我们检查是否存在任何抽样偏差。
-
数据是在什么时间段内收集的?
-
从个人直接收集数据还是通过第三方收集的?
-
如果数据与人物相关,是否已获得他们的数据收集同意?如果数据集与人物相关,我们与人类学等领域的专家合作非常重要。这个问题的答案也是确定数据集是否符合欧盟(EU)的通用数据保护条例(GDPR)等规定的重要部分。
-
是否存在个人在未来撤销同意的机制?这也是确定数据集是否符合规定的重要部分。
-
-
使用
-
数据集将用于什么?目标是确定数据集的所有可能任务或用途。
-
是否不应将数据集用于任何任务?这个问题的答案将帮助我们确保数据集不会被用于其未打算用于的任务。
-
数据集的数据表已经被研究和工业界采用。一些例子包括用于问答的 QuAC 数据集(quac.ai/datasheet.pdf)、包含烹饪食谱的 RecipeQA 数据集(mng.bz/GGnA)和 Open Images 数据集(github.com/amukka/openimages)。尽管数据集的数据表为数据集创建者增加了额外的开销,但它们提高了透明度和问责制,帮助我们确定是否存在任何偏差的来源,并确保我们遵守欧盟的 GDPR 等规定。
摘要
-
数据集中可能存在各种偏差来源,例如采样偏差和标签偏差。采样偏差发生在数据集未能正确代表真实人口时。标签偏差发生在对人口中不同群体的标签记录方式存在偏差时。
-
不同的公平性概念包括人口统计学上的平等、机会和概率的平等、预测质量的平等、通过无意识实现的公平以及反事实公平。常用的公平性概念包括人口统计学上的平等和机会及概率的平等。
-
人口统计学上的平等有时也称为独立性或统计平等,在法律上被称为不同影响。它断言模型在不同受保护群体的积极预测率中包含相等性。人口统计学上的平等概念对于我们想要确保所有受保护群体在人口中的普遍性不受影响时保持平等的场景是有用的。它确保少数群体得到与多数群体相同的待遇。
-
对于我们想要考虑所有受保护群体实际标签分布的场景,我们可以使用机会和概率的公平性概念。我们说存在机会平等,当组间真实阳性率存在相等性时。机会平等将机会平等的定义扩展到另一个对称的测量,即假阳性率。
-
我们可以将所有公平性概念分为两组:群体公平性和个体公平性。群体公平性确保模型对不同受保护群体是公平的。另一方面,个体公平性确保模型对相似个体做出相似的预测。
-
我们可以使用本书中学到的可解释性技术来检测模型导致的歧视来源。歧视的来源可以大致分为两种类型:通过输入特征的歧视和通过表示的歧视。
-
通过输入追踪将歧视或公平性问题追溯到输入特征。我们可以使用 SHAP 可解释性技术将公平性问题追溯到输入。
-
这些类型的公平性问题难以追溯到输入特征,特别是对于处理图像和文本等输入的深度学习模型。在这种情况下,我们可以将歧视的来源追溯到模型学习到的深度表示。我们可以使用网络分解框架和第六章和第七章中学习的 t-SNE 技术来分别追踪歧视的来源。
-
我们可以使用两种技术来减轻偏差:通过无意识实现的公平和用于纠正标签偏差的重加权技术。通过无意识实现的公平并不保证公平,但重加权技术确实提供了公平保证。
-
存在一个标准化的流程,用于使用数据表来记录数据集。数据表旨在回答关于数据集动机、组成、数据收集过程以及使用的关键问题。尽管为数据集创建数据表会给数据集创建者带来额外的开销,但它们提高了透明度和问责制,帮助我们确定是否存在任何偏见来源,并确保我们符合欧盟的 GDPR 等法规。
9 可解释人工智能之路
本章涵盖
-
总结本书中学到的可解释性技术
-
理解可解释人工智能系统的特性
-
常见的问题以及对可解释人工智能系统应用可解释性技术来回答这些问题
-
使用反事实示例来提出对比性解释
我们现在正接近通过可解释人工智能世界的旅程的终点。图 9.1 提供了这次旅程的地图。让我们花点时间来反思和总结我们所学的。可解释性全部关于理解人工智能系统中的因果关系。这是我们在给定输入的情况下,能够持续估计人工智能系统中的底层模型将预测什么,理解模型是如何得出预测的,理解预测如何随着输入或算法参数的修改而变化,以及最终理解模型何时犯错的程度。由于机器学习模型在金融、医疗保健、技术和法律等各个行业中日益增多,可解释性正变得越来越重要。这些模型所做的决策需要透明度和公平性。本书中学到的技术是提高透明度和确保公平性的强大工具。
在本书中,我们研究了两种广泛的机器学习模型类别——白盒模型和黑盒模型,它们位于可解释性和预测能力的光谱上。白盒模型本质上是透明的,易于解释。然而,它们的预测能力较低到中等。我们特别关注线性回归、逻辑回归、决策树和广义加性模型(GAMs),并通过理解模型的内部结构来学习如何解释它们。黑盒模型本质上是不可透见的,难以解释,但提供了更高的预测能力。在本书中,我们主要关注解释黑盒模型,如树集成和神经网络。

图 9.1 通过可解释人工智能世界的旅程地图
我们有两种解释黑盒模型的方法。一种是对模型处理过程进行解释——即理解模型如何处理输入并得出最终预测。另一种方法是解释模型表示,这仅适用于深度神经网络。为了解释模型处理过程,我们学习了诸如局部依赖性图(PDPs)和特征交互图等后验模型无关方法,以理解输入特征对模型预测的全球影响。我们还学习了局部范围内的后验模型无关方法,例如局部可解释模型无关解释(LIME)、SHapley 加性解释(SHAP)和锚点。我们可以使用这些方法来解释模型如何得出个别预测。我们还使用了视觉归因方法,如显著性图,以理解对视觉任务使用的神经网络中哪些输入特征或图像像素是重要的。为了解释模型表示,我们学习了如何剖析神经网络并理解网络中中间或隐藏层学习的数据表示。我们还学习了如何使用主成分分析(PCA)和 t 分布随机邻域嵌入(t-SNE)等技术来可视化模型学习的高维表示。
我们最终聚焦于公平性的话题,学习了各种公平性概念以及如何利用可解释性技术来衡量公平性。我们还学习了如何通过各种预处理技术来缓解公平性问题,例如通过无意识实现的公平性和迭代标签偏差校正技术。
在这本书中,我们明确区分了可解释性和可解释性。可解释性主要关于回答“如何”的问题——模型是如何工作的,以及它是如何得出预测的?可解释性超越了可解释性,因为它帮助我们回答“为什么”的问题——为什么模型做出了一个预测而不是另一个?可解释性主要是由构建、部署或使用 AI 系统的专家所识别的,这些技术是帮助你达到可解释性的基石。我们将专注于本章中可解释人工智能的路径。
9.1 可解释人工智能
让我们来看一个可解释人工智能系统的具体例子以及对其的期望。我们将使用第八章中预测美国成年人收入的相同例子。给定一组输入特征,如教育、职业、年龄、性别和种族,假设我们已经训练了一个模型,该模型预测成年人每年是否收入超过 50,000 美元。在应用本书中学到的可解释性技术后,假设我们现在可以将此模型作为服务部署。此服务可以被公众使用,根据他们的特征作为输入来确定他们可以赚多少钱。一个可解释的人工智能系统应该为系统用户提供质疑模型做出的预测和挑战因这些预测而做出的决策的功能。这如图 9.2 所示,其中向用户提供解释的功能已内置到解释代理中。用户可以向代理提出关于模型做出的预测的各种问题,代理有责任提供有意义的答案。如图 9.2 所示,用户可能提出的一个问题就是为什么模型预测他们的薪水会低于 50K。

图 9.2 一个代理向系统用户解释模型预测的示意图
用户在图 9.2 中提出的问题,为了说明目的,关注的是理解各种特征值如何影响模型预测。这只是可能向系统提出的问题之一。表 9.1 展示了我们可以向系统提出的一些广泛类别的问题以及我们在本书中学到的可用于此类问题的技术。如表所示,我们已准备好回答关于模型如何工作、哪些特征很重要、模型如何对特定案例做出预测以及模型是否公平无偏的问题。正如之前所强调的,我们并不擅长回答“为什么”的问题,将在本章简要涉及这一点。
表 9.1 问题类型和解释方法
| 方法类别 | 问题类型 | 解释方法 |
|---|---|---|
| 解释模型 |
-
模型是如何工作的?
-
哪些特征或输入对模型来说是最重要的?
|
-
模型依赖描述。(本书提供了关于各种广泛类别的模型(包括白盒和黑盒)如何工作的良好描述。)
-
全局特征重要性(第二章和第三章)。
-
模型表示(第六章和第七章)。
|
| 解释预测 |
|---|
- 模型是如何对我的案例做出这个预测的?
|
-
本地特征重要性(第四章)。
-
视觉归因方法(第五章)。
|
| 公平性 |
|---|
-
模型是如何对待某个特定受保护群体的成员的?
-
模型是否对我的所属群体存在偏见?
|
- 公平性概念和度量(第八章)。
|
| 对比或反事实 |
|---|
-
为什么模型会为我预测这个结果?
-
为什么不是另一种结果?
|
- 反事实解释(本章将讨论)。
|
尽管我们在本书中学到的可解释性技术将帮助我们回答表 9.1 中突出的大部分问题,但提供答案或解释给用户的过程远不止于此。我们需要知道与所提问题相关的信息是什么,解释中需要提供多少信息,以及用户如何接收或理解解释(即他们的背景)。一个名为可解释人工智能(XAI)的整个领域致力于解决这个问题。如图 9.3 所示,XAI 的范围不仅限于人工智能,其中机器学习是一个特定的子领域,而且还涉及其他领域,如人机交互(HCI)和社会科学。

图 9.3 可解释人工智能(XAI)的范围
Tim Miller 发表了一篇重要的研究论文(可在arxiv.org/pdf/1706.07269.pdf找到),这篇论文探讨了与社会科学相关的见解,这些见解与 XAI 相关。以下是该论文中的关键发现:
-
解释是对比性的——人们通常不仅仅询问模型为什么预测了特定的结果,而是为什么不是另一种结果。这在表 9.1 中被突出显示为对比性或反事实解释方法,我们将在下一节简要讨论这一点。
-
解释通常是以偏概全的方式选择的——如果向用户提供了许多解释或预测的原因,那么用户通常只会选择一两个,而且选择通常是偏颇的。因此,了解提供多少信息以及哪些信息对解释最相关是很重要的。
-
解释是社会的——从 AI 系统到用户的 信息传递必须是互动的,并以对话的形式进行。因此,拥有一个如图 9.2 所示的解释代理,它能够理解问题并提供有意义的答案,是非常重要的。用户必须处于这个互动的中心,并且重要的是要关注 HCI 领域来构建这样的系统。
在下一节中,我们将具体探讨一种可以用来提供对比性或反事实解释的技术,即回答为什么和为什么不的问题。
9.2 反事实解释
反事实解释(也称为对比性解释)可以用来解释为什么模型预测了某个值而不是另一个值。让我们看看一个具体的例子。我们将使用成人收入预测模型,这是一个二元分类问题,并且为了便于可视化,我们只关注两个输入特征——年龄和教育。

图 9.4 反事实示例的说明
这两个特征在图 9.4 中显示为一个二维平面。成人收入模型的决策边界也显示为平面上的一条曲线,将平面下部分与上部分分开。对于平面下部分的成年人,模型预测的收入小于或等于$50K,而对于平面上部分的成年人,模型预测的收入大于$50K。假设我们有一个成年人向系统提供输入以预测他们将获得多少收入。这在图 9.4 中标记为“原始输入”。这位成年人受过高中教育,假设年龄为 30 岁(在这个例子中这不相关)。因为这个输入低于决策边界,模型将预测这位成年人将获得小于$50K 的收入。然后用户提出问题:为什么我的收入小于$50K 而不是大于$50K?
反事实或对比解释将提供示例,在一个反事实世界中,如果该用户满足某些标准,那么它将导致他们获得期望的结果——获得大于$50K 的收入。反事实示例在图 9.4 中标记。它们表明,如果用户的受教育程度更高——学士、硕士或博士——那么他们获得超过$50K 收入的机会就更高。
我们如何生成这些反事实示例?整个过程,如图 9.5 所示,包括一个解释者,它将以下内容作为输入:
-
原始输入—用户提供的输入
-
期望结果—用户期望得到的结果
-
反事实示例数量—在解释中要展示的反事实示例数量
-
模型—用于预测以获得反事实示例预测的模型

图 9.5 反事实生成过程
解释者随后运行一个算法来生成反事实示例。这本质上是一个寻找反事实示例的优化问题,以满足以下标准:
-
反事实示例的模型输出尽可能接近期望结果。
-
反事实示例在特征空间中也接近原始输入,即,通过更改一组高价值特征的最小值来获得期望结果。
在本章中,我们将重点关注一种称为多样化反事实解释(DiCE)的流行技术来生成反事实解释。在 DiCE 中,优化问题被表述为我们之前所做的那样。特征以某种方式被扰动,以便它们多样化且可行更改,并达到用户的期望结果。数学细节超出了本书的范围,但让我们使用 DiCE 库为成人收入预测问题生成反事实解释。库可以按照以下方式安装:
$> pip install dice-ml
以下代码片段展示了如何加载数据并将其准备成 DiCE 解释器可以处理的形式:
import dice_ml ①
from dice_ml.utils import helpers ②
dataset = helpers.load_adult_income_dataset() ③
d = dice_ml.Data(dataframe=dataset, ④
continuous_features=['age',
➥ 'hours_per_week'], ⑤
outcome_name='income') ⑥
① 导入 DiCE 库
② 在 DiCE 库中导入辅助模块
③ 使用 DiCE 提供的辅助模块加载成人收入数据集
④ 为 DiCE 解释器准备数据;在 Data 类中将 DataFrame 参数设置为预加载数的成人收入数据集
⑤ 在 Data 类中将 continuous_features 参数设置为 DataFrame 中连续特征的列列表
⑥ 将 outcome_name 设置为 DataFrame 中包含目标变量的列名
下一步是训练模型以预测成人收入。因为我们已经在第八章中使用随机森林模型完成了这项工作,所以这里不会展示相应的代码。一旦模型训练完成,我们现在就可以初始化 DiCE 解释器,可以使用以下代码片段来完成:
m = dice_ml.Model(model=adult_income_model, ①
backend="sklearn") ②
exp = dice_ml.Dice(d, m, ③
method="random") ④
① 通过将 model 参数设置为训练好的成人收入模型初始化 DiCE Model 类
② 同时将 Model 类中的 back-end 参数设置为"sklearn",因为模型是 Scikit-Learn 库提供的 RandomForestClassifier
③ 通过传递 DiCE 数据和模型初始化 DiCE 解释器
④ 同时在 DiCE 解释器中将方法设置为"random"
初始化 DiCE 解释器后,我们可以使用下一个代码片段生成反事实示例。该函数本质上接受原始输入、反事实示例的数量和期望结果作为输入。对于这里选择的输入,模型预测的是低收入(即< $50K),而用户的期望结果是高收入(即> $50K):
original_input = dataset[0:1] ①
cf_examples = exp.generate_counterfactuals
➥ (original_input, ②
total_CFs=3, ③
desired_class="opposite") ④
cf_examples.visualize_as_dataframe(show_only_changes
➥ =True) ⑤
① 选择一个用于生成反事实的输入
② 使用 DiCE 解释器为该输入生成反事实
③ 同时将 total_CFs 参数设置为要生成的反事实示例的数量
④ 此外,将 desired_class 参数设置为反事实示例的期望结果
⑤ 将反事实示例可视化为一个 Pandas DataFrame
此代码片段的输出将打印出反事实示例,作为一个 Pandas DataFrame。此输出已被重新格式化为表格,并显示在图 9.6 中。从图 9.6 中我们可以看到,模型预测低收入的贡献关键因素是教育水平。如果教育水平更高——博士、硕士或专业学校——那么用户获得期望结果的可能性就更高。未更改的特征在图中显示为“--”。

图 9.6 DiCE 反事实解释器的输出
我们还可以使用 DiCE 反事实解释器来解释回归模型。对于分类,我们在生成反事实示例时通过在generate_counterfactuals函数中设置desired_class参数来指定所需的输出。对于回归,我们必须在同一个函数中设置一个不同的参数,称为desired_range,将其设置为模型预测所需的可能值的范围。
反事实示例是提供对比性解释的绝佳方式。一种形式的反事实解释是,“模型预测 P 是因为特征 X、Y 和 Z 的值分别是 A、B 和 C,但如果特征 X 的值是 D 或 E,那么模型将预测不同的结果 Q,”这种解释更具因果信息,有助于我们理解为什么模型预测了某个结果而不是另一个。如前所述,向 AI 系统的用户提供良好的解释需要更多的内容。XAI 是 AI、社会科学和 HCI 等多个领域的交汇点,是一个非常活跃的研究领域。这超出了本书的范围,但你所学到的技术应该为你提供一个坚实的基础,特别是在 AI 领域,以便进入 XAI 的世界。
这本书的内容到此结束。在你的工具箱中拥有广泛的可解释性技术,你已准备好理解复杂机器学习模型是如何工作的以及它们是如何得出预测的。你可以利用这些技术来调试和提升模型的性能。你也可以利用它们来增加透明度,构建公平且无偏见的模型。这本书还应为你构建可解释的人工智能系统铺平道路。你应该有一个坚实的基础来学习这个非常活跃的研究领域。祝你在构建和学习中快乐!
摘要
-
可解释性主要关乎理解 AI 系统中的底层模型是如何得出预测的,理解预测如何随着输入或算法参数的修改而变化,以及理解模型何时犯了错误。
-
可解释性超越了可解释性,因为它有助于回答“为什么”的问题——为什么模型做出了特定的预测而不是另一个?可解释性主要是由构建、部署或使用 AI 系统的专家所感知的,而这些技术是帮助你达到可解释性的基石。
-
可解释人工智能的范围不仅限于人工智能,其中机器学习是一个特定的子领域,而且还涉及其他领域,如人机交互(HCI)和社会科学。
-
从社会科学领域来看,以下三个关键发现与可解释性相关:
-
解释通常是对比性的——人们通常不会仅仅询问模型为什么预测了特定的结果,而是更想知道为什么不是另一个结果。反事实解释可以用来回答这类问题。
-
解释通常是以偏概全的。了解提供多少信息以及哪些信息对解释最为相关是很重要的。
-
解释是社会的。从 AI 系统到用户的 信息传递必须以对话或交互的形式进行。参考人机交互(HCI)领域来构建这样的系统是很重要的。
-
附录 A. 准备工作
A.1 Python
在本书中,所有代码都是用 Python 编写的。您可以从 Python 语言网站([www.python.org/downloads/](https://www.python.org/downloads/))下载并安装适用于您操作系统的最新版本。本书使用的 Python 版本是 Python 3.7,但任何更高版本都应该同样适用。本书还使用了各种开源 Python 包来构建机器学习模型,并对它们进行解释和可视化。现在,让我们下载本书中使用的所有代码并安装所有相关 Python 包。
A.2 Git 代码仓库
本书中的所有代码都可以从本书的网站(https://www.manning.com/books/interpretable-ai)和 GitHub 上的 Git 仓库(github.com/thampiman/interpretable-ai-book)下载。GitHub 上的仓库组织成文件夹,每个文件夹对应一个章节。如果您是 Git 和 GitHub 版本控制的新手,可以查看 GitHub 提供的材料(mng.bz/KBXg),以了解更多相关信息。您可以从命令行下载或克隆仓库,如下所示:
$> git clone https://github.com/thampiman/interpretable-ai-book.git
A.3 Conda 环境
Conda 是一个开源系统,用于 Python 和其他语言的包、依赖和环境管理。您可以通过遵循 Conda 网站(mng.bz/9Keq)上的说明来在您的操作系统上安装 Conda。安装后,Conda 允许您轻松地查找和安装 Python 包,并将您的环境从一个机器导出并在另一个机器上重新创建。本书使用的 Python 包以 Conda 环境的形式导出,以便您可以在目标机器上轻松地重新创建它们。环境文件以 YAML 文件格式导出,可以在仓库中的packages文件夹中找到。然后,您可以从机器上下载的仓库目录中运行以下命令来创建 Conda 环境:
$> conda env create -f packages/environment.yml
此命令将安装本书所需的全部 Python 包,并创建一个名为interpretable-ai的 Conda 环境。如果您已经创建了环境并且想要更新它,可以运行以下命令:
$> conda env update -f packages/environment.yml
一旦创建或更新了环境,您应该通过运行以下命令来激活 Conda 环境:
$> conda activate interpretable-ai
A.4 Jupyter 笔记本
本书中的代码结构为 Jupyter 笔记本。Jupyter 是一个开源的 Web 应用程序,用于轻松创建和运行实时 Python 代码、方程式、可视化和标记文本。Jupyter 笔记本在数据科学和机器学习社区中得到广泛应用。在下载源代码并安装所有相关 Python 包后,您现在可以准备好在 Jupyter 上运行本书中的代码。从您机器上下载的存储库目录中,您可以运行以下命令来启动 Jupyter Web 应用程序:
$> jupyter notebook
您可以通过浏览器在 http://
A.5 Docker
Conda 包/环境管理系统确实存在一些限制。它有时在多个操作系统、同一操作系统的不同版本或不同硬件上可能无法按预期工作。如果您在创建上一节中详细说明的 Conda 环境时遇到问题,您可以使用 Docker 作为替代。Docker 是一个用于打包软件依赖项的系统,确保每个人的环境都是相同的。您可以通过遵循 Docker 网站上的说明(www.docker.com/get-started)来在您的操作系统上安装 Docker。安装完成后,您可以从命令行运行以下命令,从您机器上下载的存储库目录中构建 Docker 镜像:
$> docker build . -t interpretable-ai
注意,interpretable-ai标签用于 Docker 镜像。如果此命令运行成功,Docker 应打印构建的镜像标识符。您还可以通过运行以下命令查看构建的镜像的详细信息:
$> docker images
运行下一个命令来使用构建的镜像运行 Docker 容器并启动 Jupyter Web 应用程序:
$> docker run -p 8888:8888 interpretable-ai:latest
此命令应启动 Jupyter 笔记本应用程序,您应该可以通过从浏览器访问 http://
附录 B. PyTorch
B.1 什么是 PyTorch?
PyTorch 是一个免费、开源的库,用于科学计算和深度学习应用,如计算机视觉和自然语言处理。它是基于 Python 的,由 Facebook 的人工智能研究(FAIR)实验室开发。PyTorch 被研究界和行业从业者广泛使用。Horace He 进行了一项最近的研究(可在mng.bz/W7Kl找到),该研究表明,2019 年在主要机器学习会议上发表的大多数技术都是在 PyTorch 中实现的。其他库和框架,如 TensorFlow、Keras、CNTK 和 MXNet,也可以用于构建和训练神经网络,但我们将在这本书中使用 PyTorch。该库是 Pythonic 的,并且很好地利用了 Python 惯用语。因此,对于已经熟悉 Python 的研究人员、数据科学家和工程师来说,使用它更容易。PyTorch 还提供了实现前沿神经网络架构的出色 API。
B.2 安装 PyTorch
您可以使用 Conda 或pip安装 PyTorch 的最新稳定版本,如下所示:
# Installing PyTorch using Conda
$> conda install pytorch torchvision -c pytorch
# Installing PyTorch using pip
$> pip install pytorch torchvision
注意,除了 PyTorch 之外,torchvision包也被安装了。这个包(pytorch.org/vision/stable/index.html)包含流行的数据集、前沿神经网络架构的实现以及计算机视觉任务中对图像进行的常见转换。您可以通过在 Python 环境中导入库来确认安装是否成功,如下所示:
import torch
import torchvision
B.3 张量
张量是一个多维数组,与 NumPy 数组非常相似。张量包含单一数据类型的元素,并且可以在图形处理单元(GPU)上进行快速计算。您可以从 Python 列表初始化 PyTorch 张量如下。请注意,本节中的代码格式化方式是为了反映 Jupyter 笔记本或 iPython 环境。输入命令的行以In:为前缀,命令的输出以Out:为前缀:
In: tensor_from_list = torch.tensor([[1., 0.], [0., 1.]])
In: print(tensor_from_list)
Out: tensor([[1., 0.],
[0., 1.]])
对于机器学习问题,NumPy 被广泛使用。该库支持大型、多维数组,并提供了一组广泛的数学函数,可以用于对这些数组进行操作。您可以从 NumPy 数组初始化张量如下。请注意,print 语句的输出显示了张量以及元素的dtype,即数据类型。我们将在 B.3.1 节中介绍这一点:
In: import numpy as np
In: tensor_from_numpy = torch.tensor(np.array([[1., 0.], [0., 1.]]))
In: print(tensor_from_numpy)
Out: tensor([[1., 0.],
[0., 1.]], dtype=torch.float64)
张量的大小或多维数组的维度可以通过以下方式获得。之前初始化的张量由两行两列组成:
In: tensor_from_list.size()
Out: torch.Size([2, 2])
我们可以初始化任何大小的空张量如下。下一个张量由三行两列组成。张量中存储的值是随机的,取决于内存中存储的位值:
In: tensor_empty = torch.empty(3, 2)
In: print(tensor_empty)
Out: tensor([[ 0.0000e+00, -1.5846e+29],
[-7.5247e+03, 2.0005e+00],
[ 9.8091e-45, 0.0000e+00]])
如果我们想初始化一个由所有零组成张量,我们可以这样做:
In: tensor_zeros = torch.zeros(3, 2)
In: print(tensor_zeros)
Out: tensor([[0., 0.],
[0., 0.],
[0., 0.]])
全为 1 的张量可以按照以下方式初始化:
In: tensor_ones = torch.ones(3, 2)
In: print(tensor_ones)
Out: tensor([[1., 1.],
[1., 1.],
[1., 1.]])
我们可以按照以下方式使用随机数初始化张量。随机数在 0 和 1 之间均匀分布:
In: tensor_rand = torch.rand(3, 2)
In: print(tensor_rand)
Out: tensor([[0.3642, 0.8916],
[0.4826, 0.4896],
[0.9223, 0.9286]])
如果您运行之前的命令,您可能不会得到相同的结果,因为随机数生成器的种子可能不同。为了获得一致且可重复的结果,您可以使用 PyTorch 提供的manual_seed函数设置随机数生成器的种子,如下所示:
In: torch.manual_seed(24)
In: tensor_rand = torch.rand(3, 2)
In: print(tensor_rand)
Out: tensor([[0.7644, 0.3751],
[0.0751, 0.5308],
[0.9660, 0.2770]])
B.3.1 数据类型
数据类型(dtype),类似于 NumPy 的dtype,描述了数据类型和大小。张量常见的数据类型如下:
-
torch.float32或torch.float: 32 位浮点数 -
torch.float64或torch.double: 64 位浮点数 -
torch.int32或torch.int: 32 位有符号整数 -
torch.int64或torch.long: 64 位有符号整数 -
torch.bool: 布尔值
所有数据类型的完整列表可以在 PyTorch 文档中找到,网址为pytorch.org/docs/stable/tensors.html。您可以通过以下方式确定张量的数据类型。我们将使用之前初始化的tensor_from_list张量:
In: tensor_from_list.dtype
Out: torch.float32
您可以按照以下方式使用给定的数据类型初始化张量:
In: tensor_from_list_float64 = torch.tensor([[1., 0.], [0., 1.]],
dtype=torch.float64) ①
In: print(tensor_from_list_float64)
Out: tensor([[1., 0.],
[0., 1.]], dtype=torch.float64) ②
① 将 dtype 参数设置为 torch.float64
② 初始化为 64 位浮点数的张量
B.3.2 CPU 和 GPU 张量
PyTorch 中的张量默认加载在 CPU 上。您可以通过检查张量所在的设备来查看这一点。我们将使用之前初始化的随机张量(tensor _rand):
In: tensor_rand.device
Out: device(type='cpu')
为了加快处理速度,您可以在 GPU 上加载张量。所有流行的深度学习框架,包括 PyTorch,都使用 CUDA(代表计算统一设备架构)在 GPU 上执行通用计算。CUDA 是由 NVIDIA 构建的平台,它提供了直接访问 GPU 的 API。CUDA 启用设备的列表可以在developer.nvidia.com/cuda-gpus#compute找到。您可以通过以下方式检查您的机器上是否可用 CUDA:
In: torch.cuda.is_available()
Out: True
如果可用,您现在可以按照以下方式在 GPU 上初始化张量:
if torch.cuda.is_available(): ①
device = torch.device(“cuda”) ②
tensor_rand_gpu = torch.rand(3, 2, device=device) ③
① 首先检查 CUDA 是否可用
② 如果是,获取 CUDA 启用设备
③ 初始化张量并将设备设置为 CUDA 启用设备
以下代码片段展示了如何将 CPU 张量转移到 GPU 上:
if torch.cuda.is_available():
device = torch.device(“cuda”)
tensor_rand = tensor_rand.to(device)
B.3.3 操作
我们可以在张量上执行多个操作。让我们看看添加两个张量的简单操作。我们首先初始化两个随机张量,x和y,如下所示:
In: x = tensor.rand(3, 2)
In: x
Out: tensor([[0.2989, 0.3510],
[0.0529, 0.1988],
[0.8022, 0.1249]])
In: y = tensor.rand(3, 2)
In: y
Out: tensor([[0.6708, 0.9704],
[0.4365, 0.7187],
[0.7336, 0.1431]])
我们可以使用add函数通过以下方式获得两个张量的和,或者直接运行x + y:
In: torch.add(x, y)
Out: tensor([[0.9697, 1.3214],
[0.4894, 0.9176],
[1.5357, 0.2680]])
PyTorch 还提供了各种其他数学运算和函数。有关所有操作的最新列表,请参阅 pytorch.org/docs/stable/torch.html。PyTorch 还提供了一个 NumPy 桥接功能,可以将张量转换为 NumPy 数组,如下所示:
In: x_numpy = x.numpy()
In: x_numpy
Out: array([[0.29888242, 0.35096592],
[0.05293709, 0.19883835],
[0.8021769 , 0.12490124]], dtype=float32)
B.4 数据集和数据加载器
PyTorch 提供了一个 Dataset 类,允许您加载和创建用于模型训练的自定义数据集。让我们看一个假设的例子。我们首先使用 Scikit-Learn 创建一个随机数据集,如下所示:
In: from sklearn.datasets import make_classification ①
In: X, y = make_classification(n_samples=100, ②
n_features=5, ③
n_classes=2, ④
random_state=42) ⑤
① 导入 make_classification 函数以创建一个随机的 n 类分类数据集
② 将样本数设置为 100
③ 将输入特征数设置为 5
④ 将类别数设置为 2 以生成二元分类数据集
⑤ 设置随机数生成器的种子
该数据集包含 100 个样本或行。每个样本包含五个输入特征和一个由两个类别组成的目标变量。每个特征的值是从正态分布中抽取的。我们可以如下检查输入特征的第一行:
In: X[0]
Out: array([-0.43066755, 0.67287309, -0.72427983, -0.53963044, -0.65160035])
我们现在将创建一个继承自 PyTorch 提供的 Dataset 类的自定义数据集类。这将在下一个代码片段中展示:
from torch.utils.data import Dataset ①
class CustomDataset(Dataset): ②
def __init__(self, ③
X, y, ④
transform=None): ⑤
self.X = X
self.y = y
self.transform = transform
def __len__(self): ⑥
return len(self.X) ⑥
def __getitem__(self, idx): ⑦
x, label = X[idx], y[idx] ⑧
if self.transform: ⑨
x = self.transform(x) ⑨
return x, label ⑩
① 导入 PyTorch 数据集类
② 创建一个继承自 Dataset 的 CustomDataset 类
③ 初始化构造函数
④ 构造函数的位置参数是输入特征矩阵 X 和目标变量数组 y。
⑤ 可选参数是一个可以应用于数据的转换
⑥ 重写 __len__ 方法以返回数据集的长度
⑦ 重写 __getitem__ 方法以返回索引 idx 处的元素
⑧ 从索引 idx 提取输入特征和目标变量
⑨ 如果已定义,则在特征上应用转换
⑩ 在索引 idx 处返回特征和目标变量
CustomDataset 类的构造函数接受两个位置参数以初始化输入特征矩阵 X 和目标变量 y。还有一个可选参数称为 transform,我们可以使用它来在数据集上应用转换函数。请注意,我们需要重写 Dataset 类提供的 __len__ 和 __getitem__ 方法,以返回数据集的长度并提取指定索引处的数据。我们可以初始化自定义数据集并检查数据集的长度,如下所示:
In: custom_dataset = CustomDataset(X, y)
In: len(custom_dataset)
Out: 100
让我们现在也检查输入特征的第一行,如下所示:
In: custom_dataset[0][0]
Out: array([-0.43066755, 0.67287309, -0.72427983, -0.53963044, -0.65160035])
我们现在将创建一个自定义数据集并将其应用转换函数。我们将传递 torch.tensor 函数以将输入特征数组转换为张量。这将在下面展示。我们可以看到输入特征的第一行现在是一个包含 64 位浮点值的张量:
In: transformed_dataset = CustomDataset(X, y,
transform=torch.tensor)
In: transformed_data[0][0]
Out: tensor([-0.4307, 0.6729, -0.7243, -0.5396, -0.6516],
➥ dtype=torch.float64)
一些常见的图像变换函数,如裁剪、翻转、旋转和调整大小,也被 PyTorch 作为torchvision包的一部分实现。完整的变换列表可以在pytorch.org/vision/stable/transforms.html找到。我们将在第五章中使用它们。
另一个值得了解的有用数据实用类是DataLoader。这个类接受一个从Dataset类继承的对象,以及一些可选参数,允许您遍历您的数据。DataLoader类提供了数据批处理、打乱和并行加载数据的功能,使用多进程工作进程。以下代码片段展示了如何初始化一个DataLoader对象并遍历之前创建的自定义数据集:
from torch.utils.data import DataLoader ①
dataloader = DataLoader(transformed_dataset, ②
batch_size=4, ③
shuffle=True, ④
num_workers=4) ⑤
for i_batch, sample_batched in enumerate(dataloader): ⑥
print(f"[Batch {i_batch}] Number of rows in batch:
➥ {len(sample_batched[0])}") ⑦
① 导入 PyTorch 提供的 DataLoader 类
② 初始化 DataLoader 并传递之前初始化的 transformed_dataset
③ 将数据分批为每批四个
④ 打乱数据集
⑤ 利用四个核心或 CPU 并行加载数据
⑥ 遍历加载器并在批处理中加载数据
⑦ 打印批号和每批加载的行数
执行此代码后,您将注意到有 25 个批处理,每个批处理有 4 行,因为输入数据集的长度为 100,DataLoader类中的batch_size参数设置为 4。我们将在 B.5.3 节和第五章中使用Dataset和DataLoader类。
B.5 建模
在本节中,我们重点关注建模以及如何使用 PyTorch 构建和训练神经网络。我们首先介绍自动微分,这是一种高效计算梯度的方法,用于优化神经网络中的权重。然后,我们将介绍模型定义和模型训练。
B.5.1 自动微分
在第四章中,我们将学习神经网络。神经网络由许多通过边相互连接的层组成。网络中每一层的每个单元对其所有输入执行数学运算,并将输出传递给下一层。连接单元的边与权重相关联,学习算法的目标是确定所有边的权重,使得神经网络的预测尽可能接近标记数据集中的目标。
确定权重的一个有效方法是使用反向传播算法。我们将在第四章中了解更多关于这个内容。在本节中,我们将学习自动微分及其在 PyTorch 中的实现。自动微分是一种数值评估函数导数的方法。反向传播是自动微分的一个特例。让我们看一个简单的例子,看看我们如何在 PyTorch 中应用自动微分。考虑一个表示为 x 的输入张量。我们对这个输入张量进行的第一个操作是将它乘以一个因子 2。让我们将这个操作的输出表示为 w,其中 w = 2x。给定 w,我们现在对它执行第二个数学操作,并将输出张量表示为 y。这个操作如下所示:

我们执行的最后一个操作是简单地求和张量 y 中的所有值。我们将最终的输出张量表示为 z。如果我们现在想计算输出 z 关于输入 x 的梯度,我们需要应用链式法则如下:

该方程中的偏导数如下所示:



对于更复杂的数学函数,这些梯度的计算可能很复杂。PyTorch 通过使用 autograd 包使这变得更容易。autograd 包实现了自动微分,并允许您数值评估函数的导数。通过应用前面显示的链式法则,autograd 允许您自动计算任意阶函数的梯度。让我们通过使用张量实现前面的数学操作来观察这一过程。我们首先初始化一个大小为 2 × 3 的输入张量 x,其中包含所有 1。请注意,当初始化张量时,将一个名为 requires_grad 的参数设置为 True。此参数让 autograd 知道要记录对它们的操作以进行自动微分:
In: x = torch.ones(2, 3,
requires_grad=True)
In: x
Out: tensor([[1., 1., 1.],
[1., 1., 1.]], requires_grad=True)
现在我们实现第一个数学操作,将张量 x 乘以一个因子 2 以获得张量 w。请注意,张量 w 的输出显示了 grad_fn,它用于记录对 x 执行的操作以获得 w。此函数用于使用自动微分数值评估梯度:
In: w = 2 * x
In: w
Out: tensor([[2., 2., 2.],
[2., 2., 2.]], grad_fn=<MulBackward0>)
现在我们实现第二个数学操作,用于将张量 w 转换为 y:
In: y = w * w * w + 3 * w * w + 2 * w + 1
In: y
Out: tensor([[25., 25., 25.],
[25., 25., 25.]], grad_fn=<AddBackward0>)
最终的操作简单地取张量 y 中所有值的总和以获得 z,如下所示:
In: z = torch.sum(y)
In: z
Out: tensor(150., grad_fn=<SumBackward0>)
我们可以通过调用 backward 函数轻松地计算张量 z 关于输入 x 的梯度。这将应用链式法则并计算输出相对于输入的梯度:
In: z.backward()
我们可以看到梯度的数值评估如下:
In: x.grad
Out: tensor([[52., 52., 52.],
[52., 52., 52.]])
为了验证答案是否正确,让我们根据前面提供的方程数学推导出z相对于x的导数。以下是总结:


作为练习,我鼓励您使用张量评估这个方程。这个练习的解决方案可以在与本书相关的 GitHub 仓库github.com/thampiman/interpretable-ai-book中找到。
B.5.2 模型定义
现在我们来看如何使用 PyTorch 定义一个神经网络。我们将关注一个全连接神经网络。我们在 A.4 节中生成的合成数据集由五个输入特征和一个二进制输出组成。现在让我们定义一个包含一个输入层、两个隐藏层和一个输出层的全连接神经网络。输入层必须包含五个单元,因为数据集包含五个输入特征。输出层必须包含一个单元,因为我们处理的是一个二进制输出。我们在选择两个隐藏层中单元的数量方面有灵活性。让我们分别为第一和第二隐藏层选择五个和三个单元。我们在神经网络中每个单元的输入上进行线性组合,并在隐藏层使用 ReLU 激活函数,在输出层使用 sigmoid 激活函数。有关神经网络和激活函数的更多详细信息,请参阅第四章。
在 PyTorch 中,我们可以使用torch.nn.Sequential容器按顺序定义神经网络中的单元和层。PyTorch 中的每个单元层都必须继承自torch.nn.Module基类。PyTorch 已经提供了许多神经网络中常用的层,包括线性层、卷积层和循环层。常见的激活函数如 ReLU、sigmoid 和双曲正切(tanh)也已实现。层和激活函数的完整列表可以在pytorch.org/docs/master/nn.html找到。我们现在可以使用这些构建块来定义模型,如下所示:
model = torch.nn.Sequential(
torch.nn.Linear(5, 5),
torch.nn.ReLU(),
torch.nn.Linear(5, 3),
torch.nn.ReLU(),
torch.nn.Linear(3, 1),
torch.nn.Sigmoid()
)
在这里定义的 Sequential 容器按顺序定义层。第一个 Linear 模块对应于第一个隐藏层,它接收数据集中的五个特征并产生五个输出,这些输出被馈送到下一层。Linear 模块对输入执行线性变换。容器中的下一个模块定义了第一个隐藏层的 ReLU 激活函数。接下来的 Linear 模块接收来自第一个隐藏层的五个输入特征,执行线性变换,并产生三个输出,这些输出被馈送到下一层。同样,在第二个隐藏层中也使用了 ReLU 激活函数。最后的 Linear 模块接收来自第二个隐藏层的三个输入特征并产生一个输出,即输出层。因为我们处理的是二分类,所以在输出层使用 Sigmoid 激活函数。如果我们通过执行命令 print(model) 打印模型,我们将得到以下输出:
Sequential(
(0): Linear(in_features=5, out_features=5, bias=True)
(1): ReLU()
(2): Linear(in_features=5, out_features=3, bias=True)
(3): ReLU()
(4): Linear(in_features=3, out_features=1, bias=True)
(5): Sigmoid()
)
我们现在可以查看如何将神经网络定义为类,其中层数和单元数可以轻松定制,如下面的代码片段所示:
class BinaryClassifier(torch.nn.Sequential): ①
def __init__(self, layer_dims): ②
super(BinaryClassifier, self).__init__() ③
for idx, dim in enumerate(layer_dims): ④
if (idx < len(layer_dims) - 1): ⑤
module = torch.nn.Linear(dim, layer_dims[idx + 1]) ⑤
self.add_module(f"linear{idx}", module) ⑤
if idx < len(layer_dims) - 2: ⑥
activation = torch.nn.ReLU() ⑥
self.add_module(f"relu{idx}", activation) ⑥
elif idx == len(layer_dims) - 2: ⑦
activation = torch.nn.Sigmoid() ⑦
self.add_module(f"sigmoid{idx}", activation) ⑦
① 扩展 Sequential 容器的 BinaryClassifier 类
② 构造函数接收一个名为 layer_dims 的数组,该数组定义了网络的架构。
③ 初始化 Sequential 容器
④ 遍历 layer_dims 数组
⑤ 为所有层添加线性模块,并命名为“linear”,后跟层的索引
⑥ 为所有隐藏层添加 ReLU 模块,并命名为“relu”,后跟隐藏层的索引
⑦ 对于输出层,添加 Sigmoid 模块,并命名为“sigmoid”,后跟输出层的索引
BinaryClassifier 类继承自 torch.nn.Sequential。构造函数接收一个位置参数,这是一个名为 layer_dims 的整数数组,它定义了每层的层数和单元数。数组的长度定义了层数,而索引 i 处的元素定义了第 i+1 层的单元数。在构造函数内部,我们遍历 layer_dims 数组,并使用 add_module 函数将一个层添加到容器中。实现使用线性模块来处理所有层,并命名为 linear,后跟层的索引。我们为所有隐藏层使用 ReLU 激活函数,对于输出层使用 sigmoid 激活函数。有了这个自定义类,我们现在可以初始化二分类器,并使用数组轻松定义结构,如下所示:
num_features = 5 ①
num_outputs = 1 ②
layer_dims = [num_features, 5, 3, num_outputs] ③
bc_model = BinaryClassifier(layer_dims) ④
① 设置输入特征的数量为 5
② 设置输出数量为 1
③ 初始化 layer_dims 数组,该数组定义了由输入层五个单元、第一个隐藏层五个单元、第二个隐藏层三个单元和输出层一个单元组成的网络结构
④ 使用 BinaryClassifier 类初始化模型
通过执行print(bc_model),我们可以看到网络的架构,它给出了以下输出。我们将在第四章中使用类似的实现:
BinaryClassifier(
(linear0): Linear(in_features=5, out_features=5, bias=True)
(relu0): ReLU()
(linear1): Linear(in_features=5, out_features=3, bias=True)
(relu1): ReLU()
(linear2): Linear(in_features=3, out_features=1, bias=True)
(sigmoid2): Sigmoid()
)
B.5.3 训练
在模型就绪后,我们现在可以将其训练在之前创建的数据集上。从高层次来看,训练循环包括以下步骤:
-
循环 epoch:对于每个 epoch,循环遍历数据批次。
-
对于每个小批量数据
-
将数据通过模型以获得输出
-
计算损失
-
运行反向传播算法以优化权重
-
-
一个 epoch 是一个超参数,它定义了我们在神经网络中正向和反向传播整个训练数据的次数。在每个 epoch 中,我们加载一批数据,对于每一批数据,我们将其通过网络以获取输出,计算损失,并使用反向传播算法根据该损失优化权重。
PyTorch 提供了大量的损失函数或优化标准。其中一些常用的如下:
-
torch.nn.L1Loss—这个函数计算输出预测和实际值的平均绝对误差(MAE)。这通常用于回归任务。 -
torch.nn.MSELoss—这个函数计算输出预测和实际值的平均平方误差(MSE)。像 L1 损失一样,这也通常用于回归任务。 -
torch.nn.BCELoss—这个函数计算输出预测和实际标签的二进制交叉熵或对数损失。这个函数通常用于二分类任务。 -
torch.nn.CrossEntropyLoss—这个函数结合了 softmax 和负对数似然损失函数,通常用于分类任务。我们将在第五章中学习更多关于 BCE 损失和交叉熵损失的内容。
你可以在mng.bz/Dx5A找到所有损失函数的完整列表。由于我们只处理我们创建的数据集中的两个目标类别,我们将使用 BCE 损失函数。
PyTorch 还提供了各种优化算法,我们可以在反向传播过程中使用它们来更新权重。在本节中,我们将使用 Adam 优化器。PyTorch 中实现的全部优化器的完整列表可以在pytorch.org/docs/stable/optim.html找到。以下代码片段初始化了优化器的损失函数或标准或上一节中初始化的模型的所有参数或权重:
criterion = torch.nn.BCELoss()
optimizer = torch.optim.Adam(bc_model.parameters())
我们可以如下实现训练循环。注意,我们正在训练 10 个 epoch。在每个 epoch 中,我们使用在 A.4 节中初始化的DataLoader对象批量加载数据和标签。对于每个数据小批量,我们首先需要将梯度重置为零,然后计算该小批量的梯度。然后我们通过模型的前向方向运行以获得输出。然后我们使用这些输出来计算 BCE 损失。通过调用backward函数,使用自动微分计算损失函数相对于输入的梯度。然后我们调用优化器中的step函数,根据计算出的梯度更新权重或模型参数:
num_epochs = 10 ①
for epoch in range(num_epochs): ②
for idx, (X_batch, labels) in enumerate(dataloader): ③
optimizer.zero_grad() ④
outputs = bc_model(X_batch) ⑤
loss = criterion(outputs, labels) ⑥
loss.backward() ⑦
optimizer.step() ⑧
① 初始化 epoch 数量的变量
② 循环遍历 epoch
③ 遍历每个数据和小批量标签
④ 为每个小批量重置梯度到 0
⑤ 将数据通过模型的前向方向运行以获取输出预测
⑥ 通过与真实标签比较来计算损失
⑦ 执行反向传播来计算损失函数相对于输入的梯度
⑧ 通过调用 step 更新模型中的参数
一旦我们训练好了模型,我们就可以对数据点进行如下预测。注意,我们将以下代码片段的格式转换为模仿 Jupyter 笔记本或 iPython 环境:
In: pred_var = bc_model(transformed_dataset[0][0])
In: pred_var
Out: tensor([0.5884], grad_fn=<SigmoidBackward>)
模型的输出是一个包含概率度量的张量。这个概率度量对应于神经网络中最终层的 sigmoid 激活函数的输出。你可以如下获得预测的标量:
In: pred_var.detach().numpy()[0]
Out: 0.5884
这就结束了我们对 PyTorch 的快速浏览,我们希望您已经拥有了足够的知识来实施和训练神经网络,并理解本书中的代码。有很多关于 PyTorch 的书籍和在线资源,可以在bookauthority.org/books/new-pytorch-books和mng.bz/laBd找到。PyTorch 文档pytorch.org/docs/stable/index.html也是一个深入了解库的极好资源。


函数的输出范围在 0 到 1 之间。它是可导的,并且如图所示是单调的。
函数的输出范围从 -1 到 1。它也是可微和单调的,如图所示。
函数的输出范围从 0 到无穷大(取决于输入 x 的值)。它是可微和单调的,如图所示。
浙公网安备 33010602011771号