1.6 案例:基于Gym库的智能体/环境接口
Gym库(网址为https://www.gymlibrary.dev/)是OpenAI推出的强化学习实验环境库。它是目前最有影响力的强化学习环境库。它用Python语言实现了离散时间智能体/环境接口中的环境部分。整个项目是开源免费的。在这一节,我们将安装和使用Gym库,并通过一个完整的实例来演示智能体与环境的交互。
Gym库实现了上百种环境,还支持自定义环境的扩展。Gym库内置的环境包括以下几类。
❑简单文本环境:包括几个用文本表示的简单游戏。
❑经典控制环境:包括一些简单几何体的运动,常用于经典强化学习算法的研究。
❑二维方块(Box2D)环境:基于Box2D库开发的环境。这些环境利用Box2D库来构造物体、提供图形化界面。
❑Atari游戏环境:包括数十个Atari 2600游戏,它们有像素化的图形界面,希望玩家尽可能争夺高分。
Gym的代码在GitHub上开源,网址为https://github.com/openai/gym。
1.6.1 安装Gym库
Gym库在Windows系统、Linux系统和macOS系统上都可以安装。本节展示如何在Anaconda 3环境里安装Gym库。
安装Gym时,可以选择只进行最小安装,也可以进行更完整的安装。本书大多数内容只需要Gym及其简单文本环境、经典控制环境和Atari子包。安装Gym和这些环境的方法是在安装环境(比如Anaconda 3的管理员模式)里输入下列命令:
注意:本书后续章节的实战环节将反复使用到Gym库(见表1-1),请务必安装Gym库。上述安装命令已经可以完全满足本书前9章对Gym库的需求。后续章节用到其他扩展库时会介绍更多的安装方法。Gym库也在不断更新中,推荐按需安装,不需要追求大而全。完整的安装方法会在GitHub上更新。
表1-1 本书实例的智能体和环境依赖的主要Python扩展库
1.6.2 使用Gym库
本节介绍Gym库的使用。
要使用Gym库,首先要导入Gym库。导入Gym库的方法如下:
在导入Gym库后,可以通过gym.make()函数来得到环境对象。每一个环境任务都有一个ID,它是形如“Xxxxx-vd”的Python字符串,如'CartPole-v0'、'Taxi-v3'等。任务名称最后的部分表示版本号,不同版本的任务可能有不同的行为。获得任务'CartPole-v0'的一个环境对象的代码为:
想要查看当前Gym库已经注册了哪些任务,可以使用以下代码:
每个任务都定义了自己的观测空间和动作空间。环境env的观测空间用env.observation_space表示,动作空间用env.action_space表示。Gym库提供了gym.spaces.Box类来表示空间,空间中的元素类型为np.array。元素个数有限的空间也可以用gym.spaces.Discrete类表示,空间中的元素类型为int。Gym还定义了其他空间类型。例如,环境'CartPole-v0'的观测空间是Box(4,),表示观测可以用形状为(4,)的np.array对象表示;环境'CartPole-v0'的动作空间是Discrete(2),表示动作取值自{0,1}。对于Box对象表示的空间,可以用成员low和high查看每个浮点数的取值范围,对于Discrete对象表示的空间,可以用成员n查看有几个可能的取值。
接下来使用环境对象env。首先我们初始化环境对象。初始化环境对象env的代码为:
该调用能返回初始观测observation和信息info。观测的类型是和env.observation_space兼容的。比如,'CartPole-v0'的observation_space是Box(4,),所以观测的类型是形状为(4,)的np.array对象。
接下来我们使用环境对象的step()方法来完成每一次的交互。step()方法有一个参数,是动作空间中的一个动作。该方法返回值包括以下五个部分。
❑观测(observation):表示观测,与env.reset()第一个返回值的含义相同。
❑奖励(reward):float类型的值。
❑回合终止指示(terminated):bool类型的数值。Gym库里的实验环境大多都是回合制的。这个返回值可以指示在当前动作后回合是否结束。如果回合结束了,可以通过env.reset()开始下一回合。
❑回合截断指示(truncated):bool类型的数值。无论是回合制任务还是连续型任务,我们都可以限制回合的最大步数,使其成为一个回合步数有限的回合制任务。当一个回合内的步数达到最大步数时,回合截断,该指示为True。还有一些情况,由于环境实现的限制,回合运行到某个步骤后资源不够了(比如内存不够了,或是超出了预先设计好的数据范围),这时只好对回合进行截断。
❑其他信息(info):dict类型的值,含有一些调试信息。不一定要使用这个信息。与env.reset()第二个返回值的含义相同。
每次调用env.step()只会让环境前进一步。所以,env.step()往往放在循环结构里,通过循环调用来完成整个回合。
在env.reset()或env.step()后,可以用下列语句以图形化的方法显示当前环境。
环境使用完后,可以使用下列语句关闭环境。
注意:如果你绘制了实验的图形界面窗口,那么关闭该窗口的最佳方式是调用env.close()。直接试图关闭图形界面窗口可能会导致内存不能释放,甚至会导致死机。
学术界在测试智能体在Gym库中某个任务的性能时,一般最关心100个回合的平均回合奖励。至于为什么是100个回合而不是其他回合数(比如128个回合),完全是习惯使然,没有什么特别的原因。对于有些环境,还会指定一个参考的回合奖励值,当连续100个回合的奖励大于指定的值时,认为这个任务被解决了。但是,并不是所有的任务都指定了这样的值。对于没有指定值的任务,就无所谓任务被解决了或是没有被解决。
对于有参考回合奖励参考阈值的环境,回合奖励参考阈值存储在下列变量中:
在线内容:本书GitHub给出了Gym库部分内容的源码解读,供学有余力的读者查阅。本节涉及的类包括gym.Env类、gym.space.Space类、gym.space.Box类、gym.space.Discrete类、gym.Wrapper类、gym.wrapper.TimeLimit类。
1.6.3 小车上山
本节通过一个完整的例子来学习如何与Gym库中的环境交互。本节选用的例子是一套经典的控制任务:小车上山。这套任务有两个版本,版本MountainCar-v0的动作空间是有限集,版本MountainCarContinuous-v0的动作空间是连续动作空间。本节主要关心交互的Gym的API的使用,而不详细介绍这个任务的内容及其求解方法。任务的具体描述和求解方式会在后文中介绍。
首先我们来关注有限动作空间的版本MountainCar-v0。每接触到一个新的任务,一定要试图了解任务。首先要了解的就是这个任务的观测空间是什么、动作空间是什么。我们可以用代码清单1-1查看这个任务的观测空间和动作空间。
值得一提的是,本书使用logging模块来打印,而不是直接使用print()函数。logging模块在输出时可以同时输出时间戳,有助于了解程序运行时间。
代码清单1-1 查看MountainCar-v0的观测空间和动作空间
上述代码的运行结果为:
运行结果告诉我们:
❑动作空间action_space是Discrete(3),所以动作是取自{0,1,2}的int型数值。
❑观测空间observation_space是Box(2,),所以观测是形状为(2,)的浮点型np.array。
❑每个回合的最大步数max_episode_steps是200。
❑参考的回合奖励值reward_threshold是-110,如果连续100个回合的平均回合奖励大于-110,则认为这个任务被解决了。
接下来我们准备一个和环境交互的智能体。Gym里面一般没有智能体,智能体需要我们自己实现。代码清单1-2给出了一个针对这个任务的智能体ClosedFormAgent类。智能体的step()方法实现了决策功能。ClosedFormAgent类是一个比较简单的类,它只能根据给定的数学表达式进行决策,并且不能有效学习,所以它并不是一个真正意义上的强化学习智能体类。但是,用于演示智能体和环境的交互已经足够了。
代码清单1-2 根据指定确定性策略决定动作的智能体,用于MountainCar-v0
接下来我们试图让智能体与环境交互。代码清单1-3中的play_episode()函数可以让智能体和环境交互一个回合。这个函数可以接受以下参数。
❑参数env是环境类。
❑参数agent是智能体类。
❑参数seed可以是None或是一个int类型的变量,用作初始化回合的随机数种子。
❑参数mode是None或是str类型的变量'train'。如果是'train',则试图让智能体进行学习。当然,如果智能体没有学习功能,这个参数就没有作用。
❑参数render是bool类型变量,指示在运行过程中是否要图形化显示。如果函数参数render为True,那么在交互过程中会调用env.render()以显示图形化界面。
这个函数返回episode_reward和elapsed_step,它们分别是float类型和int类型,表示智能体与环境交互一个回合的回合总奖励和交互步数。
代码清单1-3 智能体和环境交互一个回合的代码
借助于代码清单1-1给出的环境、代码清单1-2给出的智能体和代码清单1-3给出的交互函数,我们可以用下列代码让智能体和环境交互一个回合,并在交互过程中图形化显示。交互完毕后,可用env.close()关闭图形化界面。然后,我们使用了Python语言内置的logging模块来输出运行的结果。您也可以使用print()函数来输出结果。不过,我还是推荐您使用logging模块来输出,因为它能帮助我们了解每个输出语句是什么时候输出的,让我们更好地估计程序的运行时间。很多强化学习的算法运行时间很长,了解输出的时间便于我们估计程序的运行进度。
为了系统性地评估智能体的性能,代码清单1-4求了连续100个回合交互的平均回合奖励。ClosedFormAgent类对应的策略的平均回合奖励大概在-103,超过了奖励阈值-110。所以,智能体ClosedFormAgent解决了这个任务。
代码清单1-4 运行100回合交互求平均回合奖励
接下来我们来看连续动作空间的任务MountainCarContinuous-v0。我们将代码清单1-1略作改动,使用代码清单1-5导入环境。
代码清单1-5 查看MountainCarContinuous-v0的观测空间和动作空间
这样得到的输出为:
这个环境的动作空间是Box(1,),动作是形状为(1,)的np.array对象;观测空间仍然是Box(2,),观测是形状为(2,)的np.array对象。回合最大步数变为999步。成功求解的阈值变为90,即需要在连续100回合的平均回合奖励超过90。
不同的任务往往需要使用不同的智能体来求解。代码清单1-6给出了用于求解MountainCarContinuous-v0的智能体。在成员step()中,观测observation分解为位置position和速度velocity两个分量,然后用这两个分量决定的大小关系决定采用何种动作action。我们可以再用代码清单1-3和代码清单1-4来测试这个智能体的性能,可以知道这个智能体平均回合奖励大概在93左右,大于阈值90。所以,代码清单1-6给出的这个智能体成功求解了MountainCarContinuous-v0任务。
代码清单1-6 用于求解MountainCarContinuous-v0的智能体