OpenAI-Gym-智能体实用指南-全-
OpenAI Gym 智能体实用指南(全)
原文:
annas-archive.org/md5/e4fd128cf9b93e0f7a542b053330517a译者:飞龙
前言
本书将指导你实现自己的智能体,解决离散和连续值的顺序决策问题,并提供所有必要的构建块,以便在各种学习环境中开发、调试、训练、可视化、定制和测试你的智能体实现,涵盖从 Mountain Car 和 Cart Pole 问题到 Atari 游戏和 CARLA——一个高级自动驾驶模拟器。
本书适用对象
如果你是学生、游戏/机器学习开发者,或者是希望开始构建智能体和算法来解决多种问题的 AI 爱好者,并且希望使用 OpenAI Gym 接口中的学习环境,本书适合你。如果你希望学习如何构建基于深度强化学习的人工智能智能体来解决你感兴趣领域中的问题,你也会觉得这本书有用。虽然本书涵盖了你需要了解的所有基本概念,但如果你有一定的 Python 知识,会让你更好地理解书中的内容。
本书涵盖内容
第一章,智能体和学习环境概述,该章节使得多个 AI 系统的开发成为可能。它揭示了工具包的重要特性,提供了无限的机会,让你创建自动智能体来解决多种算法任务、游戏和控制任务。到本章结束时,你将能够使用 Python 创建一个 Gym 环境实例。
第二章,强化学习和深度强化学习,提供了一个简明的
解释强化学习中的基本术语和概念。本章
将帮助你很好地理解基本的强化学习框架,
开发 AI 智能体。本章还将介绍深度强化学习和
为你提供一种算法能够解决的高级问题类型的概念。
解决这些问题。
第三章,OpenAI Gym 和深度强化学习入门,直接开始并准备好你的开发机器/计算机,进行所需的安装和配置,以便使用学习环境以及 PyTorch 来开发深度学习算法。
第四章,探索 Gym 及其特性,带你了解 Gym 库中可用的学习环境清单,从环境如何分类和命名的概述开始,这将帮助你从 700 多个学习环境中选择正确的版本和类型。接下来,你将学习如何探索 Gym,测试任何你想要的环境,理解不同环境的接口和描述。
第五章,实现你的第一个学习智能体 – 解决 Mountain Car 问题,解释如何使用强化学习实现一个 AI 智能体来解决 Mountain Car 问题。
汽车问题。你将实现代理,训练它,并看到它自主改进。
实现细节将帮助你应用这些概念来开发和训练智能体。
用来解决其他各种任务和/或游戏。
第六章,使用深度 Q 学习实现智能代理的最优控制,涵盖了改进 Q 学习的各种方法,包括使用深度神经网络的动作-价值函数近似、经验回放、目标网络,以及用于训练和测试深度强化学习代理的必要工具和构建模块。你将实现一个基于 DQN 的智能代理,以采取最优的离散控制动作,并训练它玩多个 Atari 游戏,观察代理的表现。
第七章,创建自定义 OpenAI Gym 环境——Carla 驾驶模拟器,将教你如何将现实问题转化为与 OpenAI Gym 兼容的学习环境。你将学习 Gym 环境的结构,并基于 Carla 模拟器创建一个自定义的学习环境,能够注册到 Gym 并用于训练我们开发的代理。
第八章,使用深度演员-评论家算法实现智能与自主驾驶代理,将教你基于策略梯度的强化学习算法的基础,并帮助你直观理解深度 n 步优势演员-评论家算法。然后你将学习如何实现一个超级智能的代理,使其能够在 Carla 模拟器中自主驾驶汽车,使用同步和异步实现的深度 n 步优势演员-评论家算法。
第九章,探索学习环境的全景——Roboschool,Gym-Retro,StarCraft-II,DeepMindLab,将带你超越 Gym,展示一套你可以用来训练智能代理的其他成熟学习环境。你将了解并学习使用各种 Roboschool 环境、Gym Retro 环境、广受欢迎的 StarCraft II 环境和 DeepMind Lab 环境。
第十章,探索学习算法的全景——DDPG(演员-评论家),PPO(策略梯度),Rainbow(基于价值),提供了最新深度强化学习算法的洞察,并基于你在本书前几章学到的知识,揭示了它们的基本原理。你将快速理解三种不同类型的深度强化学习算法背后的核心概念,分别是:基于演员-评论家的深度确定性策略梯度(DDPG)算法、基于策略梯度的近端策略优化(PPO)和基于价值的 Rainbow 算法。
为了最大化本书的价值
以下内容将是必需的:
-
需要具备一定的 Python 编程基础,以理解语法、模块导入和库安装。
-
一些使用 Linux 或 macOS X 命令行的基础任务经验,例如浏览文件系统和运行 Python 脚本。
下载示例代码文件
你可以从www.packtpub.com账户下载本书的示例代码文件。如果你是在其他地方购买的本书,可以访问www.packtpub.com/support并注册,将文件直接通过电子邮件发送给你。
你可以按照以下步骤下载代码文件:
-
登录或注册到www.packtpub.com。
-
选择“支持”标签。
-
点击“代码下载与勘误”。
-
在搜索框中输入书名并按照屏幕上的指示操作。
文件下载完成后,请确保使用最新版本的工具解压或提取文件夹:
-
Windows 使用 WinRAR/7-Zip。
-
Mac 使用 Zipeg/iZip/UnRarX。
-
Linux 使用 7-Zip/PeaZip。
本书的代码包也托管在 GitHub 上,地址为github.com/PacktPublishing/Hands-On-Intelligent-Agents-with-OpenAI-Gym。如果代码有更新,将会更新现有的 GitHub 仓库。
我们还在丰富的书籍和视频目录中提供了其他代码包,可以访问github.com/PacktPublishing/查看!
下载彩色图片
我们还提供了一个 PDF 文件,包含本书中使用的截图/图表的彩色图像。你可以在此下载:www.packtpub.com/sites/default/files/downloads/HandsOnIntelligentAgentswithOpenAIGym_ColorImages.pdf。
使用的约定
本书中使用了多种文本约定。
CodeInText:表示文本中的代码词语、数据库表名、文件夹名称、文件名、文件扩展名、路径名、虚拟网址、用户输入和 Twitter 用户名。例如:“将下载的WebStorm-10*.dmg磁盘映像文件挂载为系统中的另一个磁盘。”
代码块的设置方式如下:
#!/usr/bin/env python
import gym
env = gym.make("Qbert-v0")
MAX_NUM_EPISODES = 10
MAX_STEPS_PER_EPISODE = 500
当我们希望引起你对代码块中特定部分的注意时,相关的行或项目会以粗体显示:
for episode in range(MAX_NUM_EPISODES):
obs = env.reset()
for step in range(MAX_STEPS_PER_EPISODE):
env.render()
任何命令行输入或输出都将如下所示:
$ python get_observation_action_space.py 'MountainCar-v0'
粗体:表示新术语、重要词汇或在屏幕上显示的词语。例如,菜单或对话框中的词语会以这种方式显示。示例:“从管理面板中选择系统信息。”
警告或重要说明会以这种方式出现。
提示和技巧以这种方式出现。
联系我们
我们欢迎读者的反馈。
一般反馈:通过电子邮件发送至feedback@packtpub.com并在邮件主题中注明书名。如果你对本书的任何方面有疑问,请发送邮件至questions@packtpub.com。
勘误:尽管我们已尽力确保内容的准确性,但难免会有错误。如果您在本书中发现错误,欢迎您向我们反馈。请访问 www.packtpub.com/submit-errata,选择您的书籍,点击“勘误提交表格”链接并填写相关信息。
盗版:如果您在互联网上发现任何我们作品的非法复制品,若您能提供相关网址或网站名称,我们将不胜感激。请通过copyright@packtpub.com联系并提供该资料的链接。
如果您有兴趣成为作者:如果您在某个领域有专业知识,并且有兴趣写书或为书籍做贡献,请访问 authors.packtpub.com。
评论
请留下评论。阅读并使用本书后,为什么不在您购买书籍的网站上留下评论呢?潜在的读者可以通过您的公正意见来做出购买决策,我们 Packt 也能了解您对我们产品的看法,而我们的作者也能看到您对他们书籍的反馈。谢谢!
若想了解更多关于 Packt 的信息,请访问 packtpub.com。
第一章:智能体与学习环境简介
欢迎!欢迎来到本书的第一章。本书将向你介绍令人兴奋的 OpenAI Gym 学习环境,并引导你踏上令人兴奋的旅程,帮助你掌握足够的技能,以训练基于人工智能智能体的先进系统。本书将通过从开发自动驾驶汽车到开发能够超越人类表现的 Atari 游戏代理等实用项目,帮助你获得强化学习和深度强化学习的实践经验。完成本书后,你将能够探索使用人工智能解决算法任务、玩游戏和修复控制问题的无限可能性。
本章将涵盖以下主题:
-
理解智能体与学习环境
-
了解 OpenAI Gym 的基本概念
-
各种任务/环境的类别,并简要描述每个类别适用于什么场景
-
理解 OpenAI Gym 的关键特性
-
了解使用 OpenAI Gym 工具包可以做什么
-
创建并可视化你的第一个 Gym 环境
让我们从了解智能体是什么开始我们的旅程。
什么是智能体?
人工智能的一个主要目标是构建智能体。感知环境、理解、推理、学习计划、做出决策并付诸实践是智能体的基本特征。我们将在第一章开始时,通过了解智能体是什么,从智能体的基本定义入手,逐步扩展到如何在此基础上增加智能。
代理是一个基于对其环境的观察(感知)采取行动的实体。人类和机器人是具有物理形态的代理的例子。
人类或动物是一个例子,作为代理通过使用其器官(眼睛、耳朵、鼻子、皮肤等)作为传感器来观察/感知环境,并通过其物理身体(手臂、手、腿、头等)来采取行动。机器人使用其传感器(摄像头、麦克风、LiDAR、雷达等)来观察/感知环境,并通过其物理机器人身体(机械臂、机械手/抓手、机械腿、扬声器等)来采取行动。
软件代理是能够通过与环境互动来做出决策并采取行动的计算机程序。软件代理可以通过物理形式表现出来,如机器人。自主代理是那些根据对环境观察的理解和推理,自动做出决策并采取行动的实体。
智能体是一个自主实体,能够根据与环境的互动进行学习和改进。智能体能够通过观察自我行为和表现进行分析。
在本书中,我们将开发智能代理来解决序列决策问题,这些问题可以通过在(松散的)马尔可夫环境中一系列(独立的)决策/行动来解决,在某些环境条件下,至少可以通过感知获取奖励信号作为反馈。
学习环境
学习环境是一个系统的重要组成部分,在该环境中,智能代理可以被训练来开发智能系统。学习环境定义了代理需要完成的问题或任务。
依赖于一系列决策或采取的行动而产生结果的问题或任务被称为序列决策问题。以下是一些学习环境的种类:
-
完全可观察与部分可观察
-
确定性与随机性
-
经验性与序列性
-
静态与动态
-
离散与连续
-
离散状态空间与连续状态空间
-
离散行动空间与连续行动空间
在本书中,我们将使用使用 OpenAI Gym Python 库实现的学习环境,因为它提供了一个简单且标准的接口和环境实现,同时还支持实现新的自定义环境。
在接下来的子章节中,我们将简要了解 OpenAI Gym 工具包。本节旨在帮助完全的新手熟悉 OpenAI Gym 工具包。我们假设读者没有任何先前的知识或经验。我们首先尝试了解 Gym 工具包,并浏览不同类别下可用的各种环境。接下来,我们将讨论 Gym 中可能引起你兴趣的特性,无论你关注的是哪个应用领域。然后,我们将简要讨论 Gym 工具包的价值主张以及如何利用它。在随后的章节中,我们将基于 Gym 工具包构建多个酷炫且智能的代理。因此,本章实际上是所有这些的基础。我们还将在本章末尾快速创建并可视化我们的第一个 OpenAI Gym 环境。兴奋吗?那我们就开始吧。
什么是 OpenAI Gym?
OpenAI Gym 是一个开源工具包,提供了多种任务集合,称为环境,并为开发和测试智能代理算法提供了统一的接口。该工具包引入了一个标准的应用程序编程接口(API),用于与为强化学习设计的环境进行交互。每个环境都有一个版本号,这确保了随着算法和环境本身的发展,能够进行有意义的比较并获得可复现的结果。
Gym 工具包通过其各种环境提供了一个强化学习的情境,在该情境中,智能体的经验被分解为一系列的回合。在每个回合中,智能体的初始状态是从一个分布中随机抽取的,智能体与环境的互动一直进行到环境达到终止状态。如果你不熟悉强化学习也没关系。在第二章中,我们将介绍强化学习和深度强化学习,强化学习与深度强化学习。
以下是 OpenAI Gym 库中可用的一些基本环境的截图:

OpenAI Gym 中可用的基本环境示例,并简要描述任务
在本书撰写时,OpenAI Gym 原生支持大约 797 个环境,分布在不同任务类别中。著名的 Atari 类别占有最大份额,约有 116 个环境(其中一半使用屏幕输入,另一半使用 RAM 输入)!该工具包支持的任务/环境类别如下所示:
-
算法
-
Atari
-
棋类游戏
-
Box2D
-
经典控制
-
Doom(非官方)
-
Minecraft(非官方)
-
MuJoCo
-
足球
-
玩具文本
-
机器人(新增加的)
接下来将介绍在不同类别下可用的各种类型的环境(或任务),并简要描述每个环境。请记住,运行这些类别下的环境可能需要你在系统上安装一些额外的工具和软件包。别担心!在接下来的章节中,我们将逐步讲解如何让每个环境运行起来,敬请期待!
现在我们将详细查看前面提到的各类内容,如下所示:
-
算法环境:这些环境提供需要智能体执行计算的任务,例如多位数相加、从输入序列中复制数据、反转序列等。
-
Atari 环境:这些环境提供了几个经典 Atari 游戏的接口。这些环境接口是 街机学习环境(ALE)的封装。它们提供游戏的屏幕图像或 RAM 作为输入,用于训练智能体。
-
棋盘游戏:这一类别包含了 9x9 和 19x19 围棋的环境。如果你一直在关注谷歌 DeepMind 最近在围棋领域的突破,那么这可能会非常有趣。DeepMind 开发了一个名为 AlphaGo 的智能体,使用强化学习和其他学习与规划技术,包括蒙特卡洛树搜索,击败了世界顶尖的围棋选手,包括范辉和李世石。DeepMind 还发布了 AlphaGo Zero 的研究成果,它从零开始进行训练,与原始的 AlphaGo 不同,后者是通过人类下的棋局来进行训练的。AlphaGo Zero 的表现超越了原始 AlphaGo。之后,AlphaZero 也被发布,它是一个自主系统,使用自我对弈(不需要任何人工监督的训练)学习下棋、围棋和将棋,并达到了比之前开发的系统更高的表现。
-
Box2D:这是一个开源的物理引擎,用于模拟二维刚体。Gym 工具包有一些使用 Box2D 模拟器开发的连续控制任务:

使用 Box2D 模拟器构建的环境示例列表
这些任务包括训练一个双足机器人行走、引导月球着陆器到达着陆平台,以及训练赛车在赛道上行驶。激动人心!在本书中,我们将使用强化学习训练一个 AI 代理,使其能够自主驾驶赛车绕赛道行驶!敬请期待。
-
经典控制:这一类别包含了许多已开发的任务,过去在强化学习文献中得到了广泛应用。这些任务为强化学习算法的一些早期开发和基准测试奠定了基础。例如,经典控制类别下的一个环境是 Mountain Car 环境,它最早由安德鲁·摩尔(CMU 计算机科学学院院长、Pittsburgh 创始人)在 1990 年他的博士论文中提出。这个环境至今仍然有时被作为强化学习算法的测试平台。你将在本章的最后几页创建你第一个来自该类别的 OpenAI Gym 环境!
-
Doom:这个类别为流行的第一人称射击游戏 Doom 提供了一个环境接口。它是一个非官方的、社区创建的 Gym 环境类别,基于 ViZDoom 构建,ViZDoom 是一个基于 Doom 的 AI 研究平台,提供了一个易于使用的 API,适合从原始视觉输入开发智能代理。它使得开发能够通过仅使用屏幕缓冲区来进行多个具有挑战性的 Doom 游戏回合的 AI 机器人成为可能!如果你玩过这个游戏,你就知道在某些回合中没有失去生命地前进是多么刺激和困难!虽然这款游戏的图像可能不如一些新的第一人称射击游戏那样炫酷,但放在一边,这仍然是一款很棒的游戏。近年来,机器学习,尤其是深度强化学习的多项研究,已经利用 ViZDoom 平台,开发出新的算法,来解决游戏中遇到的目标导向导航问题。你可以访问 ViZDoom 的研究网页(
vizdoom.cs.put.edu.pl/research),查看使用该平台的研究研究列表。以下截图列出了一些在 Gym 中可作为单独环境供你训练代理的任务:

Doom 环境中可用的任务或回合列表
- MineCraft:这是另一个很棒的平台。游戏 AI 开发者可能对这个环境尤其感兴趣。MineCraft 是一个在爱好者中非常受欢迎的电子游戏。MineCraft Gym 环境是基于微软的 Malmo 项目构建的,Malmo 是一个基于 Minecraft 的人工智能实验和研究平台。以下截图展示了一些作为环境在 OpenAI Gym 中可用的任务。这些环境为开发解决方案,解决这个独特环境中呈现的挑战性新问题提供了灵感:

OpenAI Gym 中可用的 MineCraft 环境
- MuJoCo:你对机器人技术感兴趣吗?你是否梦想开发能够让类人机器人走路、奔跑,或像波士顿动力的 Atlas 机器人一样做后空翻的算法?你可以做到!你将能够在本书中学习的强化学习方法,应用于 OpenAI Gym 的 MuJoCo 环境中,开发自己的算法,让 2D 机器人走路、奔跑、游泳或跳跃,甚至让 3D 多足机器人走路或奔跑!在以下截图中,你可以看到一些在 MuJoCo 环境中可用的酷炫、现实世界的机器人类环境:

-
足球:这是一个适用于训练可以协作的多个代理的环境。通过 Gym 工具包提供的足球环境具有连续的状态和动作空间。想知道这是什么意思吗?当我们在下一章讨论强化学习时,你会学到所有关于它的内容。现在,简单解释一下:连续的状态和动作空间意味着代理可以采取的动作和代理接收到的输入都是连续的值。这意味着它们可以取任意实数值,例如 0 和 1 之间的任意数值(如 0.5、0.005 等),而不是被限制在几个离散的值集合中,如 {1, 2, 3}。环境有三种类型。普通的足球环境初始化一个单一的对手,并在进球时给予 +1 的奖励,其他情况下奖励为 0。为了让代理进球,它需要学会识别球、接近球并将球踢向球门。听起来很简单,对吧?但对于计算机来说,要自己弄明白这一点是非常困难的,尤其是当你只告诉它进球时奖励 +1,其他情况下奖励 0。它没有其他线索!你可以开发出能够通过自学足球并学会进球的代理,使用本书中介绍的方法。
-
玩具文本:OpenAI Gym 在此类别下还提供一些简单的基于文本的环境。这些包括一些经典问题,例如 Frozen Lake,其中的目标是找到一条安全的路径穿越冰雪和水域的方格。它被归类为玩具文本,因为它使用了更简单的环境表示方式——主要通过文本。
有了这些,你就能对作为 OpenAI Gym 工具包一部分的所有不同类别和类型的环境有一个非常好的概览。值得注意的是,OpenAI Gym 工具包的发布伴随着一个 OpenAI Gym 网站(gym.openai.com),该网站维护了每个提交用于评估的算法的排行榜。它展示了用户提交算法的性能,并且一些提交还附有详细的解释和源代码。不幸的是,OpenAI 决定停止对评估网站的支持,该服务于 2017 年 9 月下线。
现在你已经对 OpenAI Gym 中可用的各种环境类别和每个类别为你提供的内容有了清晰的了解。接下来,我们将看一下 OpenAI Gym 的关键特性,这些特性使其成为今天许多智能代理开发进展中不可或缺的组成部分,特别是那些使用强化学习或深度强化学习的进展。
理解 OpenAI Gym 的特点
在本节中,我们将重点介绍 OpenAI Gym 工具包在强化学习社区中非常受欢迎的关键特性,并且导致它被广泛采用。
简单的环境接口
OpenAI Gym 提供了一个简单且通用的 Python 环境接口。具体来说,它以动作作为输入,并在每一步基于动作提供观察、奖励、是否完成以及一个可选的info对象作为输出。如果这对你来说还不是很明显,别担心。我们会以更详细的方式再次讲解接口,帮助你理解。这段话只是为了概述接口,让你明白它有多简单。这为用户提供了极大的灵活性,因为他们可以根据喜欢的任何范式设计和开发他们的代理算法,而不受限于使用任何特定的范式。
可比性和可复现性
我们直觉地感觉应该能够比较一个任务中代理或算法的表现与同一任务中另一个代理或算法的表现。例如,如果一个代理在太空侵略者的 Atari 游戏中平均得分为1,000,我们应该能够说这个代理比同样训练时间下平均得分为5000的代理表现更差。但是,如果游戏的评分系统略有变化会怎么样呢?或者如果环境接口被修改以包含关于游戏状态的额外信息,从而为第二个代理提供优势呢?这将使得分对比不公平,对吧?
为了处理环境的这些变化,OpenAI Gym 对环境使用严格的版本控制。该工具包保证如果环境发生任何变化,都会伴随着一个不同的版本号。因此,如果原始的 Atari 太空侵略者游戏环境的名称是SpaceInvaders-v0,并且对环境进行了一些修改以提供更多关于游戏状态的信息,那么环境的名称将更改为SpaceInvaders-v1。这种简单的版本控制系统确保我们始终在相同的环境设置上比较性能。这样,得到的结果是可比较和可复现的。
监控进展能力
Gym 工具包提供的所有环境都配备了监视器。该监视器记录模拟的每一个时间步骤和环境的每一次重置。这意味着环境会自动跟踪我们的代理在每一步中是如何学习和适应的。你甚至可以配置监视器,在代理学习玩游戏时自动记录视频。多么酷!
OpenAI Gym 工具包能做什么呢?
Gym 工具包提供了一种标准化的方式来定义为可以使用强化学习解决的问题开发的环境接口。如果你熟悉或听说过ImageNet 大规模视觉识别挑战(ILSVRC),你可能意识到标准化基准平台对加速研究和开发的影响有多大。对于那些对 ILSVRC 不熟悉的人,这里是一个简要总结:这是一个竞赛,参与团队评估他们为给定数据集开发的监督学习算法,并竞争在几个视觉识别任务中取得更高的准确性。这个共同平台,再加上由 AlexNet 推广的基于深度神经网络的算法的成功(papers.nips.cc/paper/4824-imagenet-classification-with-deep-convolutional-neural-networks.pdf),为我们目前所处的深度学习时代铺平了道路。
类似地,Gym 工具包提供了一个共同平台来对强化学习算法进行基准测试,并鼓励研究人员和工程师开发能在多个具有挑战性的任务中获得更高奖励的算法。简而言之,Gym 工具包对强化学习而言就像 ILSVRC 对监督学习一样重要。
创建您的第一个 OpenAI Gym 环境
我们将详细介绍设置 OpenAI Gym 依赖项和其他训练强化学习代理所需工具的步骤,详见第三章,开始使用 OpenAI Gym 和深度强化学习。本节提供了在 Linux 和 macOS 上使用virtualenv快速开始使用 OpenAI Gym Python API 的方法,以便您可以快速了解 Gym 的功能!
MacOS 和 Ubuntu Linux 系统默认安装了 Python。您可以通过在终端窗口中运行python --version来检查安装的 Python 版本。如果返回python后跟一个版本号,则可以继续进行下一步!如果出现 Python 命令未找到的错误,则需要安装 Python。请参阅本书的第三章,开始使用 OpenAI Gym 和深度强化学习中的详细安装部分:
- 安装
virtualenv:
$pip install virtualenv
如果您的系统未安装pip,您可以通过输入sudo easy_install pip来安装它。
- 使用 virtualenv 工具创建名为
openai-gym的虚拟环境:
$virtualenv openai-gym
- 激活
openai-gym虚拟环境:
$source openai-gym/bin/activate
- 从上游安装 Gym 工具包的所有软件包:
$pip install -U gym
如果在运行 pip install 命令时遇到权限被拒绝或错误代码为 1 的失败,通常是因为你尝试安装包的目录(在本例中是 virtualenv 中的 openai-gym 目录)需要特殊的/root 权限。你可以通过运行 sudo -H pip install -U gym[all] 来解决这个问题,或者通过运行 sudo chmod -R o+rw ~/openai-gym 更改 openai-gym 目录的权限。
- 测试以确保安装成功:
$python -c 'import gym; gym.make("CartPole-v0");'
创建和可视化一个新的 Gym 环境
只需一分钟或两分钟,你就创建了一个 OpenAI Gym 环境实例,准备开始使用!
让我们打开一个新的 Python 提示符并导入 gym 模块:
>>import gym
一旦导入了 gym 模块,我们可以使用 gym.make 方法像这样创建我们的新环境:
>>env = gym.make('CartPole-v0')
>>env.reset()
env.render()
这将弹出一个类似这样的窗口:

太棒了!
总结
恭喜你完成了第一章!希望你在创建自己的环境时感到有趣。在这一章中,你了解了 OpenAI Gym 的基本概念、它提供的功能,以及你可以用这个工具包做什么。现在,你对 OpenAI Gym 有了很好的了解。在下一章,我们将介绍强化学习的基础知识,为你打下坚实的基础,帮助你在书中不断进步,构建你酷炫的智能代理。兴奋吗?快进入下一章吧!
第二章:强化学习与深度强化学习
本章简要解释了强化学习中的基本术语和概念,帮助你更好地理解用于开发人工智能智能体的基本强化学习框架。本章还将介绍深度强化学习,并为你展示算法能够解决的高级问题类型。你会在本章中看到不少数学表达式和公式。尽管强化学习和深度强化学习背后有足够的理论可以填满整本书,但本章讨论的是对实际应用有帮助的关键概念,因此当我们在 Python 中实现算法以训练我们的智能体时,你可以清晰地理解其背后的逻辑。如果你第一次阅读时没有完全掌握,完全没有问题。你可以随时返回本章,进行复习,直到你更好地理解为止。
本章将涵盖以下主题:
-
什么是强化学习?
-
马尔科夫决策过程
-
强化学习框架
-
什么是深度强化学习?
-
深度强化学习智能体在实践中是如何工作的?
什么是强化学习?
如果你是 人工智能 (AI) 或机器学习领域的新手,可能会想知道强化学习到底是什么。简单来说,它是通过强化来学习。强化,如你从普通英语或心理学中所知,是在对某个行为的反应中增加或加强某个选择的行为,因为通过采取该行为可以获得更高的回报。我们人类从小就擅长通过强化来学习。那些有孩子的父母,可能会更频繁地利用这一点来教导他们良好的习惯。不过,我们每个人都能与此产生共鸣,因为就在不久之前,我们每个人都经历过这个阶段!比如,父母每天奖励孩子巧克力,如果孩子按时完成作业。孩子学会了只要按时完成作业,就能得到巧克力(奖励)。因此,这增强了他们每天完成作业的决心,以获得巧克力。这个通过奖励强化某个特定行为选择的学习过程,就是通过强化学习或强化学习进行的学习。
你可能会想,"哦,是的。人类心理学听起来对我很熟悉。那么,这与机器学习或人工智能有什么关系呢?" 好问题。强化学习的概念实际上是受行为心理学的启发。它位于多个研究领域的交汇处,最重要的包括计算机科学、数学、神经科学和心理学,正如下图所示:

正如我们很快就会意识到的那样,强化学习是机器学习中最有前景的方法之一,指向人工智能的未来。如果这些术语对你来说很陌生,别担心!从下一段开始,我们将一一讲解这些术语,并理解它们之间的关系,让你轻松理解。如果你已经了解这些术语,那将是一次耳目一新的阅读,从不同的视角来看待这些概念。
直观地理解人工智能的含义及其内涵
人类和动物所展示的智能被称为自然智能,而机器所展示的智能被称为人工智能(AI),原因显而易见。我们人类开发了为机器提供智能的算法和技术。迄今为止,在这一领域的伟大进展主要体现在机器学习、人工神经网络和深度学习等领域。这些领域共同推动了人工智能的发展。目前已经发展出了三种主要的机器学习范式,并且已经达到了一定的成熟度,它们分别是:
-
有监督学习
-
无监督学习
-
强化学习
在下图中,你可以直观地了解人工智能的领域。你会看到,这些学习范式是机器学习领域的子集,而机器学习本身是人工智能的一个子集/分支:

有监督学习
监督学习类似于我们教孩子通过名字识别某人或某物的方式。我们提供一个输入和与该输入相关的名字/类别标签(简称标签),并期望机器学习这种输入到标签的映射。如果我们只希望机器学习几个对象(如在物体识别类任务中)或几个人(如在面部/语音/人物识别任务中)的输入到标签的映射,这听起来可能很简单。但如果我们希望机器学习几千个类别,而每个类别可能在输入中有多个不同的变化呢?例如,如果任务是通过图像输入识别一个人的面部,而且需要从其他一千个包含面部的输入图像中区分出来,这个任务即使对于成年人来说也可能非常复杂。相同一个人的面部输入图像可能存在多种变化。某个输入图像中的人可能戴着眼镜,另一个图像中可能戴着帽子,或者表现出完全不同的面部表情。对于机器来说,能够看懂输入图像、识别面部并将其辨认出来是一个更具挑战性的任务。随着深度学习领域的最新进展,像这样的监督分类任务对机器来说不再困难。机器可以以前所未有的准确度识别面部,以及许多其他事物。例如,由 Facebook AI 研究实验室开发的 DeepFace 系统(research.fb.com/wp-content/uploads/2016/11/deepface-closing-the-gap-to-human-level-performance-in-face-verification.pdf)在 Labelled Faces in the Wild 数据集上的面部识别准确度达到了 97.45%。
无监督学习
无监督学习是一种学习形式,与监督学习范式不同,它在输入数据的同时不会为学习算法提供标签。这类学习算法通常用于发现输入数据中的模式,并将相似的数据聚类在一起。深度学习领域的最新进展引入了一种新型学习方法,称为生成对抗网络(Generative Adversarial Networks),在本书写作期间,这一方法获得了巨大的关注。如果你感兴趣,可以通过这个视频进一步了解生成对抗网络:www.packtpub.com/big-data-and-business-intelligence/learning-generative-adversarial-networks-video。
强化学习
强化学习是一种混合型的学习方式,相较于监督学习和无监督学习。正如我们在本节开始时所了解到的,强化学习是由奖励信号驱动的。在“做作业的孩子”问题中,奖励信号来自父母给的巧克力。在机器学习的世界中,巧克力可能并不吸引计算机(好吧,我们可以编程让计算机想要巧克力,但为什么要这么做呢?难道孩子们还不够吗?),但是一个简单的标量值(一个数字)就能解决问题!奖励信号仍然是以某种方式由人类指定的,表示任务的预期目标。例如,为了训练智能体使用强化学习来玩雅达利游戏,游戏得分可以作为奖励信号。这使得强化学习变得更简单(对于人类而非机器!),因为我们不需要在游戏的每一时刻标记按下哪个按钮来教机器如何玩游戏。相反,我们只是让机器自主学习如何最大化得分。难道这不令人着迷吗?我们能够让机器自己找出如何玩游戏、如何驾驶汽车,或如何做作业,只要我们给出分数来评估它的表现?这就是我们在本章学习这一内容的原因。在接下来的章节中,你将亲自开发一些这样的酷炫机器。
实践中的强化学习
现在你已经对人工智能的真正含义以及推动其发展的各种算法类别有了直观的理解,我们将专注于构建强化学习机器的实际方面。
以下是你在开发强化学习系统时需要了解的核心概念:
-
智能体
-
奖励
-
环境
-
状态
-
价值函数
-
策略
智能体
在强化学习的世界里,机器是由(软件)智能体来运行或指导的。智能体是机器中具备智能并决定接下来做什么的部分。当我们深入了解强化学习时,你会多次遇到“智能体”这个术语。强化学习基于奖励假设,该假设指出任何目标都可以通过最大化期望的累计奖励来描述。那么,究竟什么是这个奖励呢?接下来我们将讨论这个问题。
奖励
奖励,通常表示为
,通常是一个标量量,它作为反馈提供给智能体,以驱动其学习。智能体的目标是最大化奖励的总和,而该信号表示智能体在时间步
时的表现。以下是不同任务的奖励信号示例,可能帮助你更直观地理解这一概念:
-
对于我们之前讨论的雅达利游戏,或者一般的计算机游戏,每当分数增加时,奖励信号可以是
+1,而每当分数减少时,奖励信号则是-1。 -
对于股票交易,奖励信号可以是每赚取一美元就奖励
+1,每亏损一美元就惩罚-1。 -
对于模拟驾驶汽车,奖励信号可以是每行驶一英里奖励
+1,每发生一次碰撞则惩罚-100。 -
有时,奖励信号可能是稀疏的。例如,在一场国际象棋或围棋比赛中,如果代理赢得比赛,奖励信号可能是
+1,如果代理输了比赛,则奖励信号为-1。奖励是稀疏的,因为代理只有在完成一整局游戏后才能收到奖励信号,而在此过程中,它无法知道每一步棋的好坏。
环境
在第一章中,我们探讨了 OpenAI Gym 工具包提供的不同环境。你可能会疑惑,为什么它们被称为“环境”而不是“问题”、 “任务”或其他什么东西。现在你已经进展到了这一章,是否在脑海中响起了某种提示?
环境是代表我们感兴趣的任务或问题的平台,代理在其中与环境进行互动。下图展示了最高抽象层次下的强化学习范式:

在每个时间步(
)中,代理从环境中接收到一个观察(
),然后执行一个动作(
),并从环境中获得一个标量奖励(
),同时还会获得下一个观察(
)。这个过程会不断重复,直到达到终止状态。什么是观察,什么是状态?接下来我们来深入探讨一下。
状态
随着代理与环境的互动,这一过程会产生一系列的观察(
)、动作(
)和奖励(
),正如之前所描述的那样。在某一时间步(
),代理到目前为止知道的是它在时间步(
)之前观察到的一系列(
)、(
)和(
)。直观地看,这可以被称为历史:

在时间步(
)时,接下来发生的事情取决于历史。正式来说,用来决定接下来发生什么的信息被称为状态。***因为它依赖于直到该时间步为止的历史,所以可以表示如下:
,
在这里,
表示某个函数。
在我们继续之前,有一个细节非常重要,你需要理解。我们再来看看强化学习系统的一般表示形式:

现在,你会注意到系统中的两个主要实体——智能体和环境——各自都有自己的状态表示。环境状态,有时用
表示,是环境自身的(私有)表示,环境用它来选择下一个观察结果和奖励。这个状态通常对智能体不可见/不可用。同样,智能体也有自己对状态的内部表示,有时用
表示,这是智能体用来基于其动作的内部信息。因为这个表示是智能体内部的,所以由智能体决定使用任何函数来表示它。通常,它是基于智能体迄今为止观察到的历史的某种函数。顺便提一下,马尔可夫状态是使用来自历史的所有有用信息来表示状态的方式。根据定义,使用马尔可夫性质,状态
是马尔可夫状态或马尔可夫过程,当且仅当
,这意味着给定当前状态,未来与过去无关。换句话说,这样的状态是未来的充分统计量。一旦状态已知,历史信息可以被丢弃。通常,环境状态
和历史
满足马尔可夫性质。
在某些情况下,环境可能会直接向智能体展示其内部的环境状态。这种环境称为完全可观察环境。在智能体无法直接观察环境状态的情况下,智能体必须根据自己所观察到的信息构建自己的状态表示。这种环境称为部分可观察环境。例如,一个玩扑克的智能体只能观察到公共牌,而无法知道其他玩家手中的牌。因此,它是一个部分可观察环境。同样,只有摄像头的自动驾驶汽车无法知道其在环境中的绝对位置,这使得该环境仅为部分可观察。
在接下来的章节中,我们将学习智能体的一些关键组成部分。
模型
模型是智能体对环境的表示。它类似于我们对周围人和事物的心理模型。智能体利用其对环境的模型来预测接下来会发生什么。它有两个关键部分:
-
:状态转移模型/概率 -
:奖励模型
状态转移模型
是一个概率分布或函数,用来预测在下一个时间步骤
中,给定状态
和动作
在时间步骤
后进入状态
的概率。数学表达式如下:

智能体使用奖励模型
来预测如果它在时间步
处于状态
并采取行动
,它将获得的即时奖励
。这种对下一个时间步奖励的期望可以通过以下数学公式表示:

值函数
值函数表示智能体对未来奖励的预测。值函数有两种类型:状态值函数和动作值函数。
状态值函数
状态值函数是一个表示智能体在时间步 t 处于状态
时的估计值的函数。它用
表示,通常简称为 值函数。它表示智能体对未来奖励的预测,即如果它最终在时间步 t 处于状态
,将会获得的奖励。数学上,它可以表示为:

这个表达式的意思是,策略
下状态
的值是未来奖励的折扣总和的期望,其中
是折扣因子,是一个位于 [0,1] 范围内的实数。在实际中,折扣因子通常设置在 [0.95,0.99] 的范围内。另一个新术语是
,它是智能体的策略。
动作值函数
动作值函数是一个表示智能体在状态
下采取动作
的估计值的函数。它用
表示。它与状态值函数通过以下方程式相关:

策略
策略用
表示,规定了给定状态下应该采取的动作。它可以看作是一个将状态映射到动作的函数。策略主要有两种类型:确定性策略和随机策略。
一个确定性策略为给定状态规定一个动作,也就是说,给定状态 s 只有一个动作
。数学上,它表示为
。
一个随机策略在给定状态
和时间步
时,规定了一种动作分布,即每个动作都有一个概率值。数学上,它表示为
。
遵循不同策略的智能体可能在相同环境下表现出不同的行为。
马尔可夫决策过程
马尔可夫决策过程(MDP)为强化学习提供了一个正式框架。它用于描述一个完全可观察的环境,其中结果部分是随机的,部分依赖于智能体或决策者所采取的行动。以下图示展示了一个马尔可夫过程如何通过马尔可夫奖励过程演变为马尔可夫决策过程:

