深度强化学习实践(原书第2版)
上QQ阅读APP看书,第一时间看更新

2.3 OpenAI Gym API

OpenAI(www.openai.com)开发并维护了名为Gym的Python库。Gym的主要目的是使用统一的接口来提供丰富的RL环境。所以这个库的核心类是称为Env的环境也就不足为奇了。此类的实例暴露了几个方法和字段,以提供和其功能相关的必要信息。在较高层次上,每个环境都提供了下列信息和功能:

  • 在环境中允许执行的一系列动作。Gym同时支持离散动作和连续动作,以及它们的组合。
  • 环境给智能体提供的观察的形状[1]和边界。
  • 用来执行动作的step方法,它会返回当前的观察、奖励以及片段是否结束的指示。
  • reset方法会将环境初始化成最初状态并返回第一个观察。

是时候详细讨论一下环境的各个组件了。

2.3.1 动作空间

如前所述,智能体执行的动作可以是离散的、连续的,或两者的组合。离散动作是智能体可以执行的一组固定的操作,比如,网格中的方向移动:向左、向右、向上或向下。另一个例子是按键,可以是按下去或释放。这些状态都是互斥的,因为离散动作空间的主要特征就是在有限的动作集合中,只能选择一个动作执行。

连续动作会附上一个数值,例如控制方向盘,它能转到特定的角度,再比如油门踏板,它能用不同的力度踩压。连续动作会有一个数值范围限制的描述。在控制方向盘的场景下,范围限制为–720°~720°。油门踏板则通常是0~1。

当然,我们并没有限制说只能执行一个动作。在环境中可以同时执行多个动作,例如同时按下好几个按钮,或者控制方向盘的同时踩两个踏板(刹车和油门)。为了支持这样的场景,Gym定义了一个特殊的容器类,允许用户嵌入好几个动作组成一个组合动作。

2.3.2 观察空间

如第1章所述,观察是除了奖励之外,环境在每一时刻提供给智能体的信息。观察可以简单如一串数字,也可以复杂如包含来自多个摄像机彩色图像的多维张量。观察甚至可以像动作空间一样是离散的。离散观察空间的一个例子是灯泡,它可以处于两种状态——开或关,以布尔值的形式提供给我们。

因此,动作和观察之间存在某种相似性,图2.1展示了它们的类如何在Gym中表示。

041-01

图2.1 Gym中Space类的层级

最基本的抽象类Space包含两个我们关心的方法:

  • sample():从该空间中返回随机样本。
  • contains(x):校验参数x是否属于空间。

两个方法都是抽象方法,会在每个Space的子类被重新实现:

  • Discrete类表示一个互斥的元素集,用数字0到n–1标记。它只有一个字段n,表示它包含的元素个数。例如,Discrete(n=4)表示动作空间有四个方向(上、下、左、右)可以移动。
  • Box类表示有理数的n维张量,范围在[low, high]之间。例如,油门踏板只有一个0.0~1.0之间的值,它能被编码成Box(low=0.0, high=1.0, shape=(1,), dtype=np.float32)shape参数被赋值成长度为1、值为1的元组,为我们提供了只有一个值的一个一维张量)。dtype参数指定空间的值类型,在此将其指定成NumPy 32-bit float。另一个Box的例子可以是Atari[2]屏幕的观察(稍后将介绍更多Atari环境),它是一幅210×160的RGB(red, green, blue)图像:Box(low=0, high=255, shape=(210, 160, 3), dtype=np.uint8)。在这个场景下,shape参数是有三个元素的元组,第一个维度是图像的高,第二个维度是图像的宽,第三个维度的值是3,它对应于三原色的红绿蓝。因此,总结来说,每个观察都是一个有100 800字节的三维张量。
  • 最后一个Space的子类是Tuple类,它允许我们将不同的Space实例组合起来使用。这使我们能够创建出任何动作空间和观察空间。例如,想象一下我们要为汽车创建一个动作空间。汽车每时每刻有好几个可以改变的控制手段,包括:方向盘的方向、刹车踏板的位置,以及油门踏板的位置。这三个控制手段能在一个Box实例中,用三个浮点数来指定。除了这三个最基本的控制手段,汽车还有一些额外的离散控制手段,例如转向灯(可能的状态是关闭、右、左)或喇叭(开或关)。为了将所有的这些组合到一个动作空间类中去,可以创建Tuple(spaces=(Box(low=-1.0, high=1.0, shape=(3,), dtype=np.float32), Discrete(n=3), Discrete(n=2)))。这样过于灵活的用法很少见到,例如,在本书中,你只会看到BoxDiscrete的动作空间和观察空间,但是Tuple类在某些场景下是很有用的。

