TensorFlow-强化学习快速启动指南-全-
TensorFlow 强化学习快速启动指南(全)
原文:
annas-archive.org/md5/7fda30dfb2bb9f5d7ff4a34ce0c3bea9译者:飞龙
前言
本书提供了几种不同的强化学习(RL)算法的总结,包括算法中的理论以及如何使用 Python 和 TensorFlow 编写这些算法。具体来说,本书涉及的算法有 Q 学习、SARSA、DQN、DDPG、A3C、TRPO 和 PPO。这些 RL 算法的应用包括 OpenAI Gym 中的计算机游戏和使用 TORCS 赛车模拟器进行的自动驾驶。
本书的目标读者
本书面向机器学习(ML)从业人员,特别是有兴趣学习强化学习的读者。它将帮助机器学习工程师、数据科学家和研究生。读者应具备基本的机器学习知识,并有 Python 和 TensorFlow 编程经验,以便能够顺利完成本书。
本书内容概述
第一章,强化学习入门,概述了 RL 的基本概念,如代理、环境以及它们之间的关系。还涉及了奖励函数、折扣奖励、价值函数和优势函数等主题。读者还将熟悉 Bellman 方程、策略性算法和非策略性算法,以及无模型和基于模型的 RL 算法。
第二章,时序差分学习、SARSA 与 Q 学习,向读者介绍了时序差分学习、SARSA 和 Q 学习。它还总结了如何在 Python 中编码这些算法,并在两个经典的 RL 问题——GridWorld 和 Cliff Walking——上进行训练和测试。
第三章,深度 Q 网络,向读者介绍了本书中的第一个深度 RL 算法——DQN。它还将讨论如何在 Python 和 TensorFlow 中编写此算法。然后,代码将用于训练 RL 代理玩Atari Breakout。
第四章,双重 DQN、对抗性架构与彩虹算法,在前一章的基础上进行了扩展,介绍了双重 DQN。它还讨论了涉及价值流和优势流的对抗性网络架构。这些扩展将在 Python 和 TensorFlow 中编码,并用于训练 RL 代理玩Atari Breakout。最后,将介绍 Google 的多巴胺代码,并用于训练 Rainbow DQN 代理。
第五章,深度确定性策略梯度,是本书中的第一个演员-评论家算法,也是第一个用于连续控制的 RL 算法。它向读者介绍了策略梯度,并讨论了如何使用它来训练演员的策略。本章将使用 Python 和 TensorFlow 编写此算法,并用其训练一个代理来解决倒立摆问题。
第六章,异步方法——A3C 和 A2C,向读者介绍了 A3C 算法,这是一种异步强化学习算法,其中一个主处理器将更新策略网络,多个工作处理器将使用该网络收集经验样本,这些样本将用于计算策略梯度,并传递给主处理器。本章中还将使用 A3C 训练 RL 代理来玩 OpenAI Gym 中的CartPole和LunarLander。最后,还简要介绍了 A2C。
第七章,信赖域策略优化和近端策略优化,讨论了两种基于策略分布比率的强化学习算法——TRPO 和 PPO。本章还讨论了如何使用 Python 和 TensorFlow 编码 PPO,并用其训练一个 RL 代理来解决 OpenAI Gym 中的 MountainCar 问题。
第八章,深度强化学习在自动驾驶中的应用,向读者介绍了 TORCS 赛车模拟器,编写 DDPG 算法来训练一个代理以自主驾驶汽车。本章的代码文件还包括用于相同 TORCS 问题的 PPO 算法,并作为练习提供给读者。
为了从本书中获得最大的收获
读者应具备机器学习算法的良好知识,如深度神经网络、卷积神经网络、随机梯度下降法和 Adam 优化。读者还应具备 Python 和 TensorFlow 的实践编程经验。
下载示例代码文件
您可以通过您的账户从www.packt.com下载本书的示例代码文件。如果您在其他地方购买了此书,可以访问www.packt.com/support,注册后将文件直接通过电子邮件发送给您。
您可以按照以下步骤下载代码文件:
-
登录或注册 www.packt.com。
-
选择“支持”标签。
-
点击“代码下载和勘误”。
-
在搜索框中输入书名并按照屏幕上的指示操作。
下载文件后,请确保使用以下最新版解压或提取文件夹:
-
WinRAR/7-Zip for Windows
-
Zipeg/iZip/UnRarX for Mac
-
7-Zip/PeaZip for Linux
本书的代码包也托管在 GitHub 上,地址为github.com/PacktPublishing/TensorFlow-Reinforcement-Learning-Quick-Start-Guide。如果代码有更新,将会在现有的 GitHub 仓库中更新。
我们还提供了来自我们丰富书籍和视频目录的其他代码包,您可以在github.com/PacktPublishing/查看。赶紧去看看吧!
下载彩色图像
我们还提供了一个 PDF 文件,其中包含了本书中使用的带颜色的截图/图表。您可以在这里下载:www.packtpub.com/sites/default/files/downloads/9781789533583_ColorImages.pdf。
使用的约定
本书中使用了许多文本约定。
CodeInText:表示文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 用户名。举个例子:“将下载的WebStorm-10*.dmg磁盘映像文件挂载为您系统中的另一个磁盘。”
一段代码的格式如下:
import numpy as np
import sys
import matplotlib.pyplot as plt
当我们希望您注意代码块中的特定部分时,相关行或项会以粗体显示:
def random_action():
# a = 0 : top/north
# a = 1 : right/east
# a = 2 : bottom/south
# a = 3 : left/west
a = np.random.randint(nact)
return a
所有命令行输入或输出如下所示:
sudo apt-get install python-numpy python-scipy python-matplotlib
粗体:表示新术语、重要单词或您在屏幕上看到的词语。例如,菜单或对话框中的词语会以这种形式出现在文本中。举个例子:“从管理面板中选择系统信息。”
警告或重要说明如下所示。
提示和技巧如下所示。
联系我们
我们欢迎读者的反馈。
一般反馈:如果您对本书的任何方面有疑问,请在邮件主题中注明书名,并通过电子邮件联系我们 customercare@packtpub.com。
勘误:虽然我们已尽力确保内容的准确性,但错误是难免的。如果您在本书中发现错误,我们将感激您向我们报告。请访问 www.packt.com/submit-errata,选择您的书籍,点击勘误提交表单链接并输入相关细节。
盗版:如果您在互联网上发现任何非法的我们作品的副本,无论其形式如何,我们将感激您提供该材料的网址或网站名称。请通过copyright@packt.com联系我们,并附上相关链接。
如果您有兴趣成为作者:如果您对某个领域有专业知识,并且有兴趣写书或为书籍做贡献,请访问 authors.packtpub.com。
评论
请留下评论。当您阅读并使用了本书后,为什么不在您购买本书的网站上留下评论呢?潜在读者可以看到并参考您公正的意见来做出购买决定,我们在 Packt 可以了解您对我们产品的看法,而我们的作者也能看到您对他们书籍的反馈。谢谢!
想了解更多关于 Packt 的信息,请访问 packt.com。
第一章:强化学习入门
本书将涵盖深度强化学习(RL)中的有趣主题,包括更广泛使用的算法,并提供 TensorFlow 代码来解决许多使用深度 RL 算法的挑战性问题。具备一定的 RL 基础知识将有助于你掌握本书涵盖的高级主题,但本书将以简明易懂的语言讲解,便于机器学习从业人员理解。本书选择的编程语言是 Python,使用的深度学习框架是 TensorFlow,我们预计你对这两者有一定的了解。如果没有,也有许多 Packt 出版的书籍涉及这些主题。我们将介绍几种不同的 RL 算法,例如深度 Q 网络(DQN)、深度确定性策略梯度(DDPG)、信任区域策略优化(TRPO)和近端策略优化(PPO)等。让我们一起深入探索深度 RL。
在本章中,我们将深入探讨强化学习的基本概念。我们将学习 RL 术语的含义,它们之间的数学关系,以及如何在 RL 环境中使用这些概念来训练智能体。这些概念将为我们在后续章节中学习 RL 算法奠定基础,并展示如何应用这些算法来训练智能体。祝学习愉快!
本章将涉及的主要主题如下:
-
构建 RL 问题模型
-
理解什么是智能体(agent)和环境(environment)
-
定义贝尔曼方程
-
在线学习与离线学习
-
无模型训练与基于模型的训练
为什么选择强化学习(RL)?
强化学习(RL)是机器学习的一个子领域,其中学习是通过试错法进行的。这与其他机器学习策略不同,例如:
-
监督学习:目标是学习拟合一个模型分布,该分布能够捕捉给定标签数据分布
-
无监督学习:目标是在给定的数据集中找到内在的模式,例如聚类
强化学习(RL)是一种强大的学习方法,因为你不需要标签数据,前提是你能够掌握 RL 中采用的通过探索学习的方法。
尽管强化学习(RL)已经有三十多年的历史,但近年来随着深度学习在 RL 中成功应用于解决实际任务的演示,RL 领域经历了新的复兴。在这些任务中,深度神经网络用于做出决策。RL 与深度学习的结合通常被称为深度强化学习(deep RL),也是本书讨论的主要内容。
深度强化学习(Deep RL)已经成功地被研究人员应用于视频游戏、自动驾驶、工业机器人抓取物体、交易员进行投资组合下注、医疗保健领域等多个场景。最近,Google DeepMind 构建了 AlphaGo,一个基于强化学习(RL)系统,它能够玩围棋,并轻松击败围棋的冠军。OpenAI 也构建了另一个系统,在《Dota》视频游戏中战胜人类。这些例子展示了 RL 在现实世界中的应用。广泛认为,这一领域有着非常光明的未来,因为你可以训练神经网络进行预测,而无需提供标注数据。
现在,让我们深入探讨强化学习问题的表述。我们将比较强化学习与孩子学走路之间的相似性。
强化学习问题的表述
要解决的基本问题是训练一个模型,在没有任何标注数据的情况下对某些预定义任务进行预测。这是通过试错方法实现的,类似于婴儿第一次学走路的过程。婴儿由于好奇想探索周围的世界,首先会爬出婴儿床,不知道该去哪儿,也不知道该做什么。最初,他们迈出小步伐,犯错误,摔倒在地上并哭泣。但是,经过许多这样的尝试后,他们开始能够独立站立,令父母感到欣喜。然后,他们凭借一股信念迈出了稍长的步伐,慢慢而小心地走着。他们依然会犯错误,但比之前少得多。
经过更多次的尝试——和失败——他们变得更加自信,从而能够迈出更长的步伐。随着时间的推移,这些步伐变得越来越长、越来越快,直到最终他们开始奔跑。这就是他们成长为一个孩子的过程。是否给他们提供了任何标注数据供他们学习走路?没有。他们是通过试错学习的,沿途犯错,从中学习,并在每次尝试中取得微小的进步。这就是强化学习的工作原理,通过试错学习。
在前面的例子基础上,这里是另一种情况。假设你需要通过试错法来训练一个机器人,下面是具体做法。首先,让机器人在环境中随机游走。然后收集好的和不好的行为,并使用奖励函数对其进行量化,因此,在某个状态下执行好的行为会获得高奖励;另一方面,坏行为会受到惩罚。这可以作为机器人自我改进的学习信号。在经历了多次这样的试错过程后,机器人就会根据奖励学会在给定状态下执行最优的行为。这就是强化学习(RL)中的学习方式。但是在本书的其余部分,我们将不再讨论人的特征。之前描述的孩子是代理,而他们的周围环境是强化学习术语中的环境。代理与环境互动,在这个过程中,学习如何执行任务,环境会提供奖励。
代理与其环境之间的关系
从最基本的层面上讲,强化学习涉及一个代理和一个环境。代理是一个人工智能实体,拥有某些目标,必须时刻警惕可能阻碍这些目标的事物,并且必须同时追求有助于实现这些目标的事物。环境是代理可以与之互动的所有事物。让我通过一个涉及工业移动机器人的例子来进一步解释。
例如,在一个工业移动机器人在工厂内导航的情境中,机器人是代理,而工厂是环境。
机器人有一些预定义的目标,例如,要将货物从工厂的一边移动到另一边,并避免与墙壁和/或其他机器人等障碍物发生碰撞。环境是机器人可以导航的区域,包括机器人可以到达的所有地方,也包括可能撞到的障碍物。因此,机器人的主要任务,或者更准确地说,代理的主要任务,是探索环境,理解它所采取的行为如何影响奖励,意识到可能导致灾难性碰撞或失败的障碍物,然后掌握如何最大化目标并随着时间的推移提高其表现。
在这个过程中,代理不可避免地与环境互动,这对代理来说,在某些任务上可能是有益的,但在其他任务上可能是不利的。因此,代理必须学习环境如何响应它所采取的行动。这是一种试错学习方法,只有经过多次这样的尝试,代理才能学会环境如何响应其决策。
现在我们来了解一下代理的状态空间,以及代理执行的动作以探索环境。
定义代理的状态
在强化学习术语中,状态表示智能体的当前情况。例如,在前述的工业移动机器人智能体案例中,给定时刻的状态是机器人在工厂内的位置——即它所在的位置、朝向,或者更准确地说,是机器人的姿态。对于具有关节和效应器的机器人,状态还可以包括关节和效应器在三维空间中的精确位置。对于自动驾驶汽车,它的状态可以表示速度、在地图上的位置、与其他障碍物的距离、车轮上的扭矩、引擎的转速等等。
状态通常通过现实世界中的传感器推导出来;例如,来自里程计、激光雷达、雷达和摄像头的测量值。状态可以是一个一维的实数或整数向量,或者是二维的摄像头图像,甚至是更高维度的,例如三维体素。实际上,状态没有精确的限制,状态仅仅代表智能体当前的情况。
在强化学习文献中,状态通常表示为s[t],其中下标 t 用于表示与该状态对应的时间瞬间。
定义智能体的动作
智能体执行动作以探索环境。获取这个动作向量是强化学习中的主要目标。理想情况下,你需要努力获得最优动作。
动作是智能体在某一状态 s[t] 下做出的决策。通常,它表示为 a[t],其中,像之前一样,下标 t 表示对应的时间瞬间。智能体可用的动作取决于问题。例如,迷宫中的智能体可以决定朝北、南、东或西迈出一步。这些被称为离散动作,因为有固定的可能性数量。另一方面,对于自动驾驶汽车,动作可以是转向角度、油门值、刹车值等等,这些被称为连续动作,因为它们可以在有限范围内取实数值。例如,转向角度可以是从南北线偏离 40 度,油门可以是 60%的压下程度,等等。
因此,动作 a[t] 可以是离散的,也可以是连续的,这取决于具体问题。一些强化学习方法处理离散动作,而其他方法则适用于连续动作。
下图展示了智能体及其与环境的交互示意图:

图 1:示意图,展示了智能体及其与环境的交互
现在我们知道了什么是智能体,接下来我们将探讨智能体学习的策略、什么是价值函数和优势函数,以及这些量如何在强化学习(RL)中使用。
理解策略、价值和优势函数
策略 定义了代理在给定状态下行为的指导方针。在数学术语中,策略是从代理的一个状态映射到在该状态下要采取的行动的映射。它类似于代理遵循的刺激-反应规则,当其学习探索环境时。在强化学习文献中,它通常表示为 π(a[t]|s[t]) –,即在给定状态 s**[t] 下采取行动 a**[t] 的条件概率分布。策略可以是确定性的,在这种情况下,a**[t] 的确切值在 s**[t] 处是已知的,也可以是随机的,其中 a**[t] 从一个分布中采样 - 典型情况下为高斯分布,但也可以是任何其他概率分布。
在强化学习中,价值函数 用于定义代理状态的优劣程度。它们通常用 V(s) 表示在状态 s 处的预期长期平均奖励。V(s) 的表达式如下,其中 E[.] 是对样本的期望:

注意,V(s) 不关心代理在状态 s 下需要采取的最佳行动。相反,它衡量的是状态的好坏程度。那么,代理如何在给定时间点 t 中找出在状态 s[t] 下采取的最佳行动 a[t] 呢?为此,您还可以定义一个动作值函数,其表达式如下所示:

注意 Q(s,a) 表示在状态 s 中采取行动 a 并随后遵循相同策略的优劣程度。因此,Q(s,a) 与 V(s) 不同,后者衡量的是给定状态的优劣程度。在接下来的章节中,我们将看到如何使用价值函数来训练强化学习设置下的代理。
优势函数 定义如下:
A(s,a) = Q(s,a) - V(s)
这个优势函数被认为能够减少策略梯度的方差,这是后续章节将深入讨论的一个主题。
经典的强化学习教材是《强化学习导论》(Reinforcement Lea**rning: An Introduction),作者是 Richard S Sutton 和 Andrew G Barto,出版社是 The MIT Press,出版年份是 1998 年。
现在我们将定义什么是一个情节及其在强化学习环境中的重要性。
识别情节
我们之前提到,代理在能够最大化其目标之前需要在环境中进行多次试验和错误的探索。从开始到结束的每次这样的试验被称为一个 情节。起始位置可能并不总是相同。同样,情节的结束可以是愉快的或悲伤的结局。
一个愉快或良好的结局可以是代理实现其预定义目标的情况,这可能是移动机器人成功导航到最终目的地,或者工业机器人臂成功拾取和放置卡柄到孔中等等。情节也可能有悲伤的结局,例如代理撞到障碍物或被困在迷宫中,无法摆脱等等。
在许多强化学习问题中,通常会指定一个时间步骤的上限来终止一个回合,尽管在其他问题中没有这样的上限,回合可以持续很长时间,直到达成目标,或者撞到障碍物、跌下悬崖,或发生类似情况为止。旅行者号宇宙飞船是由 NASA 于 1977 年发射的,它已经飞出我们的太阳系——这是一个无限时间回合系统的例子。
接下来我们将了解什么是奖励函数以及为什么需要折扣未来奖励。这个奖励函数是关键,因为它是代理学习的信号。
确定奖励函数和折扣奖励的概念
强化学习中的奖励与现实世界中的奖励没有区别——我们因表现好而获得奖励,因表现差而受到惩罚(即惩罚)。奖励函数由环境提供,指导代理在探索环境时进行学习。具体来说,它衡量的是代理的表现好坏。
奖励函数定义了代理可能发生的好事和坏事。例如,一个达到目标的移动机器人会获得奖励,但如果撞到障碍物,则会受到惩罚。同样,一个工业机器人臂在将钉子放入孔中时会获得奖励,但如果它处于可能导致破裂或撞击的危险姿势,则会受到惩罚。奖励函数是代理获取关于什么是最优的、什么不是最优的信号。代理的长期目标是最大化奖励并最小化惩罚。
奖励
在强化学习文献中,时间点 t 的奖励通常表示为 R[t]。因此,整个回合中获得的总奖励为 R = r1 + r2 + ... + r[t],其中 t 是回合的长度(可以是有限的,也可以是无限的)。
在强化学习中使用了折扣的概念,其中使用了一个称为折扣因子的参数,通常表示为 γ 且 0 ≤ γ ≤ 1,它的幂次与 r[t] 相乘。γ = 0 使得代理变得短视,只关注即时奖励;γ = 1 使得代理变得过于远视,以至于拖延最终目标的实现。因此,使用 0 到 1 范围内(不包括 0 和 1)的 γ 值来确保代理既不会过于短视,也不会过于远视。γ 确保代理优先考虑其行动,最大化从时间点 t 开始的总折扣奖励 R[t],其计算公式如下:

由于 0 ≤ γ ≤ 1,远期的奖励相比即时的奖励价值较低。这有助于代理不浪费时间,并优先考虑其行动。在实际应用中,γ = 0.9-0.99 通常用于大多数强化学习问题中。
学习马尔可夫决策过程
马尔可夫性质在强化学习中被广泛应用,表示环境在时间t+1时的反应仅依赖于时间t的状态和行动。换句话说,未来的即时状态只依赖于当前状态,而不依赖于过去的状态。这一特性大大简化了数学计算,并在强化学习和机器人学等多个领域得到广泛应用。
考虑一个系统,它通过采取行动a[0]从状态s[0]转移到状态s[1]并获得奖励r[1],然后再从s[1]到s[2],采取行动a[1],如此类推,直到时间t。如果在时间t+1时,处于状态s'的概率可以通过以下函数在数学上表示,那么该系统就被认为遵循马尔可夫性质:

请注意,处于状态s[t+1]的概率仅取决于s[t]和a[t],与过去无关。满足以下状态转移性质和奖励函数的环境被称为马尔可夫决策过程(MDP):


现在,让我们定义强化学习的基础:贝尔曼方程。这个方程将帮助我们提供一个迭代解法来获得价值函数。
定义贝尔曼方程
贝尔曼方程以伟大的计算机科学家和应用数学家理查德·E·贝尔曼的名字命名,是与动态规划相关的最优性条件。它在强化学习(RL)中被广泛用于更新智能体的策略。
让我们定义以下两个量:


