贝叶斯推理深度学习增强指南-全-

贝叶斯推理深度学习增强指南(全)

原文:annas-archive.org/md5/3925f12c16b3ab4fff402d1f04c4210b

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

在过去十年中,机器学习领域取得了巨大进展,并因此吸引了公众的关注。但我们必须牢记,尽管这些算法令人印象深刻,它们并非万无一失。通过本书,我们希望提供一种易于理解的介绍,说明如何在深度学习中应用贝叶斯推理,赋予读者开发“知道自己不知道”的模型的工具。通过这样做,你将能够开发出更强健的深度学习系统,更好地满足当今基于机器学习的应用需求。

本书适合人群

本书面向从事机器学习算法开发和应用的研究人员、开发人员和工程师,特别是那些希望开始使用具备不确定性感知的深度学习模型的人。

本书内容

第一章深度学习时代的贝叶斯推理 介绍了传统深度学习方法的应用场景和局限性。

第二章贝叶斯推理基础 讨论了贝叶斯建模和推理,并探讨了贝叶斯推理的黄金标准机器学习方法。

第三章深度学习基础 介绍了深度学习模型的主要构建块。

第四章贝叶斯深度学习简介第二章贝叶斯推理基础第三章深度学习基础 中介绍的概念结合起来,讨论贝叶斯深度学习。

第五章贝叶斯深度学习的原则方法 介绍了贝叶斯神经网络近似的有原则的方法。

第六章使用标准工具箱进行贝叶斯深度学习 介绍了利用常见深度学习方法来促进模型不确定性估计的方法。

第七章贝叶斯深度学习的实际考虑 探讨并比较了在第五章贝叶斯深度学习的原则方法第六章使用标准工具箱进行贝叶斯深度学习 中介绍的方法的优缺点。

第八章应用贝叶斯深度学习 提供了贝叶斯深度学习多种应用的实际概述,如检测分布外数据或应对数据集偏移的鲁棒性。

第九章, 贝叶斯深度学习的下一步,讨论了贝叶斯深度学习中的一些最新趋势。

充分利用本书

你需要具备一定的机器学习和深度学习的基础知识,并对贝叶斯推理的相关概念有所了解。具备 Python 和机器学习框架(如 TensorFlow 或 PyTorch)的一些实践经验也会很有帮助,但并不是必要的。

推荐使用 Python 3.8 或更高版本,因为所有代码都已在 Python 3.8 上进行了测试。第一章深度学习时代的贝叶斯推理提供了关于设置本书代码示例环境的详细说明。

下载示例代码文件

本书的代码包也托管在 GitHub 上,地址是 github.com/PacktPublishing/Enhancing-Deep-Learning-with-Bayesian-Inference。如果代码有更新,它将会更新到现有的 GitHub 仓库中。

我们还提供了来自我们丰富的书籍和视频目录中的其他代码包,地址是 github.com/PacktPublishing/。快去看看吧!

下载彩色图像

我们还提供了一个 PDF 文件,包含本书中使用的截图/图表的彩色图像。你可以在这里下载:packt.link/7xy1O

使用的约定

本书中使用了许多文本约定。

CodeInText:表示文本中的代码词汇、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟网址和用户输入。示例:“任何尝试运行包含此类问题的代码都会立即导致解释器失败,并抛出 SyntaxError 异常。”

代码块如下设置:


{const set = function(...items) { 
this.arr  = [...items]; 
this.add = {function}(item) { 
if( this._arr.includes(item) ) { 
            return false; (SC-Source)}

任何命令行输入或输出都如下书写:


$ python3 script.py

一些代码示例将表示 shell 的输入。你可以通过特定的提示符号来识别它们:

  • >>> 表示交互式 Python shell

  • $ 表示 Bash shell(macOS 和 Linux)

  • > 表示 CMD 或 PowerShell(Windows)

警告或重要提示会以这种方式出现。

重要提示

警告或重要提示会以这种方式出现。

提示和技巧以这种方式出现。

提示或技巧

以这种方式出现。

联系我们

我们欢迎读者的反馈。

一般反馈:如果你对本书的任何方面有疑问,请在邮件主题中提到书名,并发送至 customercare@packtpub.com。

勘误表:尽管我们已经尽力确保内容的准确性,但错误仍然可能发生。如果您在本书中发现了错误,我们将非常感激您能向我们报告。请访问 www.packtpub.com/submit-errata,选择您的书籍,点击“勘误表提交表单”链接,并输入详细信息。

盗版:如果您在互联网上遇到我们作品的任何非法复制品,我们将非常感激您能提供该内容的地址或网站名称。请通过 copyright@packtpub.com 联系我们,并附上相关链接。

如果您有兴趣成为作者:如果您在某个领域有专业知识,并且有兴趣编写或为书籍贡献内容,请访问 authors.packtpub.com

分享您的想法

一旦您阅读了《通过贝叶斯推断增强深度学习》,我们很希望听到您的想法!请 点击这里直接前往 Amazon 评论页面 以分享您的反馈。

您的评论对我们和技术社区至关重要,将帮助我们确保提供优质的内容。

下载这本书的免费 PDF 版本

感谢购买本书!

您喜欢随时随地阅读,但无法随身携带纸质书籍吗?

您购买的电子书是否与您选择的设备不兼容?

不用担心,现在每本 Packt 书籍都会免费提供无 DRM 保护的 PDF 版本。

您可以在任何地方、任何设备上阅读。直接从您最喜欢的技术书籍中搜索、复制并粘贴代码到您的应用程序中。

这些福利不止于此,您还可以每天收到独家的折扣、新闻通讯以及精彩的免费内容。

按照以下简单步骤获得福利:

  1. 扫描二维码或访问下面的链接

    PIC

    packt.link/free-ebook/9781803246888

  2. 提交您的购买凭证

  3. 就是这样!我们将把您的免费 PDF 和其他福利直接发送到您的电子邮件中。

第一章

深度学习时代的贝叶斯推理

在过去的十五年里,机器学习ML)从一个相对不为人知的领域,迅速成为科技圈的流行词。这在很大程度上归功于神经网络NNs)的惊人成就。曾经是该领域中的一个冷门角色,深度学习在几乎每个可想象的应用中的成就,导致其人气的迅速飙升。它的成功如此广泛,以至于我们不再因深度学习所带来的特性而感到惊讶,反而开始期望它们的出现。从社交应用中的滤镜,到在海外度假时依赖谷歌翻译,深度学习无疑已经深深嵌入到技术领域中。

尽管深度学习取得了令人印象深刻的成就,并为我们带来了丰富的产品和特性,但它仍然没有跨越最后的障碍。随着复杂的神经网络越来越多地应用于关键任务和安全任务,关于它们稳健性的疑问变得越来越重要。许多深度学习算法的“黑箱”特性使得它们成为安全意识强的解决方案架构师的难题——以至于许多人宁愿接受次优的表现,也不愿承担使用不透明系统的潜在风险。

那么,我们如何克服围绕深度学习的疑虑,确保构建更加稳健、值得信赖的模型呢?虽然一些答案可以从可解释人工智能XAI)的路径中找到,但一个重要的构建块在于贝叶斯深度学习BDL)领域。通过本书,你将通过实际示例了解 BDL 背后的基本原理,帮助你深入理解该领域,并为你提供所需的知识和工具,以构建自己的 BDL 模型。

但在开始之前,让我们更深入地探讨贝叶斯深度学习(BDL)的理论依据,为什么典型的深度学习方法可能没有我们想象中的那么稳健。在本章中,我们将了解深度学习的一些关键成功与失败案例,以及 BDL 如何帮助我们避免标准深度模型可能带来的悲剧性后果。然后,我们将概述本书其余章节的核心内容,并介绍我们将在实际示例中使用的库和数据。

以下主题将在接下来的章节中讨论:

  • 深度学习时代的奇迹

  • 理解深度学习的局限性

  • 核心主题

  • 设置工作环境

1.1 技术要求

本书的所有代码都可以在本书的 GitHub 仓库中找到:github.com/PacktPublishing/Enhancing-Deep-Learning-with-Bayesian-Inference

1.2 深度学习时代的奇迹

在过去的 10 到 15 年里,由于深度学习的巨大成功,我们见证了机器学习领域的剧变。深度学习影响力最广泛的成就之一,或许就是它已经渗透到从医学影像学、制造业到翻译工具和内容创作等各个领域。

尽管深度学习在最近几年才取得了巨大成功,但它的许多核心原理已经得到了很好的确立。研究人员已经在神经网络领域工作了一段时间——实际上,早在 1957 年,Frank Rosenblatt 就提出了第一个神经网络!当然,这比我们今天的模型要简单得多,但它是这些模型的重要组成部分:感知器,如图 1.1所示。

PIC

图 1.1:单一感知器的示意图

1980 年代引入了许多如今已熟知的概念,其中包括 Kunihiko Fukushima 于 1980 年提出的卷积神经网络CNNs),以及 John Hopfield 于 1982 年开发的递归神经网络RNN)。1980 年代和 1990 年代,这些技术进一步成熟:Yann LeCun 在 1989 年将反向传播应用于创建能够识别手写数字的 CNN,而长短期记忆(LSTM)RNN 的关键概念则由 Hochreiter 和 Schmidhuber 于 1997 年提出。

然而,尽管在世纪之交之前我们已经有了今天强大模型的基础,直到现代 GPU 的引入,整个领域才真正蓬勃发展。随着 GPU 带来的加速训练和推理,开发包含几十层(甚至几百层)网络成为可能。这为非常复杂的神经网络架构开辟了大门,这些架构能够学习高维数据的紧凑特征表示。

PIC

图 1.2:AlexNet 架构图

最早具有重大影响的网络架构之一是 AlexNet。这一网络由 Alex Krizhevsky、Ilya Sutskever 和 Geoffrey Hinton 开发,包含 11 层,能够将图像分类为 1000 种可能的类别之一。它在 2012 年的 ImageNet 大规模视觉识别挑战赛中取得了前所未有的成绩,展示了深度网络的强大能力。AlexNet 是许多具有影响力的神经网络架构中的第一个,接下来的几年里,许多现在熟知的架构相继问世,包括 VGG Net、Inception 架构、ResNet、EfficientNet、YOLO……这个列表还在继续!

但是神经网络不仅仅在计算机视觉应用中取得了成功。2014 年,Dzmitry Bahdanau、Kyunghyun Cho 和 Yoshua Bengio 的研究表明,端到端神经网络模型可以用于在机器翻译中取得最先进的成果。这是该领域的一个分水岭时刻,随后大规模的机器翻译服务迅速采用了这些端到端网络,进一步推动了自然语言处理领域的发展。时至今日,这些概念已经发展成熟,产生了Transformer架构——一种通过自监督学习获取丰富特征嵌入能力的架构,极大地推动了深度学习的发展。

借助各种架构赋予的令人印象深刻的灵活性,神经网络如今已在几乎每个可想象的领域的应用中达到了最先进的性能,并且它们已成为我们日常生活中熟悉的一部分。无论是我们在移动设备上使用的面部识别,还是像谷歌翻译这样的翻译服务,亦或是智能设备中的语音识别,显然这些网络不仅在图像分类挑战中具有竞争力,它们现在是我们开发的技术中不可或缺的一部分,甚至能够超越 人类

随着深度学习模型超越人类专家的报道越来越频繁,最深刻的例子或许是在医学影像领域。2020 年,由伦敦帝国学院和谷歌健康研究人员开发的一个网络在从乳腺 X 光片中检测乳腺癌时超越了六位放射科医生。几个月后,2021 年 2 月的一项研究表明,一个深度学习模型能够超越两位人类专家,诊断胆囊疾病。另一项在同年晚些时候发布的研究显示,一个卷积神经网络(CNN)在检测皮肤异常图像中的黑色素瘤时超越了 157 位皮肤科医生。

到目前为止,我们讨论的所有应用都是监督式的机器学习应用,其中模型被训练用于分类或回归问题。然而,深度学习最令人印象深刻的成就之一出现在其他应用中,包括生成建模和强化学习。也许最著名的强化学习例子之一是AlphaGo,这是 DeepMind 开发的一个强化学习模型。顾名思义,这个算法通过强化学习训练来下围棋。与象棋等一些游戏不同,象棋可以通过相对直接的人工智能方法来解决,而围棋在计算上要复杂得多。这是因为围棋的复杂性——众多可能的着法组合使得传统方法难以应对。因此,当 AlphaGo 在 2015 年和 2016 年分别战胜围棋冠军范辉和李世石时,这成了轰动一时的新闻。

DeepMind 通过创建一个自我对弈的版本进一步改进了 AlphaGo——AlphaGo Zero。这个模型优于任何以前的模型,在围棋中达到了超人类的表现。其成功的核心算法 AlphaZero 也在其他一系列游戏中实现了超人类的表现,证明了该算法能够推广到其他应用领域。

过去十年,深度学习的另一个重要里程碑是生成对抗网络GANs)的出现。GAN 通过使用两个网络来工作。第一个网络的目标是生成与训练集具有相同统计特征的数据。第二个网络的目标是根据从数据集中学到的内容对第一个网络的输出进行分类。因为第一个网络并没有直接在数据上进行训练,它不会简单地复制数据——而是通过学习欺骗第二个网络来工作。这就是为什么使用对抗这一术语的原因。通过这一过程,第一个网络能够学习哪些输出能够成功欺骗第二个网络,从而生成符合数据分布的内容。

GAN 能够生成特别令人印象深刻的输出。例如,以下图像是由 StyleGAN2 模型生成的:

图片

图 1.3:由 StyleGAN2 从 thispersondoesnotexist.com 生成的面孔。

但 GAN 不仅仅用于生成真实的面孔;它们在许多其他领域也有实际应用,比如为药物发现建议分子组合。它们还是通过数据增强提升其他机器学习方法的强大工具——使用 GAN 生成的数据来扩充数据集。

所有这些成功可能让深度学习看起来无懈可击。虽然它的成就令人印象深刻,但它们并没有讲述整个故事。在下一节中,我们将了解深度学习的一些失败,并开始理解贝叶斯方法如何帮助我们避免这些问题。

1.3 理解深度学习的局限性

正如我们所见,深度学习取得了一些显著的成就,不可否认它正在革新我们处理数据和预测建模的方式。但深度学习的短暂历史中也充满了更黑暗的故事:这些故事带来了重要的教训,帮助我们开发出更强大、更安全的系统。

在本节中,我们将介绍一些深度学习失败的关键案例,并讨论贝叶斯视角如何有助于产生更好的结果。

1.3.1 深度学习系统中的偏差

我们将从一个教科书式的偏差例子开始,这是数据驱动方法面临的一个关键问题。这个例子围绕着亚马逊展开。如今已经是家喻户晓的名字,亚马逊最初通过彻底改变书籍零售的世界而起步,随后成为了几乎任何商品的一站式购物平台:从花园家具到新笔记本电脑,甚至是家庭安全系统,如果你能想到它,你大概可以在亚马逊上购买到。该公司还在技术上取得了显著进展,通常是通过改进基础设施以促进其扩展。从硬件基础设施到优化方法中的理论和技术飞跃,最初的电子商务公司如今已经成为技术领域的关键人物之一。

虽然这些技术飞跃通常会设定行业标准,但这个例子却恰恰相反:它展示了数据驱动方法的一个关键弱点。我们所指的案例是亚马逊的 AI 招聘软件。由于自动化在亚马逊成功中的关键作用,将这一自动化扩展到简历审查是合情合理的。2014 年,亚马逊的机器学习工程师部署了一个工具来实现这一目标。该工具基于过去十年的申请者数据进行训练,旨在从公司庞大的申请者池中识别出有利的特征。然而,2015 年时,大家发现它学会了依赖某些特征,从而导致了深具负面影响的行为。

这个问题在很大程度上源于底层数据:由于当时科技行业的特性,亚马逊的简历数据集主要由男性申请者主导。这导致了模型预测中的巨大不平衡:它实际上学会了偏向男性,变得极其偏向男性申请者,严重歧视女性申请者。模型的歧视行为导致亚马逊放弃了这个项目,至今它已成为 AI 社区中偏差问题的一个重要案例。

在这里提出的问题中,一个重要的因素是,这种偏见不仅仅是由显式信息驱动的,比如一个人的名字(这可能是性别的线索):算法会学习潜在的信息,进而驱动偏见。这意味着,问题不能仅仅通过匿名化人们来解决——需要工程师和科学家确保对偏见进行全面评估,以确保我们部署的算法是公平的。尽管贝叶斯方法无法消除偏见,但它为我们提供了一系列工具,帮助我们解决这些问题。正如我们在本书后面将看到的,贝叶斯方法使我们能够确定数据是否处于分布内(in-distribution)还是分布外OOD)。在这种情况下,亚马逊本可以利用贝叶斯方法的这一能力:将分布外(OOD)数据分离出来,并进行分析,了解为何这些数据是 OOD 的。是因为它关注到了一些相关因素,比如经验不匹配的申请者?还是关注到了某些无关且具有歧视性的因素,比如申请者的性别?这本可以帮助亚马逊的机器学习团队尽早发现不良行为,从而开发出无偏的解决方案。

1.3.2 过度自信预测的危险

另一个广泛引用的深度学习失败案例,出现在 Kevin Eykholt 等人的论文《深度学习视觉分类的稳健物理世界攻击》中(arxiv.org/abs/1707.08945)。这篇论文在揭示深度学习模型的对抗攻击问题上起到了重要作用:稍微修改输入数据,让模型做出错误预测。在他们论文中的一个关键例子中,他们在停车标志上粘贴了白色和黑色的贴纸。尽管对标志的修改非常微妙,计算机视觉模型却将修改后的标志误解为限速 45 标志。

PIC

图 1.4:展示了一个简单对抗攻击对模型解读停车标志的影响。

起初,这看起来可能无关紧要,但如果我们退一步,考虑特斯拉、优步等公司在自动驾驶汽车方面投入的巨大工作,就不难看出这种对抗性扰动如何导致灾难性后果。在这个标志的案例中,误分类可能导致自动驾驶汽车忽略停车标志,冲入交叉路口的交通中。这显然对乘客或其他道路使用者来说都不好。事实上,2016 年发生过一个与我们所描述的非常相似的事件,当时一辆特斯拉 Model S 在佛罗里达州北部与一辆卡车发生碰撞(www.reuters.com/article/us-tesla-crash-idUSKBN19A2XC)。据特斯拉称,由于无法将卡车后面明亮的天空与拖车区分开来,特斯拉的自动驾驶系统没有检测到拖车。驾驶员也未能注意到拖车,最终导致了致命的碰撞。但如果自动驾驶系统的决策过程更为复杂呢?本书的一个关键主题是如何在我们的机器学习系统中做出鲁棒的决策,特别是在任务关键或安全关键的应用中。

虽然这个交通标志的例子直观地展示了误分类带来的危险,但这一点同样适用于广泛的其他场景,从用于制造的机器人设备到自动化手术程序。

对于这些系统而言,了解一定程度的信心(或不确定性)是提高其鲁棒性并确保一致性安全行为的重要步骤。在停车标志的案例中,拥有一个“知道自己不知道”的模型可以防止潜在的悲剧性后果。正如我们将在本书后面看到的,BDL 方法通过不确定性估计可以帮助我们检测对抗性输入。在我们的自动驾驶汽车示例中,可以将其纳入逻辑中,以便在模型不确定时,汽车安全停车并切换到手动模式,让驾驶员能够安全地应对这一情况。这就是具有不确定性感知模型的智慧:让我们设计出了解自己局限性的模型,从而在意外情况下更加鲁棒。

1.3.3 趋势变化

我们之前的例子探讨了应对数据随时间变化的挑战——这是现实世界应用中常见的问题。我们将考虑的第一个问题,通常被称为数据集漂移协变量漂移,发生在模型推理时遇到的数据相对于训练时的数据发生了变化。这通常是由于现实世界问题的动态性以及训练集——即使是非常大的训练集——也很少能代表它们所代表现象的全部变化。一个重要的例子可以在论文《Systematic Review of Approaches to Preserve Machine Learning Performance in the Presence of Temporal Dataset Shift in Clinical Medicine》中找到,在这篇论文中,Lin Lawrence Guo 人强调了数据集漂移的问题(www.ncbi.nlm.nih.gov/pmc/articles/PMC8410238/)。他们的研究表明,关于如何解决临床环境中应用的机器学习模型中的数据集漂移问题的文献相对较少。这是一个问题,因为临床数据是动态的。让我们来看一个例子。

在我们的例子中,我们有一个模型,它被训练用来根据病人的症状自动开药。当病人向医生抱怨呼吸道症状时,医生使用该模型开药。由于模型接收到的数据,模型开出了抗生素。这个方法对许多病人有效,但随着时间的推移,情况发生了变化:一种新疾病在群体中变得流行。这种新疾病恰好与之前流行的细菌感染症状非常相似,但它是由病毒引起的。由于模型无法适应数据集的变化,它继续推荐抗生素。这不仅无法帮助病人,还可能导致局部群体中抗生素耐药性的产生。

为了能够应对现实世界数据中的这些变化,模型需要对数据集漂移保持敏感性。做到这一点的一种方法是通过使用贝叶斯方法,这些方法提供不确定性估计。将其应用到我们的自动开药示例中,当模型能够生成不确定性估计时,它对数据中的微小变化变得敏感。例如,可能会有一些微妙的症状差异,比如与我们新的病毒感染相关的不同类型的咳嗽。这将导致模型预测的不确定性上升,表明模型需要通过新数据进行更新。

一个相关问题,被称为灾难性遗忘,是由模型适应数据变化引起的。根据我们的例子,这听起来像是件好事:如果模型正在适应数据变化,那它们就永远是最新的,对吧?不幸的是,事情并没有那么简单。灾难性遗忘发生在模型从新数据中学习时,但在此过程中“忘记”了过去的数据。

例如,假设开发了一种机器学习算法来识别欺诈性文档。它可能一开始表现得非常好,但欺诈者很快会注意到曾经能够欺骗自动文档验证的方法不再有效,因此他们开发了新方法。虽然其中一些方法能成功突破,但模型通过其不确定性估计注意到它需要适应新数据。模型更新了其数据集,专注于当前流行的攻击方法,并进行几轮额外的训练。再次,它成功地阻止了欺诈者,但让模型设计师吃惊的是,模型开始放行一些较老、更不复杂的攻击:这些攻击曾经很容易被模型识别出来。

在新数据的训练中,模型的参数发生了变化。由于更新后的数据集中没有足够的旧数据支持,模型丧失了关于输入(文档)和其分类(是否欺诈)的旧关联信息。

尽管这个例子使用不确定性估计来解决数据集漂移问题,但它本可以进一步利用这些估计来确保数据集的平衡性。这可以通过不确定性采样等方法来实现,这些方法旨在从不确定区域进行采样,确保用于训练模型的数据集涵盖当前和过去数据中的所有可用信息。

1.4 核心主题

本书的目标是为您提供开发自己 BDL 解决方案所需的工具和知识。为此,尽管我们假设您对统计学习和深度学习的概念有所了解,但我们仍然会提供这些基础概念的复习。

第二章贝叶斯推断基础中,我们将回顾贝叶斯推断的一些关键概念,包括概率和模型不确定性估计。在第三章深度学习基础中,我们将介绍深度学习的几个重要方面,包括通过反向传播学习以及神经网络的常见变种。在掌握这些基础后,我们将在第四章引入贝叶斯深度学习中开始探索 BDL。在第五章第六章中,我们将深入探讨 BDL;首先学习一些有原则的方法,然后继续了解更多用于逼近贝叶斯神经网络的实用方法。

第七章 贝叶斯深度学习的实际考虑 中,我们将探讨一些贝叶斯深度学习的实际应用考虑,帮助我们理解如何最好地将这些方法应用于现实问题。在 第八章 应用贝叶斯深度学习 中,我们应该已经对核心的贝叶斯深度学习方法有了扎实的理解,并通过一系列实际的示例进一步巩固这一知识。最后,在 第九章 贝叶斯深度学习的下一步 中,我们将概述贝叶斯深度学习领域当前面临的挑战,并对技术的发展方向有所了解。

在本书的大多数章节中,理论内容将配以实践示例,帮助你通过自己动手实现这些方法来加深理解。为了跟随这些编码示例,你需要设置好 Python 环境并安装所需的先决条件。我们将在下一节中介绍这些内容。

1.5 设置工作环境

为了完成本书中的实践部分,你需要一个包含必要先决条件的 Python 3.9 环境。我们建议使用 conda,它是一个专为科学计算应用设计的 Python 包管理器。要安装 conda,只需访问 conda.io/projects/conda/en/latest/user-guide/install/index.html,并按照你的操作系统的安装说明进行操作。

安装了 conda 后,你可以设置用于本书的 conda 环境:


conda create -n bdl python=3.9

当你按下 Enter 键执行此命令时,系统会询问是否继续安装所需的软件包;只需输入 y 并按 Enterconda 将继续安装核心软件包。

你现在可以通过输入以下命令激活你的环境:


conda activate bdl

你现在会看到你的 shell 提示符中包含 bdl,这表示你的 conda 环境已激活。现在,你可以开始安装本书所需的先决条件了。本书所需的关键库如下:

  • NumPy:数值 Python,或 NumPy,是 Python 中进行数值编程的核心库。你可能已经非常熟悉这个库了。

  • SciPy:SciPy,或科学 Python,提供了科学计算应用程序所需的基础包。整个科学计算栈,包括 SciPy、matplotlib、NumPy 和其他库,通常被称为 SciPy 栈。

  • scikit-learn:这是核心的 Python 机器学习库。基于 SciPy 栈,它提供了许多流行机器学习方法的易用实现。它还提供了大量的辅助类和函数用于数据加载和处理,我们将在本书中多次使用。

  • TensorFlow:TensorFlow 与 PyTorch 和 JAX 一起,是流行的 Python 深度学习框架之一。它提供了开发深度学习模型所需的工具,并将在本书中的许多编程示例中提供基础。

  • TensorFlow Probability:基于 TensorFlow,TensorFlow Probability 提供了处理概率神经网络所需的工具。我们将在本书中使用它与 TensorFlow 一起进行许多贝叶斯神经网络的示例。

要安装本书所需的所有依赖项,请在激活 conda 环境后输入以下命令:


conda install -c conda-forge scipy sklearn matplotlib seaborn 
tensorflow tensorflow-probability

让我们总结一下所学内容。

1.6 总结

在本章中,我们回顾了深度学习的成功,重新认识了它巨大的潜力以及它在当今技术中的普遍存在。我们还探索了一些它不足之处的关键例子:深度学习未能解决的场景,展示了潜在的灾难性后果。虽然 BDL 无法消除这些风险,但它可以帮助我们构建更强健的机器学习系统,结合深度学习的灵活性与贝叶斯推理的谨慎。

在下一章中,我们将深入探讨贝叶斯推理和概率的一些核心概念,为我们进入 BDL 做准备。

第二章

贝叶斯推理基础

在我们开始探讨使用深度神经网络DNNs)进行贝叶斯推理之前,我们应该花一些时间理解基本原理。在本章中,我们将进行这样的探讨:探索贝叶斯建模的核心概念,并了解一些常用的贝叶斯推理方法。在本章结束时,你应该能很好地理解我们为何使用概率建模,以及我们在良好原则化的或良好条件化的模型中寻求什么样的特性。

本内容将涵盖以下部分:

  • 刷新我们对贝叶斯建模的理解

  • 通过采样进行贝叶斯推理

  • 探索高斯过程

2.1 刷新我们对贝叶斯建模的理解

贝叶斯建模关注的是在一些先验假设和观察数据的基础上理解事件发生的概率。先验假设描述了我们对事件的初步信念或假设。例如,假设我们有两个六面骰子,并且我们想预测两个骰子和为 5 的概率。首先,我们需要了解有多少种可能的结果。因为每个骰子有 6 面,所有可能的结果数是 6 × 6 = 36。为了计算掷出和为 5 的可能性,我们需要算出哪些数值的组合加起来是 5:

PIC

图 2.1:展示掷出两个六面骰子时所有数值和为 5 的情况

如我们所见,有 4 种组合的和为 5,因此两个骰子和为 5 的概率是!-4 36,即1 9。我们称这个初步的信念为先验。现在,如果我们加入来自观察的信息会发生什么呢?假设我们知道其中一个骰子的值是 3,这就将我们可能的值缩小到 6,因为我们只剩下另一个骰子要掷,为了使结果为 5,我们需要另一个骰子的值是 2。

PIC

图 2.2:展示剩余的值,在掷出第一个骰子后和为 5

因为我们假设骰子是公平的,现在两个骰子和为 5 的概率是1 6。这个概率,称为后验,是通过我们的观察得到的信息计算出来的。贝叶斯统计的核心是贝叶斯定理(因此称为“贝叶斯”),我们用它来在有一些先验知识的情况下确定后验概率。贝叶斯定理定义如下:

P(A |B ) = P(B-|A)×-P-(A)- P(B )

在这里,我们可以定义P(A|B)为P(d[1] + d[2] = 5|d[1] = 3),其中d[1]和d[2]分别代表骰子 1 和骰子 2。我们可以通过之前的例子看到这一点。从似然开始,即分子左侧的项,我们看到:

1- P (B |A) = P (d1 = 3|d1 + d2 = 5) = 4

我们可以通过查看我们的网格来验证这一点。移动到分子部分的第二部分——先验——我们看到:

 4 1 P(A ) = P (d1 + d2 = 5) =--= -- 36 9

在分母部分,我们有我们的归一化常数(也称为边际似然),它就是:

P(B ) = P (d1 = 3) = 1 6

将这一切结合在一起使用贝叶斯定理,我们得到:

 14 × 19 1 P(d1 + d2 = 5|d1 = 3) = --1---= 6- 6

这里我们得到的是,如果我们知道一个骰子的值,结果为 5 的概率。然而,在本书中,我们将经常提到不确定性,而不是概率——并且学习如何通过深度神经网络(DNN)获得不确定性估计。这些方法属于更广泛的不确定性量化类别,旨在量化来自机器学习模型预测中的不确定性。也就是说,我们希望预测 P(ŷ|𝜃),其中 ŷ 是模型的预测值,而 𝜃 代表模型的参数。

正如我们从基础概率论中知道的那样,概率是介于 0 和 1 之间的。越接近 1,事件发生的可能性就越大——或称为概率越高。我们可以将我们的不确定性视为从 1 中减去概率。在这里的示例中,和为 5 的概率是 P(d[1] + d[2] = 5|d[1] = 3) = 1 6 = 0.166。所以,我们的不确定性就是 1 −16 = 56 = 0.833,意味着结果不会是 5 的概率大于 80%。随着我们深入本书,我们将学习不同的不确定性来源,以及如何利用不确定性来开发更强大的深度学习系统。

让我们继续使用我们的骰子示例,以更好地理解模型不确定性估计。许多常见的机器学习模型基于最大似然估计MLE)。也就是说,它们寻求预测最可能的值:在训练过程中调节其参数,以根据一些输入 x 产生最可能的输出 ŷ。作为简单的说明,假设我们要预测 d[1] + d[2] 的值,给定 d[1] 的值。我们可以简单地将其定义为期望,即在 d[1] 条件下,d[1] + d[2] 的期望值:

ˆy = 𝔼 [d + d |d ] 1 2 1

即,d[1] + d[2] 的可能值的均值

设置 d[1] = 3,我们可能得到的 d[1] + d[2] 的值为 {4,5,6,7,8,9}(如图 2.2所示),使得我们的均值:

 1 ∑6 4+ 5 + 6+ 7+ 8 + 9 μ = -- ai = --------------------= 6.5 6 i=1 6

这是我们从简单线性模型中得到的值,比如由以下定义的线性回归模型:

ˆy = βx + ξ

在这种情况下,我们的交集和偏差值为β = 1,ξ = 3.5。如果我们将d[1]的值更改为 1,我们会看到均值变为 4.5——即d[1] + d[2]|d[1] = 1 的可能值集合的均值,换句话说,{2,3,4,5,6,7}。这种对我们模型预测的视角很重要:虽然这个例子非常简单,但同样的原则适用于更复杂的模型和数据。我们在机器学习模型中通常看到的值是期望值,也就是均值。如你所知,均值通常被称为第一统计矩——而第二统计矩就是方差,方差使我们能够量化不确定性。

我们简单示例的方差定义如下:

 ∑6 2 σ2 = --i=1(ai −-μ) n − 1

这些统计量对你应该是熟悉的,方差在这里表示为标准差的平方,σ。对于我们的示例,这里假设d[2]是一个公平的骰子,方差始终是常数:σ² = 2.917。也就是说,给定任何d[1]的值,我们知道d[2]的所有值都是同等可能的,因此不确定性不会变化。但如果我们有一个不公平的骰子d[2],它有 50%的概率掷出 6,且每个其他数字的概率是 10%呢?这将改变我们的均值和方差。我们可以通过看如何表示这一点作为一组可能的值(换句话说,骰子的一个完美样本)来看到这一点——d[1] + d[2]|d[1] = 1 的可能值集合现在变为{2,3,4,5,6,7,7,7,7,7}。我们的新模型现在将有一个偏差ξ = 4.5,这样我们的预测就变为:

ˆy = 1 × 1 + 4.5 = 5.5

我们看到,期望值由于骰子d[1]的基础概率的变化而增加。然而,这里重要的区别在于方差值的变化:

 ∑10 (a − μ)2 σ2 = --i=1--i----- = 3.25 n − 1

我们的方差已经增加。由于方差本质上给出了每个可能值与均值的距离的平均值,这并不令人惊讶:考虑到加权骰子,结果更有可能远离均值,而不像不加权骰子那样,因此我们的方差增加。总结来说,在不确定性方面:结果距离均值越远的可能性越大,不确定性也就越大。

这对我们如何解读机器学习模型(以及更广泛的统计模型)预测结果具有重要意义。如果我们的预测是均值的近似值,而我们的不确定性量化了某个结果偏离均值的可能性,那么我们的不确定性告诉我们我们的模型预测错误的可能性有多大。因此,模型的不确定性让我们能够决定何时信任预测,何时需要更加谨慎。

这里给出的例子非常基础,但应该能帮助你理解我们希望通过模型不确定性量化实现的目标。随着我们学习一些贝叶斯推断的基准方法,我们将继续探索这些概念,了解它们如何应用于更复杂的现实问题。我们将从可能是贝叶斯推断最基本的方法——采样开始。

2.2 通过采样进行贝叶斯推断

在实际应用中,无法精确知道某个结果会是什么,类似地,也无法观察到所有可能的结果。在这种情况下,我们需要根据已有的证据做出最佳估计。证据由样本构成——即可能结果的观察。广义而言,机器学习的目标是学习能够从数据子集良好泛化的模型。贝叶斯机器学习的目标是在做到这一点的同时,还要提供一个模型预测的不确定性估计。在这一部分中,我们将学习如何利用采样来实现这一点,同时也会了解为什么采样可能不是最合理的方法。

2.2.1 近似分布

从最基础的层面来看,采样就是在进行分布的近似。假设我们想知道纽约人身高的分布。我们可以去测量每个人的身高,但那将涉及到测量 840 万人!虽然这会给我们最准确的答案,但这种方法在实践中是极为不切实际的。

相反,我们可以从总体中进行采样。这给我们提供了一个基本的蒙特卡洛采样的例子,在这个过程中,我们通过随机采样提供数据,从而近似一个分布。例如,假设我们有一个纽约居民的数据库,我们可以随机选择一部分居民,并用这些数据来近似所有居民的身高分布。通过随机采样——以及任何采样——这种近似的准确性取决于子总体的大小。我们希望实现的是一个统计显著的子样本,从而使我们对近似结果有信心。

为了更好地理解这一点,我们将通过从截尾正态分布生成 10 万个数据点来模拟这个问题,以近似看到 10 万人口的身高分布。假设我们随机抽取 10 个样本。这里是我们的分布图(右侧)与真实分布图(左侧)的对比:

图片

图 2.3:真实分布图(左)与样本分布图(右)的绘制。

从这里我们可以看出,这并不是真实分布的一个很好的表示:这里看到的更接近一个三角形分布而不是一个截尾正态分布。如果我们仅仅基于这个分布推断人口的身高,我们会得出许多不准确的结论,比如忽略了 200 厘米以上的截尾,以及分布左侧的尾部。

通过增加样本量,我们可以得到更好的估计 —— 让我们尝试抽取 100 个样本:

图片

图 2.4:真实分布图(左)与样本分布图(右)的绘制。

情况开始好转:我们开始看到左侧的一些尾部以及朝 200 厘米处的截尾。然而,这个样本从某些区域采样更多,导致了误代表:我们的平均数被拉低了,我们看到了两个明显的高峰,而不是真实分布中的单一高峰。让我们将样本量增加一个数量级,扩展到 1,000 个样本:

图片

图 2.5:真实分布图(左)与样本分布图(右)的绘制。

现在看起来好多了 —— 通过一个比真实人口规模小一百倍的样本集,我们现在看到的分布与真实分布非常接近。这个例子展示了通过随机抽样,我们可以使用显著较小的观察池来近似真实分布。但是,这个池子仍然必须包含足够的信息,以使我们能够得出对真实分布良好近似的结论:样本太少,我们的子集将统计上不足,导致对潜在分布的近似不良。

但是,简单随机采样并不是逼近分布的最实际方法。为了实现这一点,我们转向概率推断。给定一个模型,概率推断提供了一种方法来找到最能描述我们数据的模型参数。为此,我们需要首先定义模型的类型——这就是我们的先验。在我们的例子中,我们使用截断的高斯分布:基于我们的直觉,假设人类的身高遵循正态分布是合理的,但很少有人会超过 6’5” (约 196 厘米)。因此,我们将指定一个截断的高斯分布,设定上限为 205 厘米,或者说稍超过 6’5”。由于它是一个高斯分布,换句话说,𝒩(μ,σ),我们的模型参数是𝜃 = {μ,σ}——并且有额外的约束条件,即我们的分布的上限为b = 205。

这让我们进入了一类基础的算法:马尔科夫链蒙特卡罗Markov Chain Monte Carlo,或简称MCMC)方法。与简单随机采样一样,这些方法也允许我们构建真实底层分布的图像,但它们是顺序进行的,每个样本都依赖于之前的样本。这种顺序依赖性被称为马尔科夫性质,因此名字中的马尔科夫链部分。这个顺序方法考虑了样本之间的概率依赖性,使我们能够更好地逼近概率密度。

MCMC 通过顺序随机采样实现这一点。就像我们熟悉的随机采样一样,MCMC 从我们的分布中随机采样。但与简单随机采样不同,MCMC 考虑的是成对的样本:一些先前的样本 x[t−1] 和一些当前的样本 x[t]。对于每一对样本,我们有一些标准来指定是否保留该样本(这个标准取决于具体的 MCMC 变体)。如果新值符合这个标准,比如说 x[t] 比我们的先前值 x[t−1] “更优”,那么该样本会被加入到链中,并成为下一轮的 x[t]。如果样本不符合标准,我们则会保留当前的 x[t] 作为下一轮的样本。我们会在(通常是很大的)迭代次数中重复这个过程,最终应该能够得到我们分布的良好近似。

结果是一个高效的采样方法,能够密切地逼近我们分布的真实参数。让我们看看它是如何应用于我们的身高分布示例的。使用仅有 10 个样本的 MCMC,我们得到了以下的近似结果:

PIC

图 2.6:真实分布(左)与通过 MCMC 得到的近似分布(右)的对比图

十个样本的表现不错——显然比我们通过简单随机采样得到的三角分布要好得多。让我们看看使用 100 个样本的效果:

PIC

图 2.7:真实分布(左)与通过 MCMC 得到的近似分布(右)的对比图

这个结果看起来相当不错——实际上,通过使用 100 个 MCMC 样本,我们能够获得比使用 1,000 个简单随机样本更好的分布近似。如果我们继续增加样本数量,我们将得到越来越接近真实分布的近似值。但我们的简单例子并没有完全展现 MCMC 的强大:MCMC 的真正优势在于能够近似高维分布,这使得它成为在各种领域中近似难以求解的高维积分的宝贵技术。

本书的重点是如何估计机器学习模型参数的概率分布——这使我们能够估计与预测相关的不确定性。在下一节中,我们将实际演示如何通过将采样应用于贝叶斯线性回归来完成这一任务。

2.2.2 使用贝叶斯线性回归实现概率推断

在典型的线性回归中,我们希望使用线性函数f(x)从某个输入x预测输出ŷ,使得ŷ = βx + ξ。在贝叶斯线性回归中,我们以概率的方式进行预测,引入了另一个参数σ²,使得我们的回归方程变为:

ŷ = 𝒩 (x β + ξ,σ² )

也就是说,ŷ服从高斯分布。

在这里,我们看到了我们熟悉的偏置项ξ和截距β,并引入了方差参数σ²。为了拟合我们的模型,我们需要为这些参数定义先验——就像我们在上一节中的 MCMC 示例一样。我们将这些先验定义为:

ξ ≈ 𝒩 (0,1 ) β ≈ 𝒩 (0,1) σ² ≈ |𝒩 (0,1)|

请注意,方程 2.15 表示的是高斯分布的半正态分布(零均值高斯的正半部分,因为标准差不能为负)。我们将模型参数表示为𝜃 = β,ξ,σ²,并使用采样来找到最大化给定数据下这些参数似然的参数,换句话说,就是在给定数据D的条件下,我们参数的条件概率P(𝜃|D)。

有多种 MCMC 采样方法可以用来寻找我们的模型参数。一种常见的方法是使用Metropolis-Hastings算法。Metropolis-Hastings 特别适合从难以求解的分布中进行采样。它通过使用一个提议分布Q(𝜃′|𝜃)来实现这一点,该分布与我们的真实分布成比例,但并不完全相同。这意味着,例如,如果某个值x[1]在真实分布中是另一个值x[2]的两倍可能性,那么在我们的提议分布中也会是如此。由于我们关心的是观察结果的概率,我们不需要知道在真实分布中的精确值——我们只需要知道我们的提议分布在比例上与真实分布等价。

下面是我们贝叶斯线性回归中 Metropolis-Hastings 的关键步骤。

首先,我们从根据每个参数的先验分布在参数空间中随机选择一个点 𝜃 作为初始化。然后,使用以我们的第一个参数集 𝜃 为中心的高斯分布,选择一个新的点 𝜃′。接着,对于每次迭代 tT,执行以下操作:

  1. 计算接受准则,定义为:

     P(𝜃′|D ) α = -------- P(𝜃|D )

  2. 从均匀分布 𝜖 ∈ [0,1] 中生成一个随机数。如果 𝜖 < = α,则接受新的候选参数——将这些加入到链中,设 𝜃 = 𝜃′。如果 𝜖 > α,则保持当前的 𝜃 并重新抽取一个新值。

这个接受准则意味着,如果我们的新参数集比上一个参数集具有更高的似然性,我们会看到 α > 1,在这种情况下 α < 𝜖。这意味着,当我们根据数据采样得到的参数更“可能”时,我们总是会接受这些参数。另一方面,如果 α < 1,虽然有可能会拒绝这些参数,但我们也可能会接受它们——允许我们探索低似然区域。

Metropolis-Hastings 的这些机制产生的样本可用于计算我们后验分布的高质量近似值。在实际操作中,Metropolis-Hastings(以及更一般的 MCMC 方法)需要一个预热阶段——一个初步采样阶段,用于逃离低密度区域,这些区域通常在给定任意初始化时会遇到。

让我们将其应用于一个简单的问题:我们将为函数 y = x² + 5 + η 生成一些数据,其中 η 是一个根据 η ≈𝒩(0,5) 分布的噪声参数。使用 Metropolis-Hastings 来拟合我们的贝叶斯线性回归器,得到以下拟合结果,该拟合使用从我们的函数中采样的点(通过交叉点表示):

图片

图 2.8:在低方差生成数据上的贝叶斯线性回归

我们看到我们的模型以标准线性回归预期的方式拟合数据。然而,与标准线性回归不同,我们的模型产生了预测不确定性:这通过阴影区域表示。这种预测不确定性提供了关于我们基础数据变化程度的一个近似;这使得这个模型比标准线性回归更有用,因为我们现在不仅可以得到数据的趋势,还能获取数据的变化幅度。如果我们生成新数据并重新拟合,增加数据的标准差,可以通过修改噪声分布为 η ≈𝒩(0,20) 来实现:

图片

图 2.9:在高方差生成数据上的贝叶斯线性回归

我们看到,预测的不确定性与数据的标准差成正比增加。这是不确定性感知方法中的一个重要特性:当不确定性较小时,我们知道预测与数据拟合得很好;而当不确定性较大时,我们知道需要谨慎对待预测,因为这表明模型在该区域的拟合效果较差。我们将在下一节中看到一个更好的例子,展示数据更多或更少的区域如何影响我们模型的不确定性估计。

在这里,我们看到我们的预测与数据拟合得相当好。此外,我们还看到σ²根据不同区域中数据的可用性而变化。我们在这里看到的是一个非常重要的概念的极好例子,良好校准的不确定性——也称为高质量 不确定性。这意味着,在预测不准确的区域,我们的不确定性也很高。如果我们在预测不准确的区域过于自信,或者在预测准确的区域过于不确定,我们的不确定性估计就是校准不良的。由于其良好的校准性,采样常常被作为不确定性量化的基准。

不幸的是,尽管采样对于许多应用非常有效,但每个参数需要获得多个样本,这意味着当参数维度很高时,采样很快变得在计算上无法承受。例如,如果我们想为复杂的非线性关系(例如神经网络权重的采样)开始进行采样,那么采样就不再具有实用性。尽管如此,它在某些情况下仍然有用,稍后我们将看到各种 BDL 方法如何利用采样。

在下一节中,我们将探讨高斯过程——一种贝叶斯推断的基本方法,并且是一种不像采样方法那样遭受计算开销的技术。

2.3 探索高斯过程

正如我们在前一节中所看到的,采样很快会变得非常昂贵。为了解决这个问题,我们可以使用专门设计的机器学习模型来产生不确定性估计——其中最为经典的就是高斯 过程

高斯过程,或称GP,已经成为一种基础的概率论机器学习模型,广泛应用于从药理学到机器人学等多个领域。它的成功主要归功于其能够以一种科学的方式生成对预测结果的高质量不确定性估计。那么,什么是高斯过程呢?

本质上,GP 是一个函数的分布。为了理解我们所说的这一点,我们来看一个典型的机器学习用例。我们希望学习某个函数 f(x),它将一系列输入 x 映射到一系列输出 y,使得我们能够通过 y = f(x) 来近似我们的输出。在我们看到任何数据之前,我们对潜在的函数一无所知;它可能是无限多种可能的函数之一:

PIC

图 2.10:在看到数据之前,可能函数空间的示意图

这里,黑线表示我们希望学习的真实函数,而虚线表示在给定数据(在这种情况下,没有数据)下可能的函数。一旦我们观察到一些数据,就会发现可能函数的数量变得更加有限,如下所示:

PIC

图 2.11:在观察到一些数据后,可能函数空间的示意图

在这里,我们看到所有可能的函数都经过我们观察到的数据点,但在这些数据点之外,我们的函数会取一系列非常不同的值。在一个简单的线性模型中,我们不关心这些可能值的偏差:我们乐于在一个数据点和另一个数据点之间进行插值,正如我们在 2.12 中看到的那样:

PIC

图 2.12:通过我们的观测值进行线性插值的示意图

但是这种插值可能导致非常不准确的预测,并且无法考虑与模型预测相关的不确定性程度。我们在没有数据点的区域看到的偏差,正是我们希望通过高斯过程捕捉的内容。当我们的函数可以取不同的可能值时,就会有不确定性——通过捕捉这种不确定性的程度,我们能够估计这些区域内可能的变化。

从形式上讲,高斯过程可以定义为一个函数:

f(x) ≈ GP (m (x),k(x,x′))

这里,m(x) 仅仅是给定点 x 上我们可能函数值的均值:

m (x) = 𝔼[f (x)]

下一个项,k(x,x′) 是协方差函数,或称为核函数。这是高斯过程(GP)的一个基本组成部分,因为它定义了我们如何建模数据中不同点之间的关系。高斯过程使用均值和协方差函数来建模可能函数的空间,从而生成预测及其相关的不确定性。现在我们已经介绍了一些高层次的概念,接下来让我们更深入地理解它们如何建模可能函数的空间,并估计不确定性。为此,我们需要理解高斯过程的先验。

2.3.1 使用核函数定义我们的先验信念

高斯过程的核函数描述了我们对数据的先验信念,因此你会经常看到它们被称为高斯过程先验。就像方程 2.3 中的先验告诉我们关于两个骰子投掷结果的概率一样,高斯过程先验告诉我们有关我们期望从数据中得到的关系的重要信息。

虽然有一些高级方法可以根据我们的数据推断先验,但它们超出了本书的范围。我们将专注于高斯过程的更传统用法,其中我们使用对所处理数据的了解来选择先验。

在文献中以及你遇到的任何实现中,你会发现高斯过程先验常常被称为核函数协方差函数(就像我们在这里一样)。这三个术语是可以互换的,但为了与其他工作的术语一致,今后我们将称之为核函数。核函数提供了一种计算两个数据点之间距离的方法,通常表示为 k(x, x′),其中 xx′ 是数据点,而 k() 表示核函数。虽然核函数可以有多种形式,但有少数几种基本核函数在大量高斯过程应用中被广泛使用。

也许最常见的核函数是平方指数径向基函数RBF)核函数。这个核函数的形式是:

 (x − x ′)² k(x,x ′) = σ²exp − ----2---- 2l

这引入了几个常见的核参数:lσ²。输出方差参数 σ² 只是一个缩放因子,用于控制函数与其均值之间的距离。长度尺度参数 l 控制函数的平滑度——换句话说,控制函数在特定维度上的变化幅度。这个参数可以是一个标量,应用于所有输入维度,或者是一个向量,每个输入维度有不同的标量值。后者通常是通过自动相关性 确定ARD)实现的,该方法识别输入空间中的相关值。

高斯过程通过基于核函数的协方差矩阵进行预测——本质上是将新数据点与之前观察到的数据点进行比较。然而,就像所有机器学习模型一样,高斯过程也需要训练,这就是长度尺度发挥作用的地方。长度尺度构成了我们高斯过程的参数,通过训练过程,它学习长度尺度的最佳值。这通常通过使用非线性优化器来完成,例如Broyden-Fletcher-Goldfarb-ShannoBFGS)优化器。可以使用多种优化器,包括你可能熟悉的深度学习优化器,例如随机梯度下降及其变种。

让我们看看不同的核函数如何影响高斯过程预测。我们从一个简单的示例开始——一个简单的正弦波:

PIC

图 2.13:带有四个采样点的正弦波图

我们可以看到这里展示的函数,以及从这个函数中采样的一些点。现在,让我们为数据拟合一个具有周期性核的高斯过程。周期性核函数定义为:

 ′ 2 ( 2sin²(π |x − x′|∕p)) kper(x, x) = σ exp -------l²--------

在这里,我们看到一个新的参数:p。这只是周期函数的周期。设定 p = 1,并将具有周期性核的高斯过程应用到前面的示例中,我们得到如下结果:

PIC

图 2.14:周期性核函数预测结果图,p = 1

这看起来相当嘈杂,但你应该能够看到后验生成的函数中存在明显的周期性。它之所以嘈杂,原因有几个:数据不足和先验较差。如果数据有限,我们可以尝试通过改善先验来解决这个问题。在这种情况下,我们可以利用我们对函数周期性的知识,通过设置p = 6 来改进我们的先验:

PIC

图 2.15:周期性核函数的后验预测图,p = 6

我们看到这与数据拟合得相当好:我们仍然在数据稀缺的区域存在不确定性,但后验的周期性现在看起来合理。这之所以能实现,是因为我们使用了一个信息丰富的先验;即,包含了描述数据的关键信息的先验。这个先验由两个关键部分组成:

  • 我们的周期性核函数

  • 我们关于该函数周期性的知识

如果我们将高斯过程修改为使用 RBF 核函数,就能清楚看到这个差异:

PIC

图 2.16:RBF 核函数的后验预测图

使用 RBF 核函数时,我们看到情况又变得相当混乱:由于我们只有有限的数据和较差的先验,我们无法恰当地约束可能的函数空间以拟合我们的真实函数。在理想情况下,我们可以通过使用更合适的先验来解决这一问题,就像在 2.15中看到的那样——但这并不总是可行的。另一种解决方案是增加数据量。继续使用我们的 RBF 核函数,我们从函数中采样了 10 个数据点并重新训练了我们的高斯过程(GP):

PIC

图 2.17:基于 10 个观测值训练的 RBF 核函数的后验预测图

这看起来好多了——但如果我们有更多数据并且一个信息丰富的先验呢?

PIC

图 2.18:周期性核函数的后验预测图,p = 6,基于 10 个观测值训练

现在后验预测非常接近真实函数。因为我们没有无限的数据,仍然存在一些不确定区域,但不确定性相对较小。

现在我们已经看到一些核心原理的实际应用,让我们回到 2.10-2.12中的例子。这里是我们目标函数、后验样本和之前看到的线性插值的快速回顾:

PIC

图 2.19:展示线性插值与真实函数差异的图

现在我们对高斯过程(GP)如何影响预测后验有了一些了解,很容易看出线性插值远远达不到我们通过高斯过程实现的效果。为了更清楚地说明这一点,让我们看看在给定三个样本的情况下,高斯过程预测这个函数的结果:

PIC

图 2.20:展示高斯过程预测与真实函数差异的图

在这里,虚线表示我们高斯过程(GP)的均值 (μ) 预测值,阴影区域表示与这些预测值相关的不确定性——即均值周围的标准差 (σ)。让我们将 2.20 中看到的内容与 2.19 进行对比。差异一开始可能看起来很微妙,但我们可以清楚地看到,这不再是一个简单的线性插值:高斯过程的预测值正在“拉”向我们的实际函数值。就像我们之前的正弦波示例一样,高斯过程预测的行为受两大关键因素的影响:先验(或核函数)和数据。

但在 2.20 中还展示了另一个至关重要的细节:我们高斯过程(GP)的预测不确定性。我们可以看到,与许多典型的机器学习模型不同,高斯过程会给出与其预测相关的不确定性。这意味着我们可以更好地决定如何使用模型的预测——拥有这些信息将帮助我们确保我们的系统更为稳健。例如,如果不确定性太大,我们可以回退到手动系统。我们甚至可以追踪那些具有较高预测不确定性的数据点,以便不断改进我们的模型。

我们可以通过增加一些观测点来看到这种改进如何影响我们的预测——就像我们在之前的示例中做的那样:

PIC

图 2.21:展示在 5 个观测点上训练的高斯过程(GP)预测与真实函数之间差异的图表

2.21 展示了我们的不确定性如何在不同数量观测的区域之间变化。我们可以看到,在 x = 3 和 x = 4 之间,我们的不确定性相当高。这很有道理,因为我们也可以看到,我们的高斯过程(GP)的均值预测值与真实函数值存在较大偏差。相反,如果我们查看 x = 0.5 和 x = 2 之间的区域,我们可以看到我们的 GP 预测值与真实函数非常接近,并且我们对这些预测也更有信心,因为我们可以从这个区域的不确定性间隔较小中看出。

我们在这里看到的是一个非常重要概念的典范:良好 校准的不确定性——也称为 高质量的不确定性。这指的是在我们预测不准确的区域,我们的不确定性也很高。如果我们在预测不准确的区域非常自信,或在预测准确的区域非常不确定,那么我们的不确定性估计就是 校准不良的

高斯过程(GP)是一种我们可以称之为 原理明确 的方法——这意味着它们有坚实的数学基础,因此具有强大的理论保障。其中之一是它们具有良好的校准性,这也是高斯过程如此受欢迎的原因:如果我们使用高斯过程,我们知道可以依赖它们的不确定性估计。

不幸的是,GP 并非没有缺点——我们将在接下来的章节中详细了解这些问题。

2.3.2 高斯过程的局限性

考虑到 GP 具有良好的原理基础,并且能够生成高质量的不确定性估计,您可能会认为它们是完美的、不确定性感知型机器学习模型。然而,GP 在几个关键情境下表现不佳:

  • 高维数据

  • 大量数据

  • 高度复杂的数据

这里的前两点主要归因于 GP 在扩展性上的不足。为了理解这一点,我们只需要看看 GP 的训练和推理过程。尽管详细讨论这些内容超出了本书的范围,但关键点在于 GP 训练所需的矩阵运算。

在训练过程中,需要对一个 D × D 的矩阵进行求逆,其中 D 是数据的维度。因此,GP 的训练很快变得计算开销巨大。通过使用 Cholesky 分解来代替直接矩阵求逆,可以在一定程度上缓解这一问题。Cholesky 分解不仅在计算上更高效,而且数值稳定性也更好。不幸的是,Cholesky 分解也有其不足之处:在计算上,它的复杂度是 O(n³)。这意味着,随着数据集大小的增加,GP 训练变得越来越昂贵。

但受影响的不仅仅是训练过程:因为在推理时我们需要计算新数据点与所有已观察数据点之间的协方差,GP(高斯过程)在推理时的计算复杂度为 O(n²)。

除了计算成本,GP 在内存使用上也不轻便:因为我们需要存储协方差矩阵 K,GP 的内存复杂度为 O(n²)。因此,在大数据集的情况下,即使我们拥有训练它们所需的计算资源,由于其内存需求,使用它们进行实际应用可能也不现实。

我们清单中的最后一点涉及数据的复杂性。正如你可能已经知道的——并且我们将在 第三章 深度学习基础 中提到——DNN(深度神经网络)的一个主要优点是它们能够通过层层非线性变换处理复杂的高维数据。虽然 GP 很强大,但它们也是相对简单的模型,无法学习 DNN 所能实现的强大特征表示。

所有这些因素表明,虽然 GP 对于相对低维数据和较小的数据集是一个很好的选择,但对于我们在机器学习中面临的许多复杂问题来说,它们并不实用。因此,我们转向 BDL 方法:这些方法具备深度学习的灵活性和可扩展性,同时还能够提供模型的不确定性估计。

2.4 总结

在本章中,我们介绍了一些与贝叶斯推断相关的基本概念和方法。首先,我们回顾了贝叶斯定理和概率论的基础知识——使我们能够理解不确定性的概念,以及如何将其应用于机器学习模型的预测中。接下来,我们介绍了抽样和一个重要的算法类别:马尔可夫链蒙特卡洛方法(MCMC)。最后,我们讲解了高斯过程,并阐明了良好校准不确定性的关键概念。这些核心主题将为后续内容提供必要的基础,然而,我们鼓励您阅读推荐的参考材料,以便更全面地学习本章中介绍的主题。

在下一章中,我们将看到深度神经网络(DNN)如何在过去十年中改变机器学习的格局,探索深度学习带来的巨大优势,以及 BDL 方法发展的动机。

2.5 进一步阅读

为了提高高斯过程(GP)的灵活性和可扩展性,正在探索多种技术——例如深度高斯过程(Deep GPs)或稀疏高斯过程(Sparse GPs)。以下资源探讨了其中一些主题,并对本章内容进行了更深入的阐述:

  • 《Python 贝叶斯分析》,马丁:这本书全面覆盖了统计建模和概率编程的核心主题,包括对各种抽样方法的实用演示,以及对高斯过程和其他多种贝叶斯分析核心技术的良好概述。

  • 《机器学习中的高斯过程》,拉斯穆森和威廉姆斯:这本书通常被视为高斯过程的权威教材,提供了高斯过程背后理论的详细解释。对于任何认真学习贝叶斯推断的人来说,这是一本关键的教材。

第三章

深度学习基础

在全书中,当我们学习如何将贝叶斯方法及其扩展应用于神经网络时,将会遇到不同的神经网络架构和应用。 本章将介绍常见架构类型,为稍后将贝叶斯扩展引入这些架构打下基础。我们还将回顾这些常见神经网络架构的一些局限性,特别是它们产生过度自信输出的倾向以及它们容易受到对抗性输入操控的影响。到本章结束时,您应该能够充分理解深度神经网络的基础知识,并知道如何用代码实现最常见的神经网络架构类型。这将帮助您跟上后续章节中的代码示例。

内容将在以下章节中讨论:

  • 介绍多层感知机

  • 审查神经网络架构

  • 理解典型神经网络的问题

3.1 技术要求

要完成本章的实践任务,您需要一个 Python 3.8 环境,并安装 pandasscikit-learn 堆栈,以及以下额外的 Python 包:

  • TensorFlow 2.0

  • Matplotlib 绘图库

本书的所有代码都可以在本书的 GitHub 仓库中找到:github.com/PacktPublishing/Enhancing-Deep-Learning-with-Bayesian-Inference

3.2 介绍多层感知机

深度神经网络是深度学习革命的核心。本节的目的是介绍深度神经网络的基本概念和构建块。首先,我们将回顾多层感知机MLP)的组成部分,并使用 TensorFlow 框架实现它。这将作为本书中其他代码示例的基础。如果您已经熟悉神经网络,并知道如何用代码实现它们,可以跳过,直接进入理解典型神经网络的问题部分,我们将在那里讨论深度神经网络的局限性。本章专注于架构构建块和原理,而不涉及学习规则和梯度。如果您需要更多关于这些主题的背景信息,我们推荐 Packt 出版社的 Sebastian Raschka 所著的优秀书籍 Python Machine Learning(特别是 第二章贝叶斯推断基础)。

MLP 是一种前馈型全连接神经网络。前馈型意味着信息在 MLP 中仅在一个方向上传递,从输入层传递到输出层;没有反馈连接。全连接意味着每个神经元与前一层的所有神经元都有连接。为了更好地理解这些概念,让我们看一下 3.1,它提供了 MLP 的示意图。在这个例子中,MLP 拥有一个包含三个神经元的输入层(红色表示),两个包含四个神经元的隐藏层(蓝色表示),以及一个包含单个输出节点的输出层(绿色表示)。例如,假设我们想要建立一个预测伦敦房价的模型。在这个例子中,三个输入神经元将代表模型的三个输入特征的值,如距离市中心的距离、房屋的楼面面积以及房屋的建造年份。如图中的黑色连接所示,这些输入值会传递到并被第一隐藏层的每个神经元汇总。然后,这些神经元的值会传递到并被第二隐藏层的神经元汇总,最后传递到输出神经元,该神经元将表示我们模型预测的房屋价值。

PIC

图 3.1:多层感知机的示意图

神经元聚合值究竟是什么意思?为了更好地理解这一点,让我们关注单个神经元及其对传递给它的值执行的操作。在 3.2中,我们展示了 3.1(左侧面板),并放大了第一隐藏层中的第一个神经元及其接收输入的神经元(中间面板)。在图的右面板中,我们略微重新排列了神经元,并将输入神经元命名为 x[1]、x[2] 和 x[3]。我们还明确标出了它们的连接,并分别将与之相关的权重命名为 w[1]、w[2] 和 w[3]。从图的右面板可以看到,一个人工神经元执行两个基本操作:

  1. 首先,它对输入进行加权平均(由 Σ 表示)。

  2. 其次,它对第一步的输出应用非线性函数(由 σ 表示。请注意,这里 σ 并不表示标准差——在本书的大部分内容中我们将使用 σ 表示标准差),例如使用 sigmoid 函数。

第一个操作可以更正式地表示为 z = ∑ [n=1]³ x[n] w[n]。第二个操作可以表示为 a = σ(z) =  1 1+e−-z。神经元的激活值 a = σ(z) 然后会传递给第二隐藏层中的神经元,重复相同的操作。

PIC

图 3.2:人工神经元在神经网络中的聚合与变换

现在我们已经回顾了 MLP 模型的不同部分,接下来让我们在 TensorFlow 中实现一个。首先,我们需要导入所有必要的函数。这些包括Sequential,用于构建像 MLP 这样的前馈模型,Input,用于构建输入层,以及Dense,用于构建全连接层:


from tensorflow.keras.models import Sequential, Input, Dense

配备了这些工具,实现 MLP 就变得简单了,只需按正确的顺序和正确的神经元数量链接Input层和Dense层:


multi_layer_perceptron = Sequential( 
[ 
# input layer with 3 neurons 
Input(shape=(3,)) 
# first hidden layer with 4 neurons 
Dense(4, activation="sigmoid"), 
# second hidden layer with 4 neurons 
Dense(4, activation="sigmoid"), 
# output layer 
Dense(1, activation="sigmoid"), 
] 
)

加权平均的聚合操作在使用Dense层对象时,由 TensorFlow 自动处理。此外,实现激活函数也变得非常简单,只需将所需函数的名称传递给Dense层的activation参数(如前面示例中的sigmoid)。

在我们转向 MLP 以外的其他神经网络架构之前,先谈一下“深度”这个词。一个神经网络被认为是深度的,如果它有多个隐藏层。例如,前面展示的 MLP 有两个隐藏层,可以视为一个深度神经网络。你可以添加更多的隐藏层,构建非常深的神经网络架构。训练这种深度架构有其自身的一套挑战,训练这类深度架构的科学(或艺术)被称为深度学习DL)。

在下一节中,我们将了解一些常见的深度神经网络架构,而在随后的章节中,我们将探讨与这些架构相关的实际挑战。

3.3 回顾神经网络架构

在上一节中,我们看到如何实现一个以 MLP 形式的全连接网络。虽然这种网络在深度学习的早期非常流行,但随着时间的推移,机器学习研究人员开发了更为复杂的架构,通过包含领域特定的知识(如计算机视觉或自然语言处理NLP))来取得更好的效果。在这一节中,我们将回顾一些最常见的神经网络架构,包括卷积神经网络CNNs)和递归神经网络RNNs),以及注意力机制和变换器。

3.3.1 探索 CNNs

回顾我们尝试使用 MLP 模型预测伦敦房价的例子,我们所使用的输入特征(距离市中心的距离、房屋的楼面面积以及建造年份)仍然是“手工设计”的,这意味着人类会根据问题来判断哪些输入可能与模型的价格预测相关。如果我们要构建一个模型,输入是图像并尝试预测图像中展示的对象,那些输入特征可能是什么样的呢?深度学习的一个突破性时刻是认识到神经网络可以直接从原始数据中学习并提取任务所需的最有用特征——在视觉对象分类的情况下,这些特征是直接从图像的像素中学习得到的。

如果我们想要从图像中提取最相关的输入特征用于物体分类任务,那么神经网络架构需要是什么样的呢?在尝试回答这个问题时,早期的机器学习研究者转向了哺乳动物的大脑。物体分类是我们的视觉系统相对轻松完成的任务。启发 CNN 发展的一个观察是,负责物体识别的哺乳动物视觉皮层实现了一个特征提取器的层级结构,这些提取器具有越来越大的感受野。感受 是生物神经元对图像中的区域的响应范围。视觉皮层早期层的神经元仅响应图像的相对小区域,而高层神经元则响应覆盖较大部分(甚至整个)输入图像的区域。

受大脑皮层层级结构的启发,CNN 实现了特征提取器的层级结构,其中高层的人工神经元具有更大的感受野。为了理解这个是如何工作的,让我们看一下 CNN 如何基于输入图像构建特征。 3.3展示了 CNN 中早期的卷积层如何对输入图像(左侧所示)进行操作,将特征提取到特征图中(右侧所示)。你可以将特征图想象成一个具有n行和m列的矩阵,特征图中的每个特征都是一个标量值。这个例子突出了卷积层在图像不同局部区域操作的两个实例。在第一个实例中,特征图中的特征接收来自小猫脸部的输入。在第二个实例中,特征接收来自小猫右爪的输入。最终的特征图将是将这个相同操作在输入图像的所有区域重复进行的结果,通过从左到右、从上到下滑动一个卷积核来填充特征图中的所有值。

PIC

图 3.3:从输入图像构建特征图

这样的单一操作在数值上是什么样的呢?这在 3.4中有说明。

图片

图 3.4:卷积层执行的数值操作

在这里,我们放大了输入图像的一部分,并明确了其像素值(左侧)。你可以想象,卷积核(如图中所示)一步一步地滑过输入图像。在图中显示的这一步,卷积核正在操作输入图像的左上角(以红色高亮)。给定输入图像的值和卷积核的值,特征图中的最终值(示例中的28)是通过加权平均得到的:输入图像中的每个值都由对应的卷积核值加权,从而得到 9 ∗ 0 + 3 ∗ 1 + 1 ∗ 0 + 4 ∗ 0 + 8 ∗ 2 + 5 ∗ 0 + 5 ∗ 1 + 2 ∗ 1 + 2 ∗ 1 = 28。

稍微正式一点,我们用x表示输入图像,w表示卷积核。卷积神经网络中的卷积操作可以表示为z = ∑ [i=1]^(n) ∑ [j=1]^(m)x[i,j]w[i,j]。通常,这之后会有一个非线性操作,a = σ(z),就像多层感知机(MLP)中一样。σ可以是之前介绍的 sigmoid 函数,但卷积神经网络(CNN)中更常用的选择是修正线性单元ReLU),其定义为ReLU(z) = max(0,z)。

在现代 CNN 中,许多这些卷积层会堆叠在一起,使得一个卷积层的输出特征图将作为下一个卷积层的输入(图像),如此往复。将卷积层按顺序排列,这使得 CNN 能够构建越来越抽象的特征表示。在研究层级中不同位置的特征图时,Matthew Zeiler 等人(见进一步阅读)发现,早期卷积层的特征图通常显示边缘和简单纹理,而后期卷积层的特征图则展示了更复杂的图案和物体部件。类似于视觉皮层的层级结构,后期卷积层中的神经元倾向于具有更大的感受野,因为它们从多个早期神经元接收输入,而这些神经元又从图像的不同局部区域接收输入。

堆叠在一起的卷积层的数量决定了卷积神经网络(CNN)的深度:层数越多,网络越深。另一个重要的维度是 CNN 的宽度,这由每层的卷积核数量决定。你可以想象,在某一卷积层上,我们可以应用多个卷积核,这将导致额外的特征图——每增加一个卷积核,就增加一张特征图。在这种情况下,后续卷积层中的卷积核需要是三维的,以处理输入中多个特征图,其中卷积核的第三维由输入特征图的数量决定。

与卷积层一起,CNN(卷积神经网络)的另一个常见组成部分是池化层,特别是均值池化最大池化层。这些层的作用是对输入进行子采样,从而减小图像的输入尺寸,进而减少网络中所需的参数数量(也减少了计算负担和内存占用)。

池化层是如何工作的?在 3.5中,我们看到均值池化(左)和最大池化(右)层的工作过程。我们可以看到,像卷积层一样,它们对输入的局部区域进行操作。它们执行的操作非常简单——要么取接收区域内像素值的均值,要么取最大值。

PIC

图 3.5: 池化层执行的数值操作

除了计算和内存方面的考虑外,池化层的另一个优点是它们可以使网络对输入的小变化更具鲁棒性。例如,假设示例中的一个输入像素值发生了变化,变为 0。这将对输出产生很小的影响(均值池化层)或根本没有影响(最大池化层)。

现在我们已经回顾了基本操作,接下来我们将在 TensorFlow 中实现一个 CNN。导入所有必要的函数,包括我们已经熟悉的Sequential函数,用于构建前馈模型,以及Dense层。此外,这次我们还导入了Conv2D用于卷积,MaxPooling2D用于最大池化。有了这些工具,我们可以通过按照正确顺序链接这些层函数来实现一个 CNN:


from tensorflow.keras import Sequential 
from tensorflow.keras.layers import Flatten, Conv2D, MaxPooling2D, Dense 

convolutional_neural_network = Sequential([ 
Conv2D(32, (3,3), activation="relu", input_shape=(28, 28, 1)), 
MaxPooling2D((2,2)), 
Conv2D(64, (3,3), activation="relu"), 
MaxPooling2D((2,2)),    Flatten(), 
Dense(64, activation="relu"), 
Dense(10) 
])

我们已经通过将一个包含 32 个核的卷积层与一个最大池化操作串联起来,接着是一个包含 64 个核的卷积层,再加上另一个最大池化操作,最终添加了两个Dense层,构建了一个 CNN。最终的Dense层将用于将输出神经元的数量与分类问题中的类别数量匹配。在前面的示例中,类别数量是10。我们的网络现在已经准备好进行训练。

CNN 已经成为解决各种问题的关键工具,成为从自动驾驶汽车到医学影像等广泛问题中系统的关键组成部分。它们还为其他重要的神经网络架构提供了基础,例如图卷积 网络GCNs)。然而,仅凭 CNN,深度学习领域还无法主导机器学习的世界。在接下来的部分,我们将学习另一种重要架构:循环神经网络(RNN),这是一种处理序列数据的宝贵方法。

3.3.2 探索 RNN(循环神经网络)

到目前为止,我们所见到的神经网络是我们所称之为前馈网络的类型:网络的每一层都将信息传递给下一层;没有循环。此外,我们看到的卷积神经网络接受一个单一的输入(图像)并输出一个单一的结果:一个标签或该标签的评分。但在许多情况下,我们处理的任务比单一输入、单一输出任务更为复杂。在本节中,我们将重点介绍一类模型,称为递归神经网络RNN),它们专注于处理输入序列,有些还会生成顺序输出。

RNN 任务的典型例子是机器翻译。例如,将英文句子“the apple is green”翻译成法语。为了使这类任务有效,网络需要考虑我们输入之间的关系。另一个任务可能是视频分类,其中我们需要查看视频的不同帧,以分类视频的内容。RNN 逐步处理每个输入,其中每个时间步可以表示为 [t]。在每个时间步,模型计算一个隐藏状态 h[t] 和一个输出 y[t]。但是为了计算 h[t],模型不仅接收输入 x[t],还接收前一个时间步的隐藏状态 h[t−1]。对于单个时间步,标准 RNN 计算如下:

ht = f(Wxxt + Whht− 1 + b)

其中:

  • W[x] 是 RNN 中输入 x[t] 的权重

  • W[h] 是来自前一个时间步 h[t−1] 的隐藏层输出的权重

  • b 是偏置项

  • f 是激活函数——在一个标准的 RNN 中,使用的是 tanh 激活函数

这样,在每个时间步,模型也能感知到前一个时间步发生了什么,因为有了额外的输入 h[t−1]。

我们可以如下可视化 RNN 的流程:

PIC

图 3.6:RNN 示例

我们可以看到,在时间步零时,我们还需要一个初始的隐藏状态。通常这只是一个全零向量。

一种常见的神经网络变体是序列到序列seq2seq)神经网络,这是一种在机器翻译中非常流行的范式。正如其名字所示,这种网络的思路是将一个序列作为输入,并输出另一个序列。重要的是,这两个序列不需要是相同长度的。这使得该架构能够以更加灵活的方式进行句子翻译,这一点至关重要,因为不同语言在表达相同意思的句子时使用的单词数量可能不同。这种灵活性是通过编码器-解码器架构来实现的。这意味着我们的神经网络将有两个部分:一个初始部分将输入编码成一个单一的权重矩阵(多个输入被编码成一个隐藏向量),然后该矩阵作为解码器的输入,解码器根据它来生成多个输出(一个输入对应多个输出)。编码器和解码器有各自独立的权重矩阵。对于一个有两个输入和两个输出的模型,效果可以通过如下方式可视化:

PIC

图 3.7:序列到序列网络的示例

在这个图中,w[e]是编码器的权重,w[d]是解码器的权重。我们可以看到,与我们的 RNN 相比,我们现在有了解码器的新隐藏状态 s[0],并且我们还可以看到 c,这是一个上下文向量。在标准的序列到序列模型中,c 等于编码器结束时的隐藏状态,而 s[0],编码器的初始隐藏状态,通常是通过一个或多个前馈层来计算的。上下文向量是解码器每一部分的额外输入;它允许解码器的每一部分使用编码器的信息。

3.3.3 注意力机制

尽管递归神经网络模型(RNN)可能非常强大,但它们有一个重要的缺点:编码器传递给解码器的所有信息必须都在隐藏瓶颈层中——即解码器在开始时接收到的隐藏输入状态。这对于短句子来说是没问题的,但可以想象,当我们想要翻译整个段落或非常长的句子时,这就变得更加困难。我们不能指望一个单一的向量能包含翻译长句子所需的所有信息。这个缺点通过一个叫做“注意力机制”的方法得到了解决。稍后我们将会对注意力机制的概念进行概括,但首先让我们看看在 seq2seq 模型的上下文中,注意力机制是如何应用的。

注意力机制允许 seq2seq 模型的解码器根据某些注意力权重“关注”编码器的隐藏状态。这意味着解码器不再仅依赖瓶颈层来翻译输入,而是可以回溯到编码器的每个隐藏状态,并决定它希望使用多少信息。这是通过在解码器的每个时间步使用一个上下文向量来实现的,这个上下文向量现在充当概率向量,决定给予编码器每个隐藏状态的权重。我们可以将注意力机制在这个上下文中理解为在每个解码器时间步的以下序列:

  • e[t,i] = f(s[t−1],h[i]) 计算每个编码器隐藏状态的 alignment scores。这个计算可以是一个多层感知机(MLP),针对编码器的每个隐藏状态,输入为解码器当前的隐藏状态 s[t−1],以及编码器的隐藏状态 h[i]。

  • e[t,i] 给出了我们对齐分数;它们告诉我们编码器每个隐藏状态与解码器一个隐藏状态之间的关系。但 f 的输出是一个标量,这使得比较不同的对齐分数变得不可能。这就是为什么我们随后对所有对齐分数进行 softmax 操作,以获得一个概率向量;注意力权重:a[t,i] = softmax(e[t,i])。这些权重现在是介于 0 和 1 之间的数值,告诉我们对于解码器的单个隐藏状态,应该给予编码器每个隐藏状态多少权重。

  • 通过我们的注意力权重,我们现在对编码器的隐藏状态进行加权平均。这生成了上下文向量 c[1],它可以用于解码器的第一个时间步。

因为这个机制计算了解码器每个时间步的注意力权重,模型现在变得更加灵活:在每个时间步,它知道应该给予编码器每个部分多少权重。此外,由于我们在这里使用的是多层感知机(MLP)来计算注意力权重,这个机制可以端到端地训练。

这告诉你如何在序列到序列模型中使用注意力。但注意力机制可以被泛化,使其更加强大。这种泛化是如今最强大的神经网络应用中的构建块。它使用三个主要组件:

  • 查询,记作 Q。你可以将其视为解码器的隐藏状态。

  • 键,记作 K。你可以将键视为输入的隐藏状态 h[i]。

  • 值,记作 V。在标准的注意力机制中,值与键相同,但被分开作为独立的值。

查询、键和值共同构成了注意力机制,形式为

 QKT 注意力 (Q, K, V) = softmax (-√---)V dk

我们可以区分出三种泛化:

  • 使用 MLP 计算注意力权重对于每个时间步来说是一个相对繁重的操作。相反,我们可以使用一种更轻量的方式,使我们能够更快速地为解码器的每个隐状态计算注意力权重:我们使用解码器隐状态与编码器隐状态的缩放点积。我们通过K的维度的平方根来缩放点积:

     T Q√K--- dk

    这是由于两个原因:

    • softmax 操作可能导致极端值——接近零和接近一的值。这使得优化过程更加困难。通过缩放点积,我们避免了这个问题。

    • 注意力机制对高维向量计算点积。这导致点积值非常大。通过缩放点积,我们可以抵消这种趋势。

  • 我们分别使用输入向量——将它们分别作为键和值,在不同的输入流中。这使得模型可以更灵活地以不同的方式处理它们。两者都是可学习的矩阵,因此模型可以以不同的方式优化它们。

  • 注意力机制将一组输入作为查询向量。这种做法在计算上更高效;我们不需要为每一个查询向量单独计算点积,而是可以一次性计算所有查询向量。

这三种概括使得注意力成为一种非常广泛应用的算法。你可以在今天大多数表现最好的模型中看到它,包括一些最佳的图像分类模型——生成非常逼真文本的大型语言模型,或者能够创造最美丽、最具创意图像的文本到图像模型。由于注意力机制的广泛应用,它在 TensorFlow 等深度学习库中非常容易使用。在 TensorFlow 中,你可以这样使用注意力机制:


from tensorflow.keras.layers import Attention 
attention = Attention(use_scale=True, score_mode='dot')

它可以通过我们的查询(query)、键(key)和值(value)来调用:


context_vector, attention_weights = attention( 
inputs = [query, value, keys], 
return_attention_scores = True, 
)

在前面的章节中,我们讨论了神经网络的一些重要构建模块;我们讨论了基本的 MLP、卷积的概念、递归神经网络和注意力机制。还有其他一些我们未在此讨论的组件,以及更多我们讨论过的组件的变种和组合。如果你想了解更多关于这些构建模块的内容,请参考本章末尾的阅读列表。如果你想深入研究神经网络架构和组件,有许多优秀的资源可以参考。

3.4 理解典型神经网络中的问题

我们在前几节中讨论的深度神经网络非常强大,配合适当的训练数据,推动了机器感知领域的巨大进步。在机器视觉中,卷积神经网络使我们能够对图像进行分类、定位图像中的物体、将图像分割成不同的区域或实例,甚至生成全新的图像。在自然语言处理方面,循环神经网络和变压器使我们能够对文本进行分类、识别语音、生成新文本,或者如前所述,在两种语言之间进行翻译。

然而,这些标准类型的神经网络模型也有若干限制。在本节中,我们将探讨这些限制中的一些。我们将着眼于以下几个方面:

  • 这样的神经网络模型的预测得分如何会过于自信

  • 这样的模型如何能在 OOD 数据上产生非常自信的预测

  • 微小、几乎不可察觉的输入图像变化如何导致模型做出完全错误的预测

3.4.1 未校准和过于自信的预测

现代普通神经网络的一个问题是,它们往往会产生未经过良好校准的输出。这意味着这些网络产生的置信度得分不再代表它们的经验正确性。为了更好地理解这意味着什么,我们来看一下理想校准网络的可靠性图,如 3.8 所示。

PIC

图 3.8:良好校准的神经网络的可靠性图。经验确定的("实际")准确性与网络输出的预测值一致

如你所见,可靠性图展示了准确性(在 y 轴上)作为置信度(在 x 轴上)的函数。基本的思路是,对于一个经过良好校准的网络,预测结果(或置信度)所关联的得分应该与其经验正确性相匹配。这里,经验正确性被定义为一组具有相似输出值的样本的准确性,因此,这些样本会被分到可靠性图中的同一组。因此,例如,对于所有输出得分在 0.7 和 0.8 之间的样本组,一个经过良好校准的网络的期望是,其中 75% 的预测应该是正确的。

为了使这个想法更加正式,我们假设有一个数据集X,其中包含N个数据样本。每个数据样本x都有一个对应的目标标签y。在分类问题中,我们会有y,它表示属于K个类别中的一个。为了获得可靠性图,我们将使用神经网络模型对整个数据集X进行推理,并为每个样本x获得一个输出分数ŷ。然后,我们将使用输出分数将每个数据样本分配到可靠性图中的一个* m 区间。在前面的图中,我们选择了M* = 10 个区间。B[m]将是落入区间m的样本的索引集合。最后,我们将测量并绘制每个区间内所有样本预测的平均准确度,定义为acc(B[m]) = -1-- |Bm |∑ [iB[m]]1(ŷ[i] = y[i])。

对于一个经过良好校准的网络,给定区间内样本的平均准确度应该与该区间的置信度值匹配。在前面的图中,我们可以看到,例如,对于落在输出分数在 0.2 到 0.3 之间的区间的样本,我们观察到的平均准确度为 0.25。现在,让我们看看在一个对预测过度自信的未经校准的网络中会发生什么情况。图 3.9 展示了这种情况,这是许多现代原始神经网络架构所表现的行为的典型代表。

图片

图 3.9:过度自信神经网络的可靠性图。由经验确定的“实际”准确度(紫色条)始终低于网络预测值(粉色条和灰色虚线)所建议的准确度。

我们可以观察到,对于所有的区间,区间内样本的实际准确度低于基于样本输出分数所期望的准确度。这就是过度自信预测的表现。网络的输出让我们相信它对预测具有很高的信心,而实际上,实际表现并不符合预期。

过度自信的预测在安全和任务关键应用中可能非常有问题,例如医学决策、自驾车以及法律或财务决策。具有过度自信预测的网络将缺乏向我们人类(或其他网络)指示它们何时可能出错的能力。这种缺乏意识可能变得危险,例如,当一个网络被用来帮助决定被告是否应该被允许保释时。假设一个网络接收到被告的数据(如过去的定罪记录、年龄、教育水平),并以 95%的置信度预测不应允许保释。基于这个输出,法官可能错误地认为该模型可以信任,并主要根据模型的输出作出裁决。相比之下,经过校准的置信度输出可以表明我们可以多大程度上信任模型的输出。如果模型不确定,这表明输入数据中有一些在模型的训练数据中没有得到很好的表示——这表明模型更可能出错。因此,良好校准的不确定性使我们能够决定是否将模型的预测纳入我们的决策中,或是否完全忽略模型。

绘制和检查可靠性图表对于可视化一些神经网络的校准非常有用。然而,有时我们希望比较多个神经网络的校准性能,可能每个网络都使用了多个配置。在需要比较多个网络和设置的情况下,将神经网络的校准总结为一个标量统计量是很有用的。期望校准误差ECE)就是这样的总结统计量。对于可靠性图表中的每个区间,它衡量观察到的准确度 acc(B[m]) 和基于样本输出得分的预期准确度 conf(B[m]) 之间的差异,定义为 -1-- |Bm |∑ [iB[m]]ŷ。然后,它对所有区间进行加权平均,其中每个区间的权重由该区间中的样本数决定:

 M∑ ECE = |Bm-||acc(Bm ) − conf(Bm )| m=1 n

这为 ECE 的初步介绍以及如何衡量它提供了一个开端。我们将在第八章中详细回顾 ECE,章节中将提供一个代码实现,作为通过贝叶斯方法揭示数据集偏移案例研究的一部分。

在一个完全校准的神经网络输出情况下,ECE将为零。神经网络的校准程度越差,ECE将越大。让我们来看看神经网络校准差和过度自信的几个原因。

一个原因是,softmax 函数通常是分类网络的最后一个操作,它使用指数函数确保所有值都是正数:

 zi σ(⃗z)i = ∑--e----- Kj=1ezj

结果是,输入到 softmax 函数的微小变化会导致其输出发生显著变化。

另一个导致过度自信的原因是现代深度神经网络增加了模型的容量(Chuan Guo 等人,2017)。随着时间的推移,神经网络架构变得更深(更多的层)和更宽(每层更多的神经元)。这样的深度和宽度的神经网络具有较高的方差,能够非常灵活地拟合大量的输入数据。当实验调整神经网络的层数或每层的滤波器数量时,Chuan Guo 等人观察到,随着网络架构的加深和加宽,错误的校准(从而导致过度自信)会变得更严重。他们还发现,使用批量归一化或训练时减少权重衰减会对校准产生负面影响。这些观察结果表明,现代深度神经网络的模型容量增加是其过度自信的原因之一。

最终,过度自信的估计可能源于选择特定的神经网络组件。例如,已经证明,使用 ReLU 函数的全连接网络会导致连续分段仿射分类器([?])。这反过来意味着,总是可以找到一些输入样本,使得 ReLU 网络会产生高度自信的输出。即使对于那些与训练数据不同的输入样本,这一结果依然成立,尽管此时网络的泛化性能可能较差,因此我们期望得到较低自信度的输出。这种任意高自信度的预测同样适用于使用最大池化或平均池化的卷积网络,或任何其他会导致分段仿射分类器函数的网络。

这个问题是这种神经网络架构固有的,只能通过改变架构本身来解决([?])。

3.4.2 在分布外数据上的预测

现在我们已经看到了模型可能会过度自信,因此未经过校准,我们再来看一个神经网络的问题。神经网络通常在假设测试数据和训练数据来自同一分布的前提下进行训练。然而,实际上,情况并非总是如此。当一个模型在真实世界中部署时,它所看到的数据可能会发生变化。我们称这些变化为数据集偏移,通常将其分为三类:

  • 协变量偏移:特征分布 p(x) 发生变化,而 p(y|x) 保持不变

  • 开放集识别:测试时会出现新标签

  • 标签偏移:标签分布 p(y) 发生变化,而 p(x|y) 保持不变

以下是前述项目的示例:

  • 协变量偏移:模型被训练用于识别面孔。训练数据主要由年轻人的面孔组成。在测试时,模型看到的是各个年龄段的面孔。

  • 开集识别:模型被训练来分类有限数量的犬种。在测试时,模型会看到比训练数据集中更多的犬种。

  • 标签偏移:一个模型被训练来预测不同的疾病,其中一些疾病在训练时非常罕见。然而,随着时间的推移,这些罕见疾病的发生频率发生变化,并且在测试时它们成为最常见的疾病之一。

PIC

图 3.10:训练数据分布与实际世界中的数据大多数重合,但我们不能期望模型在图中右上角的分布外点上表现良好。

由于这些变化,当模型在实际环境中部署时,如果遇到的数据显示与训练数据分布不同,模型的表现可能会降低。模型面临分布外数据的可能性在很大程度上取决于模型部署的环境:有些环境较为静态(较低的分布外数据出现概率),而其他环境则较为动态(较高的分布外数据出现概率)。

深度学习模型在处理分布外数据时的问题之一,是因为这些模型通常拥有大量的参数,因此可以记住训练数据中的特定模式和特征,而不是反映底层数据分布的稳健和有意义的表示。当测试时出现看起来与训练数据略有不同的新数据时,模型实际上并没有能力进行泛化并做出正确的预测。一个例子是海滩上的牛的图像( 3.11),而训练数据集中的牛恰好是在绿色草地上。模型通常利用数据中的上下文来进行预测。

PIC

图 3.11:在不同环境中的物体(这里是海滩上的牛)可能会使模型难以识别图像中包含该物体。

在我们深入探讨如何通过一个简单模型处理分布外数据的实际例子之前,让我们先看看几种方法,这些方法可以突出典型神经网络中分布外数据的问题。理想情况下,我们希望模型在遇到与其训练数据分布不同的数据时,能够表现出较高的不确定性。如果是这样,分布外数据在模型部署到实际环境中时就不会是一个大问题。例如,在任务关键型系统中,模型错误的代价很高,因此通常会有一个置信度阈值,模型的预测必须达到该阈值才会被信任。如果一个模型经过良好的标定,并且对分布外输入赋予较低的置信度评分,那么围绕该模型的业务逻辑可以抛出异常并且不使用模型的输出。例如,自动驾驶汽车可以提醒司机接管控制,或者它可以减速以避免发生事故。

然而,常见的神经网络不知道自己什么时候不知道;它们通常不会对外部分布数据赋予较低的置信度分数。

Google 论文《你能信任你模型的不确定性吗?在数据集偏移下评估预测不确定性》中给出了一个例子。论文表明,如果你对测试数据集应用模糊或噪声等扰动,使得图像变得越来越偏离分布,模型的准确度会下降。然而,模型的置信度校准也会下降。这意味着在外部分布数据上,模型的得分已经不再可信:它们无法准确反映模型对其预测的信心。我们将在第八章的案例研究《通过贝叶斯方法揭示数据集偏移》中探索这一行为,应用 贝叶斯深度学习

另一种确定模型如何处理外部分布数据的方法是输入与其训练数据集完全不同的数据,而不仅仅是扰动过的数据。衡量模型外部分布检测性能的过程如下:

  1. 在内部分布数据集上训练一个模型。

  2. 保存模型在内部分布测试集上的置信度分数。

  3. 向模型输入一个完全不同的外部分布数据集,并保存模型对应的置信度分数。

  4. 现在,将来自两个数据集的得分视为二分类问题的得分:内部分布或外部分布。计算二分类指标,例如接收者操作特征AUROC)曲线下的面积或精确度-召回曲线下的面积。

这个策略告诉你模型在多大程度上能够区分内部分布数据和外部分布数据。假设内部分布数据应该始终比外部分布数据获得更高的置信度分数;在理想情况下,两者之间没有重叠。然而,实际上情况并非如此。模型确实经常会给外部分布数据较高的置信度分数。我们将在下一节中探讨一个例子,并在后续章节中提出一些解决方案。

3.4.3 自信的外部分布预测示例

让我们看看一个普通神经网络是如何对外部分布数据做出自信预测的。在这个例子中,我们首先训练一个模型,然后输入外部分布数据。为了简单起见,我们将使用一个包含不同种类狗和猫的数据集,并构建一个二分类器,预测图像中是狗还是猫。

我们首先下载我们的数据:


curl -X GET https://s3.amazonaws.com/fast-ai-imageclas/oxford-iiit-pet.tgz  \ 
--output pets.tgz 
tar -xzf pets.tgz

然后,我们将数据加载到数据框中:


import pandas as pd 

df = pd.read_csv("oxford-iiit-pet/annotations/trainval.txt", sep=" ") 
df.columns = ["path", "species", "breed", "ID"] 
df["breed"] = df.breed.apply(lambda x: x - 1) 
df["path"] = df["path"].apply( 
lambda x: f"/content/oxford-iiit-pet/images/{x}.jpg" 
)

然后,我们可以使用 scikit-learn 的train_test_val()函数来创建训练集和验证集:


import tensorflow as tf 
from sklearn.model_selection import train_test_split 

paths_train, paths_val, labels_train, labels_val = train_test_split( 
df["path"], df["breed"], test_size=0.2, random_state=0 
)

然后我们创建我们的训练和验证数据。我们的divprocess()函数将下载的图像加载到内存中,并格式化标签以便模型处理。我们使用 256 的批量大小和 160x160 像素的图像大小:


IMG_SIZE = (160, 160) 
AUTOTUNE = tf.data.AUTOTUNE 

@tf.function 
def divprocess_image(filename): 
raw = tf.io.read_file(filename) 
image = tf.image.decode_png(raw, channels=3) 
return tf.image.resize(image, IMG_SIZE) 

@tf.function 
def divprocess(filename, label): 
return divprocess_image(filename), tf.one_hot(label, 2) 

train_dataset = (tf.data.Dataset.from_tensor_slices( 
(paths_train, labels_train) 
).map(lambda x, y: divprocess(x, y)) 
.batch(256) 
.divfetch(buffer_size=AUTOTUNE) 
) 

validation_dataset = (tf.data.Dataset.from_tensor_slices( 
(paths_val, labels_val)) 
.map(lambda x, y: divprocess(x, y)) 
.batch(256) 
.divfetch(buffer_size=AUTOTUNE) 
)

现在我们可以创建我们的模型。为了加速学习,我们可以使用迁移学习,并从一个在 ImageNet 上预训练过的模型开始:


def get_model(): 
IMG_SHAPE = IMG_SIZE + (3,) 
base_model = tf.keras.applications.ResNet50( 
input_shape=IMG_SHAPE, include_top=False, weights='imagenet' 
) 
base_model.trainable = False 
inputs = tf.keras.Input(shape=IMG_SHAPE) 
x = tf.keras.applications.resnet50.divprocess_input(inputs) 
x = base_model(x, training=False) 
x = tf.keras.layers.GlobalAveragePooling2D()(x) 
x = tf.keras.layers.Dropout(0.2)(x) 
outputs = tf.keras.layers.Dense(2)(x) 
  return tf.keras.Model(inputs, outputs)

在训练模型之前,我们首先需要编译它。编译意味着我们为模型指定一个损失函数和一个优化器,并可选地添加一些用于训练期间监控的度量。在以下代码中,我们指定模型应该使用二元交叉熵损失和 Adam 优化器进行训练,并且在训练期间我们想要监控模型的准确性:


model = get_model() 
model.compile(optimizer=tf.keras.optimizers.Adam(learning_rate=0.01), 
loss=tf.keras.losses.BinaryCrossentropy(from_logits=True), 
              metrics=['accuracy'])

由于迁移学习,仅训练模型三轮就能获得大约 99%的验证准确率:


model.fit(train_dataset, epochs=3, validation_data=validation_dataset)

我们还可以在该数据集的测试集上测试我们的模型。我们首先准备好数据集:


df_test = pd.read_csv("oxford-iiit-pet/annotations/test.txt", sep=" ") 
df_test.columns = ["path", "species", "breed", "ID"] 
df_test["breed"] = df_test.breed.apply(lambda x: x - 1) 
df_test["path"] = df_test["path"].apply( 
lambda x: f"/content/oxford-iiit-pet/images/{x}.jpg" 
) 

test_dataset = tf.data.Dataset.from_tensor_slices( 
(df_test["path"], df_test["breed"]) 
).map(lambda x, y: divprocess(x, y)).batch(256)

然后我们将数据集输入到训练好的模型中。我们获得了大约 98.3%的测试集准确率:


test_predictions = model.predict(test_dataset) 
softmax_scores = tf.nn.softmax(test_predictions, axis=1) 
df_test["predicted_label"] = tf.argmax(softmax_scores, axis=1) 
df_test["prediction_correct"] = df_test.apply( 
lambda x: x.predicted_label == x.breed, axis=1 
) 
accuracy = df_test.prediction_correct.value_counts(True)[True] 
print(accuracy)

现在我们有一个能够很好地分类猫和狗的模型。但是如果我们给这个模型一个既不是猫也不是狗的图像,会发生什么呢?理想情况下,模型应该识别出该图像不属于数据分布的一部分,并应该输出接近均匀分布的结果。让我们看看在实际操作中是否真的发生了这种情况。我们将一些来自 ImageNet 数据集的图像输入到模型中——这是用于预训练我们模型的实际数据集。ImageNet 数据集很大。这就是为什么我们下载了该数据集的一个子集:一个名为 Imagenette 的数据集。该数据集仅包含原始 ImageNet 数据集中的 10 个类别:


curl -X GET https://s3.amazonaws.com/fast-ai-imageclas/imagenette-160.tgz \ 
--output imagenette.tgz 
tar -xzf imagenette.tgz

然后我们从parachute类中选取一张图像:


image_path = "imagenette-160/val/n03888257/ILSVRC2012_val_00018229.JPEG" 
image = divprocess_image(image_path).numpy() 
plt.figure(figsize=(5,5)) 
plt.imshow(image.astype(int)) 
plt.axis("off") 
plt.show()

该图像显然不包含狗或猫;它显然是超出分布的:

PIC

图 3.12:来自 ImageNet 数据集的降落伞图像

我们将图像通过我们的模型并打印分数:


logits = model.predict(tf.expand_dims(image, 0)) 
dog_score = tf.nn.softmax(logits, axis=1)[0][1].numpy() 
print(f"Image classified as a dog with {dog_score:.4%} confidence") 
# output: Image classified as a dog with 99.8226% confidence

我们可以看到,模型将降落伞图像分类为狗,且信心度超过 99%。

我们还可以更系统地测试模型在 ImageNet parachute类上的表现。让我们将所有来自训练集的降落伞图像通过模型,并绘制出dog类的分数直方图。

我们首先创建一个小函数,用来创建包含所有降落伞图像的特定数据集:


from pathlib import Path 

parachute_image_dir = Path("imagenette-160/train/n03888257") 
parachute_image_paths = [ 
str(filepath) for filepath in parachute_image_dir.iterdir() 
] 
parachute_dataset = (tf.data.Dataset.from_tensor_slices(parachute_image_paths) 
.map(lambda x: divprocess_image(x)) 
.batch(256) 
.divfetch(buffer_size=AUTOTUNE))

然后我们可以将数据集输入到我们的模型,并创建与dog类相关的所有 softmax 分数列表:


Predictions = model.predict(parachute_dataset) 
dog_scores = tf.nn.softmax(predictions, axis=1)[:, 1]

然后我们可以使用这些分数绘制直方图——这将显示 softmax 分数的分布:


plt.rcParams.update({'font.size': 22}) 
plt.figure(figsize=(10,5)) 
plt.hist(dog_scores, bins=10) 
plt.xticks(tf.range(0, 1.1, 0.1)) 
plt.grid() 
plt.show()

理想情况下,我们希望这些分数分布接近 0.5,因为该数据集中的图像既不是狗也不是猫;模型应该非常不确定:

PIC

图 3.13:降落伞数据集的 softmax 分数分布

然而,我们看到的却是完全不同的情况。超过 800 张图像被错误分类为狗,且置信度至少为 90%。我们的模型显然不知道如何处理分布外数据。

3.4.4 对对抗性操作的易感性

大多数神经网络的另一个脆弱性是它们容易受到对抗性攻击。简单来说,对抗性攻击是欺骗深度学习系统的方式,通常这些方式是人类不会被欺骗的。这些攻击可以是无害的也可以是有害的。以下是一些对抗性攻击的例子:

  • 一个分类器可以检测不同种类的动物。它将一张熊猫的图像以 57.7%的置信度分类为熊猫。通过对图像进行微小扰动,使其对人类不可见,图像现在以 93.3%的置信度被分类为长臂猿。

  • 一个模型可以检测电影推荐是正面还是负面。它将给定的电影分类为负面。通过改变一个不会改变整体评论基调的词汇,例如,将“surprising”改为“astonishing”,模型的预测可以从负面推荐变为正面推荐。

  • 停止标志检测器可以检测停止标志。然而,通过在停止标志上贴一个相对较小的贴纸,模型就不再识别停止标志了。

这些例子表明,有不同种类的对抗性攻击。一种有用的对抗性攻击分类方法是,尝试确定攻击者对模型了解多少信息(无论是人类还是机器)。攻击者总是可以将输入传递给模型,但模型返回什么或者攻击者如何检查模型是不同的。通过这个视角,我们可以看到以下类别:

  • 硬标签黑盒:攻击者只能访问通过向模型输入获得的标签。

  • 软标签黑盒:攻击者可以访问模型的分数和标签。

  • 白盒设置:攻击者可以完全访问模型。他们可以访问权重,查看分数、模型结构等。

你可以想象,这些不同的设置使得攻击模型的难易程度有所不同。如果想要欺骗模型的人只能看到输入的标签结果,他们无法确定输入的细微变化是否会导致模型行为的变化,只要标签保持不变。当他们可以访问模型的标签和得分时,这种情况就变得更加容易。他们就可以看到输入变化是否增加或减少了模型的置信度。结果,他们可以更系统地尝试以减少模型对标签的置信度的方式来改变我们的输入。这可能会让我们找到模型的脆弱性,前提是有足够的时间以迭代的方式来改变输入。现在,如果有人拥有模型的完全访问权限(白盒设置),那么找到脆弱性可能会变得更容易。因为他们可以使用更多的信息来引导图像的变化,比如输入图像的损失梯度。

攻击者可获得的信息量并不是区分不同类型对抗性攻击的唯一标准;攻击方式有很多种。例如,在针对视觉模型的攻击中,有些攻击基于对图像的单一补丁调整(甚至是单个像素!),而其他攻击则会改变整个图像。有些攻击特定于某个模型,有些攻击可以应用于多个模型。我们还可以区分那些通过数字方式操控图像的攻击和那些能够在现实世界中实施、欺骗模型的攻击,或者是那些人眼可见的攻击与不可见的攻击。由于攻击方式种类繁多,关于这个话题的研究文献依然非常活跃——总是有新的方法来攻击模型,进而也需要找到应对这些攻击的防御方法。

在关于分布外数据的章节中,我们训练了一个模型来判断给定的图像是猫还是狗。我们看到分类器的表现非常好:它的测试准确率大约为 98.3%。这个模型对对抗性攻击是否具有鲁棒性?让我们创造一个攻击方法来验证一下。我们将使用快速梯度符号方法FGSM)轻微扰动一张狗的图像,使得模型误认为它实际上是一张猫的图像。快速梯度符号方法是由 Ian Goodfellow 等人于 2014 年提出的,至今仍然是最著名的对抗性攻击之一。这可能是因为它的简单性;我们将看到,仅需几行代码就能创建这样的攻击。此外,这种攻击的结果令人震惊——Goodfellow 本人提到,当他第一次测试这个攻击时,他简直不敢相信结果,甚至需要验证输入给模型的扰动图像确实与原始图像不同。

为了创建有效的对抗图像,我们必须确保图像中的像素发生变化——但变化的幅度不能大到以至于人眼能察觉。如果我们以某种方式扰动狗的图像,使得图像看起来像猫,那么如果模型将其分类为猫,模型并没有犯错。我们确保不会对图像进行过多扰动,通过最大范数约束来限制扰动——这基本上告诉我们图像中任何像素的变化不能超过某个量𝜖

∥˜x − x∥∞ ≤ 𝜖

其中 x 是我们的扰动图像,x 是我们原始的输入图像。

现在,为了使用快速梯度符号法创建我们的对抗样本,我们使用关于输入图像的损失梯度来创建新图像。与梯度下降法中最小化损失不同,我们现在想要最大化损失。给定我们的网络权重𝜃、输入x、标签y,以及用来计算损失的函数J,我们可以通过以下方式扰动图像来创建对抗图像:

η = 𝜖sgn(∇xJ (𝜃,x,y))

在这个方程中,我们计算损失关于输入的梯度的符号,即确定梯度是正(1)、负(-1)还是 0。符号约束最大范数,通过将其乘以 epsilon,我们确保扰动很小——计算符号只是告诉我们,如果我们想通过加或减 epsilon 来扰动图像,以便对模型的图像识别产生影响。η 就是我们要添加到图像中的扰动:

˜x = x+ η

让我们看一下在 Python 中的实现。给定我们已经训练好的网络,它可以将图像分类为狗或猫,我们可以创建一个函数,生成一个扰动,当它与 epsilon 相乘并加到我们的图像上时,会形成一次对抗攻击:


import tensorflow as tf 

loss_object = tf.keras.losses.BinaryCrossentropy() 

def get_adversarial_perturbation(image, label): 
image = tf.expand_dims(image, 0) 
with tf.GradientTape() as tape: 
tape.watch(image) 
prediction = model(image) 
loss = loss_object(label, prediction) 

gradient = tape.gradient(loss, image) 
  return tf.sign(gradient)[0]

然后,我们创建一个小函数,将输入图像通过我们的模型,并返回模型认为该图像包含狗的置信度:


def get_dog_score(image) -*>* float: 
scores = tf.nn.softmax( 
model.predict(np.expand_dims(image, 0)), axis=1 
).numpy()[0] 
  return scores[1]

我们下载了一张猫的图片:


curl https://images.pexels.com/photos/1317844/pexels-photo-1317844.jpeg *>* \ 
cat.png

然后,我们对图像进行预处理,以便将其输入到模型中。我们将标签设置为 0,对应于 cat 标签:


# divprocess function defined in the out-of-distribution section 
image, label = divprocess("cat.png", 0)

我们可以扰动我们的图像:


epsilon = 0.05 
perturbation = get_adversarial_perturbation(image, label) 
image_perturbed = image + epsilon * perturbation

现在,让我们获取模型对原始图像为猫的置信度,以及模型对扰动图像为狗的置信度:


cat_score_original_image = 1 - get_dog_score(image) 
dog_score_perturbed_image = get_dog_score(image_perturbed)

有了这个基础,我们可以创建以下图表,展示原始图像、施加在图像上的扰动以及扰动后的图像:


import matplotlib.pyplot as plt 

ax = plt.subplots(1, 3, figsize=(20,10))[1] 
[ax.set_axis_off() for ax in ax.ravel()] 
ax[0].imshow(image.numpy().astype(int)) 
ax[0].title.set_text("Original image") 
ax[0].text( 
0.5, 
-.1, 
f"\"Cat\"\n {cat_score:.2%} confidence", 
size=12, 
ha="center", 
transform=ax[0].transAxes 
) 
ax[1].imshow(perturbations) 
ax[1].title.set_text( 
"Perturbation added to the image\n(multiplied by epsilon)" 
) 
ax[2].imshow(image_perturbed.numpy().astype(int)) 
ax[2].title.set_text("Perturbed image") 
ax[2].text( 
0.5, 
-.1, 
f"\"Dog\"\n {dog_score:.2%} confidence", 
size=12, 
ha="center", 
transform=ax[2].transAxes 
) 
plt.show()

图 3.14 展示了原始图像和扰动图像,并显示了每张图像的模型预测结果。

PIC

图 3.14:对抗攻击的示例

3.14中,我们可以看到,模型最初将图像分类为猫,置信度为 100%。当我们对原始猫图像(左侧)应用扰动(中间显示)后,右侧的图像现在被分类为狗,置信度为 98.73%,尽管该图像在视觉上看起来与原始输入图像相同。我们成功地创建了一个对抗性攻击,欺骗了我们的模型!

3.5 总结

在这一章中,我们介绍了几种常见的神经网络类型。首先,我们讨论了神经网络的关键构建块,特别关注了多层感知器。接着,我们回顾了常见的神经网络架构:卷积神经网络、递归神经网络和注意力机制。所有这些组件使我们能够构建非常强大的深度学习模型,这些模型有时可以达到超人类的表现。然而,在本章的第二部分,我们回顾了神经网络的一些问题。我们讨论了它们如何表现得过于自信,并且在处理分布外数据时表现不佳。我们还看到,神经网络输入的微小、不可察觉的变化可以导致模型做出错误的预测。

在下一章中,我们将结合本章和 第三章深度学习基础中学到的概念,讨论贝叶斯深度学习,它有可能克服我们在本章中看到的标准神经网络的一些挑战。

3.6 进一步阅读

有很多优秀的资源可以帮助我们深入了解深度学习的基本构建块。以下是一些很好的起点资源:

  • Nielsen, M.A., 2015. 神经网络与深度学习(第 25 卷)。美国加州旧金山:Determination press。,neuralnetworksanddeeplearning.com/

  • Chollet, F., 2021. Python 深度学习。Simon 和 Schuster。

  • Raschka, S., 2015. Python 机器学习。Packt Publishing Ltd.

  • Ng, Andrew, 2022,深度学习专业化。Coursera。

  • Johnson, Justin, 2019. EECS 498-007 / 598-005, 深度学习与计算机视觉。密歇根大学。

想了解更多关于深度学习模型问题的内容,你可以阅读以下一些资源:

  • 过度自信与校准:

    • Guo, C., Pleiss, G., Sun, Y. 和 Weinberger, K.Q., 2017 年 7 月。现代神经网络的校准。在国际机器学习会议(第 1321-1330 页)。PMLR。

    • Ovadia, Y., Fertig, E., Ren, J., Nado, Z., Sculley, D., Nowozin, S., Dillon, J., Lakshminarayanan, B. 和 Snoek, J., 2019. 你能信任模型的不确定性吗?评估数据集转移下的预测不确定性。神经信息处理系统进展,32。

  • 分布外检测:

    • Hendrycks, D. 和 Gimpel, K., 2016. 神经网络中 错误分类和分布外样本的检测基准。arXiv 预印本 arXiv:1610.02136。

    • Liang, S., Li, Y. 和 Srikant, R., 2017. 增强神经网络中分布外图像检测的可靠性。 arXiv 预印本 arXiv:1706.02690。

    • Lee, K., Lee, K., Lee, H. 和 Shin, J., 2018. 一个简单的 统一框架用于检测分布外样本和 对抗攻击。 神经信息处理系统进展,31。

    • Fort, S., Ren, J. 和 Lakshminarayanan, B., 2021. 探索 分布外检测的极限。神经信息处理系统进展,34,第 7068-7081 页。

  • 对抗攻击:

    • Szegedy, C., Zaremba, W., Sutskever, I., Bruna, J., Erhan, D., Goodfellow, I. 和 Fergus, R., 2013. 神经网络的 有趣特性。arXiv 预印本 arXiv:1312.6199。

    • Goodfellow, I.J., Shlens, J. 和 Szegedy, C., 2014. 解释和 利用对抗样本。arXiv 预印本 arXiv:1412.6572。

    • Nicholas Carlini, 2019. 对抗性机器学习阅读 列表 nicholas.carlini.com/writing/2019/all-adversarial-example-papers.html

你可以查看以下资源,以深入了解本章所涵盖的主题和实验:

  • Jasper Snoek, MIT 6.S191:深度学习中的不确定性,2022 年 1 月。

  • TensorFlow 核心教程,使用 FGSM 生成对抗样本

  • Goodfellow, I.J., Shlens, J. 和 Szegedy, C., 2014. 解释和 利用对抗样本。arXiv 预印本 arXiv:1412.6572。

  • Chuan Guo, Geoff Pleiss, Yu Sun 和 Kilian Q Weinberger. 现代神经网络的标定问题。发表于 国际机器学习会议,第 1321-1330 页。PMLR, 2017。

  • 斯坦福大学工程学院,CS231N,第 16 讲 — 对抗样本与对抗训练

  • Danilenka, Anastasiya, Maria Ganzha, Marcin Paprzycki 和 Jacek Mańdziuk, 2022. 使用对抗图像改进非 IID 数据的 联邦学习结果。arXiv 预印本 arXiv:2206.08124。

  • Szegedy, C., Zaremba, W., Sutskever, I., Bruna, J., Erhan, D., Goodfellow, I. 和 Fergus, R., 2013. 神经网络的 有趣特性。arXiv 预印本 arXiv:1312.6199。

  • Sharma, A., Bian, Y., Munz, P. 和 Narayan, A., 2022. 基于视觉任务的对抗性 补丁攻击与防御:一项调查。arXiv 预印本 arXiv:2206.08304。

  • Nicholas Carlini, 2019. 对抗性机器学习阅读列表 nicholas.carlini.com/writing/2019/all-adversarial-example-papers.html

  • Parkhi, O.M., Vedaldi, A., Zisserman, A. 和 Jawahar, C.V., 2012 年 6 月。猫与狗。发表于 2012 年 IEEE 计算机视觉与模式识别大会(第 3498-3505 页)。IEEE。(数据集:猫与狗)。

  • 邓杰、董文、索彻、李俊杰、李凯和费菲,2009 年 6 月。Imagenet:一个大规模的层次化图像数据库。发表于 2009 年 IEEE 计算机视觉与模式识别会议(第 248-255 页)。IEEE。(ImageNet 数据集)。

  • 马修·D·泽勒和罗布·费格斯。可视化与理解 卷积网络。发表于欧洲计算机视觉会议,第 818–833 页。Springer,2014 年。

第四章

介绍贝叶斯深度学习

第二章贝叶斯推断基础贝叶斯推断基础中,我们看到传统的贝叶斯推断方法如何用来产生模型的不确定性估计,并介绍了良好校准和有原则的不确定性估计方法的特性。尽管这些传统方法在许多应用中非常强大,第二章贝叶斯推断基础也突出了它们在扩展性方面的一些局限性。在第三章深度学习基础中,我们看到了 DNNs 在大量数据下所能展现的令人印象深刻的能力;但我们也了解到它们并不完美。特别是,它们往往缺乏对分布外数据的鲁棒性——这是我们考虑将这些方法部署到现实世界应用中的一个主要问题。

PIC

图 4.1:BDL 结合了深度学习和传统贝叶斯推断的优势

BDL 旨在改进传统贝叶斯推断和标准 DNN 的不足,利用一种方法的优势来弥补另一种方法的不足。基本思想相当直接:我们的 DNN 获得不确定性估计,因此可以更稳健地实施,而我们的贝叶斯推断方法则获得了 DNN 的可扩展性和高维非线性表示学习能力。

虽然从概念上讲,这相当直观,但实际上并不是简单地将两者拼接在一起。随着模型复杂性的增加,贝叶斯推断的计算成本也会增加——使得某些贝叶斯推断方法(例如通过采样)变得不可行。

在本章中,我们将介绍理想贝叶斯神经网络BNN)的概念,并讨论其局限性,我们还将学习如何使用 BNN 创建更稳健的深度学习系统。具体来说,我们将涵盖以下内容:

  • 理想的 BNN

  • BDL 基础

  • BDL 工具

4.1 技术要求

要完成本章中的实际任务,您需要一个安装了SciPy堆栈的 Python 3.8 环境,并安装以下额外的 Python 软件包:

  • TensorFlow 2.0

  • TensorFlow 概率

  • Seaborn 绘图库

本书的所有代码可以在书籍的 GitHub 仓库中找到:github.com/PacktPublishing/Enhancing-Deep-Learning-with-Bayesian-Inference

4.2 理想的 BNN

正如我们在上一章所看到的,一个标准的神经网络由多个层组成。每一层由若干感知机组成——这些感知机包含乘法组件(权重)和加法组件(偏置)。每个权重和偏置参数都是单一的参数——或点估计——并且这些参数的组合将输入转换为感知机的输出。正如我们所见,通过反向传播训练的多个感知机层能够实现令人印象深刻的成就。然而,这些点估计包含的信息非常有限——我们来看看。

一般而言,深度学习的目标是找到(可能非常非常多的)参数值,最好的将一组输入映射到一组输出。也就是说,给定某些数据,对于网络中的每个参数,我们将选择最能描述数据的参数。这通常归结为取候选参数值的均值——或期望值。让我们看看这对于神经网络中的单一参数来说可能是什么样的:

PIC

图 4.2:展示如何在机器学习模型中对参数进行平均的数值表

为了更好地理解这一点,我们将使用表格来说明输入值、模型参数和输出值之间的关系。该表格显示了对于五个示例输入值(第一列),获得目标输出值(第四列)所需的理想参数(第二列)。在这种情况下,理想的意思是输入值乘以理想参数将完全等于目标输出值。因为我们需要找到一个最佳映射输入数据到输出数据的单一值,所以我们最终取理想参数的期望(或均值)。

如我们所见,取这些参数的均值是我们模型需要做出的折衷,以找到一个最适合示例中五个数据点的参数值。这是传统深度学习所做的折衷——通过使用分布,而不是点估计,BDL 能够在此基础上进行改进。如果我们查看标准差(σ)值,我们可以大致了解理想参数值的变化(从而输入值的方差)如何转化为损失的变化。那么,如果我们选择了不合适的参数值,会发生什么呢?

PIC

图 4.3:展示如何在参数不理想的情况下,参数σ值增大的数值表

如果我们比较图 4.2图 4.3,我们会看到参数值的显著方差如何导致模型近似度降低,而较大的σ可能表明模型存在误差(至少对于经过良好校准的模型)。虽然在实际中事情要复杂一些,但我们在这里看到的本质上是在每个深度学习模型的参数中发生的事情:参数分布被压缩成点估计,过程中的信息丢失。在 BDL 中,我们关注的是从这些参数分布中获取额外信息,用于更强健的训练和创建具有不确定性意识的模型。

BNNs 通过对神经网络参数的分布建模来实现这一目标。在理想情况下,BNN 能够学习每个网络参数的任意分布。在推理时,我们将从神经网络中采样,获得输出值的分布。利用第二章中介绍的采样方法,贝叶斯推断基础,我们将重复这一过程,直到获得足够数量的样本,从而能够假设我们的输出分布已得到很好的近似。然后,我们可以利用这个输出分布推断输入数据的某些特征,无论是分类语音内容还是对房价进行回归分析。

因为我们会有参数分布,而不是点估计,所以我们的理想 BNN 将提供精确的不确定性估计。这些估计将告诉我们给定输入数据的情况下,参数值的可能性有多大。这样,它们可以帮助我们检测输入数据与训练时数据的偏离情况,并通过给定样本值与训练时学习到的分布之间的差异量化这种偏差的程度。有了这些信息,我们就能更智能地处理神经网络的输出——例如,如果输出的不确定性很高,我们可以回退到一些安全的、预定义的行为。这种基于不确定性来解读模型预测的概念应该很熟悉:我们在第二章贝叶斯推断基础中学到,高不确定性表明模型预测存在误差。

回顾第二章贝叶斯推断基础,我们看到采样很快变得计算上不可行。现在,假设从每个神经网络参数的分布中进行采样——即使我们选择一个相对较小的网络,如 MobileNet(一种专门设计以提高计算效率的架构),我们仍然需要处理多达 420 万个参数。对这样的网络进行基于采样的推断将非常计算密集,而对于其他网络架构,这种情况会更加糟糕(例如,AlexNet 有 6000 万个参数!)。

由于这种不可行性,BDL 方法采用了各种近似方法,以促进不确定性量化。在下一节中,我们将了解一些基本原理,这些原理被应用于使得使用深度神经网络(DNN)进行不确定性估计成为可能。

4.3 BDL 基础

在本书的其余部分,我们将介绍使得 BDL 成为可能的一系列方法。这些方法中有许多共同的主题。我们将在这里覆盖这些内容,以便在稍后遇到时能够很好地理解这些概念。

这些概念包括以下内容:

  • 高斯假设:许多 BDL 方法使用高斯假设来使计算变得可行。

  • 不确定性来源:我们将查看不同的不确定性来源,并了解如何确定这些来源在某些 BDL 方法中的贡献。

  • 似然:我们在第二章贝叶斯推断基础中介绍了似然,在这里我们将进一步了解似然作为评估概率模型校准的度量标准的重要性。

接下来我们将查看以下小节中的每个问题。

4.3.1 高斯假设

在前面描述的理想情况下,我们讨论了为每个神经网络参数学习分布。实际上,虽然每个参数将遵循特定的非高斯分布,但这将使本已困难的问题变得更加复杂。这是因为,对于贝叶斯神经网络(BNN),我们关注的是学习两个关键概率:

  • 给定某些数据 D,权重 W 的概率:

    P (W |D)

  • 给定某些输入 x,输出 ŷ 的概率:

    P (yˆ|x)

对于任意概率分布,获得这些概率需要求解无法求解的积分。而高斯积分有封闭解——使得它们成为近似分布时的非常流行的选择。

因此,在贝叶斯深度学习(BDL)中,假设我们可以用高斯分布近似我们权重的真实底层分布是很常见的(这与我们在第二章贝叶斯推理基础中看到的类似)。让我们看看这会是什么样子——以我们典型的线性感知机模型为例:

z = f(x) = βX + ξ

在这里,x 是我们输入到感知机的值,β 是我们学习到的权重值,ξ 是我们学习到的偏置值,而 z 是返回的值(通常传递给下一层)。采用贝叶斯方法,我们将 βξ 转换为分布,而不是点估计,具体来说:

β ≈ 𝒩 (μ β,σβ) ξ ≈ 𝒩 (μ ξ,σξ)

现在学习过程将涉及学习四个参数,而不是两个,因为每个高斯分布由两个参数描述:均值(μ)和标准差(σ)。对我们神经网络中的每个感知机执行此操作后,我们最终需要学习的参数数量翻倍——我们可以通过从图 4.4Figure)开始看到这一点:

PIC

图 4.4:标准 DNN 的示意图

引入一维高斯分布作为我们的权重后,网络变为如下:

PIC

图 4.5:带有高斯先验的贝叶斯神经网络(BNN)示意图

第五章,贝叶斯深度学习的原则方法中,我们将看到正是这些方法。虽然这确实增加了网络的计算复杂性和内存占用,但它使得通过神经网络进行贝叶斯推理成为可能——这使得它成为一个非常值得的权衡。

那么,我们实际上想要在这些不确定性估计中捕获什么呢?在第二章贝叶斯推理基础中,我们看到了不确定性是如何根据用于训练的数据样本而变化的——但是这种不确定性的来源是什么?为什么它在深度学习应用中很重要?让我们继续往下看,找出答案。

4.3.2 不确定性的来源

正如我们在第二章贝叶斯推理基础中看到的,正如我们将在本书后续章节中看到的,我们通常将不确定性视为与某个参数或输出相关的标量变量。这些变量表示参数或输出的变化,但尽管它们只是标量变量,但有多个来源对它们的值产生影响。这些不确定性的来源可以分为两类:

  • 偶然性不确定性,也称为观测不确定性或数据不确定性,是与输入相关的不确定性。它描述了我们观测值的变化,因此是不可约的

  • 认知不确定性,也称为模型不确定性,是源于我们模型的不确定性。在机器学习中,这指的是与我们模型的参数相关的方差,它并非来源于观察,而是模型本身或模型的训练方式的产物。例如,在第二章贝叶斯推理基础》中,我们看到不同的先验如何影响高斯过程产生的不确定性。这是模型参数如何影响认知不确定性的一个例子——在这种情况下,因为它们明确地修改了模型对不同数据点之间关系的解释。

我们可以通过一些简单的例子来建立对这些概念的直觉。假设我们有一篮水果,其中包含苹果和香蕉。如果我们测量一些苹果和香蕉的高度和长度,我们会发现苹果通常是圆形的,而香蕉通常是长的,如 4.6所示。通过观察我们知道,每种水果的具体尺寸会有所不同:我们接受与任何给定的苹果分布的测量相关的随机性或随机性,但我们知道它们大致相似。这就是不可减少的不确定性:数据中的固有不确定性。

PIC

图 4.6:使用水果形状作为示例的偶然不确定性的插图

我们可以利用这些信息来构建一个模型,根据这些输入特征将水果分类为苹果或香蕉。但如果我们主要基于苹果来训练模型,而只有少量香蕉的测量数据会发生什么呢?这在 4.7中有示例。

PIC

图 4.7:基于水果示例的高认知不确定性的插图

在这里,我们看到——由于数据有限——我们的模型错误地将香蕉分类为苹果。虽然这些数据点落在我们模型的苹果边界内,但我们也看到它们离其他苹果非常远,这意味着,尽管它们被分类为苹果,但我们的模型(如果是贝叶斯模型)会对这些数据点具有较高的预测不确定性。这种认知不确定性在实际应用中非常有用:它能告诉我们何时可以信任模型,何时我们应该对模型的预测保持谨慎。与偶然不确定性不同,认知不确定性是可减少的——如果我们给模型更多的香蕉示例,它的分类边界会改善,认知不确定性将接近偶然不确定性。

PIC

图 4.8:低认知不确定性的插图

图**4.8中,我们可以看到,随着我们的模型观察到更多数据,认识不确定性显著减少,它现在看起来更像是图**4.6中展示的随机不确定性。因此,认识不确定性在两方面都极其有用:它不仅能指示我们可以多大程度上信任模型,而且还能作为提高模型性能的一种手段。

随着深度学习方法越来越多地应用于任务关键和安全关键的应用,使用的方法能够估计与其预测相关的认识不确定性的程度变得至关重要。为了说明这一点,让我们将示例的领域从图**4.7中的水果分类,改为现在分类喷气引擎是否在安全参数范围内运行,如图**4.9所示。

PIC

图 4.9:高认识不确定性在安全关键应用中的示意图

在这里,我们可以看到我们的认识不确定性可能是引擎故障的一个生死攸关的指示。如果没有这个不确定性估计,我们的模型会假设一切正常,尽管在其他参数的情况下引擎的温度异常——这可能导致灾难性的后果。幸运的是,由于我们有不确定性估计,尽管我们的模型从未遇到过这种情况,它依然能够告诉我们出了问题。

分离不确定性的来源

在本节中,我们介绍了两种不确定性的来源,并且我们看到认识不确定性对于理解如何解释模型输出非常有用。那么,你可能会想:我们能否将不确定性来源分离开来?

一般来说,在尝试将不确定性分解为认识性不确定性和随机性不确定性时,提供的保证有限,但有些模型允许我们获得较好的近似。集成方法提供了一个特别好的示例。

假设我们有一个包含M个模型的集合,它们为某些输入x和输出y从数据D中生成预测后验Py|x,D)。对于给定的输入,我们的预测将具有熵:

 1 ∑M m m H [P (y|x, D)] ≈ H [M- P (y|x,𝜃 )],𝜃 ∼ p(𝜃|D ) m=1

在这里,H表示熵,𝜃表示我们的模型参数。这是我们已经讨论过的概念的正式表达,表明当我们的随机不确定性和/或认识不确定性较高时,预测后验的熵(换句话说,不确定性)将很高。因此,这代表了我们的不确定性,这是本书中我们将处理的不确定性。我们可以用一种更符合本书内容的方式来表示这一点——以我们的预测标准差σ为单位:

σ = σa + σe

其中,ae分别表示偶然不确定性和认知不确定性。

因为我们正在使用集成方法,我们可以进一步超越总不确定性。集成方法的独特之处在于每个模型从数据中学习到的内容略有不同,原因在于不同的数据或参数初始化。由于我们为每个模型都获得了不确定性估计,我们可以对这些不确定性估计值进行期望(换句话说,即求平均):

 ∑M 𝔼 [H [P(y|x,𝜃)]] ≈ -1- H [P (y|x,𝜃m )],𝜃m ∼ p(𝜃|D ) p(𝜃|D) M m=1

这给了我们期望的数据不确定性——对偶然不确定性的估计。随着集成规模的增加,这种偶然不确定性的近似度量变得更为准确。这是因为集成成员从不同数据子集学习的方式。如果没有认知不确定性,那么模型是一致的,意味着它们的输出是相同的,总不确定性完全由偶然不确定性构成。

另一方面,如果存在认知不确定性,那么我们的总不确定性包括偶然不确定性和认知不确定性。我们可以使用期望的数据不确定性来确定我们总不确定性中存在多少认知不确定性。我们通过使用互信息来做到这一点,公式如下:

I[y,𝜃|x,D ] = H [P (y|x, D)]− 𝔼p (𝜃|D )[H [P(y|x,𝜃)]]

我们还可以通过方程 4.3.2 来表示这个问题:

I[y,𝜃|x,D ] = σe = σ − σa

正如我们所看到的,这个概念相当直接:简单地将我们的偶然不确定性从总不确定性中减去!能够估计偶然不确定性使得集成方法在不确定性量化中更具吸引力,因为它允许我们分解不确定性,从而提供通常无法获得的额外信息。在第六章,贝叶斯推断与标准深度学习工具箱中,我们将学习更多关于 BDL 的集成技术。对于非集成方法,我们只有一般的预测不确定性,σ(合并了偶然和认知不确定性),这在大多数情况下是合适的。

在下一节中,我们将看到如何将不确定性纳入到模型评估中,并且如何将其纳入损失函数以改善模型训练。

4.3.3 超越最大似然估计:似然的重要性

在上一节中,我们看到了不确定性量化如何帮助避免在机器学习的实际应用中出现潜在的危险场景。回顾更早之前的第二章贝叶斯推断基础第三章深度学习基础,我们引入了校准的概念,并展示了校准良好的方法如何随着推理数据偏离训练数据而增加其不确定性——这一概念在 4.7中得到了说明。

虽然用简单数据来说明校准的概念很容易——正如我们在第二章中的贝叶斯推断基础(通过 2.21)中看到的那样——不幸的是,在大多数应用中,做这个并不容易或实际。理解给定方法的校准程度的一个更实际的方法是使用一个包含其不确定性的度量——这正是似然所提供的。

似然是某些参数描述某些数据的概率。如前所述,我们通常使用高斯分布来简化问题——因此我们对高斯似然感兴趣:即高斯分布的参数拟合一些观测数据的似然。高斯似然的公式如下:

 1 (y − μ)2 p(y) = √----exp {− ----2--} 2π σ 2σ

让我们看看这些分布在我们之前在 4.24.3 中看到的参数值下会是什么样子:

PIC

图 4.10:与图 4.2 和 4.3 中的参数集对应的高斯分布图

可视化这两个分布突出了这两组参数的不确定性差异:我们的第一组参数具有高概率(实线),而我们的第二组参数具有低概率(虚线)。但是,这对与我们模型输出相关的似然值意味着什么呢?为了调查这些,我们需要将这些值代入方程 4.3.3。为此,我们需要一个y的值。我们将使用目标值的均值:24.03. 对于我们的μσ值,我们将分别取预测输出值的均值和标准差:

 1 (24.03 − 24.01)2 p(𝜃1) = √---------exp { − ----------2----} = 0.29 2π × 1.37 2 × 1.37  -----1----- (24.03−--31.11)2 − 5 p(𝜃2) = √2-π-× 1.78 exp {− 2× 1.782 } = 7.88× 10

我们看到,在参数集𝜃[1]与𝜃[2]之间,前者的似然得分显著高于后者。这与 4.10一致,表明根据数据,参数𝜃[1]比参数𝜃[2]具有更高的概率——换句话说,参数𝜃[1]更好地将输入映射到输出。

这些例子展示了引入不确定性估计的影响,使我们能够计算数据的似然性。虽然由于平均预测较差,我们的误差有所增加,但我们的似然性下降得更为显著——下降了多个数量级。这告诉我们,这些参数在描述数据方面表现得非常糟糕,而且它比仅仅计算输出和目标之间的误差更具原则性。

似然性的重要特征之一是它平衡了模型的准确性和不确定性。过于自信的模型在数据的预测不正确时,表现出较低的不确定性,而似然性会因这种过度自信而惩罚它们。同样,校准良好的模型在预测正确的数据上表现出信心,而在预测错误的数据上表现出不确定性。虽然模型仍会因错误的预测而受到惩罚,但它们也会因在正确的地方表现出不确定性而获得奖励,而不会过度自信。为了实践这一点,我们可以再次使用 4.2 4.3中显示的目标输出值:y = 24.03,但我们也会使用一个不正确的预测值:ŷ = 5.00。如我们所见,这产生了一个相当大的误差:|yŷ| = |24.03 − 5.00| = 19.03。让我们来看一下,当我们增加与此预测相关的σ²值时,似然性会发生什么变化:

图片

图 4.11:方差增加时似然值的变化图

如我们所见,当σ² = 0.00 时,我们的似然值非常小,但随着σ²增加到约 0.15 时,它又开始上升,然后再次下降。这表明,在预测不正确的情况下,与没有不确定性相比,一定的不确定性对于似然值更有利。因此,使用似然性可以帮助我们训练出更好校准的模型。

同样地,我们可以看到,如果我们固定不确定性,这里设为σ² = 0.1,并改变预测值,似然性在正确值处达到峰值,当预测ŷ变得不准确时,似然性在任一方向上都会下降,同时我们的误差|yŷ|也在增大:

图片

图 4.12:随着预测变化的似然值图

实际上,我们通常不使用似然函数,而是使用负对数似然NLL)。我们将其取负,因为在损失函数中,我们关心的是寻找最小值,而不是最大值。我们使用对数,因为这使得我们能够使用加法而非乘法,从而提高计算效率(利用对数恒等式log(ab) = log(a) + log(b))。因此,我们通常使用的方程是:

 2 N LL (y) = − log{-1--}− (y-−-μ)- 2πσ 2 σ2

现在我们已经熟悉了不确定性和似然性这两个核心概念,接下来我们准备进入下一部分,学习如何在代码中使用 TensorFlow 概率库处理概率概念。

4.4 BDL 工具

在本章中,正如在第二章贝叶斯推断基础中所见,我们已经看到了许多涉及概率的方程。虽然没有概率库也能创建 BDL 模型,但有一个支持基本函数的库会使事情变得更容易。由于本书中的示例使用了 TensorFlow,我们将使用TensorFlow 概率TFP)库来帮助我们实现这些概率组件。在本节中,我们将介绍 TFP,并展示如何使用它轻松实现我们在第二章贝叶斯推断基础第四章介绍贝叶斯深度 学习中看到的许多概念。

到目前为止,很多内容都在介绍如何与分布进行工作。因此,我们将要学习的第一个 TFP 模块是distributions模块。让我们来看看:


import tensorflow_probability as tfp 
tfd = tfp.distributions 
mu = 0 
sigma = 1.5 
gaussian_dist = tfd.Normal(loc=mu, scale=sigma)

在这里,我们有一个简单的例子,使用distributions模块初始化一个高斯(或正态)分布。我们现在可以从这个分布中进行采样——我们将使用seabornmatplotlib可视化我们的样本分布:


import seaborn as sns 
samples = gaussian_dist.sample(1000) 
sns.histplot(samples, stat="probability", kde=True) 
plt.show()

这将生成以下图表:

PIC

图 4.13:使用 TFP 从高斯分布中抽样得到的样本的概率分布

正如我们所见,样本遵循由我们的参数μ = 0 和σ = 1.5 定义的高斯分布。TFD 分布类还具有一些有用的函数方法,如概率密度函数PDF)和累积分布函数CDF)。让我们先从计算 PDF 在一系列值上的表现开始:


pdf_range = np.arange(-4, 4, 0.1) 
pdf_values = [] 
for x in pdf_range: 
pdf_values.append(gaussian_dist.prob(x)) 
plt.figure(figsize=(10, 5)) 
plt.plot(pdf_range, pdf_values) 
plt.title("Probability density function", fontsize="15") 
plt.xlabel("x", fontsize="15") 
plt.ylabel("probability", fontsize="15") 
plt.show()

通过以下代码,我们将生成以下图表:

PIC

图 4.14:一系列输入(x = −4 到x = 4)对应的概率密度函数值的图

同样,我们也可以计算 CDF:


cdf_range = np.arange(-4, 4, 0.1) 
cdf_values = [] 
for x in cdf_range: 
cdf_values.append(gaussian_dist.cdf(x)) 
plt.figure(figsize=(10, 5)) 
plt.plot(cdf_range, cdf_values) 
plt.title("Cumulative density function", fontsize="15") 
plt.xlabel("x", fontsize="15") 
plt.ylabel("CDF", fontsize="15") 
plt.show()

与 PDF 相比,CDF 生成累积概率值,范围从 0 到 1,正如我们在下面的图表中所看到的:

PIC

图 4.15:针对范围内输入值的累积分布函数值,x = −4 到x = 4

tfp.distributions类还为我们提供了轻松访问分布参数的方式,例如,我们可以通过以下方式恢复高斯分布的参数:


mu = gaussian_dist.mean() 
sigma = gaussian_dist.stddev()

请注意,这些将返回tf.Tensor对象,但可以通过.numpy()函数轻松访问 NumPy 值,例如:


mu = mu.numpy() 
sigma = sigma.numpy()

这给出了我们的musigma变量的两个 NumPy 标量值:分别为 0.0 和 1.5。

就像我们可以使用prob()函数计算概率,从而得到 PDF 一样,我们也可以轻松地使用log_prob()函数计算对数概率或对数似然。这使得我们比每次都编写完整的似然方程(例如,方程 4.3.3)更简单一些:


x = 5 
log_likelihood = gaussian_dist.log_prob(x) 
negative_log_likelihood = -log_likelihood

在这里,我们首先获得某个值x = 5 的对数似然值,然后获得 NLL,这在梯度下降的上下文中会用到。

随着我们继续阅读本书,我们将更多地了解 TFP 的功能——使用distributions模块从参数分布中采样,并探索强大的tfp.layers模块,该模块实现了常见神经网络层的概率版本。

4.5 总结

在本章中,我们介绍了实现并使用 BNN 所需的基本概念。最重要的是,我们了解了理想的 BNN,这使我们接触到 BDL 的核心思想,以及在实践中实现这一点的计算困难。我们还介绍了 BDL 中使用的基本实践方法,为我们实现计算可行的 BNN 打下了基础。

本章还介绍了不确定性来源的概念,描述了数据不确定性和模型不确定性之间的区别,这些如何影响总不确定性,并且我们如何通过不同的模型估计各种不确定性类型的贡献。我们还介绍了概率推断中最基本的组成部分之一——似然函数,并了解了它如何帮助我们训练更好的、原则性更强且更为精确的模型。最后,我们介绍了 TensorFlow 概率:一个强大的概率推断库,并且是本书后面实践例子中的一个关键组成部分。

既然我们已经涵盖了这些基础知识,我们准备好看看我们迄今为止遇到的概念如何应用到多个关键 BDL 模型的实现中。我们将了解这些方法的优缺点,并学习如何将它们应用于各种实际问题。继续阅读第五章贝叶斯深度学习的原则性方法,在这里我们将学习两种关键的 BDL 原则性方法。

4.6 进一步阅读

本章介绍了开始使用 BDL 所需的材料;然而,还有许多资源可以更深入地探讨不确定性来源的相关主题。以下是一些推荐,供那些有兴趣更深入探索理论和代码的读者参考:

  • 机器学习:一种概率视角,Murphy:凯文·墨菲(Kevin Murphy)关于机器学习的极为流行的书籍已成为该领域学生和研究人员的必备读物。本书从概率的角度详细介绍了机器学习,统一了统计学、机器学习和贝叶斯概率的概念。

  • TensorFlow Probability 教程:在本书中,我们将看到如何使用 TensorFlow Probability 开发 BNNs,但他们的网站提供了广泛的教程,更广泛地涉及概率编程:www.tensorflow.org/probability/overview

  • Pyro 教程:Pyro 是一个基于 PyTorch 的概率编程库——这是一个强大的贝叶斯推断工具,Pyro 网站上有许多关于概率推断的优秀教程和示例:pyro.ai/

第五章

贝叶斯深度学习的原理方法

现在我们已经介绍了贝叶斯神经网络BNN)的概念,接下来我们准备探索实现它们的各种方法。正如我们之前讨论的,理想的 BNN 计算量巨大,随着更复杂的架构或更大数据量,变得不可处理。近年来,研究人员开发了一系列方法,使得 BNN 变得可处理,从而能够在更大且更复杂的神经网络架构中实现。

在本章中,我们将探讨两种特别流行的方法:概率反向传播PBP)和贝叶斯反向传播BBB)。这两种方法都可以称为概率神经网络模型:旨在学习其权重的概率,而不仅仅是学习点估计(这是 BNN 的一个基本特征,正如我们在第四章中学习的那样,介绍贝叶斯深度学习)。因为它们在训练时显式地学习权重的分布,所以我们称之为原理性方法;与我们将在下一章探讨的方法相比,后者更加宽松地近似贝叶斯推断与神经网络的结合。我们将在本章的以下部分讨论这些主题:

  • 符号说明

  • 深度学习中的常见概率概念

  • 通过反向传播的贝叶斯推断

  • 使用 TensorFlow 实现 BBB

  • 使用 PBP 的可扩展贝叶斯深度学习

  • 实现 PBP

首先,让我们快速回顾一下本章的技术要求。

5.1 技术要求

要完成本章的实际任务,你将需要一个安装了 Python 3.8 环境以及 Python SciPy 栈和以下附加 Python 包的环境:

  • TensorFlow 2.0

  • TensorFlow 概率

本书的所有代码都可以在本书的 GitHub 仓库中找到:github.com/PacktPublishing/Enhancing-Deep-Learning-with-Bayesian-Inference

5.2 符号说明

尽管我们在前几章中介绍了书中使用的大部分符号,但在接下来的章节中,我们将介绍与 BDL 相关的更多符号。因此,我们在这里提供了符号的概述,供参考:

  • μ:均值。为了方便将本章与原始的概率反向传播论文进行交叉引用,讨论 PBP 时,这个符号用m表示。

  • σ:标准差。

  • σ²:方差(即标准差的平方)。为了方便将本章与论文进行交叉引用,在讨论 PBP 时,使用v表示。

  • x:输入模型的单一向量。如果考虑多个输入,我们将使用X表示由多个向量输入组成的矩阵。

  • x:我们输入的近似值x

  • y:单个标量目标。当考虑多个目标时,我们将使用 y 来表示多个标量目标的向量。

  • ŷ:来自我们模型的单个标量输出。当考虑多个输出时,我们将使用 ŷ 来表示多个标量输出的向量。

  • z:我们模型中间层的输出。

  • P:某个理想或目标分布。

  • Q:近似分布。

  • KL[QP]:我们的目标分布 P 和近似分布 Q 之间的 KL 散度。

  • ℒ:损失。

  • :期望。

  • N(μ,σ):一个由均值 μ 和标准差 σ 参数化的正态(或高斯)分布。

  • 𝜃:一组模型参数。

  • Δ:梯度。

  • :偏导数。

  • f():某个函数(例如 y = f(x) 表示 y 是通过对输入 x 应用函数 f() 得到的)。

我们将遇到不同变体的符号,使用不同的下标或变量组合。

5.3 深度学习中的常见概率概念

本书介绍了许多可能不熟悉的概念,但你可能会发现这里讨论的一些想法是你已经熟悉的。特别是,变分推断VI)可能因为在变分自编码器VAE)中的应用而为你所熟知。

快速回顾一下,VAE 是一种生成模型,它学习可以用于生成合理数据的编码。与标准自编码器类似,VAE 也采用编码器-解码器架构。

PIC

图 5.1:自编码器架构的示意图

对于标准自编码器,模型学习从编码器到潜在空间的映射,然后从潜在空间到解码器的映射。

如图所示,我们的输出简单地定义为 x = f**d,其中我们的编码 z 定义为:z = f**e,其中 fefd 分别是我们的编码器和解码器函数。如果我们想使用潜在空间中的值生成新数据,我们可以通过向解码器输入注入一些随机值;绕过编码器,直接从潜在空间中随机采样:

PIC

图 5.2:从标准自编码器的潜在空间中采样的示意图

这种方法的问题在于,标准自编码器在学习潜在空间的结构上表现不佳。这意味着,虽然我们可以自由地在该空间中随机采样点,但无法保证这些点能够被解码器处理,生成合理的数据。

在 VAE 中,潜在空间被建模为一个分布。因此,z = f**e 变为 z ≈𝒩(μ[x], σ[x]);也就是说,我们的潜在空间 z 现在变成了一个条件于输入 x 的高斯分布。现在,当我们想使用训练好的网络生成数据时,我们可以简单地从正态分布中采样。

为了实现这一点,我们需要确保潜在空间近似一个高斯分布。为此,我们在训练过程中使用Kullback-Leibler 散度(或 KL 散度),并将其作为正则化项加入:

 2 ℒ = ∥x− ˆx ∥ + KL [Q ∥P]

这里,P 是我们的目标分布(在此情况下是多变量高斯分布),我们正试图用Q来逼近它,Q是与我们潜在空间相关的分布,在此情况下如下所示:

Q = z ≈ 𝒩 (μ,σ)

所以,我们的损失现在变为:

ℒ = ∥x− ˆx ∥2 + KL [q(z|x)∥p(z )]

我们可以如下扩展:

ℒ = ∥x − ˆx∥2 + KL [𝒩 (μ,σ)∥𝒩 (0,I)]

这里,I 是单位矩阵。这将使我们的潜在空间能够收敛到我们的高斯先验,同时最小化重构损失。KL 散度还可以重新写成如下形式:

KL [q(z|x )∥p (z)] =q (z|x) logq(z|x )− q(z|x) log p(z)

我们方程右侧的项是对数q(z|x)和对数p(z)的期望(或均值)。正如我们从第二章贝叶斯推断基础第四章引入贝叶斯深度学习中了解到的那样,我们可以通过采样来获得给定分布的期望。因此,正如我们所见,KL 散度的所有项都是相对于我们近似分布q(z|x)计算的期望,我们可以通过从q(z|x)中采样来近似我们的 KL 散度,这正是我们接下来要做的!

现在我们的编码由方程 5.3 中显示的分布表示,我们的神经网络结构必须发生变化。我们需要学习我们分布的均值(μ)和标准差(σ)参数:

图片

图 5.3:带有均值和标准差权重的自动编码器架构示意图

以这种方式构建 VAE 的问题在于我们的编码z现在是随机的,而不是确定性的。这是一个问题,因为我们无法为随机变量获得梯度——如果我们无法获得梯度,就无法进行反向传播——因此我们无法进行学习!

我们可以使用一种叫做重参数化技巧的方法来解决这个问题。重参数化技巧涉及到修改我们计算z的方式。我们不再从分布参数中抽样z,而是将其定义为如下:

z = μ + σ ⊙ 𝜖

如你所见,我们引入了一个新的变量,𝜖,它是从一个均值为 0,标准差为 1 的高斯分布中采样的:

𝜖 = 𝒩 (0,1)

引入𝜖使我们能够将随机性移出反向传播路径。由于随机性仅存在于𝜖中,我们能够像正常情况一样通过权重进行反向传播:

图片

图 5.4:典型 VAE 架构的示意图,其中包含均值和标准差权重,并将采样组件移出了反向传播路径

这意味着我们能够将编码表示为一个分布,同时仍然能够反向传播 z 的梯度:学习 μσ 的参数,并使用 𝜖 从分布中进行采样。能够将 z 表示为分布意味着我们能够利用它来计算 KL 散度,从而将正则化项纳入方程 5.1,这反过来又使我们的嵌入在训练过程中向高斯分布收敛。

这些是变分学习的基本步骤,它们将我们的标准自编码器转变为 VAE。但这不仅仅是关于学习的。对于 VAE 来说,至关重要的是,因为我们已经学到了一个正态分布的潜在空间,我们现在可以从这个潜在空间有效地进行采样,使得我们可以使用 VAE 根据训练期间学到的数据景观生成新数据。与标准自编码器中的脆弱随机采样不同,我们的 VAE 现在能够生成合理的数据!

为了做到这一点,我们从正态分布中采样 𝜖 并将 σ 与该值相乘。这将给我们一个 z 的样本,传递给解码器,从而在输出端获得我们生成的数据,x

现在我们已经熟悉了变分学习的基础,在下一节中,我们将看到如何将这些原理应用于创建 BNN。

5.4 通过反向传播进行贝叶斯推断

在 2015 年的论文《神经网络中的权重不确定性》中,Charles Blundell 及其在 DeepMind 的同事们提出了一种使用变分学习进行神经网络贝叶斯推断的方法。他们的方法通过标准反向传播来学习 BNN 参数,并且这个方法被恰当地命名为贝叶斯反向传播BBB)。

在前一节中,我们看到了如何使用变分学习来估计我们编码的后验分布 z,学习 P(z|x)。对于 BBB,我们将做非常类似的事情,只是这一次我们不仅仅关心编码。我们这次要学习的是所有参数(或权重)的后验分布:P(𝜃|D)。

你可以将其看作是一个由 VAE 编码层组成的整个网络,类似于下面这样:

PIC

图 5.5:BBB 的示意图

因此,学习策略也与我们为 VAE 使用的策略类似,这是合乎逻辑的。我们再次使用变分学习的原理来学习 Q 的参数,并近似真实分布 P,但这一次我们要寻找的是最小化以下内容的参数 𝜃^():

𝜃⋆ = 𝜃 KL [q(w |𝜃)||P(w |D)]

这里,D 是我们的数据,w 是我们的网络权重,𝜃 是我们的分布参数,例如在高斯分布中,μσ 是参数。为了做到这一点,我们使用了贝叶斯学习中的一个重要成本函数:证据下界ELBO,也叫变分自由能)。我们用以下公式表示:

ℒ(D,𝜃 ) = KL [q(w |𝜃)||P (w)]− q(w |𝜃) [log P(D |w)]

这看起来相当复杂,但其实它只是我们在方程 5.4 中看到的内容的一种概括。我们可以按以下方式进行拆解:

  1. 在左边,我们有先验 P(w) 和近似分布 q(w|𝜃) 之间的 KL 散度。这与我们在上一节的方程 5.1-5.4 中看到的内容类似。在损失中加入 KL 散度使得我们可以调整参数 𝜃,使得我们的近似分布收敛到先验分布。

  2. 在右边,我们有关于给定神经网络权重 w 和变分分布的情况下,数据 D 的负对数似然的期望值。最小化这个(因为它是负对数似然)可以确保我们学习到的参数能最大化给定权重情况下数据的似然;我们的网络学会将输入映射到输出。

就像 VAE 一样,BBB 使用了重参数化技巧,使得我们能够通过网络参数反向传播梯度。和之前一样,我们从分布中采样。根据方程 5.5 中引入的 KL 散度形式,我们的损失函数变为如下:

 ∑N ℒ (D,𝜃) ≈ logq(wi |𝜃)− log P(wi) − log P(D |wi) i=1

N 是样本的数量,i 表示某个特定样本。虽然我们这里使用的是高斯先验,但这个方法的一个有趣特点是,它可以应用于各种分布。

下一步是使用我们的权重样本来训练网络:

  1. 首先,就像在 VAE 中一样,我们从高斯分布中采样 𝜖

    𝜖 ≈ 𝒩 (0,I)

  2. 接下来,我们将 𝜖 应用于某一层的权重,就像在 VAE 编码中一样:

    w = μ + log(1 + exp(ρ))⊙ 𝜖

    注意,在 BBB 中,σ 被参数化为 σ = log(1 + exp(ρ))。这确保了它始终是非负的(因为标准差不能是负数!)。

  3. 使用我们的参数 𝜃 = (μ,ρ),我们根据方程 3.10 定义我们的损失函数如下:

    f(w, 𝜃) = logq(w |𝜃 )− log P (w )P (D |w )

  4. 因为我们的神经网络包含均值和标准差的权重,所以我们需要分别计算它们的梯度。我们首先计算相对于均值 μ 的梯度:

     ∂f(w,-𝜃) ∂f-(w,𝜃) Δ μ = ∂w + ∂μ

    然后我们计算相对于标准差参数 ρ 的梯度:

     ∂f(w, 𝜃) 𝜖 ∂f (w, 𝜃) Δ ρ = --------------------+ -------- ∂w 1 + exp(− ρ) ∂ρ

  5. 现在,我们已经具备了通过反向传播更新权重所需的所有组件,这与典型的神经网络类似,唯一不同的是,我们使用各自的梯度更新均值和方差权重:

    μ ← μ − α Δμ ρ ← ρ − αΔ ρ

你可能注意到,在方程 5.14 和 5.15 中的梯度计算的第一项,正是你会为典型神经网络的反向传播计算的梯度;我们只是通过μρ特定的更新规则增强了这些梯度。

虽然这部分内容在数学方面相对较重,但我们可以将其分解为几个简单的概念:

  1. 与变分自编码器(VAE)中的编码类似,我们在这里使用表示多元分布均值和标准差的权重,唯一不同的是,这次这些权重构成了整个网络,而不仅仅是编码层。

  2. 因此,我们再次使用一个包含 KL 散度的损失函数:我们的目标是最大化 ELBO。

  3. 由于我们正在处理均值和标准差权重,我们将使用更新规则分别更新它们,规则使用的是各自权重集的梯度。

现在我们已经理解了 BBB 背后的核心原理,准备好看看它如何在代码中实现!

5.5 使用 TensorFlow 实现 BBB

在这一部分中,我们将看到如何在 TensorFlow 中实现 BBB。你将看到一些你已经熟悉的代码;层、损失函数和优化器的核心概念与我们在第三章 深度学习基础中所涵盖的非常相似。与第三章深度学习基础中的例子不同,我们将看到如何创建能够进行概率推断的神经网络。

第 1 步:导入包

我们首先导入相关包。重要的是,我们将导入tensorflow-probability,它将为我们提供网络的层,这些层用分布替代了点估计,并实现了重新参数化技巧。我们还设置了推理次数的全局参数,这将决定稍后我们从网络中采样的频率:


import tensorflow as tf 
import numpy as np 
import matplotlib.pyplot as plt 
import tensorflow_probability as tfp 

NUM_INFERENCES = 7

第 2 步:获取数据

然后我们下载 MNIST Fashion 数据集,这是一个包含十种不同衣物图像的数据集。我们还设置了类别名称,并推导出训练样本和类别的数量:


# download MNIST fashion data set 
fashion_mnist = tf.keras.datasets.fashion_mnist 
(train_images, train_labels), (test_images, test_labels) = fashion_mnist.load_data() 

# set class names 
CLASS_NAMES = ['T-shirt', 'Trouser', 'Pullover', 'Dress', 'Coat', 
'Sandal', 'Shirt', 'Sneaker', 'Bag', 'Ankle boot'] 

# derive number training examples and classes 
NUM_TRAIN_EXAMPLES = len(train_images) 
NUM_CLASSES = len(CLASS_NAMES)

第 3 步:辅助函数

接下来,我们创建一个辅助函数来定义我们的模型。如你所见,我们使用了一个非常简单的卷积神经网络结构进行图像分类,其中包括一个卷积层,随后是一个最大池化层和一个全连接层。卷积层和全连接层来自tensorflow-probability包,如tfp前缀所示。与定义权重的点估计不同,这里我们定义的是权重分布。

Convolution2DReparameterizationDenseReparameterization的名称所示,这些层将使用重参数化技巧在反向传播过程中更新权重参数:


def define_bayesian_model(): 
# define a function for computing the KL divergence 
kl_divergence_function = lambda q, p, _: tfp.distributions.kl_divergence( 
q, p 
) / tf.cast(NUM_TRAIN_EXAMPLES, dtype=tf.float32) 

# define our model 
model = tf.keras.models.Sequential([ 
tfp.layers.Convolution2DReparameterization( 
64, kernel_size=5, padding='SAME', 
kernel_divergence_fn=kl_divergence_function, 
activation=tf.nn.relu), 
tf.keras.layers.MaxPooling2D( 
pool_size=[2, 2], strides=[2, 2], 
padding='SAME'), 
tf.keras.layers.Flatten(), 
tfp.layers.DenseReparameterization( 
NUM_CLASSES, kernel_divergence_fn=kl_divergence_function, 
activation=tf.nn.softmax) 
]) 
  return model

我们还创建了另一个辅助函数来为我们编译模型,使用Adam作为优化器和分类交叉熵损失。提供了这个损失和前述的网络结构后,tensorflow-probability将自动将包含在卷积和密集层中的 KL 散度添加到交叉熵损失中。这种组合有效地相当于计算我们在方程式 5.9 中描述的 ELBO 损失:


def compile_bayesian_model(model): 
# define the optimizer 
optimizer = tf.keras.optimizers.Adam() 
# compile the model 
model.compile(optimizer, loss='categorical_crossentropy', 
metrics=['accuracy'], experimental_run_tf_function=False) 
# build the model 
model.build(input_shape=[None, 28, 28, 1]) 
  return model

步骤 4:模型训练

在我们能够训练模型之前,我们首先需要将训练数据的标签从整数转换为独热向量,因为这是 TensorFlow 对于分类交叉熵损失的期望。例如,如果一张图像显示一件 T 恤,而 T 恤的整数标签为 1,则该标签将被转换如下:[1,`` 0,`` 0,`` 0,`` 0,`` 0,`` 0,`` 0,`` 0,`` 0]


train_labels_dense = tf.one_hot(train_labels, NUM_CLASSES)

现在,我们准备在训练数据上训练我们的模型。我们将训练十个 epoch:


# use helper function to define the model architecture 
bayesian_model = define_bayesian_model() 
# use helper function to compile the model 
bayesian_model = compile_bayesian_model(bayesian_model) 
# initiate model training 
bayesian_model.fit(train_images, train_labels_dense, epochs=10)

步骤 5:推断

然后,我们可以使用训练好的模型对测试图像进行推断。在这里,我们预测测试集中前 50 张图像的类标签。对于每张图像,我们从网络中进行七次采样(由NUM_INFERENCES确定),这将为每张图像给出七个预测:


NUM_SAMPLES_INFERENCE = 50 
softmax_predictions = tf.stack( 
[bayesian_model.predict(test_images[:NUM_SAMPLES_INFERENCE]) 
     for _ in range(NUM_INFERENCES)],axis=0)

就这样:我们有了一个可用的 BBB 模型!让我们可视化测试集中的第一张图像以及该图像的七个不同预测。首先,我们获取类别预测:


# get the class predictions for the first image in the test set 
image_ind = 0 
# collect class predictions 
class_predictions = [] 
for ind in range(NUM_INFERENCES): 
prediction_this_inference = np.argmax(softmax_predictions[ind][image_ind]) 
class_predictions.append(prediction_this_inference) 
# get class predictions in human-readable form 
predicted_classes = [CLASS_NAMES[ind] for ind in class_predictions]

然后,我们可视化图像以及每次推断的预测类别:


# define image caption 
image_caption = [] 
for caption in range(NUM_INFERENCES): 
image_caption.append(f"Sample {caption+1}: {predicted_classes[caption]}\n") 
image_caption = ' '.join(image_caption) 
# visualise image and predictions 
plt.figure(dpi=300) 
plt.title(f"Correct class: {CLASS_NAMES[test_labels[image_ind]]}") 
plt.imshow(test_images[image_ind], cmap=plt.cm.binary) 
plt.xlabel(image_caption) 
plt.show()

查看Figure 5.7中的图像,在大多数样本中,网络预测为“踝靴”(这是正确的类别)。在两个样本中,网络还预测为“运动鞋”,这在图像显示鞋类时也是合理的。

PIC

图 5.6:来自采用 BBB 方法训练的网络的七个不同样本中的第一张测试图像的类预测

现在,我们每张图像有了七个预测,我们还可以计算这些预测之间的平均方差,以近似不确定性值:


# calculate variance across model predictions 
var_predictions = tf.reduce_mean( 
tf.math.reduce_variance(softmax_predictions, axis=0), 
    axis=1)

例如,时装 MNIST 数据集中第一张测试图像的不确定性值为 0.0000002。为了将此不确定性值置于上下文中,让我们从常规 MNIST 数据集加载一些图像,其中包含介于 0 到 9 之间的手写数字,并从我们训练过的模型中获取不确定性值。我们加载数据集,然后再次进行推断并获取不确定性值:


# load regular MNIST data set 
(train_images_mnist, train_labels_mnist), 
(test_images_mnist, test_labels_mnist) = 
tf.keras.datasets.mnist.load_data() 

# get model predictions in MNIST data 
softmax_predictions_mnist = 
tf.stack([bayesian_model.predict( 
test_images_mnist[:NUM_SAMPLES_INFERENCE]) 
for _ in range(NUM_INFERENCES)], axis=0) 

# calculate variance across model predictions in MNIST data 
var_predictions_mnist = tf.reduce_mean( 
tf.math.reduce_variance(softmax_predictions_mnist, axis=0), 
    axis=1)

然后,我们可以可视化比较时装 MNIST 数据集中前 50 张图像和常规 MNIST 数据集之间的不确定性值。

5.7 中,我们可以看到,来自常规 MNIST 数据集的图像的不确定性值远高于时尚 MNIST 数据集的图像。这是预期中的情况,因为我们的模型在训练时只看到了时尚 MNIST 图像,而常规 MNIST 数据集中的手写数字对于我们训练的模型来说是超出分布的。

PIC

图 5.7:时尚 MNIST 数据集(左)与常规 MNIST 数据集(右)中图像的不确定性值

BBB 可能是最常遇到的高度原则化的贝叶斯深度学习方法,但对于那些关注更好原则性方法的人来说,它并不是唯一的选择。在接下来的章节中,我们将介绍另一种高度原则化的方法,并了解它与 BBB 的区别。

5.6 可扩展的贝叶斯深度学习与概率反向传播

BBB 为贝叶斯推断与神经网络的结合提供了很好的介绍,但变分方法有一个关键的缺点:它们依赖于训练和推理时的采样。与标准神经网络不同,我们需要使用一系列 𝜖 值从权重参数中进行采样,以生成进行概率训练和推理所需的分布。

在 BBB 被引入的同时,哈佛大学的研究人员也在研究他们自己的贝叶斯推断与神经网络相结合的方法:概率反向传播,或称 PBP。像 BBB 一样,PBP 的权重也构成了一个分布的参数,在这种情况下是均值和方差权重(使用方差 σ²,而不是 σ)。实际上,相似之处不仅限于此——我们将看到许多与 BBB 相似的地方,但关键是,我们最终会采用一种不同的 BNN 近似方法,这种方法有其独特的优缺点。那么,让我们开始吧。

为了简化问题,并与各种 PBP 论文保持一致,我们将在阐述 PBP 核心思想时坚持使用单个权重。以下是一个小型神经网络中这些权重如何关联的可视化:

PIC

图 5.8:PBP 中神经网络权重的示意图

正如之前所见,我们的网络本质上是由两个子网络组成:一个用于均值权重,或者说 m,另一个用于方差权重,或者说 v。PBP 的核心思想是,对于每个权重,我们都有一个分布 P(w|D),我们正在尝试对其进行近似:

q(w) = 𝒩 (w|m, v)

这个符号现在应该很熟悉了,其中 P() 是真实(不可解)分布,q() 是近似分布。在 PBP 中,如公式 5.18 所示,这是由均值 m 和方差 v 参数化的高斯分布。

在 BBB 中,我们看到变分学习通过 ELBO 使用 KL 散度确保我们的权重分布收敛到我们的先验 P(w)。在 PBP 中,我们将再次使用 KL 散度,尽管这一次我们是间接地实现的。我们通过使用一种叫做假定密度滤波ADF)的过程来实现这一点。

ADF 是一种快速的顺序方法,用于最小化真实后验 P(w|D) 与某个近似 q(w|D) 之间的 KL 散度。这里的一个关键点是它是一个顺序算法:就像我们与标准神经网络一起使用的梯度下降一样,ADF 也是顺序地更新其参数。这使得它特别适合适应神经网络。ADF 算法可以通过两个关键步骤来描述:

  1. 初始化我们的参数,m = 0,v = 1;也就是说,我们从单位高斯 𝒩(0,1) 开始。

  2. 接下来,我们遍历每个数据点 x[i] ∈ x,并使用一组特定的更新方程更新我们的模型参数 mv,这两个参数分别进行更新。

虽然在本书的范围之外提供 ADF 的完整推导,但你应该知道,在通过 ADF 更新参数时,我们也在最小化 KL 散度。

因此,对于 PBP,我们需要调整典型的神经网络更新规则,使得权重沿着 ADF 的方向进行更新。我们通过以下更新规则来实现这一点,这些规则是从原始 ADF 方程推导出来的:

mnew = m + v ∂logZ-- ∂m  [ ( ) ] new 2 ∂-log-Z- ∂-log-Z- v = v − v ∂m − 2 ∂v

这里,log Z 表示高斯边际似然,其定义如下:

 2 logZ = − log p(y|m, v) = − 0.5 × log-v +-(y-−-m-) v

这是负对数似然NLL)。方程 5.21 对于我们学习 PBP 的参数至关重要,因为这是我们试图优化的损失函数——所以我们需要花些时间来理解其中的含义。就像我们在 BBB 中的损失(方程 5.9)一样,我们可以看到我们的对数Z损失包含了几个重要的信息:

  1. 在分子中,我们看到 (ym)²。这类似于我们在标准神经网络训练中常见的典型损失(L2 损失)。它包含了目标 y 和我们对该值的均值估计 m 之间的惩罚。

  2. 整个方程给出了 NLL 函数,它描述了我们的目标 y 作为我们由 mv 参数化的分布的联合概率。

这具有一些重要的性质,我们可以通过几个简单的例子来探索。让我们看一下对于给定目标 y = 0.6,参数 m = 0.8 和 v = 0.4 的损失:

 2 2 − 0.5× logv-+-(y −-m)- = − 0.5 × log(0.4)-+-(0.6-−-0.8)- = 1.095 v 0.4

在这里,我们可以看到我们的典型误差,在这种情况下是平方误差,(0.6 − 0.8)² = 0.04,并且我们知道,当m趋近于y时,这个误差会缩小。此外,似然函数会缩放我们的误差。这很重要,因为一个用于不确定性量化的良好条件模型在错误时会更不确定,在正确时会更自信。似然函数为我们提供了一种方法,确保在我们对错误预测不确定时,它的似然值较大,而对正确预测时,则较为确定。

我们可以通过替换另一个v的值,查看它如何改变 NLL,来观察这个过程。例如,我们可以将方差增加到v = 0.9:

 2 − 0.5× log(0.9)+-(0.6−-0.8)- = 0.036 0.9

方差的显著增加会导致 NLL 的显著下降。类似地,如果我们对一个正确的预测m = y有很高的方差,我们会看到 NLL 再次增加:

 log(0.9)+-(0.8−-0.8)2 − 0.5× 0.9 = 0.059

希望通过这个例子,你能看到使用 NLL 损失如何转化为我们输出的良好校准的不确定性估计。实际上,这个属性——利用方差来缩放目标函数——是所有有原则的 BNN 方法的基本组成部分:BBB 也做了这件事,尽管由于需要采样,它在纸面上展示起来有点复杂。

在实现过程中,我们将遇到一些 PBP 的低级细节。它们与 ADF 过程有关,我们鼓励你查看进一步阅读部分中的文章,以获得 PBP 和 ADF 的详细推导。

既然我们已经涵盖了 PBP 的核心概念,接下来让我们看看如何使用 TensorFlow 实现它。

5.7 实现 PBP

由于 PBP 相当复杂,我们将把它实现为一个类。这样做可以使我们的示例代码更加简洁,并且方便我们将不同的代码块进行模块化。它还将使得实验变得更容易,例如,如果你想探索改变网络中单元或层的数量。

第一步:导入库

我们首先导入各种库。在这个例子中,我们将使用 scikit-learn 的加利福尼亚住房数据集来预测房价:


from typing import List, Union, Iterable 
import math 
from sklearn import datasets 
from sklearn.model_selection import train_test_split 
import tensorflow as tf 
import numpy as np 
from tensorflow.python.framework import tensor_shape 
import tensorflow_probability as tfp

为了确保每次生成相同的输出,我们初始化我们的种子值:


RANDOM_SEED = 0 
np.random.seed(RANDOM_SEED) 
tf.random.set_seed(RANDOM_SEED)

然后我们可以加载数据集并创建训练集和测试集:


# load the California Housing dataset 
X, y = datasets.fetch_california_housing(return_X_y=True) 
# split the data (X) and targets (y) into train and test sets 
X_train, X_test, y_train, y_test = train_test_split( 
X, y, test_size=0.1, random_state=0 
)

第二步:辅助函数

接下来,我们定义两个辅助函数,确保我们的数据格式正确,一个用于输入,另一个用于输出数据:


def ensure_input(x, dtype, input_shape): 
# a function to ensure that our input is of the correct shape 
x = tf.constant(x, dtype=dtype) 
call_rank = tf.rank(tf.constant(0, shape=input_shape, dtype=dtype)) + 1 
if tf.rank(x) *<* call_rank: 
x = tf.reshape(x, [-1, * input_shape.as_list()]) 
return x 

def ensure_output(y, dtype, output_dim): 
# a function to ensure that our output is of the correct shape 
output_rank = 2 
y = tf.constant(y, dtype=dtype) 
if tf.rank(y) *<* output_rank: 
y = tf.reshape(y, [-1, output_dim]) 
    return y

我们还将创建一个简短的类来初始化一个伽玛分布:ReciprocalGammaInitializer。这个分布被用作 PBP 精度参数λ和噪声参数γ的先验。


class ReciprocalGammaInitializer: 
def __init__(self, alpha, beta): 
self.Gamma = tfp.distributions.Gamma(concentration=alpha, rate=beta) 

def __call__(self, shape: Iterable, dtype=None): 
g = 1.0 / self.Gamma.sample(shape) 
if dtype: 
g = tf.cast(g, dtype=dtype) 

        return g

对这些变量的深入处理对于理解 PBP 并不是必需的。如需进一步了解, 请参见进一步阅读部分中列出的 PBP 论文。

第三步:数据准备

在实现这些先决条件后,我们可以对数据进行归一化处理。在这里,我们将数据归一化为均值为零,标准差为单位。这是一个常见的预处理步骤,有助于模型更容易找到合适的权重:


def get_mean_std_x_y(x, y): 
# compute the means and standard deviations of our inputs and targets 
std_X_train = np.std(x, 0) 
std_X_train[std_X_train == 0] = 1 
mean_X_train = np.mean(x, 0) 
std_y_train = np.std(y) 
if std_y_train == 0.0: 
std_y_train = 1.0 
mean_y_train = np.mean(y) 
return mean_X_train, mean_y_train, std_X_train, std_y_train 

def normalize(x, y, output_shape): 
# use the means and standard deviations to normalize our inputs and targets 
x = ensure_input(x, tf.float32, x.shape[1]) 
y = ensure_output(y, tf.float32, output_shape) 
mean_X_train, mean_y_train, std_X_train, std_y_train = get_mean_std_x_y(x, y) 
x = (x - np.full(x.shape, mean_X_train)) / np.full(x.shape, std_X_train) 
y = (y - mean_y_train) / std_y_train 
return x, y 

# run our normalize() function on our data 
x, y = normalize(X_train, y_train, 1)

第 4 步:定义我们的模型类

现在我们可以开始定义我们的模型了。我们的模型将由三层组成:两层 ReLU 层和一层线性层。我们使用 Keras 的Layer来定义这些层。由于这一层的代码比较长,因此我们将其拆分成几个子部分。

首先,我们继承Layer类来创建我们自己的PBPLayer并定义init方法。我们的初始化方法设置了层中的单元数量:


from tensorflow.keras.initializers import HeNormal 

# a class to handle our PBP layers 
class PBPLayer(tf.keras.layers.Layer): 
def __init__(self, units: int, dtype=tf.float32, *args, **kwargs): 
super().__init__(dtype=tf.as_dtype(dtype), *args, **kwargs) 
self.units = units 
    ...

然后我们创建一个build()方法,用于定义我们层的权重。正如我们在上一节中讨论的,PBP 包含了均值权重和方差权重。由于一个简单的 MLP 由乘法组件或权重和偏置组成,我们将权重和偏置分解为均值和方差变量:


... 
def build(self, input_shape): 
input_shape = tensor_shape.TensorShape(input_shape) 
last_dim = tensor_shape.dimension_value(input_shape[-1]) 
self.input_spec = tf.keras.layers.InputSpec( 
min_ndim=2, axes={-1: last_dim} 
) 
self.inv_sqrtV1 = tf.cast( 
1.0 / tf.math.sqrt(1.0 * last_dim + 1), dtype=self.dtype 
) 
self.inv_V1 = tf.math.square(self.inv_sqrtV1) 

over_gamma = ReciprocalGammaInitializer(6.0, 6.0) 
self.weights_m = self.add_weight( 
"weights_mean", shape=[last_dim, self.units], 
initializer=HeNormal(), dtype=self.dtype, trainable=True, 
) 
self.weights_v = self.add_weight( 
"weights_variance", shape=[last_dim, self.units], 
initializer=over_gamma, dtype=self.dtype, trainable=True, 
) 
self.bias_m = self.add_weight( 
"bias_mean", shape=[self.units], 
initializer=HeNormal(), dtype=self.dtype, trainable=True, 
) 
self.bias_v = self.add_weight( 
"bias_variance", shape=[self.units], 
initializer=over_gamma, dtype=self.dtype, trainable=True, 
) 
self.Normal = tfp.distributions.Normal( 
loc=tf.constant(0.0, dtype=self.dtype), 
scale=tf.constant(1.0, dtype=self.dtype), 
) 
self.built = True 
    ...

weights_mweights_v变量是我们的均值和方差权重,构成了 PBP 模型的核心。我们将在通过模型拟合函数时继续定义PBPLayer。现在,我们可以继承该类来创建我们的 ReLU 层:


class PBdivLULayer(PBPLayer): 
@tf.function 
def call(self, x: tf.Tensor): 
"""Calculate deterministic output""" 
# x is of shape [batch, divv_units] 
x = super().call(x) 
z = tf.maximum(x, tf.zeros_like(x))  # [batch, units] 
return z 

@tf.function 
def predict(self, previous_mean: tf.Tensor, previous_variance: tf.Tensor): 
ma, va = super().predict(previous_mean, previous_variance) 
mb, vb = get_bias_mean_variance(ma, va, self.Normal) 
        return mb, vb

你可以看到我们重写了两个函数:call()predict()函数。call()函数调用我们常规的线性call()函数,然后应用我们在第三章,《深度学习基础》Chapter 3中看到的 ReLU 最大操作。predict()函数调用我们常规的predict()函数,但随后也调用了一个新函数get_bias_mean_variance()。该函数以数值稳定的方式计算偏置的均值和方差,如下所示:


def get_bias_mean_variance(ma, va, normal): 
variance_sqrt = tf.math.sqrt(tf.maximum(va, tf.zeros_like(va))) 
alpha = safe_div(ma, variance_sqrt) 
alpha_inv = safe_div(tf.constant(1.0, dtype=alpha.dtype), alpha) 
alpha_cdf = normal.cdf(alpha) 
gamma = tf.where( 
alpha *<* -30, 
-alpha + alpha_inv * (-1 + 2 * tf.math.square(alpha_inv)), 
safe_div(normal.prob(-alpha), alpha_cdf), 
) 
vp = ma + variance_sqrt * gamma 
bias_mean = alpha_cdf * vp 
bias_variance = bias_mean * vp * normal.cdf(-alpha) + alpha_cdf * va * ( 
1 - gamma * (gamma + alpha) 
) 
    return bias_mean, bias_variance

在我们定义好层之后,就可以构建我们的网络。我们首先创建一个包含网络中所有层的列表:


units = [50, 50, 1] 
layers = [] 
last_shape = X_train.shape[1] 

for unit in units[:-1]: 
layer = PBdivLULayer(unit) 
layer.build(last_shape) 
layers.append(layer) 
last_shape = unit 
layer = PBPLayer(units[-1]) 
layer.build(last_shape) 
layers.append(layer)

然后,我们创建一个PBP类,包含模型的fit()predict()函数,类似于你在使用 Keras 的tf.keras.Model类定义的模型。接下来,我们将看到一些重要的变量;让我们在这里一起了解它们:

  • alphabeta:这些是我们伽马分布的参数

  • Gamma:一个tfp.distributions.Gamma()类的实例,用于我们的伽马分布,它是 PBP 精度参数λ的超先验

  • layers:这个变量指定了模型中的层数

  • Normal:在这里,我们实例化了tfp.distributions.Normal()类,它实现了一个高斯概率分布(此处均值为 0,标准差为 1):


class PBP: 
def __init__( 
self, 
layers: List[tf.keras.layers.Layer], 
dtype: Union[tf.dtypes.DType, np.dtype, str] = tf.float32 
): 
self.alpha = tf.Variable(6.0, trainable=True, dtype=dtype) 
self.beta = tf.Variable(6.0, trainable=True, dtype=dtype) 
self.layers = layers 
self.Normal = tfp.distributions.Normal( 
loc=tf.constant(0.0, dtype=dtype), 
scale=tf.constant(1.0, dtype=dtype), 
) 
self.Gamma = tfp.distributions.Gamma( 
concentration=self.alpha, rate=self.beta 
) 

def fit(self, x, y, batch_size: int = 16, n_epochs: int = 1): 
data = tf.data.Dataset.from_tensor_slices((x, y)).batch(batch_size) 
for epoch_index in range(n_epochs): 
print(f"{epoch_index=}") 
for x_batch, y_batch in data: 
diff_square, v, v0 = self.update_gradients(x_batch, y_batch) 
alpha, beta = update_alpha_beta( 
self.alpha, self.beta, diff_square, v, v0 
) 
self.alpha.assign(alpha) 
self.beta.assign(beta) 

@tf.function 
def predict(self, x: tf.Tensor): 
m, v = x, tf.zeros_like(x) 
for layer in self.layers: 
m, v = layer.predict(m, v) 
return m, v 
    ...

PBP类的__init__函数创建了多个参数,但本质上是通过正态分布和伽马分布初始化我们的αβ超先验。此外,我们还保存了在上一步创建的层。

fit()函数更新我们层的梯度,然后更新αβ参数。更新梯度的函数定义如下:


... 
@tf.function 
def update_gradients(self, x, y): 
trainables = [layer.trainable_weights for layer in self.layers] 
with tf.GradientTape() as tape: 
tape.watch(trainables) 
m, v = self.predict(x) 
v0 = v + safe_div(self.beta, self.alpha - 1) 
diff_square = tf.math.square(y - m) 
logZ0 = logZ(diff_square, v0) 
grad = tape.gradient(logZ0, trainables) 
for l, g in zip(self.layers, grad): 
l.apply_gradient(g) 
        return diff_square, v, v0

在更新梯度之前,我们需要通过网络传播梯度。为此,我们将实现我们的predict()方法:


# ... PBPLayer continued 

@tf.function 
def predict(self, previous_mean: tf.Tensor, previous_variance: tf.Tensor): 
mean = ( 
tf.tensordot(previous_mean, self.weights_m, axes=[1, 0]) 
+ tf.expand_dims(self.bias_m, axis=0) 
) * self.inv_sqrtV1 

variance = ( 
tf.tensordot( 
previous_variance, tf.math.square(self.weights_m), axes=[1, 0] 
) 
+ tf.tensordot( 
tf.math.square(previous_mean), self.weights_v, axes=[1, 0] 
) 
+ tf.expand_dims(self.bias_v, axis=0) 
+ tf.tensordot(previous_variance, self.weights_v, axes=[1, 0]) 
) * self.inv_V1 

        return mean, variance

现在我们可以通过网络传播值,我们准备好实现我们的损失函数了。正如我们在前一节所看到的,我们使用 NLL(负对数似然),在这里我们将定义它:


pi = tf.math.atan(tf.constant(1.0, dtype=tf.float32)) * 4 
LOG_INV_SQRT2PI = -0.5 * tf.math.log(2.0 * pi) 

@tf.function 
def logZ(diff_square: tf.Tensor, v: tf.Tensor): 
v0 = v + 1e-6 
return tf.reduce_sum( 
-0.5 * (diff_square / v0) + LOG_INV_SQRT2PI - 0.5 * tf.math.log(v0) 
) 

@tf.function 
def logZ1_minus_logZ2(diff_square: tf.Tensor, v1: tf.Tensor, v2: tf.Tensor): 
return tf.reduce_sum( 
-0.5 * diff_square * safe_div(v2 - v1, v1 * v2) 
- 0.5 * tf.math.log(safe_div(v1, v2) + 1e-6) 
    )

现在我们可以通过网络传播值,并计算相对于损失的梯度(就像我们在标准神经网络中一样)。这意味着我们可以根据公式 5.19 和 5.20 中的更新规则,分别更新均值权重和方差权重:


# ... PBPLayer continued 

@tf.function 
def apply_gradient(self, gradient): 
dlogZ_dwm, dlogZ_dwv, dlogZ_dbm, dlogZ_dbv = gradient 

# Weights 
self.weights_m.assign_add(self.weights_v * dlogZ_dwm) 
new_mean_variance = self.weights_v - ( 
tf.math.square(self.weights_v) 
* (tf.math.square(dlogZ_dwm) - 2 * dlogZ_dwv) 
) 
self.weights_v.assign(non_negative_constraint(new_mean_variance)) 

# Bias 
self.bias_m.assign_add(self.bias_v * dlogZ_dbm) 
new_bias_variance = self.bias_v - ( 
tf.math.square(self.bias_v) 
* (tf.math.square(dlogZ_dbm) - 2 * dlogZ_dbv) 
) 
        self.bias_v.assign(non_negative_constraint(new_bias_variance))

如前一节所讨论,PBP 属于假设****密度滤波ADF)方法的类别。因此,我们根据 ADF 的更新规则更新αβ参数:


def update_alpha_beta(alpha, beta, diff_square, v, v0): 
alpha1 = alpha + 1 
v1 = v + safe_div(beta, alpha) 
v2 = v + beta / alpha1 
logZ2_logZ1 = logZ1_minus_logZ2(diff_square, v1=v2, v2=v1) 
logZ1_logZ0 = logZ1_minus_logZ2(diff_square, v1=v1, v2=v0) 
logZ_diff = logZ2_logZ1 - logZ1_logZ0 
Z0Z2_Z1Z1 = safe_exp(logZ_diff) 
pos_where = safe_exp(logZ2_logZ1) * (alpha1 - safe_exp(-logZ_diff) * alpha) 
neg_where = safe_exp(logZ1_logZ0) * (Z0Z2_Z1Z1 * alpha1 - alpha) 
beta_denomi = tf.where(logZ_diff *>*= 0, pos_where, neg_where) 
beta = safe_div(beta, tf.maximum(beta_denomi, tf.zeros_like(beta))) 

alpha_denomi = Z0Z2_Z1Z1 * safe_div(alpha1, alpha) - 1.0 

alpha = safe_div( 
tf.constant(1.0, dtype=alpha_denomi.dtype), 
tf.maximum(alpha_denomi, tf.zeros_like(alpha)), 
) 

    return alpha, beta

步骤 5:避免数值错误

最后,让我们定义一些辅助函数,以确保在拟合过程中避免数值错误:


@tf.function 
def safe_div(x: tf.Tensor, y: tf.Tensor, eps: tf.Tensor = tf.constant(1e-6)): 
_eps = tf.cast(eps, dtype=y.dtype) 
return x / (tf.where(y *>*= 0, y + _eps, y - _eps)) 

@tf.function 
def safe_exp(x: tf.Tensor, BIG: tf.Tensor = tf.constant(20)): 
return tf.math.exp(tf.math.minimum(x, tf.cast(BIG, dtype=x.dtype))) 

@tf.function 
def non_negative_constraint(x: tf.Tensor): 
    return tf.maximum(x, tf.zeros_like(x))

步骤 6:实例化我们的模型

就这样:训练 PBP 的核心代码完成了。现在我们准备实例化我们的模型,并在一些数据上进行训练。在这个例子中,我们使用较小的批次大小和一个训练周期:


model = PBP(layers) 
model.fit(x, y, batch_size=1, n_epochs=1)

步骤 7:使用我们的模型进行推理

现在我们已经得到了拟合的模型,接下来看看它在测试集上的表现如何。我们首先对测试集进行标准化:


# Compute our means and standard deviations 
mean_X_train, mean_y_train, std_X_train, std_y_train = get_mean_std_x_y( 
X_train, y_train 
) 

# Normalize our inputs 
X_test = (X_test - np.full(X_test.shape, mean_X_train)) / 
np.full(X_test.shape, std_X_train) 

# Ensure that our inputs are of the correct shape 
X_test = ensure_input(X_test, tf.float32, X_test.shape[1])

然后我们得到模型预测结果:均值和方差:


m, v = model.predict(X_test)

然后我们对这些值进行后处理,以确保它们具有正确的形状,并且在原始输入数据的范围内:


# Compute our variance noise - the baseline variation we observe in our targets 
v_noise = (model.beta / (model.alpha - 1) * std_y_train**2) 

# Rescale our mean values 
m = m * std_y_train + mean_y_train 

# Rescale our variance values 
v = v * std_y_train**2 

# Reshape our variables 
m = np.squeeze(m.numpy()) 
v = np.squeeze(v.numpy()) 
v_noise = np.squeeze(v_noise.numpy().reshape(-1, 1))

现在我们得到了预测结果,可以计算我们的模型表现如何。我们将使用标准误差指标 RMSE,以及我们在损失函数中使用的指标:NLL。我们可以使用以下公式计算它们:


rmse = np.sqrt(np.mean((y_test - m) ** 2)) 
test_log_likelihood = np.mean( 
-0.5 * np.log(2 * math.pi * v) 
- 0.5 * (y_test - m) ** 2 / v 
) 
test_log_likelihood_with_vnoise = np.mean( 
-0.5 * np.log(2 * math.pi * (v + v_noise)) 
- 0.5 * (y_test - m) ** 2 / (v + v_noise) 
)

评估这两个指标是任何回归任务的好做法,尤其是当你有模型不确定性估计时。RMSE 给出了标准误差指标,它允许你直接与非概率方法进行比较。NLL 则通过评估当模型表现好与表现差时模型的信心,来判断你的方法的校准程度,正如我们在本章前面讨论过的那样。总体来看,这些指标提供了贝叶斯模型性能的全面评估,你会在文献中反复看到它们的应用。

5.8 小结

在这一章中,我们学习了两个基础的、原则明确的贝叶斯深度学习模型。BBB 展示了如何利用变分推断高效地从权重空间进行采样并生成输出分布,而 PBP 则展示了通过不进行采样也能获得预测不确定性的可能性。这样,PBP 在计算上比 BBB 更高效,但每个模型都有其优缺点。

在 BBB 的情况下,虽然它在计算效率上不如 PBP,但它也更具适应性(特别是在 TensorFlow 中用于变分层的工具)。我们可以将其应用于各种不同的 DNN 架构,且相对不费力。代价则是在推理和训练时所需的采样:我们需要进行的不仅仅是一次前向传递才能获得输出分布。

相反,PBP 允许我们通过一次传递获得不确定性估计,但正如我们刚才所见,它的实现相当复杂。这使得它在适应其他网络架构时显得有些笨拙,尽管已经有实现(参见进一步阅读部分),但鉴于实施的技术开销以及与其他方法相比相对较小的收益,它并不是一种特别实用的方法。

总之,如果你需要稳健且原则清晰的 BNN 近似,并且在推理时不受内存或计算开销的限制,这些方法非常优秀。但如果你有有限的内存和/或计算资源,比如在边缘设备上运行,怎么办?在这种情况下,你可能需要转向更实用的方法来获得预测的不确定性。

第六章,使用标准深度学习工具箱的贝叶斯神经网络近似中,我们将看到如何使用 TensorFlow 中更熟悉的组件来创建更实用的概率神经网络模型。

5.9 进一步阅读

  • 神经网络中的权重不确定性,Charles Blundell 等人:这篇论文介绍了 BBB,并且是 BDL 文献中的关键文献之一。

  • 神经网络的实用变分推断,Alex Graves 等人:这是一篇关于神经网络中使用变分推断的有影响力的论文,介绍了一种简单的随机变分方法,可以应用于各种神经网络架构。

  • 可扩展贝叶斯神经网络学习的概率反向传播,José Miguel Hernández-Lobato 等人:BDL 文献中的另一项重要工作,介绍了 PBP,展示了如何通过更具可扩展性的方法实现贝叶斯推断。

  • 概率反向传播的实用考虑,Matt Benatan 等人:在这项工作中,作者介绍了使 PBP 更适合实际应用的方法。

  • 用于安全强化学习的完全贝叶斯递归神经网络,Matt Benatan 等人:这篇论文展示了如何将 PBP 适应于 RNN 架构,并展示了 BNN 在安全关键系统中的优势。

1 本书的范围不包括引导读者推导 ELBO,但我们鼓励读者参阅进一步阅读部分中的文本,以获得对 ELBO 更全面的概述。

第六章

使用标准工具箱进行贝叶斯深度学习

正如我们在前面的章节中看到的,普通的神经网络往往产生较差的不确定性估计,并且往往会做出过于自信的预测,而有些甚至根本无法生成不确定性估计。相比之下,概率架构提供了获得高质量不确定性估计的原则性方法;然而,在扩展性和适应性方面,它们有一些局限性。

尽管 PBP 和 BBB 都可以通过流行的机器学习框架来实现(正如我们在之前的 TensorFlow 示例中所展示的),但它们非常复杂。正如我们在上一章中看到的,实现一个简单的网络也并非易事。这意味着将它们适应到新架构中是一个笨拙且耗时的过程(特别是 PBP,尽管是可能的——参见完全贝叶斯递归神经网络用于安全强化 学习)。对于一些简单任务,例如第五章,贝叶斯深度学习的原则性方法中的示例,这并不是问题。但在许多现实世界的任务中,例如

机器翻译或物体识别等任务,需要更为复杂的网络架构。

虽然一些学术机构或大型研究组织可能具备足够的时间和资源来将这些复杂的概率方法适应到各种复杂的架构中,但在许多情况下,这并不可行。此外,越来越多的行业研究人员和工程师正在转向基于迁移学习的方法,使用预训练的网络作为模型的骨干。在这些情况下,简单地将概率机制添加到预定义架构中是不可行的。

为了解决这个问题,本章将探讨如何利用深度学习中的常见范式来开发概率模型。这里介绍的方法表明,通过相对较小的调整,您可以轻松地将大型复杂架构适应到高质量的不确定性估计中。我们甚至会介绍一些技术,使您能够从已训练的网络中获取不确定性估计!

本章将涵盖三种关键方法,以便在常见的深度学习框架中轻松进行模型不确定性估计。首先,我们将介绍蒙特卡洛 DropoutMC dropout),一种通过在推理时使用 Dropout 来引入预测方差的方法。其次,我们将介绍深度集成方法,即通过结合多个神经网络来促进不确定性估计和提高模型性能。最后,我们将探索将贝叶斯层添加到模型中的各种方法,使任何模型都能产生不确定性估计。

以下内容将在接下来的章节中讨论:

  • 通过 Dropout 引入近似贝叶斯推断

  • 使用集成方法进行模型不确定性估计

  • 探索通过贝叶斯最后一层方法增强神经网络

6.1 技术要求

要完成本章的实际任务,您需要一个 Python 3.8 环境,并安装 SciPy 堆栈以及以下附加的 Python 包:

  • TensorFlow 2.0

  • TensorFlow 概率

本书的所有代码可以在本书的 GitHub 仓库找到:github.com/PacktPublishing/Enhancing-Deep-Learning-with-Bayesian-Inference

6.2 通过 dropout 引入近似贝叶斯推断

Dropout 传统上用于防止神经网络的过拟合。它最早在 2012 年提出,现在被广泛应用于许多常见的神经网络架构,并且是最简单且最常用的正则化方法之一。Dropout 的核心思想是在训练过程中随机关闭(或丢弃)神经网络的某些单元。因此,模型不能仅依赖某一小部分神经元来解决任务。相反,模型被迫找到不同的方式来完成任务。这提高了模型的鲁棒性,并使其不太可能过拟合。

如果我们简化一个网络为 y = Wx,其中 y 是我们网络的输出,x 是输入,W 是我们的模型权重,我们可以将 dropout 理解为:

 ( { wj, p wˆj = ( 0, otherwise

其中 w[j] 是应用 dropout 后的新权重,w[j] 是应用 dropout 前的权重,p 是我们不应用 dropout 的概率。

原始的 dropout 论文建议随机丢弃网络中 50% 的单元,并对所有层应用 dropout。输入层的 dropout 概率不应相同,因为这意味着我们丢弃了 50% 的输入信息,这会使模型更难收敛。实际上,您可以尝试不同的 dropout 概率,找到最适合您的特定数据集和模型的丢弃率;这是另一个您可以优化的超参数。Dropout 通常作为一个独立的层,在所有标准的神经网络库中都可以找到。您通常在激活函数之后添加它:


from tensorflow.keras import Sequential 
from tensorflow.keras.layers import Flatten, Conv2D, MaxPooling2D, Dropout, Dense 

model = Sequential([ 
Conv2D(32, (3,3), activation="relu", input_shape=(28, 28, 1)), 
MaxPooling2D((2,2)), 
Dropout(0.2), 
Conv2D(64, (3,3), activation="relu"), 
MaxPooling2D((2,2)), 
Dropout(0.5), 
Flatten(), 
Dense(64, activation="relu"), 
Dropout(0.5), 
Dense(10) 
]) 

现在我们已经回顾了 dropout 的基本应用,让我们看看如何将其用于贝叶斯推断。

6.2.1 使用 dropout 进行近似贝叶斯推断

传统的 dropout 方法使得在测试时 dropout 网络的预测是确定性的,因为在推理过程中关闭了 dropout。然而,我们也可以利用 dropout 的随机性来为我们带来优势。这就是所谓的 蒙特卡罗 (MC) dropout,其思想如下:

  1. 我们在测试时使用 dropout。

  2. 我们不是只运行一次推理,而是运行多次(例如,30-100 次)。

  3. 然后我们对预测结果取平均,以获得我们的不确定性估计。

为什么这有益?正如我们之前所说,使用 dropout 可以迫使模型学习解决任务的不同方法。因此,当我们在推理过程中保持启用 dropout 时,我们使用的是稍微不同的网络,这些网络通过模型的不同路径处理输入数据。这种多样性在我们希望获得校准的不确定性评分时非常有用,正如我们在下一节中所看到的,我们将讨论深度集成的概念。我们现在不再为每个输入预测一个点估计(一个单一值),而是让网络生成一组值的分布(由多个前向传递组成)。我们可以使用这个分布来计算每个输入数据点的均值和方差,如 6.1所示。

PIC

图 6.1:MC dropout 示例

我们也可以用贝叶斯的方式来解释 MC dropout。使用这些稍微不同的网络进行 dropout 可以看作是从所有可能模型的分布中进行采样:网络所有参数(或权重)上的后验分布:

𝜃t ∼ P (𝜃|D )

这里,𝜃[t]是一个 dropout 配置,∼表示从我们的后验分布P(𝜃|D)中抽取的单个样本。这样,MC dropout 就相当于一种近似贝叶斯推断的方法,类似于我们在第五章中看到的方法,贝叶斯深度学习的原则性方法

现在我们已经对 MC dropout 的工作原理有所了解,让我们在 TensorFlow 中实现它。

6.2.2 实现 MC dropout

假设我们已经训练了本章第一个实践练习中描述的卷积架构的模型。现在,我们可以通过将training=True来在推理过程中使用 dropout:


def mc_dropout_inference( 
imgs: np.ndarray, 
nb_inference: int, 
model: Sequential 
) -*>* np.ndarray: 
"""" 
Run inference nb_inference times with random dropout enabled 
(training=True) 
""" 
divds = [] 
for _ in range(nb_inference): 
divds.append(model(imgs, training=True)) 
return tf.nn.softmax(divds, axis=-1).numpy() 

Predictions = mc_dropout_inference(test_images, 50, model)

这使得我们能够为模型的每次预测计算均值和方差。我们的Predictions变量的每一行都包含与每个输入相关的预测结果,这些预测是通过连续的前向传递获得的。从这些预测中,我们可以计算均值和方差,如下所示:


predictive_mean = np.mean(predictions, axis=0) 
predictive_variance = np.var(predictions, axis=0)

与所有神经网络一样,贝叶斯神经网络需要通过超参数进行一定程度的微调。以下三个超参数对于 MC dropout 尤为重要:

  • Dropout 层数:在我们的Sequential对象中,使用 dropout 的层数是多少,具体是哪些层。

  • Dropout 率:节点被丢弃的概率。

  • MC dropout 样本数量:这是 MC dropout 特有的一个新超参数。这里表示为nb_inference,它定义了在推理时从 MC dropout 网络中采样的次数。

我们现在已经看到 MC dropout 可以以一种新的方式使用,提供了一种简单直观的方法来利用熟悉的工具计算贝叶斯不确定性。但这并不是我们唯一可以使用的方法。在下一节中,我们将看到如何将集成方法应用于神经网络;这为我们提供了另一种逼近 BNN 的直接方法。

6.3 使用集成方法进行模型不确定性估计

本节将介绍深度集成方法:这是一种通过深度网络集成来获得贝叶斯不确定性估计的流行方法。

6.3.1 介绍集成方法

机器学习中一个常见的策略是将多个单一模型组合成一个模型委员会。学习这种模型组合的过程称为集成学习,而得到的模型委员会则称为集成模型。集成学习包含两个主要部分:首先,多个单一模型需要被训练。有多种策略可以从相同的训练数据中获得不同的模型:可以在不同的数据子集上训练模型,或者训练不同类型的模型或具有不同架构的模型,亦或是使用不同超参数初始化相同类型的模型。其次,需要将不同单一模型的输出进行组合。常见的组合单一模型预测的策略是直接取其平均值,或者对集成模型中的所有成员进行多数投票。更高级的策略包括取加权平均值,或者如果有更多的训练数据,则可以学习一个额外的模型来结合集成成员的不同预测结果。

集成方法在机器学习中非常流行,因为它们通常通过最小化意外选择性能较差模型的风险来提高预测性能。事实上,集成模型至少能够与任何单一模型一样好地执行。更重要的是,如果集成成员的预测存在足够的多样性,集成方法的表现将优于单一模型。这里的多样性意味着不同的集成成员在给定的数据样本上会犯不同的错误。例如,如果一些集成成员将一只狗的图像误分类为“猫”,但大多数集成成员做出了正确的预测(“狗”),那么集成模型的最终输出仍然是正确的(“狗”)。更一般来说,只要每个单一模型的准确率超过 50%,并且模型的错误是独立的,那么随着集成成员数量的增加,集成的预测性能将接近 100%的准确度。

除了提高预测性能外,我们还可以利用集成成员之间的一致性(或不一致性)来获得不确定性估计,并与集成的预测结果一起使用。例如,在图像分类的情况下,如果几乎所有集成成员都预测图像显示的是一只狗,那么我们可以说集成模型以高置信度(或低不确定性)预测为“狗”。相反,如果不同集成成员的预测存在显著的不一致,那么我们将观察到高不确定性,即集成成员输出之间的方差较大,这表明预测的置信度较低。

现在我们已经具备了对集成方法的基本理解,值得指出的是,我们在前一节中探讨的 MC Dropout 也可以看作一种集成方法。当我们在推理过程中启用 Dropout 时,我们实际上每次都在运行一个略有不同的(子)网络。这些不同子网络的组合可以看作是多个模型的委员会,因此也是一种集成方法。这一观察促使谷歌团队研究从深度神经网络(DNN)创建集成的替代方法,最终发现了深度集成(Lakshminarayan 等,2016),这一方法将在接下来的章节中介绍。

6.3.2 引入深度集成

深度集成的主要思想很简单:训练多个不同的深度神经网络(DNN)模型,然后通过平均它们的预测结果来提高模型性能,并利用这些模型预测结果的一致性来估计预测的不确定性。

更正式地说,假设我们有一些训练数据X,其中 X ∈ℝ^(D),以及相应的目标标签y。例如,在图像分类中,训练数据是图像,目标标签是表示图像中显示的是哪一类物体的整数,所以 y ∈{1,...,K},其中 K 是类别的总数。训练一个单一的神经网络意味着我们对标签建模概率预测分布 p**𝜃,并优化 𝜃,即神经网络的参数。对于深度集成,我们训练M个神经网络,它们的参数可以表示为{𝜃[m]}[m=1]^(M),其中每个 𝜃[m] 都是使用Xy独立优化的(这意味着我们在相同的数据上独立训练每个神经网络)。深度集成成员的预测通过平均值进行结合,使用 p(y|x) = M^(−1) ∑ [m=1]^(M)p𝜃[m]

6.2 说明了深度集成的思想。在这里,我们训练了 M = 3 个不同的前馈神经网络。请注意,每个网络都有自己独特的网络权重集,正如通过连接网络节点的边缘厚度不同所示。三个网络中的每一个都会输出自己的预测分数,如绿色节点所示,我们通过平均这些分数来进行结合。

图片

图 6.2:深度集成示例。请注意,三个网络在权重上有所不同,正如通过不同厚度的边缘所示。

如果只有一个数据集可供训练,我们如何训练多个不同的神经网络模型?原始论文提出的策略(也是目前最常用的策略)是每次训练都从网络权重的随机初始化开始。如果每次训练都从不同的权重集合开始,那么不同的训练运行可能会产生不同的网络,其训练数据的函数逼近方式也会有所不同。这是因为神经网络往往拥有比训练数据集中的样本数量更多的权重参数。因此,训练数据集中的相同观测值可以通过许多不同的权重参数组合来逼近。在训练过程中,不同的神经网络模型将各自收敛到自己的参数组合,并在损失函数的局部最优点上占据不同位置。因此,不同的神经网络通常会对给定的数据样本(例如,一只狗的图像)有不同的看法。这也意味着不同的神经网络在分类数据样本时可能会犯不同的错误。集成中不同网络之间的共识程度提供了关于集成模型对某一数据点预测的置信度信息:网络越一致,我们对预测的信心就越强。

使用相同训练数据集训练不同神经网络模型的替代方法包括:在训练过程中使用迷你批次的随机排序、为每次训练运行使用不同的超参数,或为每个模型使用不同的网络架构。这些策略也可以结合使用,精确理解哪些策略组合能带来最佳结果(无论是预测性能还是预测不确定性)仍然是一个活跃的研究领域。

6.3.3 实现深度集成

以下代码示例展示了如何使用随机权重初始化策略训练深度集成模型,以获得不同的集成成员。

步骤 1:导入库

我们首先导入相关的包,并将集成数量设置为3,用于本代码示例:


import tensorflow as tf 
import numpy as np 
import matplotlib.pyplot as plt 

ENSEMBLE_MEMBERS = 3

步骤 2:获取数据

然后,我们下载MNIST`` Fashion数据集,这是一个包含十种不同服装项目图像的数据集:


# download data set 
fashion_mnist = tf.keras.datasets.fashion_mnist 
# split in train and test, images and labels 
(train_images, train_labels), (test_images, test_labels) = fashion_mnist.load_data() 

# set class names 
CLASS_NAMES = ['T-shirt', 'Trouser', 'Pullover', 'Dress', 'Coat', 
               'Sandal', 'Shirt', 'Sneaker', 'Bag', 'Ankle boot']

步骤 3:构建集成模型

接下来,我们创建一个辅助函数来定义我们的模型。如你所见,我们使用一个简单的图像分类器结构,包含两个卷积层,每个卷积层后跟一个最大池化操作,以及若干全连接层:


def build_model(): 
# we build a forward neural network with tf.keras.Sequential 
model = tf.keras.Sequential([ 
# we define two convolutional layers followed by a max-pooling operation each 
tf.keras.layers.Conv2D(filters=32, kernel_size=(5,5), padding='same', 
activation='relu', input_shape=(28, 28, 1)), 
tf.keras.layers.MaxPool2D(strides=2), 
tf.keras.layers.Conv2D(filters=48, kernel_size=(5,5), padding='valid', 
activation='relu'), 
tf.keras.layers.MaxPool2D(strides=2), 
# we flatten the matrix output into a vector 
tf.keras.layers.Flatten(), 
# we apply three fully-connected layers 
tf.keras.layers.Dense(256, activation='relu'), 
tf.keras.layers.Dense(84, activation='relu'), 
tf.keras.layers.Dense(10) 
]) 

return model 

我们还创建了另一个辅助函数,使用Adam作为优化器,并采用类别交叉熵损失来编译模型:


def compile_model(model): 
model.compile(optimizer='adam', 
loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True), 
metrics=['accuracy']) 
return model 

步骤 4:训练

然后,我们在相同的数据集上训练三个不同的网络。由于网络权重是随机初始化的,这将导致三个不同的模型。你会看到不同模型的训练准确度略有差异:


deep_ensemble = [] 
for ind in range(ENSEMBLE_MEMBERS): 
model = build_model() 
model = compile_model(model) 
print(f"Train model {ind:02}") 
model.fit(train_images, train_labels, epochs=10) 
    deep_ensemble.append(model)

步骤 5:推理

然后,我们可以执行推理并获得测试集中的每个模型对所有图像的预测结果。我们还可以对三个模型的预测结果取平均值,这样每个图像就会有一个预测向量:


# get logit predictions for all three models for images in the test split 
ensemble_logit_predictions = [model(test_images) for model in deep_ensemble] 
# convert logit predictions to softmax 
ensemble_softmax_predictions = [ 
tf.nn.softmax(logits, axis=-1) for logits in ensemble_logit_predictions] 

# take mean across models, this will result in one prediction vector per image 
ensemble_predictions = tf.reduce_mean(ensemble_softmax_predictions, axis=0)

就这样。我们已经训练了一个网络集成并进行了推理。由于现在每个图像都有多个预测,我们还可以查看三个模型预测结果不一致的图像。

比如,我们可以找到预测结果不一致最多的图像并将其可视化:


# calculate variance across model predictions 
ensemble_std = tf.reduce_mean( 
tf.math.reduce_variance(ensemble_softmax_predictions, axis=0), 
axis=1) 
# find index of test image with highest variance across predictions 
ind_disagreement = np.argmax(ensemble_std) 

# get predictions per model for test image with highest variance 
ensemble_disagreement = [] 
for ind in range(ENSEMBLE_MEMBERS): 
model_prediction = np.argmax(ensemble_softmax_predictions[ind][ind_disagreement]) 
ensemble_disagreement.append(model_prediction) 
# get class predictions 
predicted_classes = [CLASS_NAMES[ind] for ind in ensemble_disagreement] 

# define image caption 
image_caption = \ 
f"Network 1: {predicted_classes[0]}\n" + \ 
f"Network 2: {predicted_classes[1]}\n" + \ 
f"Network 3: {predicted_classes[2]}\n" 

# visualise image and predictions 
plt.figure() 
plt.title(f"Correct class: {CLASS_NAMES[test_labels[ind_disagreement]]}") 
plt.imshow(test_images[ind_disagreement], cmap=plt.cm.binary) 
plt.xlabel(image_caption) 
plt.show()

看看 6.3中的图像,甚至对于人类来说,也很难判断图像中是 T 恤、衬衫还是包:

PIC

图 6.3:集成预测中方差最大的图像。正确的真实标签是"t-shirt",但即使是人类也很难判断。

虽然我们已经看到深度集成有几个有利的特性,但它们也不是没有局限性。在下一节中,我们将探讨在考虑深度集成时可能需要注意的事项。

6.3.4 深度集成的实际局限性

从研究环境到大规模生产环境中应用集成模型时,一些实际局限性变得显而易见。我们知道,理论上,随着我们增加更多的集成成员,集成模型的预测性能和不确定性估计会有所提升。然而,增加更多集成成员是有代价的,因为集成模型的内存占用和推理成本会随着集成成员数量的增加而线性增长。这可能使得在生产环境中部署集成模型成为一个高成本的选择。对于我们添加到集成中的每一个神经网络,我们都需要存储一组额外的网络权重,这会显著增加内存需求。同样,对于每个网络,我们还需要在推理过程中进行额外的前向传递。尽管不同网络的推理可以并行进行,因此推理时间的影响可以得到缓解,但这种方法仍然需要比单一模型更多的计算资源。由于更多的计算资源往往意味着更高的成本,使用集成模型与单一模型之间的决策需要在更好的性能和不确定性估计的好处与成本增加之间进行权衡。

最近的研究尝试解决或减轻这些实际限制。例如,在一种叫做 BatchEnsembles([?])的方法中,所有集成成员共享一个基础权重矩阵。每个集成成员的最终权重矩阵是通过将该共享权重矩阵与一个唯一的秩一矩阵按元素相乘得到的,这个秩一矩阵对每个集成成员都是唯一的。这减少了每增加一个集成成员需要存储的参数数量,从而减小了内存占用。BatchEnsembles 的计算成本也得到了降低,因为它们可以利用向量化,并且所有集成成员的输出可以在一次前向传递中计算出来。在另一种方法中,称为多输入/多输出处理(MIMO;[?]),单个网络被鼓励学习多个独立的子网络。在训练过程中,多个输入与多个相应标注的输出一起传递。例如,网络会被呈现三张图片:一张狗的、一张猫的和一张鸡的。相应的输出标签也会传递,网络需要学习在第一个输出节点上预测“狗”,在第二个输出节点上预测“猫”,在第三个输出节点上预测“鸡”。在推理过程中,一张单独的图片会被重复三次,MIMO 集成会产生三个不同的预测(每个输出节点一个)。因此,MIMO 方法的内存占用和计算成本几乎与单一神经网络相当,同时仍能提供集成方法的所有优势。

6.4 探索贝叶斯最后一层方法在神经网络增强中的应用

通过第五章、《贝叶斯深度学习的原则方法》和第六章、《使用标准工具箱进行贝叶斯深度学习》,我们探索了多种用于深度神经网络(DNN)的贝叶斯推理方法。这些方法在每一层中都引入了某种形式的不确定性信息,无论是通过显式的概率方法,还是通过基于集成或丢弃法的近似。这些方法有其独特的优势。它们一致的贝叶斯(或者更准确地说,近似贝叶斯)机制意味着它们是一致的:相同的原理在每一层都得到应用,无论是在网络架构还是更新规则方面。这使得从理论角度解释它们变得更容易,因为我们知道任何理论上的保证都适用于每一层。除此之外,这还意味着我们能够在每一层访问不确定性:我们可以像在标准深度学习模型中利用嵌入一样,利用这些网络中的嵌入,并且我们将能够同时访问这些嵌入的不确定性。

然而,这些网络也有一些缺点。正如我们所看到的,像 PBP 和 BBB 这样的算法具有更复杂的机制,这使得它们更难应用于更复杂的神经网络架构。本章前面讨论的内容表明,我们可以通过使用 MC dropout 或深度集成来绕过这些问题,但它们会增加我们的计算和/或内存开销。此时,贝叶斯最后一层BLL)方法(参见 6.4)便派上用场。这类方法既能让我们灵活地使用任何神经网络架构,同时比 MC dropout 或深度集成方法在计算和内存上更为高效。

图片

图 6.4: Vanilla NN 与 BLL 网络的比较

正如你可能已经猜到的,BLL 方法背后的基本原理是仅在最后一层估计不确定性。但是你可能没有猜到的是,为什么这会成为可能。深度学习的成功归因于神经网络的非线性特性:连续的非线性变换使其能够学习高维数据的丰富低维表示。然而,这种非线性使得模型不确定性估计变得困难。线性模型的模型不确定性估计有现成的封闭形式解,但不幸的是,对于我们高度非线性的 DNN 来说,情况并非如此。那么,我们能做什么呢?

幸运的是,DNN 学到的表示也可以作为更简单线性模型的输入。通过这种方式,我们让 DNN 来承担繁重的工作:将高维输入空间压缩为特定任务的低维表示。因此,神经网络中的倒数第二层要处理起来容易得多;毕竟,在大多数情况下,我们的输出仅仅是该层的某种线性变换。这意味着我们可以将线性模型应用于该层,这也意味着我们可以应用封闭形式解来进行模型不确定性估计。

我们也可以利用其他最后一层方法;最近的研究表明,当仅在最后一层应用时,MC dropout 也很有效。尽管这仍然需要多次前向传播,但这些前向传播只需在单一层中完成,因此在计算上更加高效,尤其是对于较大的模型。

6.4.1 贝叶斯推理的最后一层方法

Jasper Snoek 等人在他们 2015 年的论文《可扩展 贝叶斯优化使用深度神经网络》中提出的方法,引入了使用事后贝叶斯线性回归器来获得 DNN 模型不确定性的概念。该方法被设计为一种实现类似高斯过程的高质量不确定性估计的方式,并且具有更好的可扩展性。

该方法首先涉及在一些数据X和目标y上训练一个神经网络(NN)。这个训练阶段训练一个线性输出层,z[i],结果是一个生成点估计的网络(这在标准的深度神经网络中是典型的)。然后,我们将倒数第二层(或最后一层隐藏层)z[i−1]作为我们的基础函数集。从这里开始,只需要将最后一层替换为贝叶斯线性回归器。现在,我们的网络将生成预测的均值和方差,而不是点估计。关于该方法和自适应基础回归的更多细节,请参阅 Jasper Snoek 等人的论文,以及 Christopher Bishop 的模式识别与机器学习

现在,让我们看看如何通过代码实现这一过程。

步骤 1:创建和训练我们的基础模型

首先,我们设置并训练我们的网络:


from tensorflow.keras import Model, Sequential, layers, optimizers, metrics, losses 
import tensorflow as tf 
import tensorflow_probability as tfp 
from sklearn.datasets import load_boston 
from sklearn.model_selection import train_test_split 
from sklearn.preprocessing import StandardScaler 
from sklearn.metrics import mean_squared_error 
import pandas as pd 
import numpy as np 

seed = 213 
np.random.seed(seed) 
tf.random.set_seed(seed) 
dtype = tf.float32 

boston = load_boston() 
data = boston.data 
targets = boston.target 

X_train, X_test, y_train, y_test = train_test_split(data, targets, test_size=0.2) 

# Scale our inputs 
scaler = StandardScaler() 
X_train = scaler.fit_transform(X_train) 
X_test = scaler.transform(X_test) 

model = Sequential() 
model.add(layers.Dense(20, input_dim=13, activation='relu', name='layer_1')) 
model.add(layers.Dense(8, activation='relu', name='layer_2')) 
model.add(layers.Dense(1, activation='relu', name='layer_3')) 

model.compile(optimizer=optimizers.Adam(), 
loss=losses.MeanSquaredError(), 
metrics=[metrics.RootMeanSquaredError()],) 

num_epochs = 200 
model.fit(X_train, y_train, epochs=num_epochs) 
mse, rmse = model.evaluate(X_test, y_test)

步骤 2:使用神经网络层作为基础函数

现在我们已经有了基础网络,我们只需要访问倒数第二层,这样我们就可以将其作为基础函数传递给我们的贝叶斯回归器。这可以通过使用 TensorFlow 的高级 API 轻松完成,例如:


basis_func = Model(inputs=self.model.input, 
                           outputs=self.model.get_layer('layer_2').output)

这将构建一个模型,允许我们通过简单地调用其predict方法来获得第二个隐藏层的输出:


layer_2_output = basis_func.predict(X_test)

这就是我们为传递给贝叶斯线性回归器准备基础函数所需要做的一切。

步骤 3:为贝叶斯线性回归准备我们的变量

对于贝叶斯回归器,我们假设我们的输出,y[i] ∈ y,根据与输入x[i] ∈ X的线性关系条件地服从正态分布:

yi = 𝒩 (α + x⊺iβ, σ²)

这里,α是我们的偏置项,β是我们的模型系数,σ²是与我们的预测相关的方差。我们还将对这些参数做出一些先验假设,即:

α ≈ 𝒩 (0,1) β ≈ 𝒩 (0,1) σ² ≈ |𝒩 (0,1)|

请注意,公式 6.6 表示的是高斯分布的半正态分布。为了将贝叶斯回归器包装成易于(且实用地)与我们的 Keras 模型集成的形式,我们将创建一个BayesianLastLayer类。这个类将使用 TensorFlow Probability 库,使我们能够实现贝叶斯回归器所需的概率分布和采样函数。让我们逐步了解我们类的各个组件:


class BayesianLastLayer(): 

def __init__(self, 
model, 
basis_layer, 
n_samples=1e4, 
n_burnin=5e3, 
step_size=1e-4, 
n_leapfrog=10, 
adaptive=False): 
# Setting up our model 
self.model = model 
self.basis_layer = basis_layer 
self.initialize_basis_function() 
# HMC Settings 
# number of hmc samples 
self.n_samples = int(n_samples) 
# number of burn-in steps 
self.n_burnin = int(n_burnin) 
# HMC step size 
self.step_size = step_size 
# HMC leapfrog steps 
self.n_leapfrog = n_leapfrog 
# whether to be adaptive or not 
        self.adaptive = adaptive

如我们所见,我们的类在实例化时至少需要两个参数:model,即我们的 Keras 模型;和basis``_layer,即我们希望馈送给贝叶斯回归器的层输出。接下来的参数都是哈密顿蒙特卡罗HMC)采样的参数,我们为其定义了一些默认值。根据输入的不同,这些值可能需要调整。例如,对于更高维度的输入(例如,如果你使用的是layer``_1),你可能希望进一步减小步长并增加燃烧期步骤的数量以及总体样本数。

第 4 步:连接我们的基础函数模型

接下来,我们简单定义几个函数,用于创建我们的基础函数模型并获取其输出:


def initialize_basis_function(self): 
self.basis_func = Model(inputs=self.model.input, 
outputs=self.model.get_layer(self.basis_layer).output) 

def get_basis(self, X): 
        return self.basis_func.predict(X)

第 5 步:创建适配贝叶斯线性回归参数的方法

现在事情变得有些复杂。我们需要定义fit()方法,它将使用 HMC 采样来找到我们的模型参数αβσ²。我们将在这里提供代码做了什么的概述,但关于采样的更多(实践)信息,我们推荐读者参考 Osvaldo Martin 的《Python 贝叶斯分析》。

首先,我们使用方程 4.3-4.5 中描述的先验定义一个联合分布。得益于 TensorFlow Probability 的distributions模块,这非常简单:


def fit(self, X, y): 
X = tf.convert_to_tensor(self.get_basis(X), dtype=dtype) 
y = tf.convert_to_tensor(y, dtype=dtype) 
y = tf.reshape(y, (-1, 1)) 
D = X.shape[1] 

# Define our joint distribution 
distribution = tfp.distributions.JointDistributionNamedAutoBatched( 
dict( 
sigma=tfp.distributions.HalfNormal(scale=tf.ones([1])), 
alpha=tfp.distributions.Normal( 
loc=tf.zeros([1]), 
scale=tf.ones([1]), 
), 
beta=tfp.distributions.Normal( 
loc=tf.zeros([D,1]), 
scale=tf.ones([D,1]), 
), 
y=lambda beta, alpha, sigma: 
tfp.distributions.Normal( 
loc=tf.linalg.matmul(X, beta) + alpha, 
scale=sigma 
) 
) 
) 
. . .

然后,我们使用 TensorFlow Probability 的HamiltonianMonteCarlo采样器类来设置我们的采样器。为此,我们需要定义目标对数概率函数。distributions模块使得这一过程相当简单,但我们仍然需要定义一个函数,将我们的模型参数传递给分布对象的log``_prob()方法(第 28 行)。然后我们将其传递给hmc``_kernel的实例化:


. . . 
# Define the log probability function 
def target_log_prob_fn(beta, alpha, sigma): 
return distribution.log_prob(beta=beta, alpha=alpha, sigma=sigma, y=y) 

# Define the HMC kernel we'll be using for sampling 
hmc_kernel  = tfp.mcmc.HamiltonianMonteCarlo( 
target_log_prob_fn=target_log_prob_fn, 
step_size=self.step_size, 
num_leapfrog_steps=self.n_leapfrog 
) 

# We can use adaptive HMC to automatically adjust the kernel step size 
if self.adaptive: 
adaptive_hmc = tfp.mcmc.SimpleStepSizeAdaptation( 
inner_kernel = hmc_kernel, 
num_adaptation_steps=int(self.n_burnin * 0.8) 
) 
. . .

现在一切已经设置好,我们准备运行采样器了。为此,我们调用mcmc.sample``_chain()函数,传入我们的 HMC 参数、模型参数的初始状态和我们的 HMC 采样器。然后我们运行采样,它会返回states,其中包含我们的参数样本,以及kernel``_results,其中包含一些关于采样过程的信息。我们关心的信息是关于接受样本的比例。如果采样器运行成功,我们将有一个较高比例的接受样本(表示接受率很高)。如果采样器没有成功,接受率会很低(甚至可能是 0%!),这时我们可能需要调整采样器的参数。我们会将这个信息打印到控制台,以便随时监控接受率(我们将对sample``_chain()的调用封装在run``_chain()函数中,这样它可以扩展为多链采样):


. . . 
# If we define a function, we can extend this to multiple chains. 
@tf.function 
def run_chain(): 
states, kernel_results = tfp.mcmc.sample_chain( 
num_results=self.n_samples, 
num_burnin_steps=self.n_burnin, 
current_state=[ 
tf.zeros((X.shape[1],1), name='init_model_coeffs'), 
tf.zeros((1), name='init_bias'), 
tf.ones((1), name='init_noise'), 
], 
kernel=hmc_kernel 
) 
return states, kernel_results 

print(f'Running HMC with {self.n_samples} samples.') 
states, kernel_results = run_chain() 

print('Completed HMC sampling.') 
coeffs, bias, noise_std = states 
accepted_samples = kernel_results.is_accepted[self.n_burnin:] 
acceptance_rate = 100*np.mean(accepted_samples) 
# Print the acceptance rate - if this is low, we need to check our 
# HMC parameters 
        print('Acceptance rate: %0.1f%%' % (acceptance_rate))

一旦我们运行了采样器,我们就可以获取我们的模型参数。我们从后燃烧样本中提取它们,并将其分配给类变量,以便后续推断使用:


# Obtain the post-burnin samples 
self.model_coeffs = coeffs[self.n_burnin:,:,0] 
self.bias = bias[self.n_burnin:] 
        self.noise_std = noise_std[self.n_burnin:]

第 6 步:推断

我们需要做的最后一件事是实现一个函数,利用我们联合分布的学习到的参数来进行预测。为此,我们将定义两个函数:get``_divd``_dist(),它将根据我们的输入获取后验预测分布;以及predict(),它将调用get``_divd``_dist()并计算我们后验分布的均值(μ)和标准差(σ):


def get_divd_dist(self, X): 
predictions = (tf.matmul(X, tf.transpose(self.model_coeffs)) + 
self.bias[:,0]) 
noise = (self.noise_std[:,0] * 
tf.random.normal([self.noise_std.shape[0]])) 
return predictions + noise 

def predict(self, X): 
X = tf.convert_to_tensor(self.get_basis(X), dtype=dtype) 
divd_dist = np.zeros((X.shape[0], self.model_coeffs.shape[0])) 
X = tf.reshape(X, (-1, 1, X.shape[1])) 
for i in range(X.shape[0]): 
divd_dist[i,:] = self.get_divd_dist(X[i,:]) 

y_divd = np.mean(divd_dist, axis=1) 
y_std = np.std(divd_dist, axis=1) 
        return y_divd, y_std

就这样!我们实现了 BLL!通过这个类,我们可以通过使用倒数第二层神经网络作为贝叶斯回归的基函数,获得强大而有原则的贝叶斯不确定性估计。使用它的方法非常简单,只需传入我们的模型并定义我们希望使用哪个层作为基函数:


bll = BayesianLastLayer(model, 'layer_2') 

bll.fit(X_train, y_train) 

y_divd, y_std = bll.predict(X_test)

虽然这是一个强大的工具,但并不总是适合当前任务。你可以自己进行实验:尝试创建一个更大的嵌入层。随着层的大小增加,你应该会看到采样器的接受率下降。一旦它变得足够大,接受率甚至可能下降到 0%。因此,我们需要修改采样器的参数:减少步长,增加样本数,并增加烧入样本数。随着嵌入维度的增加,获取一个代表性样本集来描述分布变得越来越困难。

对于一些应用来说,这不是问题,但在处理复杂的高维数据时,这可能很快成为一个问题。计算机视觉、语音处理和分子建模等领域的应用都依赖于高维嵌入。这里的一个解决方案是进一步降低这些嵌入的维度,例如通过降维。但这样做可能会对这些编码产生不可预测的影响:事实上,通过降低维度,你可能会无意中去除一些不确定性的来源,从而导致更差的质量的不确定性估计。

那么,我们能做些什么呢?幸运的是,我们可以使用一些其他的最后一层选项。接下来,我们将看看如何使用最后一层的丢弃法(dropout)来逼近这里介绍的贝叶斯线性回归方法。

6.4.2 最后一层 MC 丢弃

在本章早些时候,我们看到如何在测试时使用丢弃法获取模型预测的分布。在这里,我们将这个概念与最后一层不确定性概念结合:添加一个 MC 丢弃层,但仅作为我们添加到预训练网络中的一个单一层。

步骤 1:连接到我们的基础模型

与贝叶斯最后一层方法类似,我们首先需要从模型的倒数第二层获取输出:


basis_func = Model(inputs=model.input, 
                   outputs=model.get_layer('layer_2').output)

步骤 2:添加 MC 丢弃层

现在,我们不再实现一个贝叶斯回归器,而是简单地实例化一个新的输出层,应用丢弃法(dropout)到倒数第二层:


ll_dropout = Sequential() 
ll_dropout.add(layers.Dropout(0.25)) 
ll_dropout.add(layers.Dense(1, input_dim=8, activation='relu', name='dropout_layer'))

步骤 3:训练 MC 丢弃的最后一层

因为我们现在增加了一个新的最终层,我们需要进行额外的训练步骤,让它能够学习从倒数第二层到新输出的映射;但由于我们原始模型已经完成了大部分工作,这个训练过程既计算成本低,又运行快速:


ll_dropout.compile(optimizer=optimizers.Adam(), 
loss=losses.MeanSquaredError(), 
metrics=[metrics.RootMeanSquaredError()],) 
num_epochs = 50 
ll_dropout.fit(basis_func.predict(X_train), y_train, epochs=num_epochs)

第 4 步:获取不确定性

现在我们的最后一层已经训练完成,我们可以实现一个函数,通过对 MC dropout 层进行多次前向传递来获取预测的均值和标准差;从第 3 行开始应该和本章前面的内容相似,第 2 行只是获取我们原始模型倒数第二层的输出:


def predict_ll_dropout(X, basis_func, ll_dropout, nb_inference): 
basis_feats = basis_func(X) 
ll_divd = [ll_dropout(basis_feats, training=True) for _ in range(nb_inference)] 
ll_divd = np.stack(ll_divd) 
    return ll_divd.mean(axis=0), ll_divd.std(axis=0)

第 5 步:推理

剩下的就是调用这个函数,获取我们的新模型输出,并附带不确定性估计:


y_divd, y_std = predict_ll_dropout(X_test, basis_func, ll_dropout, 50)

最后一层 MC dropout 迄今为止是从预训练网络中获得不确定性估计的最简单方法。与标准的 MC dropout 不同,它不需要从头开始训练模型,因此你可以将其应用于你已经训练好的网络。此外,与其他最后一层方法不同,它只需要几个简单的步骤即可实现,并且始终遵循 TensorFlow 的标准 API。

6.4.3 最后一层方法回顾

最后一层方法是当你需要从预训练网络中获取不确定性估计时的一个极好的工具。考虑到神经网络训练的高昂成本和耗时,能够在不从头开始的情况下仅因为需要预测不确定性而避免重新训练,实在是非常便利。此外,随着越来越多的机器学习从业者依赖于最先进的预训练模型,这些技术在事后结合模型不确定性是一个非常实用的方法。

但是,最后一层方法也有其缺点。与其他方法不同,我们依赖的是一个相对有限的方差来源:我们模型的倒数第二层。这限制了我们能够在模型输出上引入的随机性,因此我们有可能会面临过于自信的预测。在使用最后一层方法时请记住这一点,如果你看到过度自信的典型迹象,考虑使用更全面的方法来获取预测的不确定性。

6.5 小结

在这一章中,我们看到了如何利用熟悉的机器学习和深度学习概念开发带有预测不确定性的模型。我们还看到了,通过相对少量的修改,我们可以将不确定性估计添加到预训练模型中。这意味着我们可以超越标准神经网络的点估计方法:利用不确定性获得关于模型性能的宝贵见解,从而使我们能够开发更稳健的应用。

然而,就像第五章贝叶斯深度学习的原则方法中介绍的方法一样,所有技术都有其优点和缺点。例如,最后一层方法可能使我们能够向任何模型添加不确定性,但它们受到模型已经学习到的表示的限制。这可能导致输出的方差非常低,从而产生过于自信的模型。同样,集成方法虽然允许我们捕获网络每一层的方差,但它们需要显著的计算成本,需要我们有多个网络,而不仅仅是单个网络。

在接下来的章节中,我们将更详细地探讨优缺点,并学习如何解决这些方法的一些缺点。

第七章

贝叶斯深度学习的实际考虑

在过去的两章中,第五章贝叶斯深度学习的原则性方法第六章使用标准工具箱进行贝叶斯深度学习,我们介绍了一系列能够促进神经网络贝叶斯推断的方法。第五章贝叶斯深度学习的原则性方法 介绍了专门设计的贝叶斯神经网络近似方法,而 第六章使用标准工具箱进行贝叶斯深度学习 展示了如何使用机器学习的标准工具箱为我们的模型添加不确定性估计。这些方法家族各有其优缺点。在本章中,我们将探讨一些在实际场景中的差异,以帮助您理解如何为当前任务选择最佳方法。

我们还将探讨不同来源的不确定性,这有助于提升您对数据的理解,或根据不确定性的来源帮助您选择不同的异常路径。例如,如果一个模型因输入数据本身的噪声而产生不确定性,您可能需要将数据交给人类进行审查。然而,如果模型因未见过的输入数据而产生不确定性,将该数据添加到模型中可能会有所帮助,这样模型就能减少对这类数据的不确定性。贝叶斯深度学习技术能够帮助您区分这些不确定性的来源。以下部分将详细介绍这些内容:

  • 平衡不确定性质量和计算考虑

  • BDL 与不确定性来源

7.1 技术要求

要完成本章的实际任务,您需要一个 Python 3.8 环境,安装有 SciPy 和 scikit-learn 堆栈,并且还需要安装以下额外的 Python 包:

  • TensorFlow 2.0

  • TensorFlow 概率

本书的所有代码都可以在书籍的 GitHub 仓库中找到:github.com/PacktPublishing/Enhancing-Deep-Learning-with-Bayesian-Inference

7.2 平衡不确定性质量和计算考虑

虽然贝叶斯方法有许多优点,但在内存和计算开销方面也有需要考虑的权衡。这些因素在选择适用于实际应用的最佳方法时起着至关重要的作用。

在本节中,我们将考察不同方法在性能和不确定性质量方面的权衡,并学习如何使用 TensorFlow 的性能分析工具来衡量不同模型的计算成本。

7.2.1 设置实验环境

为了评估不同模型的性能,我们需要一些不同的数据集。其一是加利福尼亚住房数据集,scikit-learn 已经方便地提供了这个数据集。我们将使用的其他数据集是常见于不确定性模型比较论文中的:葡萄酒质量数据集和混凝土抗压强度数据集。让我们来看看这些数据集的详细信息:

  • 加利福尼亚住房:这个数据集包含了从 1990 年加利福尼亚人口普查中得出的不同地区的多个特征。因变量是房屋价值,以每个住宅区的中位房价表示。在较早的论文中,您会看到使用波士顿住房数据集;由于波士顿住房数据集存在伦理问题,现在更倾向于使用加利福尼亚住房数据集。

  • 葡萄酒质量:葡萄酒质量数据集包含与不同葡萄酒的化学成分相关的特征。我们要预测的值是葡萄酒的主观质量。

  • 混凝土抗压强度:混凝土抗压强度数据集的特征描述了用于混合混凝土的成分,每个数据点代表不同的混凝土配方。因变量是混凝土的抗压强度。

以下实验将使用本书 GitHub 仓库中的代码( github.com/PacktPublishing/Bayesian-Deep-Learning),我们在前几章中已经见过不同形式的代码示例。示例假定我们从这个仓库内部运行代码。

导入我们的依赖库

像往常一样,我们将首先导入我们的依赖库:


import tensorflow as tf 
import numpy as np 
import matplotlib.pyplot as plt 
import tensorflow_probability as tfp 
from sklearn.metrics import accuracy_score, mean_squared_error 
from sklearn.datasets import fetch_california_housing, load_diabetes 
from sklearn.model_selection import train_test_split 
import seaborn as sns 
import pandas as pd 
import os 

from bayes_by_backprop import BBBRegressor 
from pbp import PBP 
from mc_dropout import MCDropout 
from ensemble import Ensemble 
from bdl_ablation_data import load_wine_quality, load_concrete 
from bdl_metrics import likelihood

在这里,我们可以看到我们正在使用仓库中定义的多个模型类。虽然这些类支持不同架构的模型,但它们将使用默认结构,该结构在 constants.py 中定义。该结构包含一个 64 个单元的密集连接隐藏层,以及一个密集连接的输出层。BBB 和 PBP 等价物将使用并在各自的类中定义为其默认架构。

准备数据和模型

现在我们需要准备数据和模型以运行实验。首先,我们将设置一个字典,便于我们遍历并访问不同数据集中的数据:


datasets = { 
"california_housing": fetch_california_housing(return_X_y=True, as_frame=True), 
"diabetes": load_diabetes(return_X_y=True, as_frame=True), 
"wine_quality": load_wine_quality(), 
"concrete": load_concrete(), 
}

接下来,我们将创建另一个字典,以便我们可以遍历不同的 BDL 模型:


models = { 
"BBB": BBBRegressor, 
"PBP": PBP, 
"MCDropout": MCDropout, 
"Ensemble": Ensemble, 
}

最后,我们将创建一个字典来保存我们的结果:


results = { 
"LL": [], 
"MSE": [], 
"Method": [], 
"Dataset": [], 
}

在这里,我们看到我们将记录两个结果:对数似然和均方误差。我们使用这些指标是因为我们在处理回归问题,但对于分类问题,您可以选择使用 F 分数或准确度替代均方误差,使用预期校准误差替代(或与)对数似然。我们还将在 Method 字段中存储模型类型,在 Dataset 字段中存储数据集。

运行我们的实验

现在我们准备开始运行实验了。然而,我们不仅对模型性能感兴趣,还对我们各种模型的计算考虑因素感兴趣。因此,我们将在接下来的代码中看到对tf.profiler的调用。首先,我们将设置一些参数:


# Parameters 
epochs = 10 
batch_size = 16 
logdir_base = "profiling"

在这里,我们设置每个模型的训练周期数以及每个模型将使用的批次大小。我们还设置了logdir_base,即所有分析日志的存储位置。

现在我们准备好插入实验代码了。我们将首先遍历数据集:


for dataset_key in datasets.keys(): 
X, y = datasets[dataset_key] 
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.33) 
    ...

在这里,我们看到对于每个数据集,我们将数据拆分,使用2 3的数据进行训练,使用1 3的数据进行测试。

接下来,我们遍历模型:


... 
for model_key in models.keys(): 
logdir = os.path.join(logdir_base, model_key + "_train") 
os.makedirs(logdir, exist_ok=True) 
tf.profiler.experimental.start(logdir) 
    ...

对于每个模型,我们实例化一个新的日志目录来记录训练信息。然后我们实例化模型并运行model.fit()


... 
model = models[model_key]() 
        model.fit(X_train, y_train, batch_size=batch_size, n_epochs=epochs)

一旦模型拟合完成,我们停止分析器,并创建一个新目录来记录预测信息,之后我们再次启动分析器:


... 
tf.profiler.experimental.stop() 
logdir = os.path.join(logdir_base, model_key + "_predict") 
os.makedirs(logdir, exist_ok=True) 
tf.profiler.experimental.start(logdir) 
        ...

在分析器运行的情况下,我们进行预测,然后再次停止分析器。手头有了预测后,我们可以计算均方误差和对数似然,并将其存储到results字典中。最后,我们在每次实验结束后运行tf.keras.backend.clear_session()来清理 TensorFlow 图:


... 
y_divd, y_var = model.predict(X_test) 

tf.profiler.experimental.stop() 

y_divd = y_divd.reshape(-1) 
y_var = y_var.reshape(-1) 

mse = mean_squared_error(y_test, y_divd) 
ll = likelihood(y_test, y_divd, y_var) 
results["MSE"].append(mse) 
results["LL"].append(ll) 
results["Method"].append(model_key) 
results["Dataset"].append(dataset_key) 
tf.keras.backend.clear_session() 
...

一旦我们获得了所有模型和所有数据集的结果,我们将结果字典转换为 pandas DataFrame:


... 
results = pd.DataFrame(results)

现在我们准备好分析数据了!

7.2.2 分析模型性能

利用从实验中获得的数据,我们可以绘制图表,查看哪些模型在不同的数据集上表现最佳。为此,我们将使用以下绘图代码:


results['NLL'] = -1*results['LL'] 

i = 1 
for dataset in datasets.keys(): 
for metric in ["NLL", "MSE"]: 
df_plot = results[(results['Dataset']==dataset)] 
df_plot = groupedvalues = df_plot.groupby('Method').sum().reset_index() 
plt.subplot(3,2,i) 
ax = sns.barplot(data=df_plot, x="Method", y=metric) 
for index, row in groupedvalues.iterrows(): 
if metric == "NLL": 
ax.text(row.name, 0, round(row.NLL, 2), 
color='white', ha='center') 
else: 
ax.text(row.name, 0, round(row.MSE, 2), 
color='white', ha='center') 
plt.title(dataset) 
if metric == "NLL" and dataset == "california_housing": 
plt.ylim(0, 100) 
i+=1 
fig = plt.gcf() 
fig.set_size_inches(10, 8) 
plt.tight_layout()

请注意,最初我们将'NLL'字段添加到 pandas DataFrame 中。这为我们提供了负对数似然。这使得查看图表时不那么混乱,因为对于均方误差和负对数似然,较低的值是更好的。

代码遍历数据集和度量,借助 Seaborn 绘图库生成一些漂亮的条形图。此外,我们还使用ax.text()调用将度量值叠加到条形图上,以便清晰地看到数值。

还要注意,对于加利福尼亚住房数据,我们将负对数似然中的y值限制在 100。这是因为,在这个数据集中,我们的负对数似然值极其高,使得它与其他值一起显示时变得困难。因此,我们叠加了度量值,以便在某些值超过图表限制时,能够更容易地进行比较。

PIC

图 7.1:LL 和 MSE 实验结果的条形图

值得注意的是,为了公平比较,我们在所有模型中使用了等效的架构,使用了相同的批次大小,并进行了相同次数的训练周期。

如我们所见,并没有一种单一的最佳方法:每种模型根据数据的不同表现不同,而且具有较低均方误差的模型并不保证也会有较低的负对数似然分数。一般来说,MC dropout 表现出最差的均方误差分数;然而,它也产生了在我们实验中观察到的最佳负对数似然分数,对于葡萄酒质量数据集,它达到了 2.9 的负对数似然。这是因为,尽管它在误差方面通常表现较差,但它的不确定性非常高。因此,由于在其错误的区域表现出更大的不确定性,它会产生更有利的负对数似然分数。如果我们将误差与不确定性绘制出来,就能看到这一点:

PIC

图 7.2:误差与不确定性估计的散点图

7.2中,我们可以看到左侧图中是 BBB、PBP 和集成方法的结果,而右侧图则是 MC dropout 的结果。原因在于,MC dropout 的不确定性估计比其他方法高出两个数量级,因此它们无法在同一坐标轴上清晰地表示。这些极高的不确定性也是其相对较低负对数似然分数的原因。这是 MC dropout 的一个相当令人惊讶的例子,因为它通常是过于自信的,而在这种情况下,它显然是不自信的

尽管 MC dropout 的低自信可能会导致更好的似然分数,但这些度量需要在具体背景下进行考虑;我们通常希望在似然与误差之间取得良好的平衡。因此,在葡萄酒质量数据集的情况下,PBP 可能是最佳选择,因为它具有最低的误差,同时也有合理的似然;它的负对数似然并不低到让人怀疑,但又足够低,可以知道其不确定性估计将会是合理一致和有原则的。

对于其他数据集,选择会更直接一些:对于加利福尼亚住房数据集,BBB 显然是最优选择,而在混凝土抗压强度数据集的情况下,PBP 再次被证明是最为理智的选择。需要注意的是,这些网络并未针对这些数据集进行专门优化:这只是一个说明性示例。

关键在于,最终决定将取决于具体应用以及强健的不确定性估计有多重要。例如,在安全关键的场景中,你会希望选择具有最强健不确定性估计的方法,因此你可能会偏向于选择低自信而非较低的误差,因为你想确保只有在对模型结果非常有信心时才会使用该模型。在这些情况下,你可能会选择一种不自信但高概率(低负概率)的方法,比如在葡萄酒质量数据集上的 MC dropout。

在其他情况下,也许不需要考虑不确定性,这时你可能会选择一个标准的神经网络。但在大多数关键任务或安全关键型应用中,你会希望找到一个平衡点,利用模型不确定性估计提供的附加信息,同时保持较低的错误率。然而,实际上,在开发机器学习系统时,性能指标并不是唯一需要考虑的因素。我们还关心实际的应用影响。在下一部分,我们将看看这些模型的计算要求如何相互比较。

7.2.3 贝叶斯深度学习模型的计算考虑

对于机器学习的每一个现实世界应用,除了性能之外,还有其他考虑因素:我们还需要理解计算基础设施的实际限制。它们通常由几个因素决定,但现有的基础设施和成本往往反复出现。

现有基础设施通常很重要,因为除非是一个全新的项目,否则需要解决如何将机器学习模型集成的问题,这意味着要么找到现有的计算资源,要么请求额外的硬件或软件资源。成本是一个显著的因素也不足为奇:每个项目都有预算,机器学习解决方案的开销需要与其带来的优势相平衡。预算往往会决定哪些机器学习解决方案是可行的,这取决于训练、部署和推理时所需计算资源的成本。

为了深入了解这些方法在计算要求方面的比较,我们将查看我们实验代码中包含的 TensorFlow 性能分析器的输出。为此,我们只需从命令行运行 TensorBoard,并指向我们感兴趣的特定模型的日志目录:


tensorboard --logdir profiling/BBB_train/

这将启动一个 TensorBoard 实例(通常在 http://localhost:6006/)。将 URL 复制到浏览器中,你将看到 TensorBoard 的图形用户界面(GUI)。TensorBoard 为你提供了一套工具,用于理解 TensorFlow 模型的性能,从执行时间到不同进程的内存分配。你可以通过屏幕左上角的 Tools 选择框浏览可用的工具:

PIC

图 7.3:TensorBoard 图形用户界面的工具选择框

要更详细地了解发生了什么,请查看追踪查看器(Trace Viewer):

PIC

图 7.4:TensorBoard 图形用户界面中的追踪查看器

在这里,我们可以获得一个整体视图,展示运行模型函数所需的时间,以及一个详细的视图,展示哪些进程在后台运行,以及每个进程的运行时间。我们甚至可以通过双击一个模块查看其统计信息。例如,我们可以双击 train 模块:

图片

图 7.5:TensorBoard 图形界面中的追踪查看器,突出显示训练模块

这将在屏幕底部显示一些信息。这使我们能够密切检查此过程的运行时间。如果我们点击 持续时间,则会得到此过程的详细运行时统计数据:

图片

图 7.6:在 TensorBoard 追踪查看器中检查模块的统计数据

在这里,我们看到该过程运行了 10 次(每个 epoch 运行一次),平均持续时间为 144,527,053 纳秒(ns)。让我们使用混凝土抗压强度数据集的性能分析结果,并通过 TensorBoard 收集运行时和内存分配信息。如果我们为每个模型的训练过程都进行此操作,就能得到以下信息:

模型训练的性能分析数据

|


|


|


|


|

模型 峰值内存使用量(MiB) 持续时间(ms)

|


|


|


|


|

BBB 0.09 4270
PBP 0.253 10754
MC Dropout 0.126 2198
集成模型 0.215 20630

|


|


|


|


|

图 7.7:混凝土抗压强度数据集模型训练的性能分析数据表

在这里,我们看到 MC dropout 是该数据集中训练速度最快的模型,训练时间仅为 BBB 的一半。我们还看到,尽管集成模型只包含 5 个模型,但其训练时间远远是最久的,几乎是 MC dropout 的 10 倍。就内存使用而言,我们看到集成模型的表现较差,而 PBP 是所有模型中内存最消耗的,BBB 则具有最低的峰值内存使用量。

但并非仅仅训练才是关键。我们还需要考虑推理的计算成本。查看我们模型预测函数的性能分析数据,我们看到如下信息:

模型预测的性能分析数据

|


|


|


|


|

模型 峰值内存使用量(MiB) 持续时间(ms)

|


|


|


|


|

BBB 0.116 849
PBP 1.27 176
MC Dropout 0.548 23
集成模型 0.389 17

|


|


|


|


|

图 7.8:混凝土抗压强度数据集模型预测的性能分析数据表

有趣的是,在模型推理速度方面,集成方法领先,而在预测时的峰值内存使用上也位居第二。相比之下,PBP 的峰值内存使用最高,而 BBB 推理所需的时间最长。

这里有多种因素导致了我们看到的结果。首先,需要注意的是,这些模型都没有针对计算性能进行优化。例如,我们可以通过并行训练所有集成成员来显著缩短集成方法的训练时长,而在这里我们并没有这么做。类似地,由于 PBP 在实现中使用了大量的高级代码(不同于其他方法,这些方法都是基于优化过的 TensorFlow 或 TensorFlow Probability 代码),因此其性能受到影响。

最关键的是,我们需要确保在选择适合的模型时,既要考虑计算影响,也要考虑典型的性能指标。那么,考虑到这一点,我们如何选择合适的模型呢?

7.2.4 选择合适的模型

有了我们的性能指标和分析信息,我们拥有了选择适合任务的模型所需的所有数据。但模型选择并不容易;正如我们在这里看到的,所有模型都有其优缺点。

如果我们从性能指标开始,那么我们看到 BBB 的均方误差最低,但它的负对数似然值却非常高。所以,仅从性能指标来看,最佳选择是 PBP:它的负对数似然得分最低,且均方误差也远远好于 MC dropout 的误差,这使得 PBP 在综合考虑下成为最佳选择。

然而,如果我们查看 7.77.8中的计算影响,我们会发现 PBP 在内存使用和执行时间方面都是最差的选择。在这里,综合来看,最好的选择是 MC dropout:它的预测时间仅比集成方法稍慢,而且训练时长最短。

归根结底,这完全取决于应用:也许推理不需要实时运行,那么我们可以选择 PBP 实现。或者,也许推理时间和低误差是我们关注的重点,在这种情况下,集成方法是一个不错的选择。正如我们在这里看到的,指标和计算开销需要在具体情况下加以考虑,正如任何一类机器学习模型一样,并没有一个适用于所有应用的最佳选择。一切都取决于选择合适的工具来完成工作。

在本节中,我们介绍了用于全面理解模型性能的工具,并演示了在选择模型时考虑多种因素的重要性。从根本上讲,性能分析和分析配置文件对帮助我们做出正确的实际选择与帮助我们发现进一步改进的机会同样重要。我们可能没有时间进一步优化代码,因此可能需要务实地选择我们手头上最优化的计算方法。或者,业务需求可能决定我们需要选择性能最好的模型,这可能值得花时间优化代码并减少某种方法的计算开销。在下一节中,我们将进一步探讨使用 BDL 方法时的另一个重要实际考虑因素,学习如何使用这些方法更好地理解不确定性的来源。

7.3 BDL 与不确定性来源

在这个案例研究中,我们将探讨如何在回归问题中建模 aleatoric 和 epistemic 不确定性,目标是预测一个连续的结果变量。我们将使用一个现实世界的钻石数据集,该数据集包含了超过 50,000 颗钻石的物理属性以及它们的价格。特别地,我们将关注钻石的重量(以 克拉 测量)和钻石价格之间的关系。

步骤 1:设置环境

为了设置环境,我们导入了几个包。我们导入 tensorflowtensorflow_probability 用于构建和训练传统的和概率神经网络,导入 tensorflow_datasets 用于导入钻石数据集,导入 numpy 用于对数值数组进行计算和操作(如计算均值),导入 pandas 用于处理 DataFrame,导入 matplotlib 用于绘图:


import matplotlib.pyplot as plt 
import numpy as np 
import pandas as pd 
import tensorflow as tf 
import tensorflow_probability as tfp 
import tensorflow_datasets as tfds

首先,我们使用 tensorflow_datasets 提供的 load 函数加载钻石数据集。我们将数据集加载为一个 pandas DataFrame,这对于准备训练和推理数据非常方便。


ds = tfds.load('diamonds', split='train') 
df = tfds.as_dataframe(ds)

数据集包含了钻石的许多不同属性,但在这里我们将重点关注克拉和价格,通过选择 DataFrame 中相应的列:


df = df[["features/carat", "price"]]

然后,我们将数据集分为训练集和测试集。我们使用 80% 的数据进行训练,20% 的数据进行测试:


train_df = df.sample(frac=0.8, random_state=0) 
test_df = df.drop(train_df.index)

为了进一步处理,我们将训练和测试 DataFrame 转换为 NumPy 数组:


carat = np.array(train_df['features/carat']) 
price = np.array(train_df['price']) 
carat_test = np.array(test_df['features/carat']) 
price_test = np.array(test_df['price'])

我们还将训练样本的数量保存到一个变量中,因为我们在模型训练过程中需要它:


NUM_TRAIN_SAMPLES = carat.shape[0]

最后,我们定义了一个绘图函数。这个函数将在接下来的案例研究中派上用场。它允许我们绘制数据点以及拟合模型的预测值和标准差:


def plot_scatter(x_data, y_data, x_hat=None, y_hats=None, plot_std=False): 
# Plot the data as scatter points 
plt.scatter(x_data, y_data, color="k", label="Data") 
# Plot x and y values predicted by the model, if provided 
if x_hat is not None and y_hats is not None: 
if not isinstance(y_hats, list): 
y_hats = [y_hats] 
for ind, y_hat in enumerate(y_hats): 
plt.plot( 
x_hat, 
y_hat.mean(), 
color="#e41a1c", 
label="prediction" if ind == 0 else None, 
) 
# Plot standard deviation, if requested 
if plot_std: 
for ind, y_hat in enumerate(y_hats): 
plt.plot( 
x_hat, 
y_hat.mean() + 2 * y_hat.stddev(), 
color="#e41a1c", 
linestyle="dashed", 
label="prediction + stddev" if ind == 0 else None, 
) 
plt.plot( 
x_hat, 
y_hat.mean() - 2 * y_hat.stddev(), 
color="#e41a1c", 
linestyle="dashed", 
label="prediction - stddev" if ind == 0 else None, 
) 
# Plot x- and y-axis labels as well as a legend 
plt.xlabel("carat") 
plt.ylabel("price") 
    plt.legend()

使用这个函数,我们可以通过运行以下代码来首次查看训练数据:


plot_scatter(carat, price)

训练数据分布如 图 7.9 所示。我们观察到,克拉和钻石价格之间的关系是非线性的,随着克拉数的增加,价格的增长速度也更快。

PIC

图 7.9:钻石克拉数与价格之间的关系

步骤 2:拟合一个不带不确定性的模型

完成设置后,我们可以开始为数据拟合回归模型。首先,我们拟合一个神经网络模型,但不对预测中的不确定性进行量化。这使我们能够建立一个基准,并引入一些对本案例研究中所有模型都非常有用的工具(以函数的形式)。

建议对神经网络模型的输入特征进行归一化。在本示例中,这意味着对钻石的克拉重量进行归一化。归一化输入特征将使模型在训练过程中收敛得更快。tensorflow.keras 提供了一个方便的归一化函数,可以帮助我们实现这一点。我们可以按如下方式使用它:


normalizer = tf.keras.layers.Normalization(input_shape=(1,), axis=None) 
normalizer.adapt(carat)

我们还需要一个损失函数,理想情况下,该损失函数可以用于本案例研究中的所有模型。回归模型可以表示为 P(y|x,w),即给定输入 x 和模型参数 w 的标签 y 的概率分布。我们可以通过最小化负对数似然损失 −logP(y|x) 来拟合此类模型。用 Python 代码表示时,可以编写一个函数,该函数将真实结果值 y_true 和预测结果分布 y_divd 作为输入,并返回在预测结果分布下的结果值的负对数似然值,该方法由 tensorflow_probability 中的 distributions 模块提供的 log_prob() 实现:


def negloglik(y_true, y_divd): 
    return -y_divd.log_prob(y_true)

有了这些工具,我们可以构建第一个模型。我们使用刚才定义的归一化函数来归一化模型的输入。然后,我们在其上堆叠两个全连接层。第一个全连接层包含 32 个节点,这使我们能够建模数据中观察到的非线性关系。第二个全连接层包含一个节点,用于将模型的预测压缩为一个单一的值。重要的是,我们不使用第二个全连接层产生的输出作为模型的最终输出。相反,我们使用该全连接层的输出作为正态分布均值的参数化,这意味着我们正在使用正态分布来建模真实标签。我们还将正态分布的方差设为 1。参数化分布的均值并将方差设为固定值,意味着我们正在建模数据的总体趋势,但尚未量化模型预测中的不确定性:


model = tf.keras.Sequential( 
[ 
normalizer, 
tf.keras.layers.Dense(32, activation="relu"), 
tf.keras.layers.Dense(1), 
tfp.layers.DistributionLambda( 
lambda t: tfp.distributions.Normal(loc=t, scale=1) 
), 
] 
)

正如我们在之前的案例研究中看到的,训练模型时,我们使用compile()fit()函数。在模型编译时,我们指定了Adam优化器和之前定义的损失函数。对于fit函数,我们指定了希望在克拉和价格数据上训练模型 100 个周期:


# Compile 
model.compile(optimizer=tf.optimizers.Adam(learning_rate=0.01), loss=negloglik) 
# Fit 
model.fit(carat, price, epochs=100, verbose=0)

然后,我们可以通过我们的plot_scatter()函数获得模型在保留测试数据上的预测,并可视化所有结果:


# Define range for model input 
carat_hat = tf.linspace(carat_test.min(), carat_test.max(), 100) 
# Obtain model's price predictions on test data 
price_hat = model(carat_hat) 
# Plot test data and model predictions 
plot_scatter(carat_test, price_test, carat_hat, price_hat)

这将生成以下图表:

PIC

图 7.10:没有不确定性的钻石测试数据预测

我们可以在 7.10中看到,模型捕捉到了数据的非线性趋势。随着钻石重量的增加,模型预测的价格也随着重量的增加而迅速上升。

然而,数据中还有另一个显而易见的趋势是模型未能捕捉到的。我们可以观察到,随着重量的增加,价格的变异性越来越大。在低重量时,我们仅观察到拟合线周围少量的散布,但在较高重量时,散布增大。我们可以将这种变异性视为问题的固有特性。也就是说,即使我们有更多的训练数据,我们仍然无法完美地预测价格,尤其是在高重量时。这种类型的变异性就是随机不确定性,我们在第四章中首次遇到过,将在下一小节中仔细探讨。

第三步:拟合带有随机不确定性的模型

我们可以通过预测正态分布的标准差来考虑模型中的随机不确定性,除了预测其均值之外。和之前一样,我们构建了一个带有标准化层和两个全连接层的模型。然而,这次第二个全连接层将输出两个值,而不是一个。第一个输出值将再次用于参数化正态分布的均值。但第二个输出值将参数化正态分布的方差,从而使我们能够量化模型预测中的随机不确定性:


model_aleatoric = tf.keras.Sequential( 
[ 
normalizer, 
tf.keras.layers.Dense(32, activation="relu"), 
tf.keras.layers.Dense(2), 
tfp.layers.DistributionLambda( 
lambda t: tfp.distributions.Normal( 
loc=t[..., :1], scale=1e-3 + tf.math.softplus(0.05 * t[..., 1:]) 
) 
), 
] 
)

我们再次在重量和价格数据上编译并拟合模型:


# Compile 
model_aleatoric.compile( 
optimizer=tf.optimizers.Adam(learning_rate=0.05), loss=negloglik 
) 
# Fit 
model_aleatoric.fit(carat, price, epochs=100, verbose=0)

现在,我们可以获得并可视化测试数据的预测结果。请注意,这次我们传递了plot_std=True,以便同时绘制预测输出分布的标准差:


carat_hat = tf.linspace(carat_test.min(), carat_test.max(), 100) 
price_hat = model_aleatoric(carat_hat) 
plot_scatter( 
carat_test, price_test, carat_hat, price_hat, plot_std=True, 
)

我们现在已经训练了一个模型,表示数据固有的变异性。 7.11中的虚线误差条显示了价格作为重量函数的预测变异性。我们可以观察到,模型确实对重量超过 1 克拉时的价格预测不太确定,这反映了我们在较高重量范围内观察到的数据的较大散布。

PIC

图 7.11:包含随机不确定性的钻石测试数据预测

第四步:拟合带有认知不确定性的模型

除了偶然性不确定性,我们还需要处理认知不确定性——这种不确定性来源于我们的模型,而不是数据本身。回顾一下图 7.11,例如,实线代表我们模型预测的均值,它似乎能合理地捕捉数据的趋势。然而,由于训练数据有限,我们无法百分之百确定我们找到了数据分布的真实均值。也许真实均值实际上略大于或略小于我们估计的值。在这一部分,我们将研究如何建模这种不确定性,我们还将看到,通过观察更多的数据,认知不确定性是可以减少的。

模型认知不确定性的技巧再次是,将我们神经网络中的权重表示为分布,而不是点估计。我们可以通过将之前使用的密集层替换为tensorflow_probability中的 DenseVariational 层来实现这一点。在底层,这将实现我们在第五章中首次学习到的 BBB 方法,贝叶斯深度学习的原则方法。简而言之,使用 BBB 时,我们通过变分学习原理来学习网络权重的后验分布。为了实现这一点,我们需要定义先验和后验分布函数。

请注意,在第五章中展示的 BBB 代码示例,贝叶斯深度学习的原则方法使用了预定义的tensorflow_probability模块来处理 2D 卷积和密集层,并应用重参数化技巧,这样我们就隐式地定义了先验和后验函数。在本示例中,我们将自己定义密集层的先验和后验函数。

我们从定义密集层权重的先验开始(包括内核和偏置项)。先验分布描述了我们在观察到任何数据之前,对权重的猜测不确定性。它可以通过一个多元正态分布来定义,其中均值是可训练的,方差固定为 1:


def prior(kernel_size, bias_size=0, dtype=None): 
n = kernel_size + bias_size 
return tf.keras.Sequential( 
[ 
tfp.layers.VariableLayer(n, dtype=dtype), 
tfp.layers.DistributionLambda( 
lambda t: tfp.distributions.Independent( 
tfp.distributions.Normal(loc=t, scale=1), 
reinterpreted_batch_ndims=1, 
) 
), 
] 
    )

我们还定义了变分后验。变分后验是我们观察到训练数据后,密集层权重分布的近似值。我们再次使用多元正态分布:


def posterior(kernel_size, bias_size=0, dtype=None): 
n = kernel_size + bias_size 
c = np.log(np.expm1(1.0)) 
return tf.keras.Sequential( 
[ 
tfp.layers.VariableLayer(2 * n, dtype=dtype), 
tfp.layers.DistributionLambda( 
lambda t: tfp.distributions.Independent( 
tfp.distributions.Normal( 
loc=t[..., :n], 
scale=1e-5 + tf.nn.softplus(c + t[..., n:]), 
), 
reinterpreted_batch_ndims=1, 
) 
), 
] 
    )

有了这些先验和后验函数,我们就能定义我们的模型。和之前一样,我们使用归一化层对输入进行归一化,然后堆叠两个密集层。但这一次,密集层将把它们的参数表示为分布,而不是点估计。我们通过将tensorflow_probability中的 DenseVariational 层与我们的先验和后验函数结合使用来实现这一点。最终的输出层是一个正态分布,其方差设置为 1,均值由前一个 DenseVariational 层的输出来参数化:


def build_epistemic_model(): 
model = tf.keras.Sequential( 
[ 
normalizer, 
tfp.layers.DenseVariational( 
32, 
make_prior_fn=prior, 
make_posterior_fn=posterior, 
kl_weight=1 / NUM_TRAIN_SAMPLES, 
activation="relu", 
), 
tfp.layers.DenseVariational( 
1, 
make_prior_fn=prior, 
make_posterior_fn=posterior, 
kl_weight=1 / NUM_TRAIN_SAMPLES, 
), 
tfp.layers.DistributionLambda( 
lambda t: tfp.distributions.Normal(loc=t, scale=1) 
), 
] 
) 
  return model

为了观察可用训练数据量对表征性不确定性估计的影响,我们首先在一个较小的数据子集上拟合模型,然后再用所有可用的训练数据拟合模型。我们取训练数据集中的前 500 个样本:


carat_subset = carat[:500] 
price_subset = price[:500]

我们按之前的方式构建、编译并拟合模型:


# Build 
model_epistemic = build_epistemic_model() 
# Compile 
model_epistemic.compile( 
optimizer=tf.optimizers.Adam(learning_rate=0.01), loss=negloglik 
) 
# Fit 
model_epistemic.fit(carat_subset, price_subset, epochs=100, verbose=0)

接着我们在测试数据上获得并绘制预测结果。请注意,这里我们从后验分布中抽取了 10 次样本,这使我们能够观察每次样本迭代时预测均值的变化。如果预测均值变化很大,说明表征性不确定性估计较大;如果均值变化非常小,则表示表征性不确定性较小:


carat_hat = tf.linspace(carat_test.min(), carat_test.max(), 100) 
price_hats = [model_epistemic(carat_hat) for _ in range(10)] 
plot_scatter( 
carat_test, price_test, carat_hat, price_hats, 
)

7.12中,我们可以观察到,预测均值在 10 个不同的样本中有所变化。有趣的是,变化(因此表征性不确定性)在较低权重时似乎较低,而随着权重的增加,变化逐渐增大。

PIC

图 7.12:在钻石测试数据上,具有高表征性不确定性的预测

为了验证通过更多数据训练可以减少表征性不确定性,我们在完整的训练数据集上训练我们的模型:


# Build 
model_epistemic_full = build_epistemic_model() 
# Compile 
model_epistemic_full.compile( 
optimizer=tf.optimizers.Adam(learning_rate=0.01), loss=negloglik 
) 
# Fit 
model_epistemic_full.fit(carat, price, epochs=100, verbose=0)

然后绘制完整数据模型的预测结果:


carat_hat = tf.linspace(carat_test.min(), carat_test.max(), 100) 
price_hats = [model_epistemic_full(carat_hat) for _ in range(10)] 
plot_scatter( 
carat_test, price_test, carat_hat, price_hats, 
)

正如预期的那样,我们在 7.13中看到,表征性不确定性现在要低得多,且预测均值在 10 个样本中变化很小(以至于很难看到 10 条红色曲线之间的任何差异):

PIC

图 7.13:在钻石测试数据上,具有低表征性不确定性的预测

第 5 步:拟合具有偶然性和表征性不确定性的模型

作为最后的练习,我们可以将所有构建模块结合起来,构建一个同时建模偶然性和表征性不确定性的神经网络。我们可以通过使用两个 DenseVariational 层(这将使我们能够建模表征性不确定性),然后在其上堆叠一个正态分布层,该层的均值和方差由第二个 DenseVariational 层的输出参数化(这将使我们能够建模偶然性不确定性):


# Build model. 
model_epistemic_aleatoric = tf.keras.Sequential( 
[ 
normalizer, 
tfp.layers.DenseVariational( 
32, 
make_prior_fn=prior, 
make_posterior_fn=posterior, 
kl_weight=1 / NUM_TRAIN_SAMPLES, 
activation="relu", 
), 
tfp.layers.DenseVariational( 
1 + 1, 
make_prior_fn=prior, 
make_posterior_fn=posterior, 
kl_weight=1 / NUM_TRAIN_SAMPLES, 
), 
tfp.layers.DistributionLambda( 
lambda t: tfp.distributions.Normal( 
loc=t[..., :1], scale=1e-3 + tf.math.softplus(0.05 * t[..., 1:]) 
) 
), 
] 
)

我们可以按照之前的相同流程构建和训练该模型。然后我们可以再次在测试数据上进行 10 次推理,这会产生如 7.14所示的预测结果。每一次推理都会产生一个预测均值和标准差。标准差代表每次推理的偶然性不确定性,而在不同推理之间观察到的变化则代表表征性不确定性。

PIC

图 7.14:在钻石测试数据上,具有表征性和偶然性不确定性的预测

7.3.1 不确定性的来源:图像分类案例研究

在前面的案例研究中,我们看到如何在回归问题中建模随机不确定性和认知不确定性。在本节中,我们将再次查看 MNIST 数字数据集,以建模随机不确定性和认知不确定性。我们还将探讨随机不确定性如何难以减少,而认知不确定性则可以通过更多数据来减少。

让我们从数据开始。为了让我们的例子更具启发性,我们不仅使用标准的 MNIST 数据集,还使用一个名为 AmbiguousMNIST 的 MNIST 变体。这个数据集包含生成的图像,显然是固有模糊的。让我们首先加载数据,然后探索 AmbiguousMNIST 数据集。我们从必要的导入开始:


import tensorflow as tf 
import tensorflow_probability as tfp 
import matplotlib.pyplot as plt 
import numpy as np 
from sklearn.utils import shuffle 
from sklearn.metrics import roc_auc_score 
import ddu_dirty_mnist 
from scipy.stats import entropy 
tfd = tfp.distributions

我们可以通过ddu_dirty_mnist库下载 AmbiguousMNIST 数据集:


dirty_mnist_train = ddu_dirty_mnist.DirtyMNIST( 
".", 
train=True, 
download=True, 
normalize=False, 
noise_stddev=0 
) 

# regular MNIST 
train_imgs = dirty_mnist_train.datasets[0].data.numpy() 
train_labels = dirty_mnist_train.datasets[0].targets.numpy() 
# AmbiguousMNIST 
train_imgs_amb = dirty_mnist_train.datasets[1].data.numpy() 
train_labels_amb = dirty_mnist_train.datasets[1].targets.numpy()

然后我们将图像和标签进行拼接和混洗,以便在训练期间两种数据集能有良好的混合。我们还固定数据集的形状,以使其适应我们模型的设置:


train_imgs, train_labels = shuffle( 
np.concatenate([train_imgs, train_imgs_amb]), 
np.concatenate([train_labels, train_labels_amb]) 
) 
train_imgs = np.expand_dims(train_imgs[:, 0, :, :], -1) 
train_labels = tf.one_hot(train_labels, 10)

图 7.157.15)给出了 AmbiguousMNIST 图像的示例。我们可以看到图像处于两类之间:一个 4 也可以被解释为 9,一个 0 可以被解释为 6,反之亦然。这意味着我们的模型很可能难以正确分类这些图像中的至少一部分,因为它们本质上是噪声。

PIC

图 7.15:来自 AmbiguousMNIST 数据集的图像示例

现在我们已经有了训练数据集,让我们也加载我们的测试数据集。我们将仅使用标准的 MNIST 测试数据集:


(test_imgs, test_labels) = tf.keras.datasets.mnist.load_data()[1] 
test_imgs = test_imgs / 255\. 
test_imgs = np.expand_dims(test_imgs, -1) 
test_labels = tf.one_hot(test_labels, 10)

现在我们可以开始定义我们的模型。在这个例子中,我们使用一个小型的贝叶斯神经网络,带有Flipout层。这些层在前向传递过程中从内核和偏置的后验分布中采样,从而为我们的模型增加随机性。我们可以在以后需要计算不确定性值时使用它:


kl_divergence_function = lambda q, p, _: tfd.kl_divergence(q, p) / tf.cast( 
60000, dtype=tf.float32 
) 

model = tf.keras.models.Sequential( 
[ 
*block(5), 
*block(16), 
*block(120, max_pool=False), 
tf.keras.layers.Flatten(), 
tfp.layers.DenseFlipout( 
84, 
kernel_divergence_fn=kl_divergence_function, 
activation=tf.nn.relu, 
), 
tfp.layers.DenseFlipout( 
10, 
kernel_divergence_fn=kl_divergence_function, 
activation=tf.nn.softmax, 
), 
] 
)

我们定义一个块如下:


def block(filters: int, max_pool: bool = True): 
conv_layer =  tfp.layers.Convolution2DFlipout( 
filters, 
kernel_size=5, 
padding="same", 
kernel_divergence_fn=kl_divergence_function, 
activation=tf.nn.relu) 
if not max_pool: 
return (conv_layer,) 
max_pool = tf.keras.layers.MaxPooling2D( 
pool_size=[2, 2], strides=[2, 2], padding="same" 
) 
    return conv_layer, max_pool

我们编译我们的模型并开始训练:


model.compile( 
tf.keras.optimizers.Adam(), 
loss="categorical_crossentropy", 
metrics=["accuracy"], 
experimental_run_tf_function=False, 
) 
model.fit( 
x=train_imgs, 
y=train_labels, 
validation_data=(test_imgs, test_labels), 
epochs=50 
)

现在我们有兴趣通过认知不确定性和随机不确定性来分离图像。认知不确定性应该将我们的分布内图像与分布外图像区分开,因为这些图像可以被视为未知的未知:我们的模型以前从未见过这些图像,因此应该对它们分配较高的认知不确定性(或知识不确定性)。尽管我们的模型是在 AmbiguousMNIST 数据集上训练的,但在测试时,当它看到这个数据集中的图像时,它仍然应该具有较高的随机不确定性:用这些图像进行训练并不会减少随机不确定性(或数据不确定性),因为这些图像本质上是模糊的。

我们使用 FashionMNIST 数据集作为分布外数据集。我们使用 AmbiguousMNIST 测试集作为我们用于测试的模糊数据集:


(_, _), (ood_imgs, _) = tf.keras.datasets.fashion_mnist.load_data() 
ood_imgs = np.expand_dims(ood_imgs / 255., -1) 

ambiguous_mnist_test = ddu_dirty_mnist.AmbiguousMNIST( 
".", 
train=False, 
download=True, 
normalize=False, 
noise_stddev=0 
) 
amb_imgs = ambiguous_mnist_test.data.numpy().reshape(60000, 28, 28, 1)[:10000] 
amb_labels = tf.one_hot(ambiguous_mnist_test.targets.numpy(), 10).numpy()

让我们利用模型的随机性来生成多样的模型预测。我们对测试图像进行 50 次迭代:


divds_id = [] 
divds_ood = [] 
divds_amb = [] 
for _ in range(50): 
divds_id.append(model(test_imgs)) 
divds_ood.append(model(ood_imgs)) 
divds_amb.append(model(amb_imgs)) 
# format data such that we have it in shape n_images, n_predictions, n_classes 
divds_id = np.moveaxis(np.stack(divds_id), 0, 1) 
divds_ood = np.moveaxis(np.stack(divds_ood), 0, 1) 
divds_amb = np.moveaxis(np.stack(divds_amb), 0, 1)

然后我们可以定义一些函数来计算不同类型的不确定性:


def total_uncertainty(divds: np.ndarray) -*>* np.ndarray: 
return entropy(np.mean(divds, axis=1), axis=-1) 

def data_uncertainty(divds: np.ndarray) -*>* np.ndarray: 
return np.mean(entropy(divds, axis=2), axis=-1) 

def knowledge_uncertainty(divds: np.ndarray) -*>* np.ndarray: 
    return total_uncertainty(divds) - data_uncertainty(divds)

最后,我们可以看到我们的模型在区分分布内、模糊和分布外图像方面的表现。让我们根据不同的不确定性方法绘制不同分布的直方图:


labels = ["In-distribution", "Out-of-distribution", "Ambiguous"] 
uncertainty_functions = [total_uncertainty, data_uncertainty, knowledge_uncertainty] 
fig, axes = plt.subplots(1, 3, figsize=(20,5)) 
for ax, uncertainty in zip(axes, uncertainty_functions): 
for scores, label in zip([divds_id, divds_ood, divds_amb], labels): 
ax.hist(uncertainty(scores), bins=20, label=label, alpha=.8) 
ax.title.set_text(uncertainty.__name__.replace("_", " ").capitalize()) 
ax.legend(loc="upper right") 
plt.legend() 
plt.savefig("uncertainty_types.png", dpi=300) 
plt.show()

这会生成以下输出:

PIC

图 7.16:MNIST 上的不同类型不确定性

我们能观察到什么?

  • 总体不确定性和数据不确定性在区分分布内数据、分布外数据和模糊数据方面相对有效。

  • 然而,数据不确定性和总体不确定性无法区分模糊数据与分布外数据。要做到这一点,我们需要知识不确定性。我们可以看到,知识不确定性能够清晰地区分模糊数据和分布外数据。

  • 我们也对模糊样本进行了训练,但这并没有将模糊测试样本的不确定性降低到与原始分布内数据类似的水平。这表明数据不确定性不能轻易降低。无论模型看到多少模糊数据,数据本身就是模糊的。

我们可以通过查看不同分布组合的 AUROC 来验证这些观察结果。

我们可以首先计算 AUROC 分数,以评估我们模型区分分布内和模糊图像与分布外图像的能力:


def auc_id_and_amb_vs_ood(uncertainty): 
scores_id = uncertainty(divds_id) 
scores_ood = uncertainty(divds_ood) 
scores_amb = uncertainty(divds_amb) 
scores_id = np.concatenate([scores_id, scores_amb]) 
labels = np.concatenate([np.zeros_like(scores_id), np.ones_like(scores_ood)]) 
return roc_auc_score(labels, np.concatenate([scores_id, scores_ood])) 

print(f"{auc_id_and_amb_vs_ood(total_uncertainty)=:.2%}") 
print(f"{auc_id_and_amb_vs_ood(knowledge_uncertainty)=:.2%}") 
print(f"{auc_id_and_amb_vs_ood(data_uncertainty)=:.2%}") 
# output: 
# auc_id_and_amb_vs_ood(total_uncertainty)=91.81% 
# auc_id_and_amb_vs_ood(knowledge_uncertainty)=98.87% 
# auc_id_and_amb_vs_ood(data_uncertainty)=84.29%

我们在直方图中看到的结果得到了确认:知识不确定性在区分分布内和模糊数据与分布外数据方面远胜于另外两种不确定性类型。


def auc_id_vs_amb(uncertainty): 
scores_id, scores_amb = uncertainty(divds_id), uncertainty(divds_amb) 
labels = np.concatenate([np.zeros_like(scores_id), np.ones_like(scores_amb)]) 
return roc_auc_score(labels, np.concatenate([scores_id, scores_amb])) 

print(f"{auc_id_vs_amb(total_uncertainty)=:.2%}") 
print(f"{auc_id_vs_amb(knowledge_uncertainty)=:.2%}") 
print(f"{auc_id_vs_amb(data_uncertainty)=:.2%}") 
# output: 
# auc_id_vs_amb(total_uncertainty)=94.71% 
# auc_id_vs_amb(knowledge_uncertainty)=87.06% 
# auc_id_vs_amb(data_uncertainty)=95.21%

我们可以看到,整体不确定性和数据不确定性能够相当好地区分分布内和模糊数据。使用数据不确定性相较于使用总体不确定性有所改善。然而,知识不确定性无法区分分布内数据和模糊数据。

7.4 总结

在这一章中,我们探讨了使用贝叶斯深度学习的一些实际考虑因素:探索模型性能的权衡,并了解如何使用贝叶斯神经网络方法更好地理解不同不确定性来源对数据的影响。

在下一章中,我们将通过各种案例研究进一步探讨应用贝叶斯深度学习,展示这些方法在多种实际环境中的优势。

7.5 进一步阅读

  • 《概率反向传播的实践考虑》,Matt Benatan :在这篇论文中,作者探讨了如何最大限度地利用 PBP,展示了不同的早停方法如何改善训练,探讨了迷你批次的权衡,等等。

  • 《使用 TensorFlow 和 TensorFlow 概率建模阿莱托里克和认知不确定性》,Alexander Molak:在这个 Jupyter 笔记本中,作者展示了如何在回归玩具数据上建模阿莱托里克不确定性和认知不确定性。

  • 神经网络中的权重不确定性,Charles Blundell :在本文中,作者介绍了 BBB,我们在回归案例研究中使用了它,它是贝叶斯深度学习文献中的关键部分。

  • 深度确定性不确定性:一个简单的基准,Jishnu Mukhoti :在这项工作中,作者描述了与不同类型的不确定性相关的几项实验,并介绍了我们在最后一个案例研究中使用的AmbiguousMNIST数据集。

  • 深度学习中的不确定性估计及其在语音语言评估中的应用,Andrey Malinin:本论文通过直观的示例突出不同来源的不确定性。

第八章

应用贝叶斯深度学习

本章将引导你了解贝叶斯深度学习(BDL)的多种应用。这些应用包括 BDL 在标准分类任务中的使用,以及展示如何在异常数据检测、数据选择和强化学习等更复杂的任务中使用 BDL。

我们将在接下来的章节中讨论这些主题:

  • 检测异常数据

  • 提高对数据集漂移的鲁棒性

  • 使用基于不确定性的数据显示选择,保持模型的新鲜度

  • 使用不确定性估计进行更智能的强化学习

  • 对抗性输入的易感性

8.1 技术要求

本书的所有代码都可以在本书的 GitHub 仓库中找到:github.com/PacktPublishing/Enhancing-Deep-Learning-with-Bayesian-Inference

8.2 检测异常数据

典型的神经网络在处理异常数据时表现不佳。我们在第三章深度学习基础中看到,猫狗分类器将一张降落伞的图像错误地分类为狗,并且置信度超过 99%。在本节中,我们将探讨如何解决神经网络这一弱点。我们将进行以下操作:

  • 通过扰动MNIST数据集中的一个数字,直观地探索这个问题

  • 解释文献中通常如何报告异常数据检测的性能

  • 回顾我们在本章中讨论的几种标准实用贝叶斯深度学习(BDL)方法在异常数据检测中的表现

  • 探索更多专门用于异常数据检测的实用方法

8.2.1 探索异常数据检测的问题

为了更好地帮助你理解异常数据检测的效果,我们将从一个视觉示例开始。以下是我们将要做的事情:

  • 我们将在MNIST数字数据集上训练一个标准网络

  • 然后,我们将扰动一个数字,并逐渐使其变得更加异常

  • 我们将报告标准模型和 MC dropout 的置信度得分

通过这个视觉示例,我们可以看到简单的贝叶斯方法如何在异常数据检测上优于标准的深度学习模型。我们首先在MNIST数据集上训练一个简单的模型。

图片

图 8.1:MNIST 数据集的类别:零到九的 28x28 像素数字图像

我们使用TensorFlow来训练模型,使用numpy让我们的图像更具异常性,使用Matplotlib来可视化数据。


import tensorflow as tf 
from tensorflow.keras import datasets, layers, models 
import numpy as np 
import matplotlib.pyplot as plt

MNIST数据集可以在 TensorFlow 中找到,所以我们可以直接加载它:


(train_images, train_labels), ( 
test_images, 
test_labels, 
) = datasets.mnist.load_data() 
train_images, test_images = train_images / 255.0, test_images / 255.0

MNIST是一个简单的数据集,因此使用简单的模型可以让我们在测试中达到超过 99%的准确率。我们使用一个标准的 CNN,包含三层卷积层:


def get_model(): 
model = models.Sequential() 
model.add( 
layers.Conv2D(32, (3, 3), activation="relu", input_shape=(28, 28, 1)) 
) 
model.add(layers.MaxPooling2D((2, 2))) 
model.add(layers.Conv2D(64, (3, 3), activation="relu")) 
model.add(layers.MaxPooling2D((2, 2))) 
model.add(layers.Conv2D(64, (3, 3), activation="relu")) 
model.add(layers.Flatten()) 
model.add(layers.Dense(64, activation="relu")) 
model.add(layers.Dense(10)) 
return model 

model = get_model()

然后,我们可以编译并训练我们的模型。经过 5 个 epochs 后,我们的验证准确率超过 99%。


def fit_model(model): 
model.compile( 
optimizer="adam", 
loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True), 
metrics=["accuracy"], 
) 

model.fit( 
train_images, 
train_labels, 
epochs=5, 
validation_data=(test_images, test_labels), 
) 
return model 

model = fit_model(model)

现在,让我们看看这个模型如何处理分布外数据。假设我们部署这个模型来识别数字,但用户有时无法写下完整的数字。当用户没有写下完整的数字时会发生什么?我们可以通过逐渐移除数字中的信息,观察模型如何处理这些扰动输入,来回答这个问题。我们可以这样定义移除signal的函数:


def remove_signal(img: np.ndarray, num_lines: int) -*>* np.ndarray: 
img = img.copy() 
img[:num_lines] = 0 
   return img

然后我们对图像进行扰动:


imgs = [] 
for i in range(28): 
img_perturbed = remove_signal(img, i) 
if np.array_equal(img, img_perturbed): 
continue 
imgs.append(img_perturbed) 
if img_perturbed.sum() == 0: 
     break

我们只有在将某一行设为 0 确实改变了原始图像时,才将扰动后的图像添加到我们的图像列表中(if np.array_equal(img, img_perturbed))),并且一旦图像完全变黑,即仅包含值为 0 的像素,我们就停止。我们对这些图像进行推理:


softmax_predictions = tf.nn.softmax(model(np.expand_dims(imgs, -1)), axis=1)

然后,我们可以绘制所有图像及其预测标签和置信度分数:


plt.figure(figsize=(10, 10)) 
bbox_dict = dict( 
fill=True, facecolor="white", alpha=0.5, edgecolor="white", linewidth=0 
) 
for i in range(len(imgs)): 
plt.subplot(5, 5, i + 1) 
plt.xticks([]) 
plt.yticks([]) 
plt.grid(False) 
plt.imshow(imgs[i], cmap="gray") 
prediction = softmax_predictions[i].numpy().max() 
label = np.argmax(softmax_predictions[i]) 
plt.xlabel(f"{label} - {prediction:.2%}") 
plt.text(0, 3, f" {i+1}", bbox=bbox_dict) 
plt.show()

这生成了如下的图:

PIC

图 8.2:标准神经网络对于逐渐偏离分布的图像所预测的标签及相应的 softmax 分数

我们可以在 8.2中看到,最初,我们的模型非常自信地将图像分类为2。值得注意的是,即使在这种分类显得不合理时,这种自信依然存在。例如,模型仍然以 97.83%的置信度将图像 14 分类为2。此外,模型还预测完全水平的线条是1,置信度为 92.32%,正如我们在图像 17 中所见。这看起来我们的模型在预测时过于自信。

让我们看看一个稍有不同的模型会如何对这些图像做出预测。我们现在将使用 MC Dropout 作为我们的模型。通过采样,我们应该能够提高模型的不确定性,相较于标准的神经网络。我们先定义我们的模型:


def get_dropout_model(): 
model = models.Sequential() 
model.add( 
layers.Conv2D(32, (3, 3), activation="relu", input_shape=(28, 28, 1)) 
) 
model.add(layers.Dropout(0.2)) 
model.add(layers.MaxPooling2D((2, 2))) 
model.add(layers.Conv2D(64, (3, 3), activation="relu")) 
model.add(layers.MaxPooling2D((2, 2))) 
model.add(layers.Dropout(0.5)) 
model.add(layers.Conv2D(64, (3, 3), activation="relu")) 
model.add(layers.Dropout(0.5)) 
model.add(layers.Flatten()) 
model.add(layers.Dense(64, activation="relu")) 
model.add(layers.Dropout(0.5)) 
model.add(layers.Dense(10)) 
    return model

那么我们来实例化它:


dropout_model = get_dropout_model() 
dropout_model = fit_model(dropout_model)

使用 dropout 的模型将实现与原始模型类似的准确性。现在,我们使用 dropout 进行推理,并绘制 MC Dropout 的平均置信度分数:


Predictions = np.array( 
[ 
tf.nn.softmax(dropout_model(imgs_np, training=True), axis=1) 
for _ in range(100) 
] 
) 
Predictions_mean = np.mean(predictions, axis=0) 
plot_predictions(predictions_mean)

这再次生成了一个图,显示了预测标签及其相关的置信度分数:

PIC

图 8.3:MC Dropout 网络对于逐渐偏离分布的图像所预测的标签及相应的 softmax 分数

我们可以在 8.3中看到,模型的自信度平均来说较低。当我们从图像中移除行时,模型的置信度大幅下降。这是期望的行为:当模型不知道输入时,它应该表现出不确定性。然而,我们也能看到模型并不完美:

  • 对于那些看起来并不像2的图像,模型仍然保持较高的置信度。

  • 当我们从图像中删除一行时,模型的置信度变化很大。例如,模型的置信度在图像 14 和图像 15 之间从 61.72%跃升至 37.20%。

  • 模型似乎更有信心将没有任何白色像素的图像 20 分类为1

在这种情况下,MC Dropout 是一个朝着正确方向迈出的步骤,但它并没有完美地处理分布外数据。

8.2.2 系统地评估 OOD 检测性能

上述示例表明,MC dropout 通常会给出分布外图像较低的置信度分数。但我们仅评估了 20 张图像,且变化有限——我们只是删除了一行。这一变化使得图像更加分布外,但前一部分展示的所有图像与MNIST的训练分布相比,还是相对相似的,如果拿它和自然物体图像比较。例如,飞机、汽车或鸟类的图像肯定比带有几行黑色的MNIST图像更具分布外特征。因此,似乎合理的是,如果我们想评估模型的 OOD 检测性能,我们应该在更加分布外的图像上进行测试,也就是说,来自完全不同数据集的图像。这正是文献中通常用于评估分布外检测性能的方法。具体步骤如下:

  1. 我们在内部分布(ID)图像上训练模型。

  2. 我们选取一个或多个完全不同的 OOD 数据集,并将这些数据喂给我们的模型。

  3. 我们现在将模型在 ID 和 OOD 测试数据集上的预测视为一个二进制问题,并为每个图像计算一个单一的得分。

    • 在评估 softmax 分数的情况下,这意味着我们为每个 ID 和 OOD 图像取模型的最大 softmax 分数。
  4. 使用这些得分,我们可以计算二进制指标,如接收者操作特征曲线下面积(AUROC)。

模型在这些二进制指标上的表现越好,模型的 OOD 检测性能就越好。

8.2.3 无需重新训练的简单分布外检测

尽管 MC dropout 可以有效检测出分布外数据,但它在推理时存在一个主要缺点:我们需要进行五次,甚至一百次推理,而不是仅仅一次。对于某些其他贝叶斯深度学习方法也可以说类似:虽然它们有理论依据,但并不总是获得良好 OOD 检测性能的最实际方法。主要的缺点是,它们通常需要重新训练网络,如果数据量很大,这可能会非常昂贵。这就是为什么有一整套不显式依赖贝叶斯理论的 OOD 检测方法,但能提供良好、简单,甚至是优秀的基线。这些方法通常不需要任何重新训练,可以直接在标准神经网络上应用。文献中经常使用的两种方法值得一提:

  • ODIN:使用预处理和缩放进行 OOD 检测

  • 马哈拉诺比斯:使用中间特征进行 OOD 检测

ODIN:使用预处理和缩放进行 OOD 检测

Out-of-DIstribution 检测器(ODIN)是实际应用中常用的标准分布外检测方法之一,因为它简单有效。尽管该方法在 2017 年被提出,但它仍然经常作为提出分布外检测方法的论文中的对比方法。

ODIN 包含两个关键思想:

  • 对 logit 分数进行温度缩放,然后再应用 softmax 操作,以提高 softmax 分数区分在分布内和分布外图像的能力。

  • 输入预处理 使分布内图像更符合分布内

让我们更详细地看看这两个思想。

温度缩放 ODIN 适用于分类模型。给定我们计算的 softmax 分数如下:

pi(x) = ∑--exp(fi(x))--- Nj=1 exp(fj(x))

在这里,f**i 是单个 logit 输出,f**j 是单个示例中所有类别的 logits,温度缩放意味着我们将这些 logit 输出除以常数 T

 exp(f (x)∕T) pi(x; T) = ∑N------i---------- j=1 exp (fj(x)∕T)

对于较大的 T 值,温度缩放使得 softmax 分数更接近均匀分布,从而有助于减少过于自信的预测。

我们可以在 Python 中应用温度缩放,假设有一个简单的模型输出 logits:


logits = model.predict(images) 
logits_scaled = logits / temperature 
softmax = tf.nn.softmax(logits, axis=1)

输入预处理 我们在 第三章深度学习基础 中看到,快速梯度 符号方法FGSM)使我们能够欺骗神经网络。通过稍微改变一张猫的图像,我们可以让模型以 99.41% 的置信度预测为“狗”。这里的想法是,我们可以获取损失相对于输入的梯度符号,将其乘以一个小值,并将该噪声添加到图像中——这将把我们的图像从分布内类别中移动。通过做相反的事情,即从图像中减去噪声,我们使得图像更接近分布内。ODIN 论文的作者表明,这导致分布内图像的 softmax 分数比分布外图像更高。这意味着我们增加了 OOD 和 ID softmax 分数之间的差异,从而提高了 OOD 检测性能。

˜x = x − 𝜀sign(− ∇x log Sˆy(x;T))

其中 x 是输入图像,我们从中减去扰动幅度 𝜖 乘以交叉熵损失相对于输入的梯度符号。有关该技术的 TensorFlow 实现,请参见 第三章深度学习基础

尽管输入预处理和温度缩放易于实现,ODIN 现在还需要调节两个超参数:用于缩放 logits 的温度和 𝜖(快速梯度符号法的逆)。ODIN 使用一个单独的分布外数据集来调节这些超参数(iSUN 数据集的验证集:8925 张图像)。

马氏距离:使用中间特征进行 OOD 检测

在《一种简单统一的框架用于检测分布外样本和 对抗攻击》一文中,Kimin Lee 等人提出了一种检测 OOD 输入的不同方法。该方法的核心思想是每个类别的分类器在网络的特征空间中遵循多元高斯分布。基于这一思想,我们可以定义C个类别条件高斯分布,并且具有共享的协方差 σ

P(f(x) | y = c) = 𝒩 (f(x) | μc,σ )

其中 μ[c] 是每个类别 c 的多元高斯分布的均值。这使得我们能够计算给定中间层输出的每个类别的经验均值和协方差。基于均值和协方差,我们可以计算单个测试图像与分布内数据的马氏距离。我们对与输入图像最接近的类别计算该距离:

M (x) = max − (f(x)− ^μc)⊤ ^σ− 1(f(x)− ^μc) c

对于分布内的图像,这个距离应该较小,而对于分布外的图像,这个距离应该较大。

numpy 提供了方便的函数来计算数组的均值和协方差:


mean = np.mean(features_of_class, axis=0) 
covariance = np.cov(features_of_class.T)

基于这些,我们可以按如下方式计算马氏距离:


covariance_inverse = np.linalg.pinv(covariance) 
x_minus_mu = features_of_class - mean 
mahalanobis = np.dot(x_minus_mu, covariance_inverse).dot(x_minus_mu.T) 
mahalanobis = np.sqrt(mahalanobis).diagonal()

马氏距离计算不需要任何重新训练,一旦你存储了网络某一层特征的均值和(协方差的逆矩阵),这是一项相对廉价的操作。

为了提高方法的性能,作者表明我们还可以应用 ODIN 论文中提到的输入预处理,或者计算并平均从网络多个层提取的马氏距离。

8.3 抵抗数据集偏移

我们在第三章深度学习基础》中已经遇到过数据集偏移。提醒一下,数据集偏移是机器学习中的一个常见问题,发生在模型训练阶段和模型推理阶段(例如,在测试模型或在生产环境中运行时)输入 X 和输出 Y 的联合分布 P(X,Y) 不同的情况下。协变量偏移是数据集偏移的一个特定案例,其中只有输入的分布发生变化,而条件分布 P(Y |X) 保持不变。

数据集偏移在大多数生产环境中普遍存在,因为在训练过程中很难包含所有可能的推理条件,而且大多数数据不是静态的,而是随着时间发生变化。在生产环境中,输入数据可能沿着许多不同的维度发生偏移。地理和时间数据集偏移是两种常见的偏移形式。例如,假设你已在一个地理区域(例如欧洲)获得的数据上训练了模型,然后将模型应用于另一个地理区域(例如拉丁美洲)。类似地,模型可能是在 2010 到 2020 年间的数据上训练的,然后应用于今天的生产数据。

我们将看到,在这样的数据偏移场景中,模型在新的偏移数据上的表现通常比在原始训练分布上的表现差。我们还将看到,普通神经网络通常无法指示输入数据何时偏离训练分布。最后,我们将探讨本书中介绍的各种方法如何通过不确定性估计来指示数据集偏移,以及这些方法如何增强模型的鲁棒性。以下代码示例将集中在图像分类问题上。然而,值得注意的是,这些见解通常可以推广到其他领域(如自然语言处理)和任务(如回归)。

8.3.1 测量模型对数据集偏移的响应

假设我们有一个训练数据集和一个单独的测试集,我们如何衡量模型在数据发生偏移时是否能及时反应?为了做到这一点,我们需要一个额外的测试集,其中数据已经发生偏移,以检查模型如何响应数据集偏移。一个常用的创建数据偏移测试集的方法最初由 Dan Hendrycks、Thomas Dietterich 及其他人于 2019 年提出。这个方法很简单:从你的初始测试集中取出图像,然后对其应用不同程度的图像质量损坏。Hendrycks 和 Dietterich 提出了一套包含 15 种不同类型图像质量损坏的方法,涵盖了图像噪声、模糊、天气损坏(如雾霾和雪)以及数字损坏等类型。每种损坏类型都有五个严重程度级别,从 1(轻度损坏)到 5(严重损坏)。 8.4展示了一只小猫的图像最初样子(左侧)以及在图像上施加噪声损坏后的效果,分别是严重程度为 1(中间)和 5(右侧)的情况。

PIC

图 8.4:通过在不同损坏严重程度下应用图像质量损坏来生成人工数据集偏移

所有这些图像质量损坏可以方便地使用imgaug Python 包生成。以下代码假设我们磁盘上有一个名为"kitty.png"的图像。我们使用 PIL 包加载图像。然后,我们通过损坏函数的名称指定损坏类型(例如,ShotNoise),并使用通过传递相应整数给关键字参数severity来应用损坏函数,选择严重性等级 1 或 5。


from PIL import Image 
import numpy as np 
import imgaug.augmenters.imgcorruptlike as icl 

image = np.asarray(Image.open("./kitty.png").convert("RGB")) 
corruption_function = icl.ShotNoise 
image_noise_level_01 = corruption_function(severity=1, seed=0)(image=image) 
image_noise_level_05 = corruption_function(severity=5, seed=0)(image=image)

通过这种方式生成数据偏移的优势在于,它可以应用于广泛的计算机视觉问题和数据集。应用这种方法的少数前提条件是数据由图像组成,并且在训练过程中没有使用过这些图像质量损坏(例如,用于数据增强)。此外,通过设置图像质量损坏的严重性,我们可以控制数据集偏移的程度。这使我们能够衡量模型对不同程度的数据集偏移的反应。我们可以衡量性能如何随着数据集偏移而变化,以及校准(在第二章贝叶斯推断基础中引入)如何变化。我们预计使用贝叶斯方法或扩展方法训练的模型会有更好的校准,这意味着它们能够告诉我们数据相较于训练时已经发生了偏移,因此它们对输出的信心较低。

8.3.2 使用贝叶斯方法揭示数据集偏移

在以下的代码示例中,我们将查看书中到目前为止遇到的两种 BDL 方法(基于反向传播的贝叶斯方法和深度集成),并观察它们在前面描述的人工数据集偏移下的表现。我们将它们的表现与普通的神经网络进行比较。

步骤 1:准备环境

我们通过导入一系列包来开始这个示例。这些包包括用于构建和训练神经网络的 TensorFlow 和 TensorFlow Probability;用于处理数值数组(如计算均值)的numpy;用于绘图的SeabornMatplotlibpandas;用于加载和处理图像的cv2imgaug;以及用于计算模型准确度的scikit-learn


import cv2 
import imgaug.augmenters as iaa 
import imgaug.augmenters.imgcorruptlike as icl 
import matplotlib.pyplot as plt 
import numpy as np 
import pandas as pd 
import seaborn as sns 
import tensorflow as tf 
import tensorflow_probability as tfp 
from sklearn.metrics import accuracy_score

在训练之前,我们将加载CIFAR10数据集,这是一个图像分类数据集,并指定不同类别的名称。该数据集包含 10 个不同的类别,我们将在以下代码中指定这些类别的名称,并提供 50,000 个训练图像和 10,000 个测试图像。我们还将保存训练图像的数量,这将在稍后使用重参数化技巧训练模型时用到。


cifar = tf.keras.datasets.cifar10 
(train_images, train_labels), (test_images, test_labels) = cifar.load_data() 

CLASS_NAMES = [ 
"airplane","automobile", "bird", "cat", "deer", 
"dog", "frog", "horse", "ship", "truck" 
] 

NUM_TRAIN_EXAMPLES = train_images.shape[0]

步骤 2:定义和训练模型

在这项准备工作完成后,我们可以定义并训练我们的模型。我们首先创建两个函数来定义和构建 CNN。我们将使用这两个函数来构建普通神经网络和深度集成网络。第一个函数简单地将卷积层与最大池化层结合起来——这是一种常见的做法,我们在第三章《深度学习基础》中介绍过。


def cnn_building_block(num_filters): 
return tf.keras.Sequential( 
[ 
tf.keras.layers.Conv2D( 
filters=num_filters, kernel_size=(3, 3), activation="relu" 
), 
tf.keras.layers.MaxPool2D(strides=2), 
] 
    )

第二个函数则依次使用多个卷积/最大池化块,并在此序列后面跟着一个最终的密集层:


def build_and_compile_model(): 
model = tf.keras.Sequential( 
[ 
tf.keras.layers.Rescaling(1.0 / 255, input_shape=(32, 32, 3)), 
cnn_building_block(16), 
cnn_building_block(32), 
cnn_building_block(64), 
tf.keras.layers.MaxPool2D(strides=2), 
tf.keras.layers.Flatten(), 
tf.keras.layers.Dense(64, activation="relu"), 
tf.keras.layers.Dense(10, activation="softmax"), 
] 
) 
model.compile( 
optimizer="adam", 
loss="sparse_categorical_crossentropy", 
metrics=["accuracy"], 
) 
    return model

我们还创建了两个类似的函数,用于基于重新参数化技巧定义和构建使用 Bayes By Backprop(BBB)的网络。策略与普通神经网络相同,只不过我们现在将使用来自 TensorFlow Probability 包的卷积层和密集层,而不是 TensorFlow 包。卷积/最大池化块定义如下:


def cnn_building_block_bbb(num_filters, kl_divergence_function): 
return tf.keras.Sequential( 
[ 
tfp.layers.Convolution2DReparameterization( 
num_filters, 
kernel_size=(3, 3), 
kernel_divergence_fn=kl_divergence_function, 
activation=tf.nn.relu, 
), 
tf.keras.layers.MaxPool2D(strides=2), 
] 
    )

最终的网络定义如下:


def build_and_compile_model_bbb(): 

kl_divergence_function = lambda q, p, _: tfp.distributions.kl_divergence( 
q, p 
) / tf.cast(NUM_TRAIN_EXAMPLES, dtype=tf.float32) 

model = tf.keras.models.Sequential( 
[ 
tf.keras.layers.Rescaling(1.0 / 255, input_shape=(32, 32, 3)), 
cnn_building_block_bbb(16, kl_divergence_function), 
cnn_building_block_bbb(32, kl_divergence_function), 
cnn_building_block_bbb(64, kl_divergence_function), 
tf.keras.layers.Flatten(), 
tfp.layers.DenseReparameterization( 
64, 
kernel_divergence_fn=kl_divergence_function, 
activation=tf.nn.relu, 
), 
tfp.layers.DenseReparameterization( 
10, 
kernel_divergence_fn=kl_divergence_function, 
activation=tf.nn.softmax, 
), 
] 
) 

model.compile( 
optimizer="adam", 
loss="sparse_categorical_crossentropy", 
metrics=["accuracy"], 
experimental_run_tf_function=False, 
) 

model.build(input_shape=[None, 32, 32, 3]) 
    return model

然后我们可以训练普通神经网络:


vanilla_model = build_and_compile_model() 
vanilla_model.fit(train_images, train_labels, epochs=10)

我们还可以训练一个五成员的集成模型:


NUM_ENSEMBLE_MEMBERS = 5 
ensemble_model = [] 
for ind in range(NUM_ENSEMBLE_MEMBERS): 
member = build_and_compile_model() 
print(f"Train model {ind:02}") 
member.fit(train_images, train_labels, epochs=10) 
    ensemble_model.append(member)

最后,我们训练 BBB 模型。注意,我们将训练 BBB 模型 15 个 epoch,而不是 10 个 epoch,因为它收敛的时间稍长。


bbb_model = build_and_compile_model_bbb() 
bbb_model.fit(train_images, train_labels, epochs=15)

步骤 3:获取预测结果

现在我们已经有了三个训练好的模型,可以使用它们对保留的测试集进行预测。为了保持计算的可控性,在这个例子中,我们将专注于测试集中的前 1000 张图像:


NUM_SUBSET = 1000 
test_images_subset = test_images[:NUM_SUBSET] 
test_labels_subset = test_labels[:NUM_SUBSET]

如果我们想要衡量数据集偏移的响应,首先需要对数据集应用人工图像损坏。为此,我们首先指定一组来自imgaug包的函数。从这些函数的名称中,我们可以推断出每个函数实现的损坏类型:例如,函数icl.GaussianNoise通过向图像应用高斯噪声来损坏图像。我们还通过函数的数量推断出损坏类型的数量,并将其保存在NUM_TYPES变量中。最后,我们将损坏级别设置为 5。


corruption_functions = [ 
icl.GaussianNoise, 
icl.ShotNoise, 
icl.ImpulseNoise, 
icl.DefocusBlur, 
icl.GlassBlur, 
icl.MotionBlur, 
icl.ZoomBlur, 
icl.Snow, 
icl.Frost, 
icl.Fog, 
icl.Brightness, 
icl.Contrast, 
icl.ElasticTransform, 
icl.Pixelate, 
icl.JpegComdivssion, 
] 
NUM_TYPES = len(corruption_functions) 
NUM_LEVELS = 5

配备了这些函数后,我们现在可以开始损坏图像了。在下一个代码块中,我们遍历不同的损坏级别和类型,并将所有损坏的图像收集到名为corrupted_images的变量中。


corrupted_images = [] 
# loop over different corruption severities 
for corruption_severity in range(1, NUM_LEVELS+1): 
corruption_type_batch = [] 
# loop over different corruption types 
for corruption_type in corruption_functions: 
corrupted_image_batch = corruption_type( 
severity=corruption_severity, seed=0 
)(images=test_images_subset) 
corruption_type_batch.append(corrupted_image_batch) 
corruption_type_batch = np.stack(corruption_type_batch, axis=0) 
corrupted_images.append(corruption_type_batch) 
corrupted_images = np.stack(corrupted_images, axis=0)

在训练完三个模型并获得损坏图像后,我们现在可以看到模型对不同级别数据集偏移的反应。我们将首先获取三个模型对损坏图像的预测结果。为了进行推理,我们需要将损坏的图像调整为模型接受的输入形状。目前,这些图像仍然存储在针对损坏类型和级别的不同轴上。我们通过重新调整corrupted_images数组来改变这一点:


corrupted_images = corrupted_images.reshape((-1, 32, 32, 3))

然后,我们可以使用普通 CNN 模型对原始图像和腐蚀图像进行推理。在推理模型预测后,我们将预测结果重塑,以便分离腐蚀类型和级别的预测:


# Get predictions on original images 
vanilla_predictions = vanilla_model.predict(test_images_subset) 
# Get predictions on corrupted images 
vanilla_predictions_on_corrupted = vanilla_model.predict(corrupted_images) 
vanilla_predictions_on_corrupted = vanilla_predictions_on_corrupted.reshape( 
(NUM_LEVELS, NUM_TYPES, NUM_SUBSET, -1) 
)

为了使用集成模型进行推理,我们首先定义一个预测函数以避免代码重复。此函数处理对集成中不同成员模型的循环,并最终通过平均将不同的预测结果结合起来:


def get_ensemble_predictions(images, num_inferences): 
ensemble_predictions = tf.stack( 
[ 
ensemble_model[ensemble_ind].predict(images) 
for ensemble_ind in range(num_inferences) 
], 
axis=0, 
) 
    return np.mean(ensemble_predictions, axis=0)

配备了这个函数后,我们可以对原始图像和腐蚀图像使用集成模型进行推理:


# Get predictions on original images 
ensemble_predictions = get_ensemble_predictions( 
test_images_subset, NUM_ENSEMBLE_MEMBERS 
) 
# Get predictions on corrupted images 
ensemble_predictions_on_corrupted = get_ensemble_predictions( 
corrupted_images, NUM_ENSEMBLE_MEMBERS 
) 
ensemble_predictions_on_corrupted = ensemble_predictions_on_corrupted.reshape( 
(NUM_LEVELS, NUM_TYPES, NUM_SUBSET, -1) 
)

就像对于集成模型一样,我们为 BBB 模型编写了一个推理函数,该函数处理不同采样循环的迭代,并收集并结合结果:


def get_bbb_predictions(images, num_inferences): 
bbb_predictions = tf.stack( 
[bbb_model.predict(images) for _ in range(num_inferences)], 
axis=0, 
) 
    return np.mean(bbb_predictions, axis=0)

然后,我们利用这个函数获取 BBB 模型在原始图像和腐蚀图像上的预测。我们从 BBB 模型中采样 20 次:


NUM_INFERENCES_BBB = 20 
# Get predictions on original images 
bbb_predictions = get_bbb_predictions( 
test_images_subset, NUM_INFERENCES_BBB 
) 
# Get predictions on corrupted images 
bbb_predictions_on_corrupted = get_bbb_predictions( 
corrupted_images, NUM_INFERENCES_BBB 
) 
bbb_predictions_on_corrupted = bbb_predictions_on_corrupted.reshape( 
(NUM_LEVELS, NUM_TYPES, NUM_SUBSET, -1) 
)

我们可以通过返回具有最大 softmax 得分的类别索引和最大 softmax 得分,分别将三个模型的预测转换为预测类别及其相关的置信度得分:


def get_classes_and_scores(model_predictions): 
model_predicted_classes = np.argmax(model_predictions, axis=-1) 
model_scores = np.max(model_predictions, axis=-1) 
    return model_predicted_classes, model_scores

然后可以应用此函数来获取我们三个模型的预测类别和置信度得分:


# Vanilla model 
vanilla_predicted_classes, vanilla_scores = get_classes_and_scores( 
vanilla_predictions 
) 
( 
vanilla_predicted_classes_on_corrupted, 
vanilla_scores_on_corrupted, 
) = get_classes_and_scores(vanilla_predictions_on_corrupted) 

# Ensemble model 
( 
ensemble_predicted_classes, 
ensemble_scores, 
) = get_classes_and_scores(ensemble_predictions) 
( 
ensemble_predicted_classes_on_corrupted, 
ensemble_scores_on_corrupted, 
) = get_classes_and_scores(ensemble_predictions_on_corrupted) 

# BBB model 
( 
bbb_predicted_classes, 
bbb_scores, 
) = get_classes_and_scores(bbb_predictions) 
( 
bbb_predicted_classes_on_corrupted, 
bbb_scores_on_corrupted, 
) = get_classes_and_scores(bbb_predictions_on_corrupted)

让我们可视化这三个模型在一张展示汽车的选定图像上预测的类别和置信度得分。为了绘图,我们首先将包含腐蚀图像的数组重塑为更方便的格式:


plot_images = corrupted_images.reshape( 
(NUM_LEVELS, NUM_TYPES, NUM_SUBSET, 32, 32, 3) 
)

然后,我们绘制了列表中前三种腐蚀类型的选定汽车图像,涵盖所有五个腐蚀级别。对于每种组合,我们在图像标题中显示每个模型的预测得分,并在方括号中显示预测类别。该图如图**8.5所示。

PIC

图 8.5:一张汽车图像已经被不同的腐蚀类型(行)和级别(列,严重程度从左到右增加)腐蚀

代码继续:


# Index of the selected images 
ind_image = 9 
# Define figure 
fig, axes = plt.subplots(nrows=3, ncols=5, figsize=(16, 10)) 
# Loop over corruption levels 
for ind_level in range(NUM_LEVELS): 
# Loop over corruption types 
for ind_type in range(3): 
# Plot slightly upscaled image for easier inspection 
image = plot_images[ind_level, ind_type, ind_image, ...] 
image_upscaled = cv2.resize( 
image, dsize=(150, 150), interpolation=cv2.INTER_CUBIC 
) 
axes[ind_type, ind_level].imshow(image_upscaled) 
# Get score and class predicted by vanilla model 
vanilla_score = vanilla_scores_on_corrupted[ 
ind_level, ind_type, ind_image, ... 
] 
vanilla_prediction = vanilla_predicted_classes_on_corrupted[ 
ind_level, ind_type, ind_image, ... 
] 
# Get score and class predicted by ensemble model 
ensemble_score = ensemble_scores_on_corrupted[ 
ind_level, ind_type, ind_image, ... 
] 
ensemble_prediction = ensemble_predicted_classes_on_corrupted[ 
ind_level, ind_type, ind_image, ... 
] 
# Get score and class predicted by BBB model 
bbb_score = bbb_scores_on_corrupted[ind_level, ind_type, ind_image, ...] 
bbb_prediction = bbb_predicted_classes_on_corrupted[ 
ind_level, ind_type, ind_image, ... 
] 
# Plot prediction info in title 
title_text = ( 
f"Vanilla: {vanilla_score:.3f} " 
+ f"[{CLASS_NAMES[vanilla_prediction]}] \n" 
+ f"Ensemble: {ensemble_score:.3f} " 
+ f"[{CLASS_NAMES[ensemble_prediction]}] \n" 
+ f"BBB: {bbb_score:.3f} " 
+ f"[{CLASS_NAMES[bbb_prediction]}]" 
) 
axes[ind_type, ind_level].set_title(title_text, fontsize=14) 
# Remove axes ticks and labels 
axes[ind_type, ind_level].axis("off") 
fig.tight_layout() 
plt.show()

图**8.5只显示了单张图像的结果,因此我们不应过度解读这些结果。然而,我们已经可以观察到,两个贝叶斯方法(尤其是集成方法)的预测得分通常比普通神经网络更不极端,后者的预测得分高达 0.95。此外,我们看到,对于所有三个模型,预测得分通常随着腐蚀级别的增加而降低。这是预期的:由于图像中的汽车在腐蚀越严重时变得越难以辨认,我们希望模型的置信度也会随之降低。特别是,集成方法在增加腐蚀级别时显示出了预测得分的明显且一致的下降。

第 4 步:衡量准确性

有些模型比其他模型更能适应数据集的偏移吗?我们可以通过查看三种模型在不同损坏水平下的准确性来回答这个问题。预计所有模型在输入图像逐渐损坏时准确性会降低。然而,更鲁棒的模型在损坏变得更严重时,准确性下降应该较少。

首先,我们可以计算三种模型在原始测试图像上的准确性:


vanilla_acc = accuracy_score( 
test_labels_subset.flatten(), vanilla_predicted_classes 
) 
ensemble_acc = accuracy_score( 
test_labels_subset.flatten(), ensemble_predicted_classes 
) 
bbb_acc = accuracy_score( 
test_labels_subset.flatten(), bbb_predicted_classes 
)

我们可以将这些准确性存储在字典列表中,这将使我们更容易系统地绘制它们。我们传递相应的模型名称。对于损坏的类型级别,我们传递0,因为这些是原始图像上的准确性。


accuracies = [ 
{"model_name": "vanilla", "type": 0, "level": 0, "accuracy": vanilla_acc}, 
{"model_name": "ensemble", "type": 0, "level": 0, "accuracy": ensemble_acc}, 
{"model_name": "bbb", "type": 0, "level": 0, "accuracy": bbb_acc}, 
]

接下来,我们计算三种模型在不同损坏类型和损坏级别组合下的准确性。我们还将结果附加到之前开始的准确性列表中:


for ind_type in range(NUM_TYPES): 
for ind_level in range(NUM_LEVELS): 
# Calculate accuracy for vanilla model 
vanilla_acc_on_corrupted = accuracy_score( 
test_labels_subset.flatten(), 
vanilla_predicted_classes_on_corrupted[ind_level, ind_type, :], 
) 
accuracies.append( 
{ 
"model_name": "vanilla", 
"type": ind_type + 1, 
"level": ind_level + 1, 
"accuracy": vanilla_acc_on_corrupted, 
} 
) 

# Calculate accuracy for ensemble model 
ensemble_acc_on_corrupted = accuracy_score( 
test_labels_subset.flatten(), 
ensemble_predicted_classes_on_corrupted[ind_level, ind_type, :], 
) 
accuracies.append( 
{ 
"model_name": "ensemble", 
"type": ind_type + 1, 
"level": ind_level + 1, 
"accuracy": ensemble_acc_on_corrupted, 
} 
) 

# Calculate accuracy for BBB model 
bbb_acc_on_corrupted = accuracy_score( 
test_labels_subset.flatten(), 
bbb_predicted_classes_on_corrupted[ind_level, ind_type, :], 
) 
accuracies.append( 
{ 
"model_name": "bbb", 
"type": ind_type + 1, 
"level": ind_level + 1, 
"accuracy": bbb_acc_on_corrupted, 
} 
        )

然后,我们可以绘制原始图像和逐渐损坏图像的准确性分布。我们首先将字典列表转换为 pandas dataframe。这有一个优势,即 dataframe 可以直接传递给绘图库seaborn,这样我们可以指定不同模型的结果以不同色调进行绘制。


df = pd.DataFrame(accuracies) 
plt.figure(dpi=100) 
sns.boxplot(data=df, x="level", y="accuracy", hue="model_name") 
plt.legend(loc="center left", bbox_to_anchor=(1, 0.5)) 
plt.tight_layout 
plt.show()

这会生成以下输出:

图片

图 8.6:三种不同模型(不同色调)在原始测试图像(级别 0)以及不同程度的损坏(级别 1-5)上的准确性

结果图如图**8.6所示。我们可以看到,在原始测试图像上,普通模型和 BBB 模型的准确性相当,而集成模型的准确性稍高。随着损坏的引入,我们看到普通神经网络的表现比集成模型或 BBB 模型更差(通常是显著差)。BDL 模型性能的相对提升展示了贝叶斯方法的正则化效应:这些方法能更有效地捕捉数据的分布,使其对扰动更加鲁棒。BBB 模型特别能抵御数据损坏的增加,展示了变分学习的一个关键优势。

步骤 5:衡量校准

查看准确度是确定模型在数据集变化下的鲁棒性的一种好方法。但它并没有真正告诉我们模型是否能够通过较低的置信度分数(当数据集发生变化时)提醒我们,并且模型在输出时变得不那么自信。这个问题可以通过观察模型在数据集变化下的校准表现来回答。我们在第三章的《深度学习基础》中已经介绍了校准和期望校准误差的概念。现在,我们将把这些概念付诸实践,以理解当图像变得越来越受损且难以预测时,模型是否适当地调整了它们的置信度。

首先,我们将实现第三章《深度学习基础》中介绍的期望校准误差(ECE),作为校准的标量衡量标准:


def expected_calibration_error( 
divd_correct, 
divd_score, 
n_bins=5, 
): 
"""Compute expected calibration error. 
---------- 
divd_correct : np.ndarray (n_samples,) 
Whether the prediction is correct or not 
divd_score : np.ndarray (n_samples,) 
Confidence in the prediction 
n_bins : int, default=5 
Number of bins to discretize the [0, 1] interval. 
""" 
# Convert from bool to integer (makes counting easier) 
divd_correct = divd_correct.astype(np.int32) 

# Create bins and assign prediction scores to bins 
bins = np.linspace(0.0, 1.0, n_bins + 1) 
binids = np.searchsorted(bins[1:-1], divd_score) 

# Count number of samples and correct predictions per bin 
bin_true_counts = np.bincount( 
binids, weights=divd_correct, minlength=len(bins) 
) 
bin_counts = np.bincount(binids, minlength=len(bins)) 

# Calculate sum of confidence scores per bin 
bin_probs = np.bincount(binids, weights=divd_score, minlength=len(bins)) 

# Identify bins that contain samples 
nonzero = bin_counts != 0 
# Calculate accuracy for every bin 
bin_acc = bin_true_counts[nonzero] / bin_counts[nonzero] 
# Calculate average confidence scores per bin 
bin_conf = bin_probs[nonzero] / bin_counts[nonzero] 

    return np.average(np.abs(bin_acc - bin_conf), weights=bin_counts[nonzero])

然后,我们可以计算三个模型在原始测试图像上的期望校准误差(ECE)。我们将箱子的数量设置为10,这是计算 ECE 时常用的选择:


NUM_BINS = 10 

vanilla_cal = expected_calibration_error( 
test_labels_subset.flatten() == vanilla_predicted_classes, 
vanilla_scores, 
n_bins=NUM_BINS, 
) 

ensemble_cal = expected_calibration_error( 
test_labels_subset.flatten() == ensemble_predicted_classes, 
ensemble_scores, 
n_bins=NUM_BINS, 
) 

bbb_cal = expected_calibration_error( 
test_labels_subset.flatten() == bbb_predicted_classes, 
bbb_scores, 
n_bins=NUM_BINS, 
)

就像我们之前处理准确度一样,我们将把校准结果存储在一个字典列表中,这样就更容易绘制它们:


calibration = [ 
{ 
"model_name": "vanilla", 
"type": 0, 
"level": 0, 
"calibration_error": vanilla_cal, 
}, 
{ 
"model_name": "ensemble", 
"type": 0, 
"level": 0, 
"calibration_error": ensemble_cal, 
}, 
{ 
"model_name": "bbb", 
"type": 0, 
"level": 0, 
"calibration_error": bbb_cal, 
}, 
]

接下来,我们根据不同的腐蚀类型和腐蚀级别组合,计算三个模型的期望校准误差。我们还将结果附加到之前开始的校准结果列表中:


for ind_type in range(NUM_TYPES): 
for ind_level in range(NUM_LEVELS): 
# Calculate calibration error for vanilla model 
vanilla_cal_on_corrupted = expected_calibration_error( 
test_labels_subset.flatten() 
== vanilla_predicted_classes_on_corrupted[ind_level, ind_type, :], 
vanilla_scores_on_corrupted[ind_level, ind_type, :], 
) 
calibration.append( 
{ 
"model_name": "vanilla", 
"type": ind_type + 1, 
"level": ind_level + 1, 
"calibration_error": vanilla_cal_on_corrupted, 
} 
) 

# Calculate calibration error for ensemble model 
ensemble_cal_on_corrupted = expected_calibration_error( 
test_labels_subset.flatten() 
== ensemble_predicted_classes_on_corrupted[ind_level, ind_type, :], 
ensemble_scores_on_corrupted[ind_level, ind_type, :], 
) 
calibration.append( 
{ 
"model_name": "ensemble", 
"type": ind_type + 1, 
"level": ind_level + 1, 
"calibration_error": ensemble_cal_on_corrupted, 
} 
) 

# Calculate calibration error for BBB model 
bbb_cal_on_corrupted = expected_calibration_error( 
test_labels_subset.flatten() 
== bbb_predicted_classes_on_corrupted[ind_level, ind_type, :], 
bbb_scores_on_corrupted[ind_level, ind_type, :], 
) 
calibration.append( 
{ 
"model_name": "bbb", 
"type": ind_type + 1, 
"level": ind_level + 1, 
"calibration_error": bbb_cal_on_corrupted, 
} 
        )

最后,我们将使用pandasseaborn再次绘制校准结果的箱形图:


df = pd.DataFrame(calibration) 
plt.figure(dpi=100) 
sns.boxplot(data=df, x="level", y="calibration_error", hue="model_name") 
plt.legend(loc="center left", bbox_to_anchor=(1, 0.5)) 
plt.tight_layout 
plt.show()

校准结果显示在图**8.7中。我们可以看到,在原始测试图像上,所有三个模型的校准误差都比较低,集成模型的表现略逊色于另外两个模型。随着数据集变化程度的增加,我们可以看到,传统模型的校准误差大幅增加。对于两种贝叶斯方法,校准误差也增加了,但比传统模型要少得多。这意味着贝叶斯方法在数据集发生变化时能够更好地通过较低的置信度分数来指示(即模型在输出时变得相对不那么自信,随着腐蚀程度的增加,表现出这种特征)。

PIC

图 8.7:三种不同模型在原始测试图像(级别 0)和不同腐蚀级别(级别 1-5)上的期望校准误差

在下一节中,我们将讨论数据选择。

8.4 使用通过不确定性进行的数据选择来保持模型的更新

我们在本章开头看到,能够使用不确定性来判断数据是否是训练数据的一部分。在主动学习这一机器学习领域的背景下,我们可以进一步扩展这个想法。主动学习的承诺是,如果我们能够控制模型训练的数据类型,模型可以在更少的数据上更有效地学习。从概念上讲,这是有道理的:如果我们在质量不足的数据上训练模型,它的表现也不会很好。主动学习是一种通过提供可以从不属于训练数据的数据池中获取数据的函数,来引导模型学习过程和训练数据的方法。通过反复从数据池中选择正确的数据,我们可以训练出比随机选择数据时表现更好的模型。

主动学习可以应用于许多现代系统,在这些系统中有大量未标记的数据可供使用,我们需要仔细选择想要标记的数据量。一个例子是自动驾驶系统:车上的摄像头记录了大量数据,但通常没有预算标记所有数据。通过仔细选择最具信息量的数据点,我们可以以比随机选择数据标记时更低的成本提高模型性能。在主动学习的背景下,估计不确定性发挥着重要作用。模型通常会从数据分布中那些低置信度预测的区域学到更多。让我们通过一个案例研究来看看如何在主动学习的背景下使用不确定性。

在这个案例研究中,我们将重现一篇基础性主动学习论文的结果:基于图像数据的深度贝叶斯主动学习(2017)。我们将使用MNIST数据集,并在越来越多的数据上训练模型,通过不确定性方法选择要添加到训练集中的数据点。在这种情况下,我们将使用认知不确定性来选择最具信息量的数据点。具有高认知不确定性的图像应该是模型之前没有见过的图像;通过增加更多这样的图像,可以减少不确定性。作为对比,我们还将随机选择数据点。

第一步:准备数据集

我们将首先创建加载数据集的函数。数据集函数需要以下库导入:


import dataclasses 
from pathlib import Path 
import uuid 
from typing import Optional, Tuple 

import numpy as np 
import tensorflow as tf 
from sklearn.utils import shuffle

由于我们的总数据集将包含相当多的组件,我们将创建一个小的dataclass,以便轻松访问数据集的不同部分。我们还将修改__repr__函数,使其能够以更易读的格式打印数据集内容。


@dataclasses.dataclass 
class Data: 
x_train: np.ndarray 
y_train: np.ndarray 
x_test: np.ndarray 
y_test: np.ndarray 
x_train_al: Optional[np.ndarray] = None 
y_train_al: Optional[np.ndarray] = None 

def __repr__(self) -*>* str: 
repr_str = "" 
for field in dataclasses.fields(self): 
repr_str += f"{field.name}: {getattr(self, field.name).shape} \n" 
        return repr_str

然后我们可以定义函数来加载标准数据集。


def get_data() -*>* Data: 
num_classes = 10 
(x_train, y_train), (x_test, y_test) = tf.keras.datasets.mnist.load_data() 
# Scale images to the [0, 1] range 
x_train = x_train.astype("float32") / 255 
x_test = x_test.astype("float32") / 255 
# Make sure images have shape (28, 28, 1) 
x_train = np.expand_dims(x_train, -1) 
x_test = np.expand_dims(x_test, -1) 
y_train = tf.keras.utils.to_categorical(y_train, num_classes) 
y_test = tf.keras.utils.to_categorical(y_test, num_classes) 
    return Data(x_train, y_train, x_test, y_test)

最初,我们将从MNIST数据集中仅使用 20 个样本进行训练。然后我们每次获取 10 个数据点,并重新训练我们的模型。为了在开始时帮助我们的模型,我们将确保这 20 个数据点在数据集的不同类别之间是平衡的。以下函数给出了我们可以使用的索引,用于创建初始的 20 个样本,每个类别 2 个样本:


def get_random_balanced_indices( 
data: Data, initial_n_samples: int 
) -*>* np.ndarray: 
labels = np.argmax(data.y_train, axis=1) 
indices = [] 
label_list = np.unique(labels) 
for label in label_list: 
indices_label = np.random.choice( 
np.argwhere(labels == label).flatten(), 
size=initial_n_samples // len(label_list), 
replace=False 
) 
indices.extend(indices_label) 
indices = np.array(indices) 
np.random.shuffle(indices) 
    return indices

然后我们可以定义一个小函数,实际获取我们的初始数据集:


def get_initial_ds(data: Data, initial_n_samples: int) -*>* Data: 
indices = get_random_balanced_indices(data, initial_n_samples) 
x_train_al, y_train_al = data.x_train[indices], data.y_train[indices] 
x_train = np.delete(data.x_train, indices, axis=0) 
y_train = np.delete(data.y_train, indices, axis=0) 
return Data( 
x_train, y_train, data.x_test, data.y_test, x_train_al, y_train_al 
    )

步骤 2:设置配置

在我们开始构建模型并创建主动学习循环之前,我们定义一个小的配置dataclass来存储一些在运行主动学习脚本时可能想要调整的主要变量。创建这样的配置类使你可以灵活调整不同的参数。


@dataclasses.dataclass 
class Config: 
initial_n_samples: int 
n_total_samples: int 
n_epochs: int 
n_samples_per_iter: int 
# string representation of the acquisition function 
acquisition_type: str 
# number of mc_dropout iterations 
    n_iter: int

步骤 3:定义模型

我们现在可以定义我们的模型。我们将使用一个简单的小型 CNN 并加入 Dropout。


def build_model(): 
model = tf.keras.models.Sequential([ 
Input(shape=(28, 28, 1)), 
layers.Conv2D(32, kernel_size=(4, 4), activation="relu"), 
layers.Conv2D(32, kernel_size=(4, 4), activation="relu"), 
layers.MaxPooling2D(pool_size=(2, 2)), 
layers.Dropout(0.25), 
layers.Flatten(), 
layers.Dense(128, activation="relu"), 
layers.Dropout(0.5), 
layers.Dense(10, activation="softmax"), 
]) 
model.compile( 
tf.keras.optimizers.Adam(), 
loss="categorical_crossentropy", 
metrics=["accuracy"], 
experimental_run_tf_function=False, 
) 
    return model

步骤 4:定义不确定性函数

如前所述,我们将使用认知不确定性(也称为知识不确定性)作为我们主要的不确定性函数来获取新样本。让我们定义一个函数来计算我们预测的认知不确定性。我们假设输入的预测(divds)的形状为n_imagesn_predictionsn_classes。我们首先定义一个函数来计算总不确定性。给定一个集成模型的预测,它可以定义为集成平均预测的熵。


def total_uncertainty( 
divds: np.ndarray, epsilon: float = 1e-10 
) -*>* np.ndarray: 
mean_divds = np.mean(divds, axis=1) 
log_divds = -np.log(mean_divds + epsilon) 
    return np.sum(mean_divds * log_divds, axis=1)

然后我们定义数据不确定性(或称为随机不确定性),对于一个集成模型来说,它是每个集成成员的熵的平均值。


def data_uncertainty(divds: np.ndarray, epsilon: float = 1e-10) -*>* np.ndarray: 
log_divds = -np.log(divds + epsilon) 
    return np.mean(np.sum(divds * log_divds, axis=2), axis=1)

最终,我们得到了我们的知识(或认知)不确定性,这就是通过从预测的总不确定性中减去数据不确定性来得到的。


def knowledge_uncertainty( 
divds: np.ndarray, epsilon: float = 1e-10 
) -*>* np.ndarray: 
    return total_uncertainty(divds, epsilon) - data_uncertainty(divds, epsilon)

定义了这些不确定性函数后,我们可以定义实际的获取函数,它们的主要输入是我们的训练数据和模型。为了通过知识不确定性来获取样本,我们进行以下操作:

  1. 通过 MC Dropout 获取我们的集成预测。

  2. 计算这个集成模型的知识不确定性值。

  3. 对不确定性值进行排序,获取它们的索引,并返回我们训练数据中具有最高认知不确定性的索引。

然后,稍后我们可以重复使用这些索引来索引我们的训练数据,实际上获取我们想要添加的训练样本。


from typing import Callable 
from keras import Model 
from tqdm import tqdm 

import numpy as np 

def acquire_knowledge_uncertainty( 
x_train: np.ndarray, 
n_samples: int, 
model: Model, 
n_iter: int, 
*args, 
**kwargs 
): 
divds = get_mc_predictions(model, n_iter, x_train) 
ku = knowledge_uncertainty(divds) 
    return np.argsort(ku, axis=-1)[-n_samples:]

我们通过以下方式获得 MC Dropout 预测:


def get_mc_predictions( 
model: Model, n_iter: int, x_train: np.ndarray 
) -*>* np.ndarray: 
divds = [] 
for _ in tqdm(range(n_iter)): 
divds_iter = [ 
model(batch, training=True) 
for batch in np.array_split(x_train, 6) 
] 
divds.append(np.concatenate(divds_iter)) 
# format data such that we have n_images, n_predictions, n_classes 
divds = np.moveaxis(np.stack(divds), 0, 1) 
    return divds

为了避免内存溢出,我们将训练数据分批处理,每批 6 个样本,对于每一批,我们将计算n_iter次预测。为了确保我们的预测具有多样性,我们将模型的training参数设置为True

对于我们的比较,我们还定义了一个获取函数,该函数返回一个随机的索引列表:


def acquire_random(x_train: np.ndarray, n_samples: int, *args, **kwargs): 
    return np.random.randint(low=0, high=len(x_train), size=n_samples)

最后,我们根据工厂方法模式定义一个小函数,以确保我们可以在循环中使用相同的函数,使用随机采集函数或知识不确定性。像这样的工厂小函数有助于在你想用不同配置运行相同代码时保持代码模块化。


def acquisition_factory(acquisition_type: str) -*>* Callable: 
if acquisition_type == "knowledge_uncertainty": 
return acquire_knowledge_uncertainty 
if acquisition_type == "random": 
        return acquire_random

现在我们已经定义了采集函数,我们已经准备好实际定义运行我们主动学习迭代的循环。

第 5 步:定义循环

首先,我们定义我们的配置。在这种情况下,我们使用知识不确定性作为我们的不确定性函数。在另一个循环中,我们将使用一个随机采集函数来比较我们即将定义的循环结果。我们将从 20 个样本开始我们的数据集,直到我们达到 1,000 个样本。每个模型将训练 50 个 epoch,每次迭代我们获取 10 个样本。为了获得我们的 MC dropout 预测,我们将在整个训练集(减去已获取的样本)上运行 100 次。


cfg = Config( 
initial_n_samples=20, 
n_total_samples=1000, 
n_epochs=50, 
n_samples_per_iteration=10, 
acquisition_type="knowledge_uncertainty", 
n_iter=100, 
)

然后我们可以获取数据,并定义一个空字典来跟踪每次迭代的测试准确率。我们还创建一个空列表,用于跟踪我们添加到训练数据中的所有索引。


data: Data = get_initial_ds(get_data(), cfg.initial_n_samples) 
accuracies = {} 
added_indices = []

我们还为我们的运行分配了一个全球唯一标识符UUID),以确保我们可以轻松找到它,并且不会覆盖我们作为循环一部分保存的结果。我们创建一个目录来保存我们的数据,并将配置保存到该目录,以确保我们始终知道model_dir中的数据是使用何种配置创建的。


run_uuid = str(uuid.uuid4()) 
model_dir = Path("./models") / cfg.acquisition_type / run_uuid 
model_dir.mkdir(parents=True, exist_ok=True)

现在,我们可以实际运行我们的主动学习循环。我们将把这个循环分成三个部分:

  1. 我们定义循环,并在已获取的样本上拟合模型:

    
    for i in range(cfg.n_total_samples // cfg.n_samples_per_iter): 
    iter_dir = model_dir / str(i) 
    model = build_model() 
    model.fit( 
    x=data.x_train_al, 
    y=data.y_train_al, 
    validation_data=(data.x_test, data.y_test), 
    epochs=cfg.n_epochs, 
    callbacks=[get_callback(iter_dir)], 
    verbose=2, 
        )
    
  2. 然后,我们加载具有最佳验证准确率的模型,并根据采集函数更新我们的数据集:

    
    model = tf.keras.models.load_model(iter_dir) 
    indices_to_add = acquisition_factory(cfg.acquisition_type)( 
    data.x_train, 
    cfg.n_samples_per_iter, 
    n_iter=cfg.n_iter, 
    model=model, 
    ) 
    added_indices.append(indices_to_add) 
        data, (iter_x, iter_y) = update_ds(data, indices_to_add)
    
  3. 最后,我们保存已添加的图片,计算测试准确率,并保存结果:

    
    save_images_and_labels_added(iter_dir, iter_x, iter_y) 
    divds = model(data.x_test) 
    accuracy = get_accuracy(data.y_test, divds) 
    accuracies[i] = accuracy 
        save_results(accuracies, added_indices, model_dir)
    

在这个循环中,我们定义了一些小的辅助函数。首先,我们为我们的模型定义了一个回调,以将具有最高验证准确率的模型保存到我们的模型目录:


def get_callback(model_dir: Path): 
model_checkpoint_callback = tf.keras.callbacks.ModelCheckpoint( 
str(model_dir), 
monitor="val_accuracy", 
verbose=0, 
save_best_only=True, 
) 
    return model_checkpoint_callback

我们还定义了一个函数来计算测试集的准确率:


def get_accuracy(y_test: np.ndarray, divds: np.ndarray) -*>* float: 
acc = tf.keras.metrics.CategoricalAccuracy() 
acc.update_state(divds, y_test) 
    return acc.result().numpy() * 100

我们还定义了两个小函数,用于每次迭代保存结果:


def save_images_and_labels_added( 
output_path: Path, iter_x: np.ndarray, iter_y: np.ndarray 
): 
df = pd.DataFrame() 
df["label"] = np.argmax(iter_y, axis=1) 
iter_x_normalised = (np.squeeze(iter_x, axis=-1) * 255).astype(np.uint8) 
df["image"] = iter_x_normalised.reshape(10, 28*28).tolist() 
df.to_parquet(output_path / "added.parquet", index=False) 

def save_results( 
accuracies: Dict[int, float], added_indices: List[int], model_dir: Path 
): 
df = pd.DataFrame(accuracies.items(), columns=["i", "accuracy"]) 
df["added"] = added_indices 
    df.to_parquet(f"{model_dir}/results.parquet", index=False)

请注意,运行主动学习循环需要相当长的时间:每次迭代,我们训练并评估模型 50 个 epoch,然后在我们的池集(完整的训练数据集减去已获取的样本)上运行 100 次。使用随机采集函数时,我们跳过最后一步,但仍然每次迭代将验证数据运行 50 次,以确保使用具有最佳验证准确率的模型。这需要时间,但仅仅选择具有最佳训练准确率的模型是有风险的:我们的模型在训练过程中多次看到相同的几张图片,因此很可能会过拟合训练数据。

第 6 步:检查结果

现在,我们有了循环,可以检查这个过程的结果。我们将使用seabornmatplotlib来可视化我们的结果:


import seaborn as sns 
import matplotlib.pyplot as plt 
import pandas as pd 
import numpy as np 
sns.set_style("darkgrid") 
sns.set_context("paper")

我们最感兴趣的主要结果是两种模型的测试准确率随时间的变化,这些模型分别是基于随机获取函数训练的模型和通过知识不确定性获取数据训练的模型。为了可视化这个结果,我们定义一个函数,加载结果并返回一个图表,显示每个主动学习迭代周期的准确率:


def plot(uuid: str, acquisition: str, ax=None): 
acq_name = acquisition.replace("_", " ") 
df = pd.read_parquet(f"./models/{acquisition}/{uuid}/results.parquet")[:-1] 
df = df.rename(columns={"accuracy": acq_name}) 
df["n_samples"] = df["i"].apply(lambda x: x*10 + 20) 
return df.plot.line( 
x="n_samples", y=acq_name, style='.-', figsize=(8,5), ax=ax 
    )

然后,我们可以使用这个函数绘制两个获取函数的结果:


ax = plot("bc1adec5-bc34-44a6-a0eb-fa7cb67854e4", "random") 
ax = plot( 
"5c8d6001-a5fb-45d3-a7cb-2a8a46b93d18", "knowledge_uncertainty", ax=ax 
) 
plt.xticks(np.arange(0, 1050, 50)) 
plt.yticks(np.arange(54, 102, 2)) 
plt.ylabel("Accuracy") 
plt.xlabel("Number of acquired samples") 
plt.show()

这将产生以下输出:

图片

图 8.8:主动学习结果

8.8 显示,通过知识不确定性获取样本开始显著提高模型的准确性,尤其是在大约获取了 300 个样本之后。该模型的最终准确率比随机样本训练的模型高出大约两个百分点。虽然这看起来不多,但我们也可以从另一个角度来分析数据:为了实现特定的准确率,需要多少样本?如果我们检查图表,可以看到,知识不确定性线在 400 个训练样本下达到了 96%的准确率。而随机样本训练的模型则至少需要 750 个样本才能达到相同的准确率。这意味着,在相同准确率下,知识不确定性方法只需要几乎一半的数据量。这表明,采用正确的获取函数进行主动学习非常有用,特别是在计算资源充足但标注成本昂贵的情况下:通过正确选择样本,我们可能能够将标注成本降低一倍,从而实现相同的准确率。

因为我们保存了每次迭代获取的样本,所以我们也可以检查两种模型选择的图像类型。为了使我们的可视化更易于解释,我们将可视化每种方法对于每个标签所选的最后五个图像。为此,我们首先定义一个函数,返回每个标签的图像集,对于一组模型目录:


def get_imgs_per_label(model_dirs) -*>* Dict[int, np.ndarray]: 
imgs_per_label = {i: [] for i in range(10)} 
for model_dir in model_dirs: 
df = pd.read_parquet(model_dir / "images_added.parquet") 
df.image = df.image.apply( 
lambda x: x.reshape(28, 28).astype(np.uint8) 
) 
for label in df.label.unique(): 
dff = df[df.label == label] 
if len(dff) == 0: 
continue 
imgs_per_label[label].append(np.hstack(dff.image)) 
    return imgs_per_label

然后,我们定义一个函数,创建一个PIL 图像,其中按标签将图像进行拼接,以便用于特定的获取函数:


from PIL import Image 
from pathlib import Path 

def get_added_images( 
acquisition: str, uuid: str, n_iter: int = 5 
) -*>* Image: 
base_dir = Path("./models") / acquisition / uuid 
model_dirs = filter(lambda x: x.is_dir(), base_dir.iterdir()) 
model_dirs = sorted(model_dirs, key=lambda x: int(x.stem)) 
imgs_per_label = get_imgs_per_label(model_dirs) 
imgs = [] 
for i in range(10): 
label_img = np.hstack(imgs_per_label[i])[:, -(28 * n_iter):] 
imgs.append(label_img) 
    return Image.fromarray(np.vstack(imgs))

然后,我们可以调用这些函数,在我们的案例中使用以下设置和UUID


uuid = "bc1adec5-bc34-44a6-a0eb-fa7cb67854e4" 
img_random = get_added_images("random", uuid) 
uuid = "5c8d6001-a5fb-45d3-a7cb-2a8a46b93d18" 
img_ku = get_added_images("knowledge_uncertainty", uuid)

让我们比较一下输出。

图片图片

图 8.9:随机选择的图像(左)与通过知识不确定性和 MC 丢弃法选择的图像(右)。每一行显示每个标签所选的最后五个图像

我们可以在 8.9中看到,通过知识不确定性获取函数选取的图像相比随机选择的图像可能更难以分类。这个不确定性获取函数选择了数据集中一些不寻常的数字表示。由于我们的获取函数能够选取这些图像,模型能够更好地理解数据集的整体分布,从而随着时间的推移提高了准确率。

8.5 使用不确定性估计实现更智能的强化学习

强化学习旨在开发能够从环境中学习的机器学习技术。强化学习背后的基本原则在其名称中有一丝线索:目标是加强成功的行为。一般来说,在强化学习中,我们有一个智能体能够在环境中执行一系列的动作。在这些动作之后,智能体从环境中获得反馈,而这些反馈被用来帮助智能体更好地理解哪些动作更可能导致在当前环境状态下获得积极的结果。

从形式上讲,我们可以使用一组状态 S、一组动作 A 来描述它们如何从当前状态 s 转换到新的状态 s^′,以及奖励函数 R(s,s^′),描述当前状态 s 和新状态 s^′ 之间的过渡奖励。状态集由环境状态集 S[e] 和智能体状态集 S[a] 组成,两者共同描述整个系统的状态。

我们可以将此类比为一场马可·波罗的游戏,其中一个玩家通过“喊叫”与“回应”的方式来找到另一个玩家。当寻找的玩家喊“Marco”时,另一个玩家回应“Polo”,根据声音的方向和幅度给出寻找者其位置的估计。如果我们将此简化为考虑距离,那么较近的状态是距离减少的状态,例如δ = dd^′ > 0,其中 d 是状态 s 的距离,d^′ 是状态 s^′ 的距离。相反,较远的状态是δ = dd^′ < 0。因此,在这个例子中,我们可以使用我们的δ值作为模型的反馈,使得我们的奖励函数为δ = R(s,s^′) = dd^′。

PIC

图 8.10:马可·波罗强化学习场景的插图

让我们把智能体视为寻找玩家,把目标视为隐藏玩家。在每一步,智能体会收集更多关于环境的信息,从而更好地建模其行动 A(s) 和奖励函数 R(s,s^′) 之间的关系(换句话说,它在学习需要朝哪个方向移动,以便更接近目标)。在每一步,我们需要预测奖励函数,给定当前状态下的可能行动集 A[s],以便选择最有可能最大化该奖励函数的行动。在这种情况下,行动集可以是我们可以移动的方向集,例如:前进、后退、左转和右转。

传统的强化学习使用一种叫做Q 学习的方法来学习状态、行动和奖励之间的关系。Q 学习不涉及神经网络模型,而是将状态、行动和奖励信息存储在一个表格中——Q 表格——然后用来确定在当前状态下最有可能产生最高奖励的行动。虽然 Q 学习非常强大,但对于大量的状态和行动,它的计算成本变得不可承受。为了解决这个问题,研究人员引入了深度 Q 学习的概念,其中 Q 表格被神经网络所替代。在通常经过大量迭代后,神经网络会学习在给定当前状态的情况下,哪些行动更有可能产生更高的奖励。

为了预测哪种行动可能产生最高的奖励值,我们使用一个经过训练的模型,该模型基于所有历史行动 A[h]、状态 S[h] 和奖励 R[h]。我们的训练输入 X 包含行动 A[h] 和状态 S[h],而目标输出 y 包含奖励值 R[h]。然后,我们可以将该模型作为模型预测控制器MPC)的一部分,选择行动,依据是哪个行动与最高预测奖励相关:

anext = argmax yi∀ai ∈ As

这里,y[i] 是我们的模型产生的奖励预测,f(a[i],s),它将当前状态 s 和可能的动作 a[i] ∈ A[s] 映射到奖励值。然而,在我们的模型有任何用处之前,我们需要收集数据进行训练。我们将在多个回合中积累数据,每个回合包括代理采取的一系列动作,直到满足某些终止标准。理想的终止标准是代理找到目标,但我们也可以设置其他标准,例如代理遇到障碍物或代理用尽最大动作数。由于模型开始时没有任何信息,我们使用一种在强化学习中常见的贪婪策略,叫做 𝜖greedy 策略,允许代理通过从环境中随机采样开始。这里的想法是,我们的代理以 𝜖 的概率执行随机动作,否则使用模型预测来选择动作。在每个回合之后,我们会减少 𝜖,使得代理最终仅根据模型来选择动作。让我们构建一个简单的强化学习示例,看看这一切是如何运作的。

第一步:初始化我们的环境

我们的强化学习示例将围绕我们的环境展开:这定义了所有事件发生的空间。我们将使用Environment类来处理这个问题。首先,我们设置环境参数:


import numpy as np 
import tensorflow as tf 
from scipy.spatial.distance import euclidean 
from tensorflow.keras import ( 
Model, 
Sequential, 
layers, 
optimizers, 
metrics, 
losses, 
) 
import pandas as pd 
from sklearn.preprocessing import StandardScaler 
import copy 

class Environment: 
def __init__(self, env_size=8, max_steps=2000): 
self.env_size = env_size 
self.max_steps = max_steps 
self.agent_location = np.zeros(2) 
self.target_location = np.random.randint(0, self.env_size, 2) 
self.action_space = { 
0: np.array([0, 1]), 
1: np.array([0, -1]), 
2: np.array([1, 0]), 
3: np.array([-1, 0]), 
} 
self.delta = self.compute_distance() 
self.is_done = False 
self.total_steps = 0 
self.ideal_steps = self.calculate_ideal_steps() 
    ...

在这里,注意我们的环境大小,用env_size表示,它定义了环境中的行数和列数——在这个例子中,我们将使用 8 × 8 的环境,结果是 64 个位置(为了简便,我们将使用一个方形环境)。我们还将设置一个max_steps限制,以确保在代理随机选择动作时,回合不会进行得太长。

我们还设置了agent_locationtarget_location变量——代理总是从 [0, 0] 点开始,而目标位置则是随机分配的。

接下来,我们创建一个字典,将整数值映射到一个动作。从 0 到 3,这些动作分别是:向前、向后、向右、向左。我们还设置了delta变量——这是代理与目标之间的初始距离(稍后我们将看到compute_distance()是如何实现的)。

最后,我们初始化一些变量,用于跟踪终止标准是否已满足(is_done)、总步骤数(total_steps)和理想步骤数(ideal_steps)。后者是代理从起始位置到达目标所需的最小步骤数。我们将用它来计算遗憾,这是强化学习和优化算法中一个有用的性能指标。为了计算遗憾,我们将向我们的类中添加以下两个函数:


... 

def calculate_ideal_action(self, agent_location, target_location): 
min_delta = 1e1000 
ideal_action = -1 
for k in self.action_space.keys(): 
delta = euclidean( 
agent_location + self.action_space[k], target_location 
) 
if delta *<*= min_delta: 
min_delta = delta 
ideal_action = k 
return ideal_action, min_delta 

def calculate_ideal_steps(self): 
agent_location = copy.deepcopy(self.agent_location) 
target_location = copy.deepcopy(self.target_location) 
delta = 1e1000 
i = 0 
while delta *>* 0: 
ideal_action, delta = self.calculate_ideal_action( 
agent_location, target_location 
) 
agent_location += self.action_space[ideal_action] 
i += 1 
return i 
    ...

在这里,calculate_ideal_steps()将一直运行,直到代理与目标之间的距离(delta)为零。在每次迭代中,它使用calculate_ideal_action()来选择能使代理尽可能接近目标的动作。

第二步:更新我们环境的状态

现在我们已经初始化了我们的环境,我们需要添加我们类中最关键的一个部分:update方法。这控制了当代理采取新动作时环境的变化:


... 
def update(self, action_int): 
self.agent_location = ( 
self.agent_location + self.action_space[action_int] 
) 
# prevent the agent from moving outside the bounds of the environment 
self.agent_location[self.agent_location *>* (self.env_size - 1)] = ( 
self.env_size - 1 
) 
self.compute_reward() 
self.total_steps += 1 
self.is_done = (self.delta == 0) or (self.total_steps *>*= self.max_steps) 
return self.reward 
    ...

该方法接收一个动作整数,并使用它来访问我们之前定义的action_space字典中对应的动作。然后更新代理位置。因为代理位置和动作都是向量,所以我们可以简单地使用向量加法来完成这一点。接下来,我们检查代理是否移出了环境的边界 – 如果是,则调整其位置使其仍然保持在我们的环境边界内。

下一行是另一个关键的代码片段:使用compute_reward()计算奖励 – 我们马上就会看到这个。一旦计算出奖励,我们增加total_steps计数器,检查终止条件,并返回动作的奖励值。

我们使用以下函数来确定奖励。如果代理与目标之间的距离增加,则返回低奖励(1),如果减少,则返回高奖励(10):


... 
def compute_reward(self): 
d1 = self.delta 
self.delta = self.compute_distance() 
if self.delta *<* d1: 
self.reward = 10 
else: 
self.reward = 1 
    ...

这里使用了compute_distance()函数,计算代理与目标之间的欧氏距离:


... 
def compute_distance(self): 
return euclidean(self.agent_location, self.target_location) 
    ...

最后,我们需要一个函数来允许我们获取环境的状态,以便将其与奖励值关联起来。我们将其定义如下:


... 
def get_state(self): 
return np.concatenate([self.agent_location, self.target_location]) 
    ...

第三步:定义我们的模型

现在我们已经设置好了环境,我们将创建一个模型类。这个类将处理模型训练和推断,以及根据模型预测选择最佳动作。和往常一样,我们从__init__()方法开始:


class RLModel: 
def __init__(self, state_size, n_actions, num_epochs=500): 
self.state_size = state_size 
self.n_actions = n_actions 
self.num_epochs = 200 
self.model = Sequential() 
self.model.add( 
layers.Dense( 
20, input_dim=self.state_size, activation="relu", name="layer_1" 
) 
) 
self.model.add(layers.Dense(8, activation="relu", name="layer_2")) 
self.model.add(layers.Dense(1, activation="relu", name="layer_3")) 
self.model.compile( 
optimizer=optimizers.Adam(), 
loss=losses.Huber(), 
metrics=[metrics.RootMeanSquaredError()], 
) 
    ...

在这里,我们传递了一些与我们的环境相关的变量,如状态大小和动作数量。与模型定义相关的代码应该很熟悉 – 我们只是使用 Keras 实例化了一个神经网络。需要注意的一点是,我们在这里使用 Huber 损失,而不是更常见的均方误差。这是在强化学习和健壮回归任务中常见的选择。Huber 损失动态地在均方误差和平均绝对误差之间切换。前者非常擅长惩罚小误差,而后者对异常值更为健壮。通过 Huber 损失,我们得到了一个既对异常值健壮又惩罚小误差的损失函数。

这在强化学习中特别重要,因为算法具有探索性特征:我们经常会遇到一些极具探索性的样本,它们与其他数据相比偏离较大,从而在训练过程中导致较大的误差。

在完成类的初始化后,我们继续处理 fit()predict() 函数:


... 
def fit(self, X_train, y_train, batch_size=16): 
self.scaler = StandardScaler() 
X_train = self.scaler.fit_transform(X_train) 
self.model.fit( 
X_train, 
y_train, 
epochs=self.num_epochs, 
verbose=0, 
batch_size=batch_size, 
) 

def predict(self, state): 
rewards = [] 
X = np.zeros((self.n_actions, self.state_size)) 
for i in range(self.n_actions): 
X[i] = np.concatenate([state, [i]]) 
X = self.scaler.transform(X) 
rewards = self.model.predict(X) 
        return np.argmax(rewards)

fit() 函数应该非常熟悉——我们只是对输入进行缩放,然后再拟合我们的 Keras 模型。predict() 函数则稍微复杂一点。因为我们需要对每个可能的动作(前进、后退、右转、左转)进行预测,所以我们需要为这些动作生成输入。我们通过将与动作相关的整数值与状态进行拼接,来生成完整的状态-动作向量,正如第 11 行所示。对所有动作执行此操作后,我们得到输入矩阵 X,其中每一行都对应一个特定的动作。然后,我们对 X 进行缩放,并在其上运行推理,以获得预测的奖励值。为了选择一个动作,我们简单地使用 np.argmax() 来获取与最高预测奖励相关的索引。

第 4 步:运行我们的强化学习

现在,我们已经定义了 EnvironmentRLModel 类,准备开始强化学习了!首先,我们设置一些重要的变量并实例化我们的模型:


env_size = 8 
state_size = 5 
n_actions = 4 
epsilon = 1.0 
history = {"state": [], "reward": []} 
n_samples = 1000 
max_steps = 500 
regrets = [] 

model = RLModel(state_size, n_actions)

这些内容现在应该已经很熟悉了,但我们还是会再回顾一些尚未覆盖的部分。history 字典是我们存储状态和奖励信息的地方,在每一轮的每个步骤中,我们会更新这些信息。然后,我们会利用这些信息来训练我们的模型。另一个不太熟悉的变量是 n_samples——我们设置这个变量是因为每次训练模型时,并不是使用所有可用的数据,而是从数据中随机抽取 1,000 个数据点。这样可以避免随着数据量的不断增加,我们的训练时间也不断暴增。这里的最后一个新变量是 regrets。这个列表将存储每一轮的遗憾值。在我们的案例中,遗憾被简单地定义为模型所采取的步骤数与智能体到达目标所需的最小步骤数之间的差值:

regret = steps − steps model ideal

因此,遗憾为零 steps[model] == steps[ideal]。遗憾值对于衡量模型学习过程中的表现非常有用,正如我们稍后将看到的那样。接下来就是强化学习过程的主循环:


for i in range(100): 
env = Environment(env_size, max_steps=max_steps) 
while not env.is_done: 
state = env.get_state() 
if np.random.rand() *<* epsilon: 
action = np.random.randint(n_actions) 
else: 
action = model.predict(state) 
reward = env.update(action) 
history["state"].append(np.concatenate([state, [action]])) 
history["reward"].append(reward) 
print( 
f"Completed episode {i} in {env.total_steps} steps." 
f"Ideal steps: {env.ideal_steps}." 
f"Epsilon: {epsilon}" 
) 
regrets.append(np.abs(env.total_steps-env.ideal_steps)) 
idxs = np.random.choice(len(history["state"]), n_samples) 
model.fit( 
np.array(history["state"])[idxs], 
np.array(history["reward"])[idxs] 
) 
    epsilon-=epsilon/10

在这里,我们的强化学习过程会运行 100 轮,每次都重新初始化环境。通过内部的 while 循环可以看到,我们会不断地迭代——更新智能体并衡量奖励——直到满足其中一个终止条件(无论是智能体达到目标,还是我们达到最大允许的迭代次数)。

每一轮结束后,print语句会告诉我们该轮是否没有错误完成,并告诉我们我们的智能体与理想步数的对比结果。接着,我们计算遗憾值,并将其附加到regrets列表中,从history中的数据进行采样,并在这些样本数据上拟合我们的模型。最后,每次外循环迭代结束时,我们会减少 epsilon 值。

运行完之后,我们还可以绘制遗憾值图,以查看我们的表现:


import matplotlib.pyplot as plt 
import seaborn as sns 

df_plot = pd.DataFrame({"regret": regrets, "episode": np.arange(len(regrets))}) 
sns.lineplot(x="episode", y="regret", data=df_plot) 
fig = plt.gcf() 
fig.set_size_inches(5, 10) 
plt.show()

这将生成以下图表,展示我们模型在 100 轮训练中的表现:

PIC

图 8.11:强化学习 100 轮后的遗憾值图

正如我们在这里看到的,它一开始表现得很差,但模型很快学会了预测奖励值,从而能够预测最优动作,将遗憾减少到 0。

到目前为止,事情还算简单。事实上,你可能会想,为什么我们需要模型呢——为什么不直接计算目标和拟议位置之间的距离,然后选择相应的动作呢?首先,强化学习的目标是让智能体在没有任何先验知识的情况下发现如何在给定环境中进行交互——所以,尽管我们的智能体可以执行动作,但它没有距离的概念。这是通过与环境的互动来学习的。其次,情况可能没有那么简单:如果环境中有障碍物呢?在这种情况下,我们的智能体需要比简单地朝声音源移动更聪明。

尽管这只是一个示范性的例子,但强化学习在现实世界中的应用涉及一些我们知识非常有限的情境,因此,设计一个能够探索环境并学习如何最优互动的智能体,使我们能够为那些无法使用监督学习方法的应用开发模型。

另一个在现实世界情境中需要考虑的因素是风险:我们希望我们的智能体做出明智的决策,而不仅仅是最大化奖励的决策:我们需要它能够理解风险/回报的权衡。这就是不确定性估计的作用所在。

8.5.1 带有不确定性的障碍物导航

通过不确定性估计,我们可以在奖励和模型对其预测的信心之间找到平衡。如果模型的信心较低(意味着不确定性较高),那么我们可能希望对如何整合模型的预测保持谨慎。例如,假设我们刚刚探讨的强化学习场景。在每一轮中,我们的模型预测哪个动作将获得最高的奖励,然后我们的智能体选择该动作。在现实世界中,事情并不是那么可预测——我们的环境可能会发生变化,导致意外的后果。如果我们的环境中出现了障碍物,并且与障碍物发生碰撞会阻止我们的智能体完成任务,那么显然,如果我们的智能体还没有遇到过这个障碍物,它注定会失败。幸运的是,在贝叶斯深度学习的情况下,情况并非如此。只要我们有某种方式来感知障碍物,我们的智能体就能够检测到障碍物并选择不同的路径——即使该障碍物在之前的回合中没有出现。

PIC

图 8.12:不确定性如何影响强化学习智能体行动的示意图

这一切之所以可能,得益于我们的不确定性估计。当模型遇到不寻常的情况时,它对该预测的不确定性估计将会较高。因此,如果我们将其融入到我们的 MPC 方程中,我们就能在奖励和不确定性之间找到平衡,确保我们优先考虑较低的风险,而非较高的奖励。为了做到这一点,我们修改了我们的 MPC 方程,具体如下:

anext = argmax (yi − λσi)∀ai ∈ As

在这里,我们看到我们正在从我们的奖励预测 y[i] 中减去一个值,λσ[i]。这是因为 σ[i] 是与第 i 次预测相关的不确定性。我们使用 λ 来缩放不确定性,以便适当惩罚不确定的动作;这是一个可以根据应用进行调整的参数。通过一个经过良好校准的方法,我们将看到在模型对预测不确定时,σ[i] 的值会较大。让我们在之前的代码示例的基础上,看看这一过程如何实现。

第一步:引入障碍物

为了给我们的智能体制造挑战,我们将向环境中引入障碍物。为了测试智能体如何应对不熟悉的输入,我们将改变障碍物的策略——它将根据我们的环境设置,选择遵循静态策略或动态策略。我们将修改 Environment 类的 __init__() 函数,以便整合这些更改:


def __init__(self, env_size=8, max_steps=2000, dynamic_obstacle=False, lambda_val=2): 
self.env_size = env_size 
self.max_steps = max_steps 
self.agent_location = np.zeros(2) 
self.dynamic_obstacle = dynamic_obstacle 
self.lambda_val = lambda_val 
self.target_location = np.random.randint(0, self.env_size, 2) 
while euclidean(self.agent_location, self.target_location) *<* 4: 
self.target_location = np.random.randint(0, self.env_size, 2) 
self.action_space = { 
0: np.array([0, 1]), 
1: np.array([0, -1]), 
2: np.array([1, 0]), 
3: np.array([-1, 0]), 
} 
self.delta = self.compute_distance() 
self.is_done = False 
self.total_steps = 0 
self.obstacle_location = np.array( 
[self.env_size / 2, self.env_size / 2], dtype=int 
) 
self.ideal_steps = self.calculate_ideal_steps() 
self.collision = False 

这里涉及的内容比较复杂,所以我们将逐一讲解每个更改。首先,为了确定障碍物是静态的还是动态的,我们设置了 dynamic_obstacle 变量。如果该值为 True,我们将随机设置障碍物的位置。如果该值为 False,则我们的物体将停留在环境的中央。我们还在此设置了我们的 lambda (λ) 参数,默认值为 2。

我们还在设置 target_location 时引入了一个 while 循环:我们这么做是为了确保智能体和目标之间有一定的距离。我们需要这么做是为了确保在智能体和目标之间留有足够的空间,以便放置动态障碍物——否则,智能体可能永远无法遇到这个障碍物(这将稍微违背本示例的意义)。

最后,我们在第 17 行计算障碍物的位置:你会注意到这只是将它设置在环境的中央。这是因为我们稍后会使用 dynamic_obstacle 标志将障碍物放置在智能体和目标之间——我们在 calculate_ideal_steps() 函数中这么做,因为这样我们就知道障碍物将位于智能体的理想路径上(因此更有可能被遇到)。

步骤 2:放置动态障碍物

dynamic_obstacleTrue 时,我们希望在每个回合将障碍物放置在不同的位置,从而为我们的智能体带来更多挑战。为此,我们在之前提到的 calculate_ideal_steps() 函数中进行了一些修改:


def calculate_ideal_steps(self): 
agent_location = copy.deepcopy(self.agent_location) 
target_location = copy.deepcopy(self.target_location) 
delta = 1e1000 
i = 0 
while delta *>* 0: 
ideal_action, delta = self.calculate_ideal_action( 
agent_location, target_location 
) 
agent_location += self.action_space[ideal_action] 
if np.random.randint(0, 2) and self.dynamic_obstacle: 
self.obstacle_location = copy.deepcopy(agent_location) 
i += 1 
        return i

在这里,我们看到我们在每次执行 while 循环时都调用了 np.random.randint(0, 2)。这是为了随机化障碍物沿理想路径的放置位置。

步骤 3:添加感知功能

如果我们的智能体无法感知环境中引入的物体,那么它将没有任何希望避免这个物体。因此,我们将添加一个函数来模拟传感器:get_obstacle_proximity()。该传感器将为我们的智能体提供关于如果它执行某个特定动作时,它会接近物体的距离信息。根据给定动作将我们的智能体靠近物体的距离,我们将返回逐渐增大的数值。如果动作将智能体置于足够远的位置(在这种情况下,至少 4.5 个空间),则我们的传感器将返回零。这个感知功能使得我们的智能体能够有效地看到一步之遥,因此我们可以将该传感器视为具有一步的感知范围。


def get_obstacle_proximity(self): 
obstacle_action_dists = np.array( 
[ 
euclidean( 
self.agent_location + self.action_space[k], 
self.obstacle_location, 
) 
for k in self.action_space.keys() 
] 
) 
return self.lambda_val * ( 
np.array(obstacle_action_dists *<* 2.5, dtype=float) 
+ np.array(obstacle_action_dists *<* 3.5, dtype=float) 
+ np.array(obstacle_action_dists *<* 4.5, dtype=float) 
        )

在这里,我们首先计算每个动作后智能体的未来接近度,然后计算整数“接近度”值。这些值是通过首先构造每个接近度条件的布尔数组来计算的,在这种情况下分别为 δ[o] < 2.5, δ[o] < 3.5 和 δ[o] < 4.5,其中 δ[o] 是与障碍物的距离。然后,我们将这些条件求和,使得接近度得分具有 3、2 或 1 的整数值,具体取决于满足多少个条件。这为我们提供了一个传感器,它会根据每个提议的动作返回有关障碍物未来接近度的基本信息。

步骤 4:修改奖励函数

准备环境的最后一件事是更新我们的奖励函数:


def compute_reward(self): 
d1 = self.delta 
self.delta = self.compute_distance() 
if euclidean(self.agent_location, self.obstacle_location) == 0: 
self.reward = 0 
self.collision = True 
self.is_done = True 
elif self.delta *<* d1: 
self.reward = 10 
else: 
            self.reward = 1

在这里,我们添加了一条语句来检查代理与障碍物是否发生碰撞(检查两者之间的距离是否为零)。如果发生碰撞,我们将返回奖励值 0,并将collisionis_done变量设置为True。这引入了新的终止标准——碰撞,并将允许我们的代理学习到碰撞是有害的,因为这些情况会得到最低的奖励。

第 5 步:初始化我们的不确定性感知模型

现在我们的环境已经准备好,我们需要一个新的模型——一个能够生成不确定性估计的模型。对于这个模型,我们将使用一个带有单个隐藏层的 MC dropout 网络:


class RLModelDropout: 
def __init__(self, state_size, n_actions, num_epochs=200, nb_inference=10): 
self.state_size = state_size 
self.n_actions = n_actions 
self.num_epochs = num_epochs 
self.nb_inference = nb_inference 
self.model = Sequential() 
self.model.add( 
layers.Dense( 
10, input_dim=self.state_size, activation="relu", name="layer_1" 
) 
) 
# self.model.add(layers.Dropout(0.15)) 
# self.model.add(layers.Dense(8, activation='relu', name='layer_2')) 
self.model.add(layers.Dropout(0.15)) 
self.model.add(layers.Dense(1, activation="relu", name="layer_2")) 
self.model.compile( 
optimizer=optimizers.Adam(), 
loss=losses.Huber(), 
metrics=[metrics.RootMeanSquaredError()], 
) 

self.proximity_dict = {"proximity sensor value": [], "uncertainty": []} 
    ...

这看起来应该很熟悉,但你会注意到几个关键的不同之处。首先,我们再次使用 Huber 损失函数。其次,我们引入了一个字典proximity_dict,它将记录从传感器接收到的邻近值和相关的模型不确定性。这将使我们能够稍后评估模型对异常邻近值的敏感性。

第 6 步:拟合我们的 MC Dropout 网络

接下来,我们需要以下几行代码:


... 
def fit(self, X_train, y_train, batch_size=16): 
self.scaler = StandardScaler() 
X_train = self.scaler.fit_transform(X_train) 
self.model.fit( 
X_train, 
y_train, 
epochs=self.num_epochs, 
verbose=0, 
batch_size=batch_size, 
) 
    ...

这应该再次看起来很熟悉——我们只是通过首先对输入进行缩放来准备数据,然后拟合我们的模型。

第 7 步:进行预测

在这里,我们看到我们稍微修改了predict()函数:


... 
def predict(self, state, obstacle_proximity, dynamic_obstacle=False): 
rewards = [] 
X = np.zeros((self.n_actions, self.state_size)) 
for i in range(self.n_actions): 
X[i] = np.concatenate([state, [i], [obstacle_proximity[i]]]) 
X = self.scaler.transform(X) 
rewards, y_std = self.predict_ll_dropout(X) 
# we subtract our standard deviations from our predicted reward values, 
# this way uncertain predictions are penalised 
rewards = rewards - (y_std * 2) 
best_action = np.argmax(rewards) 
if dynamic_obstacle: 
self.proximity_dict["proximity sensor value"].append( 
obstacle_proximity[best_action] 
) 
self.proximity_dict["uncertainty"].append(y_std[best_action][0]) 
return best_action 
    ...

更具体地说,我们添加了obstacle_proximitydynamic_obstacle变量。前者允许我们接收传感器信息,并将其纳入传递给模型的输入中。后者是一个标志,告诉我们是否进入了动态障碍物阶段——如果是,我们希望在proximity_dict字典中记录传感器值和不确定性的相关信息。

下一段预测代码应该再次看起来很熟悉:


... 
def predict_ll_dropout(self, X): 
ll_divd = [ 
self.model(X, training=True) for _ in range(self.nb_inference) 
] 
ll_divd = np.stack(ll_divd) 
        return ll_divd.mean(axis=0), ll_divd.std(axis=0)

该函数简单地实现了 MC dropout 推断,通过nb_inference次前向传递获得预测,并返回与我们的预测分布相关的均值和标准差。

第 8 步:调整我们的标准模型

为了理解我们的贝叶斯模型带来的差异,我们需要将其与非贝叶斯模型进行比较。因此,我们将更新之前的RLModel类,添加从邻近传感器获取邻近信息的功能:


class RLModel: 
def __init__(self, state_size, n_actions, num_epochs=500): 
self.state_size = state_size 
self.n_actions = n_actions 
self.num_epochs = 200 
self.model = Sequential() 
self.model.add( 
layers.Dense( 
20, input_dim=self.state_size, activation="relu", name="layer_1" 
) 
) 
self.model.add(layers.Dense(8, activation="relu", name="layer_2")) 
self.model.add(layers.Dense(1, activation="relu", name="layer_3")) 
self.model.compile( 
optimizer=optimizers.Adam(), 
loss=losses.Huber(), 
metrics=[metrics.RootMeanSquaredError()], 
) 

def fit(self, X_train, y_train, batch_size=16): 
self.scaler = StandardScaler() 
X_train = self.scaler.fit_transform(X_train) 
self.model.fit( 
X_train, 
y_train, 
epochs=self.num_epochs, 
verbose=0, 
batch_size=batch_size, 
) 

def predict(self, state, obstacle_proximity, obstacle=False): 
rewards = [] 
X = np.zeros((self.n_actions, self.state_size)) 
for i in range(self.n_actions): 
X[i] = np.concatenate([state, [i], [obstacle_proximity[i]]]) 
X = self.scaler.transform(X) 
rewards = self.model.predict(X) 
return np.argmax(rewards) 

至关重要的是,我们在这里看到我们的决策函数并没有变化:因为我们没有模型不确定性,我们的模型的predict()函数仅基于预测的奖励来选择动作。

第 9 步:准备运行我们的新强化学习实验

现在我们准备好设置我们的新实验了。我们将初始化之前使用的变量,并引入几个新的变量:


env_size = 8 
state_size = 6 
n_actions = 4 
epsilon = 1.0 
history = {"state": [], "reward": []} 
model = RLModelDropout(state_size, n_actions, num_epochs=400) 
n_samples = 1000 
max_steps = 500 
regrets = [] 
collisions = 0 
failed = 0

在这里,我们看到我们引入了一个collisions变量和一个failed变量。这些变量将追踪碰撞次数和失败的回合次数,以便我们可以将贝叶斯模型的表现与非贝叶斯模型的表现进行比较。现在我们准备好运行实验了!

第 10 步:运行我们的 BDL 强化学习实验

如前所述,我们将对实验进行 100 回合的运行。然而,这次,我们只会在前 50 回合进行模型训练。之后,我们将停止训练,评估模型在找到安全路径到达目标方面的表现。在这最后 50 回合中,我们将dynamic_obstacle设置为True,意味着我们的环境将为每一回合随机选择一个新的障碍物位置。重要的是,这些随机位置将会位于代理与目标之间的理想路径上。

让我们来看一下代码:


for i in range(100): 
if i *<* 50: 
env = Environment(env_size, max_steps=max_steps) 
dynamic_obstacle = False 
else: 
dynamic_obstacle = True 
epsilon = 0 
env = Environment( 
env_size, max_steps=max_steps, dynamic_obstacle=True 
) 
    ...

首先,我们检查回合是否在前 50 回合之内。如果是,我们通过设置dynamic_obstacle=False实例化环境,并将全局变量dynamic_obstacle设置为False

如果回合是最后 50 回合之一,我们创建一个带有随机障碍物的环境,并将epsilon设置为 0,以确保我们在选择动作时总是使用模型预测。

接下来,我们进入while循环,使我们的代理开始移动。这与我们在上一个示例中看到的循环非常相似,只不过这次我们调用了env.get_obstacle_proximity(),并将返回的障碍物接近信息用于我们的预测,同时也将此信息存储在回合历史中:


... 
while not env.is_done: 
state = env.get_state() 
obstacle_proximity = env.get_obstacle_proximity() 
if np.random.rand() *<* epsilon: 
action = np.random.randint(n_actions) 
else: 
action = model.predict(state, obstacle_proximity, dynamic_obstacle) 
reward = env.update(action) 
history["state"].append( 
np.concatenate([state, [action], 
[obstacle_proximity[action]]]) 
) 
history["reward"].append(reward) 
    ...

最后,我们将记录一些已完成回合的信息,并将最新回合的结果打印到终端。我们更新failedcollisions变量,并打印回合是否成功完成,代理是否未能找到目标,或代理是否与障碍物发生碰撞:


if env.total_steps == max_steps: 
print(f"Failed to find target for episode {i}. Epsilon: {epsilon}") 
failed += 1 
elif env.total_steps *<* env.ideal_steps: 
print(f"Collided with obstacle during episode {i}. Epsilon: {epsilon}") 
collisions += 1 
else: 
print( 
f"Completed episode {i} in {env.total_steps} steps." 
f"Ideal steps: {env.ideal_steps}." 
f"Epsilon: {epsilon}" 
) 
regrets.append(np.abs(env.total_steps-env.ideal_steps)) 
if not dynamic_obstacle: 
idxs = np.random.choice(len(history["state"]), n_samples) 
model.fit( 
np.array(history["state"])[idxs], 
np.array(history["reward"])[idxs] 
) 
        epsilon-=epsilon/10

这里的最后一条语句还检查我们是否处于动态障碍物阶段,如果不是,则进行一次训练,并减少我们的 epsilon 值(如同上一个示例)。

那么,我们的表现如何?重复进行上述 100 回合的实验,对于RLModelRLModelDropout模型,我们得到了以下结果:

|


|


|


|


|

模型 失败的回合数 碰撞次数 成功的回合数

|


|


|


|


|

RLModelDropout 19 3 31

|


|


|


|


|

RLModel 16 10 34

|


|


|


|


|

图 8.13:一张显示碰撞预测的表格

如我们所见,在选择使用标准神经网络还是贝叶斯神经网络时,都有其优缺点——标准神经网络完成了更多的成功回合。然而,关键是,使用贝叶斯神经网络的代理仅与障碍物发生了 3 次碰撞,而标准方法发生了 10 次碰撞——这意味着碰撞减少了 70%!

请注意,由于实验是随机的,您的结果可能会有所不同,但在 GitHub 仓库中,我们已包括完整的实验以及用于生成这些结果的种子。

我们可以通过查看在RLModelDropoutproximity_dict字典中记录的数据,更好地理解为什么会这样:


import matplotlib.pyplot as plt 
import seaborn as sns 

df_plot = pd.DataFrame(model.proximity_dict) 
sns.boxplot(x="proximity sensor value", y="uncertainty", data=df_plot)

这将产生以下图表:

PIC

图 8.14:与增加的接近传感器值相关的不确定性估计分布

如我们所见,模型的不确定性估计随着传感器值的增加而增加。这是因为,在前 50 个回合中,我们的智能体学会了避开环境的中心(因为障碍物就在这里)——因此,它习惯了较低(或为零)的接近传感器值。这意味着较高的传感器值是异常的,因此能够被模型的不确定性估计所捕捉到。然后,我们的智能体通过使用不确定性感知 MPC 方程,成功地解决了这种不确定性。

在这个示例中,我们看到了如何将 BDL 应用于强化学习,以促进强化学习智能体更谨慎的行为。尽管这里的示例相对基础,但其含义却相当深远:想象一下将其应用于安全关键的应用场景。在这些环境中,如果满足更好的安全要求,我们往往愿意接受模型性能较差。因此,BDL 在安全强化学习领域中占有重要地位,能够开发出适用于安全关键场景的强化学习方法。

在下一节中,我们将看到如何使用 BDL 创建对抗性输入具有鲁棒性的模型,这是现实世界应用中的另一个关键考虑因素。

8.6 对抗性输入的易感性

第三章深度学习基础中,我们看到通过稍微扰动图像的输入像素,可以欺骗 CNN。原本清晰看起来像猫的图片,被高置信度地预测为狗。我们创建的对抗性攻击(FSGM)是许多对抗性攻击之一,BDL 可能提供一定的防护作用。让我们看看这在实践中是如何运作的。

第一步:模型训练

我们不是使用预训练模型,如在第三章深度学习基础中所做的那样,而是从零开始训练一个模型。我们使用与第三章深度学习基础中相同的训练和测试数据——有关如何加载数据集,请参见该章节。提醒一下,数据集是一个相对较小的猫狗数据集。我们首先定义我们的模型。我们使用类似 VGG 的架构,但在每个MaxPooling2D层之后加入了 dropout:


def conv_block(filters): 
return [ 
tf.keras.layers.Conv2D( 
filters, 
(3, 3), 
activation="relu", 
kernel_initializer="he_uniform", 
), 
tf.keras.layers.MaxPooling2D((2, 2)), 
tf.keras.layers.Dropout(0.5), 
] 

model = tf.keras.models.Sequential( 
[ 
tf.keras.layers.Conv2D( 
32, 
(3, 3), 
activation="relu", 
input_shape=(160, 160, 3), 
kernel_initializer="he_uniform", 
), 
tf.keras.layers.MaxPooling2D((2, 2)), 
tf.keras.layers.Dropout(0.2), 
*conv_block(64), 
*conv_block(128), 
*conv_block(256), 
*conv_block(128), 
tf.keras.layers.Conv2D( 
64, 
(3, 3), 
activation="relu", 
kernel_initializer="he_uniform", 
), 
tf.keras.layers.Flatten(), 
tf.keras.layers.Dense(64, activation="relu"), 
tf.keras.layers.Dropout(0.5), 
tf.keras.layers.Dense(2), 
] 
) 

然后,我们对数据进行归一化,并编译和训练模型:


train_dataset_divprocessed = train_dataset.map(lambda x, y: (x / 255., y)) 
val_dataset_divprocessed = validation_dataset.map(lambda x, y: (x / 255., y)) 

model.compile(optimizer=tf.keras.optimizers.Adam(learning_rate=0.001), 
loss=tf.keras.losses.CategoricalCrossentropy(from_logits=True), 
metrics=['accuracy']) 
model.fit( 
train_dataset_divprocessed, 
epochs=200, 
validation_data=val_dataset_divprocessed, 
)

这将使我们的模型准确率达到大约 85%。

第二步:运行推理并评估我们的标准模型

现在我们已经训练好了我们的模型,让我们看看它对抗对抗攻击的保护效果有多好。在第三章**深度学习基础中,我们从头开始创建了一个对抗攻击。在本章中,我们将使用cleverhans库来为多个图像一次性创建相同的攻击:


from cleverhans.tf2.attacks.fast_gradient_method import ( 
fast_gradient_method as fgsm, 
)

首先,让我们衡量我们确定性模型在原始图像和对抗图像上的准确率:


Predictions_standard, predictions_fgsm, labels = [], [], [] 
for imgs, labels_batch in test_dataset: 
imgs /= 255\. 
predictions_standard.extend(model.predict(imgs)) 
imgs_adv = fgsm(model, imgs, 0.01, np.inf) 
predictions_fgsm.extend(model.predict(imgs_adv)) 
  labels.extend(labels_batch)

现在我们有了我们的预测结果,我们可以打印出准确率:


accuracy_standard = CategoricalAccuracy()( 
labels, predictions_standard 
).numpy() 
accuracy_fgsm = CategoricalAccuracy()( 
labels, predictions_fgsm 
).numpy() 
print(f"{accuracy_standard=.2%}, {accuracy_fsgm=:.2%}") 
# accuracy_standard=83.67%, accuracy_fsgm=30.70%

我们可以看到,我们的标准模型对这种对抗攻击几乎没有提供任何保护。尽管它在标准图像上的表现相当不错,但它在对抗图像上的准确率仅为 30.70%!让我们看看一个贝叶斯模型能否做得更好。因为我们训练了带 dropout 的模型,我们可以很容易地将其转变为 MC dropout 模型。我们创建一个推理函数,在推理过程中保持 dropout,如training=True参数所示:


import numpy as np 

def mc_dropout(model, images, n_inference: int = 50): 
return np.swapaxes(np.stack([ 
model(images, training=True) for _ in range(n_inference) 
  ]), 0, 1)

有了这个函数,我们可以用 MC dropout 推理替代标准的循环。我们再次跟踪所有的预测,并对标准图像和对抗图像进行推理:


Predictions_standard_mc, predictions_fgsm_mc, labels = [], [], [] 
for imgs, labels_batch in test_dataset: 
imgs /= 255\. 
predictions_standard_mc.extend( 
mc_dropout(model, imgs, 50) 
) 
imgs_adv = fgsm(model, imgs, 0.01, np.inf) 
predictions_fgsm_mc.extend( 
mc_dropout(model, imgs_adv, 50) 
) 
  labels.extend(labels_batch)

我们可以再次打印出我们的准确率:


accuracy_standard_mc = CategoricalAccuracy()( 
labels, np.stack(predictions_standard_mc).mean(axis=1) 
).numpy() 
accuracy_fgsm_mc = CategoricalAccuracy()( 
labels, np.stack(predictions_fgsm_mc).mean(axis=1) 
).numpy() 
print(f"{accuracy_standard_mc=.2%}, {accuracy_fgsm_mc=:.2%}") 
# accuracy_standard_mc=86.60%, accuracy_fgsm_mc=80.75%

我们可以看到,简单的修改使得模型设置在面对对抗样本时更加稳健。准确率从约 30%提高到了 80%以上,接近于确定性模型在未扰动图像上的 83%的准确率。此外,我们还可以看到,MC dropout 也使得我们的标准图像准确率提高了几个百分点,从 83%提升到了 86%。几乎没有任何方法能够完美地对抗对抗样本,因此能够接近我们模型在标准图像上的准确率是一个伟大的成就。

因为我们的模型之前没有见过对抗图像,所以一个具有良好不确定性值的模型应该在对抗图像上相对于标准模型表现出更低的平均信心。让我们看看是否是这样。我们创建一个函数来计算我们确定性模型预测的平均 softmax 值,并为 MC dropout 预测创建一个类似的函数:


def get_mean_softmax_value(predictions) -*>* float: 
mean_softmax = tf.nn.softmax(predictions, axis=1) 
max_softmax = np.max(mean_softmax, axis=1) 
mean_max_softmax = max_softmax.mean() 
return mean_max_softmax 

def get_mean_softmax_value_mc(predictions) -*>* float: 
predictions_np = np.stack(predictions) 
predictions_np_mean = predictions_np.mean(axis=1) 
  return get_mean_softmax_value(predictions_np_mean)

然后,我们可以打印出两个模型的平均 softmax 分数:


mean_standard = get_mean_softmax_value(predictions_standard) 
mean_fgsm = get_mean_softmax_value(predictions_fgsm) 
mean_standard_mc = get_mean_softmax_value_mc(predictions_standard_mc) 
mean_fgsm_mc = get_mean_softmax_value_mc(predictions_fgsm_mc) 
print(f"{mean_standard=:.2%}, {mean_fgsm=:.2%}") 
print(f"{mean_standard_mc=:.2%}, {mean_fgsm_mc=:.2%}") 
# mean_standard=89.58%, mean_fgsm=89.91% 
# mean_standard_mc=89.48%, mean_fgsm_mc=85.25%

我们可以看到,与标准图像相比,我们的标准模型在对抗图像上的信心实际上稍微更高,尽管准确率显著下降。然而,我们的 MC dropout 模型在对抗图像上的信心低于标准图像。虽然信心的下降幅度不大,但我们很高兴看到,尽管准确率保持合理,模型在对抗图像上的平均信心下降了。

8.7 总结

在本章中,我们通过五个不同的案例研究展示了现代 BDL 的各种应用。每个案例研究都使用了代码示例,突出了 BDL 在应对应用机器学习实践中的各种常见问题时的特定优势。首先,我们看到如何使用 BDL 在分类任务中检测分布外图像。接着,我们探讨了 BDL 方法如何用于使模型更加鲁棒,以应对数据集偏移,这是生产环境中一个非常常见的问题。然后,我们学习了 BDL 如何帮助我们选择最有信息量的数据点,以训练和更新我们的机器学习模型。接着,我们转向强化学习,看到 BDL 如何帮助强化学习代理实现更加谨慎的行为。最后,我们看到了 BDL 在面对对抗性攻击时的应用。

在下一章中,我们将通过回顾当前趋势和最新方法来展望 BDL 的未来。

8.8 进一步阅读

以下阅读清单将帮助你更好地理解我们在本章中涉及的一些主题:

  • 基准测试神经网络对常见损坏和 扰动的鲁棒性,Dan Hendrycks 和 Thomas Dietterich,2019 年:这篇论文介绍了图像质量扰动,以基准测试模型的鲁棒性,正如我们在鲁棒性案例研究中看到的那样。

  • 你能信任模型的不确定性吗?评估数据集偏移下的 预测不确定性,Yaniv Ovadia、Emily Fertig 等,2019 年:这篇比较论文使用图像质量扰动,在不同的严重程度下引入人工数据集偏移,并衡量不同的深度神经网络在准确性和校准方面如何响应数据集偏移。

  • 用于检测神经网络中误分类和分布外 样本的基准,Dan Hendrycks 和 Kevin Gimpel,2016 年:这篇基础性的分布外检测论文介绍了该概念,并表明当涉及到分布外(OOD)检测时,softmax 值并不完美。

  • 提高神经网络中分布外图像检测的可靠性,Shiyu Liang、Yixuan Li 和 R. Srikant,2017 年:表明输入扰动和温度缩放可以改善用于分布外检测的 softmax 基准。

  • 用于检测分布外 样本和对抗性攻击的简单统一框架,Kimin Lee、Kibok Lee、Honglak Lee 和 Jinwoo Shin,2018 年:表明使用马哈拉诺比斯距离在分布外检测中可能是有效的。

第九章

贝叶斯深度学习的下一步

在本书中,我们已经介绍了贝叶斯深度学习(BDL)的基本概念,从理解什么是不确定性及其在开发稳健的机器学习系统中的作用,到学习如何实现和分析几个基本 BDL 的性能。虽然你所学到的内容足以帮助你开始开发自己的 BDL 解决方案,但该领域发展迅速,许多新技术正在涌现。

在本章中,我们将回顾 BDL 的当前趋势,然后深入探讨该领域的一些最新发展。最后,我们将介绍一些 BDL 的替代方法,并提供一些建议,介绍你可以使用的额外资源,帮助你继续深入贝叶斯机器学习方法的探索。

我们将涵盖以下章节:

  • 当前 BDL 的趋势

  • BDL 方法是如何应用于解决现实问题的?

  • BDL 的最新方法

  • BDL 的替代方法

  • 你在 BDL 中的下一步

9.1 当前 BDL 的趋势

在本节中,我们将探讨 BDL 的当前趋势。我们将查看哪些模型在文献中尤为流行,并讨论为何某些模型被选用于特定应用。这将为你提供一个关于本书中涵盖的基础知识如何更广泛应用于各种应用领域的良好印象。

PIC

图 9.1:BDL 关键搜索词的流行度随时间的变化

正如我们在 9.1 中看到的,过去十年与 BDL 相关的搜索词的流行度显著增加。毫不奇怪,这与深度学习相关搜索词的流行趋势一致,正如我们在 9.2中看到的;随着深度学习的普及,人们对量化 DNN 预测结果的不确定性的兴趣也随之增加。有趣的是,这些图表都显示了在 2021 年中至晚期流行度出现了类似的下降,表明只要深度学习受欢迎,BDL 也会受到关注。

PIC

图 9.2:关键深度学习搜索词的流行度随时间的变化

9.1 展示了另一个有趣的观点,一般而言,术语变分推断比我们在此使用的另外两个 BDL 相关的搜索词更受欢迎。如在第五章中提到的,贝叶斯深度学习的原则性方法中我们讨论了变分自编码器,变分推断是 BDL 的一部分,在机器学习社区产生了显著影响,现在已成为许多不同深度学习架构的一个特点。因此,它比明确包含“贝叶斯”一词的术语更受欢迎也就不足为奇了。

那么,我们在书中探讨的那些方法在它们的流行度和在各种深度学习解决方案中的应用情况如何呢?我们可以通过简单地查看每篇原始论文的引用次数来了解更多信息。

PIC

图 9.3:深度学习关键搜索词的流行度随时间变化

图 9.3中,我们看到 MC dropout 论文在引用次数上遥遥领先——几乎是第二种最受欢迎方法的两倍。到目前为止,书中已经相当清楚地说明了这个原因:它不仅是最容易实现的方法之一(正如我们在第六章使用标准工具箱进行贝叶斯深度学习中看到的),而且从计算角度来看,它也是最具吸引力的之一。它所需的内存与标准神经网络相同,而且正如我们在第七章中看到的,贝叶斯深度学习的实际考虑,它也是运行推理时最快的模型之一。这些实际因素在选择模型时通常比不确定性质量等因素更重要。

实际考虑因素很可能是第二种最流行方法——深度集成法的原因。虽然从训练时间来看,这可能不是最高效的方法,但通常推理的速度才是最重要的:再次回顾第七章贝叶斯深度学习的实际考虑的结果,我们看到尽管需要对多个不同的网络进行推理,集成法在这里表现优异。

深度集成法通常在实现的简便性和理论考虑之间取得良好的平衡:正如在第六章使用标准工具箱进行贝叶斯深度学习中讨论的那样,集成法是机器学习中的一个强大工具,因此,神经网络集成法表现良好并且通常能生成良好的不确定性估计也就不足为奇了。

排名第三和第四的最后两种方法是 BBB 和 PBP。虽然 BBB 比 PBP 更容易实现,但由于它需要一些概率组件,通常意味着——尽管在许多情况下它可能是最合适的工具——机器学习工程师可能不了解它,或者不习惯实现它。PBP 则更为极端:正如我们在第五章中所看到的,贝叶斯深度学习的原则性方法,实现 PBP 不是一项简单的任务。写作时,没有深度学习框架提供易用且经过优化的 PBP 实现——除了 BDL 社区外,许多机器学习研究人员和实践者根本不知道它的存在,这一点从它的引用次数较少可以看出(尽管它的引用数量仍然相当可观!)。

对贝叶斯深度学习方法流行度的分析似乎讲述了一个相当清晰的故事:BNN 方法的选择主要是基于实现的简便性。事实上,有大量文献讨论了使用 BDL 方法进行不确定性估计,而没有考虑模型不确定性估计的质量。幸运的是,随着不确定性感知方法的日益流行,这一趋势开始下降,我们希望本书已为你提供了必要的工具,使你在选择 BNN 方法时更加有原则。不论所使用的方法或其选择方式如何,很明显,机器学习研究人员和实践者对 BDL 方法越来越感兴趣——那么这些方法到底是用来做什么的呢?让我们来看看。

9.2 BDL 方法如何应用于解决实际问题?

就像深度学习正在对各种应用领域产生影响一样,BDL 正在成为越来越重要的工具,特别是在安全关键或任务关键系统中使用大量数据的情况下。在这些场景中——正如大多数实际应用所面临的情况——能够量化模型何时“知道自己不知道”对于开发可靠且稳健的系统至关重要。

BDL 的一个重要应用领域是安全关键系统。在他们 2019 年发表的论文《具有模型不确定性估计的安全强化学习》中,Björn Lütjens 等人展示了使用 BDL 方法可以在避撞场景中产生更安全的行为(这是我们在第八章中强化学习示例的灵感来源,应用贝叶斯深度学习)。

类似地,在论文《不确定性感知深度学习用于安全着陆点选择》中,作者 Katharine Skinner 等人探讨了如何利用贝叶斯神经网络进行行星表面着陆点的自主危险检测。这项技术对于促进自主着陆至关重要,最近深度神经网络(DNNs)在这一应用中展现了显著的能力。在他们的论文中,Skinner 等人展示了不确定性感知模型的使用能够改善安全着陆点的选择,甚至使得能够从大量噪声的传感器数据中选择安全的着陆点。这证明了贝叶斯深度学习(BDL)在提高深度学习方法的安全性鲁棒性方面的能力。

鉴于贝叶斯神经网络在安全关键场景中的日益流行,它们也被应用于医疗领域就不足为奇了。正如我们在第一章中提到的,深度学习时代的贝叶斯推理,深度学习在医学影像领域表现出了特别强的性能。然而,在这些关键应用中,不确定性量化至关重要:技术人员和诊断人员需要能够理解模型预测的误差范围。在论文《迈向安全深度学习:精确量化神经网络预测中的生物标志物不确定性》中,Zach Eaton-Rosen 等人应用了贝叶斯深度学习(BDL)方法来量化使用深度网络进行肿瘤体积估计时的生物标志物不确定性。他们的工作展示了贝叶斯神经网络可以用来设计具有良好标定误差条的深度学习系统。这些高质量的不确定性估计对于深度网络模型在临床中的安全使用至关重要,因此贝叶斯深度学习方法在诊断应用中显得尤为重要。

随着技术的进步,我们收集和组织数据的能力也在不断提升。这一趋势将许多“少数据”问题转变为“海量数据”问题——这并非坏事,因为更多的数据意味着我们能够更深入地了解生成数据的底层过程。一个例子就是地震监测:近年来,密集的地震监测网络显著增加。这从监测的角度来看是极好的:科学家现在比以往拥有更多的数据,因此能够更好地理解和监测地球物理过程。然而,为了做到这一点,他们还需要能够从大量高维数据中进行学习。

在他们的论文《贝叶斯深度学习与不确定性量化应用于荷兰格罗宁根天然气田的诱发地震位置:我们需要什么才能确保 AI 安全?》中,作者陈谷等人探讨了格罗宁根气藏的地震监测问题。正如他们在论文中提到的,虽然深度学习已被应用于许多地球物理问题,但使用不确定性感知的深度网络仍然罕见。他们的工作展示了贝叶斯神经网络可以成功地应用于地球物理问题,并且在格罗宁根气藏的案例中,从安全关键和任务关键两个角度来看,都可能至关重要。从安全角度来看,这些方法可以利用大量数据开发模型,推测地面运动活动,并用于地震预警系统。从任务关键的角度来看,相同的数据可以通过这些方法输入,以生成能够进行储层生产估算的模型。

在这两种情况下,若要将这些方法应用于任何实际系统,不确定性量化是关键,因为信任错误预测的后果可能是代价高昂甚至灾难性的。

这些例子让我们对 BDL 在现实世界中的应用有了一些了解。与之前的其他机器学习解决方案一样,随着这些方法在越来越多样化的应用场景中被使用,我们也越来越了解其潜在的不足之处。在下一节中,我们将了解一些该领域的最新进展,基于书中介绍的核心方法,开发出越来越强大的 BNN 近似。

9.3 BDL 中的最新方法

在本书中,我们介绍了一些 BDL 中使用的核心技术:反向传播贝叶斯(BBB)、概率反向传播(PBP)、蒙特卡洛 dropout(MC dropout)和深度集成方法。你在文献中遇到的许多 BNN 方法都会基于这些技术,而掌握这些技术为你提供了一套多功能的工具箱,可以帮助你开发自己的 BDL 解决方案。然而,正如机器学习的其他方面一样,BDL 领域正在迅速发展,新的技术也在不断涌现。在本节中,我们将探讨该领域的一些最新进展。

9.3.1 结合 MC dropout 和深度集成方法

为什么只使用一种贝叶斯神经网络技术,而不使用两种呢?爱丁堡大学的研究人员 Remus Pop 和 Patric Fulop 在他们的论文《深度集成贝叶斯主动学习:通过集成解决蒙特卡罗 Dropout 中的模式崩溃问题》中正是采用了这种方法。在这项工作中,Pop 和 Fulop 描述了使用主动学习使深度学习方法在标签数据耗时或昂贵的应用中变得可行的问题。这里的问题是,正如我们之前讨论过的,深度学习方法已经在一系列医学影像任务中证明了其巨大的成功。问题在于,这些数据需要经过仔细标注,并且为了使深度网络达到高水平的性能,它们需要大量这样的数据。

因此,机器学习研究者提出了主动学习方法,通过使用获取函数来自动评估新数据点并将其添加到数据集中,以确定何时将新数据添加到训练集中。模型的不确定性估计是关键的一环:它提供了新数据点与模型当前理解领域的关系的关键衡量标准。在他们的论文中,Pop 和 Fulop 展示了一个流行的深度贝叶斯主动学习(DBAL)方法的关键缺陷:即 MC dropout 模型中使用的过度自信。在他们的论文中,作者通过将深度集成和 MC dropout 结合在一个模型中来解决这个问题。他们证明,得到的模型具有更好的校准不确定性估计,从而纠正了 MC dropout 所表现出的过度自信预测。最终提出的方法,被称为深度 集成贝叶斯主动学习,为在数据获取困难或昂贵的应用中稳健地采用深度学习方法提供了一个框架——再次证明 BDL 在将深度网络应用于现实世界中的重要性。

图片

图 9.4:结合 MC dropout 和深度集成网络的示意图

将深度集成和 MC dropout 结合的这种方法也已应用于其他领域。例如,Lütjens 等人之前提到的碰撞避免论文也使用了结合 MC dropout 和深度集成网络的方法。这表明,选择一种网络而非另一种网络并不总是最优解——有时,结合不同方法是开发稳健且更好校准的 BDL 解决方案的关键。

9.3.2 通过促进多样性来改进深度集成

正如我们在本章前面看到的,按照引用次数来判断,深度集成是本书中介绍的关键 BDL 技术中第二受欢迎的技术。因此,研究人员一直在探索改进深度集成标准实现的方法,这也就不足为奇了。

在 Tim Pearce 人的论文《神经网络中的不确定性:近似贝叶斯集成》中,作者强调,标准的深度集成方法因其非贝叶斯性质而受到批评,并认为标准方法在许多情况下缺乏多样性,从而产生了描述性较差的后验分布。换句话说,深度集成通常由于缺乏多样性,导致过于自信的预测。

为了弥补这一点,作者提出了一种他们称之为锚定 集成的方法。锚定集成像深度集成一样,使用神经网络的集成。然而,它使用了一种特别适配的损失函数,惩罚集成成员的参数过度偏离其初始值。我们来看看:

Lossj = 1-||y − ˆy||2+ -1||Γ 12 × (𝜃j − 𝜃anc,j)||2 N 2 N 2

这里,Loss[j] 是计算出的第 j 个网络的损失。我们在方程中看到了一个熟悉的损失形式,即 ||yy||[2]²。Γ 是一个对角正则化矩阵,𝜃[j] 是网络的参数。这里的关键是 𝜃[j] 与 𝜃[anc,j] 变量之间的关系。这里,anc 表示该方法名称中的锚定。参数 𝜃[anc,j] 是第 j 个网络的初始参数集。因此(如通过乘法所示),如果该值很大——换句话说,如果 𝜃[j] 和 𝜃[anc,j] 相差很大——损失将增加。因此,如果网络的参数偏离其初始值太远,这会惩罚集成中的网络,迫使它们找到能够尽量保持接近初始值的参数,同时最小化方程中的第一项。

这一点非常重要,因为如果我们使用一种更可能产生多样化初始参数值的初始化策略,那么保持这种多样性将确保我们的集成在训练后包含多样化的网络。正如作者在论文中展示的那样,这种多样性是产生原则性不确定性估计的关键:确保网络预测在高数据区域收敛,而在低数据区域发散,就像我们在第二章贝叶斯推断基础的高斯过程示例中看到的那样。

PIC

图 9.5:使用高斯过程获得的原则性不确定性估计示意图

提醒一下,这里的实线是实际函数,点是函数的样本,虚线是高斯过程的均值预测,浅灰色虚线是可能函数的样本,阴影区域表示不确定性。

在他们的论文中,Pearce 人展示了他们的锚定集成方法能够比标准深度集成方法更接近地逼近这样的描述性后验分布。

9.3.3 超大网络中的不确定性

本书的核心目标是介绍在 DNN 中近似贝叶斯推断的方法,但我们尚未讨论如何将其应用于近年来最成功的神经网络架构之一:变换器。变换器—就像之前的典型深度网络一样—在多种任务中实现了突破性性能。尽管深度网络已经能够处理大量数据,变换器将其推向了一个新高度:处理巨量数据,拥有数百亿的参数。其中最著名的变换器网络之一是 GPT-3,这是由 OpenAI 开发的变换器,包含超过 1750 亿个参数。

变换器最初用于自然语言处理NLP)任务,并展示了通过使用自注意力机制和足够的数据量,可以在不使用递归神经网络的情况下实现竞争性性能。这是神经网络架构发展的一个重要步骤:展示了可以通过自注意力机制学习序列上下文,并提供能够从前所未有的数据量中学习的架构。

图片

图 9.6:变换器架构示意图

然而,就像之前更典型的深度网络一样,变换器的参数是点估计,而不是分布,因此无法用于不确定性量化。作者徐博扬等人在他们的论文《贝叶斯变换器语言模型用于语音识别》中试图解决这个问题。在他们的工作中,他们展示了变分推断可以成功地应用于变换器模型,从而促进了近似贝叶斯推断。然而,由于变换器的庞大规模,对所有参数进行贝叶斯参数估计是非常昂贵的。因此,徐博扬等人将贝叶斯估计应用于模型参数的一个子集,特别是前馈和多头自注意力模块中的参数。正如我们在 9.6中看到的,这排除了相当多的层,从而节省了计算周期。

论文《变换器可以进行贝叶斯推断》中,由 Samuel Müller等人提出的另一种方法,通过利用训练变换器时使用的大量数据来近似贝叶斯推断。在他们的方法中,称为先验数据拟合网络(PFNs),作者将后验近似问题重新表述为一个监督学习任务。也就是说,他们的方法不是通过采样获得预测分布,而是直接从数据集样本中学习近似后验预测分布。

算法 1:PFN 模型训练过程   输入: 数据集的先验分布 p(D),从中可以抽取样本,抽取的样本数为 K

输出: 一个模型 q𝜃 用于近似 PPD,初始化神经网络 q𝜃

for i:=1 到 10 do- 1:    采样 D ∪ (x[i],y[i])[i=1]^(m) ≈ p(D)

2: 计算随机损失近似 l[𝜃] = ∑ [i=1]^(m)(−log q**𝜃)

3: 使用随机梯度下降更新参数 𝜃 ,基于 ▿[𝜃]l[𝜃] =0

如此伪代码所示,在训练过程中,模型会采样多个数据子集,这些子集包含输入 x 和标签 y。接着,它会屏蔽掉一个标签,并学习基于其他数据点对该标签做出概率预测。这使得 PFN 能够在单次前向传递中进行概率推断——类似于我们在第五章贝叶斯深度学习的原则方法中看到的 PBP。尽管在单次前向传递中逼近贝叶斯推断对于任何应用来说都是理想的,但对于具有大量参数的 transformers 来说,这种方法更具价值——因此,这里描述的 PFN 方法尤其具有吸引力。

当然,transformers 在迁移学习中被广泛应用:使用 transformers 提取的丰富特征嵌入作为输入,送入计算要求较低的较小网络。因此,在贝叶斯背景下,使用 transformers 的嵌入作为 BDL 网络的输入,可能是最明显的使用方式——事实上,在许多情况下,这可能是最合理的第一步。

在本节中,我们探讨了贝叶斯深度学习(BDL)中的一些最新进展。所有这些都建立在本书中介绍的方法之上,并直接应用于这些方法。当你为近似贝叶斯推断开发自己的深度网络解决方案时,你可能想考虑实现这些进展。然而,鉴于机器学习研究的快速进展,贝叶斯近似的改进列表不断增长,我们鼓励你亲自探索文献,了解研究人员如何在大规模实现贝叶斯推断,并利用各种计算和理论优势。不过,BDL 并不总是正确的解决方案,在下一节中,我们将探讨其原因。

9.4 贝叶斯深度学习的替代方法

尽管本书的重点是使用深度神经网络(DNN)进行贝叶斯推断,但它们并不总是最合适的选择。一般而言,当你拥有大量高维数据时,深度网络是一个很好的选择。正如我们在第三章深度学习基础中所讨论的(以及你可能已经知道的),深度网络在这些场景中表现优异,因此将它们应用于贝叶斯推断是一个明智的选择。另一方面,如果你拥有的是少量低维数据(特征数十个,数据点少于 10,000),那么你可能更适合使用更传统、更有原则的贝叶斯推断方法,例如通过采样或高斯过程。

话虽如此,关于扩展高斯过程的研究一直备受关注,研究社区已经开发出能够处理大量数据并能进行复杂非线性变换的基于高斯过程的方法。在这一节中,我们将介绍这些替代方法,以便你如果有兴趣进一步探讨它们时,可以了解相关内容。

9.4.1 可扩展的高斯过程

在书的开头,我们介绍了高斯过程并讨论了为什么它们是机器学习中关于原则性和计算上可处理的不确定性量化的黄金标准。关键是,我们谈到了高斯过程的局限性:当数据维度高或数据量大时,它们变得计算上不可行。

然而,高斯过程(GP)是极其强大的工具,机器学习社区并没有准备放弃它们。在第二章贝叶斯推断基础中,我们讨论了高斯过程训练和推断中的关键障碍因素:反转协方差矩阵。虽然有一些方法可以使这一过程更具计算可行性(例如,Cholesky 分解),但这些方法也只能做到一定程度。因此,使高斯过程可扩展的关键方法被称为稀疏高斯过程,它们通过稀疏高斯过程近似法来修改协方差矩阵,从而解决了不可处理的高斯过程训练问题。简单来说,如果我们能缩小或简化协方差矩阵(例如,通过减少数据点数量),就能使协方差矩阵的反演变得可行,从而使高斯过程训练和推断变得可处理。

其中最流行的方法之一是在 Edward Snelson 和 Zoubin Ghahramani 的论文《使用伪输入的稀疏高斯过程》中提出的。与其他稀疏高斯过程方法一样,作者们开发了一种利用大数据集的可处理高斯过程方法。在论文中,作者们展示了他们如何通过使用数据的子集来接近全数据集的训练:他们通过将大数据问题转化为小数据问题,实际上绕过了大数据问题。然而,做到这一点需要选择一个合适的数据子集,作者们称之为 输入

作者通过联合优化过程实现这一目标,该过程从完整数据集N中选择出数据子集M,同时优化核函数的超参数。这个优化过程实质上是寻找可以最好地描述整体数据的数据子集:我们在图**9.7中展示了这一点。

PIC

图 9.7:伪输入的简单示意图

在这个图示中,所有的数据点都有展示,但我们看到某些数据点被选中,因为它们描述了我们变量之间的关键关系。然而,这些点不仅需要描述关系,就像多项式回归可能做的那样——它们还需要复制底层数据中的方差,使得高斯过程(GP)依然能够产生良好校准的不确定性估计。换句话说,虽然伪输入有效地减少了数据点的数量,但伪输入的分布仍需近似真实输入的分布:如果真实数据分布中的某个区域数据丰富,从而在该区域产生有信心的预测,那么伪输入也需要满足这一点。

最近,由 Ke Wang et al.在他们的论文《百万数据点上的精确高斯过程》中提出了另一种可扩展高斯过程的方法。在这项工作中,作者利用了多 GPU 并行化方法的最新进展来实现可扩展高斯过程。使这一方法得以实现的技术被称为黑盒矩阵-矩阵乘法BBMM),它将高斯过程推断问题简化为矩阵乘法的迭代过程。通过这样做,它使得该过程更容易进行并行化,因为矩阵乘法可以被划分并分配到多个 GPU 上。作者展示了这种方法将高斯过程训练的内存需求减少到每个 GPU 的O(n)。这使得高斯过程能够受益于深度学习方法在过去十多年里获得的计算优势!

这里呈现的两种方法都很好地解决了高斯过程(GP)面临的可扩展性问题。第二种方法尤其令人印象深刻,因为它实现了精确的高斯过程推断,但确实需要显著的计算基础设施。另一方面,伪输入方法在更大比例的使用场景中具有实用性。然而,这两种方法都未解决贝叶斯深度学习(BDL)的关键优势之一:深度网络通过复杂的非线性变换学习丰富的嵌入的能力。

9.4.2 深度高斯过程

深度高斯过程由 Andreas Damianou 和 Neil Lawrence 在他们标题直白的论文《深度高斯过程》中提出,深度高斯过程通过拥有多层高斯过程来解决丰富嵌入的问题,就像深度网络有多层神经元一样。与之前提到的可扩展高斯过程不同,深度高斯过程的提出正是受到可扩展性问题反向问题的启发:我们如何能用非常少的数据获得深度网络的性能?

面对这个问题,并且意识到高斯过程在少量数据上表现非常好,Damianou 和 Lawrence 着手研究是否可以将高斯过程分层,从而生成类似的丰富嵌入。

PIC

图 9.8:深度 GP 的示意图

它们的方法,尽管在实施上比较复杂,但原理很简单:正如一个深度神经网络(DNN)由许多层组成,每一层接收前一层的输入并将输出传递给后一层,深度高斯过程(GP)也假设有这种图形结构——正如我们在 9.8中看到的那样。从数学角度来看,就像深度网络一样,深度 GP 可以视为函数的组合。因此,之前展示的 GP 可以描述为:

y = g(x ) = f2(f1(x))

虽然这将我们在深度学习中习惯的丰富非线性变换引入了高斯过程中,但这也带来了代价。正如我们已经知道的那样,标准的高斯过程在可扩展性方面存在限制。不幸的是,深度 GP 通过这种方式组合是分析上无法解决的。因此,Damianou 和 Lawrence 必须找到一种可行的实现深度 GP 的方法,他们通过一种现在你应该已经熟悉的工具:变分近似,解决了这一问题。正如它在本书中介绍的一些 BDL 方法中构成了重要的构建块一样,它也是使深度 GP 成为可能的关键组成部分。在他们的论文中,他们展示了如何借助变分近似实现深度 GP——这不仅使得使用 GP 生成丰富的非线性嵌入成为可能,而且使得在少量 数据的情况下实现丰富的非线性嵌入成为可能。这使得深度 GP 成为贝叶斯方法工具箱中的一个重要工具,因此,它是一个值得记住的方法。

9.5 你在 BDL 中的下一步

在本章中,我们通过回顾一些可以帮助你改进书中探讨的基本方法的技术,完成了对 BDL 的介绍。我们还研究了如何将贝叶斯推理的强大标准——高斯过程——适应于一般用于深度学习的任务。虽然确实可以将高斯过程适应这些任务,但我们也建议,一般来说,使用本书中介绍的方法或从中衍生的方法会更容易且更实用。像往常一样,作为机器学习工程师,你需要自己判断什么方法最适合当前的任务,我们相信本书的内容将为你迎接未来的挑战提供充足的准备。

虽然本书为你提供了开始的必要基础,但在如此快速发展的领域中,总有更多的东西值得学习!在下一节中,我们将提供一些关键的最终建议,帮助你规划下一步的学习与应用 BDL 的过程。

我们希望您觉得这篇关于贝叶斯深度学习的介绍内容既全面、实用又有趣。感谢您的阅读——我们祝愿您在进一步探索这些方法并将其应用到您自己的机器学习解决方案中时取得成功。

9.6 进一步阅读

以下阅读推荐适合那些希望进一步了解本章所呈现的最新方法的读者。这些资源为当前领域中的挑战提供了极好的洞察,关注的不仅仅是贝叶斯神经网络,还包括更广泛的可扩展贝叶斯推断:

  • 深度集成贝叶斯主动学习,Pop 和 Fulop:本文展示了将深度集成与 MC dropout 结合的优势,能够提供更精确的预测不确定性估计,正如在应用该方法到主动学习任务时所展示的那样。

  • 神经网络中的不确定性:近似贝叶斯集成,Pearce 等人:本文介绍了一种简单且有效的方法,用于提高深度集成的性能。作者展示了通过对损失函数做简单调整来促进多样性,从而使得集成能够生成更为精确的不确定性估计。

  • 使用伪输入的稀疏高斯过程,Snelson 和 Gharamani:本文介绍了基于伪输入的高斯过程概念,提出了可扩展高斯过程推断中的一个关键方法。

  • 百万数据点上的精确高斯过程,Wang 等人:一篇重要论文,展示了通过使用 BBMM 技术,高斯过程可以受益于计算硬件的发展,使得大数据的精确高斯过程推断成为可能。

  • 深度高斯过程,Damianou 和 Lawrence:引入深度高斯过程(GPs)这一概念,本文展示了如何使用高斯过程实现复杂的非线性变换,且所需的数据集远小于深度学习所需的数据集。

我们挑选了一些关键资源,帮助您进入贝叶斯深度学习的下一阶段,让您深入理论并帮助您更好地利用本章内容:

  • 机器学习:一种概率视角,Murphy:该书于 2012 年发布,之后成为机器学习领域的核心教材之一,呈现了一种理解机器学习中所有关键方法的合理方法。该书的概率视角使其成为贝叶斯文献收藏中的一大亮点。

  • 概率机器学习:入门,Murphy:另一篇较新的 Murphy 作品。该书于 2022 年发布,提供了关于概率机器学习(包括贝叶斯神经网络部分)的详细内容。尽管与 Murphy 之前的作品有所重叠,但两者都值得拥有,且各有其独到之处。

  • 机器学习中的高斯过程,拉斯穆森和威廉姆斯:也许是关于高斯过程最重要的文本,这本书在贝叶斯推断中具有极高的价值。作者对高斯过程的详细解释将帮助你全面理解贝叶斯难题中的这一重要部分。

  • Python 中的贝叶斯分析,马丁:涵盖了贝叶斯分析的所有基础知识,这本书是一本出色的基础文献,将帮助你更深入地理解贝叶斯推断的基础。

posted @ 2025-07-12 11:41  绝不原创的飞龙  阅读(10)  评论(0)    收藏  举报