这些阶段可以描述如下:
-
马尔可夫过程(或 马尔可夫链)是一个随机状态序列 s1, s2,...,它遵循 马尔可夫性质。简单来说,它是一个没有关于历史的记忆的随机过程。
-
马尔可夫奖励过程(MRP)是一个 马尔可夫过程(也叫 马尔可夫链)带有值。
-
马尔可夫决策过程 是一个 马尔可夫奖励过程,并且带有决策。
使用动态规划进行规划
动态规划是一种非常通用的方法,用于高效地解决可以分解为重叠子问题的问题。如果你在代码中使用过任何类型的递归函数,你可能已经有了一些动态规划的初步体验。简单来说,动态规划尝试缓存或存储子问题的结果,以便在需要时可以重复使用,而不是重新计算结果。
好吧,你可能会问,这与这里有什么关系呢?嗯,它们对于解决一个完全定义的 MDP 非常有用,这意味着如果代理完全了解 MDP,它可以使用动态规划找到在环境中采取最优行动的方式,从而获得最高奖励!在下表中,你会看到一个简明的总结,列出了在我们关注顺序预测或控制时的输入和输出:
| 任务/目标 | 输入 | 输出 |
|---|---|---|
| 预测 | MDP 或 MRP 和策略 ![]() |
值函数 ![]() |
| 控制 | MDP | 最优值函数 和最优策略 ![]() |
蒙特卡洛学习和时序差分学习
此时,我们明白了学习状态值函数
对于代理来说非常有用,它可以告知代理处于状态
时的长期价值,从而帮助代理决定是否这个状态是值得处于的。蒙特卡洛(MC)和 时序差分(TD)学习方法使得代理能够学到这一点!
MC 和 TD 学习的目标是从代理的经验中学习值函数,代理遵循其策略
。
下表总结了 MC 和 TD 学习方法的值估计更新公式:
| 学习方法 | 状态值函数 |
|---|---|
| 蒙特卡洛 | ![]() |
| 时序差分 | ![]() |
MC 学习方法将值更新到 实际回报
,这是从时间步 t 开始的总折现奖励。这意味着
直到结束。需要注意的是,我们只能在序列结束后计算该值,而时序差分学习(严格来说是 TD(0))会根据
给出的 估计回报 更新值,这个值可以在每一步后计算。
SARSA 和 Q-learning
对于代理来说,学习行动价值函数也非常有用!,该函数告知代理在状态!下采取行动!的长期价值,从而帮助代理采取那些能够最大化其预期折扣未来奖励的行动。SARSA 和 Q 学习算法使得代理能够学习到这一点!下表总结了 SARSA 算法和 Q 学习算法的更新公式:
| 学习方法 | 行动价值函数 |
|---|---|
| SARSA | ![]() |
| Q 学习 | ![]() |
SARSA 之所以得名,是因为算法的更新步骤依赖于序列 State->Action->Reward->State'->Action'。该序列的描述如下:代理在状态S下采取行动 A 并获得奖励 R,最终进入下一个状态 S',然后代理决定在新状态下采取行动 A'。基于这一经验,代理可以更新其对 Q(S,A)的估计。
Q 学习是一个流行的离策略学习算法,它与 SARSA 相似,除了一个区别:它并不是使用新状态下代理所采取行动的 Q 值估计,而是使用一个对应于从该新状态 S'出发能够获得的最大Q 值的行动的 Q 值估计。
深度强化学习
在你对强化学习有了基本理解后,你现在应该处于一个更好的状态(希望你不是处于一个严格的马尔可夫状态,忘记了之前学过的历史/知识),以便理解这套近年来在人工智能领域引起轰动的新算法的基础知识。
深度强化学习自然地出现在人们在深度学习领域取得进展并将其应用于强化学习时。我们学习了状态值函数、动作值函数和策略。让我们简要看看它们是如何在数学上表示或者通过计算机代码实现的。状态值函数
是一个实值函数,它将当前状态
作为输入,并输出一个实值数字(例如 4.57)。这个数字是智能体预测在状态
中所处的情况有多好,智能体会根据它获得的新经验不断更新该值函数。同样,动作值函数
也是一个实值函数,它除了状态
之外,还将动作
作为输入,输出一个实数。表示这些函数的一种方式是使用神经网络,因为神经网络是通用的函数逼近器,能够表示复杂的非线性函数。对于一个试图通过仅仅查看屏幕上的图像(就像我们做的那样)来玩 Atari 游戏的智能体,状态
可能是屏幕上图像的像素值。在这种情况下,我们可以使用带有卷积层的深度神经网络从状态/图像中提取视觉特征,然后通过几个全连接层来最终输出
或者
,具体取决于我们想要逼近哪个函数。
回忆本章早些时候提到的,
是状态值函数,它提供了处于状态
时的价值估计,而
是动作值函数,它提供了在给定状态下每个动作的价值估计。
如果我们这样做,那么我们就在进行深度强化学习!够简单理解吧?希望如此。让我们看看还有哪些方式可以将深度学习应用于强化学习。
请回想,在确定性策略的情况下,策略表示为
,在随机策略的情况下,策略表示为
,其中动作
可以是离散的(例如“向左移动”,“向右移动”或“直行”)或连续的值(例如“0.05”表示加速,“0.67”表示转向,等等),并且这些值可以是单维的或多维的。因此,策略有时可能是一个复杂的函数!它可能需要接受多维状态(例如图像)作为输入,并输出一个多维的概率向量作为输出(在随机策略的情况下)。那么,这看起来像是一个巨大的函数,不是吗?是的,确实如此。正是在这里,深度神经网络派上了用场!我们可以通过深度神经网络来逼近智能体的策略,并直接学习如何更新策略(通过更新深度神经网络的参数)。这被称为基于策略优化的深度强化学习,并且已被证明在解决一些具有挑战性的控制问题时非常高效,尤其是在机器人学领域。
总结来说,深度强化学习是将深度学习应用于强化学习,迄今为止,研究人员已经成功地通过两种方式将深度学习应用于强化学习。一种方式是使用深度神经网络来逼近价值函数,另一种方式是使用深度神经网络来表示策略。
这些想法从早期就已被人们知晓,当时研究人员试图使用神经网络作为价值函数的逼近器,甚至早在 2005 年。但直到最近,这些想法才崭露头角,因为尽管神经网络或其他非线性价值函数逼近器能更好地表示环境状态和动作的复杂值,它们却容易导致不稳定性,并且常常会导致次优函数。直到最近,像 Volodymyr Mnih 和他在 DeepMind(现为 Google 的一部分)的同事们才找到了稳定学习的窍门,并用深度非线性函数逼近器训练智能体,使得其收敛到近优的价值函数。在本书的后续章节中,我们实际上将重现他们当时开创性的成果,这些成果甚至超越了人类在 Atari 游戏中的表现!
强化学习和深度强化学习算法的实际应用
直到最近,由于样本复杂性和不稳定性,强化学习和深度强化学习的实际应用一直受到限制。但这些算法在解决一些真正困难的实际问题上,已被证明非常强大。以下列出了一些应用实例,帮助你了解这些算法:
-
学习比人类更好地玩视频游戏: 这一消息可能已经传到你耳中了。DeepMind 和其他研究人员开发了一系列算法,从 DeepMind 的深度 Q 网络(简称 DQN)开始,该算法在玩 Atari 游戏时达到了人类水平的表现。我们将在本书的后续章节中实现这一算法!本质上,它是 Q-learning 算法的一个深度变种,我们在本章中简要提到过,经过一些修改,使学习速度更快,稳定性更强。它能够在进行几局游戏后达到人类水平的游戏得分。更令人印象深刻的是,同一个算法在没有进行任何特定游戏的微调或修改的情况下,达到了这一水平的表现!
-
掌握围棋游戏: 围棋是一种中国游戏,几十年来一直是人工智能的挑战。围棋在 19 x 19 的完整棋盘上进行,比国际象棋更为复杂,因为可能的棋盘位置数量庞大(见图)。直到最近,尚没有任何人工智能算法或软件能够在这一游戏中接近人类的水平。AlphaGo—DeepMind 开发的使用深度强化学习和蒙特卡洛树搜索的 AI 代理—改变了这一切,战胜了人类世界冠军李世石(4-1)和范晖(5-0)。DeepMind 发布了更先进版本的 AI 代理,命名为 AlphaGo Zero(该版本不依赖任何人类知识,完全自学会下围棋!)和 AlphaZero(能够玩围棋、国际象棋和将棋!),它们都以深度强化学习为核心算法。
-
帮助人工智能赢得《Jeopardy!》游戏: IBM 的 Watson—由 IBM 开发的人工智能系统,以战胜人类选手而成名—利用 TD 学习的扩展,制定了每日双倍下注策略,帮助其战胜了人类冠军。
-
机器人运动和操作: 强化学习和深度强化学习都使得控制复杂的机器人成为可能,涵盖了运动和导航功能。来自 UC 伯克利大学的几项最新研究展示了如何通过深度强化学习训练策略,提供机器人操作任务的视觉与控制,并生成关节驱动,使复杂的双足人形机器人能够行走和奔跑。
摘要
在本章中,我们讨论了智能体如何通过根据从环境中获得的观察信息采取行动与环境互动,环境则通过(可选的)奖励和下一个观察来响应智能体的行动。
在简要了解强化学习的基础知识后,我们深入探讨了深度强化学习的概念,并揭示了我们可以使用深度神经网络来表示价值函数和策略的事实。尽管这一章在符号和定义上稍显繁琐,但希望它为我们在接下来的章节中开发一些有趣的智能体奠定了坚实的基础。在下一章中,我们将巩固前两章的学习,并通过为训练一个智能体来解决一些有趣的问题打下基础。
第三章:开始使用 OpenAI Gym 和深度强化学习
介绍章节让你对 OpenAI Gym 工具包和强化学习有了很好的了解。在本章中,我们将直接进入正题,确保你和你的计算机做好了所有必要的准备、安装和配置,以开始开发你的智能体。更重要的是,你还会找到访问本书代码库的说明,这些代码库包含了你跟随本书学习所需的所有代码,并且有许多其他的代码示例、有用的说明和更新。
在本章中,我们将涵盖以下主题:
-
访问本书的代码库
-
为本书创建一个 Anaconda 环境
-
如何在你的系统上安装和配置 OpenAI Gym 及其依赖项
-
安装深度强化学习的工具、库和依赖项
代码库、设置和配置
首先,让我们确保你拥有访问本书代码库的所有信息。源代码为你提供了本书中讨论的所有必要代码示例,并提供了有关如何设置和运行每章训练或测试脚本的详细说明。要开始,请访问本书的 GitHub 代码库,链接如下:github.com/PacktPublishing/Hands-On-Intelligent-Agents-with-OpenAI-Gym。
如果你还没有 GitHub 账户,请创建一个,并且将代码库 fork 到你自己的 GitHub 账户中。这样做是推荐的,因为它允许你在跟随学习的同时,随意修改代码,也能在你有有趣的内容时,发送 pull request,并且可能会在本书的博客中展示!
你可以使用以下命令将代码库克隆到主目录中的一个名为 HOIAWOG 的文件夹:
git clone https://github.com/PacktPublishing/Hands-On-Intelligent-Agents-with-OpenAI-Gym.git ~/HOIAWOG
请注意,本书假设你已经将代码库设置在以下位置:~/HOIAWOG。如果你因某些原因更改了这个位置,请务必记住它,并相应地修改书中的一些命令。
如果你在想为什么目录名称选为 HOIAWOG,不要再想了。这是本书标题 Hands On Intelligent Agents With OpenAI Gym(HOIAWOG)的缩写!
本书的代码库将会保持更新,以应对外部库或其他软件的变化,确保智能体实现代码和其他代码示例能够正常运行。偶尔也会添加新的代码和更新,帮助你进一步探索智能体的开发。为了及时了解变化并接收更新通知,建议你在 GitHub 账户中为本书的代码库加星标。
在第一章 智能体和学习环境简介的最后,我们快速安装了 OpenAI Gym,简单了解了 Gym 的功能。那是一次最小化的安装,目的是让我们迅速入门。在接下来的部分,我们将逐步介绍安装过程,确保你所需要的用于开发 Gym 智能体的所有内容都已正确安装和配置。我们将在这里讨论不同级别和方法的安装方式,确保你了解整个安装过程。你可能最终会修改系统,或者在家里或工作中使用其他系统,甚至更换电脑。这一部分将确保你能以正确的方式完成所有设置。可以根据你的实际需求选择合适的安装方式。
先决条件
使用 OpenAI Gym 的唯一主要前提条件是 Python 3.5 及以上版本。为了让进一步的开发更简单且有组织,我们将使用 Anaconda Python 发行版。如果你不熟悉 Anaconda,它是一个 Python 发行版(虽然也有 R 语言的发行版),包含了数百个流行的机器学习和数据科学包,并配有一个名为 conda 的易用包和虚拟环境管理器。好消息是,Anaconda Python 发行版支持 Linux、macOS 和 Windows!另一个使用 Anaconda 发行版的主要原因是,它能帮助我们轻松创建、安装、管理和升级一个独立的 Python 虚拟环境。这确保了我们在本书中学习和开发的代码无论在哪个操作系统上执行,都能得到相同的结果。这将让你摆脱手动处理依赖问题或库版本不匹配问题的麻烦,而如果不使用像 Anaconda 这样的 Python 发行版,你本来得自己处理这些问题。你会发现它只是“有效”,这非常好,令人开心。让我们开始吧,安装 Anaconda Python 发行版。
打开命令提示符或终端,并输入以下内容:
praveen@ubuntu:~$wget http://repo.continuum.io/archive/Anaconda3-4.3.0-Linux-x86_64.sh -O ~/anaconda.sh
该命令使用 wget 工具来获取/下载 Anaconda 版本 3-4.3 的安装脚本,并将其保存为 anaconda.sh 文件在你的主目录下。此命令应该适用于预装了 wget 工具的 macOS 和 Linux(如 Ubuntu、Kubuntu 等)。请注意,我们下载的是特定版本的 Anaconda(3-4.3),这将确保本书中的配置保持一致。即使这不是最新版本,也不用担心。你可以稍后通过以下命令升级发行版:
conda update conda
anaconda.sh 是一个 shell 脚本,包含了安装 Anaconda 所需的所有内容!如果你感兴趣,可以使用你喜欢的文本编辑器打开它,看看如何巧妙地将二进制文件、安装过程说明和 shell 命令都合并到一个文件中。
现在让我们在你的主目录下安装 Anaconda Python 发行版。以下安装过程已经精心安排,确保它能在 Linux 和 macOS 系统上都能正常工作。在输入命令之前,你需要注意一件事。以下命令将在静默模式下运行安装程序。这意味着它将使用默认的安装参数并继续安装,而不会逐一询问你是否继续进行每个配置。这也意味着你同意 Anaconda 发行版的许可条款。如果你想手动一步步进行安装过程,可以运行以下命令,但不带-b和-f参数:
praveen@ubuntu:~$bash ~/anaconda.sh -b -f -p $HOME/anaconda
等待安装过程完成,接着我们就完成了!
为了开始使用conda和 Anaconda Python 发行版中的其他工具,我们需要确保系统知道在哪里找到 Anaconda 工具。让我们通过将 Anaconda 二进制目录的路径添加到PATH环境变量中,如下所示:
praveen@ubuntu:~$export PATH=$HOME/anaconda/bin:$PATH
我强烈建议你将这一行添加到~/.bashrc文件的末尾,这样每次打开新的 bash 终端时,Anaconda 工具都会可用。
你可以输入以下命令来确认安装是否成功:
praveen@ubuntu:~$conda list
这个命令将仅仅打印出你默认环境中可用的包列表。
创建 conda 环境
现在我们已经设置好了 Anaconda,让我们使用 conda 创建一个 Python 虚拟环境,在本书中我们将使用这个环境。
如果你更倾向于一键安装并且不想逐步进行安装过程,一个大大简化的方法是使用conda_env.yamlconda 环境配置文件,该文件可以在本书的代码库中找到。你只需从本书代码库目录(HOIAWOG)运行以下命令,即可创建包含所有必要包的环境,前提是我们在上一节中已经创建了该目录:
praveen@ubuntu:~/HOIAWOG$ conda create -f conda_env.yaml -n rl_gym_book
此时,我们将仅创建一个新的最小环境以继续进行。请在终端中输入以下命令:
praveen@ubuntu:~$conda create --name rl_gym_book python=3.5
这将创建一个名为rl_gym_book的 conda 环境,并使用 Python3 解释器。它会打印一些即将下载的信息以及将要安装的包。你可能会被提示是否要继续,输入y并按Enter键。环境创建过程完成后,你可以使用以下命令激活该环境:
praveen@ubuntu:~$source activate rl_gym_book
现在你会看到命令提示符的前缀变成类似于这样,表示你已经进入了rl_gym_book虚拟环境:
(rl_gym_book) praveen@ubuntu:~$
你可以将其作为进度的指示,帮助你了解何时需要在这个环境中输入命令,何时可以在环境外输入命令。要退出或停用环境,只需输入以下命令:
praveen@ubuntu:~$source deactivate
最小安装——快速简便的方法
OpenAI Gym 是一个 Python 包,已发布在 Python 包索引 (PyPI) 仓库中。你可以使用 easy_install 或 pip 从 PyPI 仓库获取并安装包。如果你有 Python 脚本经验,pip 是你可能熟悉的一个 Python 包管理工具:
(rl_gym_book) praveen@ubuntu:~$pip install gym
就这些!
让我们通过运行以下代码快速检查安装是否成功。创建一个 gym_install_test.py 文件并保存在 ~/rl_gym_book 目录下,将以下代码输入/复制到该文件中并保存。你也可以从书本的代码仓库下载 gym_quick_install_test.py 文件:
#! /usr/bin/env python
import gym
env = gym.make("MountainCar-v0") # Create a MountainCar environment
env.reset()
for _ in range(2000): # Run for 2000 steps
env.render()
env.step(env.action_space.sample()) # Send a random action
让我们尝试运行这个脚本:
(rl_gym_book) praveen@ubuntu:~/HOIAWOG$python gym_quick_install_test.py
这应该会弹出一个新窗口,显示一辆车/纸箱和一个 V 形山脉,你应该会看到汽车左右随机移动。山地车窗口应该看起来像这样的截图:

你还会在控制台/终端看到一些类似如下的值:

如果你看到这种情况,那就恭喜你!你现在已经拥有了一个(最小)版本的 OpenAI Gym 设置!
完整安装 OpenAI Gym 学习环境
不是所有的环境都能通过最小安装使用。为了能够使用 Gym 中的大部分或所有环境,我们将安装所需的依赖项,并从主分支的最新源代码构建 OpenAI Gym。
要开始,我们首先需要安装所需的系统包。接下来,你将找到 Ubuntu 和 macOS 的安装指南。根据你的开发平台选择相应的安装指南。
Ubuntu 安装指南
以下命令已在 Ubuntu 14.04 LTS 和 Ubuntu 16.04 LTS 上测试过,但应该也能在其他/未来的 Ubuntu 版本中正常运行。
让我们通过在终端/控制台运行以下命令来安装所需的系统包:
sudo apt-get update
sudo apt-get install -y build-essential cmake python-dev python-numpy python-opengl libboost-all-dev zlib1g-dev libsdl2-dev libav-tools xorg-dev libjpeg-dev swig
此命令将安装所需的系统包。请注意,-y 标志将自动确认安装包,无需你手动确认。如果你想查看将要安装的包,可以不加该标志运行命令。
macOS 安装指南
在 macOS 上,所需安装的额外系统包数量比在 Ubuntu 系统上要少。
从终端运行以下命令:
brew install cmake boost sdl2 swig wget
brew install boost-python --with-python3
这些命令将安装所需的系统包。
OpenAI Gym 中的机器人和控制环境使用 多关节动力学与接触 (MuJoCo) 作为物理引擎,模拟刚体动力学及其他特性。我们在第一章《智能体与学习环境介绍》中简要看过 MuJoCo 环境,了解到你可以开发算法使 2D 机器人行走、奔跑、游泳或跳跃,或使 3D 多足机器人行走或奔跑,均可通过 MuJoCo 环境实现。MuJoCo 是一个专有引擎,因此需要许可证。幸运的是,我们可以获得 30 天免费的许可证!
此外,如果你是学生,他们提供 1 年免费的 MuJoCo Pro 个人许可证,这样更好!对于其他人来说,30 天后,遗憾的是,1 年许可证的费用相当高(约 500 美元)。我们在本书中不会使用 MuJoCo 环境,因为并不是每个人都能获得许可证。如果你拥有许可证,可以将本书中关于其他环境的学习应用到 MuJoCo 环境中。如果你计划使用这些环境,接下来你需要按照 MuJoCo 安装部分的说明进行操作。如果不打算使用,可以跳过这一部分,继续设置 OpenAI Gym 的下一部分。
MuJoCo 安装
希望你已经阅读了之前的信息框。MuJoCo 是本书中会遇到的一个特殊库,因为与我们在本书中使用的其他库和软件不同,MuJoCo 使用时需要许可证。MuJoCo 的 Python 接口(可在 Gym 库中找到)只与 MuJoCo 版本 1.31 兼容(截至本章写作时),尽管最新的 MuJoCo 版本已经更高(截至写作时为 1.50)。请按照以下两步设置 MuJoCo 以用于 OpenAI Gym 环境:
-
从以下网址下载适合你平台的 MuJoCo 1.31(Linux/macOS)版本:
www.roboti.us/index.html -
从以下网址获取 MuJoCo Pro 许可证:
www.roboti.us/license.html
完成 OpenAI Gym 设置
首先,让我们更新我们的 pip 版本:
(rl_gym_book) praveen@ubuntu:~$ pip install --ignore-installed pip
然后,让我们从 GitHub 仓库中下载 OpenAI Gym 的源代码到我们的主目录:
(rl_gym_book) praveen@ubuntu:~$cd ~
(rl_gym_book) praveen@ubuntu:~$git clone https://github.com/openai/gym.git
(rl_gym_book) praveen@ubuntu:~$cd gym
如果你遇到类似 git command not found 的错误,你可能需要安装 Git。在 Ubuntu 系统中,你可以通过运行以下命令来安装 Git:sudo apt-get install git。在 macOS 上,如果你尚未安装 Git,运行 git clone 命令时会提示你安装它。
现在我们已经进入完整 Gym 安装的最后阶段!如果你获得了 MuJoCo 许可证并成功按照 MuJoCo 安装说明进行操作,那么可以继续通过运行以下命令来完成完整安装:
(rl_gym_book) praveen@ubuntu:~/gym$pip install -e '.[all]'
如果你没有安装 MuJoCo,那么此命令将返回错误。我们将安装我们将使用的 Gym 环境,除了 MuJoCo(它需要许可证)。确保你仍然在gym目录下的home文件夹中,并且仍然在rl_gym_book conda 环境中。你的提示符应包含rl_gym_book前缀,如下所示,其中~/gym表示提示符位于home文件夹下的 gym 目录中:
(rl_gym_book) praveen@ubuntu:~/gym$
以下是一个表格,汇总了安装第一章,智能体与学习环境简介中讨论过的环境的安装命令。
| 环境 | 安装命令 |
|---|---|
| Atari | pip install -e '.[atari]' |
| Box2D | pip install -e '.[box2d]'``conda install -c https://conda.anaconda.org/kne pybox2d |
| 经典控制 | pip install -e '.[classic_control]' |
| MuJoCo(需要许可) | pip install -e '.[mujoco]' |
| 机器人学(需要许可) | pip install -e '.[robotics]' |
让我们继续安装不需要许可证的环境。运行以下命令安装 Atari、Box2D 和经典控制环境:
(rl_gym_book) praveen@ubuntu:~/gym$pip install -e '.[atari]'
(rl_gym_book) praveen@ubuntu:~/gym$pip install -e '.[box2d]'
(rl_gym_book) praveen@ubuntu:~/gym$conda install -c https://conda.anaconda.org/kne pybox2d
(rl_gym_book) praveen@ubuntu:~/gym$pip install -e '.[classic_control]'
test_box2d.pyunder the ~/rl_gym_book directory:
#!/usr/bin/env python
import gym
env = gym.make('BipedalWalker-v2')
env.reset()
for _ in range(1000):
env.render()
env.step(env.action_space.sample())
使用以下命令运行代码:
(rl_gym_book) praveen@ubuntu:~/gym$cd ~/rl_gym_book
(rl_gym_book) praveen@ubuntu:~/rl_gym_book$python test_box2d.py
你将看到一个窗口弹出,展示 BipedalWalker-v2 环境,行走者正在尝试随机执行一些动作:

所以,我们已经设置好了 Gym 环境。你可能会问,接下来是什么?在下一节中,我们将设置开发深度强化学习代理所需的工具和库,以便在这些环境中进行训练!
安装深度强化学习所需的工具和库
第二章,强化学习与深度强化学习,为你提供了强化学习的基础知识。有了这些理论背景,我们将能够实现一些很酷的算法。在那之前,我们将确保我们拥有所需的工具和库。
我们实际上可以在 Python 中编写很酷的强化学习算法,而不使用任何高级库。然而,当我们开始使用函数逼近器来近似价值函数或策略,尤其是当我们使用深度神经网络作为函数逼近器时,最好使用经过高度优化的深度学习库,而不是编写自己的代码。深度学习库是我们需要安装的主要工具/库。如今有许多不同的库:PyTorch、TensorFlow、Caffe、Chainer、MxNet 和 CNTK 等。每个库都有自己的哲学、优点和缺点,具体取决于使用场景。由于 PyTorch 易于使用并且支持动态图定义,我们将在本书中使用 PyTorch 来开发深度强化学习算法。本书中讨论的算法及其实现方式将以一种方式进行解释,以便你可以轻松地使用你选择的框架重新实现它们。
如果你的机器没有 GPU,或者如果你不打算使用 GPU 进行训练,你可以跳过 GPU 驱动安装步骤,并使用以下 conda 命令安装仅限 CPU 的 PyTorch 二进制版本:
(rl_gym_book) praveen@ubuntu:~$ conda install pytorch-cpu torchvision -c pytorch
请注意,你将无法加速我们将在本书中开发的部分代理的训练,这些代理可以利用 GPU 来加速训练。
安装前置系统软件包
让我们首先确保我们从 Ubuntu 上游仓库获得最新的软件包版本。我们可以通过运行以下命令来实现:
sudo apt-get update
sudo apt-get upgrade
接下来,我们将安装所需的前置软件包。注意
其中一些软件包可能已经在你的系统上安装,但最好确保我们拥有所有必要的软件包:
sudo apt-get install -y gfortran pkg-config software-properties-common
安装计算统一设备架构(CUDA)
如果你没有 Nvidia GPU,或者如果你有一块较旧的 Nvidia GPU 且不支持 CUDA,你可以跳过这一步,继续执行下一部分,在其中我们将介绍 PyTorch 的安装:
-
从 Nvidia 官方网站下载适用于你的 Nvidia GPU 的最新 CUDA 驱动程序,网址为:
developer.nvidia.com/cuda-downloads。 -
在操作系统下选择 Linux 以及你的架构(通常是 x86_64),然后根据你的版本选择相应的 Linux 操作系统发行版(Ubuntu)版本 14.04、16.04 或 18.04,并选择 deb(local)作为安装类型。这样会下载名为
cuda-repo-ubuntu1604-8-0-local_8.0.44-1_amd64的 CUDA 本地安装文件。记下你的 CUDA 版本(在本例中为 8.0)。稍后我们将使用这个 CUDA 版本来安装 PyTorch。 -
然后,你可以按照说明或运行以下命令来安装 CUDA:
sudo dpkg -i cuda-repo-ubuntu*.deb
sudo apt-get update
sudo apt-get install -y cuda
如果一切顺利,你应该已经成功安装了 CUDA。为了快速检查一切是否正常,请运行以下命令:
nvcc -V
这会打印出 CUDA 版本信息,类似于以下截图中显示的输出。请注意,你的输出可能不同,取决于你安装的 CUDA 版本:

如果你得到了类似的输出,那么这是个好消息!
你可以继续在系统上安装最新的CUDA 深度神经网络(cuDNN)。本书不会详细讲解安装步骤,但安装步骤很简单,并列出了 Nvidia 官方 CuDNN 下载页面,网址为:developer.nvidia.com/rdp/form/cudnn-download-survey。请注意,你需要注册一个免费的 Nvidia 开发者账户才能下载。
安装 PyTorch
现在我们准备好安装 PyTorch 了!幸运的是,在我们的rl_gym_bookconda 环境中运行以下命令就能轻松完成:
(rl_gym_book) praveen@ubuntu:~$ conda install pytorch torchvision -c pytorch
请注意,这个命令会安装带有 CUDA 8.0 的 PyTorch。你应该已经注意到之前安装的 CUDA 版本,命令可能会根据你安装的 CUDA 版本有所不同。例如,如果你安装了 CUDA 9.1,安装命令会是:
conda install pytorch torchvision cuda91 -c pytorch
你可以在pytorch.org找到根据你的操作系统、包管理器(conda 或 pip 或源码安装)、Python 版本(我们使用的是 3.5)和 CUDA 版本更新的安装命令。
就这样!我们快速尝试导入 PyTorch 库,确保它能够正常工作。将以下代码行输入或复制到名为pytorch_test.py的文件中,并将其保存在~/rl_gym_book目录下:
#!/usr/bin/env python
import torch
t = torch.Tensor(3,3) # Create a 3,3 Tensor
print(t)
在rl_gym_bookconda 环境中运行此脚本。以下截图作为示例提供:

请注意,你可能会看到张量的不同值,并且每次运行脚本时看到的值可能也不同。这是由于torch的原因。Tensor()函数会生成给定形状的随机张量,在我们的例子中是(3, 3)。PyTorch 采用与 NumPy 相似的语法。如果你熟悉 NumPy,学习 PyTorch 会很容易。如果你不熟悉 NumPy 或 PyTorch,建议你先参考官方 PyTorch 教程来熟悉它。
你可能会注意到,在一些示例控制台截图中使用的文件夹名称是read rl_gym_book而不是 HOIAWOG。这两个目录名是可以互换的,实际上它们是指向同一目录的符号链接。
总结
在本章中,我们详细讲解了使用 conda、OpenAI Gym 和 Pytorch 安装和配置开发环境的逐步设置过程!本章帮助我们确保已经安装了所有必需的工具和库,以便开始在 Gym 环境中开发我们的智能体。在下一章中,我们将探索 Gym 环境的功能,了解它们是如何工作的,以及我们如何使用它们来训练智能体。在第五章,实现你的第一个学习智能体——解决山地车问题,我们将直接开始开发第一个强化学习智能体来解决山地车问题!随后,我们将逐步实现更复杂的强化学习算法,敬请期待后续章节。
第四章:探索 Gym 及其功能
现在你已经有了一个工作环境,我们将开始探索 Gym 工具包提供的各种功能和选项。本章将带你了解一些常用的环境、它们解决的任务以及你的智能体需要做什么才能掌握这些任务。
在本章中,我们将探索以下主题:
-
探索各种类型的 Gym 环境
-
理解强化学习循环的结构
-
理解不同的观察空间和动作空间
探索环境列表和命名法
让我们从选择一个环境并理解 Gym 接口开始。你可能已经熟悉从前几章中创建 Gym 环境的基本函数调用,我们曾用这些函数测试过安装情况。在这里,我们将正式通过这些函数。
让我们激活rl_gym_book的 conda 环境,并打开 Python 提示符。第一步是使用以下代码导入 Gym Python 模块:
import gym
现在我们可以使用gym.make方法从可用的环境列表中创建一个环境。你可能会问,如何查找系统中可用的 Gym 环境列表。我们将创建一个小的实用脚本来生成环境列表,方便你在需要时参考。让我们在~/rl_gym_book/ch4目录下创建一个名为list_gym_envs.py的脚本,内容如下:
#!/usr/bin/env python
from gym import envs
env_names = [spec.id for spec in envs.registry.all()]
for name in sorted(env_names):
print(name)
该脚本将按字母顺序打印通过 Gym 安装可用的所有环境的名称。你可以使用以下命令运行此脚本,以查看系统中已安装并可用的环境名称:
(rl_gym_book) praveen@ubntu:~/rl_gym_book/ch4$python list_gym_envs.py
你将得到如下输出。注意,只有前几个环境名称会显示出来,它们可能会有所不同,具体取决于你系统上安装的环境,这些环境是根据我们在第三章中讨论的内容,开始使用 OpenAI Gym 和深度强化学习所安装的:

从环境名称列表中,你可能会注意到有一些相似的名称,带有一些变化。例如,Alien 环境有八个不同的变种。让我们在开始使用它之前,先理解命名法。
命名法
环境名称中包含ram这个词,意味着环境返回的观察值是游戏设计运行的 Atari 主机的随机存取内存(RAM)的内容。
环境名称中包含deterministic这个词,意味着智能体发送给环境的动作会在一个确定性的/固定的四帧时长内重复执行,然后返回结果状态。
包含单词 NoFrameskip 表示智能体发送到环境中的动作会执行一次,结果状态立即返回,中间不跳过任何帧。
默认情况下,如果环境名称中没有包含 deterministic 和 NoFrameskip,则发送到环境中的动作会重复执行 n 帧的持续时间,其中 n 从 {2,3,4} 中均匀采样。
环境名称中的字母 v 后跟一个数字,表示环境的版本。这是为了确保环境实现的任何变化都体现在其名称中,从而使得在不同算法/智能体下得到的结果具有可比性,不会出现任何差异。
让我们通过观察 Atari Alien 环境来理解这种命名法。以下列出了可用的各种选项及其描述:
| 版本名称 | 描述 |
|---|---|
Alien-ram-v0 |
观察是 Atari 机器的 RAM 内容,总大小为 128 字节,发送到环境中的动作会重复执行 n 帧的持续时间,其中 n 从 {2,3,4} 中均匀采样。 |
Alien-ram-v4 |
观察是 Atari 机器的 RAM 内容,总大小为 128 字节,发送到环境中的动作会重复执行 n 帧的持续时间,其中 n 从 {2,3,4} 中均匀采样。与 v0 版本相比,环境有所修改。 |
Alien-ramDeterministic-v0 |
观察是 Atari 机器的 RAM 内容,总大小为 128 字节,发送到环境中的动作会重复执行四帧的持续时间。 |
Alien-ramDeterministic-v4 |
观察是 Atari 机器的 RAM 内容,总大小为 128 字节,发送到环境中的动作会重复执行四帧的持续时间。与 v0 版本相比,环境有所修改。 |
Alien-ramNoFrameskip-v0 |
观察是 Atari 机器的 RAM 内容,总大小为 128 字节,发送到环境中的动作会应用,并且结果状态立即返回,不跳过任何帧。 |
Alien-v0 |
观察是屏幕的 RGB 图像,表示为形状为 (210, 160, 3) 的数组,发送到环境中的动作会重复执行 n 帧的持续时间,其中 n 从 {2,3,4} 中均匀采样。 |
Alien-v4 |
观察是屏幕的 RGB 图像,表示为形状为 (210, 160, 3) 的数组,发送到环境中的动作会重复执行 n 帧的持续时间,其中 n 从 {2,3,4} 中均匀采样。与 v0 版本相比,环境有所修改。 |
AlienDeterministic-v0 |
观察是一个 RGB 图像,表示为形状为(210, 160, 3)的数组,发送到环境的动作会反复执行,持续四帧。 |
AlienDeterministic-v4 |
观察是一个 RGB 图像,表示为形状为(210, 160, 3)的数组,发送到环境的动作会反复执行,持续四帧。与 v0 版本相比,环境有所修改。 |
AlienNoFrameskip-v0 |
观察是一个 RGB 图像,表示为形状为(210, 160, 3)的数组,发送到环境的动作会立即应用,且不会跳过任何帧。 |
AlienNoFrameskip-v4 |
观察是一个 RGB 图像,表示为形状为(210, 160, 3)的数组,发送到环境的动作会立即应用,且不会跳过任何帧。与 v0 版本相比,环境有所修改。 |
这个总结有助于你理解环境的命名法,并且它适用于所有环境。RAM 可能是专门针对 Atari 环境的,但现在你已经对遇到几个相关的环境名称时应该期望什么有了一个大致的了解。
探索 Gym 环境
为了便于我们可视化一个环境的样子或它的任务是什么,我们将使用一个简单的脚本,它可以启动任何环境并通过一些随机抽取的动作进行步骤。你可以从本书的代码仓库中下载这个脚本,位于ch4目录下,或者在~/rl_gym_book/ch4目录下创建一个名为run_gym_env.py的文件,内容如下:
#!/usr/bin/env python
import gym
import sys
def run_gym_env(argv):
env = gym.make(argv[1]) # Name of the environment supplied as 1st argument
env.reset()
for _ in range(int(argv[2])):
env.render()
env.step(env.action_space.sample())
env.close()
if __name__ == "__main__":
run_gym_env(sys.argv)
该脚本将接受作为第一个命令行参数传入的环境名称,以及要运行的步骤数。例如,我们可以像这样运行脚本:
(rl_gym_book) praveen@ubntu:~/rl_gym_book/ch4$python run_gym_env.py Alien-ram-v0 2000
该命令将启动Alien-ram-v0环境,并通过随机动作在环境的动作空间中进行 2,000 次步骤。
你会看到一个窗口弹出,显示Alien-ram-v0环境,像这样:

理解 Gym 接口
让我们继续探索 Gym,了解 Gym 环境与我们将开发的智能体之间的接口。为此,我们再回顾一下在第二章中看到的图片,强化学习与深度强化学习,那时我们讨论了强化学习的基本知识:

这张图给你提供了关于智能体与环境之间接口的想法吗?我们将通过详细讲解接口的描述来巩固你的理解。
在我们import gym之后,我们可以使用以下代码行来make一个环境:
env = gym.make("ENVIRONMENT_NAME")
在这里,ENVIRONMENT_NAME是我们想要的环境名称,它是从我们系统中安装的环境列表中选择的。从前面的图中可以看出,第一个箭头是从环境指向智能体的,命名为“Observation”。在第二章《强化学习与深度强化学习》中,我们理解了部分可观察环境与完全可观察环境的区别,以及每种情况下状态与观察的不同。我们通过调用env.reset()从环境中获取第一次观察。我们使用以下代码将观察存储在名为obs的变量中:
obs = env.reset()
现在,智能体已经接收到了观察(第一个箭头的终点)。是时候让智能体采取行动并将其发送到环境中,看看会发生什么。实际上,这正是我们为智能体开发的算法应该解决的问题!我们将在接下来的章节中开发各种最先进的算法,帮助智能体的发展。让我们继续了解 Gym 接口的旅程。
一旦决定了要采取的动作,我们就通过env.step()方法将其发送到环境(图中的第二个箭头),该方法将按顺序返回四个值:next_state、reward、done和info:
next_state是采取前一个状态下的动作后,环境所产生的结果状态。
有些环境可能在内部运行一个或多个步骤,使用相同的动作,然后再返回next_state。我们在前一节讨论了确定性和无帧跳跃类型,这些都是此类环境的示例。
-
reward(图中的第三个箭头)是由环境返回的。 -
done变量是一个布尔值(真或假),如果回合已经结束/完成(因此是时候重置环境了),它的值为真,否则为假。这个变量对于智能体了解回合是否结束或环境是否即将重置为某个初始状态非常有用。 -
info变量是一个可选变量,某些环境可能会返回该变量,并附带一些额外的信息。通常,智能体不会使用它来决定采取哪个动作。
这里是由 Gym 环境的step()方法返回的四个值的汇总总结,以及它们的类型和简要描述:
| 返回值 | 类型 | 描述 |
|---|---|---|
next_state(或观察) |
对象 |
环境返回的观察。该对象可能是来自屏幕/摄像头的 RGB 像素数据、RAM 内容、机器人关节角度和关节速度等,具体取决于环境。 |
reward |
Float |
先前动作所获得的奖励,发送到环境中的Float类型奖励值。Float值的范围因环境而异,但无论环境如何,较高的奖励总是更好,智能体的目标应该是最大化总奖励。 |
done |
Boolean |
指示环境是否将在下一步被重置。当布尔值为真时,这通常意味着回合已经结束(可能是由于智能体失去生命、超时或其他回合终止标准)。 |
info |
Dict |
一些额外的信息,可以作为任意键值对的字典,由环境可选地发送出来。我们开发的智能体不应该依赖该字典中的任何信息来执行动作。它可以在(如果可用的情况下)用于调试目的。 |
请注意,以下代码是为了展示一般结构而提供的,不能直接执行,因为ENVIRONMENT_NAME和agent.choose_action()在此代码片段中没有定义。
让我们把所有的部分放在一起,集中查看:
import gym
env = gym.make("ENVIRONMENT_NAME")
obs = env.reset() # The first arrow in the picture
# Inner loop (roll out)
action = agent.choose_action(obs) # The second arrow in the picture
next_state, reward, done, info = env.step(action) # The third arrow (and more)
obs = next_state
# Repeat Inner loop (roll out)
我希望你已经对环境和智能体之间交互的一个周期有了清晰的理解。这个过程会一直重复,直到我们决定在一定数量的回合或步骤后终止这个周期。现在让我们看看一个完整的示例,其中内循环运行MAX_STEPS_PER_EPISODE,外循环运行MAX_NUM_EPISODES,并且环境是Qbert-v0:
#!/usr/bin/env python
import gym
env = gym.make("Qbert-v0")
MAX_NUM_EPISODES = 10
MAX_STEPS_PER_EPISODE = 500
for episode in range(MAX_NUM_EPISODES):
obs = env.reset()
for step in range(MAX_STEPS_PER_EPISODE):
env.render()
action = env.action_space.sample()# Sample random action. This will be replaced by our agent's action when we start developing the agent algorithms
next_state, reward, done, info = env.step(action) # Send the action to the environment and receive the next_state, reward and whether done or not
obs = next_state
if done is True:
print("\n Episode #{} ended in {} steps.".format(episode, step+1))
break
当你运行此脚本时,你会注意到 Qbert 屏幕会弹出,Qbert 会进行随机动作并获得分数,如下所示:

