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')