Python-游戏人工智能实用指南-全-
Python 游戏人工智能实用指南(全)
原文:
zh.annas-archive.org/md5/0fbda7efffcdd3e7baf00f61988a1cc0译者:飞龙
前言
本书是学习各种强化学习(RL)技术和算法如何在使用Python进行游戏开发中发挥重要作用的一站式商店。
本书将从基础知识开始,为你提供理解强化学习在游戏开发中扮演重要角色的必要基础。每一章都将帮助你实现各种强化学习技术,如马尔可夫决策过程、Q学习、演员-评论家方法、状态-动作-奖励-状态-动作(SARSA)和确定性策略梯度算法,以构建逻辑自学习智能体。你将使用这些技术来提高你的游戏开发技能,并添加各种功能以提高整体生产力。本书的后期,你将学习如何使用深度强化学习技术来制定策略,使智能体能够从自己的行动中学习,从而构建有趣且引人入胜的游戏。
到本书结束时,你将能够使用强化学习技术来构建各种项目,并为开源应用做出贡献。
本书面向的对象
本书面向希望通过实现强化学习技术从头开始构建游戏来增加知识的游戏开发者。本书也将吸引机器学习和深度学习从业者,以及希望了解自我学习智能体如何在游戏领域应用的强化学习研究者。本书假定读者具备游戏开发的基础知识以及Python编程的实际操作能力。
本书涵盖的内容
第一章,理解基于奖励的学习,探讨了学习的基础,学习的本质,以及强化学习与其他更经典的学习方法的不同之处。从那里,我们探讨了马尔可夫决策过程在代码中的工作原理以及它与学习的关系。这引导我们进入经典的带臂机和上下文赌博机问题。最后,我们将了解Q学习和基于质量的模型学习。
第二章,动态规划和贝尔曼方程,深入探讨了动态规划,并研究了贝尔曼方程如何与强化学习交织在一起。在这里,你将学习贝尔曼方程是如何用来更新策略的。然后,我们进一步详细介绍了策略迭代或价值迭代方法,通过我们对Q学习的理解,通过在一个新的网格式环境中训练智能体来实现。
第三章,蒙特卡洛方法,探讨了基于模型的方法以及它们如何被用来训练智能体在更经典的棋盘游戏中。
第四章,时间差分学习,探讨了强化学习的核心以及它如何解决学术界经常讨论的时间信用分配问题。我们将时间差分学习(TDL)应用于Q学习,并使用它来解决网格世界环境(如FrozenLake)。
第五章,探索SARSA,深入探讨了在线策略方法如SARSA的基本原理。我们将通过理解部分可观察马尔可夫决策过程来探索基于策略的学习。然后,我们将探讨如何使用Q-learning实现SARSA。这将为我们在后续章节中探讨的更高级策略方法奠定基础,称为PPO和TRPO。
第六章,深入DQN,将Q学习模型与深度学习相结合,创建了称为深度Q学习网络(DQNs)的高级智能体。从这一点出发,我们解释了基本的深度学习模型是如何用于回归的,或者在这种情况下,用于解决Q方程。我们将在CartPole环境中使用DQNs。
第七章,深入DDQNs,探讨了深度学习扩展,称为卷积神经网络(CNNs),如何用于观察视觉状态。然后,我们将使用这些知识来玩Atari游戏,并探讨进一步的增强。
第八章,策略梯度方法,深入探讨了更高级的策略方法以及它们如何集成到深度强化学习智能体中。这是一个高级章节,因为它涵盖了更高级的微积分和概率概念。你将在本章中获得MuJoCo动画强化学习环境作为你辛勤工作的回报。
第九章,优化连续控制,探讨了如何改进之前用于连续控制高级环境的策略方法。我们首先设置并安装MuJoCo环境。之后,我们研究了一种新颖的改进方法,称为循环网络,用于捕捉上下文,并了解循环网络是如何在PPO之上应用的。然后,我们回到演员-评论家方法,这次在几种不同的配置下研究异步演员-评论家,最后进展到带有经验回放的演员-评论家。
第十章,彩虹DQN的全部内容,告诉我们所有关于Rainbow的信息。Google DeepMind最近在一种称为Rainbow的算法中探索了将多个强化学习增强功能结合在一起。Rainbow是另一个你可以探索的工具包,你可以从中借用或使用它来与更高级的强化学习环境一起工作。
第十一章,利用ML-Agents,探讨了如何在我们自己的智能体中使用ML-Agents工具包的元素,或者使用工具包来获得一个完全开发的智能体。
第十二章,DRL框架,开启了在多种环境中与单独智能体玩耍的可能性。我们还将探索各种多智能体环境。
第13章,3D世界,训练我们有效地使用RL代理来应对各种3D环境挑战。
第14章,从DRL到AGI,超越了DRL,进入了AGI的领域,或者至少是我们希望AGI能去往的地方。我们还将探讨各种可以在现实世界中应用的DRL算法。
为了充分利用本书
掌握Python和游戏开发的基本知识是必要的。拥有一台配备GPU的好PC将很有帮助。
下载示例代码文件
您可以从www.packt.com上的账户下载本书的示例代码文件。如果您在其他地方购买了此书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。
您可以通过以下步骤下载代码文件:
-
在www.packt.com上登录或注册。
-
选择“支持”选项卡。
-
点击“代码下载”。
-
在搜索框中输入书籍名称,并遵循屏幕上的说明。
下载文件后,请确保使用最新版本的以下软件解压缩或提取文件夹:
-
Windows上的WinRAR/7-Zip
-
Zipeg/iZip/UnRarX for Mac
-
7-Zip/PeaZip for Linux
本书代码包也托管在GitHub上,网址为https://github.com/PacktPublishing/Hands-On-Reinforcement-Learning-for-Games。如果代码有更新,它将在现有的GitHub仓库中更新。
我们还有其他来自我们丰富的图书和视频目录的代码包可供下载,网址为https://github.com/PacktPublishing/。查看它们!
下载彩色图像
我们还提供了一份包含本书中使用的截图/图表彩色图像的PDF文件。您可以从这里下载:http://www.packtpub.com/sites/default/files/downloads/9781839214936_ColorImages.pdf。
使用的约定
本书使用了多种文本约定。
CodeInText:表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟URL、用户输入和Twitter昵称。以下是一个示例:“三个函数make_atari、wrap_deepmind和wrap_pytorch都位于我们之前导入的wrappers.py新文件中。”
代码块是这样设置的:
env_id = 'PongNoFrameskip-v4'
env = make_atari(env_id)
env = wrap_deepmind(env)
env = wrap_pytorch(env)
当我们希望您注意代码块中的特定部分时,相关的行或项目将以粗体显示:
epsilon_start = 1.0
epsilon_final = 0.01
epsilon_decay = 30000
epsilon_by_episode = lambda episode: epsilon_final + (epsilon_start - epsilon_final) * math.exp(-1\. * episode / epsilon_decay)
plt.plot([epsilon_by_episode(i) for i in range(1000000)])
plt.show()
任何命令行输入或输出都应如下编写:
pip install mujoco
粗体:表示新术语、重要单词或屏幕上看到的单词。例如,菜单或对话框中的单词在文本中显示如下。以下是一个示例:“在此基础上,我们将探讨DQN的一个变体,称为DDQN,或双(对抗)DQN。”
警告或重要提示看起来是这样的。
小贴士和技巧看起来是这样的。
联系我们
我们始终欢迎读者的反馈。
一般反馈:如果您对本书的任何方面有疑问,请在邮件主题中提及书名,并给我们发送邮件至 customercare@packtpub.com。
勘误:尽管我们已经尽一切努力确保内容的准确性,但错误仍然可能发生。如果您在这本书中发现了错误,我们将不胜感激,如果您能向我们报告这一错误。请访问 www.packtpub.com/support/errata,选择您的书籍,点击勘误提交表单链接,并输入详细信息。
盗版:如果您在互联网上发现我们作品的任何非法副本,我们将不胜感激,如果您能提供位置地址或网站名称。请通过 copyright@packt.com 联系我们,并提供材料的链接。
如果您想成为一名作者:如果您在某个领域有专业知识,并且对撰写或参与一本书籍感兴趣,请访问 authors.packtpub.com。
评论
请留下评论。一旦您阅读并使用了这本书,为何不在您购买它的网站上留下评论?潜在读者可以查看并使用您的客观意见来做出购买决定,我们Packt可以了解您对我们产品的看法,我们的作者也可以看到他们对书籍的反馈。谢谢!
如需更多关于Packt的信息,请访问 packt.com。
第一章:第1节:探索环境
强化学习(RL)是一个复杂的话题,它包含了似乎混合在一起的术语和概念。在本节中,我们为初学者或更高级的用户揭示RL的术语和基础知识。
本节包含以下章节:
第二章:理解基于奖励的学习
世界正被机器学习革命所吞噬,特别是对功能性通用人工智能或AGI的寻找。不要与有意识的AI混淆,AGI是对机器智能的更广泛定义,它寻求将通用学习方法应用于广泛的任务,就像我们用大脑的能力一样——甚至小老鼠也有这样的能力。基于奖励的学习,特别是强化学习(RL),被视为迈向更通用智能的下一步。
"短期AGI是一个严肃的可能性。"
– OpenAI联合创始人兼首席科学家,伊利亚·苏茨克维
在本书中,我们从基于奖励的学习和强化学习的历史开始,探讨其从现代起源到在游戏和模拟中的应用。强化学习,尤其是深度强化学习,在研究和应用中都越来越受欢迎。仅仅几年时间,强化学习的发展就非常显著,这使得它既令人印象深刻,同时,也难以跟上并理解。通过本书,我们将详细揭示困扰这个多分支和复杂主题的抽象术语。到本书结束时,你应该能够认为自己是一个自信的强化学习和深度强化学习实践者。
在本章的第一部分,我们将从强化学习的概述开始,探讨术语、历史和基本概念。在本章中,我们将涵盖以下高级主题:
-
理解基于奖励的学习
-
介绍马尔可夫决策过程
-
使用多臂赌博机的价值学习
-
探索带有上下文赌博机的Q学习
在进入下一节之前,我们想提及一些重要的技术要求。
技术要求
本书是一本实践性很强的书,这意味着有很多代码示例供你亲自操作和探索。本书的代码可以在以下GitHub仓库中找到:https://github.com/PacktPublishing/Hands-On-Reinforcement-Learning-for-Games.
因此,请确保你已经设置了一个可工作的Python编码环境。Anaconda,这是一个跨平台的Python和R的包装框架,是本书推荐的平台。我们还推荐使用Visual Studio Code或带有Python工具的Visual Studio Professional作为好的集成开发环境或IDE。
本书推荐使用Anaconda,可以从https://www.anaconda.com/distribution/下载。
在这个问题解决之后,我们可以继续学习强化学习(RL)的基础知识,在下一节中,我们将探讨基于奖励的学习为何有效。
理解基于奖励的学习
机器学习正在迅速成为一个广泛且不断发展的类别,涵盖了多种学习系统。我们根据问题的形式以及我们如何为机器处理它进行分类。在监督机器学习的案例中,数据在输入机器之前首先被标记。这类学习的例子包括简单的图像分类系统,这些系统被训练从预先标记的猫和狗图像集中识别猫或狗。监督学习是最受欢迎且直观的学习系统类型。其他越来越强大的学习形式是无监督学习和半监督学习。这两种方法都消除了对标签的需求,或者在半监督学习的案例中,需要更抽象地定义标签。以下图示展示了这些学习方法和它们如何处理数据:

监督学习的变体
一些最近在 arXiv.org(发音为archive.org)上的论文建议使用半监督学习来解决强化学习任务。虽然论文建议不使用外部奖励,但它们确实谈到了内部更新或反馈信号。这表明了一种使用内部奖励强化学习的方法,正如我们之前提到的,这是一个存在的事物。
尽管这个监督学习方法的家族在过去几年中取得了令人印象深刻的进步,但它们仍然缺乏我们从真正智能机器中期望的必要规划和智能。这就是强化学习发挥作用并区别于其他方法的地方。强化学习系统通过在与代理所在的环境中进行交互和选择来学习。一个典型的强化学习系统图示如下:

一个强化学习系统
在前面的图中,您可以识别出强化学习系统的主要组成部分:代理和环境,其中代理代表强化学习系统,而环境可能是游戏板、游戏屏幕,以及/或可能是流数据。连接这些组件的是三个主要信号:状态、奖励和动作。状态信号基本上是环境当前状态的快照。奖励信号可能由环境外部提供,并为代理提供反馈,无论是好是坏。最后,动作信号是代理在每个时间步在环境中选择的动作。一个动作可能像跳跃一样简单,或者是一组更复杂的控制伺服机构。无论如何,强化学习中的另一个关键区别是代理能够与环境交互并改变它。
现在,如果这一切仍然显得有些混乱,请不要担心——早期研究人员经常在区分监督学习和强化学习之间遇到麻烦。
在下一节中,我们将探讨更多的强化学习术语,并探讨强化学习代理的基本要素。
强化学习的要素
每个强化学习代理都由四个主要元素组成。这些是策略、奖励函数、价值函数,以及可选的模型。现在让我们更详细地探讨这些术语的含义:
-
策略:策略代表了代理的决策和规划过程。策略决定了代理在每一步将采取哪些动作。
-
奖励函数:奖励函数决定了代理在完成一系列动作或单个动作后所获得的奖励量。通常,奖励是由外部给予代理的,但正如我们将看到的,也存在内部奖励系统。
-
价值函数:价值函数决定了长期状态下状态的值。确定状态的值是强化学习的基础,我们的第一个练习将是确定状态值。
-
模型:模型代表了整个环境。在井字棋游戏中,这可能代表所有可能的游戏状态。对于更高级的强化学习算法,我们使用部分可观察状态的概念,这允许我们摆脱环境的完整模型。在这本书中我们将要解决的问题中,有些环境的状态数量比宇宙中的原子数量还要多。是的,您没有看错。在如此庞大的环境中,我们永远无法希望对整个环境状态进行建模。
我们将在接下来的几章中详细探讨这些术语,所以如果感觉有些抽象,请不要担心。在下一节中,我们将回顾强化学习的历史。
强化学习的历史
Sutton和Barto(1998年)的《强化学习导论》讨论了现代强化学习的起源,它源自两个主要线索,后来又加入了一个线索。这两个主要线索是基于试错的学习和动态规划,第三个线索以时间差分学习的形式在后来出现。Sutton创立的主要线索,即试错,基于动物心理学。至于其他方法,我们将在各自的章节中更详细地探讨。下面是一个展示这三个线索如何汇聚形成现代强化学习的图表:

现代强化学习的历史
Dr. Richard S. Sutton,DeepMind的杰出研究科学家,同时也是阿尔伯塔大学的著名教授,被认为是现代强化学习(RL)之父。
最后,在我们深入探讨强化学习之前,让我们在下一节中看看为什么使用这种学习形式与游戏相结合是有意义的。
为什么是游戏中的强化学习?
在游戏领域,已经使用了各种形式的机器学习系统,其中监督学习是首选。虽然这些方法可以表现得像智能系统,但它们仍然受限于处理标记或分类的数据。尽管生成对抗网络(GANs)在关卡和其他资产生成方面显示出特别的潜力,但这些算法家族无法规划和理解长期决策。现在,在游戏中复制规划和交互行为的AI系统通常是通过硬编码的状态机系统,如有限状态机或行为树来实现的。能够开发出能够自己学习最佳移动或行为的智能体,这在实际上可以说是改变游戏规则,不仅对游戏产业如此,而且这肯定会在全球的每个产业中引起反响。
在下一节中,我们将探讨强化学习系统的基础,即马尔可夫决策过程。
介绍马尔可夫决策过程
在强化学习中,智能体通过解释状态信号从环境中学习。环境的状态信号需要定义当时环境的离散切片。例如,如果我们的智能体正在控制一枚火箭,每个状态信号都会定义火箭在时间上的确切位置。在这种情况下,状态可能由火箭的位置和速度定义。我们将从环境中定义的这个状态信号称为马尔可夫状态。马尔可夫状态不足以做出决策,智能体需要理解先前状态、可能的行为以及任何未来的奖励。所有这些附加属性可能汇聚形成一个马尔可夫性质,我们将在下一节中进一步讨论。
马尔可夫性质和马尔可夫决策过程
如果所有马尔可夫信号/状态都预测未来状态,那么一个强化学习问题就满足了马尔可夫性质。随后,如果一个马尔可夫信号或状态能够使智能体从该状态预测值,那么它就被认为是具有马尔可夫性质的。同样,如果一个既是马尔可夫性质又是有限的学习任务,那么它被称为有限马尔可夫决策过程或MDP。这里展示了一个非常经典的MDP例子,经常用来解释强化学习:

马尔可夫决策过程(Dr. David Silver)
上述图表取自YouTube上David Silver博士的优秀在线讲座(https://www.youtube.com/watch?v=2pWv7GOvuf0)。Silver博士是Sutton博士的前学生,后来因成为DeepMind在强化学习(RL)早期成就背后的智慧大脑而声名鹊起。
该图是一个有限离散MDP的例子,用于一个试图优化其动作以获得最大奖励的大学后学生。学生可以选择上课、去健身房、在Instagram上闲逛,或者做其他事情,通过考试和/或睡觉。状态用圆圈表示,文本定义了活动。此外,每个圆圈旁边数字表示使用该路径的概率。注意,围绕单个圆圈的所有值总和为1.0或100%的概率。R=表示当学生在该状态下时,奖励函数的奖励或输出。为了进一步巩固这个抽象概念,让我们在下一节构建我们自己的MDP。
构建一个MDP
在这个动手练习中,我们将使用你日常生活中或经验中的任务来构建一个马尔可夫决策过程(MDP)。这应该能让你更好地将这个抽象概念应用到更具体的事物上。让我们开始吧:
-
想想你每天做的可能包含六个左右状态的任务。这类任务的例子可能包括上学、穿衣、吃饭、洗澡、浏览Facebook和旅行。
-
在一张完整的纸张上或在某个数字绘图应用程序中,将每个状态写在圆圈内。
-
使用你认为最合适的动作连接各个状态。例如,不要在洗澡之前穿衣。
-
分配你将用于采取每个动作的概率。例如,如果你有两个离开当前状态的动作,你可以使它们都是50/50或0.5/0.5,或者任何其他总和为1.0的组合。
-
分配奖励。决定你在每个状态下会得到什么奖励,并在你的图上标记出来。
-
将你完成的图与前面的例子进行比较。你做得怎么样?
在我们解决你的MDP或其他MDP之前,我们首先需要了解一些关于计算值的基础知识。我们将在下一节中揭示这一点。
使用多臂老丨虎丨机进行价值学习
首先解决一个完整的MDP,从而解决完整的强化学习(RL)问题,需要我们了解值以及如何使用价值函数计算状态的价值。回想一下,价值函数是RL系统的一个主要元素。我们不是使用完整的MDP来解释这一点,而是依赖于一个更简单的单状态问题,即多臂老丨虎丨机问题。这个名字来源于那些被称为“强盗”的顾客通常提到的一臂老丨虎丨机,但在这个案例中,机器有多个臂。也就是说,我们现在考虑一个单状态或稳态问题,有多个动作导致终端状态提供恒定的奖励。更简单地说,我们的代理将玩一个多臂老丨虎丨机,根据拉动的臂提供胜利或失败,每个臂总是返回相同的奖励。这里展示了我们的代理玩这个机器的例子:

多臂老丨虎丨机的代理演示
我们可以考虑单个状态的值依赖于下一个动作,前提是我们还了解该动作提供的奖励。数学上,我们可以定义一个简单的学习价值方程如下:

在这个方程中,我们有以下内容:
-
V(a):给定动作的值
-
a:动作 -
α:alpha 或学习率
-
r:奖励
注意新增了一个名为 α(alpha)或学习率的变量。这个学习率表示代理需要多快从拉动到拉动学习价值。学习率越小(0.1),代理的学习速度越慢。这种动作价值学习方法对于强化学习(RL)是基本的。让我们在下一节中通过编写这个简单的示例来进一步巩固。
编写价值学习器
由于这是我们第一个示例,请确保您的 Python 环境已经设置好。为了简单起见,我们更喜欢使用 Anaconda。请确保您对所选 IDE 的编码感到舒适,并打开代码示例 Chapter_1_1.py,并跟随操作:
- 让我们检查代码的第一个部分,如下所示:
import random
reward = [1.0, 0.5, 0.2, 0.5, 0.6, 0.1, -.5]
arms = len(reward)
episodes = 100
learning_rate = .1
Value = [0.0] * arms
print(Value)
-
我们首先开始通过
import导入random。我们将在每个训练轮次中使用random随机选择一个臂。 -
接下来,我们定义一个奖励列表,
reward。这个列表定义了每个臂(动作)的奖励,从而定义了在老丨虎丨机上臂/动作的数量。 -
接着,我们使用
len()函数确定臂的数量。 -
然后,我们设置代理将使用的训练轮数来评估每个臂的价值。
-
将
learning_rate值设置为.1。这意味着代理将缓慢地学习每个拉动的价值。 -
然后,我们使用以下代码在一个名为
Value的列表中初始化每个动作的值:
Value = [0.0] * arms
- 然后,我们将
Value列表打印到控制台,确保所有值都是 0.0。
代码的第一个部分初始化了我们的奖励、臂的数量、学习率和值列表。现在,我们需要实现训练周期,在这个周期中,我们的代理/算法将学习每个拉动的价值。让我们回到 Chapter_1_1.py 的代码中,查看下一部分:
- 列表中我们想要关注的下一部分代码是标题为
agent learns的部分,如下所示供参考:
# agent learns
for i in range(0, episodes):
action = random.randint(0,arms-1)
Value[action] = Value[action] + learning_rate * (
reward[action] - Value[action])
print(Value)
-
我们首先定义一个
for循环,该循环从0到我们的轮数。对于每个轮次,我们让代理拉动一个臂,并使用该拉动的奖励来更新其对该动作或臂的价值判断。 -
然后,我们想要确定代理随机拉动的动作或臂,使用以下代码:
action = random.randint(0,arms-1)
-
代码只是根据老丨虎丨机上臂的总数(减一以允许正确的索引)随机选择一个臂/动作编号。
-
这允许我们使用下一行代码确定拉动的价值,该代码与我们的先前价值方程非常相似:
Value[action] = Value[action] + learning_rate * ( reward[action] - Value[action])
-
这行代码明显类似于我们之前的
Value方程的数学公式。现在,思考一下learning_rate是如何在剧集的每次迭代中应用的。注意,以.1的速率,我们的智能体正在学习或应用智能体收到的reward与智能体之前等价的Value函数之差的1/10^(th)。这个小技巧的效果是在剧集之间平均化值。 -
最后,在循环完成后,并运行了所有剧集后,我们打印出每个动作更新的
Value函数。 -
您可以通过命令行或您喜欢的Python编辑器运行代码。在Visual Studio中,这就像按一下播放按钮那么简单。代码运行完成后,你应该会看到以下类似的内容,但不是确切的输出:

来自Chapter_1_1.py的输出
你肯定会看到不同的输出值,因为你在电脑上的随机动作选择将是不同的。Python有许多方法可以设置随机种子的静态值,但我们现在还不必担心这个问题。
现在,回想一下,并将这些输出值与为每个杠杆设定的奖励进行比较。它们是否相同或不同?以及如果不同,差异有多大?一般来说,仅经过100个剧集的学习后,应该可以清楚地显示出价值,但可能不是最终的价值。这意味着这些值将小于最终奖励,但它们应该仍然显示出偏好。
我们在这里展示的解决方案是一个试错学习的例子;它就是我们在RL历史部分提到的那第一条线索。正如你所见,智能体通过随机拉动杠杆并确定其价值来学习。然而,我们的智能体从未学会根据那些更新的价值做出更好的决策。智能体总是随机拉动。我们的智能体目前还没有决策机制,或者我们称之为RL中的策略。我们将在下一节中探讨如何实现一个基本的贪婪策略。
实现贪婪策略
我们当前的价值学习器除了在几个剧集内找到每个动作的最优计算值或奖励之外,并没有真正学习。由于我们的智能体没有学习,这也使得它成为一个效率较低的智能体。毕竟,智能体在每一期中只是随机选择任何杠杆,而它本可以使用其获得的知识,即Value函数,来确定其下一个最佳选择。我们可以在下一个练习中通过一个非常简单的策略,即贪婪策略来实现这一点:
- 打开
Chapter_1_2.py示例。代码基本上与我们的上一个示例相同,除了剧集迭代和特别地,动作或杠杆的选择。完整的列表可以在这里看到——注意新的高亮部分:
import random
reward = [1.0, 0.5, 0.2, 0.5, 0.6, 0.1, -.5]
arms = len(reward)
learning_rate = .1
episodes = 100
Value = [0.0] * arms
print(Value)
def greedy(values):
return values.index(max(values))
# agent learns
for i in range(0, episodes):
action = greedy(Value)
Value[action] = Value[action] + learning_rate * (
reward[action] - Value[action])
print(Value)
-
注意到新加入的
greedy()函数。这个函数将始终选择具有最高价值的动作,并返回相应的索引/动作索引。这个函数本质上就是我们的智能体的策略。 -
在代码中向下滚动,注意在训练循环中我们现在是如何使用
greedy()函数来选择我们的动作,如下所示:
action = greedy(Value)
- 再次,运行代码并查看输出。这是你预期的结果吗?出了什么问题?
查看你的输出可能表明智能体正确地计算了最大奖励臂,但可能没有确定其他臂的正确价值。原因是,一旦智能体找到了最有价值的臂,它就会一直拉这个臂。本质上,智能体找到了最佳路径并坚持它,这在单步或静态环境中是可以的,但在需要多个决策的多个步骤问题中肯定不行。相反,我们需要平衡智能体探索和寻找新路径的需求,与最大化即时最优奖励。这个问题在强化学习中被称为探索与利用的困境,我们将在下一节中探讨。
探索与利用
正如我们所见,让我们的智能体总是做出最佳选择限制了它们学习单个状态(更不用说多个相连状态)的完整价值的能力。这也严重限制了智能体学习的能力,尤其是在多个状态汇聚和发散的环境中。因此,我们需要一种让我们的智能体根据一个偏向于更均匀的动作/价值分布的策略来选择动作的方法。本质上,我们需要一种策略,允许我们的智能体探索并利用其知识以最大化学习。探索与利用之间的权衡有多种变体和方式,这很大程度上取决于特定的环境和您使用的特定强化学习实现。我们永远不会使用绝对贪婪策略,而是使用贪婪策略的某种变体或完全不同的方法。在我们的下一个练习中,我们将展示如何实现一个初始乐观值方法,这可能非常有效:
- 打开
Chapter_1_3.py并查看这里显示的突出显示的行:
episodes = 10000
Value = [5.0] * arms
-
首先,我们将
episodes的数量增加到10000。这将使我们能够确认我们的新策略正在收敛到某个适当的解决方案。 -
接下来,我们将
Value列表的初始值设置为5.0。请注意,这个值远高于奖励值的最大值1.0。使用高于我们奖励值的高值迫使我们的智能体始终探索最有价值的路径,现在变成任何它尚未探索的路径,从而确保我们的智能体将始终至少探索每个动作或臂一次。 -
没有更多的代码更改,你可以像平常一样运行示例。示例的输出如下所示:

Chapter_1_3.py 的输出
您的输出可能略有不同,但很可能显示非常相似的价值。注意计算出的值现在更加相对。也就是说,1.0的价值清楚地表明了最佳的行动方案,即奖励为1.0的臂,但其他值对实际奖励的指示性较弱。初始选项价值方法有效,但会迫使智能体探索所有路径,这在较大的环境中效率不高。当然,还有许多其他方法可以用来平衡探索与利用,我们将在下一节中介绍一种新方法,其中我们将介绍如何使用Q学习解决完整的RL问题。
带有上下文老丨虎丨机的探索Q学习
现在我们已经了解了如何计算值以及探索和利用之间的微妙平衡,我们可以继续解决整个MDP问题。正如我们将看到的,不同的解决方案在RL问题和环境中表现得好或不好。这实际上是下一几个章节的基础。然而,目前我们只想介绍一种足够基本的方法,可以解决完整的RL问题。我们将完整的RL问题描述为非平稳或上下文化的多臂老丨虎丨机问题,即在每个回合中移动到不同的老丨虎丨机并从多个臂中选择一个臂的智能体。现在每个老丨虎丨机代表一个不同的状态,我们不再只想确定动作的价值,而是其质量。我们可以使用这里显示的Q学习方程来计算给定状态的动作质量:

在前面的方程中,我们有以下内容:
-
![状态]()
-
:状态 -
![下一动作]()
-
![当前动作]()
-
ϒ:gamma—奖励折扣
-
α:alpha—学习率
-
r:奖励
-
:奖励折扣 -
:质量
现在,如果所有这些术语都有些陌生,这个方程看起来有些令人望而生畏,请不要过分担心。这是由克里斯·沃特金斯在 1989 年开发的 Q 学习方程,它是一种简化求解有限马尔可夫决策过程或FMDP的方法。在这个阶段,关于这个方程的重要观察点是理解它与我们之前看到的动作值方程的相似之处。在第 2 章《动态规划和贝尔曼方程》中,我们将更详细地学习这个方程是如何推导和工作的。现在,我们需要掌握的重要概念是,我们现在正在根据之前的状态和奖励以及动作来计算一个基于质量的值,而不仅仅是单个动作值。这反过来又允许我们的智能体为多个状态做出更好的规划。在下一节中,我们将实现一个可以玩多个多臂老丨虎丨机的 Q 学习智能体,并能够最大化奖励。
实现一个 Q 学习智能体
虽然那个 Q 学习方程可能看起来复杂得多,但实际上实现这个方程并不像我们之前学习值时构建我们的智能体那样。为了使事情更简单,我们将使用相同的代码基础,但将其转换为 Q 学习示例。打开代码示例,Chapter_1_4.py,并按照这里的练习进行:
- 这里是完整的代码列表,供参考:
import random
arms = 7
bandits = 7
learning_rate = .1
gamma = .9
episodes = 10000
reward = []
for i in range(bandits):
reward.append([])
for j in range(arms):
reward[i].append(random.uniform(-1,1))
print(reward)
Q = []
for i in range(bandits):
Q.append([])
for j in range(arms):
Q[i].append(10.0)
print(Q)
def greedy(values):
return values.index(max(values))
def learn(state, action, reward, next_state):
q = gamma * max(Q[next_state])
q += reward
q -= Q[state][action]
q *= learning_rate
q += Q[state][action]
Q[state][action] = q
# agent learns
bandit = random.randint(0,bandits-1)
for i in range(0, episodes):
last_bandit = bandit
bandit = random.randint(0,bandits-1)
action = greedy(Q[bandit])
r = reward[last_bandit][action]
learn(last_bandit, action, r, bandit) print(Q)
- 所有加亮的代码部分都是新的,值得仔细关注。让我们更详细地看看每个部分:
arms = 7 bandits = 7
gamma = .9
- 我们首先将
arms变量初始化为7,然后创建一个新的bandits变量也是7。回想一下,arms类似于actions,而bandits同样是state。最后一个新变量gamma是一个新的学习参数,用于折现奖励。我们将在未来的章节中探讨这个折现因子概念:
reward = [] for i in range(bandits):
reward.append([]) for j in range(arms):
reward[i].append(random.uniform(-1,1)) print(reward)
- 下一节代码构建了一个奖励表矩阵,其中包含从 -1 到 1 的随机值。在这个例子中,我们使用列表的列表来更好地表示单独的概念:
Q = [] for i in range(bandits):
Q.append([])
for j in range(arms):
Q[i].append(10.0) print(Q)
-
下一个部分非常相似,这次设置了一个 Q 表矩阵来存储我们计算的质量值。注意我们如何将初始 Q 值初始化为 10.0。我们这样做是为了考虑到数学中的细微变化,我们将在后面讨论这一点。
-
由于我们的状态和动作都可以映射到一个矩阵/表中,我们将我们的强化学习系统称为使用模型。模型代表环境中的所有动作和状态:
def learn(state, action, reward, next_state):
q = gamma * max(Q[next_state]) q += reward q -= Q[state][action] q *= learning_rate q += Q[state][action] Q[state][action] = q
- 接下来,我们定义了一个新的函数,称为
learn。这个新函数只是我们之前观察到的 Q 方程的直接实现:
bandit = random.randint(0,bandits-1) for i in range(0, episodes):
last_bandit = bandit
bandit = random.randint(0,bandits-1) action = greedy(Q[bandit]) r = reward[last_bandit][action]
learn(last_bandit, action, r, bandit) print(Q)
-
最后,智能体学习部分通过新的代码进行了显著更新。这段新代码设置了我们需要的新学习函数的参数。注意老丨虎丨机或状态每次都是随机选择的。本质上,这意味着我们的智能体只是在老丨虎丨机之间随机漫步。
-
正常运行代码,并注意最后打印出的新计算出的Q值。它们是否与每次拉动臂杆的奖励相匹配?
很可能,你的几个臂杆与相应的奖励值不匹配。这是因为新的Q学习方程解决了整个MDP,但我们的智能体并没有在MDP中移动。相反,我们的智能体只是在状态之间随机移动,而不关心它之前看到了哪个状态。回想一下我们的例子,你就会意识到,由于我们的当前状态不会影响我们的未来状态,它没有满足马尔可夫属性,因此不是一个MDP。然而,这并不意味着我们不能成功地解决这个问题,我们将在下一节中探讨这一点。
移除折现奖励
我们当前解决方案和全Q学习方程的问题在于,该方程假设我们的智能体(agent)所处的任何状态都会影响未来的状态。然而,记住在我们的例子中,智能体只是随机地从强盗移动到强盗。这意味着使用任何先前状态信息将是无用的,正如我们所看到的。幸运的是,我们可以通过移除折现奖励的概念来轻松解决这个问题。回想一下,在这个复杂项中出现的新的变量gamma:
。Gamma和这个项是折现未来奖励的一种方式,我们将在第二章,“动态规划和贝尔曼方程”中详细讨论。现在,尽管如此,我们可以通过从我们的学习函数中移除这个项来修复这个样本。让我们打开代码示例,Chapter_1_5.py,并遵循这里的练习:
- 我们真正需要关注的代码部分只有更新的
learn函数,如下所示:
def learn(state, action, reward, next_state):
#q = gamma * max(Q[next_state])
q = 0
q += reward
q -= Q[state][action]
q *= learning_rate
q += Q[state][action]
Q[state][action] = q
-
函数中的第一行代码负责折现下一个状态的未来奖励。由于我们例子中的所有状态都没有连接,我们只需注释掉那一行。在下一行,我们为
q = 0创建一个新的初始化器。 -
正常运行代码。现在你应该会看到非常接近的值与各自的奖励非常接近。
通过省略计算中的折现奖励部分,希望你能理解这将仅仅回归到一个价值计算问题。或者,你也可能意识到,如果我们的强盗(bandits)是相互连接的。也就是说,拉动一个臂杆会导致另一个具有更多动作的臂杆机器,如此类推。那么我们可以使用Q学习方程来解决这个问题。
这就结束了对强化学习(RL)的主要组件和元素的非常基础的介绍。在这本书的其余部分,我们将深入探讨策略、价值、动作和奖励的细微差别。
摘要
在本章中,我们首先向RL的世界介绍了自己。我们探讨了为什么RL如此独特以及为什么它对游戏来说是有意义的。之后,我们研究了现代RL的基本术语和历史。从那里,我们转向RL的基础和马尔可夫决策过程,我们发现是什么构成了RL问题。然后我们转向构建我们的第一个学习者——一个价值学习者,它计算状态在动作上的值。这使我们发现了探索和利用的需求以及不断挑战RL实施者的困境。接下来,我们深入研究了完整的Q学习方程以及如何构建Q学习者,后来我们意识到完整的Q方程超出了我们未连接状态环境的需求。然后我们将我们的Q学习重新转换为价值学习者,并观察它解决上下文投币机问题。
在下一章中,我们将继续我们留下的内容,探讨如何使用贝尔曼方程对奖励进行折现,以及查看动态规划为RL引入的许多其他改进。
问题
使用这些问题和练习来巩固你刚刚学到的内容。这些练习可能很有趣,所以请确保尝试至少两到四个问题/练习:
问题:
-
强化学习(RL)系统的主要组成部分有哪些名称?提示:第一个是环境。
-
列出强化学习(RL)系统的四个要素。请记住,其中一个要素是可选的。
-
列出组成现代强化学习(RL)的三个主要线程。
-
什么使马尔可夫状态具有马尔可夫性质?
-
政策是什么?
练习:
-
使用
Chapter_1_2.py,修改代码使代理从一个有1,000个臂的投币机中抽取。你需要做出哪些代码更改? -
使用
Chapter_1_3.py,修改代码使代理从平均值而非贪婪/最大值中抽取。这如何影响了代理的探索? -
使用
Chapter_1_3.py,修改learning_rate变量以确定你可以使代理学习得多快或多慢。你需要运行多少个剧集才能使代理解决问题? -
使用
Chapter_1_5.py,修改代码使代理使用不同的策略(无论是贪婪策略还是其他策略)。如果你在这本书或网上查找解决方案,请扣分。 -
使用
Chapter_1_4.py,修改代码以便使投币机连接起来。因此,当代理拉动一个臂时,他们会获得奖励并被传输到另一个特定的投币机,不再是随机的。提示:这很可能需要构建一个新的目的地表,并且你现在需要包括我们之前移除的折现奖励项。
即使完成这些问题和/或练习中的几个,也会对你的学习产生巨大影响。毕竟,这是一本实践性很强的书。
第三章:动态规划与贝尔曼方程
动态规划(DP)是继试错学习之后,对现代强化学习(RL)产生重大影响的第二个主要分支。在本章中,我们将探讨DP的基础,并研究它们如何影响RL领域。我们还将探讨贝尔曼方程和最优性概念如何与RL交织在一起。从那里,我们将探讨策略和值迭代方法来解决适合DP的一类问题。最后,我们将探讨如何使用本章学到的概念来教一个智能体玩OpenAI Gym中的FrozenLake环境。
下面是我们将在本章中涵盖的主要主题:
-
介绍动态规划(DP)
-
理解贝尔曼方程
-
构建策略迭代
-
构建值迭代
-
玩策略迭代与值迭代
对于本章,我们将探讨如何使用贝尔曼最优方程,用DP解决有限马尔可夫决策过程(MDP)。本章旨在作为DP和贝尔曼的历史课程和背景介绍。如果您已经非常熟悉DP,那么您可能希望跳过本章,因为我们只将探索入门级DP,涵盖足够的背景知识,以了解它如何影响和改进RL。
介绍DP
DP是由理查德·E·贝尔曼在20世纪50年代开发的,作为一种优化和解决复杂决策问题的方法。该方法最初应用于工程控制问题,但后来在所有需要分析和建模问题及其子问题的学科中找到了应用。实际上,所有DP都是关于解决子问题,然后找到将这些子问题连接起来以解决更大问题的关系。它通过首先应用贝尔曼最优方程,然后求解来实现这一点。
在我们用DP解决有限MDP之前,我们希望更详细地了解我们正在讨论的内容。让我们在下一节中看看正常递归和DP之间的简单示例差异。
正规编程与DP的比较
我们将通过首先使用常规方法解决问题,然后使用DP来进行比较。在这个过程中,我们将识别出使我们的解决方案成为DP的关键元素。大多数经验丰富的程序员发现,他们可能已经在某种程度上做过DP,所以如果这一切听起来非常熟悉,请不要感到惊讶。让我们打开Chapter_2_1.py示例并跟随练习:
- 这段代码是使用递归查找斐波那契序列中第
n个数字的示例,如下所示:
def Fibonacci(n):
if n<0:
print("Outside bounds")
elif n==1:
return 0 # n==1, returns 0
elif n==2:
return 1 # n==2, returns 1
else:
return Fibonacci(n-1)+Fibonacci(n-2)
print(Fibonacci(9))
- 回想一下,我们可以通过将序列中的前两个数字相加来解决问题序列的第
n个元素。我们考虑当n == 1时,值为0,当n == 2时,返回的值是1。因此,序列中的第三个元素将是Fibonacci(1)和Fibonacci(2)的和,并将返回一个值为1。这在以下代码行中得到了反映:
return Fibonacci(n-1)+Fibonacci(n-2)
- 因此,使用递归的线性规划版本找到第四个元素的解决方案如下所示:

解决斐波那契数列的第四个元素
我们可以注意以下几点:
-
上述图表显示了计算前一个元素所需的每个递归调用
Fibonacci函数。注意这种方法需要解决或调用Fibonacci(2)函数两次。当然,在这个例子中,额外的调用是微不足道的,但这些额外的调用可以迅速累积。 -
按照常规方式运行代码,并查看第九个元素的打印结果。
为了欣赏递归可能有多么低效,我们已经修改了之前的示例并将其保存为Chapter_2_2.py。现在打开这个示例并跟随下一个练习:
- 修改后的代码,带有高亮显示的额外行,如下所示供参考:
def Fibonacci(n):
if n<0:
print("Outside bounds")
elif n==1:
return 0 # n==1, returns 0
elif n==2:
return 1 # n==2, returns 1
else:
print("Solving for {}".format(n))
return Fibonacci(n-1)+Fibonacci(n-2)
print(Fibonacci(9))
- 我们所做的一切只是打印出我们需要计算两个更多序列以返回总和的时刻。运行代码并注意以下截图所示的输出:

来自示例Chapter_2_1.py的输出
- 注意,仅使用递归计算斐波那契数列的第九个元素时,
Fibonacci(3)被调用的次数。
现在,解决方案有效,正如人们所说,它既美观又简洁。事实上,作为一个程序员,你可能曾经被教导要崇拜这种编程风格。然而,我们可以清楚地看到这种方法在扩展时的低效,这就是动态规划(DP)发挥作用的地方,我们将在下一节中讨论。
进入动态规划和记忆化
动态规划(DP)的一个关键概念是将较大的问题分解成较小的子问题,然后解决这些较小的子问题并存储结果。这种活动的时髦名称叫做记忆化,展示这种工作方式的最佳方式是使用一个例子。打开Chapter_2_3.py并跟随练习:
- 作为参考,我们之前示例中的整个代码块已被修改如下:
fibSequence = [0,1]
def Fibonacci(n):
if n<0:
print("Outside bounds")
elif n<= len(fibSequence):
return fibSequence[n-1]
else:
print("Solving for {}".format(n))
fibN = Fibonacci(n-1) + Fibonacci(n-2)
fibSequence.append(fibN)
return fibN
print(Fibonacci(9))
- 再次强调,高亮显示的行表示代码更改,但在这个例子中,我们将更详细地逐个分析代码更改,从第一个更改开始,如下所示:
fibSequence = [0,1]
- 这条新线创建了一个新的斐波那契列表,包含我们的两个基数
0和1。我们仍然使用递归函数,但现在我们还存储每次独特计算的每个结果以供以后使用:
elif n<= len(fibSequence):
return fibSequence[n-1]
- 下一个代码更改是在算法返回之前存储的值的地方,例如
0或1,或者计算后存储在fibSequence列表中:
fibN = Fibonacci(n-1) + Fibonacci(n-2) fibSequence.append(fibN) return fibN
-
最后一批代码更改现在保存了递归计算,将新值添加到整个序列中。这现在要求算法只需计算序列的第
n个值一次。 -
按照常规方式运行代码,并查看以下截图所示的结果:

来自示例Chapter_2_3.py的输出示例
注意我们现在只计算序列中的数字,而不重复任何计算。显然,这种方法比我们之前看到的线性规划示例要好得多,而且代码看起来也不差。现在,正如我们所说的,如果这种解决方案看起来很显然,那么你可能比你意识到的更了解DP。
在这本书中,我们将仅简要介绍与强化学习相关的动态规划(DP)。正如你所见,DP是一种强大的技术,可以惠及任何认真优化代码的开发者。
在下一节中,我们将进一步探讨贝尔曼的工作以及以他的名字命名的方程。
理解贝尔曼方程
贝尔曼在解决有限MDP问题时使用了动态规划(DP),正是在这些努力中他推导出了他著名的方程。这个方程背后的美丽之处——以及更抽象地说,一般概念——在于它描述了一种优化状态价值或质量的方法。换句话说,它描述了在给定动作和后续状态的选择的情况下,我们如何确定处于某个状态的最优价值/质量。在分解方程本身之前,让我们首先在下一节重新审视有限MDP。
解开有限MDP
考虑我们在第1章,“理解奖励学习”中开发的有限MDP,它描述了你的早晨常规。如果你之前没有完成那个练习,不用担心,我们将考虑一个更具体的例子,如下所示:

用于醒来和乘坐公交车的MDP
前面的有限MDP描述了某人醒来并准备乘坐公交车去上学或上班的可能常规。在这个MDP中,我们定义了一个初始状态(BEGIN)和一个结束状态,即上车(END)。R = 表示移动到该状态时分配的奖励,而靠近动作行末尾的数字表示采取该动作的概率。我们可以用以下图示表示通过这个有限MDP的最优路径:

MDP的最佳解决方案
从数学上讲,我们可以将最优结果描述为通过环境遍历获得的所有奖励的总和。更正式地说,我们也可以这样数学地写出:

然而,这正是贝尔曼介入的地方,我们不能将所有奖励视为相等。在不更深入地探讨数学细节的情况下,贝尔曼方程引入了未来奖励应该被折现的概念。当你这样思考时,这也是相当直观的。从未来的动作中感受到的经验或效果会根据我们需要决定的时间距离而减弱。这正是我们应用贝尔曼方程时所采用的概念。因此,我们现在可以对前面的方程应用一个折现因子(伽马)并展示以下内容:

伽马(
),在方程中表示,代表未来奖励的折现因子。这个值可以从 0.0 到 1.0。如果值为 1.0,则我们不考虑未来奖励的折现,而值为 0.1 将会大幅度折现未来奖励。在大多数强化学习问题中,我们保持这个数值相当高,并且远高于 0.9。这引出了下一节,我们将讨论贝尔曼方程如何优化问题。
贝尔曼最优方程
贝尔曼方程表明,你可以通过首先找到允许智能体遍历 MDP 的最优策略来解决任何 MDP。回想一下,策略定义了指导智能体通过 MDP 的每个动作的决定。理想情况下,我们想要找到的是最优策略:一个可以最大化每个状态的价值并确定要遍历哪些状态以获得最大奖励的策略。当我们结合其他概念并应用更多的数学技巧,然后与贝尔曼最优方程结合,我们得到以下最优策略方程:

非常奇怪的开头术语([
)是一种描述函数的方式,该函数在给定一组状态和动作的情况下最大化奖励,同时考虑到我们通过一个称为伽马(gamma)的因子对未来的奖励进行折现。注意我们如何也使用
来表示策略方程,但我们通常认为这是一个质量,可能会将其称为 q 或 Q。如果你回想起我们之前对 Q-learning 方程的简要了解,那么现在你可以清楚地看到奖励的折现因子伽马是如何起作用的。
在下一节中,我们将探讨使用动态规划(DP)和基于我们对贝尔曼最优原则和结果策略方程的理解的策略迭代方法来解决 MDP 的方法。
构建策略迭代
为了确定最佳策略,我们首先需要一个方法来评估给定状态的政策。我们可以通过搜索 MDP 的所有状态并进一步评估所有动作来评估政策。这将为我们提供给定状态的价值函数,然后我们可以使用它来迭代地执行新值函数的连续更新。从数学上讲,我们可以使用前面的 Bellman 最优性方程并推导出新的状态值函数更新,如下所示:

在前面的方程中,[
] 符号代表期望值,表示期望状态值更新到新的值函数。在这个期望值内部,我们可以看到它依赖于返回的奖励加上给定已选择动作的下一个状态的先前折现值。这意味着我们的算法将遍历每个状态和动作,使用前面的更新方程评估新的状态值。这个过程称为备份或规划,使用备份图可视化这个算法的工作方式对我们很有帮助。以下是动作值和状态值备份的备份图示例:

动作值和状态值备份的备份图
图(a)或[
]是备份或评估中尝试每个动作的部分,因此为我们提供了动作值。评估的第二部分来自更新,并在图(b)中显示为[
]。回想一下,更新通过评估每个状态动作来评估前向状态。这些图用实心圆圈表示评估点。注意动作值只关注前向动作,而状态值关注每个前向状态的动作值。当然,查看代码中这些是如何结合起来的会有所帮助。然而,在我们到达那里之前,我们想要在下一节做一些整理工作。
安装 OpenAI Gym
为了鼓励强化学习(RL)的研究和开发,OpenAI 团队提供了一个开源的 RL 训练平台,称为 Gym。由 OpenAI 提供的 Gym 拥有大量的样本测试环境,我们可以在阅读本书的过程中探索这些环境。此外,其他 RL 开发者也使用 Gym 相同的标准接口开发了其他环境。因此,通过学习使用 Gym,我们将在本书的后面部分也能够探索其他前沿的 RL 环境。
Gym 的安装相当简单,但同时我们想要避免任何可能让你感到沮丧的小错误。因此,最好使用以下说明来设置和安装一个用于开发的 RL 环境。
强烈建议您使用Anaconda进行本书的Python开发。Anaconda是一个免费的开源跨平台工具,可以显著提高您的开发便利性。除非您认为自己是一位经验丰富的Python开发者,否则请坚持使用Anaconda。通过Google搜索python anaconda下载并安装它。
按照练习设置和安装带有Gym的Python环境:
-
打开一个新的Anaconda Prompt或Python shell。如果您需要,请以管理员身份执行这些命令。
-
从命令行运行以下命令:
conda create -n chapter2 python=3.6
-
这将为您的开发创建一个新的虚拟环境。虚拟环境允许您隔离依赖项并控制版本。如果您不使用Anaconda,您可以使用Python虚拟环境来创建一个新环境。您还应该注意到,我们正在强制环境使用Python 3.6。再次强调,这确保我们知道我们使用的是哪个版本的Python。
-
安装完成后,我们使用以下命令激活环境:
activate chapter2
- 接下来,我们使用以下命令安装Gym:
pip install gym
- Gym将安装几个依赖项,包括我们稍后将在其上训练的各种样本环境。
在我们走得太远之前,现在让我们在下一节中使用代码测试我们的Gym安装。
测试Gym
在下一个练习中,我们将编写代码来测试Gym和一个名为FrozenLake的环境,这个环境也恰好是我们本章的测试环境。打开Chapter_2_4.py代码示例并按照练习进行:
- 为了参考,代码如下所示:
from os import system, name
import time
import gym
import numpy as np
env = gym.make('FrozenLake-v0')
env.reset()
def clear():
if name == 'nt':
_ = system('cls')
else:
_ = system('clear')
for _ in range(1000):
clear()
env.render()
time.sleep(.5)
env.step(env.action_space.sample()) # take a random action
env.close()
-
在顶部,我们有导入语句,用于加载
system模块以及gym、time和numpy。numpy是一个辅助库,我们用它来构建张量。张量是数学/编程概念,可以描述单个值或数字的多维数组。 -
接下来,我们使用以下代码构建和重置环境:
env = gym.make('FrozenLake-v0')
env.reset()
-
之后,我们有一个
clear函数,我们用它来清除对示例不重要的渲染。代码应该也是不言自明的。 -
这将带我们到
for循环,也就是所有动作发生的地方。以下是最重要的那一行:
env.step(env.action_space.sample())
-
env变量代表环境,在那一行中,我们让算法每一步或迭代随机采取一个动作。在这个例子中,代理目前什么也没学到,只是随机移动。 -
按照正常方式运行代码,并注意输出。以下是一个输出屏幕的示例:

从FrozenLake环境中提取的示例渲染
由于算法/代理随机移动,它很可能会撞到表示为H的洞,并停留在那里。为了参考,以下是FrozenLake的图例:
-
S= 开始:当调用重置时,代理从这里开始。 -
F= 冻结:这允许代理在这个区域内移动。 -
H= 洞:这是冰中的洞;如果智能体移动到这里,它会掉进去。 -
G= 目标:这是智能体想要达到的目标,当它达到时,它将获得 1.0 的奖励。
现在我们已经设置了 Gym,我们可以进入下一节来评估策略。
策略评估
与试错学习不同,你已经接触到了 DP 方法,它们作为一种静态学习形式或我们可能称之为规划的形式工作。在这里,规划是一个合适的定义,因为算法在评估整个 MDP 以及所有状态和动作之前。因此,这些方法需要完全了解环境,包括所有有限状态和动作。虽然这对于我们在这个章节中玩的环境等已知有限环境有效,但这些方法对于现实世界的物理问题来说还不够充分。当然,我们将在本书的后面解决现实世界的问题。不过,现在让我们看看如何从之前的更新方程中在代码中评估策略。打开 Chapter_2_5.py 并遵循练习:
- 为了参考,整个代码块
Chapter_2_5.py显示如下:
from os import system, name
import time
import gym
import numpy as np
env = gym.make('FrozenLake-v0')
env.reset()
def clear():
if name == 'nt':
_ = system('cls')
else:
_ = system('clear')
def act(V, env, gamma, policy, state, v):
for action, action_prob in enumerate(policy[state]):
for state_prob, next_state, reward, end in env.P[state][action]:
v += action_prob * state_prob * (reward + gamma * V[next_state])
V[state] = v
def eval_policy(policy, env, gamma=1.0, theta=1e-9, terms=1e9):
V = np.zeros(env.nS)
delta = 0
for i in range(int(terms)):
for state in range(env.nS):
act(V, env, gamma, policy, state, v=0.0)
clear()
print(V)
time.sleep(1)
v = np.sum(V)
if v - delta < theta:
return V
else:
delta = v
return V
policy = np.ones([env.env.nS, env.env.nA]) / env.env.nA
V = eval_policy(policy, env.env)
print(policy, V)
-
在代码的开始,我们执行与我们的测试示例相同的初始步骤。我们加载
import语句,初始化和加载环境,然后定义clear函数。 -
接下来,移动到代码的末尾并注意我们是如何使用
numpy as np初始化策略来填充一个大小为环境statexaction的张量。然后,我们将张量除以状态中的动作数量——在这个例子中是4。这给出了每个动作的分布式概率0.25。记住,在马尔可夫属性中,所有动作的概率总和需要达到1.0或 100%。 -
现在,向上移动到
eval_policy函数并关注双重循环,如下面的代码块所示:
for i in range(int(terms)):
for state in range(env.nS):
act(V, env, gamma, policy, state, v=0.0)
clear()
print(V)
time.sleep(1)
v = np.sum(V)
if v - delta < theta:
return V
else:
delta = v
return V
-
第一个
for循环遍历在终止之前的项数或迭代次数。在这里我们设置一个限制以防止无限循环。在内循环中,通过act函数对环境中的所有状态进行迭代并采取行动。之后,我们使用之前的渲染代码来显示更新的值。第一个for循环结束时,我们检查计算出的v值的总变化是否小于特定的阈值,theta。如果值的变化小于阈值,函数返回计算出的值函数,V。 -
算法的核心是
act函数以及更新方程操作的地方;该函数内部的显示如下:
for action, action_prob in enumerate(policy[state]):
for state_prob, next_state, reward, end
in env.P[state][action]:
v += action_prob * state_prob * (reward + gamma * V[next_state]) #update
V[state] = v
-
第一个
for循环遍历给定状态下策略中的所有动作。回想一下,我们首先将策略初始化为每个action函数的0.25,即action_prob = 0.25。然后,我们遍历从状态和动作到每个转换并应用更新。更新在突出显示的方程中显示。最后,当前状态的价值函数V更新为v。 -
运行代码并观察输出。注意
value函数是如何不断更新的。在运行结束时,你应该看到类似于以下截图的内容:

运行示例Chapter_2_5.py
如果策略没有更新看起来有些不对劲,实际上这现在是完全可以接受的。这里的重要部分是看到我们是如何更新value函数的。在下一节中,我们将探讨如何改进策略。
策略改进
在掌握策略评估之后,是时候通过前瞻性思考来改进策略了。回想一下,我们是通过对当前状态之前的一个状态进行观察,然后评估所有可能的行为来做到这一点的。让我们看看在代码中是如何实现这一点的。打开Chapter_2_6.py示例并跟随练习:
- 为了简洁起见,以下从
Chapter_2_6.py中摘录的代码仅显示了添加到上一个示例中的新代码部分:
def evaluate(V, action_values, env, gamma, state):
for action in range(env.nA):
for prob, next_state, reward, terminated in env.P[state][action]:
action_values[action] += prob * (reward + gamma * V[next_state])
return action_values
def lookahead(env, state, V, gamma):
action_values = np.zeros(env.nA)
return evaluate(V, action_values, env, gamma, state)
def improve_policy(env, gamma=1.0, terms=1e9):
policy = np.ones([env.nS, env.nA]) / env.nA
evals = 1
for i in range(int(terms)):
stable = True
V = eval_policy(policy, env, gamma=gamma)
for state in range(env.nS):
current_action = np.argmax(policy[state])
action_value = lookahead(env, state, V, gamma)
best_action = np.argmax(action_value)
if current_action != best_action:
stable = False
policy[state] = np.eye(env.nA)[best_action]
evals += 1
if stable:
return policy, V
#replaced bottom code from previous sample with
policy, V = improve_policy(env.env)
print(policy, V)
-
在上一个示例中添加了三个新的函数:
improve_policy、lookahead和evaluate。improve_policy使用有限循环遍历当前环境中的状态;在遍历每个状态之前,它调用eval_policy通过传递当前的policy、environment和gamma(折扣因子)参数来更新value函数。然后,它调用lookahead函数,该函数内部调用一个evaluate函数来更新状态的动作值。evaluate是act函数的一个修改版本。 -
虽然
eval_policy和improve_policy这两个函数都使用有限循环的术语来防止无限循环,但它们仍然使用非常大的限制;在示例中,默认值是1e09。因此,我们仍然希望确定一个条件,以便在术语限制之前尽早停止循环。在策略评估中,我们通过观察价值函数的变化或delta来控制这一点。在策略改进中,我们现在要改进实际策略,为此,我们假设一个贪婪策略。换句话说,我们希望改进我们的策略,使其总是选择具有最高价值的动作,如下面的代码所示:
action_value = lookahead(env, state, V, gamma)best_action = np.argmax(action_value)
if current_action != best_action:
stable = False
policy[state] = np.eye(env.nA)[best_action]
evals += 1
if stable:
return policy, V
-
上述代码块首先使用
numpy函数——np.argmax在lookahead函数返回的action_value列表上返回最大值或best_action,换句话说,就是贪婪动作。然后我们考虑current_action是否不等于best_action;如果不等于,那么我们认为策略是不稳定的,将stable设置为false。由于动作不是最好的,我们还使用np.eye为定义的形状更新policy,使用单位张量。这一步只是将策略的值分配为1.0给最佳/贪婪动作,而其他所有动作的值为0.0。 -
在代码的末尾,你可以看到我们现在只是调用
improve_policy并打印策略和价值函数的结果。 -
按照正常方式运行代码并观察输出,如下面的截图所示:

Chapter_2_6.py的示例输出
这个示例将需要更长的时间来运行,你应该会看到随着示例的运行,value函数得到改善。当示例完成后,它将打印出值函数和策略。你现在可以看到策略如何清楚地指示每个状态的最佳动作,其值为1.0。一些状态的所有动作仍然具有0.25的值,原因在于算法认为在这些状态下没有必要评估或改进策略。这些状态可能是空缺状态或位于最优路径之外。
政策评估和改进是我们可以使用DP进行规划的一种方法,但在下一节中,我们将探讨第二种方法,称为值迭代。
构建值迭代
在值迭代中,我们遍历整个MDP中的所有状态,寻找每个状态的最佳值,当我们找到时,就停止或中断。然而,我们并没有就此停止,而是继续向前查看所有状态,并假设最佳动作的概率为100%。这产生了一种新的策略,可能比之前的策略迭代演示表现得更好。这两种方法之间的差异很微妙,最好通过代码示例来理解。打开Chapter_2_7.py并跟随下一个练习:
- 这个代码示例建立在之前的示例之上。示例
Chapter_2_7.py中的新代码变化如下:
def value_iteration(env, gamma=1.0, theta=1e-9, terms=1e9):
V = np.zeros(env.nS)
for i in range(int(terms)):
delta = 0
for state in range(env.nS):
action_value = lookahead(env, state, V, gamma)
best_action_value = np.max(action_value)
delta = max(delta, np.abs(V[state] - best_action_value))
V[state] = best_action_value
if delta < theta: break
policy = np.zeros([env.nS, env.nA])
for state in range(env.nS):
action_value = lookahead(env, state, V, gamma)
best_action = np.argmax(action_value)
policy[state, best_action] = 1.0
return policy, V
#policy, V = improve_policy(env.env)
#print(policy, V)
policy, V = value_iteration(env.env)
print(policy, V)
-
这段代码的大部分与我们之前在示例中已经审查过的代码非常相似,但也有一些值得注意的细微差别。
-
首先,这次,在有限项循环内部,我们遍历状态并使用
lookahead函数进行直接前瞻性查看。此代码的详细信息如下:
for state in range(env.nS):
action_value = lookahead(env, state, V, gamma)
best_action_value = np.max(action_value)
delta = max(delta, np.abs(V[state] - best_action_value))
V[state] = best_action_value
- 与政策评估和改进相比,前述代码的细微差别在于,这次我们立即进行前瞻性查看,遍历动作值,然后根据最佳值更新
value函数。在这段代码块中,我们还计算了一个新的delta值或从先前最佳动作值的变化量:
if delta < theta: break
- 在循环之后,有一个
if语句检查计算出的delta值或动作值变化量是否低于特定的阈值theta。如果delta足够小,我们就中断有限项循环:
policy = np.zeros([env.nS, env.nA])
for state in range(env.nS):
action_value = lookahead(env, state, V, gamma)
best_action = np.argmax(action_value)
policy[state, best_action] = 1.0
return policy, V
-
从那里,我们使用
numpy np.zeros函数将policy初始化为零。然后,我们再次遍历所有状态,并使用lookahead函数进行另一步前瞻性查看。这个函数返回一个动作值的列表,我们确定最大索引值,即best_action。然后我们将policy设置为1.0;我们假设最佳动作总是为该状态选择。最后,我们返回新的策略和value函数,V。 -
按照正常方式运行代码,并检查以下截图所示的输出:

从 Chapter_2_8.py 生成的输出示例
这次,我们没有进行任何策略迭代或改进,因此样本运行得更快。你也应该注意策略是如何对所有状态进行更新的。回想一下,在策略迭代中,只有算法/智能体能够通过的相关状态才会被评估。
在下一节中,我们将使用策略迭代和改进与价值迭代计算出的策略,将实际智能体释放到环境中。
策略与价值迭代的比较
策略和价值迭代方法非常相似,被视为配套方法。因此,为了评估使用哪种方法,我们通常需要将两种方法都应用于所讨论的问题。在下一个练习中,我们将在 FrozenLake 环境中同时评估策略和价值迭代方法:
- 打开
Chapter_2_8.py示例。此示例基于之前的代码示例,因此我们只展示新的附加代码:
def play(env, episodes, policy):
wins = 0
total_reward = 0
for episode in range(episodes):
term = False
state = env.reset()
while not term:
action = np.argmax(policy[state])
next_state, reward, term, info = env.step(action)
total_reward += reward
state = next_state
if term and reward == 1.0:
wins += 1
average_reward = total_reward / episodes
return wins, total_reward, average_reward
policy, V = improve_policy(env.env)
print(policy, V)
wins, total, avg = play(env.env, 1000, policy)
print(wins)
policy, V = value_iteration(env.env)
print(policy, V)
wins, total, avg = play(env.env, 1000, policy)
print(wins)
- 附加代码包括一个新的函数
play和末尾的不同测试代码。在末尾的代码中,我们首先使用improve_policy函数计算策略,该函数执行策略迭代:
wins, total, avg = play(env.env, 1000, policy)print(wins)
-
接下来,我们使用
play函数评估policy的获胜次数。之后,我们打印获胜次数。 -
然后,我们使用价值迭代评估一个新的策略,再次使用
play函数评估获胜次数,并打印结果:
for episode in range(episodes):
term = False
state = env.reset()
while not term:
action = np.argmax(policy[state])
next_state, reward, term, info = env.step(action)
total_reward += reward
state = next_state
if term and reward == 1.0:
wins += 1
average_reward = total_reward / episodes return wins, total_reward, average_reward
- 在
play函数中,我们遍历次数。每个回合都被认为是智能体从起点移动到目标的一次尝试。在这个例子中,回合的终止发生在智能体遇到洞或目标时。如果它达到目标,它将获得1.0的奖励。大部分代码都是自解释的,除了智能体执行动作并再次显示的时刻如下:
next_state, reward, term, info = env.step(action)
- 回想一下,在我们的 Gym 环境测试中,我们只是随机移动智能体。现在,在前面代码中,我们执行由策略设定的特定动作。执行动作的回报是
next_state、reward(如果有)、term或终止,以及一个info变量。这一行代码完全控制智能体,并允许其移动并与环境交互:
total_reward += reward
state = next_state
if term and reward == 1.0:
wins += 1
-
智能体移动一步后,我们更新
total_reward和state。然后,我们测试智能体是否获胜,环境是否终止,以及返回的奖励是否为1.0。否则,智能体继续。智能体也可能因掉入洞而结束回合。 -
按照正常方式运行代码,并检查以下截图所示的输出:

以下是从示例 Chapter_2_8.py 生成的输出示例
注意结果之间的差异。这是在FrozenLake问题上策略迭代与值迭代的差异。你可以调整theta和gamma参数的值,看看是否可以得到更好的结果。同样,就像强化学习本身一样,你需要自己进行一些尝试和错误,以确定最佳的DP方法。
在下一节中,我们将探讨一些可以帮助你进一步理解材料的附加练习。
练习
完成本节中的练习完全是可选的,但希望你能开始欣赏到,作为强化学习者的我们,通过实践学习是最好的。尽力而为,并尝试完成以下至少2-3个练习:
-
考虑其他可以使用动态规划(DP)解决的问题?你将如何将问题分解为子问题并计算每个子问题?
-
编写另一个示例,比较线性编程与动态编程的问题。使用练习1中的示例。代码示例
Chapter_2_2.py和Chapter_2_3.py是并列比较的好例子。 -
查阅OpenAI文档,并探索其他强化学习环境。
-
使用
Chapter_2_4.py中的示例测试代码创建、渲染和探索Gym中的其他强化学习环境。 -
解释使用DP评估和改进策略的过程/算法。
-
解释策略迭代与值迭代之间的区别。
-
打开
Chapter_2_5.py策略迭代示例,并调整theta和gamma参数。这些参数对学习率和值有什么影响? -
打开
Chapter_2_6.py策略改进示例,并调整theta和gamma参数。这些参数对学习率和值有什么影响? -
打开
Chapter_2_7.py值迭代示例,并调整theta和gamma参数。这些参数对学习率和值有什么影响? -
使用
FrozenLake 8x8环境完成所有策略和值迭代示例。这是湖泊问题的更大版本。现在,哪种方法表现更好?
使用这些练习来加强你对本章所涵盖材料的理解。在下一节中,我们将总结本章所涵盖的内容。
概述
在本章中,我们深入探讨了动态规划(DP)和贝尔曼方程。通过引入未来奖励和优化的概念,具有DP的贝尔曼方程对强化学习(RL)产生了显著影响。在本章中,我们首先深入研究了动态规划以及如何动态地解决问题,从而介绍了贝尔曼的贡献。然后,我们进一步理解了贝尔曼最优性方程及其如何通过迭代方法来考虑未来奖励以及确定期望的状态和动作值。特别是,我们关注了在Python中实现策略迭代和改进的实现。接着,我们从那里转向了价值迭代。最后,我们通过使用由策略和价值迭代生成的策略,在FrozenLake环境中设置了一个智能体测试,以此结束本章。对于本章,我们研究了一类非常适合DP的问题,这也有助于我们推导出强化学习中的其他概念,例如折现奖励。
在下一章中,我们将继续探讨这一主题,通过研究蒙特卡洛方法。
第四章:蒙特卡洛方法
对于本章,我们将回到强化学习(RL)的试错线程,并探讨蒙特卡洛方法。这是一类通过周期性地在一个环境中进行游戏而不是进行规划来工作的方法。我们将看到这种方法如何改进我们的最佳策略搜索,现在我们开始将我们的算法视为一个实际的代理——一个探索游戏环境而不是预先规划策略的代理,这反过来又使我们能够理解使用模型进行规划或不使用模型的益处。从那里,我们将探讨蒙特卡洛方法及其在代码中的实现。然后,我们将使用我们新的蒙特卡洛代理算法重新审视一个更大的FrozenLake环境版本。
在本章中,我们将继续探讨强化学习(RL)的演变过程,特别是关注蒙特卡洛方法中的试错线程。以下是本章我们将涵盖的主要主题总结:
-
理解基于模型和无模型学习
-
介绍蒙特卡洛方法
-
添加强化学习
-
玩FrozenLake游戏
-
使用预测和控制
我们再次探索强化学习、变分推理和试错方法的基础。这些知识对于任何认真完成这本书的人来说都是至关重要的,所以请不要跳过本章。
理解基于模型和无模型学习
如果您还记得我们非常第一章节,第1章,“基于奖励的学习理解”,我们探讨了强化学习的主要元素。我们了解到强化学习包括策略、价值函数、奖励函数,以及可选的模型。在这个上下文中,我们使用“模型”一词来指代环境的详细计划。回到上一章,我们使用了FrozenLake环境,我们对该环境有一个完美的模型:

FrozenLake环境的模型
当然,在有限MDP中用完全描述的模型来观察问题对于学习来说是非常好的。然而,当涉及到现实世界时,拥有任何环境的完整和完全理解模型很可能是不太可能,甚至是不可能的。这是因为任何现实世界问题中都有太多的状态需要考虑或建模。实际上,这也可能适用于许多其他模型。在本书的后面部分,我们将看到比已知宇宙中原子数量还要多的状态的环境。我们永远无法模拟这样的环境。因此,我们在第2章,“动态规划和贝尔曼方程”中学到的规划方法将不起作用。相反,我们需要一种可以探索环境并从中学习的方法。这就是蒙特卡洛方法出现的地方,我们将在下一节中介绍。
介绍蒙特卡洛方法
蒙特卡洛方法之所以得名,是因为它与赌博或机会相似。因此,该方法以当时著名的赌博目的地命名。虽然这种方法非常强大,但它已被用来描述原子、量子力学以及[
]本身的数量。直到最近20年,它才在从工程到金融分析等各个领域得到广泛应用。现在,这种方法已成为机器学习许多方面的基础,对于AI领域的任何人来说都值得进一步研究。
在下一节中,我们将看到蒙特卡洛方法如何用来求解[
]。
求解
蒙特卡洛方法的标准介绍是展示它如何用来求解[
]。回想一下几何学,[
]代表圆周的一半,2π代表整个圆。为了找到这个关系和值,让我们考虑一个半径为1个单位的单位圆。这个单位可以是英尺、米、秒差距,或者任何其他东西——这并不重要。然后,如果我们把这个圆放在一个边长为1个单位的正方形盒子里,我们可以看到以下情况:

单位圆在单位正方形内
根据前面的内容,我们知道我们有一个边长为2个单位或4平方单位的正方形,它覆盖了100%的区域。回到几何学,我们知道圆的面积由以下公式给出:[
]。知道圆在正方形内并且知道总面积,我们就可以应用蒙特卡洛方法来解决以下问题:

蒙特卡洛方法通过随机采样一个区域,然后确定该样本中有多少百分比是正确或错误的。回到我们的例子,我们可以将其想象为随机将飞镖扔到正方形上,然后计算有多少落在圆内。通过计算落在圆内的飞镖数量,我们可以使用以下公式回推π的值:

在前面的公式中,我们有以下内容:
-
ins:落在圆内的飞镖或样本的总数
-
total:扔下的飞镖总数
前面公式的关键是要意识到我们在这里所做的只是取落在圆内的飞镖数与总数(ins/total)的百分比来确定π的值。这可能仍然有点不清楚,所以让我们在下一节中看看几个例子。
实现蒙特卡洛
在许多情况下,即使没有现实世界的例子,理解抽象的简单概念也可能很困难。因此,打开 Chapter_3_1.py 代码示例。
在开始之前,我们应该提到,
,在这种情况下,指的是我们估计的实际值 3.14。
按照练习进行:
- 以下是为参考而列出的整个代码列表:
from random import *
from math import sqrt
ins = 0
n = 100000
for i in range(0, n):
x = (random()-.5) * 2
y = (random()-.5) * 2
if sqrt(x*x+y*y)<=1:
ins+=1
pi = 4 * ins / n
print(pi)
这段代码使用蒙特卡洛方法求解
,当你考虑到代码的简单性时,这相当令人印象深刻。让我们逐一分析代码的每个部分。
-
我们从
import语句开始,这里我们只导入了random和math函数中的sqrt。 -
从那里,我们定义了几个变量,
ins和n。ins变量存储飞镖或样本落在圆内的次数。n变量代表要投掷的迭代次数或飞镖数量。 -
接下来,我们使用以下代码随机投掷飞镖:
for i in range(0, n):
x = (random()-.5) * 2
y = (random()-.5) * 2
if sqrt(x*x+y*y)<=1:
ins+=1
-
所有这些代码所做的只是随机在
-1到1的范围内采样x和y的值,然后确定它们是否在半径为1的圆内,这是由平方根函数中的计算给出的。 -
最后,最后几行执行计算并输出结果。
-
按照常规方式运行示例并观察输出。
你可能会发现猜测可能有些偏差。这完全取决于样本的数量。你看,蒙特卡洛方法的置信度和因此答案的质量会随着样本数量的增加而提高。因此,为了改进最后的例子,你必须增加变量 n 的值。
在下一节中,我们再次查看这个例子,但这次我们将看看那些飞镖样本可能看起来是什么样子。
绘制猜测
如果你仍然难以理解这个概念,可视化这个例子可能更有帮助。如果你想要可视化这种采样看起来是什么样子,请运行下一节的练习:
- 在开始这个练习之前,我们将安装
matplotlib库。使用以下命令使用pip安装库:
pip install matplotlib
- 安装完成后,打开这里显示的
Chapter_3_2.py代码示例:
import matplotlib.pyplot as plt
from random import random
ins = 0
n = 1000
x_ins = []
y_ins = []
x_outs = []
y_outs = []
for _ in range(n):
x = (random()-.5) * 2
y = (random()-.5) * 2
if (x**2+y**2) <= 1:
ins += 1
x_ins.append(x)
y_ins.append(y)
else:
x_outs.append(x)
y_outs.append(y)
pi = 4 * ins/n
print(pi)
fig, ax = plt.subplots()
ax.set_aspect('equal')
ax.scatter(x_ins, y_ins, color='g', marker='s')
ax.scatter(x_outs, y_outs, color='r', marker='s')
plt.show()
-
代码与上一个练习非常相似,应该相当容易理解。我们只需关注前面突出显示的重要代码部分。
-
在这个例子中,最大的不同之处在于我们记住了飞镖落下的位置,并确定了它们是否落在圆内或圆外。之后,我们绘制结果。我们为每个点绘制一个点,并将落在圆内的点用绿色表示,落在圆外的点用红色表示。
-
运行示例并观察输出,如图所示:

来自 Chapter_3_2.py 的示例输出
输出看起来像一个圆圈,正如我们预期的那样。然而,π的输出值存在问题。注意现在π的估计值相当低。这是因为n——即飞镖或样本的数量——仅为1,000。这意味着,为了蒙特卡洛方法成为一个好的估计器,我们还需要意识到它需要一个足够大的猜测数量。
在下一节中,我们将探讨如何将这种方法应用于强化学习(RL)的FrozenLake问题的扩展版本。
添加强化学习(RL)
现在我们已经理解了蒙特卡洛方法,我们需要了解如何将其应用于强化学习(RL)。回想一下,我们的期望现在是我们所处的环境相对未知,也就是说,我们没有模型。相反,我们现在需要通过试错来开发一个算法来探索环境。然后,我们可以通过蒙特卡洛方法对所有这些不同的试验进行平均,并确定最佳或更好的策略。然后,我们可以使用这个改进的策略来继续探索环境以实现进一步的改进。本质上,我们的算法变成了一个探险者而不是规划者,这就是为什么我们现在将其称为智能体的原因。
使用术语智能体提醒我们,我们的算法现在是一个探险者和学习者。因此,我们的智能体不仅会探索,还会从探索中学习并改进。现在,这真的是人工智能。
除了之前在第一章“基于奖励的学习”中已经讨论过的探索部分,智能体仍然需要评估价值函数并改进策略。因此,我们在第二章“动态规划和贝尔曼方程”中讨论的大部分内容都将适用。然而,这一次,我们的智能体将不是进行规划,而是探索环境,然后在每个回合之后重新评估价值函数并更新策略。一个回合被定义为从开始到终止的完整移动集。我们称这种类型的学习为回合式学习,因为它指的是智能体只在回合结束后学习和改进。当然,这有其局限性,我们将在第四章“时序差分学习”中看到如何进行连续控制。在下一节中,我们将深入探讨代码以及这一切是如何工作的。
蒙特卡洛控制
在一个智能体上实现所谓的蒙特卡洛控制有两种方式。这两种方法的区别在于它们如何计算平均回报或样本均值。在所谓的首次访问蒙特卡洛中,智能体只在首次访问某个状态时采样均值。另一种方法,每次访问蒙特卡洛,每次访问状态时都会采样平均回报。后一种方法是我们将在本章代码示例中探讨的方法。
本例的原始源代码来自Ankit Choudhary的博客(https://www.analyticsvidhya.com/blog/2018/11/reinforcement-learning-introduction-monte-carlo-learning-openai-gym/)。
代码已经从原始版本中进行了大量修改。Ankit对这个方法的数学进行了更深入的探讨,对于那些对探索更多数学感兴趣的读者,原始版本是推荐的。
打开Chapter_3_3.py并按照练习进行:
-
打开代码并查看导入。本例的代码太大,无法内联显示。相反,代码已经被分成几个部分。
-
滚动到样本的底部并查看以下行:
env = gym.make('FrozenLake8x8-v0')
policy = monte_carlo_e_soft(env,episodes=50000)print(test_policy(policy, env))
-
在第一行,我们构建了环境。然后,我们使用名为
monte_carlo_e_soft的函数创建policy。我们通过打印出test_policy函数的结果来完成这一步。 -
滚动到
monte_carlo_e_soft函数。我们稍后会解释这个名字,但现在,显示的是顶部几行:
if not policy:
policy = create_random_policy(env)
Q = create_state_action_dictionary(env, policy)
returns = {}
- 这些行创建了一个策略,如果没有的话。这显示了随机策略是如何创建的:
def create_random_policy(env):
policy = {}
for key in range(0, env.observation_space.n):
p = {}
for action in range(0, env.action_space.n):
p[action] = 1 / env.action_space.n
policy[key] = p
return policy
- 之后,我们创建一个字典来存储状态和动作值,如下所示:
def create_state_action_dictionary(env, policy):
Q = {}
for key in policy.keys():
Q[key] = {a: 0.0 for a in range(0, env.action_space.n)}
return Q
- 然后,我们从一个
for循环开始,遍历所有剧集的数量,如下所示:
for e in range(episodes):
G = 0
episode = play_game(env=env, policy=policy, display=False)
evaluate_policy_check(env, e, policy, test_policy_freq)
- 将前面高亮的
display=False改为display=True,如下所示:
episode = play_game(env=env, policy=policy, display=True)
- 现在,在我们走得太远之前,看看代理是如何玩游戏可能会有所帮助。运行代码示例并观察输出。不要运行到完成——只需几秒钟或一分钟即可。确保在保存之前撤销你的代码更改:

代理玩游戏的示例输出
这张截图显示了代理探索扩展的8 x 8 FrozenLake环境的示例。在下一节中,我们将看看代理是如何玩游戏。
再次确保在继续之前撤销你的代码,并将display=True改为display=False。
玩FrozenLake游戏
代理代码现在正在玩游戏或探索环境,如果我们理解这段代码是如何运行的,这将很有帮助。再次打开Chapter_3_3.py并按照练习进行:
- 对于本节,我们只需要关注代理如何玩游戏。滚动到
play_game函数,如下所示:
def play_game(env, policy, display=True):
env.reset()
episode = []
finished = False
while not finished:
s = env.env.s
if display:
clear_output(True)
env.render()
sleep(1)
timestep = []
timestep.append(s)
n = random.uniform(0, sum(policy[s].values()))
top_range = 0
action = 0
for prob in policy[s].items():
top_range += prob[1]
if n < top_range:
action = prob[0]
break
state, reward, finished, info = env.step(action)
timestep.append(action)
timestep.append(reward)
episode.append(timestep)
if display:
clear_output(True)
env.render()
sleep(1)
return episode
- 我们可以看到这个函数接受
env和policy环境作为输入。然后,在内部,它使用reset重置环境并初始化变量。while循环的开始是代理开始玩游戏的地方:
while not finished:
-
对于这个环境,我们让代理无限期地玩游戏。也就是说,我们不会限制代理可能采取的步骤数量。然而,对于这个环境来说,这并不是问题,因为它很可能代理会掉入洞中。但这种情况并不总是如此,我们经常需要限制代理在环境中的步骤数量。在许多情况下,这个限制被设置为
100,例如。 -
在
while循环内部,我们更新代理的状态s,然后显示环境display=True。之后,我们设置一个timestep列表来保存那个state、action和value。然后,我们添加状态s:
s = env.env.s
if display:
clear_output(True)
env.render()
sleep(1)
timestep = []
timestep.append(s)
- 接下来,我们查看基于
policy值进行随机采样动作的代码,如下所示:
n = random.uniform(0, sum(policy[s].values())) top_range = 0
action = 0
for prob in policy[s].items():
top_range += prob[1]
if n < top_range:
action = prob[0]
break
- 这实际上是代理使用
random.uniform对策略进行均匀采样的地方,这是蒙特卡洛方法。均匀意味着采样在值之间是均匀的,并且如果是从正态或高斯方法中来的话,不会偏斜。之后,在for循环中根据策略中随机选择的项选择一个动作。记住,在开始时,所有动作可能具有相等的0.25概率,但后来,随着代理学习策略项,它也会相应地学习分布。
蒙特卡洛方法使用各种采样分布来确定随机性。到目前为止,我们广泛地使用了均匀分布,但在大多数现实世界环境中,通常使用正态或高斯采样方法。
- 然后,在选择一个随机动作后,代理采取一步并记录它。它已经记录了
state,现在它添加action和reward。然后,它将timestep列表添加到episode列表中,如下所示:
state, reward, finished, info = env.step(action) timestep.append(action)
timestep.append(reward)
episode.append(timestep)
- 最后,当代理
完成后,通过找到目标或掉入洞中,它返回episode中的步骤列表。
现在,我们理解了代理如何玩游戏后,我们可以继续评估游戏并优化它以预测和控制。
使用预测和控制
当我们之前有一个模型时,我们的算法可以学习在线下规划和改进策略。现在,没有模型,我们的算法需要成为一个代理,并学习探索,同时在这个过程中学习和改进。这使得我们的代理现在可以通过试错有效地学习。让我们回到Chapter_3_3.py代码示例,并跟随练习:
- 我们将从我们离开的地方开始,回顾最后几行包括
play_game函数:
episode = play_game(env=env, policy=policy, display=False)
evaluate_policy_check(env, e, policy, test_policy_freq)
- 在
evaluate_policy_check内部,我们测试是否达到了test_policy_freq数字。如果是,我们输出代理的当前进度。实际上,我们正在评估当前策略将如何运行代理。evaluate_policy_check函数调用test_policy来评估当前策略。test_policy函数如下所示:
def test_policy(policy, env):
wins = 0
r = 100
for i in range(r):
w = play_game(env, policy, display=False)[-1][-1]
if w == 1:
wins += 1
return wins / r
-
test_policy通过运行play_game函数并设置由r = 100确定的几个游戏来评估当前策略。这提供了一个wins百分比,该百分比输出以显示代理的进度。 -
回到主函数,我们进入一个
for循环,以相反的顺序遍历最后一轮游戏,如下所示:
for i in reversed(range(0, len(episode))):
s_t, a_t, r_t = episode[i]
state_action = (s_t, a_t)
G += r_t
-
以相反的顺序遍历场景允许我们使用最后一个奖励并将其反向应用。因此,如果代理收到了负奖励,所有动作都会受到负面影响。对于正奖励也是如此。我们使用
G变量跟踪总奖励。 -
在最后一个循环内部,我们检查状态是否已经为这个场景评估过;如果没有,我们找到回报列表并计算它们的平均值。然后,我们可以从平均值中确定最佳动作
A_star。这显示在代码块中:
if not state_action in [(x[0], x[1]) for x in episode[0:i]]:
if returns.get(state_action):
returns[state_action].append(G)
else:
returns[state_action] = [G]
Q[s_t][a_t] = sum(returns[state_action]) / len(returns[state_action])
Q_list = list(map(lambda x: x[1], Q[s_t].items()))
indices = [i for i, x in enumerate(Q_list) if x == max(Q_list)]
max_Q = random.choice(indices)
A_star = max_Q
-
这段代码块中有很多事情在进行,所以如果你需要的话,要慢慢工作。关键要点是我们在这里所做的只是平均回报或状态,然后根据蒙特卡洛在该状态内确定最可能的最佳动作。
-
在我们跳转到代码的最后部分之前,像平常一样运行示例。这应该会产生与以下类似的输出:

Chapter_3_3.py的示例输出
注意我们现在如何可视化代理在随机探索时的进度。你可能会看到的胜利百分比可能完全不同,在某些情况下,它们可能高得多或低得多。这是因为代理正在随机探索。要完全评估一个代理,你可能需要运行代理超过50,000个场景。然而,在添加新样本后,在50,000次迭代中不断平均平均值将非常计算量大。相反,我们使用另一种称为增量平均值的方法,我们将在下一节中探讨。
增量意味着
增量或运行平均值允许我们在不记住列表的情况下保持一系列数字的平均值。当然,当我们需要保持50,000、1,000,000或更多场景的平均值时,这具有巨大的好处。我们不是从完整的列表中更新平均值,而是对于每个场景,我们保持一个值,并使用以下方程增量更新:

在前面的方程中,我们有以下内容:
-
= 当前策略的状态值 -
= 代表折扣率 -
= 当前总回报
通过应用这个方程,我们现在有了更新策略的方法,而且巧合的是,我们在完整的Q方程中也使用了类似的方法。然而,我们还没有达到那里,而是使用以下算法来更新值:

蒙特卡洛ε-soft策略算法
该算法展示了e-soft或epsilon soft版本的蒙特卡洛算法是如何工作的。回想一下,这是我们使用蒙特卡洛定义代理的第二个方法。虽然前面的算法可能特别令人恐惧,但我们感兴趣的部分是最后一个,如以下方程所示:

这成为了一种更有效的策略更新方法,这正是示例中展示的。打开Chapter_3_3.py并遵循练习:
- 滚动到以下代码部分:
for a in policy[s_t].items():
if a[0] == A_star:
policy[s_t][a[0]] = 1 - epsilon + (epsilon / abs(sum(policy[s_t].values())))
else:
policy[s_t][a[0]] = (epsilong / abs(sum(policy[s_t].values())))
- 正是在这段代码的最后部分,我们逐步更新策略到最佳值,如下所示:
policy[s_t][a[0]] = 1 - alpha + (alpha / abs(sum(policy[s_t].values())))
- 或者我们给它一个基值,如下所示:
policy[s_t][a[0]] = (alpha / abs(sum(policy[s_t].values())))
- 从这里,我们可以再次运行示例并享受输出。
现在你已经了解了蒙特卡洛方法的基础,你可以继续阅读下一节中的更多示例练习。
练习
如往常一样,本节中的练习旨在提高你对材料的知识和理解。请尝试独立完成1-3个这些练习:
-
我们可以使用蒙特卡洛方法计算哪些其他常数,比如π?想想一个实验来计算我们使用的另一个常数。
-
打开
Chapter_3_1.py的示例代码并更改n的值,即投掷的飞镖数量。这如何影响π的计算值?使用n的更高或更低值。 -
当我们计算π时,我们假设飞镖是均匀分布的。然而,在现实世界中,飞镖可能以正态或高斯方式分布。这会如何影响蒙特卡洛实验?
-
参考示例
Chapter_3_2.py并更改n的值。这如何影响绘图生成?你能否修复它? -
打开
Chapter_3_3.py并更改在test_policy函数中运行的测试剧集数量到一个更高的或更低的值。 -
打开
Chapter_3_3.py并增加用于训练代理的剧集数量。如果有的话,代理的性能如何提高? -
打开
Chapter_3_3.py并更改用于更新增量平均值的alpha值。这如何影响代理的学习能力? -
添加在图表中可视化每个策略测试的能力。看看你是否能转移我们在示例
Chapter_3_2.py中创建图表的方式。 -
由于代码相当通用,请在另一个Gym环境中测试此代码。从标准的4 x 4 FrozenLake环境开始,看看它的表现如何。
-
想想这个示例中给出的蒙特卡洛方法可以如何改进。
这些练习不会花费太多额外的时间,并且它们可以极大地影响你对本书中材料的理解。请使用它们。
总结
在本章中,我们扩展了我们对强化学习(RL)的探索,并再次审视了试错方法。特别是,我们关注了蒙特卡洛方法如何作为一种从实验中学习的方式。我们首先看了一个蒙特卡洛方法计算π的示例实验。从那里,我们探讨了如何使用matplotlib可视化这个实验的输出。然后,我们查看了一个代码示例,展示了如何使用蒙特卡洛方法解决FrozenLake问题的一个版本。通过详细探索代码示例,我们揭示了智能体如何玩游戏,并通过这种探索学习改进策略。最后,我们通过理解智能体如何使用增量样本均值来改进策略,结束了本章的内容。
蒙特卡洛方法功能强大,但正如我们所学的,它需要基于短期的游戏玩法,而在现实世界中,一个正在工作的智能体需要在其控制过程中持续学习。这种学习形式被称为时序差分学习,这是我们将在下一章中探讨的内容。
第五章:时间差分学习
在我们之前关于强化学习历史的讨论中,我们涵盖了两个主要线索,试错和动态规划(DP),它们结合在一起推导出现代强化学习(RL)。正如我们在前面的章节中提到的,还有一个后来到达的第三线索,称为时间差分学习(TDL)。在本章中,我们将探讨TDL以及它是如何解决时间信用分配(TCA)问题的。从那里,我们将探讨TD与蒙特卡洛(MC)的不同之处以及它是如何演变成完整的Q-learning的。然后,我们将探讨在策略学习和离策略学习之间的差异,最后,我们将尝试一个新的RL环境示例。
对于本章,我们将介绍TDL以及它是如何改进我们在前几章中查看的先前技术的。以下是本章我们将涵盖的主要主题:
-
理解TCA问题
-
介绍TDL
-
将TDL应用于Q-learning
-
在Q-learning中探索TD(0)
-
运行离策略与在策略
本章详细介绍了TDL和Q-learning。因此,值得仔细回顾和理解这些材料。对基础材料的深入了解将有助于你后来的学习。
理解TCA问题
信用分配问题被描述为理解你需要采取哪些行动来获得最多的信用或,在RL的情况下,奖励。RL通过允许算法或智能体找到最大化奖励的最佳动作集来解决信用分配问题。在我们之前的所有章节中,我们都看到了如何使用DP和MC方法实现这一点的变体。然而,这两种先前的方法都是离线的,因此它们在执行任务时无法学习。
TCA问题与信用分配CA问题的区别在于它需要在时间上解决;也就是说,算法需要在时间步长中找到最佳策略,而不是在MC的情况下在事件后学习,或者在DP的情况下需要提前规划。这也意味着,一个在时间上解决CA问题的算法也能够或应该能够实时学习,即在任务进行过程中能够更新策略,而不是像我们在前面的章节中看到的那样在之前或之后。
通过引入时间或事件进展的概念,我们也允许我们的智能体学习事件时间的重要性。以前,我们的智能体对时间关键事件,如击中移动目标或准确计时跳跃,没有任何意识。另一方面,TDL允许智能体理解事件时间并采取适当的行动。我们将在下一节介绍TDL的概念和直觉。
介绍TDL
TDL是由强化学习之父理查德·萨顿博士在1988年提出的。萨顿将此方法作为MC/DP的改进,但正如我们将看到的,该方法本身导致了1989年由克里斯·沃特金斯(Chris Watkins)提出的Q-learning的发展。这种方法本身是无模型的,不需要在智能体学习之前完成一个回合。这使得这种方法在实时探索未知环境时非常强大,正如我们将看到的。
在我们深入探讨更新此方法的新数学方法之前,查看下一节中涵盖的所有方法的备份图可能会有所帮助。
自举和备份图
TDL可以通过近似给定先前经验更新的价值函数来在回合期间学习。这使得算法在回合中进行学习,并根据需要做出修正。为了进一步了解差异,让我们回顾以下图中DP、MC和TD的备份图的组合:

DP、MC和TDL的备份图
该图取自巴托(Barto)和萨顿(Sutton)的《强化学习导论》(2018年)。在图中,你可以看到我们之前两种方法,DP和MC,以及TDL。阴影区域(红色或黑色)表示算法的学习空间。也就是说,智能体在能够更新其价值函数和策略之前需要探索的区域。注意,随着图从DP到TDL的进展,阴影区域变得较小——也就是说,对于每个进步的算法,智能体在开始学习之前或在学习过程中需要探索的面积越来越少。正如我们将看到的,这允许智能体在学习完成回合之前就开始学习。
在我们查看代码之前,我们应该看看这种新的方法如何在下一节中修改我们的价值函数的数学。
应用TD预测
在整本书中,我们将探讨允许算法预测和控制智能体完成任务的方法。预测和控制是强化学习(RL)的核心,之前我们有两种方法分开。也就是说,它们要么在(DP)之前运行,要么在(MC)之后运行。现在,为了使智能体能够实时学习,我们需要一个在线更新规则,该规则将在指定的时间步之后更新价值函数。在TDL中,这被称为TD更新规则。
规则在此以方程形式展示:

在前面的方程中,我们有以下内容:
-
: 当前状态的价值函数 -
: 学习率α -
: 下一个状态的重奖 -
: 折扣因子 -
: 下一个状态的价值
因此,我们可以说当前状态的价值等于当前状态的价值加上alpha,乘以下一个奖励的总和,再加上一个折现因子,gamma,乘以下一个状态价值与当前状态价值的差。
为了更好地理解这一点,让我们在下一节中查看一个代码示例。
TD(0)或一步TD
在我们深入之前,我们应该确定的是,我们这里查看的方法是针对一步TD或我们所说的TD(0)。记住,作为程序员,我们从0开始计数,所以TD(0)本质上意味着一步TD。我们将在第5章中查看多步TD,探索SARSA。
现在,我们将查看使用一步TD在下一练习中的示例:
- 打开
Chapter_4_1.py源代码示例,如下所示:
import numpy as np
from tqdm import tqdm
import random
gamma = 0.5
rewardSize = -1
gridSize = 4
alpha = 0.5
terminations = [[0,0], [gridSize-1, gridSize-1]]
actions = [[-1, 0], [1, 0], [0, 1], [0, -1]]
episodes = 10000
V = np.zeros((gridSize, gridSize))
returns = {(i, j):list() for i in range(gridSize) for j in range(gridSize)}
deltas = {(i, j):list() for i in range(gridSize) for j in range(gridSize)}
states = [[i, j] for i in range(gridSize) for j in range(gridSize)]
def generateInitialState():
initState = random.choice(states[1:-1])
return initState
def generateNextAction():
return random.choice(actions)
def takeAction(state, action):
if list(state) in terminations:
return 0, None
finalState = np.array(state)+np.array(action)
if -1 in list(finalState) or gridSize in list(finalState):
finalState = state
return rewardSize, list(finalState)
for it in tqdm(range(episodes)):
state = generateInitialState()
while True:
action = generateNextAction()
reward, finalState = takeAction(state, action)
if finalState is None:
break
before = V[state[0], state[1]]
V[state[0], state[1]] += alpha*(reward + gamma*V[finalState[0], finalState[1]] - V[state[0], state[1]])
deltas[state[0], state[1]].append(float(np.abs(before-V[state[0], state[1]])))
state = finalState
print(V)
-
这是一个直接展示价值更新工作原理的代码示例,并且不使用RL环境。我们将重点关注的第一个部分是在导入之后初始化我们的参数。在这里,我们初始化学习率
alpha(0.5)、折现因子gamma(0.5)、环境大小gridSize(4)、状态terminations列表、actions列表,最后是episodes。动作代表移动向量,终止代表一个episod将终止的网格方块。 -
接下来,我们使用
numpy函数zeros将值函数V初始化为零。然后,我们使用Python列表推导式创建了三个列表,returns、deltas和states。这些列表用于后续检索,并注意这一点如何与动态规划技术相关。 -
然后,我们定义了一些效用函数,
generateInitialState、generateNextAction和takeAction。前两个函数是自解释的,但让我们专注于takeAction函数:
def takeAction(state, action):
if list(state) in terminations:
return 0, None
finalState = np.array(state)+np.array(action)
if -1 in list(finalState) or gridSize in list(finalState):
finalState = state
return rewardSize, list(finalState)
-
前面的函数接受当前
state和action作为输入。然后,它确定当前状态是否是终端状态;如果是,则返回。否则,它使用简单的向量数学计算下一个状态以获得finalState。 -
然后,我们进入开始episodic训练的
for循环。注意,尽管代理是episodically探索环境的,但由于环境有开始和结束,它仍然在时间上学习。也就是说,它在每个时间步之后学习。tqdm库是一个辅助枚举器,当我们在for循环中运行时,它会打印一个状态栏。 -
在
for循环开始时发生的第一件事是代理的状态被随机初始化。之后,它进入一个while循环,运行整个一个episodic。大部分代码都是自解释的,除了这里所示的价值更新方程的实现:
V[state[0], state[1]] += alpha*(reward + gamma*V[finalState[0], finalState[1]] - V[state[0], state[1]])
-
这段代码块是之前价值更新函数的实现。注意学习率
alpha和折现因子gamma的使用。 -
按照正常方式运行代码,并注意
Value函数的输出:

在第_4_1章中运行示例
现在,函数不是最优的,这更多与我们的训练或使用的超参数有关。我们将在下一节探讨调整超参数的重要性。
调整超参数
我们在上一个代码示例顶部调查的训练参数集合被称为超参数,这样命名是为了将它们与我们在深度学习中使用的正常参数或权重区分开来。我们尚未详细探讨深度学习,但了解为什么它们被称为超参数是很重要的。之前,我们围绕学习率和折扣因子进行了实验,但现在我们需要正式化它们并理解它们在方法和环境中的影响。
在我们最后的例子中,学习率(alpha)和折扣因子(gamma)都被设置为 .5。我们需要理解这些参数对训练的影响。让我们打开示例代码 Chapter_4_2.py 并跟随下一个练习:
-
Chapter_4_2.py几乎与上一个示例相同,除了几个细微的差异。其中第一个差异是我们在这里import matplotlib以便稍后能够查看一些结果。 -
请记住,你可以使用以下命令从 Python 控制台安装
matplotlib:
pip install matplotlib
-
我们使用
matplotlib来渲染我们训练努力的成果。随着我们继续阅读本书,我们将在后面探讨更高级的方法。 -
接下来,我们看到超参数 alpha 和 gamma 已经被修改为
0.1的值:
gamma = 0.1
rewardSize = -1
gridSize = 4
alpha = 0.1
-
现在,由于你经常对训练进行微调,你很可能只想一次只修改一个参数。这将使你能够更好地控制并理解参数可能产生的影响。
-
最后,在文件末尾,我们看到输出训练值或改变训练值的代码。回想一下我们之前创建的名为
deltas的列表。在这个列表中捕获了训练期间所做的所有 delta 或变化。这可以非常有助于可视化,正如我们将看到的:
plt.figure(figsize=(20,10))
all_series = [list(x)[:50] for x in deltas.values()]
for series in all_series:
plt.plot(series)
plt.show()
print(V)
-
这段代码只是遍历每个一集中训练期间所做的变化列表。我们期望的是,随着训练的进行,变化量将减少。减少变化量允许代理收敛到某个最优值函数和策略。
-
按照常规方式运行代码,注意值函数的输出已经发生了显著变化,但我们也可以看到代理的训练进展:

来自 Chapter_4_2.py 的示例输出
在图表上,你现在可以看到训练如何随时间收敛。图表表示的是一集中步骤的数量。注意,图表左侧的变化量或 delta 更大,因为一集中的步骤更少,然后随着时间的推移,随着步骤的增加而减少。这种收敛确保代理确实在使用提供的超参数进行学习。
调整超参数是深度学习和深度强化学习的基础。许多人认为调整实践是所有工作的核心,在大多数情况下,这是真的。你可能经常需要花费数天、数周或数月来调整单个网络模型的超参数。
随意探索调整超参数,看看每个参数对训练收敛的影响。在下一节中,我们将探讨TD如何与Q-learning结合。
将TDL应用于Q-learning
Q-learning被认为是最受欢迎且经常使用的强化学习基础方法之一。该方法本身由Chris Watkins于1989年作为其论文《从延迟奖励中学习》的一部分开发。Q-learning或更确切地说,深度Q-learning(我们将在第6章中介绍,即《使用DQN深入探索》),因其被DeepMind(谷歌)用于玩经典Atari游戏并优于人类而变得非常流行。Watkins所做的是展示了如何使用学习率α和折扣因子γ在状态-动作对之间应用更新。
这将更新方程改进为Q或状态-动作质量更新方程,如下公式所示:

在前面的方程中,我们有以下内容:
此方程允许我们根据学习到的未来状态-动作对来更新状态-动作对。它也不需要模型,因为算法通过试错进行探索,并且可以在一个回合中学习,因为更新是在回合中运行的。
此方法现在可以解决时间信用分配问题,我们将在下一节中查看一个代码示例。
探索Q-learning中的TD(0)
TDL对于第一步或TD(0)实际上简化为Q-learning。为了全面比较此方法与DP和MC,我们首先回顾Gym中的FrozenLake环境。打开示例代码Chapter_4_4.py并遵循练习:
- 代码的完整列表太大,无法展示。相反,我们将从导入部分开始分节审查代码:
from os import system, name
from time import sleep
import numpy as np
import gym
import random
from tqdm import tqdm
- 我们之前已经看到所有这些导入,所以这里没有什么新内容。接下来,我们将介绍环境初始化和输出一些初始环境变量的过程:
env = gym.make("FrozenLake-v0")
env.render()
action_size = env.action_space.n
print("Action size ", action_size)
state_size = env.observation_space.n
print("State size ", state_size)
- 这里也没有什么新内容。接下来,我们介绍Q表或质量表的概念,它现在以
状态-动作对的形式定义了我们的策略。我们通过将每个状态-动作对的质量设置为等于该状态下动作的总数(action-size)来设置这个值:
qtable = np.ones((state_size, action_size))/action_size
print(qtable)
- 接下来,我们可以看到超参数的一部分:
total_episodes = 50000
total_test_episodes = 100
play_game_test_episode = 1000
max_steps = 99
learning_rate = 0.7
gamma = 0.618
-
这里有两个新的参数,称为
play_game_test_episode和max_steps。max_steps决定了我们的算法在一个场景中可能运行的最大步数。我们这样做是为了限制智能体陷入可能的无穷循环。play_game_test_episode设置场景编号,以显示基于当前最佳 Q 表的智能体预览。 -
接下来,我们介绍一组全新的参数,这些参数必须处理探索和利用:
epsilon = 1.0
max_epsilon = 1.0
min_epsilon = 0.01
decay_rate = 0.01
-
回想一下,我们在 第 1 章 中讨论了 RL 中的探索与利用的困境,理解基于奖励的学习。在本节中,我们介绍了
epsilon、max_epsilon、min_epsilon和decay_rate。这些超参数控制智能体在探索时的探索率,其中epsilon是智能体在时间步长内探索的概率。最大和最小 ε 代表智能体探索的多少或多少的限制,decay_rate控制从时间步到时间步epsilon值衰减的程度。 -
理解探索与利用的困境对于 RL 是至关重要的,因此我们将在这里暂停,让你运行示例。以下是在 FrozenLake 环境中智能体玩游戏的示例:

来自 Chapter_4_4.py 的示例输出
观察智能体在后续场景(40,000 之后)如何玩游戏,你会发现智能体可以迅速在洞周围移动并找到目标,通常情况下。它可能不这样做的原因与探索/利用有关,我们将在下一节再次回顾。
重新审视探索与利用
在过去的几章中,我们一直假设我们的智能体是贪婪的。也就是说,它总是根据策略选择最佳行动。然而,正如我们所看到的,这并不总是提供最佳的最优奖励路径。相反,我们发现,通过允许智能体在早期随机探索,然后随着时间的推移减少探索的机会,学习效果有显著提高。除非环境太大或太复杂,智能体可能需要比小得多环境更多的探索时间。如果我们在一个小环境中保持高探索,我们的智能体就会浪费时间探索。这是你需要平衡的权衡,这通常与需要执行的任务相关。
在这个例子中,我们使用的探索方法是称为 ε-贪婪或 ε-greedy。之所以这样命名,是因为我们一开始以高 ε 值贪婪,然后随着时间的推移逐渐减少它。我们可以使用几种探索方法,这里展示了一些更常见的版本及其描述:
-
随机:这是一种始终随机的策略,可以作为一个有效的基线测试,在新环境中执行。
-
贪婪:这始终是在状态中采取贪婪或最佳动作。正如我们所见,这可能会产生不良后果。
-
ε-贪婪:ε-贪婪允许通过在每个时间步或回合期间通过一个因子减少ε(探索率)来平衡随时间变化的探索率。
-
贝叶斯或汤普森采样:这种方法利用统计学和概率来从一系列随机采样的动作中选择最佳动作。本质上,动作是从动作分布中选择出来的。例如,如果一个状态有一系列动作可供选择,那么每个袋子也会存储每个动作的先前奖励。通过从每个动作袋中随机选择一个动作并与所有其他选择进行比较,选择一个新的动作。选择最佳动作,即给出最高价值的动作。我们实际上并不存储所有先前动作的所有奖励,而是确定描述这些返回奖励的样本分布。
如果我们刚才讨论的统计学和概率概念对你来说很陌生,那么你应该参与一些关于这些主题的免费在线学习。这些概念将在后面的章节中反复深入探讨。
还有其他方法提供了额外的选项,用于选择Q-学习者的最佳动作的策略。然而,现在我们将坚持使用ε-贪婪,因为它相对简单易行且相当有效。
现在,如果你回到示例Chapter_4_4.py并仔细观察,你会看到智能体可能达到目标,也可能不会。事实上,FrozenLake环境比我们给予它的评价要危险得多。这意味着该环境中的奖励更加稀疏,并且通常需要相当长的时间才能通过试错技术进行训练。相反,这种学习方法在具有连续奖励的环境中表现更好。也就是说,当智能体在回合期间而不是仅在终止时收到奖励时。
幸运的是,Gym提供了大量的环境,我们可以玩这些环境,这将使我们获得更多的连续奖励,我们将在下一节中探索一个有趣的例子。
教智能体驾驶出租车
OpenAI Gym提供了许多有趣的环境,使我们能够轻松切换和测试新的环境。正如我们所见,这使我们能够更容易地比较算法的结果。然而,正如我们所见,各种算法都有局限性,本节中我们探索的新环境引入了时间的限制。也就是说,它将时间限制作为目标的一部分。通过这样做,我们之前的算法,DP和MC,无法解决这样的问题,这使得这是一个介绍基于时间或时间敏感奖励的好例子。
想要介绍时间依赖性学习,还有什么比考虑一个时间依赖性任务更好的方法呢?有很多任务,但其中一个效果很好的例子是出租车。也就是说,智能体是出租车司机,必须迅速接乘客并送他们到正确的目的地。在Gym Taxi-v2环境中,智能体的目标是接乘客到某个位置,然后将其送到目标。如果智能体成功送达,将获得+20分的奖励,每经过一个时间步长将扣除1分。因此,智能体需要尽可能快地接乘客并送达。
下面展示了智能体在这个环境中玩耍的一个示例:

来自Taxi-v2 Gym环境的示例输出
在截图上,汽车是绿色的,这意味着它已经从其中一个符号接过了一名乘客。当前的目标是让智能体将乘客送到指定的目标(符号)处,该目标被高亮显示。现在让我们回到代码,这次是Chapter_4_5.py,这是我们上一个示例的更新版本,现在使用的是Taxi-v2环境。
在打开代码后,请按照以下练习进行操作:
- 这个代码示例几乎与
Chapter_4_4.py相同,除了显示的环境初始化不同:
env = gym.make("Taxi-v2")
- 我们将使用之前相同的超参数,因此没有必要再次查看它们。相反,跳到下面的
play_game函数,如下面的代码块所示:
def play_game(render_game):
state = env.reset()
step = 0
done = False
total_rewards = 0
for step in range(max_steps):
if render_game:
env.render()
print("**...*****************")
print("EPISODE ", episode)
sleep(.5)
clear()
action = np.argmax(qtable[state,:])
new_state, reward, done, info = env.step(action)
total_rewards += reward
if done:
rewards.append(total_rewards)
if render_game:
print ("Score", total_rewards)
break
state = new_state
return done, state, step, total_rewards
play_game函数本质上使用的是qtable列表,这实际上是状态-动作对的生成策略。现在代码应该很熟悉了,一个需要注意的细节是智能体如何使用以下代码从qtable列表中选择动作:
action = np.argmax(qtable[state,:])
- 这里的
play_game函数扮演了我们之前提到的智能体测试函数的角色。这个函数将允许你在智能体训练过程中看到它玩游戏。这是通过将render_game设置为play_game为True来实现的。这样做可以让你可视化智能体在游戏中的一个回合。
你通常会想观察你的智能体是如何训练的,至少最初是这样。这可以为你提供关于实现中可能出现的错误或智能体在新环境中找到可能的作弊方法的线索。我们发现智能体是非常好的作弊者,或者至少能够很容易地找到作弊方法。
- 接下来,我们跳到下一个for循环,该循环遍历训练回合并训练
qtable。当经过play_game_test_episode设定的回合数阈值后,我们允许智能体玩一个可见的游戏。这样做可以让我们可视化整体训练进度。然而,重要的是要记住,这只是一个回合,智能体可能正在进行广泛的探索。因此,当观察智能体时,它们可能只是偶尔随机探索。代码展示了我们如何遍历回合:
for episode in tqdm(range(total_episodes)):
state = env.reset()
step = 0
done = False
if episode % play_game_test_episode == 0:
play_game(True)
for step in range(max_steps):
exp_exp_tradeoff = random.uniform(0,1)
if exp_exp_tradeoff > epsilon:
action = np.argmax(qtable[state,:])
else:
action = env.action_space.sample()
new_state, reward, done, info = env.step(action)
qtable[state, action] = qtable[state, action] + learning_rate * (reward + gamma * np.max(qtable[new_state, :]) - qtable[state, action])
state = new_state
if done == True:
break
epsilon = min_epsilon + (max_epsilon - min_epsilon)*np.exp(-decay_rate*episode)
- 首先,在情节循环内部,我们通过采样一个随机值并将其与epsilon进行比较来处理探索-利用困境。如果它大于贪婪动作,则选择该动作;否则,选择一个随机的探索性动作,如代码所示:
exp_exp_tradeoff = random.uniform(0,1)
if exp_exp_tradeoff > epsilon:
action = np.argmax(qtable[state,:])
else:
action = env.action_space.sample()
- 然后,下一行是执行所选动作的地方。之后,根据之前的Q-learning方程更新
qtable。这一行代码中发生了很多事情,所以请确保你理解它:
qtable[state, action] = qtable[state, action] + learning_rate * (reward + gamma * np.max(qtable[new_state, :]) - qtable[state, action])
- 之后,我们通过
done标志检查情节是否结束。如果是,我们终止并继续下一个情节。否则,我们使用以下代码更新epsilon的值:
epsilon = min_epsilon + (max_epsilon - min_epsilon)*np.exp(-decay_rate*episode)
- 最后,剩余的代码如下:
env.reset()
print(qtable)
for episode in range(total_test_episodes):
done, state, step, total_rewards = play_game(False)
env.close()
print ("Score over time: " + str(sum(rewards)/total_test_episodes))
-
最后一段代码重置并使用训练好的
qtable测试环境,进行total_test_episodes次,然后输出一个情节的平均得分或奖励。 -
最后,像平时一样运行代码,并仔细观察输出结果。特别关注在后续的情节中出租车如何接载和放下乘客。以下是训练过程中的示例输出:

样本输出来自Chapter_4_5.py
在这个例子中,你可以清楚地看到代理在训练中的进步,从什么都不做到迅速接载和放下乘客。实际上,代理在这个环境中会比在其他看似更简单的环境中(如FrozenLake)表现得更好。这更多与学习方法以及相关任务有关,这表明我们需要谨慎选择针对不同问题的方法。在某些情况下,你可能会发现某些高级算法在简单问题上的表现不佳,反之亦然。也就是说,像Q-learning这样的简单算法,当与其他技术结合时,可以变得非常强大,正如我们将在第6章“深入DQN”中看到的。
在本章的最后部分,我们将探讨如何改进之前的Q-learning方法。
运行离线策略与在线策略
当我们查看 第2章 中的 MC 训练时,我们之前已经讨论了 on-policy 和 off-policy 的术语,蒙特卡洛方法。回想一下,代理直到一个回合结束后才更新其策略。因此,这在上一个例子中将 TD(0) 学习方法定义为 off-policy 学习者。在我们的上一个例子中,可能看起来代理是在在线学习,但实际上它仍然在外部训练策略或 Q 表。也就是说,代理在学会做出决策和玩游戏之前需要建立策略。理想情况下,我们希望代理在玩一个回合的过程中学习或改进其策略。毕竟,我们不是离线学习,任何其他生物动物也不是。相反,我们的目标将是了解代理如何使用 on-policy 学习来学习。on-policy 学习将在 第5章 探索 SARSA 中介绍。
练习
随着我们这本书的进展,每章末尾的练习将更多地针对为你提供代理训练经验。训练 RL 代理不仅需要相当多的耐心,还需要对如何判断对错有直觉。这只有通过训练经验才能获得,所以请使用以下练习来学习:
-
打开示例
Chapter_4_2.py并将gridSize变量更改,以查看这将对收敛产生什么影响。 -
打开示例
Chapter_4_2.py并调整 alpha 和 gamma 的超参数。尝试找到两者的最佳值。这需要你多次运行示例。 -
打开示例
Chapter_4_2.py并更改回合数,增加或减少。看看大量回合,如 100,000 或 1,000,000,对训练有什么影响。 -
在示例
Chapter_4_4.py中调整learning_rate和gamma超参数。它们是否可以改进? -
从示例
Chapter_4_4.py中调整探索(epsilon、max_epsilon、min_epsilon和decay_rate)超参数。改变这些值会如何影响训练性能或缺乏性能? -
在示例
Chapter_4_5.py中调整learning_rate和gamma超参数。它们是否可以改进? -
从示例
Chapter_4_5.py中调整探索(epsilon、max_epsilon、min_epsilon和decay_rate)超参数。改变这些值会如何影响训练性能或缺乏性能? -
将跟踪训练期间 delta 或 Q 值变化的能力添加到示例
Chapter_4_4.py或Chapter_4_5.py中。回想一下,我们在示例Chapter_4_2.py中是如何跟踪和输出 delta 到图的。 -
将绘制训练性能图的能力添加到示例
Chapter_4_4.py或Chapter_4_5.py中。这需要你完成之前的练习。 -
使用示例
Chapter_4_5.py中的代码,并在其他环境中尝试 Q-learner。Gym 中的 Mountain Car 或 Cart Pole 环境很有趣,我们很快就会探索。
在你的RL训练生涯的这个阶段,了解超参数的工作方式非常重要。正确或错误的选择超参数可能会使实验成功或彻底失败。这给你留下了两个选择:阅读大量枯燥的数学诱导论文,或者只是动手实践。由于这是一本实践性书籍,我们期待你更倾向于后者。
摘要
在本章中,我们讨论了如何将强化学习(RL)的第三条线索——时序差分学习(Temporal Difference Learning)结合,以发展出TD(0)和Q-learning。我们首先探讨了时序信用分配问题及其与信用分配问题的区别。从那里,我们了解了TD学习的工作原理以及如何将TD(0)或第一步TD简化为Q-learning。
之后,我们再次在FrozenLake环境中进行实验,以了解新算法与我们的过去努力相比如何。使用无模型离策略Q-learning使我们能够解决更困难的出租车环境问题。这就是我们学习如何调整超参数,并最终查看离策略和在线策略学习之间的差异的地方。在下一章中,我们将继续探讨在线策略与离策略,当我们探索SARSA时。
第六章:探索SARSA
在本章中,我们继续关注时间差分学习(TDL),并从TD(0)扩展到多步TD以及更远。我们将研究一种新的强化学习(RL)方法,称为SARSA,探索它是什么,以及它与Q学习的区别。从那里,我们将查看一些来自Gym的新持续控制学习环境示例。然后,我们将更深入地理解TDL,并介绍称为TD lambda(λ)和资格痕迹的概念。最后,我们将通过查看SARSA的示例来完成本章。
对于本章,我们将扩展我们对TDL的讨论,并揭示状态-动作-奖励-状态-动作(SARSA),连续动作空间,TD(λ),资格痕迹和在线策略学习。以下是本章我们将涵盖的内容概述:
-
探索SARSA在线策略学习
-
使用SARSA与连续空间
-
扩展连续空间
-
使用TD(λ)和资格痕迹进行工作
-
理解SARSA(λ)
本章在很大程度上是第4章,时间差分学习的延续。请在阅读本章之前阅读那一章。在下一节中,我们将继续上一章结束的地方。
探索SARSA在线策略学习
SARSA,这是该方法模拟的过程。也就是说,算法通过移动到状态,然后选择一个动作,获得奖励,然后移动到下一个状态动作来工作。这使得SARSA成为一个在线策略方法,即算法通过使用相同的策略学习和决策。这与我们在第4章,时间差分学习中看到的Q作为离线策略学习器的方法不同。
以下图表显示了Q学习和SARSA的回溯图之间的差异:

Q和SARSA的回溯图
回想一下,我们的Q学习器是一个离线策略学习器。也就是说,它需要算法离线更新策略或Q表,然后从那里做出决策。然而,如果我们想解决超过一步或TD(0)的TDL问题,那么我们需要有一个在线策略学习器。我们的学习代理或算法必须能够在观察到的任何数量的TD步骤之间更新其策略。这也要求我们更新我们的Q更新方程,使用新的SARSA更新方程,如下所示:

回想一下,我们的Q学习方程是这样的:

在前面的方程中,我们有以下内容:
-
![当前状态-动作质量正在更新]()
-
![学习率]()
-
![下一个状态奖励]()
-
![伽马,折扣因子]()
-
最大最佳或贪婪动作
我们可以将这一点进一步可视化,如下面的图所示:

SARSA图和方程
注意,在SARSA中那个有趣的max项现在消失了,我们现在使用期望值而不是仅仅使用最佳值。这与动作选择策略有关。如果你还记得在Q-learning中,我们总是使用最大或最佳动作,根据平均奖励来选择。回想一下,Q-learning假设你平均最大奖励。相反,我们希望选择代理认为将返回最佳可能回报的动作。希望你也注意到了,我们是如何从谈论奖励到价值、状态动作、状态价值,再到现在的回报的,其中回报代表对动作的感知价值。我们将在本章后面更详细地讨论最大化回报。
在下一节中,我们将学习如何解决一种称为连续动作空间的新类型问题。然后,我们将探讨如何使用SARSA来解决一个新的Gym环境。
使用SARSA的连续空间
到目前为止,我们一直在探索有限马尔可夫决策过程或有限MDP。这类问题对于模拟和玩具问题来说都很好,但它们并没有展示我们如何解决现实世界的问题。现实世界的问题可以被分解或离散化为有限MDP,但现实问题并不是有限的。现实问题是无限的,也就是说,它们不定义像洗澡或吃早餐这样的离散简单状态。无限MDP在连续空间或连续动作空间中建模问题,也就是说,在我们将状态视为时间点的问题中,状态被定义为时间的切片。因此,吃早餐的离散任务可以分解为包括个别咀嚼动作的每个时间步。
使用我们当前的工具集解决无限MDP或连续空间问题并不简单,但我们需要应用离散化技巧。应用离散化或把连续空间分解成离散空间将使这个问题可以用我们的当前工具集解决。在第6章“深入DQN”,我们将探讨如何将深度学习应用于连续动作空间,这样我们就可以不使用这些离散化技巧来解决这些环境。
许多连续强化学习环境的环境状态比可观测宇宙中的原子还多,是的,这是一个非常大的数字。我们已经通过应用深度学习,从第6章“深入DQN”开始,解决了这些问题。
本章的代码最初来源于这个GitHub仓库:https://github.com/srnand/Reinforcement-Learning-using-OpenAI-Gym。看起来作者Shrinand Thakkar已经转向了其他追求,并且没有完成他原本打算完成的这项优秀的工作。
打开Chapter_5_1.py并遵循这里显示的练习:
- 列出的完整源代码如下:
import gym
import math
from copy import deepcopy
import numpy as np
import matplotlib.pyplot as plt
env = gym.make('MountainCar-v0')
Q_table = np.zeros((20,20,3))
alpha=0.3
buckets=[20, 20]
gamma=0.99
rewards=[]
episodes = 3000
def to_discrete_states(observation):
interval=[0 for i in range(len(observation))]
max_range=[1.2,0.07]
for i in range(len(observation)):
data = observation[i]
inter = int(math.floor((data + max_range[i])/(2*max_range[i]/buckets[i])))
if inter>=buckets[i]:
interval[i]=buckets[i]-1
elif inter<0:
interval[i]=0
else:
interval[i]=inter
return interval
def expect_epsilon(t):
return min(0.015, 1.0 - math.log10((t+1)/220.))
def expect_alpha(t):
return min(0.1, 1.0 - math.log10((t+1)/125.))
def get_action(observation,t):
if np.random.random()<max(0.001, expect_epsilon(t)):
return env.action_space.sample()
interval = to_discrete_states(observation)
return np.argmax(np.array(Q_table[tuple(interval)]))
def update_SARSA(observation,reward,action,ini_obs,next_action,t):
interval = to_discrete_states(observation)
Q_next = Q_table[tuple(interval)][next_action]
ini_interval = to_discrete_states(ini_obs)
Q_table[tuple(ini_interval)][action]+=max(0.4, expect_alpha(t))*(reward + gamma*(Q_next) - Q_table[tuple(ini_interval)][action])
for episode in range(episodes):
observation = env.reset()
t=0
done=False
while (done==False):
env.render()
print(observation)
action = get_action(observation,episode)
obs_next, reward, done, info = env.step(action)
next_action = get_action(obs_next,episode)
update_SARSA(obs_next,reward,action,observation,next_action,episode)
observation=obs_next
action = next_action
t+=1
rewards.append(t+1)
plt.plot(rewards)
plt.show()
- 跳过导入部分,我们将查看超参数初始化代码:
env = gym.make('MountainCar-v0')
Q_table = np.zeros((20,20,3))
alpha=0.3
buckets=[20, 20]
gamma=0.99
rewards=[]
episodes = 3000
-
我们从创建一个新的环境
MountainCar-v0开始,它位于连续空间环境中。然后我们看到Q_table表被初始化为全零。然后,我们设置了学习率alpha、折现因子gamma和剧集数episodes的值。我们还看到构建了一个新的列表buckets。我们将在稍后介绍buckets的作用。 -
从那里跳到代码的末尾。我们首先想要对代码的功能有一个高级概述。看看这里显示的剧集
for循环:
observation = env.reset()
t=0
done=False
while (done==False):
env.render()
print(observation)
action = get_action(observation,episode)
obs_next, reward, done, info = env.step(action)
next_action = get_action(obs_next,episode)
update_SARSA(obs_next,reward,action,observation,next_action,episode)
observation=obs_next
action = next_action
t+=1
rewards.append(t+1)
- 前面的代码是剧集循环代码,非常遵循我们在前几章中看到的模式。这里的一个主要不同点是算法/智能体似乎选择了两次动作,如下面的代码块所示:
action = get_action(observation,episode)
obs_next, reward, done, info = env.step(action)
next_action = get_action(obs_next,episode)
-
与Q-learning相比,这里的区别在于SARSA中的智能体是按策略的,也就是说,它选择的行为也需要决定其下一个行为。回想一下,在Q-learning中,智能体是离策略的,也就是说,它从一个之前学习过的策略中采取行动。同样,这也回到了TD(0)或一步,其中算法仍然只看一步。
-
在这个阶段,让我们运行算法来看看它是如何工作的。这里可以看到一些汽车爬山的示例:

来自Chapter_5_1.py的示例输出
从前面的截图,我们可以看到智能体正在爬山。让智能体继续爬山直到它到达旗帜;它应该几乎要到达那里了。现在,这很酷,而且相当强大,但考虑到我们可以通过假设我们的无限MDP(连续空间)可以在离散步骤中控制,因此是一个有限MDP,这一点更加突出。为了做到这一点,我们必须学习如何离散化连续的动作空间,我们将在下一节中看到如何做到这一点。
离散化连续状态空间
强化学习(RL)局限于离散空间,或者我们之前所学的有限马尔可夫决策过程(MDP)。一个有限MDP描述了一组离散的步骤或状态,以及一个通过概率决定在状态间移动的动作。这个过程的无限版本可能在任意一组状态之间定义无限数量的状态-动作。因此,一个篮球运动员从球场一端移动到另一端投篮,描述了一个无限MDP或连续空间。也就是说,对于每一个时间点,球员可能处于无限多个位置,运球或不运球,投篮等等。同样,在MountainCar环境中,汽车可以在任何时间点向上或向下移动,无论是哪个方向。这使得MountainCar环境成为一个连续状态空间,但仅仅是刚刚好。幸运的是,我们可以使用一个巧妙的技巧来离散化状态空间,如下所示:

山地车示例离散化
在前面的图中,我们在环境中叠加了一个网格,以表示小车可能处于的状态空间。对于这个示例,使用了一个4x4的网格,但在我们的代码中,我们将使用一个更大的网格。这样做可以让我们捕捉到小车在网格上的位置。由于这个任务的目标是将小车移动到山上,因此通过应用网格技术来离散化空间效果相当好。在更复杂的连续空间中,你的网格可能代表空间中的多个维度或其他特征。幸运的是,当我们学习如何将深度学习应用于连续空间时,我们不必担心那些复杂的数学问题。
现在我们已经了解了空间是如何离散化的,让我们回到Chapter_5_1.py中的示例代码,并回顾以下练习中它是如何工作的:
- 我们将从上次结束的地方开始。在上一个点,我们只是在事件
for循环中用以下行更新Q_table表:
update_SARSA(obs_next,reward,action,observation,next_action,episode)
- 这调用了一个名为
update_SARSA的函数,如下所示:
def update_SARSA(observation,reward,action,ini_obs,next_action,t):
interval = to_discrete_states(observation)
Q_next = Q_table[tuple(interval)][next_action]
ini_interval = to_discrete_states(ini_obs)
Q_table[tuple(ini_interval)][action]+=max(0.4, expect_alpha(t))*(reward + gamma*(Q_next) - Q_table[tuple(ini_interval)][action])
- 现在,忽略
Q_table更新代码,而是专注于高亮的to_discrete_states调用。这些调用接受一个观察值作为输入。观察值表示小车在x,y坐标中的绝对位置。这就是我们使用以下函数来离散化状态的地方:
def to_discrete_states(observation):
interval=[0 for i in range(len(observation))]
max_range=[1.2,0.07]
for i in range(len(observation)):
data = observation[i]
inter = int(math.floor((data + max_range[i])/(2*max_range[i]/buckets[i])))
if inter>=buckets[i]:
interval[i]=buckets[i]-1
elif inter<0:
interval[i]=0
else:
interval[i]=inter
return interval
to_discrete_states函数返回小车当前所在的网格区间。在update_SARSA函数中,我们使用以下方式将区间列表转换回一个元组:
tuple(interval)
- 再次运行示例,只是为了确认它按预期工作。
这种简单的离散化方法对于这个任务来说效果很好,但根据环境的复杂度,可能会迅速失效或变得过于复杂。在我们继续其他话题之前,我们想要回顾一下下一节中如何使用SARSA更新Q_table。
预期SARSA
在选择值方面,Vanilla SARSA与Q-learning非常相似。它通常会使用epsilon-greedy最大动作策略,这与我们之前使用的方法类似;然而,我们发现,尤其是在按策略工作的情况下,算法需要更加选择性地进行。现在,这确实是所有强化学习(RL)的目标,但在这个特定的情况下,我们通过引入期望来更好地管理这些权衡。当我们结合SARSA时,我们称之为期望SARSA。
在期望SARSA中,我们假设一个未知的学习率alpha,以及一个未知的探索率epsilon。相反,我们使用基于分配奖励的函数将学习率alpha和探索率epsilon等同起来。我们为每个时间步分配一个时间点奖励,然后根据这些计算新的alpha和epsilon。打开Chapter_5_2.py文件,让我们通过以下练习来查看这是如何工作的:
- 我们感兴趣的代码的两个函数在此处显示:
def expect_epsilon(t):
return min(0.015, 1.0 - math.log10((t+1)/220.))
def expect_alpha(t):
return min(0.1, 1.0 - math.log10((t+1)/125.))
-
这两个函数,
expect_epsilon和expect_alpha,根据到目前为止返回的奖励计算期望或比率,其中t等于小车在环境中移动的总时间。 -
我们可以通过查看此处显示的
get_action函数来关注expect_epsilon的使用:
def get_action(observation,t):
if np.random.random()<max(0.001, expect_epsilon(t)):
return env.action_space.sample()
interval = to_discrete_states(observation)
return np.argmax(np.array(Q_table[tuple(interval)]))
-
get_action函数根据观察到的信息(小车x和y位置)返回动作。它是通过首先检查是否要采样随机动作,或者相反,选择最佳动作来做到这一点的。我们通过使用expect_epsilon方程来确定这种概率,该方程根据在环境中玩的总游戏时间计算epsilon。这意味着在这个例子中,epsilon的范围将在0.001和0.0015之间;看看你是否能在代码中找出这一点。 -
接下来,我们做类似的事情来计算
update_SARSA函数中显示的alpha。再次显示使用此功能的单行:
Q_table[tuple(ini_interval)][action]+=max(0.4, expect_alpha(t))*(reward + gamma*(Q_next) - Q_table[tuple(ini_interval)][action])
-
之前的代码现在应该很熟悉了,因为它看起来像我们的常规策略更新方程,只是在这次实例中,我们使用基于当前任务时间的期望来调整
alpha的值。你也可以从某些方面将其视为一种次级奖励。 -
再次运行代码并让它完成。注意输出,因为我们很快会将其用作比较:

训练时间内的回报/奖励输出
图表显示了小车在环境中累积的奖励/时间。每次小车在环境中停留的时间片都会获得时间奖励,如果小车在几个时间片内保持静止或相对静止,则游戏结束。因此,小车在环境中停留的时间越长,也等于移动得越多。
在考虑实时问题时,我们不仅需要关注连续状态或连续观察。在现实世界中,我们还要处理连续动作空间。目前,我们一直在研究具有离散动作空间的问题,即使用任意离散动作来控制智能体的环境。这些动作通常是上、下、左和右。然而,对于现实世界,我们需要更精细的控制,通常将动作分类为左转x度或右转y度。通过添加连续动作空间,我们的强化学习算法变得更加灵活,提供了更精细的控制。将离散动作空间离散化到连续动作空间更困难,这不是我们关注的重点。相反,我们将探讨如何在下一节中转换另一个更流行的连续动作空间,该空间用于深度强化学习。
扩展连续空间
通常,我们会将具有大观察空间的问题留给深度学习来解决。正如我们将学习的,深度学习非常适合这类问题。然而,深度学习并非没有问题,有时尝试在没有深度学习的情况下解决环境是明智的。现在,并非所有环境都能很好地离散化,正如我们之前提到的,但我们确实想看看另一个例子。下一个我们将要查看的例子是臭名昭著的滑车杆环境,它几乎总是使用深度强化学习来解决,主要是因为它使用了一个具有四个维度的连续动作空间。记住,我们之前的观察空间只有一个维度,而在我们上一个例子中,我们只有两个维度。
能够转换智能体的观察空间可以是一个有用的技巧,尤其是在更抽象的游戏环境中。记住,好的游戏机制往往更注重乐趣而非准确性。这当然也适用于游戏中的一些AI元素。
如果环境有GitHub页面,你可以通过访问该页面找到观察和状态空间的详细信息。大多数更受欢迎的环境都有自己的页面。以下摘录显示了滑车杆和山地车的观察和动作空间:

山地车和滑车杆环境的空间
上述摘录显示了山地车与滑车杆环境的比较。这两个环境都使用离散动作空间,这是好的。然而,滑车杆环境使用了一个具有四个维度的观察空间,其值在截图中的表格中显示。这可能有点棘手,了解多维观察空间如何工作将非常有帮助。
打开 Chapter_5_3.py 并按照这个练习来查看我们的上一个例子是如何转换为滑车杆的:
- 大部分代码与最后两个示例相同,所以我们只需要查看差异。我们将从顶部的环境构建部分开始,如下所示:
env = gym.make('CartPole-v0')
-
这构建了臭名昭著的 Cart Pole 环境。再次强调,切换环境很容易,但你的代码必须适应观察和动作空间。Cart Pole 和 Mountain Car 具有相同的观察/动作空间类型。也就是说,其观察空间是连续的,但动作空间是离散的。
-
接下来,我们将查看并了解这如何影响我们在此处代码中的
Q_table表的初始化:
Q_table = np.zeros((20,20,20,20,3))
-
注意现在表格是如何配置为四个维度,每个维度大小为 20。之前,这仅仅是两个维度,每个维度大小为 20。如果你需要比较,请回过头去检查最后的代码示例。
-
随着
Q_table表中维度的增加,这也意味着我们需要在我们的离散化桶中添加更多维度,如下所示:
buckets=[20, 20, 20, 20]
-
再次,我们将
buckets数组从两个维度增加到四个,所有维度的大小都是20。我们任意使用大小为 20,但我们也可以使用更大的或更小的值。 -
我们最后需要做的是重新定义环境观察的边界。回想一下,我们能够从 GitHub 页面提取出这些信息。这是显示范围中最小/最大值的表格。我们感兴趣的代码行就在
to_discrete_states函数内部,如下所示:
def to_discrete_states(observation):
interval=[0 for i in range(len(observation))]
max_range=[2.4,999999, 41.8,999999]
-
这一行被突出显示并声明了
max_range变量。max_range设置观察空间中每个维度的最大值。我们用表中的值填充这个变量,在无穷大的情况下,我们使用六个 9(999999),这通常适用于具有无穷大的值的上限。 -
接下来,我们需要更新用于索引
Q_table表的轴维度,如下所示代码所示:
Q_table[:,:,:,:,action]+=lr*td_error*(eligibility[:,:,:,:,action])
-
在前面的代码中,请注意我们现在正在对四个维度和动作进行索引。
-
按照常规运行代码并观察输出;这里有一个示例:

示例:Chapter_5_3.py
最终,使用离散化观察空间的 SARSA 可以解决 CartPole 环境。这个可能需要一段时间来学习,所以请耐心等待,但智能体将学会在车上平衡杆子。你应该对离散化如何工作以及 TD (0) 中的 SARSA 有相当好的理解。在下一节中,我们将探讨向前/向后看超过一步的情况。
与 TD (λ) 和资格迹(eligibility traces)一起工作
到目前为止,我们一直关注的是前视或代理感知到的下一个最佳奖励或状态。在MC中,我们查看整个剧集,然后使用这些值来反向计算回报。对于Q学习、SARSA等TDL方法,我们查看单步前瞻或我们所说的TD(0)。然而,我们希望我们的代理能够提前考虑n步。如果我们能这样做,那么我们的代理肯定能够做出更好的决策。
如我们之前所见,我们可以使用折现因子gamma对步骤间的回报进行平均。然而,在这个阶段,我们需要更加小心地处理回报的平均或收集方式。相反,我们可以将所有回报在无限多个步骤前进行平均定义为以下内容:

在前一个方程中,我们有以下内容:
-
这是所有回报的加权平均值。 -
这是从t到t+n的单个剧集回报。 -
Lambda,一个介于[0,1]之间的权重值。
由于lambda小于1,随着n的增加,对最终平均回报的贡献量会逐渐减小。这是由于在前一个方程中将lambda(λ)提升到n的幂次所致。再次强调,这与使用折现因子gamma的原理相同。现在,当我们从n步或我们所说的lambda的角度思考时,我们可以回顾以下图表中的情况:

TD(λ)的进展
为了找到n时间步长的通用解,其中n是我们称为lambda(λ)的未知数,我们需要确定一个寻找lambda的通用解,即lambda的值可以泛化问题。我们可以通过首先假设任何剧集将在时间步长t结束,然后按照以下方式重写我们之前的方程来实现这一点:

当lambda的值为0时,这代表TD(0)。lambda的值为1代表MC或需要完整的剧集前瞻。然而,实现这种前瞻模型比较复杂,并且直观上看,前瞻只是生物动物学习过程中的一个非常小的部分。实际上,我们学习的主要来源是经验,这正是我们在下一节将要考虑的内容。
后视和资格痕迹
你还记得上一次你在地板或街上找到硬币的时间吗?在你捡起硬币后,你是否想过:a) "我知道低头看那么久会得到回报," 或 b) "哇,我找到了一个硬币,我是怎么做到的?" 事实上,在大多数情况下,会是选项 b,也就是说,我们学到某件事是好的,然后回想起我们是怎样发现它的。选项 a 中的精彩时刻类似于相信量子粒子、原子和细菌。在强化学习(RL)中,情况也是如此,我们发现回顾过去发生的事情通常更有用;然而,不要回溯得太远,以至于成为像蒙特卡洛(MC)那样的过去事件。
我们可以使用 TDL 来回顾几步的回报。然而,我们不能仅仅使用状态转换的绝对值。相反,我们需要使用以下方程确定每步回溯的预测误差:

在前面的方程中,我们有以下:
-
δₜ这是 TD 错误或 delta。 -
V(·)值函数,它可以进一步定义为以下:

我们可以进一步定义以下:


在前面的方程中,1(Sₜ = S)当状态处于 s. 时赋予完整的值 1。
E 表示资格因子或值在 TD 错误中应考虑的量。这里发生的情况是,值函数正在通过 n 步的 TD 错误数量进行更新,但我们不是向前看,而是向后看。就像强化学习(RL)中的所有事情一样,这似乎需要在算法的多个变体中应用。对于 n 步 TDL 或 TD (λ),我们有三个我们关注的变体。它们是表格式 TD (λ)、SARSA (λ) 和 Q (λ)。以下图表显示了伪代码中的每个算法变体:

TD (λ), SARSA (λ), 和 Q (λ)
每个算法在计算值和 TD 错误的方式上都有细微的差别。在下一节中,我们将查看 SARSA (λ) 的完整代码实现。
理解 SARSA (λ)
我们当然可以使用表格式在线方法实现 TD (λ),这是我们还没有覆盖的,或者使用 Q-learning。然而,由于这是关于 SARSA 的章节,我们继续这一主题是合情合理的。打开 Chapter_5_4.py 并遵循练习:
- 代码与我们的前例相当相似,但让我们回顾一下完整的源代码,如下所示:
import gym
import math
from copy import deepcopy
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
env = gym.make('MountainCar-v0')
Q_table = np.zeros((65,65,3))
alpha=0.3
buckets=[65, 65]
gamma=0.99
rewards=[]
episodes=2000
lambdaa=0.8
def to_discrete_states(observation):
interval=[0 for i in range(len(observation))]
max_range=[1.2,0.07]
for i in range(len(observation)):
data = observation[i]
inter = int(math.floor((data + max_range[i])/(2*max_range[i]/buckets[i])))
if inter>=buckets[i]:
interval[i]=buckets[i]-1
elif inter<0:
interval[i]=0
else:
interval[i]=inter
return interval
def expect_epsilon(t):
return min(0.015, 1.0 - math.log10((t+1)/220.))
def get_action(observation,t):
if np.random.random()<max(0.001, expect_epsilon(t)):
return env.action_space.sample()
interval = to_discrete_states(observation)
return np.argmax(np.array(Q_table[tuple(interval)]))
def expect_alpha(t):
return min(0.1, 1.0 - math.log10((t+1)/125.))
def updateQ_SARSA(observation,reward,action,ini_obs,next_action,t,eligibility):
interval = to_discrete_states(observation)
Q_next = Q_table[tuple(interval)][next_action]
ini_interval = to_discrete_states(ini_obs)
lr=max(0.4, expect_alpha(t))
td_error=(reward + gamma*(Q_next) - Q_table[tuple(ini_interval)][action])
Q_table[:,:,action]+=lr*td_error*(eligibility[:,:,action])
for episode in range(episodes):
observation = env.reset()
t=0
eligibility = np.zeros((65,65,3))
done=False
while (done==False):
env.render()
action = get_action(observation,episode)
next_obs, reward, done, info = env.step(action)
interval = to_discrete_states(observation)
eligibility *= lambdaa * gamma
eligibility[tuple(interval)][action]+=1
next_action = get_action(next_obs,episode)
updateQ_SARSA(next_obs,reward,action,observation,next_action,episode,eligibility)
observation=next_obs
action = next_action
t+=1
rewards.append(t+1)
plt.plot(rewards)
plt.show()
- 代码的上半部分与之前的例子相当相似,但有几个显著的不同之处。注意以下代码初始化了
MountainCar环境和Q_table表格设置:
env = gym.make('MountainCar-v0')
Q_table = np.zeros((65,65,3))
-
注意我们是如何在初始化
Q_table表时将离散状态的数量从20 x 20增加到65 x 65。 -
现在的主要区别是使用lambda计算可选性。我们可以在下面的代码中的底部
for循环中找到此代码,如下所示:
env.render()
action = get_action(observation,episode) next_obs, reward, done, info = env.step(action)
interval = to_discrete_states(observation)
eligibility *= lambdaa * gamma
eligibility[tuple(interval)][action]+=1
next_action = get_action(next_obs,episode)
updateQ_SARSA(next_obs,reward,action,observation,next_action,episode,eligibility)
observation=next_obs
action = next_action
t+=1
- 可选性的计算在突出显示的行中进行。注意我们是如何将
eligibility乘以lambda和gamma,然后为当前状态加一的。然后将此值传递到update_SARSA函数中,如下所示:
def updateQ_SARSA(observation,reward,action,ini_obs,next_action,t,eligibility):
interval = to_discrete_states(observation)
Q_next = Q_table[tuple(interval)][next_action]
ini_interval = to_discrete_states(ini_obs)
lr=max(0.4, expect_alpha(t))
td_error=(reward + gamma*(Q_next) - Q_table[tuple(ini_interval)][action])
Q_table[:,:,action]+=lr*td_error*(eligibility[:,:,action])
-
注意我们现在是根据
td_error和eligibility的确定来更新Q_table表的。换句话说,我们现在考虑信息的当前性和过去的价值。 -
按照常规再次运行代码示例,并观察智能体如何完成任务。此任务的训练输出如下所示:

SARSA(λ)的奖励输出图
生成前面图表所示的图形可能需要几分钟时间,所以请耐心等待。务必注意这与我们在本章中已经运行过的先前示例相比如何。你真的完成了所有的样本练习吗?注意每个时间段的回报/奖励输出是如何更快地增加并更快地收敛的。
我们想在下一个例子中查看一个更复杂的例子,将我们对离散化的使用推向极致。
SARSA lambda和Lunar Lander
随着我们开发的算法变得越来越复杂,它们的性能也越来越强大。然而,它们有局限性,了解任何技术的局限性都很重要。为了测试这些限制,我们想看看一个能够推动它们的例子。对于这个特定的情况,我们将查看Gym中的Lunar Lander环境。这个环境是根据同名经典街机游戏建模的,目标是将登月舱降落在月球表面。在这个环境中,观察空间由八个维度描述,动作空间由四个维度描述。我们将看到,这很快就会超出我们当前的计算限制。
LunarLander环境需要安装一个名为Box2D的特殊模块。这本质上是一个图形包。
按照下一节的练习来设置和运行Gym的高级Box2D模块:
- 按照以下步骤在Windows(Anaconda)上操作:
- 以管理员身份打开Anaconda Prompt。运行以下命令:
conda install swig
SWIG是Box2D的要求。
- 接下来,运行以下命令安装Box2D:
pip install box2d-py
- 按照以下步骤在Mac/Linux(或没有Anaconda的Windows)上操作:
- 打开Python shell并运行以下命令:
pip install gym[all]
- 如果遇到问题,请参阅Windows安装说明。
现在的安装允许你运行所有更高级的Box2D环境。这些环境在游戏性和训练上都要有趣得多。打开Chapter_5_5.py并按照练习设置和训练月球着陆器上的SARSA:
Chapter_5_5.py的源代码几乎与Chapter_5_4.py相同,除了在设置离散状态方面有一些细微的差异。我们将首先查看如何使用以下代码设置Q_table表:
env = gym.make('LunarLander-v2')
Q_table = np.zeros((5,5,5,5,5,5,5,5,4))
-
注意我们是如何从65步的值减少到5。最后一个值表示动作空间的大小,它从
MountainCar中的三个增加到LunarLander中的四个。然而,由于有八个维度,我们必须小心数组的尺寸。因此,在这个例子中,我们需要将每个步长限制为五。 -
接下来,我们初始化
buckets状态:
buckets=[5,5,5,5,5,5,5,5]
-
再次,初始化为三个大小,用于八个维度。
-
然后,我们设置
max_range值,这是我们想要我们的步骤跨越的最大值,如下所示:
max_range=[100,100,100,100,100,100,100,100]
-
我们在这里使用100这个值来表示某个任意的最大值。改变或调整这些值可能会提高训练效率。
-
接下来,我们需要扩展
Q_table索引以包括8个维度,如下所示:
Q_table[:,:,:,:,:,:,:,:,action]+=lr*td_error*(eligibility[:,:,:,:,:,:,:,:,action])
-
注意在这个例子中我们对代理施加的限制。我们实际上让代理以大块的方式观察,其中每个块或轴特征只分为三个部分。这个方法的有效性令人惊讶。
-
运行示例并让它完成。是的,这个会花一些时间,但这是值得的。以下是一个来自月球着陆器环境的示例输出:

来自Chapter_5_5.py的示例输出
在上一个例子中,我们简要地探讨了在另一个连续观察空间环境——月球着陆器上使用SARSA。虽然在这些环境中玩耍并观察我们的离散化如何适当地处理无限MDP很有趣,但现在是时候转向使用深度学习的强大工具来处理连续观察空间了。从奖励输出中我们可以看到,这个例子根本就没有收敛。这很可能是由于离散化不够精细;也许你可以在这方面有所改进?
在这个例子中,离散化过程并不最优,并且可以使用一些DP方法进行改进。
将深度学习网络应用于强化学习(RL)使我们能够处理巨大的连续观察和动作空间。因此,在未来的常规操作中,我们可能不需要经常进行空间离散化,但对于更简单的问题,它可能是一个有用的技巧或优势。
这完成了这一章,我鼓励你继续前进并探索练习,以提升你自己的学习。
练习
这些练习是为了让你使用和学习而提供的。至少尝试2-3个,你做得越多,后面的章节也会越容易:
-
在线策略代理和离线策略代理之间有什么区别?
-
调整本章中任何或所有示例的超参数,包括新的超参数
lambda。 -
在任何使用离散化的示例中更改离散化步骤,并观察它对训练的影响。
-
使用示例
Chapter_5_3.py,SARSA(0),并将其适配到另一个使用连续观察空间和离散动作空间的Gym环境。 -
使用示例
Chapter_5_4.py,SARSA(λ),并将其适配到另一个使用连续观察空间和离散动作空间的Gym环境。 -
代码中显示了一个未使用的超参数。它是哪个参数?
-
使用示例
Chapter_5_5.py,SARSA(λ),月球着陆器,并优化离散化以使其表现更佳。例如,你仍然受限于数组维度,但你可以增加或减少一些更重要的维度。 -
使用示例
Chapter_5_5.py,SARSA(λ),月球着陆器,并优化max_range值以使其表现更佳。例如,而不是将所有值都设置为999,检查是否某些值可以缩小或需要扩展。 -
更新一个示例以适应连续动作环境。这需要你将动作空间离散化。
-
将其中一个示例转换为Q-learning,即它使用离线策略。
随意探索更多内容。我们仅仅触及了这些方法复杂性的表面。最后,我们将在下一节中总结。
摘要
对于本章,我们继续探索TD学习。我们查看了一个名为SARSA的在线TD (0)方法的示例。然后,我们探讨了如何将观察空间离散化以解决更难的问题,但仍然使用相同的工具集。从那里,我们探讨了如何解决更难的连续空间问题,例如CartPole。之后,我们回顾了TDL,然后转向n步前瞻视角,认为这不够理想,然后转向后向视角和资格痕迹,这导致我们发现了TD (λ)、SARSA(λ)和Q (λ)。使用SARSA(λ),我们能够在远少于预期的时间内解决MountainCar环境。最后,我们想要使用SARSA(λ)而不使用深度学习来解决一个更困难的LunarLander环境。
在下一章中,我们将探讨引入深度学习,并提升自己成为深度强化学习者。
第七章:第2节:利用知识
在探索了强化学习的基础并使用它于玩具环境之后,是时候通过添加深度学习和其他几种高级方法,将我们的智能体提升到下一个层次了。
本节包含以下章节:
第八章:深度学习与DQN
在本章中,你将了解深度学习(DL),以便处理更新、更具挑战性的无限马尔可夫决策过程(MDP)问题。我们将涵盖一些与强化学习(RL)相关的DL基础知识,然后探讨如何解决Q学习问题。之后,我们将探讨如何构建深度Q学习或DQN代理来解决一些Gym环境。
下面是本章我们将涵盖的主题摘要:
-
深度学习在强化学习中的应用
-
使用PyTorch进行深度学习
-
使用PyTorch构建神经网络
-
在PyTorch中理解DQN
-
锻炼DQN
在本章中,我们介绍深度学习与强化学习的关系。将深度学习应用于深度强化学习(DRL)非常具体,这里没有详细涵盖。
深度学习在强化学习中的应用
在前五章中,我们学习了如何评估给定有限MDP的状态和动作的价值。我们学习了如何使用MC、DP、Q学习和SARSA等方法解决各种有限MDP问题。然后我们探讨了无限MDP或连续观察/动作空间问题,我们发现这类问题引入了计算限制,这些限制只能通过引入其他方法来克服,这就是深度学习介入的地方。
深度学习现在如此流行且易于获取,我们决定在这本书中只涵盖该主题的非常广泛的概述。任何认真想要构建DRL代理的人应该自己进一步学习深度学习。
对于许多人来说,深度学习(DL)涉及图像分类、语音识别,或者那个被称为生成对抗网络(GAN)的新潮事物。现在,这些都是深度学习的伟大应用,但本质上,深度学习是关于学习如何最小化损失或错误。因此,当将图像展示给网络以学习图像时,它首先被分割,然后输入到网络中。然后网络输出一个答案。答案的正确性被确定,任何错误都会作为学习方式被推回到网络中。这种将错误推回网络的方法被称为反向传播。
整个系统的基本原理如下所示:

深度学习过于简化
深度学习网络通过反向传播错误,作为对每个称为神经元的单元的修正来学习,神经元内部有称为权重的参数。每个权重控制着与该神经元的连接强度。网络中每个连接或权重的强度使用基于梯度下降的优化方法进行修改。梯度下降(GD)是一种源自微积分的方法,它允许我们计算每个连接/权重对答案的影响。反向工作,因此我们可以使用GD来确定每个权重需要的修正量。
使用GD(梯度下降)进行反向传播的主要缺点是训练或学习需要非常缓慢地进行。因此,可能需要向网络展示数千或数百万个图像,以便它能够学习。当我们将深度学习应用于强化学习时,这实际上效果很好,因为我们的试错学习方法也是迭代的。
DeepMind和其他公司目前正在研究除了用于深度学习网络的反向传播之外的其他学习方法。甚至有人谈论过进行一次学习。也就是说,只需用一个图像就能训练一个网络。这与我们人类学习的方式非常相似。
现在,人们常说深度学习可以有多种解释。作为强化学习从业者,我们对深度学习的兴趣在于将其用作方程求解器。你本质上会发现,深度学习做的就是解方程;无论是用于图像分类、语音翻译还是用于强化学习的方程。深度强化学习是应用深度学习来解决我们在前几章中看到的以及更多的学习方程。实际上,深度学习的加入也为学习提供了许多其他能力,我们将在本书的其余部分进行探讨。在下一节中,我们将概述用于DRL的常见深度学习框架。
DRL的深度学习框架
目前有多个深度学习框架可供使用,但只有少数在强化学习研究或项目中得到了广泛应用。这些框架之间有许多相似之处,因此从一个框架转移到另一个框架相对简单。在过去几年中,用于强化学习的三个最受欢迎的框架是Keras、TensorFlow和PyTorch。
下面这张表展示了每个框架的优缺点总结:
| 框架 | Keras | TensorFlow | PyTorch |
|---|---|---|---|
| 可访问性 | 最容易学习和使用 | 提供高级Keras接口和低级接口。 | 中级到低级接口 |
| 可扩展性 | 适用于小型项目 | 可扩展到任何规模的项目,并且支持的输出网络模型可以在许多不同的平台上运行。 | 适用于需要扩展的大型项目 |
| 性能/功率 | 简单的接口和有限的定制 | 强大且非常适合性能 | 优秀的性能,并提供最多的控制和额外的接口,用于定制开发 |
| 流行度 | 流行度下降 | 持续流行的框架,被认为是行业最佳。 | 流行度上升,尤其是在DRL应用中 |
如果你查看这张表,我们选择深度学习框架的明显选择将是PyTorch。基于Torch的PyTorch是一个相对较新的框架,但在短短几年内,它作为深度学习和深度强化学习框架已经获得了巨大的流行度。因此,它将成为我们本章选择的框架。在下一节中,我们将探讨如何开始使用PyTorch进行深度学习。
使用PyTorch进行深度学习
PyTorch 提供了构建深度学习网络/计算图的低级和中级接口。尽管我们构建深度学习系统时将其视为具有层中连接的神经元的网络,但神经网络的实际实现是通过计算图来完成的。计算图位于所有深度学习框架的核心,TensorFlow 也不例外。然而,Keras 从用户那里抽象掉了计算图的概念,这使得学习更容易,但并不像 PyTorch 那样提供灵活性。但在我们开始使用 PyTorch 构建计算图之前,让我们先在下一个练习中安装 PyTorch:
- 将您的浏览器导航到 pytorch.org, 并向下滚动到 运行此命令 部分,如下面的截图所示:

生成 PyTorch 安装命令
-
选择 稳定 版本,然后选择您的特定 操作系统(Linux、Mac 或 Windows)。接下来选择 包(Conda、Pip、LibTorch 或 源);我们在这里的首选是 Conda 用于 Anaconda,但如果您有其他经验,请使用它们。
-
接下来,选择 语言(Python 2.7、Python 3.5、Python 3.7 或 C++);对于我们来说,我们将使用 Python 3.6。
-
下一个选项 CUDA(9.2、10.0 或 None)确定您是否有能够运行 CUDA 的 图形处理单元(GPU)。目前,唯一受支持的 GPU 是由 NVIDIA 制造的。这种情况不太可能在短期内改变。对于我们来说,我们将使用 None。None 或 CPU 仅在 CPU 上运行,这较慢,但可以在大多数设备上运行。
-
在管理员权限下打开一个 64 位 Python 控制台。如果您使用 Conda,请以管理员身份启动窗口。
-
使用以下命令创建新的虚拟环境:
conda create -n gameAI python=3.6
- 这将使用 Python 3.6 创建虚拟环境。PyTorch 目前在 Windows 64 位上运行。这可能会根据操作系统而有所不同。然后使用以下命令激活环境:
activate gameAI
- 将之前生成的
install命令复制并粘贴到窗口中,并执行它。以下是在 Windows 上运行 Anaconda 的命令示例:
conda install pytorch torchvision cpuonly -c pytorch
- 此命令应安装 PyTorch。如果您遇到任何问题,例如显示库对 32 位不可用的错误,请确保您正在使用 64 位版本的 Python。
前面的过程将安装 PyTorch 以及我们现在需要的所有依赖项。如果您在安装框架时遇到问题,请检查在线文档或众多在线帮助论坛之一。在大多数情况下,通过确保您使用 64 位并作为管理员运行,可以解决安装问题。
本书的所有代码示例都已准备并使用Visual Studio Professional或Visual Studio Code进行测试,两者都安装了Python工具。VS Code是一个免费且跨平台的优秀编辑器。它是Python开发的相对新手,但受益于微软多年来在构建集成开发环境(IDEs)方面的经验。
在安装了PyTorch之后,我们可以在下一节中继续使用一个简单的示例,该示例创建一个计算深度学习图。
张量计算图
所有深度学习框架的核心概念是张量,或者我们通常认为的多维数组或矩阵。我们构建的计算图将使用各种操作在张量上工作,将输入线性地转换成最终输出。您可以将其视为一种流程,这也是TensorFlow命名的原因。在接下来的练习中,我们将使用计算PyTorch图构建一个两层深度学习网络,并训练该网络:
这里所涉及的概念假设读者已经理解了线性代数、矩阵乘法和线性方程组。因此,建议任何缺乏这些技能或需要快速复习的读者进行复习。当然,快速复习微积分也可能很有用。
- 打开
Chapter_6_1.py代码示例。该示例是从PyTorch快速入门手册中提取的,其中一些变量名被更改以更具上下文性:
import torch
dtype = torch.float
device = torch.device("cpu")
# device = torch.device("cuda:0") # Uncomment this to run on GPU
batch_size, inputs, hidden, outputs = 64, 1000, 100, 10
x = torch.randn(batch_size, inputs, device=device, dtype=dtype)
y = torch.randn(batch_size, outputs, device=device, dtype=dtype)
layer1 = torch.randn(inputs, hidden, device=device, dtype=dtype)
layer2 = torch.randn(hidden, outputs, device=device, dtype=dtype)
learning_rate = 1e-6
for t in range(500):
h = x.mm(layer1)
h_relu = h.clamp(min=0)
y_pred = h_relu.mm(layer2)
loss = (y_pred - y).pow(2).sum().item()
if t % 100 == 99:
print(t, loss)
grad_y_pred = 2.0 * (y_pred - y)
grad_layer2 = h_relu.t().mm(grad_y_pred)
grad_h_relu = grad_y_pred.mm(layer2.t())
grad_h = grad_h_relu.clone()
grad_h[h < 0] = 0
grad_layer1 = x.t().mm(grad_h)
layer1 -= learning_rate * grad_layer1
layer2 -= learning_rate * grad_layer2
- 我们首先使用
import torch导入PyTorch库。然后设置我们首选的数据类型dtype变量为torch.float。接着通过使用torch.device并传入cpu来初始化设备变量,表示仅使用CPU。示例中保留了启用CUDA在GPU上运行的选项,但安装CUDA由您自行决定:
batch_size, inputs, hidden, outputs = 64, 1000, 100, 10
- 接下来,我们设置一些变量来定义数据的处理方式和网络的架构。
batch_size参数表示在一次迭代中要训练的项目数量。inputs变量表示输入空间的大小,而hidden变量代表网络中隐藏或中间层的神经元数量。最后的outputs变量表示输出空间或网络输出层的神经元数量:
x = torch.randn(batch_size, inputs, device=device, dtype=dtype)
y = torch.randn(batch_size, outputs, device=device, dtype=dtype)
- 之后,我们设置了输入和输出变量:
x作为输入,y作为输出,这些变量将基于batch_size的随机采样进行学习。在这个例子中,inputs变量的大小为1,000,因此批次的每个元素都将有1,000个输入用于x。输出有10个值,因此每个y的样本也将有10个项目:
layer1 = torch.randn(inputs, hidden, device=device, dtype=dtype)
layer2 = torch.randn(hidden, outputs, device=device, dtype=dtype)
-
这两条线创建了我们深度学习网络中的计算层,这些层是由我们之前的
inputs、hidden和outputs参数定义的。此时,layer1和layer2的张量内容包含一个初始化的随机权重集,其大小由输入数量、隐藏层和输出数量设置。 -
您可以通过在层设置之后的行设置断点,然后在 Visual Studio Code 或 Professional 中以调试模式 F5 运行文件来可视化这些张量的大小。当断点被触发时,您可以使用鼠标悬停在变量上以查看有关张量的信息,如下面的截图所示:

检查层权重张量的大小
-
注意第一层的维度是 1000 x 100,第二层的维度是 100 x 10。在计算上,我们通过乘以第一层的权重来转换输入,然后将结果输出到第二层。在这里,第二层的权重乘以第一层的输出。我们很快就会看到这是如何工作的。
-
接下来,我们定义一个
learning_rate参数,或者我们现在将明确地称之为超参数。学习率是一个乘数,我们可以用它来缩放学习的速率,并且与之前探索的学习率 alpha 没有区别:
learning_rate = 1e-6
在深度学习中,我们经常使用 weight 和 parameter 这两个术语来表示相同的意思。因此,其他参数,如 learning_rate、epoch、批量大小等,将被描述为超参数。学习调整超参数将是构建深度学习示例的持续旅程。
- 在我们进入训练循环之前,让我们运行示例并观察输出。像平常一样运行示例,无论是以调试模式还是非调试模式。此示例的输出如下截图所示:

示例输出 Chapter_6_1.py
输出基本上显示了错误损失如何在训练迭代中下降。在第 99 次迭代时,我们可以看到在前面的例子中错误大约为 635,但通过第 499 次迭代下降到几乎为零。尽管输入和输出都是随机的,我们仍然可以看到网络学会了在数据中识别模式,从而减少错误。在下一节中,我们将更详细地探讨这种学习是如何进行的。
训练神经网络 – 计算图
为了训练一个网络或计算图,我们首先需要给它输入数据,确定图认为的答案,然后通过反向传播迭代地纠正它。让我们回到 Chapter_6_1.py 代码示例,并跟随下一个练习来学习训练是如何进行的:
- 我们将从以下代码中显示的
for循环开始的训练循环的开始处开始。
for t in range(500):
h = x.mm(layer1)
h_relu = h.clamp(min=0)
y_pred = h_relu.mm(layer2)
- 在这个例子中,500表示总的训练迭代次数或周期数。在每个迭代中,我们使用接下来的三行计算预测输出。这一步被称为通过图或网络的正向传递。第一行使用
x.mm对layer1权重与x输入进行矩阵乘法。然后,它将这些输出值通过一个名为clamp的激活函数。clamp为网络的输出设置限制,在这种情况下,我们使用clamp限制为0。这也恰好对应于修正线性单元或ReLU函数。
在深度学习中,我们使用许多不同形式的激活函数。ReLU函数目前是较为流行的函数之一,但在这本书的整个过程中,我们还会使用其他函数。
-
在ReLU函数激活输出之后,它随后与第二层权重
layer2进行矩阵乘法。这个结果的输出是y_pred,它是一个包含输出预测的张量。 -
从那里,我们使用以下代码预测我们想要实际预测的
y张量与我们的网络刚刚预测的张量y_pred之间的损失或误差量:
loss = (y_pred - y).pow(2).sum().item()
if t % 100 == 99:
print(t, loss)
-
使用均方误差或MSE方法计算
loss值或总误差。请注意,由于y_pred和y都是张量,减法操作是在张量范围内进行的。也就是说,所有10个预测值都从预测的y值中减去,然后平方并求和。我们在这里使用相同的输出技术来打印出每99次迭代的总损失。 -
在计算损失之后,我们接下来需要计算图权重的梯度,以确定如何推动和纠正图中的错误。计算这个梯度超出了本书的范围,但代码如下所示:
grad_y_pred = 2.0 * (y_pred - y)
grad_layer2 = h_relu.t().mm(grad_y_pred)
grad_h_relu = grad_y_pred.mm(layer2.t())
grad_h = grad_h_relu.clone()
grad_h[h < 0] = 0
grad_layer1 = x.t().mm(grad_h)
- 我们在这里展示低级代码,以GD(梯度下降)对抗一个简单的网络图为例,来说明数学是如何工作的。幸运的是,自动微分让我们大部分时间可以忽略那些更精细、更痛苦细节。这里计算出的梯度现在需要使用以下代码应用到图层的权重上:
layer1 -= learning_rate * grad_layer1
layer2 -= learning_rate * grad_layer2
-
注意我们再次使用张量减法来减去按学习率缩放的已计算的梯度
grad_layer1和grad_layer2。 -
再次运行示例,你应该会看到类似的输出。尝试调整
learning_rate超参数以查看它对训练有什么影响可能会有所帮助。
之前的例子是低级地查看我们如何实现一个表示两层神经网络的计算图。虽然这个例子旨在向您展示实际操作中事物是如何工作的内部细节,但我们将使用PyTorch的高级神经网络子集来构建图。我们将在下一节中看到如何构建一个示例。
使用Torch构建神经网络
在最后一节中,我们探讨了构建类似于神经网络的计算图。这正如你所预期的那样是一个相当常见的任务。如此之多,以至于PyTorch以及大多数深度学习框架都提供了构建深度学习图的辅助方法、类和函数。Keras本质上是一个围绕TensorFlow的包装器,只做这件事。因此,在本节中,我们将使用PyTorch中的神经网络辅助函数重新创建上一节练习的例子。打开Chapter_6_2.py代码示例,并跟随下一个练习:
- 整个示例的源代码如下:
import torch
batch_size, inputs, hidden, outputs = 64, 1000, 100, 10
x = torch.randn(batch_size, inputs)
y = torch.randn(batch_size, outputs)
model = torch.nn.Sequential(
torch.nn.Linear(inputs, hidden),
torch.nn.ReLU(),
torch.nn.Linear(hidden, outputs),
)
loss_fn = torch.nn.MSELoss(reduction='sum')
learning_rate = 1e-4
for t in range(500):
y_pred = model(x)
loss = loss_fn(y_pred, y)
if t % 100 == 99:
print(t, loss.item())
model.zero_grad()
loss.backward()
with torch.no_grad():
for param in model.parameters():
param -= learning_rate * param.grad
- 代码变得大大简化,但并没有到无法让我们控制深度学习图内部结构的地步。这可能是你直到与其他深度学习框架一起工作时才完全欣赏的东西。然而,推动PyTorch成为深度学习领域第一框架的不是简单性,而是灵活性:
model = torch.nn.Sequential(
torch.nn.Linear(inputs, hidden),
torch.nn.ReLU(),
torch.nn.Linear(hidden, outputs),
)
- 代码的上部有一些重大变化,最值得注意的是使用
torch.nn.Sequential设置模型。这个模型或图的设置与我们之前做的是一样的,只是更明确地描述了每个连接点。我们可以看到第一层是用torch.nn.Linear定义的,它以inputs和hidden作为参数。这连接到激活函数,再次是ReLU,由torch.nn.ReLU表示。之后,我们使用hidden和outputs作为参数创建最终的层。模型的Sequential术语表示整个图是完全连接的;就像我们在上一个例子中看到的那样:
loss_fn = torch.nn.MSELoss(reduction='sum')
- 在模型定义之后,我们还可以看到我们的
loss_fn损失函数通过使用torch.nn.MSELoss作为函数变得更加描述性。这让我们明确知道loss函数是什么以及它将如何被减少,在这种情况下,减少总和,用reduction='sum'表示,或者平均平方误差的总和:
for t in range(500):
y_pred = model(x)
- 训练循环的开始与之前相同,但这次
y_pred是从将整个x批次输入到model中获得的。这个操作与正向传播相同,或者说是网络输出答案的地方:
loss = loss_fn(y_pred, y)
- 之后,我们使用
loss_fn函数计算loss作为Torch张量。接下来的代码片段与之前看到的相同损失输出代码:
model.zero_grad()
loss.backward()
- 接下来,我们在模型中清除任何梯度——这本质上是一个重置。然后我们使用
backward函数计算损失张量中的梯度。这本质上是我们之前看到的那个讨厌的代码片段,现在已经被简化为单行:
with torch.no_grad():
for param in model.parameters():
param -= learning_rate * param.grad
-
我们通过调整模型中的权重来结束训练,使用计算出的
loss张量的梯度。虽然这段代码比我们之前的例子更冗长,但它更好地解释了实际的学习过程。 -
按照你之前的方式运行示例,你应该会看到与
Chapter_6_1.py示例中非常相似的结果。
您注意到第二个示例中的 learning_rate 变量略低吗?这是因为神经网络模型在几个方面略有不同,包括每个神经元使用另一个称为 偏置 的权重。如果您想了解更多关于偏置的信息,请确保选择一门好的深度学习课程。
在对如何使用 PyTorch 有良好的基本理解之后,我们现在将探讨如何将我们的知识应用到下一节中的强化学习。
在 PyTorch 中理解 DQN
深度强化学习之所以突出,是因为将 Q-learning 与深度学习相结合的工作。这种组合被称为深度 Q-learning 或 DQN(深度 Q 网络)。该算法为一些 DRL 的尖端示例提供了动力,当谷歌 DeepMind 在 2012 年使用它使经典 Atari 游戏比人类表现得更好时。这个算法有许多实现,谷歌甚至为其申请了专利。目前的共识是,谷歌为了阻止专利勒索者攻击小公司或开发使用 DQN 的商业应用程序而申请了这样一个基础算法。谷歌不太可能行使这项法律权利,或者由于这个算法不再被认为是尖端技术,因此不太可能需要这样做。
专利勒索是一种做法,其中一家往往不太道德的公司会为任何和所有类型的发明申请专利,只是为了确保专利。在许多情况下,这些发明甚至不是由公司发明的,但它们高效的专利流程使它们能够以较低的成本获得知识产权。这些勒索者通常主要在软件领域工作,因为软件初创公司和该领域的其他创新者往往忽视申请专利。谷歌和其他大型软件公司现在会不遗余力地申请这些专利,但本质上暗示他们永远不会执行这些专利。当然,他们可能会改变主意——只需看看 Java。
DQN 就像是 DRL 的 Hello World,几乎每本关于这个主题的书籍或课程都会有一个演示版本。我们在这里将要查看的版本遵循标准模式,但以一种展示我们之前在 TD 和时间信用分配方面的学习成果的方式分解。这将提供使用深度学习和不使用深度学习之间的良好比较。在接下来的章节中,我们将学习如何设置和运行一个 DQN 模型。
刷新环境
新用户感到沮丧的主要原因往往是仅仅设置示例。这就是为什么我们想要确保您的环境已经安装了适当的组件。如果您还记得,您应该已经创建了一个名为 gameAI 的新虚拟环境。我们现在将安装其他我们为下一个练习所需的模块:
-
您现在应该在与新虚拟环境中工作。因此,我们将需要再次安装 Gym 和其他所需的组件。
-
确保您已安装了最新的Windows C++编译器。有关支持的编译器列表,请查看此网站:https://wiki.python.org/moin/WindowsCompilers.
-
首先,让我们安装Windows所需的库,记住这些步骤仅适用于Windows安装,以下命令:
conda install swig
pip install box2d-py
- 在Windows上安装了所有必备条件后,您可以使用安装Gym的命令来完成剩余的安装工作。这个命令在Mac和Linux的安装中也会用到:
pip install gym[all]
- 接下来,使用以下命令安装
matplotlib和tqdm:
pip install matplotlib
pip install tqdm
- 回想一下,那些是我们用来监控训练的辅助库。
安装了这些包之后,请确保您的IDE配置为指向gameAI虚拟环境。您的特定IDE将提供如何做到这一点的说明。在下一节中,我们将探讨一些允许我们使用深度学习解决无限MDP,如DQN的假设。
部分可观察马尔可夫决策过程
我们已经看到,我们可以通过将其离散化成桶来处理连续或无限的观察空间。这工作得很好,但正如我们所看到的,它在处理观察状态空间的巨大问题时并不容易扩展。通过引入深度学习,我们可以有效地增加我们的状态空间输入,但远未达到我们需要的程度。相反,我们需要引入部分可观察马尔可夫决策过程(POMDP)的概念。也就是说,我们可以将任何无限MDP问题视为部分可观察的,这意味着智能体或算法只需要观察局部或观察到的状态来采取行动。如果你这么想,这正是你与环境互动的方式。你可能将你的日常活动视为对全局无限MDP的部分可观察观察,或者说,是宇宙。而你的日常行动和决策发生在更高的可观察状态,你只能对整个全局无限MDP有一个部分可观察的视角。
能够在不同的部分可观察视角之间切换同一无限MDP的概念是许多研究的核心。目前,有两个主要的DRL分支正在解决此问题。它们是分层强化学习(HRL),它试图将问题描述为MDP层次结构的起点。另一个分支被称为元强化学习(MRL),它采取更广泛的方法,试图在不同的时间步长中学习部分可观察性。通过引入时间序列,我们还可以开始使用其他形式的神经网络,称为循环神经网络,它们可以学习时间。我们将在第14章中重新探讨MRL,从DRL到AGI。
在下一节中,我们将最终探讨如何使用PyTorch构建DQN。
构建DQN
你可能可以在每个深度学习框架中找到一个DQN的版本。这个算法本身在学习方面是一个了不起的成就,因为它现在允许我们学习连续或无限的空间/无限马尔可夫决策过程。打开Chapter_6_DQN.py,按照下一个练习来构建DQN示例:
这个示例的源代码最初是从这个GitHub仓库中提取的:https://github.com/higgsfield/RL-Adventure/blob/master/1.dqn.ipynb。为了与本书中的先前示例相匹配,它已经进行了重大修改。
- 到目前为止,样本已经变得太大,无法在一个列表中列出。相反,我们将像往常一样分部分进行。通常,如果你在编辑器中跟随代码,这会有所帮助:
import math, random
import torch
import torch.nn as nn
import torch.optim as optim
import torch.autograd as autograd
import torch.nn.functional as F
import matplotlib.pyplot as plt
import gym
import numpy as np
from collections import deque
from tqdm import trange
- 这些是常见的导入,但应该指出的是,
torch需要在导入gym或numpy等其他导入之前加载。我们将跳过第一个ReplayBuffer函数,直到稍后:
env_id = "CartPole-v0"
env = gym.make(env_id)
epsilon_start = 1.0
epsilon_final = 0.01
epsilon_decay = 500
eps_by_episode = lambda epoch: epsilon_final + (epsilon_start - epsilon_final) * math.exp(-1\. * epoch / epsilon_decay)
plt.plot([eps_by_episode(i) for i in range(10000)])
plt.show()
- 前面的代码显示了创建强化学习环境和设置超参数的典型设置。注意我们是如何使用lambda表达式生成
eps_by_episode来生成衰减的epsilon的。这是一种非常Pythonic的方式来产生衰减的epsilon。代码的最后几行将衰减的epsilon绘制在图表中,并输出类似于以下图表的内容:

展示训练时期望epsilon衰减的图表
- 你可以从前面的图中看到,epsilon在大约2,000次迭代时稳定在某个水平;这似乎表明代理那时应该已经学到了足够的知识。我们现在将向下滚动到函数外的下一块代码:
model = DQN(env.observation_space.shape[0], env.action_space.n)
optimizer = optim.Adam(model.parameters())
replay_buffer = ReplayBuffer(1000)
- 这三行代码设置了关键组件——模型,它是
DQN类型的,以及我们很快就会接触到的类。在这种情况下,优化器是Adam类型的,由optim.Adam定义。最后一行创建了ReplayBuffer,这是我们很快就会接触到的另一个类。我们再次向下滚动,跳过函数中的所有代码,并回顾主代码的下一部分:
episodes = 10000
batch_size = 32
gamma = 0.99
losses = []
all_rewards = []
episode_reward = 0
state = env.reset()
tot_reward = 0
tr = trange(episodes+1, desc='Agent training', leave=True)
- 到现在为止,大部分代码应该看起来很熟悉。注意我们现在设置了一个新的超参数,称为
batch_size。基本上,batch_size是我们一次通过网络推送的项目数量的大小。我们更喜欢批量处理,因为这提供了更好的平均机制。这意味着当我们训练模型时,我们将以批量的方式进行:
for episode in tr:
tr.set_description("Agent training (episode{}) Avg Reward {}".format(episode+1,tot_reward/(episode+1)))
tr.refresh()
epsilon = eps_by_episode(episode)
action = model.act(state, epsilon)
next_state, reward, done, _ = env.step(action)
replay_buffer.push(state, action, reward, next_state, done)
tot_reward += reward
state = next_state
episode_reward += reward
if done:
state = env.reset()
all_rewards.append(episode_reward)
episode_reward = 0
if len(replay_buffer) > batch_size:
loss = compute_td_loss(batch_size)
losses.append(loss.item())
if epoch % 2000 == 0:
plot(epoch, all_rewards, losses)
-
再次强调,现在大部分代码应该已经很熟悉了,因为它与我们之前的许多示例相似。我们将关注两个代码高亮部分。第一个是推送
state、action、reward、next_state和done函数到replay_buffer的行。我们尚未查看replay缓冲区,但在此阶段,所有这些信息都将被存储以供后续使用。另一个高亮部分与使用compute_td_loss函数计算损失有关。该函数使用TD误差来计算损失,正如我们在介绍TD和SARSA时所见。 -
在我们探索额外的函数和类之前,运行样本以便您可以看到输出。当样本运行时,您需要在每2,000次迭代后重复关闭绘图窗口。以下图表显示了数千次训练迭代的输出:

几千次训练迭代后的示例输出
图表中的输出显示了智能体在各个回合中的学习情况,并且能够快速最大化奖励,如左图所示。相比之下,右图显示了智能体预测中的损失或总误差在下降。实际上,我们可以在第8000个回合看到,智能体确实已经学会了这个问题,并且能够持续获得最大奖励。如果你还记得第5章,探索SARSA,我们解决了CartPole环境,但只是勉强通过离散化的SARSA解决了。尽管如此,这仍然需要大约50,000个回合的训练。现在我们已经观察到这种方法比我们之前的尝试好得多,在下一节中,我们需要探索这种方法是如何工作的细节。
重放缓冲区
对于深度学习方法来说,基本的需求是我们需要将观察到的智能体事件批次输入到神经网络中。记住,我们这样做是为了让算法能够更好地平均错误或损失。这一需求更多的是深度学习的方法,而不是与强化学习有关。因此,我们想要存储之前观察到的状态、动作、下一个状态、奖励和从智能体采取动作返回的值,并将它们存储在一个称为ReplayBuffer的容器中。然后,我们从重放缓冲区中随机抽取这些事件,并将它们注入神经网络进行训练。让我们再次通过重新打开Chapter_6_DQN.py样本并遵循这个练习来查看缓冲区的构建方式:
ReplayBuffer类的整个代码如下所示:
class ReplayBuffer(object):
def __init__(self, capacity):
self.buffer = deque(maxlen=capacity)
def push(self, state, action, reward, next_state, done):
state = np.expand_dims(state, 0)
next_state = np.expand_dims(next_state, 0)
self.buffer.append((state, action, reward, next_state, done))
def sample(self, batch_size):
state, action, reward, next_state, done
= zip(*random.sample(self.buffer, batch_size))
return np.concatenate(state), action,
reward, np.concatenate(next_state), done
def __len__(self):
return len(self.buffer)
- 在内部,
ReplayBuffer使用一个名为deque的类,这是一个可以存储任何类型对象的类。在init函数中,我们创建了一个所需指定大小的队列。该类有三个函数push、sample和len。len函数相当直观,但我们应该查看的其他函数:
def push(self, state, action, reward, next_state, done):
state = np.expand_dims(state, 0)
next_state = np.expand_dims(next_state, 0)
self.buffer.append((state, action, reward, next_state, done))
push函数将state、action、reward、next_state和done观察结果推送到队列中以便稍后处理:
def sample(self, batch_size):
state, action, reward, next_state, done
= zip(*random.sample(self.buffer, batch_size))
return np.concatenate(state), action,
reward, np.concatenate(next_state), done
-
另一个函数
sample是缓冲区从队列中随机采样事件并使用zip将它们组合起来。然后,它将返回这个随机事件批次以供网络学习。 -
找到设置重放缓冲区大小的代码行,并将其更改为以下内容:
replay_buffer = ReplayBuffer(3000)
-
再次运行示例,并观察新的缓冲区大小对训练的影响。
-
现在再次使用以下代码更改缓冲区大小:
replay_buffer = ReplayBuffer(333)
- 再次运行示例,并仔细观察输出。注意训练性能的变化。
我们实际上尝试了3次运行代理,以及缓冲区大小的1/3。你会在这个问题中发现,较小的缓冲区大小更有效,但可能不是最优的。你可以将缓冲区大小视为你需要学习的另一个基本超参数。
重放缓冲区是我们深度学习模型的一个必要组件,我们将在未来看到其他类似的类。在下一节中,我们将继续构建DQN类。
DQN类
之前,我们看到了如何使用DQN类构建我们将用于学习TD损失函数的神经网络模型。再次打开Chapter_6_DQN.py练习以回顾DQN类的构建:
- DQN类的整个代码如下:
class DQN(nn.Module):
def __init__(self, num_inputs, num_actions):
super(DQN, self).__init__()
self.layers = nn.Sequential(
nn.Linear(env.observation_space.shape[0], 128),
nn.ReLU(),
nn.Linear(128, 128),
nn.ReLU(),
nn.Linear(128, env.action_space.n))
def forward(self, x):
return self.layers(x)
def act(self, state, epsilon):
if random.random() > epsilon:
state = autograd.Variable(torch.FloatTensor(state).unsqueeze(0),
volatile=True)
q_value = self.forward(state)
action = q_value.max(1)[1].item()
else:
action = random.randrange(env.action_space.n)
return action
-
init函数使用PyTorch的nn.Sequential类初始化网络,以生成一个全连接网络。我们可以看到第一层的输入由env.observation_space.shape[0]设置,神经元数量为128。 -
我们可以看到这个网络有三个层,第一层由128个神经元组成,通过ReLU连接到中间层,中间层也有128个神经元。这一层连接到输出层,输出数量由
env.action_space.n定义。我们可以看到,网络将学习选择哪个动作。 -
forward函数只是网络模型的正向传递或预测。 -
最后,
act函数与其他我们之前构建的Q学习样本非常相似。我们想要关注的一点是在非探索期间如何选择实际动作,如下面的代码片段所示:
state = autograd.Variable(torch.FloatTensor(state).unsqueeze(0),
volatile=True)
q_value = self.forward(state)
action = q_value.max(1)[1].item()
-
在第一行使用
autograd.Variable计算state张量是将状态转换为张量以便它可以被输入到前向传递中。这是在下一行调用self.forward来计算该state张量的所有Q值q_value的地方。然后我们在最后一行使用贪婪(最大)选择策略来选择动作。 -
将网络大小从128个神经元更改为32、64或256,以观察这对训练的影响。以下代码显示了配置示例以使用64个神经元的正确方式:
self.layers = nn.Sequential(
nn.Linear(env.observation_space.shape[0], 64),
nn.ReLU(),
nn.Linear(64, 64),
nn.ReLU(),
nn.Linear(64, env.action_space.n))
- 再次运行示例,并观察不同大小变化对训练性能的影响。
好消息,我们认为网络中神经元和层数的数量是我们解决问题时需要观察的额外训练超参数。正如你可能已经注意到的,这些新输入可以对训练性能产生重大影响,需要仔细选择。
我们几乎已经拼凑出整个算法的所有部分,以便理解。在下一节中,我们将介绍最后一部分,确定损失和训练网络。
计算损失和训练
最后,我们可以看到所有这些是如何结合在一起来训练智能体学习策略的。再次打开Chapter_6_DQN.py,并跟随下一个练习来了解损失是如何计算的:
- 计算基于TD误差的损失的函数如下所示:
def compute_td_loss(batch_size):
state, action, reward, next_state, done = replay_buffer.sample(batch_size)
state = autograd.Variable(torch.FloatTensor(np.float32(state)))
next_state = autograd.Variable(torch.FloatTensor(np.float32(next_state)),
volatile=True)
action = autograd.Variable(torch.LongTensor(action))
reward = autograd.Variable(torch.FloatTensor(reward))
done = autograd.Variable(torch.FloatTensor(done))
q_values = model(state)
next_q_values = model(next_state)
q_value = q_values.gather(1, action.unsqueeze(1)).squeeze(1)
next_q_value = next_q_values.max(1)[0]
expected_q_value = reward + gamma * next_q_value * (1 - done)
loss = (q_value - autograd.Variable(expected_q_value.data)).pow(2).mean()
optimizer.zero_grad()
loss.backward()
optimizer.step()
return loss
-
在第一行中,我们使用
batch_size作为输入从replay_buffer调用sample。这返回了从之前运行中随机采样的一组事件。这返回了state、next_state、action、reward和done。然后,在接下来的五行中,使用autograd.Variable函数将这些转换为张量。这个函数是一个辅助函数,用于将类型转换为适当类型的张量。注意,动作是long类型,使用torch.LongTensor,而其他变量只是浮点数。 -
下一节代码计算Q值:
q_values = model(state)
next_q_values = model(next_state)
q_value = q_values.gather(1, action.unsqueeze(1)).squeeze(1)
-
记住,当我们调用
model(state)时,这相当于在网络上进行前向传递或预测。现在这和我们在之前的例子中从策略中采样是相同的。 -
然后,我们回到之前定义的Q学习方程,并使用它来确定最佳期望Q值应该是什么,以下代码所示:
next_q_value = next_q_values.max(1)[0]
expected_q_value = reward + gamma * next_q_value * (1 - done)
- 从之前计算
expected_q_value值使用Q学习方程来确定期望值应该是什么。基于期望值,我们可以确定网络误差的大小以及它需要通过以下行来纠正的损失:
loss = (q_value - autograd.Variable(expected_q_value.data)).pow(2).mean()
- 这行代码将值转换为张量,然后使用我们老朋友均方误差(MSE)来确定损失。我们的最后一步是使用以下代码来优化或减少网络的损失:
optimizer.zero_grad()
loss.backward()
optimizer.step()
- 代码与我们之前用来优化我们的神经网络和计算图示例的代码非常相似。我们首先对优化器应用
zero_grad,以便将任何梯度归零作为重置。然后我们将损失反向传播,最后在优化器上执行一步。最后这部分是新的,与我们使用的优化器类型有关。
我们不会深入探讨你可以在深度学习中使用的各种优化器,直到第6章,深入探索DDQN。在大多数情况下,我们将使用Adam优化器或其某种变体,这取决于环境。
- 随意再次运行代码示例,以便更好地观察训练中的所有细节。
希望到现在为止,即使包括了深度学习(DL),这些示例也开始感觉越来越熟悉。在某种程度上,DL 使得这些算法比我们之前的例子简单得多。幸运的是,这是一个好事,因为随着我们向更难的环境进化,我们的智能体将需要变得更加复杂和健壮。
到目前为止,我们只能查看智能体的训练过程,看不到性能的实际更新。由于这是我们对智能体训练理解的关键,在下一节中,我们将将其添加到上一个示例中。
练习 DQN
随着我们通过这本书的进展,我们花时间确保我们可以看到我们的智能体在各自环境中的进展。在本节中,我们的目标是使用我们最后的 DQN 示例在训练期间为智能体环境添加渲染。然后我们可以看到智能体实际的表现,也许还可以在途中尝试几个新的环境。
添加观看智能体在环境中玩的能力并不困难,我们可以像其他示例一样实现它。打开 Chapter_6_DQN_wplay.py 代码示例,并遵循下一个练习:
- 代码几乎与之前的 DQN 示例完全相同,所以我们不需要审查整个代码。然而,我们确实想介绍两个新的变量作为超参数;这将使我们能够更好地控制网络训练和观察性能:
buffer_size = 1000
neurons = 128
-
我们将使用
buffer_size来表示缓冲区的大小。这个值在我们确定我们的模型是否有一些训练量时也会很有用。DQN 不会开始训练模型,直到重放缓冲区或我们通常所说的经验缓冲区填满。注意我们还添加了一个新的神经元超参数;这将允许我们根据需要快速调整网络。 -
接下来,我们将查看将渲染智能体玩游戏代码注入到训练循环中的方法:
if done:
if episode > buffer_size:
play_game()
state = env.reset()
all_rewards.append(episode_reward)
episode_reward = 0
-
高亮行表示新的代码,该代码将检查当前剧集是否大于
buffer_size。如果是,则使用模型/策略渲染智能体玩游戏。 -
接下来,我们将查看新的
play_game函数,如下所示:
def play_game():
done = False
state = env.reset()
while(not done):
action = model.act(state, epsilon_final)
next_state, reward, done, _ = env.step(action)
env.render()
state = next_state
-
这段代码与我们之前编写的其他
play_game函数非常相似。注意高亮行显示了如何使用model.act函数预测下一个动作。传递给这个函数的是状态和我们的最小 epsilon 值,称为epsilon_final。我们在这里设置最小值,因为我们选择执行最小探索的智能体,并且动作完全从策略/模型中选择。 -
运行这个示例,你可以看到智能体成功地在以下图中玩 CartPole 环境:

智能体成功玩 CartPole 的示例渲染
在有了能够轻松应对CartPole环境的智能体之后,在下一节中,我们将现在来看一下上一章中失败的例子,即LunarLander环境。
回顾LunarLander及其他
现在我们有了DQN的稳固示例,我们可以继续解决更困难的环境,如LunarLander。在这个练习中,我们设置了DQN智能体来解决LunarLander环境,以便比较我们之前的尝试与离散化SARSA:
- 打开
Chapter_6_DQN_lunar.py示例,注意env_id环境ID和创建的环境如下所示:
env_id = 'LunarLander-v2'
env = gym.make(env_id)
- 我们还调整了一些超参数,以应对环境复杂性的增加:
epsilon_decay = 1000
buffer_size = 3000
neurons = 192
-
我们增加
epsilon_decay的值,以鼓励智能体进行更长时间的探索。探索是我们始终需要与环境平衡的权衡。注意,buffer_size也增加到3,000,以应对环境复杂性的增加。此外,我们还把网络的神经元数量统一增加到192。 -
您可能还需要将总训练轮数从10,000增加到更高的值。我们将把这个决定留给你。
-
从这里,你可以运行示例并可视化智能体着陆的情况,如下面的屏幕截图所示:

智能体在LunarLander环境中进行游戏
这就完成了我们对DQN的探索,并鼓励你在下一节中跟随练习来练习这些新技能。
练习
随着我们通过本书的进展,我希望你能看到进行这些额外动手练习的价值。学习如何调整超参数对于构建能够应对困难环境的DRL模型至关重要。使用以下练习来巩固你对材料的理解:
-
从
Chapter_6_1.py中修改batch_size、inputs、hidden和outputs超参数,并观察这些参数对输出损失的影响。 -
在
Chapter_6_1.py示例中,结合其他超参数调整训练迭代次数,以评估这对训练的影响。 -
从
Chapter_6_2.py中修改batch_size、inputs、hidden和outputs超参数,并观察这些参数对输出损失的影响。 -
在
Chapter_6_2.py示例中,结合其他超参数调整训练迭代次数,以评估这对训练的影响。 -
在
Chapter_6_DQN.py示例中调整超参数,以改善CartPole环境上的训练性能。创建你可能需要的任何额外超参数。 -
在
Chapter_6_DQN_wplay.py示例中调整超参数,以改善CartPole环境上的训练性能。创建你可能需要的任何额外超参数。 -
在
Chapter_6_DQN_lunar.py示例中调整超参数,以改善LunarLander环境上的训练性能。创建你可能需要的任何附加超参数。 -
将
batch_size超参数调整到低值,如8或16,一直调整到256、512和1,024,以观察这将对所有DQN示例产生什么影响。 -
介绍一个主要功能,该功能将接受命令行参数,允许你在运行时配置各种超参数。你可能需要使用辅助库(
argparse)来完成此操作。 -
添加渲染训练性能的能力,而不会阻塞训练执行。
做两到三个这样的练习可以极大地提高你对这些知识的掌握,而且实际上没有比实践更好的学习方法了,只需问问你的智能体。唉,我们已经到达了本章的结尾,在下一节中我们将有总结。
摘要
在本章中,我们探讨了DRL的Hello World,即DQN算法,以及将深度学习应用于强化学习。我们首先探讨了为什么我们需要深度学习来处理更复杂的连续观察状态环境,如CartPole和LunarLander。然后,我们探讨了你可能用于深度学习的更常见的深度学习环境,以及我们使用的PyTorch。从那里,我们安装了PyTorch,并使用计算图作为低级神经网络设置了一个示例。随后,我们使用PyTorch神经网络接口构建了第二个示例,以便比较原始计算图和神经网络之间的差异。
在获得这些知识后,我们便深入探讨了DQN。我们研究了DQN如何使用经验回放或回放缓冲区在训练网络/策略时重放事件。此外,我们还研究了基于预测值和期望值之间差异的TD损失是如何计算的。我们使用我们熟悉的朋友Q学习方程来计算期望值,并将差异作为损失反馈给模型。通过这样做,我们能够训练模型/策略,使得智能体能够在足够多的迭代次数下解决CartPole和LunarLander环境。
在下一章中,我们将再次扩展本章的知识,并探索DQN的下一个层次,即Double DQN或DDQN。同时,我们将探索网络图像处理方面的进展,使用CNN,以便我们可以处理更复杂的环境,例如经典的Atari。
第九章:深入学习 DDQN
深度学习是原始计算学习的演变,它正在迅速发展并开始主导数据科学、机器学习(ML)和人工智能(AI)等所有领域。反过来,这些增强带来了在深度强化学习(DRL)方面的惊人创新,使得它能够玩以前被认为不可能的游戏。现在,DRL 能够处理像经典 Atari 2600 系列这样的游戏环境,并且比人类玩得更好。在本章中,我们将探讨深度学习中的哪些新特性使得 DRL 能够玩视觉状态游戏,例如 Atari 游戏。首先,我们将探讨如何将游戏屏幕用作视觉状态。然后,我们将了解深度学习如何通过一个名为卷积神经网络(CNNs)的新组件来消费视觉状态。之后,我们将利用这些知识来构建一个修改后的 DQN 代理来处理 Atari 环境。在此基础上,我们将探讨 DQN 的一个增强版本,称为DDQN,或双重(对抗)DQN。最后,我们将通过玩其他视觉环境来结束本章。
总结来说,在本章中,我们将探讨深度学习(DL)的扩展,称为卷积神经网络(CNNs),如何被用来观察视觉状态。然后,我们将利用这些知识来玩 Atari 游戏,并在过程中实现进一步的增强。以下是本章我们将涵盖的内容:
-
理解视觉状态
-
介绍 CNNs
-
在 Atari 上使用 DQN
-
介绍 DDQN
-
扩展回放与优先经验回放
我们将继续在本章中使用我们在第 6 章“深入学习 DQN”中构建的相同虚拟环境。为了使用本章中的示例,您需要正确设置和配置该环境。
理解视觉状态
迄今为止,我们观察状态作为编码值或值。这些值可能是网格中的单元格编号或区域中的 x,y 位置。无论如何,这些值都是相对于某个参考进行编码的。在网格环境的情况下,我们可能使用一个数字来表示方块或一对数字。对于 x,y 坐标,我们仍然需要表示一个原点,以下是一些三种类型编码机制的示例:

代理的编码状态的三种类型
在前面的图中,有三个用于环境状态编码的例子。对于第一个例子,位于左侧,我们只是用一个数字来表示该状态。向右移动到下一个网格,状态现在表示为一对数字,按行和列排列。在右侧,我们可以看到我们熟悉的好朋友月球着陆器,以及它的部分状态,即位置,相对于着陆点,即原点。在这些所有情况下,状态总是以某种编码形式表示,无论是单个数字还是像着陆器环境中的八个数字。通过编码,我们是指我们使用一个值,即一个数字,来表示环境的这种状态。在第五章《探索SARSA》中,我们学习了状态离散化是这种编码转换成更简单形式的一种类型,但转换这种编码需要调整或学习,我们意识到需要有一种更好的方法来做这件事。幸运的是,我们确实设计了一种更好的方法,但在我们到达那里之前,让我们考虑一下状态本身是什么。
状态只是我们策略的一个数值表示或索引,它让我们的智能体确定其下一步行动的选择。在这里需要记住的重要一点是,状态需要是策略或,在DRL的情况下,模型的索引。因此,我们的智能体始终需要将那种状态转换成模型中的数值索引。正如我们已经看到的,使用深度学习(DL)可以使这个过程大大简化。理想的情况是智能体能够像我们人类一样直观地消费相同的可见状态——游戏区域——并学会自行编码状态。10年前,这样的说法听起来像是科幻小说。今天,这已经成为科学事实,我们将在下一节中学习它是如何实现的。
编码视觉状态
幸运的是,对于DRL来说,从图像中学习这一概念已经成为了深度学习(DL)持续研究30多年的中心。深度学习已经将这一概念从识别手写数字扩展到检测物体位置和旋转,再到理解人类姿态。所有这些都是在将原始像素输入深度学习网络并教会(或教会自己)如何将这些图像编码成某种答案的过程中完成的。我们将在本章中使用这些相同的工具,但在我们这样做之前,让我们了解将图像输入网络的基本原理。以下图示展示了你可能如何进行这一操作:

将图像剖析为深度学习输入
在前面的图中,图像被分割成四个部分,每个部分作为一块被输入到网络中。需要注意的是,每个部分是如何被输入到输入层上的每个神经元的。现在,我们可以使用四个部分,就像前面的图一样,或者100个部分,可能是将图像分解成像素。无论如何,我们仍然是在盲目地离散化空间,即图像,并试图理解它。有趣的是,我们在强化学习(RL)中识别出的离散化问题在深度学习(DL)中也会遇到。在DL中,这个问题可能更加复杂,因为我们通常会简单地将图像,一个二维的数据矩阵,展平成一个一维的数字向量。例如,在前面的例子中,我们可以看到两个眼睛被输入到网络中,但没有表明它们之间的关系,例如间距和方向。当我们展平图像时,这些信息完全丢失,而且随着我们对输入图像的离散化程度越高,这些信息就越重要。我们需要的是,深度学习(DL)发现的是,从一组数据,如图像,中提取特定特征,并保留这些特征,以便以某种方式对整个图像进行分类。实际上,深度学习(DL)很好地解决了这个问题,我们将在下一节中了解到这一点。
介绍CNN
2012年9月,多伦多大学的杰弗里·辛顿博士领导的团队,被认为是深度学习的教父,竞争构建AlexNet。AlexNet是在对抗一个名为ImageNet的巨大图像测试集时进行训练的。ImageNet包含超过1400万张图片,分布在20000多个不同的类别中。AlexNet那年以超过10分的优势击败了其竞争对手,一个非深度学习解决方案,并实现了许多人认为不可能的事情——即图像中对象的识别做得和人类一样好,甚至可能更好。从那时起,使这一切成为可能的组件——CNN——在某些情况下已经超过了人类在图像识别方面的认知水平。
使这一切成为可能的组件,CNN(卷积神经网络),通过将图像分解成特征来工作——这些特征是通过学习检测这些特征来学习的。这听起来有点递归,确实如此,但这也是它之所以能如此有效的原因。所以,让我们再重复一遍。CNN通过检测图像中的特征来工作,但我们没有指定这些特征——我们指定的是答案是对还是错。通过使用那个答案,我们可以使用反向传播将任何错误推回网络并通过纠正网络检测这些特征的方式来纠正。
为了检测特征,我们使用滤波器,这与你在Photoshop中使用滤波器的方式类似。这些滤波器是我们现在训练的部件,我们通过在新的层类型CNN(卷积或CONV)中引入它们来进行训练。我们发现,我们还可以将这些层堆叠起来以提取更多特征。这些概念可能仍然很抽象。幸运的是,有许多优秀的工具可以帮助我们在下一个练习中探索这些概念。让我们看看其中一个:
-
打开并使用网络浏览器访问tensorspace.org。
-
找到Playground的链接并点击它。
-
在TensorSpace Playground页面,注意左侧的各种模型名称。点击以下截图所示的AlexNet示例:

TensorSpace Playground – AlexNet
Playground允许你交互式地探索各种深度学习模型,例如AlexNet,直到层级别。
- 在图中移动并点击各个层。你可以放大和缩小,并在3D中探索模型。你将能够查看网络模型中的所有层。每种层类型都有不同的颜色编码。这包括CNN层(黄色),以及特殊的池化层(蓝色)。
池化层,这些层从CNN层收集学习到的特征,允许网络更快地学习,因为层实际上减少了学习空间的大小。然而,这种减少消除了特征之间的空间关系。因此,我们通常在DRL和游戏中避免使用池化层。
- 如果放大,你可以查看图像是如何被每个颜色通道(红色、绿色和蓝色)分割,然后输入到网络中的。以下截图显示了这一点:

检查图像分割和滤波器提取
-
从图像分离的方式中,我们可以看到CNN的第一层,即滤波器,是如何提取特征的。通过这种方式,可以识别整个狗,但随着通过层的深入,特征会越来越小。
-
最后,有一个蓝色的最终池化层,接着是一个绿色的层,这是一个单行层。这个单行层表示输入数据被展平,以便可以输入到典型的深度学习网络的后续层中。
当然,你也可以自由探索Playground中的许多其他模型。理解层如何提取特征对于理解CNN的工作原理非常重要。在下一节中,我们将看看如何升级我们的DQN代理,使其能够使用CNN玩Atari游戏。
在Atari上使用DQN
现在我们已经看到了CNN在滤波器方面的输出,了解这一工作原理的最好方式是查看构建它们的代码。在我们到达那里之前,让我们开始一个新的练习,使用新的DQN形式来解决Atari:
- 打开本章的示例代码,该代码位于
Chapter_7_DQN_CNN.py文件中。代码与Chapter_6_lunar.py非常相似,但有一些关键的不同之处。我们将只关注这个练习中的差异。如果你需要更好的代码解释,请回顾第6章,深入DQN:
from wrappers import *
- 从顶部开始,唯一的改变是从一个名为
wrappers.py的本地文件中导入一个新的模块。我们将通过创建环境来检查这个模块的作用:
env_id = 'PongNoFrameskip-v4'
env = make_atari(env_id)
env = wrap_deepmind(env)
env = wrap_pytorch(env)
-
由于几个原因,我们在这里以相当不同的方式创建环境。三个函数
make_atari、wrap_deepmind和wrap_pytorch都位于我们之前导入的新wrappers.py文件中。这些包装器基于OpenAI为创建Gym环境包装器而制定的规范。我们稍后会更多地讨论包装器,但现在,这三个函数执行以下操作:-
make_atari: 这项操作准备环境,以便我们可以以CNN可编码的形式捕获视觉输入。我们这样设置是为了能够以设定的时间间隔对环境进行截图。 -
wrap_deepmind: 这又是一个包装器,允许使用一些辅助工具。我们稍后会查看这个包装器。 -
wrap_pytorch: 这是一个辅助库,它将我们加载到CNN网络中的视觉输入图像转换为PyTorch的特殊格式。不同的深度学习框架为CNN层有不同的输入风格,因此在所有DL框架标准化之前,你必须注意你的输入图像中通道的排列方式。在PyTorch中,图像通道需要放在第一位。对于其他框架,如Keras,则正好相反。
-
-
之后,我们需要修改一些设置超参数的其他代码,如下所示:
epsilon_start = 1.0
epsilon_final = 0.01
epsilon_decay = 30000
epsilon_by_episode = lambda episode: epsilon_final + (epsilon_start - epsilon_final) * math.exp(-1\. * episode / epsilon_decay)
plt.plot([epsilon_by_episode(i) for i in range(1000000)])
plt.show()
- 突出的行显示了我们所做的更改。我们主要改变的是增加值——很多。Pong Atari环境是最简单的,但仍可能需要一百万次迭代才能解决。在某些系统上,这可能需要几天时间:
model = CnnDQN(env.observation_space.shape, env.action_space.n)
optimizer = optim.Adam(model.parameters(), lr=0.00001)
replay_start = 10000
replay_buffer = ReplayBuffer(100000)
- 在前面的代码块中,我们可以看到我们正在构建一个新的类
CnnDQN。我们很快就会接触到它。之后,代码基本上相同,除了有一个新的变量replay_start以及现在设置的回放缓冲区的大小。我们的缓冲区大小增加了100倍,从1,000条记录增加到100,000条记录。然而,我们希望在缓冲区完全填满之前就能训练智能体。毕竟,那是一个很大的数字。因此,我们使用replay_start来表示当缓冲区用于训练智能体时的训练起点:
episodes = 1400000
- 接下来,我们将剧集计数更新到一个更高的数值。这是因为我们可以预期这个环境至少需要一百万个剧集来训练一个智能体:
if episode % 200000 == 0:
plot(episode, all_rewards, losses)
- 除了训练循环的最后部分之外,所有其他代码都保持不变,这部分代码可以在前面的代码中看到。这段代码显示我们每200,000个回合绘制一次迭代。以前,我们每2,000个回合就做一次。当然,你可以增加这个值,或者完全删除它,如果长时间训练感到烦恼的话。
这个环境和我们将要查看的许多其他环境现在可能需要数小时或数天才能训练。实际上,DeepMind最近估计,一个普通的桌面系统大约需要45年才能训练其最顶尖的强化学习算法。如果你想知道的话,大多数其他环境需要4000万次迭代才能收敛。Pong是最简单的,只需要100万次迭代。
- 按照常规方式运行示例。等待一段时间,也许可以继续阅读这本书的其余部分。这个样本需要数小时才能训练,所以我们在它运行的同时将继续探索代码的其他部分。不过,为了确认样本正在正确运行,只需确认环境正在渲染,如下面的图片所示:

运行代码示例
保持样本运行。在下一节中,我们将探讨CNN层是如何构建到新模型中的。
添加CNN层
现在我们已经了解了CNN层背后的基本原理,是时候深入探讨它们是如何工作的了。打开代码示例,可以在Chapter_7_DQN_CNN.py文件中找到,并按照以下步骤进行:
- 到目前为止,我们只需要关注一个名为
CnnDQN的新类的代码,如下所示:
class CnnDQN(nn.Module):
def __init__(self, input_shape, num_actions):
super(CnnDQN, self).__init__()
self.input_shape = input_shape
self.num_actions = num_actions
self.features = nn.Sequential(
nn.Conv2d(input_shape[0], 32, kernel_size=8, stride=4),
nn.ReLU(),
nn.Conv2d(32, 64, kernel_size=4, stride=2),
nn.ReLU(),
nn.Conv2d(64, 64, kernel_size=3, stride=1),
nn.ReLU())
self.fc = nn.Sequential(
nn.Linear(self.feature_size(), 512),
nn.ReLU(),
nn.Linear(512, self.num_actions))
def forward(self, x):
x = self.features(x)
x = x.view(x.size(0), -1)
x = self.fc(x)
return x
def feature_size(self):
return self.features(autograd.Variable(torch.zeros(1,
*self.input_shape))).view(1, -1).size(1)
def act(self, state, epsilon):
if random.random() > epsilon:
state = autograd.Variable(torch.FloatTensor(
np.float32(state)).unsqueeze(0), volatile=True)
q_value = self.forward(state)
action = q_value.max(1)[1].data[0]
else:
action = random.randrange(env.action_space.n)
return action
- 上述类替换了我们的先前vanilla DQN版本。两者之间存在一些关键差异,所以让我们从网络设置和构建第一个卷积层开始,如下所示:
self.features = nn.Sequential(
nn.Conv2d(input_shape[0], 32, kernel_size=8, stride=4),
-
首先要注意的是,我们正在构建一个新的模型并将其放入
self.features。features将是我们的模型,用于执行卷积和分离特征。第一层是通过传递input_shape、过滤器数量(32)、kernel_size(8)和stride(4)来构建的。所有这些输入的详细信息都可以在这里找到:-
input_shape[0]:输入形状指的是观察空间。通过我们之前查看的包装器,我们将输入空间转换为(1,84,84)。记住,我们需要首先对通道进行排序。1个通道意味着我们可以看到我们的图像是灰度的(没有RGB)。1个通道也是我们输入到Conv2d的第一个值。 -
数量(
32):下一个输入表示我们想要在这个层中构建的过滤器补丁的数量。每个过滤器都应用于图像,并由窗口大小(核大小)和移动(步长)确定。我们在使用TensorSpace Playground查看CNN模型细节时观察了这些补丁的结果。 -
kernel_size(8): 这代表窗口大小。在这种情况下,由于我们使用的是2D卷积,Conv2d,这个大小实际上代表了一个8x8的值。将窗口或内核在图像上移动并应用学习到的滤波器是卷积操作。 -
stride(4): 步长表示窗口或内核在操作之间移动的距离。步长为4意味着窗口移动了4个像素或单位,这实际上是一半的窗口大小8。
-
-
卷积如何工作的一个例子可以在以下图像中看到。上方区域是一个单独的输出块。内核中的每个元素,即以下图像中的3x3块,是正在学习的部分:

步长卷积过程的解释
- 在图像上应用内核的过程是通过简单地乘以块中的值与图像中的每个值来完成的。所有这些值相加,然后输出为输出滤波器操作的结果中的单个元素:
self.fc = nn.Sequential(
nn.Linear(self.feature_size(), 512),
- 使用构建卷积层模型的代码,我们构建另一个线性模型,就像我们在之前的例子中所构建的那样。这个模型将卷积层的输出展平,并使用这个展平的模型来预测动作。在这种情况下,我们最终有两个网络模型,但请注意,我们将从第一个模型传递输出到第二个模型,以及从第一个模型反向传播错误到第二个模型。
feature_size函数只是一个辅助函数,以便我们可以计算CNN模型到Linear模型的输入:
def forward(self, x):
x = self.features(x)
x = x.view(x.size(0), -1)
x = self.fc(x)
return x
-
在
forward函数内部,我们可以看到我们模型的预测已经改变。现在,我们将通过将其传递到self.features或我们模型的CNN部分来分解预测。然后,我们需要展平数据,并通过self.fc将其馈入线性部分。 -
action函数与我们的先前DQN实现相同。
如果智能体仍在运行,看看你是否可以等待它完成。这可能需要一段时间,但看到最终结果可能会很有奖励和趣味。像RL中的几乎所有事情一样,DQN模型已经经历了各种改进,我们将在下一节中探讨那些改进。
介绍DDQN
DDQN代表对战的DQN,与双DQN不同,尽管人们经常混淆它们。这两种变体都假设某种形式的对偶性,但在第一种情况下,模型被假设在基础部分被分割,而在第二种情况,双DQN中,模型被假设分割成两个完全不同的DQN模型。
以下图表展示了DDQN和DQN之间的区别,不要与对战的DQN混淆:

DQN和DDQN之间的区别
在前面的图表中,两个模型都在使用CNN层,但在即将到来的练习中,我们将只使用线性全连接层,只是为了简化问题。
注意DDQN网络如何分为两部分,然后又回到一个答案。这就是DDQN模型中我们将很快讨论的对抗部分。在此之前,让我们先探索双重DQN模型。
双重DQN或固定Q目标
为了理解为什么我们可能会组合使用两个网络,或者说是“对抗”,我们首先需要明白为什么我们需要这样做。让我们回顾一下我们是如何计算TD损失并使用它作为我们估计动作的方法的。如您所回忆的那样,我们是基于目标估计来计算损失的。然而,在我们的DQN模型中,那个目标现在是持续变化的。我们可以用的类比是,我们的智能体有时可能会追逐自己的尾巴,试图找到目标。那些非常细心的你们可能在前面的训练中已经看到了这一点,通过看到波动的奖励。我们在这里可以做的就是创建另一个目标网络,我们将朝着它前进并在过程中更新它。这听起来比实际上要复杂得多,所以让我们看看一个例子:
- 打开
Chapter_7_DoubleDQN.py文件中的代码示例。这个例子是从我们之前看过的Chapter_6_DQN_lunar.py文件构建的。这里有一些细微的变化,所以我们将详细审查每一个,从模型构建开始:
current_model = DQN(env.observation_space.shape[0], env.action_space.n)
target_model = DQN(env.observation_space.shape[0], env.action_space.n)
optimizer = optim.Adam(current_model.parameters())
- 如其名称所示,我们现在构建了两个DQN模型:一个用于在线使用,一个作为目标。我们训练
current_model的值,然后每x次迭代后使用以下代码切换回目标模型:
def update_target(current_model, target_model):
target_model.load_state_dict(current_model.state_dict())
update_target(current_model, target_model)
-
update_target函数通过使用current_model更新target_model,确保目标Q值总是足够地提前或落后,因为我们使用跳过跟踪并回顾过去。 -
随后是
compute_td_loss函数,需要按照以下方式更新:
def compute_td_loss(batch_size):
state, action, reward, next_state, done = replay_buffer.sample(batch_size)
state = autograd.Variable(torch.FloatTensor(np.float32(state)))
next_state = autograd.Variable(torch.FloatTensor(np.float32(next_state)),
volatile=True)
action = autograd.Variable(torch.LongTensor(action))
reward = autograd.Variable(torch.FloatTensor(reward))
done = autograd.Variable(torch.FloatTensor(done))
q_values = current_model(state)
next_q_values = current_model(next_state)
next_q_state_values = target_model(next_state)
q_value = q_values.gather(1, action.unsqueeze(1)).squeeze(1)
next_q_value = next_q_state_values.gather(1,
torch.max(next_q_values, 1)[1].unsqueeze(1)).squeeze(1)
expected_q_value = reward + gamma * next_q_value * (1 - done)
loss = (q_value - autograd.Variable(expected_q_value.data)).pow(2).mean()
optimizer.zero_grad()
loss.backward()
optimizer.step()
return loss
- 函数中突出显示的行显示了被更改的行。注意新的模型
current_model和target_model是如何现在用来预测损失,而不仅仅是单个模型本身。最后,在训练或试错循环中,我们可以看到一些最终的变化:
action = current_model.act(state, epsilon)
- 第一个变化是我们现在从
current_model模型中获取动作:
if episode % 500 == 0:
update_target(current_model, target_model)
- 第二个变化是使用
update_target更新target_model,使用current_model的权重:
def play_game():
done = False
state = env.reset()
while(not done):
action = current_model.act(state, epsilon_final)
next_state, reward, done, _ = env.step(action)
env.render()
state = next_state
-
我们还需要更新
play_game函数,以便我们可以从current_model中获取动作。如果你将其更改为目标模型,可能会很有趣地看到会发生什么。 -
在这一点上,像平常一样运行代码,并观察结果。
现在我们已经理解了为什么我们可能想要使用不同的模型,我们将继续学习如何使用对抗DQN或DDQN来解决相同的环境。
对抗DQN或真正的DDQN
对抗DQN或DDQN扩展了固定目标或固定Q目标的概念,并将其扩展到包括一个称为优势的新概念。优势是一个概念,其中我们确定通过采取其他动作可能获得的额外价值或优势。理想情况下,我们希望计算优势,使其包括所有其他动作。我们可以通过计算图来实现这一点,通过将层分离为状态值的计算和从状态和动作的所有排列中计算优势的另一个计算。
这种结构可以在以下图中看到:

DDQN的详细可视化
上述图再次显示了CNN层,但我们的示例将仅从线性展平模型开始。我们可以看到模型在展平后分为两部分。第一部分计算状态值或价值,第二部分计算优势或动作值。然后,这些值被聚合以输出Q值。这种设置之所以有效,是因为我们可以使用优化(也称为反向传播)将损失推回整个网络。因此,网络学习如何计算每个动作的优势。让我们看看如何在新的代码示例中实现这一点。在Chapter_7_DDQN.py文件中打开它,并按照以下步骤操作:
- 这个例子使用之前的例子作为源,但在许多重要细节上有所不同:
class DDQN(nn.Module):
def __init__(self, num_inputs, num_outputs):
super(DDQN, self).__init__()
self.feature = nn.Sequential(
nn.Linear(num_inputs, 128),
nn.ReLU())
self.advantage = nn.Sequential(
nn.Linear(128, 128),
nn.ReLU(),
nn.Linear(128, num_outputs))
self.value = nn.Sequential(
nn.Linear(128, 128),
nn.ReLU(),
nn.Linear(128, 1))
def forward(self, x):
x = self.feature(x)
advantage = self.advantage(x)
value = self.value(x)
return value + advantage - advantage.mean()
def act(self, state, epsilon):
if random.random() > epsilon:
state = autograd.Variable(torch.FloatTensor(state).unsqueeze(0),
volatile=True)
q_value = self.forward(state)
action = q_value.max(1)[1].item()
else:
action = random.randrange(env.action_space.n)
return action
- 除了
act函数外,DDQN类几乎完全是新构建的。在init函数中,我们可以看到三个子模型的构建:self.feature、self.value和self.advantage。然后,在forward函数中,我们可以看到输入x是如何被第一个feature子模型转换的,然后输入到优势和价值子模型中。然后,输出advantage和value被用来计算预测值,如下所示:
return value + advantage - advantage.mean()
- 我们可以看到预测值是表示为价值的州价值。这被添加到优势或组合状态-动作值中,并从平均值或平均中减去。结果是最佳优势的预测或智能体可能学习到的优势:
current_model = DDQN(env.observation_space.shape[0], env.action_space.n)
target_model = DDQN(env.observation_space.shape[0], env.action_space.n)
-
下一个变化是我们现在在之前的double DQN示例中构建两个DDQN模型实例,而不是一个DQN。这意味着我们也继续使用两个模型来评估我们的目标。毕竟,我们不想退步。
-
下一个主要变化发生在
compute_td_loss函数中。更新的行如下:
q_values = current_model(state)
next_q_values = target_model(next_state)
q_value = q_values.gather(1, action.unsqueeze(1)).squeeze(1)
next_q_value = next_q_values.max(1)[0]
expected_q_value = reward + gamma * next_q_value * (1 - done)
loss = (q_value - expected_q_value.detach()).pow(2).mean()
-
这实际上简化了前面的代码。现在,我们可以清楚地看到我们的下一个_q_values是从
target_model中获取的。 -
按照您通常的方式运行代码示例,并观察智能体玩Lander。确保您让智能体训练直到它达到一定数量的正奖励。这可能需要您增加训练迭代次数或剧集数。
作为提醒,我们使用术语“episode”来表示一次训练观察或迭代一个时间步。许多例子将使用“frame”和“frames”来表示相同的东西。虽然在某些情况下“frame”可能是合适的,但在其他情况下则不太合适,尤其是在我们开始堆叠帧或输入观察时。如果你觉得这个名字令人困惑,一个替代方案可能是使用“训练迭代”。
你会发现这个算法确实收敛得更快,但正如你所预期的,我们还可以对这个算法进行改进。我们将在下一节中探讨如何改进。
使用优先级经验回放扩展重放
到目前为止,我们已经看到了如何使用重放缓冲区或经验回放机制,使我们能够在稍后的时间以批量的方式拉回值,以便训练网络图。这些数据批次由随机样本组成,效果不错,但当然,我们可以做得更好。因此,我们不仅存储所有数据,还可以做出两个决定:存储哪些数据和哪些数据是优先使用的。为了简化问题,我们只需关注从经验回放中提取数据的优先级。通过优先提取数据,我们希望这能显著提高我们提供给网络用于学习的信息,从而提高整个智能体的性能。
不幸的是,优先级回放背后的想法虽然简单易懂,但在实践中推导和估计则困难得多。我们可以做的是,通过TD误差或预测和实际期望目标的损失来优先级返回事件。因此,我们优先考虑智能体预测中误差最大或智能体错误最多的值。另一种思考方式是,我们优先考虑最令智能体惊讶的事件。回放缓冲区结构化得如此之好,以至于它通过惊讶程度优先级来优先考虑这些事件,然后返回这些事件的样本,但它并不一定按惊讶程度对事件进行排序。在这里,最好是从按惊讶程度排序的桶或分布中随机采样事件。这意味着智能体更有可能选择来自更平均的惊讶事件的样本。
在本节中,我们将使用优先级经验回放机制,该机制首次在以下论文中介绍:https://arxiv.org/pdf/1511.05952.pdf。然后它被编码在PyTorch中,来自这个仓库:https://github.com/higgsfield/RL-Adventure/blob/master/4.prioritized%20dqn.ipynb。我们的实现已被修改,以便在笔记本外运行,并适用于Python 3.6 (https://github.com/higgsfield/RL-Adventure/blob/master/4.prioritized%20dqn.ipynb)).
我们将使用一个全新的样本。打开 Chapter_7_DDQN_wprority.py 并按照以下步骤操作:
- 这个样本的第一个重大变化是将
ReplayBuffer类升级到NaivePrioritizedBuffer,如下所示:
class NaivePrioritizedBuffer(object):
def __init__(self, capacity, prob_alpha=0.6):
self.prob_alpha = prob_alpha
self.capacity = capacity
self.buffer = []
self.pos = 0
self.priorities = np.zeros((capacity,), dtype=np.float32)
def push(self, state, action, reward, next_state, done):
assert state.ndim == next_state.ndim
state = np.expand_dims(state, 0)
next_state = np.expand_dims(next_state, 0)
max_prio = self.priorities.max() if self.buffer else 1.0
if len(self.buffer) < self.capacity:
self.buffer.append((state, action, reward, next_state, done))
else:
self.buffer[self.pos] = (state, action, reward, next_state, done)
self.priorities[self.pos] = max_prio
self.pos = (self.pos + 1) % self.capacity
def sample(self, batch_size, beta=0.4):
if len(self.buffer) == self.capacity:
prios = self.priorities
else:
prios = self.priorities[:self.pos]
probs = prios ** self.prob_alpha
probs /= probs.sum()
indices = np.random.choice(len(self.buffer), batch_size, p=probs)
samples = [self.buffer[idx] for idx in indices]
total = len(self.buffer)
weights = (total * probs[indices]) ** (-beta)
weights /= weights.max()
weights = np.array(weights, dtype=np.float32)
batch = list(zip(*samples))
states = np.concatenate(batch[0])
actions = batch[1]
rewards = batch[2]
next_states = np.concatenate(batch[3])
dones = batch[4]
return states, actions, rewards, next_states, dones, indices, weights
def update_priorities(self, batch_indices, batch_priorities):
for idx, prio in zip(list(batch_indices), [batch_priorities]):
self.priorities[idx] = prio
def __len__(self):
return len(self.buffer)
-
这段代码天真地根据观察到的错误预测分配优先级。然后,它根据优先级顺序对这些值进行排序。接着,它随机抽取这些事件。再次,由于抽样是随机的,但样本是按优先级对齐的,因此随机抽样通常会抽取平均误差的样本。
-
发生的事情是通过重新排序样本,我们重新排序到预期的实际数据分布。因此,为了解决这个问题,我们引入了一个新的因子,称为 beta,或 重要性抽样。Beta 允许我们控制事件的分布,并基本上将它们重置到原始位置:
beta_start = 0.4
beta_episodes = episodes / 10
beta_by_episode = lambda episode: min(1.0,
beta_start + episode * (1.0 - beta_start) / beta_episodes)
plt.plot([beta_by_episode(i) for i in range(episodes)])
- 现在,我们将定义一个函数,使用前面的代码返回随剧集增加的 beta。然后,代码像我们绘制 epsilon 一样绘制 beta,如下所示:

beta 和 epsilon 绘图示例
- 在修改了重放缓冲区中的样本函数之后,我们还需要更新
compute_td_loss函数,如下所示:
def compute_td_loss(batch_size, beta):
state, action, reward, next_state, done, indices,
weights = replay_buffer.sample(batch_size, beta)
state = autograd.Variable(torch.FloatTensor(np.float32(state)))
next_state = autograd.Variable(torch.FloatTensor(np.float32(next_state)))
action = autograd.Variable(torch.LongTensor(action))
reward = autograd.Variable(torch.FloatTensor(reward))
done = autograd.Variable(torch.FloatTensor(done))
weights = autograd.Variable(torch.FloatTensor(weights))
q_values = current_model(state)
next_q_values = target_model(next_state)
q_value = q_values.gather(1, action.unsqueeze(1)).squeeze(1)
next_q_value = next_q_values.max(1)[0]
expected_q_value = reward + gamma * next_q_value * (1 - done)
loss = (q_value - expected_q_value.detach()).pow(2).mean()
prios = loss + 1e-5
loss = loss.mean()
optimizer.zero_grad()
loss.backward()
replay_buffer.update_priorities(indices, prios.data.cpu().numpy())
optimizer.step()
return loss
- 只有前面突出显示的行与我们已经看到的有所不同。第一个区别是返回了两个新的值:
indices和weights。然后,我们可以看到replay_buffer根据之前返回的indices调用update_priorities:
if done:
if episode > buffer_size and avg_reward > min_play_reward:
play_game()
state = env.reset()
all_rewards.append(episode_reward)
episode_reward = 0
- 接下来,在训练循环内部,我们更新了
play_game的调用,并引入了一个新的min_play_reward阈值。这允许我们在渲染游戏之前设置一些最低奖励阈值。渲染游戏可能相当耗时,这将也会加快训练速度:
if len(replay_buffer) > batch_size:
beta = beta_by_episode(episode)
loss = compute_td_loss(batch_size, beta)
losses.append(loss.item())
-
继续在训练循环内部,我们可以看到我们如何提取
beta并在td_compute_loss函数中使用它。 -
再次运行样本。这次,你可能需要等待看到智能体驾驶 Lander,但一旦它这样做,它将表现得相当不错,如下所示:

智能体成功着陆 Lander
通常,在合理短的时间内,智能体将能够持续地将 Lander 安全着陆。算法应该在 75,000 次迭代内收敛到着陆。当然,你可以继续调整和玩转超参数,但这正是我们下一节要讨论的内容。
练习
随着我们在这本书中的进步,这些练习的价值和成本将越来越大。这里的“昂贵”是指你需要投入每个练习的时间将增加。这可能意味着你倾向于做更少的练习,但请继续尝试自己完成两到三个练习:
-
回到 TensorSpace 游戏场,看看你是否能理解池化在这些模型中产生的差异。记住,我们避免使用池化是为了避免丢失空间完整性。
-
打开
Chapter_7_DQN_CNN.py并修改一些卷积层的输入,例如内核或步长大小。看看这会对训练产生什么影响。 -
调整
Chapter_7_DoubleDQN.py的超参数或创建新的超参数。 -
调整
Chapter_7_DDQN.py的超参数或创建新的超参数。 -
调整
Chapter_7_DoubleDQN_wprority.py的超参数或创建新的超参数。 -
将
Chapter_7_DoubleDQN.py转换为使用卷积层,然后升级示例,使其能够与Pong等Atari环境一起工作。 -
将
Chapter_7_DDQN.py转换为使用卷积层,然后升级示例,使其能够与Pong等Atari环境一起工作。 -
将
Chapter_7_DDQN_wprority.py转换为使用卷积层,然后升级示例,使其能够与Pong等Atari环境一起工作。 -
在一个示例中的卷积层之间添加一个池化层。你可能需要查阅PyTorch文档来学习如何做到这一点。
-
你还能如何改进前面示例中的经验回放缓冲区?你还能使用其他形式的回放缓冲区吗?
像往常一样,享受通过示例工作的乐趣。毕竟,如果你不高兴看到你的代码在玩月球探险者或Atari游戏,你什么时候才会开心呢?
在下一节中,我们将总结本章内容,并看看我们将学习什么。
摘要
从我们之前中断的地方继续使用DQN,我们探讨了如何通过添加CNN来扩展这个模型,并添加额外的网络来创建双DQN和对抗DQN,或DDQN。在探索CNN之前,我们简要介绍了视觉观察编码及其必要性。然后,我们简要介绍了CNN,并使用TensorSpace Playground探索了一些知名的最先进模型。接下来,我们将CNN添加到DQN模型中,并使用它来玩Atari游戏环境Pong。之后,我们更详细地研究了如何通过添加另一个网络作为目标,并添加另一个网络来对抗或反驳其他网络来扩展DQN,这也被称为对抗DQN或DDQN。这引入了选择动作时的优势概念。最后,我们探讨了如何扩展经验回放缓冲区,以便我们可以优先处理那里捕获的事件。使用这个框架,我们能够仅通过少量代理训练就轻松地将Lander着陆。
在下一章中,我们将探讨新的策略选择方法,而不再关注全局平均值。相反,我们将使用策略梯度方法来采样分布。
第十章:策略梯度方法
之前,我们的强化学习(RL)方法主要集中在寻找在任何给定状态下选择特定动作的最大值或最佳值。虽然这在之前的章节中对我们来说效果不错,但这种方法当然也存在着它自己的问题,其中之一就是始终需要确定何时实际采取最大或最佳动作,这就是我们的探索/利用权衡。正如我们所看到的,最佳动作并不总是最佳选择,有时取平均最佳动作可能更好。然而,从数学上来说,平均是危险的,它并不能告诉我们代理在环境中实际采样了什么。理想情况下,我们希望有一种方法可以学习环境中每个状态的动作分布。这引入了强化学习中的一个新类别的方法,称为策略梯度(PG)方法,这也是本章的重点。
在本章中,我们将探讨PG方法以及它们如何以许多不同的方式改进我们之前的尝试。我们首先了解PG方法背后的直觉,然后转向第一个方法,REINFORCE。之后,我们将探讨优势函数类别,并介绍演员-评论家方法。从那里,我们将继续探讨深度确定性策略梯度方法以及它们如何用于解决月球着陆问题。然后,我们将介绍一种称为信任区域策略优化的高级方法,以及它是如何根据信任区域估计回报的。
以下是本章我们将重点关注的主要主题总结:
-
理解策略梯度方法
-
介绍 REINFORCE
-
使用优势演员-评论家
-
构建深度确定性策略梯度
-
探索信任区域策略优化
PG方法在数学上比我们之前的尝试要复杂得多,并且更深入地涉及到统计和概率方法。虽然我们将专注于理解这些方法的直觉,而不是数学原理,但对于一些读者来说,这仍然可能令人困惑。如果你发现这样,你可能需要复习一下统计学和概率学,这可能会有所帮助。在下一节中,我们将开始理解PG方法背后的直觉。
本章中所有代码最初都来源于这个GitHub仓库:https://github.com/seungeunrho/minimalRL。原始作者出色地收集了原始资料。按照惯例,代码已经进行了重大修改,以适应本书的风格和其他代码的一致性。
理解策略梯度方法
我们需要了解PG方法的一点是为什么需要它们,以及它们背后的直觉是什么。然后,我们可以在深入研究代码之前简要地介绍一些数学知识。因此,让我们来探讨使用PG方法背后的动机以及它们希望实现的目标,这些目标超越了之前我们研究过的其他方法。我已经总结了为什么/PG方法做什么以及它们试图解决的问题的主要观点:
- 确定性函数与随机函数:我们在科学和数学的早期学习中经常了解到许多问题需要单一的或确定性答案。然而,在现实世界中,我们经常将一定程度的误差等同于确定性计算来量化其准确性。这种量化一个值准确性的方法可以通过随机或概率方法进一步发展。
随机方法通常用于量化风险或不确定性的期望,它们通过找到描述一系列值的分布来实现这一点。而之前我们使用值函数来找到描述动作的最优状态值,现在我们想要了解产生该值的分布。以下图表显示了确定性函数与随机函数输出的一个示例,分布位于均值、中位数、众数和最大值旁边:

偏斜的正态分布
以前,我们假设我们的智能体总是从完美的正态分布中进行采样。这个假设允许我们使用最大值甚至平均值。然而,正态分布永远不会仅仅是正常的,在大多数情况下,环境可能甚至没有接近正态分布。
-
确定性环境与随机环境:我们对所有事物都是正态分布的假设的另一个问题是它往往不是这样,在现实世界中,我们经常需要将环境解释为随机或随机的。我们之前的环境大部分是静态的,这意味着它们在各个剧集之间变化很小。现实世界环境永远不会完全静态,在游戏中更是如此。因此,我们需要一个能够对环境中的随机变化做出反应的算法。
-
离散动作空间与连续动作空间:我们已经花费了一些时间考虑离散与连续的观察空间,并学习了如何通过离散化和深度学习来处理这些环境,但现实世界环境和/或游戏并不总是离散的。也就是说,除了上、下、左、右等离散动作之外,我们现在还需要考虑左10-80%、右10-90%、上10-90%等连续动作。幸运的是,PG方法提供了一种机制,使得连续动作更容易实现。反过来,离散动作空间是可行的,但训练效果不如连续动作空间。
由于算法本身的性质,PG方法在连续动作空间中工作得更好。它们可以用来解决离散动作空间环境,但通常不会像我们后面将要介绍的其他方法表现得那么好。
既然我们已经了解了为什么需要PG方法,那么我们接下来需要在下一节中探讨如何使用它。
策略梯度上升
PG方法背后的基本直觉是我们从寻找描述确定性策略的价值函数转变为具有参数的随机策略,这些参数用于定义策略分布。这样思考,我们现在可以假设我们的策略函数需要被定义,以便我们的策略π可以通过调整参数θ来设置,这样我们就能理解在某个状态下采取特定动作的概率。从数学上讲,我们可以简单地定义如下:

你应该考虑我们在本章中涵盖的数学知识是理解代码所需的最小知识。如果你确实认真考虑开发自己的PG方法扩展,那么你可能想要花些时间进一步探索数学,使用《强化学习导论》(Barto/Sutton,第2版,2017年)。
π表示由参数θ确定的策略,我们计划通过深度学习网络轻松地找到这些参数。现在,我们已经看到我们如何使用深度学习通过梯度下降最小化网络的损失,我们现在将问题颠倒过来。我们现在想要找到那些给出最佳动作概率的参数,对于给定的状态,这些动作应该最大化到1.0或100%。这意味着我们不再减少一个数字,我们现在需要使用梯度上升来最大化它。这也将我们的更新从价值转变为描述策略的参数,并且我们重新编写我们的更新方程如下:

在方程中,我们有以下内容:
-
前一时间步的参数值 -
学习率 -
针对动作a的计算更新梯度,或最优动作
这里的直觉是我们正在推动向产生最佳策略的动作。然而,我们发现,进一步假设所有推动都是相等的同样也是错误的。毕竟,我们应该能够将那些关于价值的确定性预测重新引入前面的方程中,作为对真实价值的进一步指导。我们可以通过以下方式更新最后一个方程:

在这里,我们现在引入以下内容:
:这是我们对于给定状态和动作对的Q值的猜测。
因此,具有更高估计Q值的州-行动对将比那些没有的受益更多,但除了,我们现在必须退一步重新考虑我们的老朋友探索/利用困境,并考虑我们的算法/代理需要如何选择行动。我们不再希望我们的代理只采取最佳或随机行动,而是使用策略本身的学习。这意味着几件事。我们的代理现在需要不断地从同一策略中采样和学习,这意味着PG是按策略的,但也意味着我们需要更新我们的更新方程来考虑这一点,如下所示:

在这里,我们现在引入以下内容:
:这是给定状态下给定行动的概率——本质上,这是策略本身所预测的。
通过将策略在给定状态采取行动的概率进行除法,可以解释该行动可能被采取的频率。因此,如果一个行动比另一个行动流行两倍,那么它只会更新一半的次数,但可能被采取的次数是两倍。再次强调,这试图消除采样频率更高的行动的偏差,并允许算法相应地更重视那些罕见但有益的行动。
现在你已经理解了我们新的更新和过程的基本直觉,我们可以看到它在实际中的应用。在实践中实现PG方法在数学上更困难,但幸运的是,深度学习通过提供梯度上升来缓解这一点,正如我们将在下一节解决我们的第一个实际算法时看到的。
引入REINFORCE
我们将要查看的第一个算法被称为REINFORCE。它以一种非常优雅的方式引入了PG的概念,特别是在PyTorch中,它掩盖了此实现中许多数学复杂性。REINFORCE还通过反向解决优化问题来工作。也就是说,它不是使用梯度上升,而是反转数学,这样我们可以将问题表示为损失函数,从而使用梯度下降。更新方程现在转换为以下形式:

在这里,我们现在假设以下条件:
-
A_hat(s,a)这是由Q_hat(s,a)表示的相对于基线的优势;我们将在稍后更详细地介绍优势函数。 -
▽[θ] log π[θ](s,a)这是现在表示为损失的梯度,并且与π[θt](a*|s) / π[θ](a,s)等价,假设使用链式法则和对1/x = log x的导数。
实质上,我们使用链式法则和性质1/x = log x来翻转方程。再次强调,详细解析数学内容超出了本书的范围,但这里的关键直觉是使用对数函数作为导数技巧,将我们的方程转换为结合优势函数的损失函数。
REINFORCE 代表 REward Increment = Non-negative Factor x Offset Reinforcement x Characteristic Eligibility**。这个缩写试图描述算法本身的数学直觉,其中非负因子代表优势函数,A_hat。偏置强化是梯度本身,表示为▽。然后,我们引入特征有效性,这使我们回到使用π[θ]学习 TD 和有效性迹的学习。通过α或学习率来缩放整个因子,使我们能够调整算法/代理学习的速度。
能够直观地调整超参数,学习率(alpha)和折扣因子(gamma),应该是一项你已经开始掌握的技能。然而,PG 方法带来了关于代理想要/需要如何学习的不同直觉。因此,请确保花同样多的时间来理解调整这些值是如何改变的。
当然,作为游戏程序员,我们理解这一点的最好方式是与代码一起工作,这正是我们在下一个练习中将要做的。打开示例 Chapter_8_REINFORCE.py 并遵循这里的练习:
- 在 PyTorch 中,REINFORCE 成为一个紧凑的算法,整个代码列表如下所示:
import gym
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torch.distributions import Categorical
#Hyperparameters
learning_rate = 0.0002
gamma = 0.98
class REINFORCE(nn.Module):
def __init__(self, input_shape, num_actions):
super(REINFORCE, self).__init__()
self.data = []
self.fc1 = nn.Linear(input_shape, 128)
self.fc2 = nn.Linear(128, num_actions)
self.optimizer = optim.Adam(self.parameters(), lr=learning_rate)
def act(self, x):
x = F.relu(self.fc1(x))
x = F.softmax(self.fc2(x), dim=0)
return x
def put_data(self, item):
self.data.append(item)
def train_net(self):
R = 0
for r, log_prob in self.data[::-1]:
R = r + gamma * R
loss = -log_prob * R
self.optimizer.zero_grad()
loss.backward()
self.optimizer.step()
self.data = []
env = gym.make('LunarLander-v2')
pi = REINFORCE(env.observation_space.shape[0], env.action_space.n)
score = 0.0
print_interval = 100
iterations = 10000
min_play_reward = 20
def play_game():
done = False
state = env.reset()
its = 500
while(not done and its > 0):
its -= 1
prob = pi.act(torch.from_numpy(state).float())
m = Categorical(prob)
action = m.sample()
next_state, reward, done, _ = env.step(action.item())
env.render()
state = next_state
for iteration in range(iterations):
s = env.reset()
for t in range(501):
prob = pi.act(torch.from_numpy(s).float())
m = Categorical(prob)
action = m.sample()
s_prime, r, done, info = env.step(action.item())
pi.put_data((r,torch.log(prob[action])))
s = s_prime
score += r
if done:
if score/print_interval > min_play_reward:
play_game()
break
pi.train_net()
if iteration%print_interval==0 and iteration!=0:
print("# of episode :{}, avg score : {}".format(iteration, score/print_interval))
score = 0.0
env.close()
-
如同往常,我们从我们常用的导入开始,增加了一个来自
torch.distributions的新导入,名为Categorical。现在,Categorical用于从连续概率空间采样我们的动作空间到离散动作值。之后,我们初始化我们的基本超参数,learning_rate和gamma。 -
接下来,我们来到一个新的类
REINFORCE,它封装了我们的代理算法的功能。我们在 DQN 和 DDQN 配置中已经看到了大部分代码。然而,我们想要关注的是这里所示的训练函数train_net。
def train_net(self):
R = 0
for r, log_prob in self.data[::-1]:
R = r + gamma * R
loss = -log_prob * R
self.optimizer.zero_grad()
loss.backward()
self.optimizer.step()
self.data = []
-
train_net是我们使用损失计算来推动(反向传播)策略网络中的错误的地方。注意,在这个课程中,我们不使用重放缓冲区,而是仅使用一个名为data的列表。也应该清楚,我们将列表中的所有值都反向通过网络。 -
在类定义之后,我们跳转到创建环境和设置一些额外的变量,如下所示:
env = gym.make('LunarLander-v2')
pi = REINFORCE(env.observation_space.shape[0], env.action_space.n)
score = 0.0
print_interval = 100
iterations = 10000
min_play_reward = 20
-
你可以看到我们又回到了玩月球着陆环境。其他变量与我们之前用来控制训练量和输出结果频率的变量相似。如果你将其更改为不同的环境,你很可能需要调整这些值。
-
再次,训练迭代代码与我们之前的例子非常相似,唯一的显著区别是我们如何在环境中采样和执行动作。以下是完成这一部分的代码:
prob = pi.act(torch.from_numpy(s).float())
m = Categorical(prob)
action = m.sample()
s_prime, r, done, info = env.step(action.item())
pi.put_data((r,torch.log(prob[action])))
- 这里要注意的主要事情是我们正在从 REINFORCE 生成的策略中提取动作的概率,使用
pi.act。之后,我们使用Categorical将这个概率转换为分类或离散的值箱。然后我们使用m.sample()提取离散的动作值。这种转换对于离散动作空间,如月球着陆 v2 环境,是必要的。
之后,我们将看到如何在不需要转换的情况下将其应用于连续空间环境。如果你滚动到 play_game 函数,你会注意到相同的代码块用于在玩游戏时从策略中提取动作。特别注意最后一行,其中使用了 pi.put_data 来存储结果,并注意我们是如何在 prop[action] 值上使用 torch.log 的。记住,通过在这里使用对数函数,我们转换或反转了使用梯度上升来最大化动作值的需求。相反,我们可以使用梯度下降和 backprop 在我们的策略网络上。
- 按照你通常的方式运行代码并观察结果。这个算法通常训练得很快。
这个算法的优雅性,尤其是在 PyTorch 中,将这里的复杂数学美妙地掩盖了。不幸的是,除非你理解了直觉,否则这可能不是一件好事。在下一节中,我们将探讨上一节练习中提到的优势函数,并看看这与演员-评论家方法有何关联。
使用优势演员-评论家
我们已经在几个前几章中讨论了几次优势的概念,包括最后一个练习。优势通常被认为是在理解将不同的代理/策略应用于相同问题之间的差异。该算法学习优势,从而提供增强奖励的好处。这有点抽象,让我们看看这如何应用于我们之前的一个算法,比如 DDQN。在 DDQN 中,优势是通过理解如何缩小移动到已知目标或目标的差距来定义的。如果你需要复习,请参考第 7 章,《DDQN 深入研究》。
优势的概念可以扩展到我们所说的演员-评论家方法。在演员-评论家方法中,我们通过训练两个网络来定义优势,一个作为演员;也就是说,它对策略做出决策,另一个网络根据预期回报对这些决策进行评论。现在的目标不仅是要优化演员和评论家,而且要以减少意外情况的方式去做。你可以把意外想象成代理可能期望获得一些奖励,但结果却没有或可能获得了更多奖励的时刻。在AC方法中,目标是最小化意外,它通过使用基于价值的批评方法(DQN)作为评论家和PG(REINFORCE)方法作为演员来实现。参见以下图表,了解这是如何结合在一起的:

解释演员评论家方法
在下一节中,我们将深入探讨如何将AC应用于我们之前的PG示例。
演员评论家
AC方法使用网络组合来预测价值和策略函数的输出,其中我们的价值函数网络类似于DQN,我们的策略函数使用PG方法(如REINFORCE)定义。现在,这基本上就像听起来那么简单;然而,我们在编码这些实现的方式中有几个细节需要注意。因此,在审查代码时,我们将详细介绍这个实现的细节。打开Chapter_8_ActorCritic.py并跟随下一个练习:
- 由于此代码遵循与之前示例相同的模式,我们只需要详细说明几个部分。最重要的部分是文件顶部的
ActorCritic类,如下所示:
class ActorCritic(nn.Module):
def __init__(self, input_shape, num_actions):
super(ActorCritic, self).__init__()
self.data = []
self.fc1 = nn.Linear(input_shape,256)
self.fc_pi = nn.Linear(256,num_actions)
self.fc_v = nn.Linear(256,1)
self.optimizer = optim.Adam(self.parameters(), lr=learning_rate)
def pi(self, x, softmax_dim = 0):
x = F.relu(self.fc1(x))
x = self.fc_pi(x)
prob = F.softmax(x, dim=softmax_dim)
return prob
def v(self, x):
x = F.relu(self.fc1(x))
v = self.fc_v(x)
return v
def put_data(self, transition):
self.data.append(transition)
def make_batch(self):
s_lst, a_lst, r_lst, s_prime_lst, done_lst = [], [], [], [], []
for transition in self.data:
s,a,r,s_prime,done = transition
s_lst.append(s)
a_lst.append([a])
r_lst.append([r/100.0])
s_prime_lst.append(s_prime)
done_mask = 0.0 if done else 1.0
done_lst.append([done_mask])
s_batch, a_batch, r_batch, s_prime_batch, done_batch
= torch.tensor(s_lst, dtype=torch.float),
torch.tensor(a_lst), \
torch.tensor(r_lst, dtype=torch.float),
torch.tensor(s_prime_lst, dtype=torch.float), \
torch.tensor(done_lst, dtype=torch.float)
self.data = []
return s_batch, a_batch, r_batch, s_prime_batch, done_batch
def train_net(self):
s, a, r, s_prime, done = self.make_batch()
td_target = r + gamma * self.v(s_prime) * done
delta = td_target - self.v(s)
pi = self.pi(s, softmax_dim=1)
pi_a = pi.gather(1,a)
loss = -torch.log(pi_a) * delta.detach()
+ F.smooth_l1_loss(self.v(s), td_target.detach())
self.optimizer.zero_grad()
loss.mean().backward()
self.optimizer.step()
-
从
init函数开始,我们可以看到我们构建了三个Linear网络层:fc1和fc_pi用于policy,fc_v用于value。然后,在init之后,我们看到pi和v函数。这些函数对每个网络(pi和v)进行前向传递。注意这两个网络都共享fc1作为输入层。这意味着我们网络的第一层将用于以演员和评论家网络共享的形式编码网络状态。在更高级的网络配置中,共享这样的层是常见的。 -
接下来,我们看到
put_data函数,它只是将记忆放入重放或经验缓冲区。 -
之后,我们有一个名为
make_batch的强大函数,它只是构建我们在经验重放中使用的批量数据。 -
我们将跳过
ActorCritic训练函数train_net,并跳到下面的迭代训练代码,如下所示:
for iteration in range(iterations):
done = False
s = env.reset()
while not done:
for t in range(n_rollout):
prob = model.pi(torch.from_numpy(s).float())
m = Categorical(prob)
a = m.sample().item()
s_prime, r, done, info = env.step(a)
model.put_data((s,a,r,s_prime,done))
s = s_prime
score += r
if done:
if score/print_interval > min_play_reward:
play_game()
break
model.train_net()
if iteration%print_interval==0 and iteration!=0:
print("# of episode :{},
avg score : {:.1f}".format(iteration, score/print_interval))
score = 0.0
env.close()
-
你可能没有注意到,但我们的最后一个练习使用了基于事件的训练或我们所说的蒙特卡洛或离线策略训练。这次,我们的训练是在策略下进行的,这意味着我们的代理在接收到新的更新后立即采取行动。否则,代码与其他许多示例非常相似,并且可以运行。
-
按照常规方式运行示例。训练可能需要一段时间,所以先启动它,然后回到这本书。
现在我们已经了解了示例代码的基本布局,是时候进入下一节训练的细节了。
训练优势 AC
使用优势并训练多个网络协同工作,正如您所想象的那样,并不简单。因此,我们想要专注于整个练习来理解 AC 中的训练工作。再次打开 Chapter_8_ActorCritic.py 并跟随练习:
- 我们的主要关注点将是之前看到的
ActorCritic类中的train_net函数。从前两行开始,我们可以看到这是训练批次首先被创建的地方,我们计算td_target。回想一下,当我们实现 DDQN 时,我们覆盖了 TD 错误计算的形式检查:
s, a, r, s_prime, done = self.make_batch()
td_target = r + gamma * self.v(s_prime) * done
- 接下来,我们计算目标函数和值函数之间的变化或增量。同样,这在 DDQN 中已经覆盖了,执行此操作的代码如下:
delta = td_target - self.v(s)
- 之后,我们使用
self.pi在 π 网络上执行前向传递,然后收集结果。收集函数本质上是对数据进行对齐或收集。感兴趣的读者应查阅 PyTorch 网站以获取有关gather的进一步文档。此步骤的代码如下:
pi = self.pi(s, softmax_dim=1)
pi_a = pi.gather(1,a)
- 然后,我们使用以下代码计算损失:
loss = -torch.log(pi_a) * delta.detach()
+ F.smooth_l1_loss(self.v(s), td_target.detach())
-
损失是通过更新策略方法计算的,其中我们使用对数来对动作进行逆优化。回想一下,在我们之前的讨论中,介绍了
函数。此函数表示优势函数,其中我们取策略的负对数并将其添加到值函数 v和td_target的 L1 平方误差输出中。张量上的detach函数仅允许网络在训练时不对这些值进行更新。 -
最后,我们使用以下代码将损失反向传递到网络中:
self.optimizer.zero_grad()
loss.mean().backward()
self.optimizer.step()
- 这里没有什么新的内容。代码首先将梯度置零,然后计算批次的平均损失,并通过调用
backward将其反向传递,最后使用step步进优化器完成。
您将需要调整此示例中的超参数来训练一个能够完成环境的智能体。当然,您现在完全能够接受这个挑战。在下一节中,我们将向上移动并查看另一类 PG 方法。
构建深度确定性策略梯度
我们在PG方法中面临的一个问题是可变性或过多的随机性。当然,我们可能期望从随机策略中采样时会出现这种情况。深度确定性策略梯度(DDPG)方法是在2015年由Tim Lillicrap发表的一篇题为《使用深度强化学习进行连续控制》的论文中提出的。它的目的是解决通过连续动作空间控制动作的问题,这是我们之前一直避免的问题。记住,连续动作空间与离散空间的不同之处在于,动作可以指示一个方向,也可以指示一个量或值,这表达了在该方向上的努力程度,而离散动作中,任何动作选择都被假定为始终是100%的努力。
那么,这有什么关系呢?好吧,在我们上一章的练习中,我们探索了离散动作空间上的PG方法。通过在离散空间中使用这些方法,我们本质上通过将动作概率转换为离散值来缓冲或掩盖了可变性的问题。然而,在具有连续控制或连续动作空间的环境中,这并不奏效。于是出现了DDPG。答案就在其名称中:深度确定性策略梯度,本质上意味着我们正在将确定性重新引入PG方法,以纠正可变性的问题。
本章我们将介绍的最后两种PG方法,即DDPG和TRPO,通常被认为是特定需求的,并且在某些情况下过于复杂,难以有效实现。因此,在过去的几年里,这些方法在更先进的发展中并没有得到太多应用。这些方法的代码已经提供以供完整性,但解释可能有些仓促。
让我们通过打开Chapter_8_DDPG.py并跟随下一个练习来看看这代码是如何实现的:
- 这个示例的完整源代码太大,无法全部列出。相反,我们将通过练习中的相关部分进行讲解,从超参数开始,如下所示:
lr_mu = 0.0005
lr_q = 0.001
gamma = 0.99
batch_size = 32
buffer_limit = 50000
tau = 0.005
-
看起来我们介绍了一些新的超参数,但实际上我们只介绍了一个新的参数,称为
tau。其他变量lr_mu和lr_q是两个不同网络的 学习率。 -
接下来,我们跳过了
ReplayBuffer类,这个类我们之前已经见过,用于存储经验,然后继续跳过其他代码,直到我们到达环境设置和更多变量定义的部分,如下所示:
env = gym.make('Pendulum-v0')
memory = ReplayBuffer()
q, q_target = QNet(), QNet()
q_target.load_state_dict(q.state_dict())
mu, mu_target = MuNet(), MuNet()
mu_target.load_state_dict(mu.state_dict())
score = 0.0
print_interval = 20
min_play_reward = 0
iterations = 10000
mu_optimizer = optim.Adam(mu.parameters(), lr=lr_mu)
q_optimizer = optim.Adam(q.parameters(), lr=lr_q)
ou_noise = OrnsteinUhlenbeckNoise(mu=np.zeros(1))
- 首先,我们看到新环境的设置,
Pendulum。现在,Pendulum是一个连续控制环境,需要学习连续空间动作。之后,创建了memory和ReplayBuffer,接着创建了两个名为QNet和MuNet的类。接下来,初始化了更多的控制/监控参数。在最后一行之前,我们看到创建了两个优化器,mu_optimizer和q_optimizer,分别用于MuNet和QNet网络。最后,在最后一行,我们看到创建了一个新的张量ou_noise。这里有很多新的事情在进行,但我们很快就会看到这一切是如何结合在一起的:
for iteration in range(iterations):
s = env.reset()
for t in range(300):
- 接下来,向下移动到前面行中显示的训练循环的顶部。我们确保算法可以完全循环通过一个场景。因此,我们将内循环的范围设置为高于智能体在环境中获得的迭代次数的值:
a = mu(torch.from_numpy(s).float())
a = a.item() + ou_noise()[0]
s_prime, r, done, info = env.step([a]) memory.put((s,a,r/100.0,s_prime,done))
score +=r
s = s_prime
- 接下来是试错训练代码。请注意,
a动作是从名为mu的网络中提取的。然后,在下一行,我们将ou_noise值添加到其中。之后,我们让智能体迈出一步,将结果存入记忆中,并更新分数和状态。我们这里使用的噪声值基于 Ornstein-Uhlenbeck 过程,并由同名类生成。这个过程生成一个移动的随机值,倾向于收敛到值
或 mu。
回想一下,我们在 OrnsteinUhlenbeckNoise 类的早期实例化中将此值初始化为零。这里的直觉是我们希望噪声在实验过程中收敛到 0。这具有控制智能体执行探索量的效果。更多的噪声会导致它选择动作的不确定性增加,因此智能体会更随机地选择。你现在可以将动作中的噪声视为智能体在该动作中具有的不确定性以及它需要探索多少以减少这种不确定性。
在这里使用 Ornstein-Uhlenbeck 过程来生成噪声,因为它以随机但可预测的方式收敛,即它总是收敛。当然,你可以在这里使用任何你喜欢的噪声值,甚至更确定性的东西。
- 在训练循环内部,我们跳转到执行实际训练的代码部分:
if memory.size()>2000:
for i in range(10):
train(mu, mu_target, q, q_target, memory, q_optimizer, mu_optimizer)
soft_update(mu, mu_target)
soft_update(q, q_target)
- 我们可以看到,一旦记忆
ReplayBuffer超过2000,智能体就开始以 10 个循环进行训练。首先,我们看到对train函数的调用,其中包含构建的各种网络/模型mu、mu_target、q和q_target;memory;以及q_optimizer和mu_optimizer优化器。然后,有两个对soft_update函数的调用,使用各种模型。这里显示的soft_update只是通过使用tau缩放每次迭代的改变量,以迭代方式将输入模型收敛到目标:
def soft_update(net, net_target):
for param_target, param in zip(net_target.parameters(), net.parameters()):
param_target.data.copy_(param_target.data * (1.0 - tau) + param.data * tau)
- 这种从某些演员模型到目标的收敛并不新鲜,但是随着AC的引入,它确实使事情复杂化了。不过,在我们到达那里之前,让我们运行这个示例并看看它是如何运作的。像平常一样运行代码并等待:这个可能需要一段时间。如果你的智能体达到足够高的分数,你将获得以下奖励:

这是摆锤环境的示例
在运行示例时,特别关注屏幕上分数的更新。尽量感受一下这可能会在图形上看起来如何。在下一节中,我们将探讨这个最后示例的更详细细节。
训练DDPG
现在,正如你可能在上一个示例中注意到的,Chapter_8_DDPG.py正在使用四个网络/模型进行训练,使用两个网络作为演员,两个作为评论者,但也使用两个网络作为目标,两个作为当前。这给我们以下图示:

演员评论者目标-当前网络图
前一个图中的每个椭圆形代表一个完整的深度学习网络。注意评论者,即价值或Q网络实现,正在接受环境输出的奖励和状态。然后评论者将一个值推回给演员或策略目标网络。
打开示例Chapter_8_DDPG.py,然后按照下一个练习来查看代码是如何组合在一起的:
- 我们将首先查看这里显示的评论者或
QNet网络类的定义:
class QNet(nn.Module):
def __init__(self):
super(QNet, self).__init__()
self.fc_s = nn.Linear(3, 64)
self.fc_a = nn.Linear(1,64)
self.fc_q = nn.Linear(128, 32)
self.fc_3 = nn.Linear(32,1)
def forward(self, x, a):
h1 = F.relu(self.fc_s(x))
h2 = F.relu(self.fc_a(a))
cat = torch.cat([h1,h2], dim=1)
q = F.relu(self.fc_q(cat))
q = self.fc_3(q)
return q
- 这个网络的构建也略有不同,这里发生的事情是
fc_s层编码状态,然后fc_a编码动作。这两个层在正向传递中连接,创建一个单一的Q层,fc_q,然后通过最后一层,fc_3输出。
如果你需要帮助想象这些类型的网络,绘制它们通常很有帮助。这里的关键是查看训练函数中的代码,它描述了层是如何连接的。
- 从评论者网络转移到由
MuNet类定义的演员网络,如下所示:
class MuNet(nn.Module):
def __init__(self):
super(MuNet, self).__init__()
self.fc1 = nn.Linear(3, 128)
self.fc2 = nn.Linear(128, 64)
self.fc_mu = nn.Linear(64, 1)
def forward(self, x):
x = F.relu(self.fc1(x))
x = F.relu(self.fc2(x))
mu = torch.tanh(self.fc_mu(x))*2
return mu
-
MuNet是一个将状态从3个值编码到128个输入神经元fc1的简单网络实现,然后是64个隐藏层神经元fc2,最后输出到输出层上的单个值fc_mu。唯一值得注意的注释是我们如何将fc_mu,forward函数中的输出层,转换为mu输出值。这是为了考虑到Pendulum环境中的控制范围,该环境接受-2到2的动作值。如果你将这个示例转换到另一个环境,请确保考虑到动作空间值的变化。 -
接下来,我们将向下移动到
train函数的开始,如下所示:
def train(mu, mu_target, q, q_target, memory, q_optimizer, mu_optimizer):
s,a,r,s_prime,done_mask = memory.sample(batch_size)
train函数接受所有网络、记忆和优化器作为输入。在第一行,它从replayBuffer记忆中提取s状态、a动作、r奖励、s_prime下一个状态和done_mask:
target = r + gamma * q_target(s_prime, mu_target(s_prime))
q_loss = F.smooth_l1_loss(q(s,a), target.detach())
q_optimizer.zero_grad()
q_loss.backward()
q_optimizer.step()
- 函数内部的第一块代码根据
q_target网络的输出计算目标值,该网络以最后一个状态s_prime和从最后一个状态输出的mu_target作为输入。然后,我们根据使用target值作为目标的输入状态和动作来计算q_loss损失。这种有些抽象的转换是为了将值从随机转换为确定性。在最后三行中,我们看到典型的优化器代码用于归零梯度并进行反向传播:
mu_loss = -q(s,mu(s)).mean()
mu_optimizer.zero_grad()
mu_loss.backward()
mu_optimizer.step()
-
计算
mu_loss策略损失要简单得多,我们只需使用mu网络的状态和输出动作来获取q网络的输出。需要注意的是,我们将损失设置为负值并取平均值。然后,我们使用典型的优化器反向传播来完成mu_loss函数。 -
如果智能体仍在运行之前的练习,请用这种新获得的知识检查结果。考虑如何或可以调整哪些超参数来改善这个示例的结果。
对于一些人来说,DDPG的纯代码解释可能有点抽象,但希望不是。希望到这一点,你可以阅读代码并假设你需要理解概念所需的数学或直觉。在下一节中,我们将探讨被认为是DRL中更复杂的方法之一,即信任区域策略优化(TRPO)方法。
探索信任区域策略优化
PG方法存在几个技术问题,其中一些你可能已经注意到了。这些问题在训练中表现出来,你可能已经观察到缺乏训练收敛或波动。这是由几个我们可以总结的因素造成的:
- 梯度上升与梯度下降:在PG中,我们使用梯度上升假设最大动作值位于山顶。然而,我们选择的优化方法(SGD或ADAM)是针对梯度下降或寻找山谷或平坦区域的值进行调优的,这意味着它们在寻找山谷底部时表现良好,但在寻找脊顶时表现不佳,尤其是如果脊或山很陡。这里展示了这种比较:

梯度下降与上升的比较
因此,找到峰值成为问题,尤其是在需要精细控制或窄离散动作的环境中。这通常表现为训练波动,其中智能体不断增加分数,但每隔一段时间就会后退几步。
-
策略与参数空间映射:根据其本质,我们通常需要将策略映射到某个已知动作空间,无论是通过连续变换还是离散化变换。这一步,不出所料,并非没有问题。离散化动作空间可能特别有问题,并进一步加剧了之前提到的爬山问题。
-
静态与动态学习率:部分原因是前面提到的优化器问题,我们也倾向于发现使用静态学习率是有问题的。也就是说,我们经常发现,随着智能体继续找到那些最大动作山峰的顶峰,我们需要降低学习率。
-
策略采样效率:PG方法限制我们在每个轨迹中只能更新策略一次。如果我们尝试更频繁地更新,例如在
x步之后,我们会看到训练发散。因此,我们被限制在每个轨迹或回合中只更新一次。这可能在具有多个步骤的训练环境中提供非常低的样本效率。
TRPO和另一种名为近端策略优化(proximal policy optimization)的PG方法,我们将在第9章(2f6812c0-fd1f-4eda-9df2-6c67c8077aec.xhtml)中探讨,异步动作和政策,试图使用几种常见策略解决所有这些问题。
这个TRPO实现的代码直接来源于https://github.com/ikostrikov/pytorch-trpo,在写作时,代码仅稍作修改以允许更容易地运行。这是一个很好的例子,值得进一步探索和改进。
在我们开始审查这些策略之前,让我们打开代码并跟随下一个练习:
-
TRPO是一个庞大的算法,不容易在一个文件中运行。我们将首先通过在源文件夹中打开TRPO文件夹来查看代码结构。这个例子涵盖了多个文件中的代码,我们在这里只会审查一小部分。建议你在继续之前快速全面地审查源代码。
-
参考一下
main.py文件;这是一个启动文件。main在运行时接受在这个文件中定义的几个参数作为输入。 -
滚动到大约中间位置,你将看到环境是如何在主要策略和价值网络上构建的,如图所示:
env = gym.make(args.env_name)
num_inputs = env.observation_space.shape[0]
num_actions = env.action_space.shape[0]
env.seed(args.seed)
torch.manual_seed(args.seed)
policy_net = Policy(num_inputs, num_actions)
value_net = Value(num_inputs)
- 接下来,继续向下滚动,直到你到达那个熟悉的训练循环。大部分应该看起来与其他例子相似,除了引入了另一个
while循环,如下所示:
while num_steps < args.batch_size:
-
这段代码确保一个智能体(agent)的回合(episode)由
batch_size决定的给定步数组成。然而,我们仍然不会在环境表示回合完成之前打破内部训练循环。但是,现在,只有在达到提供的batch_size之后,才会完成一个回合或轨迹更新。这试图解决我们之前讨论过的PG方法采样问题。 -
快速审查每个源文件;以下列表总结了每个文件的目的:
-
main.py:这是启动源文件,也是智能体训练的主要点。 -
conjugate_gradients.py:这是一个辅助方法,用于共轭或连接梯度。 -
models.py: 此文件定义了网络类 Policy(演员)和 value(评论家)。这些网络的构建有点独特,所以请确保查看。 -
replay_memory.py: 这是一个辅助类,用于包含重放内存。 -
running_state.py: 这是一个辅助类,用于计算状态的运行方差,本质上,是均值和标准差的运行估计。这对于对正态分布进行任意采样是有益的。 -
trpo.py: 这是针对 TRPO 的特定代码,旨在解决我们之前提到的 PG 问题。 -
utils.py: 这提供了一些辅助方法。
-
-
使用默认的起始参数,像通常运行 Python 文件一样运行
main.py,并观察输出,如下所示:

TRPO 样本的输出
此特定实现的输出更为详细,并显示了监控我们提到的问题(PG 方法遭受的问题)的性能因素。在接下来的几节中,我们将通过练习展示 TRPO 如何尝试解决这些问题。
Jonathan Hui (https://medium.com/@jonathan_hui) 在 Medium.com 上有几篇关于 DRL 算法各种实现的优秀文章。他在解释 TRPO 和其他更复杂方法背后的数学方面做得特别出色。
共轭梯度
我们需要用策略方法解决的基本问题是将梯度上升转换为自然梯度形式。以前,我们通过简单地应用对数函数来处理这个梯度的共轭。然而,这并不产生自然梯度。自然梯度不易受模型参数化影响,并提供了一种不变的方法来计算稳定的梯度。让我们通过再次打开我们的 IDE 到 TRPO 示例并跟随下一个练习来看看这是如何实现的:
- 在
TRPO文件夹中打开trpo.py文件。此文件中的三个函数旨在解决我们遇到的 PG 问题的各种问题。我们遇到的第一问题是反转梯度,执行此操作的代码如下所示:
def conjugate_gradients(Avp, b, nsteps, residual_tol=1e-10):
x = torch.zeros(b.size())
r = b.clone()
p = b.clone()
rdotr = torch.dot(r, r)
for i in range(nsteps):
_Avp = Avp(p)
alpha = rdotr / torch.dot(p, _Avp)
x += alpha * p
r -= alpha * _Avp
new_rdotr = torch.dot(r, r)
betta = new_rdotr / rdotr
p = r + betta * p
rdotr = new_rdotr
if rdotr < residual_tol:
break
return x
-
conjugate_gradients函数被迭代使用,以产生一个更自然、更稳定的梯度,我们可以用它来进行上升。 -
滚动到
trpo_step函数,你将看到这个方法如何在代码中展示使用:
stepdir = conjugate_gradients(Fvp, -loss_grad, 10)
- 这输出一个
stepdir张量,表示用于移动网络的梯度。我们可以通过输入参数看到,输出共轭梯度将通过一个近似函数Fvp和损失梯度的逆loss_grad在 10 次迭代中求解。这与其他一些优化纠缠在一起,所以我们现在暂停。
共轭梯度是我们可以使用的一种方法,以更好地管理 PG 方法中遇到的梯度下降与梯度上升问题。接下来,我们将再次探讨优化,以解决梯度上升的问题。
信任区域方法
我们可以对梯度上升进行进一步优化的方法是使用信任区域或更新的控制区域。这些方法当然是 TRPO 的基础,但这个概念被进一步扩展到其他基于策略的方法。在 TRPO 中,我们使用 最小化-最大化 或 MM 算法在近似函数上扩展信任区域。MM 的直觉是存在一个下界函数,我们可以预期回报/奖励总是高于这个下界。因此,如果我们最大化这个下界函数,我们也会得到最佳策略。默认情况下,梯度下降是一个线搜索算法,但这又引入了超调的问题。相反,我们首先可以近似步长,然后在那个步长内建立一个信任区域。这个信任区域然后成为我们优化的空间。
我们经常用来解释这个概念的类比是让你想象自己在爬一个狭窄的山脊。你面临从山脊两边掉下去的风险,所以使用正常的梯度下降或线搜索变得危险。相反,你决定为了避免掉下去,你想踩在山脊的中心或你信任的中心区域。以下是从 Jonathan Hui 的博客文章中截取的屏幕截图,进一步展示了这个概念:

线搜索与信任区域的比较
我们可以通过打开 TRPO 文件夹并跟随下一个练习来查看代码中的样子:
- 再次打开
trpo.py并向下滚动到以下代码块:
def linesearch(model, f, x, fullstep, expected_improve_rate,
max_backtracks=10, accept_ratio=.1):
fval = f(True).data
print("fval before", fval.item())
for (_n_backtracks, stepfrac) in enumerate(.5**np.arange(max_backtracks)):
xnew = x + stepfrac * fullstep
set_flat_params_to(model, xnew)
newfval = f(True).data
actual_improve = fval - newfval
expected_improve = expected_improve_rate * stepfrac
ratio = actual_improve / expected_improve
print("a/e/r", actual_improve.item(), expected_improve.item(),
ratio.item())
if ratio.item() > accept_ratio and actual_improve.item() > 0:
print("fval after", newfval.item())
return True, xnew
return False, x
linesearch函数用于确定我们想要在山脊上定位下一个信任区域的距离。这个函数用于指示到下一个信任区域的距离,并使用以下代码执行:
success, new_params = linesearch(model, get_loss, prev_params, fullstep,
neggdotstepdir / lm[0])
- 注意到
neggdotstepdir的使用。这个值是从我们在上一个练习中计算的步长方向stepdir计算出来的,以下代码所示:
neggdotstepdir = (-loss_grad * stepdir).sum(0, keepdim=True)
- 现在我们有了
neggdotstepdir方向和linesearch数量,我们可以用以下代码确定信任区域:
set_flat_params_to(model, new_params)
set_flat_params_to函数位于utils.py文件中,代码如下所示:
def set_flat_params_to(model, flat_params):
prev_ind = 0
for param in model.parameters():
flat_size = int(np.prod(list(param.size())))
param.data.copy_(
flat_params[prev_ind:prev_ind + flat_size].view(param.size()))
prev_ind += flat_size
- 这段代码本质上是将参数平坦化到信任区域。这是我们用来测试下一步是否在其中的信任区域,使用
linesearch函数。
现在我们理解了信任区域的概念以及在使用 PG 方法时正确控制步长、方向和数量的必要性。在下一节中,我们将查看步骤本身。
TRPO 步骤
如你现在所看到的,使用 TRPO 进行步骤或更新并不简单,事情还会变得更加复杂。这个步骤本身要求智能体从更新策略和值函数中学习几个因素,以获得优势,也称为演员-评论家。理解步骤函数的实际细节超出了本书的范围,你再次被推荐参考外部参考资料。然而,回顾 TRPO 中步骤的构成以及这与我们未来将要探讨的其他方法的复杂度比较可能是有帮助的。再次打开样本 TRPO 文件夹,并遵循下一个练习:
- 打开
main.py文件,找到大约在第 130 行的以下代码行:
trpo_step(policy_net, get_loss, get_kl, args.max_kl, args.damping)
-
这最后一行代码位于
update_params函数中,这是大部分训练发生的地方。 -
你可以在
main.py文件几乎最底部看到对update_params函数的调用,其中batch是从memory中抽取的样本,如下面的代码所示:
batch = memory.sample()
update_params(batch)
- 滚回
update_params函数,注意第一个循环使用以下代码构建returns、deltas和advantages:
for i in reversed(range(rewards.size(0))):
returns[i] = rewards[i] + args.gamma * prev_return * masks[i]
deltas[i] = rewards[i] + args.gamma * prev_value *
masks[i] - values.data[i]
advantages[i] = deltas[i] + args.gamma * args.tau *
prev_advantage * masks[i]
-
注意我们是如何反转奖励,然后通过它们循环以构建我们的各种列表
returns、deltas和advantages。 -
从那里,我们将参数展开并设置值网络,即评论家。然后,我们计算优势、动作均值和标准差。我们在处理分布而非确定性值时这样做。
-
之后,我们使用
trpo_step函数进行一次训练步骤或策略更新。
你可能已经注意到了源代码中使用了 kl。这代表KL散度,我们将在后面的章节中探讨。
保持示例运行大约 5,000 次训练迭代。这可能需要一些时间,所以请耐心等待。如果可能的话,完成整个运行过程是值得的。本节中的 TRPO 示例旨在进行实验和使用,以各种控制环境进行测试。在下一节中,请确保回顾你可以尝试的实验,以探索更多关于这种方法的信息。
练习
使用这些练习来享受学习,并获取额外的经验。深度学习和深度强化学习是非常需要通过实际操作示例来提高知识的领域。不要期望在训练智能体时能自然而然地成功;这需要大量的尝试和错误。幸运的是,我们需要的经验量并不像我们那些表现不佳的智能体那么大,但仍然需要投入一些时间。
-
打开示例文件
Chapter_8_REINFORCE.py,将其备份并修改超参数,以查看这对训练有何影响。 -
打开示例文件
Chapter_8_ActorCritic.py,将其备份并修改超参数,以查看这对训练有何影响。 -
打开示例文件
Chapter_8_DDPG.py,将其备份并修改超参数,以查看这对训练有何影响。 -
如何将 REINFORCE 或
ActorCritic示例转换为使用连续动作空间?尝试为例如LunarLanderContinous-v2的新环境执行此操作。 -
设置示例
Chapter_8_DDPG.py以使用LunarLanderContinuous-v2或其他连续环境。您需要将动作状态从3修改为您选择的环境。 -
调整
Chapter_8_DDPG.py示例的超参数。这需要您学习和理解新的参数tau。 -
调整 TRPO 示例的超参数。这需要您学习如何从命令行设置超参数,然后调整这些参数。您不应该修改任何代码来完成此练习。
-
启用MuJoCo环境,并使用TRPO示例运行这些环境之一。
-
将绘图输出添加到各个示例中。
-
将单个文件示例中的一个转换为使用主方法,该方法接受参数并允许用户动态训练超参数,而不是修改源代码。
在进入下一节和本章结尾之前,请确保完成前面的1-3个练习。
摘要
在本章中,我们介绍了策略梯度方法,我们学习了如何使用随机策略通过REINFORCE算法驱动我们的智能体。之后,我们了解到从随机策略中采样的部分问题是随机采样的随机性。我们发现这可以通过双智能体网络来纠正,其中一个代表动作网络,另一个作为评论家。在这种情况下,动作者是引用评论家网络的策略网络,它使用确定性价值函数。然后,我们看到了如何通过观察DDPG的工作来改进PG。最后,我们探讨了被认为是DRL中更复杂方法之一的TRPO,并了解了它如何试图管理PG方法的几个缺点。
在继续探讨PG方法的基础上,我们将在下一章探索下一代方法,如PPO、AC2、AC2 和 ACER。
第十一章:优化连续控制
到目前为止,我们考虑的大多数训练/挑战环境都被视为是分段的;也就是说,游戏或环境有一个开始和一个结束。这是好的,因为大多数游戏都有开始和结束——毕竟,它是一个游戏。然而,在现实世界或某些游戏中,一个场景可能持续几天、几周、几个月,甚至几年。对于这类环境,我们不再考虑场景;相反,我们与需要连续控制的环境概念一起工作。到目前为止,我们已经查看了一组可以解决这类问题的算法子集,但它们并不做得很好。因此,像大多数RL中的事情一样,我们有一类特殊的算法专门用于这些类型的环境,我们将在本章中探讨它们。
在本章中,我们将探讨改进先前用于执行高级环境连续控制的策略方法。我们将从设置和安装Mujoco环境开始,这是一个我们可以用来测试这些新算法的专用领域,其中第一个将是近端策略优化或PPO方法。之后,我们将查看一种新颖的改进,称为循环网络,用于捕捉上下文,并了解它是如何应用于PPO之上的。然后,我们将回到actor-critic,这次我们将查看几种不同的异步actor-critic配置。最后,我们将查看ACER和具有经验回放的actor-critic。
下面是本章我们将涵盖的主要主题的总结:
-
使用Mujoco理解连续控制
-
介绍近端策略优化
-
使用循环网络进行PPO
-
决定同步和异步actor
-
使用经验回放构建actor-critic
在本章中,我们将探讨一类尝试专门解决机器人或其他控制系统现实世界问题的RL方法。当然,这并不意味着这些相同的算法不能用于游戏——它们确实可以。在下一节中,我们将首先查看专门的Mujoco环境。
使用Mujoco理解连续控制
构建连续控制代理的标准环境是Mujoco环境。Mujoco代表具有约束的多关节动力学,它是一个用于训练机器人或仿真代理的完整物理环境。该环境提供了一系列模拟,挑战某些形式的机器人控制代理执行任务,例如行走、爬行和实施基于物理控制的多个其他任务。这些环境的多样性在以下图像中得到了很好的总结,该图像是从Mujoco主页提取的:

从Mujoco主页提取的示例环境摘录
显然,我们希望使用这个酷炫的环境。然而,这个包不是免费的,需要许可证,但请注意,提供了一个30天的试用期。现在,坏消息来了。这个包的设置、安装和训练都非常困难,尤其是如果你使用Windows的话。事实上,它如此困难,尽管我们强烈建议使用Mujoco作为环境,但我们不会在本章剩余的练习中使用它。为什么?再次强调,它非常困难,我们不希望排除那些无法安装Mujoco环境的人。
有许多博客文章或Stack Overflow文章可供参考,它们介绍了Windows上Mujoco各种版本的安装方法。Mujoco在1.5版本之后停止了对Windows的支持。尽管在Windows上安装Mujoco仍然可能,但这并不简单,并且可能会经常发生变化。因此,如果你倾向于使用Windows与Mujoco,你最好的选择是查找最近的博客或论坛帖子以获取帮助。
在这个练习中,我们将介绍Mujoco的基本安装(不包括Windows):
-
我们首先需要的是一个许可证。打开你的浏览器,访问mujoco.org,并找到页面顶部的许可证按钮。然后,点击它。
-
在页面上,你会看到一个计算机ID的条目。这需要你从右侧显示的蓝色链接中下载一个密钥生成器。点击其中一个链接下载密钥生成器。
-
在你的系统上运行密钥生成器,并在计算机ID字段中输入密钥。
-
使用你的姓名和电子邮件填写其余的许可证信息,然后点击提交,如图所示:

提交Mujoco许可证
-
你应该在几分钟内收到一封电子邮件,其中包含有关将密钥放在何处方向的说明。然后,你需要下载你平台上的二进制文件。点击页面顶部的“产品”链接以转到下载页面。下载你操作系统所需的版本。
-
将文件解压到你的根用户文件夹中,
~/.mujoco/mujoco%version%,其中%version%表示软件的版本。在Windows上,你的用户文件夹是C:\Users\%username%,其中%username%表示登录用户的名字。 -
现在,你需要构建Mujoco包并设置
mujoco-py脚本。这取决于安装方式。使用以下命令构建和安装Mujoco:
pip3 install -U 'mujoco-py<2.1,>=2.0' #use the version appropriate for you
cd path/to/mujoco-py/folder
python -c "import mujoco_py" #force compile mujoco_py
python setup.py install
- 为了测试安装并检查依赖项,运行以下命令重新安装整个Gym:
pip install gym[all]
如果你运行此命令仍然看到错误,你可能需要更多帮助。咨询在线资源,进行关于mujoco install的最新搜索,并尝试那些说明。再次强调,在撰写本文时,Windows不再受支持,你可能更适合使用其他平台。幸运的是,现在为这个设置虚拟机或云服务可以相当容易,并且你可能在那里有更多运气。
- 您可以通过运行
Chapter_9_Mujoco.py来测试Mujoco的安装,并确保许可证已经全部设置好。列表如下所示:
import gym
from gym import envs
env = gym.make('FetchReach-v1')
env.reset()
for _ in range(1000):
env.render()
env.step(env.action_space.sample()) # take a random action
env.close()
如果您已正确安装所有内容,那么您应该会看到以下类似图像,该图像是从Mujoco环境中获取的:

Fetch reach Mujoco环境
如果您能够安装Mujoco环境,那么太好了——尽情探索一个全新的环境世界。对于那些无法安装Mujoco的读者,请不要担心。当我们开始使用Unity时,我们将在第10章利用ML-Agents中学习如何创建自己的基于物理的环境。请放心,虽然Mujoco确实很酷,就像我们之前看到的Atari游戏一样,但它也不是那么容易训练。与Atari类似,Mujoco环境可能需要数百万次的训练迭代。因此,为了保持简单并保持节能,我们将使用常规的Gym环境。现在的额外好处是我们可以在单个环境中更好地比较各种算法。
深度强化学习(DRL)和一般机器学习(ML)由于它们消耗的额外能量而获得了一些坏名声。许多最先进的DRL模型可以从能耗的角度进行衡量,在大多数情况下,能耗相当高。在某个案例中,DeepMind承认,它用于训练单个模型的处理/能耗足以让一台台式电脑运行45年。这在需要谨慎能源消耗的世界中是一个惊人的数字。因此,在适用的情况下,在这本书中,我们将优先考虑成本更低的训练环境。
在下一节中,我们将探讨如何通过梯度优化来提升这些策略方法。
介绍近端策略优化
现在,我们将进入一个领域,我们将开始研究最先进的算法,至少在撰写本文时是这样。当然,这可能会发生变化,事物将会进步。不过,目前,由OpenAI引入的近端策略优化算法(PPO)被认为是最先进的深度强化学习算法。因此,我们可以将各种环境抛给这个问题。然而,为了量化我们的进展以及各种其他原因,我们将继续以Lunar Lander环境为基准。
PPO算法只是对我们在第8章中介绍的信任域策略优化(TRPO)算法的扩展和简化,但有一些关键的不同之处。PPO也更容易理解和遵循。出于这些原因,我们将回顾每个使TRPO和PPO中的信任域剪裁策略优化变得如此强大的特征。
本章的代码最初来源于以下仓库:https://github.com/seungeunrho/minimalRL。对代码进行了一些修改,以便它符合本书中的示例。
既然我们已经了解了这一改进的主要直觉,让我们通过打开Chapter_9_PPO.py来跳入下一个编码练习。执行以下步骤:
- 这个列表的代码与其他我们已审查的列表非常相似。因此,我们将仅限于审查关键部分:
for iteration in range(iterations):
s = env.reset()
done = False
while not done:
for t in range(T_horizon):
prob = model.pi(torch.from_numpy(s).float())
m = Categorical(prob)
a = m.sample().item()
s_prime, r, done, info = env.step(a)
model.put_data((s, a, r/100.0, s_prime, prob[a].item(),done))
s = s_prime
score += r
if done:
if score/print_interval > min_play_reward:
play_game()
break
model.train_net()
if iteration%print_interval==0 and iteration!=0:
print("# of episode :{}, avg score : {:.1f}".format(iteration,
score/print_interval))
score = 0.0
env.close()
- 滚动到最底部,我们可以看到训练代码几乎与前面章节中我们最近的一些示例相同。一个需要注意的关键点是引入了一个新的超参数
T_horizon,我们将在稍后定义它:
learning_rate = 0.0005
gamma = 0.98
lmbda = 0.95
eps_clip = 0.1
K_epoch = 3
T_horizon = 20
-
如果我们滚动回顶部,你会看到为
T_horizon、K_epoch、eps_clip和lambda定义的新超参数。现在只需记住这些新变量——我们很快就会了解它们的目的。 -
让我们跳到一些其他的重要差异,例如网络定义,这可以在
PPO类的init方法中看到如下:
def __init__(self, input_shape, num_actions):
super(PPO, self).__init__()
self.data = []
self.fc1 = nn.Linear(input_shape,256)
self.fc_pi = nn.Linear(256,num_actions)
self.fc_v = nn.Linear(256,1)
self.optimizer = optim.Adam(self.parameters(), lr=learning_rate)
-
我们可以看到,网络由一个名为
fc1的第一输入状态Linear层组成,该层包含256个神经元。然后,我们可以看到fc_pi或策略网络被定义为Linear,包含256个神经元,并输出num_actions或动作的数量。接下来是fc_v的定义,这是值层。同样,它也有256个神经元和一个输出,即期望值。 -
PPO类的其余代码几乎与前面的示例相同,我们在这里不需要详细说明。
-
按正常方式运行代码。这个示例将需要一段时间才能运行,但不会像之前的版本那样长。我们将由你来决定是否在示例完成之前继续。
你应该很快就能注意到算法训练的速度有多快。确实,智能体很快就变得很好,实际上可以在少于10,000次迭代中解决环境,这相当令人印象深刻。既然我们已经看到了策略优化的潜力,我们将在下一节中探讨这是如何实现的。
策略优化的方法
在第8章“策略梯度方法”中,我们介绍了策略梯度方法可能失败的情况,然后介绍了TRPO方法。在这里,我们讨论了TRPO用来解决PG方法中失败的一般策略。然而,正如我们所看到的,TRPO相当复杂,看到它在代码中的工作也没有太大帮助。这就是我们在介绍TRPO时尽量减少对细节讨论的主要原因,而是等到我们到达这一节才来讲述。
以简洁的方式讲述完整的故事。
话虽如此,让我们回顾一下使用TRPO或PPO进行策略优化的方法是如何做到这一点的:
-
最小化-最大化MM算法:回想一下,这是我们在找到上界函数的最小值时,通过找到受限于在上界函数内的下界函数的最大值来实现的。
-
线搜索:我们已经看到这是用来定义我们如何以及多少可以优化我们的函数(深度学习网络)的方向和量的。这允许我们的算法避免超过优化的目标。
-
信任区域:除了MM和线搜索之外,我们还想让策略函数有一个稳定的基或平台来移动。你可以把这个稳定的基想象成一个信任或安全区域。在PPO中,这被定义得不同,正如我们将看到的。
PPO和TRPO都以寻找更好的策略作为共同改进的方式。PPO通过理解我们每轮迭代想要改变策略分布的程度来改进这一点。这种理解也使我们能够限制每轮迭代中的变化量。我们已经看到TRPO如何通过KL散度在一定程度上做到这一点,但PPO通过调整或适应变化量更进一步。在下一节中,我们将探讨这种适应是如何工作的。
PPO和剪裁目标
在我们深入探讨PPO的工作细节之前,我们需要退一步理解我们如何将分布式数据分布或分布之间的差异等同起来。记住,PG方法试图理解基于回报的采样分布,然后使用它来找到最佳动作或最佳动作的概率。因此,我们可以使用一种称为KL散度的方法来确定两个分布之间的差异。通过理解这一点,我们可以确定我们可以允许我们的优化算法探索多少空间或信任区域。PPO通过使用两个策略网络来剪裁目标函数来改进这一点。
乔纳森·惠(Jonathan Hui)在关于各种强化学习(RL)和策略梯度(PG)方法的数学方面有许多有见地的博客文章。特别是他关于PPO的文章(https://medium.com/@jonathan_hui/rl-proximal-policy-optimization-ppo-explained-77f014ec3f12)相当不错。请注意,它们确实假设了一个非常复杂的数学知识水平。如果你对RL认真,你将需要在某个时候能够阅读和理解这些内容。然而,通过直观地理解大多数算法,就像我们在这里做的那样,你可以通过深度强化学习(DRL)走得很远。
让我们通过打开Chapter_9_PPO.py并执行以下步骤来学习如何在代码中实现这一点:
- 在查看主要代码的大部分内容后,我们只想关注这里的训练代码,特别是
PPO类中的train_net函数,如下所示:
def train_net(self):
s, a, r, s_prime, done_mask, prob_a = self.make_batch()
for i in range(K_epoch):
td_target = r + gamma * self.v(s_prime) * done_mask
delta = td_target - self.v(s)
delta = delta.detach().numpy()
advantage_lst = []
advantage = 0.0
for delta_t in delta[::-1]:
advantage = gamma * lmbda * advantage + delta_t[0]
advantage_lst.append([advantage])
advantage_lst.reverse()
advantage = torch.tensor(advantage_lst, dtype=torch.float)
pi = self.pi(s, softmax_dim=1)
pi_a = pi.gather(1,a)
ratio = torch.exp(torch.log(pi_a) - torch.log(prob_a))
surr1 = ratio * advantage
surr2 = torch.clamp(ratio, 1-eps_clip, 1+eps_clip) * advantage
loss = -torch.min(surr1, surr2) + F.smooth_l1_loss(self.v(s) ,
td_target.detach())
self.optimizer.zero_grad()
loss.mean().backward()
self.optimizer.step()
- 在初始的
make_batch函数调用之后,为了构建列表,我们进入由K_epoch控制的迭代循环。K_epoch是一个新的超参数,它控制我们用于优化优势收敛的迭代次数:
td_target = r + gamma * self.v(s_prime) * done_mask
delta = td_target - self.v(s)
delta = delta.detach().numpy()
K_epoch迭代内部的第一个代码块是使用奖励r计算td_target,加上折扣因子gamma乘以v或价值网络的输出和done_mask。然后,我们取delta或TD变化并将其转换为numpy张量:
for delta_t in delta[::-1]:
advantage = gamma * lmbda * advantage + delta_t[0]
advantage_lst.append([advantage])
- 接下来,使用delta,我们通过
advantage函数构建一个优势列表,如下所示:
pi = self.pi(s, softmax_dim=1)
pi_a = pi.gather(1,a)
ratio = torch.exp(torch.log(pi_a) - torch.log(prob_a))
- 然后,我们将状态
s推入策略网络pi。接下来,我们沿着第一个维度收集轴,然后使用方程
计算比率,该方程用于计算我们想要用于信任的剪切区域或区域的可能比率:
surr1 = ratio * advantage
surr2 = torch.clamp(ratio, 1-eps_clip, 1+eps_clip) * advantage
loss = -torch.min(surr1, surr2) + F.smooth_l1_loss(self.v(s) ,
td_target.detach())
- 我们使用
ratio值来计算surr1值,它定义了表面或剪切区域。下一行通过夹紧这个比率并使用由eps_clip设置的剪切区域边界来定义区域,计算第二个版本的表面surr2。然后,它取两个表面中的最小面积,并使用该面积来计算损失。
我们在这里使用术语“表面”来理解损失计算是在一个多维数组值上进行的。我们的优化器在这个表面上工作,以找到最佳的全局最小值或表面的最低区域。
- 代码的最后部分是我们的典型梯度下降优化,下面为了完整性而展示:
self.optimizer.zero_grad()
loss.mean().backward()
self.optimizer.step()
- 这里没有什么新的内容。继续运行样本或回顾之前练习的输出。以下是一个训练输出的示例:

来自Chapter_9_PPO.py的示例输出
需要注意的是,我们仍在使用离散动作空间,而不是连续动作空间。再次强调,我们这样做的主要原因是继续使用基线一致的环境,即Lunar Lander v2。Lunar Lander确实有一个连续动作环境,你可以尝试,但你需要转换样本,以便使用连续动作。第二个需要注意的事项是,PPO和其他PG方法在连续动作环境中表现实际上更好,这意味着你并没有真正看到它们的全部潜力。那么,我们为什么还在继续使用离散动作空间呢?嗯,在几乎所有情况下,游戏和交互式环境都会使用离散空间。由于这本书是关于游戏和AI的,而不是关于机器人和AI的,我们将坚持使用离散空间。
关于其他PG方法的研究项目有很多,但你应该将PPO视为DRL的一个里程碑,就像DQN一样。对于那些好奇的人来说,PPO通过在DOTA2策略游戏中击败人类玩家而闻名。在下一节中,我们将探讨其他叠加在PG和其他方法之上的方法,以改进DRL。
使用PPO与循环网络结合
在第7章《使用DDQN深入探索》中,我们看到了如何使用称为卷积神经网络(CNNs)的概念来解释视觉状态。CNN网络用于检测视觉环境中的特征,例如Atari游戏。虽然这项技术允许我们使用同一个智能体玩许多游戏,但增加的CNN层需要更多的时间来训练。最终,额外的训练时间并不值得玩Atari游戏的酷炫效果。然而,我们可以在网络之上添加其他网络结构,以更好地解释状态。其中一种网络结构称为循环网络。循环网络层允许我们在模型对状态的解释中添加上下文或时间概念。这在任何上下文或记忆重要的问题上都可以非常有效。
循环网络层是一种深度学习感知器,它本质上将状态反馈给前面的神经元。这实际上赋予了网络理解时间或上下文的能力。它做得如此之好,以至于循环网络现在是所有文本和语言处理网络的核心。语言尤其具有上下文性,循环层,以各种配置,可以轻松理解上下文。循环网络层有多种配置,但在这里我们将关注的一种称为长短期记忆(LSTM)。
循环网络和 LSTM 层值得深入研究。这些强大的网络层在过去几年中负责了一些非常有趣的发现。虽然循环层在 DRL 中已被证明用途有限,但人们认为它们应该有更多用途。毕竟,在轨迹中理解上下文肯定很重要。
对于 DRL 的 LSTM 层来说,放置起来相当简单。打开 Chapter_9_PPO_LSTM.py 并按照以下步骤操作,看看这是如何工作的:
-
这个示例几乎与
Chapter_9_PPO.py完全相同,但有一些关键的不同之处,所有这些我们都会在这里查看。 -
跳转到
PPO类定义,如下所示:
class PPO(nn.Module):
def __init__(self, input_shape, num_actions):
super(PPO, self).__init__()
self.data = []
self.fc1 = nn.Linear(input_shape,64)
self.lstm = nn.LSTM(64,32)
self.fc_pi = nn.Linear(32,num_actions)
self.fc_v = nn.Linear(32,1)
self.optimizer = optim.Adam(self.parameters(), lr=learning_rate)
-
这里唯一的新部分是定义了一个新的层,
lstm,其类型为LSTM(64,32)。注入到状态编码顶部的 LSTM 层允许网络学习动作或记忆中的上下文。现在,我们的智能体不是学习哪些状态-动作提供最佳轨迹,而是在学习哪些状态-动作集提供最佳结果。在游戏中,这可能类似于学习一个特殊动作可以解锁一系列动作以获得一些特殊奖励。 -
接下来,我们将向下移动到策略 pi 函数和值 v 函数的网络定义,并查看它们是如何被修改的:
def pi(self, x, hidden):
x = F.relu(self.fc1(x))
x = x.view(-1, 1, 64)
x, lstm_hidden = self.lstm(x, hidden)
x = self.fc_pi(x)
prob = F.softmax(x, dim=2)
return prob, lstm_hidden
def v(self, x, hidden):
x = F.relu(self.fc1(x))
x = x.view(-1, 1, 64)
x, lstm_hidden = self.lstm(x, hidden)
v = self.fc_v(x)
return v
-
pi和v函数接受一个隐藏层,但只有pi,即策略函数,被用作隐藏层的输出。我们很快就会看到这些隐藏 LSTM 层是如何工作的。 -
然后,在
train_net函数的顶部,我们可以看到层是从批处理过程中提取出来的,make_batch:
def train_net(self):
s,a,r,s_prime,done_mask, prob_a, (h1_in, h2_in), (h1_out, h2_out) = self.make_batch()
first_hidden = (h1_in.detach(), h2_in.detach())
second_hidden = (h1_out.detach(), h2_out.detach())
- 我们在 actor-critics 之间使用两个隐藏或中间 LSTM 层,其中
second_hidden表示输出,first_hidden表示输入。在下面的for循环中,我们可以看到使用 LSTM 输入和输出的 delta 计算过程:
v_prime = self.v(s_prime, second_hidden).squeeze(1)
td_target = r + gamma * v_prime * done_mask
v_s = self.v(s, first_hidden).squeeze(1)
delta = td_target - v_s
delta = delta.detach().numpy()
-
这里 delta 的计算是通过应用 LSTM 层应用前后的差异来完成的,这使得 delta 能够封装 LSTM 对值
v计算的影响。 -
按照正常方式运行示例,并观察输出,如下所示:

Chapter_9_PPO_LSTM.py 的示例输出
注意这个轻微的改进是如何显著提高了我们在上一练习中查看的 vanilla PPO 示例的训练性能。在下一节中,我们将通过应用并行环境进一步改进 PPO。
决定同步和异步的 actors
我们这本书的开篇以对人工通用智能(AGI)的简单讨论开始。简而言之,AGI是我们尝试将智能系统泛化以解决多个任务的尝试。强化学习(RL)通常被视为通往AGI的阶梯,主要是因为它试图泛化基于状态的学习。虽然RL和AGI都从我们的思考方式中汲取了深刻的灵感,无论是奖励还是可能的意识本身,但前者倾向于包含直接的类比。RL中的演员-评论家(actor-critic)概念是我们在创建一种学习形式时如何使用对人类心理学的解释的一个极好例子。例如,我们人类经常考虑我们行为的后果,并确定它们可能或可能不会给我们带来的优势。这个例子与我们在RL中使用的演员-评论家和优势方法完美类比。更进一步,我们可以考虑另一种人类思维过程:异步和同步思维。
异步/同步思维的一个直接例子是在被问了一个问题几小时后,答案突然出现在你的脑海中。也许你当时没有答案,但几个小时后它就出现了。你一直在想这个问题吗?不太可能,而且更有可能的是答案突然出现在你的脑海中。但是,你是在某个后台过程中一直在想它,还是某个过程突然启动并提供了答案?动物大脑总是这样思考,我们称之为计算机术语中的并行处理。那么,我们的代理不能也从这种思维过程中受益吗?结果证明,是的。
这种灵感可能部分来自前面的类比,但也具有如何评估优势的数学背景。直接评估我们大脑的思考方式仍然是一个大问题,但我们可以假设我们的思维是同步的或异步的。因此,除了考虑一个思维过程之外,如果我们考虑几个会怎样?我们可以将这个类比更进一步,并将其应用于深度强化学习(DRL)——特别是演员-评论家。在这里,我们有一个单一的思维过程或全局网络,它被几个工人思维过程的输出所喂养。这里展示了这样一个例子:

亚瑟·朱利安尼博士异步AC的示例
我们在这里看到的是演员-评论家架构的优势,A2C,以及异步演员-评论家架构,A3C的基本直觉。注意每个工人大脑/代理都有自己的独立环境副本。所有这些工人代理将他们的学习输入到一个主大脑中。然后,每个工人大脑通过迭代更新以与主大脑同步,这与我们之前的优势计算非常相似。在下一节中,我们将看到如何通过实现A2C来将此付诸实践。
使用A2C
为了避免任何混淆,理解A2C和A3C都使用AC,但它们更新模型的方式不同是很重要的。在A2C中,方法是同步的,所以每个大脑都将思想输入到主大脑中。
让我们通过打开Chapter_9_A2C.py文件并查看其中的超参数来查看代码中的样子:
n_train_processes = 3
learning_rate = 0.0002
update_interval = 5
gamma = 0.98
max_train_steps = 60000
PRINT_INTERVAL = update_interval * 100
environment = "LunarLander-v2"
保持样本打开并按照以下步骤继续这个练习:
- 这是一个大的代码示例,所以我们将限制我们在这里展示的部分。这里需要注意的是文件顶部的超参数列表。唯一需要注意的是
n_train_processes,它设置了工作进程的数量:
class ActorCritic(nn.Module):
def __init__(self, input_shape, num_actions):
super(ActorCritic, self).__init__()
self.fc1 = nn.Linear(input_shape, 256)
self.fc_pi = nn.Linear(256, num_actions)
self.fc_v = nn.Linear(256, 1)
def pi(self, x, softmax_dim=1):
x = F.relu(self.fc1(x))
x = self.fc_pi(x)
prob = F.softmax(x, dim=softmax_dim)
return prob
def v(self, x):
x = F.relu(self.fc1(x))
v = self.fc_v(x)
return v
- 接下来是
ActorCritic类,这是我们之前使用的同一个类:
def worker(worker_id, master_end, worker_end):
master_end.close()
env = gym.make(environment)
env.seed(worker_id)
while True:
cmd, data = worker_end.recv()
if cmd == 'step':
ob, reward, done, info = env.step(data)
if done:
ob = env.reset()
worker_end.send((ob, reward, done, info))
elif cmd == 'reset':
ob = env.reset()
worker_end.send(ob)
elif cmd == 'reset_task':
ob = env.reset_task()
worker_end.send(ob)
elif cmd == 'close':
worker_end.close()
break
elif cmd == 'get_spaces':
worker_end.send((env.observation_space, env.action_space))
else:
raise NotImplementedError
- 然后是
worker函数的定义。这个函数是工作节点的大脑在工作和主大脑之间发送消息的地方:
class ParallelEnv:
def __init__(self, n_train_processes):
self.nenvs = n_train_processes
self.waiting = False
self.closed = False
self.workers = list()
master_ends, worker_ends = zip(*[mp.Pipe() for _ in range(self.nenvs)])
self.master_ends, self.worker_ends = master_ends, worker_ends
for worker_id, (master_end, worker_end) in enumerate(zip(master_ends, worker_ends)):
p = mp.Process(target=worker,
args=(worker_id, master_end, worker_end))
p.daemon = True
p.start()
self.workers.append(p)
# Forbid master to use the worker end for messaging
for worker_end in worker_ends:
worker_end.close()
- 在这些函数之后是大的
ParallelEnv类。前面的代码只是展示了该类的init函数,因为它相当大。这个类仅仅协调主节点和工作节点之间的活动:
def compute_target(v_final, r_lst, mask_lst):
G = v_final.reshape(-1)
td_target = list()
for r, mask in zip(r_lst[::-1], mask_lst[::-1]):
G = r + gamma * G * mask
td_target.append(G)
return torch.tensor(td_target[::-1]).float()
- 滚动到
test函数之后,或者在我们其他示例中的play_game函数之后,我们可以看到compute_target函数。这是TD损失的计算,这里的区别在于使用了mask变量。mask只是一个标志或过滤器,它会移除任何关于0回报的折现G的计算:
if __name__ == '__main__':
envs = ParallelEnv(n_train_processes)
env = gym.make(environment)
state_size = env.observation_space.shape[0]
action_size = env.action_space.n
model = ActorCritic(state_size, action_size)
optimizer = optim.Adam(model.parameters(), lr=learning_rate)
- 之后,我们进入一个
if函数,它确定当前过程是否为'__main__'。我们这样做是为了避免额外的工作进程尝试运行相同的代码块。之后,我们可以看到典型的环境和模型设置完成:
for _ in range(update_interval):
prob = model.pi(torch.from_numpy(s).float())
a = Categorical(prob).sample().numpy()
s_prime, r, done, info = envs.step(a)
s_lst.append(s)
a_lst.append(a)
r_lst.append(r/100.0)
mask_lst.append(1 - done)
s = s_prime
step_idx += 1
- 间隔训练循环的代码几乎与之前的示例相同,大部分应该已经很直观。需要注意的是
env.steps函数调用。这代表所有工作环境中的同步步骤。记住,在A2C中工作代理是同步运行的:
s_final = torch.from_numpy(s_prime).float()
v_final = model.v(s_final).detach().clone().numpy()
td_target = compute_target(v_final, r_lst, mask_lst)
td_target_vec = td_target.reshape(-1)
s_vec = torch.tensor(s_lst).float().reshape(-1, state_size)
a_vec = torch.tensor(a_lst).reshape(-1).unsqueeze(1)
mod = model.v(s_vec)
advantage = td_target_vec - mod.reshape(-1)
pi = model.pi(s_vec, softmax_dim=1)
pi_a = pi.gather(1, a_vec).reshape(-1)
loss = -(torch.log(pi_a) * advantage.detach()).mean() +\
F.smooth_l1_loss(model.v(s_vec).reshape(-1), td_target_vec)
optimizer.zero_grad()
loss.backward()
optimizer.step()
-
然后,我们来到外部的训练循环。在这个示例中,我们可以看到训练目标是如何从工人构建的列表中提取的,其中
s_lst是状态,a_lst是动作,r_lst是奖励,mask_lst是完成。除了torch张量操作外,计算与PPO相同。 -
按照你通常的方式运行代码并可视化输出,以下是一个示例:

来自Chapter_9_A2C.py的示例输出
你需要调整超参数才能使这个示例完美运行。现在,我们将继续并查看A2C的异步版本——A3C。
使用A3C
同步actor-critic工人通过基本上提供更多的采样变体来提供训练优势,这应该反过来减少预期错误的数量,从而提高训练性能。从数学上讲,我们只是在提供更大的采样空间,正如任何统计学家都会告诉你的,这只会减少采样误差。然而,如果我们假设每个工人都是异步的,这意味着它在自己的时间更新全局模型,这也为我们提供了在整个轨迹空间采样中的更多统计变异性。这也可以在采样空间的同时发生。本质上,我们可以在许多不同的点上让工人采样轨迹,如下面的图所示:

在轨迹空间中进行多次工人采样
使用A3C和异步actor-critic工人,我们可以更快地获得整个轨迹空间的更清晰图景,这允许我们的代理做出更清晰、更好的决策。它是通过使用多个工人在轨迹空间中进行异步采样来做到这一点的。让我们通过打开Chapter_9_A3C.py并执行以下步骤来了解这是如何工作的:
- 我们将从查看典型的超参数和设置代码开始,如下所示:
n_train_processes = 6
learning_rate = 0.0002
update_interval = 6
gamma = 0.98
max_train_ep = 3000
max_test_ep = 400
environment = "LunarLander-v2"
env = gym.make(environment)
state_size = env.observation_space.shape[0]
action_size = env.action_space.n
-
在这里,我们可以看到包含两个新的超参数,
max_train_ep和max_test_ep。第一个变量max_train_ep设置了最大训练剧集数,而第二个变量max_test_ep用于评估性能。 -
下一个部分是
ActorCritic类,与我们之前的几个例子完全相同,所以在这里我们不需要回顾它。之后是train函数,如下所示:
def train(global_model, rank):
local_model = ActorCritic(state_size, action_size)
local_model.load_state_dict(global_model.state_dict())
optimizer = optim.Adam(global_model.parameters(), lr=learning_rate)
env = gym.make(environment)
for n_epi in range(max_train_ep):
done = False
s = env.reset()
while not done:
s_lst, a_lst, r_lst = [], [], []
for t in range(update_interval):
prob = local_model.pi(torch.from_numpy(s).float())
m = Categorical(prob)
a = m.sample().item()
s_prime, r, done, info = env.step(a)
s_lst.append(s)
a_lst.append([a])
r_lst.append(r/100.0)
s = s_prime
if done:
break
s_final = torch.tensor(s_prime, dtype=torch.float)
R = 0.0 if done else local_model.v(s_final).item()
td_target_lst = []
for reward in r_lst[::-1]:
R = gamma * R + reward
td_target_lst.append([R])
td_target_lst.reverse()
s_batch, a_batch, td_target = torch.tensor(s_lst, dtype=torch.float), torch.tensor(a_lst), \
torch.tensor(td_target_lst)
advantage = td_target - local_model.v(s_batch)
pi = local_model.pi(s_batch, softmax_dim=1)
pi_a = pi.gather(1, a_batch)
loss = -torch.log(pi_a) * advantage.detach() + \
F.smooth_l1_loss(local_model.v(s_batch), td_target.detach())
optimizer.zero_grad()
loss.mean().backward()
for global_param, local_param in zip(global_model.parameters(), local_model.parameters()):
global_param._grad = local_param.grad
optimizer.step()
local_model.load_state_dict(global_model.state_dict())
env.close()
print("Training process {} reached maximum episode.".format(rank))
-
train函数与我们之前的训练代码非常相似。然而,请注意我们传递了一个global_model输入。这个全局模型被用作本地模型的克隆,然后我们在工人代理学习到的经验上进行训练。关于这段代码的一个关键观察点是最后部分,这是我们使用独立训练的本地模型更新全局模型的地方。 -
接下来是测试函数。这是使用以下代码评估
global_model的地方:
def test(global_model):
env = gym.make(environment)
score = 0.0
print_interval = 20
for n_epi in range(max_test_ep):
done = False
s = env.reset()
while not done:
prob = global_model.pi(torch.from_numpy(s).float())
a = Categorical(prob).sample().item()
s_prime, r, done, info = env.step(a)
s = s_prime
score += r
if n_epi % print_interval == 0 and n_epi != 0:
print("# of episode :{}, avg score : {:.1f}".format(
n_epi, score/print_interval))
score = 0.0
time.sleep(1)
env.close()
-
所有这些代码所做的只是通过使用它来玩游戏和评估分数来评估模型。这当然是在训练时渲染环境的绝佳位置。
-
最后,我们有主要的处理代码块。这个代码块通过以下
nameif语句被标识:
if __name__ == '__main__':
global_model = ActorCritic(state_size, action_size)
global_model.share_memory()
processes = []
for rank in range(n_train_processes + 1): # + 1 for test process
if rank == 0:
p = mp.Process(target=test, args=(global_model,))
else:
p = mp.Process(target=train, args=(global_model, rank,))
p.start()
processes.append(p)
for p in processes:
p.join()
-
如我们所见,这是使用共享内存构建
global_model模型的地方。然后,我们使用第一个或排名0的进程作为测试或评估进程来启动子进程。最后,我们可以看到代码在所有进程通过p.join重新连接时结束。 -
按照常规运行代码并查看结果,以下是一个示例:

来自Chapter_9_A3C.py的示例输出
基于经验回放构建actor-critic
在这本书中,我们已经学到了DRL的所有主要概念。在后面的章节中,我们还将向您展示更多工具,就像我们在这个部分展示的那样,但如果您已经走到这一步,您应该认为自己对DRL有了一定的了解。因此,考虑构建自己的工具或对DRL进行增强,就像我们在这个部分展示的那样。如果您想知道是否需要先解决数学问题,那么答案是无需。通常,首先在代码中构建这些模型,然后理解数学会更直观。
基于经验回放的actor-critic(ACER)通过根据以往经验调整采样提供了另一个优势。这个概念最初由DeepMind在一篇题为《Sample Efficient Actor-Critic with Experience Replay》的论文中提出,并开发了ACER的概念。ACER背后的直觉是我们开发双打随机网络来减少偏差和方差,并更新我们在PPO中选择的信任区域。在下一个练习中,我们将探索结合经验回放的actor-critic。
通过回顾上述论文或搜索博客文章,可以最好地理解ACER背后的数学。强烈建议您在处理论文之前,首先理解TRPO和PPO背后的数学。
打开Chapter_9_ACER.py并按照以下步骤完成这个练习:
- 在这个示例中,你首先会注意到的是
ReplayBuffer类,如下所示:
class ReplayBuffer():
def __init__(self):
self.buffer = collections.deque(maxlen=buffer_limit)
def put(self, seq_data):
self.buffer.append(seq_data)
def sample(self, on_policy=False):
if on_policy:
mini_batch = [self.buffer[-1]]
else:
mini_batch = random.sample(self.buffer, batch_size)
s_lst, a_lst, r_lst, prob_lst, done_lst, is_first_lst = [], [], [], [], [], []
for seq in mini_batch:
is_first = True
for transition in seq:
s, a, r, prob, done = transition
s_lst.append(s)
a_lst.append([a])
r_lst.append(r)
prob_lst.append(prob)
done_mask = 0.0 if done else 1.0
done_lst.append(done_mask)
is_first_lst.append(is_first)
is_first = False
s,a,r,prob,done_mask,is_first = torch.tensor(s_lst, dtype=torch.float), torch.tensor(a_lst), \
r_lst, torch.tensor(prob_lst, dtype=torch.float), done_lst, \
is_first_lst
return s,a,r,prob,done_mask,is_first
def size(self):
return len(self.buffer)
-
这是我们在前几章中看到的
ReplayBuffer类的更新版本。 -
除了
train函数中的新部分之外,大部分代码应该是自解释的,首先是代码的前几个块:
q = model.q(s)
q_a = q.gather(1,a)
pi = model.pi(s, softmax_dim = 1)
pi_a = pi.gather(1,a)
v = (q * pi).sum(1).unsqueeze(1).detach()
rho = pi.detach()/prob
rho_a = rho.gather(1,a)
rho_bar = rho_a.clamp(max=c)
correction_coeff = (1-c/rho).clamp(min=0)
-
新代码是从动作概率
prob除以pi的比率来计算rho,然后代码将张量聚合成1,将其夹具,并计算一个称为correction_coeff的校正系数。 -
滚过一些其他熟悉的代码,我们来到了一个新部分,其中损失的计算已经更新为使用
rho_bar和correction_coeff的值,如下所示:
loss1 = -rho_bar * torch.log(pi_a) * (q_ret - v)
loss2 = -correction_coeff * pi.detach() * torch.log(pi) * (q.detach()-v) loss = loss1 + loss2.sum(1) + F.smooth_l1_loss(q_a, q_ret)
-
在这里,我们可以看到
rho_bar和correction_coeff的倒数都被用来偏斜损失的计算。rho是我们用来计算这些系数的原始值,它基于先前动作和预测动作之间的比率。应用这种偏差产生的效果是缩小沿着轨迹路径的搜索范围。当应用于连续控制任务时,这是一个非常好的效果。 -
最后,让我们跳到训练循环代码,看看数据是如何附加到
ReplayBuffer的:
seq_data.append((s, a, r/100.0, prob.detach().numpy(), done))
-
在这里我们可以看到,动作概率
prob是通过使用detach()从PyTorch张量中分离出来,然后将其转换为numpy张量来输入的。这个值是我们后来在train_net函数中用来计算rho的。 -
按照正常方式运行代码并观察输出,以下是一个示例:

来自Chapter_9_ACER.py的示例输出
在这里,我们可以看到缓冲区大小如何增加,以及智能体如何变得更聪明。这是因为我们正在使用这些经验在重放缓冲区中调整从策略分布中理解偏差和方差的理解,这反过来又减少了我们使用的信任区域的大小或裁剪区域。正如我们从这个练习中可以看到的,这是本章中最令人印象深刻的练习之一,它确实以比我们本章之前的尝试更收敛的方式学习了环境。
关于优化PG方法的内容就到这里。在下一节中,我们将探讨一些你可以自己进行的练习。
练习
本节中的练习是为了让你自己探索。从现在开始,显著提高我们涵盖的任何技术都是一个成就,所以你在这里所做的可能不仅仅是学习。确实,你现在工作的环境和示例可能会表明你未来的工作偏好。一如既往,尝试完成以下练习中的两到三个:
-
调整
Chapter_9_PPO.py和/或Chapter_9_PPO_LSTM.py的超参数。 -
调整
Chapter_9_A2C.py和/或Chapter_9_A3C.py的超参数。 -
调整
Chapter_9_ACER.py的超参数。 -
将LSTM层应用于A2C和/或A3C示例。
-
将LSTM层应用于ACER示例。
-
将
play_game函数添加到A2C和/或A3C示例中。 -
将
play_game函数添加到ACER示例中。 -
在ACER示例中调整缓冲区大小,看看这如何改善训练。
-
将同步和/或异步工作者添加到ACER示例中。
-
添加将结果输出到matplot或TensorBoard的功能。这相当高级,但这是我们将在后面的章节中介绍的内容。
这些练习旨在巩固我们在本章中学到的内容。在下一节中,我们将总结本章内容。
摘要
在本章中,我们学习了PG方法并非没有自己的缺陷,并探讨了修复或纠正它们的方法。这使我们探索了更多提高采样效率和优化目标或裁剪梯度函数的实现方法。我们通过查看使用裁剪目标函数来优化我们用来计算梯度的信任区域的PPO方法来实现这一点。在那之后,我们查看添加新的网络层配置来理解状态中的上下文。
然后,我们在PPO之上使用了新的层类型,即LSTM层,以观察它带来的改进。接着,我们研究了通过并行环境和同步或异步工作者来提高采样效率的方法。我们通过构建一个A2C示例来实现同步工作者,然后查看在A3C上使用异步工作者的示例。我们通过探讨使用ACER(经验回放的动作-评论家)来提高采样效率的另一种改进方法来结束这一章。
在下一章中,我们将通过探讨更多适用于游戏的不同方法来提升我们的知识。PG方法在游戏任务中已被证明非常成功,但在下一章中,我们将回到DQN,看看它如何通过不断改进达到最先进水平。
第十二章:全部关于 Rainbow DQN
在本书中,我们已经学习了各种 强化学习(RL)的线索是如何结合形成现代强化学习,然后通过加入 深度学习(DL)而发展到 深度强化学习(DRL)。像大多数其他从这种融合中产生的专业领域一样,我们现在看到的是回到针对特定环境类别的专门方法。我们在涵盖 策略梯度(PG)方法和它专门化的连续控制环境章节中开始看到这一点。这一面的反面是更典型的周期性游戏环境,它具有某种形式的离散控制机制。这些环境通常使用 DQN 表现更好,但问题变成了关于 DQN 的。好吧,在这一章中,我们将探讨聪明人如何通过引入 Rainbow DQN 来解决这个问题。
在这一章中,我们将介绍 Rainbow DQN,并理解它试图解决的问题以及它提供的解决方案。由于我们已经在其他章节中涵盖了大多数这些解决方案,我们将通过首先查看用于更好地理解采样和探索的噪声或模糊网络来介绍使 Rainbow 不同的少数几个解决方案。然后,我们将探讨分布式强化学习以及它如何通过预测分布来提高价值估计,这与我们的策略网络从 PG 类似,将这些改进和其他改进结合到 Rainbow 中,并观察其表现如何。最后,我们将探讨分层 DQN 以理解任务和子任务,为可能的训练环境提供计划,并计划在以后使用这种高级 DQN 在更复杂的环境中。
本章我们将涵盖的主要主题包括:
-
Rainbow – 结合深度强化学习中的改进
-
使用 TensorBoard
-
介绍分布式强化学习
-
理解噪声网络
-
揭示 Rainbow DQN
一定要复习你对概率、统计学、随机过程以及/或贝叶斯推理和变分推理方法的理解。这应该是你在前面的章节中就已经在做的事情,但随着我们转向 DRL 以及 DL 的更高级内容,这些知识现在将变得至关重要。
在下一节中,我们将探讨理解 Rainbow DQN 是什么以及为什么需要它。
Rainbow – 结合深度强化学习中的改进
2017年10月,DeepMind发表的介绍Rainbow DQN的论文,Rainbow:结合深度强化学习的改进,旨在解决DQN的几个不足。DQN是由DeepMind的同一组人引入的,由David Silver领导,旨在在Atari游戏中打败人类。然而,正如我们在几个章节中学到的,虽然该算法具有开创性,但它确实存在一些不足。其中一些我们已经通过DDQN和经验回放等进步解决了。为了理解Rainbow所包含的所有内容,让我们看看它对RL/DRL的主要贡献:
-
DQN:当然,这是核心算法,我们到现在应该已经很好地理解了。我们已经在第6章,“深入探索DQN”中介绍了DQN。
-
双DQN:这不要与DDQN或对抗DQN混淆。再次强调,我们已经在第7章,“深入探索DDQN”中介绍过这一点。
-
优先经验回放:这是我们已经在第6章,“深入探索DQN”中介绍过的另一个改进。
-
对抗网络架构(DDQN):这是我们之前已经介绍过的元素,并在之前提到过。
-
多步回报:这是我们计算TD lambda和期望或优势估计的方法。
-
分布式强化学习:这试图理解价值分布,与我们的策略模型在演员-评论家中的作用类似,但在这个情况下,我们使用的是价值。这种增强将在本章的单独部分进行介绍。
-
噪声网络:噪声或模糊网络是深度学习网络,它们学习平衡权重参数的分布,而不是网络权重的实际判别值。这些高级深度学习网络已被用于更好地理解数据分布及其数据,通过将使用的权重建模为分布来实现。我们将在本章的后续部分介绍这些高级深度学习网络。
我们在之前的章节中已经介绍了许多这些改进,除了分布式强化学习和噪声网络。我们将在本章中介绍这两个改进,从未来的部分开始介绍分布式强化学习。但在我们这样做之前,让我们退一步,在下一节中通过TensorBoard提高我们的日志输出能力。
使用TensorBoard
在本书的这个阶段,我们需要超越构建玩具示例,转向构建你可以用来在未来训练自己的代理的模块或框架。实际上,我们将使用本章中的代码来训练代理解决我们在后续章节中提出的其他挑战环境。这意味着我们需要一种更通用的方式来记录我们的进度,最好是记录到我们以后可以查看的日志文件中。由于构建这样的框架对于整个机器学习来说是一个如此常见的任务,因此Google开发了一个非常有用的日志框架,称为TensorBoard。TensorBoard最初是作为我们之前提到的其他深度学习框架的一个子集开发的。幸运的是,对于我们来说,PyTorch包含了一个支持将日志记录到TensorBoard的扩展。因此,在本节中,我们将设置和安装TensorBoard,用作日志记录和绘图平台。
在下一个练习中,我们将为与PyTorch一起使用安装TensorBoard。如果你只使用过PyTorch,你可能需要遵循这些说明。对于那些之前已经安装过TensorFlow的你们,你们已经可以开始了,可以跳过这个练习:
-
打开一个Anaconda或Python shell并切换到你的虚拟环境。你很可能已经有一个打开的环境了。你可能想为TensorBoard创建一个完全独立的干净虚拟环境。这可以最小化在该环境中可能或将会破坏的代码量。TensorBoard是一个服务器应用程序,最好将其视为服务器应用程序,这意味着它运行的应该是一个纯净的环境。
-
然后,我们需要使用以下命令从你的Anaconda或Python shell中安装TensorBoard:
pip install tensorboard --upgrade
这将在你的虚拟环境中安装TensorBoard。
- 接下来,为了避免可能出现的依赖性问题,我们需要运行以下命令:
pip install future
- 安装完成后,我们可以使用以下命令运行TensorBoard:
tensorboard --logdir runs
- 这将在默认的
6006端口启动一个服务器应用程序,并从名为runs的文件夹中拉取日志,这是PyTorch默认使用的。如果你需要自定义端口或输入日志文件夹,可以使用以下命令选项:
tensorboard --logdir=/path_to_log_dir/ --port 6006
- TensorBoard是一个具有网络界面的服务器应用程序。这对于我们总是希望运行并从某些输出日志或其他数据处理文件夹中提取数据的应用程序来说是很常见的。以下图显示了在shell中运行的TensorBoard:

TensorBoard启动中
-
当TB首次运行时,它将输出你可以在浏览器中使用的地址以查看界面。按照图示复制URL并粘贴到你喜欢的网页浏览器中。请注意,如果你访问页面有困难,可能是一个绑定问题,意味着你的电脑可能阻止了这种访问。你可以尝试使用
localhost:6006或127.0.0.1:6006地址,或者为TB使用绑定所有选项。 -
当你的浏览器打开,并且假设你之前没有运行过TensorBoard或者没有在数据文件夹中放置输出,你将看到如下内容:

空的TensorBoard运行
当第一次运行TB时,需要注意的一个重要点是它正在使用正确的数据文件夹。你应该会看到数据位置:runs标签,指明将包含日志输出的文件夹。
在设置好TB之后,我们现在可以继续探索使Rainbow DQN比vanilla DQN好得多的创新之处。虽然我们已经在使用和探索了一些这些创新,但现在我们可以继续理解Rainbow中包含的剩余两个创新。我们将在下一节中从分布式RL开始。
分布式强化学习介绍
分布式强化学习这个名字可能有点误导,可能会让人联想到多层分布式网络中的DQN一起工作。好吧,这确实可能是分布式RL的描述,但分布RL是我们试图找到DQN预测的价值分布,也就是说,不仅仅是找到最大值或平均值,而是理解生成它的数据分布。这与PG方法的直觉和目的非常相似。我们通过将已知的或先前预测的分布投影到未来的或未来预测的分布来实现这一点。
这确实需要我们回顾一个代码示例,所以打开Chapter_10_QRDQN.py并跟随下一个练习:
- 整个代码列表太长了,无法在这里全部展示,所以我们将查看重要的部分。我们将从QRDQN或Quantile Regressive DQN开始。分位数回归是一种从观察中预测分布的技术。QRDQN的列表如下:
class QRDQN(nn.Module):
def __init__(self, num_inputs, num_actions, num_quants):
super(QRDQN, self).__init__()
self.num_inputs = num_inputs
self.num_actions = num_actions
self.num_quants = num_quants
self.features = nn.Sequential(
nn.Linear(num_inputs, 32),
nn.ReLU(),
nn.Linear(32, 64),
nn.ReLU(),
nn.Linear(64, 128),
nn.ReLU(),
nn.Linear(128, self.num_actions * self.num_quants)
)
self.num_quants, use_cuda=USE_CUDA)
def forward(self, x):
batch_size = x.size(0)
x = self.features(x)
x = x.view(batch_size, self.num_actions, self.num_quants)
return x
def q_values(self, x):
x = self.forward(x)
return x.mean(2)
def act(self, state, epsilon):
if random.random() > epsilon:
state = autograd.Variable(torch.FloatTensor(np.array(state, dtype=np.float32)).unsqueeze(0), volatile=True)
qvalues = self.forward(state).mean(2)
action = qvalues.max(1)[1]
action = action.data.cpu().numpy()[0]
else:
action = random.randrange(self.num_actions)
return action
-
这段代码的大部分看起来和之前一样,但需要注意的一点是
qvalues表示一个Q值(状态-动作),而不是像PG方法中我们看到的那样表示Q策略值。 -
接下来,我们将滚动到
projection_distribution函数,如下所示:
def projection_distribution(dist, next_state, reward, done):
next_dist = target_model(next_state)
next_action = next_dist.mean(2).max(1)[1]
next_action = next_action.unsqueeze(1).unsqueeze(1).expand(batch_size, 1, num_quant)
next_dist = next_dist.gather(1, next_action).squeeze(1).cpu().data
expected_quant = reward.unsqueeze(1) + 0.99 * next_dist * (1 - done.unsqueeze(1))
expected_quant = autograd.Variable(expected_quant)
quant_idx = torch.sort(dist, 1, descending=False)[1]
tau_hat = torch.linspace(0.0, 1.0 - 1./num_quant, num_quant) + 0.5 / num_quant
tau_hat = tau_hat.unsqueeze(0).repeat(batch_size, 1)
quant_idx = quant_idx.cpu().data
batch_idx = np.arange(batch_size)
tau = tau_hat[:, quant_idx][batch_idx, batch_idx]
return tau, expected_quant
-
这段代码相当数学化,并且超出了本书的范围。它本质上只是提取它认为的Q值的分布。
-
之后,我们可以看到我们两个模型的构建,这表明我们在这里正在使用以下代码构建一个DDQN模型:
current_model = QRDQN(env.observation_space.shape[0], env.action_space.n, num_quant)
target_model = QRDQN(env.observation_space.shape[0], env.action_space.n, num_quant)
- 之后,我们得到使用
computer_td_loss函数计算TD损失,如下所示:
def compute_td_loss(batch_size):
state, action, reward, next_state, done = replay_buffer.sample(batch_size)
state = autograd.Variable(torch.FloatTensor(np.float32(state)))
next_state = autograd.Variable(torch.FloatTensor(np.float32(next_state)), volatile=True)
action = autograd.Variable(torch.LongTensor(action))
reward = torch.FloatTensor(reward)
done = torch.FloatTensor(np.float32(done))
dist = current_model(state)
action = action.unsqueeze(1).unsqueeze(1).expand(batch_size, 1, num_quant)
dist = dist.gather(1, action).squeeze(1)
tau, expected_quant = projection_distribution(dist, next_state, reward, done)
k = 1
huber_loss = 0.5 * tau.abs().clamp(min=0.0, max=k).pow(2)
huber_loss += k * (tau.abs() - tau.abs().clamp(min=0.0, max=k))
quantile_loss = (tau - (tau < 0).float()).abs() * huber_loss
loss = torch.tensor(quantile_loss.sum() / num_quant, requires_grad=True)
optimizer.zero_grad()
loss.backward()
nn.utils.clip_grad_norm(current_model.parameters(), 0.5)
optimizer.step()
return loss
- 这个损失计算函数与我们之前看到的其他DQN实现类似,尽管这个实现确实暴露了一些曲折。大多数曲折都是通过使用分位数回归(QR)引入的。QR本质上是通过使用分位数或分位数来预测分布,即概率的切片,以迭代地确定预测分布。然后使用这个预测分布来确定网络损失,并通过深度学习网络进行训练。如果你向上滚动,你可以注意到引入了三个新的超参数,允许我们调整搜索。这里显示的新值允许我们定义迭代次数
num_quants和搜索范围Vmin和Vmax:
num_quant = 51
Vmin = -10
Vmax = 10
- 最后,我们可以通过滚动到代码的底部并在这里查看,来了解训练代码是如何运行的:
state = env.reset()
for iteration in range(1, iterations + 1):
action = current_model.act(state, epsilon_by_frame(iteration))
next_state, reward, done, _ = env.step(action)
replay_buffer.push(state, action, reward, next_state, done)
state = next_state
episode_reward += reward
if done:
state = env.reset()
all_rewards.append(episode_reward)
episode_reward = 0
if len(replay_buffer) > batch_size:
loss = compute_td_loss(batch_size)
losses.append(loss.item())
if iteration % 200 == 0:
plot(iteration, all_rewards, losses, episode_reward)
if iteration % 1000 == 0:
update_target(current_model, target_model)
- 我们在第六章和第七章中已经看到过非常类似的代码,当时我们之前查看DQN时,所以这里不会进行回顾。相反,如果你需要的话,再次熟悉一下DQN模型。注意它与PG方法之间的区别。当你准备好时,像平常一样运行代码。运行样本的输出如下所示:

Chapter_10_QRDQN.py的示例输出
从这个样本生成的输出只是一个提醒,我们还有更多信息被输出到日志文件夹中。要查看该日志文件夹,我们需要再次运行TensorBoard,我们将在下一节中这样做。
回到TensorBoard
在上一个练习的样本仍在运行的情况下,我们想回到TensorBoard,并现在查看样本运行的输出。为此,打开一个新的Python/Anaconda命令行窗口,并按照下一个练习进行操作:
- 打开与你在运行之前的练习代码示例相同的文件夹的shell。切换到你的虚拟环境或专门用于TB的特殊环境,然后运行以下命令以启动过程:
tensorboard --logdir=runs
- 这将在当前文件夹中启动TB,使用该
runs文件夹作为数据转储目录。样本运行一段时间后,当你现在访问TB网络界面时,你可能看到以下类似的内容:

TensorBoard输出来自Chapter_10_QRDQN.py
-
将截图所示的平滑控制调高,以查看数据的可视化趋势。看到数据的一般趋势允许你推断你的智能体何时可能完全训练。
-
现在,我们需要回到
Chapter_10_QRDQN.py示例代码,看看我们是如何生成这些输出数据的。首先,注意新的import和声明一个新变量writer,它是从torch.utils.tensorboard导入的SummaryWriter类,如下所示:
from common.replay_buffer import ReplayBuffer
from torch.utils.tensorboard import SummaryWriter
env_id = "LunarLander-v2"
env = gym.make(env_id)
writer = SummaryWriter()
-
writer对象用于输出到在run文件夹中构建的日志文件。现在每次我们运行这个示例代码块时,这个 writer 都会输出到run文件夹。你可以通过将目录输入到SummaryWriter构造函数中来改变这种行为。 -
接下来,向下滚动到修订的
plot函数。这个函数,如这里所示,现在生成我们可以用 TB 可视化的日志输出:
def plot(iteration, rewards, losses, ep_reward):
print("Outputing Iteration " + str(iteration))
writer.add_scalar('Train/Rewards', rewards[-1], iteration)
writer.add_scalar('Train/Losses', losses[-1], iteration)
writer.add_scalar('Train/Exploration', epsilon_by_frame(iteration), iteration)
writer.add_scalar('Train/Episode', ep_reward, iteration)
writer.flush()
-
这个更新的代码块现在使用 TB
writer而不是我们之前使用的matplotlib plot输出结果。每次调用writer.add_scalar都会将一个值添加到我们之前可视化的数据图中。有许多其他你可以调用的函数来添加许多不同类型的输出。考虑到我们生成令人印象深刻输出的容易程度,你可能永远不会再次需要使用matplotlib。 -
返回你的 TB 网页界面,并观察持续的训练输出。
这个代码示例可能需要一些调整才能将智能体调整到成功的策略。然而,你现在有更多更强大的工具 TensorBoard 来帮助你做到这一点。在下一节中,我们将探讨 Rainbow 引入的最后一个改进:噪声网络。
理解噪声网络
噪声网络并不是指那些需要知道一切的网络——那些会是好奇的网络。相反,噪声网络引入了噪声的概念,用于通过网络预测输出时的权重。因此,我们不再只有一个标量值来表示感知器中的权重,我们现在认为权重是从某种形式的分布中抽取的。显然,我们在这里有一个共同的主题,那就是从处理单个标量值作为数字到更好的描述为数据分布。如果你研究过贝叶斯或变分推理的主题,你可能会具体理解这个概念。
对于没有那个背景的人来说,让我们看看以下图中分布可能是什么:

不同数据分布的示例
前面图表的来源是Akshay Sharma的一篇博客文章(https://medium.com/mytake/understanding-different-types-of-distributions-you-will-encounter-as-a-data-scientist-27ea4c375eec)。图表中显示的是各种知名数据分布的采样模式。基本统计学假设所有数据总是均匀或正态分布的。在统计学中,你将了解到其他分布,你将使用它们来定义各种方差测试或拟合测试,如卡方或学生t分布。如果你在计算机程序中随机采样过数字,你很可能非常熟悉均匀分布。大多数计算机程序总是假设均匀分布,这在某种程度上是机器学习中的问题,因此转向更好地理解真实数据或动作/事件是如何分布的。
变分推理或定量风险分析是一种技术,我们使用工程、经济学或其他学科的典型方程,并假设它们的输入是分布而不是判别值。因此,使用数据的分布作为输入,我们假设我们的输出也是某种形式的分布。这个分布可以用来评估风险或奖励的术语。
是时候进行另一个练习了,所以打开Chapter_10_NDQN.py并遵循下一个练习:
- 这是一个很大的例子,所以我们只会关注重要的部分。让我们先向下滚动,看看这里的
NoisyDQN类:
class NoisyDQN(nn.Module):
def __init__(self, num_inputs, num_actions):
super(NoisyDQN, self).__init__()
self.linear = nn.Linear(env.observation_space.shape[0], 128)
self.noisy1 = NoisyLinear(128, 128)
self.noisy2 = NoisyLinear(128, env.action_space.n)
def forward(self, x):
x = F.relu(self.linear(x))
x = F.relu(self.noisy1(x))
x = self.noisy2(x)
return x
def act(self, state):
state = autograd.Variable(torch.FloatTensor(state).unsqueeze(0), volatile=True)
q_value = self.forward(state)
action = q_value.max(1)[1].item()
return action
def reset_noise(self):
self.noisy1.reset_noise()
self.noisy2.reset_noise()
-
这与我们的之前的DQN示例非常相似,但有一个关键的区别:添加了一个新的专业深度学习网络层类型,称为
NoisyLinear。 -
继续向下滚动,我们可以看到
td_compute_loss函数已更新以处理噪声或模糊层:
def compute_td_loss(batch_size, beta):
state, action, reward, next_state, done, weights, indices = replay_buffer.sample(batch_size, beta)
state = autograd.Variable(torch.FloatTensor(np.float32(state)))
next_state = autograd.Variable(torch.FloatTensor(np.float32(next_state)))
action = autograd.Variable(torch.LongTensor(action))
reward = autograd.Variable(torch.FloatTensor(reward))
done = autograd.Variable(torch.FloatTensor(np.float32(done)))
weights = autograd.Variable(torch.FloatTensor(weights))
q_values = current_model(state)
next_q_values = target_model(next_state)
q_value = q_values.gather(1, action.unsqueeze(1)).squeeze(1)
next_q_value = next_q_values.max(1)[0]
expected_q_value = reward + gamma * next_q_value * (1 - done)
loss = (q_value - expected_q_value.detach()).pow(2) * weights
prios = loss + 1e-5
loss = loss.mean()
optimizer.zero_grad()
loss.backward()
optimizer.step()
replay_buffer.update_priorities(indices, prios.data.cpu().numpy())
current_model.reset_noise()
target_model.reset_noise()
return loss
-
这个函数与我们之前的vanilla DQN示例非常相似,这是因为所有的工作/差异都在新的噪声层中,我们将在稍后讨论。
-
滚动回
NoisyLinear类的定义,如下所示:
class NoisyLinear(nn.Module):
def __init__(self, in_features, out_features, std_init=0.4):
super(NoisyLinear, self).__init__()
self.in_features = in_features
self.out_features = out_features
self.std_init = std_init
self.weight_mu = nn.Parameter(torch.FloatTensor(out_features, in_features))
self.weight_sigma = nn.Parameter(torch.FloatTensor(out_features, in_features))
self.register_buffer('weight_epsilon', torch.FloatTensor(out_features, in_features))
self.bias_mu = nn.Parameter(torch.FloatTensor(out_features))
self.bias_sigma = nn.Parameter(torch.FloatTensor(out_features))
self.register_buffer('bias_epsilon', torch.FloatTensor(out_features))
self.reset_parameters()
self.reset_noise()
def forward(self, x):
if self.training:
weight = self.weight_mu + self.weight_sigma.mul(autograd.Variable(self.weight_epsilon))
bias = self.bias_mu + self.bias_sigma.mul(autograd.Variable(self.bias_epsilon))
else:
weight = self.weight_mu
bias = self.bias_mu
return F.linear(x, weight, bias)
def reset_parameters(self):
mu_range = 1 / math.sqrt(self.weight_mu.size(1))
self.weight_mu.data.normal_(-mu_range, mu_range)
self.weight_sigma.data.fill_(self.std_init / math.sqrt(self.weight_sigma.size(1)))
self.bias_mu.data.normal_(-mu_range, mu_range)
self.bias_sigma.data.fill_(self.std_init / math.sqrt(self.bias_sigma.size(0)))
def reset_noise(self):
epsilon_in = self._scale_noise(self.in_features)
epsilon_out = self._scale_noise(self.out_features)
self.weight_epsilon.copy_(epsilon_out.ger(epsilon_in))
self.bias_epsilon.copy_(self._scale_noise(self.out_features))
def _scale_noise(self, size):
x = torch.randn(size)
x = x.sign().mul(x.abs().sqrt())
return x
NoisyLinear类是一个使用正态分布来定义层中每个权重的层。这个分布被假定为正态分布,这意味着它由均值、mu和标准差、sigma定义。因此,如果我们之前在一个层中假设有100个权重,我们现在将有两个值(mu和sigma)来定义权重的采样方式。反过来,mu和sigma的值也成为了我们在网络上训练的值。
在其他框架中,将应用变分权重到层中的能力构建进去相当困难,通常需要更多的代码。幸运的是,这是PyTorch的一个优势,它增强了一个内置的概率框架,旨在预测和处理分布数据。
-
不同的分布可能使用不同的描述性值来定义它们。正态分布或高斯分布由mu和sigma定义,而均匀分布通常只由最小/最大值定义,而三角形分布则是由最小/最大值和峰值定义的。我们几乎总是更喜欢使用正态分布来描述大多数自然事件。
-
滚动到训练代码,你会看到几乎与上一个练习相同的代码,只有一个关键的区别。在探索中,我们不是使用epsilon,而是引入了一个称为beta的术语。beta成为我们的实际探索术语,并取代了epsilon。
-
按照正常方式运行应用程序,并在TensorBoard中观察训练,如图中所示截图:

从样本Chapter_10_NDQN.py生成的TensorBoard输出
箭头及其指向的方向表示我们希望我们的代理/算法移动的方向。我们通过将平滑参数增加到最大值,即.99,来获得这些趋势图。通过这样做,更容易看到一般或中值趋势。
在我们继续到Rainbow之前,我们需要重新审视一下在使用噪声网络时如何管理探索。这也有助于解释新beta参数对于z的使用。
用于探索和重要性采样的噪声网络
使用噪声网络也会在我们的动作预测中引入模糊性。也就是说,由于网络的权重现在是从一个分布中抽取的,这也意味着它们正在变得分布化。我们也可以说它们是随机的,而随机性是由一个分布定义的,基本上意味着相同的输入可能会产生两个完全不同的结果,这意味着我们不能再仅仅取最大或最佳动作,因为现在这只是一个模糊的概念。相反,我们需要一种方法来减小我们用于权重的采样分布的大小,因此减少我们在代理选择的动作中的不确定性。
减小分布的大小基本上等同于减少该数据的不确定性。这是数据科学和机器学习的一个基石。
我们通过引入一个称为beta的因子来减少这种不确定性,这个因子会随着时间的推移而增加。这种增加与epsilon不同,只是方向相反。让我们通过重新打开Chapter_10_NDQN.py文件并跟随这里的练习来看看这看起来像什么代码:
- 我们可以通过查看主要代码来了解beta是如何定义的:
beta_start = 0.4
beta_iterations = 50000
beta_by_iteration = lambda iteration: min(1.0, beta_start + iteration * (1.0 - beta_start) / beta_iterations)
-
这个设置和方程再次与之前定义的epsilon类似。这里的区别在于beta是逐渐增加的。
-
Beta用于纠正正在训练的权重,从而引入了重要性采样的概念。重要性采样是关于我们在纠正/采样权重之前对它们的重视程度。Beta因此成为重要性采样因子,其中1.0的值表示100%重要,而0表示没有重要性。
-
打开同一项目中
common文件夹中的replay_buffer.py文件。向下滚动到sample函数,并注意代码,如图所示:
assert beta > 0
idxes = self._sample_proportional(batch_size)
weights = []
p_min = self._it_min.min() / self._it_sum.sum()
max_weight = (p_min * len(self._storage)) ** (-beta)
for idx in idxes:
p_sample = self._it_sum[idx] / self._it_sum.sum()
weight = (p_sample * len(self._storage)) ** (-beta)
weights.append(weight / max_weight)
weights = np.array(weights)
encoded_sample = self._encode_sample(idxes)
return tuple(list(encoded_sample) + [weights, idxes])
-
sample函数是我们使用的PrioritizedExperienceReplay类的一部分,用于存储经验。除了意识到它按优先级排序经验之外,我们不需要审查这个类的全部内容。 -
最后,回到示例代码并回顾一下绘图函数。现在TensorBoard中生成我们的beta绘图的那一行看起来是这样的:
writer.add_scalar('Train/Beta', beta_by_iteration(iteration), iteration)
- 到目前为止,你可以回顾更多的代码,或者尝试调整新的超参数,然后再继续。
这样,我们就完成了对噪声和非噪声网络探索的考察。我们看到了如何引入分布作为我们深度学习网络权重的使用。然后,我们看到为了补偿这一点,我们需要引入一个新的训练参数,beta。在下一节中,我们将看到所有这些部分如何在Rainbow DQN中结合在一起。
揭示Rainbow DQN
《Rainbow:结合深度强化学习中的改进》一书的作者Matteo Hessel (https://arxiv.org/search/cs?searchtype=author&query=Hessel%2C+M),与其他一些最先进的DRL模型进行了多次比较,其中许多我们已经看过。他们使用标准的2D经典Atari游戏进行了这些比较,并取得了令人印象深刻的结果。Rainbow DQN优于所有当前最先进的算法。在论文中,他们使用了熟悉的经典Atari环境。这是可以的,因为DeepMind为该环境有大量数据,可以与比较的模型相关联。然而,许多人观察到,论文缺乏PG方法,如PPO的比较。当然,PPO是OpenAI的进步,它可能被Google DeepMind视为侵权,或者只是想通过完全不进行比较来避免认可。不幸的是,这也表明,即使是像DRL这样高度智力追求的东西也无法摆脱政治。
PPO等方法已被用于克服DRL当前的一些最大挑战。实际上,PPO在Unity Obstacle Tower Challenge中赢得了10万美元的大奖。因此,你很快就不应该低估PG方法。
根据前面的绘图,我们应该对Rainbow抱有很高的期望。所以,让我们打开 Chapter_10_Rainbow.py 文件,并跟随下一个练习:
- 到现在为止,这个例子应该已经很熟悉了,我们将限制自己只查看差异,从下面这里
RainbowDQN类的主要实现开始:
class RainbowDQN(nn.Module):
def __init__(self, num_inputs, num_actions, num_atoms, Vmin, Vmax):
super(RainbowDQN, self).__init__()
self.num_inputs = num_inputs
self.num_actions = num_actions
self.num_atoms = num_atoms
self.Vmin = Vmin
self.Vmax = Vmax
self.linear1 = nn.Linear(num_inputs, 32)
self.linear2 = nn.Linear(32, 64)
self.noisy_value1 = NoisyLinear(64, 64, use_cuda=False)
self.noisy_value2 = NoisyLinear(64, self.num_atoms, use_cuda=False)
self.noisy_advantage1 = NoisyLinear(64, 64, use_cuda=False)
self.noisy_advantage2 = NoisyLinear(64, self.num_atoms * self.num_actions, use_cuda=False)
def forward(self, x):
batch_size = x.size(0)
x = F.relu(self.linear1(x))
x = F.relu(self.linear2(x))
value = F.relu(self.noisy_value1(x))
value = self.noisy_value2(value)
advantage = F.relu(self.noisy_advantage1(x))
advantage = self.noisy_advantage2(advantage)
value = value.view(batch_size, 1, self.num_atoms)
advantage = advantage.view(batch_size, self.num_actions, self.num_atoms)
x = value + advantage - advantage.mean(1, keepdim=True)
x = F.softmax(x.view(-1, self.num_atoms)).view(-1, self.num_actions, self.num_atoms)
return x
def reset_noise(self):
self.noisy_value1.reset_noise()
self.noisy_value2.reset_noise()
self.noisy_advantage1.reset_noise()
self.noisy_advantage2.reset_noise()
def act(self, state):
state = autograd.Variable(torch.FloatTensor(state).unsqueeze(0), volatile=True)
dist = self.forward(state).data.cpu()
dist = dist * torch.linspace(self.Vmin, self.Vmax, self.num_atoms)
action = dist.sum(2).max(1)[1].numpy()[0]
return action
- 上述代码定义了Rainbow DQN的网络结构。这个网络有点复杂,所以我们已经将主要元素放在这里的图中:

Rainbow网络架构
-
如果你查看
init和forward函数,你应该能够看到这个图是如何构建的。 -
我们还不能离开前面的代码,我们需要再次审查act函数,如下所示:
def act(self, state):
state = autograd.Variable(torch.FloatTensor(state).unsqueeze(0), volatile=True)
dist = self.forward(state).data.cpu()
dist = dist * torch.linspace(self.Vmin, self.Vmax, self.num_atoms)
action = dist.sum(2).max(1)[1].numpy()[0]
return action
act函数展示了智能体如何选择动作。我们在这里已经细化了动作选择策略,现在使用Vmin、Vmax和num_atoms的值。我们将这些值作为输入传递给torch.linspace,以此创建一个从Vmin到Vmax的离散分布,步长由num_atoms定义。这会输出最小/最大范围内的缩放值,然后这些值乘以forward函数输出的原始分布dist。将forward函数返回的分布与由torch.linspace生成的分布相乘,这是一种缩放类型。
你可能已经注意到,超参数num_atoms、Vmin和Vmax现在在调整模型参数时具有双重作用。这通常是一件坏事。也就是说,你总是希望定义的超参数具有单一目的。
- 接下来,我们将向下滚动并查看
projection_distribution函数中的差异。记住这个函数是执行寻找分布的分布部分,而不是离散值:
def projection_distribution(next_state, rewards, dones):
batch_size = next_state.size(0)
delta_z = float(Vmax - Vmin) / (num_atoms - 1)
support = torch.linspace(Vmin, Vmax, num_atoms)
next_dist = target_model(next_state).data.cpu() * support
next_action = next_dist.sum(2).max(1)[1]
next_action = next_action.unsqueeze(1).unsqueeze(1).expand(next_dist.size(0), 1, next_dist.size(2))
next_dist = next_dist.gather(1, next_action).squeeze(1)
rewards = rewards.unsqueeze(1).expand_as(next_dist)
dones = dones.unsqueeze(1).expand_as(next_dist)
support = support.unsqueeze(0).expand_as(next_dist)
Tz = rewards + (1 - dones) * 0.99 * support
Tz = Tz.clamp(min=Vmin, max=Vmax)
b = (Tz - Vmin) / delta_z
l = b.floor().long()
u = b.ceil().long()
offset = torch.linspace(0, (batch_size - 1) * num_atoms, batch_size).long()\
.unsqueeze(1).expand(batch_size, num_atoms)
proj_dist = torch.zeros(next_dist.size())
proj_dist.view(-1).index_add_(0, (l + offset).view(-1), (next_dist * (u.float() - b)).view(-1))
proj_dist.view(-1).index_add_(0, (u + offset).view(-1), (next_dist * (b - l.float())).view(-1))
return proj_dist
-
这段代码与我们之前查看的量分回归代码有很大不同。这里的主要区别是使用了PyTorch库,而之前代码更底层。使用库会使代码更加冗长,但希望你现在可以欣赏到代码的说明性比之前的示例更清晰。
-
这里需要注意的是,我们继续使用
epsilon进行探索,如下面的代码所示:
epsilon_start = 1.0
epsilon_final = 0.01
epsilon_decay = 50000
epsilon_by_frame = lambda iteration: epsilon_final + (epsilon_start - epsilon_final) * math.exp(-1\. * iteration / epsilon_decay)
- 按照常规方式运行示例,并观察输出。
请记住,由于这个示例缺少优先级回放缓冲区,它无法成为完整的RainbowDQN实现。然而,它确实涵盖了80/20规则,实现优先级回放缓冲区被留作读者的练习。在我们跳转到观察训练的下一节时,让样本继续运行。
训练何时会失败?
对于任何新进入深度学习领域的人来说,尤其是对于深度强化学习,一个常见的问题就是何时知道你的模型是失败的,只是有点固执,或者永远都不会工作。这是一个在人工智能领域引起挫败感和焦虑的问题,常常让你想知道:“如果我让那个智能体再训练一天会怎样?”不幸的是,如果你和专家交谈,他们通常会说你只需要耐心,继续训练,但这可能是在那些挫败感的基础上。毕竟,如果你构建的东西永远没有希望做任何事情,你是在浪费时间精力去维持它吗?
许多人都面临的一个问题是,算法/模型越复杂,训练所需的时间就越长,除非你之前训练过它或者阅读过一篇使用完全相同模型的非常优秀的论文。即使使用完全相同的模型,环境也可能有所不同,可能更加复杂。考虑到所有这些因素,以及调整超参数的痛苦,真让人惊讶,为什么任何理智的人会想要在强化学习(DRL)领域工作。
作者主持了一个针对强化学习(RL)和深度强化学习(DRL)的深度学习Meetup支持小组。这个小组中经常讨论的一个话题是,深度学习研究人员如何保持他们的理智和/或降低他们的压力水平。如果你从事人工智能工作,你就会理解不断满足世界期望的必要性,这种期望是好事,但涉及投资者或缺乏耐心的老板时,也可能是一件坏事。
幸运的是,有了像TensorBoard这样的高级工具,我们可以深入了解代理是如何训练或希望如何训练的。
打开TensorBoard并遵循下一个练习,了解如何有效地诊断训练问题:
- 以下截图显示了TB的输出:

Rainbow DQN的TensorBoard输出
-
从前面的截图可以看出,平滑已经提高到.99,我们可以看到训练失败了。记住,截图中的图表都有注释,以显示首选的方向。对于所有这些图表,情况并非如此。然而,不要假设如果图表的方向相反,它就一定是坏的——它不是。相反,任何移动都是一些训练活动的更好指标。这也是我们为什么要高度平滑这些图表的原因。
-
常常决定未来训练性能的一个关键图是损失图。当损失减少时,代理在学习;当损失增加时,代理可能会忘记/困惑。如果损失保持不变,那么代理可能停滞不前,可能会困惑或陷入困境。这里展示的截图是对这一点的有益总结:

损失图总结
-
上述截图显示了在超过50万次回合的理想训练过程中,对于这个环境,你可以预期训练的回合数是原来的两倍或三倍。一般来说,如果训练时间超过10%,无论是正向还是负向的移动,都应被视为失败。例如,如果你正在训练一个代理进行100万次迭代,那么你的10%窗口大约是10万次迭代。如果你的代理在某个图上持续训练或处于平坦状态,除了优势之外,在等于或大于10%窗口大小的期间,可能最好调整超参数并重新开始。
-
再次强调,特别关注损失图,因为它提供了训练问题的最强指标。
-
您可以通过重复运行样本相同的迭代次数来并排查看多个训练尝试的结果,如图中所示:

同一图表上的多个训练输出示例
- 停止当前样本,更改一些超参数,然后再次运行以查看前面的示例。
这些简单的规则可能会帮助您在构建/训练新或不同环境中的模型时避免挫败感。幸运的是,我们还有更多章节要学习,其中包括大量类似下一节中展示的练习。
练习
当涉及到在现实世界中工作时,您从这些练习中获得的经验可能意味着得到那份工作与保住那份工作的区别。作为一名程序员,您不仅需要理解某物是如何工作的;您是一个需要亲自动手并实际工作的机械师/工程师:
-
调整
Chapter_10_QRDQN.py的超参数,并查看这对训练有什么影响。 -
调整
Chapter_10_NDQN.py的超参数,并查看这对训练有什么影响。 -
调整
Chapter_10_Rainbow.py的超参数,并查看这对训练有什么影响。 -
在另一个环境(如CartPole、FrozenLake或更复杂的环境如Atari)上运行和调整本章样本的超参数。如果您的电脑较旧且需要更努力地训练智能体,降低环境的复杂性也是有帮助的。
-
本章还包括
Chapter_10_HDQN.py和Chapter_10_C51.py示例中的分层DQNs和分类DQNs的示例代码。运行这些示例,审查代码,并自行研究这些示例为DRL带来的改进。 -
添加从任何示例中保存/加载训练模型的功能。现在您可以使用训练好的模型来展示智能体玩游戏吗?
-
添加将其他可能认为对训练智能体重要的训练值输出到TensorBoard的功能。
-
将
NoisyLinear层添加到Chapter_10_QRDQN.py示例中。示例中可能已经存在一些只是被注释掉的代码。 -
将优先级回放缓冲区添加到
Chapter_10_Rainbow.py示例中。您可以使用在Chapter_10_NDQN.py示例中找到的相同方法。 -
TensorBoard允许您输出和可视化训练模型。使用TensorBoard从示例中输出训练模型。
显然,练习的数量已经增加,以反映您在DRL技能水平和/或兴趣上的增长。您当然不需要完成所有这些练习,但2-3个就足够了。在下一节中,我们将总结本章内容。
摘要
在本章中,我们特别关注了DeepMind在DRL领域的一项更先进的进展,称为Rainbow DQN。Rainbow在DQN的基础上结合了多项改进,这些改进显著提高了训练性能。由于我们已经涵盖了这些改进中的许多,我们只需要回顾一些新的进展。然而,在这样做之前,我们安装了TensorBoard作为调查训练性能的工具。然后,我们探讨了分布式RL的第一个进展以及如何通过理解采样分布来建模动作。继续探讨分布,我们接下来研究了有噪声的网络层——这些网络层没有单个权重,而是有描述每个权重的单个分布。基于这个例子,我们转向了Rainbow DQN,在我们的最后一个例子中完成,并快速讨论了何时确定智能体不可训练或表现停滞不前。
在下一章中,我们将从构建DRL算法/智能体转向使用Unity构建环境,并使用ML-Agents工具包在这些环境中构建智能体。
第十三章:利用 ML-Agents
在某个时候,我们需要超越构建和训练代理算法,探索构建我们自己的环境。构建自己的环境也将使你在制作良好的奖励函数方面获得更多经验。我们在强化学习(RL)和深度强化学习(DRL)中几乎忽略了这个问题,而这正是制作良好奖励函数的关键。
在本章中,我们将探讨什么使奖励函数良好,或者奖励函数是什么。我们将通过使用 Unity 游戏引擎构建新环境来讨论奖励函数。我们将首先安装和设置 Unity ML-Agents,这是一个用于构建代理和环境的高级 DRL 工具包。从那里,我们将探讨如何构建一个标准的 Unity 示例环境,以便我们使用 PyTorch 模型。方便的是,这使我们能够使用 ML-Agents 工具包从 Python 和 PyTorch 中使用 Unity 环境,与之前探索的 Rainbow DQN 模型一起。之后,我们将探讨创建一个新环境,然后通过探讨 Unity 为推进 RL 所开发的进步来结束本章。
在本章中,我们将涵盖以下主要主题:
-
安装 ML-Agents
-
构建 Unity 环境
-
使用 Rainbow 训练 Unity 环境
-
创建一个新环境
-
使用 ML-Agents 推进 RL
Unity 是游戏开发中最大和最常用的游戏引擎。如果你是游戏开发者,你很可能已经知道这一点。游戏引擎本身是用 C++ 开发的,但它提供了一个 C# 脚本接口,99% 的游戏开发者都在使用它。因此,在本章中,我们需要向您展示一些 C# 代码,但只是很少量。
在下一节中,我们将安装 Unity 和 ML-Agents 工具包。
安装 ML-Agents
安装游戏引擎本身(Unity)并不困难,但在与 ML-Agents 一起工作时,您在选择版本时需要小心。因此,下一个练习旨在更具可配置性,这意味着您在执行练习时可能需要提问/回答问题。我们这样做是为了使这个练习更持久,因为这个工具包已知经常发生许多破坏性更改。
Unity 可以在任何主流桌面计算机(Windows、Mac 或 Linux)上运行,所以打开您的开发计算机,按照下一个练习安装 Unity 和 ML-Agents 工具包:
- 在安装 Unity 之前,请检查 ML-Agents GitHub 安装页面 (https://github.com/Unity-Technologies/ml-agents/blob/master/docs/Installation.md) 并确认当前支持哪个版本的 Unity。在撰写本文时,这是 2017.4 版本,尽管文档建议支持后续版本,但我们更倾向于只使用那个版本。
您可以直接下载并安装 Unity,或者通过 Unity Hub 进行安装。由于管理多个版本的 Unity 非常常见,Unity 为此目的构建了一个管理应用程序,即 Unity Hub。
-
下载并安装所需的最低版本的 Unity。如果您从未安装过 Unity,您将需要创建一个用户帐户并验证其许可协议。创建用户帐户后,您将能够运行 Unity 编辑器。
-
打开 Python/Anaconda 命令行,并确保使用以下命令激活您的虚拟环境:
conda activate gameAI
--- or ---
activate gameAI
- 使用以下命令安装 Unity Gym 包装器:
pip install gym_unity
- 切换到根工作文件夹,最好是
C:或/,并使用以下命令创建一个目录以克隆 ML-Agents 工具包:
cd /
mkdir mlagents
cd mlagents
- 然后,假设您已安装
git,使用以下命令使用git拉取 ML-Agents 工具包:
git clone https://github.com/Unity-Technologies/ml-agents.git
我们更喜欢根目录的原因是 ML-Agents 目录结构可能相当深,这可能在某些操作系统中导致文件名过长错误。
- 通过查阅当前 Unity 文档并使用他们最新的指南来测试整个安装效果最佳。一个好的起点是第一个示例环境,3D 平衡球。您可以在https://github.com/Unity-Technologies/ml-agents/blob/master/docs/Getting-Started-with-Balance-Ball.md找到此文档。
抽出一些时间,自己探索 ML-Agents 工具包。它旨在易于访问,如果您在 DRL 方面的唯一经验是这本书,那么您现在应该有足够的背景知识来理解运行 Unity 环境的一般概念。我们将回顾一些这些流程,但还有许多其他有用的指南可以帮助您在 Unity 中运行 ML-Agents。我们在这里的优先事项将是使用 Unity 构建环境,以及可能的新环境,我们可以使用这些环境来测试我们的模型。虽然我们不会使用 ML-Agents 工具包来训练智能体,但我们将使用 Gym 包装器,这确实需要了解大脑或学院是什么。
Adam Kelly 拥有一个优秀的博客,沉浸式极限 (http://www.immersivelimit.com/tutorials/tag/Unity+ML+Agents),专注于机器学习和 DRL,并专注于创建非常酷的 ML-Agents 环境和项目。
ML-Agents 目前使用 PPO 和 Soft Actor-Critic 方法来训练代理。它还提供了使用卷积和循环网络进行状态编码的几个有用模块,从而允许进行视觉观察编码和记忆或上下文。此外,它提供了定义离散或连续动作空间的方法,以及启用混合动作或观察类型。这个工具包做得非常好,但随着机器学习领域的快速变化,它已经变得很快过时,或者可能只是被炒作得过多。最终,似乎大多数研究人员或严肃的 DRL 实践者现在只想构建自己的框架。
虽然 DRL 非常复杂,但制作出强大功能的代码量仍然相当小。因此,我们可能会看到许多 RL 框架试图在这个领域站稳脚跟。无论您决定使用这些框架中的哪一个,还是构建自己的框架,这完全取决于您。只需记住,框架有来有去,但您对某个主题的底层知识越多,您指导未来决策的能力就越强。
无论您决定是否使用 ML-Agents 框架来训练 DRL 代理,使用其他框架,还是构建自己的框架,Unity 都为您提供了构建新的、更令人兴奋的环境的绝佳机会。我们将在下一节学习如何构建一个可以使用 DRL 训练的 Unity 环境。
构建 Unity 环境
ML-Agents 工具包不仅提供了一个 DRL 训练框架,还提供了一种在 Unity 游戏中快速轻松地设置 AI 代理的机制。这些代理可以通过 Gym 接口外部控制——是的,就是我们在训练大多数先前代理/算法时使用的同一个接口。这个平台真正伟大的地方之一是 Unity 提供了几个我们可以探索的新演示环境。稍后,我们将探讨如何为训练代理构建自己的环境。
本节中的练习旨在总结构建用于 Python 训练的可执行环境所需的设置步骤。它们是为那些只想构建训练环境而不想学习 Unity 所有知识的 Unity 新手准备的。如果您在使用这些练习时遇到问题,那么 SDK 可能已经发生了变化。如果是这种情况,那么只需回退并查阅完整的在线文档。
构建用于代理训练的 Unity 环境需要一些专门的步骤,我们将在本练习中介绍:
-
首先,打开 Unity 编辑器,无论是通过 Unity Hub 还是直接使用 Unity 本身。请记住使用支持 ML-Agents 工具包的版本。
-
使用 Unity Hub,我们可以通过以下截图所示的 添加 按钮添加项目:

在 Unity Hub 中添加项目
-
点击添加后,您将被提示定位项目文件夹。使用对话框找到并选择我们在之前的练习中用
git下载的UnitySDK项目文件夹。这个文件夹应该位于您的/mlagents/ml-agents/UnitySDK文件夹中。 -
当项目被添加后,它也将被添加到项目列表的顶部。您可能会看到一个警告图标,表示您需要选择版本号。选择与ML-Agents工具包相匹配的Unity版本,然后选择项目以在编辑器中启动它。
-
您可能会被提示升级项目。如果您被提示,请选择是以进行升级。如果升级失败或项目无法正常运行,您可以简单地删除所有旧文件,并尝试使用不同版本的Unity。加载此项目可能需要一些时间,所以请耐心等待,拿杯饮料,然后等待。
-
项目加载完成并打开编辑器后,通过双击位于
Assets/ML-Agents/Examples/Hallway/Scenes文件夹中的3DBall场景文件来打开3DBall环境的场景。 -
我们需要将学院设置为控制大脑,也就是说,允许大脑进行训练。为此,选择学院,然后在检查器窗口中找到走廊学院组件,并选择控制选项,如图所示:

将学院设置为控制大脑
-
接下来,我们需要修改环境的运行参数。这里的想法是,我们将构建Unity环境作为一个可执行的游戏,然后我们可以使用包装器来训练智能体进行游戏。但是,为了做到这一点,我们需要对游戏做一些假设:
-
游戏是无窗口的,在后台运行。
-
任何玩家动作都需要由智能体控制。对话框会提示警告、错误或其他必须避免的事情。
-
确保首先加载训练场景。
-
-
在完成那之前,请将学院的控制选项关闭或打开,然后通过界面顶部的播放按钮运行场景。您将能够观察到一个已经训练好的智能体在场景中玩耍。完成观看后,请确保将控制选项重新打开。
现在,ML-Agents工具包将允许您通过运行一个单独的Python命令行和脚本控制编辑器来直接从这里进行训练。到目前为止,这还不可能,我们运行环境的唯一方法是通过包装器。在下一节中,我们将通过设置一些最终参数并构建它们来完成环境的设置。
为Gym包装器构建
配置环境的设置只需设置一些额外的参数。我们将在接下来的练习中学习如何做到这一点:
-
从编辑菜单中选择编辑 | 项目设置...以打开项目设置窗口。您可以在编辑完成后锚定此窗口或关闭它。
-
选择玩家选项。在这里,玩家指的是玩家或游戏运行者——不要与实际的人类玩家混淆。将公司名称和产品名称字段中的文本更改为
GameAI。 -
打开分辨率和显示部分,并确保在后台运行被勾选,显示分辨率对话框设置为禁用,如下面的截图所示:

设置玩家设置
-
关闭对话框或将其锚定,然后从菜单中选择文件 | 构建设置。
-
点击添加打开场景按钮,并确保选择你的默认训练平台。这应该是一个你可以轻松用 Python 运行的桌面环境。下面的截图显示了默认的 Windows 选项:

将场景构建成游戏环境
-
点击对话框底部的构建按钮来构建可执行环境。
-
你将被提示将输出保存到文件夹中。请确保注意此文件夹的位置,并将其保存在可访问的地方。一个建议的位置是
/mlagents中的mlagents根文件夹。创建一个名为desktop的新文件夹并将输出保存在那里。
环境现在应该作为 Gym 环境构建并可运行。我们将在下一节中设置此环境并开始对其进行训练。
使用 Rainbow 训练 Unity 环境
训练一个智能体来学习 Unity 环境并不像我们已经做过的许多训练那样。我们在交互和设置环境的方式上做了一些细微的调整,但总体上还是相同的,这使我们受益匪浅,因为我们现在可以回到过去,在完全新的环境中训练几个不同的智能体/算法,甚至是我们自己设计的。此外,我们现在可以使用其他 DRL 框架用 Python 训练智能体——即除了 ML-Agents 智能体之外。我们将在第 12 章DRL 框架中详细介绍如何使用其他框架。
在下一个练习中,我们将看到如何将我们最新且最先进的样本之一Chapter_10_Rainbow.py转换为Chapter_11_Unity_Rainbow.py。打开Chapter_11_Unity_Rainbow.py并按照下一个练习进行操作:
-
我们首先需要从最后的构建中复制输出文件夹,即桌面文件夹,并将其放置在与本章源代码相同的文件夹中。这将允许我们以该构建作为智能体训练的环境。
-
由于你可能会想要将我们之前的一些样本转换为运行 Unity 环境,我们将逐步介绍所需的变化,首先从新的导入开始,如下所示:
from unityagents import UnityEnvironment
- 这导入了
UnityEnvironment类,这是一个Unity的Gym适配器。接下来,我们使用这个类来实例化env环境,如下所示;注意,我们为其他操作系统放置了注释行:
env = UnityEnvironment(file_name="desktop/gameAI.exe") # Windows
#env = UnityEnvironment(file_name="desktop/gameAI.app") # Mac
#env = UnityEnvironment(file_name="desktop/gameAI.x86") # Linux x86
#env = UnityEnvironment(file_name="desktop/gameAI.x86_64") # Linux x86_64
- 接下来,我们从环境中获取
brain和brain_name。Unity使用大脑的概念来控制智能体。我们将在后面的章节中探讨智能体大脑。现在,请记住我们只是用以下代码获取第一个可用的大脑:
brain_name = env.brain_names[0]
brain = env.brains[brain_name]
- 然后,我们从大脑中提取动作大小(
action_size)和状态大小(state_size),并使用这些作为输入来构建我们的RainbowDQN模型,如下所示:
action_size = brain.vector_action_space_size
state_size = brain.vector_observation_space_size
current_model = RainbowDQN(state_size, action_size, num_atoms, Vmin, Vmax)
target_model = RainbowDQN(state_size, action_size, num_atoms, Vmin, Vmax)
- 我们需要关注的最后一部分是在训练代码中,与环境的重置方式有关。Unity允许多个智能体/大脑在并发环境中并发运行,这可以是A2C/A3C或其他机制的方式机制。因此,我们需要更加小心地确定我们想要重置的具体大脑和模式。以下代码展示了我们在训练Unity时如何重置环境:
env_info = env.reset(train_mode=True)[brain_name]
state = env_info.vector_observations[0]
-
如前所述,这种稍微有些令人困惑的索引目的在于确定你想要从哪个大脑/智能体中提取状态。Unity可能拥有多个大脑在多个子环境中训练多个智能体,这些智能体要么协同工作,要么相互竞争。我们将在第14章中详细介绍多个智能体环境的训练,即从DRL到AGI。
-
我们还必须更改任何其他可能重置环境的情况,如下例所示,当算法检查是否完成一个回合时,使用以下代码:
if done:
#state = env.reset()
env_info = env.reset(train_mode=True)[brain_name]
state = env_info.vector_observations[0]
all_rewards.append(episode_reward)
episode_reward = 0
- 运行代码并观察智能体训练过程。除非你在另一个shell中运行TensorBoard并完成所有步骤,否则你将看不到除了TensorBoard输出之外的任何视觉内容,现在你很可能可以自己做到这一点。
这个例子可能由于API兼容性问题而难以运行。如果你在运行示例时遇到问题,那么尝试设置一个全新的虚拟环境并重新安装所有内容。如果问题仍然存在,那么在网上寻找帮助,例如在Stack Overflow或GitHub上。务必参考最新的Unity文档关于ML-Agents的部分。
将Unity连接并使用它的真正好处是能够构建自己的环境,然后使用这些新环境与自己的或另一个RL框架一起使用。在下一节中,我们将探讨使用Unity ML-Agents构建自己的RL环境的基本知识。
创建一个新环境
ML-Agents工具包的伟大之处在于它提供了快速简单地创建新智能体环境的能力。你甚至可以将现有的游戏或游戏项目转换成用于各种目的的训练环境,从构建完整的机器人模拟到简单的游戏智能体,甚至扮演非玩家角色的游戏智能体。甚至有潜力使用深度强化学习智能体进行游戏质量保证测试。想象一下,建立一个游戏测试员团队,他们只通过试错来学习玩你的游戏。可能性是无限的,Unity甚至正在构建一个完整的基于云的模拟环境,用于未来运行或训练这些智能体。
在本节中,我们将通过使用游戏项目作为新的训练环境来演示。在设置自己的Python代码之前,你最好使用ML-Agents工具包测试你在Unity中创建的任何环境。深度强化学习智能体是发现错误和/或作弊的大师。因此,你几乎总是希望在用你的代码训练环境之前,先使用ML-Agents测试环境。我已经推荐你通过设置和运行ML-Agents Python代码来训练智能体的过程。记住,一旦你导出环境用于Gym训练,它就变成了无窗口的,你将无法了解智能体在环境中的训练或表现情况。如果有任何作弊或错误需要被发现,智能体几乎肯定能找到。毕竟,你的智能体将尝试数百万种不同的试错组合,试图找到如何玩游戏的方法。
我们将查看基本ML-Agents环境,以了解如何构建我们自己的扩展或新环境。如果这里的信息不足,ML-Agents文档是一个很好的备选资源。这个练习的目的是让你快速掌握构建自己的环境:
-
打开Unity编辑器到我们之前打开的UnitySDK ML-Agents项目。在
Assets/ML-Agents/Examples/Basic/Scenes中找到并打开(双击)基本场景。 -
任何环境的中心都是学院。在层次结构窗口中找到并选择学院对象,然后在检查器窗口中查看属性,如图所示:

检查学院
- 在基本学院 | 广播中心 | 大脑条目中点击并选择基本学习(学习大脑)大脑。这将突出显示项目窗口中的条目。在项目窗口中选择基本学习大脑,并在检查器窗口中查看大脑设置,如图所示:

检查学习大脑
-
在这里我们可以看到关于大脑的一些信息。大脑控制智能体,因此大脑的观察和动作空间实际上与智能体的相同。在检查器窗口中,你可以看到有20个向量观察和一个包含三个离散动作的动作空间。对于这个环境,动作是左移或右移以及空操作。0动作变为空操作或暂停操作。
-
接下来,我们想要检查智能体本身。在层次结构窗口中点击并展开基本对象。选择BasicAgent对象,然后根据截图查看检查器窗口:

检查基本智能体
-
检查基本智能体组件,你可以看到大脑设置为BasicLearning大脑,并且这里还显示了其他属性。注意重置完成和按需决策都被勾选了。重置完成使环境在完成一个回合后自动重置——这是你期望的默认行为,但实际上并非如此。按需决策等同于使用在线策略模型和离线策略模型,在用ML-Agents工具包训练时更为相关。
-
按下播放键将显示智能体在玩游戏。观察智能体的游戏过程,当智能体移动时,确保在编辑器中选择并检查对象。Unity非常适合查看你的游戏机制如何工作,这在构建自己的智能体环境时尤其有用。
在构建任何新的环境时,你需要考虑的主要元素是学院、大脑和智能体。只要遵循这个基本示例,你应该能够快速构建一个简单的可工作环境。构建自己的环境时另一个棘手的部分是你可能需要做的特殊编码,我们将在下一节中介绍。
编写智能体/环境代码
Unity提供了一个出色的界面用于原型设计和构建商业游戏。实际上,你可以用很少的代码走得很远。不幸的是,目前构建新的ML-Agents环境并不是这样。
因此,我们将在下一项练习中探索重要的编码部分:
-
接下来,定位并打开位于
Assets/ML-Agents/Examples/Basic下的脚本文件夹,并在其中双击打开BasicAgent.cs。这是一个C#(CSharp)文件,它将在默认编辑器中打开。 -
在文件顶部,你会注意到这个
BasicAgent类是从Agent扩展的,而不是Unity默认的MonoBehaviour。Agent是Unity中的一个特殊类,正如你可能猜到的,它定义了一个能够探索环境的智能体。然而,在这种情况下,智能体更多地指的是异步或同步actor-critic中的工作者。这意味着单个大脑可以控制多个智能体,这通常是这种情况:
using UnityEngine;
using MLAgents;
public class BasicAgent : Agent
- 跳过字段,我们跳到以
CollectObservations开始的方法定义,如下所示:
public override void CollectObservations()
{
AddVectorObs(m_Position, 20);
}
- 在这个方法内部,我们可以看到代理/大脑如何从环境中收集观察结果。在这种情况下,观察是通过
AddVectorObs添加的,它将观察结果添加为所需大小的 one-hot 编码向量。在这种情况下,向量大小是 20,与大脑的状态大小相同。
One-hot 编码是一种方法,通过在向量内部使用二进制值来编码类信息。因此,如果一个表示类别或位置 1 的一热编码向量是激活的,它将被写成 [0,1,0,0]。
- 主要的动作方法是
AgentAction方法。这是代理在环境中执行动作的地方,无论是移动还是其他动作:
public override void AgentAction(float[] vectorAction, string textAction)
{
var movement = (int)vectorAction[0];
var direction = 0;
switch (movement)
{
case 1:
direction = -1;
break;
case 2:
direction = 1;
break;
}
m_Position += direction;
if (m_Position < m_MinPosition) { m_Position = m_MinPosition; }
if (m_Position > m_MaxPosition) { m_Position = m_MaxPosition; }
gameObject.transform.position = new Vector3(m_Position - 10f, 0f, 0f);
AddReward(-0.01f);
if (m_Position == m_SmallGoalPosition)
{
Done();
AddReward(0.1f);
}
if (m_Position == m_LargeGoalPosition)
{
Done();
AddReward(1f);
}
}
- 这段代码的第一部分只是根据代理所采取的动作确定其移动方式。您可以看到代码如何根据移动调整代理的位置。然后,我们看到以下代码行:
AddReward(-0.01f);
-
这行代码添加了一个步骤奖励,意味着它会在每一步都添加这个奖励。它这样做是为了限制代理的移动。因此,代理做出错误决策所需的时间越长,奖励就越少。我们有时会使用步骤奖励,但它也可能有负面影响,并且通常有理由完全消除步骤奖励。
-
在
AgentAction方法的底部,我们可以看到当代理达到小目标或大目标时会发生什么。如果代理达到大目标,它会获得 1 的奖励,如果达到小目标,它会获得 0.1 的奖励。有了这些,我们还可以看到,当它达到目标时,通过调用Done()来终止游戏:
Done();
AddReward(0.1f); //small goal
// or
Done();
AddReward(1f); //large goal
- 将奖励的数字反转,保存代码,然后返回编辑器。将 Academy 设置为控制大脑,然后使用 ML-Agents 或我们之前开发的代码训练代理。您应该非常清楚地看到代理对较小目标的偏好。
将这些概念扩展并构建您自己的环境现在将取决于您。天空真的是无极限,Unity 提供了几个出色的示例供您学习和使用。在下一节中,我们将有机会查看 ML-Agents 提供的作为增强您的代理或探索新的训练方式的机制。
使用 ML-Agents 推进强化学习
ML-Agents 工具包,允许您训练深度强化学习(DRL)代理的部分,被认为是训练代理中较为严肃和高端的框架之一。由于该框架是在 Unity 上开发的,因此在类似 Unity 的环境中表现通常更好。然而,与许多花费时间训练代理的人一样,Unity 开发者早期就意识到,某些环境提出了如此困难的挑战,以至于我们需要协助我们的代理。
现在,这种辅助作用不是那么直接,而是间接的,并且通常直接关系到代理找到奖励的难易程度。这反过来又直接关系到环境设计师能否构建一个代理可以用来学习环境的奖励函数。也有时候,环境的状态空间如此之大且不明显,以至于创建一个典型的奖励函数根本不可能。考虑到所有这些,Unity 已经竭尽全力通过以下新的学习形式来增强 ML-Agents 内的强化学习:
-
课程学习
-
行为克隆(模仿学习)
-
好奇心学习
-
训练通用强化学习代理
我们将通过使用 Unity 环境的快速示例来介绍每种学习形式。
课程学习
课程学习允许你通过随着代理的学习增加任务的复杂性来训练代理。这相当直观,可能非常类似于我们从数学到编程学习各种任务的方式。
按照练习快速了解如何为课程学习进行设置:
-
打开位于
Assets/ML-Agents/Examples/WallJump/Scenes文件夹中的WallJump场景。 -
在场景中选择 Academy,并在 Inspector 窗口中查看 Wall Jump Academy 组件的设置,如图所示:

检查 WallJump 学院
-
在学院内部有一个扩展的部分,称为 Reset Parameters。这些参数代表我们想要让代理经历的各个训练状态的训练级别参数。
-
这些参数现在需要在配置文件中进行配置,ML-Agents 工具包将使用该配置文件来训练使用课程的代理。该文件的 内容可以在
config/curricula/wall-jump/中找到或创建,并包括以下内容:
{
"measure" : "progress",
"thresholds" : [0.1, 0.3, 0.5],
"min_lesson_length" : 100,
"signal_smoothing" : true,
"parameters" :
{
"big_wall_min_height" : [0.0, 4.0, 6.0, 8.0],
"big_wall_max_height" : [4.0, 7.0, 8.0, 8.0]
}
}
-
通过参考 ML-Agents 文档可以最好地理解这些参数。基本上,这里的想法是这些参数控制着随时间增加的墙壁高度。因此,代理需要学会移动方块跳过墙壁,随着难度越来越大。
-
在 Academy 脑上设置 Control 标志,然后在 Python shell 中运行以下命令以启动 ML-Agents 会话:
mlagents-learn config/trainer_config.yaml --curriculum=config/curricula/wall-jump/ --run-id=wall-jump-curriculum --train
- 假设配置文件位于正确的位置,你将被提示运行编辑器并观察代理在环境中的训练。此示例的结果如下所示:

课程训练示例的输出
课程学习以新颖的方式解决了环境在没有明显答案的问题。在这种情况下,智能体的目标是使目标方块。然而,如果墙壁一开始就很高,智能体需要移动方块到那里以跳过它,它可能甚至不会理解它需要到达方块。因此,我们通过首先允许它到达目标,然后逐渐使其更难做到这一点来帮助它进行训练。随着难度的增加,智能体学会如何使用方块跳过墙壁。
在下一节中,我们将探讨另一种帮助智能体解决具有难以找到的或我们称之为稀疏奖励的任务的方法。
行为克隆
行为克隆有时也被称为模仿学习。虽然这两个术语并不完全相同,但在这里我们将交替使用这两个术语。在强化学习(RL)中,我们使用“稀疏奖励”或“奖励稀疏性”这个术语来描述任何对智能体来说仅通过试错和可能的好运很难完成任务的环境。环境越大,奖励越稀疏,在许多情况下,观察空间可能如此之大,以至于训练智能体的任何希望都极其困难。幸运的是,一种称为行为克隆或模仿学习的方法可以通过使用人类的观察作为先前采样的观察来解决稀疏奖励的问题。Unity 提供了三种生成先前观察的方法,如下所示:
-
生成对抗模仿学习(GAIL):你可以使用称为 GAIL 奖励信号的东西来增强从少量观察中学习到的奖励。
-
预训练:这允许你使用预先录制的人类演示,并利用这些演示来启动智能体的学习。如果你使用预训练,你还需要在你的 ML-Agents 配置文件中提供一个配置部分,如下所示:
pretraining:
demo_path: ./demos/Tennis.demo
strength: 0.5
steps: 10000
- 行为克隆(BC):在这个训练中,设置直接在 Unity 编辑器中进行。这对于小演示可以帮助智能体增加学习的环境来说非常好。BC 在具有大观察状态空间的大型环境中效果不佳。
这三种方法可以以各种配置组合在一起,并在预训练和 GAIL 与其他方法(如好奇心学习,我们将在后面看到)一起使用的情况下一起使用。
使用 BC 在实时中训练智能体特别有趣,正如我们将在下一个练习中看到的那样。遵循下一个练习来探索使用 BC 方法向智能体演示:
-
打开位于
Assets/ML-Agents/Examples/Tennis/Scenes文件夹中的 TennisIL 场景。 -
这个环境是一个稀疏奖励环境的例子,其中智能体需要找到并击打球回对手。这个环境是测试 BC 的绝佳例子。
-
在层次结构窗口中选择Academy对象,然后在检查器窗口中检查TennisLearning (LearningBrain)的控制选项,如图中所示:

打开学习大脑以进行控制
-
如您所见,在这个场景中有两个大脑:一个学生大脑——学习大脑,和一个教师大脑——玩家大脑。教师大脑由人类玩家控制,不会控制实际的代理,而是直接从玩家那里获取输入。学生大脑观察教师的行为,并使用这些行为作为其策略的样本。从基本意义上讲,这变成了教师根据目标策略,即代理需要学习的策略,从人类策略中工作。这实际上与我们拥有当前网络和目标网络并没有太大的区别。
-
我们接下来要做的事情是自定义ML-Agents的超参数配置文件。我们通过为
StudentBrain添加以下条目来自定义文件:
StudentBrain:
trainer: imitation
max_steps: 10000
summary_freq: 1000
brain_to_imitate: TeacherBrain
batch_size: 16
batches_per_epoch: 5
num_layers: 4
hidden_units: 64
use_recurrent: false
sequence_length: 16
buffer_size: 128
-
在前面的配置中,突出显示的元素显示了
trainer:设置为imitation和brain_to_imitate:为TeacherBrain。有关设置ML-Agents配置的更多信息,可以在在线文档中找到。 -
接下来,您需要打开Python/Anaconda shell并切换到
mlagents文件夹。之后,运行以下命令以开始训练:
mlagents-learn config/trainer_config.yaml --run-id=tennis1 --train
-
这将启动训练器,不久之后您将被提示以播放模式启动Unity编辑器。
-
按播放将编辑器置于播放模式,并使用WASD控制来操纵球拍与代理进行网球比赛。假设您做得很好,代理也会提高。这次训练的截图如下所示:

使用BC训练网球代理
模仿学习是训练代理AlphaStar的关键因素。AlphaStar被证明在一种非常复杂的实时策略游戏星际争霸2中击败了人类玩家。它在帮助代理克服稀疏奖励问题方面有许多显著优势。然而,在强化学习(RL)社区中,许多人希望避免模仿学习(IL)或行为克隆(BC),因为这可能会引入人类偏见。研究表明,与完全未经BC训练的代理相比,人类偏见会降低代理的性能。实际上,AlphaStar在开始自我训练之前已经达到了足够的可玩性水平。正是这种自我训练被认为使其能够击败人类玩家。
在下一节中,我们将探讨Unity尝试捕捉解决稀疏奖励问题的一种另类激动人心的方法。
好奇心学习
到目前为止,我们只考虑了环境给予智能体的外部奖励。然而,我们和其他动物会接收到各种各样的外部和内部奖励。内部奖励通常由情绪或感觉来表征。智能体可能有一种内部奖励,每次它看向某个面孔时都会得到 +1,这或许代表了一些内在的爱或迷恋奖励。这类奖励被称为内在奖励,它们代表了智能体内部或自我产生的奖励。这为从创建有趣的动机智能体到增强智能体的学习能力等方面提供了强大的能力。
这是 Unity 引入好奇心学习或内部好奇心奖励系统的第二种方式,作为一种让智能体在感到惊讶时探索更多的方式。也就是说,每当智能体对某个动作感到惊讶时,它的好奇心就会增加,因此它需要探索使其感到惊讶的状态动作空间。
Unity 在一个名为 Pyramids 的环境中产生了一个非常强大的好奇心学习示例。在这个环境中,智能体的目标是找到一堆带有金色方块在顶部的黄色方块。推倒方块堆,然后获取金色方块。问题是,一开始在许多房间里都有盒子堆,但没有一个是黄色的。要使方块变黄,智能体需要找到并按下按钮。使用直接强化学习(RL)找到这个任务序列可能会出现问题,或者会耗费时间。幸运的是,有了 CL,我们可以显著提高这种性能。我们将在下一节中探讨如何使用 CL 来训练 Pyramids 环境:
-
打开位于
Assets/ML-Agents/Examples/Pyramids/Scenes文件夹中的 Pyramids 场景。 -
按下播放按钮运行默认智能体;这将是一个使用 Unity 训练的智能体。当你运行智能体时,观察它如何在环境中玩耍,你会看到智能体首先找到按钮,按下它,然后定位到它需要推倒的方块堆。它将像这里截图序列中所示那样推倒盒子:

Pyramids 智能体在玩环境
- 仅需将学院设置为控制大脑,并使用适当的配置运行 ML-Agents 训练器,就可以训练一个好奇心的智能体。这份关于驱动 CL 的文档在 ML-Agents 开发过程中已经更改了好几次。因此,建议您查阅 ML-Agents 文档以获取最新的文档信息。
CL 可以非常强大,内在奖励的整个概念在游戏中有一些有趣和有趣的应用。想象一下,能够为可能表现出贪婪、权力或其他邪恶特质的敌方智能体提供内部奖励系统。在下一节中,我们将通过探讨训练通用强化学习智能体来结束这一章。
训练通用强化学习智能体
我们经常需要提醒自己,强化学习只是数据科学最佳实践的衍生,我们经常需要考虑如何使用数据科学来修复训练问题。在强化学习的案例中,我们看到与数据科学和机器学习相同的问题,只是在不同的规模和不同的方式下暴露出来。一个例子是,当代理过度拟合到我们试图应用于该环境其他一般变体的环境中时。例如,想象一下可能具有不同大小或甚至提供随机起点或其他变体的 Frozen Lake 环境。通过引入这些类型的变体,我们允许我们的代理更好地泛化到更广泛的类似环境中。我们希望将这种泛化引入到我们的环境中。
AGI 或 通用人工智能 是将通用训练代理扩展到 n 次方的概念。预期一个真正的 AGI 代理能够被放置在任何环境中并学会解决任务。这可能需要一定量的训练,但理想情况下,不应需要其他超参数或其他人为干预。
通过使环境具有随机性,我们实际上增加了我们使用分布式强化学习和有噪声网络的方法的效力。不幸的是,启用这些类型的参数与其他训练代码或我们的 PyTorch 代码目前不可用。在下一个练习中,我们将学习如何设置一个通用的训练环境:
-
打开位于
Assets/ML-Agents/Examples/WallJump/Scenes文件夹中的 WallJump 场景。 -
WallJump 已经设置并配置了我们在审查课程学习时看到的几个重置参数。这次,我们不是逐步改变这些参数,而是让环境随机采样它们。
-
我们想要重新采样的参数基于这个样本。我们可以在配置文件夹中创建一个新的通用 YAML 文件,命名为
walljump_generalize.yaml。 -
打开文件,将以下文本放入其中,然后保存:
resampling-interval: 5000
big_wall_min_height:
sampler-type: "uniform"
min_value: 5
max_value: 8
big_wall_max_height:
sampler-type: "uniform"
min_value: 8
max_value: 10
small_wall_height:
sampler-type: "uniform"
min_value: 2
max_value: 5
no_wall_height:
sampler-type: "uniform"
min_value: 0
max_value: 3
- 这设置了我们将如何采样值的采样分布。然后,可以使用以下代码对环境的值进行采样:
SamplerFactory.register_sampler(*custom_sampler_string_key*, *custom_sampler_object*)
- 我们还可以定义新的采样器类型或使用自定义采样器以自定义方式采样数据值,该采样器由我们在 ML-Agents 代码中的
sample_class.py文件中的类放置。以下是一个自定义采样器的示例:
class CustomSampler(Sampler):
def __init__(self, argA, argB, argC):
self.possible_vals = [argA, argB, argC]
def sample_all(self):
return np.random.choice(self.possible_vals)
- 然后,您可以配置配置文件以运行此采样器,如下所示:
height:
sampler-type: "custom-sampler"
argB: 1
argA: 2
argC: 3
-
请记住,当代理重置时,您仍然需要采样值并修改环境的配置。这需要修改代码以使用适当的采样器采样输入。
-
您可以使用以下命令运行 Unity ML-Agents 训练器代码:
mlagents-learn config/trainer_config.yaml --sampler=config/walljump_generalize.yaml
--run-id=walljump-generalization --train
能够以这种方式训练代理可以使您的代理更加健壮,能够应对各种环境形态。如果您正在构建需要实用代理的游戏,您很可能会需要以通用方式训练您的代理。通用代理通常能够更好地适应环境中不可预见的变化,比其他方式训练的代理要好得多。
这就是本章的内容,在下一节中,我们将探讨如何通过本章的示例练习获得更多经验。
练习
本节中的练习旨在更详细地向您介绍Unity ML-Agents。如果您不打算使用ML-Agents作为训练框架,那么请继续下一节和本章的结尾。对于那些仍然在这里的人,ML-Agents本身是一个强大的工具包,可以快速探索DRL代理。该工具包隐藏了DRL的大部分细节,但您现在应该能够自己找出这些细节:
-
在编辑器中设置并运行Unity ML-Agents的一个示例环境来训练代理。这需要您查阅Unity ML-Agents文档。
-
调整示例Unity环境的超参数。
-
启动TensorBoard并运行它,以便从Unity运行文件夹中收集日志。这将允许您查看使用ML-Agents训练的代理的训练性能。
-
使用彩虹DQN示例构建Unity环境并进行训练。
-
通过更改设置、参数、重置参数和/或奖励函数来自定义现有的Unity环境。也就是说,改变代理在完成动作或任务时收到的奖励反馈。
-
使用预训练数据设置和训练代理。这需要您设置一个玩家大脑来记录演示。玩游戏以记录这些演示,然后设置游戏以学习大脑进行训练。
-
使用网球环境通过行为克隆训练代理。
-
使用金字塔场景通过好奇心学习训练代理。
-
设置并运行一个用于通用训练的Unity环境。使用采样从分布中提取环境的随机值。不同的分布对代理的训练性能有什么影响?
-
将PG方法示例(如PPO)转换为可以在Unity环境中运行。其性能与彩虹DQN相比如何?
使用这些示例熟悉Unity ML-Agents和RL中的更高级概念。在下一节中,我们将总结并完成本章。
摘要
在本章中,我们偏离了主线,构建了我们自己的DRL环境,用于使用自己的代码、另一个框架或使用Unity的ML-Agents框架进行训练。起初,我们探讨了安装ML-Agents工具包的基本知识,用于开发环境、训练以及使用自己的代码进行训练。然后,我们了解了如何从Gym界面构建一个基本的Unity环境,就像我们在整本书中一直做的那样。之后,我们学习了如何定制我们的RainbowDQN示例以训练一个智能体。从那里,我们探讨了如何从基础创建一个全新的环境。我们通过查看如何在环境中管理奖励以及ML-Agents用于增强具有稀疏奖励的环境的工具集来结束本章。在那里,我们探讨了Unity为ML-Agents添加的几种方法,以帮助处理困难的环境和稀疏奖励。
从本章继续前进,我们将继续探索其他可用于训练智能体的DRL框架。ML-Agents是许多强大的框架之一,可用于训练智能体,正如我们很快将看到的。
第十四章:DRL 框架
在本书中通过和探索代码旨在成为学习如何强化学习(RL)算法工作的练习,同时也了解让这些算法工作起来的难度。正是因为这种难度,每天都有许多开源的RL框架出现。在本章中,我们将探讨几个更受欢迎的框架。我们将从为什么您想要使用一个框架开始,然后转向探索更受欢迎的框架,如Dopamine、Keras-RL、TF-Agents和RL Lib。
下面是本章我们将涵盖的主要主题的简要总结:
-
选择一个框架
-
介绍 Google Dopamine
-
玩转 Keras-RL
-
探索 RL Lib
-
使用 TF agents
我们将结合使用Google Colab上的笔记本环境和虚拟环境,这取决于本章示例的复杂性。Jupyter Notebooks,Colab的基础,是展示代码的绝佳方式。它通常不是开发代码的首选方式,这也是我们为什么直到现在才避免使用这种方法的原因。
在下一节中,我们将探讨为什么您会选择一个框架。
选择一个框架
如您现在可能已经推测到的,在深度学习框架(如PyTorch)之上编写自己的RL算法和函数并非易事。还重要的是要记住,本书中的算法回顾了RL发展的约30年。这意味着任何RL的重大新进展都需要大量的努力和时间——是的,包括开发和特别是训练。除非您有开发自己框架的时间、资源和动力,那么强烈建议您使用成熟的框架。然而,新的和可比较的框架数量正在不断增加,因此您可能会发现您无法只选择一个。直到这些框架实现真正的AGI,您可能还需要为不同的环境或甚至不同的任务使用不同的框架。
记住,AGI代表人工通用智能,这确实是任何RL框架的目标。一个AGI框架可以在任何环境中进行训练。一个高级AGI框架可能能够跨任务进行迁移学习。迁移学习是指一个智能体可以学习一个任务,然后利用这些学习来完成另一个类似的任务。
我们将在后面的章节中查看目前最受欢迎且最有潜力的框架。比较各种当前框架,以确定其中一个可能更适合您和您的团队,这一点很重要。因此,我们将查看以下列表中目前可用的各种RL框架的比较。
此列表按当前流行度(由Google)排序,列出当前最受欢迎的框架,但预计随着时间的推移,此列表将发生变化:
-
OpenAI Gym 和 Baselines:OpenAI Gym 是我们在本书中探索的大多数环境所使用的框架。这个库还有一个配套的库叫做 Baselines,它为 Gym 环境提供了几个代理,正如你所猜测的,用于基准测试 Gym 环境。Baselines 也是一个非常受欢迎且优秀的强化学习框架,但在这里我们为了查看其他库而省略了它。
-
Google Dopamine:这是一个相对较新的框架,迅速获得了人气。这可能是部分原因在于其实施了 RainbowDQN 代理。这个框架已经得到了很好的发展,但被描述为笨拙且不太模块化。我们在这里展示它,因为它很受欢迎,你很可能仍然想更仔细地看看它。
-
ML-Agents:我们已经在某种程度上覆盖了整个关于这个框架的章节,所以在这里我们不需要进一步探索。Unity 开发了一个非常稳固但不太模块化的框架。当前的实现仅支持 PG 方法,如 PPO 和 Soft Actor-Critic。然而,ML-Agents 本身可以是一个展示强化学习给开发团队或向客户介绍概念的极好且推荐的方式。
-
RL Lib 与 ray-project:这个项目的起源很奇特,它最初是一个 Python 的并行化项目,后来演变成一个强化学习的训练平台。因此,它倾向于使用异步代理(如 A3C)的训练制度,非常适合复杂的环境。不仅如此,这个项目基于 PyTorch,所以它值得一看。
-
Keras-RL:Keras 本身也是一个非常流行的深度学习框架。这个深度学习库本身相当简洁且易于使用——也许在某些方面,过于简单。然而,它可以用作原型化强化学习概念或环境的绝佳方式,值得我们进一步关注。
-
TRFL:这个库与 Keras-RL 类似,是 TensorFlow 框架的扩展,以纳入强化学习。TensorFlow 是另一个低级深度学习框架。因此,构建任何有效代理的代码也需要相当低级,使用这个库可能不适合你,尤其是如果你喜欢 PyTorch。
-
Tensorforce:这是一个专注于扩展 TensorFlow 以用于强化学习的库。使用基于 TF 的解决方案的好处是跨兼容性,甚至可以将你的代码移植到网页或移动设备。然而,构建低级计算图并不适合每个人,并且确实需要比本书中涵盖的更高水平的数学知识。
-
Horizon:这个框架来自 Facebook,是在 PyTorch 上开发的。不幸的是,这个框架在多个领域中的优势不足,包括没有
pip安装程序。它还缺乏与 Gym 环境的紧密集成,所以除非你在 Facebook 工作,否则你可能会想避免使用这个框架。 -
Coach:这是那种可能有一天会建立自己大量追随者的隐藏框架之一。Coach有很多有用和强大的功能,包括专门的仪表板和直接支持Kubernetes。这个框架目前也拥有最大的RL算法实现,将为你提供大量的探索空间。Coach是一个值得你自己探索的框架。
-
MAgent:这个项目与RLLib(Ray)类似,因为它专门用于异步或各种配置下训练多个代理。它是基于TensorFlow开发的,并使用自己设计的网格世界环境进行所谓的现实生活模拟。这是一个非常专业的框架,适用于开发者或现实生活中的RL解决方案。
-
TF-Agents:这是谷歌在TensorFlow之上开发的另一个RL实现。因此,它是一个更底层的框架,但比这里提到的其他TF框架更稳健和强大。这个框架似乎是一个更严肃的研究和/或生产实现的强劲竞争者,值得那些想要进行此类工作的读者进一步关注。
-
SLM-Lab:这是另一个基于PyTorch的框架,实际上是基于Ray(RLLib)之上的,尽管它更多的是为纯研究而设计。因此,它没有
pip安装程序,并假设用户直接从存储库中拉取代码。现在最好把这个框架留给研究人员。 -
DeeR:这是另一个与Keras集成的库,旨在更加易于使用。这个库维护得很好,文档也很清晰。然而,这个框架是为那些学习RL的人设计的,如果你已经走到这一步,你可能已经需要更高级和更稳健的东西了。
-
车库(Garage):这是另一个基于TF的框架,它具有一些出色的功能,但缺乏文档和良好的安装流程,这使得它又是一个很好的研究框架,但对于那些对开发工作代理感兴趣的人来说可能更好避免。
-
Surreal:这个框架更多的是为机器人应用而设计的,因此它更加封闭。使用Mujoco等环境进行机器人RL已被证明具有商业可行性。因此,这个RL分支正在看到那些试图分得一杯羹的人的影响。这意味着这个框架目前是免费的,但不是开源的,而且免费的部分可能很快就会改变。尽管如此,如果你专注于机器人应用,这可能值得认真考虑。
-
RLgraph:这可能是一个值得关注的潜在项目。这个库目前正在吸收大量的提交,并且变化很快。它也使用PyTorch和TensorFlow映射构建。我们将在后面的部分花时间探讨如何使用这个框架。
-
简单RL:这可能是一个RL框架所能达到的最简单形式。该项目旨在非常易于访问,并且可以在少于八行代码的情况下开发出具有多个代理的示例。实际上,它可能就像以下从示例文档中摘取的代码块那样简单:
from simple_rl.agents import QLearningAgent, RandomAgent, RMaxAgent
from simple_rl.tasks import GridWorldMDP
from simple_rl.run_experiments import run_agents_on_mdp
# Setup MDP.
mdp = GridWorldMDP(width=4, height=3, init_loc=(1, 1), goal_locs=[(4, 3)], lava_locs=[(4, 2)], gamma=0.95, walls=[(2, 2)], slip_prob=0.05)
# Setup Agents.
ql_agent = QLearningAgent(actions=mdp.get_actions())
rmax_agent = RMaxAgent(actions=mdp.get_actions())
rand_agent = RandomAgent(actions=mdp.get_actions())
# Run experiment and make plot.
run_agents_on_mdp([ql_agent, rmax_agent, rand_agent], mdp, instances=5, episodes=50, steps=10)
由于有这么多框架可供选择,我们只有时间在本章中概述最受欢迎的框架。虽然框架因为编写得好并且倾向于在各种环境中表现良好而变得流行,但直到我们达到通用人工智能(AGI),你可能仍然需要探索各种框架,以找到适合你和你问题的算法/代理。
为了了解这种演变,我们可以使用谷歌趋势进行搜索比较分析。这样做通常可以给我们一个特定框架在搜索词中的流行趋势的指示。更多的搜索词意味着对框架的兴趣更大,这反过来又导致更多的开发和更好的软件。
下面的谷歌趋势图显示了前五个列表框架的比较:

谷歌趋势对比RL框架
你可以在图中看到RL库和谷歌多巴胺的趋势增长。值得注意的是,对RL开发的主要兴趣目前在美国和日本最为浓厚,日本对ML-Agents特别感兴趣。
ML-Agents的流行归因于几个因素,其中之一是Unity公司AI和ML副总裁,丹尼·兰格博士。兰格博士在日本居住了几年,日语流利,这很可能促成了这种特定的流行。
值得注意的是,中国在这一领域缺席,至少对于这些类型的框架来说是这样。中国对强化学习的兴趣目前非常具体,专注于由强化学习代理击败围棋游戏而流行起来的规划应用。那个强化学习代理是使用蒙特卡洛树搜索算法开发的,该算法旨在对复杂但有限的状态空间进行全面探索。我们开始研究有限状态空间,但转向探索连续或无限状态空间。这类代理也很好地过渡到通用游戏和机器人技术,而这并不是中国的主要兴趣。因此,我们还需要看看中国在这个领域将如何或表现出什么兴趣,但一旦发生,这很可能会影响这个领域。
在下一节中,我们将探讨我们的第一个框架,可能也是我们最熟悉的框架,谷歌多巴胺。
介绍谷歌多巴胺
多巴胺是在谷歌开发的,作为一个平台来展示公司在深度强化学习方面的最新进展。当然,谷歌还有其他团队在做同样的事情,这或许是对这些平台仍然多样化的证明,以及它们需要进一步多样化的证明。在下一个练习中,我们将使用谷歌Colab构建一个示例,使用云上的多巴胺训练一个代理。
要访问 Colab 上的所有功能,您可能需要创建一个已授权支付的 Google 账户。这可能意味着输入一张信用卡或借记卡。好处是 Google 为 GCP 平台提供了 300 美元的信用额度,而 Colab 只是其中的一小部分。
打开您的浏览器到 colab.research.google.com 并跟随下一个练习:
- 我们将首先创建一个新的 Python 3 笔记本。请确保通过提示对话框或通过 Colab 文件 菜单选择此选项。
这个笔记本是基于 Dopamine 作者的一个变体,原始版本可以在以下链接中找到:https://github.com/google/dopamine/blob/master/dopamine/colab/agents.ipynb。
- 我们首先需要安装几个支持训练的包。在 Colab 笔记本中,我们可以通过在命令前加上
!来将任何命令传递给底层的 shell。在一个单元格中输入以下代码,然后运行该单元格:
!pip install --upgrade --no-cache-dir dopamine-rl
!pip install cmake
!pip install atari_py
!pip install gin-config
- 然后,我们在新的单元格中进行一些导入并设置一些全局字符串:
import numpy as np
import os
from dopamine.agents.dqn import dqn_agent
from dopamine.discrete_domains import run_experiment
from dopamine.colab import utils as colab_utils
from absl import flags
import gin.tf
BASE_PATH = '/tmp/colab_dope_run' # @param
GAME = 'Asterix' # @param
@param函数表示该值为参数,并在界面提供了一个有用的文本框,以便稍后更改此参数。这是一个很酷的笔记本功能:
!gsutil -q -m cp -R gs://download-dopamine-rl/preprocessed-benchmarks/* /content/
experimental_data = colab_utils.load_baselines('/content')
- 然后,我们在另一个新的单元格中运行前面的命令和代码。这将加载我们将用于在代理上运行的数据:
LOG_PATH = os.path.join(BASE_PATH, 'random_dqn', GAME)
class MyRandomDQNAgent(dqn_agent.DQNAgent):
def __init__(self, sess, num_actions):
"""This maintains all the DQN default argument values."""
super(MyRandomDQNAgent, self).__init__(sess, num_actions)
def step(self, reward, observation):
"""Calls the step function of the parent class, but returns a random action.
"""
_ = super(MyRandomDQNAgent, self).step(reward, observation)
return np.random.randint(self.num_actions)
def create_random_dqn_agent(sess, environment, summary_writer=None):
"""The Runner class will expect a function of this type to create an agent."""
return MyRandomDQNAgent(sess, num_actions=environment.action_space.n)
random_dqn_config = """
import dopamine.discrete_domains.atari_lib
import dopamine.discrete_domains.run_experiment
atari_lib.create_atari_environment.game_name = '{}'
atari_lib.create_atari_environment.sticky_actions = True
run_experiment.Runner.num_iterations = 200
run_experiment.Runner.training_steps = 10
run_experiment.Runner.max_steps_per_episode = 100
""".format(GAME)
gin.parse_config(random_dqn_config, skip_unknown=False)
random_dqn_runner = run_experiment.TrainRunner(LOG_PATH, create_random_dqn_agent)
-
创建一个新的单元格并输入前面的代码并运行它。这将创建一个用于在环境中进行盲探索的随机 DQN 代理。
-
接下来,我们想要通过创建一个新的单元格并输入以下代码来训练代理:
print('Will train agent, please be patient, may be a while...')
random_dqn_runner.run_experiment()
print('Done training!')
- 这可能需要一段时间,所以如果您已经启用了支付授权,您可以通过更改笔记本类型来在 GPU 实例上运行此示例。您可以通过选择 Runtime | Change runtime type 菜单来完成此操作。将弹出一个对话框;更改运行时类型并关闭对话框,如图所示:

在 Colab 上更改运行时类型
- 在更改运行时类型后,您需要再次运行整个笔记本。为此,请从菜单中选择 Runtime | Run all 以再次运行所有单元格。您仍然需要等待一段时间才能完成训练;毕竟,这是在运行 Atari 环境,但这正是目的所在。
我们刚刚构建的代理正在使用运行在经典 Atari 游戏 Asterix 上的随机 DQN 代理。Dopamine 是一个功能强大且易于使用的框架,正如我们刚刚看到的。您可以从源本身找到有关库的更多信息,包括如何输出最后一个示例练习的结果。
在下一节中,我们将离开 Colab 并探索另一个框架,即使用常规 Python 的 Keras-RL。
玩转 Keras-RL
Keras 是一个非常流行的深度学习框架,它本身就被那些想要学习构建网络基础知识的新手大量使用。该框架被认为是高度高级和抽象的,它抽象了构建网络的大部分内部细节。因此,可以假设使用 Keras 构建的强化学习框架会尝试做同样的事情。
此示例依赖于 Keras 和 TensorFlow 的版本,并且如果这两个版本不能协同工作,可能无法正确运行。如果您遇到问题,请尝试安装 TensorFlow 的不同版本,然后再次尝试。
要运行这个示例,我们首先将在本练习中进行安装和所有设置:
- 要安装 Keras,您应该使用 Python 3.6 创建一个新的虚拟环境,并使用
pip安装它以及keras-rl框架。在 Anaconda 上执行所有这些命令的命令如下所示:
conda create -n kerasrl python=3.6
conda activate kerasrl
pip install tensorflow==1.7.1 #not TF 2.0 at time of writing
pip install keras
pip install keras-rl
pip install gym
- 在安装完所有包后,打开示例代码文件,
Chapter_12_Keras-RL.py,如图所示:
import numpy as np
import gym
from keras.models import Sequential
from keras.layers import Dense, Activation, Flatten
from keras.optimizers import Adam
from rl.agents.dqn import DQNAgent
from rl.policy import BoltzmannQPolicy
from rl.memory import SequentialMemory
ENV_NAME = 'CartPole-v0'
env = gym.make(ENV_NAME)
np.random.seed(123)
env.seed(123)
nb_actions = env.action_space.n
model = Sequential()
model.add(Flatten(input_shape=(1,) + env.observation_space.shape))
model.add(Dense(16))
model.add(Activation('relu'))
model.add(Dense(16))
model.add(Activation('relu'))
model.add(Dense(16))
model.add(Activation('relu'))
model.add(Dense(nb_actions))
model.add(Activation('linear'))
print(model.summary())
memory = SequentialMemory(limit=50000, window_length=1)
policy = BoltzmannQPolicy()
dqn = DQNAgent(model=model, nb_actions=nb_actions, memory=memory, nb_steps_warmup=10,
target_model_update=1e-2, policy=policy)
dqn.compile(Adam(lr=1e-3), metrics=['mae'])
dqn.fit(env, nb_steps=50000, visualize=True, verbose=2)
dqn.save_weights('dqn_{}_weights.h5f'.format(ENV_NAME), overwrite=True)
dqn.test(env, nb_episodes=5, visualize=True)
-
我们还没有涵盖任何 Keras 代码,但希望代码的简单性使得它相当直观。如果有什么不同的话,代码应该感觉相当熟悉,尽管缺少训练循环。
-
注意在先前的代码块中,模型是如何使用名为
Sequential的类构建的。该类是网络层的容器,我们随后添加适当的激活函数。注意在网络的末尾,最后一个层使用的是线性激活函数。 -
接下来,我们将更详细地研究记忆、策略和智能体本身的构建。请参见以下代码:
memory = SequentialMemory(limit=50000, window_length=1)
policy = BoltzmannQPolicy()
dqn = DQNAgent(model=model, nb_actions=nb_actions, memory=memory, nb_steps_warmup=10,
target_model_update=1e-2, policy=policy)
-
这里值得注意的有趣之处在于我们如何在智能体外部构建网络模型,并将其作为输入与记忆和政策一起提供给智能体。这非常强大,并为一些有趣的扩展提供了可能。
-
在文件末尾,我们可以找到训练代码。使用名为
fit的训练函数来迭代训练智能体。所有执行此操作的代码都封装在fit函数中,如下面的代码所示:
dqn.fit(env, nb_steps=50000, visualize=True, verbose=2)
- 代码的最后部分保存了模型,然后使用以下代码对智能体进行了测试:
dqn.save_weights('dqn_{}_weights.h5f'.format(ENV_NAME), overwrite=True)
dqn.test(env, nb_episodes=5, visualize=True)
- 按照常规运行代码,并观察以下图中所示的视觉训练输出和测试:

来自 Chapter_12_Keras-RL.py 的示例输出
Keras-RL 是一个轻量级的强大框架,可以快速测试概念或其他想法。Keras 本身的性能并不像 TensorFlow 或 PyTorch 那样强大,因此任何严肃的开发都应该使用这些平台之一。在下一节中,我们将探讨另一个基于 PyTorch 的强化学习平台,称为 RLLib。
探索 RL Lib
RL Lib 基于 Ray 项目,本质上是一个基于 Python 作业的系统。RL Lib 更像 ML-Agents,它通过配置文件公开功能,尽管在 ML-Agents 的情况下,结构完全运行在其平台上。Ray 非常强大,但需要详细了解配置参数和设置。因此,我们在这里展示的练习只是为了展示 Ray 的强大和灵活性,但您被指引到完整的在线文档以进行进一步的自我探索。
打开您的浏览器访问 colab.research.google.com 并按照下一个练习进行操作:
- 使用 Colab 的好处是它运行和设置起来相当容易。创建一个新的 Python 3 笔记本并输入以下命令:
!pip uninstall -y pyarrow
!pip install tensorflow ray[rllib] > /dev/null 2>&1
-
这些命令会在 Colab 实例上安装框架。安装完成后,您需要通过从菜单中选择来重启运行时:运行时 | 重启运行时。
-
在运行时重启后,创建一个新的单元格并输入以下代码:
import ray
from ray import tune
ray.init()
-
那段代码导入了框架和用于超参数调整的 tune 类。
-
创建一个新的单元格并输入以下代码:
tune.run("DQN", stop={"episode_reward_mean": 100},
config={
"env": "CartPole-v0",
"num_gpus": 0,
"num_workers": 1,
"lr": tune.grid_search([0.01, 0.001, 0.0001]),
"monitor": False, },)
-
信不信由你,就是这样。这就是构建一个在
CartPole环境中运行和训练的 DQN 代理的代码剩余部分。更不用说tune类被设置为使用tune.grid_search函数调整学习率超参数lr(alpha)。 -
运行最后一个单元格并观察输出。输出非常全面,这里展示了一个例子:

在 Google Colab 上训练 RLLib
如前一个截图所示,这是一个非常强大的框架,旨在优化超参数调整,并提供大量选项来实现这一点。它还允许在多种配置中进行多智能体训练。对于任何在强化学习方面进行严肃工作或研究的人来说,这个框架是必学的。在下一节中,我们将探讨最后一个框架,TF-Agents。
使用 TF-Agents
我们将要探讨的最后一个框架是 TF-Agents,这是一个相对较新但正在崛起的工具,同样来自 Google。Google 构建强化学习框架的方法有点像强化学习本身。他们正在尝试多次试错尝试/动作以获得最佳奖励——对于 Google 来说,这并不是一个坏主意,考虑到他们投入强化学习的资源,看到更多强化学习库的出现可能并不令人意外。
TF-Agents,虽然较新,但通常被认为更稳健和成熟。这是一个为笔记本设计的框架,这使得它非常适合尝试各种配置、超参数或环境。该框架基于 TensorFlow 2.0 开发,在 Google Colab 上运行得非常好。它很可能会成为未来教授基本强化学习概念和演示强化学习的默认平台。
在 TF-Agents Colab 仓库中有许多笔记本示例,展示了如何使用 TF-Agents(https://github.com/tensorflow/agents/tree/master/tf_agents/colabs)。整个仓库是一个很好的资源,但这个部分本身对于我们想要看到工作代码示例的人来说可能特别有用。
打开您的浏览器,访问前面的链接中的 TF-Agents Colab 页面,并遵循下一个练习:
-
对于这个练习,我们将修改其中一个样本的训练环境。这应该足以让我们了解代码的样子以及如何自己稍后进行修改。定位
1_dqn_tutorial.ipynb并点击它以打开页面。请注意,.ipynb代表 I-Python Notebook;I-Python 是一个用于托管笔记本的服务器平台。 -
点击顶部链接,链接上写着 在 Google Colab 中运行。这将打开 Colab 中的笔记本。
-
从菜单中选择 运行时 | 更改运行时类型 到 GPU,然后点击 保存。我们将把这个示例转换为使用来自 Cart Pole 的 Lunar Lander。正如我们所知,这将需要更多的计算周期。
-
首先,我们希望修改初始
pip install命令,通过更新第一个单元格中的命令来导入完整的gym包。
!apt-get install xvfb
!pip install gym[all]
!pip install 'imageio==2.4.0'
!pip install PILLOW
!pip install 'pyglet==1.3.2'
!pip install pyvirtualdisplay
!pip install tf-agents-nightly
try:
%%tensorflow_version 2.x
except:
pass
- 接下来,我们希望定位到两个提及 CartPole 环境的单元格。我们希望将所有提及 CartPole 的内容更改为 LunarLander,如下所示:
env_name = 'LunarLander-v2'
env = suite_gym.load(env_name)
# -- and --
example_environment = tf_py_environment.TFPyEnvironment(uite_gym.load('LunarLander-v2'))
-
这个示例使用的算法是一个简单的 DQN 模型。根据我们的经验,我们不能只是为
LunarLander运行相同的超参数;因此,我们将它们更改为以下内容:-
迭代次数: 从 20000 变为 500000 -
初始收集步骤: 从 1000 变为 20000 -
每迭代收集步骤数: 从 1 变为 5 -
重放缓冲区最大长度: 从 100000 变为 250000 -
批量大小: 从 64 变为 32 -
学习率: 从 1e-3 变为 1e-35 -
日志间隔: 从 200 变为 2000 -
评估回合数: 从 10 变为 15 -
评估间隔: 从 1000 变为 500
-
-
让我们继续调整网络大小。定位以下代码行,并按所示进行更改:
fc_layer_params = (100,)
# change to
fc_layer_params = (256,)
- 随意更改其他参数。如果你已经完成了作业,使用这个示例应该非常直接。TF-Agents 和 Google Colab 的一般优点之一是样本和训练输出的交互性。
这本书几乎完全是用 Google Colab 笔记本编写的。然而,尽管它们很好,但笔记本仍然缺少一些对于更大样本所需的一些良好元素。它们也使得在多个原因下难以在其他示例中稍后使用。因此,优先考虑将样本保留在 Python 文件中。
- 从菜单中选择 运行所有,以运行示例,然后耐心等待输出。这可能需要一段时间,所以拿一杯饮料放松一下。
在页面上,你将能够看到我们已覆盖的几个其他算法形式,以及我们没有时间在本书中涵盖的。以下是目前TF-Agents支持的代理类型列表及其简要描述:
-
DQN:这是我们已经多次探讨的标准深度Q学习网络代理。目前没有DDQN代理,所以看起来你可能需要将两个DQN代理组合在一起。
-
REINFORCE:这是我们首先探讨的策略梯度方法。
-
DDPG:这是一种PG方法,更具体地说,是深度确定性策略梯度方法。
-
TD3:这最好描述为一个剪裁的双Q学习模型,它使用Actor-Critic来更好地描述离散动作空间中的优势。通常,PG方法在离散动作空间中表现不佳。
-
PPO:这是我们熟悉的最邻近策略优化,另一种PG方法。
-
SAC:这是基于软Actor-Critic——一种具有随机actor的离策略最大熵深度强化学习方法。这里的推理是,代理通过尽可能随机来最大化预期奖励。
TF-Agents是一个稳定且优秀的平台,它允许你轻松地在云端构建直观的样本进行训练。这可能会使其成为构建各种问题概念模型的非常受欢迎的框架。在下一节中,我们将通过通常的附加练习来结束本章。
练习
本节中的练习在本章中范围更广,希望你能自己查看几个框架:
-
花些时间查看本章中未审查的早期框架之一。
-
使用SimpleRL解决一个与示例中不同的网格世界MDP。务必花时间调整超参数。
-
使用Google Dopamine训练一个代理来玩LunarLander环境。最佳选择可能是RainbowDQN或其变体。
-
使用Keras-RL训练一个代理来玩月球着陆环境;确保花时间调整超参数。
-
使用RL Lib训练一个代理来玩月球着陆环境;确保花时间调整超参数。
-
修改Keras-RL示例并修改网络结构。改变神经元和层的数量。
-
修改RL Lib示例并更改一些超参数,例如
num工作者和GPU数量,如下面的tune代码所示:
tune.run("DQN", stop={"episode_reward_mean": 100},
config={
"env": "CartPole-v0",
"num_gpus": 0,
"num_workers": 1,
"lr": tune.grid_search([0.01, 0.001, 0.0001]),
"monitor": False, },)
-
修改RLLib示例并使用不同的代理类型。你可能需要检查RLLib的文档以查看支持的其他代理。
-
使用TF-Agents中的TD3训练一个代理来完成月球着陆环境。
-
使用TF-Agents中的SAC并使用它来训练月球着陆环境。
随意使用 Google Colab 或您喜欢的 IDE 来执行这些练习。如果您使用的是 IDE,可能需要特别注意安装一些依赖项。在下一节和本章的最后一节中,我们将完成总结。
总结
这是一个短暂但紧张的章节,我们花时间审视了各种第三方 DRL 框架。幸运的是,所有这些框架仍然都是免费和开源的,让我们希望它们保持这种状态。我们首先审视了许多正在增长的框架以及一些优缺点。然后,我们研究了目前最受欢迎或最有前途的库。从 Google Dopamine 开始,它展示了 RainbowDQN,我们探讨了如何在 Google Colab 上运行一个快速示例。之后,Keras-RL 接下来,我们介绍了 Keras 框架以及如何使用 Keras-RL 库。接着转向 RLLib,我们研究了 DRL 框架强大的自动化功能,它具有许多功能。最后,我们用 Google 的另一个项目 TF-Agents 完成了这一章,我们在 Google Colab 笔记本上使用 TF-Agents 运行了一个完整的 DQN 代理。
我们已经花费了大量时间学习和使用强化学习(RL)和深度强化学习(DRL)算法。如此之多,以至于我们应该对训练和寻找更具挑战性的环境感到相当舒适。
在下一章中,我们将转向在更复杂的环境中训练代理,例如现实世界。然而,我们不会使用现实世界,而是将使用下一个最佳选择:3D 世界。
第十五章:第3节:奖励自己
我们已经到达了这本书的结尾。我们首先探索了强化学习的基础,然后在第二部分中利用并增强这些知识。最后,我们将通过探索更具挑战性的环境来娱乐或盈利,来完成第三部分和最后一部分。
本节包含以下章节:
第十六章:3D世界
我们几乎接近了探索什么是通用人工智能(AGI)以及如何使用深度强化学习(DRL)帮助我们达到这一目标的旅程的终点。尽管目前对DRL是否确实是通往AGI的正确路径还存在疑问,但它似乎是我们当前最好的选择。然而,我们质疑DRL的原因在于其能否或不能掌握多样化的3D空间或世界,正如人类和所有动物所掌握的3D空间,但我们发现很难在RL代理上训练。事实上,许多AGI研究人员的信念是,解决3D状态空间问题对于解决真正的通用人工智能可能大有裨益。我们将在本章中探讨为什么这是可能的。
对于本章,我们将探讨为什么3D世界对DRL代理构成了如此独特的问题,以及我们可以如何训练它们来解释状态。我们将探讨典型3D代理如何使用视觉来解释状态,并考察由此产生的深度学习网络类型。然后,我们将探讨在环境中使用3D视觉的一个实际例子以及我们处理状态的可选方案。接下来,继续使用Unity,我们将探讨障碍塔挑战,这是一个有10万美元奖金的AI挑战,以及赢得奖金所使用的实现方法。在章节的结尾,我们将探讨另一个名为Habitat的3D环境,以及它如何用于开发代理。
本章我们将讨论的主要要点如下:
-
在3D世界中推理
-
训练视觉代理
-
通用化3D视觉
-
挑战Unity障碍塔挑战
-
探索FAIR的栖息地——具身代理
本章中的示例可能需要特别长的时间来训练,所以请耐心等待,或者也许您可以选择只做一项。这不仅节省了您的时间,还减少了能源消耗。在下一章中,我们将探讨为什么3D世界如此特别。
在3D世界中推理
那么,为什么3D世界如此重要,或者至少人们认为它很重要呢?好吧,这一切都归结于状态解释,或者我们DRL(深度强化学习)领域喜欢称之为状态表示。目前有很多工作致力于为强化学习和其他问题提供更好的状态表示。理论上是这样的,能够仅仅表示状态的关键点或收敛点,可以使我们显著简化问题。我们在多个章节中探讨了使用各种技术来实现这一点。回想一下,我们是如何将连续观察空间的状态表示离散化到网格网格中的。这种技术是我们当时使用现有工具解决更困难连续空间问题的方法。从那时起,在几个章节中,我们看到了如何将连续空间直接输入到我们的深度学习网络中。这包括直接将图像作为游戏状态、截图输入,使用卷积神经网络。然而,3D世界,那些代表现实世界的世界,在表示状态方面提出了独特的挑战。
那么,在3D环境中表示状态空间有什么困难呢?我们难道不能像在其他环境中那样给智能体传感器吗?好吧,是的,也不完全是。问题是给智能体传感器实际上是我们对智能体需要用来解释问题的需求强加偏见。例如,我们可以给智能体一个传感器,直接告诉它前方、左侧和右侧物体的距离。虽然这可能对任何驾驶智能体来说足够了,但对于需要爬楼梯的智能体来说,可能就不适用了。相反,我们可能需要提供楼梯高度作为另一个传感器输入,这意味着我们为3D世界引入状态给智能体的首选方法是使用视觉或环境图像。当然,这样做的原因是为了消除我们(人类)的任何偏见,我们最好的做法就是直接将环境状态作为图像直接喂给智能体。
我们已经看到,当我们研究玩Atari游戏时,如何使用游戏区域的图像输入游戏状态。然而,那些游戏环境都是2D的,这意味着状态空间本质上被扁平化或收敛了。在这里,“收敛”这个词适用,因为当处理3D环境和现实世界时,这变成了一个问题。在3D空间中,一个视角可能产生多个状态,同样,一个视角也可能观察到多个状态空间。
我们可以在以下图中看到这是如何工作的:

3D世界中的智能体状态示例
在图中,我们可以看到代理,在 Unity 的 Visual Hallway 环境中心的一个蓝色点,使用 ML-Agents 工具包。我们很快将回顾这个环境的示例,所以请不用担心现在就复习它。您可以从图中看到,代理是如何使用代理相机从同一物理位置观察不同状态观察的。代理相机是我们给予代理以观察世界的视觉。
从这个相机,代理将状态作为视觉观察摄入,这个观察作为图像输入到深度学习网络中。这个图像被 2D 卷积神经网络层分解成特征,这些特征是代理学习的。问题是,我们正在使用 2D 滤波器来尝试消化 3D 信息。在 第 7 章,使用 DDQN 深入学习 中,我们探讨了使用 CNN 从 Atari 游戏中摄入图像状态,正如我们所看到的,这非常有效。
您需要安装 ML-Agents 工具包,并且应该已经打开了 UnitySDK 测试项目。如果您需要这方面的帮助,请返回到 第 11 章,利用 ML-Agents,并首先遵循那里的一些练习。
Unity 对其代理相机设置也做同样的事情,在下一个练习中,我们将看到以下内容:
-
定位到位于 ML-Agents 仓库中的
ml-agents/ml-agents/mlagents/trainers文件夹。如果您需要帮助拉取仓库,请遵循之前给出的信息提示。 -
从这个文件夹中,找到并打开文本或 Python IDE 中的
models.py文件。ML-Agents 使用 TensorFlow 编写,一开始可能有些令人畏惧,但代码遵循了许多与 PyTorch 相同的原则。 -
大约在第 250 行,从
LearningModel基类创建了一个create_visual_observation_encoder函数。这是 ML-Agents、PPO 和 SAC 实现使用的基类模型。
ML-Agents 最初是在 Keras 中开发的,然后成熟到 TensorFlow 以提高性能。从那时起,PyTorch 在学术研究人员和构建者中看到了巨大的流行增长。在撰写本文时,PyTorch 是增长最快的深度学习框架。目前尚不清楚 Unity 是否也会效仿并将代码转换为 PyTorch,或者只是升级到 TensorFlow 2.0。
create_visual_observation_encoder函数是编码状态的基函数,函数的完整定义(不包括注释)如下所示:
def create_visual_observation_encoder(
self,
image_input: tf.Tensor,
h_size: int,
activation: ActivationFunction,
num_layers: int,
scope: str,
reuse: bool,
) -> tf.Tensor:
with tf.variable_scope(scope):
conv1 = tf.layers.conv2d(
image_input,
16,
kernel_size=[8, 8],
strides=[4, 4],
activation=tf.nn.elu,
reuse=reuse,
name="conv_1",
)
conv2 = tf.layers.conv2d(
conv1,
32,
kernel_size=[4, 4],
strides=[2, 2],
activation=tf.nn.elu,
reuse=reuse,
name="conv_2",
)
hidden = c_layers.flatten(conv2)
with tf.variable_scope(scope + "/" + "flat_encoding"):
hidden_flat = self.create_vector_observation_encoder(
hidden, h_size, activation, num_layers, scope, reuse
)
return hidden_flat
- 虽然代码在 TensorFlow 中,但有一些明显的常见术语的指标,例如 layers 和 conv2d。有了这些信息,您可以看到这个编码器使用了两个 CNN 层:一个具有 8 x 8 的内核大小、4 x 4 的步长和 16 个滤波器;接着是一个使用 4 x 4 的内核大小、2 x 2 的步长和 32 个滤波器的第二层。
注意再次使用了没有池化层的做法。这是因为当我们使用池化在 CNN 层之间时,会丢失空间信息。然而,根据网络的深度,靠近顶部的单个池化层可能是有益的。
- 注意函数的返回值是一个由
hidden_flat表示的隐藏平坦层。回想一下,我们的 CNN 层被用来学习状态,然后作为以下图所示的那样,将其输入到我们的学习网络中。

示例网络图
-
上述图是一个简化的网络图,显示了 CNN 层在输入到隐藏中间层时会被展平。展平是将卷积 2D 数据转换为一条一维向量,然后将其输入到网络的其余部分。
-
通过打开 Unity 编辑器到
Assets/ML-Agents/Examples/Hallway/Scenes文件夹中的 VisualHallway 场景来查看图像源是如何定义的。 -
展开第一个 VisualSymbolFinderArea,在 Hierarchy 窗口中选择 Agent 对象。然后,在 Inspector 窗口中找到并双击 Brain,将其在以下窗口中打开:

检查 VisualHallwayLearning 脑
这里需要注意的重要一点是,代理被设置为接受 84 x 84 像素大小的图像。这意味着代理摄像头被采样到一个与相同像素面积匹配的图像大小。由于场景中缺乏细节,这个相对较小的像素面积对于这个环境来说是可以工作的。如果细节增加,我们可能还需要增加输入图像的分辨率。
在下一节中,我们将探讨使用 ML-Agents 工具包通过视觉训练代理。
训练视觉代理
Unity 开发了一个 2D 和 3D 游戏引擎/平台,它已经成为构建游戏最受欢迎的平台。其中大部分游戏是 3D 类型,因此 Unity 对掌握能够处理更多 3D 自然世界的代理的任务产生了专门兴趣。因此,Unity 在这个问题上投入了大量资金,并且与 DeepMind 合作进一步开发。这种合作的结果还有待观察,但有一点可以肯定的是,Unity 将成为我们探索 3D 代理训练的首选平台。
在下一个练习中,我们将回到 Unity,看看我们如何在视觉 3D 环境中训练一个代理。Unity 可以说是设置和构建这类环境的最佳场所,正如我们在前面的章节中看到的。打开 Unity 编辑器,按照以下步骤操作:
-
打开位于
Assets/ML-Agents/Examples/Hallway/Scenes文件夹中的 VisualHallway 场景。 -
在场景层次结构窗口中找到 Academy 对象,并将 Hallway Academy 组件的 Brains 部分的 Control 选项设置为启用,如图所示:

将学院设置为控制学习大脑
-
这将设置学院以控制智能体的大脑。
-
接下来,从(1)到(7)选择所有VisualSymbolFinderArea对象,并确保通过在检查器窗口中点击对象的Active选项来启用它们,如下截图所示:

启用场景中的所有子环境
-
这使得所有子环境区域都可以运行,并且我们在训练时可以额外运行七个智能体。正如我们使用演员-评论员方法时所看到的,能够更有效地从环境中采样有许多优势。几乎所有示例ML-Agents环境都提供了多个子训练环境。这些多个环境被认为是独立的环境,但允许大脑与多个智能体同步训练。
-
从文件菜单保存场景和项目文件。
-
打开一个新的Python或Anaconda shell,并将虚拟环境设置为使用你之前为ML-Agents设置的虚拟环境。如果你需要帮助,请参阅第11章,利用ML-Agents。
-
导航到Unity
ml-agents文件夹并执行以下命令以开始训练:
mlagents-learn config/trainer_config.yaml --run-id=vishall_1 --train
- 这将启动Python训练器,几秒钟后,会提示你在编辑器中点击播放。完成此操作后,所有环境中的智能体将开始训练,你可以在编辑器中可视化这个过程。以下截图显示了在命令行中看起来是怎样的:

运行ML-Agents训练器
现在我们已经了解了如何在Unity中使用ML-Agents训练智能体,我们可以继续探索下一节中的一些其他未记录的训练选项。
如果你遇到在走廊环境中训练的问题,你总是可以尝试其他各种环境。由于发布或版本冲突,一些环境变得损坏并不罕见。
推广3D视觉
如前文在第11章中提到的,利用ML-Agents,我们了解到Unity团队在训练3D世界中的智能体方面是领导者之一。毕竟,他们确实有很强的利益驱动,提供开发者可以轻松接入并构建智能体的AI平台。然而,这种适用于广泛应用的智能体现在被认为是通往通用人工智能的第一步,因为如果Unity能够成功构建一个能够玩任何游戏的通用智能体,那么它实际上就构建了一个初级通用人工智能。
定义 AGI 的问题在于试图理解一种智能有多广泛或有多一般,以及我们如何量化智能体对环境的理解以及将知识转移到其他任务的可能能力。我们真的不知道如何最好地定义它,直到有人有信心站起来并声称已经开发出 AGI。这个声明的一个很大部分将取决于智能体如何泛化环境状态,而其中很大一部分将是泛化 3D 视觉本身。
Unity 有一种未记录的方法可以改变在环境中训练时可以使用的视觉编码器的类型(至少在撰写本文时是这样)。
在下一个练习中,我们将查看如何通过以下步骤将超参数添加到配置中并设置不同的视觉编码器:
- 找到位于
mlagents/ml-agents/config文件夹中的trainer_config.yaml配置文件,并在 IDE 或文本编辑器中打开它。
YAML 是一个缩写,代表 YAML ain't markup language。ML-Agents 配置标记文件的格式与旧版的 Windows INI 配置文件非常相似。
- 此文件定义了各种学习大脑的配置。找到
VisualHallwayLearning大脑的章节,如下所示:
VisualHallwayLearning:
use_recurrent: true
sequence_length: 64
num_layers: 1
hidden_units: 128
memory_size: 256
beta: 1.0e-2
num_epoch: 3
buffer_size: 1024
batch_size: 64
max_steps: 5.0e5
summary_freq: 1000
time_horizon: 64
- 这些超参数是除了在配置文件顶部默认大脑配置中设置的基值之外额外的。如下所示:
idefault:
trainer: ppo
batch_size: 1024
beta: 5.0e-3
buffer_size: 10240
epsilon: 0.2
hidden_units: 128
lambd: 0.95
learning_rate: 3.0e-4
learning_rate_schedule: linear
max_steps: 5.0e4
memory_size: 256
normalize: false
num_epoch: 3
num_layers: 2
time_horizon: 64
sequence_length: 64
summary_freq: 1000
use_recurrent: false
vis_encode_type: simple
reward_signals:
extrinsic:
strength: 1.0
gamma: 0.99
- 我们感兴趣的超参数是设置为简单并突出显示在前面代码示例中的
vis_encode_type值。ML-Agents 通过更改此选项支持两种额外的视觉编码类型:
-
vis_enc_type:设置视觉编码类型的超参数: -
simple:这是默认版本,也是我们之前看过的版本。 -
nature_cnn:这定义了由 Nature 期刊中一篇论文提出的 CNN 架构,我们将在稍后更详细地了解它。 -
resnet:ResNet 是一种已发表的 CNN 架构,已被证明在图像分类方面非常有效。
- 我们将通过在
VisualHallwayLearning大脑配置的末尾添加新行来更改我们大脑中的默认值:
VisualHallwayLearning:
use_recurrent: true
sequence_length: 64
num_layers: 1
hidden_units: 128
memory_size: 256
beta: 1.0e-2
num_epoch: 3
buffer_size: 1024
batch_size: 64
max_steps: 5.0e5
summary_freq: 1000
time_horizon: 64
vis_enc_type: nature_cnn --or-- resnet
- 现在我们知道了如何设置这些,让我们通过打开
ml-agents/trainers文件夹中我们之前打开的models.py代码来看看它们的样子。滚动到create_visual_observation_encoder函数下方,找到如下所示的create_nature_cnn_observation_encoder函数:
def create_nature_cnn_visual_observation_encoder(
self,
image_input: tf.Tensor,
h_size: int,
activation: ActivationFunction,
num_layers: int,
scope: str,
reuse: bool,
) -> tf.Tensor:
with tf.variable_scope(scope):
conv1 = tf.layers.conv2d(
image_input,
32,
kernel_size=[8, 8],
strides=[4, 4],
activation=tf.nn.elu,
reuse=reuse,
name="conv_1",
)
conv2 = tf.layers.conv2d(
conv1,
64,
kernel_size=[4, 4],
strides=[2, 2],
activation=tf.nn.elu,
reuse=reuse,
name="conv_2",
)
conv3 = tf.layers.conv2d(
conv2,
64,
kernel_size=[3, 3],
strides=[1, 1],
activation=tf.nn.elu,
reuse=reuse,
name="conv_3",
)
hidden = c_layers.flatten(conv3)
with tf.variable_scope(scope + "/" + "flat_encoding"):
hidden_flat = self.create_vector_observation_encoder(
hidden, h_size, activation, num_layers, scope, reuse
)
return hidden_flat
-
与这种实现的主要区别在于使用了名为
conv3的第三层。我们可以看到这个第三层的核大小为 3 x 3,步长为 1 x 1,有 64 个滤波器。由于核和步长尺寸较小,我们可以看到这个新层被用来提取更精细的特征。这种特征有多有用取决于环境。 -
接下来,我们想查看列在最后一个函数之后的第三个视觉编码实现。下一个函数是
create_resent_visual_observation_encoder,如下所示:
def create_resnet_visual_observation_encoder(
self,
image_input: tf.Tensor,
h_size: int,
activation: ActivationFunction,
num_layers: int,
scope: str,
reuse: bool,
) -> tf.Tensor:
n_channels = [16, 32, 32]
n_blocks = 2
with tf.variable_scope(scope):
hidden = image_input
for i, ch in enumerate(n_channels):
hidden = tf.layers.conv2d(
hidden,
ch,
kernel_size=[3, 3],
strides=[1, 1],
reuse=reuse,
name="layer%dconv_1" % i,
)
hidden = tf.layers.max_pooling2d(
hidden, pool_size=[3, 3], strides=[2, 2], padding="same"
)
for j in range(n_blocks):
block_input = hidden
hidden = tf.nn.relu(hidden)
hidden = tf.layers.conv2d(
hidden,
ch,
kernel_size=[3, 3],
strides=[1, 1],
padding="same",
reuse=reuse,
name="layer%d_%d_conv1" % (i, j),
)
hidden = tf.nn.relu(hidden)
hidden = tf.layers.conv2d(
hidden,
ch,
kernel_size=[3, 3],
strides=[1, 1],
padding="same",
reuse=reuse,
name="layer%d_%d_conv2" % (i, j),
)
hidden = tf.add(block_input, hidden)
hidden = tf.nn.relu(hidden)
hidden = c_layers.flatten(hidden)
with tf.variable_scope(scope + "/" + "flat_encoding"):
hidden_flat = self.create_vector_observation_encoder(
hidden, h_size, activation, num_layers, scope, reuse
)
return hidden_flat
- 你现在可以回到配置文件中更新
vis_enc_type超参数,并重新训练视觉代理。如果你有时间运行两个版本,注意哪个编码器更成功。
我们已经看到了ML-Agents支持的视觉编码器的各种变体,Unity团队也加入了一个相对较新的变体,称为ResNet。ResNet是一个重要的成就,迄今为止,它已被证明在某些视觉环境中训练代理是有用的。因此,在下一节中,我们将花更多的时间来探讨ResNet。
用于视觉观察编码的ResNet
卷积层已经被用于各种配置,用于成功执行图像分类和识别任务已有一些时间了。我们使用直接2D CNN时遇到的问题是,我们本质上是在平坦化状态表示,但通常不是以好的方式。这意味着我们正在将3D空间的视觉观察结果平坦化成2D图像,然后尝试从中提取重要特征。这导致代理认为如果它从同一3D环境中的不同位置识别出相同的视觉特征,它就处于相同的状态。这会在代理中造成混淆,你可以通过观察一个代理只是绕着圈子乱转来可视化这一点。
同样的代理混淆通常是由于梯度消失或爆炸引起的。我们并没有经常遇到这个问题,因为我们的网络相当浅。然而,为了提高网络性能,我们经常通过添加额外的层来加深网络。实际上,在一些视觉分类网络中,可能有100层或更多的卷积层试图提取各种特征。通过添加这么多额外的层,我们引入了梯度消失的机会。梯度消失是我们用来描述梯度变得如此之小,以至于看起来消失了,或者实际上对训练/学习没有影响的术语。记住,我们的梯度计算需要一个总损失,然后通过网络传递回来。损失需要推回网络的层数越多,它就越小。这是我们用于图像分类和解释的深度CNN网络中的一个主要问题。
ResNet或残差CNN网络被引入作为一种允许更深的编码结构而不受梯度消失影响的方法。残差网络之所以被称为残差网络,是因为它们携带一个称为身份快捷连接的残差身份。以下图表来自深度残差学习用于图像识别论文,展示了残差块中的基本组件:

一个残差块
论文作者的直觉是,堆叠的层不应该仅仅因为它们是堆叠的而降低网络性能。相反,通过将最后一层的输出推送到下一层,我们实际上能够有效地将训练隔离到各个单独的层。我们称这为恒等,因为最后一层输出的尺寸可能不会匹配下一层的输入,因为我们绕过了中间层。相反,我们用恒等输入张量乘以最后一层的输出,以便匹配输出和输入。
让我们回到ML-Agents中的ResNet编码器实现,看看在下一个练习中是如何做到这一点的:
-
打开位于
mlagents/ml-agents/trainers文件夹中的models.py文件。 -
再次向下滚动到
create_resnet_visual_observation_encoder函数。查看定义构建残差网络的变量,如下所示的前两行:
n_channels = [16, 32, 32] # channel for each stack
n_blocks = 2 # number of residual blocks
- 接下来,再向下滚动一点,到我们列举构建每个输入层所需通道数的地方。代码如下所示:
for i, ch in enumerate(n_channels):
hidden = tf.layers.conv2d(
hidden,
ch,
kernel_size=[3, 3],
strides=[1, 1],
reuse=reuse,
name="layer%dconv_1" % i,)
hidden = tf.layers.max_pooling2d(
hidden, pool_size=[3, 3], strides=[2, 2], padding="same")
-
n_channels变量表示每个输入卷积层使用的通道数或过滤器。因此,我们正在创建包含输入层和中间块的三个残差层组。这些块用于将训练隔离到每一层。 -
继续向下滚动,我们可以看到以下代码中块是如何在层之间构建的:
for j in range(n_blocks):
block_input = hidden
hidden = tf.nn.relu(hidden)
hidden = tf.layers.conv2d(
hidden,
ch,
kernel_size=[3, 3],
strides=[1, 1],
padding="same",
reuse=reuse,
name="layer%d_%d_conv1" % (i, j),)
hidden = tf.nn.relu(hidden)
hidden = tf.layers.conv2d(
hidden,
ch,
kernel_size=[3, 3],
strides=[1, 1],
padding="same",
reuse=reuse,
name="layer%d_%d_conv2" % (i, j),)
hidden = tf.add(block_input, hidden)
- 这段代码创建了一个类似于以下图中所示的网络结构:

ML-Agents中ResNet架构图
-
从本质上讲,我们仍然只有三个不同的卷积层提取特征,但每个这样的层现在可以独立训练。此外,我们很可能将这个网络的深度增加几倍,并预期视觉编码性能会有所提高。
-
回去,如果你还没有这样做,用残差网络训练一个视觉代理进行视觉观察编码。
如果你回去用残差网络训练另一个视觉代理,你可能会发现代理的表现略有提高,但它们仍然可能会感到困惑。再次强调,这更多的是视觉编码系统的问题,而不是DRL本身的问题。然而,人们认为,一旦我们能够解决视觉环境的视觉编码问题,真正的AGI(通用人工智能)将无疑会大大接近。
在下一节中,我们将探讨Unity团队(在Google DeepMind的帮助下)为挑战DRL研究人员而构建的一个特殊环境,这正是3D世界视觉编码的问题。
挑战Unity障碍塔挑战
到2018年底,Unity在DeepMind的帮助下开始开发一个挑战,旨在让DRL领域最具挑战性的研究人员承担任务。挑战是以Unity作为Gym接口环境开发的,并使用3D第一人称视角的游戏。3D视角是一种游戏界面,如《古墓丽影》和《生化危机》等游戏使其闻名。以下截图显示了游戏界面的一个示例:

举例说明障碍塔挑战
障碍塔挑战不仅是在3D中,而且房间和墙壁上的模式和材料会随着关卡的变化而变化。这使得视觉泛化变得更加困难。此外,挑战提出了多个并发步骤来完成任务。也就是说,每个关卡都需要角色找到一扇门并将其打开。在更高级的关卡中,门需要特殊的钥匙才能激活或获取,这使得这几乎成为一个多任务RL问题——这不是我们之前考虑解决的问题。幸运的是,正如我们使用ML-Agents Curiosity Learning所展示的,只要任务线性连接,多步RL是可以实现的。这意味着没有分支或需要决策的任务。
多任务强化学习在研究方面迅速发展,但它仍然是一个非常复杂的话题。解决MTRL的当前首选方法被称为元强化学习。我们将在第14章“从DRL到AGI”中介绍元强化学习,我们将讨论未来几个月或几年DRL的下一阶段发展。
在下一个练习中,我们将仔细审查Unity障碍塔挑战的获胜者Alex Nichol的工作。Alex通过提交一个在分类图像和人类记录的演示(行为克隆)上预训练的修改后的PPO智能体赢得了10万美元的挑战。他实际上是通过使用一系列工程解决方案更好地泛化智能体的状态观察而获胜的。
打开你的Anaconda提示符,并按照以下示例操作:
- 建议在安装任何新代码和环境之前创建一个新的虚拟环境。这可以通过Anaconda使用以下命令轻松完成:
conda create -n obtower python=3.6
conda activate obstower
- 首先,您需要从以下存储库下载并安装Unity障碍塔挑战(https://github.com/Unity-Technologies/obstacle-tower-env),或者只需从新的虚拟环境使用以下命令:
git clone git@github.com:Unity-Technologies/obstacle-tower-env.git
cd obstacle-tower-env
pip install -e .
- 运行OTC环境相当简单,可以使用以下简单的代码块在环境中执行随机动作:
from obstacle_tower_env import ObstacleTowerEnv, ObstacleTowerEvaluation
def run_episode(env):
done = False
episode_return = 0.0
while not done:
action = env.action_space.sample()
obs, reward, done, info = env.step(action)
episode_return += reward
return episode_return
if __name__ == '__main__':
eval_seeds = [1001, 1002, 1003, 1004, 1005]
env = ObstacleTowerEnv('./ObstacleTower/obstacletower')
env = ObstacleTowerEvaluation(env, eval_seeds)
while not env.evaluation_complete:
episode_rew = run_episode(env)
print(env.results)
env.close()
-
运行OTC环境的代码现在应该相当熟悉了,但有一个需要注意的项目。代理会循环通过回合或生命,但代理只有一定数量的生命。这个环境模拟了一个真实游戏,因此代理只有有限的尝试次数和时间来完成挑战。
-
接下来,从Alex Nichol(
unixpickle)这里拉取仓库:https://github.com/unixpickle/obs-tower2.git,或者检查Chapter_13/obs-tower2源文件夹。 -
导航到文件夹并运行以下命令以安装所需的依赖项:
pip install -e .
- 之后,你需要配置一些环境变量到以下内容:
`OBS_TOWER_PATH` - the path to the obstacle tower binary.
`OBS_TOWER_RECORDINGS` - the path to a directory where demonstrations are stored.
`OBS_TOWER_IMAGE_LABELS` - the path to the directory of labeled images.
- 你设置这些环境变量的方式将取决于你的操作系统以及你想要设置它们的位置级别。对于Windows用户,你可以使用系统环境变量设置面板来设置环境变量,如下所示:

设置环境变量(Windows)
现在一切都已经设置好了,是时候开始预训练代理了。我们将在下一节中介绍这一训练过程。
预训练代理
我们已经介绍了几种管理训练性能的方法,这些方法通常由低奖励或奖励稀疏性引起。这包括使用一种称为行为克隆的技术,其中人类演示一系列导致奖励的动作,然后这些动作作为预训练策略反馈给代理。在这里,获胜的实现是行为克隆与预训练图像分类的组合。
我们将从上一个练习结束的地方继续,学习我们需要执行哪些步骤来首先预训练一个分类器:
- 首先,我们需要捕获环境中的图像以预训练一个分类器。这需要你运行位于
obs_tower2/recorder/record.py文件夹中的record.py脚本。确保在运行此脚本时,你的环境变量配置正确。
仓库中的文档或README.md是好的,但它主要面向对复制结果非常感兴趣的先进用户。如果你在这次演练中遇到问题,请参考该文档。
- 运行脚本将启动Unity OTC,并允许你作为玩家与游戏互动。在你玩游戏的过程中,
record.py脚本会在每个回合结束后记录你的动作图像。你需要玩几场比赛才能收集到足够的训练数据。或者,Alex已经在这个位置上提供了一些在线记录:http://obstower.aqnichol.com/。
注意:
记录和标签都在tar文件中,记录文件大小为25 GB。
-
接下来,我们需要对记录的图像进行标注,以帮助智能体进行分类。找到并运行位于
obs_tower2/labeler/文件夹中的main.py脚本。这将启动一个网络应用程序。只要你的路径设置正确,你现在就可以打开浏览器并访问http://127.0.0.1:5000(本地主机,端口5000)。 -
你现在将通过网页界面被提示对图像进行标注。对于每张图像,按照以下截图所示进行分类:

为分类标注图像数据
-
亚历克斯在他的原始文档中提到,经过一些练习后,他可以每秒标注20-40张图像。再次提醒,如果你想避免这一步骤,只需下载包含他示例录音和标签的tar文件。
-
接下来,你需要运行该分类器,使用你刚刚生成或下载的训练输入图像和标签。通过执行以下命令来运行分类器:
cd obs_tower2/scripts
python run_classifier.py
-
分类完成后,结果将定期输出到
save_classifier.pk1文件。整个过程可能需要几个小时才能完全训练完成。 -
在构建了预分类器之后,我们可以使用人类样本进行行为克隆。这意味着你将使用保存并预先标注的会话作为后续智能体训练的输入。你可以通过运行以下命令来启动此过程:
python run_clone.py
- 运行此脚本会定期将输出生成到
save_clone.pkl文件,整个脚本可能需要一天或更长时间才能运行。当脚本完成后,将输出复制到save_prior.pkl文件,如下所示:
cp save_clone.pkl save_prior.pkl
这创建了一个先验记录集或记忆集,我们将在下一节中用它来训练智能体。
层次化 - 隐式层次
亚历克斯使用分层强化学习的概念来解决OTC要求你解决的多个任务智能体学习问题。HRL是Meta-RL之外另一种用于成功解决多任务问题的方法。Prierarchy-RL通过构建一个先验层次结构来改进这一点,允许通过熵或不确定性定义动作或动作状态。高熵或高度不确定的动作成为高级或基于顶部的动作。这个概念有些抽象,所以让我们通过一个代码示例来看看它是如何结合在一起的:
- 用于赢得挑战的基础智能体是PPO;以下是该智能体的完整源代码列表以及PPO的复习:
import itertools
import numpy as np
import torch
import torch.nn.functional as F
import torch.optim as optim
from .util import atomic_save
class PPO:
def __init__(self, model, epsilon=0.2, gamma=0.99, lam=0.95, lr=1e-4, ent_reg=0.001):
self.model = model
self.epsilon = epsilon
self.gamma = gamma
self.lam = lam
self.optimizer = optim.Adam(model.parameters(), lr=lr)
self.ent_reg = ent_reg
def outer_loop(self, roller, save_path='save.pkl', **kwargs):
for i in itertools.count():
terms, last_terms = self.inner_loop(roller.rollout(), **kwargs)
self.print_outer_loop(i, terms, last_terms)
atomic_save(self.model.state_dict(), save_path)
def print_outer_loop(self, i, terms, last_terms):
print('step %d: clipped=%f entropy=%f explained=%f' %
(i, last_terms['clip_frac'], terms['entropy'], terms['explained']))
def inner_loop(self, rollout, num_steps=12, batch_size=None):
if batch_size is None:
batch_size = rollout.num_steps * rollout.batch_size
advs = rollout.advantages(self.gamma, self.lam)
targets = advs + rollout.value_predictions()[:-1]
advs = (advs - np.mean(advs)) / (1e-8 + np.std(advs))
actions = rollout.actions()
log_probs = rollout.log_probs()
firstterms = None
lastterms = None
for entries in rollout.batches(batch_size, num_steps):
def choose(values):
return self.model.tensor(np.array([values[t, b] for t, b in entries]))
terms = self.terms(choose(rollout.states),
choose(rollout.obses),
choose(advs),
choose(targets),
choose(actions),
choose(log_probs))
self.optimizer.zero_grad()
terms['loss'].backward()
self.optimizer.step()
lastterms = {k: v.item() for k, v in terms.items() if k != 'model_outs'}
if firstterms is None:
firstterms = lastterms
del terms
return firstterms, lastterms
def terms(self, states, obses, advs, targets, actions, log_probs):
model_outs = self.model(states, obses)
vf_loss = torch.mean(torch.pow(model_outs['critic'] - targets, 2))
variance = torch.var(targets)
explained = 1 - vf_loss / variance
new_log_probs = -F.cross_entropy(model_outs['actor'], actions.long(), reduction='none')
ratio = torch.exp(new_log_probs - log_probs)
clip_ratio = torch.clamp(ratio, 1 - self.epsilon, 1 + self.epsilon)
pi_loss = -torch.mean(torch.min(ratio * advs, clip_ratio * advs))
clip_frac = torch.mean(torch.gt(ratio * advs, clip_ratio * advs).float())
all_probs = torch.log_softmax(model_outs['actor'], dim=-1)
neg_entropy = torch.mean(torch.sum(torch.exp(all_probs) * all_probs, dim=-1))
ent_loss = self.ent_reg * neg_entropy
return {
'explained': explained,
'clip_frac': clip_frac,
'entropy': -neg_entropy,
'vf_loss': vf_loss,
'pi_loss': pi_loss,
'ent_loss': ent_loss,
'loss': vf_loss + pi_loss + ent_loss,
'model_outs': model_outs,
}
-
熟悉这个实现与我们在PPO中讨论的内容之间的区别。我们的示例为了解释目的而简化,但遵循相同的模式。
-
特别注意
inner_loop中的代码,并理解其工作原理:
def inner_loop(self, rollout, num_steps=12, batch_size=None):
- 打开位于根目录
obs_tower2文件夹中的prierarchy.py文件,如下所示:
import numpy as np
import torch
import torch.nn.functional as F
from .ppo import PPO
class Prierarchy(PPO):
def __init__(self, prior, *args, kl_coeff=0, **kwargs):
super().__init__(*args, **kwargs)
self.prior = prior
self.kl_coeff = kl_coeff
def print_outer_loop(self, i, terms, last_terms):
print('step %d: clipped=%f entropy=%f explained=%f kl=%f' %
(i, last_terms['clip_frac'], last_terms['entropy'], terms['explained'],
terms['kl']))
def inner_loop(self, rollout, num_steps=12, batch_size=None):
if batch_size is None:
batch_size = rollout.num_steps * rollout.batch_size
prior_rollout = self.prior.run_for_rollout(rollout)
prior_logits = prior_rollout.logits()
rollout = self.add_rewards(rollout, prior_rollout)
advs = rollout.advantages(self.gamma, self.lam)
targets = advs + rollout.value_predictions()[:-1]
actions = rollout.actions()
log_probs = rollout.log_probs()
firstterms = None
lastterms = None
for entries in rollout.batches(batch_size, num_steps):
def choose(values):
return self.model.tensor(np.array([values[t, b] for t, b in entries]))
terms = self.extended_terms(choose(prior_logits),
choose(rollout.states),
choose(rollout.obses),
choose(advs),
choose(targets),
choose(actions),
choose(log_probs))
self.optimizer.zero_grad()
terms['loss'].backward()
self.optimizer.step()
lastterms = {k: v.item() for k, v in terms.items() if k != 'model_outs'}
if firstterms is None:
firstterms = lastterms
del terms
return firstterms, lastterms
def extended_terms(self, prior_logits, states, obses, advs, targets, actions, log_probs):
super_out = self.terms(states, obses, advs, targets, actions, log_probs)
log_prior = F.log_softmax(prior_logits, dim=-1)
log_posterior = F.log_softmax(super_out['model_outs']['actor'], dim=-1)
kl = torch.mean(torch.sum(torch.exp(log_posterior) * (log_posterior - log_prior), dim=-1))
kl_loss = kl * self.ent_reg
super_out['kl'] = kl
super_out['kl_loss'] = kl_loss
super_out['loss'] = super_out['vf_loss'] + super_out['pi_loss'] + kl_loss
return super_out
def add_rewards(self, rollout, prior_rollout):
rollout = rollout.copy()
rollout.rews = rollout.rews.copy()
def log_probs(r):
return F.log_softmax(torch.from_numpy(np.array([m['actor'] for m in r.model_outs])),
dim=-1)
q = log_probs(prior_rollout)
p = log_probs(rollout)
kls = torch.sum(torch.exp(p) * (p - q), dim=-1).numpy()
rollout.rews -= kls[:-1] * self.kl_coeff
return rollout
-
我们在这里看到的是
Hierarchy类,它是PPO的扩展,通过扩展inner_loop函数来工作。简单来说,这段代码优化了KL-Divergence计算,使我们能够在山丘上稳固地占据位置而不会掉落。回想一下,这是我们关于裁剪目标函数的讨论。 -
注意到使用了
prior策略或基于先前预训练和行为克隆生成的策略。这个先验策略定义了动作是高不确定性还是低不确定性。这样,代理实际上可以使用先验层次结构或先验结构来选择一系列高熵/不确定的动作。以下图表说明了这种有效的工作方式:

基于熵层次结构选择动作的代理
- 因此,代理不再决定何时以及是否进行探索,而是根据其层次结构或不确定性来决定随机动作。这意味着高级动作可以快速减少不确定性,因为每个后续动作的不确定性都更小。
当试图理解层级结构时,一个有帮助的例子是电影《 Groundhog Day》,由比尔·默瑞主演。在电影中,这个角色不断地经历同一天,通过尝试和错误来寻找突破路径的最佳途径。在电影中,我们可以看到这个角色尝试了数千次,甚至数百万次不同的组合,但我们看到这是以层次步骤完成的。我们首先看到这个角色在一天中疯狂地四处走动,却什么也没做成,直到他通过过去的层次动作学会了最佳可能的奖励。他意识到,通过自我提升,他在永恒中的时间变得更加愉快。最后,我们看到这个角色试图过上最好的生活,却发现他们解决了游戏,可以进入下一天。
- 你可以通过在第一层的前10层运行以下命令来训练代理:
cp save_prior.pkl save.pkl
python run_tail.py --min 0 --max 1 --path save.pkl
- 然后,为了在超过10层的楼层上训练代理,你可以使用以下方法:
cp save_prior.pkl save_tail.pkl
python run_tail.py --min 10 --max 15 --path save_tail.pkl
在OTC的每10层,游戏主题都会改变。这意味着墙壁颜色和纹理也会改变,以及需要完成的任务。正如我们之前提到的,这种视觉变化,加上3D,将使Unity OTC成为我们在首次变得足够聪明/大胆和/或勇敢地应对AGI时,最具挑战性和基准挑战之一。AGI和通过DRL走向更普遍智能的道路将是我们在第14章从DRL到AGI的关注点。
在下一节中,我们将探讨Facebook的3D世界Habitat,这更具挑战性但同样有趣。
探索栖息地 - 由FAIR的具身代理
Habitat是Facebook AI Research为新型具身智能体提出的一个相对较新的平台。这个平台代表了能够从真实世界的复杂场景中展示全3D世界的能力。该环境旨在为机器人及其类似应用提供AI研究,这些应用在未来几年可能会由DRL(深度强化学习)提供动力。公平地说,这个环境是为了训练所有类型的AI在这个环境中而实现的。当前的Habitat仓库只包含一些简单的示例和PPO的实现。
Habitat平台由两部分组成:Habitat Sim和Habitat API。模拟环境是一个全3D的强大世界,可以每秒渲染数千帧,由摄影测量RGBD数据驱动。RGBD本质上是由RGB颜色数据和深度数据组成。因此,任何图像都会有一个颜色值和深度值。这使得数据可以以超现实的方式在3D中映射,成为真实环境的超逼真表示。您可以通过在浏览器中使用Habitat本身来探索这些环境的外观,请按照下一个快速练习进行操作:
- 将您的浏览器导航到https://aihabitat.org/demo/。
Habitat目前只能在Chrome浏览器或您的桌面上运行。
- 加载应用程序可能需要一些时间,请耐心等待。当应用程序加载完成后,您将看到以下截图类似的内容:

Habitat在浏览器中运行的示例
- 使用WASD键在环境中移动。
Habitat支持从以下三个供应商导入:MatterPort3D、Gibson和Replica,他们提供捕获RGBD数据的工具和实用程序,并拥有这些数据库。现在我们了解了Habitat是什么,我们将在下一节中设置它。
安装Habitat
在撰写本文时,Habitat仍然是一个新产品,但文档工作得很好,可以轻松安装和运行用于训练的智能体。在我们的下一个练习中,我们将介绍该文档的部分内容,以在Habitat中安装和运行训练智能体:
- 打开Anaconda命令提示符并导航到一个干净的文件夹。使用以下命令下载和安装Habitat:
git clone --branch stable git@github.com:facebookresearch/habitat-sim.git
cd habitat-sim
- 然后,创建一个新的虚拟环境,并使用以下命令安装所需的依赖项:
conda create -n habitat python=3.6 cmake=3.14.0
conda activate habitat
pip install -r requirements.txt
- 接下来,我们需要使用以下命令构建Habitat Sim:
python setup.py install
-
从以下链接下载测试场景:http://dl.fbaipublicfiles.com/habitat/habitat-test-scenes.zip。
-
将场景文件解压缩到熟悉的路径,一个您可以稍后链接到的路径。这些文件是代表场景的RGBD数据集。
RGBD图像捕捉并不新鲜,传统上,它很昂贵,因为它需要移动一个装有特殊传感器的摄像头在房间内移动。幸运的是,大多数现代智能手机也具备这种深度传感器。这个深度传感器现在常用于构建增强现实应用。也许在几年后,智能体本身将被训练,仅使用简单的手机就能捕捉这些类型的图像。
- 安装完成后,我们可以通过运行以下命令来测试Habitat的安装:
python examples/example.py --scene /path/to/data/scene_datasets/habitat-test-scenes/skokloster-castle.glb
- 这将以非交互方式启动模拟并执行一些随机移动。如果您想查看或与环境交互,您将需要下载并安装存储库文档中找到的交互式插件。
模拟安装完成后,我们可以在下一节中继续安装API并训练智能体。
在Habitat中进行训练
在撰写本文时,Habitat相当新颖,但显示出巨大的潜力,尤其是在训练智能体方面。这意味着当前环境中只有简单的PPO智能体实现,您可以快速训练智能体。当然,由于Habitat使用PyTorch,您可能能够实现我们之前介绍的其他算法之一。在下一项练习中,我们将通过查看Habitat中的PPO实现及其运行方式来结束:
- 使用以下命令下载并安装Habitat API:
git clone --branch stable git@github.com:facebookresearch/habitat-api.git
cd habitat-api pip install -r requirements.txt
python setup.py develop --all
- 到目前为止,您可以使用多种方式使用API。我们首先将查看一个基本的代码示例,您可以用它来运行模拟:
import habitat
# Load embodied AI task (PointNav) and a pre-specified virtual robot
env = habitat.Env(
config=habitat.get_config("configs/tasks/pointnav.yaml")
)
observations = env.reset()
# Step through environment with random actions
while not env.episode_over:
observations = env.step(env.action_space.sample())
-
如您所见,模拟允许我们使用我们熟悉的Gym风格界面来编程智能体。
-
接下来,我们需要安装Habitat Baselines包。这个包是强化学习部分,目前提供了一个PPO的示例。这个包以OpenAI同名测试包命名。
-
使用以下命令安装Habitat Baselines包:
# be sure to cd to the habitat_baselines folder
pip install -r requirements.txt
python setup.py develop --all
- 安装完成后,您可以通过运行以下命令来运行
run.py脚本来训练一个智能体:
python -u habitat_baselines/run.py --exp-config habitat_baselines/config/pointnav/ppo_pointnav.yaml --run-type train
- 然后,您可以使用以下命令测试这个智能体:
python -u habitat_baselines/run.py --exp-config habitat_baselines/config/pointnav/ppo_pointnav.yaml --run-type eval
Habitat是一个相对较新的发展,为在真实世界环境中训练智能体/机器人打开了大门。虽然Unity和ML-Agents是训练3D游戏环境中智能体的优秀平台,但它们仍然无法与真实世界的复杂性相比。在真实世界中,物体很少完美,通常是复杂的,这使得这些环境特别难以泛化,因此难以训练。在下一节中,我们通过典型的练习来结束这一章。
练习
随着我们通过这本书的进展,练习已经从学习练习转变为几乎接近研究工作,这一章也是如此。因此,这一章的练习是为那些热衷于强化学习的人准备的,可能并不适合每个人:
-
调整ML-Agents工具包中一个样本视觉环境的超参数。
-
修改ML-Agents工具包中找到的视觉观察标准编码器,以包括额外的层或不同的内核滤波器设置。
-
使用
nature_cnn或resnet视觉编码器网络训练一个代理,并将它们的性能与使用基础视觉编码器的早期示例进行比较。 -
修改
resnet视觉编码器,以适应更多层或其他滤波器/内核尺寸的变化。 -
下载、安装并玩Unity Obstacle Tower Challenge,看看你在游戏中能走多远。在玩游戏的同时,将自己视为一个代理,反思你所采取的行动以及它们如何反映你当前的任务轨迹。
-
构建自己的算法实现,以测试Unity OTC。如果你打败了前一名获胜者的结果,完成这个挑战将特别有成就感。这个挑战仍然相对开放,任何声称达到20级以上的人可能会对未来DRL产生重大影响。
-
将Habitat Baselines模块中的PPO基础示例替换为Rainbow DQN的实现。性能比较如何?
-
为Habitat Baselines框架实现一个不同的视觉编码器。也许可以使用
nature_cnn或resnet的先前示例。 -
参加Habitat挑战。这是一个要求代理通过一系列航点完成导航任务的挑战。这当然不像OTC那么困难,但视觉环境要复杂得多。
-
Habitat更适用于传感器开发而不是视觉开发。看看你是否能够将视觉观察编码与其他传感器输入相结合,作为视觉和传感器观察输入的组合。
本章中的练习完全是可选的;请仅在您有理由这样做的情况下选择进行这些练习。由于这是一个非常复杂的领域,因此它们可能需要额外的时间来开发。
摘要
在本章中,我们探讨了3D世界的概念,不仅限于游戏,还包括现实世界。现实世界,以及更大程度上是3D世界,是DRL研究的下一个伟大前沿。我们探讨了为什么3D为DRL创造了我们尚未完全找到最佳解决方案的细微差别。然后,我们研究了使用针对3D空间调整的2D视觉观察编码器,包括Nature CNN和ResNet或残差网络的变化。之后,我们研究了Unity Obstacle Tower Challenge,这个挑战要求开发者构建一个能够解决3D多任务环境的代理。
从那里,我们研究了获胜条目使用Prierarchy;一种用于管理多个任务空间的HRL形式。我们还详细研究了代码,以了解这如何反映在获胜者修改的PPO实现中。最后,我们通过研究Habitat来结束本章;这是一个高级AI环境,使用RGBD和基于颜色的深度数据,以3D形式渲染现实世界环境。
我们几乎完成了这次旅程,在下一章和最后一章中,我们将探讨深度强化学习(DRL)是如何迈向通用人工智能,或者我们称之为AGI的方向。
第十七章:从 DRL 到 AGI
我们在这本书中的旅程是对强化和深度强化学习(DRL)演变的探索。我们查看了许多你可以用来解决各种环境中的各种问题的方法,但总的来说,我们一直坚持在一个单一的环境中;然而,DRL 的真正目标是能够构建一个能够在许多不同环境中学习的代理,一个能够在多个任务中泛化其知识的代理,就像我们动物一样。这种类型的代理,那种可以在没有人类干预的情况下跨多个任务泛化的代理,被称为人工通用智能,或 AGI。这个领域目前正在因各种原因迅速增长,并将是我们本章的重点。
在本章中,我们将探讨如何构建 DRL 的 AGI 代理。我们首先将探讨元学习或学习如何学习的概念。然后,我们将学习元学习如何应用于强化学习,并查看一个应用于 DRL 的模型无关元学习的例子。超越元学习,我们将转向后见之明经验回放,这是一种使用轨迹后见之明来提高跨任务学习的方法。接下来,我们将转向生成对抗模仿学习(GAIL),并了解其实现方式。我们将以一个新概念结束本章,这个概念正在应用于 DRL,称为想象和推理。
下面是本章我们将涵盖的主题简要概述:
-
学习元学习
-
介绍元强化学习
-
使用后见之明经验回放
-
想象和推理
-
理解增强想象力的代理
在最后一章中,我们将快速涵盖各种复杂示例。本章的每个部分都足以成为一个完整的章节或一本书。如果你对其中任何内容感兴趣,请务必在网上进行进一步研究;一些领域可能或可能没有在撰写此材料后发展。在下一节中,我们将探讨机器学习(ML)和元强化学习(MRL)。
学习元学习
“元”一词定义为“指代自身或其类型或体裁”。当谈论元学习时,我们是在谈论理解学习过程的学习——也就是说,我们不是在思考一个代理如何学习一个任务,而是想思考一个代理如何能够在多个任务中学习如何学习。这是一个既有趣又抽象的问题,因此我们首先想探索元学习是什么。在下一节中,我们将探讨机器学习如何学习如何学习。
学习如何学习
任何好的学习模型都应该在各种任务上进行训练,然后推广以适应这些任务的最佳分布。虽然我们在广义机器学习方面涉及很少,但考虑一下简单的深度学习图像分类问题。我们通常会以一个目标或任务来训练这样的模型,比如识别数据集中是否包含猫或狗,但不是两者,也没有其他。通过元学习,猫/狗数据集将是一组图像分类任务中的一个训练条目,这些任务可以覆盖广泛的任务,从识别花朵到汽车。以下示例图像进一步说明了这一概念:

将元学习应用于图像分类的例子(图片来源:Google)
因此,这个概念涉及训练模型来分类识别老鼠或驼鹿以及猫或猞猁的任务,然后通过元学习,将这一方法应用于识别老虎或熊的任务。本质上,我们以递归测试模型的方式训练模型——迭代地使用少量样本集合暴露给网络,作为泛化学习的一种方式。您可能已经听说过在元学习背景下使用的“少样本学习”这个术语,它描述了每个任务中暴露给模型的少量样本。通过这个过程更新模型的方式已被归类为以下三种当前思想流派:
-
基于度量:这些解决方案之所以被称为如此,是因为它们依赖于训练一个度量来评估和监控性能。这通常要求学习者在定义网络试图模拟的分布的核而不是显式调整网络。我们发现,使用两个相对对立的网络进行对抗性学习可以平衡和细化这种学习,以编码或嵌入的形式学习度量。这种类型方法的一些优秀例子包括用于少样本学习的卷积孪生网络、匹配网络、全上下文嵌入、关系网络和原型网络。
-
基于模型:解决方案定义了一组依赖于某种形式记忆增强或上下文的方法。记忆增强神经网络(MANN)是使用此解决方案时将找到的主要实现。这一概念进一步基于神经图灵机(NTM),它描述了一个学习从记忆-软注意力中读取和写入的控制器网络。以下NTM架构图展示了这一概念的一个例子:

神经图灵机的例子
-
NTM架构被用来为元学习提供动力。训练MANN需要对模型如何记忆和编码任务细节给予关注。这个模型通常以延长不同训练数据重新引入和记忆所需的时间的方式来训练。本质上,代理(agent)在单个任务上的训练时间会越来越长,然后在后续的训练中被迫回忆预先学习任务的记忆。有趣的是,这正是我们人类经常用来专注于学习特定复杂任务的方法。然后,我们会在之后重新测试这些知识,以加强记忆中的这些知识。这个概念在MANN中同样适用,许多人认为NTM或记忆是任何元学习模式的关键成分。
-
基于优化的:解决方案既是前两种解决方案的结合,也是一种反模式。在基于优化的问题中,我们考虑问题的根源,因此优化我们的函数问题,不仅使用梯度下降,还通过上下文或时间引入梯度下降。通过上下文或时间进行的梯度下降也被称为时间反向传播(BPTT),这是我们查看循环网络时简要提到过的。通过将循环网络或长短期记忆(LSTM)层引入网络,我们鼓励网络记住梯度上下文。另一种思考方式是,网络学习它在训练期间应用的梯度历史。因此,元学习器通过以下图示所示的过程进行训练:

使用LSTM训练元学习
该图来源于Sachin Ravi和Hugo Larochelle发表的论文《Optimization as a Model for Few-Shot Learning》,并且是原始版本的一个大幅简化版。在图中,我们可以看到元学习器是如何在常规模型之外进行训练的,通常在一个外部循环中,而内部循环则定义为单个分类、回归或其他基于学习任务的训练过程。
虽然存在三种不同的元学习形式,但我们将特别关注优化形式,特别是通过假设无模型(agnostic model)的方法,我们将在下一节中探讨。
模型无关的元学习
模型无关的元学习(MAML)被描述为一种通用的优化方法,它将适用于任何使用梯度下降进行优化或学习的机器学习方法。这里的直觉是我们希望找到一个损失近似,它能最好地匹配我们当前正在执行的任务。MAML通过在模型训练任务中添加上下文来实现这一点。这个上下文被用来细化模型训练参数,从而使我们的模型能够更好地为特定任务应用梯度损失。
这个例子使用了MNIST数据集,这是一个包含60,000个手写数字的集合,通常用于基本的图像分类任务。虽然该数据集已经通过多种方法以高精度解决,但它通常是图像分类任务的基准比较。
这可能仍然听起来很抽象,所以在下一次练习中,我们将下载一个名为learn2learn的PyTorch机器学习框架,并展示如何使用MAML:
- 我们首先创建一个新的虚拟环境,然后下载一个名为
learn2learn的包,这是一个元学习框架,它提供了PyTorch中MAML的出色实现。确保你创建一个新的环境并安装PyTorch和Gym环境,就像我们之前做的那样。你可以使用以下命令安装learn2learn:
pip install learn2learn # after installing new environment with torch
pip install tqdm # used for displaying progress
- 为了了解如何在基本任务中使用
learn2learn,我们将回顾存储库中找到的基本MNIST训练样本,但不会查看源代码Chapter_14_learn.py中提供的每个代码示例的每个部分。打开样本并查看代码的顶部部分,如下所示:
import learn2learn as l2l
class Net(nn.Module):
def __init__(self, ways=3):
super(Net, self).__init__()
self.conv1 = nn.Conv2d(1, 20, 5, 1)
self.conv2 = nn.Conv2d(20, 50, 5, 1)
self.fc1 = nn.Linear(4 * 4 * 50, 500)
self.fc2 = nn.Linear(500, ways)
def forward(self, x):
x = F.relu(self.conv1(x))
x = F.max_pool2d(x, 2, 2)
x = F.relu(self.conv2(x))
x = F.max_pool2d(x, 2, 2)
x = x.view(-1, 4 * 4 * 50)
x = F.relu(self.fc1(x))
x = self.fc2(x)
return F.log_softmax(x, dim=1)
-
这段代码的顶部部分显示了
learn2learn的import语句和Net类的定义。这是我们将要训练的网络模型。注意模型由两个卷积/池化层组成,随后是一个连接到输出层的全连接线性层。注意使用ways作为输入变量,它定义了最后一个输出层的输出数量。 -
接下来,我们将向下滚动到
main函数。这里发生所有的主设置和初始化。这个样本比大多数样本更健壮,它提供了输入参数,你可以使用这些参数而不是在代码中修改超参数。以下代码展示了main函数的顶部:
def main(lr=0.005, maml_lr=0.01, iterations=1000, ways=5, shots=1, tps=32, fas=5, device=torch.device("cpu"),
download_location="/tmp/mnist"):
transformations = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.1307,), (0.3081,)),
lambda x: x.view(1, 1, 28, 28),
])
mnist_train = l2l.data.MetaDataset(MNIST(download_location, train=True, download=True, transform=transformations))
# mnist_test = MNIST(file_location, train=False, download=True, transform=transformations)
train_gen = l2l.data.TaskGenerator(mnist_train, ways=ways, tasks=10000)
# test_gen = l2l.data.TaskGenerator(mnist_test, ways=ways)
model = Net(ways)
model.to(device)
meta_model = l2l.algorithms.MAML(model, lr=maml_lr)
opt = optim.Adam(meta_model.parameters(), lr=lr)
loss_func = nn.NLLLoss(reduction="sum")
-
虽然我们之前没有通过图像分类示例,但希望代码对你来说相对容易理解且熟悉。需要注意的是,在代码高亮行使用
l2l.algorithms.MAML模型构建meta_model。注意meta_model是如何通过将其作为输入来包装model网络的。 -
从这里,我们将向下滚动到我们之前多次见过的熟悉的训练循环。然而,这次有一些有趣的不同之处。具体查看第一个迭代循环内部的代码,如下所示:
iteration_error = 0.0
iteration_acc = 0.0
for _ in range(tps):
learner = meta_model.clone()
train_task = train_gen.sample()
valid_task = train_gen.sample(task=train_task.sampled_task)
注意我们是如何构建一个meta_model学习器的learner克隆。这个learner克隆变成了我们的目标学习网络。最后两行展示了为训练和验证任务构建采样器的过程。
- 接下来,让我们看看如何使用
learner在另一个循环中以迭代方式再次计算损失,如下所示:
for step in range(fas):
train_error, _ = compute_loss(train_task, device, learner, loss_func, batch=shots * ways)
learner.adapt(train_error)
- 在这个阶段,运行样本并观察输出,以了解训练是如何进行的。
现在我们已经理解了一些基本的代码设置,我们将继续探索如何在下一节中探索样本的训练和损失计算。
训练元学习器
learn2learn框架提供了MAML框架,用于构建我们可以用来学习如何学习的学习者模型;然而,它不是自动的,并且确实需要一些设置和思考,关于如何计算特定任务集的损失。我们已经看到了我们计算损失的地方——现在我们将更详细地看看如何在任务之间计算损失。重新打开Chapter_14_learn.py并完成以下练习:
-
滚动回
main函数中最内层的训练循环。 -
这里的内部循环被称为快速自适应训练循环,因为我们向网络展示了一些或小批量或数据样本进行训练。计算网络的损失是通过
compute_loss函数完成的,如下面的代码所示:
def compute_loss(task, device, learner, loss_func, batch=5):
loss = 0.0
acc = 0.0
dataloader = DataLoader(task, batch_size=batch, shuffle=False, num_workers=0)
for i, (x, y) in enumerate(dataloader):
x, y = x.squeeze(dim=1).to(device), y.view(-1).to(device)
output = learner(x)
curr_loss = loss_func(output, y)
acc += accuracy(output, y)
loss += curr_loss / x.size(0)
loss /= len(dataloader)
return loss, acc
-
注意损失是如何通过迭代任务训练批次来计算的,通过迭代
dataloader列表。然后我们通过将总损失loss除以数据加载器的数量来计算所有任务的平均损失。 -
这个平均
loss和准确度acc由compute_loss函数返回。从这个学习实例中,学习者随后使用以下代码行进行适应或更新:
train_error, _ = compute_loss(train_task, device, learner, loss_func, batch=shots * ways)
learner.adapt(train_error)
- 在快速自适应循环和通过每个循环更新学习者之后,我们可以使用以下代码验证学习者:
valid_error, valid_acc = compute_loss(valid_task, device, learner, loss_func, batch=shots * ways)
iteration_error += valid_error
iteration_acc += valid_acc
-
valid_error验证错误和valid_acc准确度随后累计在总的iteration_error错误和iteration_acc准确度值上。 -
我们通过以下代码计算平均迭代和准确度误差,
iteration_error或iteration_acc值,然后将该误差反向传播到网络中:
iteration_error /= tps
iteration_acc /= tps
tqdm_bar.set_description("Loss : {:.3f} Acc : {:.3f}".format(iteration_error.item(), iteration_acc))
# Take the meta-learning step
opt.zero_grad()
iteration_error.backward()
opt.step()
- 这个示例的训练相当快,所以再次运行示例并观察算法在元学习任务上的训练速度有多快。
每个元学习步骤都涉及使用BPTT将损失反向推回网络,因为元网络由循环层组成。这个细节在这里被抽象化,但希望你能欣赏我们如何无缝地将元学习引入到训练这个常规图像分类任务中。在下一节中,我们将看看如何将元学习应用于强化学习。
介绍元强化学习
现在,我们理解了元学习的概念,我们可以继续探讨元强化学习。元-RL——或者称为RL^2(RL平方),正如它被称呼的那样——正在迅速发展,但额外的复杂性仍然使得这种方法目前难以接触。虽然概念与原始元学习非常相似,但它仍然为RL引入了许多细微差别。其中一些可能难以理解,因此希望以下图表能有所帮助。它来自一篇题为《强化学习,快与慢》的论文,作者为Botvinick等人,2019年(https://www.cell.com/action/showPdf?pii=S1364-6613%2819%2930061-0):

元强化学习
在图中,你可以看到典型的元学习特征,即熟悉的内外循环。这意味着我们也在元-RL中从评估任何观察到的状态的政策,现在也包括最后一步、最后奖励和观察到的状态。这些差异总结如下:
-
强化学习 =
分布在
上 -
元强化学习 =
分布在
上
正如我们所看到的常规元学习,元-RL中有许多变体被使用和实验,但它们都共享以下三个共同元素:
-
模型记忆:我们通过循环网络层或LSTM的形式向我们的模型添加记忆。通常,外循环由LSTM层激活的记忆组件组成。
-
MDPs的分布:代理/算法需要在多个不同的MDPs上进行训练,这通常是通过将其暴露于不同的或随机化的环境中来完成的。
-
元学习算法:代理需要一种元学习算法来学习如何学习。
Unity Obstacle Tower Challenge很可能是为了鼓励开发者构建元-RL代理而开发的,但正如我们所看到的,获胜的条目使用了分层强化学习的变体。虽然HRL旨在完成与元-RL相同的功能,但它缺乏自动生成记忆的能力。
为了了解元-RL算法的多样性,我们将查看一个似乎是最新的方法列表:
-
优化权重:这本质上上是MAML或另一个称为Reptile的变体。MAML是目前使用较为流行的变体之一,我们将在稍后详细探讨。
-
元学习超参数:我们有几个内部使用的超参数来平衡RL中的学习。这些是我们之前调整过的gamma和alpha值,但想象一下,如果它们可以通过元学习来自动调整。
-
元学习损失:这考虑了损失函数本身可能需要调整,并使用一种模式在迭代中进化它。这种方法使用的是本书范围之外的进化策略。
-
元学习探索:这使用元学习来构建更有效的探索策略。这反过来又减少了探索所需的时间,并提高了有效的训练性能。
-
周期性控制:这为智能体提供了一种方法来保持重要的周期在记忆中,并忘记其他周期。这听起来很像优先经验回放,但这里的控制方法是在损失计算中,而不是从回放中。
-
进化算法:这些是梯度无关的、基于优化的解决方案,它们使用一种遗传搜索形式来找到解决方案方法。进化算法与深度学习的碰撞是一个持续的努力,许多人尝试过并失败了。这两种方法本身都非常强大和有能力,因此它们可能很快就会结合成一个有效的模型。
如您所见,元RL方法有很多变化,我们将在下一节中详细查看一种方法的实现。
MAML-RL
learn2learn存储库包含如何使用他们的库来实现这种方法的一些变体的另一个很好的例子。我们将要查看的一个好方法是Meta-SGD的实现,它通过采用vanilla策略梯度来使用每个参数的学习率进一步扩展了MAML,通常被称为MetaSGD-VPG。这个概念最初在论文《带有任务嵌入和共享策略的元强化学习》中提出,该论文本身是在IJCAI-19上提出的。
在继续之前,请确保您已经完成了上一个练习中的所有安装步骤。如果您在运行示例时遇到问题,请在新虚拟环境中重新安装。一些问题可能与您使用的PyTorch版本有关,因此请检查您的版本是否兼容。
打开Chapter_14_MetaSGD-VPG.py并按照以下步骤进行:
- 您需要首先通过在您的虚拟环境窗口中输入以下命令来安装cherry RL包:
pip install cherry-rl
- 我们不会审查整个代码列表,只审查关键部分。首先,让我们看看
main函数,它启动初始化并托管训练。此函数的开始部分如下所示:
def main(
experiment='dev',
env_name='Particles2D-v1',
adapt_lr=0.1,
meta_lr=0.01,
adapt_steps=1,
num_iterations=200,
meta_bsz=20,
adapt_bsz=20,
tau=1.00,
gamma=0.99,
num_workers=2,
seed=42,
):
random.seed(seed)
np.random.seed(seed)
th.manual_seed(seed)
def make_env():
return gym.make(env_name)
在main函数的定义中,我们可以看到所有相关的超参数以及它们选择的默认值。请注意,用于适应和元学习步骤的两个新超参数组分别以前缀adapt和meta开头。
- 接下来,我们将使用以下代码查看环境的初始化、策略、元学习者和优化器的初始化:
env = l2l.gym.AsyncVectorEnv([make_env for _ in range(num_workers)])
env.seed(seed)
env = ch.envs.Torch(env)
policy = DiagNormalPolicy(env.state_size, env.action_size)
meta_learner = l2l.algorithms.MetaSGD(policy, lr=meta_lr)
baseline = LinearValue(env.state_size, env.action_size)
opt = optim.Adam(policy.parameters(), lr=meta_lr)
all_rewards = []
- 在这里,我们可以看到三个训练循环。首先,外层迭代循环控制元学习的重复次数。在这个循环内部,我们有任务设置和配置循环;记住,我们希望每个学习会话都需要一个不同但相关的任务。第三个、最内层的循环是自适应发生的地方,我们将损失反向传递通过模型。所有三个循环的代码如下所示:
for iteration in range(num_iterations):
iteration_loss = 0.0
iteration_reward = 0.0
for task_config in tqdm(env.sample_tasks(meta_bsz)):
learner = meta_learner.clone()
env.set_task(task_config)
env.reset()
task = ch.envs.Runner(env)
# Fast Adapt
for step in range(adapt_steps):
train_episodes = task.run(learner, episodes=adapt_bsz)
loss = maml_a2c_loss(train_episodes, learner, baseline, gamma, tau)
learner.adapt(loss)
- 在快速自适应循环完成后,我们然后回到第二个循环并使用以下代码计算验证损失:
valid_episodes = task.run(learner, episodes=adapt_bsz)
loss = maml_a2c_loss(valid_episodes, learner, baseline, gamma, tau)
iteration_loss += loss
iteration_reward += valid_episodes.reward().sum().item() / adapt_bsz
- 验证损失是在第二个循环中为每个不同的任务计算的。然后,这个损失被累积到迭代损失
iteration_loss中。离开第二个循环后,我们打印出一些统计数据并计算自适应损失adaption_loss,并使用以下代码将这个损失作为梯度反向传递通过网络进行训练:
adaptation_loss = iteration_loss / meta_bsz
print('adaptation_loss', adaptation_loss.item())
opt.zero_grad()
adaptation_loss.backward()
opt.step()
- 记住,在损失方程(迭代和自适应)中的除数都使用了一个相似的值
20,meta_bsz= 20,和adapt_bsz = 20。基本损失函数由maml_a2c_loss和compute_advantages函数定义,如下面的代码所示:
def compute_advantages(baseline, tau, gamma, rewards, dones, states, next_states):
# Update baseline
returns = ch.td.discount(gamma, rewards, dones)
baseline.fit(states, returns)
values = baseline(states)
next_values = baseline(next_states)
bootstraps = values * (1.0 - dones) + next_values * dones
next_value = th.zeros(1, device=values.device)
return ch.pg.generalized_advantage(tau=tau,
gamma=gamma,
rewards=rewards,
dones=dones,
values=bootstraps,
next_value=next_value)
def maml_a2c_loss(train_episodes, learner, baseline, gamma, tau):
states = train_episodes.state()
actions = train_episodes.action()
rewards = train_episodes.reward()
dones = train_episodes.done()
next_states = train_episodes.next_state()
log_probs = learner.log_prob(states, actions)
advantages = compute_advantages(baseline, tau, gamma, rewards,
dones, states, next_states)
advantages = ch.normalize(advantages).detach()
return a2c.policy_loss(log_probs, advantages)
注意樱桃RL库如何帮助我们避免了某些复杂代码的实现。幸运的是,我们应该已经知道樱桃函数 ch.td.discount 和 ch.pg.generalized_advantage 是什么,因为我们已经在之前的章节中遇到过它们,所以我们在这里不需要回顾它们。
- 按照正常方式运行示例并观察输出。以下代码展示了生成的输出示例:

来自第14章 Chapter_14_MetaSQG-VPG.py 的示例输出
注意样本在首次运行于CPU时预期的训练时间量。当预测在不到一个小时的时间内从五天减少到略超过两天时,它仍然展示了此类训练的计算需求。因此,如果你计划进行任何严肃的元强化学习(meta-RL)工作,你可能会想使用一个非常快的GPU进行训练。在非常快的GPU上进行测试时,前面的样本处理时间减少了1,000倍。是的,你读对了,1,000倍。虽然你可能不会体验到如此巨大的差异,但从CPU升级到GPU的任何提升都将非常显著。
在强化学习(RL)社区中,许多人坚信元强化学习(meta-RL)是我们需要解决以接近通用人工智能(AGI)的下一个重大飞跃。这个领域的大部分发展仍然由当前最先进的技术所指导,以及如何和何时变化将决定RL的未来。考虑到这一点,我们将探讨一些其他潜在的下一步,从下一节中的HER开始。
使用事后经验重放
OpenAI引入了回溯经验重放作为处理稀疏奖励的方法,但该算法也已被证明能够成功地在任务间泛化,部分原因是HER工作的新颖机制。用来解释HER的类比是一个保龄球游戏,其目的是将一个圆盘滑过一张长桌以达到目标。当我们刚开始学习这个游戏时,我们经常会反复失败,圆盘掉落桌子或游戏区域。但是,我们假设我们通过预期失败并在失败时给自己奖励来学习。然后,在内部,我们可以通过减少失败奖励来反向工作,从而增加其他非失败奖励。在某种程度上,这种方法类似于层次强化学习(我们之前看过的HRL的一种形式),但没有广泛的预训练部分。
下面的部分样本再次来源于https://github.com/higgsfield,是来自哈萨克斯坦阿拉木图的一名年轻人Dulat Yerzat的结果。
打开Chapter_14_wo_HER.py和Chapter_14_HER.py的样本。这两个样本是应用了和未应用HER的简单DQN网络的比较。按照以下步骤进行:
- 这两个示例几乎相同,除了HER的实现,所以比较将帮助我们理解代码的工作方式。接下来,环境已经被简化并定制构建,以执行简单的随机位移动操作。创建环境的代码如下:
class Env(object):
def __init__(self, num_bits):
self.num_bits = num_bits
def reset(self):
self.done = False
self.num_steps = 0
self.state = np.random.randint(2, size=self.num_bits)
self.target = np.random.randint(2, size=self.num_bits)
return self.state, self.target
def step(self, action):
if self.done:
raise RESET
self.state[action] = 1 - self.state[action]
if self.num_steps > self.num_bits + 1:
self.done = True
self.num_steps += 1
if np.sum(self.state == self.target) == self.num_bits:
self.done = True
return np.copy(self.state), 0, self.done, {}
else:
return np.copy(self.state), -1, self.done, {}
- 我们从未真正讲解过如何构建自定义环境,但正如你所见,它可以相当简单。接下来,我们将查看我们将用于训练的简单DQN模型,如下面的代码所示:
class Model(nn.Module):
def __init__(self, num_inputs, num_outputs, hidden_size=256):
super(Model, self).__init__()
self.linear1 = nn.Linear(num_inputs, hidden_size)
self.linear2 = nn.Linear(hidden_size, num_outputs)
def forward(self, state, goal):
x = torch.cat([state, goal], 1)
x = F.relu(self.linear1(x))
x = self.linear2(x)
return x
- 这就是你可以得到的简单DQN模型。接下来,让我们通过并排查看代码来比较这两个示例,如下面的截图所示:

VS中的代码示例比较
- 新的代码部分也在这里显示:
new_episode = []
for state, reward, done, next_state, goal in episode:
for t in np.random.choice(num_bits, new_goals):
try:
episode[t]
except:
continue
new_goal = episode[t][-2]
if np.sum(next_state == new_goal) == num_bits:
reward = 0
else:
reward = -1
replay_buffer.push(state, action, reward, next_state, done, new_goal)
new_episode.append((state, reward, done, next_state, new_goal))
-
我们在这里看到的是添加了另一个循环,这与元强化学习类似,但这次它作为一个兄弟存在。第二个循环在第一个内部循环完成一个回合后激活。然后,它遍历前一个回合中的每个事件,并根据新的目标调整目标或目标,基于返回的奖励。这本质上就是回溯部分。
-
这个例子剩余的部分与我们之前的许多例子相似,现在应该已经很熟悉了。但有趣的部分是
get_action函数,如下面的代码所示:
def get_action(model, state, goal, epsilon=0.1):
if random.random() < 0.1:
return random.randrange(env.num_bits)
state = torch.FloatTensor(state).unsqueeze(0).to(device)
goal = torch.FloatTensor(goal).unsqueeze(0).to(device)
q_value = model(state, goal)
return q_value.max(1)[1].item()
注意,这里我们使用了一个默认为.1的epsilon值,表示探索的倾向。实际上,你可能注意到这个例子没有使用变量探索。
- 继续探讨差异,下一个关键差异是
compute_td_loss函数,如下面的代码所示:
def compute_td_error(batch_size):
if batch_size > len(replay_buffer):
return None
state, action, reward, next_state, done, goal = replay_buffer.sample(batch_size)
state = torch.FloatTensor(state).to(device)
reward = torch.FloatTensor(reward).unsqueeze(1).to(device)
action = torch.LongTensor(action).unsqueeze(1).to(device)
next_state = torch.FloatTensor(next_state).to(device)
goal = torch.FloatTensor(goal).to(device)
mask = torch.FloatTensor(1 - np.float32(done)).unsqueeze(1).to(device)
q_values = model(state, goal)
q_value = q_values.gather(1, action)
next_q_values = target_model(next_state, goal)
target_action = next_q_values.max(1)[1].unsqueeze(1)
next_q_value = target_model(next_state, goal).gather(1, target_action)
expected_q_value = reward + 0.99 * next_q_value * mask
loss = (q_value - expected_q_value.detach()).pow(2).mean()
optimizer.zero_grad()
loss.backward()
optimizer.step()
return loss
- 首先运行不带HER的示例并观察结果,然后运行带有HER的示例。带有HER的示例的输出如下所示:

从Chapter_14_HER.py章节的示例输出
与没有HER的示例相比,前面的输出明显更好。您需要亲自运行这两个示例以查看确切差异。注意损失的计算始终保持一致且没有收敛,而平均奖励却在增加。在下一节中,我们将转向RL领域预期的下一波浪潮——想象和推理。
RL中的想象和推理
从我们自己的学习经验中,我们可以观察到想象如何有助于学习过程。纯想象是深层次抽象思维和梦想的素材,通常比解决实际问题的任何方法更接近于幻觉。然而,这种相同的想象力可以用来填补我们对知识的理解中的空白,并允许我们推理出可能的解决方案。比如说,我们正在尝试解决拼图的问题,而我们只有三个剩余的、大部分是黑色的碎片,如下面的图像所示:

想象三个缺失的拼图碎片可能的样子
由于前面的图示非常简单,我们很容易想象那些拼图碎片可能的样子。我们能够利用之前的观察和推理的想象力轻松填补这些空白。这种使用想象力填补空白的方法是我们经常使用的,人们常说,想象力越丰富,智力也越高。现在,这条通往AI的道路是否真的能证明这一理论,还有待观察,但它确实看起来是一个可能性。
想象并非凭空而来,同样地,为了给我们的代理赋予想象,我们必须基本上引导他们的记忆或之前的经验。我们将在下一项练习中这样做,以便我们能够从这些经验中生成想象。打开样本Chapter_14_Imagine_A2C.py并按照以下步骤进行:
- 我们将使用一个简单的A2C Vanilla PG方法作为生成我们想象训练引导的基础代理。让我们首先在文件中向下滚动,看看定义我们的代理的
ActorCritic类:
class ActorCritic(OnPolicy):
def __init__(self, in_shape, num_actions):
super(ActorCritic, self).__init__()
self.in_shape = in_shape
self.features = nn.Sequential(
nn.Conv2d(in_shape[0], 16, kernel_size=3, stride=1),
nn.ReLU(),
nn.Conv2d(16, 16, kernel_size=3, stride=2),
nn.ReLU(),
)
self.fc = nn.Sequential(
nn.Linear(self.feature_size(), 256),
nn.ReLU(),
)
self.critic = nn.Linear(256, 1)
self.actor = nn.Linear(256, num_actions)
def forward(self, x):
x = self.features(x)
x = x.view(x.size(0), -1)
x = self.fc(x)
logit = self.actor(x)
value = self.critic(x)
return logit, value
def feature_size(self):
return self.features(autograd.Variable(torch.zeros(1, *self.in_shape))).view(1, -1).size(1)
- 我们可以看到一个简单的PG代理,它将由A2C同步的actor-critic提供动力。接下来,我们来到另一个新类,称为
RolloutStorage。Rollout storage在概念上与经验回放相似,但它还使我们能够进行持续的回报计算,如下面的代码所示:
class RolloutStorage(object):
def __init__(self, num_steps, num_envs, state_shape):
self.num_steps = num_steps
self.num_envs = num_envs
self.states = torch.zeros(num_steps + 1, num_envs, *state_shape)
self.rewards = torch.zeros(num_steps, num_envs, 1)
self.masks = torch.ones(num_steps + 1, num_envs, 1)
self.actions = torch.zeros(num_steps, num_envs, 1).long()
#self.use_cuda = False
def cuda(self):
#self.use_cuda = True
self.states = self.states.cuda()
self.rewards = self.rewards.cuda()
self.masks = self.masks.cuda()
self.actions = self.actions.cuda()
def insert(self, step, state, action, reward, mask):
self.states[step + 1].copy_(state)
self.actions[step].copy_(action)
self.rewards[step].copy_(reward)
self.masks[step + 1].copy_(mask)
def after_update(self):
self.states[0].copy_(self.states[-1])
self.masks[0].copy_(self.masks[-1])
def compute_returns(self, next_value, gamma):
returns = torch.zeros(self.num_steps + 1, self.num_envs, 1)
#if self.use_cuda:
# returns = returns.cuda()
returns[-1] = next_value
for step in reversed(range(self.num_steps)):
returns[step] = returns[step + 1] * gamma * self.masks[step + 1] + self.rewards[step]
return returns[:-1]
- 如果我们滚动到
main函数,我们可以看到有16个同步环境正在以下代码的运行下进行:
def main():
mode = "regular"
num_envs = 16
def make_env():
def _thunk():
env = MiniPacman(mode, 1000)
return env
return _thunk
envs = [make_env() for i in range(num_envs)]
envs = SubprocVecEnv(envs)
state_shape = envs.observation_space.shape
- 我们将在稍后更多地讨论
RolloutStorage类。现在,向下移动到代码的训练部分。这是典型的双层循环代码,外部循环控制剧集,内部循环控制步骤,如下面的代码所示:
for i_update in range(num_frames):
for step in range(num_steps):
action = actor_critic.act(autograd.Variable(state))
其余的训练代码应该很熟悉,但你应该自己详细复习。
- 我们想要观察的下一个主要区别是在外部训练循环的末尾。这段最后的代码块是计算损失并将其推回网络的地方:
optimizer.zero_grad()
loss = value_loss * value_loss_coef + action_loss - entropy * entropy_coef
loss.backward()
nn.utils.clip_grad_norm(actor_critic.parameters(), max_grad_norm)
optimizer.step()
- 注意前面代码块中突出显示的行。这是独特的,因为我们正在将梯度裁剪到可能避免爆炸梯度的最大值。代码的最后部分渲染出游戏区域,并显示代理玩游戏。
爆炸梯度是指梯度值变得如此之大,以至于它使网络忘记了知识。网络权重开始进行剧烈波动,任何以前的知识通常会丢失。
- 按照正常方式运行代码并观察输出。
运行前面的代码还会创建一个保存状态字典的记忆,我们将用它来填充想象。如果你想继续进行后续练习,你必须完成这个最后的练习。在下一节中,我们将探讨如何使用这些潜在痕迹来生成代理的想象。
生成想象
在这个算法的当前版本中,我们首先需要通过一个代理或可能是一个人的先前运行来引导我们在代理中填充的记忆。这实际上与模仿学习或行为克隆没有太大区别,只是我们使用的是一个我们将后来用作想象离策略基础的在线策略代理。在我们将想象结合到代理之前,我们可以看到预测的下一个状态与代理的实际状态相比将是什么样子。让我们通过打开下一个示例Chapter_14_Imagination.py并执行以下步骤来了解这是如何工作的:
-
这个例子通过加载我们在上一个练习中生成的上一个保存状态字典来工作。在继续之前,请确保这些数据是在同一文件夹中带有前缀为
actor_critic_的文件中生成并保存的。 -
这段代码的目的是提取我们之前记录的保存状态观察字典。然后我们想要提取观察结果,并使用它来想象下一个状态将是什么样子。然后我们可以比较想象的状态和下一个状态之间的相似程度。这种相似程度将反过来用于训练想象损失。我们可以通过查看以下代码行来了解如何加载先前的模型:
actor_critic.load_state_dict(torch.load("actor_critic_" + mode))
- 上一行代码重新加载了我们之前训练的模型。现在我们想使用想象力(例如)合理地填补代理可能没有探索的区域。向下滚动,我们可以看到将学习代理的想象力部分的训练循环:
for frame_idx, states, actions, rewards, next_states, dones in play_games(envs, num_updates):
states = torch.FloatTensor(states)
actions = torch.LongTensor(actions)
batch_size = states.size(0)
onehot_actions = torch.zeros(batch_size, num_actions, *state_shape[1:])
onehot_actions[range(batch_size), actions] = 1
inputs = autograd.Variable(torch.cat([states, onehot_actions], 1))
- 此循环遍历之前玩过的游戏,并使用独热编码对动作进行编码。向下滚动,我们可以看到如何学习
imagined_state状态和imagined_reward奖励:
imagined_state, imagined_reward = env_model(inputs)
target_state = pix_to_target(next_states)
target_state = autograd.Variable(torch.LongTensor(target_state))
target_reward = rewards_to_target(mode, rewards)
target_reward = autograd.Variable(torch.LongTensor(target_reward))
optimizer.zero_grad()
image_loss = criterion(imagined_state, target_state)
reward_loss = criterion(imagined_reward, target_reward)
loss = image_loss + reward_coef * reward_loss
loss.backward()
optimizer.step()
losses.append(loss.item())
all_rewards.append(np.mean(rewards))
这是学习从之前观察到的观察中正确想象目标状态和奖励的代码部分。当然,观察越多,想象力越好,但到了某个点,过多的观察将完全消除所有想象力。平衡这种新的权衡将需要一些自己的尝试和错误。
- 向文件底部滚动,您可以看到以下代码输出的想象力和目标状态的示例:
while not done:
steps += 1
actions = get_action(state)
onehot_actions = torch.zeros(batch_size, num_actions, *state_shape[1:])
onehot_actions[range(batch_size), actions] = 1
state = torch.FloatTensor(state).unsqueeze(0)
inputs = autograd.Variable(torch.cat([state, onehot_actions], 1))
imagined_state, imagined_reward = env_model(inputs)
imagined_state = F.softmax(imagined_state)
iss.append(imagined_state)
next_state, reward, done, _ = env.step(actions[0])
ss.append(state)
state = next_state
imagined_image = target_to_pix(imagined_state.view(batch_size, -1, len(pixels))[0].max(1)[1].data.cpu().numpy())
imagined_image = imagined_image.reshape(15, 19, 3)
state_image = torch.FloatTensor(next_state).permute(1, 2, 0).cpu().numpy()
plt.figure(figsize=(10,3))
plt.subplot(131)
plt.title("Imagined")
plt.imshow(imagined_image)
plt.subplot(132)
plt.title("Actual")
plt.imshow(state_image)
plt.show()
time.sleep(0.3)
if steps > 30:
break
- 以下示例截图展示了原始作者通过长时间训练代理所能达到的最佳效果:

想象与实际比较示例
-
运行示例并查看您的输出:根据您之前的训练量,它可能看起来并不那么好。再次强调,想象力的质量将基于先前的经验和训练量来完善想象力本身。
-
最后要注意的一点是如何提取想象图像。这是通过在
BasicBlock类中使用反转CNN来完成的,它将编码转换回正确分辨率的图像。BasicBlock类的代码在此处显示:
class BasicBlock(nn.Module):
def __init__(self, in_shape, n1, n2, n3):
super(BasicBlock, self).__init__()
self.in_shape = in_shape
self.n1 = n1
self.n2 = n2
self.n3 = n3
self.maxpool = nn.MaxPool2d(kernel_size=in_shape[1:])
self.conv1 = nn.Sequential(
nn.Conv2d(in_shape[0] * 2, n1, kernel_size=1, stride=2, padding=6),
nn.ReLU(),
nn.Conv2d(n1, n1, kernel_size=10, stride=1, padding=(5, 6)),
nn.ReLU(),
)
self.conv2 = nn.Sequential(
nn.Conv2d(in_shape[0] * 2, n2, kernel_size=1),
nn.ReLU(),
nn.Conv2d(n2, n2, kernel_size=3, stride=1, padding=1),
nn.ReLU(),
)
self.conv3 = nn.Sequential(
nn.Conv2d(n1 + n2, n3, kernel_size=1),
nn.ReLU()
)
def forward(self, inputs):
x = self.pool_and_inject(inputs)
x = torch.cat([self.conv1(x), self.conv2(x)], 1)
x = self.conv3(x)
x = torch.cat([x, inputs], 1)
return x
def pool_and_inject(self, x):
pooled = self.maxpool(x)
tiled = pooled.expand((x.size(0),) + self.in_shape)
out = torch.cat([tiled, x], 1)
return out
如我们所见,训练想象力过程本身并不困难。真正的困难是将这一切整合到一个运行代理中,我们将在下一节中了解如何做到这一点,当我们学习I2A时。
理解增强想象力代理
增强想象力代理(I2A)的概念于2018年2月由T. Weber等人发表在题为《Imagination-Augmented Agents for Deep Reinforcement Learning》的论文中提出。我们之前已经讨论了为什么想象力对学习和学习学习很重要。想象力使我们能够填补学习中的空白,并在我们的知识上实现飞跃。
给予智能体想象能力,使我们能够结合基于模型和无模型的学习。我们在这本书中使用的多数智能体算法都是无模型的,这意味着我们没有环境的代表模型。早期,我们确实覆盖了基于模型的RL,包括MC和DP,但我们的大部分努力都集中在无模型智能体上。拥有环境模型的好处是智能体可以规划。没有模型,我们的智能体就只是通过试错尝试变得反应性。增加想象能力使我们能够在无模型的同时结合使用环境模型的一些方面。本质上,我们希望通过想象实现两者的最佳结合。
我们已经探讨了想象在I2A架构中的核心作用。这是我们上一节中查看的部分,它生成了想象特征和奖励,本质上属于模型部分。以下图展示了I2A架构、想象核心部分和 rollout 编码器:

I2A架构总结
I2A架构展示了我们可以构建在DRL之上的系统的复杂性,以期望添加额外的学习优势,例如想象。为了真正理解这个架构,我们应该查看一个代码示例。打开Chapter_14_I2A.py并按照以下步骤进行:
- 我们已经涵盖了架构的第一部分,所以在这个阶段,我们可以从策略本身开始。看看I2A策略类:
class I2A(OnPolicy):
def __init__(self, in_shape, num_actions, num_rewards, hidden_size, imagination, full_rollout=True):
super(I2A, self).__init__()
self.in_shape = in_shape
self.num_actions = num_actions
self.num_rewards = num_rewards
self.imagination = imagination
self.features = nn.Sequential(
nn.Conv2d(in_shape[0], 16, kernel_size=3, stride=1),
nn.ReLU(),
nn.Conv2d(16, 16, kernel_size=3, stride=2),
nn.ReLU(),
)
self.encoder = RolloutEncoder(in_shape, num_rewards, hidden_size)
if full_rollout:
self.fc = nn.Sequential(
nn.Linear(self.feature_size() + num_actions * hidden_size, 256),
nn.ReLU(),
)
else:
self.fc = nn.Sequential(
nn.Linear(self.feature_size() + hidden_size, 256),
nn.ReLU(),
)
self.critic = nn.Linear(256, 1)
self.actor = nn.Linear(256, num_actions)
def forward(self, state):
batch_size = state.size(0)
imagined_state, imagined_reward = self.imagination(state.data)
hidden = self.encoder(autograd.Variable(imagined_state), autograd.Variable(imagined_reward))
hidden = hidden.view(batch_size, -1)
state = self.features(state)
state = state.view(state.size(0), -1)
x = torch.cat([state, hidden], 1)
x = self.fc(x)
logit = self.actor(x)
value = self.critic(x)
return logit, value
def feature_size(self):
return self.features(autograd.Variable(torch.zeros(1, *self.in_shape))).view(1, -1).size(1)
-
在大多数情况下,这是一个相当简单的PG策略,除了增加了想象元素。注意在
forward函数中,前向传递指的是提取imagined_state和imagined_reward值所需的想象。 -
接下来,我们再向下滚动一点,来到
ImaginationCore类。这个类封装了我们之前看到的功能,但全部封装在一个单独的类中,如下面的代码所示:
class ImaginationCore(object):
def __init__(self, num_rolouts, in_shape, num_actions, num_rewards, env_model, distil_policy, full_rollout=True):
self.num_rolouts = num_rolouts
self.in_shape = in_shape
self.num_actions = num_actions
self.num_rewards = num_rewards
self.env_model = env_model
self.distil_policy = distil_policy
self.full_rollout = full_rollout
def __call__(self, state):
state = state.cpu()
batch_size = state.size(0)
rollout_states = []
rollout_rewards = []
if self.full_rollout:
state = state.unsqueeze(0).repeat(self.num_actions, 1, 1, 1, 1).view(-1, *self.in_shape)
action = torch.LongTensor([[i] for i in range(self.num_actions)]*batch_size)
action = action.view(-1)
rollout_batch_size = batch_size * self.num_actions
else:
action = self.distil_policy.act(autograd.Variable(state, volatile=True))
action = action.data.cpu()
rollout_batch_size = batch_size
for step in range(self.num_rolouts):
onehot_action = torch.zeros(rollout_batch_size, self.num_actions, *self.in_shape[1:])
onehot_action[range(rollout_batch_size), action] = 1
inputs = torch.cat([state, onehot_action], 1)
imagined_state, imagined_reward = self.env_model(autograd.Variable(inputs, volatile=True))
imagined_state = F.softmax(imagined_state).max(1)[1].data.cpu()
imagined_reward = F.softmax(imagined_reward).max(1)[1].data.cpu()
imagined_state = target_to_pix(imagined_state.numpy())
imagined_state = torch.FloatTensor(imagined_state).view(rollout_batch_size, *self.in_shape)
onehot_reward = torch.zeros(rollout_batch_size, self.num_rewards)
onehot_reward[range(rollout_batch_size), imagined_reward] = 1
rollout_states.append(imagined_state.unsqueeze(0))
rollout_rewards.append(onehot_reward.unsqueeze(0))
state = imagined_state
action = self.distil_policy.act(autograd.Variable(state, volatile=True))
action = action.data.cpu()
return torch.cat(rollout_states), torch.cat(rollout_rewards)
- 现在我们已经看到了这些大型组件是如何工作的,是时候进入
main函数了。我们将从查看代码的前十几行开始:
envs = [make_env() for i in range(num_envs)]
envs = SubprocVecEnv(envs)
state_shape = envs.observation_space.shape
num_actions = envs.action_space.n
num_rewards = len(task_rewards[mode])
full_rollout = True
env_model = EnvModel(envs.observation_space.shape, num_pixels, num_rewards)
env_model.load_state_dict(torch.load("env_model_" + mode))
distil_policy = ActorCritic(envs.observation_space.shape, envs.action_space.n)
distil_optimizer = optim.Adam(distil_policy.parameters())
imagination = ImaginationCore(1, state_shape, num_actions, num_rewards, env_model, distil_policy, full_rollout=full_rollout)
actor_critic = I2A(state_shape, num_actions, num_rewards, 256, imagination, full_rollout=full_rollout)
注意代码的流程。代码从实例化环境模型env_model和distil_policy,来自ActorCritic类开始。然后代码设置优化器,并实例化ImaginationCore类型的imagination对象,输入为env_model和distil_policy。最后一行使用imagination对象作为输入创建actor_critic I2A策略。
- 跳转到训练循环。注意它看起来相当标准:
for i_update in tqdm(range(num_frames)):
for step in range(num_steps):
action = actor_critic.act(autograd.Variable(current_state))
next_state, reward, done, _ = envs.step(action.squeeze(1).cpu().data.numpy())
reward = torch.FloatTensor(reward).unsqueeze(1)
episode_rewards += reward
masks = torch.FloatTensor(1-np.array(done)).unsqueeze(1)
final_rewards *= masks
final_rewards += (1-masks) * episode_rewards
episode_rewards *= masks
- 内部剧集循环完成后,我们接着跳转到损失计算和更新代码,如下所示:
_, next_value = actor_critic(autograd.Variable(rollout.states[-1], volatile=True))
next_value = next_value.data
returns = rollout.compute_returns(next_value, gamma)
logit, action_log_probs, values, entropy = actor_critic.evaluate_actions(
autograd.Variable(rollout.states[:-1]).view(-1, *state_shape),
autograd.Variable(rollout.actions).view(-1, 1)
)
distil_logit, _, _, _ = distil_policy.evaluate_actions(
autograd.Variable(rollout.states[:-1]).view(-1, *state_shape),
autograd.Variable(rollout.actions).view(-1, 1)
)
distil_loss = 0.01 * (F.softmax(logit).detach() * F.log_softmax(distil_logit)).sum(1).mean()
values = values.view(num_steps, num_envs, 1)
action_log_probs = action_log_probs.view(num_steps, num_envs, 1)
advantages = autograd.Variable(returns) - values
value_loss = advantages.pow(2).mean()
action_loss = -(autograd.Variable(advantages.data) * action_log_probs).mean()
optimizer.zero_grad()
loss = value_loss * value_loss_coef + action_loss - entropy * entropy_coef
loss.backward()
nn.utils.clip_grad_norm(actor_critic.parameters(), max_grad_norm)
optimizer.step()
distil_optimizer.zero_grad()
distil_loss.backward()
optimizer.step()
-
这里需要注意的一点是,我们正在使用两个损失梯度将损失推回到
distil模型,该模型调整distil模型的参数和actor_critic模型或策略及其参数。不深入细节,这里的主要概念是我们训练distil模型来学习想象力和用于一般策略训练的其他损失。 -
再次运行示例。等待它开始,然后你可能想在几轮之后关闭它,因为这个样本在较慢的CPU上每迭代可能需要超过一个小时,可能更长。以下是一个训练开始的示例截图:

Chapter_14_I2A.py训练示例
现在,如果你想完成这个练习,至少应该使用一个GPU。在CPU上训练一万个小时需要一年时间,这并不是你愿意花时间的事情。如果你使用GPU,你将不得不修改样本以支持GPU,这将需要取消注释部分并设置PyTorch以便它可以与CUDA一起运行。
这完成了本节、本章的内容以及整本书的内容。在下一节中,我们将查看最后一组练习。
练习
以下是一些简单和非常困难的练习的组合。选择那些你觉得适合你兴趣、能力和资源的练习。以下列表中的某些练习可能需要相当多的资源,因此请选择那些在你时间/资源预算范围内的练习:
-
调整
Chapter_14_learn.py样本的超参数。这个样本是一个标准的深度学习模型,但参数应该足够熟悉,可以自己找出。 -
按照常规方式调整
Chapter_14_MetaSGD-VPG.py样本的超参数。 -
调整
Chapter_14_Imagination.py样本的超参数。在这个样本中有几个新的超参数,你应该熟悉一下。 -
调整
Chapter_14_wo_HER.py和Chapter_14_HER.py示例的超参数。使用相同的技术在有和没有HER的情况下训练样本对你的理解非常有帮助。 -
调整
Chapter_14_Imagine_A2C.py示例的超参数。这对后续运行Chapter_14_Imagination.py示例有什么影响? -
将HER示例(
Chapter_14_HER.py)升级为使用不同的PG或价值/DQN方法。 -
将
Chapter_14_MetaSGD-VPG.py示例升级为使用更先进的PG或DQN方法。 -
将
Chapter_14_MetaSGD-VPG.py示例调整以在不同的环境中进行训练,这些环境使用连续或甚至可能是离散的动作。 -
将
Chapter_14_I2A.py样本训练完成。你需要配置示例以使用CUDA运行,以及安装带有CUDA的PyTorch。 -
调整
Chapter_14_I2A.py样本的超参数。你可以选择只使用CPU进行部分训练运行,这是可以接受的。因此,你可以一次训练几个迭代,并仍然优化那些新的超参数。
做你最感兴趣的练习,并记得要享受乐趣。
摘要
在本章中,我们超越了DRL,进入了通用人工智能(AGI)的领域,或者至少是我们希望AGI会走向的方向。更重要的是,我们探讨了DRL的下一阶段,我们如何解决其当前的不足,以及它可能走向何方。我们探讨了元学习以及学习如何学习意味着什么。然后,我们介绍了优秀的learn2learn库,并看到了它如何被用于一个简单的深度学习问题,以及一个更高级的元强化学习问题(MAML)。从那里,我们探讨了使用后见之明(HER)的另一种新的学习方法。从后见之明转向想象和推理,以及这些如何被纳入智能体中。然后,我们通过探讨I2A——想象增强智能体——以及想象如何帮助我们填补知识空白来结束本章。
我只想感谢你抽出时间与我们共同完成这本书。这是一段令人惊叹的旅程,涵盖了几乎整个强化学习(RL)和深度强化学习(DRL)的概念、术语和缩写。这本书从RL的基础开始,深入到DRL。只要你具备数学背景,你现在很可能可以独立探索,并构建你自己的最新和最优秀的智能体。RL,尤其是DRL,有一个神话,即你需要大量的计算资源才能做出有价值的贡献。虽然对于某些研究来说这确实如此,但还有很多更基础的元素需要更好的理解,并且可以进一步改进。DRL领域仍然相对较新,我们很可能在旅途中遗漏了一些东西。因此,无论你的资源如何,你很可能在未来几年内对DRL做出有价值的贡献。如果你确实计划追求这个梦想,我祝愿你成功,并希望这本书能对你的旅程有所帮助。


:状态

:奖励折扣
:质量
= 当前策略的状态值
= 代表折扣率
= 当前总回报
: 当前状态的价值函数
: 学习率α
: 下一个状态的重奖
: 折扣因子
: 下一个状态的价值








最大最佳或贪婪动作
这是所有回报的加权平均值。
这是从
Lambda,一个介于[0,1]之间的权重值。
前一时间步的参数值
学习率
针对动作
函数。此函数表示优势函数,其中我们取策略的负对数并将其添加到值函数
或
计算比率,该方程用于计算我们想要用于信任的剪切区域或区域的可能比率:
分布在
上
分布在
上
浙公网安备 33010602011771号