你还会看到控制台上出现如下的打印语句,这取决于回合何时结束。请注意,你看到的步骤编号可能不同,因为动作是随机的:

样板代码可在本书的代码库中的ch4文件夹下找到,名为rl_gym_boilerplate_code.py。它确实是样板代码,因为程序的整体结构将保持不变。当我们在后续章节中构建智能体时,我们将扩展这个样板代码。值得花些时间逐行阅读脚本,以确保你能很好地理解它。
你可能已经注意到,在本章和第三章《开始使用 OpenAI Gym 和深度强化学习》中提供的示例代码片段中,我们使用了env.action_space.sample()来代替之前代码中的action。env.action_space返回动作空间的类型(例如,在 Alien-v0 环境中是Discrete(18)),而sample()方法会从该action_space中随机采样一个值。就这层意思!
现在我们将更仔细地观察 Gym 中的空间,以理解环境的状态空间和动作空间。
Gym 中的空间
我们可以看到,Gym 中的每个环境都是不同的。每个 Atari 类别下的游戏环境也都不同。例如,在 VideoPinball-v0 环境中,目标是用两个挡板不断弹跳球,通过球的撞击位置来获得积分,并确保球永远不会掉到挡板下方,而在 Alien-v0,这是另一个 Atari 游戏环境,目标是在迷宫(船舱中的房间)中移动并收集 点,这些点等同于摧毁外星人的蛋。外星人可以通过收集脉冲点来杀死,且当发生这种情况时,奖励/分数会增加。你能看到游戏/环境中的变化吗?我们如何知道在游戏中哪些动作是有效的?
在 VideoPinball 环境中,行动自然是上下移动挡板,而在 Alien 环境中,行动是指令玩家向左、向右、向上或向下移动。请注意,在 VideoPinball 环境中没有“向左移动”或“向右移动”的动作。我们如果看其他种类的环境,变化就更大了。例如,在最近发布的机器人环境中,像 fetch 机器人手臂这样的连续控制环境中,动作是改变连续值的关节位置和关节速度来完成任务。关于环境中观察值的讨论也是一样的。我们已经看到在 Atari 环境中不同的观察对象类型(RAM 与 RGB 图像)。
这就是为什么为每个环境定义观察和动作的 空间(如数学中的空间)的动机。在本书编写时,OpenAI Gym 支持六种空间(再加一个叫做 prng 的随机种子空间)。这些空间在下表中列出,并简要描述了每种空间:
Box 和 Discrete 是最常用的动作空间类型。我们现在对 Gym 中可用的各种空间类型有了基本的了解。接下来,我们来看看如何查找一个环境使用了哪些观察和动作空间。
以下脚本将打印出给定环境的观察空间和动作空间,并在 Box Space 的情况下,选项性地打印出值的下界和上界。此外,如果环境提供了动作的描述/含义,它还会打印出环境中可能的动作的描述:
#!/usr/bin/env python
import gym
from gym.spaces import *
import sys
def print_spaces(space):
print(space)
if isinstance(space, Box): # Print lower and upper bound if it's a Box space
print("\n space.low: ", space.low)
print("\n space.high: ", space.high)
if __name__ == "__main__":
env = gym.make(sys.argv[1])
print("Observation Space:")
print_spaces(env.observation_space)
print("Action Space:")
print_spaces(env.action_space)
try:
print("Action description/meaning:",env.unwrapped.get_action_meanings())
except AttributeError:
pass
该脚本也可以在本书的代码库中下载,位于 ch4 文件夹下,名为 get_observation_action_space.py。你可以使用以下命令运行脚本,在其中我们将环境名称作为第一个参数提供给脚本:
(rl_gym_book) praveen@ubuntu:~/rl_gym_book/ch4$ python get_observation_action_space.py CartPole-v0
脚本将打印出类似这样的输出:

在这个例子中,脚本打印出 CartPole-v0 环境的观察空间是 Box(4,),这对应于 小车位置、小车速度、杆角度 和 杆尖端速度 四个 box 值。
动作空间被打印为 Discrete(2),这对应于推动小车向左 和 推动小车向右,离散值 0 和 1 分别代表这两个动作。
我们来看看另一个例子,它包含了一些更复杂的空间。这一次,让我们使用 BipedalWalker-v2 环境运行脚本:
(rl_gym_book) praveen@ubuntu:~/rl_gym_book/ch4$ python get_observation_action_space.py BipedalWalker-v2
这将产生类似这样的输出:

下面是 Bipedal Walker (v2) 环境状态空间的详细描述表格,供你快速参考:
| 索引 | 名称/描述 | 最小值 | 最大值 |
|---|---|---|---|
| 0 | hull_angle | 0 | 2*pi |
| 1 | hull_angularVelocity | -inf | +inf |
| 2 | vel_x | -1 | +1 |
| 3 | vel_y | -1 | +1 |
| 4 | hip_joint_1_angle | -inf | +inf |
| 5 | hip_joint_1_speed | -inf | +inf |
| 6 | knee_joint_1_angle | -inf | +inf |
| 7 | knee_joint_1_speed | -inf | +inf |
| 8 | leg_1_ground_contact_flag | 0 | 1 |
| 9 | hip_joint_2_angle | -inf | +inf |
| 10 | hip_joint_2_speed | -inf | +inf |
| 11 | knee_joint_2_angle | -inf | +inf |
| 12 | knee_joint_2_speed | -inf | +inf |
| 13 | leg_2_ground_contact_flag | 0 | 1 |
| 14-23 | 10 个激光雷达读数 | -inf | +inf |
如你所见,状态空间非常复杂,这对于一个复杂的双足行走机器人来说是合理的。它或多或少类似于我们在现实世界中可以找到的实际双足机器人系统和传感器配置,例如波士顿动力公司的 Atlas 双足机器人,它在 2015 年的 DARPA 机器人挑战赛中成为了焦点。
接下来,我们将深入了解动作空间。下面是 Bipedal Walker (v2) 环境的动作空间的详细描述表格,供你快速参考:
| 索引 | 名称/描述 | 最小值 | 最大值 |
|---|---|---|---|
| 0 | Hip_1(扭矩/速度) | -1 | +1 |
| 1 | Knee_1(扭矩/速度) | -1 | +1 |
| 2 | Hip_2(扭矩/速度) | -1 | +1 |
| 3 | Knee_2(扭矩/速度) | -1 | +1 |
动作
扭矩控制是默认的控制方法,它控制关节处施加的扭矩量。
总结
在本章中,我们探讨了系统中可用的 Gym 环境列表,这是你在上一章中安装的,然后了解了环境的命名约定或术语。接着我们回顾了代理与环境的交互(RL 循环)图,并理解了 Gym 环境如何提供与我们在图像中看到的每个箭头对应的接口。然后,我们以表格形式,易于理解的格式总结了 Gym 环境的step()方法返回的四个值,来加深你对它们含义的理解!
我们还详细探索了在 Gym 中用于观察和动作空间的各种空间类型,并使用脚本打印出环境使用的空间,以更好地理解 Gym 环境接口。在下一章中,我们将整合到目前为止的所有学习,来开发我们的第一个人工智能代理!激动吗?!赶快翻到下一章吧!
第五章:实现你的第一个学习智能体——解决山地车问题
恭喜你走到这一步!在前面的章节中,我们已经对 OpenAI Gym 进行了很好的介绍,了解了它的功能,并学习了如何在自己的程序中安装、配置和使用它。我们还讨论了强化学习的基础知识,以及深度强化学习是什么,并设置了 PyTorch 深度学习库来开发深度强化学习应用程序。在本章中,你将开始开发你的第一个学习智能体!你将开发一个智能体,学习如何解决山地车问题。在接下来的章节中,我们将解决越来越具挑战性的问题,随着你在 OpenAI Gym 中开发强化学习算法来解决问题,你会变得更加熟练。我们将从理解山地车问题开始,这是强化学习和最优控制领域中的一个经典问题。我们将从零开始开发我们的学习智能体,然后训练它利用 Gym 中的山地车环境来解决问题。最后,我们将看到智能体的进展,并简要了解如何改进智能体,使其能够解决更复杂的问题。本章将覆盖以下主题:
-
理解山地车问题
-
实现基于强化学习的智能体来解决山地车问题
-
在 Gym 中训练强化学习智能体
-
测试智能体的性能
理解山地车问题
对于任何强化学习问题,有两个基本的定义是至关重要的,无论我们使用什么学习算法。这两个定义分别是状态空间和动作空间的定义。我们在本书前面提到过,状态空间和动作空间可以是离散的,也可以是连续的。通常,在大多数问题中,状态空间由连续值组成,并且表示为向量、矩阵或张量(多维矩阵)。与连续值问题和环境相比,具有离散动作空间的问题和环境相对简单。在本书中,我们将为一些具有状态空间和动作空间组合的不同问题和环境开发学习算法,以便当你开始独立开发智能体和算法时,能够应对任何此类变化。
让我们首先通过高级描述来理解山地车问题,然后再看一下山地车环境的状态空间和动作空间。
山地车问题和环境
在 Mountain Car Gym 环境中,一辆车位于一条一维轨道上,位置在两座山之间。目标是将车开到右边的山顶;然而,汽车的引擎力量不足,即使以最大速度行驶也无法爬上山顶。因此,唯一成功的方法是前后驱动来积累动能。简而言之,Mountain Car 问题就是让一辆动力不足的车爬上山顶。
在实现你的智能体算法之前,理解环境、问题以及状态和动作空间会大有帮助。我们如何知道 Mountain Car 环境的状态和动作空间呢?嗯,我们已经知道如何做了,从第四章,探索 Gym 及其特性中。我们编写了一个名为 get_observation_action_space.py 的脚本,它会打印出作为脚本第一个参数传入的环境的状态、观察和动作空间。让我们用以下命令来让它打印出 MountainCar-v0 环境的空间:
(rl_gym_book) praveen@ubuntu:~/rl_gym_book/ch4$ python get_observation_action_space.py 'MountainCar-v0'
注意命令提示符中有 rl_gym_book 前缀,这表示我们已激活了 rl_gym_book conda Python 虚拟环境。此外,当前目录 ~/rl_gym_book/ch4 表示脚本是从本书代码库中与第四章,探索 Gym 及其特性对应的 ch4 目录中运行的。
上述命令将产生如下输出:
Observation Space:
Box(2,)
space.low: [-1.20000005 -0.07 ]
space.high: [ 0.60000002 0.07 ]
Action Space:
Discrete(3)
从这个输出中,我们可以看到状态和观察空间是一个二维盒子,而动作空间是三维的且离散的。
如果你想复习 box 和 discrete 空间的含义,你可以快速翻到第四章,探索 Gym 及其特性,在其中我们讨论了这些空间以及它们的含义,具体在 Gym 中的空间 部分。理解它们是非常重要的。
状态和动作空间类型、描述以及允许值的范围总结在下面的表格中,供你参考:
| MountainCar-v0 环境 | 类型 | 描述 | 范围 |
|---|---|---|---|
| 状态空间 | Box(2,) |
(位置,速度) | 位置:-1.2 到 0.6 速度:-0.07 到 0.07 |
| 动作空间 | Discrete(3) |
0: 向左走 1: 滑行/不动作 2: 向右走 | 0, 1, 2 |
举个例子,小车从* -0.6 到 -0.4 之间的随机位置开始,初始速度为零,目标是到达右侧山顶,位置为0.5。(实际上,小车可以超过0.5,到达0.6,也会被考虑。)每个时间步,环境将返回 -1 作为奖励,直到小车到达目标位置(0.5)。环境将终止回合。如果小车到达0.5*位置或采取的步骤数达到 200,done变量将被设置为True。
从头开始实现 Q 学习智能体
在这一部分,我们将一步一步地开始实现我们的智能体。我们将使用NumPy库和 OpenAI Gym 库中的MountainCar-V0环境来实现著名的 Q 学习算法。
让我们重新回顾一下我们在第四章中使用的强化学习 Gym 基础模板代码,探索 Gym 及其特性,如下所示:
#!/usr/bin/env python
import gym
env = gym.make("Qbert-v0")
MAX_NUM_EPISODES = 10
MAX_STEPS_PER_EPISODE = 500
for episode in range(MAX_NUM_EPISODES):
obs = env.reset()
for step in range(MAX_STEPS_PER_EPISODE):
env.render()
action = env.action_space.sample()# Sample random action. This will be replaced by our agent's action when we start developing the agent algorithms
next_state, reward, done, info = env.step(action) # Send the action to the environment and receive the next_state, reward and whether done or not
obs = next_state
if done is True:
print("\n Episode #{} ended in {} steps.".format(episode, step+1))
break
这段代码是开发我们强化学习智能体的一个良好起点(即基础模板!)。我们将首先将环境名称从Qbert-v0更改为MountainCar-v0。注意,在前面的脚本中,我们设置了MAX_STEPS_PER_EPISODE。这是智能体在回合结束前可以采取的步骤或动作的数量。这在持续的、循环的环境中非常有用,在这种环境中,环境本身不会结束回合。这里,我们为智能体设置了一个限制,以避免无限循环。然而,大多数在 OpenAI Gym 中定义的环境都有回合终止条件,一旦满足其中任何一个条件,env.step(...)函数返回的done变量将被设置为True。我们在前面的章节中看到,对于我们感兴趣的 Mountain Car 问题,如果小车到达目标位置(0.5)或所采取的步骤数达到200,环境将结束回合。因此,我们可以进一步简化基础模板代码,使其看起来如下,适用于 Mountain Car 环境:
#!/usr/bin/env python
import gym
env = gym.make("MountainCar-v0")
MAX_NUM_EPISODES = 5000
for episode in range(MAX_NUM_EPISODES):
done = False
obs = env.reset()
total_reward = 0.0 # To keep track of the total reward obtained in each episode
step = 0
while not done:
env.render()
action = env.action_space.sample()# Sample random action. This will be replaced by our agent's action when we start developing the agent algorithms
next_state, reward, done, info = env.step(action) # Send the action to the environment and receive the next_state, reward and whether done or not
total_reward += reward
step += 1
obs = next_state
print("\n Episode #{} ended in {} steps. total_reward={}".format(episode, step+1, total_reward))
env.close()
如果你运行前面的脚本,你将看到 Mountain Car 环境在一个新窗口中出现,小车会在 1,000 个回合中随机地左右移动。你还会看到每个回合结束时,打印出回合编号、所采取的步骤数以及获得的总奖励,正如以下屏幕截图所示:

样本输出应该类似于以下屏幕截图:

你应该从前一节回忆起,代理每一步都会获得–1的奖励,而且MountainCar-v0环境会在200步后终止任务;这就是为什么代理有时会获得总奖励–200!的原因!毕竟,代理在没有思考或从之前的动作中学习的情况下进行随机行动。理想情况下,我们希望代理能够找出如何用最少的步骤到达山顶(接近旗帜,靠近、到达或超越位置0.5)。别担心——我们将在本章结束时构建这样一个智能代理!
记得在运行脚本之前始终激活rl_gym_book conda 环境!否则,你可能会遇到不必要的模块未找到错误。你可以通过查看 shell 前缀来直观确认是否已激活环境,前缀会显示如下内容:(rl_gym_book) praveen@ubuntu:~/rl_gym_book/ch5$。
让我们继续看一下 Q-learning 部分。
重温 Q-learning
在第二章,强化学习与深度强化学习中,我们讨论了 SARSA 和 Q-learning 算法。这两种算法提供了一种系统的方法来更新表示行动值函数的估计,记作
。具体而言,我们看到 Q-learning 是一种离策略学习算法,它通过以下方式更新当前状态和动作的行动值估计:向后续状态的最大可获得行动值,
,该状态根据代理的策略最终会到达。我们还看到,Q-learning 更新的公式如下所示:

在下一节中,我们将在 Python 中实现一个Q_Learner类,该类实现了学习更新规则以及其他必要的函数和方法。
使用 Python 和 NumPy 实现 Q-learning 代理
让我们通过实现Q_Learner类来开始实现我们的 Q-learning 代理。该类的主要方法如下:
-
init(self, env)
-
discretize(self, obs)
-
get_action(self, obs)
-
learn(self, obs, action, reward, next_obs)
你稍后会发现,这里的方法是常见的,几乎所有我们在本书中实现的代理都会用到这些方法。这使得你更容易掌握它们,因为这些方法会一遍又一遍地被重复使用(会有一些修改)。
discretize()函数通常对代理实现不是必需的,但当状态空间很大且是连续时,将空间离散化为可计数的区间或值范围可能更好,从而简化表示。这还减少了 Q 学习算法需要学习的值的数量,因为它现在只需学习有限的值集,这些值可以简洁地以表格格式或使用n维数组表示,而不是复杂的函数。此外,用于最优控制的 Q 学习算法,对于 Q 值的表格表示,保证会收敛。
定义超参数
在Q_Learner类声明之前,我们将初始化一些有用的超参数。以下是我们将用于Q_Learner实现的超参数:
-
EPSILON_MIN:这是我们希望代理在遵循 epsilon-greedy 策略时使用的 epsilon 值的最小值。 -
MAX_NUM_EPISODES:这是我们希望代理与环境交互的最大回合数。 -
STEPS_PER_EPISODE:这是每个回合中的步数。它可以是环境在每个回合中允许的最大步数,或者是我们基于某些时间预算希望设置的自定义值。允许每个回合更多的步数意味着每个回合可能需要更长时间才能完成,在非终止环境中,环境直到达到此限制才会重置,即使代理停留在同一位置。 -
ALPHA:这是我们希望代理使用的学习率。这是上一节中列出的 Q 学习更新方程中的 alpha。一些算法会随着训练的进行调整学习率。 -
GAMMA:这是代理将用于考虑未来奖励的折扣因子。此值对应于上一节中 Q 学习更新方程中的 gamma。 -
NUM_DISCRETE_BINS:这是将状态空间离散化为的值的区间数量。对于 Mountain Car 环境,我们将状态空间离散化为30个区间。你可以尝试更高或更低的值。
请注意,MAX_NUM_EPISODES和STEPS_PER_EPISODE已经在我们之前某一节中讲解的代码模板中定义。
这些超参数在 Python 代码中定义如下,并具有一些初始值:
EPSILON_MIN = 0.005
max_num_steps = MAX_NUM_EPISODES * STEPS_PER_EPISODE
EPSILON_DECAY = 500 * EPSILON_MIN / max_num_steps
ALPHA = 0.05 # Learning rate
GAMMA = 0.98 # Discount factor
NUM_DISCRETE_BINS = 30 # Number of bins to Discretize each observation dim
实现 Q_Learner 类的 init 方法
接下来,让我们看一下Q_Learner类的成员函数定义。__init__(self, env)函数将环境实例env作为输入参数,并初始化观察空间和行动空间的维度/形状,同时根据我们设置的NUM_DISCRETE_BINS确定离散化观察空间的参数。__init__(self, env)函数还会根据离散化后的观察空间形状和行动空间的维度,将 Q 函数初始化为一个 NumPy 数组。__init__(self, env)的实现是直接的,因为我们只是在初始化智能体所需的值。以下是我们的实现:
class Q_Learner(object):
def __init__(self, env):
self.obs_shape = env.observation_space.shape
self.obs_high = env.observation_space.high
self.obs_low = env.observation_space.low
self.obs_bins = NUM_DISCRETE_BINS # Number of bins to Discretize each observation dim
self.bin_width = (self.obs_high - self.obs_low) / self.obs_bins
self.action_shape = env.action_space.n
# Create a multi-dimensional array (aka. Table) to represent the
# Q-values
self.Q = np.zeros((self.obs_bins + 1, self.obs_bins + 1,
self.action_shape)) # (51 x 51 x 3)
self.alpha = ALPHA # Learning rate
self.gamma = GAMMA # Discount factor
self.epsilon = 1.0
实现 Q_Learner 类的 discretize 方法
让我们花点时间理解一下我们是如何离散化观察空间的。最简单且有效的离散化观察空间(以及一般度量空间)的方法是将值的范围划分为一个有限的集合,称为“箱子”(bins)。值的跨度/范围是由空间中每个维度的最大可能值与最小可能值之间的差异给出的。一旦我们计算出跨度,就可以将其除以我们决定的NUM_DISCRETE_BINS,从而得到箱子的宽度。我们在__init__函数中计算了箱子的宽度,因为它在每次新的观察中不会改变。discretize(self, obs)函数接收每个新的观察值并应用离散化步骤,以确定观察值在离散化空间中属于哪个箱子。做法其实很简单:
(obs - self.obs_low) / self.bin_width)
我们希望它属于任何一个箱子(而不是在两个箱子之间);因此,我们将前面的代码转换为一个整数:
((obs - self.obs_low) / self.bin_width).astype(int)
最后,我们将这个离散化的观察值作为一个元组返回。所有这些操作都可以用一行 Python 代码表示,如下所示:
def discretize(self, obs):
return tuple(((obs - self.obs_low) / self.bin_width).astype(int))
实现 Q_Learner 类的 get_action 方法
我们希望智能体根据观察值采取行动。get_action(self, obs)是我们定义的函数,用来根据obs中的观察值生成一个行动。最常用的行动选择策略是 epsilon-greedy 策略,它以(高)概率1-选择智能体估计的最佳行动!,并以由 epsilon 给定的(小)概率选择随机行动!。我们使用 NumPy 的 random 模块中的random()方法来实现 epsilon-greedy 策略,像这样:
def get_action(self, obs):
discretized_obs = self.discretize(obs)
# Epsilon-Greedy action selection
if self.epsilon > EPSILON_MIN:
self.epsilon -= EPSILON_DECAY
if np.random.random() > self.epsilon:
return np.argmax(self.Q[discretized_obs])
else: # Choose a random action
return np.random.choice([a for a in range(self.action_shape)])
实现 Q_Learner 类的 learn 方法
正如你可能猜到的,这就是Q_Learner类中最重要的方法,它实现了学习 Q 值的魔法,从而使得智能体能够随着时间的推移采取智能的行动!最棒的部分是,它的实现并不像看起来那样复杂!它仅仅是我们之前看到的 Q 学习更新方程的实现。你不相信我说它很简单吗?!好吧,这是学习函数的实现:
def learn(self, obs, action, reward, next_obs):
discretized_obs = self.discretize(obs)
discretized_next_obs = self.discretize(next_obs)
td_target = reward + self.gamma * np.max(self.Q[discretized_next_obs])
td_error = td_target - self.Q[discretized_obs][action]
self.Q[discretized_obs][action] += self.alpha * td_error
现在你同意了吗? 😃
我们本可以将 Q 学习更新规则写成一行代码,像这样:
self.Q[discretized_obs][action] += self.alpha * (reward + self.gamma * np.max(self.Q[discretized_next_obs] - self.Q[discretized_obs][action]
但是,将每个项分别写在单独的行中会让它更易于阅读和理解。
完整的 Q_Learner 类实现
如果我们将所有方法的实现放在一起,就会得到一个看起来像这样的代码片段:
EPSILON_MIN = 0.005
max_num_steps = MAX_NUM_EPISODES * STEPS_PER_EPISODE
EPSILON_DECAY = 500 * EPSILON_MIN / max_num_steps
ALPHA = 0.05 # Learning rate
GAMMA = 0.98 # Discount factor
NUM_DISCRETE_BINS = 30 # Number of bins to Discretize each observation dim
class Q_Learner(object):
def __init__(self, env):
self.obs_shape = env.observation_space.shape
self.obs_high = env.observation_space.high
self.obs_low = env.observation_space.low
self.obs_bins = NUM_DISCRETE_BINS # Number of bins to Discretize each observation dim
self.bin_width = (self.obs_high - self.obs_low) / self.obs_bins
self.action_shape = env.action_space.n
# Create a multi-dimensional array (aka. Table) to represent the
# Q-values
self.Q = np.zeros((self.obs_bins + 1, self.obs_bins + 1,
self.action_shape)) # (51 x 51 x 3)
self.alpha = ALPHA # Learning rate
self.gamma = GAMMA # Discount factor
self.epsilon = 1.0
def discretize(self, obs):
return tuple(((obs - self.obs_low) / self.bin_width).astype(int))
def get_action(self, obs):
discretized_obs = self.discretize(obs)
# Epsilon-Greedy action selection
if self.epsilon > EPSILON_MIN:
self.epsilon -= EPSILON_DECAY
if np.random.random() > self.epsilon:
return np.argmax(self.Q[discretized_obs])
else: # Choose a random action
return np.random.choice([a for a in range(self.action_shape)])
def learn(self, obs, action, reward, next_obs):
discretized_obs = self.discretize(obs)
discretized_next_obs = self.discretize(next_obs)
td_target = reward + self.gamma * np.max(self.Q[discretized_next_obs])
td_error = td_target - self.Q[discretized_obs][action]
self.Q[discretized_obs][action] += self.alpha * td_error
所以,我们已经准备好智能体了。你可能会问,接下来我们该做什么呢?好吧,我们应该在 Gym 环境中训练智能体!在下一部分,我们将看看训练过程。
在 Gym 中训练强化学习智能体
训练 Q-learning 智能体的过程你可能已经很熟悉了,因为它有很多与我们之前使用的模板代码相同的行,并且结构也相似。我们不再从环境的动作空间中随机选择动作,而是通过agent.get_action(obs)方法从智能体中获取动作。在将智能体的动作发送到环境并收到反馈后,我们还会调用agent.learn(obs, action, reward, next_obs)方法。训练函数如下所示:
def train(agent, env):
best_reward = -float('inf')
for episode in range(MAX_NUM_EPISODES):
done = False
obs = env.reset()
total_reward = 0.0
while not done:
action = agent.get_action(obs)
next_obs, reward, done, info = env.step(action)
agent.learn(obs, action, reward, next_obs)
obs = next_obs
total_reward += reward
if total_reward > best_reward:
best_reward = total_reward
print("Episode#:{} reward:{} best_reward:{} eps:{}".format(episode,
total_reward, best_reward, agent.epsilon))
# Return the trained policy
return np.argmax(agent.Q, axis=2)
测试并记录智能体的表现
一旦我们让智能体在 Gym 中进行训练,我们就希望能够衡量它的学习效果。为此,我们让智能体进行测试。就像在学校一样!test(agent, env, policy)接受智能体对象、环境实例和智能体的策略,以测试智能体在环境中的表现,并返回一个完整回合的总奖励。它类似于我们之前看到的train(agent, env)函数,但不会让智能体学习或更新它的 Q 值估计:
def test(agent, env, policy):
done = False
obs = env.reset()
total_reward = 0.0
while not done:
action = policy[agent.discretize(obs)]
next_obs, reward, done, info = env.step(action)
obs = next_obs
total_reward += reward
return total_reward
1,000 episodes and save the recorded agent's action in the environment as video files in the gym_monitor_path directory:
if __name__ == "__main__":
env = gym.make('MountainCar-v0')
agent = Q_Learner(env)
learned_policy = train(agent, env)
# Use the Gym Monitor wrapper to evalaute the agent and record video
gym_monitor_path = "./gym_monitor_output"
env = gym.wrappers.Monitor(env, gym_monitor_path, force=True)
for _ in range(1000):
test(agent, env, learned_policy)
env.close()
一个简单而完整的 Q-Learner 实现,用于解决 Mountain Car 问题
在这一部分,我们将整个代码整合到一个单独的 Python 脚本中,初始化环境,启动智能体的训练过程,获得训练后的策略,测试智能体的表现,并记录它在环境中的行为!
#!/usr/bin/env/ python
import gym
import numpy as np
MAX_NUM_EPISODES = 50000
STEPS_PER_EPISODE = 200 # This is specific to MountainCar. May change with env
EPSILON_MIN = 0.005
max_num_steps = MAX_NUM_EPISODES * STEPS_PER_EPISODE
EPSILON_DECAY = 500 * EPSILON_MIN / max_num_steps
ALPHA = 0.05 # Learning rate
GAMMA = 0.98 # Discount factor
NUM_DISCRETE_BINS = 30 # Number of bins to Discretize each observation dim
class Q_Learner(object):
def __init__(self, env):
self.obs_shape = env.observation_space.shape
self.obs_high = env.observation_space.high
self.obs_low = env.observation_space.low
self.obs_bins = NUM_DISCRETE_BINS # Number of bins to Discretize each observation dim
self.bin_width = (self.obs_high - self.obs_low) / self.obs_bins
self.action_shape = env.action_space.n
# Create a multi-dimensional array (aka. Table) to represent the
# Q-values
self.Q = np.zeros((self.obs_bins + 1, self.obs_bins + 1,
self.action_shape)) # (51 x 51 x 3)
self.alpha = ALPHA # Learning rate
self.gamma = GAMMA # Discount factor
self.epsilon = 1.0
def discretize(self, obs):
return tuple(((obs - self.obs_low) / self.bin_width).astype(int))
def get_action(self, obs):
discretized_obs = self.discretize(obs)
# Epsilon-Greedy action selection
if self.epsilon > EPSILON_MIN:
self.epsilon -= EPSILON_DECAY
if np.random.random() > self.epsilon:
return np.argmax(self.Q[discretized_obs])
else: # Choose a random action
return np.random.choice([a for a in range(self.action_shape)])
def learn(self, obs, action, reward, next_obs):
discretized_obs = self.discretize(obs)
discretized_next_obs = self.discretize(next_obs)
td_target = reward + self.gamma * np.max(self.Q[discretized_next_obs])
td_error = td_target - self.Q[discretized_obs][action]
self.Q[discretized_obs][action] += self.alpha * td_error
def train(agent, env):
best_reward = -float('inf')
for episode in range(MAX_NUM_EPISODES):
done = False
obs = env.reset()
total_reward = 0.0
while not done:
action = agent.get_action(obs)
next_obs, reward, done, info = env.step(action)
agent.learn(obs, action, reward, next_obs)
obs = next_obs
total_reward += reward
if total_reward > best_reward:
best_reward = total_reward
print("Episode#:{} reward:{} best_reward:{} eps:{}".format(episode,
total_reward, best_reward, agent.epsilon))
# Return the trained policy
return np.argmax(agent.Q, axis=2)
def test(agent, env, policy):
done = False
obs = env.reset()
total_reward = 0.0
while not done:
action = policy[agent.discretize(obs)]
next_obs, reward, done, info = env.step(action)
obs = next_obs
total_reward += reward
return total_reward
if __name__ == "__main__":
env = gym.make('MountainCar-v0')
agent = Q_Learner(env)
learned_policy = train(agent, env)
# Use the Gym Monitor wrapper to evalaute the agent and record video
gym_monitor_path = "./gym_monitor_output"
env = gym.wrappers.Monitor(env, gym_monitor_path, force=True)
for _ in range(1000):
test(agent, env, learned_policy)
env.close()
该脚本可在代码仓库的ch5文件夹中找到,名为Q_learner_MountainCar.py。
激活rl_gym_book的 conda 环境并启动脚本,查看其运行效果!当你启动脚本时,你将看到类似于这张截图的初始输出:

在初始训练过程中,当智能体刚开始学习时,你会看到它总是以-200的奖励结束。从你对 Gym 的 Mountain Car 环境的理解来看,你可以看出智能体在200个时间步内没有到达山顶,因此环境会自动重置;因此,智能体只得到-200。你还可以观察到
(eps)探索值缓慢衰减。
如果你让智能体学习足够长的时间,你会看到它逐步提高,并且学会在越来越少的步骤内到达山顶。这是它在典型笔记本电脑硬件上训练5分钟后的进展示例:

一旦脚本运行完成,你将看到记录的视频(以及一些.stats.json和.meta.json文件),它们位于gym_monitor_output文件夹中。你可以观看这些视频,看看你的智能体表现如何!
下面是一张截图,展示了智能体成功地将汽车引导到山顶:

万岁!
总结
在这一章中我们学到了很多内容。更重要的是,我们实现了一个智能体,它在大约 7 分钟内学会了聪明地解决 Mountain Car 问题!
我们首先理解了著名的 Mountain Car 问题,并查看了 Gym 中MountainCar-v0环境的设计,包括环境、观察空间、状态空间和奖励的设计。我们重新审视了上一章中使用的强化学习 Gym 模板代码,并对其进行了改进,这些改进也已在本书的代码库中提供。
然后,我们为 Q 学习智能体定义了超参数,并开始从零实现 Q 学习算法。我们首先实现了智能体的初始化函数,初始化智能体的内部状态变量,包括使用 NumPy 的n维数组表示 Q 值。接着,我们实现了discretize方法,将状态空间离散化;get_action(...)方法根据 epsilon 贪婪策略选择一个动作;最后,我们实现了learn(...)函数,它实现了 Q 学习的更新规则,并构成了智能体的核心。我们看到实现这些功能是多么简单!我们还实现了用于训练、测试和评估智能体表现的函数。
我希望你在实现智能体并观看它解决 Gym 中的 Mountain Car 问题时玩得很开心!在下一章,我们将深入探讨一些高级方法,以解决更多具有挑战性的问题。
第六章:使用深度 Q-learning 实现智能代理进行最优控制
在上一章中,我们实现了一个使用 Q-learning 的智能代理,该代理在约七分钟内使用双核笔记本 CPU 从零开始解决了 Mountain Car 问题。在这一章中,我们将实现 Q-learning 的一个高级版本,叫做深度 Q-learning,它可以用来解决比 Mountain Car 问题复杂得多的多个离散控制问题。离散控制问题是(顺序)决策问题,在这些问题中,动作空间被离散化为有限的值。在上一章中,学习代理使用了一个二维的状态空间向量作为输入,这个向量包含了小车的位置和速度信息,以便采取最佳控制行动。在这一章中,我们将看到如何实现一个学习代理,该代理以(屏幕上的)视觉图像作为输入,并学习采取最佳控制行动。这与我们解决问题的方法非常接近,不是吗?我们人类不会计算物体的位置和速度来决定接下来做什么。我们只是观察发生了什么,然后学习采取随时间改进的行动,最终完全解决问题。
本章将指导你如何通过逐步改进我们的 Q-learning 代理实现,使用最近发布的稳定 Q-learning 深度神经网络函数近似方法,逐步构建一个更好的代理。在本章结束时,你将学会如何实现并训练一个深度 Q-learning 代理,该代理通过观察屏幕上的像素并使用 Atari Gym 环境玩 Atari 游戏,获得相当不错的分数!我们还将讨论如何在学习过程中可视化和比较代理的表现。你将看到相同的代理算法如何在多个不同的 Atari 游戏上进行训练,并且该代理仍然能够学会很好地玩这些游戏。如果你迫不及待地想看到实际效果,或者你喜欢在深入之前先大致了解你将开发的内容,可以查看本章代码仓库中 ch6 文件夹下的代码,并在多个 Atari 游戏上尝试预训练的代理!有关如何运行预训练代理的说明,可以在 ch6/README.md 文件中找到。
本章包含大量的技术细节,旨在为你提供足够的背景和知识,使你能够理解逐步改进基本 Q-learning 算法的过程,并基于深度 Q-learning 构建一个更强大、更智能的代理,同时提供多个训练和测试代理所需的模块和工具。以下是本章将涵盖的高级主题的概述:
-
改进 Q-learning 代理的各种方法,包括以下内容:
-
神经网络近似动作-价值函数
-
经验回放
-
探索调度
-
-
使用 PyTorch 实现深度卷积神经网络进行动作-价值函数逼近
-
使用目标网络稳定深度 Q 网络
-
使用 TensorBoard 记录和监控 PyTorch 代理的学习性能
-
管理参数和配置
-
Atari Gym 环境
-
训练深度 Q 学习者玩 Atari 游戏
让我们从第一个主题开始,看看如何从上一章的内容接续开始,继续朝着更强大、更智能的代理迈进。
改进 Q 学习代理
在上一章中,我们回顾了 Q 学习算法,并实现了 Q_Learner 类。对于 Mountain car 环境,我们使用了形状为 51x51x3 的多维数组来表示动作-价值函数,
。请注意,我们已经将状态空间离散化为固定数量的区间,这个数量由 NUM_DISCRETE_BINS 配置参数给出(我们使用了 50)。我们本质上是对观察进行了量化或近似,用低维离散表示来减少 n 维数组中可能的元素数量。通过对观察/状态空间的这种离散化,我们将小车的可能位置限制为 50 个固定位置,速度也限制为 50 个固定值。任何其他位置或速度值都会被近似为这些固定值之一。因此,代理可能会收到相同的位置值,即使小车实际上位于不同的位置。对于某些环境,这可能是一个问题。代理可能无法学会区分从悬崖掉下去和仅站在悬崖边缘以便向前跳跃。在下一节中,我们将研究如何使用更强大的函数逼近器来表示动作-价值函数,而不是使用简单的 n 维数组/表格,因为它有一定的局限性。
使用神经网络逼近 Q 函数
神经网络被证明是有效的通用函数逼近器。事实上,有一个通用逼近定理,表明一个单隐层前馈神经网络可以逼近任何闭合且有界的连续函数
。这基本上意味着即使是简单(浅层)神经网络也可以逼近多个函数。难道不觉得用一个固定数量的权重/参数的简单神经网络来逼近几乎任何函数,听起来太好了不真实吗?但这确实是真的,唯一需要注意的地方是,它并不能在任何地方和所有地方都能实际应用。虽然单隐层神经网络可以用有限的参数集逼近任何函数,但我们并没有一种普遍保证的学习这些参数的方法,以便最好地表示任何函数。你会看到,研究人员已经能够使用神经网络来逼近多个复杂且有用的函数。如今,几乎所有智能手机内置的智能功能都由(经过大量优化的)神经网络驱动。许多表现最好的系统,比如自动根据人物、地点和照片中的上下文将照片分类的系统,识别你面部和声音的系统,或者为你自动撰写电子邮件回复的系统,都是由神经网络提供动力的。即使是生成类似人类真实语音的最先进技术,比如你在 Google Assistant 等语音助手中听到的声音,也是由神经网络驱动的。
Google Assistant 当前使用由 Deepmind 开发的 WaveNet 和 WaveNet2 进行 文本到语音 (TTS) 合成,研究表明,这比目前开发的任何其他 TTS 系统都更加逼真。
我希望这足以激励你使用神经网络来逼近 Q 函数!在本节中,我们将从用一个浅层(非深层)单隐层神经网络来逼近 Q 函数开始,并利用它来解决著名的倒立摆问题。尽管神经网络是强大的函数逼近器,我们将看到,即便是单隐层神经网络,训练它来逼近强化学习问题中的 Q 函数也并非易事。我们将探讨一些利用神经网络逼近改进 Q 学习的方法,并且在本章的后续部分,我们将研究如何使用具有更强表示能力的深度神经网络来逼近 Q 函数。
让我们通过重新审视前一章中实现的 Q_Learner 类的 __init__(...) 方法来开始神经网络逼近:
class Q_Learner(object):
def __init__(self, env):
self.obs_shape = env.observation_space.shape
self.obs_high = env.observation_space.high
self.obs_low = env.observation_space.low
self.obs_bins = NUM_DISCRETE_BINS # Number of bins to Discretize each observation dim
self.bin_width = (self.obs_high - self.obs_low) / self.obs_bins
self.action_shape = env.action_space.n
# Create a multi-dimensional array (aka. Table) to represent the
# Q-values
self.Q = np.zeros((self.obs_bins + 1, self.obs_bins + 1,
self.action_shape)) # (51 x 51 x 3)
self.alpha = ALPHA # Learning rate
self.gamma = GAMMA # Discount factor
self.epsilon = 1.0
在前面的代码中,粗体字体的那一行是我们将 Q 函数初始化为一个多维的 NumPy 数组。在接下来的部分,我们将看到如何将 NumPy 数组表示替换为更强大的神经网络表示。
使用 PyTorch 实现一个浅层 Q 网络
在这一部分中,我们将开始使用 PyTorch 的神经网络模块实现一个简单的神经网络,然后看看如何使用它来替代基于多维数组的 Q 行为值表函数。
让我们从神经网络的实现开始。以下代码演示了如何使用 PyTorch 实现一个 单层感知机 (SLP):
import torch
class SLP(torch.nn.Module):
"""
A Single Layer Perceptron (SLP) class to approximate functions
"""
def __init__(self, input_shape, output_shape, device=torch.device("cpu")):
"""
:param input_shape: Shape/dimension of the input
:param output_shape: Shape/dimension of the output
:param device: The device (cpu or cuda) that the SLP should use to store the inputs for the forward pass
"""
super(SLP, self).__init__()
self.device = device
self.input_shape = input_shape[0]
self.hidden_shape = 40
self.linear1 = torch.nn.Linear(self.input_shape, self.hidden_shape)
self.out = torch.nn.Linear(self.hidden_shape, output_shape)
def forward(self, x):
x = torch.from_numpy(x).float().to(self.device)
x = torch.nn.functional.relu(self.linear1(x))
x = self.out(x)
return x
SLP 类实现了一个单层神经网络,输入层和输出层之间有 40 个隐藏单元,使用 torch.nn.Linear 类,并使用 修正线性单元 (ReLU 或 relu) 作为激活函数。本书的代码库中提供了这段代码,路径为 ch6/function_approximator/perceptron.py。数字 40 并没有特别的含义,你可以根据需要调整神经网络中的隐藏单元数量。
实现 Shallow_Q_Learner
然后,我们可以修改 Q_Learner 类,使用这个 SLP 来表示 Q 函数。请注意,我们还需要修改 Q_Learner 类中的 learn(...) 方法,以计算损失函数关于 SLP 权重的梯度,并进行反向传播,以更新和优化神经网络的权重,从而改进其 Q 值表示,使其更接近实际值。同时,我们还会稍微修改 get_action(...) 方法,通过神经网络的前向传播来获得 Q 值。以下是带有 Q_Learner 类实现中变化的 Shallow_Q_Learner 类代码,修改部分用 粗体 显示,以便你一眼看出差异:
import torch
from function_approximator.perceptron import SLP EPSILON_MIN = 0.005
max_num_steps = MAX_NUM_EPISODES * STEPS_PER_EPISODE
EPSILON_DECAY = 500 * EPSILON_MIN / max_num_steps
ALPHA = 0.05 # Learning rate
GAMMA = 0.98 # Discount factor
NUM_DISCRETE_BINS = 30 # Number of bins to Discretize each observation dim
class Shallow_Q_Learner(object):
def __init__(self, env):
self.obs_shape = env.observation_space.shape
self.obs_high = env.observation_space.high
self.obs_low = env.observation_space.low
self.obs_bins = NUM_DISCRETE_BINS # Number of bins to Discretize each observation dim
self.bin_width = (self.obs_high - self.obs_low) / self.obs_bins
self.action_shape = env.action_space.n
# Create a multi-dimensional array (aka. Table) to represent the
# Q-values
self.Q = SLP(self.obs_shape, self.action_shape)
self.Q_optimizer = torch.optim.Adam(self.Q.parameters(), lr=1e-5)
self.alpha = ALPHA # Learning rate
self.gamma = GAMMA # Discount factor
self.epsilon = 1.0
def discretize(self, obs):
return tuple(((obs - self.obs_low) / self.bin_width).astype(int))
def get_action(self, obs):
discretized_obs = self.discretize(obs)
# Epsilon-Greedy action selection
if self.epsilon > EPSILON_MIN:
self.epsilon -= EPSILON_DECAY
if np.random.random() > self.epsilon:
return np.argmax(self.Q(discretized_obs).data.to(torch.device('cpu')).numpy())
else: # Choose a random action
return np.random.choice([a for a in range(self.action_shape)])
def learn(self, obs, action, reward, next_obs):
#discretized_obs = self.discretize(obs)
#discretized_next_obs = self.discretize(next_obs)
td_target = reward + self.gamma * torch.max(self.Q(next_obs))
td_error = torch.nn.functional.mse_loss(self.Q(obs)[action], td_target)
#self.Q[discretized_obs][action] += self.alpha * td_error
self.Q_optimizer.zero_grad()
td_error.backward()
self.Q_optimizer.step()
这里讨论了 Shallow_Q_Learner 类的实现,目的是让你更容易理解如何实现基于神经网络的 Q 函数逼近,进而替代传统的表格型 Q 学习实现。
使用 Shallow Q-Network 解决 Cart Pole 问题
在这一部分中,我们将实现一个完整的训练脚本,使用我们在前一部分中开发的 Shallow Q_Learner 类来解决 Cart Pole 问题:
#!/usr/bin/env python import gym import random import torch from torch.autograd import Variable import numpy as np from utils.decay_schedule import LinearDecaySchedule from function_approximator.perceptron import SLP
env = gym.make("CartPole-v0")
MAX_NUM_EPISODES = 100000
MAX_STEPS_PER_EPISODE = 300
class Shallow_Q_Learner(object):
def __init__(self, state_shape, action_shape, learning_rate=0.005,
gamma=0.98):
self.state_shape = state_shape
self.action_shape = action_shape
self.gamma = gamma # Agent's discount factor
self.learning_rate = learning_rate # Agent's Q-learning rate
# self.Q is the Action-Value function. This agent represents Q using a
# Neural Network.
self.Q = SLP(state_shape, action_shape)
self.Q_optimizer = torch.optim.Adam(self.Q.parameters(), lr=1e-3)
# self.policy is the policy followed by the agent. This agents follows
# an epsilon-greedy policy w.r.t it's Q estimate.
self.policy = self.epsilon_greedy_Q
self.epsilon_max = 1.0
self.epsilon_min = 0.05
self.epsilon_decay = LinearDecaySchedule(initial_value=self.epsilon_max,
final_value=self.epsilon_min,
max_steps= 0.5 * MAX_NUM_EPISODES * MAX_STEPS_PER_EPISODE)
self.step_num = 0
def get_action(self, observation):
return self.policy(observation)
def epsilon_greedy_Q(self, observation):
# Decay Epsilion/exploratin as per schedule
if random.random() < self.epsilon_decay(self.step_num):
action = random.choice([i for i in range(self.action_shape)])
else:
action = np.argmax(self.Q(observation).data.numpy())
return action
def learn(self, s, a, r, s_next):
td_target = r + self.gamma * torch.max(self.Q(s_next))
td_error = torch.nn.functional.mse_loss(self.Q(s)[a], td_target)
# Update Q estimate
#self.Q(s)[a] = self.Q(s)[a] + self.learning_rate * td_error
self.Q_optimizer.zero_grad()
td_error.backward()
self.Q_optimizer.step()
if __name__ == "__main__":
observation_shape = env.observation_space.shape
action_shape = env.action_space.n
agent = Shallow_Q_Learner(observation_shape, action_shape)
first_episode = True
episode_rewards = list()
for episode in range(MAX_NUM_EPISODES):
obs = env.reset()
cum_reward = 0.0 # Cumulative reward
for step in range(MAX_STEPS_PER_EPISODE):
# env.render()
action = agent.get_action(obs)
next_obs, reward, done, info = env.step(action)
agent.learn(obs, action, reward, next_obs)
obs = next_obs
cum_reward += reward
if done is True:
if first_episode: # Initialize max_reward at the end of first episode
max_reward = cum_reward
first_episode = False
episode_rewards.append(cum_reward)
if cum_reward > max_reward:
max_reward = cum_reward
print("\nEpisode#{} ended in {} steps. reward ={} ; mean_reward={} best_reward={}".
format(episode, step+1, cum_reward, np.mean(episode_rewards), max_reward))
break
env.close()
在 ch6 文件夹中创建一个名为 shallow_Q_Learner.py 的脚本,并像下面这样运行:
(rl_gym_book) praveen@ubuntu:~/rl_gym_book/ch6$ python shallow_Q_Learner.py
你将看到代理在 Gym 的 CartPole-v0 环境中学习如何平衡杠杆。你应该能够看到控制台打印出的以下信息:当前回合数、代理在回合结束前所采取的步数、代理所获得的回合奖励、代理所获得的平均回合奖励,以及代理至今所获得的最佳回合奖励。如果你想直观地看到 Cart Pole 环境,并了解代理如何尝试学习并保持平衡,你可以取消注释 env.render() 这一行代码。
Shallow_Q_Learner类的实现和完整的训练脚本展示了如何使用简单的神经网络来逼近 Q 函数。这并不是解决复杂游戏(如 Atari)的一个好实现。在接下来的几个部分中,我们将使用新的技术系统地改进它们的性能。我们还将实现一个深度卷积 Q 网络,该网络可以将原始的屏幕图像作为输入,并预测智能体可以用来玩各种 Atari 游戏的 Q 值。
你可能会注意到,智能体需要很长时间才能改善并最终能够解决问题。在下一部分中,我们将实现经验回放的概念来提升性能。
经验回放
在大多数环境中,智能体接收到的信息并不是独立同分布(i.i.d)。这意味着智能体接收到的观察值与它之前接收到的观察值以及它将来接收到的观察值之间存在较强的相关性。这个现象是可以理解的,因为通常,智能体在典型的强化学习环境中所解决的问题是顺序性的。研究表明,如果样本是 i.i.d,神经网络的收敛效果会更好。
经验回放还使得智能体能够重用过去的经验。神经网络更新,尤其是在较低学习率的情况下,需要多次反向传播和优化步骤才能收敛到好的值。重用过去的经验数据,尤其是通过小批量来更新神经网络,极大地帮助了 Q 网络的收敛,Q 网络的结果接近真实的动作值。
实现经验记忆
让我们实现一个经验记忆类,用于存储智能体收集的经验。在此之前,让我们先明确一下我们所说的经验是什么意思。在强化学习中,问题通常通过马尔可夫决策过程(MDP)来表示,正如我们在第二章《强化学习与深度强化学习》中所讨论的那样,表示一次经验为一个数据结构,其中包含了在时间步t的观察值、根据该观察值采取的行动、为该行动获得的奖励,以及由于智能体的行动而导致环境过渡到的下一个观察值(或状态)。包括“done”布尔值,表示该次观察是否标志着一轮实验的结束也是有用的。我们可以使用 Python 的namedtuple(来自 collections 库)来表示这种数据结构,如下方代码片段所示:
from collections import namedtuple
Experience = namedtuple("Experience", ['obs', 'action', 'reward', 'next_obs',
'done'])
namedtuple数据结构使得使用名称属性(如'obs'、'action'等)而不是数字索引(如 0、1 等)来访问元素变得更加方便。
现在我们可以继续使用刚刚创建的经验数据结构来实现经验记忆类。为了弄清楚我们需要在经验记忆类中实现哪些方法,让我们考虑一下以后如何使用它。
首先,我们希望能够存储智能体收集的新经验到经验记忆中。然后,当我们想要回放并更新 Q 函数时,我们希望能从经验记忆中按批次采样或提取经验。因此,基本上,我们将需要一个方法来存储新经验,以及一个可以采样单个或批量经验的方法。
让我们深入了解经验记忆的实现,从初始化方法开始,我们用所需的容量初始化内存,如下所示:
class ExperienceMemory(object):
"""
A cyclic/ring buffer based Experience Memory implementation
"""
def __init__(self, capacity=int(1e6)):
"""
:param capacity: Total capacity (Max number of Experiences)
:return:
"""
self.capacity = capacity
self.mem_idx = 0 # Index of the current experience
self.memory = []
mem_idx成员变量将用于指向当前的写入头或索引位置,我们将在该位置存储新到达的经验。
“循环缓冲区”也有其他名字,你可能听过:“环形缓冲区”、“环形队列”和“循环队列”。它们都代表相同的底层数据结构,使用类似环形的固定大小数据表示。
接下来,我们将查看store方法的实现:
def store(self, experience):
"""
:param experience: The Experience object to be stored into the memory
:return:
"""
self.memory.insert(self.mem_idx % self.capacity, experience)
self.mem_idx += 1
足够简单,对吧?我们正在存储经验到mem_idx,正如我们所讨论的那样。
接下来的代码是我们sample方法的实现:
import random
def sample(self, batch_size):
"""
:param batch_size: Sample batch_size
:return: A list of batch_size number of Experiences sampled at random from mem
"""
assert batch_size <= len(self.memory), "Sample batch_size is more than available exp in mem"
return random.sample(self.memory, batch_size)
在前面的代码中,我们利用 Python 的 random 库从经验记忆中均匀地随机采样经验。我们还将实现一个简单的get_size辅助方法,用于查找经验记忆中已经存储了多少经验:
def get_size(self):
"""
:return: Number of Experiences stored in the memory
"""
return len(self.memory)
经验记忆类的完整实现可以在ch6/utils/experience_memory.py中找到,位于本书的代码仓库中。
接下来,我们将看看如何从经验记忆中回放采样的经验,以更新智能体的 Q 函数。
实现 Q-learner 类的回放经验方法
因此,我们已经为智能体实现了一个内存系统,使用整洁的循环缓冲区来存储它的过去经验。在本节中,我们将探讨如何在 Q-learner 类中使用经验记忆来回放经验。
replay_experience method that shows how we sample from the experience memory and call a soon-to-be-implemented method that lets the agent learn from the sampled batch of experiences:
def replay_experience(self, batch_size=REPLAY_BATCH_SIZE):
"""
Replays a mini-batch of experience sampled from the Experience Memory
:param batch_size: mini-batch size to sample from the Experience Memory
:return: None
"""
experience_batch = self.memory.sample(batch_size)
self.learn_from_batch_experience(experience_batch)
对于像 SARSA 这样的在线学习方法,行动值估计在代理与环境交互的每一步之后都会更新。这种方式使得更新传播了代理刚刚经历的信息。如果代理不经常经历某些事物,这样的更新可能会导致代理忘记这些经历,当代理在未来遇到类似情况时可能表现不佳。这是不可取的,特别是对于具有许多参数(或权重)需要调整到正确值的神经网络来说。这是使用经验记忆并在更新 Q 行动值估计时重播过去经验的主要动机之一。我们现在将实现learn_from_batch_experience方法,扩展我们在上一章中实现的learn方法,以从一批经验中学习,而不是从单个经验中学习。以下是该方法的实现:
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
def learn_from_batch_experience(self, experiences):
"""
Updated the DQN based on the learning from a mini-batch of experience.
:param experiences: A mini-batch of experience
:return: None
"""
batch_xp = Experience(*zip(*experiences))
obs_batch = np.array(batch_xp.obs)
action_batch = np.array(batch_xp.action)
reward_batch = np.array(batch_xp.reward)
next_obs_batch = np.array(batch_xp.next_obs)
done_batch = np.array(batch_xp.done)
td_target = reward_batch + ~done_batch * \
np.tile(self.gamma, len(next_obs_batch)) * \
self.Q(next_obs_batch).detach().max(1)[0].data
td_target = td_target.to(device)
action_idx = torch.from_numpy(action_batch).to(device)
td_error = torch.nn.functional.mse_loss(
self.Q(obs_batch).gather(1, action_idx.view(-1, 1)),
td_target.float().unsqueeze(1))
self.Q_optimizer.zero_grad()
td_error.mean().backward()
self.Q_optimizer.step()
该方法接收一批(或小批量)经验,并首先分别提取观察批次、动作批次、奖励批次和下一个观察批次,以便在随后的步骤中单独使用它们。done_batch表示每个经验的下一个观察是否是一集的结束。然后,我们计算最大化动作的时间差分(TD)误差,这是 Q 学习的目标。请注意,在td_target计算中,我们将第二项乘以~done_batch。
这负责为终止状态指定零值。因此,如果next_obs_batch中的特定next_obs是终止状态,则第二项将变为 0,结果仅为td_target = rewards_batch。
然后计算td_target(目标 Q 值)与 Q 网络预测的 Q 值之间的均方误差。我们将此误差作为指导信号,并在进行优化步骤之前将其反向传播到神经网络中的所有节点,以更新参数/权重以最小化误差。
重新审视ε-greedy 行动策略
在上一章,我们讨论了贪心行动选择策略,它根据智能体的行动-价值估计,以 1-的概率采取最佳行动, 以给定的 epsilon 概率采取随机行动。epsilon 是一个可以根据实验调节的超参数。较高的 epsilon 值意味着智能体的行为将更加随机,而较低的 epsilon 值则意味着智能体更可能利用它已知的环境信息而不会尝试探索。我的目标是通过采取从未尝试或较少尝试的行动来进行更多探索,还是通过采取我已知的最佳行动来进行利用?这是强化学习智能体面临的探索-利用困境。
直观上,在智能体学习过程的初期,保持一个很高的值(最大为 1.0)对于 epsilon 是有帮助的,这样智能体可以通过大多数随机行动来探索状态空间。一旦它积累了足够的经验并对环境有了更好的理解,降低 epsilon 值将使智能体更常基于它认为的最佳行动来采取行动。我们需要一个工具函数来处理 epsilon 值的变化,对吧?让我们在下一节实现这样的函数。
实现 epsilon 衰减调度
我们可以线性衰减(或减少)epsilon 值(如下左侧图表),也可以采用指数衰减(如下右侧图表)或其他衰减方案。线性和指数衰减是探索参数 epsilon 最常用的衰减调度:

在前面的图表中,你可以看到 epsilon(探索)值如何随着不同的调度方案变化(左图为线性,右图为指数)。前面图表中显示的衰减调度在线性情况下使用了 epsilon_max(初始值)为 1,epsilon_min(最终值)为 0.01,在指数情况下使用了 exp(-10000/2000),在经过 10,000 个回合后,它们都保持一个常数值的 epsilon_min。
以下代码实现了LinearDecaySchedule,我们将在Deep_Q_Learning智能体的实现中使用它来玩 Atari 游戏:
#!/usr/bin/env python
class LinearDecaySchedule(object):
def __init__(self, initial_value, final_value, max_steps):
assert initial_value > final_value, "initial_value should be < final_value"
self.initial_value = initial_value
self.final_value = final_value
self.decay_factor = (initial_value - final_value) / max_steps
def __call__(self, step_num):
current_value = self.initial_value - self.decay_factor * step_num
if current_value < self.final_value:
current_value = self.final_value
return current_value
if __name__ == "__main__":
import matplotlib.pyplot as plt
epsilon_initial = 1.0
epsilon_final = 0.05
MAX_NUM_EPISODES = 10000
MAX_STEPS_PER_EPISODE = 300
linear_sched = LinearDecaySchedule(initial_value = epsilon_initial,
final_value = epsilon_final,
max_steps = MAX_NUM_EPISODES * MAX_STEPS_PER_EPISODE)
epsilon = [linear_sched(step) for step in range(MAX_NUM_EPISODES * MAX_STEPS_PER_EPISODE)]
plt.plot(epsilon)
plt.show()
上述脚本可以在本书的代码仓库中的 ch6/utils/decay_schedule.py 找到。如果运行该脚本,您将看到 main 函数为 epsilon 创建一个线性衰减计划并绘制该值。您可以尝试不同的 MAX_NUM_EPISODES、MAX_STEPS_PER_EPISODE、epsilon_initial 和 epsilon_final 值,以直观地查看 epsilon 值如何随步数变化。在下一节中,我们将实现 get_action(...) 方法,它实现了
- 贪婪动作选择策略。
实现深度 Q 学习智能体
在本节中,我们将讨论如何将我们的浅层 Q 学习器扩展为更复杂、更强大的深度 Q 学习器基础智能体,这样它可以基于原始视觉图像输入来学习行动,我们将在本章末尾用来训练能够玩 Atari 游戏的智能体。请注意,您可以在任何具有离散动作空间的学习环境中训练这个深度 Q 学习智能体。Atari 游戏环境就是我们将在本书中使用的一个有趣的环境类别。
我们将从一个深度卷积 Q 网络实现开始,并将其整合到我们的 Q 学习器中。接着,我们将看到如何使用目标 Q 网络技术来提高深度 Q 学习器的稳定性。然后,我们将结合目前为止讨论的所有技术,完整实现我们的深度 Q 学习基础智能体。
在 PyTorch 中实现深度卷积 Q 网络
让我们实现一个 3 层深度 卷积神经网络 (CNN),该网络将 Atari 游戏的屏幕像素作为输入,输出每个可能动作的动作值,这些动作是在 OpenAI Gym 环境中定义的。以下是 CNN 类的代码:
import torch
class CNN(torch.nn.Module):
"""
A Convolution Neural Network (CNN) class to approximate functions with visual/image inputs
"""
def __init__(self, input_shape, output_shape, device="cpu"):
"""
:param input_shape: Shape/dimension of the input image. Assumed to be resized to C x 84 x 84
:param output_shape: Shape/dimension of the output.
:param device: The device (cpu or cuda) that the CNN should use to store the inputs for the forward pass
"""
# input_shape: C x 84 x 84
super(CNN, self).__init__()
self.device = device
self.layer1 = torch.nn.Sequential(
torch.nn.Conv2d(input_shape[0], 64, kernel_size=4, stride=2, padding=1),
torch.nn.ReLU()
)
self.layer2 = torch.nn.Sequential(
torch.nn.Conv2d(64, 32, kernel_size=4, stride=2, padding=0),
torch.nn.ReLU()
)
self.layer3 = torch.nn.Sequential(
torch.nn.Conv2d(32, 32, kernel_size=3, stride=1, padding=0),
torch.nn.ReLU()
)
self.out = torch.nn.Linear(18 * 18 * 32, output_shape)
def forward(self, x):
x = torch.from_numpy(x).float().to(self.device)
x = self.layer1(x)
x = self.layer2(x)
x = self.layer3(x)
x = x.view(x.shape[0], -1)
x = self.out(x)
return x
如您所见,向神经网络中添加更多层非常简单。我们可以使用一个更深的网络,层数超过三层,但这样做的代价是需要更多的计算能力和时间。在深度强化学习,尤其是 Q 学习与函数近似的情况下,并没有已证明的收敛保证。因此,在通过使用更深的神经网络来增加 Q /值函数表示的能力之前,我们应该确保智能体的实现已经足够好,能够在学习和进步上取得良好效果。
使用目标 Q 网络来稳定智能体的学习
冻结 Q 网络固定步数,并利用该网络生成 Q 学习目标来更新深度 Q 网络的参数,这一简单技术已被证明在减少振荡和稳定 Q 学习与神经网络近似方面非常有效。这个技术相对简单,但实际上对于稳定学习非常有帮助。
实现将非常直接和简单。我们需要对现有的深度 Q 学习器类进行两处修改或更新:
-
创建目标 Q 网络并定期与原始 Q 网络同步/更新
-
使用目标 Q 网络生成 Q 学习目标。
为了比较代理在使用和不使用目标 Q 网络时的表现,你可以使用我们在本章前面部分开发的参数管理器、日志记录和可视化工具,来直观地验证启用目标 Q 网络后性能的提升。
self.DQN in the deep_Q_learner.py script:
self.Q = self.DQN(state_shape, action_shape, device).to(device)
self.Q_optimizer = torch.optim.Adam(self.Q.parameters(), lr=self.learning_rate)
if self.params['use_target_network']:
self.Q_target = self.DQN(state_shape, action_shape, device).to(device)
然后我们可以修改之前实现的 learn_from_batch_experience 方法,使用目标 Q 网络来创建 Q 学习目标。以下代码片段显示了我们第一次实现中的更改,已用粗体标出:
def learn_from_batch_experience(self, experiences):
batch_xp = Experience(*zip(*experiences))
obs_batch = np.array(batch_xp.obs)
action_batch = np.array(batch_xp.action)
reward_batch = np.array(batch_xp.reward)
next_obs_batch = np.array(batch_xp.next_obs)
done_batch = np.array(batch_xp.done)
if self.params['use_target_network']:
if self.step_num % self.params['target_network_update_freq'] == 0:
# The *update_freq is the Num steps after which target net is updated.
# A schedule can be used instead to vary the update freq.
self.Q_target.load_state_dict(self.Q.state_dict())
td_target = reward_batch + ~done_batch * \
np.tile(self.gamma, len(next_obs_batch)) * \
self.Q_target(next_obs_batch).max(1)[0].data
else:
td_target = reward_batch + ~done_batch * \
np.tile(self.gamma, len(next_obs_batch)) * \
self.Q(next_obs_batch).detach().max(1)[0].data
td_target = td_target.to(device)
action_idx = torch.from_numpy(action_batch).to(device)
td_error = torch.nn.functional.mse_loss( self.Q(obs_batch).gather(1, action_idx.view(-1, 1)),
td_target.float().unsqueeze(1))
self.Q_optimizer.zero_grad()
td_error.mean().backward()
writer.add_scalar("DQL/td_error", td_error.mean(), self.step_num)
self.Q_optimizer.step()
这完成了我们的目标 Q 网络实现。
我们如何知道代理是否从目标 Q 网络和前面讨论的其他改进中受益?在下一节中,我们将探讨如何记录和可视化代理的表现,从而监控并弄清楚这些改进是否真的导致了更好的表现。
记录和可视化代理的学习过程
现在我们有了一个学习代理,它使用神经网络来学习 Q 值,并更新自身以便在任务中表现得更好。代理需要一段时间来学习,直到它开始做出明智的决策。我们如何知道代理在某一时刻的状态?我们如何知道代理是否在进步,还是只是表现得很笨?我们如何随着时间的推移看到和衡量代理的进展?我们应该只是坐在那里等待训练结束吗?不,应该有更好的方法,不是吗?
是的,确实如此!对我们这些开发代理的人员来说,能够观察代理的表现是很重要的,这样才能发现实现中是否存在问题,或者某些超参数是否太差,导致代理无法学习任何东西。我们已经有了日志记录的初步版本,并通过使用打印语句生成的控制台输出,看到了代理学习的进展情况。这为我们提供了关于回合数、回合奖励、最大奖励等的第一手数据,但它更像是在某一时刻的快照。我们希望能够看到学习过程的历史,看看代理的学习是否在收敛,学习误差是否在减少,等等。这将帮助我们朝着正确的方向思考,更新实现或调整参数,以提高代理的学习表现。
TensorFlow 深度学习库提供了一种名为 TensorBoard 的工具。它是一个强大的工具,可以用来可视化神经网络图、绘制量化指标(如学习误差、奖励等),随着训练的进行,它还可以用来可视化图像以及其他几种有用的数据类型。它使得我们更容易理解、识别和调试我们的深度学习算法实现。在下一节中,我们将看到如何使用 TensorBoard 来记录和可视化代理的进展。
使用 TensorBoard 记录和可视化 PyTorch 强化学习代理的进展
尽管 TensorBoard 是为 TensorFlow 深度学习库发布的工具,但它本身是一个灵活的工具,可以与其他深度学习库(如 PyTorch)一起使用。基本上,TensorBoard 工具读取来自日志文件的 TensorFlow 事件总结数据,并定期更新可视化和图表。幸运的是,我们有一个叫做tensorboardX的库,它提供了一个方便的接口来创建 TensorBoard 可以使用的事件。通过这种方式,我们可以轻松地从代理训练代码中生成适当的事件,以记录和可视化代理学习过程的进展。使用这个库非常简单和直接。我们只需要导入tensorboardX,然后创建一个带有所需日志文件名的SummaryWriter对象。然后,我们可以使用SummaryWriter对象添加新的标量(以及其他支持的数据),以便将新的数据点添加到图表中,并定期更新。以下截图是一个例子,展示了在我们的代理训练脚本中记录的那些信息,TensorBoard 的输出将会是什么样的:

在前面的截图中,右下角的图表标题为main/mean_ep_reward,展示了代理如何在时间步长上逐渐学会获得越来越高的奖励。在前面截图中的所有图表中,x-轴表示训练步骤的数量,y-轴则表示记录的数据的值,具体由每个图表的标题所指示。
现在,我们已经知道如何在训练过程中记录和可视化代理的性能。但是,仍然有一个问题需要解决,那就是我们如何比较包含或不包含本章前面讨论的一个或多个改进的代理。我们讨论了几项改进,每一项都增加了新的超参数。为了管理这些不同的超参数,并方便地启用或禁用这些改进和配置,在下一节中,我们将讨论通过构建一个简单的参数管理类来实现这一目标。
管理超参数和配置参数
正如你可能注意到的,我们的智能体有多个超参数,比如学习率、gamma、epsilon 起始值/最小值等等。还有一些智能体和环境的配置参数,我们希望能够轻松修改并运行,而不是在代码中查找这些参数的定义。拥有一种简单而良好的方法来管理这些参数,也有助于我们在想要自动化训练过程或进行参数扫描等方法时,找到适合智能体的最佳参数集。
在接下来的两个小节中,我们将探讨如何使用 JSON 文件以一种易于使用的方式指定参数和超参数,并实现一个参数管理类来处理这些外部可配置的参数,从而更新智能体和环境的配置。
使用 JSON 文件轻松配置参数
parameters.json file that we will use to configure the parameters of the agent and the environment. The JavaScript Object Notation (JSON) file is a convenient and human-readable format for such data representation. We will discuss what each of these parameters mean in the later sections of this chapter. For now, we will concentrate on how we can use such a file to specify or change the parameters used by the agent and the environment:
{
"agent": {
"max_num_episodes": 70000,
"max_steps_per_episode": 300,
"replay_batch_size": 2000,
"use_target_network": true,
"target_network_update_freq": 2000,
"lr": 5e-3,
"gamma": 0.98,
"epsilon_max": 1.0,
"epsilon_min": 0.05,
"seed": 555,
"use_cuda": true,
"summary_filename_prefix": "logs/DQL_"
},
"env": {
"type": "Atari",
"episodic_life": "True",
"clip_reward": "True",
"useful_region": {
"Default":{
"crop1": 34,
"crop2": 34,
"dimension2": 80
}
}
}
}
参数管理器
你喜欢刚才看到的参数配置文件示例吗?我希望你喜欢。在本节中,我们将实现一个参数管理器,帮助我们根据需要加载、获取和设置这些参数。
我们将从创建一个名为 ParamsManger 的 Python 类开始,该类通过使用 JSON Python 库从 params_file 读取的参数字典来初始化 params 成员变量,如下所示:
#!/usr/bin/env python
import JSON
class ParamsManager(object):
def __init__(self, params_file):
"""
A class to manage the Parameters. Parameters include configuration parameters and Hyper-parameters
:param params_file: Path to the parameters JSON file
"""
self.params = JSON.load(open(params_file, 'r'))
然后,我们将实现一些对我们有用的方法。我们将从get_params方法开始,该方法返回我们从 JSON 文件读取的整个参数字典:
def get_params(self):
"""
Returns all the parameters
:return: The whole parameter dictionary
"""
return self.params
有时候,我们可能只想获取与智能体或环境对应的参数,这些参数可以在初始化智能体或环境时传入。由于我们在上一节中已经将智能体和环境的参数整齐地分开存储在parameters.json文件中,因此实现起来非常直接,如下所示:
def get_env_params(self):
"""
Returns the environment configuration parameters
:return: A dictionary of configuration parameters used for the environment
"""
return self.params['env']
def get_agent_params(self):
"""
Returns the hyper-parameters and configuration parameters used by the agent
:return: A dictionary of parameters used by the agent
"""
return self.params['agent']
我们还将实现另一个简单的方法来更新智能体的参数,这样我们也可以在启动训练脚本时从命令行传入/读取智能体参数:
def update_agent_params(self, **kwargs):
"""
Update the hyper-parameters (and configuration parameters) used by the agent
:param kwargs: Comma-separated, hyper-parameter-key=value pairs. Eg.: lr=0.005, gamma=0.98
:return: None
"""
for key, value in kwargs.items():
if key in self.params['agent'].keys():
self.params['agent'][key] = value
上述参数管理器的实现以及一个简单的测试程序可以在本书的代码库中的 ch6/utils/params_manager.py 找到。在下一节中,我们将整合所有我们已经讨论和实现的技术,打造一个完整的基于深度 Q 学习的智能体。
一个完整的深度 Q 学习者,用于解决复杂问题的原始像素输入
从本章开始,我们已经实现了几种额外的技术和实用工具来改进智能体。在本节中,我们将把迄今为止讨论的所有改进和实用工具整合成一个统一的 deep_Q_Learner.py 脚本。我们将在下一节中使用这个统一的智能体脚本在 Atari Gym 环境中进行训练,并观察智能体如何逐步提升性能,随着时间的推移获取越来越多的分数。
以下代码是我们在本章前面各节中开发的功能的统一版本:
-
经验记忆
-
使用经验重放从(小批量)经验中学习
-
线性 epsilon 衰减计划
-
稳定学习的目标网络
-
使用 JSON 文件进行参数管理
-
使用 TensorBoard 进行性能可视化和日志记录:
#!/usr/bin/env python
import gym
import torch
import random
import numpy as np
import environment.atari as Atari
from utils.params_manager import ParamsManager
from utils.decay_schedule import LinearDecaySchedule
from utils.experience_memory import Experience, ExperienceMemory
from function_approximator.perceptron import SLP
from function_approximator.cnn import CNN
from tensorboardX import SummaryWriter
from datetime import datetime
from argparse import ArgumentParser
args = ArgumentParser("deep_Q_learner")
args.add_argument("--params-file",
help="Path to the parameters JSON file. Default is parameters.JSON",
default="parameters.JSON",
type=str,
metavar="PFILE")
args.add_argument("--env-name",
help="ID of the Atari environment available in OpenAI Gym. Default is Pong-v0",
default="Pong-v0",
type=str,
metavar="ENV")
args = args.parse_args()
params_manager= ParamsManager(args.params_file)
seed = params_manager.get_agent_params()['seed']
summary_file_path_prefix = params_manager.get_agent_params()['summary_file_path_prefix']
summary_file_name = summary_file_path_prefix + args.env_name + "_" + datetime.now().strftime("%y-%m-%d-%H-%M")
writer = SummaryWriter(summary_file_name)
global_step_num = 0
use_cuda = params_manager.get_agent_params()['use_cuda']
# new in PyTorch 0.4
device = torch.device("cuda" if torch.cuda.is_available() and use_cuda else "cpu")
torch.manual_seed(seed)
np.random.seed(seed)
if torch.cuda.is_available() and use_cuda:
torch.cuda.manual_seed_all(seed)
class Deep_Q_Learner(object):
def __init__(self, state_shape, action_shape, params):
"""
self.Q is the Action-Value function. This agent represents Q using a Neural Network
If the input is a single dimensional vector, uses a Single-Layer-Perceptron else if the input is 3 dimensional
image, use a Convolutional-Neural-Network
:param state_shape: Shape (tuple) of the observation/state
:param action_shape: Shape (number) of the discrete action space
:param params: A dictionary containing various Agent configuration parameters and hyper-parameters
"""
self.state_shape = state_shape
self.action_shape = action_shape
self.params = params
self.gamma = self.params['gamma'] # Agent's discount factor
self.learning_rate = self.params['lr'] # Agent's Q-learning rate
if len(self.state_shape) == 1: # Single dimensional observation/state space
self.DQN = SLP
elif len(self.state_shape) == 3: # 3D/image observation/state
self.DQN = CNN
self.Q = self.DQN(state_shape, action_shape, device).to(device)
self.Q_optimizer = torch.optim.Adam(self.Q.parameters(), lr=self.learning_rate)
if self.params['use_target_network']:
self.Q_target = self.DQN(state_shape, action_shape, device).to(device)
# self.policy is the policy followed by the agent. This agents follows
# an epsilon-greedy policy w.r.t it's Q estimate.
self.policy = self.epsilon_greedy_Q
self.epsilon_max = 1.0
self.epsilon_min = 0.05
self.epsilon_decay = LinearDecaySchedule(initial_value=self.epsilon_max,
final_value=self.epsilon_min,
max_steps= self.params['epsilon_decay_final_step'])
self.step_num = 0
self.memory = ExperienceMemory(capacity=int(self.params['experience_memory_capacity'])) # Initialize an Experience memory with 1M capacity
def get_action(self, observation):
if len(observation.shape) == 3: # Single image (not a batch)
if observation.shape[2] < observation.shape[0]: # Probably observation is in W x H x C format
# Reshape to C x H x W format as per PyTorch's convention
observation = observation.reshape(observation.shape[2], observation.shape[1], observation.shape[0])
observation = np.expand_dims(observation, 0) # Create a batch dimension
return self.policy(observation)
def epsilon_greedy_Q(self, observation):
# Decay Epsilon/exploration as per schedule
writer.add_scalar("DQL/epsilon", self.epsilon_decay(self.step_num), self.step_num)
self.step_num +=1
if random.random() < self.epsilon_decay(self.step_num):
action = random.choice([i for i in range(self.action_shape)])
else:
action = np.argmax(self.Q(observation).data.to(torch.device('cpu')).numpy())
return action
def learn(self, s, a, r, s_next, done):
# TD(0) Q-learning
if done: # End of episode
td_target = reward + 0.0 # Set the value of terminal state to zero
else:
td_target = r + self.gamma * torch.max(self.Q(s_next))
td_error = td_target - self.Q(s)[a]
# Update Q estimate
#self.Q(s)[a] = self.Q(s)[a] + self.learning_rate * td_error
self.Q_optimizer.zero_grad()
td_error.backward()
self.Q_optimizer.step()
def learn_from_batch_experience(self, experiences):
batch_xp = Experience(*zip(*experiences))
obs_batch = np.array(batch_xp.obs)
action_batch = np.array(batch_xp.action)
reward_batch = np.array(batch_xp.reward)
next_obs_batch = np.array(batch_xp.next_obs)
done_batch = np.array(batch_xp.done)
if self.params['use_target_network']:
if self.step_num % self.params['target_network_update_freq'] == 0:
# The *update_freq is the Num steps after which target net is updated.
# A schedule can be used instead to vary the update freq.
self.Q_target.load_state_dict(self.Q.state_dict())
td_target = reward_batch + ~done_batch * \
np.tile(self.gamma, len(next_obs_batch)) * \
self.Q_target(next_obs_batch).max(1)[0].data
else:
td_target = reward_batch + ~done_batch * \
np.tile(self.gamma, len(next_obs_batch)) * \
self.Q(next_obs_batch).detach().max(1)[0].data
td_target = td_target.to(device)
action_idx = torch.from_numpy(action_batch).to(device)
td_error = torch.nn.functional.mse_loss( self.Q(obs_batch).gather(1, action_idx.view(-1, 1)),
td_target.float().unsqueeze(1))
self.Q_optimizer.zero_grad()
td_error.mean().backward()
writer.add_scalar("DQL/td_error", td_error.mean(), self.step_num)
self.Q_optimizer.step()
def replay_experience(self, batch_size = None):
batch_size = batch_size if batch_size is not None else self.params['replay_batch_size']
experience_batch = self.memory.sample(batch_size)
self.learn_from_batch_experience(experience_batch)
def save(self, env_name):
file_name = self.params['save_dir'] + "DQL_" + env_name + ".ptm"
torch.save(self.Q.state_dict(), file_name)
print("Agent's Q model state saved to ", file_name)
def load(self, env_name):
file_name = self.params['load_dir'] + "DQL_" + env_name + ".ptm"
self.Q.load_state_dict(torch.load(file_name))
print("Loaded Q model state from", file_name)
if __name__ == "__main__":
env_conf = params_manager.get_env_params()
env_conf["env_name"] = args.env_name
# If a custom useful_region configuration for this environment ID is available, use it if not use the Default
custom_region_available = False
for key, value in env_conf['useful_region'].items():
if key in args.env_name:
env_conf['useful_region'] = value
custom_region_available = True
break
if custom_region_available is not True:
env_conf['useful_region'] = env_conf['useful_region']['Default']
print("Using env_conf:", env_conf)
env = Atari.make_env(args.env_name, env_conf)
observation_shape = env.observation_space.shape
action_shape = env.action_space.n
agent_params = params_manager.get_agent_params()
agent = Deep_Q_Learner(observation_shape, action_shape, agent_params)
if agent_params['load_trained_model']:
try:
agent.load(env_conf["env_name"])
except FileNotFoundError:
print("WARNING: No trained model found for this environment. Training from scratch.")
first_episode = True
episode_rewards = list()
for episode in range(agent_params['max_num_episodes']):
obs = env.reset()
cum_reward = 0.0 # Cumulative reward
done = False
step = 0
#for step in range(agent_params['max_steps_per_episode']):
while not done:
if env_conf['render']:
env.render()
action = agent.get_action(obs)
next_obs, reward, done, info = env.step(action)
#agent.learn(obs, action, reward, next_obs, done)
agent.memory.store(Experience(obs, action, reward, next_obs, done))
obs = next_obs
cum_reward += reward
step += 1
global_step_num +=1
if done is True:
if first_episode: # Initialize max_reward at the end of first episode
max_reward = cum_reward
first_episode = False
episode_rewards.append(cum_reward)
if cum_reward > max_reward:
max_reward = cum_reward
agent.save(env_conf['env_name'])
print("\nEpisode#{} ended in {} steps. reward ={} ; mean_reward={:.3f} best_reward={}".
format(episode, step+1, cum_reward, np.mean(episode_rewards), max_reward))
writer.add_scalar("main/ep_reward", cum_reward, global_step_num)
writer.add_scalar("main/mean_ep_reward", np.mean(episode_rewards), global_step_num)
writer.add_scalar("main/max_ep_rew", max_reward, global_step_num)
if agent.memory.get_size() >= 2 * agent_params['replay_batch_size']:
agent.replay_experience()
break
env.close()
writer.close()
上述代码以及一些额外的更改(用于使用我们将在下一节讨论的 Atari 包装器)可以在本书代码库的 ch6/deep_Q_Learner.py 中找到。在我们完成下一节关于 Atari Gym 环境 的内容后,我们将使用 deep_Q_Learner.py 中的智能体实现来训练 Atari 游戏中的智能体,并最终查看它们的表现。
本书的代码库将包含最新的代码实现,包括改进和 bug 修复,这些将会在本书印刷后提交。因此,建议您在 GitHub 上为本书的代码库加星并关注,以便获得关于这些更改和改进的自动更新。
Atari Gym 环境
在第四章,探索 Gym 及其特性 中,我们查看了 Gym 中提供的各种环境列表,包括 Atari 游戏类别,并使用脚本列出了计算机上所有可用的 Gym 环境。我们还了解了环境名称的命名法,特别是 Atari 游戏的命名。在本节中,我们将使用 Atari 环境,并查看如何使用 Gym 环境包装器定制这些环境。以下是来自 9 个不同 Atari 环境的 9 张截图的拼贴:

定制 Atari Gym 环境
有时,我们可能希望改变环境返回观察结果的方式,或者改变奖励的尺度,以便我们的智能体能更好地学习,或者在智能体接收信息之前过滤掉一些信息,或改变环境在屏幕上的渲染方式。到目前为止,我们一直在开发和定制我们的智能体,使其在环境中表现良好。若能对环境返回给智能体的内容和方式进行一定的灵活定制,让我们可以根据需要定制智能体的学习行为,这岂不是很好吗?幸运的是,Gym 库通过 Gym 环境包装器使我们可以轻松扩展或定制环境返回的信息。包装器接口允许我们通过子类化并添加新的例程作为现有例程的附加层来进行定制。我们可以向 Gym 环境类的一个或多个方法中添加自定义处理语句:
-
__init__(self, env)__ -
_seed -
_reset -
_step -
_render -
_close
根据我们希望对环境进行的自定义,可以决定我们要扩展哪些方法。例如,如果我们希望改变观察的形状/大小,我们可以扩展_step和_reset方法。在接下来的小节中,我们将展示如何利用包装器接口来定制 Atari Gym 环境。
实现自定义的 Gym 环境包装器
在这一部分,我们将介绍一些对于 Gym Atari 环境尤其有用的环境包装器。我们将在这一部分实现的大多数包装器也可以与其他环境一起使用,以提高智能体的学习表现。
下表列出了我们将在接下来的部分实现的包装器,并为每个包装器提供简要描述,帮助你了解概况:
| 包装器 | 目的简要描述 |
|---|---|
ClipRewardEnv |
实现奖励裁剪 |
AtariRescale |
将屏幕像素重新缩放为 84x84x1 的灰度图像 |
NormalizedEnv |
根据环境中观察到的均值和方差对图像进行标准化 |
NoopResetEnv |
在重置时执行随机数量的noop(空操作),以采样不同的初始状态 |
FireResetEnv |
在重置时执行火焰动作 |
EpisodicLifeEnv |
将生命结束标记为回合结束,并在游戏结束时进行重置 |
MaxAndSkipEnv |
对一个固定数量的步骤(使用skip参数指定,默认为 4)重复执行动作 |
奖励裁剪
不同的问题或环境提供不同范围的奖励值。例如,我们在上一章中看到,在Mountain Car v0环境中,智能体在每个时间步都会收到-1 的奖励,直到回合结束,无论智能体如何操作小车。在Cart Pole v0环境中,智能体在每个时间步都会收到+1 的奖励,直到回合结束。在 MS Pac-Man 这样的 Atari 游戏环境中,如果智能体吃掉一个鬼魂,它将获得最高+1,600 的奖励。我们可以开始看到,不同环境和学习问题中,奖励的幅度和奖励出现的时机差异非常大。如果我们的深度 Q 学习算法要解决这种不同的问题,而我们又不希望单独微调每个环境的超参数,那么我们必须解决奖励尺度的差异。这正是奖励裁剪背后的直觉:根据从环境中收到的实际奖励的符号,我们将奖励裁剪为-1、0 或+1。通过这种方式,我们限制了奖励的幅度,避免了在不同环境之间存在过大的差异。我们可以通过继承gym.RewardWrapper类并修改reward(...)函数来实现这一简单的奖励裁剪技术,并将其应用于我们的环境,代码片段如下所示:
class ClipRewardEnv(gym.RewardWrapper):
def __init__(self, env):
gym.RewardWrapper.__init__(self, env)
def reward(self, reward):
""" Clip rewards to be either -1, 0 or +1 based on the sign"""
return np.sign(reward)
截取奖励值到 (-1, 0, 1) 的技术对于 Atari 游戏效果很好。但是,值得注意的是,这可能不是处理奖励大小和频率变化的环境的最佳技术。截取奖励值会改变智能体的学习目标,有时可能导致智能体学习到与预期不同的策略。
预处理 Atari 屏幕图像帧
Atari Gym 环境产生的观察通常具有 210x160x3 的形状,代表一个 RGB(彩色)图像,宽度为 210 像素,高度为 160 像素。尽管原始分辨率为 210x160x3 的彩色图像包含更多的像素,因此包含更多的信息,但事实证明,降低分辨率往往能获得更好的性能。较低的分辨率意味着每一步由智能体处理的数据较少,从而加快训练速度,尤其是在我们常见的消费级计算硬件上。
我们来创建一个预处理管道,该管道将接收原始的 Atari 屏幕图像,并执行以下操作:

我们可以裁剪掉屏幕上没有任何与环境相关的有用信息的区域。
最后,我们将图像调整为 84x84 的尺寸。我们可以选择不同的数字,除了 84,只要它包含适量的像素。然而,选择一个方形矩阵(如 84x84 或 80x80)是高效的,因为卷积操作(例如使用 CUDA)会对这种方形输入进行优化。
def process_frame_84(frame, conf):
frame = frame[conf["crop1"]:conf["crop2"] + 160, :160]
frame = frame.mean(2)
frame = frame.astype(np.float32)
frame *= (1.0 / 255.0)
frame = cv2.resize(frame, (84, conf["dimension2"]))
frame = cv2.resize(frame, (84, 84))
frame = np.reshape(frame, [1, 84, 84])
return frame
class AtariRescale(gym.ObservationWrapper):
def __init__(self, env, env_conf):
gym.ObservationWrapper.__init__(self, env)
self.observation_space = Box(0.0, 1.0, [1, 84, 84])
self.conf = env_conf
def observation(self, observation):
return process_frame_84(observation, self.conf)
注意,假设每一帧观察图像的分辨率为 84x84 像素,数据类型为 numpy.float32,每个像素占用 4 字节,那么我们需要大约 4x84x84 = 28,224 字节。如你在经验记忆部分所记得的,每个经验对象包含两帧(一个是当前观察,另一个是下一次观察),这意味着我们需要 2x 28,224 = 56,448 字节(再加上 2 字节用于 action 和 4 字节用于 reward)。56,448 字节(或 0.056448 MB)看起来不多,但如果考虑到通常情况下经验记忆的容量是 1e6(百万),你会意识到我们需要大约 1e6 x 0.056448 MB = 56,448 MB 或 56.448 GB!这意味着我们仅仅为了存储 100 万个经验对象,就需要 56.448 GB 的内存!
你可以进行一些内存优化,以减少训练代理所需的 RAM。在某些游戏中,使用较小的经验内存是一种减少内存占用的直接方法。在某些环境中,较大的经验内存可以帮助代理更快地学习。减少内存占用的一种方法是,在存储时不对帧进行缩放(即不除以 255),这要求使用浮点表示(numpy.float32),而是将帧存储为numpy.uint8,这样我们每个像素只需要 1 个字节,而不是 4 个字节,从而帮助减少内存需求,降低四倍。然后,当我们想在前向传递到深度 Q 网络时使用存储的经验以获取 Q 值预测时,我们可以将图像缩放到 0.0 到 1.0 的范围内。
标准化观测值
在某些情况下,标准化观测值有助于提高收敛速度。最常用的标准化过程包含两个步骤:
-
使用均值减法进行零中心化
-
使用标准差进行缩放
本质上,以下是标准化过程:
(x - numpy.mean(x)) / numpy.std(x)
在之前的过程中,x 是观测值。请注意,其他标准化过程也可以使用,具体取决于所需的标准化值范围。例如,如果我们希望标准化后的值位于 0 到 1 之间,可以使用以下方法:
(x - numpy.min(x)) / (numpy.max(x) - numpy.min(x))
在之前的过程中,我们不是减去均值,而是减去最小值并除以最大值和最小值之间的差。这种方式下,观测值/x 中的最小值将被标准化为 0,最大值将被标准化为 1。
或者,如果我们希望标准化后的值位于-1 到+1 之间,可以使用以下方法:
2 * (x - numpy.min(x)) / (numpy.max(x) - numpy.min(x)) - 1
在我们的环境标准化包装器实现中,我们将使用第一种方法,通过均值减法进行零中心化,并使用观测数据的标准差进行缩放。事实上,我们会更进一步,计算我们迄今为止接收到的所有观测值的运行均值和标准差,以根据代理目前所观察到的观测数据分布来标准化观测值。这种方法更为适合,因为同一环境下不同观测值之间可能存在较大的方差。以下是我们讨论的标准化包装器的实现代码:
class NormalizedEnv(gym.ObservationWrapper):
def __init__(self, env=None):
gym.ObservationWrapper.__init__(self, env)
self.state_mean = 0
self.state_std = 0
self.alpha = 0.9999
self.num_steps = 0
def observation(self, observation):
self.num_steps += 1
self.state_mean = self.state_mean * self.alpha + \
observation.mean() * (1 - self.alpha)
self.state_std = self.state_std * self.alpha + \
observation.std() * (1 - self.alpha)
unbiased_mean = self.state_mean / (1 - pow(self.alpha, self.num_steps))
unbiased_std = self.state_std / (1 - pow(self.alpha, self.num_steps))
return (observation - unbiased_mean) / (unbiased_std + 1e-8)
我们从环境中获取的图像帧(即使经过我们的预处理包装器)已经处于相同的尺度(0-255 或 0.0 到 1.0)。在这种情况下,标准化过程中的缩放步骤可能没有太大帮助。这个包装器通常对其他类型的环境可能会很有用,并且我们也没有观察到它会对来自 Gym 环境(如 Atari)已经缩放的图像观测值的性能产生不利影响。
在重置时进行随机无操作
当环境重置时,智能体通常从相同的初始状态开始,因此在重置时会得到相同的观察结果。智能体可能会记住或习惯某个游戏关卡的初始状态,甚至可能在稍微改变起始位置或游戏关卡时表现不佳。有时候,发现随机化初始状态会有帮助,比如从不同的初始状态中随机选择一个作为智能体开始的地方。为了实现这一点,我们可以添加一个 Gym 包装器,在发送重置后的第一次观察之前,执行一个随机数量的“无操作”(no-op)动作。Gym 库使用的 Atari 2600 的街机学习环境(Arcade Learning Environment)支持一种“NOOP”或无操作动作,在 Gym 库中,这个动作的值为 0。所以,我们将在环境中执行一个随机数量的action=0,然后再将观察结果返回给智能体,如下代码片段所示:
class NoopResetEnv(gym.Wrapper):
def __init__(self, env, noop_max=30):
"""Sample initial states by taking random number of no-ops on reset.
No-op is assumed to be action 0.
"""
gym.Wrapper.__init__(self, env)
self.noop_max = noop_max
self.noop_action = 0
assert env.unwrapped.get_action_meanings()[0] == 'NOOP'
def reset(self):
""" Do no-op action for a number of steps in [1, noop_max]."""
self.env.reset()
noops = random.randrange(1, self.noop_max + 1) # pylint: disable=E1101
assert noops > 0
obs = None
for _ in range(noops):
obs, _, done, _ = self.env.step(self.noop_action)
return obs
def step(self, ac):
return self.env.step(ac)
重置时按下 Fire 按钮
一些 Atari 游戏要求玩家按下 Fire 按钮以开始游戏。某些游戏要求在每次失去一条生命后按下 Fire 按钮。通常来说,这几乎是 Fire 按钮唯一的用途!虽然对我们来说,这看起来微不足道,但有时候对于强化学习的智能体来说,自己弄明白这一点可能并不容易。并不是说它们无法学会这一点,事实上,它们能够发现游戏中的许多隐藏漏洞或模式,这些人类从未发现过!例如,在 Qbert 游戏中,使用进化策略(这是一种受遗传算法启发的黑箱学习策略)训练的智能体发现了一种特殊的方法,可以不断获得分数并让游戏永远不会结束!你知道它的得分是多少吗?大约 1,000,000!它们之所以能得这么多分,是因为游戏由于时间限制被人为重置过。你能在 Qbert 游戏中尝试获得这么多分吗?你可以在这里看到智能体得分的实际情况:www.youtube.com/watch?v=meE5aaRJ0Zs。
关键不在于智能体如此聪明能够搞明白所有这些事情。它们当然能做到,但大多数情况下,这反而会妨碍智能体在合理时间内取得进展。特别是当我们希望一个智能体同时应对几种不同类型的游戏(一次一个)时,这种情况尤为明显。我们最好从简单的假设开始,并在通过这些简单假设训练智能体成功后,再逐步让假设变得更复杂。
因此,我们将实现一个FireResetEnv Gym 包装器,它会在每次重置时按下 Fire 按钮,并启动环境供智能体使用。代码实现如下:
class FireResetEnv(gym.Wrapper):
def __init__(self, env):
"""Take action on reset for environments that are fixed until firing."""
gym.Wrapper.__init__(self, env)
assert env.unwrapped.get_action_meanings()[1] == 'FIRE'
assert len(env.unwrapped.get_action_meanings()) >= 3
def reset(self):
self.env.reset()
obs, _, done, _ = self.env.step(1)
if done:
self.env.reset()
obs, _, done, _ = self.env.step(2)
if done:
self.env.reset()
return obs
def step(self, ac):
return self.env.step(ac)
片段化的生活
在许多游戏中,包括 Atari 游戏,玩家有多个生命可以使用。
Deepmind 观察到并报告称,当一个生命丧失时终止一个回合,有助于智能体更好地学习。需要注意的是,目的是向智能体表明失去一条生命是不好的行为。在这种情况下,当回合终止时,我们不会重置环境,而是继续进行,直到游戏结束后再重置环境。如果在每次失去生命后都重置游戏,我们会限制智能体对仅凭一条生命可以收集到的观察和经验的暴露,而这通常对智能体的学习表现不利。
为了实现我们刚才讨论的内容,我们将使用 EpisodicLifeEnv 类,该类在失去生命时标记回合结束,并在游戏结束时重置环境,代码片段如下所示:
class EpisodicLifeEnv(gym.Wrapper):
def __init__(self, env):
"""Make end-of-life == end-of-episode, but only reset on true game over.
Done by DeepMind for the DQN and co. since it helps value estimation.
"""
gym.Wrapper.__init__(self, env)
self.lives = 0
self.was_real_done = True
def step(self, action):
obs, reward, done, info = self.env.step(action)
self.was_real_done = True
# check current lives, make loss of life terminal,
# then update lives to handle bonus lives
lives = info['ale.lives']
if lives < self.lives and lives > 0:
# for Qbert sometimes we stay in lives == 0 condition for a few frames
# so its important to keep lives > 0, so that we only reset once
# the environment advertises done.
done = True
self.was_real_done = False
self.lives = lives
return obs, reward, done, info
def reset(self):
"""Reset only when lives are exhausted.
This way all states are still reachable even though lives are episodic,
and the learner need not know about any of this behind-the-scenes.
"""
if self.was_real_done:
obs = self.env.reset()
self.lives = 0
else:
# no-op step to advance from terminal/lost life state
obs, _, _, info = self.env.step(0)
self.lives = info['ale.lives']
return obs
最大值和跳帧
Gym 库提供了在其 ID 中带有 NoFrameskip 的环境,我们在 第四章《探索 Gym 及其特性》中讨论过,在那里我们讨论了 Gym 环境的命名法。如你从 第四章《探索 Gym 及其特性》中回忆的那样,默认情况下,如果环境名称中没有 Deterministic 或 NoFrameskip,发送到环境的动作会在 n 帧内重复执行,其中 n 从 (2, 3, 4) 中均匀采样。如果我们希望以特定速率逐步进行环境的操作,可以使用带有 NoFrameskip 的 Gym Atari 环境,这样会在没有改变步骤持续时间的情况下逐步执行底层环境。在这种情况下,步骤速率为每秒 60 帧。然后我们可以自定义环境,选择跳过特定速率 (k) 来以特定速率执行步骤。以下是自定义步骤/跳帧速率的实现:
class MaxAndSkipEnv(gym.Wrapper):
def __init__(self, env=None, skip=4):
"""Return only every `skip`-th frame"""
gym.Wrapper.__init__(self, env)
# most recent raw observations (for max pooling across time steps)
self._obs_buffer = deque(maxlen=2)
self._skip = skip
def step(self, action):
total_reward = 0.0
done = None
for _ in range(self._skip):
obs, reward, done, info = self.env.step(action)
self._obs_buffer.append(obs)
total_reward += reward
if done:
break
max_frame = np.max(np.stack(self._obs_buffer), axis=0)
return max_frame, total_reward, done, info
def reset(self):
"""Clear past frame buffer and init. to first obs. from inner env."""
self._obs_buffer.clear()
obs = self.env.reset()
self._obs_buffer.append(obs)
return obs
请注意,我们还会在跳过的帧上取像素值的最大值,并将其作为观察值,而不是完全忽略跳过的所有中间图像帧。
包装 Gym 环境
最后,我们将应用之前根据我们使用 parameters.JSON 文件指定的环境配置开发的包装器:
def make_env(env_id, env_conf):
env = gym.make(env_id)
if 'NoFrameskip' in env_id:
assert 'NoFrameskip' in env.spec.id
env = NoopResetEnv(env, noop_max=30)
env = MaxAndSkipEnv(env, skip=env_conf['skip_rate'])
if env_conf['episodic_life']:
env = EpisodicLifeEnv(env)
if 'FIRE' in env.unwrapped.get_action_meanings():
env = FireResetEnv(env)
env = AtariRescale(env, env_conf['useful_region'])
env = NormalizedEnv(env)
if env_conf['clip_reward']:
env = ClipRewardEnv(env)
return env
我们之前讨论过的所有环境包装器都已经在本书代码库的 ch6/environment/atari.py 中实现并可用。
训练深度 Q 学习器玩 Atari 游戏
本章我们介绍了几种新的技术。你已经做得很棒,值得为自己鼓掌!接下来是有趣的部分,你可以让你的智能体自主训练,玩几个 Atari 游戏,并观察它们的进展。我们深度 Q 学习器的一个亮点是,我们可以使用相同的智能体来训练和玩任何 Atari 游戏!
到本节结束时,你应该能够使用我们的深度 Q 学习代理观察屏幕上的像素,并通过向 Atari Gym 环境发送摇杆命令来执行动作,就像下面的截图所示:

将所有我们讨论的技巧整合成一个全面的深度 Q 学习器
现在是时候将我们讨论过的所有技术结合起来,形成一个全面的实现,利用这些技术以获得最佳性能。我们将使用前一节中创建的environment.atari模块,并添加几个有用的 Gym 环境包装器。让我们来看一下代码大纲,了解代码结构:
你会注意到,代码的某些部分被省略了,为了简洁起见,这些部分用...表示,意味着这些部分的代码已经被折叠/隐藏。你可以在本书的代码库中的ch6/deep_Q_Learner.py找到完整的代码的最新版本。
#!/usr/bin/env python
#!/usr/bin/env python
import gym
import torch
import random
import numpy as np
import environment.atari as Atari
import environment.utils as env_utils
from utils.params_manager import ParamsManager
from utils.decay_schedule import LinearDecaySchedule
from utils.experience_memory import Experience, ExperienceMemory
from function_approximator.perceptron import SLP
from function_approximator.cnn import CNN
from tensorboardX import SummaryWriter
from datetime import datetime
from argparse import ArgumentParser
args = ArgumentParser("deep_Q_learner")
args.add_argument("--params-file", help="Path to the parameters json file. Default is parameters.json",
default="parameters.json", metavar="PFILE")
args.add_argument("--env-name", help="ID of the Atari environment available in OpenAI Gym. Default is Seaquest-v0",
default="Seaquest-v0", metavar="ENV")
args.add_argument("--gpu-id", help="GPU device ID to use. Default=0", default=0, type=int, metavar="GPU_ID")
args.add_argument("--render", help="Render environment to Screen. Off by default", action="store_true", default=False)
args.add_argument("--test", help="Test mode. Used for playing without learning. Off by default", action="store_true",
default=False)
args = args.parse_args()
params_manager= ParamsManager(args.params_file)
seed = params_manager.get_agent_params()['seed']
summary_file_path_prefix = params_manager.get_agent_params()['summary_file_path_prefix']
summary_file_path= summary_file_path_prefix + args.env_name + "_" + datetime.now().strftime("%y-%m-%d-%H-%M")
writer = SummaryWriter(summary_file_path)
# Export the parameters as json files to the log directory to keep track of the parameters used in each experiment
params_manager.export_env_params(summary_file_path + "/" + "env_params.json")
params_manager.export_agent_params(summary_file_path + "/" + "agent_params.json")
global_step_num = 0
use_cuda = params_manager.get_agent_params()['use_cuda']
# new in PyTorch 0.4
device = torch.device("cuda:" + str(args.gpu_id) if torch.cuda.is_available() and use_cuda else "cpu")
torch.manual_seed(seed)
np.random.seed(seed)
if torch.cuda.is_available() and use_cuda:
torch.cuda.manual_seed_all(seed)
class Deep_Q_Learner(object):
def __init__(self, state_shape, action_shape, params):
...
def get_action(self, observation):
...
def epsilon_greedy_Q(self, observation):
...
def learn(self, s, a, r, s_next, done):
...
def learn_from_batch_experience(self, experiences):
...
def replay_experience(self, batch_size = None):
...
def load(self, env_name):
...
if __name__ == "__main__":
env_conf = params_manager.get_env_params()
env_conf["env_name"] = args.env_name
# If a custom useful_region configuration for this environment ID is available, use it if not use the Default
...
# If a saved (pre-trained) agent's brain model is available load it as per the configuration
if agent_params['load_trained_model']:
...
# Start the training process
episode = 0
while global_step_num <= agent_params['max_training_steps']:
obs = env.reset()
cum_reward = 0.0 # Cumulative reward
done = False
step = 0
#for step in range(agent_params['max_steps_per_episode']):
while not done:
if env_conf['render'] or args.render:
env.render()
action = agent.get_action(obs)
next_obs, reward, done, info = env.step(action)
#agent.learn(obs, action, reward, next_obs, done)
agent.memory.store(Experience(obs, action, reward, next_obs, done))
obs = next_obs
cum_reward += reward
step += 1
global_step_num +=1
if done is True:
episode += 1
episode_rewards.append(cum_reward)
if cum_reward > agent.best_reward:
agent.best_reward = cum_reward
if np.mean(episode_rewards) > prev_checkpoint_mean_ep_rew:
num_improved_episodes_before_checkpoint += 1
if num_improved_episodes_before_checkpoint >= agent_params["save_freq_when_perf_improves"]:
prev_checkpoint_mean_ep_rew = np.mean(episode_rewards)
agent.best_mean_reward = np.mean(episode_rewards)
agent.save(env_conf['env_name'])
num_improved_episodes_before_checkpoint = 0
print("\nEpisode#{} ended in {} steps. reward ={} ; mean_reward={:.3f} best_reward={}".
format(episode, step+1, cum_reward, np.mean(episode_rewards), agent.best_reward))
writer.add_scalar("main/ep_reward", cum_reward, global_step_num)
writer.add_scalar("main/mean_ep_reward", np.mean(episode_rewards), global_step_num)
writer.add_scalar("main/max_ep_rew", agent.best_reward, global_step_num)
# Learn from batches of experience once a certain amount of xp is available unless in test only mode
if agent.memory.get_size() >= 2 * agent_params['replay_start_size'] and not args.test:
agent.replay_experience()
break
env.close()
writer.close()
超参数
以下是我们的深度 Q 学习器使用的一些超参数列表,简要描述它们的作用及其接受的值类型:
| 超参数 | 简要描述 | 值的类型 |
|---|---|---|
max_num_episodes |
运行代理的最大回合数。 | 整数(例如:100,000) |
replay_memory_capacity |
经验记忆的总容量。 | 整数或指数表示法(例如:1e6) |
replay_batch_size |
每次更新过程中,用于更新 Q 函数的(迷你)批次中的过渡数量。 | 整数(例如:2,000) |
use_target_network |
是否使用目标 Q 网络。 | 布尔值(true/false) |
target_network_update_freq |
更新目标 Q 网络的步数,使用主 Q 网络进行更新。 | 整数(例如:1,000) |
lr |
深度 Q 网络的学习率。 | 浮动数值(例如:1e-4) |
gamma |
MDP 的折扣因子。 | 浮动数值(例如:0.98) |
epsilon_max |
epsilon 的最大值,从该值开始衰减。 | 浮动数值(例如:1.0) |
epsilon_min |
epsilon 的最小值,衰减最终将会稳定到该值。 | 浮动数值(例如:0.05) |
seed |
用于为 numpy 和 torch(以及torch.cuda)设置随机种子的种子值,以便(在某种程度上)重现这些库引入的随机性。 |
整数(例如:555) |
use_cuda |
是否在 GPU 可用时使用基于 CUDA 的 GPU。 | 布尔值(例如:true) |
load_trained_model |
是否加载已经训练好的模型(如果存在的话),适用于当前的环境/问题。如果设置为 true,但没有可用的已训练模型,则模型将从头开始训练。 | 布尔值(例如:true) |
load_dir |
训练模型应该从哪个目录加载以恢复训练的路径(包括斜杠)。 | 字符串(例如:"trained_models/") |
save_dir |
保存模型的目录路径。每当智能体取得新的最佳分数/奖励时,新模型将被保存。 | string(例如:trained_models/) |
请参考本书代码库中的 ch6/parameters.JSON 文件,以获取智能体使用的最新参数列表。
启动训练过程
我们现在已经将深度 Q 学习者的所有部分拼凑在一起,准备好训练智能体了!务必查看/拉取/下载本书代码库中的最新代码。
你可以从 Atari 环境列表中选择任何一个环境,并使用以下命令训练我们开发的智能体:
(rl_gym_book) praveen@ubuntu:~/HOIAWOG/ch6$ python deep_Q_learner.py --env "ENV_ID"
在前面的命令中,ENV_ID 是 Atari Gym 环境的名称/ID。例如,如果你想在 pong 环境中训练智能体,并且不使用帧跳跃,你可以运行以下命令:
(rl_gym_book) praveen@ubuntu:~/HOIAWOG/ch6$ python deep_Q_learner.py --env "PongNoFrameskip-v4"
默认情况下,训练日志将保存在 ./logs/DQL_{ENV}_{T},其中 {ENV} 是环境的名称,{T} 是运行智能体时获得的时间戳。如果你使用以下命令启动 TensorBoard 实例:
(rl_gym_book) praveen@ubuntu:~/HOIAWOG/ch6$ tensorboard --logdir=logs/
默认情况下,我们的 deep_Q_learner.py 脚本将使用与脚本位于同一目录中的 parameters.JSON 文件来读取可配置的参数值。你可以使用命令行 --params-file 参数覆盖并使用不同的参数配置文件。
如果在 parameters.JSON 文件中将 load_trained_model 参数设置为 true,并且为所选环境有一个已保存的模型,我们的脚本会尝试用之前训练的模型初始化智能体,这样它可以从中断的地方继续,而不是从头开始训练。
测试你的深度 Q 学习者在 Atari 游戏中的表现
感觉不错吧?你现在已经开发出一个能够学习任何 Atari 游戏并且自动提升表现的智能体!一旦你训练好你的智能体在任何 Atari 游戏中,你可以使用脚本的测试模式来根据它到目前为止的学习情况测试智能体的表现。你可以通过在 deep_q_learner.py 脚本中使用 --test 参数来启用测试模式。启用环境渲染也非常有用,这样你不仅可以看到控制台上打印的奖励,还能直观地看到智能体的表现。例如,你可以使用以下命令在 Seaquest Atari 游戏中测试智能体:
(rl_gym_book) praveen@ubuntu:~/HOIAWOG/ch6$ python deep_Q_learner.py --env "Seaquest-v0" --test --render
你会看到 Seaquest 游戏窗口弹出,智能体展示它的技能!
关于 test 模式,有几个需要注意的要点:
-
测试模式会关闭智能体的学习过程。因此,智能体在测试模式下不会学习或更新自己。这个模式仅用于测试已训练的智能体的表现。如果你想查看智能体在学习过程中如何表现,你可以只使用
--render选项,而不使用--test选项。 -
测试模式假定你选择的环境中已经存在一个训练好的模型,存放在
trained_models文件夹中。否则,一个没有任何先验知识的全新代理将从头开始玩游戏。此外,由于学习被禁用,你将看不到代理的进步!
现在轮到你了,去外面进行实验,回顾并比较我们在不同雅达利 Gym 环境中实现的代理的表现,看看代理能得多少分!如果你训练了一个代理并使其在游戏中表现良好,你可以通过在本书代码库的分支上发起 pull request,向其他读者展示并分享你的成果。你会被特写在页面上!
一旦你熟悉了我们开发的代码库,你可以进行多种实验。例如,你可以通过简单地修改parameters.JSON文件,关闭目标 Q 网络或增加/减少经验记忆/重放批量大小,并使用非常方便的 TensorBoard 仪表板比较性能。
总结
本章以开发能够在雅达利游戏中取得高分的智能学习代理为宏大目标展开。我们通过实现几种技术,逐步改进了上一章中开发的 Q-学习器。我们首先学习了如何使用神经网络来逼近 Q 动作值函数,并通过实际实现一个浅层神经网络来解决著名的 Cart Pole 问题,使我们的学习更加具体。接着,我们实现了经验记忆和经验重放机制,使得代理能够从(小批量)随机采样的经验中学习,这有助于通过打破代理与环境互动之间的相关性,提升性能,并通过批量重放代理先前的经验来增加样本效率。然后,我们重新审视了 epsilon-贪婪行动选择策略,并实现了一个衰减计划,根据预定计划减少探索,使代理更多依赖于自己的学习。
然后,我们看了如何利用 TensorBoard 的日志记录和可视化功能,与基于 PyTorch 的学习代理一起使用,从而以一种简单直观的方式观察代理的训练进展。我们还实现了一个小巧的参数管理类,使我们能够通过外部易于阅读的 JSON 文件配置代理的超参数和其他配置参数。
在我们得到了一个良好的基准并实现了有用的工具之后,我们开始实现深度 Q 学习器。我们首先通过在 PyTorch 中实现一个深度卷积神经网络来表示我们代理的 Q(动作值)函数。接着,我们展示了如何轻松实现使用目标 Q 网络的想法,目标 Q 网络被证明能稳定代理的 Q 学习过程。然后,我们将这些结合起来,构建了一个基于深度 Q 学习的代理,能够仅凭 Gym 环境中的原始像素观测进行学习并执行动作。
接着,我们将目光和双手放在了 Atari Gym 环境上,研究了几种使用 Gym 环境包装器定制 Gym 环境的方法。我们还讨论了几个用于 Atari 环境的有用包装器,特别实现了包装器来裁剪奖励、预处理观察图像帧、对所有采样的观测分布进行归一化、在重置时发送随机 noop 动作以采样不同的初始状态、在重置时按下 Fire 按钮,并通过跳帧以自定义速率进行步进。最终,我们展示了如何将这些整合成一个全面的代理训练代码库,并在任何 Atari 游戏上训练代理,随后在 TensorBoard 中查看进展摘要。我们还展示了如何保存代理的状态,并从之前保存的状态恢复训练,而不是从头开始重新训练。最后,我们看到了我们实现并训练的代理性能的提升。
我们希望你在整个章节中都玩得很开心。在下一个章节中,我们将研究并实现一种不同的算法,这种算法可以用于执行比一组离散按钮按压更复杂的动作,并且我们将展示如何利用它训练一个代理,在模拟中自主地控制汽车!
第七章:创建自定义 OpenAI Gym 环境 - CARLA 驾驶模拟器
在第一章中,我们查看了 OpenAI Gym 环境目录中可用的各种学习环境类别。接着,我们在第五章,实现你的第一个学习智能体 - 解决 Mountain Car 问题中探讨了环境列表及其命名法,并提前了解了其中的一些内容。我们还开发了智能体来解决 Mountain Car、Cart Pole 问题以及一些 Atari 游戏环境。到现在为止,你应该已经很好地理解了 OpenAI Gym 提供的各种环境类型和变种。通常,在我们学会如何开发自己的智能体之后,我们希望将这种知识和技能应用于开发智能体来解决新的问题,解决我们已经面临的问题,甚至是我们感兴趣的问题。例如,你可能是一个游戏开发者,想为游戏角色增加智能行为,或者是一个机器人工程师,想将人工智能引入到你的机器人中,或者你可能是一个自动驾驶工程师,想将强化学习应用于自动驾驶。你也许是一个喜欢修理的小工具爱好者,想将一件小工具变成一个智能的物联网(IoT)设备,或者你甚至可能是一个医疗行业的专业人士,想通过机器学习提高实验室的诊断能力。应用的潜力几乎是无限的。
我们选择 OpenAI Gym 作为我们的学习环境的原因之一,是因为它简洁而标准的接口将环境的类型和特性与环境-智能体接口解耦。在本章中,我们将探讨如何根据个人或专业需求创建你自己的环境。这将使你能够在自己的设计或问题上使用我们在前几章中开发的智能体实现、训练和测试脚本、参数管理器以及日志记录和可视化程序。
了解 Gym 环境的构成
任何与 Gym 兼容的环境都应该继承 gym.Env 类,并实现 reset 和 step 方法,以及 observation_space 和 action_space 属性和特性。还有机会实现其他可选方法,增加我们自定义环境的附加功能。下表列出了并描述了其他可用的方法:
| 方法 | 功能描述 |
|---|---|
observation_space |
环境返回的观察值的形状和类型。 |
action_space |
环境接受的动作的形状和类型。 |
reset() |
用于在每个 episode 开始或结束时重置环境的常规操作。 |
step(...) |
用于计算推进环境、仿真或游戏至下一步所需的信息的程序。该程序包括在环境中应用所选的动作、计算奖励、生成下一次观察,并判断回合是否结束。 |
_render() |
(可选)此项渲染 Gym 环境的状态或观察结果。 |
_close() |
(可选)此项用于关闭 Gym 环境。 |
_seed |
(可选)此项为 Gym 环境的随机函数提供一个自定义种子,使得环境在给定种子下能以可复现的方式运行。 |
_configure |
(可选)此项启用额外的环境配置。 |
创建自定义 Gym 环境实现的模板
基于我们已经讨论的 Gym 环境结构,我们现在将展示一个名为 CustomEnv 的自定义环境类实现的基本版本,它将是 gym.Env 的子类,并实现所需的必要方法和参数,以使其成为一个与 Gym 兼容的环境。以下是最小实现的模板:
import gym
class CustomEnv(gym.Env):
"""
A template to implement custom OpenAI Gym environments
"""
metadata = {'render.modes': ['human']}
def __init__(self):
self.__version__ = "0.0.1"
# Modify the observation space, low, high and shape values according to your custom environment's needs
self.observation_space = gym.spaces.Box(low=0.0, high=1.0, shape=(3,))
# Modify the action space, and dimension according to your custom environment's needs
self.action_space = gym.spaces.Box(4)
def step(self, action):
"""
Runs one time-step of the environment's dynamics. The reset() method is called at the end of every episode
:param action: The action to be executed in the environment
:return: (observation, reward, done, info)
observation (object):
Observation from the environment at the current time-step
reward (float):
Reward from the environment due to the previous action performed
done (bool):
a boolean, indicating whether the episode has ended
info (dict):
a dictionary containing additional information about the previous action
"""
# Implement your step method here
# - Calculate reward based on the action
# - Calculate next observation
# - Set done to True if end of episode else set done to False
# - Optionally, set values to the info dict
# return (observation, reward, done, info)
def reset(self):
"""
Reset the environment state and returns an initial observation
Returns
-------
observation (object): The initial observation for the new episode after reset
:return:
"""
# Implement your reset method here
# return observation
def render(self, mode='human', close=False):
"""
:param mode:
:return:
"""
return
在我们完成环境类的实现后,我们应当将其注册到 OpenAI Gym 注册表中,以便可以像之前使用 Gym 环境一样通过 gym.make(ENV_NAME) 创建环境实例。
注册自定义环境到 OpenAI Gym
CustomEnv class we implemented is as follows:
from gym.envs.registration import register
register(
id='CustomEnv-v0',
entry_point='custom_environments.envs:CustomEnv',
)
我们将在本章后面使用此模板来创建一个使用非常复杂的驾驶模拟器的自定义 Gym 环境。
创建一个与 OpenAI Gym 兼容的 CARLA 驾驶模拟环境
CARLA 是一个基于 UnrealEngine4 游戏引擎构建的驾驶模拟环境,相较于一些竞争对手,CARLA 提供了更为真实的渲染效果。你可以在其官方网站 carla.org 阅读更多关于 CARLA 模拟器的信息。在本节中,我们将探讨如何创建一个与 OpenAI Gym 兼容的自定义汽车驾驶环境,以训练我们的学习代理。这个环境相当复杂,需要 GPU 支持才能运行——与我们之前见过的其他 Gym 环境不同。一旦你理解了如何为 CARLA 创建一个兼容 Gym 的自定义环境接口,你就能获得足够的信息,来为任何复杂的自定义环境开发接口。
CARLA 的最新版本是 CARLA 0.8.2。虽然大多数(如果不是全部的话)核心环境接口,尤其是PythonClient库,可能保持不变,但未来可能会有变更,这需要对自定义环境实现进行调整。如果发生这种情况,本书的代码库将相应更新,以支持 CARLA 的新版。在进行本章的工作时,你可能需要确保使用本书代码库中的最新版本代码(这也是订阅 GitHub 通知的另一个原因)。不过,本章讨论的自定义环境实现构建模块将普遍适用,并将指导你定义兼容 OpenAI Gym 接口的自定义环境。自定义 CARLA 环境接口的完整代码可以在本书的代码库中的ch7/carla-gym找到。
在我们开始一个兼容 Gym 的 CARLA 环境之前,让我们先看一下 CARLA 模拟器。因此,让我们首先下载 CARLA 发布的二进制文件。在接下来的部分,我们将使用VER_NUM表示版本号,因此在运行以下命令之前,请确保将VER_NUM文本替换为你正在使用的版本号:
- 首先,在你的主目录中创建一个名为
software的文件夹,可以使用以下 bash 命令:
mkdir ~/software && cd ~/software
-
使用官方发布页面上的链接下载 CARLA 的 Linux 二进制发布版本(CARLA_VER_NUM.tar.gz),该页面地址为
github.com/carla-simulator/carla/releases/tag/VER_NUM。(版本 0.8.2 的直接链接是:drive.google.com/open?id=1ZtVt1AqdyGxgyTm69nzuwrOYoPUn_Dsm。)然后,将其解压到~/software目录下。你现在应该在~/software/CARLA_VER_NUM文件夹中看到一个名为CarlaUE4.sh的文件。 -
使用以下命令将
CARLA_SERVER环境变量设置为指向你计算机上的CarlaUE4.sh:
export CARLA_SERVER=~/software/CARLA_VER_NUM/CarlaUE4.sh
现在你准备好测试运行 CARLA 驾驶模拟器了!只需执行$CARLA_SERVER,或者直接执行~/software/CARLA_VER_NUM/CarlaUE4.sh。对于 CARLA 0.8.2 版本,命令将是~/software/CARLA_0.8.2/CarlaUE4.sh。此时,你应该能看到一个 CARLA 模拟器的界面,具体如以下截图所示:

上一张截图显示的是车辆(代理)在 CARLA 的一个起始位置。下面的截图显示的是车辆在 CARLA 环境中的另一个起始位置:

一旦车辆初始化完成,你应该能够使用键盘上的w、a、s、d键来控制车辆。w键将使汽车向前行驶,a键将使汽车向左转,其他操作你大概能猜出来!
现在让我们继续,并开始实现我们的 Gym 兼容 CARLA 环境的配置和初始化。
配置与初始化
我们将首先定义一些特定于环境的配置参数,并简要了解场景配置。然后,我们将开始 CarlaEnv 类的初始化过程,该类将继承自 Gym.Env 类。
配置
首先,让我们使用字典定义环境的配置参数列表,如下所示:
# Default environment configuration
ENV_CONFIG = {
"enable_planner": True,
"use_depth_camera": False,
"discrete_actions": True,
"server_map": "/Game/Maps/" + city,
"scenarios": [scenario_config["Lane_Keep_Town2"]],
"framestack": 2, # note: only [1, 2] currently supported
"early_terminate_on_collision": True,
"verbose": False,
"render_x_res": 800,
"render_y_res": 600,
"x_res": 80,
"y_res": 80
}
scenario_config 定义了若干在创建各种驾驶场景时有用的参数。场景配置描述在 scenarios.json 文件中,该文件可以在本书的代码库中找到,路径为 ch7/carla-gym/carla_gym/envs/scenarios.json。
初始化
在 __init__ 方法中,我们定义了初始化参数以及动作和状态空间,正如我们在上一节中看到的那样,这些是必需的。实现非常直接,如下所示:
def __init__(self, config=ENV_CONFIG):
self.config = config
self.city = self.config["server_map"].split("/")[-1]
if self.config["enable_planner"]:
self.planner = Planner(self.city)
if config["discrete_actions"]:
self.action_space = Discrete(len(DISCRETE_ACTIONS))
else:
self.action_space = Box(-1.0, 1.0, shape=(2,))
if config["use_depth_camera"]:
image_space = Box(
-1.0, 1.0, shape=(
config["y_res"], config["x_res"],
1 * config["framestack"]))
else:
image_space = Box(
0.0, 255.0, shape=(
config["y_res"], config["x_res"],
3 * config["framestack"]))
self.observation_space = Tuple(
[image_space,
Discrete(len(COMMANDS_ENUM)), # next_command
Box(-128.0, 128.0, shape=(2,))]) # forward_speed, dist to goal
self._spec = lambda: None
self._spec.id = "Carla-v0"
self.server_port = None
self.server_process = None
self.client = None
self.num_steps = 0
self.total_reward = 0
self.prev_measurement = None
self.prev_image = None
self.episode_id = None
self.measurements_file = None
self.weather = None
self.scenario = None
self.start_pos = None
self.end_pos = None
self.start_coord = None
self.end_coord = None
self.last_obs = None
实现 reset 方法
正如你可能已经注意到的,在每一集开始时,我们调用 Gym 环境的 reset 方法。对于 CARLA 环境,我们希望通过 CARLA 客户端更新 CARLA 服务器,以重新启动场景。
那么,让我们继续开始实现 reset 方法。
使用 CarlaSettings 对象自定义 CARLA 仿真
当我们开始一个新的一集时,我们希望能够配置起始状态(代理或车辆的起始位置)、目标状态(代理或车辆的预定目的地)、场景的复杂度(通过场景中的车辆或行人数量来衡量)、观测的类型和来源(配置在车辆上的传感器)等。
CARLA 项目使用服务器-客户端架构管理 UE4 环境与外部配置和控制之间的接口,因此有两个服务器。
对于 CARLA 环境,我们可以使用 CarlaSettings 对象或 CarlaSettings.ini 文件配置环境的起始状态、目标状态、复杂度级别以及传感器源。
现在,让我们创建一个 CarlaSettings 对象并配置一些设置,如下所示:
settings = CarlaSettings() # Initialize a CarlaSettings object with default values
settings.set(
SynchronousMode=True,
SendNonPlayerAgentsInfo=True, # To receive info about all other objs
NumberOfVehicles=self.scenario["num_vehicles"],
NumberOfPedestrians=self.scenario["num_pedestrians"],
WeatherId=self.weather)
SynchronousMode to True to enable the synchronous mode, in which the CARLA server halts the execution of each frame until a control message is received. Control messages are based on the actions the agent takes and are sent through the CARLA client.
向 CARLA 中的车辆添加摄像头和传感器
要在 CARLA 环境中添加 RGB 彩色摄像头,请使用以下代码:
# Create a RGB Camera Object
camera1 = Camera('CameraRGB')
# Set the RGB camera image resolution in pixels
camera1.set_image_size(640, 480)
# Set the camera/sensor position relative to the car in meters
camera1.set_positions(0.25, 0, 1.30)
# Add the sensor to the Carla Settings object
settings.add_sensor(camera1)
你还可以使用以下代码片段添加深度测量传感器或摄像头:
# Create a depth camera object that can provide us the ground-truth depth of the driving scene
camera2 = Camera("CameraDepth",PostProcessing="Depth")
# Set the depth camera image resolution in pixels
camera2.set_image_size(640, 480)
# Set the camera/sensor position relative to the car in meters
camera2.set_position(0.30, 0, 1.30)
# Add the sensor to the Carla settings object
settings.add_sensor(camera)Setting up the start and end positions in the scene for the Carla Simulation
要向 CARLA 环境中添加 LIDAR,请使用以下代码:
# Create a LIDAR object. The default LIDAR supports 32 beams
lidar = Lidar('Lidar32')
# Set the LIDAR sensor's specifications
lidar.set(
Channels=32, # Number of beams/channels
Range=50, # Range of the sensor in meters
PointsPerSecond=1000000, # Sample rate
RotationFrequency=10, # Frequency of rotation
UpperFovLimit=10, # Vertical field of view upper limit angle
LowerFovLimit=-30) # Vertical field of view lower limit angle
# Set the LIDAR position & rotation relative to the car in meters
lidar.set_position(0, 0, 2.5)
lidar.set_rotation(0, 0, 0)
# Add the sensor to the Carla settings object
settings.add_sensor(lidar)
一旦我们根据期望的驾驶仿真配置创建了一个 CARLA 设置对象,我们可以将其发送到 CARLA 服务器,以设置环境并启动仿真。
一旦我们将 CARLA 设置对象发送到 CARLA 服务器,服务器会响应一个场景描述对象,其中包含可用于自我驾驶车辆的起始位置,如下所示:
scene = self.client.load_settings(settings)
available_start_spots = scene.player_start_spots
我们现在可以为主车或自车选择一个特定的起始位置,或者随机选择一个起始点,如以下代码片段所示:
start_spot = random.randint(0, max(0, available_start_spots))
我们还可以将这个起始点偏好发送到服务器,并使用以下代码片段请求启动新的一集:
self.client.start_episode(start_spot)
请注意,前一行是一个阻塞函数调用,它将在 CARLA 服务器实际启动本集之前阻塞动作。
现在,我们可以从这个起始位置开始,逐步进行,直到本集的结束。在下一节中,我们将看到我们需要实现 CARLA 环境的step()方法,这个方法用于逐步推进环境,直到本集结束:
def _reset(self):
self.num_steps = 0
self.total_reward = 0
self.prev_measurement = None
self.prev_image = None
self.episode_id = datetime.today().strftime("%Y-%m-%d_%H-%M-%S_%f")
self.measurements_file = None
# Create a CarlaSettings object. This object is a wrapper around
# the CarlaSettings.ini file. Here we set the configuration we
# want for the new episode.
settings = CarlaSettings()
# If config["scenarios"] is a single scenario, then use it if it's an array of scenarios, randomly choose one and init
self.config = update_scenarios_parameter(self.config)
if isinstance(self.config["scenarios"],dict):
self.scenario = self.config["scenarios"]
else: #ininstance array of dict
self.scenario = random.choice(self.config["scenarios"])
assert self.scenario["city"] == self.city, (self.scenario, self.city)
self.weather = random.choice(self.scenario["weather_distribution"])
settings.set(
SynchronousMode=True,
SendNonPlayerAgentsInfo=True,
NumberOfVehicles=self.scenario["num_vehicles"],
NumberOfPedestrians=self.scenario["num_pedestrians"],
WeatherId=self.weather)
settings.randomize_seeds()
if self.config["use_depth_camera"]:
camera1 = Camera("CameraDepth", PostProcessing="Depth")
camera1.set_image_size(
self.config["render_x_res"], self.config["render_y_res"])
camera1.set_position(30, 0, 130)
settings.add_sensor(camera1)
camera2 = Camera("CameraRGB")
camera2.set_image_size(
self.config["render_x_res"], self.config["render_y_res"])
camera2.set_position(30, 0, 130)
settings.add_sensor(camera2)
# Setup start and end positions
scene = self.client.load_settings(settings)
positions = scene.player_start_spots
self.start_pos = positions[self.scenario["start_pos_id"]]
self.end_pos = positions[self.scenario["end_pos_id"]]
self.start_coord = [
self.start_pos.location.x // 100, self.start_pos.location.y // 100]
self.end_coord = [
self.end_pos.location.x // 100, self.end_pos.location.y // 100]
print(
"Start pos {} ({}), end {} ({})".format(
self.scenario["start_pos_id"], self.start_coord,
self.scenario["end_pos_id"], self.end_coord))
# Notify the server that we want to start the episode at the
# player_start index. This function blocks until the server is ready
# to start the episode.
print("Starting new episode...")
self.client.start_episode(self.scenario["start_pos_id"])
image, py_measurements = self._read_observation()
self.prev_measurement = py_measurements
return self.encode_obs(self.preprocess_image(image), py_measurements)
为 CARLA 环境实现step()函数
一旦我们通过将 CARLA 设置对象发送到 CARLA 服务器并调用client.start_episode(start_spot)来初始化 CARLA 模拟器,驾驶模拟就会开始。然后,我们可以使用client.read_data()方法获取在给定步骤下模拟产生的数据。我们可以通过以下代码行来实现这一点:
measurements, sensor_data = client.read_data()
访问相机或传感器数据
我们可以通过返回的sensor_data对象的data属性,在任何给定的时间步获取传感器数据。要获取 RGB 摄像头帧,请输入以下代码:
rgb_image = sensor_data['CameraRGB'].data
rgb_image是一个 NumPy n 维数组,您可以像通常访问和操作 NumPy n 维数组一样访问和操作它。
例如,要访问 RGB 摄像头图像在(x, y)图像平面坐标处的像素值,可以使用以下代码行:
pixel_value_at_x_y = rgb_image[X, Y]
要获取深度摄像头帧,请输入以下代码:
depth_image = sensor_data['CameraDepth'].data
发送动作以控制 CARLA 中的代理
我们可以通过向 CARLA 服务器发送所需的转向、油门、刹车、手刹和倒车(档)命令,来控制 CARLA 中的汽车。下表展示了汽车在 CARLA 中将遵循的命令的值、范围和描述:
| 命令/动作名称 | 值类型,范围 | 描述 |
|---|---|---|
| 转向 | Float,[-1.0,+1.0] |
标准化转向角度 |
| 油门 | Float,[0.0,1.0] |
标准化油门输入 |
| 刹车 | Float,[0.0,1.0] |
标准化刹车输入 |
| 手刹 | Boolean,真/假 |
这告诉汽车是否启用手刹(True)或不启用(False) |
| 倒车 | Boolean,真/假 |
这告诉汽车是否处于倒档(True)或不是(False) |
如 CARLA 文档中所述,实际的转向角度将取决于车辆。例如,默认的 Mustang 车辆的最大转向角度为 70 度,这在车辆的前轮 UE4 蓝图文件中有所定义。这些是控制 CARLA 中车辆所需的五个不同命令。在这五个命令中,三者(转向、油门和刹车)是实值浮动点数。尽管它们的范围限制在-1 到+1 或 0 到 1 之间,但可能的(唯一)数值组合却是庞大的。例如,如果我们使用单精度浮动点表示油门值,该值位于 0 到 1 之间,那么总共有
种可能的不同值,这意味着油门命令有 1,056,964,608 种不同的可能值。刹车命令也是如此,因为它的值也位于 0 到 1 之间。转向命令的可能浮动值大约是其余两个命令的两倍,因为它位于-1 到+1 之间。由于单个控制消息由五个命令中每个命令的一个值组成,因此不同动作(或控制消息)的数量是每个命令唯一值的乘积,其大致顺序如下:

如你所见,这会生成一个巨大的动作空间,这可能对于一个深度学习代理来说是一个非常困难的问题,因为它需要回归到这样一个巨大的动作空间。因此,让我们简化动作空间,定义两种不同的动作空间——一种是连续空间,另一种是离散空间,这对于应用不同的强化学习算法很有用。例如,基于深度 Q 学习的算法(没有自然化优势函数)只能在离散动作空间中工作。
CARLA 中的连续动作空间
在驾驶过程中,我们通常不会同时加速和刹车;因为 CARLA 中的动作空间是连续的,而且代理将在每一步执行一个动作,所以可能只需要一个命令来同时处理加速和减速。现在,我们将油门和刹车命令合并为一个命令,其值范围从-1 到+1,其中-1 到 0 的范围用于刹车命令,0 到 1 的范围用于油门或加速命令。我们可以使用以下命令来定义它:
action_space = gym.space.Box(-1.0, 1.0, shape=2(,))
action[0]代表转向命令,action[1]代表我们合并的油门和刹车命令的值。目前,我们将hand_brake和reverse都设为 False。接下来,我们将查看如何定义一个离散的动作空间,以便选择我们希望代理执行的动作。
CARLA 中的离散动作空间
我们已经看到完整的动作空间非常大(大约是
)。你可能玩过那种只需要使用四个方向键或游戏手柄来控制速度和车头方向(汽车指向的方向)的游戏,那么为什么我们不能要求代理以类似的方式来控制汽车呢?好吧,这就是离散化动作空间的想法。虽然我们无法对汽车进行精确控制,但我们可以确保离散化后的空间能在模拟环境中提供良好的控制。
让我们从使用与连续动作空间情况类似的约定开始——在连续动作空间中,我们使用一个浮点值来表示油门(加速)和刹车(减速)动作,从而在内部使用二维有界空间。这意味着在这种情况下,动作空间可以定义如下:
action_space = gym.spaces.Discrete(NUM_DISCRETE_ACTIONS)
如你所见,NUM_DISCRETE_ACTONS 等于可用动作的数量,我们将在本节稍后定义。
然后,我们将使用二维有界空间对该空间进行离散化,并将其作为离散动作空间暴露给代理。为了在保持控制车辆的同时最小化可能的动作数量,我们使用以下动作列表:
| 动作索引 | 动作描述 | 动作数组值 |
|---|---|---|
| 0 | 滑行 | [0.0, 0.0] |
| 1 | 向左转 | [0.0, -0.5] |
| 2 | 向右转 | [0.0, 0.5] |
| 3 | 向前 | [1.0, 0.0] |
| 4 | 刹车 | [-0.5, 0.0] |
| 5 | 左转并加速 | [1.0, -0.5] |
| 6 | 向右转并加速 | [1.0, 0.5] |
| 7 | 向左转并减速 | [-0.5, -0.5] |
| 8 | 向右转并减速 | [-0.5, 0.5] |
现在,让我们在 carla_env 实现脚本中定义前述的离散动作集合,并展示如下:
DISCRETE_ACTIONS = {
0: [0.0, 0.0], # Coast
1: [0.0, -0.5], # Turn Left
2: [0.0, 0.5], # Turn Right
3: [1.0, 0.0], # Forward
4: [-0.5, 0.0], # Brake
5: [1.0, -0.5], # Bear Left & accelerate
6: [1.0, 0.5], # Bear Right & accelerate
7: [-0.5, -0.5], # Bear Left & decelerate
8: [-0.5, 0.5], # Bear Right & decelerate
}
向 CARLA 模拟服务器发送动作
现在我们已经定义了 CARLA Gym 环境的动作空间,我们可以查看如何将我们定义的连续或离散动作转换为 CARLA 模拟服务器接受的值。
由于我们在连续和离散动作空间中都遵循了相同的二维有界动作值约定,我们可以使用以下代码片段将动作转换为转向、油门和刹车命令:
throttle = float(np.clip(action[0], 0, 1)
brake = float(np.abs(np.cllip(action[0], -1, 0)
steer = float(p.clip(action[1], -1, 1)
hand_brake = False
reverse = False
如你所见,这里 action[0] 表示油门和刹车,而 action[1] 表示转向角度。
我们将利用 CARLA PythonClient 库中 CarlaClient 类的实现来处理与 CARLA 服务器的通信。如果你想了解如何使用协议缓冲区处理与服务器的通信,可以查看 ch7/carla-gym/carla_gym/envs/carla/client.py 中 CarlaClient 类的实现。
要在 CARLA 环境中实现奖励函数,请输入以下代码:
def calculate_reward(self, current_measurement):
"""
Calculate the reward based on the effect of the action taken using the previous and the current measurements
:param current_measurement: The measurement obtained from the Carla engine after executing the current action
:return: The scalar reward
"""
reward = 0.0
cur_dist = current_measurement["distance_to_goal"]
prev_dist = self.prev_measurement["distance_to_goal"]
if env.config["verbose"]:
print("Cur dist {}, prev dist {}".format(cur_dist, prev_dist))
# Distance travelled toward the goal in m
reward += np.clip(prev_dist - cur_dist, -10.0, 10.0)
# Change in speed (km/hr)
reward += 0.05 * (current_measurement["forward_speed"] - self.prev_measurement["forward_speed"])
# New collision damage
reward -= .00002 * (
current_measurement["collision_vehicles"] + current_measurement["collision_pedestrians"] +
current_measurement["collision_other"] - self.prev_measurement["collision_vehicles"] -
self.prev_measurement["collision_pedestrians"] - self.prev_measurement["collision_other"])
# New sidewalk intersection
reward -= 2 * (
current_measurement["intersection_offroad"] - self.prev_measurement["intersection_offroad"])
# New opposite lane intersection
reward -= 2 * (
current_measurement["intersection_otherlane"] - self.prev_measurement["intersection_otherlane"])
return reward
确定 CARLA 环境中剧集的结束条件
我们已经实现了meta hod来计算奖励,并定义了允许的动作、观察和自定义 CARLA 环境的重置方法。根据我们的自定义 Gym 环境创建模板,这些是我们需要实现的必要方法,用于创建与 OpenAI Gym 接口兼容的自定义环境。
虽然这是真的,但还有一件事需要我们处理,以便代理能够持续与我们的环境交互。记得我们在第五章中开发 Q-learning 代理时,实现你的第一个学习代理——解决山地车问题,针对山地车环境,环境在 200 步后会自动重置?或者在杠杆杆环境中,当杆子低于某个阈值时环境会重置?再比如在 Atari 游戏中,当代理失去最后一条命时,环境会自动重置?是的,我们需要关注决定何时重置环境的例程,目前我们在自定义 CARLA Gym 环境实现中缺少这一部分。
尽管我们可以选择任何标准来重置 CARLA Gym 环境,但有三点需要考虑,如下所示:
-
当主机或代理控制的自驾车与其他车辆、行人、建筑物或路边物体发生碰撞时,这可能是致命的(类似于 Atari 游戏中失去生命的情况)
-
当主机或自驾车达到目的地或终点目标时
-
当超出时间限制时(类似于我们在山地车 Gym 环境中的 200 时间步限制)
我们可以使用这些条件来形成决定一集结束的标准。确定.step(...)返回的done变量值的伪代码如下(完整代码可以在书籍的代码仓库ch7/carla-gym/carla_gym/envs/中找到):
# 1\. Check if a collision has occured
m = measurements_from_carla_server
collided = m["collision_vehicles"] > 0 or m["collision_pedestrians"] > 0 or m["collision_other"] > 0
# 2\. Check if the ego/host car has reached the destination/goal
planner = carla_planner
goal_reached = planner["next_command"] == "REACHED_GOAL"
# 3\. Check if the time-limit has been exceeded
time_limit = scenario_max_steps_config
time_limit_exceeded = num_steps > time_limit
# Set "done" to True if either of the above 3 criteria becomes true
done = collided or goal_reached or time_limit_exceeded
我们现在已经完成了创建基于 CARLA 驾驶模拟器的自定义 Gym 兼容环境所需的所有组件!在接下来的部分中,我们将测试这个环境,并最终看到它的实际效果。
测试 CARLA Gym 环境
为了方便测试我们环境实现的基础部分,我们将实现一个简单的main()例程,这样我们就可以像运行脚本一样运行环境。这将帮助我们验证基本接口是否已正确设置,以及环境的实际表现如何!
CarlaEnv and runs five episodes with a fixed action of going forward. The ENV_CONFIG action, which we created during initialization, can be changed to use discrete or continuous action spaces, as follows:
# Part of https://github.com/PacktPublishing/Hands-On-Intelligent-Agents-with-OpenAI-Gym/ch7/carla-gym/carla_gym/envs/carla_env.py
if __name__ == "__main__":
for _ in range(5):
env = CarlaEnv()
obs = env.reset()
done = False
t = 0
total_reward = 0.0
while not done:
t += 1
if ENV_CONFIG["discrete_actions"]:
obs, reward, done, info = env.step(3) # Go Forward
else:
obs, reward, done, info = env.step([1.0, 0.0]) # Full throttle, zero steering angle
total_reward += reward
print("step#:", t, "reward:", round(reward, 4), "total_reward:", round(total_reward, 4), "done:", done)
现在,开始测试我们刚刚创建的环境吧!请记住,CARLA 需要 GPU 才能平稳运行,并且系统环境变量CARLA_SERVER需要定义,并指向你系统中的CarlaUE4.sh文件。一旦准备好,你可以通过在rl_gym_book conda 环境中运行以下命令来测试我们创建的环境:
(rl_gym_book) praveen@ubuntu:~/rl_gym_book/ch7$ python carla-gym/carla_gym/envs/carla_env.py
上述命令应该会打开一个小的 CARLA 模拟器窗口,并初始化在 carla_env.py 脚本中使用的场景配置。这应该类似于以下截图:

如你所见,默认情况下,车辆被脚本设置为直行。请注意,carla_env.py 脚本还会产生控制台输出,显示当前环境中的时间步、计算的瞬时奖励、该回合的总奖励以及 done(True 或 False)的值,这些对测试我们的环境非常有用。当车辆开始前进时,你应该看到奖励值在增加!
控制台输出如下:

现在,你的自定义 CARLA Gym 环境已经运行了!你可以使用 ch7/carla-gym/carla_gym/envs/scenarios.json 文件中的定义创建多个不同的驾驶场景。然后,你可以为这些场景创建新的自定义 CARLA 环境,注册后可以使用常见的 gym.make(...) 命令来使用它们,例如 gym.make("Carla-v0")。
本书代码库中的代码负责通过我们之前在本章讨论的方法进行环境注册,并将其注册到 OpenAI Gym 注册表中。现在,你可以使用 OpenAI Gym 创建我们构建的自定义环境的实例。
以下截图展示了你可以用来测试自定义 Gym 环境的 Python 命令:

就是这样!其余部分和其他 Gym 环境类似。
总结
在本章中,我们逐步讲解了自定义 Gym 环境的实现,从一个模板开始,搭建了一个 OpenAI Gym 环境的基本结构,并提供了所有必要的接口供智能体使用。我们还探讨了如何在 Gym 注册表中注册自定义环境实现,使得我们可以使用熟悉的 gym.make(ENV_NAME) 命令来创建现有环境的实例。接着,我们学习了如何为基于开源驾驶模拟器 CARLA 的 UnrealEngine 创建一个与 Gym 兼容的环境实现。然后,我们快速走过了安装和运行 CARLA 所需的步骤,并开始逐步实现 CarlaEnv 类,详细涵盖了实现与 OpenAI Gym 兼容的自定义环境所涉及的重要细节。
在下一章,我们将从零开始构建一个高级智能体,通过实践示例,最终使用我们在本章创建的自定义 CARLA 环境来训练一个可以独立驾驶汽车的智能体!
第八章:使用深度演员-评论家算法实现智能-自动驾驶汽车代理
在第六章,实现一个用于最优控制的智能代理,使用深度 Q 学习,我们实现了使用深度 Q 学习的代理来解决涉及离散动作或决策的问题。我们看到它们如何被训练来玩视频游戏,比如 Atari 游戏,就像我们一样:看着游戏屏幕并按下游戏手柄/摇杆上的按钮。我们可以使用这样的代理在给定有限的选择集的情况下,做出最佳选择、做决策或执行动作,其中可能的决策或动作数量是有限的,通常较少。有许多现实世界的问题可以通过能够学习执行最优离散动作的代理来解决。我们在第六章中看到了些例子,实现一个用于最优离散控制的智能代理,使用深度 Q 学习。
在现实世界中,还有其他类别的问题和任务要求执行的动作是低级的,并且是连续值而不是离散的。例如,一个智能温控系统或恒温器需要能够对内部控制电路进行精细调整,以维持房间的指定温度。控制动作信号可能包括一个连续值的实数(例如1.456)来控制供暖、通风和空调(HVAC)系统。再考虑一个例子,我们希望开发一个智能代理来自动驾驶汽车。人类驾驶汽车时,通过换挡、踩油门或刹车踏板以及转向来操控汽车。虽然当前的档位是五到六个值中的一个,取决于汽车的变速系统,但如果一个智能软件代理必须执行所有这些动作,它必须能够为油门(加速器)、刹车(刹车)和转向产生连续值的实数。
在像这些例子中,当我们需要代理执行连续值的动作时,我们可以使用基于策略梯度的演员-评论家方法来直接学习和更新代理的策略,而不是像在第六章中看到的深度 Q 学习代理那样通过状态和/或动作值函数来进行学习,实现一个用于最优离散控制的智能代理,使用深度 Q 学习。在本章中,我们将从演员-评论家算法的基础开始,逐步构建我们的代理,同时训练它使用 OpenAI Gym 环境解决各种经典的控制问题。我们将把代理构建到能够在 CARLA 驾驶模拟环境中驾驶汽车,使用我们在上一章中实现的自定义 Gym 接口。
深度 n 步优势演员-评论者算法
在我们基于深度 Q 学习的智能代理实现中,我们使用深度神经网络作为函数逼近器来表示动作值函数。然后代理根据值函数提出策略。特别地,我们在实现中使用了
-贪婪算法。因此,我们理解最终代理必须知道在给定观测/状态时采取什么行动是好的。而不是对状态/行动函数进行参数化或逼近,然后根据该函数导出策略,我们可以直接参数化策略吗?是可以的!这正是策略梯度方法的精确思想。
在接下来的小节中,我们将简要介绍基于策略梯度的学习方法,然后转向结合价值和基于策略的学习的演员-评论者方法。然后,我们将看一些扩展到演员-评论者方法,已被证明能提高学习性能的方法。
策略梯度
在基于策略梯度的方法中,策略可以通过使用带参数的神经网络表示,例如
,目标是找到最佳参数集
。这可以直观地看作是一个优化问题,我们试图优化策略的目标,以找到表现最佳的策略。代理策略的目标是什么?我们知道,代理应该在长期内获得最大的奖励,以完成任务或实现目标。如果我们能数学化地表述这个目标,我们可以使用优化技术找到最佳策略,供代理根据给定任务遵循。
我们知道状态值函数
告诉我们从状态
开始,按照策略
直到本集结束的预期回报。它告诉我们身处状态
有多好。因此,一个良好的策略在环境中起始状态的值应较高,因为它代表了在该状态下执行策略
直至本集结束时的预期/平均/总体价值。起始状态值越高,遵循策略的代理可以获得的总长期奖励也越高。因此,在一个情节性环境中——环境即一个情节,即具有终端状态——我们可以根据起始状态的值来衡量策略的优劣。数学上,这样的目标函数可以写成如下形式:

但如果环境不是序列性的呢?这意味着它没有终止状态,并且一直持续下去。在这种环境中,我们可以使用遵循当前策略时所访问的状态的平均值
。从数学角度来看,平均值目标函数可以表示为以下形式:

这里,
是
对应的马尔可夫链的平稳分布,表示遵循策略
时访问状态
的概率。
我们还可以使用在这种环境中每个时间步获得的平均奖励,这可以通过以下方程式在数学上表示:

这本质上是当智能体根据策略
采取行动时可以获得的奖励的期望值,可以简写为如下形式:

为了使用梯度下降法优化此策略目标函数,我们将对方程关于
进行求导,找到梯度,进行反向传播,并执行梯度下降步骤。从之前的方程中,我们可以写出如下公式:

让我们通过展开项并进一步简化,求解前面方程对
的导数。按照以下方程从左到右的顺序,理解得出结果所涉及的一系列步骤:

为了理解这些方程以及如何将策略梯度
等同于似然比
,我们先回顾一下我们的目标是什么。我们的目标是找到策略的最优参数集
,使得遵循该策略的智能体能够在期望中获得最大奖励(即平均奖励)。为了实现这个目标,我们从一组参数开始,然后不断更新这些参数,直到我们达到最优参数集。为了确定在参数空间中需要更新哪些方向,我们利用策略
对参数
的梯度指示的方向。我们先从前面方程中的第二项
(这是第一项
按定义得到的结果)开始:
是在策略
下,采取动作
在状态
中获得的步长奖励的期望值的梯度。根据期望值的定义,可以将其写成如下的和式:

我们将研究似然比技巧,在此上下文中用于将该方程转化为一种使计算可行的形式。
似然比技巧
由
表示的策略假设在其非零时为可微分函数,但计算策略相对于 theta 的梯度,
,可能并不直接。我们可以在两边同时乘以和除以策略
,得到以下公式:

从微积分中,我们知道函数的对数的梯度是该函数相对于其本身的梯度,数学表达式如下:

因此,我们可以将策略相对于其参数的梯度写成以下形式:

这在机器学习中被称为似然比技巧,或对数导数技巧。
策略梯度定理
由于策略
是一个概率分布函数,它描述了给定状态和参数
下的动作概率分布,根据定义,跨状态和动作的双重求和项可以表示为通过奖励
在分布
上的得分函数的期望值。这在数学上等价于以下公式:

请注意,在前面的方程中,
是采取行动
从状态
获得的步奖励。
策略梯度定理通过用长期行动值
替换即时步奖励
,对这种方法进行了推广,可以写成如下形式:

这个结果非常有用,并且形成了多个策略梯度方法变体的基础。
通过对策略梯度的理解,我们将在接下来的几节中深入探讨演员-评论员算法及其变体。
演员-评论员算法
让我们从演员-评论员架构的图示表示开始,如下图所示:

如名称和前面的图示所示,演员-评论员算法有两个组成部分。演员负责在环境中执行动作,即根据对环境的观察并根据代理的策略采取行动。演员可以被视为策略的持有者/制定者。另一方面,评论员负责估计状态值、状态-动作值或优势值函数(取决于所使用的演员-评论员算法的变体)。让我们考虑一个例子,其中评论员试图估计动作值函数
。如果我们使用一组参数 w 来表示评论员的参数,评论员的估计值可以基本写成:

将真实的动作值函数替换为评论家对动作值函数的近似(在策略梯度定理部分的最后一个方程),从上一节的策略梯度定理结果中得到以下近似的策略梯度:

实际操作中,我们进一步通过使用随机梯度上升(或者带负号的下降)来逼近期望值。
优势演员-评论家算法
动作值演员-评论家算法仍然具有较高的方差。我们可以通过从策略梯度中减去基准函数 B(s)来减少方差。一个好的基准是状态值函数,
。使用状态值函数作为基准,我们可以将策略梯度定理的结果重写为以下形式:

我们可以定义优势函数
为以下形式:

当与基准一起用于前述的策略梯度方程时,这将给出演员-评论家策略梯度的优势:

回顾前几章,值函数的 1 步时序差分(TD)误差
给出如下:

如果我们计算这个 TD 误差的期望值,我们将得到一个方程,它类似于我们在第二章中看到的动作值函数的定义,强化学习与深度强化学习。从这个结果中,我们可以观察到,TD 误差实际上是优势函数的无偏估计,正如从左到右推导出的这个方程所示:

有了这个结果和本章迄今为止的方程组,我们已经具备了足够的理论基础,可以开始实现我们的代理!在进入代码之前,让我们先理解算法的流程,以便在脑海中对其有一个清晰的图像。
最简单的(一般/基础)优势演员-评论家算法包括以下步骤:
-
初始化(随机)策略和价值函数估计。
-
对于给定的观察/状态
,执行当前策略
规定的动作
。 -
基于得到的状态
和通过 1 步 TD 学习方程获得的奖励
,计算 TD 误差:![]()
-
-
如果
> 0,增加采取动作
的概率,因为
是一个好的决策,并且效果很好 -
如果
< 0,则减少采取动作
的概率,因为
导致了代理的表现不佳
-
-
通过调整其对
的估计值,使用 TD 误差更新评论员:
,其中
是评论员的学习率
-
将下一个状态
设置为当前状态
,并重复步骤 2。
n 步优势演员-评论员算法
在优势演员-评论员算法部分,我们查看了实现该算法的步骤。在第 3 步中,我们注意到需要基于 1 步回报(TD 目标)计算 TD 误差。这就像让智能体在环境中迈出一步,然后根据结果计算评论员估计值的误差,并更新智能体的策略。这听起来直接且简单,对吧?但是,是否有更好的方法来学习和更新策略呢?正如你从本节标题中可能猜到的那样,思路是使用 n 步回报,与基于 1 步回报的 TD 学习相比,n 步回报使用了更多的信息来学习和更新策略。n 步 TD 学习可以看作是一个广义版本,而在前一节中讨论的演员-评论员算法中使用的 1 步 TD 学习是 n 步 TD 学习算法的特例,n=1。我们来看一个简短的示例,以理解 n 步回报的计算,然后实现一个 Python 方法来计算 n 步回报,我们将在智能体实现中使用它。
n 步回报
n 步回报是一个简单但非常有用的概念,已知能够为多种强化学习算法提供更好的性能,不仅仅是优势演员-评论员算法。例如,目前在57款游戏的 Atari 套件上表现最好的算法,显著超越第二名的算法,便使用了 n 步回报。我们实际上会在第十章中讨论这个智能体算法,名为 Rainbow,探索学习环境的景观:Roboschool, Gym-Retro, StarCraft-II, DMLab。
首先,我们需要对 n 步回报过程有一个直观的理解。我们使用以下图示来说明环境中的一步。假设智能体在时间 t=1 时处于状态
,并决定采取动作
,这导致环境在时间 t=t+1=1+1=2 时过渡到状态
,同时智能体获得奖励
:

我们可以使用以下公式计算 1 步 TD 回报:

这里,
是根据价值函数(评论员)对状态
的价值估计。实质上,智能体采取一步,并利用所接收到的回报以及智能体对下一个/结果状态的价值估计的折扣值来计算回报。
如果我们让智能体继续与环境交互更多的步数,智能体的轨迹可以用以下简化的图示表示:

该图展示了智能体与环境之间的 5 步交互。采用与前一段中 1 步回报计算类似的方法,我们可以使用以下公式计算 5 步回报:

然后,我们可以在优势演员-评论员算法的步骤 3 中使用此作为 TD 目标,以提高智能体的性能。
你可以通过在任意 Gym 环境中运行 ch8/a2c_agent.py 脚本,设置parameters.json文件中的learning_step_thresh参数为 1(用于 1 步回报)和 5 或 10(用于 n 步回报),来比较具有 1 步回报和 n 步回报的优势演员-评论员智能体的性能。
例如,你可以运行
(rl_gym_book) praveen@ubuntu:~/HOIAWOG/ch8$python a2c_agent.py --env Pendulum-v0 使用learning_step_thresh=1,通过以下命令使用 Tensorboard 监控其性能
(rl_gym_book) praveen@ubuntu:~/HOIAWOG/ch8/logs$tensorboard --logdir=., 然后在大约一百万步后,你可以比较使用learning_step_thresh=10训练的智能体的性能。请注意,训练后的智能体模型将保存在~/HOIAWOG/ch8/trained_models/A2_Pendulum-v0.ptm。你可以在开始第二次运行之前重命名它或将其移动到不同的目录,以便从头开始训练!
为了使概念更加明确,让我们讨论如何在步骤 3 中以及在优势演员-评论员算法中使用它。我们将首先使用 n 步回报作为 TD 目标,并使用以下公式计算 TD 误差(算法的步骤 3):

然后,我们将按照前一小节中讨论的算法步骤 4 更新评论员。接着,在步骤 5 中,我们将使用以下更新规则来更新评论员:

然后,我们将继续进行算法的步骤 6,使用来自
的 5 步过渡,直到
,并计算 5 步回报,然后重复更新
的过程。
实现 n 步回报计算
如果我们暂停片刻并分析一下发生了什么,你可能会发现我们可能没有充分利用这条 5 步长的轨迹。从状态
开始的代理 5 步长轨迹提供了信息,但我们最终只学到了一个新信息,这仅仅是关于
来更新演员和评论员(
)。其实,我们可以通过使用相同的 5 步轨迹来计算轨迹中每个状态值的更新,并根据轨迹结束的 n 值使学习过程更加高效。例如,在一个简化的轨迹表示中,如果我们考虑状态
,并将轨迹的前视部分包含在气泡中,如下图所示:

我们可以使用气泡中的信息来提取状态
的 TD 学习目标。在这种情况下,由于从
处只能获得一步信息,我们将计算 1 步回报,如下方程所示:

如我们之前讨论过的,我们可以使用该值作为方程中的 TD 目标,以获得另一个 TD 误差值,并利用第二个值来更新演员和
,以及之前的更新(
)。现在,我们又为代理提供了一个新的学习信息!
如果我们应用相同的直觉,考虑状态
,并将轨迹的前视部分包含在气泡中,如下图所示:

我们可以使用气泡中的信息来提取
的 TD 学习目标。在这种情况下,从
获得了两种信息,因此我们将使用以下方程计算 2 步回报:

如果我们看一下这个方程和前一个方程,我们可以观察到
和
之间的关系,公式如下所示:

这为代理提供了另一个学习的信息。同样,我们可以从这条轨迹中提取更多的信息。将相同的概念扩展到
和
,我们可以得到以下关系:

同样,简而言之,我们可以观察到以下内容:

最后,我们还可以观察到以下内容:

简而言之,我们可以从轨迹中的最后一步开始,计算 n 步回报直到轨迹结束,然后回到上一部来使用之前计算的值计算回报。
实现过程直接且简单,建议自己尝试实现。这些内容提供如下,供参考:
def calculate_n_step_return(self, n_step_rewards, final_state, done, gamma):
"""
Calculates the n-step return for each state in the input-trajectory/n_step_transitions
:param n_step_rewards: List of rewards for each step
:param final_state: Final state in this n_step_transition/trajectory
:param done: True rf the final state is a terminal state if not, False
:return: The n-step return for each state in the n_step_transitions
"""
g_t_n_s = list()
with torch.no_grad():
g_t_n = torch.tensor([[0]]).float() if done else self.critic(self.preproc_obs(final_state)).cpu()
for r_t in n_step_rewards[::-1]: # Reverse order; From r_tpn to r_t
g_t_n = torch.tensor(r_t).float() + self.gamma * g_t_n
g_t_n_s.insert(0, g_t_n) # n-step returns inserted to the left to maintain correct index order
return g_t_n_s
深度 n 步优势演员-评论家算法
我们观察到演员-评论家算法结合了基于价值的方法和基于策略的方法。评论家估计价值函数,演员遵循策略,我们研究了如何更新演员和评论家。通过我们在第六章中,使用深度 Q 学习实现最优离散控制的智能代理的经验,我们自然产生了使用神经网络来逼近价值函数,从而代替评论家的想法。我们还可以使用神经网络来表示策略!,在这种情况下,参数!是神经网络的权重。使用深度神经网络来逼近演员和评论家的方法,正是深度演员-评论家算法的核心思想。
实现深度 n 步优势演员评论家代理
我们已经准备好所有实现深度 n 步优势演员-评论家(A2C)代理所需的背景信息。让我们先看看代理实现过程的概述,然后直接进入实际的实现过程。
以下是我们 A2C 代理的高级流程:
-
初始化演员和评论家的网络。
-
使用演员的当前策略从环境中收集 n 步经验并计算 n 步回报。
-
计算演员和评论家的损失。
-
执行随机梯度下降优化步骤以更新演员和评论家的参数。
-
从第 2 步开始重复。
我们将在一个名为DeepActorCriticAgent的 Python 类中实现该代理。你可以在本书的代码仓库中的第八章找到完整的实现:ch8/a2c_agent.py。我们将使该实现具有灵活性,以便我们可以轻松扩展它,进一步实现批处理版本,并且还可以实现异步版本的 n 步优势演员-评论家代理。
初始化演员和评论家的网络
DeepActorCriticAgent类的初始化很直接。我们将快速浏览它,然后查看我们如何实际定义和初始化演员和评论家的网络。
代理的初始化函数如下所示:
class DeepActorCriticAgent(mp.Process):
def __init__(self, id, env_name, agent_params):
"""
An Advantage Actor-Critic Agent that uses a Deep Neural Network to represent it's Policy and the Value function
:param id: An integer ID to identify the agent in case there are multiple agent instances
:param env_name: Name/ID of the environment
:param agent_params: Parameters to be used by the agent
"""
super(DeepActorCriticAgent, self).__init__()
self.id = id
self.actor_name = "actor" + str(self.id)
self.env_name = env_name
self.params = agent_params
self.policy = self.multi_variate_gaussian_policy
self.gamma = self.params['gamma']
self.trajectory = [] # Contains the trajectory of the agent as a sequence of Transitions
self.rewards = [] # Contains the rewards obtained from the env at every step
self.global_step_num = 0
self.best_mean_reward = - float("inf") # Agent's personal best mean episode reward
self.best_reward = - float("inf")
self.saved_params = False # Whether or not the params have been saved along with the model to model_dir
self.continuous_action_space = True #Assumption by default unless env.action_space is Discrete
你可能会想知道为什么agent类继承自multiprocessing.Process类。虽然在我们的第一个代理实现中,我们将一个代理运行在一个进程中,但我们可以利用这个灵活的接口,启用并行运行多个代理,从而加速学习过程。
接下来我们将介绍使用 PyTorch 操作定义的神经网络实现的演员和评论员。按照与我们在第六章的深度 Q 学习智能体相似的代码结构,使用深度 Q 学习实现最优离散控制的智能体,在代码库中你会看到我们使用一个名为function_approximator的模块来包含我们的基于神经网络的函数逼近器实现。你可以在本书代码库中的ch8/function_approximator文件夹下找到完整的实现。
由于一些环境具有较小且离散的状态空间,例如Pendulum-v0、MountainCar-v0或CartPole-v0环境,我们还将实现浅层版本的神经网络,并与深层版本一同使用,以便根据智能体训练/测试所用的环境动态选择合适的神经网络。当你查看演员的神经网络示例实现时,你会注意到在shallow和deep函数逼近器模块中都有一个名为Actor的类和一个不同的类叫做DiscreteActor。这是为了通用性,方便我们根据环境的动作空间是连续的还是离散的,让智能体动态选择和使用最适合表示演员的神经网络。为了实现智能体的完整性和通用性,你还需要了解另外一个变体:我们实现中的shallow和deep函数逼近器模块都有一个ActorCritic类,它是一个单一的神经网络架构,既表示演员又表示评论员。这样,特征提取层在演员和评论员之间共享,神经网络中的不同头(最终层)用于表示演员和评论员。
有时,实现的不同部分可能会令人困惑。为了帮助避免困惑,以下是我们基于神经网络的演员-评论员实现中各种选项的总结:
| 模块/类 | 描述 | 目的/用例 | |
|---|---|---|---|
1. function_approximator.shallow |
用于演员-评论员表示的浅层神经网络实现。 | 具有低维状态/观察空间的环境。 | |
1.1 function_approximator.shallow.Actor |
前馈神经网络实现,输出两个连续值:mu(均值)和 sigma(标准差),用于基于高斯分布的策略表示。 | 低维状态/观察空间和连续动作空间。 | |
1.2 function_approximator.shallow.DiscreteActor |
前馈神经网络,为动作空间中的每个动作输出一个 logit。 | 低维状态/观察空间和离散动作空间。 | |
1.3 function_approximator.shallow.Critic |
前馈神经网络,输出一个连续值。 | 用于表示评论员/值函数,适用于低维状态/观测空间的环境。 | |
1.4 function_approximator.shallow.ActorCritic |
前馈神经网络,输出高斯分布的均值(mu)和标准差(sigma),以及一个连续值。 | 用于表示同一网络中的演员(actor)和评论员(critic),适用于低维状态/观测空间的环境。也可以将其修改为离散的演员-评论员网络。 | |
2. function_approximator.deep |
深度神经网络实现,用于演员(actor)和评论员(critic)表示。 | 适用于具有高维状态/观测空间的环境。 | |
2.1 function_approximator.deep.Actor |
深度卷积神经网络,输出基于高斯分布的策略表示的均值(mu)和标准差(sigma)。 | 高维状态/观测空间和连续动作空间。 | |
2.2 function_approximator.deep.DiscreteActor |
深度卷积神经网络,为动作空间中的每个动作输出一个 logit 值。 | 高维状态/观测空间和离散动作空间。 | |
2.3 function_approximator.deep.Critic |
深度卷积神经网络,输出一个连续值。 | 用于表示评论员/值函数,适用于高维状态/观测空间的环境。 | |
2.4 function_approximator.deep.ActorCritic |
深度卷积神经网络,输出高斯分布的均值(mu)和标准差(sigma),以及一个连续值。 | 用于表示同一网络中的演员(actor)和评论员(critic),适用于高维状态/观测空间的环境。也可以将其修改为离散的演员-评论员网络。 |
现在让我们看看run()方法的第一部分,在这里我们根据环境的状态和动作空间的类型,以及根据先前表格中状态空间是低维还是高维的不同,来初始化演员(actor)和评论员(critic)网络:
from function_approximator.shallow import Actor as ShallowActor
from function_approximator.shallow import DiscreteActor as ShallowDiscreteActor
from function_approximator.shallow import Critic as ShallowCritic
from function_approximator.deep import Actor as DeepActor
from function_approximator.deep import DiscreteActor as DeepDiscreteActor
from function_approximator.deep import Critic as DeepCritic
def run(self):
self.env = gym.make(self.env_name)
self.state_shape = self.env.observation_space.shape
if isinstance(self.env.action_space.sample(), int): # Discrete action space
self.action_shape = self.env.action_space.n
self.policy = self.discrete_policy
self.continuous_action_space = False
else: # Continuous action space
self.action_shape = self.env.action_space.shape[0]
self.policy = self.multi_variate_gaussian_policy
self.critic_shape = 1
if len(self.state_shape) == 3: # Screen image is the input to the agent
if self.continuous_action_space:
self.actor= DeepActor(self.state_shape, self.action_shape, device).to(device)
else: # Discrete action space
self.actor = DeepDiscreteActor(self.state_shape, self.action_shape, device).to(device)
self.critic = DeepCritic(self.state_shape, self.critic_shape, device).to(device)
else: # Input is a (single dimensional) vector
if self.continuous_action_space:
#self.actor_critic = ShallowActorCritic(self.state_shape, self.action_shape, 1, self.params).to(device)
self.actor = ShallowActor(self.state_shape, self.action_shape, device).to(device)
else: # Discrete action space
self.actor = ShallowDiscreteActor(self.state_shape, self.action_shape, device).to(device)
self.critic = ShallowCritic(self.state_shape, self.critic_shape, device).to(device)
self.actor_optimizer = torch.optim.Adam(self.actor.parameters(), lr=self.params["learning_rate"])
self.critic_optimizer = torch.optim.Adam(self.critic.parameters(), lr=self.params["learning_rate"])
使用当前策略收集 n 步经验
下一步是执行所谓的rollouts,即使用当前策略让代理收集n步转移。这个过程本质上是让代理与环境进行交互,并生成新的经验,通常表示为一个包含状态、动作、奖励以及下一个状态的元组,简写为(, , , ),如下图所示:

在前面的示意图中,代理将用如下方式填充它的self.trajectory,该列表包含五个转移:[T1, T2, T3, T4, T5]。
在我们的实现中,我们将使用稍微修改过的转换表示法,以减少冗余的计算。我们将使用以下定义来表示转换:
Transition = namedtuple("Transition", ["s", "value_s", "a", "log_prob_a"])
这里,s 是状态,value_s 是评论家对状态 s 的值的预测,a 是采取的行动,log_prob_a 是根据演员/代理当前策略采取行动 a 的概率的对数。
我们将使用之前实现的 calculate_n_step_return(self, n_step_rewards, final_state, done, gamma) 方法,基于包含每个步骤获得的标量奖励值的 n_step_rewards 列表和用于计算评论家对轨迹中最后/最终状态的估计值的 final_state,来计算 n 步回报,正如我们在 n 步回报计算部分中讨论过的那样。
计算演员和评论家的损失
从我们之前讨论的 n 步深度演员-评论家算法的描述中,你可能记得评论家是通过神经网络来表示的,它试图解决一个问题,这个问题类似于我们在第六章中看到的,使用深度 Q 学习实现最优离散控制的智能体,即表示值函数(类似于我们在本章中使用的动作值函数,但稍微简单一些)。我们可以使用标准的均方误差(MSE)损失,或者使用平滑的 L1 损失/Huber 损失,这些损失是基于评论家的预测值和在前一步计算的 n 步回报(TD 目标)计算的。
对于演员(actor),我们将使用通过策略梯度定理得到的结果,特别是优势演员-评论家版本,在这种方法中,优势值函数被用来指导演员策略的梯度更新。我们将使用 TD_error,它是优势值函数的无偏估计。
总结来说,评论家和演员的损失如下:
-
critic_loss = MSE(
, critic_prediction) -
actor_loss = log(
) * TD_error
在捕获了主要的损失计算方程后,我们可以通过 calculate_loss(self, trajectory, td_targets) 方法在代码中实现它们,以下是代码片段的示例:
def calculate_loss(self, trajectory, td_targets):
"""
Calculates the critic and actor losses using the td_targets and self.trajectory
:param td_targets:
:return:
"""
n_step_trajectory = Transition(*zip(*trajectory))
v_s_batch = n_step_trajectory.value_s
log_prob_a_batch = n_step_trajectory.log_prob_a
actor_losses, critic_losses = [], []
for td_target, critic_prediction, log_p_a in zip(td_targets, v_s_batch, log_prob_a_batch):
td_err = td_target - critic_prediction
actor_losses.append(- log_p_a * td_err) # td_err is an unbiased estimated of Advantage
critic_losses.append(F.smooth_l1_loss(critic_prediction, td_target))
#critic_loss.append(F.mse_loss(critic_pred, td_target))
if self.params["use_entropy_bonus"]:
actor_loss = torch.stack(actor_losses).mean() - self.action_distribution.entropy().mean()
else:
actor_loss = torch.stack(actor_losses).mean()
critic_loss = torch.stack(critic_losses).mean()
writer.add_scalar(self.actor_name + "/critic_loss", critic_loss, self.global_step_num)
writer.add_scalar(self.actor_name + "/actor_loss", actor_loss, self.global_step_num)
return actor_loss, critic_loss
更新演员-评论家模型
在我们计算了演员和评论家的损失后,学习过程的下一步也是最后一步,是基于损失更新演员和评论家的参数。由于我们使用了强大的 PyTorch 库,它自动处理部分微分、误差反向传播和梯度计算,因此在使用前面步骤的结果时,代码实现简单而直接,如下所示的代码示例所示:
def learn(self, n_th_observation, done):
td_targets = self.calculate_n_step_return(self.rewards, n_th_observation, done, self.gamma)
actor_loss, critic_loss = self.calculate_loss(self.trajectory, td_targets)
self.actor_optimizer.zero_grad()
actor_loss.backward(retain_graph=True)
self.actor_optimizer.step()
self.critic_optimizer.zero_grad()
critic_loss.backward()
self.critic_optimizer.step()
self.trajectory.clear()
self.rewards.clear()
保存/加载、日志记录、可视化和监控工具
在前面的章节中,我们走访了智能体学习算法实现的核心部分。除了这些核心部分,还有一些实用函数,我们将使用它们在不同的学习环境中训练和测试智能体。我们将重用在第六章中已经开发的组件,使用深度 Q 学习实现最优离散控制的智能体,例如utils.params_manager,以及save()和load()方法,它们分别用于保存和加载智能体训练好的大脑或模型。我们还将使用日志工具来记录智能体的进展,以便使用 Tensorboard 进行良好的可视化和快速查看,还可以用于调试和监控,以观察智能体的训练过程中是否存在问题。
有了这些,我们就可以完成 n 步优势演员-评论员智能体的实现!你可以在ch8/a2c_agent.py文件中找到完整的实现。在我们看看如何训练智能体之前,在下一节中,我们将快速了解一下可以应用于深度 n 步优势智能体的一个扩展,以使它在多核机器上表现得更好。
扩展 - 异步深度 n 步优势演员-评论员
我们可以对智能体实现做的一个简单扩展是,启动多个智能体实例,每个实例都有自己的学习环境实例,并以异步方式返回它们从中学到的更新,也就是说,它们在有可用更新时发送,而无需进行时间同步。这种算法通常被称为 A3C 算法,A3C 是异步优势演员-评论员的缩写。
这个扩展背后的动机之一来源于我们在第六章中学到的内容,使用深度 Q 学习实现最优离散控制的智能体,特别是使用经验回放内存。我们的深度 Q 学习智能体在加入经验回放内存后,学习效果显著提升,本质上它帮助解决了序列决策问题中的依赖性问题,并使智能体能从过去的经验中提取更多的信息。类似地,使用多个并行运行的演员-学习者实例的想法,也被发现有助于打破过渡之间的相关性,并有助于探索环境状态空间的不同部分,因为每个演员-学习者进程都有自己的策略参数和环境实例来进行探索。一旦并行运行的智能体实例有一些更新需要发送回来,它们会将这些更新发送到一个共享的全局智能体实例,这个全局实例作为其他智能体实例同步的新参数来源。
我们可以使用 Python 的 PyTorch 多进程库来实现这个扩展。没错!你猜对了。这正是我们在实现中让DeepActorCritic代理一开始就继承了torch.multiprocessing.Process的原因,这样我们就可以在不进行重大代码重构的情况下将这个扩展添加到其中。如果你有兴趣,可以查看书籍代码仓库中的ch8/README.md文件,获取更多关于探索这一架构的资源。
我们可以轻松扩展a2c_agent.py中 n 步优势演员-评论员代理的实现,来实现同步深度 n 步优势演员-评论员代理。你可以在ch8/async_a2c_agent.py中找到异步实现。
训练一个智能和自主驾驶的代理
现在我们已经具备了实现本章目标所需的所有部分,目标是组建一个智能的自主驾驶代理,并训练它在我们在上一章中开发的、使用 Gym 接口构建的真实感 CARLA 驾驶环境中自主驾驶。代理的训练过程可能需要一段时间。根据你用来训练代理的机器硬件,训练可能需要从几个小时(例如Pendulum-v0、CartPole-v0和一些 Atari 游戏等简单环境)到几天(例如 CARLA 驾驶环境等复杂环境)。为了先了解训练过程以及在训练期间如何监控进展,我们将从一些简单的示例开始,逐步走过训练和测试代理的整个过程。然后,我们将展示如何轻松地将其转移到 CARLA 驾驶环境中,进一步进行训练。
训练和测试深度 n 步优势演员-评论员代理
因为我们的代理实现是通用的(如上一节第 1 步中通过表格讨论的那样),我们可以使用任何具有 Gym 兼容接口的学习环境来训练/测试代理。你可以在本书初期章节中讨论的各种环境中进行实验并训练代理,下一章我们还会讨论一些更有趣的学习环境。别忘了我们的自定义 CARLA 自动驾驶环境!
我们将选择一些环境作为示例,逐步展示如何启动训练和测试过程,帮助你开始自己的实验。首先,更新你从书籍代码仓库 fork 的代码,并cd到ch8文件夹,这里存放着本章的代码。像往常一样,确保激活为本书创建的 conda 环境。完成这些后,你可以使用a2c_agent.py脚本启动 n 步优势演员评论员代理的训练过程,示例如下:
(rl_gym_book) praveen@ubuntu:~/HOIAWOG/ch8$ python a2c_agent --env Pendulum-v0
你可以将Pendulum-v0替换为任何在你的机器上设置好的兼容 Gym 的学习环境名称。
这应该会启动代理的训练脚本,它将使用~/HOIAWOG/ch8/parameters.json文件中指定的默认参数(你可以更改这些参数以进行实验)。如果有可用的已训练代理的大脑/模型,它还会从~/HOIAWOG/ch8/trained_models目录加载适用于指定环境的模型,并继续训练。对于高维状态空间环境,例如 Atari 游戏或其他环境,其中状态/观察是场景的图像或屏幕像素,将使用我们在之前章节中讨论的深度卷积神经网络,这将利用你机器上的 GPU(如果有的话)来加速计算(如果你希望禁用此功能,可以在parameters.json文件中将use_cuda = False)。如果你的机器上有多个 GPU,并且希望在不同的 GPU 上训练不同的代理,可以通过--gpu-id标志指定 GPU 设备 ID,要求脚本在训练/测试时使用特定的 GPU。
一旦训练过程开始,你可以通过从logs目录运行以下命令来启动tensorboard,监控代理的进程:
(rl_gym_book) praveen@ubuntu:~/HOIAWOG/ch8/logs$ tensorboard --logdir .
在使用上述命令启动tensorboard后,你可以访问http://localhost:6006网页来监控代理的进展。这里提供了示例截图供你参考;这些截图来自两次 n 步优势演员-评论员代理的训练运行,使用了不同的n步值,并使用了parameters.json文件中的learning_step_threshold参数:
演员-评论员(使用单独的演员和评论员网络):
-
Pendulum-v0; n-step (learning_step_threshold = 100)

2. - Pendulum-v0; n-step (learning_step_threshold = 5)

- 比较 1(100 步 AC,绿色)和 2(5 步 AC,灰色)在
Pendulum-v0上进行 1000 万步训练:

训练脚本还会将训练过程的摘要输出到控制台。如果你想可视化环境,以查看代理正在做什么或它是如何学习的,可以在启动训练脚本时在命令中添加--render标志,如下所示:
(rl_gym_book) praveen@ubuntu:~/HOIAWOG/ch8$ python a2c_agent --env CartPole-v0 --render
如你所见,我们已经达到一个阶段,你只需一条命令即可开始训练、记录并可视化代理的表现!到目前为止,我们已经取得了非常好的进展。
您可以在相同的环境或不同的环境上使用不同参数集运行多个代理实验。前面的示例被选择来展示其在更简单的环境中的表现,以便您可以轻松地运行全长实验并重现和比较结果,无论您可能拥有的硬件资源如何。作为本书代码库的一部分,针对一些环境提供了训练代理脑/模型,以便您可以快速启动并在测试模式下运行脚本,查看训练代理在任务中的表现。它们可以在您 fork 的书籍代码库的ch8/trianed_models文件夹中找到,或者在此处的上游源:github.com/PacktPublishing/Hands-On-Intelligent-Agents-with-OpenAI-Gym/tree/master/ch8/trained_models。您还会在书籍代码库中找到其他资源,例如其他环境中的学习曲线插图和代理在各种环境中表现的视频剪辑,供您参考。
一旦您准备好测试代理,可以使用您自己训练的代理的脑模型或使用预训练的代理脑,您可以使用--test标志来表示您希望禁用学习并在测试模式下运行代理。例如,要在LunarLander-v2环境中测试代理,并且打开学习环境的渲染,您可以使用以下命令:
(rl_gym_book) praveen@ubuntu:~/HOIAWOG/ch8$ python a2c_agent --env LunarLander-v2 --test --render
我们可以互换地使用我们讨论的异步代理作为我们基础代理的扩展。由于两种代理实现都遵循相同的结构和配置,我们可以通过仅使用async_a2c_agent.py脚本来轻松切换到异步代理训练脚本。它们甚至支持相同的命令行参数,以简化我们的工作。当使用async_a2c_agent.py脚本时,您应确保根据您希望代理使用的进程数或并行实例数在parameters.json文件中设置num_agents参数。例如,我们可以使用以下命令在BipedalWalker-v2环境中训练我们的异步代理版本:
(rl_gym_book) praveen@ubuntu:~/HOIAWOG/ch8$ python async_a2c_agent --env BipedalWalker-v2
正如您可能已经意识到的那样,我们的代理实现能够学习在各种不同的环境中行动,每个环境都有其自己的任务集,以及它们自己的状态、观察和行动空间。正是这种多功能性使得基于深度强化学习的代理变得流行,并适合解决各种问题。现在我们已经熟悉了训练过程,我们终于可以开始训练代理驾驶汽车,并跟随 CARLA 驾驶模拟器中的车道行驶。
在 CARLA 驾驶模拟器中训练代理驾驶汽车。
让我们开始在 CARLA 驾驶环境中训练一个代理!首先,确保你的 GitHub 分支是最新的,已经与上游主分支同步,这样你就能获得来自书籍仓库的最新代码。由于我们在上一章创建的 CARLA 环境与 OpenAI Gym 接口兼容,因此使用 CARLA 环境进行训练就像使用任何其他 Gym 环境一样简单。你可以使用以下命令训练 n 步优势演员-评论家代理:
(rl_gym_book) praveen@ubuntu:~/HOIAWOG/ch8$ python a2c_agent --env Carla-v0
这将启动代理的训练过程,像我们之前看到的那样,进度摘要将被打印到控制台窗口,并且日志会被写入logs文件夹,你可以使用tensorboard查看它们。
在训练过程的初期阶段,你会注意到代理驾驶汽车像疯了一样!
经过几小时的训练,你会看到代理能够控制汽车,成功地沿着道路行驶,同时保持在车道内并避免撞到其他车辆。你可以在ch8/trained_models文件夹中找到一个经过训练的自动驾驶代理模型,方便你快速让代理进行测试驾驶!你还可以在书籍的代码仓库中找到更多资源和实验结果,帮助你的学习和实验。祝你实验愉快!
总结
在本章中,我们从基础开始,动手实践了基于演员-评论家架构的深度强化学习代理。我们从策略梯度方法的介绍开始,逐步讲解了表示策略梯度优化的目标函数、理解似然比技巧,最后推导出策略梯度定理。接着,我们了解了演员-评论家架构如何利用策略梯度定理,并使用演员组件表示代理的策略,使用评论家组件表示状态/动作/优势值函数,具体实现取决于架构的实现方式。在对演员-评论家架构有了直观的理解之后,我们进入了 A2C 算法,并讨论了其中涉及的六个步骤。然后,我们通过图示讨论了 n 步回报的计算,并展示了如何在 Python 中轻松实现 n 步回报计算方法。接着,我们逐步实现了深度 n 步优势演员-评论家代理。
我们还讨论了如何使实现变得灵活和通用,以适应各种不同的环境,这些环境可能具有不同的状态、观察和动作空间维度,并且可能是连续的或离散的。接着,我们探讨了如何在不同进程中并行运行多个智能体实例,以提高学习性能。在最后一部分,我们走过了训练智能体过程中涉及的步骤,一旦智能体训练完成,我们可以使用--test和--render标志来测试智能体的表现。我们从简单的环境开始,以便熟悉训练和监控过程,最终实现了本章的目标——在 CARLA 驾驶模拟器中训练一个智能体,使其能够自主驾驶一辆车!希望你在阅读这一相对较长的章节时学到了很多东西。到此为止,你已经积累了理解和实现本章以及第六章中两大类高性能学习智能体算法的经验,《使用深度 Q 学习实现最优离散控制的智能体》。在下一章,我们将探索新的有前景的学习环境,在这些环境中,你可以训练自定义智能体,并开始朝着下一个层次取得进展。
第九章:探索学习环境的全景 - Roboschool、Gym-Retro、StarCraft-II、DeepMindLab
在你不断积累经验的过程中,已经走过了很长一段路,目的是通过动手实践构建智能体,解决各种具有挑战性的问题。在前几章中,我们探讨了 OpenAI Gym 中提供的几个环境。在本章中,我们将超越 Gym,看看一些其他开发完善的环境,供你训练智能体或进行实验。
在我们查看其他提供良好学习环境的开源库,以帮助开发智能体之前,先来看一下最近添加到 OpenAI Gym 库中的一类环境。如果你像我一样对机器人技术感兴趣,你一定会非常喜欢这个。没错!它就是机器人环境类,提供了许多非常有用的环境,用于机器人臂执行抓取、滑动、推动等操作。这些机器人环境基于 MuJoCo 引擎,你可能还记得在第三章, 《OpenAI Gym 和深度强化学习入门》中提到过,MuJoCo 引擎需要付费许可,除非你是学生,并且仅用于个人或课堂用途。以下截图总结了这些机器人环境,包括每个环境的名称和简要描述,供你参考,如果你有兴趣探索这些问题:

与 Gym 接口兼容的环境
在本节中,我们将深入探讨与 Gym 接口完全兼容的环境。你应该能够在这些环境中使用我们在前几章中开发的任何智能体。让我们开始,看看一些非常有用且充满前景的学习环境。
Roboschool
Roboschool (https://github.com/openai/roboschool) 提供了几个用于仿真控制机器人的环境。它由 OpenAI 发布,且这些环境与我们在本书中使用的 OpenAI Gym 环境接口相同。Gym 的基于 MuJoCo 的环境提供了丰富多样的机器人任务,但 MuJoCo 在免费试用期过后需要许可证。Roboschool 提供了八个环境,这些环境与 MuJoCo 环境非常相似,这是一个好消息,因为它提供了一个免费的替代方案。除了这八个环境,Roboschool 还提供了几个新的、具有挑战性的环境。
以下表格展示了 MuJoCo Gym 环境与 Roboschool 环境之间的快速对比:
| 简要描述 | MuJoCo 环境 | Roboschool 环境 |
|---|---|---|
| 让一只单腿 2D 机器人尽可能快地向前跳跃 | Hopper-v2![]() |
RoboschoolHopper-v1![]() |
| 让一个 2D 机器人行走 | Walker2d-v2![]() |
RoboschoolWalker2d-v1![]() |
| 让一个四足 3D 机器人行走 | Ant-v2![]() |
RoboschoolAnt-v1![]() |
| 让一个双足 3D 机器人尽可能快地行走而不摔倒 | Humanoid-v2![]() |
RoboschoolHumanoid-v1![]() |
以下表格提供了 Roboschool 库中可用环境的完整列表,包括它们的状态空间和动作空间,供您快速参考:
| 环境 ID | Roboschool 环境 | 观察空间 | 动作空间 |
|---|---|---|---|
| RoboschoolInvertedPendulum-v1 | ![]() |
Box(5,) | Box(1,) |
| RoboschoolInvertedPendulumSwingup-v1 | ![]() |
Box(5,) | Box(1,) |
| RoboschoolInvertedDoublePendulum-v1 | ![]() |
Box(9,) | Box(1,) |
| RoboschoolReacher-v1 | ![]() |
Box(9,) | Box(2,) |
| RoboschoolHopper-v1 | ![]() |
Box(15,) | Box(3,) |
| RoboschoolWalker2d-v1 | ![]() |
Box(22,) | Box(6,) |
| RoboschoolHalfCheetah-v1 | ![]() |
Box(26,) | Box(6,) |
| RoboschoolAnt-v1 | ![]() |
Box(28,) | Box(8,) |
| RoboschoolHumanoid-v1 | ![]() |
Box(44,) | Box(17,) |
| RoboschoolHumanoidFlagrun-v1 | ![]() |
Box(44,) | Box(17,) |
| RoboschoolHumanoidFlagrunHarder-v1 | ![]() |
Box(44,) | Box(17,) |
| RoboschoolPong-v1 | ![]() |
Box(13,) | Box(2,) |
快速入门指南:设置和运行 Roboschool 环境
Roboschool 环境使用开源的 Bulletphysics 引擎,而不是专有的 MuJoCo 引擎。让我们快速浏览一个 Roboschool 环境,以便您了解如何使用 Roboschool 库中的任何环境,如果您觉得它对您的工作有帮助的话。首先,我们需要在rl_gym_book conda 环境中安装 Roboschool Python 库。由于该库依赖于多个组件,包括 Bulletphysics 引擎,因此安装过程涉及几个步骤,具体步骤可以在官方 Roboschool GitHub 仓库中找到:github.com/openai/roboschool。为了简化操作,您可以使用本书代码仓库中的脚本ch9/setup_roboschool.sh,该脚本会自动编译和安装Roboschool库。请按照以下步骤运行该脚本:
-
激活
rl_gym_bookconda 环境,使用source activate rl_gym_book。 -
使用
cd ch9导航到ch9文件夹。 -
确保脚本的执行权限已设置为
chmod a+x setup_roboschool.sh。 -
使用
sudo运行脚本:./setup_roboschool.sh。
这应该会安装所需的系统依赖项,获取并编译兼容的 bullet3 物理引擎源代码;将 Roboschool 源代码拉取到您主目录下的software文件夹;最后在rl_gym_book conda 环境中编译、构建并安装 Roboschool 库。如果设置成功完成,您将在控制台看到以下信息:
Setup completed successfully. You can now import roboschool and use it. If you would like to \test the installation, you can run: python ~/roboschool/agent_zoo/demo_race2.py"
您可以使用以下命令运行一个快速入门演示脚本:
`(rl_gym_book) praveen@ubuntu:~$ python ~/roboschool/agent_zoo/demo_race2.py`
这将启动一场有趣的机器人竞赛,您将看到一只跳跃者、半猎豹和类人机器人进行比赛!有趣的是,每个机器人都由基于强化学习训练的策略控制。比赛将看起来像这个快照:

安装完成后,您可以创建一个 Roboschool 环境,并使用我们在前面章节中开发的代理来训练并在这些环境中运行。
您可以使用本章代码仓库中的run_roboschool_env.py脚本 github.com/PacktPublishing/Hands-On-Intelligent-Agents-with-OpenAI-Gym/tree/master/ch9 来查看任何 Roboschool 环境。例如,要查看RoboschoolInvertedDoublePendulum-v1环境,您可以运行以下脚本:
(rl_gym_book) praveen@ubuntu:~/HOIAWOG/ch9$python run_roboschool_env.py --env RoboschoolInvertedDoublePendulum-v1
您可以使用前面表格中列出的任何其他 Roboschool 环境名称,以及在发布时提供的新的 Roboschool 环境。
Gym retro
Gym Retro (github.com/openai/retro) 是 OpenAI 于 2018 年 5 月 25 日发布的一个相对较新的 Python 库 (blog.openai.com/gym-retro/),作为一个用于开发游戏玩法强化学习算法的研究平台。尽管在 OpenAI Gym 中有一个包含 60 多个游戏的 Atari 游戏合集,但可用的游戏总数有限。Gym Retro 支持使用为多个控制台/复古游戏平台开发的游戏,例如任天堂的 NES、SNES、Game Boy 控制台、世嘉 Genesis 和世嘉 Master System 等。通过使用 Libretro API 的模拟器,这一切成为可能:

Gym Retro 提供了便捷的包装器,将超过 1000 款视频游戏转化为兼容 Gym 接口的学习环境!是不是很棒!多个新的学习环境,但接口保持一致,这样我们就可以轻松地训练和测试我们到目前为止开发的代理,无需对代码做任何必要的修改……
为了感受在 Gym Retro 中使用环境的简易性,我们暂时把安装步骤放在一边,快速看看安装后如何创建一个新的 Gym Retro 环境的代码:
import retro
env = retro.make(game='Airstriker-Genesis', state='Level1')
这段代码片段将创建一个env对象,该对象具有与我们之前看到的所有 Gym 环境相同的接口和方法,例如step(...)、reset()和render()。
Gym Retro 的快速入门指南
让我们通过使用以下命令,快速安装预构建的二进制文件并试用 Gym Retro 库:
(rl_gym_book) praveen@ubuntu:~/rl_gym_book/ch9$ pip install gym-retro
安装成功后,我们可以通过以下脚本快速查看其中一个可用的 Gym Retro 环境:
#!/usr/bin/env python
import retro
if __name__ == '__main__':
env = retro.make(game='Airstriker-Genesis', state='Level1')
obs = env.reset()
while True:
obs, rew, done, info = env.step(env.action_space.sample())
env.render()
if done:
obs = env.reset()
运行此脚本后,将会弹出一个窗口,显示《Airstriker》游戏并展示飞船执行随机动作。游戏窗口将呈现如下图所示:

在我们继续之前,值得注意的是,包含整个游戏数据的ROM(只读内存)文件并不是所有游戏都可以免费获得。一些非商业性的游戏机游戏的 ROM 文件,如《Airstriker》(在前面的脚本中使用的游戏)、《Fire》、Dekadrive、Automaton、《Fire》、《Lost Marbles》等,已包含在 Gym Retro 库中,可以免费使用。其他游戏,如《刺猬索尼克》系列(《刺猬索尼克》、《刺猬索尼克 2》、《刺猬索尼克 3 & Knuckles》),需要购买 ROM 文件以合法使用,可以通过像Steam这样的平台进行购买。这对于希望在此类环境中开发算法的爱好者、学生以及其他热衷者来说,是一个障碍。但至少这个障碍相对较小,因为在 Steam 上购买《刺猬索尼克》的 ROM 大约需要 1.69 美元。一旦你拥有了游戏的 ROM 文件,Gym Retro 库提供了一个脚本,允许你将这些文件导入到库中,方法如下:
(rl_gym_book) praveen@ubuntu:~/rl_gym_book/ch9$ python -m retro.import /PATH/TO/YOUR/ROMs/DIRECTORY
OpenAI Universe
请注意,在创建新的 Gym Retro 环境时,我们需要提供游戏名称以及游戏状态retro.make(game='游戏名称', state='状态名称')
若要获取可用 Gym Retro 环境的列表,可以运行以下命令:
(rl_gym_book) praveen@ubuntu:~/rl_gym_book/ch9$ python -c "import retro; retro.list_games()"
若要获取可用游戏状态的列表,可以运行以下 Python 脚本:
#!/usr/bin/evn python
import retro
for game in retro.list_games():
print(game, retro.list_states(game))
到目前为止,我们已经熟悉了 Gym Retro 库。接下来,分析一下这个库在我们已经看到和使用的内容之外,提供了哪些优势或新特性。首先,Gym Retro 库使用了比 Atari 主机更新的游戏主机(如 SEGA Genesis)。做个对比,SEGA Genesis 游戏主机的 RAM 是 Atari 主机的 500 倍,这使得它能够提供更好的视觉效果和更广泛的控制选项。这为我们提供了相对复杂的学习环境,并且为我们的智能体提供了一些更复杂的任务和挑战,供它们学习和解决。其次,这些主机游戏中的几个是渐进性的,游戏的复杂度通常随着每一关的提升而增加,而且各关卡在某些方面(例如目标、物体外观、物理效果等)具有许多相似之处,同时在其他方面(如布局、新物体等)也提供了多样性。这样的训练环境,随着难度逐步增加的关卡,帮助开发能够学习解决一般任务的智能体,而不仅仅是特定的任务或环境(如监督学习中的过拟合)。智能体能够学会将自己在一个关卡中的技能和知识转移到下一个关卡,进而转移到另一个游戏中。这个领域正在积极研究,通常被称为课程学习、分阶段学习或渐进式进化。毕竟,我们最终的目标是开发能够学习解决一般任务的智能体,而不仅仅是训练中给定的具体任务。Gym Retro 库提供了一些有用的、仅限于游戏的环境来促进此类实验和研究。
其他开源基于 Python 的学习环境
在本节中,我们将讨论一些最近的基于 Python 的学习环境,这些环境为智能体开发提供了良好的平台,但不一定具有与 Gym 兼容的环境接口。虽然它们没有提供与 Gym 兼容的接口,但本节中讨论的这些环境经过精心挑选,确保要么已经有 Gym 的包装器(使其与 Gym 接口兼容),要么它们很容易实现,可以用来测试和实验我们在本书中开发的智能体。正如你所猜测的那样,未来将有更多的优秀基于 Python 的智能体开发学习环境出现,因为这一领域目前正在积极研究。本书的代码库将提供新环境的信息和快速入门指南,一旦这些新环境发布,你可以在本书的 GitHub 仓库中注册获取更新通知。在接下来的子章节中,我们将讨论一些现成的、有前景的学习环境,供大家使用。
星际争霸 II - PySC2
《星际争霸 II》非常受欢迎,实际上是有史以来最成功的实时战略游戏之一,全球有数百万人在玩这款游戏。它甚至有一个世界锦标联赛(wcs.starcraft2.com/en-us/)!游戏环境相当复杂,主要目标是建立一个军队基地,管理经济,防守基地,并摧毁敌人。玩家从第三人称视角控制基地和军队。如果你不熟悉《星际争霸》,你应该先观看几场在线比赛,以便了解游戏的复杂性和快速节奏。
人类要想在这款实时战略游戏中表现出色,需要大量的练习(甚至需要几个月,实际上职业玩家需要数年训练),计划和快速反应。尽管软件代理可以在每帧中按下多个软件按钮,做出相当快速的动作,但行动速度并不是唯一决定胜利的因素。代理还需要进行多任务处理和微管理军队单位,并最大化得分,这比 Atari 游戏复杂几个数量级。
《星际争霸 II》的制作公司暴雪发布了《星际争霸 II》API,提供了与《星际争霸 II》游戏接口的必要钩子,使得玩家可以不受限制地控制游戏。这为开发我们所追求的智能代理提供了新的可能性。暴雪甚至为在 AI 和机器学习许可下公开使用该环境提供了单独的最终用户许可协议(EULA)!对于像暴雪这样从事游戏制作和销售的公司来说,这一举措非常受欢迎。暴雪将StarCraft2(SC2)客户端协议实现开源,并提供了 Linux 安装包,以及多个附加组件,如地图包,用户可以从他们的 GitHub 页面免费下载,链接为github.com/Blizzard/s2client-proto。除此之外,Google DeepMind 还开源了他们的 PySC2 库,通过 Python 暴露了 SC2 客户端接口,并提供了一个封装,使其成为一个强化学习(RL)环境。
以下截图展示了 PySC2 的用户界面,右侧是可供代理作为观察的特征层,左侧是游戏场景的简化概览:

如果你对这些类型的环境感兴趣,尤其是如果你是游戏开发者,你可能也对 Dota 2 环境感兴趣。Dota 2 是一款实时战略游戏,和 StarCraft II 类似,由两队五名玩家进行对战,每个玩家控制一个英雄角色。你可以了解更多关于 OpenAI 如何开发一个由五个神经网络代理组成的团队,他们学会了团队协作,单日内进行 180 年游戏量的训练,学习如何克服多个挑战(包括高维连续状态和动作空间以及长期的决策时间),而这一切都发生在自我对战中!你可以在 https://blog.openai.com/openai-five/ 阅读更多关于五代理团队的内容。
StarCraft II PySC2 环境设置和运行的快速入门指南
我们将展示如何快速设置并开始使用 StarCraft II 环境。和往常一样,请使用代码库中的 README 文件获取最新的操作说明,因为链接和版本可能会发生变化。如果你还没有这么做,请为该书的代码库添加星标并关注,以便收到关于更改和更新的通知。
下载 StarCraft II Linux 包
从 https://github.com/Blizzard/s2client-proto#downloads 下载 StarCraft 游戏的最新 Linux 包,并将其解压到硬盘上的 ~/StarCraftII 目录。例如,要将版本 4.1.2 下载到你的~/StarCraftII/文件夹中,可以使用以下命令:
wget http://blzdistsc2-a.akamaihd.net/Linux/SC2.4.1.2.60604_2018_05_16.zip -O ~/StarCraftII/SC2.4.1.2.zip
让我们将文件解压到~/StarCraftII/目录:
unzip ~/StarCraftII/SC2.4.1.2.zip -d ~/StarCraftII/
请注意,正如下载页面所述,这些文件是受密码保护的,密码是'iagreetotheeula。
通过输入该命令,暴雪确保我们同意接受其 AI 和机器学习许可协议,详情请见下载页面。
下载 SC2 地图
我们需要 StarCraft II 地图包和迷你游戏包才能开始。
从github.com/Blizzard/s2client-proto#map-packs下载地图包
解压到你的~/StarCraftII/Maps目录。
作为示例,我们将使用以下命令下载 2018 年第二赛季发布的梯子地图:
wget http://blzdistsc2-a.akamaihd.net/MapPacks/Ladder2018Season2_Updated.zip -O ~/StarCraftII/Maps/Ladder2018S2.zip
让我们将地图解压到~/StarCraftII/Maps目录:
unzip ~/StarCraftII/Maps/Ladder2018S2.zip -d ~/StarCraftII/Maps/
接下来,我们将下载并解压迷你游戏地图文件:
wget https://github.com/deepmind/pysc2/releases/download/v1.2/mini_games.zip -O ~/StarCraftII/Maps/mini_games1.2.zip
unzip ~/StarCraftII/Maps/mini_games1.2.zip -d ~/StarCraftII/Maps
安装 PySC2
让我们安装 PySC2 库以供 RL 环境接口使用,并安装所需的依赖项。这个步骤会很简单,因为 PySC2 库在 PyPi 上有提供 Python 包:
(rl_gym_book) praveen@ubuntu:~/HOIAWOG/ch9$ pip install pysc2
玩 StarCraft II 或运行示例代理
为了测试安装是否成功并查看 StarCraft II 学习环境的样子,你可以使用以下命令在 Simple64 地图或 CollectMineralShards 地图上快速启动一个随机行为的代理:
(rl_gym_book) praveen@ubuntu:~/HOIAWOG/ch9$ python -m pysc2.bin.agent --map Simple64
你也可以加载环境中的其他可用地图。例如,以下命令加载 CollectMineralShards 地图:
(rl_gym_book) praveen@ubuntu:~/HOIAWOG/ch9$ python -m pysc2.bin.agent --map CollectMineralShards
这应该会弹出一个 UI,显示随机智能体所采取的动作,帮助你了解有效的动作是什么,并帮助你可视化智能体在环境中行动时的情况。
若要自己玩这个游戏,PySC2 提供了一个人类智能体接口,这对于调试(如果你感兴趣,也可以用来玩游戏!)非常有用。以下是运行并亲自玩游戏的命令:
(rl_gym_book) praveen@ubuntu:~/HOIAWOG/ch9$ python -m pysc2.bin.play --map Simple64
你还可以运行一个示例智能体,该智能体的脚本任务是收集矿物碎片,这是游戏中的一项任务,使用以下命令:
(rl_gym_book) praveen@ubuntu:~/HOIAWOG/ch9$ python -m pysc2.bin.agent --map CollectMineralShards --agent pysc2.agents.scripted_agent.CollectMineralShards
请关注本书的代码库,获取新的智能体源代码以及训练和测试具有高级技能的新智能体的说明。你还可以自定义我们在上一章中开发的智能体,学习如何玩《星际争霸 II》。如果你这样做了,请向本书的代码库提交一个拉取请求,给作者发封邮件,或者大声说出来,让大家知道你做了哪些很酷的事情!
DeepMind lab
DeepMind Lab (github.com/deepmind/lab) 是一个 3D 学习环境,提供了一系列具有挑战性任务的环境,例如通过迷宫进行 3D 导航和解谜。它是基于一系列开源软件构建的,包括著名的《Quake III Arena》。
环境接口与我们在本书中广泛使用的 Gym 接口非常相似。为了让你了解环境接口的实际样子,可以查看以下代码片段:
import deepmind_lab
num_steps = 1000
config = {
'width': 640,
'height': 480,
'fps': 30
}
...
env = deepmind_lab.Lab(level, ['RGB_INTERLEAVED'], config=config, renderer='software')
for step in range(num_steps)
if done:
env.reset()
obs = env.observations()
action = agent.get_action(...)
reward = env.step(action, num_steps=1)
done = not env.is_running()
这段代码虽然与 OpenAI Gym 接口不是一一兼容,但提供了一个非常相似的接口。
DeepMind Lab 学习环境接口
我们将简要讨论 DeepMind Lab (DM Lab) 的环境接口,以便你能够熟悉它,看到它与 OpenAI Gym 接口的相似之处,并开始在 DM Lab 环境中进行智能体实验!
reset(episode=-1, seed=None)
这类似于我们在 Gym 接口中看到的 reset() 方法,但与 Gym 环境不同,DM Lab 的 reset 方法调用不会返回观察结果。我们稍后会看到如何获取观察结果,所以现在我们讨论的是 DM Lab 的 reset(episode=-1, seed=None) 方法。它将环境重置为初始状态,并且需要在每一集的结尾调用,以便创建新的集。可选的 episode 参数接受一个整数值,用于指定特定集中的关卡。如果未设置 episode 值,或者将其设置为 -1,则关卡将按数字顺序加载。seed 参数也是可选的,用于为环境的随机数生成器设置种子,以便实现可重复性。
step(action, num_steps=1)
这与 Gym 接口的step(action)方法类似,但与reset(...)方法一样,调用该方法不会返回下一个观测(或奖励、结束状态、信息)。调用此方法会将环境推进num_steps帧,且在每一帧中执行由action定义的动作。这种动作重复的行为在我们希望相同的动作在连续四帧左右应用时非常有用,事实上,几位研究人员发现这种方法有助于学习。有一些 Gym 环境包装器可以实现这种动作重复行为。
observations()
这是我们在调用reset(...)或step(action)之后用来接收来自 DM Lab 环境的观测的方法。该方法返回一个 Python 字典对象,其中包含我们从环境的可用类型列表中指定的每种类型的观测。例如,如果我们希望将环境的RGBD(红绿蓝深度)信息作为观测类型,我们可以在初始化环境时通过'RGBD'键指定,随后可以通过相同的'RGBD'键从返回的观测字典中检索该信息。下面是一个简单的示例来说明这一点:
env = deepmind_lab.Lab('tests/empty_room_test', ['RGBD'])
env.reset()
obs = env.observations()['RGBD']
DM Lab 环境还支持其他类型的观测。我们可以使用observation_spec()来获取支持的观测类型列表,我们将在稍后详细讨论。
is_running()
这个方法类似于(但意义相反)Gym 接口的step(action)方法返回的done布尔值。
当环境完成一个回合或停止运行时,该方法将返回False。只要环境仍在运行,它将返回True。
observation_spec()
这个方法类似于我们在 Gym 环境中使用的env.observation_space()。该方法返回一个列表,指定了 DM Lab 环境支持的所有可用观测。它还包括有关与关卡相关的自定义观测的规格说明。
这些规格包含了如果在观测列表中指定了该规格名称时将返回的张量或字符串的名称、类型和形状(例如之前提到的'RGBD'示例)。例如,以下代码片段列出了列表中将返回的两个项,帮助你了解规格内容:
{
'dtype': <type 'numpy.uint8'>, ## Array data type
'name': 'RGBD', ## Name of observation.
'shape': (4, 180, 320) ## shape of the array. (Heights, Width, Colors)
}
{
'name': 'RGB_INTERLEAVED', ## Name of observation.
'dtype': <type 'numpy.uint8'>, ## Data type array.
'shape': (180, 320, 3) ## Shape of array. (Height, Width, Colors)
}
为了快速理解如何使用这个方法,让我们看看以下代码行和输出:
import deepmind_lab
import pprint
env = deepmind_lab.Lab('tests/empty_room_test', [])
observation_spec = env.observation_spec()
pprint.pprint(observation_spec)
# Outputs:
[{'dtype': <type 'numpy.uint8'>, 'name': 'RGB_INTERLEAVED', 'shape': (180, 320, 3)},
{'dtype': <type 'numpy.uint8'>, 'name': 'RGBD_INTERLEAVED', 'shape': (180, 320, 4)},
{'dtype': <type 'numpy.uint8'>, 'name': 'RGB', 'shape': (3, 180, 320)},
{'dtype': <type 'numpy.uint8'>, 'name': 'RGBD', 'shape': (4, 180, 320)},
{'dtype': <type 'numpy.uint8'>, 'name': 'BGR_INTERLEAVED', 'shape': (180, 320, 3)},
{'dtype': <type 'numpy.uint8'>, 'name': 'BGRD_INTERLEAVED', 'shape': (180, 320, 4)},
{'dtype': <type 'numpy.float64'>, 'name': 'MAP_FRAME_NUMBER', 'shape': (1,)},
{'dtype': <type 'numpy.float64'>, 'name': 'VEL.TRANS', 'shape': (3,)},
{'dtype': <type 'numpy.float64'>, 'name': 'VEL.ROT', 'shape': (3,)},
{'dtype': <type 'str'>, 'name': 'INSTR', 'shape': ()},
{'dtype': <type 'numpy.float64'>, 'name': 'DEBUG.POS.TRANS', 'shape': (3,)},
{'dtype': <type 'numpy.float64'>, 'name': 'DEBUG.POS.ROT', 'shape': (3,)},
{'dtype': <type 'numpy.float64'>, 'name': 'DEBUG.PLAYER_ID', 'shape': (1,)},
# etc...
action_spec()
类似于observation_spec(),action_spec()方法返回一个列表,其中包含空间中每个元素的最小值、最大值和名称。min和max值分别代表动作空间中相应元素可以设置的最小值和最大值。这个列表的长度等于动作空间的维度/形状。这类似于我们在 Gym 环境中使用的env.action_space。
以下代码片段让我们快速了解调用此方法时返回值的样子:
import deepmind_lab
import pprint
env = deepmind_lab.Lab('tests/empty_room_test', [])
action_spec = env.action_spec()
pprint.pprint(action_spec)
# Outputs:
# [{'max': 512, 'min': -512, 'name': 'LOOK_LEFT_RIGHT_PIXELS_PER_FRAME'},
# {'max': 512, 'min': -512, 'name': 'LOOK_DOWN_UP_PIXELS_PER_FRAME'},
# {'max': 1, 'min': -1, 'name': 'STRAFE_LEFT_RIGHT'},
# {'max': 1, 'min': -1, 'name': 'MOVE_BACK_FORWARD'},
# {'max': 1, 'min': 0, 'name': 'FIRE'},
# {'max': 1, 'min': 0, 'name': 'JUMP'},
# {'max': 1, 'min': 0, 'name': 'CROUCH'}]
num_steps()
这个工具方法就像一个计数器,它计算自上次调用reset()以来环境执行的帧数。
fps()
这个工具方法返回每秒钟实际(墙钟)执行的帧数(或环境步数)。这个方法对于跟踪环境的执行速度以及代理从环境中采样的速度非常有用。
events()
这个工具方法可以在调试时非常有用,因为它返回自上次调用reset()或step(...)以来发生的事件列表。返回的元组包含一个名称和一组观察数据。
close()
与 Gym 环境中的close()方法类似,这个方法也会关闭环境实例并释放底层资源,例如 Quake III Arena 实例。
设置和运行 DeepMind Lab 的快速入门指南
在前面简短讨论了 DeepMind Lab 环境接口之后,我们已经准备好亲自体验这个学习环境。在接下来的子章节中,我们将逐步介绍设置 DeepMind Lab 并运行一个示例代理的过程。
设置和安装 DeepMind Lab 及其依赖项
DeepMind Lab 库使用 Bazel 作为构建工具,而 Bazel 又依赖于 Java。书中的代码库有一个脚本,可以帮助你轻松设置 DeepMind Lab。你可以在章节 9 文件夹下找到这个脚本,路径是github.com/PacktPublishing/Hands-On-Intelligent-Agents-with-OpenAI-Gym/tree/master/ch9。你可以使用以下命令运行该脚本:
(rl_gym_book) praveen@ubuntu:~/HOIAWOG/ch9$./setup_deepmindlab.sh
这个脚本可能需要一些时间来完成,但它会自动安装所有必要的包和库,包括 Bazel 及其依赖项,并为你设置好一切。
玩游戏、测试随机行为代理或训练自己的代理!
安装完成后,你可以通过运行以下命令,使用键盘输入测试游戏:
(rl_gym_book) praveen@ubuntu:~/HOIAWOG/ch9$ cd deepmindlab
(rl_gym_book) praveen@ubuntu:~/HOIAWOG/ch9/deepmindlab$ bazel run :game -- --level_script=tests/empty_room_test
你还可以通过以下命令,在随机行为代理的帮助下进行测试:
(rl_gym_book) praveen@ubuntu:~/HOIAWOG/ch9/deepmindlab$ bazel run :python_random_agent --define graphics=sdl -- --length=5000
要开始自己的代理开发,你可以使用已经配置好的示例代理脚本,它可以与 DeepMind Lab 环境进行交互。你可以在~/HOIAWOG/ch9/deepmindlab/python/random_agent.py找到该脚本。要开始训练这个代理,你可以使用以下命令:
(rl_gym_book) praveen@ubuntu:~/HOIAWOG/ch9/deepmindlab$ bazel run :python_random_agent
概述
在本章中,我们探讨了几个有趣且有价值的学习环境,了解了它们的界面设置,甚至通过每个环境的快速入门指南和书中代码库中的设置脚本亲自操作了这些环境。我们首先查看了与我们现在非常熟悉的 OpenAI Gym 接口兼容的环境。具体来说,在这一类别中,我们探索了 Roboschool 和 Gym Retro 环境。
我们还查看了其他有用的学习环境,这些环境不一定具备与 Gym 兼容的环境接口,但它们拥有非常相似的 API,因此我们可以轻松地调整我们的智能体代码,或在学习环境周围实现一个包装器,使其兼容 Gym API。具体来说,我们探索了著名的实时战略游戏《星际争霸 II》环境和 DeepMind Lab 环境。我们还简要地提到了 DOTA2 环境,该环境曾被用于训练单一智能体和一个智能体团队,由 OpenAI 训练,这个团队成功击败了业余人类玩家,甚至一些职业电竞团队,在 DOTA 2 比赛中取得胜利。
我们观察了每个学习环境库中提供的不同任务和环境,并尝试了一些示例,以熟悉这些环境,同时了解如何使用我们在前几章中开发的智能体来训练并解决这些相对较新的学习环境中的挑战性任务。
第十章:探索学习算法领域 - DDPG(演员-评论家),PPO(策略梯度),Rainbow(基于值的方法)
在上一章中,我们讨论了几种有前景的学习环境,你可以用来训练智能体解决不同的任务。在第七章,创建自定义 OpenAI Gym 环境——CARLA 驾驶模拟器,我们还展示了如何创建自己的环境,以解决你可能感兴趣的任务或问题,借助智能和自主软件代理。这为你提供了完成后可以继续深入的方向,以探索和尝试本书中讨论的所有环境、任务和问题。按此思路,在本章中,我们将讨论几种有前景的学习算法,它们将成为你智能代理开发工作中的未来参考。
到目前为止,在本书中,我们已经详细介绍了实现智能代理的逐步过程,这些代理能够学习改进并解决离散决策/控制问题(第六章,使用深度 Q 学习实现最优离散控制的智能代理)和连续动作/控制问题(第八章,使用深度演员-评论家算法实现智能自主汽车驾驶代理)。这些内容为开发这样的学习代理提供了良好的起点。希望之前的章节为你展示了一个能够学习改进并应对手头任务或问题的自主智能软件代理/系统的整体框架。我们还审视了开发、训练和测试这些复杂系统时,有助于的整体管道及其有用的工具和常规(如日志记录、可视化、参数管理等)。我们看到两类主要算法:基于深度 Q 学习(及其扩展)和深度演员-评论家(及其扩展)的深度强化学习算法。它们是良好的基准算法,实际上,直到今天,它们仍然是该领域最新研究论文中的参考。这一研究领域近年来活跃发展,提出了若干新的算法。一些算法在样本复杂度上表现更好,即智能体在达到某一性能水平之前,从环境中收集的样本数。一些其他算法具有稳定的学习特性,并且在足够的时间内能够找到最优策略,适用于大多数问题且几乎不需要调参。还引入了几种新的架构,如 IMPALA 和 Ape-X,它们使得高可扩展的学习算法实现成为可能。
我们将快速了解这些有前景的算法、它们的优点以及潜在的应用类型。我们还将查看这些算法为我们已有的知识添加的关键组件的代码示例。这些算法的示例实现可在本书的代码库中找到,位于ch10文件夹,github.com/PacktPublishing/Hands-On-Intelligent-Agents-with-OpenAI-Gym。
深度确定性策略梯度
深度确定性策略梯度(DDPG)是一种离策略、无模型、演员-评论家算法,基于确定性策略梯度(DPG)定理(proceedings.mlr.press/v32/silver14.pdf)。与基于深度 Q 学习的方法不同,基于演员-评论家的策略梯度方法不仅适用于离散动作空间的问题/任务,也能轻松应用于连续动作空间。
核心概念
在第八章,使用深度演员-评论家算法实现智能自动驾驶汽车代理,我们带你走过了策略梯度定理的推导,并为引入上下文,复现了以下内容:

你可能还记得,我们考虑的策略是一个随机函数,给定状态(s)和参数(
)为每个动作分配一个概率。在确定性策略梯度中,随机策略被一个确定性策略所替代,该策略为给定的状态和参数集指定了一个固定策略!。简而言之,DPG 可以通过以下两个方程表示:
这是策略目标函数:

这里,
是由
参数化的确定性策略,r(s,a)是执行动作a在状态 s 下的奖励函数,而
是该策略下的折扣状态分布。
确定性策略目标函数的梯度已在之前链接的论文中证明为:

我们现在看到熟悉的动作-价值函数项,通常称为评论员。DDPG 基于这个结果,并使用深度神经网络表示动作-价值函数,就像我们在第六章中做的那样,使用深度 Q 学习实现智能最优离散控制代理,以及其他一些修改来稳定训练。具体而言,使用了 Q 目标网络(就像我们在第六章中讨论的那样,使用深度 Q 学习实现智能最优离散控制代理),但是现在这个目标网络是缓慢更新的,而不是在几个更新步骤后保持固定并更新它。DDPG 还使用经验重放缓冲区,并使用噪声版本的
,用方程式
表示,以鼓励探索作为策略。
是确定性的。
DDPG 有一个扩展版本叫做 D4PG,简称为分布式分布式 DDPG。我猜你可能在想:DPG -> DDPG -> {缺失?} -> DDDDPG。没错!缺失的部分就是你需要实现的内容。
D4PG 算法对 DDPG 算法做了四个主要改进,如果你有兴趣,可以简要列出如下:
-
分布式评论员(评论员现在估计 Q 值的分布,而不是给定状态和动作的单一 Q 值)
-
N 步回报(类似于我们在第八章中使用的实现智能自动驾驶代理,使用深度演员-评论员算法,使用了 n 步 TD 回报,而不是通常的 1 步回报)
-
优先经验重放(用于从经验重放记忆中抽样经验)
-
分布式并行演员(利用 K 个独立的演员并行收集经验并填充经验重放记忆)
邻近策略优化(Proximal Policy Optimization)
邻近策略优化(PPO) 是一种基于策略梯度的方法,是已经被证明既稳定又具有可扩展性的算法之一。事实上,PPO 是 OpenAI Five 团队使用的算法,该团队的代理与多位人类 DOTA II 玩家对战并获胜,这一点我们在前一章中讨论过。
核心概念
在策略梯度方法中,算法通过执行回合(rollouts)来收集状态转移和(可能的)奖励样本,并使用梯度下降更新策略的参数,以最小化目标函数。其思想是不断更新参数以改进策略,直到获得一个较好的策略。为了提高训练的稳定性,信任域策略优化(TRPO)算法对策略更新施加了Kullback-Liebler(KL)散度约束,确保策略在与旧策略的比较中不会在一步中更新得过多。TRPO 是 PPO 算法的前身。让我们简要讨论一下 TRPO 算法中使用的目标函数,以便更好地理解 PPO。
脱策略学习
正如我们所知,在脱策略学习的情况下,代理遵循一个与其试图优化的策略不同的行为策略。提醒一下,我们在第六章中讨论过的 Q-learning,使用深度 Q-learning 实现智能代理进行最优离散控制,以及一些扩展,都是脱策略算法。我们用
来表示行为策略。那么,我们可以将代理的目标函数写为状态访问分布和动作的总优势,如下所示:

这里,
是更新前的策略参数,
是旧策略参数下的状态访问概率分布。我们可以在内层求和式中对项进行乘除操作,使用行为策略
,其目的是使用重要性采样来考虑到状态转移是通过行为策略
进行采样的:

与前一方程相比,前述方程中变化的项用红色表示。
我们可以将之前的求和表示为一个期望,如下所示:

在策略学习中
在策略学习的情况下,行为策略和目标策略是相同的。因此,自然地,当前策略(更新前)是代理用来收集样本的策略,表示为
,这就是行为策略,因此目标函数变为:

与前一方程相比,前述方程中变化的项用红色表示。
TRPO 通过信任域约束优化前述目标函数,使用以下方程给出的 KL 散度度量:

这是确保新策略更新不会与当前策略相差过大的约束条件。尽管 TRPO 背后的理念简洁且直观,但其实现和梯度更新涉及复杂性。PPO 通过使用一个裁剪的替代目标简化了这一方法,且既有效又简单。让我们通过算法背后的数学原理深入理解 PPO 的核心概念。假设在给定状态 s 下采取动作 a 的新策略与旧策略的概率比定义如下:

将这个代入我们之前讨论的 TRPO 的在线策略目标函数方程中,得到的目标函数如下:

简单地移除 KL 散度约束将导致不稳定,因为可能会出现大量的参数更新。PPO 通过强制! 落在区间!内,施加了这个约束,其中!是一个可调超参数。实际上,PPO 中使用的目标函数取原始参数值和裁剪版本中的最小值,其数学描述如下所示:

这导致了一个稳定的学习目标,并且策略单调地改善。
Rainbow
Rainbow (arxiv.org/pdf/1710.02298.pdf) 是基于 DQN 的一种离策略深度强化学习算法。我们在第六章,使用深度 Q 学习实现最优离散控制的智能代理中研究并实现了深度 Q 学习(DQN)以及一些 DQN 的扩展。DQN 算法已经有了几个扩展和改进。Rainbow 结合了其中六个扩展,显示出它们的组合效果更佳。Rainbow 是一个最先进的算法,目前在所有 Atari 游戏中保持着最高分记录。如果你想知道为什么这个算法叫做 Rainbow,很可能是因为它结合了七个(即彩虹的颜色数)Q 学习算法的扩展,具体包括:
-
DQN
-
双重 Q 学习
-
优先经验回放
-
对抗网络
-
多步学习/n 步学习
-
分布式 RL
-
噪声网络
核心概念
Rainbow 结合了 DQN 和六个已被证明能解决原始 DQN 算法局限性的扩展。我们将简要回顾这六个扩展,了解它们如何为整体性能提升做出贡献,并使 Rainbow 在 Atari 基准测试中占据了榜首,同时也证明了它们在 OpenAI Retro 大赛中的成功。
DQN
到现在为止,你应该已经非常熟悉 DQN,因为我们在第六章,使用深度 Q-Learning 实现最优离散控制的智能体中逐步实现了深度 Q-learning 智能体,在那里我们详细讨论了 DQN 以及它如何通过深度神经网络函数逼近、回放记忆和目标网络扩展标准 Q-learning。让我们回顾一下我们在第六章,使用深度 Q-Learning 实现最优离散控制的智能体中使用的 Q-learning 损失:

这基本上是 TD 目标和 DQN 的 Q 估计之间的均方误差,正如我们在第六章,使用深度 Q-Learning 实现最优离散控制的智能体中所提到的,其中
是缓慢变化的目标网络,
是主 Q 网络。
双重 Q-Learning
在双重 Q-Learning 中,有两个动作值/Q 函数。我们将它们称为 Q1 和 Q2。双重 Q-Learning 的理念是将动作选择与价值估计解耦。也就是说,当我们想更新 Q1 时,我们根据 Q1 选择最佳动作,但使用 Q2 来找到选定动作的价值。类似地,当 Q2 被更新时,我们基于 Q2 选择动作,但使用 Q1 来确定选定动作的价值。实际上,我们可以使用主 Q 网络
作为 Q1,使用缓慢变化的目标网络
作为 Q2,从而得到以下双重 Q-Learning 损失方程(与 DQN 方程的差异以红色显示):

更改损失函数的动机是,Q-learning 会受到过度估计偏差的影响,这可能会损害学习。过度估计的原因是最大值的期望大于或等于期望的最大值(通常是不等式成立),这源于 Q-learning 算法和 DQN 中的最大化步骤。双重 Q-Learning 所引入的变化已被证明可以减少有害的过度估计,从而改善性能,相较于 DQN 更具优势。
优先经验回放
在我们实现深度 Q 学习的第六章,使用深度 Q 学习实现智能代理进行最优离散控制中,我们使用经验回放内存来存储和检索采样的过渡经验。在我们的实现中,以及在 DQN 算法中,来自回放内存缓冲区的经验是均匀采样的。直观地说,我们希望更频繁地采样这些经验,因为有很多内容需要学习。优先经验回放根据以下方程,以相对最后遇到的绝对 TD 误差的概率
进行过渡采样:

在这里,
是一个超参数,决定分布的形状。这确保我们采样那些 Q 值预测与正确值相差较大的过渡。实际上,新的过渡会以最高优先级插入回放内存,以表示最近过渡经验的重要性。
对战网络
对战网络(Dueling networks)是一种为基于值的强化学习设计的神经网络架构。名称对战(dueling)来源于该架构的主要特点,即它有两条计算流,一条用于值函数,另一条用于优势函数。下面的图示来自一篇研究论文(arxiv.org/pdf/1511.06581.pdf),展示了对战网络架构(图中底部的网络)与典型 DQN 架构(图中顶部的网络)之间的对比:

编码特征的卷积层是由值流和优势流共享的,并通过一个特殊的聚合函数进行合并,正如在论文中所讨论的,它对应于以下的动作值分解:

,和
分别是值流、共享卷积编码器和优势流的参数,
是它们的连接。
多步学习/n 步学习
在第八章,使用深度演员-评论家算法实现智能自主汽车驾驶代理中,我们实现了 n 步返回 TD 方法,并讨论了如何使用前视多步目标替代单步 TD 目标。我们可以将这个 n 步返回与 DQN 一起使用,这本质上是该扩展的思想。回想一下,从状态
的截断 n 步返回给出如下:

使用这个方程,可以定义 DQN 的多步变体,以最小化以下损失(与 DQN 方程的差异以红色显示):

这个方程展示了 DQN 中引入的变化。
分布式强化学习
分布式强化学习方法(arxiv.org/abs/1707.06887)是学习如何近似回报的分布,而不是期望(平均)回报。分布式强化学习方法提出使用放置在离散支持集上的概率质量来建模这些分布。本质上,这意味着与其试图建模给定状态下的单一动作值,不如为每个动作给定状态下寻找一个动作值的分布。虽然不深入细节(因为那需要大量背景信息),我们将看看此方法对强化学习的一个关键贡献,即分布式贝尔曼方程的公式化。如你从本书前几章回忆的那样,使用一步贝尔曼备份的动作值函数可以通过如下方式返回:

在分布式贝尔曼方程的情况下,标量量
被随机变量
所替代,从而得到如下方程:

由于该量不再是标量,因此更新方程需要比单纯将下一状态-动作值的折扣值加到步进回报中更加小心地处理。分布式贝尔曼方程的更新步骤可以通过以下图示轻松理解(从左到右的各个阶段):

在前面的插图中,下一状态的动作值分布用红色显示在左侧,接着通过折扣因子
(中间)进行缩放,最终通过
平移该分布,从而得到分布贝尔曼更新。在更新之后,由前一更新操作得到的目标分布
通过最小化
和
之间的交叉熵损失,投影到当前分布的支持集
上。
在此背景下,你可以简要浏览分布式强化学习论文中的 C51 算法伪代码,它已经集成到 Rainbow 智能体中:

噪声网络
如果你还记得,我们在第六章《使用深度 Q 学习实现智能代理进行最优离散控制》中使用了
-贪心策略,基于深度 Q 网络学习到的动作值来选择动作,这基本上意味着大多数时候都会选择给定状态下的最高动作值对应的动作,除了在一小部分时间(即非常小的概率
)内,代理会选择一个随机动作。这可能会阻止代理探索更多的奖励状态,特别是当它已经收敛的动作值不是最优的动作值时。使用
-贪心策略进行探索的局限性,在 Atari 游戏《Montezuma's Revenge》的表现中显而易见,在该游戏中,必须以正确的方式执行一系列动作才能获得第一个奖励。为了克服这一探索限制,Rainbow 使用了喧嚣网络的思想——这是一种 2017 年提出的简单但有效的方法。
喧嚣网络的主要思想是线性神经网络层的一个喧嚣版本,它结合了确定性流和喧嚣流,如以下方程所示,针对线性神经网络层的情况:

在这里,
和
是喧嚣层的参数,它们与 DQN 的其他参数一起通过梯度下降进行学习。
代表元素级的乘积操作,
和
是均值为零的随机噪声。我们可以在 DQN 实现中用喧嚣线性层代替普通的线性层,这将具有更好的探索优势。由于
和
是可学习的参数,网络可以学会忽略喧嚣流。随着时间的推移,对于每个神经元来说,喧嚣流会在状态空间的不同部分以不同的速率衰减,从而通过一种自我退火的形式实现更好的探索。
Rainbow 代理的实现将所有这些方法结合起来,达到了最先进的成果,并且在 57 款 Atari 游戏的测试中,比其他任何方法的表现都要优秀。总体来说,Rainbow 代理与以前最佳表现的算法在 Atari 游戏综合基准测试中的表现,如下图所示:

从图中可以清楚地看出,Rainbow 代理所包含的方法在 57 款不同的 Atari 游戏中带来了显著的性能提升。
优势和应用的快速总结
Rainbow 代理的一些关键优势总结如下,供您快速参考:
-
融合了过去几年中 Q-learning 的多个显著扩展
-
在 Atari 基准测试中取得了最先进的结果。
-
使用适当调整的 n 值的 n 步目标通常能加速学习。
-
与其他 DQN 变体不同,Rainbow 智能体可以在经验回放内存中收集的帧数减少 40% 的情况下开始学习。
-
在不到 10 小时(700 万帧)内,在单 GPU 机器上匹配了 DQN 的最佳表现。
Rainbow 算法已成为离散控制问题中最受追捧的智能体,尤其适用于动作空间小且离散的场景。它在其他游戏环境中也非常成功,例如 Gym-Retro,特别是经过调优的 Rainbow 智能体在 2018 年 OpenAI Retro 比赛中获得了第二名。该比赛是一个迁移学习竞赛,任务是学习在某些关卡中玩复古的 Genesis 游戏《刺猬索尼克》、《刺猬索尼克 II》和《刺猬索尼克与纳克尔斯》,然后能够在未经过训练的其他关卡中表现良好。考虑到在典型的强化学习环境中,智能体通常在相同环境中训练和测试,Retro 比赛衡量了学习算法从先前经验中泛化学习的能力。总的来说,Rainbow 智能体是任何具有离散动作空间的强化学习问题/任务的最佳初步选择。
总结
作为本书的最后一章,本章总结了当前在该领域内最先进的关键学习算法。我们了解了三种不同最先进算法的核心概念,每种算法都有其独特的元素和分类(演员-评论家/基于策略/基于价值函数)。
具体来说,我们讨论了深度确定性策略梯度算法,这是一种演员-评论家架构方法,采用确定性策略,而非通常的随机策略,并在多个连续控制任务中取得了良好的表现。
接下来我们研究了 PPO 算法,这是一种基于策略梯度的方法,采用了 TRPO 目标的剪辑版本,并学习到一个单调提升且稳定的策略,在像 DOTA II 这样的高维环境中取得了成功。
最后,我们看到了 Rainbow 算法,这是一种基于价值的方法,结合了多个扩展到非常流行的 Q-learning 算法的技术,即 DQN、双重 Q-learning、优先经验回放、对抗网络、多步学习/n 步学习、分布式强化学习和噪声网络层。Rainbow 智能体在 57 款 Atari 基准游戏中取得了显著更好的表现,并且在 OpenAI Retro 比赛中的迁移学习任务上表现也非常出色。
现在,我们进入了这本书的最后一段!希望您在阅读过程中享受了旅程,学到了很多,并获得了实现智能代理算法以及在您选择的学习环境/问题上训练和测试代理所需的许多实际技能的机会。您可以使用书籍代码库中的问题追踪系统报告代码问题,或者如果您想进一步讨论某个主题,或者需要任何额外的参考资料/指引来进入下一个阶段。


:状态转移模型/概率
:奖励模型

和最优策略 




,执行当前策略
规定的动作
。
和通过 1 步 TD 学习方程获得的奖励
,计算 TD 误差:
> 0,增加采取动作
的概率,因为
是一个好的决策,并且效果很好
< 0,则减少采取动作
的概率,因为
导致了代理的表现不佳
的估计值,使用 TD 误差更新评论员:
,其中
是评论员的学习率
设置为当前状态
,并重复步骤 2。
, critic_prediction)
) * TD_error



















浙公网安备 33010602011771号