第一个量,P[s,s'],是从状态s到新状态s'的转移概率。第二个量,R[s,s'],是智能体从状态s出发,采取行动a,然后转移到新状态s'时所获得的期望奖励。请注意,我们假设了 MDP 的属性,即在时间t+1时的状态转移仅依赖于时间t的状态和行动。用这些术语表达时,贝尔曼方程是一个递归关系,并且对于价值函数和行动价值函数,分别给出如下方程:


请注意,贝尔曼方程表示了状态下的价值函数V,并且是其他状态下价值函数的函数;行动价值函数Q也是如此。
在线学习与离线学习
强化学习算法可以分为在线学习和离线学习。接下来我们将了解这两种类型,并讨论如何将给定的强化学习算法区分为其中之一。
在线学习方法
策略方法使用与做出动作决策时相同的策略来进行评估。策略算法通常没有重放缓冲区;遇到的经验用于现场训练模型。从状态 t 到状态 t+1 期间,使用相同的策略来评估性能好坏。例如,如果一个机器人在给定状态下探索世界,它如果使用当前策略来判断其在当前状态下采取的行动是好是坏,那么它就是一个策略算法,因为当前策略也用于评估其行为。SARSA、A3C、TRPO 和 PPO 是我们将在本书中讨论的策略算法。
离策略方法
相反,离策略方法使用不同的策略来做出决策和评估性能。例如,许多离策略算法使用重放缓冲区来存储经验,并从该缓冲区中采样数据来训练模型。在训练步骤中,会随机采样一小批经验数据,并用于训练策略和价值函数。回到之前的机器人示例,在离策略设置下,机器人不会使用当前策略来评估其性能,而是使用不同的策略进行探索和评估。如果使用重放缓冲区来采样经验数据的小批量,然后训练代理,则这是离策略学习,因为机器人当前的策略(用于获取即时动作的策略)与用于获取小批量经验样本的策略不同(因为策略从数据收集时的早期时刻变更为当前时刻)。DQN、DDQN 和 DDPG 是我们将在本书后续章节中讨论的离策略算法。
无模型和基于模型的训练
不学习环境运作模型的 RL 算法称为无模型算法。相反,如果构建了环境模型,则该算法称为基于模型的算法。通常,如果使用价值(V)或动作价值(Q)函数来评估性能,则称其为无模型算法,因为没有使用环境的具体模型。另一方面,如果你建立了一个模型,描述环境如何从一个状态转移到另一个状态,或者通过模型决定代理将从环境中获得多少奖励,那么这些算法称为基于模型算法。
在无模型算法中,如前所述,我们并未构建环境的模型。因此,智能体必须在一个状态下采取行动,判断这是一个好的选择还是一个坏的选择。在基于模型的 RL 中,我们会学习一个环境的近似模型;该模型可以与策略共同学习,或是事先学习好。这个环境模型用于做决策,并且用于训练策略。我们将在后续章节中深入学习这两类 RL 算法。
本书涵盖的算法
在第二章,时间差分法、SARSA 和 Q-学习中,我们将探讨前两个强化学习(RL)算法:Q-学习和 SARSA。这两个算法都是基于表格的,不需要使用神经网络。因此,我们将用 Python 和 NumPy 进行编程。在第三章,深度 Q 网络中,我们将介绍 DQN,并使用 TensorFlow 为本书后续章节中的智能体进行编码。然后我们将训练它玩 Atari Breakout。在第四章,双重 DQN、对抗网络架构和 Rainbow中,我们将讨论双重 DQN、对抗网络架构以及 Rainbow DQN。在第五章,深度确定性策略梯度中,我们将探讨我们的第一个演员-评论员 RL 算法,称为 DDPG,学习策略梯度,并将其应用于连续动作问题。在第六章,异步方法 – A3C 和 A2C中,我们将研究 A3C,这是另一个使用主节点和多个工作节点的 RL 算法。在第七章,信任区域策略优化和近端策略优化中,我们将研究另外两种 RL 算法:TRPO 和 PPO。最后,在第八章,深度强化学习在自动驾驶中的应用中,我们将应用 DDPG 和 PPO 训练一个智能体,来实现自主驾驶。 从第三章,深度 Q 网络到第八章,深度强化学习在自动驾驶中的应用,我们将使用 TensorFlow 智能体。希望你在学习强化学习的过程中玩得愉快。
摘要
本章介绍了强化学习(RL)的基本概念。我们理解了智能体与环境之间的关系,也学习了马尔可夫决策过程(MDP)的设置。我们学习了奖励函数的概念和折扣奖励的使用,以及价值和优势函数的思想。此外,我们还了解了贝尔曼方程及其在 RL 中的应用。我们还学习了在策略型和非策略型 RL 算法之间的区别。此外,我们还考察了无模型和基于模型的 RL 算法之间的区别。所有这些为我们深入探讨 RL 算法以及如何利用它们训练智能体执行特定任务奠定了基础。
在下一章中,我们将探讨我们首两个 RL 算法:Q-learning 和 SARSA。请注意,在第二章,时序差分、SARSA 和 Q-Learning 中,我们将使用基于 Python 的代理,因为它们是表格学习方法。但从第三章,深度 Q 网络 开始,我们将使用 TensorFlow 来编写深度 RL 代理,因为我们将需要神经网络。
问题
-
对于基于策略或非基于策略的 RL 算法,是否需要回放缓存?
-
为什么我们要对奖励进行折扣?
-
如果折扣因子是 γ > 1 会发生什么?
-
由于我们拥有环境状态的模型,基于模型的 RL 代理是否总是比无模型的 RL 代理表现更好?
-
RL 和深度 RL 有什么区别?
进一步阅读
-
强化学习:导论,理查德·S·萨顿 和 安德鲁·G·巴托,MIT 出版社,1998 年
-
深度强化学习实战,马克西姆·拉潘,Packt 出版公司,2018 年:
www.packtpub.com/big-data-and-business-intelligence/deep-reinforcement-learning-hands
第二章:时间差、SARSA 和 Q-Learning
在上一章中,我们了解了强化学习的基础知识。在本章中,我们将介绍时间差(TD)学习、SARSA 和 Q-learning,这些都是深度强化学习(deep RL)普及之前广泛使用的算法。理解这些旧一代算法对你掌握该领域非常重要,并且为深入研究深度强化学习奠定基础。因此,我们将在本章中通过使用这些旧一代算法的示例来进行学习。另外,我们还会使用 Python 编写这些算法的代码。本章不会使用 TensorFlow,因为这些问题不涉及任何深度神经网络。然而,本章将为后续章节涉及的更高级主题奠定基础,并且是我们第一次亲手实现强化学习算法的编码实践。本章将是我们第一次深入探讨标准强化学习算法,并学习如何用它来训练智能体完成特定任务。它还将是我们第一次实践强化学习,包括理论与实践相结合。
本章将涵盖的部分主题如下:
-
理解 TD 学习
-
学习 SARSA
-
理解 Q-learning
-
使用 SARSA 和 Q-learning 进行悬崖行走
-
使用 SARSA 的网格世界
技术要求
以下知识将帮助你更好地理解本章所讲的概念:
-
Python(版本 2 或 3)
-
NumPy
-
TensorFlow(版本 1.4 或更高版本)
理解 TD 学习
我们首先将学习 TD 学习。这是强化学习中的一个非常基础的概念。在 TD 学习中,智能体的学习通过经验获得。智能体通过多次与环境的互动来进行试探,所获得的奖励用于更新值函数。具体来说,智能体会随着经验的积累,持续更新状态-动作值函数。贝尔曼方程被用来更新这一状态-动作值函数,目标是最小化 TD 错误。这本质上意味着智能体正在减少对给定状态下最优动作的不确定性;它通过降低 TD 错误来提高对最优动作的信心。
值函数与状态之间的关系
值函数是智能体对给定状态好坏的估计。例如,如果一个机器人靠近悬崖边缘,可能会掉下去,那么这个状态就是坏的,值应该很低。另一方面,如果机器人/智能体接近最终目标,那么这个状态就是一个好状态,因为它们很快会获得高奖励,因此该状态的值会较高。
值函数 V 在到达 s[t] 状态并从环境中获得 r[t] 奖励后会被更新。最简单的 TD 学习算法叫做 TD(0),它使用以下方程进行更新,其中 α 是学习率,0 ≤ α ≤ 1:

请注意,在某些参考论文或书籍中,上述公式可能会使用 r[t] 替代 r[t+1]。这只是约定上的差异,并不是错误;此处的 r[t+1] 表示从 s[t] 状态获得的奖励,并且过渡到 s[t+1]。
还有一种名为 TD(λ) 的 TD 学习变体,它使用可达性踪迹 e(s),即访问状态的记录。更正式地说,我们按照以下方式执行 TD(λ) 更新:

可达性踪迹由以下公式给出:

在这里,e(s) = 0 当 t = 0。对于代理所采取的每一步,可达性踪迹会对所有状态减少 γλ,并在当前时间步访问的状态上增加 1。这里,0 ≤ λ ≤ 1,它是一个决定奖励的影响程度分配给远程状态的参数。接下来,我们将探讨我们下两个 RL 算法(SARSA 和 Q-learning)的理论,这两个算法在 RL 社区中都非常流行。
理解 SARSA 和 Q-Learning
在本节中,我们将学习 SARSA 和 Q-Learning,并探讨如何使用 Python 编写它们。在深入了解之前,让我们先弄清楚什么是 SARSA 和 Q-Learning。SARSA 是一种使用状态-动作 Q 值进行更新的算法。这些概念源自计算机科学中的动态规划领域,而 Q-learning 是一种离政策算法,最早由 Christopher Watkins 在 1989 年提出,并且是广泛使用的强化学习算法。
学习 SARSA
SARSA 是另一种非常流行的在政策算法,特别是在 1990 年代。它是 TD-learning 的扩展,我们之前已经看到过,并且是一个在政策算法。SARSA 会更新状态-动作值函数,并且随着新经验的获得,这个状态-动作值函数会使用动态规划中的 Bellman 方程进行更新。我们将前述的 TD 算法扩展到状态-动作值函数 Q(s[t],a[t]),这个方法被称为 SARSA:

在这里,从给定的状态 s[t] 开始,我们采取行动 a[t],获得奖励 r[t+1],过渡到一个新的状态 s[t+1],然后继续采取行动 a[t+1],如此循环下去。这个五元组 (s[t], a[t], r[t+1], s[t+1], a[t+1]) 赋予了算法 SARSA 这个名字。SARSA 是一个在政策算法,因为它更新的是与估计 Q 时使用的相同策略。为了探索,你可以使用例如 ε-贪心策略。
理解 Q-learning
Q 学习是一种脱离策略的算法,最早由 Christopher Watkins 在 1989 年提出,是一种广泛使用的强化学习算法。与 SARSA 一样,Q 学习会保持每个状态-动作对的状态-动作值函数的更新,并使用动态编程的贝尔曼方程递归更新它,随着新经验的收集。请注意,它是一种脱离策略的算法,因为它使用的是评估过的状态-动作值函数,这个值将最大化。Q 学习适用于动作是离散的情况——例如,如果我们有向北、向南、向东、向西的动作,并且我们要在某个给定状态下决定最优的动作,那么 Q 学习在这种设置下是适用的。
在经典的 Q 学习方法中,更新方式如下,其中对动作执行 max 操作,即我们选择与状态s[t+1]对应的 Q 值最大值的动作 a:

α是学习率,这是一个超参数,用户可以指定。
在我们用 Python 编写算法之前,让我们先了解一下将考虑哪些类型的问题。
悬崖行走和网格世界问题
让我们考虑悬崖行走和网格世界问题。首先,我们将介绍这些问题,然后进入编码部分。对于这两个问题,我们考虑一个具有nrows(行数)和ncols(列数)的矩形网格。我们从底部左侧单元格正下方的一个单元格开始,目标是到达底部右侧单元格正下方的目标单元格。
请注意,起点和终点单元格不属于nrows x ncols的网格单元格。在悬崖行走问题中,除了起点和终点单元格外,位于底部行单元格以南的单元格构成了悬崖,如果智能体进入这些单元格,剧集将以灾难性地掉入悬崖结束。同样,如果智能体试图离开网格单元格的左边、上边或右边边界,它将被放回到同一单元格,也就是说,它相当于没有采取任何行动。
对于网格世界问题,我们没有悬崖,但网格世界内有障碍物。如果智能体试图进入任何这些障碍物单元格,它将被弹回到它原本所在的单元格。在这两个问题中,目标是找到从起点到终点的最优路径。
那么,接下来我们深入研究吧!
使用 SARSA 进行悬崖行走
现在,我们将学习如何在 Python 中编码上述方程,并使用 SARSA 实现悬崖行走问题。首先,让我们导入 Python 中的numpy、sys和matplotlib包。如果你以前没有使用过这些包,市面上有几本关于这些主题的 Packt 书籍可以帮助你尽快掌握。请在 Linux 终端中输入以下命令来安装所需的包:
sudo apt-get install python-numpy python-scipy python-matplotlib
现在我们将总结解决网格世界问题所涉及的代码。在终端中,使用你喜欢的编辑器(例如 gedit、emacs 或 vi)编写以下代码:
import numpy as np
import sys
import matplotlib.pyplot as plt
我们将使用一个 3 x 12 的网格来解决悬崖行走问题,即有 3 行和 12 列。我们还有 4 个动作可以在每个单元格中执行。你可以向北、向东、向南或向西移动:
nrows = 3
ncols = 12
nact = 4
我们将考虑总共 100000 次回合。为了进行探索,我们将使用 ε-贪婪策略,ε 的值为 0.1。我们将考虑一个常数值的 ε,尽管有兴趣的读者可以考虑使用变化的 ε 值,并在回合过程中将其逐渐衰减至零。
学习率 α 选择为 0.1,折扣因子 γ 选择为 0.95,这些是解决该问题的典型值:
nepisodes = 100000
epsilon = 0.1
alpha = 0.1
gamma = 0.95
接下来,我们将为奖励分配值。对于任何不掉入悬崖的正常动作,奖励是 -1;如果代理掉入悬崖,奖励是 -100;到达目的地的奖励也是 -1。稍后可以探索这些奖励的其他值,并研究其对最终 Q 值以及从起点到终点路径的影响:
reward_normal = -1
reward_cliff = -100
reward_destination = -1
状态-动作对的 Q 值初始化为零。我们将使用一个 NumPy 数组来存储 Q 值,形状为 nrows x ncols x nact,即每个单元格有 nact 个条目,整个网格有 nrows x ncols 个单元格:
Q = np.zeros((nrows,ncols,nact),dtype=np.float)
我们将定义一个函数,使代理回到起始位置,该位置的 (x, y) 坐标为(x=0,y=nrows):
def go_to_start():
# start coordinates
y = nrows
x = 0
return x, y
接下来,我们定义一个函数来执行随机动作,在该函数中我们定义 a = 0 表示向 top/north 移动,a = 1 表示向 right/east 移动,a = 2 表示向 bottom/south 移动,a = 4 表示向 left/west 移动。具体来说,我们将使用 NumPy 的 random.randint() 函数,如下所示:
def random_action():
# a = 0 : top/north
# a = 1 : right/east
# a = 2 : bottom/south
# a = 3 : left/west
a = np.random.randint(nact)
return a
现在我们将定义 move 函数,该函数将接受代理的给定位置 (x, y) 和当前的动作 a,然后执行该动作。它将返回执行该动作后代理的新位置 (x1, y1) 以及代理的状态,我们定义 state = 0 表示代理在执行动作后 OK;state = 1 表示到达目的地;state = 2 表示掉入悬崖。如果代理通过左侧、顶部或右侧离开网格,它将被送回到同一网格(等同于没有执行任何动作):
def move(x,y,a):
# state = 0: OK
# state = 1: reached destination
# state = 2: fell into cliff
state = 0
if (x == 0 and y == nrows and a == 0):
# start location
x1 = x
y1 = y - 1
return x1, y1, state
elif (x == ncols-1 and y == nrows-1 and a == 2):
# reached destination
x1 = x
y1 = y + 1
state = 1
return x1, y1, state
else:
# inside grid; perform action
if (a == 0):
x1 = x
y1 = y - 1
elif (a == 1):
x1 = x + 1
y1 = y
elif (a == 2):
x1 = x
y1 = y + 1
elif (a == 3):
x1 = x - 1
y1 = y
# don't allow agent to leave boundary
if (x1 < 0):
x1 = 0
if (x1 > ncols-1):
x1 = ncols-1
if (y1 < 0):
y1 = 0
if (y1 > nrows-1):
state = 2
return x1, y1, state
接下来,我们将定义 exploit 函数,该函数将接受代理的 (x, y) 位置,并根据 Q 值采取贪婪的动作,即选择在该 (x, y) 位置具有最高 Q 值的 a 动作。如果我们处于起始位置,我们将向北移动(a = 0);如果我们距离目的地一步之遥,我们将向南移动(a = 2):
def exploit(x,y,Q):
# start location
if (x == 0 and y == nrows):
a = 0
return a
# destination location
if (x == ncols-1 and y == nrows-1):
a = 2
return a
if (x == ncols-1 and y == nrows):
print("exploit at destination not possible ")
sys.exit()
# interior location
if (x < 0 or x > ncols-1 or y < 0 or y > nrows-1):
print("error ", x, y)
sys.exit()
a = np.argmax(Q[y,x,:])
return a
接下来,我们将使用以下 bellman() 函数执行贝尔曼更新:
def bellman(x,y,a,reward,Qs1a1,Q):
if (y == nrows and x == 0):
# at start location; no Bellman update possible
return Q
if (y == nrows and x == ncols-1):
# at destination location; no Bellman update possible
return Q
# perform the Bellman update
Q[y,x,a] = Q[y,x,a] + alpha*(reward + gamma*Qs1a1 - Q[y,x,a])
return Q
接着我们将定义一个函数,根据随机数是否小于 ε 来选择探索或利用,这个参数是我们在 ε-贪心探索策略中使用的。为此,我们将使用 NumPy 的 np.random.uniform(),它将输出一个介于零和一之间的实数随机数:
def explore_exploit(x,y,Q):
# if we end up at the start location, then exploit
if (x == 0 and y == nrows):
a = 0
return a
# call a uniform random number
r = np.random.uniform()
if (r < epsilon):
# explore
a = random_action()
else:
# exploit
a = exploit(x,y,Q)
return a
现在我们已经具备了解决悬崖行走问题所需的所有函数。因此,我们将对回合进行循环,在每个回合中,我们从起始位置开始,接着进行探索或利用,然后根据动作移动智能体一步。以下是这部分的 Python 代码:
for n in range(nepisodes+1):
# print every 1000 episodes
if (n % 1000 == 0):
print("episode #: ", n)
# start
x, y = go_to_start()
# explore or exploit
a = explore_exploit(x,y,Q)
while(True):
# move one step
x1, y1, state = move(x,y,a)
我们根据所获得的奖励执行贝尔曼更新;请注意,这基于本章理论部分之前呈现的方程。如果我们到达目的地或掉下悬崖,我们就停止该回合;如果没有,我们继续进行一次探索或利用策略,然后继续下去。以下代码中的state变量取值为1表示到达目的地,取值为2表示掉下悬崖,否则为0:
# Bellman update
if (state == 1):
reward = reward_destination
Qs1a1 = 0.0
Q = bellman(x,y,a,reward,Qs1a1,Q)
break
elif (state == 2):
reward = reward_cliff
Qs1a1 = 0.0
Q = bellman(x,y,a,reward,Qs1a1,Q)
break
elif (state == 0):
reward = reward_normal
# Sarsa
a1 = explore_exploit(x1,y1,Q)
if (x1 == 0 and y1 == nrows):
# start location
Qs1a1 = 0.0
else:
Qs1a1 = Q[y1,x1,a1]
Q = bellman(x,y,a,reward,Qs1a1,Q)
x = x1
y = y1
a = a1
上述代码将完成所有回合,现在我们已经得到了收敛的 Q 值。接下来,我们将使用 matplotlib 绘制每个动作的 Q 值:
for i in range(nact):
plt.subplot(nact,1,i+1)
plt.imshow(Q[:,:,i])
plt.axis('off')
plt.colorbar()
if (i == 0):
plt.title('Q-north')
elif (i == 1):
plt.title('Q-east')
elif (i == 2):
plt.title('Q-south')
elif (i == 3):
plt.title('Q-west')
plt.savefig('Q_sarsa.png')
plt.clf()
plt.close()
最后,我们将使用之前收敛的 Q 值进行路径规划。也就是说,我们将绘制出智能体从起点到终点的准确路径,使用最终收敛的 Q 值。为此,我们将创建一个名为path的变量,并为其存储追踪路径的值。然后,我们将使用 matplotlib 来绘制它,如下所示:
path = np.zeros((nrows,ncols,nact),dtype=np.float)
x, y = go_to_start()
while(True):
a = exploit(x,y,Q)
print(x,y,a)
x1, y1, state = move(x,y,a)
if (state == 1 or state == 2):
print("breaking ", state)
break
elif (state == 0):
x = x1
y = y1
if (x >= 0 and x <= ncols-1 and y >= 0 and y <= nrows-1):
path[y,x] = 100.0
plt.imshow(path)
plt.savefig('path_sarsa.png')
就这样。我们已经完成了使用 SARSA 解决悬崖行走问题所需的编码。现在我们来查看结果。在下面的屏幕截图中,我们展示了在网格中每个位置的每个动作(向北、向东、向南或向西)的 Q 值。如图例所示,黄色表示高 Q 值,紫色表示低 Q 值。
从 SARSA 的结果来看,它明显地通过选择不向南走来避免掉进悬崖,尤其是在智能体正好位于悬崖北侧时,南方动作的负 Q 值很大:

图 1:使用 SARSA 进行悬崖行走问题的 Q 值
接下来,我们将绘制智能体从起点到终点的路径,见下图:

图 2:使用 SARSA 的智能体路径
现在我们将使用 Q-learning 来研究相同的悬崖行走问题。
使用 Q-learning 进行悬崖行走
我们现在将重复相同的悬崖行走问题,尽管这次我们使用 Q-learning 来代替 SARSA。大部分代码与 SARSA 相同,除了几个总结的差异。由于 Q-learning 使用贪心策略选择动作,我们将使用以下函数来计算给定位置的最大 Q 值。大部分代码与前一节相同,因此我们只指定需要更改的部分。
现在,让我们用 Q-learning 编写悬崖行走代码。
max_Q()函数定义如下:
def max_Q(x,y,Q):
a = np.argmax(Q[y,x,:])
return Q[y,x,a]
我们将使用先前定义的max_Q()函数来计算新状态下的 Q 值:
Qs1a1 = max_Q(x1,y1,Q)
此外,选择探索还是利用的动作是在while循环内完成的,因为在利用时我们会贪婪地选择动作:
# explore or exploit
a = explore_exploit(x,y,Q)
这就是编写 Q-learning 代码的全部内容。我们将应用它来解决悬崖行走问题,并展示每个动作的 Q 值以及代理从起点到终点所走的路径,具体内容将在以下截图中显示:

图 3:使用 Q-learning 进行悬崖行走问题的 Q 值
如图所示,对于 Q-learning 和 SARSA,路径有所不同。由于 Q-learning 是一种贪婪策略,代理现在选择接近悬崖的路径(见下图图 4),因为它是最短路径:

图 4:使用 Q-learning 解决悬崖行走问题时,代理所走的路径
另一方面,由于 SARSA 更具远见,因此它选择了安全但较长的路径,即位于顶部的单元格行(见图 2)。
我们的下一个问题是网格世界问题,我们必须在一个网格中进行导航。我们将在 SARSA 中实现这个问题。
使用 SARSA 的网格世界
我们接下来将考虑网格世界问题,并使用 SARSA 来解决它。我们将在悬崖位置引入障碍物。代理的目标是从起点导航到目标,同时避开障碍物。我们将在obstacle_cells列表中存储障碍物单元格的坐标,每个条目表示障碍物单元格的(x, y)坐标。
以下是此任务涉及的步骤总结:
-
大部分代码与之前使用的相同,区别将在此总结
-
在网格中放置障碍物
-
move()函数还需要在网格中查找障碍物 -
绘制 Q 值以及代理所走的路径
在这里,我们将开始用 Python 编写算法:
import numpy as np
import sys
import matplotlib.pyplot as plt
nrows = 3
ncols = 12
nact = 4
nepisodes = 100000
epsilon = 0.1
alpha = 0.1
gamma = 0.95
reward_normal = -1
reward_destination = -1
# obstacles
obstacle_cells = [(4,1), (4,2), (8,0), (8,1)]
move()函数现在将发生变化,因为我们还需要检查障碍物。如果代理最终进入某个障碍物单元格,它将被推回到原来的位置,如以下代码片段所示:
def move(x,y,a):
# state = 0: OK
# state = 1: reached destination
state = 0
if (x == 0 and y == nrows and a == 0):
# start location
x1 = x
y1 = y - 1
return x1, y1, state
elif (x == ncols-1 and y == nrows-1 and a == 2):
# reached destination
x1 = x
y1 = y + 1
state = 1
return x1, y1, state
else:
if (a == 0):
x1 = x
y1 = y - 1
elif (a == 1):
x1 = x + 1
y1 = y
elif (a == 2):
x1 = x
y1 = y + 1
elif (a == 3):
x1 = x - 1
y1 = y
if (x1 < 0):
x1 = 0
if (x1 > ncols-1):
x1 = ncols-1
if (y1 < 0):
y1 = 0
if (y1 > nrows-1):
y1 = nrows-1
# check for obstacles; reset to original (x,y) if inside obstacle
for i in range(len(obstacle_cells)):
if (x1 == obstacle_cells[i][0] and y1 == obstacle_cells[i][1]):
x1 = x
y1 = y
return x1, y1, state
这就是用 SARSA 编写网格世界代码的全部内容。Q 值和所走的路径将在以下图表中显示:

图 5:使用 SARSA 解决网格世界问题时,每个动作的 Q 值
如下图所示,代理绕过障碍物到达目的地:

图 6:使用 SARSA 解决网格世界问题时代理所走的路径
使用 Q-learning 解决网格世界问题并非易事,因为采用的贪婪策略无法轻易避免代理在某一状态下重复执行相同动作。通常,收敛速度非常慢,因此暂时避免使用。
总结
本章中,我们探讨了时序差分(TD)概念。我们还学习了前两个 RL 算法:Q 学习和 SARSA。我们看到如何用 Python 编写这两个算法,并用它们解决悬崖行走和网格世界问题。这两个算法使我们对 RL 的基础有了良好的理解,并展示了如何从理论过渡到代码。这两个算法在 1990 年代和 2000 年代初期非常流行,在深度强化学习崭露头角之前。尽管如此,Q 学习和 SARSA 在 RL 社区中至今仍然被使用。
在下一章中,我们将探讨深度神经网络在强化学习(RL)中的应用,进而形成深度强化学习(deep RL)。我们将看到 Q 学习的一个变种,称为深度 Q 网络(DQNs),它将使用神经网络代替我们在本章中看到的表格型状态-动作值函数。请注意,只有状态和动作数量较少的问题才适合 Q 学习和 SARSA。当状态和/或动作数量较多时,我们会遇到所谓的“维度灾难”(Curse of Dimensionality),在这种情况下,由于过度的内存使用,表格方法变得不可行;在这些问题中,DQN 更为适用,并且将是下一章的核心内容。
进一步阅读
- 强化学习:导论 由 理查德·萨顿 和 安德鲁·巴尔托 编著,2018 年
第三章:深度 Q 网络
深度 Q 网络(DQN)革新了强化学习(RL)领域。我相信你一定听说过 Google DeepMind,它曾是一个名为 DeepMind Technologies 的英国公司,直到 2014 年被 Google 收购。DeepMind 在 2013 年发布了一篇论文,标题为Playing Atari with Deep RL,在这篇论文中,他们在强化学习的背景下使用了深度神经网络(DNN),或者被称为 DQN——这是该领域的奠基性想法。这篇论文彻底革新了深度强化学习的领域,接下来的发展也成为了历史!后来,在 2015 年,他们又发布了第二篇论文,标题为Human Level Control Through Deep RL,发表于《自然》杂志,提出了一些更有趣的想法,进一步改进了前一篇论文。两篇论文一起引发了深度强化学习领域的“寒武纪大爆发”,出现了多个新算法,改善了使用神经网络训练智能体的效果,也推动了深度强化学习在实际问题中的应用。
在本章中,我们将研究 DQN,并使用 Python 和 TensorFlow 进行编码。这将是我们在强化学习中第一次使用深度神经网络。同时,这也是我们本书中第一次尝试通过深度强化学习解决实际的控制问题。
在本章中,将涉及以下主题:
-
学习 DQN 背后的理论
-
理解目标网络
-
了解回放缓冲区
-
介绍 Atari 环境
-
在 TensorFlow 中编写 DQN
-
评估 DQN 在 Atari Breakout 上的表现
技术要求
了解以下内容将有助于你更好地理解本章介绍的概念:
-
Python(2 及以上版本)
-
NumPy
-
TensorFlow(版本 1.4 或更高)
学习 DQN 背后的理论
在本节中,我们将探讨 DQN 背后的理论,包括其中的数学原理,并学习如何使用神经网络来评估value函数。
之前,我们讨论了 Q 学习,其中Q(s,a)被存储并作为一个多维数组进行评估,每个状态-动作对有一个条目。这在网格世界和悬崖行走问题中效果良好,因为这两个问题在状态和动作空间中都是低维的。那么,我们能将此方法应用于高维问题吗?答案是否定的,因为存在维度灾难,使得存储大量的状态和动作变得不可行。此外,在连续控制问题中,动作作为一个实数变化,虽然可能有无限多个实数值,这些值无法表示为表格型的Q数组。这就催生了强化学习中的函数逼近方法,尤其是使用深度神经网络(DNNs)的方法——也就是 DQN。在这里,Q(s,a)被表示为一个 DNN,输出的是Q的值。
以下是 DQN 涉及的步骤:
- 使用 Bellman 方程更新状态-动作价值函数,其中(s, a)是某时刻的状态和动作,t、s'和a'分别是下一个时刻t+1的状态和动作,γ是折扣因子:

- 然后我们在第i步定义一个损失函数来训练 Q 网络,如下所示:

上述参数是神经网络参数,用θ表示,因此 Q 值写作Q(s, a; θ)。
- y[i]是第i次迭代的目标,给定的方程如下:

- 然后我们通过最小化损失函数L(θ),使用优化算法如梯度下降、RMSprop 和 Adam 来训练 DQN 神经网络。
我们之前使用了最小二乘损失来计算 DQN 的损失函数,也叫做 L2 损失。你也可以考虑其他损失函数,如 Huber 损失,它结合了 L1 和 L2 损失,L2 损失在接近零的地方,而 L1 损失在远离零的区域。Huber 损失对异常值比 L2 损失更不敏感。
我们现在来看一下目标网络的使用。这是一个非常重要的概念,对于稳定训练是必不可少的。
理解目标网络
DQN 的一个有趣特点是在训练过程中使用第二个网络,称为目标网络。这个第二个网络用于生成目标-Q 值,这些值在训练过程中用于计算损失函数。为什么不只用一个网络进行两个估算,也就是说既用来选择行动a,又用来更新 Q 网络呢?问题在于,在训练的每一步,Q 网络的值都会发生变化。如果我们使用不断变化的值来更新网络,那么估算可能很容易变得不稳定——网络可能会在目标 Q 值和估算 Q 值之间进入反馈回路。为了减少这种不稳定性,目标网络的权重是固定的——即,慢慢地更新为主 Q 网络的值。这使得训练变得更加稳定和实际。
我们有第二个神经网络,我们称之为目标网络。它的架构与主 Q 网络相同,尽管神经网络的参数值不同。每经过N步,参数会从 Q 网络复制到目标网络。这会导致训练更加稳定。例如,可以使用N = 10,000 步。另一种选择是慢慢更新目标网络的权重(这里,θ是 Q 网络的权重,θ^t是目标网络的权重):

这里,τ是一个小的数值,比如 0.001。使用指数移动平均的方法是本书推荐的选择。
现在让我们了解一下在离策略算法中回放缓冲区的使用。
了解回放缓冲区
我们需要元组(s,a,r,s',done)来更新 DQN,其中s和a分别是时间t的状态和动作;s'是时间t+1的新状态;done是一个布尔值,根据回合是否结束,它是True或False,也就是文献中所称的终止值。这个布尔变量done或terminal用于在 Bellman 更新中正确处理回合的最后终止状态(因为我们无法对终止状态做*r + γ max Q(s',a')*)。DQNs 中的一个问题是,我们使用连续的(s,a,r,s',done)元组样本,这些样本是相关的,因此训练可能会过拟合。
为了缓解这个问题,使用了回放缓冲区,其中元组(s,a,r,s',done)从经验中存储,且从回放缓冲区中随机采样一个这样的经验小批量用于训练。这确保了每个小批量采样的样本是独立同分布(IID)的。通常使用较大的回放缓冲区,例如 500,000 到 100 万个样本。在训练开始时,回放缓冲区会填充到足够数量的样本,并且会随着新经验的到来而更新。一旦回放缓冲区填满到最大样本数量,旧样本会被一个一个地丢弃。这是因为旧样本是由劣质策略生成的,在后期训练时不再需要,因为智能体在学习过程中已经进步。
在最近的一篇论文中,DeepMind 提出了一种优先回放缓冲区,其中使用时间差分误差的绝对值来为缓冲区中的样本赋予重要性。因此,误差较大的样本具有更高的优先级,因此被采样的几率更大。这种优先回放缓冲区比普通的回放缓冲区学习更快。然而,它的编码稍微困难一些,因为它使用了 SumTree 数据结构,这是一个二叉树,其中每个父节点的值是其两个子节点值的总和。此优先经验回放将在此处不再深入讨论!
优先经验回放缓冲区基于这篇 DeepMind 论文:arxiv.org/abs/1511.05952
现在我们将研究 Atari 环境。如果你喜欢玩视频游戏,你一定会喜欢这一部分!
介绍 Atari 环境
Atari 2600 游戏套件最早于 1970 年代发布,并在当时非常火爆。它包括几个游戏,用户通过键盘输入动作进行游戏。这些游戏在当时非常受欢迎,激励了 1970 年代和 1980 年代的许多电脑游戏玩家,但按照今天视频游戏玩家的标准来看,它们显得过于原始。然而,它们今天在强化学习(RL)社区中仍然很受欢迎,因为它们是可以被 RL 智能体训练的游戏入口。
Atari 游戏总结
这里是 Atari 部分精选游戏的总结(由于版权原因,我们不会展示游戏截图,但会提供链接)。
Pong
我们的第一个例子是一个叫做 Pong 的乒乓球游戏,用户可以上下移动击打乒乓球,将其击打给对方,另一方是计算机。首先得到 21 分的人将赢得比赛。可以在gym.openai.com/envs/Pong-v0/找到来自 Atari 的 Pong 游戏截图。
Breakout
在另一个游戏中,叫做 Breakout,用户必须左右移动挡板来击打球,球随后会弹跳并撞击屏幕顶部的方块。击中方块的数量越多,玩家能够获得的积分或奖励就越高。每局游戏总共有五条生命,如果玩家未能击中球,就会失去一条生命。可以在gym.openai.com/envs/Breakout-v0/找到来自 Atari 的 Breakout 游戏截图。
Space Invaders
如果你喜欢射击外星人,那么 Space Invaders 就是你的游戏。在这个游戏中,一波接一波的外星人从顶部降下,目标是用激光束射击它们,获得积分。你可以在gym.openai.com/envs/SpaceInvaders-v0/找到该游戏的链接。
LunarLander
或者,如果你对太空旅行感兴趣,那么 LunarLander 是关于将太空船(类似于阿波罗 11 号的“鹰”号)着陆在月球表面。在每个关卡中,着陆区的表面会发生变化,目标是引导太空船在两个旗帜之间的月球表面着陆。可以在gym.openai.com/envs/LunarLander-v2/找到 LunarLander 的 Atari 截图。
经典街机学习环境
Atari 上有超过 50 款此类游戏,它们现在是经典街机学习环境(ALE)的一部分,ALE 是一个面向对象的框架,建立在 Atari 之上。如今,OpenAI 的 gym 被用于调用 Atari 游戏,以便训练强化学习(RL)代理来玩这些游戏。例如,你可以在 Python 中导入gym并按如下方式进行游戏。
reset()函数重置游戏环境,render()函数渲染游戏截图:
import gym
env = gym.make('SpaceInvaders-v0')
env.reset()
env.render()
我们现在将使用 TensorFlow 和 Python 编写一个 DQN,来训练一个代理如何玩 Atari Breakout。
用 TensorFlow 编写 DQN
在这里,我们将使用 TensorFlow 编写一个 DQN 并玩 Atari Breakout。我们将使用三个 Python 文件:
-
dqn.py:这个文件将包含主循环,在这里我们将探索环境并调用更新函数。 -
model.py:这个文件将包含 DQN 代理的类,在这里我们将构建神经网络并实现训练所需的函数。 -
funcs.py:此文件将涉及一些工具函数——例如,用于处理图像帧或填充重放缓冲区
使用 model.py 文件
首先编写 model.py 文件。相关步骤如下:
- 导入所需的包:
import numpy as np
import sys
import os
import random
import tensorflow as tf
- 选择 更大 或 更小 的网络:我们将使用两种神经网络架构,一种称为
bigger,另一种称为smaller。暂时使用bigger网络,感兴趣的用户可以稍后将网络更改为smaller选项并比较性能:
NET = 'bigger' # 'smaller'
- 选择 损失 函数(L2 损失或 Huber 损失):对于 Q-learning 的
loss函数,我们可以选择 L2 损失或 Huber 损失。两者都会在代码中使用。我们暂时选择huber:
LOSS = 'huber' # 'L2'
- 定义神经网络权重初始化:接下来,我们将为神经网络的权重指定一个初始化器。
tf.variance_scaling_initializer(scale=2)用于 He 初始化。也可以使用 Xavier 初始化,并且已提供为注释。感兴趣的用户稍后可以比较 He 和 Xavier 初始化器的性能:
init = tf.variance_scaling_initializer(scale=2) # tf.contrib.layers.xavier_initializer()
- 定义 QNetwork() 类:然后我们将按如下方式定义
QNetwork()类。它将包含一个__init__()构造函数,以及_build_model()、predict()和update()函数。__init__构造函数如下所示:
class QNetwork():
def __init__(self, scope="QNet", VALID_ACTIONS=[0, 1, 2, 3]):
self.scope = scope
self.VALID_ACTIONS = VALID_ACTIONS
with tf.variable_scope(scope):
self._build_model()
- 完成 _build_model() 函数:在
_build_model()中,我们首先定义 TensorFlow 的tf_X、tf_Y和tf_actions占位符。请注意,图像帧在重放缓冲区中以uint8格式存储,以节省内存,因此它们通过转换为float并除以255.0来进行归一化,以将X输入值压缩到 0-1 范围内:
def _build_model(self):
# input placeholders; input is 4 frames of shape 84x84
self.tf_X = tf.placeholder(shape=[None, 84, 84, 4], dtype=tf.uint8, name="X")
# TD
self.tf_y = tf.placeholder(shape=[None], dtype=tf.float32, name="y")
# action
self.tf_actions = tf.placeholder(shape=[None], dtype=tf.int32, name="actions")
# normalize input
X = tf.to_float(self.tf_X) / 255.0
batch_size = tf.shape(self.tf_X)[0]
- 定义卷积层:如前所述,我们有两个神经网络选项,
bigger和smaller。bigger网络有三个卷积层,后面跟着一个全连接层。smaller网络只有两个卷积层,后面跟着一个全连接层。我们可以使用tf.contrib.layers.conv2d()定义卷积层,使用tf.contrib.layers.fully_connected()定义全连接层。请注意,在最后一个卷积层之后,我们需要先将输出展平,然后再传递给全连接层,展平操作将使用tf.contrib.layers.flatten()。我们使用之前定义的winit对象作为权重初始化器:
if (NET == 'bigger'):
# bigger net
# 3 conv layers
conv1 = tf.contrib.layers.conv2d(X, 32, 8, 4, padding='VALID', activation_fn=tf.nn.relu, weights_initializer=winit)
conv2 = tf.contrib.layers.conv2d(conv1, 64, 4, 2, padding='VALID', activation_fn=tf.nn.relu, weights_initializer=winit)
conv3 = tf.contrib.layers.conv2d(conv2, 64, 3, 1, padding='VALID', activation_fn=tf.nn.relu, weights_initializer=winit)
# fully connected layers
flattened = tf.contrib.layers.flatten(conv3)
fc1 = tf.contrib.layers.fully_connected(flattened, 512, activation_fn=tf.nn.relu, weights_initializer=winit)
elif (NET == 'smaller'):
# smaller net
# 2 conv layers
conv1 = tf.contrib.layers.conv2d(X, 16, 8, 4, padding='VALID', activation_fn=tf.nn.relu, weights_initializer=winit)
conv2 = tf.contrib.layers.conv2d(conv1, 32, 4, 2, padding='VALID',activation_fn=tf.nn.relu, weights_initializer=winit)
# fully connected layers
flattened = tf.contrib.layers.flatten(conv2)
fc1 = tf.contrib.layers.fully_connected(flattened, 256, activation_fn=tf.nn.relu, weights_initializer=winit)
- 定义全连接层:最后,我们有一个全连接层,其大小根据动作的数量来确定,使用
len(self.VALID_ACTIONS)来指定。这个最后的全连接层的输出存储在self.predictions中,表示Q(s,a),我们在之前的学习 DQN 背后的理论部分的方程中看到过。传递给这个函数的动作(self.tf_actions)需要转换为独热编码格式,我们使用tf.one_hot()。请注意,one_hot是一种表示动作编号的方式,它将所有动作的值设为零,除了某个动作外,该动作对应的值为1.0。然后,我们使用self.predictions * action_one_hot将预测与独热编码的动作相乘,最后使用tf.reduce_sum()对其求和;这被存储在self.action_predictions变量中:
# Q(s,a)
self.predictions = tf.contrib.layers.fully_connected(fc1, len(self.VALID_ACTIONS), activation_fn=None, weights_initializer=winit)
action_one_hot = tf.one_hot(self.tf_actions, tf.shape(self.predictions)[1], 1.0, 0.0, name='action_one_hot')
self.action_predictions = tf.reduce_sum(self.predictions * action_one_hot, reduction_indices=1, name='act_pred')
- 计算训练 Q 网络的损失:我们通过使用 L2 损失或 Huber 损失来计算训练 Q 网络的损失,存储在
self.loss中,损失的类型由LOSS变量决定。对于 L2 损失,我们使用tf.squared_difference()函数;对于 Huber 损失,我们使用huber_loss(),我们很快会定义它。损失会在多个样本上取平均,为此我们使用tf.reduce_mean()函数。请注意,我们将计算之前定义的tf_y占位符和前一步获得的action_predictions变量之间的损失:
if (LOSS == 'L2'):
# L2 loss
self.loss = tf.reduce_mean(tf.squared_difference(self.tf_y, self.action_predictions), name='loss')
elif (LOSS == 'huber'):
# Huber loss
self.loss = tf.reduce_mean(huber_loss(self.tf_y-self.action_predictions), name='loss')
- 使用优化器:我们使用 RMSprop 或 Adam 优化器,并将其存储在
self.optimizer中。我们的学习目标是最小化self.loss,因此我们使用self.optimizer.minimize()。这被存储在self.train_op中:
# optimizer
#self.optimizer = tf.train.RMSPropOptimizer(learning_rate=0.00025, momentum=0.95, epsilon=0.01)
self.optimizer = tf.train.AdamOptimizer(learning_rate=2e-5)
self.train_op=
self.optimizer.minimize(self.loss,global_step=tf.contrib.framework.get_global_step())
- 为类定义 predict() 函数:在
predict()函数中,我们使用 TensorFlow 的sess.run()运行之前定义的self.predictions函数,其中sess是传递给此函数的tf.Session()对象。状态作为参数传递给此函数,存储在s变量中,之后传递给 TensorFlow 占位符tf_X:
def predict(self, sess, s):
return sess.run(self.predictions, { self.tf_X: s})
- 为类定义 update() 函数:最后,在
update()函数中,我们调用train_op和loss对象,并将字典a传递给参与执行这些操作的占位符,我们称之为feed_dict。状态存储在s中,动作存储在a中,目标存储在y中:
def update(self, sess, s, a, y):
feed_dict = { self.tf_X: s, self.tf_y: y, self.tf_actions: a }
_, loss = sess.run([self.train_op, self.loss], feed_dict)
return loss
- 在类外定义 huber_loss() 函数:完成
model.py的最后一项任务是定义 Huber 损失函数,它是 L1 和 L2 损失的结合。当输入小于1.0时,使用 L2 损失,其他情况使用 L1 损失:
# huber loss
def huber_loss(x):
condition = tf.abs(x) < 1.0
output1 = 0.5 * tf.square(x)
output2 = tf.abs(x) - 0.5
return tf.where(condition, output1, output2)
使用funcs.py文件
接下来,我们将通过完成以下步骤来编写funcs.py:
- 导入包:首先,我们导入所需的包:
import numpy as np
import sys
import tensorflow as tf
- 完成 ImageProcess() 类:接下来,将来自 Atari 模拟器的 210 x 160 x 3 RGB 图像转换为 84 x 84 的灰度图像。为此,我们创建了一个
ImageProcess()类,并使用 TensorFlow 的实用函数,如rgb_to_grayscale()将 RGB 转换为灰度,crop_to_bounding_box()将图像裁剪到感兴趣的区域,resize_images()将图像调整为所需的 84 x 84 大小,以及squeeze()去除输入中的维度。该类的process()函数通过调用self.output上的sess.run()函数来执行这些操作;请注意,我们将state变量作为字典传递。
# convert raw Atari RGB image of size 210x160x3 into 84x84 grayscale image
class ImageProcess():
def __init__(self):
with tf.variable_scope("state_processor"):
self.input_state = tf.placeholder(shape=[210, 160, 3], dtype=tf.uint8)
self.output = tf.image.rgb_to_grayscale(self.input_state)
self.output = tf.image.crop_to_bounding_box(self.output, 34, 0, 160, 160)
self.output = tf.image.resize_images(self.output, [84, 84], method=tf.image.ResizeMethod.NEAREST_NEIGHBOR)
self.output = tf.squeeze(self.output)
def process(self, sess, state):
return sess.run(self.output, { self.input_state: state })
- 从一个网络复制模型参数到另一个网络:下一步是编写一个名为
copy_model_parameters()的函数,该函数将接受tf.Session()对象sess和两个网络(在本例中为 Q 网络和目标网络)作为参数。我们将它们命名为qnet1和qnet2。该函数将把qnet1的参数值复制到qnet2:
# copy params from qnet1 to qnet2
def copy_model_parameters(sess, qnet1, qnet2):
q1_params = [t for t in tf.trainable_variables() if t.name.startswith(qnet1.scope)]
q1_params = sorted(q1_params, key=lambda v: v.name)
q2_params = [t for t in tf.trainable_variables() if t.name.startswith(qnet2.scope)]
q2_params = sorted(q2_params, key=lambda v: v.name)
update_ops = []
for q1_v, q2_v in zip(q1_params, q2_params):
op = q2_v.assign(q1_v)
update_ops.append(op)
sess.run(update_ops)
- 编写一个使用 ε-贪婪策略进行探索或开发的函数:我们将编写一个名为
epsilon_greedy_policy()的函数,该函数将根据通过 NumPy 的np.random.rand()计算得到的随机实数是否小于epsilon(之前描述的ε-贪婪策略参数)来决定是进行探索还是开发。对于探索,所有的动作具有相等的概率,每个动作的概率为1/(num_actions),其中num_actions是动作的数量(在 Breakout 游戏中为四个)。另一方面,对于开发,我们使用 Q 网络的predict()函数获取 Q 值,并通过 NumPy 的np.argmax()函数确定具有最高 Q 值的动作。该函数的输出是每个动作的概率,在开发的情况下,除了与最大 Q 值对应的那个动作,其他动作的概率都为0,对应的最大 Q 值动作的概率被赋为1.0:
# epsilon-greedy
def epsilon_greedy_policy(qnet, num_actions):
def policy_fn(sess, observation, epsilon):
if (np.random.rand() < epsilon):
# explore: equal probabiities for all actions
A = np.ones(num_actions, dtype=float) / float(num_actions)
else:
# exploit
q_values = qnet.predict(sess, np.expand_dims(observation, 0))[0]
max_Q_action = np.argmax(q_values)
A = np.zeros(num_actions, dtype=float)
A[max_Q_action] = 1.0
return A
return policy_fn
- 编写一个函数来填充回放记忆:最后,我们将编写
populate_replay_mem函数,用于将replay_memory_init_size数量的样本填充到回放缓冲区。首先,我们使用env.reset()重置环境。然后,我们处理从重置中获得的状态。每个状态需要四帧,因为代理否则无法判断球或挡板的运动方向、速度和/或加速度(在 Breakout 游戏中;对于其他游戏,如《太空侵略者》,类似的推理也适用于确定何时以及在哪里开火)。对于第一帧,我们堆叠四个副本。我们还计算delta_epsilon,即每个时间步长ε的减少量。回放记忆被初始化为空列表:
# populate replay memory
def populate_replay_mem(sess, env, state_processor, replay_memory_init_size, policy, epsilon_start, epsilon_end, epsilon_decay_steps, VALID_ACTIONS, Transition):
state = env.reset()
state = state_processor.process(sess, state)
state = np.stack([state] * 4, axis=2)
delta_epsilon = (epsilon_start - epsilon_end)/float(epsilon_decay_steps)
replay_memory = []
-
计算动作概率:然后,我们将循环
replay_memory_init_size四次,按delta_epsilon减少 epsilon,并使用传入的policy()计算动作概率,这些概率存储在action_probs变量中。通过使用 NumPy 的np.random.choice从action_probs变量中采样确定具体的动作。然后,env.render()渲染环境,接着将动作传递给env.step(),它输出下一个状态(存储在next_state中)、该转移的奖励,以及该回合是否结束,这些信息存储在布尔变量done中。 -
追加到重放缓冲区:我们接着处理下一个状态,并将其追加到重放记忆中,格式为元组(
state、action、reward、next_state、done)。如果回合结束,我们将重置环境,进入新一轮游戏,处理图像并像之前一样叠加四次。如果回合尚未结束,新的状态将成为下一个时间步的当前状态,我们将继续这样进行,直到循环结束:
for i in range(replay_memory_init_size):
epsilon = max(epsilon_start - float(i) * delta_epsilon, epsilon_end)
action_probs = policy(sess, state, epsilon)
action = np.random.choice(np.arange(len(action_probs)), p=action_probs)
env.render()
next_state, reward, done, _ = env.step(VALID_ACTIONS[action])
next_state = state_processor.process(sess, next_state)
next_state = np.append(state[:,:,1:], np.expand_dims(next_state, 2), axis=2)
replay_memory.append(Transition(state, action, reward, next_state, done))
if done:
state = env.reset()
state = state_processor.process(sess, state)
state = np.stack([state] * 4, axis=2)
else:
state = next_state
return replay_memory
这完成了funcs.py。
使用 dqn.py 文件
我们将首先编写dqn.py文件。这需要以下步骤:
- 导入必要的包:我们将按如下方式导入所需的包:
import gym
import itertools
import numpy as np
import os
import random
import sys
import matplotlib.pyplot as plt
import tensorflow as tf
from collections import deque, namedtuple
from model import *
from funcs import *
- 设置游戏并选择有效的动作:然后,我们将设置游戏。现在选择
BreakoutDeterministic-v4游戏,它是 Breakout v0 的后续版本。该游戏有四个动作,编号从零到三,分别表示0:无操作(noop)、1:开火、2:向左移动、3:向右移动:
GAME = "BreakoutDeterministic-v4" # "BreakoutDeterministic-v0"
# Atari Breakout actions: 0 (noop), 1 (fire), 2 (left) and 3 (right)
VALID_ACTIONS = [0, 1, 2, 3]
- 设置模式(训练/测试)和初始迭代次数:我们接下来将设置
train_or_test变量中的模式。首先我们选择train开始(训练完成后,你可以将其设置为test来评估模型)。我们还将从0迭代开始训练:
# set parameters for running
train_or_test = 'train' #'test' #'train'
train_from_scratch = True
start_iter = 0
start_episode = 0
epsilon_start = 1.0
- 创建环境:我们将创建
env环境对象,它将创建GAME游戏。env.action_space.n将打印该游戏中的动作数量。env.reset()将重置游戏并输出初始状态/观察(请注意,在强化学习术语中,状态和观察是相同的,可以互换)。observation.shape将打印状态空间的形状:
env = gym.envs.make(GAME)
print("Action space size: {}".format(env.action_space.n))
observation = env.reset()
print("Observation space shape: {}".format(observation.shape)
- 创建存储检查点文件的路径和目录:接下来,我们将创建存储检查点模型文件的路径并创建目录:
# experiment dir
experiment_dir = os.path.abspath("./experiments/{}".format(env.spec.id))
# create ckpt directory
checkpoint_dir = os.path.join(experiment_dir, "ckpt")
checkpoint_path = os.path.join(checkpoint_dir, "model")
if not os.path.exists(checkpoint_dir):
os.makedirs(checkpoint_dir)
- 定义 deep_q_learning() 函数:接下来我们将创建
deep_q_learning()函数,该函数将接受一个包含多个参数的长列表,涉及 TensorFlow 会话对象、环境、Q和目标网络对象等。要遵循的策略是epsilon_greedy_policy():
def deep_q_learning(sess, env, q_net, target_net, state_processor, num_episodes, train_or_test='train', train_from_scratch=True,start_iter=0, start_episode=0, replay_memory_size=250000, replay_memory_init_size=50000, update_target_net_every=10000, gamma=0.99, epsilon_start=1.0, epsilon_end=[0.1,0.01], epsilon_decay_steps=[1e6,1e6], batch_size=32):
Transition = namedtuple("Transition", ["state", "action", "reward", "next_state", "done"])
# policy
policy = epsilon_greedy_policy(q_net, len(VALID_ACTIONS))
- 用初始随机动作遇到的经验填充重放记忆:然后,我们用初始样本填充重放记忆:
# populate replay memory
if (train_or_test == 'train'):
print("populating replay memory")
replay_memory = populate_replay_mem(sess, env, state_processor, replay_memory_init_size, policy, epsilon_start, epsilon_end[0], epsilon_decay_steps[0], VALID_ACTIONS, Transition)
- 设置 epsilon 值: 接下来,我们将设置
epsilon值。注意,我们有一个双线性函数,它将首先把epsilon的值从 1 减少到 0.1,然后从 0.1 减少到 0.01,步数由epsilon_decay_steps指定:
# epsilon start
if (train_or_test == 'train'):
delta_epsilon1 = (epsilon_start - epsilon_end[0])/float(epsilon_decay_steps[0])
delta_epsilon2 = (epsilon_end[0] - epsilon_end[1])/float(epsilon_decay_steps[1])
if (train_from_scratch == True):
epsilon = epsilon_start
else:
if (start_iter <= epsilon_decay_steps[0]):
epsilon = max(epsilon_start - float(start_iter) * delta_epsilon1, epsilon_end[0])
elif (start_iter > epsilon_decay_steps[0] and start_iter < epsilon_decay_steps[0]+epsilon_decay_steps[1]):
epsilon = max(epsilon_end[0] - float(start_iter) * delta_epsilon2, epsilon_end[1])
else:
epsilon = epsilon_end[1]
elif (train_or_test == 'test'):
epsilon = epsilon_end[1]
- 然后,我们将设置总的时间步数:
# total number of time steps
total_t = start_iter
- 然后,主循环从开始到总集数开始遍历每一集。我们重置集数,处理第一帧,并将其堆叠
4次。接着,我们将loss、time_steps和episode_rewards初始化为0。对于 Breakout 游戏,每集的总生命数为5,因此我们在ale_lives变量中跟踪这一数据。该代理在这一生命阶段的总时间步数初始化为一个较大的数字:
for ep in range(start_episode, num_episodes):
# save ckpt
saver.save(tf.get_default_session(), checkpoint_path)
# env reset
state = env.reset()
state = state_processor.process(sess, state)
state = np.stack([state] * 4, axis=2)
loss = 0.0
time_steps = 0
episode_rewards = 0.0
ale_lives = 5
info_ale_lives = ale_lives
steps_in_this_life = 1000000
num_no_ops_this_life = 0
- 跟踪时间步数: 我们将使用一个内部
while循环来跟踪给定集中的时间步数(注意:外部for循环是针对集数的,而这个内部while循环是针对当前集中的时间步数的)。我们将根据epsilon的值是否处于 0.1 到 1 的范围内,或 0.01 到 0.1 的范围内,来相应地减少epsilon,两者有不同的delta_epsilon值:
while True:
if (train_or_test == 'train'):
#epsilon = max(epsilon - delta_epsilon, epsilon_end)
if (total_t <= epsilon_decay_steps[0]):
epsilon = max(epsilon - delta_epsilon1, epsilon_end[0])
elif (total_t >= epsilon_decay_steps[0] and total_t <= epsilon_decay_steps[0]+epsilon_decay_steps[1]):
epsilon = epsilon_end[0] - (epsilon_end[0]-epsilon_end[1]) / float(epsilon_decay_steps[1]) * float(total_t-epsilon_decay_steps[0])
epsilon = max(epsilon, epsilon_end[1])
else:
epsilon = epsilon_end[1]
- 更新目标网络: 如果迄今为止的总时间步数是
update_target_net_every的倍数,我们将更新目标网络,这个值是一个用户定义的参数。这通过调用copy_model_parameters()函数来实现:
# update target net
if total_t % update_target_net_every == 0:
copy_model_parameters(sess, q_net, target_net)
print("\n copied params from Q net to target net ")
-
在每次代理的新生命开始时,我们进行一次无操作(对应动作概率[1, 0, 0, 0]),次数是零到七之间的随机数,以使得该集与过去的集不同,这样代理在探索和学习环境时能看到更多的变化。这在原始的 DeepMind 论文中也有采用,确保代理能够更好地学习,因为这种随机性确保了体验到更多的多样性。一旦我们脱离了这个初始的随机性循环,接下来的动作将按照
policy()函数执行。 -
注意,我们仍然需要在每次新生命开始时进行一次射击操作(对应的动作概率是[0, 1, 0, 0]),这对于启动代理是必需的。对于 ALE 框架来说,如果没有这一步,画面将会冻结。因此,生命周期的演变是:首先进行一次射击操作,然后进行一个随机次数(介于零到七之间)的无操作,再然后代理使用
policy函数:
time_to_fire = False
if (time_steps == 0 or ale_lives != info_ale_lives):
# new game or new life
steps_in_this_life = 0
num_no_ops_this_life = np.random.randint(low=0,high=7)
action_probs = [0.0, 1.0, 0.0, 0.0] # fire
time_to_fire = True
if (ale_lives != info_ale_lives):
ale_lives = info_ale_lives
else:
action_probs = policy(sess, state, epsilon)
steps_in_this_life += 1
if (steps_in_this_life < num_no_ops_this_life and not time_to_fire):
# no-op
action_probs = [1.0, 0.0, 0.0, 0.0] # no-op
- 然后,我们将使用 NumPy 的
random.choice根据action_probs的概率选择行动。接着,我们渲染环境并执行一步操作。info['ale.lives']将告知我们代理剩余的生命数,从而帮助我们确定代理是否在当前时间步丧失了生命。在 DeepMind 的论文中,奖励被设定为+1或-1,具体取决于奖励的符号,以便比较不同的游戏。这可以通过np.sign(reward)实现,但我们现在不使用它。然后,我们将处理next_state_img,将其转换为所需大小的灰度图像,接着将其追加到next_state向量中,该向量保持四帧连续的图像。获得的奖励将用于增加episode_rewards,同时我们也增加time_steps:
action = np.random.choice(np.arange(len(action_probs)), p=action_probs)
env.render()
next_state_img, reward, done, info = env.step(VALID_ACTIONS[action])
info_ale_lives = int(info['ale.lives'])
# rewards = -1,0,+1 as done in the paper
#reward = np.sign(reward)
next_state_img = state_processor.process(sess, next_state_img)
# state is of size [84,84,4]; next_state_img is of size[84,84]
#next_state = np.append(state[:,:,1:], np.expand_dims(next_state, 2), axis=2)
next_state = np.zeros((84,84,4),dtype=np.uint8)
next_state[:,:,0] = state[:,:,1]
next_state[:,:,1] = state[:,:,2]
next_state[:,:,2] = state[:,:,3]
next_state[:,:,3] = next_state_img
episode_rewards += reward
time_steps += 1
-
更新网络: 接下来,如果我们处于训练模式,我们更新网络。首先,如果回放记忆的大小超过限制,我们弹出最旧的元素。然后,我们将最近的元组(
state,action,reward,next_state,done)追加到回放记忆中。请注意,如果我们丧失了一条生命,我们会在最后一个时间步将done = True,这样代理可以学习避免丧失生命;否则,done = True仅在回合结束时才会被体验到,也就是当所有生命都丧失时。然而,我们也希望代理能够意识到生命的损失。 -
从回放缓冲区采样一个小批次: 我们从回放缓冲区中采样一个小批次,大小为
batch_size。我们使用目标网络计算下一个状态的Q值(q_values_next),并利用它计算贪婪的Q值,该值用于计算目标(公式中之前提到的y)。每四个时间步更新一次 Q 网络,使用q_net.update();这种更新频率是每四次,因为已知这样更稳定:
if (train_or_test == 'train'):
# if replay memory is full, pop the first element
if len(replay_memory) == replay_memory_size:
replay_memory.pop(0)
# save transition to replay memory
# done = True in replay memory for every loss of life
if (ale_lives == info_ale_lives):
replay_memory.append(Transition(state, action, reward, next_state, done))
else:
#print('loss of life ')
replay_memory.append(Transition(state, action, reward, next_state, True))
# sample a minibatch from replay memory
samples = random.sample(replay_memory, batch_size)
states_batch, action_batch, reward_batch, next_states_batch, done_batch = map(np.array, zip(*samples))
# calculate q values and targets
q_values_next = target_net.predict(sess, next_states_batch)
greedy_q = np.amax(q_values_next, axis=1)
targets_batch = reward_batch + np.invert(done_batch).astype(np.float32) * gamma * greedy_q
# update net
if (total_t % 4 == 0):
states_batch = np.array(states_batch)
loss = q_net.update(sess, states_batch, action_batch, targets_batch)
- 如果
done = True,我们将退出内部的while循环,否则,我们将继续到下一个时间步,此时状态将是来自上一时间步的new_state。我们还可以在屏幕上打印出回合号、该回合的时间步数、回合中获得的总奖励、当前的epsilon以及回放缓冲区的大小。这些值对于后续分析也很有用,因此我们将它们存储在名为performance.txt的文本文件中:
if done:
#print("done: ", done)
break
state = next_state
total_t += 1
if (train_or_test == 'train'):
print('\n Episode: ', ep, '| time steps: ', time_steps, '| total episode reward: ', episode_rewards, '| total_t: ', total_t, '| epsilon: ', epsilon, '| replay mem size: ', len(replay_memory))
elif (train_or_test == 'test'):
print('\n Episode: ', ep, '| time steps: ', time_steps, '| total episode reward: ', episode_rewards, '| total_t: ', total_t, '| epsilon: ', epsilon)
if (train_or_test == 'train'):
f = open("experiments/" + str(env.spec.id) + "/performance.txt", "a+")
f.write(str(ep) + " " + str(time_steps) + " " + str(episode_rewards) + " " + str(total_t) + " " + str(epsilon) + '\n')
f.close()
- 接下来的几行代码将完成
dqn.py。首先,我们使用tf.reset_default_graph()重置 TensorFlow 图。然后,我们创建QNetwork类的两个实例,分别是q_net和target_net对象。我们还创建了一个state_processor对象,属于ImageProcess类,并创建了 TensorFlow 的saver对象:
tf.reset_default_graph()
# Q and target networks
q_net = QNetwork(scope="q",VALID_ACTIONS=VALID_ACTIONS)
target_net = QNetwork(scope="target_q", VALID_ACTIONS=VALID_ACTIONS)
# state processor
state_processor = ImageProcess()
# tf saver
saver = tf.train.Saver()
-
接下来,我们将通过调用
tf.Session()来执行 TensorFlow 图。如果我们是从头开始进行训练模式,则必须初始化变量,这可以通过调用sess.run()执行tf.global_variables_initializer()来完成。否则,如果我们处于测试模式,或者在训练模式下但不是从头开始,则通过调用saver.restore()来加载最新的检查点文件。 -
replay_memory_size参数的大小受限于可用的内存大小。本次模拟是在一台 16GB 内存的计算机上进行的,replay_memory_size = 300000是该计算机的内存限制。如果读者有更大的内存,可以为此参数使用更大的值。供参考,DeepMind 使用了 1,000,000 的重放记忆大小。更大的重放记忆大小有助于提供更多的多样性,从而在采样迷你批次时有更好的训练效果:
with tf.Session() as sess:
# load model/ initialize model
if ((train_or_test == 'train' and train_from_scratch == False) or train_or_test == 'test'):
latest_checkpoint = tf.train.latest_checkpoint(checkpoint_dir)
print("loading model ckpt {}...\n".format(latest_checkpoint))
saver.restore(sess, latest_checkpoint)
elif (train_or_test == 'train' and train_from_scratch == True):
sess.run(tf.global_variables_initializer())
# run
deep_q_learning(sess, env, q_net=q_net, target_net=target_net, state_processor=state_processor, num_episodes=25000, train_or_test=train_or_test,train_from_scratch=train_from_scratch, start_iter=start_iter, start_episode=start_episode, replay_memory_size=300000, replay_memory_init_size=5000, update_target_net_every=10000, gamma=0.99, epsilon_start=epsilon_start, epsilon_end=[0.1,0.01], epsilon_decay_steps=[1e6,1e6], batch_size=32)
就是这样——这就完成了 dqn.py。
接下来,我们将评估 DQN 在 Atari Breakout 上的表现。
评估 DQN 在 Atari Breakout 上的表现
在这里,我们将使用之前在代码中编写的 performance.txt 文件,绘制 DQN 算法在 Breakout 上的表现。我们将使用 matplotlib 绘制如下两张图:
-
每个回合的时间步数与回合数的关系
-
总回合奖励与时间步数的关系
这其中涉及的步骤如下:
- 使用 DQN 绘制 Atari Breakout 的时间步数与回合数的关系图:首先,我们在以下图表中绘制了每个训练回合中代理持续的时间步数。如图所示,经过大约 10,000 个回合后,代理已经学会在每个回合中生存 2,000 个时间步数(蓝色曲线)。我们还绘制了使用权重系数 α = 0.02 的指数加权移动平均(橙色曲线)。在训练结束时,平均每个回合持续的时间步数大约为 1,400:

图 1:使用 DQN 在 Atari Breakout 中每回合持续的时间步数
- 绘制回合奖励与时间步数的关系图:在下图中,我们绘制了使用 DQN 算法的 Atari Breakout 中,总回合奖励与时间步数的关系。如图所示,回合奖励的峰值接近 400(蓝色曲线),并且在训练结束时,指数加权移动平均大约在 160 到 180 之间。我们使用的重放记忆大小为 300,000,由于内存限制,这个值相对较小。如果使用更大的重放记忆大小,可能会获得更高的平均回合奖励。这个可以留给读者进行实验:

图 2:使用 DQN 在 Atari Breakout 中的总回合奖励与时间步数的关系
本章内容至此结束,关于 DQN 的讨论完结。
总结
在本章中,我们研究了第一个深度强化学习算法——DQN,它可能是当今最流行的强化学习算法。我们了解了 DQN 背后的理论,也探讨了目标网络的概念及其在稳定训练中的应用。我们还介绍了 Atari 环境,这是目前最流行的强化学习环境套件。事实上,今天发布的许多强化学习论文都将其算法应用于 Atari 套件中的游戏,并报告其每回合的奖励,与使用其他算法的研究人员报告的相应值进行比较。因此,Atari 环境是训练强化学习智能体并与之比较、验证算法稳健性的自然游戏套件。我们还研究了回放缓冲区的使用,并了解了为什么它在脱离策略算法中很有用。
本章为我们深入研究深度强化学习奠定了基础(没有双关的意思!)。在下一章中,我们将研究其他 DQN 的扩展,如 DDQN、对抗网络结构和彩虹网络。
问题
-
为什么在 DQN 中使用回放缓冲区?
-
为什么我们要使用目标网络?
-
为什么我们要将四帧图像堆叠成一个状态?仅使用一帧图像能否足以表示一个状态?
-
为什么有时 Huber 损失比 L2 损失更受欢迎?
-
我们将 RGB 输入图像转换为灰度图像。我们能否直接使用 RGB 图像作为网络的输入?使用 RGB 图像而不是灰度图像有什么优缺点?
进一步阅读
-
通过深度强化学习玩 Atari,作者为Volodymyr Mnih、Koray Kavukcuoglu、David Silver、Alex Graves、Ioannis Antonoglou、Daan Wierstra 和 Martin Riedmiller,arXiv:1312.5602:
arxiv.org/abs/1312.5602 -
通过深度强化学习实现人类水平的控制,作者为Volodymyr Mnih、Koray Kavukcuoglu、David Silver、Andrei A. Rusu、Joel Veness、Marc G. Bellemare、Alex Graves、Martin Riedmiller、Andreas K. Fidjeland、Georg Ostrovski、Stig Petersen、Charles Beattie、Amir Sadik、Ioannis Antonoglou、Helen King、Dharshan Kumaran、Daan Wierstra、Shane Legg 和 Demis Hassabis,Nature,2015:
www.nature.com/articles/nature14236
第四章:Double DQN、对抗架构和彩虹网络
在上一章中,我们讨论了深度 Q 网络(DQN)算法,使用 Python 和 TensorFlow 编写了它,并训练其玩 Atari Breakout。在 DQN 中,使用相同的 Q 网络来选择和评估动作。不幸的是,已知这种方法会高估 Q 值,从而导致值的过度乐观估计。为了缓解这个问题,DeepMind 发布了另一篇论文,提出了将动作选择与动作评估解耦。这就是 Double DQN(DDQN)架构的核心,我们将在本章中探讨这一点。
后来,DeepMind 发布了另一篇论文,提出了一种 Q 网络架构,具有两个输出值,一个表示值 V(s),另一个表示在给定状态下采取某动作的优势 A(s,a)。DeepMind 然后将这两个值结合,计算 Q(s,a) 动作值,而不是像 DQN 和 DDQN 中那样直接确定。这些 Q 网络架构被称为 对抗 网络架构,因为神经网络现在有两个输出值 V(s) 和 A(s,a),这些值随后结合得到 Q(s,a)。我们将在本章中进一步了解这些对抗网络。
另一个我们将在本章考虑的扩展是 彩虹网络,它是几种不同思想融合成一个算法的产物。
本章将涉及以下主题:
-
学习 DDQN 背后的理论
-
编写 DDQN 并训练其玩 Atari Breakout
-
评估 DDQN 在 Atari Breakout 上的表现
-
理解对抗网络架构
-
编写对抗网络架构并训练其玩 Atari Breakout
-
评估对抗架构在 Atari Breakout 上的表现
-
理解彩虹网络
-
在 Dopamine 上运行彩虹网络
技术要求
为了成功完成本章,以下知识将大有帮助:
-
Python(版本 2 或 3)
-
NumPy
-
Matplotlib
-
TensorFlow(版本 1.4 或更高)
-
Dopamine(我们稍后会详细讨论)
理解 Double DQN
DDQN 是 DQN 的扩展,在贝尔曼更新中我们使用目标网络。具体来说,在 DDQN 中,我们使用主网络的 Q 函数的贪心最大化动作来评估目标网络的 Q 函数。首先,我们将使用普通 DQN 目标进行贝尔曼方程更新步骤,然后,我们将扩展到 DDQN 进行相同的贝尔曼方程更新步骤;这就是 DDQN 算法的核心。接下来,我们将用 TensorFlow 编写 DDQN 来玩 Atari Breakout。最后,我们将比较 DQN 和 DDQN 两种算法。
更新贝尔曼方程
在普通 DQN 中,贝尔曼更新的目标是:

θ[t]表示目标网络的模型参数。已知它会过度预测Q,因此在 DDQN 中的改动是将这个目标值y[t]替换为:

我们必须区分 Q 网络参数θ和目标网络模型参数θ[t]。
编写 DDQN 并训练玩 Atari Breakout
我们现在将在 TensorFlow 中编写 DDQN 来玩 Atari Breakout。和之前一样,我们有三个 Python 文件:
-
funcs.py -
model.py -
ddqn.py
funcs.py和model.py与之前在第三章的 DQN 中使用的相同,深度 Q 网络(DQN)。ddqn.py文件是唯一需要修改的代码文件,以实现 DDQN。我们将使用上一章中的dqn.py文件,并对其进行修改以编写 DDQN。所以,首先将之前的dqn.py文件复制一份并将其重命名为ddqn.py。
我们将总结对ddqn.py所做的改动,这些改动其实非常小。我们仍然不会删除文件中与 DQN 相关的代码行,而是使用if语句在两种算法之间进行选择。这样有助于用一份代码同时处理两种算法,这是更好的编码方式。
首先,我们创建一个名为ALGO的变量,它将存储两个字符串之一:DQN或DDQN,这决定了我们要使用哪一个算法:
ALGO = "DDQN" #"DQN" # DDQN
然后,在我们评估 mini-batch 的目标值的代码行中,我们使用if语句来决定使用 DQN 还是 DDQN 算法,并相应地计算目标,如下所示。请注意,在 DQN 中,greedy_q变量存储对应于贪婪动作的 Q 值,即目标网络中最大的 Q 值,这是通过np.amax()计算的,然后用于计算目标变量targets_batch。
而在 DDQN 中,我们计算与主 Q 网络中最大 Q 值对应的动作,这个值存储在greedy_q中,并通过np.argmax()进行评估。然后,我们在目标网络 Q 值中使用greedy_q(它现在表示一个动作)。注意,对于终止时间步骤,即done = True,我们不应考虑下一个状态;而对于非终止步骤,done = False,我们考虑下一个步骤。可以通过对done_batch使用np.invert().astype(np.float32)轻松实现。以下代码行展示了 DDQN:
# calculate q values and targets
if (ALGO == 'DQN'):
q_values_next = target_net.predict(sess, next_states_batch)
greedy_q = np.amax(q_values_next, axis=1)
targets_batch = reward_batch + np.invert(done_batch).astype(np.float32) * gamma * greedy_q
elif (ALGO == 'DDQN'):
q_values_next = q_net.predict(sess, next_states_batch)
greedy_q = np.argmax(q_values_next, axis=1)
q_values_next_target = target_net.predict(sess, next_states_batch)
targets_batch = reward_batch + np.invert(done_batch).astype(np.float32) * gamma * q_values_next_target[np.arange(batch_size), greedy_q]
这就是ddqn.py的内容。我们现在将对其进行 Atari Breakout 的评估。
评估 DDQN 在 Atari Breakout 上的表现
我们现在将评估 DDQN 在 Atari Breakout 上的表现。在这里,我们将使用在代码中编写的performance.txt文件,绘制 DDQN 算法在 Atari Breakout 上的表现。我们将使用matplotlib绘制两张图,如下所述。
在下面的截图中,我们展示了在 Atari Breakout 上使用 DDQN 进行训练时,每集的时间步数及其指数加权移动平均值。如图所示,许多集的时间步数在训练结束时达到了大约 2,000 次,并且有一集的时间步数甚至超过了 3,000 次!移动平均值在训练结束时大约是 1,500 次:

图 1:使用 DDQN 在 Atari Breakout 中,每集的时间步数
在下图中,我们展示了每集获得的总奖励与全球时间步数的关系。峰值奖励超过 350,移动平均值接近 150。有趣的是,移动平均值(橙色曲线)在训练结束时仍在上升,这意味着你可以继续训练以获得更大的收益。对此有兴趣的读者可以进一步探究:

图 2:使用 DDQN 在 Atari Breakout 中,累计奖励与时间步长的关系
请注意,由于 RAM 限制(16 GB),我们仅使用了 300,000 的回放缓冲区大小。如果用户拥有更多的 RAM 资源,可以使用更大的回放缓冲区大小——例如 500,000 到 1,000,000,这可以获得更好的成绩。
如我们所见,DDQN 智能体正在学习如何玩 Atari Breakout。每集奖励的移动平均值在不断上升,这意味着你可以训练更长时间,以获得更高的奖励。这种奖励的上升趋势证明了 DDQN 算法在此类问题中的有效性。
理解对抗网络架构
现在我们来理解对抗网络架构的应用。在 DQN、DDQN 及其他文献中的 DQN 变体中,焦点主要集中在算法上,即如何高效稳定地更新价值函数神经网络。虽然这对于开发强健的强化学习算法至关重要,但推动该领域发展的另一个平行但互补的方向是创新并开发适合无模型强化学习的全新神经网络架构。这正是对抗网络架构的概念,另一个来自 DeepMind 的贡献。
对抗架构涉及的步骤如下:
-
对抗网络架构图;与标准 DQN 进行比较
-
计算 Q(s,a)
-
从
优势函数中减去优势的平均值
正如我们在上一章看到的,在 DQN 中,Q 网络的输出是Q(s,a),即动作价值函数。在对抗网络中,Q 网络的输出改为两个值:状态值函数 V(s) 和优势函数 A(s,a)。然后,你可以将它们结合起来计算状态-动作价值函数 Q(s,a)。这样做的好处在于,网络不需要为每个状态下的每个动作学习价值函数。对于那些动作不影响环境的状态,这尤其有用。
例如,如果智能体是一辆在没有交通的直路上行驶的汽车,则无需采取任何行动,因此在这些状态下,仅V(s)就足够了。另一方面,如果道路突然弯曲或有其他车辆接近智能体,则智能体需要采取行动,因此在这些状态下,advantage 函数开始发挥作用,用于找出给定行动相对于state value函数能提供的增量回报。这就是通过使用两个不同的分支在同一网络中分离估计 V(s) 和 A(s,a) 的直觉,随后将它们合并。
请参考以下图表,展示标准 DQN 网络和对抗网络架构的对比:

图 3:标准 DQN 网络(上)和对抗网络架构(下)的示意图
你可以按如下方式计算 action-value 函数 Q(s,a):

然而,这并不是唯一的,因为你可以在 V(s) 中过预测一个数量 δ,而在 A(s,a) 中预测同样数量的 δ。这会导致神经网络的预测不可辨识。为了绕过这个问题,对抗网络论文的作者建议以下方式来组合 V(s) 和 A(s,a):

|A| 代表动作数量,θ 是在 V(s) 和 A(s,a) 流中共享的神经网络参数;此外,α 和 β 用于表示两个不同流中的神经网络参数,即分别在 A(s,a) 和 V(s) 流中。实际上,在上述方程中,我们从 advantage 函数中减去其平均值,并将其加到 state value 函数中,从而得到 Q(s,a)。
这是对抗网络架构论文的链接:arxiv.org/abs/1511.06581。
编写对抗网络架构并训练它来玩 Atari Breakout
我们现在将编写对抗网络架构,并训练它学习玩 Atari Breakout。对于对抗网络架构,我们需要以下代码:
-
model.py -
funcs.py -
dueling.py
我们将使用之前用于 DDQN 的 funcs.py,因此我们重用它。dueling.py 代码与 ddqn.py 完全相同(它之前已使用过,所以我们只是重命名并重用)。需要进行的唯一更改是在 model.py 中。我们从 DDQN 中复制相同的 model.py 文件,并在此总结对对抗网络架构所做的更改。涉及的步骤如下:
我们首先在 model.py 中创建一个名为 DUELING 的布尔变量,并在使用对抗网络架构时将其赋值为 True,否则赋值为 False:
DUELING = True # False
我们将编写带有if循环的代码,以便在DUELING变量为False时使用我们在 DDQN 中使用的早期代码,而当其为True时使用对战网络。我们将使用flattened对象,它是卷积层输出的扁平化版本,用来创建两个子神经网络流。我们将flattened分别传入两个不同的完全连接层,每个层有512个神经元,使用relu激活函数和先前定义的winit权重初始化器;这些完全连接层的输出值分别称为valuestream和advantagestream:
if (not DUELING):
# Q(s,a)
self.predictions = tf.contrib.layers.fully_connected(fc1, len(self.VALID_ACTIONS), activation_fn=None, weights_initializer=winit)
else:
# Deuling network
# branch out into two streams using flattened (i.e., ignore fc1 for Dueling DQN)
valuestream = tf.contrib.layers.fully_connected(flattened, 512, activation_fn=tf.nn.relu, weights_initializer=winit)
advantagestream = tf.contrib.layers.fully_connected(flattened, 512, activation_fn=tf.nn.relu, weights_initializer=winit)
将 V 和 A 结合得到 Q
advantagestream对象被传入一个完全连接的层,该层的神经元数量等于动作数量,即len(self.VALID_ACTIONS)。同样,valuestream对象被传入一个只有一个神经元的完全连接层。请注意,我们在计算advantage和state value函数时不使用激活函数,因为它们可以是正数也可以是负数(relu会将所有负值设为零!)。最后,我们使用tf.subtract()将advantage和advantage函数的均值相减,从而将优势流和价值流结合起来。均值通过对advantage函数使用tf.reduce_mean()来计算:
# A(s,a)
self.advantage = tf.contrib.layers.fully_connected(advantagestream, len(self.VALID_ACTIONS), activation_fn=None, weights_initializer=winit)
# V(s)
self.value = tf.contrib.layers.fully_connected(valuestream, 1, activation_fn=None, weights_initializer=winit)
# Q(s,a) = V(s) + (A(s,a) - 1/|A| * sum A(s,a'))
self.predictions = self.value + tf.subtract(self.advantage, tf.reduce_mean(self.advantage, axis=1, keep_dims=True))
这就是编码对战网络架构的全部内容。我们将使用对战网络架构训练一个智能体,并评估其在 Atari Breakout 上的表现。请注意,我们可以将对战架构与 DQN 或 DDQN 结合使用。也就是说,我们仅改变了神经网络架构,而没有更改实际的 Bellman 更新,因此对战架构适用于 DQN 和 DDQN。
评估对战架构在 Atari Breakout 上的表现
现在,我们将评估对战架构在 Atari Breakout 上的表现。在这里,我们将使用在训练智能体时写入的performance.txt文件,绘制我们在 Atari Breakout 上使用 DDQN 的对战网络架构的表现。我们将使用matplotlib绘制以下两张图:
在下图中,我们展示了在使用 DDQN 的 Atari Breakout 中每一回合的时间步数(蓝色)以及其指数加权移动平均(橙色)。如图所示,训练结束时许多回合的时间步数峰值约为 2,000,甚至有少数回合超过了 4,000 时间步!而在训练结束时,移动平均约为 1,500 时间步:

图 4:使用对战网络架构和 DDQN 在 Atari Breakout 中每回合的时间步数
在以下截图中,我们展示了每回合收到的总奖励与全局时间步长的时间关系。峰值回合奖励超过 400,移动平均值接近 220。我们还注意到,移动平均值(橙色线)在接近尾部时仍然在增加,这意味着你可以继续运行训练,以获得进一步的提升。总体而言,使用对抗网络架构的平均奖励高于非对抗架构,因此强烈推荐使用这些对抗网络架构:

图 5:使用对抗网络架构和 DDQN 进行 Atari Breakout 时,总回合奖励与全局时间步长的关系
请注意,由于 RAM 限制(16 GB),我们只使用了 300,000 的回放缓冲区大小。如果用户拥有更多的 RAM,可以使用更大的回放缓冲区大小——例如 500,000 到 1,000,000,这样可能会得到更好的得分。
理解 Rainbow 网络
现在我们将讨论Rainbow 网络,它是多个不同 DQN 改进的融合。从原始 DQN 论文开始,提出了几个不同的改进并取得了显著成功。这促使 DeepMind 将多个不同的改进结合到一个集成的智能体中,并称之为Rainbow DQN。具体来说,六个不同的 DQN 改进被集成到一个 Rainbow DQN 智能体中。这六个改进总结如下:
-
DDQN
-
对抗网络架构
-
优先经验回放
-
多步学习
-
分布式强化学习
-
噪声网络
DQN 改进
我们已经看过了 DDQN 和对抗网络架构,并在 TensorFlow 中实现了它们。其余的改进将在接下来的章节中描述。
优先经验回放
我们使用了一个回放缓冲区,其中所有样本都有相等的被采样概率。然而,这种方法并不是很高效,因为某些样本比其他样本更为重要。这就是优先经验回放的动机,优先经验回放中,具有更高时序差分(TD)误差的样本将以更高的概率被采样。第一次将样本添加到回放缓冲区时,给它设置一个最大优先级值,以确保缓冲区中的所有样本至少会被采样一次。之后,TD 误差用于确定经验被采样的概率,我们的计算方式如下:

其中,前一个 r 是奖励,θ 是主 Q 网络模型参数,θ^t 是目标网络参数。ω 是一个正的超参数,用于确定分布的形状。
多步学习
在 Q 学习中,我们会累积单一奖励,并在下一个步骤使用贪婪动作。或者,你也可以使用多步目标,并从一个状态计算 n 步回报:

然后,n步回报,r[t]^((n)),在贝尔曼更新中使用,并且已知能够加速学习。
分布式强化学习
在分布式强化学习中,我们学习逼近回报的分布,而不是期望回报。这在数学上是复杂的,超出了本书的范围,因此不再进一步讨论。
嘈杂网络
在某些游戏中(如《蒙特祖马的复仇》),ε-贪婪策略表现不佳,因为需要执行许多动作才能获得第一个奖励。在这种设置下,推荐使用结合了确定性流和嘈杂流的嘈杂线性层,如下所示:

在这里,x是输入,y是输出,b和W分别是确定性流中的偏置和权重;b(noisy)*和*W(noisy)分别是嘈杂流中的偏置和权重;εb*和*εW是随机变量,并作为元素级的乘积应用于嘈杂流中的偏置和权重。网络可以选择在状态空间的某些区域忽略嘈杂流,也可以根据需要使用它们。这允许根据状态确定的探索策略。
我们不会编写完整的 Rainbow DQN 代码,因为它非常复杂。相反,我们将使用一个开源框架叫做 Dopamine 来训练 Rainbow DQN 智能体,具体内容将在下一节讨论。
在多巴胺上运行 Rainbow 网络
2018 年,谷歌的一些工程师发布了一个开源、轻量级、基于 TensorFlow 的强化学习训练框架,名为Dopamine。正如你可能已经知道的,多巴胺是大脑中一种重要的有机化学物质。我们将使用 Dopamine 来运行 Rainbow。
Dopamine 框架基于四个设计原则:
-
简单的实验
-
灵活的开发
-
紧凑且可靠
-
可重现性
要从 GitHub 下载 Dopamine,请在终端中输入以下命令:
git clone https://github.com/google/dopamine.git
我们可以通过在终端中输入以下命令来测试 Dopamine 是否成功安装:
cd dopamine
export PYTHONPATH=${PYTHONPATH}:.
python tests/atari_init_test.py
其输出将类似于以下内容:
2018-10-27 23:08:17.810679: I tensorflow/core/platform/cpu_feature_guard.cc:141] Your CPU supports instructions that this TensorFlow binary was not compiled to use: SSE4.1 SSE4.2 AVX AVX2 FMA
2018-10-27 23:08:18.079916: I tensorflow/stream_executor/cuda/cuda_gpu_executor.cc:897] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero
2018-10-27 23:08:18.080741: I tensorflow/core/common_runtime/gpu/gpu_device.cc:1392] Found device 0 with properties:
name: GeForce GTX 1060 with Max-Q Design major: 6 minor: 1 memoryClockRate(GHz): 1.48
pciBusID: 0000:01:00.0
totalMemory: 5.93GiB freeMemory: 5.54GiB
2018-10-27 23:08:18.080783: I tensorflow/core/common_runtime/gpu/gpu_device.cc:1471] Adding visible gpu devices: 0
2018-10-27 23:08:24.476173: I tensorflow/core/common_runtime/gpu/gpu_device.cc:952] Device interconnect StreamExecutor with strength 1 edge matrix:
2018-10-27 23:08:24.476247: I tensorflow/core/common_runtime/gpu/gpu_device.cc:958] 0
2018-10-27 23:08:24.476273: I tensorflow/core/common_runtime/gpu/gpu_device.cc:971] 0: N
2018-10-27 23:08:24.476881: I tensorflow/core/common_runtime/gpu/gpu_device.cc:1084] Created TensorFlow device (/job:localhost/replica:0/task:0/device:GPU:0 with 5316 MB memory) -> physical GPU (device: 0, name: GeForce GTX 1060 with Max-Q Design, pci bus id: 0000:01:00.0, compute capability: 6.1)
.
.
.
Ran 2 tests in 8.475s
OK
你应该在最后看到OK,以确认下载一切正常。
使用多巴胺的 Rainbow
要运行 Rainbow DQN,请在终端中输入以下命令:
python -um dopamine.atari.train --agent_name=rainbow --base_dir=/tmp/dopamine --gin_files='dopamine/agents/rainbow/configs/rainbow.gin'
就这样。多巴胺将开始训练 Rainbow DQN,并在屏幕上打印训练统计信息,同时保存检查点文件。配置文件存储在以下路径:
dopamine/dopamine/agents/rainbow/configs/rainbow.gin
代码如下所示。game_name 默认设置为 Pong;也可以尝试其他 Atari 游戏。训练的代理步骤数设置在training_steps中,评估步骤数设置在evaluation_steps中。此外,它通过使用粘性动作的概念引入了训练中的随机性,其中最近的动作以 0.25 的概率被重复多次。也就是说,如果均匀随机数(使用 NumPy 的np.random.rand()计算)< 0.25,则重复最近的动作;否则,从策略中采取新的动作。
粘性动作是一种引入学习随机性的新方法:
# Hyperparameters follow Hessel et al. (2018), except for sticky_actions,
# which was False (not using sticky actions) in the original paper.
import dopamine.agents.rainbow.rainbow_agent
import dopamine.atari.run_experiment
import dopamine.replay_memory.prioritized_replay_buffer
import gin.tf.external_configurables
RainbowAgent.num_atoms = 51
RainbowAgent.vmax = 10.
RainbowAgent.gamma = 0.99
RainbowAgent.update_horizon = 3
RainbowAgent.min_replay_history = 20000 # agent steps
RainbowAgent.update_period = 4
RainbowAgent.target_update_period = 8000 # agent steps
RainbowAgent.epsilon_train = 0.01
RainbowAgent.epsilon_eval = 0.001
RainbowAgent.epsilon_decay_period = 250000 # agent steps
RainbowAgent.replay_scheme = 'prioritized'
RainbowAgent.tf_device = '/gpu:0' # use '/cpu:*' for non-GPU version
RainbowAgent.optimizer = @tf.train.AdamOptimizer()
# Note these parameters are different from C51's.
tf.train.AdamOptimizer.learning_rate = 0.0000625
tf.train.AdamOptimizer.epsilon = 0.00015
Runner.game_name = 'Pong'
# Sticky actions with probability 0.25, as suggested by (Machado et al., 2017).
Runner.sticky_actions = True
Runner.num_iterations = 200
Runner.training_steps = 250000 # agent steps
Runner.evaluation_steps = 125000 # agent steps
Runner.max_steps_per_episode = 27000 # agent steps
WrappedPrioritizedReplayBuffer.replay_capacity = 1000000
WrappedPrioritizedReplayBuffer.batch_size = 32
随意尝试不同的超参数,看看它们如何影响学习。这是检验不同超参数对强化学习代理学习敏感度的一个非常好的方法。
总结
在本章中,我们介绍了 DDQN、对战网络架构和彩虹 DQN。我们将之前的 DQN 代码扩展到 DDQN 和对战架构,并在 Atari Breakout 上进行了测试。我们可以清楚地看到,随着这些改进,平均每集奖励更高,因此这些改进自然成为使用的选择。接下来,我们还看到了谷歌的 Dopamine,并使用它来训练一个彩虹 DQN 代理。Dopamine 有几个其他强化学习算法,鼓励用户深入挖掘并尝试这些其他的强化学习算法。
本章深入探讨了 DQN 的变体,我们在强化学习算法的编码方面确实取得了很多进展。在下一章中,我们将学习下一种强化学习算法,叫做深度确定性策略梯度(DDPG),这是我们第一个演员-评论员(Actor-Critic)强化学习算法,也是我们第一个连续动作空间的强化学习算法。
问题
-
为什么 DDQN 比 DQN 表现更好?
-
对战斗网络架构如何帮助训练?
-
为什么优先经验回放能加速训练?
-
粘性动作如何帮助训练?
进一步阅读
-
DDQN 论文,使用双重 Q 学习的深度强化学习,由 Hado van Hasselt、Arthur Guez 和 David Silver 撰写,可以通过以下链接获取,建议有兴趣的读者阅读:
arxiv.org/abs/1509.06461 -
彩虹:结合深度强化学习中的改进,Matteo Hessel,Joseph Modayil,Hado van Hasselt,Tom Schaul,Georg Ostrovski,Will Dabney,Dan Horgan,Bilal Piot,Mohammad Azar,David Silver,arXiv:1710.02298(彩虹 DQN):
arxiv.org/abs/1710.02298 -
优先经验回放,Tom Schaul,John Quan,Ioannis Antonoglou,David Silver,arXiv:1511.05952:
arxiv.org/abs/1511.05952 -
多步强化学习:一个统一的算法,作者:Kristopher de Asis、J Fernando Hernandez-Garcia、G Zacharias Holland、Richard S Sutton:
arxiv.org/pdf/1703.01327.pdf -
噪声网络用于探索,作者:Meire Fortunato、Mohammad Gheshlaghi Azar、Bilal Piot、Jacob Menick、Ian Osband、Alex Graves、Vlad Mnih、Remi Munos、Demis Hassabis、Olivier Pietquin、Charles Blundell 和 Shane Legg,arXiv:1706.10295:
arxiv.org/abs/1706.10295
第五章:深度确定性策略梯度(Deep Deterministic Policy Gradient)
在之前的章节中,你已经看到如何使用强化学习(RL)来解决离散动作问题,如 Atari 游戏中的问题。现在我们将基于此,处理连续的、实值动作问题。连续控制问题非常普遍,例如,机器人手臂的电机扭矩;自动驾驶汽车的转向、加速和刹车;地形上的轮式机器人运动;以及无人机的俯仰、滚转和偏航控制。对于这些问题,我们在强化学习环境中训练神经网络以输出实值动作。
许多连续控制算法涉及两个神经网络——一个被称为演员(基于策略),另一个被称为评论员(基于价值)——因此,这类算法被称为演员-评论员算法。演员的角色是学习一个好的策略,能够根据给定状态预测出好的动作。评论员的角色是评估演员是否采取了一个好的动作,并提供反馈,作为演员学习的信号。这就像学生-老师或员工-老板的关系,其中学生或员工完成一项任务或工作,而老师或老板的角色是提供关于所执行动作质量的反馈。
连续控制强化学习的基础是通过所谓的策略梯度,它是一个估计值,用来表示神经网络的权重应该如何调整,以最大化长期累计折扣奖励。具体来说,它利用了链式法则,并且它是一个需要反向传播到演员网络中的梯度估计,以便改进策略。这个估计通过小批量样本的平均值来评估。我们将在本章中介绍这些话题,特别是,我们将介绍一个名为深度确定性策略梯度(DDPG)的算法,它是用于连续控制的最先进的强化学习算法。
连续控制在现实世界中有许多应用。例如,连续控制可以用于评估自动驾驶汽车的转向、加速和刹车。它还可以用于确定机器人执行器电机所需的扭矩。或者,它可以应用于生物医学领域,控制可能是确定类人运动的肌肉运动。因此,连续控制问题的应用非常广泛。
本章将涵盖以下主题:
-
演员-评论员算法和策略梯度
-
DDPG
-
在 Pendulum-v0 上训练和测试 DDPG
技术要求
为了成功完成本章,以下内容是必需的:
-
Python(2 及以上版本)
-
NumPy
-
Matplotlib
-
TensorFlow(版本 1.4 或更高)
-
一台至少拥有 8GB 内存的电脑(更高内存更佳!)
演员-评论员算法和策略梯度
在本节中,我们将介绍什么是演员-评论家(Actor-Critic)算法。你还将了解什么是策略梯度以及它们如何对演员-评论家算法有所帮助。
学生如何在学校学习呢?学生在学习过程中通常会犯很多错误。当他们在某个任务上表现良好时,老师会给予正面反馈。另一方面,如果学生在某个任务上表现不佳,老师会提供负面反馈。这些反馈作为学生改进任务的学习信号。这就是演员-评论家算法的核心。
以下是涉及的步骤概述:
-
我们将有两个神经网络,一个被称为演员,另一个被称为评论家。
-
演员就像我们之前描述的学生,在给定状态下采取行动。
-
评论家就像我们之前描述的老师,提供反馈给演员学习。
-
与学校中的老师不同,评论家网络也应该从头开始训练,这使得问题变得具有挑战性。
-
策略梯度用于训练演员
-
Bellman 更新的 L2 范数用于训练评论家
策略梯度
策略梯度定义如下:

