精通-Python-强化学习-全-

精通 Python 强化学习(全)

原文:annas-archive.org/md5/39faf67615f2d59edcd86792ee1fcf06

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

强化学习(RL)是人工智能的一个领域,用于创建自学习的自主智能体。本书采取务实的方法,通过真实的商业和行业问题中的实际例子来教授你强化学习技巧。

从强化学习元素的概述开始,你将掌握马尔可夫链和马尔可夫决策过程(MDP),它们是建模强化学习问题的数学基础。接下来,你将学习蒙特卡罗方法和时间差分(TD)学习方法,它们被用于解决强化学习问题。然后,你将了解深度 Q 学习、策略梯度算法、演员-评论家方法、基于模型的方法和多智能体强化学习。随着学习的深入,你将探讨许多新颖的算法,并利用现代 Python 库实现高级功能。你还将了解如何实施强化学习来解决现实世界中的挑战,例如自主系统、供应链管理、游戏、金融、智慧城市和网络安全等领域的问题。最后,你将清楚了解该使用哪种方法以及何时使用,如何避免常见的陷阱,并克服在实施强化学习过程中遇到的挑战。

到本书的结尾,你将掌握如何训练和部署你自己的 RL 智能体,以解决强化学习问题。

本书适合谁阅读

本书适合有经验的机器学习从业者和深度学习研究人员,特别是那些希望在现实世界项目中实现高级强化学习概念的人。本书还将吸引那些希望通过自学习智能体解决复杂序列决策问题的强化学习专家。需要具备 Python 编程、机器学习基础知识和之前的强化学习经验。

本书内容概述

第一章,强化学习介绍,提供了强化学习的介绍,给出激励性的例子和成功故事,并探讨了强化学习在行业中的应用。接着,书中给出了强化学习概念的基本定义,并以软件和硬件设置部分作为总结。

第二章,多臂赌博机,介绍了一个相对简单的强化学习(RL)设定,即没有上下文的赌博机问题,这在工业界有着巨大的应用前景,是传统 A/B 测试的替代方法。本章还作为对一个非常基础的强化学习概念——探索与利用(exploration versus exploitation)的介绍。我们还通过四种不同的方法解决了一个原型在线广告案例。

第三章,上下文赌博者,通过为决策过程添加上下文并引入深度神经网络参与决策,使多臂赌博机问题(MABs)达到了一个更高级的层次。我们将来自美国人口普查的真实数据集应用于在线广告问题。最后,我们通过讨论赌博问题在工业和商业中的应用来结束本章内容。

第四章,马尔可夫决策过程的构建,构建了我们用来建模强化学习问题的数学理论。我们从马尔可夫链开始,描述状态的类型、遍历性、转移行为和稳态行为。接着我们讨论马尔可夫奖励过程和决策过程。在过程中,我们介绍了回报、折扣、策略和价值函数,以及贝尔曼最优性,这些都是强化学习理论中的关键概念,将在后续章节中频繁提及。最后,我们讨论了部分可观察马尔可夫决策过程。在整个章节中,我们使用一个网格世界的例子来说明这些概念。

第五章,解决强化学习问题,介绍了动态规划(DP)方法,这些方法是理解如何解决马尔可夫决策过程(MDP)问题的基础。介绍并说明了诸如策略评估、策略迭代和价值迭代等关键概念。整个章节通过解决一个库存补货问题来进行演示。最后,我们讨论了在现实世界例子中使用动态规划时遇到的问题。

第六章,大规模深度 Q 学习,提供了深度强化学习的介绍,并涵盖了从头到尾的深度 Q 学习。我们首先讨论为什么需要深度强化学习,然后介绍流行且可扩展的 RL 库——RLlib。在介绍完将要使用的案例研究(一个简单的、一个中等难度的以及一个视频游戏例子)之后,我们将从拟合 Q 迭代、DQN 到 Rainbow 构建深度 Q 学习方法。接着,我们将进入更高级的话题,讨论分布式 DQN(APEX)、连续 DQN,并讨论需要调整的重要超参数。对于经典的 DQN,你将使用 TensorFlow 实现;而对于 Rainbow,我们将使用 RLlib。

第七章,基于策略的方法,介绍了强化学习方法的第二大类:基于策略的方法。你将首先了解它们与其他方法的不同之处以及它们的必要性。接着,我们将深入探讨几种最先进的策略梯度和信赖域方法。最后,我们将讨论演员-评论员算法。我们主要依赖 RLlib 对这些算法的实现,并关注如何以及何时使用这些算法,而不是详细的实现细节。

第八章, 基于模型的方法,展示了基于模型的方法所做的假设以及它们相较于其他方法的优势。我们还讨论了著名的 AlphaGo Zero 背后的模型。本章的最后,我们通过一个使用基于模型算法的练习来总结这一章。章节内容包括手动和 RLlib 实现的结合使用。

第九章, 多智能体强化学习,为你提供了一个框架,用以建模多智能体 RL 问题,并介绍了 MADDPG 来解决此类问题。章节中使用了 RLlib 的 MADDPG 实现。

第十章, 机器教学,讨论了机器教学方法,如何将复杂问题分解为更小的部分,并使其可解。该方法对于许多现实生活中的问题是必需的,你将学到一些实用的技巧,如何设计一个 RL 模型,并超越算法选择来解决 RL 问题。

第十一章, 泛化与领域随机化,讨论了部分可观察性和 sim2real 差距为何成为问题,并介绍了如何通过使用 LSTM-like 模型和领域随机化来克服这些问题。

第十二章, 元强化学习,介绍了使我们能够为多个任务使用单一模型的方法。由于样本效率是 RL 中的一个主要问题,本章让你了解了 RL 中的一个非常重要的未来方向。

第十三章, 其他高级主题,介绍了前沿的 RL 研究。到目前为止讨论的许多方法都有一定的假设和局限性。本章讨论的主题解决了这些局限性,并给出了如何克服它们的思路。在本章结束时,你将了解到当你遇到我们在前面章节中讨论的算法的局限性时,应该关注哪些方法。

第十四章, 自主系统,探讨了 RL 在创建现实世界自主系统方面的潜力。我们涵盖了成功案例和自主机器人以及自动驾驶汽车的样本问题。

第十五章, 供应链管理,让你亲身体验库存规划和箱子装配问题。我们将这些问题建模为 RL 问题,并解决一些样本案例。

第十六章, 营销、个性化和金融,涵盖了 RL 在营销、广告、推荐系统和金融中的应用。本章将帮助你广泛了解 RL 在商业中的应用,以及其机会与局限性。在本章中,我们还讨论了上下文多臂赌博机问题的例子。

第十七章,智慧城市与网络安全,涵盖了智慧城市和网络安全领域的示例问题,如交通控制、服务提供监管和入侵检测。我们还讨论了多智能体方法如何在这些应用中发挥作用。

第十八章,强化学习的挑战与未来方向,详细探讨了这些挑战是什么以及最前沿的研究提出了如何克服它们的建议。本章教你如何评估 RL 方法在特定问题中的可行性。

为了充分利用本书

如果你使用的是本书的电子版,建议你手动输入代码,或者通过 GitHub 仓库(下节中会提供链接)访问代码。这样可以避免因复制粘贴代码而导致的潜在错误。

下载示例代码文件

你可以从www.packt.com账户中下载本书的示例代码文件。如果你在其他地方购买了本书,可以访问www.packtpub.com/support,注册后将文件直接发送给你。

你可以按照以下步骤下载代码文件:

  1. 登录或注册账号,访问www.packt.com。

  2. 选择支持标签。

  3. 点击代码下载。

  4. 在搜索框中输入书名,并按照屏幕上的指示操作。

下载完成后,请确保使用以下最新版本的工具解压或提取文件夹:

  • Windows 下的 WinRAR/7-Zip

  • Mac 下的 Zipeg/iZip/UnRarX

  • Linux 下的 7-Zip/PeaZip

本书的代码包也托管在 GitHub 上,地址为github.com/PacktPublishing/Mastering-Reinforcement-Learning-with-Python。如果代码有更新,更新内容将会同步到现有的 GitHub 仓库中。

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

下载彩色图像

我们还提供了一个 PDF 文件,其中包含本书中使用的截图/图表的彩色图像。你可以在此下载:static.packt-cdn.com/downloads/9781838644147_ColorImages.pdf。

使用的约定

本书中使用了一些文本约定。

文本中的代码:表示文本中的代码词、数据库表名、文件夹名称、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 用户名。例如:“例如,使用sudo apt-get install nvidia-modprobe命令安装 NVIDIA Modprobe。”

代码块如下所示:

ug = UserGenerator()
visualize_bandits(ug)

当我们希望引起您对代码块的特定部分的注意时,相关行或条目将以粗体显示:

./run_local.sh [Game] [Agent] [Num. actors] 
./run_local.sh atari r2d2 4

提示或重要说明

Appear like this.

联系我们

我们始终欢迎读者的反馈。

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

勘误:尽管我们已经尽一切努力确保内容的准确性,但错误确实会发生。如果您发现了本书中的错误,请向我们报告。请访问 www.packtpub.com/support/errata,选择您的书籍,点击勘误提交表单链接,并输入详细信息。

盗版:如果您在互联网上发现我们作品的任何形式的非法拷贝,请向我们提供位置地址或网站名称。请通过链接 copyright@packt.com 联系我们。

如果您有兴趣成为作者:如果您对某个您擅长的主题感兴趣,并且有意撰写或贡献一本书,请访问 authors.packtpub.com。

评论

请留下评论。在阅读和使用本书后,为什么不在购买它的网站上留下评论呢?潜在的读者可以看到并使用您的客观意见来做购买决策,我们在 Packt 可以了解您对我们产品的看法,而我们的作者可以看到您对他们书籍的反馈。谢谢!

有关 Packt 的更多信息,请访问 packt.com。

第一部分:强化学习基础

本部分介绍了强化学习(RL)的必要背景,为后续章节的高级内容做准备。包括定义、数学基础和强化学习解决方法的概述。

本节包含以下章节:

  • 第一章,强化学习简介

  • 第二章,多臂老丨虎丨机

  • 第三章,上下文老丨虎丨机

  • 第四章,马尔可夫决策过程的构建

  • 第五章,解决强化学习问题

第一章:第一章:强化学习简介

强化学习(RL)旨在创建能够在复杂和不确定的环境中做出决策的人工智能(AI)代理,其目标是最大化代理的长期利益。这些代理通过与环境互动来学习如何做到这一点,这模仿了我们作为人类通过经验学习的方式。因此,强化学习具有极其广泛和可适应的应用领域,具有颠覆和革命全球行业的潜力。

本书将为你提供关于这一领域的高级理解。我们将深入探讨一些你可能已经知道的算法背后的理论,并覆盖最前沿的强化学习。此外,本书是一本实践性很强的书。你将看到来自现实行业问题的示例,并在过程中学习到专家技巧。到书的最后,你将能够使用 Python 建模并解决你自己的序列决策问题。

所以,让我们从复习一下强化学习的概念开始,为接下来的高级内容做好准备。具体来说,本章将涵盖以下内容:

  • 为什么选择强化学习?

  • 机器学习的三种范式

  • 强化学习应用领域和成功案例

  • 强化学习问题的元素

  • 设置你的强化学习环境

为什么选择强化学习?

创建能够做出与人类相当或更优决策的智能机器是许多科学家和工程师的梦想,而这个梦想正逐渐变得触手可及。在过去七十年里,自从图灵测试以来,人工智能(AI)的研究和发展经历了过山车般的起伏。最初的期望值非常高;例如,20 世纪 60 年代,赫伯特·西蒙(后来获得诺贝尔经济学奖)预测,机器将在 20 年内能够完成所有人类能做的工作。正是这种兴奋感吸引了大量政府和企业的资金流入人工智能研究,但接着迎来的是巨大的失望和一个被称为“人工智能寒冬”的时期。几十年后,得益于计算、数据和算法的惊人进展,人类再次感到前所未有的兴奋,追求人工智能梦想的脚步比以往任何时候都更加坚定。

注意事项

如果你不熟悉艾伦·图灵在 1950 年关于人工智能基础的开创性工作,那么值得了解一下图灵测试: youtu.be/3wLqsRLvV-c。

人工智能的梦想无疑是宏伟的。毕竟,智能自主系统的潜力巨大。想想看,全球专科医生的数量是如何受到限制的。培养一位专科医生需要多年时间,并且要投入大量的智力和财力资源,而许多国家在这方面并不具备足够的水平。此外,即便经过多年的教育,专科医生也几乎不可能跟上自己领域中所有的科学进展,无法从世界各地成千上万的治疗结果中学习,并有效地将所有这些知识应用于实践。

相反,一个 AI 模型可以处理并从所有这些数据中学习,并将其与有关患者的丰富信息(如病史、实验室结果、表现症状、健康档案等)结合起来,做出诊断并建议治疗方案。这个模型可以服务于世界上最偏远的地区(只要有互联网连接和计算机),并指导当地的医疗人员进行治疗。毫无疑问,它将彻底改变国际医疗体系,并改善数百万人的生活。

注意

人工智能已经在改变医疗行业。在一篇最近的文章中,谷歌公布了其 AI 系统在乳腺癌预测中超越人类专家的成果,该系统使用了乳腺 X 光片(McKinney 等,2020)。微软正在与印度最大的医疗服务提供商之一合作,利用 AI 检测心脏疾病(Agrawal,2018)。IBM 的 Watson 临床试验匹配系统使用自然语言处理技术,从医疗数据库中为患者推荐潜在的治疗方案(youtu.be/grDWR7hMQQQ)。

在我们追求开发与人类水平相当或超越人类水平的 AI 系统的过程中——这一目标有些争议,称之为人工通用智能(AGI)——开发一种可以从自身经验中学习的模型是有意义的,而不必依赖于监督者。强化学习(RL)是使我们能够创造这种智能体的计算框架。为了更好地理解 RL 的价值,有必要将其与其他机器学习(ML)范式进行比较,我们将在接下来的部分进行探讨。

机器学习的三种范式

强化学习(RL)是机器学习(ML)中的一个独立范式,和监督学习(SL)以及无监督学习(UL)并列。它超越了其他两个范式所涉及的内容——例如感知、分类、回归和聚类——并做出决策。然而,更重要的是,RL 在实现这一点时,利用了监督学习和无监督学习的方法。因此,RL 是一个独立但与 SL 和 UL 紧密相关的领域,掌握它们是很重要的。

监督学习

有监督学习是学习一个数学函数,将一组输入映射到相应的输出/标签,并尽可能精确。其核心思想是,我们不知道生成输出的过程的动态,但我们尝试利用从中获得的数据来推测它。考虑以下示例:

  • 一个图像识别模型,它将自动驾驶汽车摄像头中的物体分类为行人、停车标志、卡车等。

  • 一个预测特定假期季节产品客户需求的预测模型,使用的是过去的销售数据。

确定如何在视觉上区分物体,或是什么因素导致客户需求某个产品,极其困难。因此,有监督学习模型从标记数据中推断这些规则。以下是它如何工作的关键要点:

  • 在训练过程中,模型从监督者(可以是人类专家或某个过程)提供的真实标签/输出中学习。

  • 在推理过程中,模型会根据输入预测输出可能是什么。

  • 模型使用函数逼近器来表示生成输出的过程的动态。

无监督学习

无监督学习算法识别数据中先前未知的模式。在使用这些模型时,我们可能有一定的预期结果,但不会给模型提供标签。考虑以下示例:

  • 识别自动驾驶汽车摄像头提供的图像中的同质区域。模型可能会根据图像中的纹理将天空、道路、建筑物等区分开来。

  • 根据销售量将每周销售数据分成三组。输出可能是销售量低、中、高的周次。

如你所见,这与有监督学习的工作方式非常不同,具体体现在以下几个方面:

  • 无监督学习模型不知道什么是真实标签,也没有标签来映射输入。它们只是识别数据中的不同模式。即使这样做了,例如,模型也不会意识到它将天空与道路分开,或将假期周与常规周区分开来。

  • 在推理过程中,模型会将输入聚类到它已识别的某个组中,同样,它并不知道这个组代表的是什么。

  • 函数逼近器,如神经网络,在某些无监督学习算法中被使用,但并不总是如此。

在重新引入有监督学习(SL)和无监督学习(UL)之后,我们将它们与强化学习(RL)进行比较。

强化学习

强化学习是一个框架,旨在通过试错法在不确定性下做出决策,以最大化长期收益。这些决策是按顺序做出的,早期的决策会影响后续将遇到的情况和收益。这使得强化学习与有监督学习(SL)和无监督学习(UL)有所不同,因为后者并不涉及任何决策。让我们重新回顾一下之前提供的示例,看看强化学习模型与有监督学习和无监督学习模型在它们尝试找出什么方面有何不同:

  • 对于自驾车来说,给定摄像头捕捉到的所有物体的类型和位置,以及道路上的车道边缘,模型可能会学到如何转动方向盘以及应该以何种车速通过前方的车辆,以尽可能安全、快速地通过。

  • 给定产品的历史销售数据以及将库存从供应商送到商店所需的时间,模型可能会学到何时以及订购多少单位库存,以便以高概率满足季节性客户需求,同时将库存和运输成本最小化。

正如你可能已经注意到的,RL 试图完成的任务与 SL 和 UL 单独处理的任务性质不同,且更为复杂。我们来详细阐述一下 RL 的不同之处:

  • RL 模型的输出是在特定情况下的决策,而不是预测或聚类。

  • 没有由监督者提供的“真实”决策来告诉模型在不同情况下理想的决策是什么。相反,模型通过反馈自己的经验和过去做出的决策来学习最佳决策。例如,通过反复试验,RL 模型会学到,超速超车可能会导致事故,而在假期前订购过多库存则会导致后期的库存过剩。

  • RL 模型通常使用 SL 模型的输出作为输入来做决策。例如,自驾车中图像识别模型的输出可以用来做驾驶决策。同样,预测模型的输出经常作为 RL 模型的输入,用来做库存补充决策。

  • 即便没有来自辅助模型的这种输入,RL 模型也会在隐式或显式的情况下预测其决策未来将导致的情况。

  • RL 利用了许多为 SL 和 UL 开发的方法,例如将各种类型的神经网络作为函数近似器。

那么,RL 与其他机器学习方法的不同之处在于,它是一个决策框架。然而,使其令人兴奋和强大的地方在于它与我们人类通过经验做决策的学习方式相似。试想一个学步的孩子如何学着用玩具积木搭建塔楼。通常,塔楼越高,孩子就越开心。每增加一块积木的高度都是一次成功,每次倒塌都是一次失败。他们很快发现,下一块积木越靠近下面积木的中心,塔楼就越稳定。当积木放得离边缘太近时,它更容易倒塌。这种经验得到了强化,经过练习,他们能将几块积木堆叠起来。他们意识到,早期积木的堆放方式创造了一个基础,决定了他们能建造多高的塔楼。由此,他们学会了。

当然,那个学步的小孩并不是从蓝图中学到这些建筑原理的。他们从失败和成功的共性中学习。塔楼高度的增加或倒塌为他们提供了反馈信号,基于这些信号他们调整了策略。从经验中学习,而非从蓝图中学习,这正是强化学习的核心。就像小孩发现哪些积木摆放的位置可以造出更高的塔一样,强化学习的智能体通过试错来识别出能带来最高长期回报的行动。这就是强化学习如此深刻的原因,它显然是人类的智慧体现。

在过去的几年里,许多令人惊叹的成功故事证明了强化学习的潜力。此外,强化学习正在改变许多行业。所以,在深入探讨强化学习的技术细节之前,让我们通过了解强化学习在实际中的应用来进一步激励自己。

强化学习的应用领域和成功故事

强化学习并不是一个新兴领域。在过去的七十年里,强化学习的许多基本思想是在动态规划和最优控制的背景下提出的。然而,得益于深度学习的突破和更强大的计算资源,强化学习的成功实现最近才迎来了爆发。在这一节中,我们将讨论一些强化学习的应用领域以及一些著名的成功故事。我们将在接下来的章节中深入探讨这些实现背后的算法。

游戏

桌面游戏和视频游戏一直是强化学习的研究实验室,催生了许多著名的成功案例。游戏之所以成为良好的强化学习问题,原因如下:

  • 游戏本质上涉及到带有不确定性的顺序决策。

  • 它们以计算机软件的形式存在,使得强化学习模型可以灵活地与这些游戏互动,并生成数十亿的数据点用于训练。而且,训练过的强化学习模型也会在同样的计算环境中进行测试。这与许多物理过程不同,后者很难创建出准确且快速的模拟器。

  • 游戏中的自然基准是最优秀的人类玩家,这使得它成为人工智能与人类比较的一个充满吸引力的战场。

在本介绍之后,让我们来看看一些引人注目的强化学习(RL)工作,这些工作已经登上了头条。

TD-Gammon

第一个著名的强化学习实现是 TD-Gammon,这是一个学习如何玩超级人类水平的西洋双陆棋(backgammon)的模型——这是一种具有 1,020 种可能配置的双人棋盘游戏。该模型由 IBM 研究院的 Gerald Tesauro 于 1992 年开发。TD-Gammon 的成功引起了当时西洋双陆棋社区的巨大关注,它为人类带来了许多新颖的策略。该模型中使用的许多方法(例如时序差分、自对弈和神经网络的应用)至今仍是现代强化学习实现的核心。

在 Atari 游戏中超越人类表现

强化学习(RL)领域最具影响力和开创性的研究之一是 2015 年由 Volodymry Mnih 及其在 Google DeepMind 的同事们完成的工作。研究人员训练了 RL 代理,使其仅通过屏幕输入和游戏得分,利用深度神经网络学习如何玩 Atari 游戏,超越人类表现,而无需任何手工设计或特定于游戏的特征。他们将这个算法命名为深度 Q 网络(DQN),它是今天最流行的 RL 算法之一。

击败围棋、国际象棋和将棋的世界冠军

也许为 RL 带来最多声誉的 RL 实现是 Google DeepMind 的 AlphaGo。它是第一个在 2015 年击败职业围棋选手的计算机程序,后来在 2016 年击败了世界冠军李世石。这一故事后来被拍成了同名纪录片。AlphaGo 模型是通过人类专家棋局数据以及通过自我对弈与 RL 训练出来的。后来的版本 AlphaGo Zero 以 100-0 的成绩击败了原版 AlphaGo,而这个模型只通过自我对弈训练,并且没有任何人类知识输入。最后,公司在 2018 年发布了 AlphaZero,它能够学习国际象棋、将棋(日本象棋)和围棋,成为每个游戏历史上最强的玩家,且只了解游戏规则,没有任何关于游戏的先验信息。AlphaZero 在仅数小时的张量处理单元(TPU)训练后达到了这个表现。AlphaZero 的非常规策略得到了世界著名棋手的赞扬,比如加里·卡斯帕罗夫(国际象棋)和羽生善治(将棋)。

在复杂战略游戏中的胜利

RL 的成功后来超越了 Atari 和棋盘游戏,扩展到了马里奥、Quake III Arena、Capture the Flag、Dota 2 和 StarCraft II 等游戏。这些游戏对 AI 程序极具挑战性,需要战略规划、多方决策者之间的博弈论、信息不完全以及大量的可能行动和游戏状态。由于这些复杂性,训练这些模型需要大量资源。例如,OpenAI 使用 256 个 GPU 和 128,000 个 CPU 核心训练了 Dota 2 模型,进行了数月的训练,每天为模型提供了 900 年的游戏经验。Google DeepMind 的 AlphaStar 在 2019 年击败了星际争霸 II 的顶级职业选手,其训练过程也需要数百个精密模型的复制,每个模型有 200 年的实时游戏经验,尽管这些模型最初是基于人类玩家的真实游戏数据进行训练的。

机器人学与自主系统

机器人技术和物理自主系统是强化学习领域中的挑战性领域。这是因为强化学习智能体通常是在仿真环境中训练的,以收集足够的数据,但仿真环境无法反映现实世界的所有复杂性。因此,这些智能体在实际任务中常常失败,尤其是在任务涉及到安全性时,这种失败尤为严重。此外,这些应用通常涉及连续的动作,这需要与 DQN 不同类型的算法。尽管面临这些挑战,但另一方面,强化学习在这些领域也有许多成功的案例。此外,使用强化学习应用于自动驾驶地面和空中车辆等令人兴奋的应用领域的研究也有很多。

电梯优化

一个早期的成功案例证明了强化学习(RL)可以为现实世界的应用创造价值,这个案例发生在 1996 年,由 Robert Crites 和 Andrew Barto 进行的电梯优化研究。研究人员开发了一个强化学习模型,用于优化一栋有 10 层楼和 4 部电梯的建筑中的电梯调度。这比早期的 TD-Gammon 问题要具有更大的挑战性,因为模型可能会遇到的情境数量、部分可观察性(例如,不同楼层等待的人数对于强化学习模型不可观察)以及可选择的决策数量都大大增加。这个强化学习模型在多个指标上显著提高了当时最佳电梯控制启发式算法的表现,例如平均乘客等待时间和旅行时间。

人形机器人与灵巧操作

2017 年,谷歌 DeepMind 的 Nicolas Heess 等人能够在计算机仿真中教会不同类型的身体(例如,人形机器人等)各种运动行为,例如如何奔跑、跳跃等。2018 年,OpenAI 的 Marcin Andrychowicz 等人训练了一个五指人形机器人手臂,将一个方块从初始状态移动到目标状态。2019 年,OpenAI 的 Ilge Akkaya 等研究人员再次成功地训练了一个机器人手臂来解魔方:

图 1.1 – OpenAI 的强化学习模型,通过仿真训练(a),并部署到物理机器人上(b)(图像来源:OpenAI Blog,2019)

图 1.1 – OpenAI 的强化学习模型,通过仿真训练(a),并部署到物理机器人上(b)(图像来源:OpenAI Blog,2019)

后两种模型都是在仿真中训练的,并成功地通过领域随机化技术转移到物理实现中(图 1.1)。

紧急响应机器人

在灾难发生后,使用机器人可能会非常有帮助,特别是在危险环境下作业时。例如,机器人可以在损坏的建筑物中寻找幸存者,关闭燃气阀门等。创造能够自主操作的智能机器人将使紧急响应操作得到扩展,并为更多的人提供支持,这比目前人工操作能够提供的支持要多得多。

自主驾驶车辆

尽管完全自动驾驶汽车的复杂性使得单靠 RL 模型无法解决,但其中一些任务可以由 RL 处理。例如,我们可以训练 RL 代理来实现自动停车,并决定何时及如何超车。此外,我们还可以使用 RL 代理来执行自动驾驶无人机中的某些任务,例如如何起飞、着陆、避免碰撞等。

供应链

供应链中的许多决策具有顺序性质并涉及不确定性,因此 RL 是一个自然的解决方案。以下是一些这类问题的示例:

  • 库存规划是关于决定何时下订单以补充商品库存以及订购多少数量的问题。订购不足会导致短缺,订购过多则会导致库存积压、产品腐烂以及以降价处理库存。RL 模型被用来做出库存规划决策,以减少这些操作的成本。

  • 装箱问题是制造业和供应链中常见的问题,其中到达某一工作站的物品需要被放入容器中,以最小化使用的容器数量并确保工厂内操作顺利进行。这是一个难题,可以通过 RL 来解决。

制造业

强化学习(RL)将在制造业中产生巨大影响,许多人工任务可能会由自主代理以更低的成本和更高的质量完成。因此,许多公司正在研究将 RL 引入其制造环境。以下是一些 RL 在制造业中的应用示例:

  • 机器校准是制造环境中常由人类专家处理的任务,这种方式既低效又容易出错。RL 模型通常能够以更低的成本和更高的质量完成这些任务。

  • 化工厂操作通常涉及顺序决策,这些决策通常由人类专家或启发式方法来处理。研究表明,RL 代理能够有效地控制这些过程,从而获得更好的最终产品质量并减少设备磨损。

  • 设备维护需要规划停机时间以避免昂贵的设备故障。RL 模型可以有效平衡停机成本与潜在故障的成本。

  • 除了这些示例之外,许多成功的 RL 应用在机器人技术领域也可以转化为制造解决方案。

个性化与推荐系统

个性化无疑是 RL 迄今为止创造最大商业价值的领域。大科技公司通过 RL 算法为个性化提供服务,算法在背后运行。以下是一些示例:

  • 在广告中,向(潜在)客户传递促销材料的顺序和内容是一个顺序决策问题,可以通过 RL 来解决,从而提高客户满意度和转化率。

  • 新闻推荐是微软新闻广为应用强化学习的一个领域,通过改进文章的选择和推荐顺序,成功增加了访客的参与度。

  • 个性化的艺术作品,比如 Netflix 中展示的标题,是由强化学习算法来处理的。通过这个方式,观众可以更好地识别与自己兴趣相关的标题。

  • 个性化医疗变得越来越重要,因为它能以较低的成本提供更有效的治疗。强化学习在为患者选择合适治疗方案方面有许多成功的应用。

智慧城市

强化学习可以帮助改善城市运作的许多领域。以下是几个例子:

  • 在一个包含多个交叉口的交通网络中,交通信号灯应该协调工作,以确保交通流畅。事实证明,这个问题可以建模为一个多智能体强化学习问题,并改善现有的交通信号灯控制系统。

  • 实时平衡电网中的电力生成和需求是一个重要的问题,以确保电网安全。实现这一目标的一种方式是控制需求,比如在电力生成充足时,充电电动车和开启空调系统,而不会影响服务质量,强化学习方法已经成功地应用于此。

这个列表可以写好几页,但这已经足够展示强化学习的巨大潜力。领域先驱安德鲁·吴(Andrew Ng)关于人工智能的观点,对于强化学习同样适用:

就像 100 年前电力几乎改变了所有行业一样,今天我很难想到一个行业,我认为人工智能在未来几年不会改变的。(安德鲁·吴:为什么人工智能是新的电力;斯坦福新闻;2017 年 3 月 15 日)

当前的强化学习仍处于其黄金时代的初期,而你正在通过投入精力去理解强化学习的本质及其潜力,做出一个伟大的投资。现在,是时候更技术化地定义强化学习问题中的各个元素了。

强化学习(RL)问题的元素

到目前为止,我们已经涵盖了可以用强化学习建模的各种问题类型。在接下来的章节中,我们将深入探讨解决这些问题的最先进算法。然而,在此过程中,我们需要正式定义强化学习问题中的元素。这将为更技术化的内容奠定基础,帮助建立我们的词汇体系。在给出这些定义之后,我们将通过井字游戏的例子来解释这些概念的具体含义。

强化学习概念

让我们从定义强化学习问题中的最基本组成部分开始:

  • 在强化学习问题的核心,是学习者,强化学习术语中称之为智能体(agent)。我们处理的大多数问题类只有一个智能体。另一方面,如果有多个智能体,那么这个问题类被称为多智能体强化学习(multi-agent RL),简称MARL。在 MARL 中,智能体之间的关系可以是合作的、竞争的,或者两者的混合。

  • 强化学习问题的本质是智能体学习在其所处的世界中该做什么——也就是采取哪个行动——以应对不同的情况。我们将这个世界称为环境,它指的是智能体之外的所有事物。

  • 所有能够精确且充分描述环境中情况的信息集合称为状态。因此,如果环境在不同时间点处于相同的状态,意味着环境的所有情况完全相同——就像复制粘贴一样。

  • 在某些问题中,智能体对状态的知识是完全可用的。在许多其他问题中,尤其是在更现实的情况中,智能体并不能完全观察到状态,而只能观察到其中的一部分(或某部分状态的推导)。在这种情况下,智能体使用其观察来采取行动。当情况如此时,我们称该问题为部分可观察。除非另有说明,我们假设智能体能够完全观察到环境所处的状态,并基于该状态来执行其行动。

    信息

    状态 这一术语及其符号 在抽象讨论中更常被使用,尤其是在假设环境是完全可观察的情况下,尽管 观察 是一个更广泛的术语;智能体所接收到的总是观察结果,有时是状态本身,有时是状态的一部分或从状态中推导出的内容,这取决于环境。如果你看到它们在某些语境中交替使用,不要感到困惑。

到目前为止,我们还没有真正定义什么是好的或不好的行动。在强化学习中,每当智能体采取一个行动时,它都会从环境中获得一个奖励(尽管有时奖励为零)。奖励在一般意义上可以有很多含义,但在强化学习术语中,其含义非常具体:它是一个标量数字。数字越大,奖励也越高。在强化学习问题的每次迭代中,智能体观察到环境所处的状态(无论是完全观察还是部分观察),并根据其观察采取行动。结果是,智能体获得奖励,环境进入新的状态。这个过程在图 1.2中有所描述,您可能已经很熟悉了:

图 1.2 – 强化学习过程图

图 1.2 – 强化学习过程图

记住,在强化学习中,智能体关心的是长期有益的行动。这意味着智能体必须考虑其行动的长期后果。一些行动可能会使智能体立即获得高奖励,但接下来却会迎来非常低的奖励,反之亦然。因此,智能体的目标是最大化其获得的累计奖励。自然的后续问题是:这个时间跨度是多长?答案取决于所关注的问题是在有限还是无限的时间范围内定义的:

  • 如果是前者,那么问题被描述为一个情节任务,其中情节定义为从初始状态到终止状态的交互序列。在情节任务中,代理的目标是最大化在一个情节内收集到的期望总累计奖励。

  • 如果问题是在无限时域内定义的,它被称为持续任务。在这种情况下,代理会尝试最大化平均奖励,因为总奖励将趋向于无穷大。

  • 那么,代理如何实现这个目标呢?代理根据对环境的观察,确定最好的行动。换句话说,强化学习问题的核心是找到一种策略,它将给定的观察映射到一个(或多个)行动,从而最大化期望的累计奖励。

所有这些概念都有具体的数学定义,我们将在后面的章节中详细讨论。但现在,让我们试着理解这些概念在具体例子中的含义。

将井字棋视为一个强化学习问题

井字棋是一个简单的游戏,两个玩家轮流在一个网格中标记空位。我们现在将其作为一个强化学习问题,将之前提供的定义映射到游戏中的概念。玩家的目标是将自己的标记排成一行(纵向、横向或对角线),以赢得比赛。如果没有任何一个玩家在所有空位用完之前完成这一目标,游戏将以平局结束。在游戏进行中,井字棋的棋盘可能看起来像这样:

图 1.3 – 井字棋的示例棋盘配置

图 1.3 – 井字棋的示例棋盘配置

现在,假设我们有一个强化学习代理与一个人类玩家对战:

  • 代理采取的行动是在轮到代理的回合时,将其标记(比如叉)放置到棋盘上的空位之一。

  • 这里,棋盘就是整个环境,棋盘上标记的位置就是状态,代理可以完全观察到这些状态。

  • 在一个 3 x 3 的井字棋游戏中,有 765 种状态(唯一的棋盘位置,排除了旋转和镜像),代理的目标是学习一种策略,能够为这些状态中的每一个建议一个行动,以最大化获胜的概率。

  • 这个游戏可以定义为一个情节性强化学习任务。为什么?因为游戏最多会持续 9 轮,环境会达到一个终止状态。终止状态是指三个 X 或 O 排成一行,或者没有任何标记能排成一行且棋盘上没有空位(即平局)。

  • 注意,除非在游戏结束时某个玩家获胜,否则在游戏过程中,玩家每进行一次操作都不会获得奖励。所以,如果代理获胜,它会得到+1 奖励,失败则得到-1 奖励,平局则得到 0 奖励。在所有迭代中,直到游戏结束,代理将获得 0 奖励。

  • 我们可以通过用另一个 RL 代理替换人类玩家来将其转化为一个多智能体 RL 问题,让新代理与第一个代理竞争。

希望这能刷新你对智能体、状态、动作、观察、策略和奖励含义的记忆。这只是一个简单的例子,放心,后面会有更先进的内容。在这个入门性的背景介绍完成后,我们需要做的就是设置计算机环境,以便能够运行我们在接下来的章节中将要讲解的 RL 算法。

设置你的 RL 环境

RL 算法利用最先进的机器学习库,这些库需要一些复杂的硬件。为了跟随本书中我们将解决的示例,你需要设置你的计算机环境。接下来我们将介绍你在设置过程中所需的硬件和软件。

硬件要求

如前所述,最先进的 RL 模型通常在数百个 GPU 和数千个 CPU 上进行训练。我们当然不期望你能访问这些资源。然而,拥有多个 CPU 核心将帮助你同时模拟多个智能体和环境,更快地收集数据。拥有一个 GPU 将加速训练现代 RL 算法中使用的深度神经网络。此外,为了能够高效处理所有这些数据,拥有足够的内存资源也很重要。但不用担心,利用你现有的硬件,你仍然能够从本书中获得大量的知识。供参考,以下是我们用来运行实验的桌面配置:

  • AMD Ryzen Threadripper 2990WX CPU,拥有 32 核

  • NVIDIA GeForce RTX 2080 Ti GPU

  • 128 GB 内存

作为构建带有昂贵硬件的桌面的替代方案,你可以使用由各家公司提供的具有相似能力的虚拟机(VMs)。最著名的几个如下:

  • 亚马逊 AWS

  • Microsoft Azure

  • 谷歌云平台

这些云服务提供商还为你的虚拟机(VM)提供数据科学镜像,这样用户就不需要安装深度学习所需的软件(例如 CUDA、TensorFlow 等)。他们还提供了详细的指南,说明如何设置你的虚拟机,关于设置的细节我们将在后面讨论。

一个最终选项是谷歌的 Colab,它可以让你在 TensorFlow 上进行小规模的深度学习实验。Colab 提供的虚拟机实例可以直接从浏览器访问,且所需的软件已经安装。你可以立即在类似 Jupyter Notebook 的环境中开始实验,这是一个非常方便的快速实验选项。

操作系统

当你为教育目的开发数据科学模型时,Windows、Linux 或 macOS 之间通常没有太大区别。然而,在本书中,我们计划做的事情不仅仅如此,还会使用运行在 GPU 上的高级 RL 库。这个环境在 Linux 上得到最好的支持,我们使用的是 Ubuntu 18.04.3 LTS 发行版。另一个选择是 macOS,但它通常没有 GPU。最后,尽管设置可能有些复杂,Windows 子系统 Linux (WSL) 2 是你可以探索的一个选项。

软件工具箱

设置数据科学项目的软件环境时,人们通常首先安装 Anaconda,它为你提供了一个 Python 平台,并附带了许多有用的库。

提示

virtualenv 是一个比 Anaconda 更轻量的工具,用于创建 Python 虚拟环境,并且在大多数生产环境中更为推荐。我们也将在某些章节中使用它。你可以在 virtualenv.pypa.io/en/latest/installation.html 找到 virtualenv 的安装说明。

我们特别需要以下几个包:

  • Python 3.7:Python 是今天数据科学的共同语言。我们将使用版本 3.7。

  • NumPy:这是 Python 中用于科学计算的最基础的库之一。

  • pandas 是一个广泛使用的库,提供强大的数据结构和分析工具。

  • Jupyter Notebook:这是一个非常方便的工具,特别适合运行小规模的 Python 代码任务。它通常默认随 Anaconda 安装一起提供。

  • TensorFlow 2.x:这是我们选择的深度学习框架。在本书中,我们使用的是版本 2.3.0。有时,我们也会提到使用 TensorFlow 1.x 的仓库。

  • Ray 和 RLlib:Ray 是一个用于构建和运行分布式应用的框架,正在越来越受欢迎。RLlib 是一个运行在 Ray 上的库,包含许多流行的 RL 算法。在编写本书时,Ray 只支持 Linux 和 macOS 进行生产环境部署,Windows 支持仍处于 Alpha 阶段。我们将使用版本 0.8.7。

  • gym:这是一个由 OpenAI 创建的 RL 框架,如果你曾接触过 RL,你可能已经使用过它。它允许我们以标准方式定义 RL 环境,并让它们与像 RLlib 这样的算法包进行交互。

  • OpenCV Python 绑定:我们需要这个库来进行一些图像处理任务。

  • Cufflinks 包将它绑定到 pandas。

你可以在终端中使用以下命令来安装特定的包。使用 Anaconda 时,我们使用以下命令:

conda install pandas==0.20.3

使用 virtualenv(在大多数情况下也适用于 Anaconda),我们使用以下命令:

pip install pandas==0.20.3

有时候,你可以灵活选择包的版本,在这种情况下,你可以省略等号及其后面的内容。

提示

为了实验的顺利进行,最好为本书创建一个专门的虚拟环境,并在该环境中安装所有相关的包。这样,你就不会破坏其他 Python 项目的依赖性。Anaconda 提供了全面的在线文档,指导你如何管理环境,文档地址:bit.ly/2QwbpJt。

就是这样!有了这些,你就准备好开始编写强化学习(RL)代码了!

摘要

这是我们对强化学习基础知识的复习!我们从讨论强化学习是什么,以及为什么它是如此热门并成为人工智能的下一前沿话题开始。我们还探讨了强化学习的多种应用及其在过去几年成为新闻头条的成功案例。我们还定义了将在本书中使用的基本概念。最后,我们介绍了你需要的硬件和软件,以运行我们将在接下来的章节中介绍的算法。到目前为止的内容,都是为了让你刷新强化学习的记忆,激励你,并为接下来的内容做好准备:实现先进的强化学习算法,解决具有挑战性的实际问题。在下一章,我们将直接进入多臂赌博机问题,它是强化学习算法中的一个重要类别,在个性化和广告领域有着广泛的应用。

参考文献

  1. Sutton, R. S., Barto, A. G. (2018). 强化学习:导论。MIT 出版社。

  2. Tesauro, G. (1992). 时序差分学习中的实际问题。Machine Learning 8, 257–277。

  3. Tesauro, G. (1995). 时序差分学习与 TD-Gammon。Commun. ACM 38, 3, 58-68。

  4. Silver, D. (2018). 深度强化学习的成功案例。来源:youtu.be/N8_gVrIPLQM。

  5. Crites, R. H., Barto, A.G. (1995). 使用强化学习提高电梯性能。在第 8 届国际神经信息处理系统会议论文集(NIPS'95)中。

  6. Mnih, V. 等人 (2015). 通过深度强化学习实现人类级控制。Nature, 518(7540), 529–533。

  7. Silver, D. 等人 (2018). 一种通用的强化学习算法,通过自我对弈掌握国际象棋、将棋和围棋。Science, 362(6419), 1140–1144。

  8. Vinyals, O. 等人 (2019). 通过多智能体强化学习在《星际争霸 II》上达到大师级水平。Nature。

  9. OpenAI. (2018). OpenAI Five。来源:blog.openai.com/openai-five/。

  10. Heess, N. 等人 (2017). 在丰富环境中出现的运动行为。ArXiv, abs/1707.02286。

  11. OpenAI 等人 (2018). 学习灵巧的手内操作。ArXiv, abs/1808.00177。

  12. OpenAI 等人 (2019). 用机器人手解决魔方。ArXiv, abs/1910.07113。

  13. OpenAI 博客 (2019). 用机器人手解决魔方。来源:openai.com/blog/solving-rubiks-cube/。

  14. Zheng, G. 等(2018)。DRN:一种用于新闻推荐的深度强化学习框架。2018 年全球信息网大会论文集(WWW '18)。国际全球信息网会议指导委员会,瑞士日内瓦,167–176。DOI:doi.org/10.1145/3178876.3185994。

  15. Chandrashekar, A. 等(2017)。Netflix 中的艺术作品个性化。The Netflix Tech Blog。取自 medium.com/netflix-techblog/artwork-personalization-c589f074ad76。

  16. McKinney, S. M. 等(2020)。国际评估用于乳腺癌筛查的人工智能系统。Nature, 89-94。

  17. Agrawal, R.(2018 年 3 月 8 日)。Microsoft News Center India。取自 news.microsoft.com/en-in/features/microsoft-ai-network-healthcare-apollo-hospitals-cardiac-disease-prediction/。

第二章:第二章:多臂赌博机

当你登录到你最喜欢的社交媒体应用时,你很可能会看到当时正在测试的多种版本之一。当你访问一个网站时,展示给你的广告是根据你的个人资料量身定制的。在许多在线购物平台上,价格是动态决定的。你知道这些现象有什么共同点吗?它们通常被建模为多臂赌博机(MAB)问题,用于识别最优决策。MAB 问题是一种强化学习(RL)的形式,其中智能体在一个由单一步骤组成的时间范围内做出决策。因此,目标是最大化即时奖励,并且没有考虑任何后续步骤的后果。虽然这比多步骤 RL 做了简化,但智能体仍然必须处理 RL 中的一个基本权衡:探索可能导致更高奖励的新动作与利用已知能带来合理奖励的动作之间的权衡。许多商业问题,如前面提到的,涉及到优化这种探索与利用的权衡。在接下来的两章中,你将了解这个权衡的意义——它几乎会成为所有 RL 方法的反复出现的主题——并学习如何有效地应对它。

在本章中,我们通过解决不考虑“上下文”的 MAB 问题来奠定基础,例如访问网站/应用的用户资料、时间等。为此,我们介绍了四种基本的探索策略。在下一章中,我们将扩展这些策略以解决上下文相关的 MAB问题。在这两章中,我们将使用在线广告——多臂赌博机问题的重要应用——作为我们的持续案例研究。

好的,让我们开始吧!在本章中,我们将具体讨论以下内容:

  • 探索与利用的权衡

  • 什么是 MAB?

  • 案例研究——在线广告

  • A/B/n 测试作为探索策略

  • -贪婪动作用于探索

  • 使用上置信界限进行动作选择

  • 汤普森(后验)采样

探索与利用的权衡

正如我们之前提到的,RL 完全依赖于从经验中学习,而无需监督员为代理标记正确的行动。代理观察其行动的后果,确定在每种情况下导致最高奖励的行动,并从这种经验中学习。现在,请考虑一下你从自己的经验中学到的东西——例如,如何为考试学习。很可能你探索了不同的方法,直到发现最适合你的方法。也许你先是定期为考试学习,但后来你测试过在考试前夜学习是否足够有效——也许对某些类型的考试确实有效。关键是你必须进行探索以找到最大化你的“奖励”的方法(s),这是你的考试成绩、闲暇活动时间、考试前后的焦虑水平等的函数。实际上,探索对基于经验的任何学习都至关重要。否则,我们可能永远不会发现更好的做事方式或根本可行的方式!另一方面,我们不能总是试验新方法。不去利用我们已经学到的东西是愚蠢的!因此,探索和开发之间存在探索与开发之间的权衡,这种权衡是 RL 的核心所在。在 MAB 中有效平衡这种权衡至关重要。

如果探索与开发的权衡在所有 RL 问题中都是一个挑战,那么为什么我们特别在 MAB 的背景下提出它?这主要有两个原因:

  • MAB 是单步 RL。因此,它允许我们从多步 RL 的复杂性中分离出各种探索策略,并在理论上证明它们的优越性。

  • 在多步 RL 中,我们经常在离线状态下训练代理(并在模拟中),并在线使用其策略,在 MAB 问题中,代理通常在线训练和使用(几乎总是)。因此,低效的探索成本不仅仅是计算机时间:它实际上通过不良行动花费真钱。因此,在 MAB 问题中有效平衡探索和开发变得非常关键。

有了这个想法,现在是时候定义什么是 MAB 问题,然后看一个例子。

什么是 MAB?

MAB 问题的关键是在通过试错确定的一组行动中识别最佳行动,例如在一些选择中找出网站的最佳外观或产品的最佳广告横幅。我们将专注于 MAB 的更常见变体,其中代理可以选择 离散行动,也称为 -臂老丨虎丨机问题。

让我们通过得名的示例更详细地定义问题。

问题定义

MAB 问题是以需要选择一台老丨虎丨机(强盗)来玩的赌徒为案例命名的情况:

  • 当拉动机器的拉杆时,它会根据该机器特定的概率分布给出一个随机奖励。

  • 尽管这些机器看起来相同,但它们的奖励概率分布是不同的。

赌博者的目标是最大化他们的总奖励。因此,在每一轮中,他们需要决定是继续玩目前为止提供最高平均奖励的机器,还是尝试其他机器。最初,赌博者并不知道机器的奖励分布。

很明显,赌博者需要在利用目前为止表现最好的机器和探索其他选择之间找到平衡。为什么需要这样做呢?因为奖励是随机的。一个机器可能在长期内不会提供最高的平均奖励,但由于某种偶然性,它可能在短期内看起来是最好的!

图 2.1 – MAB 问题涉及从多个选项中识别出最好的拉杆

图 2.1 – MAB 问题涉及从多个选项中识别出最好的拉杆

所以,总结一下 MAB 问题的特点,我们可以得出以下结论:

  • 代理执行顺序动作。在每次动作之后,会获得奖励。

  • 一个动作只会影响即时奖励,而不会影响后续奖励。

  • 系统中没有“状态”,也就是说,代理采取的动作不会改变任何状态。

  • 代理没有任何用于决策的输入。这个问题将在下一章中讨论上下文强盗问题时涉及。

到目前为止,一切顺利!让我们通过实际编码一个例子来更好地理解这个问题。

实验一个简单的 MAB 问题

在这一部分,你将通过一个例子体验即使是一个简单的 MAB 问题也可能非常棘手。我们将创建一些虚拟的老丨虎丨机,并通过识别最幸运的机器来最大化总奖励。此代码可以在 GitHub 代码库的Chapter02/Multi-armed bandits.ipynb中找到。

设置虚拟环境

在开始之前,我们建议你使用virtualenv或 Conda 命令为练习创建一个虚拟环境。在你希望放置虚拟环境文件的文件夹中,在终端执行以下命令:

virtualenv rlenv
source rlenv/bin/activate
pip install pandas==0.25.3
pip install plotly==4.10.0
pip install cufflinks==0.17.3
pip install jupyter
ipython kernel install --name «rlenv» –user
jupyter notebook

这将打开一个浏览器标签,加载一个 Jupyter 笔记本。找到从代码库中获得的.ipynb文件,打开它,并将内核设置为我们刚刚创建的rlenv环境。

强盗练习

让我们开始练习吧:

  1. 首先,让我们为一个单一的老丨虎丨机创建一个类,该老丨虎丨机根据给定的均值和标准差从正态(高斯)分布中获取奖励:

    import numpy as np # Class for a single slot machine. Rewards are Gaussian.class GaussianBandit(object):    def __init__(self, mean=0, stdev=1):        self.mean = mean         self.stdev = stdev         def pull_lever(self):        reward = np.random.normal(self.mean, self.stdev)        return np.round(reward, 1)
    
  2. 接下来,我们创建一个类来模拟游戏:

    class GaussianBanditGame(object):
        def __init__(self, bandits):
            self.bandits = bandits
            np.random.shuffle(self.bandits)
            self.reset_game()
    
        def play(self, choice):
            reward = self.bandits[choice - 1].pull_lever()
            self.rewards.append(reward)
            self.total_reward += reward
            self.n_played += 1
            return reward
    
        def user_play(self):
            self.reset_game()
            print("Game started. " + 
                  "Enter 0 as input to end the game.")
            while True:
                print(f"\n -- Round {self.n_played}")
                choice = int(input(f"Choose a machine " + 
                         f"from 1 to {len(self.bandits)}: "))
                if choice in range(1, len(self.bandits) + 1):
                    reward = self.play(choice)
                    print(f"Machine {choice} gave " + 
                          f"a reward of {reward}.")
                    avg_rew = self.total_reward/self.n_played
                    print(f"Your average reward " +
                          f"so far is {avg_rew}.")
                else:
                    break
            print("Game has ended.")
            if self.n_played > 0:
                print(f"Total reward is {self.total_reward}" + 
                      f" after {self.n_played} round(s).")
                avg_rew = self.total_reward/self.n_played
                print(f"Average reward is {avg_rew}.")              
    
        def reset_game(self):
            self.rewards = []
            self.total_reward = 0
            self.n_played = 0
    

    一个游戏实例接收一组老丨虎丨机作为输入。它会打乱这些老丨虎丨机的顺序,以便你无法识别哪个机器提供最高的平均奖励。在每一步,你需要选择一个机器并尽量获得最高的奖励。

  3. 然后,我们创建一些老丨虎丨机和一个游戏实例:

    slotA = GaussianBandit(5, 3)slotB = GaussianBandit(6, 2)slotC = GaussianBandit(1, 5)game = GaussianBanditGame([slotA, slotB, slotC])
    
  4. 现在,通过调用游戏对象的user_play()方法开始游戏:

    game.user_play()
    

    输出结果如下所示:

    Game started. Enter 0 as input to end the game. 
    -- Round 0
    Choose a machine from 1 to 3:
    
  5. 当你输入选择时,你将看到该回合获得的奖励。我们对机器一无所知,所以从 1 开始吧:

    slotB machine, so there is no reason to try something else and lose money! 
    
  6. 让我们再玩几个回合同样的机器:

    -- Round 1
    Choose a machine from 1 to 3: 1
    Machine 1 gave a reward of 4.9.
    Your average reward so far is 6.65.
     -- Round 2
    Choose a machine from 1 to 3: 1
    Machine 1 gave a reward of -2.8.
    Your average reward so far is 3.5.
    

    咔嚓!这看起来确实是最差的机器!slotA或slotB机器给出-2.8奖励的可能性非常小。

  7. 让我们检查一下游戏中的第一台机器(记住第一台机器对应bandits列表中的索引 0),通过查看它的均值参数。执行game.bandits[0].mean时,我们得到的输出是1!

事实上,我们以为我们选择了最好的机器,尽管它实际上是最差的!为什么会发生这种情况呢?嗯,原因在于奖励是随机的。根据奖励分布的方差,某个特定奖励可能与我们期望的该机器的平均奖励相差甚远。正因为如此,在我们经历足够的游戏轮数之前,很难知道该拉哪个拉杆。事实上,只有少量的样本,我们的观察结果可能会非常具有误导性,就像刚刚发生的那样。此外,如果你自己玩这个游戏,你会发现很难区分slotA和slotB,因为它们的奖励分布相似。你可能会想,“这有这么重要吗?”嗯,如果差异对应着显著的金钱和资源,像许多现实世界的应用一样,那是非常重要的。

接下来,我们将介绍一种应用——在线广告,这是我们在本章及下一章中的示例。

案例研究——在线广告

假设有一家公司希望通过数字横幅广告在各大网站上推广产品,目的是吸引访客进入产品的登陆页面。在多个广告选择中,广告主希望找出最有效的横幅,并且拥有最高的点击率(CTR),点击率定义为广告获得的总点击数除以广告展示的总次数(即广告的曝光次数)。

每次横幅广告将在网站上展示时,都是广告主的算法选择横幅(例如,通过广告主提供的 API 接口)并观察该展示是否导致了点击。这是一个很好的多臂老丨虎丨机(MAB)模型应用场景,可以提高点击率和产品销量。我们希望 MAB 模型尽早识别出表现最好的广告,更多地展示它,并尽早淘汰明显失败的广告。

提示

在展示后观察点击与未点击的概率(二元结果),可以使用伯努利分布来建模。它有一个参数,,即接收到点击的概率,或者更一般地,观察到 1 而不是 0 的概率。注意,这是一个离散概率分布,而我们之前使用的正态分布是一个连续的分布。

在前面的例子中,我们的奖励来自正态分布。在在线广告的情况中,奖励是二元的。对于每个广告版本,有不同的点击概率(CTR),广告商不知道这个概率,但试图去发现它。所以,奖励将来自每个广告的不同伯努利分布。我们来编写代码,以便稍后与我们的算法一起使用:

  1. 我们首先创建一个类来建模广告行为:

    class BernoulliBandit(object):
        def __init__(self, p):
            self.p = p
        def display_ad(self):
            reward = np.random.binomial(n=1, p=self.p)
            return reward
    
  2. 现在,我们来创建五个不同的广告(横幅),并随便选择相应的 CTR:

    adA = BernoulliBandit(0.004)
    adB = BernoulliBandit(0.016)
    adC = BernoulliBandit(0.02)
    adD = BernoulliBandit(0.028)
    adE = BernoulliBandit(0.031)
    ads = [adA, adB, adC, adD, adE]
    

到目前为止,一切顺利。现在,是时候实施一些探索策略,以最大化广告活动的 CTR 了!

A/B/n 测试

最常见的探索策略之一是所谓的A/B 测试,这是一种确定两个备选方案(如在线产品、页面、广告等)中哪个表现更好的方法。在这种测试中,用户会被随机分成两组,尝试不同的备选方案。在测试期结束时,比较结果以选择最佳方案,然后在剩余的时间内用于生产。在我们的例子中,我们有多个广告版本。因此,我们将实施所谓的A/B/n 测试。

我们将使用 A/B/n 测试作为基准策略,便于与之后介绍的更先进方法进行比较。在进入实现之前,我们需要定义一些符号,这些符号将在本章中使用。

符号

在各种算法的实现过程中,我们需要跟踪与特定行为(选择展示的广告)相关的一些量,。现在,我们为这些量定义一些符号。最初,为了简洁起见,我们省略了,但在本节末,我们会重新添加:

  • 首先,我们表示在选择动作后收到的奖励(即,点击为 1,未点击为 0),,为第次由获得。

  • 在选择此相同行为之前观察到的平均奖励定义如下:

这估计了该行为产生的奖励的期望值!,在进行次观察后。

  • 这也被称为行为值!。在这里,这就是在选择此行为次后,的行为值估计。

  • 现在,我们需要一些简单的代数运算,就能得到一个非常方便的公式来更新动作值:

  • 记住, 是我们在执行 次动作之前对 的动作值的估计。当我们观察到奖励 时,它为我们提供了一个新的动作值信号。我们不想丢弃之前的观察结果,但我们也希望更新我们的估计以反映这个新信号。

    因此,我们调整当前估计值 ,使其朝着我们基于最新观察到的奖励 计算出的 误差 的方向进行调整,步长为 ,并获得新的估计值 。这意味着,例如,如果最新观察到的奖励大于我们当前的估计值,我们会将动作值估计向上修正。

  • 为了方便起见,我们定义 。

  • 请注意,随着我们进行更多观察,我们调整估计的速率会变得更小,因为有了 项。所以,我们会对最新的观察结果赋予较小的权重,并且某个特定动作的动作值估计会随着时间的推移而趋于稳定。

  • 然而,如果环境不是静态的,而是随时间变化,这可能是一个缺点。在这些情况下,我们希望使用一个不会随时间衰减的步长,例如固定步长 。

  • 请注意,为了使估计收敛,这个步长必须小于 1(并且大于 0 以确保适当更新)。

  • 使用固定值 会使得随着我们越来越多地执行动作 ,较早的观察结果的权重呈指数衰减。

让我们把 带回符号中,这样我们就能得到更新动作值的公式:

这里, 是一个介于 0 和 1 之间的数。对于静态问题,我们通常设定 ,其中 是到目前为止已采取的动作 的次数(最初表示为 )。在静态问题中,由于逐渐减少的 项,这将有助于动作值更快地收敛,而不是追逐噪声观察值。

这就是我们需要的一切。事不宜迟,让我们实现一个 A/B/n 测试。

应用于在线广告场景

在我们的示例中,我们有五个不同的广告版本,我们以相等的概率随机展示给用户。让我们在 Python 中实现这一点:

  1. 我们从创建变量来跟踪实验中的奖励开始:

    n_test = 10000
    n_prod = 90000
    n_ads = len(ads)
    Q = np.zeros(n_ads)  # Q, action values
    N = np.zeros(n_ads)  # N, total impressions
    total_reward = 0
    avg_rewards = []  # Save average rewards over time
    
  2. 现在,让我们运行 A/B/n 测试:

    for i in range(n_test):
        ad_chosen = np.random.randint(n_ads)
        R = ads[ad_chosen].display_ad() # Observe reward
        N[ad_chosen] += 1
        Q[ad_chosen] += (1 / N[ad_chosen]) * (R - Q[ad_chosen])
        total_reward += R
        avg_reward_so_far = total_reward / (i + 1)
        avg_rewards.append(avg_reward_so_far)
    

    请记住,我们在测试期间随机选择一个广告进行展示,并观察是否获得点击。我们更新计数器、行动值估计值以及到目前为止观察到的平均奖励。

  3. 在测试期结束时,我们选择获得最高行动值的广告作为获胜者:

    best_ad_index = np.argmax(Q)
    
  4. 我们使用print语句展示获胜者:

    print("The best performing ad is {}".format(chr(ord('A') + best_ad_index)))
    
  5. 结果如下:

    The best performing ad is D.
    

    在这种情况下,A/B/n 测试将 D 识别为表现最佳的广告,但这并不完全正确。显然,测试期不够长。

  6. 让我们在生产中运行 A/B/n 测试识别出的最佳广告:

    ad_chosen = best_ad_index
    for i in range(n_prod):
        R = ads[ad_chosen].display_ad()
        total_reward += R
        avg_reward_so_far = total_reward / (n_test + i + 1)
        avg_rewards.append(avg_reward_so_far)
    

    在此阶段,我们不会再探索其他操作。所以,广告 D 的错误选择将在整个生产期内产生影响。我们继续记录到目前为止观察到的平均奖励,以便之后可视化广告活动表现。

    现在,展示结果的时间到了:

  7. 让我们创建一个pandas DataFrame 来记录 A/B/n 测试的结果:

    import pandas as pd
    df_reward_comparison = pd.DataFrame(avg_rewards, columns=['A/B/n'])
    
  8. 为了显示平均奖励的进展,我们使用 Plotly 和 Cufflinks:

    import cufflinks as cf
    import plotly.offline
    cf.go_offline()
    cf.set_config_file(world_readable=True, theme="white")
    df_reward_comparison['A/B/n'].iplot(title="A/B/n Test Avg. Reward: {:.4f}"
                                       .format(avg_reward_so_far),
                                        xTitle='Impressions', 
                                        yTitle='Avg. Reward')
    

    这将产生以下输出:

图 2.2 – A/B/n 测试奖励

图 2.2 – A/B/n 测试奖励

从图 2.2中可以看到,在探索结束后,平均奖励接近 2.8%,这是广告 D 的预期 CTR。另一方面,由于在前 10k 展示期间进行了一些探索,我们尝试了几个表现不佳的选择,因此在 100k 展示后,CTR 最终为 2.71%。如果 A/B/n 测试能识别广告 E 作为最佳选择,CTR 可能会更高。

就这样!我们刚刚实现了一个 A/B/n 测试。总体而言,这个测试帮助我们识别出了一些表现较好的广告,尽管并非最好的。接下来,我们讨论 A/B/n 测试的优缺点。

A/B/n 测试的优缺点

现在,让我们对这种方法进行定性评估,并讨论其缺点:

  • A/B/n 测试效率低下,因为它不会通过从观察中学习动态地修改实验。相反,它在固定的时间预算内探索,并且尝试不同选择的概率是预先设定的。它没有利用测试中的早期观察,甚至在某些选择明显表现不佳/表现出色时,也未能及时淘汰或推广这些选择。

  • 它一旦做出决策就无法纠正。如果由于某些原因,测试期间错误地将某个选择识别为最佳(通常是因为测试时间不足),则该选择在生产期间会一直保持固定。因此,无法在剩余的部署期内纠正该决策。

  • 它无法适应动态环境中的变化。与前述问题相关,这种方法在非静态环境中尤其有问题。因此,如果基础的奖励分布随时间发生变化,简单的 A/B/n 测试就无法在选择固定后检测到这些变化。

  • 测试期长度是一个需要调整的超参数,影响测试的效率。如果选择的期限比所需期限短,由于观察中的噪声,可能会错误地宣布某个错误的替代品是最佳选择。如果测试期限选择得太长,将在探索中浪费太多资金。

  • A/B/n 测试很简单。尽管存在这些缺点,但它直观且易于实施,因此在实践中被广泛使用。

因此,普通的 A/B/n 测试对于 MAB 而言是一种相当幼稚的方法。接下来,让我们探讨一些其他更高级的方法,这些方法将克服 A/B/n 测试的一些缺点,首先是ε贪婪。

ε贪婪动作

一种易于实施、有效且广泛使用的探索-利用问题方法被称为ε贪婪动作。这种方法建议在大多数决策中贪心地选择到目前为止根据观察到的奖励最好的动作(即以 1-ε的概率);但偶尔(即以ε的概率),无论动作表现如何,都会随机选择一个动作。在这里,ε是一个介于 0 和 1 之间的数字,通常接近零(例如,0.1),以便在实验中持续探索替代动作。

应用于在线广告场景

现在,让我们将ε贪婪动作应用于我们的在线广告场景:

  1. 我们开始初始化实验所需的变量,用于跟踪动作值估计、每个广告显示次数和奖励的移动平均值:

    eps = 0.1
    n_prod = 100000
    n_ads = len(ads)
    Q = np.zeros(n_ads)
    N = np.zeros(n_ads)
    total_reward = 0
    avg_rewards = []
    

    请注意我们选择 0.1 作为ε值,但这是一个相对随意的选择。不同的ε值将导致不同的性能,因此应将其视为需要调整的超参数。一个更复杂的方法是从较高的ε值开始,逐渐减少。稍后我们将详细讨论这一点。

  2. 接下来,我们进行实验。注意我们如何以ε的概率选择随机动作,否则选择最佳动作。根据我们之前描述的规则更新我们的动作值估计:

    ad_chosen = np.random.randint(n_ads)
    for i in range(n_prod):
        R = ads[ad_chosen].display_ad()
        N[ad_chosen] += 1
        Q[ad_chosen] += (1 / N[ad_chosen]) * (R - Q[ad_chosen])
        total_reward += R
        avg_reward_so_far = total_reward / (i + 1)
        avg_rewards.append(avg_reward_so_far)
        # Select the next ad to display
        if np.random.uniform() <= eps:
            ad_chosen = np.random.randint(n_ads)
        else:
            ad_chosen = np.argmax(Q)
    df_reward_comparison['e-greedy: {}'.format(eps)] = avg_rewards
    
  3. 对不同的ε值(即 0.01、0.05、0.1 和 0.2),运行步骤 1 和 2。然后,比较ε选择如何影响性能,如下:

    greedy_list = ['e-greedy: 0.01', 'e-greedy: 0.05', 'e-greedy: 0.1', 'e-greedy: 0.2']
    df_reward_comparison[greedy_list].iplot(title="ε-Greedy Actions",
     dash=['solid', 'dash', 'dashdot', 'dot'],
     xTitle='Impressions', 
     yTitle='Avg. Reward')
    

    这导致以下输出:

图 2.3 – 使用ε贪婪动作进行探索

图 2.3 – 使用ε贪婪动作进行探索

最佳奖励由ε=0.05 和ε=0.1 分别为 2.97%给出。结果表明,使用其他两个ε值的探索效果要么太低要么太高。此外,所有的ε贪婪策略都比 A/B/n 测试给出了更好的结果,特别是因为在这种情况下 A/B/n 测试做出了错误的选择。

ε贪婪动作的优缺点

让我们讨论使用ε贪婪动作的利弊:

  • ε-贪心策略和 A/B/n 测试在分配探索预算时同样低效且静态。ε-贪心策略也未能及时淘汰明显不好的动作,而是继续将相同的探索预算分配给每一个备选项。例如,在实验进行到一半时,显然广告 A 的表现很差。将探索预算用于尝试区分其他备选项,找出最佳选择,效率会更高。相关的一个问题是,如果某个动作在某一时刻被探索过少或过多,探索预算并不会相应调整。

  • 使用ε-贪心策略时,探索是持续的,这与 A/B/n 测试不同。这意味着,如果环境不是静态的,ε-贪心策略有潜力发现变化,并调整对最佳备选项的选择。然而,在静态环境中,我们可以预期 A/B/n 测试和ε-贪心策略表现相似,因为它们在本质上非常相似,区别仅在于何时进行探索。

  • 通过动态变化ε值,ε-贪心策略可以提高效率。例如,你可以从较高的ε值开始,初期进行更多的探索,之后逐渐降低ε值以进行更多的利用。这样,仍然会有持续的探索,但不像最开始那样频繁,因为那时对环境还没有了解。

  • 通过提高近期观察结果的重要性,可以使ε-贪心策略更加动态。在标准版本中,前述的 值是作为简单平均值来计算的。请记住,在动态环境中,我们可以使用以下公式:

这将以指数方式减小较早观察结果的权重,从而使得该方法能够更容易地察觉环境的变化。

  • 修改ε-贪心策略引入了新的超参数,这些超参数需要调整。前面提到的两个建议——逐渐减小ε值和使用指数平滑来处理Q——都涉及额外的超参数,且可能不容易直观地知道该设置什么值。而且,选择这些超参数不当可能导致比标准版本更差的结果。

到目前为止,一切顺利!我们已经使用ε-贪心策略优化了我们的在线广告活动,并取得了比 A/B/n 测试更好的结果。我们还讨论了如何修改该方法,以便在更广泛的环境中使用。然而,ε-贪心策略的动作选择仍然过于静态,我们可以做得更好。现在,让我们看看另一种方法——上置信区间,它能动态调整动作的探索。

使用上置信区间进行动作选择

上置信界 (UCB) 是一种简单而有效的解决探索与利用权衡问题的方法。其思路是在每一个时间步骤中,我们选择潜在回报最高的动作。动作的潜力通过动作值估计和该估计的不确定性度量之和来计算。这个和就是我们所说的 UCB。因此,选择一个动作的原因可能是我们对该动作值的估计很高,或者该动作还没有得到足够的探索(即,探索的次数不如其他动作),且对其价值的不确定性很高,或者两者兼有。

更正式地说,我们使用以下公式在时间 选择要执行的动作:

让我们稍微拆解一下:

  • 现在,我们使用了一种与之前介绍的略有不同的符号! 和 的含义与之前基本相同。这个公式关注的是变量值,这些值可能是在决策时已经更新过的!,而之前的公式描述的是如何更新这些值。

  • 在这个方程中,平方根项是一个衡量动作值估计不确定性的指标!。

  • 我们选择的次数越多,关于 的不确定性就越小,因此分母中的 项也会减小。

  • 然而,随着时间的推移,由于 项(特别是在环境不是静态的情况下,这一点是有道理的),不确定性会增加,从而鼓励更多的探索。

  • 另一方面,决策时对不确定性的重视程度是由超参数 控制的。显然,这需要调优,而不恰当的选择可能会降低该方法的效果。

现在,是时候看看 UCB 的实际应用了。

应用于在线广告场景

跟着一起实现 UCB 方法来优化广告展示:

  1. 和往常一样,让我们首先初始化必要的变量:

    c = 0.1
    n_prod = 100000
    n_ads = len(ads)
    ad_indices = np.array(range(n_ads))
    Q = np.zeros(n_ads)
    N = np.zeros(n_ads)
    total_reward = 0
    avg_rewards = []
    
  2. 现在,实现主循环来使用 UCB 进行动作选择:

    for t in range(1, n_prod + 1):
        if any(N==0):
            ad_chosen = np.random.choice(ad_indices[N==0])
        else:
            uncertainty = np.sqrt(np.log(t) / N)
            ad_chosen = np.argmax(Q + c * uncertainty)  
        R = ads[ad_chosen].display_ad()
        N[ad_chosen] += 1
        Q[ad_chosen] += (1 / N[ad_chosen]) * (R - Q[ad_chosen])
        total_reward += R
        avg_reward_so_far = total_reward / t
        avg_rewards.append(avg_reward_so_far)
    df_reward_comparison['UCB, c={}'.format(c)] = avg_rewards
    

    请注意,我们在每个时间步骤中选择具有最高 UCB 的动作。如果一个动作还没有被选择过,那么它将具有最高的 UCB。如果有多个动作具有相同的 UCB,我们将随机打破平局。

  3. 如前所述,不同的 选择会导致不同的表现水平。使用不同的 超参数选择运行 步骤 1 和 2。然后,比较结果,如下所示:

    ucb_list = [‹UCB, c=0.1›, ‹UCB, c=1›, ‹UCB, c=10›]
    best_reward = df_reward_comparison.loc[t-1,ucb_list].max()
    df_reward_comparison[ucb_list].iplot(title=»Action Selection using UCB. Best avg. reward: {:.4f}»
                                        .format(best_reward),
                                        dash = [‹solid›, ‹dash›, ‹dashdot›],
                                        xTitle=›Impressions›, 
                                        yTitle=›Avg. Reward›)
    

    这会产生以下输出:

图 2.4 – 使用 UCB 进行探索

图 2.4 – 使用 UCB 进行探索

在这种情况下,使用 UCB 进行探索,在经过一些超参数调优后,取得了比 ε-greedy 探索和 A/B/n 测试更好的结果(3.07% 点击率)!当然,问题的关键在于如何进行超参数调优。有趣的是,这本身就是一个 MAB 问题!首先,你需要形成一组合理的 值,并使用我们到目前为止描述的某种方法选择最佳值。

提示

尝试使用对数尺度的超参数,如 [0.01, 0.1, 1, 10],而不是线性尺度,如 [0.08, 0.1, 0.12, 0.14]。前者允许探索不同数量级的变化,在这些变化中,我们可能会看到显著的性能跳跃。在确定了正确的数量级后,可以使用线性尺度进行搜索。

为了简化事情,你可以使用 A/B/n 测试来选择 。这可能看起来像一个无限循环——你用 MAB 来解决一个 MAB 问题,而 MAB 本身可能还需要调优超参数,依此类推。幸运的是,一旦你为你的问题类型(例如,在线广告)找到了一个合适的 值,通常可以在以后的实验中反复使用该值,只要奖励尺度保持相似(例如,在线广告的点击率大约在 1–3% 之间)。

使用 UCB 的优缺点

最后,让我们讨论一下 UCB 方法的优缺点:

  • UCB 是一种设置后即忘的方法。它系统地并动态地将预算分配给需要探索的选项。如果环境发生变化——例如,如果某个广告由于某种原因变得更受欢迎,奖励结构发生变化——该方法会相应地调整其行动选择。

  • UCB 可以进一步优化以适应动态环境,可能会引入额外的超参数代价。我们提供的 UCB 公式是一个常见的公式,但它可以得到改进——例如,通过使用指数平滑来计算 值。文献中也有更有效的不确定性成分估算方法。然而,这些修改可能会使方法变得更加复杂。

  • UCB 可能难以调优。相比于 ε-greedy 方法中简单地说“我想 10% 的时间用于探索,其余时间用于开发”,要说“我希望我的 为 0.729”对 UCB 方法来说要复杂一些,尤其是在你尝试这些方法来解决一个全新问题时。如果没有调优,UCB 实现可能会给出出乎意料的差结果。

就这样!你现在已经实现了多种解决在线广告问题的方法,并且使用 UCB 方法特别有助于在潜在的非平稳环境中有效管理探索。接下来,我们将介绍另一种非常强大的方法——汤普森抽样,它将是你武器库中的一个重要补充。

汤普森(后验)抽样

在多臂老丨虎丨机(MAB)问题中的目标是估计每个臂(即前面例子中的广告)的奖励分布参数。此外,衡量我们对估计的 uncertainty 是指导探索策略的一个好方法。这个问题非常符合贝叶斯推断框架,而汤普森采样正是利用了这一框架。贝叶斯推断从先验概率分布开始——即参数 的初始假设——并随着数据的到来更新这个先验分布。这里, 指的是正态分布的均值和方差,或伯努利分布中观察到 1 的概率。因此,贝叶斯方法将参数视为给定数据后的随机变量。

这个公式的表达式如下所示:

在这个公式中, 是 先验分布,表示当前对 分布的假设。 代表数据,通过这些数据我们可以得到 后验分布,即 。这就是我们基于所观察到的数据对参数分布的更新假设。 被称为 似然函数(给定参数下观察到数据 的概率),而 被称为 证据。

接下来,让我们探讨如何在 0–1 类型的结果中实现汤普森采样,例如在在线广告场景中出现的情况。

在线广告场景的应用

在我们的例子中,对于给定的广告 ,观察一次点击是一个伯努利随机变量,参数为 ,我们试图对其进行估计。由于 本质上是广告 显示时被点击的概率,因此 CTR 介于 0 和 1 之间。需要注意的是,除了在线广告外,许多问题也具有这样的二元结果。因此,我们在这里的讨论和公式可以扩展到其他类似的情况。

汤普森采样的细节

现在,让我们看看如何将贝叶斯方法应用到我们的问题中:

  • 最初,我们没有理由相信给定广告的参数是高还是低。因此,假设 在 上服从均匀分布是合理的。

  • 假设我们展示了广告 ,并且该广告获得了点击。我们将此视为一个信号,更新 的概率分布,使得期望值略微向 1 移动。

  • 随着我们收集越来越多的数据,我们还应该看到参数的方差估计值逐渐缩小。这正是我们希望平衡探索和利用的方式。当我们使用 UCB 时,我们做了类似的事情:我们将参数的估计值与该估计值的相关不确定性结合起来,以指导探索。汤普森采样使用贝叶斯推断,正是这样做的。

  • 这种方法告诉我们从参数的后验分布中采样,。如果!的期望值较高,我们可能会得到更接近 1 的样本。如果方差较高,因为该广告!在此时尚未被多次选择,我们的样本也将具有较高的方差,这将导致更多的探索。在给定的时间步长内,我们为每个广告采样一次,并选择最大的样本来确定要显示的广告。

在我们的例子中,似然(广告印象转化为点击的概率)是伯努利分布,我们将应用之前描述的逻辑。以下是用较少的技术术语来描述实际情况:

  • 我们想要了解每个广告的 CTR。我们有估计值,但我们对它们不确定,因此我们会为每个 CTR 关联一个概率分布。

  • 随着新数据的到来,我们会更新 CTR 的概率分布。

  • 当需要选择广告时,我们会对每个广告的 CTR 做出猜测——也就是采样!。然后我们选择我们猜测 CTR 最高的广告。

  • 如果某个广告的 CTR 概率分布方差较大,意味着我们对此非常不确定。这将导致我们对该广告做出较为冒险的猜测,并更频繁地选择它,直到方差减少——也就是我们变得更加确信它。

现在,让我们讨论伯努利分布的更新规则。如果你没有完全理解这里的术语也没关系,前面的解释应该能告诉你发生了什么:

  • 一个常见的先验选择是贝塔分布。如果你稍微思考一下,参数!的取值范围在!内。因此,我们需要使用一个具有相同支持度的概率分布来建模!,贝塔分布正好符合这一要求。

  • 此外,如果我们使用贝塔分布作为先验,并将其代入贝叶斯公式与伯努利似然结合,后验分布也会变成贝塔分布。这样,我们就可以在观察到新数据时,将后验分布作为下一次更新的先验。

  • 具有相同分布族的后验与先验相比,带来了极大的便利,甚至它有一个特殊的名称:它们被称为共轭分布,先验被称为似然函数的共轭先验。贝塔分布是伯努利分布的共轭先验。根据你选择的似然建模方式,在实现汤普森抽样之前,可以找到一个共轭先验。

不再废话,接下来我们来实现汤普森抽样用于在线广告的例子。

实现

广告!的先验的贝塔分布由以下公式给出:

这里,!和!是表征贝塔分布的参数,!是伽马函数。别让这个公式吓到你!其实它非常容易实现。为了初始化先验,我们使用!,这使得!在!上均匀分布。一旦我们观察到奖励,!,选择了广告!,我们就能得到如下的后验分布:

s

现在,让我们用 Python 来实现:

  1. 首先,初始化我们需要的变量:

    n_prod = 100000
    n_ads = len(ads)
    alphas = np.ones(n_ads)
    betas = np.ones(n_ads)
    total_reward = 0
    avg_rewards = []
    
  2. 现在,初始化主循环并进行贝叶斯更新:

    for i in range(n_prod):
        theta_samples = [np.random.beta(alphas[k], betas[k]) for k in range(n_ads)]
        ad_chosen = np.argmax(theta_samples)
        R = ads[ad_chosen].display_ad()
        alphas[ad_chosen] += R
        betas[ad_chosen] += 1 - R
        total_reward += R
        avg_reward_so_far = total_reward / (i + 1)
        avg_rewards.append(avg_reward_so_far)
    df_reward_comparison['Thompson Sampling'] = avg_rewards
    

    我们从各自的后验分布中为每个!值抽取样本,并展示与最大抽样参数对应的广告。一旦我们观察到奖励,我们就将后验作为先验,并按照前述规则更新,以获得新的后验。

  3. 然后,展示结果:

    df_reward_comparison['Thompson Sampling'].iplot(title="Thompson Sampling Avg. Reward: {:.4f}"
                                       .format(avg_reward_so_far),
                                        xTitle='Impressions', 
                                        yTitle='Avg. Reward')
    

    这将产生如下输出:

图 2.5 – 使用汤普森抽样的探索

图 2.5 – 使用汤普森抽样的探索

汤普森抽样的表现与ε-贪心和 UCB 方法相似,CTR 为 3%。

汤普森抽样的优缺点

汤普森抽样是一个非常有竞争力的方法,其相较于ε-贪心和 UCB 方法有一个主要的优势:汤普森抽样不需要我们进行任何超参数调优。在实践中,这带来了以下好处:

  • 节省大量时间,这些时间本来会花费在超参数调优上。

  • 节省大量资金,这些资金本来会在其他方法中被浪费在低效的探索和超参数选择错误上。

此外,文献中展示了汤普森抽样在许多基准测试中是一个非常有竞争力的选择,并且在过去几年中越来越受欢迎。

干得好!现在汤普森抽样已经在你的工具箱中了,和其他方法一起,你已经准备好解决现实世界中的 MAB 问题!

总结

在本章中,我们讨论了多臂赌博机(MAB)问题,这是一种一阶强化学习(RL)方法,具有许多实际的商业应用。尽管表面上看似简单,但在 MAB 问题中平衡探索与利用的难度很大,任何在管理这一权衡方面的改进,都能带来成本节省和收入增加。我们介绍了四种方法来解决这一问题:A/B/n 测试、ε-贪婪策略、基于 UCB 的动作选择和汤普森抽样。我们在在线广告场景中实现了这些方法,并讨论了它们的优缺点。

到目前为止,在做决策时,我们并没有考虑到环境中的任何情境信息。例如,在在线广告场景中,我们没有使用任何关于用户的信息(如位置、年龄、历史行为等),这些信息可能对我们的决策算法有所帮助。在下一章中,您将学习一种更高级的多臂赌博机形式——上下文强盗问题,它可以利用这些信息来做出更好的决策。

参考文献

  • Chapelle, O., & Li, L. (2011). 汤普森抽样的实证评估. 神经信息处理系统进展 24,(第 2249-2257 页)

  • Marmerola, G. D. (2017 年 11 月 28 日). 汤普森抽样在上下文强盗问题中的应用. 取自 Guilherme 的博客:gdmarmerola.github.io/ts-for-contextual-bandits/

  • Russo, D., Van Roy, B., Kazerouni, A., Osband, I., & Wen, Z. (2018). 汤普森抽样教程. 机器学习基础与趋势,(第 1-96 页)

第三章:第三章:上下文赌博机

多臂赌博机的一个更高级版本是上下文赌博机(CB)问题,在这种问题中,决策是根据所处的上下文量身定制的。在上一章中,我们识别了在线广告场景中表现最佳的广告。在此过程中,我们没有使用任何关于用户的人物特征、年龄、性别、位置或之前访问的信息,而这些信息本应增加点击的可能性。CB 允许我们利用这些信息,这意味着它们在商业个性化和推荐应用中起着核心作用。

上下文类似于多步强化学习(RL)问题中的状态,唯一的区别是。在多步 RL 问题中,智能体采取的动作会影响它在后续步骤中可能访问的状态。例如,在玩井字棋时,智能体在当前状态下的动作会以某种方式改变棋盘配置(状态),这会影响对手可以采取的动作,依此类推。然而,在 CB 问题中,智能体只是观察上下文,做出决策,并观察奖励。智能体接下来会观察到的上下文并不依赖于当前的上下文/动作。这个设置虽然比多步 RL 简单,但在许多应用中都有出现。因此,本章内容将为你添加一个重要的工具。

我们将继续解决不同版本的在线广告问题,使用更先进的工具,如神经网络,与 CB 模型一起使用。具体而言,在本章中,你将学习以下内容:

  • 为什么我们需要函数逼近

  • 使用函数逼近来处理上下文

  • 使用函数逼近来处理动作

  • 多臂赌博机和 CB 的其他应用

为什么我们需要函数逼近

在解决(上下文)多臂赌博机问题时,我们的目标是从观察中学习每个臂(动作)的行动值,我们将其表示为 。在在线广告的例子中,它代表了我们对用户点击广告的概率的估计,如果我们展示了 。现在,假设我们对看到广告的用户有两条信息,分别如下:

  • 设备类型(手机或台式机)

  • 位置(国内/美国或国际/非美国)

广告表现很可能会因设备类型和位置的不同而有所差异,这些差异构成了这个例子中的上下文。因此,CB 模型将利用这些信息,估计每个上下文的行动值,并相应地选择行动。

这看起来像是为每个广告填写一个类似于以下内容的表格:

表格 3.1 – 广告 D 的样本行动值

表格 3.1 – 广告 D 的样本行动值

这意味着解决四个 MAB 问题,每个上下文一个:

  • 手机 – 国内

  • 手机 – 国际

  • 台式机 – 国内

  • 台式机 – 国际

虽然在这个简单的例子中可以正常工作,但考虑到当你将额外的信息(例如年龄)添加到上下文中时会发生什么,这就引入了许多挑战:

  • 首先,我们可能没有足够的观察数据来(准确地)学习每个上下文的动作值(例如:移动设备、国际、57 岁)。然而,我们希望能够进行跨学习,并且如果我们有接近年龄的用户数据,就可以估计 57 岁用户的动作值(或者改进该估计)。

  • 第二,可能的上下文数量增加了 100 倍。当然,我们可以通过定义年龄组来缓解这个问题,但这样我们就需要花时间和数据来校准这些组,这并不是一件简单的事。此外,上下文空间的增长将会更受限制(增长系数为 10 而不是 100),但仍然是指数级的。随着我们向上下文中添加越来越多的维度,这在任何现实的实现中都是非常可能的,问题可能很容易变得无法处理。

接下来,我们使用函数逼近来解决这个问题。这将使我们能够处理非常复杂和高维的上下文。稍后,我们还将使用函数逼近来处理动作,这将使我们能够处理变化和/或高维的动作空间。

使用函数逼近处理上下文

函数逼近使我们能够从我们已经观察到的数据(如上下文和广告点击)中建模过程的动态。像上一章一样,考虑一个在线广告场景,其中有五个不同的广告(A、B、C、D 和 E),上下文包括用户设备、位置和年龄。在本节中,我们的智能体将学习五个不同的 Q 函数,每个广告一个,每个广告接收一个上下文 ,并返回动作值估计。这在图 3.1 中进行了说明:

图 3.1 – 我们为每个动作学习一个函数,接收上下文并返回动作值

图 3.1 – 我们为每个动作学习一个函数,接收上下文并返回动作值

在这一点上,我们需要解决一个有监督的机器学习问题,针对每个动作。我们可以使用不同的模型来获得 Q 函数,例如逻辑回归或神经网络(实际上,这允许我们使用一个单一的网络来估计所有动作的值)。一旦我们选择了函数逼近的类型,就可以使用我们在上一章中介绍的探索策略,来确定在给定上下文下要展示的广告。但首先,我们先创建一个合成过程来生成模仿用户行为的点击数据。

案例研究 – 使用合成用户数据的上下文在线广告

假设用户的真实点击行为遵循逻辑函数:

这里, 是用户在上下文 和广告 展示时点击的概率。另假设 device 对于移动设备为 1,其他设备为 0;location 对于美国为 1,其他地区为 0。这里有两个需要注意的重要事项:

  • 这种行为,特别是 参数,对于广告商来说是未知的,他们将尝试揭示这些信息。

  • 请注意 上标在 中,表示这些因素对用户行为的影响可能因广告而异。

现在,让我们在 Python 中实现这一点,按照以下步骤:

Chapter03/Contextual Bandits.ipynb

  1. 首先,我们需要导入所需的 Python 包:

    import numpy as np
    import pandas as pd
    from scipy.optimize import minimize
    from scipy import stats
    import plotly.offline
    from plotly.subplots import make_subplots
    import plotly.graph_objects as go
    import cufflinks as cf
    cf.go_offline()
    cf.set_config_file(world_readable=True, theme='white') 
    

    这些包括用于科学计算的库,如 NumPy 和 SciPy,以及强大的可视化工具 Plotly。

  2. 现在,我们创建一个 UserGenerator 类来模拟用户动态。在这里设置一些真实的 参数,广告商(代理)将尝试学习这些参数:

    class UserGenerator(object):
        def __init__(self):
            self.beta = {}
            self.beta['A'] = np.array([-4, -0.1, -3, 0.1]) 
            self.beta['B'] = np.array([-6, -0.1, 1, 0.1])
            self.beta['C'] = np.array([2, 0.1, 1, -0.1])
            self.beta['D'] = np.array([4, 0.1, -3, -0.2])
            self.beta['E'] = np.array([-0.1, 0, 0.5, -0.01])
            self.context = None    
    
  3. 让我们定义生成点击或无点击的方法,给定用户的上下文:

        def logistic(self, beta, context):
            f = np.dot(beta, context)
            p = 1 / (1 + np.exp(-f))
            return p
        def display_ad(self, ad):
            if ad in ['A', 'B', 'C', 'D', 'E']:
                p = self.logistic(self.beta[ad], self.context)
                reward = np.random.binomial(n=1, p=p)
                return reward
            else:
                raise Exception('Unknown ad!') 
    

    请注意,每个广告都有一组不同的 值。当广告展示给用户时,logistic 方法会计算点击的概率,而 display_ad 方法会根据这个概率生成一个点击。

  4. 我们定义了一个方法,将随机生成不同上下文的用户:

        def generate_user_with_context(self):
            # 0: International, 1: U.S.
            location = np.random.binomial(n=1, p=0.6)
            # 0: Desktop, 1: Mobile
            device = np.random.binomial(n=1, p=0.8)
            # User age changes between 10 and 70, 
            # with mean age 34
            age = 10 + int(np.random.beta(2, 3) * 60)
            # Add 1 to the concept for the intercept
            self.context = [1, device, location, age]
            return self.context
    

    如你所见,generate_user_with_context 方法生成一个 60%概率的美国用户。此外,广告有 80%的概率在移动设备上展示。最后,用户年龄范围为 10 到 70 岁,平均年龄为 34 岁。这些数字是为了示例目的我们设定的,具有一定的随意性。为了简化,我们不假设这些用户属性之间存在任何相关性。你可以修改这些参数并引入相关性,以创建更现实的场景。

  5. 我们可以创建一些函数(在类外部)来可视化,直观地展示上下文与与之相关的点击概率之间的关系。为此,我们需要一个函数来为给定的广告类型和数据创建散点图:

    def get_scatter(x, y, name, showlegend):
        dashmap = {'A': 'solid',
                   'B': 'dot',
                   'C': 'dash',
                   'D': 'dashdot',
                   'E': 'longdash'}
        s = go.Scatter(x=x, 
                       y=y, 
                       legendgroup=name, 
                       showlegend=showlegend,
                       name=name, 
                       line=dict(color='blue', 
                                 dash=dashmap[name]))
        return s 
    
  6. 现在,我们定义一个函数来绘制点击概率如何随年龄变化,在不同的子图中展示每种设备类型和位置的组合:

    def visualize_bandits(ug):
        ad_list = 'ABCDE'
        ages = np.linspace(10, 70)
        fig = make_subplots(rows=2, cols=2, 
                subplot_titles=("Desktop, International", 
                                "Desktop, U.S.", 
                                "Mobile, International", 
                                "Mobile, U.S."))
        for device in [0, 1]:
            for loc in [0, 1]:
                showlegend = (device == 0) & (loc == 0)
                for ad in ad_list:
                    probs = [ug.logistic(ug.beta[ad], 
                              [1, device, loc, age]) 
                                     for age in ages]
                    fig.add_trace(get_scatter(ages, 
                                              probs, 
                                              ad, 
                                              showlegend), 
                               row=device+1, 
                               col=loc+1)             
        fig.update_layout(template="presentation")
        fig.show()
    
  7. 现在,我们创建一个对象实例来生成用户并可视化用户行为:

    ug = UserGenerator()
    visualize_bandits(ug)
    

    输出如 图 3.2 所示:

图 3.2 – 给定上下文的真实广告点击概率比较(x 轴:年龄,y 轴:点击概率)

图 3.2 – 给定上下文的真实广告点击概率比较(x 轴:年龄,y 轴:点击概率)

看着图 3.2中的图表,我们应该期望我们的算法能够识别出,例如,对于大约 40 岁、来自美国并使用移动设备的用户,展示广告 E。同时,请注意,这些概率不现实地偏高。为了使logistic类中的p计算更为现实,我们将调整其值为 0.05。为了简化问题,暂时保持这种方式。

现在,我们已经实现了一个生成用户点击的过程。以下是这一场景的流程:

  1. 我们将生成一个用户,并使用ug对象中的generate_user_with_context方法获取相关的上下文信息。

  2. 一个 CB 模型将利用上下文来展示五个广告中的一个:A、B、C、D 或 E。

  3. 选定的广告将传递给ug对象中的display_ad方法,给予奖励 1(点击)或 0(未点击)。

  4. CB 模型将根据奖励进行训练,并且这一循环将不断进行。

在实际实现这个流程之前,让我们深入了解将要使用的 CB 方法。

使用正则化逻辑回归的函数逼近

我们希望我们的 CB 算法能够观察用户对广告的反应,更新估计行动值的模型(函数逼近),并根据上下文、行动值估计以及探索策略来决定展示哪个广告。需要注意的是,在大多数现实设置中,用户流量较高,模型通常不会在每次观察后更新,而是在一批观察之后更新。因此,让我们先讨论使用哪种函数逼近器。我们有许多选择,包括许多为 CB 设计的定制和复杂算法。这些模型大多数基于以下几种方法:

  • 逻辑回归

  • 决策树/随机森林

  • 神经网络

在探索策略方面,我们将继续关注以下三种基本方法:

  • ε-贪心算法(ε-greedy)

  • 上置信界(Upper Confidence Bounds)

  • 汤普森/贝叶斯采样

现在,假设作为主题专家,我们知道 CTR 可以通过逻辑回归来建模。我们还提到过,更新模型在每次观察后进行并不实际,因此我们更倾向于对模型进行批量更新。最后,我们希望在探索工具箱中加入汤普森采样(Thompson sampling),因此我们需要获得逻辑回归模型参数的后验分布。为此,我们使用一种正则化逻辑回归算法,并由代理提供批量更新(Chapelle 等,2011)。该算法执行以下操作:

  • 通过高斯分布来近似模型权重的后验分布。这样,我们可以在下一个批次中将后验分布作为先验,并且可以使用高斯分布作为似然函数,因为高斯族是自共轭的。

  • 使用对角协方差矩阵表示权重,这意味着我们假设权重之间不相关。

  • 使用拉普拉斯近似来获取权重分布的均值和方差估计,这是统计学中常用的估计后验参数的方法之一,假设后验是高斯分布。

    信息

    你可以在bookdown.org/rdpeng/advstatcomp/laplace-approximation.html了解更多关于拉普拉斯近似计算后验均值的内容。

接下来,我们来看看这个算法的实际应用。

实现正则化逻辑回归

我们将按照以下步骤实现正则化逻辑回归,之后将用它:

  1. 首先,我们创建一个类并初始化我们将跟踪的参数:

    class RegularizedLR(object):
        def __init__(self, name, alpha, rlambda, n_dim):
            self.name = name
            self.alpha = alpha
            self.rlambda = rlambda
            self.n_dim = n_dim
            self.m = np.zeros(n_dim)
            self.q = np.ones(n_dim) * rlambda
            self.w = self.get_sampled_weights()
    

    让我们更好地理解这些参数是什么:

    a) name 用于识别对象实例正在估算的广告动作值。我们为每个广告都有一个单独的模型,并且根据各自的点击数据分别更新它们。

    b) alpha超参数控制探索与利用之间的权衡。较小的值减少方差(例如,0.25),从而鼓励利用。

    c) 这是一种正则化回归,意味着我们有一个正则化项,λ。它是一个需要调整的超参数。我们也用它来初始化q数组。

    d) n_dim表示参数向量的维度,每个上下文输入的元素对应一个,以及一个偏置项。

    e) 逻辑函数的权重由w数组表示,其中w[i]对应我们赌博机动态模型中的。

    f) w[i]的均值估计由m[i]给出,方差估计由q[i]的倒数给出。

  2. 然后,我们定义一个方法来对逻辑回归函数的参数进行采样:

        def get_sampled_weights(self): 
            w = np.random.normal(self.m, self.alpha * self.q**(-1/2))
            return w
    

    注意,我们需要这个方法来使用汤普森采样,它要求从后验中对w数组参数进行采样,而不是使用均值。这里的后验是一个正态分布。

  3. 定义损失函数和拟合函数,后者将执行训练:

        def loss(self, w, *args):
            X, y = args
            n = len(y)
            regularizer = 0.5 * np.dot(self.q, (w - self.m)**2)
            pred_loss = sum([np.log(1 + np.exp(np.dot(w, X[j])))
                                        - y[j] * np.dot(w, X[j]) for j in range(n)])
            return regularizer + pred_loss
        def fit(self, X, y):
            if y:
                X = np.array(X)
                y = np.array(y)
                minimization = minimize(self.loss, 
                                        self.w, 
                                        args=(X, y), 
                                        method="L-BFGS-B", 
                                        bounds=[(-10,10)]*3 + [(-1, 1)],
                                        options={'maxiter': 50})
                self.w = minimization.x
                self.m = self.w
                p = (1 + np.exp(-np.matmul(self.w, X.T)))**(-1)
                self.q = self.q + np.matmul(p * (1 - p), X**2)
    

    让我们详细说明拟合部分是如何工作的:

    a) 我们使用fit方法和loss函数通过给定的上下文和相关的点击数据(点击为 1,未点击为 0)来更新模型。

    b) 我们使用 SciPy 的 minimize 函数进行模型训练。为了防止指数项中的数值溢出,我们对w施加了边界。根据输入值的范围,这些边界需要进行调整。对于设备类型的二进制特征和年龄输入,位置[-10, +10]和[-1, +1]分别是我们用例中的合理范围。

    c) 在每次使用新一批数据更新模型时,之前的w值作为先验。

  4. 实现预测的上置信界限,这是我们将在实验中使用的探索方法之一:

        def calc_sigmoid(self, w, context):
            return 1 / (1 + np.exp(-np.dot(w, context)))
        def get_ucb(self, context):
            pred = self.calc_sigmoid(self.m, context)
            confidence = self.alpha * np.sqrt(np.sum(np.divide(np.array(context)**2, self.q)))
            ucb = pred + confidence
            return ucb
    
  5. 实现两种预测方法,一种使用均值参数估计,另一种使用采样参数,并与汤普森采样结合使用:

        def get_prediction(self, context):
            return self.calc_sigmoid(self.m, context)
        def sample_prediction(self, context):
            w = self.get_sampled_weights()
            return self.calc_sigmoid(w, context)
    

现在,在实际开始解决问题之前,我们将定义一个度量标准来比较不同的探索策略。

目标——遗憾最小化

用于比较多臂老丨虎丨机(MAB)和上下文绑定(CB)算法的一个常见指标叫做遗憾。我们通过以下公式定义总遗憾,直到我们观察到用户:

这里,是用户的上下文,是应该采取的最佳行动(广告),它能带来最高的预期点击率(CTR),而是所选行动(广告)的预期点击率。需要注意的是,我们能够计算遗憾是因为我们可以访问真实的行动值(预期 CTR),而在现实中并不具备这一条件(尽管遗憾仍然可以被估计)。请注意,任何步骤中的最小遗憾值为零。

提示

使用良好的探索策略,我们应该看到随着算法发现最佳行动,累积遗憾会随着时间的推移而逐渐减缓。

我们将使用以下代码根据上下文和选择的广告来计算遗憾:

def calculate_regret(ug, context, ad_options, ad):
    action_values = {a: ug.logistic(ug.beta[a], context) for a in ad_options}
    best_action = max(action_values, key=action_values.get)
    regret = action_values[best_action] - action_values[ad]
    return regret, best_action

最后,让我们编写代码,使用不同的探索策略来实际解决这个问题。

解决在线广告问题

由于我们已经定义了所有辅助方法来使用之前提到的三种探索策略,按策略选择行动将变得非常简单。现在,我们来实现这些策略的相关函数:

  1. 我们从编写一个函数开始,来实现ε-贪心策略,该策略大多数时候选择最佳行动,其他时候则探索随机行动:

    def select_ad_eps_greedy(ad_models, context, eps):
        if np.random.uniform() < eps:
            return np.random.choice(list(ad_models.keys()))
        else:
            predictions = {ad: ad_models[ad].get_prediction(context) 
                           for ad in ad_models}
            max_value = max(predictions.values()); 
            max_keys = [key for key, value in predictions.items() if value == max_value]
            return np.random.choice(max_keys)
    
  2. 接下来,我们编写一个函数,使用上置信度界限进行行动选择:

    def select_ad_ucb(ad_models, context):
        ucbs = {ad: ad_models[ad].get_ucb(context) 
                       for ad in ad_models}
        max_value = max(ucbs.values()); 
        max_keys = [key for key, value in ucbs.items() if value == max_value]
        return np.random.choice(max_keys)
    
  3. 然后,我们定义一个函数来实现使用汤普森采样的行动选择:

    def select_ad_thompson(ad_models, context):
        samples = {ad: ad_models[ad].sample_prediction(context) 
                       for ad in ad_models}
        max_value = max(samples.values()); 
        max_keys = [key for key, value in samples.items() if value == max_value]
        return np.random.choice(max_keys)
    
  4. 最后,我们进行实际的实验,依次运行并比较每种策略。我们从初始化广告名称、实验名称和必要的数据结构开始:

    ad_options = ['A', 'B', 'C', 'D', 'E']
    exploration_data = {}
    data_columns = ['context', 
                    'ad', 
                    'click', 
                    'best_action', 
                    'regret', 
                    'total_regret']
    exploration_strategies = ['eps-greedy', 
                              'ucb', 
                              'Thompson']
    
  5. 我们需要实现一个外部的for循环来启动一个干净的实验,针对每一种探索策略。我们初始化所有算法参数和数据结构:

    for strategy in exploration_strategies:
        print("--- Now using", strategy)
        np.random.seed(0)
        # Create the LR models for each ad
        alpha, rlambda, n_dim = 0.5, 0.5, 4
        ad_models = {ad: RegularizedLR(ad, 
                                       alpha, 
                                       rlambda, 
                                       n_dim) 
                     for ad in 'ABCDE'}
        # Initialize data structures
        X = {ad: [] for ad in ad_options}
        y = {ad: [] for ad in ad_options}
        results = []
        total_regret = 0
    
  6. 现在,我们实现一个内部循环,用于在 10K 次用户展示中运行活跃的策略:

        for i in range(10**4):
            context = ug.generate_user_with_context()
            if strategy == 'eps-greedy':
                eps = 0.1
                ad = select_ad_eps_greedy(ad_models, 
                                          context,
                                          eps)
            elif strategy == 'ucb':
                ad = select_ad_ucb(ad_models, context)
            elif strategy == 'Thompson':
                ad = select_ad_thompson(ad_models, context)
            # Display the selected ad
            click = ug.display_ad(ad)
            # Store the outcome
            X[ad].append(context)
            y[ad].append(click)
            regret, best_action = calculate_regret(ug, 
                                                   context, 
                                                   ad_options, 
                                                   ad)
            total_regret += regret
            results.append((context, 
                            ad, 
                            click, 
                            best_action, 
                            regret, 
                            total_regret))
            # Update the models with the latest batch of data
            if (i + 1) % 500 == 0:
                print("Updating the models at i:", i + 1)
                for ad in ad_options:
                    ad_models[ad].fit(X[ad], y[ad])
                X = {ad: [] for ad in ad_options}
                y = {ad: [] for ad in ad_options}
    
        exploration_data[strategy] = {'models': ad_models,
                           'results': pd.DataFrame(results, 
                                             columns=data_columns)}
    

    让我们逐步分析一下:

    a) 我们生成一个用户,并根据上下文来决定在每次迭代中展示哪个广告,依据的是探索策略。

    b) 我们观察并记录结果。我们还会在每次展示后计算遗憾,以便比较不同策略的效果。

    c) 我们以批量的方式更新逻辑回归模型,也就是每当有 500 次广告展示后进行一次更新。

  7. 执行完此代码块后,我们可以使用以下代码来可视化结果:

    df_regret_comparisons = pd.DataFrame({s: exploration_data[s]['results'].total_regret
                                         for s in exploration_strategies})
    df_regret_comparisons.iplot(dash=['solid', 'dash','dot'],
                                xTitle='Impressions', 
                                yTitle='Total Regret',
                                color='black')
    

    这将生成图 3.3 中所示的图表:

    图 3.3 – 在线广告示例中探索策略的比较

    图 3.3 – 在线广告示例中探索策略的比较

    我们清楚地看到,汤普森采样优于ε-贪婪和 UCB 的alpha,后者可能会导致更好的表现。但这正是重点:汤普森采样提供了一种非常有效的探索策略,几乎可以开箱即用。这是Chapelle 等人,2011通过实验证明的,并帮助该方法在它被提出近一个世纪后获得了广泛的关注。

    提示

    在实际生产系统中,使用维护良好的库来处理 CB 中的监督学习部分,比我们这里做的自定义实现要更为合理。一个用于概率编程的库是 PyMC3(docs.pymc.io/)。使用 PyMC3,你可以将监督学习模型拟合到你的数据上,然后对模型参数进行采样。作为一个练习,考虑在 PyMC3 中使用逻辑回归模型实现汤普森采样。

  8. 让我们通过可视化模型的参数估计来结束这一部分。例如,当我们使用ε-贪婪策略时,广告 A 的系数被估计如下:

    lrmodel = exploration_data['eps-greedy']['models']['A']
    df_beta_dist = pd.DataFrame([], index=np.arange(-4,1,0.01))
    mean = lrmodel.m
    std_dev = lrmodel.q ** (-1/2)
    for i in range(lrmodel.n_dim):
        df_beta_dist['beta_'+str(i)] = stats.norm(loc=mean[i], 
                                                  scale=std_dev[i]).pdf(df_beta_dist.index)
    
    df_beta_dist.iplot(dash=['dashdot','dot', 'dash', 'solid'],
                       yTitle='p.d.f.',
                       color='black')
    

    这会生成以下输出:

图 3.4 – 实验结束时,使用ε-贪婪探索的广告 A 的后验分布可视化

图 3.4 – 实验结束时,使用ε-贪婪探索的广告 A 的后验分布可视化

逻辑回归模型的系数估计为,而实际的系数为。该模型对于的估计特别确定,这在图中的分布非常狭窄。

做得非常好!这是一个相当长的练习,但它将为你在实际应用中的成功奠定基础。深呼吸一下,休息片刻,接下来我们将看看一个更为现实的在线广告版本,其中广告库存随时间变化。

使用函数逼近来处理行动

在我们到目前为止的在线广告示例中,我们假设有一组固定的广告(行动/臂)可供选择。然而,在许多 CB 的应用中,可用的行动集合是随着时间变化的。以一个现代广告网络为例,该网络使用广告服务器将广告匹配到网站/应用。这是一个非常动态的操作,涉及到的,抛开定价不谈,主要有三个组成部分:

  • 网站/应用内容

  • 观众/用户画像

  • 广告库存

之前,我们只考虑了用户配置文件作为上下文。广告服务器需要额外考虑网站/应用内容,但这并没有真正改变我们之前解决的问题结构。然而,现在,由于广告库存是动态的,我们不能为每个广告使用单独的模型。我们通过将广告特征输入单一模型来处理这个问题。这在图 3.5中有所说明:

图 3.5 – 在广告网络示例中,带有上下文和动作输入的行动值的函数近似

图 3.5 – 在广告网络示例中,带有上下文和动作输入的行动值的函数近似

在做出决策时,我们将上下文视为已知条件。因此,决策是关于从当前可用广告库存中展示哪个广告。因此,为了做出这个决定,我们使用这个单一模型为所有可用广告生成行动值。

现在是时候谈谈在这种情况下使用什么样的模型了:

  • 记住模型的作用:它学习了给定用户在给定网站/应用上看到的给定广告后的反应,并估计点击的概率。

  • 当你考虑所有可能的用户和网站/应用上下文,以及所有可能的广告时,这是一个非常复杂的关系需要弄清楚。

  • 这样的模型需要在大量数据上进行训练,并且应足够复杂,以能够提供真实点击动态的良好近似。

  • 当我们面对如此复杂性,并希望有足够的数据时,有一个明显的选择:深度神经网络(DNNs)。

在前一节中,我们比较了不同的探索策略,并展示了汤普森抽样是一个非常有竞争力的选择。然而,汤普森抽样要求我们能够从模型参数的后验分布中进行采样;对于诸如神经网络之类的复杂模型,这通常是不可行的。为了克服这一挑战,我们依赖于文献中提供的近似贝叶斯方法。

信息

有许多近似方法,它们的比较超出了这里的范围。Riquelme 等人,2018在 TensorFlow 存储库中提供了一个很好的比较及其代码。

一个这些近似方法包括在深度神经网络中使用辍学正则化,并在推断时保持其活跃。作为提醒,辍学正则化会以给定概率停用 DNN 中的每个神经元,并增加泛化能力。通常,辍学仅在训练期间使用。当推断时保持其活跃时,由于神经元以概率方式被禁用,输出相应变化。Gal 等人,2015显示,这类似于近似贝叶斯推断,这是我们用于汤普森抽样所需的。

案例研究 – 利用来自美国人口普查的用户数据进行上下文在线广告投放

现在,我们来谈谈这一部分将使用的示例。之前,我们设计了自己的示例。这一次,我们将使用一个经过修改的数据集,该数据集来源于 1994 年的美国人口普查,并将其调整为在线广告场景。这个数据集被称为人口普查收入数据集,可以在archive.ics.uci.edu/ml/datasets/Census+Income找到。

在这个数据集中,我们使用了参与普查的个人的以下信息:年龄、工作类别、教育、婚姻状况、职业、关系、种族、性别、每周工作小时数、原籍国和收入水平。

接下来,让我们讨论如何将这些数据转化为在线广告场景。

场景描述

假设有一个广告服务器,它知道用户的所有前述信息,除了教育水平。另一方面,广告网络管理着针对特定教育水平的广告。例如,在任何给定时刻,广告服务器可能有一则广告针对接受过大学教育的用户,另一则广告则针对接受过小学教育的用户。如果展示的广告目标用户的教育水平与用户的实际教育水平相匹配,则点击的概率很高。如果不匹配,随着目标教育水平与用户教育水平之间的差距增大,点击的概率会逐渐下降。换句话说,广告服务器在隐式地尝试尽可能准确地预测用户的教育水平。

接下来,我们准备为这个场景提供数据集。

数据准备

按照以下步骤清理和准备数据:

  1. 我们首先导入稍后将使用的必要包:

    from collections import namedtuple
    from numpy.random import uniform as U
    import pandas as pd
    import numpy as np
    import io
    import requests
    from tensorflow import keras
    from tensorflow.keras.layers import Dense, Dropout
    import cufflinks as cf
    cf.go_offline()
    cf.set_config_file(world_readable=True, theme='white')
    
  2. 接下来,我们需要下载数据并选择感兴趣的列:

    url="https://archive.ics.uci.edu/ml/machine-learning-databases/adult/adult.data"
    s=requests.get(url).content
    names = ['age', 
               'workclass', 
               'fnlwgt', 
               'education',
               'education_num',
               'marital_status',
               'occupation',
               'relationship',
               'race',
               'gender',
               'capital_gain',
               'capital_loss',
               'hours_per_week',
               'native_country',
              'income']
    usecols = ['age', 
               'workclass', 
               'education',
               'marital_status',
               'occupation',
               'relationship',
               'race',
               'gender',
               'hours_per_week',
               'native_country',
               'income']
    df_census = pd.read_csv(io.StringIO(s.decode('utf-8')), 
                            sep=',',
                            skipinitialspace=True,
                            names=names,
                            header=None,
                            usecols=usecols)
    
  3. 让我们删除包含缺失数据的行,这些行用?标记:

    df_census = df_census.replace('?', np.nan).dropna()
    

    通常,一个缺失的条目本身可能是一个有价值的指示,模型可以使用它。此外,仅因为某个条目缺失而删除整行数据也有些浪费。然而,数据插补超出了本讨论的范围,所以我们继续专注于 CB 问题。

  4. 我们还将不同的教育水平合并为四个类别:小学、中学、本科和研究生:

    edu_map = {'Preschool': 'Elementary',
               '1st-4th': 'Elementary',
               '5th-6th': 'Elementary',
               '7th-8th': 'Elementary',
               '9th': 'Middle',
               '10th': 'Middle',
               '11th': 'Middle',
               '12th': 'Middle',
               'Some-college': 'Undergraduate',
               'Bachelors': 'Undergraduate',
               'Assoc-acdm': 'Undergraduate',
               'Assoc-voc': 'Undergraduate',
               'Prof-school': 'Graduate',
               'Masters': 'Graduate',
               'Doctorate': 'Graduate'}
    for from_level, to_level in edu_map.items():
        df_census.education.replace(from_level, to_level, inplace=True)
    
  5. 接下来,我们将分类数据转换为独热编码(one-hot vectors),以便输入到 DNN 中。我们保留教育列不变,因为它不是上下文的一部分:

    context_cols = [c for c in usecols if c != 'education']
    df_data = pd.concat([pd.get_dummies(df_census[context_cols]),
               df_census['education']], axis=1)
    

    通过在开始时进行此转换,我们假设我们知道所有可能的工作类别和原籍国类别。

就这样!我们已经准备好数据。接下来,我们实现逻辑,通过用户的实际教育水平和广告所针对的教育水平来模拟广告点击。

模拟广告点击

在这个示例中,广告的可用性是随机的,广告点击是随机的。我们需要设计一些逻辑来模拟这种行为:

  1. 让我们先确定每个教育类别的广告可用性概率,并实现广告的抽样:

    def get_ad_inventory():
        ad_inv_prob = {'Elementary': 0.9, 
                       'Middle': 0.7, 
                       'HS-grad': 0.7, 
                       'Undergraduate': 0.9, 
                       'Graduate': 0.8}
        ad_inventory = []
        for level, prob in ad_inv_prob.items():
            if U() < prob:
                ad_inventory.append(level)
        # Make sure there are at least one ad
        if not ad_inventory:
            ad_inventory = get_ad_inventory()
        return ad_inventory
    

    如前所述,广告服务器每个目标组最多只有一个广告。我们还确保库存中至少有一个广告。

  2. 然后,我们定义一个函数,通过概率生成一个点击,其中点击的可能性随着用户教育水平和广告目标匹配的程度而增加:

    def get_ad_click_probs():
        base_prob = 0.8
        delta = 0.3
        ed_levels = {'Elementary': 1, 
                     'Middle': 2, 
                     'HS-grad': 3, 
                     'Undergraduate': 4, 
                     'Graduate': 5}
        ad_click_probs = {l1: {l2: max(0, base_prob - delta * abs(ed_levels[l1]- ed_levels[l2])) for l2 in ed_levels}
                               for l1 in ed_levels}
        return ad_click_probs
    def display_ad(ad_click_probs, user, ad):
        prob = ad_click_probs[ad][user['education']]
        click = 1 if U() < prob else 0
        return click
    

    所以,当一个广告展示给用户时,如果广告的目标匹配用户的教育水平,则点击概率为 。每当不匹配一个级别时,这个概率会减少 。例如,一个拥有高中学历的人点击一个针对小学毕业生(或大学毕业生)用户组的广告的概率为 。注意,CB 算法并不知道这些信息,它只会用于模拟点击。

我们已经设置好了问题。接下来,我们将转向实现一个 CB 模型。

使用神经网络的函数逼近

如前所述,我们使用一个(并不那么)DNN,它将在给定上下文和行动的情况下估计行动值。我们将使用的 DNN 有两层,每层有 256 个隐藏单元。这个模型使用 Keras——TensorFlow 的高级 API——来创建相当简单。

提示

请注意,在我们的模型中,我们使用了 dropout,并且在推理时将其保持激活状态,作为我们为 Thompson 采样所需的贝叶斯近似。这是通过在 dropout 层设置 training=True 来配置的。

网络输出一个标量,这是给定上下文和行动特征(目标用户组)下的行动值估计。使用二元交叉熵最适合这种输出,所以我们将在模型中使用它。最后,我们将使用流行的 Adam 优化器。

信息

如果你需要开始使用 Keras 或者回顾一下,访问 www.tensorflow.org/guide/keras。使用它构建标准的 DNN 模型非常简单。

现在,让我们创建用于模型创建和更新的函数:

  1. 我们创建一个函数,返回一个已编译的 DNN 模型,给定输入维度和一个 dropout 比率:

    def get_model(n_input, dropout):
        inputs = keras.Input(shape=(n_input,))
        x = Dense(256, activation='relu')(inputs)
        if dropout > 0:
            x = Dropout(dropout)(x, training=True)
        x = Dense(256, activation='relu')(x)
        if dropout > 0:
            x = Dropout(dropout)(x, training=True)
        phat = Dense(1, activation='sigmoid')(x)
        model = keras.Model(inputs, phat)
        model.compile(loss=keras.losses.BinaryCrossentropy(),
                      optimizer=keras.optimizers.Adam(),
                      metrics=[keras.metrics.binary_accuracy])
        return model
    
  2. 当数据可用时,我们将按批次更新这个模型。接下来,编写一个函数,用每个批次训练模型 10 个周期:

    def update_model(model, X, y):
        X = np.array(X)
        X = X.reshape((X.shape[0], X.shape[2]))
        y = np.array(y).reshape(-1)
        model.fit(X, y, epochs=10)
        return model
    
  3. 然后,我们定义一个函数,返回基于目标教育水平的指定广告的一-hot 表示:

    def ad_to_one_hot(ad):
        ed_levels = ['Elementary', 
                     'Middle', 
                     'HS-grad', 
                     'Undergraduate', 
                     'Graduate']
        ad_input = [0] * len(ed_levels)
        if ad in ed_levels:
            ad_input[ed_levels.index(ad)] = 1
        return ad_input
    
  4. 我们实现了 Thompson 采样来根据上下文和手头的广告库存选择一个广告:

    def select_ad(model, context, ad_inventory):
        selected_ad = None
        selected_x = None
        max_action_val = 0
        for ad in ad_inventory:
            ad_x = ad_to_one_hot(ad)
            x = np.array(context + ad_x).reshape((1, -1))
            action_val_pred = model.predict(x)[0][0]
            if action_val_pred >= max_action_val:
                selected_ad = ad
                selected_x = x
                max_action_val = action_val_pred
        return selected_ad, selected_x
    

    要显示的广告是基于我们从 DNN 获得的最大行动值估计来选择的。我们通过尝试库存中所有可用的广告来获得这一点——请记住,我们每个目标用户组最多只有一个广告——并结合用户的上下文。注意,目标用户组等同于action,我们以一-hot 向量的格式将其输入到 DNN。

  5. 最后,我们编写一个函数,通过从数据集中随机选择来生成用户。该函数将返回用户数据以及派生的上下文:

    def generate_user(df_data):
        user = df_data.sample(1)
        context = user.iloc[:, :-1].values.tolist()[0]
        return user.to_dict(orient='records')[0], context
    

这就结束了我们使用汤普森采样来决定展示广告所需的步骤。

计算后悔值

我们将继续使用后悔值来比较 CB 算法的不同版本。计算方法如下:

def calc_regret(user, ad_inventory, ad_click_probs, ad_selected):
    this_p = 0
    max_p = 0
    for ad in ad_inventory:
        p = ad_click_probs[ad][user['education']]
        if ad == ad_selected:
            this_p = p
        if p > max_p:
            max_p = p
    regret = max_p - this_p
    return regret

在后悔值计算也完成后,让我们现在实际解决问题。

解决在线广告问题

现在我们准备将所有这些组件整合在一起。我们将在 5,000 次展示中尝试使用不同的丢弃概率的算法。每进行 500 次迭代后,我们会更新 DNN 参数。以下是 Python 中不同丢弃率的实现:

ad_click_probs = get_ad_click_probs()
df_cbandits = pd.DataFrame()
dropout_levels = [0, 0.01, 0.05, 0.1, 0.2, 0.4]
for d in dropout_levels:
    print("Trying with dropout:", d)
    np.random.seed(0)
    context_n = df_data.shape[1] - 1
    ad_input_n = df_data.education.nunique()
    model = get_model(context_n + ad_input_n, 0.01)
    X = []
    y = []
    regret_vec = []
    total_regret = 0
    for i in range(5000):
        if i % 20 == 0:
            print("# of impressions:", i)
        user, context = generate_user(df_data)
        ad_inventory = get_ad_inventory()
        ad, x = select_ad(model, context, ad_inventory)
        click = display_ad(ad_click_probs, user, ad)
        regret = calc_regret(user, ad_inventory,    ad_click_probs, ad)
        total_regret += regret
        regret_vec.append(total_regret)
        X.append(x)
        y.append(click)
        if (i + 1) % 500 == 0:
            print('Updating the model at', i+1)
            model = update_model(model, X, y)
            X = []
            y = []

    df_cbandits['dropout: '+str(d)] = regret_vec

累积后悔值随时间的变化存储在df_cbandits pandas DataFrame 中。让我们来可视化它们的比较:

df_cbandits.iplot(dash = ['dash', 'solid', 'dashdot', 
                          'dot', 'longdash', 'longdashdot'],
                  xTitle='Impressions', 
                  yTitle='Cumulative Regret')

这将产生以下输出:

图 3.6 – 不同丢弃率下累积后悔的比较

图 3.6 – 不同丢弃率下累积后悔的比较

图 3.6中的结果显示,我们的赌徒模型在经过一些观察后,学会了如何根据用户特征选择广告。由于不同的丢弃率导致了不同的算法表现,一个重要的问题再次出现,那就是如何选择丢弃率。一个明显的答案是,随着时间的推移,尝试不同的丢弃率,以确定在类似的在线广告问题中最有效的丢弃率。这个方法通常适用于业务需要长期解决类似问题的情况。然而,更好的方法是学习最优的丢弃率。

提示

Concrete dropout是一种变体,能够自动调节丢弃概率。(Collier & Llorens, 2018)成功地在 CB 问题中使用了这种方法,并报告了比固定丢弃选择更优的性能。关于 Concrete dropout 的 TensorFlow 实现,参见github.com/Skydes/Concrete-Dropout。

至此,我们的 CB 讨论已结束。请注意,在制定 CB 问题时,我们主要关注了两个组件:

  • 函数逼近

  • 探索策略

你可以经常将不同的函数逼近与各种探索技术混合使用。虽然汤普森采样与 DNN 的组合可能是最常见的选择,但我们鼓励你参考文献,了解其他方法。

多臂老丨虎丨机和 CB 的其他应用

到目前为止,我们一直将在线广告作为我们的示例。如果你在想这种带有赌博算法的应用在这个领域的普及程度如何,其实它们相当常见。例如,微软有一个基于赌博算法的服务,叫做 Personalizer(免责声明:作者在写这本书时是微软的员工)。这里的示例本身受到 HubSpot(一个营销解决方案公司)的工作的启发(Collier & Llorens, 2018)。此外,赌博问题在广告以外有许多实际应用。在本节中,我们将简要介绍其中一些应用。

推荐系统

本章中我们所定义并解决的赌博问题是一种推荐系统:它们推荐应该显示哪个广告,可能会利用关于用户的信息。还有许多其他推荐系统也以类似的方式使用赌博算法,诸如:

  • 电影标题的艺术作品选择,正如 Netflix 著名的实现方式(Chandrashekar, Amat, Basilico, & Jebara, 2017)

  • 新闻门户的文章推荐

  • 社交媒体平台上的帖子推荐

  • 在线零售平台上的产品/服务推荐

  • 根据用户在搜索引擎上的行为定制搜索结果

网页/应用功能设计

我们每天访问的大多数著名网站和应用,都会在经过大量测试后决定使用哪种设计。例如,他们会为购物车中的“购买”按钮设计不同的版本,并观察哪种设计带来最多的销售量。这些实验会不间断地进行,涵盖数百个功能。进行这些实验的一种高效方式是使用多臂赌博算法。通过这种方式,可以尽早识别出差的功能设计,并将其淘汰,从而最小化这些实验对用户的干扰(Lomas et al., 2016)。

医疗保健

赌博问题在医疗保健中有重要应用。尤其是随着患者数据的增加,通过维护良好的患者数据库和通过移动设备收集数据,现在许多治疗方法都可以个性化地应用于个人。因此,在随机对照试验中决定应用哪种治疗方法时,CB 是一个重要工具。CB 成功应用的另一个例子是决定药物的治疗剂量,比如调节血液凝固的华法林(Bastani and Bayati, 2015)。另一个应用是与最优分配数据采样到不同动物模型以评估治疗效果相关。CB 通过比传统方法更好地识别有前景的治疗方式,证明了能提高这一过程的效率(Durand et al., 2018)。

动态定价

对在线零售商来说,一个重要的挑战是如何在数百万种商品上动态调整价格。这可以建模为一个上下文带宽(CB)问题,其中上下文可能包括产品需求预测、库存水平、产品成本和位置。

财务

CBs 在文献中用于通过混合被动和主动投资,构建最佳投资组合,以实现风险和预期回报之间的平衡。

控制系统调节

许多机械系统使用 比例-积分-微分 (PID) 控制器的变体来控制系统。PID 控制器需要调节,通常由相关领域的专家分别为每个系统进行调节。这是因为控制器的最佳增益取决于设备的具体情况,如材料、温度和磨损。这一手动过程可以通过使用 CB 模型来自动化,CB 模型评估系统特性并相应地调节控制器。

总结

在这一章中,我们结束了关于带有 CBs 的强盗问题的讨论。正如我们所提到的,强盗问题有许多实际应用。因此,如果你已经在自己的业务或研究中遇到一个可以建模为强盗问题的情况,也不会感到惊讶。现在你知道如何构造和解决一个强盗问题,去应用你所学的知识吧!强盗问题对于发展解决探索与利用困境的直觉非常重要,这一困境几乎会出现在每个强化学习场景中。

现在你已经深入理解了如何解决单步强化学习,接下来是时候进入完整的多步强化学习了。在下一章中,我们将深入探讨与多步强化学习相关的马尔科夫决策过程理论,并为现代深度强化学习方法打下基础,这些方法将在随后的章节中讨论。

参考文献

  • Bouneffouf, D., & Rish, I. (2019). 多臂和情境强盗的实际应用调查. 取自 arXiv: arxiv.org/abs/1904.10040

  • Chandrashekar, A., Amat, F., Basilico, J., & Jebara, T. (2017 年 12 月 7 日). Netflix 技术博客. 取自 Netflix 的艺术作品个性化: netflixtechblog.com/artwork-personalization-c589f074ad76

  • Chapelle, O., & Li, L. (2011). 汤普森采样的实证评估. 神经信息处理系统进展, 24, (第 2249-2257 页)

  • Collier, M., & Llorens, H. U. (2018). 深度情境多臂强盗. 取自 arXiv: arxiv.org/abs/1807.09809

  • Gal, Y., Hron, J., & Kendall, A. (2017). 具体丢弃法. 神经信息处理系统进展, 30, (第 3581-3590 页)

  • Marmerola, G. D. (2017 年 11 月 28 日). 情境强盗的汤普森采样. 取自 Guilherme 的博客: gdmarmerola.github.io/ts-for-contextual-bandits

  • Riquelme, C., Tucker, G., & Snoek, J. (2018). 深度贝叶斯强盗对决:汤普森采样的贝叶斯深度网络实证比较. 国际学习表征会议(ICLR)

  • Russo, D., Van Roy, B., Kazerouni, A., Osband, I., & Wen, Z. (2018). Thompson Sampling 教程。机器学习基础与趋势,(第 1-96 页)

  • Lomas, D., Forlizzi, J., Poonawala, N., Patel, N., Shodhan, S., Patel, K., Koedinger, K., & Brunskill, E. (2016). 作为多臂 bandit 问题的界面设计优化。(第 4142-4153 页)。10.1145/2858036.2858425

  • Durand, A., Achilleos, C., Iacovides, D., Strati, K., Mitsis, G.D., & Pineau, J. (2018). 用于在小鼠新生致癌模型中适应性治疗的上下文 bandit 方法。第三届医疗健康机器学习会议论文集,PMLR 85:67-82

  • Bastani, H. & Bayati, M. (2015). 具有高维协变量的在线决策制定。可在 SSRN 2661896 上获得

第四章:第四章:马尔可夫决策过程的构成

在第一章中,我们讨论了强化学习(RL)的许多应用,从机器人学到金融。在为这些应用实现任何 RL 算法之前,我们需要先对其进行数学建模。马尔可夫决策过程(MDP)是我们用来建模这些序贯决策问题的框架。MDP 具有一些特殊的特性,使得我们可以更容易地从理论上分析这些问题。在此基础上,动态规划(DP)是提出 MDP 解决方法的领域。从某种意义上讲,RL 是一组近似 DP 方法,能够让我们为非常复杂、无法通过精确的 DP 方法解决的问题找到较好的(但不一定是最优的)解决方案。

在本章中,我们将一步一步地构建一个 MDP,解释其特性,并为后续章节中将介绍的 RL 算法奠定数学基础。在 MDP 中,智能体采取的行动具有长期后果,这也是它与我们之前讨论的多臂老丨虎丨机(MAB)问题的区别。本章重点讨论一些量化这一长期影响的关键概念。虽然涉及的理论比其他章节稍多,但别担心,我们将很快进入 Python 实践环节,帮助更好地掌握这些概念。本章具体包括以下内容:

  • 马尔可夫链

  • 马尔可夫奖励过程

  • 马尔可夫决策过程

  • 部分可观测的 MDP

从马尔可夫链开始

我们从马尔可夫链开始,这不涉及任何决策过程。它们仅仅是建模某些内部转移动态驱动的特定类型的随机过程。因此,我们暂时不谈及智能体。理解马尔可夫链如何工作,将帮助我们为后续涉及的马尔可夫决策过程(MDP)奠定基础。

具有马尔可夫性质的随机过程

我们已经定义了状态,即完全描述环境所处情境的集合。如果环境将要转移到的下一个状态仅仅依赖于当前状态,而不依赖于过去的状态,我们称该过程具有马尔可夫性质。这一性质得名于俄国数学家安德烈·马尔可夫。

想象一个在网格世界中随机移动的坏掉的机器人。在任何给定的步骤中,机器人以 0.2、0.3、0.25 和 0.25 的概率分别向上、向下、向左和向右移动。如下图 图 4.1 所示:

图 4.1 – 一个坏掉的机器人,当前位于 (1,2) 的网格世界中

图 4.1 – 一个坏掉的机器人,当前位于 (1,2) 的网格世界中

机器人当前处于状态中。它从哪里来并不重要;它将以的概率进入状态,以的概率进入状态,依此类推。由于它下一步的转移概率仅取决于当前所处的状态,而与之前所处的状态无关,因此该过程具有马尔可夫性质。

让我们更正式地定义这一点。我们用表示时间时的状态。如果对于所有状态和时间,以下条件成立,则该过程具有马尔可夫性质:

这样的随机过程称为马尔可夫链。请注意,如果机器人撞到墙壁,我们假设它会反弹回去并保持在原状态。因此,例如,在状态时,机器人将在下一步仍停留在那里,概率为。

马尔可夫链通常使用有向图来表示。在网格世界中,破损机器人示例的有向图如下所示,如图 4.2所示:

图 4.2 – 2x2 网格世界中机器人示例的马尔可夫链图

图 4.2 – 2x2 网格世界中机器人示例的马尔可夫链图

提示

许多系统可以通过将历史信息包含在状态中来变成马尔可夫过程。考虑一个修改后的机器人示例,其中机器人更可能继续沿着上一步的方向移动。虽然这样的系统表面上似乎不满足马尔可夫性质,但我们可以简单地重新定义状态,将过去两步中访问过的单元格包括在内,例如。在这个新状态定义下,转移概率将与过去的状态无关,马尔可夫性质将得到满足。

现在我们已经定义了马尔可夫链,接下来让我们深入探讨。接下来,我们将研究如何根据转移行为的不同来分类马尔可夫链中的状态。

马尔可夫链中状态的分类

一种环境可以在若干次过渡后从任何状态转移到任何其他状态,正如我们在机器人示例中所看到的那样,这是一种特殊类型的马尔可夫链。正如你所想,现实中更复杂的系统会涉及到具有更丰富特征集的状态,接下来我们将介绍这些特征。

可达状态与可通信状态

如果环境能够在一定步数后以正概率从状态 过渡到状态 ,我们称 是从 可达 的。如果 也是从 可达的,那么这些状态被称为通讯。如果马尔可夫链中的所有状态都彼此通讯,我们称该马尔可夫链是不可约的,这正是我们机器人示例中的情况。

吸收态

如果一个状态 只能过渡到自身,即 ,那么这个状态被称为吸收态。想象一下,如果在前述例子中机器人撞到墙壁后无法再移动,那么这就是吸收态的一个例子,因为机器人永远无法离开这个状态。带有吸收态的网格世界版本可以用马尔可夫链图表示,如图 4.3所示:

图 4.3 – 具有吸收态的马尔可夫链图

图 4.3 – 具有吸收态的马尔可夫链图

在 RL 上下文中,吸收态等同于标记了一集结束的终端状态,我们在第一章** 强化学习导论中定义过。除了终端状态外,一集还可以在达到时间限制 T 后结束。

瞬态状态和经常态状态

如果存在另一个状态 ,可以从状态 到达它,但反之不成立,则状态 被称为瞬态状态。在足够长的时间内,环境最终会远离瞬态状态并且不再返回。

考虑一个修改后的网格世界,分为两个部分;我们可以玩味地称它们为光面和暗面。这个世界中的可能过渡如 图 4.4 所示。你能识别出瞬态状态吗?

图 4.4 – 具有明暗面的网格世界

图 4.4 – 具有明暗面的网格世界

如果你认为光面上的答案是 ,请再想一想。光面上的每个状态都有一条通向暗面的出路,但没有返回的可能性。因此,无论机器人位于光面的哪个位置,它最终都会过渡到暗面并且无法返回。因此,所有光面上的状态都是瞬态的。这是一个类似于反乌托邦的世界!同样,在带有崩溃状态的修改后网格世界中,所有状态都是瞬态的,除了崩溃状态。

最后,不是瞬态的状态被称为经常态状态。在这个例子中,暗面上的状态是经常态的。

周期性和非周期性状态

我们称一个状态 为周期性,如果所有从 离开的路径在经过某个多倍数的 步后会返回。考虑 图 4.5 中的例子,其中所有状态的周期为 :

图 4.5 – 具有周期状态的马尔可夫链,k=4

图 4.5 – 具有周期状态的马尔可夫链,k=4

如果 ,则称重现状态为非周期性。

遍历性

我们最终可以定义一个重要类别的马尔可夫链。如果所有状态都表现出以下特性,则称马尔可夫链为遍历:

  • 彼此通信(不可约)

  • 是重现的

  • 是非周期性的

对于遍历马尔可夫链,我们可以计算出一个单一的概率分布,告诉我们系统在经过很长时间从初始化开始后,会以什么概率处于某个状态。这被称为稳态概率分布。

到目前为止,一切顺利,但我们所涉及的内容也稍微有些密集,充满了定义的集合。在我们进入实际例子之前,让我们先定义一下马尔可夫链在状态之间转换的数学原理。

转移和稳态行为

我们可以通过数学方法计算马尔可夫链随时间的行为。为此,我们首先需要知道系统的初始概率分布。例如,当我们初始化一个网格世界时,机器人一开始会处于哪个状态?这就是由初始概率分布给出的。然后,我们定义转移概率矩阵,它的条目给出了从一个时间步到下一个时间步之间所有状态对的转移概率。更正式地,矩阵中位于 行和 列的条目给出 ,其中 和 是状态索引(按照我们的惯例,从 1 开始)。

现在,为了计算系统在经过 步后处于状态 的概率,我们使用以下公式:

这里, 是初始概率分布, 是转移概率矩阵的幂 。请注意, 给出了在从状态 开始后,经过 步后处于状态 的概率。

信息

一个马尔可夫链完全由 元组来表征,其中 是所有状态的集合, 是转移概率矩阵。

是的,到目前为止我们已经介绍了很多定义和理论。现在,正是时候看一下实际的例子了。

示例 – 网格世界中的 n 步行为

在许多强化学习算法中,核心思想是使我们对环境当前状态的理解与经过步转移后的理解保持一致,并不断迭代,直到这种一致性得到确保。因此,了解作为马尔可夫链建模的环境随时间演化的直觉非常重要。为此,我们将研究步的网格世界行为。跟着一起走吧!

  1. 我们从创建一个网格世界开始,机器人在其中,类似于图 4.1中的情况。现在,我们总是将机器人初始化在中心。此外,我们将状态/单元格索引为所以,初始概率分布由以下代码给出:

    import numpy as np
    m = 3
    m2 = m ** 2
    q = np.zeros(m2)
    q[m2 // 2] = 1
    

    这里,q 是初始概率分布。

  2. 我们定义一个函数,给出的转移概率矩阵:

    def get_P(m, p_up, p_down, p_left, p_right):
        m2 = m ** 2
        P = np.zeros((m2, m2))
        ix_map = {i + 1: (i // m, i % m) for i in range(m2)}
        for i in range(m2):
            for j in range(m2):
                r1, c1 = ix_map[i + 1]
                r2, c2 = ix_map[j + 1]
                rdiff = r1 - r2
                cdiff = c1 - c2
                if rdiff == 0:
                    if cdiff == 1:
                        P[i, j] = p_left
                    elif cdiff == -1:
                        P[i, j] = p_right
                    elif cdiff == 0:
                        if r1 == 0:
                            P[i, j] += p_down
                        elif r1 == m - 1:
                            P[i, j] += p_up
                        if c1 == 0:
                            P[i, j] += p_left
                        elif c1 == m - 1:
                            P[i, j] += p_right
                elif rdiff == 1:
                    if cdiff == 0:
                        P[i, j] = p_down
                elif rdiff == -1:
                    if cdiff == 0:
                        P[i, j] = p_up
        return P
    

    这段代码看起来有点长,但它的功能非常简单:它根据指定的上、下、左、右移动概率,填充一个 转移概率矩阵。

  3. 获取我们这个网格世界的转移概率矩阵:

    P = get_P(3, 0.2, 0.3, 0.25, 0.25)
    
  4. 计算步的转移概率。例如,对于,我们有以下内容:

    n = 1
    Pn = np.linalg.matrix_power(P, n)
    np.matmul(q, Pn)
    
  5. 结果将如下所示:

    array([0., 0.3, 0., 0.25, 0., 0.25, 0., 0.2, 0.])
    

    没什么令人惊讶的,对吧?输出只是告诉我们,从中心出发的机器人,以的概率在上面一个单元,以的概率在下面一个单元,依此类推。让我们对 3 步、10 步和 100 步进行测试。结果如图 4.6所示:

图 4.6 – n 步转移概率

图 4.6 – n 步转移概率

你可能会注意到,10 步和 100 步后的概率分布非常相似。这是因为系统在几步之后几乎达到了稳态。因此,我们在特定状态下找到机器人的机会在 10 步、100 步或 1000 步后几乎是相同的。此外,你应该注意到,我们更有可能在底部单元格找到机器人,这仅仅是因为我们有。

在我们结束关于转移状态和稳态行为的讨论之前,让我们回到遍历性,探讨遍历马尔可夫链的一个特殊性质。

示例 – 在遍历马尔可夫链中的一个样本路径

如果马尔可夫链是遍历的,我们可以通过长时间模拟它,并通过访问频率估计状态的稳态分布。这对于我们无法访问系统转移概率的情况特别有用,但我们可以进行模拟。

让我们通过一个例子来看看:

  1. 首先,让我们导入 SciPy 库来统计访问次数。将样本路径的步数设为一百万,初始化一个向量来跟踪访问次数,并将第一个状态初始化为4,即 :

    from scipy.stats import itemfreq
    s = 4
    n = 10 ** 6
    visited = [s]
    
  2. 模拟环境一百万步:

    for t in range(n):
    s = np.random.choice(m2, p=P[s, :])
    visited.append(s)
    
  3. 统计每个状态的访问次数:

    itemfreq(visited)
    
  4. 你将看到类似于以下的数字:

    array([[0, 158613],       [1, 157628],       [2, 158070],       [3, 105264],       [4, 104853],       [5, 104764],       [6,  70585],       [7,  70255],       [8,  69969]], dtype=int64)
    

这些结果确实与我们计算的稳态概率分布非常一致。

到目前为止,你做得很棒,我们已经详细讨论了马尔可夫链,做了一些例子,并且获得了扎实的直觉!在我们结束这一部分之前,让我们简要地了解一种更现实的马尔可夫过程类型。

半马尔可夫过程和连续时间马尔可夫链

到目前为止,我们提供的所有示例和公式都与离散时间马尔可夫链相关,这是过渡在离散时间步长(如每分钟或每 10 秒)发生的环境。但在许多现实世界的场景中,下一个过渡发生的时间也是随机的,这使得它们成为半马尔可夫过程。在这种情况下,我们通常感兴趣的是预测经过 时间后系统的状态(而不是经过 步骤后的状态)。

时间成分重要的一个示例场景是排队系统——例如,顾客在客服队列中的等待人数。顾客可以随时加入队列,而客服人员也可以在任何时间为顾客提供服务——而不仅仅是在离散时间步长处。另一个例子是工厂中等待在组装站前处理的在制品库存。在这些情况下,随着时间推移分析系统的行为是非常重要的,这样才能改善系统并采取相应的行动。

在半马尔可夫过程中,我们需要知道系统的当前状态,以及系统在该状态下的持续时间。这意味着从时间的角度来看,系统依赖于过去,但从过渡类型的角度来看并不依赖过去——因此称为半马尔可夫过程。

让我们来看一下几个可能的版本,看看这对我们来说有何意义:

  • 如果我们只对过渡本身感兴趣,而不关心它们发生的时刻,我们可以简单地忽略所有与时间相关的内容,并使用半马尔可夫过程的嵌入式马尔可夫链,这实际上与使用离散时间马尔可夫链是一样的。

  • 在某些过程中,虽然过渡之间的时间是随机的,但它是无记忆的,这意味着是指数分布的。那么,我们就完全满足马尔可夫性质,系统就是连续时间马尔可夫链。排队系统就是常常属于这一类的模型。

  • 如果我们既对时间成分感兴趣,又过渡时间不是无记忆的,那么我们就有一个一般的半马尔可夫过程。

在使用强化学习(RL)解决这些类型的环境时,尽管不是最理想的,通常将所有内容视为离散的,并使用为离散时间系统开发的相同 RL 算法,并通过一些变通方法解决。现在,你需要了解并承认这些差异,但我们不会深入讨论半马尔可夫过程。相反,当我们在后面的章节中解决连续时间示例时,你会看到这些变通方法是如何工作的。

我们在通过马尔可夫链构建 MDP(马尔可夫决策过程)理解方面取得了很大进展。接下来,我们的目标是为环境引入“奖励”。

引入奖励 – 马尔可夫奖励过程

在我们目前的机器人示例中,我们并未真正识别出任何“好”或“坏”的情境/状态。然而,在任何系统中,都会有理想的状态和其他不太理想的状态。在本节中,我们将奖励附加到状态/转移上,这给我们带来了马尔可夫奖励过程(MRP)。然后我们评估每个状态的“价值”。

将奖励附加到网格世界示例

记得机器人示例的版本吗?当它撞到墙壁时,它无法反弹回原来的格子,而是以无法恢复的方式碰撞并崩溃?从现在开始,我们将使用这个版本,并为过程附加奖励。现在,让我们构建这个示例:

  1. 修改转移概率矩阵,将自转移概率分配给我们添加到矩阵中的“碰撞”状态:

    P = np.zeros((m2 + 1, m2 + 1))
    P[:m2, :m2] = get_P(3, 0.2, 0.3, 0.25, 0.25)
    for i in range(m2):
        P[i, m2] = P[i, i]
        P[i, i] = 0
    P[m2, m2] = 1
    
  2. 为转移分配奖励:

    n = 10 ** 5
    avg_rewards = np.zeros(m2)
    for s in range(9):
        for i in range(n):
            crashed = False
            s_next = s
            episode_reward = 0
            while not crashed:
                s_next = np.random.choice(m2 + 1, \
                                          p=P[s_next, :])
                if s_next < m2:
                    episode_reward += 1
                else:
                    crashed = True
            avg_rewards[s] += episode_reward
    avg_rewards /= n
    

    每当机器人保持活着时,它就会获得+1 的奖励。当它碰撞时,获得 0 奖励。由于“碰撞”是一个终止/吸收状态,我们将在此处终止该回合。模拟该模型,使用不同的初始化,每个初始化进行 100K 次模拟,并观察每种情况下的平均奖励。

    结果将如图 4.7所示(由于随机性,你的结果会稍有不同):

图 4.7 – 相对于初始状态的平均回报

图 4.7 – 相对于初始状态的平均回报

在这个例子中,如果初始状态是,那么平均回报是最高的。这使得它成为一个“有价值”的状态。与此对比的是状态,其平均回报为。不出所料,这不是一个理想的状态。这是因为当机器人从角落开始时,它更可能早早撞到墙壁。另一个不奇怪的现象是,回报在垂直方向上几乎对称,因为。

现在我们已经计算了每种初始化情况下的平均奖励,让我们深入探讨一下它们之间的关系。

不同初始化情况下平均奖励的关系

我们观察到的平均回报之间有着相当的结构性关系。想一想:假设机器人从开始,并过渡到。由于它仍然存活,我们获得了+1 的奖励。如果我们知道状态的“值”,我们还需要继续模拟以确定预期的回报吗?其实不需要!这个值已经告诉我们从那时起的预期回报。记住,这是一个马尔科夫过程,接下来的事情不依赖于过去的状态!

我们可以扩展这一关系,从其他状态值推导出某一状态的值。但请记住,机器人可能已经过渡到其他状态。考虑到其他可能性,并将状态的值表示为,我们得到以下关系:

如你所见,除了状态值估计中的一些小误差外,状态值彼此之间是一致的。

提示

状态值之间的这种递归关系是许多强化学习算法的核心,它将一再出现。我们将在下一节使用贝尔曼方程来正式化这个想法。

让我们在下一节正式化所有这些概念。

返回、折扣和状态值

我们将马尔科夫过程在时间步骤之后的回报定义如下:

这里,是时间的奖励,是终止时间步骤。然而,这个定义可能会有潜在的问题。在一个没有终止状态的马尔科夫过程(MRP)中,回报可能会无限大。为了解决这个问题,我们在此计算中引入了折扣率,,并定义了折扣回报,如下所示:

对于,只要奖励序列是有限的,这个和就一定是有限的。下面是折扣率变化时如何影响和的示意:

  • 对于接近 1 的值,远期奖励和即时奖励几乎被赋予相等的重要性。

  • 当时,所有的奖励,无论是远期的还是即时的,都会被赋予相同的权重。

  • 对于接近 0 的值,和的结果更为短视。

  • 当时,回报等于即时奖励。

在接下来的章节中,我们的目标是最大化预期的折扣回报。因此,理解使用折扣计算回报的其他好处是很重要的:

  • 折扣减少了我们对遥远未来所获得奖励的重视。这是合理的,因为我们对于遥远未来的估计可能并不准确,尤其是当我们利用其他估计来推算价值时(稍后会详细讨论)。

  • 人类(以及动物)行为倾向于选择立即奖励而非未来奖励。

  • 对于金融奖励,立即奖励更具价值,因为金钱的时间价值。

现在我们已经定义了折扣回报,状态的价值,,被定义为在状态下开始时的期望折扣回报:

请注意,这一定义允许我们使用我们在上一节中推导出的递归关系:

这个方程被称为Bellman 方程(MRP)。它就是我们在前面的网格世界示例中用来根据其他状态值计算一个状态的价值时所使用的方程。Bellman 方程是许多强化学习算法的核心,具有至关重要的意义。我们将在介绍 MDP 之后给出它的完整版本。

让我们以 MRP 的一个更正式的定义结束这一部分,详细内容请见下方的信息框。

信息

MRP 完全由一个元组来表征,其中是一个状态集合,是一个转移概率矩阵,是奖励函数,而是折扣因子。

接下来,我们将看看如何分析地计算状态值。

分析地计算状态值

Bellman 方程为我们提供了状态值、奖励和转移概率之间的关系。当转移概率和奖励动态已知时,我们可以使用 Bellman 方程精确计算状态值。当然,只有在状态的总数足够小以便进行计算时,这才是可行的。现在让我们看看如何做到这一点。

当我们将 Bellman 方程写成矩阵形式时,它如下所示:

这里,是一个列向量,每个条目是相应状态的值,而是另一个列向量,每个条目对应转移到该状态时获得的奖励。因此,我们得到了前面公式的扩展表示:

我们可以通过如下方式求解这个线性方程组:

现在是时候在我们的网格世界示例中实现这一点了。请注意,在示例中,我们有 10 个状态,其中第 10 个状态表示机器人的撞车。转移到任何状态都会得到+1 奖励,除了“撞车”状态。让我们开始吧:

  1. 构建向量:

    R = np.ones(m2 + 1)
    R[-1] = 0
    
  2. 设定 (实际例子中接近 1 的值),并计算状态值:

    inv = np.linalg.inv(np.eye(m2 + 1) - 0.9999 * P)
    v = np.matmul(inv, np.matmul(P, R))
    print(np.round(v, 2)) 
    

    输出将类似于这样:

    [1.47 2.12 1.47 2.44 3.42 2.44 1.99 2.82 1.99 0.]
    

请记住,这些是真正的(理论上的,而非估算的)状态值(对于给定的折扣率),它们与我们之前通过模拟在图 4.7中估算的结果是一致的!

如果你在想,为什么我们不直接将 设为 1,请记住,我们现在引入了折扣因子,这对于数学收敛是必要的。如果你仔细想想,机器人有可能随机移动,但能无限长时间存活,获得无限奖励。是的,这种情况极其不可能,而且你在实际中也永远不会遇到。所以,你可能会认为我们可以在这里设定 。然而,这会导致一个奇异矩阵,我们无法求其逆。所以,我们选择 。在实际应用中,这个折扣因子几乎等权重地考虑了即时奖励和未来奖励。

我们可以通过其他方法来估算状态值,而不仅仅是通过模拟或矩阵求逆。接下来我们来看一种迭代方法。

迭代估算状态值

强化学习中的一个核心思想是使用价值函数定义来迭代估算价值函数。为了实现这一点,我们随便初始化状态值,并使用其定义作为更新规则。由于我们是基于其他估算值来估算状态,这是一种自举方法。我们会在所有状态的最大更新低于设定阈值时停止。

这是估算我们机器人例子中的状态值的代码:

def estimate_state_values(P, m2, threshold):
    v = np.zeros(m2 + 1)
    max_change = threshold
    terminal_state = m2 
    while max_change >= threshold:
        max_change = 0
        for s in range(m2 + 1):
            v_new = 0
            for s_next in range(m2 + 1):
                r = 1 * (s_next != terminal_state)
                v_new += P[s, s_next] * (r + v[s_next])
            max_change = max(max_change, np.abs(v[s] - v_new))
            v[s] = v_new
    return np.round(v, 2)

结果将与图 4.7中的估计值非常相似。只需运行以下代码:

estimate_state_values(P, m2, 0.01)

你应该得到类似以下的结果:

array([1.46, 2.11, 1.47, 2.44, 3.41, 2.44, 1.98, 2.82, 1.99, 0.])

看起来不错!再次提醒,记住以下几点:

  • 我们必须遍历所有可能的状态。当状态空间很大时,这是不可行的。

  • 我们显式地使用了转移概率。在一个现实的系统中,我们并不知道这些概率。

现代强化学习算法通过使用函数逼近来表示状态,并从(环境的模拟)中采样转移,从而解决了这些缺点。我们将在后续章节中探讨这些方法。

到目前为止,一切顺利!现在,我们将把最后一个主要部分加入到这个图景中:动作。

引入动作 – MDP

MRP 使我们能够建模和研究带有奖励的马尔可夫链。当然,我们的最终目标是控制这样的系统,以实现最大奖励。现在,我们将把决策融入到 MRP 中。

定义

MDP 只是一个 MRP,其中决策影响转移概率,并可能影响奖励。

信息

MDP 的特征是一个 元组,其中我们在 MRP 的基础上添加了一个有限的动作集合,。

MDP 是强化学习背后的数学框架。所以,现在是时候回顾我们在第一章中介绍的强化学习图示 Introduction to Reinforcement Learning:

图 4.8 – MDP 图示

图 4.8 – MDP 图示

我们在 MDP 中的目标是找到一个策略,使得期望的累积奖励最大化。策略简单地告诉给定状态下应该采取哪些行动。换句话说,它是一个从状态到行动的映射。更正式地说,策略是给定状态下的行动分布,用 表示:

代理的策略可能会影响转移概率和奖励,它完全定义了代理的行为。策略也是静态的,不会随时间改变。因此,MDP 的动态由以下转移概率定义:

这些适用于所有状态和行动。

接下来,让我们看看 MDP 在网格世界示例中的表现。

网格世界作为 MDP

假设我们可以控制网格世界中的机器人,但仅限于某种程度。在每一步中,我们可以采取以下行动之一:上、下、左和右。然后,机器人以 70%的概率朝着所选方向移动,并以 10%的概率朝着其他方向移动。基于这些动态,可能的一个策略如下:

  • 对了,当处于状态 时

  • 上,当处于状态 ­ 时

策略还决定了转移概率矩阵和奖励分布。例如,我们可以将状态 下给定策略的转移概率写成如下:

  • 提示

    一旦在 MDP 中定义了策略,状态和奖励序列就变成了 MRP。

到目前为止,一切顺利。现在,让我们更加严谨地表达策略。记住,策略实际上是给定状态下的一种行动的概率分布。因此,说“策略是在状态 下采取‘右’行动”,实际上意味着“在状态 下,我们以概率 1 采取‘右’行动。”这可以更正式地表示如下:

一个完全合法的策略是一个概率策略。例如,我们可以选择在状态 下以相等的概率采取左或上行动,表达式如下:

再次强调,我们在强化学习中的目标是找出一个最优策略,使得环境和当前问题最大化期望的折扣回报。从下一章开始,我们将详细介绍如何做到这一点,并解决详细的示例。现在,这个例子足以说明在玩具示例中 MDP 是什么样子的。

接下来,我们将像定义 MRP 时那样,定义 MDP 的值函数和相关方程。

状态值函数

我们已经在 MRP 的背景下讨论过状态的值。现在我们正式称之为状态值函数,它被定义为在状态 开始时的期望折扣回报。然而,这里有一个关键点:在 MDP 中,状态值函数是针对策略定义的。毕竟,转移概率矩阵是由策略决定的。因此,改变策略很可能会导致不同的状态值函数。这个定义如下:

请注意状态值函数中的 下标,以及期望操作符。除此之外,思路与我们在 MRP 中定义的相同。

现在,我们终于可以为 定义贝尔曼方程:

你已经知道,状态的值彼此是相关的,这一点我们在讨论 MRP 时已经提到过。这里唯一不同的是,转移概率现在依赖于动作以及根据策略在给定状态下采取这些动作的相应概率。假设“无动作”是我们网格世界中的一种可能动作。图 4.7中的状态值对应于在任何状态下都不采取动作的策略。

动作值函数

在强化学习中,我们常用的一个有趣量是动作值函数。假设你有一个策略,(不一定是最优的)。该策略已经告诉你每个状态应该采取哪些动作以及相应的概率,并且你会遵循该策略。然而,对于当前时间步,你问:“如果我在当前状态下最初采取动作 ,然后在所有状态下都遵循 ,那么预期的累计回报会是多少?”这个问题的答案就是动作值函数。正式地,我们可以以各种但等效的方式定义它:

现在,你可能会问,如果我们之后反正会遵循策略 ,那么定义这个量有什么意义呢?好吧,事实证明,我们可以通过选择在状态 下给出最高动作值的动作来改进我们的策略,这个值由 表示。

我们将在下一章讨论如何改进并找到最优策略。

最优状态值和动作值函数

最优策略是给出最优状态值函数的策略:

最优策略用表示。请注意,可能有多个策略是最优的。然而,只有一个最优状态值函数。我们也可以定义最优的行动值函数,如下所示:

最优状态值函数和行动值函数之间的关系如下所示:

贝尔曼最优性

当我们之前定义贝尔曼方程时,针对,我们需要在方程中使用。这是因为状态值函数是针对策略定义的,我们需要计算期望的奖励以及根据策略在状态下建议的行动所得到的后续状态的值(如果多个行动被建议且有正概率被采取,还需要考虑相应的概率)。这个方程式如下所示:

然而,在处理最优状态值函数时,我们实际上不需要从某个地方检索并将其代入方程式。为什么?因为最优策略应该建议一个能够最大化后续表达式的行动。毕竟,状态值函数代表的是累积的期望奖励。如果最优策略没有建议一个能够最大化期望项的行动,那它就不是最优策略。因此,对于最优策略和状态值函数,我们可以写出贝尔曼方程的特殊形式。这就是贝尔曼最优性方程,定义如下:

我们可以类似地为行动值函数写出贝尔曼最优性方程:

贝尔曼最优性方程是强化学习中的核心思想之一,它将构成我们在下一章中介绍的许多算法的基础。

这样,我们已经覆盖了强化学习算法背后的大部分理论。在我们实际使用这些算法来解决一些强化学习问题之前,我们将讨论 MDP 的一个扩展,叫做部分可观测的 MDP,这种情况在许多现实世界问题中经常出现。

部分可观测的 MDP

在本章中,我们所使用的策略定义是,它是从环境状态到行动的映射。现在,我们应该问一个问题:在所有类型的环境中,代理是否真的能完全知道状态? 记住状态的定义:它描述了与代理决策相关的环境中的一切(例如,在网格世界的例子中,墙壁的颜色并不重要,因此它不会成为状态的一部分)。

如果你仔细想想,这是一个非常强的定义。考虑一下有人开车的情形。司机在做出驾驶决策时,是否了解周围世界的所有信息?当然不是!首先,汽车经常会挡住司机的视线。尽管不知道世界的精确状态,并不会阻止任何人驾驶。在这种情况下,我们会基于观察来做决策,例如在驾驶过程中我们看到和听到的内容,而不是基于状态。然后,我们就说环境是部分可观察的。如果这是一个 MDP,我们就称之为部分可观察的 MDP,或POMDP。

在 POMDP 中,智能体看到特定观察的概率取决于最新的动作和当前状态。描述这一概率分布的函数称为观察函数。

信息

一个部分可观察的马尔可夫决策过程(POMDP)由一个 元组来描述,其中 是可能观察的集合, 是观察函数,具体如下:

在实际操作中,部分可观察环境通常需要保留观察记忆,以便基于这些观察采取行动。换句话说,策略不仅是基于最新的观察,还基于过去几步的观察。为了更好地理解为什么这样有效,可以想象一辆自动驾驶汽车从它的摄像头获取到的一幅静态场景能获得多少信息。仅凭这一张图片无法揭示有关环境的一些重要信息,比如其他汽车的速度和确切行驶方向。为了推断这些信息,我们需要一系列场景,并观察汽车在这些场景之间是如何移动的。

提示

在部分可观察环境中,保留观察记忆使得能够揭示更多关于环境状态的信息。这就是为什么许多著名的强化学习(RL)设置使用长短期记忆(LSTM)网络来处理观察信息。我们将在后续章节中更详细地讨论这个问题。

至此,我们已结束关于 MDP 的讨论。现在你可以开始深入了解如何解决强化学习问题了!

概述

本章中,我们覆盖了建模现实生活中顺序决策问题的数学框架:马尔可夫决策过程(MDPs)。为此,我们从不涉及奖励或决策概念的马尔可夫链开始。马尔可夫链仅仅描述了基于当前状态转移的随机过程,转移与之前访问过的状态无关。接着,我们加入了奖励的概念,开始讨论哪些状态在预期未来奖励的角度更具优势。这样产生了“状态价值”的概念。最后,我们引入了“决策/行动”的概念,并定义了 MDP。我们最终明确了状态值函数和行动值函数的定义。最后,我们讨论了部分可观察环境以及它们如何影响智能体的决策。

本章介绍的贝尔曼方程变体是今天许多强化学习算法的核心,通常被称为“基于价值的方法”。现在你已经对它们有了扎实的理解,从下一章开始,我们将利用这些思想来得出最优策略。具体来说,我们将首先讨论马尔可夫决策过程(MDPs)的精确解法算法,即动态规划方法。接着,我们将讨论像蒙特卡罗和时序差分学习这样的算法,它们提供近似解法,但不像动态规划方法那样需要知道环境的精确动态。

敬请期待,下章再见!

练习

  1. 使用我们介绍的马尔可夫链模型计算机器人在状态下的-步转移概率。你会注意到,系统到达稳态所需的时间稍长。

  2. 修改马尔可夫链,将机器人撞墙的吸收状态包括在内。对于一个较大的,你的看起来是什么样的?

  3. 使用图 4.7中的状态值,计算一个角落状态的值,利用邻近状态值的估计。

  4. 使用矩阵形式和运算代替for循环,迭代估计网格世界马尔可夫决策过程中的状态值。

  5. 计算的行动值,其中π策略对应于在任何状态下不采取行动,使用图 4.7中的数值。根据与的比较,你会考虑在状态中改变策略,采取向上的行动,而不是不行动吗?

进一步阅读

  • Silver, D. (2015). 第 2 讲:马尔可夫决策过程。来自 UCL 的强化学习课程:www.davidsilver.uk/wp-content/uploads/2020/03/MDP.pdf

  • Sutton, R. S., & Barto, A. G. (2018). 强化学习:导论。Bradford 出版社

  • Ross, S. M. (1996). 随机过程. 第 2 版,Wiley

第五章:第五章:解决强化学习问题

在上一章,我们为强化学习(RL)问题的建模提供了数学基础。在本章中,我们将奠定解决该问题的基础。接下来的许多章节将专注于一些特定的解决方法,这些方法将基于这个基础进行展开。为此,我们将首先讲解动态规划(DP)方法,通过它我们将介绍一些关键的理念和概念。DP 方法为马尔可夫决策过程(MDPs)提供了最优解,但需要对环境的状态转移和奖励动态有完整的知识和紧凑的表示。在现实场景中,这可能会变得极为限制且不切实际,因为在这种情况下,代理要么直接在环境中进行训练,要么在该环境的仿真中进行训练。与 DP 不同,后面我们将讲解的蒙特卡洛(Monte Carlo)和时序差分(TD)方法使用来自环境的采样转移,并放宽了上述限制。最后,我们还将详细讨论什么样的仿真模型适合用于强化学习。

本章特别介绍了以下几个部分:

  • 探索动态规划

  • 使用蒙特卡洛方法训练你的代理

  • 时序差分学习

  • 理解仿真在强化学习中的重要性

探索动态规划

DP 是数学优化的一个分支,提出了针对 MDPs 的最优解法。尽管大多数现实问题过于复杂,无法通过 DP 方法得到最优解,但这些算法背后的思想对许多 RL 方法至关重要。因此,深入理解这些方法是非常重要的。在本章中,我们将从这些最优方法出发,通过系统地介绍近似方法,逐步过渡到更实际的解决方案。

我们将通过描述一个示例来开始本节,该示例将作为我们后续介绍的算法的用例。接下来,我们将讲解如何使用动态规划(DP)进行预测和控制。

让我们开始吧!

示例用例——食品车的库存补充

我们的用例涉及一个食品车业务,需要决定每个工作日购买多少个汉堡肉饼以补充库存。库存规划是零售和制造业中一个重要的问题类别,许多公司都需要不断处理这类问题。当然,为了教学的需要,我们的示例比实际情况要简单得多。然而,它仍然能帮助你理解这个问题类别。

现在,让我们深入到这个例子中:

  • 我们的食品车在工作日运营于市中心。

  • 每个工作日上午,店主需要决定购买多少个汉堡肉饼,选择如下:。单个肉饼的成本是。

  • 餐车在工作日最多可以存储 个肉饼。然而,由于餐车在周末不运营,且任何到周五晚上仍未售出的库存都会变质,如果在工作日内,购买的肉饼数量和现有库存超过容量,超出的库存也会变质。

  • 每个工作日的汉堡需求是一个随机变量 ,其概率质量函数如下:

  • 每个汉堡的净收入(扣除除肉饼之外的其他原料成本)是 。

  • 一天的销售量是需求量与可用库存中的最小值,因为餐车不能卖出超过可用肉饼数量的汉堡。

所以,我们面临的是一个多步骤的库存规划问题,我们的目标是在一周内最大化总预期利润 ()。

信息

单步库存规划问题通常被称为“新闻 vendor 问题”。它是关于在给定需求分布的情况下平衡过剩和不足的成本。对于许多常见的需求分布,这个问题可以通过解析方法解决。当然,许多现实世界中的库存问题是多步骤的,类似于我们在本章中将要解决的问题。你可以在en.wikipedia.org/wiki/Newsvendor_model 阅读更多关于新闻 vendor 问题的内容。我们将在第十五章《供应链管理》中解决这个问题的更复杂版本。

到目前为止一切顺利。接下来,让我们在 Python 中创建这个环境。

在 Python 中实现餐车环境

我们即将创建的是一个餐车示例的仿真环境,依据我们上述描述的动态。在这个过程中,我们将使用一个专门为此目的设计的流行框架,即 OpenAI 的 Gym 库。你可能以前听说过它。如果没有,也完全没问题,因为它在本示例中并不起决定性作用。我们将逐步讲解你需要知道的内容。

信息

OpenAI 的 Gym 是定义强化学习(RL)环境、开发和比较解决方法的标准库。它还兼容各种 RL 解决库,如 RLlib。如果你还不熟悉 Gym 环境,可以查看它的简明文档:gym.openai.com/docs/。

现在,让我们进入实现部分:

  1. 我们首先导入我们需要的库:

    import numpy as np
    import gym
    
  2. 接下来,我们创建一个 Python 类,该类初始化时使用我们在上一节中描述的环境参数:

    class FoodTruck(gym.Env):
        def __init__(self):
            self.v_demand = [100, 200, 300, 400]
            self.p_demand = [0.3, 0.4, 0.2, 0.1]
            self.capacity = self.v_demand[-1]
            self.days = ['Mon', 'Tue', 'Wed', 
                         'Thu', 'Fri', "Weekend"]
            self.unit_cost = 4
            self.net_revenue = 7
            self.action_space = [0, 100, 200, 300, 400]
            self.state_space = [("Mon", 0)] \
                             + [(d, i) for d in self.days[1:] 
                                 for i in [0, 100, 200, 300]]
    

    状态是一个元组,包括星期几(或周末)和当天的起始库存水平。再次强调,动作是销售开始前要购买的肉饼数量。这些购买的库存会立即可用。请注意,这是一个完全可观察的环境,因此状态空间和观察空间是相同的。给定某一天的可能库存水平是 0、100、200 和 300(因为我们定义了动作集、可能的需求情景和容量);除非我们从星期一开始没有库存。

  3. 接下来,我们定义一个方法,给定当前状态、动作和需求,计算下一个状态和奖励,以及相关的量。请注意,这个方法不会改变对象的任何内容:

        def get_next_state_reward(self, state, action, demand):
            day, inventory = state
            result = {}
            result['next_day'] = self.days[self.days.index(day) \
                                           + 1]
            result['starting_inventory'] = min(self.capacity, 
                                               inventory 
                                               + action)
            result['cost'] = self.unit_cost * action 
            result['sales'] = min(result['starting_inventory'], 
                                  demand)
            result['revenue'] = self.net_revenue * result['sales']
            result['next_inventory'] \
                = result['starting_inventory'] - result['sales']
            result['reward'] = result['revenue'] - result['cost']
            return result
    
  4. 现在,我们定义一个方法,使用 get_next_state_reward 方法返回给定状态-动作对的所有可能转换和奖励,以及相应的概率。请注意,如果需求超过库存,不同的需求情景将导致相同的下一个状态和奖励:

        def get_transition_prob(self, state, action):
            next_s_r_prob = {}
            for ix, demand in enumerate(self.v_demand):
                result = self.get_next_state_reward(state, 
                                                    action, 
                                                    demand)
                next_s = (result['next_day'],
                          result['next_inventory'])
                reward = result['reward']
                prob = self.p_demand[ix]
                if (next_s, reward) not in next_s_r_prob:
                    next_s_r_prob[next_s, reward] = prob
                else:
                    next_s_r_prob[next_s, reward] += prob
            return next_s_r_prob
    

现在我们所需的内容就这些。稍后,我们将向这个类中添加其他方法,以便能够模拟环境。现在,我们将深入讨论使用策略评估方法的动态规划(DP)。

策略评估

在马尔可夫决策过程(MDPs)和强化学习(RL)中,我们的目标是获得(近似)最优策略。那么我们如何评估给定的策略呢?毕竟,如果我们不能评估它,就无法与其他策略进行比较,进而决定哪个更好。因此,我们开始讨论使用策略评估(也称为预测问题)的动态规划方法。有多种方式可以评估给定的策略。事实上,在 第四章,《马尔可夫决策过程的构建》中,当我们定义状态值函数时,我们讨论了如何通过解析和迭代的方式计算它。嗯,这就是策略评估!在本节中,我们将采用迭代版本,接下来我们会详细介绍。

迭代策略评估

让我们首先讨论迭代策略评估算法,并回顾一下我们在上一章中覆盖的内容。然后,我们将评估食品车老板已经拥有的策略(即基础策略)。

迭代策略迭代算法

回想一下,状态的值是根据给定的策略如下定义的:

是从状态 开始并遵循策略 的预期折扣累积奖励。在我们的食品车示例中,状态 的值是从零库存开始的星期一的预期奖励(利润)。最大化 的策略就是最优策略!

现在,贝尔曼方程告诉我们状态值必须彼此一致。这意味着预期的单步奖励加上下一个状态的折扣值应该等于当前状态的值。更正式地说,表达式如下:

由于我们知道这个简单问题的所有转移概率,我们可以解析地计算出这个期望:

开头的 项是因为策略可能会根据状态概率性地选择动作。由于转移概率依赖于动作,我们需要考虑策略可能引导我们执行的每一个可能动作。

现在,我们所需要做的就是将贝尔曼方程转换为如下的更新规则,从而获得一个迭代算法:

一次更新过程, ,涉及更新所有状态值。该算法会在状态值的变化在连续迭代中变得足够小后停止。我们不讨论证明,但这个更新规则可以证明会收敛到 ,当 时。这个算法被称为迭代政策评估,具有期望更新,因为我们考虑了所有可能的单步转移。

在我们实现这个方法之前,最后要提到的是,与其为 和 维护两个状态值副本,并在一次完整的更新后将 替换为 ,我们将直接进行原地更新。这种方法往往收敛得更快,因为我们会立即将最新的状态值估算结果用于其他状态的更新。

接下来,我们来评估库存补充问题的一个基础策略。

基于库存补充策略的迭代评估

假设食品卡车的老板有以下策略:在一个工作日的开始,老板将库存补充到 200 或 300 个肉饼,且概率相等。例如,如果当天开始时库存为 100 个,他们有相同的概率购买 100 个或 200 个肉饼。我们来评估这个策略,看看在一周内我们应该预期多少利润:

  1. 我们首先定义一个返回policy字典的函数,其中字典的键对应状态。每个状态对应的值是另一个字典,字典的键是动作,值是该状态下选择该动作的概率:

    def base_policy(states):
        policy = {}
        for s in states:
            day, inventory = s
            prob_a = {} 
            if inventory >= 300:
                prob_a[0] = 1
            else:
                prob_a[200 - inventory] = 0.5
                prob_a[300 - inventory] = 0.5
            policy[s] = prob_a
        return policy
    
  2. 现在是政策评估。我们定义一个函数,用于计算给定状态的期望更新和该状态对应的策略:

    def expected_update(env, v, s, prob_a, gamma):
        expected_value = 0
        for a in prob_a:
            prob_next_s_r = env.get_transition_prob(s, a)
            for next_s, r in prob_next_s_r:
                expected_value += prob_a[a] \
                                * prob_next_s_r[next_s, r] \
                                * (r + gamma * v[next_s])
        return expected_value
    

    换句话说,这个函数为给定的 计算 。

  3. 策略评估函数会对所有状态执行预期的更新,直到状态值收敛(或达到最大迭代次数):

    def policy_evaluation(env, policy, max_iter=100, 
                          v = None, eps=0.1, gamma=1):
        if not v:
            v = {s: 0 for s in env.state_space}
        k = 0
        while True:
            max_delta = 0
            for s in v:
                if not env.is_terminal(s):
                    v_old = v[s]
                    prob_a = policy[s]
                    v[s] = expected_update(env, v, 
                                           s, prob_a, 
                                           gamma)
                    max_delta = max(max_delta, 
                                    abs(v[s] - v_old))
            k += 1
            if max_delta < eps:
                print("Converged in", k, "iterations.")
                break
            elif k == max_iter:
                print("Terminating after", k, "iterations.")
                break
        return v
    

    让我们详细说明一下这个函数的工作原理:

    a) policy_evaluation函数接收一个环境对象,在我们的示例中它将是FoodTruck类的一个实例。

    b) 该函数评估指定的策略,策略以字典的形式存在,字典将状态映射到动作概率。

    c) 所有的状态值初始为 0,除非在函数中传入了初始化值。终止状态(在本例中对应周末的状态)的状态值不会被更新,因为从那时起我们不再期望任何奖励。

    d) 我们定义了一个 epsilon 值作为收敛的阈值。如果在某一轮更新中,所有状态值的最大变化小于该阈值,则评估终止。

    e) 由于这是一个具有有限步数的阶段性任务,我们默认将折扣因子gamma设置为 1。

    f) 该函数返回状态值,稍后我们将需要这些状态值。

  4. 现在,我们使用这个函数来评估所有者的基础策略。首先,从我们之前定义的类中创建一个foodtruck对象:

    foodtruck = FoodTruck()
    
  5. 获取环境的基础策略:

    policy = base_policy(foodtruck.state_space)
    
  6. 评估基础策略并获取对应的状态值——特别是对于初始状态:

    v = policy_evaluation(foodtruck, policy)
    print("Expected weekly profit:", v["Mon", 0])
    
  7. 结果将如下所示:

    Converged in 6 iterations.
    Expected weekly profit: 2515.0
    

在这个策略下,("Mon", 0)这个初始状态的状态值为 2515。对于一周的时间来说,这可不算坏的利润!

到目前为止做得很好!现在你已经能够评估给定的策略并计算出对应的状态值。在进入策略改进之前,我们再做一件事。让我们验证一下,在这个策略下模拟环境是否会导致类似的奖励。

比较策略评估与模拟结果

为了能够模拟环境,我们需要向FoodTruck类添加一些额外的方法:

  1. 创建一个reset方法,简单地将对象初始化/重置为周一早晨,并将库存设为零。每次开始一个阶段时,我们都会调用这个方法:

        def reset(self):
            self.day = "Mon"
            self.inventory = 0
            state = (self.day, self.inventory)
            return state
    
  2. 接下来,定义一个方法来检查给定状态是否是终止状态。请记住,在本例中,阶段会在周末结束时终止:

        def is_terminal(self, state):
            day, inventory = state
            if day == "Weekend":
                return True
            else:
                return False
    
  3. 最后,定义step方法来模拟给定当前状态和动作的一次环境步骤:

        def step(self, action):
            demand = np.random.choice(self.v_demand, 
                                      p=self.p_demand)
            result = self.get_next_state_reward((self.day, 
                                                 self.inventory), 
                                           action, 
                                           demand)
            self.day = result['next_day']
            self.inventory = result['next_inventory']
            state = (self.day, self.inventory)
            reward = result['reward']
            done = self.is_terminal(state)
            info = {'demand': demand, 'sales': result['sales']}
            return state, reward, done, info
    

    该方法返回新的状态、一轮奖励、该阶段是否完成,以及我们想要返回的任何附加信息。这是标准的 Gym 约定。它还会更新类中的状态。

  4. 现在我们的FoodTruck类已经准备好进行模拟。接下来,让我们创建一个函数,在给定状态的情况下,从一个(可能是概率性的)策略中选择一个动作:

    def choose_action(state, policy):
        prob_a = policy[state]
        action = np.random.choice(a=list(prob_a.keys()), 
                                  p=list(prob_a.values()))
        return action
    
  5. 让我们创建一个函数(在类外部)来模拟给定的策略:

    def simulate_policy(policy, n_episodes):
        np.random.seed(0)
        foodtruck = FoodTruck()
        rewards = []
        for i_episode in range(n_episodes):
            state = foodtruck.reset()
            done = False
            ep_reward = 0
            while not done:
                action = choose_action(state, policy)
                state, reward, done, info = foodtruck.step(action) 
                ep_reward += reward
            rewards.append(ep_reward)
        print("Expected weekly profit:", np.mean(rewards))
    

    simulate_policy函数仅执行以下操作:

    a) 接收一个策略字典,返回给定状态下该策略建议的动作及其相应的概率。

    b) 它模拟策略执行指定次数的回合。

    c) 在一次回合内,它从初始状态开始,并在每一步按概率选择策略建议的动作。

    d) 选择的动作传递给环境,环境根据其动态转移到下一个状态。

  6. 现在,让我们用基础策略来模拟环境!

    simulate_policy(policy, 1000)
    
  7. 结果应该如下所示:

    Expected weekly profit: 2518.1
    

太棒了!这和我们通过解析计算得到的结果非常接近!现在是时候利用这种迭代策略评估方法做些更有用的事情了:寻找最优策略!

策略迭代

现在我们有了一种评估给定策略的方法,可以用它来比较两个策略并迭代改进它们。在这一节中,我们首先讨论如何比较策略。接着,我们介绍策略改进定理,最后将所有内容整合到策略改进算法中。

策略比较与改进

假设我们有两个策略, 和 ,我们想要比较它们。我们说如果满足以下条件, 就和 一样好:

。

换句话说,如果在一个策略 下的状态值大于或等于另一个策略 下的状态值,且对于所有可能的状态都成立,那么意味着 和 一样好。如果对于任何状态 ,这种关系是严格不等式,那么 就是比 更好的策略。这应该是直观的,因为状态值表示从该点开始的期望累积奖励。

现在,问题是我们如何从 过渡到更好的策略 。为此,我们需要回顾在 第四章 中定义的动作价值函数,马尔科夫决策过程的构建:

记住,动作价值函数的定义有些细微之处。它是当以下情况发生时的期望累积未来奖励:

  • 在当前状态 执行动作 。

  • 然后遵循该策略 。

其中的细微之处是,策略 在状态 时,通常可能会建议另一个动作。q 值表示当前步骤中从策略 偏离的即时偏差。

那么这对改进策略有什么帮助呢?策略改进定理表明,如果在状态 时选择 初始动作比一直选择 更好,则在状态 时,每次选择 而不是一直跟随 将是一个比 更好的策略。换句话说,如果 ,那么我们可以通过在状态 采取动作 并在其余状态下跟随 来改进 。我们这里不包括该定理的证明,但其实它是非常直观的,并且可以参考 Sutton & Barto, 2018。

让我们对这个论点进行一般化。假设某个策略 至少和另一个策略 一样好,如果对于所有 ,以下条件成立:

接下来,我们要做的就是选择能够最大化每个状态的 q 值的动作来改进策略。即,

在我们结束这个讨论之前,有一个最后的说明:尽管我们描述了确定性策略的策略改进方法,对于给定状态,策略只建议一个单一动作 ,但这个方法同样适用于随机策略。

到目前为止,一切顺利!现在,让我们将这个策略改进转化为一个算法,帮助我们找到最优策略!

策略迭代算法

策略迭代算法的基本过程是:从任意策略开始,进行策略评估步骤,然后进行策略改进步骤。当这个过程重复进行时,最终会得到一个最优策略。这个过程在下面的图中得到了展示:

图 5.1 – 广义策略迭代

实际上,在某些形式的策略评估和策略改进步骤之间迭代,是解决强化学习问题的一种通用方法。这就是为什么这个想法被称为广义策略迭代(GPI) Sutton & Barto, 2018。只不过我们在这一节描述的策略迭代方法涉及这些步骤的特定形式。

让我们为餐车环境实现一个策略迭代。

为库存补货问题实现策略迭代

我们已经编写了策略评估和期望更新步骤。我们为策略迭代算法所需要的附加步骤是策略改进步骤,完成后我们就能得到最优策略!这真令人兴奋,让我们开始吧:

  1. 让我们从实现上述描述的策略改进开始:

    def policy_improvement(env, v, s, actions, gamma):
        prob_a = {}
        if not env.is_terminal(s):
            max_q = np.NINF
            best_a = None
            for a in actions:
                q_sa = expected_update(env, v, s, {a: 1}, gamma)
                if q_sa >= max_q:
                    max_q = q_sa
                    best_a = a
            prob_a[best_a] = 1
        else:
            max_q = 0
        return prob_a, max_q
    

    该函数根据当前策略下得到的值函数,搜索在给定状态下产生最大 q 值的动作。对于终止状态,q 值始终为 0。

  2. 现在,我们将所有内容整合到一个策略迭代算法中:

    def policy_iteration(env,  eps=0.1, gamma=1):
        np.random.seed(1)
        states = env.state_space
        actions = env.action_space
        policy = {s: {np.random.choice(actions): 1}
                 for s in states}
        v = {s: 0 for s in states}
        while True:
            v = policy_evaluation(env, policy, v=v, 
                              eps=eps, gamma=gamma)
            old_policy = policy
            policy = {}
            for s in states:
                policy[s], _ = policy_improvement(env, v, s, 
                                        actions, gamma)
            if old_policy == policy:
                break
        print("Optimal policy found!")
        return policy, v
    

    该算法从一个随机策略开始,在每次迭代中实施策略评估和改进步骤。当策略稳定时,它停止。

  3. 终于迎来了关键时刻!让我们找出食品车的最优策略,并看看预期的周利润是多少!

    policy, v = policy_iteration(foodtruck)
    print("Expected weekly profit:", v["Mon", 0])
    
  4. 结果应如下所示:

    Converged in 6 iterations.
    Converged in 6 iterations.
    Converged in 5 iterations.
    Optimal policy found!
    Expected weekly profit: 2880.0
    

我们刚刚找到了一个策略,它的预期周利润为$2,880。这比基础策略有了显著的提升!感谢您支持本地企业!

从输出结果可以看出,相较于随机策略,进行了两次策略改进。第三次策略改进未导致策略的任何变化,算法因此终止。

让我们看看最优策略是什么样的:

图 5.2 – 食品车示例的最优策略

策略迭代算法得出的结果非常直观。让我们分析一下这个策略:

  • 在周一和周二,保证在剩余的时间内将售出 400 个汉堡。由于肉饼可以在工作日安全存储,因此将库存填充至最大容量是有意义的。

  • 在周三开始时,可能会发生直到本周末销售总量为 300,并且 100 个肉饼会变质的情况。然而,这种情况的可能性较小,预期利润仍然为正。

  • 对于周四和周五,更保守一些是更明智的,以防需求少于库存,从而避免昂贵的变质损失。

恭喜!你已经成功地使用策略迭代解决了一个 MDP 问题!

提示

我们发现的最优策略,严重依赖于肉饼的成本、每单位的净收入以及需求分布。通过修改问题参数并再次求解,你可以更直观地理解最优策略的结构以及它如何变化。

你已经走了很长一段路!我们从 MDP 和 DP 的基础开始构建了一个精确的求解方法。接下来,我们将研究另一种通常比策略迭代更高效的算法。

值迭代

策略迭代要求我们完全评估所有策略,直到状态值收敛,然后才进行改进步骤。在更复杂的问题中,等待完成评估可能会非常耗费计算资源。即使在我们的例子中,单次策略评估步骤也需要对所有状态进行 5-6 次遍历才能收敛。事实证明,我们可以在策略评估未收敛时提前终止,而不会失去策略迭代的收敛保证。实际上,我们甚至可以通过将上一章中介绍的贝尔曼最优方程转化为更新规则,将策略迭代和策略改进合并为一个步骤:

我们不断对所有状态执行更新,直到状态值收敛为止。这个算法称为价值迭代。

提示

请注意策略评估更新和价值迭代更新之间的区别。前者从给定的策略中选择动作,因此在期望更新前有项。而后者则不遵循策略,而是通过运算符主动搜索最佳动作。

这就是实现价值迭代算法所需要的全部内容。接下来,让我们直接进入实现部分。

实现库存补充问题的价值迭代

为了实现价值迭代,我们将使用之前定义的policy_improvement函数。然而,在改进每个状态的策略后,我们还将更新该状态的状态值估计。

现在,我们可以按照以下步骤实现价值迭代:

  1. 我们首先定义如上所述的价值迭代函数,通过原地替换状态值来实现:

    def value_iteration(env, max_iter=100, eps=0.1, gamma=1):
        states = env.state_space
        actions = env.action_space
        v = {s: 0 for s in states}
        policy = {}
        k = 0
        while True:
            max_delta = 0
            for s in states:
                old_v = v[s]
                policy[s], v[s] = policy_improvement(env, 
                                                     v, 
                                                     s, 
                                                     actions, 
                                                     gamma)
                max_delta = max(max_delta, abs(v[s] - old_v))
            k += 1
            if max_delta < eps:
                print("Converged in", k, "iterations.")
                break
            elif k == max_iter:
                print("Terminating after", k, "iterations.")
                break
        return policy, v
    
  2. 然后,我们执行价值迭代并观察初始状态的值:

    policy, v = value_iteration(foodtruck)
    print("Expected weekly profit:", v["Mon", 0])
    
  3. 结果应如下所示:

    Converged in 6 iterations.
    Expected weekly profit: 2880.0
    

价值迭代提供了最优策略,但相比于策略迭代算法,它的计算量更小!价值迭代只对状态空间进行 6 次遍历,而策略迭代则需要 20 次遍历(17 次用于策略评估,3 次用于策略改进)。

现在,记住我们关于广义策略改进的讨论。实际上,你可以将策略改进步骤与截断的策略评估步骤结合起来,在一些复杂的示例中,当策略改进后状态值发生显著变化时,收敛速度比策略迭代和价值迭代算法都要快。

干得好!我们已经涵盖了如何使用动态规划解决 MDP 的预测问题,以及找到最优策略的两种算法,并且它们在我们的简单示例中表现出色。另一方面,动态规划方法在实践中有两个重要的缺点。接下来我们将讨论这些缺点,以及为什么我们需要在本章后续介绍的其他方法。

动态规划的缺点

动态规划方法非常适合学习如何扎实掌握 MDP 的解决方法。与直接搜索算法或线性规划方法相比,它们效率更高。另一方面,在实际应用中,这些算法仍然要么不可行,要么无法使用。接下来我们将详细说明为什么。

维度灾难

策略迭代和价值迭代算法都会多次遍历整个状态空间,直到找到最优策略。同时,我们还以表格形式存储每个状态的策略、状态值和动作值。然而,任何实际问题都会有一个巨大的可能状态数,这一现象被称为维度灾难。这指的是,随着我们增加维度,变量(状态)的可能值数呈指数增长。

以我们的餐车示例为例。除了跟踪肉饼,我们还假设同时跟踪汉堡包胚、番茄和洋葱。还假设这些物品的每个容量是 400,并且我们精确地统计了库存数量。在这种情况下,可能的状态数量将是,即大于。这是一个荒谬的状态数量,要为这么一个简单的问题跟踪这么多状态。

解决维度灾难的一个方法是异步动态规划:

  • 这种方法建议在每次策略改进迭代中,避免遍历整个状态空间,而是集中在更有可能被遇到的状态上。

  • 对于许多问题,状态空间的各个部分并不是同等重要的。因此,在对整个状态空间进行完整遍历之前等待更新策略是浪费的。

  • 使用异步算法,我们可以在进行策略改进的同时并行模拟环境,观察哪些状态被访问,并更新这些状态的策略和值函数。

  • 同时,我们可以将更新后的策略传递给代理,这样模拟将继续以新的策略进行。

假设代理足够探索了状态空间,算法最终会收敛到最优解。

另一方面,我们用来解决这个问题的一个更重要的工具是函数逼近器,比如深度神经网络。想一想!如果我们为库存水平 135、136、137 分别存储一个单独的策略/状态值/动作值,那有什么好处呢?其实并没有什么太大意义。与表格表示法相比,函数逼近器以一种更紧凑(尽管是近似的)方式表示我们想要学习的内容。事实上,在许多情况下,深度神经网络之所以成为函数逼近的一个有意义的选择,是因为它们具有强大的表示能力。这也是为什么从下一章开始,我们将专注于深度强化学习算法的原因。

对环境完整模型的需求

在我们目前使用的方法中,我们依赖于环境的状态转移概率来进行策略评估、策略迭代和价值迭代算法,从而获得最优策略。然而,这种方法在实际中通常并不可行。通常要么这些概率对于每一个可能的转移计算起来非常困难(甚至往往无法列举出来),要么我们根本不知道这些概率。你知道什么更容易获得吗?一个从环境本身或其模拟中获得的样本轨迹。事实上,模拟是强化学习中一个特别重要的组成部分,我们将在本章末尾单独讨论。

那么问题变成了如何使用样本轨迹来学习近似最优策略。实际上,这正是我们将在本章剩余部分通过蒙特卡洛(Monte Carlo)和时序差分(TD)方法讲解的内容。你将学到的这些概念是许多高级强化学习(RL)算法的核心。

使用蒙特卡洛方法训练你的代理

假设你想要学习一个特定的、可能存在偏差的硬币翻转正面朝上的概率:

  • 计算这个概率的一种方法是通过仔细分析硬币的物理属性。尽管这样做能够给出结果的精确概率分布,但这远不是一种实用的方法。

  • 或者,你可以多次投掷硬币,查看样本中的分布。如果你的样本量不大,估计值可能会有些偏差,但对于大多数实际应用来说,这已经足够了。使用后一种方法时需要处理的数学问题将简单得多。

就像硬币示例中一样,我们可以通过随机样本估计 MDP 中的状态值和动作值。蒙特卡洛(MC)估计是一种通用概念,指的是通过重复的随机采样进行估计。在强化学习的背景下,它指的是通过完整回合的样本轨迹估计状态值和动作值的方法集合。使用随机样本非常方便,事实上,对于任何实际的 RL 问题来说都是至关重要的,因为环境动态(状态转移和奖励概率分布)通常具有以下特点:

  • 过于复杂,无法处理

  • 初始时未知

因此,蒙特卡罗方法是强大的方法,可以让强化学习代理仅通过与环境交互收集的经验来学习最优策略,而无需了解环境是如何工作的。

在本节中,我们首先将研究如何使用蒙特卡罗方法估计给定策略的状态值和动作值。然后,我们将介绍如何进行改进以获得最优策略。

蒙特卡罗预测

与 DP 方法一样,我们需要能够评估给定策略!,才能改进它。在本节中,我们将介绍如何通过估计相应的状态和动作值来评估策略。在此过程中,我们将简要回顾上一章的网格世界示例,并进一步探讨食品卡车库存补充问题。

估计状态值函数

记住,在策略!下,状态!的值,!定义为从状态!开始时的期望累计奖励:

MC 预测建议通过观察(多个)样本轨迹,即一系列的状态-动作-奖励元组,从状态!开始,以估计这种期望值。这类似于抛硬币来估计其分布。

最好通过一个例子来解释蒙特卡罗方法。特别是在网格世界的例子中,它的工作原理非常直观,因此我们接下来将再次回顾它。

使用样本轨迹进行状态值估计

回想一下,网格世界中的机器人每次移动都能获得+1 的奖励,只要它不撞墙。当它撞到墙壁时,该回合结束。假设这个机器人只能在一定程度上被控制。当指示它朝某个特定方向移动时,它有 70%的概率会按命令执行。其余三个方向的概率各为 10%。

考虑一个确定性策略!,如图 5.3 (a)所示。如果机器人从状态(1,2)开始,它可以遵循的两个示例轨迹,!和!,以及每个转移的相应概率如图 5.3 (b)所示:

图 5.3 – a) 一个确定性策略π,b) 在π下的两个样本轨迹

提示

请注意,机器人遵循的是随机轨迹,但策略本身是确定性的,这意味着在给定状态下采取的动作(发给机器人的命令)始终是相同的。随机性来自环境,由于概率性的状态转移。

对于轨迹!,观察到该轨迹的概率和相应的折扣回报如下:

那对于 来说:

对于这两个示例轨迹,我们能够计算出相应的概率和回报。然而,要计算 的状态值,我们需要评估以下表达式:

这意味着我们需要识别以下内容:

  • 每一条可能的轨迹 ,它可以从 出发,依据策略

  • 在 下观察到 , 的概率

  • 相应的折扣回报,

嗯,那是一个不可能完成的任务。即便是在这个简单的问题中,可能的轨迹数也是无限的。

这正是蒙特卡洛预测的作用所在。它简单地告诉我们通过如下方式,利用样本回报的平均值来估计状态 的值:

就是这样!样本轨迹和回报就是你估计状态值所需的全部。

小贴士

请注意, 表示状态的真实值,而 表示估计值。

在这一点上,你可能会问以下问题:

  • 怎么会仅凭两个样本回报就能估计出一个结果,这个结果是无限多个轨迹的结果? 其实并不行。你有的样本轨迹越多,你的估计就越准确。

  • 我们怎么知道我们有足够的样本轨迹? 这个问题很难量化。但在更复杂的环境中,尤其是当存在显著的随机性时,可能需要更多的样本才能做出准确的估计。不过一个好主意是,在增加更多轨迹样本时,检查估计值是否趋于收敛。

  • 和 发生的概率是非常不同的。将它们在估计中赋予相等的权重合适吗? 当我们只有两个轨迹样本时,这确实是一个问题。然而,随着样本轨迹的增多,我们可以期待样本中轨迹的出现与它们真实发生的概率成比例。

  • 我们能否使用相同的轨迹来估计它访问的其他状态的值? 是的!实际上,这就是我们在蒙特卡洛预测中所做的事情。

接下来我们来详细讨论如何使用相同的轨迹来估计不同状态的值。

首次访问与每次访问的蒙特卡洛预测

如果你回忆一下什么是马尔可夫性质,它简单地告诉我们,未来依赖于当前状态,而不是过去。因此,我们可以把例如 看作是从状态 和 出发的三个独立轨迹。我们称后两个轨迹为 和 。因此,我们可以为样本集中的所有访问过的状态获得一个值估计。例如,状态 的值估计如下:

由于没有其他轨迹访问状态 ,我们使用单一回报来估计状态值。注意,折现因子会根据回报与初始时间步的时间距离应用。这就是为什么 折现因子的指数减少了一。

请考虑图5.4中的一组轨迹,并假设我们再次想估计 。这些样本轨迹中没有任何轨迹真正从状态 出发,但这完全没问题。我们可以使用轨迹 、 和 来进行估计。但这里有一个有趣的情况: 访问了状态 两次。我们应该仅使用它第一次访问的回报,还是每次访问的回报?

图 5.4 – 蒙特卡洛估计

这两种方法都是有效的。前者被称为首次访问 MC 方法,后者被称为每次访问 MC 方法。它们的比较如下:

  • 随着访问次数趋近于无限,它们都收敛到真实的 。

  • 首次访问 MC 方法给出了状态值的无偏估计,而每次访问 MC 方法则是有偏的。

  • 均方误差 (MSE) 在首次访问 MC 方法中样本较少时较高,但在样本较多时低于每次访问的 MSE。

  • 每次访问的 MC 方法在使用函数逼近时更为自然。

如果听起来很复杂,其实并不复杂!只需记住以下几点:

  • 我们尝试估计一个参数,例如 、 的状态值。

  • 通过观察随机变量 ,从 开始,折现值返回多次。

  • 考虑从它们首次访问 开始的轨迹,或者从每次访问开始的轨迹,都是有效的。

现在是时候实现蒙特卡洛预测了!

实现首次访问的蒙特卡洛状态值估计

我们使用了网格世界示例来获得直观的理解,现在让我们回到我们的餐车示例来进行实现。在这里,我们将实现首次访问蒙特卡洛(MC)方法,但这个实现可以很容易地修改为每次访问 MC。只需要去掉在计算回报时的条件,即只有当状态在此之前的轨迹中没有出现时才计算回报。对于餐车示例,由于轨迹中每天的状态都不同——因为每次转换后,状态中的星期几部分都会变化——所以这两种方法是相同的。

让我们按照这些步骤进行实现:

  1. 我们首先定义一个函数,接收一个轨迹并计算从首次访问开始的每个状态的回报:

    def first_visit_return(returns, trajectory, gamma):
        G = 0
        T = len(trajectory) - 1
        for t, sar in enumerate(reversed(trajectory)):
            s, a, r = sar
            G = r + gamma * G
            first_visit = True
            for j in range(T - t):
                if s == trajectory[j][0]:
                    first_visit = False
            if first_visit:
                if s in returns:
                    returns[s].append(G)
                else:
                    returns[s] = [G]
        return returns
    

    该函数执行以下操作:

    a) 输入一个字典returns,其键是状态,值是一些其他轨迹中计算出的回报列表。

    b) 输入一个trajectory列表,它是一个状态-动作-奖励元组的列表。

    c) 将每个状态的计算回报添加到returns中。如果该状态以前从未被其他轨迹访问过,则初始化该状态的列表。

    d) 从轨迹的末尾开始反向遍历,方便计算折扣回报。每一步都会应用折扣因子。

    e) 在每次计算后检查某个状态是否在轨迹中已被访问过。如果该状态是首次访问,则将计算的回报保存到returns字典中。

  2. 接下来,我们实现一个函数,用给定的策略模拟一个单一的回合,并返回轨迹:

    def get_trajectory(env, policy):
        trajectory = []
        state = env.reset()
        done = False
        sar = [state]
        while not done:
            action = choose_action(state, policy)
            state, reward, done, info = env.step(action)
            sar.append(action)
            sar.append(reward)
            trajectory.append(sar)
            sar = [state]
        return trajectory
    
  3. 现在,实现在指定回合数/轨迹数的环境中模拟,使用给定策略,并跟踪轨迹,计算每个状态的回报平均值,这些回报由first_visit_return函数计算:

    def first_visit_mc(env, policy, gamma, n_trajectories):
        np.random.seed(0)
        returns = {}
        v = {}
        for i in range(n_trajectories):
            trajectory = get_trajectory(env, policy)
            returns = first_visit_return(returns, 
                                         trajectory, 
                                         gamma)
        for s in env.state_space:
            if s in returns:
                v[s] = np.round(np.mean(returns[s]), 1)
        return v
    
  4. 我们确保创建一个环境实例(或者直接使用前面章节中的环境实例)。同时获取基本策略,这个策略会以相等的概率将汉堡饼库存填充到 200 或 300:

    foodtruck = FoodTruck()
    policy = base_policy(foodtruck.state_space)
    
  5. 现在,让我们使用首次访问 MC 方法,通过 1,000 个轨迹来估计状态值:

    v_est = first_visit_mc(foodtruck, policy, 1, 1000)
    
  6. 结果,v_est,将如下所示:

    {('Mon', 0): 2515.9,
     ('Tue', 0): 1959.1,
     ('Tue', 100): 2362.2,
     ('Tue', 200): 2765.2,
    ...
    
  7. 现在,记住我们可以使用动态规划(DP)中的策略评估方法来计算真实的状态值进行比较:

    v_true = policy_evaluation(foodtruck, policy)
    
  8. 真实的状态值看起来会非常相似:

    {('Mon', 0): 2515.0,
     ('Tue', 0): 1960.0,
     ('Tue', 100): 2360.0,
     ('Tue', 200): 2760.0,
    ...
    
  9. 我们可以使用不同数量的轨迹来获得估计结果,例如 10、100 和 1000 个轨迹。我们来做一下这个比较,看看状态值的估计如何逐渐接近真实值,如图 5.5所示:

图 5.5 – 首次访问蒙特卡洛估计与真实状态值

让我们更仔细地分析一下结果:

a) 随着我们收集更多的轨迹,估计值越来越接近真实的状态值。你可以增加轨迹的数量,甚至使用更高的数量来获得更精确的估计。

b) 在我们收集了 10 条轨迹后,状态("Tue", 200)没有被估算值。这是因为在这 10 条轨迹中该状态从未被访问过。这凸显了收集足够轨迹的重要性。

c) 对于那些在当天开始时拥有 300 单位库存的状态,没有估算值。这是因为在基础策略下,这些状态是无法访问的。但是,我们对这些状态的值一无所知。另一方面,这些状态可能是我们希望策略引导我们去的有价值的状态。这是一个我们需要解决的探索问题。

现在我们有了一种估算状态值的方法,无需了解环境的动态,只需使用智能体在环境中的经验。到目前为止,做得很棒!然而,仍然存在一个重要问题。仅凭状态值估算,我们无法真正改进当前的策略。为了理解为什么会这样,回想一下我们是如何通过动态规划方法(如值迭代)来改进策略的:

我们结合状态值估算与转移概率来获得行动(q)值。然后,我们为每个状态选择一个最大化该状态 q 值的行动。现在,由于我们假设不了解环境,我们无法通过状态值来推导行动值。

这给我们留下了一个选择:我们需要直接估算行动值。幸运的是,这将类似于我们估算状态值的方式。接下来,我们来探讨使用蒙特卡洛方法估算行动值。

估算行动值函数

行动值,,表示从状态开始,执行行动,并遵循策略时的预期折现累计回报。考虑以下轨迹:

可以使用观察到的折现回报来估算,等等。然后,我们可以用它们来确定给定状态的最佳行动,如下所示:

这是一个挑战:如果我们没有所有可能的的行动值估算,怎么办?考虑网格世界的例子。如果策略总是选择在状态下往右走,我们将永远不会有一个以状态-行动对或开始的轨迹。因此,即使这些行动中的某一个提供的行动值高于,我们也永远无法发现它。这个问题在食物车示例中的基础策略下也是类似的。一般来说,当我们使用确定性策略,或者使用一个在某些状态下不会以正概率选择所有行动的随机策略时,都会遇到这种情况。

因此,我们在这里面临的本质上是一个探索问题,这是强化学习中的一个基本挑战。

解决这个问题有两种可能的方案:

  • 从一个随机选择的动作开始轨迹,在一个随机初始状态下,然后像往常一样遵循策略 。这被称为 探索性起始。这样可以确保每个状态-动作对至少被选择一次,从而可以估计动作值。这个方法的缺点是,我们需要始终从随机初始化开始每个 episode。如果我们希望通过与环境的持续互动来学习动作值,而不需要频繁重新开始,那么这种方法就不是特别有用。

  • 探索问题的另一种更常见的解决方案是保持一个策略,在任何状态下都以正概率选择所有动作。更正式地说,我们需要一个满足 的策略,适用于所有 和所有 ;其中 是所有可能状态的集合, 是状态 中所有可能动作的集合。这样的策略 称为 软策略。

在接下来的章节中,我们将使用软策略动作值估计,进而用于策略改进。

蒙特卡罗控制

蒙特卡罗控制指的是一类方法,通过使用折扣回报的样本来找到最优/近似最优的策略。换句话说,这就是通过经验学习最优策略。而且,由于我们依赖经验来发现最优策略,我们必须进行探索,正如我们上面所解释的那样。接下来,我们将实现 ε-greedy 策略,使我们能够在训练过程中进行探索,这是一种特殊形式的软策略。之后,我们将讨论蒙特卡罗控制的两种不同变体,即在策略方法和离策略方法。

实现 -greedy 策略

与我们在赌徒问题中所做的非常相似,-greedy 策略以 ε 的概率随机选择一个动作;以 的概率选择最大化动作值函数的动作。这样,我们可以继续探索所有状态-动作对,同时选择我们识别出的最佳动作,并且高概率地做出选择。

现在让我们实现一个函数,将一个确定性动作转换为 -greedy 动作,稍后我们将需要它。该函数将为最佳动作分配 的概率,并为所有其他动作分配 的概率:

def get_eps_greedy(actions, eps, a_best):
    prob_a = {}
    n_a = len(actions)
    for a in actions:
        if a == a_best:
            prob_a[a] = 1 - eps + eps/n_a
        else:
            prob_a[a] = eps/n_a
    return prob_a

在训练过程中,探索是找到最优策略所必需的。另一方面,在训练结束后(即推理阶段),我们并不希望进行探索性行动,而是选择最佳行动。因此,这两种策略是不同的。为了区分这两者,前者被称为行为策略,后者被称为目标策略。我们可以使状态和值函数与前者或后者对齐,从而产生两种不同的方法:在策略方法和离策略方法。接下来我们将详细比较这两种方法。

在策略与离策略方法

请记住,状态和值函数与特定的策略相关联,因此使用和符号。在策略方法估计的是用于训练的行为策略下的状态和值函数,例如生成训练数据/经验的策略。离策略方法则估计的是与行为策略不同的策略下的状态和值函数,例如目标策略。我们理想的情况是将探索与值估计分开。接下来我们将详细探讨为什么要这样做。

在策略方法对值函数估计的影响

探索性策略通常不是最优的,因为它们会不定期地采取随机动作以进行探索。由于在策略方法是针对行为策略来估计状态和值函数的,这种次优性会反映在值估计中。

请考虑以下修改后的网格世界示例,以观察将探索性行动的影响纳入值估计可能会带来的负面影响:机器人需要在状态下选择向左或向右,并在状态 1 和 3 之间选择向上或向下。机器人完美地遵循这些动作,因此环境中没有随机性。此情况在图 5.6中有所展示:

图 5.6 – 修改后的网格世界

该机器人采用了一个-贪婪策略,意味着以 0.99 的概率选择最佳行动,以 0.01 的概率选择探索性行动。在状态 3 下,最佳策略是以较高的可能性向上移动。在状态 1 中,选择其实并不重要。通过在策略上进行估计得到的状态值如下:

在基于策略的方式下,为状态 2 获得的策略会建议向左走,朝向状态 1。另一方面,在这个确定性环境中,当没有探索时,机器人可以完美地避免大的惩罚。基于策略的方法无法识别这一点,因为探索会影响价值估计,从而导致次优策略。另一方面,在某些情况下,如果例如样本是通过物理机器人收集的,且某些状态访问成本非常高,我们可能希望智能体考虑探索的影响。

基于策略和离策略方法的样本效率比较

如上所述,离策略方法估计与行为策略不同的策略的状态和动作值。而基于策略的方法,只估算行为策略的状态和动作值。当我们在后面的章节中讨论深度强化学习的基于策略方法时,我们将看到,这将要求基于策略的方法在行为策略更新后丢弃过去的经验。然而,离策略的深度强化学习方法可以一次又一次地重复利用过去的经验。这在样本效率上是一个显著的优势,尤其是在获得经验的成本很高时。

离策略方法能够利用由非行为策略生成的经验的另一个场景是,当我们希望基于非强化学习控制器(例如经典的 PID 控制器或人工操作员)生成的数据来热启动训练时。这在环境难以模拟或采集经验时尤为有用。

基于策略方法的优势

那么,天然的问题是,为什么我们还要讨论基于策略的方法,而不是直接忽略它们呢?基于策略方法有几个优势:

  • 如上所述,如果从环境中采样的成本很高(指实际环境而非模拟环境),我们可能希望有一个能够反映探索影响的策略,以避免灾难性后果。

  • 离策略方法与函数逼近器结合时,可能会出现收敛到一个良好策略的问题。我们将在下一章讨论这一点。

  • 当动作空间是连续时,基于策略的方法更容易操作,正如我们稍后会讨论的那样。

有了这些,我们终于可以实现基于策略的蒙特卡洛方法,然后是离策略的蒙特卡洛方法!

基于策略的蒙特卡洛控制

我们之前描述的 GPI 框架,它建议在某些形式的策略评估和策略改进之间来回交替,这也是我们与蒙特卡洛方法一起用来获得最优策略的方式。在每个周期中,我们从一个完整的情节中收集一个轨迹,然后估算动作值并更新策略,依此类推。

让我们实现一个基于策略的蒙特卡洛控制算法,并用它来优化餐车库存补充:

  1. 我们首先创建一个生成随机策略的函数,在这个策略中所有动作的概率是一样的,用于初始化策略:

    def get_random_policy(states, actions):
        policy = {}
        n_a = len(actions)
        for s in states:
            policy[s] = {a: 1/n_a for a in actions}
        return policy
    
  2. 现在,我们构建在策略的首次访问蒙特卡洛控制算法:

    import operator
    def on_policy_first_visit_mc(env, n_iter, eps, gamma):
        np.random.seed(0)
        states =  env.state_space
        actions = env.action_space
        policy =  get_random_policy(states, actions)
        Q = {s: {a: 0 for a in actions} for s in states}
        Q_n = {s: {a: 0 for a in actions} for s in states}
        for i in range(n_iter):
            if i % 10000 == 0:
                print("Iteration:", i)
            trajectory = get_trajectory(env, policy)
            G = 0
            T = len(trajectory) - 1
            for t, sar in enumerate(reversed(trajectory)):
                s, a, r = sar
                G = r + gamma * G
                first_visit = True
                for j in range(T - t):
                    s_j = trajectory[j][0]
                    a_j = trajectory[j][1]
                    if (s, a) == (s_j, a_j):
                        first_visit = False
                if first_visit:
                    Q[s][a] = Q_n[s][a] * Q[s][a] + G
                    Q_n[s][a] += 1
                    Q[s][a] /= Q_n[s][a]
                    a_best = max(Q[s].items(), 
                                 key=operator.itemgetter(1))[0]
                    policy[s] = get_eps_greedy(actions, 
                                               eps, 
                                               a_best)
        return policy, Q, Q_n
    

    这与首次访问蒙特卡洛预测方法非常相似,关键的不同之处如下:

    a) 我们不再估计状态值 ,而是估计动作值 。

    b) 为了探索所有的状态-动作对,我们使用 ε-贪婪策略。

    c) 这是一个首次访问方法,但我们不检查状态是否早些时候出现在轨迹中,而是在更新 估计值之前,检查状态-动作对是否在轨迹中出现过。

    d) 每当我们更新状态-动作对的 估计值时,我们还会更新策略,给与能最大化 的动作最高概率,即 。

  3. 使用该算法优化餐车策略:

    policy, Q, Q_n = on_policy_first_visit_mc(foodtruck, 
                                              300000, 
                                              0.05, 
                                              1)
    
  4. 显示 policy 字典。你会看到它是我们之前使用 DP 方法找到的最优策略:

    {('Mon', 0):{0:0.01, 100:0.01, 200:0.01, 300:0.01, 400:0.96},
    …
    

就是这样!这个算法不像 DP 方法那样使用任何环境知识。它从与(环境模拟)的交互中学习!而且,当我们运行该算法足够长时间时,它会收敛到最优策略。

做得很好!接下来,让我们继续进行离策略蒙特卡洛控制。

离策略蒙特卡洛控制

在离策略方法中,我们有一组样本(轨迹),这些样本是在某种行为策略 下收集的,但我们希望利用这些经验来估计在目标策略 下的状态和动作值。这要求我们使用一种称为 重要性采样 的技巧。接下来我们将深入了解它,并描述离策略蒙特卡洛控制。

重要性采样

让我们从一个简单的游戏设置开始描述重要性采样,在这个游戏中,你投掷一个六面骰子。根据骰子上出现的点数,你会获得一个随机奖励。可能是这样的情况:如果你得到 1,那么你将获得一个均值为 10,方差为 25 的正态分布的奖励。对于所有的结果,都有类似的隐藏奖励分布,这些分布对你来说是未知的。我们用随机变量 来表示当面朝 时,你所获得的奖励。

你想估计一次投掷后的期望奖励,即

其中 是观察到面朝 的概率。

现在,假设你有两个骰子可以选择,A 和 B,它们具有不同的概率分布。你首先选择 A,并投掷骰子 次。你的观察结果如下表所示:

表 5.1 – 使用骰子 A 投掷 n 次后的观察奖励

在这里, 表示在第 次投掷后观察到的奖励,当观察到的面是 时。使用骰子 A 的估计期望奖励,,简单地给出如下公式:

现在的问题是,我们是否可以使用这些数据来估计使用骰子 B 时的期望奖励,比如 ,而无需任何新的观测?答案是,如果我们知道 和 ,那么答案是肯定的。下面是方法。

在当前的估计中,每个观测的权重为 1。重要性采样建议根据 来调整这些权重。现在,考虑观测 。如果使用骰子 B 时,我们观察到 的可能性是使用骰子 A 时的三倍,那么我们将在总和中增加该观测的权重,增加至 3。更正式地,我们执行如下操作:

这里,比例 被称为重要性采样比率。顺便说一下,上述的前一个估计,,被称为普通重要性采样。我们还可以根据新的权重对新估计进行归一化,从而得到加权重要性采样,如下所示:

现在,在这个绕道之后,让我们回到使用这些来获得脱离策略的预测。在蒙特卡罗预测的上下文中,观察骰子的某一面对应于观察一个特定的轨迹,而骰子的奖励则对应于总回报。

如果你在想 “嗯,我们并不真正知道观察特定轨迹的概率,这就是我们使用蒙特卡罗方法的原因,不是吗?” 你是对的,但我们不需要知道。原因如下。从某个 开始,在行为策略 下观察到轨迹 的概率如下:

对于目标策略,表达式是一样的,唯一的不同是 替换了 。现在,当我们计算重要性采样比率时,转移概率会相互抵消,我们最终得到如下结果(Sutton & Barto, 2018):

有了这些,我们就可以从估计行为策略下的期望值开始:

估计目标策略下的期望值:

在我们实现之前,让我们通过一些关于重要性采样的笔记来结束这一部分:

  • 为了能够使用在行为策略下获得的样本! 来估计在! 下的状态和值动作,需要满足!,如果!成立。由于我们不希望强加目标策略下可能采取的!,通常选择!作为一种软策略。

  • 在加权重要性采样中,如果分母为零,则该估计值被视为零。

  • 上述公式忽略了回报中的折扣因素,这处理起来稍微复杂一些。

  • 观察到的轨迹可以根据首次访问或每次访问规则进行切分。

  • 普通的重要性采样是无偏的,但可能具有非常高的方差。加权重要性采样则是有偏的,但通常具有较低的方差,因此在实践中更受青睐。

这有点绕远了,希望你还跟得上!如果你跟得上,我们回到编码吧!

应用于库存补充问题

在我们应用离策略蒙特卡罗方法时,我们使用加权重要性采样。行为策略选择为!-贪心策略,而目标是最大化每个状态下动作值的贪心策略。此外,我们使用增量方法更新状态和值动作估计,如下所示(Sutton & Barto,2018):

以及这个:

现在,可以按如下方式实现:

  1. 我们首先定义用于增量实现离策略蒙特卡罗的函数:

    def off_policy_mc(env, n_iter, eps, gamma):
        np.random.seed(0)
        states =  env.state_space
        actions = env.action_space
        Q = {s: {a: 0 for a in actions} for s in states}
        C = {s: {a: 0 for a in actions} for s in states}
        target_policy = {}
        behavior_policy = get_random_policy(states, 
                                            actions)
        for i in range(n_iter):
            if i % 10000 == 0:
                print("Iteration:", i)
            trajectory = get_trajectory(env, 
                                        behavior_policy)
            G = 0
            W = 1
            T = len(trajectory) - 1
            for t, sar in enumerate(reversed(trajectory)):
                s, a, r = sar
                G = r + gamma * G
                C[s][a] += W
                Q[s][a] += (W/C[s][a]) * (G - Q[s][a])
                a_best = max(Q[s].items(), 
                             key=operator.itemgetter(1))[0]
                target_policy[s] = a_best
                behavior_policy[s] = get_eps_greedy(actions, 
                                                    eps, 
                                                    a_best)
                if a != target_policy[s]:
                    break
                W = W / behavior_policy[s][a]
        target_policy = {s: target_policy[s] for s in states}
        return target_policy, Q
    
  2. 我们使用离策略蒙特卡罗(MC)方法来优化食品车库存补充策略:

    policy, Q = off_policy_mc(foodtruck, 300000, 0.05, 1)
    
  3. 最后,展示获得的策略。你会看到它是最优的!

    {('Mon', 0): 400,
     ('Tue', 0): 400,
     ('Tue', 100): 300,
     ('Tue', 200): 200,
     ('Tue', 300): 100,
     ('Wed', 0): 400,
    ...
    

耶!我们暂时完成了蒙特卡罗方法。恭喜,这部分并不简单!你应该休息一下。接下来,我们将深入探讨另一个非常重要的话题:时序差分学习。

时序差分学习

本章介绍的解决 MDP 的第一类方法是动态规划(DP):

  • 它需要完全了解环境的动态,才能找到最优解。

  • 它允许我们通过对价值函数进行一步步更新来推进解决方案。

接下来,我们介绍了蒙特卡罗(MC)方法:

  • 它们只要求能够从环境中采样,因此它们从经验中学习,而不是知道环境的动态——这是相对于动态规划(DP)的巨大优势。

  • 但是它们需要等待一个完整的剧集轨迹来更新策略。

时序差分(TD)方法在某种意义上是两全其美:它们从经验中学习,并且可以通过自举在每一步后更新策略。TD 与 DP 和 MC 的比较在表 5.2中进行了说明:

表 5.2 – 动态规划(DP)、蒙特卡罗(MC)和时序差分(TD)学习方法的比较

因此,TD 方法在强化学习中占据核心地位,你将反复遇到它们的不同形式。在本节中,你将学习如何在表格形式中实现 TD 方法。我们将在接下来的章节中介绍现代强化学习算法,这些算法使用函数逼近方法(如神经网络)来实现 TD 方法。

一步 TD 学习 – TD(0)

TD 方法可以在单次状态转移或多次状态转移后更新策略。前者被称为一步 TD 学习或 TD(0),相比之下,-步 TD 学习更复杂,实施起来更困难。我们将从一步 TD 学习开始,首先探讨预测问题。然后,我们将介绍一种基于策略的方法 SARSA,再介绍一种离策略的算法——著名的 Q-learning。

TD 预测

记得我们如何将一个策略的状态-价值函数 通过一步奖励和下一个状态的价值来表示:

当智能体在状态 下执行策略 所采取的动作时,它会观察到三个随机变量的实现:

我们已经知道在策略中观察到的 的概率,但后两个来自环境。观察到的量 基于单一样本为我们提供了 的新估计。当然,我们不希望完全丢弃现有的估计并用新估计替换它,因为过渡通常是随机的,即使采取相同的行动 ,我们也可能观察到完全不同的 和 值。TD 学习中的思想是,我们利用这个观察结果来更新现有的估计 ,使其朝着这个新估计的方向移动。步长 控制我们向新估计迈进的速度。更正式地,我们使用以下更新规则:

方括号中的项被称为TD 误差。顾名思义,它估计当前的状态-价值估计 , 与基于最新观察的真实值之间的偏差。 将完全忽略新的信号,而 将完全忽略现有的估计。再次强调,由于单次观察通常是噪声的,并且新估计本身使用了另一个错误的估计();因此,不能完全依赖新估计。为此, 的值在 和 1 之间选择,且通常更接近 。

这样,我们就可以轻松实现一个 TD 预测方法来评估给定的策略 并估计状态值。请继续跟随,使用 TD 预测在食品卡车示例中评估基本策略:

  1. 首先,我们实现如上所述的 TD 预测,如下函数所示:

    def one_step_td_prediction(env, policy, gamma, alpha, n_iter):
        np.random.seed(0)
        states = env.state_space
        v = {s: 0 for s in states}
        s = env.reset()
        for i in range(n_iter):
            a = choose_action(s, policy)
            s_next, reward, done, info = env.step(a)
            v[s] += alpha * (reward + gamma * v[s_next] - v[s])
            if done:
                s = env.reset()
            else:
                s = s_next
        return v
    

    该函数简单地通过指定的策略,在指定的迭代次数内模拟给定的环境。在每次观察后,它使用给定的 步长和折扣因子 进行一步的 TD 更新。

  2. 接下来,我们获得之前介绍的 base_policy 函数定义的基本策略:

    policy = base_policy(foodtruck.state_space)
    
  3. 然后,让我们通过 TD 预测估算 和 在 100,000 步中得到的状态值:

    one_step_td_prediction(foodtruck, policy, 1, 0.01, 100000)
    
  4. 经过四舍五入,状态值估计将如下所示:

    {('Mon', 0): 2507.0, 
     ('Tue', 0): 1956.0
    ...
    

如果你回到 DP 方法部分,查看在基本策略下真实的状态值(我们使用策略评估算法获得),你会发现 TD 估计与这些值非常一致。

太好了!我们已经成功使用 TD 预测评估了给定的策略,一切按预期工作。另一方面,就像 MC 方法一样,我们知道必须估算动作值,才能在缺乏环境动态的情况下改进策略并找到最优策略。接下来,我们将研究两种不同的方法,SARSA 和 Q-learning,它们正是为此而设计。

使用 SARSA 进行策略控制

通过对 TD(0) 做轻微的添加和修改,我们可以将其转化为一个最优控制算法。具体来说,我们将执行以下操作:

  • 确保我们始终拥有一个软策略,例如 -贪婪,以便随着时间的推移尝试在给定状态下的所有动作。

  • 估计动作值。

  • 基于动作-价值估计来改进策略。

我们将在每个步骤中使用观察值 来执行所有这些操作,因此得名 SARSA。特别地,动作值的更新如下:

现在,让我们深入了解实现过程!

  1. 我们定义了函数 sarsa,它将接受环境、折扣因子 、探索参数 和学习步长 作为参数。此外,实施常规初始化:

    def sarsa(env, gamma, eps, alpha, n_iter):
        np.random.seed(0)
        states = env.state_space
        actions = env.action_space
        Q = {s: {a: 0 for a in actions} for s in states}
        policy = get_random_policy(states, actions)
        s = env.reset()
        a = choose_action(s, policy)
    
  2. 接下来,我们实现算法循环,在该循环中,我们模拟环境的单步操作,观察 和 ,基于 和 -贪婪策略选择下一个动作 ,并更新动作-价值估计:

        for i in range(n_iter):
            if i % 100000 == 0:
                print("Iteration:", i)
            s_next, reward, done, info = env.step(a)
            a_best = max(Q[s_next].items(), 
                         key=operator.itemgetter(1))[0]
            policy[s_next] = get_eps_greedy(actions, eps, a_best)
            a_next = choose_action(s_next, policy)
            Q[s][a] += alpha * (reward 
                                + gamma * Q[s_next][a_next] 
                                - Q[s][a])
            if done:
                s = env.reset()
                a_best = max(Q[s].items(), 
                             key=operator.itemgetter(1))[0]
                policy[s] = get_eps_greedy(actions, eps, a_best)
                a = choose_action(s, policy)
            else:
                s = s_next
                a = a_next
        return policy, Q
    
  3. 然后,让我们使用一些超参数(例如 )并执行算法,进行 100 万次迭代:

    policy, Q = sarsa(foodtruck, 1, 0.1, 0.05, 1000000)
    
  4. 我们获得的 policy 如下:

    {('Mon', 0): {0: 0.02, 100: 0.02, 200: 0.02, 300: 0.02, 400: 0.92},
     ('Tue', 0): {0: 0.02, 100: 0.02, 200: 0.02, 300: 0.92, 400: 0.02},
     ('Tue', 100): {0: 0.02, 100: 0.02, 200: 0.02, 300: 0.92, 400: 0.02},
    ...
    

    注意,实施该策略后,探索性动作将被忽略,我们将简单地为每个状态始终选择概率最高的动作。

  5. 例如,状态为星期一 – 0 初始库存(通过 Q[('Mon', 0)] 访问)时的动作值如下:

    {0: 2049.95351191411,
     100: 2353.5460655683123,
     200: 2556.736260693101,
     300: 2558.210868908282,
     400: 2593.7601273913133}
    

就是这样!我们已经成功实现了针对我们的示例的 TD(0) 算法。然而,注意到,我们获得的策略是一个近似最优策略,而不是我们通过动态规划方法获得的最优策略。策略中也存在一些不一致性,例如在状态(星期二,0)和(星期二,100)时都有买 300 个肉饼的策略。未能获得最优策略的原因有很多:

  • SARSA 在极限情况下会收敛到最优解,例如当 时。在实际操作中,我们会运行该算法有限步数。尝试增加 ,你会发现策略(通常)会变得更好。

  • 学习率 是需要调整的超参数。收敛的速度依赖于这个选择。

  • 这是一个基于策略的算法。因此,动作值反映了由于 -贪婪策略而产生的探索性,这不是我们在这个示例中真正想要的。因为在训练后,执行策略时不会进行探索(因为我们只需要探索来发现每个状态的最佳动作)。我们实际使用的策略与我们为其估算动作值的策略不同。

接下来,我们转向 Q-learning,它是一种脱政策的 TD 方法。

基于 Q-learning 的脱政策控制

如上所述,我们希望将动作值估计与探索效应隔离开来,这意味着需要一种脱政策的方法。Q-learning 就是这样的一种方法,这使得它非常强大,因此也非常流行。

以下是 Q-learning 中动作值的更新方式:

注意到,代替 ,我们有了项 。这看起来可能很小,但它是关键。这意味着代理用于更新动作值的动作,,不一定是在 * 中下一个步骤时执行的动作,。相反,* 是一个最大化 * 的动作,就像我们在非训练时会使用的那样。*因此,动作值估计中不涉及探索性动作,它们与训练后实际会遵循的策略一致。

这意味着智能体在下一步所采取的行动,例如 ,不会被用于更新。相反,我们在更新中使用状态 的最大动作值,例如 。这种动作 是我们在使用这些动作值进行训练后会采用的,因此在动作值估计中不涉及探索性动作。

Q 学习的实现与 SARSA 的实现仅有微小差异。我们接下来看看 Q 学习的实际应用:

  1. 我们通过定义常见初始化的 q_learning 函数来开始:

    def q_learning(env, gamma, eps, alpha, n_iter):
        np.random.seed(0)
        states = env.state_space
        actions = env.action_space
        Q = {s: {a: 0 for a in actions} for s in states}
        policy = get_random_policy(states, actions)
        s = env.reset()
    
  2. 然后,我们实现主循环,其中智能体在 中采取的行动来自 -贪婪策略。在更新过程中,使用 的最大值:

         for i in range(n_iter):
            if i % 100000 == 0:
                print("Iteration:", i)
            a_best = max(Q[s].items(), 
                         key=operator.itemgetter(1))[0]
            policy[s] = get_eps_greedy(actions, eps, a_best)
            a = choose_action(s, policy)
            s_next, reward, done, info = env.step(a)
            Q[s][a] += alpha * (reward 
                                + gamma * max(Q[s_next].values()) 
                                - Q[s][a])
            if done:
                s = env.reset()
            else:
                s = s_next
    
  3. 在主循环结束后,我们返回去除探索性动作的策略:

        policy = {s: {max(policy[s].items(), 
                     key=operator.itemgetter(1))[0]: 1}
                     for s in states}
        return policy, Q
    
  4. 最后,我们通过选择超参数来执行算法,例如以下内容:

    policy, Q = q_learning(foodtruck, 1, 0.1, 0.01, 1000000)
    
  5. 观察返回的 policy:

    {('Mon', 0): {400: 1},
     ('Tue', 0): {400: 1},
     ('Tue', 100): {300: 1},
     ('Tue', 200): {200: 1},
     ('Tue', 300): {100: 1},
    ...
    

你会看到这个超参数集为你提供了最佳策略(或者根据随机化的不同,可能接近最佳策略)。

这就是我们对 Q 学习的讨论。接下来,让我们讨论这些方法如何扩展到 -步学习。

n 步 TD 学习

在蒙特卡洛方法中,我们在进行策略更新之前会收集完整的回合。另一方面,在 TD(0)方法中,我们在环境中的一次过渡后就会更新价值估计和策略。通过在中间路径上更新策略,例如在 -步过渡后,可能会找到一个合适的平衡点。对于 ,两步回报看起来如下:

其一般形式如下:

这种形式可以在 TD 更新中使用,以减少在自举中使用的估计值的权重,特别是在训练初期,这些估计值可能会非常不准确。我们在这里不包括实现,因为它会变得有些复杂,但仍然想提醒你,这个替代方法可以作为你工具箱中的一种选择。

有了这些,我们完成了 TD 方法!在结束本章之前,让我们更深入地了解模拟在强化学习中的重要性。

理解模拟在强化学习中的重要性

正如我们多次提到的,特别是在第一章讨论强化学习成功案例时,强化学习对数据的需求远远大于常规深度学习。这就是为什么训练一些复杂的强化学习智能体通常需要数月时间,经历数百万或数十亿次迭代。由于在物理环境中收集如此数据通常不可行,我们在训练强化学习智能体时高度依赖模拟模型。这也带来了一些挑战:

  • 许多企业没有自己的业务过程仿真模型,这使得在业务中应用强化学习技术变得具有挑战性。

  • 当存在仿真模型时,它通常过于简单,无法捕捉真实世界的动态。因此,强化学习模型可能容易过拟合仿真环境,并在部署时失败。要校准和验证一个仿真模型,使其足够反映现实,通常需要大量的时间和资源。

  • 一般来说,将一个在仿真中训练的强化学习代理部署到现实世界并不容易,因为,它们是两个不同的世界。这与机器学习中的核心原则相悖,该原则认为训练和测试应遵循相同的分布。这被称为仿真到现实(sim2real)的差距。

  • 仿真精度的提高通常伴随着速度变慢和计算资源的消耗,这对于快速实验和强化学习(RL)模型开发来说是一个真实的劣势。

  • 许多仿真模型不够通用,无法涵盖过去未遇到但未来可能遇到的场景。

  • 许多商业仿真软件可能很难与强化学习包天然支持的语言(如 Python)进行集成(由于缺乏适当的 API)。

  • 即使可以进行集成,仿真软件可能也不够灵活,无法与算法兼容。例如,它可能无法揭示环境的状态,在需要时重置环境,定义终止状态,等等。

  • 许多仿真供应商在每个许可证下允许的会话数量有限,而强化学习模型开发是最快的——你可以并行运行成千上万的仿真环境。

在本书中,我们将介绍一些克服这些挑战的技术,例如针对 sim2real 差距的领域随机化和针对没有仿真的环境的离线强化学习。然而,本节的关键内容是,通常你应该投资于你的仿真模型,以便从强化学习中获得最佳效果。特别是,你的仿真模型应该快速、准确,并且能够扩展到多个会话。

到此为止,我们结束了这一章。干得好!这标志着我们在本书旅程中的一个里程碑。我们已经走了很长一段路,并建立了强化学习解决方案方法的坚实基础!接下来,让我们总结一下我们所学的内容,并看看下一章将带来什么。

总结

在本章中,我们介绍了解决 MDP 的三种重要方法:动态规划(DP)、蒙特卡洛方法和时序差分学习(TD)。我们已经看到,虽然动态规划提供了 MDP 的精确解,但它依赖于对环境的了解。另一方面,蒙特卡洛和 TD 学习方法则是通过探索环境并从经验中学习。特别地,TD 学习方法可以仅凭环境中的单步转移进行学习。在此过程中,我们还讨论了策略方法,它估计行为策略的价值函数,以及目标策略的离策略方法。最后,我们还讨论了模拟器在强化学习实验中的重要性,以及在使用模拟器时需要注意的事项。

接下来,我们将把旅程推向下一个阶段,深入探讨深度强化学习,这将使我们能够使用强化学习解决一些实际问题。特别地,在下一章中,我们将详细介绍深度 Q 学习。

到时候见!

习题

  1. 改变这些值,观察修改后的问题中最优策略的变化。

  2. 在价值迭代算法中的策略改进步骤后,添加一个策略评估步骤。你可以设置一个迭代次数,用于执行评估,然后再返回策略改进。使用policy_evaluation函数,并选择一个max_iter值。同时,注意如何跟踪状态值的变化。

参考文献

第二部分:深度强化学习

这一部分深入探讨了最先进的强化学习算法,并让你对每个算法的优缺点有一个扎实的理解。

本部分包含以下章节:

  • 第六章,大规模深度 Q 学习

  • 第七章,基于策略的方法

  • 第八章,基于模型的方法

  • 第九章,多智能体强化学习

第六章:第六章:大规模深度 Q 学习

在上一章中,我们介绍了用 动态规划(DP)方法解决 马尔可夫决策过程(MDP),并提到它们存在两个重要的局限性:首先,DP 假设我们完全了解环境的奖励和转移动态;其次,DP 使用表格化的状态和动作表示,而在许多实际应用中,由于可能的状态-动作组合太多,这种方式无法扩展。我们通过引入 蒙特卡洛(MC)和 时间差分(TD)方法解决了前者问题,这些方法通过与环境的交互(通常是在模拟环境中)来学习,而无需了解环境的动态。另一方面,后者问题尚未解决,这正是深度学习发挥作用的地方。深度强化学习(深度 RL 或 DRL)是利用神经网络的表示能力来学习适应各种情况的策略。

尽管听起来很棒,但在 强化学习(RL)的背景下,让函数逼近器(function approximators)有效工作是相当棘手的,因为我们在表格化 Q 学习中所拥有的许多理论保证在深度 Q 学习中都丧失了。因此,深度 Q 学习的故事在很大程度上是关于使神经网络在 RL 中有效工作的技巧。本章将带你了解函数逼近器为何失败以及如何解决这些失败。

一旦我们让神经网络与 RL 配合好,就会面临另一个挑战:深度 RL 对数据的巨大需求,这一需求甚至比监督学习更为严重。这要求我们开发高度可扩展的深度 RL 算法,本章将使用现代 Ray 库实现深度 Q 学习的可扩展性。最后,我们将向你介绍 RLlib,这是一个基于 Ray 的生产级 RL 库。因此,本章的重点将是加深你对各种深度 Q 学习方法之间联系的理解,什么有效,为什么有效;而不是在 Python 中实现每一个算法,你将使用 Ray 和 RLlib 来构建和使用可扩展的方法。

这将是一次激动人心的旅程,让我们一起深入探索!具体来说,本章将涵盖以下内容:

  • 从表格化 Q 学习到深度 Q 学习

  • 深度 Q 网络

  • DQN 的扩展 – Rainbow

  • 分布式深度 Q 学习

  • 使用 Ray 实现可扩展的深度 Q 学习算法

  • 使用 RLlib 进行生产级深度 RL

从表格化 Q 学习到深度 Q 学习

在我们在第五章《解决强化学习问题》中讨论表格 Q-learning 方法时,应该已经很明显,我们无法将这些方法真正扩展到大多数实际场景中。想象一下一个使用图像作为输入的强化学习问题。一个包含三个 8 位色彩通道的图像将会导致种可能的图像,这是你的计算器无法计算的数字。正因为如此,我们需要使用函数逼近器来表示值函数。鉴于它们在监督学习和无监督学习中的成功,神经网络/深度学习成为了这里的明显选择。另一方面,正如我们在引言中提到的,当函数逼近器介入时,表格 Q-learning 的理论保证就不再成立。本节介绍了两种深度 Q-learning 算法——神经拟合 Q 迭代(NFQ)和在线 Q-learning,并讨论了它们的一些不足之处。通过这些内容,我们为接下来讨论现代深度 Q-learning 方法奠定了基础。

神经拟合 Q 迭代

NFQ 算法旨在拟合一个神经网络,表示行动值,即 Q 函数,将目标 Q 值与从环境中采样并通过先前可用的 Q 值自举的值进行匹配(Riedmiller,2015)。让我们首先了解 NFQ 如何工作,然后讨论一些实际考虑事项以及 NFQ 的局限性。

算法

回想一下在表格 Q-learning 中,行动值是从环境中收集的样本中学习的,这些样本是元组,通过反复应用以下更新规则:

这里,表示最优策略的行动值估计,(注意我们开始使用大写字母,这是深度强化学习文献中的惯例)。目标是通过将采样的贝尔曼最优性操作符应用到样本上,更新现有估计值,使其接近一个“目标”值。NFQ 有类似的逻辑,以下是不同之处:

  • Q 值由一个由参数化的神经网络表示,而不是一个表格,我们用表示它。

  • 与通过每个样本逐步更新 Q 值不同,NFQ 一次性从环境中收集一批样本,并将神经网络拟合到目标值上。

  • 有多轮计算目标值并拟合参数的过程,以便能够通过最新的 Q 函数获得新的目标值。

在整体描述之后,下面是 NFQ 算法的详细步骤:

  1. 初始化和策略。

  2. 使用 策略收集一组 样本,。

  3. 将采样的贝尔曼最优算子应用于所有样本,以获得目标值,,,但如果 是终止状态,则设置 。

  4. 通过最小化 和目标值之间的差距,获得 。更正式地说,

    ,其中 是一个损失函数,例如平方误差,。

  5. 根据新的 值更新 。

对拟合 Q 迭代可以进行许多改进,但这不是我们在此讨论的重点。相反,接下来我们将提到在实现该算法时需要考虑的几个关键要点。

拟合 Q 迭代的实际考虑

为了使拟合 Q 迭代在实践中有效,有几个重要的注意事项,我们在此列出:

  • 策略应该是软策略,在样本收集过程中允许足够的探索不同的状态-动作对,例如 -贪心策略。因此,探索率是一个超参数。

  • 设置 太大可能会导致问题,因为某些状态只有在坚持使用良好的策略(它开始改善后)若干步之后才能到达。一个例子是在视频游戏中,只有在成功完成早期步骤后才能到达后续关卡,而高度随机的策略不太可能做到这一点。

  • 当目标值被获取时,这些值很可能使用了不准确的动作值估计,因为我们使用不准确的 值进行自举。因此,我们需要重复 步骤 2 和 步骤 3, 次,期望在下一轮中获得更准确的目标值。这为我们提供了另一个超参数,。

  • 我们最初用于收集样本的策略可能不足以将智能体引导到状态空间的某些部分,这类似于高 的情况。因此,通常在更新策略后收集更多的样本,将它们添加到样本集,并重复该过程是个好主意。

  • 请注意,这是一个离策略算法,因此样本可以来自所选策略,也可以来自其他地方,比如环境中已部署的非强化学习控制器。

即使有这些改进,在实践中,使用 NFQ 求解 MDP 可能会很困难。接下来我们将在下一节中探讨原因。

拟合 Q 迭代的挑战

尽管拟合 Q 迭代有一些成功的应用,但它存在几个主要缺点:

  • 它需要每次重复使用手头的目标批次来从头学习 ,换句话说,步骤 3 涉及到一个 运算符,而不是像梯度下降中那样逐步更新 以适应新数据。在一些应用中,强化学习模型需要在数十亿个样本上训练。一次又一次地在数十亿个样本上训练神经网络,并且使用更新后的目标值进行训练是不切实际的。

  • SARSA 和类似 Q-learning 的方法在表格情形下有收敛性保证。然而,当使用函数逼近时,这些理论保证会丧失。

  • 使用带有函数逼近的 Q-learning,这是一个使用自举的脱策略方法,特别不稳定,这被称为 致命三重奏。

在深入探讨如何解决这些问题之前,我们先来详细分析后两个点。现在,这将涉及一些理论,如果你理解这些理论,将帮助你对深度强化学习中的挑战有更深入的直觉。另一方面,如果你不想了解理论,可以跳过直接进入 在线 Q-learning 部分。

使用函数逼近器的收敛性问题

为了说明为什么当使用函数逼近器时,Q-learning 的收敛性保证会丧失,我们先回顾一下为什么表格 Q-learning 一开始是收敛的:

  • 的定义是,如果我们在状态 下选择动作 ,仅在一开始偏离策略 ,然后在剩余时间内遵循该策略,期望的折扣回报:

  • Bellman 最优性算子,用符号 表示,接收一个动作-价值函数 、 和 ,并映射到以下量:

请注意期望值中的 的使用,而不是遵循其他策略 。 是一个算子,一个函数,不同于动作-价值函数的定义。

  • 如果且仅当动作-价值函数是最优时, 会将 映射回 ,对于所有 和 的实例:

更正式地说,算子 的唯一固定点是最优的 ,用 表示。这就是 Bellman 最优性方程的内容。

  • 是一个收缩,这意味着每次我们将其应用于任何两个不同的动作值函数,例如 和 向量,这些向量的条目是所有 和 实例的某些动作值估计,它们会变得越来越接近。这是相对于 范数,即 和 以及 的元组之间绝对差值的最大值:,这里是 。

    如果我们选择其中一个动作值向量作为最优向量,我们将得到以下关系:

这意味着我们可以通过从某个任意的 值开始,反复应用 Bellman 操作符,并更新 值,逐渐接近 。

  • 有了这些, 就变成了一个更新规则,通过任意的 值来获得 ,这与值迭代方法的工作原理非常相似。

现在请注意,将神经网络拟合到一批采样的目标值并不保证能使动作值估计接近每个 元组的最优值,因为拟合操作并不关心个别误差——而且它不一定有能力做到这一点,因为它假设由于参数化,动作值函数具有某种结构——但它最小化了平均误差。因此,我们失去了 Bellman 操作在 范数下的收缩性质。相反,NFQ 将 拟合到目标值,以 范数为基础,但它并不具备相同的收敛性属性。

信息

如果你想更详细和直观地了解为什么值函数理论在函数逼近中失败,可以查看 Sergey Levine 教授的讲座:youtu.be/doR5bMe-Wic?t=3957,这也启发了本节内容。整个课程可以在线访问,是你深入了解强化学习理论的一个很好的资源。

现在让我们来看看著名的致命三元组,它提供了一个新的视角,说明为什么在诸如 Q-learning 等脱机策略算法中使用带有自举的函数逼近器会有问题。

致命三元组

Sutton 和 Barto 提出了“致命三元组”这一术语,表示如果一个强化学习算法涉及以下所有操作,它很可能会发散:

  • 函数逼近器

  • 自举

  • 脱机策略样本收集

他们提供了这个简单的例子来解释问题。考虑一个由两个状态(左和右)组成的 MDP 的一部分。左边只有一个动作,就是右移,奖励为 0。左状态的观察值为 1,右状态的观察值为 2。一个简单的线性函数近似器用于表示动作值,只有一个参数,。这是在下图中表示的:

图 6.1 – 一段发散的 MDP 片段(来源:Sutton & Barto,2018)

图 6.1 – 一段发散的 MDP 片段(来源:Sutton & Barto,2018)

现在,想象一下一个行为策略只从左边的状态中采样。同时,假设初始值为 为 10 和 。然后,TD 误差计算如下:

现在,如果使用唯一的现有数据(即从左到右的转换,假设使用 ),更新线性函数近似,那么新的 值变为 。请注意,这也更新了右状态的动作值估计。在下一轮中,行为策略再次只从左侧采样,而新的 TD 误差变为如下:

它甚至大于第一个 TD 误差!你可以看到它最终如何发散。问题发生的原因如下:

  • 这是一种离策略方法,行为策略恰好只访问状态-动作空间的一个部分。

  • 使用一个函数近似器,其参数基于我们有限的样本进行更新,但未访问的状态动作的价值估计也会随之更新。

  • 我们进行引导式估计,并使用我们从未实际访问过的状态动作所得到的错误价值估计来计算目标值。

这个简单的例子说明了当这三种组件结合在一起时,它如何可能会破坏强化学习方法。有关其他例子和更详细的解释,我们建议您阅读 Sutton & Barto,2018 中的相关章节。

由于我们只讨论了挑战,现在我们终于开始解决它们。记住,NFQ 要求我们完全将整个神经网络拟合到现有的目标值,并且我们如何寻找更渐进的更新。这就是在线 Q 学习给我们的结果,我们将在下一节介绍它。另一方面,在线 Q 学习也引入了其他挑战,我们将在下一节使用深度 Q 网络(DQNs)来解决这些问题。

在线 Q 学习

如前所述,拟合 Q 迭代法的一个缺点是每次处理一批样本时都需要计算 值,这在问题复杂且需要大量数据训练时显得不切实际。在线 Q 学习则走到了另一个极端:在观察到每一个样本后,它通过梯度更新 ,即 。接下来,我们将深入探讨在线 Q 学习算法的细节。

算法

在线 Q 学习算法的工作方式如下:

  1. 初始化 和一个策略,,然后初始化环境并观察 。

    对于 到 ,继续进行以下步骤。

  2. 在给定状态 的情况下,执行某些操作 ,使用策略 ,然后观察 和 ,它们构成了 元组。

  3. 获取目标值 ,但如果 是终止状态,则设置 。

  4. 执行梯度步骤以更新 ,其中 是步长。

  5. 更新策略以 为新 值。

    结束

如你所见,与 NFQ 的关键区别是,每当从环境中抽取一个 元组时,才更新神经网络参数。以下是关于在线 Q 学习的额外考虑:

  • 类似于 NFQ 算法,我们需要一个不断探索状态-动作空间的策略。同样,这可以通过使用 -贪婪策略或其他软策略来实现。

  • 与拟合 Q 迭代法相似,样本可能来自与 Q 网络所建议的策略无关的政策,因为这是一种离策略方法。

除了这些之外,在线 Q 学习方法还可以进行许多其他改进。我们将暂时聚焦于 DQN,它是 Q 学习的突破性改进,而不是讨论一些相对次要的在线 Q 学习调整。但在此之前,我们先来看一下为何目前形式的在线 Q 学习难以训练。

在线 Q 学习的挑战

在线 Q 学习算法面临以下问题:

  • 梯度估计有噪声:类似于机器学习中的其他梯度下降方法,在线 Q 学习的目标是通过样本估计梯度。另一方面,它在执行时仅使用一个样本,这导致了噪声较大的估计,使得优化损失函数变得困难。理想情况下,我们应使用一个包含多个样本的小批量来估计梯度。

  • 梯度步伐并不是真正的梯度下降:这是因为 包含了 ,我们将其视为常量,尽管它并非如此。 本身依赖于 ,然而我们忽略了这一事实,因为我们没有对 求其导数。

  • 目标值在每次梯度步骤后更新,这变成了一个网络试图从中学习的动态目标:这与监督学习不同,在监督学习中,标签(例如图像标签)不会根据模型预测的结果而变化,这使得学习变得非常困难。

  • 样本并非独立同分布(i.i.d.):事实上,它们通常高度相关,因为马尔可夫决策过程(MDP)是一个顺序决策问题,接下来的观察高度依赖于我们之前采取的行动。这是与经典梯度下降的另一种偏离,打破了其收敛性。

由于所有这些挑战,以及我们在 NFQ 部分中提到的致命三联体,在线 Q 学习算法并不是解决复杂强化学习问题的可行方法。随着 DQNs 的革命性工作,这改变了我们之前提到的后两个挑战。事实上,正是通过 DQN,我们才开始讨论深度强化学习。所以,不再赘述,让我们直接进入讨论 DQNs。

深度 Q 网络

DQN 是 Mnih 等人(2015)的一项开创性工作,它使得深度强化学习成为解决复杂顺序控制问题的可行方法。作者证明了一个单一的 DQN 架构能够在许多雅达利游戏中达到超人水平的表现,且不需要任何特征工程,这为人工智能的进展带来了巨大的兴奋。让我们看看是什么使得 DQN 比我们之前提到的算法更为有效。

DQN 中的关键概念

DQN 通过使用经验重放和目标网络这两个重要概念,修改了在线 Q 学习,从而极大地稳定了学习过程。接下来我们将描述这些概念。

经验重放

如前所述,单纯地使用从环境中顺序采样的经验会导致梯度步骤高度相关。而 DQN 则将这些经验元组 存储在重放缓冲区(记忆)中,这一概念早在 1993 年就由 Lin 提出(1993 年)。在学习过程中,样本是从该缓冲区中均匀随机抽取的,这消除了用于训练神经网络的样本之间的相关性,提供了类似独立同分布(i.i.d.)的样本。

使用经验重放而非在线 Q 学习的另一个好处是,经验被重用而不是丢弃,这减少了与环境交互所需的次数——鉴于强化学习中需要大量数据,这是一项重要的优势。

关于经验回放的一个有趣注解是,有证据表明,动物的大脑中也有类似的过程发生。动物似乎会在其海马体中回放过去的经验,这有助于它们的学习(McClelland,1995)。

目标网络

使用引导法(bootstrapping)与函数逼近的另一个问题是它会创建一个不断变化的目标供学习。这使得本已具有挑战性的任务,如从噪声样本中训练神经网络,变成了一项注定失败的任务。作者提出的一个关键见解是创建一个神经网络的副本,该副本仅用于生成用于采样 Bellman 更新的 Q 值估计。即,样本的目标值 如下所示:

这里, 是目标网络的参数,它会每隔 个时间步更新一次,通过设置 来更新。

创建更新目标网络的滞后可能会使其动作价值估计与原始网络相比略显过时。另一方面,作为回报,目标值变得稳定,原始网络可以进行训练。

在给出完整的 DQN 算法之前,我们先来讨论一下它所使用的损失函数。

损失函数

引入经验回放和目标网络后,DQN 最小化以下损失函数:

这里, 是重放缓冲区,从中均匀随机抽取一个最小批量的 元组以更新神经网络。

现在,终于到了给出完整算法的时候。

DQN 算法

DQN 算法包含以下步骤:

  1. 初始化 和一个具有固定容量 的重放缓冲区 。将目标网络参数设置为 。

  2. 设置策略 ,使其对 采用 -贪心策略。

  3. 给定状态 和策略 ,采取动作 ,并观察 和 。将过渡 添加到重放缓冲区 。如果 ,则从缓冲区中弹出最旧的过渡。

  4. 如果 ,则均匀采样一个来自 的随机最小批量的 过渡,否则返回 第 2 步。

  5. 获取目标值,,,除非 是终止状态,此时设置 。

  6. 进行一次梯度更新步骤来更新 ,它是 。这里,

  7. 每次步骤,更新目标网络参数,。

  8. 返回到步骤 1。

DQN 算法可以通过图 6.2中的示意图来说明:

图 6.2 – DQN 算法概述(来源:Nair 等人,2015)

图 6.2 – DQN 算法概述(来源:Nair 等人,2015)

在 DQN 开创性工作之后,许多论文提出了多种扩展方法来改进它们。Hessel 等人(2018)将其中一些最重要的扩展结合起来,命名为 Rainbow,接下来我们将讨论这些扩展。

DQN 的扩展 – Rainbow

Rainbow 的改进相较于原始的 DQN 提供了显著的性能提升,并且已成为大多数 Q 学习实现中的标准方法。在本节中,我们将讨论这些改进的内容,它们如何提供帮助,以及它们相对的重要性。最后,我们将讨论 DQN 及这些扩展如何共同克服致命三重困境。

这些扩展

Rainbow 算法包含了对 DQN 的六种扩展。这些扩展包括双重 Q 学习、优先重放、对抗网络、多步学习、分布式强化学习和噪声网络。我们将从双重 Q 学习开始描述这些扩展。

双重 Q 学习

Q 学习中的一个著名问题是,我们在学习过程中获得的 Q 值估计值通常高于真实 Q 值,这是由于最大化操作所致,。这种现象被称为最大化偏差,其产生的原因在于我们在对 Q 值的噪声观察结果上进行最大化操作。结果,我们最终估算的不是真实值的最大值,而是可能观察到的最大值。

为了简单地说明这种现象,考虑图 6.3中的示例:

图 6.3 – 最大化偏差的两个示例

图 6.3 – 最大化偏差的两个示例

图 6.3 (a) 和 6.3 (b) 展示了在给定状态下,对于可用动作的各种 Q 值估计的概率分布,,其中竖线表示真实的动作值。在(a)中,有三个可用的动作。在某些轮次的样本收集中,偶然地我们获得的估计值恰好是。不仅最优动作被错误地预测为,而且其动作值被高估了。在(b)中,有六个可用的动作,它们具有相同的 Q 值估计的概率分布。尽管它们的真实动作值相同,但当我们随机抽样时,它们之间就会出现顺序,仅仅是偶然的结果。而且,由于我们对这些噪声观察结果进行最大化操作,因此很有可能最终估算值会高于真实值,Q 值又一次被高估。

双 Q 学习通过使用两个独立的动作价值函数, 和 ,将寻找最大化动作和为其获取动作价值估计的过程解耦,从而提出了对最大化偏差的解决方案。更正式地,我们通过其中一个函数来寻找最大化动作:

然后,我们使用另一个函数来获得动作价值,即 。

在表格 Q 学习中,这需要额外的努力来维持两个动作价值函数。然后, 和 在每一步中会随机交换。另一方面,DQN 已经提出了维护一个目标网络,使用 参数来为引导提供动作价值估计。因此,我们在 DQN 上实现了双 Q 学习,以获得最大化动作的动作价值估计,具体如下:

然后,状态-动作对 的对应损失函数变为以下形式:

就是这样!这就是双 Q 学习在 DQN 中的工作原理。现在,让我们深入了解下一个改进:优先重放。

优先经验重放

正如我们提到的,DQN 算法建议从重放缓冲区中均匀随机地采样经验。另一方面,自然地可以预期某些经验比其他经验更“有趣”,因为代理从中学到的东西更多。这尤其在困难探索问题中表现突出,尤其是那些稀疏奖励的问题,在这些问题中有很多“不感兴趣”的“失败”案例,只有少数几个带有非零奖励的“成功”案例。Schaul 等人(2015)建议使用 TD 误差来衡量某个经验对代理来说有多么“有趣”或“令人惊讶”。从重放缓冲区中采样某个特定经验的概率将与 TD 误差成正比。即,在时间 , 遇到的经验的采样概率与 TD 误差之间有以下关系:

这里, 是一个超参数,用来控制分布的形状。注意,对于 ,这会在经验上产生均匀分布,而更大的 值则会把更多的权重放在具有大 TD 误差的经验上。

对战网络

在 RL 问题中,一个常见的情况是,在某些状态下,代理采取的动作对环境几乎没有影响。举个例子,考虑以下情况:

  • 在网格世界中移动的机器人应该避免进入“陷阱”状态,因为从该状态中机器人无法通过自己的动作逃脱。

  • 相反,环境会以某个低概率将机器人随机从这个状态转移出去。

  • 在此状态下,机器人失去了一些奖励积分。

在这种情况下,算法需要估计陷阱状态的价值,以便它知道应该避免该状态。另一方面,尝试估计单个行动值是没有意义的,因为它只是追逐噪声。事实证明,这会损害 DQN 的有效性。

对偶网络通过一种架构提出了解决这个问题的方法,该架构同时估计给定状态下的状态值和行动优势,并在并行流中进行处理。一个给定状态下的行动的优势值,顾名思义,是选择该行动所带来的额外预期累计奖励,而不是所使用的策略所建议的行动!。它的正式定义如下:

因此,选择具有最高优势的行动等同于选择具有最高 Q 值的行动。

通过从状态值和行动优势的显式表示中获取 Q 值,如图 6.4所示,我们使得网络能够很好地表示状态值,而不需要准确估计给定状态下的每个行动值:

图 6.4 – (a) 常规 DQN 和(b) 对偶 DQN(来源:Wang 等,2016)

在这一点上,你可能会认为,行动-价值估计只是通过我们之前给出的公式,在这个架构中得到的。事实证明,这种简单的实现并不有效。这是因为仅凭这个架构并不能强制网络学习相应分支中的状态和行动值,因为它们是通过它们的总和间接监督的。例如,如果你从状态值估计中减去 100,并且将 100 加到所有的优势估计中,那么总和并不会改变。为了克服这个“可识别性”问题,我们需要记住这一点:在 Q 学习中,策略是选择具有最高 Q 值的行动。我们用!表示这个最佳行动。然后,我们得到以下内容:

这导致了!。为了强制执行这一点,一种获得行动-价值估计的方法是使用以下方程:

这里, 和 分别表示常见的编码器、状态值和优势流;而!。另一方面,作者使用以下替代方案,这导致了更稳定的训练:

通过这种架构,作者在当时的 Atari 基准测试中获得了最先进的结果,证明了该方法的价值。

接下来,让我们来看看 DQN 的另一个重要改进:多步学习。

多步学习

在上一章中,我们提到可以通过在从环境获得的估计中使用多步折扣奖励来获得更准确的状态-动作对的目标值。在这种情况下,回溯中使用的 Q 值估计会受到更大的折扣,从而减小这些估计不准确的影响。相反,目标值将更多地来自采样的奖励。更正式地,多个步骤设置中的时间差分(TD)误差变为以下形式:

可以看到,随着的增大,项的影响逐渐减小,因为。

下一个扩展是分布式强化学习(distributional RL),这是基于值学习中最重要的思想之一。

分布式强化学习

在传统的 Q 学习设置中,动作值函数估计在状态下执行动作时的预期折扣回报,然后遵循某个目标策略。Bellemare 等人(2017)提出的分布式强化学习模型则学习状态值的离散支持上的概率质量函数。这是一个包含个原子的向量,其中,。然后,神经网络架构被修改为估计每个原子的,即。当使用分布式强化学习时,TD 误差可以通过当前和目标分布之间的Kullback-Leibler(KL)散度来计算。

这里举个例子,假设环境中任何状态的状态值范围可以从到。我们可以将这个范围离散化为 11 个原子,从而得到。然后,值网络会估计,对于给定的值,其值为 0、10、20 等的概率。事实证明,这种对值函数的细粒度表示在深度 Q 学习中带来了显著的性能提升。当然,这里的额外复杂性在于,和是需要调优的附加超参数。

最后,我们将介绍最后一个扩展——噪声网络(noisy nets)。

噪声网络

正常 Q 学习中的探索由控制,该值在状态空间中是固定的。另一方面,某些状态可能需要比其他状态更高的探索。噪声网络将噪声引入动作值函数的线性层,噪声的程度在训练过程中学习。更正式地,噪声网络定位线性层:

然后将其替换为以下形式:

在这里, 和 是学习到的参数,而 和 是具有固定统计数据的随机变量, 表示逐元素乘积。通过这种设置,探索率成为学习过程的一部分,尤其在困难探索问题中,这一点尤为重要(Fortunato 等,2017 年)。

这就结束了关于扩展的讨论。接下来,我们将转向讨论这些扩展结合后的结果。

集成智能体的性能

Rainbow 论文的贡献在于将所有前述的改进整合成一个单一的智能体。因此,它在当时的著名 Atari 2600 基准测试中取得了最先进的结果,展示了将这些改进整合在一起的重要性。当然,一个自然的问题是,每一个单独的改进是否对结果产生了显著影响。作者通过一些消融实验来展示结果,我们将在接下来的部分讨论这一点。

如何选择使用哪些扩展 —— Rainbow 的消融实验

Rainbow 论文得出了关于各个扩展重要性的以下结论:

  • 事实证明,优先回放和多步学习是对结果贡献最大的扩展。将这些扩展从 Rainbow 架构中移除导致性能的最大下降,表明它们的重要性。

  • 分布式 DQN 被证明是下一个重要的扩展,尤其在训练的后期阶段更为显著。

  • 移除 Rainbow 智能体中的噪声网络导致性能下降,尽管其影响不如之前提到的其他扩展显著。

  • 移除对抗性架构和双重 Q 学习对性能没有显著影响。

当然,这些扩展的效果取决于具体问题,而它们的选择变成了一个超参数。然而,这些结果表明,优先回放、多步学习和分布式 DQN 是训练强化学习智能体时需要尝试的重要扩展。

在我们结束本节内容之前,让我们回顾一下关于致命三重奏的讨论,并尝试理解为什么随着这些改进的出现,它变得不再是一个大问题。

致命三重奏发生了什么?

致命三重奏假设当离策略算法与函数逼近器和自举法结合时,训练可能会很容易发散。另一方面,前述的深度 Q 学习工作却展现了巨大的成功。那么,如果致命三重奏的理论是准确的,我们如何能够实现如此好的结果呢?

Hasselt 等人研究了这个问题,并支持以下假设:

  • 在将 Q 学习与传统的深度强化学习函数空间结合时,无界发散并不常见。因此,尽管可能发生发散,但并不意味着一定会发生。作者们提出的结果表明,这其实并不是一个重大问题。

  • 在独立网络上进行自举时,发散较少。DQN 论文中引入的目标网络有助于减少发散。

  • 在纠正过度估计偏差时,发散较少,这意味着双重 DQN 在缓解发散问题方面发挥了作用。

  • 更长的多步回报将更不容易发散,因为它减少了自举影响。

  • 更大、更灵活的网络由于其表征能力接近表格化表示,而不像容量较小的函数逼近器那样容易发散。

  • 更强的更新优先级(高 )将更容易发散,这不好。但随后,可以通过重要性采样来纠正更新量,从而帮助防止发散。

这些为我们提供了关于深度 Q 学习情况为何不像最初看起来那么糟糕的重要见解。这也从过去几年报告的非常令人兴奋的结果中得到了体现,深度 Q 学习已成为一种非常有前景的强化学习问题解决方案。

这标志着我们关于深度 Q 学习理论的讨论结束。接下来,我们将转向深度强化学习中的一个非常重要的维度——其可扩展的实现。

分布式深度 Q 学习

深度学习模型以对数据的渴求而闻名。对于强化学习而言,这种对数据的需求更为强烈,因此需要在训练强化学习模型时使用并行化。原始的 DQN 模型是一个单线程过程。尽管取得了巨大的成功,但它的可扩展性有限。在本节中,我们将介绍将深度 Q 学习并行化到多个(可能是成千上万个)进程的方法。

分布式 Q 学习背后的关键洞察是其脱离策略的特性,这实际上将训练与经验生成解耦。换句话说,生成经验的特定进程/策略对训练过程并不重要(尽管这一说法有一些警告)。结合使用重放缓冲区的思想,这使我们能够并行化经验生成,并将数据存储在中央或分布式重放缓冲区中。此外,我们还可以并行化从这些缓冲区中采样数据的方式,并更新行动-价值函数。

让我们深入探讨分布式深度 Q 学习的细节。

分布式深度 Q 学习架构的组件

在本节中,我们将描述分布式深度 Q 学习架构的主要组成部分,然后我们将深入探讨具体实现,遵循 Nair 等人(2015)提出的结构。

演员

演员是与环境副本交互的进程,它们根据给定的策略在所处状态下采取行动,并观察奖励和下一个状态。例如,如果任务是学习如何下棋,每个演员都会进行自己的棋局并收集经验。演员由参数服务器提供 Q 网络副本,并提供一个探索参数,帮助它们选择行动。

经验重放记忆(缓冲区)

当演员收集经验元组时,它们会将其存储在重放缓冲区中。根据实现的不同,可能会有一个全局重放缓冲区,或多个本地重放缓冲区,每个演员可能有一个。当重放缓冲区是全局的时,数据仍然可以以分布式的方式存储。

学习者

一个学习者的工作是计算将更新 Q 网络的梯度,并将其传递到参数服务器。为此,学习者携带 Q 网络的副本,从重放记忆中采样一个小批量的经验,计算损失和梯度,然后将这些信息传回参数服务器。

参数服务器

参数服务器是存储 Q 网络主副本的地方,并随着学习的进行更新。所有进程会定期从这个参数服务器同步它们的 Q 网络版本。根据实现的不同,参数服务器可能包括多个分片,以便存储大量数据并减少每个分片的通信负载。

在介绍这个通用结构后,我们来详细了解 Gorila 的实现——这是最早的分布式深度 Q 学习架构之一。

Gorila – 一般强化学习架构

Gorila 架构引入了一个通用框架,通过我们之前描述的组件来并行化深度 Q 学习。这一架构的具体版本,由作者实现,将演员、学习者和本地重放缓冲区组合在一起进行学习。然后,你可以创建多个包来进行分布式学习。该架构在下图中有所描述:

图 6.5 – Gorila 架构

图 6.5 – Gorila 架构

请注意,随着 Rainbow 的改进,具体流程会有所变化。

分布式深度 Q 学习算法的详细步骤如下,包含在一个包中:

  1. 初始化一个重放缓冲区,,具有固定的容量,。初始化参数服务器并提供一些。同步行动值函数和目标网络的参数,和。

    对于 到,继续执行以下步骤。

  2. 将环境重置为初始状态,。同步。

    对于到,继续执行以下步骤。

  3. 执行一个动作,,根据给定的-贪心策略,使用和;观察和。将经验存储在回放缓冲区中,。

  4. 同步;从中随机采样一个小批次并计算目标值,。

  5. 计算损失;计算梯度并将其发送到参数服务器。

  6. 每个梯度更新在参数服务器中;同步。

  7. 结束。

伪代码中的一些细节被省略了,例如如何计算目标值。原始的 Gorila 论文实现了一个普通的 DQN,而没有 Rainbow 的改进。然而,你可以修改它来使用,例如,-步学习。算法的细节需要相应地填写。

Gorila 架构的一个缺点是它涉及大量在参数服务器、演员和学习者之间传递参数。根据网络的大小,这将意味着一个显著的通信负载。接下来,我们将探讨 Ape-X 架构如何改进 Gorila。

Ape-X — 分布式优先经验回放

Horgan 等人(2018)介绍了 Ape-X DQN 架构,它在 DQN、Rainbow 和 Gorila 上取得了一些显著的改进。实际上,Ape-X 架构是一个通用框架,可以应用于除 DQN 之外的其他学习算法。

Ape-X 的关键贡献

以下是 Ape-X 如何分配强化学习训练的关键点:

  • 与 Gorila 类似,每个演员从其自己的环境实例中收集经验。

  • 与 Gorila 不同,Ape-X 有一个单独的回放缓冲区,所有的经验都在其中收集。

  • 与 Gorila 不同,Ape-X 有一个单独的学习者,它从回放缓冲区中采样并更新中央 Q 和目标网络。

  • Ape-X 架构完全解耦了学习者和演员,它们按照自己的节奏运行。

  • 与常规的优先经验回放不同,演员在将经验元组添加到回放缓冲区之前计算初始优先级,而不是将它们设置为最大值。

  • Ape-X DQN 在其论文中适应了双重 Q 学习和多步学习的改进,尽管其他 Rainbow 的改进可以集成到架构中。

  • 每个演员被分配不同的探索率,在范围内,其中具有低值的演员利用已学到的环境信息,而具有高值的演员则增加收集经验的多样性。

Ape-X DQN 架构在下图中描述:

图 6.6 – DQN 的 Ape-X 架构

图 6.6 – DQN 的 Ape-X 架构

现在,让我们深入了解演员和学习者算法的细节。

演员算法

以下是演员的算法:

  1. 初始化 、 和 。

    对于 到 ,继续以下步骤。

  2. 采取行动 ,该行动来自 ,并观察 。

  3. 将 经验添加到本地缓冲区。

    如果本地缓冲区中的元组数超过阈值,,则继续以下步骤。

  4. 从本地缓冲区获取 ,一个多步转移的批次。

  5. 计算 以得到 ,并为经验设置初始优先级。

  6. 将 和 发送到中央重放缓冲区。

  7. 如果结束

  8. 每经过 步骤从学习者同步本地网络参数,

  9. 结束

对于前述算法有一点澄清:不要将本地缓冲区与重放缓冲区混淆。本地缓冲区只是临时存储,用于积累经验并在发送到重放缓冲区之前处理,而学习者并不直接与本地缓冲区交互。此外,向重放缓冲区发送数据的过程在后台运行,并不会阻塞与环境交互的过程。

现在,让我们来看一下学习者算法。

学习者算法

以下是学习者的工作方式:

  1. 初始化 Q 和目标网络,

    对于 到 ,继续以下步骤。

  2. 从经验中采样一个批次,,其中 有助于唯一标识所采样的经验。

  3. 使用 、 和 计算梯度 ;使用这些梯度更新网络参数到 。

  4. 计算新的优先级 ,用于 ,并使用 信息更新重放缓冲区中的优先级。

  5. 定期从重放缓冲区中移除旧经验。

  6. 定期更新目标网络参数。

  7. 结束

如果你查看演员和学习者算法,它们并不复杂。然而,将它们解耦的关键直觉带来了显著的性能提升。

在我们结束这一部分讨论之前,让我们接下来讨论一些 Ape-X 框架的实际细节。

实现 Ape-X DQN 时的实际考虑

Ape-X 论文包含了实现的更多细节,以下是一些关键点:

  • 演员的探索率为 ,当 和 与 时,这些值在训练过程中保持不变。

  • 在学习开始之前,有一个宽限期来收集足够的经验,作者将其设置为 50,000 次转换,用于 Atari 环境。

  • 奖励和梯度范数被裁剪以稳定学习过程。

所以,请记住在你的实现中关注这些细节。

到目前为止,这是一个漫长的旅程,充满了理论和抽象讨论——感谢你的耐心!现在,终于到了实践环节。接下来的章节和本书的其余部分,我们将大量依赖 Ray/RLlib 库。所以,接下来让我们先了解一下 Ray,然后实现一个分布式深度 Q 学习代理。

使用 Ray 实现可扩展的深度 Q 学习算法

在本节中,我们将使用 Ray 库实现一个并行化的 DQN 变体。Ray 是一个强大、通用且简单的框架,适用于在单台机器以及大型集群上构建和运行分布式应用程序。Ray 是为具有异构计算需求的应用程序而构建的。这正是现代深度 RL 算法所需要的,因为它们涉及到长时间和短时间任务的混合、GPU 和 CPU 资源的使用等。事实上,Ray 本身有一个强大的 RL 库,称为 RLlib。Ray 和 RLlib 在学术界和工业界的应用日益广泛。

信息

要将 Ray 与其他分布式后端框架(如 Spark 和 Dask)进行比较,请参见bit.ly/2T44AzK。你会发现,Ray 是一个非常具有竞争力的替代方案,甚至在某些基准测试中击败了 Python 自带的多进程实现。

编写生产级的分布式应用程序是一项复杂的工作,这并不是我们在这里的目标。为此,我们将在下一节中介绍 RLlib。另一方面,实现你自己的自定义——虽然是简单的——深度 RL 算法是非常有益的,至少从教育角度来说是如此。因此,这个练习将帮助你实现以下目标:

  • 向你介绍 Ray,除了 RL,你还可以用它来做其他任务。

  • 让你了解如何构建自定义的并行化深度 RL 算法。

  • 如果你希望深入了解 RLlib 源代码,这将是一个踏脚石。

如果你愿意,你可以在这个练习的基础上构建自己的分布式深度 RL 想法。

好了,让我们开始吧!

Ray 简介

我们将先介绍 Ray,然后进入我们的练习。这将是一次简短的介绍,确保内容连贯。关于 Ray 工作原理的详细文档,请参考 Ray 的官方网站。

信息

Ray 和 RLlib 的文档可以在 docs.ray.io/en/latest/index.html 找到,其中包括 API 参考、示例和教程。源代码可以在 GitHub 上找到,地址是 github.com/ray-project/ray。

接下来,让我们讨论一下 Ray 中的主要概念。

Ray 中的主要概念

在我们深入探讨如何编写 Ray 应用程序之前,我们需要先讨论它所涉及的主要组件。Ray 使得你可以通过一个简单的 Python 装饰器 @ray.remote,让常规的 Python 函数和类在分离的远程进程上运行。在执行过程中,Ray 会处理这些函数和类的执行位置——无论是在你本地机器上的进程,还是如果你有集群的话,在集群中的某个地方。更详细地说,以下是它们的内容:

  • 远程函数(任务) 类似于常规的 Python 函数,只不过它们是异步执行的,并且是分布式运行的。一旦调用,远程函数会立即返回一个对象 ID,并创建一个任务在工作进程上执行它。请注意,远程函数在调用之间不会保持状态。

  • 对象 ID(未来对象) 是远程 Python 对象的引用,例如,远程函数的整数输出。远程对象存储在共享内存对象存储中,可以通过远程函数和类访问。请注意,对象 ID 可能指向一个未来可用的对象,例如,等远程函数执行完成后,该对象才会可用。

  • 远程类(演员) 类似于常规的 Python 类,但它们运行在工作进程中。与远程函数不同,远程类是有状态的,它们的方法像远程函数一样工作,共享远程类中的状态。顺便提一下,这里的 "演员" 术语不要与分布式 RL 中的 "演员" 混淆——尽管可以使用 Ray actor 实现一个 RL actor。

接下来,让我们看看如何安装 Ray 并使用远程函数和类。

安装和启动 Ray

Ray 可以通过简单的 pip install -U ray 命令安装。如果要与我们稍后使用的 RLlib 库一起安装,只需使用 pip install -U ray[rllib]。

信息

请注意,Ray 支持 Linux 和 macOS。在本书撰写时,它的 Windows 版本仍处于测试阶段。

一旦安装完成,Ray 需要在创建任何远程函数、对象或类之前进行初始化:

import ray
ray.init()

接下来,让我们创建一些简单的远程函数。在此过程中,我们将使用 Ray 文档中的示例。

使用远程函数

如前所述,Ray 通过一个简单的装饰器将常规的 Python 函数转换为远程函数:

@ray.remote
def remote_function():
    return 1

一旦调用,该函数将执行一个工作进程。因此,调用该函数多次将创建多个工作进程以实现并行执行。为此,远程函数需要通过 remote() 来调用:

object_ids = []
for _ in range(4):
    y_id = remote_function.remote()    
    object_ids.append(y_id)

请注意,函数调用不会等待彼此完成。然而,一旦调用,函数会立即返回一个对象 ID。为了像常规 Python 对象一样使用对象 ID 获取函数结果,我们只需要使用 objects = ray.get(object_ids)。请注意,这会使进程等待该对象可用。

对象 ID 可以像常规 Python 对象一样传递给其他远程函数或类:

@ray.remote
def remote_chain_function(value):
    return value + 1
y1_id = remote_function.remote()
chained_id = remote_chain_function.remote(y1_id)

这里有几点需要注意:

  • 这在两个任务之间创建了依赖关系。remote_chain_function 的调用将等待 remote_function 调用的输出。

  • 在 remote_chain_function 中,我们不需要调用 ray.get(value)。Ray 会自动处理,无论是对象 ID 还是已经接收到的对象。

  • 如果这两个任务的两个工作进程位于不同的机器上,输出将会从一台机器复制到另一台机器。

这只是 Ray 远程函数的简要概述。接下来,我们将深入了解远程对象。

使用远程对象

普通 Python 对象可以轻松地转换为 Ray 远程对象,如下所示:

y = 1
object_id = ray.put(y)

这会将对象存储在共享内存对象存储中。请注意,远程对象是不可变的,创建后其值不能更改。

最后,让我们来了解一下 Ray 远程类。

使用远程类

在 Ray 中使用远程类(演员)与使用远程函数非常相似。以下是如何使用 Ray 的远程装饰器装饰一个类的示例:

@ray.remote
class Counter(object):
    def __init__(self):
        self.value = 0
    def increment(self):
        self.value += 1
        return self.value

为了初始化该类的对象,我们除了调用类外,还需要使用 remote:

a = Counter.remote()

同样,调用该对象的方法时需要使用 remote:

obj_id = a.increment.remote()
ray.get(obj_id) == 1

就这样!Ray 的这一简要概述为我们继续实现可扩展的 DQN 算法奠定了基础。

Ray 实现的 DQN 变体

在本节中,我们将使用 Ray 实现一个 DQN 变体,这将类似于 Ape-X DQN 结构,只是为了简单起见,我们没有实现优先回放。代码将包括以下组件:

  • train_apex_dqn.py 是主脚本,接受训练配置并初始化其他组件。

  • actor.py 包含与环境交互并收集经验的 RL 演员类。

  • parameter_server.py 包含一个参数服务器类,它将优化后的 Q 模型权重提供给演员。

  • replay.py 包含回放缓冲区类。

  • learner.py 包含一个学习者类,该类从回放缓冲区接收样本,进行梯度更新,并将新的 Q 网络权重推送到参数服务器。

  • models.py 包含使用 TensorFlow/Keras 创建前馈神经网络的函数。

然后我们在 Gym 的 CartPole (v0) 上运行这个模型,看看它的表现如何。让我们开始吧!

主脚本

主脚本的初步步骤是接收一组将在训练过程中使用的配置。这看起来像下面这样:

    max_samples = 500000
    config = {"env": "CartPole-v0",
              "num_workers": 50,
              "eval_num_workers": 10,
              "n_step": 3,
              "max_eps": 0.5,
              "train_batch_size": 512,
              "gamma": 0.99,
              "fcnet_hiddens": [256, 256],
              "fcnet_activation": "tanh",
              "lr": 0.0001,
              "buffer_size": 1000000,
              "learning_starts": 5000,
              "timesteps_per_iteration": 10000,
              "grad_clip": 10}

让我们看看这些配置的一些细节:

  • env 是 Gym 环境的名称。

  • num_workers 是将会创建的训练环境/代理的数量,用于收集经验。请注意,每个 worker 会占用计算机上的一个 CPU,因此你需要根据你的机器进行调整。

  • eval_num_workers 是将会创建的评估环境/代理的数量,用于在训练的该时刻评估策略。同样,每个 worker 会占用一个 CPU。请注意,这些代理具有 ,因为我们不需要它们来探索环境。

  • n_step 是多步学习的步数。

  • max_eps 将设置训练代理中的最大探索率,,因为我们会为每个训练代理分配不同的探索率,范围在 之间。

  • timesteps_per_iteration 决定我们运行评估的频率;多步学习的步数。请注意,这不是我们进行梯度更新的频率,因为学习者会持续采样并更新网络参数。

使用这个配置,我们创建了参数服务器、重放缓存和学习者。我们稍后会详细介绍这些类的具体内容。请注意,由于它们是 Ray actor,我们使用remote来启动它们:

    ray.init()
    parameter_server = ParameterServer.remote(config)
    replay_buffer = ReplayBuffer.remote(config)
    learner = Learner.remote(config, 
                             replay_buffer,
                             parameter_server)

我们提到过,学习者是一个独立的进程,它会持续从重放缓存中采样并更新 Q 网络。我们在主脚本中启动学习过程:

learner.start_learning.remote()

当然,单独这样做不会有什么效果,因为 actor 还没有开始收集经验。接下来我们启动训练 actor 并立即让它们开始从环境中采样:

    for i in range(config["num_workers"]):
        eps = config["max_eps"] * i / config["num_workers"]
        actor = Actor.remote("train-" + str(i), 
                             replay_buffer, 
                             parameter_server, 
                             config, 
                             eps)
        actor.sample.remote()

我们还启动了评估 actor,但我们不希望它们立即开始采样。这将在学习者更新 Q 网络时发生:

    for i in range(config["eval_num_workers"]):
        eps = 0
        actor = Actor.remote("eval-" + str(i), 
                             replay_buffer, 
                             parameter_server, 
                             config, 
                             eps, 
                             True)

最后,我们有主循环,在其中交替进行训练和评估。随着评估结果的改善,我们会保存训练过程中最好的模型:

    total_samples = 0
    best_eval_mean_reward = np.NINF
    eval_mean_rewards = []
    while total_samples < max_samples:
        tsid = replay_buffer.get_total_env_samples.remote()
        new_total_samples = ray.get(tsid)
        if (new_total_samples - total_samples
                >= config["timesteps_per_iteration"]):
            total_samples = new_total_samples
            parameter_server.set_eval_weights.remote()
            eval_sampling_ids = []
            for eval_actor in eval_actor_ids:
                sid = eval_actor.sample.remote()
                eval_sampling_ids.append(sid)
            eval_rewards = ray.get(eval_sampling_ids)
            eval_mean_reward = np.mean(eval_rewards)
            eval_mean_rewards.append(eval_mean_reward)
            if eval_mean_reward > best_eval_mean_reward:
                best_eval_mean_reward = eval_mean_reward
                parameter_server.save_eval_weights.remote()

请注意,代码中还有一些没有包括在这里的内容(例如将评估指标保存到 TensorBoard)。有关所有细节,请参阅完整的代码。

接下来,让我们详细了解 actor 类的内容。

RL actor 类

RL actor 负责根据探索策略从环境中收集经验。探索的速率在主脚本中为每个 actor 确定,并且在整个采样过程中保持不变。actor 类还在将经验推送到重放缓存之前,先在本地存储经验,以减少通信开销。同时,请注意,我们区分训练和评估 actor,因为我们仅为评估 actor 运行单一回合的采样步骤。最后,actor 会定期拉取最新的 Q 网络权重,以更新其策略。

这是我们初始化 actor 的方法:

@ray.remote
class Actor:
    def __init__(self,
                 actor_id,
                 replay_buffer,
                 parameter_server,
                 config,
                 eps,
                 eval=False):
        self.actor_id = actor_id
        self.replay_buffer = replay_buffer
        self.parameter_server = parameter_server
        self.config = config
        self.eps = eps
        self.eval = eval
        self.Q = get_Q_network(config)
        self.env = gym.make(config["env"])
        self.local_buffer = []
        self.obs_shape = config["obs_shape"]
        self.n_actions = config["n_actions"]
        self.multi_step_n = config.get("n_step", 1)
        self.q_update_freq = config.get("q_update_freq", 100)
        self.send_experience_freq = \
                    config.get("send_experience_freq", 100)
        self.continue_sampling = True
        self.cur_episodes = 0
        self.cur_steps = 0

actor 使用以下方法来更新和同步其策略:

    def update_q_network(self):
        if self.eval:
            pid = \
              self.parameter_server.get_eval_weights.remote()
        else:
            pid = \
              self.parameter_server.get_weights.remote()
        new_weights = ray.get(pid)
        if new_weights:
            self.Q.set_weights(new_weights)

评估权重被单独存储和提取的原因是,由于学习者始终在学习,不论主循环中发生了什么,我们需要对 Q 网络进行快照以进行评估。

现在,我们为演员编写采样循环。让我们从初始化将在循环中更新的变量开始:

    def sample(self):
        self.update_q_network()
        observation = self.env.reset()
        episode_reward = 0
        episode_length = 0
        n_step_buffer = deque(maxlen=self.multi_step_n + 1)

循环中的第一件事是获取一个动作并在环境中采取一步:

        while self.continue_sampling:
            action = self.get_action(observation)
            next_observation, reward, \
            done, info = self.env.step(action)

我们的代码支持多步学习。为了实现这一点,滚动轨迹存储在一个最大长度为 的双端队列中。当双端队列满时,表示轨迹足够长,可以将经验存储到重放缓冲区中:

            n_step_buffer.append((observation, action,
                                  reward, done))
            if len(n_step_buffer) == self.multi_step_n + 1:
                self.local_buffer.append(
                    self.get_n_step_trans(n_step_buffer))

我们记得更新我们所拥有的计数器:

            self.cur_steps += 1
            episode_reward += reward
            episode_length += 1

在每一集结束时,我们重置环境和特定于该集的计数器。我们还将经验保存到本地缓冲区,无论其长度如何。还要注意,如果这是一次评估回合,我们会在回合结束时中断采样循环:

            if done:
                if self.eval:
                    break
                next_observation = self.env.reset()
                if len(n_step_buffer) > 1:
                    self.local_buffer.append(
                        self.get_n_step_trans(n_step_buffer))
                self.cur_episodes += 1
                episode_reward = 0
                episode_length = 0

我们定期将经验发送到重放缓冲区,并且也定期更新网络参数:

            observation = next_observation
            if self.cur_steps % \
                    self.send_experience_freq == 0 \
                    and not self.eval:
                self.send_experience_to_replay()
            if self.cur_steps % \
                    self.q_update_freq == 0 and not self.eval:
                self.update_q_network()
        return episode_reward

接下来,让我们看看动作采样的细节。动作是以 -贪婪的方式选择的,如下所示:

     def get_action(self, observation):
        observation = observation.reshape((1, -1))
        q_estimates = self.Q.predict(observation)[0]
        if np.random.uniform() <= self.eps:
            action = np.random.randint(self.n_actions)
        else:
            action = np.argmax(q_estimates)
        return action

经验是从轨迹双端队列中提取的,如下所示:

    def get_n_step_trans(self, n_step_buffer):
        gamma = self.config['gamma']
        discounted_return = 0
        cum_gamma = 1
        for trans in list(n_step_buffer)[:-1]:
            _, _, reward, _ = trans
            discounted_return += cum_gamma * reward
            cum_gamma *= gamma
        observation, action, _, _ = n_step_buffer[0]
        last_observation, _, _, done = n_step_buffer[-1]
        experience = (observation, action, discounted_return,
                      last_observation, done, cum_gamma)
        return experience

最后,存储在本地的经验元组将被发送到重放缓冲区,如下所示:

    def send_experience_to_replay(self):
        rf = self.replay_buffer.add.remote(self.local_buffer)
        ray.wait([rf])
        self.local_buffer = []

到这里,演员部分就完成了!接下来,让我们看看参数服务器。

参数服务器类

参数服务器是一种简单的结构,用于接收来自学习者的更新参数(权重),并将它们提供给演员。它主要由设置器和获取器,以及一个保存方法组成。再一次提醒,我们定期拍摄参数的快照并将其用于评估。如果结果超过之前的最佳结果,则保存权重:

@ray.remote
class ParameterServer:
    def __init__(self, config):
        self.weights = None
        self.eval_weights = None
        self.Q = get_Q_network(config)
    def update_weights(self, new_parameters):
        self.weights = new_parameters
        return True
    def get_weights(self):
        return self.weights
    def get_eval_weights(self):
        return self.eval_weights
    def set_eval_weights(self):
        self.eval_weights = self.weights
        return True
    def save_eval_weights(self,
                          filename=
                          'checkpoints/model_checkpoint'):
        self.Q.set_weights(self.eval_weights)
        self.Q.save_weights(filename)
        print("Saved.")

请注意,参数服务器仅存储实际的 Q 网络结构,以便能够使用 TensorFlow 方便的保存功能。除此之外,仅在不同进程之间传递神经网络的权重,而不是完整的模型,以避免不必要的开销和序列化问题。

接下来,我们将介绍重放缓冲区的实现。

重放缓冲区类

如前所述,为了简化,我们实现了一个标准的重放缓冲区(没有优先级采样)。因此,重放缓冲区从演员处接收经验,并将采样的经验发送给学习者。它还跟踪它在训练过程中已经接收到的所有经验元组的数量:

@ray.remote
class ReplayBuffer:
    def __init__(self, config):
        self.replay_buffer_size = config["buffer_size"]
        self.buffer = deque(maxlen=self.replay_buffer_size)
        self.total_env_samples = 0
    def add(self, experience_list):
        experience_list = experience_list
        for e in experience_list:
            self.buffer.append(e)
            self.total_env_samples += 1
        return True
    def sample(self, n):
        if len(self.buffer) > n:
            sample_ix = np.random.randint(
                len(self.buffer), size=n)
            return [self.buffer[ix] for ix in sample_ix]
    def get_total_env_samples(self):
        return self.total_env_samples

模型生成

由于我们仅在进程之间传递 Q 网络的权重,因此每个相关演员都会创建其自己的 Q 网络副本。这些 Q 网络的权重随后会根据从参数服务器接收到的内容进行设置。

Q 网络使用 Keras 创建,如下所示:

def get_Q_network(config):
    obs_input = Input(shape=config["obs_shape"],
                      name='Q_input')
    x = Flatten()(obs_input)
    for i, n_units in enumerate(config["fcnet_hiddens"]):
        layer_name = 'Q_' + str(i + 1)
        x = Dense(n_units,
                  activation=config["fcnet_activation"],
                  name=layer_name)(x)
    q_estimate_output = Dense(config["n_actions"],
                              activation='linear',
                              name='Q_output')(x)
    # Q Model
    Q_model = Model(inputs=obs_input,
                    outputs=q_estimate_output)
    Q_model.summary()
    Q_model.compile(optimizer=Adam(), loss='mse')
    return Q_model

这里一个重要的实现细节是,这个 Q 网络并不是我们想要训练的,因为它对于给定的状态,会预测所有可能动作的 Q 值。另一方面,给定的经验元组只包含一个目标值,针对这些可能动作中的一个:在该元组中由智能体选择的动作。因此,当我们使用该经验元组更新 Q 网络时,梯度应该仅通过所选动作的输出流动。其余的动作应该被遮蔽。我们通过使用基于所选动作的遮蔽输入,在这个 Q 网络之上添加一个自定义层来实现,这个层仅计算所选动作的损失。这样,我们就得到了一个可以训练的模型。

下面是我们如何实现遮蔽损失的:

def masked_loss(args):
    y_true, y_pred, mask = args
    masked_pred = K.sum(mask * y_pred, axis=1, keepdims=True)
    loss = K.square(y_true - masked_pred)
    return K.mean(loss, axis=-1)

然后,得到可训练模型,如下所示:

def get_trainable_model(config):
    Q_model = get_Q_network(config)
    obs_input = Q_model.get_layer("Q_input").output
    q_estimate_output = Q_model.get_layer("Q_output").output
    mask_input = Input(shape=(config["n_actions"],),
                       name='Q_mask')
    sampled_bellman_input = Input(shape=(1,),
                                  name='Q_sampled')
    # Trainable model
    loss_output = Lambda(masked_loss,
                         output_shape=(1,),
                         name='Q_masked_out')\
                        ([sampled_bellman_input,
                          q_estimate_output,
                          mask_input])
    trainable_model = Model(inputs=[obs_input,
                                    mask_input,
                                    sampled_bellman_input],
                            outputs=loss_output)
    trainable_model.summary()
    trainable_model.compile(optimizer=
                            Adam(lr=config["lr"],
                            clipvalue=config["grad_clip"]),
                            loss=[lambda y_true,
                                         y_pred: y_pred])
    return Q_model, trainable_model

正是这个可训练的模型将由学习者来优化。编译后的 Q 网络模型永远不会单独训练,我们在其中指定的优化器和损失函数只是占位符。

最后,让我们看一下学习者的部分。

学习者类

学习者的主要任务是从重放缓冲区接收一批经验样本,解包它们,并通过梯度步骤优化 Q 网络。这里,我们只包括了类初始化和优化步骤的一部分。

类的初始化如下:

@ray.remote
class Learner:
    def __init__(self, config, replay_buffer, parameter_server):
        self.config = config
        self.replay_buffer = replay_buffer
        self.parameter_server = parameter_server
        self.Q, self.trainable = get_trainable_model(config)
        self.target_network = clone_model(self.Q)

现在是优化步骤。我们从重放缓冲区中采样,并更新我们保持的计数器:

    def optimize(self):
        samples = ray.get(self.replay_buffer
                          .sample.remote(self.train_batch_size))
        if samples:
            N = len(samples)
            self.total_collected_samples += N
            self.samples_since_last_update += N
            ndim_obs = 1
            for s in self.config["obs_shape"]:
                if s:
                    ndim_obs *= s

然后,我们解包样本并重新调整其形状:

            n_actions = self.config["n_actions"]
            obs = np.array([sample[0] for sample \
                        in samples]).reshape((N, ndim_obs))
            actions = np.array([sample[1] for sample \
                        in samples]).reshape((N,))
            rewards = np.array([sample[2] for sample \
                        in samples]).reshape((N,))
            last_obs = np.array([sample[3] for sample \
                        in samples]).reshape((N, ndim_obs))
            done_flags = np.array([sample[4] for sample \
                        in samples]).reshape((N,))
            gammas = np.array([sample[5] for sample \
                        in samples]).reshape((N,))

我们创建掩码,仅更新在经验元组中选择的动作的 Q 值:

            masks = np.zeros((N, n_actions))
            masks[np.arange(N), actions] = 1
            dummy_labels = np.zeros((N,))

在主函数中,我们首先准备好输入给可训练 Q 网络,然后调用fit函数。在此过程中,我们使用双 DQN:

            # double DQN
            maximizer_a = np.argmax(self.Q.predict(last_obs), axis=1)
            target_network_estimates = self.target_network.predict(last_obs)
            q_value_estimates = np.array([target_network_estimates[i,
                                   maximizer_a[i]]
                                   for i in range(N)]).reshape((N,))
            sampled_bellman = rewards + gammas * \
                              q_value_estimates * (1 - done_flags)
            trainable_inputs = [obs, masks,
                                sampled_bellman]
            self.trainable.fit(trainable_inputs, dummy_labels, verbose=0)
            self.send_weights()

最后,我们定期更新目标网络:

            if self.samples_since_last_update > 500:
                self.target_network.set_weights(self.Q.get_weights())
                self.samples_since_last_update = 0
            return True

更多细节,请参见learner.py中的完整代码。

就这样!让我们看看这个架构在 CartPole 环境中的表现。

结果

你可以通过简单地运行主脚本来开始训练。在运行之前有几点需要注意:

  • 不要忘记激活已安装 Ray 的 Python 环境。强烈推荐使用虚拟环境。

  • 将工人的总数(用于训练和评估)设置为小于你机器上 CPU 的数量。

这样,你就可以按照如下方式开始训练:

python train_apex_dqn.py

完整代码包括一些保存评估进度到 TensorBoard 的附加功能。你可以在相同的文件夹内启动 TensorBoard,方法如下:

tensorboard --logdir logs/scalars

然后,访问默认的 TensorBoard 地址http://localhost:6006/。我们实验的评估图如下所示:

图 6.7 – CartPole v0 的分布式 DQN 评估结果

图 6.7 – CartPole v0 的分布式 DQN 评估结果

你可以看到,在大约 150,000 次迭代后,奖励达到了最大值 200。

做得好!你已经实现了一个深度 Q 学习算法,并且能够通过 Ray 将其扩展到多个 CPU,甚至是集群中的多个节点!可以随意改进这个实现,加入更多技巧,融入自己的创意!

让我们以在 RLlib 中如何运行类似实验来结束本章。

使用 RLlib 进行生产级深度强化学习

正如我们在开头提到的,Ray 的创造者之一的动机是构建一个易于使用的分布式计算框架,能够处理像深度强化学习这样的复杂和异构应用。因此,他们还基于 Ray 创建了一个广泛使用的深度强化学习库。使用 RLlib 训练一个类似于我们的模型非常简单,主要步骤如下:

  1. 导入 Ape-X DQN 的默认训练配置和训练器。

  2. 自定义训练配置。

  3. 训练训练器。

就是这样!所需的代码非常简单。你只需要以下内容:

import pprint
from ray import tune
from ray.rllib.agents.dqn.apex import APEX_DEFAULT_CONFIG
from ray.rllib.agents.dqn.apex import ApexTrainer
if __name__ == '__main__':
    config = APEX_DEFAULT_CONFIG.copy()
    pp = pprint.PrettyPrinter(indent=4)
    pp.pprint(config)
    config['env'] = "CartPole-v0"
    config['num_workers'] = 50
    config['evaluation_num_workers'] = 10
    config['evaluation_interval'] = 1
    config['learning_starts'] = 5000
    tune.run(ApexTrainer, config=config)

这样,你的训练应该开始了。RLlib 有很棒的 TensorBoard 日志记录功能。通过运行以下命令来初始化 TensorBoard:

tensorboard --logdir=~/ray_results

我们训练的结果如下所示:

图 6.8 – RLlib 对 CartPole v0 的评估结果

图 6.8 – RLlib 对 CartPole v0 的评估结果

事实证明,我们的 DQN 实现非常具有竞争力!但现在,借助 RLlib,你可以访问来自强化学习文献的许多改进。你可以通过更改默认配置来自定义你的训练。请花点时间浏览我们代码中打印出来的所有可用选项的长长列表。它看起来像这样:

{   'adam_epsilon': 1e-08,
    'batch_mode': 'truncate_episodes',
    'beta_annealing_fraction': -1,
    'buffer_size': 2000000,
    'callbacks': <class 'ray.rllib.agents.callbacks.DefaultCallbacks'>,
    'clip_actions': True,
    'clip_rewards': None,
    'collect_metrics_timeout': 180,
    'compress_observations': False,
    'custom_eval_function': None,
    'custom_resources_per_worker': {},
    'double_q': True,
    'dueling': True,
...

再次提醒,列表很长。但这展示了你在 RLlib 中拥有的强大功能!我们将在接下来的章节中继续使用 RLlib,并深入探讨更多细节。

恭喜你!你在本章中做得非常出色,取得了很多成就。仅仅我们在这里所覆盖的内容,就为你提供了一个强大的工具库,能够解决许多顺序决策问题。接下来的章节将深入探讨更先进的深度强化学习材料,现在你已经准备好迎接挑战!

摘要

在本章中,我们从使用表格型 Q 学习到实现一个现代化的分布式深度 Q 学习算法,已经走了很长一段路。在此过程中,我们介绍了 NFQ、在线 Q 学习、带有 Rainbow 改进的 DQN、Gorila 和 Ape-X DQN 算法的细节。我们还介绍了 Ray 和 RLlib,这两个强大的分布式计算和深度强化学习框架。

在下一个章节中,我们将探讨另一类深度 Q 学习算法:基于策略的方法。这些方法将允许我们直接学习随机策略并使用连续动作。

参考文献

  • Sutton, R. S. & Barto, A. G. (2018). 强化学习:导论. MIT 出版社. URL:incompleteideas.net/book/the-book.html

  • Mnih, V. 等人 (2015). 通过深度强化学习实现人类级控制. 自然,518(7540),529–533

  • Riedmiller, M. (2005) 神经拟合 Q 迭代 – 数据高效的神经强化学习方法初步经验。载于:Gama, J., Camacho, R., Brazdil, P.B., Jorge, A.M., & Torgo L.(编辑)《机器学习:ECML 2005》. ECML 2005. 计算机科学讲座笔记,第 3720 卷。Springer,柏林,海德堡

  • Lin, L. (1993). 使用神经网络的机器人强化学习。

  • McClelland, J. L., McNaughton, B. L., & O'Reilly, R. C. (1995). 为何海马体和新皮层中存在互补的学习系统:来自联结主义学习和记忆模型成功与失败的启示. 心理学评论,102(3),419–457

  • van Hasselt, H., Guez, A., & Silver, D. (2016). 深度强化学习与双 Q 学习. 载于:AAAI 会议论文集,2094–2100

  • Schaul, T., Quan, J., Antonoglou, I., & Silver, D. (2015). 优先经验回放. 载于:ICLR 会议论文集

  • Wang, Z., Schaul, T., Hessel, M., van Hasselt, H., Lanctot, M., & de Freitas, N. (2016). 深度强化学习的对抗网络架构. 载于:第 33 届国际机器学习会议论文集,1995–2003

  • Sutton, R. S. (1988). 通过时间差分方法学习预测. 机器学习 3(1), 9–44

  • Bellemare, M. G., Dabney, W., & Munos, R. (2017). 强化学习的分布式视角. 载于:ICML 会议论文集

  • Fortunato, M., Azar, M. G., Piot, B., Menick, J., Osband, I., Graves, A., Mnih, V., Munos, R., Hassabis, D., Pietquin, O., Blundell, C., & Legg, S. (2017). 用于探索的噪声网络. 网址:arxiv.org/abs/1706.10295

  • Hessel, M., Modayil, J., Hasselt, H.V., Schaul, T., Ostrovski, G., Dabney, W., Horgan, D., Piot, B., Azar, M.G., & Silver, D. (2018). Rainbow: 结合深度强化学习中的改进. 网址:arxiv.org/abs/1710.02298

  • Hasselt, H.V., Doron, Y., Strub, F., Hessel, M., Sonnerat, N., & Modayil, J. (2018). 深度强化学习与致命三合一问题. 网址:arxiv.org/abs/1812.02648

  • Nair, A., Srinivasan, P., Blackwell, S., Alcicek, C., Fearon, R., Maria, A.D., Panneershelvam, V., Suleyman, M., Beattie, C., Petersen, S., Legg, S., Mnih, V., Kavukcuoglu, K., & Silver, D. (2015). 大规模并行深度强化学习方法. 网址:arxiv.org/abs/1507.04296

  • Horgan, D., Quan, J., Budden, D., Barth-Maron, G., Hessel, M., Hasselt, H.V., & Silver, D. (2018). 分布式优先经验回放. 网址:arxiv.org/abs/1803.00933

第七章:第七章:基于策略的方法

在上一章中我们讨论的基于价值的方法在许多具有离散控制空间的环境中取得了良好的效果。然而,许多应用,如机器人技术,需要连续控制。在本章中,我们将深入讨论另一类重要的算法——基于策略的方法,这些方法使我们能够解决连续控制问题。此外,这些方法直接优化策略网络,因此站在更强的理论基础上。最后,基于策略的方法能够学习真正的随机策略,这是部分可观察环境和游戏中所需的,而基于价值的方法无法学习。总的来说,基于策略的方法在许多方面补充了基于价值的方法。本章将深入探讨基于策略的方法,因此你将深入理解它们的工作原理。

本章我们将讨论以下主题:

  • 为什么我们应该使用基于策略的方法?

  • 基础策略梯度

  • Actor-critic 方法

  • 信任区域方法

  • 离策略方法

  • 在《月球着陆》中的基于策略方法的比较

  • 如何选择合适的算法

  • 开源的基于策略方法的实现

让我们直接开始吧!

为什么我们应该使用基于策略的方法?

我们将从本章开始讨论为什么我们需要基于策略的方法,因为我们已经介绍了许多基于价值的方法。基于策略的方法 i) 可以说更有原则,因为它们直接基于策略参数进行优化,ii) 允许我们使用连续动作空间,iii) 能够学习真正的随机策略。现在让我们深入探讨这些要点的细节。

一种更有原则的方法

在 Q 学习中,策略是通过学习动作值间接获得的,然后用这些值来确定最佳动作。然而,我们真的需要知道一个动作的值吗?大多数时候我们不需要,因为它们只是帮助我们获得最优策略的代理。基于策略的方法通过学习函数逼近来直接给出策略,而不需要这样的中间步骤。这可以说是一种更有原则的方法,因为我们可以直接通过梯度步骤来优化策略,而不是代理的动作值表示。当存在许多具有相似值的动作时,后者尤其低效,可能这些动作对我们来说都没有吸引力,因为它们都是糟糕的动作。

使用连续动作空间的能力

我们在上一节提到的所有基于价值的方法都适用于离散动作空间。另一方面,有许多应用场景需要使用连续动作空间,例如机器人技术,其中动作的离散化会导致代理行为变差。那么,使用基于价值的方法与连续动作空间有什么问题呢?神经网络当然可以为连续动作学习价值表示——毕竟,状态并没有这样的限制。

然而,记住我们在计算目标值时是如何进行动作最大化的:

并在使用时获得在环境中执行的最佳动作。虽然我们可以通过以下方法使这些最大化在连续动作空间上有效,但实现起来并不简单:

  1. 在最大化过程中,从连续动作空间中采样离散动作,并使用具有最大值的动作。或者,可以拟合一个函数来表示采样动作的值,并在该函数上进行最大化,这被称为交叉熵方法(CEM)。

  2. 替代使用神经网络,采用一个如动作二次函数的函数逼近器,其最大值可以通过解析方法计算得出。一个例子是归一化优势函数(NAF)(Gu et al, 2016)。

  3. 学习一个单独的函数逼近器来获得最大值,例如在深度确定性策略梯度(DDPG)算法中。

现在,CEM 和 NAF 的缺点是与直接表示连续动作策略的神经网络相比,它们的效果较差。另一方面,DDPG 仍然是一个有竞争力的替代方案,我们将在本章后续部分讨论。

信息

大多数基于策略的方法同时适用于离散和连续动作空间。

学习真正随机的随机策略的能力

在整个 Q 学习过程中,我们使用了软策略,如-贪婪策略,以便在训练期间让智能体探索环境。尽管这种方法在实践中效果相当不错,而且可以通过退火来使其更复杂,但它仍然不是一个学习到的参数。基于策略的方法可以学习随机策略,从而在训练过程中实现更有原则的探索。

也许更大的问题是,我们可能不仅仅为了训练需要学习随机策略,推理时也可能需要这样做。我们可能想要这样做的原因有两个:

  • 在部分可观察环境(POMDPs)中,我们可能会遇到所谓的同态状态,这些状态虽然不同,但发出的观察是相同的,对于这些状态,最佳动作可能是不同的。考虑以下示例:

图 7.1 – 在部分可观察环境中的机器人

图 7.1 – 在部分可观察环境中的机器人

智能体只观察它所在状态的形态,但无法判断状态是什么。智能体随机初始化在除状态 3 以外的状态中,目标是通过左右移动,在最少步数内到达状态 3 中的硬币。当智能体观察到六边形时,最佳动作是随机的,因为一个确定性的策略(比如始终向左走)会让智能体在 1 和 2 之间(如果始终选择左走)或 4 和 5 之间卡住。

  • 在对抗性智能体的游戏设置中,可能会出现唯一最优策略是随机策略的情况。经典的例子是石头剪子布,在这种情况下,最优策略是随机选择一个动作。任何其他策略都可能被环境中的对手利用。

基于价值的方法无法学习这种随机策略进行推断,而基于策略的方法则可以。

提示

如果环境是完全可观察的,并且不是游戏环境,那么总会存在一个确定性的最优策略(尽管可能有多个最优策略,而且其中一些可能是随机的)。在这种情况下,我们在推断过程中不需要随机策略。

通过这个介绍,让我们深入了解最流行的基于策略的方法。接下来,我们将概述基础的策略梯度方法,为我们稍后将要介绍的更复杂的算法做铺垫。

原始策略梯度方法

我们将从讨论最基础的算法——原始策略梯度方法开始。尽管这种算法在实际问题中很少有用,但理解它对于建立强大的直觉和理论基础至关重要,以便我们能够理解后面将要介绍的更复杂的算法。

策略梯度方法中的目标

在基于价值的方法中,我们专注于找到对动作值的良好估计,之后利用这些估计来获得策略。而策略梯度方法则直接专注于根据强化学习目标优化策略——尽管我们仍然会利用价值估计。如果你不记得这个目标是什么,它是期望的折扣回报:

这是比之前写法稍微严谨一些的目标表达方式。让我们来解读一下:

  • 目标由 表示,它是当前策略的一个函数,。

  • 策略本身由 参数化,我们正在尝试确定这个参数。

  • 智能体观察到的轨迹 是随机的,其概率分布为 。正如你所期待的,它是策略的一个函数,因此也是 的函数。

  • 是一个函数(对智能体未知),根据环境动态、给定的状态 和动作 给予奖励。

现在,我们有一个目标函数 ,我们希望最大化它,这个目标函数依赖于我们可以控制的参数 。一种自然的做法是朝上升方向采取梯度步伐:

其中 是某个步长。这就是策略梯度方法的主要思想,正如之前所说,它直接优化策略。

现在,百万美元的问题(好吧,也许没那么多)是如何求出梯度项。接下来我们来看看怎么解决。

计算梯度

理解如何从目标函数对策略参数的梯度,,中获得梯度对于理解不同的策略梯度方法变种至关重要。接下来让我们一步步推导在经典策略梯度中使用的公式。

目标函数的另一种表示方式

首先,让我们稍微不同地表示目标函数:

在这里,我们只是将轨迹和与之对应的奖励整体表示出来,而不是逐个状态-动作对表示,。然后,我们使用期望的定义将其写成一个积分(稍微有点滥用符号,因为我们用相同的来表示随机变量和它的一个实现,但从上下文中应该能明显看出来)。请记住,观察到特定轨迹的概率是以下形式:

这只是一个关于观察某个状态、在给定状态下采取特定动作并观察下一个状态的概率链条。这里,表示环境转移概率。

接下来,我们用这个来找到一个便捷的梯度公式。

得出便捷的梯度表达式

现在让我们回到目标函数。我们可以将其梯度表示为:

现在我们有了需要处理的项。我们将做一个简单的技巧来消除它:

这直接来自的定义。将其代回积分后,我们得到了目标函数梯度的期望值(小心—不是目标函数本身的期望值):

现在,这将变成一个非常便捷的梯度公式。退后一步,我们现在得到了梯度的期望。当然,我们不能完全评估它,因为我们不知道,但我们可以从环境中获取样本。

提示

每当你在强化学习公式中看到期望值时,你可以合理地预期我们将使用来自环境的样本来进行评估。

这个公式构成了策略梯度方法的核心。接下来,让我们看看我们如何能更便捷地进行。

获得梯度

在开始从样本中估计梯度之前,我们需要消除另外一个项,因为我们并不真正知道它是什么。结果证明,我们可以通过为轨迹写出明确的概率乘积来解决这个问题:

当我们对 求梯度时,求和中的第一个和最后一个项会被去掉,因为它们不依赖于 。因此,我们可以用已知的内容来表示这个梯度,即我们拥有的策略 :

我们可以通过以下方式从一批 轨迹中估计梯度:

该梯度旨在增加具有高总奖励的轨迹的可能性,并减少那些低总奖励的轨迹(或增加它们的可能性较少)。

这为我们提供了构建一个策略梯度算法的所有要素,即 REINFORCE,接下来我们将继续介绍。

REINFORCE

REINFORCE 算法是最早的策略梯度方法之一,使用了我们上面介绍的内容。为了能够解决现实问题,我们需要在此基础上做很多改进。另一方面,理解 REINFORCE 对于在算法的背景下形式化这些想法是有用的。

在忽略折扣因子的有限时间问题中,REINFORCE 的工作原理如下:

  1. 初始化一个策略 :

    当某些停止准则未满足时,执行:

  2. 使用 从环境中收集 轨迹 。

  3. 计算

    。

  4. 更新 :

    结束时

REINFORCE 算法简单地建议使用当前的策略从环境中采样轨迹,然后使用这些样本估计梯度,并采取梯度步骤来更新策略参数。

信息

由于采样的轨迹用于获取当前策略参数的梯度估计,策略梯度方法是基于策略的。因此,我们不能使用在不同策略下获得的样本来改善现有策略,这与基于价值的方法不同。话虽如此,本章最后会有一节讨论几种基于策略外的方法。

REINFORCE 算法要求使用完整的轨迹进行网络更新,因此它是一种蒙特卡洛方法。

接下来,让我们讨论为什么我们需要对 REINFORCE 进行改进。

REINFORCE 和所有策略梯度方法的问题

一般来说,策略梯度算法的最重要问题是 估计的高方差。如果你仔细思考,实际上有许多因素导致了这一点:

  • 环境中的随机性可能导致即使使用相同策略,代理也会有许多不同的轨迹,这些轨迹的梯度可能会有很大差异。

  • 样本轨迹的长度可能会有很大差异,导致对数和奖励项的总和差异很大。

  • 稀疏奖励的环境可能尤其成问题(根据稀疏奖励的定义)。

  • 大小 通常保持在几千左右,以使学习过程可行,但它可能不足以捕捉完整的轨迹分布。

因此,我们从样本中获得的梯度估计可能具有较高的方差,这可能会导致学习不稳定。减少这种方差是使学习可行的一个重要目标,我们为此采用了各种技巧。接下来,我们将介绍其中的第一个技巧。

用前往奖励替换奖励总和

让我们首先将梯度估计中的项重新排列如下:

这种原始形式意味着每个 项都会根据整个轨迹获得的总奖励进行加权。然而直觉告诉我们,我们应该只根据跟随该状态-动作对的奖励总和来加权对数项,因为它们无法影响之前发生的事情(因果关系)。更正式地,我们可以将梯度估计写为如下:

事实证明,这仍然能为我们提供一个无偏的梯度估计。随着我们减少奖励项的数量,方差也会减小,结果是乘以对数项的权重变得更小。 被称为时间 的前往奖励。请注意,这实际上是对 的估计。我们将在后续的演员-评论员算法中使用它。

这种对 REINFORCE 算法的改进是一种 普通策略梯度 方法。接下来,我们将展示如何使用 RLlib 的普通策略梯度实现。

使用 RLlib 的普通策略梯度

RLlib 允许我们使用普通策略梯度与多个回合工作者(演员)一起并行化样本收集。你会注意到,与基于值的方法不同,样本收集将与网络权重更新同步,因为普通策略梯度算法是一种基于策略的方法。

信息

由于策略梯度方法是基于当前策略的,我们需要确保用于更新神经网络参数(权重)的样本来自网络建议的现有策略。这就要求在所有回合工作者中同步使用的策略。

并行化的普通策略梯度的总体架构如下所示:

图 7.2 – 普通策略梯度架构

图 7.2 – 普通策略梯度架构

此时,值得注意的是 RLlib 实现将样本作为 从演员传输到学习者,并在学习者中将它们连接起来以恢复完整的轨迹。

在 RLlib 中使用原始策略梯度非常简单,且与我们在上一章中使用基于值的方法非常相似。让我们为 OpenAI Lunar Lander 环境(具有连续动作空间)训练一个模型。请跟着一起操作!

  1. 首先,为了避免与 Gym 和 Box2D 包发生冲突,请使用以下命令安装 Gym:

    pip install gym[box2d]==0.15.6
    
  2. 然后开始实现 Python 代码。导入我们需要的包,用于参数解析的 ray 和 tune:

    import argparse
    import pprint
    from ray import tune
    import ray
    
  3. 导入原始 策略 梯度(PG)训练器类以及相应的配置字典:

    from ray.rllib.agents.pg.pg import (
        DEFAULT_CONFIG,
        PGTrainer as trainer)
    

    注意,当我们想使用不同的算法时,这部分会有所不同。

  4. 创建一个主函数,该函数接收 Gym 环境名称作为参数:

    if __name__ == "__main__":
        parser = argparse.ArgumentParser()
        parser.add_argument('--env',
                            help='Gym env name.')
        args = parser.parse_args()
    
  5. 修改配置字典,设置我们希望用于训练的 GPU 数量以及我们希望用于样本收集和评估的 CPU 数量:

        config = DEFAULT_CONFIG.copy()
        config_update = {
                     "env": args.env,
                     "num_gpus": 1,
                     "num_workers": 50,
                     "evaluation_num_workers": 10,
                     "evaluation_interval": 1
                }
        config.update(config_update)
        pp = pprint.PrettyPrinter(indent=4)
        pp.pprint(config)
    

    print 语句是让您查看如果需要更改配置时,还有哪些其他配置可供选择。例如,您可以修改学习率。现在,我们暂时不讨论这种超参数优化的细节。对于原始策略梯度,超参数的数量远少于更复杂算法所涉及的数量。最后一点说明:我们设置单独的评估工作者是为了使训练与我们后续介绍的离策略算法保持一致。通常,我们不需要这么做,因为在策略方法中,训练和评估过程中都遵循相同的策略。

  6. 实现这一部分,初始化 ray 并为给定的迭代次数训练代理:

        ray.init()
        tune.run(trainer,
                 stop={"timesteps_total": 2000000},
                 config=config
                 )
    
  7. 将此代码保存在一个 Python 文件中,例如 pg_agent.py。然后,您可以按如下方式训练代理:

    python pg_agent.py --env "LunarLanderContinuous-v2"
    
  8. 在 TensorBoard 上监控训练过程:

    tensorboard --logdir=~/ray_results
    

    训练进度将如下所示:

图 7.3 – Gym 中连续 Lunar Lander 环境下原始策略梯度代理的训练进度

图 7.3 – Gym 中连续 Lunar Lander 环境下原始策略梯度代理的训练进度

这就是关于原始策略梯度方法的全部内容!对于一个没有许多改进的算法来说,表现还不错,我们将在接下来的章节中引入这些改进。欢迎尝试在其他 Gym 环境中使用此算法。提示:Pendulum 环境可能会让您头疼。

提示

在使用 Tune 训练模型时,为了保存最佳表现的模型,您需要编写一个简单的包装训练函数,详细描述可以参考这里:github.com/ray-project/ray/issues/7983。每当您观察到评估得分的提升时,就保存模型。

接下来,我们将介绍一种更强大的算法类别:演员-评论家方法。

演员-评论家方法

演员-评论家方法为解决策略梯度算法中的高方差问题提出了进一步的补救措施。就像 REINFORCE 和其他策略梯度方法一样,演员-评论家算法已经存在了几十年。然而,将这种方法与深度强化学习结合,使其能够解决更现实的 RL 问题。我们将在本节开始时介绍演员-评论家方法背后的思想,随后会更详细地定义它们。

进一步减少基于策略方法中的方差

记住,之前为了减少梯度估计中的方差,我们将轨迹中获得的奖励总和替换为奖励累积项。虽然这朝着正确的方向迈出了第一步,但通常还不够。我们现在将介绍两种方法,进一步减少这个方差。

估计奖励的累积值

在一条轨迹中获得的奖励累积项,,是现有策略下的动作值 的估计值 。

信息

注意我们在此使用的动作值估计值 和 Q-learning 方法估计的值 之间的区别。前者估计的是现有行为策略下的动作值 ,而后者估计的是目标策略下的动作值,即 。

现在,每条访问特定 对的轨迹可能会产生不同的奖励累积估计值。这会增加梯度估计中的方差。如果我们能够在每次策略更新周期中使用一个固定的估计值来表示给定的 呢?这将消除由那些噪声奖励累积(动作值)估计值引起的方差。但是我们如何获得这样的估计值呢?答案是训练一个神经网络来为我们生成该估计值。然后我们使用采样的奖励累积值来训练这个网络。当我们查询它以获取特定状态-动作对的估计时,它会给我们一个单一的数字,而不是多个不同的估计,这反过来减少了方差。

本质上,这种网络的作用是评估策略,这就是为什么我们称之为评论家,而策略网络则告诉智能体如何在环境中行动——因此得名演员-评论家。

信息

我们不会在每次策略更新后从头开始训练评论家网络。相反,像策略网络一样,我们使用新的采样数据进行梯度更新。因此,评论家会偏向于旧的策略。然而,我们愿意做出这个权衡,以减少方差。

最后但同样重要的是,我们使用基准线来减少方差,接下来我们将详细讲解这一点。

使用基准线

策略梯度方法背后的直觉是,我们希望调整策略的参数,使得产生高奖励轨迹的动作变得更加可能,而产生低奖励轨迹的动作变得不太可能:

这种公式的一个缺点是,梯度步长的方向和大小在很大程度上由轨迹中的总奖励!决定。考虑以下两个例子:

  • 这是一个迷宫环境,智能体试图在最短时间内退出。奖励是直到到达出口所用的时间的负值。

  • 相同的迷宫环境,但奖励为$1M,每经过一秒钟就会扣除一美元的惩罚,直到到达出口。

在数学上,这两者是相同的优化问题。现在,考虑某个策略!下,特定轨迹会导致什么样的梯度步长,假设奖励结构有所不同。第一种奖励结构会导致所有轨迹的负梯度更新(尽管有些比其他的更小),而不管策略的质量如何;而第二种奖励结构几乎肯定会导致正梯度更新。此外,在后者的情况下,经过的时间的影响可以忽略不计,因为固定奖励太大,这使得策略网络的学习变得非常困难。

理想情况下,我们希望衡量在特定轨迹中观察到的相对奖励表现,即相对于其他轨迹,某个动作序列所带来的奖励。这样,我们就可以朝着那些产生高奖励轨迹的参数方向进行正梯度更新,而对于其他轨迹则进行负梯度更新。为了衡量相对表现,一个简单的技巧是从奖励和中减去基准!:

最直观的基准选择是平均轨迹奖励,样本采样如下:

事实证明,减去这样的项仍然可以给出梯度的无偏估计,但方差较小,而且在某些情况下,差异可能非常显著。因此,使用基准几乎总是好的。

当结合使用奖励未来预估时,基准的自然选择是状态价值,这样就得到了以下的梯度估计:

其中!是优势项,表示智能体通过在状态!采取动作!相较于遵循现有策略,能够获得多少收益。

由评论员估计优势,直接或间接地,产生了优势演员-评论员算法,接下来我们将介绍这一部分。

优势演员-评论员 – A2C

到目前为止,我们所涵盖的内容几乎足以组成所谓的 A2C 算法。让我们在详细讨论完整的算法和 RLlib 实现之前,再详细讨论一下如何估计优势项。

如何估计优势

使用评论家估计优势有不同的方法。评论家网络可以执行以下操作:

  • 直接估计 。

  • 估计 ,从中我们可以恢复 。

请注意,这两种方法都涉及到维护一个网络,其输出依赖于状态和动作。然而,我们可以用一个更简单的结构。记住动作值的定义:

当我们采样单步转移时,我们已经观察到了奖励和下一个状态,并获得了一个元组 。因此,我们可以按如下方式获得估计值 :

其中 是真实状态值 的某个估计值。

信息

注意符号中的细微差别。 和 表示真实值,而 和 是它们的估计值。 和 是随机变量,而 和 是它们的实现。

我们最终可以按如下方式估计优势:

这使我们能够使用一个仅仅估计状态值的神经网络来获得优势估计。为了训练这个网络,我们可以使用引导法来获得状态值的目标值。因此,使用采样元组 , 的目标被计算为 (与 的估计相同,因为我们碰巧从现有的随机策略中获得了动作)。

在展示完整的 A2C 算法之前,让我们先来看一下实现架构。

A2C 架构

A2C 提出不同代理之间同步采样,也就是说,所有的回合工作者在同一时间使用相同的策略网络来收集样本。然后,这些样本被传递给学习者,更新演员(策略网络)和评论家(价值网络)。从这个意义上讲,架构和普通的策略梯度基本相同,关于这一点我们上面已经给出了示意图。唯一的区别是,这次我们有一个评论家网络。那么,问题是如何将评论家网络引入其中。

演员和评论家的设计可以从完全隔离的神经网络(如下图左侧所示)到完全共享的设计(除了最后几层)不等:

图 7.4 – 隔离神经网络与共享神经网络

图 7.4 – 独立与共享神经网络

独立设计的优势在于它通常更稳定。这是因为演员和评论员目标的方差和目标值的大小可能非常不同。如果它们共享神经网络,就需要仔细调整学习率等超参数,否则学习可能会不稳定。另一方面,使用共享架构的优势在于可以进行交叉学习,并利用共同的特征提取能力。当特征提取是训练的重要部分时(例如当观测值为图像时),这一点尤其有用。当然,介于两者之间的任何架构也是可能的。

最后,是时候介绍 A2C 算法了。

A2C 算法

让我们将所有这些思想汇集在一起,形成 A2C 算法:

  1. 初始化演员和评论员网络, 和 。

    当某些停止准则未被满足时,执行以下操作:

  2. 使用 从(并行)环境收集一批 样本 。

  3. 获取状态值目标 。

  4. 使用梯度下降更新 ,以优化损失函数 ,例如平方损失。

  5. 获取优势值估计:

    。

  6. 计算

    。

  7. 更新

    。

  8. 将新的 广播到回滚工作者。

    结束时

请注意,我们也可以使用多步学习,而不是使用单步估计来计算优势估计和状态值目标。我们将在演员-评论员部分的最后呈现多步学习的广义版本。但现在,让我们来看一下如何使用 RLlib 的 A2C 算法。

使用 RLlib 的 A2C

在 RLlib 中使用 A2C 训练 RL 代理与我们为传统策略梯度所做的非常相似。因此,我们不会再次呈现完整的流程,而是简要描述它们之间的区别。主要的区别在于导入 A2C 类:

from ray.rllib.agents.a3c.a2c import (
    A2C_DEFAULT_CONFIG as DEFAULT_CONFIG,
    A2Ctrainer as trainer)

然后,您可以像传统策略梯度代理一样训练该代理。我们不会在这里展示我们的训练结果,而是会在本章结束时对所有算法进行比较。

接下来,我们介绍另一种著名的演员-评论员算法:A3C。

异步优势演员评论员:A3C

A3C 在损失函数和如何使用评论员方面与 A2C 非常相似。事实上,A3C 是 A2C 的前身,尽管我们为了教学目的将它们按相反顺序呈现。A2C 和 A3C 之间的区别在于架构,以及梯度的计算和应用方式。接下来,让我们讨论 A3C 的架构。

A3C 架构

A3C 架构与 A2C 的不同之处如下:

  • A3C 中的异步性是因为回滚工作者以自己的节奏从主策略网络拉取参数!,而不是与其他工作者同步。

  • 结果是,工作者们可能会同时使用不同的策略。

  • 为了避免在中央学习者计算梯度时使用可能已经在不同策略下获得的样本,梯度是由回滚工作者根据当时在工作者中使用的策略参数计算的。

  • 因此,传递给学习者的不是样本,而是梯度。

  • 这些梯度被应用到主策略网络,再次是异步地应用,随着它们到达。

下图描绘了 A3C 架构:

图 7.5 – A3C 架构

图 7.5 – A3C 架构

A3C 有两个重要的缺点:

  • 更新主策略网络的梯度可能已经过时,并且是在与主策略网络中不同的!下获得的。这在理论上是有问题的,因为这些不是策略网络参数的真实梯度。

  • 传递梯度,尤其是当神经网络很大时,梯度可能是一个巨大的数字向量,这可能会产生相对于仅传递样本的显著通信开销。

尽管存在这些缺点,A3C 的主要动机是获得不相关的样本和梯度更新,类似于经验回放在深度 Q 学习中的作用。另一方面,在许多实验中,人们发现 A2C 与 A3C 一样好,有时甚至更好。因此,A3C 并不常用。不过,我们展示它是为了让你理解这些算法是如何发展的,以及它们之间的关键区别。让我们也看看如何使用 RLlib 的 A3C 模块。

使用 RLlib 的 A3C

RLlib 的 A3C 算法可以通过导入相应的训练器类轻松访问:

from ray.rllib.agents.a3c.a3c import (
    DEFAULT_CONFIG,
    A3CTrainer as trainer)

然后,您可以按照我们之前提供的代码训练一个代理。

最后,我们将在本节结束时讨论政策梯度方法背景下的多步 RL 的概括。

广义优势估计器

我们之前提到,您可以使用多步估计来计算优势函数。也就是说,不仅仅使用单步过渡,如下所示:

使用!-步过渡可能会提供更准确的优势函数估计:

当然,当 时,我们实际上回到了使用采样奖励的方法,这个方法我们曾经放弃过,以减少优势估计中的方差。另一方面, 可能会对现有的 估计引入过多的偏差。因此,超参数 是控制偏差-方差权衡的一种方式,同时估计优势。

自然的问题是,是否必须在优势估计中使用“单一的” 。例如,我们可以使用 、 和 计算优势估计,并取其平均值。那么,取所有可能的 的加权平均(凸组合)怎么样呢?这正是广义优势估计器(GAE)所做的。更具体地说,它以指数衰减的方式加权 项:

其中 和 是超参数。因此,GAE 为我们提供了另一个控制偏差-方差权衡的调节器。具体来说, 会导致 ,这具有较高的偏差;而 导致 ,这相当于使用采样奖励减去基线,这具有较高的偏差。任何值的 都是两者之间的折衷。

我们通过注意到,你可以通过配置标志"use_gae"和"lambda"来开启或关闭 RLlib 中的 GAE(广义优势估计)功能,这适用于其演员-评论家实现。

这就结束了我们对演员-评论家函数的讨论。接下来,我们将研究一种名为信赖域方法的最新方法,它已在 A2C 和 A3C 之上带来了显著的改进。

信赖域方法

在基于策略的方法中,一个重要的发展是信赖域方法的演变。特别是,TRPO 和 PPO 算法相较于 A2C 和 A3C 等算法带来了显著的改进。例如,著名的 Dota 2 AI 代理,达到了比赛中的专家级表现,就是使用 PPO 和 GAE 进行训练的。在这一部分,我们将深入探讨这些算法的细节,帮助你更好地理解它们是如何工作的。

信息

Sergey Levine 教授,TRPO 和 PPO 论文的共同作者,在他的在线讲座中详细讲解了这些方法背后的数学内容,讲解的深度超过了我们在本节中的内容。该讲座可通过youtu.be/uR1Ubd2hAlE观看,我强烈推荐你观看它,以提升你对这些算法的理论理解。

不再赘述,让我们直接深入探讨!

策略梯度作为策略迭代

在前面的章节中,我们描述了大多数 RL 算法可以看作是一种策略迭代的形式,在策略评估和改进之间交替进行。你可以在相同的背景下理解策略梯度方法:

  • 样本收集和优势估计:策略评估

  • 梯度步长:策略改进

现在我们将使用策略迭代的观点来为即将到来的算法做铺垫。首先,让我们看看如何量化 RL 目标中的改进。

量化改进

一般来说,策略改进步骤的目标是尽可能改进现有策略。更正式地说,目标如下:

其中 是现有的策略。利用 的定义和一些代数运算,我们可以推导出以下结果:

让我们解读一下这个公式告诉我们的信息:

  • 新策略 相对于现有策略 的改进可以通过现有策略下的优势函数来量化。

  • 我们在这个计算中需要的期望运算是在新策略下的 。

请记住,完全计算这样的期望或优势函数几乎从来不可行。我们总是使用现有策略进行估计,在这个案例中是 ,并与环境进行交互。现在,前者是一个愉快的点,因为我们知道如何利用样本估计优势——上一节讲的正是这个内容。我们也知道,我们可以收集样本来估计期望,这就是我们在 中所做的。然而,问题在于,这个期望是相对于一个新策略 计算的。我们并不知道 是什么,事实上,这正是我们试图找出的问题。因此,我们不能使用 从环境中收集样本。从现在开始,我们将讨论如何绕过这个问题,以便我们能够逐步改进策略。

去除

让我们展开这个期望,并用组成 的边际概率表示它:

它使用以下公式:

我们可以通过重要性采样去除内层期望中的 :

现在,去除外层期望中的 是最具挑战性的部分。关键思想是在优化过程中保持与现有策略“足够接近”,也就是说, 。在这种情况下,可以证明 ,并且我们可以将前者替换为后者。

这里的一个关键问题是如何衡量策略的“接近度”,以确保前面的近似是有效的。由于其良好的数学性质,一个常用的度量方法是库尔巴克-莱布勒(KL)散度。

信息

如果你对 KL 散度不太熟悉,或者需要复习一下,可以参考这里的一个很好的解释:youtu.be/2PZxw4FzDU?t=226。

利用策略之间接近的近似,并界定这种接近性,结果得到了以下优化函数:

其中 是某个界限。

接下来,让我们看看如何使用其他近似方法来进一步简化这个优化问题。

在优化中使用泰勒级数展开

现在我们有了一个函数 ,并且我们知道它是在接近另一个点 处被评估的,这应该让你想到使用泰勒级数展开。

信息

如果你需要复习泰勒级数,或者加深你的直觉,一个很好的资源是这个视频:youtu.be/3d6DsjIBzJ4。我还推荐订阅该频道——3Blue1Brown 是一个非常好的视觉化数学概念资源。

在 处的一阶展开如下:

注意,第一项与 无关,因此我们可以在优化中去掉它。还要注意,梯度项是相对于 而不是 的,这应该会很有帮助。

我们的目标变成了以下内容:

最后,让我们看看如何计算梯度项。

计算

首先,让我们看看 长什么样。记住,我们可以写出以下公式:

因为根据定义,。然后我们可以写成:

现在,记住我们在寻找的是 ,而不是 。将所有的 替换为 ,得到如下结果:

我们得到了一个看起来应该很熟悉的结果! 正是进入优势行为者-批评方法中梯度估计 的内容。最大化策略更新间的政策改进目标使我们得到了一个与常规梯度上升方法相同的目标。当然,我们不能忘记约束条件。所以,我们要解决的优化问题变成了以下内容:

这是一个关键结果!让我们梳理一下目前为止得到的内容:

  • 常规的演员-评论员方法与梯度上升法和信任域方法具有相同的目标,即朝着梯度的方向移动。

  • 信任域方法的目标是通过限制 KL 散度,保持接近现有策略。

  • 另一方面,常规的梯度上升法则是按照特定的步长朝着梯度的方向移动,就像在中那样。

  • 因此,常规梯度上升法的目标是将保持得尽可能接近,而不是将保持得接近。

  • 在中为所有维度使用相同的步长,就像常规梯度上升法那样,可能会导致收敛速度非常慢,或者根本不收敛,因为参数向量中的某些维度可能对策略(变化)有比其他维度更大的影响。

    提示

    信任域方法的关键目标是在更新策略到某个时,保持足够接近现有策略。这不同于简单地将保持得接近,这是常规梯度上升法的做法。

所以,我们知道和应该是接近的,但我们尚未讨论如何实现这一点。事实上,TRPO 和 PPO 这两种不同的算法将以不同的方式处理这一要求。接下来,我们将深入讨论 TRPO 算法的细节。

TRPO – 信任域策略优化

TRPO 是一个重要的算法,它在 PPO 之前。让我们在本节中理解它如何处理我们上面提到的优化问题,以及 TRPO 解决方案的挑战是什么。

处理 KL 散度

TRPO 算法通过其二阶泰勒展开近似 KL 散度约束:

其中被称为费舍尔信息矩阵,其定义如下:

其中期望值需要从样本中估计。请注意,如果是一个维的向量,那么就会变成一个矩阵。

信息

费舍尔信息矩阵是一个重要的概念,你可能会想了解更多,维基百科页面是一个很好的起点:en.wikipedia.org/wiki/Fisher_information。

这种近似方法导致了以下的梯度更新步骤(我们省略了推导过程):

其中和是超参数,和。

如果这让你感到害怕,你并不孤单!TRPO 确实不是一个最容易实现的算法。接下来,让我们看看 TRPO 所涉及的挑战。

TRPO 的挑战

下面是实现 TRPO 时遇到的一些挑战:

  • 由于 KL 散度约束是通过其二阶泰勒展开式近似的,因此可能会出现违反约束的情况。

  • 这时,项就发挥作用了:它会缩小梯度更新的幅度,直到满足约束条件。为此,在估算和之后,会进行一次线搜索:从开始,每次增加 1,直到更新的幅度足够小,以使约束条件得到满足。

  • 请记住,是一个矩阵,这个矩阵的大小取决于策略网络的大小,因此可能会非常庞大,存储起来也非常昂贵。

  • 由于是通过样本估算的,鉴于其大小,估算过程中可能会引入很多不准确性。

  • 计算和存储是一个更加痛苦的步骤。

  • 为了避免处理和的复杂性,作者使用了共轭梯度算法,这允许你在不构建整个矩阵和求逆的情况下进行梯度更新。

如你所见,实施 TRPO 可能会很复杂,我们省略了其实现的细节。这就是为什么沿着相同思路工作的更简单算法 PPO 更加流行和广泛使用的原因,接下来我们会介绍它。

PPO – 近端策略优化

PPO 再次通过最大化策略改进目标来激励:

同时保持接近。PPO 有两个变种:PPO-Penalty 和 PPO-Clip。后者更简单,我们将在这里重点介绍它。

PPO-clip 代理目标

相比 TRPO,通过剪辑目标函数来实现旧策略和新策略之间的接近度是一种更简单的方法,这样偏离现有策略就不会带来额外的好处。更正式地说,PPO-clip 在这里最大化代理目标:

其中定义如下:

这简单地说,如果优势是正的,那么最小化过程就会变为:

因此,这会限制比率能达到的最大值。这意味着,即使趋势是增加在状态中采取行动的可能性,因为它对应于一个正的优势,我们也会限制这种可能性偏离现有策略的程度。因此,进一步的偏离不会对优势产生贡献。

相反,如果优势是负的,表达式将变为:

类似地,它限制了在状态 下采取行动的可能性 的减少。这一比率受 的限制。

接下来,让我们列出完整的 PPO 算法。

PPO 算法

PPO 算法的工作流程如下:

  1. 初始化演员和评论员网络, 和 。

    当某个停止准则未满足时,执行以下操作:

  2. 从(并行)环境中收集一批 样本 ,使用 。

  3. 获取状态值目标 。

  4. 使用梯度下降法更新 ,以最小化损失函数 ,例如平方损失。

  5. 获取优势值估计

    。

  6. 朝着最大化代理目标函数的方向执行梯度上升步骤

    并更新 。虽然我们没有提供这种梯度更新的显式形式,但可以通过如 TensorFlow 等包轻松实现。

  7. 将新的 广播到执行的工作者。

    当满足某停止准则时结束

最后,请注意,PPO 实现的架构与 A2C 非常相似,采用同步采样和策略更新的执行工作者。

使用 RLlib 的 PPO

非常类似于我们如何为早期算法导入代理训练器类,PPO 类可以通过以下方式导入:

from ray.rllib.agents.ppo.ppo import (
    DEFAULT_CONFIG,
    PPOTrainer as trainer)

我们将在本章稍后展示训练结果。

这就是我们关于信任域方法讨论的结束。本章最后一类算法将介绍基于策略的离策略方法。

离策略方法

基于策略的方法的一个挑战是它们是基于策略的,这要求在每次策略更新后收集新的样本。如果从环境中收集样本的成本很高,那么基于策略的方法的训练可能会非常昂贵。另一方面,我们在上一章中讨论的基于价值的方法是离策略的,但它们仅适用于离散的动作空间。因此,需要一种适用于连续动作空间并且是离策略的方法。在本节中,我们将介绍这类算法。让我们从第一个算法开始:深度确定性策略梯度(DDPG)。

DDPG – 深度确定性策略梯度

从某种意义上讲,DDPG 是深度 Q 学习对连续动作空间的扩展。记住,深度 Q 学习方法学习动作值的表示,。然后,在给定状态 中,最佳动作由 给出。现在,如果动作空间是连续的,学习动作值的表示就不是问题。然而,接着,执行最大操作来获取连续动作空间中的最佳动作会非常繁琐。DDPG 解决了这个问题。接下来我们来看它是如何处理的。

DDPG 如何处理连续动作空间

DDPG 只是学习另一个近似,即 ,它估计在给定状态下的最佳动作。如果你想知道为什么这样可行,可以考虑以下的思考过程:

  • DDPG 假设连续动作空间对 可微。

  • 暂时假设动作值 是已知的或已经学习过的。

  • 然后,问题就简单变成了学习一个函数近似,即 ,其输入为 ,输出为 ,参数为 。优化过程的“奖励”则由 提供。

  • 因此,我们可以使用梯度上升方法来优化 。

  • 随着时间的推移,学习到的动作值希望能够收敛,并且成为策略函数训练中的不变目标。

    信息

    由于 DDPG 假设策略函数相对于动作是可微的,因此它只能用于连续的动作空间。

接下来,让我们深入了解一些关于 DDPG 算法的更多细节。

DDPG 算法

由于 DDPG 是深度 Q 学习的扩展,并且学习了一个策略函数,因此我们不需要在这里写出完整的算法。此外,我们在上一章讨论的许多方法,如优先经验回放和多步学习,可以用来构成 DDPG 的变种。而原始的 DDPG 算法则更接近 DQN 算法,并使用以下内容:

  • 一个回放缓冲区用于存储经验元组,并从中均匀地随机进行采样。

  • 一个目标 网络,它通过使用 Polyak 平均方法更新,如 ,而不是每隔 步骤与行为网络同步。

DDPG 然后用以下公式替换了 DQN 算法中的目标计算:

替换成这个:

DQN 和 DDPG 之间的另一个重要区别在于,DQN 在训练过程中使用-贪心策略。然而,DDPG 中的策略网络为给定状态提供一个确定性的动作,因此名字中有“deterministic”一词。为了在训练过程中进行探索,会向动作中添加一些噪声。更正式地说,动作的获取方式如下:

其中可以选择为白噪声(尽管原始实现使用了所谓的 OU 噪声)。在此操作中,和代表连续动作空间的边界。

这就是 DDPG 的核心内容。接下来,我们将探讨如何对其进行并行化。

Ape-X DDPG

鉴于深度 Q 学习和 DDPG 之间的相似性,DDPG 的并行化可以通过 Ape-X 框架轻松实现。事实上,原始的 Ape-X 论文将 DDPG 与 DQN 一起展示在他们的实现中。它在某些基准测试中将常规 DDPG 的性能提高了几个数量级。作者还表明,随着回滚工人(演员)数量的增加,实际运行时间的性能持续提高。

使用 RLlib 的 DDPG 和 Ape-X DPG

可以按如下方式导入 DDPG 的训练器类和配置:

from ray.rllib.agents.ddpg.ddpg import(
    DEFAULT_CONFIG,
    DDPGTrainer as trainer)

同样,对于 Ape-X DDPG,我们引入如下内容:

from ray.rllib.agents.ddpg.apex import (
    APEX_DDPG_DEFAULT_CONFIG as DEFAULT_CONFIG,
    ApexDDPGTrainer as trainer) 

就是这样!其余部分与我们在章节开始时描述的训练流程几乎相同。现在,在深入讨论改进 DDPG 的算法之前,我们先来探讨一下 DDPG 算法的不足之处。

DDPG 的缺点

尽管最初受到欢迎,但该算法仍然存在几个问题:

  • 它对超参数的选择可能非常敏感。

  • 它在学习行动价值时会遇到最大化偏差的问题。

  • 行动价值估计中的峰值(可能是错误的)被策略网络利用,进而干扰学习过程。

接下来,我们将研究 TD3 算法,它引入了一系列改进来解决这些问题。

TD3 – 双延迟深度确定性策略梯度

TD3 算法的关键在于,它解决了 DDPG 中的函数逼近误差。因此,它在 OpenAI 的连续控制基准测试中,大大超过了 DDPG、PPO、TRPO 和 SAC,达到了最大奖励。让我们看看 TD3 提出了什么改进。

TD3 相较于 DDPG 的改进

TD3 相较于 DDPG 有三个主要改进:

  • 它学习两个(双重)Q 网络,而不是一个,从而创建两个目标 Q 网络。然后,目标使用以下方式获得:

其中是给定状态的目标动作。这是一种双 Q 学习的形式,用于克服最大化偏差。

  • 在训练过程中,策略和目标网络的更新速度比 Q 网络更新慢,推荐的周期是每进行两次 Q 网络更新后进行一次策略更新,因此算法名称中有“延迟”一词。

  • 目标动作,,是在策略网络的输出中添加一些噪声后得到的:

其中 是一些白噪声。请注意,这与用于环境探索的噪声不同。此噪声的作用是作为正则化项,防止策略网络利用 Q 网络错误估算为非常高且在其区域内不平滑的某些动作值。

信息

就像 DDPG 一样,TD3 只能用于连续动作空间。

接下来,让我们看看如何使用 RLlib 的 TD3 实现来训练一个 RL 代理。

使用 RLlib 的 TD3

可以按如下方式导入 TD3 训练器类:

from ray.rllib.agents.ddpg.td3 import (
    TD3_DEFAULT_CONFIG as DEFAULT_CONFIG,
    TD3Trainer as trainer)

另一方面,如果你查看 RLlib 中 td3.py 模块的代码,你会发现它只是修改了默认的 DDPG 配置,并在后台使用了 DDPG 训练器类。这意味着 TD3 的改进可以在 DDPG 训练器类中选择性地使用,你可以修改它们来获得 Ape-X TD3 变体。

这就是 TD3 的全部内容。接下来,我们将讨论 SAC。

SAC – Soft actor-critic(软演员-评论家)

SAC 是另一个流行的算法,它对 TD3 进行了进一步的改进。它使用熵作为奖励的一部分来鼓励探索:

其中 是熵项, 是相应的权重。

提示

SAC 可以同时用于连续和离散动作空间。

要导入 SAC 训练器,可以使用以下代码:

from ray.rllib.agents.sac.sac import (
    DEFAULT_CONFIG,
    SACTrainer as trainer)

我们将讨论的最后一个算法是 IMPALA。

IMPALA – 基于重要性加权的演员-学习者架构

IMPALA 是一种基于策略梯度的算法,而 DDPG、TD3 和 SAC 本质上是基于值的方法。因此,IMPALA 并不完全是一个离策略方法。实际上,它类似于 A3C,但有以下几个关键区别:

  • 与 A3C 不同,它发送采样经验(异步)给学习者,而不是参数梯度。这显著减少了通信开销。

  • 当一个样本轨迹到达时,它很可能是通过一个比学习者当前策略更新滞后几个步骤的策略获得的。IMPALA 使用截断的重要性采样来考虑策略滞后,同时计算价值函数目标。

  • IMPALA 允许多个同步的工作学习者从样本中计算梯度。

可以按如下方式将 IMPALA 训练器类导入到 RLlib 中:

from ray.rllib.agents.impala.impala import (
    DEFAULT_CONFIG,
    ImpalaTrainer as trainer)

这就是我们关于算法讨论的总结。接下来是有趣的部分!让我们在 OpenAI 的连续控制 Lunar Lander 环境中比较它们的表现!

Lunar Lander 中基于策略的方法对比

下面是不同基于策略的算法在单次训练过程中在月球着陆器环境中的评估奖励表现进展对比:

图 7.6 – 各种基于策略的算法在月球着陆器训练中的表现

图 7.6 – 各种基于策略的算法在月球着陆器训练中的表现

为了让你更清楚每次训练耗时以及训练结束时的表现,以下是前述图表的 TensorBoard 工具提示:

图 7.7 – 实际时间和训练结束时的性能对比

图 7.7 – 实际时间和训练结束时的性能对比

在进一步讨论之前,让我们先做以下免责声明:这里的对比不应被视为不同算法的基准测试,原因有多方面:

  • 我们没有进行任何超参数调整,

  • 这些图表来自每个算法的单次训练实验。训练一个强化学习(RL)智能体是一个高度随机的过程,公平的比较应包括至少 5-10 次训练实验的平均结果。

  • 我们使用的是 RLlib 中这些算法的实现,这些实现可能比其他开源实现效率更高或更低。

在这个免责声明之后,让我们讨论一下这些结果中的观察:

  • PPO 在训练结束时获得了最高奖励。

  • 传统的策略梯度算法在达到“合理”奖励时是最快的(从实际时间角度来看)。

  • TD3 和 DDPG 在实际时间方面非常慢,尽管它们的奖励高于 A2C 和 IMPALA。

  • 相比其他算法,TD3 的训练图表明显更不稳定。

  • 在某些时刻,TD3 在相同样本数量下的奖励超过了 PPO。

  • IMPALA 在达到(并超越)2M 样本时非常迅速。

你可以扩展这个列表,但这里的核心思想是,不同算法可能有不同的优缺点。接下来,我们来讨论你在为应用选择算法时应该考虑哪些标准。

如何选择合适的算法

和所有机器学习领域一样,针对不同应用的算法选择没有“银弹”。你应该考虑多个标准,在某些情况下,某些标准可能比其他标准更为重要。

选择算法时,以下是你应该关注的不同算法表现维度:

  • 最高奖励:当你不受计算和时间资源限制,且目标仅仅是为你的应用训练出最优秀的智能体时,最高奖励是你应关注的标准。在这种情况下,PPO 和 SAC 是很有前景的选择。

  • 样本效率:如果您的采样过程耗费时间或成本高昂,那么样本效率(使用较少样本实现更高奖励)就变得很重要。在这种情况下,您应该考虑离策略算法,因为它们可以重用过去的经验进行训练,而在策略方法中,样本的使用通常是极其浪费的。在这种情况下,SAC 是一个很好的起点。

  • 墙钟效率:如果您的模拟器速度快和/或您有资源进行大规模并行化,那么 PPO、IMPALA 和 Ape-X SAC 通常是一个不错的选择。

  • 稳定性:在不运行多次试验的情况下获得良好的奖励,并且在训练过程中稳步提高,也是很重要的。离策略算法可能难以稳定。在这方面,PPO 通常是一个不错的选择。

  • 泛化能力:如果一个算法需要为每个训练环境进行广泛调整,这可能会耗费大量时间和资源。由于其在奖励中使用熵,SAC 被认为对超参数选择不太敏感。

  • 简单性:拥有一个易于实现的算法对于避免错误和确保可维护性至关重要。这就是为什么 TRPO 被 PPO 所取代而被放弃的原因。

这就是算法选择标准讨论的结束。最后,让我们进入一些资源,您可以找到这些算法易于理解的实现。

政策梯度方法的开源实现

在本章中,我们涵盖了许多算法。由于空间限制,我们无法显式实现所有这些算法。相反,我们依赖 RLlib 的实现来训练我们的用例代理。RLlib 是开源的,因此您可以访问github.com/ray-project/ray/tree/ray-0.8.5/rllib,并深入研究这些算法的实现。

话虽如此,RLlib 的实现是为生产系统而构建的,因此涉及许多其他关于错误处理和预处理的实现。此外,还有很多代码复用,导致了具有多个类继承的实现。OpenAI 的 Spinning Up 库提供了一个更简单的实现集合,可访问github.com/openai/spinningup,我强烈推荐你进入该库,并深入研究我们在本章讨论的算法的实现细节。

信息

OpenAI 的 Spinning Up 也是一个很好的资源,可以查看强化学习主题和算法的概述,可访问spinningup.openai.com。

就是这样!我们已经走过了很长的路,并深入讨论了基于策略的方法。祝贺您达到了这个重要的里程碑!

摘要

在本章中,我们介绍了一类重要的算法,称为基于策略的方法。这些方法直接优化策略网络,与我们在上一章介绍的基于价值的方法不同。因此,它们具有更强的理论基础。此外,它们可以用于连续的动作空间。通过这一部分,我们详细介绍了无模型方法。在下一章中,我们将深入探讨基于模型的方法,旨在学习智能体所处环境的动态。

参考文献

第八章:第八章:基于模型的方法

到目前为止,我们所讨论的所有深度强化学习(RL)算法都是无模型的,这意味着它们并不假设任何关于环境过渡动态的知识,而是从采样的经验中学习。事实上,这种做法是故意有意偏离了动态规划方法,从而避免了需要环境模型的要求。在本章中,我们稍微将方向调整,讨论一类依赖于模型的方法,称为基于模型的方法。这些方法在某些问题中能显著提高样本效率,效率提升幅度可能达到几个数量级,这使得它成为一种非常有吸引力的方法,特别是在像机器人技术这样收集经验代价高昂的情况下。话虽如此,我们仍然不会假设我们有一个现成的模型,而是会讨论如何学习一个模型。一旦我们拥有了模型,它可以用于决策时的规划,并改善无模型方法的性能。

本章重要内容包括以下主题:

  • 介绍基于模型的方法

  • 通过模型进行规划

  • 学习世界模型

  • 统一基于模型和无模型的方法

让我们开始吧。

技术要求

本章的代码可以在本书的 GitHub 仓库找到,网址为github.com/PacktPublishing/Mastering-Reinforcement-Learning-with-Python。

介绍基于模型的方法

想象一下,你正驾驶汽车在一条没有分隔带的道路上行驶,突然,另一辆汽车在与你对向而来的车道上快速行驶,并且它正在超车一辆卡车。你的大脑可能会自动模拟不同的情境,预测接下来可能发生的情景:

  • 另一辆车可能会立刻回到它的车道上,或者加速尽快超车。

  • 另一种情境可能是汽车朝着右侧行驶,但这种情况不太可能发生(在右侧通行的交通流中)。

驾驶员(可能是你)接着评估每种情景的可能性和风险,以及他们可能采取的行动,并做出决定,安全地继续行驶。

在一个不太夸张的例子中,考虑一场象棋比赛。在做出一步棋之前,棋手会在脑海中“模拟”多种场景,并评估几步之后可能的结果。事实上,能够准确评估更多的可能场景,会增加赢得比赛的机会。

在这两个例子中,决策过程涉及到想象多个“虚拟”的环境演化,评估不同的备选方案,并根据情况采取适当的行动。但我们是怎么做到这一点的呢?我们之所以能够这样做,是因为我们对生活的世界有一个心理模型。在驾驶汽车的例子中,驾驶员会对可能的交通行为、其他驾驶员的动作以及物理规律有一定了解。在国际象棋的例子中,玩家知道游戏的规则,哪些走法是好的,并且可能知道某个玩家可能使用的策略。这种“基于模型”的思维几乎是我们规划行动的自然方式,不同于不利用关于世界运作的先验知识的无模型方法。

基于模型的方法由于利用了更多关于环境的信息和结构,可能比无模型方法更具样本效率。这在样本采集代价昂贵的应用场景中尤为有用,比如机器人技术。因此,这是我们在本章讨论的一个重要话题。我们将重点讨论基于模型方法的两个主要方面:

  • 如何使用环境模型(或我们所称的世界模型)来实现最优的行动规划

  • 当没有模型时,如何学习这样一个模型

在下一部分,我们从前者开始,介绍在有模型时进行规划的一些方法。一旦我们确信学习环境模型是值得的,并且确实可以通过最优规划方法获得良好的行动,我们将讨论如何学习这些模型。

通过模型进行规划

在这一部分,我们首先定义通过模型进行规划在最优控制中的含义。然后,我们将介绍几种规划方法,包括交叉熵法和协方差矩阵自适应进化策略。你还将看到如何使用 Ray 库将这些方法并行化。现在,让我们开始定义问题。

定义最优控制问题

在强化学习(RL)或一般的控制问题中,我们关心的是智能体所采取的行动,因为我们希望完成某个任务。我们将这个任务表达为一个数学目标,以便我们可以使用数学工具来找到实现任务的行动——在强化学习中,这就是预期的累积折扣奖励总和。你当然已经了解了这一点,因为这正是我们一直在做的事情,但现在是时候重申一下了:我们实际上是在解决一个优化问题。

现在,假设我们试图为一个有时间步的任务找出最佳行动。作为例子,你可以想象 Atari 游戏、Cartpole、自动驾驶汽车、网格世界中的机器人等等。我们可以如下定义优化问题(使用Levine, 2019中的符号):

这所有的意思是如何找到一系列动作,其中 对应于时间步长 处的动作,能够在 步内最大化得分。这里需要注意的是, 可能是多维的(例如,),如果在每个步骤中有多个动作(比如汽车中的转向和加速/刹车决策)。我们还可以用 表示一系列 个动作。因此,我们关注的是找到一个 ,它能够最大化 。

在这一点上,我们可以选择不同的优化和控制方式。接下来我们将详细探讨这些。

基于导数和无导数优化

当我们遇到一个优化问题时,解决它的自然反应可能是“我们取一阶导数,设其为零”,以此类推。但不要忘了,大多数时候,我们并没有像 这样的封闭式数学表达式可以求导。以玩 Atari 游戏为例。我们可以通过玩游戏来评估在给定的 下, 的值,但我们无法计算任何导数。这一点对于我们能采用的优化方法至关重要,特别需要注意以下几种类型:

  • 基于导数的方法需要对目标函数进行求导,以便优化它。

  • 无导数方法依赖于系统地反复评估目标函数,以寻找最佳输入。

因此,我们将在这里依赖后者。

这部分讲的是我们将使用哪种优化过程,最终它给我们带来一些 。另一个重要的设计选择是如何执行它,接下来我们将讨论这个问题。

开环、闭环和模型预测控制

让我们从一个例子开始解释不同类型的控制系统:假设我们有一个代理,它是一个前锋位置的足球运动员。为了简单起见,我们假设该代理的唯一目标是在接到球时进球。在拥有球的第一时刻,代理可以执行以下任意操作:

  • 想出一个计划来得分,闭上眼睛和耳朵(也就是任何感知手段),然后执行计划直到结束(无论是得分还是失误)。

  • 或者,代理可以保持感知手段活跃,并根据环境发生的最新信息修改计划。

前者是开环控制的一个例子,在采取下一动作时不会使用来自环境的反馈,而后者则是闭环控制的例子,它使用环境反馈。一般来说,在强化学习中,我们使用的是闭环控制。

提示

使用闭环控制的优点是能够在规划时考虑最新的信息。这在环境和/或控制器动态不确定的情况下尤其有优势,因为在这种情况下,完美的预测是不可能的。

现在,智能体可以以不同的方式使用反馈,即在时间点 从环境中获得的最新观察结果。具体来说,智能体可以执行以下操作:

  • 从给定 的策略 中选择一个动作,即 。

  • 解决优化问题,找到后续 时间步的 。

后者被称为模型预测控制(MPC)。重申一下,在 MPC 中,智能体重复执行以下循环:

  1. 为接下来的 步骤制定一个最佳控制计划。

  2. 执行第一步的计划。

  3. 继续执行下一步。

请注意,到目前为止我们提出的优化问题尚未给出一个策略 。相反,我们将使用无导数优化方法来搜索一个好的 。

接下来,让我们讨论一种非常简单的无导数方法:随机射击。

随机射击

随机射击程序仅涉及以下步骤:

  1. 生成一堆候选动作序列,均匀地随机生成,例如生成 个。

  2. 评估每一个 。

  3. 采取行动 ,它给出最佳的 ,即 。

如你所见,这不是一个特别复杂的优化过程。然而,它可以作为一个基准,供我们与更复杂的方法进行比较。

提示

基于随机搜索的方法可能比你想象的更有效。Mania 等人在他们的论文《简单随机搜索提供了一种竞争性的 RL 方法》中概述了这种方法,用于优化策略参数,正如其名字所示,产生了一些令人惊讶的好结果(Mania 等人, 2018)。

为了使我们的讨论更加具体,让我们引入一个简单的例子。在此之前,我们需要设置将在本章中使用的 Python 虚拟环境。

设置 Python 虚拟环境

你可以在虚拟环境中安装我们需要的包,方法如下:

$ virtualenv mbenv
$ source mbenv/bin/activate
$ pip install ray[rllib]==1.0.1
$ pip install tensorflow==2.3.1
$ pip install cma==3.0.3
$ pip install gym[box2d]

现在,我们可以继续我们的示例了。

简单的加农炮射击游戏

我们中的一些人足够老,曾经在 Windows 3.1 或 95 上玩过经典的Bang! Bang!游戏。游戏的玩法非常简单,就是通过调整火炮弹的射击角度和速度来击中对手。在这里,我们将玩一个更简单的游戏:我们有一门火炮,可以调整射击角度()。我们的目标是最大化距离,即弹丸在平面上行进的距离,初速度为固定值。如图 8.1所示:

图 8.1 – 通过调整射击角度来最大化的简单火炮射击游戏

](https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/ms-rl-py/img/B14160_08_001.jpg)

图 8.1 – 通过调整来最大化的简单火炮射击游戏

现在,如果你记得一些高中数学,你会意识到这实际上并不是一个游戏:最大距离可以通过设置来实现。好吧,假设我们不知道这一点,并用这个例子来说明我们目前已经介绍的概念:

  • 动作是,即火炮的角度,这是一个标量。

  • 这是一个单步问题,也就是说,我们只采取一个动作,游戏就结束了。因此,和。

现在我们来编写代码:

  1. 我们可以访问环境,它将评估我们考虑的动作,在我们实际执行之前。我们并不假设知道环境内所有数学方程式和动态的定义。换句话说,我们可以调用black_box_projectile函数来获取,对于我们选择的,以及固定的初速度和重力:

    from math import sin, pi
    def black_box_projectile(theta, v0=10, g=9.81):
        assert theta >= 0
        assert theta <= 90
        return (v0 ** 2) * sin(2 * pi * theta / 180) / g
    
  2. 对于随机射击过程,我们只需在和之间均匀随机地生成个动作,像这样:

    import random
    def random_shooting(n, min_a=0, max_a=90):
        return [random.uniform(min_a, max_a) for i in range(n)]
    
  3. 我们还需要一个函数来评估所有候选动作并选择最好的一个。为此,我们将定义一个更通用的函数,稍后会用到。它将选择最好的 精英,即最佳动作:

    import numpy as np
    def pick_elites(actions, M_elites):
        actions = np.array(actions)
        assert M_elites <= len(actions)
        assert M_elites > 0
        results = np.array([black_box_projectile(a)
                            for a in actions])
        sorted_ix = np.argsort(results)[-M_elites:][::-1]
        return actions[sorted_ix], results[sorted_ix]
    
  4. 寻找最佳动作的循环就很简单:

    n = 20
    actions_to_try = random_shooting(n)
    best_action, best_result = pick_elites(actions_to_try, 1)
    

就是这样。要执行的动作就是best_action[0]。重申一遍,到目前为止,我们并没有做任何特别有趣的事。这里只是用来说明这些概念,并为即将到来的更有趣的方法做准备。

交叉熵方法

在火炮射击的例子中,我们评估了在寻找最佳动作过程中生成的一些动作,这个最佳动作恰好是。正如你可以想象的那样,我们可以在搜索中更加智能。例如,如果我们有预算生成并评估个动作,为什么要盲目地用均匀生成的动作呢?相反,我们可以采取以下方法:

  1. 首先生成一些动作(这可以通过均匀随机的方式完成)。

  2. 查看动作空间中哪个区域似乎给出了更好的结果(在炮弹射击示例中,这个区域大约在 附近)。

  3. 在动作空间的该部分生成更多动作。

我们可以重复这个过程来引导我们的搜索,这将更有效地使用我们的搜索预算。事实上,这正是交叉熵方法(CEM)所建议的!

我们之前对 CEM 的描述有些模糊。更正式的描述如下:

  1. 初始化一个概率分布 ,参数为 。

  2. 从 , 中生成 个样本(解,动作)。

  3. 按照奖励从高到低对解进行排序,索引为 , 。

  4. 选择最佳的 解,精英, ,并将分布 拟合到精英集合中。

  5. 返回到第 2 步并重复,直到满足停止准则。

该算法特别通过将概率分布拟合到当前迭代中最佳的动作,来识别最佳的动作区域,并从中采样下一代动作。由于这种进化性质,它被视为进化策略(ES)。

提示

当搜索的维度,即动作维度乘以 ,相对较小时(比如小于 50),CEM 可能会显示出良好的前景。还要注意,CEM 在过程的任何部分都不使用实际的奖励,这使我们不用担心奖励的尺度问题。

接下来,让我们实现 CEM 在炮弹射击示例中的应用。

交叉熵方法的简单实现

我们可以通过对随机射击方法进行一些轻微修改来实现 CEM。在我们这里的简单实现中,我们执行以下操作:

  • 从一个均匀生成的动作集合开始。

  • 将一个正态分布拟合到精英集合中,以生成下一组样本。

  • 使用固定的迭代次数来停止过程。

这可以通过以下方式实现:

from scipy.stats import norm
N, M_elites, iterations = 5, 2, 3
actions_to_try = random_shooting(N)
elite_acts, _ = pick_elites(actions_to_try, M_elites)
for r in range(iterations - 1):
    mu, std = norm.fit(elite_acts)
    actions_to_try = np.clip(norm.rvs(mu, std, N), 0, 90)
    elite_acts, elite_results = pick_elites(actions_to_try, 
                                                    M_elites)            
best_action, _ = norm.fit(elite_acts)

如果你添加一些print语句来查看执行该代码后的结果,它将看起来像下面这样:

--iteration: 1
actions_to_try: [29.97 3.56 57.8 5.83 74.15]
elites: [57.8  29.97]
--iteration: 2
fitted normal mu: 43.89, std: 13.92
actions_to_try: [26.03 52.85 36.69 54.67 25.06]
elites: [52.85 36.69]
--iteration: 3
fitted normal mu: 44.77, std: 8.08
actions_to_try: [46.48 34.31 56.56 45.33 48.31]
elites: [45.33 46.48]
The best action: 45.91

你可能会想,为什么我们需要拟合分布,而不是选择我们已经识别出的最佳动作。嗯,对于环境是确定性的炮弹射击示例,这样做没有太大意义。然而,当环境中存在噪声/随机性时,选择我们遇到的最佳动作意味着过拟合噪声。相反,我们将分布拟合到一组精英动作上,以克服这个问题。你可以参考 GitHub 仓库中的 Chapter08/rs_cem_comparison.py 来查看完整的代码。

CEM 的评估(和动作生成)步骤可以并行化,这将减少做出决策所需的墙钟时间。接下来我们将实现这一点,并在一个更复杂的示例中使用 CEM。

交叉熵方法的并行化实现

在本节中,我们使用 CEM 来解决 OpenAI Gym 的 Cartpole-v0 环境。这个示例与大炮射击的不同之处如下:

  • 动作空间是二元的,对应于左和右。因此,我们将使用多变量伯努利分布作为概率分布。

  • 最大问题时间范围为 200 步。然而,我们将使用 MPC 在每一步中规划 10 步前瞻,并执行计划中的第一步动作。

  • 我们使用 Ray 库进行并行化。

现在,让我们来看看实现中的一些关键组件。完整的代码可以在 GitHub 仓库中找到。

Chapter08/cem.py

让我们从描述代码开始,首先是从多变量伯努利分布中采样动作序列(我们使用 NumPy 的binomial函数),,并在规划范围内执行该序列以估计奖励:

@ray.remote
def rollout(env, dist, args):
    if dist == "Bernoulli":
        actions = np.random.binomial(**args)
    else:
        raise ValueError("Unknown distribution")
    sampled_reward = 0
    for a in actions:
        obs, reward, done, info = env.step(a)
        sampled_reward += reward
        if done:
            break
    return actions, sampled_reward

ray.remote装饰器将允许我们轻松地并行启动这些工作者。

CEM 以我们创建的CEM类的以下方法运行:

  1. 我们初始化伯努利分布的参数,设置时间范围为look_ahead步,初始值为0.5。我们还根据总样本的指定比例确定精英样本的数量:

        def cross_ent_optimizer(self):
            n_elites = int(np.ceil(self.num_parallel * \
                                       self.elite_frac))
            if self.dist == "Bernoulli":
                p = [0.5] * self.look_ahead
    
  2. 对于固定次数的迭代,我们在并行的展开工作中生成和评估动作。注意我们是如何将现有环境复制到展开工作者中,以便从该点开始采样。我们将分布重新拟合到精英集合中:

                for i in range(self.opt_iters):
                    futures = []
                    for j in range(self.num_parallel):
                        args = {"n": 1, "p": p, 
                                "size": self.look_ahead}
                        fid = \
                           rollout.remote(copy.deepcopy(self.env), 
                                                  self.dist, args)
                        futures.append(fid)
                    results = [tuple(ray.get(id)) 
                                           for id in futures]
                    sampled_rewards = [r for _, r in results]
                    elite_ix = \
                         np.argsort(sampled_rewards)[-n_elites:]
                    elite_actions = np.array([a for a, 
                                        _ in results])[elite_ix]
                    p = np.mean(elite_actions, axis=0)
    
  3. 我们基于最新的分布参数来最终确定计划:

                actions = np.random.binomial(n=1, p=p,
                                             size=self.look_ahead)
    

执行此代码将解决环境问题,你将看到摆杆在最大时间范围内保持存活!

这就是如何使用 Ray 实现并行化 CEM 的方式。到目前为止,一切顺利!接下来,我们将更进一步,使用 CEM 的高级版本。

协方差矩阵自适应进化策略

协方差矩阵自适应进化策略(CMA-ES)是最先进的黑盒优化方法之一。其工作原理与 CEM 类似。另一方面,CEM 在整个搜索过程中使用固定的方差,而 CMA-ES 则动态地调整协方差矩阵。

我们再次使用 Ray 来并行化 CMA-ES 的搜索。但这次,我们将搜索的内部动态交给一个名为pycma的 Python 库来处理,这个库是由 CMA-ES 算法的创造者 Nikolaus Hansen 开发和维护的。当你为本章创建虚拟环境时,已经安装了这个包。

信息

pycma库的文档和详细信息可以在github.com/CMA-ES/pycma中找到。

CMA-ES 与 CEM 实现的主要区别在于它使用 CMA 库来优化行动:

    def cma_es_optimizer(self):
        es = cma.CMAEvolutionStrategy([0] \
                                      * self.n_tot_actions, 1)
        while (not es.stop()) and \
                es.result.iterations <= self.opt_iter:
            X = es.ask()  # get list of new solutions
            futures = [
                rollout.remote(self.env, x,
                               self.n_actions, 
                               self.look_ahead)
                for x in X
            ]
            costs = [-ray.get(id) for id in futures]
            es.tell(X, costs)  # feed values
            es.disp()
        actions = [
            es.result.xbest[i * self.n_actions : \
                            (i + 1) * self.n_actions]
            for i in range(self.look_ahead)
        ]
        return actions

你可以在我们的 GitHub 仓库中的Chapter08/cma_es.py找到完整的代码。它解决了双足行走器环境,输出将会是 CMA 库中的如下结果:

(7_w,15)-aCMA-ES (mu_w=4.5,w_1=34%) in dimension 40 (seed=836442, Mon Nov 30 05:46:55 2020)
Iterat #Fevals   function value  axis ratio  sigma  min&max std  t[m:s]
    1     15 7.467667956594279e-01 1.0e+00 9.44e-01  9e-01  9e-01 0:00.0
    2     30 8.050216186274498e-01 1.1e+00 9.22e-01  9e-01  9e-01 0:00.1
    3     45 7.222612141709712e-01 1.1e+00 9.02e-01  9e-01  9e-01 0:00.1
   71   1065 9.341667377266198e-01 1.8e+00 9.23e-01  9e-01  1e+00 0:03.1
  100   1500 8.486571756945928e-01 1.8e+00 7.04e-01  7e-01  8e-01 0:04.3
Episode 0, reward: -121.5869865603307

你应该能看到你的双足行走器走出 50 到 100 步!不错!

接下来,让我们讨论另一类重要的搜索方法,称为蒙特卡罗树搜索(MCTS)。

蒙特卡罗树搜索

一种自然的规划未来行动的方式是,首先考虑第一步,然后将第二个决策基于第一步做出,依此类推。这本质上是在一个决策树上进行搜索,这正是 MCTS 所做的。它是一个非常强大的方法,已被 AI 社区广泛采纳。

信息

MCTS 是一种强大的方法,在 DeepMind 战胜围棋世界冠军、传奇人物李世石的比赛中发挥了关键作用。因此,MCTS 值得广泛讨论;而不是将一些内容塞进本章,我们将其解释和实现推迟到博客文章中,链接为:int8.io/monte-carlo-tree-search-beginners-guide/。

到目前为止,我们讨论了代理如何通过环境模型进行规划的不同方法,我们假设存在这样的模型。在接下来的部分,我们将探讨当代理所在的世界模型(即环境)无法获得时,如何学习该模型。

学习世界模型

在本章的介绍部分,我们提到过如何从动态规划方法中脱离,以避免假设代理所在的环境模型是可用且可以访问的。现在,回到模型的讨论,我们还需要探讨如何在模型不可用时学习世界模型。具体来说,在这一部分中,我们讨论当我们需要学习模型时,学习什么内容,何时学习,以及学习模型的一般程序,如何通过将模型的不确定性纳入学习过程中来改进模型,以及当我们面对复杂的观测时该怎么做。让我们深入探讨一下!

理解模型的含义

从我们目前所做的来看,环境的模型可以等同于你脑海中对环境的模拟。另一方面,基于模型的方法并不要求完全还原模拟的精度。相反,我们从模型中期望获得的是在当前状态和动作下的下一状态。也就是说,当环境是确定性时,模型就是一个函数 :

如果环境是随机的,那么我们需要一个概率分布来确定下一状态,,以便进行抽样:

将其与模拟模型进行对比,后者通常对所有基础动态(如运动物理学、客户行为和市场动态等,具体取决于环境类型)有明确的表示。我们学习的模型将是一个“黑箱”,通常表示为神经网络。

信息

我们学习的世界模型并不能替代完整的模拟模型。模拟模型通常具有更强的泛化能力;它的忠实度也更高,因为它基于对环境动态的明确表示。另一方面,模拟也可以充当世界模型,正如前一节所述。

请注意,在本节剩余部分中,我们将使用 来表示该模型。现在,让我们讨论何时可能需要学习一个世界模型。

确定何时学习一个模型

有许多原因可能导致我们学习一个世界模型:

  • 可能并不存在一个模型,即便是一个模拟模型。这意味着智能体正在实际环境中进行训练,这将不允许我们进行想象中的“展开”(rollouts)来进行规划。

  • 可能存在一个模拟模型,但它可能太慢或计算要求过高,无法用于规划。将神经网络作为世界模型进行训练可以在规划阶段探索更广泛的场景。

  • 可能存在一个模拟模型,但它可能不允许从某个特定状态开始进行“展开”。这可能是因为模拟可能无法揭示底层状态,或者它不允许用户将其重置为所需的状态。

  • 我们可能希望明确表示具有预测未来状态能力的状态/观察,这样就不再需要复杂的策略表示,甚至不需要基于“展开”的规划。该方法具有生物学启发,已被证明是有效的,如Ha 等人,2018 年所述。你可以在worldmodels.github.io/上访问这篇互动版论文,它是关于这一主题的非常好的阅读资料。

现在我们已经确定了几种学习模型可能是必要的情况,接下来,让我们讨论如何实际进行学习。

引入一种学习模型的通用程序

学习一个模型( 或对于随机环境的 )本质上是一个监督学习问题:我们希望从当前状态和动作预测下一个状态。然而,请注意以下几个关键点:

  • 我们不像传统的监督学习问题那样从手头有数据开始。相反,我们需要通过与环境的互动来生成数据。

  • 我们也没有一个(良好的)策略来开始与环境互动。毕竟,我们的目标就是获得一个策略。

所以,我们首先需要做的是初始化一些策略。一个自然的选择是使用随机策略,这样我们可以探索状态-行动空间。另一方面,纯粹的随机策略在一些困难的探索问题中可能无法带来太多进展。举个例子,考虑训练一个类人机器人走路。随机的动作不太可能让机器人走起来,我们也无法获得足够的数据来训练这个状态下的世界模型。这需要我们同时进行规划和学习,使得智能体既能进行探索,也能进行利用。为此,我们可以使用以下过程(Levine, 2019):

  1. 初始化一个软策略 ,以将数据元组 收集到数据集 中。

  2. 训练 来最小化 。

  3. 通过 来规划以选择行动。

  4. 执行 MPC:执行第一个规划的动作并观察结果的 。

  5. 将获得的 附加到 。

  6. 每隔 步骤,返回第 3 步;每隔 步骤,返回第 2 步。

这种方法最终会得到一个训练好的 ,你可以在推理时与 MPC 程序一起使用。另一方面,事实证明,使用这种方法的智能体表现往往比无模型方法差。在下一节,我们将探讨为什么会发生这种情况,以及如何减轻这个问题。

理解并缓解模型不确定性的影响

当我们像之前描述的那样训练一个世界模型时,我们不应期望得到一个完美的模型。这应该不足为奇,但事实证明,当我们使用像 CMA-ES 这样的优秀优化器在不完美的模型上进行规划时,这些缺陷会严重影响智能体的表现。尤其是当我们使用高容量模型,如神经网络,并且数据有限时,模型会有大量错误,错误地预测高奖励状态。为了减轻模型误差的影响,我们需要考虑模型预测中的不确定性。

说到模型预测中的不确定性,实际上有两种类型,我们需要区分它们。接下来,我们就来做这个区分。

统计性(随机性)不确定性

假设有一个预测模型,它预测一个六面公平骰子的掷出结果。一个完美的模型对于结果会有很高的不确定性:任何一面都有相等的概率出现。这可能令人失望,但这并不是模型的“错”。这种不确定性来源于过程本身,而不是因为模型没有正确解释它观察到的数据。这种类型的不确定性被称为统计性或随机性不确定性。

认识论性(模型)不确定性

在另一个例子中,假设训练一个预测模型来预测一个六面骰子的掷骰结果。我们不知道骰子是否公平,事实上,这正是我们试图从数据中学习的内容。现在,假设我们仅根据一个观察值进行模型训练,而这个观察值恰好是 6。当我们用模型预测下一个结果时,模型可能会预测出 6,因为它只见过这个值。然而,这仅基于非常有限的数据,因此我们对模型的预测会有很大的不确定性。这种不确定性被称为知识性不确定性(epistemic)或模型不确定性。正是这种类型的不确定性在基于模型的 RL 中给我们带来了麻烦。

接下来我们将介绍一些处理模型不确定性的方法。

缓解模型不确定性的影响

将模型不确定性融入基于模型的 RL 过程中的两种常见方法是使用贝叶斯神经网络和集成模型(ensemble models)。

使用贝叶斯神经网络

贝叶斯神经网络为(网络参数)中的每个参数分配一个分布,而不是单一的数值。这为我们提供了一个概率分布,可以从中采样一个神经网络。通过这种方式,我们可以量化神经网络参数的不确定性。

信息

注意,我们在第三章《上下文赌博机(Contextual Bandits)》中使用了贝叶斯神经网络。回顾那一章可能会帮助你复习这个话题。如果你想深入了解,可以参考Jospin 等人,2020的完整教程。

使用这种方法,每当我们进入规划步骤时,我们会多次从中采样,以估算一个动作序列的奖励。

使用集成模型与自助法

估算不确定性的另一种方法是使用自助法,它比贝叶斯神经网络更易于实现,但也是一种不那么严格的方法。自助法简单地通过训练多个(例如 10 个)神经网络,每个网络都使用从原始数据集中重新抽样的数据(带有替换)。

信息

如果你需要快速复习统计学中的自助法(bootstrapping),可以查看 Trist'n Joseph 的这篇博客文章:bit.ly/3fQ37r1。

与使用贝叶斯网络类似,这时我们会对这些多个神经网络给出的奖励进行平均,以评估在规划过程中的一个动作序列。

这样,我们就结束了关于将模型不确定性融入基于模型的强化学习(RL)的讨论。在结束本节之前,简要讨论一下如何处理复杂的观察数据来学习世界模型。

从复杂的观察数据中学习模型

到目前为止,我们所描述的内容,在以下情况之一或两者同时出现时,可能会变得有些复杂:

  • 部分可观察的环境,因此智能体看到的是而不是。

  • 高维度观察,如图像。

我们将在第十一章,《泛化与部分可观察性》一章中详细讨论部分可观察性。在这一章中,我们将讨论如何通过保留过去的观察来帮助我们揭示环境中的隐藏状态。一个常用的架构是长短期记忆(LSTM)模型,这是一类特殊的递归神经网络(RNN)架构。因此,在部分可观察性下,使用 LSTM 来表示是常见的选择。

当面对高维度的观察时,例如图像,一个常见的方法是将它们编码成紧凑的向量。变分自编码器(VAE)是获取这种表示的首选方法。

信息

Jeremy Jordan 的关于变分自编码器(VAE)的精彩教程可以在www.jeremyjordan.me/variational-autoencoders/找到。

当环境既是部分可观察的,又产生图像观察时,我们需要首先将图像转换为编码,使用 RNN 来预测与下一个观察相对应的编码,并通过这个进行规划。Ha 等人,2018 年在他们的“世界模型”论文中使用了类似的方法来处理图像和部分可观察性。

这结束了我们关于学习世界模型的讨论。在本章的下一节,也是最后一节,我们来讨论如何使用到目前为止描述的方法,为强化学习智能体获得一个策略。

统一基于模型和无模型的方法

当我们从基于动态规划的方法转向蒙特卡罗和时序差分方法时,参见第五章,《解决强化学习问题》,我们的动机是:假设环境转移概率已知是有限制的。现在我们知道如何学习环境动态,我们将利用这一点找到一个折衷方案。事实证明,拥有一个已学习的环境模型后,使用无模型的方法进行学习可以加速。因此,在这一部分,我们首先回顾 Q-learning,然后介绍一类叫做Dyna的方法。

Q-learning 复习

让我们从回顾动作值函数的定义开始:

这里的期望算子是因为向下一个状态的过渡是概率性的,因此 和 都是随机变量。另一方面,如果我们知道 和 的概率分布,我们可以通过解析方法计算这个期望值,这就是像价值迭代等方法所做的。

在缺乏过渡动态信息的情况下,像 Q 学习这样的算法通过从单一样本 中估算期望值:

Dyna 算法基于这样的想法:与其使用从环境中采样的简单 ,我们可以通过从环境中采样多个 ,利用学习到的环境模型来更好地估计期望值,给定 。

提示

到目前为止,我们在讨论中隐含地假设一旦 已知,奖励 就可以计算。如果不是这样,尤其是在部分可观测的情况下,我们可能需要为 学习一个单独的模型。

接下来,让我们更正式地概述这个想法。

使用世界模型加速无模型方法的 Dyna 风格

Dyna 方法是一个相当古老的算法(Sutton, 1990),旨在“集成学习、规划和反应”。这个方法有以下一般流程(Levine, 2019):

  1. 当处于状态 时,使用 采样 。

  2. 观察 和 ,并将元组 添加到回放缓冲区 。

  3. 更新世界模型 ,并可选地更新 。

    对于 到 :

  4. 从 中采样 。

  5. 选择某些 ,可以从 、从 或随机选择。

  6. 采样 和 。

  7. 使用无模型强化学习方法(深度 Q 学习)在 上进行训练。

  8. 可选地,在 后执行进一步的步骤。

    结束 For

  9. 返回到 步骤 1(并且 )。

就是这样!Dyna 是强化学习中的一个重要方法类别,现在你知道它是如何工作的了!

信息

RLlib 有一个先进的 Dyna 风格方法实现,称为 基于模型的 RL 通过元策略优化,或 MBMPO。你可以在 docs.ray.io/en/releases-1.0.1/rllib-algorithms.html#mbmpo 查看。自 Ray 1.0.1 以来,它已在 PyTorch 中实现,因此如果你想尝试它,请在你的虚拟环境中安装 PyTorch。

这就是我们关于基于模型的强化学习(RL)章节的结尾,恭喜你走到了这一步!我们只是略微触及了这一广泛话题的表面,但现在你已经具备了使用基于模型的方法来解决问题的知识!接下来,让我们总结一下我们所覆盖的内容。

总结

在这一章节中,我们介绍了基于模型的方法。我们从描述人类如何利用大脑中的世界模型来规划行为开始,然后介绍了几种在拥有模型的情况下,可以用来规划智能体在环境中行为的方法。这些方法是无导数的搜索方法,对于 CEM 和 CMA-ES 方法,我们实现了并行版本。作为本节的自然延伸,我们接着讲解了如何学习一个世界模型,用于规划或开发策略。本节内容包含了关于模型不确定性以及学习到的模型可能受到不确定性影响的重要讨论。章节的最后,我们将无模型和基于模型的方法统一在 Dyna 框架中。

随着我们结束关于基于模型的强化学习的讨论,我们将进入下一个章节,探讨另一个令人兴奋的话题:多智能体强化学习。休息一下,我们很快再见!

参考文献

  • Levine, Sergey. (2019). 最优控制与规划。CS285 Fa19 10/2/19。YouTube。网址:youtu.be/pE0GUFs-EHI

  • Levine, Sergey. (2019). 基于模型的强化学习。CS285 Fa19 10/7/19。YouTube。网址:youtu.be/6JDfrPRhexQ

  • Levine, Sergey. (2019). 基于模型的策略学习。CS285 Fa19 10/14/19。YouTube。网址:youtu.be/9AbBfIgTzoo。

  • Ha, David, 和 Jürgen Schmidhuber. (2018). 世界模型。arXiv.org,网址:arxiv.org/abs/1803.10122。

  • Mania, Horia 等人. (2018). 简单随机搜索提供了一种具有竞争力的强化学习方法。arXiv.org,网址:arxiv.org/abs/1803.07055

  • Jospin, Laurent Valentin 等人. (2020). 动手实践贝叶斯神经网络 - 深度学习用户教程。arXiv.org,网址:arxiv.org/abs/2007.06823。

  • Joseph, Trist'n. (2020). 引导统计学:它是什么以及为什么使用它。Medium。网址:bit.ly/3fOlvjK。

  • Richard S. Sutton. (1991). Dyna,一种集成学习、规划和反应的架构。SIGART Bull. 2, 4 (1991 年 8 月),160–163。DOI:doi.org/10.1145/122344.122377

第九章:第九章:多智能体强化学习

如果有比训练一个强化学习(RL)智能体表现出智能行为更令人兴奋的事情,那就是训练多个智能体进行合作或竞争。多智能体 RL(MARL)是你真正能感受到人工智能潜力的地方。许多著名的 RL 故事,例如 AlphaGo 或 OpenAI Five,都源自 MARL,我们将在这一章中介绍它们。当然,天下没有免费的午餐,MARL 也伴随着许多挑战与机遇,我们也将对此进行探索。在本章的最后,我们将通过竞争性自我博弈训练一群井字游戏智能体。所以,最后你将有一些伙伴一起玩游戏。

这一章将会很有趣,我们将具体涵盖以下主题:

  • 介绍多智能体强化学习,

  • 探索多智能体强化学习中的挑战,

  • 在多智能体环境中训练策略,

  • 通过自我博弈训练井字游戏智能体。

让我们开始吧!

介绍多智能体强化学习

到目前为止,我们书中讨论的所有问题和算法都涉及在环境中训练单一智能体。另一方面,在从游戏到自动驾驶车队的许多应用中,存在多个决策者,即多个智能体,这些智能体并行训练,但执行的是本地策略(即没有中央决策者)。这引导我们进入 MARL,它涉及比单一智能体 RL 更为丰富的问题和挑战。在本节中,我们将概述 MARL 的整体格局。

MARL 智能体之间的协作与竞争

MARL 问题可以根据智能体之间的协作和竞争结构分为三大类。让我们看看这三类是什么,以及每类适合哪些应用。

完全合作环境

在这种环境下,所有智能体都朝着一个共同的长期目标努力。环境所反馈的回报会平等地分配给所有智能体,因此没有任何智能体有动机偏离共同目标。

这里有一些完全合作环境的例子:

  • 自动驾驶车辆/机器人车队:有许多应用场景,自动驾驶车辆/机器人车队可以协同完成一个共同的任务。一个例子是灾后恢复/紧急响应/救援任务,车队尝试完成如向急救人员运送紧急物资、关闭气阀、清理道路上的杂物等任务。类似地,在供应链中出现的运输问题,或通过多个机器人运输大物体的场景也属于这一类。

  • 制造业:工业 4.0 背后的整体理念是实现设备和网络物理系统的互联,以达到高效的生产和服务。如果你想象一个单一的制造车间,里面通常有许多决策设备,MARL 是建模此类控制问题的自然选择。

  • 智能电网:在新兴的智能电网领域,许多问题可以归类为这一类。例如,涉及许多冷却单元的数据中心冷却问题。类似地,交叉路口交通信号灯的控制也是这一领域的另一个好例子。事实上,在第十七章**《智能城市与网络安全》中,我们将使用 MARL 来建模和解决这个问题。

在讨论其他类型的 MARL 环境之前,在我们讨论自动驾驶车辆的 MARL 时,让我们简要提到一个有用的平台,MACAD-Gym,供你实验使用。

MACAD-Gym 用于多代理连接的自动驾驶

MACAD-Gym 是一个基于 Gym 的库,用于多代理设置中的连接与自动驾驶应用,建立在著名的 CARLA 模拟器之上。

图 9.1:MACAD-Gym 平台(来源:MACAD-Gym GitHub 仓库)

图 9.1:MACAD-Gym 平台(来源:MACAD-Gym GitHub 仓库)

该平台提供了一套丰富的场景,包括汽车、行人、交通信号灯、自行车等,如图 9.1 所示。更详细地说,MACAD-Gym 环境包含各种 MARL 配置,如以下示例所示:

Environment-ID: Short description
{'HeteNcomIndePOIntrxMATLS1B2C1PTWN3-v0': 
'Heterogeneous, Non-communicating, '
'Independent,Partially-Observable '
'Intersection Multi-Agent scenario '
'with Traffic-Light Signal, 1-Bike, '
'2-Car,1-Pedestrian in Town3, '
'version 0'}

要查看你可以用 MACAD-Gym 做什么,请访问其由 Praveen Palanisamy 开发和维护的 Github 仓库:github.com/praveen-palanisamy/macad-gym。

在这一短暂的绕道之后,我们继续讨论 MARL 中的完全竞争环境。

完全竞争环境

在完全竞争的环境中,一个代理的成功意味着其他代理的失败。因此,这些环境被建模为零和博弈:

其中!是!代理的奖励。

完全竞争环境的一些例子如下:

  • 棋盘游戏:这是这种环境的经典例子,例如国际象棋、围棋和井字游戏。

  • 对抗性设置:在我们希望最小化代理在现实生活中失败风险的情况下,我们可能会让它与对抗性代理进行训练。这就创建了一个完全竞争的环境。

最后,让我们来看一下混合合作-竞争环境。

混合合作-竞争环境

第三类环境涉及代理之间的合作与竞争。这些环境通常被建模为一般和博弈:

这里!是!代理的奖励,!是代理可以收集的固定总奖励。

以下是一些混合环境的例子:

  • 团队竞争:当智能体分成多个团队并相互竞争时,每个团队内的智能体会合作,以击败其他团队。

  • 经济:如果你思考我们所参与的经济活动,它是竞争与合作的结合。一个很好的例子是像微软、谷歌、Facebook 和亚马逊这样的科技公司,它们在某些业务上相互竞争,但也在一些开源项目上合作,以推动软件技术的发展。

    信息

    在这一点上,值得暂停一下,观看 OpenAI 的演示,展示了智能体在团队中玩捉迷藏的场景。智能体在经历了大量对局后,发展出了非常酷的合作与竞争策略,这激发了我们对强化学习(RL)在实现人工通用智能(AGI)方面的潜力的思考。参见图 9.2,获取快速截图和视频链接。

图 9.2:OpenAI 的智能体玩捉迷藏(来源: https://youtu.be/kopoLzvh5jY )

图 9.2:OpenAI 的智能体玩捉迷藏(来源:youtu.be/kopoLzvh5jY)

现在我们已经介绍了基础知识,接下来让我们探讨一下多智能体强化学习(MARL)面临的一些挑战。

探索多智能体强化学习中的挑战

在本书的前几章中,我们讨论了强化学习中的许多挑战。特别是,我们最初介绍的动态规划方法无法扩展到具有复杂和大规模观察与动作空间的问题。另一方面,深度强化学习方法尽管能够处理复杂问题,但缺乏理论保证,因此需要许多技巧来稳定和收敛。现在,我们谈论的是在多个智能体共同学习、相互作用并影响环境的情况下;单智能体强化学习中的挑战和复杂性被成倍增加。因此,MARL 中的许多结果都是经验性的。

在本节中,我们讨论了为什么 MARL 特别复杂且充满挑战。

非平稳性

单智能体强化学习背后的数学框架是马尔可夫决策过程(MDP),它表明环境动态依赖于当前状态,而非历史。这表明环境是平稳的,许多方法依赖于此假设来实现收敛。但现在环境中有多个智能体在学习,因此随着时间的推移改变其行为,这一基本假设被打破,阻碍了我们像分析单智能体强化学习那样分析 MARL。

举个例子,考虑使用带有重放缓冲区的 Q 学习等脱政策方法。在 MARL 中,使用这种方法尤其具有挑战性,因为较早收集的经验可能与环境(部分由其他智能体构成)对单个智能体行动的反应截然不同。

可扩展性

解决非平稳性的一个可能方法是考虑其他智能体的行为,例如使用联合行动空间。随着智能体数量的增加,这种方法会变得越来越不可行,这使得 MARL 的可扩展性成为一个问题。

话虽如此,当环境中只有两个智能体时,分析智能体行为如何收敛相对容易。如果你熟悉博弈论,一种常见的看待这种系统的方式是理解均衡点,在这些点上,智能体不会通过改变他们的策略而受益。

信息

如果你需要对博弈论和纳什均衡的简要介绍,可以查看这个视频:www.youtube.com/watch?v=0i7p9DNvtjk

当环境中有超过两个智能体时,这种分析会变得非常困难,这使得大规模 MARL 变得非常难以理解。

不明确的强化学习目标

在单一智能体强化学习(RL)中,目标很明确:最大化预期的累积回报。另一方面,在多智能体强化学习(MARL)中并没有一个唯一明确的目标。

想象一个棋局,我们试图训练一个非常强大的棋手。为此,我们训练许多智能体,让它们通过自我对弈相互竞争。你会如何设定这个问题的目标?最初的想法可能是最大化最佳智能体的奖励。但是,这可能导致除了一个以外所有智能体都成为糟糕的棋手。这显然不是我们所希望的结果。

MARL 中一个流行的目标是实现纳什均衡的收敛。这个方法通常很有效,但当智能体不完全理性时,它也有缺点。此外,纳什均衡自然意味着对其他智能体策略的过拟合,这不一定是我们想要的。

信息共享

MARL 中的另一个重要挑战是设计智能体之间的信息共享结构。我们可以考虑三种信息结构:

  • 完全中心化:在这种结构下,所有智能体收集的信息都由一个中央机制处理,局部策略将利用这一集中知识。该结构的优势在于智能体之间的完全协调。另一方面,这可能导致一个优化问题,随着智能体数量的增加,它的扩展性变差。

  • 完全去中心化:在这种结构下,智能体之间不交换信息,每个智能体将基于他们的本地观察做出决策。显而易见的好处是没有中心化协调器的负担。另一方面,由于智能体对环境的信息有限,智能体的行为可能是次优的。此外,当训练完全独立时,由于高度部分可观测性,强化学习算法可能会很难收敛。

  • 去中心化但网络化的代理:这种结构允许小群体(邻居)代理之间交换信息。反过来,这有助于信息在它们之间传播。这里的挑战是创建一个能够在不同环境条件下有效工作的强健通信结构。

根据强化学习问题的目标以及计算资源的可用性,不同的方式可能会更为优选。考虑一个合作环境,其中大量的机器人群体试图达成一个共同的目标。在这种问题中,完全集中式或网络化的控制可能是合理的。而在完全竞争的环境中,比如策略类视频游戏,完全去中心化的结构可能更为适合,因为在代理之间没有共同的目标。

在这么多理论之后,现在是时候进入实践了!很快,我们将训练一个井字游戏代理,你可以在会议或课堂上与其对战。首先,我们将描述如何进行训练,然后再进入实现部分。

在多代理环境中训练策略

有许多针对 MARL 设计的算法和方法,可以大致分为以下两大类。

  • 独立学习:这种方法建议独立训练代理,并将环境中的其他代理视为环境的一部分。

  • 集中式训练与去中心化执行:在这种方法中,有一个集中式控制器,在训练过程中使用来自多个代理的信息。在执行(推理)时,代理将独立地执行策略,而不依赖于中央机制。

一般来说,我们可以选择之前章节中提到的任何算法,并将其应用于多代理环境中,通过独立学习来训练策略,事实证明,这是一种非常有竞争力的替代方法,比专门的多代理强化学习(MARL)算法还要有效。因此,在本章中,我们将跳过讨论任何特定 MARL 算法的技术细节,并将相关资料交给你参考文献部分。

信息

关于深度 MARL 算法比较的简明且强烈推荐的阅读资料可以参考(Papoudakis et al. 2020)。只需访问参考文献部分,找到论文的链接。

所以,我们将采用独立学习。那么它是如何工作的呢?嗯,它要求我们:

  • 拥有多个代理的环境,

  • 维持支持代理的策略,

  • 适当地将来自环境的奖励分配给代理。

对我们来说,设计一个合适的框架来处理上述问题可能会有些棘手。幸运的是,RLlib 提供了一个多代理环境来解决这个问题。接下来,让我们看看它是如何工作的。

RLlib 多代理环境

RLlib 的多智能体环境灵活地允许我们连接到一个你已经熟悉的算法,用于 MARL。事实上,RLlib 文档方便地展示了哪些算法与这种环境类型兼容:

![图 9.3:RLlib 的算法列表显示了多智能体的兼容性(来源:docs.ray.io/en/releases-1.0.1/rllib-algorithms.html)

](https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/ms-rl-py/img/B14160_09_003.jpg)

图 9.3:RLlib 的算法列表显示了多智能体的兼容性(来源:docs.ray.io/en/releases-1.0.1/rllib-algorithms.html)

在该列表中,你还会看到一个单独的部分,列出了针对 MARL 特定算法的内容。在本章中,我们将使用 PPO。

当然,下一步是理解如何将我们选择的算法与多智能体环境一起使用。此时,我们需要做一个关键区分:使用 RLlib 时,我们将训练策略,而不是智能体(至少不是直接训练)。一个智能体将映射到正在训练的某个策略,以获取动作。

RLlib 文档通过以下图示说明了这一点:

![图 9.4:RLlib 中智能体和策略之间的关系(来源:docs.ray.io/en/releases-1.0.1/rllib-env.html#multi-agent-and-hierarchical)

](https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/ms-rl-py/img/B14160_09_004.jpg)

图 9.4:RLlib 中智能体和策略之间的关系(来源:docs.ray.io/en/releases-1.0.1/rllib-env.html#multi-agent-and-hierarchical)

如果你还没有意识到,这为我们提供了一个非常强大的框架,用来建模多智能体强化学习(MARL)环境。例如,我们可以灵活地向环境中添加智能体、移除智能体,并为同一任务训练多个策略。只要我们指定了策略与智能体之间的映射,一切都能顺利进行。

有了这些,让我们看看 RLlib 中的训练循环在与多智能体环境配合使用时需要哪些要求:

  1. 带有相应 id 的策略列表。这些是将要被训练的策略。

  2. 一个将给定智能体 id 映射到策略 id 的函数,这样 RLlib 就知道为给定智能体生成的动作来自哪里。

一旦设置好,环境将使用 Gym 规范与 RLlib 进行通信。不同之处在于,观察、奖励和终止语句将为环境中的多个智能体发出。例如,重置函数将返回如下所示的观察字典:

> env.reset()
{"agent_1": [[...]], "agent_2":[[...]], "agent_4":[[...]],...

同样,由策略生成的动作会传递给我们从中收到观察的智能体,类似于:

... = env.step(actions={"agent_1": ..., "agent_2":..., "agent_4":..., ...

这意味着,如果环境返回一个智能体的观察,它就会要求返回一个动作。

到目前为止一切顺利!这些内容应该能给你一些关于它如何工作的初步了解。等我们进入实现部分时,事情会变得更加清晰!

很快,我们将训练井字棋策略,正如我们之前提到的。使用这些策略的智能体将相互竞争,学习如何玩游戏。这就是竞争性自我对弈,我们接下来会讨论这个话题。

竞争性自我对弈

自我对弈是训练 RL 智能体进行竞争性任务的一个很好的工具,这些任务包括棋类游戏、多人视频游戏、对抗性场景等。你听说过的许多著名 RL 智能体,都是通过这种方式训练的,比如 AlphaGo、OpenAI Five(用于 Dota 2)以及 DeepMind 的《星际争霸 II》智能体。

信息

OpenAI Five 的故事非常有趣,展示了该项目是如何开始并发展成今天的模样的。关于该项目的博客文章提供了许多有用的信息,从模型中使用的超参数到 OpenAI 团队如何克服工作中的各种挑战。你可以在openai.com/projects/five/找到该项目页面。

传统自我对弈的一个缺点是,智能体仅看到以相同方式训练的其他智能体,因此容易过拟合到彼此的策略。为了解决这个问题,训练多个策略并让它们彼此对抗是有意义的,这也是我们在本章中要做的事情。

信息

过拟合在自我对弈中是一个挑战,甚至训练多个策略并将它们彼此对抗也不足以解决问题。DeepMind 创建了一个代理/策略的“联赛”,就像篮球联赛一样,以获得一个真正具有竞争性的训练环境,这也促成了他们在《星际争霸 II》上的成功。他们在一个很棒的博客中解释了他们的方法,deepmind.com/blog/article/AlphaStar-Grandmaster-level-in-StarCraft-II-using-multi-agent-reinforcement-learning。

最后,到了尝试多智能体强化学习的时候了!

通过自我对弈训练井字棋智能体

在本节中,我们将为您提供一些关于我们 Github 仓库中代码的关键解释,以便您更好地掌握使用 RLlib 进行 MARL 的技巧,同时在 3x3 的井字棋棋盘上训练智能体。完整代码可以参考github.com/PacktPublishing/Mastering-Reinforcement-Learning-with-Python。

图 9.5:一个 3x3 的井字棋。图片来源以及如何玩游戏的说明,见 https://en.wikipedia.org/wiki/Tic-tac-toe

图 9.5:一个 3x3 的井字棋。图片来源以及如何玩游戏的说明,见en.wikipedia.org/wiki/Tic-tac-toe。

我们从设计多智能体环境开始。

设计多智能体井字棋环境

在游戏中,我们有两个代理,X 和 O,它们在玩游戏。我们将为代理训练四个策略,每个策略可以选择 X 或 O 的行动。我们构建环境类如下:

Chapter09/tic_tac_toe.py

class TicTacToe(MultiAgentEnv):
    def __init__(self, config=None):
        self.s = 9
        self.action_space = Discrete(self.s)
        self.observation_space = MultiDiscrete([3] * self.s)
        self.agents = ["X", "O"]
        self.empty = " "
        self.t, self.state, self.rewards_to_send = \
                                    self._reset()

这里,9 代表棋盘上的方格数量,每个方格可以被 X、O 或空白填充。

我们按以下方式重置这个环境:

    def _next_agent(self, t):
        return self.agents[int(t % len(self.agents))]
    def _reset(self):
        t = 0
        agent = self._next_agent(t)
        state = {"turn": agent, 
                 "board": [self.empty] * self.s}
        rews = {a: 0 for a in self.agents}
        return t, state, rews

然而,我们不会将这个状态直接传递给策略,因为它仅仅是充满字符。我们会处理它,以便在即将进行的玩家面前,他们自己的标记总是用 1 表示,而对手的标记总是用 2 表示。

    def _agent_observation(self, agent):
        obs = np.array([0] * self.s)
        for i, e in enumerate(self.state["board"]):
            if e == agent:
                obs[i] = 1
            elif e == self.empty:
                pass
            else:
                obs[i] = 2
        return obs

这个处理后的观察结果就是传递给策略的数据:

    def reset(self):
        self.t, self.state, self.rewards_to_send =\
                            self._reset()
        obs = {self.state["turn"]: \
               self._agent_observation(self.state["turn"])}
        return obs

最后,step 方法会处理玩家的操作,并将环境推进到下一步。如果赢了,玩家会得到一个 ,如果输了,则得到 。注意,策略可能会建议在已占据的方格上放置标记,这种行为会被惩罚并扣除 分。

配置训练器

我们创建了 4 个策略来训练,给它们分配一些 ID,并指定它们的观察空间和行动空间。我们是这样做的:

Chapter09/ttt_train.py

    env = TicTacToe()
    num_policies = 4
    policies = {
        "policy_{}".format(i): (None, 
                                env.observation_space, 
                                env.action_space, {})
        for i in range(num_policies)}

在创建传递给训练器的配置字典时,我们将代理映射到策略上。为了减少过拟合,而不是为某个特定代理分配特定策略,我们随机选择一个策略来获取即将执行的代理的行动。

    policy_ids = list(policies.keys())
    config = {
        "multiagent": {
            "policies": policies,
            "policy_mapping_fn": (lambda agent_id: \
                           random.choice(policy_ids)),
        },
...

在训练过程中,我们会保存模型,以便跟踪它们的改进。由于涉及多个策略,作为进度的代理,我们检查回合是否因有效的操作而变长。我们希望随着代理变得更具竞争力,越来越多的游戏将以平局结束,这时棋盘上将充满标记。

    trainer = PPOTrainer(env=TicTacToe, config=config)
    best_eps_len = 0
    mean_reward_thold = -1
    while True:
        results = trainer.train()
        if results["episode_reward_mean"] > mean_reward_thold\
           and results["episode_len_mean"] > best_eps_len:
            trainer.save("ttt_model")
            best_eps_len = results["episode_len_mean"]
        if results.get("timesteps_total") > 10 ** 7:
            break

就这样!现在开始观看训练的乐趣吧!

观察结果

游戏开始时,会有很多无效操作,导致回合长度延长,并且代理受到过多惩罚。因此,代理的平均奖励图像将如下所示:

图 9.6:平均代理奖励

图 9.6:平均代理奖励

注意到它如何从深度负值开始,逐渐趋近于零,表明平局成为了常见的结果。与此同时,你应该看到回合长度趋近于 9:

图 9.7:回合长度进展

图 9.7:回合长度进展

当你看到竞争愈演愈烈时,你可以停止训练!更有趣的是,运行 ttt_human_vs_ai.py 脚本与 AI 对战,或者运行 ttt_ai_vs_ai.py 观察它们的对战。

就这样,我们结束了这一章。这是一个有趣的章节,不是吗?接下来我们来总结一下本章的学习内容。

总结

在这一章中,我们讲解了多智能体强化学习(MARL)。与其他强化学习分支相比,它更具挑战性,因为有多个决策者影响环境,而且这些决策者在时间上也会发生变化。在介绍了 MARL 的一些概念后,我们详细探讨了这些挑战。随后,我们通过 RLlib 进行竞争性自我博弈来训练井字游戏智能体。它们竞争激烈,训练结束时总是以平局告终!

在下一章中,我们将转向讨论一种新兴的强化学习方法——机器教学,它将让领域专家(也就是你)更积极地参与到训练过程中,指导训练进展。期待在那里见到你!

参考文献

  1. Mosterman, P. J. 等(2014)。用于自动化人道主义任务的异构车队。计算科学与工程,16 卷,第 3 期,页 90-95。网址:msdl.cs.mcgill.ca/people/mosterman/papers/ifac14/review.pdf

  2. Papoudakis, Georgios, 等(2020)。多智能体深度强化学习算法的比较评估。arXiv.org,arxiv.org/abs/2006.07869

  3. Palanisamy, Praveen.(2019)。基于深度强化学习的多智能体自动驾驶。arxiv.org,arxiv.org/abs/1911.04175v1

第三部分:强化学习中的高级话题

在这一节中,你将学习强化学习中的高级技巧,如机器学习,这对于解决实际问题非常有用。此外,本节还涵盖了强化学习中的各种未来叙事,这将有助于改善模型。

本节包含以下章节:

  • 第十章,机器教学

  • 第十一章,泛化与领域随机化

  • 第十二章,元强化学习

  • 第十三章,其他高级话题

第十章:第十章:机器教学

强化学习(RL)的巨大兴奋感,在很大程度上源于它与人类学习的相似性:RL 智能体通过经验进行学习。这也是为什么许多人认为它是通向人工通用智能的路径。另一方面,如果你仔细想想,将人类学习仅仅归结为反复试验,实在是大大的低估了人类的学习过程。我们并非从出生开始就从零发现我们所知道的一切——无论是在科学、艺术、工程等领域!相反,我们建立在数千年来积累的知识和直觉之上!我们通过各种不同的、有结构的或无结构的教学形式将这些知识在我们之间传递。这种能力使得我们能够相对快速地获得技能并推动共识知识的进步。

从这个角度来看,我们使用机器学习的方式似乎相当低效:我们将大量原始数据投入到算法中,或者让它们暴露于环境中(对于 RL 而言),几乎没有任何指导。这部分也是为什么机器学习需要如此大量的数据,并且有时会失败的原因。

机器教学(MT)是一种新兴的方式,它将重点转向从教师中提取知识,而不是仅仅依赖原始数据,从而指导训练机器学习算法的过程。反过来,学习新技能和映射的过程变得更加高效,且所需的数据、时间和计算资源更少。在本章中,我们将介绍 MT 在 RL 中的组成部分及其一些最重要的方法,例如奖励函数工程、课程学习、示范学习和动作屏蔽。最后,我们还将讨论 MT 的缺点与未来发展。具体而言,我们将在本章中涵盖以下内容:

  • MT 简介

  • 奖励函数的设计

  • 课程学习

  • 热启动与示范学习

  • 动作屏蔽

  • 概念网络

  • MT 的缺点与前景

技术要求

本章的所有代码可以在以下 GitHub 链接找到:

github.com/PacktPublishing/Mastering-Reinforcement-Learning-with-Python

MT 简介

MT 是一种通用方法及其相关技术的集合,旨在高效地将知识从教师——即学科专家——转移到机器学习算法中。通过这种方式,我们旨在使训练过程更加高效,甚至使那些否则不可能完成的任务变得可行。接下来,我们将详细讨论 MT 是什么,为什么我们需要它,以及它的组成部分。

理解 MT 的需求

你知道吗?美国预计在 2021 年将花费约 1.25 万亿美元,约占其国内生产总值的 5%,用于教育开支。这应该能反映教育对我们社会和文明的生死攸关的重要性(许多人会争辩说,我们应该投入更多)。我们人类建立了这样一个庞大的教育系统,我们期望人们在其中花费多年,因为我们不指望自己能够独立解码字母表或数学。不仅如此,我们不断从身边的老师那里学习,如何使用软件、如何开车、如何做饭,等等。这些老师不必是人类教师:书籍、博客文章、手册和课程材料都为我们提炼了有价值的信息,让我们能够学习,不仅是在学校,而是在整个生活过程中。

我希望这能让你信服教学的重要性。但如果你觉得这个例子过于民粹化,或许与 RL 有些不相关,那么让我们来讨论机器翻译(MT)如何在 RL 中发挥特定作用。

学习的可行性

你是否曾在没有(好的)老师的情况下,尝试独自学习某些东西时感到力不从心?这就像一个 RL 代理由于可能的策略数量过多,无法为当前问题找到合适的策略。这个过程中主要的障碍之一是缺乏关于策略质量的适当反馈。你也可以将困难的探索问题与稀疏奖励相提并论,这是 RL 中的一个严重挑战。

考虑以下例子:一个 RL 代理正在与一个竞争对手对弈国际象棋,胜利奖励为+1,平局奖励为 0,失败奖励为-1。RL 代理需要不断“偶然”地找到数十个“好棋步”,一招接一招,在每一步的众多备选棋步中,才能获得第一次 0 或+1 的奖励。由于这种情况的发生概率较低,如果没有巨大的探索预算,训练很可能会失败。另一方面,教师可以引导探索,使得 RL 代理至少知道几种成功的方式,从而逐渐改进其胜利策略。

信息

当 DeepMind 创建其 AlphaStar 代理来玩《星际争霸 II》时,他们首先使用监督学习训练代理,基于过去的人类游戏日志,然后才进入基于强化学习(RL)的训练。在某种意义上,人类玩家是代理的第一批教师,没有他们,这种训练将变得不可行或成本过高。为了支持这一论点,可以以训练 OpenAI Five 代理玩《Dota 2》的例子为例。训练该代理花费了近一年的时间。

以下图展示了代理在行动中的情形:

图 10.1 – DeepMind 的 AlphaStar 代理在行动中(来源:AlphaStar 团队,2019)

图 10.1 – DeepMind 的 AlphaStar 代理在行动中(来源:AlphaStar 团队,2019)

总结来说,有一个教师可以让学习在合理的时间内变得可行。

时间、数据和计算效率

假设你拥有足够的计算资源,并且能够尝试大量的动作序列,让 RL 代理在一个环境中发现获胜策略。仅仅因为你能做到,并不意味着你应该这样做并浪费所有这些资源。教师可以帮助你大大减少训练时间、数据和计算量。你可以利用节省下来的资源来反复改进你的想法,开发更好的代理人。

提示

你是否有导师帮助你在职业、教育、婚姻等方面的发展?或者你是否阅读过关于这些主题的书籍?你的动力是什么?你不想重复他人的错误,也不想重新发明别人已经知道的东西,浪费你的时间、精力和机会,对吧?MT 以类似的方式帮助你的代理人快速开始任务。

MT 的好处不仅仅在于学习的可行性或效率。接下来,我们来谈谈另一个方面:你代理人的安全性。

确保代理人的安全性

教师是某个主题的专家。因此,教师通常对在什么条件下采取哪些行动可能会让代理人陷入困境有一个相当清晰的认识。教师可以通过限制代理人可以采取的行动来告知代理人这些条件,从而确保其安全。例如,在为自动驾驶汽车训练强化学习(RL)代理时,根据路况限制汽车的速度是很自然的。如果训练发生在现实世界中,这一点尤为重要,以确保代理人不会盲目探索危险的行为来发现如何驾驶。即便训练发生在模拟环境中,施加这些限制也有助于高效使用探索预算,这与上一节中的提示相关。

机器学习的民主化

当教师训练学生时,他们不会担心学习的生物机制细节,比如哪些化学物质在什么脑细胞之间传递。这些细节被从教师的视野中抽象了出来;神经科学家和研究大脑的专家会发布关于有效教学和学习技术的研究。

就像教师不需要是神经科学家一样,学科专家也不必是机器学习专家才能训练机器学习算法。MT 范式通过开发有效且直观的教学方法,建议将机器学习的低层细节从机器教师中抽象出来。这样,学科专家将更容易将他们的知识注入到机器中。最终,这将导致机器学习的民主化,并使其在更多的应用中得到更广泛的使用。

数据科学通常需要将商业洞察和专业知识与数学工具和软件相结合,以创造价值。当你想将强化学习(RL)应用于商业问题时,情况也是一样的。这通常要求数据科学家了解商业领域,或者要求领域专家了解数据科学,或者让两者领域的人一起合作组成团队。这给许多场景下(高级)机器学习技术的采用带来了很高的门槛,因为很少能在同一个地方找到这两类人。

信息

一项麦肯锡的研究表明,缺乏分析人才是释放数据和分析价值的主要障碍。机器教学,除了它的特定工具外,是一种克服这些障碍的范式,通过创建直观的工具降低非机器学习专家的入门门槛,以此来解决问题。要查看这项研究,请访问mck.co/2J3TFEj。

我们刚刚提到的这个愿景更侧重于长远目标,因为它需要大量的研究和机器学习方面的抽象。我们将在本节中讨论的方法将非常技术性。例如,我们将讨论动作屏蔽方法,根据代理所处的状态限制可用的动作,这将需要编码并修改神经网络的输出。然而,你可以想象一个高级机器教学工具,听到老师说“在城市限速范围内不要超过 40 英里每小时”,然后解析这个命令,在后台为自动驾驶汽车代理实现动作屏蔽:

图 10.2 – 机器教学的未来?

图 10.2 – 机器教学的未来?

在结束这一节并深入探讨机器教学的细节之前,让我先声明一下必要的免责声明。

免责声明

机器教学(MT)方法的最 vocal 拥护者之一是微软及其自动化系统部门。截至本书编写时,我是微软自动化系统组织的一名员工,致力于利用机器教学创建智能系统。然而,我在这里的目标并不是推广任何微软的产品或话语,而是向你介绍这个我认为很重要的新兴话题。此外,我并没有以任何身份正式代表微软,我对这个话题的看法也不一定与公司立场一致。如果你对微软对机器教学的看法感兴趣,可以查看blogs.microsoft.com/ai/machine-teaching/上的博客文章以及www.microsoft.com/en-us/ai/autonomous-systems上的自动化系统网站。

现在,是时候让讨论变得更具体了。在下一节中,我们将看看机器教学的要素。

探索机器教学的要素

由于机器教学是一个新兴领域,定义其元素是非常困难的。不过,让我们来看一下其中一些常见的组成部分和主题。我们已经讨论了机器教师是谁,但为了完整性,让我们从这点开始。接着,我们将探讨概念、课程、训练数据和反馈。

机器教师

机器教师,或简称教师,是当前问题的主题专家。在没有将机器学习与教学解耦的抽象概念的情况下,教师通常是数据科学家——也就是你——但这次的角色是明确地利用你对问题领域的知识来指导训练。

概念

概念是解决问题所需技能集的一个特定部分。想象一下在现实生活中训练一名篮球运动员。训练不仅仅是进行练习赛,还包括掌握个别技能。一些技能如下:

  • 投篮

  • 传球

  • 运球

  • 停止与着陆

传统的强化学习训练篮球智能体通常是通过进行整场比赛来实现,期望智能体通过比赛掌握这些个别技能。机器教学建议将问题拆解成更小的概念进行学习,比如我们之前列出的那些技能。这样做有几个好处:

  • 单一的任务往往伴随着稀疏的奖励,这对于强化学习智能体来说是很难从中学习的。例如,赢得篮球比赛的奖励是+1,输掉比赛则是-1。然而,机器教师会知道,通过掌握个别技能,赢得比赛是可能的。为了训练智能体掌握个别技能和概念,将会为这些技能和概念分配奖励。这有助于绕过稀疏奖励的问题,并为智能体提供更频繁的反馈,从而促进学习。

  • 信用分配问题是强化学习中的一个严重挑战,它涉及到将后期阶段的奖励归因于早期阶段个别动作的难度。当训练被拆分成概念时,更容易看到智能体在哪些概念上不擅长。具体来说,这并不能解决信用分配问题。仍然是教师决定是否掌握某个特定概念很重要。但一旦这些概念被教师定义,就更容易隔离出智能体擅长和不擅长的部分。

  • 作为前述观点的推论,教师可以将更多的训练预算分配给那些需要更多训练和/或难以学习的概念。这样可以更高效地利用时间和计算资源。

正因为如此,一个不切实际或代价高昂的任务,可以通过将其拆分成概念来高效地解决。

课程和教学大纲

机器教学中的另一个重要元素是课程学习。在训练智能体某个概念时,暴露给它专家级难度可能会偏离训练目标。相反,更合理的做法是从一些简单的设置开始,逐步增加难度。这些难度级别分别构成一个课程,它们与定义从一个课程到下一个课程的过渡标准的成功阈值一起,组成了整个课程体系。

课程学习是强化学习中最重要的研究领域之一,我们将在后续详细讲解。课程可以由教师手动设计,也可以使用自动课程算法来生成。

训练材料/数据

与之前的观点相关,机器教学的另一个方面是设计智能体学习的数据。例如,机器教师可以通过使用非策略方法来为训练提供包含成功案例的数据,从而克服困难的探索任务。这些数据可以通过现有的非 RL 控制器或教师的动作获取。这种方法也被称为示范学习。

信息

示范学习是一种流行的强化学习智能体训练方法,特别是在机器人领域。Nair 等人在 ICARA 论文中展示了如何通过示范机器人如何拾取和放置物体来为强化学习智能体的训练提供种子。请查看视频:youtu.be/bb_acE1yzDo。

反过来,教师可以引导智能体远离不良行为。一种有效的实现方式是通过行动掩蔽,即将观察到的可用动作空间限制为一组期望的动作。

另一种设计智能体训练数据的方式是监控智能体的表现,识别其在状态空间中需要更多训练的部分,并将智能体暴露于这些状态中以提升其表现。

反馈

RL 智能体通过奖励的反馈进行学习。设计奖励函数使得学习变得简单——甚至在某些情况下,原本不可能实现的情况也能变得可行——这是机器教师最重要的任务之一。通常这是一个迭代过程。在项目的过程中,奖励函数往往需要多次修订,以促使智能体学习期望的行为。未来的机器教学工具可能涉及通过自然语言与智能体互动,提供这种反馈,从而塑造背后使用的奖励函数。

通过这些内容,我们已经向您介绍了机器教学及其元素。接下来,我们将探讨具体的方法。与其讨论一个样本问题的完整机器教学策略,不如专注于单个方法。根据您的问题需求,您可以将它们作为机器教学策略的构建模块。我们将从最常见的方法开始——奖励函数设计,您可能已经使用过它。

奖励函数设计

奖励函数工程是指在强化学习问题中设计环境的奖励动态,使其反映出你为智能体设定的确切目标。你如何定义奖励函数可能会使训练变得容易、困难,甚至是不可能的。因此,在大多数强化学习项目中,会投入大量精力来设计奖励。在本节中,我们将介绍一些需要设计奖励的具体案例以及如何设计奖励,接着提供一个具体的例子,最后讨论进行奖励函数设计时所面临的挑战。

何时进行奖励函数设计

本书中多次提到,包括在我们讨论概念的上一节中,我们讨论了稀疏奖励给学习带来的问题。解决这一问题的一种方法是奖励塑形,使奖励变得不再稀疏。因此,稀疏奖励是我们需要进行奖励函数设计的常见原因之一。然而,这并不是唯一的原因。并非所有环境/问题都像 Atari 游戏那样为你预定义了奖励。此外,在某些情况下,你可能希望你的智能体实现多个目标。由于这些原因,许多现实生活中的任务需要机器教师根据他们的专业知识来指定奖励函数。接下来,我们将探讨这些情况。

稀疏奖励

当奖励稀疏时,意味着智能体通过一系列不太可能的随机动作,看到奖励的变化(从恒定的 0 变为正数/负数,从恒定的负数变为正数,等等),学习就变得困难。这是因为智能体需要通过随机试错来偶然碰到这个序列,这使得问题的探索变得困难。

与一位竞争性玩家对弈时,奖励设定为赢得比赛为+1、平局为 0、输掉比赛为-1,最后的这个例子就是一个稀疏奖励环境的典型例子。强化学习基准测试中常用的经典例子是《蒙特祖玛的复仇》,这是一款 Atari 游戏,玩家需要收集装备(钥匙、火把等),开门等,才能取得进展,而仅凭随机行动几乎不可能完成:

图 10.3 – 《蒙特祖玛的复仇》

图 10.3 – 《蒙特祖玛的复仇》

在这样的困难探索问题中,一个常见的策略是奖励塑形,即修改奖励,以引导智能体朝向高奖励方向。例如,奖励塑形策略可以是在学习国际象棋时,如果智能体丢掉了皇后,就给它-0.1 的惩罚奖励,丢失其他棋子的惩罚也较小。通过这种方式,机器教师将关于皇后是游戏中重要棋子的知识传达给智能体,尽管丢失皇后或其他棋子(除国王外)本身并不是游戏的目标。

我们将在后续部分更详细地讨论奖励塑形。

定性目标

假设你正在试图教一个类人机器人如何走路。那么,什么是走路?你如何定义走路?如何用数学定义走路?什么样的走路会得到高奖励?仅仅是前进就够了吗,还是其中有一些美学的元素?如你所见,把你脑海中的走路概念转化为数学表达式并不容易。

在他们的著名研究中,DeepMind 的研究人员为他们训练的类人机器人走路使用了以下奖励函数:

在这里, 和 是沿着 和 轴的速度, 是在 轴上的位置, 是速度奖励的截止值,而 是施加在关节上的控制。正如你所看到的,这里有许多任意系数,很可能对于其他种类的机器人会有所不同。实际上,这篇论文为三种不同的机器人身体使用了三个独立的函数。

信息

如果你对机器人在经过这种奖励训练后的走路方式感到好奇,可以观看这个视频:youtu.be/gn4nRCC9TwQ。机器人的走路方式,怎么说呢,有点奇怪……

简而言之,定性目标需要精心设计奖励函数,以实现预期的行为。

多目标任务

在强化学习中,一个常见的情况是任务是多目标的。另一方面,传统上,强化学习算法优化的是标量奖励。因此,当存在多个目标时,它们需要被调和成一个单一的奖励。这通常导致“把苹果和橙子混在一起”,而在奖励中恰当地加权这些目标可能相当困难。

当任务目标是定性时,它通常也是多目标的。例如,驾驶汽车的任务包含了速度、安全性、燃油效率、设备磨损、舒适度等因素。你可以猜到,用数学表达舒适度是件不容易的事情。但也有许多任务需要同时优化多个定量目标。一个例子是控制暖通空调(HVAC)系统,使房间温度尽可能接近指定的设定点,同时最小化能源成本。在这个问题中,机器学习的任务是平衡这些权衡。

在强化学习(RL)任务中,涉及到前述一种或多种情况是非常常见的。接着,设计奖励函数成为了一个主要的挑战。

在经过这么多讨论后,我们稍微更专注于奖励塑造(reward shaping)一点。

奖励塑造

奖励塑造的背后思想是通过使用相对较小的正负奖励(相较于实际奖励和惩罚),激励智能体朝着成功状态前进,并阻止其到达失败状态。这通常会缩短训练时间,因为智能体不会花费过多时间去探索如何达到成功状态。为了让讨论更具体,这里有一个简单的例子。

简单机器人示例

假设一个机器人在一个水平坐标轴上以 0.01 的步长移动。目标是到达+1 并避免-1,它们是终止状态,如下所示:

图 10.4 – 带稀疏奖励的简单机器人示例

图 10.4 – 带稀疏奖励的简单机器人示例

正如你所想,使用稀疏奖励时,例如为到达奖杯给+1,为到达失败状态给-1,机器人很难发现奖杯。如果任务有超时限制,比如 200 步后,任务可能就会结束。

在这个例子中,我们可以通过给机器人提供一个随着其朝奖杯移动而增加的奖励来引导它。一个简单的选择是设定!,其中!是坐标轴上的位置。

这个奖励函数有两个潜在问题:

  • 随着机器人向右移动,继续向右移动的相对增益逐渐变小。例如,从!到!的步进奖励增加了 10%,但从!到!的奖励只增加了 1.1%。

  • 由于智能体的目标是最大化总的累计奖励,因此,智能体最好的选择并不是去达成奖杯目标,因为那样会终止任务。相反,智能体可能会选择永远停留在 0.99 处(或直到时间限制到达)。

我们可以通过塑造奖励来解决第一个问题,使得智能体朝成功状态移动时获得越来越多的附加奖励。例如,我们可以将奖励设置为!,范围为!:

图 10.5 – 奖励塑造的示例,其中

图 10.5 – 奖励塑造的示例,其中!

这样,随着机器人接近奖杯,激励的强度加大,进一步鼓励机器人向右移动。对于向左移动的惩罚情况也是类似的。

为了解决后者,我们应鼓励智能体尽快结束任务。我们需要通过对每一步时间进行惩罚,来促使智能体尽早结束任务。

这个例子展示了两点:即使在如此简单的问题中,设计奖励函数也可能变得复杂,并且我们需要将奖励函数的设计与我们设置的终止条件一起考虑。

在进入具体的奖励塑形建议之前,让我们也讨论一下终端条件在智能体行为中的作用。

终端条件

由于智能体的目标是最大化整个剧集的期望累积奖励,因此剧集的结束方式将直接影响智能体的行为。因此,我们可以并且应该利用一组好的终端条件来引导智能体。

我们可以讨论几种类型的终端条件:

  • 正向终端表示智能体已经完成任务(或部分任务,具体取决于您如何定义成功)。该终端条件伴随着显著的正奖励,以鼓励智能体达成目标。

  • 负向终端表示失败状态,并会给予显著的负奖励。智能体将尽力避免这些条件。

  • 中性终端本身既不是成功也不是失败,但它表明智能体没有成功的路径,且在最后一步时,剧集会以零奖励结束。机器教师不希望智能体在此后继续在环境中待下去,而是应该重置回初始条件。虽然这不会直接惩罚智能体,但它防止了智能体收集额外的奖励(或惩罚)。因此,这是对智能体的隐性反馈。

  • 时间限制限制了在环境中所花费的时间步数。它鼓励智能体在这一时间预算内寻求高奖励,而不是永远游荡。它作为反馈,告知哪些行动序列在合理的时间内能够获得奖励,哪些则不能。

在某些环境中,终端条件是预设的;但在大多数情况下,机器教师具有设置终端条件的灵活性。

现在我们已经描述了所有组件,接下来让我们讨论一些奖励塑形的实用技巧。

奖励塑形的实用技巧

在设计奖励函数时,您应该牢记以下一些一般性指导原则:

  • 尽可能保持步奖励在和之间,以保证数值稳定性。

  • 用可以推广到问题其他版本的术语来表达您的奖励(和状态)。例如,与其奖励智能体到达某一点!,不如根据激励其减少与目标的距离。

  • 拥有平滑的奖励函数将为智能体提供易于跟随的反馈。

  • 智能体应该能够将奖励与其观察结果相关联。换句话说,观察结果必须包含一些信息,说明什么行为导致了高奖励或低奖励。否则,智能体就无法根据这些信息做出决策。

  • 接近目标状态的总激励不应超过到达目标状态的实际奖励。否则,智能体将倾向于集中精力积累这些激励,而不是实现实际目标。

  • 如果您希望智能体尽快完成任务,可以为每个时间步分配一个负奖励。智能体将尝试完成回合,以避免积累负奖励。

  • 如果智能体能够通过不到达终止状态来收集更多的正奖励,它将尝试在达到终止条件之前收集奖励。如果智能体在一个回合内可能只会收集到负奖励(例如,每个时间步都有惩罚),它将尝试尽快到达终止状态。如果智能体的生命过于痛苦,它可能会产生自杀行为,即智能体会寻求任何终止状态,包括自然的或失败的状态,以避免因保持生命而遭受过度的惩罚。

现在,我们将来看一个使用 OpenAI 进行奖励塑形的示例。

示例 – 山地车的奖励塑形

在 OpenAI 的山地车环境中,汽车的目标是到达其中一座山丘顶部的目标点,如图 10.6所示。动作空间包括将汽车推向左、推向右或不施加任何力量:

图 10.6 – 山地车环境

图 10.6 – 山地车环境

由于我们施加的力量不足以爬上山丘并到达目标,汽车需要通过朝相反方向逐渐积累势能来实现目标。弄清楚这一点并不简单,因为汽车在到达目标之前并不知道目标是什么,而这一点可以通过 100 多个正确动作来实现。在默认环境中,唯一的奖励是在每个时间步内给予-1,以鼓励汽车尽可能快地到达目标,从而避免积累负面奖励。该过程在 200 步后结束。

在本章中,我们将使用各种 MT 技术来训练我们的智能体。为此,我们将有一个自定义的训练流程和一个可以用来实验这些方法的定制化环境。首先,先进行设置。

设置环境

我们的自定义MountainCar环境包装了 OpenAI 的MountainCar-v0,其外观如下:

Chapter10/custom_mcar.py

class MountainCar(gym.Env):
    def __init__(self, env_config={}):
        self.wrapped = gym.make("MountainCar-v0")
        self.action_space = self.wrapped.action_space
        ...

如果现在访问该文件,它可能看起来很复杂,因为它包含了一些我们还未讲解的附加组件。目前,只需要知道这是我们将要使用的环境。

我们将在本章中使用 Ray/RLlib 的 Ape-X DQN 来训练我们的智能体。请确保已安装这些工具,最好是在虚拟环境中:

$ virtualenv rlenv
$ source rlenv/bin/activate
$ pip install gym[box2d]
$ pip install tensorflow==2.3.1
$ pip install ray[rllib]==1.0.1

这样,接下来让我们通过训练一个不使用任何 MT 的智能体来获取基线表现。

获取基线表现

我们将使用一个脚本进行所有的训练。在脚本的顶部,我们定义一个STRATEGY常量,用于控制训练中使用的策略:

Chapter10/mcar_train.py

ALL_STRATEGIES = [
    "default",
    "with_dueling",
    "custom_reward",
    ...
]
STRATEGY = "default"

对于每种策略,我们将启动五次不同的训练,每次训练 200 万时间步,因此我们设置 NUM_TRIALS = 5 和 MAX_STEPS = 2e6。在每次训练结束时,我们将对训练好的代理进行评估,共进行 NUM_FINAL_EVAL_EPS = 20 次回合评估。因此,每种策略的结果将反映 100 个测试回合的平均长度,较低的数字表示更好的表现。

对于大多数策略,你会看到我们有两种变体:启用和不启用对战网络。当启用对战网络时,代理会取得接近最优的结果(大约 100 步达到目标),因此对于我们的案例来说变得不再有趣。此外,当我们在本章后续实现动作掩蔽时,为了避免 RLlib 中的复杂性,我们将不会使用对战网络。因此,在我们的示例中,我们将重点关注不使用对战网络的情况。最后,请注意,实验结果将写入 results.csv 文件。

这样,我们就开始训练我们的第一个代理。当我没有使用 MT 时,得到的平均回合长度如下:

STRATEGY = "default"

结果如下:

Average episode length: 192.23

接下来,我们来看看奖励塑造是否能在这里帮助我们。

通过奖励塑造解决问题

任何人看到山地车问题时都会明白,我们应该鼓励汽车向右行驶,至少最终如此。在本节中,我们就是这样做的。汽车的下沉位置对应于 的 -0.5 位置。我们修改奖励函数,为代理提供一个二次增长的奖励,当它越过该位置向右行驶时。这发生在自定义的 MountainCar 环境中:

    def step(self, action):
        self.t += 1
        state, reward, done, info = self.wrapped.step(action)
        if self.reward_fun == "custom_reward":
            position, velocity = state
            reward += (abs(position+0.5)**2) * (position>-0.5)
        obs = self._get_obs()
        if self.t >= 200:
            done = True
        return obs, reward, done, info

当然,欢迎在这里尝试自己定义奖励塑造,以获得更好的理解。

提示

应用恒定奖励,例如 -1,是稀疏奖励的一个例子,尽管步长奖励不是 0。之所以如此,是因为代理在整个回合结束前没有任何反馈;其行动在长时间内不会改变默认奖励。

我们可以通过以下标志启用自定义(塑造的)奖励策略:

STRATEGY = "custom_reward"

我们得到的结果大致如下:

Average episode length: 131.33

显然,这是一个显著的收益,因此我们的奖励塑造函数值得赞扬!不过,诚然,我花了几次迭代才找到能够带来显著改进的方式。这是因为我们鼓励的行为比单纯地向右行驶更复杂:我们希望代理在左右之间来回移动,以加速。这在奖励函数中有点难以捕捉,且很容易让人头疼。

奖励函数工程通常会变得相当棘手且耗时——这让这个话题值得专门的章节来讨论,我们接下来将深入探讨。

奖励函数工程的挑战

强化学习的目标是找到一个策略,最大化代理所收集的期望累计奖励。我们设计并使用非常复杂的算法来克服这一优化挑战。在某些问题中,我们使用数十亿个训练样本来实现这一目标,并尽力挤出一点额外的奖励。经过这一切折腾后,观察到你的代理获得了很高的奖励,但它展示的行为并不是你所期望的,这并不罕见。换句话说,代理学到的东西与你希望它学到的不同。如果你遇到这种情况,不要太生气。因为代理的唯一目的是最大化你指定的奖励。如果这个奖励并不能准确反映你心中的目标,这比你想象的要复杂得多,那么代理的行为也不会符合你的预期。

信息

由于奖励函数指定错误,OpenAI 的 CoastRunners 代理是一个著名的行为异常例子。在游戏中,代理的目标是尽可能快地完成船只竞速,同时沿途收集奖励。经过训练后,代理找到了一种收集更多奖励而不必完成比赛的方法,这违背了原始目的。你可以在 OpenAI 的博客中阅读更多关于它的信息:openai.com/blog/faulty-reward-functions/。

因此,为你的任务指定一个好的奖励函数是至关重要的,尤其是当它包括定性和/或复杂的目标时。不幸的是,设计一个好的奖励函数更多的是一种艺术而非科学,你将通过实践和试错获得直觉。

提示

谷歌的机器学习研究员 Alex Irpan 优美地表达了奖励函数设计的重要性和挑战:“我开始把深度强化学习想象成一个恶魔,它故意误解你的奖励并积极寻找最懒的局部最优解。这有点荒谬,但我发现这实际上是一种富有成效的思维方式。”(Irpan, 2018)。Keras 的作者 François Chollet 说:“损失函数工程可能会成为未来的职业头衔。”(youtu.be/Bo8MY4JpiXE?t=5270)。

尽管面临这些挑战,我们刚才讨论的技巧应该能给你一个良好的开端。其余的将随着经验积累而到来。

这样,我们的奖励函数工程讨论就到此为止。这是一个漫长但必要的讨论。在下一节中,我们将讨论另一个主题——课程学习,这不仅在 MT 中重要,在强化学习中也是如此。

课程学习

当我们学习一项新技能时,我们从基础开始。学习篮球时,弹跳和运球是第一步。做空中接力不是第一课就该尝试的内容。你需要在掌握了前面的基本技能后,逐步进入更高级的课程。按照从基础到高级的课程体系学习,这也是整个教育系统的基础。问题是,机器学习模型是否也能从这种方法中获益?结果证明,确实可以!

在强化学习(RL)的背景下,当我们创建课程时,我们类似地从“简单”的环境配置开始。这种方式可以让智能体尽早了解什么是成功,而不是花费大量时间盲目探索环境,希望能偶然找到成功的办法。如果我们观察到智能体超过了某个奖励阈值,我们就会逐步增加难度。这些难度级别中的每一个都被视为一个课程。已经证明,课程学习能提高训练效率,并使那些对智能体来说无法实现的任务变得可行。

提示

设计课程和过渡标准是一项复杂的工作。它需要深思熟虑和领域专业知识。尽管我们在本章中遵循手动课程设计,但当我们在第十一章《泛化与部分可观察性》和第十四章《机器人学习》中重新讨论该主题时,我们将讨论自动课程生成方法。

在我们的山地车示例中,我们通过修改环境的初始条件来创建课程。通常,当剧集开始时,环境会随机化汽车在山谷底部的位置(),并将速度()设置为 0。在我们的课程中,汽车将在第一课中靠近目标并朝右以较高的速度启动。这样,它将很容易到达目标。随着课程的进展,我们将逐渐将难度恢复到原本的状态。

具体来说,下面是我们如何定义课程的:

  • 课程 0: ,

  • 课程 1: ,

  • 课程 2: ,

  • 课程 3: ,

  • 课程 4(最终版 / 原版): ,

这是如何在环境中设置的:

    def _get_init_conditions(self):
        if self.lesson == 0:
            low = 0.1
            high = 0.4
            velocity = self.wrapped.np_random.uniform(
                low=0, high=self.wrapped.max_speed
            )
        ...

一旦智能体在当前课程中取得了足够的成功,我们将允许它进入下一个课程。我们将这一成功标准定义为在 10 次评估剧集中,平均剧集长度小于 150。我们在训练和评估工作中使用以下功能来设置课程:

CURRICULUM_TRANS = 150
...
def set_trainer_lesson(trainer, lesson):
    trainer.evaluation_workers.foreach_worker(
        lambda ev: ev.foreach_env(lambda env: env.set_lesson(lesson))
    )
    trainer.workers.foreach_worker(
        lambda ev: ev.foreach_env(lambda env: env.set_lesson(lesson))
    )
    ...
def increase_lesson(lesson):
    if lesson < CURRICULUM_MAX_LESSON:
        lesson += 1
    return lesson    if "evaluation" in results:
        if results["evaluation"]["episode_len_mean"] < CURRICULUM_TRANS:
            lesson = increase_lesson(lesson)
            set_trainer_lesson(trainer, lesson)

这些设置随后会被用于训练流程中:

            if results["evaluation"]["episode_len_mean"] < CURRICULUM_TRANS:
                lesson = increase_lesson(lesson)
                set_trainer_lesson(trainer, lesson)
                print(f"Lesson: {lesson}")

所以,这就是我们如何实现手动课程安排。假设你用这个方法训练智能体:

STRATEGY = "curriculum"

你会看到,我们得到的性能接近最优!

Average episode length: 104.66

你刚刚通过课程安排教会了机器某些东西!挺酷的,对吧?现在它应该感觉像是 MT 了!

接下来,我们将看看另一种有趣的方法:使用示范的 MT 方法。

热启和示范学习

一个流行的技巧是通过训练智能体使用来自合理成功控制器(如人类)的数据,来向其展示成功的方式。在 RLlib 中,可以通过保存来自山地车环境的人类游戏数据来实现:

Chapter10/mcar_demo.py

        ...
        new_obs, r, done, info = env.step(a)
        # Build the batch
        batch_builder.add_values(
            t=t,
            eps_id=eps_id,
            agent_index=0,
            obs=prep.transform(obs),
            actions=a,
            action_prob=1.0,  # put the true action probability here
            action_logp=0,
            action_dist_inputs=None,
            rewards=r,
            prev_actions=prev_action,
            prev_rewards=prev_reward,
            dones=done,
            infos=info,
            new_obs=prep.transform(new_obs),
        )
        obs = new_obs
        prev_action = a
        prev_reward = r

这些数据然后可以输入到训练中,该训练实现于 Chapter10/mcar_train.py。当我尝试时,RLlib 在多次尝试中都在训练时由于使用这种方法进行初始化时遇到 NaN 值而停滞。因此,现在你知道了这点,我们将把这个细节留给 RLlib 的文档:docs.ray.io/en/releases-1.0.1/rllib-offline.html,而不是在这里详细讨论。

动作屏蔽

我们将使用的最后一种 MT 方法是动作屏蔽(action masking)。通过这种方法,我们可以根据我们定义的条件,防止智能体在某些步骤中执行特定的动作。对于山地车问题,假设我们有一个直觉:在尝试爬坡之前需要积累动能。因此,如果汽车已经在山谷周围向左移动,我们希望智能体施加左侧的力。那么,在这些条件下,我们将屏蔽所有动作,除了左移:

    def update_avail_actions(self):
        self.action_mask = np.array([1.0] * self.action_space.n)
        pos, vel = self.wrapped.unwrapped.state
        # 0: left, 1: no action, 2: right
        if (pos < -0.3) and (pos > -0.8) and (vel < 0) and (vel > -0.05):
            self.action_mask[1] = 0
            self.action_mask[2] = 0

为了能够使用这个屏蔽,我们需要构建一个自定义模型。对于被屏蔽的动作,我们将所有的 logits 推向负无穷大:

class ParametricActionsModel(DistributionalQTFModel):
    def __init__(
        self,
        obs_space,
        action_space,
        num_outputs,
        model_config,
        name,
        true_obs_shape=(2,),
        **kw
    ):
        super(ParametricActionsModel, self).__init__(
            obs_space, action_space, num_outputs, model_config, name, **kw
        )
        self.action_value_model = FullyConnectedNetwork(
            Box(-1, 1, shape=true_obs_shape),
            action_space,
            num_outputs,
            model_config,
            name + "_action_values",
        )
        self.register_variables(self.action_value_model.variables())
    def forward(self, input_dict, state, seq_lens):
        action_mask = input_dict["obs"]["action_mask"]
        action_values, _ = self.action_value_model(
            {"obs": input_dict["obs"]["actual_obs"]}
        )
        inf_mask = tf.maximum(tf.math.log(action_mask), tf.float32.min)
        return action_values + inf_mask, state

最后,使用这个模型时,我们关闭对抗网络,以避免过于复杂的实现。此外,我们注册我们的自定义模型:

    if strategy == "action_masking":
        config["hiddens"] = []
        config["dueling"] = False
        ModelCatalog.register_custom_model("pa_model", ParametricActionsModel)
        config["env_config"] = {"use_action_masking": True}
        config["model"] = {
            "custom_model": "pa_model",
        }

为了用这种策略训练智能体,请设置以下内容:

STRATEGY = "action_masking"

性能将如下所示:

Average episode length: 147.22

这肯定比默认情况下有所改进,但它仍落后于奖励塑形和课程学习方法。更智能的屏蔽条件和添加对抗网络可以进一步提高性能。

这就是我们在山地车问题中使用的 MT 技巧的结束部分。在总结之前,我们再看一个 MT 中的重要话题。

概念网络

MT 方法的一个重要部分是将问题划分为对应不同技能的概念,以便促进学习。例如,对于自动驾驶汽车,为高速公路上的巡航和超车训练不同的智能体可能有助于提高性能。在某些问题中,概念之间的划分更加明确。在这些情况下,为整个问题训练一个单一的智能体通常会带来更好的表现。

在结束这一章之前,我们来谈谈 MT 方法的一些潜在缺点。

MT 的缺点与前景

MT 方法有两个潜在的缺点。

首先,通常很难提出好的奖励塑造、好的课程设计、一组动作屏蔽条件等等。这在某些方面也违背了从经验中学习、无需进行特征工程的初衷。另一方面,凡是我们能够做到的,特征工程和 MT 在帮助智能体学习并提高数据效率方面可能非常有帮助。

其次,当我们采用 MT 方法时,可能会将教师的偏见传递给智能体,这可能会阻碍其学习更好的策略。机器教师需要尽量避免这种偏见。

做得好!我们已经完成了这一激动人心章节的内容。接下来,让我们总结一下我们所覆盖的内容。

摘要

在本章中,我们讨论了人工智能中的一个新兴范式——MT,它涉及将学科专家(教师)的专业知识有效传递给机器学习算法。我们讨论了这与人类教育的相似之处:通过在他人知识的基础上建立,通常不需要重新发明这些知识。这种方法的优点是极大提高了机器学习中的数据效率,并且在某些情况下,使得没有教师的情况下本不可能实现的学习变得可能。我们讨论了该范式中的各种方法,包括奖励函数工程、课程学习、示范学习、动作屏蔽和概念网络。我们观察到这些方法中的一些显著提高了 Ape-X DQN 的基本应用。最后,我们还介绍了这一范式的潜在弊端,即教学过程和工具的设计难度,以及可能引入的偏见。尽管有这些弊端,MT 将在不久的将来成为强化学习科学家工具箱中的标准组成部分。

在下一章中,我们将讨论泛化和部分可观察性,这是强化学习中的一个关键主题。在这个过程中,我们将再次回顾课程学习,并看看它如何帮助创建鲁棒的智能体。

另一面见!

参考文献

第十一章:第十一章:泛化与领域随机化

深度强化学习(RL)已经实现了早期 AI 方法无法做到的事情,例如在围棋、Dota 2 和星际争霸 II 等游戏中击败世界冠军。然而,将 RL 应用于现实问题仍然充满挑战。实现这一目标的两个关键障碍是将训练好的策略推广到广泛的环境条件,并开发能够处理部分可观测性的策略。正如我们将在本章中看到的,这两个挑战紧密相关,我们将提出解决方案。

本章将覆盖以下内容:

  • 泛化与部分可观测性的概述

  • 泛化的领域随机化

  • 利用记忆克服部分可观测性

这些主题对理解 RL 在现实世界应用中的成功实施至关重要。让我们立即深入探讨吧!

泛化与部分可观测性的概述

正如所有机器学习一样,我们希望我们的 RL 模型不仅能在训练数据上工作,还能在测试时应对广泛的条件。然而,当你开始学习 RL 时,过拟合的概念不像在监督学习中那样被优先讨论。在本节中,我们将比较监督学习和 RL 中的过拟合与泛化,描述泛化与部分可观测性之间的密切关系,并提出一个通用方法来应对这些挑战。

监督学习中的泛化与过拟合

监督学习中最重要的目标之一,例如在图像识别和预测中,就是防止过拟合,并在未见过的数据上获得高准确性——毕竟,我们已经知道训练数据中的标签。为此,我们使用各种方法:

  • 我们为模型训练、超参数选择和模型性能评估分别使用独立的训练集、开发集和测试集。模型不应根据测试集进行修改,以确保对模型性能的公正评估。

  • 我们使用各种正则化方法,例如惩罚模型方差(例如 L1 和 L2 正则化)和丢弃法,以防止过拟合。

  • 我们尽可能使用大量数据来训练模型,这本身就具有正则化作用。在数据不足的情况下,我们利用数据增强技术生成更多数据。

  • 我们的目标是拥有一个多样化的数据集,并且该数据集与我们在模型部署后期望看到的数据分布相同。

这些概念在你学习监督学习时就会出现,并且它们构成了如何训练模型的基础。然而,在 RL 中,我们似乎并没有以相同的心态来看待过拟合。

让我们更详细地了解 RL 中的不同之处,探讨原因,以及泛化是否真的不那么重要。

RL 中的泛化与过拟合

深度监督学习众所周知需要大量的数据。但深度强化学习对数据的需求远远超过了深度监督学习,因为反馈信号中存在噪声,而且强化学习任务本身的复杂性。训练强化学习模型常常需要数十亿的数据点,并且持续多个月。由于使用物理系统生成如此庞大的数据几乎不可能,深度强化学习研究已经利用了数字环境,如仿真和视频游戏。这模糊了训练与测试之间的界限。

想一想:如果你训练一个强化学习代理来玩雅达利游戏,比如《太空入侵者》(Space Invaders),并且训练得非常好(这是大多数强化学习算法的基准测试),并且使用了大量的数据,那么如果代理在训练后玩《太空入侵者》非常好,这样有问题吗?嗯,在这种情况下,没有问题。然而,正如你可能已经意识到的,这样的训练工作流程并没有像监督学习模型训练那样采取任何措施来防止过拟合。可能你的代理已经记住了游戏中的各种场景。如果你只关心雅达利游戏,那么看起来强化学习中的过拟合似乎并不是一个问题。

当我们离开雅达利(Atari)环境,例如训练一个代理去击败人类竞争者,比如围棋、Dota 2 和星际争霸 II,过拟合开始成为一个更大的问题。正如我们在第九章中看到的,多智能体强化学习,这些代理通常是通过自我对弈进行训练的。在这种环境下,一个主要的危险是代理会过拟合彼此的策略。为了防止这种情况,我们通常会训练多个代理,并让它们分阶段地相互对战,这样一个代理就能遇到多样化的对手(从代理的视角来看就是环境),从而减少过拟合的机会。

过拟合在强化学习(RL)中成为了一个巨大的问题,远远超过了我们之前提到的两种情况,即我们在仿真环境中训练模型并将其部署到物理环境中。这是因为,无论仿真有多高保真度,它(几乎)永远不会与现实世界完全相同。这就是所谓的sim2real gap(仿真到现实的差距)。仿真涉及许多假设、简化和抽象。它只是现实世界的一个模型,正如我们都知道的,所有模型都是错误的。我们无处不在地使用模型,那为什么这一下子在强化学习中变成了一个主要问题呢?嗯,原因在于,训练和强化学习代理需要大量的数据(这也是我们最初需要仿真环境的原因),而在相似数据上长期训练任何机器学习模型都会导致过拟合。因此,强化学习模型极有可能会过拟合仿真环境中的模式和怪癖。在这种情况下,我们真的需要强化学习策略能够超越仿真环境,从而使其变得有用。这对强化学习来说是一个严峻的挑战,也是将强化学习应用于实际应用中的最大障碍之一。

sim2real 差距是一个与部分可观察性密切相关的概念。接下来我们将讨论这一联系。

泛化与部分可观察性之间的联系

我们提到过,模拟永远不可能和现实世界完全相同。这种差异可以通过以下两种形式表现出来:

  • 在一些问题中,你永远无法在模拟中得到与现实世界完全相同的观察结果。训练自动驾驶汽车就是一个例子。现实场景总是不同的。

  • 在某些问题中,你可以训练代理在与现实世界中看到的相同观察下进行操作——例如一个工厂生产规划问题,其中观察到的内容包括需求预测、当前库存水平和机器状态。如果你知道观察数据的范围,你可以设计模拟以反映这一点。然而,模拟和现实生活总会有所不同。

在前一种情况中,你的训练代理可能在模拟之外无法很好地泛化,这一点更为明显。然而,后一种情况就有些微妙了。在这种情况下,尽管观察结果相同,但两种环境之间的世界动态可能并不一致(诚然,这对于前一种情况也会是一个问题)。你能回忆起这和什么相似吗?部分可观察性。你可以将模拟与现实世界之间的差距视为部分可观察性的结果:有一个环境的状态被隐藏,影响了转移动态。所以,即使代理在模拟和现实世界中做出相同的观察,它并未看到这个我们假设用来捕捉两者差异的隐藏状态。

正因为有这种联系,我们在本章中将泛化和部分可观察性一并讨论。话虽如此,即使在完全可观察的环境中,泛化仍然可能是一个问题,而即便泛化不是主要问题,我们也可能需要处理部分可观察性。我们稍后会探讨这些维度。

接下来,让我们简要讨论如何解决这些挑战,然后再深入到后续章节的细节中。

通过记忆克服部分可观察性

你还记得第一次进入高中教室时的感觉吗?很可能有很多新面孔,你希望交朋友。然而,你不会仅凭第一印象就接近别人。尽管第一印象确实能让我们了解一些关于人的信息,但它仅仅反映了他们的一部分。你真正想做的是随着时间的推移进行观察,然后再做判断。

在强化学习(RL)背景下情况类似。以下是来自 Atari 游戏《Breakout》的一个例子:

图 11.1 – Atari 游戏的单一帧画面提供了环境的部分观察

图 11.1 – Atari 游戏的单一帧画面提供了环境的部分观察

从单一游戏场景中并不十分清楚球的移动方向。如果我们有来自先前时刻的另一张快照,我们就能估算球的位置变化和速度变化。再多一张快照,能够帮助我们估算速度变化、加速度等等。因此,当环境是部分可观察的时,根据不仅是单一观察,而是一系列观察采取行动,会导致更有依据的决策。换句话说,拥有记忆使得 RL 代理能够发现单一观察中不可见的部分。

在 RL 模型中,有多种方式可以保持记忆,我们将在本章稍后详细讨论这些方法。

在此之前,让我们简要讨论如何克服过拟合。

通过随机化克服过拟合

如果你是一名司机,想想你是如何在不同条件下获得驾驶经验的。你可能开过小车、大车、加速快慢不一的车、车身高低不同的车,等等。此外,你可能还在雨天、雪天、沥青路面和碎石路面上开过车。我个人就有过这些经历。所以,当我第一次试驾特斯拉 Model S 时,刚开始确实感觉是一种完全不同的体验。但几分钟后,我就习惯了,并开始驾驶得相当舒适。

现在,作为一名司机,我们通常无法精确地定义一辆车与另一辆车之间的差异:它们在重量、扭矩、牵引力等方面的确切差异,使得我们在面对环境时会有部分不可见的感知。但是,我们在多样化驾驶条件下的过去经验帮助我们在开车几分钟后快速适应新的环境。这是如何发生的呢?我们的脑袋能够为驾驶过程建立一个通用的物理模型(并作出相应的行为),当经历足够多样时,而不是“过拟合”某种驾驶风格到特定的车和条件上。

我们处理过拟合并实现泛化的方法在我们的 RL(强化学习)代理中是类似的。我们将使代理暴露于许多不同的环境条件,包括那些它不一定能够完全观察到的条件,这被称为领域随机化(DR)。这将为代理提供超越模拟及其训练条件的泛化所必需的经验。

除此之外,我们在监督学习中使用的正则化方法对于 RL 模型也非常有帮助,正如我们将要讨论的那样。

接下来,让我们在下一节中总结与泛化相关的讨论。

泛化的配方

如今通过前面的示例,应该已经很清楚,我们实现泛化需要三种成分:

  • 多样化的环境条件帮助代理丰富其经验

  • 模型记忆帮助代理发现环境中的潜在条件,尤其是当环境是部分可观察时

  • 使用类似于监督学习中的正则化方法

现在是时候让讨论更具体,谈论如何处理泛化和部分可观测性的方法了。我们从使用域随机化进行泛化开始,然后讨论使用记忆克服部分可观测性。

域随机化用于泛化

我们之前提到过,经验的多样性有助于泛化。在强化学习(RL)中,我们通过在训练过程中随机化环境参数来实现这一点,这被称为 DR。这些参数的例子,比如对于一个搬运和操作物体的机器人手来说,可能如下:

  • 物体表面的摩擦系数

  • 重力

  • 物体的形状和重量

  • 输入执行器的功率

深度强化学习(DR)在机器人应用中尤其受欢迎,因为它能够克服仿真与现实之间的差距,因为智能体通常在仿真环境中训练,并在现实世界中部署。然而,每当涉及到泛化时,DR 是训练过程中必不可少的一部分。

为了更具体地说明环境参数是如何随机化的,我们需要讨论如何表示相同类型问题的两个环境可能会有所不同。

随机化的维度

从Rivilin(2019)借鉴的,下面的章节展示了相似环境之间差异的有用分类。

相同/相似状态下的不同观察结果

在这种情况下,两个环境发出的观察结果不同,尽管底层的状态和转移函数是相同的或非常相似的。一个例子是相同的 Atari 游戏场景,但背景和纹理颜色不同。游戏状态没有变化,但观察结果有所不同。一个更现实的例子是,当训练自动驾驶汽车时,仿真中的场景呈现“卡通化”外观,而在真实摄像头输入下却呈现真实的外观,即使是完全相同的场景。

解决方案——向观察结果中添加噪声

在这些情况下,帮助泛化的方法是向观察结果中添加噪声,这样强化学习模型就能专注于那些实际重要的观察模式,而不是对无关细节进行过拟合。很快,我们将讨论一种具体的方法,叫做网络随机化,它将解决这个问题。

相同/相似的观察结果对应不同的状态

POMDP(部分可观察马尔可夫决策过程)之间的另一个区别是,当观察结果相同或相似时,底层状态实际上是不同的,这也叫做同态状态(aliased states)。一个简单的例子是两个不同版本的山地车环境,外观完全相同,但重力不同。这种情况在机器人应用中非常常见。考虑用机器人手操作物体,正如 OpenAI 的著名工作所展示的(我们将在本章后面详细讨论):摩擦、重量、输入执行器的实际功率等,可能在不同的环境版本之间有所不同,而这些差异对智能体是不可观察的。

解决方案 – 在随机化的环境参数下进行训练,并使用记忆

在这里应该采取的方法是,在多个不同版本的环境中进行训练,每个版本有不同的底层参数,并在强化学习(RL)模型中加入记忆。这将帮助智能体揭示环境的潜在特征。一个著名的例子是 OpenAI 的机器人手 manipulating objects(操作物体)在模拟中训练,容易受到模拟与现实之间差距(sim2real gap)的影响。我们可以通过随机化模拟参数来克服这种差距,适用于以下情境:

当与记忆结合使用,并且经过大多数环境条件的训练后,策略有望获得适应所处环境的能力。

同一问题类别的不同复杂度层次

这是我们本质上处理相同类型问题,但在不同复杂度层次下的情况。一个来自Rivlin,2019的好例子是带有不同节点数量的旅行商问题(TSP)。在这个环境中,RL 智能体的任务是在每个时间步决策下一个访问的节点,使得所有节点都被访问一次且以最小成本完成,同时在最后回到初始节点。事实上,我们在 RL 中处理的许多问题自然面临这种挑战,比如训练一个象棋智能体与不同水平的对手对战等。

解决方案 – 通过课程在不同复杂度层次下进行训练

不出所料,在不同难度层次的环境中训练智能体是实现泛化的必要条件。也就是说,使用一种从简单环境配置开始,逐渐增加难度的课程,如我们之前在书中所描述的。这将可能使学习更加高效,甚至在某些没有课程时无法实现的情况下变得可行。

现在我们已经介绍了实现不同维度泛化的高级方法,接下来将讨论一些具体的算法。但首先,让我们介绍一个用于量化泛化的基准环境。

量化泛化

有多种方法可以测试某些算法/方法在未见过的环境条件下是否比其他方法更好地进行泛化,例如以下几种:

  • 创建具有不同环境参数集的验证和测试环境

  • 在现实生活中的部署中评估策略性能

执行后者并不总是可行的,因为现实生活中的部署可能不一定是一个选项。前者的挑战是确保一致性,并确保验证/测试数据在训练过程中没有被使用。此外,当基于验证性能尝试太多模型时,也可能会对验证环境进行过拟合。克服这些挑战的一种方法是使用程序化生成的环境。为此,OpenAI 创建了 CoinRun 环境,用于基准测试算法的泛化能力。我们来更详细地了解一下。

CoinRun 环境

CoinRun 环境是一个角色尝试在没有碰到任何障碍物的情况下收集硬币的游戏。角色从最左边开始,硬币在最右边。关卡是根据底层概率分布程序化生成的,难度各异,如下所示:

图 11.2:CoinRun 中的两个关卡,难度不同(来源:Cobbe 等,2018)

图 11.2:CoinRun 中的两个关卡,难度不同(来源:Cobbe 等,2018)

以下是关于环境奖励函数和终止条件的更多细节:

  • 环境中有动态和静态障碍物,当角色与其碰撞时会死亡,从而终止该回合。

  • 只有在收集到硬币时才会给予奖励,这也会终止回合。

  • 每个回合有 1,000 步的时间限制,回合会在时间到达时终止,除非角色死亡或到达硬币。

请注意,CoinRun 环境生成的所有(训练和测试)关卡都来自相同的分布,因此它不会测试策略的超出分布(外推)性能。

接下来,我们来安装这个环境并进行实验。

安装 CoinRun 环境

你可以按照以下步骤安装 CoinRun 环境:

  1. 我们从设置和激活一个虚拟 Python 环境开始,因为 CoinRun 需要特定的包。因此,在你选择的目录中运行以下命令:

    virtualenv coinenv
    source coinenv/bin/activate
    
  2. 然后,我们安装必要的 Linux 包,包括一个著名的并行计算接口 MPI:

    sudo apt-get install mpich build-essential qt5-default pkg-config
    
  3. 然后,我们安装从 GitHub 仓库获取的 Python 依赖项和 CoinRun 包:

    git clone https://github.com/openai/coinrun.git
    cd coinrun
    pip install tensorflow==1.15.3 # or tensorflow-gpu
    pip install -r requirements.txt
    pip install -e .
    

    请注意,我们需要安装一个旧版本的 TensorFlow。官方建议使用 CoinRun 的创建者推荐的 TensorFlow 1.12.0 版本。然而,使用较新的 TensorFlow 1.x 版本可能有助于避免使用 GPU 时出现的 CUDA 冲突。

  4. 你可以使用键盘上的箭头键通过以下命令来测试环境:

    python -m coinrun.interactive
    

很棒;祝你玩得开心,玩 CoinRun 愉快!我建议你先熟悉一下这个环境,以便更好地理解我们接下来要讨论的比较。

信息

你可以访问 CoinRun 的 GitHub 仓库,获取完整的命令集:github.com/openai/coinrun。

引入该环境的论文(Cobbe 等,2018)还提到了各种正则化技术如何影响 RL 中的泛化能力。我们将接下来讨论这一点,然后介绍其他的泛化方法。

正则化和网络架构对 RL 策略泛化的影响

作者发现,在监督学习中用于防止过拟合的几种技术,在强化学习(RL)中也同样有效。由于在论文中复现实验结果需要非常长的时间,每个实验需要数亿步,因此我们在此不再尝试复现。相反,我们为你提供了结果摘要和运行不同版本算法的命令。但你可以观察到,即使在 500k 时间步之后,应用我们接下来提到的正则化技术也能提高测试表现。

你可以通过以下命令查看该环境的所有训练选项:

python -m coinrun.train_agent –help

让我们从运行一个没有任何正则化应用的基准开始。

基础训练

你可以使用 PPO 和 Impala 架构训练一个 RL 代理,而不对泛化能力进行任何改进,如下所示:

python -m coinrun.train_agent --run-id BaseAgent --num-levels 500 --num-envs 60

这里,BaseAgent是你为代理决定的 ID,--num-levels 500表示训练时使用 500 个游戏关卡,并使用论文中默认的种子,--num-envs 60启动 60 个并行环境进行回滚,你可以根据机器上可用的 CPU 数量调整该参数。

为了在三个并行会话中测试训练好的代理,每个会话有 20 个并行环境,每个环境有五个关卡,你可以运行以下命令:

mpiexec -np 3 python -m coinrun.enjoy --test-eval --restore-id BaseAgent -num-eval 20 -rep 5

平均测试奖励将显示在mpi_out中。在我的情况下,奖励从训练后的 300K 时间步的约 5.5 降至测试环境中的 0.8,以给你一个大概的参考。

此外,你可以通过运行以下命令来观察你的训练代理:

python -m coinrun.enjoy --restore-id BaseAgent –hres

这实际上是很有趣的。

使用更大的网络

作者发现,正如在监督学习中一样,使用更大的神经网络通过更高的容量成功解决更多的测试场景,从而提高了泛化能力。他们还指出,然而,随着网络规模的增大,泛化能力的提升是递减的,因此泛化能力并不会随着网络规模线性提升。

要使用一个具有五个残差块的架构,而不是三个,每层中的通道数是原来的两倍,你可以添加impalalarge参数:

python -m coinrun.train_agent --run-id LargeAgent --num-levels 500 --num-envs 60 --architecture impalalarge

同样,你可以使用为大代理案例提供的运行 ID 进行测试评估。

训练数据的多样性

为了测试多样化训练数据的重要性,作者比较了两种类型的训练,均包含 256M 时间步数,跨越 100 个和 10,000 个游戏关卡(通过--num-levels控制)。使用更多样化的数据后,测试性能从 30%提升到 90%以上(这也与训练性能相当)。

提示

增加数据多样性在监督学习和强化学习(RL)中起到了正则化的作用。这是因为随着多样性的增加,模型必须在相同的模型容量下解释更多的变化,从而迫使模型利用其容量专注于输入中的最重要模式,而不是过拟合噪声。

这强调了环境参数随机化在实现泛化中的重要性,稍后我们将在本章中单独讨论这一点。

Dropout 和 L2 正则化

实验结果表明,dropout 和 L2 正则化都能改善泛化能力,分别增加了约 5%和 8%的成功率,基准测试性能约为 79%。

提示

如果您需要复习 dropout 和 L2 正则化,可以查看 Chitta Ranjan 的博客:towardsdatascience.com/simplified-math-behind-dropout-in-deep-learning-6d50f3f47275。

我们可以通过以下方式更详细地探讨这个问题:

  • 从经验上来看,作者发现最佳的 L2 权重为 ,最佳的 dropout 率为 0.1。

  • 从经验来看,L2 正则化对泛化的影响比 dropout 更大。

  • 如预期所示,使用 dropout 的训练收敛速度较慢,因此分配了两倍的时间步数(512M)。

要在普通代理的基础上使用 0.1 的 dropout 率,可以使用以下命令:

python -m coinrun.train_agent --run-id AgentDOut01 --num-levels 500 --num-envs 60 --dropout 0.1

同样,要使用 L2 正则化,权重为 0.0001,可以执行以下操作:

python -m coinrun.train_agent --run-id AgentL2_00001 --num-levels 500 --num-envs 60 --l2-weight 0.0001

在您的 TensorFlow RL 模型中,您可以通过以下方式使用Dropout层来添加 dropout:

from tensorflow.keras import layers 
...
x = layers.Dense(512, activation="relu")(x) 
x = layers.Dropout(0.1)(x)
...

要添加 L2 正则化,可以做类似以下操作:

from tensorflow.keras import regularizers
...
x = layers.Dense(512, activation="relu", kernel_regularizer=regularizers.l2(0.0001))(x) 

信息

TensorFlow 有一个非常好的关于过拟合和欠拟合的教程,您可以查看:www.tensorflow.org/tutorials/keras/overfit_and_underfit。

接下来,让我们讨论数据增强。

使用数据增强

一种常见的防止过拟合的方法是数据增强,即对输入进行修改/扭曲,通常是随机的,以增加训练数据的多样性。当应用于图像时,这些技术包括随机裁剪、改变亮度和锐度等。以下是使用数据增强的 CoinRun 场景示例:

图 11.3 – 带数据增强的 CoinRun(来源:Cobbe 等,2018 年)

图 11.3 – 带数据增强的 CoinRun(来源:Cobbe 等,2018 年)

信息

关于数据增强的 TensorFlow 教程,请查看www.tensorflow.org/tutorials/images/data_augmentation。

数据增强,事实证明,在强化学习(RL)中也很有帮助,它能够提升测试性能,虽然略逊色于 L2 正则化。

使用批量归一化

批量归一化是深度学习中的关键工具之一,它有助于稳定训练并防止过拟合。

信息

如果你需要复习批量归一化的知识,可以查看 Chris Versloot 的博客:bit.ly/3kjzjno。

在 CoinRun 环境中,你可以通过如下方式启用训练中的批量归一化层:

python -m coinrun.train_agent --run-id AgentL2_00001 --num-levels 500 --num-envs 60 --use-data-augmentation 1

这将在每个卷积层后添加一个批量归一化层。

当你实现自己的 TensorFlow 模型时,批量归一化层的语法是layers.BatchNormalization(),并且可以传入一些可选的参数。

报告结果显示,使用批量归一化在所有其他正则化方法中(除了增加训练数据的多样性)能为测试性能提供第二大的提升。

添加随机性

最后,向环境中引入随机性/噪声被证明是最有用的泛化技术,能够将测试性能提高约 10%。本文在训练中使用了两种方法与 PPO 算法:

  • 使用!-贪婪策略(通常与 Q 学习方法一起使用)

  • 在 PPO 中增加熵奖励系数(),以鼓励策略提出的动作具有更多的方差

这些方法的良好超参数选择分别是! 和 。需要注意的是,如果环境的动态已经高度随机化,那么引入更多的随机性可能没有那么显著的影响。

结合所有方法

在训练过程中同时使用所有这些正则化方法,仅稍微改善了单个方法带来的提升,表明这些方法各自都在防止过拟合方面起到了类似的作用。需要注意的是,无法确定这些方法对所有 RL 问题的影响完全相同。但我们需要记住的是,传统的监督学习正则化方法也能显著影响 RL 策略的泛化能力。

这就结束了我们关于 RL 的基本正则化技术的讨论。接下来,我们将研究另一种方法,这种方法紧随原始 CoinRun 论文之后,即网络随机化。

网络随机化和特征匹配

网络随机化,由 Lee 等人于 2020 年提出,简单地涉及使用观测的随机变换,!,如下所示:

然后,变换后的观察值作为输入被送入 RL 算法中使用的常规策略网络。在这里,是此变换的参数,每次训练迭代时都会随机初始化。通过在输入层之后添加一个不可训练并且定期重新初始化的层,可以简单实现这一点。在 TensorFlow 2 中,可以按如下方式实现一个在每次调用后转换输入的随机化层:

class RndDense(tf.keras.layers.Layer):
    def __init__(self, units=32):
        super(RndDense, self).__init__()
        self.units = units
    def build(self, input_shape):  
        self.w_init = tf.keras.initializers.GlorotNormal()
        self.w_shape = (input_shape[-1], self.units)
        self.w = tf.Variable(
            initial_value=self.w_init(shape=self.w_shape, dtype="float32"),
            trainable=True,
        )
    def call(self, inputs):  
        self.w.assign(self.w_init(shape=self.w_shape, dtype="float32"))
        return tf.nn.relu(tf.matmul(inputs, self.w))

请注意,这个自定义层具有以下特性:

  • 权重是不可训练的

  • 在每次调用时为层分配随机权重

对该架构的进一步改进是进行两次前向传递,一次带有随机输入,一次不带,并强制网络给出相似的输出。这可以通过向 RL 目标添加一个损失来实现,该损失惩罚输出差异:

在这里,是策略网络的参数,而是策略网络中倒数第二层(即在输出动作的层之前的那一层)。这被称为特征匹配,它使得网络能够区分输入中的噪声和信号。

信息

该架构在 CoinRun 环境中的 TensorFlow 1.x 实现可以在github.com/pokaxpoka/netrand找到。通过将random_ppo2.py与ppo2.py进行比较,并将random_impala_cnn方法与impala_cnn方法在policies.py中的对比,您可以将其与原始的 CoinRun 环境进行比较。

回到我们之前提到的泛化维度,网络随机化有助于 RL 策略在这三个维度上进行泛化。

接下来,我们将讨论一种实现泛化的关键方法,该方法在现实生活中已被证明有效。

泛化的课程学习

我们已经讨论过,丰富的训练经验有助于 RL 策略更好地泛化。假设在您的机器人应用中,您已识别出需要在环境中随机化的两个参数,并为它们指定了最小值和最大值:

  • 摩擦:

  • 执行器扭矩:

这里的目标是使代理准备好在测试时应对具有未知摩擦-扭矩组合的环境。

事实证明,正如我们在上一章讨论课程学习时提到的,训练可能会导致一个平庸的智能体,如果你在一开始就尝试在这些参数的整个范围内进行训练。这是因为参数范围的极端值可能对尚未掌握任务基础的智能体来说过于具有挑战性(假设这些极端值围绕着某些合理的参数值)。课程学习的核心思想是从简单的场景开始,比如第一课可以是 和 ,然后通过扩展范围逐步增加难度。

接下来,一个关键问题是我们应该如何构建课程,课程应该是什么样子(也就是说,当智能体在当前课题中成功后,下一步的参数范围应是什么),以及何时宣布当前课题的成功。在本节中,我们将讨论两种自动生成和管理课程的课程学习方法,以有效进行领域随机化。

自动领域随机化

自动领域随机化(ADR)是 OpenAI 在其研究使用机器人手操控魔方时提出的一种方法。这是 RL 在机器人技术应用中最成功的案例之一,原因有很多:

  • 灵活的机器人由于其高度的自由度,控制起来 notoriously 困难。

  • 策略完全在仿真中训练,然后成功地转移到物理机器人上,成功弥合了模拟与现实之间的差距。

  • 在测试时,机器人成功地在训练时未曾见过的条件下完成任务,例如手指被绑住、戴上橡胶手套、与各种物体发生扰动等。

    这些结果在训练策略的泛化能力方面是非常出色的。

    信息

    你应该查看这篇关于这一重要研究的博客文章,openai.com/blog/solving-rubiks-cube/。它包含了很好的可视化和对结果的深刻见解。

ADR 是该应用成功的关键方法。接下来,我们将讨论 ADR 是如何工作的。

ADR 算法

我们在训练过程中创建的每个环境都会对某些参数进行随机化,例如在前面的例子中,摩擦力和扭矩。为了正式表示这一点,我们说一个环境,,是由来参数化的,其中是参数的数量(在这个例子中为 2)。当一个环境被创建时,我们从一个分布中抽取,。ADR 调整的是参数分布的,从而改变不同参数样本的可能性,使得环境变得更难或更容易,具体取决于智能体在当前难度下是否成功。

一个例子,,将包括每个参数维度的均匀分布,,其中有。与我们的例子相联系,对应摩擦系数,。然后,对于初始课程,我们将有。对于扭矩参数也类似,。然后,变为以下内容:

ADR 建议如下:

  • 随着训练的进行,分配一些环境用于评估,以决定是否更新。

  • 在每个评估环境中,选择一个维度,,然后选择上限或下限之一进行关注,例如和。

  • 将选定维度的环境参数固定在选定的边界上。其余参数从中抽样。

  • 评估智能体在给定环境中的表现,并将该回合中获得的总奖励保存在与维度和边界相关的缓冲区中(例如,)。

  • 当缓冲区中的结果足够时,将平均奖励与您事先设定的成功和失败阈值进行比较。

  • 如果给定维度和边界的平均表现高于您的成功阈值,则扩展该维度的参数范围;如果低于失败阈值,则缩小范围。

总结来说,ADR 系统地评估每个参数维度在参数范围边界上的智能体表现,然后根据智能体的表现扩大或缩小范围。您可以参考论文中的伪代码,它应该与前面的解释配合使用时很容易理解。

接下来,我们将讨论另一种重要的自动课程生成方法。

使用高斯混合模型的绝对学习进展

另一种自动课程生成的方法是使用高斯混合模型的绝对学习进展(ALP-GMM)方法。该方法的本质如下:

  • 用于识别环境参数空间中表现出最多学习进展的部分(称为 ALP 值)

  • 为了将多个 GMM 模型拟合到 ALP 数据上,使用个核,然后选择最佳的一个

  • 从最佳 GMM 模型中采样环境参数

这个想法源于认知科学,用来建模婴儿早期的语言发展。

新采样的参数向量的 ALP 分数,,计算公式如下:

这里,是通过获得的回合奖励,是先前回合获得的最接近的参数向量,是与相关的回合奖励。所有的对都保存在一个数据库中,表示为,通过它来计算 ALP 分数。然而,GMM 模型是使用最新的对来获得的。

请注意,具有较高 ALP 分数的参数空间部分更有可能被采样以生成新环境。高 ALP 分数表明该区域有学习潜力,可以通过观察新采样的下回合奖励的大幅下降或增加来获得。

信息

ALP-GMM 论文的代码库可以在github.com/flowersteam/teachDeepRL找到,其中还包含了展示算法如何工作的动画。由于篇幅限制,我们无法在这里详细讲解代码库,但我强烈建议你查看实现和结果。

最后,我们将提供一些关于泛化的额外资源,供进一步阅读。

Sunblaze 环境

本书中无法覆盖所有的泛化方法,但一个有用的资源是 Packer & Gao, 2019 年发布的博客,介绍了 Sunblaze 环境,旨在系统地评估强化学习(RL)的泛化方法。这些环境是经典的 OpenAI Gym 环境的修改版,经过参数化以测试算法的插值和外推性能。

信息

你可以在bair.berkeley.edu/blog/2019/03/18/rl-generalization/找到描述 Sunblaze 环境及其结果的博客文章。

做得非常棒!你已经了解了有关真实世界强化学习中最重要的主题之一!接下来,我们将讨论一个紧密相关的话题,即克服部分可观察性。

利用记忆克服部分可观察性

在本章开始时,我们描述了内存作为一种处理部分可观察性的有用结构。我们还提到过,泛化问题往往可以看作是部分可观察性的结果:

  • 一个区分两个环境(例如模拟环境与真实世界)的隐藏状态可以通过内存揭示出来。

  • 当我们实现领域随机化时,我们的目标是创建许多版本的训练环境,其中我们希望代理为世界动态建立一个总体模型。

  • 通过内存,我们希望代理能够识别它所在环境的特征,即使它在训练过程中没有看到过这个特定的环境,然后相应地进行行动。

现在,模型的内存不过是将一系列观察作为输入处理的方式。如果你曾经处理过其他类型的序列数据与神经网络结合的任务,比如时间序列预测或自然语言处理(NLP),你可以采用类似的方法,将观察内存作为 RL 模型的输入。

让我们更详细地了解如何实现这一点。

堆叠观察

将观察序列传递给模型的一种简单方法是将它们拼接在一起,并将这个堆叠视为单一的观察。将时间点上的观察表示为,记作,我们可以形成一个新的观察,,并将其传递给模型,如下所示:

这里,是内存的长度。当然,对于,我们需要以某种方式初始化内存的早期部分,例如使用与维度相同的零向量。

事实上,简单地堆叠观察就是原始 DQN 工作处理 Atari 环境中部分可观察性的方法。更详细地说,该预处理的步骤如下:

  1. 获取一个重新缩放的 RGB 屏幕帧。

  2. 提取 Y 通道(亮度)进一步将帧压缩成图像。这样就得到了一个单一的观察。

  3. 最新的帧被拼接成一个图像,形成一个具有内存的模型观察。

请注意,只有最后一步涉及内存,前面的步骤并不是严格必要的。

这种方法的明显优点是非常简单,生成的模型也很容易训练。然而,缺点是,这并不是处理序列数据的最佳方法,如果你曾经处理过时间序列问题或 NLP,应该不会对这一点感到惊讶。以下是一个原因的示例。

想象一下你对虚拟语音助手(如苹果的 Siri)说的以下句子:

“帮我买一张从旧金山到波士顿的机票。”

这与以下说法是相同的:

"给我买一张从旧金山到波士顿的机票"

假设每个单词都被传递到输入神经元,神经网络很难将它们轻松地理解为相同的句子,因为通常每个输入神经元期望特定的输入。在这种结构中,你需要使用该句子的所有不同组合来训练网络。更复杂的是,输入大小是固定的,但每个句子的长度可能不同。你也可以将这个思路扩展到强化学习问题中。

现在,在大多数问题中,堆叠观察值就足够了,比如 Atari 游戏。但是如果你试图教会你的模型如何玩 Dota 2 这种策略视频游戏,那么你就会遇到困难。

幸运的是,递归 神经 网络(RNN)来救场了。

使用 RNN

RNN 设计用于处理序列数据。一个著名的 RNN 变体,长短期记忆(LSTM)网络,能够有效地训练来处理长序列。当涉及到处理复杂环境中的部分可观察性时,LSTM 通常是首选:它被应用于 OpenAI 的 Dota 2 和 DeepMind 的 StarCraft II 模型中,当然还有许多其他模型。

信息

详细描述 RNN 和 LSTM 如何工作的内容超出了本章的范围。如果你想了解更多关于它们的知识,Christopher Olah 的博客是一个不错的资源:colah.github.io/posts/2015-08-Understanding-LSTMs/。

在使用 RLlib 时,可以按如下方式启用 LSTM 层,比如在使用 PPO 时,并在默认值的基础上进行一些可选的超参数调整:

import ray
from ray.tune.logger import pretty_print
from ray.rllib.agents.ppo.ppo import PPOTrainer
from ray.rllib.agents.ppo.ppo import DEFAULT_CONFIG
config = DEFAULT_CONFIG.copy()
config["model"]["use_lstm"] = True
# The length of the input sequence
config["model"]["max_seq_len"] = 8
# Size of the hidden state
config["model"]["lstm_cell_size"] = 64
# Whether to use
config["model"]["lstm_use_prev_action_reward"] = True

请注意,输入首先被传递到 RLlib 中的(预处理)"模型",该模型通常是由一系列全连接层组成。预处理的输出随后会传递给 LSTM 层。

全连接层的超参数也可以类似地被覆盖:

config["model"]["fcnet_hiddens"] = [32]
config["model"]["fcnet_activation"] = "linear"

在配置文件中指定环境为 Gym 环境名称或自定义环境类后,配置字典将传递给训练器类:

from ray.tune.registry import register_env
def env_creator(env_config):
    return MyEnv(env_config)    # return an env instance
register_env("my_env", env_creator)
config["env"] = "my_env"
ray.init()
trainer = PPOTrainer(config=config)
while True:
    results = trainer.train()
    print(pretty_print(results))
    if results["timesteps_total"] >= MAX_STEPS:
        break
print(trainer.save())

使用 LSTM 模型时,有几个需要注意的事项,与简单地堆叠观察值不同:

  • 由于需要对多步输入进行顺序处理,LSTM 的训练通常较慢。

  • 与前馈网络相比,训练 LSTM 可能需要更多的数据。

  • 你的 LSTM 模型可能对超参数更敏感,因此你可能需要进行一些超参数调优。

说到超参数,如果你的训练在像 PPO 这样的算法上进展不顺利,以下是一些可以尝试的值:

  • 学习率(config["lr"]):。

  • LSTM 单元大小(config["model"]["lstm_cell_size"]):64,128,256。

  • 值网络和策略网络之间的层共享(config["vf_share_layers"]):如果你的回报是几百或更多,请尝试将其设为假,以防值函数损失主导策略损失。或者,你也可以减少 config["vf_loss_coeff"]。

  • 熵系数(config["entropy_coeff"]):

  • 将奖励和之前的动作作为输入(config["model"]["lstm_use_prev_action_reward"]):尝试将其设为真,以便在观察之外为智能体提供更多信息。

  • 预处理模型架构(config["model"]["fcnet_hiddens"] 和 config["model"]["fcnet_activation"]):尝试使用单一线性层。

希望这些内容有助于为你的模型构建一个良好的架构。

最后,我们来讨论一下最流行的架构之一:Transformer。

Transformer 架构

在过去几年中,Transformer 架构基本上取代了 RNNs 在自然语言处理(NLP)应用中的地位。

Transformer 架构相较于最常用的 RNN 类型 LSTM,具有几个优势:

  • LSTM 编码器将从输入序列中获得的所有信息压缩到一个单一的嵌入层,然后传递给解码器。这在编码器和解码器之间创建了一个瓶颈。而 Transformer 模型允许解码器查看输入序列的每个元素(准确来说,是查看它们的嵌入)。

  • 由于 LSTM 依赖时间反向传播,梯度很可能在更新过程中爆炸或消失。而 Transformer 模型则同时查看每个输入步骤,并不会遇到类似的问题。

  • 结果是,Transformer 模型能够有效地处理更长的输入序列。

正因为如此,Transformer 也有可能成为强化学习应用中,RNNs 的竞争替代方案。

信息

如果你想了解该主题的更多内容,Jay Alammar 提供了一篇关于 Transformer 架构的优秀教程,网址为 jalammar.github.io/illustrated-transformer/。

尽管原始的 Transformer 模型有其优势,但已被证明在强化学习应用中不稳定。已有改进方案被提出(Parisotto et al., 2019),名为Gated Transformer-XL(GTrXL)。

RLlib 已将 GTrXL 实现为自定义模型。它可以如下使用:

...
from ray.rllib.models.tf.attention_net import GTrXLNet
...
config["model"] = {
    "custom_model": GTrXLNet,
    "max_seq_len": 50,
    "custom_model_config": {
        "num_transformer_units": 1,
        "attn_dim": 64,
        "num_heads": 2,
        "memory_tau": 50,
        "head_dim": 32,
        "ff_hidden_dim": 32,
    },
}

这为我们提供了另一个强大的架构,可以在 RLlib 中尝试。

恭喜你!我们已经到达本章的结尾!我们已经涵盖了几个重要主题,这些内容远比我们的篇幅所能表达的要更为深刻。请继续阅读参考文献部分的来源,并尝试我们介绍的仓库,以加深你对该主题的理解。

总结

在本章中,我们涵盖了强化学习中的一个重要主题:泛化和部分可观测性,这对于现实世界的应用至关重要。请注意,这是一个活跃的研究领域:保持我们在这里的讨论作为建议,并尝试为您的问题尝试的第一种方法。新方法定期推出,所以请留意。重要的是,您应始终关注泛化和部分可观测性,以便在视频游戏之外成功实施强化学习。在下一节中,我们将通过元学习将我们的探险提升到另一个高级水平。所以,请继续关注!

参考文献

第十二章:第十二章:元强化学习

与强化学习(RL)智能体相比,人类从较少的数据中学习新技能。这是因为首先,我们出生时大脑中就带有先验知识;其次,我们能够高效地将一种技能的知识迁移到另一种技能上。元强化学习(Meta-RL)旨在实现类似的能力。在本章中,我们将描述什么是元强化学习,我们使用了哪些方法,以及在以下主题下面临的挑战:

  • 元强化学习简介

  • 带有递归策略的元强化学习

  • 基于梯度的元强化学习

  • 元强化学习作为部分观测强化学习

  • 元强化学习中的挑战

元强化学习简介

在本章中,我们将介绍元强化学习,这实际上是一个非常直观的概念,但一开始可能比较难以理解。为了让你更加清楚,我们还将讨论元强化学习与前几章中涉及的其他概念之间的联系。

学会学习

假设你正在说服一个朋友一起去你非常想去的旅行。你脑海中浮现出了几个论点。你可以谈论以下几点:

  • 你目的地自然风光的美丽。

  • 你已经精疲力尽,真的很需要这段时间休息。

  • 这可能是你们一起旅行的最后机会,因为你将会很忙于工作。

好吧,你已经认识你的朋友很多年了,知道他们有多喜欢大自然,所以你意识到第一个论点将是最具吸引力的,因为他们喜欢大自然!如果是你的妈妈,也许你可以用第二个论点,因为她非常关心你,并且希望支持你。在这些情况下,你知道如何达成你想要的目标,因为你和这些人有共同的过去。

你曾经在一家商店离开时,比如去车行,买了比原计划更贵的东西吗?你是怎么被说服的?也许销售员猜出了你在乎以下几点:

  • 你的家人,并说服你购买一辆让他们更舒适的 SUV

  • 你的外表,并说服你购买一辆能吸引众人目光的跑车

  • 环境,并说服你购买一辆没有排放的电动汽车

销售员不了解你,但通过多年的经验和培训,他们知道如何迅速有效地了解你。他们问你问题,了解你的背景,发现你的兴趣,弄清楚你的预算。然后,他们会给你提供一些选项,根据你的喜好和不喜欢,最终会给你一个定制的报价套餐,包括品牌、型号、升级选项和支付计划。

在这里,前面的例子对应强化学习(RL),其中智能体为特定环境和任务学习一个好的策略,以最大化其奖励。后面的例子则对应元强化学习(Meta-RL),其中智能体学习一个好的过程,以快速适应新的环境和任务,从而最大化奖励。

在这个例子之后,让我们正式定义元强化学习(meta-RL)。

定义元强化学习(meta-RL)。

在元强化学习中,每一轮,智能体面临一个任务 ,该任务来自一个分布 , 。任务 是一个马尔可夫 决策 过程(MDP),可能具有不同的转移和奖励动态,描述为 ,其中包括以下内容:

  • 是状态空间。

  • 是动作空间。

  • 是任务 的转移分布。

  • 是任务 的奖励函数。

因此,在训练和测试期间,我们期望任务来自相同的分布,但我们并不期望它们完全相同,这正是典型机器学习问题中的设定。在元强化学习中,在测试时,我们期望智能体做以下事情:

  1. 有效地探索以理解任务。

  2. 适应任务。

元学习(meta-learning)是嵌入在动物学习中的。接下来让我们探索这种联系。

与动物学习及哈洛实验的关系

人工神经网络 notoriously 需要大量数据来训练。另一方面,我们的大脑能够从少量数据中更高效地学习。这主要有两个因素:

  • 与未经训练的人工神经网络不同,我们的大脑是预训练的,且内嵌了视觉、听觉和运动技能任务的先验。一些尤其令人印象深刻的预训练生物是初生性动物,例如鸭子,它们的小鸭子在孵化后的两小时内便能下水。

  • 当我们学习新任务时,我们在两个时间尺度上学习:在快速循环中,我们学习关于当前任务的具体内容;而如我们将看到的更多例子,在慢速循环中,我们学习抽象,这有助于我们将知识快速泛化到新的例子中。假设你学习某一特定的猫品种,例如美式卷耳猫,且你见到的所有例子都呈现白色和黄色。当你看到一只黑色的这种品种猫时,你不会觉得难以辨认。这是因为你已经发展出一种抽象的知识,帮助你通过这种猫独特的耳朵(向后卷曲)识别它,而非通过它的颜色。

机器学习中的一个大挑战是使得能够从类似先前情况的少量数据中学习。为了模仿步骤 1,我们会对训练好的模型进行微调,适应新任务。例如,一个在通用语料库(如维基百科页面、新闻文章等)上训练的语言模型,可以在一个专业语料库(如海事法)上进行微调,尽管可用的数据量有限。步骤 2 就是元学习的核心内容。

小贴士

经验上,对于新任务进行微调训练的模型,在强化学习(RL)中并不像在图像或语言任务中那样有效。事实证明,强化学习策略的神经网络表示不像图像识别中的那样层次化,例如,在图像识别中,第一层检测边缘,最后一层检测完整的物体。

为了更好地理解动物中的元学习能力,我们来看一个典型的例子:哈罗实验。

哈罗实验

哈罗实验探讨了动物中的元学习,涉及一只猴子,它一次被展示两个物体:

  • 这些物体中的一个与食物奖励相关联,猴子需要发现这一点。

  • 在每一步(共六步)中,物体被随机放置在左侧或右侧位置。

  • 猴子必须学会根据物体本身,而不是物体的位置,来判断哪个物体给它带来奖励。

  • 在六个步骤结束后,物体被替换为猴子不熟悉的新物体,并且这些物体与未知的奖励关联。

  • 猴子在第一步中学会了随机挑选一个物体,理解哪个物体给它带来奖励,并在剩下的步骤中根据物体是否带来奖励而选择这个物体,而不考虑物体的位置。

这个实验很好地表达了动物中的元学习能力,因为它涉及到以下内容:

  • 对于智能体来说,这是一个不熟悉的环境/任务

  • 智能体通过一种策略有效适应不熟悉的环境/任务,这种策略包括必要的探索,实时制定特定任务的策略(根据与奖励相关联的物体做出选择,而非其位置),然后是利用阶段。

元强化学习的目标与此相似,正如我们稍后会看到的那样。现在,让我们继续探索元强化学习与我们已经涵盖的其他概念的关系。

与部分可观测性和领域随机化的关系

元强化学习程序的主要目标之一是在测试时揭示潜在的环境/任务。根据定义,这意味着环境是部分可观测的,而元强化学习是一种专门应对这一问题的方法。

在上一章,第十一章,泛化与部分可观测性中,我们讨论了处理部分可观测性时需要使用记忆和领域随机化。那么,元强化学习(meta-RL)有什么不同呢?嗯,记忆依然是元强化学习中一个关键的工具。我们还在训练元强化学习智能体时对环境/任务进行随机化,这类似于领域随机化。此时,它们可能对你来说似乎没有区别。然而,存在一个关键的区别:

  • 在领域随机化中,训练智能体的目标是为环境的所有变化开发一个健壮的策略,涵盖一系列参数范围。例如,一个机器人可以在一系列摩擦力和扭矩值下进行训练。在测试时,基于一系列携带信息和扭矩的观察,智能体使用训练好的策略采取行动。

  • 在元强化学习中,训练智能体的目标是为新的环境/任务开发一个适应程序,这可能会导致在探索阶段后测试时使用不同的策略。

在基于记忆的元强化学习方法中,差异仍然可能很微妙,并且在某些情况下,训练过程可能是相同的。为了更好地理解这种差异,请记住哈洛实验:领域随机化的想法不适用于该实验,因为每一集展示给智能体的对象在每次都是全新的。因此,智能体在元强化学习中并不是学习如何在一系列对象上采取行动。而是它学习如何发现任务,并在展示完全新对象时相应地采取行动。

现在,终于是时候讨论几种元强化学习方法了。

信息

元学习的先驱之一是斯坦福大学的切尔西·芬教授,她在博士阶段曾与加州大学伯克利分校的谢尔盖·莱文教授合作。芬教授开设了一门关于元学习的课程,课程链接在cs330.stanford.edu/。在本章中,我们主要遵循芬教授和莱文教授使用的元强化学习方法的术语和分类。

接下来,让我们从使用递归策略的元强化学习开始。

使用递归策略的元强化学习

在本节中,我们将介绍元强化学习中一种更直观的方法,该方法使用递归 神经 网络(RNNs)来保持记忆,也被称为 RL-2 算法。让我们从一个示例开始,理解这种方法。

网格世界示例

假设有一个网格世界,智能体的任务是从起始状态 S 到达目标状态 G。这些状态在不同任务中是随机放置的,因此智能体必须学会探索世界,以发现目标状态在哪里,并在此后获得丰厚奖励。当同一任务被重复时,期望智能体能够快速到达目标状态,这也就是适应环境,因为每经过一步就会遭受惩罚。这个过程在图 12.1中有所示意:

图 12.1 – 元强化学习的网格世界示例。(a)任务,(b)智能体对任务的探索,(c)智能体利用其所学的知识

图 12.1 – 元强化学习的网格世界示例。(a)任务,(b)智能体对任务的探索,(c)智能体利用其所学的知识

为了在任务中表现出色,智能体必须执行以下操作:

  1. 探索环境(在测试时)。

  2. 记住并利用其先前学到的知识。

现在,由于我们希望智能体记住其先前的经验,我们需要引入一个记忆机制,意味着使用递归神经网络(RNN)来表示策略。有几个要点需要注意:

  1. 仅仅记住过去的观察值是不够的,因为目标状态在不同任务之间是变化的。智能体还需要记住过去的动作和奖励,以便能够关联在特定状态下采取的哪些动作导致了哪些奖励,从而揭示任务的规律。

  2. 仅仅记住当前回合中的历史是不够的。请注意,一旦智能体达到目标状态,回合就结束了。如果我们不将记忆传递到下一个回合,智能体将无法从前一个回合中获得的经验中受益。再者,请注意,在这里没有进行任何训练或更新策略网络的权重。这一切都发生在测试时,在一个未知的任务中。

处理前者很简单:我们只需要将动作和奖励与观察值一起喂给 RNN。为了处理后者,我们确保除非任务发生变化,否则在回合之间不会重置循环状态,以确保记忆不会中断。

现在,在训练过程中,为什么智能体会学习一个明确地以探索阶段开始新任务的策略呢?那是因为探索阶段帮助智能体发现任务,并在后续收获更高的奖励。如果我们在训练过程中仅基于单个回合奖励智能体,智能体将不会学到这种行为。因为探索是有即时成本的,这些成本只有在后续回合中才能回收,且当相同任务的记忆跨回合传递时才会得到补偿。为此,我们形成了元回合或试验,即!是将多个同一任务的回合串联起来的过程。再次强调,在每个回合内,循环状态不会被重置,奖励是根据元回合来计算的。如图 12.2所示:

图 12.2 – 智能体与环境交互的过程(来源:Duan et al., 2017)

接下来,让我们看看如何在 RLlib 中实现这一点。

RLlib 实现

关于我们之前提到的,元回合可以通过修改环境来形成,因此与 RLlib 并没有直接关系。至于其他部分,我们在智能体配置中的模型字典里进行修改:

  1. 首先,我们启用长短期记忆(LSTM)模型:

    "use_lstm": True
    
  2. 我们将动作和奖励与观察值一起传递给 LSTM:

    "lstm_use_prev_action_reward": True
    
  3. 我们确保 LSTM 输入序列足够长,能够覆盖一个元任务中的多个回合。默认的序列长度为 20:

    max_seq_len": 20
    

就是这些!你只需要通过几行代码更改,就可以训练你的元强化学习智能体!

信息

该过程可能并不总是收敛,或者即使收敛,也可能收敛到不好的策略。可以尝试多次训练(使用不同的种子)并调整超参数设置,这可能会帮助你获得一个好的策略,但这并不能保证成功。

这样,我们可以继续使用基于梯度的方法。

基于梯度的元强化学习

基于梯度的元强化学习方法提出,通过在测试时继续训练来改进策略,使策略能够适应所应用的环境。关键在于,策略参数在适应之前的状态,,应该设置成一种能够在少数几步内完成适应的方式。

提示

基于梯度的元强化学习(meta-RL)基于一个思想:某些策略参数的初始化可以使得在适应过程中,从非常少的数据中进行学习。元训练过程旨在找到这种初始化。

这个分支中的一个特定方法叫做无模型元学习(MAML),它是一种通用的元学习方法,也可以应用于强化学习。MAML 训练智能体完成各种任务,以找到一个有助于适应和从少量样本中学习的良好 值。

让我们来看一下如何使用 RLlib 来实现这一点。

RLlib 实现

MAML 是 RLlib 中实现的一个智能体,并且可以很容易地与 Ray 的 Tune 一起使用:

tune.run(
    "MAML",
    config=dict(
        DEFAULT_CONFIG,
        ...
    )
)

使用 MAML 需要在环境中实现一些额外的方法。这些方法分别是 sample_tasks、set_task 和 get_task,它们有助于在各种任务之间进行训练。一个示例实现可以是一个摆环境,RLlib 中的实现如下 (github.com/ray-project/ray/blob/releases/1.0.0/rllib/examples/env/pendulum_mass.py):

class PendulumMassEnv(PendulumEnv, gym.utils.EzPickle, MetaEnv):
    """PendulumMassEnv varies the weight of the pendulum
    Tasks are defined to be weight uniformly sampled between [0.5,2]
    """
    def sample_tasks(self, n_tasks):
        # Mass is a random float between 0.5 and 2
        return np.random.uniform(low=0.5, high=2.0, size=(n_tasks, ))
    def set_task(self, task):
        """
        Args:
            task: task of the meta-learning environment
        """
        self.m = task
    def get_task(self):
        """
        Returns:
            task: task of the meta-learning environment
        """
        return self.m

在训练 MAML 时,RLlib 会通过 episode_reward_mean 测量智能体在任何适应之前的环境表现。经过 N 次梯度适应步骤后的表现会显示在 episode_reward_mean_adapt_N 中。这些内部适应步骤的次数是智能体的一个配置项,可以修改:

"inner_adaptation_steps": 1

在训练过程中,您可以在 TensorBoard 上看到这些指标:

图 12.3 – TensorBoard 统计数据

图 12.3 – TensorBoard 统计数据

就这样!现在,让我们介绍本章的最后一种方法。

元强化学习作为部分观测强化学习

元强化学习中的另一种方法是专注于任务的部分可观测特性,并明确地从直到此时为止的观测中估计状态:

然后,基于任务在该回合中处于活动状态的可能性,或者更准确地说,基于包含任务信息的向量,形成一个可能任务的概率分布:

然后,从这个概率分布中迭代地抽取任务向量,并将其与状态一起传递给策略:

  1. 抽样 。

  2. 从接收状态和任务向量作为输入的策略中采取行动,。

有了这些内容,我们就结束了对三种主要元强化学习方法的讨论。在总结本章之前,让我们讨论一些元强化学习中的挑战。

元强化学习中的挑战

关于元强化学习的主要挑战,参见 Rakelly,2019,如下所示:

  • 元强化学习需要在多个任务上进行元训练,这些任务通常是手工设计的。一个挑战是创建一个自动化过程来生成这些任务。

  • 在元训练过程中,应该学习的探索阶段实际上并未有效学习。

  • 元训练涉及从独立同分布的任务中进行采样,而这一假设并不现实。因此,一个目标是通过让元强化学习从任务流中学习,使其变得更加“在线”。

恭喜你走到这一步!我们刚刚讨论了元强化学习,这是一个可能很难吸收的概念。希望这次介绍能给你勇气深入阅读相关文献,并进一步探索这个话题。

总结

在这一章中,我们讨论了元强化学习(meta-RL),它是强化学习领域最重要的研究方向之一,因为它的潜力是训练能够快速适应新环境的智能体。为此,我们介绍了三种方法:递归策略、基于梯度的方法和基于部分可观察性的策略。目前,元强化学习还处于起步阶段,其表现尚不如更成熟的方法,因此我们讨论了该领域面临的挑战。

在下一章,我们将涵盖多个高级主题,并将其集中在一章中。所以,请继续关注,以进一步加深你的强化学习(RL)专业知识。

参考文献

第十三章:第十三章:其他高级主题

本章将涵盖强化学习(RL)的几个高级主题。首先,我们将深入探讨分布式强化学习,除了之前章节中涉及的内容之外。这个领域对于处理训练智能体进行复杂任务所需的过多数据至关重要。好奇驱动的强化学习处理传统探索技术无法解决的困难探索问题。离线强化学习利用离线数据来获得良好的策略。所有这些都是热门的研究领域,您将在未来几年听到更多相关内容。

所以,在本章中,您将学习以下内容:

  • 分布式强化学习

  • 好奇驱动的强化学习

  • 离线强化学习

让我们开始吧!

分布式强化学习

正如我们在之前的章节中提到的,训练复杂的强化学习智能体需要大量的数据。虽然研究的一个关键领域是提高强化学习中的样本效率;另一个互补方向则是如何最好地利用计算能力和并行化,减少训练的实际时间和成本。我们已经在前面的章节中讨论、实现并使用了分布式强化学习算法和库。因此,本节将是对之前讨论的扩展,因为这一话题非常重要。在这里,我们将介绍更多关于最先进的分布式强化学习架构、算法和库的内容。那么,让我们从 SEED RL 开始,这是一种为大规模和高效并行化设计的架构。

可扩展、高效的深度强化学习 – SEED RL

我们从重新审视 Ape-X 架构开始讨论,它是可扩展强化学习的一个里程碑。Ape-X 的关键贡献是将学习和行动解耦:演员们按自己的节奏生成经验,学习器按自己的节奏从这些经验中学习,而演员们则定期更新他们本地的神经网络策略副本。Ape-X DQN 的流程示意图见图 13.1:

图 13.1 – Ape-X DQN 架构,重新审视

图 13.1 – Ape-X DQN 架构,重新审视

现在,让我们从计算和数据通信的角度来分析这个架构:

  1. 演员们,可能有数百个,会定期从中央学习器拉取 参数,即神经网络策略。根据策略网络的大小,成千上万的数字会从学习器推送到远程演员。这会在学习器和演员之间产生很大的通信负载,远远超过传输动作和观察所需的两倍数量级。

  2. 一旦一个演员接收到策略参数,它便使用这些参数推断每个环境步骤的动作。在大多数设置中,只有学习者使用 GPU,演员则在 CPU 节点上工作。因此,在这种架构中,大量的推断必须在 CPU 上进行,相较于 GPU 推断,这种方式效率要低得多。

  3. 演员在环境和推断步骤之间切换,这两者具有不同的计算需求。将这两个步骤执行在同一节点上,要么会导致计算瓶颈(当需要推断的节点是 CPU 节点时),要么会导致资源的低效利用(当节点是 GPU 节点时,GPU 的计算能力被浪费)。

为了克服这些低效问题,SEED RL 架构提出了以下关键方案:将动作推断移至学习者端。因此,演员将观察结果发送给中央学习者(那里存储着策略参数),并接收到回传的动作。通过这种方式,推断时间被缩短,因为推断在 GPU 上进行,而不是 CPU 上。

当然,故事并未结束。我们迄今所描述的情况带来了另一组挑战:

  • 由于演员需要在每个环境步骤中将观察结果发送到远程学习者以接收动作,因此出现了延迟问题,这是之前所没有的。

  • 当演员等待动作时,它保持空闲状态,导致演员节点计算资源的低效利用。

  • 将单个观察结果传递给学习者 GPU 会增加总的与 GPU 的通信开销。

  • GPU 资源需要调整,以同时处理推断和学习。

为了克服这些挑战,SEED RL 具有以下结构:

  • 一种非常快速的通信协议,称为gRPC,用于在演员和学习者之间传输观察结果和动作。

  • 多个环境被放置在同一个演员上,以最大化利用率。

  • 在将观察结果传递给 GPU 之前,会对其进行批处理以减少开销。

资源分配调整是第四个挑战,但这只是一个调优问题,而非根本性的架构问题。因此,SEED RL 提出了一种架构,可以做到以下几点:

  • 每秒处理数百万条观察数据。

  • 将实验成本降低高达 80%。

  • 通过将训练速度提高三倍,减少墙钟时间。

SEED RL 架构如图 13.2所示,取自 SEED RL 论文,并将其与 IMPALA 进行了比较,IMPALA 也面临与 Ape-X 类似的缺点:

图 13.2 – IMPALA 与 SEED 架构的比较(来源:Espeholt 等人,2020)

图 13.2 – IMPALA 与 SEED 架构的比较(来源:Espeholt 等人,2020)

到目前为止,一切顺利。有关实现细节,我们推荐参考Espeholt 等人,2020以及与论文相关的代码库。

信息

作者已将 SEED RL 开源,地址为 github.com/google-research/seed_rl。该仓库包含了 IMPALA、SAC 和 R2D2 代理的实现。

我们将很快介绍 R2D2 代理,并进行一些实验。但在结束本节之前,我们还会为你提供另一个资源。

信息

如果你有兴趣深入了解架构的工程方面,gRPC 是一个非常有用的工具。它是一个快速的通信协议,广泛应用于许多科技公司的微服务之间的连接。可以在 grpc.io 查看。

做得好!你现在已经掌握了分布式强化学习的最新技术。接下来,我们将介绍一种在分布式 RL 架构中使用的最先进的模型,R2D2。

分布式强化学习中的递归经验回放

最近强化学习文献中最具影响力的贡献之一,是 递归回放分布式 DQN(R2D2)代理,它在当时设定了经典基准的最新技术水平。R2D2 研究的主要贡献实际上是与 递归神经网络(RNNs)在强化学习代理中的有效应用有关,且这一方法也在分布式环境中实现。论文中使用了 长短期记忆(LSTM)作为 RNN 的选择,我们在接下来的讨论中也会采用这种方法。那么,首先让我们从训练 RNN 时初始化递归状态的挑战谈起,再讨论 R2D2 代理如何解决这个问题。

递归神经网络中的初始递归状态不匹配问题

在前几章中,我们讨论了携带观察记忆的重要性,以便揭示部分可观察的状态。例如,单独使用一帧 Atari 游戏画面将无法传达物体速度等信息,而基于一系列过去的画面,推算出物体速度等信息来做决策,会带来更高的奖励。如我们前面提到的,处理序列数据的有效方法是使用 RNNs。

RNN 的基本思想是将序列的输入逐一传递给同一个神经网络,同时将过去步骤的信息、记忆和摘要从一个步骤传递到下一个步骤,这一点在 图 13.3 中有所说明:

图 13.3 – RNN 的示意图,其中 a) 为紧凑表示,b) 为展开表示

图 13.3 – RNN 的示意图,其中 a) 为紧凑表示,b) 为展开表示

这里的一个关键问题是,如何为初始递归状态 进行初始化。最常见和便捷的方式是将递归状态初始化为全零。对于环境中每一步的演员来说,这并不是一个大问题,因为这个初始递归状态对应于一个回合的开始。然而,在从对应于较长轨迹小段的存储样本进行训练时,这种初始化就成了一个问题。我们来看看为什么。

请考虑图 13.4所示的场景。我们正在尝试训练一个 RNN 来处理一个存储的样本 ,因此观测值是四帧组成的序列,这些帧被传递到策略网络中。所以, 是第一帧, 是采样的 序列中的最后一帧,也是最新的帧(同样的情况适用于 )。当我们输入这些数据时, 将被获取并传递到后续步骤中,而我们对 使用零值:

图 13.4 – 使用一系列帧从 RNN 获取动作

图 13.4 – 使用一系列帧从 RNN 获取动作

现在,记住递归状态 的作用是总结直到第 步所发生的事情。当我们在训练期间使用零向量作为 时,例如在生成价值函数预测和 Q 函数的目标值时,会产生一些问题,这些问题虽然相关,但有所不同:

  • 它不会传达任何关于该时间步之前发生了什么的有意义信息。

  • 我们使用相同的向量(零向量),无论采样序列之前发生了什么,这会导致过载的表示。

  • 由于零向量不是 RNN 的输出,它本身并不是 RNN 的有意义表示。

结果是,RNN 对隐状态的处理变得“混乱”,并减少了对记忆的依赖,这违背了使用递归神经网络的初衷。

一种解决方案是记录整个轨迹,并在训练时处理/重放它,以计算每一步的递归状态。这也存在问题,因为在训练时重放所有任意长度的样本轨迹会带来大量开销。

接下来,我们来看一下 R2D2 智能体是如何解决这个问题的。

R2D2 对初始递归状态不匹配的解决方案

R2D2 智能体的解决方案是双管齐下:

  • 存储回合中的递归状态。

  • 使用烧入期。

接下来我们将更详细地探讨这些解决方案。

存储回合中的递归状态

当智能体在环境中执行时,在每一集的开始,它初始化递归状态。然后,它使用递归策略网络在每个步骤采取行动,并且每个观察对应的递归状态也会被生成。R2D2 智能体将这些递归状态与采样的经验一起发送到重放缓冲区,以便稍后在训练时用它们初始化网络,而不是用零向量。

总体而言,这显著弥补了使用零初始化的负面影响。然而,这仍然不是一个完美的解决方案:存储在重放缓冲区中的递归状态在训练时使用时会变得过时。因为网络是不断更新的,而这些状态会携带由旧版本网络生成的表示,例如在回放时使用的网络。这被称为表示漂移。

为了缓解表示漂移,R2D2 提出了一种额外的机制,即在序列开始时使用预热期。

使用预热期

使用预热期的工作方式如下:

  1. 存储一个比我们通常存储的序列更长的序列。

  2. 使用序列开头的额外部分,用当前参数展开 RNN。

  3. 这样,生成一个不会过时的初始状态,适用于预热部分之后。

  4. 在反向传播时不要使用预热部分。

这在图 13.5中有所描述:

图 13.5 – 表示 R2D2 使用存储的递归状态并进行两步预热的示意图

图 13.5 – 表示 R2D2 使用存储的递归状态并进行两步预热的示意图

所以,图中的例子是说,与其使用 ,它是由某些旧策略 生成的,不如做如下操作:

  1. 使用 来在训练时初始化递归状态。

  2. 使用当前参数 展开 RNN 的递归状态,通过预热部分生成 。

  3. 这有望从过时的表示 中恢复,并且比 更准确地初始化。

  4. 这在精确度上更好,因为它更接近我们如果从一开始存储并展开整个轨迹,直到 ,使用 所得到的结果。

所以,这就是 R2D2 智能体。在我们结束这一部分之前,先来讨论一下 R2D2 智能体的成就。

R2D2 论文的关键结果

R2D2 的工作提供了非常有趣的见解,我强烈推荐你阅读完整的论文。然而,为了我们讨论的完整性,以下是一个总结:

  • R2D2 在 Atari 基准测试中将 Ape-X DQN 创下的先前最高纪录提高了四倍,是第一个在 57 款游戏中有 52 款取得超人类水平表现的智能体,并且具有更高的样本效率。

  • 它通过在所有环境中使用一组超参数来实现这一点,显示出智能体的强大鲁棒性。

  • 有趣的是,即便在被认为是完全可观察的环境中,R2D2 也能提高性能,而在这些环境中,通常不指望使用记忆来帮助。作者通过 LSTM 的高表示能力来解释这一点。

  • 存储递归状态并使用预热期都非常有益,其中前者的影响更大。可以将这两种方法结合使用,这是最有效的,或者单独使用。

  • 使用零初始状态会降低智能体对记忆的依赖能力。

供您参考,在五个环境中,R2D2 智能体未能超过人类级别的表现,但通过修改参数,它实际上可以实现超越。剩下的两个环境,Montezuma's Revenge 和 Pitfall,是著名的难探索问题,后续章节将进一步讨论这两个环境。

这样一来,让我们在这里总结讨论,并进入一些实践工作。下一节中,我们将使用 SEED RL 架构与 R2D2 智能体。

实验 SEED RL 和 R2D2

在本节中,我们将简要演示 SEED RL 仓库及其如何用于训练智能体。让我们从设置环境开始。

设置环境

SEED RL 架构使用多个库,如 TensorFlow 和 gRPC,它们之间以相当复杂的方式进行交互。为了简化大部分配置工作,SEED RL 的维护者使用 Docker 容器来训练 RL 智能体。

信息

Docker 和容器技术是当今互联网服务背后的基础工具。如果你从事机器学习工程,或有兴趣在生产环境中提供你的模型,了解 Docker 是必不可少的。Mumshad Mannambeth 的快速 Docker 启蒙课程可在 youtu.be/fqMOX6JJhGo 上找到。

设置说明可以在 SEED RL GitHub 页面找到。简而言之,设置步骤如下:

  1. 在你的机器上安装 Docker。

  2. 启用以非 root 用户身份运行 Docker。

  3. 安装 git。

  4. 克隆 SEED 仓库。

  5. 使用 run_local.sh 脚本启动仓库中定义的环境训练,如下所示:

    ./run_local.sh [Game] [Agent] [Num. actors] 
    ./run_local.sh atari r2d2 4
    

如果你的 NVIDIA GPU 没有被 SEED 容器识别,可能需要对该设置进行一些附加配置:

一旦你的设置成功,你应该看到代理开始在 tmux 终端上训练,如图 13.6所示:

图 13.6 – SEED RL 在 tmux 终端上的训练

图 13.6 – SEED RL 在 tmux 终端上的训练

信息

Tmux 是一个终端复用器,基本上是终端内的窗口管理器。要快速了解如何使用 tmux,请查看www.hamvocke.com/blog/a-quick-and-easy-guide-to-tmux/。

现在,你的机器上已经运行了 SEED,这是一款最先进的强化学习框架!你可以通过按照 Atari、Football 或 DMLab 示例文件夹中的说明,插入自定义环境进行训练。

信息

R2D2 代理也可以在 DeepMind 的 ACME 库中找到,里面还有许多其他代理:github.com/deepmind/acme。

接下来,我们将讨论好奇心驱动的强化学习。

好奇心驱动的强化学习

当我们讨论 R2D2 代理时,我们提到在基准测试集中只剩下少数几个 Atari 游戏,代理在这些游戏中无法超过人类表现。代理面临的剩余挑战是解决困难探索问题,这些问题有非常稀疏和/或误导性的奖励。后续的工作来自 Google DeepMind,也解决了这些挑战,使用了名为Never Give Up(NGU)和Agent57的代理,在基准测试中使用的 57 个游戏中都达到了超人类水平的表现。在本节中,我们将讨论这些代理以及它们用于有效探索的方法。

让我们从描述困难探索和好奇心驱动学习的概念开始。

针对困难探索问题的好奇心驱动学习

让我们来看看图 13.7所示的简单网格世界:

图 13.7 – 一个困难探索的网格世界问题

图 13.7 – 一个困难探索的网格世界问题

假设在这个网格世界中有以下设置:

  • 总共有 102 个状态,101 个用于网格世界,1 个用于环绕它的悬崖。

  • 代理从世界的最左端开始,目标是到达最右端的奖杯。

  • 到达奖杯的奖励为 1,000,掉入悬崖的奖励为-100,每个时间步的奖励为-1,以鼓励快速探索。

  • 一个回合结束的条件是:代理到达奖杯,掉进悬崖,或经过 1,000 个时间步。

  • 在每个时间步,代理有五种可用的动作:保持静止,或向上、向下、向左或向右移动。

如果你在当前设置下训练一个代理,即使使用我们已覆盖的最强算法,如 PPO、R2D2 等,最终得到的策略可能也是自杀式的:

  • 通过随机动作很难偶然找到奖杯,因此代理可能永远不会发现网格世界中有一个高奖励的奖杯。

  • 等到回合结束会导致总奖励为-1000。

  • 在这个黑暗的世界里,智能体可能决定尽早自杀,以避免长时间的痛苦。

即使是最强大的算法,这种方法中的薄弱环节也在于通过随机动作进行探索的策略。偶然碰到最优动作集的概率是!。

提示

为了计算智能体通过随机动作到达奖杯所需的预期步数,我们可以使用以下方程:

其中, 是智能体在状态 时到达奖杯所需的预期步数。我们需要为所有状态生成这些方程(对于 会有所不同),并求解结果的方程组。

在我们之前讨论机器教学方法时,我们提到过,教师可以设计奖励函数,鼓励智能体在世界中走对路。这种方法的缺点是,在更复杂的环境中,手动设计奖励函数可能不可行。事实上,教师可能连最优策略都不知道,无法引导智能体。

那么问题就变成了,如何鼓励智能体高效地探索环境呢?一个好的答案是对智能体首次访问的状态给予奖励,例如,在我们的网格世界中,给它+1 的奖励。享受发现世界的乐趣可能会成为智能体避免自杀的动力,这最终也会导致赢得奖杯。

这种方法被称为好奇心驱动学习,它通过基于观察的新奇性给智能体提供内在奖励。奖励的形式如下:

其中, 是环境在时间 给予的外在奖励, 是时间 对观察的新奇性给予的内在奖励,而 是调节探索相对重要性的超参数。

在我们讨论 NGU 和 Agent57 智能体之前,让我们深入探讨一下好奇心驱动的强化学习中的一些实际挑战。

好奇心驱动的强化学习中的挑战

上面我们提供的网格世界示例是最简单的设置之一。另一方面,我们对强化学习智能体的期望是它们能够解决许多复杂的探索问题。当然,这也带来了挑战。我们来讨论其中的几个挑战。

在观察处于连续空间和/或高维空间时评估新奇性

当我们有离散的观察时,评估一个观察是否新颖很简单:我们只需要计算智能体已经看到每个观察的次数。然而,当观察处于连续空间时,例如图像,就变得复杂,因为无法简单地进行计数。类似的挑战是,当观察空间的维度过大时,就像在图像中一样。

噪声电视问题

对于好奇心驱动的探索,一个有趣的失败状态是环境中有噪声源,比如在迷宫中播放随机画面的嘈杂电视。

图 13.8 – OpenAI 实验中展示的噪声电视问题(来源:OpenAI 等,2018)

图 13.8 – OpenAI 实验中展示的噪声电视问题(来源:OpenAI 等,2018)

然后,智能体会像很多人一样,困在嘈杂的电视前,进行无意义的探索,而不是实际地发现迷宫。

终身新颖性

如前所述,内在奖励是基于一个回合内观察到的新颖性给予的。然而,我们希望智能体能够避免在不同回合中一再做出相同的发现。换句话说,我们需要一个机制来评估终身新颖性,以实现有效的探索。

解决这些挑战有多种方法。接下来,我们将回顾 NGU 和 Agent57 智能体是如何应对这些挑战的,并探讨它们如何在经典强化学习基准测试中实现最先进的性能。

永不放弃

NGU 智能体有效地将一些关键的探索策略结合在一起。接下来我们将在以下章节中详细探讨这一点。

获取观察的嵌入

NGU 智能体通过从观察中获得嵌入方式,处理了关于 a) 高维观察空间和 b) 观察中的噪声这两个挑战。具体来说:给定从环境中采样的一个三元组 ,其中 是观察, 是时间 时的动作,它通过训练神经网络从两个连续观察中预测动作。这个过程如图 13.9所示:

图 13.9 – NGU 智能体嵌入网络

图 13.9 – NGU 智能体嵌入网络

这些嵌入,即来自 嵌入网络的图像的 维度表示,记作 ,是智能体稍后用于评估观察新颖性的依据。

如果你想知道为什么有这样一个复杂的设置来获取图像观察的低维表示,这是为了应对噪声电视问题。观察中的噪声在预测导致环境从发出观察到的动作时并没有提供有用的信息。换句话说,代理执行的动作不会解释观察中的噪声。因此,我们不希望一个从观察中预测动作的网络学习到包含噪声的表示,至少不会是主导的。所以,这是一个巧妙的去噪方式,处理观察表示。

接下来,让我们看看这些表示是如何使用的。

回合新颖性模块

为了评估观察结果与回合中先前观察结果的相对新颖性,并计算一个回合内的内在奖励,NGU 代理执行以下操作:

  1. 将在一个回合中遇到的观察结果的嵌入存储在一个记忆中!

  2. 将与-最近的嵌入在中进行比较

  3. 计算一个内在奖励,该奖励与和其邻居之间相似度之和成反比

这个想法在图 13.10中得到了说明:

图 13.10 – NGU 回合新颖性模块

图 13.10 – NGU 回合新颖性模块

为了避免有些拥挤的符号表示,我们将计算的细节留给论文,但这应该能让你大致了解。

最后,让我们讨论 NGU 代理如何评估终身新颖性。

终身新颖性模块,带有随机蒸馏网络

在训练过程中,强化学习代理会在许多并行进程和回合中收集经验,这在某些应用中会导致数十亿次观察。因此,判断一个观察是否在所有观察中是新颖的并不完全直接。

解决这一问题的巧妙方法是使用随机网络蒸馏(RND),这正是 NGU 代理所做的。RND 涉及两个网络:一个随机网络和一个预测器网络。它们的工作方式如下:

  1. 随机网络在训练开始时是随机初始化的。自然,它导致了从观察到输出的任意映射。

  2. 预测器网络试图学习这个映射,这是随机网络在整个训练过程中所做的。

  3. 预测器网络的误差会在先前遇到的观察上较低,而在新颖的观察上较高。

  4. 预测误差越大,内在奖励就会越大。

RND 架构在图 13.11中得到了说明:

图 13.11 – NGU 代理中的 RND 架构

图 13.11 – NGU 代理中的 RND 架构

NGU 智能体利用此误差来获得乘数,,以调整 。更具体地说,

其中, 和 是预测网络误差的均值和标准差。因此,要获得大于 1 的乘数,预测网络的误差,即“惊讶”,应该大于它所做的平均误差。

现在,让我们将一切整合起来。

结合内在奖励和外在奖励

在获得基于长期新奇性的观察的季节性内在奖励和乘数后,结合的内在奖励在时间 时的计算方式如下:

其中, 是一个超参数,用于限制乘数的上限。然后,回合奖励是内在奖励和外在奖励的加权和:

就是这样!我们已经涵盖了 NGU 智能体的一些关键思想。它还有更多的细节,例如如何在并行化的行为者中设置 值,然后利用它来参数化价值函数网络。

在我们结束关于好奇心驱动学习的讨论之前,让我们简要谈谈 NGU 智能体的扩展,Agent57。

Agent57 改进

Agent57 扩展了 NGU 智能体,设定了新的技术前沿。主要改进如下:

  • 它为内在奖励和外在奖励分别训练价值函数网络,然后将它们结合起来。

  • 它训练一组策略,并使用滑动窗口上置信界限 (UCB) 方法来选择 和折扣因子 ,同时优先考虑一个策略而非另一个。

有了这些,我们就结束了关于好奇心驱动的强化学习的讨论,这是解决强化学习中难以探索问题的关键。话虽如此,强化学习中的探索策略是一个广泛的话题。为了更全面地了解这一主题,我建议你阅读 Lilian Weng 关于此话题的博客文章(Weng,2020),然后深入研究博客中提到的论文。

接下来,我们将讨论另一个重要领域:离线强化学习。

离线强化学习

离线强化学习 是通过使用智能体与环境进行的一些先前交互(可能是非强化学习的,如人类智能体)录制的数据来训练智能体,而不是直接与环境互动。这也被称为批量强化学习。在这一部分,我们将研究离线强化学习的一些关键组成部分。让我们从一个概述开始,了解它是如何工作的。

离线强化学习工作原理概述

在离线强化学习中,智能体不会直接与环境互动来探索和学习策略。图 13.12 将其与在线策略和离策略设置进行了对比:

图 13.12 – 在线策略、离策略与离线深度强化学习的对比(改编自 Levine, 2020)

图 13.12 – 在线策略、离策略和离线深度 RL 的比较(改编自Levine, 2020)

让我们解读一下这张图所展示的内容:

  • 在在线策略 RL 中,代理会使用每个策略收集一批经验。然后,使用这批经验来更新策略。这个循环会一直重复,直到获得令人满意的策略。

  • 在离策略 RL 中,代理从重放缓冲区采样经验,以周期性地改进策略。更新后的策略会在回合中使用,生成新的经验,并逐渐替换重放缓冲区中的旧经验。这个循环会一直重复,直到获得令人满意的策略。

  • 在离线 RL 中,存在一些行为策略 与环境交互并收集经验。这个行为策略不一定属于 RL 代理。事实上,在大多数情况下,它可能是人类行为、基于规则的决策机制、经典控制器等。从这些交互中记录的经验将是 RL 代理用来学习策略的依据,希望能够改进行为策略。因此,在离线 RL 中,RL 代理并不与环境进行交互。

你可能会问的一个显而易见的问题是,为什么我们不能将离线数据放入类似重放缓冲区的东西,并使用 DQN 代理或类似的方法呢?这是一个重要的问题,我们来讨论一下。

为什么我们需要为离线学习设计特殊的算法

对于强化学习(RL)代理来说,必须与环境进行交互,以便观察其在不同状态下行为的后果。另一方面,离线 RL 不允许代理进行交互和探索,这是一个严重的限制。以下是一些示例来说明这一点:

  • 假设我们有来自一个人类在城市中开车的数据。根据日志,驾驶员达到的最大速度是 50 英里每小时。RL 代理可能会从日志中推断出,增加速度会减少旅行时间,并可能提出一个策略,建议在城市中以 150 英里每小时的速度行驶。由于代理从未观察到这种做法可能带来的后果,它没有太多机会纠正这种做法。

  • 当使用基于值的方法,如 DQN 时,Q 网络是随机初始化的。因此,某些 值可能仅凭运气就非常高,从而暗示一个策略,推动代理执行 并采取行动 。当代理能够进行探索时,它可以评估该策略并纠正这些不好的估计。但在离线 RL 中,它无法做到这一点。

所以,这里问题的核心是分布变化,即行为策略与 RL 策略之间的差异。

所以,希望你已经信服了离线 RL 需要一些特殊的算法。那么,下一个问题是,这样做值得吗?当我们可以使用我们迄今为止讨论的所有聪明方法和模型获得超人类级别的表现时,为什么我们还要为此费心呢?让我们看看原因。

为什么离线强化学习至关重要

视频游戏之所以是强化学习(RL)最常见的测试平台,是因为我们可以收集到用于训练所需的大量数据。当涉及到为实际应用(如机器人技术、自动驾驶、供应链、金融等)训练 RL 策略时,我们需要这些过程的模拟,以便能够收集必要的数据量并广泛探索各种策略。这无疑是现实世界 RL 中最重要的挑战之一。

以下是一些原因:

  • 构建一个高保真度的现实世界过程模拟通常非常昂贵,可能需要数年时间。

  • 高保真度的模拟可能需要大量的计算资源来运行,这使得它们很难在 RL 训练中进行规模化。

  • 如果环境动态发生变化且未在模拟中进行参数化,模拟可能会很快变得过时。

  • 即使保真度非常高,也可能不足以满足 RL 的要求。RL 容易对它所交互的(模拟)环境中的错误、怪癖和假设过拟合。因此,这就产生了模拟到现实的差距。

  • 部署可能已对模拟过拟合的 RL 智能体可能会很昂贵或不安全。

因此,模拟在企业和组织中是一种稀有的存在。你知道我们拥有的是什么吗?数据。我们有许多生成大量数据的过程:

  • 制造环境有机器日志。

  • 零售商有关于他们过去定价策略及其结果的数据。

  • 交易公司有他们的买卖决策日志。

  • 我们有很多汽车驾驶视频,并且能够获得它们。

离线 RL 有潜力为所有这些过程推动自动化,并创造巨大的现实世界价值。

经过这段冗长但必要的动机说明后,终于到了介绍具体的离线 RL 算法的时刻。

优势加权演员评论家

离线 RL 是一个热门的研究领域,已经提出了许多算法。一个共同的主题是确保学习到的策略保持接近行为策略。评估差异的常用衡量标准是 KL 散度:

另一方面,与其他方法不同,优势加权演员评论家(AWAC)表现出以下特征:

  • 它并不试图拟合一个模型来显式地学习 。

  • 它通过惩罚分布的偏移来隐式地进行调整。

  • 它使用动态规划来训练数据效率高的 函数。

为此,AWAC 优化以下目标函数:

这导致了以下的策略更新步骤:

其中 是超参数, 是归一化量。这里的关键思想是鼓励具有较高优势的动作。

信息

AWAC 的一个关键贡献是,基于离线数据训练的策略在有机会的情况下,可以通过与环境互动进行有效的微调。

我们将算法的详细信息推迟到论文中(由Nair 等人,2020),实现则可以在 RLkit 代码库中找到,地址为github.com/vitchyr/rlkit。

让我们总结一下关于离线强化学习的讨论,包含基准数据集及相应的代码库。

离线强化学习基准

随着离线强化学习(Offline RL)逐渐兴起,来自 DeepMind 和加利福尼亚大学伯克利分校的研究人员创建了基准数据集和代码库,以便离线强化学习算法可以通过标准化的方式进行相互比较。这些将成为离线强化学习的“Gym”,可以这么理解:

  • RL Unplugged 由 DeepMind 推出,包含来自 Atari、Locomotion、DeepMind Control Suite 环境的数据集,以及真实世界的数据集。它可以在github.com/deepmind/deepmind-research/tree/master/rl_unplugged上获得。

  • D4RL 由加利福尼亚大学伯克利分校的机器人与人工智能实验室(RAIL)推出,包含来自不同环境的数据集,如 Maze2D、Adroit、Flow 和 CARLA。它可以在github.com/rail-berkeley/d4rl上获得。

干得好!你现在已经跟上了这一新兴领域的步伐——离线强化学习。

总结

本章涵盖了几个非常热门的研究领域的高级主题。分布式强化学习是高效扩展强化学习实验的关键。基于好奇心驱动的强化学习通过有效的探索策略使解决困难的探索问题成为可能。最后,离线强化学习有潜力通过利用已经可用的许多过程的数据日志,彻底改变强化学习在现实世界问题中的应用。

在本章中,我们结束了关于算法和理论讨论的部分。接下来的章节将更侧重于应用,从下一章的机器人学应用开始。

参考文献

第四部分:强化学习的应用

在本节中,您将了解强化学习(RL)的各种应用,如自主系统、供应链管理、网络安全等。我们将学习如何利用这些技术,使用强化学习解决各行业中的问题。最后,我们将探讨强化学习的一些挑战及其未来发展。

本节包含以下章节:

  • 第十四章,自主系统

  • 第十五章,供应链管理

  • 第十六章,营销、个性化与金融

  • 第十七章,智能城市与网络安全

  • 第十八章,强化学习的挑战与未来方向

第十四章:第十四章:自主系统

到目前为止,本书已经涵盖了许多强化学习中的最前沿算法和方法。从本章开始,我们将看到它们如何在实际应用中应对现实问题!我们将从机器人学习开始,这是强化学习的重要应用领域。为此,我们将使用 PyBullet 物理仿真训练 KUKA 机器人抓取托盘上的物体。我们将讨论几种解决这个困难探索问题的方法,并且会通过手动构建课程学习法和 ALP-GMM 算法来解决它。在本章结束时,我们还将介绍其他用于机器人学和自动驾驶的仿真库,这些库通常用于训练强化学习代理。

所以,本章内容包括以下几点:

  • 介绍 PyBullet

  • 熟悉 KUKA 环境

  • 开发解决 KUKA 环境的策略

  • 使用课程学习法训练 KUKA 机器人

  • 超越 PyBullet,进入自动驾驶领域

这是强化学习中最具挑战性且有趣的领域之一。让我们深入了解吧!

介绍 PyBullet

PyBullet 是一个流行的高保真物理仿真模块,广泛应用于机器人学、机器学习、游戏等领域。它是使用强化学习进行机器人学习时最常用的库之一,特别是在从仿真到现实的迁移研究和应用中:

图 14.1 – PyBullet 环境与可视化(来源:PyBullet GitHub 仓库)

图 14.1 – PyBullet 环境与可视化(来源:PyBullet GitHub 仓库)

PyBullet 允许开发者创建自己的物理仿真。此外,它还提供了使用 OpenAI Gym 接口的预构建环境。部分这些环境如图 14.1所示。

在接下来的部分,我们将为 PyBullet 设置一个虚拟环境。

设置 PyBullet

在 Python 项目中使用虚拟环境几乎总是一个好主意,这也是我们将在本章中的机器人学习实验中所做的。所以,让我们继续执行以下命令来安装我们将使用的库:

$ virtualenv pybenv
$ source pybenv/bin/activate
$ pip install pybullet --upgrade
$ pip install gym
$ pip install tensorflow==2.3.1
$ pip install ray[rllib]==1.0.0
$ pip install scikit-learn==0.23.2

你可以通过运行以下命令来测试你的安装是否正常:

$ python -m pybullet_envs.examples.enjoy_TF_AntBulletEnv_v0_2017may

如果一切正常,你会看到一个很酷的蚂蚁机器人四处游走,正如在图 14.2中所示:

图 14.2 – 蚂蚁机器人在 PyBullet 中行走

图 14.2 – 蚂蚁机器人在 PyBullet 中行走

太棒了!我们现在可以继续进行我们将要使用的 KUKA 环境了。

熟悉 KUKA 环境

KUKA 是一家提供工业机器人解决方案的公司,这些解决方案广泛应用于制造和组装环境。PyBullet 包含了 KUKA 机器人的仿真,用于物体抓取仿真(图 14.3):

图 14.3 – KUKA 机器人在工业中被广泛使用。(a)一台真实的 KUKA 机器人(图片来源 CNC Robotics 网站),(b)一个 PyBullet 仿真

图 14.3 – KUKA 机器人广泛应用于工业中。(a) 一台真实的 KUKA 机器人(图片来源:CNC Robotics 网站),(b) 一种 PyBullet 仿真

PyBullet 中有多个 KUKA 环境,具体如下:

  • 使用机器人和物体的位置及角度抓取矩形块

  • 使用摄像头输入抓取矩形块

  • 使用摄像头/位置输入抓取随机物体

在本章中,我们将重点关注第一个动作,接下来会更详细地介绍它。

使用 KUKA 机器人抓取矩形块

在这个环境中,机器人的目标是到达一个矩形物体,抓取它并将其抬升到一定高度。环境中的一个示例场景以及机器人坐标系如图 14.4所示:

图 14.4 – 物体抓取场景和机器人坐标系

图 14.4 – 物体抓取场景和机器人坐标系

机器人关节的动力学和初始位置在 pybullet_envs 包中的 Kuka 类中定义。我们将根据需要讨论这些细节,但你可以自由深入了解类定义,以更好地理解动力学。

信息

为了更好地理解 PyBullet 环境以及 Kuka 类的构建,你可以查看 PyBullet 快速入门指南,链接:bit.ly/323PjmO。

现在让我们深入了解为控制此机器人在 PyBullet 内部创建的 Gym 环境。

KUKA Gym 环境

KukaGymEnv 封装了 Kuka 机器人类,并将其转化为一个 Gym 环境。动作、观察、奖励和终止条件定义如下。

动作

在这个环境中,代理执行的三种动作都与移动夹持器有关。这些动作如下:

  • 沿轴的速度

  • 沿轴的速度

  • 用于旋转夹持器的角速度(偏航)

环境本身将夹持器沿轴移动到托盘上,物体位于托盘中。当夹持器足够接近托盘时,它会闭合夹持器的手指以尝试抓取物体。

环境可以配置为接受离散或连续的动作。我们将在本案例中使用后者。

观察值

代理从环境中接收九个观察值:

  • 三个观察值用于夹持器的、和位置

  • 三个观察值用于夹持器相对于、和轴的欧拉角

  • 两个观察值用于物体相对于夹持器的和位置,相对于夹持器。

  • 一个观察值用于物体相对于夹持器的欧拉角,沿轴

奖励

成功抓取对象并将其提升到一定高度的奖励是 10,000 分。 此外,还有一个轻微的成本,用于惩罚夹爪与对象之间的距离。 另外,旋转夹爪也会消耗一些能量。

终止条件

一个 episode 在 1,000 步之后或夹爪关闭之后结束,以先发生者为准。

要理解环境如何运作的最佳方式是实际进行实验,这也是接下来要做的事情。

这可以通过以下代码文件完成:Chapter14/manual_control_kuka.py.

该脚本允许您手动控制机器人。 您可以使用 "类似 gym 的" 控制模式,在此模式下,环境控制垂直速度和夹爪指角度。 或者,您可以选择非类似 gym 的模式来更多地控制。

您会注意到的一件事是,即使在类似 gym 的控制模式下将速度沿着 和 轴保持为零,机器人在下降时会改变其 和 位置。 这是因为夹爪沿 轴的默认速度太高。 您可以验证,在非类似 gym 的模式下,为 以下的值对 会使其他轴的位置发生较大改变。 在我们定制环境时,我们将减少速度以减轻这种情况。

现在您已经熟悉 KUKA 环境,让我们讨论一些解决它的替代策略。

开发解决 KUKA 环境的策略

环境中的抓取物体问题是一个 难探索 问题,这意味着在抓取对象后代理程序接收的稀疏奖励不太可能被发现。 像我们即将做的减少垂直速度将使它稍微容易一些。 不过,让我们回顾一下我们已经涵盖的解决这类问题的策略:

  • 奖励塑形 是我们之前讨论过的最常见的 机器教学 策略之一。 在某些问题中,激励代理朝向目标非常直接。 虽然在许多问题中,这样做可能会很痛苦。 因此,除非有明显的方法,否则制定奖励函数可能需要太多时间(以及对问题的专业知识)。 还请注意,原始奖励函数有一个成分来惩罚夹爪与对象之间的距离,因此奖励已经在某种程度上被塑造。 在我们的解决方案中,我们将不会超越此范围。

  • 以好奇心驱动的学习 激励代理程序发现状态空间的新部分。 对于这个问题,我们不需要代理程序过多地随机探索状态空间,因为我们已经对它应该做什么有一些想法。 因此,我们也将跳过这个技术。

  • "entropy_coeff" 配置位于 RLlib 的 PPO 训练器中,这是我们将使用的配置。然而,我们的超参数搜索(稍后会详细介绍)最终将这个值选为零。

  • 课程学习 可能是这里最合适的方法。我们可以识别出使问题变得具有挑战性的因素,从简单的水平开始训练智能体,并逐渐增加难度。

因此,课程学习是我们将用来解决这个问题的方法。但首先,让我们识别出参数化环境的维度,以便创建课程。

参数化问题的难度

当你实验环境时,你可能已经注意到一些让问题变得困难的因素:

  • 夹爪的起始位置过高,无法发现正确的抓取顺序。因此,调整高度的机器人关节将是我们需要参数化的一个维度。事实证明,它是在 Kuka 类的 jointPositions 数组的第二个元素中设置的。

  • 当夹爪不在原始高度时,它可能会与物体沿 轴的位置发生错位。我们还将对控制此位置的关节进行参数化,该关节是 Kuka 类的 jointPositions 数组的第四个元素。

  • 随机化物体位置是另一个给智能体带来困难的因素,它影响到 和 的位置以及物体的角度。我们将对这些组件的随机化程度进行参数化,范围从 0% 到 100%。

  • 即使物体没有被随机放置,它的中心也未必与机器人在 轴上的默认位置对齐。我们将对物体在 位置添加一些偏差,同样进行参数化。

这太棒了!我们知道该怎么做,这是一个重要的第一步。现在,我们可以开始课程学习!

使用课程学习训练 KUKA 机器人

在实际启动训练之前,第一步是定制 Kuka 类和 KukaGymEnv,使其能够与我们上面描述的课程学习参数一起工作。接下来我们就来做这个。

为课程学习定制环境

首先,我们通过创建一个继承自 PyBullet 原始 Kuka 类的 CustomKuka 类来开始。以下是我们的实现方式:

Chapter14/custom_kuka.py

  1. 我们首先需要创建一个新的类,并接受一个额外的参数,jp_override 字典,它代表 关节位置覆盖:

    class CustomKuka(Kuka):
        def __init__(self, *args, jp_override=None, **kwargs):
            self.jp_override = jp_override
            super(CustomKuka, self).__init__(*args, **kwargs)
    
  2. 我们需要这个来改变在我们重写的 reset 方法中设置的 jointPositions 数组:

        def reset(self):
        ...
            if self.jp_override:
                for j, v in self.jp_override.items():
                    j_ix = int(j) - 1
                    if j_ix >= 0 and j_ix <= 13:
                        self.jointPositions[j_ix] = v
    

    现在,是时候创建 CustomKukaEnv 了。

  3. 创建一个自定义环境,接受所有这些课程学习的参数化输入:

    class CustomKukaEnv(KukaGymEnv):
        def __init__(self, env_config={}):
            renders = env_config.get("renders", False)
            isDiscrete = env_config.get("isDiscrete", False)
            maxSteps = env_config.get("maxSteps", 2000)
            self.rnd_obj_x = env_config.get("rnd_obj_x", 1)
            self.rnd_obj_y = env_config.get("rnd_obj_y", 1)
            self.rnd_obj_ang = env_config.get("rnd_obj_ang", 1)
            self.bias_obj_x = env_config.get("bias_obj_x", 0)
            self.bias_obj_y = env_config.get("bias_obj_y", 0)
            self.bias_obj_ang = env_config.get("bias_obj_ang", 0)
            self.jp_override = env_config.get("jp_override")
            super(CustomKukaEnv, self).__init__(
                renders=renders, isDiscrete=isDiscrete, maxSteps=maxSteps
            )
    

    请注意,我们还通过接受 env_config 使其兼容 RLlib。

  4. 我们使用reset方法中的随机化参数来覆盖物体位置的默认随机化程度:

        def reset(self):
            ...
            xpos = 0.55 + self.bias_obj_x + 0.12 * random.random() * self.rnd_obj_x
            ypos = 0 + self.bias_obj_y + 0.2 * random.random() * self.rnd_obj_y
            ang = (
                3.14 * 0.5
                + self.bias_obj_ang
                + 3.1415925438 * random.random() * self.rnd_obj_ang
            )
    
  5. 此外,我们现在应该用CustomKuka替换旧的Kuka类,并将关节位置重写的输入传递给它:

            ...
            self._kuka = CustomKuka(
                jp_override=self.jp_override,
                urdfRootPath=self._urdfRoot,
                timeStep=self._timeStep,
            )
    
  6. 最后,我们重写了环境的step方法,以降低在轴上的默认速度:

        def step(self, action):
            dz = -0.0005
            ...
    		...
                realAction = [dx, dy, dz, da, f]
            obs, reward, done, info = self.step2(realAction)
            return obs, reward / 1000, done, info
    

    还要注意,我们重新缩放了奖励(它将在-10 到 10 之间),以便训练更容易。

做得好!接下来,让我们讨论使用什么样的课程。

设计课程中的课程

确定参数化问题难度的维度是一回事,而决定如何将这种参数化暴露给代理则是另一回事。我们知道,代理应该从简单的课程开始,逐渐过渡到更难的课程。然而,这也引发了一些重要问题:

  • 参数化空间的哪些部分容易?

  • 课与课之间,调整参数的步长应该是多少?换句话说,我们应该如何将空间划分成不同的课程?

  • 代理转到下一课的成功标准是什么?

  • 如果代理在某一课上失败,意味着它的表现出乎意料地差,该怎么办?是否应该回到上一节课?失败的标准是什么?

  • 如果代理长时间无法进入下一课,该怎么办?这是否意味着我们设定的成功标准太高?是否应该将这一课划分为子课?

如你所见,在手动设计课程时,这些问题并非易事。但也请记得,在第十一章《泛化与部分可观察性》中,我们介绍了使用高斯混合模型的绝对学习进度(ALP-GMM)方法,它会为我们处理所有这些决策。在这里,我们将实现两者,首先从手动课程开始。

使用手动设计的课程对代理进行训练

我们将为这个问题设计一个相对简单的课程。当代理达到成功标准时,课程会将它推进到下一课;当表现不佳时,会回退到前一课。该课程将在CustomKukaEnv类内部实现,并包含increase_difficulty方法:

  1. 我们首先定义在课程过渡期间参数值的增量变化。对于关节值,我们将关节位置从用户输入的值(简单)降低到环境中的原始值(困难):

        def increase_difficulty(self):
            deltas = {"2": 0.1, "4": 0.1}
            original_values = {"2": 0.413184, "4": -1.589317}
            all_at_original_values = True
            for j in deltas:
                if j in self.jp_override:
                    d = deltas[j]
                    self.jp_override[j] = max(self.jp_override[j] - d, original_values[j])
                    if self.jp_override[j] != original_values[j]:
                        all_at_original_values = False
    
  2. 在每节课的过渡过程中,我们还确保增加物体位置的随机化程度:

            self.rnd_obj_x = min(self.rnd_obj_x + 0.05, 1)
            self.rnd_obj_y = min(self.rnd_obj_y + 0.05, 1)
            self.rnd_obj_ang = min(self.rnd_obj_ang + 0.05, 1)
    
  3. 最后,当物体位置完全随机化时,我们记得将偏差设置为零:

            if self.rnd_obj_x == self.rnd_obj_y == self.rnd_obj_ang == 1:
                if all_at_original_values:
                    self.bias_obj_x = 0
                    self.bias_obj_y = 0
                    self.bias_obj_ang = 0
    

到目前为止,一切顺利。我们几乎准备好训练代理了。在此之前,还有一件事:让我们讨论如何选择超参数。

超参数选择

为了在 RLlib 中调节超参数,我们可以使用 Ray Tune 库。在第十五章,《供应链管理》中,我们将提供一个示例,说明如何进行调节。现在,您可以直接使用我们在Chapter14/configs.py中选择的超参数。

小贴士

在困难的探索问题中,可能更合理的是先为问题的简化版本调节超参数。这是因为如果没有观察到一些合理的奖励,调节过程可能不会选择一个好的超参数集。我们在一个简单的环境设置中进行初步调节后,如果学习停滞,选择的值可以在后续过程中进行调整。

最后,让我们看看如何在训练过程中使用我们刚刚创建的环境,以及我们定义的课程。

使用 RLlib 在课程中训练智能体

为了继续进行训练,我们需要以下要素:

  • 课程的初始参数

  • 一些定义成功(以及失败,如果需要)的标准

  • 一个回调函数,将执行课程过渡

在以下代码片段中,我们使用 RLlib 中的 PPO 算法,设置初始参数,并在执行课程过渡的回调函数中将奖励阈值(经验值)设置为5.5:

Chapter14/train_ppo_manual_curriculum.py

config["env_config"] = {
    "jp_override": {"2": 1.3, "4": -1}, "rnd_obj_x": 0, 
    "rnd_obj_y": 0, "rnd_obj_ang": 0, "bias_obj_y": 0.04}
def on_train_result(info):
    result = info["result"]
    if result["episode_reward_mean"] > 5.5:
        trainer = info["trainer"]
        trainer.workers.foreach_worker(
            lambda ev: ev.foreach_env(lambda env: env.increase_difficulty()))
ray.init()
tune.run("PPO", config=dict(config,
                            **{"env": CustomKukaEnv,
                               "callbacks": {
                          "on_train_result": on_train_result}}
                            ),
          checkpoint_freq=10)

这应该会启动训练,您将看到课程学习的实际效果!您会注意到,当智能体过渡到下一课时,随着环境变得更困难,其表现通常会下降。

我们稍后会查看此训练的结果。现在让我们继续实现 ALP-GMM 算法。

使用绝对学习进展的课程学习

ALP-GMM 方法专注于在参数空间中性能变化最大(绝对学习进展)的位置,并在该空隙周围生成参数。这个想法在图 14.5中得到了说明:

图 14.5 – ALP-GMM 在点周围生成参数(任务),在这些点之间,观察到最大的回合奖励变化

图 14.5 – ALP-GMM 在点周围生成参数(任务),在这些点之间,观察到最大的回合奖励变化

这样,学习预算就不会花费在已经学习过的状态空间部分,或者花费在当前智能体无法学习的部分上。

在这次回顾之后,让我们继续实现它。我们首先创建一个自定义环境,在这个环境中将运行 ALP-GMM 算法。

Chapter14/custom_kuka.py

我们直接从与论文(Portelas 等,2019)附带的源代码库中获取 ALP-GMM 的实现,并将其放置在Chapter14/alp目录下。然后,我们可以将其插入到我们创建的新环境ALPKukaEnv中,关键部分如下:

  1. 我们创建类并定义我们尝试教授给智能体的参数空间的所有最小值和最大值:

    class ALPKukaEnv(CustomKukaEnv):
        def __init__(self, env_config={}):
            ...
            self.mins = [...]
            self.maxs =  [...]
            self.alp = ALPGMM(mins=self.mins, 
                         maxs=self.maxs, 
                               params={"fit_rate": 20})
            self.task = None
            self.last_episode_reward = None
            self.episode_reward = 0
            super(ALPKukaEnv, self).__init__(env_config)
    

    这里的任务是由 ALP-GMM 算法生成的参数空间中的最新样本,用于配置环境。

  2. 每个回合开始时都会采样一个任务。一旦回合结束,任务(在该回合中使用的环境参数)和回合奖励将被用来更新 GMM 模型:

        def reset(self):
            if self.task is not None and self.last_episode_reward is not None:
                self.alp.update(self.task, 
                                self.last_episode_reward)
            self.task = self.alp.sample_task()
            self.rnd_obj_x = self.task[0]
            self.rnd_obj_y = self.task[1]
            self.rnd_obj_ang = self.task[2]
            self.jp_override = {"2": self.task[3], 
                                "4": self.task[4]}
            self.bias_obj_y = self.task[5]
            return super(ALPKukaEnv, self).reset()
    
  3. 最后,我们确保跟踪每一回合的奖励:

        def step(self, action):
            obs, reward, done, info = super(ALPKukaEnv,  self).step(action)
            self.episode_reward += reward
            if done:
                self.last_episode_reward = self.episode_reward
                self.episode_reward = 0
            return obs, reward, done, info
    

这里需要注意的一点是,ALP-GMM 通常以集中式方式实现:一个中心进程为所有回合工作者生成任务,并收集与这些任务相关的回合奖励进行处理。在这里,由于我们在 RLlib 中工作,所以更容易在环境实例中实现它。为了考虑单次回合中收集的数据量减少,我们使用了"fit_rate": 20,低于原始的 250 水平,这样回合工作者在拟合 GMM 到它收集的任务-奖励数据之前不会等待太久。

创建ALPKukaEnv后,其余的工作只是简单地调用 Ray 的tune.run()函数,位于Chapter14/train_ppo_alp.py中。请注意,与手动课程不同,我们没有指定参数的初始值。而是传递了 ALP-GMM 进程的边界,这些边界引导了课程的设计。

现在,我们准备进行一个课程学习的比拼!

比较实验结果

我们启动了三次训练:一次使用我们描述的手动课程,一次使用 ALP-GMM,另一次则没有实现任何课程。训练进度的 TensorBoard 视图显示在图 14.6中:

图 14.6 – TensorBoard 上的训练进度

图 14.6 – TensorBoard 上的训练进度

一开始你可能认为手动课程和 ALP-GMM 相近,而不使用课程的则排在第三。实际上情况并非如此。让我们来解析这个图:

  • 手动课程从简单到困难。这就是为什么它大部分时间都排在顶部的原因。在我们的运行中,它甚至在时间预算内无法完成最新的课程。因此,图中显示的手动课程表现被夸大了。

  • 无课程训练始终在最困难的级别进行竞争。这就是它大部分时间处于最底部的原因:其他代理并没有运行在最难的参数配置上,因此它们会慢慢到达那里。

  • ALP-GMM 大部分时间处于中间位置,因为它在同时尝试困难和极难的配置,同时关注中间的一些地方。

    由于这个图没有给出明确结论,我们在原始(最困难)配置上评估了代理的表现。每个代理经过 100 个测试回合后的结果如下:

    Agent ALP-GMM score: 3.71
    Agent Manual score: -2.79
    Agent No Curriculum score: 2.81
    
  • 手动课程表现最差,因为在训练结束时无法完成最新的课程。

  • 无课程训练取得了一些成功,但从最困难的设置开始似乎让其退步。此外,评估性能与 TensorBoard 上显示的结果一致,因为在这种情况下,评估设置与训练设置没有不同。

  • ALP-GMM 似乎从逐渐增加难度中受益,并且表现最佳。

  • 无课程训练在 TensorBoard 图表上的峰值与 ALP-GMM 的最新表现相似。因此,我们在垂直速度方面的修改减少了两者之间的差异。然而,不使用课程训练会导致代理在许多困难探索场景中完全无法学习。

您可以在Chapter14/evaluate_ppo.py中找到评估的代码。此外,您还可以使用脚本Chapter14/visualize_policy.py来观察您训练的代理如何表现,看到它们的不足之处,并提出改进性能的想法!

这部分结束了我们对 KUKA 机器人学习示例的讨论。在下一部分,我们将总结本章,并列出一些常用的仿真环境,用于训练自动化机器人和车辆。

超越 PyBullet,进入自动驾驶领域

PyBullet 是一个极好的环境,可以在高保真物理仿真中测试强化学习算法的能力。在机器人技术和强化学习交叉的领域,您会遇到以下一些库:

此外,您将看到基于 Unity 和 Unreal Engine 的环境被用于训练强化学习代理。

下一个更常见的自主性层级当然是自动驾驶车辆。RL 在现实的自动驾驶车辆仿真中也得到了越来越多的实验。这方面最流行的库有:

到此,我们结束了这一章关于机器人学习的内容。这是 RL 中的一个非常热门的应用领域,里面有很多环境,我希望您享受这部分内容并从中获得灵感,开始动手尝试机器人。

总结

自主机器人和车辆将在未来在我们的世界中发挥巨大作用,而强化学习是创建这些自主系统的主要方法之一。在本章中,我们简单了解了训练机器人完成物体抓取任务的过程,这是机器人学中的一大挑战,在制造业和仓库物料搬运中有着广泛应用。我们使用 PyBullet 物理仿真器,在一个困难的探索环境中训练了一个 KUKA 机器人,并使用了手动和基于 ALP-GMM 的课程学习方法。现在,既然你已经较为熟练地掌握了如何利用这些技术,你可以尝试解决其他类似的问题。

在下一章中,我们将深入探讨强化学习应用的另一个主要领域:供应链管理。敬请期待另一段激动人心的旅程!

参考文献

第十五章:第十五章:供应链管理

有效的供应链管理是许多企业面临的挑战,但它对于企业的盈利能力和竞争力至关重要。这一领域的困难来自于影响供需关系的复杂动态、处理这些动态的商业约束,以及其中巨大的不确定性。强化学习(RL)为我们提供了一套关键能力,帮助解决这类序列决策问题。

在本章中,我们特别关注两个重要问题:库存和路由优化。对于前者,我们深入探讨了如何创建环境、理解环境中的变化,以及如何通过超参数调优有效地使用强化学习解决问题。对于后者,我们描述了一个现实的车辆路由问题——一名临时司机为在线餐饮订单提供配送服务。接着,我们展示了为什么传统的神经网络在解决不同规模的问题时存在局限性,以及指针网络如何克服这一问题。

这将是一段有趣的旅程。本章将涵盖以下内容:

  • 优化库存采购决策

  • 路由问题建模

优化库存采购决策

几乎所有制造商、分销商和零售商需要不断做出的最重要决策之一就是,如何保持足够的库存,以便可靠地满足客户需求,同时最小化成本。有效的库存管理对于大多数公司的盈利能力和生存至关重要,特别是在当今竞争激烈的环境中,考虑到薄弱的利润率和不断提升的客户期望。在本节中,我们将利用强化学习来解决这一挑战,并优化库存采购决策。

库存的需求及其管理中的权衡

当你走进超市时,你会看到物品堆积在一起。超市的仓库里可能有更多的这些物品,分销商的仓库里也有更多,制造商的工厂里更是如此。想一想,这些巨大的产品堆积物就静静地待在某个地方,等待着未来某个时刻被客户需求。如果这听起来像是资源浪费,那它在很大程度上确实是。另一方面,企业必须保持一定量的库存,因为以下原因:

  • 未来充满不确定性。客户需求、制造能力、运输计划和原材料的可用性,都可能在某些时刻以无法预见的方式变化。

  • 不可能以完美的准时制方式运作,并在客户要求时立即制造并交付物品。

由于保持库存通常是不可避免的,那么问题就在于“多少”。回答这一问题涉及到一个复杂的权衡:

  • 最小化无法满足客户需求的机会,这不仅会导致利润损失,更重要的是会损失客户忠诚度,而忠诚度一旦丧失,恢复将变得非常困难。

  • 降低库存,因为它会带来资本、劳动力、时间、材料、维护和仓储租金等成本,还可能导致商品过期或过时以及组织管理的开销。

那么,你会如何处理这种情况?是将客户满意度作为绝对优先考虑,还是更愿意保持库存的可控?实际上,这种平衡行为需要仔细的规划和高级方法的应用,而并非所有公司都有能力做到这一点。因此,大多数公司更倾向于选择“保险策略”,即保持比实际需求更多的库存,这有助于掩盖缺乏规划和相关问题。这个现象通常被形象地描述为“库存之海”,如图 15.1所示:

图 15.1 – 库存之海隐藏了许多问题

图 15.1 – 库存之海隐藏了许多问题

在这种情况下,强化学习(RL)可以发挥作用,在面对不确定性时优化库存决策。接下来,我们通过讨论库存优化问题的组成部分,开始构建我们的解决方案。

库存优化问题的组成部分。

有多个因素影响库存流动的动态,以及在给定商品的情况下,最佳补货政策将会是什么样子:

  • 商品的价格是决定其销售价格的关键因素。

  • 商品的采购成本是另一个关键因素,它与价格一起决定了每件商品所赚取的毛利润。这反过来影响了失去一个客户需求单位的成本。

  • 库存持有成本是指在单个时间步(例如一天、一周或一个月等)内,持有单个库存单位的所有成本总和。这包括存储租金、资本成本以及任何维护成本等。

  • 客户信任流失是由于丧失单个需求单位所导致的客户不满的货币成本。毕竟,这会减少客户忠诚度并影响未来的销售。虽然不满通常是定性衡量的,但企业需要估算其货币等价物,以便在决策中使用。

  • 对商品的客户需求在单个时间步中的变化是影响决策的主要因素之一。

  • 供应商交货时间(VLT)是指从向供应商下订单到商品到达库存的延迟时间。毫不奇怪,VLT 是决定何时下订单以应对预期需求的关键因素。

  • 容量限制,例如单次批量可订购商品的数量,以及公司的存储能力,将限制代理可以采取的行动。

这些是我们在这里设置时需要考虑的主要因素。此外,我们还将重点关注以下内容:

  • 单一商品情景

  • 具有泊松分布的随机客户需求,在给定时期内具有确定的且固定的均值。

  • 除需求外的确定性因素

这使得我们的案例在保持足够复杂性的同时变得可解。

提示

在实际环境中,大多数动态都涉及不确定性。例如,可能到达的库存存在缺陷;价格可能会随着物品的过时程度而变化;可能由于天气原因导致现有库存损失;可能有物品需要退货。估算所有这些因素的特征并创建过程的仿真模型是一个真正的挑战,成为了将强化学习作为此类问题工具的障碍。

我们所描述的是一个复杂的优化问题,当前没有可行的最优解。然而,它的单步版本,称为新闻售货员问题,已被广泛研究和应用。这是一个极好的简化方法,可以帮助我们形成对问题的直观理解,并且还将帮助我们为多步情况获得一个接近最优的基准。让我们深入研究一下。

单步库存优化 – 新闻售货员问题

当库存优化问题涉及以下内容时:

  • 单一时间步长(因此没有 VLT;库存到货是确定性的)

  • 单一商品

  • 已知价格、购买成本和未售出库存的成本

  • 一个已知的(且方便的高斯)需求分布

然后,这个问题被称为新闻售货员问题,我们可以为其获得封闭形式的解。它描述了一个报纸销售员,旨在规划当天购买多少份报纸,单位成本为,以单位价格出售,并在当天结束时以单位价格退还未售出的份数。接着,我们定义以下量:

  • 缺货成本,,是由于错失一单位需求所造成的利润损失:。

  • 过剩成本,,是未售出的单元成本:。

为了找到最优的订货量,我们接下来计算一个关键比率,,如下所示:

现在,让我们来分析这个关键比率如何随着缺货和过剩成本的变化而变化:

  • 随着的增加,增加。更高的和意味着错失客户需求的成本更高。这表明,在补充库存时应更加积极,以避免错失机会。

  • 随着的增加,减少,这意味着未售出库存的成本更高。这表明我们在库存量上应该保持保守。

结果表明,给出了为了优化预期利润,应该覆盖的需求场景的百分比。换句话说,假设需求有一个概率分布函数,并且有一个累积分布函数(CDF)。最佳订货量由给出,其中是 CDF 的逆函数。

让我们通过一个例子来进行实验。

新闻供应商示例

假设某昂贵商品的价格为,其来源为。如果该商品未售出,不能退还给供应商,且会变成废品。因此,我们有。在这种情况下,缺货成本为,过剩成本为,这使得我们得出临界比率。这表明订货量有 80%的几率能满足需求。假设需求服从正态分布,均值为 20 件,标准差为 5 件,则最佳订货量约为 24 件。

您可以使用在Newsvendor plots.ipynb文件中定义的calc_n_plot_critical_ratio函数计算并绘制最佳订货量:

Chapter15/Newsvendor plots.ipynb

calc_n_plot_critical_ratio(p=2000, c=400, r=0, mean=20, std=5)

您应该看到如下输出:

Cost of underage is 1600
Cost of overage is 400
Critical ratio is 0.8
Optimal order qty is 24.20810616786457

图 15.2展示了需求的概率分布、累积分布函数(CDF)以及该问题对应的临界比率值:

图 15.2 – 示例新闻供应商问题的最佳订货量

图 15.2 – 示例新闻供应商问题的最佳订货量

这是为了让您对单步库存优化问题的解决方案有一个直观的了解。现在,多步问题涉及到许多其他复杂性,我们在上一节中进行了描述。例如,库存到货存在滞后,剩余库存会带到下一步并产生持有成本。这就是不确定性下的序列决策问题,而这正是强化学习的强项。所以,让我们来使用它。

模拟多步库存动态

在本节中,我们创建了一个模拟环境,用于描述的多步库存优化问题。

信息

本章的其余部分紧密跟随Balaji 等人,2019中定义的问题和环境,其中的代码可在github.com/awslabs/or-rl-benchmarks找到。我们建议您阅读该论文,以获取有关经典随机优化问题的强化学习(RL)方法的更多细节。

在开始描述环境之前,让我们讨论几个注意事项:

  • 我们希望创建一个不仅适用于特定产品需求场景的策略,而是适用于广泛场景的策略。因此,对于每个周期,我们会随机生成环境参数,正如您将看到的那样。

  • 这种随机化增加了梯度估计的方差,使得学习过程比静态场景更加具有挑战性。

有了这些,我们来详细讨论一下环境的细节。

事件日历

为了正确地应用环境的阶跃函数,我们需要了解每个事件发生的时间。让我们来看一下:

  1. 每天开始时,库存补货订单会被下达。根据提前期,我们将其记录为“运输中”库存。

  2. 接下来,当前日程安排的物品到达。如果提前期为零,那么当天一开始就下的订单会立刻到达。如果提前期为一天,那么昨天的订单会到达,依此类推。

  3. 在货物收到后,需求会在一天内逐渐显现。如果库存不足以满足需求,实际销售量将低于需求,并且会失去客户的好感。

  4. 在一天结束时,我们从库存中扣除已售出的物品(不是总需求),并相应更新状态。

  5. 最后,如果提前期非零,我们会更新运输中的库存(即,我们将原本在 t + 2 到达的库存移至 t + 1)。

现在,让我们编码我们所描述的内容。

编码环境

你可以在我们的 GitHub 仓库中找到完整的环境代码,链接为:github.com/PacktPublishing/Mastering-Reinforcement-Learning-with-Python/blob/master/Chapter15/inventory_env.py。

在这里,我们只描述环境的一些关键部分:

  1. 如前所述,每个回合将抽样某些环境参数,以便获得可以在广泛的价格、需求等场景中工作的策略。我们设置这些参数的最大值,然后从中生成特定回合的参数:

    class InventoryEnv(gym.Env):
        def __init__(self, config={}):
            self.l = config.get("lead time", 5)
            self.storage_capacity = 4000
            self.order_limit = 1000
            self.step_count = 0
            self.max_steps = 40
            self.max_value = 100.0
            self.max_holding_cost = 5.0
            self.max_loss_goodwill = 10.0
            self.max_mean = 200
    

    我们使用 5 天的提前期,这对确定观察空间非常重要(可以认为它等同于此问题中的状态空间,因此我们可以互换使用这两个术语)。

  2. 价格、成本、持有成本、客户好感损失和预期需求是状态空间的一部分,我们假设这些也对代理可见。此外,我们需要追踪现有库存,以及如果提前期非零,还需要追踪运输中的库存:

            self.inv_dim = max(1, self.l)
            space_low = self.inv_dim * [0]
            space_high = self.inv_dim * [self.storage_capacity]
            space_low += 5 * [0]
            space_high += [
                self.max_value,
                self.max_value,
                self.max_holding_cost,
                self.max_loss_goodwill,
                self.max_mean,
            ]
            self.observation_space = spaces.Box(
                low=np.array(space_low), 
                high=np.array(space_high), 
                dtype=np.float32
            )
    

    请注意,对于 5 天的提前期,我们有一个维度表示现有库存,四个维度(从到)表示运输中的库存。正如你将看到的,通过在步骤计算结束时将运输中库存添加到状态中,我们可以避免追踪将会到达的运输中库存(更一般地说,我们不需要,表示提前期)。

  3. 我们将动作规范化为在 之间,其中 1 表示以订单限额下单:

            self.action_space = spaces.Box(
                low=np.array([0]), 
                high=np.array([1]), 
                dtype=np.float32
            )
    
  4. 一个非常重要的步骤是规范化观察值。通常,智能体可能不知道观察值的边界以便进行规范化。在这里,我们假设智能体有这个信息,因此我们在环境类中方便地处理了它:

        def _normalize_obs(self):
            obs = np.array(self.state)
            obs[:self.inv_dim] = obs[:self.inv_dim] / self.order_limit
            obs[self.inv_dim] = obs[self.inv_dim] / self.max_value
            obs[self.inv_dim + 1] = obs[self.inv_dim + 1] / self.max_value
            obs[self.inv_dim + 2] = obs[self.inv_dim + 2] / self.max_holding_cost
            obs[self.inv_dim + 3] = obs[self.inv_dim + 3] / self.max_loss_goodwill
            obs[self.inv_dim + 4] = obs[self.inv_dim + 4] / self.max_mean
            return obs
    
  5. 特定回合的环境参数是在 reset 函数中生成的:

        def reset(self):
            self.step_count = 0
            price = np.random.rand() * self.max_value
            cost = np.random.rand() * price
            holding_cost = np.random.rand() * min(cost, self.max_holding_cost)
            loss_goodwill = np.random.rand() * self.max_loss_goodwill
            mean_demand = np.random.rand() * self.max_mean
            self.state = np.zeros(self.inv_dim + 5)
            self.state[self.inv_dim] = price
            self.state[self.inv_dim + 1] = cost
            self.state[self.inv_dim + 2] = holding_cost
            self.state[self.inv_dim + 3] = loss_goodwill
            self.state[self.inv_dim + 4] = mean_demand
            return self._normalize_obs()
    
  6. 我们按照前一部分描述的方式实现 step 函数。首先,我们解析初始状态和接收到的动作:

        def step(self, action):
            beginning_inv_state, p, c, h, k, mu = \
                self.break_state()
            action = np.clip(action[0], 0, 1)
            action = int(action * self.order_limit)
            done = False
    
  7. 然后,我们在观察容量的同时确定可以购买的数量,如果没有提前期,就将购买的部分添加到库存中,并采样需求:

            available_capacity = self.storage_capacity \
                                 - np.sum(beginning_inv_state)
            assert available_capacity >= 0
            buys = min(action, available_capacity)
            # If lead time is zero, immediately
            # increase the inventory
            if self.l == 0:
                self.state[0] += buys
            on_hand = self.state[0]
            demand_realization = np.random.poisson(mu)
    
  8. 奖励将是收入,我们从中扣除购买成本、库存持有成本和失去的客户信誉成本:

            # Compute Reward
            sales = min(on_hand,
                        demand_realization)
            sales_revenue = p * sales
            overage = on_hand - sales
            underage = max(0, demand_realization
                              - on_hand)
            purchase_cost = c * buys
            holding = overage * h
            penalty_lost_sale = k * underage
            reward = sales_revenue \
                     - purchase_cost \
                     - holding \
                     - penalty_lost_sale
    
  9. 最后,我们通过将在途库存移动一天来更新库存水平,如果 VLT 不为零,还需要将当日购买的物品添加到在途库存中:

            # Day is over. Update the inventory
            # levels for the beginning of the next day
            # In-transit inventory levels shift to left
            self.state[0] = 0
            if self.inv_dim > 1:
                self.state[: self.inv_dim - 1] \
                    = self.state[1: self.inv_dim]
            self.state[0] += overage
            # Add the recently bought inventory
            # if the lead time is positive
            if self.l > 0:
                self.state[self.l - 1] = buys
            self.step_count += 1
            if self.step_count >= self.max_steps:
                done = True
    
  10. 最后,我们将规范化后的观察值和缩放后的奖励返回给智能体:

            # Normalize the reward
            reward = reward / 10000
            info = {
                "demand realization": demand_realization,
                "sales": sales,
                "underage": underage,
                "overage": overage,
            }
            return self._normalize_obs(), reward, done, info
    

花时间理解库存动态是如何在步骤函数中体现的。一旦准备好,我们就可以开始开发基准策略。

开发近似最优基准策略

这个问题的精确解不可得。然而,通过类似新闻商政策的两项修改,我们获得了一个近似最优的解:

  • 考虑到的总需求是在 时间步长内的需求,而不是单一时间步。

  • 我们还将客户流失的损失加到缺货成本中,除了每单位的利润之外。

之所以仍然是近似解,是因为该公式将多个步骤视为一个单一步骤,并汇总需求和供应,意味着它假设需求在某个步骤到达,并可以在随后的步骤中积压并得到满足,这一过程持续在 步长内进行。不过,这仍然为我们提供了一个近似最优的解决方案。

以下是如何编写此基准策略的代码:

def get_action_from_benchmark_policy(env):
    inv_state, p, c, h, k, mu = env.break_state()
    cost_of_overage = h
    cost_of_underage = p - c + k
    critical_ratio = np.clip(
        0, 1, cost_of_underage
              / (cost_of_underage + cost_of_overage)
    )
    horizon_target = int(poisson.ppf(critical_ratio,
                         (len(inv_state) + 1) * mu))
    deficit = max(0, horizon_target - np.sum(inv_state))
    buy_action = min(deficit, env.order_limit)
    return [buy_action / env.order_limit]

请注意,在我们计算临界比率后,我们进行以下操作:

  • 我们为 步骤找到最优的总供应量。

  • 然后,我们减去下一步 时间步长内的在手库存和在途库存。

  • 最后,我们下单以弥补这一差额,但单次订单有上限。

接下来,让我们看看如何训练一个强化学习智能体来解决这个问题,以及强化学习解决方案与此基准的比较。

一种用于库存管理的强化学习解决方案

在解决这个问题时,有几个因素需要考虑:

  • 由于环境中的随机化,每一回合的奖励有很高的方差。

  • 这要求我们使用高于正常的批量和小批量大小,以更好地估计梯度并对神经网络权重进行更稳定的更新。

  • 在高方差的情况下,选择最佳模型也是一个问题。这是因为如果测试/评估的回合数不足,可能会仅仅因为在一些幸运的配置中评估了某个策略,就错误地认为它是最好的。

为了应对这些挑战,我们可以采取以下策略:

  1. 在有限的计算预算下进行有限的超参数调优,以识别出一组好的超参数。

  2. 使用一个或两个最佳超参数集来训练模型,并在训练过程中保存最佳模型。

  3. 当你观察到奖励曲线的趋势被噪声主导时,增加批量和小批量的大小,以便更精确地估计梯度并去噪模型性能指标。再次保存最佳模型。

  4. 根据您的计算预算,重复多次并选择最佳模型。

所以,让我们在 Ray/RLlib 中实现这些步骤以获得我们的策略。

初始超参数搜索

我们使用 Ray 的 Tune 库进行初步的超参数调优。我们将使用两个函数:

  • tune.grid_search() 在指定的值集合上进行网格搜索。

  • tune.choice() 在指定的集合中进行随机搜索。

对于每个试验,我们还指定停止条件。在我们的案例中,我们希望运行一百万个时间步骤的试验。

下面是一个示例搜索的代码:

import ray
from ray import tune
from inventory_env import InventoryEnv
ray.init()
tune.run(
    "PPO",
    stop={"timesteps_total": 1e6},
    num_samples=5,
    config={
        "env": InventoryEnv,
        "rollout_fragment_length": 40,
        "num_gpus": 1,
        "num_workers": 50,
        "lr": tune.grid_search([0.01, 0.001, 0.0001, 0.00001]),
        "use_gae": tune.choice([True, False]),
        "train_batch_size": tune.choice([5000, 10000, 20000, 40000]),
        "sgd_minibatch_size": tune.choice([128, 1024, 4096, 8192]),
        "num_sgd_iter": tune.choice([5, 10, 30]),
        "vf_loss_coeff": tune.choice([0.1, 1, 10]),
        "vf_share_layers": tune.choice([True, False]),
        "entropy_coeff": tune.choice([0, 0.1, 1]),
        "clip_param": tune.choice([0.05, 0.1, 0.3, 0.5]),
        "vf_clip_param": tune.choice([1, 5, 10]),
        "grad_clip": tune.choice([None, 0.01, 0.1, 1]),
        "kl_target": tune.choice([0.005, 0.01, 0.05]),
        "eager": False,
    },
)

为了计算总的调优预算,我们执行以下操作:

  • 获取所有网格搜索的交叉乘积,因为根据定义必须尝试每一个可能的组合。

  • 将交叉乘积与 num_samples 相乘,得到将要进行的试验总数。使用前面的代码,我们将有 20 次试验。

  • 在每次试验中,每个 choice 函数会从相应的集合中随机均匀地选择一个参数。

  • 当满足停止条件时,给定的试验将停止。

执行时,您将看到搜索过程正在进行。它看起来像图 15.3:

图 15.3 – 使用 Ray 的 Tune 进行超参数调优

图 15.3 – 使用 Ray 的 Tune 进行超参数调优

一些试验会出错,除非你特别注意避免数值问题的超参数组合。然后,你可以选择表现最好的组合进行进一步的训练,如图 15.4所示:

图 15.4 – 在搜索中获得的优质超参数集的样本性能

图 15.4 – 在搜索中获得的优质超参数集的样本性能

接下来,让我们进行广泛的训练。

模型的广泛训练

现在,我们开始使用选定的超参数集进行长时间训练(或者使用多个超参数集——在我的案例中,之前的最佳集表现不好,但第二个集表现良好):

Chapter15/train_inv_policy.py

import numpy as np
import ray
from ray.tune.logger import pretty_print
from ray.rllib.agents.ppo.ppo import DEFAULT_CONFIG
from ray.rllib.agents.ppo.ppo import PPOTrainer
from inventory_env import InventoryEnv
config = DEFAULT_CONFIG.copy()
config["env"] = InventoryEnv
config["num_gpus"] = 1
config["num_workers"] = 50
config["clip_param"] = 0.3
config["entropy_coeff"] = 0
config["grad_clip"] = None
config["kl_target"] = 0.005
config["lr"] = 0.001
config["num_sgd_iter"] = 5
config["sgd_minibatch_size"] = 8192
config["train_batch_size"] = 20000
config["use_gae"] = True
config["vf_clip_param"] = 10
config["vf_loss_coeff"] = 1
config["vf_share_layers"] = False

一旦设置了超参数,就可以开始训练:

ray.init()
trainer = PPOTrainer(config=config, env=InventoryEnv)
best_mean_reward = np.NINF
while True:
    result = trainer.train()
    print(pretty_print(result))
    mean_reward = result.get("episode_reward_mean", np.NINF)
    if mean_reward > best_mean_reward:
        checkpoint = trainer.save()
        print("checkpoint saved at", checkpoint)
        best_mean_reward = mean_reward

这里有一点需要注意,那就是批量和小批量的大小:通常情况下,RLlib 中的 PPO 默认设置是"train_batch_size": 4000和"sgd_minibatch_size": 128。然而,由于环境和奖励的方差,使用如此小的批量会导致学习效果不佳。因此,调优模型选择了更大的批量和小批量大小。

现在进行训练。此时,您可以根据训练进度开发逻辑来调整各种超参数。为了简化起见,我们将手动观察进度,并在学习停滞或不稳定时停止。在那之后,我们可以进一步增加批量大小进行训练,以获得最终阶段的更好梯度估计,例如 "train_batch_size": 200000 和 "sgd_minibatch_size": 32768。这就是训练过程的样子:

图 15.5 – 训练从 20k 的批量大小开始,并继续使用 200k 的批量使用 200k 的批量大小来减少噪声

图 15.5 – 训练从 20k 的批量大小开始,并继续使用 200k 的批量大小以减少噪声

更高的批量大小微调帮助我们去噪奖励并识别真正高效的模型。然后,我们可以比较基准和强化学习(RL)解决方案。经过 2,000 个测试回合后,基准性能如下所示:

Average daily reward over 2000 test episodes: 0.15966589703658918\. 
Average total epsisode reward: 6.386635881463566

我们可以在这里看到 RL 模型的表现:

Average daily reward over 2000 test episodes: 0.14262437792900876\. 
Average total epsisode reward: 5.70497511716035

我们的 RL 模型性能在接近最优基准解的 10%以内。我们可以通过进一步的试验和训练来减少差距,但在如此噪声的环境下,这是一个具有挑战性且耗时的工作。请注意,Balaji et al., 2019报告的指标略微改善了基准,因此这是可行的。

至此,我们结束了对这个问题的讨论。做得好!我们已经将一个现实且噪声较大的供应链问题从初始形态中建模,使用 RL 进行建模,并通过 RLlib 上的 PPO 解决了它!

接下来,我们描述两个可以通过 RL 解决的额外供应链问题。由于篇幅限制,我们在这里无法解决这些问题,但我们建议您参考github.com/awslabs/or-rl-benchmarks/获取更多信息。

那么,接下来让我们讨论如何使用 RL 来建模和解决路由优化问题。

路由问题建模

路由问题是组合优化中最具挑战性和最受研究的问题之一。事实上,有相当多的研究人员将毕生精力投入到这一领域。最近,RL 方法作为一种替代传统运筹学方法的路由问题解决方案已经浮出水面。我们从一个相对复杂的路由问题开始,它是关于在线餐饮订单的取送。另一方面,这个问题的 RL 建模并不会太复杂。我们稍后将根据该领域的最新文献,扩展讨论更先进的 RL 模型。

在线餐饮订单的取送

考虑一个“零工”司机(我们的代理),他为一个在线平台工作,类似于 Uber Eats 或 Grubhub,从餐厅接订单并配送给客户。司机的目标是通过配送更多高价值订单来赚取尽可能多的小费。以下是这个环境的更多细节:

  • 城市中有多个餐厅,这些餐厅是订单的取件地点。

  • 与这些餐厅相关的订单会动态地出现在配送公司的应用程序上。

  • 司机必须接受一个订单才能去取件并进行配送。

  • 如果一个已接受的订单在创建后的一定时间内未被配送,则会遭受高额罚款。

  • 如果司机未接受一个开放订单,该订单会在一段时间后消失,意味着订单被竞争的其他司机抢走。

  • 司机可以接受任意数量的订单,但实际能携带的已取件订单数量有限。

  • 城市的不同区域以不同的速率和不同的规模生成订单。例如,一个区域可能生成频繁且高价值的订单,从而吸引司机。

  • 不必要的行程会给司机带来时间、燃料和机会成本。

在这个环境下,每个时间步,司机可以采取以下动作之一:

  • 接受其中一个开放订单。

  • 朝一个与特定订单相关的餐厅移动一步(取件)。

  • 朝客户位置移动一步(进行配送)。

  • 等待并什么都不做。

  • 朝一个餐厅移动一步(不是去取现有订单,而是希望很快会从该餐厅接到一个高价值订单)。

代理观察以下状态以做出决策:

  • 司机、餐厅和客户的坐标

  • 司机使用的和可用的运载能力

  • 订单状态(开放、已接受、已取件、非活动/已配送/未创建)

  • 订单与餐厅的关联

  • 从订单创建开始经过的时间

  • 每个订单的奖励(小费)

如需更多信息,可以访问此环境:github.com/awslabs/or-rl-benchmarks/blob/master/Vehicle%20Routing%20Problem/src/vrp_environment.py。

Balaji et al., 2019 研究表明,RL 解决方案在此问题上的表现优于基于混合整数 规划(MIP)的方法。这是一个相当令人惊讶的结果,因为 MIP 模型理论上可以找到最优解。MIP 解决方案在此情况下被超越的原因如下:

  • 它解决了一个眼前问题,而 RL 代理学习预测未来事件并相应地进行规划。

  • 它使用了有限的预算,因为 MIP 解决方案可能需要很长时间,而 RL 推理一旦训练好策略后,几乎是瞬时发生的。

对于如此复杂的问题,报告的强化学习(RL)性能相当鼓舞人心。另一方面,我们所采用的问题建模方式有一定局限性,因为它依赖于固定的状态和动作空间大小。换句话说,如果状态和动作空间设计为处理最多 N 个订单/餐馆,那么训练后的智能体无法用于更大的问题。另一方面,混合整数规划(MIP)模型可以接受任何大小的输入(尽管大规模问题可能需要很长时间才能解决)。

深度学习领域的最新研究为我们提供了指针网络,用于处理动态大小的组合优化问题。接下来我们将深入探讨这个话题。

用于动态组合优化的指针网络

指针网络使用基于内容的注意力机制来指向其输入中的一个节点,输入的数量可以是任意的。为了更好地解释这一点,考虑一个旅行推销员问题,其目标是在二维平面上访问所有节点,每个节点仅访问一次,并在最后回到初始节点,且以最小的总距离完成。图 15.6 展示了一个示例问题及其解决方案:

![图 15.6 – 旅行推销员问题的解决方案

(来源:en.wikipedia.org/wiki/Travelling_salesman_problem)

](https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/ms-rl-py/img/B14160_15_6.jpg)

图 15.6 – 旅行推销员问题的解决方案(来源:en.wikipedia.org/wiki/Travelling_salesman_problem)

该问题中的每个节点通过其 坐标表示。指针网络具有以下特点:

  • 使用递归神经网络从编码器中的输入节点 的 获得嵌入 ,并在解码的 步骤中类似地获得解码器中的 。

  • 在解码 步骤时,计算输入节点 的注意力,计算方式如下:

其中 和 是可学习的参数。

  • 拥有最高注意力 的输入节点 将成为路径中需要访问的 节点。

这种注意力方法完全灵活,可以在不对输入节点总数做任何假设的情况下指向特定的输入节点。该机制在 图 15.7 中得到了展示:

图 15.7 – 一个指针网络(来源:Vinyals 等,2017)

图 15.7 – 一个指针网络(来源:Vinyals 等,2017)

后来的研究(Nazari 等人,2018)将指针网络应用于基于策略的强化学习模型,并在相对复杂的问题中取得了非常有前景的结果,与开源路径优化器相比具有优势。指针网络的细节及其在强化学习中的应用值得进一步的探讨,我们将在本章末尾引用的论文中深入讨论。

通过上述内容,我们结束了关于强化学习在供应链问题中的应用讨论。让我们总结一下所涉及的内容,以便结束这一章节。

总结

本章中,我们讨论了供应链管理中的两个重要问题类别:库存优化和车辆路径规划。这些问题都非常复杂,强化学习(RL)最近作为一种竞争性工具出现,用于解决这些问题。在本章中,对于前者问题,我们提供了如何创建环境并解决相应强化学习问题的详细讨论。这个问题的挑战在于各个回合之间的高方差,我们通过细致的超参数调优程序来缓解这一问题。对于后者问题,我们描述了一个真实的案例,讲述了一名外卖司机如何处理来自顾客的动态订单。我们讨论了如何通过指针网络使得模型在处理不同节点大小时更加灵活。

在下一章节中,我们将讨论另一组令人兴奋的应用,涉及个性化、市场营销和金融领域。我们在那里见!

参考文献

第十六章:第十六章:营销、个性化和金融

本章中,我们讨论了强化学习(RL)在三个领域中获得显著关注的情况。首先,我们描述了它如何用于个性化和推荐系统。通过这一点,我们超越了之前章节中介绍的单步强盗方法。一个相关领域也可以从 RL 中受益匪浅,那就是营销。除了个性化营销应用,RL 还可以帮助管理活动预算和减少客户流失等领域。最后,我们讨论了 RL 在金融领域的前景和相关挑战。在这个过程中,我们介绍了 TensorTrade,这是一个用于开发和测试基于 RL 的交易算法的 Python 框架。

因此,在本章中,我们将涵盖以下内容:

  • 超越强盗模型进行个性化

  • 使用强化学习(RL)开发有效的营销策略

  • 将 RL 应用于金融

超越强盗模型进行个性化

当我们在本书的早期章节中讨论多臂强盗和上下文强盗问题时,我们提出了一个案例研究,旨在最大化点击率(CTR)的在线广告。这只是一个例子,展示了强盗模型如何用于为用户提供个性化内容和体验,这是几乎所有在线(和离线)内容提供商面临的共同挑战,从电子零售商到社交媒体平台。在这一部分,我们将超越强盗模型,描述一个多步强化学习方法来进行个性化。让我们首先讨论强盗模型的不足之处,然后再看看多步 RL 如何解决这些问题。

强盗模型的缺点

强盗问题的目标是最大化即时(单步)回报。在一个在线广告点击率(CTR)最大化问题中,这通常是一个很好的思考目标:展示在线广告,用户点击,然后完成!如果没有点击,那就是错失。

用户与平台之间的关系,比方说,YouTube 或 Amazon,远比这更复杂。在这些平台上的用户体验是一个过程,而不是一个事件。平台推荐一些内容,如果用户没有点击,也不完全是失败。可能用户发现了推荐的内容有趣且吸引人,只是继续浏览。即使该用户会话没有产生点击或转化,用户也可能很快回来,因为他们知道平台会推荐一些与其兴趣相关的内容。相反,如果会话中有太多的点击,很可能意味着用户没有找到他们想要的内容,从而导致了不好的体验。这种“过程性”的问题特性——即平台(代理)的决策对客户满意度和商业价值(奖励)有下游影响——正是使得多步强化学习成为这里吸引人的方法。

提示

虽然从赌博模型转向多步强化学习是诱人的,但在这么做之前要三思而后行。赌博算法更容易使用,并且具有公认的理论特性,而在多步强化学习的设定中成功训练代理人可能会非常具有挑战性。需要注意的是,许多多步问题可以通过为代理人包含记忆并在当前奖励中考虑动作的期望未来价值,将其转化为单步问题,这样我们就可以留在赌博框架中。

深度强化学习在个性化方面的一个成功实现与新闻推荐相关,由郑等人(2018)提出。在下一节中,我们将描述一种类似的解决方法,灵感来自这项工作,尽管我们的讨论会比论文中的内容更高层次和更广泛。

深度强化学习在新闻推荐中的应用

当我们打开最喜欢的新闻应用时,我们期待阅读一些有趣且可能重要的内容。当然,什么内容是有趣或重要的对每个人来说都不同,因此我们面临个性化问题。正如郑等人(2018)提到的,新闻推荐中有一些额外的挑战:

  • 动作空间并非固定的。实际上,情况恰恰相反:每天涌入大量新闻,每条新闻都有一些独特的特征,这使得很难将问题看作一个传统的 Gym 环境。

  • 用户偏好也是相当动态的:它们随着时间变化而改变和演变。本周对政治感兴趣的用户,下周可能会感到厌倦,转而阅读关于艺术的内容,郑等人(2018)通过数据证明了这一点。

  • 正如我们所提到的,这是一个真正的多步问题。如果有两条新闻可以展示,一条是关于灾难的,另一条是关于体育比赛的,展示前者可能因为其轰动效应而更容易被点击。然而,这也可能导致用户因情绪低落而提前离开平台,从而减少更多平台上的互动。

  • 代理人对用户的观察相对于可能影响用户行为的所有因素来说是非常有限的。因此,环境是部分可观察的。

因此,问题在于从动态库存中选择哪些新闻展示给用户,用户的兴趣一方面随着时间变化,另一方面受到许多代理人未完全观察到的因素的影响。

接下来我们描述一下强化学习问题的组成部分。

观察和动作空间

借鉴郑等人(2018)的研究,关于特定用户和新闻内容的观察和动作空间包括以下信息:

  • 用户特征,与用户在该会话中、当天、过去一周点击的所有新闻的特征相关,用户在不同时间段内访问平台的次数等等。

  • 上下文特征与一天中的时间、星期几、是否是节假日、选举日等信息相关。

  • 用户-新闻特征,例如这篇特定新闻在过去一小时内出现在特定用户的新闻流中的次数,以及与新闻中的实体、主题和类别相关的类似统计数据。

  • 这篇新闻的新闻特征,如主题、类别、提供者、实体名称、过去一小时、过去 6 小时、过去一天的点击次数等。

现在,这构成了一个与我们通常使用的观察-动作空间不同的空间:

  • 前两组特征更像是一种观察:用户出现,请求新闻内容,代理观察用户和与上下文相关的特征。

  • 然后,代理需要选择一个新闻条目进行展示(或者如论文所示,选择一组新闻条目)。因此,新闻相关的特征和用户-新闻特征对应着一个动作。

  • 有趣的是,可用的动作集是动态的,并且它包含了与用户/观察相关的元素(在用户-新闻特征中)。

尽管这不是一种传统的设置,但不要让它吓到你。我们仍然可以估算,例如,给定观察-动作对的 Q 值:我们只需要将所有这些特征输入到一个神经网络中,该网络估算 Q 值。具体来说,论文使用了两个神经网络,一个估算状态值,另一个估算优势,以计算 Q 值,尽管也可以使用一个单独的网络。这个过程在图 16.1 中有说明:

图 16.1 – 新闻推荐代理的 Q 网络

图 16.1 – 新闻推荐代理的 Q 网络

现在,比较一下传统的 Q 网络:

  • 一个常规的 Q 网络会有多个头,每个头对应一个动作(如果动作头估算的是优势而不是 Q 值,还会有一个额外的头用于价值估算)。这样的网络在一次前向传播中输出给定观察的所有固定动作集的 Q 值估算。

  • 在这种设置下,我们需要对每个可用的新闻条目进行单独的前向传播,基于用户的输入,然后选择具有最高 Q 值的新闻条目。

这种方法实际上类似于我们在第三章中使用的,上下文型强盗。

接下来,我们来讨论在这种设置中可以使用的另一种建模方法。

使用动作嵌入

当动作空间非常大和/或在每个时间步骤中有所变化时,就像这个问题中那样,动作嵌入可以用来根据观察选择一个动作。动作嵌入是一个表示动作的固定大小数组,通常通过神经网络的输出得到。

其工作原理如下:

  • 我们使用一个策略网络,它输出的不是动作值,而是意图向量,这是一个固定大小的数字数组。

  • 意图向量携带关于在给定观察情况下理想动作的相关信息。

  • 在新闻推荐问题中,这样的意图向量可能意味着:“根据这些用户和上下文特征,该用户想阅读来自国际联赛、正在进行决赛的团队运动新闻。”

  • 然后,将这个意图向量与可用的动作进行比较。选择与“意图”最“接近”的动作。例如,一篇关于外国联赛团队运动的新闻,但不是正在进行决赛的新闻。

  • 相似度的衡量标准是余弦相似度,它是意图和动作嵌入的点积。

这个设置如图 16.2 所示:

图 16.2 – 使用动作嵌入进行新闻推荐

](https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/ms-rl-py/img/B14160_16_02.jpg)

图 16.2 – 使用动作嵌入进行新闻推荐

信息

这种使用嵌入的方法是 OpenAI 为了应对 Dota 2 中巨大的动作空间而引入的。一篇很好的博客解释了他们的方法,链接在这里:neuro.cs.ut.ee/the-use-of-embeddings-in-openai-five/。这种方法也可以很容易地在 RLlib 中实现,具体内容请见:bit.ly/2HQRkfx。

接下来,我们来讨论在新闻推荐问题中,奖励函数是什么样的。

奖励函数

在我们在书本开始部分解决的在线广告问题中,最大化点击率(CTR)是唯一目标。而在新闻推荐问题中,我们希望最大化与用户的长期互动,同时也提高即时点击的可能性。这就需要一个衡量用户活跃度的标准,论文中使用了生存模型。

使用对抗式乐队梯度下降进行探索

探索是强化学习(RL)的一个基本组成部分。当我们在模拟中训练 RL 智能体时,尽管我们不介意为了学习而采取不好的行动,只是这会浪费一些计算资源。但如果 RL 智能体是在真实环境中训练的,例如新闻推荐这种场景,那么探索的后果不仅仅是计算效率低下,它可能还会损害用户满意度。

如果你考虑常见的ε-greedy 方法,在探索过程中,它会从整个动作空间中随机选择一个动作,尽管我们知道有些动作其实是很糟糕的。例如,尽管智能体知道读者主要对政治感兴趣,它仍会随机显示一些关于美妆、体育、名人等的新闻,这些对读者来说可能完全没有关联。

克服这个问题的一种方法是逐步偏离贪心策略进行探索,并在智能体通过探索性行动获得好奖励时更新策略。以下是我们可以如何操作:

  • 除了常规的网络外,我们还使用探索网络来生成探索性动作。

  • 的权重,由表示,通过扰动的权重得到,表示。

  • 更正式地说,,其中是一个系数,用来控制探索与开发之间的权衡,而生成一个介于输入之间的随机数。

  • 为了获得一组展示给用户的新闻,我们首先从和生成两组推荐,每组一个,然后从两者中随机选择一些项来添加到展示集中。

  • 一旦展示集展示给用户,代理就会收集用户反馈(奖励)。如果由生成的项的反馈更好,则不变化。否则,权重会更新为,其中是某个步长。

最后,让我们讨论一下如何训练和部署该模型以获得有效的结果。

模型训练与部署

我们已经提到用户行为和偏好的动态特性。Zheng 等人(2018 年)通过全天频繁训练和更新模型来克服这一问题,从而使得模型能够捕捉到环境中的最新动态。

这结束了我们关于 RL 个性化应用的讨论,特别是在新闻推荐环境中的应用。接下来,我们转向一个相关领域——市场营销,它也可以通过个性化受益,并通过 RL 做得更多。

使用 RL 制定有效的市场营销策略

RL 可以显著改善多个领域的市场营销策略。现在我们来谈谈其中的一些。

个性化营销内容

与上一节相关,市场营销中总是有更多个性化的空间。与其向所有客户发送相同的电子邮件或传单,或进行粗略的客户细分,不如利用 RL 来帮助确定向客户传达个性化营销内容的最佳顺序。

客户获取的市场营销资源分配

市场营销部门通常根据主观判断和/或简单模型来决定如何分配预算。RL 实际上能够提出非常动态的政策来分配市场营销资源,同时利用有关产品的信息、来自不同营销渠道的反馈,以及季节性等上下文信息。

降低客户流失率

零售商长期以来一直研究预测模型,以识别即将流失的客户。在识别后,通常会向客户发送折扣券、促销商品等。但考虑到客户类型及先前行动的反应,采取这些行动的顺序还没有得到充分探讨。强化学习可以有效评估这些行动的价值,减少客户流失。

重新赢回流失的客户

如果你曾经订阅过《经济学人》杂志,然后做了取消订阅的错误决定,你可能收到了很多电话、邮件、通知等等。人们不禁会想,这种垃圾邮件式的做法真的是最佳方法吗?可能不是。而基于强化学习的方法,则可以帮助确定使用哪些渠道、何时使用,并通过最大化客户回流的机会,同时最小化这些努力的成本,来获得相应的好处。

这个列表还可以继续下去。我建议你花一两分钟思考一下,作为某公司的客户,你觉得哪些营销行为令人不安、无效或无关紧要,并思考强化学习如何能够帮助改进这些行为。

接下来,我们本章的最后一个领域:金融。

在金融中应用强化学习

如果我们需要重申强化学习的优势,那就是通过获取用于顺序决策的策略,在不确定性下最大化回报。金融领域岂不是与这种工具的最佳匹配?在金融中,以下几项是成立的:

  • 目标确实是最大化某种货币奖励。

  • 当前做出的决策必定会在未来产生影响。

  • 不确定性是一个决定性因素。

因此,强化学习在金融社区中变得越来越受欢迎。

为了明确,这一部分将不会包含任何成功的交易策略示例,嗯,显然有两个原因:首先,作者并不知晓任何成功策略;其次,即使知道,也不会在书中提及(也没有人会这么做)。此外,在金融中使用强化学习也存在一些挑战。因此,我们将从讨论这些挑战开始。一旦我们站稳脚跟,就会继续定义一些应用领域,并介绍一些可以在该领域使用的工具。

在金融中使用强化学习(RL)面临的挑战

当你第一次玩一款视频游戏时,花了多长时间才能获得不错的成绩?一个小时?两个小时?无论如何,这只是强化学习智能体所需经验的微不足道一部分,如果不是数亿次游戏帧的话,可能需要数百万次。因为强化学习智能体在这样的环境中获得的梯度噪声太大,导致我们所使用的算法无法快速学习。而且,毕竟视频游戏并不那么难。

你是否曾想过,成为股市成功交易员需要具备什么条件?多年金融经验,或许还有物理学或数学的博士学位?即便如此,交易员也很难击败市场表现。

这可能是一个过于冗长的声明,旨在让你相信通过股市交易赚钱很困难,原因如下:

  • 金融市场是高度高效的(尽管不是完美的),并且预测市场是非常困难的,甚至几乎不可能。换句话说,信号在金融数据中非常微弱,几乎完全是噪声。

  • 金融市场是动态的。今天有利可图的交易策略可能不会持续太久,因为其他人可能会发现相同的策略并进行交易。例如,如果人们知道比特币会在第一次日全食时交易到 10 万美元,那么价格现在就会跳到那个水平,因为人们不会等到那天才购买它。

  • 与视频游戏不同,金融市场是现实世界中的过程。因此,我们需要创建一个模拟环境,以便能够收集大量数据,训练一个比视频游戏环境更嘈杂的 RL 代理。记住,所有模拟都是不完美的,代理学到的内容很可能是模拟模型的独特性,而不是实际信号,这些内容在模拟之外不会有用。

  • 现实世界的数据足够大,可以轻松地检测到其中微弱的信号。

所以,这是一个冗长的、令人沮丧但必要的免责声明,我们需要告诉你,训练一个交易代理远不是轻而易举的事。话虽如此,RL 是一种工具,一种强大的工具,它的有效使用取决于用户的能力。

现在,是时候引发一些乐观情绪了。市面上有一些很酷的开源库,供你创建交易代理,而 TensorTrade 是其中之一。它可能对教育用途有所帮助,并且可以在进入更定制的工具之前,帮助你策划一些交易思路。

介绍 TensorTrade

TensorTrade 旨在轻松构建类似 Gym 的股市交易环境。它允许用户定义并连接各种数据流、观察特征提取、动作空间、奖励结构以及训练交易代理所需的其他便捷工具。由于环境遵循 Gym API,它可以轻松地与 RL 库(如 RLlib)连接。

在本节中,我们将简要介绍 TensorTrade,并将其详细用途留给文档进行说明。

信息

你可以在 www.tensortrade.org/ 找到 TensorTrade 的文档。

让我们从安装开始,然后将一些 TensorTrade 组件组合起来创建一个环境。

安装 TensorTrade

TensorTrade 可以通过以下简单的 pip 命令进行安装:

pip install tensortrade

如果你想创建一个虚拟环境来安装 TensorTrade,请别忘了也安装 Ray RLlib。

TensorTrade 的概念和组件

正如我们提到的,TensorTrade 环境可以由高度模块化的组件组成。让我们在这里搭建一个基本环境。

工具

金融中的工具是可以交易的资产。我们可以如下定义美元和TensorTrade Coin工具:

USD = Instrument("USD", 2, "U.S. Dollar")
TTC = Instrument("TTC", 8, "TensorTrade Coin")

前面的整数参数表示这些工具的数量精度。

流

流指的是一个传输数据的对象,例如来自市场模拟的价格数据。例如,我们可以创建一个简单的正弦波 USD-TTC 价格流,如下所示:

x = np.arange(0, 2*np.pi, 2*np.pi / 1000)
p = Stream.source(50*np.sin(3*x) + 100,
                  dtype="float").rename("USD-TTC")

交易所和数据流

接下来,我们需要创建一个交易所来交易这些工具。我们将刚刚创建的价格流放入交易所:

coinbase = Exchange("coinbase", service=execute_order)(p)

现在我们已经定义了价格流,我们还可以为其定义转换,并提取一些特征和指标。所有这些特征也将是流,它们将全部打包成数据流:

feed = DataFeed([
    p,
    p.rolling(window=10).mean().rename("fast"),
    p.rolling(window=50).mean().rename("medium"),
    p.rolling(window=100).mean().rename("slow"),
    p.log().diff().fillna(0).rename("lr")])

如果你在想最后一行在做什么,它是两个连续时间步价格的对数比,用于评估相对变化。

钱包和投资组合

现在,是时候为自己创造一些财富并把它放进我们的钱包了。随意对自己慷慨一些:

cash = Wallet(coinbase, 100000 * USD)
asset = Wallet(coinbase, 0 * TTC)
portfolio = Portfolio(USD, [cash, asset])

我们的钱包共同构成了我们的投资组合。

奖励方案

奖励方案就是我们希望用来激励代理的奖励函数。如果你在想“目标只有一个,赚取利润!”,其实还有更多内容。你可以使用类似风险调整回报的东西,或者定义你自己的目标。现在,让我们保持简单,使用利润作为奖励:

reward_scheme = default.rewards.SimpleProfit()

动作方案

动作方案定义了你希望代理能够执行的动作类型,例如简单的买入/卖出/持有所有资产(BSH),或者是分数买入/卖出等。我们还将现金和资产放入其中:

action_scheme = default.actions.BSH(cash=cash, asset=asset)

将它们全部放在一个环境中

最后,这些可以全部组合起来,形成一个具有一些附加参数的环境:

env = default.create(
    feed=feed,
    portfolio=portfolio,
    action_scheme=action_scheme,
    reward_scheme=reward_scheme,
    window_size=25,
    max_allowed_loss=0.6)

这就是在 TensorTrade 中创建环境的基本知识。你当然可以做得远不止这些,但你已经明白了大致的思路。完成这一步后,它可以轻松地插入 RLlib,或者几乎任何与 Gym API 兼容的库。

在 TensorTrade 中训练 RLlib 代理

正如你从前几章记得的那样,使用 Gym 中的自定义环境的一种方式是将它们放入一个返回环境的函数中并注册它们。所以,它看起来像这样:

def create_env(config):
    ...
    return env
register_env("TradingEnv", create_env)

环境名称可以在 RLlib 的训练配置中引用。你可以在 GitHub 仓库的Chapter16/tt_example.py中找到完整的代码。

信息

这个示例主要遵循 TensorTrade 文档中的内容。要获取更详细的 Ray/RLlib 教程,你可以访问www.tensortrade.org/en/latest/tutorials/ray.html。

这就是我们关于 TensorTrade 的讨论总结。现在你可以继续尝试一些交易想法。当你回来时,我们简要谈谈如何开发基于机器学习的交易策略,并结束这一章。

开发股票交易策略

几乎与市场本身一样嘈杂的是关于如何交易它们的信息。如何制定有效的交易策略远超本书的范围。然而,下面有一篇博客文章,可以让你对在为交易开发机器学习模型时需要注意的事项有一个现实的了解。像往常一样,使用你的判断力和尽职调查来决定相信什么:www.tradientblog.com/2019/11/lessons-learned-building-an-ml-trading-system-that-turned-5k-into-200k/。

这样,我们就可以结束本章的内容。

总结

在本章中,我们介绍了三个重要的强化学习(RL)应用领域:个性化、营销和金融。对于个性化和营销,本章不仅讨论了这些领域中常用的赌博算法应用,还讨论了多步骤强化学习的优点。我们还介绍了如对抗赌博梯度下降(dueling bandit gradient descent)等方法,这帮助我们实现了更保守的探索,以避免过度的奖励损失,以及行动嵌入(action embeddings),它有助于处理大型动作空间。最后,我们讨论了强化学习在金融中的应用及其面临的挑战,并介绍了 TensorTrade 库。

下一章是本书的最后一个应用章节,我们将重点讨论智能城市和网络安全。

参考文献

Zheng, G. et al. (2018). DRN: A Deep RL Framework for News Recommendation. WWW '18: 2018 年全球互联网大会论文集,2018 年 4 月,第 167–176 页, doi.org/10.1145/3178876.3185994。

第十七章:第十七章:智能城市与网络安全

智能城市预计将在未来几十年成为定义性体验之一。智能城市通过位于城市各个区域的传感器收集大量数据,如道路、公共设施基础设施和水资源。然后,这些数据用于做出基于数据和自动化的决策,如如何分配城市资源、实时管理交通以及识别和减轻基础设施问题。这个前景带来了两个挑战:如何编程自动化以及如何保护高度互联的城市资产免受网络攻击。幸运的是,强化学习(RL)可以帮助解决这两个问题。

在本章中,我们将探讨与智能城市和网络安全相关的三个问题,并描述如何将其建模为强化学习(RL)问题。在这个过程中,我们将向你介绍 Flow 库,这是一个将交通仿真软件与 RL 库连接的框架,并使用它解决一个交通信号灯控制问题。

具体来说,我们将在本章中解决以下问题:

  • 通过交通信号灯控制优化车辆流量

  • 为电网提供辅助服务

  • 检测智能电网中的网络攻击

这将是一次有趣的旅程,让我们开始吧!

通过交通信号灯控制优化车辆流量

智能城市的一个关键挑战是优化道路网络上的交通流量。减少交通拥堵有许多好处,如下所示:

  • 减少交通中浪费的时间和能源

  • 节省燃油和减少排放

  • 延长车辆和道路使用寿命

  • 降低事故发生率

在这个领域已经进行了大量研究;但最近,强化学习(RL)已成为与传统控制方法竞争的替代方案。因此,在本节中,我们将通过使用多智能体强化学习(RL)控制交通信号灯行为来优化道路网络上的交通流量。为此,我们将使用 Flow 框架,它是一个开源的强化学习(RL)库,并在现实交通微观仿真中进行实验。

介绍 Flow

交通研究在很大程度上依赖于仿真软件,如SUMO(城市交通模拟)和 Aimsun,用于交通信号灯控制、车辆路线选择、交通监控和交通预测等领域,这些都涉及到这些智能体的最优控制。另一方面,深度强化学习作为传统控制方法的替代方案的兴起,催生了许多库,如 RLlib 和 OpenAI Baselines。Flow 是一个开源框架,连接了交通仿真器和强化学习库这两个领域。

在本节中,与之前的章节一样,我们将使用 RLlib 作为强化学习后端。对于交通仿真,我们将使用 SUMO,这是一个强大的开源库,自 2000 年代初期以来一直在开发。

信息

在这里,我们将仅简单介绍如何将强化学习应用于交通问题。详细的文档和教程(我们在这里严格遵循的)可以在 Flow 网站上找到:flow-project.github.io/。SUMO 的文档和库可以在 www.eclipse.org/sumo/ 获得。

让我们从安装 Flow 和 SUMO 开始。

安装 Flow 和 SUMO

为了安装 Flow,我们需要创建一个新的虚拟环境,因为它依赖的库版本与我们在前面章节中使用的不同:

$ sudo apt-get install python3-pip 
$ virtualenv flowenv
$ source flowenv/bin/activate

要安装 Flow,我们需要下载仓库并运行以下命令:

$ git clone https://github.com/flow-project/flow.git
$ cd flow
$ pip3 install -e .
$ pip3 install ipykernel
$ python -m ipykernel install --user --name=flowenv

这些命令安装了必要的依赖项,包括 TensorFlow 和 RLlib。最后两个命令用于在 Jupyter Notebook 上运行 Flow,这也是 Flow 教程以及我们的示例代码所依赖的环境。要在 Ubuntu 18.04 上安装 SUMO,请使用以下命令:

$ scripts/setup_sumo_ubuntu1804.sh
$ source ~/.bashrc

针对早期 Ubuntu 版本和 macOS 的设置脚本也可以在同一文件夹中找到。

你可以通过运行以下命令来查看 Flow 和 SUMO 的实际操作(在 Flow 文件夹中,并且虚拟环境已激活):

$ python examples/simulate.py ring

应该会出现一个类似于图 17.1中所示的窗口:

图 17.1 – 一个示例的 SUMO 窗口,模拟环路上的交通情况

图 17.1 – 一个示例的 SUMO 窗口,模拟环路上的交通情况

如果在设置过程中遇到问题,Flow 文档可以帮助你进行故障排除。

现在我们已经设置好环境,接下来让我们深入了解如何使用 Flow 构建交通环境。

在 Flow 中创建实验

现在我们已经完成了设置,可以在 Flow 中创建环境和实验。接下来,我们将它们与 RLlib 连接,以训练 RL 代理。

一个 Flow 实验中有几个关键元素:

  • 一个道路网络,比如一个环形道路(如图 17.1所示),或类似曼哈顿的网格网络。Flow 提供了一套预定义的网络。对于高级用户,它还允许创建自定义网络。交通信号灯与道路网络一起定义。

  • 一个仿真后端,这不是我们关注的重点,我们将使用默认设置。

  • 一个 RL 环境,它配置了实验中控制、观察和奖励的内容,类似于 Gym 环境。

  • 车辆是整个实验的核心,它们的行为和特征是单独定义的。

所有这些组件都是参数化的,并且会单独传递给 Flow。然后,我们将它们打包在一起,创建一个 Flow 参数对象。

使用自下而上的方法可能会很困难,从每个组件的单独参数开始,来拼凑出整体的情况。此外,这些细节超出了本章的范围。相反,解包一个预构建的 Flow 参数对象会更容易。接下来我们就做这个。

分析 Flow 参数对象

Flow 定义了一些用于网格网络上交通信号灯优化的基准实验。请查看 Grid-0 实验的 Flow 参数对象:

Chapter17/Traffic Lights on a Grid Network.ipynb

from flow.benchmarks.grid0 import flow_params
flow_params
{'exp_tag': 'grid_0',
 'env_name': flow.envs.traffic_light_grid.TrafficLightGridBenchmarkEnv,
 'network': flow.networks.traffic_light_grid.TrafficLightGridNetwork,
 'simulator': 'traci',
 'sim': <flow.core.params.SumoParams at 0x7f25102d1350>,
 'env': <flow.core.params.EnvParams at 0x7f25102d1390>,
 'net': <flow.core.params.NetParams at 0x7f25102d13d0>,
 'veh': <flow.core.params.VehicleParams at 0x7f267c1c9650>,
 'initial': <flow.core.params.InitialConfig at 0x7f25102d6810>}

我们可以举个例子,检查网络参数中的内容:

print(dir(flow_params['net']))
flow_params['net'].additional_params
...
{'speed_limit': 35,
 'grid_array': {'short_length': 300,
  'inner_length': 300,
  'long_length': 100,
  'row_num': 3,
...

当然,理解它们如何工作的一个好方法是通过视觉化实验来进行:

from flow.core.experiment import Experiment
sim_params = flow_params['sim']
sim_params.render = True
exp = Experiment(flow_params)
results = exp.run(1)

这将弹出一个与图 17.2中类似的 SUMO 屏幕:

图 17.2 – 网格网络的 SUMO 渲染

图 17.2 – 网格网络的 SUMO 渲染

有了这些,我们现在有了一个正在运行的示例。在进入 RL 建模和训练之前,让我们讨论一下如何为这个实验获取基准奖励。

获取基准奖励

我们 GitHub 仓库中的 Jupyter notebook 包括一个从 Flow 代码库中提取的代码片段,用于获取该环境的基准奖励。它包含一些经过精心优化的交通信号灯阶段定义,平均奖励为 -204。我们将使用这个奖励来作为 RL 结果的基准。同样,随时可以修改阶段设置,看看它们对网络中交通模式的影响。

有了这些,我们现在准备好定义 RL 环境了。

建模交通信号灯控制问题

和往常一样,我们需要为 RL 问题定义动作、观察和奖励。我们将在接下来的章节中进行定义。

定义动作

我们希望为给定交叉口的所有信号灯训练一个单一控制器,如图 17.2中所示,例子b。在图中,信号灯处于绿-红-绿-红状态。我们定义一个二进制动作,0:继续,1:切换。当指示切换时,图中的信号灯状态将变为黄-红-黄-红,几秒钟后将变为红-绿-红-绿。

默认环境接受每个交叉口的连续动作,,并且按照之前描述的方法将其四舍五入以离散化。

单一智能体与多智能体建模

我们接下来的设计决策是,是否使用一个集中式智能体控制所有交通信号灯,或者采用多智能体方法。如果选择后者,无论是为所有交叉口训练一个单一策略,还是训练多个策略,我们需要注意以下几点:

  • 集中式方法的优势在于,理论上我们可以完美协调所有交叉口,从而获得更好的奖励。另一方面,训练好的智能体可能不容易应用于不同的道路网络。此外,对于更大的网络,集中式方法不容易扩展。

  • 如果我们决定使用多智能体方法,我们没有太多理由区分交叉口和它们使用的策略。因此,训练一个单一策略更有意义。

  • 为所有交叉口训练一个单一的通用策略,其中智能体(交叉口的信号灯)协同尝试最大化奖励,这是一个可扩展且高效的方法。当然,这缺乏集中的单智能体方法的完全协调能力。实际上,这是一个需要你评估的权衡。

因此,我们将采用多智能体设置,其中策略将使用来自所有智能体的数据进行训练。智能体将根据其本地观察从策略中获取动作。

有了这些,让我们定义观察。

定义观察

默认的多智能体网格环境使用以下内容作为观察:

  • ! 接近交叉口的车辆的速度

  • ! 接近交叉口的车辆的距离

  • 这些车辆所在道路边缘的 ID 是 ­

  • 每个本地边缘的交通密度、平均速度和交通方向

  • 当前信号灯是否处于黄色状态

有关更多详细信息,您可以查看 Flow 仓库中的flow.envs.multiagent.traffic_light_grid模块。

最后,让我们定义奖励。

定义奖励

环境对于给定时间步长有一个简单直观的成本定义,衡量的是与允许的最高速度相比,车辆的平均延迟:

这里,! 是总车辆数为 的车辆的速度,而 是允许的最高速度。然后,奖励可以定义为此成本项的负值。

现在我们已经有了所有的公式,是时候解决问题了。

使用 RLlib 解决交通控制问题

由于我们将使用 RLlib 的多智能体接口,我们需要做以下操作:

  1. 在 RLlib 中注册环境,并提供一个名称和环境创建函数。

  2. 定义我们将训练的策略的名称,我们只有一个策略,tlight。

  3. 定义一个生成 RLlib 训练器所需参数的函数,以便为策略提供支持。

  4. 定义一个函数,将智能体映射到策略,在我们的情况下这很简单,因为所有智能体都映射到相同的策略。

因此,这些可以通过以下代码实现:

create_env, env_name = make_create_env(params=flow_params, 
                                       version=0)
register_env(env_name, create_env)
test_env = create_env()
obs_space = test_env.observation_space
act_space = test_env.action_space
def gen_policy():
    return PPOTFPolicy, obs_space, act_space, {}
def policy_mapping_fn(_):
    return 'tlight'
policy_graphs = {'tlight': gen_policy()}
policies_to_train = ['tlight']

一旦定义完毕,我们需要将这些函数和列表传递给 RLlib 配置:

config['multiagent'].update({'policies': policy_graphs})
config['multiagent'].update({'policy_mapping_fn': 
                             policy_mapping_fn})
config['multiagent'].update({'policies_to_train': 
                             policies_to_train})

其余部分是常规的 RLlib 训练循环。我们使用在 Flow 基准测试中使用的超参数进行 PPO 训练。所有代码的完整内容可以在Chapter17/Traffic Lights on a Grid Network.ipynb中找到。

获取并观察结果

在经过几百万次训练步骤后,奖励收敛到约-243,略低于手工制作的基准。训练进度可以在 TensorBoard 中观察到:

图 17.3 – Flow 中多智能体交通信号环境的训练进度

图 17.3 – Flow 中多智能体交通信号灯环境的训练进度

你还可以通过在 Jupyter Notebook 中运行以下格式的命令,来可视化训练后智能体的表现:

!python /path/to/your/flow/visualize/visualizer_rllib.py /path/to/your/ray_results/your_exp_tag/your_run_id 100

这里,末尾的参数指的是检查点编号,该编号在训练过程中定期生成。

接下来,我们还将讨论为什么强化学习的表现稍微逊色于手工设计的策略。

进一步改进

有几个原因可能导致强化学习未能达到基准性能:

  • 缺乏更多的超参数调优和训练。这一因素始终存在。我们无法知道是否通过更多调整模型架构和训练可以提升性能,直到你尝试为止,我们鼓励你去尝试。

  • 基准策略对黄灯持续时间进行了更精细的控制,而强化学习模型则没有对这一点进行控制。

  • 基准策略协调网络中的所有交叉口,而每个强化学习智能体则做出局部决策。因此,我们可能会遇到去中心化控制的缺点。

  • 通过增加一些观察,可以缓解这一问题,这些观察将有助于智能体与邻居之间的协调。

  • 强化学习(RL)算法在优化的最后阶段,尤其是在达到奖励曲线的顶峰时,遇到困难并不罕见。这可能需要通过减少学习率和调整批量大小来精细控制训练过程。

因此,尽管仍有改进空间,我们的智能体已经成功学会如何控制交通信号灯,这比手动设计策略更具可扩展性。

在我们结束这个话题之前,来讨论一些资源,帮助更深入了解问题和我们使用的库。

进一步阅读

我们已经提供了 Flow 和 SUMO 文档的链接。Flow 库及其获得的基准测试在 Wu et al., 2019 和 Vinitsky et al., 2018 中有所解释。在这些资源中,你将发现其他你可以用各种强化学习库建模和解决的问题。

恭喜!我们在如此短的时间和篇幅内,成功利用强化学习解决了交通控制问题。接下来,我们将探讨另一个有趣的问题,那就是调节电力需求以稳定电网。

为电网提供辅助服务

在这一部分中,我们将描述强化学习如何通过管理家居和办公大楼中的智能设备,帮助将清洁能源资源集成到电网中。

电网操作与辅助服务

从发电机到消费者的电力传输与分配是一项庞大的操作,需持续监控和控制系统。特别是,发电与消耗必须在一个地区几乎保持平衡,以使电流频率保持在标准频率(美国为 60 Hz),防止停电和损坏。这是一个具有挑战性的任务,原因如下:

  • 电力供应在能源市场中提前规划,与该地区的发电机一起匹配需求。

  • 尽管有这些规划,未来的电力供应仍然不确定,尤其是当电力来源于可再生能源时。风能和太阳能的供给可能低于或超过预期,导致供给过剩或不足。

  • 未来的需求也不确定,因为消费者通常可以自由决定何时以及消费多少。

  • 电网故障,如发电机或输电线路出现问题,可能导致供需发生突然变化,从而危及系统的可靠性。

供需平衡由称为独立系统操作员(ISO)的主管部门维持。传统上,ISO 根据电网的变化要求发电机增加或减少供应,这是发电机为 ISO 提供的附加服务,ISO 会支付费用。然而,关于发电机提供这种服务存在一些问题:

  • 发电机通常对电网平衡的突变反应较慢。例如,可能需要数小时才能投入新的发电机组来应对电网中的供应不足。

  • 近年来,可再生能源供应显著增加,进一步加剧了电网的不稳定性。

因此,已有一条研究路线开始,旨在使消费者也能向电网提供这些附加服务。换句话说,目标是调节需求,而不仅仅是供应,以更好地维持平衡。这需要更复杂的控制机制,这就是我们引入强化学习(RL)来协助的地方。

在这段介绍之后,让我们更具体地定义这里的控制问题。

描述环境与决策问题

重申一下,我们的目标是动态地增加或减少一个区域内的总电力消费。首先,我们来描述在这种情境下的各方及其角色。

独立系统操作员

该地区的 ISO 持续监控供需平衡,并向该地区的所有附加服务提供商广播自动信号,要求他们调整需求。我们将这个信号称为 ,它只是一个在 范围内的数字。稍后我们会详细解释这个数字的确切含义。目前,我们只需要说明,ISO 每 4 秒更新一次这个信号(这是一种叫做调节服务的附加服务)。

智能建筑操作员

我们假设有一个智能建筑操作员(SBO),负责调节(一栋或多栋)建筑的总需求,以跟随 ISO 信号。SBO 将作为我们的强化学习(RL)代理,按照以下方式操作:

  • SBO 将调节服务出售给该区域的 ISO(独立系统运营商)。根据这一义务,SBO 承诺将消耗维持在 kW 的速率,并在 ISO 的要求下,上下调节最多 kW。我们假设和是为我们的问题预先设定的。

  • 当,SBO 需要快速将社区的消耗量降低到 kW。当,消耗速率需要提升至 kW。

  • 一般来说,SBO 需要控制消耗,以遵循一个 kW 的速率。

SBO 控制着一群智能家电/设备,例如供暖、通风和空调(HVAC)设备和电动 汽车(EVs),以遵循信号。在这里我们将利用强化学习(RL)。

我们在图 17.4中展示了这一设置:

图 17.4 – 智能建筑运营商提供调节服务

图 17.4 – 智能建筑运营商提供调节服务

接下来,让我们更详细地了解一下智能家电是如何运作的。

智能家电

你可能会对某些算法干预你的家电并导致它们开关感到不舒服。毕竟,谁会希望在看超级碗的时候,电视为了省电而自动关掉,或者仅仅因为外面的风力比预期强,导致半夜电视突然打开呢?这显然没有意义。另一方面,如果空调比平时晚一分钟或早一分钟启动,你可能就会觉得没问题。或者你并不介意你的电动汽车在早上 4 点或 5 点充满电,只要它能在你离家前准备好。总之,重点是一些家电在运行时间上有更多的灵活性,这在本案例中是我们关注的焦点。

我们还假设这些家电是智能的,并且具备以下能力:

  • 它们可以与 SBO 进行通信,以接收指令。

  • 它们可以评估“效用”,即衡量家电在某一时刻需要消耗电力的程度。

定义效用

让我们举两个例子,说明不同情况下效用的变化。考虑一个需要在早上 7 点之前充满电的电动汽车(EV):

  • 如果是早上 6 点,且电池电量仍然较低,那么效用就会很高。

  • 相反,如果离出发时间还很远和/或电池已经接近充满,那么效用就会很低。

同样地,当房间温度即将超过用户的舒适区时,空调的效用会很高;当房间温度接近底部时,效用则很低。

请参见图 17.5以了解这些情况的示意图:

图 17.5 – 不同条件下的效用水平(a)电动汽车和(b)空调

图 17.5 – 不同条件下电动汽车(a)和空调(b)的效用水平

接下来,让我们讨论为什么这是一个顺序决策问题。

定义顺序决策问题

到现在为止,你可能已经注意到,现在采取的 SBO 动作将会对以后产生影响。过早地为系统中的电动汽车(EV)充满电,可能会限制将来在需要时消费量的增加。相反,如果保持过多房间的室温过高,可能会导致所有空调在之后一起开启,以使房间温度恢复到正常水平。

在下一节中,让我们将其视为一个强化学习问题。

强化学习模型

和往常一样,我们需要定义动作、观察和奖励来创建一个强化学习模型。

定义动作

关于如何定义 SBO 控制,有不同的方法:

  • 首先,更明显的方法是通过观察电器的效用来直接控制系统中的每个电器。另一方面,这会使模型变得不灵活,且可能无法处理:每当添加新电器时,我们必须修改并重新训练智能体。此外,当电器数量众多时,动作和观察空间会变得过于庞大。

  • 另一种方法是为每个电器类别(空调、加热设备和电动汽车)在多智能体设置中训练一个策略。这将引入多智能体强化学习的固有复杂性。例如,我们必须设计一个机制来协调各个电器。

  • 一个折衷的方法是应用间接控制。在这种方法中,SBO 会广播其动作,让每个电器自行决定该怎么做。

让我们更详细地描述这种间接控制可能是什么样子的。

电器的间接控制

这里是我们如何定义间接控制/动作:

  • 假设有 种电器类型,如空调、电动汽车和冰箱。

  • 在任何给定的时刻,一个电器,,具有效用,,其最大值为 。

  • 在每个时间步,SBO 会广播一个动作,,针对每种电器类型,。因此,动作是 和

  • 每个电器在关闭时,会时不时地检查其类别的动作。这并不是在每个时间步都会发生,而是取决于电器的类型。例如,空调(AC)可能会比电动汽车(EV)更频繁地检查广播的动作。

  • 当电器,,类型为 ,检查动作 时,只有在 的情况下才会开启。因此,动作就像是电价。电器只有在其效用大于或等于电价时才会开启。

  • 一旦开启,电器会保持开启一段时间。然后,它会关闭并开始定期检查动作。

通过这种机制,SBO 能够间接影响需求。它对环境的控制不如直接控制或多代理控制精确,但相比之下复杂度大大降低。

接下来,让我们定义观察空间。

定义观察

SBO 可以利用以下观察来做出有根据的决策:

  • 时间时的 ISO 信号,,因为 SBO 有义务通过调整其需求来跟踪该信号。

  • 时间时,每种类型的开启家电数量,。为了简单起见,可以假设每种类型家电的电力消耗率为固定值,,。

  • 时间和日期特征,例如一天中的时间、一周中的天数、节假日日历等。

  • 辅助信息,如天气温度。

除了在每个时间步进行这些观察之外,还需要保持对观察的记忆。这是一个部分可观察的环境,其中家电的能量需求和电网状态对代理是隐藏的。因此,保持记忆将帮助代理揭示这些隐藏状态。

最后,让我们描述奖励函数。

定义奖励函数

在此模型中,奖励函数由两部分组成:跟踪成本和效用。

我们提到过,SBO 有义务跟踪 ISO 信号,因为它为此项服务获得报酬。

因此,我们为偏离信号所暗示的目标时间分配了惩罚:

这里,是目标,而是时间时的实际消费率。

奖励函数的第二部分是由家电实现的总效用。我们希望家电能够开启并消耗能源,但要在它们真正需要的时候进行。以下是这一点为何有益的一个例子:当空调将室内温度保持在舒适区的上限(图 17.3中的 76°F)时,它的能耗要低于当温度保持在下限而外部温度高于舒适区时。因此,在时间时实现的总效用如下:

这里,是那些在离散时间步内开启的家电集合。然后,RL 目标变为以下形式:

这里,是一个系数,用于控制效用和跟踪成本之间的权衡,是折扣因子。

终端条件

最后,我们来谈谈这个问题的终止条件。通常,这是一个没有自然终止状态的连续任务。然而,我们可以引入终止条件,例如,如果跟踪误差过大,则停止任务。除此之外,我们还可以将其转化为一个阶段性任务,将每个阶段的长度设定为一天。

就是这样!我们没有详细介绍该模型的具体实现,但你现在已经对如何处理这个问题有了清晰的思路。如果需要更多细节,可以查看本章末尾由 Bilgin 和 Caramanis 提供的参考文献。

最后但同样重要的是,让我们转变思路,建模电网中网络攻击的早期检测。

在智能电网中检测网络攻击

智慧城市在定义上依赖于资产之间的强大数字通信。尽管这一点带来了好处,但也使得智慧城市容易受到网络攻击。随着强化学习(RL)逐步应用于网络安全,本节将描述它如何应用于检测智能电网基础设施中的攻击。在本节中,我们将遵循Kurt et al., 2019提出的模型,具体细节请参见该论文。

让我们从描述电网环境开始。

电网中网络攻击的早期检测问题

电力网由称为总线的节点组成,这些节点对应于发电、需求或电力线路的交叉点。电网管理者从这些总线收集测量数据,以做出某些决策,例如调入额外的发电单元。为此,关键的测量量是每个总线的相位角(参考总线除外),这使得它成为网络攻击者的潜在目标,因此我们对其感兴趣:

  • 不出所料,来自仪表的测量数据是嘈杂的,且容易出错。

  • 对这些仪表及其测量结果进行网络攻击,可能会误导电网管理者的决策,并导致系统崩溃。

  • 因此,检测系统是否遭受攻击是非常重要的。

  • 然而,要区分噪声和由攻击引起的异常与真实系统变化并不容易。通常,等待并收集更多的测量数据有助于解决这个问题。

  • 另一方面,迟迟不宣布攻击可能会导致此期间做出错误的决策。因此,我们的目标是尽快识别这些攻击,但又不能产生过多的误报。

因此,我们的网络安全代理可以采取的可能行动集合很简单:宣布攻击或不宣布攻击。一个关于错误和真实(但延迟的)警报的示例时间线如图 17.6所示:

图 17.6 – (a) 错误警报和 (b) 真实但延迟的警报的示例时间线

图 17.6 – (a) 错误警报和 (b) 真实但延迟的警报的示例时间线

以下是任务生命周期和奖励的详细信息:

  • 一旦宣布攻击,任务终止。

  • 如果是误报,则会产生奖励。如果是实警,则奖励为 0。

  • 如果发生攻击但行动继续(且未声明攻击),则每个时间步会产生奖励!,。

  • 在所有其他时间步中,奖励为 0。

由此,智能体的目标是最小化以下代价函数(或最大化其负值):

这里,第一个项是误报的概率,第二个项是宣布攻击的预期(正向)延迟,是用于管理两者之间折衷的成本系数。

一个缺失的部分是观测值,我们将在接下来的内容中讨论。

电网状态的部分可观测性

系统的真实状态,即是否发生攻击,智能体无法直接观测到。相反,它收集相位角的测量值,。Kurt 等人,2019的一个关键贡献是以以下方式使用相位角的测量值:

  1. 使用卡尔曼滤波器根据先前的观测值预测真实的相位角。

  2. 基于此预测,估计预期的测量值,。

  3. 定义作为和之间差异的度量,接着成为智能体使用的观测值。

  4. 观察并保留过去观测的记忆供智能体使用。

本文使用表格化的 SARSA 方法通过离散化来解决此问题,并展示了该方法的有效性。一个有趣的扩展是使用深度强化学习方法,在没有离散化的情况下,适应不同的电网拓扑和攻击特征。

由此,我们结束了这一主题的讨论以及本章内容。做得好,我们已经完成了很多!让我们总结一下本章的内容。

总结

强化学习将在自动化领域发挥重要作用。智慧城市是利用强化学习力量的一个重要领域。在本章中,我们讨论了三个示例应用:交通信号控制、电力消耗设备提供辅助服务以及检测电网中的网络攻击。第一个问题使我们能够展示一个多智能体环境,我们使用了一种类似价格的间接控制机制来解决第二个问题,最后一个问题是部分观测环境中先进输入预处理的一个很好的例子。

在下一章也是最后一章,我们将总结本书内容,并讨论现实生活中强化学习的挑战及未来的方向。

参考文献

  • Wu, C., et al. (2019). 流动:一种面向交通自主性的模块化学习框架。ArXiv:1710.05465 [Cs]。arXiv.org,arxiv.org/abs/1710.05465

  • Vinitsky, E., Kreidieh, A., Flem, L.L., Kheterpal, N., Jang, K., Wu, C., Wu, F., Liaw, R., Liang, E., & Bayen, A.M. (2018). 混合自主交通中的强化学习基准. 第二届机器人学习会议论文集,PMLR 87:399-409,proceedings.mlr.press/v87/vinitsky18a.html

  • Bilgin, E., Caramanis, M. C., Paschalidis, I. C., & Cassandras, C. G. (2016). 智能建筑提供调节服务. IEEE 智能电网学报, 第 7 卷,第 3 期,1683-1693 页,DOI: 10.1109/TSG.2015.2501428

  • Bilgin, E., Caramanis, M. C., & Paschalidis, I. C. (2013). 智能建筑实时定价以提供负荷端调节服务储备. 第 52 届 IEEE 决策与控制会议,佛罗伦萨,4341-4348 页,DOI: 10.1109/CDC.2013.6760557

  • Caramanis, M., Paschalidis, I. C., Cassandras, C., Bilgin, E., & Ntakou, E. (2012). 通过灵活的分布式负荷提供调节服务储备. 第 51 届 IEEE 决策与控制会议(CDC),毛伊岛,HI,3694-3700 页,DOI: 10.1109/CDC.2012.6426025

  • Bilgin, E. (2014). 分布式负荷参与同时优化能源和备用的电力市场. 论文,波士顿大学

  • Kurt, M. N., Ogundijo, O., Li C., & Wang, X. (2019). 智能电网中的在线网络攻击检测:一种强化学习方法. IEEE 智能电网学报, 第 10 卷,第 5 期,5174-5185 页,DOI: 10.1109/TSG.2018.2878570

第十八章:第十八章:强化学习中的挑战与未来方向

在最后一章中,我们将总结我们在本书中的旅程:你已经做了很多,所以把它当作一次庆祝,也是对你成就的全景回顾。另一方面,当你将所学应用于解决实际问题时,你可能会遇到许多挑战。毕竟,深度 RL 仍然是一个快速发展的领域,正在取得大量进展以解决这些挑战。我们在书中已经提到了大部分挑战,并提出了解决方法。接下来,我们将简要回顾这些内容,并讨论 RL 的其他未来方向。最后,我们将介绍一些资源和策略,帮助你成为 RL 专家,你已经在这条道路上走得非常远。

那么,这一章我们将讨论的内容如下:

  • 你在本书中取得的成就

  • 挑战与未来方向

  • 对有志成为 RL 专家的建议

  • 结束语

你在本书中取得的成就

首先,恭喜你!你已经远远超越了基础,掌握了在现实世界中应用强化学习(RL)所需的技能和心态。至此,我们已经共同完成了以下内容:

  • 我们花费了相当多的时间讨论赌博问题,这些问题不仅在工业界有着广泛的应用,而且在学术界也作为辅助工具来解决其他问题。

  • 我们比一般的应用书籍更深入地探讨了理论,以加强你在 RL 方面的基础。

  • 我们涵盖了很多 RL 最成功应用背后的算法和架构。

  • 我们讨论了先进的训练策略,以最大化地发挥高级 RL 算法的效果。

  • 我们通过现实案例进行过实际操作。

  • 在整个过程中,我们不仅实现了我们各自版本的一些算法,还利用了如 Ray 和 RLlib 等库,这些库为顶尖科技公司中的许多团队和平台提供了 RL 应用的技术支持。

你完全值得花点时间庆祝你的成功!

现在,一旦你回过神来,是时候讨论一下你面前的道路了。RL 正处于崛起的初期阶段。这意味着多方面的含义:

  • 首先,这也是一个机会。通过做出这一投资并走到今天,你已经走在了前沿。

  • 其次,由于这是前沿技术,RL 在成为成熟、易用的技术之前,还面临着许多需要解决的挑战。

在接下来的章节中,我们将讨论这些挑战是什么。这样,当你遇到它们时,你就能识别出来,知道自己并不孤单,并且能够根据解决问题所需的资源(数据、时间、计算资源等)设定合理的期望。但你不必担心!RL 是一个非常活跃且快速发展的研究领域,因此我们应对这些挑战的工具和方法日渐丰富。看看 Katja Hofmann,这位著名的 RL 研究员,在 2019 年 NeurIPS 会议上整理并展示的关于 RL 在 NeurIPS 会议上提交的论文数量:

图 18.1 – RL 贡献者在 NeurIPS 会议上的数量,由 Katja Hofmann 整理并展示(来源:Hofmann,2019)

图 18.1 – RL 贡献者在 NeurIPS 会议上的数量,由 Katja Hofmann 整理并展示(来源:Hofmann,2019)

因此,在我们讨论这些挑战的同时,我们也会讨论相关的研究方向,这样你就能知道应该在哪里寻找答案。

挑战与未来方向

你可能会想,为什么在完成一本高级强化学习书籍后,我们又回到讨论 RL 挑战的问题。确实,在全书中,我们提出了许多缓解这些挑战的方法。另一方面,我们不能声称这些挑战已经被解决。因此,指出这些挑战并讨论每个挑战的未来发展方向是非常重要的,这样可以为你提供一把指南针,帮助你应对这些问题。让我们从其中一个最重要的挑战开始讨论:样本效率。

样本效率

正如你现在已经清楚地知道,训练一个强化学习(RL)模型需要大量的数据。OpenAI Five,这个在战略游戏《Dota 2》中成为世界级玩家的 AI,训练过程中使用了 128,000 个 CPU 和 GPU,持续了好几个月,每天收集相当于 900 年的游戏经验每一天(OpenAI,2018)。RL 算法的性能通常是在它们已经训练过超过 100 亿帧 Atari 游戏画面后进行基准测试的(Kapturowski,2019)。这无疑需要大量的计算和资源,仅仅是为了玩游戏。所以,样本效率是现实世界 RL 应用中面临的最大挑战之一。

让我们讨论一下缓解这个问题的总体方向。

样本高效算法

一个显而易见的方向是尝试创建更加样本高效的算法。实际上,研究界对此方向有很大的推动。我们将越来越多地比较算法,不仅仅是基于它们的最佳性能,还会评估它们在达到这些性能水平时的速度和效率。

为此,我们将可能越来越多地讨论以下几种算法类别:

  • 脱策略方法,不要求数据必须是在最新策略下收集的,这使它们在样本效率上相较于策略方法具有优势。

  • 基于模型的方法,通过利用它们对环境动态的了解,比起无模型方法能在效率上高出几个数量级。

  • 具有先验知识的模型,这些模型将假设空间限制为一个合理的集合。这类模型的例子包括使用神经常微分方程和拉格朗日神经网络的 RL 模型(Du, 2020;Shen, 2020)。

用于分布式训练的专用硬件和软件架构

我们可以预期,算法前沿的进展将是渐进且缓慢的。对于那些充满激情、迫不及待的爱好者来说,更快速的解决方案是向 RL 项目投入更多的计算资源,充分利用现有资源,训练越来越大的模型。因此,完全合理地预期,在自然语言处理(NLP)领域发生的事情也会发生在 RL 中:NLP 模型的规模从 80 亿参数增加到 170 亿参数,再到 1750 亿参数,OpenAI 的 GPT-3 在不到一年的时间内达到了这一规模,这要归功于训练架构的优化,当然还有专门用于此任务的超级计算机。

图 18.2 – 最大的 NLP 模型规模的增长。纵轴是参数的数量。图像修改自 Rosset, 2020

图 18.2 – 最大的 NLP 模型规模的增长。纵轴是参数的数量。图像修改自 Rosset, 2020

此外,RL 训练架构的创新,如 Ape-X 和 SEED RL 中的创新(Espeholt, 2019),帮助现有的 RL 算法运行得更加高效,这是一个我们可以期待看到更多进展的方向。

机器教学

机器教学方法,如课程学习、奖励塑形和示范学习,旨在将上下文和专业知识融入到 RL 训练中。这些方法通常在训练过程中显著提高样本效率,在某些情况下,它们是让学习变得更加可行所必需的。机器教学方法将在不久的将来变得越来越流行,以提高 RL 中的样本效率。

多任务/元学习/迁移学习

由于从头开始训练一个 RL 模型可能非常昂贵,因此复用在其他相关任务上训练过的模型才是明智的做法。例如,当我们想开发一个涉及图像分类的应用时,现在很少有情况下我们从头开始训练模型。相反,我们会使用一个预训练的模型,并根据我们的应用需求进行微调。

信息

ONNX 模型库是一个预训练的、先进的模型集合,采用开放标准格式,适用于图像分类和机器翻译等流行任务,我强烈建议你查看一下:github.com/onnx/models。

我们可以预期在 RL 任务中也会看到类似的资源库。在相关的背景下,像多任务学习(训练多个任务的模型)和元学习(训练能够高效转移到新任务的模型)等方法,将在 RL 研究人员和实践者中获得更多的关注和更广泛的应用。

RL 即服务

如果你需要在应用程序中以编程方式翻译文本,正如我们之前提到的,一种方法是使用预训练模型。但是,如果你不想投入精力去维护这些模型呢?答案通常是从微软、谷歌、亚马逊等公司购买该服务。这些公司拥有大量的数据和计算资源,并且他们通过尖端的机器学习方法不断升级自己的模型。对于其他没有相同资源和聚焦的企业来说,做到同样的事情可能是一个艰巨的任务,甚至是不可行的,或者根本就是不实际的。我们可以预见到,RL 即服务(RL-as-a-service)将成为行业中的一种趋势。

样本效率(sample efficiency)是一个难题,但在多个领域已经有了进展,正如我们所总结的那样。接下来,我们将讨论另一个主要挑战:对良好仿真模型的需求。

对高精度和快速仿真模型的需求

强化学习在工业中广泛应用的最大障碍之一是缺乏能够有效仿真公司希望优化的过程的模型,或者这些模型的逼真度不足以达到要求。创建这些仿真模型通常需要大量投资,在一些复杂任务中,仿真模型的逼真度可能不足以支撑典型的强化学习方法。为了克服这些挑战,我们可以预见到在以下几个领域会有更多的进展。

离线强化学习从数据中直接学习

尽管工业中的大多数过程没有仿真模型,但更常见的是会有日志记录描述过程中的发生事件。离线强化学习(Offline RL)方法旨在直接从数据中学习策略,随着强化学习逐渐进入现实世界应用,这种方法的重要性将变得越来越大。

处理泛化、部分可观测性和非平稳性的方法

即使在存在过程仿真模型的情况下,这种模型通常也很难达到足够的逼真度,无法直接训练一个可以在现实世界中工作的强化学习模型,而不需要额外的考虑。这个“仿真到现实”的差距可以看作是部分可观测性的一种形式,通常通过强化学习模型架构中的记忆机制来处理。结合领域随机化和正则化等泛化技术,我们已经看到了一些非常成功的应用场景,其中在仿真中训练的策略被转移到现实世界。处理非平稳性(non-stationarity)问题也与强化学习算法的泛化能力密切相关。

在线学习和边缘设备上的微调

使强化学习方法成功应用的一个重要能力是,在将模型部署到边缘设备后仍能继续进行训练。这样,我们将能够用实际数据对在仿真中训练的模型进行微调。此外,这一能力将帮助强化学习策略适应环境中变化的条件。

总结来说,我们将看到强化学习(RL)从视频游戏中的工具转变为传统控制和决策方法的替代方案,这一转变将通过去除对高保真模拟的依赖的方式得到促进。

高维动作空间

CartPole,作为 RL 的标志性测试平台,其动作空间只有少数几个元素,就像大多数用于 RL 研究的 RL 环境一样。然而,现实问题通常在可用动作数量上非常复杂,这也常常取决于智能体所处的状态。在这种现实场景中,像动作嵌入、动作屏蔽和动作淘汰等方法将帮助应对这一挑战。

奖励函数的保真度

制定正确的奖励函数,使智能体实现期望行为,是 RL 中公认的困难任务。逆向 RL 方法(通过示范学习奖励)和基于好奇心的学习(依赖内在奖励)是有前景的减少手工设计奖励函数依赖性的方法。

奖励函数工程的挑战在目标多样且具有定性时更加复杂。关于多目标 RL 方法的文献越来越多,这些方法要么分别处理每个子问题,要么为给定的目标组合生成策略。

关于 RL 环境中奖励信号的另一个挑战是奖励的延迟和稀疏性。例如,一个由 RL 智能体控制的营销策略可能会在行动后几天、几周甚至几个月才观察到奖励。解决因果关系和信用分配问题的 RL 方法在这些环境中至关重要,能够让 RL 在这些环境中得以有效应用。

这些都是值得关注的重要分支,因为现实世界中的 RL 问题很少具有明确、密集且标量的目标。

安全性、行为保证和可解释性

在计算机仿真中训练 RL 智能体时,尝试随机和疯狂的动作以找到更好的策略是完全可以接受的,事实上,这是必须的。对于与世界级选手在棋盘游戏中竞争的 RL 模型来说,最糟糕的情况就是输掉比赛,或许是有些尴尬。当 RL 智能体负责化学过程或自动驾驶汽车时,风险则是完全不同的类别。这些都是安全关键系统,容错空间几乎为零。事实上,这是 RL 方法相比于传统控制理论方法的一大劣势,后者通常伴随着理论保证和对预期行为的深刻理解。因此,约束 RL 和安全探索的研究对于在此类系统中使用 RL 至关重要。

即便系统并非安全至关重要的场景,例如在库存补充问题中,相关的挑战仍然是 RL 智能体所建议的行动的可解释性。在这些过程中,监督决策的专家往往需要解释,尤其是当建议的行动违背直觉时。人们往往会为了可解释性而牺牲准确性,这使得黑箱方法处于不利地位。深度学习在可解释性方面已经取得了长足的进展,而 RL 无疑会从中受益。另一方面,这将是机器学习面临的持续挑战。

再现性和对超参数选择的敏感性

对于一个特定任务,通过众多专家的密切监督和指导训练一个 RL 模型,经过多次迭代,这是一回事;而在多个生产环境中部署多个 RL 模型,并定期进行再训练,同时在新数据到来时不需人工干预,这又是另一回事。在各种条件下,RL 算法生成成功策略的一致性和韧性将成为评估这些算法时越来越重要的因素,既适用于研究社区,也适用于实际操作中需要处理这些模型及其维护的从业者。

鲁棒性与对抗性智能体

深度学习在其表示方面是脆弱的。这种缺乏鲁棒性使得对抗性智能体能够操控依赖深度学习的系统。这是一个重大问题,也是机器学习社区中非常活跃的研究领域。强化学习(RL)肯定会借助广泛的机器学习研究成果,解决该领域的鲁棒性问题。

这些 RL 挑战对于希望使用这些工具解决现实世界问题的从业者来说非常重要,且这一总结希望能够帮助到你。我们在书中覆盖了许多解决方案方法,并在本节中也提到了整体的方向,所以你知道该从哪里寻找解决方案。所有这些都是活跃的研究领域,因此,每当你遇到这些挑战时,回顾文献是一个很好的做法。

在结束之前,我想对有志成为 RL 专家的人提供一些个人看法。

对有志成为 RL 专家的建议

本书面向的读者群体是那些已经掌握了强化学习基础的人。既然你已经读完了这本书,你已经具备了成为 RL 专家的基础条件。话虽如此,RL 是一个庞大的话题,本书的真正目的是为你提供一份指南和启动器。如果你决定深入研究,成为一名 RL 专家,我有一些建议。

深入了解理论

在机器学习中,模型往往无法在一开始就产生预期的结果。有一个重要因素可以帮助你克服这些障碍,那就是为你用来解决问题的算法背后的数学打下坚实的基础。这将帮助你更好地理解这些算法的局限性和假设,并能识别它们是否与当前问题的实际情况冲突。为此,以下是一些建议:

  • 深入理解概率论和统计学永远不是坏主意。别忘了,所有这些算法本质上都是统计模型。

  • 对强化学习的基本概念有扎实的理解,如 Q-learning 和贝尔曼方程,对于构建现代强化学习是至关重要的。本书在某种程度上服务于这个目的。然而,我强烈推荐你多次阅读 Rich Sutton 和 Andrew Barto 的书籍 Reinforcement Learning: An Introduction,这本书基本上是传统强化学习的圣经。

  • 赛尔吉·莱文教授的 UC Berkeley 深度强化学习课程,是本书在此领域受益匪浅的一个重要资源。这个课程可以通过 rail.eecs.berkeley.edu/deeprlcourse/ 访问。

  • 另一个很棒的资源,专门涉及多任务学习和元学习,是切尔西·芬恩教授的斯坦福课程,网址是 cs330.stanford.edu/。

  • 由该领域专家教授的 Deep RL Bootcamp 是另一个优秀的资源:sites.google.com/view/deep-rl-bootcamp/home。

当你浏览这些资源,并不时参考它们时,你会发现自己对该主题的理解变得更加深入。

关注优秀的从业者和研究实验室

有一些优秀的研究实验室专注于强化学习(RL),并且他们也通过详细的博客文章发布他们的研究成果,这些文章包含了很多理论和实践的见解。以下是一些例子:

即使你不阅读每一篇帖子,定期查看它们也是个好主意,这样可以保持与强化学习研究趋势同步。

从论文和它们的优秀解释中学习

在人工智能(AI)研究中,一年就像是狗年:发生了很多事。所以,保持最新的最好的方式就是关注该领域的研究。这也会让你接触到方法的理论和严格的解释。现在,这样做有两个挑战:

  • 每年发布的论文数量庞大,几乎不可能阅读所有论文。

  • 阅读公式和证明可能会让人感到望而生畏。

为了解决前者,我建议你关注像 NeurIPS、ICLR、ICML 和 AAAI 等会议上接收的论文。这仍然会是一大堆论文,因此你需要制定自己的筛选标准,决定哪些论文值得阅读。

为了解决后者,你可以查看是否有优秀的博客文章解释你想更好理解的论文。以下是一些高质量的博客(不限于 RL):

我个人从这些博客中学到了很多,并且仍然在持续学习。

关注深度学习其他领域的趋势

深度学习的许多重大进展,如 Transformer 架构,仅需几个月时间就能进入强化学习(RL)领域。因此,保持对广泛的机器学习和深度学习研究中的主要趋势的关注,将帮助你预测 RL 领域即将到来的发展。我们在前一部分列出的博客是跟踪这些趋势的好方法。

阅读开源代码库

到目前为止,强化学习中的算法实在是太多了,无法在书中逐行解释。因此,你需要在某个阶段培养这种素养,直接阅读这些算法的优质实现。以下是我的建议:

除了这些,许多论文现在也有自己的代码库,就像我们在本书的某些章节中使用过的那样。有一个非常好的网站,paperswithcode.com/,你可以用来查找这类论文。

实践!

无论你读了多少内容,真正的学习只有通过实践才能实现。所以,尽可能亲自动手去做。你可以通过复现强化学习论文和算法,或者更好的是,进行自己的强化学习项目。深入理解实现的内部细节所带来的益处是任何其他方式都无法替代的。

我希望这套资源对你有帮助。需要明确的是,这些内容量很大,消化这些信息需要时间,因此请设定实际的目标。此外,你还需要在阅读和跟随内容时有所选择,这个习惯会随着时间的推移而逐步养成。

最后的话

好了,是时候总结了。感谢你投入时间和精力阅读这本书。希望它对你有所帮助。最后我想强调的是,掌握一项技能需要很长时间,而且你能达到的水平是没有限制的。没有人是所有领域的专家,即使是强化学习或计算机视觉这样的小领域也是如此。你需要不断实践。无论你的目标是什么,持续性和一致性将决定你的成功。祝你在这条路上一路顺利。

参考文献

posted @ 2025-07-12 11:41  绝不原创的飞龙  阅读(376)  评论(0)    收藏  举报