Gym中还定义了一些其他的Space子类,但是前述的三个是最有用的。所有子类都实现了sample()contains()方法。sample()方法根据Space类以及传入的参数进行随机采样。它常被用在动作空间,用来选取随机的动作。contains()方法用来检验传入的参数是否符合Space给定的可选参数,Gym内部会用它来检验智能体的动作是否合理。例如,Discrete.sample()返回一个范围中的随机离散元素,Box.sample()则会返回一个维度正确的随机张量,张量的值都会在给定范围内。

每个环境都有两个类型为Space的成员:action_spaceobservation_space。这使得我们能够创建适用于任何环境的通用代码。当然,处理屏幕上的像素和处理离散的观察会有所不同(例如前面那个例子,你可能更想用卷积层或其他计算机视觉的工具库来处理图像),大多时候,Gym并不会阻止我们去编写通用的代码,一般只有为特定的环境或一组环境优化代码时才需要定制代码。

2.3.3 环境

如上所述,在Gym中环境用Env类表示,它包含下面这些成员:

  • action_spaceSpace类的一个字段,限定了环境中允许执行的动作。
  • observation_space:也是Space类的一个字段,但是限定了环境中允许出现的观察。
  • reset():将环境重置到初始状态,返回一个初始观察的向量。
  • step():这个方法允许智能体执行动作,并返回动作结果的信息——下一个观察、立即奖励以及片段是否结束的标记。这个方法有一点复杂,我们会在本节后面详细讨论。

Env类中还有一些我们不会用到的实用方法,例如render(),它允许我们获得人类可读形式的观察。你可以在Gym的文档中找到这些方法的完整列表,在本书中我们还是要集中关注Env的核心方法:reset()step()

到目前为止,你已经见过代码如何获取环境的动作和观察信息,因此现在你需要熟悉动作本身了。通过stepreset可以和环境进行交互。

由于reset更加简单,我们会从它开始。reset()方法没有参数,它命令环境将自己重置成初始状态,并返回初始观察。注意,必须在环境创建后调用reset()。你应该还记得第1章中说的,智能体和环境的交互可能是会终止的(比如屏幕上显示“游戏结束”)。这样的一个时间段称为片段,而智能体在片段结束后,需要重新开始。该方法返回的值是环境中的第一个观察。

step()方法是环境的核心部分。它在一次调用中会完成多项操作,如下所示:

  • 告诉环境下一步将要执行哪一个动作。
  • 在执行动作后获取该动作产生的新的观察。
  • 获取智能体在这一步获得的奖励。
  • 获取片段是否结束的标记。

第一项(动作)是该方法的唯一参数,剩余几项是step()方法的返回值。准确地说,这是一个包含4个元素(observation, reward, done, info)的元组(这是Python元组而不是前一节所讨论的Tuple类)。它们的类型和意义如下:

  • observation:包含观察数据的NumPy向量或矩阵。
  • reward:浮点数的奖励值。
  • done:布尔值标记,如果是True则片段结束。
  • info:包含环境信息的任何东西,它和具体的环境有关。通常的做法是在通用RL方法中忽略这个值(不考虑和特定环境相关的一些细节信息)。

你可能从智能体的代码中已经知道环境怎么用了——在环境中,我们会不停地指定动作来调用step()方法,直到方法返回的done标记为True。然后调用reset()方法重新开始。唯一遗漏的部分就是一开始如何创建Env对象。

2.3.4 创建环境

每个环境都有唯一的名字,形式为环境名-vN。N用来区分同一个环境的不同版本(例如,一些环境修复了bug或有重要的更新时会升级版本)。为了创建环境,gym包提供了函数make(env_name),它唯一的参数就是字符串形式的环境名。

在撰写本文时,Gym的版本是0.13.1,包含859个不同名字的环境。当然,并不是有这么多独立的环境,因为包含了环境的多个版本。此外,同样的环境在不同的版本下设置或观察空间可能会有变化。例如,Atari的Breakout游戏有这么多环境名字:

  • Breakout-v0、Breakout-v4:最原始的Breakout游戏,球的初始位置和方向是随机的。
  • BreakoutDeterministic-v0、BreakoutDeterministic-v4:球的初始位置和速度矢量总是一样的Breakout游戏。
  • BreakoutNoFrameskip-v0、BreakoutNoFrameskip-v4:每一帧都展示给智能体的Breakout游戏。
  • Breakout-ram-v0、Breakout-ram-v4:取代屏幕像素,用内存模拟(128字节)观察的Breakout游戏。
  • Breakout-ramDeterministic-v0、Breakout-ramDeterministic-v4
  • Breakout-ramNoFrameskip-v0、Breakout-ramNoFrameskip-v4

总体而言,一共有12个Breakout游戏的环境。游戏的截图如图2.2所示(以防你从来没有玩过)。

