Python-强化学习算法实用指南-全-
Python 强化学习算法实用指南(全)
原文:
annas-archive.org/md5/e3819a6747796b03b9288831f4e2b00c译者:飞龙
前言
强化学习(RL)是人工智能的一个热门且前景广阔的分支,涉及到构建更智能的模型和智能体,使其能够根据不断变化的需求自动确定理想行为。《用 Python 实现强化学习算法》将帮助你掌握 RL 算法,并理解它们的实现,帮助你构建自学习的智能体。
本书从介绍在 RL 环境中工作所需的工具、库和设置开始,涵盖了 RL 的基本构建模块,并深入探讨了基于值的方法,如 Q-learning 和 SARSA 算法的应用。你将学习如何将 Q-learning 与神经网络结合使用,解决复杂问题。此外,你将研究策略梯度方法、TRPO 和 PPO,以提高性能和稳定性,然后再深入到 DDPG 和 TD3 的确定性算法。本书还将介绍模仿学习技术的原理,以及如何通过 Dagger 教一个智能体飞行。你将了解进化策略和黑箱优化技术。最后,你将掌握如 UCB 和 UCB1 等探索方法,并开发出一种名为 ESBAS 的元算法。
到本书结尾时,你将通过与关键的强化学习(RL)算法的实践,克服现实世界应用中的挑战,并且你将成为 RL 研究社区的一员。
本书适合的读者
如果你是 AI 研究人员、深度学习用户,或任何希望从零开始学习 RL 的人,本书非常适合你。如果你希望了解该领域的最新进展,这本 RL 书也会对你有所帮助。需要具备一定的 Python 基础。
本书内容概述
第一章,强化学习的全景,为你提供了关于 RL 的深刻洞察。它描述了 RL 擅长解决的问题以及 RL 算法已经应用的领域。还介绍了完成后续章节项目所需的工具、库和设置。
第二章,实现 RL 循环与 OpenAI Gym,描述了 RL 算法的主要循环、用于开发算法的工具包,以及不同类型的环境。你将能够通过 OpenAI Gym 接口开发一个随机智能体,使用随机动作玩 CartPole。你还将学习如何使用 OpenAI Gym 接口运行其他环境。
第三章,通过动态规划解决问题,向你介绍 RL 的核心思想、术语和方法。你将学习 RL 的主要模块,并对如何构建 RL 算法来解决问题有一个大致的了解。你还将了解基于模型和无模型算法之间的区别,以及强化学习算法的分类。动态规划将被用来解决 FrozenLake 游戏。
第四章,Q 学习与 SARSA 应用(Q-Learning and SARSA Applications),讨论了基于价值的方法,特别是 Q 学习和 SARSA,这两种算法与动态规划不同,并且在大规模问题上具有良好的扩展性。为了深入掌握这些算法,你将把它们应用于 FrozenLake 游戏,并研究它们与动态规划的差异。
第五章,深度 Q 网络(Deep Q-Networks),描述了神经网络,特别是卷积神经网络(CNNs),如何应用于 Q 学习。你将学习为什么 Q 学习和神经网络的结合能产生惊人的结果,并且它的使用能够解决更广泛的问题。此外,你还将利用 OpenAI Gym 接口将 DQN 应用于 Atari 游戏。
第六章,学习随机优化与策略梯度优化(Learning Stochastic and PG Optimization),介绍了一类新的无模型算法:策略梯度方法。你将学习策略梯度方法和基于价值的方法之间的区别,并了解它们的优缺点。接着,你将实现 REINFORCE 和 Actor-Critic 算法,解决一款名为 LunarLander 的新游戏。
第七章,TRPO 和 PPO 实现(TRPO and PPO Implementation),提出了一种修改策略梯度方法的新机制,用以控制策略的改进。这些机制被用来提高策略梯度算法的稳定性和收敛性。特别是,你将学习并实现两种主要的策略梯度方法,这些方法运用了这些新技术,分别是 TRPO 和 PPO。你将通过在 RoboSchool 环境中实现它们,探索具有连续动作空间的环境。
第八章,DDPG 和 TD3 应用(DDPG and TD3 Applications),介绍了一类新的算法——确定性策略算法,这些算法结合了策略梯度和 Q 学习。你将了解其背后的概念,并在一个新的环境中实现 DDPG 和 TD3 这两种深度确定性算法。
第九章,基于模型的强化学习(Model-Based RL),介绍了学习环境模型以规划未来动作或学习策略的强化学习算法。你将学习它们的工作原理、优点,以及为何它们在许多情况下更受青睐。为了掌握它们,你将实现一个基于模型的算法,并在 Roboschool 环境中进行实验。
第十章,通过 DAgger 算法进行模仿学习(Imitation Learning with the DAgger Algorithm),解释了模仿学习如何工作,以及如何将其应用和调整到具体问题上。你将了解最著名的模仿学习算法——DAgger。为了深入理解它,你将通过在 FlappyBird 中实施该算法来加速智能体的学习过程。
第十一章,理解黑箱优化算法,探讨了进化算法,这是一类不依赖反向传播的黑箱优化算法。由于其快速的训练速度和易于在数百或数千个核心上并行化,这些算法正在受到越来越多的关注。本章通过特别关注进化策略算法这一进化算法的类型,提供了这些算法的理论和实践背景。
第十二章,开发 ESBAS 算法,介绍了强化学习中特有的重要探索-利用困境。通过多臂老丨虎丨机问题演示了这一困境,并使用如 UCB 和 UCB1 等方法解决。接着,你将了解算法选择问题,并开发一种名为 ESBAS 的元算法。该算法使用 UCB1 来为每种情况选择最合适的强化学习算法。
第十三章,解决强化学习挑战的实践实现,探讨了该领域的主要挑战,并解释了克服这些挑战的一些实践和方法。你还将了解将强化学习应用于现实问题的挑战、深度强化学习的未来发展,以及其对世界的社会影响。
要充分利用本书
需要具备一定的 Python 工作知识。了解强化学习及其相关工具也将是有益的。
下载示例代码文件
您可以从您的帐户在www.packt.com下载本书的示例代码文件。如果您在其他地方购买了此书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。
您可以通过以下步骤下载代码文件:
-
登录或注册www.packt.com。
-
选择“支持”标签。
-
点击“代码下载”。
-
在搜索框中输入书名,并按照屏幕上的指示操作。
一旦文件下载完成,请确保使用最新版本的工具解压或提取文件夹:
-
WinRAR/7-Zip for Windows
-
Zipeg/iZip/UnRarX for Mac
-
7-Zip/PeaZip for Linux
本书的代码包也托管在 GitHub 上,链接为github.com/PacktPublishing/Reinforcement-Learning-Algorithms-with-Python。如果代码有更新,它将被更新到现有的 GitHub 仓库中。
我们还提供了其他来自丰富书籍和视频目录的代码包,您可以在github.com/PacktPublishing/查看。快去看看吧!
下载彩色图片
我们还提供了一份包含书中所有截图/图表的彩色图像的 PDF 文件。您可以在此下载:www.packtpub.com/sites/default/files/downloads/9781789131116_ColorImages.pdf
使用的约定
本书中使用了若干文本约定。
CodeInText:表示文本中的代码词汇、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟网址、用户输入和 Twitter 账号。以下是一个示例:“在本书中,我们使用的是 Python 3.7,但 3.5 以上的所有版本应该都可以使用。我们还假设您已经安装了numpy和matplotlib。”
一段代码块的格式如下:
import gym
# create the environment
env = gym.make("CartPole-v1")
# reset the environment before starting
env.reset()
# loop 10 times
for i in range(10):
# take a random action
env.step(env.action_space.sample())
# render the game
env.render()
# close the environment
env.close()
所有命令行输入或输出如下所示:
$ git clone https://github.com/pybox2d/pybox2d [](https://github.com/pybox2d/pybox2d) $ cd pybox2d
$ pip install -e .
粗体:表示新术语、重要词汇或屏幕上看到的词汇。例如,菜单或对话框中的词汇在文中通常是这样呈现的。以下是一个示例:“在强化学习(RL)中,算法被称为代理,它通过环境提供的数据进行学习。”
警告或重要提示如下所示。
小贴士和技巧如下所示。
保持联系
我们非常欢迎读者的反馈。
一般反馈:如果您对本书的任何方面有疑问,请在邮件主题中提及书名,并通过customercare@packtpub.com联系我们。
勘误表:尽管我们已经尽力确保内容的准确性,但错误仍然可能发生。如果您在本书中发现了错误,恳请您向我们报告。请访问www.packtpub.com/support/errata,选择您的书籍,点击“勘误提交表格”链接,并填写相关细节。
盗版:如果您在互联网上发现任何我们作品的非法复制品,恳请您提供该位置地址或网站名称。请通过copyright@packt.com与我们联系,并附上相关资料的链接。
如果您有兴趣成为作者:如果您在某个主题上拥有专业知识,并且有兴趣撰写或参与书籍的编写,请访问authors.packtpub.com。
书评
请留下评论。当您阅读并使用完本书后,不妨在购买平台上留下评论。潜在读者可以通过您的公正评价做出购买决策,我们 Packt 可以了解您的意见,我们的作者也能看到您对他们书籍的反馈。谢谢!
欲了解更多关于 Packt 的信息,请访问packt.com。
第一部分:算法和环境
本节是关于强化学习的介绍。它包括建立理论基础和设置未来章节所需的环境。
本节包括以下章节:
-
第一章,强化学习的景观
-
第二章,实现 RL 循环和 OpenAI Gym
-
第三章,用动态规划解决问题
第一章:强化学习的概览
人类和动物通过试验和错误的过程进行学习。这个过程基于我们的奖励机制,对我们的行为作出反应。这个过程的目标是通过多次重复,激励那些引发正向反应的行为的重复,并减少那些引发负向反应的行为的重复。通过试验和错误的机制,我们学会与周围的人和世界互动,追求复杂而有意义的目标,而不是即时的满足感。
通过互动和经验学习至关重要。试想一下,如果你只能通过观看别人踢足球来学习,结果会怎样?如果你基于这种学习方式去参加一场足球比赛,你可能表现得非常差劲。
这一点在 20 世纪中期得到了验证,特别是理查德·赫尔德和艾伦·海因 1963 年关于两只小猫的研究。两只小猫都在旋转木马上成长。其中一只小猫能够自由活动(主动),而另一只则被限制,只能被动地跟随主动小猫的动作。当两只小猫都被引入光线时,只有能够主动移动的小猫发展出了正常的深度感知和运动技能,而被动的小猫没有。最明显的表现是,被动小猫对接近物体的眨眼反应缺失。这项相当简单的实验表明,无论是否存在视觉剥夺,动物与环境的身体互动对于学习来说是必要的。
受到动物和人类学习方式的启发,强化学习(RL)围绕从与环境的主动互动中进行试验和错误的理念构建。具体来说,在强化学习中,智能体在与世界互动的过程中逐步学习。通过这种方式,就可以训练计算机以类似人类的方式,尽管较为初级,但仍能学习和行为。
本书完全关于强化学习。本书的目的是通过实践方法,给你提供对这一领域最好的理解。在前几章,你将从学习强化学习的最基本概念开始。随着你掌握这些概念,我们将开始开发我们第一个强化学习算法。随着书本内容的深入,你将创建更强大、更复杂的算法来解决更有趣、更具吸引力的问题。你会看到,强化学习非常广泛,存在许多算法以不同的方式解决各种问题。尽管如此,我们仍会尽力为你提供一个简单但完整的描述,伴随清晰且实用的算法实现。
本章首先让你熟悉强化学习的基本概念,了解不同方法之间的区别,以及策略、价值函数、奖励和环境模型等关键概念。你还将了解强化学习的历史和应用。
本章将涉及以下主题:
-
强化学习简介
-
强化学习的要素
-
强化学习的应用
强化学习简介
强化学习(RL)是机器学习的一个领域,处理顺序决策问题,旨在实现预定目标。一个强化学习问题由一个决策者组成,称为代理(Agent),以及代理所互动的物理或虚拟世界,称为环境(Environment)。代理通过动作(Action)与环境互动,从而产生效果。结果,环境会反馈给代理一个新的状态(State)和奖励(Reward)。这两个信号是代理采取的动作的后果。特别地,奖励是表示动作好坏的一个值,状态则是代理和环境的当前表现。这个循环在下图中展示:

在此图中,代理通过 PacMan 表示,基于当前环境状态,选择采取哪种行动。它的行为会影响环境,例如它的位置和敌人的位置,这些都将由环境反馈回来,形成新的状态和奖励。这个循环会一直持续,直到游戏结束。
代理的最终目标是最大化整个过程中的总奖励。
它的生命周期。我们简化符号表示:如果
是时间
的动作,且
是时间
的奖励,那么代理将采取
动作,以最大化所有奖励
的总和。
为了最大化累积奖励,代理必须在每种情况下学习最佳行为。为此,代理必须在考虑每一个动作的同时,优化长期目标。在具有许多离散或连续状态和动作的环境中,学习非常困难,因为代理需要对每种情况负责。更为复杂的是,强化学习可能包含非常稀疏和延迟的奖励,令学习过程更加艰难。
举一个强化学习问题的例子,并解释稀疏奖励的复杂性,可以参考著名的故事《汉塞尔与格蕾特》。故事中,父母将两个孩子带入森林准备抛弃他们,但汉塞尔知道父母的意图,于是他在离开家时带上了一片面包,成功地留下了面包屑的踪迹,以便带领他和妹妹找到回家的路。在强化学习框架中,代理人是汉塞尔和格蕾特,环境是森林。每当他们找到一片面包屑时,就能获得+1 的奖励,当他们回到家时,获得+10 的奖励。在这种情况下,面包屑的轨迹越密集,兄妹俩找到回家的路就越容易。这是因为从一片面包屑走到另一片面包屑,他们需要探索的区域更小。不幸的是,在现实世界中,稀疏奖励比密集奖励要普遍得多。
强化学习(RL)的一个重要特点是它能够应对动态、不确定和非确定性的环境。这些特性对于强化学习在现实世界中的应用至关重要。以下几点是如何将现实世界的问题转化为强化学习环境的示例:
-
自动驾驶汽车是一个流行但难以用强化学习来解决的概念。这是因为在驾驶过程中需要考虑许多因素(如行人、其他汽车、自行车和交通信号灯)以及高度不确定的环境。在这种情况下,自动驾驶汽车是代理人,可以操作方向盘、加速器和刹车。环境是它周围的世界。显然,代理人无法意识到周围整个世界的情况,因为它只能通过传感器(例如相机、雷达和 GPS)捕获有限的信息。自动驾驶汽车的目标是以最短的时间到达目的地,同时遵守交通规则,避免损坏任何物品。因此,如果发生负面事件,代理人可能会收到负奖励,而在代理人到达目的地时,可以根据行驶时间获得正奖励。
-
在国际象棋游戏中,目标是将对方的棋子将死。在强化学习框架中,玩家是代理人,环境是棋盘的当前状态。代理人可以根据自己棋子的移动方式来移动棋子。由于某个动作,环境会返回一个与胜负相对应的正面或负面奖励。在其他情况下,奖励为 0,下一个状态是对手移动后的棋盘状态。与自动驾驶汽车的例子不同,这里环境状态等同于代理人状态。换句话说,代理人对环境有着完美的视角。
比较强化学习和监督学习
强化学习和监督学习是两种相似但不同的数据学习范式。许多问题可以通过监督学习和强化学习来解决;然而,在大多数情况下,它们适用于解决不同的任务。
监督学习通过一个固定的数据集进行泛化学习,数据集由包含示例的数据组成。每个示例由输入和期望的输出(或标签)组成,标签提供了即时的学习反馈。
相比之下,强化学习更加关注在特定情境下可以采取的顺序性动作。在这种情况下,唯一的监督来自奖励信号。在这种情境下,没有像监督学习那样的正确动作可供选择。
强化学习(RL)可以被视为一种更通用、更完整的学习框架。RL 独特的主要特征如下:
-
奖励可能是密集的、稀疏的或非常延迟的。在许多情况下,奖励仅在任务结束时获得(例如,在国际象棋游戏中)。
-
这个问题是顺序性和时间依赖性的;动作会影响下一步动作,而这些动作又会影响可能的奖励和状态。
-
一个智能体必须采取具有更高潜力实现目标的行动(利用),但它也应该尝试不同的行动,确保探索环境的其他部分(探索)。这个问题被称为探索-利用困境(或探索-利用权衡),它管理着在环境的探索与利用之间的平衡。这一点也非常重要,因为与监督学习不同,强化学习可以影响环境,因为它可以自由地收集新的数据,只要它认为这些数据有用。
-
环境是随机的和非确定性的,智能体必须在学习和预测下一步动作时考虑到这一点。事实上,我们将看到,许多强化学习的组件可以设计成输出一个单一的确定性值或一系列值及其概率。
第三种学习类型是无监督学习,用于在没有任何监督信息的情况下识别数据中的模式。数据压缩、聚类和生成模型是无监督学习的例子。它也可以在强化学习环境中使用,以便探索并了解环境。无监督学习与强化学习的结合被称为无监督强化学习。在这种情况下,没有奖励给出,智能体可能会产生内在动机,偏向于探索新的情境,在这些情境中它们能够探索环境。
值得注意的是,自动驾驶汽车相关的问题也曾被当作监督学习问题来解决,但结果较差。主要问题源于在训练过程中使用的数据与智能体在其生命周期中遇到的数据分布不同。
强化学习的历史
强化学习(RL)的第一个数学基础是在 1960 年代和 1970 年代的最优控制领域中建立的。这解决了动态系统随时间变化的行为度量最小化问题。该方法涉及求解一组已知动态系统的方程。在这一时期,马尔可夫决策过程(MDP)这一关键概念被提出。它为在随机情境中建模决策提供了一个通用框架。在这些年里,一种称为动态规划(DP)的最优控制求解方法被提出。DP 是一种将复杂问题分解成一系列简单子问题来求解 MDP 的方法。
注意,DP 仅为已知动态系统提供了一种更简单的解决最优控制的方法;其中并不涉及学习。它还存在维度灾难的问题,因为计算需求会随着状态数量的增加呈指数增长。
即使这些方法不涉及学习,正如理查德·S·萨顿和安德鲁·G·巴托所指出的,我们仍然必须将最优控制的求解方法,如 DP,也视为强化学习方法。
在 1980 年代,通过时间上连续的预测进行学习的概念——即所谓的时间差学习(TD 学习)方法——最终被提出。TD 学习引入了一类强大的新算法,这些算法将在本书中进行解释。
使用 TD 学习解决的第一个问题足够小,可以通过表格或数组表示。这些方法被称为表格方法,通常作为最优解出现,但不具备可扩展性。事实上,许多强化学习任务涉及庞大的状态空间,使得表格方法无法采用。在这些问题中,使用函数逼近来找到一个近似解,从而减少计算资源的消耗。
在强化学习中采用函数逼近,尤其是人工神经网络(包括深度神经网络),并非易事;然而,正如许多场合所展示的那样,它们能够取得惊人的结果。深度学习在强化学习中的应用被称为深度强化学习(深度 RL),自从 2015 年深度强化学习算法深度 Q 网络(DQN)展现出超越人类的能力,在原始图像上玩 Atari 游戏以来,它取得了巨大的普及。深度强化学习的另一个显著成就发生在 2017 年,当时 AlphaGo 成为了第一个击败李世石——这位人类职业围棋选手及 18 届世界冠军——的程序。这些突破不仅表明机器可以在高维空间中表现得比人类更好(在人类对于图像的感知方式上),而且它们还能够以有趣的方式进行行为。例如,在玩 Breakout(一个 Atari 街机游戏,玩家需要摧毁所有砖块)时,深度强化学习系统发现了一个创造性的捷径,系统发现只要在砖块的左侧打通一个隧道,并将球引导到那个方向,就可以摧毁更多砖块,从而通过一次动作提高总体得分。如下图所示:

还有许多其他有趣的案例,其中智能体展示了卓越的行为或策略,这些行为或策略人类之前并未掌握,例如 AlphaGo 在与李世石对弈时所执行的一步棋。从人类角度来看,那一步棋似乎毫无意义,但最终帮助 AlphaGo 获得了胜利(这一步棋被称为37 号棋)。
如今,在处理高维状态或动作空间时,深度神经网络作为函数逼近的使用几乎已成为默认选择。深度强化学习已被应用于更具挑战性的问题,例如数据中心能源优化、自动驾驶汽车、多期投资组合优化和机器人技术,仅举几例。
深度强化学习(Deep RL)
现在你可能会问自己——为什么深度学习与强化学习结合能表现得如此出色?主要原因是深度学习能够处理具有高维状态空间的问题。在深度强化学习出现之前,状态空间必须分解为更简单的表示,称为特征。这些特征很难设计,在某些情况下,只有专家才能做到。现在,使用深度神经网络,如卷积神经网络(CNN)或递归神经网络(RNN),强化学习可以直接从原始像素或序列数据(如自然语言)中学习不同层次的抽象。这种配置如下图所示:

此外,深度强化学习问题现在可以完全通过端到端的方式来解决。在深度学习时代之前,强化学习算法涉及两个不同的流程:一个用于处理系统的感知,另一个用于负责决策制定。而现在,借助深度强化学习算法,这些过程已被整合并通过端到端训练,从原始像素直接到行动。例如,如前所示的图表,通过使用卷积神经网络(CNN)处理视觉组件,并通过全连接神经网络(FNN)将 CNN 的输出转化为行动,可以端到端地训练吃豆人。
如今,深度强化学习是一个非常热门的话题。其主要原因是,深度强化学习被认为是能够让我们构建高度智能机器的技术类型。作为证明,两个致力于解决智能问题的著名人工智能公司——DeepMind 和 OpenAI,正在深入研究强化学习。
除了深度强化学习取得的巨大进展外,仍然有很长的路要走。仍然存在许多需要解决的挑战,部分挑战列举如下:
-
与人类相比,深度强化学习的学习速度过于缓慢。
-
强化学习中的迁移学习仍然是一个未解的难题。
-
奖励函数的设计和定义非常困难。
-
强化学习智能体在诸如物理世界等高度复杂和动态的环境中很难学习。
尽管如此,这一领域的研究正在迅速增长,越来越多的公司开始在他们的产品中采用强化学习。
强化学习的元素
如我们所知,智能体通过行动与环境互动。这将导致环境发生变化,并反馈给智能体一个与行动质量和智能体新状态成比例的奖励。通过不断的试验和错误,智能体逐步学会在每种情况下采取最佳行动,从而在长期内获得更大的累积奖励。在强化学习框架中,特定状态下行动的选择由策略决定,而从该状态可以获得的累积奖励被称为价值函数。简而言之,如果智能体想要实现最优行为,那么在每种情况下,策略必须选择将其带到具有最高价值的下一个状态的行动。现在,让我们深入了解这些基本概念。
策略
策略定义了在给定状态下,代理如何选择一个行动。策略选择最大化从该状态开始的累积奖励的行动,而不是选择即时奖励较大的行动。它关注的是代理的长期目标。例如,如果一辆车距离目的地还有 30 公里,但仅剩下 10 公里的续航,而下一个加油站分别在 1 公里和 60 公里远的地方,那么策略将选择在第一个加油站(1 公里远)加油,以避免油量耗尽。这个决定在短期内并不最优,因为加油需要一些时间,但它最终会确保完成目标。
以下图示展示了一个简单的例子,其中一个在 4x4 网格中移动的行为者需要朝着星星移动,同时避免螺旋状的障碍物。策略推荐的行动由指向移动方向的箭头表示。左侧的图示展示了一个随机初始策略,而右侧的图示展示了最终的最优策略。在有两个同样最优的行动情况下,代理可以任意选择采取哪一个行动:

一个重要的区别是随机策略和确定性策略之间的差异。在确定性情况下,策略提供了一个确定性的行动。而在随机情况下,策略则为每个行动提供一个概率。行动概率的概念很有用,因为它考虑到了环境的动态性并有助于其探索。
一种对强化学习算法的分类方法是基于策略在学习过程中如何改进。较简单的情况是当作用于环境的策略与在学习过程中改进的策略相似。换句话说,策略从它自己生成的数据中学习。这些算法被称为on-policy。相比之下,off-policy算法涉及两种策略——一种作用于环境,另一种进行学习但实际上并未被使用。前者称为行为策略,后者称为目标策略。行为策略的目标是与环境互动并收集信息,以便改进被动的目标策略。正如我们将在接下来的章节中看到的那样,off-policy 算法比 on-policy 算法更不稳定且难以设计,但它们更具样本效率,这意味着它们需要更少的经验来学习。
为了更好地理解这两个概念,我们可以想象某人需要学习一项新技能。如果这个人的行为像在策略算法中那样,那么每当他们尝试一系列动作时,他们会根据积累的奖励改变自己的信念和行为。相比之下,如果这个人的行为像是脱离策略的算法,他们(目标策略)也可以通过观看自己以前做同一项技能的老视频(行为策略)来学习——也就是说,他们可以利用旧经验来帮助自己进步。
策略梯度方法是一类强化学习算法,它直接从性能相对于策略的梯度中学习一个参数化的策略(如深度神经网络)。这些算法有许多优点,包括能够处理连续动作和以不同粒度探索环境。它们将在第六章 学习随机优化与 PG 优化、第七章 TRPO 与 PPO 实现以及第八章 DDPG 与 TD3 应用中详细介绍。
值函数
值函数表示一个状态的长期质量。这是代理从某一给定状态开始时,预计在未来获得的累计奖励。如果奖励衡量的是即时表现,则值函数衡量的是长期表现。这意味着高奖励并不一定意味着高值函数,低奖励也不意味着低值函数。
此外,值函数可以是状态的函数,也可以是状态-动作对的函数。前者称为状态值函数,后者称为动作值函数:

这里,图表显示了最终状态值(左侧)和相应的最优策略(右侧)。
使用与策略概念说明相同的网格世界示例,我们可以展示状态值函数。首先,我们可以假设除了代理到达星星时获得+1 奖励外,在其他情况下奖励为 0。此外,假设强风以 0.33 的概率将代理吹向另一个方向。在这种情况下,状态值将类似于前图左侧所示的状态。最优策略将选择能够将代理带到下一个状态并具有最高状态值的动作,如前图右侧所示。
动作价值方法(或价值函数方法)是强化学习算法的另一大类。这些方法通过学习一个动作价值函数并利用它来选择要执行的动作。从第三章 通过动态规划解决问题 开始,你将进一步了解这些算法。值得注意的是,一些策略梯度方法为了结合两者的优点,也可以使用一个价值函数来学习合适的策略。这些方法被称为演员-评论家 方法。下图展示了强化学习算法的三大类:

奖励
在每个时间步,即每次智能体行动之后,环境会向智能体返回一个数字,表示该动作的好坏。这被称为奖励。正如我们已经提到的,智能体的最终目标是最大化在与环境互动过程中获得的累计奖励。
在文献中,奖励被认为是环境的一部分,但实际上这并不完全准确。奖励也可以来自智能体,但永远不会来自决策部分。出于这个原因,并且为了简化公式,奖励总是由环境发送。
奖励是注入到强化学习循环中的唯一监督信号,因此设计正确的奖励机制对于获得行为良好的智能体至关重要。如果奖励存在缺陷,智能体可能会发现并跟随不正确的行为。例如,海岸赛跑者 是一款目标是比其他玩家先到达终点的船赛游戏。在比赛过程中,船只会因击中目标而获得奖励。OpenAI 的研究人员曾使用强化学习训练智能体来玩这款游戏。结果他们发现,训练后的船并没有尽可能快地驶向终点,而是绕圈行驶,捕捉重新出现的目标,同时撞击并着火。这样,船只找到了最大化总奖励的方法,但并没有按照预期的方式行动。这种行为源于短期奖励和长期奖励之间的不正确平衡。
奖励的出现频率可能会因环境而异。频繁出现的奖励被称为密集奖励;然而,如果奖励仅在游戏过程中出现几次,或只在游戏结束时出现,则称为稀疏奖励。在后一种情况下,智能体可能很难捕捉到奖励并找到最优的行动。
模仿学习和逆向强化学习是两种强大的技术,用于处理环境中缺乏奖励的情况。模仿学习利用专家演示将状态映射到动作上。另一方面,逆向强化学习通过专家的最优行为推导出奖励函数。模仿学习和逆向强化学习将在第十章 使用 DAgger 算法进行模仿学习 中进行研究。
模型
该模型是代理的一个可选组件,这意味着它不是寻找环境策略所必需的。模型详细描述了环境的行为,给定一个状态和一个动作,预测下一个状态和奖励。如果已知模型,可以使用规划算法与模型进行交互并推荐未来的动作。例如,在具有离散动作的环境中,可以使用前瞻搜索(例如,蒙特卡洛树搜索)来模拟潜在的轨迹。
环境的模型可以提前给定,或者通过与环境的交互来学习。如果环境复杂,使用深度神经网络来近似它是个不错的选择。使用已知环境模型或通过学习来获取模型的强化学习算法被称为基于模型的方法。这些解决方案与无模型方法相对,且将在第九章,基于模型的强化学习中做更详细的解释。
强化学习的应用
强化学习已广泛应用于多个领域,包括机器人技术、金融、医疗保健和智能交通系统。通常,这些应用可以分为三个主要领域——自动化机器(如自动驾驶汽车、智能电网和机器人技术)、优化过程(例如,计划性维护、供应链和流程规划)和控制(例如,故障检测和质量控制)。
最初,强化学习仅应用于简单的问题,但深度强化学习为解决不同问题开辟了道路,使得处理更复杂的任务成为可能。如今,深度强化学习已显示出一些非常有前景的结果。不幸的是,许多突破局限于研究应用或游戏,并且在许多情况下,很难弥合纯粹研究导向的应用和工业问题之间的差距。尽管如此,越来越多的公司正朝着在其行业和产品中采用强化学习的方向发展。
现在我们将看看已经在应用或将受益于强化学习(RL)的主要领域。
游戏
游戏是强化学习的一个完美试验场,因为它们是为了挑战人类能力而创建的,并且要完成这些游戏,需要人类大脑常见的技能(如记忆、推理和协调)。因此,一台能够与人类同等甚至更好的计算机必须具备这些相同的特质。此外,游戏容易复现,可以在计算机中轻松模拟。视频游戏因其部分可观察性(即只有一小部分游戏是可见的)和巨大的搜索空间(即计算机不可能模拟所有可能的配置)而被证明是非常难以解决的。
在 2015 年,AlphaGo 在古老的围棋比赛中击败了李世石,突破了游戏领域的一个重要里程碑。这场胜利发生时,许多人曾预测 AlphaGo 不会获胜。当时人们认为,在未来 10 年内,任何计算机都无法击败围棋专家。AlphaGo 利用强化学习和监督学习从人类职业选手的比赛中学习。几年的时间过去了,AlphaGo 的下一个版本——AlphaGo Zero,击败了 AlphaGo 100 局,且一局不败。AlphaGo Zero 通过自我对弈,仅用了三天时间就学会了下围棋。
自我对弈是一种非常有效的算法训练方式,因为它只是与自己对弈。通过自我对弈,一些有用的子技能或行为也可能会出现,否则这些技能或行为是无法被发现的。
为了捕捉真实世界的混乱和连续性,一个由五个神经网络组成的团队——OpenAI Five,接受了训练,参与玩DOTA 2这款实时战略游戏。该游戏中有两个队伍(每队五名玩家)相互对抗。玩这款游戏的陡峭学习曲线来源于游戏的长时间跨度(每局游戏平均持续 45 分钟,且有成千上万的动作)、部分可观察性(每个玩家只能看到自己周围的一小块区域),以及高维度的连续动作和观察空间。2018 年,OpenAI Five 在《国际邀请赛》上与顶级DOTA 2玩家对战,虽然最终输了比赛,但展现出了在协作和战略技能方面的天生能力。最终,在 2019 年 4 月 13 日,OpenAI Five 正式击败了世界冠军,成为第一个在电子竞技游戏中击败职业队伍的人工智能。
机器人技术与工业 4.0
强化学习在工业机器人领域是一个非常活跃的研究方向,因为这是现实世界中自然而然会采用的范式。工业智能机器人的潜力和益处巨大且广泛。强化学习推动了工业 4.0(即第四次工业革命),通过智能设备、系统和机器人来执行高度复杂和理性化的操作。能够预测维护、进行实时诊断和管理制造活动的系统可以集成在一起,从而更好地实现控制和提高生产力。
机器学习
由于 RL 的灵活性,它不仅可以应用于独立任务,还可以作为一种在监督学习算法中进行微调的方法。在许多自然语言处理(NLP)和计算机视觉任务中,优化的指标是不可微分的,因此在监督设置中使用神经网络时,需要一个辅助的可微分损失函数。然而,这两个损失函数之间的差异会影响最终的性能。解决这个问题的一种方法是,首先使用监督学习并结合辅助损失函数训练系统,然后利用 RL 对网络进行微调,优化最终的指标。例如,这个过程在机器翻译和问答等子领域中可以发挥作用,因为这些领域的评估指标复杂且不可微分。
此外,RL 还可以解决 NLP 问题,例如对话系统和文本生成。计算机视觉、定位、运动分析、视觉控制和视觉跟踪都可以通过深度 RL 进行训练。
深度学习旨在克服手动特征工程的繁重任务,同时仍然需要手动设计神经网络架构。这是一项繁琐的工作,涉及多个部分,需要以最佳方式进行组合。那么,为什么我们不能自动化这一过程呢?事实上,我们可以。神经架构设计(NAD)是一种使用强化学习(RL)设计深度神经网络架构的方法。这在计算上非常昂贵,但该技术能够创建能够在图像分类中达到最先进结果的 DNN 架构。
经济与金融
商业管理是 RL 的另一种自然应用。它已成功用于互联网广告,目的是最大化按点击付费的广告,用于产品推荐、客户管理和营销。此外,金融领域也从 RL 中获益,应用包括期权定价和多期优化等任务。
医疗健康
RL 在医疗健康领域被用于诊断和治疗。它可以为医生和护士构建一个基于 AI 的助手的基础,特别是 RL 可以为患者提供个性化的渐进治疗——这一过程称为动态治疗方案。在医疗健康中的其他 RL 应用包括个性化血糖控制、脓毒症和艾滋病的个性化治疗。
智能交通系统
智能交通系统可以借助 RL 来开发和改进各种交通系统。其应用范围包括控制拥堵的智能网络(如交通信号控制)、交通监控和安全(如碰撞预测)到自动驾驶汽车。
能源优化与智能电网
能源优化和智能电网对于智能发电、分配和消费电力至关重要。决策能源系统和控制能源系统可以采用强化学习技术,提供对环境变化的动态响应。强化学习还可以用于根据动态电价调整电力需求或减少能源使用。
总结
强化学习(RL)是一种以目标为导向的决策方法。与其他范式不同,它直接与环境进行交互,并且具有延迟奖励机制。强化学习与深度学习的结合在具有高维状态空间和感知输入问题中非常有用。策略和价值函数的概念至关重要,因为它们指示了采取的行动以及环境状态的质量。在强化学习中,环境的模型并不是必需的,但它可以提供额外的信息,从而提高策略的质量。
现在,所有关键概念已被介绍,接下来的章节将重点介绍实际的强化学习算法。但首先,在下一章中,你将获得使用 OpenAI 和 TensorFlow 开发强化学习算法的基础知识。
问题
-
什么是强化学习(RL)?
-
一个智能体的最终目标是什么?
-
监督学习和强化学习之间的主要区别是什么?
-
结合深度学习和强化学习有什么好处?
-
“强化”一词来源于哪里?
-
策略和价值函数之间有什么区别?
-
通过与环境的互动,是否可以学习到环境的模型?
深入阅读
-
有关有缺陷奖励函数的示例,请参阅以下链接:
blog.openai.com/faulty-reward-functions/。 -
有关深度强化学习的更多信息,请参阅以下链接:
karpathy.github.io/2016/05/31/rl/。
第二章:实现 RL 循环和 OpenAI Gym
在每个机器学习项目中,算法从训练数据集中学习规则和指令,以更好地执行任务。在强化学习(RL)中,算法被称为代理程序,并从环境提供的数据中学习。在这里,环境是一个连续提供信息的来源,根据代理的动作返回数据。由于环境返回的数据可能是潜在无限的,因此在训练时会涉及许多监督设置中出现的概念和实际差异。但是,对于本章的目的,重要的是强调不同的环境不仅提供不同的任务完成,还可能具有不同类型的输入、输出和奖励信号,同时需要在每种情况下调整算法。例如,机器人可以通过视觉输入(如 RGB 摄像头)或离散的内部传感器感知其状态。
在本章中,您将设置编写强化学习算法所需的环境,并构建您的第一个算法。尽管这只是一个玩 CartPole 的简单算法,但在深入研究更高级别的强化学习算法之前,它提供了一个有用的基准。此外,在后续章节中,您将编写许多深度神经网络,因此在这里我们将简要回顾 TensorFlow 并介绍 TensorBoard,一个可视化工具。
本书中几乎所有使用的环境都基于 OpenAI 开源的接口Gym。因此,我们将介绍它并使用其中一些内置环境。然后,在深入探讨后续章节中的强化学习算法之前,我们将列出并解释多个开源环境的优势和差异。这样一来,您将对可以使用强化学习解决的问题有一个广泛而实用的概述。
本章将涵盖以下主题:
-
设置环境
-
OpenAI Gym 和 RL 循环
-
TensorFlow
-
TensorBoard
-
强化学习环境的种类
设置环境
以下是创建深度强化学习算法所需的三个主要工具:
-
编程语言:Python 是开发机器学习算法的首选,因其简单性和建立在其周围的第三方库。
-
深度学习框架:在本书中,我们使用 TensorFlow,因为如同我们将在TensorFlow部分看到的那样,它可扩展、灵活且非常表达力强。尽管如此,也可以使用许多其他框架,包括 PyTorch 和 Caffe。
-
环境:在本书中,我们将使用许多不同的环境来演示如何处理不同类型的问题,并突出强化学习算法的优势。
在本书中,我们使用 Python 3.7,但所有 3.5 以上的版本也应该可以工作。我们还假设您已经安装了numpy和matplotlib。
如果你还没有安装 TensorFlow,可以通过他们的网站安装,或者在终端窗口中输入以下命令:
$ pip install tensorflow
或者,如果你的机器有 GPU,你可以输入以下命令:
$ pip install tensorflow-gpu
你可以在 GitHub 仓库中找到所有的安装说明和与本章相关的练习,仓库地址是:github.com/PacktPublishing/Reinforcement-Learning-Algorithms-with-Python
现在,让我们来看看如何安装这些环境。
安装 OpenAI Gym
OpenAI Gym 提供了一个通用接口以及多种不同的环境。
要安装它,我们将使用以下命令。
在 OSX 上,我们可以使用以下命令:
$ brew install cmake boost boost-python sdl2 swig wget
在 Ubuntu 16.04 上,我们将使用以下命令:
$ apt-get install -y python-pyglet python3-opengl zlib1g-dev libjpeg-dev patchelf cmake swig libboost-all-dev libsdl2-dev libosmesa6-dev xvfb ffmpeg
在 Ubuntu 18.04 上,我们将使用以下命令:
$ sudo apt install -y python3-dev zlib1g-dev libjpeg-dev cmake swig python-pyglet python3-opengl libboost-all-dev libsdl2-dev libosmesa6-dev patchelf ffmpeg xvfb
在运行完前面的命令之后,针对你的操作系统,可以使用以下命令:
$ git clone https://github.com/openai/gym.git
$ cd gym
$ pip install -e '.[all]'
某些 Gym 环境还需要安装pybox2d:
$ git clone https://github.com/pybox2d/pybox2d [](https://github.com/pybox2d/pybox2d) $ cd pybox2d
$ pip install -e .
安装 Roboschool
我们感兴趣的最终环境是 Roboschool,它是一个机器人模拟器。它容易安装,但如果遇到任何错误,可以查看其 GitHub 仓库:github.com/Roboschool/roboschool
$ pip install roboschool
OpenAI Gym 与强化学习周期
由于强化学习需要一个代理和环境相互作用,第一个可能想到的例子是地球——我们所生活的物理世界。不幸的是,目前它仅在少数几个案例中使用。由于当前算法的问题来自于代理必须与环境进行大量互动才能学习到良好的行为,这可能需要数百、数千甚至数百万次操作,导致所需时间过长,难以实现。一种解决方案是使用模拟环境来启动学习过程,最后才在真实世界中进行微调。这种方法比仅从周围世界中学习要好得多,但仍然需要缓慢的现实世界交互。然而,在许多情况下,任务可以完全模拟。为了研究和实现强化学习算法,游戏、视频游戏和机器人模拟器是完美的测试平台,因为它们的解决方案需要规划、策略和长期记忆等能力。此外,游戏有明确的奖励系统,可以在人工环境(计算机)中完全模拟,允许快速交互,从而加速学习过程。正因为如此,在本书中,我们将主要使用视频游戏和机器人模拟器来展示强化学习算法的能力。
OpenAI Gym 是一个开源工具包,用于开发和研究 RL 算法,旨在提供一个统一的环境接口,同时提供一个庞大且多样化的环境集合。这些环境包括 Atari 2600 游戏、连续控制任务、经典控制理论问题、模拟机器人目标导向任务以及简单的文本游戏。由于其通用性,许多第三方创建的环境都使用 Gym 接口。
开发一个 RL 循环
以下代码块展示了一个基本的 RL 循环。这本质上让 RL 模型进行 10 步操作,并在每一步渲染游戏:
import gym
# create the environment
env = gym.make("CartPole-v1")
# reset the environment before starting
env.reset()
# loop 10 times
for i in range(10):
# take a random action
env.step(env.action_space.sample())
# render the game
env.render()
# close the environment
env.close()
这将导致以下输出:

图 2.1:CartPole 渲染
让我们仔细看看代码。它通过创建一个名为 CartPole-v1 的新环境开始,这是一个经典的用于控制理论问题的游戏。然而,在使用它之前,环境需要通过调用 reset() 进行初始化。初始化后,循环执行 10 次。在每次迭代中,env.action_space.sample() 会随机选择一个动作,通过 env.step() 执行该动作,并通过 render() 方法显示结果;也就是游戏的当前状态,如前面的截图所示。最后,环境通过调用 env.close() 被关闭。
如果以下代码输出弃用警告,不用担心;这些警告是用来通知你某些函数已被更改,代码仍然能够正常运行。
这个循环对于所有使用 Gym 接口的环境都是相同的,但现在,智能体只能进行随机动作,而没有任何反馈,这是任何 RL 问题中至关重要的部分。
在 RL 中,你可能会看到状态和观察这两个术语几乎可以互换使用,但它们并不相同。当所有与环境相关的信息都被编码在其中时,我们称之为状态。当只有部分实际环境状态对智能体可见时,我们称之为观察,例如机器人的感知。为了简化这一点,OpenAI Gym 始终使用“观察”一词。
以下图示显示了循环的流程:

图 2.2:根据 OpenAI Gym 的基本 RL 循环。环境返回下一个状态、奖励、完成标志和一些附加信息。
实际上,step() 方法返回四个变量,它们提供关于与环境交互的信息。上面的图示展示了代理与环境之间的循环,以及交换的变量;即 观察、奖励、完成 和 信息。观察 是一个表示环境新观察(或状态)的对象。奖励 是一个浮动数值,表示上一个动作获得的奖励数值。完成 是一个布尔值,用于表示任务是否是阶段性任务;也就是交互次数有限的任务。每当 done 为 True 时,意味着该回合已经结束,环境应当被重置。例如,done 为 True 时,表示任务已完成或代理已失败。另一方面,信息 是一个字典,提供有关环境的额外信息,但通常不会使用。
如果你从未听说过 CartPole,它是一款目标是平衡作用在水平小车上的摆锤的游戏。每当摆锤处于竖直位置时,都会获得 +1 的奖励。游戏结束时,如果摆锤失去平衡,或者它成功平衡超过 200 个时间步(累积奖励最大为 200),游戏就会结束。
我们现在可以创建一个更完整的算法,使用以下代码来进行 10 局游戏,并打印每局游戏的累积奖励:
import gym
# create and initialize the environment
env = gym.make("CartPole-v1")
env.reset()
# play 10 games
for i in range(10):
# initialize the variables
done = False
game_rew = 0
while not done:
# choose a random action
action = env.action_space.sample()
# take a step in the environment
new_obs, rew, done, info = env.step(action)
game_rew += rew
# when is done, print the cumulative reward of the game and reset the environment
if done:
print('Episode %d finished, reward:%d' % (i, game_rew))
env.reset()
输出将类似于以下内容:
Episode: 0, Reward:13
Episode: 1, Reward:16
Episode: 2, Reward:23
Episode: 3, Reward:17
Episode: 4, Reward:30
Episode: 5, Reward:18
Episode: 6, Reward:14
Episode: 7, Reward:28
Episode: 8, Reward:22
Episode: 9, Reward:16
下表显示了 step() 方法在游戏最后四个动作中的输出:
| 观察 | 奖励 | 完成 | 信息 |
|---|---|---|---|
| [-0.05356921, -0.38150626, 0.12529277, 0.9449761 ] | 1.0 | False | {} |
| [-0.06119933, -0.57807287, 0.14419229, 1.27425449] | 1.0 | False | {} |
| [-0.07276079, -0.38505429, 0.16967738, 1.02997704] | 1.0 | False | {} |
| [-0.08046188, -0.58197758, 0.19027692, 1.37076617] | 1.0 | False | {} |
| [-0.09210143, -0.3896757, 0.21769224, 1.14312384] | 1.0 | True | {} |
请注意,环境的观察以 1 x 4 的数组进行编码;正如我们预期的那样,奖励始终为 1;并且只有在游戏结束时(即最后一行),done 为 True。此外,信息在此情况下为空。
在接下来的章节中,我们将创建代理,根据摆锤的当前状态做出更智能的决策来玩 CartPole 游戏。
习惯于空间
在 OpenAI Gym 中,动作和观察大多是 Discrete 或 Box 类的实例。这两个类表示不同的空间。Box 代表一个 n 维数组,而 Discrete 是一个允许固定范围非负数的空间。在前面的表格中,我们已经看到,CartPole 的观察由四个浮动数值编码,意味着它是 Box 类的一个实例。可以通过打印 env.observation_space 变量来检查观察空间的类型和维度:
import gym
env = gym.make('CartPole-v1')
print(env.observation_space)
确实,正如我们预期的那样,输出如下:
>> Box(4,)
在本书中,我们通过在print()输出的文本前添加>>来标记输出。
同样,可以检查动作空间的维度:
print(env.action_space)
这将产生以下输出:
>> Discrete(2)
特别地,Discrete(2)意味着动作的值可以是0或1。实际上,如果我们使用前面示例中的采样函数,我们将获得0或1(在 CartPole 中,这意味着向左或向右):
print(env.action_space.sample())
>> 0
print(env.action_space.sample())
>> 1
low和high实例属性返回Box空间允许的最小值和最大值:
print(env.observation_space.low)
>> [-4.8000002e+00 -3.4028235e+38 -4.1887903e-01 -3.4028235e+38]
print(env.observation_space.high)
>> [4.8000002e+00 3.4028235e+38 4.1887903e-01 3.4028235e+38]
使用 TensorFlow 开发机器学习模型
TensorFlow 是一个执行高性能数值计算的机器学习框架。TensorFlow 之所以如此流行,得益于其高质量和丰富的文档、能够轻松在生产环境中部署模型的能力,以及友好的 GPU 和 TPU 接口。
TensorFlow,为了便于机器学习模型的开发和部署,提供了许多高级 API,包括 Keras、Eager Execution 和 Estimators。这些 API 在许多场合非常有用,但为了开发强化学习算法,我们将只使用低级 API。
现在,让我们立即使用TensorFlow编写代码。以下代码行执行了常量a和b的加法,a和b是使用tf.constant()创建的:
import tensorflow as tf
# create two constants: a and b
a = tf.constant(4)
b = tf.constant(3)
# perform a computation
c = a + b
# create a session
session = tf.Session()
# run the session. It computes the sum
res = session.run(c)
print(res)
TensorFlow 的一个特点是,它将所有计算表达为一个计算图,首先需要定义该图,之后才能执行。只有在执行后,结果才会可用。在以下示例中,在操作c = a + b之后,c并不持有最终值。实际上,如果你在创建会话之前打印c,你将获得以下内容:
>> Tensor("add:0", shape=(), dtype=int32)
这是c变量的类,而不是加法的结果。
此外,执行必须在通过tf.Session()实例化的会话内进行。然后,为了执行计算,操作必须作为输入传递给刚创建的会话的run函数。因此,为了实际计算图并最终求和a和b,我们需要创建一个会话,并将c作为输入传递给session.run:
session = tf.Session()
res = session.run(c)
print(res)
>> 7
如果你正在使用 Jupyter Notebook,请确保通过运行tf.reset_default_graph()来重置之前的图。
张量
TensorFlow 中的变量表示为张量,是任何维度数组。张量有三种主要类型——tf.Variable、tf.constant和tf.placeholder。除了tf.Variable,其他张量都是不可变的。
要检查张量的形状,我们将使用以下代码:
# constant
a = tf.constant(1)
print(a.shape)
>> ()
# array of five elements
b = tf.constant([1,2,3,4,5])
print(b.shape)
>> (5,)
张量的元素非常容易访问,机制与 Python 中使用的类似:
a = tf.constant([1,2,3,4,5])
first_three_elem = a[:3]
fourth_elem = a[3]
sess = tf.Session()
print(sess.run(first_three_elem))
>> array([1,2,3])
print(sess.run(fourth_elem))
>> 4
常量
如我们已经看到的,常量是不可变的张量类型,可以使用tf.constant轻松创建:
a = tf.constant([1.0, 1.1, 2.1, 3.1], dtype=tf.float32, name='a_const')
print(a)
>> Tensor("a_const:0", shape=(4,), dtype=float32)
占位符
占位符是一个张量,在运行时被提供输入。通常,占位符作为模型的输入。在运行时传递给计算图的每个输入都通过feed_dict进行传递。feed_dict是一个可选参数,允许调用者覆盖图中张量的值。在以下代码片段中,a占位符的值被[[0.1,0.2,0.3]]覆盖:
import tensorflow as tf
a = tf.placeholder(shape=(1,3), dtype=tf.float32)
b = tf.constant([[10,10,10]], dtype=tf.float32)
c = a + b
sess = tf.Session()
res = sess.run(c, feed_dict={a:[[0.1,0.2,0.3]]})
print(res)
>> [[10.1 10.2 10.3]]
如果输入的第一维的大小在创建图时尚不确定,TensorFlow 可以处理它。只需将其设置为None:
import tensorflow as tf
import numpy as np
# NB: the first dimension is 'None', meaning that it can be of any length
a = tf.placeholder(shape=(None,3), dtype=tf.float32)
b = tf.placeholder(shape=(None,3), dtype=tf.float32)
c = a + b
print(a)
>> Tensor("Placeholder:0", shape=(?, 3), dtype=float32)
sess = tf.Session()
print(sess.run(c, feed_dict={a:[[0.1,0.2,0.3]], b:[[10,10,10]]}))
>> [[10.1 10.2 10.3]]
v_a = np.array([[1,2,3],[4,5,6]])
v_b = np.array([[6,5,4],[3,2,1]])
print(sess.run(c, feed_dict={a:v_a, b:v_b}))
>> [[7\. 7\. 7.]
[7\. 7\. 7.]]
这个功能在训练示例数量最初不确定时特别有用。
变量
变量是一个可变的张量,可以使用优化器进行训练。例如,它们可以是神经网络的权重和偏置所构成的自由变量。
现在,我们将创建两个变量,一个使用均匀初始化,另一个使用常数值初始化:
import tensorflow as tf
import numpy as np
# variable initialized randomly
var = tf.get_variable("first_variable", shape=[1,3], dtype=tf.float32)
# variable initialized with constant values
init_val = np.array([4,5])
var2 = tf.get_variable("second_variable", shape=[1,2], dtype=tf.int32, initializer=tf.constant_initializer(init_val))
# create the session
sess = tf.Session()
# initialize all the variables
sess.run(tf.global_variables_initializer())
print(sess.run(var))
>> [[ 0.93119466 -1.0498083 -0.2198658 ]]
print(sess.run(var2))
>> [[4 5]]
变量在调用global_variables_initializer()之前不会被初始化。
通过这种方式创建的所有变量都会被设置为可训练,意味着图可以修改它们,例如,在优化操作之后。也可以将变量设置为不可训练,如下所示:
var2 = tf.get_variable("variable", shape=[1,2], trainable=False, dtype=tf.int32)
访问所有变量的简便方法如下:
print(tf.global_variables())
>> [<tf.Variable 'first_variable:0' shape=(1, 3) dtype=float32_ref>, <tf.Variable 'second_variable:0' shape=(1, 2) dtype=int32_ref>]
创建图表
图表示低级计算,这些计算基于操作之间的依赖关系。在 TensorFlow 中,首先定义一个图,然后创建一个会话来执行图中的操作。
TensorFlow 中图的构建、计算和优化方式支持高度的并行性、分布式执行和可移植性,这些都是构建机器学习模型时非常重要的属性。
为了让你了解 TensorFlow 内部生成的计算图的结构,以下程序将生成如下图所示的计算图:
import tensorflow as tf
import numpy as np
const1 = tf.constant(3.0, name='constant1')
var = tf.get_variable("variable1", shape=[1,2], dtype=tf.float32)
var2 = tf.get_variable("variable2", shape=[1,2], trainable=False, dtype=tf.float32)
op1 = const1 * var
op2 = op1 + var2
op3 = tf.reduce_mean(op2)
sess = tf.Session()
sess.run(tf.global_variables_initializer())
sess.run(op3)
这将产生以下图表:

图 2.3:计算图示例
简单线性回归示例
为了更好地理解所有概念,我们现在创建一个简单的线性回归模型。首先,我们必须导入所有库并设置随机种子,这对于 NumPy 和 TensorFlow 都是必要的(这样我们得到的结果都是相同的):
import tensorflow as tf
import numpy as np
from datetime import datetime
np.random.seed(10)
tf.set_random_seed(10)
然后,我们可以创建一个由 100 个示例组成的合成数据集,如下图所示:

图 2.4:线性回归示例中使用的数据集
因为这是一个线性回归示例,y = W * X + b,其中W和b是任意值。在这个示例中,我们设置W = 0.5和b = 1.4。此外,我们还加入了一些正态随机噪声:
W, b = 0.5, 1.4
# create a dataset of 100 examples
X = np.linspace(0,100, num=100)
# add random noise to the y labels
y = np.random.normal(loc=W * X + b, scale=2.0, size=len(X))
下一步是创建输入和输出的占位符,以及线性模型的权重和偏置变量。在训练过程中,这两个变量将被优化,使其尽可能与数据集的权重和偏置相似:
# create the placeholders
x_ph = tf.placeholder(shape=[None,], dtype=tf.float32)
y_ph = tf.placeholder(shape=[None,], dtype=tf.float32)
# create the variables
v_weight = tf.get_variable("weight", shape=[1], dtype=tf.float32)
v_bias = tf.get_variable("bias", shape=[1], dtype=tf.float32)
然后,我们构建了定义线性操作和 均方误差 (MSE) 损失的计算图:
# linear computation
out = v_weight * x_ph + v_bias
# compute the mean squared error
loss = tf.reduce_mean((out - y_ph)**2)
现在,我们可以实例化优化器并调用 minimize() 来最小化 MSE 损失。minimize() 首先计算变量(v_weight 和 v_bias)的梯度,然后应用梯度更新变量:
opt = tf.train.AdamOptimizer(0.4).minimize(loss)
现在,让我们创建一个会话并初始化所有变量:
session = tf.Session()
session.run(tf.global_variables_initializer())
训练通过多次运行优化器并将数据集输入图表来完成。为了跟踪模型的状态,MSE 损失和模型变量(权重和偏差)每 40 个 epochs 打印一次:
# loop to train the parameters
for ep in range(210):
# run the optimizer and get the loss
train_loss, _ = session.run([loss, opt], feed_dict={x_ph:X, y_ph:y})
# print epoch number and loss
if ep % 40 == 0:
print('Epoch: %3d, MSE: %.4f, W: %.3f, b: %.3f' % (ep, train_loss, session.run(v_weight), session.run(v_bias)))
最后,我们可以打印变量的最终值:
print('Final weight: %.3f, bias: %.3f' % (session.run(v_weight), session.run(v_bias)))
输出将类似于以下内容:
>> Epoch: 0, MSE: 4617.4390, weight: 1.295, bias: -0.407
Epoch: 40, MSE: 5.3334, weight: 0.496, bias: -0.727
Epoch: 80, MSE: 4.5894, weight: 0.529, bias: -0.012
Epoch: 120, MSE: 4.1029, weight: 0.512, bias: 0.608
Epoch: 160, MSE: 3.8552, weight: 0.506, bias: 1.092
Epoch: 200, MSE: 3.7597, weight: 0.501, bias: 1.418
Final weight: 0.500, bias: 1.473
在训练阶段,可以看到 MSE 损失会逐渐减少到一个非零值(大约为 3.71)。这是因为我们在数据集中添加了随机噪声,防止了 MSE 达到完美的 0 值。
同时,如预期的那样,关于模型方法的权重和偏差,0.500和1.473的值正是构建数据集时围绕的值。下图中可见的蓝色线条是训练好的线性模型的预测值,而这些点则是我们的训练示例:

图 2.5:线性回归模型预测
本章中所有颜色引用的详细信息,请参考颜色图片包:www.packtpub.com/sites/default/files/downloads/9781789131116_ColorImages.pdf。
介绍 TensorBoard
在训练模型时,跟踪变量的变化可能是一项繁琐的工作。例如,在线性回归示例中,我们通过每 40 个 epochs 打印 MSE 损失和模型的参数来跟踪它们。随着算法复杂性的增加,需监控的变量和指标也会增多。幸运的是,这时 TensorBoard 就派上了用场。
TensorBoard 是一套可视化工具,能够用于绘制指标、可视化 TensorFlow 图表以及展示额外信息。一个典型的 TensorBoard 页面类似于以下截图所示:

图 2.6:标量 TensorBoard 页面
TensorBoard 与 TensorFlow 代码的集成非常简单,只需对代码做少量调整。特别是,为了使用 TensorBoard 可视化 MSE 损失随时间变化,并监控线性回归模型的权重和偏差,首先需要将损失张量附加到 tf.summary.scalar(),并将模型的参数附加到 tf.summary.histogram()*。以下代码片段应添加在优化器调用之后:
tf.summary.scalar('MSEloss', loss)
tf.summary.histogram('model_weight', v_weight)
tf.summary.histogram('model_bias', v_bias)
然后,为了简化过程并将其作为一个总结处理,我们可以将它们合并:
all_summary = tf.summary.merge_all()
此时,我们需要实例化一个FileWriter实例,它将把所有摘要信息记录到文件中:
now = datetime.now()
clock_time = "{}_{}.{}.{}".format(now.day, now.hour, now.minute, now.second)
file_writer = tf.summary.FileWriter('log_dir/'+clock_time, tf.get_default_graph())
前两行通过当前的日期和时间创建一个独特的文件名。在第三行,文件的路径和 TensorFlow 图形会传递给FileWriter()。第二个参数是可选的,表示要可视化的图形。
最后的修改是在训练循环中进行的,通过将之前的行train_loss, _ = session.run(..)替换为以下内容:
train_loss, _, train_summary = session.run([loss, opt, all_summary], feed_dict={x_ph:X, y_ph:y})
file_writer.add_summary(train_summary, ep)
首先,在当前会话中执行all_summary,然后将结果添加到file_writer中保存到文件。这一过程将运行之前合并的三个摘要,并将它们记录到日志文件中。TensorBoard 随后会从该文件读取并可视化标量、两个直方图以及计算图。
记得在结束时关闭file_writer,如下所示:
file_writer.close()
最后,我们可以通过进入工作目录并在终端中输入以下命令来打开 TensorBoard:
$ tensorboard --logdir=log_dir
此命令创建一个监听6006端口的 Web 服务器。要启动 TensorBoard,您需要访问 TensorBoard 显示的链接:

图 2.7:线性回归模型参数的直方图
现在,您可以通过点击页面顶部的选项卡来浏览 TensorBoard,访问图形、直方图和计算图。在之前和之后的截图中,您可以看到在这些页面上可视化的部分结果。图形和计算图是交互式的,因此请花时间浏览它们,以帮助您更好地理解它们的使用方法。还可以查看 TensorBoard 的官方文档(www.tensorflow.org/guide/summaries_and_tensorboard),了解更多关于 TensorBoard 附加功能的内容:

图 2.8:MSE 损失的标量图
强化学习(RL)环境的类型
环境,类似于监督学习中的标签数据集,是强化学习(RL)中的核心部分,因为它们决定了需要学习的信息和算法的选择。在本节中,我们将查看不同类型环境之间的主要区别,并列出一些最重要的开源环境。
为什么要有不同的环境?
虽然在实际应用中,环境的选择由要学习的任务决定,但在研究应用中,通常环境的选择由其内在特性决定。在后者的情况下,最终目标不是让智能体在特定任务上进行训练,而是展示一些与任务相关的能力。
例如,如果目标是创建一个多智能体 RL 算法,那么环境至少应包含两个智能体,并且要有彼此交流的方式,而与最终任务无关。相反,如果目标是创建一个终身学习者(即那些利用在之前更容易的任务中获得的知识,持续创建并学习更难任务的智能体),那么环境应该具备的主要特性是能够适应新情况,并且拥有一个现实的领域。
除了任务外,环境还可以在其他特征上有所不同,如复杂性、观察空间、动作空间和奖励函数:
-
复杂性:环境可以涵盖从平衡杆到用机器人手臂操控物理物体的广泛范围。可以选择更复杂的环境来展示算法处理大型状态空间的能力,模拟世界的复杂性。另一方面,也可以选择更简单的环境来展示算法的一些特定特性。
-
观察空间:正如我们已经看到的,观察空间可以从环境的完整状态到由感知系统感知的部分观察(如原始图像)。
-
动作空间:具有大规模连续动作空间的环境挑战智能体处理实值向量,而离散动作则更容易学习,因为它们只有有限的可用动作。
-
奖励函数:具有难度较大的探索和延迟奖励的环境,例如《蒙特祖玛的复仇》,非常具有挑战性,只有少数算法能够达到人类水平。因此,这些环境常用于测试旨在解决探索问题的算法。
开源环境
我们如何设计一个符合要求的环境?幸运的是,有许多开源环境专门为解决特定或更广泛的问题而构建。举个例子,CoinRun,如下图所示,旨在衡量算法的泛化能力:

图 2.9:CoinRun 环境
现在我们将列出一些主要的开源环境。这些环境由不同的团队和公司创建,但几乎所有环境都使用 OpenAI Gym 接口:

图 2.10:Roboschool 环境
-
Gym Atari (
gym.openai.com/envs/#atari):包括使用屏幕图像作为输入的 Atari 2600 游戏。它们对于衡量 RL 算法在具有相同观察空间的各种游戏中的表现非常有用。 -
Gym 经典控制 (
gym.openai.com/envs/#classic_control):经典游戏,可以用于轻松评估和调试算法。 -
Gym MuJoCo (
gym.openai.com/envs/#mujoco): 包含建立在 MuJoCo 上的连续控制任务(如 Ant 和 HalfCheetah),MuJoCo 是一个需要付费许可证的物理引擎(学生可以获得免费的许可证)。 -
MalmoEnv (
github.com/Microsoft/malmo): 基于 Minecraft 构建的环境。 -
Pommerman (
github.com/MultiAgentLearning/playground): 一个非常适合训练多智能体算法的环境。Pommerman 是著名游戏 Bomberman 的变种。 -
Roboschool (
github.com/openai/roboschool): 一个与 OpenAI Gym 集成的机器人仿真环境。它包含一个 MuJoCo 环境复制,如前面的截图所示,两个用于提高智能体鲁棒性的互动环境,以及一个多人游戏环境。 -
Duckietown (
github.com/duckietown/gym-duckietown): 一个具有不同地图和障碍物的自动驾驶汽车模拟器。 -
PLE (
github.com/ntasfi/PyGame-Learning-Environment): PLE 包括许多不同的街机游戏,例如 Monster Kong、FlappyBird 和 Snake。 -
Unity ML-Agents (
github.com/Unity-Technologies/ml-agents): 基于 Unity 构建的环境,具有逼真的物理效果。ML-agents 提供了高度的自由度,并且可以使用 Unity 创建自己的环境。 -
CoinRun (
github.com/openai/coinrun): 一个解决强化学习中过拟合问题的环境。它生成不同的环境用于训练和测试。 -
DeepMind Lab (
github.com/deepmind/lab): 提供了一套用于导航和解谜任务的 3D 环境。 -
DeepMind PySC2 (
github.com/deepmind/pysc2): 一个用于学习复杂游戏《星际争霸 II》的环境。
概述
希望在本章中,你已经了解了构建强化学习算法所需的所有工具和组件。你已经设置了开发强化学习算法所需的 Python 环境,并使用 OpenAI Gym 环境编写了你的第一个算法。由于大多数先进的强化学习算法都涉及深度学习,你也已经接触到了 TensorFlow——一本贯穿全书使用的深度学习框架。使用 TensorFlow 加速了深度强化学习算法的开发,因为它处理了深度神经网络中复杂的部分,比如反向传播。此外,TensorFlow 提供了 TensorBoard,这是一个用于监控和帮助算法调试过程的可视化工具。
因为在接下来的章节中我们将使用许多不同的环境,所以理解它们的差异和独特性非常重要。到现在为止,你应该能够为自己的项目选择最佳的环境,但请记住,尽管我们为你提供了一个全面的列表,可能还有许多其他环境更适合你的问题。
话虽如此,在接下来的章节中,你将最终学会如何开发强化学习(RL)算法。具体来说,在下一章中,你将接触到可以用于环境完全已知的简单问题的算法。之后,我们将构建更复杂的算法,以应对更复杂的情况。
问题
-
Gym 中
step()函数的输出是什么? -
如何通过 OpenAI Gym 接口采样一个动作?
-
Box和Discrete类之间的主要区别是什么? -
为什么深度学习框架在强化学习中被使用?
-
什么是张量?
-
在 TensorBoard 中可以可视化什么内容?
-
如果要创建一辆自动驾驶汽车,你会使用本章中提到的哪些环境?
进一步阅读
-
有关 TensorFlow 官方指南,请参考以下链接:
www.tensorflow.org/guide/low_level_intro。 -
有关 TensorBoard 官方指南,请参考以下链接:
www.tensorflow.org/guide/summaries_and_tensorboard。
第三章:使用动态规划解决问题
本章的目的是多方面的。我们将介绍许多对理解强化学习问题及其解决算法至关重要的主题。与前几章从广义和非技术角度讨论 强化学习 (RL) 不同,这里我们将正式化这一理解,以开发解决简单游戏的第一批算法。
强化学习问题可以被表述为 马尔可夫决策过程 (MDP),这是一个提供强化学习关键元素(如价值函数和期望奖励)形式化的框架。然后,可以使用这些数学组件创建强化学习算法。它们之间的不同在于这些组件是如何组合的,以及在设计时所做的假设。
正因如此,正如我们将在本章中看到的,强化学习算法可以分为三大类,这些类别之间可以相互重叠。这是因为某些算法可以将来自多个类别的特征结合在一起。解释完这些关键概念后,我们将介绍第一种类型的算法,称为动态规划,它可以在获得环境的完全信息时解决问题。
本章将涵盖以下主题:
-
MDP
-
强化学习算法分类
-
动态规划
MDP
MDP 表达了一个顺序决策问题,其中动作会影响下一个状态及其结果。MDP 足够通用和灵活,可以为通过交互学习目标的问题提供形式化,这正是强化学习所要解决的问题。因此,我们可以用 MDP 的语言来表述和推理强化学习问题。
MDP 是四元组 (S,A,P,R):
-
S 是状态空间,包含一个有限的状态集。
-
A 是动作空间,包含一个有限的动作集。
-
P 是转移函数,它定义了通过一个动作 a 从状态 s 转移到状态 s′ 的概率。在 P(s′, s, a) = p(s′| s, a) 中,转移函数等于 s′ 在给定 s 和 a 下的条件概率。
-
R 是奖励函数,它决定了在从状态 s 采取动作 a 后,转移到状态 s′ 时所获得的值。
以下图展示了一个 MDP 的示例。箭头表示两个状态之间的转移,箭头尾部附带转移概率,箭头主体则标注奖励。在其性质上,一个状态的转移概率之和必须等于 1。在此示例中,最终状态用一个方框表示(状态 S[5]),为了简便,我们将一个 MDP 表示为只有一个动作的情况:

图 3.1 示例:具有五个状态和一个动作的 MDP
MDP 由一系列离散的时间步骤控制,这些时间步骤创建了状态和动作的轨迹(S[0], A[0], S[1], A[1], ...),其中状态遵循 MDP 的动态,即状态转移函数 p(s′|s, a)。通过这种方式,转移函数完全表征了环境的动态。
根据定义,转移函数和奖励函数仅由当前状态决定,而不依赖于访问过的前一个状态序列。这个性质叫做马尔可夫性质,意味着该过程是无记忆的,未来状态只依赖于当前状态,而不依赖于其历史。因此,一个状态包含了所有信息。具有这种性质的系统称为完全可观察的。
在许多实际的强化学习(RL)案例中,马尔可夫性质并不成立,为了实用,我们可以假设它是一个马尔可夫决策过程(MDP),并使用有限数量的前一个状态(有限历史):S[t]、S[t-1]、S[t-2]、...、S[t-k]。在这种情况下,系统是部分可观察的,状态称为观测值。我们将在 Atari 游戏中使用这种策略,其中我们将使用行像素作为智能体的输入。这是因为单帧图像是静态的,并不包含关于物体速度或方向的信息。相反,这些值可以通过使用前三到四帧来获取(这仍然是一种近似)。
MDP 的最终目标是找到一个策略,π,最大化累计奖励,![],其中 R[π] 是按照策略 π 每一步获得的奖励。当一个策略在每个 MDP 状态中采取最佳可能的动作时,就找到了 MDP 的解。这种策略被称为最优策略。
策略
策略选择在给定情况下采取的动作,并可以分为确定性或随机性。
确定性策略表示为 a[t] = µ(st),而随机策略可以表示为 a[t] ~ π(.|s[t]),其中波浪号符号(~)表示有分布。当考虑一个动作分布时使用随机策略;例如,当需要向系统中注入噪声动作时。
一般来说,随机策略可以是分类的或高斯的。前者类似于分类问题,并通过在类别之间应用 softmax 函数进行计算。在后者中,动作是从高斯分布中采样的,通过均值和标准差(或方差)来描述。这些参数也可以是状态的函数。
当使用参数化策略时,我们将用字母 θ 来定义它们。例如,在确定性策略的情况下,它可以写作 µ[θ] (s[t])。
策略、决策者和智能体是表达相同概念的三个术语,因此在本书中,我们将交替使用这些术语。
回报
在 MDP 中运行策略时,状态和动作的序列(S[0], A[0], S[1], A[1],...)被称为轨迹或展开,并用
表示。在每个轨迹中,动作的结果会收集一系列奖励。这些奖励的函数称为回报,在其最简化的版本中,定义如下:

此时,回报可以单独分析无限和有限时间范围的轨迹。之所以需要这种区分,是因为在环境中进行的交互如果没有结束,之前展示的和将始终具有无限值的总和。这种情况是危险的,因为它并没有提供任何有用的信息。此类任务被称为持续任务,需要对奖励进行另一种表述。最好的解决方案是对短期奖励赋予更大权重,同时对远期奖励赋予较少的关注。这个过程可以通过使用一个介于 0 和 1 之间的值来实现,这个值称为折扣因子,通常用符号 λ*表示。因此,回报G可以重新表述如下:

这个公式可以看作是倾向于选择那些与远期奖励相较较近的动作的一种方式。举个例子——假设你中了彩票,你可以决定何时领取奖金。你可能更愿意在几天内领取,而不是几年后领取。
就是定义你愿意等待多长时间领取奖金的值。如果
,那意味着你不在乎何时领取奖金。如果 ![],那就意味着你希望立即领取。
在有限时间范围的轨迹中,即有自然结束的轨迹,任务被称为回合式任务(这个术语来源于“episode”,即轨迹的另一种说法)。在回合式任务中,原始公式(1)依然适用,但通常更偏好对其进行带有折扣因子的变体处理:

在有限但较长的时间范围内,使用折扣因子可以增加算法的稳定性,因为远期奖励仅部分考虑。实际上,折扣因子的值通常在 0.9 到 0.999 之间使用。
对公式(3)的一个简单但非常有用的分解是将回报定义为在时间步* t + 1 *的回报:

简化符号后,它变为以下形式:

然后,使用回报符号,我们可以将强化学习的目标定义为找到一个最优政策,
,使其最大化预期回报,表示为![],其中![]是随机变量的期望值。
值函数
回报![]提供了关于轨迹值的良好洞察,但仍然没有提供关于单个访问状态质量的任何指示。这个质量指标非常重要,因为它可以被策略用来选择下一个最佳动作。策略只需选择一个能够导致下一个质量最高状态的动作。值函数正是这样做的:它根据政策从某一状态开始的预期回报来估计质量。形式上,值函数定义如下:

动作值函数类似于值函数,它是从某个状态出发的预期回报,但也依赖于第一个动作。其定义如下:

值函数和动作值函数分别也被称为V-函数和Q-函数,它们是严格相关的,因为值函数也可以通过动作值函数来定义:

知道了最优![],最优值函数如下:

这是因为最优动作是![]。
贝尔曼方程
V和Q可以通过运行遵循策略的轨迹,
,然后对获得的值进行平均来估计。这种技术有效,且在许多场合中得到应用,但考虑到回报需要整个轨迹中的奖励,它是非常昂贵的。
幸运的是,贝尔曼方程递归地定义了动作值函数和值函数,使得它们能够从后续状态中进行估计。贝尔曼方程通过使用当前状态获得的奖励和其后继状态的值来实现这一点。我们已经看到了回报的递归公式(在公式(5)中),并且可以将其应用于状态值:

类似地,我们可以将贝尔曼方程应用于动作值函数:

现在,使用(6)和(7),![]和![]仅根据连续状态的值进行更新,而不需要像旧定义中那样展开整个轨迹到终点。
强化学习算法分类
在深入讨论解决最优贝尔曼方程的第一个强化学习算法之前,我们想先提供一个广泛但详细的强化学习算法概述。我们需要这样做,因为它们之间的区别可能会让人感到困惑。算法的设计涉及多个部分,且在决定哪种算法最适合用户的实际需求之前,需要考虑许多特性。本概述的范围呈现了强化学习的宏观图景,以便在接下来的章节中,我们将提供这些算法的全面理论和实践视角时,你将已经掌握它们的总体目标,并清晰地了解它们在强化学习算法地图中的位置。
第一个区别是在基于模型和无模型算法之间。顾名思义,基于模型的算法需要一个环境模型,而无模型算法则不依赖于此条件。环境模型非常有价值,因为它包含了可以用来找到期望策略的宝贵信息;然而,在大多数情况下,模型几乎无法获取。例如,模拟井字游戏很容易,而模拟海浪则相当困难。为此,无模型算法可以在不假设环境的情况下学习信息。强化学习算法的类别在图 3.2 中有所展示。
这里展示了基于模型和无模型之间的区别,以及两种广为人知的无模型方法,即策略梯度和基于价值的方法。此外,正如我们将在后续章节中看到的,这些方法的结合是可能的:

图 3.2. 强化学习算法的分类
第一个区别是在无模型和基于模型之间。无模型强化学习算法可以进一步细分为策略梯度算法和基于价值的算法。混合方法是结合了两者重要特性的算法。
无模型算法
在没有模型的情况下,无模型(MF)算法在给定的策略下运行轨迹以获取经验并改进智能体。MF 算法由三个主要步骤组成,直到创建出一个良好的策略,这三个步骤会不断重复:
-
通过在环境中运行策略生成新样本。轨迹会一直运行直到达到最终状态,或运行固定的步数。
-
回报函数的估计。
-
使用收集的样本和步骤 2 中完成的估计来改进策略。
这三个组件是此类算法的核心,但根据每个步骤的执行方式,它们会生成不同的算法。基于价值的算法和策略梯度算法就是两个这样的例子。它们看起来非常不同,但它们基于相似的原则,且都采用了三步法。
基于价值的算法
基于价值的算法,也称为价值函数算法,使用与我们在前一部分中看到的非常相似的范式。也就是说,它们使用贝尔曼方程来学习 Q 函数,进而用于学习策略。在最常见的设置中,它们使用深度神经网络作为函数逼近器,并采用其他技巧来处理高方差和一般的不稳定性。在某种程度上,基于价值的算法更接近监督回归算法。
通常,这些算法是离策略的,意味着它们不需要优化用于生成数据的相同策略。这意味着这些方法可以从以前的经验中学习,因为它们可以将采样数据存储在回放缓冲区中。使用以前样本的能力使得价值函数比其他无模型算法更具样本效率。
策略梯度算法
另一类无模型(MF)算法是策略梯度方法(或策略优化方法)。它们对强化学习问题有更直接和明显的解释,因为它们通过更新参数以改进的方向直接从一个参数化策略中学习。其基于强化学习原理,即应当鼓励好的行动(通过提高策略的梯度),并且应当抑制坏的行动。
与价值函数算法相反,策略优化主要依赖于策略数据,这使得这些算法在样本效率上较低。策略优化方法可能会由于在存在高曲率的表面上采取最陡的上升路径而变得非常不稳定,这容易导致在某一方向上走得太远,最终跌入不好的区域。为了解决这个问题,提出了许多算法,例如仅在信任区域内优化策略,或者优化一个代理裁剪目标函数以限制策略的变化。
策略梯度方法的一个主要优点是它们能够轻松处理具有连续动作空间的环境。这对于基于价值的算法来说是一个非常困难的问题,因为它们为离散的状态和动作对学习 Q 值。
演员-评论家算法
演员-评论家(AC)算法是离策略的策略梯度算法,它们还学习一个价值函数(通常是 Q 函数),这个函数叫做评论家,用来给策略(演员)提供反馈。想象一下,你,作为演员,想要通过一条新路线去超市。不幸的是,在到达目的地之前,你的老板打电话要求你回去工作。因为你没有到达超市,所以你不知道新路是否比旧路更快。但如果你到达了一个熟悉的地方,你可以估算从那里到超市所需的时间,并计算是否新路更优。这种估算就是评论家的作用。通过这种方式,即使你没有到达最终目标,你也可以改进演员的表现。
将评论者与行动者结合已被证明非常有效,并且在策略梯度算法中被广泛使用。这项技术还可以与其他用于策略优化的思想相结合,例如信任区域算法。
混合算法
价值函数和策略梯度算法的优势可以结合,创造出混合算法,这些算法可能更高效并且更加稳健。
混合方法将 Q 函数和策略梯度结合在一起,互相促进和改善。这些方法估计确定性动作的期望 Q 函数,以直接改善策略。
请注意,由于 AC 算法学习并使用价值函数,因此它们被归类为策略梯度,而不是混合算法。这是因为其主要目标是策略梯度方法,价值函数只是为了提供额外信息的一种升级。
基于模型的 RL
拥有环境模型意味着每个状态-动作元组的状态转换和奖励可以预测(无需与真实环境交互)。正如我们之前提到的,模型在有限的情况下是已知的,但当它已知时,可以以多种方式使用。模型最明显的应用是用于规划未来的动作。规划是一个用于表达组织未来移动的概念,当下一步动作的后果已经知道时。例如,如果你完全知道敌人将采取哪些行动,你可以提前思考并在执行第一步之前规划所有的动作。缺点是,规划可能非常昂贵,而且不是一个简单的过程。
通过与环境的交互,模型也可以通过吸收动作的后果(包括状态和奖励)来学习。这种解决方案并不总是最优的,因为在现实世界中,教一个模型可能非常昂贵。而且,如果模型只对环境有一个粗略的近似理解,可能会导致灾难性的结果。
无论是已知的还是通过学习得到的模型,都可以用来进行规划和改进策略,并可以集成到 RL 算法的不同阶段。基于模型的 RL 的著名案例包括纯规划、嵌入式规划以改进策略以及从近似模型中生成的样本。
一组使用模型估计价值函数的算法称为动态规划(DP),将在本章稍后进行研究。
算法多样性
为什么有那么多种强化学习算法?这是因为没有一种算法在所有情况下都比其他算法更好。每种算法都是为不同的需求设计的,旨在处理不同的方面。最显著的差异包括稳定性、样本效率和墙时(训练时间)。随着我们逐步深入本书,这些差异会更加明确,但作为经验法则,策略梯度算法比值函数算法更加稳定和可靠。另一方面,值函数方法更具样本效率,因为它们是离策略的,并且可以利用先前的经验。反过来,基于模型的算法比 Q 学习算法更具样本效率,但它们的计算成本更高,且速度更慢。
除了刚才提到的那些,还有其他一些权衡需要在设计和部署算法时考虑(例如易用性和鲁棒性),这不是一个简单的过程。
动态规划
动态规划(DP)是一种通用的算法范式,它将一个问题分解为多个重叠的子问题,然后通过结合子问题的解决方案来找到原始问题的解。
DP 可以用于强化学习,并且是最简单的方式之一。它通过提供环境的完美模型来计算最优策略。
动态规划(DP)是强化学习算法历史中的一个重要里程碑,并为下一代算法奠定了基础,但其计算成本非常高。DP 适用于具有有限状态和动作的马尔可夫决策过程(MDP),因为它必须更新每个状态(或动作值)的值,同时考虑到所有其他可能的状态。此外,DP 算法将价值函数存储在数组或表格中。这种存储信息的方式是有效且快速的,因为没有信息丢失,但它确实需要存储大量的表格。由于 DP 算法使用表格存储价值函数,因此被称为表格学习。这与近似学习相对,后者使用近似价值函数将值存储在固定大小的函数中,例如人工神经网络。
DP 使用自举,意味着它通过使用后续状态的期望值来改进状态的估计值。正如我们之前看到的,自举在贝尔曼方程中被使用。实际上,DP 应用了贝尔曼方程(6)和(7)来估算
和/或
。这可以通过以下方式完成:

或者通过使用 Q 函数:

然后,一旦找到最优的价值函数和动作价值函数,就可以通过采取最大化期望的行动来找到最优策略。
策略评估与策略改进
为了找到最优策略,你首先需要找到最优的价值函数。一个执行这一过程的迭代方法称为策略评估——它通过模型的状态值转移、下一个状态的期望以及即时奖励,创建一个
序列,逐步改进一个策略的价值函数,
。因此,它使用贝尔曼方程创建一个不断改进的价值函数序列:

这个序列将随着
的变化而收敛到最优值。图 3.3 展示了使用连续状态值更新
:

图 3.3. 使用公式(8)更新
只有在知道每个状态和动作的状态转移函数p和奖励函数r时,价值函数(8)才可以更新,因此只有在环境的模型完全已知时才能更新。
请注意,在(8)中的第一个动作求和是针对随机策略所必需的,因为该策略为每个动作输出一个概率。为了简便起见,从现在开始我们只考虑确定性策略。
一旦价值函数得到改进,它就可以用来找到更好的策略。这个过程称为策略改进,其目的是找到一个策略,
,如下所示:

它从原始策略的价值函数,
,创建一个策略,
。如正式证明,新策略,
,总是比
更好,并且当且仅当
是最优时,策略才是最优的。策略评估和策略改进的结合产生了两种计算最优策略的算法。一种叫做策略迭代,另一种叫做价值迭代。两者都使用策略评估来单调地改进价值函数,并使用策略改进来估计新的策略。唯一的区别在于,策略迭代按循环方式执行这两个阶段,而价值迭代将它们合并在单一的更新中。
策略迭代
策略迭代在策略评估和策略改进之间循环,策略评估使用公式(8)在当前策略下更新
,而策略改进(9)则利用改进后的价值函数计算
,该函数由
表示。最终,经过
次循环,算法将得到一个最优策略
。
伪代码如下:
Initialize  and  for every state
while  is not stable:
> policy evaluation
while  is not stable:
for each state s:
> policy improvement
for each state s:
在初始化阶段之后,外部循环会在策略评估和策略迭代之间迭代,直到找到一个稳定的策略。在每次迭代中,策略评估会评估前一步策略改进步骤中找到的策略,而这些步骤则使用估算的价值函数。
应用策略迭代到 FrozenLake
为了巩固策略迭代背后的思想,我们将其应用于一个名为 FrozenLake 的游戏。在这里,环境由一个 4 x 4 的网格组成。通过四个动作对应四个方向(0 表示左,1 表示下,2 表示右,3 表示上),代理需要移动到网格的另一端,而不能掉入洞中。此外,移动是不确定的,代理有可能朝其他方向移动。因此,在这种情况下,可能会有益于不沿着预定的方向移动。当达到目标时,奖励为+1。游戏地图如图 3.4 所示,S 是起始位置,星星是目标位置,螺旋是洞:

图 3.4 FrozenLake 游戏地图
准备好所有所需工具后,让我们看看如何解决这个问题。
本章中解释的所有代码都可以在本书的 GitHub 仓库中找到,链接如下:https://github.com/PacktPublishing/Reinforcement-Learning-Algorithms-with-Python
首先,我们需要创建环境,初始化价值函数和策略:
env = gym.make('FrozenLake-v0')
env = env.unwrapped
nA = env.action_space.n
nS = env.observation_space.n
V = np.zeros(nS)
policy = np.zeros(nS)
然后,我们需要创建一个主循环,执行一次策略评估和一次策略改进。该循环会在策略稳定时结束。为此,请使用以下代码:
policy_stable = False
it = 0
while not policy_stable:
policy_evaluation(V, policy)
policy_stable = policy_improvement(V, policy)
it += 1
最后,我们可以打印出完成的迭代次数、价值函数、策略以及通过运行一些测试游戏所达到的得分:
print('Converged after %i policy iterations'%(it))
run_episodes(env, V, policy)
print(V.reshape((4,4)))
print(policy.reshape((4,4)))
现在,在定义policy_evaluation之前,我们可以创建一个函数来评估预期的动作值,这个函数也将被用于policy_improvement:
def eval_state_action(V, s, a, gamma=0.99):
return np.sum([p * (rew + gamma*V[next_s]) for p, next_s, rew, _ in env.P[s][a]])
在这里,env.P是一个字典,包含了关于环境动态的所有信息。
gamma是折扣因子,0.99 是一个标准值,适用于简单和中等难度的问题。它越高,代理预测一个状态的值就越困难,因为它需要展望更远的未来。
接下来,我们可以定义policy_evaluation函数。policy_evaluation需要根据当前策略计算公式(8),并对每个状态进行计算,直到达到稳定值。因为策略是确定性的,所以我们只需要评估一个动作:
def policy_evaluation(V, policy, eps=0.0001):
while True:
delta = 0
for s in range(nS):
old_v = V[s]
V[s] = eval_state_action(V, s, policy[s])
delta = max(delta, np.abs(old_v - V[s]))
if delta < eps:
break
当delta小于阈值eps时,我们认为值函数稳定。当这些条件满足时,while循环语句被停止。
policy_improvement 接收值函数和策略,并对所有状态进行迭代,基于新的值函数更新策略:
def policy_improvement(V, policy):
policy_stable = True
for s in range(nS):
old_a = policy[s]
policy[s] = np.argmax([eval_state_action(V, s, a) for a in range(nA)])
if old_a != policy[s]:
policy_stable = False
return policy_stable
policy_improvement(V, policy)在策略发生变化之前返回False。这是因为它意味着策略尚未稳定。
最后一段代码运行一些游戏来测试新策略,并打印赢得的游戏次数:
def run_episodes(env, V, policy, num_games=100):
tot_rew = 0
state = env.reset()
for _ in range(num_games):
done = False
while not done:
next_state, reward, done, _ = env.step(policy[state])
state = next_state
tot_rew += reward
if done:
state = env.reset()
print('Won %i of %i games!'%(tot_rew, num_games))
就是这样。
它大约在 7 次迭代后收敛,并且赢得了大约 85%的游戏:

图 3.5 冰湖游戏的结果。最优策略在左侧,最优状态值在右侧。
从代码得到的策略显示在图 3.5 的左侧。你可以看到它走的是一些奇怪的路线,但这只是因为它遵循了环境的动态。在图 3.5 的右侧,展示了最终状态的值。
值迭代
值迭代是另一种用于在 MDP 中寻找最优值的动态规划算法,但与执行策略评估和策略迭代循环的策略迭代不同,值迭代将这两种方法合并为一次更新。特别地,它通过立即选择最佳动作来更新状态的值:

值迭代的代码比策略迭代的代码更简单,以下是总结的伪代码:
Initialize for every state
while is not stable:
> value iteration
for each state s:
> compute the optimal policy:
唯一的区别在于新的值估计更新和没有适当的策略迭代模块。最终得到的最优策略如下:

应用值迭代到冰湖
现在我们可以将值迭代应用于冰湖游戏,以便比较这两种动态规划算法,并查看它们是否会收敛到相同的策略和值函数。
我们像之前一样定义eval_state_action,用来估算状态-动作对的动作状态值:
def eval_state_action(V, s, a, gamma=0.99):
return np.sum([p * (rew + gamma*V[next_s]) for p, next_s, rew, _ in env.P[s][a]])
接下来,我们创建值迭代算法的主体部分:
def value_iteration(eps=0.0001):
V = np.zeros(nS)
it = 0
while True:
delta = 0
# update the value for each state
for s in range(nS):
old_v = V[s]
V[s] = np.max([eval_state_action(V, s, a) for a in range(nA)]) # equation 3.10
delta = max(delta, np.abs(old_v - V[s]))
# if stable, break the cycle
if delta < eps:
break
else:
print('Iter:', it, ' delta:', np.round(delta,5))
it += 1
return V
它会循环直到达到稳定的值函数(由阈值eps决定),并且在每次迭代中,使用公式(10)更新每个状态的值。
至于策略迭代,run_episodes执行一些游戏来测试策略。唯一的不同之处在于,在这种情况下,策略是在执行run_episodes时同时确定的(对于策略迭代,我们提前为每个状态定义了动作):
def run_episodes(env, V, num_games=100):
tot_rew = 0
state = env.reset()
for _ in range(num_games):
done = False
while not done:
# choose the best action using the value function
action = np.argmax([eval_state_action(V, state, a) for a in range(nA)]) #(11)
next_state, reward, done, _ = env.step(action)
state = next_state
tot_rew += reward
if done:
state = env.reset()
print('Won %i of %i games!'%(tot_rew, num_games))
最后,我们可以创建环境,解开它,运行值迭代,并执行一些测试游戏:
env = gym.make('FrozenLake-v0')
env = env.unwrapped
nA = env.action_space.n
nS = env.observation_space.n
V = value_iteration(eps=0.0001)
run_episodes(env, V, 100)
print(V.reshape((4,4)))
输出结果将类似于以下内容:
Iter: 0 delta: 0.33333
Iter: 1 delta: 0.1463
Iter: 2 delta: 0.10854
...
Iter: 128 delta: 0.00011
Iter: 129 delta: 0.00011
Iter: 130 delta: 0.0001
Won 86 of 100 games!
[[0.54083394 0.49722378 0.46884941 0.45487071]
[0.55739213 0\. 0.35755091 0\. ]
[0.5909355 0.64245898 0.61466487 0\. ]
[0\. 0.74129273 0.86262154 0\. ]]
值迭代算法在 130 次迭代后收敛。得到的值函数和策略与策略迭代算法相同。
总结
一个 RL 问题可以被形式化为一个 MDP,为学习目标驱动问题提供了一个抽象框架。MDP 由一组状态、动作、奖励和转移概率定义,解决 MDP 意味着找到一个在每个状态下最大化期望奖励的策略。马尔可夫性质是 MDP 的内在特性,它确保未来的状态仅依赖于当前状态,而不依赖于历史。
使用 MDP 的定义,我们提出了策略、回报函数、期望回报、动作-值函数和值函数的概念。后两者可以通过后续状态的值来定义,这些方程被称为贝尔曼方程。这些方程非常有用,因为它们提供了一种迭代计算值函数的方法。然后,最优值函数可以用来找到最优策略。
RL 算法可以分为基于模型的和无模型的。前者需要环境的模型来规划下一步动作,而后者独立于模型,可以通过与环境的直接交互来学习。无模型算法可以进一步分为策略梯度算法和价值函数算法。策略梯度算法通过梯度上升直接从策略中学习,通常是基于策略的。价值函数算法通常是离策略的,它们学习动作-值函数或值函数来创建策略。这两种方法可以结合在一起,产生兼具两者优点的方法。
DP 是我们深入研究的第一组基于模型的算法。它在已知环境完整模型且由有限数量的状态和动作构成时使用。DP 算法通过自举方法估计状态的值,并通过两个过程学习最优策略:策略评估和策略改进。策略评估计算任意策略的状态值函数,而策略改进则通过使用策略评估过程得到的值函数来改进策略。
通过结合策略改进和策略评估,可以创建策略迭代算法和值迭代算法。两者的主要区别在于,策略迭代是通过策略评估和策略改进的迭代过程来运行的,而值迭代则将这两个过程合并为一次更新。
尽管动态规划受到维度灾难的困扰(复杂度随着状态数量呈指数增长),但策略评估和策略迭代背后的思想几乎在所有强化学习算法中都是关键,因为它们使用了这些思想的广义版本。
动态规划的另一个缺点是它需要环境的精确模型,这限制了它在许多其他问题中的应用。
在下一章中,你将看到如何使用 V 函数和 Q 函数来学习策略,利用模型未知的情况下,直接从环境中采样的问题。
问题
-
什么是马尔可夫决策过程(MDP)?
-
什么是随机策略?
-
如何用下一个时间步的回报来定义回报函数?
-
为什么贝尔曼方程如此重要?
-
动态规划(DP)算法的限制因素是什么?
-
什么是策略评估?
-
策略迭代和值迭代有何不同?
进一步阅读
- Sutton 和 Barto,《强化学习》,第三章和第四章
第二部分:无模型强化学习算法
本节介绍了无模型强化学习算法、基于值的方法以及策略梯度方法。你还将开发许多最前沿的算法。
本节包含以下章节:
-
第四章,Q 学习与 SARSA 应用
-
第五章,深度 Q 网络
-
第六章,学习随机过程与 PG 优化
-
第七章,TRPO 与 PPO 实现
-
第八章,DDPG 与 TD3 应用
第四章:Q-Learning 和 SARSA 应用
动态规划(DP)算法对于解决 强化学习(RL)问题非常有效,但它们需要两个强假设。第一个假设是必须知道环境的模型,第二个假设是状态空间必须足够小,以至于不会受到维度灾难问题的影响。
在本章中,我们将开发一类摆脱第一个假设的算法。此外,这是一类不受动态规划(DP)算法维度灾难问题影响的算法。这些算法直接从环境和经验中学习,根据许多回报估计价值函数,而不是像 DP 算法那样使用模型计算状态值的期望。在这种新设置下,我们将讨论经验作为学习价值函数的方式。我们将研究通过与环境的互动来学习策略时出现的问题,以及可以用来解决这些问题的技术。在简要介绍这一新方法后,你将了解 时序差分(TD)学习,这是一种通过经验学习最优策略的强大方法。TD 学习借鉴了 DP 算法的思想,同时只使用从与环境互动中获得的信息。两种时序差分学习算法是 SARSA 和 Q-learning。虽然它们非常相似,并且都能在表格化的情况下保证收敛,但它们有一些有趣的差异,值得我们注意。Q-learning 是一个关键算法,许多最先进的强化学习(RL)算法结合其他技术时使用这种方法,正如我们将在后续章节中看到的那样。
为了更好地理解 TD 学习并了解如何从理论过渡到实践,你将实现 Q-learning 和 SARSA 在一个新游戏中的应用。然后,我们将详细阐述这两种算法的区别,既包括它们的表现,也包括它们的使用。
本章将涵盖以下主题:
-
无模型学习
-
TD 学习
-
SARSA
-
将 SARSA 应用到 Taxi-v2
-
Q-learning
-
将 Q-learning 应用到 Taxi-v2
无模型学习
根据定义,策略的价值函数是从给定状态开始该策略的期望回报(即折扣奖励的总和):

根据第三章《使用动态规划解决问题》的推理,DP 算法通过计算所有下一状态的期望来更新状态值:

不幸的是,计算价值函数意味着你需要知道状态转移概率。事实上,动态规划(DP)算法使用环境模型来获得这些概率。但主要的问题是,当这些信息不可用时该怎么办。最好的答案是通过与环境交互来获取所有信息。如果做得好,这种方法有效,因为通过从环境中采样多次,你应该能够近似期望值,并对价值函数有一个良好的估计。
用户体验
现在,我们需要首先澄清的是如何从环境中采样,以及如何与环境互动以获取有关其动态的可用信息:

图 4.1. 从状态![]开始的轨迹
做这件事的简单方法是执行当前策略直到本回合结束。你最终会得到如图 4.1 所示的轨迹。一旦回合结束,可以通过向上反向传播奖励总和来为每个状态计算回报值,![]。为每个状态重复这个过程多次(即运行多个轨迹),就会得到多个回报值。然后对这些回报值进行平均,以计算期望回报。以这种方式计算的期望回报是一个近似的价值函数。执行策略直到终止状态称为轨迹或回合。运行更多的轨迹会观察到更多的回报,根据大数法则,这些估计的平均值将会收敛到期望值。
与动态规划类似,通过与环境直接交互来学习策略的算法依赖于策略评估和策略改进的概念。策略评估是估算策略的价值函数,而策略改进则利用上一阶段的估算来改进策略。
策略评估
我们刚刚看到,使用真实经验来估算价值函数是一个简单的过程。这涉及到在环境中执行策略直到达到最终状态,然后计算回报值并对采样回报进行平均,如方程(1)所示:

因此,状态的期望回报可以通过对来自该状态的采样经历进行平均来近似。使用(1)估算回报函数的方法称为蒙特卡洛方法。直到所有状态-动作对都被访问并且足够的轨迹被采样,蒙特卡洛方法才能保证收敛到最优策略。
探索问题
我们如何保证每个状态的每个动作都会被选择?为什么这如此重要?我们将首先回答后一个问题,然后展示我们如何(至少在理论上)探索环境以到达每一个可能的状态。
为什么要探索?
轨迹是通过一个策略进行采样的,这个策略可以是随机的也可以是确定性的。在确定性策略的情况下,每次采样轨迹时,访问的状态将始终是相同的,值函数的更新只会考虑这一有限的状态集。这将大大限制你对环境的了解。就像从一位永远不会改变自己观点的老师那里学习——你将停留在这些想法中,无法了解其他的知识。
因此,环境的探索至关重要,如果你想获得好的结果,它确保了没有更好的策略可以被发现。
另一方面,如果一个策略被设计成不断探索环境,而不考虑已经学到的东西,那么实现一个好的策略将变得非常困难,甚至可能是不可能的。探索与利用(按照当前最好的策略行事)之间的平衡被称为探索-利用困境,且将在第十二章中详细讨论,开发一个 ESBAS 算法。
如何进行探索
当处理此类情况时,一个非常有效的方法叫做
-贪心探索。它是在以概率
随机行动的同时,以概率
采取贪心行动(即选择最佳行动)。例如,如果
,平均而言,每 10 次行动中,智能体将随机行动 8 次。
为了避免在智能体对自己的知识充满信心时,过多地进行探索,
可以随着时间的推移逐渐减少。这个策略被称为epsilon 衰减。通过这种变化,最初的随机策略将逐渐收敛到一个确定性的,并且希望是最优的策略。
还有许多其他的探索技术(如 Boltzmann 探索),它们更为精确,但也相当复杂,对于本章而言,
-贪心策略是一个完美的选择。
TD 学习
蒙特卡罗方法是一种强大的直接通过从环境中采样来学习的方法,但它们有一个很大的缺点——它们依赖于完整的轨迹。它们必须等到整个回合结束,才能更新状态值。因此,一个关键问题是,当轨迹没有结束时,或者如果轨迹非常长时,会发生什么情况。答案是它将产生可怕的结果。针对这个问题,动态规划算法已经提出了类似的解决方案,状态值在每一步更新,而不是等到结束后才更新。它并不是使用在轨迹过程中累积的完整回报,而是使用即时奖励和下一状态值的估计。这种更新的可视化示例在图 4.2 中给出,展示了学习过程中的单步涉及的部分。这个技术叫做自举,它不仅对长回合或可能无限的回合有用,对于任何长度的回合同样有效。其第一个原因是,它有助于减少期望回报的方差。方差减少的原因是,状态值只依赖于即时的下一个奖励,而不依赖于整个轨迹的所有奖励。第二个原因是,学习过程在每一步都进行,使这些算法能够在线学习。因此,这被称为一步学习。相反,蒙特卡罗方法是离线的,因为它们只在回合结束后才使用信息。使用自举进行在线学习的方法称为 TD 学习方法*。

图 4.2 一步学习更新与自举
TD 学习可以看作是蒙特卡罗方法和动态规划(DP)方法的结合,因为它们借鉴了前者的采样思想和后者的自举(bootstrapping)思想。TD 学习广泛应用于强化学习(RL)算法中,构成了许多算法的核心。本章稍后介绍的算法(即 SARSA 和 Q-learning)都是一步、表格型、无模型(意味着它们不使用环境的模型)TD 方法。
TD 更新
从上一章使用动态规划解决问题中,我们知道以下内容:

通过经验方法,蒙特卡罗更新通过对多个完整轨迹的回报进行平均来估算这个值。进一步推导该方程,我们得到如下结果:

前面的方程通过动态规划算法进行了逼近。不同之处在于,TD 算法通过估算期望值而不是计算它。估算的方式与蒙特卡罗方法相同,即通过平均来进行:

实际上,TD 更新不是计算平均值,而是通过向最优值小幅度地改善状态值来完成更新:

是一个常数,用来确定每次更新时状态值应有多少变化。如果
,那么状态值将完全不变。相反,如果
,状态值将等于
(称为 TD 目标),并且完全忘记之前的值。在实践中,我们不希望出现这些极端情况,通常
的范围从 0.5 到 0.001。
策策改进
TD 学习在每个状态的每个动作都有大于零的选择概率时会收敛到最优条件。为了满足这一要求,正如我们在前一部分看到的,TD 方法必须进行环境探索。事实上,探索可以通过使用
-贪心策略来进行。它确保在选择动作时既有贪心动作,也有随机动作,从而确保了环境的开发和探索。
比较蒙特卡洛和 TD
蒙特卡洛 TD 方法的一个重要特点是,只要它们处理的是表格型问题(即状态值存储在表格或数组中)并且具有探索性策略,它们就能收敛到最优解。然而,它们在更新值函数的方式上有所不同。总体而言,TD 学习的方差较低,但偏差高于蒙特卡洛学习。除此之外,TD 方法通常在实践中更快,因此更受偏好,相较于蒙特卡洛方法。
SARSA
到目前为止,我们将 TD 学习呈现为一种估算给定策略的值函数的通用方法。在实践中,TD 方法无法直接使用,因为它缺少实际改进策略的主要组件。SARSA 和 Q-learning 是两种一阶、表格型的 TD 算法,它们都可以估算值函数并优化策略,能够广泛应用于各种强化学习问题。在本节中,我们将使用 SARSA 学习给定马尔可夫决策过程(MDP)的最优策略。然后,我们将介绍 Q-learning。
TD 学习的一个问题是它估算的是状态的价值。想一想,在一个给定的状态下,你如何选择具有最高下一个状态值的动作?我们之前说过,你应该选择那个能把智能体移动到具有最高值的状态的动作。然而,在没有环境模型的情况下,它无法提供可能的下一个状态列表,你无法知道哪个动作能将智能体移动到那个状态。SARSA 不是学习值函数,而是学习并应用状态-动作函数,
。
告诉我们一个状态的价值,
,如果采取的动作是
。
算法
基本上,我们对 TD 更新所做的所有观察对于 SARSA 也是有效的。一旦我们将其应用于 Q 函数的定义,就得到了 SARSA 更新:

是一个系数,用于确定动作值更新了多少。
是折扣因子,一个介于 0 和 1 之间的系数,用于降低来自遥远未来决策的值的重要性(短期动作比长期动作更受偏好)。SARSA 更新的视觉解释见图 4.3。
SARSA 这个名字来源于基于状态的更新,
;
动作,
,奖励,
;下一个状态,
;最后,下一个动作,
。将所有内容结合起来,形成
,如图 4.3 所示:

图 4.3 SARSA 更新
SARSA 是一种在策略算法。所谓“在策略”,意味着通过与环境互动收集经验的策略(称为行为策略)就是被更新的策略。该方法的在策略特性来自于使用当前策略来选择下一个动作,
,以估计
,并假设在随后的动作中将遵循相同的策略(即根据动作
执行)。
在策略算法通常比离策略算法更简单,但它们的表现力较弱,通常需要更多的数据来学习。尽管如此,就像 TD 学习一样,如果 SARSA 每次都访问每个状态-动作无限次,并且随着时间推移,策略变得确定性,那么它是可以保证收敛到最优策略的。实际算法通常使用
-贪心策略,并伴有逐渐趋近于零的衰减。SARSA 的伪代码总结如下。在伪代码中,我们使用了
-贪心策略,但可以使用任何鼓励探索的策略:
Initialize for every state-action pair
for episodes:
while is not a final state:
# env() take a step in the environment
是一个实现
策略的函数。请注意,SARSA 执行与前一步骤中选择并使用的相同动作来更新状态-动作值。
将 SARSA 应用于 Taxi-v2
在对 TD 学习特别是 SARSA 进行更理论化的探讨之后,我们终于能够实现 SARSA 来解决感兴趣的问题。正如我们之前所看到的,SARSA 可以应用于具有未知模型和动态的环境,但由于它是一个具有可扩展性限制的表格化算法,因此只能应用于具有小规模离散动作和状态空间的环境。因此,我们选择将 SARSA 应用于一个名为 Taxi-v2 的 gym 环境,它满足所有要求,并且是这类算法的良好测试平台。
Taxi-v2 是一个用于研究层次化强化学习(这是一种创建策略层次结构的 RL 算法,每个策略的目标是解决一个子任务)游戏,其目标是接载一个乘客并将其送到指定位置。当出租车成功完成送客任务时,获得 +20 的奖励,但如果非法接送或送达,将受到 -10 的惩罚。此外,每经过一个时间步,都会失去 1 分。游戏的渲染效果如图 4.4 所示。该环境有六个合法动作,分别对应四个方向、接载和送客动作。在图 4.4 中,: 符号代表空位置,| 符号代表出租车无法穿越的墙壁,R, G, Y, B 是四个位置。出租车(图中的黄色矩形)必须在浅蓝色标识的地点接载乘客,并将其送到紫色标识的地点。

图 4.4 Taxi-v2 环境的起始状态
实现过程相当直接,遵循前一节中给出的伪代码。尽管我们在这里解释并展示了所有代码,但也可以在本书的 GitHub 仓库中找到。
首先实现 SARSA 算法的主函数 SARSA(..),它完成了大部分工作。之后,我们将实现一些辅助函数,执行简单但重要的任务。
SARSA 需要一个环境和一些其他超参数作为参数才能正常工作:
-
学习率
lr,以前称为
,控制每次更新时的学习量。 -
num_episodes一目了然,因为它是 SARSA 执行的总回合数,直到终止。 -
eps是
贪心策略的随机性初始值。 -
gamma是折扣因子,用来对未来的动作赋予较小的权重。 -
eps_decay是eps在各回合之间的线性递减。
代码的前几行如下:
def SARSA(env, lr=0.01, num_episodes=10000, eps=0.3, gamma=0.95, eps_decay=0.00005):
nA = env.action_space.n
nS = env.observation_space.n
test_rewards = []
Q = np.zeros((nS, nA))
games_reward = []
在这里,初始化了一些变量。nA 和 nS 分别是环境中的动作数和观察数,Q 是将包含每个状态-动作对的 Q 值的矩阵,test_rewards 和 games_rewards 是稍后用来存储游戏分数信息的列表。
接下来,我们可以实现学习 Q 值的主循环:
for ep in range(num_episodes):
state = env.reset()
done = False
tot_rew = 0
if eps > 0.01:
eps -= eps_decay
action = eps_greedy(Q, state, eps)
上述代码块中的第 2 行在每个新回合时重置环境并存储当前的环境状态。第 3 行初始化了一个布尔变量,当环境处于终止状态时,该变量将被设置为True。接下来的两行更新了eps变量,直到其值大于 0.01。我们设置此阈值是为了保持长期内环境探索的最低速率。最后一行根据当前状态和 Q 矩阵选择一个贪婪动作。我们稍后会定义这个函数。
现在,我们已经处理了每个回合开始时需要的初始化,并选择了第一个动作,我们可以开始循环,直到回合(游戏)结束。以下这段代码从环境中获取样本并根据公式(5)更新 Q 函数:
while not done:
next_state, rew, done, _ = env.step(action) # Take one step in the environment
next_action = eps_greedy(Q, next_state, eps)
Q[state][action] = Q[state][action] + lr*(rew + gamma*Q[next_state][next_action] - Q[state][action]) # (4.5)
state = next_state
action = next_action
tot_rew += rew
if done:
games_reward.append(tot_rew)
done 保存一个布尔值,表示智能体是否仍在与环境交互,正如第 2 行所示。因此,循环直到完整回合结束就相当于只要done为False就继续迭代(代码的第一行)。然后,像往常一样,env.step 返回下一个状态、奖励、done 标志和一个信息字符串。在接下来的行中,eps_greedy 根据next_state和 Q 值选择下一个动作。SARSA 算法的核心在于随后的这一行,它根据公式(5)执行更新。除了学习率和折扣因子 gamma 外,还使用了上一阶段获得的奖励以及Q数组中的值。
最后一行设置当前状态和动作与之前相同,将奖励加到游戏的总奖励中,并且如果环境处于终止状态,则将奖励的总和附加到games_reward,然后内循环终止。
在SARSA函数的最后几行中,每 300 个回合,我们运行 1,000 次测试游戏并打印信息,如回合数、eps值和测试奖励的平均值。此外,我们返回Q数组:
if (ep % 300) == 0:
test_rew = run_episodes(env, Q, 1000)
print("Episode:{:5d} Eps:{:2.4f} Rew:{:2.4f}".format(ep, eps, test_rew))
test_rewards.append(test_rew)
return Q
我们现在可以实现eps_greedy函数,它以eps的概率从允许的动作中选择一个随机动作。为此,它只需在 0 和 1 之间采样一个均匀分布的数字,如果这个值小于eps,则选择一个随机动作。否则,它选择一个贪婪动作:
def eps_greedy(Q, s, eps=0.1):
if np.random.uniform(0,1) < eps:
# Choose a random action
return np.random.randint(Q.shape[1])
else:
# Choose the greedy action
return greedy(Q, s)
贪婪策略通过返回对应于状态s中最大 Q 值的索引来实现:
def greedy(Q, s):
return np.argmax(Q[s])
最后一个要实现的函数是run_episodes,它运行若干回合来测试策略。用于选择动作的策略是贪婪策略。这是因为在测试时我们不希望进行探索。总体而言,该函数与前一章中为动态规划算法实现的函数几乎相同:
def run_episodes(env, Q, num_episodes=100, to_print=False):
tot_rew = []
state = env.reset()
for _ in range(num_episodes):
done = False
game_rew = 0
while not done:
next_state, rew, done, _ = env.step(greedy(Q, state))
state = next_state
game_rew += rew
if done:
state = env.reset()
tot_rew.append(game_rew)
if to_print:
print('Mean score: %.3f of %i games!'%(np.mean(tot_rew), num_episodes))
else:
return np.mean(tot_rew)
很好!
现在我们快完成了。最后的部分仅涉及创建和重置环境,以及调用 SARSA 函数,传入环境和所有超参数:
if __name__ == '__main__':
env = gym.make('Taxi-v2')
env.reset()
Q = SARSA(env, lr=.1, num_episodes=5000, eps=0.4, gamma=0.95, eps_decay=0.001)
如你所见,我们从 eps 为 0.4 开始。这意味着前几次动作将以 0.4 的概率是随机的,并且由于衰减,eps 会逐渐减少,直到达到最小值 0.01(即我们在代码中设置的阈值)。

图 4.5 SARSA 算法在 Taxi-v2 上的结果
测试游戏累计奖励的性能图如图 4.5 所示。此外,图 4.6 显示了使用最终策略的完整回合运行。该图应从左到右、从上到下阅读。我们可以看到,出租车(最初用黄色高亮,之后用绿色高亮)在两个方向上都沿着最优路径行驶。

图 4.6 渲染的出租车游戏。策略来自使用 SARSA 训练的 Q 值。
关于本章中提到的所有颜色参考,请参考颜色图像包,下载链接为:www.packtpub.com/sites/default/files/downloads/9781789131116_ColorImages.pdf。
为了更好地理解算法和所有超参数,我们建议你进行尝试,改变它们并观察结果。你还可以尝试使用指数
-衰减率,而不是线性衰减。你就像强化学习算法一样,通过试错学习。
Q-learning
Q-learning 是另一种 TD 算法,具有一些与 SARSA 非常不同且非常有用的特性。Q-learning 继承了 TD 学习的一步学习特性(即在每一步进行学习的能力)和从经验中学习而不需要环境的完整模型的特性。
与 SARSA 相比,Q-learning 最显著的特点是它是一个脱离策略(off-policy)算法。提醒一下,脱离策略意味着更新可以独立于收集经验的任何策略进行。这意味着脱离策略的算法可以利用旧经验来改善策略。为了区分与环境交互的策略和实际改进的策略,我们称前者为行为策略,后者为目标策略。
在这里,我们将解释处理表格案例的更原始版本的算法,但它可以很容易地适应使用如人工神经网络等函数逼近器。实际上,在下一章中,我们将实现这个算法的一个更复杂的版本,该版本能够使用深度神经网络,并且利用以前的经验来充分发挥脱离策略算法的能力。
但首先,让我们看看 Q-learning 如何工作,形式化更新规则,并创建一个伪代码版本来统一所有组件。
理论
Q-learning 的核心思想是通过使用当前的最优动作值来近似 Q 函数。Q-learning 更新与 SARSA 中的更新非常相似,唯一的区别是它采用了最大状态-动作值:

是常用的学习率,
是折扣因子。
当 SARSA 更新是在行为策略上进行时(如
-贪婪策略),Q 更新则是在通过最大动作值得到的贪婪目标策略上进行的。如果这个概念还不清楚,可以查看图 4.7。虽然在 SARSA 中我们有图 4.3,其中两个动作
和
来自同一策略,但在 Q-learning 中,动作
是根据下一个最大状态-动作值选择的。由于 Q-learning 的更新不再过多依赖于行为策略(该策略仅用于从环境中采样),因此它成为了一种离策略算法。

图 4.7. Q-learning 更新
算法
由于 Q-learning 是一种时序差分(TD)方法,它需要一个行为策略,随着时间的推移,该策略将收敛为一个确定性策略。一种好的策略是使用带有线性或指数衰减的
-贪婪策略(正如在 SARSA 中所做的那样)。
总结一下,Q-learning 算法使用以下内容:
-
不断改进的目标贪婪策略
-
用来与环境交互并探索的行为
-贪婪策略
在这些总结性的观察之后,我们最终可以得出以下 Q-learning 算法的伪代码:
Initialize for every state-action pair
for episodes:
while is not a final state:
# env() take a step in the environment
在实际操作中,
通常在 0.5 和 0.001 之间,而
的范围是从 0.9 到 0.999。
将 Q-learning 应用于 Taxi-v2
一般来说,Q-learning 可以用来解决与 SARSA 相同类型的问题,且由于它们都来自同一个家族(TD 学习),因此它们通常具有类似的表现。然而,在某些特定问题中,可能会偏好其中一种方法。因此,了解 Q-learning 的实现也是非常有用的。
因此,在这里我们将实现 Q-learning 来解决 Taxi-v2,这与 SARSA 使用的环境相同。但请注意,经过一些调整后,它也可以应用于其他具有正确特征的环境。通过比较 Q-learning 和 SARSA 在同一环境下的结果,我们将有机会对它们的性能进行比较。
为了保持尽可能一致,我们保留了一些来自 SARSA 实现的函数不变。它们如下:
-
eps_greedy(Q,s,eps)是
-贪心策略,接受一个 Q矩阵、状态s和eps值。它返回一个动作。 -
greedy(Q,s)是一个贪心策略,接受一个Q矩阵和一个状态s。它返回与状态s中最大 Q 值相关联的动作。 -
run_episodes(env,Q,num_episodes,to_print)是一个函数,用于运行num_episodes场游戏来测试与Q矩阵相关的贪心策略。如果to_print为True,则打印结果。否则,返回奖励的平均值。
若要查看这些函数的实现,可以参考 SARSA 应用于 Taxi-v2 部分或该书的 GitHub 仓库,网址为 github.com/PacktPublishing/Reinforcement-Learning-Algorithms-with-Python。
执行 Q 学习算法的主函数接受一个环境 env;学习率 lr(公式(6)中使用的
变量);训练算法的回合数 num_episodes;初始
值 eps,用于
-贪心策略;衰减率 eps_decay;和折扣因子 gamma,作为参数:
def Q_learning(env, lr=0.01, num_episodes=10000, eps=0.3, gamma=0.95, eps_decay=0.00005):
nA = env.action_space.n
nS = env.observation_space.n
# Q(s,a) -> each row is a different state and each columns represent a different action
Q = np.zeros((nS, nA))
games_reward = []
test_rewards = []
函数的前几行初始化了动作和观测空间的维度,初始化了包含每个状态-动作对的 Q 值的数组 Q,并创建了空列表用于跟踪算法的进展。
然后,我们可以实现一个循环,该循环迭代 num_episodes 次:
for ep in range(num_episodes):
state = env.reset()
done = False
tot_rew = 0
if eps > 0.01:
eps -= eps_decay
每次迭代(即每个回合)都从重置环境开始,初始化 done 和 tot_rew 变量,并线性地减小 eps。
然后,我们需要遍历一个回合的所有时间步(对应一个回合),因为 Q 学习的更新发生在这里:
while not done:
action = eps_greedy(Q, state, eps)
next_state, rew, done, _ = env.step(action) # Take one step in the environment
# get the max Q value for the next state
Q[state][action] = Q[state][action] + lr*(rew + gamma*np.max(Q[next_state]) - Q[state][action]) # (4.6)
state = next_state
tot_rew += rew
if done:
games_reward.append(tot_rew)
这是算法的主体部分,流程相当标准:
-
动作是根据
-贪心策略(行为策略)选择的。 -
动作在环境中执行,环境返回下一个状态、奖励和完成标志。
-
基于公式(6)更新状态-动作值。
-
next_state被赋值给state变量。 -
最后一步的奖励加到该回合的累计奖励中。
-
如果这是最后一步,则奖励存储在
games_reward中,循环终止。
最后,每经过外循环的 300 次迭代,我们可以运行 1,000 场游戏来测试代理,打印一些有用的信息,并返回 Q 数组。
if (ep % 300) == 0:
test_rew = run_episodes(env, Q, 1000)
print("Episode:{:5d} Eps:{:2.4f} Rew:{:2.4f}".format(ep, eps, test_rew))
test_rewards.append(test_rew)
return Q
这就是全部内容。最后,在 main 函数中,我们可以创建环境并运行算法:
if __name__ == '__main__':
env = gym.make('Taxi-v2')
Q = Q_learning(env, lr=.1, num_episodes=5000, eps=0.4, gamma=0.95, eps_decay=0.001)
从图 4.8 可以推断,算法在约 3,000 个回合后达到了稳定结果。该图可以通过绘制test_rewards来生成:

图 4.8 Q 学习在 Taxi-v2 上的结果
像往常一样,我们建议你调整超参数,并通过实践来深入理解算法。
总体而言,算法找到了一个类似于 SARSA 算法所找到的策略。若要自行寻找,可以渲染一些回合或打印出基于Q数组的贪心动作。
比较 SARSA 和 Q 学习
现在我们来看一下这两种算法的快速比较。在图 4.9 中,展示了 Q 学习和 SARSA 在 Taxi-v2 环境中随着回合数的增加而逐步演化的表现。我们可以看到,两者都以相似的速度收敛到相同的值(以及相同的策略)。在进行这些比较时,你必须考虑到环境和算法是随机的,可能会产生不同的结果。从图 4.9 中我们还可以看到,Q 学习的曲线形状更加规则。这是因为 Q 学习更具鲁棒性,并且对变化不那么敏感:

图 4.9 SARSA 与 Q 学习在 Taxi-v2 上的结果比较
那么,是否更好使用 Q 学习?总体来说,答案是肯定的,在大多数情况下,Q 学习优于其他算法,但也有一些环境中 SARSA 表现得更好。两者的选择取决于环境和任务。
总结
在本章中,我们介绍了一类新的强化学习(RL)算法,它们通过与环境互动从经验中学习。这些方法与动态规划的不同之处在于,它们能够在不依赖环境模型的情况下学习价值函数,从而学习策略。
最初,我们看到蒙特卡罗方法是一种从环境中采样的简单方法,但由于它们需要完整的轨迹才能开始学习,因此在许多实际问题中并不适用。为了克服这些缺点,蒙特卡罗方法可以与引导技术结合,产生所谓的时间差分(TD)学习。得益于引导技术,这些算法可以在线学习(一步学习),并且在收敛到最优策略时减少方差。随后,我们学习了两种一步的、基于表格的、无模型的 TD 方法,即 SARSA 和 Q 学习。SARSA 是基于策略的,因为它通过选择当前策略(行为策略)来更新状态值。Q 学习则是脱离策略的,因为它在使用不同策略(行为策略)收集经验时,估计贪心策略的状态值。SARSA 和 Q 学习之间的这种区别使得后者比前者更具鲁棒性和效率。
每个 TD 方法都需要探索环境,以便更好地了解环境并找到最优策略。环境的探索由行为策略控制,行为策略偶尔需要采取非贪婪行动,例如,遵循一个非贪婪策略。
我们实现了 SARSA 和 Q 学习,并将它们应用到一个叫做 Taxi 的表格游戏中。我们看到两者都收敛到了最优策略,并且结果相似。
Q 学习算法在强化学习中至关重要,原因在于其特点。此外,通过精心设计,它可以适应非常复杂和高维的游戏。所有这一切都得益于函数逼近方法的应用,如深度神经网络。在下一章中,我们将详细介绍这一点,并介绍一个深度 Q 网络,它可以直接从像素学习玩 Atari 游戏。
问题
-
在强化学习中,蒙特卡洛方法的主要特性是什么?
-
为什么蒙特卡洛方法是离线的?
-
TD 学习的两个主要思想是什么?
-
蒙特卡洛方法和 TD 方法有什么区别?
-
为什么在 TD 学习中探索是重要的?
-
为什么 Q 学习是离策略的?
第五章:深度 Q 网络
到目前为止,我们已经接触并开发了强化学习算法,这些算法学习每个状态的价值函数V,或每个动作-状态对的动作价值函数Q。这些方法涉及单独存储和更新每个值在一个表格(或数组)中。由于状态和动作的数量庞大,这些方法无法扩展,因为表格的维度会呈指数增长,容易超出可用的内存容量。
在本章中,我们将介绍在强化学习算法中使用函数逼近来克服这个问题。特别是,我们将关注应用于 Q-learning 的深度神经网络。本章的第一部分,我们将解释如何使用函数逼近扩展 Q-learning 来存储 Q 值,并探讨我们可能面临的一些主要困难。在第二部分,我们将介绍一种新算法——深度 Q 网络(DQN),它利用新的思想,为在传统 Q-learning 与神经网络结合中遇到的一些挑战提供了优雅的解决方案。你将看到该算法如何在仅从像素学习的多种游戏中取得令人惊讶的成果。此外,您将实现该算法并将其应用于 Pong,亲自体验它的一些优点和弱点。
自从提出 DQN 以来,其他研究人员提出了许多变种,这些变种为算法提供了更好的稳定性和效率。我们将快速浏览并实现其中一些,以便更好地理解基础版本 DQN 的弱点,并为您提供一些思路,以便您自己改进它。
本章将涵盖以下主题:
-
深度神经网络与 Q-learning
-
DQN
-
DQN 应用于 Pong
-
DQN 变种
深度神经网络与 Q-learning
如我们在第四章《Q-Learning 和 SARSA 应用》中看到的那样,Q-learning 算法具有许多优点,使其能够应用于许多现实世界的场景。该算法的一个关键要素是它利用 Bellman 方程来学习 Q 函数。Q-learning 算法使用的 Bellman 方程能够从后续的状态-动作值中更新 Q 值。这使得该算法能够在每一步学习,而无需等待轨迹完成。此外,每个状态或动作-状态对都有自己存储的值,保存在查找表中,以便保存和检索相应的值。正因为如此,Q-learning 只要反复采样所有状态-动作对,就会收敛到最优值。此外,该方法使用两种策略:非贪婪行为策略用于从环境中收集经验(例如,
-贪婪),以及遵循最大 Q 值的目标贪婪策略。
维持表格形式的值表示可能是不可取的,有时甚至是有害的。因为大多数问题都有非常高的状态和动作数量。例如,图像(包括小图像)所包含的状态比宇宙中的原子还要多。你可以轻易猜到,在这种情况下,无法使用表格。除了这种表格所需的无限存储空间之外,只有少数状态会被访问多次,这使得关于 Q 函数或 V 函数的学习变得极为困难。因此,我们可能希望对状态进行泛化。在这种情况下,泛化意味着我们不仅仅关注一个状态的精确值,V(s),还关注相似和邻近状态的值。如果一个状态从未被访问过,我们可以用一个接近它的状态的值来逼近它。一般来说,泛化的概念在所有机器学习中都极为重要,包括强化学习。
泛化的概念在代理无法全面了解环境的情况下是至关重要的。在这种情况下,环境的完整状态将被代理隐藏,代理只能基于环境的有限表示做出决策。这被称为观察。例如,想象一个类人代理处理现实世界中的基本交互。显然,它无法看到整个宇宙的状态以及所有原子的情况。它只有一个有限的视角,也就是通过其传感器(例如视频摄像头)感知到的观察。正因如此,类人代理应该对周围发生的事情进行泛化,并相应地作出行为。
函数逼近
现在我们已经讨论了表格算法的主要约束,并表达了在强化学习(RL)算法中对泛化能力的需求,我们需要处理一些工具,帮助我们摆脱这些表格约束并解决泛化问题。
我们现在可以放弃表格,并用函数逼近器来表示值函数。函数逼近使我们能够在约束域内使用固定量的内存表示值函数。资源分配仅取决于用于逼近问题的函数。函数逼近器的选择,如同以往,依赖于任务的具体需求。函数逼近的例子有线性函数、决策树、最近邻算法、人工神经网络等等。正如你所预料的,人工神经网络被优先选择,因为它们在所有其他方法中占据主导地位——这并非偶然,它已广泛应用于各种强化学习算法中。特别地,深度人工神经网络,或者简言之,深度神经网络(DNNs),被广泛使用。它们的流行源于其高效性和能够自我学习特征的能力,随着网络隐藏层的增加,能够创建层次化的表示。此外,深度神经网络,特别是卷积神经网络(CNNs),在处理图像方面表现得极为出色,正如近期的突破所示,特别是在监督学习任务中。但尽管几乎所有关于深度神经网络的研究都集中在监督学习中,它们在强化学习框架中的应用却产生了非常有趣的结果。然而,正如我们稍后会看到的,这并不容易。
使用神经网络的 Q 学习
在 Q 学习中,深度神经网络通过学习一组权重来近似 Q 值函数。因此,Q 值函数通过
(网络的权重)来参数化,并写作如下:

为了将 Q 学习与深度神经网络结合(这一组合被称为深度 Q 学习),我们必须提出一个损失函数(或目标)来进行最小化。
如你所记得,表格形式的 Q 学习更新如下:

在这里,
是下一步的状态。此更新在每个通过行为策略收集到的样本上进行在线更新。
与前几章相比,为了简化符号表示,在这里我们将
称为当前步骤中的状态和动作,而
则称为下一步中的状态和动作。
在神经网络中,我们的目标是优化权重
,使得
更接近最优的 Q 值函数。但由于我们没有最优的 Q 函数,我们只能通过最小化 Bellman 错误来朝着最优 Q 函数迈出小步伐,这个 Bellman 错误是一步的误差,即
。这一步类似于我们在表格 Q 学习中所做的。然而,在深度 Q 学习中,我们不更新单一的值
。相反,我们对 Q 函数相对于参数
进行梯度计算:

这里,
是关于
相对于
的偏导数。
被称为学习率,它表示朝着梯度方向迈出的步伐大小。
实际上,我们刚才从表格 Q 学习到深度 Q 学习的平滑过渡,并没有产生一个良好的近似。第一个修复方法是使用均方误差(MSE)作为损失函数(而不是 Bellman 错误)。第二个修复方法是将在线 Q 迭代迁移到批量 Q 迭代。这意味着神经网络的参数会通过一次性使用多个过渡来更新(例如,在监督学习设置中使用大于 1 的小批量)。这些更改产生了以下损失函数:

这里,
不是实际的动作价值函数,因为我们并没有使用它。相反,它是 Q 目标值:

然后,网络参数
通过在均方误差(MSE)损失函数
上进行梯度下降来更新:

非常重要的一点是,
被视为常数,损失函数的梯度不会进一步传播。
由于在前一章中,我们介绍了蒙特卡洛(MC)算法,我们希望强调这些算法也可以调整为与神经网络一起使用。在这种情况下,
将是回报,
。由于 MC 更新没有偏差,它在渐近意义上优于时序差分(TD),但在实践中,后者的表现更好。
深度 Q 学习的不稳定性
有了我们刚刚提出的损失函数和优化技术,你应该能够开发出深度 Q 学习算法。然而,现实要复杂得多。实际上,如果我们尝试实现它,可能无法成功。为什么?一旦引入神经网络,我们就无法保证算法的改进。尽管表格型 Q 学习具有收敛能力,但其神经网络版本并不具备。
Sutton 和 Barto 在《强化学习:导论》中提出了一个问题,叫做致命三合一问题,它出现在以下三种因素结合时:
-
函数逼近
-
自举法(即,由其他估计值进行的更新)
-
离策略学习(Q 学习是一种离策略算法,因为它的更新与所使用的策略无关)
但这些恰恰是深度 Q 学习算法的三个主要成分。正如作者所指出的那样,我们无法摆脱自举法,而不影响计算成本或数据效率。此外,离策略学习对于创建更智能、更强大的代理至关重要。显然,没有深度神经网络,我们将失去一个非常重要的组件。因此,设计能够保持这三种成分的算法,同时减轻致命三合一问题,是非常重要的。
此外,从方程(5.2)和(5.3)来看,问题可能看起来类似于有监督回归,但它并非如此。在有监督学习中,进行 SGD 时,迷你批次总是从数据集中随机抽样,以确保它们是独立同分布(IID)的。而在强化学习中,经验是由策略收集的。由于状态是顺序的并且相互关联,i.i.d 假设立即失效,这在执行 SGD 时会导致严重的不稳定性。
另一个不稳定性的原因是 Q 学习过程的非平稳性。从方程(5.2)和(5.3)中可以看出,同一个神经网络在更新时也是计算目标值的网络,
。考虑到目标值在训练过程中也会被更新,这一点是危险的。这就像是射击一个移动的圆形目标,却没有考虑到它的运动。这些行为仅仅是神经网络的泛化能力所致;实际上,在表格型情况下并不是问题。
深度 Q 学习在理论上理解较少,但正如我们很快会看到的那样,有一种算法通过使用一些技巧来增加数据的 i.i.d 性并缓解目标移动问题。这些技巧使得算法更加稳定和灵活。
DQN
DQN 首次出现在 Mnih 等人(DeepMind)发表的论文通过深度强化学习实现人类水平控制中,是第一个将 Q 学习与深度神经网络结合的可扩展强化学习算法。为了克服稳定性问题,DQN 采用了两种新的技术,这些技术对算法的平衡至关重要。
DQN 已经证明自己是第一个能够在多种具有挑战性的任务中学习的人工智能代理。此外,它已经学会了如何仅通过高维度行像素作为输入,并采用端到端的强化学习方法来控制多个任务。
解决方案
DQN 带来的关键创新包括回放缓冲区,以克服数据相关性问题,以及一个独立的目标网络,以解决非平稳性问题。
回放记忆
为了在 SGD 迭代中使用更多的 IID 数据,DQN 引入了回放记忆(也叫经验回放)来收集和存储在一个大缓冲区中的经验。这个缓冲区理想情况下包含了智能体在其生命周期中发生的所有转移。在执行 SGD 时,将从经验回放中随机抽取一个小批量样本,并用于优化过程。由于回放记忆缓冲区存储了多样化的经验,从中抽取的小批量样本将具有足够的多样性,提供独立的样本。使用经验回放的另一个非常重要的特点是,它使得数据的可重用性得以实现,因为这些转移将被多次抽取。这大大提高了算法的数据效率。
目标网络
移动目标问题是由于在训练过程中持续更新网络,这也修改了目标值。然而,神经网络必须不断更新自己,以提供最佳的状态-动作值。DQNs 中采用的解决方案是使用两个神经网络。一个被称为在线网络,它不断更新,而另一个被称为目标网络,它只在每隔N次迭代后更新(N通常在 1,000 到 10,000 之间)。在线网络用于与环境交互,而目标网络用于预测目标值。通过这种方式,对于N次迭代,目标网络生成的目标值保持不变,防止了不稳定性的传播并降低了发散的风险。一个潜在的缺点是目标网络是在线网络的旧版本。然而,在实践中,优点远远大于缺点,算法的稳定性将显著提高。
DQN 算法
在深度 Q 学习算法中引入重放缓冲区和单独的目标网络,使得从只有图像、奖励和终止信号开始,便能控制 Atari 游戏(如《太空侵略者》、《乒乓》和《打砖块》)。DQN 通过结合卷积神经网络(CNN)和全连接神经网络,完全端到端地进行学习。
DQN 已经在 49 个 Atari 游戏上分别进行了训练,使用相同的算法、网络架构和超参数。它表现得比所有以前的算法更好,在许多游戏中达到了与专业玩家相当甚至更好的水平。Atari 游戏并不容易解决,其中许多需要复杂的规划策略。实际上,其中一些(如著名的《蒙特祖马的复仇》)要求的水平甚至是 DQN 尚未能够达到的。
这些游戏的一个特点是,由于它们仅向代理提供图像,因此它们是部分可观测的。它们没有显示环境的完整状态。事实上,一张图像不足以完全理解当前的情况。例如,你能从以下图像中推断出球的方向吗?

图 5.1. Pong 的渲染
你做不到,代理也做不到。为了克服这种情况,在每个时间点,都会考虑一系列先前的观察结果。通常会使用最后两到五帧,在大多数情况下,它们能较为准确地近似实际的整体状态。
损失函数
深度 Q 网络通过最小化我们已经介绍的损失函数(5.2)来进行训练,但进一步使用了一个单独的 Q 目标网络,
,并且带有权重,
,将一切整合起来,损失函数变为:

这里,
是在线网络的参数。
可微分损失函数(5.4)的优化使用了我们最喜欢的迭代方法,即小批量梯度下降。也就是说,学习更新应用于从经验缓冲区均匀抽取的小批量数据。损失函数的导数如下:

与深度 Q 学习中的问题框架不同,在 DQN 中,学习过程更加稳定。此外,由于数据更加独立同分布(i.i.d.),并且目标(以某种方式)是固定的,因此它与回归问题非常相似。但另一方面,目标仍然依赖于网络权重。
如果你在每一步都优化损失函数(5.4),并且只对单一样本进行优化,你将得到带有函数逼近的 Q 学习算法。
伪代码
现在,DQN 的所有组件都已被解释清楚,我们可以将所有部分结合起来,向你展示该算法的伪代码版本,以澄清任何不确定的地方(如果你还是不明白,也不用担心——在下一节,你将实现它,一切都会变得更清晰)。
DQN 算法涉及三个主要部分:
-
数据收集和存储。数据通过遵循行为策略(例如,
-贪心)进行收集。 -
神经网络优化(对从缓冲区中采样的小批量数据执行 SGD)。
-
目标更新。
DQN 的伪代码如下:
Initialize function with random weight
Initialize function with random weight
Initialize empty replay memory
for do Initialize environment
for do
> Collect observation from the env:
> Store the transition in the replay buffer:
> Update the model using (5.4):
Sample a random minibatch from
Perform a step of GD on on
> Update target network:
Every C steps
end for
end for
这里,d是由环境返回的标志,指示环境是否处于最终状态。如果d=True,即表示这一回合结束,环境需要重置。
是一个预处理步骤,它通过降低图像的维度来改变图像(将图像转换为灰度图并将其调整为更小的尺寸),并将最后n帧添加到当前帧。通常,n的值介于 2 和 4 之间。预处理部分将在下一节中详细解释,我们将在那里实现 DQN。
在 DQN 中,经验回放,
,是一个动态缓冲区,用来存储有限数量的帧。在论文中,缓冲区包含了最后 100 万次转换,当它超过这个数量时,会丢弃旧的经验。
其他部分已经描述过。如果你在想为什么目标值,
,会取
的值,原因是之后不会再与环境进行交互,因此
是其实际的无偏 Q 值。
模型架构
到目前为止,我们已经讨论了算法本身,但还没有解释 DQN 的架构。除了为稳定训练而采用的新思想外,DQN 的架构在算法的最终表现中也起着至关重要的作用。在DQN论文中,所有的 Atari 环境都使用了单一的模型架构。它结合了 CNN 和 FNN。特别地,作为输入的观察图像使用 CNN 来学习这些图像的特征图。CNN 由于具有平移不变性特征和共享权重的属性,已经广泛应用于图像处理中,这使得网络能够比其他类型的深度神经网络使用更少的权重进行学习。
模型的输出对应于状态-动作值,每个动作有一个输出值。因此,为了控制一个具有五个动作的智能体,模型将为每个动作输出一个值。这种模型架构允许我们仅通过一次前向传递就计算出所有的 Q 值。
有三个卷积层。每一层包括一个卷积操作,过滤器数量逐渐增加,维度逐渐减小,同时还有一个非线性函数。最后一个隐藏层是一个全连接层,后接一个修正激活函数和一个全连接的线性层,每个动作都有一个输出。以下是该架构的简要表示:

图 5.2. DQN 的 DNN 架构示意图,由 CNN 和 FNN 组成
DQN 应用于 Pong
配备了关于 Q-learning、深度神经网络和 DQN 的所有技术知识后,我们终于可以开始实际应用并启动 GPU 了。在本节中,我们将 DQN 应用到 Atari 环境 Pong。我们选择 Pong 而非其他所有 Atari 环境,因为它更容易解决,因此需要更少的时间、计算能力和内存。话虽如此,如果你有一个不错的 GPU,可以将相同的配置应用于几乎所有其他 Atari 游戏(有些可能需要稍微调整)。出于同样的原因,我们采用了比原始 DQN 论文中更轻量的配置,既包括函数逼近器的容量(即更少的权重),也包括超参数如较小的缓存大小。这不会影响 Pong 的结果,但可能会影响其他游戏的表现。
首先,我们将简要介绍 Atari 环境和预处理管道,然后再继续进行 DQN 实现。
Atari 游戏
自从 DQN 论文中首次介绍 Atari 游戏以来,它们就成为了深度强化学习算法的标准测试平台。这些游戏最初通过 Arcade Learning Environment(ALE)提供,随后被 OpenAI Gym 封装,提供了标准接口。ALE(和 Gym)包括 57 款最受欢迎的 Atari 2600 视频游戏,如 Montezuma's Revenge、Pong、Breakout 和 Space Invaders,如下图所示。这些游戏由于其高维状态空间(210 x 160 像素)以及游戏之间的任务多样性,已广泛应用于强化学习研究:

图 5.3 Montezuma's Revenge、Pong、Breakout 和 Space Invaders 环境
关于 Atari 环境的一个非常重要的说明是,它们是确定性的,这意味着在给定一组固定的动作时,多个比赛中的结果将是相同的。从算法的角度来看,这种确定性在所有历史数据用于从随机策略中选择动作之前都是成立的。
预处理
Atari 中的每一帧为 210 x 160 像素,采用 RGB 颜色,因此其整体尺寸为 210 x 160 x 3。如果使用 4 帧的历史数据,则输入的维度为 210 x 160 x 12。如此高的维度计算量大,而且在经验缓存中存储大量帧可能会变得困难。因此,必须进行预处理以减少维度。在原始的 DQN 实现中,使用了以下预处理管道:
-
RGB 颜色被转换为灰度图。
-
图像被下采样至 110 x 84,然后裁剪为 84 x 84。
-
最后三到四帧会与当前帧拼接。
-
对帧进行归一化处理。
此外,由于游戏以高帧率运行,采用了一种称为“跳帧”的技术,用于跳过
连续帧。这项技术使得智能体能够在每局游戏中存储和训练较少的帧,而不会显著降低算法的性能。实际上,使用跳帧技术时,智能体每隔
帧选择一个动作,并在跳过的帧上重复该动作。
此外,在某些环境中,每局游戏开始时,智能体必须按下开火按钮才能开始游戏。另外,由于环境的确定性,在重置环境时,某些无操作(no-ops)会被执行,以便将智能体随机放置在一个位置。
幸运的是,OpenAI 发布了与 Gym 接口兼容的预处理管道实现。你可以在本书的 GitHub 仓库中的atari_wrappers.py文件中找到它。在这里,我们仅简要解释一下该实现:
-
NoopResetEnv(n):在重置环境时执行n次无操作(no-op),以为智能体提供一个随机的起始位置。 -
FireResetEnv():在环境重置时开火(仅在某些游戏中需要)。 -
MaxAndSkipEnv(skip):跳过skip帧,并确保重复动作和累计奖励。 -
WarpFrame():将帧大小调整为 84 x 84 并转换为灰度图。 -
FrameStack(k):将最近的k帧堆叠在一起。
所有这些功能都作为包装器实现。包装器是一种通过在环境上方添加新层,来轻松转换环境的方式。例如,要在 Pong 中缩放帧,我们会使用以下代码:
env = gym.make('Pong-v0')
env = ScaledFloatFrame(env)
包装器必须继承gym.Wrapper类,并且至少重载以下方法之一:__init__(self, env)、step、reset、render、close或seed。
这里我们不会展示所有列出的包装器的实现,因为它们超出了本书的范围,但我们将使用FireResetEnv和WrapFrame作为示例,给你一个大致的实现概念。完整的代码可以在本书的 GitHub 仓库中找到:
class FireResetEnv(gym.Wrapper):
def __init__(self, env):
"""Take action on reset for environments that are fixed until firing."""
gym.Wrapper.__init__(self, env)
assert env.unwrapped.get_action_meanings()[1] == 'FIRE'
assert len(env.unwrapped.get_action_meanings()) >= 3
def reset(self, **kwargs):
self.env.reset(**kwargs)
obs, _, done, _ = self.env.step(1)
if done:
self.env.reset(**kwargs)
obs, _, done, _ = self.env.step(2)
if done:
self.env.reset(**kwargs)
return obs
def step(self, ac):
return self.env.step(ac)
首先,FireResetEnv继承自 Gym 的Wrapper类。然后,在初始化时,通过env.unwrapped解包环境,检查fire动作的可用性。该函数通过调用在前一层中定义的self.env.reset来重写reset函数,接着通过调用self.env.step(1)执行一个开火动作,并执行一个依赖于环境的动作self.env.step(2)。
WrapFrame具有类似的定义:
class WarpFrame(gym.ObservationWrapper):
def __init__(self, env):
"""Warp frames to 84x84 as done in the Nature paper and later work."""
gym.ObservationWrapper.__init__(self, env)
self.width = 84
self.height = 84
self.observation_space = spaces.Box(low=0, high=255,
shape=(self.height, self.width, 1), dtype=np.uint8)
def observation(self, frame):
frame = cv2.cvtColor(frame, cv2.COLOR_RGB2GRAY)
frame = cv2.resize(frame, (self.width, self.height), interpolation=cv2.INTER_AREA)
return frame[:, :, None]
这次,WarpFrame继承自gym.ObservationWrapper,并创建了一个维度为 84 x 84、值范围在 0 到 255 之间的Box空间。当调用observation()时,它将 RGB 帧转换为灰度图像,并将图像调整为所选形状。
然后我们可以创建一个函数make_env,将每个包装器应用到环境中:
def make_env(env_name, fire=True, frames_num=2, noop_num=30, skip_frames=True):
env = gym.make(env_name)
if skip_frames:
env = MaxAndSkipEnv(env) # Return only every `skip`-th frame
if fire:
env = FireResetEnv(env) # Fire at the beginning
env = NoopResetEnv(env, noop_max=noop_num)
env = WarpFrame(env) # Reshape image
env = FrameStack(env, frames_num) # Stack last 4 frames
return env
唯一缺失的预处理步骤是帧的缩放。我们将在将观察帧作为输入传递给神经网络之前处理缩放。这是因为FrameStack使用了一种特定的节省内存的数组,称为懒数组,每当缩放作为包装器应用时,这种数组就会丢失。
DQN 实现
尽管 DQN 是一个相当简单的算法,但在实现和设计选择时需要特别注意。与其他深度强化学习算法一样,这个算法并不容易调试和调整。因此,在本书中,我们将为您提供一些技巧和建议,帮助您完成这些任务。
DQN 代码包含四个主要组件:
-
DNNs
-
一个经验缓冲区
-
一个计算图
-
一个训练(和评估)循环
代码,像往常一样,使用 Python 和 TensorFlow 编写,我们将使用 TensorBoard 来可视化训练过程和算法的性能。
所有代码都可以在本书的 GitHub 仓库中找到。务必去那里查看。为了避免使代码过于冗长,我们没有提供一些简单函数的实现。
让我们立即开始实现,导入所需的库:
import numpy as np
import tensorflow as tf
import gym
from datetime import datetime
from collections import deque
import time
import sys
from atari_wrappers import make_env
atari_wrappers包含我们之前定义的make_env函数。
DNNs
DNN 架构如下(组件按顺序构建):
-
一个 16 个过滤器的卷积,过滤器维度为 8 x 8,步幅为 4,采用整流非线性激活函数。
-
一个 32 个过滤器的卷积,过滤器维度为 4 x 4,步幅为 2,采用整流非线性激活函数。
-
一个 32 个过滤器的卷积,过滤器维度为 3 x 3,步幅为 1,采用整流非线性激活函数。
-
一个 128 单元的全连接层,使用 ReLU 激活函数。
-
一个全连接层,其单元数等于环境中允许的动作数量,并采用线性激活函数。
在cnn中,我们定义了前三个卷积层,而在fnn中,我们定义了最后两个全连接层:
def cnn(x):
x = tf.layers.conv2d(x, filters=16, kernel_size=8, strides=4, padding='valid', activation='relu')
x = tf.layers.conv2d(x, filters=32, kernel_size=4, strides=2, padding='valid', activation='relu')
return tf.layers.conv2d(x, filters=32, kernel_size=3, strides=1, padding='valid', activation='relu')
def fnn(x, hidden_layers, output_layer, activation=tf.nn.relu, last_activation=None):
for l in hidden_layers:
x = tf.layers.dense(x, units=l, activation=activation)
return tf.layers.dense(x, units=output_layer, activation=last_activation)
在上述代码中,hidden_layers是一个整数值的列表。在我们的实现中,它是hidden_layers=[128]。另一方面,output_layer表示代理动作的数量。
在qnet中,CNN 和 FNN 层通过一个层连接,该层将 CNN 的二维输出展平:
def qnet(x, hidden_layers, output_size, fnn_activation=tf.nn.relu, last_activation=None):
x = cnn(x)
x = tf.layers.flatten(x)
return fnn(x, hidden_layers, output_size, fnn_activation, last_activation)
深度神经网络现在已经完全定义。我们需要做的就是将其连接到主计算图。
经验缓冲区
经验缓冲区是ExperienceBuffer类型的类,并存储一个FIFO(先进先出)类型的队列,用于存储以下每个组件:观察、奖励、动作、下一观察和完成。FIFO 意味着,一旦达到maxlen指定的最大容量,它将从最旧的元素开始丢弃。在我们的实现中,容量为buffer_size:
class ExperienceBuffer():
def __init__(self, buffer_size):
self.obs_buf = deque(maxlen=buffer_size)
self.rew_buf = deque(maxlen=buffer_size)
self.act_buf = deque(maxlen=buffer_size)
self.obs2_buf = deque(maxlen=buffer_size)
self.done_buf = deque(maxlen=buffer_size)
def add(self, obs, rew, act, obs2, done):
self.obs_buf.append(obs)
self.rew_buf.append(rew)
self.act_buf.append(act)
self.obs2_buf.append(obs2)
self.done_buf.append(done)
ExperienceBuffer类还管理小批量的采样,这些小批量用于训练神经网络。这些小批量是从缓冲区中均匀采样的,并且具有预定义的batch_size大小:
def sample_minibatch(self, batch_size):
mb_indices = np.random.randint(len(self.obs_buf), size=batch_size)
mb_obs = scale_frames([self.obs_buf[i] for i in mb_indices])
mb_rew = [self.rew_buf[i] for i in mb_indices]
mb_act = [self.act_buf[i] for i in mb_indices]
mb_obs2 = scale_frames([self.obs2_buf[i] for i in mb_indices])
mb_done = [self.done_buf[i] for i in mb_indices]
return mb_obs, mb_rew, mb_act, mb_obs2, mb_done
最后,我们重写了_len方法,以提供缓冲区的长度。请注意,由于每个缓冲区的大小相同,我们只返回self.obs_buf的长度:
def __len__(self):
return len(self.obs_buf)
计算图和训练循环
算法的核心,即计算图和训练(及评估)循环,已经在DQN函数中实现,该函数将环境的名称和所有其他超参数作为参数:
def DQN(env_name, hidden_sizes=[32], lr=1e-2, num_epochs=2000, buffer_size=100000, discount=0.99, update_target_net=1000, batch_size=64, update_freq=4, frames_num=2, min_buffer_size=5000, test_frequency=20, start_explor=1, end_explor=0.1, explor_steps=100000):
env = make_env(env_name, frames_num=frames_num, skip_frames=True, noop_num=20)
env_test = make_env(env_name, frames_num=frames_num, skip_frames=True, noop_num=20)
env_test = gym.wrappers.Monitor(env_test, "VIDEOS/TEST_VIDEOS"+env_name+str(current_milli_time()),force=True, video_callable=lambda x: x%20==0)
obs_dim = env.observation_space.shape
act_dim = env.action_space.n
在前面代码的前几行中,创建了两个环境:一个用于训练,另一个用于测试。此外,gym.wrappers.Monitor是一个 Gym 封装器,它将环境的游戏保存为视频格式,而video_callable是一个函数参数,用于确定视频保存的频率,在这种情况下是每 20 集保存一次。
然后,我们可以重置 TensorFlow 图并为观察值、动作和目标值创建占位符。这可以通过以下代码行完成:
tf.reset_default_graph()
obs_ph = tf.placeholder(shape=(None, obs_dim[0], obs_dim[1], obs_dim[2]), dtype=tf.float32, name='obs')
act_ph = tf.placeholder(shape=(None,), dtype=tf.int32, name='act')
y_ph = tf.placeholder(shape=(None,), dtype=tf.float32, name='y')
现在,我们可以通过调用之前定义的qnet函数来创建目标网络和在线网络。由于目标网络需要时不时更新自己并采用在线网络的参数,因此我们创建了一个名为update_target_op的操作,用于将在线网络的每个变量分配给目标网络。这一分配通过 TensorFlow 的assign方法完成。另一方面,tf.group将update_target列表中的每个元素聚合为一个操作。实现如下:
with tf.variable_scope('target_network'):
target_qv = qnet(obs_ph, hidden_sizes, act_dim)
target_vars = tf.trainable_variables()
with tf.variable_scope('online_network'):
online_qv = qnet(obs_ph, hidden_sizes, act_dim)
train_vars = tf.trainable_variables()
update_target = [train_vars[i].assign(train_vars[i+len(target_vars)]) for i in range(len(train_vars) - len(target_vars))]
update_target_op = tf.group(*update_target)
现在,我们已经定义了创建深度神经网络的占位符,并定义了目标更新操作,剩下的就是定义损失函数。损失函数是
(或者等价地,(5.5))。它需要目标值,
,
按照公式(5.6)计算,它们通过y_ph占位符和在线网络的 Q 值传递,
。Q 值依赖于动作,
,但由于在线网络为每个动作输出一个值,我们必须找到一种方法来仅提取
的 Q 值,同时丢弃其他动作值。这个操作可以通过使用动作的独热编码(one-hot encoding)来实现,
,然后将其与在线网络的输出相乘。例如,如果有五个可能的动作且
,那么独热编码将是
。然后,假设网络输出
,与独热编码的乘积结果将是
。接下来,通过对这个向量求和,获得 q 值。结果将是
。这一切在以下三行代码中完成:
act_onehot = tf.one_hot(act_ph, depth=act_dim)
q_values = tf.reduce_sum(act_onehot * online_qv, axis=1)
v_loss = tf.reduce_mean((y_ph - q_values)**2)
为了最小化我们刚才定义的损失函数,我们将使用 Adam,这是 SGD 的一种变体:
v_opt = tf.train.AdamOptimizer(lr).minimize(v_loss)
这就完成了计算图的创建。在进入主要的 DQN 循环之前,我们需要准备好一切,以便可以保存标量和直方图。这样做后,我们就能够在 TensorBoard 中查看它们:
now = datetime.now()
clock_time = "{}_{}.{}.{}".format(now.day, now.hour, now.minute, int(now.second))
mr_v = tf.Variable(0.0)
ml_v = tf.Variable(0.0)
tf.summary.scalar('v_loss', v_loss)
tf.summary.scalar('Q-value', tf.reduce_mean(q_values))
tf.summary.histogram('Q-values', q_values)
scalar_summary = tf.summary.merge_all()
reward_summary = tf.summary.scalar('test_rew', mr_v)
mean_loss_summary = tf.summary.scalar('mean_loss', ml_v)
hyp_str = "-lr_{}-upTN_{}-upF_{}-frms_{}".format(lr, update_target_net, update_freq, frames_num)
file_writer = tf.summary.FileWriter('log_dir/'+env_name+'/DQN_'+clock_time+'_'+hyp_str, tf.get_default_graph())
一切都非常直观。唯一可能让你疑问的是mr_v和ml_v这两个变量。它们是我们希望通过 TensorBoard 跟踪的变量。然而,由于它们没有在计算图中内部定义,我们必须单独声明它们,并在稍后的session.run中赋值。FileWriter是用一个唯一的名字创建的,并与默认图相关联。
我们现在可以定义agent_op函数,用于计算缩放后的观察值的前向传递。该观察值已经通过了预处理管道(在环境中通过包装器构建),但我们将缩放操作放到了一边:
def agent_op(o):
o = scale_frames(o)
return sess.run(online_qv, feed_dict={obs_ph:[o]})
然后,创建会话,初始化变量,并重置环境:
sess = tf.Session()
sess.run(tf.global_variables_initializer())
step_count = 0
last_update_loss = []
ep_time = current_milli_time()
batch_rew = []
obs = env.reset()
接下来的步骤包括实例化回放缓冲区,更新目标网络使其具有与在线网络相同的参数,并用eps_decay初始化衰减率。epsilon 衰减的策略与 DQN 论文中采用的相同。衰减率的选择使得当它线性应用于eps变量时,它将在大约explor_steps步内达到终值end_explor。例如,如果你想在 1000 步内从 1.0 降低到 0.1,那么你必须在每一步减小一个等于
的值。所有这一切都在以下几行代码中完成:
obs = env.reset()
buffer = ExperienceBuffer(buffer_size)
sess.run(update_target_op)
eps = start_explor
eps_decay = (start_explor - end_explor) / explor_steps
如你所记得,训练循环由两个内层循环组成:第一个循环遍历训练周期,第二个循环遍历每个周期的过渡阶段。最内层循环的第一部分是相当标准的。它根据使用在线网络的
-贪婪行为策略选择一个动作,执行一步环境操作,将新的过渡加入缓冲区,并最终更新变量:
for ep in range(num_epochs):
g_rew = 0
done = False
while not done:
act = eps_greedy(np.squeeze(agent_op(obs)), eps=eps)
obs2, rew, done, _ = env.step(act)
buffer.add(obs, rew, act, obs2, done)
obs = obs2
g_rew += rew
step_count += 1
在前面的代码中,obs获取下一个观察值,累计的游戏奖励会增加。
然后,在相同的循环中,eps会衰减,如果满足某些条件,则训练在线网络。这些条件确保缓冲区已达到最小大小,并且神经网络仅在每update_freq步时训练一次。为了训练在线网络,首先,从缓冲区中采样一个小批量,并计算目标值。然后,运行会话以最小化损失函数v_loss,并将目标值、动作和小批量的观察值传入字典。在会话运行期间,它还会返回v_loss和scalar_summary以供统计使用。接着,scalar_summary会被添加到file_writer中保存到 TensorBoard 日志文件。最后,每经过update_target_net个周期,目标网络会更新。一个包含平均损失的总结也会被运行并添加到 TensorBoard 日志文件中。所有这些操作通过以下代码片段完成:
if eps > end_explor:
eps -= eps_decay
if len(buffer) > min_buffer_size and (step_count % update_freq == 0):
mb_obs, mb_rew, mb_act, mb_obs2, mb_done = buffer.sample_minibatch(batch_size)
mb_trg_qv = sess.run(target_qv, feed_dict={obs_ph:mb_obs2})
y_r = q_target_values(mb_rew, mb_done, mb_trg_qv, discount) # Compute the target values
train_summary, train_loss, _ = sess.run([scalar_summary, v_loss, v_opt], feed_dict={obs_ph:mb_obs, y_ph:y_r, act_ph: mb_act})
file_writer.add_summary(train_summary, step_count)
last_update_loss.append(train_loss)
if (len(buffer) > min_buffer_size) and (step_count % update_target_net) == 0:
_, train_summary = sess.run([update_target_op, mean_loss_summary], feed_dict={ml_v:np.mean(last_update_loss)})
file_writer.add_summary(train_summary, step_count)
last_update_loss = []
当一个周期结束时,环境会被重置,游戏的总奖励被添加到batch_rew中,并将后者设为零。此外,每隔test_frequency个周期,代理会在 10 局游戏中进行测试,统计数据会被添加到file_writer中。在训练结束时,环境和写入器将被关闭。代码如下:
if done:
obs = env.reset()
batch_rew.append(g_rew)
g_rew = 0
if ep % test_frequency == 0:
test_rw = test_agent(env_test, agent_op, num_games=10)
test_summary = sess.run(reward_summary, feed_dict={mr_v: np.mean(test_rw)})
file_writer.add_summary(test_summary, step_count)
print('Ep:%4d Rew:%4.2f, Eps:%2.2f -- Step:%5d -- Test:%4.2f %4.2f' % (ep,np.mean(batch_rew), eps, step_count, np.mean(test_rw), np.std(test_rw))
batch_rew = []
file_writer.close()
env.close()
env_test.close()
就是这样。我们现在可以调用DQN函数,传入 Gym 环境的名称和所有超参数:
if __name__ == '__main__':
DQN('PongNoFrameskip-v4', hidden_sizes=[128], lr=2e-4, buffer_size=100000, update_target_net=1000, batch_size=32, update_freq=2, frames_num=2, min_buffer_size=10000)
在报告结果之前,还有最后一点需要说明。这里使用的环境并非默认版本的Pong-v0,而是其修改版。之所以这样做,是因为在常规版本中,每个动作会执行 2、3 或 4 次,且这个次数是均匀采样的。但由于我们希望跳过固定次数,因此选择了没有内置跳过功能的版本NoFrameskip,并添加了自定义的MaxAndSkipEnv包装器。
结果
评估强化学习算法的进展非常具有挑战性。最明显的方式是追踪其最终目标;即,监控在周期中积累的总奖励。这是一个很好的指标。然而,由于权重的变化,训练过程中的平均奖励可能非常嘈杂。这会导致状态分布发生较大的变化。
基于这些原因,我们在每 20 次训练周期后评估算法在 10 个测试游戏上的表现,并跟踪整个游戏过程中累积的总(非折扣)奖励的平均值。此外,由于环境的确定性,我们使用
-贪婪策略(与
)进行测试,以便进行更稳健的评估。这个标量总结称为 test_rew。你可以通过访问保存日志的目录并执行以下命令,在 TensorBoard 中查看它:
tensorboard --logdir .
这个图示应该与你的图示类似(如果你运行了 DQN 代码)。它显示在下图中,x 轴表示步数。你可以看到,在前 250,000 步的线性增加之后,Q 值在接下来的 300,000 步内出现了更显著的增长,最终达到了一个稳定的得分
:

图 5.4. 10 场游戏的平均总奖励图示。x 轴表示步数
Pong 是一个相对简单的任务。事实上,我们的算法已经训练了大约 1.1 百万步,而在 DQN 论文中,所有的算法都是在 2 亿步上进行训练的。
评估算法的另一种方式是通过估计的动作值。事实上,估计的动作值是一个有价值的指标,因为它们衡量了状态-动作对的质量的信念。不幸的是,这个选项并不最优,因为一些算法倾向于高估 Q 值,正如我们很快会学到的那样。尽管如此,我们在训练过程中仍然追踪了它。图示如下所示,正如我们预期的那样,Q 值在训练过程中不断增加,与前面的图示相似:

图 5.5. 估计的训练 Q 值图示。x 轴表示步数
另一个重要的图示,如下图所示,展示了损失函数随时间变化的情况。尽管它不像监督学习中那么有用,因为目标值并不是实际的真实值,但它仍然可以提供对模型质量的良好洞察:

图 5.6. 损失函数的图示
DQN 变体
在 DQN 取得惊人效果之后,许多研究人员对其进行了研究,并提出了整合与改进,以提高其稳定性、效率和性能。在这一部分,我们将介绍三种改进的算法,解释它们背后的思想和解决方案,并提供它们的实现。第一种是双重 DQN(Double DQN 或 DDQN),它解决了我们在 DQN 算法中提到的过度估计问题。第二种是对抗 DQN(Dueling DQN),它将 Q 值函数解耦为状态值函数和动作-状态优势值函数。第三种是 n 步 DQN(n-step DQN),它借鉴了 TD 算法中的一个旧思想,用于扩展一步学习与蒙特卡洛学习之间的步长。
双重 DQN
Q 学习算法中的 Q 值过度估计是一个众所周知的问题。其原因是最大值操作符,它会过度估计实际的最大估计值。为了理解这个问题,假设我们有噪声估计,其均值为 0,但方差不为 0,如下图所示。尽管从渐近上看,平均值为 0,但 max 函数总是会返回大于 0 的值:

图 5.7. 从均值为 0 的正态分布中采样的六个值
在 Q 学习中,过度估计通常不是一个实际问题,直到较高的值均匀分布。然而,如果过度估计并不均匀,并且误差在状态和动作间存在差异,这种过度估计会对 DQN 算法产生负面影响,从而降低生成的策略的质量。
为了解决这个问题,在论文深度强化学习与双 Q 学习中,作者建议使用两种不同的估计器(即两个人工神经网络):一个用于动作选择,另一个用于 Q 值估计。但论文并没有使用两个不同的神经网络来增加复杂度,而是提出了使用在线网络来通过最大操作选择最佳动作,使用目标网络来计算其 Q 值。通过这个解决方案,目标值,
,将会改变,从标准 Q 学习中的如下形式:

现在,它是如下所示:
(5.7)
这种解耦版本显著减少了过度估计问题,并提高了算法的稳定性。
DDQN 实现
从实现的角度来看,实现 DDQN 的唯一变化是在训练阶段。只需要在 DDQN 实现中替换以下代码行:
mb_trg_qv = sess.run(target_qv, feed_dict={obs_ph:mb_obs2})
y_r = q_target_values(mb_rew, mb_done, mb_trg_qv, discount)
使用以下代码替换:
mb_onl_qv, mb_trg_qv = sess.run([online_qv,target_qv], feed_dict={obs_ph:mb_obs2})
y_r = double_q_target_values(mb_rew, mb_done, mb_trg_qv, mb_onl_qv, discount)
在这里,double_q_target_values是一个计算每个小批量转换的(5.7)的函数。
结果
为了验证 DQN 是否真的高估了 Q 值,相对于 DDQN,我们在下图中展示了 Q 值的图表。我们还包括了 DQN(橙线)的结果,以便进行两种算法的直接比较:

图 5.8. 估计训练 Q 值的图表。DDQN 的值用蓝色表示,DQN 的值用橙色表示。x 轴表示步骤数量
DDQN(蓝线)和 DQN(橙线)的表现,通过测试游戏的平均奖励表示,结果如下:
关于本章节中提到的所有颜色参考,请参考颜色图片包。

图 5.9. 平均测试奖励的图表。DDQN 的值用蓝色表示,DQN 的值用橙色表示。x 轴表示步骤数量
正如我们预期的那样,DDQN 中的 Q 值始终小于 DQN,这意味着后者实际上在高估 Q 值。尽管如此,测试游戏的表现似乎并未受到影响,这意味着这些高估可能并未对算法的性能造成伤害。然而,请注意,我们只在 Pong 游戏上测试了该算法。一个算法的有效性不应该仅在单一环境中进行评估。事实上,在论文中,作者将其应用于所有 57 个 ALE 游戏,并报告称 DDQN 不仅能提供更准确的值估计,还在多个游戏中获得了更高的得分。
对抗 DQN
在论文《Dueling Network Architectures for Deep Reinforcement Learning》(arxiv.org/abs/1511.06581)中,提出了一种新型的神经网络架构,包含两个独立的估计器:一个用于状态值函数,另一个用于状态-动作优势值函数。
优势函数在强化学习中无处不在,其定义如下:

优势函数告诉我们某个动作的改善程度,
,与在给定状态下的平均动作,
相比。因此,如果
是正值,这意味着该动作,
,优于该状态下的平均动作,
。相反,如果
是负值,这意味着
比该状态下的平均动作,
更差。
因此,像论文中所做的那样分别估计价值函数和优势函数,使我们能够重建 Q 函数,如下所示:
(5.8)
这里,已添加优势的均值,以提高 DQN 的稳定性。
对抗 DQN 的架构由两个头(或流)组成:一个用于价值函数,一个用于优势函数,且都共享一个公共的卷积模块。作者报告称,这种架构可以学习哪些状态是有价值的,哪些不是,而无需学习每个动作在某一状态下的绝对值。他们在 Atari 游戏上测试了这一新架构,并在整体性能方面取得了显著的提升。
对抗 DQN 实现
这种架构以及公式 (5.8) 的一个好处是,它不会对底层的强化学习算法施加任何更改。唯一的变化是在 Q 网络的构建上。因此,我们可以用 dueling_qnet 函数替换 qnet,其实现方法如下:
def dueling_qnet(x, hidden_layers, output_size, fnn_activation=tf.nn.relu, last_activation=None):
x = cnn(x)
x = tf.layers.flatten(x)
qf = fnn(x, hidden_layers, 1, fnn_activation, last_activation)
aaqf = fnn(x, hidden_layers, output_size, fnn_activation, last_activation)
return qf + aaqf - tf.reduce_mean(aaqf)
创建了两个前馈神经网络:一个只有一个输出(用于价值函数),另一个具有与智能体动作数相同的输出(用于状态依赖的动作优势函数)。最后一行返回公式 (5.8)。
结果
如下图所示,测试奖励的结果很有前景,证明了使用对抗架构的明显好处:

图 5.10。测试奖励的绘图。对抗 DQN 的值以红色绘制,DQN 的值以橙色绘制。x 轴表示步数。
N 步 DQN
n 步 DQN 背后的思想很久以前就有了,它源于时间差分学习(TD)与蒙特卡罗学习(MC)之间的转变。这些算法在 第四章中介绍过,Q-learning 和 SARSA 应用,是一个常见谱系的两个极端。TD 学习从单一步骤中学习,而 MC 从完整的轨迹中学习。TD 学习表现出最小的方差,但最大偏差,而 MC 则表现出较大的方差,但最小偏差。方差-偏差问题可以通过使用 n 步返回来平衡。n 步返回是经过 n 步后计算的回报。TD 学习可以视为 0 步返回,而 MC 可以视为一个
-步返回。
通过 n 步返回,我们可以更新目标值,具体如下:
(5.9)
这里,
是步数。
n 步回报就像向前看 n 步,但实际上,由于不可能真正预见未来,它是以相反的方式进行的,即通过计算 n 步前的值。这导致值仅在时间 t - n 处可用,从而延迟了学习过程。
这种方法的主要优点在于目标值偏差较小,这可能导致更快的学习。一个重要的问题是,这种方式计算的目标值只有在学习是 on-policy 时才正确(DQN 是 off-policy)。这是因为公式(5.9)假设代理将在接下来的 n 步中遵循相同的策略来收集经验。虽然有一些方法可以调整 off-policy 情况,但一般来说实现起来比较复杂,最好的一般实践是保持一个小的 n 并忽略这个问题。
实施
要实现 n 步 DQN,只需要对缓冲区进行少量更改。在从缓冲区采样时,需要返回 n 步回报、n 步下一个状态和 n 步完成标志。我们不会在此提供实现,因为它相当简单,但你可以在本书的 GitHub 存储库中查看代码。支持 n 步回报的代码在 MultiStepExperienceBuffer 类中。
结果
对于离线策略算法(如 DQN),n 步学习在小 n 值时效果显著。在 DQN 中,已经证明算法在 2 到 4 的 n 值范围内表现良好,从而改进了广泛的 Atari 游戏。
在下图中,我们的实现结果可见。我们使用三步回报测试了 DQN。从结果来看,在起飞之前需要更多时间。之后,它的学习曲线更陡,但与 DQN 整体类似:

图 5.11. 测试平均总奖励的图表。三步 DQN 值以紫色绘制,DQN 值以橙色绘制。x 轴表示步数
总结
在本章中,我们进一步探讨了强化学习算法,并讨论了如何将其与函数逼近器结合,以便将强化学习应用于更广泛的问题。具体来说,我们描述了如何在 Q-learning 中使用函数逼近和深度神经网络,以及由此引起的不稳定性。我们证明了,在实践中,深度神经网络不能在不进行任何修改的情况下与 Q-learning 结合使用。
能够将深度神经网络与 Q-learning 结合使用的第一个算法是 DQN。它集成了两个关键要素,以稳定学习并控制复杂任务,例如 Atari 2600 游戏。这两个要素分别是回放缓冲区,用于存储旧经验,以及更新频率低于在线网络的独立目标网络。前者用于利用 Q-learning 的离政策性质,以便它可以从不同策略(在这种情况下是旧策略)的经验中学习,并从更大的数据池中采样更多的独立同分布(i.i.d)小批量数据,以执行随机梯度下降。后者的引入是为了稳定目标值并减少非平稳性问题。
在对 DQN 进行了正式介绍后,我们将其实现并在 Pong(一款 Atari 游戏)上进行了测试。此外,我们还展示了该算法的更多实际方面,例如预处理管道和包装器。在 DQN 发布之后,许多其他变种被提出以改进该算法并克服其不稳定性。我们研究了这些变种,并实现了三种变体,即 Double DQN、Dueling DQN 和 n-step DQN。尽管在本章中,我们仅将这些算法应用于 Atari 游戏,但它们可以用于解决许多现实世界中的问题。
在下一章中,我们将介绍另一类深度强化学习算法——策略梯度算法。这些算法是基于策略的,正如我们很快将看到的,它们具有一些非常重要和独特的特性,使得它们能够应用于更广泛的问题领域。
问题
-
致命三合一问题的原因是什么?
-
DQN 是如何克服不稳定性的?
-
什么是移动目标问题?
-
DQN 是如何缓解移动目标问题的?
-
DQN 中使用的优化过程是什么?
-
状态-动作优势值函数的定义是什么?
深入阅读
-
有关 OpenAI Gym 包装器的全面教程,请阅读以下文章:
hub.packtpub.com/openai-gym-environments-wrappers-and-monitors-tutorial/。 -
要查看原始的Rainbow论文,请访问
arxiv.org/abs/1710.02298。
第六章:学习随机优化与 PG 优化
到目前为止,我们已经探讨并开发了基于价值的强化学习算法。这些算法通过学习一个价值函数来找到一个好的策略。尽管它们表现良好,但它们的应用受限于一些内在的限制。在本章中,我们将介绍一类新的算法——策略梯度方法,它们通过从不同的角度处理强化学习问题来克服基于价值方法的限制。
策略梯度方法基于学习到的参数化策略来选择动作,而不是依赖于价值函数。在本章中,我们还将详细阐述这些方法背后的理论和直觉,并在此基础上开发出最基本版本的策略梯度算法,称为REINFORCE。
REINFORCE 由于其简单性存在一些不足,但这些不足只需要稍加努力就可以得到缓解。因此,我们将展示 REINFORCE 的两个改进版本,分别是带基线的REINFORCE和演员-评论员(AC)模型。
本章将涵盖以下主题:
-
策略梯度方法
-
理解 REINFORCE 算法
-
带基线的 REINFORCE
-
学习 AC 算法
策略梯度方法
到目前为止,学习和开发的算法都是基于价值的,它们的核心是学习一个价值函数,V(s),或动作价值函数,Q(s, a)。价值函数是一个定义从给定状态或状态-动作对所能累积的总奖励的函数。然后,可以基于估计的动作(或状态)值来选择一个动作。
因此,贪婪策略可以定义如下:
![]
当结合深度神经网络时,基于价值的方法可以学习非常复杂的策略,从而控制在高维空间中操作的智能体。尽管这些方法有着很大的优点,但在处理具有大量动作或连续动作空间的问题时,它们会遇到困难。
在这种情况下,最大化操作是不可行的。策略梯度(PG)算法在这种背景下展现了巨大的潜力,因为它们可以很容易地适应连续的动作空间。
PG 方法属于更广泛的基于策略的方法类,其中包括进化策略,这将在第十一章《理解黑盒优化算法》中进一步探讨。PG 算法的独特性在于它们使用策略的梯度,因此得名策略梯度。
相较于第三章《使用动态规划解决问题》中报告的强化学习算法分类,下面的图示展示了一种更简洁的分类方式:

策略梯度方法的例子有REINFORCE和AC,将在接下来的章节中介绍。
策略的梯度
强化学习的目标是最大化一个轨迹的期望回报(总奖励,无论是否折扣)。目标函数可以表示为:

其中θ是策略的参数,例如深度神经网络的可训练变量。
在 PG 方法中,目标函数的最大化是通过目标函数的梯度![]来实现的。通过梯度上升,我们可以通过将参数朝着梯度的方向移动来改善![],因为梯度指向函数增大的方向。
我们必须沿着梯度的方向前进,因为我们的目标是最大化目标函数(6.1)。
一旦找到最大值,策略π[θ]将产生回报最高的轨迹。从直观上看,策略梯度通过增加好策略的概率并减少差策略的概率来激励好的策略。
使用方程(6.1),目标函数的梯度定义如下:

通过与前几章的概念相关联,在策略梯度方法中,策略评估是回报的估计,
。而策略改进则是参数
的优化步骤。因此,策略梯度方法必须协同进行这两个阶段,以改进策略。
策略梯度定理
在查看方程(6.2)时遇到一个初始问题,因为在其公式中,目标函数的梯度依赖于策略的状态分布;即:

我们会使用该期望的随机逼近方法,但为了计算状态的分布,
,我们仍然需要一个完整的环境模型。因此,这种公式不适用于我们的目的。
策略梯度定理在这里提供了解决方案。它的目的是提供一个分析公式,用来计算目标函数相对于策略参数的梯度,而无需涉及状态分布的导数。形式上,策略梯度定理使我们能够将目标函数的梯度表示为:

策略梯度定理的证明超出了本书的范围,因此未包含。然而,你可以在 Sutton 和 Barto 的书中找到相关内容 (incompleteideas.net/book/the-book-2nd.htmlor),或者通过其他在线资源查找。
现在目标函数的导数不涉及状态分布的导数,可以通过从策略中采样来估计期望值。因此,目标函数的导数可以近似如下:

这可以用来通过梯度上升法生成一个随机更新:

请注意,由于目标是最大化目标函数,因此使用梯度上升来将参数朝梯度的方向移动(与梯度下降相反,梯度下降执行
)。
方程 (6.5) 背后的思想是增加未来重新提出好动作的概率,同时减少坏动作的概率。动作的质量由通常的标量值
传递,这给出了状态-动作对的质量。
计算梯度
只要策略是可微的,它的梯度就可以很容易地计算,借助现代自动微分软件。
在 TensorFlow 中,我们可以定义计算图并调用 tf.gradient(loss_function,variables) 来计算损失函数(loss_function)相对于variables可训练参数的梯度。另一种方法是直接使用随机梯度下降优化器最大化objective函数,例如,调用 tf.train.AdamOptimizer(lr).minimize(-objective_function)。
以下代码片段是计算公式 (6.5) 中近似值所需步骤的示例,使用的是env.action_space.n维度的离散动作空间策略:
pi = policy(states) # actions probability for each action
onehot_action = tf.one_hot(actions, depth=env.action_space.n)
pi_log = tf.reduce_sum(onehot_action * tf.math.log(pi), axis=1)
pi_loss = -tf.reduce_mean(pi_log * Q_function(states, actions))
# calculate the gradients of pi_loss with respect to the variables
gradients = tf.gradient(pi_loss, variables)
# or optimize directly pi_loss with Adam (or any other SGD optimizer)
# pi_opt = tf.train.AdamOptimizer(lr).minimize(pi_loss) #
tf.one_hot生成actions动作的独热编码。也就是说,它生成一个掩码,其中 1 对应动作的数值,其他位置为 0。
然后,在代码的第三行中,掩码与动作概率的对数相乘,以获得actions动作的对数概率。第四行按如下方式计算损失:

最后,tf.gradient计算pi_loss的梯度,关于variables参数,如公式(6.5)所示。
策略
如果动作是离散且数量有限,最常见的方法是创建一个参数化策略,为每个动作生成一个数值。
请注意,与深度 Q 网络(Deep Q-Network)算法不同,这里策略的输出值不是 Q(s,a) 动作值。
然后,每个输出值会被转换成概率。此操作是通过 softmax 函数执行的,函数如下所示:

softmax 值被归一化以使其总和为 1,从而产生一个概率分布,其中每个值对应于选择给定动作的概率。
接下来的两个图表展示了在应用 softmax 函数之前(左侧的图)和之后(右侧的图)的五个动作值预测示例。实际上,从右侧的图中可以看到,经过 softmax 计算后,新值的总和为 1,并且它们的值都大于零:

右侧的图表表示,动作 0、1、2、3 和 4 将分别以 0.64、0.02、0.09、0.21 和 0.02 的概率被选择。
为了在由参数化策略返回的动作值上使用 softmax 分布,我们可以使用计算梯度部分给出的代码,只需做一个改动,以下代码片段中已做突出显示:
pi = policy(states) # actions probability for each action
onehot_action = tf.one_hot(actions, depth=env.action_space.n)
pi_log = tf.reduce_sum(onehot_action * tf.nn.log_softmax(pi), axis=1) # instead of tf.math.log(pi)
pi_loss = -tf.reduce_mean(pi_log * Q_function(states, actions))
gradients = tf.gradient(pi_loss, variables)
在这里,我们使用了tf.nn.log_softmax,因为它比先调用tf.nn.softmax,再调用tf.math.log更稳定。
按照随机分布选择动作的一个优势在于动作选择的内在随机性,这使得环境的动态探索成为可能。这看起来像是一个副作用,但拥有能够自主调整探索程度的策略非常重要。
在 DQN 的情况下,我们不得不用手工调整的!变量来调整整个训练过程中的探索,使用线性!衰减。现在,探索已经内建到策略中,我们最多只需在损失函数中添加一个项(熵),以此来激励探索。
策略性 PG
策略梯度算法的一个非常重要的方面是它们是策略性算法。它们的策略性特征来自公式(6.4),因为它依赖于当前的策略。因此,与 DQN 等非策略性算法不同,策略性方法不允许重用旧的经验。
这意味着,一旦策略发生变化,所有使用给定策略收集的经验都必须被丢弃。作为副作用,策略梯度算法的样本效率较低,这意味着它们需要获取更多的经验才能达到与非策略性算法相同的表现。此外,它们通常会稍微泛化得较差。
理解 REINFORCE 算法
策略梯度算法的核心已经介绍过了,但我们还有一个重要的概念需要解释。我们还需要看一下如何计算动作值。
我们已经在公式(6.4)中看到了:

我们能够通过直接从经验中采样来估计目标函数的梯度,该经验是通过遵循**![]*策略收集的。
唯一涉及的两个项是![]的值和策略对数的导数,这可以通过现代深度学习框架(如 TensorFlow 和 PyTorch)获得。虽然我们已经定义了![],但我们尚未解释如何估计动作值函数。
首次由 Williams 在 REINFORCE 算法中提出的更简单方法是使用蒙特卡洛(MC)回报来估计回报。因此,REINFORCE 被认为是一个 MC 算法。如果你还记得,MC 回报是通过给定策略运行的采样轨迹的回报值。因此,我们可以重写方程(6.4),将动作值函数![]替换为 MC 回报![]:

回报是通过完整的轨迹计算得出的,这意味着 PG 更新只有在完成
步骤后才可用,其中
是轨迹中的总步骤数。另一个后果是,MC 回报仅在情节性问题中定义良好,在这种问题中,最大步骤数有一个上限(这与我们之前学习的其他 MC 算法得出的结论相同)。
更实际一点,时间点
的折扣回报,也可以称为未来回报,因为它只使用未来的回报,如下所示:

这可以递归地重写如下:

该函数可以按相反的顺序实现,从最后一个回报开始,如下所示:
def discounted_rewards(rews, gamma):
rtg = np.zeros_like(rews, dtype=np.float32)
rtg[-1] = rews[-1]
for i in reversed(range(len(rews)-1)):
rtg[i] = rews[i] + gamma*rtg[i+1]
return rtg
在这里,首先创建一个 NumPy 数组,并将最后一个回报的值分配给rtg变量。之所以这样做,是因为在时间点
,
。然后,算法使用后续值反向计算rtg[i]。
REINFORCE 算法的主要循环包括运行几个周期,直到收集到足够的经验,并优化策略参数。为了有效,算法必须在执行更新步骤之前完成至少一个周期(它需要至少一个完整轨迹来计算回报函数(
))。REINFORCE 的伪代码总结如下:
Initialize with random weight
for episode 1..M do
Initialize environment
Initialize empty buffer
*> Generate a few episodes*
for step 1..MaxSteps do
*> Collect experience by acting on the environment*
if :
* > Compute the reward to go*
# for each t
*> Store the episode in the buffer*
# where is the length of the episode
*> REINFORCE update step using all the experience in  following formula (6.5)
*
实现 REINFORCE
现在是时候实现 REINFORCE 了。在这里,我们仅提供算法的实现,而不包括调试和监控过程。完整实现可以在 GitHub 仓库中找到。所以,务必检查一下。
代码分为三个主要函数和一个类:
-
REINFORCE(env_name, hidden_sizes, lr, num_epochs, gamma, steps_per_epoch):这是包含算法主要实现的函数。 -
Buffer:这是一个类,用于临时存储轨迹。 -
mlp(x, hidden_layer, output_size, activation, last_activation):这是用来在 TensorFlow 中构建多层感知器的。 -
discounted_rewards(rews, gamma):该函数计算折扣奖励。
我们首先将查看主要的 REINFORCE 函数,然后实现补充的函数和类。
REINFORCE 函数分为两个主要部分。在第一部分,创建计算图;而在第二部分,运行环境并循环优化策略,直到满足收敛标准。
REINFORCE 函数以 env_name 环境的名称作为输入参数,包含隐藏层大小的列表—hidden_sizes,学习率—lr,训练周期数—num_epochs,折扣因子—gamma,以及每个周期的最小步骤数—steps_per_epoch。正式地,REINFORCE 的函数头如下:
def REINFORCE(env_name, hidden_sizes=[32], lr=5e-3, num_epochs=50, gamma=0.99, steps_per_epoch=100):
在 REINFORCE(..) 开始时,TensorFlow 默认图被重置,环境被创建,占位符被初始化,策略被创建。策略是一个全连接的多层感知器,每个动作对应一个输出,且每一层的激活函数为 tanh。多层感知器的输出是未归一化的动作值,称为 logits。所有这些操作都在以下代码片段中完成:
def REINFORCE(env_name, hidden_sizes=[32], lr=5e-3, num_epochs=50, gamma=0.99, steps_per_epoch=100):
tf.reset_default_graph()
env = gym.make(env_name)
obs_dim = env.observation_space.shape
act_dim = env.action_space.n
obs_ph = tf.placeholder(shape=(None, obs_dim[0]), dtype=tf.float32, name='obs')
act_ph = tf.placeholder(shape=(None,), dtype=tf.int32, name='act')
ret_ph = tf.placeholder(shape=(None,), dtype=tf.float32, name='ret')
p_logits = mlp(obs_ph, hidden_sizes, act_dim, activation=tf.tanh)
接着,我们可以创建一个操作,用来计算损失函数,并优化策略。代码与我们之前在 The policy 部分看到的代码类似。唯一的不同是现在通过 tf.random.multinomial 来采样动作,该函数根据策略返回的动作分布来选择动作。此函数从类别分布中抽取样本。在我们的例子中,它选择一个单一的动作(根据环境,也可能选择多个动作)。
以下代码片段是 REINFORCE 更新的实现:
act_multn = tf.squeeze(tf.random.multinomial(p_logits, 1))
actions_mask = tf.one_hot(act_ph, depth=act_dim)
p_log = tf.reduce_sum(actions_mask * tf.nn.log_softmax(p_logits), axis=1)
p_loss = -tf.reduce_mean(p_log*ret_ph)
p_opt = tf.train.AdamOptimizer(lr).minimize(p_loss)
在与环境交互过程中,创建一个针对选择的动作的掩码,并与 log_softmax 相乘,以便计算
。然后,计算完整的损失函数。注意—在 tf.reduce_sum 前面有一个负号。我们关注的是目标函数的最大化。但因为优化器需要一个最小化的函数,所以我们必须传递一个损失函数。最后一行使用 AdamOptimizer 优化 PG 损失函数。
我们现在准备开始一个会话,重置计算图的全局变量,并初始化一些稍后会用到的变量:
sess = tf.Session()
sess.run(tf.global_variables_initializer())
step_count = 0
train_rewards = []
train_ep_len = []
然后,我们创建两个内部循环,这些循环将与环境交互以收集经验并优化策略,并打印一些统计数据:
for ep in range(num_epochs):
obs = env.reset()
buffer = Buffer(gamma)
env_buf = []
ep_rews = []
while len(buffer) < steps_per_epoch:
# run the policy
act = sess.run(act_multn, feed_dict={obs_ph:[obs]})
# take a step in the environment
obs2, rew, done, _ = env.step(np.squeeze(act))
env_buf.append([obs.copy(), rew, act])
obs = obs2.copy()
step_count += 1
ep_rews.append(rew)
if done:
# add the full trajectory to the environment
buffer.store(np.array(env_buf))
env_buf = []
train_rewards.append(np.sum(ep_rews))
train_ep_len.append(len(ep_rews))
obs = env.reset()
ep_rews = []
obs_batch, act_batch, ret_batch = buffer.get_batch()
# Policy optimization
sess.run(p_opt, feed_dict={obs_ph:obs_batch, act_ph:act_batch, ret_ph:ret_batch})
# Print some statistics
if ep % 10 == 0:
print('Ep:%d MnRew:%.2f MxRew:%.1f EpLen:%.1f Buffer:%d -- Step:%d --' % (ep, np.mean(train_rewards), np.max(train_rewards), np.mean(train_ep_len), len(buffer), step_count))
train_rewards = []
train_ep_len = []
env.close()
这两个循环遵循通常的流程,唯一的例外是,当轨迹结束时,与环境的交互会停止,并且临时缓冲区有足够的转移。
现在我们可以实现一个Buffer类,用于包含轨迹数据:
class Buffer():
def __init__(self, gamma=0.99):
self.gamma = gamma
self.obs = []
self.act = []
self.ret = []
def store(self, temp_traj):
if len(temp_traj) > 0:
self.obs.extend(temp_traj[:,0])
ret = discounted_rewards(temp_traj[:,1], self.gamma)
self.ret.extend(ret)
self.act.extend(temp_traj[:,2])
def get_batch(self):
return self.obs, self.act, self.ret
def __len__(self):
assert(len(self.obs) == len(self.act) == len(self.ret))
return len(self.obs)
最后,我们可以实现一个函数,创建一个具有任意数量隐藏层的神经网络:
def mlp(x, hidden_layers, output_size, activation=tf.nn.relu, last_activation=None):
for l in hidden_layers:
x = tf.layers.dense(x, units=l, activation=activation)
return tf.layers.dense(x, units=output_size, activation=last_activation)
这里,activation是应用于隐藏层的非线性函数,而last_activation是应用于输出层的非线性函数。
使用 REINFORCE 着陆航天器
算法已经完成,但最有趣的部分还没有解释。在本节中,我们将应用 REINFORCE 到LunarLander-v2,这是一个周期性的 Gym 环境,目标是让月球着陆器着陆。
以下是游戏初始位置的截图,以及一个假设的成功最终位置:

这是一个离散问题,着陆器必须在坐标(0,0)处着陆,如果远离该点则会受到惩罚。着陆器从屏幕顶部移动到底部时会获得正奖励,但当它开启引擎减速时,每一帧会损失 0.3 分。
此外,根据着陆条件,它会获得额外的-100 或+100 分。游戏被认为在获得 200 分时解决。每局游戏最多进行 1,000 步。
出于这个原因,我们将至少收集 1,000 步的经验,以确保至少完成了一个完整的回合(这个值由steps_per_epoch超参数设置)。
通过调用带有以下超参数的函数来运行 REINFORCE:
REINFORCE('LunarLander-v2', hidden_sizes=[64], lr=8e-3, gamma=0.99, num_epochs=1000, steps_per_epoch=1000)
分析结果
在整个学习过程中,我们监控了许多参数,包括p_loss(策略的损失)、old_p_loss(优化阶段前的策略损失)、总奖励和每局的长度,以便更好地理解算法,并合理调整超参数。我们还总结了一些直方图。要了解更多关于 TensorBoard 汇总的内容,请查看书籍仓库中的代码!
在下图中,我们绘制了训练过程中获得的完整轨迹的总奖励的均值:

从这张图中,我们可以看到,它在大约 500,000 步时达到了 200 的平均分数,或者稍微低一点;因此,在能够掌握游戏之前,约需要 1,000 个完整的轨迹。
在绘制训练性能图时,请记住,算法可能仍在探索中。要检查是否如此,可以监控动作的熵。如果熵大于 0,意味着算法对于所选动作不确定,并且它会继续探索——选择其他动作,并遵循它们的分布。在这种情况下,经过 500,000 步后,智能体仍在探索环境,如下图所示:

带基线的 REINFORCE
REINFORCE 具有一个很好的特性,即由于 MC 回报,它是无偏的,这提供了完整轨迹的真实回报。然而,无偏估计会以方差为代价,方差随着轨迹的长度增加而增大。为什么?这种效应是由于策略的随机性。通过执行完整的轨迹,你会知道它的真实奖励。然而,分配给每个状态-动作对的值可能并不正确,因为策略是随机的,重新执行可能会导致不同的状态,从而产生不同的奖励。此外,你会看到,轨迹中动作的数量越多,系统中引入的随机性就越大,因此,最终会得到更高的方差。
幸运的是,可以在回报估计中引入基线,
,从而减少方差,并提高算法的稳定性和性能。采用这种策略的算法称为带基线的 REINFORCE,其目标函数的梯度如下所示:

引入基线的这个技巧之所以可行,是因为梯度估计器在偏差上仍然保持不变:

与此同时,为了使这个方程成立,基线必须对动作保持常数。
我们现在的任务是找到一个合适的
基线。最简单的方法是减去平均回报。

如果你想在 REINFORCE 代码中实现这一点,唯一需要更改的是Buffer类中的get_batch()函数:
def get_batch(self):
b_ret = self.ret - np.mean(self.ret)
return self.obs, self.act, b_ret
尽管这个基线减少了方差,但它并不是最佳策略。因为基线可以根据状态进行条件化,一个更好的想法是使用值函数的估计:

请记住,
值函数平均值是通过
策略获得的回报。
这种变体给系统带来了更多复杂性,因为我们必须设计一个值函数的近似,但它是非常常见的,并且能显著提高算法的性能。
为了学习!,最佳的解决方案是用 MC 估计拟合一个神经网络:

在前面的方程中,
是需要学习的神经网络参数。
为了不使符号过于复杂,从现在开始,我们将省略指定策略的部分,因此!将变为!。
神经网络在与学习相关的相同轨迹数据上进行训练!,无需与环境进行额外的交互。计算后,MC 估计(例如,使用discounted_rewards(rews, gamma))将成为!目标值,并且神经网络将被优化,以最小化均方误差(MSE)损失——就像你在监督学习任务中做的那样:

这里,
是价值函数神经网络的权重,每个数据集元素包含!状态,以及目标值!。
实现带基准的 REINFORCE
基准用神经网络逼近的价值函数可以通过在我们之前的代码中添加几行来实现:
- 将神经网络、计算 MSE 损失函数的操作和优化过程添加到计算图中:
...
# placeholder that will contain the reward to go values (i.e. the y values)
rtg_ph = tf.placeholder(shape=(None,), dtype=tf.float32, name='rtg')
# MLP value function
s_values = tf.squeeze(mlp(obs_ph, hidden_sizes, 1, activation=tf.tanh))
# MSE loss function
v_loss = tf.reduce_mean((rtg_ph - s_values)**2)
# value function optimization
v_opt = tf.train.AdamOptimizer(vf_lr).minimize(v_loss)
...
...
# besides act_multn, run also s_values
act, val = sess.run([act_multn, s_values], feed_dict={obs_ph:[obs]})
obs2, rew, done, _ = env.step(np.squeeze(act))
# add the new transition, included the state value predictions
env_buf.append([obs.copy(), rew, act, np.squeeze(val)])
...
- 检索
rtg_batch,它包含来自缓冲区的“目标”值,并优化价值函数:
obs_batch, act_batch, ret_batch, rtg_batch = buffer.get_batch()
sess.run([p_opt, v_opt], feed_dict={obs_ph:obs_batch, act_ph:act_batch, ret_ph:ret_batch, rtg_ph:rtg_batch})
def store(self, temp_traj):
if len(temp_traj) > 0:
self.obs.extend(temp_traj[:,0])
rtg = discounted_rewards(temp_traj[:,1], self.gamma)
# ret = G - V
self.ret.extend(rtg - temp_traj[:,3])
self.rtg.extend(rtg)
self.act.extend(temp_traj[:,2])
def get_batch(self):
return self.obs, self.act, self.ret, self.rtg
你现在可以在任何你想要的环境中测试带基准的 REINFORCE 算法,并将其性能与基本的 REINFORCE 实现进行比较。
学习 AC 算法
简单的 REINFORCE 具有不偏性的显著特点,但它表现出较高的方差。添加基准可以减少方差,同时保持不偏(从渐近的角度来看,算法将收敛到局部最小值)。带基准的 REINFORCE 的一个主要缺点是它收敛得非常慢,需要与环境进行一致的交互。
加速训练的一种方法叫做引导法(bootstrapping)。这是我们在本书中已经多次看到的技巧。它允许从后续的状态值估算回报值。使用这种技巧的策略梯度算法称为演员-评论者(AC)。在 AC 算法中,演员是策略,评论者是价值函数(通常是状态值函数),它对演员的行为进行“批评”,帮助他更快学习。AC 方法的优点有很多,但最重要的是它们能够在非阶段性问题中学习。
无法使用 REINFORCE 解决连续任务,因为要计算奖赏到达目标,它们需要直到轨迹结束的所有奖赏(如果轨迹是无限的,就没有结束)。依靠引导技术,AC 方法也能够从不完整的轨迹中学习动作值。
使用评论者帮助演员学习
使用一步引导法的动作值函数定义如下:

在这里,
是臭名昭著的下一个状态。
因此,使用一个
角色和一个
评论者使用引导法(bootstrapping),我们可以得到一步的 AC 步骤:

这将用一个基准替代 REINFORCE 步骤:

注意 REINFORCE 和 AC 中使用状态值函数的区别。在前者中,它仅作为基准,用来提供当前状态的状态值。在后者示例中,状态值函数用于估算下一个状态的价值,从而只需要当前的奖励来估算
。因此,我们可以说,一步 AC 模型是一个完全在线的增量算法。
n 步 AC 模型
实际上,正如我们在 TD 学习中所看到的,完全在线的算法具有低方差但高偏差,这与 MC 学习相反。然而,通常,介于完全在线和 MC 方法之间的中间策略是首选。为了平衡这种权衡,n 步回报可以替代在线算法中的一步回报。
如果你还记得,我们已经在 DQN 算法中实现了 n 步学习。唯一的区别是 DQN 是一个脱离策略的算法,而理论上,n 步学习只能在在线策略算法中使用。然而,我们展示了通过一个小的
,性能有所提高。
AC 算法是基于策略的,因此,就性能提升而言,可以使用任意大的
值。在 AC 中集成 n 步是相当直接的;一步返回被
替代,值函数被带入
状态:

这里,
。请注意,如果
是一个最终状态,
。
除了减少偏差外,n 步返回还可以更快地传播后续的回报,从而使得学习更加高效。
有趣的是,
量可以看作是优势函数的估计。事实上,优势函数定义如下:

由于
是
的估计,我们得到优势函数的估计。通常,这个函数更容易学习,因为它仅表示在特定状态下一个特定动作相对于其他动作的偏好。它不需要学习该状态的值。
关于评论员权重的优化,它使用一种著名的 SGD 优化方法来进行优化,最小化 MSE 损失:

在前面的方程中,目标值是按如下方式计算的:
。
AC 实现
总体而言,正如我们到目前为止所看到的,AC 算法与 REINFORCE 算法非常相似,状态函数作为基准。但为了回顾一下,算法总结如下:
Initialize with random weight
Initialize environment
for episode 1..M do
Initialize empty buffer
*> Generate a few episodes*
for step 1..MaxSteps do
*> Collect experience by acting on the environment*
if :
*> Compute the n-step reward to go*
# for each t
*> Compute the advantage values*
# for each t
*> Store the episode in the buffer*
# where is the lenght of the episode
*> Actor update step using all the experience in 
*
*> Critic update using all the experience in **D***
与 REINFORCE 的唯一区别是 n 步奖励的计算、优势函数的计算,以及主函数的一些调整。
让我们首先看一下折扣奖励的新实现。与之前不同,最后一个last_sv状态的估计值现在传递给输入,并用于引导,如以下实现所示:
def discounted_rewards(rews, last_sv, gamma):
rtg = np.zeros_like(rews, dtype=np.float32)
rtg[-1] = rews[-1] + gamma*last_sv # Bootstrap with the estimate next state value
for i in reversed(range(len(rews)-1)):
rtg[i] = rews[i] + gamma*rtg[i+1]
return rtg
计算图不会改变,但在主循环中,我们需要注意一些小的但非常重要的变化。
显然,函数的名称已更改为AC,并且cr_lr评论员的学习率作为一个参数被添加进来。
第一个实际的变化涉及环境重置的方式。如果在 REINFORCE 中,偏好在每次主循环迭代时重置环境,那么在 AC 中,我们必须从上一轮迭代的环境状态继续,只有在环境达到最终状态时才重置它。
第二个变化涉及行动价值函数的引导方式,以及如何计算未来的回报。记住,对于每个状态-动作对,除非
是最终状态,否则
。在这种情况下,
。因此,我们必须在最后状态时使用0进行引导,并在其他情况下使用
进行引导。根据这些更改,代码如下:
obs = env.reset()
ep_rews = []
for ep in range(num_epochs):
buffer = Buffer(gamma)
env_buf = []
for _ in range(steps_per_env):
act, val = sess.run([act_multn, s_values], feed_dict={obs_ph:[obs]})
obs2, rew, done, _ = env.step(np.squeeze(act))
env_buf.append([obs.copy(), rew, act, np.squeeze(val)])
obs = obs2.copy()
step_count += 1
last_test_step += 1
ep_rews.append(rew)
if done:
buffer.store(np.array(env_buf), 0)
env_buf = []
train_rewards.append(np.sum(ep_rews))
train_ep_len.append(len(ep_rews))
obs = env.reset()
ep_rews = []
if len(env_buf) > 0:
last_sv = sess.run(s_values, feed_dict={obs_ph:[obs]})
buffer.store(np.array(env_buf), last_sv)
obs_batch, act_batch, ret_batch, rtg_batch = buffer.get_batch()
sess.run([p_opt, v_opt], feed_dict={obs_ph:obs_batch, act_ph:act_batch, ret_ph:ret_batch, rtg_ph:rtg_batch})
...
第三个变化发生在Buffer类的store方法中。实际上,现在我们还需要处理不完整的轨迹。在之前的代码片段中,我们看到估计的
状态值作为第三个参数传递给store函数。事实上,我们使用这些状态值进行引导,并计算"未来回报"。在新版本的store中,我们将与状态值相关的变量命名为last_sv,并将其作为输入传递给discounted_reward函数,代码如下:
def store(self, temp_traj, last_sv):
if len(temp_traj) > 0:
self.obs.extend(temp_traj[:,0])
rtg = discounted_rewards(temp_traj[:,1], last_sv, self.gamma)
self.ret.extend(rtg - temp_traj[:,3])
self.rtg.extend(rtg)
self.act.extend(temp_traj[:,2])
使用 AC 着陆航天器
我们将 AC 应用于 LunarLander-v2,这与测试 REINFORCE 时使用的环境相同。这是一个回合制的游戏,因此它并没有完全强调 AC 算法的主要特性。尽管如此,它仍提供了一个很好的测试平台,你也可以自由地在其他环境中进行测试。
我们调用AC函数时使用以下超参数:
AC('LunarLander-v2', hidden_sizes=[64], ac_lr=4e-3, cr_lr=1.5e-2, gamma=0.99, steps_per_epoch=100, num_epochs=8000)
结果图显示了训练周期中累计的总回报,图如下:

你可以看到,AC 比 REINFORCE 更快,如下图所示。然而,AC 的稳定性较差,经过大约 200,000 步后,性能有所下降,但幸运的是,之后它继续增长:

在这个配置中,AC 算法每 100 步更新一次演员和评论员。从理论上讲,你可以使用更小的steps_per_epochs,但通常这会让训练变得更不稳定。使用更长的周期可以稳定训练,但演员学习速度较慢。一切都在于找到一个好的平衡点和适合的学习率。
对于本章提到的所有颜色参考,请参见以下链接中的彩色图像包:www.packtpub.com/sites/default/files/downloads/9781789131116_ColorImages.pdf。
高级 AC,以及技巧和窍门
AC 算法有许多进一步的进展,还有许多技巧和窍门需要记住,在设计此类算法时要加以注意:
-
架构设计:在我们的实现中,我们实现了两个不同的神经网络,一个用于评估者,另一个用于演员。也可以设计一个共享主要隐藏层的神经网络,同时保持头部的独立性。这种架构可能更难调整,但总体而言,它提高了算法的效率。
-
并行环境:减少方差的一个广泛采用的技术是从多个环境中并行收集经验。A3C(异步优势演员-评估者)算法异步更新全局参数。而它的同步版本,称为 A2C(优势演员-评估者),则在更新全局参数之前等待所有并行的演员完成。智能体的并行化确保了来自环境不同部分的更多独立经验。
-
批量大小:与其他强化学习算法(尤其是脱离策略算法)相比,政策梯度和 AC 方法需要较大的批量。因此,如果在调整其他超参数后,算法仍然无法稳定,考虑使用更大的批量大小。
-
学习率:调整学习率本身非常棘手,因此请确保使用更先进的 SGD 优化方法,如 Adam 或 RMSprop。
总结
在本章中,我们学习了一类新的强化学习算法,称为政策梯度。与之前章节中研究的价值函数方法相比,这些算法以不同的方式解决强化学习问题。
PG 方法的简化版本叫做 REINFORCE,这一方法在本章过程中进行了学习、实现和测试。随后,我们提出在 REINFORCE 中加入基准值,以减少方差并提高算法的收敛性。AC 算法不需要使用评估者的完整轨迹,因此我们用 AC 模型解决了同样的问题。
在掌握经典的政策梯度算法的基础上,我们可以进一步深入。在下一章,我们将介绍一些更复杂、前沿的政策梯度算法;即,信任区域策略优化(TRPO)和近端策略优化(PPO)。这两种算法是基于我们在本章中学习的内容构建的,但它们提出了一个新的目标函数,旨在提高 PG 算法的稳定性和效率。
问题
-
PG 算法如何最大化目标函数?
-
政策梯度算法的核心思想是什么?
-
为什么在 REINFORCE 中引入基准值后,算法仍保持无偏?
-
REINFORCE 属于更广泛的哪类算法?
-
AC 方法中的评估者与 REINFORCE 中作为基准的价值函数有什么不同?
-
如果你需要为一个必须学习移动的智能体开发算法,你会选择 REINFORCE 还是 AC?
-
你能将 n 步 AC 算法用作 REINFORCE 算法吗?
进一步阅读
要了解异步版本的演员-评论员算法,请阅读 arxiv.org/pdf/1602.01783.pdf。
第七章:TRPO 和 PPO 实现
在上一章中,我们研究了策略梯度算法。它们的独特之处在于解决 强化学习(RL)问题的顺序——策略梯度算法朝着奖励增益最大化的方向迈出一步。该算法的简化版本(REINFORCE)具有直接的实现,并且单独使用时能够取得不错的效果。然而,它的速度较慢,且方差较大。因此,我们引入了一个值函数,具有双重目标——批评演员并提供基准。尽管这些演员-评论家算法具有巨大的潜力,但它们可能会受到动作分布中不希望出现的剧烈波动的影响,从而导致访问的状态发生急剧变化,随之而来的是性能的迅速下降,且这种下降可能永远无法恢复。
本章将通过展示如何引入信任区域或剪切目标来解决这一问题,从而减轻该问题的影响。我们将展示两个实际的算法,即 TRPO 和 PPO。这些算法已经证明能在控制模拟行走、控制跳跃和游泳机器人以及玩 Atari 游戏方面取得良好效果。我们将介绍一组新的连续控制环境,并展示如何将策略梯度算法适配到连续动作空间中。通过将 TRPO 和 PPO 应用于这些新环境,您将能够训练一个智能体进行跑步、跳跃和行走。
本章将涵盖以下主题:
-
Roboschool
-
自然策略梯度
-
信任区域策略优化
-
近端策略优化
Roboschool
到目前为止,我们已经处理了离散控制任务,例如 第五章中的 Atari 游戏,深度 Q 网络,以及 第六章中的 LunarLander,学习随机过程和 PG 优化。为了玩这些游戏,只需要控制少数几个离散动作,即大约两个到五个动作。如我们在 第六章 学习随机过程和 PG 优化 中所学,策略梯度算法可以很容易地适应连续动作。为了展示这些特性,我们将在一组新的环境中部署接下来的几种策略梯度算法,这些环境被称为 Roboschool,目标是控制机器人在不同情境下进行操作。Roboschool 由 OpenAI 开发,使用了我们在前几章中使用的著名的 OpenAI Gym 接口。这些环境基于 Bullet Physics 引擎(一个模拟软体和刚体动力学的物理引擎),与著名的 Mujoco 物理引擎的环境类似。我们选择 Roboschool 是因为它是开源的(而 Mujoco 需要许可证),并且它包含了一些更具挑战性的环境。
具体来说,Roboschool 包含 12 个环境,从简单的 Hopper(RoboschoolHopper,左图)到更复杂的人形机器人(RoboschoolHumanoidFlagrun,右图),后者有 17 个连续动作:

图 7.1. 左侧为 RoboschoolHopper-v1 渲染图,右侧为 RoboschoolHumanoidFlagrun-v1 渲染图
在这些环境中的一些,目标是尽可能快速地奔跑、跳跃或行走,以到达 100 米终点,并且在一个方向上移动。其他环境的目标则是移动在三维场地中,同时需要小心可能的外部因素,如被投掷的物体。该环境集合还包括一个多人 Pong 环境,以及一个互动环境,其中 3D 人形机器人可以自由向各个方向移动,并需要朝着旗帜持续移动。除此之外,还有一个类似的环境,其中机器人被不断投掷立方体以破坏平衡,机器人必须建立更强的控制系统来维持平衡。
环境是完全可观察的,这意味着一个智能体能够完全查看其状态,该状态被编码为一个 Box 类,大小可变,约为 10 到 40。正如我们之前提到的,动作空间是连续的,且它由一个 Box 类表示,大小根据环境不同而有所变化。
控制连续系统
本章将实现的策略梯度算法(如 REINFORCE 和 AC,以及 PPO 和 TRPO)都可以与离散和连续动作空间一起使用。从一种动作类型迁移到另一种非常简单。在连续控制中,不是为每个动作计算一个概率,而是通过概率分布的参数来指定动作。最常见的方法是学习正态高斯分布的参数,这是一个非常重要的分布家族,它由均值! 和标准差! 参数化。下图展示了高斯分布及其参数变化的示例:

图 7.2. 三个不同均值和标准差的高斯分布图
关于本章提到的所有颜色参考,请参阅颜色图像包:www.packtpub.com/sites/default/files/downloads/9781789131116_ColorImages.pdf。
例如,表示为参数化函数近似(如深度神经网络)的策略可以预测状态功能中正态分布的均值和标准差。均值可以近似为线性函数,通常,标准差是独立于状态的。在这种情况下,我们将表示参数化均值作为状态的函数,记作
,标准差作为固定值,记作
。此外,代替直接使用标准差,最好使用标准差的对数值。
总结一下,离散控制的参数化策略可以通过以下代码行定义:
p_logits = mlp(obs_ph, hidden_sizes, act_dim, activation=tf.nn.relu, last_activation=None)
mlp是一个函数,用于构建一个多层感知器(也称为全连接神经网络),隐藏层的大小由hidden_sizes指定,输出为act_dim维度,激活函数由activation和last_activation参数指定。这些将成为连续控制的参数化策略的一部分,并将有以下变化:
p_means = mlp(obs_ph, hidden_sizes, act_dim, activation=tf.tanh, last_activation=None)
log_std = tf.get_variable(name='log_std', initializer=np.zeros(act_dim, dtype=np.float32))
这里,p_means是
,log_std是
。
此外,如果所有的动作值都在 0 和 1 之间,最好将最后的激活函数设置为tanh:
p_means = mlp(obs_ph, hidden_sizes, act_dim, activation=tf.tanh, last_activation=tf.tanh)
然后,为了从这个高斯分布中采样并获得动作,必须将标准差乘以一个噪声向量,该向量遵循均值为 0、标准差为 1 的正态分布,并加到预测的均值上:

这里,z 是高斯噪声向量,
,它的形状与
相同。这个操作可以通过一行代码实现:
p_noisy = p_means + tf.random_normal(tf.shape(p_means), 0, 1) * tf.exp(log_std)
由于我们引入了噪声,我们不能确定值仍然位于动作的范围内,因此我们必须以某种方式裁剪p_noisy,确保动作值保持在允许的最小值和最大值之间。裁剪操作在以下代码行中完成:
act_smp = tf.clip_by_value(p_noisy, envs.action_space.low, envs.action_space.high)
最终,日志概率通过以下方式计算:

该公式在gaussian_log_likelihood函数中计算,该函数返回日志概率。因此,我们可以按以下方式检索日志概率:
p_log = gaussian_log_likelihood(act_ph, p_means, log_std)
这里,gaussian_log_likelihood在以下代码段中定义:
def gaussian_log_likelihood(x, mean, log_std):
log_p = -0.5 * (np.log(2*np.pi) + (x-mean)**2 / (tf.exp(log_std)**2 + 1e-9) + 2*log_std)
return tf.reduce_sum(log_p, axis=-1)
就是这样。现在,你可以在每个 PG 算法中实现它,并尝试各种具有连续动作空间的环境。正如你可能还记得,在上一章,我们在 LunarLander 上实现了 REINFORCE 和 AC。相同的游戏也提供了连续控制版本,叫做LunarLanderContinuous-v2。
拥有解决具有连续动作空间固有问题的必要知识后,你现在能够应对更广泛的任务。然而,一般来说,这些任务也更难解决,我们目前所学的 PG 算法过于弱小,无法很好地解决复杂问题。因此,在接下来的章节中,我们将介绍更先进的 PG 算法,从自然策略梯度开始。
自然策略梯度
REINFORCE 和演员-评论员是非常直观的方法,在中小型 RL 任务中表现良好。然而,它们存在一些问题需要解决,以便我们能调整策略梯度算法,使其适用于更大、更复杂的任务。主要问题如下:
-
很难选择合适的步长:这个问题源于强化学习的非平稳性特性,意味着数据的分布随着时间的推移不断变化,且随着智能体学习新知识,它会探索不同的状态空间。找到一个总体稳定的学习率非常棘手。
-
不稳定性:这些算法没有意识到策略会改变的幅度。这也与我们之前提到的问题相关。一次没有控制的更新可能会导致策略发生重大变化,进而剧烈改变动作分布,从而将智能体推向不良的状态空间。此外,如果新状态空间与之前的状态空间差异很大,可能需要很长时间才能恢复。
-
样本效率低:这个问题几乎所有的在策略算法都会遇到。这里的挑战是,在丢弃策略数据之前,尽可能从中提取更多信息。
本章提出的算法,即 TRPO 和 PPO,尝试通过不同的方式来解决这三个问题,尽管它们有一个共同的背景,稍后将进行解释。此外,TRPO 和 PPO 都是在策略的策略梯度算法,属于无模型家族,如下所示的 RL 分类图:

图 7.3. TRPO 和 PPO 在 RL 算法分类图中的位置
自然策略梯度(NPG)是最早提出的解决策略梯度方法不稳定性问题的算法之一。它通过引入策略步长的变化,控制策略的引导方式,从而解决这个问题。不幸的是,它只适用于线性函数逼近,不能应用于深度神经网络。然而,它是更强大算法的基础,如 TRPO 和 PPO。
NPG 背后的直觉
在寻求解决 PG 方法不稳定性的问题之前,让我们先理解它为什么会出现。想象一下,你正在攀登一座陡峭的火山,火山口位于顶部,类似于下图中的函数。我们还假设你唯一的感官是脚下的倾斜度(梯度),并且你看不见周围的世界——你是盲的。我们还假设每一步的步长是固定的(学习率),例如,步长为一米。你迈出了第一步,感知到脚下的倾斜度,并朝着最陡的上升方向移动 1 米。在多次重复这一过程后,你到达了接近火山口的一个点,但由于你是盲人,依然没有意识到这一点。此时,你观察到脚下的倾斜度依旧指向火山口的方向。然而,如果火山的高度仅比你的步长小,那么下一步你将跌落下来。此时,周围的空间对你来说是完全陌生的。在下图所示的情况下,你会很快恢复过来,因为这是一个简单的函数,但通常情况下,它可能复杂得无法预料。作为补救方法,你可以使用更小的步长,但这样你爬山的速度会变得非常慢,并且仍然无法保证能够到达最大值。这个问题不仅仅存在于强化学习(RL)中,但在这里它尤为严重,因为数据并非静态,可能造成的损害比其他领域(如监督学习)更大。让我们看看下图:

图 7.4. 在尝试到达该函数的最大值时,你可能会掉进火山口。
一个可能想到的解决方案,也是 NPG 中提出的解决方案,是在梯度的基础上加入函数的曲率。关于曲率的信息由二阶导数携带。这个信息非常有用,因为高值表示两个点之间的梯度发生了剧烈变化,作为预防,可以采取更小、更谨慎的步伐,从而避免可能的悬崖。通过这种新方法,你可以利用二阶导数来获得更多关于动作分布空间的信息,并确保在剧烈变化的情况下,动作空间的分布不会发生太大变化。在接下来的部分中,我们将看到 NPG 是如何做到这一点的。
一些数学内容
NPG 算法的创新之处在于如何通过结合一阶和二阶导数的步长更新来更新参数。为了理解自然策略梯度步长,我们需要解释两个关键概念:费舍尔信息矩阵(FIM)和Kullback-Leibler(KL)散度。但在解释这两个关键概念之前,让我们先看一下更新背后的公式:
(7.1)
这个更新与传统的策略梯度有所不同,但仅仅通过项
,它用于增强梯度项。
在这个公式中,
是 FIM,
是目标函数。
正如我们之前提到的,我们希望在分布空间中使得所有步骤的长度相同,无论梯度是什么。这是通过 FIM 的逆来实现的。
FIM 和 KL 散度
FIM 被定义为目标函数的协方差。让我们看看它如何帮助我们。为了限制我们模型的分布之间的距离,我们需要定义一个度量来提供新旧分布之间的距离。最常见的选择是使用 KL 散度。它衡量两个分布之间的差异,并在强化学习(RL)和机器学习中得到广泛应用。KL 散度不是一个真正的度量,因为它不是对称的,但它是一个很好的近似值。两个分布之间的差异越大,KL 散度的值就越高。考虑下图中的曲线。在这个例子中,KL 散度是相对于绿色函数计算的。事实上,由于橙色函数与绿色函数相似,KL 散度为 1.11,接近于 0。相反,很容易看出蓝色和绿色曲线差异较大。这个观察结果得到了它们之间 KL 散度 45.8 的确认。请注意,相同函数之间的 KL 散度始终为 0。
对于有兴趣的读者,离散概率分布的 KL 散度计算公式为
。
让我们来看一下以下图示:

图 7.5. 盒子中显示的 KL 散度是测量每个函数与绿色着色函数之间的差异。数值越大,两者之间的差距越大。
因此,利用 KL 散度,我们能够比较两个分布,并获得它们相互关系的指示。那么,我们如何在问题中使用这个度量,并限制两个后续策略分布之间的散度呢?
事实上,FIM 通过使用 KL 散度作为度量,在分布空间中定义了局部曲率。因此,通过将 KL 散度的曲率(二阶导数)与目标函数的梯度(一阶导数)结合(如公式(7.1)中所示),我们可以获得保持 KL 散度距离恒定的方向和步长。因此,根据公式(7.1)得到的更新将在 FIM 较高时更为谨慎(意味着动作分布之间存在较大距离时),沿着最陡的方向小步前进,并在 FIM 较低时采取大步(意味着存在高原且分布变化不大时)。
自然梯度的复杂性
尽管了解自然梯度在 RL 框架中的有用性,其主要缺点之一是涉及计算 FIM 的计算成本。而梯度的计算成本为
,自然梯度的计算成本为
,其中
是参数数量。事实上,在 2003 年的 NPG 论文中,该算法已应用于具有线性策略的非常小的任务。然而,对于具有数十万参数的现代深度神经网络来说,计算
的成本太高。尽管如此,通过引入一些近似和技巧,自然梯度也可以用于深度神经网络。
在监督学习中,自然梯度的使用并不像在强化学习中那样必要,因为现代优化器(如 Adam 和 RMSProp)可以以经验方式近似处理二阶梯度。
信任区域策略优化
信任区域策略优化(TRPO)是第一个成功利用多种近似方法计算自然梯度的算法,其目标是以更受控且稳定的方式训练深度神经网络策略。从 NPG 中我们看到,对于具有大量参数的非线性函数计算 FIM 的逆是不可能的。TRPO 通过在 NPG 基础上构建,克服了这些困难。它通过引入替代目标函数并进行一系列近似,成功学习复杂策略,例如从原始像素学习步行、跳跃或玩 Atari 游戏。
TRPO 是最复杂的无模型算法之一,虽然我们已经了解了自然梯度的基本原理,但它背后仍然有许多困难的部分。在这一章中,我们只会给出算法的直观细节,并提供主要方程。如果你想深入了解该算法,查阅他们的论文 (arxiv.org/abs/1502.05477),以获得完整的解释和定理证明。
我们还将实现该算法,并将其应用于 Roboschool 环境。然而,我们不会在这里讨论实现的每个组件。有关完整的实现,请查看本书的 GitHub 仓库。
TRPO 算法
从广义的角度来看,TRPO 可以视为 NPG 算法在非线性函数逼近中的延续。TRPO 引入的最大改进是对新旧策略之间的 KL 散度施加约束,形成 信任区域。这使得网络可以在信任区域内采取更大的步伐。由此产生的约束问题表述如下:
(7.2)
这里,
是我们将很快看到的目标代理函数,
是旧策略与
参数之间的 KL 散度,以及新策略之间的 KL 散度。
和
参数是约束的系数。
目标代理函数的设计方式是,利用旧策略的状态分布最大化新的策略参数。这个过程通过重要性采样来完成,重要性采样估计新策略(期望策略)的分布,同时只拥有旧策略(已知分布)的分布。重要性采样是必要的,因为轨迹是根据旧策略采样的,但我们实际关心的是新策略的分布。使用重要性采样,代理目标函数定义为:
(7.3)
是旧策略的优势函数。因此,约束优化问题等价于以下问题:
(7.4)
这里,
表示在状态条件下的动作分布,
。
我们接下来要做的是,用一批样本的经验平均值来替代期望,并用经验估计替代
。
约束问题难以解决,在 TRPO 中,方程(7.4)中的优化问题通过使用目标函数的线性近似和约束的二次近似来近似求解,使得解变得类似于 NPG 更新:

这里,
。
现在,可以使用共轭梯度(CG)方法来求解原优化问题的近似解,这是一种用于求解线性系统的迭代方法。当我们谈到 NPG 时,我们强调计算
对于大参数量而言计算非常昂贵。然而,CG 可以在不形成完整矩阵
的情况下近似求解线性问题。因此,使用 CG 时,我们可以按如下方式计算
:
(7.5)
TRPO 还为我们提供了一种估计步长的方法:
(7.6)
因此,更新变为如下:
(7.7)
到目前为止,我们已经创建了自然策略梯度步骤的一个特例,但要完成 TRPO 更新,我们还缺少一个关键成分。记住,我们通过线性目标函数和二次约束的解来逼近问题。因此,我们只是在求解期望回报的局部近似解。引入这些近似后,我们不能确定 KL 散度约束是否仍然满足。为了在改进非线性目标的同时确保非线性约束,TRPO 执行线搜索以找到满足约束的较高值,
。带有线搜索的 TRPO 更新变为如下:
(7.8)
线搜索可能看起来是算法中微不足道的一部分,但正如论文中所展示的,它起着至关重要的作用。没有它,算法可能会计算出过大的步长,从而导致性能灾难性的下降。
在 TRPO 算法中,它使用共轭梯度算法计算搜索方向,以寻找逼近目标函数和约束的解。然后,它使用线搜索来找到最大步长,
,从而满足 KL 散度的约束并改进目标。为了进一步提高算法的速度,共轭梯度算法还利用了高效的 Fisher-Vector 乘积(想了解更多,可以查看这篇论文:arxiv.org/abs/1502.05477paper)。
TRPO 可以集成到 AC 架构中,其中评论员被包含在算法中,以为策略(演员)在任务学习中提供额外的支持。这样的算法的高级实现(即 TRPO 与评论员结合)用伪代码表示如下:
Initialize with random weight
Initialize environment
for episode 1..M do
Initialize empty buffer
*> Generate few trajectories*
for step 1..TimeHorizon do
*> Collect experience by acting on the environment*
if :
*> Store the episode in the buffer*
# where is the length of the episode
Compute the advantage values and n-step reward to go
> Estimate the gradient of the objective function
(1)
> Compute using conjugate gradient
(2)
> Compute the step length
(3)
*> Update the policy using all the experience in *
Backtracking line search to find the maximum value that satisfy the constraint
(4)
*> Critic update using all the experience in *
在对 TRPO 进行概述之后,我们终于可以开始实现它了。
TRPO 算法的实现
在 TRPO 算法的实现部分,我们将集中精力在计算图和优化策略所需的步骤上。我们将省略在前面章节中讨论的其他方面的实现(例如从环境中收集轨迹的循环、共轭梯度算法和线搜索算法)。但是,请务必查看本书 GitHub 仓库中的完整代码。该实现用于连续控制。
首先,让我们创建所有的占位符以及策略(演员)和价值函数(评论员)的两个深度神经网络:
act_ph = tf.placeholder(shape=(None,act_dim), dtype=tf.float32, name='act')
obs_ph = tf.placeholder(shape=(None, obs_dim[0]), dtype=tf.float32, name='obs')
ret_ph = tf.placeholder(shape=(None,), dtype=tf.float32, name='ret')
adv_ph = tf.placeholder(shape=(None,), dtype=tf.float32, name='adv')
old_p_log_ph = tf.placeholder(shape=(None,), dtype=tf.float32, name='old_p_log')
old_mu_ph = tf.placeholder(shape=(None, act_dim), dtype=tf.float32, name='old_mu')
old_log_std_ph = tf.placeholder(shape=(act_dim), dtype=tf.float32, name='old_log_std')
p_ph = tf.placeholder(shape=(None,), dtype=tf.float32, name='p_ph')
# result of the conjugate gradient algorithm
cg_ph = tf.placeholder(shape=(None,), dtype=tf.float32, name='cg')
# Actor neural network
with tf.variable_scope('actor_nn'):
p_means = mlp(obs_ph, hidden_sizes, act_dim, tf.tanh, last_activation=tf.tanh)
log_std = tf.get_variable(name='log_std', initializer=np.ones(act_dim, dtype=np.float32))
# Critic neural network
with tf.variable_scope('critic_nn'):
s_values = mlp(obs_ph, hidden_sizes, 1, tf.nn.relu, last_activation=None)
s_values = tf.squeeze(s_values)
这里有几点需要注意:
-
带有
old_前缀的占位符指的是旧策略的张量。 -
演员和评论员被定义在两个独立的变量作用域中,因为稍后我们需要分别选择这些参数。
-
动作空间是一个高斯分布,具有对角矩阵的协方差矩阵,并且与状态独立。然后,可以将对角矩阵调整为每个动作一个元素的向量。我们还会使用这个向量的对数。
现在,我们可以根据标准差将正常噪声添加到预测的均值中,对动作进行剪切,并计算高斯对数似然,步骤如下:
p_noisy = p_means + tf.random_normal(tf.shape(p_means), 0, 1) * tf.exp(log_std)
a_sampl = tf.clip_by_value(p_noisy, low_action_space, high_action_space)
p_log = gaussian_log_likelihood(act_ph, p_means, log_std)
然后,我们需要计算目标函数!、评论员的 MSE 损失函数,并为评论员创建优化器,步骤如下:
# TRPO loss function
ratio_new_old = tf.exp(p_log - old_p_log_ph)
p_loss = - tf.reduce_mean(ratio_new_old * adv_ph)
# MSE loss function
v_loss = tf.reduce_mean((ret_ph - s_values)**2)
# Critic optimization
v_opt = tf.train.AdamOptimizer(cr_lr).minimize(v_loss)
接下来的步骤涉及为前面伪代码中给出的(2)、(3)和(4)点创建计算图。实际上,(2)和(3)并不在 TensorFlow 中执行,因此它们不属于计算图的一部分。然而,在计算图中,我们必须处理一些相关的内容。具体步骤如下:
-
估计策略损失函数的梯度。
-
定义一个过程来恢复策略参数。这是必要的,因为在进行线搜索算法时,我们将优化策略并测试约束条件,如果新策略不满足这些条件,我们将必须恢复策略参数并尝试使用更小的
系数。 -
计算费舍尔向量积。这是一种有效计算
而不形成完整的
的方法。 -
计算 TRPO 步骤。
-
更新策略。
从第 1 步开始,也就是估计策略损失函数的梯度:
def variables_in_scope(scope):
return tf.get_collection(tf.GraphKeys.TRAINABLE_VARIABLES, scope)
# Gather and flatten the actor parameters
p_variables = variables_in_scope('actor_nn')
p_var_flatten = flatten_list(p_variables)
# Gradient of the policy loss with respect to the actor parameters
p_grads = tf.gradients(p_loss, p_variables)
p_grads_flatten = flatten_list(p_grads)
由于我们使用的是向量参数,因此必须使用flatten_list将其展平。variable_in_scope返回scope中的可训练变量。此函数用于获取演员的变量,因为梯度计算仅需针对这些变量。
关于步骤 2,策略参数是通过这种方式恢复的:
p_old_variables = tf.placeholder(shape=(None,), dtype=tf.float32, name='p_old_variables')
# variable used as index for restoring the actor's parameters
it_v1 = tf.Variable(0, trainable=False)
restore_params = []
for p_v in p_variables:
upd_rsh = tf.reshape(p_old_variables[it_v1 : it_v1+tf.reduce_prod(p_v.shape)], shape=p_v.shape)
restore_params.append(p_v.assign(upd_rsh))
it_v1 += tf.reduce_prod(p_v.shape)
restore_params = tf.group(*restore_params)
它迭代每一层的变量,并将旧变量的值分配给当前变量。
步骤 3 中的 Fisher-向量积通过计算 KL 散度关于策略变量的二阶导数来完成:
# gaussian KL divergence of the two policies
dkl_diverg = gaussian_DKL(old_mu_ph, old_log_std_ph, p_means, log_std)
# Jacobian of the KL divergence (Needed for the Fisher matrix-vector product)
dkl_diverg_grad = tf.gradients(dkl_diverg, p_variables)
dkl_matrix_product = tf.reduce_sum(flatten_list(dkl_diverg_grad) * p_ph)
# Fisher vector product
Fx = flatten_list(tf.gradients(dkl_matrix_product, p_variables))
步骤 4 和 5 涉及将更新应用到策略中,其中beta_ph是
,该值通过公式(7.6)计算,alpha是通过线性搜索找到的缩放因子:
# NPG update
beta_ph = tf.placeholder(shape=(), dtype=tf.float32, name='beta')
npg_update = beta_ph * cg_ph
alpha = tf.Variable(1., trainable=False)
# TRPO update
trpo_update = alpha * npg_update
# Apply the updates to the policy
it_v = tf.Variable(0, trainable=False)
p_opt = []
for p_v in p_variables:
upd_rsh = tf.reshape(trpo_update[it_v : it_v+tf.reduce_prod(p_v.shape)], shape=p_v.shape)
p_opt.append(p_v.assign_sub(upd_rsh))
it_v += tf.reduce_prod(p_v.shape)
p_opt = tf.group(*p_opt)
注意,在没有
的情况下,更新可以看作是 NPG 更新。
更新应用到策略的每个变量。此工作由p_v.assign_sub(upd_rsh)完成,它将p_v - upd_rsh的值赋给p_v,即:
。减法是因为我们将目标函数转换为损失函数。
现在,让我们简要回顾一下每次迭代更新策略时我们所实现的各个部分是如何协同工作的。我们将在此展示的代码片段应在最内层循环中添加,其中采样了轨迹。但在深入代码之前,让我们回顾一下我们需要做什么:
-
获取输出、对数概率、标准差和我们用于采样轨迹的策略参数。这一策略是我们的旧策略。
-
获取共轭梯度。
-
计算步长,
。 -
执行回溯线性搜索以获得
。 -
运行策略更新。
第一点通过运行一些操作来实现:
...
old_p_log, old_p_means, old_log_std = sess.run([p_log, p_means, log_std], feed_dict={obs_ph:obs_batch, act_ph:act_batch, adv_ph:adv_batch, ret_ph:rtg_batch})
old_actor_params = sess.run(p_var_flatten)
old_p_loss = sess.run([p_loss], feed_dict={obs_ph:obs_batch, act_ph:act_batch, adv_ph:adv_batch, ret_ph:rtg_batch, old_p_log_ph:old_p_log})
共轭梯度算法需要一个输入函数,该函数返回估算的 Fisher 信息矩阵、目标函数的梯度和迭代次数(在 TRPO 中,该值介于 5 到 15 之间):
def H_f(p):
return sess.run(Fx, feed_dict={old_mu_ph:old_p_means, old_log_std_ph:old_log_std, p_ph:p, obs_ph:obs_batch, act_ph:act_batch, adv_ph:adv_batch, ret_ph:rtg_batch})
g_f = sess.run(p_grads_flatten, feed_dict={old_mu_ph:old_p_means,obs_ph:obs_batch, act_ph:act_batch, adv_ph:adv_batch, ret_ph:rtg_batch, old_p_log_ph:old_p_log})
conj_grad = conjugate_gradient(H_f, g_f, iters=conj_iters)
然后我们可以计算步长,
,beta_np,以及最大系数,
。
使用回溯线性搜索算法满足约束条件的best_alpha,并通过将所有值输入到计算图中运行优化:
beta_np = np.sqrt(2*delta / np.sum(conj_grad * H_f(conj_grad)))
def DKL(alpha_v):
sess.run(p_opt, feed_dict={beta_ph:beta_np, alpha:alpha_v, cg_ph:conj_grad, obs_ph:obs_batch, act_ph:act_batch, adv_ph:adv_batch, old_p_log_ph:old_p_log})
a_res = sess.run([dkl_diverg, p_loss], feed_dict={old_mu_ph:old_p_means, old_log_std_ph:old_log_std, obs_ph:obs_batch, act_ph:act_batch, adv_ph:adv_batch, ret_ph:rtg_batch, old_p_log_ph:old_p_log})
sess.run(restore_params, feed_dict={p_old_variables: old_actor_params})
return a_res
best_alpha = backtracking_line_search(DKL, delta, old_p_loss, p=0.8)
sess.run(p_opt, feed_dict={beta_ph:beta_np, alpha:best_alpha, cg_ph:conj_grad, obs_ph:obs_batch, act_ph:act_batch, adv_ph:adv_batch, old_p_log_ph:old_p_log})
...
正如您所见,backtracking_line_search使用一个名为DKL的函数,该函数返回旧策略和新策略之间的 KL 散度,
系数(这是约束值),以及旧策略的损失值。backtracking_line_search的操作是从
开始逐步减小该值,直到满足以下条件:KL 散度小于
,并且新损失函数已减小。
因此,TRPO 特有的超参数如下:
-
delta,(
),旧策略和新策略之间的最大 KL 散度。 -
共轭迭代次数
conj_iters的数量。通常情况下,它是一个介于 5 和 15 之间的数字。
恭喜你走到这一步!那真的很难。
TRPO 的应用
TRPO 的效率和稳定性使我们能够在新的更复杂的环境中进行测试。我们在 Roboschool 上应用了 TRPO。Roboschool 及其 Mujoco 对应物通常用作能够控制具有连续动作的复杂代理的算法的测试平台,例如 TRPO。具体来说,我们在 RoboschoolWalker2d 上测试了 TRPO,代理的任务是尽可能快地学会行走。环境如下图所示。当代理器倒下或者自开始以来已经过去超过 1,000 个时间步长时,环境就会终止。状态以大小为 22 的Box类编码,代理器用范围为
的 6 个浮点值进行控制:

图 7.6. RoboschoolWalker2d 环境的渲染
在 TRPO 中,每个 episode 从环境中收集的步数称为time horizon。这个数字还将决定批次的大小。此外,运行多个代理器并行可以收集更具代表性的环境数据。在这种情况下,批次大小将等于时间跨度乘以代理数量。尽管我们的实现不倾向于并行运行多个代理器,但使用比每个 episode 允许的最大步数更长的时间跨度可以达到相同的目标。例如,知道在 RoboschoolWalker2d 中,代理器最多可以进行 1,000 个时间步长以达到目标,通过使用 6,000 的时间跨度,我们可以确保至少运行六个完整的轨迹。
我们使用报告中的以下表格中列出的超参数来运行 TRPO。其第三列还显示了每个超参数的标准范围:
| 超参数 | 用于 RoboschoolWalker2 | 范围 |
|---|---|---|
| 共轭迭代次数 | 10 | [7-10] |
| Delta (δ) | 0.01 | [0.005-0.03] |
| 批次大小(时间跨度*代理数量) | 6000 | [500-20000] |
TRPO(以及在下一节中我们将看到的 PPO)的进展可以通过具体观察每个游戏中累计的总奖励以及由评论者预测的状态值来进行监控。
我们训练了 600 万步,性能结果如图所示。在 200 万步时,它能够达到一个不错的分数 1300,并且能够流畅行走,速度适中。在训练的第一阶段,我们可以注意到一个过渡期,分数略微下降,可能是由于局部最优解。之后,智能体恢复并改进,直到达到 1250 分:

图 7.7. TRPO 在 RoboschoolWalker2d 上的学习曲线
此外,预测的状态值提供了一个重要的指标,帮助我们研究结果。通常,它比总奖励更稳定,也更容易分析。以下图所示,确实证明了我们的假设,因为它显示了一个总体上更平滑的函数,尽管在 400 万和 450 万步之间有几个峰值:

图 7.8. TRPO 在 RoboschoolWalker2d 上由评论者预测的状态值
从这个图表中,我们也更容易看到,在前三百万步之后,智能体继续学习,尽管学习速度非常慢。
正如你所看到的,TRPO 是一个相当复杂的算法,涉及许多活动部分。尽管如此,它证明了将策略限制在信任区域内,以防止策略过度偏离当前分布的有效性。
但我们能否设计一个更简单、更通用的算法,使用相同的基本方法?
近端策略优化(Proximal Policy Optimization)
Schulman 等人的工作表明这是可能的。实际上,它采用了类似于 TRPO 的思想,同时减少了方法的复杂性。这种方法称为近端策略优化(PPO),其优势在于仅使用一阶优化,而不会降低与 TRPO 相比的可靠性。PPO 也比 TRPO 更通用、样本效率更高,并支持使用小批量进行多次更新。
快速概述
PPO 的核心思想是在目标函数偏离时进行剪切,而不是像 TRPO 那样约束它。这防止了策略进行过大的更新。其主要目标如下:
(7.9)
这里,
的定义如下:
(7.10)
目标所表达的是,如果新旧策略之间的概率比,
,高于或低于一个常数,
,则应取最小值。这可以防止
超出区间
。取
作为参考点,
。
PPO 算法
在 PPO 论文中介绍的实用算法使用了 广义优势估计(GAE)的截断版本,GAE 是在论文 High-Dimensional Continuous Control using Generalized Advantage Estimation 中首次提出的一个概念。GAE 通过以下方式计算优势:
(7.11)
它这样做是为了替代常见的优势估计器:
(7.12)
继续讨论 PPO 算法,在每次迭代中,N 条轨迹来自多个并行演员,并且时间跨度为 T,策略更新 K 次,使用小批量。按照这个趋势,评论员也可以使用小批量进行多次更新。下表包含每个 PPO 超参数和系数的标准值。尽管每个问题都需要特定的超参数,但了解它们的范围(见表格的第三列)仍然是有用的:
| 超参数 | 符号 | 范围 |
|---|---|---|
| 策略学习率 | - | [1e^(-5), 1e^(-3)] |
| 策略迭代次数 | K | [3, 15] |
| 轨迹数量(等同于并行演员数量) | N | [1, 20] |
| 时间跨度 | T | [64, 5120] |
| 小批量大小 | - | [64, 5120] |
| 裁剪系数 | ∈ | 0.1 或 0.2 |
| Delta(用于 GAE) | δ | [0.9, 0.97] |
| Gamma(用于 GAE) | γ | [0.8, 0.995] |
PPO 的实现
现在我们已经掌握了 PPO 的基本要素,可以使用 Python 和 TensorFlow 来实现它。
PPO 的结构和实现与演员-评论员算法非常相似,但只多了一些附加部分,我们将在这里解释所有这些部分。
其中一个附加部分是广义优势估计(7.11),它只需要几行代码,利用已实现的 discounted_rewards 函数来计算(7.12):
def GAE(rews, v, v_last, gamma=0.99, lam=0.95):
vs = np.append(v, v_last)
delta = np.array(rews) + gamma*vs[1:] - vs[:-1]
gae_advantage = discounted_rewards(delta, 0, gamma*lam)
return gae_advantage
GAE 函数在 Buffer 类的 store 方法中使用,当存储一条轨迹时:
class Buffer():
def __init__(self, gamma, lam):
...
def store(self, temp_traj, last_sv):
if len(temp_traj) > 0:
self.ob.extend(temp_traj[:,0])
rtg = discounted_rewards(temp_traj[:,1], last_sv, self.gamma)
self.adv.extend(GAE(temp_traj[:,1], temp_traj[:,3], last_sv, self.gamma, self.lam))
self.rtg.extend(rtg)
self.ac.extend(temp_traj[:,2])
def get_batch(self):
return np.array(self.ob), np.array(self.ac), np.array(self.adv), np.array(self.rtg)
def __len__(self):
...
这里的 ... 代表我们没有报告的代码行。
现在我们可以定义裁剪的替代损失函数(7.9):
def clipped_surrogate_obj(new_p, old_p, adv, eps):
rt = tf.exp(new_p - old_p) # i.e. pi / old_pi
return -tf.reduce_mean(tf.minimum(rt*adv, tf.clip_by_value(rt, 1-eps, 1+eps)*adv))
这很直观,不需要进一步解释。
计算图没有什么新东西,但我们还是快速过一遍:
# Placeholders
act_ph = tf.placeholder(shape=(None,act_dim), dtype=tf.float32, name='act')
obs_ph = tf.placeholder(shape=(None, obs_dim[0]), dtype=tf.float32, name='obs')
ret_ph = tf.placeholder(shape=(None,), dtype=tf.float32, name='ret')
adv_ph = tf.placeholder(shape=(None,), dtype=tf.float32, name='adv')
old_p_log_ph = tf.placeholder(shape=(None,), dtype=tf.float32, name='old_p_log')
# Actor
with tf.variable_scope('actor_nn'):
p_means = mlp(obs_ph, hidden_sizes, act_dim, tf.tanh, last_activation=tf.tanh)
log_std = tf.get_variable(name='log_std', initializer=np.ones(act_dim, dtype=np.float32))
p_noisy = p_means + tf.random_normal(tf.shape(p_means), 0, 1) * tf.exp(log_std)
act_smp = tf.clip_by_value(p_noisy, low_action_space, high_action_space)
# Compute the gaussian log likelihood
p_log = gaussian_log_likelihood(act_ph, p_means, log_std)
# Critic
with tf.variable_scope('critic_nn'):
s_values = tf.squeeze(mlp(obs_ph, hidden_sizes, 1, tf.tanh, last_activation=None))
# PPO loss function
p_loss = clipped_surrogate_obj(p_log, old_p_log_ph, adv_ph, eps)
# MSE loss function
v_loss = tf.reduce_mean((ret_ph - s_values)**2)
# Optimizers
p_opt = tf.train.AdamOptimizer(ac_lr).minimize(p_loss)
v_opt = tf.train.AdamOptimizer(cr_lr).minimize(v_loss)
与环境交互和收集经验的代码与 AC 和 TRPO 相同。然而,在本书 GitHub 仓库中的 PPO 实现中,你可以找到一个简单的实现,使用了多个智能体。
一旦收集到过渡数据
(其中 N 是运行的轨迹数量,T 是每条轨迹的时间跨度),我们就可以更新策略和评价器。在这两种情况下,优化会多次运行,并且在小批量上进行。但在此之前,我们必须在完整的批量上运行p_log,因为裁剪目标需要旧策略的动作对数概率:
...
obs_batch, act_batch, adv_batch, rtg_batch = buffer.get_batch()
old_p_log = sess.run(p_log, feed_dict={obs_ph:obs_batch, act_ph:act_batch, adv_ph:adv_batch, ret_ph:rtg_batch})
old_p_batch = np.array(old_p_log)
lb = len(buffer)
lb = len(buffer)
shuffled_batch = np.arange(lb)
# Policy optimization steps
for _ in range(actor_iter):
# shuffle the batch on every iteration
np.random.shuffle(shuffled_batch)
for idx in range(0,lb, minibatch_size):
minib = shuffled_batch[idx:min(idx+batch_size,lb)]
sess.run(p_opt, feed_dict={obs_ph:obs_batch[minib], act_ph:act_batch[minib], adv_ph:adv_batch[minib], old_p_log_ph:old_p_batch[minib]})
# Value function optimization steps
for _ in range(critic_iter):
# shuffle the batch on every iteration
np.random.shuffle(shuffled_batch)
for idx in range(0,lb, minibatch_size):
minib = shuffled_batch[idx:min(idx+minibatch_size,lb)]
sess.run(v_opt, feed_dict={obs_ph:obs_batch[minib], ret_ph:rtg_batch[minib]})
...
在每次优化迭代时,我们会对批量数据进行打乱,以确保每个小批量与其他批量不同。
这就是 PPO 实现的全部内容,但请记住,在每次迭代的前后,我们还会运行总结信息,稍后我们将使用 TensorBoard 来分析结果并调试算法。再次强调,我们这里不展示代码,因为它总是相同的且较长,但你可以在本书的仓库中查看完整代码。如果你想掌握这些强化学习算法,理解每个图表展示的内容是至关重要的。
PPO 应用
PPO 和 TRPO 是非常相似的算法,我们选择通过在与 TRPO 相同的环境中测试 PPO 来进行比较,即 RoboschoolWalker2d。我们为这两个算法调优时投入了相同的计算资源,以确保比较的公平性。TRPO 的超参数与前一节列出的相同,而 PPO 的超参数则显示在下表中:
| 超参数 | 值 |
|---|---|
| 神经网络 | 64, tanh, 64, tanh |
| 策略学习率 | 3e-4 |
| 执行者迭代次数 | 10 |
| 智能体数量 | 1 |
| 时间跨度 | 5,000 |
| 小批量大小 | 256 |
| 裁剪系数 | 0.2 |
| Delta(用于 GAE) | 0.95 |
| Gamma(用于 GAE) | 0.99 |
以下图示展示了 PPO 和 TRPO 的比较。PPO 需要更多的经验才能起步,但一旦达到这个状态,它会迅速提升,超过 TRPO。在这些特定设置下,PPO 在最终表现上也超过了 TRPO。请记住,进一步调整超参数可能会带来更好的结果,且略有不同:

图 7.9. PPO 和 TRPO 性能比较
一些个人观察:我们发现与 TRPO 相比,PPO 的调优更加困难。其原因之一是 PPO 中超参数的数量较多。此外,演员学习率是最重要的调优系数之一,如果没有正确调节,它会极大地影响最终结果。TRPO 的一个大优点是它没有学习率,并且策略仅依赖于几个易于调节的超参数。而 PPO 的优势则在于其速度更快,且已被证明能在更广泛的环境中有效工作。
总结
在本章中,你学习了如何将策略梯度算法应用于控制具有连续动作的智能体,并使用了一组新的环境,称为 Roboschool。
你还学习并开发了两种高级策略梯度算法:信任域策略优化和近端策略优化。这些算法更好地利用了从环境中采样的数据,并使用技术限制两个后续策略分布之间的差异。具体来说,TRPO(顾名思义)使用二阶导数和基于旧策略与新策略之间 KL 散度的一些约束,围绕目标函数构建了一个信任域。另一方面,PPO 优化的目标函数与 TRPO 相似,但只使用一阶优化方法。PPO 通过在目标函数过大时对其进行裁剪,从而防止策略采取过大的步伐。
PPO 和 TRPO 仍然是基于策略的(与其他策略梯度算法一样),但它们比 AC 和 REINFORCE 更具样本效率。这是因为 TRPO 通过使用二阶导数,实际上从数据中提取了更高阶的信息。而 PPO 的样本效率则来自于其能够在相同的基于策略的数据上执行多次策略更新。
由于其样本效率、鲁棒性和可靠性,TRPO,尤其是 PPO,被广泛应用于许多复杂的环境中,如 Dota(openai.com/blog/openai-five/)。
PPO 和 TRPO,以及 AC 和 REINFORCE,都是随机梯度算法。
在下一章中,我们将探讨两种确定性策略梯度算法。确定性算法是一个有趣的替代方案,因为它们具有一些在我们目前看到的算法中无法复制的有用特性。
问题
-
策略神经网络如何控制连续的智能体?
-
什么是 KL 散度?
-
TRPO 背后的主要思想是什么?
-
KL 散度在 TRPO 中的作用是什么?
-
PPO 的主要优点是什么?
-
PPO 如何实现良好的样本效率?
进一步阅读
-
如果你对 NPG 的原始论文感兴趣,可以阅读自然策略梯度:
papers.nips.cc/paper/2073-a-natural-policy-gradient.pdf。 -
关于介绍广义优势函数的论文,请阅读 高维连续控制与广义优势估计:
arxiv.org/pdf/1506.02438.pdf。 -
如果你对原始的信任域政策优化论文感兴趣,请阅读 信任域政策 优化:
arxiv.org/pdf/1502.05477.pdf。 -
如果你对介绍邻近政策优化算法的原始论文感兴趣,请阅读 邻近政策优化算法:
arxiv.org/pdf/1707.06347.pdf。 -
如果你需要更深入的邻近政策优化解释,请阅读以下博客文章:
openai.com/blog/openai-baselines-ppo/。 -
如果你对 PPO 在 Dota 2 中的应用感兴趣,请查看以下关于 OpenAI 的博客文章:
openai.com/blog/openai-five/。
第八章:DDPG 和 TD3 的应用
在前一章中,我们对所有主要的策略梯度算法进行了全面的概述。由于它们能够处理连续的动作空间,因此被应用于非常复杂和精密的控制系统中。策略梯度方法还可以使用二阶导数,就像 TRPO 中所做的那样,或采用其他策略,以通过防止意外的坏行为来限制策略更新。然而,处理此类算法时的主要问题是它们效率较低,需要大量经验才能有希望掌握任务。这个缺点来源于这些算法的“在策略”(on-policy)特性,这使得每次策略更新时都需要新的经验。在本章中,我们将介绍一种新的“离策略”(off-policy)演员-评论家算法,它在探索环境时使用随机策略,同时学习一个目标确定性策略。由于其学习确定性策略的特点,我们将这些方法称为确定性策略梯度方法。我们将首先展示这些算法是如何工作的,并且还将展示它们与 Q 学习方法的密切关系。然后,我们将介绍两种确定性策略梯度算法:深度确定性策略梯度(DDPG),以及它的一个后续版本,称为双延迟深度确定性策略梯度(TD3)。通过在新环境中实现和应用这些算法,你将感受到它们的能力。
本章将涵盖以下主题:
-
将策略梯度优化与 Q 学习结合
-
深度确定性策略梯度
-
双延迟深度确定性策略梯度(TD3)
将策略梯度优化与 Q 学习结合
在本书中,我们介绍了两种主要的无模型算法:基于策略梯度的算法和基于价值函数的算法。在第一类中,我们看到了 REINFORCE、演员-评论家、PPO 和 TRPO。在第二类中,我们看到了 Q 学习、SARSA 和 DQN。除了这两类算法学习策略的方式(即,策略梯度算法使用随机梯度上升,朝着估计回报的最陡增量方向前进,而基于价值的算法为每个状态-动作对学习一个动作值,然后构建策略)之外,还有一些关键的差异使我们能够偏好某一类算法。这些差异包括算法的“在策略”或“离策略”特性,以及它们处理大动作空间的倾向。我们已经在前几章讨论了在策略和离策略之间的区别,但理解它们非常重要,这样我们才能真正理解本章将介绍的算法。
离策略学习能够利用以前的经验来改进当前的策略,尽管这些经验来自不同的分布。DQN 通过将智能体在其生命周期中所有的记忆存储在回放缓存中,并从缓存中采样小批量数据来更新目标策略,从中获益。与此相对的是在策略学习,它要求经验来自当前的策略。这意味着不能使用旧的经验,每次更新策略时,必须丢弃旧数据。因此,由于离策略学习可以重复使用数据,它所需的环境交互次数较少。对于那些获取新样本既昂贵又非常困难的情况,这一差异尤为重要,选择离策略算法可能是至关重要的。
第二个因素是动作空间的问题。正如我们在第七章《TRPO 和 PPO 实现》中所看到的,策略梯度算法能够处理非常大且连续的动作空间。不幸的是,Q 学习算法并不具备这一能力。为了选择一个动作,它们必须在整个动作空间中进行最大化,当动作空间非常大或连续时,这种方法是不可行的。因此,Q 学习算法可以应用于任意复杂的问题(具有非常大的状态空间),但其动作空间必须受到限制。
总之,之前的算法中没有哪一个总是优于其他算法,选择算法通常依赖于任务的具体需求。然而,它们的优缺点是相互补充的,因此问题就出现了:是否有可能将两种算法的优点结合成一个单一的算法?
确定性策略梯度
设计一个既是离策略又能够在高维动作空间中学习稳定策略的算法是具有挑战性的。DQN 已经解决了在离策略设置中学习稳定深度神经网络策略的问题。使 DQN 适应连续动作的一种方法是对动作空间进行离散化。例如,如果一个动作的值在 0 和 1 之间,则可以将其离散化为 11 个值(0, 0.1, 0.2, .., 0.9, 1.0),并使用 DQN 预测这些值的概率。然而,这种解决方案对于动作数量较多的情况并不可行,因为可能的离散动作数随着智能体自由度的增加而呈指数增长。此外,这种技术不适用于需要更精细控制的任务。因此,我们需要找到一个替代方案。
一个有价值的想法是学习一个确定性的演员-评论家模型。它与 Q 学习有着密切的关系。如果你还记得,在 Q 学习中,为了最大化所有可能动作中的近似 Q 函数,最佳动作被选择:

这个想法是学习一个确定性的
策略,它逼近
。这克服了在每一步计算全局最大化的问题,并且为将其扩展到非常高维度和连续动作的情况打开了可能性。确定性策略梯度(DPG)成功地将这一概念应用于一些简单的问题,例如山地车、摆锤和章鱼臂。DPG 之后,DDPG 扩展了 DPG 的思想,使用深度神经网络作为策略,并采用一些更为细致的设计选择,以使算法更加稳定。进一步的算法,TD3,解决了 DPG 和 DDPG 中常见的高方差和过度估计偏差问题。接下来将解释和发展 DDPG 和 TD3。在我们构建一个分类 RL 算法的图谱时,我们将 DPG、DDPG 和 TD3 放置在策略梯度和 Q 学习算法的交集处,如下图所示。现在,让我们专注于 DPG 的基础以及它是如何工作的:

到目前为止开发的无模型 RL 算法的分类
新的 DPG 算法结合了 Q 学习和策略梯度方法。一个参数化的确定性策略只输出确定性的值。在连续的上下文中,这些值可以是动作的均值。然后,可以通过求解以下方程来更新策略的参数:

是参数化的动作值函数。注意,确定性方法与随机方法的不同之处在于,确定性方法不会向动作中添加额外的噪声。在 PPO 和 TRPO 中,我们是从一个正态分布中采样,具有均值和标准差。而在这里,策略只有一个确定性的均值。回到更新公式(8.1),和往常一样,最大化是通过随机梯度上升来完成的,这将通过小幅更新逐步改进策略。然后,目标函数的梯度可以如下计算:

是遵循
策略的状态分布。这种形式来源于确定性策略梯度定理。它表示,目标函数的梯度是通过链式法则应用于 Q 函数计算的,该 Q 函数是相对于
策略参数来求解的。使用像 TensorFlow 这样的自动微分软件,计算这一梯度非常容易。实际上,梯度是通过从 Q 值开始,沿着策略一直计算梯度,然后只更新后者的参数,如下所示:

DPG 定理的示意图
梯度是从 Q 值开始计算的,但只有策略会被更新。
这是一个更理论性的结果。我们知道,确定性策略不会探索环境,因此它们无法找到好的解决方案。为了使 DPG 成为脱离策略的,我们需要更进一步,定义目标函数的梯度,使得期望符合随机探索策略的分布:

是一种探索策略,也叫做行为策略。该方程给出了脱离策略确定性策略梯度,并给出了相对于确定性策略(
)的梯度估计,同时生成遵循行为策略(
)的轨迹。请注意,实际中,行为策略仅仅是加上噪声的确定性策略。
尽管我们之前已经讨论过确定性演员-评论家的问题,但到目前为止,我们只展示了策略学习是如何进行的。实际上,我们同时学习了由确定性策略(
)表示的演员,以及由 Q 函数(
)表示的评论家。可微分的动作值函数(
)可以通过贝尔曼更新轻松学习,从而最小化贝尔曼误差。
(
),正如 Q-learning 算法中所做的那样。
深度确定性策略梯度
如果你使用上一节中介绍的深度神经网络实现了 DPG 算法,算法将非常不稳定,且无法学到任何东西。我们在将 Q-learning 与深度神经网络结合时遇到了类似的问题。实际上,为了将 DNN 和 Q-learning 结合在 DQN 算法中,我们不得不采用一些其他技巧来稳定学习。DPG 算法也是如此。这些方法是脱离策略的,像 Q-learning 一样,正如我们很快将看到的,能让确定性策略与 DNN 配合使用的某些因素与 DQN 中使用的因素类似。
DDPG(使用深度强化学习的连续控制 由 Lillicrap 等人:arxiv.org/pdf/1509.02971.pdf)是第一个使用深度神经网络的确定性演员-评论家算法,用于同时学习演员和评论家。这个无模型、脱离策略的演员-评论家算法扩展了 DQN 和 DPG,因为它借用了 DQN 的一些见解,如回放缓冲区和目标网络,使得 DPG 能够与深度神经网络一起工作。
DDPG 算法
DDPG 使用了两个关键思想,均借鉴自 DQN,但已适配到演员-评论家的案例中:
-
回放缓冲区:在智能体的生命周期内获取的所有过渡数据都会被存储在回放缓冲区中,也叫经验回放。然后,通过从中采样小批量数据,使用它来训练演员和评论员。
-
目标网络:Q 学习是不稳定的,因为更新的网络也是用来计算目标值的网络。如果你还记得,DQN 通过使用目标网络来缓解这个问题,目标网络每 N 次迭代更新一次(将在线网络的参数复制到目标网络)。在 DDQN 论文中,他们表明,在这种情况下,软目标更新效果更好。通过软更新,目标网络的参数
会在每一步与在线网络的参数
部分更新:
通过
。是的,尽管这可能会减慢学习速度,因为目标网络只部分更新,但它的好处超过了由增加的不稳定性带来的负面影响。使用目标网络的技巧不仅适用于演员,也适用于评论员,因此目标评论员的参数也会在软更新后更新:
。
请注意,从现在开始,我们将
和
称为在线演员和在线评论员的参数,将
和
称为目标演员和目标评论员的参数。
DDPG 从 DQN 继承的一个特点是,能够在每一步环境交互后更新演员和评论员。这源于 DDPG 是一个离策略算法,并且从从回放缓冲区采样的小批量数据中学习。与基于策略的随机策略梯度方法相比,DDPG 不需要等到从环境中收集到足够大的批次数据。
之前,我们看到尽管 DPG 是在学习一个确定性策略,但它是根据一个探索行为策略来执行的。那么,这个探索性策略是如何构建的呢?在 DDPG 中,
策略是通过添加从噪声过程中采样的噪声来构建的(
):

过程将确保环境被充分探索。
总结一下,DDPG 通过不断循环执行以下三个步骤直到收敛:
-
行为策略与环境进行交互,通过将观察和奖励存储在缓冲区中,从环境中收集它们。 -
在每一步中,演员和评论员都会根据从缓冲区采样的迷你批次中的信息进行更新。具体来说,评论员通过最小化在线评论员预测的值(
)与使用目标策略(
)和目标评论员(
)计算得到的目标值之间的均方误差(MSE)损失来更新。相反,演员是按照公式(8.3)进行更新的。 -
目标网络的参数是按照软更新进行更新的。
整个算法的总结见下列伪代码:
---------------------------------------------------------------------------------
DDPG Algorithm
---------------------------------------------------------------------------------
Initialize online networks and
Initialize target networks and with the same weights as the online networks
Initialize empty replay buffer
Initialize environment
for do
> Run an episode
while not d:
> Store the transition in the buffer
> Sample a minibatch
> Calculate the target value for every i in b
(8.4)
> Update the critic
(8.5)
> Update the policy
(8.6)
> Targets update
if :
对算法有了更清晰的理解后,我们现在可以开始实现它了。
DDPG 实现
前面部分给出的伪代码已经提供了该算法的全面视图,但从实现的角度来看,仍有一些值得深入探讨的内容。在这里,我们将展示一些更有趣的特性,这些特性也可能出现在其他算法中。完整代码可在本书的 GitHub 仓库中获取:github.com/PacktPublishing/Reinforcement-Learning-Algorithms-with-Python。
具体来说,我们将重点关注以下几个主要部分:
-
如何构建确定性演员-评论员
-
如何进行软更新
-
如何优化一个损失函数,仅针对某些参数
-
如何计算目标值
我们在一个名为deterministic_actor_critic的函数中定义了一个确定性策略的演员和评论员。这个函数将被调用两次,因为我们需要同时创建在线和目标演员-评论员。代码如下:
def deterministic_actor_critic(x, a, hidden_sizes, act_dim, max_act):
with tf.variable_scope('p_mlp'):
p_means = max_act * mlp(x, hidden_sizes, act_dim, last_activation=tf.tanh)
with tf.variable_scope('q_mlp'):
q_d = mlp(tf.concat([x,p_means], axis=-1), hidden_sizes, 1, last_activation=None)
with tf.variable_scope('q_mlp', reuse=True): # reuse the weights
q_a = mlp(tf.concat([x,a], axis=-1), hidden_sizes, 1, last_activation=None)
return p_means, tf.squeeze(q_d), tf.squeeze(q_a)
在这个函数内部有三件有趣的事情。首先,我们区分了两种输入类型,都是传递给同一个评论员的。第一种是输入一个状态,策略返回一个p_means确定性动作;第二种是输入一个状态和一个任意动作。做出这种区分是因为,一个评论员将用于优化演员,而另一个将用于优化评论员。尽管这两个评论员有不同的输入,但它们是同一个神经网络,意味着它们共享相同的参数。这种不同的用法是通过为两个评论员实例定义相同的变量作用域,并将第二个实例的reuse=True来实现的。这样可以确保这两个定义的参数是相同的,实际上只创建了一个评论员。
第二个观察是,我们在一个名为p_mlp的变量作用域中定义了演员。这是因为,稍后我们只需要提取这些参数,而不是评论员的参数。
第三个观察结果是,由于策略的最终激活层是tanh函数(将值限制在-1 和 1 之间),但我们的演员可能需要超出这个范围的值,我们必须将输出乘以max_act因子(这假设最小值和最大值是相反的,即,如果最大允许值是 3,最小值是-3)。
很好!现在让我们继续查看计算图的其余部分,在这里我们定义了占位符;创建了在线和目标演员,以及在线和目标评论员;定义了损失函数;实现了优化器;并更新了目标网络。
我们将从创建我们需要的占位符开始,用于观察值、动作和目标值:
obs_dim = env.observation_space.shape
act_dim = env.action_space.shape
obs_ph = tf.placeholder(shape=(None, obs_dim[0]), dtype=tf.float32, name='obs')
act_ph = tf.placeholder(shape=(None, act_dim[0]), dtype=tf.float32, name='act')
y_ph = tf.placeholder(shape=(None,), dtype=tf.float32, name='y')
在之前的代码中,y_ph是目标 Q 值的占位符,obs_ph是观察值的占位符,act_ph是动作的占位符。
然后我们在online和target变量作用域内调用之前定义的deterministic_actor_critic函数,以便区分四个神经网络:
with tf.variable_scope('online'):
p_onl, qd_onl, qa_onl = deterministic_actor_critic(obs_ph, act_ph, hidden_sizes, act_dim[0], np.max(env.action_space.high))
with tf.variable_scope('target'):
_, qd_tar, _ = deterministic_actor_critic(obs_ph, act_ph, hidden_sizes, act_dim[0], np.max(env.action_space.high))
评论员的损失是qa_onl在线网络的 Q 值和y_ph目标动作值之间的 MSE 损失:
q_loss = tf.reduce_mean((qa_onl - y_ph)**2)
这将通过 Adam 优化器来最小化:
q_opt = tf.train.AdamOptimizer(cr_lr).minimize(q_loss)
关于演员的损失函数,它是在线 Q 网络的相反符号。在这种情况下,在线 Q 网络的输入是由在线确定性演员选择的动作(如公式(8.6)所示,这在《DDPG 算法》部分的伪代码中定义)。因此,Q 值由qd_onl表示,策略损失函数写作如下:
p_loss = -tf.reduce_mean(qd_onl)
我们取了目标函数的相反符号,因为我们必须将其转换为损失函数,考虑到优化器需要最小化损失函数。
现在,最重要的要记住的是,尽管我们计算了依赖于评论员和演员的p_loss损失函数的梯度,但我们只需要更新演员。实际上,从 DPG 中我们知道!。
这通过将p_loss传递给优化器的minimize方法来完成,该方法指定了需要更新的变量。在这种情况下,我们只需要更新在online/m_mlp变量作用域中定义的在线演员的变量:
p_opt = tf.train.AdamOptimizer(ac_lr).minimize(p_loss, var_list=variables_in_scope('online/p_mlp'))
这样,梯度的计算将从p_loss开始,经过评论员的网络,再到演员的网络。最后,只有演员的参数会被优化。
现在,我们需要定义variable_in_scope(scope)函数,它返回名为scope的作用域中的变量:
def variables_in_scope(scope):
return tf.get_collection(tf.GraphKeys.GLOBAL_VARIABLES, scope)
现在是时候查看目标网络是如何更新的了。我们可以使用variable_in_scope来获取演员和评论员的目标变量和在线变量,并使用 TensorFlow 的assign函数更新目标变量,按照软更新公式进行:

这在以下代码片段中完成:
update_target = [target_var.assign(tau*online_var + (1-tau)*target_var) for target_var, online_var in zip(variables_in_scope('target'), variables_in_scope('online'))]
update_target_op = tf.group(*update_target)
就这样!对于计算图而言,这就是全部。是不是很简单?现在我们可以快速浏览一下主要的循环,其中参数根据有限批次样本的估计梯度进行更新。策略与环境的交互是标准的,唯一的例外是,当前返回的动作是确定性的,因此我们需要添加一定量的噪声,以便充分探索环境。在这里,我们没有提供这部分代码,但你可以在 GitHub 上找到完整的实现。
当积累了足够的经验,并且缓冲区达到了某个阈值时,策略和评估网络的优化就开始了。接下来的步骤是《DDPG 算法》部分中提供的 DDPG 伪代码的摘要。步骤如下:
-
从缓冲区中采样一个小批量
-
计算目标动作值
-
优化评估网络
-
优化演员网络
-
更新目标网络
所有这些操作都在几行代码中完成:
...
mb_obs, mb_rew, mb_act, mb_obs2, mb_done = buffer.sample_minibatch(batch_size)
q_target_mb = sess.run(qd_tar, feed_dict={obs_ph:mb_obs2})
y_r = np.array(mb_rew) + discount*(1-np.array(mb_done))*q_target_mb
_, q_train_loss = sess.run([q_opt, q_loss], feed_dict={obs_ph:mb_obs, y_ph:y_r, act_ph: mb_act})
_, p_train_loss = sess.run([p_opt, p_loss], feed_dict={obs_ph:mb_obs})
sess.run(update_target_op)
...
代码的第一行采样了一个batch_size大小的小批量,第二和第三行通过运行评估器和演员目标网络在mb_obs2(包含下一个状态)上来计算目标动作值,正如公式(8.4)所定义的那样。第四行通过输入包含目标动作值、观测和动作的字典来优化评估器。第五行优化演员网络,最后一行通过运行update_target_op更新目标网络。
将 DDPG 应用于 BipedalWalker-v2
现在,我们将 DDPG 应用于一个名为 BipedalWalker-v2 的连续任务,也就是 Gym 提供的一个使用 Box2D(一个 2D 物理引擎)的环境。以下是该环境的截图。目标是让智能体在崎岖的地形上尽可能快地行走。完成任务直到结束会获得 300+的分数,但每次应用马达会消耗少量能量。智能体移动得越高效,消耗的能量就越少。此外,如果智能体摔倒,会获得-100 的惩罚。状态由 24 个浮动数值组成,表示关节、船体的速度和位置,以及 LiDar 测距仪的测量数据。智能体由四个连续的动作控制,动作的范围是[-1,1]。以下是 BipedalWalker 2D 环境的截图:

BipedalWalker2d 环境截图
我们使用以下表格中的超参数来运行 DDPG。在第一行,列出了运行 DDPG 所需的超参数,而第二行列出了在这个特定案例中使用的相应值。让我们参考以下表格:
| 超参数 | 演员学习率 | 评论家学习率 | DNN 架构 | 缓冲区大小 | 批量大小 | Tau |
|---|---|---|---|---|---|---|
| 值 | 3e-4 | 4e-4 | [64,relu,64,relu] | 200000 | 64 | 0.003 |
在训练过程中,我们在策略预测的动作中加入了额外的噪声。然而,为了衡量算法的表现,我们每 10 个回合在纯确定性策略(没有额外噪声)上进行 10 局游戏。以下图表展示了在时间步长的函数下,10 局游戏的累计奖励的平均值:

DDPG 算法在 BipedalWalker2d-v2 上的表现
从结果来看,我们可以看到性能相当不稳定,在几千步之后的得分波动范围从 250 到不到-100。众所周知,DDPG 本身是不稳定的,而且对超参数非常敏感,但经过更细致的调优,结果可能会更平滑。尽管如此,我们可以看到,在前 300k 步内,性能有所提升,达到了大约 100 的平均得分,峰值可达 300。
此外,BipedalWalker-v2 是一个非常难以解决的环境。事实上,当代理在 100 个连续回合中获得至少 300 分的平均奖励时,才算解决了这个环境。使用 DDPG 时,我们未能达到这些性能,但我们仍然获得了一个较好的策略,使得代理能够运行得相当快。
在我们的实现中,我们使用了一个恒定的探索因子。如果使用更复杂的函数,可能在更少的迭代中达到更高的性能。例如,在 DDPG 的论文中,他们使用了一个奥恩斯坦-乌伦贝克过程。如果你愿意,可以从这个过程开始。
DDPG 是一个美丽的例子,展示了如何将确定性策略与随机策略对立使用。然而,由于它是第一个解决复杂问题的算法,因此仍然有许多调整可以应用于它。本章中提出的下一个算法,进一步推动了 DDPG 的进步。
双延迟深度确定性策略梯度(TD3)
DDPG 被认为是最具样本效率的演员-评论家算法之一,但已被证明在超参数上非常敏感且易碎。后续的研究尝试通过引入新颖的思路,或者将其他算法的技巧应用于 DDPG,来缓解这些问题。最近,一种新的算法作为 DDPG 的替代方案出现:双延迟深度确定性策略梯度,简称 TD3(论文是Addressing Function Approximation Error in Actor-Critic Methods:arxiv.org/pdf/1802.09477.pdf)。我们在这里使用“替代”一词,是因为它实际上是 DDPG 算法的延续,添加了一些新的元素,使得它更稳定,性能也更优。
TD3 专注于一些在其他脱机算法中也常见的问题。这些问题包括价值估计的高估和梯度估计的高方差。针对前者问题,他们采用了类似 DQN 中使用的解决方案;对于后者问题,他们采用了两种新的解决方案。我们首先考虑高估偏差问题。
解决高估偏差
高估偏差意味着由近似 Q 函数预测的动作值高于实际值。在具有离散动作的 Q 学习算法中,这一问题被广泛研究,通常会导致不良的预测,从而影响最终性能。尽管影响较小,但这个问题在 DDPG 中也存在。
如果你还记得,减少动作值高估的 DQN 变体被称为双 DQN,它提出了两个神经网络;一个用于选择动作,另一个用于计算 Q 值。特别地,第二个神经网络的工作是由一个冻结的目标网络完成的。这个想法很合理,但正如 TD3 论文中所解释的,它在演员-评论员方法中并不有效,因为在这些方法中,策略变化太慢。因此,他们提出了一种变体,称为剪切双 Q 学习,它取两个不同评论员的估计值之间的最小值(
)。因此,目标值的计算如下:

另一方面,这并不会阻止低估偏差,但它远比高估偏差危害小。剪切双 Q 学习可以在任何演员-评论员方法中使用,并且它遵循这样一个假设:两个评论员会有不同的偏差。
TD3 的实现
为了将此策略转化为代码,我们需要创建两个具有不同初始化的评论员,计算目标动作值,如(8.7)中所示,并优化这两个评论员。
TD3 应用于我们在前一节中讨论的 DDPG 实现。以下代码片段仅是实现 TD3 所需的额外代码的一部分。完整实现可在本书的 GitHub 仓库中找到:github.com/PacktPublishing/Hands-On-Reinforcement-Learning-Algorithms-with-Python。
关于双评论员,你只需通过调用deterministic_actor_double_critic两次来创建它们,一次用于目标网络,一次用于在线网络,正如在 DDPG 中所做的那样。代码大致如下:
def deterministic_actor_double_critic(x, a, hidden_sizes, act_dim, max_act):
with tf.variable_scope('p_mlp'):
p_means = max_act * mlp(x, hidden_sizes, act_dim, last_activation=tf.tanh)
# First critic
with tf.variable_scope('q1_mlp'):
q1_d = mlp(tf.concat([x,p_means], axis=-1), hidden_sizes, 1, last_activation=None)
with tf.variable_scope('q1_mlp', reuse=True): # Use the weights of the mlp just defined
q1_a = mlp(tf.concat([x,a], axis=-1), hidden_sizes, 1, last_activation=None)
# Second critic
with tf.variable_scope('q2_mlp'):
q2_d = mlp(tf.concat([x,p_means], axis=-1), hidden_sizes, 1, last_activation=None)
with tf.variable_scope('q2_mlp', reuse=True):
q2_a = mlp(tf.concat([x,a], axis=-1), hidden_sizes, 1, last_activation=None)
return p_means, tf.squeeze(q1_d), tf.squeeze(q1_a), tf.squeeze(q2_d), tf.squeeze(q2_a)
剪切目标值(
(8.7))是通过首先运行我们称之为qa1_tar和qa2_tar的两个目标评论员,然后计算估计值之间的最小值,最后使用它来估算目标值:
...
double_actions = sess.run(p_tar, feed_dict={obs_ph:mb_obs2})
q1_target_mb, q2_target_mb = sess.run([qa1_tar,qa2_tar], feed_dict={obs_ph:mb_obs2, act_ph:double_actions})
q_target_mb = np.min([q1_target_mb, q2_target_mb], axis=0)
y_r = np.array(mb_rew) + discount*(1-np.array(mb_done))*q_target_mb
..
接下来,评论员可以像往常一样进行优化:
...
q1_train_loss, q2_train_loss = sess.run([q1_opt, q2_opt], feed_dict={obs_ph:mb_obs, y_ph:y_r, act_ph: mb_act})
...
一个重要的观察是,策略是相对于一个近似的 Q 函数进行优化的,在我们的例子中是
。事实上,如果你查看完整代码,你会发现 p_loss 被定义为 p_loss = -tf.reduce_mean(qd1_onl)。
解决方差减少问题
TD3 的第二个也是最后一个贡献是方差的减少。为什么高方差是个问题呢?因为它会产生噪声梯度,导致错误的策略更新,从而影响算法的性能。高方差的复杂性体现在 TD 误差中,它通过后续状态估计动作值。
为了缓解这个问题,TD3 引入了延迟的策略更新和目标正则化技术。让我们看看它们是什么,为什么它们如此有效。
延迟的策略更新
由于高方差归因于不准确的评论,TD3 提议将策略更新延迟,直到评论误差足够小为止。TD3 以经验方式延迟更新策略,仅在固定的迭代次数之后才更新策略。通过这种方式,评论有时间学习并稳定自身,然后再进行策略优化。实际上,策略仅在几个迭代中保持固定,通常是 1 到 6 次。如果设置为 1,则与 DDPG 中的情况相同。延迟的策略更新可以通过以下方式实现:
...
q1_train_loss, q2_train_loss = sess.run([q1_opt, q2_opt], feed_dict={obs_ph:mb_obs, y_ph:y_r, act_ph: mb_act})
if step_count % policy_update_freq == 0:
sess.run(p_opt, feed_dict={obs_ph:mb_obs})
sess.run(update_target_op)
...
目标正则化
从确定性动作更新的评论往往会在狭窄的峰值中产生过拟合。其后果是方差增加。TD3 提出了一个平滑正则化技术,它在目标动作附近的小区域添加了一个剪切噪声:

该正则化可以通过一个函数实现,该函数接受一个向量和一个比例作为参数:
def add_normal_noise(x, noise_scale):
return x + np.clip(np.random.normal(loc=0.0, scale=noise_scale, size=x.shape), -0.5, 0.5)
然后,在运行目标策略后,调用 add_normal_noise,如下代码所示(与 DDPG 实现的不同之处已加粗):
...
double_actions = sess.run(p_tar, feed_dict={obs_ph:mb_obs2})
double_noisy_actions = np.clip(add_normal_noise(double_actions, target_noise), env.action_space.low, env.action_space.high)
q1_target_mb, q2_target_mb = sess.run([qa1_tar,qa2_tar], feed_dict={obs_ph:mb_obs2, act_ph:double_noisy_actions})
q_target_mb = np.min([q1_target_mb, q2_target_mb], axis=0)
y_r = np.array(mb_rew) + discount*(1-np.array(mb_done))*q_target_mb
..
我们在添加了额外噪声后,剪切了动作,以确保它们不会超出环境设定的范围。
将所有内容结合起来,我们得到了以下伪代码所示的算法:
---------------------------------------------------------------------------------
TD 3 Algorithm
---------------------------------------------------------------------------------
Initialize online networks and
Initialize target networks and with the same weights as the online networks
Initialize empty replay buffer
Initialize environment
for do
> Run an episode
while not d:
> Store the transition in the buffer
> Sample a minibatch
> Calculate the target value for every i in b
> Update the critics
if iter % policy_update_frequency == 0:
> Update the policy
> Targets update
if :
这就是 TD3 算法的全部内容。现在,你对所有确定性和非确定性策略梯度方法有了清晰的理解。几乎所有的无模型算法都基于我们在这些章节中解释的原则,如果你掌握了它们,你将能够理解并实现所有这些算法。
将 TD3 应用到 BipedalWalker
为了直接比较 TD3 和 DDPG,我们在与 DDPG 相同的环境中测试了 TD3:BipedalWalker-v2。
针对这个环境,TD3 的最佳超参数列在下面的表格中:
| 超参数 | Actor 学习率 | Critic 学习率 | DNN 架构 | 缓冲区大小 | 批次大小 | Tau |
|---|
策略更新频率
|
Sigma
|
| 值 | 4e-4 | 4e-4 | [64,relu,64,relu] | 200000 | 64 | 0.005 | 2 | 0.2 |
|---|
结果绘制在下图中。曲线呈平滑趋势,在大约 30 万步之后达到了良好的结果,在训练的 45 万步时达到了顶峰。它非常接近 300 分的目标,但实际上并没有达到:

TD3 算法在 BipedalWalker-v2 上的表现
相比于 DDPG,找到 TD3 的超参数所花费的时间较少。而且,尽管我们只是在一个游戏上比较这两种算法,我们认为这是一个很好的初步观察,帮助我们理解它们在稳定性和性能上的差异。DDPG 和 TD3 在 BipedalWalker-v2 上的表现如下:

DDPG 与 TD3 性能比较
如果你想在更具挑战性的环境中训练算法,可以尝试 BipedalWalkerHardcore-v2。它与 BipedalWalker-v2 非常相似,唯一不同的是它有梯子、树桩和陷阱。很少有算法能够完成并解决这个环境。看到智能体无法通过这些障碍也非常有趣!
相比于 DDPG,TD3 的优越性非常明显,无论是在最终性能、改进速度还是算法稳定性上。
本章中提到的所有颜色参考,请参考此链接中的颜色图像包:www.packtpub.com/sites/default/files/downloads/9781789131116_ColorImages.pdf。
总结
在本章中,我们介绍了解决强化学习问题的两种不同方法。第一种是通过估计状态-动作值来选择最佳的下一步动作,这就是所谓的 Q-learning 算法。第二种方法是通过梯度最大化期望奖励策略。事实上,这些方法被称为策略梯度方法。本章中,我们展示了这些方法的优缺点,并证明了它们在许多方面是互补的。例如,Q-learning 算法在样本效率上表现优秀,但无法处理连续动作。相反,策略梯度算法需要更多数据,但能够控制具有连续动作的智能体。接着,我们介绍了结合 Q-learning 和策略梯度技术的 DPG 方法。特别地,这些方法通过预测一个确定性策略,克服了 Q-learning 算法中的全局最大化问题。我们还展示了如何通过 Q 函数的梯度定义 DPG 定理中的确定性策略更新。
我们学习并实现了两种 DPG 算法:DDPG 和 TD3。这两种算法都是脱离策略的演员-评论家算法,可以用于具有连续动作空间的环境。TD3 是 DDPG 的升级版,封装了一些减少方差的技巧,并限制了在 Q-learning 算法中常见的过度估计偏差。
本章总结了无模型强化学习算法的概述。我们回顾了迄今为止已知的所有最佳和最具影响力的算法,从 SARSA 到 DQN,再到 REINFORCE 和 PPO,并将它们结合在如 DDPG 和 TD3 等算法中。这些算法本身在适当的微调和大量数据的支持下,能够实现惊人的成果(参见 OpenAI Five 和 AlphaStar)。然而,这并不是强化学习的全部内容。在下一章中,我们将不再讨论无模型算法,而是展示一种基于模型的算法,其目的是通过学习环境模型来减少学习任务所需的数据量。在随后的章节中,我们还将展示更先进的技术,如模仿学习、新的有用强化学习算法,如 ESBAS,以及非强化学习算法,如进化策略。
问题
-
Q-learning 算法的主要限制是什么?
-
为什么随机梯度算法样本效率低?
-
DPG 是如何克服最大化问题的?
-
DPG 是如何保证足够的探索的?
-
DDPG 代表什么?它的主要贡献是什么?
-
TD3 提出了哪些问题来最小化?
-
TD3 使用了哪些新机制?
深入阅读
你可以通过以下链接了解更多:
-
如果你对介绍确定性策略梯度(DPG)算法的论文感兴趣,可以阅读:
proceedings.mlr.press/v32/silver14.pdf。 -
如果你对介绍深度确定性策略梯度(DDPG)算法的论文感兴趣,可以阅读:
arxiv.org/pdf/1509.02971.pdf。 -
介绍双延迟深度确定性策略梯度(TD3)算法的论文可以在这里找到:
arxiv.org/pdf/1802.09477.pdf。 -
想了解所有主要策略梯度算法的简要概述,可以查看 Lilian Weng 撰写的这篇文章:
lilianweng.github.io/lil-log/2018/04/08/policy-gradient-algorithms.html。
第三部分:超越无模型算法与改进
在本节中,你将实现基于模型的算法、模仿学习、进化策略,并了解一些可能进一步改进强化学习(RL)算法的思想。
本节包含以下章节:
-
第九章,基于模型的 RL
-
第十章,使用 DAgger 算法的模仿学习
-
第十一章,理解黑箱优化算法
-
第十二章,开发 ESBAS 算法
-
第十三章,解决 RL 挑战的实际实现
第九章:基于模型的强化学习
强化学习算法分为两类——无模型方法和基于模型的方法。这两类的区别在于对环境模型的假设。无模型算法仅通过与环境的互动学习策略,而对环境一无所知;而基于模型的算法已经对环境有深入的理解,并利用这些知识根据模型的动态来采取下一步行动。
在本章中,我们将为你提供一个全面的基于模型方法的概述,突出其与无模型方法相比的优缺点,以及当模型已知或需要学习时产生的差异。后者的划分很重要,因为它影响了问题的处理方式和用于解决问题的工具。在这段介绍之后,我们将讨论一些更为复杂的案例,其中基于模型的算法必须处理高维度的观察空间,比如图像。
此外,我们还将讨论一种结合了基于模型和无模型方法的算法类,用于在高维空间中学习模型和策略。我们将深入了解其内部工作原理,并解释为何使用这种方法。然后,为了加深我们对基于模型的算法,特别是结合基于模型和无模型方法的算法的理解,我们将开发一种最先进的算法,叫做模型集成信任区域策略优化(ME-TRPO),并将其应用于连续倒立摆问题。
本章将涵盖以下主题:
-
基于模型的方法
-
将基于模型与无模型学习结合
-
将 ME-TRPO 应用到倒立摆问题
基于模型的方法
无模型算法是一种强大的算法类型,能够学习非常复杂的策略并在复杂和多样化的环境中完成目标。正如 OpenAI 的最新工作所展示的(openai.com/five/)和 DeepMind 的工作(deepmind.com/blog/article/alphastar-mastering-real-time-strategy-game-starcraft-ii),这些算法实际上能够在《星际争霸》和《Dota 2》等挑战性游戏中展示长期规划、团队合作以及对意外情况的适应能力。
训练过的智能体已经能够击败顶级职业玩家。然而,最大的问题在于需要进行大量游戏才能训练智能体掌握这些游戏。实际上,为了取得这些结果,算法已经被大规模扩展,允许智能体与自己对战,进行数百年的游戏。但,这种方法到底有什么问题呢?
好吧,直到你为一个模拟器训练一个智能体,你可以收集你想要的任何经验。当你在一个像你生活的世界一样缓慢而复杂的环境中运行智能体时,问题就出现了。在这种情况下,你不能等上几百年才看到一些有趣的能力。那么,我们能否开发出一种与现实环境互动较少的算法?可以。正如你可能还记得的,我们已经在无模型算法中探讨过这个问题。
解决方案是使用脱离策略的算法。然而,收效甚微,对于许多现实世界问题来说,其增益并不显著。
正如你可能预料的那样,答案(或至少一个可能的答案)就在基于模型的强化学习算法中。你已经开发了一种基于模型的算法。你还记得是哪一种吗?在第三章中,使用动态规划解决问题,我们将环境模型与动态规划结合起来,训练智能体在有陷阱的地图上导航。由于动态规划使用了环境模型,因此它被视为一种基于模型的算法。
不幸的是,动态规划(DP)无法应用于中等或复杂的问题。所以,我们需要探索其他类型的基于模型的算法,这些算法能够扩展并在更具挑战性的环境中发挥作用。
基于模型的学习的广泛视角
让我们首先回顾一下什么是模型。模型由环境的转移动态和奖励组成。转移动态是一个从状态 s 和动作 a 映射到下一个状态 s' 的过程。
有了这些信息,环境就可以通过模型完全表示,并且可以用模型代替环境。如果智能体能访问该模型,那么它就有能力预测自己的未来。
在接下来的章节中,我们将看到模型可以是已知的或未知的。在已知模型的情况下,模型直接用来利用环境的动态;也就是说,模型提供了一个表示,用来代替环境。在环境模型未知的情况下,模型可以通过直接与环境互动来学习。但由于在大多数情况下,我们只学到环境的一个近似模型,因此在使用时必须考虑额外的因素。
现在我们已经解释了什么是模型,我们可以看看如何使用模型,以及它如何帮助我们减少与环境的互动次数。模型的使用方式取决于两个非常重要的因素——模型本身以及选择动作的方式。
确实,正如我们刚才提到的,模型可以是已知的或未知的,行动可以通过一个学习到的策略来规划或选择。算法会根据具体情况有所不同,因此让我们首先详细说明在模型已知的情况下使用的方式(即我们已经拥有环境的转移动态和奖励)。
一个已知的模型
当模型已知时,可以用它来模拟完整的轨迹,并计算每条轨迹的回报。然后,选择那些能够带来最高回报的动作。这个过程被称为规划,而环境模型是不可或缺的,因为它提供了生成下一个状态(给定一个状态和一个动作)和回报所需的信息。
规划算法在各个领域都有应用,但我们关注的算法与它们操作的动作空间类型不同。有些算法处理离散动作,其他则处理连续动作。
针对离散动作的规划算法通常是搜索算法,它们构建决策树,例如下面图示的那种:

当前状态是根节点,可能的动作由箭头表示,其他节点是通过一系列动作达到的状态。
你可以看到,通过尝试每一个可能的动作序列,最终会找到最优的那个。不幸的是,在大多数问题中,这个过程是不可行的,因为可能的动作数量呈指数级增长。复杂问题中使用的规划算法采用一些策略,通过依赖有限数量的轨迹来实现规划。
其中一个算法,也被 AlphaGo 采用,叫做蒙特卡洛树搜索(MCTS)。MCTS 通过生成一系列有限的模拟游戏,迭代构建决策树,同时充分探索那些尚未访问的树枝。一旦一个模拟游戏或轨迹达到叶节点(即游戏结束),它会将结果反向传播到访问过的状态,并更新节点所持有的胜/负或回报信息。然后,选择能够带来更高胜/负比或回报的动作。
相对的,处理连续动作的规划算法涉及轨迹优化技术。这些算法比其离散动作的对手更难解决,因为它们需要处理一个无限维的优化问题。
此外,许多算法需要模型的梯度。例如,模型预测控制(MPC)会对有限时间范围进行优化,但它并不执行找到的完整轨迹,而只执行第一步动作。通过这样做,MPC 与其他具有无限时间范围规划的方法相比,响应速度更快。
未知模型
当环境的模型未知时应该怎么办?学习它!到目前为止,我们所见的一切几乎都涉及学习。那么,这是否是最佳方法呢?嗯,如果你确实想使用基于模型的方法,答案是肯定的,稍后我们将看到如何做到这一点。然而,这并不总是最佳的做法。
在强化学习中,最终目标是为给定任务学习一个最优策略。在本章之前,我们提到过基于模型的方法主要用于减少与环境的互动次数,但这总是成立吗?假设你的目标是做一个煎蛋卷。知道鸡蛋的确切断裂点完全没有用;你只需要大致知道如何打破它。因此,在这种情况下,不涉及鸡蛋结构的无模型算法更为合适。
然而,这不应该让你认为基于模型的算法不值得使用。例如,当模型比策略更容易学习时,基于模型的方法在某些情况下优于无模型的方法。
学习一个模型的唯一方式是(不幸的是)通过与环境的互动。这是一个必经步骤,因为它让我们能够获取并创建关于环境的数据集。通常,学习过程是以监督方式进行的,其中一个函数逼近器(如深度神经网络)被训练以最小化损失函数,例如环境获得的转移和预测之间的均方误差损失。以下图示展示了这一过程,其中一个深度神经网络被训练来通过预测下一个状态s'和奖励r,从状态s和动作a来建模环境:

除了神经网络,还有其他选择,如高斯过程和高斯混合模型。特别是,高斯过程的特点是能够考虑到模型的不确定性,并且被认为具有很高的数据效率。事实上,在深度神经网络出现之前,它们是最受欢迎的选择。
然而,高斯过程的主要缺点是它们在处理大型数据集时比较慢。实际上,要学习更复杂的环境(从而需要更大的数据集),更倾向使用深度神经网络。此外,深度神经网络能够学习那些将图像作为观测的环境模型。
有两种主要的学习环境模型的方法;一种是模型一旦学习完毕就固定不变,另一种是在开始时学习模型,但一旦计划或策略发生变化,就重新训练模型。以下图示展示了这两种选择:

在图示的上半部分,展示了一种顺序的基于模型的算法,其中智能体仅在学习模型之前与环境进行互动。在下半部分,展示了一种基于模型的学习的循环方法,其中模型通过来自不同策略的额外数据进行改进。
为了理解算法如何从第二种选择中受益,我们必须定义一个关键概念。为了收集用于学习环境动态的数据集,你需要一个能够让你导航的策略。但在开始时,该策略可能是确定性的或完全随机的。因此,在有限的交互次数下,所探索的空间将非常有限。
这使得模型无法学习到那些用于规划或学习最优轨迹的环境部分。但是,如果模型通过来自更新和更好的策略的新交互进行再训练,它将逐步适应新策略,并捕捉到所有尚未访问的环境部分(从策略角度来看)。这就是数据聚合。
在实践中,在大多数情况下,模型是未知的,并通过数据聚合方法来适应新产生的策略。然而,学习模型可能是具有挑战性的,潜在的问题如下:
-
模型过拟合:学到的模型在环境的局部区域上过拟合,忽略了它的全局结构。
-
不准确的模型:在一个不完美的模型上进行规划或学习策略可能会引发一连串的错误,导致潜在的灾难性结论。
优秀的基于模型的算法,能够学习模型,必须处理这些问题。一个潜在的解决方案是使用能够估算不确定性的算法,如贝叶斯神经网络,或通过使用模型集成。
优势和劣势
在开发强化学习算法(各种 RL 算法)时,有三个基本方面需要考虑:
-
渐近性能:这是指如果算法拥有无限的时间和硬件资源时,它可以达到的最大性能。
-
实际时间:这是指算法在给定计算能力下,达到特定性能所需的学习时间。
-
样本效率:这是指与环境交互的次数,以达到给定的性能。
我们已经探讨了在无模型和基于模型的强化学习中样本效率的差异,且发现后者的样本效率要高得多。那么,实际时间和性能呢?其实,基于模型的算法通常具有较低的渐近性能,且训练速度较慢,相比之下,无模型算法的训练速度较快。通常,较高的数据效率往往会以牺牲性能和速度为代价。
基于模型学习性能较低的一个原因可以归因于模型的不准确性(如果是通过学习得到的),这种不准确性会为策略引入额外的误差。较长的学习时钟时间是由于规划算法的缓慢,或者是由于在不准确的学习环境中需要更多的交互才能学习到策略。此外,基于模型的规划算法由于规划的高计算成本,推理时间较慢,仍然需要在每一步进行规划。
总结来说,您必须考虑训练基于模型的算法所需的额外时间,并认识到这些方法的渐近性能较低。然而,当模型比策略本身更容易学习,并且与环境的交互代价较高或较慢时,基于模型的学习是极其有用的。
从两个方面来看,我们有无模型学习和基于模型的学习,它们各自有引人注目的特点,但也有明显的缺点。我们能否从两者中各取所长?
将基于模型的学习与无模型学习结合
我们刚刚看到,规划在训练和运行时都可能计算开销较大,并且在更复杂的环境中,规划算法无法实现良好的性能。我们简要提到的另一种策略是学习策略。策略在推理时无疑要快得多,因为它不需要在每一步进行规划。
一种简单而有效的学习策略的方法是将基于模型的学习与无模型学习相结合。随着无模型算法的最新创新,这种结合方法越来越流行,成为迄今为止最常见的方法。我们将在下一节开发的算法——ME-TRPO,就是这种方法之一。让我们深入探讨这些算法。
一种有用的结合方式
如您所知,无模型学习具有良好的渐近性能,但样本复杂度较高。另一方面,基于模型的学习从数据的角度来看是高效的,但在处理更复杂任务时存在困难。通过结合基于模型和无模型的方法,有可能找到一个平衡点,在保持无模型算法高性能的同时,持续降低样本复杂度。
有很多方法可以将这两个领域结合起来,提出这样的方法的算法之间差异很大。例如,当模型已经给定(如围棋和国际象棋中的模型),搜索树和基于价值的算法可以相互帮助,从而更好地估算行动价值。
另一个例子是将环境和策略的学习直接结合到深度神经网络架构中,以便学习到的动态能够为策略的规划提供帮助。许多算法使用的另一种策略是使用学习到的环境模型生成额外的样本,以优化策略。
换句话说,策略是通过在学习到的模型中进行模拟游戏来训练的。这可以通过多种方式实现,但主要的步骤如下所示:
while not done:
> collect transitions from the real environment using a policy
> add the transitions to the buffer
> learn a model that minimizes in a supervised way using data in
> (optionally learn )
repeat K times:
> sample an initial state
> simulate transitions from the model using a policy
> update the policy using a model-free RL
这个蓝图涉及两个循环。最外层的循环收集来自真实环境的数据用于训练模型,而最内层的循环中,模型生成的模拟样本用于使用无模型算法优化策略。通常,动态模型是通过监督学习方式训练,以最小化均方误差(MSE)损失。模型的预测越精确,策略就越准确。
在最内层的循环中,可以模拟完整的轨迹或固定长度的轨迹。实际上,为了减轻模型的不完美,后者选项可以被采用。此外,轨迹可以从包含真实转换的缓冲区中随机抽取初始状态,或从初始状态开始。前者在模型不准确时更为偏好,因为这可以防止轨迹与真实轨迹的偏差过大。为了说明这种情况,考虑以下图示。真实环境中收集到的轨迹为黑色,而模拟的轨迹为蓝色:

你可以看到,从初始状态开始的轨迹变得更长,因此,随着不准确模型的误差在随后的预测中传播,它们会更快地发散。
注意,你只进行主循环的一次迭代,并收集所有学习到的环境模型所需的数据也是可以的。然而,基于之前提到的原因,使用迭代数据聚合方法通过新的策略周期性地重新训练模型会更好。
从图像构建模型
到目前为止,结合基于模型和无模型学习的方法,特别设计用于处理低维状态空间。那么,如何处理高维观测空间(如图像)呢?
一种选择是学习潜在空间。潜在空间是高维输入(例如图像)的一种低维表示,也叫做嵌入,g(s)。它可以通过神经网络如自编码器生成。以下图示展示了自编码器的一个例子:

它包括一个编码器,将图像映射到一个小的潜在空间,g(s),以及解码器,将潜在空间映射回重建的图像。通过自编码器的作用,潜在空间应该在一个受限空间内表示图像的主要特征,使得两个相似的图像在潜在空间中也相似。
在强化学习中,自编码器可以被训练来重建输入,S,或者训练来预测下一个帧的观测值,S',(如果需要的话,还包括奖励)。然后,我们可以利用潜在空间来学习动态模型和策略。这个方法的主要好处是由于图像的表示更小,从而大大提高了速度。然而,当自编码器无法恢复正确的表示时,潜在空间中学到的策略可能会出现严重的缺陷。
高维空间的基于模型的学习仍然是一个非常活跃的研究领域。
如果你对从图像观测中学习的基于模型的算法感兴趣,Kaiser 的论文《基于模型的 Atari 强化学习》可能会引起你的兴趣(arxiv.org/pdf/1903.00374.pdf)。
到目前为止,我们已经从更具象征性和理论化的角度讨论了基于模型的学习及其与无模型学习的结合。虽然这些对理解这些范式是不可或缺的,但我们希望将其付诸实践。因此,事不宜迟,让我们专注于第一个基于模型的算法的细节和实现。
ME-TRPO 应用于倒立摆
许多变种的基于模型和无模型的算法存在于有用的组合部分的伪代码中。几乎所有这些变种都提出了不同的方式来处理环境模型的不足之处。
这是一个关键问题,需要解决以达到与无模型方法相同的性能。从复杂环境中学到的模型总是会有一些不准确性。因此,主要的挑战是估计或控制模型的不确定性,以稳定和加速学习过程。
ME-TRPO 提出了使用一个模型集来保持模型不确定性并正则化学习过程。这些模型是具有不同权重初始化和训练数据的深度神经网络。它们共同提供了一个更加稳健的环境通用模型,能够避免在数据不足的区域产生过拟合。
然后,从使用这些模型集模拟的轨迹中学习策略。特别地,选择用来学习策略的算法是信任域策略优化(TRPO),该算法在第七章中有详细解释,标题为TRPO 和 PPO 实现。
理解 ME-TRPO
在 ME-TRPO 的第一部分,环境的动态(即模型集)被学习。算法首先通过与环境的随机策略互动,
,来收集转移数据集,
。然后,这个数据集被用来以监督方式训练所有动态模型,
。这些模型,
,是通过不同的随机权重初始化并使用不同的小批量进行训练的。为了避免过拟合问题,从数据集中创建了一个验证集。此外,当验证集上的损失不再改善时,一种早停机制(在机器学习中广泛使用的正则化技术)会中断训练过程。
在算法的第二部分,策略是通过 TRPO 进行学习的。具体而言,策略是在从已学习模型中收集的数据上进行训练,我们也称之为模拟环境,而不是实际环境。为了避免策略利用单个学习模型的不准确区域,策略,
,是通过整个模型集的预测转移来训练的,
。特别地,策略是在由模型集中的随机选择的转移组成的模拟数据集上进行训练的,
。在训练过程中,策略会不断被监控,一旦性能停止提升,训练过程就会停止。
最后,由这两个部分构成的循环会一直重复,直到收敛。然而,在每次新迭代时,都会通过运行新学习到的策略,
,来收集来自实际环境的数据,并将收集到的数据与前几次迭代的数据集进行汇总。ME-TRPO 算法的简要伪代码总结如下:
Initialize randomly policy and models
Initialize empty buffer
while not done:
> populate buffer with transitions from the real environment using policy (or random)
> learn models that minimize in a supervised way using data in
until convergence:
> sample an initial state
> simulate transitions using models and the policy
> take a TRPO update to optimize policy
这里需要特别注意的是,与大多数基于模型的算法不同,奖励并未嵌入到环境模型中。因此,ME-TRPO 假设奖励函数是已知的。
实现 ME-TRPO
ME-TRPO 的代码非常长,在这一部分我们不会给出完整的代码。此外,很多部分并不有趣,所有与 TRPO 相关的代码已经在第七章,TRPO 与 PPO 实现中讨论过。然而,如果你对完整的实现感兴趣,或者想要尝试算法,完整的代码可以在本章的 GitHub 仓库中找到。
在这里,我们将提供以下内容的解释和实现:
-
内部循环,其中模拟游戏并优化策略
-
训练模型的函数
剩下的代码与 TRPO 的代码非常相似。
以下步骤将指导我们完成构建和实现 ME-TRPO 核心的过程:
- 改变策略:与真实环境的交互过程中唯一的变化是策略。具体来说,策略在第一轮中会随机执行,但在接下来的轮次中,它会从一个标准差随机设定的高斯分布中采样动作,这个标准差在算法开始时就已固定。这个变化是通过用以下代码行替换 TRPO 实现中的
act, val = sess.run([a_sampl, s_values], feed_dict=``{obs_ph:[env.n_obs]})来完成的:
...
if ep == 0:
act = env.action_space.sample()
else:
act = sess.run(a_sampl, feed_dict={obs_ph:[env.n_obs], log_std:init_log_std})
...
- 拟合深度神经网络,
:神经网络通过前一步获得的数据集学习环境模型。数据集被分为训练集和验证集,其中验证集通过早停技术来判断是否值得继续训练:
...
model_buffer.generate_random_dataset()
train_obs, train_act, _, train_nxt_obs, _ = model_buffer.get_training_batch()
valid_obs, valid_act, _, valid_nxt_obs, _ = model_buffer.get_valid_batch()
print('Log Std policy:', sess.run(log_std))
for i in range(num_ensemble_models):
train_model(train_obs, train_act, train_nxt_obs, valid_obs, valid_act, valid_nxt_obs, step_count, i)
model_buffer是FullBuffer类的一个实例,包含了由环境生成的样本,而generate_random_dataset则会创建用于训练和验证的两个数据集,之后通过调用get_training_batch和get_valid_batch返回。
在接下来的代码中,每个模型都通过train_model函数进行训练,传递数据集、当前步骤数以及需要训练的模型索引。num_ensemble_models是集成中模型的总数。在 ME-TRPO 论文中,显示 5 到 10 个模型就足够了。参数i决定了集成中哪个模型需要被优化。
- 在模拟环境中生成虚拟轨迹并拟合策略:
best_sim_test = np.zeros(num_ensemble_models)
for it in range(80):
obs_batch, act_batch, adv_batch, rtg_batch = simulate_environment(sim_env, action_op_noise, simulated_steps)
policy_update(obs_batch, act_batch, adv_batch, rtg_batch)
这一过程会重复 80 次,或者至少直到策略继续改进为止。simulate_environment通过在模拟环境中(由学习到的模型表示)执行策略来收集数据集(包括观察、动作、优势、值和回报值)。在我们的例子中,策略由函数action_op_noise表示,给定一个状态时,它返回一个遵循学习到的策略的动作。相反,环境sim_env是环境的一个模型,
,在每一步中随机从集成中选择。传递给simulated_environment函数的最后一个参数是simulated_steps,它设定了在虚拟环境中执行的步数。
最终,policy_update函数执行一个 TRPO 步骤,利用在虚拟环境中收集的数据来更新策略。
- 实现早停机制并评估策略:早停机制防止策略在环境模型上过拟合。它通过监控策略在每个独立模型上的表现来工作。如果策略改善的模型所占比例超过某个阈值,则终止该周期。这应该能很好地指示策略是否已经开始过拟合。需要注意的是,与训练不同,在测试过程中,策略是一次在一个模型上进行测试的。在训练过程中,每条轨迹都是由所有学习过的环境模型生成的:
if (it+1) % 5 == 0:
sim_rewards = []
for i in range(num_ensemble_models):
sim_m_env = NetworkEnv(gym.make(env_name), model_op, pendulum_reward, pendulum_done, i+1)
mn_sim_rew, _ = test_agent(sim_m_env, action_op, num_games=5)
sim_rewards.append(mn_sim_rew)
sim_rewards = np.array(sim_rewards)
if (np.sum(best_sim_test >= sim_rewards) > int(num_ensemble_models*0.7)) \
or (len(sim_rewards[sim_rewards >= 990]) > int(num_ensemble_models*0.7)):
break
else:
best_sim_test = sim_rewards
策略评估在每五次训练迭代后进行。对于集成中的每个模型,都会实例化一个新的NetworkEnv类对象。它提供了与真实环境相同的功能,但在后台,它返回来自环境学习模型的过渡。NetworkEnv通过继承Gym.wrapper并重写reset和step函数来实现这一点。构造函数的第一个参数是一个真实环境,仅用于获取真实的初始状态,而model_os是一个函数,当给定一个状态和动作时,它会生成下一个状态。最后,pendulum_reward和pendulum_done是返回奖励和完成标志的函数。这两个函数围绕环境的特定功能构建。
- 训练动态模型:
train_model函数优化一个模型以预测未来的状态。这个过程非常简单易懂。我们在步骤 2 中使用了这个函数,当时我们正在训练多个模型的集成。train_model是一个内部函数,接受我们之前看到的参数。在外部循环的每次 ME-TRPO 迭代中,我们会重新训练所有模型,也就是说,我们从它们的随机初始权重开始训练模型;我们不会从之前的优化继续。因此,每次调用train_model并在训练开始之前,我们都会恢复模型的初始随机权重。以下代码片段在执行此操作之前恢复权重并计算训练前后的损失:
def train_model(tr_obs, tr_act, tr_nxt_obs, v_obs, v_act, v_nxt_obs, step_count, model_idx):
mb_valid_loss1 = run_model_loss(model_idx, v_obs, v_act, v_nxt_obs)
model_assign(model_idx, initial_variables_models[model_idx])
mb_valid_loss = run_model_loss(model_idx, v_obs, v_act, v_nxt_obs)
run_model_loss返回当前模型的损失,model_assign恢复initial_variables_models[model_idx]中的参数。
然后我们训练模型,只要在最后model_iter次迭代中验证集上的损失有所改善。但由于最佳模型可能不是最后一个模型,我们会追踪最佳模型,并在训练结束时恢复其参数。我们还会随机打乱数据集并将其分成小批次。代码如下:
acc_m_losses = []
last_m_losses = []
md_params = sess.run(models_variables[model_idx])
best_mb = {'iter':0, 'loss':mb_valid_loss, 'params':md_params}
it = 0
lb = len(tr_obs)
shuffled_batch = np.arange(lb)
np.random.shuffle(shuffled_batch)
while best_mb['iter'] > it - model_iter:
# update the model on each mini-batch
last_m_losses = []
for idx in range(0, lb, model_batch_size):
minib = shuffled_batch[idx:min(idx+minibatch_size,lb)]
if len(minib) != minibatch_size:
_, ml = run_model_opt_loss(model_idx, tr_obs[minib], tr_act[minib], tr_nxt_obs[minib])
acc_m_losses.append(ml)
last_m_losses.append(ml)
# Check if the loss on the validation set has improved
mb_valid_loss = run_model_loss(model_idx, v_obs, v_act, v_nxt_obs)
if mb_valid_loss < best_mb['loss']:
best_mb['loss'] = mb_valid_loss
best_mb['iter'] = it
best_mb['params'] = sess.run(models_variables[model_idx])
it += 1
# Restore the model with the lower validation loss
model_assign(model_idx, best_mb['params'])
print('Model:{}, iter:{} -- Old Val loss:{:.6f} New Val loss:{:.6f} -- New Train loss:{:.6f}'.format(model_idx, it, mb_valid_loss1, best_mb['loss'], np.mean(last_m_losses)))
run_model_opt_loss是一个函数,它执行具有model_idx索引的模型的优化器。
这就完成了 ME-TRPO 的实现。在下一节中,我们将看到它的表现。
在 RoboSchool 上进行实验
让我们在RoboSchool 倒立摆上测试 ME-TRPO,这是一种与著名的离散控制环境 CartPole 相似的连续倒立摆环境。RoboSchool 倒立摆-v1的截图如下:

目标是通过移动小车保持杆子直立。每当杆子指向上方时,都会获得+1 的奖励。
考虑到 ME-TRPO 需要奖励函数,因此也需要done函数,我们必须为此任务定义两者。为此,我们定义了pendulum_reward,无论观察和动作是什么,它都返回 1:
def pendulum_reward(ob, ac):
return 1
pendulum_done当杆的角度绝对值大于固定阈值时返回True。我们可以直接从状态中获取角度。实际上,状态的第三和第四个元素分别是角度的余弦和正弦。然后,我们可以任意选择其中一个来计算角度。因此,pendulum_done如下所示:
def pendulum_done(ob):
return np.abs(np.arcsin(np.squeeze(ob[3]))) > .2
除了 TRPO 的常规超参数外,这些超参数几乎与第七章中使用的保持不变,TRPO 与 PPO 实现,ME-TRPO 还要求以下超参数:
-
动态模型优化器的学习率,
mb_lr -
用于训练动态模型的最小批次大小,
model_batch_size -
每次迭代中执行的模拟步数,
simulated_steps(这也是用于训练策略的批次大小) -
构成集成的模型数量,
num_ensemble_models -
如果验证结果没有下降,等待中断
model_iter训练的迭代次数
在这个环境中使用的这些超参数值如下:
| 超参数 | 值 |
|---|---|
学习率(mb_lr) |
1e-5 |
模型批次大小(model_batch_size) |
50 |
模拟步数(simulated_steps) |
50000 |
模型数量(num_ensemble_models) |
10 |
提前停止迭代次数(model_iter) |
15 |
RoboSchool 倒立摆的结果
性能图表如下所示:

奖励与与真实环境交互的步数之间的关系。经过 900 步和大约 15 场游戏后,智能体达到了 1000 的最佳性能。策略更新了 15 次,并从 750,000 个模拟步数中学习。从计算角度看,该算法在中端计算机上训练了大约 2 小时。
我们注意到,结果具有很高的变异性,如果使用不同的随机种子进行训练,可能会得到非常不同的性能曲线。这对于无模型算法也是如此,但在这里,差异更加明显。造成这种情况的一个原因可能是实际环境中收集的数据不同。
摘要
在本章中,我们暂时从无模型算法中休息,开始讨论和探索从环境模型中学习的算法。我们分析了激发我们开发这种算法的范式转变背后的关键原因。然后,我们区分了处理模型时可能遇到的两种主要情况:第一种情况是模型已知,第二种情况是模型需要被学习。
此外,我们学习了如何利用模型来规划下一步的动作或学习策略。选择使用其中之一没有固定的规则,但通常与动作和观察空间的复杂性以及推理速度相关。我们随后研究了基于模型和无模型算法的优缺点,并通过将无模型算法与基于模型的学习结合,加深了我们对如何用无模型算法学习策略的理解。这揭示了一种在高维观察空间(如图像)中使用模型的新方式。
最后,为了更好地掌握与基于模型的算法相关的所有材料,我们开发了 ME-TRPO。该方法通过使用模型集成和信任域策略优化来应对模型的不确定性,从而学习策略。所有模型都用于预测下一个状态,从而创建模拟的轨迹,基于这些轨迹学习策略。因此,策略完全基于环境的学习模型进行训练。
本章总结了关于基于模型学习的讨论,在下一章中,我们将介绍新的学习范式。我们将讨论通过模仿学习的算法。此外,我们将开发并训练一个代理,通过跟随专家的行为,能够玩 FlappyBird。
问题
-
如果你只有 10 局游戏时间来训练代理玩跳棋,你会选择基于模型的算法还是无模型的算法?
-
基于模型的算法有什么缺点?
-
如果环境的模型未知,如何学习它?
-
为什么要使用数据聚合方法?
-
ME-TRPO 是如何稳定训练的?
-
使用模型集成如何改善策略学习?
进一步阅读
-
要扩展你对从图像观察中学习策略的基于模型的算法的了解,请阅读论文《基于模型的 Atari 强化学习》:
arxiv.org/pdf/1903.00374.pdf。 -
要阅读与 ME-TRPO 相关的原始论文,请点击此链接:
arxiv.org/pdf/1802.10592.pdf。
第十章:使用 DAgger 算法的模仿学习
算法仅通过奖励来学习的能力是一个非常重要的特性,这也是我们开发强化学习算法的原因之一。它使得代理可以从零开始学习并改进其策略,而无需额外的监督。尽管如此,在某些情况下,给定环境中可能已经有其他专家代理在工作。模仿学习(IL)算法通过模仿专家的行为并从中学习策略来利用专家。
本章重点讲解模仿学习。虽然与强化学习不同,模仿学习在某些环境中提供了巨大的机会和能力,尤其是在具有非常大状态空间和稀疏奖励的环境中。显然,模仿学习只有在可以模仿的更专家代理存在时才可能进行。
本章将重点讲解模仿学习方法的主要概念和特性。我们将实现一个名为 DAgger 的模仿学习算法,并教一个代理玩 Flappy Bird。这将帮助你掌握这一新型算法,并理解其基本原理。
本章的最后部分,我们将介绍逆强化学习(IRL)。IRL 是一种通过值和奖励来提取并学习另一个代理行为的方法;也就是说,IRL 学习奖励函数。
本章将涵盖以下主题:
-
模仿方法
-
玩 Flappy Bird
-
理解数据集聚合算法
-
IRL
技术要求
在简要的理论介绍后,我们将实现一个实际的 IL 算法,帮助理解模仿学习算法背后的核心概念。然而,我们只会提供主要和最有趣的部分。如果你对完整实现感兴趣,可以在本书的 GitHub 仓库中找到: github.com/PacktPublishing/Reinforcement-Learning-Algorithms-with-Python。
安装 Flappy Bird
接下来,我们将在一个重新设计的著名游戏 Flappy Bird 上运行我们的 IL 算法(en.wikipedia.org/wiki/Flappy_Bird)。在这一部分,我们将提供安装所需的所有命令。
但在安装游戏环境之前,我们需要处理一些额外的库:
- 在 Ubuntu 中,步骤如下:
$ sudo apt-get install git python3-dev python3-numpy libsdl-image1.2-dev libsdl-mixer1.2-dev libsdl-ttf2.0-dev libsmpeg-dev libsdl1.2-dev libportmidi-dev libswscale-dev libavformat-dev libavcodec-dev libfreetype6-dev
$ sudo pip install pygame
- 如果你是 Mac 用户,可以通过以下命令安装库:
$ brew install sdl sdl_ttf sdl_image sdl_mixer portmidi
$ pip install -c https://conda.binstar.org/quasiben pygame
- 然后,对于 Ubuntu 和 Mac 用户,步骤如下:
- 首先,你需要克隆 PLE。克隆可以通过以下代码行完成:
git clone https://github.com/ntasfi/PyGame-Learning-Environment
PLE 是一套环境,也包括 Flappy Bird。因此,通过安装 PLE,你将获得 Flappy Bird。
- 然后,你需要进入
PyGame-Learning-Environment文件夹:
cd PyGame-Learning-Environment
- 最后,通过以下命令运行安装:
sudo pip install -e .
现在,你应该能够使用 Flappy Bird 了。
模仿方法
模仿学习(IL)是通过模仿专家来获得新技能的艺术。模仿学习这一特性对学习顺序决策策略来说并不是绝对必要的,但如今,它在许多问题中是不可或缺的。有些任务不能仅仅通过强化学习来解决,从复杂环境中的巨大空间中自举策略是一个关键因素。以下图表展示了模仿学习过程中涉及的核心组件的高层视图:

如果智能体(专家)已经存在于环境中,它们可以为新的智能体(学习者)提供大量关于完成任务和导航环境所需行为的信息。在这种情况下,新的智能体可以在不从零开始学习的情况下更快地学习。专家智能体还可以作为教师,指导并反馈给新的智能体其表现。注意这里的区别,专家既可以作为指导者来跟随,也可以作为监督者来纠正学生的错误。
如果能够提供引导模型或监督者,模仿学习算法可以利用它们。现在你可以理解为什么模仿学习如此重要,也明白为什么我们不能将其排除在本书之外。
驾驶助手示例
为了更好地理解这些关键概念,我们可以用一个青少年学车的例子来说明。假设他们从未坐过车,这是他们第一次看到汽车,而且他们对汽车的工作原理一无所知。学习有三种方法:
-
他们拿到钥匙后必须完全独立学习,完全没有监督。
-
在获得钥匙之前,他们需要坐在副驾驶座上观察专家驾驶 100 小时,了解在不同天气条件和道路上的驾驶情况。
-
他们观察专家驾驶,但更重要的是,他们有机会在专家驾驶时得到反馈。例如,专家可以实时指导如何停车,并对如何保持车道提出直接反馈。
正如你可能已经猜到的,第一个案例是强化学习方法,在这种方法中,智能体只有在不撞车、行人不对其大喊大叫等情况下才会获得稀疏的奖励。
至于第二个案例,这是一种被动的模仿学习方法,通过纯粹复制专家的行为来获得能力。总体而言,它与监督学习方法非常相似。
第三个也是最后一个案例是主动的模仿学习方法,形成了真正的模仿学习方法。在这种情况下,要求在训练阶段,专家对学习者的每一个动作进行指导。
比较模仿学习(IL)和强化学习(RL)
让我们通过强调模仿学习与强化学习之间的差异来更深入地了解模仿学习方法。这种对比非常重要。在模仿学习中,学习者并不意识到任何奖励。这一约束可能会带来非常大的影响。
回到我们的例子,学徒只能尽可能地模仿专家的动作,无论是被动的还是主动的。由于没有来自环境的客观奖励,他们只能受制于专家的主观监督。因此,即使他们想要改进,也无法理解和掌握老师的推理过程。
因此,模仿学习应被视为一种模仿专家动作的方式,但并不理解其主要目标。在我们的例子中,年轻的司机可能很好地模仿了老师的驾驶轨迹,但仍然不知道老师选择这些轨迹背后的动机。没有奖励的意识,经过模仿学习训练的代理无法像强化学习中那样最大化总奖励。
这突出了模仿学习和强化学习之间的主要区别。前者缺乏对主要目标的理解,因此无法超越老师。而后者则缺乏直接的监督信号,在大多数情况下,只能获得稀疏的奖励。这个情况在以下图示中得到了清晰的展示:

左侧的图表示通常的强化学习(RL)循环,而右侧则表示模仿学习(IL)循环。在这里,学习者不会获得任何奖励,只有专家提供的状态和动作。
模仿学习中专家的角色
在讨论模仿学习算法时,专家、老师和监督者这几个术语指的是相同的概念。它们表示一种可以让新代理(学习者)学习的角色。
从根本上讲,专家可以有各种形式,从真实的人类专家到专家系统。前者更为明显并且被广泛采用。你所做的事情是教算法执行一个人类已经能够做的任务。其优点显而易见,并且可以应用于大量的任务中。
第二种情况可能不太常见。选择新算法并用模仿学习进行训练的有效动机之一,可能是由于技术限制,一个缓慢的专家系统无法得到改进。例如,老师可能是一个准确但缓慢的树搜索算法,在推理时无法以合适的速度运行。这时,可以用深度神经网络来代替它。尽管在树搜索算法的监督下训练神经网络可能需要一些时间,但一旦训练完成,它在运行时的表现会更快。
到现在为止,应该很清楚,从学习者得到的策略质量在很大程度上取决于专家提供的信息质量。教师的表现是学者最终表现的上限。一个糟糕的老师总是会给学习者提供糟糕的数据。因此,专家是设定最终代理质量标准的关键组件。只有在教师强大的情况下,我们才能期望获得好的策略。
IL 结构
现在我们已经解决了模仿学习的所有要素,可以详细说明可以用于设计完整模仿学习算法的算法和方法。
解决模仿问题的最直接方法如下图所示:

前面的图示可以总结为两个主要步骤:
-
专家从环境中收集数据。
-
通过监督学习在数据集上学习一个策略。
不幸的是,尽管监督学习是模仿算法的典范,但大多数时候,它并不奏效。
为了理解为什么监督学习方法不是一个好的替代方案,我们必须回顾监督学习的基础。我们主要关注两个基本原则:训练集和测试集应该属于相同的分布,并且数据应该是独立同分布的(i.i.d.)。然而,一个策略应该能够容忍不同的轨迹,并对最终的分布变化具有鲁棒性。
如果一个代理仅通过监督学习方法来训练驾驶汽车,每当它从专家的轨迹中稍微偏离时,它将处于一个前所未见的新状态,这将导致分布不匹配。在这个新状态下,代理对下一步动作会感到不确定。在通常的监督学习问题中,这不会太影响。如果错过了一个预测,这不会对下一个预测产生影响。然而,在模仿学习问题中,算法正在学习一个策略,i.i.d.属性不再成立,因为后续的动作是严格相关的。因此,它们会对所有其他动作产生后果,并且有累积效应。
在我们自驾车的例子中,一旦分布从专家的分布发生变化,正确的路径将变得非常难以恢复,因为错误的动作会积累并导致严重后果。轨迹越长,模仿学习的效果越差。为了更清楚地说明,具有 i.i.d.数据的监督学习问题可以视为长度为 1 的轨迹。对下一步动作没有任何影响。我们刚才提出的范式就是我们之前提到的被动学习。
为了克服由于使用被动模仿而可能对策略造成灾难性影响的分布变化,可以采用不同的技术。有些是巧妙的黑客技术,而另一些则是更具算法变种的方式。以下是两种效果较好的策略:
-
学习一个在数据上能很好地泛化而不发生过拟合的模型
-
除了被动模仿,还可以使用主动模仿
因为第一个是更广泛的挑战,我们将集中精力在第二个策略上。
比较主动模仿和被动模仿
在前面的示例中,我们介绍了主动模仿这个术语,通过一个青少年学习开车的例子。具体来说,我们指的是在学习者在专家的额外反馈下进行驾驶的情境。一般来说,主动模仿是指从专家分配的动作中,通过策略数据进行学习。
从输入s(状态或观察)和输出a(动作)的角度来看,在被动学习中,s 和 a 都来自专家。在主动学习中,s 是从学习者那里采样的,a 是专家在状态 s 下应该采取的动作。新手代理的目标是学习一个映射,![]。
使用带有策略数据的主动学习可以让学习者修正那些仅靠被动模仿无法纠正的小偏差。
玩 Flappy Bird
在本章后续部分,我们将开发并测试一个名为 DAgger 的 IL 算法,应用在一个新的环境中。这个环境名为 Flappy Bird,模拟了著名的 Flappy Bird 游戏。在这里,我们的任务是为你提供所需的工具,帮助你使用这个环境实现代码,从接口的解释开始。
Flappy Bird 属于PyGame 学习环境(PLE),这是一组模仿街机学习环境(ALE)接口的环境。它类似于Gym接口,虽然使用起来很简单,稍后我们会看到它们之间的差异。
Flappy Bird 的目标是让小鸟飞过垂直的管道而不撞到它们。它只通过一个动作来控制,即让小鸟拍打翅膀。如果小鸟不飞,它会按照重力作用沿着下降轨迹前进。下面是环境的截图:

如何使用环境
在接下来的步骤中,我们将看到如何使用这个环境。
- 为了在 Python 脚本中使用 Flappy Bird,首先,我们需要导入 PLE 和 Flappy Bird:
from ple.games.flappybird import FlappyBird
from ple import PLE
- 然后,我们实例化一个
FlappyBird对象,并将其传递给PLE,并传递一些参数:
game = FlappyBird()
p = PLE(game, fps=30, display_screen=False)
在这里,通过display_screen,你可以选择是否显示屏幕。
- 通过调用
init()方法初始化环境:
p.init()
为了与环境交互并获得环境的状态,我们主要使用四个函数:
-
-
p.act(act),用来在游戏中执行act动作。act(act)返回执行该动作后获得的奖励。 -
p.game_over(),用于检查游戏是否达到了最终状态。 -
p.reset_game(),将游戏重置为初始状态。 -
p.getGameState(),用于获取当前环境的状态。如果我们想获取环境的 RGB 观察值(即整个屏幕),也可以使用p.getScreenRGB()。
-
- 将所有内容整合在一起,一个简单的脚本可以设计成如下代码片段,用于让 Flappy Bird 玩五局。请注意,为了使其工作,您仍然需要定义返回给定状态下动作的
get_action(state)函数:
from ple.games.flappybird import FlappyBird
from ple import PLE
game = FlappyBird()
p = PLE(game, fps=30, display_screen=False)
p.init()
reward = 0
for _ in range(5):
reward += p.act(get_action(p.getGameState()))
if p.game_over():
p.reset_game()
这里有几个要点需要指出:
-
getGameState()返回一个字典,其中包含玩家的位置、速度和距离,以及下一根管道和下下根管道的位置。在将状态传递给我们在此用get_action函数表示的策略制定者之前,字典会被转换为 NumPy 数组并进行标准化。 -
act(action)如果不需要执行动作,则期望输入为None;如果鸟需要拍打翅膀飞得更高,则输入为119。
理解数据集聚合算法
数据集聚合(DAgger)是从演示中学习的最成功算法之一。它是一个迭代的策略元算法,在诱发的状态分布下表现良好。DAgger 最显著的特点是,它通过提出一种主动方法来解决分布不匹配问题,在这种方法中,专家教导学习者如何从学习者的错误中恢复。
经典的 IL 算法学习一个分类器,预测专家的行为。这意味着模型拟合一个由专家观察到的训练样本数据集。输入是观察值,输出是期望的动作。然而,根据之前的推理,学习者的预测会影响未来访问的状态或观察,违反了独立同分布(i.i.d.)假设。
DAgger 通过反复迭代从学习者中采样的新数据聚合管道,来处理分布的变化,并利用聚合的数据集进行训练。算法的简单示意图如下所示:

专家填充了分类器所使用的数据集,但根据迭代的不同,环境中执行的动作可能来自专家,也可能来自学习者。
DAgger 算法
具体来说,DAgger 通过迭代以下过程进行。在第一次迭代中,从专家策略创建一个轨迹数据集 D,并用它来训练一个最适合这些轨迹且不发生过拟合的初始策略
。然后,在迭代 i 中,使用学习到的策略
收集新的轨迹,并将其添加到数据集 D 中。接着,使用包含新旧轨迹的聚合数据集 D 来训练一个新的策略,
。
根据《Dagger 论文》中的报告(arxiv.org/pdf/1011.0686.pdf),有一种活跃的基于策略的学习方法,优于许多其他模仿学习算法,并且在深度神经网络的帮助下,它能够学习非常复杂的策略。
此外,在迭代 i 时,可以修改策略,使专家控制多个动作。该技术更好地利用了专家的能力,并让学习者逐渐掌控环境。
算法的伪代码可以进一步澄清这一点:
Initialize
Initialize ( is the expert policy)
for i :
> Populate dataset with . States are given by (sometimes the expert could take the control over it) and actions are given by the expert
> Train a classifier on the aggregate dataset
DAgger 的实现
代码分为三个主要部分:
-
加载专家推理函数,以便根据状态预测动作。
-
为学习者创建计算图。
-
创建 DAgger 迭代以构建数据集并训练新策略。
在这里,我们将解释最有趣的部分,其他部分留给你个人兴趣。你可以在书籍的 GitHub 仓库中查看剩余的代码和完整版本。
加载专家推理模型
专家应该是一个以状态为输入并返回最佳动作的策略。尽管如此,它可以是任何东西。特别是,在这些实验中,我们使用了一个通过近端策略优化(PPO)训练的代理作为专家。从原则上讲,这没有什么意义,但我们为学术目的采用了这一解决方案,以便与模仿学习算法进行集成。
使用 PPO 训练的专家模型已保存在文件中,因此我们可以轻松地恢复它并使用其训练好的权重。恢复图并使其可用需要三步:
-
导入元图。可以通过
tf.train.import_meta_graph恢复计算图。 -
恢复权重。现在,我们需要将预训练的权重加载到刚刚导入的计算图中。权重已保存在最新的检查点中,可以通过
tf.train.latest_checkpoint(session, checkpoint)恢复。 -
访问输出张量。恢复的图的张量可以通过
graph.get_tensor_by_name(tensor_name)访问,其中tensor_name是图中张量的名称。
以下代码行总结了整个过程:
def expert():
graph = tf.get_default_graph()
sess_expert = tf.Session(graph=graph)
saver = tf.train.import_meta_graph('expert/model.ckpt.meta')
saver.restore(sess_expert,tf.train.latest_checkpoint('expert/'))
p_argmax = graph.get_tensor_by_name('actor_nn/max_act:0')
obs_ph = graph.get_tensor_by_name('obs:0')
然后,因为我们只关心一个简单的函数,它会根据状态返回专家动作,我们可以设计 expert 函数,使其返回该函数。因此,在 expert() 内部,我们定义一个名为 expert_policy(state) 的内部函数,并将其作为 expert() 的输出返回:
def expert_policy(state):
act = sess_expert.run(p_argmax, feed_dict={obs_ph:[state]})
return np.squeeze(act)
return expert_policy
创建学习者的计算图
以下所有代码都位于一个名为 DAgger 的函数内部,该函数接受一些超参数,我们将在代码中看到这些参数。
学习者的计算图非常简单,因为它的唯一目标是构建一个分类器。在我们的案例中,只有两个动作需要预测,一个是做不做动作,另一个是让小鸟拍翅膀。我们可以实例化两个占位符,一个用于输入状态,另一个用于真实标签,即专家的动作。动作是一个整数,表示所采取的动作。对于两个可能的动作,它们分别是 0(什么也不做)或 1(飞行)。
构建这样的计算图的步骤如下:
-
创建一个深度神经网络,具体来说,是一个具有 ReLU 激活函数的全连接多层感知器,在隐藏层使用 ReLU 激活函数,在最后一层使用线性激活函数。
-
对于每个输入状态,选择具有最高值的动作。这个操作通过
tf.math.argmax(tensor,axis)函数完成,axis=1。 -
将动作的占位符转换为 one-hot 张量。这是必要的,因为我们在损失函数中使用的 logits 和标签应该具有维度
[batch_size, num_classes]。然而,我们的标签act_ph的形状是[batch_size]。因此,我们通过 one-hot 编码将它们转换为所需的形状。tf.one_hot是 TensorFlow 用于执行这一操作的函数。 -
创建损失函数。我们使用 softmax 交叉熵损失函数。这是一个标准的损失函数,适用于具有互斥类别的离散分类问题,就像我们的情况一样。损失函数通过
softmax_cross_entropy_with_logits_v2(labels, logits)在 logits 和标签之间进行计算。 -
最后,计算 softmax 交叉熵的平均值,并使用 Adam 优化器进行最小化。
这五个步骤在接下来的代码行中实现。
obs_ph = tf.placeholder(shape=(None, obs_dim), dtype=tf.float32, name='obs')
act_ph = tf.placeholder(shape=(None,), dtype=tf.int32, name='act')
p_logits = mlp(obs_ph, hidden_sizes, act_dim, tf.nn.relu, last_activation=None)
act_max = tf.math.argmax(p_logits, axis=1)
act_onehot = tf.one_hot(act_ph, depth=act_dim)
p_loss = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits_v2(labels=act_onehot, logits=p_logits))
p_opt = tf.train.AdamOptimizer(p_lr).minimize(p_loss)
然后,我们可以初始化会话、全局变量,并定义一个函数 learner_policy(state)。该函数根据给定状态返回学习者选择的具有更高概率的动作(这与我们为专家所做的相同):
sess = tf.Session()
sess.run(tf.global_variables_initializer())
def learner_policy(state):
action = sess.run(act_max, feed_dict={obs_ph:[state]})
return np.squeeze(action)
创建 DAgger 循环
现在是设置 DAgger 算法核心的时候了。该概要已经在 The DAgger algorithm 部分的伪代码中定义,但让我们更深入地了解它是如何工作的:
- 初始化由两个列表组成的数据集,
X和y,其中存储访问过的状态和专家的目标动作。我们还初始化环境:
X = []
y = []
env = FlappyBird()
env = PLE(env, fps=30, display_screen=False)
env.init()
- 遍历所有 DAgger 迭代。在每次 DAgger 迭代的开始,我们必须重新初始化学习者的计算图(因为我们在每次迭代时都会在新的数据集上重新训练学习者),重置环境,并执行一系列随机动作。在每个游戏开始时,我们执行一些随机动作,以向确定性环境中添加随机成分。结果将是一个更强健的策略:
for it in range(dagger_iterations):
sess.run(tf.global_variables_initializer())
env.reset_game()
no_op(env)
game_rew = 0
rewards = []
- 通过与环境互动收集新数据。正如我们之前所说,第一次迭代中包含了专家,专家必须通过调用
expert_policy来选择动作,但在后续迭代中,学习者会逐渐接管控制权。学习到的策略由learner_policy函数执行。数据集通过将当前游戏状态附加到X(输入变量),并将专家在该状态下会采取的动作附加到y(输出变量)来收集。当游戏结束时,游戏将重置,并将game_rew设置为0。代码如下:
for _ in range(step_iterations):
state = flappy_game_state(env)
if np.random.rand() < (1 - it/5):
action = expert_policy(state)
else:
action = learner_policy(state)
action = 119 if action == 1 else None
rew = env.act(action)
rew += env.act(action)
X.append(state)
y.append(expert_policy(state))
game_rew += rew
if env.game_over():
env.reset_game()
np_op(env)
rewards.append(game_rew)
game_rew = 0
请注意,动作被执行了两次。这是为了将每秒的动作数量从 30 减少到 15,以符合环境的要求。
- 在汇总数据集上训练新策略。该流程是标准的。数据集被打乱并分成
batch_size长度的小批次。然后,通过在每个小批次上运行p_opt进行多个训练周期(等于train_epochs),重复优化过程。以下是代码:
n_batches = int(np.floor(len(X)/batch_size))
shuffle = np.arange(len(X))
np.random.shuffle(shuffle)
shuffled_X = np.array(X)[shuffle]
shuffled_y = np.array(y)[shuffle]
ep_loss = []
for _ in range(train_epochs):
for b in range(n_batches):
p_start = b*batch_size
tr_loss, _ = sess.run([p_loss, p_opt], feed_dict=
obs_ph:shuffled_X[p_start:p_start+batch_size],
act_ph:shuffled_y[p_start:p_start+batch_size]})
ep_loss.append(tr_loss)
print('Ep:', it, np.mean(ep_loss), 'Test:', np.mean(test_agent(learner_policy)))
test_agent在几局游戏中测试learner_policy,以了解学习者的表现如何。
在 Flappy Bird 上的结果分析
在展示模仿学习方法的结果之前,我们想提供一些数据,以便你能将这些与强化学习算法的结果进行比较。我们知道这不是一个公平的比较(这两种算法在非常不同的条件下工作),但无论如何,它们强调了为什么当有专家可用时,模仿学习是值得的。
专家已经使用近端策略优化进行了大约 200 万步的训练,并且在大约 40 万步后,达到了约 138 的停滞分数。
我们在 Flappy Bird 上测试了 DAgger,使用了以下超参数:
| 超参数 | 变量名 | 值 |
|---|---|---|
| 学习者隐藏层 | hidden_sizes | 16,16 |
| DAgger 迭代 | dagger_iterations | 8 |
| 学习率 | p_lr | 1e-4 |
| 每次 DAgger 迭代的步数 | step_iterations | 100 |
| 小批次大小 | batch_size | 50 |
| 训练周期数 | train_epochs | 2000 |
下图展示了 DAgger 性能随步数变化的趋势:

横线代表专家所达到的平均表现。从结果来看,我们可以看到几百步就足以达到专家的表现。然而,与 PPO 训练专家所需的经验相比,这表示样本效率提高了大约 100 倍。
再次强调,这并不是一个公平的比较,因为方法处于不同的背景中,但它突出了一个事实:每当有专家时,建议你使用模仿学习方法(至少可以用来学习一个初始策略)。
IRL
IL 的最大限制之一在于它无法学习其他路径来达到目标,除了从专家那里学到的路径。通过模仿专家,学习者受到教师行为范围的限制。他们并不了解专家试图达成的最终目标。因此,这些方法只有在没有意图超越教师表现的情况下才有用。
IRL 是一种强化学习算法,类似于 IL,使用专家进行学习。不同之处在于 IRL 使用专家来学习其奖励函数。因此,IRL 并不是像模仿学习那样复制示范,而是弄清楚专家的目标。一旦奖励函数被学习,智能体便可以利用它来学习策略。
由于示范仅用于理解专家的目标,智能体不受教师动作的限制,最终可以学到更好的策略。例如,一个通过 IRL 学习的自动驾驶汽车会明白,目标是以最短的时间从 A 点到达 B 点,同时减少对物体和人员的损害。然后,汽车会自行学习一个策略(例如,使用 RL 算法),以最大化这个奖励函数。
然而,IRL 也存在许多挑战,这些挑战限制了其适用性。专家的示范可能并非最优,因此,学习者可能无法充分发挥其潜力,并可能陷入错误的奖励函数中。另一个挑战在于对学习到的奖励函数的评估。
摘要
在这一章中,我们暂时跳出了强化学习算法,探讨了一种新的学习方式——模仿学习。这一新范式的创新之处在于学习的方式;即结果策略模仿专家的行为。这个范式与强化学习的不同之处在于没有奖励信号,并且能够利用专家实体带来的丰富信息源。
我们看到,学习者所学习的数据集可以通过增加额外的状态-动作对来扩展,以增加学习者在新情况中的信心。这个过程叫做数据聚合。此外,新数据可以来自新学习的策略,在这种情况下,我们称之为基于策略的数据(因为它来自同一个已学习的策略)。这种将基于策略的状态与专家反馈结合的做法是一种非常有价值的方法,可以提高学习者的质量。
然后我们探索并开发了最成功的模仿学习算法之一,名为 DAgger,并将其应用于学习 Flappy Bird 游戏。
然而,由于模仿学习算法只是复制专家的行为,这些系统无法做得比专家更好。因此,我们引入了逆向强化学习,它通过推断专家的奖励函数来克服这个问题。通过这种方式,策略可以独立于教师来学习。
在下一章,我们将介绍一组用于解决顺序任务的算法;即进化算法。你将学习这些黑箱优化算法的机制和优势,从而能够在挑战性环境中采用它们。此外,我们将更深入地探讨一种名为进化策略的进化算法,并加以实现。
问题
-
模仿学习是否被认为是一种强化学习技术?
-
你会使用模仿学习来构建一个在围棋中无法击败的智能体吗?
-
DAgger 的全名是什么?
-
DAgger 的主要优点是什么?
-
在哪里你会使用逆向强化学习而不是模仿学习?
进一步阅读
-
要阅读介绍 DAgger 的原始论文,请查看以下论文,将模仿学习和结构化预测归约为无悔在线学习:
arxiv.org/pdf/1011.0686.pdf。 -
想了解更多关于模仿学习算法的信息,请查看以下论文,模仿学习的全球概述:
arxiv.org/pdf/1801.06503.pdf。 -
想了解更多关于逆向强化学习的信息,请查看以下调查,逆向强化学习调查:挑战、方法与进展:
arxiv.org/pdf/1806.06877.pdf。
第十一章:理解黑盒优化算法
在前几章中,我们研究了强化学习(RL)算法,从基于价值的方法到基于策略的方法,以及从无模型方法到基于模型的方法。在本章中,我们将提供另一种解决序列任务的方法,那就是使用一类黑盒算法——进化算法(EA)。EAs 由进化机制驱动,有时比强化学习(RL)更受青睐,因为它们不需要反向传播。它们还为 RL 提供了其他互补的好处。本章将从简要回顾强化学习(RL)算法开始,帮助你更好地理解 EA 如何融入这些问题解决方案中。接着,你将了解 EA 的基本构建模块及其工作原理。我们还将利用这个介绍,深入研究其中一种最著名的进化算法——进化策略(ES)。
由 OpenAI 开发的一种近期算法大大推动了进化策略(ES)在解决序列任务中的应用。它们展示了 ES 算法如何能够在多个 CPU 上进行大规模并行化并线性扩展,同时实现高性能。在解释了进化策略后,我们将更深入地探讨这个算法,并在 TensorFlow 中进行开发,这样你就可以将它应用于你关心的任务。
本章将涵盖以下主题:
-
超越强化学习(RL)
-
EAs 的核心
-
可扩展的进化策略
-
可扩展的进化策略应用于 LunarLander
超越强化学习(RL)
强化学习(RL)算法通常是我们面对序列决策问题时的首选。通常,除了使用 RL,很难找到其他方法来解决这些任务。尽管有数百种不同的优化方法,但到目前为止,只有 RL 在序列决策问题上取得了良好的效果。但这并不意味着它是唯一的选择。
本章将从回顾强化学习(RL)算法的内在工作原理开始,并质疑其组件在解决序列任务中的有效性。这个简要总结将帮助我们介绍一种新的算法类型,该算法提供了许多优点(以及一些缺点),可以作为 RL 的替代方案。
强化学习(RL)简要回顾
在开始时,策略被随机初始化,并用来与环境交互,收集数据,交互可以是给定的步数或整个轨迹。在每次交互中,访问的状态、采取的行动和获得的奖励都会被记录下来。这些信息提供了代理在环境中影响的完整描述。然后,为了改进策略,基于损失函数的反向传播算法(为了将预测值移向更好的估计)计算网络每个权重的梯度。接着,这些梯度会通过随机梯度下降优化器进行应用。这个过程(从环境中收集数据并使用随机梯度下降(SGD)优化神经网络)会一直重复,直到满足收敛标准为止。
这里有两件重要的事情需要注意,它们在接下来的讨论中会非常有用:
-
时序信用分配:因为强化学习算法在每一步优化策略,所以需要对每个行动和状态分配质量。这是通过为每个状态-行动对分配一个值来完成的。此外,使用折扣因子来最小化远距离行动的影响,并给予最后行动更多的权重。这将帮助我们解决将信用分配给行动的问题,但也会引入系统中的不准确性。
-
探索:为了保持一定程度的探索,额外的噪声会被注入到强化学习算法的策略中。噪声注入的方式依赖于算法,通常情况下,行动是从一个随机分布中采样的。这样做的目的是,如果代理处于相同的情境两次,它可能会采取不同的行动,进而导致两条不同的路径。这种策略也能在确定性环境中鼓励探索。通过每次偏离路径,代理可能会发现不同的——甚至是更好的——解决方案。通过这种额外的噪声,且噪声渐进趋近于 0,代理最终能够收敛到一个更好且最终的确定性策略。
但是,反向传播、时序信用分配和随机行动,真的算是学习和构建复杂策略的前提条件吗?
替代方法
这个问题的答案是否定的。
正如我们在第十章《使用 DAgger 算法的模仿学习》中所学到的那样,通过使用反向传播和随机梯度下降(SGD)将策略学习转化为模仿问题,我们可以通过专家学习一个判别模型,以预测下一步应采取的行动。不过,这仍然涉及反向传播,并且需要一个可能并不总是能获得的专家。
另一个用于全局优化的常见算法子集确实存在。它们被称为进化算法(EAs),并且不基于反向传播,也不需要另外两个原理,即时间信用分配和噪声行为。此外,正如我们在本章开头所说的,这些进化算法非常通用,可以应用于各种问题,包括顺序决策任务。
进化算法(EAs)
正如你可能猜到的,进化算法与强化学习算法在许多方面不同,并且主要受到生物进化的启发。进化算法包括许多类似的方法,如遗传算法、进化策略和遗传编程,它们在实现细节和表示的方式上有所不同。然而,它们都主要基于四个基本机制——繁殖、变异、交叉和选择——并通过一种猜测与检验的过程循环进行。随着我们本章的进展,我们将看到这些机制具体意味着什么。
进化算法被定义为黑箱算法。这些算法优化一个函数,![],相对于![],而不对![]做任何假设。因此,![]可以是你想要的任何东西。我们只关心![]的输出。这有许多优点,也有一些缺点。主要的优点是我们不需要关心![]的结构,我们可以自由地使用对我们和当前问题最合适的方法。另一方面,主要的缺点是这些优化方法无法解释,因此其机制无法被理解。在需要可解释性的情况下,这些方法就不太有吸引力了。
强化学习几乎一直被优先用于解决顺序任务,尤其是对于中等到困难的任务。然而,OpenAI 最近的一篇论文强调,进化策略(Evolutionary Strategy),一种进化算法,可以作为强化学习的替代方法。这一观点主要源于该算法所达到的渐近性能以及其在数千个 CPU 上扩展的惊人能力。
在我们了解这个算法如何能够在学习困难任务的优秀策略时良好扩展之前,让我们更深入地了解进化算法。
进化算法的核心
进化算法(EAs)受到生物进化的启发,采用模拟生物进化的技术和机制。这意味着进化算法通过多次试验来创建新的候选解种群。这些解也被称为个体(在强化学习问题中,候选解就是策略),它们比上一代更优,类似于自然界中的过程,只有最强者生存下来并有可能繁衍后代。
进化算法的一个优势是它们是无导数的方法,意味着它们不使用导数来寻找解。这使得进化算法能够很好地处理各种可微和不可微的函数,包括深度神经网络。这种结合在下图中进行了示意。请注意,每个个体都是一个独立的深度神经网络,因此在任何时刻,神经网络的数量与个体数量相同。在下图中,种群由五个个体组成:

图 11.1. 通过进化算法优化深度神经网络
每种进化算法的具体性有所不同,但它们的基本循环在所有进化算法中是共同的,其工作原理如下:
-
创建一个由个体组成的种群(也称为候选解或表型),每个个体都有一组不同的属性(称为染色体或基因型)。初始种群是随机初始化的。
-
每个候选解都通过一个适应度函数独立评估,该函数决定解的质量。适应度函数通常与目标函数相关,按照我们到目前为止使用的术语,适应度函数可以是智能体(即候选解)在其生命周期内累积的总奖励。
-
然后,从种群中选出适应性更强的个体,并修改它们的基因组,以便生成新一代个体。在某些情况下,适应性较差的候选解可以作为负面示例,帮助生成下一代。这个过程的具体步骤在不同算法中差异较大。一些算法,例如遗传算法,通过交叉和变异这两个过程来繁殖新个体,这些新个体被称为后代。其他算法,如进化策略,仅通过变异来繁殖新个体。我们将在本章稍后更深入地解释交叉和变异,但一般来说,交叉是将两个父代的基因信息结合的过程,而变异则仅改变后代中某些基因值。
-
重复整个过程,经过步骤 1-3,直到满足终止条件。在每次迭代中,创建的种群也称为代。
如下图所示,这一迭代过程在达到给定的适应度水平或生成了最大数量的世代后终止。如我们所见,种群是通过交叉和变异生成的,但正如我们已经解释过的,这些过程可能会有所不同,具体取决于使用的算法:

图 11.2. 进化算法的主要循环
一般演化算法的主体非常简单,可以用几行代码来实现,如这里所示。概括来说,在每次迭代中,直到生成适应度合格的个体为止,会生成新的候选个体并进行评估。这些候选个体是由上一代中适应度最好的个体生成的:
solver = EvolutionaryAlgortihm()
while best_fitness < required_fitness:
candidates = solver.generate_candidates() # for example from crossover and mutation
fitness_values = []
for candidate in candidates:
fitness_values.append(evaluate(candidate))
solver.set_fitness_values(fitness_values)
best_fitness = solver.evaluate_best_candidate()
请注意,求解器的实现细节取决于所使用的算法。
进化算法的应用实际上广泛分布于多个领域和问题,从经济学到生物学,从计算机程序优化到蚁群优化。
由于我们主要关注的是进化算法在解决顺序决策任务中的应用,我们将解释两种最常见的进化算法,它们被用来解决这些类型的任务。它们分别被称为遗传算法(GAs)和进化策略(ESes)。随后,我们将进一步发展进化策略,开发其高度可扩展的版本。
遗传算法
遗传算法的思想非常直接——评估当前的代数,只使用表现最好的个体生成下一代候选解,并丢弃其他个体。如前面所示的图表所示,存活下来的个体通过交叉和变异生成下一代种群。这两个过程在以下图表中表示。交叉是通过在存活个体中选择两个解,并将它们的参数结合在一起进行的。变异则是通过改变后代基因型中的一些随机参数来实现的:

图 11.3. 变异和交叉的视觉示意图
交叉和变异可以采用多种不同的方式。在更简单的版本中,交叉是通过随机选择两个父母的部分进行的,变异则是通过向获得的解添加高斯噪声来实现的,噪声的标准差是固定的。通过只保留最优秀的个体并将它们的基因注入新生个体中,解会随着时间的推移不断改进,直到满足某一条件。然而,在复杂问题中,这种简单的解决方案容易陷入局部最优解(意味着解仅限于一个小的候选解集合)。在这种情况下,通常会选择更先进的遗传算法,如增强拓扑进化网络(NEAT)。NEAT 不仅改变网络的权重,还会改变其结构。
进化策略
进化策略(ESes)比遗传算法(GAs)更简单,因为它们主要基于突变来创建新种群。
突变通过向基因型添加从正态分布中采样的值来执行。进化策略的一个非常简单的版本是通过在整个种群中选择表现最好的个体,并从具有固定标准差且均值等于最佳表现个体的正态分布中采样下一代。
在小规模问题之外,不推荐使用该算法。这是因为仅跟随一个领导者并使用固定的标准差可能会阻止潜在解探索更多样化的搜索空间。因此,该方法的解可能会陷入狭窄的局部最小值。一个直接且更好的策略是通过结合表现最好的候选解并按其适应度排名加权来生成后代。根据适应度值对个体进行排名称为适应度排名。该策略优于使用实际适应度值,因为它对目标函数的变换不变,并且能防止新一代过度偏向潜在的异常值。
CMA-ES
协方差矩阵适应进化策略,简称CMA-ES,是一种进化策略算法。与更简单的进化策略版本不同,它根据多元正态分布来采样新的候选解。CMA 这个名字来源于这样一个事实:变量之间的依赖关系被保存在协方差矩阵中,并通过适应该矩阵来增加或减少下一代的搜索空间。
简而言之,当 CMA-ES 对周围空间有较高的信心时,它通过逐步减少协方差矩阵的值来缩小搜索空间。相反,当 CMA-ES 对空间的信心较低时,它增加协方差矩阵,从而扩大可能的搜索空间。
进化策略与强化学习
进化策略(ESes)是强化学习(RL)的一种有趣替代方法。然而,必须评估其优缺点,以便我们选择正确的方案。让我们简要了解一下进化策略的主要优势:
-
无导数方法:无需反向传播。只需执行前向传递来估算适应度函数(或等效地,累计奖励)。这为所有不可微分的函数打开了大门,例如:硬注意力机制。此外,通过避免反向传播,代码的效率和速度得到了提高。
-
非常通用:进化策略的通用性主要来自于它作为黑盒优化方法的特性。由于我们不关心智能体、其执行的动作或访问的状态,我们可以将这些内容抽象化,只专注于其评估。此外,进化策略可以在没有明确目标的情况下进行学习,并且能在极度稀疏的反馈下工作。进化策略也更为通用,因为它们能够优化更多样化的函数集合。
-
高度可并行且具有鲁棒性:正如我们即将看到的,进化策略比强化学习更容易并行化,计算可以分布到成千上万的工作节点上。进化策略的鲁棒性归功于其所需的少量超参数。例如,与强化学习相比,不需要指定轨迹的长度、lambda 值、折扣因子、跳过的帧数等。另外,进化策略对于具有非常长时间跨度的任务也非常具有吸引力。
另一方面,强化学习在以下几个关键方面更受偏爱:
-
样本效率:强化学习算法更有效地利用从环境中获得的信息,因此它们需要更少的数据和步骤来学习任务。
-
卓越的性能:总体而言,强化学习算法在性能上优于进化策略。
可扩展的进化策略
现在我们已经介绍了黑盒进化算法,特别是进化策略,我们准备将刚刚学到的内容付诸实践。OpenAI 的论文《进化策略作为强化学习的可扩展替代方案》对进化策略作为强化学习算法的替代方案的采用做出了重大贡献。
本文的主要贡献在于一种方法,该方法能够在多个 CPU 上极其有效地扩展进化策略。特别地,这种新方法使用了一种跨 CPU 的新型通信策略,仅涉及标量,因此能够在成千上万的并行工作节点之间进行扩展。
通常,进化策略(ES)需要更多的经验,因此效率低于强化学习(RL)。然而,通过将计算分布到如此多的工作节点(得益于采用这种新策略),任务可以在更少的墙钟时间内解决。例如,在论文中,作者使用 1,440 个 CPU 在仅 10 分钟内解决了 3D 人形行走模式,且 CPU 核心数呈线性加速。由于常规强化学习算法无法达到这种扩展性水平,它们需要几个小时才能解决相同的任务。
让我们看看它们是如何能够如此高效地扩展的。
核心
在论文中,使用了一种版本的进化策略,旨在最大化平均目标值,具体如下:

它通过在种群中搜索,
,并通过
进行随机梯度上升。
是目标函数(或适应度函数),而
是演员的参数。在我们的问题中,
仅仅是代理通过
在环境中获得的随机回报。
种群分布,
,是一个多元高斯分布,均值为!,标准差为!,如图所示:

从这里开始,我们可以通过使用随机梯度估算来定义步长更新,如下所示:

通过这个更新,我们可以使用来自种群的样本结果估算随机梯度(无需执行反向传播)。我们也可以使用一些著名的更新方法,比如 Adam 或 RMSProp,来更新参数。
并行化 ES
很容易看出,如何将 ES 扩展到多个 CPU:每个工作者被分配到种群的一个独立候选解上。评估可以完全自主地进行,正如论文中所描述的,优化可以在每个工作者上并行进行,且每个 CPU 单元之间只共享少量标量。
具体来说,工作者之间唯一共享的信息是一个回合的标量回报,
,以及用于采样的随机种子!。通过仅传输回报,我们可以进一步缩减数据量,但在这种情况下,每个工作者的随机种子必须与其他工作者同步。我们决定采用第一种技术,而论文使用的是第二种。在我们简单的实现中,差异可以忽略不计,且两种技术都需要极低的带宽。
其他技巧
另外有两种技术被用来提高算法的性能:
-
健身塑形 – 目标排名:我们之前讨论过这个技巧。它非常简单。与其直接使用原始回报来计算更新,不如使用排名变换。排名对于目标函数的变换是不可变的,因此在回报差异较大的情况下表现更好。此外,它还去除了异常值的噪声。
-
镜像噪声:这个技巧减少了方差,并且涉及到同时评估带有噪声的网络
和
;也就是说,对于每个个体,我们会有两种变异:
和
。
伪代码
结合所有这些特性的并行化进化策略总结如下伪代码:
---------------------------------------------------------------------------------
Parallelized Evolution Strategy
---------------------------------------------------------------------------------
Initialize parameters on each worker
Initialize random seed on each worker
for do:
for  do:
Sample
Evaluate individuals and
Spread returns to each other worker
for do:
Compute normalized rank from the returns
Reconstruct from the random seeds of the other workers
(maybe using Adam)
现在,剩下的就是实现这个算法了。
可扩展的实现
为了简化实现,并使得并行化版本的 ES 能够在有限的工作者(和 CPU)数量下运行良好,我们将开发一个类似于以下图示的结构。主进程为每个 CPU 核心创建一个工作者,并执行主循环。在每次迭代时,它会等待直到指定数量的新候选个体被工作者评估。与论文中提供的实现不同,每个工作者在每次迭代中会评估多个代理。因此,如果我们有四个 CPU,则会创建四个工作者。然后,如果我们希望在每次主进程迭代时总批量大小大于工作者的数量,比如 40,则每个工作者每次会创建并评估 10 个个体。返回值和种子会被返回给主应用程序,主应用程序会等待所有 40 个个体的结果,然后继续执行后续代码行。
然后,这些结果会以批量的形式传播给所有工作者,工作者分别优化神经网络,按照公式 (11.2) 中提供的更新进行操作:

图 11.4. 展示 ES 并行版本主要组件的示意图
根据我们刚才描述的,代码被分为三个主要部分:
-
创建并管理队列和工作者的主要过程。
-
定义工作者任务的函数。
-
此外,还有一些执行简单任务的函数,例如对回报进行排序和评估代理。
让我们解释一下主进程的代码,以便在深入了解工作者之前,您能够对整个算法有一个大致的了解。
主函数
这是在名为ES的函数中定义的,该函数具有以下参数:Gym 环境的名称,神经网络隐藏层的大小,代数总数,工作者数量,Adam 学习率,批量大小和标准差噪声:
def ES(env_name, hidden_sizes=[8,8], number_iter=1000, num_workers=4, lr=0.01, batch_size=50, std_noise=0.01):
然后,我们设置一个初始种子,在所有工作者之间共享,以初始化参数并使权重保持一致。此外,我们计算每个工作者在每次迭代中需要生成并评估的个体数量,并创建两个 multiprocessing.Queue 队列。这些队列是传递给工作者以及从工作者传回的变量的进出口:
initial_seed = np.random.randint(1e7)
indiv_per_worker = int(batch_size / num_workers)
output_queue = mp.Queue(maxsize=num_workers*indiv_per_worker)
params_queue = mp.Queue(maxsize=num_workers)
接下来,实例化多进程multiprocessing.Process。这些进程将以异步方式运行worker函数,该函数作为第一个参数传递给Process构造函数。传递给worker函数的所有其他变量被分配到args,这些变量与 ES 所接受的参数非常相似,唯一不同的是额外的两个队列。进程在调用start()方法时开始运行:
processes = []
for widx in range(num_workers):
p = mp.Process(target=worker, args=(env_name, initial_seed, hidden_sizes, lr, std_noise, indiv_per_worker, str(widx), params_queue, output_queue))
p.start()
processes.append(p)
一旦并行工作者启动,我们就可以跨代进行迭代,直到所有个体在每个工作者中被生成并单独评估。请记住,每一代生成的个体总数是工作者数量num_workers与每个工作者生成的个体数indiv_per_worker的乘积。这种架构是我们实现的独特之处,因为我们只有四个 CPU 核心,而论文中的实现则利用了成千上万的 CPU。通常,每一代生成的人口数量通常在 20 到 1000 之间:
for n_iter in range(number_iter):
batch_seed = []
batch_return = []
for _ in range(num_workers*indiv_per_worker):
p_rews, p_seed = output_queue.get()
batch_seed.append(p_seed)
batch_return.extend(p_rews)
在前面的代码片段中,output_queue.get()从output_queue获取一个元素,该队列由工作者填充。在我们的实现中,output_queue.get()返回两个元素。第一个元素,p_rews,是使用p_seed生成的智能体的适应度值(即返回值),它作为第二个元素给出。
当for循环终止时,我们对返回值进行排序,并将批量返回值和种子放入params_queue队列中,所有的工作者将读取该队列来优化智能体。代码如下所示:
batch_return = normalized_rank(batch_return)
for _ in range(num_workers):
params_queue.put([batch_return, batch_seed])
最后,当所有训练迭代执行完毕时,我们可以终止工作者:
for p in processes:
p.terminate()
这就完成了主函数。现在,我们需要做的就是实现工作者。
工作者
工作者的功能在worker函数中定义,之前已作为参数传递给mp.Process。我们不能逐行讲解所有代码,因为那会占用太多时间和篇幅,但我们会在这里解释核心部分。和往常一样,完整的实现代码可以在本书的 GitHub 仓库中找到。所以,如果你有兴趣深入了解,可以抽时间查看 GitHub 上的代码。
在worker的前几行中,创建了计算图以运行策略并优化它。具体来说,策略是一个多层感知机,tanh非线性激活函数用于激活层。在这种情况下,Adam 被用来应用根据(11.2)公式第二项计算出的预期梯度。
然后,agent_op(o)和evaluation_on_noise(noise)被定义。前者运行策略(或候选解)以获得给定状态或观测o的行动,后者评估通过将扰动noise(与策略形状相同)添加到当前策略的参数中获得的新候选解。
直接跳到最有趣的部分,我们通过指定最多可以依赖 4 个 CPU 来创建一个新的会话,并初始化全局变量。如果你没有 4 个 CPU 可用,也不用担心。将allow_soft_placement设置为True,会告诉 TensorFlow 只使用受支持的设备:
sess = tf.Session(config=tf.ConfigProto(device_count={'CPU': 4}, allow_soft_placement=True))
sess.run(tf.global_variables_initializer())
尽管使用了所有 4 个 CPU,但我们仅为每个工作线程分配一个。在计算图的定义中,我们设置了计算将在哪个设备上执行。例如,要指定工作线程仅使用 CPU 0,可以将图形放在with语句中,从而定义要使用的设备:
with tf.device("/cpu:0"):
# graph to compute on the CPUs 0
回到我们的实现,我们可以无限循环,或者至少直到工作线程有任务可以处理。这个条件稍后会在while循环内进行检查。
一个重要的注意事项是,因为我们在神经网络的权重上进行了很多计算,所以处理扁平化的权重要容易得多。因此,例如,我们不会处理形如[8,32,32,4]的列表,而是对一个长度为 83232*4 的一维数组进行计算。执行从前者到后者的转换以及反向转换的函数在 TensorFlow 中已经定义(如果你有兴趣了解是如何实现的,可以查看 GitHub 上的完整实现)。
此外,在开始while循环之前,我们检索扁平化代理的形状:
agent_flatten_shape = sess.run(agent_variables_flatten).shape
while True:
在while循环的第一部分,生成并评估候选解。候选解是通过向权重添加正态扰动构建的;也就是说,
。这是通过每次选择一个新的随机种子来完成的,该种子将唯一地从正态分布中采样扰动(或噪声),
。这是算法的关键部分,因为稍后,其他工作线程将必须从相同的种子重新生成相同的扰动。之后,两个新后代(由于我们使用的是镜像采样,所以有两个)将被评估,并将结果放入output_queue队列中:
for _ in range(indiv_per_worker):
seed = np.random.randint(1e7)
with temp_seed(seed):
sampled_noise = np.random.normal(size=agent_flatten_shape)
pos_rew= evaluation_on_noise(sampled_noise)
neg_rew = evaluation_on_noise(-sampled_noise)
output_queue.put([[pos_rew, neg_rew], seed])
请注意,以下代码片段(我们之前使用过的)只是用于在本地设置 NumPy 随机种子seed的一种方式:
with temp_seed(seed):
..
在with语句外,生成随机值所用的种子将不再是seed。
while循环的第二部分涉及获取所有返回值和种子,从这些种子中重建扰动,按照公式(11.2)计算随机梯度估计,并优化策略。params_queue队列由主进程填充,正如我们之前看到的那样。它通过发送在第一阶段由工作线程生成的种群的归一化排名和种子来完成此操作。代码如下:
batch_return, batch_seed = params_queue.get()
batch_noise = []
# reconstruction of the perturbations used to generate the individuals
for seed in batch_seed:
with temp_seed(seed):
sampled_noise = np.random.normal(size=agent_flatten_shape)
batch_noise.append(sampled_noise)
batch_noise.append(-sampled_noise)
# Computation of the gradient estimate following the formula (11.2)
vars_grads = np.zeros(agent_flatten_shape)
for n, r in zip(batch_noise, batch_return):
vars_grads += n * r
vars_grads /= len(batch_noise) * std_noise
sess.run(apply_g, feed_dict={new_weights_ph:-vars_grads})
上述代码中的最后几行计算了梯度估计;也就是说,它们计算了公式(11.2)中的第二项:

这里,
是
和
候选项的归一化排名以及它们的扰动。
apply_g 是应用 vars_grads 梯度(11.3)使用 Adam 的操作。请注意,我们传递 -var_grads,因为我们希望进行梯度上升而不是梯度下降。
实施已完成。现在,我们必须将其应用于环境并进行测试,以查看其表现如何。
应用可扩展 ES 到 LunarLander
可扩展演化策略在 LunarLander 环境中的表现如何?让我们来看看吧!
您可能还记得,我们已经在第六章中对比了 LunarLander 与 A2C 和 REINFORCE,学习随机和 PG 优化。这个任务包括通过连续动作在月球上着陆。我们决定使用这个中等难度的环境来比较 ES 的结果与使用 A2C 获得的结果。
在这个环境中表现最佳的超参数如下:
| 超参数 | 变量名 | 值 |
|---|---|---|
| 神经网络大小 | hidden_sizes |
[32, 32] |
| 训练迭代次数(或代数) | number_iter |
200 |
| 工作人数 | num_workers |
4 |
| Adam 学习率 | lr |
0.02 |
| 每个工作人员的个体数 | indiv_per_worker |
12 |
| 标准差 | std_noise |
0.05 |
结果显示在以下图表中。立即引人注目的是曲线非常稳定平滑。此外,请注意,在约 250-300 万步后,它达到了平均分数约为 200。与在图 6.7 中使用 A2C 获得的结果进行比较,您会发现演化策略比 A2C 和 REINFORCE 多花了近 2-3 倍的步骤。
正如论文所示,通过大规模并行化(至少使用数百个 CPU),您应该能够在短短几分钟内获得非常好的策略。不幸的是,我们没有这样的计算能力。但是,如果您有的话,可以自行尝试:

图 11.5 可扩展演化策略的性能
总体而言,结果非常好,显示出 ES 对于非常长的时间跨度问题和具有非常稀疏奖励的任务是一个可行的解决方案。
摘要
在本章中,你了解了进化算法(EAs),这是一类受生物进化启发的新型黑箱算法,可以应用于强化学习任务。与强化学习相比,进化算法从不同的角度解决这些问题。你看到,在设计强化学习算法时需要处理的许多特性,在进化方法中并不适用。它们的不同之处既体现在内在的优化方法上,也体现在基本假设上。例如,由于进化算法是黑箱算法,我们可以优化任何我们想要的函数,因为我们不再受限于使用可微分函数,这一点与强化学习时有所不同。正如我们在本章中看到的,进化算法有许多其他优点,但它们也有许多缺点。
接下来,我们研究了两种进化算法:遗传算法和进化策略。遗传算法更复杂,因为它通过交叉和变异从两个父母创建后代。进化策略从一个仅通过变异生成的种群中选择表现最好的个体。ES 的简单性是算法能够在成千上万个并行工作者之间实现巨大可扩展性的关键因素之一。OpenAI 的论文中展示了这种可扩展性,证明了 ES 能够在复杂环境中达到与 RL 算法相当的表现。
为了实践进化算法,我们实现了本章引用的论文中的可扩展进化策略。此外,我们还在 LunarLander 上进行了测试,发现 ES 能够以高性能解决该环境。尽管结果很棒,ES 的学习任务比 AC 和 REINFORCE 多用了两到三倍的步骤。这是 ES 的主要缺点:它们需要大量的经验。尽管如此,凭借其线性扩展到工作者数量的能力,在足够的计算能力支持下,你可能能够在比强化学习算法短得多的时间内解决这个任务。
在下一章,我们将回到强化学习,讨论一个被称为探索-利用困境的问题。我们将了解它是什么以及为什么它在在线设置中至关重要。接着,我们将使用一个潜在的解决方案来开发一个元算法,称为 ESBAS,它为每种情况选择最合适的算法。
问题
-
有哪两种替代强化学习的算法可以解决序列决策问题?
-
进化算法中产生新个体的过程是什么?
-
进化算法(如遗传算法)的灵感来源是什么?
-
CMA-ES 如何演化进化策略?
-
进化策略的一个优点和一个缺点是什么?
-
在论文《进化策略作为强化学习的可扩展替代方法》中,使用了什么技巧来减少方差?
深入阅读
-
要阅读 OpenAI 提出的可扩展版本 ES 的原始论文,即Evolution Strategies as a Scalable Alternative to Reinforcement Learning论文,请访问
arxiv.org/pdf/1703.03864.pdf。 -
要阅读提出 NEAT 的论文,即通过增强拓扑演化神经网络,请访问
nn.cs.utexas.edu/downloads/papers/stanley.ec02.pdf。
第十二章:开发 ESBAS 算法
到目前为止,你已经能够以系统化和简洁的方式处理 RL 问题。你能够为手头的问题设计和开发 RL 算法,并从环境中获得最大收益。此外,在前两章中,你学习了超越 RL 的算法,但这些算法也可以用来解决相同的任务。
在本章开始时,我们将提出一个在许多前几章中已经遇到过的困境:即探索与利用困境。我们已经在全书中提出了该困境的潜在解决方案(例如,
-贪心策略),但我们希望为你提供更全面的视角,并简洁地介绍解决该问题的算法。许多算法,如上置信界(UCB)算法,比我们迄今为止使用的简单启发式方法(例如,
-贪心策略)更加复杂和优秀。我们将通过一个经典问题——多臂强盗问题来说明这些策略。尽管它是一个简单的表格游戏,我们将以此为起点,展示这些策略如何应用于非表格型和更复杂的任务。
本文介绍了探索与利用困境,概述了许多最近的强化学习(RL)算法所采用的主要方法,用以解决非常困难的探索环境。我们还将提供一个更广泛的视角,探讨该困境在解决其他类型问题时的适用性。作为证明,我们将开发一个名为时代随机强盗算法选择(ESBAS)的元算法,该算法解决了 RL 背景下的在线算法选择问题。ESBAS 通过利用从多臂强盗问题中出现的思想和策略,选择出最能在每个回合中最大化预期回报的 RL 算法。
本章将涵盖以下主题:
-
探索与利用
-
探索方法
-
时代随机强盗算法选择
探索与利用
探索-利用困境或探索-利用问题影响着许多重要领域。实际上,它不仅限于强化学习的背景,还适用于日常生活。这个困境的核心思想是判断是选择已经知道的最优解,还是值得尝试新的选项。假设你正在买一本新书。你可以选择你最喜欢的作者的书,或者选择亚马逊推荐的同类书籍。在第一种情况下,你对自己选择的书充满信心,但在第二种情况下,你无法预测将会得到什么。然而,后者可能让你感到意外惊喜,读到一本非常好的书,甚至超越了你最喜欢作者写的书。
这种在利用已学知识和冒险探索新选项之间的冲突,在强化学习中也非常常见。智能体可能需要牺牲短期奖励,去探索一个新的空间,以便在未来获得更高的长期奖励。
这一切对你来说可能并不新鲜。事实上,当我们开发第一个强化学习算法时,就开始处理这个问题了。直到现在,我们主要采用了简单的启发式方法,如ε-greedy策略,或遵循随机策略来决定是探索还是利用。经验上,这些策略效果很好,但还有一些其他技术能够实现理论上的最优表现。
本章将从基础讲解探索-利用困境,并介绍一些在表格问题上实现近乎最优表现的探索算法。我们还将展示如何将相同的策略适应于非表格和更复杂的任务。
对于一个强化学习算法,最具挑战性的 Atari 游戏之一是《蒙特祖玛的复仇》,其截图如下。游戏的目标是通过收集宝石和击杀敌人来获得分数。主角需要找到所有的钥匙,以便在迷宫中的房间间穿行,并收集必要的工具来移动,同时避免障碍物。稀疏奖励、长期目标和与最终目标无关的部分奖励,使得这款游戏对每个强化学习算法都非常具有挑战性。事实上,这四个特点使得《蒙特祖玛的复仇》成为测试探索算法的最佳环境之一:

《蒙特祖玛的复仇》截图
让我们从基础开始,以便全面了解这一领域。
多臂老丨虎丨机
多臂老丨虎丨机问题是经典的强化学习问题,用来说明探索-利用的权衡困境。在这个困境中,代理必须从一组固定的资源中选择,以最大化预期奖励。多臂老丨虎丨机这个名称来源于赌徒玩多个老丨虎丨机,每个老丨虎丨机都有来自不同概率分布的随机奖励。赌徒必须学习最佳策略,以获得最高的长期奖励。
这种情况在以下图示中进行了说明。在这个特定的例子中,赌徒(幽灵)需要从五台不同的老丨虎丨机中选择一台,每台老丨虎丨机都有不同且未知的奖励概率,以便赢得最高金额:

五臂老丨虎丨机问题示例
如果你在想多臂老丨虎丨机问题与更有趣的任务(比如《蒙特祖玛的复仇》)有何关系,答案是它们都涉及到决定,在长期来看,当尝试新的行为(拉动新的杠杆)时,是否会获得最高奖励,还是继续做迄今为止做得最好的事情(拉动最知名的杠杆)。然而,多臂老丨虎丨机与《蒙特祖玛的复仇》之间的主要区别在于,后者每次都会改变代理的状态,而在多臂老丨虎丨机问题中,只有一个状态,并且没有顺序结构,这意味着过去的行为不会影响未来。
那么,如何在多臂老丨虎丨机问题中找到探索与利用的正确平衡呢?
探索的方法
简单来说,多臂老丨虎丨机问题,以及一般的每个探索问题,可以通过随机策略或者更智能的技术来解决。属于第一类的最臭名昭著的算法叫做
-贪婪;而乐观探索,如 UCB,以及后验探索,如汤普森采样,属于第二类。在本节中,我们将特别关注
-贪婪和 UCB 策略。
这完全是关于平衡风险与奖励。但我们如何衡量一个探索算法的质量呢?通过后悔。后悔被定义为在一步操作中失去的机会,即在时间
时的后悔,
如下所示:

这里,
表示最优值,
表示
的行动值。
因此,目标是通过最小化所有行动的总后悔,找到探索与利用之间的折衷:

请注意,总遗憾的最小化等同于累积奖励的最大化。我们将利用这一遗憾的概念来展示探索算法的表现。
∈-贪心策略
我们已经扩展了
-贪心策略背后的思想,并将其应用于帮助我们在 Q 学习和 DQN 等算法中的探索。这是一种非常简单的方法,但它在非平凡的任务中也能达到非常高的性能。这是它在许多深度学习算法中广泛应用的主要原因。
为了帮助你回忆,
-贪心大多数时候采取最佳行动,但偶尔会选择一个随机行动。选择随机行动的概率由
值决定,它的范围从 0 到 1。也就是说,具有
的概率,算法会利用最佳行动,而具有
的概率,则会通过随机选择来探索其他可能性。
在多臂强盗问题中,行动值是根据过去的经验估计的,方法是对采取这些行动所获得的奖励进行平均:

在前述方程中,
表示
行动被选中的次数,而
是一个布尔值,表示在时刻
时,是否选择了行动
。然后,强盗会根据
-贪心算法采取行动,或通过选择随机行动来探索,或通过选择
行动来利用较高的
值。
-贪心的一个缺点是它具有期望的线性遗憾。然而,根据大数法则,最优的期望总遗憾应该是与时间步数呈对数关系的。这意味着,
-贪心策略并不是最优的。
达到最优的一种简单方法是使用一个
值,该值随着时间的推移逐渐衰减。通过这样做,探索的总体权重将逐渐消失,最终只会选择贪心行动。实际上,在深度强化学习算法中,
-贪心几乎总是与
的线性或指数衰减相结合。
也就是说,
及其衰减率很难选择,并且还有其他策略可以最优地解决多臂赌博机问题。
UCB 算法
UCB 算法与一种称为“面对不确定性时的乐观原则”(optimism in the face of uncertainty)有关,这是一种基于大数法则的统计学原理。UCB 构建了一个乐观的猜测,基于奖励的样本均值,并根据奖励的上置信界估算。乐观的猜测决定了每个动作的期望回报,同时考虑了动作的不确定性。因此,UCB 始终能够通过平衡风险与回报,选择潜在回报更高的动作。然后,当当前动作的乐观估算低于其他动作时,算法会切换到另一个动作。
具体来说,UCB 通过
跟踪每个动作的平均奖励,以及每个动作的
UCB(因此得名)。然后,算法选择最大化以下内容的动作:
(12.1)
在这个公式中,
的作用是提供一个附加的参数,作为考虑动作不确定性的平均奖励。
UCB1
UCB1 属于 UCB 系列,其贡献在于选择
。
在 UCB1 中,UCB 通过跟踪某个动作(
)被选择的次数,以及
和
这两个量的总和来计算,如下式所示:
(12.2)
一个动作的不确定性与它被选择的次数有关。如果你仔细想想,这也能解释清楚,因为根据大数法则,在无限次数的试验下,你可以对期望值有一个明确的了解。相反,如果你只尝试过某个动作几次,你会对期望的奖励感到不确定,只有通过更多的经验,你才能判断它是好是坏。因此,我们会激励探索那些只被选择过少数几次的动作,因为它们的不确定性较高。重点是,如果
很小,意味着这个动作只是偶尔被尝试,那么
会很大,从而带来较高的不确定估计。然而,如果
很大,那么
就会很小,估计值也会更准确。然后,我们只会在
具有较高的平均奖励时才选择它。
与
-贪婪算法相比,UCB 的主要优势实际上在于对动作进行计数。事实上,多臂老丨虎丨机问题可以通过此方法轻松解决,方法是为每个被采取的动作保持一个计数器,以及它的平均奖励。这两个信息可以集成到公式(12.1)和公式(12.2)中,从而获得在时刻(
)应该采取的最佳动作;即:
(12.3)
UCB 是一个非常强大的探索方法,它在多臂老丨虎丨机问题上实现了对数期望总遗憾,从而达到了最优趋势。值得注意的是,
-贪婪探索也可以获得对数遗憾,但它需要精心设计,并且需要细调指数衰减,因此在平衡上会更加困难。
UCB 还有一些变体,如 UCB2、UCB-Tuned 和 KL-UCB。
探索复杂性
我们已经看到,UCB,特别是 UCB1,能够通过相对简单的算法降低总体的遗憾,并在多臂老丨虎丨机问题上实现最优收敛。然而,这仍然是一个简单的无状态任务。
那么,UCB 在更复杂的任务上表现如何呢?为了回答这个问题,我们可以将问题过于简化,并将所有问题分为以下三大类:
-
无状态问题:这些问题的一个实例就是多臂老丨虎丨机问题。在这种情况下,探索可以通过更复杂的算法来处理,如 UCB1。
-
中小型表格问题:作为基本规则,探索仍然可以通过更先进的机制来处理,但在某些情况下,总体收益较小,不值得增加额外的复杂性。
-
大型非表格化问题:我们现在处于更加复杂的环境中。在这些环境中,前景尚不明确,研究人员仍在积极努力寻找最佳的探索策略。其原因在于,随着复杂性的增加,像 UCB 这样的最优方法变得难以处理。例如,UCB 无法处理具有连续状态的问题。然而,我们不必抛弃一切,可以借鉴在多臂老丨虎丨机问题中研究的探索算法。也就是说,有许多方法能够逼近最优的探索策略,并且在连续环境中也能很好地发挥作用。例如,基于计数的方法,如 UCB,已经通过为相似状态提供相似的计数来适应了无限状态问题。这类算法在一些极其复杂的环境中也能显著提升性能,例如在《蒙特祖马的复仇》游戏中。尽管如此,在大多数强化学习应用场景中,这些更复杂方法所带来的额外复杂性是得不偿失的,简单的随机策略,如
-贪婪策略,通常就足够了。
值得注意的是,尽管我们仅概述了基于计数的探索方法,如 UCB1,但实际上还有两种更复杂的方式可以处理探索问题,它们能在遗憾值上实现最优结果。第一种方法叫做后验采样(其中一个例子是汤普森采样),它基于后验分布;第二种方法叫做信息增益,它依赖于通过估计熵来测量不确定性。
时代随机带算法选择
探索策略在强化学习中的主要用途是帮助智能体进行环境探索。我们在 DQN 中看到了这种用例,使用了
-贪婪策略,而在其他算法中则通过向策略中注入额外的噪声来实现。然而,探索策略的使用方式不仅限于此。为了更好地理解到目前为止介绍的探索概念,并引入这些算法的另一种应用场景,我们将展示并开发一种名为 ESBAS 的算法。该算法首次出现在论文《强化学习算法选择》中。
ESBAS 是一种在线算法选择(AS)的元算法,适用于强化学习的上下文。它利用探索方法来选择在整个轨迹中使用的最佳算法,以最大化预期奖励。
为了更好地解释 ESBAS,我们将首先解释什么是算法选择,以及它在机器学习和强化学习中的应用。接着,我们将重点介绍 ESBAS,详细描述其内部工作原理,并提供其伪代码。最后,我们将实现 ESBAS 并在一个名为 Acrobot 的环境中进行测试。
算法选择的开箱操作
为了更好地理解 ESBAS 的作用,我们首先来关注算法选择(AS)是什么。在普通设置中,针对给定任务开发并训练一个特定且固定的算法。问题在于,如果数据集随着时间变化,数据集发生过拟合,或者在某些限制性情境下另一个算法表现更好,就无法改变算法。选择的算法将永远保持不变。算法选择的任务就是解决这个问题。
AS(算法选择)是机器学习中的一个开放问题。它涉及设计一个叫做元算法的算法,该算法始终从一个不同选项的池中(称为投资组合)选择最适合当前需求的算法。以下图示展示了这一过程。AS 的假设是,投资组合中的不同算法在问题空间的不同部分会优于其他算法。因此,拥有具有互补能力的算法非常重要。
例如,在下面的图示中,元算法从可用的算法(或代理)中选择哪一个(如 PPO 和 TD3)将在给定时刻作用于环境。这些算法不是互补的,而是每个算法提供不同的优势,元算法可以选择其中一个,以便在特定情境中表现得更好:

强化学习中算法选择方法的表示
例如,如果任务是设计一个能够在各种地形上行驶的自动驾驶汽车,那么训练一个能够在道路、沙漠和冰面上表现出色的算法可能会很有用。然后,AS 可以智能地选择在每种情境下使用这三种版本中的哪一个。例如,AS 可能会发现,在雨天,训练过的冰面策略比其他策略更有效。
在强化学习(RL)中,策略变化的频率非常高,且数据集随时间不断增加。这意味着在起始点(代理处于初步状态)与代理处于高级状态时,最佳神经网络大小和学习率可能存在很大差异。例如,代理可能在学习初期使用较高的学习率,并随着经验的积累逐渐降低学习率。这突出显示了强化学习是一个非常有趣的算法选择平台。因此,这正是我们将在其中测试 AS 的地方。
ESBAS 的底层原理
提出 ESBAS 的论文,在批处理和在线设置中对算法进行了测试。然而,在本章的剩余部分,我们将主要集中讨论前者。这两个算法非常相似,如果你对纯在线版本感兴趣,可以在论文中找到进一步的解释。在真正的在线设置中,AS 被重新命名为滑动随机带算法(SSBAS),它从最新的选择的滑动窗口中学习。但我们先从基础开始。
关于 ESBAS,首先要说的是它基于 UCB1 策略,并使用这种带算法选择离策略算法,从固定的投资组合中进行选择。具体来说,ESBAS 可以分为三个主要部分,工作原理如下:
-
它循环遍历多个指数大小的 epoch。在每个 epoch 中,它首先做的是更新投资组合中所有可用的离策略算法。它使用的是迄今为止收集的数据(在第一个 epoch 时,数据集将为空)。另外,它还会重置元算法。
-
然后,在每个 epoch 中,元算法根据公式(12.3)计算乐观猜测,以选择将控制下一个轨迹的离策略算法(在投资组合中),从而最小化总的遗憾。然后,使用该算法运行轨迹。同时,轨迹中的所有转换都会被收集,并添加到数据集,这些数据稍后将由离策略算法用来训练策略。
-
当轨迹结束时,元算法使用从环境中获得的 RL 回报更新该特定离策略算法的平均奖励,并增加出现次数。平均奖励和出现次数将由 UCB1 用来计算 UCB,如公式(12.2)所示。这些值用于选择下一个离策略算法,该算法将执行下一个轨迹。
为了让你更好地理解算法,我们还在代码块中提供了 ESBAS 的伪代码,见下方:
---------------------------------------------------------------------------------
ESBAS
---------------------------------------------------------------------------------
Initialize policy for every algorithm in the portfolio
Initialize empty dataset
for do
for in do
Learn policy on with algortihm
Initialize AS variables: and for every :
for do > Select the best algorithm according to UCB1
Generate trajectory with policy and add transitions to
> Update the average return and the counter of
(12.4)
这里,
是一个超参数,
是在
轨迹中获得的 RL 回报,
是算法
的计数器,
是其平均回报。
正如论文中所解释的,在线 AS 解决了四个来自 RL 算法的实际问题:
-
样本效率:策略的多样化提供了额外的信息源,使 ESBAS 具有样本效率。此外,它结合了课程学习和集成学习的特性。
-
鲁棒性:投资组合的多样化提供了对不良算法的鲁棒性。
-
收敛性:ESBAS 保证最小化后悔值。
-
课程学习:AS 能够提供一种课程策略,例如一开始选择较简单、浅层的模型,最后选择较深的模型。
实现
实现 ESBAS 很简单,因为它只需要添加几个组件。最关键的部分在于对投资组合的离策略算法的定义和优化。对于这些,ESBAS 并不限制算法的选择。本文中使用了 Q-learning 和 DQN。我们决定使用 DQN,以提供一个能够处理更复杂任务的算法,并且可以与具有 RGB 状态空间的环境一起使用。我们在第五章中详细讲解了 DQN,深度 Q 网络,对于 ESBAS,我们将使用相同的实现。
在开始实现之前,我们需要指定投资组合的构成。我们创建了一个多样化的投资组合,就神经网络架构而言,但你也可以尝试其他组合。例如,你可以将投资组合与具有不同学习率的 DQN 算法组合。
实现分为如下几个部分:
-
DQN_optimization类构建计算图,并使用 DQN 优化策略。 -
UCB1类定义了 UCB1 算法。 -
ESBAS函数实现 ESBAS 的主要流程。
我们将提供最后两点的实现,但你可以在书的 GitHub 仓库中找到完整的实现:github.com/PacktPublishing/Reinforcement-Learning-Algorithms-with-Python.
让我们首先了解ESBAS(..)。除了 DQN 的超参数外,还有一个额外的xi参数,代表
超参数。ESBAS函数的主要结构与之前给出的伪代码相同,因此我们可以快速浏览一遍。
在定义了所有参数的函数后,我们可以重置 TensorFlow 的默认图,并创建两个 Gym 环境(一个用于训练,另一个用于测试)。然后,我们可以通过为每个神经网络大小实例化一个DQN_optimization对象并将它们添加到列表中来创建投资组合:
def ESBAS(env_name, hidden_sizes=[32], lr=1e-2, num_epochs=2000, buffer_size=100000, discount=0.99, render_cycle=100, update_target_net=1000, batch_size=64, update_freq=4, min_buffer_size=5000, test_frequency=20, start_explor=1, end_explor=0.1, explor_steps=100000, xi=16000):
tf.reset_default_graph()
env = gym.make(env_name)
env_test = gym.wrappers.Monitor(gym.make(env_name), "VIDEOS/TEST_VIDEOS"+env_name+str(current_milli_time()),force=True, video_callable=lambda x: x%20==0)
dqns = []
for l in hidden_sizes:
dqns.append(DQN_optimization(env.observation_space.shape, env.action_space.n, l, lr, discount))
现在,我们定义一个内部函数DQNs_update,它以 DQN 的方式训练投资组合中的策略。请注意,投资组合中的所有算法都是 DQN,它们唯一的区别在于神经网络的大小。优化通过DQN_optimization类的optimize和update_target_network方法完成:
def DQNs_update(step_counter):
if len(buffer) > min_buffer_size and (step_counter % update_freq == 0):
mb_obs, mb_rew, mb_act, mb_obs2, mb_done = buffer.sample_minibatch(batch_size)
for dqn in dqns:
dqn.optimize(mb_obs, mb_rew, mb_act, mb_obs2, mb_done)
if len(buffer) > min_buffer_size and (step_counter % update_target_net == 0):
for dqn in dqns:
dqn.update_target_network()
一如既往,我们需要初始化一些(不言自明的)变量:重置环境,实例化ExperienceBuffer对象(使用我们在其他章节中使用的相同类),并设置探索衰减:
step_count = 0
batch_rew = []
episode = 0
beta = 1
buffer = ExperienceBuffer(buffer_size)
obs = env.reset()
eps = start_explor
eps_decay = (start_explor - end_explor) / explor_steps
我们终于可以开始遍历各个时期的循环了。至于前面的伪代码,在每个时期,以下事情会发生:
-
策略在经验缓冲区上进行训练
-
轨迹由 UCB1 选择的策略运行
第一步是通过调用我们之前定义的DQNs_update来完成的,整个时期的长度(具有指数长度):
for ep in range(num_epochs):
# policies training
for i in range(2**(beta-1), 2**beta):
DQNs_update(i)
关于第二步,在轨迹运行之前,实例化并初始化了UCB1类的新对象。然后,一个while循环遍历指数大小的回合,其中,UCB1对象选择运行下一条轨迹的算法。在轨迹过程中,动作由dqns[best_dqn]选择:
ucb1 = UCB1(dqns, xi)
list_bests = []
beta += 1
ep_rew = []
while step_count < 2**beta:
best_dqn = ucb1.choose_algorithm()
list_bests.append(best_dqn)
g_rew = 0
done = False
while not done:
# Epsilon decay
if eps > end_explor:
eps -= eps_decay
act = eps_greedy(np.squeeze(dqns[best_dqn].act(obs)), eps=eps)
obs2, rew, done, _ = env.step(act)
buffer.add(obs, rew, act, obs2, done)
obs = obs2
g_rew += rew
step_count += 1
每次回合后,ucb1会根据上次轨迹获得的强化学习回报进行更新。此外,环境被重置,当前轨迹的奖励被追加到列表中,以便跟踪所有奖励:
ucb1.update(best_dqn, g_rew)
obs = env.reset()
ep_rew.append(g_rew)
g_rew = 0
episode += 1
这就是ESBAS函数的全部内容。
UCB1由一个构造函数组成,该构造函数初始化计算所需的属性(见 12.3);一个choose_algorithm()方法,返回当前投资组合中最佳的算法(如 12.3 所示);以及update(idx_algo, traj_return),它使用获得的最后一个奖励更新idx_algo算法的平均奖励,如 12.4 所理解的那样。代码如下:
class UCB1:
def __init__(self, algos, epsilon):
self.n = 0
self.epsilon = epsilon
self.algos = algos
self.nk = np.zeros(len(algos))
self.xk = np.zeros(len(algos))
def choose_algorithm(self):
return np.argmax([self.xk[i] + np.sqrt(self.epsilon * np.log(self.n) / self.nk[i]) for i in range(len(self.algos))])
def update(self, idx_algo, traj_return):
self.xk[idx_algo] = (self.nk[idx_algo] * self.xk[idx_algo] + traj_return) / (self.nk[idx_algo] + 1)
self.nk[idx_algo] += 1
self.n += 1
有了代码,我们现在可以在一个环境中进行测试,并查看它的表现。
解决 Acrobot
我们将在另一个 Gym 环境——Acrobot-v1上测试 ESBAS。正如 OpenAI Gym 文档中所描述的,Acrobot 系统包括两个关节和两个链节,其中两个链节之间的关节是有驱动的。最初,链节垂直挂着,目标是将下链节的末端摆动到给定的高度。下图显示了 Acrobot 在简短的时间步长序列中的运动,从起始位置到结束位置:

Acrobot 运动序列
投资组合包含三种不同大小的深度神经网络。一种小型神经网络,只有一个大小为 64 的隐藏层;一种中型神经网络,具有两个大小为 16 的隐藏层;以及一种大型神经网络,具有两个大小为 64 的隐藏层。此外,我们设置了超参数!(使用的值与论文中的一致)。
结果
以下图表显示了结果。该图展示了 ESBAS 的学习曲线:较暗的阴影部分表示完整的组合(包括之前列出的三种神经网络);较浅的橙色部分则表示 ESBAS 仅使用一个表现最好的神经网络(一个深度神经网络,具有两个隐藏层,每层大小为 64)的学习曲线。我们知道,ESBAS 仅使用一种算法的组合并不能真正发挥元算法的潜力,但我们引入它是为了提供一个基准,以便对比结果。图表本身说明了问题,显示了蓝色线始终高于橙色线,从而证明 ESBAS 实际上选择了最佳可用选项。这个不寻常的形状是因为我们正在离线训练 DQN 算法:

ESBAS 在使用三种算法的组合时以较深的阴影显示,而仅使用一种算法时则以较浅的阴影显示。
有关本章中提到的所有颜色参考,请参见www.packtpub.com/sites/default/files/downloads/9781789131116_ColorImages.pdf中的彩色图像包。
此外,您在训练开始时以及在约 20K、65K 和 131K 步时看到的尖峰,是策略训练和元算法重置的时刻。
我们现在可以问自己,ESBAS 在何时优先选择某个算法,而非其他算法。答案可以从以下图表中看到。在此图中,小型神经网络以值 0 表示,中型以值 1 表示,大型以值 2 表示。点表示每条轨迹上选择的算法。我们可以看到,在一开始,大型神经网络被优先选择,但随后立即转向中型神经网络,再到小型神经网络。大约 64K 步之后,元算法又切换回较大的神经网络:

该图展示了元算法的偏好。
从前面的图中,我们还可以看到,两个 ESBAS 版本最终都收敛到相同的值,但收敛速度非常不同。事实上,利用 AS 的真正潜力的 ESBAS 版本(即包含三种算法的组合)收敛得要快得多。两者都收敛到相同的值,因为从长远来看,最佳的神经网络是 ESBAS 版本中使用的那个单一选项(即深度神经网络,具有两个隐藏层,每层大小为 64)。
总结
在本章中,我们讨论了探索与利用的困境。这个问题在之前的章节中已经有所提及,但当时只是以一种简单的方式,通过采用简单的策略进行探讨。本章中,我们更加深入地研究了这个困境,从著名的多臂老丨虎丨机问题开始。我们看到,更加复杂的基于计数的算法,如 UCB,实际上能够达到最优表现,并且具有期望的对数后悔值。
然后,我们使用了探索算法来解决 AS。AS 是探索性算法的一种有趣应用,因为元算法需要选择最适合当前任务的算法。AS 在强化学习中也有应用。例如,AS 可以用来选择在不同算法组合中训练出的最佳策略,以执行下一条轨迹。这也是 ESBAS 的作用。它通过采用 UCB1 来解决在线选择离策略强化学习算法的问题。我们深入研究并实现了 ESBAS。
现在,你已经掌握了设计和开发高性能强化学习算法所需的所有知识,这些算法能够平衡探索与利用。此外,在之前的章节中,你已经获得了必要的技能,能理解在许多不同的环境中应该使用哪个算法。然而,直到现在,我们还忽视了一些更高级的强化学习话题和问题。在下一章也是最后一章,我们将填补这些空白,讨论无监督学习、内在动机、强化学习的挑战以及如何提高算法的鲁棒性。我们还将看到如何利用迁移学习从仿真环境转向现实世界。此外,我们还会给出一些关于训练和调试深度强化学习算法的额外建议和最佳实践。
问题
-
什么是探索与利用的困境?
-
我们在之前的强化学习算法中使用过的两种探索策略是什么?
-
什么是 UCB?
-
哪个问题更难解决:Montezuma 的复仇还是多臂老丨虎丨机问题?
-
ESBAS 是如何解决在线强化学习算法选择问题的?
进一步阅读
-
要了解关于多臂老丨虎丨机问题的更全面的调查,请阅读在线实验设计的调查与随机多臂老丨虎丨机:
arxiv.org/pdf/1510.00757.pdf -
要阅读利用内在动机来玩 Montezuma 的复仇的论文,请参阅统一基于计数的探索与内在动机:
arxiv.org/pdf/1606.01868.pdf -
要阅读原始的 ESBAS 论文,请访问此链接:
arxiv.org/pdf/1701.08810.pdf。
第十三章:解决强化学习挑战的实用实现
本章中,我们将总结前几章中解释的深度强化学习(深度 RL)算法的部分概念,以便给你一个广泛的视角来了解它们的应用,并为选择最适合某一问题的算法制定一个通用的规则。此外,我们还将提出一些指导方针,帮助你开始开发自己的深度 RL 算法。这些指导方针展示了从开发开始时需要采取的步骤,使你可以轻松地进行实验,而不至于在调试过程中浪费太多时间。在同一节中,我们还列出了最重要的超参数调优以及需要处理的额外标准化过程。
接下来,我们将通过解决稳定性、效率和泛化等问题,探讨这一领域的主要挑战。我们将以这三个主要问题为转折点,过渡到更先进的强化学习技术,如无监督强化学习和迁移学习。无监督强化学习和迁移学习对于部署和解决要求高的强化学习任务至关重要,因为它们是解决我们之前提到的三个挑战的技术。
我们还将探讨如何将强化学习应用于实际问题,以及强化学习算法如何在弥合仿真与现实世界之间的差距方面发挥作用。
为了总结本章及整本书,我们将从技术和社会角度讨论强化学习的未来。
本章将涵盖以下主题:
-
深度强化学习的最佳实践
-
深度强化学习的挑战
-
高级技术
-
强化学习在现实世界中的应用
-
强化学习的未来及其对社会的影响
深度强化学习的最佳实践
在整本书中,我们介绍了许多强化学习算法,其中一些仅是升级版(例如 TD3、A2C 等),而其他一些则与其他算法有根本性的不同(例如 TRPO 和 DPG),并提出了达成相同目标的替代方式。此外,我们还探讨了非强化学习优化算法,如模仿学习和进化策略,以解决序列决策任务。所有这些替代方案可能会让你感到困惑,你可能不确定哪种算法最适合某一特定问题。如果是这种情况,别担心,我们现在将介绍一些规则,帮助你决定在给定任务中使用最合适的算法。
此外,如果你在本书中实现了我们介绍的一些算法,可能会发现很难将所有部分整合起来,使算法正常工作。深度强化学习算法因调试和训练难度大而著称,而且训练时间非常长。因此,整个训练过程非常缓慢且艰难。幸运的是,有一些策略可以帮助你避免在开发深度强化学习算法时遇到一些可怕的头痛问题。但在讨论这些策略之前,我们先来看看如何选择合适的算法。
选择合适的算法
区分不同类型的强化学习(RL)算法的主要驱动力是样本效率和训练时间。
我们将样本效率视为一个智能体在学习任务时与环境交互的次数。我们提供的数字是算法效率的一个指标,是相对于其他算法在典型环境中的表现进行衡量的。
显然,还有其他参数会影响算法选择,但通常这些参数的影响较小,重要性较低。举个例子,其他需要评估的参数包括 CPU 和 GPU 的可用性、奖励函数的类型、可扩展性、算法的复杂度以及环境的复杂度。
在本次比较中,我们将考虑无梯度黑箱算法(如进化策略)、基于模型的 RL 算法(如 DAgger)以及无模型的 RL 算法。在后者中,我们将区分策略梯度算法(如 DDPG 和 TRPO)和基于价值的算法(如 DQN)。
以下图表展示了这四类算法的数据效率(注意,最左侧的方法在样本效率上低于最右侧的方法)。特别地,随着图表向右移动,算法的效率逐渐增加。因此,你可以看到,无梯度方法需要从环境中获取更多的数据点,其次是策略梯度方法、基于价值的方法,最后是基于模型的 RL,它们是最具样本效率的。

图 13.1:基于模型的 RL 方法、策略梯度算法、基于价值的算法和无梯度算法的样本效率比较(最左侧的方法效率低于最右侧的方法)。
相反,这些算法的训练时间与样本效率呈反比关系。这个关系在以下图表中总结(注意,最左侧的方法训练速度慢于最右侧的方法)。我们可以看到,基于模型的算法的训练速度远低于基于价值的算法,几乎慢了五倍,而基于策略梯度的算法则比无梯度方法慢约5 倍。
请注意,这些数字只是为了突出平均情况,训练时间仅与算法的训练速度相关,而与从环境中获取新过渡所需的时间无关:

图 13.2. 模型基础 RL 方法、策略梯度算法、值基算法和无梯度算法的训练时间效率比较(最左侧的方法训练速度比最右侧的慢)
我们可以看到,算法的样本效率与其训练时间是互补的,这意味着一个数据高效的算法训练起来很慢,反之亦然。因此,由于智能体的总体学习时间既考虑到训练时间,也考虑到环境的速度,你需要在样本效率和训练时间之间找到一个平衡点,以满足你的需求。事实上,基于模型的算法和更高效的无模型算法的主要目的是减少与环境的交互步骤,使得这些算法在实际世界中更容易部署和训练,因为实际世界中的交互速度通常比模拟器中的慢。
从零到一
一旦你定义了最适合你需求的算法,无论它是一个广为人知的算法还是一个新的算法,你就需要开发它。正如本书所示,强化学习算法与监督学习算法没有太多共同之处。出于这个原因,有一些不同的方面值得注意,以便于调试、实验和调整算法:
-
从简单问题开始:最初,你可能会想尽快尝试一个可行版本的代码。然而,建议你逐步进行,逐渐增加环境的复杂度。这将大大帮助减少整体的训练和调试时间。让我举个例子。如果你需要一个离散环境,可以从 CartPole-v1 开始;如果需要一个连续环境,可以选择 RoboschoolInvertedPendulum-v1。接着,你可以进入一个中等复杂度的环境,例如 RoboschoolHopper-v1、LunarLander-v2,或者一个带有 RGB 图像的相关环境。在这个阶段,你应该已经拥有一份没有 bug 的代码,最终可以在你的最终任务上进行训练和调整。此外,你应该尽可能熟悉这些简单的任务,这样如果出现问题,你能够知道应该查找哪些方面。
-
训练很慢:训练深度强化学习算法需要时间,学习曲线可能呈现任何形状。正如我们在前几章中看到的,学习曲线(即相对于步数的轨迹累积奖励)可能类似于对数函数、双曲正切函数(如下图所示),或更复杂的函数。可能的形状取决于奖励函数、其稀疏性和环境的复杂性。如果你正在处理一个新环境,并且不知道会有什么结果,建议你保持耐心,直到你确定进展已停止为止。同时,训练过程中不要过于关注图表。
-
开发一些基准线:对于新任务,建议至少开发两个基准线,以便与它们比较你的算法。一个基准线可以是一个随机代理,另一个基准线可以是像 REINFORCE 或 A2C 这样的算法。这些基准线可以用作性能和效率的下限。
-
图表和直方图:为了监控算法的进展并帮助调试阶段,一个重要的因素是绘制并显示关键参数的直方图,如损失函数、累积奖励、动作(如果可能)、轨迹的长度、KL 惩罚、熵和价值函数。除了绘制均值外,你还可以添加最小值、最大值和标准差。在本书中,我们主要使用 TensorBoard 来可视化这些信息,但你可以使用任何你喜欢的工具。
-
使用多个种子:深度强化学习在神经网络和环境中都嵌入了随机性,这通常导致不同运行之间的结果不一致。因此,为了确保一致性和稳定性,最好使用多个随机种子。
-
归一化:根据环境的设计,归一化奖励、优势和观测值可能会有所帮助。优势值(例如,在 TRPO 和 PPO 中)可以在一个批次中进行归一化,使其均值为 0,标准差为 1。此外,观测值可以使用一组初始随机步骤进行归一化。奖励则可以通过折扣或未折扣奖励的均值和标准差的运行估计来归一化。
-
超参数调优:超参数根据算法的类别和类型变化很大。例如,基于值的方法与策略梯度方法相比有多个不同的超参数,但这些类别的实例,如 TRPO 和 PPO,也有许多独特的超参数。也就是说,对于本书中介绍的每一个算法,我们都指定了所使用的超参数以及最重要的调优参数。在这些参数中,至少有两个是所有强化学习算法都会用到的:学习率和折扣因子。前者在监督学习中稍微不那么重要,但仍然是调优的首要参数之一,因为它决定了我们是否能够得到一个有效的算法。折扣因子是强化学习算法特有的。引入折扣因子可能会引入偏差,因为它会修改目标函数。然而,在实际应用中,它能够生成更好的策略。因此,从某种程度上来说,时间跨度越短越好,因为它可以减少不稳定性:

图 13.3. 对数函数和双曲正切函数的示例
关于本章中提到的所有颜色参考,请参阅www.packtpub.com/sites/default/files/downloads/9781789131116_ColorImages.pdf中的彩色图像包。
采用这些技术后,你将能够更轻松地训练、开发和部署算法。此外,你将拥有更加稳定和健壮的算法。
对深度强化学习的缺点有一个批判性的视角和理解,是推动强化学习算法发展的关键因素,这样才能设计出更好的最先进算法。在接下来的部分中,我们将以更简洁的方式呈现深度强化学习的主要挑战。
深度强化学习中的挑战
近年来,在强化学习算法的研究中投入的努力巨大。特别是深度神经网络作为函数逼近方法引入之后,进展和成果都非常突出。然而,一些主要问题仍未解决,这些问题限制了强化学习算法在更广泛和有趣任务中的应用。我们在这里讨论的是稳定性、可重复性、效率和泛化性问题,尽管可扩展性和探索问题也可以列入此清单。
稳定性和可重复性
稳定性和可重复性在某种程度上是相互关联的,因为目标是设计一个能够在多次运行中保持一致的算法,并且不会对微小的调整产生过大敏感性。例如,算法不应该对超参数值的变化过于敏感。
使深度强化学习算法难以复制的主要因素,源自深度神经网络的固有特性。这主要是因为深度神经网络的随机初始化和优化过程的随机性。此外,在强化学习中,这种情况因环境的随机性而加剧。综合来看,这些因素也不利于结果的可解释性。
稳定性还面临强化学习(RL)算法高不稳定性的考验,就像我们在 Q-learning 和 REINFORCE 中看到的那样。例如,在基于值的算法中,没有任何收敛的保证,算法会受到高偏差和不稳定性的困扰。DQN 使用了许多技巧来稳定学习过程,比如经验回放和目标网络更新的延迟。尽管这些策略可以缓解不稳定性问题,但它们并未完全消除这些问题。
为了克服算法在稳定性和可重复性方面的固有限制,我们需要在算法之外进行干预。为此,可以采用许多不同的基准测试和一些经验法则,以确保较高的可重复性和结果一致性。具体方法如下:
-
如果可能,应该在多个相似的环境中测试算法。例如,可以在 Roboschool 或 Atari Gym 等环境集上测试,其中任务在动作和状态空间上相似,但目标不同。
-
运行多个不同随机种子的实验。通过改变种子,结果可能会有显著差异。以下图示为例,展示了相同超参数的算法在不同种子下的两次运行,你可以看到差异很大。因此,根据你的目标,使用多个随机种子(一般为三到五个)可能会有帮助。例如,在学术论文中,通常的做法是对五次运行的结果进行平均,并考虑标准差。
-
如果结果不稳定,考虑使用更稳定的算法或采取进一步的策略。同时,请记住,超参数的变化对不同算法和环境的影响可能会有显著差异:

图 13.4. 两次相同算法在不同随机种子下的性能对比
效率
在上一节选择合适的算法中,我们看到算法之间的样本效率差异很大。此外,从前几章我们了解到,更高效的方法,如基于值的学习,仍然需要大量的环境交互才能学习。也许只有基于模型的强化学习才能解决数据匮乏的问题。不幸的是,基于模型的方法也有其他缺点,比如较低的性能上限。
因此,已经构建了混合的基于模型和无模型的方法。然而,这些方法在工程实现上较为困难,且不适用于现实世界中的问题。正如你所看到的,效率相关的问题非常难以解决,但同时也非常重要,需要解决这些问题才能将强化学习方法应用于现实世界。
有两种替代方法可以处理像物理世界这样非常缓慢的环境。一种方法是在最初使用低保真度的模拟器,然后在最终环境中微调智能体。另一种方法是直接在最终环境中训练智能体,但需要转移一些先前的相关知识,以避免从头开始学习任务。这就像是在你已经训练了自己的感官系统后学习驾驶。无论哪种方式,因为我们是在将知识从一个环境转移到另一个环境,所以我们讨论了一种叫做迁移学习的方法。我们将在高级技术部分进一步详细阐述这种方法。
泛化
泛化的概念涉及两个不同但又相关的方面。一般而言,强化学习中的泛化概念指的是算法在相关环境中获得良好表现的能力。例如,如果一个智能体已经接受了在泥泞道路上行走的训练,我们可能会期望该智能体在铺设的道路上表现良好。泛化能力越强,智能体在不同环境中的表现就越好。第二种较少使用的泛化方式是指算法在只能收集有限数据的环境中取得良好表现的特性。
在强化学习中,智能体可以自行选择要访问的状态,并且可以任意停留在这些状态上,从而可能会在某个特定问题空间上过拟合。然而,如果需要良好的泛化能力,就必须找到一种折中方法。如果允许智能体为环境收集潜在的无限数据,这种做法只在某种程度上是有效的,因为它会作为一种自我正则化方法。
尽管如此,为了帮助智能体在其他环境中实现泛化,它必须具备抽象推理的能力,从简单的状态-动作映射中辨别出任务,并使用多个因素来解释任务。抽象推理的例子可以在基于模型的强化学习、迁移学习以及使用辅助任务中找到。我们稍后会介绍后者,但简而言之,这是一种通过将辅助任务与主任务共同学习来改善泛化和样本效率的技术。
高级技术
我们之前列出的挑战没有简单的解决方案。然而,已经有努力在尝试克服这些挑战,并提出新的策略来提高效率、泛化能力和稳定性。专注于效率和泛化的两种最广泛且有前景的技术是无监督强化学习和迁移学习。在大多数情况下,这些策略与我们在前几章中开发的深度强化学习算法是相辅相成的。
无监督强化学习
无监督强化学习与常规的无监督学习相关,因为两者都不使用任何监督来源。在无监督学习中,数据没有标签,而在强化学习的对等体中,奖励并没有给出。也就是说,在给定一个动作时,环境只返回下一个状态。奖励和完成状态都被移除。
无监督强化学习(Unsupervised RL)在许多场景中都有帮助,例如,当用手工设计的奖励对环境进行标注不可扩展,或者当一个环境可以服务于多个任务时。在后一种情况下,可以采用无监督学习,以便我们能够了解环境的动态。能够从无监督来源中学习的方法,也可以作为在奖励非常稀疏的环境中额外的信息来源。
我们如何设计一个能够在没有任何监督来源的情况下学习环境的算法?难道我们不能仅仅采用基于模型的学习吗?实际上,基于模型的强化学习仍然需要奖励信号来规划或推断下一个动作。因此,需要一个不同的解决方案。
内在奖励
一个潜在的公平替代方法是开发一个内在的奖励函数,这个函数是由智能体的信念独立控制的。这种方法接近新生儿学习所采用的方式。实际上,他们采用纯探索的范式,在没有直接利益的情况下探索世界。然而,所获得的知识可能在以后的生活中有所帮助。
内在奖励是一种基于对状态新颖性估计的探索奖励。一个状态越陌生,它的内在奖励就越高。因此,代理通过这种奖励被激励去探索环境中的新空间。到现在为止,可能已经清楚,内在奖励可以作为一种替代的探索策略。事实上,许多算法将内在奖励与外在奖励(即环境返回的常规奖励)结合使用,以增强在稀疏环境中的探索,例如《Montezuma's Revenge》。然而,尽管估算内在奖励的方法与我们在第十二章《开发 ESBAS 算法》中研究的非常相似,用于激励策略探索(这些探索策略仍与外部奖励相关),但在这里,我们仅专注于纯粹的无监督探索方法。
两种主要的基于好奇心驱动的策略,它们能在未知状态下提供奖励并有效探索环境,分别是基于计数的策略和基于动态的策略:
-
基于计数的策略(也称为访问计数策略)旨在计数或估算每个状态的访问次数,并鼓励探索那些访问次数较少的状态,对这些状态分配较高的内在奖励。
-
基于动态的策略训练一个环境的动态模型,并与智能体的策略结合,计算内在奖励,通常基于预测误差、预测不确定性或预测改进。其基本思想是通过对已访问状态进行建模,新颖和未知的状态将具有更高的不确定性或估计误差。然后,这些值被用来计算内在奖励,并激励探索未知状态。
如果我们仅对常规环境应用好奇心驱动的方法,会发生什么?论文《好奇心驱动学习的大规模研究》提出了这个问题,并发现,在 Atari 游戏中,纯粹的好奇心驱动代理可以在没有任何外部奖励的情况下学习并掌握任务。此外,他们还注意到,在 Roboschool 中,步态行为完全是由这些基于内在奖励的无监督算法产生的。论文的作者还提出,这些发现是由于环境设计的方式。确实,在人类设计的环境中(如游戏),外部奖励通常与寻求新奇的目标是一致的。然而,在非游戏化的环境中,纯粹的好奇心驱动的无监督方法能够完全依靠自己探索并学习环境,而无需任何监督。或者,RL 算法也可以通过结合内在奖励与外在奖励,从而显著提高探索能力,并因此提升性能。
迁移学习
在两个环境之间迁移知识,尤其是当这些环境彼此相似时,是一项困难的任务。迁移学习策略旨在弥合知识差距,使得从初始环境到新环境的过渡尽可能平滑和容易。具体而言,迁移学习是将知识从源环境(或多个源环境)高效地迁移到目标环境的任务。因此,从一组源任务中获取的经验越多,并且迁移到新目标任务,智能体学习的速度就越快,在目标任务上的表现也会越好。
一般来说,当你想到一个尚未经过训练的智能体时,你需要想象一个完全没有任何信息的系统。相反,当你玩游戏时,你会使用大量的先验知识。例如,你可能会根据敌人的形状、颜色以及它们的动态来推测敌人的含义。这意味着,当敌人向你开火时,你能识别出敌人,就像下面的《太空侵略者》游戏截图所示。此外,你还可以轻松地猜测游戏的基本动态。然而,在训练的开始阶段,强化学习智能体什么都不知道。这个比较非常重要,因为它提供了关于在多个环境之间传递知识重要性的宝贵洞察。一个能够利用从源任务中获得的经验的智能体,在目标环境中可以更快地学习。例如,如果源环境是 Pong 而目标环境是 Breakout,那么许多视觉组件可以被重用,从而节省大量计算时间。为了准确理解其总体重要性,想象一下在更复杂的环境中获得的效率:

图 13.5. 《太空侵略者》的截图。你能推测出精灵的角色吗?
说到迁移学习时,我们通常提到 0-shot 学习、1-shot 学习,依此类推,这些术语指的是在目标领域中所需的尝试次数。例如,0-shot 学习意味着在源领域上训练的策略可以直接应用到目标领域,而无需进一步训练。在这种情况下,智能体必须具备强大的泛化能力,以便适应新任务。
迁移学习的类型
存在多种类型的迁移学习,其使用取决于具体情况和需求。一个区分点与源环境的数量有关。显然,你训练智能体的源环境越多,它的多样性就越大,能够在目标领域使用的经验也就越多。从多个源领域迁移学习被称为多任务学习。
1-任务学习
单任务学习或称为迁移学习,是在一个领域上训练策略并将其迁移到另一个领域的任务。可以采用三种主要技术来实现这一目标,具体如下:
-
微调:这涉及对目标任务上学到的模型进行优化。如果你参与过机器学习,尤其是在计算机视觉或自然语言处理领域,你可能已经使用过这种技术。不幸的是,在强化学习中,微调并不像在上述领域那样容易,因为它需要更精细的工程设计,并且通常带来的收益较小。其原因在于,一般而言,两个强化学习任务之间的差距比两个不同图像领域之间的差距要大。例如,猫和狗的分类差异较小,而 Pong 和 Breakout 之间的差异则较大。然而,微调仍然可以在强化学习中使用,调整最后几层(或者在动作空间完全不同的情况下替换它们)可能会带来更好的泛化能力。
-
领域随机化:这一方法基于这样一个思想,即源领域的动态多样化可以增强策略在新环境中的鲁棒性。领域随机化通过操控源领域来实现,例如,通过改变仿真器的物理特性,使得在多个随机修改的源领域上训练的策略足够鲁棒,能够在目标领域中表现良好。这种策略对于训练需要应用于现实世界的代理更加有效。在这种情况下,策略更具鲁棒性,仿真不必与物理世界完全相同,仍然能够提供所需的性能水平。
-
领域适应:这是另一个常用的过程,尤其用于将仿真源领域的策略映射到目标物理世界。领域适应的核心是将源领域的数据分布调整为与目标领域匹配。它主要应用于基于图像的任务,模型通常使用生成对抗网络(GANs)将合成图像转化为真实图像。
多任务学习
在多任务学习中,代理已训练的环境数量越多,代理在目标环境上的多样性和性能就会越好。多个源任务可以由一个或多个代理来学习。如果只有一个代理被训练,那么将其部署到目标任务上很容易。否则,如果多个代理学习了不同的任务,那么得到的策略可以作为一个集成模型使用,并对目标任务的预测进行平均,或者使用一个名为蒸馏的中间步骤,将多个策略合并为一个。具体来说,蒸馏过程将多个模型的知识压缩成一个,便于部署且推理速度更快。
真实世界中的强化学习
到目前为止,在本章中,我们介绍了开发深度强化学习算法的最佳实践和强化学习背后的挑战。我们还看到无监督强化学习和元学习如何缓解低效和泛化不良的问题。现在,我们想向你展示在将强化学习智能体应用于真实世界时需要解决的问题,以及如何弥合模拟环境中的差距。
设计一个能够在真实世界中执行动作的智能体是非常具有挑战性的。但大多数强化学习应用需要在现实世界中部署。因此,我们必须了解在面对物理世界的复杂性时所面临的主要挑战,并考虑一些有用的技术。
面对真实世界的挑战
除了样本效率和泛化能力方面的重大问题之外,在处理真实世界时,我们还需要面对诸如安全性和领域约束等问题。事实上,由于安全性和成本约束,智能体通常不能自由地与世界进行交互。一种解决方案可能来自使用约束算法,例如 TRPO 和 PPO,这些算法被嵌入系统机制中,以限制在训练过程中行动的变化。这可以防止智能体行为的剧烈变化。不幸的是,在高度敏感的领域,这还不够。例如,如今,你不能直接在道路上开始训练自动驾驶汽车。政策可能需要经过数百或数千个周期才能理解从悬崖上掉下来会导致不良结果,并学会避免这种情况。先在模拟中训练政策的替代方案是可行的。然而,当应用到城市环境时,必须做出更多与安全相关的决策。
正如我们刚才提到的,模拟优先的解决方案是一种可行的方法,根据实际任务的复杂性,它可能会带来良好的性能。然而,模拟器必须尽可能地模仿真实世界的环境。例如,如果下图左侧的模拟器与右侧的真实世界相似,那么该模拟器就无法使用。真实世界与模拟世界之间的差距被称为现实差距:

图 13.6. 人工世界与物理世界的比较
另一方面,使用高度准确和现实的环境可能也不可行。瓶颈现在是模拟器所需的计算能力。这个限制可以通过从一个速度更快、精度更低的模拟器开始,并逐步增加保真度以缩小现实差距来部分克服。最终,这将牺牲速度,但此时,代理应该已经学习了大部分任务,可能只需要少量的迭代来进行微调。然而,开发高度准确的模拟器来模拟物理世界是非常困难的。因此,在实践中,现实差距将依然存在,改善泛化的技术将有责任处理这种情况。
弥合模拟和现实世界之间的差距
为了从模拟到现实世界无缝过渡,从而克服现实差距,我们之前提出的一些泛化技术,例如领域自适应和领域随机化,可以被使用。例如,在论文《学习灵巧的手内操作》中,作者通过领域随机化训练了一个类人机器人,利用惊人的灵巧性操作物理物体。该策略通过许多不同的平行模拟进行训练,这些模拟旨在提供具有随机物理和视觉属性的多样化体验。这种偏向于泛化而非现实的机制至关重要,考虑到系统在部署时展示了一系列丰富的手内灵巧操作策略,其中许多策略也被人类使用。
创建你自己的环境
出于教育目的,在本书中,我们主要使用了适合我们需求的快速且小规模的任务。然而,现有许多模拟器可以用于运动任务(如 Gazebo、Roboschool 和 Mujoco)、机械工程、运输、自动驾驶汽车、安全等。这些现有环境种类繁多,但并不是每个应用都有现成的模拟器。因此,在某些情况下,你可能需要自己创建环境。
奖励函数本身很难设计,但它是强化学习(RL)中的关键部分。如果奖励函数设置错误,环境可能无法解决,代理可能会学到错误的行为。在第一章《强化学习的全景》中,我们举了一个赛船游戏的例子,其中船通过绕圈子来最大化奖励,捕捉重新生成的目标,而不是尽可能快地朝着轨迹终点行驶。这些就是在设计奖励函数时需要避免的行为。
设计奖励函数的一般建议(适用于任何环境)是使用正奖励来激励探索,并通过负奖励来抑制终止状态,或者如果目标是尽可能快速地达到终止状态,则使用负奖励。奖励函数的形状是一个重要的考虑因素。在本书中,我们曾多次警告稀疏奖励的问题。一个理想的奖励函数应该提供平滑且密集的反馈。
如果由于某些原因,奖励函数非常难以用公式表示,可以通过两种额外方式提供监督信号:
-
使用模仿学习或逆强化学习展示任务。
-
使用人类偏好来提供关于智能体行为的反馈。
后者仍然是一种新颖的方法,如果你对此感兴趣,可以阅读论文《从依赖策略的人类反馈中进行深度强化学习》(arxiv.org/abs/1902.04257),这将是一本有趣的读物。
强化学习的未来及其对社会的影响
人工智能的最初基础建立于 50 多年前,但直到最近几年,人工智能所带来的创新才作为主流技术传播到世界各地。这一波创新的浪潮主要得益于深度神经网络在监督学习系统中的演变。然而,人工智能的最新突破涉及强化学习,特别是深度强化学习。在围棋和 Dota 游戏中取得的成果突显了强化学习算法的卓越质量,这些算法能够进行长期规划、展现团队协作能力,并发现新的游戏策略,甚至是人类难以理解的策略。
在模拟环境中取得的显著成果开启了强化学习在物理世界中的新应用浪潮。我们才刚刚开始,但许多领域正在并将继续受到影响,带来深刻的变革。嵌入日常生活中的强化学习智能体可以通过自动化繁琐的工作、解决全球性挑战、发现新药物等方式提升生活质量——这些只是一些可能性。然而,这些将在我们世界和生活中广泛应用的系统需要安全可靠。目前我们尚未达到这一点,但我们在朝着正确的方向迈进。
AI 的伦理使用已经成为广泛关注的问题,比如在使用自主武器时。随着技术的快速发展,政策制定者和民众很难在这些问题上进行开放讨论。许多有影响力和有声望的人也认为 AI 是对人类的潜在威胁。但未来是无法预测的,且技术还有很长的路要走,才能开发出具备与人类相当能力的代理。我们有创造力、情感和适应性,这些现在是强化学习无法模拟的。
通过细心的关注,强化学习带来的短期益处可以显著超过其负面影响。但是,要将复杂的强化学习代理嵌入到物理环境中,我们需要解决之前提到的强化学习挑战。这些问题是可以解决的,一旦解决,强化学习有可能减少社会不平等,改善我们的生活质量以及地球的质量。
总结
在本书中,我们学习并实现了许多强化学习算法,但所有这些不同的算法在选择时可能会让人感到困惑。因此,在最后一章中,我们提供了一个经验法则,帮助你选择最适合你问题的强化学习算法类别。它主要考虑算法的计算时间和样本效率。此外,我们还提供了一些技巧,以帮助你更好地训练和调试深度强化学习算法,从而使这个过程更容易。
我们还讨论了强化学习中的隐性挑战:稳定性与可复现性、效率和泛化能力。这些是必须克服的主要问题,以便将强化学习代理应用于物理世界。事实上,我们详细介绍了无监督强化学习和迁移学习,这两种策略可以大大提高泛化能力和样本效率。
此外,我们详细阐述了强化学习可能对我们的生活产生的最关键的开放性问题,以及它在文化和技术方面的影响。
我们希望这本书能让你对强化学习有一个全面的了解,并激发你对这个迷人领域的兴趣。
问题
-
你会如何根据样本效率来排名 DQN、A2C 和 ES?
-
如果根据训练时间并且提供 100 个 CPU,他们的排名会如何?
-
你会在调试强化学习算法时从 CartPole 还是 MontezumaRevenge 开始?
-
为什么在比较多个深度强化学习算法时,使用多个种子更好?
-
内在奖励是否有助于环境探索?
-
什么是迁移学习?
进一步阅读
-
如果你对在 Atari 游戏中使用纯粹的好奇心驱动方法感兴趣,可以阅读这篇论文 Large-scale study of curiosity-driven learning (
arxiv.org/pdf/1808.04355.pdf)。 -
关于将领域随机化应用于学习灵巧的手内操作,阅读论文 学习灵巧的手内操作 (
arxiv.org/pdf/1808.00177.pdf)。 -
关于展示如何将人类反馈作为奖励函数的替代方法的工作,阅读论文 基于策略依赖的人类反馈的深度强化学习 (
arxiv.org/pdf/1902.04257.pdf)。
第十四章:评估
第三章
-
什么是随机策略?
- 它是一个基于概率分布定义的策略。
-
如何用下一个时间步的回报来定义回报?
-
为什么贝尔曼方程如此重要?
- 因为它提供了一个通用公式,用于计算一个状态的价值,利用当前的奖励和后续状态的价值。
-
DP 算法的限制因素是什么?
- 由于状态数量的复杂性爆炸,它们必须受到限制。另一个限制是系统的动态必须完全已知。
-
什么是策略评估?
- 是一种使用贝尔曼方程计算给定策略的价值函数的迭代方法。
-
策略迭代和价值迭代有什么区别?
- 策略迭代在策略评估和策略改进之间交替进行,而价值迭代则将二者结合在一个单一的更新中,使用最大函数。
第四章
-
RL 中使用的 MC 方法的主要特点是什么?
- 将状态的平均回报作为价值函数的估计。
-
为什么蒙特卡洛(MC)方法是离线的?
- 因为它们只有在完整的轨迹可用时才会更新状态值。因此,它们必须等到本轮结束。
-
TD 学习的两大主要思想是什么?
- 它们结合了采样和自举的思想。
-
MC 和 TD 有什么不同?
- MC 从完整的轨迹中学习,而 TD 则在每一步都学习,也能从不完整的轨迹中获取知识。
-
为什么探索在 TD 学习中如此重要?
- 因为 TD 更新仅在访问的状态-动作对上进行,所以如果某些状态-动作对未被发现,在没有探索策略的情况下,它们将永远不会被访问到。因此,一些好的策略可能永远不会被发现。
-
为什么 Q 学习是脱离策略的?
- 因为 Q 学习的更新独立于行为策略。它使用最大操作的贪婪策略。
第五章
-
死亡三元组问题的产生原因是什么?
- 当脱离策略学习与函数逼近和自举(bootstrapping)结合时。
-
DQN 如何克服不稳定性?
- 使用重放缓冲区和独立的在线网络与目标网络。
-
什么是移动目标问题?
- 这是一个问题,当目标值不是固定的,它们会随着网络的优化而变化。
-
DQN 是如何减轻移动目标问题的?
- 引入一个目标网络,其更新频率低于在线网络。
-
DQN 中使用的优化过程是什么?
- 通过随机梯度下降优化均方误差损失函数,这是一种对批量进行梯度下降的迭代方法。
-
状态-动作优势值函数的定义是什么?
第六章
-
PG 算法如何最大化目标函数?
- 它们通过沿着目标函数的导数的相反方向迈出一步来进行。该步长与回报成正比。
-
PG 算法背后的主要直觉是什么?
- 鼓励好的行为,并劝阻代理执行不好的行为。
-
为什么在 REINFORCE 中引入基准仍然保持无偏?
- 因为在期望下
![]()
- 因为在期望下
-
REINFORCE 属于哪个更广泛的算法类别?
- 它是一种蒙特卡罗方法,因为它像 MC 方法一样依赖完整的轨迹。
-
AC 方法中的评论员(critic)与 REINFORCE 中作为基准使用的价值函数有何不同?
- 除了学习的函数相同,评论员使用的是近似的价值函数来为动作-状态值进行自举,而在 REINFORCE(但也在 AC 中),它被用作基准来减少方差。
-
如果你需要为一个必须学会移动的代理开发算法,你会选择 REINFORCE 还是 AC?
- 你应该首先尝试一个演员-评论员算法,因为代理需要学习一个连续任务。
-
你可以使用 n 步演员-评论员算法作为 REINFORCE 算法吗?
- 是的,只要
大于环境中可能的最大步骤数,你就可以这么做。
- 是的,只要
第七章
-
策略神经网络如何控制一个连续的代理?
- 一种方法是预测描述高斯分布的均值和标准差。标准差可以根据状态(神经网络的输入)进行条件化,或者是一个独立的参数。
-
什么是 KL 散度?
- 是两个概率分布接近程度的度量。
-
TRPO 背后的主要思想是什么?
- 在接近旧概率分布的区域内优化新的目标函数。
-
KL 散度在 TRPO 中是如何使用的?
- 它作为硬约束用于限制旧策略和新策略之间的偏差。
-
PPO 的主要优点是什么?
- 它只使用一阶优化,这提高了算法的简洁性,并且具有更好的样本效率和性能。
-
PPO 是如何实现良好的样本效率的?
- 它运行小批量更新,充分利用数据。
第八章
-
Q 学习算法的主要限制是什么?
- 动作空间必须是离散的且较小,以便计算全局最大值。
-
为什么随机梯度算法样本效率低?
- 因为它们是策略梯度方法,需要每次策略变化时都获取新的数据。
-
确定性策略梯度是如何克服最大化问题的?
- DPG 将策略建模为一个确定性函数,只预测一个确定性的动作,确定性策略梯度定理提供了一种计算梯度的方法,用于更新策略。
-
DPG 是如何保证足够的探索的?
- 通过向确定性策略中加入噪声,或通过学习不同的行为策略。
-
DDPG 代表什么?它的主要贡献是什么?
- DDPG 代表深度确定性策略梯度,是一种将确定性策略梯度适应深度神经网络的算法。它们使用新策略来稳定和加速学习。
-
TD3 提出了哪些问题以进行最小化?
- Q 学习中常见的高估偏差和高方差估计。
-
TD3 采用了哪些新机制?
- 为了减少高估偏差,他们使用了剪切双 Q 学习(Clipped Double Q-learning),同时通过延迟策略更新和平滑正则化技术来解决方差问题。
第九章
-
如果你只有 10 局棋的时间来训练你的智能体玩跳棋,你会选择基于模型的算法还是无模型的算法?
- 我会使用基于模型的算法。跳棋的模型是已知的,并且规划是可行的任务。
-
基于模型的算法有哪些缺点?
- 总的来说,它们需要更多的计算能力,并且相较于无模型算法,获得的渐近性能较低。
-
如果环境的模型未知,如何学习该模型?
- 一旦通过与真实环境的交互收集了数据集,动态模型可以通过常规的监督学习方式进行学习。
-
为什么要使用数据聚合方法?
- 因为通常与环境的首次交互是通过一个天真的策略完成的,该策略没有探索环境的全部。后续的交互需要更明确的策略来优化环境模型。
-
ME-TRPO 如何稳定训练过程?
- ME-TRPO 采用了两个主要特性:一组模型和早停技术。
-
为什么一组模型可以改善策略学习?
- 因为由一组模型做出的预测考虑了单一模型的任何不确定性。
第十章
-
模仿学习是否被视为强化学习技术?
- 不,因为它们的底层框架不同。IL 的目标不是像 RL 那样最大化奖励。
-
你会使用模仿学习来构建一个不可战胜的围棋智能体吗?
- 可能不是,因为它需要一个专家来学习。如果智能体必须成为世界上最强的玩家,那么就没有值得学习的专家了。
-
DAgger 的全名是什么?
- 数据集聚合
-
DAgger 的主要优势是什么?
- 它通过让专家主动教导学习者从错误中恢复,克服了分布不匹配的问题。
-
在什么情况下你会使用 IRL 而不是 IL?
- 在奖励函数更容易学习并且需要学习比专家更好的策略的情况下。
第十一章
-
有哪两种强化学习的替代算法可以解决顺序决策问题?
- 进化策略和遗传算法
-
在进化算法中,哪些过程会产生新的个体?
- 突变是改变父代基因,而交叉是结合来自两个父代的遗传信息。
-
启发式算法,如遗传算法,的灵感来源于什么?
- 进化算法主要受到生物进化的启发。
-
CMA-ES 如何进化进化策略?
- CMA-ES 从一个多元正态分布中采样新候选,协方差矩阵会根据种群进行调整。
-
进化策略的一个优点和一个缺点是什么?
- 一个优点是它们是无导数的方法,而缺点是样本效率较低。
-
在“进化策略作为可扩展的强化学习替代方案”论文中,采用了什么技巧来减少方差?
- 他们提出使用镜像噪声,并生成一个带有相反符号扰动的附加突变。
第十二章
-
什么是探索-开发困境?
- 这是一个决策问题,是否最好进行探索,以便在未来做出更好的决策,还是利用当前最好的选项。
-
我们在之前的 RL 算法中使用过哪两种探索策略?
- 贪心策略和一个向策略中引入额外噪声的策略。
-
什么是 UCB?
- 上置信界(UCB)是一种乐观的探索算法,它为每个值估计一个上置信界,并选择最大化该值的动作(12.3)。
-
是 Montezuma's Revenge 还是多臂老丨虎丨机问题更难解决?
- Montezuma's Revenge 比多臂老丨虎丨机问题要难得多,仅仅因为后者是无状态的,而前者有着天文数字般的可能状态。Montezuma's Revenge 游戏本身也具有更多的内在复杂性。
-
ESBAS 如何解决在线 RL 算法选择的问题?
- 通过使用一个元算法,学习在特定情况下哪个固定算法组合表现更好。
第十三章
-
如何根据样本效率对 DQN、A2C 和 ES 进行排名?
- DQN 是最具样本效率的,其次是 A2C 和 ES。
-
如果按训练时间和可用的 100 个 CPU 排名,它们的排名会是多少?
- ES 可能是训练最快的,然后是 A2C 和 DQN。
-
你会在 CartPole 还是 MontezumaRevenge 上开始调试一个 RL 算法?
- CartPole。你应该从一个简单的任务开始调试算法。
-
为什么在比较多个深度 RL 算法时,使用多个种子会更好?
- 单次试验的结果可能因为神经网络和环境的随机性而高度波动。通过对多个随机种子进行平均,结果会接近平均情况。
-
内在奖励是否有助于环境的探索?
- 是的,这是因为内在奖励是一种探索奖励,它会增加智能体对访问新状态的好奇心。
-
什么是迁移学习?
- 任务是有效地在两个环境之间转移知识。


,控制每次更新时的学习量。
贪心策略的随机性初始值。
-贪婪策略
-贪心策略,接受一个
-贪心策略(行为策略)选择的。
-贪心)进行收集。
)和目标值!
系数。
而不形成完整的
的方法。
。
。
),旧策略和新策略之间的最大 KL 散度。
会在每一步与在线网络的参数
部分更新:
通过
。是的,尽管这可能会减慢学习速度,因为目标网络只部分更新,但它的好处超过了由增加的不稳定性带来的负面影响。使用目标网络的技巧不仅适用于演员,也适用于评论员,因此目标评论员的参数也会在软更新后更新:
。
)与使用目标策略(
)和目标评论员(
)计算得到的目标值之间的均方误差(MSE)损失来更新。相反,演员是按照公式(8.3)进行更新的。
和
;也就是说,对于每个个体,我们会有两种变异:
和
。
-贪婪策略,通常就足够了。


大于环境中可能的最大步骤数,你就可以这么做。
- 贪心策略和一个向策略中引入额外噪声的策略。
浙公网安备 33010602011771号