贝叶斯优化实战(全)

贝叶斯优化实战(全)

原文:zh.annas-archive.org/md5/5ea9246d4fedff957a1c887275e38f01

译者:飞龙

协议:CC BY-NC-SA 4.0

第一章:开头内容

前言

随着我们在机器学习和相关领域面临的问题复杂性不断增加,优化我们对资源的利用和有效地做出知情决策变得越来越重要。贝叶斯优化是一种强大的技术,用于找到昂贵评估的目标函数的最大值和最小值,已经成为解决这一挑战的非常有用的解决方案。其中一个原因是该函数可以被视为黑匣子,这使得研究人员和从业者可以用贝叶斯推断作为主要优化方法来处理非常复杂的函数。

由于其复杂性,贝叶斯优化对于初学者机器学习从业者而言比其他方法更难以掌握。然而,像贝叶斯优化这样的工具必须是任何想要取得最佳结果的机器学习从业者的工具包中的一部分。要精通这个主题,必须对微积分和概率有非常扎实的直觉。

这就是贝叶斯优化实战的用武之地。在这本书中,Quan 美妙而成功地揭开了这些复杂概念的神秘面纱。通过实践方法、清晰的图表、真实世界的例子和有用的代码示例,他从理论和实践两个角度剖析了这个主题的复杂性。

Quan 利用他作为数据科学家和教育工作者的丰富经验,给读者提供了这些技术的非常清晰的图景以及它们如何应用于解决实际问题。从贝叶斯推断的原理开始,本书逐渐建立了贝叶斯优化和高斯过程模型的概念。它教授了最先进的库,如 GPyTorch 和 BoTorch,并探讨了它们在几个领域的应用。

这本书是任何数据科学或机器学习从业者必读的书籍,他们想要利用贝叶斯优化真正解决实际问题的潜力。我强烈推荐给任何想通过贝叶斯推断掌握优化艺术的人。

—路易斯·塞拉诺,博士,人工智能科学家和推广者,

机器学习的领悟者的作者

工程师和科学家面临着一个共同的挑战,这是捕捉其研究和创造力价值的关键所在。他们需要优化。一个机器学习工程师找到使模型泛化的超参数。一群物理学家调整自由电子激光以获得最大脉冲能量。一个软件工程师配置 JVM 的垃圾收集器以最大化服务器的吞吐量。一个材料科学工程师选择使太阳能电池的光吸收最大化的微观结构形态。在每个例子中,都有不能基于第一原理做出的设计决策。它们取决于实验评估。

要通过实验评估某物,人们可能需要执行软件、运行硬件或构建新物体,同时测量其性能。要找到一个好的设计,就需要进行多次评估。这些评估需要时间、花费金钱,可能还会带来风险。因此,必须尽可能少地进行实验评估以找到最优设计。这就是贝叶斯优化的全部内容。

在过去的 20 年中,我在我的工作中使用了贝叶斯优化和相关的先驱方法。在这段时间里,学术研究和工业应用的报告改善了贝叶斯优化的性能和扩展了其适用性。现在已经存在高质量的软件工具和具有项目特定优化器的技术。

可以将目前的状态类比为使用线性模型进行预测的状态。一个想要构建线性模型的工程师将发现,像 sklearn 这样的软件工具使他们能够设计各种类型(例如连续或分类)和数量的输入和输出变量,执行自动变量选择,并测量一般化的质量。同样地,一个想要构建贝叶斯优化器的工程师将发现,构建在 GPyTorch、pyro 和 PyTorch 之上的 BoTorch 提供了优化不同变量类型、最大化多个目标、处理约束和更多问题的工具。

本书从最基本的组件——高斯过程回归和获取函数的数值优化——开始教授贝叶斯优化,直到最新的处理大量评估(也称为观测)和异类设计空间的方法。在这个过程中,它覆盖了你可能需要的所有特殊化工具:处理约束、多目标、并行化评估和通过成对比较进行评估。你将找到足够的技术深度来让你对工具和方法感到舒适,并且足够的真实代码可以让你快速地将这些工具和方法用于实际工作。

尽管贝叶斯优化取得了很多成就,但鲜有面向新手的文献。这本书很好地填补了这个空缺。

——David Sweet,叶史瓦大学兼职教授

《面向工程师的实验》作者,Cogneato.xyz

前言

2019 年秋季,我是一名大一博士生,不确定应该在研究中解决哪个问题。我知道我想要专注于人工智能(AI)——用计算机自动化思考过程很有吸引力——但 AI 是一个庞大的领域,我很难将我的研究范围缩小到一个具体的主题上。

当我参加了一门名为《机器学习的贝叶斯方法》的课程时,所有的不确定性都消失了。在此之前,我在本科阶段曾简要接触过贝叶斯定理,但正是在这门课的第一节讲座中,一切开始变得清晰起来!贝叶斯定理提供了一种直观的思考概率的方式,对我来说,这是一种人类信念的优雅模型:我们每个人都有一个先验信念(关于任何事情),我们从这个先验开始,当我们观察到支持或反对该先验的证据时,我们的信念会更新,结果是反映先验和数据的后验信念。贝叶斯定理将这种保持信念的优雅方式引入到人工智能中,并在许多问题上找到应用,这对我来说是一个强烈的信号,表明贝叶斯机器学习是一个值得追求的主题。

当我们到达关于贝叶斯优化(BayesOpt)的讲座时,我的决定已经做出:理论是直观的,应用是多样的,可以建立的可能性非常大。再次,我内心的某种东西(至今仍然如此)被自动化思维或更具体地说是决策吸引。BayesOpt 正是这个完美的吸引力。我加入了罗曼·加内特(Roman Garnett)教授教授的研究实验室,我的 BayesOpt 之旅开始了!

跳到 2021 年,我花了一些时间研究和实施 BayesOpt 解决方案,我对 BayesOpt 的欣赏只增加了。我会向朋友和同事推荐使用它来处理困难的优化问题,并承诺 BayesOpt 会表现良好。只有一个问题:我找不到一个好的资源可以指向。研究论文数学内容繁重,在线教程太短,无法提供实质性的见解,而 BayesOpt 软件的教程杂乱无章,缺乏良好的叙述。

然后,一个想法浮现在脑海中,以托尼·莫里森(Toni Morrison)的话说,“如果有一本你想读的书,但它还没有被写出来,那么你必须写它。”多么真实啊!这个前景让我兴奋,原因有两个:我可以写一本关于我心爱的事物的书,而写作无疑会帮助我获得更深层次的洞察。我拼凑了一个提案,并联系了曼宁,这是我最喜欢的书籍的出版商,风格正是我所设想的。

2021 年 11 月,我的收购编辑安迪·沃尔德龙(Andy Waldron)给我发了一封电子邮件,标志着曼宁(Manning)的第一次沟通。 2021 年 12 月,我签署了合同并开始写作,这比我最初想象的时间要长(我相信每本书都是如此)。 2023 年 4 月,在出版前的最后几个步骤之一,我写了这篇前言!

致谢

抚养一个孩子需要整个村庄的参与,写一本书也是如此。以下只是我自己村庄的一小部分,在写作过程中给予了我巨大帮助的人。

我首先要感谢我的父母 Bang 和 Lan,他们的持续支持使我能够毫无畏惧地探索未知:出国留学;攻读博士学位;当然,还有写书。我还要诚挚地感谢我的姐姐和知己 Nhu,她总是在我最艰难的时刻帮助我。

贝叶斯优化是我博士研究的重要组成部分,我要感谢那些在项目中真正让我的博士经历变得宝贵的人。特别感谢我的指导老师罗曼·加内特,他毫不费力地说服我去从事贝叶斯机器学习的研究。是你开启了这一切。我还要感谢来自主动学习实验室的朋友们:叶虎·陈、沙扬·莫纳德杰米和阿尔维塔·奥特利教授。他们说博士阶段的回报很少,而与你们一起工作正是构成了其中大部分回报。

接下来,我要由衷感谢曼宁公司的出色团队。我感谢我的开发编辑 Marina Michaels,她以最高水平的专业精神、关怀、支持和耐心领导着这艘船,从一开始就与我同舟共济。能够与你配对完成我们的项目,我感到非常幸运。感谢我的收购编辑 Andy Waldron,即使已经有一个更好的作者在写一本类似主题的书,他仍对这个想法充满信心,以及 Ivan Martinovic´,他帮助我解决了 AsciiDoc 的问题,并耐心修复了我的标记代码。

我要感谢审稿人们投入时间和精力,大大提高了写作质量:Allan Makura、Andrei Paleyes、Carlos Aya-Moreno、Claudiu Schiller、Cosimo Attanasi、Denis Lapchev、Gary Bake、George Onofrei、Howard Bandy、Ioannis Atsonios、Jesús Antonino Juárez Guerrero、Josh McAdams、Kweku Reginald Wade、Kyle Peterson、Lokesh Kumar、Lucian Mircea Sasu、Marc-Anthony Taylor、Marcio Nicolau、Max Dehaut、Maxim Volgin、Michele Di Pede、Mirerfan Gheibi、Nick Decroos、Nick Vazquez、Or Golan、Peter Henstock、Philip Weiss、Ravi Kiran Bamidi、Richard Tobias、Rohit Goswami、Sergio Govoni、Shabie Iqbal、Shreesha Jagadeesh、Simone Sguazza、Sriram Macharla、Szymon Harabasz、Thomas Forys 和 Vlad Navitski。

写书时不可避免地会有盲点,而审稿人们帮助填补了这些盲点,并让作者专注于真正重要的事情。衷心感谢 Kerry Koitzsch 提供的有见地的反馈和 James Byleckie 在代码和写作方面提出的优秀建议。

最后,我要感谢 GPyTorch 和 BoTorch 库背后的团队,这些库是为本书开发的代码的主要工作马力。我尝试过各种高斯过程和贝叶斯优化的库,但总是发现自己回到 GPyTorch 和 BoTorch。我希望本书能在这些库周围构建一个已经很棒的社区中发挥作用。

关于本书

过去要了解贝叶斯优化,人们需要搜索相关库的在线文章和教程,这些文章和教程零散分布,由于其性质,不会深入探讨具体细节。你也可以求助于技术教材,但它们通常太过密集和数学密集,这对于希望立即着手实践的从业者来说是一个挑战。

本书通过提供一系列实践讨论、对感兴趣的读者的更深入材料的引用以及可直接使用的代码示例来填补这一空白。它首先为贝叶斯优化的组成部分建立直觉,然后使用最先进的软件在 Python 中实现它们。

本书的精神是提供一个以数学和概率的高层次直觉为基础的贝叶斯优化易于理解的介绍。感兴趣的读者可以在全书中找到更多被引用的更深入的技术文本,以深入了解感兴趣的主题。

谁应该阅读本书?

对于对超参数调优、A/B 测试或实验以及更一般的决策制定感兴趣的数据科学家和 ML 从业者,本书将有所裨益。

化学、材料科学和物理等科学领域的研究人员面临着困难的优化问题,也会发现本书有所帮助。尽管大多数跟随内容所必需的背景知识将会被涵盖,但读者应该熟悉 ML 中的常见概念,例如训练数据、预测模型、多元正态分布等。

本书的组织方式:一条路线图

本书包括四个主要部分。每个部分都包含涵盖相应主题的几章:

  • 第一章介绍了使用真实用例的贝叶斯优化。它还包括了一个视觉示例,展示了贝叶斯优化如何加速寻找昂贵函数的全局最优解,而不涉及技术细节。

第一部分涵盖了高斯过程作为我们希望优化的函数的预测模型。其核心论点是,高斯过程提供了校准的不确定性量化,这在我们的贝叶斯优化框架中是必不可少的。本部分由两章组成:

  • 第二章显示高斯过程是从一些观察数据中学习回归模型的自然解决方案。高斯过程定义了函数的分布,并且可以根据一些观察数据来更新以反映我们对函数值的信念。

  • 第三章介绍了我们将先验信息合并到高斯过程中的两种主要方式:均值函数和协方差函数。均值函数指定了一般趋势,而协方差函数指定了函数的平滑程度。

第二部分列举了贝叶斯优化策略,这些策略是关于如何进行函数评估的决策过程,以便尽可能高效地确定全局最优值。虽然不同的策略由不同的目标驱动,但它们都旨在平衡探索和利用之间的权衡。这部分由三章组成:

  • 第四章讨论了一种自然的方法来决定哪个函数评估是最有利的:考虑从当前最佳函数值中获得的改进。由于基于高斯过程的对函数的信念,我们可以计算这些与改进相关的数量,并且可以在封闭形式下廉价地进行,从而实现了两种特定的贝叶斯优化策略:改进的概率和预期改进。

  • 第五章探讨了贝叶斯优化与另一种常见的问题类别之间的联系,称为多臂老丨虎丨机。我们学习如何将多臂老丨虎丨机策略转换为贝叶斯优化设置,并获得相应的策略:上置信界和汤普森采样。

  • 第六章考虑了一种减少我们对函数全局最优值信念中不确定性的策略。这构成了基于熵的策略,使用了一种称为信息论的数学子领域。

第三部分介绍了一些最常见的用例,这些用例不能很好地适应本书迄今为止开发的工作流程,并展示了如何修改贝叶斯优化来解决这些优化任务:

  • 第七章介绍了批量优化,在这种情况下,为了提高吞吐量,我们允许实验并行运行。例如,可以同时在一组 GPU 上并行训练大型神经网络的多个实例。这需要优化策略同时返回多个建议。

  • 第八章讨论了安全关键的用例,我们在这些情况下不能自由地探索搜索空间,因为一些函数评估可能会产生不利影响。这促使了这样一种设置,即对于所讨论的函数如何行为有一定的约束,并且我们需要在优化策略的设计中考虑这些约束。

  • 第九章表明,当我们可以以不同成本和精度级别观察函数值的多种方式时——通常称为多信度贝叶斯优化——考虑可变成本可以提高优化性能。

  • 第十章涵盖了成对比较,已经显示出比数字评估或评级更准确地反映了个人偏好,因为它们更简单,对标注者的认知负荷较轻。第十章将贝叶斯优化扩展到此设置,首先使用特殊的高斯过程模型,然后修改现有策略以适应这个成对比较的工作流程。

  • 一个人可能希望同时优化多个可能冲突的目标。第十一章研究了这个多目标优化问题,并展示了贝叶斯优化如何在这种情况下扩展。

第四部分涉及高斯过程模型的特殊变体,展示了它们在建模和提供不确定性校准预测方面的灵活性和效果,即使在贝叶斯优化环境之外也是如此:

  • 在第十二章中,我们了解到在某些情况下,无法获得经过训练的高斯过程的闭合形式解。然而,可以使用复杂的近似策略进行高保真度的近似。

  • 第十三章展示了由于 Torch 生态系统的存在,将 PyTorch 神经网络与 GPyTorch 高斯过程结合起来是一个无缝过程。这使得我们的高斯过程模型更加灵活和表达能力更强。

初学者将从前六章获益良多。有经验的从业者希望将贝叶斯优化应用于他们的案例中,可能会在第七章到第十一章中找到价值,这些章节可以独立阅读,任意顺序。长期使用高斯过程的用户很可能对最后两章感兴趣,我们在这些章节中开发了专门的高斯过程模型。

关于代码

您可以从本书的在线版本 livebook.manning.com/book/bayesian-optimization-in-action 获取可执行的代码片段。代码可以从 Manning 网站 www.manning.com/books/bayesian-optimization-in-action 和 GitHub github.com/KrisNguyen135/bayesian-optimization-in-action 下载;后者接受问题和拉取请求。

您将使用 Jupyter notebooks 来运行附带书籍的代码。Jupyter notebooks 提供了一种干净的方式来动态地使用代码,使我们能够探索每个对象的行为以及它与其他对象的交互。有关如何开始使用 Jupyter notebook 的更多信息,请查找它们的官方网站:jupyter.org。在我们的情况下,动态探索对象的能力特别有帮助,因为 Bayesian 优化工作流的许多组件是由 GPyTorch 和 BoTorch 实现的 Python 对象。

GPyTorch 和 BoTorch 是 Python 中用于高斯过程建模和贝叶斯优化的首选库。还有其他选择,例如 scikit-Learn 的 scikit-optimize 扩展或 GPflow 和 GPflowOpt,它们扩展了 TensorFlow 框架用于贝叶斯优化。然而,GPyTorch 和 BoTorch 的组合构成了最全面和灵活的代码库,其中包括许多来自贝叶斯优化研究的最新算法。根据我自己使用贝叶斯优化软件的经验,我发现 GPyTorch 和 BoTorch 在易于初学者使用和提供最新方法之间取得了良好的平衡。

有一件事需要注意:正是因为这些库正在积极地维护,书中展示的 API 可能在新版本中略有变化,因此重要的是您按照requirements.txt文件中指定的库版本来运行代码,以避免错误。你可以在官方 Python 文档中找到更多关于如何使用requirements.txt文件创建 Python 环境的说明,例如在packaging.python.org/en/latest/guides/installing-using-pip-and-virtual-environments。话虽如此,要使用新版本,您可能只需要对代码做些微小的修改。

当您阅读本书时,您会注意到文本往往只专注于代码的关键组件,省略了许多细节,比如库的导入和繁琐的代码。(当然,代码第一次使用时,它会在文本中被正确介绍。)简洁的讨论有助于我们专注于每一章中真正新颖的内容,避免重复。另一方面,Jupyter 笔记本中的代码是自包含的,可以单独运行每个笔记本,无需任何修改。

liveBook 讨论论坛

购买《Bayesian Optimization in Action》包括免费访问 liveBook,Manning 的在线阅读平台。使用 liveBook 独有的讨论功能,您可以将评论附加到全书或特定的部分或段落上。您可以轻松地为自己做笔记,提出和回答技术问题,并从作者和其他用户那里获得帮助。要访问论坛,请转到livebook.manning.com/book/bayesian-optimization-in-action/discussion。您还可以在livebook.manning.com/discussion了解更多关于 Manning 论坛和行为规则的信息。

Manning 对我们的读者的承诺是提供一个有意义的对话场所,既是个人读者之间的对话,也是读者与作者之间的对话。这并不是对作者参与的具体数量的承诺,他们对论坛的贡献仍然是自愿的(且未支付的)。我们建议您尝试向作者提出一些具有挑战性的问题,以免他们的兴趣消失!论坛和以前讨论的存档将在该书印刷期间都可以从出版商的网站上访问到。

关于作者

Quan Nguyen 是一位 Python 程序员和机器学习爱好者。他对解决涉及不确定性的决策问题感兴趣。Quan 撰写了几本关于 Python 编程和科学计算的书籍。他目前正在华盛顿大学圣路易斯分校攻读计算机科学博士学位,他在那里研究机器学习中的贝叶斯方法。

关于技术编辑

本书的技术编辑是 Kerry Koitzsch。Kerry 是一位作者,软件架构师,在企业应用程序和信息架构解决方案的实施方面拥有三十多年的丰富经验。Kerry 是一本关于分布式处理的书籍的作者,以及许多较短的技术出版物的作者,并拥有一项关于创新 OCR 技术的专利。他还是美国陆军成就奖的获得者。

关于封面插图

Bayesian Optimization in Action 封面上的图案标题为“波兰人”,摘自 Jacques Grasset de Saint-Sauveur 的一本 1797 年出版的作品集。每一幅插图都是手工精细绘制和上色的。

在那些日子里,通过人们的服装很容易辨别他们住在哪里,以及他们的行业或生活地位。Manning 凭借基于数个世纪前地区文化的丰富多样性的书籍封面,赞美了计算机业务的创造性和主动性,这些书籍封面是由这类集合中的图片重新唤起的。

第二章:贝叶斯优化简介

本章内容包括

  • 是什么促使了贝叶斯优化以及它是如何工作的

  • 贝叶斯优化问题的实际例子

  • 贝叶斯优化的一个玩具示例

你选择阅读本书是一个很棒的选择,我对你即将开始的旅程感到兴奋!从高层次来看,贝叶斯优化是一种优化技术,当我们试图优化的函数(或者一般情况下,当输入一个值时产生输出的过程)是一个黑盒且评估起来时间、金钱或其他资源成本很高时,可以应用此技术。这个设置涵盖了许多重要的任务,包括超参数调优,我们将很快定义它。使用贝叶斯优化可以加速搜索过程,并帮助我们尽快找到函数的最优解。

尽管贝叶斯优化在机器学习研究界一直受到持久的关注,但在实践中,它并不像其他机器学习话题那样常用或广为人知。但为什么呢?有些人可能会说贝叶斯优化具有陡峭的学习曲线:使用者需要理解微积分、使用概率,并且需要是一个经验丰富的机器学习研究者,才能在应用中使用贝叶斯优化。本书的目标是打破贝叶斯优化难以使用的观念,并展示该技术比想象的更直观、更易用。

在本书中,我们会遇到许多插图、图表和代码,旨在使讨论的主题更加简单明了和具体。你将了解贝叶斯优化的每个组成部分在高层次上是如何工作的,并学会如何使用 Python 中的最先进的库来实现它们。配套的代码还可帮助你快速上手你自己的项目,因为贝叶斯优化框架非常通用和“即插即用”。这些练习对此也非

总的来说,我希望这本书对你的机器学习需求有所帮助,并且是一本有趣的阅读。在我们深入讨论实际内容之前,让我们花点时间来讨论贝叶斯优化试图解决的问题。

1.1 寻找一个昂贵黑盒函数的最优解

如前所述,超参数调优是贝叶斯优化在机器学习中最常见的应用之一。我们在本节中探讨了这个问题以及其他一些问题,作为黑盒优化问题的一个例子。这将帮助我们理解为什么需要贝叶斯优化。

1.1.1 超参数调优作为昂贵黑盒优化问题的一个示例

假设我们想在一个大数据集上训练神经网络,但我们不确定这个神经网络应该有多少层。我们知道神经网络的架构是深度学习中的一个成功因素,因此我们进行了一些初步测试,并得到了表格 1.1 中显示的结果。

表格 1.1 超参数调优任务的示例

层数 测试集准确率
5 0.72
10 0.81
20 0.75

我们的任务是决定神经网络在寻找最高准确率时应该有多少层。很难决定我们应该尝试下一个数字是多少。我们找到的最佳准确率为 81%,虽然不错,但我们认为通过不同数量的层,我们可以做得更好。不幸的是,老板已经设定了完成模型实施的截止日期。由于在我们的大型数据集上训练神经网络需要几天时间,我们只剩下几次试验的机会,然后就需要决定我们的网络应该有多少层。考虑到这一点,我们想知道我们应该尝试哪些其他值,以便找到提供最高可能准确率的层数。

这项任务旨在找到最佳设置(超参数值),以优化模型的某些性能指标,如预测准确率,在机器学习中通常被称为超参数调整。在我们的示例中,神经网络的超参数是其深度(层数)。如果我们使用决策树,常见的超参数包括最大深度、每个节点的最小数据点数和分裂标准。对于支持向量机,我们可以调整正则化项和核函数。由于模型的性能很大程度上取决于其超参数,超参数调整是任何机器学习流水线的重要组成部分。

如果这是一个典型的真实世界数据集,这个过程可能需要大量的时间和资源。来自 OpenAI 的图 1.1(openai.com/blog/ai-and-compute/)显示,随着神经网络变得越来越大和越来越深,所需的计算量(以 petaflop/s-days 为单位)呈指数增长。

图 1.1 训练大型神经网络的计算成本一直在稳步增长,使得超参数调整变得越来越困难。

这意味着在大型数据集上训练模型是相当复杂的,并且需要大量的工作。此外,我们希望确定能够提供最佳准确率的超参数值,因此需要进行多次训练。我们应该如何选择数值来对我们的模型进行参数化,以便尽快找到最佳组合?这是超参数调整的核心问题。

回到我们在第 1.1 节中的神经网络示例,我们应该尝试多少层才能找到高于 81%的准确度?在 10 层和 20 层之间的某个数值是有前途的,因为在 10 层和 20 层,我们的性能比在 5 层时更好。但我们应该检查哪个确切的数值仍然不明显,因为在 10 和 20 之间的数值仍然可能有很大变异性。当我们说变异性时,我们隐含地谈论了我们关于模型测试准确性如何随层数变化而变化的不确定性。即使我们知道 10 层导致 81%,20 层导致 75%,我们仍然不能确定例如 15 层会产生什么值。这就是说,当我们考虑 10 和 20 之间的这些值时,我们需要考虑我们的不确定性水平。

此外,如果某个大于 20 的数值为我们提供了最高可能的准确度怎么办?这对于许多大型数据集来说是一种情况,其中足够的深度对于神经网络学习任何有用的东西都是必要的。或者,尽管不太可能,少于 5 层的小层数实际上是我们需要的吗?

我们应该如何以有原则的方式探索这些不同的选择,以便在时间耗尽和我们必须向老板汇报时,我们可以充分自信地认为我们已经找到了我们模型的最佳层数?该问题是昂贵黑盒优化问题的一个例子,我们接下来会讨论这个问题。

1.1.2 昂贵黑盒优化问题

在这个子章节中,我们正式介绍了昂贵黑盒优化问题,这是贝叶斯优化的目标。理解为什么这个问题很难有助于我们理解,为什么贝叶斯优化比更简单的、更天真的方法更受欢迎,比如网格搜索(我们将搜索空间分为相等的段)或随机搜索(我们使用随机性来指导我们的搜索)。

在这个问题中,我们可以黑匣子方式访问函数(一些输入-输出机制),我们的任务是找到最大化此函数输出的输入。该函数通常称为目标函数,因为优化它是我们的目标,并且我们希望找到目标函数的最优解——产生最高函数值的输入。

目标函数的特点

术语黑盒意味着我们不知道目标的底层公式;我们唯一能够访问的是通过在某个输入处计算函数值进行观察时得到的函数输出。在我们的神经网络示例中,我们不知道如果我们逐层增加层数,我们的模型的准确性将如何变化(否则,我们将只选择最佳层)。

这个问题很昂贵,因为在许多情况下,做出观察(在某个位置评估目标)的成本非常高昂,使得像穷举搜索这样的天真方法难以处理。在机器学习和尤其是深度学习中,时间通常是主要的约束条件,正如我们之前讨论过的那样。

超参数调整属于这一类昂贵的黑盒优化问题,但不是唯一的!任何试图找到一些设置或参数来优化一个过程,而不知道不同设置如何影响和控制过程结果的程序都属于黑盒优化问题。此外,尝试特定设置并观察其对目标过程(目标函数)的结果是耗时的、昂贵的或在某种程度上成本高昂的。

定义 尝试特定设置的行为——即,在某个输入处评估目标函数的值——称为发出查询查询目标函数。整个过程总结如图 1.2。

图 1.2 黑盒优化问题的框架。我们反复查询不同位置的函数值以找到全局最优解。

1.1.3 其他昂贵的黑盒优化问题的实际例子

现在,让我们考虑一些属于昂贵的黑盒优化问题类别的实际例子。我们会发现这样的问题在这个领域中很常见;我们经常会遇到想要优化但只能评估少数次的函数。在这些情况下,我们希望找到一种方法来智能地选择在哪里评估函数。

第一个例子是药物发现——科学家和化学家识别具有理想化学特性的化合物,可以合成成药物的过程。正如你可以想象的那样,实验过程非常复杂并且成本很高。使这项药物发现任务令人生畏的另一个因素是近年来已经观察到的药物发现研发生产力下降趋势。这种现象被称为艾尔姆定律——摩尔定律的反转——它大致说明了每十亿美元批准的新药物数量在固定时间内减半。艾尔姆定律在杰克·W·斯坎内尔(Jack W. Scannell)、亚历克斯·布兰克利(Alex Blanckley)、海伦·波尔登(Helen Boldon)和布莱恩·沃灵顿(Brian Warrington)撰写的自然杂志论文“诊断药物研发效率下降”(www.nature.com/articles/nrd3681)的第 1 张图片中有可视化。(或者,你也可以简单地在谷歌上搜索“艾尔姆定律”的图像。)

赫尔姆斯定律显示,每十亿美元的药物研究与开发(R&D)投资所得到的药物发现能力,经过对数尺度上的线性下降。换句话说,对于固定数量的 R&D 投资,药物发现能力在最近几年呈指数级下降。虽然在不同年份存在局部趋势的起伏,但从 1950 年到 2020 年,指数下降趋势是明显的。

实际上,同样的问题适用于任何科学发现任务,其中科学家们通过使用需要顶级设备和可能需要几天或几周才能完成的实验,搜索罕见、新颖和有用的新化学品、材料或设计,用于某种度量标准。换句话说,他们试图针对极为昂贵的数据评估来优化它们各自的目标函数。

以表 1.2 为例,展示了真实数据集中的几个数据点。目标是找到具有最低混合温度的合金组成(来自四个母元素)。这是一个黑盒优化问题。在这里,材料科学家们研究了铅(Pb)、锡(Sn)、锗(Ge)和锰(Mn)的合金组成。每个给定的组合百分比对应于一个可以在实验室中合成和实验的潜在合金。

表 1.2 来自材料发现任务的数据

Pb 的百分比 Sn 的百分比 Ge 的百分比 Mn 的百分比 混合温度(°F)
0.50 0.50 0.00 0.00 192.08
0.33 0.33 0.33 0.00 258.30
0.00 0.50 0.50 0.00 187.24
0.00 0.33 0.33 0.33 188.54
来源: 作者的研究工作。

由于低的混合温度表示合金结构稳定、有价值,目标是找到混合温度尽可能低的组成。但是有一个瓶颈:通常需要数天时间才能确定给定合金的混合温度。我们要算法地解决的问题是类似的:给定我们看到的数据集,我们应该尝试下一个组合(在铅、锡、锗和锰的含量上如何)以找到最低的混合温度?

另一个例子是在采矿和石油钻探中,或者更具体地说,在一个大区域内找到具有最高价值矿物或石油产量的地区。这需要大量的规划、投资和劳动力,是一项昂贵的事业。由于挖掘作业对环境有重大负面影响,因此有相应的法规来减少采矿活动,在这个优化问题中对可以进行的函数评估数量设定了限制。

昂贵的黑盒优化中的中心问题是:如何决定在哪里评估这个目标函数,以便在搜索结束时找到其最优值?正如我们在后面的例子中看到的,简单的启发式方法——如随机或网格搜索,这些方法是流行的 Python 包(如 scikit-learn)实现的——可能会导致对目标函数的浪费性评估,从而导致整体优化性能较差。这就是贝叶斯优化发挥作用的地方。

1.2 介绍贝叶斯优化

考虑到昂贵的黑盒优化问题,现在我们介绍贝叶斯优化作为这个问题的解决方案。这给了我们一个高层次的关于贝叶斯优化是什么以及它如何使用概率机器学习来优化昂贵的黑盒函数的概念。

贝叶斯优化(BayesOpt)的定义是一种机器学习技术,它同时维护一个预测模型来学习关于目标函数的信息并且通过贝叶斯概率和决策理论决定如何获取新数据来完善我们对目标的知识。

通过数据,我们指的是输入输出对,每个对应一个输入值到该输入处的目标函数值的映射。在超参数调优的具体案例中,这些数据与我们旨在调整的机器学习模型的训练数据不同。

在贝叶斯优化过程中,我们根据贝叶斯优化算法的建议做出决定。一旦我们采取了贝叶斯优化建议的行动,贝叶斯优化模型将根据该行动的结果进行更新,并继续推荐下一步要采取的行动。这个过程重复进行,直到我们有信心找到了最优行动。

这个工作流程有两个主要组成部分:

  • 一个从我们所做的观察中学习并对未见数据点上的目标函数值进行预测的机器学习模型

  • 通过评估目标以定位最优值的优化策略

我们在以下小节中介绍每个组件。

1.2.1 用高斯过程建模

贝叶斯优化首先在我们试图优化的目标函数上拟合一个预测的机器学习模型——有时,这被称为替代模型,因为它充当我们从观察中相信的函数和函数本身之间的替代。这个预测模型的作用非常重要,因为它的预测结果会影响贝叶斯优化算法的决策,并且直接影响优化性能。

几乎在所有情况下,高斯过程(GP)被用于这种预测模型的角色,我们在本小节中对此进行了研究。在高层次上,与任何其他机器学习模型一样,高斯过程(GP)的运行原则是相似的数据点产生相似的预测。与岭回归、决策树、支持向量机或神经网络等其他模型相比,GPs 可能不是最受欢迎的模型类别。然而,正如我们在本书中一再看到的那样,GPs 带有一个独特且至关重要的特性:它们不像其他提到的模型那样产生点估计预测;相反,它们的预测是以概率分布的形式。以概率分布或概率预测的形式进行的预测在贝叶斯优化中是关键的,它使我们能够量化我们的预测不确定性,进而改善我们在做出决策时的风险-回报折衷。

首先让我们看看当我们在数据集上训练一个 GP 时它是什么样子的。比如说,我们有兴趣训练一个模型来从表 1.3 的数据集中学习,该数据集在图 1.3 中被可视化为黑色的 x

表 1.3 对应于图 1.3 的一个示例回归数据集

训练数据点 标签
1.1470 1.8423
-4.0712 0.7354
0.9627 0.9627
1.2471 1.9859

图 1.3 非贝叶斯模型,比如岭回归器,做出的是点估计,而高斯过程则产生概率分布作为预测。因此,高斯过程提供了一个校准的不确定性量化,这是在做出高风险决策时的一个重要因素。

我们首先在这个数据集上拟合了一个岭回归模型,并在 -5 和 5 的范围内做出预测;图 1.3 的顶部面板显示了这些预测。岭回归模型是线性回归模型的改进版本,其中模型的权重被正则化,以便更偏爱较小的值,以防止过拟合。该模型在给定测试点时所做的每个预测都是一个单值数字,它并没有捕获我们对所学习函数行为的不确定性水平的认识。例如,给定 2 的测试点,该模型简单地预测为 2.2。

我们不需要过多地了解这个模型的内部工作原理。关键在于岭回归器产生没有不确定性度量的点估计,这也是许多机器学习模型的情况,比如支持向量机、决策树和神经网络。

那么,高斯过程是如何做出预测的呢?如图 1.3 的底部面板所示,高斯过程的预测是以概率分布的形式(具体来说,是正态分布)进行的。这意味着在每个测试点,我们有一个平均预测(实线)以及所谓的 95% 置信区间(CI)(阴影区域)。

需要注意的是,“CI”这个缩写词在统计学的频率主义中常用来缩写“置信区间”(confidence interval);在本书中,我只使用“CI”来指代“可信区间”(credible interval)。虽然这两个概念在技术上有许多不同之处,但从高层次上来看,我们仍然可以将本书中的 CI 视为一个区间,这个区间内有可能包含一个感兴趣的数量(在这种情况下,就是我们正在预测的函数的真实值)。

GP vs. 岭回归

有趣的是,当使用相同的协方差函数(也称为“核”)时,“高斯过程”(GP)和“岭回归模型”产生了相同的预测结果(对于 GP 来说是均值预测),如图 1.3 所示。我们会在第三章更深入地讨论协方差函数。这意味着,GP 具有岭回归模型所有的好处,同时还提供了额外的 CI 预测。

在实际测试位置上,这个 CI 有效地度量了我们对每个测试位置的价值的不确定性水平。如果一个位置的预测 CI 较大(比如图 1.3 中的-2 或 4),则这个值的可能值范围更广。换句话说,我们对这个值的确定性更低。如果一个位置的 CI 较小(图 1.3 中的 0 或 2),则我们对这个位置的值更有信心。GP 的一个很好的特点是,对于训练数据的每个点,预测 CI 接近于 0,这表示我们对其值没有任何不确定性。这是有道理的;毕竟,我们已经从训练集中知道了该值。

带噪音的函数评估

虽然在图 1.3 中不是这种情况,但是在我们的数据集中,数据点的标签可能是有噪声的。在实际世界中,观察数据的过程很可能会受到噪声的干扰。在这种情况下,我们可以使用 GP 进一步指定噪声水平,观察数据点的 CI 将不会降为 0,而是降至指定的噪声水平。这表明了使用 GP 建模所具有的灵活性。

能够将我们对不确定性的水平进行量化的能力(称为“不确定性量化”)在任何高风险的决策过程中都非常有用,比如贝叶斯优化。在 1.1 节中出现的情景再次设想一下,我们调整神经网络中的层数,并且只有时间尝试一个更多的模型。假设在那些数据点上训练之后,GP 预测 25 层的平均精度将为 0.85,相应的 95% CI 为 0.81 至 0.89。另一方面,对于 15 层,GP 预测我们的精度平均也是 0.85,但是 95% CI 为 0.84 至 0.86。在这种情况下,即使这两个数字具有相同的期望值,选择 15 层是相当合理的,因为我们更“确定”15 层将给我们带来好的结果。

清楚地说,GP 不会为我们做出任何决定,但它确实通过其概率预测为我们提供了一种方法。决策留给 BayesOpt 框架的第二部分:策略。

使用 BayesOpt 策略做决策

除了作为预测模型的 GP 之外,在 BayesOpt 中,我们还需要一个决策过程,我们将在本小节中探讨这个问题。这是 BayesOpt 的第二个组成部分,它接受 GP 模型所做的预测,并推理如何最好地评估目标函数,以便有效地找到最优解。

如前所述,95% CI 为 0.84 至 0.86 的预测要比 95% CI 为 0.81 至 0.89 的预测更好,特别是如果我们只有一次尝试的机会。这是因为前者更像是一件确定的事情,保证为我们带来一个好结果。在两个点的预测均值和预测不确定性可能不同的更一般情况下,我们应该如何做出这个决定?

这正是 BayesOpt 策略帮助我们做的事情:量化一个点的有用性,考虑到其预测概率分布。策略的工作是接受 GP 模型,该模型代表我们对目标函数的信念,并为每个数据点分配一个分数,表示该点在帮助我们识别全局最优解方面的帮助程度。这个分数有时被称为获取分数。我们的工作是选择最大化这个获取分数的点,并在该点评估目标函数。

我们在图 1.4 中看到与图 1.3 中相同的 GP,在底部面板中显示了一个名为Expected Improvement的特定 BayesOpt 策略如何在x-轴上的每个点(在我们的搜索空间内的-5 到 5 之间)得分。我们将在第四章学习这个名称的含义以及该策略如何对数据点进行评分。现在,让我们先记住,如果一个点具有较大的获取分数,这个点对于定位全局最优解是有价值的。

图 1.4 BayesOpt 策略通过其在定位全局最优解中的有用性对每个单独的数据点进行评分。该策略倾向于高预测值(其中回报更有可能)以及高不确定性(其中回报可能较大)。

在图 1.4 中,最佳点在 1.8 左右,这是有道理的,因为根据我们在顶部面板中的 GP,在那里我们也实现了最高的预测均值。这意味着我们将选择在 1.8 处评估我们的目标,希望从我们收集到的最高值中得到改进。

我们应该注意到,这不是一个一次性的过程,而是一个学习循环。在循环的每一次迭代中,我们根据我们从目标中观察到的数据训练一个高斯过程(GP),在这个高斯过程上运行贝叶斯优化策略,以得到一个希望帮助我们确定全局最优的建议,然后在推荐位置进行观察,将新点添加到我们的训练数据中,并重复整个过程,直到达到某个终止条件。事情可能变得有点混乱,所以是时候退后一步,看看贝叶斯优化的大局了。

与实验设计的联系

此时,贝叶斯优化的描述可能让你想起了统计学中的实验设计(DoE)的概念,它旨在通过调整可控设置来解决优化目标函数的问题。这两种技术之间存在许多联系,但是贝叶斯优化可以被看作是一种更一般的方法,它由机器学习模型高斯过程(GP)驱动。

1.2.3 组合高斯过程和优化策略形成优化循环

在本小节中,我们总结了我们迄今为止所描述的内容,并使过程更加具体。我们全面地看到了贝叶斯优化的工作流程,并更好地理解了各个组成部分是如何相互配合的。

我们从一个初始数据集开始,就像表 1.1、1.2 和 1.3 中的那样。然后,贝叶斯优化的工作流程在图 1.5 中进行了可视化,总结如下:

  1. 我们在这个数据集上训练了一个高斯过程(GP)模型,根据我们从训练数据中观察到的内容给出了对我们的目标在每个地方的信念。这种信念由实线和阴影区域表示,就像图 1.3 和 1.4 中的那样。

  2. 然后,贝叶斯优化策略接收这个高斯过程,并根据该点在域中的价值对每个点进行评分,这如图 1.4 中的下曲线所示。

  3. 最大化该分数的点是我们选择下一个要评估目标的点,然后将其添加到我们的训练数据集中。

  4. 这个过程会重复进行,直到我们无法再评估目标。

BayesOpt 循环

图 1.5 贝叶斯优化循环,结合了高斯过程(GP)建模和决策制定的策略。现在可以使用这个完整的工作流程来优化黑盒函数。

与监督学习任务不同,我们只需在训练数据集上拟合一个预测模型并在测试集上进行预测(只包括步骤 1 和 2),贝叶斯优化工作流程通常被称为主动学习。主动学习是机器学习中的一个子领域,我们可以决定我们的模型从哪些数据点中学习,而这个决策过程则由模型本身来决定。

正如我们所说,GP 和政策是这个 BayesOpt 过程的两个主要组成部分。如果 GP 没有很好地对客观函数进行建模,那么我们将无法很好地将训练数据中的信息通知给政策。另一方面,如果政策不能很好地给“好”点分配高分和给“坏”点分配低分(其中意味着有助于找到全局最优解),那么我们的后续决策将是错误的,并且很可能会取得糟糕的结果。

换句话说,如果没有一个好的预测模型,比如一个 GP,我们就无法通过校准的不确定性做出良好的预测。没有政策,我们可以做出良好的预测,但我们不会做出良好的决策

我们在本书中多次考虑的一个例子是天气预报。想象一下这样一个情景,你要决定在离开家去上班前是否带伞,并查看手机上的天气预报应用程序。

不用说,应用程序的预测需要准确可靠,这样你就可以自信地根据它们做出决定。一个总是预测晴天的应用程序是不够的。此外,你需要一种明智的方式来根据这些预测做出决策。无论多么可能下雨,永远不带伞都是一个糟糕的决策政策,当下雨时会让你陷入麻烦。另一方面,即使天气预报有 100% 的晴天可能性,也不是一个明智的决定。你希望根据天气预报自适应地决定是否带伞。

自适应地做出决策是 BayesOpt 的核心,为了有效地实现这一点,我们需要一个好的预测模型和一个好的决策政策。在这个框架的两个组成部分都需要注意;这就是为什么本章后面的两个主要部分分别涵盖了用 GP 进行建模和用 BayesOpt 政策进行决策。

1.2.4 BayesOpt 的实际应用

此时,你可能想知道所有这些复杂的机器真的是否有效—或者是否比一些简单的策略如随机抽样更有效。为了找出答案,让我们看一下 BayesOpt 在一个简单函数上的“演示”。这也是我们从抽象到具体的好方法,并揭示了我们在后续章节中能做什么。

假设我们试图优化的黑盒客观函数(特别是在这种情况下,最大化)是图 1.6 中的一维函数,从 -5 到 5 定义。同样,这个图片仅供参考;在黑盒优化中,我们实际上不知道客观函数的形状。我们看到客观函数在 -5(大约 -2.4 和 1.5)附近有几个局部极大值,但全局最大值在右侧大约是 4.3。此外,假设我们被允许最多评估客观函数 10 次。

图 1.6 要最大化的目标函数,在随机搜索中浪费资源于不利区域

在看到贝叶斯优化如何解决这个优化问题之前,让我们看看两种基准策略。第一种是随机搜索,在–5 到 5 之间均匀采样;我们得到的任何点都是我们将评估目标的位置。图 1.6 是这样一个方案的结果。这里找到的价值最高的点大约在x = 4 处,其值为f(x) = 3.38。

随机搜索的工作原理

随机搜索涉及在我们的目标函数域内均匀随机选择点。也就是说,我们最终到达域内某点的概率等于我们最终到达其他任何点的概率。如果我们认为搜索空间中有重要区域应该更加关注,我们可以从非均匀分布中抽取这些随机样本,而不是均匀采样。然而,这种非均匀策略需要在开始搜索之前知道哪些区域是重要的。

你可能会觉得不满意的是,这些随机抽样的点中有许多恰好落入 0 周围的区域。当然,许多随机样本聚集在 0 周围只是偶然的,而在另一个搜索实例中,我们可能会发现在另一个区域有许多样本。然而,我们仍然可能浪费宝贵的资源来检查函数的一个小区域,其中包含许多评估。直觉上,扩展我们的评估更有利于我们了解目标函数。

这种扩散评估的想法将我们带到了第二个基准:网格搜索。在这里,我们将搜索空间划分为均匀间隔的段,并在这些段的端点进行评估,就像图 1.7 所示。

图 1.7 网格搜索仍然无法有效缩小好的区域。

这次搜索中的最佳点是最右边的最后一个点,在 5 处进行评估,大约为 4.86。这比随机搜索更好,但仍然错过了实际的全局最优点。

现在,我们准备看贝叶斯优化如何运作!贝叶斯优化从一个随机抽样的点开始,就像随机搜索一样,如图 1.8 所示。

图 1.8 贝叶斯优化的开始与随机搜索相似。

图 1.8 的顶部面板表示对评估点进行训练的高斯过程,而底部面板显示了由期望改进策略计算的分数。请记住,这个分数告诉我们应该如何评价我们搜索空间中的每个位置,我们应该选择下一个评估分数最高的位置。有趣的是,在这一点上,我们的策略告诉我们,我们搜索的-5 到 5 之间的几乎整个范围都很有前景(除了围绕 1 的区域,我们已经进行了一次查询)。这应该是直观的,因为我们只看到了一个数据点,而且我们还不知道其他区域的目标函数是什么样子的。我们的策略告诉我们我们应该探索更多!现在让我们看看从第一个查询到第四个查询时我们模型的状态如何在图 1.9 中。

图 1.9

图 1.9 在四次查询之后,我们确定了第二个最佳点。

四次查询中有三次集中在点 1,这里有一个局部最优点,我们还看到我们的策略建议我们下一步查询这个区域的另一个点。此时,你可能担心我们会陷入这个局部最优区域无法摆脱,无法找到真正的最优点,但我们会看到情况并非如此。让我们快进到图 1.10 中的接下来两次迭代。

图 1.10

图 1.10 在充分探索局部最优点后,我们被鼓励看看其他区域。

在对这个局部最优区域进行五次查询后,我们的策略决定有其他更有前景的区域可供探索——即左边约为-2 和右边约为 4 的区域。这非常令人放心,因为它表明一旦我们足够探索一个区域,贝叶斯优化就不会陷入那个区域。现在让我们看看图 1.11 中进行八次查询后会发生什么。

图 1.11

图 1.11 贝叶斯优化成功地忽略了左边的大区域。

在这里,我们观察到了右边的另外两个点,这些点更新了我们的高斯过程模型和我们的策略。观察均值函数(实线,表示最可能的预测),我们看到它几乎与真实目标函数从 4 到 5 的情况完全匹配。此外,我们的策略(底部曲线)现在非常接近全局最优点,基本上没有其他区域。这很有趣,因为我们并没有彻底检查左边的区域(我们只有一个观察到左边的 0),但我们的模型认为与当前区域相比,无论那个区域的函数长什么样子,都不值得调查。在我们的情况下,这实际上是正确的。

最后,在进行了 10 次查询的搜索结束时,我们的工作流现在在图 1.12 中可视化。现在几乎没有疑问,我们已经确定了约为 4.3 的全局最优点。

图 1.12

图 1.12 贝叶斯优化在搜索结束时找到了全局最优点。

这个例子清楚地向我们展示了贝叶斯优化比随机搜索和网格搜索要好得多。这对我们来说应该是一个非常鼓舞人心的迹象,因为后两种策略是许多机器学习实践者在面临超参数调优问题时使用的方法。

例如,scikit-learn 是 Python 中最流行的机器学习库之一,它提供了model_selection模块用于各种模型选择任务,包括超参数调优。然而,随机搜索和网格搜索是该模块实现的唯一超参数调优方法。换句话说,如果我们确实使用随机或网格搜索调整超参数,我们有很大的提升空间。

总的来说,使用 BayesOpt 可能会导致优化性能显著提高。我们可以快速看几个真实世界的例子:

  • 一份名为“贝叶斯优化优于随机搜索的机器学习超参数调优”的 2020 年研究论文(arxiv.org/pdf/2104.10201.pdf)是 Facebook、Twitter、英特尔等公司的联合研究成果,发现 BayesOpt 在许多超参数调优任务中都非常成功。

  • 弗朗西斯·阿诺德(2018 年诺贝尔奖获得者,加州理工学院教授)在她的研究中使用 BayesOpt 来引导寻找高效催化理想化学反应的酶。

  • 一项名为“通过高通量虚拟筛选和实验方法设计高效的分子有机发光二极管”的研究(www.nature.com/articles/nmat4717)发表在自然上,将 BayesOpt 应用于分子有机发光二极管(一种重要类型的分子)的筛选问题,并观察到效率大幅提高。

还有很多类似的例子。

不适用 BayesOpt 的情况

同样重要的是要知道问题设置不合适的情况以及何时使用 BayesOpt。正如我们所说,当我们的有限资源阻止我们多次评估目标函数时,BayesOpt 是有用的。如果不是这种情况,评估目标函数是廉价的,我们没有理由在观察目标函数时吝啬。

如果我们能够彻底检查密集网格上的目标,那将确保找到全局最优解。否则,可能会使用其他策略,例如 DIRECT 算法或进化算法,这些算法在评估成本较低时通常擅长优化。此外,如果目标梯度的信息可用,梯度算法将更适合。

我希望这一章能激发你的兴趣,让你对即将发生的事情感到兴奋。在下一节中,我们将总结你将在本书中学到的关键技能。

1.3 你将在本书中学到什么?

本书深入理解 GP 模型和 BayesOpt 任务。您将学习如何使用最先进的工具和库在 Python 中实现 BayesOpt 流水线。您还将接触到一系列建模和优化策略,当处理 BayesOpt 任务时。到本书结束时,您将能够做到以下几点:

  • 使用 GPyTorch 实现高性能的 GP 模型,这是 Python 中的首选 GP 建模工具;可视化和评估它们的预测;为这些模型选择适当的参数;并实现扩展,例如变分 GP 和贝叶斯神经网络,以适应大数据

  • 使用最先进的 BayesOpt 库 BoTorch 实现各种 BayesOpt 策略,并与 GPyTorch 很好地集成,并检查以及理解它们的决策策略

  • 使用 BayesOpt 框架处理不同的专业化设置,例如批处理、约束和多目标优化

  • 将 BayesOpt 应用于真实任务,例如调整机器学习模型的超参数

进一步地,我们在练习中使用真实世界的例子和数据来巩固我们在每一章学到的知识。在整本书中,我们在许多不同的环境中运行我们的算法在相同的数据集上,以便我们可以比较和分析不同的方法。

摘要

  • 现实世界中的许多问题可以被看作是昂贵的黑盒优化问题。在这些问题中,我们只观察到函数值,没有任何额外的信息。此外,观察一个函数值是昂贵的,使得许多盲目的优化算法无法使用。

  • BayesOpt 是一种机器学习技术,通过设计目标函数的智能评估来解决这个黑盒优化问题,以便尽快找到最优解。

  • 在 BayesOpt 中,GP 充当预测模型,预测给定位置的目标函数的值。GP 不仅生成均值预测,还通过正态分布表示不确定性的 95% CI。

  • 要优化黑盒函数,BayesOpt 策略会迭代地决定在哪里评估目标函数。该策略通过量化每个数据点在优化方面的帮助程度来实现这一点。

  • 在 BayesOpt 中,GP 和策略是相辅相成的。前者用于进行良好的预测,后者用于做出良好的决策。

  • 通过以自适应的方式做出决策,BayesOpt 在优化方面比随机搜索或网格搜索更好,后者通常用作黑盒优化问题中的默认策略。

  • BayesOpt 在机器学习和其他科学应用中,如药物发现中的超参数调优中取得了显著的成功。

第一部分:使用高斯过程建模

预测模型在贝叶斯优化中起着至关重要的作用,通过准确的预测指导决策。正如我们在第 1.2.1 节中看到的,并且在这一部分中一再看到的,高斯过程提供了校准的不确定性量化,这是任何决策任务中的关键组成部分,也是许多机器学习模型缺乏的特性。

我们从第二章开始,该章解释了高斯过程作为函数分布的直观理解,以及作为无限维度中多元正态分布的一般化。我们通过贝叶斯定理探讨了如何更新高斯过程以反映我们对函数值的信念,考虑到新数据。

第三章展示了高斯过程的数学灵活性。这种灵活性使我们,用户,能够通过全局趋势和高斯过程预测的可变性将先验信息合并到预测中。通过结合高斯过程的不同组件,我们获得了数学建模广泛函数的能力。

我们在本部分的讨论中附带了代码实现,使用了最先进的 Python 高斯过程库 GPyTorch。当你阅读本部分的材料时,你将获得使用 GPyTorch 设计和训练高斯过程模型的实践经验。

第三章:高斯过程作为函数分布

本章内容包括

  • 对多元高斯分布及其属性的速成课程

  • 将 GPs 理解为无限维度中的多元高斯分布

  • 在 Python 中实现 GP

现在我们已经看到贝叶斯优化可以帮助我们做什么,我们已经准备好踏上掌握贝叶斯优化的旅程。正如我们在第一章中看到的,贝叶斯优化工作流程由两个主要部分组成:高斯过程(GP)作为预测模型或替代模型,以及用于决策的策略。使用 GP,我们不仅获得测试数据点的点估计作为预测,而且我们有一个完整的概率分布表示我们对预测的信念。

使用 GP,我们从相似的数据点产生相似的预测。例如,在天气预报中,当估计今天的温度时,GP 会查看与今天相似的几天的气候数据,即最近几天或一年前的这一天。另一个季节的天数不会在进行此预测时通知 GP。同样,当预测房屋价格时,GP 将会说预测目标所在地区的相似房屋比另一个州的房屋更具信息量。

数据点之间的相似程度是使用 GP 的协方差函数来编码的,此外,该函数还模拟了 GP 预测的不确定性。请记住,在第一章中我们对比了岭回归模型和 GP 的模型,再次显示在图 2.1 中。在这里,虽然岭回归器只产生单值预测,但 GP 在每个测试点输出一个正态分布。不确定性量化是将 GP 与其他 ML 模型区分开来的因素,特别是在不确定性决策的背景下。

图 2.1 岭回归和 GP 的预测。尽管 GP 的平均预测与岭回归的预测相同,但 GP 还提供了表示预测不确定性的 CI。

我们将看到如何通过高斯分布在数学上实现相关建模和不确定性量化,并学习如何在 GPyTorch 中实际实现 GP,这是 Python 中首选的 GP 建模工具。能够用 GP 对函数进行建模是迈向贝叶斯优化的第一步,我们将在本章中完成这一步。

为什么选择 GPyTorch?

在 Python 中还有其他的 GP 建模库,如 GPy 或 GPflow,但我们选择了 GPyTorch 作为本书的工具。基于 PyTorch 构建且处于积极维护状态,GPyTorch 提供了从数组操作到 GP 建模再到使用 BoTorch 进行贝叶斯优化的简化工作流程,我们将在第四章开始使用 BoTorch。

该库也在积极维护,并且已实现了许多最先进的方法。例如,第十二章介绍了使用 GPyTorch 对大型数据集进行缩放的方法,在第十三章中,我们学习将神经网络集成到 GP 模型中。

2.1 如何以贝叶斯方式出售您的房屋

在我们立即进入高斯过程之前,让我们考虑一个房价建模领域的示例场景,以及房子价格如何与其他房子相关确定的例子。这个讨论作为多元高斯分布中相关性如何工作的示例,是高斯过程的核心部分。

假设你是密苏里州的一位房主,正打算出售你的房子。你正在尝试确定一个合适的要价,并与朋友讨论如何做到这一点:

你:     我不确定该怎么办。我只是不知道我的房子值多少钱。

朋友: 你有个大概的估算吗?

你:     我猜大概在 15 万到 30 万之间。

朋友: 这个范围挺大的。

你:     是啊,我希望我认识已经卖掉房子的人。我需要一些参考。

朋友: 我听说爱丽丝卖了她的房子 25 万。

你:     在加利福尼亚的阿利克斯吗?这真让人吃惊!而且,我不认为加利福尼亚的房子会帮助我更好地估算自己的房子。它可能仍然在 15 万到 30 万之间。

朋友: 不,是住在你隔壁的爱丽丝。

你:     哦,我明白了。这实际上非常有用,因为她的房子和我的非常相似!现在,我猜我的房子估价在 23 万到 27 万之间。是时候和我的房地产经纪人谈谈了!

朋友: 很高兴我能帮上忙。

在这次对话中,你说使用你的邻居爱丽丝的房子作为参考是估算你自己价格的好策略。这是因为这两个房子在属性上相似,并且彼此物理上靠近,所以你期望它们卖出的价格相似。另一方面,阿利克斯的房子位于加利福尼亚,与我们的房子毫不相关,所以即使你知道她的房子卖了多少钱,你也无法获得任何关于你感兴趣的新信息:你自己的房子值多少钱。

我们刚刚进行的计算是关于我们对房子价格的信念的贝叶斯更新。你可能熟悉贝叶斯定理,如图 2.2 所示。有关贝叶斯定理和贝叶斯学习的优秀介绍,请参阅路易斯·塞拉诺的《精通机器学习》(Manning,2021)第八章。

贝叶斯定理给了我们一种更新我们对我们感兴趣的数量的信念的方法,这种数量在这种情况下是我们房子的合适价格。在应用贝叶斯定理时,我们从先验信念,即我们的第一个猜测,到关于所讨论数量的后验信念。这个后验信念结合了先验信念和我们观察到的任何数据的可能性。

图片

图 2.2 贝叶斯定理,它提供了一种更新对感兴趣的数量的信念的方法,表示为一个随机变量的概率分布。在观察到任何数据之前,我们对 X 有先验信念。在使用数据更新后,我们获得了关于 X 的后验信念。

在我们的例子中,我们首先有一个先验置信度,认为房价在 150k 到 300k 之间。正如你的朋友所说的那样,150k 到 300k 的范围很大,所以在这个初始先验置信度中没有太多信息,任何在 150k 到 300k 之间的价格都是可能的。当我们根据两个房子中任意一个的价格的新信息更新这个范围到后验置信度时,一件有趣的事情发生了。

首先,假设 Alix 在加利福尼亚的房子价值为 250k,我们对我们自己房子的后验置信度保持不变:从 150k 到 300k。同样,这是因为 Alix 的房子与我们的房子无关,她的房子的价格也无法告诉我们我们感兴趣的东西的数量。

其次,如果新的信息是 Alice 的房子,它就在我们的旁边,价值为 250k,那么我们的后验置信度就会从先验置信度大幅改变:变为 230k 到 270k 的范围。有了 Alice 的房子作为参考,我们已经根据观察值 250k 更新了我们的置信度,同时缩小了置信度的范围(从 150k 的差异缩小到 40k 的差异)。这是非常合理的事情,因为 Alice 的房子对我们房子的价格非常具有信息量。图 2.3 可视化了整个过程。

图 2.3 以贝叶斯方式更新我们房价的置信度。根据观察的房价与我们房子的相似程度,后验置信度要么保持不变,要么发生 drastīc 更新。

注意,示例中的数字不是精确的,只是为了使例子更具直观性。然而,我们将看到,使用多元高斯分布来建模我们的置信度,可以以可量化的方式实现这种直观的更新过程。此外,利用这样的高斯分布,我们可以确定一个变量(某人的房子)是否与我们感兴趣的变量(我们自己的房子)足够相似,以影响我们的后验置信度的程度。

2.2 用多元高斯分布和贝叶斯更新建模相关性

在本节中,我们学习多元高斯分布(或多元高斯分布,或简称高斯分布)以及它们如何促进我们之前看到的更新规则。这为我们后续讨论高斯过程奠定了基础。

2.2.1 使用多元高斯分布共同建模多个变量

在这里,我们首先介绍了多元高斯分布是什么以及它们可以模拟的内容。我们将看到,通过使用协方差矩阵,多元高斯分布描述了不仅是单个随机变量的行为,而且还描述了这些变量之间的相关性。

首先,让我们来考虑正态分布,也叫钟形曲线。正态分布在现实世界中非常常见,被用来模拟各种量,比如身高、智商、收入和出生体重。

当我们想要建模多于一个量时,我们将使用 MVN 分布。为此,我们将这些量聚合成一个随机变量向量,然后称此向量遵循 MVN 分布。这个聚合如图 2.4 所示。

图 2.4 MVN 分布将多个正态分布的随机变量组合在一起。虽然 MVN 的均值向量连接了均值,但协方差矩阵模拟了各个变量之间的相关性。

定义 考虑一个随机向量X = [X[1]X[2] ... X[n]],它遵循一个被标记为N(μ, Σ)的高斯分布,其中μ是长度为n的向量,Σ是一个n乘以n的矩阵。在这里,μ被称为均值向量,其各个元素表示X中相应随机变量的期望值,Σ是协方差矩阵,描述了各个变量的方差以及变量之间的相关性。

让我们花一点时间解析 MVN 分布的定义:

  • 首先,由于 MVN 的便利性质,向量X中的每个随机变量都遵循正态分布。具体来说,第i个变量X[i]的平均值为μ[i],这是 MVN 的均值向量μ的第i个元素。

  • 此外,X[i]的方差是协方差矩阵Σ的第i对角条目。

  • 如果我们有一个遵循 MVN 的随机变量向量,那么每个单独的变量对应于一个已知的正态分布。

如果协方差矩阵Σ中的对角线条目是各个变量的方差,那么非对角线条目呢?该矩阵中第i行和第j列的条目表示X[i]X[j]之间的协方差,这与两个随机变量之间的相关性有关。假设相关性为正,则以下结论成立:

  • 如果这种相关性很高,那么两个随机变量X[i]X[j]被认为是相关的。这意味着如果一个值增加,另一个值也倾向于增加,如果一个值减少,另一个值也会减少。你的邻居爱丽丝的房子和你自己的房子就是相关变量的例子。

  • 另一方面,如果这种相关性很低且接近零,则无论X[i]的值是什么,我们关于X[j]值的了解很可能不会发生太大变化。这是因为两个变量之间没有相关性。加利福尼亚州的阿利克斯的房子和我们的房子属于这个类别。

负相关性

前面的描述是针对正相关性的。相关性也可以是负的,表示变量朝相反的方向移动:如果一个变量增加,另一个变量就会减少,反之亦然。正相关性展示了我们在这里想要学习的重要概念,所以我们不会担心负相关性的情况。

为了使我们的讨论更具体,让我们定义一个 MVN 分布,同时对三个随机变量进行建模:我们房子的价格 A;邻居爱丽丝房子的价格 B;以及加利福尼亚的阿利克斯房子的价格 C。这个三维高斯分布的协方差矩阵也在图 2.4 中描述。

注意 通常方便假设这个高斯分布的均值向量归一化为零向量。这种归一化通常在实践中完成,以简化许多数学细节。

再次,对角线单元格告诉我们单个随机变量的方差。B 的方差(3)略大于 A(1),这意味着我们对 B 的值更不确定,因为我们对邻居的房子不了解所有情况,所以不能做出更准确的估计。另一方面,第三个变量 C 具有最大的方差,表示加利福尼亚的房屋价格范围更广。

注意 这里使用的值(1, 3, 10)是示例值,目的是说明随机变量方差越大,我们对该变量的值越不确定(在了解其值之前)。

我们家(A)和邻居家(B)之间的协方差为 0.9,这意味着两栋房子的价格存在着显著的相关性。这是有道理的,因为如果我们知道邻居房子的价格,我们就能更好地估算出我们自己房子的价格,因为它们位于同一条街上。还要注意的是,无论是 A 还是 B 与加利福尼亚房价都没有任何相关性,因为位置上来看,C 与 A 或 B 没有任何共同之处。另一种说法是,即使我们知道加利福尼亚房子的价格,我们也不会对我们自己房子的价格了解多少。现在让我们在图 2.5 中使用平行坐标图来可视化这个三维高斯分布。

图 2.5 平行坐标图可视化了来自房价示例的均值归一化 MVN。误差条表示相应正态分布的 95% CI,而淡化的线显示了从多元高斯中绘制的样本。

注意图中的粗体菱形和相应的误差条:

  • 粗体菱形代表我们高斯分布的均值向量,即零向量。

  • 误差条表示三个单独变量的 95%可信区间(CI)。从 A 到 B 到 C,我们观察到越来越大的 CI,对应着相应方差的增加。

可信区间

正态分布随机变量x的(1 – α) CI 是一个范围,其中x落入这个范围的概率恰好为(1 – α)。统计学家通常对正态分布使用 95% CI。这里并没有什么特别之处,只是因为 95% 是许多统计程序用来确定某事是否有意义的阈值。例如,一个t检验通常使用置信水平 1 – α = 0.95,对应着p值小于 α = 0.05 表示显著结果。关于正态分布的一个方便事实是μ ± 1.96σ是一个 95% CI(有些甚至使用μ ± 2σ),其中μ和σ是变量x的均值和标准差,这是一个容易计算的量。

图 2.5 表示我们关于三栋房子标准化价格的先验信念。从这个先验开始,我们猜测所有三栋房子的标准化价格都为零,并且对我们的猜测有不同程度的不确定性。此外,由于我们正在处理一个随机分布,我们可以从这个 MVN 中抽取样本。这些样本显示为相连的半透明菱形。

2.2.2 更新 MVN 分布

有了一个 MVN 分布,我们将看到如何在本小节中观察到一些数据后更新这个分布。具体地说,跟随本章开头的示例,我们想要根据观察到的 B 或 C 的值推导出关于这些价格的后验信念。这是一个重要的任务,因为这是 MVN 以及 GP 从数据中学习的方式。

定义 这个更新过程有时被称为条件设定:推导出一个变量的条件分布,在我们已知某个其他变量的值的情况下。更具体地说,我们正在将我们的信念——一个联合三元高斯——条件设定为 B 或 C 的值,获得这三个变量的联合后验分布。

在这里,利用图 2.2 中的贝叶斯定理,我们可以得出这个后验分布的闭式形式。然而,推导过程相当数学密集,所以我们不会在这里详细介绍。我们只需要知道,我们有一个公式,可以插入我们想要条件的 B 或 C 的值,然后这个公式会告诉我们 A、B 和 C 的后验分布是什么。令人惊讶的是,高斯的后验分布是根据同样是高斯的数据进行条件设定的,我们可以获得指定后验高斯的确切后验均值和方差。(在本章后面,我们会看到当我们在 Python 中实现 GP 时,GPyTorch 会为我们处理这个数学密集的更新。)

注意 对于感兴趣的读者,这个公式及其推导可以在 Carl Edward Rasmussen 和 Christopher K. I. Williams(MIT Press,2006)的书籍Gaussian Processes for Machine Learning的第二章第二部分中找到,这本书通常被认为是 GP 的圣经。

现在让我们重新生成平行坐标图,以 B = 4 作为 B 的示例值进行条件限制。结果如图 2.6 所示。

图 2.6 平行坐标图可视化了图 2.5 中 MVN 在 B = 4 条件下的情况。在这里,A 的分布被更新,所有绘制的样本都插值为 B = 4。

在用关于 B 的观察更新我们的信念后,我们的后验信念发生了一些变化:

  • A 的分布会发生变化,由于 A 和 B 之间的正相关关系,其均值会略微增加。此外,其误差范围现在变得更小。

  • B 的后验分布简单地变成了一个具有零方差的特殊正态分布,因为我们现在对后验中的 B 值完全确定。换句话说,对于 B 的值不再存在不确定性。

  • 与此同时,C 的分布在更新后保持不变,因为它与 B 没有相关性。

所有这些都是有道理的,并且与我们从房价示例中得到的直觉相符。具体来说,当我们得知邻居的房子价格时,关于我们自己房子的信念被更新为类似于观察到的价格,并且我们的不确定性也减少了。

当我们以 C 为条件时会发生什么?正如您可能猜到的那样,由于在 B 的值上进行条件限制后 C 保持不变的原因,当我们以 C 为条件时,A 和 B 的后验分布都保持不变。图 2.7 展示了 C = 4 的情况。

图 2.7 平行坐标图可视化了图 2.5 中 MVN 在 C = 4 条件下的情况。在这里,没有其他边缘分布改变。所有绘制的样本都插值为 C = 4。

当我们得知加利福尼亚州的一栋房子被卖掉时,我们发现这栋房子与我们在密苏里州的房子无关,因此对我们房子价格的信念保持不变。

图 2.6 和图 2.7 还有另一个有趣之处。请注意,在图 2.6 中,当我们以 B = 2 为条件时,我们绘制的所有后验 MVN 的样本都经过点 (B, 2)。这是因为在我们的后验信念中,我们对于 B 取值不再有任何不确定性,因此从后验分布中绘制的任何样本都需要满足此条件的约束。图 2.7 中的点 (C, 2) 也是同样道理。

从视觉上来看,您可以将这理解为当我们对一个变量进行条件限制时,我们将从先验分布(在图 2.5 中)绘制的样本在相应的变量条件处“绑定”成一个结,如图 2.8 所示。

图 2.8 在观察上对高斯进行条件限制类似于在该观察周围打结。后验分布中的所有样本都需要通过这个结,且在观察点没有不确定性。

最后,我们可以通过类似于图 2.3 的图表来描述我们刚刚进行过的贝叶斯条件过程。这在图 2.9 中显示。

图 2.9:以贝叶斯方式更新关于我们房子价格的信念。根据观察到的房价与我们房子的相似程度,后验信念要么保持不变,要么得到极大更新。

再次,如果我们将高斯分布条件设置为 C,则不相关变量的后验分布保持不变。然而,如果我们将其设置为 B,与之相关的变量 A 会得到更新。

2.2.3:用高维高斯分布建模多个变量

MVN 分布不仅需要包含三个随机变量;事实上,它可以同时模拟任意数量的变量。在本小节中,我们了解到高维高斯分布的工作方式与我们迄今所见的相同。所以,我们可以说,一个代表三座房子的 3 维高斯分布,我们可以将其替换为一个编码街道上一系列房屋信息的 20 维高斯分布。甚至更高维的高斯分布可以模拟城市或国家中的房屋。

此外,通过这些平行坐标图,我们可以同时可视化高维高斯分布的所有单个变量。这是因为每个变量对应一个单独的误差条,只占用x轴上的一个位置。

我们再次将其均值向量归一化为零向量,虽然显示其 20×20 的协方差矩阵不方便,但我们可以绘制一个热图来可视化这个矩阵,就像图 2.10 中所示的那样。

图 2.10:显示 20 维高斯分布的协方差矩阵的热图。相邻变量之间的相关性比远离的变量更大,这由较深的色调表示。

对角线条目,或者单个变量的方差,在这种情况下都是 1。此外,变量被排序,使得彼此靠近的变量是相关的;也就是说,它们的协方差取值较大。相反,彼此远离的变量则不太相关,它们的协方差接近于零。例如,在这个高斯分布中的任意一对连续变量(第一个和第二个,第二个和第三个等等)的协方差大约为 0.87。也就是说,任意两个相邻的房子的协方差为 0.87。如果我们考虑第 1 个和第 20 个变量——也就是,街道一端的房子和另一端的房子——它们的协方差实际上是零。

这非常直观,因为我们期望附近的房子价格相似,所以一旦我们知道一座房子的价格,我们就可以更多地了解该地区周围房屋的价格,而不是远处房屋的价格。

图 2.11 误差线和从先验(左)和后验(右)高斯分布中抽取的样本,以第 10 个变量的值为 2 条件。接近第 10 个变量的变量在后验中的不确定性减少,其均值更新为接近 2。

这在平行坐标图中如何体现?图 2.11 显示了我们左侧的先验高斯和右侧第 10 个变量的值为 2 的后验高斯。基本上,我们正在模拟以下事件,即我们发现第 10 座房子的价格为 2(其确切单位被省略):

  • 首先,我们再次看到这种现象,在后验分布中,误差线和样本围绕我们条件观察到的观察点打成结。

  • 其次,由于协方差矩阵施加的相关结构,接近第 10 个变量的变量其均值被“拉高”,以便均值向量现在平滑地插值到点 (10, 2)。这意味着我们更新了我们的信念,因为周围的房屋现在其价格增加以反映我们学到的信息。

  • 最后,在这一点 (10, 2) 条件之后,围绕这一点的不确定性(由误差线表示)减小。这是一个非常好的属性,因为直觉上,如果我们知道一个变量的值,我们应该对与我们所知变量相关的其他变量的值更加确定。也就是说,如果我们知道一座房子的价格,我们就会更加确定附近房屋的价格。这个属性是高斯过程提供的校准不确定性量化的基础,我们在本章的下一节中会看到。

2.3 从有限到无限的高斯分布

现在我们准备讨论什么是高斯过程。与前面三个变量 A、B 和 C,或者之前章节中的 20 个变量相同的方式,我们假设现在有无限多个变量,所有这些变量都属于多元正态分布。这个 无限维 高斯被称为 高斯过程

想象一下在一个非常大而密集的区域内预测房价。整个区域的规模如此之大,以至于如果我们离一座房子移动了一个非常小的距离,我们就会到达另一座房子。鉴于高斯分布中变量(房屋)的高密度,我们可以将整个区域视为有无限多个房屋;也就是说,高斯分布有无限多个变量。

图 2.12 使用不同数量的变量对加州房价建模。我们拥有的变量越多,我们的模型就越平滑,我们越接近无限维模型。

这在图 2.12 中使用一个包含加利福尼亚州 5,000 个房价的数据集进行了说明。在左上角的面板中,我们展示了散点图中的个别数据点。在其余的面板中,我们使用各种数量的变量来对数据进行建模,其中每个变量对应于加利福尼亚地图内的一个区域。随着变量数量的增加,我们的模型变得更加精细。当这个数量是无限的时候——也就是说,当我们可以在这张地图上的任何区域进行预测,无论多么小——我们的模型存在于一个无限维空间中。

这正是高斯过程的含义:在无限维空间中的高斯分布。在任何区域进行预测的能力帮助我们摆脱了有限维多元正态分布,并获得了一个机器学习模型。严格来说,当变量有无穷多个时,高斯分布的概念并不适用,因此技术上的定义如下。

定义 高斯过程是一组随机变量,使得这些变量的任意有限子集的联合分布是一个多元正态分布。

这个定义意味着,如果我们有一个高斯过程模型来描述一个函数 ƒ,那么在任何一组点处的函数值都由一个多元正态分布来建模。例如,变量向量 [ƒ(1) ƒ(2) ƒ(3)] 遵循一个三维高斯分布;[ƒ(1) ƒ(0) ƒ(10) ƒ(5) ƒ(3)] 遵循另一个不同的、五维的高斯分布;以及 [ƒ(0.1) ƒ(0.2) ... ƒ(9.9) ƒ(10)] 遵循另一个高斯分布。

图 2.13 不同高斯分布的平行坐标图。高斯过程的任何有限子集都是多元正态分布。随着变量数量趋于无穷,我们得到一个高斯过程,并且可以在域中的任何地方进行预测。

这在图 2.13 中有所说明。前三个面板显示了平行坐标图中的三元高斯分布,分别为 [ƒ(–2) ƒ(1) ƒ(4)],一个 11 元高斯分布,对应 [ƒ(–4.5) ƒ(–4) ... ƒ(4) ƒ(4.5)],以及一个在更密集的网格上的 101 元高斯分布。最后,在最后一个面板中,我们有无限多个变量,这给了我们一个高斯过程。

由于我们现在处于无限维空间,讨论均值向量和协方差矩阵已经没有意义了。相反,我们在高斯过程中有的是一个均值 函数 和一个协方差 函数,但这两个对象的角色仍然与多元正态分布相同:

  • 首先是均值函数,它接受一个输入 x,计算函数值 ƒ(x) 的期望。

  • 第二,协方差函数接受两个输入,x[1] 和 x[2],并计算两个变量 ƒ(x[1]) 和 ƒ(x[2]) 之间的协方差。如果 x[1] 等于 x[2],那么这个协方差值就是 ƒ(x) 的正态分布的方差。如果 x[1] 不同于 x[2],协方差表示两个变量之间的相关性。

由于均值和协方差是函数,我们不再受限于固定数量的变量——相反,我们有效地拥有无限数量的变量,并且可以在任何地方进行我们的预测,如图 2.13 所示。这就是为什么尽管高斯过程具有多元正态分布的所有特性,但高斯过程存在于无限维度的原因。

出于同样的原因,高斯过程可以被视为对函数的分布,就像本章的标题所暗示的那样。本章我们经历的从一维正态分布到高斯过程的过程,在表 2.1 中总结。

表 2.1 高斯分布对象及其模拟对象。使用高斯过程时,我们在无限维度下操作,模拟函数而不是数字或向量。

分布类型 模拟变量数量 描述
一维正态分布 一个 数字的分布
多元正态分布 有限数量 有限长度向量的分布
高斯过程 无限数量 函数的分布

要看高斯过程的实际应用,让我们重新审视本章开头图 2.1 中的曲线拟合过程,我们将我们的域限制在-5 到 5 之间。如图 2.14 所示。

图 2.14 高斯过程对零、一个、两个和四个观测条件下的预测

在每个面板中,以下内容为真:

  • 中间的实线是平均函数,类似于图 2.11 中连接菱形的实线。

  • 另一方面,阴影区域是域上的 95% CI,对应于图 2.11 中的误差条。

  • 各种曲线是从相应高斯过程中抽取的样本。

在观察任何数据之前,我们从左上角的先验高斯过程开始。就像先验多元正态分布一样,在没有训练数据的情况下,我们的先验高斯过程产生恒定的均值预测和不确定性。这是一个合理的行为。

当我们将高斯过程与各种数据点进行条件化时,有趣的部分就出现了。这在图 2.14 剩余的面板中进行了可视化。正如多元正态分布的离散情况一样,高斯过程在连续域中工作时,均值预测以及从后验分布中抽取的样本在训练集的数据点之间平滑插值,而关于函数值的不确定性,由置信区间(CI)量化,在这些观测点周围的区域平滑减少。这就是我们所说的校准不确定性量化,这是高斯过程的最大卖点之一。

高斯过程的平滑性

平滑性 特性是指要求相似点彼此相关的约束。换句话说,相似的点应该产生相似的函数值。这也是为什么当我们在顶部右侧面板的数据点处于 3 时,2.9 和 3.1 处的平均预测会更新为比它们的先验均值大的原因。这些点,2.9 和 3.1,与 3 相似,因为它们彼此接近。这种平滑性是通过 GP 的协方差函数来设置的,这是第三章的主题。虽然我们迄今为止看到的示例都是一维的,但当我们的搜索空间是更高维度时,这种平滑性仍然保持,正如我们后面看到的那样。

总的来说,我们已经看到,GP 在扩展到无限维度时是多元正态分布,由于高斯分布具有许多便利的数学特性,GP 不仅产生平均预测,而且通过其预测协方差以一种原则性的方式量化了我们对函数值的不确定性。平均预测正好通过训练数据点,并且不确定性在这些数据点处收敛。

建模非高斯数据

在现实生活中,并不是所有的数据都遵循高斯分布。例如,对于限制在数值范围内的值或不遵循钟形分布的变量,高斯分布是不合适的,可能会导致低质量的预测。

在这些情况下,我们可以应用各种数据处理技术来“转换”我们的数据点以遵循高斯分布。例如,Box-Muller 变换是一个从均匀分布的随机数生成一对正态分布的随机数的算法。有兴趣的读者可以在 Wolfram 的 MathWorld 上找到有关此算法的更多详细信息 (mathworld.wolfram.com/Box-MullerTransformation.xhtml)。

2.4 在 Python 中实现 GPs

在本章的最后一节中,我们迈出了实现 Python 中高斯过程(GPs)的第一步。我们的目标是熟悉我们将用于此任务的库的语法和 API,并学习如何重新创建我们迄今为止看到的可视化效果。这个动手实践部分还将帮助我们更深入地理解 GPs。

首先,确保您已经下载了本书的附带代码并安装了必要的库。关于如何做到这一点的详细说明包含在前言中。我们使用包含在 Jupyter 笔记本 CH02/01 - Gaussian processes.ipynb 中的代码。

2.4.1 设置训练数据

在我们开始实现 GP 模型的代码之前,让我们先花一些时间创建我们想要建模的目标函数和训练数据集。为此,我们需要导入 PyTorch 用于计算和操作张量,以及 Matplotlib 用于数据可视化:

import torch
import matplotlib.pyplot as plt

在本示例中,我们的目标函数是一维的 Forrester 函数。Forrester 函数是具有一个全局最大值和一个局部最大值的多模式函数(www.sfu.ca/~ssurjano/forretal08.xhtml),使得拟合和找到函数的最大值成为一个非平凡的任务。该函数具有以下公式:

这是如下实现的:

def forrester_1d(x):
    y = -((x + 1) ** 2) * torch.sin(2 * x + 2) / 5 + 1
    return y.squeeze(-1)

让我们快速在图表中绘制此函数。在这里,我们限制自己在-3 到 3 之间的域,并在此范围内的 100 个点的密集网格上计算此 Forrester 函数。我们还需要一些用于训练的样本点,我们通过torch.rand()进行随机采样并将其存储在train_x中;train_y包含这些训练点的标签,可以通过评估forrester_1d(train_x)获得。此图由以下代码生成,产生图 2.15:

xs = torch.linspace(-3, 3, 101).unsqueeze(1)
ys = forrester_1d(xs)

torch.manual_seed(0)
train_x = torch.rand(size=(3, 1)) * 6 - 3
train_y = forrester_1d(train_x)

plt.figure(figsize=(8, 6))

plt.plot(xs, ys, label="objective", c="r")
plt.scatter(train_x, train_y, marker="x", c="k", label="observations")

plt.legend(fontsize=15);

图 2.15 显示的是当前示例中使用的目标函数,如实线所示。标记表示训练数据集中的点。

我们看到的三个标记是我们随机选择包含在我们的训练数据集中的点。这些训练数据点的位置存储在train_x中,它们的标签(这些位置处的 Forrester 函数的值)存储在train_y中。这设置了我们的回归任务:在这三个数据点上实现和训练一个 GP,并在范围在-3 到 3 之间的点上可视化其预测。在这里,我们还创建了xs,这是在此范围内的密集网格。

2.4.2 实现 GP 类

在本小节中,我们将学习如何在 Python 中实现 GP 模型。我们使用 GPyTorch 库,这是一种现代 GP 建模的最先进工具。

重要提示:GPyTorch 的设计理念是遵循 DL 库 PyTorch,并使其所有模型类扩展基础模型类。如果您熟悉在 PyTorch 中实现神经网络,您可能会知道这个基类是torch.nn.Module。使用 GPyTorch,我们通常扩展gpytorch.models.ExactGP类。

要实现我们的模型类,我们使用以下结构:

import gpytorch

class BaseGPModel(gpytorch.models.ExactGP):
    def __init__(self, train_x, train_y, likelihood):
        ...

    def forward(self, x):
        ...

在这里,我们实现了一个名为BaseGPModel的类,它有两个特定方法:__init__()forward()。我们的 GP 模型的行为在很大程度上取决于我们如何编写这两个方法,无论我们想要实现什么样的 GP 模型,我们的模型类都需要具有这些方法。

让我们首先讨论__init__()方法。它的作用是接收由第一个和第二个参数train_xtrain_y定义的训练数据集,以及存储在变量likelihood中的似然函数,并初始化 GP 模型,即一个BaseGPModel对象。我们实现该方法如下:

def __init__(self, train_x, train_y, likelihood):
        super().__init__(train_x, train_y, likelihood)
        self.mean_module = gpytorch.means.ZeroMean()
        self.covar_module = gpytorch.kernels.RBFKernel()

在这里,我们简单地将三个输入参数传递给我们的超类的__init__()方法,而gpytorch.models.ExactGP的内置实现则为我们处理了繁重的工作。剩下的是均值和协方差函数的定义,正如我们所说的,这是高斯过程的两个主要组成部分。

在 GPyTorch 中,对于均值和协方差函数都有广泛的选择,我们将在第三章中进行探讨。现在,我们使用 GP 的最常见选项:

  • 对于均值函数,我们使用gpytorch.means.ZeroMean(),在先验模式下输出零均值预测

  • 对于协方差函数,我们使用gpytorch.kernels.RBFKernel(),它实现了径向基函数(RBF)核——这是高斯过程中最常用的协方差函数之一,它实现了数据点彼此接近时彼此相关的思想。

我们分别将这些对象存储在mean_modulecovar_module类属性中。这就是我们对__init__()方法需要做的全部。现在,让我们将注意力转向forward()方法。

forward()方法非常重要,因为它定义了模型如何处理其输入。如果您在 PyTorch 中使用过神经网络,那么您知道网络类的forward()方法会依次将其输入通过网络的层,并且最终层的输出就是神经网络产生的内容。在 PyTorch 中,每个层都被实现为一个模块,这是一个表示 PyTorch 中任何处理数据的对象的基本构建块的术语。

在 GPyTorch 中,高斯过程的forward()方法工作方式类似:高斯过程的均值和协方差函数被实现为模块,并且该方法的输入被传递给这些模块。我们不是将结果依次通过不同的模块传递,而是同时将输入传递给均值和协方差函数。这些模块的输出然后被组合以创建一个多变量正态分布。PyTorch 和 GPyTorch 之间的这种差异在图 2.16 中说明。

图 2.16 PyTorch 和 GPyTorch 如何在其各自的forward()方法中处理数据的过程。输入由不同的模块处理以产生最终输出,对于前馈神经网络来说,输出是一个数字,对于 GP 来说是一个多变量正态分布。

forward()方法在以下代码中实现:

def forward(self, x):
        mean_x = self.mean_module(x)
        covar_x = self.covar_module(x)
        return gpytorch.distributions.MultivariateNormal(mean_x, covar_x)

这里的逻辑非常简单:因为我们有一个均值函数和一个协方差函数,所以我们只需在输入x上调用它们来计算均值和协方差预测。最后,我们需要返回的是一个 MVN 分布,由gpytorch.distributions.MultivariateNormal类实现,带有相应的均值和协方差。换句话说,我们只是根据模型类的mean_ modulecovar_module属性计算出的均值向量和协方差矩阵创建了一个 MVN 分布。

就是这样!用 GPyTorch 实现 GP 模型是多么容易,令人惊讶。对我们来说最大的收获是,我们需要在 __init__() 方法中实现均值和协方差函数。在 forward() 方法中,当我们需要进行预测时,我们只需在输入上调用这两个函数即可。

2.4.3 用 GP 进行预测

有了 BaseGPModel 类,我们可以开始用 GP 进行预测了!回想一下,在 __init__() 方法中,我们需要传递一个似然函数 likelihood,以及我们的训练数据。在许多回归任务中,gpytorch.likelihoods.GaussianLikelihood 对象就足够了。我们可以这样创建这个对象:

likelihood = gpytorch.likelihoods.GaussianLikelihood()

现在,我们可以初始化我们的 BaseGPModel 对象了。但在我们将其初始化为我们的三项训练数据之前,我们首先想要用先验 GP 进行预测。

要初始化一个没有任何训练数据的 GP 对象,我们将 None 传递给训练特征 (train_x) 和标签 (train_y)。因此,我们的先验 GP 被创建如下:

model = BaseGPModel(None, None, likelihood)

最后,在我们进行任何预测之前,需要进行一些簿记工作。首先,我们设置 GP 的超参数:

lengthscale = 1
noise = 1e-4

model.covar_module.lengthscale = lengthscale
model.likelihood.noise = noise

model.eval()
likelihood.eval()

我们将在第三章讨论每个超参数控制的内容。目前,我们只使用我个人喜欢的默认值:长度尺度为 1,噪声方差为 0.0001。最后一个细节是通过调用相应对象的 eval() 方法在 GP 模型和其似然性中启用预测模式。

处理完这些簿记任务后,我们现在终于可以在我们的测试数据上调用这个 GP 模型进行预测了。我们可以这样做:

with torch.no_grad():
    predictive_distribution = likelihood(model(xs))

请记住,在模型类的 forward() 方法中,我们返回 MVN 分布,因此当我们通过 model(xs) 使用我们的模型传递一些测试数据时,这就是输出。(在 PyTorch 的语法中,调用 model(xs) 是对测试数据 xs 调用 forward() 方法的一种简写。)我们还将相同的输出通过似然函数 likelihood,将噪声方差合并到我们的预测中。简而言之,我们在 predictive_distribution 中存储的是代表测试点 xs 的预测的 MVN 分布。此外,我们在 torch.no_grad() 上下文中计算此对象,当我们不希望 PyTorch 跟踪这些计算的梯度时,这是一个好的做法。

注意 我们只想在优化模型参数时计算操作的梯度。但是当我们想要进行预测时,我们应该完全固定我们的模型,因此禁用梯度检查是适当的。

2.4.4 可视化 GP 的预测

有了这个预测的高斯分布,我们现在可以重新创建我们迄今为止见过的 GP 图。这些图中的每一个都包括一个均值函数 μ,我们可以从 MVN 中获取

predictive_mean = predictive_distribution.mean

另外,我们想显示 95% 置信区间。从数学上讲,这可以通过提取预测协方差矩阵Σ的对角元素来完成(请记住,这些元素表示单个方差σ²),取这些值的平方根以计算标准差σ,并计算 μ ± 1.96σ 的置信区间范围。

幸运的是,当我们使用 GP 时,计算 95% 置信区间是一个常见的操作,所以 GPyTorch 提供了一个方便的辅助方法,称为 confidence_ region(),我们可以直接从 MVN 分布对象中调用该方法:

predictive_lower, predictive_upper =
    ➥    predictive_distribution.confidence_region()

此方法返回两个 Torch 张量的元组,分别存储置信区间的下限和上限端点。

最后,我们可能希望从当前 GP 模型中为我们的图形绘制样本。我们可以通过直接从高斯对象 predictive_distribution 调用方法 sample() 来完成这个操作。如果我们不传入任何输入参数,该方法将返回一个样本。在这里,我们希望从我们的 GP 中抽取五次样本,操作如下:

torch.manual_seed(0)
    samples = predictive_distribution.sample(torch.Size([5]))

我们传递一个 torch.Size() 对象以表示我们希望返回五个样本。在抽样之前设置随机种子是一种确保代码可重现性的良好实践。有了这些,我们就可以开始绘制一些图形了!

第一件事是简单地绘制均值函数:

plt.plot(xs, predictive_mean.detach(), label="mean")

至于 95% 置信区间,我们通常使用像我们到目前为止看到的那样的阴影区域,这可以使用 Matplotlib 的 fill_between() 函数完成:

plt.fill_between(
    xs.flatten(),
    predictive_upper,
    predictive_lower,
    alpha=0.3,
    label="95% CI"
)

最后,我们绘制单独的样本:

for i in range(samples.shape[0]):
    plt.plot(xs, samples[i, :], alpha=0.5)

此代码将生成图 2.17 中的图形。

图 2.17 先验 GP 的预测,具有零均值和 RBF 核。虽然均值和置信区间是恒定的,但单个样本表现出复杂的非线性行为。

我们看到,在整个域上,我们的先验 GP 产生的均值函数在零处保持恒定,我们的 95% 置信区间也是恒定的。这是可以预期的,因为我们使用了一个 gpytorch .means.ZeroMean() 对象来实现均值函数,并且在没有任何训练数据的情况下,我们的先验预测默认为此 0 值。

话虽如此,均值和置信区间只是期望的测量值:它们表示我们预测的平均行为在许多不同的实现中的平均行为。然而,当我们绘制单个样本时,我们会看到每个样本都有一个非常复杂的形状,根本不是恒定的。所有这些都是说,虽然我们在任何点的预测期望值为零,但它可能取值范围很广。这表明 GP 可以以灵活的方式对复杂的非线性行为进行建模。

到目前为止,我们已经学会了在没有任何训练数据的情况下生成和可视化预测的先前 GP 的过程。现在,让我们实际上在我们随机生成的训练集上训练一个 GP 模型,并观察预测结果的变化。迄今为止,我们编码的好处就是这一切可以完全重复进行,只不过现在我们要用我们的训练数据来初始化 GP(请记住,之前我们在第一个和第二个参数中使用了 None):

model = BaseGPModel(train_x, train_y, likelihood)

重新运行代码将给我们 Figure 2.18。

图 2.18 由后验 GP 生成的预测。均值函数和随机绘制样本能很好地插值训练数据点,同时不确定性在这些数据点周围消失。

这正是我们想要看到的预测类型:均值线和样本完美地插值了我们观测到的数据点,并且我们的不确定性(由 CI 表示)在这些数据点周围也减小了。

我们已经可以看到这种不确定性量化在建模目标函数方面的作用。在仅观测到三个数据点之后,我们的 GP 对真实目标函数有一个相当好的近似。实际上,几乎所有的目标函数都在 95% CI 内,表明我们的 GP 成功地考虑了目标函数的行为方式,即使在我们尚未从函数中获得任何数据的区域也是如此。这种校准的量化在我们实际需要根据 GP 模型做出决策的情况下特别有益——也就是说,在我们决定在哪些点上观察函数值以找到最优值时——但是让我们将其保留到本书的下一部分。

2.4.5 超越一维目标函数

到目前为止,我们只看到了在一维目标函数上训练的 GP 的示例。然而,GP 并不局限于一个维度。实际上,只要我们的均值和协方差函数能够处理高维输入,GP 就可以在高维上高效地运算。在本小节中,我们将学习如何训练一个基于二维数据集的 GP。

我们按照前一节的步骤进行。首先,我们需要一个训练数据集。在这里,我将自行创建一个带有点(0, 0),(1, 2),和(–1, 1)以及相应标签为 0,–1 和 0.5 的虚拟集合。换句话说,我们从中学习的目标函数在(0, 0)处的值为 0,在(1, 2)处的值为–1,在(–1, 1)处的值为 0.5。我们希望在[–3, 3]-by-[–3, 3]的正方形内进行预测。

在 Python 中的设置如下:

# training data
train_x = torch.tensor(
    [
        [0., 0.],
        [1., 2.],
        [-1., 1.]
    ]
)

train_y = torch.tensor([0., -1., 0.5])

# test data
grid_x = torch.linspace(-3, 3, 101)               ❶

grid_x1, grid_x2 = torch.meshgrid(grid_x, grid_x,
➥indexing="ij")                                  ❷
xs = torch.vstack([grid_x1.flatten(), grid_x2.flatten()]).transpose(-1, -2)

❶ 一维点阵

❷ 二维点阵

变量 xs 是一个 10,201×2 的矩阵,其中包含了我们希望进行预测的正方形上的点阵。

重要提示:这里有 10,201 个点,因为我们在两个维度中各取了一个 101 点的网格。现在,我们只需重新运行之前用于训练 GP 并对这个二维数据集进行预测的 GP 代码。请注意,不需要修改我们的BaseGPModel类或任何预测代码,这非常了不起!

不过,我们确实需要更改的一件事是如何可视化我们的预测。由于我们正在操作二维空间,因此在单个图中绘制预测均值和 CI 变得更加困难。在这里,一个典型的解决方案是绘制预测均值的热力图和预测标准差的另一个热力图。虽然标准差不完全等于 95% CI,但这两个对象实质上量化了同样的内容:我们对函数值的不确定性。

因此,我们现在不再调用predictive_distribution.confidence_region(),而是这样提取预测标准差:

predictive_stddev = predictive_distribution.stddev

现在,要绘制热力图,我们使用 Matplotlib 中的imshow()函数。我们需要注意predictive_meanpredictive_stddev的形状。这里是一个长度为 10,000 的张量,因此在传递给imshow()函数之前,需要将其重塑为一个方阵。可以按如下方式完成:

fig, ax = plt.subplots(1, 2)

ax[0].imshow(
    predictive_mean.detach().reshape(101, 101).transpose(-1, -2),
    origin="lower",
    extent=[-3, 3, -3, 3]
)                           ❶

ax[1].imshow(
    predictive_stddev.detach().reshape(101, 101).transpose(-1, -2),
    origin="lower",
    extent=[-3, 3, -3, 3]
)                           ❷

❶ 预测均值的第一个热力图

❷ 预测标准差的第二个热力图

此代码生成了图 2.19 中的两个热力图。

图 2.19 由二维 GP 进行的预测。均值函数仍然与训练数据一致,并且在这些数据点周围的区域,不确定性再次消失。

我们看到,在一维情况下我们所拥有的东西在这个例子中也有所延续:

  • 左侧面板显示我们的均值预测与训练数据一致:左侧的亮斑点对应于 (-1, 1),其值为 0.5,而右侧的暗斑点对应于 (1, 2),其值为 -1(我们在 (0, 0) 处的观测值为 0,这也是先验均值,因此在左侧面板中并不像其他两个那样明显)。

  • 我们的不确定性(由预测标准差测量)在我们的训练数据的三个点周围接近零,如右面板所示。远离这些数据点,标准差平滑地增加到归一化的最大不确定性 1。

这意味着在高维情况下,GP 的所有良好属性,如平滑插值和不确定性量化,都得以保留。

这标志着第二章的结束。我们已经对 GP 的概念有了理解,并学会了如何使用 GPyTorch 在 Python 中实现基本的 GP 模型。如前所述,我们将在第三章深入探讨 GP 的均值和协方差函数,包括它们的超参数,以及每个组件如何控制我们的 GP 模型的行为。

2.5 练习

在这个练习中,我们在第一章中看到的一个真实数据集上训练了一个高斯过程(GP),该数据集在表 2.2 中再次显示。每个数据点(行)对应于通过在不同比例下混合铅(Pb)、锡(Sn)、锗(Ge)和锰(Mn)—这些称为父化合物—创建的合金(一种金属)。特征包含在前四列中,这些是父化合物的百分比。预测目标,混合温度,在最后一列中,表示合金形成的最低温度。任务是根据合金的组成百分比预测混合温度。

表 2.2 来自材料发现任务的数据。特征是材料结构以父化合物百分比表示,预测目标是混合温度。

Pb 的% Sn 的% Ge 的% Mn 的% 混合温度(°F)
0.50 0.50 0.00 0.00 192.08
0.33 0.33 0.33 0.00 258.30
0.00 0.50 0.50 0.00 187.24
0.00 0.33 0.33 0.33 188.54

有多个步骤:

  1. 创建表 2.2 中包含的四维数据集。

  2. 将第五列标准化,方法是减去所有值的均值并将结果除以它们的标准差。

  3. 将前四列视为特征,第五列视为标签。在这些数据上训练一个高斯过程(GP)。您可以重复使用我们在该章中实现的 GP 模型类。

  4. 创建一个测试数据集,其中含有零百分比的锗和锰的组合。换句话说,测试集是一个在单位正方形上的网格,其轴是铅和锡的百分比。

    测试集应该如下 PyTorch 张量所示:

    tensor([[0.0000, 0.0000, 0.0000, 0.0000],
            [0.0000, 0.0100, 0.0000, 0.0000],
            [0.0000, 0.0200, 0.0000, 0.0000],
            ...,
            [1.0000, 0.9800, 0.0000, 0.0000],
            [1.0000, 0.9900, 0.0000, 0.0000],
            [1.0000, 1.0000, 0.0000, 0.0000]])
    

    注意第三列和第四列都是零。

  5. 对此测试集预测混合温度。也就是说,计算测试集中每个点的标准化混合温度的后验均值和标准差。

  6. 可视化预测。这涉及以与图 2.19 相同的方式将均值和标准差显示为热图。解决方案包含在 CH02/02 - Exercise.ipynb 中。

总结

  • 多元高斯分布(MVN)模型化了许多随机变量的联合分布。均值向量表示变量的期望值,而协方差矩阵模拟了这些变量的方差和它们之间的协方差。

  • 通过应用贝叶斯定理,我们可以计算 MVN 的后验分布。通过这种贝叶斯更新,与观察变量相似的变量被更新以反映这种相似性。总的来说,相似的变量产生相似的预测。

  • 高斯过程(GP)将多元正态分布(MVN)扩展到无限维,使其成为函数的分布。然而,GP 的行为仍然类似于 MVN 分布。

  • 即使没有任何训练数据,GP 仍然可以根据先验 GP 产生预测。

  • 训练完成后,高斯过程(GP)的平均预测平滑地插值了训练数据点。

  • 使用高斯过程的最大优势之一是模型提供的不确定性的校准量化:对于观察到的数据点周围的预测更加自信;另一方面,远离训练数据的预测则更不确定。

  • 使用多变量正态分布或高斯过程进行条件化在视觉上类似于在观察点处打结。这迫使模型确切地经过观察,并将不确定性减少到零。

  • 使用 GPyTorch 实现 GP 时,我们可以以模块化的方式编写一个扩展基类的模型类。具体来说,我们实现了两个特定的方法:__init__(),声明了 GP 的均值和协方差函数;forward(),为给定输入构造了一个多变量正态(MVN)分布。

第四章:使用均值和协方差函数自定义高斯过程

本章涵盖内容

  • 使用均值函数控制 GP 的预期行为

  • 使用协方差函数控制 GP 的平滑度

  • 使用梯度下降学习 GP 的最优超参数

在第二章中,我们了解到均值和协方差函数是高斯过程 (GP) 的两个核心组成部分。即使在实现我们的 GP 时使用了零均值和 RBF 协方差函数,你在这两个组成部分上可以选择很多选项。

通过选择均值或协方差函数的特定选择,我们实际上在为我们的 GP 模型指定先验知识。将先验知识纳入预测是我们在任何贝叶斯模型中都需要做的事情,包括 GP。虽然我说我们需要这样做,但将先验知识纳入模型中总是一件好事,特别是在数据获取昂贵的情况下,比如贝叶斯优化。

例如,在天气预报中,如果我们想要估计一月份在密苏里的典型天气的温度,我们不必进行任何复杂的计算就能猜到温度会相当低。在温度较高的加州的夏天,我们也可以猜到天气会相对炎热。这些粗略的估计可以用作贝叶斯模型中的初始猜测,实质上就是模型的先验知识。如果我们没有这些初始猜测,我们将需要进行更复杂的建模来进行预测。

正如我们在本章中学到的,将先验知识纳入 GP 中可以大大改变模型的行为,从而带来更好的预测性能(最终带来更有效的决策)。只有当我们对函数的行为没有任何好的猜测时,才应该不使用先验知识;否则,这等同于浪费信息。

在本章中,我们将讨论均值和协方差函数的不同选项,以及它们如何影响生成的 GP 模型。与第二章不同,我们在这里采用了一种实践的方法,并围绕 Python 中的代码实现展开讨论。到本章结束时,我们将开发一种选择适当的均值和协方差函数以及优化它们的超参数的流程。

3.1 先验概率对于贝叶斯模型的重要性

问题:为什么你似乎无法改变一些人的想法?答案:因为他们的先验概率。为了说明先验概率在贝叶斯模型中有多么重要,请考虑以下情景。

假设你和你的朋友鲍勃和爱丽丝在嘉年华上闲逛,你正在和一个声称自己是灵媒的人交谈。他们允许你通过以下程序来测试这一说法:你和你的朋友们每个人都想一个 0 到 9 之间的数字,而“灵媒”将告诉你们每个人在想什么数字。你可以重复这个过程任意次数。

现在,你们三个人都对这个所谓的灵媒感到好奇,你们决定进行这个测试 100 次。令人惊讶的是,在这 100 次测试之后,嘉年华上的这位自称的灵媒准确地猜出了你们每个人心里想的数字。然而,测试结束后,你们每个人的反应却各不相同,如图 3.1 所示。

图 3.1 显示了你们朋友中看到一个人连续猜对一个秘密数字 100 次后的反应。由于他们的先验信念不同,每个人得出了不同的结论。

关于贝叶斯定理的进一步阅读

如果你需要恢复记忆,请随时返回图 2.2,我们在那里研究了贝叶斯定理。在本书中,我们只是大致概述了这一过程,但我建议你阅读威尔·库尔特(Will Kurt)的《贝叶斯统计学的有趣方法》(No Starch Press,2019)的第 1 和第二章,如果你想深入研究这个过程。

你们三个人怎么可能观察同样的事件(嘉年华上的人连续猜对 100 次)却得出不同的结论呢?要回答这个问题,考虑使用贝叶斯定理更新信念的过程:

  1. 每个人都从特定的先验概率开始,认为这个人是个灵媒。

  2. 然后,你们每个人都观察到他们猜对你的数字一次的事件。

  3. 然后,你们每个人都计算可能性项。首先,鉴于他们确实是灵媒,他们的猜测正确的可能性是完全的 1,因为真正的灵媒总是能通过这个测试。其次,鉴于他们 不是 灵媒,他们的猜测正确的可能性是 10 个中的 1,因为每次,你们都是在 0 到 9 之间随机选择一个数字,所以这 10 个选项中的任何一个猜测都有相等的可能性:10 分之 1。

  4. 最后,通过将先验与这些可能性项相结合来计算这个人不是灵媒的后验概率,你们更新了自己的信念。具体来说,这个后验概率将与先验和第一个可能性项 相乘 成比例。

  5. 你们然后重复这个过程 100 次,每次使用前一次迭代的后验概率作为当前迭代的先验概率。

这里高层次的重要性在于,经过每次测试,你和你的朋友对这个人是灵媒的后验信念 从未减少,因为这个陈述与你观察到的数据不符。具体来说,图 3.2 显示了你们小组中每个人的渐进后验概率,作为“嘉年华中的灵媒”通过了多少次测试的函数。

图 3.2 进展后验概率,即在成功猜测次数的函数中,卡尼瓦尔上的女士是一个灵媒的概率。这个后验概率从不下降,但根据初始先验的不同而行为也不同。

如图所示,三条曲线中的每一条要么增加,要么保持不变——没有一条曲线实际下降,因为一个减少的成为灵媒的概率不符合 100 次连续成功猜测。但为什么这三条曲线看起来如此不同呢?你可能已经猜到了,曲线的起始位置——即每个人认为女士是灵媒的先验概率——是原因。

在鲍勃的案例中,在左侧面板上,他最初的先验相对较高,认为那个人是灵媒的概率为 1%。鲍勃是一个信徒。随着他观察到越来越多与这一信念一致的数据,他的后验概率也越来越高。

在你自己的情况下,在中间,作为怀疑论者,你的先验要低得多:10 的 14 次方中的 1。然而,由于你的观察确实表明女士是灵媒,随着更多数据的输入,你的后验概率也增加到最后的 1%。

另一方面,艾丽斯的情况则不同。从一开始,她不相信灵媒是真实存在的,因此她给了她的先验概率精确的零。现在,请记住,根据贝叶斯定理,后验概率与先验概率乘以似然性成比例。由于艾丽斯的先验概率恰好为零,贝叶斯更新中的这种乘法将总是产生另一个零。

由于艾丽斯最初的概率为零,在一次成功测试后,这个概率保持不变。一次正确的猜测后,艾丽斯的后验概率为零。两次猜测后,仍为零。所有 100 次正确的猜测后,这个数字仍然是零。所有的一切都符合贝叶斯更新规则,但由于艾丽斯的先验不允许灵媒存在的可能性,任何数据都无法说服她相反。

这突显了贝叶斯学习的一个重要方面——我们的先验确定了学习的方式(见图 3.3):

  • 鲍勃的先验相当高,因此在 100 次测试结束时,他完全相信那个人是灵媒。

  • 另一方面,你更加怀疑,你的初始先验比鲍勃低得多。这意味着你需要更多的证据才能得出高后验。

  • 艾丽斯对可能性的完全忽视,用她的零先验表示,使她的后验概率保持在零。

图 3.3 各人的先验信念如何被相同的数据更新。与鲍勃相比,你的先验更低,增长速度更慢。艾丽斯的先验为 0,始终保持为 0。

虽然我们的例子中的主张是关于某个人是否是通灵者的事件,但是同样的贝叶斯更新过程适用于所有情形,其中我们对某个事件有概率信念,并经常根据数据进行更新。事实上,这正是我们有时似乎无法改变某个人的想法的原因:因为他们的先验概率为零,没有任何东西可以将后验概率更新为非零。

从哲学角度来看,这个讨论非常有趣,因为它表明,为了能够说服某个人做某事,他们需要至少想到一种可能性,即指定事件的非零先验概率。更具体地说,这个例子说明了对贝叶斯模型拥有良好的先前知识的重要性。正如我们所说,我们通过均值和协方差函数来指定高斯过程的先验知识。每个选择都会在高斯过程的预测中产生不同的行为。

3.2 将先前已知内容并入高斯过程

在本节中,我们将确定在高斯过程中指定先前知识的重要性。这个讨论为我们在本章剩余部分的讨论下了动力。

先验高斯过程可能一开始具有恒定的平均值和 CI。如图 3.4 所示,该高斯过程然后被更新为平稳地内插观测到的数据点。也就是说,均值预测恰好穿过数据点,并且 95%的 CI 在那些区域消失。

图 3.4 先验高斯过程和后验高斯过程的比较。先验高斯过程包含有关目标函数的先前信息,而后验高斯过程将该信息与实际观测值相结合。

图 3.4 中的先验高斯过程不对我们正在建模的目标函数做出任何假设。这就是为什么这个高斯过程的平均预测值在任何地方都是零。但是,在许多情况下,即使我们不知道目标函数的确切形式,我们也了解目标的某些方面。

以以下为例:

  • 在超参数调整应用程序中模拟模型准确性时,我们知道目标函数的范围在 0 到 1 之间。

  • 在我们从第 2.1 节中的住房价格示例中,函数值(价格)严格为正,当房屋的理想属性(例如生活区)增加时,应该增加。

  • 在住房示例中,函数值对某些特征更加敏感。例如,与生活区面积的函数相比,房屋价格随着层数的增加更快——多一层楼会增加房屋价格,而多一个平方英尺的生活区则不会。

这种信息正是我们希望用 GP 表示的先验知识,使用 GP 的最大优势之一是我们有许多方法来融入先验知识。这样做有助于缩小 GP 代理与其建模的实际目标函数之间的差距,这也将更有效地引导后续的优化。

用 GP 结合先验知识

我们通过选择适当的平均和协方差函数以及设置它们的参数值来融入先验知识。特别是

  • 平均函数定义了目标函数的预期行为。

  • 协方差函数定义了目标的结构,或者更具体地说,定义了任意一对数据点之间的关系,以及目标函数在其定义域内变化的速度和平滑程度。

前面的每个选择都会导致生成的 GP 的行为发生 drastical 不同。例如,线性平均函数将导致 GP 预测中的线性行为,而二次平均函数将导致二次行为。通过在协方差函数中使用不同的参数,我们还可以控制 GP 的变异性。

3.3 使用平均函数定义函数行为

首先,我们介绍了 GP 的平均函数,它定义了 GP 的预期行为,或者说我们相信函数在所有可能的情况下平均情况下的样子。正如我们将要看到的,这有助于我们指定与函数的一般行为和形状相关的任何先验知识。本节中使用的代码包含在 CH03/01 - Mean functions.ipynb 中。为了使我们的讨论具体化,我们使用了一个房价数据集,其中表 3.1 中有五个数据点。

表 3.1 示例训练数据集。预测目标(价格)随特征(居住面积)的增加而增加。

居住面积(以 1000 平方英尺为单位) 价格(以 10 万美元为单位)
0.5 0.0625
1 0.25
1.5 0.375
3 2.25
4 4

在这个数据集中,我们建模的函数值是房价,它们是严格正的,并且随着居住面积的增加而增加。这些性质是直观的,即使不知道未观察到的房屋的价格,我们也确信这些未见价格也具有这些性质。

我们的目标是将这些性质纳入我们的平均函数中,因为它们描述了我们对函数行为的预期。在我们开始建模之前,我们首先编写一个辅助函数,该函数接受一个 GP 模型(以及其似然函数),并在范围从 0 到 10(即,一个 10,000 平方英尺的居住面积)内可视化其预测。实现如下:

def visualize_gp_belief(model, likelihood):
    with torch.no_grad():                                ❶
        predictive_distribution = likelihood(model(xs))  ❶
        predictive_mean = predictive_distribution.mean   ❶
        predictive_upper, predictive_lower =             ❶
        ➥ predictive_distribution .confidence_region()  ❶

    plt.figure(figsize=(8, 6))

    plt.plot(xs, ys, label="objective", c="r")
    plt.scatter(train_x, train_y, marker="x", c="k", label="observations")

    plt.plot(xs, predictive_mean, label="mean")          ❷
    plt.fill_between(
        xs.flatten(), predictive_upper, predictive_lower, alpha=0.3,
        label="95% CI"
    )                                                    ❸

    plt.legend(fontsize=15);

❶ 计算预测

❷ 绘制平均线

❸ 绘制 95% CI 区域

我们在第 2.4.3 节中看到了这段代码是如何工作的,现在我们将其放入一个方便的函数中。有了这个,我们就准备好实现我们的 GP 模型,并看看我们的选择如何影响所产生的预测。

3.3.1 使用零均值函数作为基本策略

均值的最简单形式是一个在零处的常数函数。在没有数据的情况下,此函数将产生零作为其默认预测。当没有关于我们可能将其作为先验知识合并到 GP 中的目标函数的额外信息时,将使用零均值函数。

使用零均值函数实现的 GP 如下所示:

class ConstantMeanGPModel(gpytorch.models.ExactGP):
    def __init__(self, train_x, train_y, likelihood):
       super().__init__(train_x, train_y, likelihood)
       self.mean_module = gpytorch.means.ConstantMean()   ❶
       self.covar_module = gpytorch.kernels.RBFKernel()

    def forward(self, x):
       mean_x = self.mean_module(x)
       covar_x = self.covar_module(x)
       return gpytorch.distributions.MultivariateNormal(mean_x, covar_x)

❶ 默认值为零的常数均值函数

请记住,根据第 2.4.2 节的内容,要构建一个使用 GPyTorch 的 GP 模型,我们实现 __init__()forward() 方法。在第一个方法中,我们初始化我们的均值和协方差函数;在第二个方法中,我们将输入 x 通过这些函数,并返回相应的多元高斯分布。

注意:在我们从第 2.4.2 节中的实现中,我们使用 gpytorch.means.ZeroMean 类,而在这里,我们使用 gpytorch.means.ConstantMean 类来初始化我们的均值函数。然而,这个常数均值函数的默认值是零,因此实际上,我们仍然在实现相同的 GP 模型。尽管这两种选择目前导致相同的模型,但在本章中,我们将展示如何使用 gpytorch.means.ConstantMean 来调整常数均值值,以获得更好的模型。

现在让我们初始化该类的一个对象,在我们的训练数据上对其进行训练,并可视化其预测。我们用以下代码来实现这个:

lengthscale = 1
noise = 1e-4

likelihood = gpytorch.likelihoods.GaussianLikelihood()     ❶
model = ConstantMeanGPModel(train_x, train_y, likelihood)  ❶

model.covar_module.lengthscale = lengthscale               ❷
model.likelihood.noise = noise                             ❷

model.eval()
likelihood.eval()

visualize_gp_belief(model, likelihood)

❶ 声明 GP

❷ 修复超参数

在这里,我们初始化 GP 模型并设置其超参数——长度尺度和噪声方差分别为 1 和 0.0001。我们将在本章后面看到如何适当设置这些超参数的值;现在,让我们只使用这些值。最后,我们在我们的 GP 模型上调用我们刚刚编写的辅助函数 visualize_gp_belief(),它将生成图 3.5。

所有我们在第 2.4.4 节中指出的 GP 的良好性质仍然存在:

  • 后验均值函数平滑地插值出我们的训练数据点。

  • 95% CI 在这些数据点周围消失,表示了一个良好校准的不确定性量化。

图 3.5 使用零均值函数的 GP 的预测。后验均值函数插值了观察到的数据点,并在远离这些观测点的区域回归到零。

从这个图表中我们还注意到,一旦我们足够远离我们的训练数据点(图表的右侧),我们的后验均值函数就会恢复到先验均值,即零。这实际上是高斯过程的一个重要特征:在没有数据的情况下(在没有观测到的区域),先验均值函数是推断过程的主要驱动力。这在直觉上是有道理的,因为没有实际观察,预测模型能做的最好的事情就是简单地依赖于其均值函数中编码的先验知识。

注意:在这一点上,我们看到为什么将先验知识明确定义地编码到先验高斯过程中是如此重要:在没有数据的情况下,预测的唯一驱动因素就是先验高斯过程。

然后自然地提出了一个问题:是否可以使用非零均值函数来诱导我们的高斯过程在这些未探索的区域中产生不同的行为,如果可以,我们有哪些选择?本节的剩余部分旨在回答这个问题。我们首先使用一个不为零的常数均值函数。

使用梯度下降法使用常数函数 3.3.2

如果一个常数均值函数不为零,那么当我们期望我们正在建模的目标函数具有我们先验知道的一些值范围时,这是合适的。由于我们正在建模房价,使用一个常数均值函数,其常数大于零,是有意义的,因为我们确实期望价格是正的。

当然,在许多情况下,我们不可能知道目标函数的平均值是多少。那么,我们应该如何为我们的均值函数找到一个合适的值呢?我们使用的策略是依赖于一个特定的数量:给定我们均值函数的值时,训练数据集有多大可能性。粗略地说,这个数量衡量了我们的模型解释其训练数据的能力。我们展示了如何在这一小节中使用这个数量来选择我们的高斯过程的最佳均值函数。

如果给定一些值c[1]时训练数据的似然性高于给定另一个值c[2]时的情况,那么我们更喜欢使用c[1]而不是使用c[2]。这量化了我们先前关于使用非零均值函数来建模正函数的直觉:一个常数均值函数,其值为正,比值为零(或负值)的函数更好地解释了来自完全正函数的观测。

我们如何计算这个似然性呢?GPyTorch 提供了一个方便的类,gpytorch.mlls.ExactMarginalLogLikelihood,它接受一个高斯过程模型并计算其训练数据的边际对数似然性,给定模型的超参数。

要看到这个似然数量在量化数据拟合方面的有效性,考虑图 3.6。这个图可视化了两个不同 GP 模型的预测:一个我们在前面子段中看到的零均值 GP,在左边,以及均值函数值为 2 的 GP,在右边。注意在第二个面板中,均值函数在图的右侧恢复到 2 而不是 0。在这里,第二个 GP 的(对数)似然性比第一个高,这意味着值 2 比值 0 更好地解释了我们的训练数据。

图 3.6 GP 预测,给出两个不同常数均值函数。值 2 比值 0 给出了更高的似然值,表明前者的均值函数比后者更合适。

有了这个对数似然计算,我们的最后一步就是简单地找到我们的均值函数的值,使得对数似然被最大化。换句话说,我们的目标是寻找最能解释我们训练数据的均值。由于我们可以访问对数似然计算,我们可以使用基于梯度的优化算法,例如梯度下降,来迭代地优化我们的均值。当收敛时,我们将得到一个很好的均值,它给出了高数据似然度。如果您需要恢复一下梯度下降的工作原理,我推荐 Luis Serrano 的《Grokking Machine Learning》(Manning,2021)的附录 B,它很好地解释了这个概念。

现在,让我们看看如何用代码实现这个过程。由于我们用gpytorch.means.ConstantMean类为我们的均值函数实现了 GP 模型,所以我们这里不需要做任何更改。所以,现在,让我们再次初始化我们的 GP 模型:

# declare the GP
lengthscale = 1
noise = 1e-4

likelihood = gpytorch.likelihoods.GaussianLikelihood()
model = ConstantMeanGPModel(train_x, train_y, likelihood)

# fix the hyperparameters
model.covar_module.lengthscale = lengthscale
model.likelihood.noise = noise

这个过程的核心步骤是定义对数似然函数以及梯度下降算法。如前所述,前者是gpytorch.mlls.ExactMarginalLogLikelihood类的一个实例,实现如下:

mll = gpytorch.mlls.ExactMarginalLogLikelihood(likelihood, model)

对于梯度下降算法,我们使用 Adam,这是一种在许多 ML 任务中取得了很大成功的最先进算法,尤其是 DL。我们用 PyTorch 声明如下:

optimizer = torch.optim.Adam([model.mean_module.constant], lr=0.01)

请注意,我们正在传递给torch.optim.Adam类的是model.mean_module.constant,这是我们希望优化的均值。当我们运行梯度下降过程时,Adam 算法会迭代地更新model.mean_module.constant的值,以改善似然函数。

现在我们需要做的最后一件事是运行梯度下降,其实现如下:

model.train()                         ❶
likelihood.train()                    ❶

losses = []
constants = []
for i in tqdm(range(500)):
    optimizer.zero_grad()

    output = model(train_x)
    loss = -mll(output, train_y)      ❷

    loss.backward()                   ❸

    losses.append(loss.item())
    constants.append(model.mean_module.constant.item())

    optimizer.step()                  ❸

model.eval()                          ❹
likelihood.eval()                     ❹

❶ 启用训练模式

❷ 损失作为负边缘对数似然

❸ 损失上的梯度下降

❹ 启用预测模式

开始时的train()调用和最后的eval()调用是我们总是需要进行的记录步骤,分别启用我们 GP 模型的训练模式和预测模式。每一步都使用optimizer.zero_grad()重置梯度是另一个记录任务,以确保我们不会错误地计算梯度。

在中间,我们有一个 500 步的梯度下降过程,通过迭代计算损失(即我们的负对数似然)并根据梯度下降来降低这个损失。在这个 for循环过程中,我们跟踪所获得的负对数似然值以及在每一步中调整的平均值。这是为了在训练之后,我们可以可视化检查这些值以确定它们是否收敛。

图 3.7 展示了这种可视化,显示了我们希望最小化的负对数似然的运行值以及 GP 的均值函数值。我们的损失一直降低(这是件好事!)随着均值常数的增加,显示出正常数比零更有可能性。两条曲线在 500 次迭代后都趋于平稳,表明我们已经收敛到均值常数的最优值。

图 3.7 展示了梯度下降过程中的负对数似然(较低的值为更好)和均值值。在这两个面板中,这些值已经收敛,表明我们已经到达了最优解。

注意,在使用梯度下降时,我建议您始终绘制出渐进损失的图像,就像我们刚才在这里所做的那样,以查看您是否已经收敛到最优值。在收敛之前停止可能会导致模型性能不佳。虽然在本章节中我们不会再展示这些渐进损失图,但附带的代码中包含了它们。

到目前为止,我们已经学会了在我们的 GP 模型中使用零均值函数作为默认值,并优化相对于数据似然的平均常数值。然而,在许多使用情况下,您可能对目标函数的行为有先验知识,因此更喜欢将更多结构纳入您的均值函数中。

例如,我们如何实现房屋价格随着更大的居住面积而增加的想法?向前迈进,我们学会了使用线性或二次均值函数来实现这一点。

3.3.3 使用带梯度下降的线性函数

我们继续使用线性均值函数,其形式为μ = w^T**x + b。这里,μ是测试点x上的预测均值,而w是连接x中每个特征的系数的权重向量,b是一个常数偏置项。

通过使用线性平均函数,我们对我们的目标函数的预期行为进行编码,即它等于数据点x的特征的线性组合。对于我们的住房价格示例,我们只有一个特征,即生活区域,我们期望它有一个正权重,因此通过增加生活区域,我们的模型将预测出更高的价格。

另一种思考线性平均模型的方式是,我们有一个线性回归模型(也假设目标标签为特征的线性组合),然后我们在预测上加上一个概率信念,一个 GP 模型。这给予我们线性回归模型的能力,同时保持使用 GP 建模的所有好处,即不确定性量化。

注意 在一个常数均值函数下,权重向量w固定为零向量,偏差b是我们学会在上一小节优化的平均值。换句话说,线性函数比常数均值函数是一个更一般的模型。

关于实施,使用具有线性平均函数的 GP 模型相当简单。我们只需用gpytorch.means.LinearMean实例取代我们的常数均值,这样(我们的forward()方法保持不变):

class LinearMeanGPModel(gpytorch.models.ExactGP):
    def __init__(self, train_x, train_y, likelihood):
        super().__init__(train_x, train_y, likelihood)
        self.mean_module = gpytorch.means.LinearMean(1)    ❶
        self.covar_module = gpytorch.kernels.RBFKernel()

❶ 线性平均

在这里,我们使用1来初始化我们的均值模块,表示我们正在处理一维目标函数。当处理高维函数时,你可以简单地在这里传递该函数的维度。除此之外,我们模型的其它部分与之前的相似。在我们的三个点数据集上拟合和训练这个新模型,我们得到图 3.8 中的预测。

图 3.8 GP 带有线性平均函数的预测。GP 有一个上升趋势,这直接是线性平均函数的正斜率的结果。

与我们到目前为止看到的恒定平均值不同,我们在这里使用的线性平均函数驱使整个 GP 模型具有上升趋势。这是因为我们在训练数据中的五个数据点的最佳拟合线是具有正斜率的线,这正是我们希望作为生活区域和价格之间关系的建模。

3.3.4 通过实施自定义均值函数使用二次函数

我们这里的线性平均函数成功捕捉到了价格的上涨趋势,但它假定价格增长率是恒定的。也就是说,对生活区域增加一个额外的平方英尺,预期上会导致价格的恒定增加。

然而,在许多情况下,我们可能有先验知识,即我们的目标函数以非恒定的速率增长,而线性均值无法建模。事实上,我们使用的数据点是这样生成的,即价格是生活区域的二次函数。这就是为什么我们看到较大的房子比较小的房子更快地变得更贵。在本小节中,我们将将我们的 GP 均值实现为一个二次函数。

在撰写本文时,GPyTorch 仅提供了常数和线性均值函数的实现。但是,正如我们将在本书中一次又一次地看到的那样,这个软件包的美妙之处在于其模块化:GP 模型的所有组件,如均值函数、协方差函数、预测策略,甚至边缘对数似然函数,都被实现为模块,因此,它们可以以面向对象的方式进行修改、重新实现和扩展。当我们实现自己的二次均值函数时,我们首先亲自体会到这一点。

我们要做的第一件事是定义一个均值函数类:

class QuadraticMean(gpytorch.means.Mean):
    def __init__(self, batch_shape=torch.Size(), bias=True):
        ...

    def forward(self, x):
        ...

这个类扩展了 gpytorch.means.Mean 类,它是所有 GPyTorch 均值函数实现的基类。为了实现我们的自定义逻辑,我们需要重新编写两个方法:__init__()forward(),这与我们实现 GP 模型时完全相同!

__init__() 中,我们需要声明我们的均值函数包含哪些参数。这个过程称为 参数注册

虽然线性函数有两个参数,斜率和截距,而二次函数有三个参数:一个二阶项 x[2] 的系数;一个一阶项 x 的系数;和一个零阶项的系数,通常称为偏差。这在图 3.9 中有所说明。

图 3.9 线性函数和二次函数的函数形式。线性函数有两个参数,而二次函数有三个。当这些函数用作 GP 的均值函数时,相应的参数是 GP 的超参数。

在这个基础上,我们这样实现 __init__() 方法:

class QuadraticMean(gpytorch.means.Mean):
    def __init__(self, batch_shape=torch.Size(), bias=True):
        super().__init__()
        self.register_parameter(
            name="second",
            parameter=torch.nn.Parameter(torch.randn(*batch_shape, 1, 1))
        )                                                                 ❶

        self.register_parameter(
            name="first",
            parameter=torch.nn.Parameter(torch.randn(*batch_shape, 1, 1))
        )                                                                 ❷

        if bias:
            self.register_parameter(
                name="bias",
                parameter=torch.nn.Parameter(torch.randn(*batch_shape, 1))
            )                                                             ❸
        else:
            self.bias = None

❶ 二阶系数

❷ 一阶系数

❸ 偏差

我们按顺序调用 register_parameter() 来注册二阶系数、一阶系数和偏差。由于我们不清楚这些系数应该取什么值,我们只需使用 torch.randn() 随机初始化它们。

注意 我们需要将这些参数注册为 torch.nn .Parameter 类的实例,这允许它们的值在梯度下降期间进行调整(训练)。

对于 forward() 方法,我们需要定义我们的均值函数如何处理输入。正如我们所说的,二次函数的形式为 ax² + bx + c,其中 abc 分别是二阶系数、一阶系数和偏差。所以我们只需要实现这个逻辑,如下所示:

class QuadraticMean(gpytorch.means.Mean):
    def __init__(self, train_x, train_y, likelihood):
        ...                                               ❶

    def forward(self, x):
        res = x.pow(2).matmul(self.second).squeeze(-1) \
            + x.matmul(self.first).squeeze(-1)            ❷
        if self.bias is not None:
            res = res + self.bias
        return res

❶ 已省略

❷ 二次函数的公式

有了这个二次均值函数,我们现在可以编写一个初始化其均值模块的高斯过程模型,使用的是我们刚刚实现的自定义 QuadraticMean 类:

class QuadraticMeanGPModel(gpytorch.models.ExactGP):
    def __init__(self, train_x, train_y, likelihood):
        super().__init__(train_x, train_y, likelihood)
        self.mean_module = QuadraticMean()
        self.covar_module = gpytorch.kernels.RBFKernel()

    def forward(self, x):
        ...                 ❶

❶ 省略

通过梯度下降重新运行整个训练过程,我们得到了图 3.10 中的预测。

图 3.10 由具有二次均值函数的高斯过程进行的预测。该高斯过程预测随着较大的居住面积,价格增长速度更快。

在这里,我们成功地模拟了房屋价格相对于居住面积的非恒定增长率。我们的预测在图的右侧增长得比左侧快得多。

我们可以对我们希望假设的任何功能形式进行这样的操作,例如更高阶的多项式或次线性函数。我们需要做的是实现一个具有适当参数的均值函数类,并使用梯度下降为这些参数分配值,以便为我们的训练数据提供良好的拟合。

到目前为止,我们的讨论展示了高斯过程模型的数学灵活性,即它们可以利用任何结构的均值函数,并且仍然能够产生概率预测。这种灵活性激发并推动了 GPyTorch 的设计,其对模块化的强调帮助我们轻松地扩展和实现自己的自定义均值函数。在接下来我们将讨论的 GPyTorch 的协方差函数中,我们看到了同样的灵活性和模块化。

3.4 用协方差函数定义变异性和平滑度

虽然均值函数定义了我们对目标函数整体行为的期望,但高斯过程的协方差函数或核函数扮演着更复杂的角色:定义了域内数据点之间的关系,并控制了高斯过程的结构和平滑度。在本节中,我们比较了高斯过程对模型不同组成部分进行更改时的预测。通过这样做,我们能够实际洞察如何为高斯过程模型选择适当的协方差函数。我们使用的代码在 CH03/02 - Covariance functions.ipynb 中。

在这些示例中,我们使用了在第 2.4.1 节中看到的 Forrester 函数作为我们的目标函数。我们再次在 -3 到 3 之间随机采样三个数据点,并将它们用作我们的训练数据集。本节中可视化的所有预测都来自于在这三个点上训练的高斯过程。

3.4.1 设置协方差函数的尺度

通过其协方差函数来控制高斯过程行为的第一种方法是设置长度尺度和输出尺度。这些尺度,就像均值函数中的常数或系数一样,是协方差函数的超参数:

  • 长度尺度控制着高斯过程输入的尺度,因此,高斯过程沿轴变化的速度有多快—也就是说,我们相信目标函数在输入维度上的变化程度有多大。

  • 输出尺度定义了 GP 的输出范围或者说是其预测范围。

通过设置这些尺度的不同值,我们可以增加或减少 GP 预测的不确定性,以及缩放我们的预测范围。我们使用以下代码实现:

class ScaleGPModel(gpytorch.models.ExactGP):
    def __init__(self, train_x, train_y, likelihood):
        super().__init__(train_x, train_y, likelihood)
        self.mean_module = gpytorch.means.ZeroMean()
        self.covar_module =
        ➥ gpytorch.kernels.ScaleKernel(
          gpytorch.kernels.RBFKernel())     ❶

    def forward(self, x):
        ...                                 ❷

❶ gpytorch.kernels.ScaleKernel 实现了输出尺度。

❷ 忽略

注意,这里的 covar_module 属性代码与之前不同:我们将一个 gpytorch.kernels.ScaleKernel 对象放在了通常的 RBF 内核之外。这实际上实现了输出尺度,它通过某个定值因子对 RBF 内核的输出进行了缩放。而长度尺度则已经包含在了 gpytorch.kernels.RBFKernel 内核中。

在我们迄今为止使用的代码中,我们有一行代码用于设置我们内核的长度尺度,即 model.covar_module.base_kernel.lengthscale = lengthscale。这就是长度尺度的值所保存的位置。使用类似的 API,我们可以使用 model.covar_module.outputscale = outputscale 来设置内核的输出尺度。现在,为了验证长度尺度确实可以控制函数变化的速度,我们将比较两个 GP 的预测,一个长度尺度为 1,另一个为 0.3,如图 3.11 所示。

图 3.11 显示了 GP 以长度尺度为 1(左边)和 0.3(右边)进行预测的结果。长度尺度越小,GP 的预测就越不确定,变异性越高。

这两个面板的巨大差异清晰地表明了长度尺度的效果:

  • 一个较短的长度尺度相当于给定输入常量变化情况下客观函数更多的变异性。

  • 相反,一个较长的长度尺度强制函数更加光滑,也就是说给定输入更少的变化会使其变异性减少。

例如,沿着 x 轴移动一个单位,在图 3.11 左侧面板中的样本的变化幅度小于右侧面板的样本。

那么输出尺度呢?我们之前说这个参数将协方差函数的输出值缩放到一个不同的范围。这是通过将协方差输出结果乘以这个参数来实现的。因此,较大的输出尺度会使 GP 的预测范围更宽,而较小的输出尺度则会使预测范围缩小。为了验证这一点,让我们再次运行代码并重新生成预测,这次将输出尺度设置为 3。生成的输出结果如图 3.12 所示。

图 3.12 显示了 GP 的输出尺度为 3 的预测结果。较大的输出尺度使 GP 模拟的函数范围更宽,也容许更多的预测不确定性。

尽管图 3.11 和图 3.12 的左侧面板看起来相同,因为 GP 及其样本在两个图中具有相同的形状,但我们注意到图 3.12 的 y-轴更大,因为它的预测值和样本值都取得了较大的值(负值和正值都有)。这是使用具有较大输出尺度的 RBF 核对协方差值进行缩放的直接结果。

通过我们的协方差函数的两个超参数,我们已经看到我们可以解释由我们的 GP 模型建模的各种功能行为,我们在表 3.2 中总结了这些行为。我邀请您使用不同的长度和输出尺度重新运行此代码,以查看其效果并验证表格!

表 3.2 GP 的长度和输出尺度的角色总结

参数 大值 小值
长度尺度 更平滑的预测,较少的不确定性 更多的变异性,更多的不确定性
输出尺度 较大的输出值,更多的不确定性 较窄的输出范围,较少的不确定性

注意 这种建模的灵活性引发了一个自然的问题:你应该如何适当地设置这些超参数的值?幸运的是,我们已经知道了一种设置 GP 模型超参数的好方法。我们可以通过选择最好地解释我们的数据的值,或者换句话说,通过梯度下降来最大化边缘对数似然,从而实现这一目标。

就像我们希望优化均值函数的超参数一样,我们现在只需将我们希望优化的变量 - 协方差函数的参数 - 传递给 Adam 即可实现:

optimizer = torch.optim.Adam(model.covar_module.parameters(), lr=0.01)

通过运行梯度下降,我们可以获得这些参数的良好值。具体来说,我获得了大约 1.3 的长度尺度和大约 2.1 的输出尺度。也就是说,为了很好地拟合这三点训练数据集,我们希望 GP 稍微更平滑一些(具有大于 1 的长度尺度),并且还希望我们的预测范围更大一些(具有较大的输出尺度)。这无疑是一个让人放心的结果,因为我们的目标函数具有广泛的数值范围 - 在输入 3 处,它的值将达到 -2,这远超出了具有输出尺度 1 的 CI。

3.4.2 使用不同的协方差函数控制平滑度

到目前为止,我们仅仅使用了 RBF 核作为我们的协方差函数。然而,如果 RBF 不合适,完全可以使用不同的核函数用于我们的 GP。在本小节中,我们将学习使用另一种核函数家族,即马特恩核,并看看这种核函数对我们的 GP 会产生什么影响。

注意 通过使用马特恩核,我们正在指定 GP 模型函数的平滑度。这里的 "平滑度" 是一个技术术语,指的是函数的可微性;函数可微性越多次,它就越平滑。我们可以大致将其视为函数值以曲折方式 "跳动" 的程度。

RBF 核模拟的函数具有无限可微性,这是现实世界中很少有的函数特性。与此同时,Matérn 核生成的函数是有限可微的,这些函数可以被微分的次数(即这些函数的平滑度)由可设置的参数控制,我们马上就会讨论到。

要看到 Matérn 核的实际效果,我们首先重新实现我们的 GP 模型类:

class MaternGPModel(gpytorch.models.ExactGP):
    def __init__(self, train_x, train_y, likelihood, nu):
        super().__init__(train_x, train_y, likelihood)
        self.mean_module = gpytorch.means.ZeroMean()
        self.covar_module = gpytorch.kernels.MaternKernel(nu)

    def forward(self, x):
        ...                  ❶

❶ 省略

在这里,我们的 covar_module 属性被初始化为 gpytorch .kernels.MaternKernel 类的实例。这个初始化接受一个参数 nu,定义了我们的 GP 将具有的平滑程度,这也是我们 __init__() 方法的一个参数。

重要提示:在撰写本文时,GPyTorch 支持三个 nu 值,1/2、3/2 和 5/2,对应函数分别是不可微分、一次可微分和两次可微分的。换句话说,这个 nu 参数越大,我们的 GP 越平滑。

让我们首先尝试 nu = 0.5,通过在初始化 GP 时设置该值来实现:

likelihood = gpytorch.likelihoods.GaussianLikelihood()
model = MaternGPModel(train_x, train_y, likelihood, 0.5)

...     ❶

visualize_gp_belief(model, likelihood)

❶ 修正超参数并启用预测模式

这段代码生成图 3.13。

图 3.13 中 Matérn 1/2 核的 GP 预测,这表明目标函数不可微分,对应非常粗糙的样本

与我们之前在 RBF 中看到的情况不同,这个 Matérn 核的样本都非常参差不齐。事实上,它们都不可微分。当建模时间序列数据时,比如股票价格,nu = 0.5 是 Matérn 核的一个好值。

然而,在 BayesOpt 中通常不使用这个值,因为像图 3.13 中那样的参差不齐的函数非常不稳定(它们可以以不可预测的方式上下跳动),通常不是自动优化技术的目标。我们需要目标函数具有一定的平滑度,以便进行优化;否则,有效的优化是不切实际的目标。

Matérn 5/2 核通常是首选。它的预测结果与 Matérn 3/2 生成的结果在图 3.14 中可视化。

图 3.14 中 Matérn 5/2(左)和 Matérn 3/2(右)核的 GP 预测。这里的样本足够平滑,以便 GP 有效地从数据中学习,但也足够参差不齐,以真实地模拟现实生活中的过程。

我们看到这个 5/2 核的样本要平滑得多,这导致 GP 更有效地学习。然而,这些样本也足够粗糙,以至于它们类似于我们在现实世界中可能看到的函数。因此,BayesOpt 中的大多数工作,无论是研究还是应用,都使用这个 Matérn 5/2 核。在未来的章节中,当我们讨论 BayesOpt 的决策时,我们将相应地默认使用这个核。

注意 虽然我们在这里没有包含相应的细节,但 Matérn 核函数有自己的长度尺度和输出尺度,可以像前面的小节一样指定,以进一步定制生成的 GP 的行为。

通过将均值函数与核函数配对,我们可以在 GP 的预测中诱发复杂的行为。就像我们的先验影响了我们朋友圈中每个人在看到有人正确猜出一个秘密数字 100 次后的结论一样,我们对均值函数和核函数的选择决定了 GP 的预测。图 3.15 展示了三个例子,其中每种均值函数和核函数的组合导致了截然不同的行为。

图 3.15 展示了在相同数据集上训练时,均值函数和核函数的三种不同选择以及它们各自后验 GP 的预测,每种选择都导致不同的预测行为。

3.4.3 用多个长度尺度建模不同级别的变异性

因为我们一直只考虑一维目标函数(其输入具有一个特征),所以我们只需要考虑一个长度尺度。然而,我们可以想象到一种情况,在这种情况下,高维目标函数(其输入具有多个特征)在某些维度上具有更多的变异性,在其他维度上较为平滑。也就是说,某些维度具有小的长度尺度,而其他维度具有大的长度尺度。还记得本章开头的启发性例子吗:房屋价格的预测增加一个楼层的幅度比增加一个平方英尺的生活面积更大。在本小节中,我们探讨了如何在 GP 中维护多个长度尺度以对这些函数进行建模。

如果我们只使用一个长度尺度来处理所有维度,那么我们将无法忠实地对目标函数进行建模。这种情况需要 GP 模型为每个维度维护一个单独的长度尺度,以完全捕获它们各自的变异性。在本章的最后一节中,我们学习如何在 GPyTorch 中实现这一点。

为了帮助我们的讨论,我们使用一个具体的二维目标函数,称为 Ackley,它可以修改为在不同维度中具有各种级别的变异性。我们将该函数实现如下:

def ackley(x):
    # a modification of https:/ /www.sfu.ca/~ssurjano/ackley.xhtml
    return -20 * torch.exp(
        -0.2 * torch.sqrt((x[:, 0] ** 2 + x[:, 1] ** 2) / 2)
    )
    ➥ - torch.exp(torch.cos(2 * pi * x[:, 0] / 3)
    ➥ + torch.cos(2 * pi * x[:, 1]))

我们特别将该函数的定义域限制为两个维度上的方形区域,即 -3 到 3,通常表示为 [-3, 3]²。为了可视化这个目标函数,我们在图 3.16 中使用热图。

图 3.16 作为我们的目标使用的二维 Ackley 函数。在这里,x 轴的变异性比 y 轴小(变化较少),需要不同的长度尺度。

热图中的每个暗斑点都可以被看作是目标函数表面上具有低值的山谷。在这里,沿y轴有更多的山谷,而沿x轴则没有那么多,这表明第二个维度的变异性更大——也就是说,目标函数沿y轴上下波动的次数比沿x轴多得多。

再次强调,这意味着仅使用一个长度尺度来描述两个维度并不是一个好选择。相反,我们应该为每个维度(在本例中为两个)有一个长度尺度。然后,可以使用梯度下降独立地优化每个长度尺度。

使用每个维度的长度尺度的内核称为自动相关确定(ARD)。这个术语表示,在使用梯度下降优化这些长度尺度之后,我们可以推断出目标函数的每个维度与函数值相关的程度。具有较大长度尺度的维度具有较低的变异性,因此,在建模目标函数值时比具有较小长度尺度的维度不太相关。

使用 GPyTorch 实现 ARD 非常容易:我们只需在初始化协方差函数时将 ard_num_dims 参数指定为我们的目标函数具有的维度数即可。像这样使用 RBF 内核:

class ARDGPModel(gpytorch.models.ExactGP):
    def __init__(self, train_x, train_y, likelihood):
        super().__init__(train_x, train_y, likelihood)
        self.mean_module = gpytorch.means.ZeroMean()
        self.covar_module = gpytorch.kernels.ScaleKernel(
            gpytorch.kernels.RBFKernel(ard_num_dims=2)
        )

    def forward(self, x):
        ...                 ❶

❶ 省略

让我们看看,当在我们的 Ackley 函数上训练时,这个模型是否为两个维度给出了不同的长度尺度。为此,我们首先构造一个包含 100 个点的随机抽样训练数据集:

torch.manual_seed(0)
train_x = torch.rand(size=(100, 2)) * 6 - 3
train_y = ackley(train_x)

在使用梯度下降训练模型后,我们可以通过打印出优化后的长度尺度的值来检查它们。

>>> model.covar_module.base_kernel.lengthscale
tensor([[0.7175, 0.4117]])

这确实是我们预期的结果:第一个维度的长度尺度较大,函数值的变异性较低,而第二个维度的长度尺度较小,函数值的变异性较高。

更多关于内核的阅读

内核本身已经受到了 ML 社区的极大关注。除了我们迄今所涵盖的内容之外,还有一点需要注意的是,内核还可以编码复杂的结构,如周期性、线性和噪声。对于更全面和技术性的内核讨论,感兴趣的读者可以参考 David Duvenaud 的 内核菜谱 (www.cs.toronto.edu/~duvenaud/cookbook/)。

这个讨论标志着第三章的结束。在本章中,我们广泛地研究了我们的 GP 模型是如何受到均值和协方差函数的影响,特别是它们的各个参数。我们将这视为将我们对目标函数的了解(即先验信息)融入到我们的 GP 模型中的一种方式。我们还学会了使用梯度下降来估计这些参数的值,以获得最佳解释我们数据的 GP 模型。

这也标志着本书的第一部分的结束,我们将重点放在了 GP 上。从下一章开始,我们开始学习 BayesOpt 框架的第二个组成部分:决策制定。我们从两种最常用的 BayesOpt 策略开始,这两种策略旨在改善已见到的最佳点:概率改进和期望改进。

3.5 练习

这个练习是为了练习使用 ARD 实现 GP 模型。为此,我们创建一个目标函数,沿一个轴变化的程度比沿另一个轴变化的程度更大。然后,我们使用来自该函数的数据点训练一个有或没有 ARD 的 GP 模型,并比较学习到的长度尺度值。解决方案包含在 CH03/03 - Exercise.ipynb 中。

这个过程有多个步骤:

  1. 使用 PyTorch 在 Python 中实现以下二维函数:

    这个函数模拟了一个支持向量机(SVM)模型在超参数调整任务中的准确性曲面。x轴表示惩罚参数c的值,y轴表示 RBF 核参数γ的值。(我们在未来的章节中也将使用该函数作为我们的目标函数。)

  2. 在区间[0, 2]²上可视化该函数。热图应该看起来像图 3.17。

    图 3.17 SVM 模型在测试数据集上的准确率作为惩罚参数c和 RBF 核参数γ的函数。函数对γ的变化比对c的变化更快。

  3. 在区间[0, 2]²中随机选择 100 个数据点,作为我们的训练数据。

  4. 使用常数均值函数和 Matérn 5/2 卷积核来实现一个 GP 模型,其中输出规模作为gpytorch.kernels.ScaleKernel对象实现。

  5. 在初始化核对象时不要指定ard_num_dims参数,或将该参数设置为None。这将创建一个没有 ARD 的 GP 模型。

  6. 使用梯度下降训练 GP 模型的超参数,并在训练后检查长度尺度。

  7. 重新定义 GP 模型类,这次设置ard_num_dims = 2。使用梯度下降重新训练 GP 模型,并验证两个长度尺度具有显著不同的值。

总结

  • 先验知识在贝叶斯模型中起着重要作用,可以极大地影响模型的后验预测结果。

  • 使用 GP 模型可以通过均值和协方差函数来指定先验知识。

  • 均值函数描述了高斯过程模型的预期行为。在没有数据的情况下,高斯过程的后验均值预测回归到先验均值。

  • 高斯过程的均值函数可以采用任何函数形式,包括常数、线性函数和二次函数,这可以通过 GPyTorch 实现。

  • 高斯过程的协方差函数控制了高斯过程模型的平滑度。

  • 长度尺度指定了输出与函数输入之间的变异性水平。较大的长度尺度导致更加平滑,因此预测的不确定性较小。

  • 高斯过程中的每个维度都可以有自己的长度尺度。这被称为自动相关性确定(ARD),用于模拟在不同维度上具有不同变异程度的目标函数。

  • 输出尺度指定了函数输出的范围。较大的输出尺度导致更大的输出范围,因此在预测中有更多的不确定性。

  • Matérn 核类是 RBF 核类的泛化。通过指定其参数 nu,我们可以模拟高斯过程预测中的各种平滑程度。

  • 高斯过程的超参数可以通过最大化使用梯度下降的数据的边际似然来进行优化。

第二部分:用贝叶斯优化做决策

GP 只是方程式的一部分。为了充分实现 BayesOpt 技术,我们需要方程式的第二部分:决策策略,这些策略规定了如何进行函数评估以尽快优化目标函数。本部分列举了最流行的 BayesOpt 策略,包括它们的动机、数学直觉和实现。虽然不同的策略受到不同目标的驱使,但它们都旨在平衡勘探和开发之间的权衡——这是 BayesOpt 特别是以及不确定性问题下的决策制定的核心挑战。

第四章通过引入获取分数的概念来开始介绍事物,这是一种量化进行函数评估价值的方法。该章还描述了一种试图从迄今为止看到的最佳点改进的启发式方法,这导致了两种流行的 BayesOpt 策略:改进概率和预期改进。

第五章将 BayesOpt 与一个密切相关的问题联系起来:多臂赌博机。我们探索了流行的上置信界限策略,该策略使用乐观主义下的不确定性启发式,以及 Thompson 抽样策略,该策略利用了 GP 的概率性质来辅助决策。

第六章向我们介绍了信息理论,这是数学的一个子领域,在决策问题中有许多应用。利用信息理论的一个核心概念熵,我们设计了一个名为 BayesOpt 策略,该策略旨在获取关于我们搜索目标的最多信息。

在这一部分中,我们了解了解决勘探-开发权衡的不同方法,并建立了多种优化方法的多样化工具集。虽然我们已经学会了在前一部分中使用 GPyTorch 实现 GPs,但 BoTorch,首要的 BayesOpt 库,是我们在这一部分的重点。我们学习如何声明 BayesOpt 策略,使用它们来促进优化循环,并比较它们在各种任务中的性能。到本部分结束时,您将获得关于实施和运行 BayesOpt 策略的实践知识。

第五章:通过基于改进的政策优化最佳结果

本章包括

  • BayesOpt 循环

  • 在 BayesOpt 政策中开发开发和探索之间的权衡

  • 以改进为标准来寻找新数据点。

  • 使用改进的 BayesOpt 政策

在本章中,我们首先提醒自己 BayesOpt 的迭代性质:我们在已收集的数据上交替训练高斯过程(GP)并使用 BayesOpt 政策找到下一个要标记的数据点。这形成了一个良性循环,在这个循环中,我们的过去数据指导未来的决策。然后我们谈论我们在 BayesOpt 政策中寻找什么:一个决策算法,它决定标记哪个数据点。一个好的 BayesOpt 政策需要平衡足够探索搜索空间并集中在高性能区域。

最后,我们了解了两种政策,这两种政策旨在改进到目前为止 BayesOpt 循环中见过的最佳数据点:改进概率和最常用的 BayesOpt 政策之一,即期望改进。例如,如果我们有一个超参数调整应用程序,我们想要识别在数据集上提供最高验证准确性的神经网络,并且到目前为止我们见过的最高准确性为 90%,那么我们很可能想要改进这个 90%的阈值。本章中学到的策略试图创建这种改进。在我们在表 1.2 中看到的材料发现任务中,我们想要搜索混合温度低(对应高稳定性)的金属合金,而我们找到的最低温度为 187.24,上述两种政策将寻求找到低于这个 187.24 基准的值。

令人惊讶的是,由于我们对目标函数的信念的高斯性质,我们可以期望从最佳观察点的改进程度可以通过封闭形式计算。也就是说,虽然我们不知道未见位置的目标函数的样子,但是在 GP 下,仍然可以轻松地计算基于改进的数量。到本章结束时,我们深入了解了 BayesOpt 政策需要做什么以及如何使用这两个基于改进的政策来完成此操作。我们还学习了如何集成 BoTorch,即我们从本章到本书结束时使用的 Python 中的 BayesOpt 库(botorch.org/docs/introduction),以实现 BayesOpt 政策。

4.1 在 BayesOpt 中导航搜索空间

我们如何确保我们正确利用过去的数据来指导未来的决策?我们在 BayesOpt 政策中寻找什么作为自动决策程序?本节回答了这些问题,并为我们清楚地说明了在使用 GP 时 BayesOpt 的工作原理。

具体来说,在下一小节中,我们将重新检查在第 1.2.2 节中简要介绍的贝叶斯优化循环,以了解如何通过贝叶斯优化策略与高斯过程的训练同时进行决策。然后,我们将讨论贝叶斯优化策略需要解决的主要挑战:在具有高不确定性的区域和在搜索空间中利用良好区域之间的平衡。

以图 4.1 为例,该图显示了在 1 和 2 处训练的两个数据点的高斯过程。在这里,贝叶斯优化策略需要决定我们应该在 -5 和 5 之间的哪个点评估下一个目标函数。探索和利用的权衡是明显的:我们需要决定是否在搜索空间的左右极端(大约在 -5 和 5 附近)检查目标函数,在这些位置,我们的预测存在相当大的不确定性,或者留在平均预测值最高的区域周围。探索和利用的权衡将为我们讨论的不同贝叶斯优化策略奠定基础,这些策略分布在本章和后续章节中。

图 4.1 贝叶斯优化中的探索和利用的权衡。每个策略都需要决定是否查询具有高不确定性的区域(探索),还是查询具有高预测平均值的区域(利用)。

4.1.1 贝叶斯优化循环和策略

首先,让我们回顾一下贝叶斯优化循环的外观以及贝叶斯优化策略在此过程中的作用。在本章中,我们还实现了这个循环的框架,我们将在以后的章节中用来检查贝叶斯优化策略。回顾图 4.2,它是图 1.6 的重复,显示了贝叶斯优化在高层次上的工作方式。

图 4.2 贝叶斯优化循环,结合了模拟高斯过程和决策制定策略。此完整工作流现在可以用于优化黑盒函数。

具体来说,贝叶斯优化是通过一个循环完成的,该循环在以下几个方面交替进行:

  • 对当前训练集进行高斯过程(GP)训练。我们已经在以前的章节中彻底讨论了如何做到这一点。

  • 使用训练有素的高斯过程(GP)对搜索空间中的数据点进行评分,评估它们在帮助我们确定目标函数最优值时的价值(图 4.2 中的步骤 2)。选择最大化此分数的点进行标记,并添加到训练数据中(图 4.2 中的步骤 3)。如何进行此评分由使用的贝叶斯优化策略决定。我们在本章和第 5 和第六章中了解更多有关不同策略的信息。

我们重复此循环,直到达到终止条件,通常是一旦我们已经评估了目标函数达到目标循环迭代次数。这个过程是端到端的,因为我们不仅仅是使用高斯过程进行预测,而是利用这些预测来决定下一步收集哪些数据点,进而推动未来预测的生成。

定义 BayesOpt 循环是模型训练(GP)和数据收集(策略)的良性循环,彼此互相帮助和受益,目标是找到目标函数的最优解。BayesOpt 的良性循环是一个反馈循环,向着具有期望性质的平衡状态迭代;其组件协同作用以实现期望的结果,而不是在导致不良结果的恶性循环中相互恶化。

决定数据点如何根据其在帮助我们实现这一目标的价值方面得分的规则由 BayesOpt 策略决定,因此对于优化性能至关重要。一个好的策略会将高分分配给对优化真正有价值的数据点,从而更快、更高效地指引我们走向目标函数的最优点,而一个设计不良的策略可能会误导我们的实验,并浪费宝贵的资源。

定义 BayesOpt 策略 根据其价值对每个潜在的查询进行评分,从而决定我们应该在下一步查询目标函数的位置(评分最高的地方)。由策略计算的此评分被称为获取分,因为我们将其用作数据获取的方法。

连接到强化学习策略

如果你有强化学习(RL)的经验,你可能会注意到 BayesOpt 策略和 RL 策略之间的联系。在这两种技术中,策略告诉我们在决策问题中应该采取哪些行动。而在 RL 中,策略可能为每个动作分配一个分数,然后我们选择分数最高的动作,或者策略可能只是输出我们应该采取的动作。在 BayesOpt 中,它始终是前者,策略输出一个量化每个可能查询价值的分数,因此我们的工作是确定最大化此分数的查询。

不幸的是,设计一个好的 BayesOpt 策略的问题没有完美的解决方案。也就是说,并没有一种单一的 BayesOpt 策略能够在所有目标函数上始终表现出色。正如我们在本章和后续章节中所见,不同的策略使用不同的焦点和启发式方法。虽然某些启发式方法在某种类型的目标函数上效果良好,但其他启发式方法可能在不同类型的函数上有效。这意味着我们需要接触广泛的 BayesOpt 策略,并了解它们的目的,以便将它们应用于适当的情况——这正是我们将在第 4 至 6 章中所做的。

什么是策略?

每个 BayesOpt 策略都是一个决策规则,根据给定的标准或启发式评分数据点,以确定其在优化中的有用性。不同的标准和启发式导致不同的策略,而没有预先确定的一组 BayesOpt 策略。事实上,BayesOpt 研究人员仍然发布提出新策略的论文。在本书中,我们只讨论实践中最流行和常用的策略。

现在让我们花点时间来实现一个占位的贝叶斯优化循环,从现在开始我们将使用它来检查各种贝叶斯优化策略。这段代码在 CH04/01 - BayesOpt loop.ipynb 中实现。我们需要的第一个组件是一个我们想要使用贝叶斯优化来优化的目标函数。在这里,我们使用熟悉的一维 Forrester 函数作为要最大化的目标函数,它被定义在 -5 到 5 之间。我们还使用 xsys 作为基本事实,在其定义域 [–5, 5] 内计算 Forrester 函数的值:

def forrester_1d(x):                                              ❶
    y = -((x + 1) ** 2) * torch.sin(2 * x + 2) / 5 + 1            ❶
    return y.squeeze(-1)                                          ❶

bound = 5                                                         ❷

xs = torch.linspace(-bound, bound, bound * 100 + 1).unsqueeze(1)  ❷
ys = forrester_1d(xs)                                             ❷

❶ 目标函数的形式,假设未知

❷ 在 -5 到 5 之间的网格上计算的测试数据

我们需要做的另一件事是修改 GP 模型的实现方式,以便它们可以与 BoTorch 中的贝叶斯优化策略一起使用。实现 GP 构成了我们的贝叶斯优化循环的第一步。

由于 BoTorch 就建立在 GPyTorch 之上,因此只需要进行最小的修改。具体来说,我们使用以下 GP 实现,其中除了我们通常的 gpytorch.models.ExactGP 类之外,我们还继承了 botorch.models.gpytorch.GPyTorchModel 类。此外,我们声明了类特定属性 num_outputs 并将其设置为 1。这些是我们需要进行的最小修改,以便将我们的 GPyTorch 模型与 BoTorch 一起使用,后者实现了本章后面我们使用的贝叶斯优化策略:

class GPModel(gpytorch.models.ExactGP,
  botorch.models.gpytorch.GPyTorchModel):  ❶
    num_outputs = 1                        ❶

    def __init__(self, train_x, train_y, likelihood):
        super().__init__(train_x, train_y, likelihood)
        self.mean_module = gpytorch.means.ConstantMean()
        self.covar_module = gpytorch.kernels.ScaleKernel(
            gpytorch.kernels.RBFKernel()
        )
    def forward(self, x):
        mean_x = self.mean_module(x)
        covar_x = self.covar_module(x)
        return gpytorch.distributions.MultivariateNormal(mean_x, covar_x)

❶ 用于 BoTorch 集成的修改

除此之外,我们 GP 实现中的其他一切都保持不变。现在我们编写一个帮助函数,用于在我们的训练数据上训练 GP:

def fit_gp_model(train_x, train_y, num_train_iters=500):
    noise = 1e-4                                     ❶

    likelihood = gpytorch.likelihoods
    ➥.GaussianLikelihood()                          ❷
    model = GPModel(train_x, train_y, likelihood)    ❷
    model.likelihood.noise = noise                   ❷

    optimizer = torch.optim.Adam(model.parameters(),
    ➥lr=0.01)                                       ❸
    mll = gpytorch.mlls.ExactMarginalLogLikelihood
    ➥(likelihood, model)                            ❸

    model.train()                                    ❸
    likelihood.train()                               ❸

    for i in tqdm(range(num_train_iters)):           ❸
        optimizer.zero_grad()                        ❸

        output = model(train_x)                      ❸
        loss = -mll(output, train_y)                 ❸

        loss.backward()                              ❸
        optimizer.step()                             ❸

    model.eval()                                     ❸
    likelihood.eval()                                ❸

    return model, likelihood

❶ 使用梯度下降训练 GP

❷ 声明 GP

❸ 使用梯度下降训练 GP

注意 我们在前几章中使用了所有先前的代码。如果您对某段代码有困难理解,请参考第 3.3.2 节以获取更多细节。

这涵盖了图 4.2 的第 1 步。现在,我们跳过第 2 步,即实现贝叶斯优化策略,并将其留待下一节和未来章节。要实现的下一个组件是可视化到目前为止收集的数据,当前 GP 信念以及一个贝叶斯优化策略如何评分其余数据点。该可视化的目标显示在图 4.3 中,我们在第一章中见过。具体来说,图的顶部面板显示了 GP 模型对真实目标函数的预测,而底部面板显示了由贝叶斯优化策略计算的收购分数。

图 4.3 贝叶斯优化进展的典型可视化。顶部面板显示了 GP 预测和真实的目标函数,而底部面板显示了一个名为期望改进(Expected Improvement)的贝叶斯优化策略所做的收购分数,我们将在第 4.3 节中学习到。

我们已经熟悉如何生成顶部面板,生成底部面板同样简单。这将使用类似于我们在第 3.3 节中使用的辅助函数来完成。该函数接受一个 GP 模型及其似然函数以及两个可选输入:

  1. policy 指的是 BayesOpt 策略对象,可以像调用任何 PyTorch 模块一样调用。在这里,我们将它调用在代表我们搜索空间的网格 xs 上,以获取整个空间的获取分数。我们将在下一节中讨论如何使用 BoTorch 实现这些策略对象,但我们现在不需要更多了解这些对象。

  2. next_x 是使获取分数最大化的数据点的位置,将其添加到正在运行的训练数据中:

def visualize_gp_belief_and_policy(
    model, likelihood, policy=None, next_x=None
):
    with torch.no_grad():
        predictive_distribution = likelihood(model(xs))  ❶
        predictive_mean = predictive_distribution.mean   ❶
        predictive_upper, predictive_lower =             ❶
          ➥predictive_distribution.confidence_region()  ❶

        if policy is not None:                           ❷
            acquisition_score = policy(xs.unsqueeze(1))  ❷

    ...                                                  ❸

❶ GP 预测

❷ 获取分数

❸ 省略

在这里,我们从 GP 和测试数据 xs 中生成预测。请注意,如果未传入 policy,我们不会计算获取分数,在这种情况下,我们也以我们已经熟悉的方式可视化 GP 预测-散点表示训练数据,平均预测的实线,95% CI 的阴影区域:

    if policy is None:
        plt.figure(figsize=(8, 3))

        plt.plot(xs, ys, label="objective", c="r")         ❶
        plt.scatter(train_x, train_y, marker="x", c="k",
            label="observations")                          ❷

        plt.plot(xs, predictive_mean, label="mean")        ❸
        plt.fill_between(                                  ❸
            xs.flatten(),                                  ❸
            predictive_upper,                              ❸
            predictive_lower,                              ❸
            alpha=0.3,                                     ❸
            label="95% CI",                                ❸
        )                                                  ❸

        plt.legend()
        plt.show()

❶ 真实值

❷ 训练数据

❸ 平均预测和 95% CI

请参考第 2.4.4 节以了解这个可视化的基础知识。

另一方面,如果传入了策略对象,我们将创建另一个子图以显示搜索空间中的获取分数:

    else:
        fig, ax = plt.subplots(
            2,
            1,
            figsize=(8, 6),
            sharex=True,
            gridspec_kw={"height_ratios": [2, 1]}
        )

        ...                                                    ❶

        if next_x is not None:                                 ❷
            ax[0].axvline(next_x, linestyle="dotted", c="k")   ❷

        ax[1].plot(xs, acquisition_score, c="g")               ❸
        ax[1].fill_between(                                    ❸
          xs.flatten(),                                        ❸
          acquisition_score,                                   ❸
          0,                                                   ❸
          color="g",                                           ❸
          alpha=0.5                                            ❸
        )                                                      ❸

        if next_x is not None:                                 ❷
            ax[1].axvline(next_x, linestyle="dotted", c="k")   ❷

        ax[1].set_ylabel("acquisition score")

        plt.show()

❶ GP 预测(与以前相同)

❷ 最大化获取分数的点,使用虚线垂直线进行可视化

❸ 获取分数

当传入 policynext_x 时,此函数将创建一个显示根据 BayesOpt 策略的获取分数的较低面板。最后,我们需要实现图 4.2 中 BayesOpt 循环的第 3 步,即(1)找到具有最高获取分数的点,并(2)将其添加到训练数据并更新 GP。对于识别给出最高获取分数的点的第一个任务,虽然在我们的 Forrester 示例中可能在一维搜索空间上进行扫描,但随着目标函数维数的增加,穷举搜索变得越来越昂贵。

请注意,我们可以使用 BoTorch 的辅助函数 botorch.optim.optimize .optimize_acqf(),该函数找到最大化任何 BayesOpt 策略得分的点。辅助函数使用 L-BFGS,一种准牛顿优化方法,通常比梯度下降方法更有效。

我们这样做:

  • policy 是 BayesOpt 策略对象,我们很快就会了解更多。

  • bounds 存储了我们搜索空间的边界,在本例中为-5 和 5。

  • q = 1 指定我们希望辅助函数返回的点数,这是一个。 (在第七章中,我们学习了允许同时对目标函数进行多次查询的设置。)

  • num_restartsraw_samples 分别表示在搜索给出最高获取分数的最佳候选项时 L-BFGS 使用的重复次数和初始数据点数。一般来说,我建议分别使用这些参数的维数的 20 倍和 50 倍。

  • 返回的值,next_xacq_val,分别是给出最高获取分数的点的位置和相应的最大化获取分数:

next_x, acq_val = botorch.optim.optimize_acqf(
    policy,
    bounds=torch.tensor([[-bound * 1.0], [bound * 1.0]]),
    q=1,
    num_restarts=20,
    raw_samples=50,
)

设置重新启动和原始样本的数量

num_restartsraw_samples 的值越高,当搜索最大化获取分数的最佳候选项时,L-BFGS 的穷举程度就越高。这也意味着 L-BFGS 算法运行的时间将更长。如果发现 L-BFGS 在最大化获取分数时失败,或者算法运行时间太长,可以增加这两个数字;反之,可以减少它们。

作为最后一步,我们在贝叶斯优化循环中汇总了我们到目前为止实现的内容。在该循环的每次迭代中,我们执行以下操作:

  1. 我们首先打印出迄今为止我们看到的最佳值(train_y.max()),这显示了优化的进展情况。

  2. 然后,我们重新在当前训练数据上训练 GP,并重新声明贝叶斯优化策略。

  3. 使用 BoTorch 中的辅助函数 botorch.optim.optimize_acqf(),我们确定在搜索空间中最大化获取分数的点。

  4. 我们调用辅助函数 visualize_gp_belief_and_policy(),该函数可视化我们当前的 GP 信念和优化进展。

  5. 最后,我们在确定的点(next_x)查询函数值并更新我们观察到的数据。

整个过程总结在图 4.4 中,该图显示了贝叶斯优化循环中的步骤及实现这些步骤的相应代码。每个步骤都由我们的辅助函数或 BoTorch 的模块化代码实现,使得整个过程易于跟踪。

图 4.4 贝叶斯优化循环中的步骤及相应的代码。每个步骤的代码是模块化的,这使得整个循环易于跟踪。

实际的代码实现如下:

num_queries = 10                                         ❶

for i in range(num_queries):
    print("iteration", i)
    print("incumbent", train_x[train_y.argmax()], train_y.max())

    model, likelihood = fit_gp_model(train_x, train_y)   ❷

    policy = ...                                         ❸

    next_x, acq_val = botorch.optim.optimize_acqf(       ❹
        policy,                                          ❹
        bounds=torch.tensor([[-bound * 1.0],             ❹
        ➥[bound * 1.0]]),                               ❹
        q=1,                                             ❹
        num_restarts=20,                                 ❹
        raw_samples=50,                                  ❹
    )                                                    ❹

    visualize_gp_belief_and_policy(model, likelihood, policy,
        next_x=next_x)                                   ❺

    next_y = forrester_1d(next_x)                        ❻

    train_x = torch.cat([train_x, next_x])               ❻
    train_y = torch.cat([train_y, next_y])               ❻

❶ 可以进行的目标函数评估次数

❷ 更新当前数据上的模型

❸ 初始化贝叶斯优化策略,稍后讨论

❹ 找到给出最高获取分数的点

❺ 可视化当前 GP 模型和获取分数

❻ 在确定的点观察并更新训练数据

有了这一点,我们已经实现了一个 BayesOpt 循环的框架。现在唯一要做的就是用我们想要使用的实际 BayesOpt 策略来填充policy的初始化,然后笔记本就能在 Forrester 函数上运行 BayesOpt 了。请注意,虽然调用visualize_gp_belief_and_policy()不是必需的(也就是说,之前的 BayesOpt 循环仍然能够运行而不需要那一行代码),但是这个函数对我们观察 BayesOpt 策略的行为和特性以及诊断任何潜在问题是有用的,正如我们后面在本章中讨论的那样。

BayesOpt 策略最重要的特征之一是探索和利用之间的平衡,这是许多人工智能和机器学习问题中的一个经典权衡。在这里,发现我们目前不知道的高性能区域(探索)的可能性与集中在已知的良好区域(利用)的机会之间进行权衡。我们将在下一小节中更详细地讨论这种权衡。

4.1.2 平衡探索和利用

在这一小节中,我们讨论了决策过程中固有的一个问题,包括 BayesOpt 在内:在充分探索整个搜索空间和及时利用产生良好结果的区域之间的平衡。这一讨论将帮助我们形成对什么是一个好的 BayesOpt 策略的理解,并让我们意识到我们所学到的每个策略如何解决这种权衡。

为了说明探索和利用的权衡,想象一下你正在一家你之前只去过几次的餐厅用餐(见图 4.5)。你知道这家餐厅的汉堡很棒,但你不确定他们的鱼和牛排是否好吃。在这里,你面临着一个探索与利用的问题,你需要在尝试点可能是一道很好的菜肴(探索)和点你经常吃但可靠的餐点(利用)之间做出选择。

图 4.5 在餐厅点菜具有固有的探索(尝试新事物)与利用(点常吃的)的权衡。

过度的探索可能会导致你点到你不喜欢的东西,而持续的利用则可能会导致你错过你真正喜欢的一道菜。因此,两者之间的合理平衡至关重要。

这个无处不在的问题不仅存在于点餐中,也存在于人工智能的常见问题中,比如强化学习、产品推荐和科学发现。在 BayesOpt 中,我们面临着同样的权衡:我们需要充分探索搜索空间,以便不错过一个好的区域,但我们也应该专注于具有高客观价值的区域,以确保我们在优化方面取得进展。

注意:“具有高目标值的区域”指的是由输入 x 产生高输出 f(x) 值的区域,这是我们优化(特别是最大化)任务的目标。

让我们回到我们的代码示例,并假设当我们刚开始时,我们的训练数据集包含 Forrester 目标函数的两个观察值,分别为 x = 1 和 x = 2:

train_x = torch.tensor([
    [1.],
    [2.]
])
train_y = forrester_1d(train_x)

model, likelihood = fit_gp_model(train_x, train_y)

print(torch.hstack([train_x, train_y.unsqueeze(1)]))

这给出了输出

tensor([[1.0000, 1.6054],
        [2.0000, 1.5029]])

这表明点 1 处的评估大约为 1.6,而点 2 处的评估为 1.5。通过可视化训练好的 GP 所做出的预测,我们获得了图 4.6 中熟悉的样式。该图显示了我们面临的探索与利用之间的权衡:我们应该在不确定性较高的地方评估目标函数,还是应该留在平均预测较高的区域?

图 4.6:由 GP 训练的两个数据点的预测来自 Forrester 函数

每个 BayesOpt 策略都有一种不同的方式来处理这种权衡,因此,在如何最好地探索搜索空间方面提供了不同的建议。在图 4.6 中,一些策略可能会导致我们进一步探索未知区域,而另一些策略可能会建议我们聚焦于已知的高价值区域。同样,通常没有一种一刀切的方法(即,没有一种策略始终表现良好)。

4.2 寻找 BayesOpt 中的改进

我们几乎已经准备好在给定目标函数上运行 BayesOpt 了。现在我们需要一个具有关于我们搜索目标中每个潜在标记数据点的价值的评分规则的策略。同样,我们将看到的每个策略都提供了一个不同的评分规则,这些规则是受到不同的优化启发式的驱使的。

在这一部分,我们学习了一种在优化目标时具有直观意义的启发式方法。当我们寻求优化时,这种方法的形式是寻求从迄今为止所见过的最佳点开始改进。在即将到来的小节中,我们将了解到 GPs 有助于促进这一改进度量的计算。然后,我们将介绍不同定义改进的方式如何导致两种最常见的 BayesOpt 策略:提高概率和期望改进。

4.2.1 用 GP 测量改进

在本小节中,我们将讨论改进 BayesOpt 的定义、它如何构成一个良好的效用度量,并且使用正态分布处理与改进相关的量是直接的。因为我们在 BayesOpt 中的最终目标是确定目标函数的全局最优值——给出最高目标值的点——所以我们在评估目标函数时所观察到的值越高,我们的效用就应该越高。假设当我们在某个点x[1]处评估目标函数时,我们观察到值为 2。在另一种情况下,当我们评估另一个点x[2]时,我们观察到值为 10。直观地说,我们应该更重视第二个点,因为它给了我们一个更高的函数值。

但是,如果在观察到x[1]和x[2]之前,我们已经看到一个x[0]的点,它的值为 20 呢?在这种情况下,自然会认为即使x[2]比x[1]好,但两个点都不会带来任何额外的效用,因为我们在x[0]上已经有了一个更好的观察结果。另一方面,如果我们有一个x[3]的点,它的值为 21,那么我们会更高兴,因为我们已经找到了比x[0]更好的值。这在图 4.7 中有所说明。

图 4.7 寻求从观察到的最佳点改进。虽然点x[2]优于点x[1],但两者都“不好”意味着它们没有改进点x[0]。

这些比较指出了在 BayesOpt 中,我们关心的不仅仅是我们观察到的观察值的原始值,还有新发现的观察是否比我们的观察更好。在这种情况下,由于x[0]在函数值方面设定了一个非常高的标准,无论x[1]还是x[2]都不构成改进——至少不是我们在优化中关心的那种改进。换句话说,在优化中一个合理的目标是寻求从迄今为止我们见过的最佳点改进,因为只要我们从观察到的最佳点改进,我们就在取得进步。

定义 观察到的最佳点,或者产生了迄今为止我们找到的最高函数值的点,通常被称为现任。这个术语表示,这个点目前在我们搜索期间查询的所有点中持有最高值。

假设我们对目标函数有一个 GP 的信念,那么检查我们可以期望从观察到的最佳点改进多少可能会很容易。让我们从我们正在运行的 Forrester 函数的示例开始,我们的当前 GP 如图 4.6 所示。

在我们的训练集中的两个数据点中,(x = 1,y = 1.6) 是更好的一个,因为它有一个更高的函数值。这是我们当前的现任。同样,我们正在专注于从这个 1.6 的阈值改进;也就是说,我们希望找到的数据点会产生高于 1.6 的函数值。

从视觉上来看,我们可以将这个基于改进的方法想象为将高斯过程水平切断在现有解处,如图 4.8 所示。较暗颜色突出显示的部分对应于“改进现有解”(在 1.6 处)。任何低于这条线的点都不能构成改进,因此不会给我们带来额外的优化效益。虽然我们不知道某个点-比如x=0-是否会产生更高的函数值,但我们仍然可以尝试通过从高斯过程模型中得知的信息来推理它的可能性。

图 4.8 从高斯过程的角度看改进自现有解。高斯过程预测对应的改进自现有解的部分在较深的颜色中突出显示。

推理 x = 0 的概率将产生更高的函数值很容易做到,因为通过查询点 x = 0,我们观察到的改进自现有解正好对应于一个被部分截断的正态分布,如图 4.9 所示。

图 4.9 的左面板包含与图 4.8 相同的高斯过程,该过程在现有解处被切断,并额外显示了在x=0 处的正态分布预测的 CI。在此点 0 处垂直切割高斯过程,我们获得了图 4.9 的右面板,两个面板中的 CI 相同。我们看到,在右面板中正态分布的只有突出显示的部分代表我们可以从现有解中观察到的改进,这是我们关心的。这突出显示的部分是正态分布的一部分,正如我们在接下来的小节中所介绍的那样,这导致了许多数学的便利。

图 4.9 在 0 处对现有解的改进,以较深的颜色突出显示。左面板显示整个高斯过程,而右面板仅显示与 0 处的预测相对应的正态分布(误差栏在两个面板上相同)。在这里,对现有解的改进遵循一个被截断的正态分布。

这些便利不仅适用于 x = 0 。由于高斯过程在任何给定点处的预测是正态分布,因此在任何点处的改进也遵循被截断的正态分布,如图 4.10 所示。

图 4.10 在–2 和 3 处对现有解的改进,以较深的颜色突出显示。左面板显示整个高斯过程,中心面板显示-2 处的预测,右面板显示 3 处的预测。突出显示的部分显示可能的改进,这取决于给定点上的正态分布。

我们看到与图 4.9 相比,在 0 处突出显示了大部分正态分布为可能的改进,以下是正确的:

  • -2 处的预测(中心面板)更糟,因为它只有一半被突出显示为可能改进。这是因为 -2 处的平均预测值大致等于现有值,因此,在 -2 处的函数值改进为 1.6 的可能性是 50-50。

  • 作为另一个例子,右侧面板显示了在 3 处的预测,根据我们关于客观情况的高斯过程的信念,几乎不可能从现有情况改进,因为几乎整个正态分布在 3 处都低于现有门槛。

这表明不同的点将导致不同的可能改进,这取决于高斯过程的预测。

4.2.2 计算改进的概率

在 BayesOpt 中,我们的目标特别明确,即从当前的现有情况中提高。现在,我们终于准备开始讨论旨在实现这一目标的 BayesOpt 策略。在本小节中,我们了解改进概率(PoI),这是一种衡量候选点可能改进的策略。

衡量候选点从现有情况改进的可能性的想法对应于图 4.7 中一个点是“好”的概率的概念。这在第 4.2.1 节中与高斯过程中也有所提及,我们说:

  1. 0 处的点(图 4.9)的正态分布的大部分被突出显示为可能改进。换句话说,它有很高的改进可能性。

  2. -2 处的点(图 4.10 的中心面板)有 0.5 的改进概率,因为其正态分布的一半超过了现有状态。

  3. 3 处的点(图 4.10 的右侧面板)的正态分布大部分在门槛以下,因此它几乎不可能改进。

通过注意到从现有情况改进的概率等于图 4.9 和 4.10 中突出显示部分的面积,我们可以使这一计算更具体。

定义 任何正态曲线下的整个面积都为 1,因此图 4.9 和 4.10 中突出显示部分的面积恰好衡量了该正态随机变量(给定点的函数值)超过现有情况的可能性。

与突出显示的区域的面积对应的量与累积密度函数(CDF)有关,它被定义为一个随机变量取得小于或等于目标值的概率。换句话说,CDF 衡量了突出显示的区域的面积,即突出显示的区域的面积为 1 减去突出显示的区域的面积。

多亏了数学上的便利性,正态分布和高斯过程,我们可以轻松地使用累积分布函数计算此突出区域的面积,这需要相关正态分布的均值和标准差。在计算上,我们可以利用 PyTorch 的torch.distributions.Normal类,该类实现了正态分布并提供了有用的cdf()方法。具体来说,假设我们有兴趣计算 0 点的点能够改进现有情况的可能性。我们将按照图 4.11 中描述的过程进行操作:

  1. 首先,我们使用高斯过程来计算 0 点的均值和标准差预测。

  2. 接着,我们计算以先前的均值和标准差定义的正态曲线下的面积,以现有值为截止点。我们使用累积分布函数进行此计算。

  3. 最后,我们从 1 中减去 CDF 值,以获得候选点的 PoI。

图 4.11

图 4.11 PoI 分数计算的流程图。通过按照此过程,我们可以计算任何候选点能够从现有情况中改进的可能性。

注释 技术上,累积分布函数计算正态分布左侧的部分的面积,因此我们需要从 1 中减去 CDF 的输出,以获取阈值右侧的部分的面积,这对应于可能的改进。

我们首先生成该点处高斯过程的预测:

with torch.no_grad():
    predictive_distribution = likelihood(model(torch.tensor([[0.]])))
    predictive_mean = predictive_distribution.mean     ❶
    predictive_sd = predictive_distribution.stddev     ❷

❶ 0 点处的预测均值

❷ 0 点处的预测标准差

首先,我们使用相应的均值和标准差初始化一个一维正态分布:

normal = torch.distributions.Normal(predictive_mean, predictive_sd)

此正态分布是图 4.9 右侧面板中可视化的正态分布。最后,为了计算突出显示部分的面积,我们调用带有现有值的cdf()方法(即train_y.max(),我们的训练数据的最大值),并从 1 中减去结果:

>>> 1 - normal.cdf(train_y.max())

tensor([0.8305])

在这里,我们的代码显示,在 0 点处,我们有超过 80%的机会从现有情况中改进,这与图 4.9 中突出显示的正态分布的大部分一致。使用相同的计算,我们可以发现-2 点的 PoI 为 0.4948,3 点的 PoI 为 0.0036。通过观察图 4.10,我们可以看到这些数字是合理的。除了这三个点(0、-2 和 3)之外,我们还可以使用相同的公式计算给定点能够改进的可能性,即给定点的 PoI,跨越我们的搜索空间。

定义 图 4.11 中的过程给出了 PoI 策略的评分规则,其中搜索空间中每个点的得分等于该点能够改进现有情况的可能性。同样,这个分数也称为收集分数,因为我们将其用作数据收集的方法。

我们可以看到,此 PoI 策略使用了cdf()方法,但使用BoTorch更干净,这是一个实现 BayesOpt 策略的 Python 库。BoTorch 建立在 PyTorch 和 GPyTorch 之上,并与其无缝合作。正如我们之前所看到的,我们只需更改 GP 类中的两行代码,即可使模型与 BoTorch 兼容。此外,BoTorch 将其策略实现为模块,使我们能够以模块化的方式在 BayesOpt 循环中交换不同的策略。

BoTorch 策略的模块化

通过模块化,我们指的是我们可以在 BayesOpt 循环中用另一个策略替换当前正在使用的策略,只需更改策略的初始化。BayesOpt 循环的其余部分(训练 GP 模型、可视化优化进度和更新训练数据)不必更改。我们在 3.3 和 3.4.2 节中也观察到了与 GPyTorch 的均值函数和核函数类似的模块化。

要使用 BoTorch 实现 PoI 策略,我们执行以下操作:

policy = botorch.acquisition.analytic.ProbabilityOfImprovement( ❶
    model, best_f=train_y.max()                                 ❶
)                                                               ❶

with torch.no_grad():                                           ❷
    scores = policy(xs.unsqueeze(1))                            ❷

❶ 声明 PoI 策略

❷ 计算分数

BoTorch 类ProbabilityOfImprovement将 PoI 实现为 PyTorch 模块,将 GP 作为第一个参数,现有结果值作为第二个参数。变量scores现在存储xs中点的 PoI 分数,xs是-5 到 5 之间的密集网格。

再次,每个点的获取分数等于该点从现有结果改善的概率,根据我们的高斯过程信念。图 4.12 显示了我们的搜索空间中的这个分数,以及我们的高斯过程。

图 4.12 高斯过程预测(顶部)和 PoI(底部),虚线表示最大化 PoI 分数的点。这一点是我们在优化的下一次迭代中查询目标函数的地方。

我们观察到一些有趣的 PoI 分数行为:

  • 在现有结果的左侧区域(从 0 到 1)的 PoI 分数相对较高。这对应于此区域的高均值预测。

  • 绘图左侧区域的得分稍低。这是因为均值预测不那么高,但在这个区域存在足够的不确定性,仍然有相当大的改善概率。

  • 2 周围的区域的 PoI 接近 0。正如我们所见,此处的点的预测正态分布大多位于现有结果阈值下方。

现在,我们要做的就是确定在-5 到 5 之间具有最高 PoI 分数的点,即最大化从现有结果改善的概率的点。如前所述,我们利用 BoTorch 的辅助函数botorch.optim.optimize.optimize_acqf(),该函数找到最大化任何 BayesOpt 策略得分的点。我们使用以下代码执行此操作,该代码是实现 BayesOpt 循环的代码的一部分:

next_x, acq_val = botorch.optim.optimize.optimize_acqf(
    policy,
    bounds=torch.tensor([[-bound], [bound]], dtype=torch.float),
    q=1,
    num_restarts=10,
    raw_samples=20,
)

返回的值是 L-BFGS 找到的给出最高获取分数的点的位置以及相应的最大化获取分数。经过检查,我们有以下结果:

>>> next_x, acq_val

(tensor([[0.5985]]), tensor(0.9129))

此输出表明,最大化 PoI 分数的候选点在 0.91 PoI 处大约在 0.6 附近,这对应于图 4.12 中的点线。这个点就是我们将查询目标函数(即评估函数)以收集 BayesOpt 中的下一个数据点的地方。

具有最高预测均值的候选点

有趣的是,我们选择查询的点(大约在 0.6 附近)并不是具有最高预测均值的点(大约在 -0.5 左右)。后者的 PoI 稍低一些,因为此点的不确定性很高,因此事实上,与现有点相比,它不太可能得到改善,尽管其预测均值很高。

这就是在 BayesOpt 的单次迭代中决定使用 PoI 策略查询哪个点的全部内容。但是请记住图 4.2 中的 BayesOpt 循环,在该循环中我们在寻找下一个要查询的数据点(步骤 2)和使用新数据更新我们的 GP(步骤 1 和 3)之间交替。我们将在第 4.2.3 节中执行此操作。

运行 PoI 策略

在本小节中,我们最终运行 PoI 策略并分析其行为。我们再次重复整个过程——训练模型、声明 PoI 策略,并使用 optimize_acqf() 多次找到最佳点,直到达到终止条件为止。正如我们所见,这个循环在 CH04/01 - BayesOpt loop.ipynb 笔记本中实现。现在,我们需要在适当的 for 循环中初始化 PoI 策略。

此代码生成了一系列由辅助函数 visualize_gp_belief_and_policy() 生成的图,每个图显示了我们 BayesOpt 循环在我们进行的 10 次查询中的当前状态。这些图看起来类似于图 4.12,但增加了我们参考的目标函数:

num_queries = 10

for i in range(num_queries):
    print("iteration", i)
    print("incumbent", train_x[train_y.argmax()], train_y.max())

    model, likelihood = fit_gp_model(train_x, train_y)

    policy = botorch.acquisition.analytic.ProbabilityOfImprovement( ❶
        model, best_f=train_y.max()                                 ❶
    )                                                               ❶

    next_x, acq_val = botorch.optim.optimize_acqf(
    ...                                                             ❷

❶ 我们的 PoI 策略

❷ 已省略

BayesOpt 中的函数评估次数

我们在 BayesOpt 中使用的查询次数完全取决于我们能够承担的函数评估次数。第 1.1 节定义了昂贵的黑盒优化问题,这假设我们可以进行的查询次数相对较低,因为函数评估的成本很高。

还有其他标准可以用来确定何时终止 BayesOpt 循环。例如,当我们达到目标值或最近的 5 或 10 次查询中没有显著改进时,我们可以停止。在整本书中,我们坚持假设我们有一定数量的函数评估是可以进行的。

我们使用 10 次查询作为默认值来运行一维 Forrester 函数的 BayesOpt 策略,并检查策略的行为。本章的练习 2 处理二维函数并使用 20 次查询。

图 4.13 显示了第一、第五和最后一次迭代的绘图。我们发现 PoI 策略始终保持在 0 和 2 之间的区域内,在第 10 次和最后一次迭代中,我们已经收敛到局部最优点。这意味着我们未能充分探索搜索空间,因此错过了右侧区域中目标函数的全局最优点,即约为 4。

图 4.13 是 PoI 策略的进展情况。由于该策略旨在追求任何数量的改进,进展停滞在接近 2 的局部最优点,我们未能探索其他搜索空间区域。

当使用优化辅助函数 optimize_acqf()时,BoTorch 会发出警告。

当在上一页中运行 BayesOpt 与 PoI 的代码时,您可能会收到 BoTorch 发出的以下警告:

RuntimeWarning: Optimization failed in
`gen_candidates_scipy` with the following warning(s):
[OptimizationWarning('Optimization failed within `scipy.optimize.minimize`
with status 2 and message ABNORMAL_TERMINATION_IN_LNSRCH.')]
Trying again with a new set of initial conditions.
  warnings.warn(first_warn_msg, RuntimeWarning)

当帮助函数 optimize_acqf()(具体来说是线搜索子程序)未能成功优化收集得分(在这种情况下为 PoI 得分)时,会显示此警告。当收集得分函数高度不平滑时(例如,在图 4.13 的最后一个面板中,x=1.5 周围出现了一个尖峰),数值优化不稳定,这种故障经常发生。

不用详细了解优化例行程序,我们可以在使用 optimize_acqf()时增加重启次数(num_restarts 参数)和原始样本数(raw_samples 参数),这样可以增加发现拥有最高收集得分的数据点的机会。

为了便于讲解,从现在开始,在我们的代码中运行帮助函数 optimize_acqf()时,我们关闭此警告,使用警告模块中的上下文管理器:

with warnings.catch_warnings():
    warnings.filterwarnings('ignore', category=RuntimeWarning)
    next_x, acq_val = botorch.optim.optimize_acqf(...)

注意 尽管图 4.13 中的 PoI 表现可能令人失望(毕竟,我们已经花了很多时间构建这个看起来过度利用的 PoI 策略),但分析发生的情况将为我们提供改进性能的见解。

我们注意到,虽然 PoI 停留在局部最优点,但它正在实现其所需的功能。具体来说,由于 PoI 旨在改善当前的 incumbent,因此策略发现缓慢向右移动将以较高的概率实现这一目标。虽然 PoI 通过缓慢移动不断发现更多的改进,但我们将这种行为视为过度利用,因为该策略没有充分探索其他区域。

重要 换句话说,即使 PoI 正在做的事情符合我们最初想要实现的目标——即从在职者中进行改进,但结果的行为并不是我们想要的。这意味着,单纯追求从在职者处的任何改进都不是我们所关心的。

修复这种过度利用行为有两种方法。第一种是限制我们所说的 改进 的含义。我们对 PoI 的实验表明,在每次迭代中,策略只会通过缓慢朝着 GP 认为函数向上移动的方向移动,从现有状况中找到微小的改进。

如果我们重新定义所谓的 改进,即只有当改进比当前现有状况值至少大 ε 时才有效,并相应修改 PoI,那么策略将更有可能更有效地探索搜索空间。这是因为 GP 将知道停留在局部最优点不会导致与现有状况的显著改进。图 4.14 说明了这个想法。

图 4.14

图 4.14 通过要求改进至少为 ε = 0(左)和 ε = 2(右)来定义对 改进 更严格的定义。要求越大,PoI 策略就越具有探索性。更多细节请参见练习 1。

我们不会在这里深入讨论,但练习 1 探讨了这种方法。有趣的是,我们会观察到要求 PoI 观察到的改进越多,策略就越具有探索性。

4.3 优化改进的预期值

如前一节所示,天真地试图从现有状况中寻求改进会导致来自 PoI 的过度利用。这是因为简单地沿着适当方向微移离开现有状况就能获得高的 PoI。因此,优化这个 PoI 并不 是我们想要做的。在本节中,我们将学习进一步考虑我们可能观察到的改进的 幅度。换句话说,我们也关心我们能从现有状况中获得多少改进。这将引导我们进入最流行的贝叶斯优化策略之一:期望改进(EI)。

寻求考虑改进幅度的动机是明确的。考虑图 4.15 中的例子。

图 4.15

图 4.15 PoI(左)和 EI(右)之间的差异。前者只关心我们是否从现有状况改进,而后者考虑了改进的幅度。

左侧面板显示了 PoI 策略的计算,它只考虑每个候选数据点是否从现有状况改进。因此,稍微改进和显着从现有状况改进的点被平等地对待。

另一方面,右侧面板展示了如果我们还考虑可能改进的幅度会发生什么。在这里,虽然点 x[1] 和 x[2] 仍然被视为不理想(因为它们并未从现有状况 x[0] 改进),但 x[4] 被认为比 x[3] 更好,因为前者提供了更大的改进。同样,x[5] 被认为是五个候选点中最好的。

当然,这并不意味着我们现在可以简单地设计选择从现有指标获得最大改进的策略。我们仍然需要知道我们将观察到多少(如果有的话)改进,这只有当我们实际查询目标函数时才能发现。

注意 尽管我们不知道我们将观察到的改进的确切值,但我们可以以概率的方式推断每个候选点的改进量的大小。

回想一下,在图 4.9 和 4.10 中,我们有一个表示在给定点观察到的改进的截断正态分布。通过计算突出显示区域的面积,我们获得了一个点将从现有指标改进的概率,从而得到了 PoI 策略。然而,我们也可以执行其他计算。

定义 除了 PoI 之外,我们还可以计算与突出显示区域对应的随机变量的期望值。我们处理截断正态分布的事实使我们能够以封闭形式计算这个期望值。使用这个度量来评分数据点的 BayesOpt 策略被称为期望改进(EI)。

虽然 EI 得分的闭式公式不像 PoI 那样简单,但 EI 的评分公式仍然很容易计算。直观地说,使用改进的期望值可能比 PoI 更好地平衡探索和利用。毕竟,周围的点可能以很高的概率改进,但它们的改进可能很小(这是我们在实验中经验性地观察到的)。

一个远离我们不太了解的点可能会给出较低的改进概率,但因为有机会这个点会有较大的改进,EI 可能会给它分配较高的分数。换句话说,虽然 PoI 可能被认为是一种风险规避的 BayesOpt 策略,它关心的是从现有指标改进,无论改进有多小,但 EI 在风险和回报之间平衡,找到最好平衡权衡的点。图 4.16 对比了使用相同数据集和训练的 GP 的 PoI 和 EI。

图片

图 4.16 显示了 PoI(左)和 EI(右)之间的区别。EI 更好地平衡了探索和利用。

我们看到 PoI 选择的候选数据点(大约在 0.6 附近)与 EI 选择的数据点(大约在 –0.7 附近)不同。前者接近当前的指标,因此查询它有助于我们改进的可能性较大。然而,EI 看到远离现有指标的其他区域存在更大的不确定性,这可能导致更大的改进。由于这种推理,EI 更倾向于提供更好的探索和利用平衡的数据点。

EI 的另一个好的特性是它如何给具有相同预测均值或标准差的数据点分配获取分数。具体而言,它通过以下方式来实现:

  • 如果两个数据点具有相同的预测均值但不同的预测标准差,那么具有更高不确定性的数据点将获得更高的分数。因此,该策略奖励探索。

  • 如果两个数据点具有相同的预测标准差但不同的预测均值,那么具有更高均值的数据点将获得更高的分数。因此,该策略奖励开发。

这是 BayesOpt 策略的一个期望,因为它表达了我们在一切相等时对探索的偏好(即当预测均值相等时)但也表达了我们在一切相等时对开发的偏好(即不确定性相等时)。我们在第五章中再次可以看到这一特性,另一种 BayesOpt 策略称为置信上界

从计算上讲,我们可以使用几乎与 PoI 代码相同的代码将 EI 初始化为 BayesOpt 策略对象:

policy = botorch.acquisition.analytic.ExpectedImprovement(
    model, best_f=train_y.max()
)

现在,让我们使用 EI 策略重新运行整个 BayesOpt 循环,确保我们从相同的初始数据集开始。这产生了图 4.17,与 PoI 的图 4.13 进行比较。

图 4.17 显示了 EI 策略的进展。与 PoI 相比,该策略在探索和开发之间取得了更好的平衡,并在最后找到了全局最优解。

在这里,我们可以看到,尽管 EI 最初仍然集中在 2 附近的局部最优区域,但策略很快探索了搜索空间中的其他区域,以寻找对现有情况的更大改进。在第五次迭代中,我们可以看到我们现在正在检查左侧的区域。最后,在使用了所有的 10 个查询之后,EI 成功地确定了目标函数的全局最优解,优于上一节的 PoI。

注意:由于其简单性和在探索和开发之间的自然平衡,EI 是 BayesOpt 中最常用的策略之一,如果没有其他原因可以优先选择其他策略,该策略是一个很好的默认选择。

4.4 练习

本章中有两个练习:

  1. 第一个练习涵盖了使用 PoI 进行探索,通过改变我们对改进的定义。

  2. 第二个示例涵盖了使用 BayesOpt 进行超参数调整,目标函数模拟了 SVM 模型的精度曲面。

4.4.1 练习 1:通过 PoI 促进探索

解决 PoI 过度开发的一个方法是设定更高的标准来确定什么是改进。具体来说,我们发现单纯地找到最大化从现状改进的可能性的点会阻止我们摆脱局部最优解。

作为解决方案,我们可以修改策略以指定我们仅接受至少 ε 的改进。这将指导 PoI 在局部区域已经足够覆盖后,在搜索空间中寻找改进的其他区域。在 CH04/02 - Exercise 1.ipynb 中实现此练习,并演示其对 PoI 的积极影响。其步骤如下:

  1. 重新创建 CH04/01 - BayesOpt loop.ipynb 中的 BayesOpt 循环,其中将一维的 Forrester 函数作为优化目标。

  2. 在实现 BayesOpt 的 for 循环之前,声明一个名为 epsilon 的变量。此变量将作为鼓励探索的最小改进阈值。暂时将此变量设置为 0.1。

  3. for 循环内,像以前一样初始化 PoI 策略,但这次指定由 best_f 参数设置的现任阈值为现任值 加上 存储在 epsilon 中的值。

  4. 重新运行笔记本,并观察此修改是否比原始 PoI 策略更好地促进了更多的探索。

  5. PoI 变得更具探索性的程度在很大程度上取决于存储在 epsilon 中的最小改进阈值。将此变量设置为 0.001,观察当改进阈值不够大时是否能成功促进探索。当此值设置为 0.5 时会发生什么?

  6. 在上一步中,我们看到将改进阈值设置为适当的值对 PoI 非常关键。然而,如何在多个应用和目标函数中进行这样的设置并不明显。一个合理的启发式方法是将其动态设置为现任值的某个 α 百分比,指定我们希望看到现任值增加 1 + α。在代码中实现这一点,并设置 110% 的改进要求。

4.4.2 练习 2:用于超参数调整的 BayesOpt

此练习,在 CH04/03 - Exercise 2.ipynb 中实现,将 BayesOpt 应用于模拟 SVM 模型超参数调整任务的准确度表面的目标函数。x-轴表示惩罚参数 C 的值,而 y-轴表示 RBF 核参数 γ 的值。有关更多详细信息,请参阅第三章中的练习。步骤如下:

  1. 重新创建 CH04/01 - BayesOpt loop.ipynb 中的 BayesOpt 循环:

    1. 我们不再需要 Forrester 函数;相反,复制第三章中练习描述的二维函数的代码,并将其用作目标函数。

    2. 请注意,此函数的定义域为 [0, 2]²。

  2. 使用 xs 声明相应的测试数据,表示域的二维网格,ys 表示 xs 的函数值。

  3. 修改可视化优化进展的辅助函数。对于一维目标函数,很容易可视化 GP 预测以及收购积分。对于这个二维目标,该辅助函数应生成两个面板的绘图:一个显示基本事实,另一个显示获得分数。两个面板也应显示标记数据。绘图应类似于图 4.18。

    图 4.18 一个参考,显示可视化 BayesOpt 进展的辅助函数应该是什么样子。左面板显示真实的目标函数,而右面板显示收购分数。

  4. 从第三章的练习中复制 GP 类,该类使用 ARD 实现 Matérn 2.5 核。进一步修改此类,使其与 BoTorch 集成。

  5. 复用辅助函数 fit_gp_model() 和实现 BayesOpt 的 for 循环:

    1. 初始培训数据集应包含域中心的点:(1, 1)。

    2. 由于我们的搜索空间是二维的,请在 botorch.optim.optimize_acqf() 中设置 num_restarts=40raw_samples=100,以更详尽地搜索最大化收购分数的点。

    3. 将我们可以查询的数量设置为 20(我们可以评估目标函数的次数)。

  6. 对这个目标函数运行 PoI 策略。注意到该策略再次陷入了局部最优。

  7. 运行修改后的 PoI,其中最小改进阈值设置为 0.1:

    1. 参见练习 1,了解有关为 PoI 设置最小改进阈值的更多详细信息。

    2. 注意到此修改再次导致更好的优化性能。

    3. 在哪一轮迭代中达到了至少 90% 准确度?实现此准确度的模型参数是什么?

  8. 对这个目标函数运行 EI 策略:

    1. 注意到这种策略优于 PoI。

    2. 在达到至少 90% 的准确度的第一轮迭代是什么?实现此准确度的模型参数是什么?

  9. 检查基于单次 BayesOpt 运行的策略的性能可能会引导错误结论。更好地进行多次 BayesOpt 实验,使用不同的起始数据重复实验:

    1. 实现重复实验的想法,并可视化 10 个实验中的平均入职价值和误差线。

    2. 每个实验都应从搜索空间中均匀抽样的单个数据点开始。

    3. 运行我们列出的策略,比较它们的性能。

这标志着我们关于改进的基于 BayesOpt 策略的章节的结束。重要的是要记住在这里使用 Forrester 函数实现 BayesOpt 循环的代码,因为我们将在未来的章节中再次使用它来基准测试其他策略。具体来说,在第五章中,我们将了解受多臂老丨虎丨机问题启发的 BayesOpt 策略。

总结

  • 贝叶斯优化策略使用训练后的高斯过程来评分每个数据点在我们寻找目标函数最优解的过程中的价值。策略计算的得分被称为获取得分。

  • 在贝叶斯优化循环的每次迭代中,基于观察到的数据来训练高斯过程,策略建议一个新的数据点来查询,并且将此点的标签添加到训练集中。这个过程重复进行,直到我们不能再进行任何函数评估为止。

  • GPyTorch 模型只需要进行最小的修改就可以集成到实现贝叶斯优化策略的 BoTorch 中。

  • BoTorch 提供了一个名为optimize_acqf()的帮助函数,该函数来自优化模块optim .optimize,它接受一个策略对象,并返回最大化获取得分的数据点。

  • 一个好的贝叶斯优化策略需要平衡探索(学习高度不确定的区域)和利用(缩小高绩效区域)。

  • 不同的贝叶斯优化策略以不同的方式处理探索-利用的权衡。检查优化进度以分析和调整所使用策略的性能是很重要的。

  • 在贝叶斯优化中可能使用的一种启发式方法是找到比最佳值更好的点。

  • 寻找能够最有可能从最佳值改进的点得到了 PoI 策略。

  • 寻找从最佳值改进的期望提高最高的点得到了 EI 策略。

  • PoI 可以被认为是一种过度的利用和风险规避的策略,因为该策略仅仅旨在从最佳值改进,无论改进多小。没有任何进一步的修改,EI 往往比 PoI 更好地平衡探索和利用。

  • 由于对函数值的高斯信念,通过 PoI 和 EI 计算分数可以在闭合形式下完成。因此,我们可以轻松地计算和优化这些策略定义的分数。

第六章:使用老丨虎丨机风格策略探索搜索空间

本章涵盖了

  • 多臂老丨虎丨机问题及其与 BayesOpt 的关系

  • 在 BayesOpt 中的上限置信度策略

  • 在 BayesOpt 中的 Thompson 抽样策略

在赌场应该玩哪台老丨虎丨机以最大化你的收益?你如何制定一个策略,智能地尝试多台老丨虎丨机并缩小最有利可图的机器?这个问题与 BayesOpt 有什么关系?这些是本章将帮助我们回答的问题。

第四章是我们对 BayesOpt 策略的介绍,它决定了如何探索和检查搜索空间。BayesOpt 策略的探索策略应该指导我们朝着我们想要优化的目标函数的最优解前进。我们学到的两个特定策略是改进概率(PoI)和期望改进(EI),它们利用了我们希望从到目前为止看到的最佳目标值中改进的想法。这种基于改进的思维方式只是一种启发式方法,因此并不构成 BayesOpt 的唯一方法。

在本章中,我们学习了另外两个直接来自于与决策制定问题密切相关的 多臂老丨虎丨机(MAB)的 BayesOpt 策略。作为在赌场玩哪台老丨虎丨机最有利可图的问题,MAB 问题为许多决策不确定性问题设置了舞台。MAB 有着悠久的历史和广泛的研究,为这个问题开发了许多良好的解决方案。正如我们在本章中所学到的,MAB 和 BayesOpt 是非常相似的问题——都处理决策不确定性的优化——因此预期是 MAB 问题的解决方案也将在 BayesOpt 上表现良好。

首先,我们简要讨论 MAB 问题及其与 BayesOpt 的关系。这个讨论提供了一些关于问题的背景,并将 BayesOpt 与 AI 中的其他问题联系起来。然后,我们学习了 MAB 中两个最流行的策略,并将它们应用到 BayesOpt 中。第一个是 上限置信度 策略,它使用 面对不确定性的乐观主义 原则来推断其决策。第二个策略被称为 Thompson 抽样,它是一种主动利用我们预测模型的概率性质的随机化解决方案。然后,我们在我们的运行示例上实现并运行这些策略,并分析它们的性能。

通过本章末尾,我们对 MAB 问题及其与 BayesOpt 的关系有了了解。更重要的是,我们将 BayesOpt 策略的投资组合增加了两项,使我们暴露于在黑盒优化问题中探索搜索空间的更多方式。

5.1 MAB 问题简介

在本节中,我们从高层次学习 MAB 问题。我们首先讨论它的问题陈述和设置在第一小节。

重要提示 在 MAB 问题中,我们需要在一个长时间段内每一步选择一个要采取的动作。每个动作根据未知的奖励率产生奖励,我们的目标是在长时间段结束时最大化我们获得的总奖励。

我们还探讨了它与 BayesOpt 的关系,以及 AI 和 ML 中的其他问题。这为我们提供了背景,将 MAB 与文本的其余部分联系起来。

5.1.1 寻找赌场最好的老丨虎丨机

虽然多臂老丨虎丨机可能在你的脑海中勾起神秘的形象,但这个术语实际上指的是一个赌徒在赌场选择玩哪些老丨虎丨机以获取最大奖励的问题。想象一下,你在一家赌场,有一台可以拉动“手臂”的老丨虎丨机。

拉动老丨虎丨机的手臂时,你可能会得到硬币作为奖励;但是,这个过程中存在随机性。具体来说,在这台老丨虎丨机的内部机制中编程了一个奖励概率p。每次拉动机器的手臂时,机器都会根据该概率返回硬币。这台老丨虎丨机在图 5.1 中可视化。如果p=0.5,则该机器向其玩家奖励一半的时间。如果p=0.01,则大约只有 100 次拉动中的 1 次会导致返回硬币。由于这个概率被编程在机器内部——因此隐藏在我们看不见的地方——我们无法确定这个概率是多少。

图 5.1 一个带有可以拉动手臂的老丨虎丨机。当拉动手臂时,根据其奖励概率,该机器可能返回硬币。

在这个假设的情景中,赌场调整其老丨虎丨机的程序,以便这些机器向玩家奖励硬币的速度低于玩家所玩的机器和收到硬币的机器。换句话说,即使偶尔会有赢家从这些老丨虎丨机中获得奖励,平均而言,赚取利润的是赌场。

定义 特别不成功的玩家,他们比赢得硬币的速度更快地失去硬币,可能会不情愿地称呼他们一直在玩的老丨虎丨机为“强盗”,认为机器在偷他们的钱。由于这台机器有一个可以拉动的手臂,因此可以称为单臂强盗

现在,想象一下,不只是一个老丨虎丨机,而是一排我们可以选择玩的老丨虎丨机,每个都有自己的奖励概率p,如图 5.2 所示。

图 5.2 一个带有可以拉动手臂的老丨虎丨机。当拉动手臂时,根据其奖励概率,该机器可能返回硬币。

对于这一排老丨虎丨机,一个有战略眼光的玩家可能会将这种设置转化为一个决策挑战,并试图以某种智能的方式尝试这些老丨虎丨机,以便尽快确定哪台老丨虎丨机具有最高的奖励概率。他们的目的是在只能拉动这些机器的手臂特定次数的情况下最大化他们获得的奖励量。

定义 这个决策问题被称为多臂老丨虎丨机(或 MAB),因为我们可以拉动多个手臂。目标是设计一个策略,决定接下来应该拉动哪个机器的手臂,以最大化我们最终获得的总奖励。

我们发现 MAB 具有许多不确定性优化问题的特征,例如 BayesOpt:

  • 我们可以采取具体的行动。每个行动对应于拉动特定老丨虎丨机的手臂。

  • 我们有限的预算。我们只能在特定次数内拉动这些手臂,直到我们不得不停止。

  • 在可能采取的行动的结果中存在不确定性。我们不知道每台老丨虎丨机的奖励概率是多少,甚至在我们多次拉动其手臂之前我们也无法估计。此外,拉动手臂后,我们不能确定是否会收到硬币,因为奖励中存在随机性。

  • 我们想要为一个目标进行优化。我们的目标是最大化累积奖励,即我们在拉动这些机器的手臂直到停止时收到的硬币总数。

或许最重要的是,在 MAB 中,我们面临与我们在 4.1.2 节中讨论的探索-利用权衡相同的问题。特别是,每次我们决定拉动一台机器的手臂时,我们需要在迄今为止给我们较好成功率的机器(利用)和其他奖励概率我们了解不多的机器(探索)之间做出选择。

我们面临的问题是一个权衡,因为通过探索,我们可能会冒着将我们的拉动浪费在奖励概率低的机器上的风险,但过度利用意味着我们可能会完全错过一个奖励率比我们目前观察到的更高的机器。图 5.3 展示了一个例子,其中以下情况为真:

  1. 我们在第一台机器上拉了 100 次手臂,收集了 70 枚硬币。也就是说,迄今为止第一台机器提供的经验成功率最高,为 70%。

  2. 我们从第二台机器收集了更多数据,因此我们对其奖励率的不确定性最小,大约为 50%。

  3. 尽管第三台机器的经验成功率最低(0%),但我们可能需要尝试更多次拉动它的手臂,以更确定其奖励率。

图 5.3 一个展示探索-利用困境的 MAB 数据集示例。MAB 策略必须在一个成功率持续高的机器和一个奖励率不确定的机器之间做出选择。

与贝叶斯优化策略类似,多臂老丨虎丨机策略的工作是查看过去奖励的数据,并决定我们接下来应该拉动哪个臂,同时平衡探索和开发之间的权衡。多臂老丨虎丨机问题模拟了你可能在现实世界中看到的广泛应用的一系列应用场景:

  • 在产品推荐中,引擎需要从商店中的许多产品中选择一个向用户推荐。每个产品都可以看作是一个老丨虎丨机的臂,拉动臂意味着引擎选择该产品向用户展示。如果用户点击了该产品的广告,我们可以视为收到了奖励,因为用户的点击是我们想要实现的目标。

  • 许多资源管理问题可以被构建为多臂老丨虎丨机问题,其中我们需要考虑如何最好地将不同资源分配给不同的组织,以最佳地优化一些高级目标(例如,利润或生产力),而不知道每个组织的运作效果。投资组合管理也可以以同样的方式构建为多臂老丨虎丨机问题。

  • 多臂老丨虎丨机问题也在临床试验设计中得到应用,其中每个患者需要被分配到特定的治疗中。我们希望优化所有患者的治疗结果,但需要处理有限的资源以及确定每个患者从给定治疗中受益的可能性。

在这些应用中,我们可以采取一组行动——也就是说,一组可以拉动的臂——以在不确定性下优化一个目标。

5.1.2 从多臂老丨虎丨机到贝叶斯优化

我们已经看到多臂老丨虎丨机和贝叶斯优化具有许多共同的特征。在这两个问题中,我们需要思考我们应该采取什么决策,以便我们可以最大化我们关心的数量。此外,每个行动的结果都不是确定的。这意味着我们不知道一个行动是否会产生好的结果,直到我们真正采取行动为止。

但是,这两个问题并不相等。在多臂老丨虎丨机问题中,我们的目标是随着时间的推移最大化累积奖励——也就是说,接收到的硬币总数。而在贝叶斯优化中,我们只是寻找一个导致高价值的函数输入;只要我们收集的数据集中有一个良好的目标值,我们就会成功优化。这种单值目标有时被称为简单奖励。这种差异意味着我们需要确保在多臂老丨虎丨机问题中频繁地获得奖励,以维持良好的累积奖励,而在贝叶斯优化中,我们可以更加勇于探索,以潜在地找到一个良好的目标值。

定义术语简单奖励并不意味着目标更容易或更简单地优化,而是目标是一个单一的数字,而不是累积奖励的总和。

此外,在多臂老丨虎丨机中只有有限数量的动作可供选择(我们可以拉动的手臂数量有限)。在 BayesOpt 中,由于我们试图优化连续域中的目标函数,有无限多个动作。由于我们假设使用 GP 时接近点的函数值彼此相似,我们可以将这看作是彼此接近的动作产生类似的奖励率。这在图 5.4 中有所说明,其中每个微不足道的点都是我们可以拉动其手臂的老丨虎丨机(即,查询目标函数值),彼此接近的机器具有相似的颜色。

图 5.4 BayesOpt 类似于具有无限多个动作的 MBA 问题。每个微不足道的点都是一个老丨虎丨机,我们可以拉动其手臂。此外,彼此接近的机器在某种意义上是相关的,因为它们具有类似的奖励率。

大多数多臂老丨虎丨机问题的形式化考虑了二进制设置,即拉动老丨虎丨机时,要么返回一个硬币,要么什么也不返回。另一方面,在 BayesOpt 中我们可能观察到的函数值可以取任意实数值。

多臂老丨虎丨机和 BayesOpt 之间的主要区别在表 5.1 中总结。虽然这些是根本性的差异,但在决策中探索和利用之间的权衡在两个问题中都存在,因此将多臂老丨虎丨机策略重新用于 BayesOpt 是合理的。

表 5.1 多臂老丨虎丨机和 BayesOpt 之间的区别

标准 多臂老丨虎丨机 BayesOpt
要最大化的目标 累积奖励 简单奖励
观察/奖励类型 二进制 实值
动作数量 有限 无限
动作之间的相关性 是对于相似的动作

在本章的其余部分,我们将学习两种这样的策略,它们背后的动机以及如何使用 BoTorch 实现它们。我们将在下一节从上置信界限策略开始。

5.2 在上置信界限策略下面对不确定性的乐观主义

我们应该如何考虑在特定位置评估目标函数时可能观察到的无限多种可能性的值?此外,我们应该如何以简单、高效的方式推理这些可能性,以促进决策?在本节中,我们探讨了多臂老丨虎丨机中的上置信界限(UCB)策略,该策略导致了同名的 BayesOpt 策略。

UCB 遵循面对不确定性的乐观主义原则。在多臂老丨虎丨机中,思想是使用每个老丨虎丨机奖励率的估计上界作为真实的未知奖励率的替代品。也就是说,我们乐观地估计每台机器的奖励率,使用我们认为的奖励率的上界,最后选择具有最高上界的机器。

我们首先讨论这个原则以及它如何辅助我们的决策推理。然后,我们学习如何使用 BoTorch 实现 UCB 策略并分析其行为。

5.2.1 不确定情况下的乐观主义

让我们举个简单的例子来具体说明这个想法。假设你某一天醒来发现外面虽然是晴天,但地平线上有乌云。你查看手机上的天气应用程序,看看今天是否会一直晴朗,以及是否应该带伞上班以防下雨。不幸的是,该应用程序无法确切地告诉你天气是否会晴朗。相反,你只能看到晴天的概率估计在 30%到 60%之间。

你心里想,如果晴天的概率低于 50%,那么你会带伞。然而,这里并不是一个单一值的估计,而是一个介于 30%到 60%之间的范围。那么,你应该如何决定是否需要带伞呢?

悲观主义者可能会说,因为晴天的概率可能只有 30%,所以你应该采取保险措施,做最坏的打算。考虑平均情况的人可能会进一步研究应用程序,看看晴天的平均估计概率是多少,然后做出相应的决定。而从乐观主义者的角度来看,60%的机会足以让人相信天气会晴朗,所以这个人不会打算带伞去上班。这些思考方式如图 5.5 所示。

在图 5.5 中,第三个人的推理对应于 UCB 策略背后的思想:对未知事件的结果持乐观态度,并根据这种信念做出决策。在多臂老丨虎丨机问题中,UCB 会构建每个老丨虎丨机奖励率的上限,并选择具有最高上限的老丨虎丨机。

图 5.5 关于未来思考和做出决策的不同方式。最后一个人对应于 UCB 策略,以乐观的方式推理一个未知量。

注:在 BayesOpt 中实现这一策略的方式特别简单,因为我们使用高斯过程作为目标函数的预测模型,我们已经有了每个动作的奖励率的上限。也就是说,我们已经有了在任何给定输入位置的目标值的上限。

具体来说,我们知道给定位置的目标值遵循正态分布,而量化正态分布不确定性的常用度量是 95% CI,其中包含分布的 95% 的概率质量。使用 GP,我们将该 95% CI 在输入空间中可视化为图 5.6 中粗线。该 95% CI 的上限,也就是图 5.6 中突出显示的阴影区域的上边界,正是 UCB 策略将用作搜索空间中数据点的获取分数的上边界,并且给出最高分数的点是我们将评估下一个目标函数的位置,这在本例中是由虚线围绕的位置约为 –1.3。

图 5.6 对应于 95% CI 的 GP 的 UCB。此边界可用作 UCB 策略的获取分数。

获取分数 的定义量化了数据点在引导我们朝着目标函数的最优值的过程中的价值。我们首次在 4.1.1 节了解到了获取分数。

您可能认为这种决策方式可能不合适,特别是在决策成本高昂的高风险情况下。在我们的伞例子中,通过乐观地低估下雨的概率,您可能会冒着没有伞就被雨淋的风险。

然而,这是一种特别高效的推理方式,因为我们只需要提取我们对感兴趣数量的估计的一个上界,并将该边界用于决策。此外,正如我们在下一小节中看到的那样,通过选择我们想要使用的 CI(而不是坚持使用 95% CI),我们完全控制 UCB 有多乐观。这种控制还允许策略平衡探索和利用,这是任何 BayesOpt 策略需要解决的核心问题。

5.2.2 平衡探索和利用

在本小节中,我们进一步讨论了我们,BayesOpt 用户,如何调整 UCB 策略。这提供了一种在高不确定性区域(探索)和高预测均值区域(利用)之间平衡的控制水平。这次讨论旨在在下一小节 BoTorch 实现之前更深入地理解 UCB。

请记住,对于正态分布,从平均值(即,均值 μ 加/减 2 倍标准差 σ)偏离两个标准偏差会给我们带来 95% CI。这个区间的上限(μ + 2σ)就是我们在上一小节中看到的 UCB 的获取分数。

95% 置信区间(CI)不是正态分布的唯一 CI。通过在公式 μ + βσ 中设置标准差 σ 的乘法器(表示为 β),我们可以获得其他 CI。例如,在一维正态分布中,如图 5.7 所示,以下结论成立:

  • 离均值上一个标准差(μ + σ)——即设置β = 1——给出了 68%的置信区间:正态分布的 68%的概率质量位于μ - σ和μ + σ之间。

  • 同样,距离均值三个标准差(β = 3)给出了 99.7%的置信区间。

图 5.7 展示了标准正态分布的不同置信区间。从均值偏离一个、两个和三个标准差,我们得到了 68%、95%和 99.7%的置信区间。UCB 策略使用这些间隔的上界。

实际上,任何β的值都可以给我们一个唯一的正态分布置信区间。由于 UCB 只指示我们应该使用来自预测模型的上界来做决策,所以形如μ + βσ的任何值都可以作为 UCB 中使用的上界。通过设置此参数β,我们可以控制 UCB 策略的行为。

图 5.8 展示了对应于β = 1、2、3 的三个不同的上界,而事实上,均值函数对应于设置β = 0. 我们可以看到,尽管上界的形状大致相同,但这些上界以不同的速率上下波动。此外,由于最大化此上界的数据点是 UCB 选择进行优化查询的点,不同的上界,即不同的β值,将导致不同的优化行为。

图 5.8 展示了 GP 的不同上界,对应不同的置信区间和β值。β越大,UCB 策略就越趋向于探索性。

重要的是,β越小,UCB 就越趋向于剥削性。相反,β越大,UCB 就越趋向于探索性。

我们可以通过检查获取分数的公式μ + βσ来看到β值控制 UCB 行为的方式。当β值很小时,平均值μ对获取分数做出的贡献最大。因此,具有最高预测均值的数据点将最大化此分数。此选择对应纯粹的剥削性,因为我们只是选择具有最大预测值的点。另一方面,当β值很大时,标准差σ,即量化我们的不确定性,在 UCB 分数中变得更加重要,强调了探索的需求。

我们可以从图 5.8 中看出这个差异,其中 0 附近是我们实现最高预测均值的地方,表明这是开发区域。随着β的增加,不同上界峰值所在的点逐渐向左移动,在这里我们的预测更加不确定。

注意有趣的是,在β → ∞的极限情况下,最大化 UCB 获取分数的点是使标准差σ最大化的点,即我们的不确定性最大化的点。这种行为对应纯粹的探索性,因为我们选择具有高度不确定性的数据点。

最后,UCB 正确地为提供在探索和开发之间提供更好平衡的数据点分配更高的分数:

  • 如果两个数据点具有相同的预测均值但不同的预测标准差,则具有较高不确定性的数据点将具有更高的分数。因此,该策略奖励探索。

  • 如果两个数据点具有相同的预测标准差但不同的预测均值,则具有较高均值的数据点将具有更高的分数。因此,该策略奖励利用。

记住,在第四章讨论的策略中,EI 也具有这种特性,这是任何贝叶斯优化策略的期望。

总的来说,这个参数 β 控制 UCB 以及策略如何探索和利用搜索空间。这意味着通过设置该参数的值,我们可以直接控制其行为。不幸的是,除了 β 的值对应于探索程度这一事实之外,没有一种直观的、原则性的方法来设置该参数,某些值可能在某些问题上有效,但在其他问题上却无效。本章的练习 1 进一步讨论了一种更为复杂的设置 β 的方法,该方法可能通常工作得足够好。

注意 BoTorch 的文档通常显示 UCB 的 β = 0.1,而许多使用 UCB 进行贝叶斯优化的研究论文选择 β = 3。所以,如果你更偏向利用,0.1 可以是你使用该策略的首选值,如果你更倾向于探索,3 应该是默认值。

5.2.3 在 BoTorch 中实现

在充分讨论了 UCB 的动机和数学之后,我们现在学习如何使用 BoTorch 实现该策略。我们在这里看到的代码包含在 CH05/01 - BayesOpt loop.ipynb 中。请记住,虽然我们可以手动实现 PoI 策略,但声明 BoTorch 策略对象,将其与 GP 模型一起使用,并使用 BoTorch 的辅助函数 optimize_acqf() 优化获取分数,这样可以更轻松地实现我们的贝叶斯优化循环。

出于这个原因,我们在这里做同样的事情,并使用内置的 UCB 策略类,尽管我们可以简单地自己计算 μ + βσ 的数量。这可以通过以下方式完成

policy = botorch.acquisition.analytic.UpperConfidenceBound(
    model, beta=1
)

在这里,BoTorch 中的 UCB 类实现将 GP 模型作为其第一个输入,并将正值作为其第二个输入 beta。正如你所料,这第二个输入表示 UCB 参数 β 在评分公式 μ + βσ 中的值,该公式在探索和利用之间进行权衡。在这里,我们暂时将其设置为 1。

注意 信不信由你,这就是我们需要从上一章的贝叶斯优化代码中更改的全部内容,以在我们拥有的目标函数上运行 UCB 策略。这展示了 BoTorch 模块化的好处,它允许我们将任何我们想要使用的策略插入我们的贝叶斯优化流程中。

使用 β = 1 运行我们的 UCB 策略在我们熟悉的 Forrester 目标函数上生成图 5.9,它显示了在 10 个函数评估过程中的优化进展。我们看到,与 PoI 策略发生的情况类似,对于 Forrester 函数来说,β = 1 的 UCB 未能充分探索搜索空间,并且在一个局部最优解上停滞不前。这意味着 β 的值太小。

图 5.9 UCB 策略在权衡参数 β = 1 时的进展。参数值不足以鼓励探索,导致进展停滞在局部最优解。

注意:我们在第 4.2 节了解到了改进概率策略。

让我们再试一次,这次将这个权衡参数设置为一个更大的值:

policy = botorch.acquisition.analytic.UpperConfidenceBound(
    model, beta=2
)

这个版本的 UCB 策略的进展如图 5.10 所示。这次,由于更大的 β 值引起的探索水平更高,UCB 能够找到全局最优解。然而,如果 β 太大,以至于 UCB 只花费预算来探索搜索空间,那么我们的优化性能也可能会受到影响。(我们稍后在本章练习中会看到一个例子。)

图 5.10 UCB 策略在权衡参数 β = 2 时的进展。该策略成功找到了全局最优解。

总的来说,使用 UCB 策略时,使用一个好的权衡参数值的重要性是明显的。然而,同样地,很难说什么值会对给定的目标函数起作用良好。本章的练习 1 探索了一种调整这个参数值的策略,随着搜索的进行。

这标志着我们对 UCB 的讨论结束了。我们已经看到,通过从多臂赌博问题中采用面对不确定性的乐观态度思维模式,我们得到了一个贝叶斯优化策略,其探索行为可以直接通过一个权衡参数进行控制和调整。在下一节中,我们将继续介绍从多臂赌博问题中采用的第二个策略,它具有完全不同的动机和策略。

5.3 使用 Thompson 采样策略进行智能采样

在这一节中,我们将学习关于多臂赌博问题中的另一种启发式方法,它直接转化为一个被广泛使用的贝叶斯优化策略,称为Thompson 采样(TS)。正如我们将看到的,这个策略使用了与 UCB 完全不同的动机,因此会引发不同的优化行为。与第 5.2 节中的 UCB 类似,我们先学习这个贝叶斯优化策略的一般思想,然后再进入其代码实现。

5.3.1 用一个样本来代表未知

使用 UCB,我们根据我们关心的未知量的乐观估计来做出决策。这提供了一种简单的方式来推理我们所采取的行动和我们所获得的奖励的后果,这种方式在探索和开发之间进行权衡。那么 TS 呢?

定义 Thompson sampling 的理念是首先维持我们关心的数量的概率信念,然后从该信念中抽样,并将该样本视为我们感兴趣的真实未知量的替代。然后使用这个抽样替代物来选择我们应该做出的最佳决策。

让我们回到我们的天气预报示例,看看这是如何运作的。同样,我们对是否应该带雨伞去工作的问题感兴趣,在这个问题中,我们得到了一份天气将全天保持晴朗的概率估计。请记住,UCB 依赖于这个估计的上限来指导其决策,但 TS 策略会做什么呢?

TS 首先从我们用来模拟未知数量的概率分布中抽取一个样本——在这种情况下,是否会晴朗——然后根据这个样本做出决策。假设我们手机上的天气应用现在宣布有 66%(大约三分之二)的机会天气会保持晴朗。这意味着在遵循 TS 策略时,我们首先抛一枚硬币,其正面有三分之二的倾向:

  • 如果硬币是正面(有 66% 的几率),那么我们会把它视为天气全天晴朗,并得出我们不需要雨伞的结论。

  • 如果硬币是反面(有 34% 的几率),那么我们会把它视为会下雨,并得出我们应该带雨伞去工作的结论。

这个 TS 过程在图 5.11 中被可视化为一个决策树,在开始时,我们抛一个有偏的硬币,以获取晴天的概率分布样本。根据硬币是正面(代表晴天的样本)还是反面(代表下雨的样本),我们决定是否要带雨伞去工作。

图 5.11 TS 策略作为决策树。我们抛一个有偏的硬币以获取晴天的概率分布样本,并根据此样本决定是否带雨伞。

虽然这乍一看可能是一种任意的决策方法,但 TS 在 MAB 和 BayesOpt 中特别有效。首先,鉴于表示感兴趣数量的概率分布,该分布的样本是该数量的可能实现,因此该样本可以用作分布的表示。在不确定性优化问题中,TS 提供了与 UCB 相同的好处,即从概率分布中抽样通常很容易。就像 UCB 对奖励率的乐观估计可能以高效的方式生成一样,抽样也可以同样高效地完成。

让我们考虑 TS 在 BayesOpt 中的工作原理。从在当前观察到的数据上训练的 GP 中绘制一个样本。从第三章记得,从 GP 中绘制的样本是表示我们的 GP 信念下目标函数特定实现的函数。然而,与在没有观察到数据的区域中未知的真实目标函数不同,从 GP 中绘制的样本是完全已知的。这意味着我们可以找到使得这个样本被最大化的位置。

图 5.12 显示了我们在 Forrester 函数上训练的 GP 及其绘制的三个样本作为虚线。此外,沿着每个样本,我们使用一个菱形来指示最大化样本的位置。正如我们所看到的,一个样本在 –3.2 处最大化,另一个在 –1.2 处最大化,第三个在 5 处。

图 5.12 绘制自 GP 的样本和最大化相应样本的数据点。无论绘制哪个样本,TS 都会选择最大化该样本的数据点作为下一个要评估目标函数的点。

当使用 TS 策略时,完全是由偶然决定我们从 GP 中绘制这三个样本中的哪一个(或者一个完全不同的样本)。然而,无论我们绘制哪个样本,最大化样本的数据点都是我们将要查询的下一个点。也就是说,如果我们绘制出在 –3.2 处最大化的样本,那么我们将在 –3.2 处评估目标函数。如果我们绘制出在 5 处最大化的样本,那么我们将查询点 x = 5。

定义 TS 计算的获取分数是从 GP 中绘制的随机样本的值。最大化此样本的数据点是我们将要查询的下一个点。

与我们迄今为止见过的其他策略不同,TS 是一种随机策略,这意味着当面对相同的训练数据和 GP 时,不保证策略会做出相同的决定(除非我们在计算上设置了随机种子)。然而,这种随机性并不是一种不利。我们已经说过,从 GP 中绘制样本是很容易的,因此可以有效地计算 TS 获取分数。此外,对随机样本的最大化本质上在探索和利用之间取得平衡,这正是我们在 BayesOpt 中的主要关注点:

  • 如果数据点有很高的预测均值,那么该数据点处的随机样本的值很可能很高,使其更有可能是最大化样本的那个数据点。

  • 如果数据点具有较高的预测标准偏差(即不确定性),那么该数据点处的随机样本也将具有较高的变异性,因此更有可能具有较高的值。因此,这种更高的变异性也使得更有可能选择该数据点作为下一个要查询的点。

TS 可能利用具有高预测均值的区域中最大化的随机样本,但在相同情况下,该策略也可能利用另一个样本进行探索。我们在图 5.12 中看到了这一点,其中一个样本在–1.2 左右最大化,具有相对较高的预测均值。如果这是我们抽取的样本,那么通过在–1.2 处评估目标函数,我们将在利用函数。然而,如果我们抽取另外两个样本中的任何一个,那么我们将在探索,因为样本最大化的区域具有高的不确定性。

这是一个相当优雅的权衡方案。通过使用样本的随机性,TS 直接利用了我们预测模型 GP 的概率性质来探索和利用搜索空间。TS 的随机性意味着在 BayesOpt 循环的任何给定迭代中,该策略可能不会做出最佳决策以权衡探索和利用,但随着时间的推移,在其决策的聚合中,该策略将能够充分探索空间并缩小高性能区域。

5.3.2 使用 BoTorch 实现

现在让我们转而在 Python 中实现这个 TS 策略。再次提醒,代码包含在 CH05/01 - BayesOpt loop.ipynb 笔记本中。请记住,对于我们见过的 BayesOpt 策略,实现归结为声明一个 BoTorch 策略对象并指定相关信息。然后,为了找到下一个应该查询的数据点,我们优化策略计算出的获取分数。然而,这与 TS 不同。

实现 TS 作为通用的 PyTorch 模块的主要挑战是,从 GP 中取样只能使用有限数量的点。过去,我们用密集网格上的高维 MVN 分布的样本来表示 GP 的样本。当绘制这些密集高斯的样本时,它们看起来像是具有平滑曲线的实际函数,但实际上它们是在网格上定义的。

所有这些都是为了说,从 GP 中绘制函数样本在计算上是不可能的,因为这将需要无限数量的比特。一个典型的解决方案是在跨越输入空间的大量点上绘制相应的 MVN,以便搜索空间中的所有区域都得到表示。

重要提示:这正是我们实现 TS 的方法:在搜索空间中生成大量点,并从这些点上的 GP 预测中绘制 MVN 分布的样本。

TS 的流程总结在图 5.13 中,我们使用 Sobol 序列 作为跨度搜索空间的点。 (我们稍后讨论为什么 Sobol 序列优于其他采样策略。)然后,我们从这些点上的 GP 中抽取一个样本,并挑选出从样本中产生的值最高的点。 然后,该样本用于表示 GP 本身的样本,并且我们在下一步中评估目标函数在采样值最大化的位置处。

图 5.13 BoTorch 中 TS 实现的流程图。我们使用 Sobol 序列填充搜索空间,在序列上从 GP 中抽取一个样本,并选择使样本最大化的点以评估目标函数。

定义 Sobol 序列 是欧几里得空间中一个无限点列表,旨在均匀覆盖该区域。

让我们首先讨论为什么我们需要使用 Sobol 序列来生成跨度搜索空间的点。 一个更简单的解决方案是使用密集网格。 但是,随着搜索空间维数的增长,生成密集网格很快变得难以处理,因此这种策略是不可行的。 另一个潜在的解决方案是从该空间均匀采样,但是统计理论表明均匀采样实际上不是生成均匀覆盖空间的最佳方法,而 Sobol 序列则做得更好。

图 5.14 显示了一个由 100 个点组成的 Sobol 序列与在二维单位正方形内均匀采样的相同数量的点的比较。 我们看到 Sobol 序列覆盖正方形更均匀,这是我们希望使用 TS 实现的目标。 在更高维度中,这种对比更加明显,这更加增加了我们更喜欢 Sobol 序列而不是均匀采样数据点的理由。 我们这里不详细讨论 Sobol 序列; 对我们重要的是要知道 Sobol 序列是覆盖空间均匀的标准方法。

图 5.14 Sobol 序列中的点与二维单位正方形中均匀采样的点的比较。Sobol 序列覆盖正方形更均匀,因此应该被 TS 使用。

PyTorch 提供了 Sobol 序列的实现,可以如下使用:

dim = 1                   ❶
num_candidates = 1000     ❷

sobol = torch.quasirandom.SobolEngine(dim, scramble=True)
candidate_x = sobol.draw(num_candidates)

❶ 空间的维数

❷ 要生成的点的数量

在这里,sobolSobolEngine 类的一个实例,它实现了单位立方体中 Sobol 序列的采样逻辑,而 candidate_x 是形状为 (num_candidates, dim) 的 PyTorch 张量,其中包含了具有正确维度的生成点。

注意 重要的是要记住 SobolEngine 生成覆盖单位立方体的点。 要使 candidate_x 覆盖我们想要的空间,我们需要相应地调整此张量的大小。

Sobol 序列应该包含多少个点(即 num_ candidates 的值)由我们(用户)决定;前面的例子展示了我们使用的是 1,000。在典型情况下,您会想要一个足够大的值,以便搜索空间被足够覆盖。然而,一个值太大会使从后验 GP 中采样数值上不稳定。

绘制 GP 样本时的数值不稳定问题

在绘制 GP 样本时的数值不稳定性可能会导致我们在运行 TS 时出现以下警告:

NumericalWarning: A not p.d., added jitter of 1.0e−06 to the diagonal
  warnings.warn(

这个警告表明,代码遇到了数值问题,因为 GP 的协方差矩阵不是正定 (p.d.) 的。然而,这个代码也应用了自动修复,其中我们向这个协方差矩阵中添加了 1e–6 的“抖动”,使矩阵成为正定的,所以我们用户不需要再做任何事情。

与我们在 4.2.3 节中所做的一样,我们使用 warnings 模块来禁用此警告,使我们的代码输出更干净,如下所示:

with warnings.catch_warnings():
    warnings.filterwarnings('ignore', category=RuntimeWarning)
    ...            ❶

❶ TS 代码

您可以玩耍数千个点来找到最适合您的用例、目标函数和训练 GP 的数量。然而,您应该至少使用 1,000 个点。

接下来,我们转向 TS 的第二个组成部分,即从我们的后验 GP 中采样 MVN 并最大化它的过程。首先,实现取样的采样器对象可以被声明为:

ts = botorch.generation.MaxPosteriorSampling(model, replacement=False)

BoTorch 中的 MaxPosteriorSampling 类实现了 TS 的逻辑:从 GP 后验中采样并最大化该样本。这里,model 指的是所观察数据上训练的 GP。重要的是将 replacement 设为 False,确保我们是无替换采样(替换采样不适用于 TS)。最后,为了获得在 candidate_x 中最大样本值的数据点,我们将其传递给采样器对象:

next_x = ts(candidate_x, num_samples=1)

返回的值确实是最大化样本的点,这是我们下一个查询的点。有了这一点,我们的 TS 策略实现就完成了。我们可以将这个代码插入到迄今为止我们用于 Forrester 函数的贝叶斯优化循环中,并使用以下代码:

for i in range(num_queries):
  print("iteration", i)
  print("incumbent", train_x[train_y.argmax()], train_y.max())

  sobol = torch.quasirandom.SobolEngine(1, scramble=True)           ❶
  candidate_x = sobol.draw(num_candidates)                          ❶
  candidate_x = 10 * candidate_x − 5                                ❷

  model, likelihood = fit_gp_model(train_x, train_y)

  ts = botorch.generation.MaxPosteriorSampling(model,
  ➥replacement=False)                                              ❸
  next_x = ts(candidate_x, num_samples=1)                           ❸

  visualize_gp_belief_and_policy(model, likelihood, next_x=next_x)  ❹

  next_y = forrester_1d(next_x)

  train_x = torch.cat([train_x, next_x])
  train_y = torch.cat([train_y, next_y])

❶ 从 Sobol 引擎生成点

❷ 调整生成的点的大小为 −5 到 5,即我们的搜索空间。

❸ 生成下一个要查询的 TS 候选

❹ 在没有采集函数的情况下可视化我们的当前进度

请注意,我们的 BayesOpt 循环的总体结构仍然相同。不同的是,我们现在有一个 Sobol 序列来生成涵盖我们搜索空间的点集,然后将其馈送到实现 TS 策略的 MaxPosteriorSampling 对象中,而不是 BoTorch 策略对象。变量 next_x,就像之前一样,包含我们将查询的数据点。

注意:由于在使用 visualize_gp_belief_and_policy() 辅助函数可视化过程中,我们没有 BoTorch 策略对象,因此不再指定 policy 参数。因此,该函数仅显示每个迭代中的训练好的 GP,而没有获取分数。

图 5.15 TS 策略的进展。该策略探索搜索空间一段时间后逐渐将关注点锁定在全局最优解上。

图 5.15 显示了 TS 的优化进展,我们可以观察到该策略成功地将关注点锁定在全局最优解上但不是不花费探索空间查询次数。这展示了 TS 在 BayesOpt 中协调探索和开发的能力。

我们讨论了基于 MAB 设置的策略所启发的 BayesOpt 策略。我们已经看到,我们学习的两个策略,UCB 和 TS,每个都使用自然启发式,以高效的方式推理未知量并相应地做出决策。BayesOpt 中的一个挑战,即探索和开发之间的平衡问题,也由这两种策略解决,使这些策略具有良好的优化性能。在本书第二部分的下一章节中,我们将学习另一种常用的启发式决策方法,即使用信息论。

5.4 练习

本章有两个练习:

  1. 第一个练习探索了为 UCB 策略设置权衡参数的潜在方法,该方法考虑了我们在优化过程中的进展情况。

  2. 第二个练习将本章中学习到的两个策略应用于先前章节中看到的超参数调整问题。

5.4.1 练习 1: 为 UCB 设置探索计划

此练习在 CH05/02 - Exercise 1.ipynb 中实现,讨论了一种自适应设置 UCB 策略的权衡参数 β 的方法。正如 UCB 部分中提到的那样,策略的表现严重依赖于此参数,但我们不清楚该如何设置其值。一个值在某些目标函数上可能效果很好,但在其他目标函数上效果很差。

BayesOpt 从业者已经注意到,随着我们收集越来越多的数据,UCB 可能会过于开发。这是因为随着训练数据集的大小增加,我们对目标函数的了解更多,GP 预测的不确定性也会减少。这意味着 GP 产生的 CI 将变得更紧,将 UCB 使用的获取分数的上限移动到了平均预测附近。

然而,如果 UCB 收获分数与平均预测相似,则该策略是开发性的,因为它只查询具有高预测平均值的数据点。这种现象表明,我们观察到的数据越多,我们对 UCB 的探索应该越多。这里,一种渐进的鼓励 UCB 更多探索的自然方式是慢慢增加权衡参数 β 的值,这是我们在这个练习中学习的,按照以下步骤进行:

  1. 重新创建 CH04/02 - Exercise 1.ipynb 中的 BayesOpt 循环,将一维 Forrester 函数用作优化目标。

  2. 我们旨在通过在循环的每个迭代中将其乘以一个常量来逐渐增加权衡参数 β 的值。也就是说,在每次迭代结束时,我们需要使用 beta *= multiplier 更新参数。

    假设我们希望β的值从 1 开始,并在搜索结束时(第十次迭代)达到 10。乘数β的值是多少?

  3. 实现这个调度逻辑,并观察所得的优化性能:

    1. 特别是,尽管这个版本的 UCB 从β=1 开始,但它是否会像参数固定在 1 的版本一样陷入局部最优?

5.4.2 练习 2:BayesOpt 用于超参数调整

这个练习在 CH05/03 - Exercise 2.ipynb 中实现,将 BayesOpt 应用于超参数调整任务中支持向量机模型的准确率表面。x-轴表示罚项参数C的值,y-轴表示 RBF 核参数γ的值。有关更多详细信息,请参见第三章和第四章的练习。按照以下步骤进行:

  1. 重新创建 CH04/03 - Exercise 2.ipynb 中的 BayesOpt 循环,包括实施重复实验的外层循环。

  2. 运行 UCB 策略,并将权衡参数的值设置为 β ∈ { 1, 3, 10, 30 },观察结果的总体表现:

    1. 哪个值导致了过度开发,哪个导致了过度探索?哪个值效果最好?
  3. 运行 UCB 的自适应版本(参见练习 1):

    1. 权衡参数应该从 3 开始,并在 10 结束。

    2. 注意,将结束值从 10 改为 30 不会对优化性能产生太大影响。因此,我们认为这种策略对于该结束值的值是健壮的,这是一个期望。

    3. 比较此自适应版本与其他具有固定 β 的版本的性能。

  4. 运行 TS 策略,并观察其总体表现。

总结

  • MAB 问题由可以执行的一组操作(可以拉动的老丨虎丨机的臂)组成,每个操作根据其特定的奖励率返回奖励。目标是在给定一定数量的迭代之后最大化我们接收到的奖励总和(累积奖励)。

  • MAB 策略根据过去的数据选择下一步要采取的行动。好的策略需要在未被探索的行动和高性能行动之间保持平衡。

  • 与 MAB 问题不同,在 BayesOpt 中我们可以采取无限多的行动,而不是有限个部分。

  • BayesOpt 的目标是最大化观察到的最大回报,这通常被称为简单回报

  • BayesOpt 的回报是相关的:相似的行动会产生相似的回报。这在 MAB 中不一定成立。

  • UCB 策略使用对感兴趣数量的乐观估计来做决策。这种在面对不确定性的乐观主义启发式方法可以平衡探索和利用,其中的权衡参数由我们用户设定。

  • UCB 策略的权衡参数越小,策略就越倾向于停留在已知有高回报的区域,变得更加自利。权衡参数越大,策略就越倾向于查询远离观测数据的区域,变得更加寻求探索。

  • TS 策略从感兴趣的数量的概率模型中抽取样本并使用这个样本来做决策。

  • TS 的随机性质使得策略可以适当地探索和利用搜索空间:TS 有可能选择高不确定性和高预测平均值的区域。

  • 出于计算原因,在实现 TS 时需要更加谨慎。我们首先生成一组点以均匀覆盖搜索空间,然后为这些点从 GP 后验中抽取样本。

  • 为了均匀地覆盖空间,我们可以使用 Sobol 序列生成单位立方体内的点,并将它们缩放到目标空间。

第七章:使用基于熵的策略和信息论的知识

本章内容包括

  • 作为衡量不确定性的信息论量度的熵

  • 通过信息增益减少熵的方法

  • 使用信息理论进行搜索的 BayesOpt 策略

我们在第四章中看到,通过力争从迄今为止取得的最佳值改进,我们可以设计基于改进的 BayesOpt 策略,如改进概率 (POI) 和期望改进 (EI)。在第五章中,我们使用多臂老丨虎丨机 (MAB) 策略获得了上限置信度 (UCB) 和汤普森抽样 (TS),每种策略都使用独特的启发式方法来平衡在搜索目标函数全局最优解时的探索和开发。

在本章中,我们学习了另一种启发式决策方法,这次是利用信息理论来设计我们可以在优化流程中使用的 BayesOpt 策略。与我们所见过的启发式方法(寻求改进、面对不确定性的乐观和随机抽样)不同,这些方法可能看起来独特于与优化相关的任务,信息理论是数学的一个主要子领域,其应用涵盖广泛的主题。正如我们在本章中讨论的,通过诉诸信息理论或更具体地说是,一种以信息量衡量不确定性的量,我们可以设计出以一种有原则和数学上优雅的方式来减少我们对待优化的目标函数不确定性的 BayesOpt 策略。

基于熵的搜索背后的想法非常简单:我们看看我们关心的数量的信息将最大增加的地方。正如我们在本章后面所讨论的,这类似于在客厅寻找遥控器,而不是在浴室里。

本章的第一部分是对信息理论、熵以及在执行动作时最大化我们所接收的信息量的方法的高层级阐述。这是通过重新解释熟悉的二分查找示例来完成的。具备信息理论的基础知识后,我们继续讨论最大化关于目标函数全局最优解的信息的 BayesOpt 策略。这些策略是将信息理论应用于 BayesOpt 任务的结果。与往常一样,我们还学习如何在 Python 中实现这些策略。

在本章结束时,你将对信息理论是什么、熵作为不确定性度量是如何量化的以及熵如何转化为 BayesOpt 有一个工作理解。本章为我们的 BayesOpt 工具包增加了另一个策略,并结束了关于 BayesOpt 策略的本书第二部分。

使用信息论测量知识

信息论是数学的一个子领域,研究如何以原则性和数学性的方式最佳表示、量化和推理信息。在本节中,我们从信息论的角度重新审视了二分查找的思想,这是计算机科学中一个常用的算法。这次讨论随后允许我们将信息论与 BayesOpt 相连接,并为优化问题提出信息论策略。

6.1.1 使用熵来度量不确定性

信息论在计算机科学中特别常见,其中数字信息被表示为二进制(0 和 1)。你可能还记得计算表示给定数量所需的比特数的例子,例如,一个比特足以表示两个数字,0 和 1,而五个比特则需要表示 32 个(2 的五次方)不同数字。这些计算是信息论在实践中的例子。

决策不确定性下,信息论中的重要概念是。熵度量了我们对未知数量的不确定程度。如果将这个未知数量建模为随机变量,熵度量的是随机变量可能取值的变异性。

注意: 这个不确定性度量,熵,与迄今为止我们所称的高斯过程预测中的不确定性有点相似,后者简单地是预测分布的标准差。

在本小节中,我们将进一步了解熵作为一个概念以及如何计算二元事件的伯努利分布的熵。我们展示熵如何成功地量化了对未知数量的不确定性。

让我们回到第一个概率论课的例子:抛硬币。假设你准备抛一枚有偏差的硬币,硬币以概率p(介于 0 和 1 之间)正面朝上,你想要推理这个硬币正面朝上的事件。用二进制随机变量X表示这个事件是否发生(即,如果硬币正面朝上,X = 1,否则X = 0)。那么,我们说X符合参数为p的伯努利分布,并且X = 1 的概率等于p

在这里,X的熵定义为–p log p – (1 – p) log(1 – p),其中log是以 2 为底的对数函数。我们看到这是一个关于硬币正面概率p的函数。图 6.1 展示了p在(0, 1)区间内熵函数的形状,从中我们可以得出一些见解:

  • 熵始终为非负数。

  • p小于 0.5 时,p的增加而增加,在p = 0.5 时达到最高点,然后逐渐下降。

图 6.1 伯努利随机变量的熵作为成功概率的函数。当成功概率为 0.5 时,熵最大化(不确定性达到最高)。

当我们研究我们对硬币是否落在正面的不确定性时,这两种见解都是宝贵的。首先,我们不应该对某事物有负面不确定性,因此熵永远不是负数。更重要的是,熵在中间处达到最大值,当 p = 0.5 时。这是非常合理的:随着 p 离 0.5 越来越远,我们对事件结果的确定性越来越大—硬币是否落在正面。

例如,如果 p = 0.7,则我们更确定它会落在正面上—这里我们的熵约为 0.9。如果 p = 0.1,则我们对结果更确定(这次是它会落在反面上)—这里的熵大约是 0.5。虽然由于对数函数的原因,熵在端点处未定义,但当我们接近任一端点时,熵接近零,表示零不确定性。另一方面,当 p = 0.5 时,我们的不确定性达到最大值,因为我们对硬币是落正面还是反面最不确定。这些计算表明熵是不确定性的合适度量。

熵 vs. 标准差

当我们在之前的章节中使用术语 不确定性 时,我们指的是由 GP 产生的预测正态分布的标准差。分布的 标准差 正如其名称所示,衡量了分布内的值与平均值偏离的程度,因此是一种有效的不确定性度量。

熵,另一方面,是受到信息理论概念的启发,它也是一种有效的不确定性度量。事实上,它是一种更加优雅和通用的方法来量化不确定性,并且能更准确地模拟许多情况下的边缘情况中的不确定性。

定义 对于给定的概率分布,熵被定义为 –Σ[i] p[i] log p[i],其中我们对不同可能的事件按 i 索引进行求和。

我们看到我们用于前述伯努利分布的公式是这个公式的一个特殊情况。我们在本章后面处理均匀分布时也使用了这个公式。

6.1.2 使用熵寻找遥控器

由于熵衡量了我们对感兴趣的数量或事件的知识中有多少不确定性,因此它可以指导我们的决策,帮助我们最有效地减少我们对数量或事件的不确定性。我们在本小节中看一个例子,我们在其中决定在哪里最好地寻找丢失的遥控器。虽然简单,但这个例子呈现了我们在后续讨论中使用的信息论推理,在那里熵被用于更复杂的决策问题。

想象一下,有一天,当你试图在客厅里打开电视时,你意识到找不到通常放在桌子上的遥控器。因此,你决定对这个遥控器进行搜索。首先,你推理说它应该在客厅的某个地方,但你不知道遥控器在客厅的哪个位置,所以所有的位置都是同样可能的。用我们一直在使用的概率推断的语言来说,你可以说遥控器位置的分布在客厅内是均匀的。

图 6.2 是寻找遥控器示例的一个样本平面图。客厅均匀阴影表示遥控器位置在客厅内的分布是均匀的。

图 6.2 可视化了你对遥控器位置的信念,用阴影标示的客厅表示遥控器所在的位置(根据你的信念)。现在,你可能会问自己这个问题:在这个房子里,你应该在哪里找遥控器?认为你应该在客厅里找到遥控器是合理的,而不是在浴室里,因为电视就在那里。但是,如何量化地证明这个选择是正确的呢?

信息理论,特别是熵,通过允许我们推理在客厅与浴室中寻找遥控器后还剩多少熵来提供了一种方法。也就是说,它允许我们确定在客厅里寻找遥控器后与在浴室里寻找遥控器后我们对遥控器位置的不确定性有多少。

图 6.3 显示了在搜索了客厅的一部分后遥控器位置的熵。如果找到遥控器(右上角),则不再存在不确定性。否则,熵仍然会减少(右下角),因为遥控器位置的分布现在更窄了。

图 6.3 显示了一旦搜索了客厅的上部分,遥控器位置的熵如何减少。我们可以推理如下:

  • 如果在搜索的区域内找到遥控器,那么你将不再对其位置有任何不确定性。换句话说,熵将为零。

  • 如果没有找到遥控器,那么我们对遥控器位置的后验信念将被更新为右下角阴影区域。这个分布跨越的区域比图 6.2 中的区域要小,因此不确定性(熵)更小。

无论哪种方式,查找客厅指定部分的内容都会减少熵。那么,如果你决定在浴室里寻找遥控器会发生什么呢?图 6.4 显示了相应的推理:

  • 如果在浴室里找到了遥控器,那么熵仍然会降到零。然而,根据你对遥控器位置的信念,这种情况不太可能发生。

  • 如果遥控器在浴室中找不到,那么你对遥控器位置的后验信念不会从图 6.2 中改变,结果熵保持不变。

图 6.4 在搜索完浴室后,遥控器位置的熵。由于遥控器在浴室中找不到,遥控器位置的后验分布中的熵不变。

在浴室里搜索而没有找到遥控器并不会减少遥控器位置的熵。换句话说,在浴室里寻找并不提供有关遥控器位置的任何额外信息,因此根据信息论,这是次优的决定。

如果遥控器位置的先验分布(关于它在哪里的你的初始猜测)涵盖整个房子而不仅仅是客厅,那么这种比较就不会那么明显了。毕竟,遥控器被误放在客厅外的概率总是很小的。然而,决定在哪里寻找的过程——即能够为你提供有关遥控器位置最大信息的房子部分——仍然是相同的:

  1. 考虑如果找到了遥控器的话,遥控器位置的后验分布,并计算该分布的熵。

  2. 计算如果遥控器没有找到时的熵。

  3. 计算两种情况下的平均熵。

  4. 为考虑寻找的所有位置重复此计算,并选择给出最低熵的位置。

熵提供了一种用信息论方法量化我们对感兴趣的数量的不确定性的方法,利用其在概率分布中的信息。此过程使用熵来识别最大程度减少熵的行动。

注意 这是一个在许多不确定性下决策情况下适用的数学上优雅的过程。我们可以将这种基于熵的搜索过程看作是一种搜索真相的过程,我们的目标是通过最大程度地减少不确定性来尽可能地接近真相。

6.1.3 利用熵进行二进制搜索

为了进一步理解基于熵的搜索,我们现在看看这个过程如何在计算机科学中的经典算法之一:二分查找中体现。您很可能已经熟悉这个算法,所以我们在这里不会详细介绍。对于对二分查找有很好且适合初学者的解释,我推荐阅读 Aditya Bhargava 的《Grokking Algorithms》(Manning,2016)的第一章。从高层次上来说,当我们想要在一个排序列表中查找特定目标数字的位置,使得列表中的元素从第一个到最后一个元素递增时,我们使用二分查找。

提示 二分搜索的思想是查看列表的中间元素并将其与目标进行比较。 如果目标小于中间元素,则我们只查看列表的前一半;否则,我们查看后一半。 我们重复这个列表减半的过程,直到找到目标。

考虑一个具体的例子,我们有一个排序过的 100 个元素列表 [x[1],x[2],...,x[100]],我们想要找到给定目标 z 的位置,假设 z 确实在排序列表中。

图片

图 6.5 在 100 个元素列表上执行二分搜索的示例。 在搜索的每次迭代中,目标被与当前列表的中间元素进行比较。 根据这个比较的结果,我们将第一半或第二半列表从搜索空间中移除。

正如图 6.5 所示,二分搜索通过将列表分为两半进行工作:前 50 个元素和后 50 个元素。 由于我们知道列表是排序过的,我们知道以下内容:

  • 如果我们的目标 z 小于第 50 个元素 x[50],那么我们只需要考虑前 50 个元素,因为最后 50 个元素都大于目标 z

  • 如果我们的目标大于 x[50],那么我们只需要查看列表的后一半。

终止搜索

对于图 6.5 中的每次比较,我们忽略z等于被比较的数字的情况,这种情况下我们可以简单地终止搜索。

平均而言,这个过程可以帮助我们更快地在列表中找到 z 的位置,比在列表中顺序搜索从一端到另一端要快得多。 如果我们从概率的角度来处理这个问题,二分搜索是在排序列表中的数字位置搜索游戏中基于信息理论作出最佳决策的实现目标的方法。

注意 二分搜索策略是寻找 z 的最佳解决方案,使我们能够比其他任何策略更快地找到它,平均来看。

首先,让我们用随机变量 L 表示我们目标 z 在排序列表中的位置。 这里,我们想要使用一个分布来描述我们对这个变量的信念。 由于从我们的角度来看,列表中的任何位置都同样可能包含 z 的值,我们使用均匀分布进行建模。

图 6.6 可视化了这种分布,再次表示我们对 z 位置的信念。 由于每个位置都与其他任何位置一样可能,特定位置包含 z 的概率是均匀的 1 ÷ 100,即 1%。

图片

图 6.6 给出了 100 个元素列表中目标 z 位置的先验分布。 由于每个位置一样可能包含 z,特定位置包含 z 的概率为 1%。

注意 让我们尝试计算这个均匀分布的熵。记住,熵的公式是 –Σ[i] p[i] log p[i],其中我们对不同可能事件 i 进行求和。这等于

因此,我们对 L 先验分布的不确定性大约为 6.64。

接下来,我们解决同样的问题:我们应该如何在这个 100 元素列表中搜索以尽快找到 z?我们通过遵循第 6.1.2 节描述的熵搜索过程来做到这一点,我们的目标是尽量减少我们关心的量的后验分布的熵,也就是说,在这种情况下是位置 L

在检查了给定位置后,我们如何计算后验分布 L 的熵?这个计算要求我们推理在检查了给定位置后我们对 L 可以得出什么结论,这是相当容易做到的。假设我们决定检查第一个位置 x[1]。根据我们对 L 的信念,L 在这个位置的可能性为 1%,而在其余位置的可能性为 99%:

  • 如果 L 确实在这个位置,那么关于 L 的后验熵将降为 0,因为对于这个量再也没有不确定性了。

  • 否则,L 的分布将更新以反映出观察到 z 不是列表的第一个数字这一事实。

图 6.7 将这个过程显示为一个图表,我们需要更新 L 的分布,以便每个位置都有 1 ÷ 99,或大约 1.01% 的概率包含 z。每个位置仍然同样可能,但每个位置的概率稍微增加了一些,因为在这种假设的情况下,我们已经排除了第一个位置。

图 6.7 给出了在检查第一个元素后目标 z 在 100 元素列表中位置的后验分布。在每种情景中,z 在给定位置的概率会相应更新。

注意 再次说明,我们只考虑 z 存在于列表中的情况,所以要么列表中最小的元素 x[1] 等于 z,要么前者小于后者。

按照同样的计算,我们可以得到这个新分布的熵为

再次说明,这是在第二种情况下 z 不在第一个位置的情况下 L 的后验熵。我们需要采取的最后一步来计算在检查第一个位置后的总后验熵是取这两种情况的平均值:

  • 如果 z 在第一个位置,这个可能性是 1%,那么后验熵为 0。

  • 如果 z 不在第一个位置,这个可能性是 99%,那么后验熵为 6.63。

取平均值,我们有 0.01(0)+0.99(6.63)=6.56。因此,平均而言,当我们选择查看数组的第一个元素时,我们期望看到的后验熵为 6.56。现在,为了确定是否查看第一个元素是最佳决定,或者是否有更好的位置可以获取更多关于L的信息,我们需要重复此过程以检查列表中的其他位置。具体而言,对于给定的位置,我们需要

  1. 在检查位置时迭代每个潜在的情况

  2. 计算每个场景中L的后验熵

  3. 基于每个场景的可能性来计算跨场景的平均后验熵

让我们再次为第 10 个位置x[10]做一次;相应的图示在图 6.8 中显示。虽然这种情况与我们刚刚讨论的稍有不同,但基本思想仍然相同。首先,当我们查看x[10]时,可能会出现各种情况:

  1. 第 10 个元素x[10]可能大于z,在这种情况下,我们可以排除列表中的最后 91 个元素,并将搜索集中在前 9 个元素上。在这里,每个位置都有 11%的机会包含z,并且通过使用相同的公式,可以计算后验熵约为 3.17。

  2. 第十个元素x[10]可能正好等于z,在这种情况下,我们的后验熵再次为零。

  3. 第十个元素x[10]可能小于z,在这种情况下,我们将搜索范围缩小到最后 90 个元素。在这种情况下,后验熵约为 6.49。

图片

在检查第 10 个元素时,目标z在 100 个元素列表中的后验分布。在每种情况下,z在给定位置的概率会相应更新。

注意确保自己尝试熵计算,以了解我们是如何得到这些数字的。

最后,我们使用相应的概率对这些熵进行加权平均:0.09(3.17)+0.01(0)+0.9(6.49)=6.13。这个数字表示了预期的后验熵——即,在检查第 10 个元素x[10]后,我们对L的位置z的预期后验不确定性。

与第一个元素x[1]的相同数量,6.56,相比,我们得出结论:平均而言,查看x[10]比查看x[1]给我们更多关于L的信息。换句话说,从信息理论的角度来看,检查x[10]是更好的决定。

但从信息理论的角度来看,什么是最佳决定——哪个给我们L的最多信息?为了确定这一点,我们只需为列表中的其他位置重复刚刚对x[1]和x[10]执行的计算,并挑选出具有最低预期后验熵的位置。

图 6.9 展示了我们所寻找目标的预期后验熵随我们选择的检查位置而变化的情况。我们首先注意到曲线的对称性:只看最后一个位置与只看第一个位置的预期后验熵(不确定性)是相等的;同样地,第 10 个位置和第 90 个位置给我们提供的信息是一样的。

图 6.9 展示了随着检查列表中位置的变化,目标的预期后验熵如何变化。检查中间位置是最优的,可以最小化预期熵。

更为重要的是,我们可以看到只有检查中间位置,也就是列表的第 50 个或第 51 个位置,才能提供最大的信息量。这是因为,一旦我们这样做了,无论我们的目标数字大于还是小于中间数字,我们都能排除一半的列表。但对于其他位置并非如此。如前所述,当我们检查第 10 个数字时,可能能排除列表中的 90 个数字,但这只有 0.1 的概率。而当我们检查第一个数字时,99%的概率是只能排除一个数字。

注意在平均上,只检查中间的数字最大化我们获得的信息。

寻找目标的其余流程遵循同样的程序:计算每个决策可能导致的预期后验熵,然后选择使得后验熵最小的决策。由于每次更新后我们所处理的概率分布总是一个均匀分布,因此最优的检查位置总是还没有被排除的中间位置。

这正是二分查找的策略!从信息论角度来看,二分查找是在有序列表中查找数字的最优解决方案。

用信息论证明二分查找

当我第一次学习这个算法时,我记得曾经认为在数组中间搜索的策略似乎是独特的且来源神秘。然而,我们刚刚从信息理论的角度推导出了相同的解决方案,这明确量化了在服务于获得尽可能多的信息或减少尽可能多的熵的目的下,排除一半的搜索空间的想法。

信息论和熵的应用不仅限于二分搜索。正如我们所见,我们经历的过程可以推广到其他决策问题:如果我们能够用概率分布来建模我们感兴趣的问题,包括未知数量、我们可以采取的行动以及在采取行动时如何更新分布,那么我们可以再次选择在信息理论的框架下的最优行动,这是能够最大程度减少我们对感兴趣的数量的不确定性的行动。在本章的剩余部分,我们将学习如何将这个想法应用到 BayesOpt,并使用 BoTorch 实现由此产生的熵搜索策略。

6.2 BayesOpt 中的熵搜索

采用前一节提出的相同方法,我们在 BayesOpt 中获得了熵搜索策略。主要思想是选择我们的行动,我们的实验,以便我们可以在我们关心的后验分布中减少最大数量的熵。在本节中,我们首先讨论如何在高层次上做到这一点,然后转移到在 BoTorch 中实现。

6.2.1 使用信息理论搜索最优解

在我们的遥控器示例中,我们的目标是在公寓内搜索遥控器,因此希望减少遥控器位置分布的熵。在二分搜索中,过程类似:我们的目标是在列表中搜索我们想要搜索的特定数字的位置,并且我们希望减少该数字的分布的熵。现在,要设计一个熵搜索策略,我们必须确定在 BayesOpt 中使用什么作为我们的目标以及如何使用信息理论来辅助搜索过程,这是我们在这里要学习的。

回想一下我们在使用 BayesOpt 时的最终目标:在黑盒函数的定义域D内搜索函数最大化的位置。这意味着我们的自然搜索目标是使函数的目标值最大化的位置x,即f = f(x)f(x),对于D中的所有x

定义 最优位置x通常称为目标函数f优化器

给定对目标函数f的 GP 信念,存在对优化器x的相应概率信念,它被视为随机变量。图 6.10 显示了一个经过训练的 GP 的示例以及从 GP 中导出的目标优化器x的分布。需要牢记这个分布的一些有趣特征:

  • 分布复杂且多峰(具有几个局部最优解)。

  • 优化器x最有可能的位置在零的左边一点。这是 GP 的预测均值最大化的地方。

  • 优化器x位于端点-5 或 5 的概率不可忽略。

  • x大约为 2 的概率几乎为零,这对应于我们已经观察到的高于f(2)的目标值。

图 6.10 GP 信念(顶部)和函数优化器x的分布(底部)。优化器的分布是非高斯的,相当复杂,这对建模和决策提出了挑战。

这些特征使得建模优化器x的分布变得非常具有挑战性。衡量这个量x分布的最简单方法就是从 GP 中简单地抽取许多样本,并记录每个样本被最大化的位置。事实上,这就是图 6.10 生成的方法。

更糟糕的是,当目标函数的维度(输入向量x的长度或每个x具有的特征数量)增加时,我们需要指数级别的样本来估计x的分布。

定义 这是维度诅咒的一个例子,在机器学习中,它经常用来指代与感兴趣对象的维度相关的许多过程的指数成本。

要在 BayesOpt 中使用熵搜索,我们需要对最优解的位置x的信念建模,使用概率分布。然而,我们无法精确地建模优化器x的分布;相反,我们必须使用从 GP 中抽取的样本来近似它。不幸的是,随着目标函数中维度的增加(x的长度),这个过程很快就变得计算昂贵起来。

注意 实际上,有研究论文在 BayesOpt 中寻求使用熵来搜索优化器x的位置。然而,由此产生的策略通常运行起来成本过高,且未在 BoTorch 中实现。

但这并不意味着我们需要完全放弃在 BayesOpt 中使用信息理论的努力。这只意味着我们需要修改我们的搜索过程,使其更适合计算方法。一个简单的方法是针对除了优化器x之外的其他量,一方面与寻找x相关联,另一方面更容易进行推理。

在优化中的一个感兴趣的量,除了优化器x之外,就是在优化器处实现的最优值f = f(x),它也是一个随机变量,根据我们对目标函数f的 GP 信念而定。正如大家可以想象的那样,了解最优值f可能会告诉我们很多关于优化器x的信息;也就是说,这两个量在信息理论上是相关联的。然而,最优值f比优化器x更容易处理,因为前者只是一个实数值,而后者是一个长度等于目标函数维度的向量。

图 6.11 右侧面板显示了由 GP 诱导的最优值f**的分布示例。我们看到,这个分布大致截断在 1.6 左右,这恰好是我们训练数据集中的现任值;这是有道理的,因为最优值f**必须至少是现任值 1.6。

图 6.11 GP 信念(左上)、函数优化器x**的分布(底部)以及最优值f**的分布(右)。最优值的分布始终是一维的,因此比优化器的分布更容易处理。

注意 将我们的努力集中在*f**的分布上的主要优势在于,不管目标函数的维度如何,该分布始终是一维的。

具体来说,我们可以从这个一维分布中抽样来近似查询后验熵的期望。从这一点出发,我们遵循熵搜索背后的相同思想:选择(近似)最小化期望后验熵的查询,换句话说,最大化熵的减少

定义 通过使用这个期望熵减少量作为获取得分,我们得到了最大值熵搜索(MES)策略。术语最大值表示我们正在使用信息理论来搜索最大值,或目标函数的最优值*f**。

图 6.12 在底部面板显示了我们运行示例中的 MES 获取得分,根据这个信息理论基础的准则,我们应该在约为-2 的位置查询下一个点。MES 策略更喜欢这个位置,因为它既有相对较高的均值又有较高的 CI,因此平衡了探索和利用。

图 6.12 GP 信念(左上)、最优值*f**的分布(右)以及用作获取函数得分的近似期望熵减少量。最优值的分布始终是一维的,因此比优化器的分布更容易处理。

有趣的是,这里的收益情况看起来与优化器*x**的分布有些相似,如图 6.11 所示,我们看到曲线

  1. 在中间某处达到顶峰

  2. 在端点处达到一个非零值

  3. 在 2 附近达到 0,我们确切知道该位置不是最佳的

这表明最优值f**与优化器x密切相关,虽然我们通过改变我们的目标失去了一些信息,但搜索*f是搜索*x**的一个良好代理,并且计算上更容易处理。

6.2.2 使用 BoTorch 实现熵搜索

讨论了 MES 背后的高层思想后,我们现在准备使用 BoTorch 实现它,并将其插入我们的优化管道中。MES 策略由 botorch.acquisition.max_value_entropy_search 中的 qMaxValueEntropy 类实现为一个 PyTorch 模块,类似于我们之前见过的大多数贝叶斯优化策略。当初始化时,这个类接受两个参数:一个 GPyTorch GP 模型和一组将用作前面部分描述的近似过程中样本的点。

尽管有许多方法可以生成这些样本点,但我们从 5.3.2 节学到的一种特殊方式是使用 Sobol 序列,它更好地覆盖了目标空间。总的来说,MES 策略的实现如下所示:

num_candidates = 1000                                    ❶
sobol = torch.quasirandom.SobolEngine(1, scramble=True)  ❶
candidate_x = sobol.draw(num_candidates)                 ❶

candidate_x = 10 * candidate_x - 5                       ❷

policy = botorch.acquisition.max_value_entropy_search
➥.qMaxValueEntropy(                                     ❸
    model, candidate_x                                   ❸
)                                                        ❸

with torch.no_grad():                                    ❹
    acquisition_score = policy(xs.unsqueeze(1))          ❹

❶ 使用 Sobol 序列在 0 和 1 之间生成样本

❷ 重新缩放样本以使其位于域内

❸ 声明 MES 策略对象

❹ 计算获取分数

这里,num_candidates 是一个可调参数,它设置了在 MES 计算中您想要使用的样本数量。较大的值意味着更高保真度的近似,但这将带来更高的计算成本。

现在让我们将此代码应用于我们正在解决的优化一维 Forrester 函数的问题中,该函数在 CH06/01 - BayesOpt loop.ipynb 笔记本中实现。我们已经熟悉大部分代码,所以我们不在这里详细介绍。

图 6.13 展示了 MES 对 10 个查询的进展,其中策略在五次查询后迅速找到了 Forrester 函数的全局最优解。有趣的是,随着优化的进行,我们对于查看搜索空间中的其他区域不会导致任何实质性熵减少越来越确信,这有助于我们保持接近最优位置。

图 6.13 MES 策略的进展。策略在五次查询后迅速找到全局最优解。

我们已经看到信息理论为我们提供了一个基于数学的、优雅的决策框架,围绕着尽可能多地了解感兴趣的数量。这归结为减少模型我们关心的数量的预期后验熵。

在贝叶斯优化中,我们看到直接将这个过程进行转换在模型化目标函数的最优值的位置方面存在计算挑战,这是我们的主要搜索目标。相反,我们将重点转移到目标函数本身的最优值,使计算更易处理。幸运的是,所有这些数学内容都被 BoTorch 很好地抽象出来,留下了一个方便的、模块化的接口,我们可以将其插入任何优化问题中。

这也是关于 BayesOpt 策略的书的第二部分的结束。最后三章涵盖了一些最常用的启发式方法来进行 BayesOpt 中的决策以及相应的策略,从寻求改进到借用多臂老丨虎丨机方法,以及在本章中使用信息理论。

本书的剩余部分将我们的讨论提升到一个新的水平,介绍了与我们迄今所见不同的特殊优化设置,在这些设置中,我们在优化的每一步都会顺序观察一个单独的数据点。这些章节表明了我们学到的方法可以被转化为现实世界中的实际设置,以加速优化。

6.3 练习

本章有两个练习:

  1. 第一个练习涵盖了一种二分搜索的变体,在这种搜索中可以考虑先前的信息来做决策。

  2. 第二个练习讲解了在先前章节中出现的超参数调整问题中实施 MES 的过程。

6.3.1 练习 1:将先验知识纳入熵搜索

我们在第 6.1.3 节中看到,通过在数组中目标位置上放置均匀先验分布,最优的信息理论搜索决策是将数组切分一半。如果均匀分布不能忠实地表示你的先验信念,你想使用不同的分布会发生什么?这个练习在 CH06/02 - Exercise 1.ipynb 笔记本中实现,向我们展示了这个例子以及如何推导出结果的最优决策。解决这个练习应该会帮助我们进一步欣赏熵搜索作为一种在不确定性下的通用决策过程的优雅和灵活性。

想象一下以下情景:你在一个电话制造公司的质量控制部门工作,你目前的项目是对公司最新产品的外壳的耐用性进行压力测试。具体来说,你的团队想要找出从一个 10 层楼的建筑的哪一层楼可以将手机扔到地上而不会摔坏。有一些规则适用:

  • 制造手机的工程师确定如果从一楼扔下手机,它不会摔坏。

  • 如果手机从某个楼层掉下来时摔坏了,那么它也会在更高的楼层掉下时摔坏。

你的任务是找出可以从中扔手机而不会摔坏的最高楼层 — 我们将这个未知楼层称为 X — 通过进行试验。也就是说,你必须从特定楼层扔真正的手机来确定 X。问题是:你应该如何选择从哪些楼层扔手机以找到 X?由于手机很昂贵,你需要尽量少进行试验,并且希望使用信息理论来辅助搜索:

  1. 假设通过考虑物理学、手机的材料和构造,工程师们对可能的楼层有一个初始猜测。

    具体来说,X 的先验分布是指数型的,即X 等于一个数字的概率与该数字的倒数成指数关系:Pr(X = n) = 1 / 2^(n),对于n = 1, 2, ..., 9;与最高(第十)层对应的概率是Pr(X = 10) = 1 / 2⁹。因此,X = 1 的概率为 50%,随着数字的增加,这个概率减半。这个概率分布在图 6.14 中可视化。

    图 6.14 X 等于 1 到 10 之间的数字的概率(即,当手机从高楼掉落时不会摔碎的最高楼层的概率)

    确认这是一个有效的概率分布,方法是证明概率之和等于一。也就是说,证明Pr(X = 1) + Pr(X = 2) + ... + Pr(X = 10) = 1。

  2. 使用第 6.1.1 节末尾给出的公式计算这个先验分布的熵。

  3. 给定在 1 到 10 之间定义的先验分布,从第二层掉落时手机摔碎的概率是多少?第五层呢?第一层呢?

  4. 假设在观察任何试验结果之后,X 的后验分布再次是指数型的,并且在最低和最高可能的楼层之间定义。

    例如,如果观察到手机从五楼掉落时不会摔碎,那么我们知道X至少为 5,X 的后验分布是这样的,Pr(X = 5) = 1 / 2,Pr(X = 6) = 1 / 4,...,Pr(X = 9) = 1 / 32,Pr(X = 10) = 1 / 32。另一方面,如果手机从五楼掉落时摔碎了,那么我们知道X至多为 4,后验分布是这样的,Pr(X = 1) = 1 / 2,Pr(X = 2) = 1 / 4,Pr(X = 3) = 1 / 8,Pr(X = 4) = 1 / 8。图 6.15 显示了这两种情况。

    图 6.15 当手机从五楼掉落时X的两种情况下的后验概率分布。每个后验分布仍然是指数型的。

    计算这个虚构的后验分布在这两种情况下的熵。

  5. 给定先验分布,在你对第五层进行一次试验之后(即,你从第五层扔手机并观察是否摔碎),计算期望后验熵。

  6. 计算其他楼层的预期后验熵。哪一层楼的熵减少最多?这仍然与二分搜索的结果相同吗?如果不是,发生了什么变化?

6.3.2 练习 2:贝叶斯优化用于超参数调整

此练习在 CH06/03 - Exercise 2.ipynb 笔记本中实现,将 BayesOpt 应用于模拟超参数调整任务中支持向量机模型的准确度表面的目标函数。 x轴表示惩罚参数C的值,而y轴表示 RBF 核参数γ的值。有关更多详细信息,请参阅第三章和第四章的练习。完成以下步骤:

  1. 在 CH05/03 - Exercise 2.ipynb 笔记本中重新创建 BayesOpt 循环,包括实现重复实验的外部循环。

  2. 运行 MES 策略。由于我们的目标函数是二维的,我们应该增加 MES 使用的 Sobol 序列的大小。例如,您可以将其设置为 2,000。观察其聚合性能。

BayesOpt 中的重复实验

参考第四章练习 2 的第 9 步,看看我们如何在 BayesOpt 中运行重复实验。

摘要

  • 信息论研究信息的表示、量化和传输。该领域的核心概念之一是熵,它量化了我们对随机变量的不确定性,根据变量的概率分布。

  • 熵搜索过程考虑通过采取行动减少量化的期望熵(因此减少不确定性)的感兴趣数量。我们可以将这个通用过程应用于许多不确定性下的决策问题。

  • 二进制搜索可能是将熵搜索应用于在排序数组中找到特定数字位置的问题的结果。

  • 维度诅咒是指与所关注对象的维度相对应的许多 ML 过程的指数成本。随着维度的增加,完成该过程所需的时间呈指数增长。

  • 在 BayesOpt 中,虽然熵搜索可以应用于寻找函数优化器位置的问题,但由于维度诅咒,它的计算成本很高。

  • 为了克服维度诅咒,我们修改了找到函数优化值的目标,将其变为一维搜索问题。由此产生的 BayesOpt 策略称为最大值熵搜索(MES)。

  • 由于由 GP 模型的全局最优的复杂行为,计算 MES 的收购分数的封闭形式是不可行的。但是,我们可以从概率分布中抽取样本来近似获得收购分数。

  • 在 BoTorch 中实现 MES 遵循与实现其他 BayesOpt 策略相同的过程。为了促进收购分数近似中的采样过程,我们在初始化策略对象时使用 Sobol 序列。

第三部分:将贝叶斯优化扩展到专门的设置

我们学习到的贝叶斯优化循环代表了广泛的优化问题。然而,现实生活场景通常不遵循这种高度理想化的模型。如果您可以同时运行多个函数评估,这在多 GPU 可用的超参数调整应用中很常见,会怎样?如果您有多个竞争的目标,您想要优化?本部分介绍了您可能在现实世界中遇到的一些最常见的优化情景,并讨论了如何将贝叶斯优化扩展到这些情景中。

为了增加吞吐量,许多设置允许实验并行运行。第七章介绍了批量贝叶斯优化框架,在这种框架中进行函数评估是批量进行的。我们学习如何扩展第二部分学到的决策策略到这个设置中,同时确保充分利用系统的并行性。

在关键安全用例中,我们不能自由地探索搜索空间,因为某些函数评估可能会产生有害效果。这促使出现了在所讨论的函数应如何行为上存在约束,并且在优化策略的设计中需要考虑这些约束的情况。第八章处理了这种情境,称为受限制的优化,并开发了必要的机制来应用贝叶斯优化。

第九章探讨了我们可以以不同成本和精度水平观察函数值的多种方式的情境;这通常被称为多信度贝叶斯优化。我们讨论了熵的自然扩展,以量化在不同保真度水平上进行评估的价值,并将该算法应用于平衡信息和成本。

已经证明,两两比较比数字评估或评分更准确地反映了一个人的偏好,因为它们更简单,对标注者的认知负荷更轻。第十章将贝叶斯优化应用于这种情境,首先使用特殊的 GP 模型,然后修改现有策略以适应这种两两比较工作流程。

多目标优化是一个常见的用例,我们旨在同时优化多个可能存在冲突的目标。我们研究了多目标优化问题,并开发了一个可以共同优化我们的多个目标的贝叶斯优化解决方案。

在这一系列特殊的优化设置中,有一个共同的主题:在考虑问题的结构时权衡探索和利用。通过看到在本部分如何将贝叶斯优化应用于各种设置中,我们不仅巩固了对这一技术的理解,而且使该技术在实际场景中更加适用。这些章节中开发的代码将帮助您立即解决您在现实生活中可能遇到的任何优化问题。

第八章:在批处理优化中最大化吞吐量

本章涵盖了

  • 批量进行函数评估

  • 扩展贝叶斯优化到批处理设置

  • 优化难以计算的获取分数

到目前为止,我们一直使用贝叶斯优化循环,该循环一次处理一个查询,并在进行下一个查询之前返回查询的函数评估结果。我们在函数评估必须按顺序进行的情况下使用该循环。但是,很多黑盒优化的实际场景都允许用户批量评估目标函数。例如,在调整 ML 模型的超参数时,如果我们可以访问多个处理单元或计算机,我们可以并行尝试不同的超参数组合,而不是逐个运行单独的组合。通过利用所有可用资源,我们可以增加进行的实验数量并最大限度地提高贝叶斯优化循环的函数评估吞吐量。

我们将同时可以进行多个查询的 BayesOpt 变体称为批处理贝叶斯优化。除了超参数调整之外,批处理 BayesOpt 的其他例子包括药物发现,科学家使用实验室中的多台机器来合成单个药物原型,以及产品推荐,推荐引擎同时向客户呈现多个产品。总的来说,在多个实验可以同时运行的黑盒优化情景下,批处理 BayesOpt 是一个非常常见的设置。

鉴于计算和物理资源通常可以并行化,批处理 BayesOpt 是贝叶斯优化在实际应用中最常见的设置之一。在本章中,我们介绍了批处理 BayesOpt,并了解到我们在前几章中学习的策略如何扩展到此设置。我们讨论了为什么将 BayesOpt 策略扩展到批处理设置不是一个简单的任务,为什么它需要仔细考虑。然后,我们学习了各种策略,以便在 Python 中使用 BoTorch 实现 BayesOpt 策略。

本章结尾时,你将理解批处理贝叶斯优化的概念,了解批处理贝叶斯优化的适用场景,以及如何在这种情况下实现贝叶斯优化策略。掌握在批处理贝叶斯优化中如何并行化贝叶斯优化,我们可以使贝叶斯优化在实际应用中更加实用和适用。

7.1 同时进行多个函数评估

在黑箱问题中同时进行多个函数评估的能力在许多实际场景中很常见,批次 BayesOpt 是 BayesOpt 的一种设置,其中考虑了函数评估的并行性。在本节中,我们将介绍批量 BayesOpt 的确切设置以及在使用 BayesOpt 策略向目标函数提出多个查询时可能面临的挑战。本节将激发将 BayesOpt 策略扩展到批量设置的各种策略。我们将在本章后面的章节中介绍这些策略,从第 7.2 节开始。

7.1.1 充分利用所有可用资源并行处理

昂贵的黑箱优化问题的一个定义性特征是进行函数评估可能成本过高。在第 1.1 节中,我们研究了在许多应用中进行函数评估的高成本;调整神经网络的超参数需要大量时间和计算资源,而创建新药物的成本近年来呈指数增长,这是两个例子。在黑箱优化中查询目标函数的成本促使我们需要使查询目标函数的过程更有效率。我们可以通过并行性来实现这一点的方式之一。

定义并行性指的是同时运行独立进程,以便完成这些进程所需的总时间缩短。

并行性的好处总结在图 7.1 中,其中三个过程(可以是要运行的程序,要完成的计算等)要么顺序运行,要么并行运行。当并行运行时,三个过程只需原本顺序运行所需总时间的三分之一。

图 7.1 并行性的好处示意图。三个通用过程可以顺序运行(左)或并行运行(右)。当并行运行时,三个过程只需原本顺序运行所需总时间的三分之一。

并行性在计算机科学中特别常见,计算机可以并行使用多个处理单元同时处理多个程序。如果这些程序彼此独立(它们不使用彼此的数据或写入相同的文件),它们可以并行运行而不会出现任何问题。

相同的理念适用于使用 BayesOpt 的优化设置。例如,一位 ML 工程师调整神经网络可以利用他们可以访问的多个 GPU 同时训练多个模型。试图发现新药物的科学家可以使用实验室中的设备同时合成多个配方。同时进行多个查询可以在相同的学习目标函数所花费的时间内获取更多信息。

什么是 GPU?

GPU,即图形处理单元,是优化执行并行矩阵乘法的硬件。因此,它们通常用于训练神经网络。

以烤一批饼干为例。如果你愿意的话,你可以一次只烤一个饼干,但这样做会浪费诸如烤箱能源和时间等资源。相反,你更有可能一次性同时烤多个饼干。

烘焙饼干的贝叶斯优化

关于烘焙的话题,事实上,有一篇关于批次贝叶斯优化的研究论文(static.googleusercontent.com/media/research.google.com/en//pubs/archive/46507.pdf)讨论了通过找到制作饼干面团时使用的最佳鸡蛋、糖和肉桂的量来优化饼干配方的问题。

在贝叶斯优化中,批次设置允许同时评估目标函数的多个输入。也就是说,我们可以一次性向评估目标的黑盒发送多个查询,x[1],x[2],...,x[k],并一次性接收到相应的目标值f(x[1]),f(x[2]),...,f(x[k])。相比之下,在经典的顺序贝叶斯优化设置中,只有在观察到f(x[1])后,我们才能继续在另一个位置x[2]处进行查询。

在每一次批次贝叶斯优化循环的迭代中,我们挑选出多个输入位置来评估目标函数,而不是像我们之前一直做的那样只评估单个位置。图 7.2 显示了这个批次贝叶斯优化循环。一次性需要多次查询的要求意味着我们需要新的贝叶斯优化策略来评分这些输入位置的有用性。我们将在下一节更多地讨论为什么我们学到的贝叶斯优化策略不能轻易扩展到批次设置中。贝叶斯优化循环的另一个组成部分,GP,保持不变,因为我们仍然需要一个产生概率预测的机器学习模型。换句话说,贝叶斯优化的决策组件需要修改以适应批次设置。

图 7.2 批次贝叶斯优化循环。与顺序贝叶斯优化相比,批次贝叶斯优化需要在步骤 2 中识别多个查询点,并在步骤 3 中同时评估这些点上的目标函数。

策略的收获分数

贝叶斯优化策略为搜索空间中的每个输入位置分配一个称为收获分数的分数,该分数量化了输入在寻找目标函数全局最优解过程中的有用程度。每个策略使用不同的启发式来计算这个分数,详情见第 4 到 6 章。

可以同时进行的查询数量——即批次的大小——取决于应用程序。例如,您可以同时烘烤多少块饼干取决于您的烤箱和烤盘的大小。您可用的计算资源(CPU 和 GPU 的数量)决定了调整模型超参数时可以并行训练多少神经网络。图 7.1 显示了同时运行三个进程的示例,因此批次大小为 3。

7.1.2 为什么我们不能在批处理设置中使用常规 BayesOpt 策略?

我们在上一节中说过,在顺序设置下学习的 BayesOpt 策略(其中对目标函数的查询是顺序地进行的,一个接一个地进行)不能重新用于批处理设置中而不经过修改。在本节中,我们将更详细地讨论为什么会出现这种情况,以及为什么我们需要专门针对批处理设置的策略。

从第 4.1 节我们知道,BayesOpt 策略会为搜索空间中的每个点分配一个分数,该分数量化了该点对我们寻找目标函数全局最优解的有用程度。然后,我们寻找给出最高分数的点,并将其选择为下一个查询的目标函数。图 7.3 显示了由预期改进(EI)策略(在第 4.3 节介绍)计算的分数,作为底部面板中的曲线,其中 1.75,如下所示的垂直标记在较低曲线上,最大化了分数,是我们的下一个查询。

图 7.3 BayesOpt 的一个例子。顶部面板显示了 GP 预测和地面真实目标函数,而底部面板显示了 EI 产生的采集分数,这在第 4.3 节中讨论过。在较低曲线上的垂直刻度为 1.75,表示下一个查询。

如果我们使用相同的 EI 分数,即图 7.3 中的较低曲线,来挑选不止一个点来查询目标函数,会发生什么?我们需要确定许多给出最高 EI 分数的点。然而,这些给出高 EI 分数的点会简单地聚集在顺序设置下选择的点周围。这是因为沿着较低曲线,如果我们从 1.75 移动一个无穷小的距离,我们仍然会得到一个高的 EI 分数。也就是说,接近给出最高采集分数的点也会给出高的采集分数。

如果我们简单地挑选出获得分数最高的点,我们的查询将聚集在搜索空间的一个区域,实质上是把所有的鸡蛋放在一个篮子里。这在图 7.4 中有所说明,那些给出最高 EI 分数的查询聚集在 1.75 附近。这种聚集效应是不可取的,因为我们在本质上浪费了宝贵的资源,评估了目标函数在基本上是一个输入位置的值。这些聚集点比分散的点不那么有用。

图 7.4 如果我们仅仅选择具有最高获得分数的点,并通过水平刻度线在下方图表上标识出来,那么在批量设置中所进行的查询会很接近,并且不如更分散排布的情况有用。

选择所有查询的点都聚集在一个位置附近会阻碍我们从批量设置中固有的并行性中受益。到目前为止,我们的讨论表明,设计一批查询并不像选择具有最高贝叶斯优化策略获得分数的顶点那样简单。在本章的剩余部分中,我们将讨论专门为批量设置设计的贝叶斯优化策略。方便的是,对我们来说,这些策略是对我们在第四章到第六章中所学习的贝叶斯优化策略的扩展,因此我们只需要学习如何将我们所学到的优化启发式扩展到批量设置中。

7.2 计算一批点的改进和上限置信度

我们将要扩展到批量设置的第一类策略是基于改进的策略,这是第四章的主题,以及在第 5.2 节讨论的 UCB 策略。这些策略使用的启发式方法可以被修改为在批量设置中工作,我们稍后会看到。

在下一节中,我们介绍这些启发式的数学修改,并讨论生成的批量策略的工作原理。之后,我们将学习如何使用 BoTorch 声明和运行这些批量策略。

7.2.1 将优化启发式扩展到批量设置

在第 7.1.2 节的讨论中,我们可以看出,选择一批点来评估目标函数并不像找到最大化顺序策略获得分数的顶点那样简单。相反,我们需要重新定义这些顺序策略的数学公式,以使它们适用于批量设置。

适用于我们所学到的三种贝叶斯优化策略(PoI,EI 和 UCB)的策略有一种策略,它们将其获取得分公式定义为各个正态分布上的平均值。也就是说,这三种策略中,每种策略分配给给定点的分数都可以写为顺序设置中数量的平均值。对于 PoI,这个数量是我们是否能观察到改进;对于 EI,这个数量是改进的大小。

图 7.5 将贝叶斯优化策略的数学表达扩展到批量设置中。在两种情况下,我们使用的是感兴趣数量的平均值。在批量设置中,我们会在对整个批次的效用进行平均之前先选取批次中点的最大值来表示整个批次的效用。

正如图 7.5 顶部所示,顺序 BayesOpt 策略使用某个数量 G(f(x)) 在我们对目标函数 f(x) 的值的信念的正态分布上的平均值来对候选查询 x 进行评分。这个数量 G 取决于 BayesOpt 策略用于平衡探索和利用的启发式方法。对于查询批次 x[1],x[2],...,x[k],我们相反地计算批次中点的数量 G 的最大值的平均值,如图 7.5 底部所示。这个平均值是在对应于目标值 f(x[1]),f(x[2]),...,f(x[k]) 的多元高斯分布上计算的。

探索与利用的平衡

所有的 BayesOpt 策略都需要解决在搜索空间中找到高性能区域(利用)和检查未探索区域(探索)之间的折衷。更深入地讨论此折衷,请参阅第 4.1.2 节。

这种使用兴趣量 G 的最大值来表示整个批次效用的策略在优化的背景下是直观的。G 的最大值越高,整个查询批次的价值就越高。有了一种方法来量化任何给定查询批次的价值,我们现在可以继续寻找最大化该数量的批次。我们使用的启发式方法类似于奥运会等体育比赛中的做法:每个国家可能在整年中训练很多运动员,但当时机成熟时,只选择最优秀的个人参加比赛。图 7.6 可视化了这个过程。

图 7.6 批量 BayesOpt 启发式选择具有最高 G 值的最佳元素来表示整个批次(底部)。这种策略类似于奥运会中的团队选拔,只选择最优秀的运动员代表一个国家。

如何利用上述三种政策实现这种策略?让我们首先讨论前两种:基于改进的策略。请记住,在第四章中,PoI 使用下一个查询将从最佳点(现任者)改进的概率作为获取分数。一个点更有可能比现任者产生更好的结果,PoI 给予该点的分数就越高。另一方面,EI 政策考虑改进的幅度,给出的获取分数较高,表明这些点很可能从现任者那里改进,而且改进幅度较大。

这两种策略的区别在图 7.7 中得到了可视化,其中不同的结果位于 x 轴上,y 轴显示了要优化的目标值。PoI 将所有在 x 轴上产生比现任者更高值的点视为平等,而 EI 则考虑每个点的改进程度。

图 7.7 PoI(左)和 EI(右)之间的区别。前者仅考虑我们是否从现有值中提高,而后者考虑了提高多少。

在批次设置中,我们可以类似地推理出在 BayesOpt 循环的当前迭代后观察到的提高。与针对一批查询中的多个点推理不同,我们可以在这些点的函数评估中单独找出最大值。也就是说,如果我们的查询批次分别为 x[1],x[2],...,x[k],我们不需要使用所有函数评估 f(x[1]),f(x[2]),...,f(x[k]) 来推理我们观察到的提高。我们只需使用最大值 max {*f*(*x*[1]), *f*(*x*[2]), ... ,*f*(*x[k]*)},因为这个最大值定义了我们观察到的提高。

按照图 7.7 中的例子,假设我们的现有值具有 20 的目标值,并考虑图 7.8 中右侧可视化的以下情景:

  • 如果我们的查询批次大小为 3,且返回的值全都低于 20(右板块中的 X[1]),那么我们将不会观察到提高。在 X[1] 中的最高函数评估是 3,意味着本批次中的函数评估都没有从现有值中提高。

  • 如果所有返回值都超过了现有值(对应于 X[2]),那么我们将会观察到一个从现有值中提高的情况。特别地,这个批次中的最大值 X[2] 等于 30,导致了一个 10 的提高。

  • 更重要的是,即使只有一些而不是所有返回的函数评估优于现有值(例如 X[3]),我们仍然会观察到一个提高。X[3] 的最大值是 22,这是从现有值 20 中的确有所提高的。

通过关注从一批次查询中返回的最大评估值,我们可以立即确定这个批次是否从现有值中提高。图 7.8 显示了 PoI 的这种基于提高的推理,其中批次 X[2] 和 X[3] 被平等地处理,因为它们(或更具体地说,它们的最大值)都导致了提高。现在,我们有了一种方法,将计算提高的概率从顺序扩展到批量设置。

图 7.8 查询(左)还是一批查询(右)是否会从现有值中提高。在右侧的批量设置中,我们仅考虑每个批次中的最大值,以确定是否存在提高。

收购分数(acquisition score) 是指给定候选查询批次的概率,即返回的函数评估中最大值是否会超过现有值。

从计算函数评估 f(x) 将超出现有值 f* 的概率,记为 Pr(f(x) > f*),在顺序设置下,我们推导出计算最大函数评估将超出现有值 f 的概率,Pr(max {f(x[1]), f(x[2]), ..., f(x[k])} > f**)。然后,我们将使用该概率 Pr(max {f(x[1]), f(x[2]), ..., f(x[k])} > f**) 作为批处理查询 x[1]、x[2]、...、x[k]* 的 PoI 采集分数。

正如本节前面提到的,这些概率 Pr(f(x) > f**) 和 Pr(max {f(x[1]), f(x[2]), ..., f(x[k])} > f**) 可以被视为高斯分布下对我们的优化进展重要性的量的平均值。具体而言,这些概率分别是二进制随机变量的平均值,指示 f(x) > f* 和 max {f(x[1]), f(x[2]), ..., f(x[k])} > f* 是否为真。该比较在图 7.9 中可视化。

图 7.9 将 POI 政策扩展到批处理设置中。在顺序情况下(上),我们考虑下一个查询相比现有值是否有改进。在批处理设置下(下),我们考虑批处理中所有点的最大值相比现有值是否有改进。

要完成具有这种 PoI 政策的批处理 BayesOpt 循环,我们需要找到批处理 x[1]、x[2]、...、x[k] 来最大化采集分数 Pr(max {f(x[1]), f(x[2]), ..., f(x[k])} > f**)。正如我们在 4.1.1 节中所学到的,我们可以使用 BoTorch 的 optim.optimize 模块中的辅助函数 optimize_acqf() 来促进批处理 x[1]、x[2]、...、x[k]* 的搜索,以优化采集分数,我们将在 7.2.2 节中看到。

我们现在进入 EI 政策,它计算从查询特定点得出的相对于现有值的改进的预期值。由于我们已经有了一种方式来推理出在观察到一批函数评估后相对于现有值的改进,因此我们可以扩展 EI。即,我们只计算从返回批处理中的最大函数评估所得到的相对于现有值的改进的预期值,即 max {f(x[1]), f(x[2]), ..., f(x[k])}。与 PoI 计算最大值是否超出现有值的概率不同,EI 则考虑最大值超出改进的程度。EI 和其批处理变体之间的差异在图 7.10 中可视化。

图 7.10 将 EI 政策扩展到批处理设置中。在顺序情况下(上),我们使用下一个查询相比现有值的平均提升量。在批处理设置下(下),我们计算批处理中所有点的最大值相比现有值的平均提升量。

为了说明这种推理,图 7.11 显示了顺序(左侧面板)和批处理设置(右侧面板)中 EI 评分不同结果的区别。右侧面板中以下内容为真:

  • 不具有任何点能从 20 的现有值改进的批次(以X[1]为例)将构成零改进。

  • 批次X[2]中的最大值为 22,所以即使这批次中的某些值低于现有值,我们也观察到了 2 的改进。

  • 尽管批次X[3]中的值都高于现有值,但我们观察到的改进完全是由最大值 30 决定的。

  • 最后,即使大多数批次X[4]低于现有值 20,X[4]的最大值为 50,使得这个批次成为一个非常好的结果。

图 7.11 查询(左)或一批查询(右)是否导致从现有值的改进。在右侧的批处理设置中,我们只考虑每个批次中的最大值,以确定是否有改进。

要继续进行批量 EI,我们计算批次内最大值比现有值高多少的期望值。这种改进的期望值或预期改进是 EI 用于评估给定批次x[1]、x[2]、...、x[k]的价值的收购分数批次。辅助函数optimize_acqf()可以再次用于找到提供最高预期改进的批次。

到目前为止的讨论帮助我们将基于改进的两个政策,PoI 和 EI,扩展到批处理设置。我们现在剩下的是 UCB 政策。幸运的是,选择从一批查询中挑选出最大值的策略也适用于 UCB。为了将与兴趣函数G有关的批次的最大值挑选出来以计算改进的相同策略应用到 UCB 上,我们需要将 UCB 收购得分重新构建为正态分布的平均值。

UCB 政策的数学细节

在 5.2.2 节中,我们讨论了 UCB 收购分数为μ + βσ。在这里,术语μσf(x)的预测均值和标准差,β是一个可调参数,用于权衡勘探和开发。我们现在需要将μ + βσ 重写为正态分布N(μ, σ²)上某个数量的平均值,以扩展 UCB 到批处理设置。虽然可以进行这种重塑,但我们不在此处讨论数学。感兴趣的读者可以参考本文附录 A(arxiv.org/pdf/1712.00424.pdf),其中详细介绍了数学细节。

将 UCB 扩展到批处理设置的其余部分遵循相同的流程:

  1. 我们取被重写的数量μ + βσ 在整个批次中的最大值的平均值,并将其用作批次 UCB 收购得分。

  2. 然后,我们使用辅助函数 optimize_acqf() 找到给出最高分数的批次。

这就是我们需要了解如何将这三种 BayesOpt 策略扩展到批量设置的全部内容。我们将在下一节中学习如何在 BoTorch 中实现这些策略。

7.2.2 实施批量改进和 UCB 策略

与我们在第 4 至 6 章中看到的情况类似,BoTorch 使得在 Python 中实现和使用 BayesOpt 策略变得简单,并且前一节讨论的三种策略(PoI、EI 和 UCB)的批量变体也不例外。虽然我们需要了解这三种策略的数学公式,但我们将看到,使用 BoTorch,我们只需在我们的 Python 程序中替换一行代码就可以运行这些策略。本节中使用的代码可以在名为 CH07/01 - Batch BayesOpt loop.ipynb 的 Jupyter 笔记本中找到。

在新的设置下,我们现在将查询目标函数的操作批量执行,您可能会认为我们需要修改实现 BayesOpt 循环的代码(同时获取多个函数评估值,将多个点附加到训练集,训练 GP 模型)。然而,由于 BoTorch 能够无缝支持批处理模式,所需的修改很小。特别是,在使用辅助函数 optimize_acqf() 来找到最大化获取分数的下一个查询时,我们只需要指定参数 q = k 为批量大小(即可以并行运行的函数评估的数量)。

图 7.12 显示了批量 BayesOpt 循环的步骤及相应的代码。与顺序设置相比,当转移到批处理设置时,我们的代码需要最少的修改。

整个批量 BayesOpt 循环总结在图 7.12 中,它与图 4.4 非常相似。对少量更改进行了注释:

  • 当使用辅助函数 optimize_acqf() 时,我们指定 q = k 为批量大小 k

  • 此辅助函数返回包含 k 个点的 next_x。变量 next_x 是一个 k-by-d PyTorch 张量,其中 d 是我们搜索空间中的维数(即数据集中的特征数)。

  • 然后,我们在 next_x 指定的位置查询目标函数,并获得包含函数评估值的 next_y。与顺序设置不同,这里的 next_y 是一个包含 k 个元素的张量,对应于 next_x 的函数评估。

注意:在图 7.12 的第 1 步中,我们仍然需要一个 GP 模型的类实现和辅助函数 fit_gp_model(),该函数对训练数据进行训练。幸运的是,在顺序设置中使用的相同代码可以在不做任何修改的情况下重用。有关此代码的完整讨论,请参阅第 4.1.1 节。

为了方便我们的代码演示,我们使用了一个二维合成目标函数来模拟超参数调整应用程序的模型准确性。该函数首次出现在第三章的练习中,并且被实现如下,我们指定函数域,即我们的搜索空间,在两个维度的每一个上都在 0 和 2 之间:

def f(x):                                                      ❶
  return (                                                     ❶
    torch.sin(5 * x[..., 0] / 2 - 2.5) * torch
    ➥.cos(2.5 - 5 * x[..., 1])                                ❶
    + (5 * x[..., 1] / 2 + 0.5) ** 2 / 10                      ❶
  ) / 5 + 0.2                                                  ❶

lb = 0                                                         ❷
ub = 2                                                         ❷
bounds = torch.tensor([[lb, lb], [ub, ub]], dtype=torch.float) ❷

❶ 函数定义。

❷ 函数域,每个维度在 0 和 2 之间。

此目标函数在图 7.13 中可视化,我们可以看到全局最优点位于空间的右上角附近,给出的准确性为 90%。

图 7.13 SVM 模型在测试数据集上的准确性,作为惩罚参数 C 和 RBF 核参数 γ 的函数。这是我们在本章中要优化的目标函数。

要设置我们的批量优化问题,我们假设我们可以同时在四个不同的进程中训练模型。换句话说,我们的批次大小是 4。此外,我们只能重新训练模型五次,因此我们批处理 BayesOpt 循环的迭代次数为 5,我们可以进行的总查询次数为 4 × 5 = 20:

num_queries = 20
batch_size = 4
num_iters = num_queries // batch_size   ❶

❶ 此变量等于 5。

现在,唯一要做的就是运行批处理 BayesOpt 策略。我们使用以下代码来完成此操作,首先在搜索空间中随机选择一个点作为训练集:

torch.manual_seed(0)
train_x = bounds[0] + (bounds[1] - bounds[0]) * torch.rand(1, 2)  ❶
train_y = f(train_x)                                              ❷

❶ 在搜索空间中随机选择一个点。

❷ 在随机选择的点处评估目标函数。

然后,我们对五个迭代中的每一个执行以下操作:

  1. 记录迄今为止见过的最佳准确性。

  2. 使用当前训练集重新训练 GP 模型。

  3. 初始化一个批处理 BayesOpt 策略。

  4. 使用辅助函数 optimize_acqf() 找到最佳的查询批次。

  5. 在由查询批次指定的位置评估目标函数。

  6. 将新的观察结果附加到训练集并重复:

incumbents = torch.zeros(num_iters)
for i in tqdm(range(num_iters)):
    incumbents[i] = train_y.max()                      ❶

    model, likelihood = fit_gp_model(train_x, train_y) ❷

    policy = ...                                       ❸

    next_x, acq_val = botorch.optim.optimize_acqf(     ❹
        policy,
        bounds=bounds,
        q=batch_size,                                  ❺
        num_restarts=40,
        raw_samples=100,
    )

    next_y = f(next_x)                                 ❻

    train_x = torch.cat([train_x, next_x])             ❼
    train_y = torch.cat([train_y, next_y])             ❼

❶ 跟踪优化进展。

❷ 对当前训练集进行 GP 训练。

❸ 初始化一个即将讨论的批处理 BayesOpt 策略。

❹ 找到下一个要查询的批次。

❺ 将参数 q 设置为批处理大小。

❻ 在所选批次上评估目标函数。

❼ 更新训练数据。

再次,此代码几乎与我们在第四章的第 4.1.1 节中使用的代码相同,该代码实现了 BayesOpt 的顺序设置。我们需要注意的是将辅助函数 optimize_acqf() 的参数 q 设置为正确的批量大小。

要运行批处理 BayesOpt 策略,我们使用 BoTorch 的类实现该策略进行初始化。对于 PoI 策略,我们使用

policy = botorch.acquisition.monte_carlo.qProbabilityOfImprovement(
    model, best_f=train_y.max()
)

同样地,对于 EI 策略,我们使用

policy = botorch.acquisition.monte_carlo.qExpectedImprovement(
    model, best_f=train_y.max()
)

注意类名前面的 q,它表示这些类实现了批处理 BayesOpt 策略。类似于顺序 PoI 和 EI 所采用的参数 best_f,这里的参数 best_f 指定了当前的现任值,我们将其设置为 train_y.max()

对于 UCB,我们使用等效的 API,其中参数 beta 设置收获分数 μ + βσ 中的权衡参数 β,其中 μ 和 σ 是给定点处预测的均值和标准差:

policy = botorch.acquisition.monte_carlo.qUpperConfidenceBound(
    model, beta=2
)

参数 BayesOpt 策略所需

我们在相应的章节 4.2.2、4.3 和 5.2.3 中了解了顺序 POI、EI 和 UCB 的实现。这些策略的每个参数在其批次对应策略中是相同的,这使得在 BoTorch 中过渡到批次设置变得简单。

由于我们现在可以运行 PoI、EI 和 UCB 的批次版本,让我们花一点时间来检查这些策略的行为。特别是,假设我们当前的 BayesOpt 进展与图 7.3 中的一维目标函数相同。该图还显示了底部面板中 EI 计算的单点收获分数。我们感兴趣的是看看 EI 对两个点的批次的收获分数是什么样的——也就是说,对给定一对查询的现任者的预期改进。

我们在图 7.14 中用热图展示这些收获分数,其中方格上每个位置的亮度表示给定一对查询的预期改进,给出最高收获分数的位置标有星号。(热图的横纵坐标显示了观察到的数据和热图轴上目标函数的当前 GP 信念。)我们观察到一些有趣的趋势:

  • 热图上有两条直线带,表示高收获分数。这些带接近数据点 x = 2,意味着任何一个接近 x = 2 的查询批次(大小为 2)都会获得高分。这是有道理的,因为在 x = 2 附近是 GP 的后验均值最大化的地方。

  • 热图的对角线很暗,意味着查询批次 x[1] 和 x[2],其中 x[1] 大致等于 x[2],很可能会产生低改进。这一观察验证了我们在第 7.1.2 节中所说的内容:选择在一起聚集的查询批次是一种不好的策略,本质上是把所有的蛋都放在一个篮子里。

  • 最后,由星号标出的两个最佳查询批次是相同的批次,因为位置相对于彼此对称。该批次包含 1.68 和 2.12,仍在 x = 2 的邻域内,GP 告诉我们目标函数在这里产生高值。此外,所选的两个查询 1.68 和 2.12 相距甚远,因此帮助我们摆脱了将查询聚集在一起的陷阱。

图 7.14 显示了一个热图,显示了一维目标函数的批处理 EI 策略的收获分数,批处理大小为 2。顶部和右侧面板显示了热图的轴上观察到的数据以及目标函数的当前 GP 信念。两个最优查询对,表示为两个星星,包含 1.68 和 2.12,它们彼此之间相对较远。

图 7.14 显示,批处理 EI 的批次版本以合理的方式评估给定的一批查询,优先考虑那些可能产生高目标值且足够分散的批次。

批处理与顺序 EI

有趣的是,批处理 EI 选择的两个点,1.68 和 2.12,与最大化顺序 EI 收获分数的点 1.75 不同。顺序设置中的最佳决策与批处理设置中的最佳决策不一定相同,这种差异展示了。

回到我们的超参数调整示例,我们准备使用这些初始化来运行批处理策略。在保存每个策略实现的运行现任值并将它们相互绘制后,我们可以生成图 7.15,该图显示了我们示例中每个策略所做的优化进展。首先我们观察到,这个进展是以四个一批进行绘制的,这是有道理的,因为我们使用的批处理大小为 4。在性能方面,我们看到 EI 和 UCB 能够在开始时比 PoI 更快地取得进展,但三者最终收敛到大致相同的准确性。

图 7.15 显示了在超参数调整示例中各种批处理 BayesOpt 策略所取得的进展。进展以四个一批进行,这是使用的批处理大小。

贝叶斯优化中的重复实验

要准确比较这些策略在超参数调整应用程序中的性能,我们需要使用随机生成的不同初始训练集重复此实验。请参阅第四章练习 2 的第 9 步,了解如何在 BayesOpt 中运行重复实验。

我们现在已经学会了如何在 BoTorch 中实现 PoI、EI 和 UCB 的批处理版本,并且已经看到从顺序到批处理设置的过渡需要对我们的代码进行最少的修改。现在让我们转向剩下的 BayesOpt 策略,TS 和 MES,它们需要不同的策略才能扩展到批处理设置。

7.3 练习 1:通过重新抽样将 TS 扩展到批处理设置

与其他贝叶斯优化策略不同,汤普森抽样(TS)由于其抽样策略的原因,可以很容易地扩展到批处理设置中。我们将在这个练习中探讨这个扩展是如何实现的。请记住,在顺序设置中,TS 会从当前高斯过程(GP)对目标函数的信念中抽取一个样本,并查询最大化该样本的数据点,正如我们在第 5.3 节中所学的那样。

在批量设置中,我们只需重复从 GP 中抽样并多次最大化样本,以组装出所需大小的一批查询。例如,如果我们的批量 BayesOpt 问题的批量大小为 3,则我们从 GP 中抽取三个样本,并且我们最终得到的查询批包含了这三个样本的极大值(每个样本一个极大值)。这一逻辑在图 7.16 中有所说明,在该图中我们不断从 GP 中抽样并将最新样本的极大点添加到运行批次中,直到批次满为止——也就是说,直到达到适当的批量大小。

图 7.16 批量 TS 实现的流程图。我们不断从 GP 中抽样并将最新样本的极大点添加到运行批次中,直到批次满为止。

每次我们从 GP 中抽样,我们都得到目标函数可能的不同实现。通过优化从 GP 中抽取的多个样本,我们有一种简单的方法来选择多个可能引导我们到达目标函数全局最优解的点。为了在超参数调整示例中实现和运行此策略,我们采取如下步骤,这些步骤在 CH07/02 - 练习 1.ipynb 笔记本中实现:

  1. 重新在 CH07/01 - 批量 BayesOpt 循环.ipynb 中重现批量 BayesOpt 循环。

  2. 按照第 5.3 节中的描述,实现带有 Sobol 抽样器的 TS:

    1. 使用 2,000 个候选点进行 Sobol 抽样。

    2. 在调用 TS 对象时,请指定样本数等于批量大小:

    ts = botorch.generation.MaxPosteriorSampling(model, replacement=False)
    next_x = ts(candidate_x, num_samples=batch_size)
    
  3. 在超参数调整目标函数上运行此 TS 策略,并观察其性能。

Sobol 序列

Sobol 抽样器生成 Sobol 序列,该序列可以比均匀抽样序列更好地覆盖空间。关于 Sobol 序列的更多讨论可以在第 5.3.2 节中找到。

7.4 使用信息论计算一批点的值

现在我们学习如何将我们工具包中的最终 BayesOpt 策略扩展到批量设置中,即最大值熵搜索(MES)。与基于改进和老丨虎丨机的策略不同,MES 在批量设置中需要更加谨慎的考虑才能有效运行。我们将在下一节中讨论 MES 的批量版本以及在将其扩展到批量设置时遇到的问题,最后讨论如何在 BoTorch 中实现该策略。

注意 MES 是第六章的主题,在该章中我们学习有关信息论的基础知识以及如何使用 BoTorch 实现 MES 策略。

7.4.1 使用循环精化找到最具信息量的一批点

在顺序设置中,MES 根据我们在查询候选点后将获得的关于目标函数最大值 f 的信息量来评估每个候选查询的分数。候选点提供的关于最大值的信息量越多,该点引导我们朝向目标函数的全局最优解 x 的可能性就越大。

我们希望在批处理设置中使用相同的策略。也就是说,我们想要计算在查询候选点批处理之后我们将获得多少关于最大目标值f的信息。一批点的信息论价值是一个明确定义的数学数量,我们可以在理论上计算和使用它作为批处理设置中的采集得分。但是,在实践中计算这个信息理论数量是非常昂贵的。

主要的瓶颈在于我们必须考虑批处理中所有可能的函数评估,以知道我们将获得有关f的信息量有多少。尽管这些函数评估遵循多元高斯分布,这提供了许多数学上的便利,但计算* f 的信息增益是高斯性无法简化的任务之一。这种计算成本意味着,尽管我们可以计算一批点的采集分数,但这种计算成本昂贵而且不易优化。也就是说,找到最大化f*信息的批次点是非常困难的。

注意 查找最大化采集得分的查询是通过 L-BFGS 完成的, L-BFGS 是一种拟牛顿优化方法,通常比梯度下降更好,在辅助函数optimize_acqf()中完成。但是,由于信息理论采集分数的批处理版本计算方式,既不是 L-BFGS 也不是梯度下降能够有效地优化该得分。

BayesOpt 中的采集得分

请记住,采集得分量化了查询或一批查询的价值,以帮助我们找到目标函数的全局最优解,因此在 BayesOpt 循环的每次迭代中,我们需要识别最大化采集得分的查询或查询批次。请查看第 4.1 节以讨论最大化采集得分。

如果我们通常用来根据信息理论找到下一个最佳查询的方法 L-BFGS,在顺序设置中仅适用于一个候选点,那么我们如何在批设置中仍然使用它?我们的策略是以循环方式,一次一个成员地使用该方法找到批次的各个成员,直到收敛。具体而言,我们执行以下操作:

  1. 我们从起始批次x[1],x[2],...,x[k]开始。这个批次可以从搜索空间随机选择。

  2. 由于 L-BFGS 无法同时运行x[1],x[2],...,x[k]的所有成员,因此我们仅在固定批处理的其他成员x[2],x[3],...,x[k]时在x[1]上运行它。 L-BFGS 确实可以单独优化x[1],因为这个任务类似于在顺序设置中最大化采集得分。

  3. 一旦 L-BFGS 返回x[1]的值,我们在固定x[1]和其他成员x[3],x[4],...,x[k]的情况下运行 L-BFGS 在x[2]上。

  4. 我们重复这些单独的例程,直到我们完成处理批处理的最后一个成员x[k],此时我们返回到x[1]并重复整个过程。

  5. 我们运行这些优化循环直到收敛,即,直到我们获得的收获分数不再增加为止。这些步骤总结在图 7.17 中。

图 7.17 循环优化流程图,用于找到最大化批处理 MES 中最大目标值信息的批处理。该过程是循环的,因为我们按顺序在循环中逐步完善批处理的每个成员,直到收敛于良好的收获分数。

定义 整个过程称为cyclic optimization,因为我们按顺序在循环中逐步完善批处理的每个成员,直到收敛于良好的收获分数。

循环优化策略使我们能够避开在多个点的批处理上运行 L-BFGS 的挑战,相反,我们只在单个点上运行 L-BFGS,对收获分数进行个别的完善。借助此优化策略,我们可以在批处理设置中实现 MES 策略。

注意 我们可以将循环优化与艺术家绘画的方式进行类比。艺术家可能会分别处理绘画的各个部分,并随着进展而切换。他们可能会先处理前景,然后暂时转向背景,然后再回到前景,每次对每个部分进行小幅改进。

7.4.2 使用 BoTorch 实现批量熵搜索

现在我们学习如何在 BoTorch 中声明批量 MES 策略,并将其连接到我们的批处理 BayesOpt 循环中。幸运的是,前一节讨论的循环优化细节被 BoTorch 抽象化了,我们可以以直观的方式初始化批量 MES。以下代码包含在 CH07/03 - Max-value Entropy Search.ipynb 笔记本中。

我们仍然使用超参数调整示例。首先,我们需要对我们的 GP 模型进行一些微小修改。具体来说,为了推理后验 GP 的熵(即“幻想”未来观察结果),我们的 GP 模型的类实现需要从botorch.models.model模块中继承FantasizeMixin类:

class GPModel(
    gpytorch.models.ExactGP,
    botorch.models.gpytorch.GPyTorchModel,
    botorch.models.model.FantasizeMixin      ❶
):
    _num_outputs = 1

    ...                                      ❷

❶ 从 FantasizeMixin 继承使我们能够更有效地推理后验 GP。

❷ 其余代码保持不变。

此类实现的其余代码保持不变。现在,在实现 BayesOpt 的for循环内部,我们以与顺序设置相同的方式声明 MES:

  1. 我们从 Sobol 序列中抽取样本,并将它们用作 MES 策略的候选集。这些样本最初在单位立方体内抽取,然后调整大小以跨越我们的搜索空间。

  2. MES 策略使用 GP 模型和先前生成的候选集初始化:

num_candidates = 2000

sobol = torch.quasirandom.SobolEngine(2, scramble=True)   ❶
candidate_x = sobol.draw(num_candidates)
candidate_x = (bounds[1] - bounds[0]) * candidate_x +
➥bounds[0]                                               ❷
policy = botorch.acquisition.max_value_entropy_search.qMaxValueEntropy(
    model, candidate_x
)

❶ 我们的搜索空间是二维的。

❷ 调整候选集的大小以跨越搜索空间

Sobol 序列

Sobol 序列首次在第 5.3.2 节中讨论了 TS 策略。 MES 策略的实现还需要 Sobol 序列,我们在第 6.2.2 节中了解到了它。

虽然批量 MES 策略的初始化与我们在顺序设置中所做的完全相同,但我们需要一个辅助函数来替代optimize_acqf(),以便进行前一节中描述的循环优化过程,以识别最大化关于f的后验信息的批次。

具体来说,我们使用辅助函数optimize_acqf_cyclic(),可以从相同的 BoTorch 模块botorch.optim中访问。在这里,我们只需将optimize_acqf()替换为optimize_acqf_cyclic();其余的参数,例如边界和批量大小,保持不变:

next_x, acq_val = botorch.optim.optimize_acqf_cyclic(
    policy,
    bounds=bounds,
    q=batch_size,
    num_restarts=40,
    raw_samples=100,
)

BoTorch 维度警告

在运行批量 MES 的代码时,您可能会遇到警告:

BotorchTensorDimensionWarning:

Non-strict enforcement of botorch tensor conventions. Ensure that target 
tensors Y has an explicit output dimension.

此警告表示,我们没有根据 BoTorch 的约定格式化包含观察值 train_y 的张量。但是,这不是一个导致代码错误的错误,因此为了能够继续使用与其他策略相同的 GP 实现,我们简单地使用warnings模块忽略此警告。

由于其算法复杂性,批量 MES 策略可能需要相当长的时间来运行。可以跳过运行优化循环的代码部分并继续进行章节。

有了这些,我们现在准备在我们的超参数调整示例中运行批量 MES。使用相同的初始训练数据,批量 MES 的进展在图 7.18 中可视化,该图显示该策略与本次运行中的其他策略相当。

图 7.18:超参数调整示例中各种批量 BayesOpt 策略的进展,包括 MES

我们现在已经学会了将 BayesOpt 策略转换为批量设置,在该设置中,可以并行进行多个查询。根据策略的不同,此转换需要考虑各种不同的级别。对于基于改进的策略和 UCB,我们使用这样一个启发式方法:表现最好的成员应该代表整个批次。在练习 1 中,我们看到 TS 可以通过简单地重复抽样过程来扩展到批量设置,以组装所需大小的批次。另一方面,MES 需要一个修改后的例程,该例程使用循环优化来搜索最大化其收购得分的批次。在下一章中,我们将学习另一种专门的 BayesOpt 设置,在该设置中,在优化目标函数时需要考虑约束。

7.5 练习 2:优化飞机设计

在这个练习中,我们在物理学中的一个模拟优化问题上运行了本章中探讨的批处理贝叶斯优化策略。这个问题是我们遇到的维度最高的问题,将为我们提供一个机会观察贝叶斯优化如何处理一个高维度的通用黑盒优化问题。更具体地说,我们将看到各种批处理贝叶斯优化策略在一个真实世界优化问题上的表现。

我们对飞机工程师常常处理的一种气动结构优化问题感兴趣。在这种优化问题中,我们有各种可调参数(每个参数都构成了我们搜索空间中的一个维度),这些参数控制着飞机的工作方式。这些参数可能是飞机的长度和宽度,翼与机身的形状和角度,或者涡轮叶片的角度和旋转速度。优化工程师的工作是调整这些参数的值,使飞机正常运行或优化某些性能指标,如速度或能源效率。

尽管工程师们可能对某些变量如何影响飞机性能有一定了解,但测试一个实验飞机设计的好方法是运行各种计算机模拟并观察飞机的模拟行为。通过这些模拟,我们根据飞机在各种性能指标上的表现来评分。有了模拟程序,我们可以将这个调整过程视为一个黑盒优化问题。也就是说,我们不知道每个可调参数如何影响模拟飞机的最终性能,但我们希望优化这些参数以获得最佳结果。

这个练习提供了一个模拟飞机设计性能基准测试过程的目标函数。代码在 CH07/04 - Exercise 2.ipynb 笔记本中提供。有多个步骤:

  1. 实现模拟性能基准测试的目标函数。这是一个四参数函数,其代码如下,用于计算以四个输入参数指定的飞机的效用的分数。由于我们将这个函数视为黑盒,我们假设我们不知道函数内部的运行方式和输出是如何产生的:

    def flight_utility(X):
      X_copy = X.detach().clone()
      X_copy[:, [2, 3]] = 1 - X_copy[:, [2, 3]]
      X_copy = X_copy * 10 - 5
    
      return -0.005 * (X_copy**4 - 16 * X_copy**2 + 5 * X_copy).sum(dim=-1) + 3
    

    四个参数是飞机的各种设置,缩放到 0 和 1 之间。也就是说,我们的搜索空间是四维单位超立方体。虽然这对我们的黑盒优化方法并不重要,但这些参数的名称如下:

    labels = [
        "scaled body length",
        "scaled wing span",
        "scaled ρ",
        "scaled ω"
    ]
    

    虽然很难可视化一个完整的四维函数,但我们可以展示这个函数在二维空间中的行为。图 7.19 展示了我们的目标函数在我们可以调整的各种参数对中的行为,显示了这些二维空间中的复杂非线性趋势。

    图 7.19 在不同的二维子空间中,虚拟飞机设计优化问题的目标函数对应于可调参数对,显示为轴标签。明亮的点表示高目标值,即我们的优化目标;黑暗的点表示低目标值。

    再次强调,我们的目标是使用 BayesOpt 找到该函数的最大值。

  2. 使用一个具有恒定均值函数和 Matérn 2.5 核函数的 GP 模型,输出尺度为一个 gpytorch.kernels.ScaleKernel 对象:

    1. 在初始化核函数时,我们需要指定参数 ard_num_dims = 4 ,以考虑到我们的目标函数是四维的。

    注意:在第 3.4.2 节以及第三章练习中,我们学习了如何使用 Matérn 核函数。

  3. 实现一个辅助函数,该函数在给定的训练数据集上训练 GP。该函数应该接收一个训练集,在使用梯度下降最小化负对数似然的同时训练 GP,并返回该 GP 及其似然函数。有关如何实现这个辅助函数的刷新,请参见第 4.1.1 节。

  4. 定义我们优化问题的设置:

    1. 搜索空间是四维单位超立方体,因此我们应该有一个名为 bounds 的变量,其中存储以下张量:

      tensor([[0., 0., 0., 0.],
              [1., 1., 1., 1.]])
      

      我们将这些边界传递给我们在本练习后面运行的 BayesOpt 策略。

    2. 在每次运行中,BayesOpt 策略可以在总共 100 次查询目标函数(即 100 次函数评估)中进行,每次批量为 5 次。我们还对每个策略重复实验五次。

  5. 在刚刚实现的目标函数上运行本章学习到的每个批次的 BayesOpt 策略:

    1. 每个实验应该以一个随机选择的函数评估作为训练集开始。

    2. 记录在搜索中找到的最佳值。

    3. 使用一个 5,000 点的 Sobol 序列进行 TS 和 MES。

    4. 在高维问题中运行 MES 计算代价很高。缓解这一负担的常见策略是限制循环优化的次数。例如,要在五个周期后终止 MES 采集分数的优化,我们可以将 cyclic_options={"maxiter": 5} 传递给辅助函数 optimize_acqf_cyclic() 。在实验中运行这个更轻量化的 MES 版本。

  6. 绘制我们运行过的 BayesOpt 策略的优化进程,并观察它们的性能。每个策略应该有一条曲线,显示作为查询次数的函数的平均最佳观测点及其标准误差。有关如何进行这种可视化的更多详细信息,请参见第四章练习 2 的最后一步。

总结

  • 在现实世界中,许多黑盒优化设置允许多个实验(函数评估)同时并行进行。通过利用这种并行性,我们可以在 BayesOpt 中进行更多的实验,并可能获得更好的性能。

  • 在批处理 BayesOpt 设置的每次迭代中,会选择一批查询,并在这些查询上评估目标函数。这种设置要求所使用的 BayesOpt 策略能够根据查询在帮助我们定位全局最优解方面的效用来评分一批查询。

  • 将 BayesOpt 策略扩展到批处理设置并不像在顺序设置中选择得分最高的顶部数据点那样简单。这样做会导致所选查询之间的距离非常接近,从而违背了并行性的目的。

  • 三种 BayesOpt 策略——PoI、EI 和 UCB——可以使用相同的策略扩展到批处理设置。该策略使用批次查询中的最大值来量化整个批次的价值。从数学上讲,使用最大值来代表整个批次的策略需要将收益分数重写为某种感兴趣数量的平均值。

  • 由于其随机性质,TS 策略可以很容易地扩展到批处理设置中。批处理 TS 不是仅从 GP 中抽样并仅最大化一次样本,而是重复这个抽样和最大化过程,直到达到目标批次大小。

  • 计算多个点的信息论价值在计算上是具有挑战性的。这一困难阻碍了助手函数optimize_acqf()所使用的算法 L-BFGS 在批处理设置中与 MES 策略一起找到最大化给定策略的收益分数的点或批次的使用。

  • 为了避免使用 L-BFGS 与批处理 MES 的计算挑战,我们使用循环优化。这种策略涉及以循环方式优化我们当前查询批次中的各个成员,直到收益分数收敛。在 BoTorch 中,可以使用助手函数optimize_acqf_cyclic()来使用循环优化。

  • 为了最大化我们的优化吞吐量,在使用助手函数optimize_acqf()optimize_acqf_cyclic()搜索最大化给定策略的收益分数的批次时,设置正确的批次大小非常重要。我们通过将参数q设置为所需的批次大小来做到这一点。

  • 大多数 BayesOpt 策略的 BoTorch 实现都遵循与顺序设置中的实现相同的接口。这种一致性使程序员可以在不需要显着修改其代码的情况下转换到批处理设置。

第九章:满足额外约束的满意条件优化

本章包括

  • 带约束的黑盒优化问题

  • 在 BayesOpt 中考虑约束时做出决策

  • 实施考虑约束的 BayesOpt 策略

在前几章中,我们解决了黑盒优化问题,其中我们仅旨在最大化客观函数,没有其他考虑因素。这被称为无约束优化问题,因为我们可以自由地探索搜索空间以寻找客观函数的全局最优解。然而,许多现实情况并不遵循这种无约束的制定,客观函数的全局最优解可能存在成本,使实践中无法实现这种最优解。

例如,当调整神经网络的架构时,您可能会发现增加网络层数通常会产生更高的准确性,并且拥有数百万和数十亿层的网络将表现最佳。然而,除非我们有昂贵的、强大的计算资源,否则运行这样的大型神经网络是不切实际的。也就是说,在这种超参数调整任务中,运行大型神经网络会有成本,这在实践中可能对应于客观函数的全局最优解。因此,在调整此神经网络时,我们需要考虑这种计算成本,并且只寻找实际可实现的架构。

我们需要考虑额外约束的另一个黑盒优化问题是科学发现,例如在化学和材料科学中。例如,科学家的目标是设计出优化所需特性的化学品和材料,比如对抗疾病有效的药物,抵御压力的玻璃,或者易于操作的可塑金属。不幸的是,对抗疾病最有效的药物可能会有许多副作用,使其使用起来有危险,或者最具韧性的玻璃可能在大规模生产上成本过高。

这些都是受限优化问题的示例,我们需要在满足其他约束的同时优化客观函数。仅寻求优化客观函数可能会导致我们找到的解决方案违反重要约束,使我们找到的解决方案在实践中无用。相反,我们需要识别搜索空间中的其他区域,这些区域既能产生高客观值,又能满足这些重要约束。

在本章中,我们了解约束优化问题,并看到在某些情况下,额外的约束可能会完全改变优化问题的解。考虑到这些约束的需要引发了 BayesOpt 中的约束感知优化策略。我们介绍了一种考虑约束的预期改进(EI)策略的变体,并学习了如何在 BoTorch 中实现它。到本章结束时,您将了解约束优化问题,学习如何使用 BayesOpt 解决它,并看到我们使用的约束感知策略要比不考虑约束的策略表现得更好。本章所学将帮助我们在现实生活中解决更多实际的 BayesOpt 问题,并因此做出更有效的决策。

8.1 在约束优化问题中考虑约束

正如介绍中提到的,现实世界中存在许多约束优化问题:制造具有高效性和最小副作用的药物,寻找最大化理想特性并且廉价生产的材料,或者在保持计算成本低的同时进行超参数调整。

注意 我们关注不等式约束,其中我们要求结果y在预定的数值范围ayb内。

我们首先在接下来的一节中更仔细地看看约束优化问题,并了解为什么它在数学上与我们在前几章中看到的无约束问题不同。然后,我们重新定义了迄今为止一直使用的 BayesOpt 框架以考虑额外的约束条件。

8.1.1 约束条件可能改变优化问题的解

约束如何使黑盒函数的优化变得复杂?在许多情况下,搜索空间内部给出高目标值的区域可能会违反随优化问题而来的约束条件。

注意 在优化问题中,我们的目标是找到给出高目标值的区域,因为我们想要最大化目标函数的值。

如果高目标值区域违反给定的约束条件,我们需要排除这些违反约束的区域,并仅在满足约束的其他区域内进行搜索。

违反预定义约束条件的数据点在约束优化问题中被称为不可行点,因为将该点作为优化问题的解是不可行的。另一方面,满足约束条件的数据点被称为可行点。我们的目标是找到最大化目标函数值的可行点。

约束条件可能会影响无约束优化问题的最优解质量,或者完全改变最优解。考虑图 8.1 中的示例,我们的目标函数(实线)是我们在前几章中使用过的福雷斯特函数。除了这个目标函数之外,我们还有一个成本函数,如虚线所示。假设在这个约束优化问题中,约束是成本需要最大为零——也就是说,成本 c ≤ 0. 这个约束意味着只有图 8.1 右侧面板中阴影区域内的可行点可以作为优化结果使用。

图 8.1 一维约束优化问题的示例。实线是我们希望最大化的目标函数,虚线是约束优化问题的成本函数。只有产生负成本的阴影区域(右侧面板)是可行的。在这里,非正成本的约束导致最高目标值从超过 8 减少到 4 左右。

注意:我们在 BayesOpt 的 2.4.1 节中首次使用了福雷斯特函数作为示例目标函数。

因此,包含 x > 4 的区域,其中包含目标值的真实全局最优解(在右侧面板中用钻石标记)被切断。也就是说,产生目标值超过 8 的全局最优解是不可行的,而约束最优解(用星号标记)只能达到大约 4 的目标值。这种“截断”情况的一个例子是当有效药物有太严重的副作用时,药品公司决定使用同一化学成分的效果较差的变体来使产品安全。

另一个具有相同目标函数但成本函数略有不同的示例显示在图 8.2 中,这个额外的成本约束改变了我们优化问题的最优解。在没有约束的情况下,目标函数的全局最优解位于 x = 4.6. 然而,这个点是一个不可行的点,会产生正成本,因此违反了我们的约束。约束问题的最优解在 x = 1.6. 这种现象可能会发生,例如当某种高效药物的整个家族对患者有危险而不能生产时,因此我们需要寻找与危险药物化学成分不同的其他解决方案。

图 8.2 一维约束优化问题的示例。在这里,由于非正成本约束排除了 x > 3 的区域,最优解变为不同的局部最优解。

总的来说,不等式约束可能对优化问题施加复杂的要求,并改变其最优解。也就是说,约束可能排除函数的全局最优解作为不可行点——这在现实世界中是常见的情况:

  • 成本过高的神经网络倾向于实现良好的预测性能,但在实践中无法实现。

  • 最有效的药物通常太过激进和危险,无法生产。

  • 最好的材料价格太高,无法使用。

我们需要修改我们的优化策略来考虑约束并找到最佳可行解,而不是使用违反我们约束的无约束最优点。也就是说,我们需要追求两个目标:优化目标函数并满足给定的约束。单纯优化目标函数而不考虑约束会导致无法使用的不可行解。相反,我们需要找到既产生高目标值又满足约束的点。

8.1.2 约束感知的贝叶斯优化框架

我们应该如何从贝叶斯优化的角度解决这个受约束的优化问题?在本节中,我们学习如何修改我们的贝叶斯优化框架以考虑在受约束优化问题中给定的约束。

在贝叶斯优化中,我们使用高斯过程(GP)来训练我们从目标函数观察到的数据点,并对未见数据进行预测。在受约束优化中,除了我们需要满足的一个或多个定义约束的函数之外,我们还有一个或多个定义约束的函数。例如,在第 8.1.1 节中,成本函数如图 8.1 和 8.2 中的虚线所示,定义了解决方案需要具有非正成本的约束。

注意你可以参考第 1.2.3 节和第 4.1.1 节来重新了解贝叶斯优化框架。

我们假设,就像目标函数一样,我们不知道真实的成本函数是什么样的。换句话说,成本函数是一个黑盒。我们只能观察到我们查询目标函数的数据点处的成本值,从那里,我们确定这些数据点是否满足约束。

注意如果我们知道定义约束的函数的样子,我们可以简单地确定可行区域,并将我们的搜索空间限制在这些可行区域内。在我们的受约束优化问题中,我们假设我们的约束也是黑盒。

由于我们只能黑盒访问定义约束的函数,我们还可以使用 GP 来模拟每个这些函数。也就是说,除了模拟我们目标函数的 GP 外,我们使用更多的 GP,每个函数定义一个约束,以指导我们下一步在哪里查询目标函数。我们遵循相同的程序来训练每个 GP——只是使用每个 GP 的适当训练集:

  • 模拟目标函数的 GP 是在观察到的目标数值上进行训练。

  • 模拟定义约束函数的 GP 是在观察到的成本数值上进行训练。

我们的受限贝叶斯优化框架,是图 1.6 的修改版本,在图 8.3 中进行了可视化:

  • 在步骤 1 中,我们在来自目标函数的数据和定义约束函数的每个函数的数据上训练一个 GP。

  • 在步骤 3 中,我们使用贝叶斯优化策略确定的点查询目标函数和定义约束函数。

图 8.3 的步骤 1 和 3 很容易实现:我们只需要同时维护多个 GP,跟踪相应的数据集,并保持这些数据集的更新。更有趣的问题出现在步骤 2 中:决策制定。也就是说,我们应该如何设计一个贝叶斯优化策略,以指导我们朝向产生高客观价值的可行区域?这是我们在下一节讨论的主题。

8.2 贝叶斯优化中的约束感知决策

一个有效的受限贝叶斯优化策略需要同时追求优化和满足约束条件。设计这样一个策略的一种简单方法是将约束条件纳入非受限贝叶斯优化策略做出决策的方式中。也就是说,我们希望修改一个我们已经知道的策略,以考虑受限制条件的约束优化问题,并得出一种约束感知的决策程序。

我们选择用于这种修改的策略是 EI,我们在第 4.3 节学到了它。(我们稍后在本节中讨论其他贝叶斯优化策略。)请记住,EI 策略将每个未见过的数据点的预期值得到评分,以表明如果我们在这个未见点查询目标函数,我们会观察到多少改进。

图 8.3 受限贝叶斯优化循环。一个单独的 GP 模型对目标函数或定义约束的函数进行建模。一个贝叶斯优化策略推荐下一个点,我们可以查询目标函数和定义约束的函数。

定义 在任职者 这个术语指的是我们训练集中具有最高客观价值的点,这是我们需要“超越”的点,以便在优化过程中取得进展。

EI 使用的收获得分,再次,计算每个潜在查询的改进的平均值,忽略约束优化问题中的不等式约束,因此在优化受约束的目标函数时我们不能直接使用 EI。幸运的是,有一种简单的方法来考虑这些约束:我们可以通过未见点满足约束的概率来缩放每个未见点的 EI 收购得分,即数据点是可行点的概率:

  • 如果数据点可能满足约束条件,则其 EI 分数将乘以一个大数(可行性的高概率),从而保持 EI 分数较高。

  • 如果数据点不太可能满足约束条件,那么它的 EI 得分将乘以一个较小的值(较低的可行性概率),从而降低该数据点的优先级。

提示 约束变种的 EI 获取得分是正常的 EI 得分和数据点满足约束条件的概率的乘积。

约束感知的 EI 获取得分的公式如图 8.4 所示。这个获取得分是两个术语的乘积:EI 得分鼓励优化目标函数,而可行性概率鼓励停留在可行区域内。正是这种在优化目标函数和满足约束条件之间平衡的方式,正如 8.1.1 节所述,我们希望实现的。

图 8.4 所示的公式是约束 EI 获取得分的公式,它是正常的 EI 得分和可行性概率的乘积。这个策略旨在同时优化目标值并满足约束条件。

我们已经知道如何计算 EI 得分,但是如何计算第二个术语-给定数据点是可行点的概率?正如图 8.4 所述,我们使用对约束进行建模的高斯过程完成此操作。具体而言,每个高斯过程提供有关定义约束函数形状的概率信念。从这种概率信念中,我们可以计算未见过数据点满足相应不等式约束的概率。

例如,当解决图 8.2 中定义的约束优化问题时,假设我们已经在x=0,x=3 和x=4 处观察到了目标函数和成本函数。从此培训集中,我们训练了一个用于目标函数和另一个用于成本函数的高斯过程,并获得了图 8.5 中可视化的预测结果。

图 8.5 所示的是相应高斯过程的目标函数和成本函数的预测。每个高斯过程都可以让我们以概率方式推理相应函数的形状。

现在,假设我们想计算x=1 的约束 EI 得分。我们已经找到了计算任何数据点的正常 EI 得分的方法,现在我们需要做的就是计算x=1 是可行数据点的概率。为了做到这一点,我们查看表示我们对x=1 成本值的预测的正态分布,如图 8.6 所示。

图 8.6 展示了x=1 是可行点的概率,用较深的颜色突出显示。左侧显示整个高斯过程,右侧则仅显示与x=1(误差条在两个面板中相同)预测相对应的正态分布。在此,可行性遵循一个被截断的正态分布。

图 8.6 的左面板包含与图 8.5 底部面板相同的 GP,它被截断在约束阈值 0,另外还显示了 x = 1 处正态分布预测的 CI。在这一点 x = 1 垂直切割 GP,我们得到图 8.6 的右面板,在这两个面板中的 CI 是相同的。换句话说,从图 8.6 的左面板到右面板,我们已经放大了垂直刻度,而不是显示成本函数,我们只保留了成本约束(虚线)和 x = 1 处的 GP 预测,它是一个正态分布。我们看到右面板中正态分布的突出部分表示 x = 1 遵守成本约束的概率,这是我们关心的内容。

如果图 8.6 让你想起了图 4.9 和 4.10,涵盖了 PoI 策略,那是因为我们在这两种情况下的思考过程是相同的:

  • 对于 PoI,我们计算给定数据点产生的目标值高于现任的概率。因此,我们使用现任值作为下界,指定我们只关心目标值高于现任的情况。

  • 通过可行性概率,我们计算给定数据点产生的成本值低于 0 的概率。我们使用 0 作为上界来指定我们只针对成本值低于阈值的情况(以遵守我们的成本约束)。

处理不同的不等式约束

在我们当前的示例中,约束要求成本低于 0。如果我们有一个要求函数值高于某个阈值的约束条件,那么可行性的概率将是给定点产生函数值高于某个阈值的概率,而图 8.6 中的阴影区域将位于截止线的右侧。

如果存在一个约束条件,要求数值在一个范围内(ayb),那么可行性的概率将是数据点给出一个在范围的下限和上限之间的值的概率。

在我们的情况下,我们想要计算 x = 1 处成本值低于 0 的概率,这是图 8.6 中右侧面板下曲线阴影区域的面积。正如我们在第 4.2.2 节的第四章中看到的,正态分布允许我们使用累积密度函数(CDF)计算曲线下面积。在图 8.6 中,x = 1 可行的概率大约为 84% —— 这是我们用于计算受约束 EI 采集分数的图 8.4 中的第二项。

此外,我们可以计算搜索空间内任何点的可行性概率。例如,图 8.7 显示了 x = –1(中心面板)和 x = 2(右侧面板)的截断正态分布。正如我们所见,给定点可行的概率取决于该点的预测正态分布:

  • x = –1 处,几乎整个预测正态分布都位于成本阈值 0 以下,因此这里的可行性概率很高,几乎为 98%。

  • x = 2 处,只有一小部分正态分布落在成本阈值以下,导致可行性概率较低,大约为 6%。

图 8.7 在 x = –1 和 x = 2 处突出显示的可行性概率,呈深色。左侧面板显示了整个 GP,中间面板显示了 x = –1 的预测,右侧面板显示了 x = 2 的预测。突出显示的部分显示了可行性概率,这取决于给定点的正态分布。

有了计算任意给定点可行性概率的能力,我们现在可以计算图 8.4 中描述的受约束 EI 采集分数。再次强调,此分数在潜在的高目标值(由常规 EI 分数量化)和满足不等式约束(由可行性概率量化)之间平衡。

图 8.8 在右下面板显示了此分数,以及常规 EI 分数和当前 GPs。我们看到受约束 EI 意识到我们需要满足的成本约束,并且对空间右侧(其中 x > 2)的区域分配了大约零分。这是因为成本 GP(右上面板)认为这是一个应该避免的不可行区域。最终,常规 EI 策略建议将具有不可行点 x = 4 作为下一个要查询的点。受约束 EI,另一方面,建议 x = –0.8,这确实满足了我们的成本约束。

图 8.8 EI 的采集分数(左下)和受约束 EI 的采集分数(右下),以及我们对目标函数(左上)和成本函数(右上)的当前信念。通过意识到成本约束,受约束 EI 可以避免不可行的区域,并建议从常规 EI 完全不同的点进行查询。

我们已经找到了从常规策略中推导出具有约束感知的 BayesOpt 策略的一个很好的启发式方法:将策略的收购分数与可行性概率相乘以考虑不等式约束。有趣的是,将可行性概率因子添加到 EI 中并不简单是一种启发式方法——图 8.4 中的公式可以从一个无启发式、更具数学严谨性的过程中得到。感兴趣的读者可以参考一篇定义了约束 EI 策略的研究论文以获取更多细节(proceedings.mlr.press/v32/gardner14.pdf)。

虽然我们可以将相同的启发式方法应用于我们已学习的其他 BayesOpt 策略,例如 UCB、TS 和 Entropy Search,但数学上严格的过程将不再适用。此外,在撰写本文时,BoTorch 仅支持受约束 EI,这也被广泛用于实践中解决受约束优化问题。因此,我们只关注受约束 EI 及其优化结果在本章的其余部分。

8.3 练习 1:手动计算受约束的 EI

我们在图 8.4 中看到,受约束 EI 策略的收购得分是 EI 得分和可行性概率的乘积。虽然 BoTorch 的ConstrainedExpectedImprovement类提供了受约束 EI 得分的实现,但实际上我们可以手动进行计算。在这个练习中,我们将探索这种手动计算,并将我们的结果与ConstrainedExpectedImprovement类的结果进行验证。此练习的解决方案在 CH08/02 - Exercise 1.ipynb 笔记本中:

  1. 重新创建在 CH08/01 - Constrained optimization.ipynb 中使用的约束 BayesOpt 问题,包括目标函数、成本函数、GP 实现以及训练 GP 的辅助函数fit_gp_model()

  2. 使用例如torch.linspace()方法创建一个在-5 到 5 之间的密集网格的 PyTorch 张量。此张量将作为我们的测试集。

  3. 通过从我们的搜索空间(在-5 和 5 之间)随机抽样 3 个数据点,创建一个玩具训练数据集,并评估这些点的目标和成本函数。

  4. 使用辅助函数fit_gp_model()在目标函数数据和成本函数数据上训练一个 GP。

  5. 使用从成本函数数据中训练的 GP 来计算测试集中每个点的可行性概率。您可以使用torch.distributions.Normal类来初始化一个正态分布对象,并在 0 上调用此对象的cdf()方法(实现为torch.zeros(1))来计算每个数据点产生低于 0 的成本的概率。

  6. 使用model参数初始化一个常规 EI 策略,其中 GP 是由数据从目标函数训练的,并且best_f参数是当前可行的候选者:

    1. 计算测试集中每个点的 EI 得分。

    2. 有关实现 EI 策略的更多详细信息,请参见 4.3 节。

  7. 初始化受约束 EI 策略,为测试集中的每个点计算受约束 EI 分数。

  8. 计算 EI 分数和可行性概率的乘积,并验证此手动计算是否导致与 BoTorch 实现相同的结果。您可以使用torch.isclose(a,b,atol = 1e-3),它在两个张量ab之间执行逐元素比较,指定atol = 1e-3以考虑数值不稳定性,以验证所有相应分数是否匹配。

  9. 在图表中绘制 EI 分数和受约束 EI 分数,并通过图 8.4 视觉验证前者始终大于或等于后者。

8.4 在 BoTorch 中实现受约束 EI

虽然我们可以手动乘以两个量,即 EI 分数和可行性概率,以生成新的收购分数,但 BoTorch 已经处理了低级别的簿记。这意味着我们可以从 BoTorch 中导入受约束 EI 策略,并像使用任何其他 BayesOpt 策略一样使用它,而没有太多开销。我们在本节中学习如何这样做,并且我们使用的代码已包含在 CH08/01-Constrained optimization.ipynb 笔记本中。

首先,我们需要实现图 8.2 中定义约束的目标函数和成本函数。在以下代码中,目标函数实现为objective(),成本函数为cost()。我们的搜索空间介于-5 和 5 之间,并且我们制作包含这些数字的变量bounds,将在以后传递给 BayesOpt 策略:

def objective(x):                                       ❶
    y = -((x + 1) ** 2) * torch.sin(2 * x + 2)
    ➥/ 5 + 1 + x / 3                                   ❶
    return y                                            ❶

def cost(x):                                            ❷
    return -(0.1 * objective(x) + objective(x - 4))
    ➥/ 3 + x / 3 - 0.5                                 ❷

lb = -5                                                 ❸
ub = 5                                                  ❸
bounds = torch.tensor([[lb], [ub]], dtype=torch.float)  ❸

❶ 要最大化的目标函数

❷ 成本函数

❸ 搜索空间的边界

我们还需要 GP 模型的类实现和一个 helper 函数fit_ gp_model(),该函数训练给定训练数据集的 GP。由于受限制的优化不需要对 GP 以及我们如何训练它进行任何更改,因此我们可以重复使用之前章节中使用的类实现和助手函数。有关此实现的更深入讨论,请参见 4.1.1 节。

为了基准测试我们使用的策略的优化性能,我们指定每个 BayesOpt 运行具有 10 个查询,并且总共有 10 个运行:

num_queries = 10
num_repeats = 10

注意 我们会多次运行每个 BayesOpt 策略,以全面了解策略的表现。有关重复实验的讨论,请参见第四章练习 2。

最后,我们需要修改我们的 BayesOpt 循环以考虑图 8.3 中可视化的更改。在图 8.3 的第 1 步(即 BayesOpt 循环中的每个步骤开始时),我们需要重新训练多个 GP:一个用于目标函数,另一个(s)用于约束条件。

由于我们已经有了训练高斯过程的辅助函数fit_gp_model(),这一步只需要将适当的数据集传递给该辅助函数即可。在我们的当前例子中,我们只有一个定义约束的成本函数,所以总共有两个可以用以下代码重新训练的高斯过程:

utility_model, utility_likelihood = fit_gp_model(   ❶
    train_x, train_utility.squeeze(-1)              ❶
)                                                   ❶

cost_model, cost_likelihood = fit_gp_model(         ❷
    train_x, train_cost.squeeze(-1)                 ❷
)                                                   ❷

❶ 在目标函数的数据上训练一个高斯过程(GP)。

❷ 在成本函数的数据上训练一个高斯过程(GP)。

在这里,变量train_x包含我们评估目标和成本函数的位置;train_utility是相应的目标值,train_cost是成本值。

图 8.3 的步骤 2 是指运行 BayesOpt 策略,我们很快就会学习到如何运行。图 8.3 的步骤 3 中,我们评估由所选 BayesOpt 策略推荐的数据点处的目标和成本函数,该数据点存储在变量next_x中。我们通过在next_x处评估目标和成本函数来完成这一点:

next_utility = objective(next_x)                           ❶
next_cost = cost(next_x)                                   ❶

train_x = torch.cat([train_x, next_x])                     ❷
train_utility = torch.cat([train_utility, next_utility])   ❷
train_cost = torch.cat([train_cost, next_cost])            ❷

❶ 在推荐点处评估目标和成本函数

❷ 更新各种数据集

我们还需要做一个额外的记账步骤,跟踪我们的优化进展。与无约束优化问题不同,在每个步骤中,我们只需记录最优解(迄今为止见到的最高目标值),而在这里,我们需要在取最大值之前过滤掉不可行的观察结果。我们首先创建一个张量,张量具有num_repeats行(每次重复运行一个)和num_queries列(每个时间步骤一个)。此张量默认只包含一个值,如果 BayesOpt 期间未找到可行点,则表示我们的效用:

检查图 8.2,我们可以看到在我们的搜索空间内(在-5 到 5 之间),我们的目标函数在任何地方都大于-2,所以我们将-2 作为默认值:

default_value = -2     ❶
feasible_incumbents = torch.ones((num_repeats, num_queries)) * default_value

❶ 检查是否找到可行点

然后,在每个 BayesOpt 循环的步骤中,我们只记录通过取筛选后观测结果的最大值找出的可行的最优解:

feasible_flag = (train_cost <= 0).any()    ❶

if feasible_flag:
    feasible_incumbents[trial, i] = train_utility[train_cost <= 0].max()

❶ 检查是否找到可行点

上述代码完成了我们的约束 BayesOpt 循环。我们只需要声明我们要用来解决约束优化问题的 BayesOpt 策略。我们使用在第 8.2 节中讨论的约束 EI 策略,使用 BoTorch 类ConstrainedExpectedImprovement。该类需要一些重要参数:

  • model - ModelListGP(utility_model, cost_model)-目的函数的 GP 模型列表(在我们的例子中是utility_model)和定义约束的函数(在我们的例子中是cost_model)。我们使用 BoTorch 的models模块中的model_list_gp_regression.ModelListGP类来创建此列表。

  • objective_index - model 列表model中模型目标函数的索引。由于utility_model是我们传递给ModelListGP的第一个 GP,所以在我们的例子中该索引为 0。

  • constraints — 将定义约束函数的每个函数的索引映射到存储约束的下限和上限的两元素列表的字典。如果一个约束没有下限或上限,我们使用 None 代替实际数值。我们的示例要求与 cost_model 对应的成本最大为 0,因此我们设置 constraints={1: [None, 0]}

  • best_f — 当前可行的最佳解决方案,如果我们找到至少一个可行点,则为 train_utility[train_cost <= 0].max(),否则为默认值 -2。

总的来说,我们如下初始化受约束 EI 策略:

policy = botorch.acquisition.analytic.ConstrainedExpectedImprovement(
    model=botorch.models.model_list_gp_regression.ModelListGP(  ❶
        utility_model, cost_model                               ❶
    ),                                                          ❶
    best_f=train_utility[train_cost <= 0].max(),                ❷
    objective_index=0,                                          ❸
    constraints={1: [None, 0]}                                   ❹
)

❶ GP 模型列表

❷ 当前可行的最佳解决方案。

❸ 目标函数在模型列表中的索引

❹ 将每个约束的索引映射到下限和上限的字典。

现在我们手头有了受约束 EI 的实现,让我们在一维受约束优化问题上运行这个策略并观察其性能。作为基线,我们还可以运行不考虑约束的常规 EI 版本。

图 8.9 显示了这两种策略找到的平均可行最优解值以及时间函数的误差条。我们看到,与常规 EI 相比,约束变体平均找到更好的可行解,并且几乎总是收敛到最佳解。图 8.9 强调了我们的受约束优化策略相对于不考虑约束的方法的好处。

图 8.9 一维约束 EI 优化问题的优化进展。与常规 EI 相比,约束变体平均找到更好的可行解。

对于不考虑优化问题的约束的 EI 策略,往往会偏离不可行最优解。检查此策略找到的最佳解值,我们注意到在许多运行中,该策略未能从其初始值中取得进展:

torch.set_printoptions(precision=1)
print(ei_incumbents)

Output:
tensor([[ 0.8,  0.8,  0.8,  0.8,  0.8,  0.8,  0.8,  0.8,  0.8,  0.8],
        [-2.0, -2.0, -2.0, -2.0, -2.0, -2.0, -2.0, -2.0, -2.0, -2.0],
        [ 2.2,  2.2,  2.7,  2.7,  2.7,  2.7,  2.7,  2.7,  2.7,  2.7],
        [ 2.5,  2.5,  2.5,  2.5,  2.5,  2.5,  2.5,  2.5,  2.5,  2.5],
        [-2.0,  0.2,  1.9,  2.3,  2.6,  2.7,  2.7,  2.7,  2.7,  2.7],
        [-2.0,  0.5,  2.1,  2.4,  2.5,  2.5,  2.5,  2.5,  2.7,  2.7],
        [-2.0,  1.5,  2.5,  2.5,  2.5,  2.5,  2.5,  2.5,  2.5,  2.5],
        [-2.0, -2.0, -2.0, -2.0, -2.0, -2.0, -2.0, -2.0, -2.0, -2.0],
        [ 1.9,  1.9,  2.5,  2.5,  2.7,  2.7,  2.7,  2.7,  2.7,  2.7],
        [ 2.7,  2.7,  2.7,  2.7,  2.7,  2.7,  2.7,  2.7,  2.7,  2.7]])

本章我们学习了黑盒约束优化问题,并且了解到它与前几章讨论的经典黑盒优化问题有何不同。我们知道一个有效的优化策略需要追求优化目标函数和满足约束条件两者兼顾。然后我们设计了这样一种策略,即将一个等于可行性概率的因子添加到获取分数中的方法的变体。这个新的获取分数会偏向于可行区域,从而更好地引导我们朝着可行最优解前进。

在下一章中,我们将讨论一个新的 BayesOpt 设置,即多信任度优化,其中查询目标函数的成本不同。这种设置要求我们平衡寻找高目标值和保留查询预算。

8.5 练习 2:飞机设计的受约束优化

在这个练习中,我们使用了第七章练习 2 中的飞机效用目标函数来解决受约束的优化问题。这个过程允许我们在一个高维问题上运行受约束的 BayesOpt,其中不明显的是可行的最优解在哪里。这个练习的解决方案包括在 CH08/03 - Exercise 2.ipynb 笔记本中。

  1. 重新创建在 CH07/04 - Exercise 2.ipynb 笔记本中使用的 BayesOpt 问题,包括名为flight_utility()的飞机效用目标函数,我们搜索空间的边界(四维单位超立方体),GP 实现以及训练一些训练数据的 GP 的辅助函数fit_gp_model()

  2. 实现以下成本函数,模拟通过四维输入指定的飞机设计的成本:

    def flight_cost(X):
      X = X * 20 - 10
    
      part1 = (X[..., 0] - 1) ** 2
      i = X.new(range(2, 5))
      part2 = torch.sum(i * (2.0 * X[..., 1:] ** 2 - X[..., :-1]) ** 2, dim=-1)
    
      return -(part1 + part2) / 100_000 + 2
    

    图 8.10 可视化了我们可以调整的各个参数对应的成本函数,显示了跨越这些二维空间的复杂非线性趋势。

    图 8.10 在各种二维子空间中模拟飞机设计优化问题的成本函数,对应于可调参数的成对显示为坐标轴标签。

  3. 我们的目标是在遵循flight_cost()计算出的成本小于或等于 0 的约束条件的情况下,最大化目标函数flight_utility()

    1. 为此,我们将每次实验中 BayesOpt 策略可以进行的查询次数设置为 50,并指定每个策略需要运行 10 次重复实验。

    2. 如果找不到可行解决方案,刻画优化进展的默认值应该设置为-2。

  4. 在此问题上运行受约束的 EI 策略以及常规 EI 策略,然后可视化并比较它们的平均进展(以及误差条)。绘图应该类似于图 8.9。

总结

  • 约束优化是一种优化问题,除了优化目标函数外,我们需要满足其他约束条件以获得实际解决方案。约束优化在材料和药物发现以及超参数调整中很常见,其中目标函数的最优解在实践中太难或太危险而无法使用。

  • 在受约束的优化问题中满足约束的数据点称为可行点,而违反约束的数据点称为不可行点。我们的目标是在可行点中找到最大化目标函数的点。

  • 约束条件可以极大地改变优化问题的解,切断或者排除具有高目标值的区域。因此,在优化目标函数时,我们需要积极考虑约束条件。

  • 在约束 BayesOpt 框架中,我们对定义约束的每个函数训练一个 GP。这些 GP 允许我们以概率方式推理出数据点是否满足约束。具体来说,由于 GP 的预测分布是正态分布,因此很容易计算给定数据点可行性的概率。

  • 我们可以通过将可行性的概率添加到 EI 策略的获取得分中来修改 EI 策略以考虑约束。约束 EI 策略可以平衡优化目标函数和满足约束条件。

  • BoTorch 提供了一个约束 EI 策略的类实现。在实现约束 EI 时,我们需要传入建模目标和约束函数的 GP,以及声明约束的下界和上界。

第十章:在多信度优化中平衡效用和成本

本章涵盖了

  • 变成本的多信度优化问题

  • 对来自多个来源的数据进行高斯过程训练

  • 实施一个考虑成本的多信度贝叶斯优化策略

考虑以下问题:

  • 你是否应该相信在线评论,说你最喜欢的电视剧的最新季度不如以前的好,你应该停止观看这部剧,还是应该花费下个周末的时间观看,以便自己找出你是否会喜欢这个新季度?

  • 在看到他们的神经网络模型经过几个时期的训练后表现不佳之后,机器学习工程师是否应该放弃,转而使用其他模型,还是应该继续训练更多时期,希望能够获得更好的性能?

  • 当物理学家想要理解一个物理现象时,他们能否使用计算机模拟来获得见解,或者真实的物理实验对于研究这一现象是必要的?

这些问题相似,因为它们要求被问及的人在两种可能的行动之间选择,这些行动可以帮助他们回答他们感兴趣的问题。一方面,这个人可以采取一个相对低成本的行动,但从这个行动中产生的答案可能会被噪声破坏,因此不一定是真实的。另一方面,这个人可以选择成本更高的行动,这将帮助他们得出更确定的结论:

  • 阅读有关你喜欢的电视剧的最新季度的在线评论只需几分钟,但评论者可能和你的口味不同,你仍然可能喜欢这部剧。确定的方法是亲自观看,但这需要巨大的时间投入。

  • 经过几个时期的训练后,神经网络的性能可能会表现出来,但不一定能反映出其真正的性能。然而,更多的训练意味着更多的时间和资源花费在一个可能没有价值的任务上,如果模型最终表现不佳的话。

  • 计算机模拟可以告诉物理学家有关现象的许多信息,但不能捕捉到现实世界中的一切,因此模拟可能无法提供正确的见解。另一方面,进行物理实验肯定会回答物理学家的问题,但会耗费大量的金钱和精力。

这些情况属于一类称为多保真度决策的问题,我们可以决定以各种粒度和成本观察某些现象。以浅层次观察现象可能廉价且易于实现,但它并不能给我们尽可能多的信息。另一方面,仔细检查现象可能需要更多的努力。这里的保真度一词指的是一个观察如何与所讨论现象的真相密切相关。廉价的、低保真度的观察是嘈杂的,因此可能导致我们得出错误的结论,而高质量(或高保真度)的观察是昂贵的,因此不能随意进行。黑盒优化有自己的多保真度变体。

多保真度优化的定义是一个优化问题,除了要最大化的真实目标函数之外,我们还可以观察到并不完全匹配但仍然提供关于目标函数的信息的近似值。这些低保真度的近似值可以以比真实目标函数更低的成本进行评估。

在多保真度优化中,我们需要同时使用这些多源数据来获取关于我们感兴趣的内容(即目标函数的最优值)的最多信息。在本章中,我们将更详细地探讨多保真度优化问题以及如何从贝叶斯优化的角度来解决它。我们将了解一种平衡对目标函数和成本学习的策略,这导致了多保真度设置下的成本感知贝叶斯优化策略。然后我们看看如何在 Python 中实现这个优化问题和成本感知策略。通过本章的学习,我们将学会如何进行多保真度贝叶斯优化,并看到我们的成本感知策略比仅使用基本真相函数的算法更有效。

9.1 使用低保真度近似值研究昂贵现象

我们首先讨论了多保真度贝叶斯优化问题的动机、设置以及问题的实际示例。这个讨论将有助于澄清我们在这种情况下寻找决策策略的目标。

在贝叶斯优化的最简单设置中,我们在每个搜索迭代中评估目标函数,每次都仔细考虑在哪里进行这个评估,以取得最大的优化进展。这种仔细的推理的需要源于进行函数评估的高成本,这在昂贵的黑盒优化问题中是典型的。这个成本可以指的是我们花费在等待一个大型神经网络完成训练时所花费的时间,同时在寻找最佳网络架构,或者在药物发现过程中,合成实验药物和进行测试其有效性所需的金钱和精力。

但是,如果有办法在不实际评估目标函数的情况下评估函数评估结果,表示为f(x)?也就是说,除了目标函数外,我们可以查询一个廉价的替代 (x)。这个替代 (x) 是目标函数的一个不精确的近似,因此评估它并不能告诉我们关于真实目标函数f(x)的一切。然而,由于 (x) 是 f(x) 的近似,对前者的了解仍然为我们提供了对后者的洞察。我们需要问自己的问题是:我们应该如何平衡使用昂贵但提供精确信息的真实目标函数 f(x) 和使用不精确但查询成本低的替代 (x)?这种平衡在图 9.1 中有所体现,其中地面真实数据源 f(x) 是高保真度,而替代 (x) 是低保真度的近似。

图 9.1 多保真度决策问题模型,在该模型中,代理需要在查询真实目标函数 f(x) 获取准确信息和查询廉价替代 (x) 之间进行平衡。

正如在引言中所指出的,在现实世界中使用低保真度近似是很常见的,如以下示例所示:

  • 只对神经网络进行少数次数的训练,以评估其在某个数据集上的性能。 例如,在 5 个时期内神经网络的性能是其在 50 个时期后可以实现的性能的低保真逼近。

  • 以计算机模拟代替真实实验来研究某些科学现象。 这种计算机模拟模仿了真实世界中发生的物理过程,并近似了物理学家想要研究的现象。然而,这种近似是低保真度的,因为计算机不能准确地模拟真实世界。

在多保真度优化问题中,我们旨在优化目标函数 f(x),我们可以选择查询高保真度 f(x) 还是低保真度 (x) 来最好地了解和优化 f(x)。当然,查询 f(x) 将提供更多关于 f(x) 本身的信息,但查询成本阻止我们多次进行这样的查询。相反,我们可以选择利用低保真度近似 (x) 尽可能多地了解我们的目标 f(x),同时最小化查询成本。

拥有多个低保真逼近

为了保持简单,我们在本章的示例中只使用一个低保真度近似 (x)。然而,在许多实际场景中,我们提供多个低保真度近似 1, 2,...,f̄[k](x) 给目标函数,每个近似都有自己的查询成本和准确性。

我们在下一节学习的贝叶斯方法不限制我们可以访问的低保真近似数量,并且我们在本章练习 2 中解决了一个具有两个低保真近似 1 和 2 的多保真优化问题。

当例如近似实验的计算机模拟具有控制近似质量的设置时,拥有多个低保真近似是适用的。如果将模拟质量设置为低,计算机程序将运行对实际世界进行粗略模拟并更快返回结果。另一方面,如果模拟质量设置为高,程序可能需要运行更长时间才能更好地近似实验。目前,我们只使用一个目标函数和一个低保真近似。

考虑图 9.2,除了作为贝叶斯优化示例目标函数的弗雷斯特函数,表示为实线外,我们还有一个对目标的低保真近似,表示为虚线。在这里,虽然低保真近似不完全匹配实际情况,但它捕捉到了后者的大致形状,因此在搜索目标最优解时可能会有所帮助。

图 9.2 弗雷斯特函数(实线)和函数的低保真近似(虚线)。尽管低保真近似不完全匹配实际情况,但前者提供了关于后者的信息,因为这两个函数大致具有相同的形状。

例如,由于低保真近似对真实目标函数具有信息性,我们可以多次查询近似以研究其在搜索空间中的行为,只有在想要“缩小”目标最优解时才查询真实情况。本章的目标是设计一个贝叶斯优化策略,以便我们导航这个搜索,并决定在哪里以及查询哪个函数以尽快和尽可能廉价地优化我们的目标函数。

多保真贝叶斯优化循环在图 9.3 中总结,与图 1.6 中传统的贝叶斯优化循环相比,有以下显著变化:

  • 在步骤 1 中,高保真或实际情况下的函数以及低保真近似的数据都用于训练高斯过程。也就是说,我们的数据分为两组:在实际情况下评估的数据点集f(x)和在近似情况下评估的点集(x)。在两个数据集上进行训练可以确保预测模型能够推理出在低保真数据但没有高保真数据的区域的目标函数情况。

  • 在第 2 步中,贝叶斯优化策略为搜索空间中的每个数据点生成一个获取分数,以量化数据点在帮助我们识别目标最优解方面的价值。然而,不仅仅对数据点进行评分,而是对数据点-保真度对进行评分;也就是说,策略量化查询给定数据点在特定函数(高保真或低保真函数)上的价值。此分数需要平衡目标的优化和查询成本。

  • 在第 3 步中,我们查询与最大化贝叶斯优化策略获取分数相对应的保真度上的数据点。然后,我们使用新观察更新我们的训练数据集,并回到第 1 步继续我们的贝叶斯优化过程。

在本章的剩余部分,我们将学习多保真度贝叶斯优化循环的组成部分以及如何在 Python 中实现它们,从训练一个包含高保真和低保真观测的数据集的 GP 开始。

9.2 使用 GP 进行多保真度建模

如图 9.3 所示,我们的 GP 模型是在包含多个保真度观测的组合数据集上进行训练的。这种组合训练使得 GP 能够对目标函数做出预测,即使在只有低保真观测的区域也是如此,这随后会通知贝叶斯优化策略做出与优化相关的决策。在接下来的一节中,我们将学习如何表示多保真度数据集并在数据集上训练 GP 的特殊变体;我们使用的代码包含在 CH09/01 - Multifidelity modeling.ipynb 中。

图 9.3 多保真度贝叶斯优化循环。GP 在高保真函数和低保真函数的数据上进行训练,并且贝叶斯优化策略决定循环的每次迭代在哪里以及查询哪个函数。

9.2.1 格式化多保真数据集

为了建立多保真度优化问题,我们使用以下代码来描述我们的一维福瑞斯特目标函数及其在图 9.2 中的低保真近似;我们的搜索空间介于 -5 和 5 之间:

def objective(x):                                               ❶
    y = -((x + 1) ** 2) * torch.sin(2 * x + 2) / 5 + 1 + x / 3  ❶
    return y                                                    ❶

def approx_objective(x):                                        ❷
    return 0.5 * objective(x) + x / 4 + 2                       ❷

lb = -5                                                         ❸
ub = 5                                                          ❸
bounds = torch.tensor([[lb], [ub]], dtype=torch.float)          ❸

❶ 真实的目标函数

❷ 目标函数的低保真近似

❸ 搜索空间的边界,供后续优化策略使用

特别重要的是一个 PyTorch 张量,它存储我们可以访问的每个保真度函数与我们试图最大化的真实目标函数之间的相关信息。我们假设我们知道这些相关性的值,并声明此张量 fidelities 如下所示:

fidelities = torch.tensor([0.5, 1.0])

此张量有两个元素,对应于我们可以访问的两个保真度:0.5,我们用它来表示福瑞斯特函数 f(x) 与其低保真近似 (x) 之间的相关性(图 9.2 中的实线和虚线),以及确切地是 1,这是福瑞斯特函数与其自身之间的相关性。

这些相关性值很重要,因为它们告诉我们 GP(高斯过程)在后续训练中应该对来自特定关联性的数据依赖多少:

  • 如果低保真度近似值与真实目标函数的相关性很高,则该近似值提供了关于目标的大量信息。一个极端的例子是目标函数本身,它提供了完美的关于我们感兴趣的内容的信息,因此具有相关性值等于 1。

  • 我们在示例中使用了一个相关性值为 0.5 的低保真度近似值,它提供了关于目标的不精确但仍然有价值的信息。

  • 在相关性值为 0 的近似值的另一端,它对目标函数不提供任何信息;一个完全水平的线就是一个例子,因为这个“近似值”在整个定义域上都是常数。

图 9.4 展示了相关性的尺度:相关性越高,低保真度近似值提供的关于真实情况的信息越多。

设置关联性变量

通常,fidelities是一个具有k个元素的张量,其中k是我们可以查询的函数数目,包括目标函数。这些元素是介于 0 和 1 之间的数字,表示函数与目标之间的相关性。对于后续的学习和决策任务来说,将真实目标与自身之间的相关性 1 放置在张量的末尾更加方便。

不幸的是,关于如何设置这些关联性值并没有明确的规定;这个决定留给 BayesOpt 工程师决定。如果在您自己的用例中不知道这些值,您可以根据图 9.4 做出粗略估计,估计您的低保真度函数位于高保真度函数(真实情况)和无信息数据源之间的位置。

图 9.4 展示了低保真度近似值与真实情况之间的 0 到 1 之间的相关性尺度。相关性越高,低保真度近似值提供的关于真实情况的信息越多。

有了函数和相关性值,现在让我们创建一个示例训练数据集。我们首先在搜索空间内随机选择 10 个位置,并将它们存储为张量train_x

n = 10                                                             ❶

torch.manual_seed(0)                                               ❷
train_x = bounds[0] + (bounds[1] - bounds[0]) * torch.rand(n, 1)   ❸

❶ 训练集的大小

❷ 为了可重复性固定随机种子

❸ 从空间中均匀随机抽取点

张量train_x有 10 行 1 列,因为我们在一维空间中有 10 个数据点。其中的每个数据点都与一个关联性相关,表明观测结果来自高保真度还是低保真度(也就是说,每个数据点是高保真度或低保真度观测)。我们通过在train_x中添加一列来表示每个数据点的关联性来将这些信息编码到我们的数据集中,如图 9.5 所示。

图 9.5 是对多保真度数据集中的特征进行格式化说明。每个数据点都与正确度相关联;这些正确度的值存储在训练集的额外列中。

注意要记住,我们的目标是对来自两个来源的数据进行高斯过程训练:地面真实信息和低保真度函数。为此,我们将为我们拥有的 10 个数据点随机分配每个数据点的正确度。

我们使用torch.randint(2)来随机选择介于 0(包含)和 2(不包含)之间的整数,有效地在 0 和 1 之间进行选择。这个数字确定每个数据点来自于哪个函数:0 表示数据点在低保真度近似(x)上进行评估;1 表示数据点在目标函数f(x)上进行评估。然后,我们提取fidelities中每个数据点对应的相关值,并将这个相关值的数组连接到我们的训练数据中:

train_f = fidelities[torch.randint(2, (n, 1))]         ❶
train_x_full = torch.cat([train_x, train_f], dim=1)    ❷

❶ 随机选择每个数据点的正确度(因此也是相关值)

❷ 将相关值添加到训练数据中

查看完整的训练数据train_x_full,我们可以看到前两个数据点是

tensor([[-0.0374,  1.0000],   ❶
        [ 2.6822,  0.5000],   ❷
        ...

❶ 第一个数据点在 f(x)上进行评估。

❷ 第二个数据点在 (x)上进行评估。

train_x_full的第一列包含数据点在-5 到 5 之间的位置,而第二列包含相关值。这个输出意味着我们的第一个训练点在-0.0374 处,并且在f(x)上进行评估。另一方面,第二个训练点在 2.6822 处,这次在(x)上进行评估。

现在,我们需要适当地生成观测值train_y,以便使用正确的函数计算观测值:train_y的第一个元素等于f(–0.0374),第二个元素等于 (2.6822),依此类推。为了做到这一点,我们编写了一个辅助函数,该函数接受完整的训练集,其中最后一列包含相关值,并调用适当的函数来生成train_y。即,如果相关值为 1,则调用objective(),即f(x),如前所定义;如果相关值为 0.5,则调用approx_objective()求解(x):

def evaluate_all_functions(x_full):
    y = []
    for x in x_full:                             ❶
        if torch.isclose(x[-1], torch.ones(1)):  ❷
            y.append(objective(x[:-1]))          ❷
        else:                                    ❸
            y.append(approx_objective(x[:-1]))   ❸

    return torch.tensor(y).unsqueeze(-1)         ❹

❶ 迭代遍历数据点

❷ 如果相关值为 1,则查询 f(x)

❸ 如果相关值为 0.5,则查询 (x)

❹ 重新调整观测张量的形状以符合正确的形状要求

train_x_full上调用evaluate_all_functions()会给我们提供通过适当的函数评估得到的观测值train_y。我们的训练集在图 9.6 中可视化,其中包含三个高保真观测结果和七个低保真度观测结果。

图 9.6 是一个从 Forrester 函数及其低保真度近似中随机抽样得到的训练数据集。此训练集包含三个高保真观测结果和七个低保真度观测结果。

这就是我们在多精度贝叶斯优化中生成和格式化训练集的方法。我们的下一个任务是以一种同时使用基本事实和低精度近似的方式在这个数据集上训练 GP。

9.2.2 训练多精度 GP

我们在本节的目标是拥有一个接收一组多精度观测并输出关于目标函数的概率预测的 GP——即要最大化的目标函数f(x)。

请记住,在第 2.2 节中,GP 是无限多个变量的 MVN 分布。GP 使用协方差函数模拟任意一对变量之间的协方差(因此也是相关性)。正是通过任意两个变量之间的这种相关性,GP 可以在观察到另一个变量的值时对一个变量进行预测。

关于变量的相关性和更新信念的提醒

假设有三个变量ABC,用三元高斯分布联合建模,其中AB之间的相关性很高,但AC以及BC之间的相关性都很低。

现在,当我们观察到A的值时,我们关于B的更新信念的不确定性(表示为B值的后验分布)显著减少。这是因为AB之间的相关性很高,因此观察到A的值给了我们关于B值的很多信息。然而,对于C来说情况并非如此,因为AC之间的相关性很低,所以对C的更新信念仍然存在相当大的不确定性。请参阅第 2.2.2 节,了解关于房价的类似且详细的讨论。

正如我们在第 2.2.2 节中学到的,只要我们有一种方法来模拟任意一对变量之间的相关性(即任意两个给定位置的函数值),我们就可以相应地更新 GP,以反映我们关于域中任何位置函数的更新信念。在多精度设置中,这仍然是正确的:只要我们有一种方法来模拟两个观察之间的相关性,即使其中一个来自高精度f(x),另一个来自低精度(x),我们也可以更新 GP 上的目标函数f(x)。

我们需要使用一个协方差函数,它可以计算两个给定观测之间的协方差,这些观测可能来自同一精度,也可能不是。幸运的是,对于我们来说,BoTorch 提供了一个修改过的 Matérn 核函数,考虑了我们训练集中每个数据点关联的精度相关值:

  • 如果数据点的相关值很高,核函数将在观察到的数据点和任何附近点之间产生高协方差,从而使我们能够通过一个信息丰富的观察来减少 GP 的不确定性。

  • 如果相关值很低,核函数将输出低协方差,后验不确定性将保持较高。

注意:我们首次在第 3.4.2 节了解到 Matérn 内核。虽然我们不会在这里详细介绍多保真度 Matérn 内核,但感兴趣的读者可以在 BoTorch 的文档中找到更多信息(mng.bz/81ZB)。

由于具有多保真度内核的 GP 被实现为特殊的 GP 类,我们可以从 BoTorch 中导入它,而不必编写自己的类实现。具体来说,这个 GP 是SingleTaskMultiFidelityGP类的一个实例,它接受一个多保真度训练集train_x_fulltrain_y。初始化还有一个data_fidelity参数,应设置为包含相关值的train_x_full中的列的索引;在我们的情况下,这是1

from botorch.models.gp_regression_fidelity
➥import SingleTaskMultiFidelityGP         ❶

model = SingleTaskMultiFidelityGP(
➥train_x_full, train_y, data_fidelity=1)  ❷

❶ 导入 GP 类实现

❷ 初始化多保真度 GP

初始化模型后,我们现在需要通过最大化观察数据的似然来训练它。(有关为什么选择最大化似然来训练 GP 的更多信息,请参见第 3.3.2 节。)由于我们拥有的 GP 是来自 BoTorch 的一个特殊类的实例,我们可以利用 BoTorch 的辅助函数fit_gpytorch_mll(),它在幕后促进了训练过程。我们需要做的就是初始化一个(对数)似然对象作为我们的训练目标,并将其传递给辅助函数:

from gpytorch.mlls.exact_marginal_log_likelihood import  ❶
➥ExactMarginalLogLikelihood                             ❶
from botorch.fit import fit_gpytorch_mll                 ❶

mll = ExactMarginalLogLikelihood(model.likelihood,
➥model)                                                 ❷
fit_gpytorch_mll(mll);                                   ❸

❶ 导入对数似然目标和用于训练的辅助函数

❷ 初始化对数似然目标

❸ 训练 GP 以最大化对数似然

这些令人惊讶的几行代码是我们需要训练一组观测的多保真度 GP 的全部内容。

BoTorch 关于数据类型和缩放的警告

当运行上述代码时,较新版本的 GPyTorch 和 BoTorch 可能会显示两个警告,第一个警告是

UserWarning: The model inputs are of type torch.float32\. It is strongly 
recommended to use double precision in BoTorch, as this improves both 
precision and stability and can help avoid numerical errors. See 
https:/ /github.com/pytorch/botorch/discussions/1444
  warnings.warn(

此警告指示我们应该为train_xtrain_y使用不同的数据类型,默认为torch.float32,以提高数值精度和稳定性。为此,我们可以在代码中添加以下内容(在脚本开头):

torch.set_default_dtype(torch.double)

第二个警告涉及将输入特征train_x缩放到单位立方体(每个特征值介于 0 和 1 之间)以及将响应值train_y标准化为零均值和单位方差:

InputDataWarning: Input data is not 
 contained to the unit cube. Please consider min-max scaling the input data.
  warnings.warn(msg, InputDataWarning)
InputDataWarning: Input data is not standardized. Please consider scaling 
the input to zero mean and unit variance.
  warnings.warn(msg, InputDataWarning)

缩放train_xtrain_y有助于我们更容易地适应 GP,并且更加数值稳定。为了保持我们的代码简单,我们不会在这里实现这样的缩放,而是使用warnings模块过滤掉这些警告。感兴趣的读者可以参考第二章的练习以获取更多细节。

现在,为了验证这个训练过的 GP 能否学习关于训练集的信息,我们使用均值和 95% CI 可视化 GP 对 f(x) 在 -5 和 5 之间的预测。

我们的测试集xs是一个密集网格(超过 200 个元素),位于 -5 和 5 之间:

xs = torch.linspace(−5, 5, 201)

与我们在之前章节中看到的情况不同,我们需要用额外的一列来增强这个测试集,表示我们想要预测的保真度。换句话说,测试集xs需要与训练集train_x_full的格式相同。由于我们对 GP 对f(x)的预测感兴趣,所以我们添加了一列额外的全为 1 的列(因为 1 是f(x)的相关值):

with torch.no_grad():                                             ❶
    pred_dist = model(torch.vstack([xs, torch.ones_like(xs)]).T)  ❷
    pred_mean = pred_dist.mean                                    ❸
    pred_lower, pred_upper = pred_dist.confidence_region()        ❹

❶ 禁用梯度跟踪

❷ 用保真度列增强测试集并将其传递给模型

❸ 计算均值预测

❹ 计算 95%置信区间

这些预测在图 9.7 中进行了可视化,该图展示了关于我们的多保真度 GP 的一些重要特征:

  1. 关于f(x)的均值预测大致经过大约–3.6,0 和 1.3 的高保真度观测点。这种插值是有意义的,因为这些数据点确实是在f(x)上评估的。

  2. 在我们只有低保真度观测但没有高保真度观测的区域(例如,在–2 和大约 2.7 附近),我们对f(x)的不确定性仍然减少了。这是因为低保真度观测提供了关于f(x)的信息,即使它们没有在f(x)上进行评估。

  3. 在这些低保真度观测中,我们发现在 4 处的数据点可能为优化策略提供有价值的信息,因为该数据点捕捉到了该区域目标函数的上升趋势。通过利用这些信息,优化策略可以在附近发现全局最优点,大约在 4.5 附近。

图 9.7 多保真度 GP 对客观函数(地面真相)的预测。均值预测适当地经过高保真度观测,但在低保真度观测周围的不确定性仍然减少。

图 9.7 显示,GP 成功地从多保真度数据集中学习。为了将我们从低保真度观测中学习和预测f(x)的能力推向极限,我们可以修改生成训练集的方式,使其只包含低保真度观测。我们通过将train_x_full中的额外列中的相关值设置为0.5来实现这一点:

train_f = torch.ones_like(train_x) * fidelities[0]     ❶
train_x_full = torch.cat([train_x, train_f], dim=1)    ❷

❶ 所有相关值均为 0.5。

❷ 将相关值添加到训练集

重新运行迄今为止的代码将生成图 9.8 的左侧面板,在那里我们看到所有数据点确实来自低保真度逼近(x)。与图 9.7 相比,我们在这里对我们的预测更不确定,这是适当的,因为仅观察到低保真度观测,GP 对客观函数f(x)的学习不如图 9.7 那样多。

图 9.8 由只基于低保真度观测训练的 GP 对客观函数(地面真相)的预测。左侧显示相关值为 0.5 时的结果;右侧显示相关值为 0.9 时的结果,表现出较少的不确定性。

为了进一步展示我们多适应性高斯过程的灵活性,我们可以玩弄存储在fidelities中的相关值(假设我们知道如何适当地设置这些相关值)。正如我们在第 9.2.1 节中所学到的,这个张量中的第一个元素表示f(x)和(x)之间的相关性,大致对应于高斯过程应该“相信”低保真度观测的程度。通过将这个第一个元素设置为 0.9(而不是我们当前的 0.5),我们可以更重视低保真度的观测。也就是说,我们告诉高斯过程从低保真度数据中学到更多,因为它提供了关于f(x)的大量信息。图 9.8 的右侧显示了产生的高斯过程,其中我们的不确定性确实比左侧面板低。

除了多适应性高斯过程模型的灵活性之外,图 9.8 还展示了在fidelities张量中具有正确相关值的重要性。比较图 9.8 中的两个面板,我们发现 0.5 是两个保真度之间的相关值的较好值,而不是 0.9:

  • 在右面板中,由于我们过度依赖和信任低保真度的观测,我们的预测在大部分空间中错过了真实目标f(x)。

  • 在左面板中,95%的置信区间适当地更宽以反映我们对f(x)的不确定性。

换句话说,我们不希望过高估计低保真度近似(x)对目标f(x)的信息量。

到目前为止,我们已经学会了如何用多适应性高斯过程建模一个函数。在本章的其余部分,我们讨论多适应性优化问题的第二部分:决策。更具体地说,我们学习如何设计一个多适应性优化策略,该策略在贝叶斯优化循环的每一步选择在哪个位置和查询哪个函数。

9.3 在多适应性优化中平衡信息和成本

为了能够在查询的信息性(低或高保真度)和运行该查询的成本之间进行权衡,我们需要一种方法来对查询成本进行建模和推理。在下一节中,我们学习如何用线性模型表示查询给定保真度的成本。使用这个成本模型,我们然后实现一个多适应性贝叶斯优化策略,平衡成本和进行优化进展。我们使用的代码存储在 CH09/02 - Multi-fidelity optimization.ipynb 笔记本中。

9.3.1 建模不同保真度查询的成本

在多保真度优化问题中,我们假设我们知道查询每个我们可以访问的函数的成本,无论是目标函数f(x)本身还是低保真度近似(x)。为了促进模块化的优化工作流程,我们需要将关于查询每个函数成本的信息表示为一个成本模型。该模型接受一个给定的数据点(其中包含一个额外的特征,包含相关值,正如我们在第 9.2.1 节中看到的那样),并返回在指定保真度上查询该数据点的已知成本。

注意,由于已知在保真度上查询的成本,因此这个成本模型中没有涉及预测。我们只需要这个模型公式来保持我们在下一节学习的优化过程。

BoTorch 提供了一个名为AffineFidelityCostModel的线性成本模型的类实现,来自于botorch.models.cost模块。这个线性成本模型假设查询成本遵循图 9.9 所示的关系,其中查询在保真度上的成本与该保真度和地面真实值f(x)之间的相关性呈线性关系。这个线性趋势的斜率是图 9.9 中的权重参数,而进行任何查询都有一个固定成本。

图 9.9 多保真度优化的线性成本模型。在保真度上查询数据点的成本与该保真度和地面真实值f(x)之间的相关性呈线性关系。

我们使用以下代码初始化这个线性成本模型,其中我们将固定成本设为 0,权重设为 1。这意味着查询低保真度数据点将花费我们确切的低保真度近似的相关值,即 0.5(成本单位)。类似地,查询高保真度数据点将花费 1(成本单位)。在这里,fidelity_weights参数接受一个字典,将train_x_full中包含相关值的列的索引映射到权重(在我们的案例中为1):

from botorch.models.cost import AffineFidelityCostModel

cost_model = AffineFidelityCostModel(
  fixed_cost=0.0,              ❶
  fidelity_weights={1: 1.0},   ❷
)

❶ 固定的查询成本

❷ 与相关值相乘的线性权重

注意,正在使用的成本单位取决于具体的应用。这个成本归结为查询目标和查询低保真度近似之间的“便利性”差异,这可以是时间(单位可以是分钟、小时或天)、金钱(以美元计算)或某种努力的度量,并应由用户设置。

线性趋势捕捉了相关值与成本之间的关系:具有高相关值的高信度函数应具有较高的查询成本,而低信度函数的查询成本应较低。两个可设置的参数——固定成本和权重——允许我们灵活地建模许多类型的查询成本。(我们将看到不同类型的查询成本如何导致下一节做出不同的决策。)有了这个成本模型,我们现在准备好学习如何在多信度优化问题中平衡成本和进展了。

建模非线性查询成本

本章中我们只使用线性成本模型。如果您的用例要求将查询成本建模为非线性趋势(例如二次或指数趋势),您可以实现自己的成本模型。

这是通过扩展我们正在使用的AffineFidelityCostModel类并重写其forward()方法来完成的。AffineFidelityCostModel类的实现在 BoTorch 的官方文档中显示(botorch.org/api/_modules/botorch/models/cost.xhtml),在那里我们看到forward()方法实现了查询成本与相关值之间的线性关系,如图 9.9 所示:

def forward(self, X: Tensor) -> Tensor:
    lin_cost = torch.einsum(                                      ❶
        "...f,f", X[..., self.fidelity_dims], self.weights.to(X)  ❶
    )                                                             ❶
    return self.fixed_cost + lin_cost.unsqueeze(-1)               ❷

❶ 将相关值与权重相乘

❷ 添加了固定成本

在自定义成本模型的新类中,您可以重写此forward()方法来实现您需要的查询成本与相关值之间的关系。即使使用自定义成本模型,我们在本章中使用的其他代码也不需要修改,这说明了 BoTorch 的模块化设计的好处。

9.3.2 优化每美元的信息量以指导优化

我们现在回到本章开头提出的问题:我们应该如何平衡通过查询函数获得的信息量和查询该函数的成本?在多信度优化中,高信度函数(真实值)为我们提供了关于要优化的目标f(x)的精确信息,但查询成本很高。另一方面,低信度近似评估成本低廉,但只能提供关于f(x)的不精确信息。多信度贝叶斯优化策略的工作是决定如何平衡这一点。

我们已经从第 9.3.1 节得到了一个模型,计算了查询任何给定数据点的成本。至于另一方面,我们需要一种方法来量化我们将从给定查询中学到的目标函数的信息量,这可以来自于目标f(x)本身,也可以来自于低信度近似(x)。

注意:关于 f(x) 或者更具体地说,关于 f(x) 最优解的信息量,正是我们在第六章学到的 Max-value Entropy Search(MES)策略用来对其查询进行排序的。MES 选择给出最多关于 f(x) 最高值的信息的查询,在单保真度设置中,我们只能查询目标。

由于这个信息增益度量是一个通用的信息论概念,因此它也可以应用于多信度设置。换句话说,我们使用 MES 作为基本策略来计算在优化过程中每个查询中关于 f(x) 最优解的信息量。现在,有了成本和信息增益这两个组成部分,我们现在需要设计一种方法来平衡这两者,从而得到一个成本感知的查询效用度量。

投资回报数量

为了量化查询的成本感知效用,我们使用经济学中的一个常见指标,称为投资回报(ROI)度量,该度量是通过将投资的利润除以投资成本来计算的。在多信度优化中使用 MES 时,利润是从查询数据点中获得的信息量,成本是查询成本。

记住,贝叶斯优化策略的收购分数是指策略为量化搜索空间内的每个数据点分配的分数,以量化该点在帮助我们优化目标函数方面的价值。在这里我们使用的 ROI 收购分数,是通过每个数据点提供关于目标最优解的信息量来评分,每个单位成本计算。这个计算在图 9.10 中可视化。

图 9.10 多信度优化的 ROI 收购分数公式。该分数量化了每个单位成本中查询提供的关于目标最优解的信息量。

我们看到,这个 ROI 分数是一个适当的度量,通过查询所获得的信息量与进行该查询的成本加权:

  • 如果我们可以潜在地进行的两个查询具有相同的成本但产生不同的信息增益,我们应该选择提供更多信息的那一个。

  • 如果两个查询提供了相同数量的关于目标最优解的信息,我们应该选择成本较低的那一个。

这种权衡允许我们从廉价的、低保真度的近似中获取关于目标函数 f(x) 的信息,如果这些近似确实是有信息的。另一方面,如果低保真度查询停止提供关于 f(x) 的信息,我们将转向高保真度数据点。基本上,我们始终选择最佳的成本感知决策,以确保“物有所值”。

要实现这种成本感知的 MES 变体,我们可以利用 BoTorch 的 qMultiFidelityMaxValueEntropy 类实现。该实现需要一些组件作为参数传入:

  • 在图 9.10 中进行 ROI 计算的成本效用对象。该对象使用 InverseCostWeightedUtility 类实现,通过其成本的倒数对查询的效用进行加权。初始化时需要我们之前创建的成本模型:

    from botorch.acquisition.cost_aware import InverseCostWeightedUtility
    
    cost_aware_utility = InverseCostWeightedUtility(cost_model=cost_model)
    
  • 用作 MES 的熵计算候选集的 Sobol 序列。我们首次了解到在第 6.2.2 节中使用 Sobol 序列与 MES,并且这里的过程与之前相同,我们从单位立方体中抽取一个 1,000 元素的 Sobol 序列(在我们的一维情况下,它只是从 0 到 1 的段),并将其缩放到我们的搜索空间。在多保真设置中,我们需要做的另一件事是用额外的列来增强此候选集,以表示我们想要在目标 f(x) 中测量熵的相关值为 1:

    torch.manual_seed(0)                                   ❶
    
    sobol = SobolEngine(1, scramble=True)                  ❷
    candidate_x = sobol.draw(1000)                         ❷
    
    candidate_x = bounds[0] + (bounds[1] - bounds[0]) *
    ➥candidate_x                                          ❸
    
    candidate_x = torch.cat([candidate_x, torch.ones_like(
    ➥candidate_x)], dim=1)                                ❹
    

    ❶ 为了可重现性而固定随机种子

    ❷ 在单位立方体内从 Sobol 序列中抽取 1,000 个点

    ❸ 将样本缩放到我们的搜索空间

    ❹ 用地面实况的指数增强样本

  • 最后,将给定数据点从任何保真度投影到地面实况的辅助函数。此投影在我们的策略用于计算采集分数的熵计算中是必要的。在这里,BoTorch 提供了该辅助函数 project_to_target_fidelity,如果我们的训练集中的最后一列包含相关值,并且地面实况的相关值为 1,那么它就不需要任何进一步的参数化,这两个条件在我们的代码中都成立。

使用上述组件,我们实现我们的成本感知、多保真 MES 策略如下所示:

from botorch.acquisition.utils import project_to_target_fidelity

policy = qMultiFidelityMaxValueEntropy(
    model,
    candidate_x,                             ❶
    num_fantasies=128,
    cost_aware_utility=cost_aware_utility,   ❷
    project=project_to_target_fidelity,      ❸
)

❶ 来自 Sobol 序列的样本

❷ 通过成本的倒数对效用进行加权的成本效用对象

❸ 投影辅助函数

到此为止,我们可以使用此策略对象根据任何保真度对每个数据点进行评分,评估其在帮助我们找到目标最优解方面的成本调整值。拼图的最后一块是我们用来优化此策略的采集分数,以找到每次搜索迭代中 ROI 分数最高的点的辅助函数。在以前的章节中,我们使用 botorch.optim.optimize 模块中的 optimize_acqf 来优化单一保真度情况下的采集分数,这仅在我们的搜索空间是连续的情况下有效。

注意:在我们当前的多保真设置中,用于查询位置的搜索空间仍然是连续的,但用于查询的函数选择是离散的。换句话说,我们的搜索空间是混合的。幸运的是,BoTorch 为混合搜索空间提供了类似的辅助函数:optimize_acqf_mixed

除了optimize_acqf通常接受的参数外,新的辅助函数optimize_acqf_mixed还有一个fixed_features_list参数,它应该是一个字典列表,每个字典将train_x_的一个离散列的索引映射到列包含的可能值。在我们的情况下,我们只有一个离散列,即包含相关值的最后一列,因此我们使用[{1: cost.item()} for cost in fidelities]作为fixed_features_list参数。此外,我们通常传递给辅助函数的bounds变量现在也需要包含相关值的边界。总的来说,我们使用以下方式优化我们的多保真 MES 获取分数:

from botorch.optim.optimize import optimize_acqf_mixed

next_x, acq_val = optimize_acqf_mixed(
    policy,
    bounds=torch.cat(                                                ❶
        [bounds, torch.tensor([0.5, 1.0]).unsqueeze(-1)], dim=1      ❶
    ),                                                               ❶
    fixed_features_list=[{1: cost.item()} for cost in fidelities],   ❷
    q=1,
    num_restarts=20,
    raw_samples=50,
)

❶ 搜索空间的边界,包括相关值的边界

❷ 相关列可能包含的离散值

这个辅助函数完成了我们需要使用多保真 MES 策略的代码。图 9.11 的底部面板可视化了通过我们在第 9.2.2 节中训练的多保真 GP 计算的获取分数。在这个底部面板中,阴影区域的边界(表示低保真查询的分数)超过了斜纹图案区域的边界(高保真查询的分数),这意味着在当前知识条件下,低保真查询比高保真查询更具成本效益。最终,我们进行的最佳查询,表示为星号,约为低保真近似(x)的 3.5。

图 9.11 对于目标函数的当前 GP 信念(顶部)和通过多保真 MES 策略计算的获取分数(底部)。在这个例子中,由于其低成本,低保真查询优于高保真查询。

在图 9.11 中,低保真查询是最佳的类型,因为相对于任何高保真查询,它提供了更多的信息。然而,情况并非总是如此。通过修改我们从第 9.2.2 节中的数据生成过程以生成所有低保真观测训练集并重新运行我们的代码,我们得到了图 9.12 的左面板,这次,最优决策是查询高保真函数f(x)。这是因为根据我们的高斯过程信念,我们已经充分从低保真数据中学到了东西,现在是时候我们检查地面真相f(x)了。

图 9.12 高保真查询优于低保真查询的情况。左侧,训练集仅包含低保真观测。右侧,低保真查询的成本几乎与高保真查询相同。

通过研究查询成本对决策的影响来最终分析我们策略的行为。为此,我们改变了查询成本,使低保真度的查询比高保真度的查询便宜得不多,具体方法是将第 9.3.1 节中描述的固定查询成本从 0 增加到 10。这种改变意味着低保真度的查询现在需要 10.5 个成本单位,而高保真度的查询现在需要 11 个。与之前的 0.5 和 1 的成本相比,10.5 和 11 更接近,使得图 9.10 中的两个保真度的分母几乎相等。这意味着低保真度的近似值(x)的查询成本几乎和目标函数f(x)本身一样高。鉴于这些查询成本,图 9.12 的右面板显示了 MES 策略如何对潜在查询打分。这一次,因为高保真度查询并不比低保真度查询昂贵得多,所以更倾向于前者,因为它们能够给我们更多关于f(x)的知识。

这些示例表明,MES 可以确定在信息和成本适当平衡的情况下的最优决策。也就是说,当低保真度查询成本低且能够提供关于目标函数的实质性信息时,策略会给低保真度查询分配更高的分数。另一方面,如果高保真度查询要么显著更具信息量,要么成本差不多,那么策略将更倾向于高保真度查询。

9.4 在多保真度优化中测量性能

我们之前的讨论表明,多保真度 MES 策略能够在选择两个保真度进行查询时做出恰当的决策。但是这个策略比只查询地面真相f(x)的常规 BayesOpt 策略好吗?如果是这样,它究竟好多少呢?在本节中,我们学习如何在多保真度设置中对 BayesOpt 策略的性能进行基准测试,这需要额外的考虑。我们显示的代码可以在 CH09/03-测量性能.ipynb 笔记本中找到。

注意在前面的章节中,为了衡量优化进展,我们记录了训练集中收集到的最高目标值(即“现有值”)。如果策略A收集的现有值超过了策略B收集的现有值,我们就说策略A在优化方面比策略B更有效。

在多保真度的设置中,记录现有值是不起作用的。首先,如果我们要在训练集中记录现有值,只有选择最大标记值的高保真度数据点才有意义。然而,这个策略忽略了低保真度查询对于学习目标f(x)的任何贡献。例如,以图 9.13 中可视化的两种可能场景为例:

  • 在左边的第一种情况中,我们进行了三次高保真度的观察,而最高观察值大约为 0.8。客观地说,在这种情况下我们没有进行优化进展;我们甚至没有探索过 x 大于 0 的区域。

  • 在右边的第二种情况中,我们只进行了低保真度的观察,因此记录高保真度现任值甚至都不适用。然而,我们看到我们非常接近找到目标函数的最优解,因为我们的查询已经发现了函数在 4.5 左右的峰值。

图 9.13 在多保真度优化中使用高保真度现任者来衡量性能是不合适的。在左边,高保真度现任者大约为 0.8,而我们还没有发现目标的最优解。在右边,即使我们接近找到目标的最优解,也没有高保真度查询来记录现任值。

换句话说,我们应该更喜欢第二种情况而不是第一种,因为第二种情况表明了接近优化成功,而第一种情况则几乎没有优化进展。然而,使用高保真度的现任者作为进展度量并不能帮助我们区分这两种情况。因此,我们需要另一种衡量优化进展的方法。

BayesOpt 社区中常见的进展度量标准是当前给出最高后验均值的位置处的目标函数值。这个度量标准对应着以下问题的答案:如果我们停止运行 BayesOpt 并推荐一个点作为优化问题的解,我们应该选择哪个点?直觉上,我们应该选择在最合理的情况下(根据我们的 GP 信念),能够给出最高值的点,也就是后验均值最大化者。

我们看到后验均值最大化者促使我们在图 9.13 中进行的比较,左边的情况下,后验均值最大化者为 0,而目标值为 0.8(在单保真度情况下,均值最大化者通常对应于现任者),而右边的情况下,均值最大化者在 4.5 左右。换句话说,后验均值最大化者度量成功地帮助我们区分了这两种情况,并显示出左边的情况不如右边的情况。

为了实现这个度量标准,我们制作了一个辅助策略,该策略使用后验均值作为其收购分数。然后,就像我们在 BayesOpt 中优化常规策略的收购分数一样,我们使用这个辅助策略来优化后验均值。这个策略需要两个组件:

  • 使用后验均值作为其收购分数的 BayesOpt 策略的类实现。这个类是 PosteriorMean,可以从 botorch.acquisition 导入。

  • 一个包装策略,仅优化高保真度指标。这个包装器是必需的,因为我们的 GP 模型是多保真度的,当将该模型传递给优化策略时,我们总是需要指定要使用哪个保真度。此包装器策略实现为来自botorch.acquisition.fixed_featureFixedFeatureAcquisitionFunction的一个实例。

总的来说,我们用以下辅助策略制定后验均值最大化器度量标准,其中包装策略接受PosteriorMean的一个实例,并且我们将其他参数指定如下:

  • 搜索空间的维度是 d = 2—我们的实际搜索空间是一维的,并且还有一个附加维度用于相关值(即查询的保真度)。

  • 要在优化期间固定的维度的索引, columns = [1] 及其固定值 values = [1]—由于我们只想找到对应于目标函数、即高保真函数的后验均值最大化器,我们指定第二列(索引1)始终应为值 1:

from botorch.acquisition.fixed_feature
➥import FixedFeatureAcquisitionFunction
from botorch.acquisition import PosteriorMean

post_mean_policy = FixedFeatureAcquisitionFunction(
    acq_function=PosteriorMean(model),   ❶
    d=2,                                 ❷
    columns=[1],                         ❸
    values=[1],                          ❹
)

❶ 优化后验均值

❷ 搜索空间的维度数

❸ 固定列的索引

❹ 固定列的数值

然后,我们使用熟悉的辅助函数optimize_acqf来找到最大化收获分数的点,即目标函数的后验均值(我们在 4.2.2 节首次了解到此辅助函数):

final_x, _ = optimize_acqf(
    post_mean_policy,       ❶
    bounds=bounds,          ❷
    q=1,                    ❷
    num_restarts=20,        ❷
    raw_samples=50,         ❷
)

❶ 优化后验均值。

❷ 其他参数与我们优化另一个策略时相同。

这个final_x变量是最大化目标函数后验均值的位置。在我们的 Jupyter 笔记本中,我们将这段代码放在一个辅助函数中,该函数返回final_x,并增加了一个相关值为 1,表示真实的目标函数:

def get_final_recommendation(model):
    post_mean_policy = FixedFeatureAcquisitionFunction(...)  ❶
    final_x, _ = optimize_acqf(...)                          ❷

    return torch.cat([final_x, torch.ones(1, 1)], dim=1)     ❸

❶ 制作包装策略

❷ 优化收获分数

❸ 使用相关值为 1 增强最终推荐

现在,在 BayesOpt 循环期间,我们不再记录 incumbent 值作为优化进度的指示,而是调用此get_final_recommendation辅助函数。此外,我们现在没有每次运行中要进行的最大查询次数,而是有一个最大预算,可以用于低保真度或高保真度查询。换句话说,我们会一直运行我们的优化算法,直到累计成本超过我们的预算限制。我们的多保真 BayesOpt 循环的框架如下:

budget_limit = 10                                  ❶

recommendations = []                               ❷
spent_budget = []                                  ❸

...                                                ❹

current_budget = 0

while current_budget < budget_limit:
    ...                                            ❺

    rec_x = get_final_recommendation(model)        ❻
    recommendations.append(evaluate_all_functions  ❻
    ➥(rec_x).item())                              ❻
    spent_budget.append(current_budget)            ❻

    ...                                            ❼

    current_budget += cost_model(next_x).item()    ❽

    ...                                            ❾

❶ 每次优化运行的最大成本

❷ 跟踪整个优化过程中最大化后验均值的推荐

❸ 跟踪每次迭代中已花费的预算

❹ 生成一个随机的起始观察

❺ 对当前数据进行 GP 训练

❻ 使用最新推荐更新记录

❼ 初始化策略并优化其收获分数

❽ 跟踪已花费的预算

❾ 更新训练数据

我们现在准备运行多保真度 MES 来优化 Forrester 目标函数。作为基准,我们还将运行单保真度 MES 策略,该策略仅查询地面实况 f(x)。我们拥有的 GP 模型是多保真度的,因此,要使用此模型运行单保真度的 BayesOpt 策略,我们需要 FixedFeatureAcquisitionFunction 类的包装策略来限制策略可以查询的保真度:

policy = FixedFeatureAcquisitionFunction(
  acq_function=qMaxValueEntropy(model, candidate_x, num_fantasies=128), ❶
  d=2,
  columns=[1],                                                          ❷
  values=[1],                                                           ❷
)

next_x, acq_val = optimize_acqf(...)                                    ❸

❶ 包装策略是单保真度 MES。

❷ 将第二列(索引 1)中的相关值固定为 1

❸ 使用辅助函数 optimize_acqf 优化收购分数

运行这两种策略会生成图 9.14 中的结果,我们观察到多保真度 MES 明显优于单保真度版本。多保真度 MES 的成本效益性说明了在信息和成本之间取得平衡的好处。然而,我们注意到这只是一次运行的结果;在练习 1 中,我们多次以不同的初始数据集运行此实验,并观察这些策略的平均性能。

图 9.14 后验均值最大化器的目标值作为两种 BayesOpt 策略花费预算的函数。在这里,多保真度 MES 明显优于单保真度版本。

在本章中,我们学习了多保真度优化问题,即在优化目标函数与获取关于目标的知识成本之间取得平衡。我们学习了如何实现一个能够从多个数据源中学习的 GP 模型。然后,这个模型允许我们根据信息理论价值来推理查询的优化值。通过将这个信息理论量与查询成本结合在一起形成的投资回报率度量,我们设计了一个成本感知的多保真度 MES 策略的变体,可以自动权衡知识和成本。

9.5 练习 1:可视化多保真度优化中的平均性能

为了比较我们的策略的性能,图 9.14 可视化了在优化过程中后验均值最大化器的目标值与花费预算的关系。然而,这只是一次优化运行的结果,我们想要展示每个策略在多次实验中的平均性能。(我们在第四章的练习 2 中首次讨论了重复实验的想法。)在这个练习中,我们多次运行优化循环,并学习如何取得平均性能以获得更全面的比较。到练习结束时,我们将看到,多保真度 MES 在信息和成本之间取得了良好的平衡,并比其单保真度的对应物更有效地优化了目标函数。解决方案存储在 CH09/04 - Exercise 1.ipynb 笔记本中。

按照以下步骤进行:

  1. 复制 CH09/03 - Measuring performance.ipynb 笔记本中的问题设置和多信度优化循环,并添加另一个变量表示我们要运行的实验次数(默认为 10)。

  2. 为了方便重复实验,在优化循环代码中添加一个外部循环。这应该是一个有 10 次迭代的for循环,每次生成一个不同的随机观察结果。(这个随机生成可以通过将 PyTorch 的随机种子设置为迭代编号来完成,这样可以确保随机数生成器在具有相同种子的不同运行中返回相同的数据。)

  3. CH09/03 - Measuring performance.ipynb 笔记本中的代码使用两个列表,recommendationsspent_budget,来跟踪优化进度。将这些变量中的每一个变量都变成一个列表的列表,其中每个内部列表都承担与 CH09/03 - Measuring performance.ipynb 笔记本中相应列表相同的目的。这些列表的列表允许我们跟踪 10 次实验的优化进度,并在后续步骤中比较不同的优化策略。

  4. 在我们的优化问题上运行多信度 MES 策略及其单信度版本。

  5. 由于查询低信度函数的成本与查询高信度函数的成本不同,spend_budget中的列表可能与彼此不完全匹配。换句话说,在图 9.14 中曲线中的点在不同运行中没有相同的x坐标。这种不匹配阻止我们在多个运行中获取存储在recommendations中的平均进度。

    为了解决这个问题,我们对每个进度曲线使用线性插值,这使我们能够在规则网格上“填充”进度值。就是在这个规则网格上,我们将对每个策略在运行中的表现进行平均。对于线性插值,使用 NumPy 中的np.interp,它以规则网格作为其第一个参数;这个网格可以是一个介于 0 和budget_limit之间的整数数组:np.arange(budget_limit)。第二个和第三个参数是组成每个进度曲线的点的xy坐标,即spend_budgetrecommendations中的每个内部列表。

  6. 使用线性插值的值来绘制我们运行的两种策略的平均性能和误差条,并比较它们的性能。

  7. 由于我们当前正在测量优化性能的方式,我们可能会发现我们在每次运行中跟踪的推荐列表不是单调递增的。(也就是说,我们可能会得到一个比上一次迭代的推荐性能更差的推荐。)为了检查这种现象,我们可以绘制代表各个运行的优化进度的线性插值曲线,以及平均性能和误差条。为我们运行的两种策略实现此可视化,并检查结果曲线的非单调性。

9.6 练习 2:使用多个低保真度近似进行多保真度优化

我们在本章中学到的方法可以推广到存在多个我们可以查询的目标函数的低保真度近似的场景中。我们的策略是相同的:将我们从每个查询中获得的信息量除以其成本,然后选择提供最高投资回报率的查询。这个练习向我们展示了我们的多保真度 MES 策略可以在多个低保真度函数之间取得平衡。解决方案存储在 CH09/05 - Exercise 2.ipynb 笔记本中。

接下来执行以下步骤:

  1. 对于我们的目标函数,我们使用名为 Branin 的二维函数,它是优化的常见测试函数,就像 Forrester 函数一样。BoTorch 提供了 Branin 的多保真度版本,因此我们使用 from botorch.test_functions.multi_fidelity import AugmentedBranin 将其导入到我们的代码中。为了方便起见,我们使用以下代码对该函数的域和输出进行缩放,这使 objective 成为我们评估查询时要调用的函数:

    problem = AugmentedBranin()                              ❶
    
    def objective(X):
        X_copy = X.detach().clone()                          ❷
        X_copy[..., :-1] = X_copy[..., :-1] * 15 - 5         ❷
        X_copy[..., -2] = X_copy[..., -2] + 5                ❷
        return (-problem(X_copy) / 500 + 0.9).unsqueeze(-1)  ❷
    

    ❶ 从 BoTorch 中导入 Branin 函数

    ❷ 处理函数的输入和输出,将值映射到一个好的范围内

  2. 将我们的搜索空间的边界定义为单位正方形。也就是说,两个下界为 0,两个上界为 1。

  3. 声明存储我们可以查询的不同函数的相关值的 fidelities 变量。在这里,我们可以访问两个相关值分别为 0.1 和 0.3 的低保真度近似,因此 fidelities 应包含这两个数字和最后一个元素为 1。

    图 9.15 目标函数 Branin(右侧)和两个低保真度近似

    这三个函数在图 9.15 中可视化,明亮的像素表示高目标值。我们可以看到,两个低保真度的近似都遵循地面真相展示的一般趋势,并且与真实情况的相似程度随着保真度值的增加而增加。也就是说,保真度为 0.3 的第二个近似(中间)与真实的目标函数(右侧)更相似,比保真度为 0.1 的第一个近似(左侧)更相似。

  4. 将线性成本模型的固定成本设置为 0.2,权重设置为 1。这意味着在图 9.15 左侧查询低保真度函数的成本为 0.2 + 1 × 0.1 = 0.3。类似地,中间函数的成本为 0.5,真实目标函数的成本为 1.2。将每个实验的预算限制设置为 10,并将重复实验的次数也设置为 10。

  5. 从 Sobol 序列中抽取的候选人数设置为 5,000,并且在使用辅助函数优化给定策略的获取分数时,使用 100 次重启和 500 个原始样本。

  6. 重新定义助手函数 get_final_recommendation,以便为我们的二维目标函数设置适当的参数:d = 3columns = [2]

  7. 运行多保真度最大值熵搜索策略及其单保真度版本的优化问题,并使用练习 1 中描述的方法绘制每个策略的平均优化进展和误差线。注意,在为单保真度策略创建包装器策略时,参数 dcolumns 需要与上一步骤中设置的方式相同。验证多保真度策略的表现优于单保真度策略。

摘要

  • 多保真度优化是一种优化设置,我们可以访问多个信息源,每个信息源具有自己的准确度和成本水平。在这种情况下,我们需要平衡从行动中获得的信息量和采取该行动的成本。

  • 在多保真度优化中,高保真度函数提供确切的信息,但评估代价高,而低保真度函数查询成本低,但可能提供不准确的信息。在优化循环的每次迭代中,我们需要决定在哪里和哪个函数进行查询,以尽快找到目标函数的最优值。

  • 每个保真度提供的信息水平通过其与真实值之间的相关性来量化,该相关性是一个介于 0 和 1 之间的数字。相关性越高,保真度与真实值越接近。在 Python 中,我们将每个数据点的相关性值存储为特征矩阵中的额外列。

  • 基于可以处理训练数据点的相关性值的内核,可以对多保真度数据集进行训练,我们可以使用多保真度变体的 Matérn 内核来完成此任务。GP 对于预测的不确定性取决于每个观测点所来自的函数,以及如果它来自于低保真度函数,则该函数的相关性值如何。

  • 我们使用线性模型来编码优化过程中每个保真度的查询成本。通过设置此模型的参数-固定成本和权重,我们可以建模查询成本与数据质量之间的正相关关系。也可以使用 BoTorch 实现非线性成本模型。

  • 为了在信息性和成本之间取得平衡,我们使用 MES 策略的变体,通过查询成本的倒数对每个查询的信息量进行加权。该度量类似于经济学中的投资回报概念,并使用 BoTorch 中的 InverseCostWeightedUtility 类进行实现。

  • 对于目标函数的最优值的信息增益的精确计算,即 MES 的核心任务,由于最优值的非高斯分布,因此是棘手的。为了近似计算信息增益,我们使用 Sobol 序列来表示整个搜索空间,减轻计算中的计算负担。

  • 多信度 MES 策略成功平衡了信息和成本,并优先考虑成本效益高的查询。

  • 为了优化多信度策略的收购分数,我们使用optimize_ acqf_mixed辅助函数,该函数可以处理混合搜索空间,其维度可以是连续的或离散的。

  • 在多信度设置中准确测量性能,我们使用每次迭代中后验均值的最大化者作为终止前的最终建议。这一数量比高保真现有值更好地捕捉了我们对目标函数的了解。

  • 在多信度设置中优化单信度收购分数,我们使用FixedFeatureAcquisitionFunction类的一个实例作为包装策略。为了初始化一个包装策略,我们声明搜索空间中的哪个维度是固定的,以及其值是多少。

第十一章:通过偏好优化进行成对比较学习

本章涵盖

  • 仅使用成对比较数据学习和优化偏好的问题

  • 在成对比较上训练 GP

  • 成对比较的优化策略

你是否曾经发现难以为某物(食物、产品或体验)打分?在 A/B 测试和产品推荐工作流程中,询问客户对产品的数值评分是一个常见任务。

定义术语 A/B 测试 指的是通过随机实验在两个环境(称为 AB)中测量用户体验,并确定哪个环境更理想的方法。 A/B 测试通常由技术公司进行。

A/B 测试人员和产品推荐工程师经常需要处理从客户收集的反馈中的高水平噪音。通过噪音,我们指的是客户反馈所受到的任何类型的损坏。产品评分中的噪音示例包括在线流媒体服务上提供的广告数量,包裹的送货服务质量,或客户在消费产品时的一般心情。这些因素影响客户对产品的评分方式,可能会破坏客户对产品的真实评价。

不可控的外部因素使客户难以报告他们对产品的真实评价。因此,当在评分时难以选择数值评分作为对产品的评价时,客户通常会发现这很困难。在 A/B 测试和产品推荐中的反馈噪音的普遍存在意味着服务平台不能仅依靠从用户那里收集到的少量数据来了解其偏好。相反,平台需要从客户那里收集更多数据,以更加确定客户真正想要的是什么。

然而,正如在其他黑盒优化设置中一样,比如超参数调整和药物发现,查询目标函数是昂贵的。在产品推荐中,每当我们询问客户对产品的评分时,我们都面临着侵入客户体验和阻止他们继续使用平台的风险。因此,需要大量数据来更好地了解客户的偏好和具有侵入性之间存在自然紧张关系,可能导致客户流失。

幸运的是,有一种方法可以解决这个问题。心理学领域的研究(mng.bz/0KOl)发现了一个直观的结果,即我们人类在进行成对比较的偏好反应方面要比在评分产品时更擅长(例如,“产品 A 比产品 B 更好”)。

定义 成对比较是一种收集偏好数据的方法。每次我们想要获取有关客户偏好的信息时,我们都会要求客户从两个项目中选择他们更喜欢的项目。成对比较不同于数值评分,其中我们要求客户在一定比例上对项目进行评分。

成对比较和评分之间难度差异的原因在于,比较两个项目是一项认知要求较低的任务,因此,我们可以在比较两个对象时更好地与我们的真实偏好保持一致,而不是提供数值评分。在图 10.1 中,考虑一个在线购物网站的两个示例界面,该网站正在尝试了解您对夏威夷衬衫的偏好:

  • 第一个界面要求您按照从 1 到 10 的比例为衬衫评分。这可能很难做到,特别是如果您没有一个参考框架的话。

  • 第二个界面要求您选择您更喜欢的衬衫。这个任务更容易完成。

图 10.1 生产推荐中用户偏好引导的示例。左侧,用户被要求对推荐产品进行评分。右侧,用户被要求选择他们更喜欢的产品。后者有助于更好地引导用户的偏好。

鉴于我们可以使用成对比较收集高质量数据的潜力,我们希望将这种偏好引导技术应用于用户偏好的 BayesOpt。问题是,“我们如何在成对比较数据上训练 ML 模型,然后,如何向用户呈现新的比较以最好地学习和优化他们的偏好?”我们在本章中回答了这些问题,首先使用一个能够有效地从成对比较中学习的 GP 模型。然后,我们开发了策略,将迄今为止我们找到的最佳数据点(代表一个产品)与一个有希望的竞争对手相比较,从而使我们能够尽快优化用户的偏好。换句话说,我们假设用户的偏好是定义在搜索空间上的目标函数,我们希望优化这个目标函数。

从成对比较中学习和优化用户偏好的这种设置是一个独特的任务,位于黑盒优化和产品推荐的交集处,两个社区都对此产生了兴趣。通过本章末尾,我们了解了如何从 BayesOpt 的角度解决这个问题,通过从用户那里收集数据来权衡开发和探索。

10.1 使用成对比较进行黑盒优化

在这一部分中,我们进一步讨论了成对比较在引导偏好任务中的有用性。然后,我们研究了为这种基于偏好的优化设置修改过的 BayesOpt 循环。

除了将精确的数字评估作为评级外,成对比较还提供了一种在生产推荐应用中收集有关客户信息的方法。与数字评级相比,成对比较对用户的认知负担较小,因此可能会产生更高质量的数据(即与用户真实偏好一致的反馈)。

多目标优化中的成对比较

成对比较特别有用的一个场景是在需要考虑多个标准的决策中。例如,假设你想买一辆车,正在选择 A 车和 B 车。为了做出决定,你列出了你在一辆车上关心的不同特征:外观、实用性、能效、成本等。然后你为两辆车在每个标准上评分,希望找到一个明显的赢家。不幸的是,A 车在某些标准上得分比 B 车高,但并不是所有标准都是如此,而 B 车在其余标准上的得分高于 A 车。

所以,在这两辆车之间没有明显的赢家,将不同标准的分数合并成一个单一分数可能会很困难。你关心某些标准胜过其他标准,所以在将这些标准与其他标准相结合以产生单一数字时,需要更加重视这些标准的权重。然而,确定这些权重的确切值可能比选择这两辆车本身更具挑战性!忽视具体细节,将每辆车作为一个整体,并将两辆车进行“头对头”比较可能更容易一些。

因此,在需要考虑多个标准的优化情况下,利用成对比较的便利性已经被利用起来。例如,Edward Abel、Ludmil Mikhailov 和 John Keane 的一个研究项目 (mng.bz/KenZ) 使用成对比较来解决群体决策问题。

当然,成对比较并不一定比数字评估更好。虽然前者更容易从用户那里获取,但它们所包含的信息明显比后者少得多。比如,在图 10.1 中,你喜欢橙色衬衫胜过红色衬衫的回答正好包含一位信息(比较的结果是二元的;要么橙色比红色好,要么红色比橙色好,所以观察结果信息理论上构成了一位信息)。而如果你报告说你给橙色衬衫评了 8 分,红色衬衫评了 6 分,那么我们获得的信息就比仅仅知道橙色被高估要多得多。

换句话说,在选择从用户那里引出反馈的方法时总是存在权衡。数值评估包含更多信息,但容易受到噪声影响,并且可能给用户带来更大的认知负担。另一方面,两两比较提供的信息较少,但用户报告起来更容易。这些优缺点在图 10.2 中总结。

图 10.2 数值评分和两两比较在信息量和报告难度方面的差异。每种偏好引出方法都有其优缺点。

在考虑信息和报告难度之间的权衡时,如果我们愿意让用户完成更具认知要求的任务以获取更多信息,并且可以考虑到噪声,那么我们应该坚持使用数值评估。另一方面,如果我们更注重客户准确表达他们真实的偏好,并且愿意获取更少的信息,那么两两比较应该是我们引出客户反馈的首选方法。

其他引出客户偏好的方法

两两比较并不是减轻数值评估认知负担的唯一形式。例如,在线流媒体服务 Netflix 通过要求观众在三个选项中进行选择来收集观众的评分:“向下拇指”表示他们不喜欢某物,“向上拇指”表示他们喜欢某物,“双向上拇指”表示他们喜欢某物 (mng.bz/XNgl)。这种设置构成了一种有序分类问题,其中项目被分类到不同的类别中,并且类别之间存在固有的顺序。在这种情况下,产品推荐问题同样值得考虑,但在本章中我们将重点放在两两比较上。

在本章中,我们学习如何利用 BayesOpt 来促进使用两两比较来学习和优化客户偏好的任务。首先,我们研究了一个修改过的 BayesOpt 循环版本,如图 1.6 所示,如图 10.3 所示:

  1. 在第一步中,GP 是根据两两比较数据而不是数值评估进行训练的。关键挑战在于确保 GP 对于目标函数(用户真实偏好函数)的信念反映了观察到的比较中的信息。

  2. 在第二步中,BayesOpt 策略计算获取分数,以量化对用户每个潜在新查询的有用程度。用户的查询需要以一对产品的形式提供给用户进行比较。就像在其他情况下一样,策略需要平衡利用我们知道用户偏好高的区域和探索我们对用户偏好了解不多的其他区域。

  3. 在第 3 步中,用户比较了由贝叶斯优化策略呈现给他们的两种产品,并报告他们更喜欢的产品。然后,将此新信息添加到我们的训练集中。

图 10.3 使用成对比较进行偏好优化的贝叶斯优化循环。高斯过程根据成对比较数据进行训练,而贝叶斯优化策略决定应该要求用户比较哪一对数据点。

我们在本章的剩余部分试图解决两个主要问题:

  1. 我们如何仅根据成对比较训练高斯过程?高斯过程在数值响应上进行训练时,会产生具有量化不确定性的概率预测,这在决策中至关重要。我们能否在这里使用相同的模型来处理成对比较响应?

  2. 我们应该如何生成新的产品对供用户比较,以便尽快确定用户偏好的最大化者?也就是说,我们如何通过成对比较最好地引出用户的反馈以优化他们的偏好?

10.2 制定偏好优化问题和格式化成对比数据

在我们开始解决这些问题之前,本节介绍了我们将在整章中解决的产品推荐问题以及我们如何在 Python 中模拟这个问题。正确设置问题将帮助我们更轻松地整合我们将在随后章节学习到的贝叶斯优化工具。我们在此处使用的代码包含在 CH10/01 - 从成对比较中学习.ipynb Jupyter 笔记本的第一部分中。

正如图 10.1 和 10.3 所示的,我们现在面临的情景是夏威夷衬衫的产品推荐问题。也就是说,想象我们经营一家夏威夷衬衫的在线购物网站,我们试图确定一款特定客户在购物时最大化偏好的产品。

为了简单起见,让我们假设在简要调查之后,我们得知对客户最重要的因素是衬衫上印花的数量。其他因素,如款式和颜色,也很重要,但对于这个客户来说,夏威夷衬衫最重要的是衬衫上的花朵有多少。此外,假设我们库存中有许多夏威夷衬衫,花朵数量各异,因此我们大致可以找到任何指定“花朵程度”的衬衫。因此,我们的目标是找到符合客户偏好的衬衫,这对我们来说是未知的。我们在一维搜索空间中进行这一搜索,其中空间的下限对应于没有花纹的衬衫,空间的上限包含覆盖着花朵的衬衫。

图 10.4 更详细地展示了我们的设置。在图的顶部部分,显示了客户的真实偏好以及随着衬衫花朵程度的变化而变化的情况:

  • x轴表示衬衫的花朵数量。在光谱的一端,我们有没有花朵的衬衫;另一端是满是花朵的衬衫。

  • y轴是每个衬衫的客户偏好度。客户对衬衫的偏好度越高,表示客户越喜欢这件衬衫。

图 10.4 在一个产品推荐问题中搜索具有最佳花朵数量的衬衫。我们的搜索空间是一维的,因为我们只搜索衬衫上花朵的数量。一件半面覆盖花朵的衬衫是一个局部最优点,而几乎完全覆盖的衬衫最大化了用户的偏好。

我们可以看到这个客户喜欢花纹衬衫:在衬衫的中间点过后有一个局部最优点,而偏好函数的全局最优点位于搜索空间的上界附近。这意味着一件有很多花朵但不完全覆盖的衬衫最大化了客户的偏好。

由于我们处理的是一个黑盒优化问题,在实际世界中我们实际上无法获得图 10.4 中客户的偏好曲线,我们需要使用成对比较来学习这个偏好函数,并尽快对其进行优化。现在,让我们看看如何在 Python 中设置这个优化问题。

你可能已经注意到,在图 10.4 中我们使用了前几章中使用的 Forrester 函数来模拟客户的目标函数,也就是客户的真实偏好。因此,这个函数的代码与前几章没有任何区别,其定义如下(定义范围是我们搜索空间的下界-5 和上界 5 之间):

def objective(x):                                       ❶
    y = -((x + 1) ** 2) * torch.sin(2 * x + 2) /
    ➥5 + 1 + x / 3                                     ❶
    return y                                            ❶

lb = -5                                                 ❷
ub = 5                                                  ❷
bounds = torch.tensor([[lb], [ub]], dtype=torch.float)  ❷

❶ 目标函数

❷ 搜索空间的边界

从前几章可以记得,当我们的数据标签具有数值值时,在变量train_x中的每个数据点都有对应的train_y标签。我们当前的设置有点不同。由于我们的数据以成对比较的形式存在,每个观察结果都来自于对train_x中的两个数据点进行比较,并且观察的标签指示了客户更看重哪个数据点。

注意:我们遵循 BoTorch 的规定,用一个包含两个元素的 PyTorch 张量来编码在 train_x 中每对数据点之间的每个配对比较的结果:第一个元素是在 train_x 中被偏好的数据点的索引,第二个元素是未被偏好的数据点的索引。

举个例子,假设根据两次用户查询,我们知道用户更喜欢x = 0 而不是x = 3(也就是说,f(0)>f(3),其中fx)是目标函数),用户也更喜欢x = 0 而不是x = -4(所以f(0)>f(-4))。我们可以用train_x来表示这两个信息作为训练数据集,train_x的取值如下:

tensor([[ 0.],   ❶
        [ 3.],   ❷
        [-4.]])  ❸

❶ 表示 x = 0

❷ 表示 x = 3

❸ 表示 x = −4

这些值是我们用来查询用户的三个x值。而训练标签train_comp则应该

tensor([[0, 1],   ❶
        [0, 2]])  ❷

❶ 表示 f(0) > f(3)

❷ 表示 f(0) > f(−4)

train_comp中的每一行都是表示成对比较结果的两个元素张量。在第一行中,[0,``1]表示train_x中索引为0的数据点(即x = 0)优先于索引为1的点(即x = 3)。同样,第二行[0,``2]编码了比较f(0) > f(-4)。

为了简化在我们的搜索空间内比较任意一对数据点的过程,我们编写了一个辅助函数,该函数接受任意两个数据点的目标值,并在第一个目标值大于第二个值时返回[0,``1],否则返回[1,``0]

def compare(y):
    assert y.numel() == 2                     ❶

    if y.flatten()[0] > y.flatten()[1]:       ❷
        return torch.tensor([[0, 1]]).long()  ❷
    else:                                     ❷
        return torch.tensor([[1, 0]]).long()  ❷

❶ 确保我们只有两个目标值进行比较

❷ 如果第一个值较大

❸ 如果第二个值较大

让我们使用这个函数来生成一个样本训练集。我们首先在我们的搜索空间内随机绘制两个数据点:

torch.manual_seed(0)                                              ❶
train_x = bounds[0] + (bounds[1] - bounds[0]) * torch.rand(2, 1)  ❷

❶ 为了可重现性,修正随机种子

❷ 在 0 和 1 之间绘制两个数字,并将它们缩放到我们的搜索空间

此处的变量train_x包含以下两个点:

tensor([[-0.0374],
        [ 2.6822]])

现在,我们通过评估用户的真实偏好函数并调用compare()来获得这两个点之间的比较结果:

train_y = objective(train_x)    ❶
train_comp = compare(train_y)   ❷

❶ 计算实际的目标值,这些值对我们是隐藏的

❷ 获取比较结果

train_x中数据点的目标值的比较结果存储在train_comp中,即

tensor([[0, 1]])

这个结果意味着train_x中的第一个数据点比第二个点受到客户的更高评价。

我们还编写了另一个名为observe_and_append_data()的辅助函数,其作用是接受一对数据点,比较它们,并将比较结果添加到运行的训练集中:

  1. 该函数首先调用辅助函数compare()来获得[0,``1][1,``0],然后调整存储在两个元素张量中的索引值,以便这些索引指向训练集中数据点的正确位置:

    def observe_and_append_data(x_next, f, x_train, comp_train, tol=1e-3):
        x_next = x_next.to(x_train)              ❶
        y_next = f(x_next)                       ❶
        comp_next = compare(y_next)              ❶
    
        n = x_train.shape[-2]                    ❷
        new_x_train = x_train.clone()            ❷
        new_comp_next = comp_next.clone() + n    ❷
    

    ❶ 根据用户的偏好评估比较

    ❷ 跟踪索引

  2. 该函数还检查训练集中彼此接近到可以视为相同点的数据点(例如,x = 1 和 x = 1.001)。这些非常相似的数据点可能会导致我们在下一节学习的基于偏好的高斯过程的训练变得数值不稳定。我们的解决方案是标记这些相似的数据点,将它们视为重复项,并删除其中一个:

    n_dups = 0
    
      dup_ind = torch.where(                                   ❶
          torch.all(torch.isclose(x_train, x_next[0],
          ➥atol=tol), axis=1)                                 ❶
      )[0]                                                     ❶
      if dup_ind.nelement() == 0:                              ❷
          new_x_train = torch.cat([x_train, x_next[0]
          ➥.unsqueeze(-2)])                                   ❷
      else:                                                    ❸
          new_comp_next = torch.where(                         ❸
              new_comp_next == n, dup_ind, new_comp_next - 1   ❸
          )                                                    ❸
          n_dups += 1
    
      dup_ind = torch.where(                                   ❹
          torch.all(torch.isclose(new_x_train, x_next[1],
          ➥atol=tol), axis=1)                                 ❹
      )[0]                                                     ❹
      if dup_ind.nelement() == 0:                              ❷
          new_x_train = torch.cat([new_x_train, x_next[1]
          ➥.unsqueeze(-2)])                                   ❷
      else:                                                    ❺
          new_comp_next = torch.where(                         ❺
              new_comp_next == n + 1 - n_dups, dup_ind,
              ➥new_comp_next                                  ❺
          )                                                    ❺
    
      new_comp_train = torch.cat([comp_train,
      ➥new_comp_next])                                        ❻
      return new_x_train, new_comp_train                       ❻
    

    ❶ 检查新对中第一个数据点的重复项

    ❷ 如果没有重复,则将数据点添加到 train_x 中

    ❸ 如果至少有一个重复项,则跟踪重复项的索引

    ❹ 检查新对中第二个数据点的重复项

    ❺ 如果至少有一个重复项,请跟踪重复项的索引

    ❻ 返回更新后的训练集

我们在训练 GP 和优化用户偏好函数的下游任务中利用这两个辅助函数,我们将在下一节中探讨第一个辅助函数。

10.3 训练基于偏好的高斯过程

我们将继续使用 CH10/01 - 从成对比较中学习.ipynb 笔记本中的代码,在本节中实现我们的 GP 模型。

我们在第 2.2.2 节中学到,在贝叶斯更新规则下(这使我们能够根据数据更新我们对数据的信念),我们可以在观察到一些变量的值的情况下获得 MVN 分布的精确后验形式。准确计算后验 MVN 分布的能力是根据新观测更新 GP 的基础。不幸的是,这种精确更新仅适用于数值观测。也就是说,我们只能使用形式为 y = f(x) 的观测精确更新 GP,其中 xy 是实数。

在我们当前的设置下,观测结果以成对比较的形式出现,当以这种类型的基于偏好的数据为条件时,GP 的后验形式再也不是 GP,这排除了我们在本书中开发的大部分依赖于我们的预测模型是 GP 这一事实的方法。然而,这并不意味着我们必须放弃整个项目。

在成对比较下近似后验 GP

机器学习(以及计算机科学一般)中的一个共同主题是在无法准确完成任务时尝试近似解决任务。在我们的上下文中,这种近似等同于为我们的 GP 找到一个后验形式,该后验形式为我们观察到的成对比较提供了最高的可能性。对此方法感兴趣的读者可以在 Wei Chu 和 Zoubin Ghahramani 提出的研究论文中找到更多细节:mng.bz/9Dmo

当然,真正最大化数据可能性的分布是非 GP 后验分布。但是由于我们希望将 GP 作为我们的预测模型,从而实现我们已学到的贝叶斯优化策略,我们的目标是找到具有最高数据可能性的 GP。请注意,找到最大化数据可能性的 GP 也是我们训练 GP 时所做的事情:我们找到最佳的 GP 超参数(例如,长度尺度和输出尺度),以最大化数据可能性。(请参见第 3.3.2 节,我们首先讨论了这种方法。)

在实现方面,我们可以使用以下代码对成对比较进行初始化和训练 GP:

  • BoTorch 为这个 GP 模型提供了一个特殊的类实现,命名为 PairwiseGP,可以从 botorch.models.pairwise_gp 模块中导入。

  • 两两比较数据的可能性需要与实值数据的可能性不同的计算。对于这种计算,我们使用从同一模块导入的 PairwiseLaplaceMarginalLogLikelihood

  • 为了能够可视化和检查 GP 进行的预测,我们固定其输出比例,使其在训练期间保持其默认值 1。我们通过使用 model.covar_module.raw_outputscale.requires_grad_(False) 来禁用其梯度来实现这一点。这一步仅用于可视化目的,因此是可选的;在本章后面运行优化策略时我们不会这样做。

  • 最后,我们使用 botorch.fit 中的辅助函数 fit_gpytorch_mll 来获得最大化我们训练数据可能性的后验 GP:

from botorch.models.pairwise_gp import PairwiseGP,        ❶
➥ PairwiseLaplaceMarginalLogLikelihood                   ❶
from botorch.fit import fit_gpytorch_mll                  ❶

model = PairwiseGP(train_x, train_comp)                   ❷
model.covar_module.raw_outputscale.requires_grad_(False)  ❸
mll = PairwiseLaplaceMarginalLogLikelihood(model)         ❹
fit_gpytorch_mll(mll);                                    ❺

❶ 导入必要的类和辅助函数

❷ 初始化 GP 模型

❸ 固定输出比例以获得更易读的输出(可选)

❹ 初始化(对数)可能性对象

❺ 通过最大化可能性训练模型

使用这个训练好的 GP 模型,我们现在可以在图 10.5 中跨我们的搜索空间进行预测并可视化。关于这些预测,我们注意到一些有趣的点:

  • 均值预测遵循训练数据中表示 f(-0.0374) > f(2.6822) 的关系,在这个关系中,在 x = –0.0374 处的均值预测大于 0,而在 x = 2.6822 处小于 0。

  • 在–0.0374 和 2.6822 处的预测不确定性也比其余预测低。这种不确定性的差异反映了观察到 f(-0.0374) > f(2.6822) 后,我们对 f(-0.0374) 和 f(2.6822) 有了一些信息,我们对这两个目标值的了解应该增加。

    然而,在这些点上的不确定性并没有显著减少到零,正如我们在训练数值观测时看到的情况(例如,图 2.14 中)。这是因为,正如我们在第 10.1 节中所述,两两比较没有提供与数值评估相同数量的信息,因此仍然存在显著水平的不确定性。图 10.5 显示了我们训练的 GP 可以有效地从两两比较中学习,其中均值函数遵循观察到的比较,并且不确定性是良好校准的。

在进行预测时的 BoTorch 警告

当使用我们刚刚训练的 GP 进行预测时,您可能会遇到 BoTorch 类似以下的警告:

NumericalWarning: A not p.d., added jitter of 1.0e-06 to the diagonal
  warnings.warn(

这个警告表示 GP 生成的协方差矩阵不是正定的,导致数值稳定性相关的问题,BoTorch 已经自动向矩阵的对角线添加了“抖动”作为修复措施,所以我们用户不需要再做进一步的操作。有关我们遇到此警告的示例,请参阅第 5.3.2 节。

图 10.5 展示了由 GP 训练出的对比 f(–0.0374) > f(2.6822) 的预测。后验均值反映了这个比较的结果,而围绕两个数据点的后验标准偏差从先验中略微减小。

要进一步玩弄这个模型,并看看它如何从更复杂的数据中学习,让我们创建一个稍大一点的训练集。具体来说,假设我们想要训练 GP 在三个单独的比较上:f(0) > f(3),f(0) > f(–4),和 f(4) > f(–0),所有这些对于我们在图 10.5 中有的目标函数都是正确的。为此,我们将我们的训练数据点存储在 train_x

train_x = torch.tensor([[0.], [3.], [-4.], [4.]])

这个集合包含了前面观察到的所有比较中涉及的所有数据点。至于 train_comp,我们使用我们在第 10.2 节中讨论过的方式,使用两元张量来编码这三个比较:

train_comp = torch.tensor(
    [
        [0, 1],    ❶
        [0, 2],    ❷
        [3, 0],    ❸
    ]
)

❶ [0, 1] 表示 f(train_x[0]) > f(train_x[1]),或者 f(0) > f(3)。

❷ [0, 2] 表示 f(train_x[0]) > f(train_x[2]),或者 f(0) > f(−4)。

❸ [3, 0] 表示 f(train_x[3]) > f(train_x[0]),或者 f(4) > f(0)。

现在,我们简单地重新声明 GP 并在这个新的训练数据上重新拟合它:

model = PairwiseGP(train_x, train_comp)              ❶
mll = PairwiseLaplaceMarginalLogLikelihood(model)    ❷
fit_gpytorch_mll(mll)                                ❸

❶ 初始化 GP 模型

❷ 初始化(对数)似然对象

❸ 通过最大化似然来训练模型

GP 模型产生了图 10.6 中显示的预测,在这里我们看到训练数据中的所有三个比较结果都反映在平均预测中,并且不确定性再次在训练数据点周围减小。

图 10.6 展示了由对比训练的 GP 进行的预测,右侧显示了后验均值反映了这个比较的结果,而在训练集中数据点周围的后验标准偏差从先验中减小了。

图 10.6 显示了我们的 GP 模型可以有效地在对比数据上进行训练。我们现在有了一种方法来从基于偏好的数据中学习,并对用户的偏好函数进行概率预测。这引导我们来到本章的最后一个话题:偏好优化中的决策制定。也就是说,我们应该如何选择数据对让用户将它们进行比较以尽快找到最受欢迎的数据点?

优先学习中目标函数的范围

在将 GP 训练成对比较的方式与使用数值评估进行比较时,一个有趣的优势是,在训练过程中不需要考虑目标函数的范围。这是因为我们只关心目标值之间的相对比较。换句话说,了解 f(x) 等同于了解 f(x) + 5,或者 2 f(x),或者 f(x) / 10。

与此同时,当训练传统的 GP 时,使用数值评估是至关重要的,因为只有这样我们才能拥有一个具有良好校准的不确定性量化的模型。 (例如,要对范围从-1 到 1 的目标函数建模,适当的输出尺度为 1,而对于范围从-10 到 10 的目标函数,我们需要更大的输出尺度。)

10.4 通过玩“山顶之王”进行偏好优化

在本节中,我们学习将 BayesOpt 应用于偏好学习。 我们使用的代码包含在 CH10/02 - 优化偏好.ipynb 笔记本中。

我们需要解决的问题是如何选择最佳的一对数据点,呈现给用户,并询问他们的偏好,以找到用户最喜欢的数据点。 与任何 BayesOpt 优化策略一样,我们的策略需要在利用(将搜索空间中用户价值高的区域归零)和探索(检查我们不太了解的区域)之间取得平衡。

我们在第四章到第六章学到的 BayesOpt 策略有效地使用各种启发式方法来解决利用-探索的权衡。 因此,我们将开发一种策略来重新利用这些策略,以适应我们基于偏好的设置。 请记住,在前几章中,BayesOpt 策略为搜索空间中的每个数据点计算一个收获分数,量化帮助我们优化目标函数的数据点的价值。 通过找到最大化此收获分数的数据点,我们获得下一个要评估目标函数的点。

使用 BayesOpt 策略建议成对比较

在我们当前的基于偏好的设置中,我们需要向用户展示一对数据点以供他们比较。 在优化循环的每次迭代中,我们首先组装这一对数据点,第一个是最大化给定 BayesOpt 策略的收获分数的数据点,第二个是我们迄今为止看到的最佳点。

我们使用的策略类似于流行的儿童游戏“山顶之王”,在每次迭代中,我们试图“击败”迄今为止收集到的最佳数据点(当前的“山顶之王”),使用一个由 BayesOpt 策略选择的挑战者,如图 10.7 所示。

图 10.7 在贝叶斯偏好优化中“山顶之王”策略的示意图。我们将迄今为止看到的最佳点与由 BayesOpt 策略确定的一个有希望的候选点进行比较。

通过使用这种“山顶之王”策略,我们将构造一对数据点的任务外包给了一个常规的 BayesOpt 策略,该策略能够很好地平衡利用-探索的权衡,并且我们已经知道如何使用它了。

从代码的角度来看,这个策略实现起来很简单。我们只需声明一个 BayesOpt 策略对象,并使用辅助函数optimize_acqf()优化其收获分数。例如,以下代码使用了我们在 5.2 节中学到的上置信度界(UCB)策略。UCB 策略使用由 GP 生成的预测正态分布的上界作为收获分数,以量化检查数据点的价值:

policy = UpperConfidenceBound(model, beta=2)   ❶

challenger, acq_val = optimize_acqf(           ❷
    policy,                                    ❷
    bounds=bounds,                             ❷
    q=1,                                       ❷
    num_restarts=50,                           ❷
    raw_samples=100,                           ❷
)                                              ❷

❶ 初始化 BayesOpt 策略

❷ 找到最大化收获分数的数据点

另一个我们使用的策略是期望改善(EI),我们在 4.3 节中学到了这个策略。EI 的一个特点使得它适用于我们的环境,就是该策略的动机与我们采用的“山顶之王”策略完全匹配。也就是说,EI 旨在搜索数据点,这些数据点平均而言可以从迄今为止看到的最佳点导致最大的改进(就目标函数的值而言,我们的优化目标)。超过迄今为止找到的最佳值恰恰是“山顶之王”策略的全部内容。为了在我们的环境中实现 EI,我们使用了一种不同的类实现,它可以处理嘈杂的观测值,命名为qNoisyExpectedImprovement

BayesOpt 中的嘈杂观测

BayesOpt 中的嘈杂观测一词指的是我们怀疑观察到的标签可能受到与本章开头描述的方式相同的噪声的污染。

正如图 10.5 和 10.6 所示,在包括在我们的训练数据train_x中的位置上,我们的 GP 预测仍然存在相当大的不确定性。在这里应该使用嘈杂的 EI 版本,因为这个策略处理这种类型的不确定预测比常规 EI 策略更好。我们实现嘈杂的 EI 如下:

policy = qNoisyExpectedImprovement(model, train_x)  ❶

challenger, acq_val = optimize_acqf(                ❷
    policy,                                         ❷
    bounds=bounds,                                  ❷
    q=1,                                            ❷
    num_restarts=50,                                ❷
    raw_samples=100,                                ❷
)                                                   ❷

❶ 初始化 BayesOpt 策略

❷ 找到最大化收获分数的数据点

作为比较的一点,让我们还包括一个简单的策略,即在搜索空间内均匀随机选择挑战者来挑选到目前为止看到的最佳点:

challenger = bounds[0] + (bounds[1] - bounds[0]) * torch.rand(1, 1)   ❶

❶ 在 0 和 1 之间随机选择一个点,并将该点缩放到我们的搜索空间

这个随机策略作为一个基准,用来确定我们手头的 BayesOpt 策略是否比随机选择更好。有了这些策略,我们现在准备好运行我们的 BayesOpt 循环,以优化我们示例问题中用户的偏好。这个循环的代码类似于我们在之前章节中使用的,除了将数据点对呈现给用户以获取他们的反馈并将结果附加到我们的训练集的步骤。这是通过我们在 10.2 节中编写的observe_and_ append_data()辅助函数完成的:

incumbent_ind = train_y.argmax()                   ❶

next_x = torch.vstack([train_x[incumbent_ind,
➥:], challenger])                                 ❷

train_x, train_comp = observe_and_append_data(     ❸
    next_x, objective, train_x, train_comp         ❸
)                                                  ❸
train_y = objective(train_x)                       ❸

❶ 找到迄今为止看到的最佳点

❷ 组装最佳点和策略建议的点的批次

❸ 更新我们的训练数据

在 CH10 / 02-Optimizing preferences.ipynb 笔记本中的代码中,每个 BayesOpt 运行都始于随机生成的一对数据点,以及比较这两个点的目标函数的反馈。然后,每个运行都进行 20 个成对比较(即,向用户查询 20 个问题)。我们还为每个策略重复实验 10 次,以便观察每种策略的汇总表现。

图 10.8 显示了我们使用的优化策略找到的平均最佳值(和误差条)。EI 性能最佳,不断发现全局最优解。也许 EI 的成功很大程度上归功于我们的“国王山”方法与 EI 背后的算法动机之间的一致性。更令人惊讶的是,UCB 未能优于随机策略;也许对于权衡参数β的不同值可以改善 UCB 的性能。

图 10.8 汇总了 10 个实验的各种 BayesOpt 策略的优化性能。EI 性能最佳,不断发现全局最优解。令人惊讶的是,UCB 未能优于随机策略。

注意,UCB 的权衡参数β直接控制策略在探索和利用之间的平衡。有关此参数的更多讨论,请参见第 5.2.2 节。

在本章中,我们介绍了使用成对比较进行偏好学习和优化的问题。我们了解了数据收集的这种特定方法背后的动机以及它优于要求用户报告数字评估的优点。然后,我们使用 BayesOpt 解决了优化问题,首先使用近似方法在成对比较上训练 GP。该 GP 模型可以有效地了解在训练集中表达的数据点之间的关系,同时仍然提供良好校准的不确定性量化。最后,我们学习将 BayesOpt 策略应用于该问题,进行最佳数据点与给定 BayesOpt 策略推荐的点之间的竞争。在下一章中,我们将了解一个黑盒优化问题的多目标变体,其中我们在优化过程中需要平衡多个竞争目标函数。

摘要

  • 在生产推荐应用中,比较两个物品可以帮助我们获得比数字评级更符合用户真实偏好的反馈。这是因为前者提出的任务量较小。

  • 成对比较包含较少的信息,因此在选择两种引出偏好的方法时,存在减轻用户认知负担和获得信息之间的权衡。

  • 可以训练 GP,使其最大化成对比较数据集的似然。当在成对比较数据上进行条件化时,此模型近似为真实的后验非 GP 模型。

  • 在成对比较上训练的高斯过程产生的均值预测与训练集中的比较结果一致。特别是在首选位置的均值预测大于非首选位置的均值预测。

  • 对于在成对比较上训练的高斯过程,其不确定性略微减小于先验高斯过程,但并未降至零,这恰如其分地反映了我们对用户偏好函数的不确定性,因为成对比较比数值评估提供的信息较少。

  • 使用贝叶斯优化优化用户偏好的策略涉及将找到的最佳数据点与由贝叶斯优化策略推荐的候选数据点进行比较。这一策略的动机是不断尝试从迄今为止找到的最佳点进行改进。

  • 成对比较的结果在 BoTorch 中表示为一个两元张量,其中第一个元素是首选的数据点在训练集中的索引,第二个元素是不被偏好的数据点的索引。

  • 在使用成对比较的优化设置中使用 EI 策略时,我们使用可以更好处理训练的高斯过程中高不确定性的噪声版本的策略,而不是常规的 EI。

第十二章:同时优化多个目标

本章涵盖内容

  • 同时优化多个目标的问题

  • 训练多个 GP 同时学习多个目标

  • 共同优化多个目标

每天,我们都面临着优化的权衡:

  • “这杯咖啡尝起来不错,但糖太多了。”

  • “那件衬衫看起来很棒,但超出了我的价格范围。”

  • “我刚训练的神经网络准确率很高,但太大了,训练时间太长。”

为了在某个目标上取得良好的性能,我们牺牲了另一个同样重要的标准:一个喜欢甜食的咖啡饮用者可能会优化咖啡的味道,同时使用不健康数量的糖;购物者在外观上打分高而在价格上打分低的服装;ML 工程师开发出具有良好预测性能但太大以至于无法在实时应用中使用的神经网络。通过专注于一个优化目标,我们可能在需要考虑的另一个目标上表现不佳。相反,我们应该将所有要优化的目标函数建模到我们的优化过程中,并尝试联合优化它们所有。例如,我们应该寻找既美味又低糖的咖啡配方,时尚又实惠的服装,或者性能良好且实用的 ML 模型。这种类型的优化问题称为多目标优化。

定义 多目标优化问题,正如其名称所示,涉及到多个要同时优化的目标函数。其目标是找到在所有目标上都达到高值的数据点。

当然,在任何非平凡的多目标优化问题中,我们可能会有竞争的目标,为了在一个目标函数上获得良好的性能,唯一的方法是牺牲另一个目标上的性能。这种优化目标之间的固有冲突导致了需要平衡这些目标的需求(非常类似于需要平衡探索和开发,讨论在第 4.1.2 节中,这是 BayesOpt 循环内需要优化的两个“目标”)。

在本章中,我们学习多目标优化,如何通过找到在一个目标上表现无法改善而不牺牲另一个目标的数据点成功地解决它,以及如何在目标函数是昂贵的黑箱的情况下将 BayesOpt 应用于这个问题。多目标优化是许多领域都面临的共同问题,到本章结束时,我们将使用贝叶斯方法来解决这个问题的能力添加到我们的工具包中。

11.1 使用 BayesOpt 平衡多个优化目标

多目标优化的应用无处不在:

  • 在工程和制造领域,工程师经常面临多个目标之间的权衡,比如产品质量与制造成本之间的权衡。例如,汽车制造商不断优化生产线以最大化质量,同时最小化成本。

  • 在资源分配问题中,如在贫困社区或自然灾害受灾人群之间分配货币和医疗援助,决策者需要在对这些社区产生最大影响和各种物流分配困难之间取得平衡。

  • 与我们在第 8.1.1 节中讨论的成本受限优化问题类似,开发治疗某种疾病的药物的科学家需要在最大化疗效和最小化对患者的副作用之间取得平衡。

  • 对于机器学习工程师更相关的是,一个可以在现实世界中部署的实用机器学习模型需要在保持低训练成本的同时实现良好的性能。

与之前章节讨论的优化设置不同,我们不再有单一的优化目标可以专注。在许多这些问题中,我们需要优化的目标之间存在冲突:只有牺牲一个指标的性能我们才能在另一个指标上取得提高。思考这些优化目标之间固有的冲突的一种方式是,我们必须同时“权衡”各种目标:我们不能简单地专注于某些目标而忽略其他目标。这种同时权衡多个目标的需求在图 11.1 中可视化。幸运的是,现在有多个目标函数不会影响我们在本书中开发的大部分贝叶斯优化工作流程。

图 11.1:漫画说明我们在多目标优化中需要取得的平衡,我们需要同时权衡不同的目标函数。

使用多个高斯过程模型建模多个目标函数

在之前的章节中,我们根据观察到的数据训练了一个高斯过程模型以建模我们对单一优化目标函数的信念。在本章中,我们需要建模多个目标函数,但每个这些目标仍然可以被建模为高斯过程。通过维护这些多个高斯过程,我们有一种以概率方式推理所有目标函数的方法。

图 11.2 展示了贝叶斯优化循环,其中有两个需要优化的目标函数。与图 1.6 相比,第 1 步现在针对每个目标都有一个高斯过程,而贝叶斯优化策略识别的每个数据点在第 3 步都要对所有目标函数进行评估。

图 11.2:多目标贝叶斯优化循环,具有两个目标函数。每个目标函数的数据进行高斯过程训练,而贝叶斯优化策略决定下一步评估目标函数的数据点。

对每个目标的数据进行 GP 训练非常容易实现;事实上,我们已经在第八章对约束优化进行了这样的训练,在那里我们对目标函数进行了一次 GP 训练,对约束函数进行了另一次 GP 训练。换句话说,我们只需要专注于图 11.2 中第 2 步中贝叶斯优化策略的设计,以帮助我们在优化过程中做出有效的决策。我们重点研究了贝叶斯优化策略如何应对多个目标之间的平衡,以便在本章的其余部分尽快找到性能优异的数据点。

11.2 寻找最优数据点的边界

在本节中,我们学习了在多目标优化中常用于量化我们在优化过程中取得多大进展的数学概念。这些概念帮助我们建立本章后面我们开发的优化策略的目标。为了使我们的讨论具体化,我们使用了 CH11/01 - 计算超体积.ipynb 笔记本中的代码。

我们从需要同时优化的两个目标函数开始:

  • 第一个目标是在以前章节中使用的熟悉的 Forrester 函数。该目标函数的全局最优解位于搜索空间的右侧。该函数在以下代码中实现为objective1()

  • 我们还有另一个目标函数,实现为objective2(),其具有与 Forrester 不同的功能形式和行为。关键是,该目标的全局最优解位于搜索空间的左侧——两个目标函数的全局最优解位置的不匹配模拟了多目标优化问题中常见的权衡。

  • 我们编写一个辅助函数joint_objective(),它返回给定输入数据点x的两个目标函数的值的 PyTorch 张量。这个函数有助于保持我们的代码简洁。

  • 最后,我们将我们的优化问题的搜索空间定义为-5 到 5 之间。

def objective1(x):                                     ❶
    return -((x + 1) ** 2) * torch.sin(2 * x + 2)
    ➥/ 5 + 1 + x / 20                                 ❶

def objective2(x):                                     ❷
    return (0.1 * objective1(x) + objective1(x - 4))
    ➥/ 3 - x / 3 + 0.5                                ❷

def joint_objective(x):                                ❸
    y1 = objective1(x)                                 ❸
    y2 = objective2(x)                                 ❸
    return torch.vstack([y1.flatten(), y2.flatten()])  ❸
    ➥.transpose(-1, -2)                               ❸

lb = -5                                                ❹
ub = 5                                                 ❹
bounds = torch.tensor([[lb], [ub]], dtype=torch.float) ❹

❶ 第一个目标函数

❷ 第二个目标函数

❸ 调用两个目标函数的辅助函数

❹ 搜索空间的边界

图 11.3 显示了我们搜索空间中的这两个目标函数。我们看到最大化这两个目标的数据点彼此不同:实线曲线在x=4.5 附近最大化,而虚线曲线在x=-4.5 附近最大化。这种差异意味着我们有两个相互冲突的目标,并且这两个函数的联合优化需要权衡它们的目标值。

图 11.3 我们当前的多目标优化问题的两个目标函数。最大化这两个目标的数据点彼此不同,因此在优化这两个目标时存在权衡。

通过“权衡”,我们指的是存在搜索空间中的点x,其第一个目标的值(表示为f1)无法提高,除非第二个目标的值(表示为f2)降低。换句话说,存在一些数据点在优化一个目标函数方面,其值无法超过,除非我们牺牲另一个目标函数。

例如,考虑图 11.3 中标示的x = –5。这是搜索空间的最左边的数据点。这个点的目标值f1 大约为 4,目标值f2 大约为 1.5。现在,x = –5 是一个数据点,如果我们想在第一个目标f1 上做得比 4 更好,我们将不得不在第二个目标f2 上做得不如 1.5。事实上,我们要实现高于 4 的f1 值的唯一方式是查询空间的最右侧,其中x > 4。在这里,f2 的值下降到 0 以下。

相反,右侧的区域(x > 4)也是f1 和f2 之间的紧张存在的地方:要增加f2 的值,我们必须向空间左侧移动,这样f1 的值就会受到影响。

定义:数据点其一个目标的值无法超过,除非另一个目标的值降低,称为非支配。相反,支配x[1]是指存在另一个点x[2],其所有目标值都超过x[1]的点。非支配点也可以称为帕累托最优帕累托有效非劣

因此,点x = –5 是一个非支配点,一些点当x > 4 也是非支配点。图 11.3 中的一个支配点的例子是x = –1.9,它给出了f1 ≈ f2 ≈ 1。这个点被x = –5 支配,因为前者的目标值低于后者:f1 < f1 和f2 < f2。

在许多情况下,我们有无限多个非支配点。图 11.4 展示了我们当前问题中的非支配点作为虚线阴影区域(我们将在本节稍后讨论如何找到这些非支配点;现在,让我们关注这些非支配点的行为):

  • 我们发现x = –5 确实是一个非支配点,以及该区域周围许多点给出了较高的第二个目标f2 的值。超出此区域的点不会产生更高的f2 值,因此该区域内的点是非支配的。我们将这些点称为group 1

  • 右侧的小区域为第一个目标f1 提供了较高的值,同样是非支配的。这些点称为group 2

  • x=4 周围还有一个第三个最小区域,它也是非支配的,在搜索空间的左侧没有被非支配点的f1 值超过。尽管这个区域不包含任何目标函数的全局最优解,但该区域在两个目标的值之间进行权衡,因此是非支配的。我们称这些点为“群组 3”。

图 11.4 两个目标函数和非支配点。在这个多目标优化问题中有无穷多个非支配点。

非支配点在多目标优化中非常有价值,因为它们本身就是优化问题的解,除非牺牲至少一个目标,否则我们无法改进它们。通过研究非支配点,它们之间的关系以及它们在搜索空间中的分布,我们可以更加了解优化问题中多个目标之间的权衡。因此,多目标优化的一个合理目标是找到尽可能多的非支配点。

然而,我们并不立即清楚如何具体量化找到非支配点的目标。我们不应该简单地寻求揭示尽可能多的非支配点,因为它们可能是无限多的。相反,我们使用一个更容易思考的量,如果我们通过将数据点可视化到一个不同的空间来观察它。

在图 11.3 和 11.4 中,x-轴对应于数据点本身,y-轴对应于这些数据点的目标值。为了研究两个冲突目标之间的权衡,我们还可以使用散点图,其中给定数据点xx-坐标是第一个目标函数f1 的值,y-坐标是第二个目标函数f2 的值。

图 11.5 展示了在-5 和 5 之间的 201 个等距点的密集网格中每个点的散点图,被支配的点用点表示,非支配点用星号表示。我们可以看出,在这个空间中,一个点是否被支配更容易确定:对于每个数据点x[1],如果存在另一个数据点x[2],它位于x[1]的上方且右侧,则x[1]是一个被支配的点;相反,如果不存在同时位于x[1]的上方且右侧的点x[2],则x[1]是非支配的。在图 11.5 中我们还可以看到三个非支配点的组,与图 11.4 相关的讨论相符。

图 11.5 基于两个目标函数值的数据点的散点图。被支配的点用点表示,非支配点用星号表示。非支配点的三个组对应于图 11.4 中的讨论。

从可视化的目标值空间中的非支配点集合中,我们现在引入另一个概念:Pareto 前沿。图 11.6 可视化了当前优化问题的 Pareto 前沿。

图片 11.6

图 11.6 通过非支配点绘制的 Pareto 前沿。没有数据点位于此 Pareto 前沿的右侧(上方和右侧)。

定义 跟踪非支配点的曲线称为 Pareto 前沿。它被称为 前沿,因为当我们将所有数据点视为一个集合时,非支配点的这条曲线构成了集合的边界或前沿,在该边界或前沿之外没有数据点。

Pareto 前沿的概念在多目标优化中至关重要,因为前沿直接导致可以量化多目标优化问题中的进展的度量标准。特别是,我们关注 Pareto 前沿覆盖的空间——由多个目标的收集目标值定义的空间——的大小;也就是说,Pareto 前沿内部(下方和左侧)的区域。该区域显示为图 11.7 的左侧面板中的阴影区域。

定义 我们使用术语 支配超体积(有时简称为 超体积)来表示 Pareto 前沿覆盖了多少空间。在我们的示例中有两个目标函数,因此空间是二维的,支配超体积是支配区域的面积。当有两个以上的目标时,支配超体积以更高维度度量相同的数量。

左侧图 11.7 中显示的分散点是使用密集的网格在搜索空间中生成的,以便我们可以详细研究 Pareto 前沿及其超体积的行为。换句话说,这个密集的网格代表了对空间的穷尽搜索,以完全绘制出 Pareto 前沿。

作为比较的一点,图 11.7 的右侧面板显示了在 -5 到 5 之间均匀选择的 20 个点的结果。从所选点中,我们再次找到在 20 个点集内没有被任何其他点支配的点,并绘制此第二个数据集的 Pareto 前沿。

图片 11.7

图 11.7 密集网格的支配超体积(左侧),等效于穷举搜索,以及随机选择的 20 个数据点(右侧)。第一个数据集具有更大的支配体积,因此在多目标优化方面的表现比第二个数据集好。

与左侧情况不同,左侧完全覆盖搜索空间,而右侧的小数据集只有四个非支配点。在多目标优化的背景下,我们使用第一个数据集(来自穷举搜索)比使用第二个数据集(来自随机搜索)取得更多进展。

与穷尽搜索得到的数据集相比,这四个非支配点构成了一个更加锯齿状的帕累托前沿,进而拥有一个更小的被支配超体积。换句话说,这个被支配超体积的度量可以用来量化多目标优化问题中的优化进展。

注意 在多目标优化问题中,我们通过当前收集的数据产生的被支配区域的超体积来衡量优化进展。我们收集的数据的被支配超体积越大,我们同时优化目标函数的进展就越大。

根据超体积度量,图 11.7 显示,穷尽搜索比随机搜索(查询更少)在优化方面做得更好,这是一个预期结果。但是要量化前者搜索策略比后者好多少,我们需要一种计算这个超体积度量的方法。对于这个计算,需要一个参考点;这个参考点充当被支配区域的终点,为该区域设置了一个左下边界。我们可以将这个参考点视为在多目标优化设置下我们能观察到的最差结果,因此该区域与帕累托前沿之间的超体积量化了我们从这个最差结果改进了多少。(如果我们不知道每个目标函数的最差结果,BayesOpt 用户可以将每个查询可以达到的最低值设定为我们认为的最低值。)

注意 在多目标优化中,一个常见的参考点是一个 数组,其中的每个元素对应于要最大化的目标函数的最低值。

例如,我们当前优化问题的参考点为[–2.0292, –0.4444],因为第一个元素–2.0292 是第一个目标函数的最小值(图 11.3 中的实线曲线),–0.4444 是第二个目标的最小值(图 11.3 中的虚线曲线)。这个参考点在图 11.8 中被可视化为星号,再次为被支配空间设定了一个下限。

图 11.8 在多目标优化问题中的参考点,它为被支配空间设定了一个下限。超体积计算为参考点与帕累托前沿之间区域的体积。

有了这个参考点,我们可以计算由多目标优化策略收集的数据集占优区域的超体积。完成这个计算的算法涉及将占优区域分成多个不相交的超矩形,这些超矩形共同构成了占优区域。从那里,我们可以容易地计算每个超矩形的超体积,并将它们相加以获得整个区域的超体积。有兴趣的读者可以参考 Renaud Lacour、Kathrin Klamroth 和 Carlos M. Fonseca 等人提出的此算法的研究论文(mng.bz/jPdp)。

使用 BoTorch,我们可以导入和运行此算法,而无需实现底层细节。具体而言,假设我们已将优化期间找到的收集标签存储在变量train_y中。因为我们的示例中有两个目标函数,所以train_y的形状应为n-by-2,其中n是收集集合中数据点的数量。然后,我们可以使用下面的代码来计算超体积度量,其中

  • DominatedPartitioning类实现了占优区域的分区。为了初始化此对象,我们传入参考点和收集标签train_y

  • 然后我们调用占优区域对象的compute_hypervolume()方法来计算其超体积度量:

from botorch.utils.multi_objective
➥.box_decompositions.dominated import                ❶
➥DominatedPartitioning                               ❶

dominated_part = DominatedPartitioning
➥(ref_point, train_y)                                ❷
volume = dominated_part.compute_hypervolume().item()  ❷

❶ 导入占优区域类的实现

❷ 计算相对于参考点的占优区域的超体积度量

使用此方法,我们可以计算完全搜索和随机搜索的超体积度量,如图 11.9 左侧和中间面板所示。我们看到,与随机搜索的超体积度量(25.72)相比,完全搜索确实实现了更高的超体积度量(31.49)。

图 11.9 各种搜索策略的多目标优化结果及相应的超体积度量。BayesOpt 几乎达到了完全搜索的超体积度量,但查询少了很多。

在图 11.9 的右侧面板中,我们还可以看到我们在下一节学习的 BayesOpt 策略仅使用了 20 个数据点就实现了相应的结果。只有预算的十分之一(20 与 201),BayesOpt 就几乎达到了完全搜索的超体积度量。与具有相同预算的随机搜索相比,BayesOpt 能够更全面地映射出真正的 Pareto 前沿,并实现更高的超体积度量。

11.3 寻求改进最佳数据边界

贝叶斯优化策略应如何最大化其收集数据中受支配区域的超体积?一个简单的策略是在迭代方式下交替优化每个目标:在贝叶斯优化循环的这一次迭代中,我们试图最大化第一个目标 f1;在下一次迭代中,我们则试图最大化第二个目标 f2;依此类推。在一个迭代中,我们有一个特定的目标要优化,我们可以通过使用我们在第 4 至 6 章学到的各种贝叶斯优化策略来实现这一点。在本章的其余部分中,我们使用了期望改进(EI),这是我们在第 4.3 节中学到的。EI 是一种在实践中常用的策略,因为它的算法简单且性能稳定。

假设在我们的多目标优化问题中,我们观察到图 11.10 顶部面板中 X 标记的数据点。通过对属于每个目标函数的数据集进行 GP 训练,我们得到了第一个目标的 GP 预测(左上角面板)和第二个目标的 GP 预测(右上角面板)。

在图 11.10 的底部面板中,我们展示了各个 EI 策略在相应目标函数上的收购分数。左下角的 EI 试图最大化第一个目标 f1,而右下角的 EI 则寻找第二个目标 f2 的最优解。我们可以看到,当第一个 EI 关注搜索空间的右侧区域,即 f1 最大化的区域时,而第二个 EI 则关注左侧区域,即 f2 最大化的区域时,两个目标之间的冲突在这里是明显的。

图 11.10:关于两个目标函数的当前 GP 信念(顶部)和相应的 EI 收购分数(底部)。每个 EI 策略都试图优化自己的目标函数,并关注不同的区域。

注意:由贝叶斯优化策略计算的数据点的收购分数 quantifies 了数据点对我们搜索目标函数最优解的价值。收购分数越高,数据点越有价值,而给出最高收购分数的点是策略建议查询的点。

在我们之前提出的交替策略中,我们要么遵循第一个 EI 策略并查询 x = 4.5 附近的点,要么遵循第二个 EI 并查询 x = –4.5 附近的点,这取决于是 f1 还是 f2 被优化的轮次。我们将这种交替策略作为基准,用来与我们最终的解决方案进行比较。

为了比简单交替不同目标函数的策略做得更好,这个解决方案应该是什么?我们注意到,通过让每个要最大化的目标都有一个 GP 模型,我们可以概率地推理出每个潜在新查询在每个目标上的值同时。具体来说,我们知道每个潜在新查询在每个目标上的价值都遵循一个已知的正态分布;这个正态分布是我们对查询价值的预测。

这个预测让我们能够推理出每个潜在新查询是否是一个无支配点,如果是的话,它将如何增加被支配区域的超体积。每个新观察到的无支配点都会延伸被支配区域的边界(即 Pareto 边界),因此增加了支配超体积。因此,我们可以使用每个新查询导致的超体积增加的期望值作为收购分数,来量化查询的价值。我们能够期望从查询获得的超体积增加越大,它对我们进行优化的帮助就越大。

当然,我们无法确定通过查询会获得多少超体积的增加,直到我们真正对目标函数进行查询。然而,我们可以通过一种概率方式来推理这个超体积增加。也就是说,我们可以计算可能查询产生的超体积增加的期望值

同样,类似于确定被支配区域的超体积的算法,这个对超体积增加的期望计算涉及将被支配区域分成超矩形,非常复杂。再一次地,我们这里不会详细介绍数学细节,但是你可以参考杨凯锋、米歇尔·埃默里奇、安德烈·德茨和托马斯·贝克提出的相应 BayesOpt 策略的研究论文,该论文称为预期超体积增加(EHVI)以获取更多详情(mng.bz/WzYw)。

定义 预期超体积增加策略使用新数据点导致的被支配区域超体积增加的期望值作为该数据点的收购分数。这个策略是将 EI 推广到了多目标设置的结果,其中我们的目标是最大化被支配超体积。

图 11.11 显示了与图 11.10 中相同数据集的 EHVI 的收购分数在底部右侧面板中。我们可以看到,与单个 EI 策略相比,EHVI 通过将高收购分数分配给可能扩展 Pareto 边界的多个区域来很好地平衡了两个目标:搜索空间的最左侧区域的得分最高,但最右侧区域以及中间的其他区域也有非常可观的收购分数。

图 11.11 当前 GP 对每个目标函数的信念(顶部)、对应的 EI 获取分数(左下角)和 EHVI 获取分数(右下角)的看法。EHVI 平衡了这两个目标,将高获取分数分配给可能延伸 Pareto 边界的多个区域。

为了验证这个 EHVI 策略确实在多目标优化中给我们带来了优势,我们实现了这个策略并在当前问题上运行它。我们使用的代码包含在 CH11/02 - Multi-objective BayesOpt loop.ipynb 笔记本中。

首先,我们需要 GP 模型的类实现和一个帮助函数 fit_gp_model(),它有助于在观察到的数据上训练每个 GP。由于我们在前几章中已经实现了这些组件,所以我们不会再次在这里展示它们的代码;您可以参考第 4.1.1 节来复习这些代码。在 BayesOpt 循环的每一步中,我们调用帮助函数在每个目标函数的数据上初始化和训练一个 GP。在我们的情况下,我们有两个目标函数,所以我们分别调用帮助函数两次,每次分别使用 train_y[:, 0](从第一个目标 f1 观察到的标签)或 train_y[:, 1](从第二个目标 f2 观察到的标签)。

model1, likelihood1 = fit_gp_model(train_x, train_y[:, 0])
model2, likelihood2 = fit_gp_model(train_x, train_y[:, 1])

然后我们使用 botorch.acquisition.multi_objective.analytic 模块中的 ExpectedHypervolumeImprovement 类来实现 EHVI 策略。为了初始化策略对象,我们设置以下参数:

  • 参数 model 接受一系列的 GPs,每个 GP 建模一个目标函数。这个 GP 列表被实现为 ModelListGP 类的一个实例,接受单独的 GP 对象 (model1, model2)。

  • 参数 ref_point 接受参考点,这对于计算 HV 和潜在 HV 增加量是必要的。

  • 最后,参数 partitioning 接受 FastNondominatedPartitioning 类的一个实例,它有助于计算 HV 增加量。这个对象的初始化与我们之前看到的 DominatedPartitioning 对象类似,接受一个参考点和观察标签 train_y

from botorch.acquisition.multi_objective
➥.analytic import                                    ❶
➥ExpectedHypervolumeImprovement                      ❶
from botorch.utils.multi_objective.box_decompositions
➥.non_dominated import                               ❶
➥FastNondominatedPartitioning                        ❶
from botorch.models.model_list_gp_regression import ModelListGP

policy = ExpectedHypervolumeImprovement(
    model=ModelListGP(model1, model2),                ❷
    ref_point=ref_point,                              ❸
    partitioning=FastNondominatedPartitioning
    ➥(ref_point, train_y)                            ❹
)

❶ 导入必要的类

❷ GP 模型列表,每个模型对应一个目标函数

❸ 参考点

❹ 无支配分区对象用于计算 HV 增加量

使用 EHVI 策略的 policy 对象,我们可以计算获取分数,表示由潜在新观测引起的预期 HV 增加量。然后我们可以使用辅助函数 optimize_acqf() 找到给出最高分数的数据点:

next_x, acq_val = optimize_acqf(
    policy,
    bounds=bounds,
    q=1,
    num_restarts=20,
    raw_samples=50
)

变量 next_x 存储我们将在下一步中使用的查询位置:next_y = joint_objective(next_x)

这就是我们在当前优化问题上运行 EHVI 所需的一切。作为参考,我们还测试了之前讨论过的交替优化策略,在这种策略中,我们使用常规的 EI 来优化选择的目标函数。由于我们有两个目标,我们只需在两个目标之间来回切换(这里的num_queries是贝叶斯优化运行中可以进行的总评估次数):

for i in range(num_queries):
    if i % 2 == 0:                        ❶
        model = model1                    ❶
        best_f = train_y[:, 0].max()      ❶
    else:                                 ❷
        model = model2                    ❷
        best_f = train_y[:, 1].max()      ❷

    policy = ExpectedImprovement(model=model,
    ➥best_f=best_f)                      ❸

❶ 如果当前迭代次数为偶数,则优化第一个目标

❷ 如果当前迭代次数为奇数,则优化第二个目标

❸ 相应地创建 EI 策略

最后,为了量化我们的优化进展,我们记录了由当前数据集在搜索过程中收集的支配区域的超体积。这个记录是用一个名为hypervolumes的张量完成的,它在实验过程中在每一步存储当前的支配超体积,跨多个实验。总的来说,我们的贝叶斯优化循环如下,对于每个策略,我们运行实验多次,每次都使用均匀随机选择的初始数据集:

hypervolumes = torch.zeros((num_repeats, num_queries))   ❶

for trial in range(num_repeats):
  torch.manual_seed(trial)                               ❷
  train_x = bounds[0] + (bounds[1] - bounds[0]) * torch  ❷
  ➥.rand(1, 1)                                          ❷
  train_y = joint_objective(train_x)                     ❷
  for i in range(num_queries):
    dominated_part = DominatedPartitioning(ref_point,
    ➥train_y)                                           ❸
    hypervolumes[trial, i] = dominated_part
    ➥.compute_hypervolume().item()                      ❸

    ...                                                  ❹

❶ 优化过程中发现的超体积历史

❷ 初始化一个随机初始训练集

❸ 记录当前的超体积

❹ 重新训练模型,初始化一个策略,并找到下一个查询

CH11/02 - 多目标贝叶斯优化循环.ipynb 笔记本对我们有两个 20 个查询的实验,每个实验都有 10 次贝叶斯优化策略进行运行。图 11.12 显示了两个策略所进行的查询次数与平均超体积和误差棒的关系。我们看到 EHVI 一直优于交替 EI 策略,这说明了基于超体积的方法的好处。

图 11.12 两个贝叶斯优化策略所进行的查询次数与平均超体积和误差棒的关系。EHVI 一直优于交替 EI 策略。

在本章中,我们学习了多目标优化问题以及如何使用贝叶斯优化方法来解决它。我们讨论了超体积的概念作为优化性能的衡量标准,量化了我们在优化目标函数方面取得的进展。通过使用 EI 策略的变体来优化超体积的增加,我们得到了一个表现强劲的 EHVI 策略。

不幸的是,本章无法涵盖多目标贝叶斯优化的其他方面。具体来说,除了 EHVI 之外,我们还可以考虑其他优化策略。一种常见的技术是标量化,它通过取加权和将多个竞争目标合并为一个目标。该策略是交替 EI 策略的一般化,我们可以认为在每次迭代中将一个目标的权重设置为 1,另一个目标的权重设置为 0。感兴趣的读者可以参考 BoTorch 文档(请参阅botorch.org/docs/multi_objectivebotorch.org/tutorials/multi_objective_bo),该文档提供了 BoTorch 提供的不同多目标优化策略的简要摘要。

11.4 练习:飞机设计的多目标优化

在这个练习中,我们将所学的多目标优化技术应用于优化飞机的航空结构设计问题。这个问题首次在第七章的练习 2 中介绍,并在第八章的练习 2 中修改为成本约束问题。我们在这里重用第八章的代码。这个练习使我们能够观察到期望超体积改进(EHVI)策略在多维问题中的性能。解决方案包含在 CH11/03 - Exercise 1.ipynb 笔记本中。

执行以下步骤:

  1. 从第八章的练习 2 中复制目标函数flight_utility()flight_cost()的代码。取反第二个函数flight_cost()返回值的符号。我们将这两个函数用作多目标优化问题的目标。

  2. 编写一个辅助函数,该函数接受一个输入X(可能包含多个数据点),并返回在两个目标函数上评估的X的值。返回的值应该是一个大小为n-by-2 的张量,其中nX中数据点的数量。

  3. 声明搜索空间为四维单位正方形。即,四个下限为 0,四个上限为 1。

  4. 要计算由优化算法收集的数据集的超体积,我们需要一个参考点。声明此参考点为[–1.5, –2],这是两个目标函数的对应最低值。

  5. 实现 GP 模型的类,该类应具有常数均值和一个四维 Matérn 2.5 核,并具有自动相关性确定(ARD;参见 3.4.2 节),以及一个辅助函数fit_gp_model(),该函数在训练集上初始化和训练 GP 模型。有关实现这些组件的详细信息,请参见 4.1.1 节。

  6. 将要运行的实验次数设置为 10,每次实验的预算(要进行的查询数量)设置为 50。

  7. 运行 EHVI 策略来优化我们拥有的两个目标函数,以及第 11.3 节中讨论的交替 EI 策略。绘制这两个策略所实现的平均超体积和误差条形图(类似于图 11.2),并比较它们的性能。

总结

  • 当存在多个潜在的冲突目标需要同时优化时,多目标优化问题就会出现。这个问题在现实世界中很常见,因为我们经常在许多真实任务中与多个竞争目标相争。

  • 在使用 BayesOpt 进行多目标优化时,我们使用多个高斯过程来模拟我们对目标函数的信念(每个目标函数使用一个模型)。我们可以使用这些高斯过程以概率方式同时推理目标函数。

  • 非支配点实现的目标值不能得到改善,除非我们牺牲至少一个目标函数的性能。发现非支配数据点是多目标优化的目标,因为它们允许我们研究目标函数之间的权衡。

  • 无支配数据点构成帕累托前沿,它设置了代表多目标优化中最优的边界。没有数据点位于所有非支配点的帕累托前沿之外。

  • 支配空间的超体积(即由帕累托前沿覆盖的区域)测量算法收集的数据集的优化性能。超体积越大,算法的性能就越好。可以通过在 BoTorch 的"DominatedPartitioning"类的实例上调用compute_hypervolume()方法来计算数据集的超体积。

  • 要计算数据集的超体积,我们需要一个作为支配空间结束点的参考点。我们通常将参考点设置为要优化的目标函数的最低值。

  • 由于高斯过程允许我们对目标函数进行预测,因此我们可以寻求改进当前数据集的超体积。这种策略对应于 EHVI 策略,是多目标优化中 EI 的一种变体。这种策略成功平衡了竞争性目标。

第四部分:特殊高斯过程模型

高斯过程(GPs),在 BayesOpt 的背景之外,本身就是一类强大的 ML 模型。虽然本书的主要主题是 BayesOpt,但如果不多关注 GPs,那就是一个错失的机会。这一部分向我们展示了如何扩展 GPs,并使它们在各种 ML 任务中更实用,同时保留了它们最有价值的特性:对预测中不确定性的量化。

第十二章中,我们学习如何加速训练高斯过程(GPs)并将其扩展到大数据集。这一章帮助我们解决了 GPs 的一个最大劣势:它们的训练成本。

第十三章展示了如何通过将 GPs 与神经网络结合,将 GP 的灵活性提升到另一个水平。这种组合提供了两全其美的好处:神经网络近似任何函数的能力和 GPs 对不确定性的量化。这一章还让我们真正欣赏到了在 PyTorch、GPyTorch 和 BoTorch 中拥有一个简化的软件生态系统的好处,这使得同时使用神经网络和 GPs 变得无缝。

第十三章:将高斯过程扩展到大型数据集

本章介绍:

  • 训练大型数据集上的 GP

  • 在训练 GP 时使用小批量梯度下降。

  • 采用高级梯度下降技术来更快地训练 GP。

到目前为止,我们已经看到 GP 提供了极高的建模灵活性。在第三章中,我们学习了如何使用 GP 的均值函数来模拟高级别趋势,并使用协方差函数来模拟变异性。GP 还提供了校准的不确定性量化。也就是说,训练数据集中接近观测值的数据点的预测比远离观测值的点的预测具有更低的不确定性。这种灵活性使 GP 与其他只产生点估计(如神经网络)的 ML 模型区别开来。然而,它也导致了速度问题。

训练和预测 GP(具体来说,计算协方差矩阵的逆)与训练数据的规模呈立方级扩展关系。也就是说,如果我们的数据集大小翻倍,GP 将需要花费八倍的时间进行训练和预测。如果数据集增加十倍,GP 将需要花费 1,000 倍的时间。这给将 GP 扩展到大型数据集的应用带来了挑战。

  • 如果我们的目标是对整个国家(例如美国)的房价进行建模,其中每个数据点表示给定时间的单个住宅的价格,则我们的数据集大小将包含数亿个点。例如,在线数据库 Statista 记录了自 1975 年至 2021 年美国住房单位数量的变化;该报告可在 www.statista.com/statistics/240267/number-of-housing-units-in-the-united-states/ 上访问。我们可以看到,自 1975 年以来,这个数字一直在稳步增长,1990 年超过了 1 亿,现在已经超过 1.4 亿。

  • 在我们在第 1.1.3 节中讨论的药物发现应用程序中,一个可能被合成为药物的可能分子的数据库可能会拥有数十亿个条目。

  • 在天气预报中,低成本的监测设备使得大规模收集天气数据变得容易。数据集可以包含跨多年的每分钟测量结果。

考虑到正常 GP 模型的立方运行时间,将其应用于这种规模的数据集是不可行的。在本章中,我们将学习如何利用一类称为“变分高斯过程”(VGPs)的 GP 模型来解决从大型数据中学习的问题。

定义变分高斯过程选择一小部分数据,很好地表示整个集合。它通过寻求最小化它本身和完整数据集上训练的普通 GP 之间的差异来实现这一点。术语“变分”是指研究函数式优化的数学子领域。

选择仅对这些代表性点的小型子集进行训练的想法是相当自然和直观的。图 12.1 展示了 VGP 的运行情况,通过从少数精选数据点中学习,该模型产生了几乎与普通 GP 产生的预测相同的预测。

图 12.1 显示了普通 GP 和 VGP 的预测。VGP 产生了几乎与 GP 相同的预测,同时训练时间显著缩短。

我们在本章介绍如何实现这个模型,并观察其在计算上的优势。此外,当使用 VGP 时,我们可以使用更高级的梯度下降版本,正如我们在第 3.3.2 节中看到的,它用于优化 GP 的超参数。我们学会使用这个算法的版本来更快、更有效地训练,并最终将我们的 GPs 扩展到大型数据集。本章附带的代码可以在 CH11/01 - 近似高斯过程推理.ipynb 笔记本中找到。

12.1 在大型数据集上训练高斯过程模型

在本节中,为了直观地看到在大型数据集上训练高斯过程模型面临的困难挑战,我们试图将我们在第二章和第三章中使用的 GP 模型应用于一个包含 1,000 个点的中型数据集。这个任务将清楚地表明使用普通 GP 是不可行的,并激发我们在下一节学习的内容:变分 GPs。

12.1.1 设置学习任务

在这个小节中,我们首先创建我们的数据集。我们重新使用了在第二章和第三章中看到的一维目标函数,即 Forrester 函数。我们再次按照以下方式实现它:

def forrester_1d(x):
    y = -((x + 1) ** 2) * torch.sin(2 * x + 2) / 5 + 1
    return y.squeeze(-1)

类似于我们在第 3.3 节中所做的,我们还将拥有一个辅助函数,该函数接收一个 GP 模型,并在整个域上可视化其预测。该函数具有以下头部,并接受三个参数——GP 模型、相应的似然函数和一个布尔标志,表示模型是否为 VGP:

此辅助函数的逻辑概述在图 12.2 中草绘出来,它包括四个主要步骤:计算预测、绘制真实函数和训练数据、绘制预测,最后,如果模型是 VGP,则绘制诱导点。

图 12.2 是可视化 GP 预测的辅助函数的流程图。该函数还显示了 VGP 的诱导点(如果传入的是该模型)。

定义:诱导点是 VGP 模型选择的表示整个数据集的小型子集,用于训练。顾名思义,这些点旨在“诱导”关于所有数据的知识。

现在我们更详细地介绍这些步骤。在第一步中,我们使用 GP 计算均值和 CI 预测:

with torch.no_grad():
    predictive_distribution = likelihood(model(xs))
    predictive_mean = predictive_distribution.mean
    predictive_upper, predictive_lower =
    ➥predictive_distribution.confidence_region()

在第二步中,我们制作 Matplotlib 图并显示存储在 xsys 中的真实函数(我们稍后生成)以及我们的训练数据 train_xtrain_y

plt.figure(figsize=(8, 6))

plt.plot(xs, ys, label="objective", c="r")  ❶
plt.scatter(                                ❷
    train_x,                                ❷
    train_y,                                ❷
    marker="x",                             ❷
    c="k",                                  ❷
    alpha=0.1 if variational else 1,        ❷
    label="observations",                   ❷
  )                                         ❷

❶ 绘制真实目标函数

❷ 为训练数据制作散点图

在这里,如果模型是一个 VGP(如果 variational 设置为 True),那么我们会用较低的不透明度绘制训练数据(通过设置 alpha = 0.1),使它们看起来更透明。这样我们可以更清楚地绘制后面学习到的 VGP 的代表性点。

GP 所做的预测随后在第三步中以实线均值线和阴影 95% CI 区域的形式显示:

plt.plot(xs, predictive_mean, label="mean")
plt.fill_between(
    xs.flatten(),
    predictive_upper,
    predictive_lower,
    alpha=0.3,
    label="95% CI"
)

最后,我们通过提取 model.variational_strategy.inducing_points 来绘制 VGP 选择的代表性点:

if variational:
  inducing_points =
  ➥model.variational_strategy.inducing_points.detach().clone()
  with torch.no_grad():
      inducing_mean = model(inducing_points).mean

  plt.scatter(
      inducing_points.squeeze(-1),
      inducing_mean,
      marker="D",
      c="orange",
      s=100,
      label="inducing pts"
  )                         ❶

❶ 散布感应点

现在,为了生成我们的训练和数据集,我们在-5 和 5 之间随机选择了 1,000 个点,并计算了这些点的函数值:

torch.manual_seed(0)
train_x = torch.rand(size=(1000, 1)) * 10 - 5
train_y = forrester_1d(train_x)

为了生成我们的测试集,我们使用 torch.linspace() 函数在-7.5 和 7.5 之间计算了一个密集的网格。该测试集包括-7.5、7.4、-7.3 等,直到 7.5:

xs = torch.linspace(-7.5, 7.5, 151).unsqueeze(1)
ys = forrester_1d(xs)

要可视化我们的训练集的样子,我们可以再次制作一个散点图如下:

plt.figure(figsize=(8, 6))
plt.scatter(
    train_x,
    train_y,
    c="k",
    marker="x",
    s=10,
    label="observations"
)
plt.legend();

这段代码产生了图 12.3,其中黑点表示我们训练集中的个别数据点。

图 12.3 我们学习任务的训练数据集,包含 1,000 个数据点。在这个集合上训练一个常规的 GP 需要相当长的时间。

12.1.2 训练常规 GP

我们现在准备在这个数据集上实现并训练一个 GP 模型。首先,我们实现 GP 模型类,其具有常数函数(gpytorch.means .ConstantMean 的一个实例)作为其均值函数,以及具有输出尺度的 RBF 核函数(使用 gpytorch.kernels.ScaleKernel(gpytorch.kernels.RBFKernel()) 实现)作为其协方差函数:

class GPModel(gpytorch.models.ExactGP):
    def __init__(self, train_x, train_y, likelihood):
        super().__init__(train_x, train_y, likelihood)
        self.mean_module = gpytorch.means.
        ➥ConstantMean()                                  ❶
        self.covar_module = gpytorch.kernels.
        ➥ScaleKernel(                                    ❷
            gpytorch.kernels.RBFKernel()                  ❷
        )                                                 ❷

    def forward(self, x):                                 ❸
        mean_x = self.mean_module(x)                      ❸
        covar_x = self.covar_module(x)                    ❸
        return gpytorch.distributions.MultivariateNormal  ❸
        ➥(mean_x, covar_x)                               ❸

❶ 常数均值函数

❷ 具有输出尺度的 RBF 核函数

❸ 创建 MVN 分布作为预测

现在,我们使用我们的训练数据和一个 GaussianLikelihood 对象初始化了这个 GP 模型:

likelihood = gpytorch.likelihoods.GaussianLikelihood()
model = GPModel(train_x, train_y, likelihood)

最后,我们通过运行梯度下降来训练我们的 GP,以最小化由数据的可能性定义的损失函数。在训练结束时,我们得到了模型的超参数(例如,均值常数、长度尺度和输出尺度),这些超参数给出了一个较低的损失值。梯度下降是使用 Adam 优化器(torch .optim.Adam)实现的,这是最常用的梯度下降算法之一:

optimizer = torch.optim.Adam(model.parameters(), lr=0.01)          ❶
mll = gpytorch.mlls.ExactMarginalLogLikelihood(likelihood, model)  ❷

model.train()                                                      ❸
likelihood.train()                                                 ❸

for i in tqdm(range(500)):                                         ❹
    optimizer.zero_grad()                                          ❹

    output = model(train_x)                                        ❹
    loss = -mll(output, train_y)                                   ❹

    loss.backward()                                                ❹
    optimizer.step()                                               ❹

model.eval()                                                       ❺
likelihood.eval()                                                  ❺

❶ 梯度下降算法 Adam

❷ 损失函数,计算由超参数确定的数据的可能性

❸ 启用训练模式

❹ 运行 500 次梯度下降迭代

❺ 启用预测模式

注意:作为提醒,当训练 GP 时,我们需要为模型和可能性都启用训练模式(使用 model.train()likelihood .train())。在训练后和进行预测之前,我们需要启用预测模式(使用 model.eval()likelihood.eval())。

使用 GPU 训练 GPs

一个将 GP 扩展到大型数据集的方法,本章不重点讨论,是使用图形处理单元(GPU)。GPU 通常用于并行化矩阵乘法并加速训练神经网络。

同样的原则也适用于此处,而 GPyTorch 通过遵循 PyTorch 的语法将 GP 训练简化到了 GPU 上(通过在对象上调用 cuda() 方法)。具体来说,我们调用 train_x = train_x.cuda()train_y = train_y.cuda() 将我们的数据放到 GPU 上,然后调用 model = model.cuda()likelihood = likelihood.cuda() 将 GP 模型和其似然放到 GPU 上。

您可以在 GPyTorch 的文档中找到有关此主题的更多详细信息,链接地址为 mng.bz/lW8B

我们还对梯度下降进行了 500 次迭代,但由于我们当前的数据集明显更大,这个循环可能需要一段时间才能完成(所以在等待时来杯咖啡吧!)。训练完成后,我们调用了之前编写的 visualize_gp_belief() 辅助函数来显示我们训练好的 GP 所做的预测,生成了图 12.4:

visualize_gp_belief(model, likelihood)

图 12.4 由普通 GP 进行的预测。预测很好地匹配了训练数据,但训练时间很长。

我们看到我们的 GP 的预测很好地匹配了训练数据点——这是一个鼓舞人心的迹象,表明我们的模型成功地从数据中学习了。然而,这个过程中存在一些问题。

12.1.3 普通 GP 训练中的问题

在这一小节中,我们讨论了在大型数据集上训练 GP 面临的一些挑战。首先,正如我们之前提到的,训练需要相当长的时间。在我的 MacBook 上,500 次梯度下降可能需要长达 45 秒的时间,这明显比我们在第二章和第三章观察到的情况要长。这直接是我们之前提到的 GP 的立方运行时间的结果,随着数据集的不断增大,这种长时间的训练只会变得更加禁锢,正如表 12.1 所示。

表 12.1 给定训练数据集大小的 GP 预计训练时间。训练很快变得困难。

训练集大小 训练时间
500 个点 45 秒
2,000 个点 48 分钟
3,000 个点 2.7 小时
5,000 个点 12.5 小时
10,000 个点 4 天

第二个,也许更令人担忧的问题源于这样一个事实,即随着训练数据的规模增加,计算损失函数(用于梯度下降的训练数据的边际对数似然)变得越来越困难。这可以通过 GPyTorch 在训练过程中打印的警告信息来看出:

NumericalWarning: CG terminated in 1000 iterations with average
  residual norm...

这些信息告诉我们,在计算损失时遇到了数值不稳定性。

注意,计算许多数据点之间的损失是一个计算上不稳定的操作。

数值不稳定性阻止我们正确计算损失,因此无法有效地最小化该损失。这在梯度下降的 500 次迭代中损失的变化中得到了说明,如图 12.5 所示。

图 12.5 在梯度下降过程中普通高斯过程的逐渐损失。由于数值不稳定性,损失曲线崎岖不平,无法有效地最小化。

与第二章和第三章中所见不同,我们这里的损失上下波动,表明梯度下降未能很好地最小化该损失。事实上,随着我们进行更多的迭代,我们的损失实际上增加了,这意味着我们得到了一个次优模型!这种现象是可以理解的:如果我们误计算了模型的损失,那么通过使用该误计算项来指导梯度下降中的学习,我们可能得到一个次优解。

你可能对梯度下降类比为下山的比喻很熟悉。假设你在山顶,想下山。沿途的每一步,你找到一个朝向能让你到达更低处的方向(即下降)。最终,经过足够的步骤,你抵达山脚。类似地,在梯度下降中,我们从一个相对较高的损失开始,并在每次迭代中调整我们模型的超参数,逐步降低损失。经过足够的迭代,我们到达最佳模型。

注 涵盖了对梯度下降以及它如何类比为下山的出色讨论,可参考路易斯·塞拉诺的《深入理解机器学习》。

只有在我们能够准确计算损失时,这个过程才能正常运行,也就是说,我们可以清楚地看到哪个方向会让我们到达山上的更低处。然而,如果这个计算容易出错,我们自然无法有效地最小化模型的损失。这就好比戴着眼罩下山一样!正如我们在图 12.5 中看到的,我们实际上停留在山上的更高处(我们的损失高于梯度下降之前的值)。

图 12.6 使用数值不稳定的损失计算运行梯度下降类似于戴着眼罩下山。

总的来说,在大型数据集上训练常规高斯过程并不是一个好的方法。训练不仅随着训练数据规模的立方级增长,而且损失值的计算也不稳定。在本章的其余部分,我们将了解到变分高斯过程或 VGPs 是解决这个问题的方案。

12.2 从大型数据集中自动选择代表性点

VGPs 的思想是选择一组代表整个数据集的点,并在这个较小的子集上训练 GP。我们已经学会了如何在小数据集上训练 GP。希望这个较小的子集能捕捉到整个数据集的一般趋势,这样当在子集上训练 GP 时,仅有最少的信息会丢失。

这个方法非常自然。大数据集通常包含冗余信息,如果我们只从最信息丰富的数据点中学习,就可以避免处理这些冗余性。我们在 2.2 节中指出,像任何 ML 模型一样,GP 工作的假设是相似的数据点会产生相似的标签。当大数据集包含许多相似的数据点时,GP 只需要关注其中一个数据点来学习其趋势。例如,即使有按分钟的天气数据可用,天气预报模型也可以从仅有的小时测量中有效地进行学习。在这个小节中,我们将学习如何通过确保从小子集中学习相对于从大集合中学习时信息损失最小的方式来自动实现这一点,以及如何使用 GPyTorch 实现这个模型。

12.2.1 最小化两个 GP 之间的差异

我们如何选择这个小子集,使得最终的 GP 模型能够从原始数据集中获得最多的信息。在这个小节中,我们讨论了如何通过 VGP 来实现这一高级想法。这个过程等同于找到诱导点的子集,当在这个子集上训练 GP 时,诱导出的后验 GP 应该尽可能接近在整个数据集上训练的后验 GP。

当训练 VGP 时,深入一些数学细节,我们的目标是最小化在诱导点上条件化的后验 GP 和在整个数据集上条件化的后验 GP 之间的差异。这需要一种方法来衡量两个分布(两个 GP)之间的差异,而为此选择的衡量标准是 Kullback-Leibler(KL)散度。

定义Kullback-Leibler(KL)散度是一种统计距离,用于衡量两个分布之间的距离,也就是 KL 散度计算概率分布与另一个分布之间的不同程度。

KL 散度的补充材料

Will Kurt 的博客文章“Kullback-Leibler Divergence Explained”中提供了 KL 散度的直观解释(www.countbayesie.com/blog/2017/5/9/kullback-leibler-divergence-explained)。有数学背景的读者可以参考 David MacKay 的《信息论、推理和学习算法》第二章(剑桥大学出版社,2003 年)。

正如欧几里得空间中点 A 和点 B 之间的欧几里得距离(即连接两点的线段的长度)衡量了这两点在欧几里得空间中的距离一样,KL 散度衡量了概率分布空间中两个给定分布之间的距离,即它们彼此之间的差异有多大。这在图 12.7 中有所说明。

图 12.7 欧几里得距离衡量了平面上两点之间的距离,而 KL 散度衡量了两个概率分布之间的距离。

注 作为一个数学上有效的距离度量,KL 散度是非负的。换句话说,任意两个分布之间的距离至少为零,当距离等于零时,两个分布完全匹配。

因此,如果我们能够轻松计算在诱导点上训练的后验 GP 与整个数据集上训练的后验 GP 之间的 KL 散度,我们应该选择使得 KL 散度为零的诱导点。不幸的是,类似于计算边际对数似然的计算不稳定性,计算 KL 散度也不容易。然而,由于其数学特性,我们可以将 KL 散度重写为两个量之间的差异,如图 12.8 所示。

图 12.8 KL 散度被分解为边际对数似然和证据下界(ELBO)之间的差异。ELBO 易于计算,因此被选择为要优化的度量。

这个方程中的第三项,也就是证据下界(ELBO),是边际对数似然和 KL 散度之间的精确差异。尽管边际对数似然和 KL 散度这两项很难计算,但 ELBO 具有简单的形式并且可以轻松计算。因此,我们可以最大化 ELBO 来间接最大化边际对数似然,而不是最小化 KL 散度,使得在诱导点上训练的后验 GP 尽可能接近在完整数据集上训练的后验 GP。

综上所述,为了找到一组诱导点,使得后验 GP 尽可能与我们能够在大数据集上训练时获得的 GP 相似,我们的目标是最小化两个 GP 之间的 KL 散度。然而,这个 KL 散度很难计算,所以我们选择优化 KL 散度的代理,即模型的 ELBO,这样更容易计算。正如我们在下一小节中看到的,GPyTorch 提供了一个方便的损失函数,用于计算这个 ELBO 项。在我们讨论实现之前,还有一件事情需要讨论:在最大化 ELBO 项时如何考虑大型训练集中的所有数据点。

12.2.2 在小批量中训练模型

由于我们的目标是找到最能代表整个训练数据集的一组感兴趣的点,我们仍然需要在计算 ELBO 时包含训练集中的所有点。但是我们之前说过,跨多个数据点计算边际对数似然是数值不稳定的,因此梯度下降变得无效。我们在这里面对相同的问题吗?在本小节中,我们看到当通过优化 ELBO 项来训练 VGP 时,我们可以通过使用更适合大型数据集的梯度下降的修改版本来避免这个数值不稳定性问题。

在许多数据点上计算 ML 模型的损失函数的任务并不是 GP 独有的。例如,神经网络通常在数千和数百万的数据点上进行训练,计算网络的损失函数对于所有数据点也是不可行的。对于这个问题的解决方法,对于神经网络和 VGP 都是近似通过对随机点的损失值计算跨所有数据点的真实损失值。例如,以下代码片段来自官方 PyTorch 文档,并显示了如何在图像数据集上训练神经网络(mng.bz/8rBB)。在这里,内部循环迭代训练数据的小子集,并在这些子集上计算的损失值上运行梯度下降:

for epoch in range(2):                        ❶

    running_loss = 0.0
    for i, data in enumerate(trainloader, 0):
        inputs, labels = data                 ❷

        optimizer.zero_grad()                 ❸

        outputs = net(inputs)                 ❹
        loss = criterion(outputs, labels)     ❹
        loss.backward()                       ❹
        optimizer.step()                      ❹

❶ 对数据集进行多次循环

❷ 获取输入;数据是一个 [输入,标签] 的列表

❸ 归零参数梯度

❹ 前向 + 反向 + 优化

当我们在少量点上计算模型的损失时,计算可以稳定有效地进行。此外,通过多次重复这个近似,我们可以很好地近似真实损失。最后,我们在这个近似损失上运行梯度下降,希望也最小化所有数据点上的真实损失。

定义 在使用数据的随机子集计算的损失上运行梯度下降的技术有时被称为小批量梯度下降。在实践中,我们通常不会在梯度下降的每次迭代中随机选择一个子集,而是将训练集分成小的子集,并通过每个这些小的子集迭代计算近似损失。

例如,如果我们的训练集包含 1,000 个点,我们可以将其分成 10 个小子集,每个子集包含 100 个点。然后,我们对每个包含 100 个点的子集计算梯度下降的损失,并迭代重复所有 10 个子集。(这恰好是我们后面代码示例中所做的。)同样,虽然从数据子集计算的这种近似损失并不完全等于真实损失,但在梯度下降中,我们重复这个近似很多次,这在聚合中指引我们朝着正确的下降方向。

图 12.9 中说明了梯度下降最小化真实损失和小批量梯度下降最小化近似损失之间的区别。与梯度下降相比(再次强调,无法在大数据上运行),小批量版本可能不会指向最有效的下降方向,但通过多次重复近似,我们仍然能够达到目标。

图 12.9 梯度下降和小批量梯度下降在损失“谷底”中的示意图,其中谷底的中心给出最低的损失。梯度下降,如果可以计算,直接导向目标。小批量梯度下降朝着不是最优的方向前进,但最终仍然到达目标。

如果我们用盲目下山的比喻来思考,小批量梯度下降类似于戴着一块可以部分看穿的薄布的盲目。并不能保证我们每迈出一步就到达一个更低的位置,但是给定足够的时间,我们将能够成功下降。

注意并非所有的损失函数都可以通过对数据子集的损失来近似。换句话说,并非所有的损失函数都可以通过小批量梯度下降来最小化。GP 的负边际对数似然就是一个例子;否则,我们可以在这个函数上运行小批量梯度下降。幸运的是,小批量梯度下降适用于 VGP 的 ELBO。

总之,训练一个 VGP 遵循大致相似的程序,就像训练一个常规的 GP 一样,我们使用梯度下降的一个版本来最小化模型的适当损失。表 12.2 总结了两个模型类之间的关键差异:常规 GP 应该通过运行梯度下降来最小化精确的负边际对数似然在小数据集上训练,而 VGP 可以通过运行小批量梯度下降来优化 ELBO,在大数据集上训练,这是真实对数似然的一个近似。

表 12.2 训练一个 GP 与训练一个 VGP。高层次的过程是相似的;只有具体的组件和设置被替换。

训练过程 GP VGP
训练数据大小 中等到大
训练类型 精确训练 近似训练
损失函数 负边际对数似然 ELBO
优化 梯度下降 小批量梯度下降

12.2.3 实现近似模型

我们现在准备在 GPyTorch 中实现一个 VGP。我们的计划是编写一个 VGP 模型类,这类似于我们已经使用的 GP 模型类,并使用小批量梯度下降最小化其 ELBO。我们在表 12.2 中描述的工作流程的不同之处反映在了本小节中的代码中。表 12.3 显示了在 GPyTorch 中实现 GP 与 VGP 时所需的组件。除了均值和协方差模块外,VGP 还需要另外两个组件:

  • 变分分布定义了 VGP 中诱导点的分布。正如我们在上一节中学到的,此分布要进行优化,以使 VGP 类似于在完整数据集上训练的 GP。

  • 变分策略定义了如何从诱导点产生预测。在第 2.2 节中,我们看到多元正态分布可以根据观测结果进行更新。这种变分策略促使对变分分布进行相同的更新。

表 12.3 在 GPyTorch 中实现 GP 与 VGP 时所需的组件。VGP 需要像 GP 一样的均值和协方差模块,但还需要变分分布和变分策略。

组件 GP VGP
均值模块
协方差模块
变分分布
变分策略

考虑到这些组件,我们现在实现了 VGP 模型类,我们将其命名为 ApproximateGPModel我们不再在 __init__() 方法中接受训练数据和似然函数。相反,我们接受一组诱导点,这些点将用于表示整个数据集。 __init__() 方法的其余部分包括声明将用于学习哪组诱导点最佳的学习流程:

  • 变分分布 variational_distribution 变量是 CholeskyVariationalDistribution 类的一个实例,在初始化期间接受诱导点的数量。 变分分布是 VGP 的核心。

  • 变分策略 variational_strategy 变量是 VariationalStrategy 类的一个实例。它接受一组诱导点以及变分分布。我们将 learn_inducing_locations = True 以便在训练过程中学习这些诱导点的最佳位置。如果将此变量设置为 False则传递给 __init__() 的点(存储在 inducing 中)将用作诱导点:

class ApproximateGPModel(gpytorch.models.ApproximateGP):             ❶
  def __init__(self, inducing_points):                               ❷
    variational_distribution =                                       ❸
    ➥gpytorch.variational.CholeskyVariationalDistribution(          ❸
        inducing_points.size(0)                                      ❸
    )                                                                ❸
    variational_strategy = gpytorch.variational.VariationalStrategy( ❸
        self,                                                        ❸
        inducing_points,                                             ❸
        variational_distribution,                                    ❸
        learn_inducing_locations=True,                               ❸
    )                                                                ❸
    super().__init__(variational_strategy)                           ❸

    ...                                                              ❹

我们的 VGP 不是一个 ExactGP 对象,而是一个近似 GP。

接受一组初始诱导点

设置训练所需的变分参数

待续

__init__() 方法的最后一步中,我们声明了 VGP 的均值和协方差函数。它们应该与在数据上训练的普通 GP 中要使用的函数相同。在我们的情况下,我们使用常数均值和具有输出比例的 RBF 核:

class ApproximateGPModel(gpytorch.models.ApproximateGP):
    def __init__(self, inducing_points):
        ...

        self.mean_module = gpytorch.means.ConstantMean()
        self.covar_module = gpytorch.kernels.ScaleKernel(
            gpytorch.kernels.RBFKernel()
        )

我们还以与常规 GP 相同的方式声明 forward() 方法,这里不再展示。现在,让我们用训练集中的前 50 个数据点来初始化这个模型作为引入点:

model = ApproximateGPMod1el(train_x[:50, :])            ❶
likelihood = gpytorch.likelihoods.GaussianLikelihood()

❶ 切片张量 train_x[:50, :] 给出了 train_x 中的前 50 个数据点。

前 50 个数据点没有什么特别之处,它们的值,在 VGP 模型内部存储,将在训练过程中被修改。这种初始化最重要的部分是,我们指定模型应该使用 50 个引入点。如果我们想要使用 100 个,我们可以将 train_x[:100, :] 传递给初始化。

很难准确地说,多少个引入点足以用于 VGP。我们使用的点越少,模型训练就越快,但是这些引入点在表示整个集合时的效果就越不好。随着点数的增加,VGP 有更多的自由度来展开引入点以覆盖整个集合,但训练会变慢。

一般规则是不超过 1,000 个引入点。正如我们马上要讨论的,50 个点足以让我们以高保真度近似前一小节中训练的 GP。

要设置小批量梯度下降,我们首先需要一个优化器。我们再次使用 Adam 优化器:

optimizer = torch.optim.Adam(
    [
        {"params": model.parameters()},
        {"params": likelihood.parameters()}    ❶
    ],
    lr=0.01
)

❶ 优化似然函数的参数以及 GP 的参数

要优化的参数

之前,我们只需要将 model.parameters() 传递给 Adam。在这里,似然函数没有与 VGP 模型耦合——常规 GP 使用似然函数初始化,而 VGP 则没有。因此,在这种情况下,有必要将 likelihood.parameters() 传递给 Adam。

对于损失函数,我们使用 gpytorch.mlls.VariationalELBO 类,该类实现了我们希望通过 VGP 优化的 ELBO 量。在初始化期间,此类的一个实例接受似然函数、VGP 模型和完整训练集的大小(我们可以通过 train_y.size(0) 访问)。有了这些,我们声明这个对象如下:

mll = gpytorch.mlls.VariationalELBO(
    likelihood,
    model,
    num_data=train_y.size(0)    ❶
)

❶ 训练数据的大小

有了模型、优化器和损失函数设置好了,我们现在需要运行小批量梯度下降。为此,我们将训练数据集分成批次,每个批次包含 100 个点,使用 PyTorch 的 TensorDatasetDataLoader 类:

train_dataset = torch.utils.data.TensorDataset(train_x, train_y)
train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=100)

这个 train_loader 对象允许我们以干净的方式迭代大小为 100 的数据集的小批量,当运行梯度下降时。损失—即 ELBO—通过以下语法计算:

output = model(x_batch)
loss = -mll(output, y_batch)

这里,x_batchy_batch 是完整训练集的一个给定批次(小子集)。总的来说,梯度下降的实现如下:

model.train()                                 ❶
likelihood.train()                            ❶

for i in tqdm(range(50)):                     ❷
    for x_batch, y_batch in train_loader:     ❸
        optimizer.zero_grad()                 ❹

        output = model(x_batch)               ❹
        loss = -mll(output, y_batch)          ❹

        loss.backward()                       ❹
        optimizer.step()                      ❹

model.eval()                                  ❺
likelihood.eval()                             ❺

❶ 启用训练模式

❷ 迭代整个训练数据集 50 次

❸ 在每次迭代中,迭代 train_loader 中的小批次

❹ 小批量梯度下降,在批次上运行梯度下降

❺ 启用预测模式

在运行这个小批量梯度下降循环时,你会注意到它比使用普通 GP 的循环要快得多。(在同一台 MacBook 上,这个过程只需不到一秒钟,速度大幅提升!)

VGP 的速度

你可能认为我们在普通 GP 中进行的 500 次迭代与 VGP 中进行的 50 次小批量梯度下降的比较不公平。但是请记住,在小批量梯度下降的外部for循环的每次迭代中,我们还要在train_loader中迭代 10 个小批量,所以最终总共进行了 500 次梯度步骤。此外,即使我们运行了 500 次小批量梯度下降的迭代,也只需要不到 1 秒乘以 10,仍然比 45 秒快 4 倍。

因此,通过小批量梯度下降,我们的 VGP 模型可以更高效地训练。但是对于训练质量如何呢?图 12.10 的左侧面板可视化了我们小批量梯度下降运行期间的逐渐 ELBO 损失。与图 12.5 相比,尽管损失并没有在每一步时持续减小(存在锯齿趋势),但损失在整个过程中有效地最小化了。

图 12-10.png

图 12.10 VGP 在小批量梯度下降期间逐步损失服从的长度尺度和输出尺度的对应关系

这表明,在优化过程中的每一步可能不是最小化损失的最佳方向,但小批量梯度下降确实是有效地减小了损失。这在图 12.11 中更清楚地显示出来。

图 12-11.png

图 12.11 过程中 VGP 的逐步损失在小批量梯度下降中逐渐减小。尽管存在一定的变异性,但损失有效地最小化了。

现在,让我们可视化这个 VGP 模型产生的预测结果,看看它是否产生了合理的结果。使用visualize_gp_belief()助手函数,我们得到图 12.12,显示我们以较小的时间成本获得了对真实损失进行训练的 GP 的高质量近似。

图 12-12.png

图 12.12 由 GP 和 VGP 进行的预测。VGP 进行的预测与 GP 的预测大致相同。

结束我们对 VGP 的讨论,让我们可视化 VGP 模型学到的诱导点的位置。我们已经说过这些诱导点应该代表整个数据集并很好地捕捉其趋势。要绘制诱导点,我们可以使用model.variational_strategy.inducing_points.detach()访问它们的位置,并将它们作为散点沿着均值预测进行绘制。当调用这个函数时,我们只需要将variational设置为True

visualize_gp_belief(model, likelihood, variational=True)

这产生了图 12.13,在其中我们看到这些感应点的非常有趣的行为。它们并不均匀分布在我们的训练数据之间;相反,它们聚集在数据的不同部分。这些部分是目标函数上升或下降或呈现某些非平凡行为的地方。通过将感应点分配到这些位置,VGP 能够捕捉嵌入大型训练数据集中最重要的趋势。

图 12.13 VGP 的感应点。这些点被放置在整个数据中,捕捉最重要的趋势。

我们已经学习了如何使用小批量梯度下降来训练 VGP,并且已经看到这有助于以更低的成本近似于不可训练的常规 GP。在接下来的部分,我们将学习另一种梯度下降算法,可以更有效地训练 VGP。

12.3 通过考虑损失表面的几何特性来进行更好的优化

在本节中,我们将学习一种名为自然梯度下降的算法,这是梯度下降的另一种版本,在计算下降步骤时更仔细地推理损失函数的几何结构。正如我们很快会看到的那样,这种谨慎的推理使我们能够快速降低损失函数,最终导致更有效的优化,迭代次数更少(也就是更快的收敛)。

要理解自然梯度下降的动机以及为什么它比我们已经拥有的更好,我们首先区分 VGP 的两种参数类型:

  • 第一种类型是 GP 的常规参数,例如均值常数和协方差函数的长度和输出比例。这些参数取得常规的数值,存在于欧几里得空间中。

  • 第二种类型包括只有 VGP 才有的变分参数。这些与感应点和促使变分分布近似所需的各种组件有关。换句话说,这些参数与概率分布相关,并且具有无法在欧几里得空间内很好表示的值。

注意:这两种参数之间的差异有些相似,尽管不完全类似于欧几里得距离可以衡量该空间中两点之间的距离,但无法衡量两个概率分布之间的差异。

尽管我们在前一节中使用的小批量梯度下降效果已经足够好,但该算法假设所有参数存在于欧几里得空间中。例如,从算法的角度来看,长度尺度从 1 变到 2 的差异与诱导点的均值从 1 变到 2 的差异是相同的。然而,事实并非如此:从长度尺度从 1 变到 2 会对 VGP 模型产生非常不同的影响,而从诱导点的均值从 1 变到 2 的影响也不同。这在图 12.14 的示例中有所体现,其中损失对长度尺度的行为与对诱导均值的行为非常不同。

图 12.14 一个示例,说明了要最小化的损失可能在正常参数和变分参数方面的行为非常不同。这就需要考虑损失的几何形状。

正常参数和 VGP 的变分参数的损失函数的几何形状之间存在这种行为差异是因为。如果小批量梯度下降能够在计算损失的下降方向时考虑到这种几何差异,那么该算法在最小化损失时将更有效。这就是自然梯度下降的作用所在。

自然梯度下降 的定义利用了关于损失函数对于变分参数的几何特性的信息,以计算这些参数更好的下降方向。

通过采用更好的下降方向,自然梯度下降可以更有效地帮助我们优化 VGP 模型,而且更快。最终的结果是我们能够在更少的步骤中收敛到我们的最终模型。继续我们对不同梯度下降算法的二维图示,图 12.15 展示了这种几何推理如何帮助自然梯度下降比小批量梯度下降更快地达到目标。也就是说,自然梯度下降在训练过程中往往需要更少的步骤来达到与小批量梯度下降相同的损失。在我们下山的类比中,使用自然梯度下降时,我们在试图下山时仍然被一块薄布蒙住了眼睛,但现在我们穿着特制的登山鞋,可以更有效地穿越地形。

图 12.15 展示了在损失“谷底”中进行梯度下降、小批量梯度下降和自然梯度下降的插图,在谷底的中心给出了最低的损失。通过考虑损失函数的几何特性,自然梯度下降比小批量梯度下降更快地达到了损失最小值。

自然梯度下降的补充材料

对于更加数学化的自然梯度下降解释,我推荐阅读 Agustinus Kristiadi 的优秀博文“自然梯度下降”:mng.bz/EQAj

注意 需要注意的是,自然梯度下降算法仅优化 VGP 的变分参数。常规参数,例如长度和输出尺度,仍然可以通过常规小批量梯度下降算法优化。在接下来实现新的训练过程时,我们将看到这一点。

因此,让我们使用自然梯度下降来训练我们的 VGP 模型。与之前部分的相同一维目标函数一样,我们实现了一个能够与自然梯度下降一起工作的 VGP 模型。这种情况下的模型类似于我们在上一节为小批量梯度下降实现的ApproximateGPModel,它

  • 仍然扩展gpytorch.models.ApproximateGP

  • 需要一个变分策略来管理学习过程

  • 具有类似常规 GP 模型的均值函数、协方差函数和forward()方法

这里唯一的区别是,当训练模型时,变分分布需要是gpytorch.variational.NaturalVariationalDistribution的一个实例,以便我们在使用自然梯度下降时进行训练。整个模型类实现如下:

class NaturalGradientGPModel(gpytorch.models.ApproximateGP):
  def __init__(self, inducing_points):
    variational_distribution =                         ❶
      gpytorch.variational.                            ❶
      ➥NaturalVariationalDistribution(                ❶
        inducing_points.size(0)                        ❶
    )                                                  ❶

    variational_strategy = gpytorch.variational.
    ➥VariationalStrategy(                             ❷
        self,                                          ❷
        inducing_points,                               ❷
        variational_distribution,                      ❷
        learn_inducing_locations=True,                 ❷
    )                                                  ❷
    super().__init__(variational_strategy)             ❷
    self.mean_module = gpytorch.means.ConstantMean()   ❷
    self.covar_module = gpytorch.kernels.ScaleKernel(  ❷
        gpytorch.kernels.RBFKernel()                   ❷
    )                                                  ❷
  def forward(self, x):
        ...                                            ❸

❶ 变分分布需要是自然的才能与自然梯度下降一起工作。

❷ 声明剩余的变分策略与以前相同。

forward()方法与以前相同。

我们再次使用 50 个引导点初始化此 VGP 模型:

model = NaturalGradientGPModel(train_x[:50, :])         ❶
likelihood = gpytorch.likelihoods.GaussianLikelihood()

❶ 50 个引导点

现在是重要的部分,我们为训练声明优化器。请记住,我们使用自然梯度下降算法来优化模型的变分参数。然而,其他参数,例如长度和输出尺度,仍然必须由 Adam 优化器优化。因此,我们使用以下代码:

ngd_optimizer = gpytorch.optim.NGD(                  ❶
  model.variational_parameters(), num_data=train_y.  ❶
  ➥size(0), lr=0.1                                  ❶
)                                                    ❶

hyperparam_optimizer = torch.optim.Adam(             ❷
  [{"params": model.parameters()}, {"params":        ❷
  ➥likelihood.parameters()}],                       ❷
  lr=0.01                                            ❷
)                                                    ❷
mll = gpytorch.mlls.VariationalELBO(
  likelihood, model, num_data=train_y.size(0)
)

❶ 自然梯度下降接受 VGP 的变分参数,model.variational_parameters()

❷ Adam 接受 VGP 的其他参数,model.parameters()likelihood.parameters()

现在,在训练期间,我们仍然使用以下方式计算损失

output = model(x_batch)
loss = -mll(output, y_batch)

在计算损失时,我们循环遍历我们训练数据的小批量(x_batchy_batch)。然而,现在我们有两个优化器同时运行,我们需要通过在每次训练迭代时调用zero_grad()(清除前一步的梯度)和step()(执行一步下降)来管理它们:

model.train()                              ❶
likelihood.train()                         

for i in tqdm(range(50)):
    for x_batch, y_batch in train_loader:
        ngd_optimizer.zero_grad()          ❷
        hyperparam_optimizer.zero_grad()   ❷

        output = model(x_batch)
        loss = -mll(output, y_batch)

        loss.backward()

        ngd_optimizer.step()               ❸
        hyperparam_optimizer.step()        ❸

model.eval()                               ❹
likelihood.eval()                          ❹

❶ 启用训练模式

❷ 清除前一步的梯度

❸ 使用每个优化器执行下降步骤

❹ 启用预测模式

注意 像往常一样,在梯度下降之前,我们需要调用model.train()likelihood.train(),在训练完成后需要调用model.eval()likelihood.eval()

请注意,我们在自然梯度下降优化器和 Adam 上都调用了 zero_grad()step(),以优化 VGP 模型的相应参数。训练循环再次只需很短的时间即可完成,并且训练得到的 VGP 产生的预测结果如图 12.16 所示。我们看到的预测结果与图 12.4 中的普通 GP 和使用小批量梯度下降训练的 VGP 在图 12.13 中的预测结果非常相似。

图 12.16 由 VGP 和通过自然梯度下降训练的感应点所做出的预测。这些预测的质量很高。

我们可以进一步检查训练过程中 ELBO 损失的逐步进展。其进展在图 12.17 的左侧面板中可视化。

图 12.17 VGP 在自然梯度下降期间的逐步损失。经过少数迭代后,损失得到了有效最小化。

令人惊讶的是,在训练过程中,我们的 ELBO 损失几乎立即降至一个较低的值,这表明自然梯度下降能够帮助我们快速收敛到一个良好的模型。这说明了当训练 VGP 时,这种梯度下降算法的变体的好处。

我们现在已经到达第十二章的结尾。在本章中,我们学习了如何通过使用感应点将 GP 模型扩展到大型数据集,感应点是一组代表性点,旨在捕获大型训练集所展现的趋势。由此产生的模型称为 VGP,它适用于小批量梯度下降,因此可以在不计算所有数据点的模型损失的情况下进行训练。我们还研究了自然梯度下降作为小批量算法的更有效版本,使我们能够更有效地优化。在第十三章中,我们将涵盖 GP 的另一种高级用法,将其与神经网络结合以建模复杂的结构化数据。

12.4 练习

此练习演示了在加利福尼亚房价的真实数据集上,从普通 GP 模型转换为 VGP 模型时效率的提高。我们的目标是观察 VGP 在实际环境中的计算优势。

完成以下步骤:

  1. 使用 Pandas 库中的 read_csv() 函数读取存储在名为 data/housing.csv 的电子表格中的数据集,该数据集来自 Kaggle 的加利福尼亚房价数据集(mng.bz/N2Q7),使用的是创意共用公共领域许可证。一旦读取,Pandas dataframe 应该看起来类似于图 12.18 中的输出。

    图 12.18 作为 Pandas dataframe 显示的房价数据集。这是本练习的训练集。

  2. 在散点图中可视化median_house_value列,这是我们的预测目标,其x-和y-轴对应于longitudelatitude列。一个点的位置对应于一个房子的位置,点的颜色对应于价格。可视化效果应该类似于图 12.19。

    图 12.19 房价数据集的散点图表示

  3. 提取除最后一列(median_house_value)以外的所有列,并将它们存储为 PyTorch 张量。这将用作我们的训练特征,train_x

  4. 提取median_house_value列,并将其对数变换存储为另一个 PyTorch 张量。这是我们的训练目标,train_y

  5. 通过减去均值并除以标准差来归一化训练标签train_y。这将使训练更加稳定。

  6. 使用具有恒定均值函数和具有自动相关性确定(ARD)的 Matérn 5/2 核实现常规 GP 模型。关于 Matérn 核和 ARD 的详细内容,请参阅第 3.4.2 和第 3.4.3 节。

  7. 使用以下代码创建一个噪声至少为 0.1 的似然性:

    likelihood = gpytorch.likelihoods.GaussianLikelihood(
        noise_constraint=gpytorch.constraints.GreaterThan(1e-1)  ❶
    )
    

    ❶该约束强制噪声至少为 0.1。

    此约束有助于通过提高噪声容限来平滑训练标签。

  8. 初始化之前实现的 GP 模型,并使用梯度下降对其进行 10 次迭代训练。观察总训练时间。

  9. 实现具有与 GP 相同的均值和协方差函数的变分 GP 模型。这个模型看起来类似于我们在本章中实现的ApproximateGPModel类,只是现在我们需要 Matérn 5/2 核和 ARD。

  10. 使用类似初始化的似然性和 100 个诱导点对此 VGP 进行训练,使用自然梯度下降进行 10 次迭代。对于小批量梯度下降,您可以将训练集分成大小为 100 的批次。

  11. 验证训练 VGP 所需的时间是否少于训练 GP 的时间。对于计时功能,您可以使用time.time()来记录每个模型训练的开始和结束时间,或者您可以使用tqdm库来跟踪训练持续时间,就像我们一直在使用的代码一样。

解决方案包含在CH11/02 - Exercise.ipynb笔记本中。

摘要

  • GP 的计算成本与训练数据集的大小呈立方比例。因此,随着数据集的大小增长,训练模型变得不可行。

  • 在大量数据点上计算 ML 模型的损失在数值上是不稳定的。以不稳定的方式计算损失可能会误导梯度下降过程中的优化,导致预测性能不佳。

  • VGP 通过仅对一小组诱导点进行训练来扩展到大型数据集。这些诱导点需要代表数据集,以便训练的模型尽可能地与在整个数据集上训练的 GP 相似。

  • 为了产生一个尽可能接近在所有训练数据上训练的模型的近似模型,Kullback-Leibler 散度,它度量两个概率分布之间的差异,被用于 VGP 的制定中。

  • 当训练一个 VGP 时,证据下界(ELBO)充当真实损失的代理。更具体地说,ELBO 限制了模型的边际对数似然,这是我们的优化目标。通过优化 ELBO,我们间接优化了边际对数似然。

  • 训练一个 VGP 可以通过小批量进行,从而实现更稳定的损失计算。在该过程中使用的梯度下降算法是小批量梯度下降。

  • 尽管小批量梯度下降的每一步都不能保证完全最小化损失,但是当运行大量迭代时,该算法可以有效降低损失。这是因为许多小批量梯度下降的步骤在聚合时可以指向最小化损失的正确方向。

  • 自然梯度下降考虑了相对于 VGP 的变分参数的损失函数的几何性质。这种几何推理使得算法能够更新训练模型的变分参数并更有效地最小化损失,从而实现更快的收敛。

  • 自然梯度下降优化了 VGP 的变分参数。诸如长度和输出比例等常规参数由小批量梯度下降进行优化。

第十四章:将高斯过程与神经网络结合

本章涵盖

  • 使用常见协方差函数处理复杂结构化数据的困难

  • 使用神经网络处理复杂结构化数据

  • 将神经网络与 GP 结合

在第二章中,我们学到了高斯过程(GP)的均值和协方差函数作为我们希望在模型中融入的先验信息,当进行预测时。因此,这些函数的选择极大地影响了训练后的 GP 的行为。因此,如果均值和协方差函数被错误地指定或不适用于手头的任务,那么得到的预测就不会有用。

例如,记住covariance function,或kernel,表示两个点之间的相关性——即相似性。两个点越相似,它们的标签值可能越相似,我们试图预测的标签值。在我们的房价预测示例中,相似的房子可能会有类似的价格。

核到底如何计算两个给定房子之间的相似性?我们考虑两种情况。在第一种情况中,核函数仅考虑前门的颜色,并对于任何具有相同门颜色的两个房子输出 1,否则输出 0。换句话说,如果两个房子的前门颜色相同,这个核函数认为它们相似。

正如图 13.1 所示,这个核函数对于房价预测模型来说是一个糟糕的选择。核函数认为左边的房子和中间的房子应该有相似的价格,而右边的房子和中间的房子应该有不同的价格。这是不合适的,因为左边的房子比其他两个大得多,而其他两个房子的大小相似。误差发生的原因是核函数错误地判断了房子的哪个特征是房子成本的良好预测特征。

图 13.1 由不合适的核函数计算的房屋之间的协方差。因为它只关注前门的颜色,所以这个核函数无法产生合适的协方差。

另一个核函数更加复杂,并考虑了相关因素,比如位置和居住面积。这个核函数更加合适,因为它可以更合理地描述两个房子之间的价格相似性。拥有合适的核函数——即正确的相似度度量——对于 GP 来说至关重要。如果核函数能够正确地描述给定数据点之间的相似性或差异性,那么使用协方差的 GP 将能够产生良好校准的预测。否则,预测将具有较低的质量。

你可能会认为一个只考虑门颜色的房屋核函数是不合适的,并且在 ML 中没有合理的核函数会表现出这种行为。然而,正如我们在本章中所展示的,到目前为止我们使用的一些常见核函数(例如,RBF 和 Matérn)在处理结构化输入数据(例如图像)时会出现相同的问题。具体来说,它们未能充分描述两个图像之间的相似性,这给在这些结构化数据类型上训练 GPs 带来了挑战。我们采取的方法是使用神经网络。神经网络是灵活的模型,可以在有足够数据的情况下很好地逼近任何函数。我们学会使用神经网络来转换 GP 的核函数无法很好地处理的输入数据。通过这样做,我们既得到了神经网络的灵活建模,从 GP 中获得了不确定性校准的预测。

在本章中,我们展示了我们通常的 RBF 核函数不能很好地捕捉常见数据集的结构,从而导致 GP 的预测不佳。然后我们将一个神经网络模型与此 GP 结合起来,看到新的核函数可以成功地推理相似性。到本章结束时,我们获得了一个框架,帮助 GP 处理结构化数据类型并提高预测性能。

13.1 包含结构的数据

在本节中,我们解释了结构化数据的确切含义。与我们在之前章节中用来训练 GPs 的数据类型不同,在那些数据类型中,数据集中的每个特征(列)可以取得连续范围内的值,而在许多应用中,数据具有更复杂性。比如说:

  • 房子的楼层数只能是正整数。

  • 在计算机视觉任务中,图像中的像素值是 0 到 255 之间的整数。

  • 在分子 ML 中,分子通常被表示为图形。

那就是,这些应用中的数据点中嵌入了结构,或者需要数据点遵循的要求:没有房子可以有负数的楼层;像素不能以分数作为其值;表示分子的图形将具有表示化学物质和结合物的节点和边缘。我们称这些类型的数据为结构化数据。在本章中,我们将使用流行的 MNIST 手写数字数据集(见huggingface.co/datasets/mnist)作为我们讨论的案例研究。

定义 修改后的美国国家标准与技术研究所(MNIST)数据集包含手写数字的图像。每个图像是一个 28×28 的整数矩阵,取值范围在 0 到 255 之间。

这个数据集中的一个示例数据点如图 13.2 所示,其中像素的阴影对应于其值;0 对应于白色像素,255 对应于黑色像素。我们看到这个数据点是数字五的图像。

图 13.2 来自 MNIST 数据集的数据点,这是一个由 28 行和 28 列像素组成的图像,表示为一个 PyTorch 张量

注意 虽然这个手写数字识别任务在技术上是一个分类问题,但我们使用它来模拟一个回归问题(这是我们在 BayesOpt 中要解决的问题类型)。由于每个标签都是一个数字(一个数字),我们假装这些标签存在于一个连续的范围内,并直接将它们用作我们的预测目标。

我们的任务是在一个图像标签数据集上训练一个 GP(每个标签都是对应图像中写的数字的值),然后在一个测试集上进行预测。这个区别在图 13.3 中有所体现,它显示与分类不同,在分类中,我们选择一个类作为每个数据点的预测,而在回归任务中,这里的每个预测是一个连续范围内的数字。

图 13.3 在 MNIST 数据的上下文中的分类与回归。每个预测是一个分类任务,对应于其中的一个类别;在回归中的每个预测是一个连续范围内的数字。

有许多现实世界的应用遵循这种结构化数据的回归问题的形式:

  • 在产品推荐中,我们希望预测某人点击自定义广告的概率。广告是可以自定义的图片,是结构化数据,点击概率是预测目标。这个概率可以是 0 到 1 之间的任何数字。

  • 在材料科学中,科学家可能希望在实验室中合成某种分子组合时预测其能量水平。每种分子组合都可以表示为具有节点和边的特定结构的图,并且能量水平可以是理论最小和最大能量水平之间的任何数字,一个组合可能表现出的。

  • 在药物发现中,我们希望预测可能产生的药物的有效性。如图 13.4 所示,每种药物对应于一种化合物,它也可以表示为一个图。其有效性可以是一个实数,介于某个范围内(比如从 0 到 10)。

图 13.4 药物发现作为一个结构化回归问题的例子。每个化合物都表示为一个结构化图,并且我们的目标是预测这种化合物在治疗某种疾病方面的有效性,其范围从 0 到 10。

在所有这些应用中,我们想要进行预测的输入数据是结构化的,我们的预测目标是一个实数值。简而言之,它们是结构化数据的回归问题。使用 MNIST 数据集,我们模拟了这样一个问题。

13.2 捕捉结构化数据内的相似性

在本节中,我们将探讨常见内核(如径向基函数内核)如何无法描述结构化数据中的相似性。量化两个输入的协方差的内核输出,x[1] 和 x[2] 的输出定义如下:

这个输出是两个变量之间的协方差,是两个输入之间差异的负指数除以一个标度。输出始终在 0 和 1 之间,而且越大的差异,输出越小。

这在许多情况下是有道理的,因为如果两个输入具有类似的值,因此差异很小,则它们的协方差将很高;而如果它们具有不同的值,则协方差将很低。两栋面积大致相等的房屋可能会有类似的价格,也就是说,它们的价格具有高的协方差;另一方面,非常大和非常小的房子的价格可能会具有较低的协方差。

13.2.1 使用 GPyTorch 的内核

让我们用代码验证一下。当我们创建 GP 模型时,通常会初始化一个 RBFKernel 对象。这里,我们直接使用这个内核对象进行工作。为此,我们首先使用 GPyTorch 创建一个 RBF 内核对象:

import gpytorch

rbf_kernel = gpytorch.kernels.RBFKernel()

请注意,作为 Python 中实现 GP 相关对象的首选库,我们一如既往地使用 GPyTorch。有关如何在 GPyTorch 中使用内核对象的详细信息,请参见第 2.4 节。

要计算两个输入之间的协方差,我们只需将它们传递给该内核对象即可。例如,让我们计算 0 和 0.1 之间的协方差:

>>> rbf_kernel(torch.tensor([0.]), torch.tensor([0.1])).evaluate().item()
0.9896470904350281

这两个数字在实数线上非常接近(也就是说,它们是相似的),因此它们的协方差非常高,几乎为 1。现在让我们计算 0 和 10 之间的协方差,这是两个不同的数字:

>>> rbf_kernel(torch.tensor([0.]), torch.tensor([10.])).evaluate().item()
0.0

这次,由于两个数字之间的差异要大得多,它们的协方差降为 0。这种对比是合理的行为,并且通过图 13.5 进行说明。

图 13.5 各种数字之间的协方差。两个数字之间的差异较小时,协方差增加;差异较大时,协方差降低。

当两个输入之间的值差异不能捕捉到数据结构差异时,问题就出现了。这通常是对结构化数据(如图像)的情况。接下来,我们将看到像径向基函数(RBF)这样的常见内核如何无法描述结构化数据中的相似性。

13.2.2 在 PyTorch 中处理图像

在这一小节中,我们将看到如何将图像导入和存储为 PyTorch 张量,以及如何处理此类数据时,基于值的相似度度量(如 RBF 内核)如何失效。首先,我们重新定义我们的 RBF 内核,使其具有较大的长度尺度,因此更有可能出现较高的协方差:

rbf_kernel = gpytorch.kernels.RBFKernel()
rbf_kernel.lengthscale = 100               ❶

❶ 长度尺度越大,协方差越高。

现在,我们需要将 MNIST 数据集中的图像导入到我们的 Python 代码中。我们可以使用 PyTorch 及其流行的附加库 torchvision 来实现:

import torch
from torchvision import datasets, transforms

transform = transforms.Compose([                               ❶
    transforms.ToTensor(),                                     ❶
    transforms.Normalize((0.1307,), (0.3081,))                 ❶
])                                                             ❶

dataset = datasets.MNIST(                                      ❷
    "../data", train=True, download=True, transform=transform  ❷
)                                                              ❷

train_x = dataset.data.view(-1, 28 * 28)                       ❸

❶ 定义规范化像素值的转换

❷ 下载并导入数据集

❸ 提取像素值作为一个扁平化的张量

我们不会深入研究这段代码,因为它不是我们讨论的重点。我们只需要知道 train_x 包含 MNIST 数据集中的图像,每个图像都存储为一个 PyTorch 张量,其中包含表示手写数字图像的像素值。

由于数据点是图像,我们可以将它们可视化为热图,使用 Matplotlib 中熟悉的 imshow() 函数。例如,以下代码可视化了 train_x 中的第一个数据点:

plt.figure(figsize=(8, 8))

plt.imshow(train_x[0, :].view(28, 28));    ❶

❶ 每个图像有 28 行和 28 列的像素,因此我们需要将其重塑为一个 28×28 的方形张量。

这段代码生成了图 13.2,我们看到它是数字 5 的图像。当我们打印出这个第一个数据点的实际值时,我们看到它是一个 28 × 28 = 784 元素的 PyTorch 张量:

>>> train_x[0, :]
tensor([ 0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
         0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
         0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
         0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
         0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
         0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
         0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
         0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
         0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
         0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
         0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   3,  18,
        18,  18, 126, 136, 175,  26, 166, 255, 247, 127,   0,   0,   0,   0,
         0,   0,   0,   0,   0,   0,   0,   0,  30,  36,  94, 154, 170, 253,
       253, 253, 253, 253, 225, 172, 253, 242, 195,  64,   0,   0,   0,   0,
         0,   0,   0,   0,   0,   0,   0,  49, 238, 253, 253, 253, 253, 253,
       253, 253, 253, 251,  93,  82,  82,  56,  39,   0,   0,   0,   0,   0,
         0,   0,   0,   0,   0,   0,   0,  18, 219, 253, 253, 253, 253, 253,
       198, 182, 247, 241,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
         0,   0,   0,   0,   0,   0,   0,   0,  80, 156, 107, 253, 253, 205,
        11,   0,  43, 154,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
         0,   0,   0,   0,   0,   0,   0,   0,   0,  14,   1, 154, 253,  90,
[output truncated]

此张量中的每个元素范围在 0 到 255 之间,表示我们在图 13.2 中看到的像素。值为 0 对应于最低信号,即图中的背景,而较高的值对应于亮点。

13.2.3 计算两个图像的协方差

这就是我们探索普通 GP 核处理结构化数据时所需要的所有背景信息。为了突出问题,我们单独提出了三个特定的数据点,分别称为点 A、点 B 和点 C,它们的索引如下:

ind1 = 304    ❶
ind2 = 786    ❷
ind3 = 4      ❸

❶ 点 A

❷ 点 B

❸ 点 C

在检查这些图像实际显示的数字之前,让我们使用我们的 RBF 核来计算它们的协方差矩阵:

>>> rbf_kernel(train_x[[ind1, ind2, ind3], :]).evaluate()
tensor([[1.0000e+00, 4.9937e-25, 0.0000e+00],
        [4.9937e-25, 1.0000e+00, 0.0000e+00],
        [0.0000e+00, 0.0000e+00, 1.0000e+00]], ...)

这是一个 3×3 协方差矩阵,具有熟悉的结构:对角线元素取值为 1,表示各个变量的方差,而非对角线元素表示不同的协方差。我们看到点 A 和 C 完全不相关,协方差为零,而点 A 和 B 稍微相关。根据 RBF 核,点 A 和 B 相似,并且与点 C 完全不同。

我们应该期望点 A 和 B 具有相同的标签。然而,事实并非如此!再次将这些数据点可视化为热图,我们得到图 13.6。

图 13.6 MNIST 数据集中的三个特定数据点。第一个和第二个点具有非零协方差,尽管标签不同。第一个和第三个点具有零协方差,尽管标签相同。

在这里,点 A 和 C 共享相同的标签(数字 9)。那么为什么 RBF 核会认为点 A 和 B 有相关性呢?看图 13.6,我们可以猜测,虽然点 A 和 B 有不同的标签,但图像本身在很多像素上是相似的。事实上,构成数字尾巴的笔画在这两幅图像中几乎完全相同。因此,在某种程度上,RBF 核正在做它的工作,根据这种差异计算图像之间的差异并输出代表它们协方差的数字。然而,这种差异是通过比较像素本身来计算的,这不是我们试图学习的指标:数字的值。

通过仅仅查看像素值,RBF 核高估了点 A 和 B 之间的协方差,这两个点具有不同的标签,并低估了点 A 和 C 之间的协方差,它们具有相同的标签,正如图 13.7 所示。这里可以使用类比来演示我们在本章开头提到的不恰当的 house 核:这个核只看前门的颜色来决定两个房屋是否相关,导致对它们价格的不准确预测。类似地(但不如此极端),RBF 核在比较两幅图像时只考虑像素的值,而不考虑更高级别的模式,这导致了较差的预测性能。

图 13.7 由 RBF 核计算的手写数字之间的协方差。因为它只看像素值,所以 RBF 核无法产生适当的协方差。

13.2.4 在图像数据上训练 GP

通过使用错误的相似度度量标准,RBF 混淆了点 B 和 C 中哪个与点 A 相关联,这导致在训练 GP 时产生了不良结果。我们再次使用 MNIST 数据集,这次提取 1,000 个数据点作为训练集,另外 500 个数据点作为测试集。我们的数据准备和学习工作流程总结在图 13.8 中,我们将详细介绍其中的不同步骤。

图 13.8 在 MNIST 上进行 GP 学习案例的流程图。我们提取 1,000 个数据点作为训练集,另外 500 个数据点作为测试集。

首先,我们导入 PyTorch 和 torchvision——后者是 PyTorch 的一个扩展,管理与计算机视觉相关的功能和数据集,如 MNIST。从 torchvision 中,我们导入模块 datasetstransforms,它们帮助我们下载和操作 MNIST 数据,分别是:

import torch
from torchvision import datasets, transforms

在第二个数据准备步骤中,我们再次使用将图像转换为 PyTorch 张量的对象(这是 GPyTorch 中实现的 GP 可以处理的数据结构)并规范化像素值。此规范化通过将像素值减去 0.1307(数据的平均值)并将值除以 0.3081(数据的标准差)来完成。这种规范化被认为是 MNIST 数据集的常见做法,有关此步骤的更多详细信息可以在 PyTorch 的官方论坛讨论中找到(mng.bz/BmBr):

transform = transforms.Compose([
    transforms.ToTensor(),                       ❶
    transforms.Normalize((0.1307,), (0.3081,))   ❷
])

❶ 将数据转换为 PyTorch 张量

❷ 规范化张量

存储在 transform 中的此转换对象现在可以传递给对任何 torchvision 数据集初始化的调用,并且将应用转换(转换为 PyTorch 张量和规范化)到我们的数据上。我们使用此转换对象初始化 MNIST 数据集如下。请注意,我们创建数据集两次,一次将 train 设置为 True 以创建训练集,另一次将 train 设置为 False 以创建测试集:

dataset1 = datasets.MNIST(                                       ❶
    "../data", train=True, download=True, transform=transform    ❶
)                                                                ❶

dataset2 = datasets.MNIST(                                       ❷
    "../data", train=False, download=True, transform=transform   ❷
)                                                                ❷

❶ 下载并导入训练集

❷ 下载并导入测试集

作为数据准备的最后一步,我们从训练集中提取前 1,000 个数据点和测试集中的 500 个点。我们通过从数据集对象 dataset1dataset2 中访问来实现这一点:

  • 使用 data 属性获取特征,即构成每个数据点图像的像素值

  • 使用 targets 属性获取标签,即手写数字的值:

train_x = dataset1.data[:1000, ...].view(1000, -1)
➥.to(torch.float)                   ❶
train_y = dataset1.targets[:1000]    ❶

test_x = dataset2.data[:500, ...].view(500, -1)
➥.to(torch.float)                   ❷
test_y = dataset2.targets[:500]      ❷

❶ 获取训练集中的前 1,000 个点

❷ 获取测试集中的前 500 个点

我们还实现了一个简单的 GP 模型,具有恒定均值和带有输出比例的 RBF 核:

class GPModel(gpytorch.models.ExactGP):
    def __init__(self, train_x, train_y, likelihood):
        super().__init__(train_x, train_y, likelihood)
        self.mean_module = gpytorch.means.ConstantMean()                  ❶
        self.covar_module = gpytorch.kernels.ScaleKernel(                 ❷
            gpytorch.kernels.RBFKernel()                                  ❷
        )                                                                 ❷

    def forward(self, x):                                                 ❸
        mean_x = self.mean_module(x)                                      ❸
        covar_x = self.covar_module(x)                                    ❸
        return gpytorch.distributions.MultivariateNormal(mean_x, covar_x) ❸

❶ 一个恒定均值函数

❷ 具有输出比例的 RBF 协方差函数

❸ 以输入 x 的预测制作 MVN 分布

注意 GPyTorch GP 模型的 forward() 方法首次讨论于第 2.4 节。

然后,我们初始化我们的 GP 并在 1,000 个点的训练集上进行训练,使用 Adam 优化器的梯度下降。此代码将优化 GP 的超参数值(例如,均值常量和长度和输出比例),以便我们获得观察到的数据的高边际似然度:

likelihood = gpytorch.likelihoods.GaussianLikelihood()     ❶
model = GPModel(train_x, train_y, likelihood)              ❶

optimizer = torch.optim.Adam(model.parameters(), lr=0.01)  ❷
mll = gpytorch.mlls.ExactMarginalLogLikelihood             ❷
➥(likelihood, model)                                      ❷

model.train()                                              ❸
likelihood.train()                                         ❸

for i in tqdm(range(500)):                                 ❹
    optimizer.zero_grad()                                  ❹

    output = model(train_x)                                ❹
    loss = -mll(output, train_y)                           ❹

    loss.backward()                                        ❹
    optimizer.step()                                       ❹

model.eval()                                               ❺
likelihood.eval()                                          ❺

❶ 声明似然函数和 GP 模型

❷ 声明梯度下降算法和损失函数

❸ 启用训练模式

❹ 运行五百次梯度下降迭代

❺ 启用预测模式

注意 参考第 2.3.2 节,了解梯度下降如何优化我们观察到的数据的似然度,即梯度下降如何训练 GP。

最后,为了查看我们的模型在测试集上的表现如何,我们计算 GP 预测值与地面实况(每个数据点的标签值)之间的平均绝对差异。这个指标通常被称为 平均绝对误差

注意 MNIST 数据集的典型指标是该模型正确预测测试集的百分比(即准确度),这是分类问题的规范。由于我们使用这个数据集来模拟一个回归问题,因此均方误差是合适的。

这是通过将均值预测与存储在test_y中的真实标签进行比较来完成的:

with torch.no_grad():
    mean_preds = model(test_x).mean

print(torch.mean(torch.abs(mean_preds - test_y)))

Output: 2.7021167278289795

这个输出意味着,平均而言,高斯过程对图像中描绘的数字的值的预测误差几乎达到了 3。考虑到这项任务只有 10 个值需要学习,这个表现相当差。这个结果强调了常规高斯过程模型处理结构化数据(如图像)的无能。

13.3 使用神经网络处理复杂的结构化数据

我们所看到的高斯过程表现较差的根本原因是核不具备处理输入数据的复杂结构的装备,从而导致协方差计算不良。特别是,径向基核具有一个简单的形式,只考虑两个输入之间的数字值的差异。在本节中,我们学习如何通过使用神经网络处理结构化数据,然后将处理后的数据馈给高斯过程的均值函数和核来解决这个问题。

13.3.1 为什么使用神经网络进行建模?

我们在本书开始时指出,神经网络在进行昂贵的数据获取时,特别是在进行不确定性校准的预测方面表现不佳。(这是为什么 BayesOpt 中使用高斯过程的全部原因。)然而,神经网络擅长学习复杂结构。这种灵活性是由于神经网络中有多个计算层(具体来说,是矩阵乘法),如图 13.9 所示。

图 13.9 神经网络是一组层计算的集合。通过将多个计算层链接在一起,神经网络可以很好地模拟复杂函数。

在神经网络中,每个层都对应于一个矩阵乘法,其输出然后经过非线性激活函数处理。通过在一次前向传递中将多个这样的层链在一起,可以以灵活的方式处理和操作网络的输入。最终结果是神经网络可以很好地模拟复杂函数。有关神经网络及其用法的详细解释,请参阅 François Chollet 的优秀著作Deep Learning with Python, Second Edition(Manning, 2021)。

神经网络具有的灵活性可以帮助我们解决上一节中描述的问题。如果高斯过程的核,如径向基核,不能很好地处理复杂的数据结构,我们可以让神经网络来处理这项工作,并将处理后的输入仅馈送给高斯过程的核。这个过程在图 13.10 中进行了可视化,其中输入的x首先通过神经网络层,然后再传递给高斯过程的均值函数和核。

13-10.png

图 13.10 结合了神经网络和 GP。 神经网络首先处理结构化数据输入x,然后将输出馈送到 GP 的均值函数和核。

尽管最终结果仍然是一个 MVN 分布,但均值函数和核函数的输入现在是由神经网络产生的处理过的输入。 这种方法很有前途,因为它具有灵活的建模能力,神经网络将能够从结构化输入数据中提取重要特征(在提供相似性计算信息方面很重要)并将其化简为适合 GP 核的数值。

定义 神经网络通常被称为组合模型的特征提取器,因为网络从结构化数据中提取有利于 GP 建模的特征。

通过这种方式,我们可以利用神经网络的灵活学习能力,同时保持使用 GP 进行不确定性校准预测的能力。 这是两全其美! 此外,训练这个组合模型的过程与训练常规 GP 的过程相同:我们定义我们的损失函数,即负对数似然,然后使用梯度下降来找到最能解释我们的数据的超参数值(通过最小化损失)。 现在,我们不仅优化均值常数、长度和输出比例,还要额外优化神经网络的权重。 在下一小节中,我们将看到,使用 GPyTorch 实现这个学习过程几乎不需要做任何改动。

注意:这种组合框架是一种动态学习如何处理结构化数据的方法,纯粹来自我们的训练数据集。 以前,我们只使用固定的核来处理数据,在多个应用程序中以相同的方式进行处理。 在这里,我们“动态地”学习处理我们的输入数据的最佳方式,这对于手头的任务是独一无二的。 这是因为神经网络的权重是相对于训练数据进行优化的。

13.3.2 在 GPyTorch 中实现组合模型

最后,我们现在实现这个框架并将其应用于我们的 MNIST 数据集。 在这里,定义我们的模型类更加复杂,因为我们需要实现神经网络并将其连接到 GP 模型类中。 让我们先解决第一部分——先实现神经网络。 我们设计一个简单的神经网络,其架构如图 13.11 所示。 此网络具有四个层,节点数分别为 1,000、5,000、50 和 2,如图中所示。 这是一个常见的 MNIST 数据集架构。

13-11.png

图 13.11 要实现的神经网络架构。 它有四层,并为每个输入数据点产生一个大小为两个的数组。

注意,我们需要关注最后一层(2)的大小,它表示要馈入高斯过程的均值函数和核函数的处理输出的维度。将该层的大小设置为 2,目的是学习存在于二维空间中的图像表示。其他值也可以使用,但为了可视化的目的,我们选择了 2。

我们使用 PyTorch 中的Linear()ReLU()类实现该体系结构。在这里,我们的网络的每一层都被实现为一个带有相应大小的torch.nn.Linear模块,如图 13.11 所定义的。每个模块还与一个torch.nn.ReLU激活函数模块相耦合,该模块实现了前面提到的非线性变换。这在图 13.12 中得到了说明,其中注释了网络体系结构的每个组件对应于实现它的相应代码。

图 13.12 中实现的神经网络体系结构及其相应的 PyTorch 代码。每个层都使用torch.nn.Linear实现,每个激活函数都使用torch.nn.ReLU实现。

通过使用方便的add_module()方法,我们隐含定义了神经网络模型的forward()方法的逻辑。接下来,我们使用LargeFeatureExtractor类实现模型。该类将其输入x依次通过我们在__init__()方法中实现的层中,该方法接收data_dim,即输入数据的维数。在我们的情况下,该数字为 28×28=784,我们使用train_x.size(-1)进行计算:

data_dim = train_x.size(-1)                            ❶

class LargeFeatureExtractor(torch.nn.Sequential):
    def __init__(self, data_dim):
        super(LargeFeatureExtractor, self).__init__()

        self.add_module('linear1', torch.nn.Linear
        ➥(data_dim, 1000))                            ❷
        self.add_module('relu1', torch.nn.ReLU())      ❷
        self.add_module('linear2', torch.nn.Linear
        ➥(1000, 500))                                 ❸
        self.add_module('relu2', torch.nn.ReLU())      ❸

        self.add_module('linear3', torch.nn.Linear
        ➥(500, 50))                                   ❹
        self.add_module('relu3', torch.nn.ReLU())      ❹

        self.add_module('linear4', torch.nn.Linear
        ➥(50, 2))                                     ❺

feature_extractor = LargeFeatureExtractor(data_dim)    ❻

❶ 数据的维度

❷ 网络的第一层

❸ 第二层

❹ 第三层

❺ 第四层

❻ 初始化网络

接下来,我们讨论组合模型——一种利用我们刚刚初始化的神经网络特征提取器feature_extractor的高斯过程模型类。我们首先实现它的__init__()方法,该方法由几个组件组成:

  1. 协方差模块被包装在gpytorch.kernels.GridInterpolationKernel对象中,为我们的中等大小训练集(1,000 个点)提供计算速度加速。我们声明输入数据的维数为二,因为这是特征提取器生成的输出的维度。

  2. 特征提取器本身就是我们之前声明的feature_extractor变量。

  3. 如果神经网络的权重初始化得不好,特征提取器的输出值可能会取极端值(负无穷或正无穷)。为解决这个问题,我们使用gpytorch.utils.grid.ScaleToBounds模块将这些输出值缩放到-1 和 1 之间的范围内。

__init__()方法的实现如下:

class GPRegressionModel(gpytorch.models.ExactGP):
  def __init__(self, train_x, train_y, likelihood):
      super(GPRegressionModel, self).__init__(train_x, train_y, likelihood)

      self.mean_module = gpytorch.means.ConstantMean()

      self.covar_module = gpytorch.kernels
      ➥.GridInterpolationKernel(                    ❶
          gpytorch.kernels.ScaleKernel(              ❶
              gpytorch.kernels.RBFKernel             ❶
              ➥(ard_num_dims=2)                     ❶
          ),                                         ❶
          num_dims=2,                                ❶
          grid_size=100                              ❶
      )                                              ❶

      self.feature_extractor = feature_extractor     ❷

      self.scale_to_bounds = gpytorch.utils.grid
      ➥.ScaleToBounds(-1., 1.)                      ❸

❶ 具有两个维度的 RBF 核函数,具有计算速度加速

❷ 神经网络特征提取器

❸ 一个用于将神经网络的输出缩放至合理值的模块

在我们的forward()方法中,我们将所有这些组件结合在一起。首先,我们使用我们的神经网络特征提取器处理输入。然后,我们将处理后的输入馈送到我们的 GP 模型的平均值和协方差模块中。最后,我们仍然得到一个 MVN 分布,就是forward()方法返回的结果:

class GPRegressionModel(gpytorch.models.ExactGP):
  def forward(self, x):
    projected_x = self.feature_extractor(x)            ❶
    projected_x = self.scale_to_bounds(projected_x)    ❶

    mean_x = self.mean_module(projected_x)             ❷
    covar_x = self.covar_module(projected_x)           ❷
    return gpytorch.distributions.MultivariateNormal   ❷
    ➥(mean_x, covar_x)                                ❷

❶ 缩放后的神经网络特征提取器的输出

❷ 从处理后的输入创建一个 MVN 分布对象

最后,为了使用梯度下降训练这个组合模型,我们声明了以下对象。在这里,除了常规的 GP 超参数,如平均常数和长度和输出尺度,我们还想优化神经网络特征提取器的权重,这些权重存储在model.feature_extractor.parameters()中:

likelihood = gpytorch.likelihoods.GaussianLikelihood()             ❶
model = GPRegressionModel(train_x, train_y, likelihood)            ❶
mll = gpytorch.mlls.ExactMarginalLogLikelihood(likelihood, model)  ❶

optimizer = torch.optim.Adam([
    {'params': model.feature_extractor.parameters()},              ❷
    {'params': model.covar_module.parameters()},                   ❷
    {'params': model.mean_module.parameters()},                    ❷
    {'params': model.likelihood.parameters()},                     ❷
], lr=0.01)

❶ 同之前一样,似然函数、GP 模型和损失函数保持不变。

❷ 现在,梯度下降优化器 Adam 需要优化特征提取器的权重和 GP 的超参数。

现在我们可以像之前一样运行梯度下降:

model.train()                      ❶
likelihood.train()                 ❶

for i in tqdm(range(500)):         ❷
    optimizer.zero_grad()          ❷

    output = model(train_x)        ❷
    loss = -mll(output, train_y)   ❷

    loss.backward()                ❷
    optimizer.step()               ❷

model.eval()                       ❸
likelihood.eval()                  ❸

❶ 启用训练模式

❷ 运行 500 次梯度下降迭代

❸ 启用预测模式

注意:提醒一下,当训练 GP 模型时,我们需要同时为模型和似然函数启用训练模式(使用model.train()likelihood.train())。在训练之后并在进行预测之前,我们需要启用预测模式(使用model.eval()likelihood.eval())。

现在,我们已经训练了与神经网络特征提取器结合的 GP 模型。在使用该模型对测试集进行预测之前,我们可以查看模型的内部,看看神经网络特征提取器是否学会了很好地处理我们的数据。请记住,每个图像都被特征提取器转化为一个二元数组。因此,我们可以将训练数据通过该特征提取器,并使用散点图可视化输出。

注意:训练这个组合模型比训练普通的 GP 模型需要更多时间。这是因为我们现在要优化的参数更多。然而,正如我们马上会看到的那样,这个成本是非常值得的,因为我们获得了更高的性能提升。

在这个散点图中,如果我们看到相同标签的点(即,描绘相同数字的图像)聚在一起,这将表明特征提取器能够有效地从数据中学习。同样,我们通过将训练数据通过特征提取器的方式进行处理来做到这一点,这与模型类的forward()方法中的数据处理方式相同:

with torch.no_grad():
    extracted_features = model.feature_extractor(train_x)
    extracted_features = model.scale_to_bounds(extracted_features)

在这里,extracted_features是一个大小为 1,000x2 的 PyTorch 张量,存储了我们训练集中 1,000 个数据点的二维提取特征。为了在散点图中可视化这个张量,我们使用 Matplotlib 库的plt.scatter()方法,确保每个标签对应一个颜色:

for label in range(10):
    mask = train_y == label           ❶

    plt.scatter(                      ❷
        extracted_features[mask, 0],  ❷
        extracted_features[mask, 1],  ❷
        c=train_y[mask],              ❷
        vmin=0,                       ❷
        vmax=9,                       ❷
        label=label,                  ❷
    )                                 ❷

❶ 过滤具有特定标签的数据点

❷ 为当前数据点创建一个散点图,它们具有相同的颜色

此代码生成图 13.13,尽管你的结果可能会有所不同,这取决于库版本和代码运行的系统。正如我们所预期的,相同标签的数据点围绕在一起。这意味着我们的神经网络特征提取器成功地将具有相同标签的点分组在一起。经过网络处理后,具有相同标签的两个图像变成了二维平面上彼此靠近的两个点,如果由 RBF 核计算,则它们将具有高的协方差。这正是我们希望我们的特征提取器帮助我们做的事情!

图 13.13 由神经网络从 MNIST 数据集中提取的特征。不仅相同标签的数据点聚集在一起,而且在图中还存在一个标签梯度:从底部到顶部,标签值逐渐增加。

图 13.13 另一个有趣的方面是,与标签值相关的梯度明显:从底部到顶部的聚类,相应标签的值从 0 逐渐增加到 9。这是特征提取器中很好的特性,因为它表明我们的模型已经找到了一种平滑的 MNIST 图像表示,符合标签值。

例如,考虑图 13.14 中的比较,左侧面板显示图 13.13,右侧面板显示相同散点图标签的随机交换,使特征变得“粗糙”。所谓“粗糙”,是指标签值在不规律地跳动:底部聚类包含 0,中间某些聚类对应于 7 和 9,顶部聚类包含 5。换句话说,具有粗糙特征的标签趋势不是单调的,这使得训练 GP 更加困难。

图 13.14 图 13.13 中提取的平滑特征与标签随机交换的比较,使特征变得不那么平滑。平滑的特征比粗糙的特征更容易通过 GP 学习。

看起来神经网络在从图像中提取有用特征方面表现不错。为了确定这是否确实导致更好的预测性能,我们再次计算平均绝对误差(MAE):

with torch.no_grad():
    mean_preds = model(test_x).mean

print(torch.mean(torch.abs(mean_preds - test_y)))

Output: 0.8524129986763

这个结果告诉我们,平均而言,我们的预测偏差为 0.85;这是对前一节中我们拥有的普通 GP 的显着改进,其 MAE 大约为 2.7。这种改进说明了联合模型的卓越性能,这源于神经网络灵活的建模能力。

正如我们在开始时所说,这个框架不仅适用于手写数字,还适用于各种类型的结构化数据,神经网络可以从中学习,包括其他类型的图像和图形结构,如分子和蛋白质。我们所要做的就是定义一个合适的 DL 架构,从这些结构化数据中提取特征,然后将这些特征传递给 GP 的均值函数和核函数。

这就结束了第十二章。在本章中,我们了解了从结构化数据中学习的困难,例如图像,在这些数据中,常见的核无法有效地计算数据点之间的协方差。通过在 GP 前面附加一个神经网络特征提取器,我们学会将这些结构化数据转换成 GP 的核函数可以处理的形式。最终结果是一个结合模型,可以灵活地从结构化数据中学习,但仍然产生具有不确定性量化的概率预测。

总结

  • 结构化数据是指其特征需要满足约束条件的数据,例如必须是整数或者非负数,并且不能被视为连续的实值数据。例如,常见应用程序中的数据,如计算机视觉中的图像和药物发现中的蛋白质结构。

  • 结构化数据对于 GP 的常见核构成挑战。这是因为这些核只考虑输入数据的数值,这可能是不良的预测特征。

  • 使用错误特征计算协方差的核可能会导致生成的 GP 的预测质量低下。使用错误特征在结构化数据中特别常见。

  • 对于图像数据特别是,像素的原始值不是一个信息量丰富的特征。使用原始像素值计算协方差的核可能导致低质量的 GP。

  • 由于具有多层非线性计算,神经网络能够有效地学习复杂函数,并且可以从结构化数据中提取特征。通过使用神经网络从结构化数据中提取连续的实值特征,GP 仍然可以有效地学习。

  • 在将神经网络与 GP 结合时,我们动态学习一种处理问题的数据方式。这种灵活性使得该模型可以推广到许多种结构化数据。

  • 在将输出缩放到小范围之前,将神经网络的输出传递给 GP 是很重要的。通过这样做,我们避免了由于神经网络特征提取器初始化不良而导致的极端值。

  • 从神经网络特征提取器学习到的表示对标签具有平滑的梯度。这种平滑的梯度使得提取的特征更容易通过 GP 学习。

附录:练习的解决方案

第二章 A.1: 高斯过程作为函数分布

在这个练习中,我们对我们在第一章看到的真实数据集进行了 GP 训练。 解决方案包含在 CH02/02 - Exercise.ipynb 笔记本中。 完成以下步骤:

  1. 创建四维数据集。

    首先,我们导入必要的库:PyTorch 用于数组/张量操作,GPyTorch 用于 GP 建模,Matplotlib 用于可视化:

    import torch
    import gpytorch
    import matplotlib.pyplot as plt
    

    然后,我们将表中的数字存储在两个 PyTorch 张量中,train_xtrain_y,分别包含数据集的特征和标签:

    train_x = torch.tensor(
        [
            [1 / 2, 1 / 2, 0, 0],
            [1 / 3, 1 / 3, 1 / 3, 0],
            [0, 1 / 2, 1 / 2, 0],
            [0, 1 / 3, 1 / 3, 1 / 3],
        ]
    )
    
    train_y = torch.tensor([192.08, 258.30, 187.24, 188.54])
    
  2. 通过从所有值中减去均值并将结果除以它们的标准差来标准化第五列。

    我们按如下方式标准化标签:

    # normalize the labels
    train_y = (train_y - train_y.mean()) / train_y.std()
    

    打印出时,train_y 应该包含以下值:tensor([-0.4183, 1.4974, -0.5583, -0.5207])

  3. 将前四列视为特征,第五列为标签。 在这个数据上训练一个 GP。

    我们如下重新实现我们的 GP 模型类:

    class BaseGPModel(gpytorch.models.ExactGP):
        def __init__(self, train_x, train_y, likelihood):
            super().__init__(train_x, train_y, likelihood)
            self.mean_module = gpytorch.means.ZeroMean()
            self.covar_module = gpytorch.kernels.RBFKernel()
    
        def forward(self, x):
            mean_x = self.mean_module(x)
            covar_x = self.covar_module(x)
            return gpytorch.distributions.MultivariateNormal(mean_x, covar_x)
    

    然后,我们用我们的训练数据初始化这个类的对象:

    lengthscale = 1
    noise = 1e-4
    
    likelihood = gpytorch.likelihoods.GaussianLikelihood()
    model = BaseGPModel(train_x, train_y, likelihood)
    
    model.covar_module.lengthscale = lengthscale
    model.likelihood.noise = noise
    
    model.eval()
    likelihood.eval()
    
  4. 创建一个包含百分之零锗和锰的组合的测试数据集。

    要组装我们的测试数据集,我们首先为第一列和第二列创建一个跨越单位正方形的网格:

    grid_x = torch.linspace(0, 1, 101)
    
    grid_x1, grid_x2 = torch.meshgrid(grid_x, grid_x, indexing="ij")
    

    这前两列存储在 grid_x1grid_x2 中。 然后,我们在 grid_x1grid_x2 中附加两个额外的全零列,完成了具有四列的测试集:

    xs = torch.vstack(
        [
            grid_x1.flatten(),      ❶
            grid_x2.flatten(),      ❷
            torch.zeros(101 ** 2),  ❸
            torch.zeros(101 ** 2),  ❹
        ]
    ).transpose(-1, -2)
    

    ❶ 第一列

    ❷ 第二列

    ❸ 第三列,包含全零

    ❹ 第四列,包含全零

  5. 预测这个测试集的混合温度。

    要在这个测试集上进行预测,我们只需在torch.no_grad()上下文中通过我们的 GP 模型传递 xs

    with torch.no_grad():
        predictive_distribution = likelihood(model(xs))
        predictive_mean = predictive_distribution.mean
        predictive_stddev = predictive_distribution.stddev
    
  6. 可视化预测。

    要可视化这些预测,我们首先创建一个具有两个面板(即,两个 Matplotlib 子图)的图:

    fig, ax = plt.subplots(1, 2, figsize=(16, 6))
    

    然后,我们使用plt.imshow()将均值和标准差向量可视化为热图,确保将这两个向量重塑为方阵:

    c = ax[0].imshow(
        predictive_mean.detach().reshape(101, 101).transpose(-1, -2),
        origin="lower",
        extent=[0, 1, 0, 1],
    )                            ❶
    
    c = ax[1].imshow(
        predictive_stddev.detach().reshape(101, 101).transpose(-1, -2),
        origin="lower",
        extent=[0, 1, 0, 1],
    )                            ❷
    plt.colorbar(c, ax=ax[1])
    

    ❶ 预测均值的热图

    ❷ 预测标准差的热图

    这将创建类似于图 A.1 中的图。

图 A.1 GP 在二维空间上的预测

注意 如果您使用不同的 GP 实现,则完全有可能生成与图 A.1 中略有不同的热图。 只要热图的一般趋势相同,您的解决方案就是正确的。

第三章 A.2: 使用均值和协方差函数结合先验知识

该练习提供了使用自动相关性确定(ARD)的 GP 模型的实践。 解决方案包含在 CH03/03 - Exercise.ipynb 中。 完成以下步骤:

  1. 使用 PyTorch 在 Python 中实现二维函数。

    首先,我们导入必要的库——PyTorch 用于数组/张量操作,GPyTorch 用于 GP 建模,Matplotlib 用于可视化:

    import torch
    import gpytorch
    import matplotlib.pyplot as plt
    

    然后使用给定的公式实现目标函数:

    def f(x):
        return (
            torch.sin(5 * x[..., 0] / 2 - 2.5) * torch.cos(2.5 - 5 * x[..., 1])
            + (5 * x[..., 1] / 2 + 0.5) ** 2 / 10
        ) / 5 + 0.2
    
  2. 在域[0, 2]²上可视化函数。

    要可视化函数,我们需要创建一个网格来表示域。我们将这个网格存储在xs中:

    lb = 0
    ub = 2
    xs = torch.linspace(lb, ub, 101)                                   ❶
    x1, x2 = torch.meshgrid(xs, xs)
    xs = torch.vstack((x1.flatten(), x2.flatten())).transpose(-1, -2)  ❷
    

    ❶ 一维网格

    ❷ 二维网格

    然后我们可以通过将xs传递给f()来在这个网格上获取函数值。结果存储在ys中:

    ys = f(xs)
    

    我们使用plt.imshow()ys可视化为热图:

    plt.imshow(ys.reshape(101, 101).T, origin="lower", extent=[lb, ub, lb, ub])
    
  3. 从域[0, 2]²中随机抽取 100 个数据点。这将作为我们的训练数据。

    要在域内随机抽取 100 个点,我们使用torch.rand()从单位正方形中进行抽样,然后将结果乘以 2 以将其缩放到我们的域中:

    torch.manual_seed(0)
    train_x = torch.rand(size=(100, 2)) * 2
    

    这些点的函数值可以通过调用f(train_x)来获取:

    train_y = f(train_x)
    
  4. 使用常数均值函数和作为gpytorch.kernels.ScaleKernel对象实现输出尺度的 Matérn 5/2 核来实现一个 GP 模型。我们按照如下指定实现我们的 GP 模型:

    class GPModel(gpytorch.models.ExactGP):
        def __init__(self, train_x, train_y, likelihood):
            super().__init__(train_x, train_y, likelihood)
            self.mean_module = gpytorch.means.ConstantMean()
            self.covar_module = gpytorch.kernels.ScaleKernel(
                gpytorch.kernels.MaternKernel(
                    nu=2.5,
                    ard_num_dims=None    ❶
                )
            )
    
        def forward(self, x):
            mean_x = self.mean_module(x)
            covar_x = self.covar_module(x)
            return gpytorch.distributions.MultivariateNormal(mean_x, covar_x)
    

    ❶ 设置为 None 以禁用 ARD,设置为 2 以启用 ARD。

  5. 在初始化核对象时不要指定ard_num_dims参数,或者将参数设置为None

    这是在先前的代码中完成的。

  6. 使用梯度下降训练 GP 模型的超参数,并在训练后检查长度尺度。

    我们初始化我们的 GP 并使用梯度下降进行 500 次迭代训练,如下所示:

    noise = 1e-4
    
    likelihood = gpytorch.likelihoods.GaussianLikelihood()
    model = GPModel(train_x, train_y, likelihood)
    
    model.likelihood.noise = noise
    
    optimizer = torch.optim.Adam(model.parameters(), lr=0.01)
    mll = gpytorch.mlls.ExactMarginalLogLikelihood(likelihood, model)
    
    model.train()                    ❶
    likelihood.train()               ❶
    
    losses = []
    for i in tqdm(range(500)):       ❷
        optimizer.zero_grad()        ❷
    
        output = model(train_x)      ❷
        loss = -mll(output, train_y) ❷
    
        loss.backward()              ❷
        losses.append(loss.item())   ❷
    
        optimizer.step()             ❷
    
    model.eval()                     ❸
    likelihood.eval()                ❸
    

    ❶ 启用训练模型

    ❷ 梯度下降来优化 GP 的超参数

    ❸ 启用预测模型

    经过这 500 次迭代,我们通过打印以下数值来检查长度尺度:

    >>> model.covar_module.base_kernel.lengthscale
    tensor([[1.1535]])
    

    换句话说,优化的长度尺度大约等于 1.15。

  7. 重新定义 GP 模型类,这次将ard_num_dims设置为2

    GPModel类中设置ard_num_dims=2,然后重新运行所有代码单元格,我们得到以下长度尺度的数值:

    >>> model.covar_module.base_kernel.lengthscale
    tensor([[1.6960, 0.8739]])
    

    在这里,第一维的长度尺度很大(大约 1.70),而第二维的长度尺度很小(大约 0.87)。这对应于目标函数沿第二维变化更多的事实。

A.3 第四章:使用基于改进的策略优化最佳结果

这一章节有两个练习:

  1. 第一个涵盖了改进概率提高(PoI)策略的方法,使其能够更好地探索搜索空间。

  2. 第二个将我们学到的两个 BayesOpt 策略应用于一个模拟的真实世界的超参数调整任务。

A.3.1 练习 1:使用概率提高鼓励探索

这个练习在 CH04/02 - Exercise 1.ipynb 笔记本中实现,引导我们如何修改 PoI 以鼓励探索。完成以下步骤:

  1. 在 CH04/01 - BayesOpt loop 笔记本中重新创建 BayesOpt 循环,该循环使用一维 Forrester 函数作为优化目标。

  2. 在实现 BayesOpt 的for循环之前,声明一个名为epsilon的变量:

    epsilon = 0.1
    
  3. for 循环内,像以前一样初始化 PoI 策略,但这次指定由 best_f 参数设置的现任阈值是现任值 加上 存储在 epsilon 中的值:

    policy = botorch.acquisition.analytic.ProbabilityOfImprovement(
        model, best_f=train_y.max() + epsilon
    )
    
  4. 重新运行笔记本,并观察是否此修改比原始 PoI 策略更好地优化性能,通过鼓励更多探索,如图 A.2 所示。

    图 A.2 修改后 PoI 在最后一次迭代中的优化进展。策略已经找到了最优解。

    在这里,修改后的 PoI 已经找到了最优解。

  5. PoI 变得更加探索性取决于存储在 epsilon 中的最小改进阈值的大小。将此变量设置为 0.001 并不足以鼓励探索,策略再次陷入困境。将此变量设置为 0.5 效果很好。

  6. 实现一个相对最小改进阈值,要求改进达到 110%:

    epsilon_pct = 0.1
    
    for i in range(num_queries):
        ...                                                    ❶
    
        policy = botorch.acquisition.analytic.ProbabilityOfImprovement(
            model, best_f=train_y.max() * (1 + epsilon_pct)    ❷
        )
    

    ❶ 省略

    ❷ 相对改进

A.3.2 练习 2:超参数调优的 BayesOpt

此练习在 CH04/03 - Exercise 2.ipynb 中实现,将 BayesOpt 应用于模拟支持向量机模型在超参数调优任务中的准确度曲面的目标函数。完成以下步骤:

  1. 在 CH04/01 - BayesOpt loop.ipynb 中重新创建 BayesOpt 循环。我们的目标函数实现为

    def f(x):
        return (
            torch.sin(5 * x[..., 0] / 2 - 2.5)
            * torch.cos(2.5 - 5 * x[..., 1])
            + (5 * x[..., 1] / 2 + 0.5) ** 2 / 10
        ) / 5 + 0.2
    
  2. 使用 xs 声明相应的测试数据,表示域的二维网格和 xs 的函数值的 ys

    lb = 0
    ub = 2
    num_queries = 20
    
    bounds = torch.tensor([[lb, lb], [ub, ub]], dtype=torch.float)
    
    xs = torch.linspace(lb, ub, 101)
    x1, x2 = torch.meshgrid(xs, xs)
    xs = torch.vstack((x1.flatten(), x2.flatten())).transpose(-1, -2)
    ys = f(xs)
    
  3. 修改可视化优化进展的辅助函数。我们将此函数声明为 visualize_progress_and_policy(),该函数只需要一个策略对象和 next_x 作为要查询的下一个点。首先,函数计算测试数据 xs 的获取分数:

    def visualize_progress_and_policy(policy, next_x=None):
        with torch.no_grad():
            acquisition_score = policy(xs.unsqueeze(1))
    
        ...    ❶
    

    ❶ 待续

    接下来,我们声明两个 Matplotlib 子图,并且对于第一个子图,绘制存储在 ys 中的真实情况:

    c = ax[0].imshow(                                                    ❶
        ys.reshape(101, 101).T, origin="lower", extent=[lb, ub, lb, ub]  ❶
    )                                                                    ❶
    ax[0].set_xlabel(r"$C$", fontsize=20)
    ax[0].set_ylabel(r"$\gamma$", fontsize=20)
    plt.colorbar(c, ax=ax[0])
    
    ax[0].scatter(train_x[..., 0], train_x[..., 1], marker="x", c="k")   ❷
    

    ❶ 显示真实情况的热图

    ❷ 显示标记数据的散点图

    最后,我们在第二个子图中绘制另一个热图,显示获取分数:

    c = ax[1].imshow(                           ❶
        acquisition_score.reshape(101, 101).T,  ❶
        origin="lower",                         ❶
        extent=[lb, ub, lb, ub]                 ❶
    )                                           ❶
    ax[1].set_xlabel(r"$C$", fontsize=20)
    plt.colorbar(c, ax=ax[1])
    

    ❶ 显示获取分数的热图

    我们可以选择显示 next_x

    if next_x is not None:
        ax[1].scatter(
            next_x[..., 0],
            next_x[..., 1],
            c="r",
            marker="*",
            s=500,
            label="next query"
        )
    
  4. 从第三章的练习中复制 GP 类,该类实现了具有 ARD 的 Matérn 2.5 核。进一步修改此类以使其与 BoTorch 集成:

    class GPModel(gpytorch.models.ExactGP,         ❶
      botorch.models.gpytorch.GPyTorchModel):      ❶
        _num_outputs = 1                           ❶
    
        def __init__(self, train_x, train_y, likelihood):
            super().__init__(train_x, train_y, likelihood)
            self.mean_module = gpytorch.means.ConstantMean()
            self.covar_module = gpytorch.kernels.  ❷
            ➥ScaleKernel(                         ❷
                gpytorch.kernels.MaternKernel(     ❷
                    nu=2.5,                        ❷
                    ard_num_dims=2                 ❷
                )                                  ❷
            )                                      ❷
    
        def forward(self, x):
            ...                                    ❸
    

    ❶ BoTorch 相关修改

    ❷ 具有 ARD 的 Matérn 2.5 核

    ❸ 省略

  5. 重用辅助函数 fit_gp_model() 和实现 BayesOpt 的 for 循环。我们复制 fit_gp_model() 并声明初始数据集:

    train_x = torch.tensor([
        [1., 1.],
    ])
    train_y = f(train_x)
    

    然后我们声明 BayesOpt 循环:

    num_queries = 20
    
    for i in range(num_queries):
        print("iteration", i)
        print("incumbent", train_x[train_y.argmax()], train_y.max())
    
        model, likelihood = fit_gp_model(train_x, train_y)
    
        policy = ...              ❶
    
        next_x, acq_val = botorch.optim.optimize_acqf(
            policy,
            bounds=bounds,
            q=1,
            num_restarts=40,      ❷
            raw_samples=100,      ❷
        )
    
        visualize_progress_and_policy(policy,
        ➥next_x=next_x)          ❸
    
        next_y = f(next_x)
    
        train_x = torch.cat([train_x, next_x])
        train_y = torch.cat([train_y, next_y])
    

    ❶ 策略初始化的占位符

    ❷ 使搜索更加穷举

    ❸ 调用新的可视化辅助函数

  6. 在这个目标函数上运行 PoI 策略。观察到该策略再次陷入局部最优。将初始化 BayesOpt 策略的行替换为

    policy = botorch.acquisition.analytic.ProbabilityOfImprovement(
        model, best_f=train_y.max()
    )
    

    运行整个笔记本,显示策略再次陷入局部最优,如图 A.3 所示。

    图 A.3 显示了 PoI 在最后一次迭代中的优化进度。该策略被困在了一个局部最优解中。

  7. 运行修改后的 PoI 版本,其中最小改进阈值设置为 0.1。将初始化 BayesOpt 策略的行替换为:

    policy = botorch.acquisition.analytic.ProbabilityOfImprovement(
        model, best_f=train_y.max() + 0.1
    )
    

    该策略更具探索性,表现优于常规 PoI。图 A.4 显示了该策略在第 17 次迭代时的进展,其中它首次达到至少 90%的准确率。

    图 A.4 显示了修改版 PoI 在第 17 次迭代中的优化进度,在该次迭代中,该策略首次达到至少 90%的准确率。

    在这里,C = 1.6770,γ = 1.9039 是提供此准确度的参数。

  8. 在此目标函数上运行 Expected Improvement(EI)策略。用初始化 BayesOpt 策略的行替换:

    policy = botorch.acquisition.analytic.ExpectedImprovement(
        model, best_f=train_y.max()
    )
    

    该策略在我们的目标函数上表现良好,如图 A.5 所示,在第 15 次迭代中找到了至少 90%的准确率。

    图 A.5 显示了 EI 在第 4 次迭代中的优化进度,在该次迭代中,该策略首次达到至少 90%的准确率。

    在这里,C = 1.6331,γ = 1.8749 是提供此准确度的参数。

  9. 实施重复实验,并可视化 10 个实验中的平均 incumbent 值和误差条。我们首先将实现 BayesOpt 循环的代码放入一个外部循环中,该循环迭代多个实验。我们在incumbents中的每一步跨实验存储每次最好的值:

    num_repeats = 10
    
    incumbents = torch.zeros((num_repeats, num_queries))
    
    for trial in range(num_repeats):
        print("trial", trial)
    
        torch.manual_seed(trial)                      ❶
        train_x = bounds[0] + (bounds[1] - bounds[0]) ❶
        ➥* torch.rand(1, 2)                          ❶
        train_y = f(train_x)                          ❶
    
        for i in tqdm(range(num_queries)):
            incumbents[trial, i] = train_y.max()      ❷
    
            ...                                       ❸
    
    torch.save(incumbents, [path to file])            ❹
    

    ❶在搜索空间中均匀采样一个数据点作为起始点

    ❷跟踪最佳值

    ❸省略的代码与之前相同。

    ❹将结果保存到文件中,以便稍后可视化。

    然后我们实现一个帮助函数,绘制平均 incumbent 值和误差条。该函数读取保存在path中的 PyTorch tensor,该 tensor 应是前一步中incumbents的保存版本:

    def show_agg_progress(path, name):
        def ci(y):                                             ❶
            return 2 * y.std(axis=0) / np.sqrt(num_repeats)    ❶
    
        incumbents = torch.load(path)                          ❷
    
        avg_incumbent = incumbents.mean(axis=0)                ❸
        ci_incumbent = ci(incumbents)                          ❸
    
        plt.plot(avg_incumbent, label=name)                    ❹
        plt.fill_between(                                      ❹
            np.arange(num_queries),                            ❹
            avg_incumbent + ci_incumbent,                      ❹
            avg_incumbent - ci_incumbent,                      ❹
            alpha=0.3,                                         ❹
        )                                                      ❹
    

    ❶计算误差条的辅助子函数

    ❷加载保存的优化结果

    ❸计算结果的平均值和误差条

    ❹可视化结果平均值和误差条

    然后我们可以运行我们在前面代码中拥有的策略,并比较它们的表现:

    plt.figure(figsize=(8, 6))
    
    show_agg_progress([path to EI data], "EI")
    show_agg_progress([path to PoI data], "PoI")
    show_agg_progress([path to modified PoI data], "PoI" + r"$(\epsilon = 0.1)$")
    plt.xlabel("# queries")
    plt.ylabel("accuracy")
    
    plt.legend()
    
    plt.show()
    

    这生成了图 A.6,显示了 PoI、修改版 PoI 和 EI 的优化性能。

    图 A.6 显示了 10 个重复实验中各种策略的优化进度。

    我们发现图 A.6 比单次运行中的策略更具见解。在这里,PoI 的表现不如其他两个策略,而且其性能也不太稳健,从大的误差条中可以看出来。修改版 PoI 和 EI 表现相当,很难判断哪个更好,因为它们的误差条重叠。

A.4 第五章:使用赌博工具风格的策略探索搜索空间

本章中有两个练习:

  1. 第一个练习探索了一种为 UCB 策略设置权衡参数的潜在方法,该方法考虑了我们在优化中的进展情况。

  2. 第二个练习将本章学到的两种策略应用于以前章节中看到的超参数调整问题。

A.4.1 练习 1:为上置信界限设置探索计划

这个练习实现在 CH05/02 - Exercise 1.ipynb 中,讨论了自适应设置 UCB 策略权衡参数β值的策略。完成以下步骤:

  1. 在 CH04/02 - Exercise 1.ipynb 中重新创建 BayesOpt 循环,该循环将一维 Forrester 函数作为优化目标。

    由于 BayesOpt 循环中有 10 次迭代,β乘以倍数m 10 次,从 1 到 10。也就是说,1 × m10 = 10。解决这个方程得到了倍数的代码:

    num_queries = 10
    
    start_beta = 1
    end_beta = 10
    
    multiplier = (end_beta / start_beta) ** (1 / num_queries)
    
  2. 实现这个调度逻辑,并观察结果的优化性能。

    我们对 BayesOpt 循环进行如下修改:

    num_queries = 10
    
    start_beta = 1
    end_beta = 10
    
    multiplier = (end_beta / start_beta) ** (1 / num_queries)
    
    beta = start_beta
    
    for i in range(num_queries):
        ...                          ❶
    
        policy = botorch.acquisition.analytic.UpperConfidenceBound(
            model, beta=beta
        )
    
        ...                          ❷
    
        beta *= multiplier
    

    ❶ 获得训练好的 GP

    ❷ 找到最大化获取分数的点,查询目标函数,并更新训练数据

    此代码生成图 A.7。

    图 A.7 自适应 UCB 策略的进展。该策略能够摆脱局部最优解,并接近全局最优解。

    我们看到该策略在第五次迭代时检查了局部最优解,但最终能够逃脱并在最后接近全局最优解。

A.4.2 练习 2:用于超参数调整的 BayesOpt

此练习实现在 CH05/03 - Exercise 2.ipynb 中,将 BayesOpt 应用于模拟超参数调整任务中支持向量机模型准确率表面的目标函数。完成以下步骤:

  1. 在 CH04/03 - Exercise 2.ipynb 中重新创建 BayesOpt 循环,包括实施重复实验的外部循环。

  2. 运行 UCB 策略,将权衡参数的值设置为β ∈ { 1, 3, 10, 30 },并观察值的聚合性能。

    可以在初始化策略对象时设置权衡参数的值:

    policy = botorch.acquisition.analytic.UpperConfidenceBound(
        model, beta=[some value]
    )
    

    图 A.8 显示了四个版本 UCB 的优化性能。我们看到当β = 1 时,策略过于探索,性能最差。

    图 A.8 不同 UCB 策略的进展

    随着权衡参数值的增加,性能也增加,但当 β = 30 时,过度探索导致 UCB 在定位 90%准确度时变慢。总体而言,β = 10 达到了最佳性能。

  3. 运行 UCB 的自适应版本(见练习 1)。

    我们对 BayesOpt 循环进行如下修改:

    num_repeats = 10
    
    start_beta = 3
    end_beta = 10
    
    multiplier = (end_beta / start_beta) ** (1 / num_queries)
    
    incumbents = torch.zeros((num_repeats, num_queries))
    
    for trial in range(num_repeats):
        ...                                ❶
    
        beta = start_beta
    
        for i in tqdm(range(num_queries)):
            ...                            ❷
    
            policy = botorch.acquisition.analytic.UpperConfidenceBound(
                model, beta=beta
            )
    
            ...                            ❸
    
            beta *= multiplier
    

    ❶ 随机生成初始训练数据

    ❷ 记录现有值并重新训练模型

    ❸ 找到最大化获取分数的点,查询目标函数,并更新训练数据

    图 A.9 展示了两种自适应版本相对于最佳性能固定值 β = 10 的优化性能。

    图 A.9 两种 UCB 策略的自适应版本的进展。该策略对交易参数的结束值具有鲁棒性。

    这些版本是可比较的,将结束值从 10 更改为 30 并不会对优化性能产生太大影响。

  4. 运行 Thompson 抽样(TS)策略,并观察其综合性能。

    我们按照以下方式实现 TS:

    num_candidates = 2000
    num_repeats = 10
    
    incumbents = torch.zeros((num_repeats, num_queries))
    
    for trial in range(num_repeats):
      ...                               ❶
    
      for i in tqdm(range(num_queries)):
        ...                             ❷
    
        sobol = torch.quasirandom.SobolEngine(1, scramble=True)
        candidate_x = sobol.draw(num_candidates)
        candidate_x = bounds[0] + (bounds[1] - bounds[0]) * candidate_x
    
        ts = botorch.generation.MaxPosteriorSampling(model, 
        ➥replacement=False)
        next_x = ts(candidate_x, num_samples=1)
    
        ...                             ❸
    

    ❶ 随机生成初始训练数据

    ❷ 记录现任价值并重新训练模型

    ❸ 找到最大化收益分数的点,查询目标函数并更新训练数据

    图 A.10 展示了 TS 的优化性能。我们看到该策略在开始时取得了显著进展,并且与第六章的 EI 相当,并且略逊于 UCB 的最佳版本。

    图 A.10 TS 的进展。该策略与 EI 相当,并略逊于 UCB 的最佳版本。

A.5 第六章:使用信息论与基于熵的策略

本章中有两个练习:

  1. 第一个练习涵盖了二分搜索的变体,其中在做出决策时可以考虑先验信息。

  2. 第二个练习将引导我们通过在前几章中看到的超参数调整问题中实现最大值熵搜索(MES)的过程。

A.5.1 练习 1:将先验知识纳入熵搜索

这个练习,实现在 CH06/02 - Exercise 1.ipynb 中,展示了在找到信息论最优决策时使用不同先验的一个实例,最终将帮助我们进一步欣赏熵搜索作为一种通用决策在不确定性下的过程的优雅和灵活性:

  1. 证明 Pr(X = 1) + Pr(X = 2) + ... + Pr(X = 10) = 1。

    我们可以通过简单地将概率相加来实现这一点:

    1 / 2 + 1 / 4 + ... + 1 / 2⁹ + 1 / 2⁹ = 1 / 2 + 1 / 4 + ... + 1 / 2⁸ + 1 / 2⁸ = ... = 1 / 2 + 1 / 2 = 1。

  2. 计算这个先验分布的熵。

    请记住熵的公式为 –Σ[i]**p[i] log p[i]。我们可以编写一个计算此和的 Python 函数:

    def compute_entropy(first, last):
        entropy = 0
        for i in range(first, last + 1):
            p = marginal_probability(i, first, last)   ❶
            entropy += -p * np.log2(p)                 ❷
    
        return entropy
    

    ❶ 获取当前概率。

    ❷ 对项求和。

    此函数将 firstlast 作为参数,它们对应于 X 可能的最小和最大值(起始值为 1 和 10),分别。然后我们遍历 firstlast 之间的数字,并累加 –p[i] log p[i] 项。这里,marginal_probability() 是一个计算 Pr(X = n) 的辅助函数,我们实现如下:

    def marginal_probability(floor, first, last):
        if floor == last:                   ❶
            return 2 ** -(last - first)     ❶
    
        return 2 ** -(floor - first + 1)
    

    ❶ 当底层是可能的最高层时的边缘情况

    运行 compute_entropy(1, 10) 将给出 1.99609375. 这是 X 先验分布的熵。

  3. 鉴于在 1 到 10 之间定义的先验分布,从二楼掉落时手机会损坏的概率是多少?从第五层呢?第一层呢?

    手机从二楼掉落时损坏的概率恰好是Pr(X = 1),即 0.5。

    手机从第五层掉落损坏的概率是X ≤ 4 的概率,即Pr(X = 1) + Pr(X = 2) + Pr(X = 3) + Pr(X = 4) = 15/16 = 0.9375。

    这两个计算可以作为一个函数来实现:

    def cumulative_density(floor, first, last):
        return sum(                                  ❶
            [                                        ❶
                marginal_probability(i, first, last) ❶
                for i in range(first, floor)         ❶
            ]                                        ❶
        )                                            ❶
    

    ❶ 对于小于阈值的X的概率求和

    由于我们的先验知识规定,如果从一楼掉落,手机不会损坏,这个概率为 0。

  4. 计算在我们在第五层进行试验的两种情况下的虚拟后验分布的熵。

    使用我们实现的compute_entropy()函数,我们可以计算两种情况下的熵。如果手机损坏,我们将first设为1last设为4;否则,我们将first设为5last设为10

    >>> compute_entropy(1, 4)
    Output: 1.75
    >>> compute_entropy(5, 10)
    Output: 1.9375
    
  5. 鉴于先验分布,计算在第五层进行试验后的预期后验熵。

    对于这个预期后验熵计算,我们已经进行了必要的计算。首先,手机从第五层掉落损坏的概率是 0.9375,在这种情况下,后验熵是 1.75。其次,手机从第五层掉落不损坏的概率是 1 - 0.9375 = 0.0625,在这种情况下,后验熵是 1.9375。

    对两种情况取平均值得到 (0.9375) 1.75 + (0.0625) 1.9375 = 1.76171875。这是你在第五层进行试验后的预期后验熵。

  6. 计算其他楼层的这个预期后验熵。

    我们可以实现一个函数来进行我们刚刚讨论的计算:

    def compute_expected_posterior_entropy(floor, first, last):
        break_probability = cumulative_density
        ➥(floor, first, last)                            ❶
    
        return (                                          ❷
            break_probability * compute_entropy           ❷
            ➥(first, floor - 1)                          ❷
            + (1 - break_probability) * compute_entropy   ❷
            ➥(floor, last)                               ❷
        )                                                 ❷
    

    ❶ 从给定楼层手机损坏的概率

    ❷ 对两种情况取平均值

    使用这个函数,我们可以绘制出在 1 到 10 之间数字的预期后验熵。

    这个图表显示在图 A.11 中,告诉我们我们第一次试验的信息论最佳地点是二楼,因为 2 给出了最低的预期后验熵(因此,不确定性最低)。

    图 A.11 预期后验熵作为试验地点的函数

    我们看到这与决策二进制搜索建议的不同,5。这是我们使用先验分布对X进行编码的领域知识的直接影响:因为X = 2 的可能性很高(50%),如果手机损坏,直接尝试这个数字可能会更好,因为这样我们可能会立即找到答案。

    有趣的是,从一楼掉下手机不会减少熵。这是因为我们确定手机不会从这一层摔坏,所以在进行这次试验之后,我们对世界的认知不会改变。

A.5.2 练习 2:超参数调优的 BayesOpt

此练习在 CH06/ 03- Exercise 2.ipynb 笔记本中实现,将 BayesOpt 应用于模拟超参数调整任务中支持向量机模型的精度曲面的目标函数:

  1. 在 CH04/ 03- Exercise 2.ipynb 中重新创建 BayesOpt 循环,包括实现重复实验的外部循环。

  2. 运行 MES 策略。

    考虑到我们的目标函数具有两个维度,我们应将 MES 使用的 Sobol 序列的大小设置为 2,000:

    num_candidates = 2000
    num_repeats = 10
    
    incumbents = torch.zeros((num_repeats, num_queries))
    
    for trial in range(num_repeats):
      ...                                 ❶
    
      for i in tqdm(range(num_queries)):
        ...                                ❷
    
        sobol = torch.quasirandom.SobolEngine(1, scramble=True)
        candidate_x = sobol.draw(num_candidates)
        candidate_x = bounds[0] + (bounds[1] - bounds[0]) * candidate_x
    
        policy = botorch.acquisition.max_value_entropy_search.qMaxValueEntropy(
            model, candidate_x
        )
    
        ...                               ❸
    

    ❶ 随机生成初始训练数据

    ❷ 记录现任值并重新训练模型

    ❸ 寻找最大化收获值的点,查询目标函数,并更新训练数据

    图 A.12 显示了 MES 的优化效果。我们可以看到,该策略在迄今为止学到的所有 BayesOpt 策略中都具有竞争力。

    图 A.12 展示了 MES 的优化效果。该策略在所显示的四个策略中表现最佳。

A.6 第七章:通过批量优化最大化吞吐量

本章包含两个练习:

  1. 第一个部分涵盖了在批量设置下实现 TS 的方法。

  2. 第二个部分介绍了如何在一个四维气动结构优化问题上运行 BayesOpt 策略。

A.6.1 练习 1:通过重新采样将 TS 扩展到批量设置

请记住,在我们在第 5.3 节学到的顺序设置中,TS 是从当前 GP 关于目标函数的信念中抽取一个样本,并查询最大化该样本的数据点。在批次设置中,我们只需重复此过程多次,从而多次采样 GP 并最大化样本,以组装所需大小的批次查询。此练习的代码可以在 CH07/ 02- Exercise 1.ipynb 笔记本中找到:

  1. 重新创建 CH05 / 01- BayesOpt loop.ipynb 笔记本中的批次 BayesOpt 循环。

  2. 根据第 5.3 节介绍的方法,使用 Sobol 采样器实现 TS。

    我们按照以下方式实施该策略,其中我们使用 2,000 元素的 Sobol 序列,并指定样本数作为批次大小:

    num_candidates = 2000                                   ❶
    
    ...                                                     ❷
    
    for i in tqdm(range(num_iters)):
        ...                                                 ❸
    
        sobol = torch.quasirandom.SobolEngine
        ➥(2, scramble=True)                                ❹
        candidate_x = sobol.draw(num_candidates)            ❹
        candidate_x = (bounds[1] - bounds[0]) *             ❹
        ➥candidate_x + bounds[0]                           ❹
    
        ts = botorch.generation.MaxPosteriorSampling(model, 
        ➥replacement=False)
        next_x = ts(candidate_x, num_samples=batch_size)    ❺
    
        ...                                                 ❻
    

    ❶ 指定 Sobol 序列的长度

    ❷ 随机选择初始训练数据

    ❸ 重新训练 GP

    ❹ 初始化 Sobol 序列

    ❺ 从 GP 中绘制多个样本

    ❻ 查询目标函数并更新训练数据

  3. 运行此 TS 策略对超参数调整目标函数进行观察其性能。

    运行批次 TS 后,我们可以绘制该策略对我们所学到的其他策略的进展,如图 A.13 所示。在仅进行第一批查询之后,TS 就能取得显著进展。

    图 A.13 批处理 TS 在超参数调整示例中的进展。只查询了第一批后,该策略就取得了显著进展。

A.6.2 练习 2: 优化飞机设计

这个练习提供了一个目标函数,用于模拟基准测试飞机设计的过程。代码提供在 CH07/04 - Exercise 2.ipynb 笔记本中。完成以下步骤:

  1. 实现模拟性能基准测试的目标函数。

    目标函数的代码已经提供,所以我们只需将其复制粘贴到程序中:

    def flight_utility(X):
      X_copy = X.detach().clone()
      X_copy[:, [2, 3]] = 1 - X_copy[:, [2, 3]]
      X_copy = X_copy * 10 - 5
    
      return -0.005 * (X_copy**4 - 16 * X_copy**2 + 5 * X_copy).sum(dim=-1) + 3
    
  2. 使用一个常数均值函数和 Matérn 2.5 核实现一个 GP 模型,输出范围由一个 gpytorch.kernels.ScaleKernel 对象实现。

    这个 GP 模型的类实现和之前大部分相同,只需要在 ARD 核中指定正确的维度数:

    class GPModel(
        gpytorch.models.ExactGP,
        botorch.models.gpytorch.GPyTorchModel,
        botorch.models.model.FantasizeMixin
    ):
        _num_outputs = 1
    
        def __init__(self, train_x, train_y, likelihood):
            super().__init__(train_x, train_y, likelihood)
            self.mean_module = gpytorch.means.ConstantMean()
            self.covar_module = gpytorch.kernels.ScaleKernel(
                gpytorch.kernels.MaternKernel(     ❶
                    nu=2.5,                        ❶
                    ard_num_dims=4                 ❶
                )                                  ❶
            )
    
        def forward(self, x):
            ...
    

    ❶ 四维的 Matérn 2.5 核

  3. 实现一个辅助函数,用于在给定的训练数据集上训练 GP。

    我们可以直接从本章其他笔记本(即 02 - Exercise 1.ipynb)中复制相同的辅助函数 fit_gp_model(),因为我们不需要在此辅助函数中进行修改。

  4. 定义优化问题的设置。

    我们先定义搜索空间的边界:

    lb = 0
    ub = 1
    
    bounds = torch.tensor([[lb] * 4, [ub] * 4], dtype=torch.float)
    

    然后我们指定可以进行的查询数量、批次大小和每个策略要重复的实验次数:

    num_experiments = 5
    
    num_queries = 100
    batch_size = 5
    num_iters = num_queries // batch_size
    
  5. 运行本章学习的每个批处理 BayesOpt 策略在先前实现的目标函数上。

    我们首先使用这段代码实现优化循环和重复每个策略实验的外部循环。具体来说,对于每个单独的实验,我们随机在搜索空间内取一个数据点,然后运行每个 BayesOpt 策略,直到我们用完查询次数。下一步我们将看到每个策略是如何定义的:

    incumbents = torch.zeros((num_experiments, num_iters))
    
    pbar = tqdm(total=num_experiments * num_iters)
    for exp in range(num_experiments):
        torch.manual_seed(exp)                       ❶
        train_x = bounds[0] + (bounds[1] -           ❶
        ➥bounds[0]) * torch.rand(1, 4)              ❶
        train_y = flight_utility(train_x)            ❶
    
        for i in range(num_iters):
            incumbents[exp, i] = train_y.max()       ❷
    
            model, likelihood = fit_gp_model
            ➥(train_x, train_y)                     ❷
    
            ...                                      ❸
    
            next_y = flight_utility(next_x)          ❹
    
            train_x = torch.cat([train_x, next_x])   ❹
            train_y = torch.cat([train_y, next_y])   ❹
    
            pbar.update()
    

    ❶ 随机初始化训练数据

    ❷ 跟踪优化进展并更新预测模型

    ❸ 定义策略并查找下一个要查询的批次

    ❹ 查询策略推荐的点并更新训练数据

    对于 PoI 策略,我们使用以下代码:

    policy = botorch.acquisition.monte_carlo.qProbabilityOfImprovement(
        model, best_f=train_y.max()
    )
    

    对于 EI 策略,我们使用以下代码:

    policy = botorch.acquisition.monte_carlo.qExpectedImprovement(
        model, best_f=train_y.max()
    )
    

    对于 UCB 策略,我们使用以下代码:

    policy = botorch.acquisition.monte_carlo.qUpperConfidenceBound(
        model, beta=2
    )
    

    然后可以使用以下代码对这三个策略进行优化:

    next_x, acq_val = botorch.optim.optimize_acqf(
        policy,
        bounds=bounds,
        q=batch_size,
        num_restarts=100,
        raw_samples=200,
    )
    

    否则,对于 TS 或 MES,我们需要先定义 Sobol 序列:

    sobol = torch.quasirandom.SobolEngine(4, scramble=True)    ❶
    candidate_x = sobol.draw(5000)
    candidate_x = (bounds[1] - bounds[0]) * candidate_x + bounds[0]
    

    ❶ 指定维度数为 4

    对于 TS 策略,我们使用以下代码:

    ts = botorch.generation.MaxPosteriorSampling(model, replacement=False)
    next_x = ts(candidate_x, num_samples=batch_size)
    

    对于 MES,我们使用以下代码来实现循环优化,其中使用了辅助函数 optimize_acqf_cyclic()。请注意,我们指定循环优化的最大迭代次数为 5:

    policy = botorch.acquisition.max_value_entropy_search.qMaxValueEntropy(
        model, candidate_x
    )
    
    next_x, acq_val = botorch.optim.optimize_acqf_cyclic(
        policy,
        bounds=bounds,
        q=batch_size,
        num_restarts=40,
        raw_samples=100,
        cyclic_options={"maxiter": 5}    ❶
    )
    

    ❶ 指定循环优化的最大迭代次数

  6. 绘制我们运行的 BayesOpt 策略的优化进展并观察其性能。

    图 A.14 显示了我们实现的策略得到的优化结果。我们看到大多数策略是可比的,除了 TS;批量 PoI 稍微领先一点。

    图 A.14 各种 BayesOpt 策略在飞机设计优化示例中的进展。大多数策略是可比的,除了 TS。

A.7 第八章:通过受限优化满足额外约束

本章有两个练习:

  1. 第一个验证我们从 BoTorch 对受限 EI 策略的实现得到的结果是否与常规 EI 分数和可行性概率的乘积相同。

  2. 第二部分向我们展示了如何在一个四维气动结构优化问题上运行受限 BayesOpt。

A.7.1 练习 1:受限 EI 的手动计算

受限 EI 策略的获取分数是 EI 分数和可行性概率的乘积。虽然 BoTorch 的 ConstrainedExpectedImprovement 类提供了受限 EI 分数的实现,但实际上我们可以手动执行计算。在这个练习中,我们探索这种手动计算,并将我们的结果与 ConstrainedExpectedImprovement 类的结果进行验证。此练习的解决方案在 CH08/02 - Exercise 1.ipynb 笔记本中,并可以解释如下:

  1. 重新创建 CH08/01 - Constrained optimization.ipynb 中使用的受限 BayesOpt 问题,包括目标函数、成本函数、GP 实现以及在一些训练数据上训练 GP 的辅助函数 fit_gp_model()

  2. 创建一个 PyTorch 张量,它是在 -5 到 5 之间的密集网格。这个张量将作为我们的测试集。我们使用 torch.linspace() 来创建一个密集网格:

    lb = -5
    ub = 5
    
    xs = torch.linspace(lb, ub, 201)
    
  3. 通过从我们的搜索空间中随机抽样三个数据点(在 -5 到 5 之间)来创建一个玩具训练数据集,并在这些点上评估目标和成本函数。我们使用 torch.rand() 在 0 和 1 之间随机采样,然后将样本缩放到我们的搜索空间:

    n = 3
    torch.manual_seed(0)                                           ❶
    train_x = bounds[0] + (bounds[1] - bounds[0]) * torch.rand(n)  ❷
    
    train_utility = objective(train_x)
    train_cost = cost(train_x)
    

    ❶ 为了可重现性而固定种子

    ❷ 在 0 和 1 之间进行采样,然后将样本缩放到我们的搜索空间

  4. 使用辅助函数 fit_gp_model() 在目标函数数据和成本函数数据上训练一个 GP。

    utility_model, utility_likelihood = fit_gp_model(   ❶
        train_x.unsqueeze(-1), train_utility            ❶
    )                                                   ❶
    
    cost_model, cost_likelihood = fit_gp_model(         ❷
        train_x.unsqueeze(-1), train_cost               ❷
    )                                                   ❷
    

    ❶ 在目标函数数据上训练一个 GP

    ❷ 在成本函数的数据上训练一个 GP

  5. 使用在成本函数数据上训练的 GP 来计算测试集中每个点的可行性概率。

    我们首先计算成本 GP 在我们的测试集上的预测分布:

    with torch.no_grad():
        cost_pred_dist = cost_likelihood(cost_model(xs))
        cost_pred_mean = cost_pred_dist.mean
        cost_pred_lower, cost_pred_upper = \
            cost_pred_dist.confidence_region()
    

    然后,我们初始化一个正态分布对象,其均值和标准差对应于 cost_pred_dist 的均值和标准差:

    normal = torch.distributions.Normal(cost_pred_mean, cost_pred_dist.stddev)
    

    最后,我们在这个对象上调用 cdf() 方法来计算可行性的概率。这个方法所取的参数是我们成本约束的上限,即 0:

    feasible_prob = normal.cdf(torch.zeros(1))
    
  6. 初始化常规 EI 策略,其中 model 参数是在目标函数数据上训练的 GP,而 best_f 参数是当前的可行入围者。

    我们使用 train_utility[train_cost <= 0].max() 计算当前的可行入围者:

    ei = botorch.acquisition.analytic.ExpectedImprovement(
        model=utility_model,
        best_f=train_utility[train_cost <= 0].max(),
    )
    

    然后,通过在 xs[:, None, None] 上调用 EI 策略对象来计算 EI 分数,该对象是为确保其形状适当而重新塑造的测试密集网格:

    with torch.no_grad():
        ei_score = ei(xs[:, None, None])
    
  7. 初始化受限 EI 策略,并为测试集中的每个点计算受限 EI 分数:

    constrained_ei = botorch.acquisition.analytic.ConstrainedExpectedImprovement(
        model=botorch.models.model_list_gp_regression.ModelListGP(
            utility_model, cost_model
        ),
        best_f=train_utility[train_cost <= 0].max(),
        objective_index=0,
        constraints={1: [None, 0]}
    )
    

    我们还使用重塑后的测试集计算受限 EI 分数:

    with torch.no_grad():
        constrained_ei_score = constrained_ei(xs[:, None, None])
    
  8. 计算 EI 分数和可行性概率的乘积,并验证这种手动计算是否与 BoTorch 的实现结果相同。 运行断言以确保所有相应的术语匹配:

    assert torch.isclose(
        ei_score * feasible_prob, constrained_ei_score, atol=1e-3
    ).all()
    
  9. 在图中绘制 EI 分数和受限 EI 分数,并直观地验证前者始终大于或等于后者。 证明这是正确的。

    我们如下绘制迄今为止计算的分数:

    plt.plot(xs, ei_score, label="EI")
    plt.plot(xs, constrained_ei_score, label="BoTorch constrained EI")
    

    此代码生成图 A.15,显示 EI 分数确实始终至少等于受限 EI 分数。

    图 A.15 EI 的获取分数(实线)和受限 EI(虚线)。 前者始终大于或等于后者。

    我们可以通过注意到受限 EI 分数等于正常 EI 分数乘以可行性概率来数学证明这一点。 可行性概率始终最大为 1,因此 EI 分数始终大于或等于受限 EI 分数。

A.7.2 练习 2:飞机设计的受限优化

在这个练习中,我们使用第七章练习 2 中的飞机效用目标函数来解决一个受限制的优化问题。 这个过程允许我们在一个高维问题上运行受限 BayesOpt,在这个问题中,不明显的是可行性最优解在哪里。 这个练习的解决方案包含在 CH08/03 - Exercise 2.ipynb 笔记本中:

  1. 重新创建在 CH07/04 - Exercise 2.ipynb 笔记本中使用的 BayesOpt 问题,包括名为 flight_utility() 的飞机效用目标函数、我们搜索空间的边界(四维单位超立方体)、GP 实现以及在一些训练数据上训练 GP 的辅助函数 fit_gp_model()

  2. 实现以下成本函数,模拟制造由四维输入指定的飞机设计的成本:

    def flight_cost(X):
      X = X * 20 - 10
    
      part1 = (X[..., 0] - 1) ** 2
    
      i = X.new(range(2, 5))
      part2 = torch.sum(i * (2.0 * X[..., 1:] ** 2 - X[..., :-1]) ** 2, 
      ➥dim=-1)
    
      return -(part1 + part2) / 100_000 + 2
    
  3. 我们的目标是在遵循成本小于或等于 0 的约束条件的情况下最大化目标函数 flight_utility()

    为此,我们将 BayesOpt 策略每个实验的查询次数设置为 50,并指定每个策略需要运行 10 次重复实验:

    num_queries = 50
    num_repeats = 10
    

    如果找不到可行解,则默认值量化优化进度应设置为-2。

    default_value = -2
    feasible_incumbents = torch.ones((num_repeats, num_queries)) * default_value
    
  4. 在此问题上运行受限 EI 策略以及常规 EI 策略;可视化并比较它们的平均进展(以及误差线)。

    我们以与第八章相同的方式实现了受限 EI 策略,其中我们将best_f设置为当前可行的最优解(如果找到了可行解)或默认值-2(如果没有找到可行解)。我们的模型列表包含目标 GP,其索引为 0,和成本 GP,其索引为 1:

    if (train_cost <= 0).any():                                      ❶
        best_f = train_utility[train_cost <= 0].max()                ❶
    else:                                                            ❶
        best_f = torch.tensor(default_value)                         ❶
    
    policy = botorch.acquisition.analytic.ConstrainedExpectedImprovement(
        model=botorch.models.model_list_gp_regression.ModelListGP(   ❷
            utility_model, cost_model                                ❷
        ),                                                           ❷
        best_f=best_f,
        objective_index=0,                                           ❸
        constraints={1: [None, 0]}                                   ❹
    )
    

    ❶ 找到当前最优解的适当值

    ❷ GP 模型列表

    ❸ 目标模型的索引

    ❹ 约束模型的索引和下限和上限

    我们如下实现常规 EI 策略:

    policy = botorch.acquisition.analytic.ExpectedImprovement(
       model=utility_model,
       best_f=train_utility.max(),
    )
    

    图 A.16 显示了我们实现的两个先前策略获得的优化结果。我们看到,受限 EI 通过考虑我们施加的成本约束完全支配了常规 EI 策略。

    图 A.16 各种贝叶斯优化策略在受限飞机设计优化示例中所取得的进展。与常规 EI 相比,受限变体平均找到更可行的解决方案。

A.8 第九章:用多信度优化平衡效用和成本

本章有两个练习:

  1. 练习 1 介绍了跨多个实验测量和可视化优化策略平均性能的过程。

  2. 练习 2 将我们所知的优化策略应用于具有三个可查询函数的二维问题。

A.8.1 练习 1:在多信度优化中可视化平均性能

在本练习中,我们多次运行优化循环,并学习如何取得平均性能以获得更全面的比较:

  1. 复制 CH09/03 - 测量性能.ipynb 笔记本中的问题设置和多信度优化循环,并添加另一个变量,表示我们要运行的实验次数(默认为 10 次)。

  2. 为了方便重复实验,在优化循环代码中添加一个外部循环。这应该是一个具有 10 次迭代的for循环,每次生成一个不同的随机观察值:

    num_repeats = 10                                   ❶
    
    for trial in range(num_repeats):
        torch.manual_seed(trial)                       ❷
        train_x = bounds[0] + (bounds[1] - bounds[0])  ❷
        ➥* torch.rand(1, 1)                           ❷
        train_x = torch.cat(                           ❷
            [train_x, torch.ones_like(train_x)         ❷
            ➥* fidelities[0]], dim=1                  ❷
        )                                              ❷
        train_y = evaluate_all_functions(train_x)      ❷
    
        current_budget = 0                             ❸
        while current_budget < budget_limit:           ❸
            ...                                        ❸
    

    ❶ 重复实验 10 次

    ❷ 生成特定于当前迭代的随机初始训练集

    ❸ 内循环,直到我们耗尽预算

  3. 将变量recommendationsspent_budget的每个变量都设为一个列表的列表,其中每个内部列表跟踪单个实验的优化性能。我们将上一步中嵌套循环的代码添加如下所示:

    num_repeats = 10
    recommendations = []                               ❶
    spent_budget = []                                  ❶
    
    for trial in range(num_repeats):
        torch.manual_seed(trial)
        train_x = bounds[0] + (bounds[1] - bounds[0]) * torch.rand(1, 1)
        train_x = torch.cat(
            [train_x, torch.ones_like(train_x) * fidelities[0]], dim=1
        )
        train_y = evaluate_all_functions(train_x)
    
        current_budget = 0
        recommendations.append([])                     ❷
        spent_budget.append([])                        ❷
    
        while current_budget < budget_limit:
            ...
    
            rec_x = get_final_recommendation(model)    ❸
            recommendations[-1].append                 ❸
            ➥(evaluate_all_functions(rec_x).item())   ❸
            spent_budget[-1].append(current_budget)    ❸
    
            ...
    

    ❶ 每个变量都是一个(当前为空的)列表的列表。

    ❷ 向每个列表的列表添加一个空列表,以供下一个实验使用

    ❸ 将优化进度统计信息添加到每个变量的最新列表中

  4. 在我们的优化问题上运行多保真度 MES 策略及其单保真度版本。

  5. 我们首先制作正则网格和当前空白的插值推荐值,稍后我们将填写其中:

    xs = np.arange(budget_limit)
    interp_incumbents = np.empty((num_repeats, budget_limit))
    

    然后我们遍历 recommendations 中的每个列表(在我们的代码中重命名为 incumbents)和 spend_budget,计算线性插值,然后填写 interp_incumbents 中的值:

    for i, (tmp_incumbents, tmp_budget) in enumerate(
        zip(incumbents, spent_budget)
    ):
        interp_incumbents[i, :] = np.interp(xs, tmp_budget, tmp_incumbents)
    
  6. 使用线性插值值绘制我们运行的两个策略的平均性能和误差条,并比较它们的性能。比较可视化在图 A.17 中展示,我们可以看到多保真度 MES 策略大大优于其单保真度竞争对手。

    图 A.17 显示了在 10 次实验中单一保真度和多保真度 MES 策略在 Forrester 函数上的平均优化进展。多保真度策略大大优于单保真度策略。

  7. 绘制线性插值曲线,代表各个运行的优化进展,以及平均性能和误差条。比较可视化在图 A.18 中呈现。事实上,我们每次运行的优化进展,由最大后验平均推荐值来衡量,并不是单调递增的。

    图 A.18 线性插值曲线代表了在 10 次实验中各个运行的优化进展。我们每次运行的优化进展,由最大后验平均推荐值来衡量,并不是单调递增的。

A.8.2 练习 2:使用多个低保真度近似进行多保真度优化

本练习向我们展示了我们的多保真度最大值熵搜索策略可以在多个低保真度函数之间平衡。解决方案,可以在 CH09/05 - Exercise 2.ipynb 笔记本中找到,解释如下:

  1. 实现目标函数。该步骤的代码已在说明中提供。

  2. 将我们的搜索空间的边界定义为单位正方形:

    bounds = torch.tensor([[0.0] * 2, [1.0] * 2])
    
  3. 声明存储我们可以查询的不同函数的相关值的 fidelities 变量:

    fidelities = torch.tensor([0.1, 0.3, 1.0])
    bounds_full = torch.cat(
        [
            bounds,
            torch.tensor([fidelities.min(), fidelities.max()]).unsqueeze(-1)
        ],
        dim=1
    )
    
  4. 将线性成本模型的固定成本设置为 0.2,权重设置为 1:

    from botorch.models.cost import AffineFidelityCostModel
    
    cost_model = AffineFidelityCostModel(fixed_cost=0.2)
    

    将每次实验中我们的预算限制设置为 10,并且重复实验的次数也设置为 10:

    budget_limit = 10
    num_repeats = 10
    
  5. 从 Sobol 序列中绘制的候选者数量设置为 5,000,并在使用辅助函数优化给定策略的收购分数时,使用 100 次重启和 500 次原始样本:

    num_samples = 5000
    
    num_restarts = 100
    raw_samples = 500
    
  6. 重新定义辅助函数 get_final_recommendation,以找到后验平均值最大化器:

    from botorch.acquisition.fixed_feature import 
    ➥FixedFeatureAcquisitionFunction
    from botorch.acquisition import PosteriorMean
    from botorch.optim.optimize import optimize_acqf, optimize_acqf_mixed
    
    def get_final_recommendation(model):
        post_mean_policy = FixedFeatureAcquisitionFunction(
            acq_function=PosteriorMean(model),
            d=3,                ❶
            columns=[2],        ❶
            values=[1],
        )
    
        final_x, _ = optimize_acqf(
            post_mean_policy,
            bounds=bounds,
            q=1,
            num_restarts=num_restarts,
            raw_samples=raw_samples,
        )
    
        return torch.cat([final_x, torch.ones(1, 1)], dim=1)
    

    ❶ 必要的更改

  7. 在我们的优化问题上运行多保真度 MES 策略及其单保真度版本,并使用练习 1 中描述的方法绘制每个策略的平均优化进展和误差条。图 A.19 显示了两种策略之间的比较。

    图 A.19 显示了单一和多层次 MES 策略在 Branin 函数上的平均优化进展。多层次策略再次优于单一层次策略。

A.9 第十一章:同时优化多个目标

在本练习中,我们将我们学到的多目标优化技术应用于优化飞机的气动结构设计问题。这个练习让我们能够观察多维问题中 Expected Hypervolume Improvement (EHVI) 策略的表现:

  1. 我们按照以下方式复制了目标函数的代码:

    def objective1(X):                                                  ❶
      X_copy = X.detach().clone()                                       ❶
      X_copy[:, [2, 3]] = 1 - X_copy[:, [2, 3]]                         ❶
      X_copy = X_copy * 10 - 5                                          ❶
      return (                                                          ❶
        -0.005                                                          ❶
        * (X_copy ** 4 - 16 * X_copy ** 2 + 5 * X_copy)                 ❶
        ➥.sum(dim=-1)                                                  ❶
          + 3                                                           ❶
      )                                                                 ❶
    
    def objective2(X):                                                  ❷
      X = X * 20 - 10                                                   ❷
      part1 = (X[..., 0] - 1) ** 2                                      ❷
      i = X.new(range(2, 5))                                            ❷
      part2 = torch.sum(i * (2.0 * X[..., 1:] ** 2 - X[..., :-1]) ** 2, ❷
      ➥ dim=-1)                                                        ❷
      return (part1 + part2) / 100_000 - 2                              ❷
    

    ❶ 第一个目标函数

    ❷ 第二个目标函数,来自第八章练习 2 中的代码取反

  2. 我们按照以下方式实现辅助函数:

    def joint_objective(X):
        return torch.vstack(
            [
                objective1(X).flatten(),
                objective2(X).flatten(),
            ]
        ).transpose(-1, -2)
    
  3. 我们声明搜索空间的边界:

    bounds = torch.tensor([[0.0] * 4, [1.0] * 4])
    
  4. 我们声明参考点:

    ref_point = torch.tensor([-1.5, -2.0])
    
  5. 类实现和辅助函数可以使用与 CH08/03 - 练习 2.ipynb 中相同的代码实现。

  6. 我们设置实验设置:

    num_queries = 50
    num_repeats = 10
    
  7. 我们按照 CH11/02 - 多目标 BayesOpt loop.ipynb 中相同的方式实现了两种 BayesOpt 策略。图 A.20 展示了两种策略在 10 个实验中的综合表现。EHVI 策略再次优于交替 EI 策略。

    图 A.20 显示两个贝叶斯优化策略根据查询次数的平均超体积和误差棒。EHVI 策略始终优于交替 EI 策略。

A.10 第十二章:将高斯过程扩展到大数据集

本练习展示了在真实数据集加利福尼亚州房价上从普通 GP 模型转换为 VGP 模型时的效率提升。我们的目标是观察 VGP 在真实世界环境中的计算优势。

完成以下步骤:

  1. 我们使用 Pandas 库读入数据集:

    import pandas as pd
    
    df = pd.read_csv("../data/housing.csv")
    

    将 Pandas dataframe 读入后,应该与图 A.21 中的输出类似。

    图 A.21 显示了房价数据集作为 Pandas dataframe。这是本练习的训练集。

  2. 我们创建散点图如下所示:

    plt.figure(figsize=(8, 6))
    plt.scatter(df.longitude, df.latitude, c=np.log(df.median_house_value))
    plt.colorbar();
    

    可视化应该类似于图 A.22。

    图 A.22 房价数据集显示为散点图

  3. 为了提取我们的训练特征,我们使用 torch.from_numpy() 方法将 NumPy 数组转换为 PyTorch 张量:

    train_x = torch.from_numpy(df.drop(["median_house_value"], axis=1).values)
    
  4. 我们同样对房价的对数进行了这样的操作,这是我们的训练标签:

    train_y = torch.from_numpy(
        df.median_house_value.values
    ).log().to(train_x.dtype)
    
  5. 我们将训练标签 train_y 标准化如下:

    train_y = (train_y - train_y.mean()) / train_y.std()
    
  6. 我们如下实现 GP 模型:

    class GPModel(gpytorch.models.ExactGP):
        def __init__(self, train_x, train_y, likelihood):
            super().__init__(train_x, train_y, likelihood)
            self.mean_module = gpytorch.means.ConstantMean()   ❶
            self.covar_module = gpytorch.kernels.ScaleKernel(  ❷
                gpytorch.kernels.MaternKernel(
                    nu=2.5,                                    ❷
                    ard_num_dims=train_x.shape[1]              ❷
                )                                              ❷
            )                                                  ❷
    
        def forward(self, x):
            mean_x = self.mean_module(x)
            covar_x = self.covar_module(x)
            return gpytorch.distributions.MultivariateNormal(mean_x, covar_x)
    

    ❶ 常数均值函数

    ❷ 具有输出比例的 ARD Matern 5/2 核函数

  7. 使用以下代码制作一个噪声至少为 0.1 的似然函数:

    likelihood = gpytorch.likelihoods.GaussianLikelihood(
        noise_constraint=gpytorch.constraints
        ➥.GreaterThan(1e-1)                    ❶
    )
    

    ❶ 约束强制噪声至少为 0.1。

  8. 我们使用梯度下降训练先前实现的 GP 模型如下:

    model = GPModel(train_x, train_y, likelihood)
    
    optimizer = torch.optim.Adam(model.parameters(),
    ➥lr=0.01)                                        ❶
    mll = gpytorch.mlls.ExactMarginalLogLikelihood
    ➥(likelihood, model)                             ❷
    
    model.train()                                     ❸
    likelihood.train()                                ❸
    
    for i in tqdm(range(10)):
        optimizer.zero_grad()
    
        output = model(train_x)
        loss = -mll(output, train_y)
    
        loss.backward()
        optimizer.step()
    

    ❶ 梯度下降优化器 Adam

    ❷ (负)边际对数似然损失函数

    ❸ 启用训练模式

    在 MacBook 上,总训练时间为 24 秒。

  9. 我们按以下步骤实现了 VGP 模型:

    class ApproximateGPModel(gpytorch.models.ApproximateGP):
      def __init__(self, inducing_points):
        variational_distribution =                    ❶
        ➥gpytorch.variational                        ❶
        ➥.NaturalVariationalDistribution(            ❶
            inducing_points.size(0)                   ❶
        )                                             ❶
        variational_strategy = gpytorch.variational   ❶
        ➥.VariationalStrategy(                       ❶
            self,                                     ❶
            inducing_points,                          ❶
            variational_distribution,                 ❶
            learn_inducing_locations=True,            ❶
        )                                             ❶
        super().__init__(variational_strategy)
        self.mean_module = gpytorch.means.ConstantMean()
        self.covar_module = gpytorch.kernels.ScaleKernel(
            gpytorch.kernels.MaternKernel(
                nu=2.5,
                ard_num_dims=inducing_points.shape[1]
            )
        )
    
      def forward(self, x):
        ...                                           ❷
    

    ❶ 变分参数

    ❷ 与 GP 相同

  10. 这个 VGP 是这样训练的:

    num_datapoints = 100                             ❶
    torch.manual_seed(0)                             ❶
    model = ApproximateGPModel(                      ❶
      train_x[torch.randint(train_x.shape[0],        ❶
      ➥(num_datapoints,)), :]                       ❶
    )                                                ❶
    
    likelihood = gpytorch.likelihoods.GaussianLikelihood(
      noise_constraint=gpytorch.constraints.GreaterThan(1e-1)
    )
    
    train_dataset = torch.utils.data.Tensordataset
    ➥(train_x, train_y)                             ❷
    train_loader = torch.utils.data.DataLoader(      ❷
      train_data set,                                ❷
      batch_size=100,                                ❷
      shuffle=True                                   ❷
    )                                                ❷
    
    ngd_optimizer = gpytorch.optim.NGD(              ❸
      model.variational_parameters(),                ❸
      ➥num_data=train_y.size(0), lr=0.1             ❸
    )                                                ❸
    hyperparam_optimizer = torch.optim.Adam(         ❹
      [{"params": model.parameters()}, {"params":    ❹
      ➥likelihood.parameters()}],                   ❹
      lr=0.01                                        ❹
    )                                                ❹
    
    mll = gpytorch.mlls.VariationalELBO(
      likelihood, model, num_data=train_y.size(0)
    )
    
    model.train()
    likelihood.train()
    
    for i in tqdm(range(10)):
      for x_batch, y_batch in train_loader:
        ngd_optimizer.zero_grad()
    
        output = model(x_batch)
        loss = -mll(output, y_batch)
    
        loss.backward()
    
        ngd_optimizer.step()
        hyperparam_optimizer.step()
    

    ❶ 随机选择 100 个点作为初始感兴趣点

    ❷ 准备小批量数据

    ❸ 对变分参数使用自然梯度下降

    ❹ 使用 Adam 更新其他参数

  11. 在同一台 MacBook 上,训练时间缩短到 6 秒,速度提升了 400%。

解决方案包含在 CH12/02 - Exercise.ipynb 笔记本中。

posted @ 2024-05-02 22:34  绝不原创的飞龙  阅读(2)  评论(0编辑  收藏  举报