044-01

图2.2 Breakout的游戏截图

即使删除了这些重复项,0.13.1版本的Gym仍提供了154个独立环境,分成以下几组:

  • 经典控制问题:这些是玩具任务,用于最优控制理论和RL论文的基准或演示。它们一般比较简单,观察空间和动作空间的维度比较低,但是在快速验证算法的实现时它们还是比较有用的。将它们看作“RL”的“MNIST”(MNIST是Yann LeCun创建的手写数字识别数据集,参见http://yann.lecun.com/exdb/mnist/)。
  • Atari 2600:来自20世纪70年代的经典游戏平台上的游戏,一共有63个。
  • 算法:这些问题旨在执行小的计算任务,例如复制观察到的序列或数字相加。
  • 棋盘游戏:围棋和六角棋。
  • Box2D:这些环境使用了Box2D物理引擎来模拟学习走路或汽车的控制。
  • MuJoCo:另一个用于连续控制问题的物理模拟环境。
  • 参数调优:用于调优NN的参数的RL。
  • 玩具文本:简单的网格世界文本环境。
  • PyGame:使用PyGame引擎实现的几个环境。
  • Doom:基于ViZDoom实现的九个小游戏。

完整的环境列表可以在https://gym.openai.com/envs找到,也可以在项目GitHub仓库的wiki页面上找到。OpenAI Universe(目前已经被OpenAI废弃)包含更大的环境集,它提供了一个通用的连接器,用于连接智能体和跑在虚拟机中的Flash、原生游戏、浏览器以及其他真实的应用。OpenAI Universe扩展了Gym的API,但是还遵循同样的设计原则和范式。你可以访问https://github.com/openai/universe来了解它。由于要处理MiniWoB和浏览器自动化,我们会在第13章中进一步使用Universe。

理论已足够!是时候用Python来处理一种Gym环境了。

2.3.5 车摆系统

我们来应用学到的知识探索Gym提供的最简单的RL环境。

045-01

这里,我们导入了gym库,创建了一个叫作CartPole(车摆系统)的环境。该环境来自经典的控制问题,其目的是控制底部附有木棒的平台(见图2.3)。

045-02

图2.3 车摆环境

这里的难点是,木棒会向左或向右倒,你需要在每一步,通过让平台往左或往右移动来保持平衡。

这个环境的观察是4个浮点数,包含了木棒质点的x坐标、速度、与平台的角度以及角速度的信息。当然,通过应用一些数学和物理知识,将这些数字转换为动作来平衡木棒并不复杂,但问题是如何在不知道这些数字的确切含义、只知道奖励的情况下,学会平衡该系统?这个环境每执行一步,奖励都是1。片段会一直持续,直到木棒掉落为止,因此为了获得更多的累积奖励,我们需要以某种避免木棒掉落的方式平衡平台。

这个问题看起来可能比较困难,但是在接下来的两章中,我们会编写一个算法,在无须了解所观察的数字有什么含义的情况下,在几分钟内轻松解决CartPole问题。我们会通过反复试验并加上一点RL魔术来做到这一点。

我们来继续编写代码。

046-01

这里,先重置一下环境并获得第一个观察(在新创建环境时,总会重置一下它)。正如我所说,观察结果是4个数字,我们来看一下如何提前知道这个信息。

046-02

action_space字段是Discrete类型,所以动作只会是0或1,其中0代表将平台推向左边,1代表推向右边。观察空间是Box(4,),这表示大小为4的向量,其值在[-inf, inf]区间内。

046-03

现在,通过执行动作0可以将平台推向左边,然后会获得包含4个元素的元组:

  • 一个新的观察,即包含4个数字的新向量。
  • 值为1.0的奖励。
  • done的标记为False,表示片段还没有结束,目前的状态多少还是可以的。
  • 环境的额外信息,在这里是一个空的字典。

接下来,对action_spaceobservation_space调用Space类的sample()方法。

046-04

这个方法从底层空间返回一个随机样本,在Discrete动作空间的情况下,这意味着为0或1的随机数,而对于观察空间来说,这意味着包含4个数字的随机向量。对观察空间的随机采样看起来没什么用,确实是这样的,但是当不知道如何执行动作的时候,从动作空间进行采样是有用的。在还不知道任何RL方法,却仍然想试一下Gym环境的时候,这个方法尤其方便。现在你知道如何为CartPole环境实现第一个行为随机的智能体了,我们来试一试吧!


[1]原文是shape,表示一个多维数组的形状,比如二维数组[[1,2], [3,4]]的shape是(2,2)。——译者注

[2]Atari是美国诺兰·布什内尔在1972年成立的电脑公司,街机、家用电子游戏机和家用电脑的早期拓荒者。——译者注