7.4 编写OpenAI Gym环境

OpenAI Gym虽然提供了大量的环境便于大家集中精力开发强化学习算法,但是实际工作中我们经常需要自己编写对应的环境,尤其是在安全领域。下面我们将结合一个实际的例子介绍如何编写自己的OpenAI Gym环境,本质上OpenAI Gym环境也是个Python对象。本节的代码在GitHub的code/gold.py。

回顾OpenAI Gym的Hello World!例子中,使用到的环境属性:

·states,返回环境的状态空间。

·actions,返回环境的动作空间。

·reset,重置环境。

·step,针对环境执行动作,使环境进入下一个状态。

因此最简化的环境对象也需要有以上这些属性和方法才行,假设该对象叫作demo,demo的类图如图7-6所示。

图7-6 demo类的类图

对应到Python代码,我们定义我们的类名称为GridEnv,继承gym.Env。下面我们将逐步完善GridEnv类的定义。


class GridEnv(gym.Env):
    def __init__(self):
        self.states = []
        self.actions = []
    def _step(self, action):
    def _reset(self):

以经典的金币问题为例,我们编写自己的OpenAI Gym环境。在金币问题中,如图7-7所示,一共有8个格子,也可以理解有8种状态,选手随机从这8个格子中的一个出发,如果达到7号格子,表明拿到了金币,游戏结束;如果达到6或者8号格子,表明选手死亡,游戏也结束。选手可以在这个8个格子中上下左右移动,但是不允许走出格子。

图7-7 金币问题

以OpenAI Gym的视角来看待金币问题,状态空间是离散的,一共有8种:


self.states = [1,2,3,4,5,6,7,8]

动作空间也是离散的,一共有4种,分别代表上下左右,也可以用东南西北来表示:


self.actions = ['n','e','s','w']

游戏的初衷是选手拿到金币,避免死亡,所以从奖励的角度讲,拿到金币奖励1,死亡奖励-1,其他为0。代码上通过一个字典变量rewards保存这一信息,表明选手从1号或者5号格子往南移动,奖励-1分;选手从3号格子往南移动奖励1分,其余为0分。


self.rewards = dict();
self.rewards['1_s'] = -1.0
self.rewards['3_s'] = 1.0
self.rewards['5_s'] = -1.0

当选手拿到金币或者死亡时游戏结束,所以需要定义游戏结束对应的状态:


self.terminate_states = dict()
self.terminate_states[6] = 1
self.terminate_states[7] = 1
self.terminate_states[8] = 1

环境初始化时,需要随机设置状态,类通过self.state记录当前的状态,使用Python的随机函数random.random()可以随机产生一个0~1之间的随机数,利用这个随机数可以在状态空间中随机选择一个初始化状态。比如随机数是0.5,那么random.random()*len(self.states))得到4.0,int函数处理后为4,得到的状态是self.states[4],最终得到的是状态5。代码如下:


def _reset(self):
    self.state = self.states[int(random.random() * len(self.states))]
    return self.state

这里需要强调的是,Python里面的int函数是截取整数部分,比如处理4.1和3.9的结果就分别是4和3:


print int(4.1)
print int(3.9)
4
3

金币问题里面最核心的是状态迁移的关系,因为在step函数里面主要涉及的其实就是状态迁移,需要解决的问题就是如何简便地表达这种迁移关系,如何表达状态s执行动作A后迁移到下一个状态。由于金币问题的状态迁移仅仅依赖当前状态和执行的动作,我们以状态迁移表的形式体现,见表7-1。其中编号为6、7、8的格子,表明游戏已经结束,任何动作也不会导致状态迁移;另外,对于像格子以外移动的情况,状态保持不变,不在迁移表里体现了,比如编号为1的格子往北移动,就挪动到格子外面去了,不会导致状态改变,但是如果从1号格子往南移动,就会挪到6号格子,状态迁移成了6,对应到迁移表就是状态编号1执行了动作编号s就变成了6。

表7-1 金币问题的状态迁移表

我们可以用一个二维数组表达这个状态迁移表,但是这个迁移表非常稀疏,所以也可以用一个字典表示,字典的键值表示为state_action,比如状态编号1执行了动作编号s就变成了状态编号6,这可以表述为键值为1_s,对应的值为6:


self.t = dict();
self.t['1_s'] = 6

完整的状态迁移表以字典形式定义如下:


self.t = dict();
self.t['1_s'] = 6
self.t['1_e'] = 2
self.t['2_w'] = 1
self.t['2_e'] = 3
self.t['3_s'] = 7
self.t['3_w'] = 2
self.t['3_e'] = 4
self.t['4_w'] = 3
self.t['4_e'] = 5
self.t['5_s'] = 8
self.t['5_w'] = 4

定义好了状态迁移表,就可以开始编写step方法了,step的重要逻辑如图7-8所示。首先获取当前状态,如果当前状态已经在标号为6~8的格子里面,表明游戏结束了,可以直接返回。代码如下:


state = self.state
#判断是否游戏结束
if state in self.terminate_states:
    return state, 0, True, {}

接着查询键值state_action,从self.t中查找对应的状态迁移关系,如果查找到就更新状态,如果查找不到就维持现有状态不变。代码如下:


key = "%d_%s"%(state, action)
#查找状态迁移表
if key in self.t:
    next_state = self.t[key]
else:
    #查不到就维持现有状态不变
    next_state = state
self.state = next_state

更新状态后查询游戏是否结束:


is_terminal = False
if next_state in self.terminate_states:
   is_terminal = True

获取当前的奖励值,在self.rewards中查询,如果查询失败,奖励值为0:


if key not in self.rewards:
    r = 0.0
else:
    r = self.rewards[key]

至此我们完成了GridEnv的编写,如何才能让这个类生效呢?假设Gym的安装路径为:


/opt/gym

拷贝我们的类文件gold.py到以下目录:


/opt/gym/gym/envs/classic_control

编辑该目录下的文件__init__.py,在最后一行增加如下内容:

图7-8 step函数流程


from gym.envs.classic_control.gold import GridEnv

编辑该目录下的文件__init__.py:


/opt/gym/gym/envs/

在文件的最后增加如下内容,其中id表明该环境的名称,entry_point指定类文件的位置:


#maidou
register(
    id='Gold-v0',
    entry_point='gym.envs.classic_control:GridEnv',
)

这样我们就完成了我们自己编写的环境的配置过程,我们就可以像创建CartPole-v0一样创建我们的金币环境了,创建方法如下:


import gym
env = gym.make('Gold-v0')