J 是需要最大化的长期奖励函数,θ 是策略神经网络参数,N 是小批量的样本大小,Q(s,a) 是状态-动作值函数,π 是策略。换句话说,我们计算状态-动作值函数相对于动作的梯度,以及策略相对于网络参数的梯度,将它们相乘,并对来自小批量数据的 N 个样本取平均。然后,我们可以在梯度上升的设置中使用这个策略梯度来更新策略参数。请注意,本质上是利用微积分的链式法则来评估策略梯度。
深度确定性策略梯度(DDPG)
我们现在将深入探讨 DDPG 算法,它是一个用于连续控制的最先进的强化学习(RL)算法。它最早由 Google DeepMind 于 2016 年发布,并在社区中引起了广泛关注,随后提出了若干新变体。与 DQN 类似,DDPG 也使用目标网络以提高稳定性。它还使用回放缓冲区来重用过去的数据,因此,它是一个脱离策略的强化学习算法。
ddpg.py 文件是我们开始训练和测试的主要文件。它将调用存在于 TrainOrTest.py 中的训练或测试函数。AandC.py 文件包含演员和评论家网络的 TensorFlow 代码。最后,replay_buffer.py 通过使用双端队列(deque)数据结构将样本存储在回放缓冲区中。我们将训练 DDPG 来学习保持倒立摆竖直,使用 OpenAI Gym 的 Pendulum-v0,它有三个状态和一个连续动作,即施加的扭矩,用来保持倒立摆的竖直状态。
编码 ddpg.py
我们将首先编写 ddpg.py 文件。涉及的步骤如下:
现在,我们将总结 DDPG 代码:
- 导入所需的包:我们将导入所需的包和其他 Python 文件:
import tensorflow as tf
import numpy as np
import gym
from gym import wrappers
import argparse
import pprint as pp
import sys
from replay_buffer import ReplayBuffer
from AandC import *
from TrainOrTest import *
- 定义 train() 函数:我们将定义
train()函数。它接受参数解析器对象args。我们创建一个 TensorFlow 会话作为sess。环境的名称用于创建一个 Gym 环境并存储在env对象中。我们还设置了随机数种子和环境中每一剧集的最大步数。我们还在state_dim和action_dim中设置了状态和动作维度,对于 Pendulum-v0 问题,它们的值分别是3和1。然后,我们创建演员和评论员对象,这些对象分别是ActorNetwork类和CriticNetwork类的实例,稍后将在AandC.py 文件中描述。接着,我们调用trainDDPG()函数,这将开始强化学习代理的训练。
最后,我们通过使用 tf.train.Saver() 和 saver.save() 保存 TensorFlow 模型:
def train(args):
with tf.Session() as sess:
env = gym.make(args['env'])
np.random.seed(int(args['random_seed']))
tf.set_random_seed(int(args['random_seed']))
env.seed(int(args['random_seed']))
env._max_episode_steps = int(args['max_episode_len'])
state_dim = env.observation_space.shape[0]
action_dim = env.action_space.shape[0]
action_bound = env.action_space.high
actor = ActorNetwork(sess, state_dim, action_dim, action_bound,
float(args['actor_lr']), float(args['tau']), int(args['minibatch_size']))
critic = CriticNetwork(sess, state_dim, action_dim,
float(args['critic_lr']), float(args['tau']), float(args['gamma']), actor.get_num_trainable_vars())
trainDDPG(sess, env, args, actor, critic)
saver = tf.train.Saver()
saver.save(sess, "ckpt/model")
print("saved model ")
- 定义 test() 函数:接下来定义
test()函数。这将在我们完成训练后使用,用来测试我们的代理的表现如何。test()函数的代码如下,与train()非常相似。我们将通过使用tf.train.Saver()和saver.restore()从train()恢复已保存的模型。然后我们调用testDDPG()函数来测试该模型:
def test(args):
with tf.Session() as sess:
env = gym.make(args['env'])
np.random.seed(int(args['random_seed']))
tf.set_random_seed(int(args['random_seed']))
env.seed(int(args['random_seed']))
env._max_episode_steps = int(args['max_episode_len'])
state_dim = env.observation_space.shape[0]
action_dim = env.action_space.shape[0]
action_bound = env.action_space.high
actor = ActorNetwork(sess, state_dim, action_dim, action_bound,
float(args['actor_lr']), float(args['tau']), int(args['minibatch_size']))
critic = CriticNetwork(sess, state_dim, action_dim,
float(args['critic_lr']), float(args['tau']), float(args['gamma']), actor.get_num_trainable_vars())
saver = tf.train.Saver()
saver.restore(sess, "ckpt/model")
testDDPG(sess, env, args, actor, critic)
-
定义 主 函数:最后,
main函数如下所示。我们通过使用 Python 的argparse定义了一个参数解析器。为演员和评论员指定了学习率,包括折扣因子gamma和目标网络指数平均参数tau。缓冲区大小、迷你批次大小和剧集数量也在参数解析器中指定。我们感兴趣的环境是 Pendulum-v0,且该环境在参数解析器中也被指定。 -
调用 train() 或 test() 函数,根据需要:运行此代码的模式为 train 或 test,并且它会调用适当的同名函数,这是我们之前定义的:
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='provide arguments for DDPG agent')
# agent parameters
parser.add_argument('--actor-lr', help='actor network learning rate', default=0.0001)
parser.add_argument('--critic-lr', help='critic network learning rate', default=0.001)
parser.add_argument('--gamma', help='discount factor for Bellman updates', default=0.99)
parser.add_argument('--tau', help='target update parameter', default=0.001)
parser.add_argument('--buffer-size', help='max size of the replay buffer', default=1000000)
parser.add_argument('--minibatch-size', help='size of minibatch', default=64)
# run parameters
parser.add_argument('--env', help='gym env', default='Pendulum-v0')
parser.add_argument('--random-seed', help='random seed', default=258)
parser.add_argument('--max-episodes', help='max num of episodes', default=250)
parser.add_argument('--max-episode-len', help='max length of each episode', default=1000)
parser.add_argument('--render-env', help='render gym env', action='store_true')
parser.add_argument('--mode', help='train/test', default='train')
args = vars(parser.parse_args())
pp.pprint(args)
if (args['mode'] == 'train'):
train(args)
elif (args['mode'] == 'test'):
test(args)
这就是 ddpg.py 的全部内容。
编写 AandC.py
我们将在 AandC.py 中指定 ActorNetwork 类和 CriticNetwork 类。涉及的步骤如下:
- 导入包:首先,我们导入所需的包:
import tensorflow as tf
import numpy as np
import gym
from gym import wrappers
import argparse
import pprint as pp
import sys
from replay_buffer import ReplayBuffer
- 定义权重和偏差的初始化器:接下来,我们定义权重和偏差的初始化器:
winit = tf.contrib.layers.xavier_initializer()
binit = tf.constant_initializer(0.01)
rand_unif = tf.keras.initializers.RandomUniform(minval=-3e-3,maxval=3e-3)
regularizer = tf.contrib.layers.l2_regularizer(scale=0.0)
- 定义 ActorNetwork 类:
ActorNetwork类定义如下。首先,它在__init__构造函数中接收参数。然后我们调用create_actor_network(),该函数将返回inputs、out和scaled_out对象。演员模型的参数通过调用 TensorFlow 的tf.trainable_variables()存储在self.network_params中。我们对演员的目标网络也做相同的操作。需要注意的是,目标网络是为了稳定性考虑而存在的;它的神经网络架构与演员网络相同,只是参数会逐渐变化。目标网络的参数通过再次调用tf.trainable_variables()被收集并存储在self.target_network_params中:
class ActorNetwork(object):
def __init__(self, sess, state_dim, action_dim, action_bound, learning_rate, tau, batch_size):
self.sess = sess
self.s_dim = state_dim
self.a_dim = action_dim
self.action_bound = action_bound
self.learning_rate = learning_rate
self.tau = tau
self.batch_size = batch_size
# actor
self.state, self.out, self.scaled_out = self.create_actor_network(scope='actor')
# actor params
self.network_params = tf.trainable_variables()
# target network
self.target_state, self.target_out, self.target_scaled_out = self.create_actor_network(scope='act_target')
self.target_network_params = tf.trainable_variables()[len(self.network_params):]
- 定义 self.update_target_network_params:接下来,我们定义
self.update_target_network_params,它会将当前的演员网络参数与tau相乘,将目标网络的参数与1-tau相乘,然后将它们加在一起,存储为一个 TensorFlow 操作。这样我们就逐步更新目标网络的模型参数。注意使用tf.multiply()来将权重与tau(或者根据情况是1-tau)相乘。然后,我们创建一个 TensorFlow 占位符,命名为action_gradient,用来存储与动作相关的Q的梯度,这个梯度由评论员提供。我们还使用tf.gradients()计算策略网络输出相对于网络参数的梯度。注意,接着我们会除以batch_size,以便对小批量数据的求和结果进行平均。这样,我们就得到了平均策略梯度,接下来可以用来更新演员网络参数:
# update target using tau and 1-tau as weights
self.update_target_network_params = \
[self.target_network_params[i].assign(tf.multiply(self.network_params[i], self.tau) + tf.multiply(self.target_network_params[i], 1\. - self.tau))
for i in range(len(self.target_network_params))]
# gradient (this is provided by the critic)
self.action_gradient = tf.placeholder(tf.float32, [None, self.a_dim])
# actor gradients
self.unnormalized_actor_gradients = tf.gradients(
self.scaled_out, self.network_params, -self.action_gradient)
self.actor_gradients = list(map(lambda x: tf.div(x, self.batch_size), self.unnormalized_actor_gradients))
- 使用 Adam 优化:我们使用 Adam 优化算法来应用策略梯度,从而优化演员的策略:
# adam optimization
self.optimize = tf.train.AdamOptimizer(self.learning_rate).apply_gradients(zip(self.actor_gradients, self.network_params))
# num trainable vars
self.num_trainable_vars = len(self.network_params) + len(self.target_network_params)
- 定义 create_actor_network() 函数:现在我们定义
create_actor_network()函数。我们将使用一个包含两层神经元的神经网络,第一层有400个神经元,第二层有300个神经元。权重使用Xavier 初始化,偏置初始值为零。我们使用relu激活函数,并使用批归一化(batch normalization)以确保稳定性。最终的输出层的权重使用均匀分布初始化,并采用tanh激活函数,以保持输出值在一定范围内。对于 Pendulum-v0 问题,动作的范围是[-2,2],而tanh的输出范围是[-1,1],因此我们需要将输出乘以 2 来进行缩放;这可以通过tf.multiply()来实现,其中action_bound = 2表示倒立摆问题中的动作范围:
def create_actor_network(self, scope):
with tf.variable_scope(scope, reuse=tf.AUTO_REUSE):
state = tf.placeholder(name='a_states', dtype=tf.float32, shape=[None, self.s_dim])
net = tf.layers.dense(inputs=state, units=400, activation=None, kernel_initializer=winit, bias_initializer=binit, name='anet1')
net = tf.nn.relu(net)
net = tf.layers.dense(inputs=net, units=300, activation=None, kernel_initializer=winit, bias_initializer=binit, name='anet2')
net = tf.nn.relu(net)
out = tf.layers.dense(inputs=net, units=self.a_dim, activation=None, kernel_initializer=rand_unif, bias_initializer=binit, name='anet_out')
out = tf.nn.tanh(out)
scaled_out = tf.multiply(out, self.action_bound)
return state, out, scaled_out
- 定义演员函数:最后,我们定义完成
ActorNetwork类所需的剩余函数。我们将定义train(),它将运行self.optimize会话;predict()函数运行self.scaled_out会话,即ActorNetwork的输出;predict_target()函数将运行self.target_scaled_out会话,即演员目标网络的输出动作。接着,update_target_network()将运行self.update_target_network_params会话,执行网络参数的加权平均。
最后,get_num_trainable_vars()函数返回可训练变量的数量:
def train(self, state, a_gradient):
self.sess.run(self.optimize, feed_dict={self.state: state, self.action_gradient: a_gradient})
def predict(self, state):
return self.sess.run(self.scaled_out, feed_dict={
self.state: state})
def predict_target(self, state):
return self.sess.run(self.target_scaled_out, feed_dict={
self.target_state: state})
def update_target_network(self):
self.sess.run(self.update_target_network_params)
def get_num_trainable_vars(self):
return self.num_trainable_vars
- 定义 CriticNetwork 类:现在我们将定义
CriticNetwork类。与ActorNetwork类似,我们将模型超参数作为参数传递。然后调用create_critic_network()函数,它将返回inputs、action和out。我们还通过再次调用create_critic_network()来创建评论者的目标网络:
class CriticNetwork(object):
def __init__(self, sess, state_dim, action_dim, learning_rate, tau, gamma, num_actor_vars):
self.sess = sess
self.s_dim = state_dim
self.a_dim = action_dim
self.learning_rate = learning_rate
self.tau = tau
self.gamma = gamma
# critic
self.state, self.action, self.out = self.create_critic_network(scope='critic')
# critic params
self.network_params = tf.trainable_variables()[num_actor_vars:]
# target Network
self.target_state, self.target_action, self.target_out = self.create_critic_network(scope='crit_target')
# target network params
self.target_network_params = tf.trainable_variables()[(len(self.network_params) + num_actor_vars):]
- 评论者目标网络:与演员的目标网络类似,评论者的目标网络也是通过加权平均进行更新。然后,我们创建一个名为
predicted_q_value的 TensorFlow 占位符,它是目标值。接着,我们在self.loss中定义 L2 范数,它是贝尔曼残差的平方误差。请注意,self.out是我们之前看到的Q(s,a),predicted_q_value是贝尔曼方程中的r + γQ(s',a')。我们再次使用 Adam 优化器来最小化这个 L2 损失函数。然后,通过调用tf.gradients()来评估Q(s,a)相对于动作的梯度,并将其存储在self.action_grads中。这个梯度稍后会在计算策略梯度时使用:
# update target using tau and 1 - tau as weights
self.update_target_network_params = \
[self.target_network_params[i].assign(tf.multiply(self.network_params[i], self.tau) \
+ tf.multiply(self.target_network_params[i], 1\. - self.tau))
for i in range(len(self.target_network_params))]
# network target (y_i in the paper)
self.predicted_q_value = tf.placeholder(tf.float32, [None, 1])
# adam optimization; minimize L2 loss function
self.loss = tf.reduce_mean(tf.square(self.predicted_q_value - self.out))
self.optimize = tf.train.AdamOptimizer(self.learning_rate).minimize(self.loss)
# gradient of Q w.r.t. action
self.action_grads = tf.gradients(self.out, self.action)
- 定义 create_critic_network():接下来,我们将定义
create_critic_network()函数。评论者网络的架构与演员相似,唯一不同的是它同时接受状态和动作作为输入。网络有两层隐藏层,分别有400和300个神经元。最后的输出层只有一个神经元,即Q(s,a)状态-动作值函数。请注意,最后一层没有激活函数,因为理论上Q(s,a)是无界的:
def create_critic_network(self, scope):
with tf.variable_scope(scope, reuse=tf.AUTO_REUSE):
state = tf.placeholder(name='c_states', dtype=tf.float32, shape=[None, self.s_dim])
action = tf.placeholder(name='c_action', dtype=tf.float32, shape=[None, self.a_dim])
net = tf.concat([state, action],1)
net = tf.layers.dense(inputs=net, units=400, activation=None, kernel_initializer=winit, bias_initializer=binit, name='cnet1')
net = tf.nn.relu(net)
net = tf.layers.dense(inputs=net, units=300, activation=None, kernel_initializer=winit, bias_initializer=binit, name='cnet2')
net = tf.nn.relu(net)
out = tf.layers.dense(inputs=net, units=1, activation=None, kernel_initializer=rand_unif, bias_initializer=binit, name='cnet_out')
return state, action, out
- 最终,完成
CriticNetwork所需的功能如下。这些与ActorNetwork类似,因此为了简洁起见,我们不再详细说明。不过有一个不同之处,即action_gradients()函数,它是Q(s,a)相对于动作的梯度,由评论者计算并提供给演员,以便用于策略梯度的评估:
def train(self, state, action, predicted_q_value):
return self.sess.run([self.out, self.optimize], feed_dict={self.state: state, self.action: action, self.predicted_q_value: predicted_q_value})
def predict(self, state, action):
return self.sess.run(self.out, feed_dict={self.state: state, self.action: action})
def predict_target(self, state, action):
return self.sess.run(self.target_out, feed_dict={self.target_state: state, self.target_action: action})
def action_gradients(self, state, actions):
return self.sess.run(self.action_grads, feed_dict={self.state: state, self.action: actions})
def update_target_network(self):
self.sess.run(self.update_target_network_params)
这就是AandC.py的内容。
Coding TrainOrTest.py
我们之前使用的trainDDPG()和testDDPG()函数将会在TrainOrTest.py中定义。涉及的步骤如下:
- 导入包和函数:
TrainOrTest.py文件首先导入了相关的包和其他 Python 文件:
import tensorflow as tf
import numpy as np
import gym
from gym import wrappers
import argparse
import pprint as pp
import sys
from replay_buffer import ReplayBuffer
from AandC import *
- 定义 trainDDPG()函数:接下来,我们定义
trainDDPG()函数。首先,我们通过调用sess.run()和tf.global_variables_initializer()初始化所有网络。然后,我们初始化目标网络的权重和重放缓冲区。接着,我们开始训练回合的主循环。在这个循环内,我们重置环境(在我们的案例中是 Pendulum-v0),并开始每个回合内的时间步循环(回顾一下,每个回合有max_episode_len个时间步)。
演员的策略被采样以获得当前状态的动作。我们将这个动作输入env.step(),它执行该动作的一个时间步,并在此过程中移动到下一个状态,s2。环境还会给出这个动作的奖励r,并将是否终止的状态信息存储在布尔变量terminal中。我们将元组(state,action,reward,terminal,new state)添加到重放缓冲区,以便稍后采样和训练:
def trainDDPG(sess, env, args, actor, critic):
sess.run(tf.global_variables_initializer())
# Initialize target networks
actor.update_target_network()
critic.update_target_network()
# Initialize replay memory
replay_buffer = ReplayBuffer(int(args['buffer_size']), int(args['random_seed']))
# start training on episodes
for i in range(int(args['max_episodes'])):
s = env.reset()
ep_reward = 0
ep_ave_max_q = 0
for j in range(int(args['max_episode_len'])):
if args['render_env']:
env.render()
a = actor.predict(np.reshape(s, (1, actor.s_dim)))
s2, r, terminal, info = env.step(a[0])
replay_buffer.add(np.reshape(s, (actor.s_dim,)), np.reshape(a, (actor.a_dim,)), r,
terminal, np.reshape(s2, (actor.s_dim,)))
- 从重放缓冲区采样小批量数据:一旦重放缓冲区中的样本数量超过小批量大小,我们就从缓冲区中采样一个小批量的数据。对于后续的状态
s2,我们使用评论员的目标网络来计算目标Q值,并将其存储在target_q中。注意使用评论员的目标网络而不是评论员网络本身——这是出于稳定性的考虑。然后,我们使用贝尔曼方程来评估目标y_i,其计算为r + γ Q(对于非终止时间步)和r(对于终止时间步):
# sample from replay buffer
if replay_buffer.size() > int(args['minibatch_size']):
s_batch, a_batch, r_batch, t_batch, s2_batch =
replay_buffer.sample_batch(int(args['minibatch
_size']))
# Calculate target q
target_q = critic.predict_target(s2_batch,
actor.predict_target(s2_batch))
y_i = []
for k in range(int(args['minibatch_size'])):
if t_batch[k]:
y_i.append(r_batch[k])
else:
y_i.append(r_batch[k] + critic.gamma *
target_q[k])
- 使用前述内容训练演员和评论员:然后,我们通过调用
critic.train()在小批量数据上训练评论员一步。接着,我们通过调用critic.action_gradients()计算Q相对于动作的梯度,并将其存储在grads中;请注意,这个动作梯度将用于计算策略梯度,正如我们之前提到的。然后,我们通过调用actor.train()并将grads作为参数,以及从重放缓冲区采样的状态,训练演员一步。最后,我们通过调用演员和评论员对象的相应函数更新演员和评论员的目标网络:
# Update critic
predicted_q_value, _ = critic.train(s_batch, a_batch, np.reshape(y_i, (int(args['minibatch_size']), 1)))
ep_ave_max_q += np.amax(predicted_q_value)
# Update the actor policy using gradient
a_outs = actor.predict(s_batch)
grads = critic.action_gradients(s_batch, a_outs)
actor.train(s_batch, grads[0])
# update target networks
actor.update_target_network()
critic.update_target_network()
新状态s2被分配给当前状态s,我们继续到下一个时间步。如果回合已经结束,我们将回合的奖励和其他观测值打印到屏幕上,并将它们写入名为pendulum.txt的文本文件,以便后续分析。由于回合已经结束,我们还会跳出内部for循环:
s = s2
ep_reward += r
if terminal:
print('| Episode: {:d} | Reward: {:d} | Qmax: {:.4f}'.format(i,
int(ep_reward), (ep_ave_max_q / float(j))))
f = open("pendulum.txt", "a+")
f.write(str(i) + " " + str(int(ep_reward)) + " " +
str(ep_ave_max_q / float(j)) + '\n')
break
- 定义 testDDPG():这就完成了
trainDDPG()函数。接下来,我们将展示testDDPG()函数,用于测试我们模型的表现。testDDPG()函数与trainDDPG()函数基本相同,不同之处在于我们没有重放缓冲区,也不会训练神经网络。和之前一样,我们有两个for循环——外层循环控制回合数,内层循环遍历每个回合的时间步。我们通过actor.predict()从训练好的演员策略中采样动作,并使用env.step()让环境按照动作演化。最后,如果terminal == True,我们终止当前回合:
def testDDPG(sess, env, args, actor, critic):
# test for max_episodes number of episodes
for i in range(int(args['max_episodes'])):
s = env.reset()
ep_reward = 0
ep_ave_max_q = 0
for j in range(int(args['max_episode_len'])):
if args['render_env']:
env.render()
a = actor.predict(np.reshape(s, (1, actor.s_dim)))
s2, r, terminal, info = env.step(a[0])
s = s2
ep_reward += r
if terminal:
print('| Episode: {:d} | Reward: {:d} |'.format(i,
int(ep_reward)))
break
这就是 TrainOrTest.py 的全部内容。
编写 replay_buffer.py
我们将使用 deque 数据结构来存储我们的重放缓冲区。涉及的步骤如下:
-
导入所需的包:首先,我们导入所需的包。
-
定义 ReplayBuffer 类:接着我们定义
ReplayBuffer类,传递给__init__()构造函数的参数。self.buffer = deque()函数是用来存储数据的队列实例:
from collections import deque
import random
import numpy as np
class ReplayBuffer(object):
def __init__(self, buffer_size, random_seed=258):
self.buffer_size = buffer_size
self.count = 0
self.buffer = deque()
random.seed(random_seed)
- 定义 add 和 size 函数:接着我们定义
add()函数,将经验作为元组(state,action,reward,terminal,new state)添加到缓冲区。self.count函数用来记录重放缓冲区中样本的数量。如果样本数量小于重放缓冲区的大小(self.buffer_size),我们将当前经验添加到缓冲区,并递增计数。另一方面,如果计数等于(或大于)缓冲区大小,我们通过调用popleft()(deque 的内置函数)丢弃缓冲区中的旧样本。然后,我们将新的经验添加到重放缓冲区;不需要增加计数,因为我们已经丢弃了一个旧的数据样本,并用新数据样本或经验替代了它,因此缓冲区中的样本总数保持不变。我们还定义了size()函数,用于获取当前重放缓冲区的大小:
def add(self, s, a, r, t, s2):
experience = (s, a, r, t, s2)
if self.count < self.buffer_size:
self.buffer.append(experience)
self.count += 1
else:
self.buffer.popleft()
self.buffer.append(experience)
def size(self):
return self.count
- 定义 sample_batch 和 clear 函数:接下来,我们定义
sample_batch()函数,从重放缓冲区中采样batch_size个样本。如果缓冲区中的样本数量小于batch_size,我们就从缓冲区中采样所有样本的数量。否则,我们从重放缓冲区中采样batch_size个样本。然后,我们将这些样本转换为NumPy数组并返回。最后,clear()函数用于完全清空重放缓冲区,使其变为空:
def sample_batch(self, batch_size):
batch = []
if self.count < batch_size:
batch = random.sample(self.buffer, self.count)
else:
batch = random.sample(self.buffer, batch_size)
s_batch = np.array([_[0] for _ in batch])
a_batch = np.array([_[1] for _ in batch])
r_batch = np.array([_[2] for _ in batch])
t_batch = np.array([_[3] for _ in batch])
s2_batch = np.array([_[4] for _ in batch])
return s_batch, a_batch, r_batch, t_batch, s2_batch
def clear(self):
self.buffer.clear()
self.count = 0
这就是 DDPG 代码的全部内容。我们现在开始测试它。
在 Pendulum-v0 上训练和测试 DDPG
我们现在将在 Pendulum-v0 上训练前面的 DDPG 代码。要训练 DDPG 代理,只需在与代码文件相同的目录下,在命令行中输入以下命令:
python ddpg.py
这将开始训练:
{'actor_lr': 0.0001,
'buffer_size': 1000000,
'critic_lr': 0.001,
'env': 'Pendulum-v0',
'gamma': 0.99,
'max_episode_len': 1000,
'max_episodes': 250,
'minibatch_size': 64,
'mode': 'train',
'random_seed': 258,
'render_env': False,
'tau': 0.001}
.
.
.
2019-03-03 17:23:10.529725: I tensorflow/stream_executor/cuda/cuda_diagnostics.cc:300] kernel version seems to match DSO: 384.130.0
| Episode: 0 | Reward: -7981 | Qmax: -6.4859
| Episode: 1 | Reward: -7466 | Qmax: -10.1758
| Episode: 2 | Reward: -7497 | Qmax: -14.0578
一旦训练完成,你也可以测试训练好的 DDPG 智能体,如下所示:
python ddpg.py --mode test
我们还可以通过以下代码绘制训练过程中的每个回合奖励:
import numpy as np
import matplotlib.pyplot as plt
data = np.loadtxt('pendulum.txt')
plt.plot(data[:,0], data[:,1])
plt.xlabel('episode number', fontsize=12)
plt.ylabel('episode reward', fontsize=12)
#plt.show()
plt.savefig("ddpg_pendulum.png")
绘图如下所示:

图 1:在训练过程中,使用 DDPG 算法时 Pendulum-v0 问题的回报曲线
如你所见,DDPG 智能体已经非常好地学习了这个问题。最大奖励值略为负值,这对于这个问题来说是最好的结果。
总结
在本章中,我们介绍了第一个连续动作强化学习算法 DDPG,它恰好也是本书中的第一个演员-评论家算法。DDPG 是一个离策略算法,因为它使用了回放缓冲区。我们还讨论了使用策略梯度来更新演员,以及使用 L2 范数来更新评论家。因此,我们有两个不同的神经网络。演员学习策略,而评论家学习评估演员的策略,从而为演员提供学习信号。你学到了如何计算状态-动作值Q(s,a)相对于动作的梯度,以及策略的梯度,这两个梯度合并来评估策略梯度,然后用来更新演员。我们在倒立摆问题上训练了 DDPG,智能体学得非常好。
本章我们走了很长一段路。你已经了解了演员-评论家算法,并且学习了如何编写第一个连续控制强化学习算法。在下一章,你将学习A3C 算法,它是一种在策略的深度强化学习算法。
问题
-
DDPG 是一个在策略算法还是离策略算法?
-
我们为演员和评论家使用了相同的神经网络架构。这样做是必须的吗?还是我们可以为演员和评论家选择不同的神经网络架构?
-
我们可以用 DDPG 来玩 Atari Breakout 吗?
-
为什么神经网络的偏置初始化为小的正值?
-
这是一个练习:你能修改本章中的代码来训练一个智能体,学习更具挑战性的 InvertedDoublePendulum-v2 问题吗?这个问题比本章中的 Pendulum-v0 要更具挑战性。
-
这里有另一个练习:改变神经网络架构,检查智能体是否能学会 Pendulum-v0 问题。例如,可以逐步减少第一层隐藏层中神经元的数量,使用值 400、100、25、10、5 和 1,然后检查智能体在不同神经元数量下的表现。如果神经元数量太小,可能会导致信息瓶颈,即网络的输入没有得到充分表示;也就是说,随着网络的加深,信息会丢失。你观察到了这个效果吗?
进一步阅读
- 深度强化学习中的连续控制,作者:Timothy P. Lillicrap、Jonathan J. Hunt、Alexander Pritzel、Nicolas Heess、Tom Erez、Yuval Tassa、David Silver 和 Daan Wierstra,原始 DDPG 论文来自 DeepMind,arXiv:1509.02971:
arxiv.org/abs/1509.02971
第六章:异步方法 - A3C 和 A2C
我们在上一章中看过 DDPG 算法。DDPG 算法(以及之前看到的 DQN 算法)的一个主要缺点是使用重放缓冲区来获取独立同分布的数据样本进行训练。使用重放缓冲区会消耗大量内存,这在强健的 RL 应用中是不可取的。为了解决这个问题,Google DeepMind 的研究人员提出了一种叫做 异步优势演员评论家(A3C)的在线算法。A3C 不使用重放缓冲区;而是使用并行工作处理器,在这里创建环境的不同实例并收集经验样本。一旦收集到有限且固定数量的样本,它们将用于计算策略梯度,并异步发送到中央处理器更新策略。更新后的策略随后会发送回工作处理器。使用并行处理器体验环境的不同场景可以产生独立同分布的样本,这些样本可以用来训练策略。本章将介绍 A3C,同时也会简要提到它的一个变种,叫做 优势演员评论家(A2C)。
本章将涵盖以下主题:
-
A3C 算法
-
A3C 算法应用于 CartPole
-
A3C 算法应用于 LunarLander
-
A2C 算法
在本章中,您将了解 A3C 和 A2C 算法,并学习如何使用 Python 和 TensorFlow 编写代码。我们还将把 A3C 算法应用于解决两个 OpenAI Gym 问题:CartPole 和 LunarLander。
技术要求
要顺利完成本章,以下知识将非常有帮助:
-
TensorFlow(版本 1.4 或更高)
-
Python(版本 2 或 3)
-
NumPy
A3C 算法
如前所述,A3C 中有并行工作者,每个工作者将计算策略梯度并将其传递给中央(或主)处理器。A3C 论文还使用 advantage 函数来减少策略梯度中的方差。loss 函数由三部分组成,它们加权相加;包括价值损失、策略损失和熵正则化项。价值损失,L[v],是状态值和目标值的 L2 损失,后者是通过折扣奖励和奖励总和计算得出的。策略损失,L[p],是策略分布的对数与 advantage 函数 A 的乘积。熵正则化,L[e],是香农熵,它是策略分布与其对数的乘积,并带有负号。熵正则化项类似于探索的奖励;熵越高,策略的正则化效果越好。这三项的加权分别为 0.5、1 和 -0.005。
损失函数
值损失计算为三个损失项的加权和:值损失 L[v],策略损失 L[p] 和熵正则化项 L[e],它们的计算方式如下:

L 是总损失,需要最小化。注意,我们希望最大化 advantage 函数,因此在 L[p] 中有一个负号,因为我们在最小化 L。同样,我们希望最大化熵项,而由于我们在最小化 L,因此在 L 中有一个负号的项 -0.005 L[e]。
CartPole 和 LunarLander
在本节中,我们将 A3C 应用于 OpenAI Gym 的 CartPole 和 LunarLander。
CartPole
CartPole 由一个垂直的杆子和一个推车组成,需要通过将推车向左或向右移动来保持平衡。CartPole 的状态维度为四,动作维度为二。
更多关于 CartPole 的详情,请查看以下链接:gym.openai.com/envs/CartPole-v0/。
LunarLander
LunarLander,顾名思义,涉及着陆器在月球表面着陆。例如,当阿波罗 11 号的鹰号着陆器在 1969 年着陆月球表面时,宇航员尼尔·阿姆斯特朗和巴兹·奥尔德林必须在下降的最后阶段控制火箭推进器,并安全地将航天器降落到月球表面。之后,阿姆斯特朗走上月球并说道那句如今家喻户晓的话:“人类的一小步, mankind 的一大步”。在 LunarLander 中,月球表面有两面黄色旗帜,目标是将航天器降落在这两面旗帜之间。与阿波罗 11 号的鹰号着陆器不同,LunarLander 的燃料是无限的。LunarLander 的状态维度为八,动作维度为四,四个动作分别是:不做任何操作、启动左侧推进器、启动主推进器,或者启动右侧推进器。
查看以下链接,获取环境的示意图:gym.openai.com/envs/LunarLander-v2/。
A3C 算法应用于 CartPole
在这里,我们将用 TensorFlow 编写 A3C 并应用它,以便训练一个代理来学习 CartPole 问题。编写代码时需要以下代码文件:
-
cartpole.py:此文件将启动训练或测试过程 -
a3c.py:此文件中编写了 A3C 算法 -
utils.py:此文件包含实用函数
编写 cartpole.py
现在,我们将开始编写 cartpole.py。请按照以下步骤开始:
- 首先,我们导入相关包:
import numpy as np
import matplotlib.pyplot as plt
import tensorflow as tf
import gym
import os
import threading
import multiprocessing
from random import choice
from time import sleep
from time import time
from a3c import *
from utils import *
- 接下来,我们设置问题的参数。我们只需训练
200个回合(没错,CartPole 是个简单问题!)。我们将折扣因子 gamma 设置为0.99。CartPole 的状态和动作维度分别为4和2。如果你想加载一个预训练的模型并继续训练,请将load_model设置为True;如果从头开始训练,请将其设置为False。我们还将设置model_path:
max_episode_steps = 200
gamma = 0.99
s_size = 4
a_size = 2
load_model = False
model_path = './model'
- 我们重置 TensorFlow 图,并创建一个用于存储模型的目录。我们将主处理器称为 CPU 0,工作线程的 CPU 编号为非零值。主处理器将执行以下任务:首先,它将创建一个
global_episodes对象,用于计算全局变量的数量。工作线程的总数将存储在num_workers中,我们可以通过调用 Python 的 multiprocessing 库中的cpu_count()来获取系统中可用的处理器数量。我们将使用 Adam 优化器,并将其存储在名为trainer的对象中,同时设定适当的学习率。接着,我们将定义一个名为AC的演员-评论家类,因此我们必须首先创建一个AC类型的主网络对象,命名为master_network,并传递适当的参数给该类的构造函数。然后,对于每个工作线程,我们将创建一个独立的 CartPole 环境实例和一个Worker类实例(稍后定义)。最后,为了保存模型,我们还将创建一个 TensorFlow saver:
tf.reset_default_graph()
if not os.path.exists(model_path):
os.makedirs(model_path)
with tf.device("/cpu:0"):
# keep count of global episodes
global_episodes = tf.Variable(0,dtype=tf.int32,name='global_episodes',trainable=False)
# number of worker threads
num_workers = multiprocessing.cpu_count()
# Adam optimizer
trainer = tf.train.AdamOptimizer(learning_rate=2e-4, use_locking=True)
# global network
master_network = AC(s_size,a_size,'global',None)
workers = []
for i in range(num_workers):
env = gym.make('CartPole-v0')
workers.append(Worker(env,i,s_size,a_size,trainer,model_path,global_episodes))
# tf saver
saver = tf.train.Saver(max_to_keep=5)
- 然后,我们启动 TensorFlow 会话。在会话中,我们为不同的工作线程创建一个 TensorFlow 协调器。接着,我们要么加载或恢复一个预训练的模型,要么运行
tf.global_variables_initializer()来为所有权重和偏差分配初始值:
with tf.Session() as sess:
# tf coordinator for threads
coord = tf.train.Coordinator()
if load_model == True:
print ('Loading Model...')
ckpt = tf.train.get_checkpoint_state(model_path)
saver.restore(sess,ckpt.model_checkpoint_path)
else:
sess.run(tf.global_variables_initializer())
- 然后,我们启动
worker_threads。具体来说,我们调用work()函数,它是Worker()类的一部分(稍后定义)。threading.Thread()将为每个worker分配一个线程。通过调用start(),我们启动了worker线程。最后,我们需要合并这些线程,确保它们在所有线程完成之前不会终止:
# start the worker threads
worker_threads = []
for worker in workers:
worker_work = lambda: worker.work(max_episode_steps, gamma, sess, coord,saver)
t = threading.Thread(target=(worker_work))
t.start()
worker_threads.append(t)
coord.join(worker_threads)
你可以在 www.tensorflow.org/api_docs/python/tf/train/Coordinator 了解更多关于 TensorFlow 协调器的信息。
编写 a3c.py
现在我们将编写 a3c.py。这涉及以下步骤:
-
导入包
-
设置权重和偏差的初始化器
-
定义
AC类 -
定义
Worker类
首先,我们需要导入必要的包:
import numpy as np
import matplotlib.pyplot as plt
import tensorflow as tf
import gym
import threading
import multiprocessing
from random import choice
from time import sleep
from time import time
from threading import Lock
from utils import *
然后,我们需要为权重和偏差设置初始化器;具体来说,我们使用 Xavier 初始化器来初始化权重,并使用零初始化偏差。对于网络的最后输出层,权重是指定范围内的均匀随机数:
xavier = tf.contrib.layers.xavier_initializer()
bias_const = tf.constant_initializer(0.05)
rand_unif = tf.keras.initializers.RandomUniform(minval=-3e-3,maxval=3e-3)
regularizer = tf.contrib.layers.l2_regularizer(scale=5e-4)
AC 类
现在我们将描述 AC 类,它也是 a3c.py 的一部分。我们为 AC 类定义了构造函数,包含一个输入占位符,以及两个全连接的隐藏层,分别有 256 和 128 个神经元,并使用 elu 激活函数。接着是策略网络,使用 softmax 激活函数,因为我们在 CartPole 中的动作是离散的。此外,我们还有一个没有激活函数的值网络。请注意,我们对策略和价值网络共享相同的隐藏层,这与过去的示例不同:
class AC():
def __init__(self,s_size,a_size,scope,trainer):
with tf.variable_scope(scope):
self.inputs = tf.placeholder(shape=[None,s_size],dtype=tf.float32)
# 2 FC layers
net = tf.layers.dense(self.inputs, 256, activation=tf.nn.elu, kernel_initializer=xavier, bias_initializer=bias_const, kernel_regularizer=regularizer)
net = tf.layers.dense(net, 128, activation=tf.nn.elu, kernel_initializer=xavier, bias_initializer=bias_const, kernel_regularizer=regularizer)
# policy
self.policy = tf.layers.dense(net, a_size, activation=tf.nn.softmax, kernel_initializer=xavier, bias_initializer=bias_const)
# value
self.value = tf.layers.dense(net, 1, activation=None, kernel_initializer=rand_unif, bias_initializer=bias_const)
对于worker线程,我们需要定义loss函数。因此,当 TensorFlow 作用域不是global时,我们定义一个动作占位符,以及其独热表示;我们还为target值和advantage函数定义占位符。然后,我们计算策略分布和独热动作的乘积,将它们相加,并将它们存储在policy_times_a对象中。然后,我们组合这些项来构建loss函数,正如我们之前提到的。我们计算值的 L2 损失的批次总和;策略分布乘以其对数的香农熵,带有一个负号;作为策略分布对数的乘积的loss函数;以及批次样本上advantage函数的总和。最后,我们使用适当的权重结合这些损失来计算总损失,存储在self.loss中:
# only workers need tf operations for loss functions and gradient updating
if scope != 'global':
self.actions = tf.placeholder(shape=[None],dtype=tf.int32)
self.actions_onehot = tf.one_hot(self.actions,a_size,dtype=tf.float32)
self.target_v = tf.placeholder(shape=[None],dtype=tf.float32)
self.advantages = tf.placeholder(shape=[None],dtype=tf.float32)
self.policy_times_a = tf.reduce_sum(self.policy * self.actions_onehot, [1])
# loss
self.value_loss = 0.5 * tf.reduce_sum(tf.square(self.target_v - tf.reshape(self.value,[-1])))
self.entropy = - tf.reduce_sum(self.policy * tf.log(self.policy + 1.0e-8))
self.policy_loss = -tf.reduce_sum(tf.log(self.policy_times_a + 1.0e-8) * self.advantages)
self.loss = 0.5 * self.value_loss + self.policy_loss - self.entropy * 0.005
正如您在上一章中看到的,我们使用tf.gradients()来计算策略梯度;具体来说,我们计算loss函数相对于本地网络变量的梯度,后者从tf.get_collection()中获得。为了减少梯度爆炸问题,我们使用 TensorFlow 的tf.clip_by_global_norm()函数将梯度裁剪为40.0的大小。然后,我们可以使用tf.get_collection()来收集全局网络的网络参数,作用于 Adam 优化器中的梯度,使用apply_gradients()。这将计算策略梯度:
# get gradients from local networks using local losses; clip them to avoid exploding gradients
local_vars = tf.get_collection(tf.GraphKeys.TRAINABLE_VARIABLES, scope)
self.gradients = tf.gradients(self.loss,local_vars)
self.var_norms = tf.global_norm(local_vars)
grads,self.grad_norms = tf.clip_by_global_norm(self.gradients,40.0)
# apply local gradients to global network using tf.apply_gradients()
global_vars = tf.get_collection(tf.GraphKeys.TRAINABLE_VARIABLES, 'global')
self.apply_grads = trainer.apply_gradients(zip(grads,global_vars))
Worker()类
现在我们将描述Worker()类,每个工作线程都会使用。首先,我们为该类定义__init__()构造函数。在其中,我们定义工作人员的名称、编号、模型路径、Adam 优化器、全局剧集计数以及增加它的操作符:
class Worker():
def __init__(self,env,name,s_size,a_size,trainer,model_path,global_episodes):
self.name = "worker_" + str(name)
self.number = name
self.model_path = model_path
self.trainer = trainer
self.global_episodes = global_episodes
self.increment = self.global_episodes.assign_add(1)
我们还创建了AC类的本地实例,并传入适当的参数。然后,我们创建一个 TensorFlow 操作,将全局模型参数复制到本地。我们还创建了一个在对角线上具有一的 NumPy 单位矩阵,以及一个环境对象:
# local copy of the AC network
self.local_AC = AC(s_size,a_size,self.name,trainer)
# tensorflow op to copy global params to local network
self.update_local_ops = update_target_graph('global',self.name)
self.actions = np.identity(a_size,dtype=bool).tolist()
self.env = env
接下来,我们创建了train()函数,这是Worker类中最重要的部分。状态、动作、奖励、下一个状态或观察值和价值是从作为参数传递给函数的经验列表中获取的。我们使用一个名为discount()的实用函数计算了奖励的折现总和,很快我们将定义它。类似地,advantage函数也被折现了:
# train function
def train(self,experience,sess,gamma,bootstrap_value):
experience = np.array(experience)
observations = experience[:,0]
actions = experience[:,1]
rewards = experience[:,2]
next_observations = experience[:,3]
values = experience[:,5]
# discounted rewards
self.rewards_plus = np.asarray(rewards.tolist() + [bootstrap_value])
discounted_rewards = discount(self.rewards_plus,gamma)[:-1]
# value
self.value_plus = np.asarray(values.tolist() + [bootstrap_value])
# advantage function
advantages = rewards + gamma * self.value_plus[1:] - self.value_plus[:-1]
advantages = discount(advantages,gamma)
然后,我们通过调用之前定义的 TensorFlow 操作来更新全局网络参数,并传入通过 TensorFlow 的 feed_dict 函数传递给占位符的所需输入。请注意,由于我们有多个工作线程在执行这个更新操作,因此需要避免冲突。换句话说,在任意时间点,只有一个线程可以更新主网络参数;两个或更多线程同时执行更新操作时,更新不会按顺序进行,这可能会导致问题。如果一个线程在另一个线程更新全局参数时也在进行更新,那么前一个线程的更新会被后一个线程覆盖,这是我们不希望发生的情况。这是通过 Python 的 threading 库中的 Lock() 函数实现的。我们创建一个 Lock() 实例,命名为 lock。lock.acquire() 只会授予当前线程访问权限,当前线程会执行更新操作,完成后通过 lock.release() 释放锁。最后,我们从函数中返回损失值:
# lock for updating global params
lock = Lock()
lock.acquire()
# update global network params
fd = {self.local_AC.target_v:discounted_rewards, self.local_AC.inputs:np.vstack(observations), self.local_AC.actions:actions, self.local_AC.advantages:advantages}
value_loss, policy_loss, entropy, _, _, _ = sess.run([self.local_AC.value_loss, self.local_AC.policy_loss, self.local_AC.entropy, self.local_AC.grad_norms, self.local_AC.var_norms, self.local_AC.apply_grads], feed_dict=fd)
# release lock
lock.release()
return value_loss / len(experience), policy_loss / len(experience), entropy / len(experience)
接下来,我们需要定义工作线程的 work() 函数。首先,我们获取全局的 episode 计数,并将 total_steps 设置为零。然后,在 TensorFlow 会话中,当线程仍然协调时,我们使用 self.update_local_ops 将全局参数复制到本地网络。接下来,我们启动一个 episode。由于 episode 尚未结束,我们获取策略分布并将其存储在 a_dist 中。我们从这个分布中使用 NumPy 的 random.choice() 函数采样一个动作。这个动作 a 被输入到环境的 step() 函数中,以获取新的状态、奖励和终止布尔值。我们可以通过将奖励除以 100.0 来调整奖励值。
经验存储在本地缓冲区中,称为 episode_buffer。我们还将奖励添加到 episode_reward 中,并增加 total_steps 计数以及 episode_step_count:
# worker's work function
def work(self,max_episode_steps, gamma, sess, coord, saver):
episode_count = sess.run(self.global_episodes)
total_steps = 0
print ("Starting worker " + str(self.number))
with sess.as_default(), sess.graph.as_default():
while not coord.should_stop():
# copy global params to local network
sess.run(self.update_local_ops)
# lists for book keeping
episode_buffer = []
episode_values = []
episode_frames = []
episode_reward = 0
episode_step_count = 0
d = False
s = self.env.reset()
episode_frames.append(s)
while not d:
# action and value
a_dist, v = sess.run([self.local_AC.policy,self.local_AC.value], feed_dict={self.local_AC.inputs:[s]})
a = np.random.choice(np.arange(len(a_dist[0])), p=a_dist[0])
if (self.name == 'worker_0'):
self.env.render()
# step
s1, r, d, info = self.env.step(a)
# normalize reward
r = r/100.0
if d == False:
episode_frames.append(s1)
else:
s1 = s
# collect experience in buffer
episode_buffer.append([s,a,r,s1,d,v[0,0]])
episode_values.append(v[0,0])
episode_reward += r
s = s1
total_steps += 1
episode_step_count += 1
如果缓冲区中有 25 个条目,说明是时候进行更新了。首先,计算并将值存储在 v1 中,然后将其传递给 train() 函数,该函数将输出三个损失值:价值、策略和熵。之后,重置 episode_buffer。如果 episode 已结束,我们就跳出循环。最后,我们在屏幕上打印出 episode 计数和奖励。请注意,我们使用了 25 个条目作为进行更新的时机。可以随意调整这个值,看看该超参数如何影响训练过程:
# if buffer has 25 entries, time for an update
if len(episode_buffer) == 25 and d != True and episode_step_count != max_episode_steps - 1:
v1 = sess.run(self.local_AC.value, feed_dict={self.local_AC.inputs:[s]})[0,0]
value_loss, policy_loss, entropy = self.train(episode_buffer,sess,gamma,v1)
episode_buffer = []
sess.run(self.update_local_ops)
# idiot check to ensure we did not miss update for some unforseen reason
if (len(episode_buffer) > 30):
print(self.name, "buffer full ", len(episode_buffer))
sys.exit()
if d == True:
break
print("episode: ", episode_count, "| worker: ", self.name, "| episode reward: ", episode_reward, "| step count: ", episode_step_count)
在退出 episode 循环后,我们使用缓冲区中的剩余样本来训练网络。worker_0 包含全局或主网络,我们可以通过 saver.save 保存它。我们还可以调用 self.increment 操作,将全局 episode 计数加一:
# Update the network using the episode buffer at the end of the episode
if len(episode_buffer) != 0:
value_loss, policy_loss, entropy = self.train(episode_buffer,sess,gamma,0.0)
print("loss: ", self.name, value_loss, policy_loss, entropy)
# write to file for worker_0
if (self.name == 'worker_0'):
with open("performance.txt", "a") as myfile:
myfile.write(str(episode_count) + " " + str(episode_reward) + " " + str(episode_step_count) + "\n")
# save model params for worker_0
if (episode_count % 25 == 0 and self.name == 'worker_0' and episode_count != 0):
saver.save(sess,self.model_path+'/model-'+str(episode_count)+'.cptk')
print ("Saved Model")
if self.name == 'worker_0':
sess.run(self.increment)
episode_count += 1
这就是 a3c.py 的内容。
编写 utils.py
最后,我们将编写utils.py中的utility函数。我们将导入所需的包,并且还将定义之前使用的update_target_graph()函数。它以源和目标参数的作用域作为参数,并将源中的参数复制到目标中:
import numpy as np
import tensorflow as tf
from random import choice
# copy model params
def update_target_graph(from_scope,to_scope):
from_params = tf.get_collection(tf.GraphKeys.TRAINABLE_VARIABLES, from_scope)
to_params = tf.get_collection(tf.GraphKeys.TRAINABLE_VARIABLES, to_scope)
copy_ops = []
for from_param,to_param in zip(from_params,to_params):
copy_ops.append(to_param.assign(from_param))
return copy_ops
另一个我们需要的工具函数是discount()函数。它会将输入列表x倒序运行,并按折扣因子gamma的权重进行求和。然后返回折扣后的值:
# Discounting function used to calculate discounted returns.
def discount(x, gamma):
dsr = np.zeros_like(x,dtype=np.float32)
running_sum = 0.0
for i in reversed(range(0, len(x))):
running_sum = gamma * running_sum + x[i]
dsr[i] = running_sum
return dsr
在 CartPole 上训练
cartpole.py的代码可以使用以下命令运行:
python cartpole.py
代码将回合奖励存储在performance.txt文件中。以下截图展示了训练过程中回合奖励的图表:

图 1:在 A3C 训练下的 CartPole 回合奖励
请注意,由于我们已经塑形了奖励,您在上图中看到的回合奖励与其他研究人员在论文和/或博客中报告的值不同。
A3C 算法应用于 LunarLander
我们将扩展相同的代码来训练一个智能体解决 LunarLander 问题,该问题比 CartPole 更具挑战性。大部分代码与之前相同,因此我们只会描述需要对前面的代码进行的更改。首先,LunarLander 问题的奖励塑形不同。因此,我们将在a3c.py文件中包含一个名为reward_shaping()的函数。它将检查着陆器是否已撞击月球表面;如果是,回合将被终止,并会受到-1.0的惩罚。如果着陆器未移动,回合将被终止,并支付-0.5的惩罚:
def reward_shaping(r, s, s1):
# check if y-coord < 0; implies lander crashed
if (s1[1] < 0.0):
print('-----lander crashed!----- ')
d = True
r -= 1.0
# check if lander is stuck
xx = s[0] - s1[0]
yy = s[1] - s1[1]
dist = np.sqrt(xx*xx + yy*yy)
if (dist < 1.0e-4):
print('-----lander stuck!----- ')
d = True
r -= 0.5
return r, d
我们将在env.step()之后调用此函数:
# reward shaping for lunar lander
r, d = reward_shaping(r, s, s1)
编写 lunar.py
之前练习中的cartpole.py文件已重命名为lunar.py。所做的更改如下。首先,我们将每个回合的最大时间步数设置为1000,折扣因子设置为gamma = 0.999,状态和动作维度分别设置为8和4:
max_episode_steps = 1000
gamma = 0.999
s_size = 8
a_size = 4
环境设置为LunarLander-v2:
env = gym.make('LunarLander-v2')
这就是在 LunarLander 上训练 A3C 的代码更改。
在 LunarLander 上训练
你可以通过以下命令开始训练:
python lunar.py
这将训练智能体并将回合奖励存储在performance.txt文件中,我们可以如下绘制图表:

图 2:使用 A3C 的 LunarLander 回合奖励
如你所见,智能体已经学会了将航天器安全着陆到月球表面。祝你着陆愉快!再强调一次,回合奖励与其他强化学习从业者在论文和博客中报告的值不同,因为我们对奖励进行了缩放。
A2C 算法
A2C 和 A3C 的区别在于 A2C 执行同步更新。在这里,所有工作线程会等待直到它们完成经验收集并计算出梯度。只有在这个过程中,全球(或主)网络的参数才会被更新。这与 A3C 不同,后者的更新是异步进行的,也就是说工作线程不会等待其他线程完成。A2C 比 A3C 更容易编码,但这里没有进行这部分的处理。如果你对此感兴趣,可以将前面提到的 A3C 代码转换为 A2C,之后可以对比两种算法的性能。
总结
在本章中,我们介绍了 A3C 算法,它是一种适用于离散和连续动作问题的在线策略算法。你已经看到三种不同的损失项是如何结合成一个并优化的。Python 的线程库非常有用,可以运行多个线程,每个线程中都有一个策略网络的副本。这些不同的工作线程计算策略梯度,并将其传递给主线程以更新神经网络的参数。我们将 A3C 应用于训练 CartPole 和 LunarLander 问题的智能体,且智能体学习得非常好。A3C 是一种非常强健的算法,不需要重放缓冲区,尽管它确实需要一个本地缓冲区来收集少量经验,之后这些经验将用于更新网络。最后,我们还介绍了该算法的同步版本——A2C。
本章应该已经极大地提升了你对另一种深度强化学习算法的理解。在下一章,我们将学习本书中的最后两个强化学习算法:TRPO 和 PPO。
问题
-
A3C 是在线策略算法还是离线策略算法?
-
为什么使用香农熵项?
-
使用大量工作线程有什么问题?
-
为什么在策略神经网络中使用 softmax?
-
为什么我们需要一个
advantage函数? -
这是一个练习:对于 LunarLander 问题,重复训练过程,不进行奖励塑形,看看智能体学习的速度是比本章看到的更快还是更慢。
进一步阅读
-
深度强化学习中的异步方法,作者:Volodymyr Mnih,Adrià Puigdomènech Badia,Mehdi Mirza,Alex Graves,Timothy P. Lillicrap,Tim Harley,David Silver,和Koray Kavukcuoglu,A3C 论文来自DeepMind,arXiv:1602.01783:
arxiv.org/abs/1602.01783 -
深度强化学习实战,作者:Maxim Lapan,Packt Publishing:
www.packtpub.com/big-data-and-business-intelligence/deep-reinforcement-learning-hands
第七章:信任区域策略优化和近端策略优化
在上一章中,我们看到了 A3C 和 A2C 的使用,其中 A3C 是异步的,A2C 是同步的。在本章中,我们将看到另一种在线策略强化学习(RL)算法;具体来说是两种数学上非常相似的算法,尽管它们在求解方式上有所不同。我们将介绍名为信任区域策略优化(TRPO)的算法,这个算法由 OpenAI 和加利福尼亚大学伯克利分校的研究人员于 2015 年提出(顺便提一下,后者是我以前的雇主!)。然而,这个算法在数学上很难求解,因为它涉及共轭梯度算法,这是一种相对较难解决的方法;需要注意的是,像广为人知的 Adam 和随机梯度下降(SGD)等一阶优化方法无法用来求解 TRPO 方程。然后,我们将看到如何将策略优化方程的求解合并成一个,从而得到近端策略优化(PPO)算法,并且可以使用像 Adam 或 SGD 这样的第一阶优化算法。
本章将涵盖以下主题:
-
学习 TRPO
-
学习 PPO
-
使用 PPO 解决 MountainCar 问题
-
评估性能
技术要求
成功完成本章所需的软件:
-
Python(2 及以上版本)
-
NumPy
-
TensorFlow(版本 1.4 或更高)
学习 TRPO
TRPO 是 OpenAI 和加利福尼亚大学伯克利分校提出的一个非常流行的在线策略算法,首次提出于 2015 年。TRPO 有多种版本,但我们将学习论文Trust Region Policy Optimization中的基础版本,作者为John Schulman, Sergey Levine, Philipp Moritz, Michael I. Jordan, 和 Pieter Abbeel,arXiv:1502.05477:arxiv.org/abs/1502.05477。
TRPO 涉及求解一个策略优化方程,并附加一个关于策略更新大小的约束。我们现在将看到这些方程。
TRPO 方程
TRPO 涉及最大化当前策略分布π[θ]与旧策略分布π[θ]^(old)(即在早期时间步的策略)之比的期望值,乘以优势函数A[t],并附加一个约束,即旧策略分布和新策略分布的Kullback-Leibler(KL)散度的期望值被限制在一个用户指定的值δ以内:

这里的第一个方程是策略目标,第二个方程是一个附加约束,确保策略更新是渐进的,不会进行大幅度的策略更新,从而避免将策略推向参数空间中非常远的区域。
由于我们有两个方程需要联合优化,基于一阶优化算法(如 Adam 和 SGD)的方法将无法工作。相反,这些方程使用共轭梯度算法来求解,对第一个方程进行线性近似,对第二个方程进行二次近似。然而,这在数学上较为复杂,因此我们在本书中不详细展示。我们将继续介绍 PPO 算法,它在编码上相对简单。
学习 PPO
PPO 是对 TRPO 的扩展,2017 年由 OpenAI 的研究人员提出。PPO 也是一种基于策略的算法,既可以应用于离散动作问题,也可以应用于连续动作。它使用与 TRPO 相同的策略分布比率,但不使用 KL 散度约束。具体来说,PPO 使用三种损失函数并将其合并为一个。接下来我们将看到这三种损失函数。
PPO 损失函数
PPO 中涉及的三个损失函数中的第一个称为裁剪替代目标。令 rt 表示新旧策略概率分布的比率:

裁剪替代目标由以下方程给出,其中 A[t] 是优势函数,ε 是超参数;通常,ε = 0.1 或 0.2 被使用:

clip() 函数将比率限制在 1-ε 和 1+ε 之间,从而保持比率在范围内。min() 函数是最小函数,确保最终目标是未裁剪目标的下界。
第二个损失函数是状态价值函数的 L2 范数:

第三个损失是策略分布的香农熵,它来源于信息理论:

现在我们将结合这三种损失函数。请注意,我们需要最大化 L^(clip) 和 L^(entropy),但最小化 L^V。因此,我们将总的 PPO 损失函数定义为以下方程,其中 c[1] 和 c[2] 是用于缩放项的正数常量:

请注意,如果我们在策略网络和价值网络之间共享神经网络参数,那么前述的 L^(PPO) 损失函数可以单独最大化。另一方面,如果我们为策略和价值使用独立的神经网络,那么我们可以像以下方程所示那样,分别定义损失函数,其中 L^(policy) 被最大化,而 L^(value) 被最小化:

请注意,在这种情况下,c[1] 常数在策略和价值使用独立神经网络的设置中并不需要。神经网络参数会在多个迭代步骤中根据一批数据点进行更新,更新步骤的数量由用户作为超参数指定。
使用 PPO 解决 MountainCar 问题
我们将使用 PPO 解决 MountainCar 问题。MountainCar 问题涉及一辆被困在山谷中的汽车。它必须加速以克服重力,并尝试驶出山谷,爬上陡峭的山墙,最终到达山顶的旗帜点。你可以从 OpenAI Gym 中查看 MountainCar 问题的示意图:gym.openai.com/envs/MountainCar-v0/。
这个问题非常具有挑战性,因为智能体不能仅仅从山脚下全力加速并尝试到达旗帜点,因为山墙非常陡峭,重力会阻止汽车获得足够的动能。最优的解决方案是汽车先向后驶,然后踩下油门,积累足够的动能来克服重力,成功地驶出山谷。我们将看到,RL 智能体实际上学会了这个技巧。
我们将编写以下两个文件来使用 PPO 解决 MountainCar 问题:
-
class_ppo.py -
train_test.py
编写 class_ppo.py 文件
现在,我们将编写class_ppo.py文件:
- 导入包:首先,我们将按照以下方式导入所需的包:
import numpy as np
import gym
import sys
- 设置神经网络初始化器:然后,我们将设置神经网络的参数(我们将使用两个隐藏层)以及权重和偏置的初始化器。正如我们在过去的章节中所做的那样,我们将使用 Xavier 初始化器来初始化权重,偏置的初始值则设置为一个小的正值:
nhidden1 = 64
nhidden2 = 64
xavier = tf.contrib.layers.xavier_initializer()
bias_const = tf.constant_initializer(0.05)
rand_unif = tf.keras.initializers.RandomUniform(minval=-3e-3,maxval=3e-3)
regularizer = tf.contrib.layers.l2_regularizer(scale=0.0
- 定义 PPO 类:现在已经定义了
PPO()类。首先,使用传递给类的参数定义__init__()构造函数。这里,sess是 TensorFlow 的session;S_DIM和A_DIM分别是状态和动作的维度;A_LR和C_LR分别是演员和评论员的学习率;A_UPDATE_STEPS和C_UPDATE_STEPS是演员和评论员的更新步骤数;CLIP_METHOD存储了 epsilon 值:
class PPO(object):
def __init__(self, sess, S_DIM, A_DIM, A_LR, C_LR, A_UPDATE_STEPS, C_UPDATE_STEPS, CLIP_METHOD):
self.sess = sess
self.S_DIM = S_DIM
self.A_DIM = A_DIM
self.A_LR = A_LR
self.C_LR = C_LR
self.A_UPDATE_STEPS = A_UPDATE_STEPS
self.C_UPDATE_STEPS = C_UPDATE_STEPS
self.CLIP_METHOD = CLIP_METHOD
- 定义 TensorFlow 占位符:接下来,我们需要定义 TensorFlow 的占位符:
tfs用于状态,tfdc_r用于折扣奖励,tfa用于动作,tfadv用于优势函数:
# tf placeholders
self.tfs = tf.placeholder(tf.float32, [None, self.S_DIM], 'state')
self.tfdc_r = tf.placeholder(tf.float32, [None, 1], 'discounted_r')
self.tfa = tf.placeholder(tf.float32, [None, self.A_DIM], 'action')
self.tfadv = tf.placeholder(tf.float32, [None, 1], 'advantage')
- 定义评论员:接下来定义评论员神经网络。我们使用状态(s[t])占位符
self.tfs作为神经网络的输入。使用两个隐藏层,分别由nhidden1和nhidden2个神经元组成,并使用relu激活函数(nhidden1和nhidden2的值之前都设定为64)。输出层有一个神经元,将输出状态价值函数V(s[t]),因此输出层不使用激活函数。接下来,我们计算优势函数,作为折扣累积奖励(存储在self.tfdc_r占位符中)与刚才计算的self.v输出之间的差异。评论员损失被计算为 L2 范数,并且评论员使用 Adam 优化器进行训练,目标是最小化该 L2 损失。
请注意,这个损失与本章理论部分之前提到的L^(value)相同:
# critic
with tf.variable_scope('critic'):
l1 = tf.layers.dense(self.tfs, nhidden1, activation=None, kernel_initializer=xavier, bias_initializer=bias_const, kernel_regularizer=regularizer)
l1 = tf.nn.relu(l1)
l2 = tf.layers.dense(l1, nhidden2, activation=None, kernel_initializer=xavier, bias_initializer=bias_const, kernel_regularizer=regularizer)
l2 = tf.nn.relu(l2)
self.v = tf.layers.dense(l2, 1, activation=None, kernel_initializer=rand_unif, bias_initializer=bias_const)
self.advantage = self.tfdc_r - self.v
self.closs = tf.reduce_mean(tf.square(self.advantage))
self.ctrain_op = tf.train.AdamOptimizer(self.C_LR).minimize(self.closs)
- 调用 _build_anet 函数:我们通过一个即将指定的
_build_anet()函数来定义 actor。具体来说,该函数输出策略分布和模型参数列表。我们为当前策略调用一次此函数,再为旧策略调用一次。可以通过调用self.pi的mean()和stddev()函数分别获得均值和标准差:
# actor
self.pi, self.pi_params = self._build_anet('pi', trainable=True)
self.oldpi, self.oldpi_params = self._build_anet('oldpi', trainable=False)
self.pi_mean = self.pi.mean()
self.pi_sigma = self.pi.stddev()
- 示例动作:我们可以通过策略分布
self.pi,使用sample()函数从 TensorFlow 的分布中采样动作:
with tf.variable_scope('sample_action'):
self.sample_op = tf.squeeze(self.pi.sample(1), axis=0)
- 更新旧策略参数:可以通过简单地将新策略的值赋给旧策略,使用 TensorFlow 的
assign()函数来更新旧策略网络的参数。请注意,新策略已经过优化——旧策略仅仅是当前策略的一个副本,尽管是来自一次更新周期之前的。
with tf.variable_scope('update_oldpi'):
self.update_oldpi_op = [oldp.assign(p) for p, oldp in zip(self.pi_params, self.oldpi_params)]
- 计算策略分布比率:策略分布比率在
self.tfa动作处计算,并存储在self.ratio中。请注意,指数地,分布的对数差异等于分布的比率。然后将这个比率裁剪,限制在1-ε和1+ε之间,正如理论部分中所解释的:
with tf.variable_scope('loss'):
self.ratio = tf.exp(self.pi.log_prob(self.tfa) - self.oldpi.log_prob(self.tfa))
self.clipped_ratio = tf.clip_by_value(self.ratio, 1.-self.CLIP_METHOD['epsilon'], 1.+self.CLIP_METHOD['epsilon'])
- 计算损失:前面提到的策略总损失包含三个损失,当策略和价值神经网络共享权重时,这些损失会结合在一起。然而,由于我们考虑到本章前面理论中提到的另一种设置,其中策略和价值各自拥有独立的神经网络,因此策略优化将有两个损失。第一个是未剪切比率与优势函数及其剪切类比的乘积的最小值——这个值存储在
self.aloss中。第二个损失是香农熵,它是策略分布与其对数的乘积,所有值相加,并带上负号。这个项通过超参数c[1] = 0.01 进行缩放,并从损失中减去。暂时将熵损失项设置为零,就像在 PPO 论文中一样。我们可以考虑稍后加入此熵损失项,看看它是否对策略的学习有任何影响。我们使用 Adam 优化器。请注意,我们需要最大化本章前面提到的原始策略损失,但 Adam 优化器具有minimize()函数,因此我们在self.aloss中加入了负号(参见下面代码的第一行),因为最大化一个损失等同于最小化它的负值:
self.aloss = -tf.reduce_mean(tf.minimum(self.ratio*self.tfadv, self.clipped_ratio*self.tfadv))
# entropy
entropy = -tf.reduce_sum(self.pi.prob(self.tfa) * tf.log(tf.clip_by_value(self.pi.prob(self.tfa),1e-10,1.0)),axis=1)
entropy = tf.reduce_mean(entropy,axis=0)
self.aloss -= 0.0 #0.01 * entropy
with tf.variable_scope('atrain'):
self.atrain_op = tf.train.AdamOptimizer(self.A_LR).minimize(self.aloss)
- 定义 更新 函数:接下来定义
update()函数,它将state(状态)a(动作)和r(奖励)作为参数。该函数涉及通过调用 TensorFlow 的self.update_oldpi_op操作来更新旧策略网络的参数。然后计算优势,结合状态和动作,利用A_UPDATE_STEPS(演员迭代次数)进行更新。接着,利用C_UPDATE_STEPS(评论者迭代次数)对评论者进行更新,运行 TensorFlow 会话以执行评论者训练操作:
def update(self, s, a, r):
self.sess.run(self.update_oldpi_op)
adv = self.sess.run(self.advantage, {self.tfs: s, self.tfdc_r: r})
# update actor
for _ in range(self.A_UPDATE_STEPS):
self.sess.run(self.atrain_op, feed_dict={self.tfs: s, self.tfa: a, self.tfadv: adv})
# update critic
for _ in range(self.C_UPDATE_STEPS):
self.sess.run(self.ctrain_op, {self.tfs: s, self.tfdc_r: r})
- 定义 _build_anet 函数:接下来我们将定义之前使用过的
_build_anet()函数。它将计算策略分布,该分布被视为高斯分布(即正态分布)。它以self.tfs状态占位符作为输入,具有两个隐藏层,分别包含nhidden1和nhidden2个神经元,并使用relu激活函数。然后,这个输出传递到两个输出层,这些层的输出数量是A_DIM动作维度,其中一个表示均值mu,另一个表示标准差sigma。
请注意,动作的均值是有限制的,因此使用tanh激活函数,并进行小幅裁剪以避免极值;对于标准差,使用softplus激活函数,并将其偏移0.1以避免出现零的标准差。一旦我们获得了动作的均值和标准差,TensorFlow 的Normal分布被用来将策略视为高斯分布。我们还可以调用tf.get_collection()来获取模型参数,Normal分布和模型参数将从函数中返回:
def _build_anet(self, name, trainable):
with tf.variable_scope(name):
l1 = tf.layers.dense(self.tfs, nhidden1, activation=None, trainable=trainable, kernel_initializer=xavier, bias_initializer=bias_const, kernel_regularizer=regularizer)
l1 = tf.nn.relu(l1)
l2 = tf.layers.dense(l1, nhidden2, activation=None, trainable=trainable, kernel_initializer=xavier, bias_initializer=bias_const, kernel_regularizer=regularizer)
l2 = tf.nn.relu(l2)
mu = tf.layers.dense(l2, self.A_DIM, activation=tf.nn.tanh, trainable=trainable, kernel_initializer=rand_unif, bias_initializer=bias_const)
small = tf.constant(1e-6)
mu = tf.clip_by_value(mu,-1.0+small,1.0-small)
sigma = tf.layers.dense(l2, self.A_DIM, activation=None, trainable=trainable, kernel_initializer=rand_unif, bias_initializer=bias_const)
sigma = tf.nn.softplus(sigma) + 0.1
norm_dist = tf.distributions.Normal(loc=mu, scale=sigma)
params = tf.get_collection(tf.GraphKeys.GLOBAL_VARIABLES, scope=name)
return norm_dist, params
- 定义 choose_action 函数:我们还定义了一个
choose_action()函数,从策略中采样以获取动作:
def choose_action(self, s):
s = s[np.newaxis, :]
a = self.sess.run(self.sample_op, {self.tfs: s})
return a[0]
- 定义 get_v 函数:最后,我们还定义了一个
get_v()函数,通过在self.v上运行 TensorFlow 会话来返回状态值:
def get_v(self, s):
if s.ndim < 2: s = s[np.newaxis, :]
vv = self.sess.run(self.v, {self.tfs: s})
return vv[0,0]
class_ppo.py部分到此结束。接下来,我们将编写train_test.py。
编写train_test.py文件
现在我们将编写train_test.py文件。
- 导入包:首先,我们导入所需的包:
import tensorflow as tf
import numpy as np
import matplotlib.pyplot as plt
import gym
import sys
import time
from class_ppo import *
- 定义函数:接着,我们定义了一个奖励塑造函数,该函数将根据良好或差劲的表现分别给予额外的奖励和惩罚。这样做是为了鼓励小车朝向位于山顶的旗帜一侧行驶,否则学习速度会变慢:
def reward_shaping(s_):
r = 0.0
if s_[0] > -0.4:
r += 5.0*(s_[0] + 0.4)
if s_[0] > 0.1:
r += 100.0*s_[0]
if s_[0] < -0.7:
r += 5.0*(-0.7 - s_[0])
if s_[0] < 0.3 and np.abs(s_[1]) > 0.02:
r += 4000.0*(np.abs(s_[1]) - 0.02)
return r
- 接下来,我们选择
MountainCarContinuous作为环境。我们将训练智能体的总集数设置为EP_MAX,并将其设置为1000。GAMMA折扣因子设置为0.9,学习率为2e-4。我们使用32的批量大小,并在每个周期执行10次更新步骤。状态和动作维度分别存储在S_DIM和A_DIM中。对于 PPO 的clip参数epsilon,我们使用0.1的值。train_test在训练时设置为0,在测试时设置为1:
env = gym.make('MountainCarContinuous-v0')
EP_MAX = 1000
GAMMA = 0.9
A_LR = 2e-4
C_LR = 2e-4
BATCH = 32
A_UPDATE_STEPS = 10
C_UPDATE_STEPS = 10
S_DIM = env.observation_space.shape[0]
A_DIM = env.action_space.shape[0]
print("S_DIM: ", S_DIM, "| A_DIM: ", A_DIM)
CLIP_METHOD = dict(name='clip', epsilon=0.1)
# train_test = 0 for train; =1 for test
train_test = 0
# irestart = 0 for fresh restart; =1 for restart from ckpt file
irestart = 0
iter_num = 0
if (irestart == 0):
iter_num = 0
- 我们创建一个 TensorFlow 会话,并命名为
sess。创建一个PPO类的实例,命名为ppo。我们还创建了一个 TensorFlow 的保存器。然后,如果我们是从头开始训练,我们通过调用tf.global_variables_initializer()初始化所有模型参数;如果我们是从保存的智能体继续训练或进行测试,则从ckpt/model路径恢复:
sess = tf.Session()
ppo = PPO(sess, S_DIM, A_DIM, A_LR, C_LR, A_UPDATE_STEPS, C_UPDATE_STEPS, CLIP_METHOD)
saver = tf.train.Saver()
if (train_test == 0 and irestart == 0):
sess.run(tf.global_variables_initializer())
else:
saver.restore(sess, "ckpt/model")
- 然后定义了一个主要的
for loop,用于遍历集数。在循环内部,我们重置环境,并将缓冲区设置为空列表。终止布尔值done和时间步骤数t也被初始化:
for ep in range(iter_num, EP_MAX):
print("-"*70)
s = env.reset()
buffer_s, buffer_a, buffer_r = [], [], []
ep_r = 0
max_pos = -1.0
max_speed = 0.0
done = False
t = 0
在外部循环中,我们有一个内层的while循环来处理时间步。这个问题涉及较短的时间步,在这些时间步内,汽车可能没有显著移动,因此我们使用粘性动作,其中动作每8个时间步才从策略中采样一次。PPO类中的choose_action()函数会为给定的状态采样动作。为了进行探索,我们会在动作中加入小的高斯噪声,并将其限制在-1.0到1.0的范围内,这是MountainCarContinuous环境所要求的。然后,动作被输入到环境的step()函数中,后者将输出下一个s_状态、r奖励以及终止标志done。调用reward_shaping()函数来调整奖励。为了跟踪智能体推动极限的程度,我们还计算它在max_pos和max_speed中分别的最大位置和速度:
while not done:
env.render()
# sticky actions
#if (t == 0 or np.random.uniform() < 0.125):
if (t % 8 ==0):
a = ppo.choose_action(s)
# small noise for exploration
a += 0.1 * np.random.randn()
# clip
a = np.clip(a, -1.0, 1.0)
# take step
s_, r, done, _ = env.step(a)
if s_[0] > 0.4:
print("nearing flag: ", s_, a)
if s_[0] > 0.45:
print("reached flag on mountain! ", s_, a)
if done == False:
print("something wrong! ", s_, done, r, a)
sys.exit()
# reward shaping
if train_test == 0:
r += reward_shaping(s_)
if s_[0] > max_pos:
max_pos = s_[0]
if s_[1] > max_speed:
max_speed = s_[1]
- 如果我们处于训练模式,状态、动作和奖励会被追加到缓冲区。新的状态会被设置为当前状态,如果回合尚未结束,我们将继续进行下一个时间步。
ep_r回合总奖励和t时间步数也会被更新:
if (train_test == 0):
buffer_s.append(s)
buffer_a.append(a)
buffer_r.append(r)
s = s_
ep_r += r
t += 1
如果我们处于训练模式,当样本数量等于一个批次,或者回合已经结束时,我们将训练神经网络。为此,首先使用ppo.get_v获取新状态的状态值。然后,我们计算折扣奖励。缓冲区列表也会被转换为 NumPy 数组,并且缓冲区列表会被重置为空列表。接下来,这些bs、ba和br NumPy 数组将被用来更新ppo对象的演员和评论员网络:
if (train_test == 0):
if (t+1) % BATCH == 0 or done == True:
v_s_ = ppo.get_v(s_)
discounted_r = []
for r in buffer_r[::-1]:
v_s_ = r + GAMMA * v_s_
discounted_r.append(v_s_)
discounted_r.reverse()
bs = np.array(np.vstack(buffer_s))
ba = np.array(np.vstack(buffer_a))
br = np.array(discounted_r)[:, np.newaxis]
buffer_s, buffer_a, buffer_r = [], [], []
ppo.update(bs, ba, br)
- 如果我们处于测试模式,Python 会短暂暂停以便更好地进行可视化。如果回合已结束,
while循环会通过break语句退出。然后,我们会在屏幕上打印最大的位置和速度值,并将它们以及回合奖励写入名为performance.txt的文件中。每 10 个回合,我们还会通过调用saver.save来保存模型:
if (train_test == 1):
time.sleep(0.1)
if (done == True):
print("values at done: ", s_, a)
break
print("episode: ", ep, "| episode reward: ", round(ep_r,4), "| time steps: ", t)
print("max_pos: ", max_pos, "| max_speed:", max_speed)
if (train_test == 0):
with open("performance.txt", "a") as myfile:
myfile.write(str(ep) + " " + str(round(ep_r,4)) + " " + str(round(max_pos,4)) + " " + str(round(max_speed,4)) + "\n")
if (train_test == 0 and ep%10 == 0):
saver.save(sess, "ckpt/model")
这标志着 PPO 编码的结束。接下来我们将在 MountainCarContinuous 上评估其性能。
评估性能
PPO 智能体通过以下命令进行训练:
python train_test.py
一旦训练完成,我们可以通过设置以下内容来测试智能体:
train_test = 1
然后,我们会再次运行python train_test.py。通过可视化智能体,我们可以观察到汽车首先向后移动,攀爬左侧的山。接着,它全速前进,获得足够的动能,成功越过右侧山峰上方的陡峭坡道。因此,PPO 智能体已经学会成功驶出山谷。
全油门
请注意,我们必须先倒车,然后踩下油门,才能获得足够的动能逃脱重力并成功驶出山谷。如果我们一开始就踩下油门,汽车还能够逃脱吗?让我们通过编写并运行mountaincar_full_throttle.py来验证。
现在,我们将动作设置为1.0,即全油门:
import sys
import numpy as np
import gym
env = gym.make('MountainCarContinuous-v0')
for _ in range(100):
s = env.reset()
done = False
max_pos = -1.0
max_speed = 0.0
ep_reward = 0.0
while not done:
env.render()
a = [1.0] # step on throttle
s_, r, done, _ = env.step(a)
if s_[0] > max_pos: max_pos = s_[0]
if s_[1] > max_speed: max_speed = s_[1]
ep_reward += r
print("ep_reward: ", ep_reward, "| max_pos: ", max_pos, "| max_speed: ", max_speed)
从训练过程中生成的视频可以看出,汽车无法逃脱重力的无情拉力,最终仍然被困在山谷的底部。
随机油门
如果我们尝试随机的油门值呢?我们将编写mountaincar_random_throttle.py,在-1.0到1.0的范围内进行随机操作:
import sys
import numpy as np
import gym
env = gym.make('MountainCarContinuous-v0')
for _ in range(100):
s = env.reset()
done = False
max_pos = -1.0
max_speed = 0.0
ep_reward = 0.0
while not done:
env.render()
a = [-1.0 + 2.0*np.random.uniform()]
s_, r, done, _ = env.step(a)
if s_[0] > max_pos: max_pos = s_[0]
if s_[1] > max_speed: max_speed = s_[1]
ep_reward += r
print("ep_reward: ", ep_reward, "| max_pos: ", max_pos, "| max_speed: ", max_speed)
在这种情况下,汽车仍然无法逃脱重力,依然被困在山谷的底部。所以,RL 智能体需要明白,最优策略是先向后行驶,然后踩下油门,逃脱重力并到达山顶的旗帜处。
这标志着我们的 PPO MountainCar 练习的结束。
总结
在这一章中,我们介绍了 TRPO 和 PPO 两种 RL 算法。TRPO 涉及两个需要求解的方程,第一个方程是策略目标,第二个方程是对更新幅度的约束。TRPO 需要二阶优化方法,例如共轭梯度。为了简化这一过程,PPO 算法应运而生,其中策略比例被限制在一个用户指定的范围内,从而保持更新的渐进性。此外,我们还看到了使用从经验中收集的数据样本来更新演员和评论家,通过多次迭代进行训练。我们在 MountainCar 问题上训练了 PPO 智能体,这是一个具有挑战性的问题,因为演员必须先将汽车倒退上左边的山,然后加速以获得足够的动能克服重力,最终到达右边山顶的旗帜处。我们还发现,全油门策略或随机策略无法帮助智能体达到目标。
本章中,我们回顾了几种强化学习(RL)算法。在下一章,我们将应用 DDPG 和 PPO 来训练一个智能体,使其能自动驾驶汽车。
问题
-
我们可以在 TRPO 中应用 Adam 或 SGD 优化吗?
-
策略优化中的熵项有什么作用?
-
为什么我们要剪辑策略比例?如果剪辑参数 epsilon 较大,会发生什么?
-
为什么我们使用
tanh激活函数作为mu的激活函数,而使用softplus作为 sigma 的激活函数?我们能否为 sigma 使用tanh激活函数? -
奖励塑造在训练中总是有效吗?
-
测试一个已经训练好的智能体时,我们需要奖励塑造吗?
进一步阅读
-
信任域策略优化,约翰·舒尔曼,谢尔盖·莱文,菲利普·莫里茨,迈克尔·I·乔丹,皮特·阿贝尔,arXiv:1502.05477 (TRPO 论文):
arxiv.org/abs/1502.05477 -
近端策略优化算法,约翰·舒尔曼,菲利普·沃尔斯基,普拉夫拉·达里瓦尔,亚历克·拉德福,奥列格·克里莫夫,arXiv:1707.06347(PPO 论文):
arxiv.org/abs/1707.06347 -
深度强化学习实战,马克西姆·拉潘,Packt 出版社:
www.packtpub.com/big-data-and-business-intelligence/deep-reinforcement-learning-hands
第八章:深度强化学习应用于自动驾驶
自动驾驶是目前正在开发的最热门的技术革命之一。它将极大地改变人类对交通的看法,显著降低旅行成本并提高安全性。自动驾驶汽车开发社区已经使用了若干前沿算法来实现这一目标。这些算法包括但不限于感知、定位、路径规划和控制。感知涉及识别自动驾驶汽车周围的环境——行人、汽车、自行车等。定位是指在预先计算好的环境地图中识别汽车的确切位置(或者更精确地说,姿态)。路径规划,顾名思义,是规划自动驾驶汽车路径的过程,包括长期路径(比如从A点到B点)以及短期路径(比如接下来的 5 秒)。控制则是实际执行所需路径的过程,包括避让操作。特别地,强化学习(RL)在自动驾驶的路径规划和控制中被广泛应用,适用于城市驾驶和高速公路驾驶。
在本章中,我们将使用开放赛车模拟器(TORCS)来训练 RL 智能体,学习如何在赛道上成功驾驶。尽管 CARLA 模拟器更强大且具有逼真的渲染效果,但 TORCS 更易于使用,因此是一个很好的入门选择。完成本书后,鼓励感兴趣的读者尝试在 CARLA 模拟器上训练 RL 智能体。
本章将涉及以下主题:
-
学习使用 TORCS
-
训练深度确定性策略梯度(DDPG)智能体来学习驾驶
-
训练近端策略优化(PPO)智能体
技术要求
本章的学习需要以下工具:
-
Python(版本 2 或 3)
-
NumPy
-
Matplotlib
-
TensorFlow(版本 1.4 或更高)
-
TORCS 赛车模拟器
汽车驾驶模拟器
在自动驾驶中应用强化学习(RL)需要使用强大的汽车驾驶模拟器,因为 RL 智能体不能直接在道路上进行训练。为此,研究社区开发了几款开源汽车驾驶模拟器,每款都有其优缺点。一些开源汽车驾驶模拟器包括:
-
CARLA
-
在英特尔实验室开发
-
适用于城市驾驶
-
TORCS
-
DeepTraffic
-
在 MIT 开发
-
适用于高速公路驾驶
学习使用 TORCS
我们将首先学习如何使用 TORCS 赛车模拟器,它是一个开源模拟器。你可以从 torcs.sourceforge.net/index.php?name=Sections&op=viewarticle&artid=3 获取下载说明,但以下是 Linux 系统下的关键步骤总结:
-
从
sourceforge.net/projects/torcs/files/all-in-one/1.3.7/torcs-1.3.7.tar.bz2/download下载torcs-1.3.7.tar.bz2文件 -
使用
tar xfvj torcs-1.3.7.tar.bz2解包该包 -
执行以下命令:
-
cd torcs-1.3.7 -
./configure -
make -
make install -
make datainstall
-
-
默认安装目录为:
-
/usr/local/bin:TORCS 命令(该目录应包含在你的PATH中) -
/usr/local/lib/torcs:TORCS 动态libs(如果不使用 TORCS shell,目录必须包含在你的LD_LIBRARY_PATH中) -
/usr/local/share/games/torcs:TORCS 数据文件
-
通过运行 torcs 命令(默认位置是 /usr/local/bin/torcs),你现在可以看到 TORCS 模拟器开启。然后可以选择所需的设置,包括选择汽车、赛道等。模拟器也可以作为视频游戏来玩,但我们更感兴趣的是用它来训练 RL 智能体。
状态空间
接下来,我们将定义 TORCS 的状态空间。表 2:可用传感器描述(第二部分)。范围与其单位(如果定义了单位)一起报告,来自 模拟汽车赛车锦标赛:竞赛软件手册 文档,在 arxiv.org/pdf/1304.1672.pdf 中提供了一个模拟器可用状态参数的总结。我们将使用以下条目作为我们的状态空间;方括号中的数字表示条目的大小:
-
angle:汽车方向与赛道之间的角度(1) -
track:这将给我们每 10 度测量一次的赛道末端,从 -90 到 +90 度;它有 19 个实数值,包括端点值(19) -
trackPos:汽车与赛道轴之间的距离(1) -
speedX:汽车在纵向方向的速度(1) -
speedY:汽车在横向方向的速度(1) -
speedZ:汽车在 Z 方向的速度;其实我们并不需要这个,但现在先保留它(1) -
wheelSpinVel:汽车四个轮子的旋转速度(4) -
rpm:汽车引擎的转速(1)
请参考之前提到的文档以更好地理解前述变量,包括它们的允许范围。总结所有实数值条目的数量,我们注意到我们的状态空间是一个实数值向量,大小为 1+19+1+1+1+1+4+1 = 29。我们的动作空间大小为3:转向、加速和刹车。转向范围为 [-1,1],加速范围为 [0,1],刹车也是如此。
支持文件
开源社区还开发了两个 Python 文件,将 TORCS 与 Python 接口,以便我们能够从 Python 命令调用 TORCS。此外,为了自动启动 TORCS,我们需要另一个sh文件。这三个文件总结如下:
-
gym_torcs.py -
snakeoil3_gym.py -
autostart.sh
这些文件包含在本章的代码文件中(github.com/PacktPublishing/TensorFlow-Reinforcement-Learning-Quick-Start-Guide),但也可以通过 Google 搜索获取。在gym_torcs.py的第~130-160 行中,设置了奖励函数。你可以看到以下行,这些行将原始的模拟器状态转换为 NumPy 数组:
# Reward setting Here #######################################
# direction-dependent positive reward
track = np.array(obs['track'])
trackPos = np.array(obs['trackPos'])
sp = np.array(obs['speedX'])
damage = np.array(obs['damage'])
rpm = np.array(obs['rpm'])
然后,奖励函数被设置如下。请注意,我们根据沿轨道的纵向速度(角度项的余弦值)给予奖励,并惩罚横向速度(角度项的正弦值)。轨道位置也会被惩罚。理想情况下,如果这是零,我们将处于轨道的中心,而+1或-1的值表示我们在轨道的边缘,这是不希望发生的,因此会受到惩罚:
progress = sp*np.cos(obs['angle']) - np.abs(sp*np.sin(obs['angle'])) - sp * np.abs(obs['trackPos'])
reward = progress
如果汽车偏离轨道或智能体的进度卡住,我们使用以下代码终止该回合:
if (abs(track.any()) > 1 or abs(trackPos) > 1): # Episode is terminated if the car is out of track
print("Out of track ")
reward = -100 #-200
episode_terminate = True
client.R.d['meta'] = True
if self.terminal_judge_start < self.time_step: # Episode terminates if the progress of agent is small
if progress < self.termination_limit_progress:
print("No progress", progress)
reward = -100 # KAUSHIK ADDED THIS
episode_terminate = True
client.R.d['meta'] = True
我们现在已经准备好训练一个 RL 智能体,让它在 TORCS 中成功驾驶。我们将首先使用 DDPG 智能体。
训练 DDPG 智能体学习驾驶
大部分 DDPG 代码与我们在第五章中看到的深度确定性策略梯度(DDPG)相同;这里只总结其中的差异。
编写 ddpg.py
我们的 TORCS 状态维度是29,动作维度是3;这些在ddpg.py中设置,如下所示:
state_dim = 29
action_dim = 3
action_bound = 1.0
编写 AandC.py
actor 和 critic 文件AandC.py也需要进行修改。特别地,ActorNetwork类中的create_actor_network被修改为拥有两个隐藏层,分别包含400和300个神经元。此外,输出由三个动作组成:steering(转向)、acceleration(加速)和brake(刹车)。由于转向在[-1,1]范围内,因此使用tanh激活函数;加速和刹车在[0,1]范围内,因此使用sigmoid激活函数。然后,我们在轴维度1上将它们concat(连接),这就是我们 actor 策略的输出:
def create_actor_network(self, scope):
with tf.variable_scope(scope, reuse=tf.AUTO_REUSE):
state = tf.placeholder(name='a_states', dtype=tf.float32, shape=[None, self.s_dim])
net = tf.layers.dense(inputs=state, units=400, activation=None, kernel_initializer=winit, bias_initializer=binit, name='anet1')
net = tf.nn.relu(net)
net = tf.layers.dense(inputs=net, units=300, activation=None, kernel_initializer=winit, bias_initializer=binit, name='anet2')
net = tf.nn.relu(net)
steering = tf.layers.dense(inputs=net, units=1, activation=tf.nn.tanh, kernel_initializer=rand_unif, bias_initializer=binit, name='steer')
acceleration = tf.layers.dense(inputs=net, units=1, activation=tf.nn.sigmoid, kernel_initializer=rand_unif, bias_initializer=binit, name='acc')
brake = tf.layers.dense(inputs=net, units=1, activation=tf.nn.sigmoid, kernel_initializer=rand_unif, bias_initializer=binit, name='brake')
out = tf.concat([steering, acceleration, brake], axis=1)
return state, out
同样,CriticNetwork类的create_critic_network()函数被修改为拥有两个隐藏层,分别包含400和300个神经元。这在以下代码中显示:
def create_critic_network(self, scope):
with tf.variable_scope(scope, reuse=tf.AUTO_REUSE):
state = tf.placeholder(name='c_states', dtype=tf.float32, shape=[None, self.s_dim])
action = tf.placeholder(name='c_action', dtype=tf.float32, shape=[None, self.a_dim])
net = tf.concat([state, action],1)
net = tf.layers.dense(inputs=net, units=400, activation=None, kernel_initializer=winit, bias_initializer=binit, name='cnet1')
net = tf.nn.relu(net)
net = tf.layers.dense(inputs=net, units=300, activation=None, kernel_initializer=winit, bias_initializer=binit, name='cnet2')
net = tf.nn.relu(net)
out = tf.layers.dense(inputs=net, units=1, activation=None, kernel_initializer=rand_unif, bias_initializer=binit, name='cnet_out')
return state, action, out
需要进行的其他更改在TrainOrTest.py中,我们接下来将查看这些更改。
编写 TrainOrTest.py
从gym_torcs导入 TORCS 环境,以便我们能够在其上训练 RL 智能体:
- 导入 TORCS:从
gym_torcs导入 TORCS 环境,代码如下:
from gym_torcs import TorcsEnv
env变量:使用以下命令创建 TORCS 环境变量:
# Generate a Torcs environment
env = TorcsEnv(vision=False, throttle=True, gear_change=False)
- 重新启动 TORCS:由于 TORCS 已知存在内存泄漏错误,每
100个回合后使用relaunch=True重新启动环境;否则,可以按如下方式在不带参数的情况下重置环境:
if np.mod(i, 100) == 0:
ob = env.reset(relaunch=True) #relaunch TORCS every N episodes due to a memory leak error
else:
ob = env.reset()
- 堆叠状态空间:使用以下命令堆叠 29 维的状态空间:
s = np.hstack((ob.angle, ob.track, ob.trackPos, ob.speedX, ob.speedY, ob.speedZ, ob.wheelSpinVel/100.0, ob.rpm))
- 每回合的时间步数:选择每个回合运行的时间步数
msteps。在前100个回合中,代理学习尚未深入,因此你可以选择每回合100个时间步;对于后续回合,我们将其线性增加,直到达到max_steps的上限。
这个步骤并不是关键,代理的学习并不依赖于我们为每个回合选择的步数。可以自由尝试设置msteps。
选择每回合的时间步数,如下所示:
msteps = max_steps
if (i < 100):
msteps = 100
elif (i >=100 and i < 200):
msteps = 100 + (i-100)*9
else:
msteps = 1000 + (i-200)*5
msteps = min(msteps, max_steps)
- 全速前进:在前
10个回合中,我们施加全速前进以预热神经网络参数。只有在此之后,我们才开始使用演员的策略。需要注意的是,TORCS 通常在大约 1,500 到 2,000 个回合后学习完成,所以前10个回合对后期的学习影响不大。通过以下方式应用全速前进来预热神经网络参数:
# first few episodes step on gas!
if (i < 10):
a[0][0] = 0.0
a[0][1] = 1.0
a[0][2] = 0.0
这就是为了让 DDPG 代理在 TORCS 中玩的代码需要做的更改。其余的代码与第五章中讲解的深度确定性策略梯度(DDPG)相同。我们可以使用以下命令来训练代理:
python ddpg.py
输入1进行训练;输入0是为了测试一个预训练的代理。训练可能需要 2 到 5 天,具体取决于使用的计算机速度。但这是一个有趣的问题,值得花费时间和精力。每个回合的步数以及奖励都会存储在analysis_file.txt文件中,我们可以用它来绘制图表。每个回合的时间步数如下图所示:

图 1:TORCS 每回合的时间步数(训练模式)
我们可以看到,经过大约 600 次训练后,汽车已经学会了合理地驾驶,而在大约 1500 次训练后,驾驶更加高效。大约 300 个时间步对应赛道的一圈。因此,代理在训练结束时能够驾驶七到八圈以上而不会中途终止。关于 DDPG 代理驾驶的精彩视频,请参考以下 YouTube 链接:www.youtube.com/watch?v=ajomz08hSIE。
训练 PPO 代理
我们之前展示了如何训练一个 DDPG 智能体在 TORCS 中驾驶汽车。如何使用 PPO 智能体则留作有兴趣的读者的练习。这是一个不错的挑战。来自第七章的 PPO 代码,信任区域策略优化与近端策略优化,可以复用,只需对 TORCS 环境做必要的修改。TORCS 的 PPO 代码也已提供在代码库中(github.com/PacktPublishing/TensorFlow-Reinforcement-Learning-Quick-Start-Guide),有兴趣的读者可以浏览它。TORCS 中 PPO 智能体驾驶汽车的酷炫视频可以在以下 YouTube 视频中查看:youtu.be/uE8QaJQ7zDI
另一个挑战是对于有兴趣的读者来说,使用信任区域策略优化(TRPO)解决 TORCS 赛车问题。如果有兴趣,也可以尝试一下!这是掌握 RL 算法的一种方法。
总结
本章中,我们展示了如何应用强化学习(RL)算法训练一个智能体,使其学会自主驾驶汽车。我们安装了 TORCS 赛车模拟器,并学习了如何将其与 Python 接口,以便我们可以训练 RL 智能体。我们还深入探讨了 TORCS 的状态空间以及这些术语的含义。随后,使用 DDPG 算法训练一个智能体,成功学会在 TORCS 中驾驶。TORCS 中的视频渲染效果非常酷!训练后的智能体能够成功驾驶超过七到八圈。最后,我们也探索了使用 PPO 算法解决同样的自主驾驶问题,并将其作为练习留给有兴趣的读者;相关代码已提供在本书的代码库中。
这也就结束了本章以及本书的内容。可以随时在线阅读更多关于 RL 在自主驾驶和机器人学应用的材料。这是目前学术界和工业界研究的热门领域,并且得到了充分的资金支持,相关领域也有许多职位空缺。祝你一切顺利!
问题
-
为什么不能使用 DQN 来解决 TORCS 问题?
-
我们使用了 Xavier 权重初始化器来初始化神经网络的权重。你知道还有哪些其他的权重初始化方法?使用它们训练的智能体表现如何?
-
为什么在奖励函数中使用
abs()函数?为什么它用于最后两个项而不是第一个项? -
如何确保比视频中观察到的驾驶更平稳?
-
为什么在 DDPG 中使用重放缓冲区而在 PPO 中不使用?
深入阅读
-
使用深度强化学习进行连续控制,Timothy P. Lillicrap, Jonathan J. Hunt, Alexander Pritzel, Nicolas Heess, Tom Erez, Yuval Tassa, David Silver, Daan Wierstra,arXiv:1509.02971(DDPG 论文):
arxiv.org/abs/1509.02971 -
近端策略优化算法,John Schulman,Filip Wolski,Prafulla Dhariwal,Alec Radford,Oleg Klimov,arXiv:1707.06347 (PPO 论文):
arxiv.org/abs/1707.06347 -
TORCS:
torcs.sourceforge.net/ -
深度强化学习实战,Maxim Lapan 著,Packt Publishing 出版:
www.packtpub.com/big-data-and-business-intelligence/deep-reinforcement-learning-hands
第九章:评估
第一章
-
离策略强化学习算法需要一个重放缓冲区。我们从重放缓冲区中抽取一个小批量的经验,用它来训练 DQN 中的Q(s,a) 状态值函数以及 DDPG 中的演员策略。
-
我们对奖励进行折扣,因为关于智能体的长期表现存在更多不确定性。因此,立即奖励具有更高的权重,下一时间步获得的奖励的权重相对较低,再下一时间步的奖励权重更低,依此类推。
-
如果γ > 1,智能体的训练将不稳定,智能体将无法学习到最优策略。
-
基于模型的强化学习智能体有潜力表现得很好,但不能保证它一定会比基于无模型的强化学习智能体表现得更好,因为我们构建的环境模型未必总是一个好的模型。而且,构建一个足够准确的环境模型也非常困难。
-
在深度强化学习中,深度神经网络用于表示Q(s,a) 和演员的策略(在 Actor-Critic 设置中是这样的)。在传统的强化学习算法中,使用的是表格型的Q(s,a),但当状态的数量非常大时,这种方式无法应用,而这通常是大多数问题的情况。
第三章
-
在 DQN 中,重放缓冲区用于存储过去的经验,从中抽取一个小批量的数据,并用来训练智能体。
-
目标网络有助于提高训练的稳定性。这是通过保持一个额外的神经网络来实现的,该网络的权重通过使用主神经网络权重的指数移动平均来更新。或者,另一种广泛使用的方法是每隔几千步就将主神经网络的权重复制到目标网络中。
-
在 Atari Breakout 问题中,单一帧作为状态是无法提供帮助的。这是因为单一帧无法提取时间信息。例如,单一帧中无法得出球的运动方向。但如果我们将多帧叠加起来,就可以确定球的速度和加速度。
-
L2 损失已知会对异常值产生过拟合。因此,更倾向使用 Huber 损失,因为它结合了 L2 和 L1 损失。详见维基百科:
en.wikipedia.org/wiki/Huber_loss。 -
也可以使用 RGB 图像。不过,我们需要为神经网络的第一隐藏层增加额外的权重,因为现在在状态堆栈中的四帧每一帧都有三个通道。这种对于状态空间的更精细的细节在 Atari 中并不是必须的。然而,RGB 图像可以在其他应用中提供帮助,例如在自动驾驶和/或机器人技术中。
第四章
-
DQN 被认为会高估状态-动作值函数Q(s,a)。为了解决这个问题,引入了 DDQN。DDQN 在高估Q(s,a)方面比 DQN 少遇到问题。
-
对抗网络架构为优势函数和状态价值函数提供了独立的流。然后,这些流被组合起来得到Q(s,a)。这种分支后再合并的方式被观察到有助于 RL 代理的训练更加稳定。
-
优先经验回放(PER)赋予代理表现较差的经验样本更高的重要性,因此这些样本比代理表现良好的其他样本被采样的频率更高。通过频繁使用表现较差的样本,代理能够更频繁地解决自身的弱点,从而 PER 加速了训练。
-
在一些计算机游戏中,例如 Atari Breakout,模拟器的帧率过高。如果在每个时间步长中都从策略中采样一个独立的动作,代理的状态可能在一个时间步长内变化不够,因为这个时间步长太小。因此,使用了粘性动作,其中相同的动作会在有限但固定的时间步数内重复,例如n,并且在这些 n 个时间步内累计的总奖励将作为执行该动作的奖励。在这些 n 个时间步内,代理的状态已经发生了足够的变化,可以评估所采取的动作的有效性。n 值过小会阻止代理学习到良好的策略;同样,n 值过大也会成为一个问题。必须选择合适的时间步长数,在这些时间步长内执行相同的动作,这取决于所使用的模拟器。
第五章
-
DDPG 是一种基于策略外的算法,因为它使用了回放缓冲区。
-
一般来说,演员和评论家的隐藏层数量以及每个隐藏层的神经元数量是相同的,但这不是强制要求。请注意,演员和评论家的输出层是不同的,演员的输出数量等于动作的数量;评论家只有一个输出。
-
DDPG 用于连续控制,即当动作是连续且为实数值时。Atari Breakout 有离散动作,因此 DDPG 不适用于 Atari Breakout。
-
我们使用
relu激活函数,因此偏置初始化为小的正值,以便它们在训练开始时就能够激活并允许梯度回传。 -
这也是一个练习。注意当第一层的神经元数量逐渐减少时,学习会发生什么变化。一般来说,信息瓶颈不仅在强化学习(RL)环境中观察到,任何深度学习(DL)问题中也会出现。
第六章
-
异步优势演员-评论家代理(A3C)是一种基于策略的算法,因为我们并没有使用回放缓冲区来采样数据。然而,使用了一个临时缓冲区来收集即时样本,这些样本会用来训练一次,然后缓冲区会被清空。
-
Shannon 熵项被用作正则化器——熵越高,策略越好。
-
当使用过多工作线程时,训练可能会变慢并崩溃,因为内存有限。然而,如果你可以访问大量的处理器集群,那么使用大量的工作线程/进程将会有所帮助。
-
Softmax 被用于策略网络中,以获取不同动作的概率。
-
优势函数被广泛使用,因为它降低了策略梯度的方差。《A3C 论文》第 3 节(
arxiv.org/pdf/1602.01783.pdf)对此有更多说明。 -
这是一个练习。
第七章
-
信任区域策略优化(TRPO)具有目标函数和约束条件。因此,它需要二阶优化方法,如共轭梯度法。SGD 和 Adam 不适用于 TRPO。
-
熵项有助于正则化,它允许智能体进行更多探索。
-
我们剪切策略比率,以限制每次更新步骤对策略的变化量。如果这个剪切参数 epsilon 设置得较大,则每次更新中策略可能发生剧烈变化,这可能导致一个次优策略,因为智能体的策略变得更加嘈杂并且波动过大。
-
动作的取值范围在负值和正值之间,因此对
mu使用了tanh激活函数。对于 sigma,使用softplus作为 sigma,它始终为正。不能对 sigma 使用tanh函数,因为tanh可能会导致 sigma 为负值,这是没有意义的! -
奖励塑形通常有助于训练。但如果操作不当,它将无法帮助训练。你必须确保奖励塑形能够保持
reward函数的密度并处于适当的范围内。 -
不,奖励塑形仅在训练过程中使用。
第八章
-
TORCS 是一个连续控制问题。DQN 仅适用于离散动作,因此不能用于 TORCS。
-
初始化是另一种初始化策略;你也可以使用指定范围内的
min和max值进行随机均匀初始化;另一种方法是从一个均值为零、sigma 值已指定的高斯分布中采样。感兴趣的读者应尝试这些不同的初始化方法,并比较智能体的表现。 -
abs()函数在reward函数中使用,因为我们对偏离中心的横向漂移进行平等惩罚(无论是左侧还是右侧)。第一个项是纵向速度,因此不需要abs()函数。 -
为了探索而加入的高斯噪声可以随着回合数的增加逐渐减少,这可以导致更平稳的驾驶体验。当然,你还可以尝试许多其他技巧!
-
DDPG 是一个脱离策略的算法,但近端策略优化(PPO)是一个基于策略的强化学习算法。因此,DDPG 需要一个回放缓冲区来存储过去的经验样本,而 PPO 则不需要回放缓冲区。


浙公网安备 33010602011771号