2.5 Gym的额外功能:包装器和监控器
到目前为止,我们已经讨论了Gym核心API的三分之二,以及编写智能体所需的基础功能。剩下的API你可以不学,但是它们能让你更轻松地编写整洁的代码。所以还是简单介绍一下剩余的API!
2.5.1 包装器
很多时候,你希望以某种通用的方式扩展环境的功能。例如,想象一个环境,它给了你一些观察,但是你想将它们累积缓存起来,用以提供智能体最近N个观察。这在动态计算机游戏中是一个很常见的场景,比如单一一帧不足以了解游戏状态的完整信息。例如,你希望能裁剪或预处理一些图像像素以便智能体来消化这些信息,又或者你想以某种方式归一化奖励值。有相同结构的场景太多了,你可能想要将现有的环境“包装”起来并附加一些额外的逻辑。Gym为这些场景提供了一个方便使用的框架——Wrapper
类。
类的结构如图2.4所示。
图2.4 Gym中Wrapper类的层级
Wrapper
类继承自Env
类。它的构造函数只有一个参数,即要被“包装”的Env
类的实例。为了附加额外的功能,需要重新定义想扩展的方法,例如step()
或reset()
。唯一的要求就是需要调用超类中的原始方法。
为了处理更多特定的要求,例如Wrapper
类只想要处理环境返回的观察或只处理动作,那么用Wrapper
的子类过滤特定的信息即可。它们分别是:
ObservationWrapper
:需要重新定义父类的observation(obs)
方法。obs
参数是被包装的环境给出的观察,这个方法需要返回给予智能体的观察。RewardWrapper
:它暴露了一个reward(rew)
方法,可以修改给予智能体的奖励值。ActionWrapper
:需要覆盖action(act)
方法,它能修改智能体传给被包装环境的动作。
为了让它更实用,假设有一个场景,我们想要以10%的概率干涉智能体发出的动作流,将当前动作替换成随机动作。这看起来不是一个明智的决定,但是这个小技巧可以解决第1章提到的利用与探索问题,它是最实用、最强大的方法之一。通过发布随机动作,让智能体探索环境,时不时地偏离它原先策略的轨迹。使用ActionWrapper
类很容易就能实现(完整的例子见Chapter02/03_random_action_wrapper.py
)。
先通过调用父类的__init__
方法初始化包装器,并保存epsilon(随机动作的概率)。
我们需要覆盖这个方法,并通过它来修改智能体的动作。每一次都先掷骰子,都会有epsilon的概率从动作空间采样一个随机动作,用来替换智能体传给我们的动作。注意,这里用了action_space
和包装抽象,这样就能写抽象的代码了,这适用于Gym的任意一个环境。另外,每次替换动作的时候必须将消息打印出来,以验证包装器是否生效。当然,在生产代码中,这不是必需的。
是时候应用一下包装器了。创建一个普通的CartPole环境,并将其传入Wrapper
构造函数。然后,将Wrapper
类当成一个普通的Env
实例,用它来取代原始的CartPole。因为Wrapper
类继承自Env
类,并且暴露了相同的接口,我们可以任意地嵌套包装器。这是一个强大、优雅且通用的解决方案。
除了智能体比较笨,每次都选择同样的0号动作外,代码几乎相同。通过运行代码,应该能看到包装器确实在生效了:
如果愿意,可以在包装器创建时指定epsilon参数,验证这样的随机性平均下来,会提升智能体得到的分数。
继续来看Gym中隐藏的另外一个有趣的宝藏:Monitor
(监控器)。
2.5.2 监控器
另一个应该注意的类是Monitor
。它的实现方式与Wrapper
类似,可以将智能体的性能信息写入文件,也可以选择将智能体的动作录下来。之前,还可以将Monitor
类的记录结果上传到https://gym.openai.com,查看智能体和其他智能体对比的结果排名(见图2.5),但不幸的是,在2017年8月末,OpenAI决定关闭此上传功能并销毁所有原来的结果。虽然有好几个提供相同功能的网站,但是它们都还没完全准备好。希望这个窘境能很快被解决,但是在撰写本书时,还无法将自己的智能体和他人的进行比较。
为了让你大致了解Gym的网页,图2.5给出了CartPole环境的排行榜。
图2.5 Gym网站上的CartPole提交页面
在网页上的每次提交都包含了训练的动态详情。例如,图2.6是我训练《毁灭战士》迷你游戏时得到的结果记录。
图2.6 DoomDefendLine环境提交后的动态显示
尽管如此,Monitor
仍然很有用,因为你可以查看智能体在环境中的行动情况。所以,还是看一下如何将Monitor
加入随机CartPole智能体中,唯一的区别就是下面这段代码(完整的代码见Chapter02/04_cartpole_random_monitor.py
):
传给Monitor
类的第二个参数是监控结果存放的目录名。目录不应该存在,否则程序会抛出异常(为了解决这个问题,要么手动删除目录,要么将force=True
的参数传入Monitor
的构造函数)。
Monitor
类要求系统中有FFmpeg工具,用来将观察转换成视频文件。这个工具必须存在,否则Monitor
将抛出异常。安装FFmpeg的最简单方式就是使用系统的包管理器,每个操作系统安装的方式都不同。
要执行此示例的代码,还应该满足以下三个前提中的一个:
- 代码应该在带有OpenGL扩展(GLX)的X11会话中运行。
- 代码应该在
Xvfb
虚拟显示器中运行。 - 在SSH连接中使用X11转发。
这样做的原因是Monitor
需要录制视频,也就是不停地对环境绘制的窗口进行截屏。一些环境使用OpenGL来画图,所以需要OpenGL的图形模式。云虚拟机可能会比较麻烦,因为它们没有显示器以及图形界面。为了解决这个问题,可以使用特殊的“虚拟”显示器,它被称为Xvfb(X11虚拟帧缓冲器),它会在服务器端启动一个虚拟的显示器并强制程序在它里面绘图。这足以使Monitor
愉快地生成视频了。
为了在程序中使用Xvfb环境,需要安装它(通常需要安装xvfb
包)并执行特定的脚本xvfb-run
:
从前面的日志可以看到,视频已经成功写入,因此可以通过播放来窥视智能体的某个部分。
另一个录制智能体动作的方法是使用SSH X11转发,它使用SSH的能力在X11客户端(想要显示图形信息的Python代码)和X11服务器(能访问物理显示器并知道如何显示这些图形信息的软件)之间构建了一个X11通信隧道。
在X11架构中,客户端和服务器能被分离到不同的机器上。为了使用这个方法,需要:
1)一个运行在本地机器上的X11服务器。X11服务器是Linux上的标准组件(所有的桌面环境都使用X11)。在Windows机器上,可以使用第三方X11实现,比如开源软件VcXsrv(https://sourceforge.net/projects/vcxsrv/)。
2)通过SSH登录远程机器的能力,传入-X
命令行选项:ssh -X servername
。该命令会建立X11隧道,并允许所有在这个会话中启动的程序访问本地的显示器输出图像。
然后,你就能启动使用Monitor
类的程序,它会捕获智能体的动作,并保存成视频文件。