Mrli
别装作很努力,
因为结局不会陪你演戏。
Contacts:
QQ博客园

OpenAI Gym使用、rendering画图

2019/10/07 Python RL
Word count: 5,280 | Reading time: 23min

OpenAI Gym使用、rendering画图

gym开源库:包含一个测试问题集,每个问题成为环境(environment),可以用于自己的RL算法开发。这些环境有共享的接口,允许用户设计通用的算法。其包含了deep mind 使用的Atari游戏测试床。

在强化学习中有2个基本概念,一个是环境(environment),称为外部世界,另一个为智能体agent(写的算法)。agent发送action至environment,environment返回观察和回报。

Gym官方文档

Hello gym

1
2
3
4
5
6
7
8
9
import gym
# 创建一个小车倒立摆模型
env = gym.make(‘CartPole-v0’)
# 初始化环境
env.reset()
# 刷新当前环境,并显示
for _ in range(1000):
env.render()
env.step(env.action_space.sample()) # take a random action

设计理念图,一个环境的step函数返回需要的信息,有4种返回值

  • observation
  • reward
  • done :判断是否到了重新设定(reset)环境
  • info :用于调试的诊断信息,有时也用于学习,但智能体(agent )在正式的评价中不允许使用该信息进行学习。

该进程通过调用reset()来启动,它返回一个初始observation。 所以之前代码的更恰当的方法是遵守done的标志:

空间(Spaces)

在上面的例子中,已经从环境的动作空间中抽取随机动作。但这些行动究竟是什么呢? 每个环境都带有action_spaceobservation_space对象。这些属性是Space类型,它们描述格式化的有效的行动和观察。

1
2
3
4
5
6
7
import gym
env = gym.make('CartPole-v0')
# 离散空间允许固定范围的非负数,因此在这种情况下,有效的动作是0或1.
print(env.action_space)
#> Discrete(2)
print(env.observation_space)
#> Box(4,)

Box空间表示一个n维box,所以有效的观察将是4个数字的数组。 也可以检查Box的范围:

1
2
3
4
print(env.observation_space.high)
#> array([ 2.4 , inf, 0.20943951, inf])
print(env.observation_space.low)
#> array([-2.4 , -inf, -0.20943951, -inf])

这种内省可以帮助编写适用于许多不同环境的通用代码。box和discrete是最常见的空间。你可以从一个空间中取样,或者检查某物是否属于它:

1
2
3
4
5
from gym import spaces
space = spaces.Discrete(8) # Set with 8 elements {0, 1, 2, ..., 7}
x = space.sample()
assert space.contains(x)
assert space.n == 8

Env.render画图

参考Gym 简单画图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# 首先,导入库文件(包括gym模块和gym中的渲染模块)
import gym
from gym.envs.classic_control import rendering

# 我们生成一个类,该类继承 gym.Env. 同时,可以添加元数据,改变渲染环境时的参数
class Test(gym.Env):
# 如果你不想改参数,下面可以不用写
metadata = {
'render.modes': ['human', 'rgb_array'],
'video.frames_per_second': 2
}
# 我们在初始函数中定义一个 viewer ,即画板
def __init__(self):
self.viewer = rendering.Viewer(600, 400) # 600x400 是画板的长和框
# 继承Env render函数
def render(self, mode='human', close=False):
# 下面就可以定义你要绘画的元素了
line1 = rendering.Line((100, 300), (500, 300))
line2 = rendering.Line((100, 200), (500, 200))
# 给元素添加颜色
line1.set_color(0, 0, 0)
line2.set_color(0, 0, 0)
# 把图形元素添加到画板中
self.viewer.add_geom(line1)
self.viewer.add_geom(line2)

return self.viewer.render(return_rgb_array=mode == 'rgb_array')

# 最后运行
if __name__ == '__main__':
t = Test()
while True:
t.render()

△.值得注意的是,画板的水平方向是 x 轴, 垂直方向是 y 轴, 且原点在左下角

画个圆

1
2
3
4
5
6
7
8
9
10
11
def render(self, mode='human', close=False):
# 画一个直径为 30 的园
circle = rendering.make_circle(30)

# 添加一个平移操作
circle_transform = rendering.Transform(translation=(100, 200))
# 让圆添加平移这个属性,
circle.add_attr(circle_transform)

self.viewer.add_geom(circle)
return self.viewer.render(return_rgb_array=mode == 'rgb_array')

△注意.是圆心在平移

RingViewr

研究rings时写的render

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
import gym
from gym.envs.classic_control import rendering
import time
import numpy as np
import random


class ringViewer(rendering.Viewer):
'''
画板,直接继承自rendering.Viewer
'''
def __init__(self,width, height, display=None):
super(ringViewer, self).__init__(width, height, display=None)

@staticmethod
def pos2loc(pos=0):
'''
根据位置索引确定画图坐标
:param pos: 位置索引0-9
:return: loc
'''
pass

@staticmethod
def getSize(size):
'''
设置画圆的半径
:param size:[0-2]
:return: radius
'''
pass

@staticmethod
def getColor(c=0):
'''
根据颜色索引选择圆圈颜色
:param c:
:return: list
'''
pass

def drawNewring(self, newring:list=None):
'''
画新生成的圆
:param newring:
:return:
'''
for i in range(len(newring)):
if newring[i] != 0:
ring = rendering.make_circle(radius=self.getSize(i),
res=50,
filled=False)
r, g, b = self.getColor(newring[i])
ring.set_color(r, g, b)
ring_transform = rendering.Transform(translation=(150,30))
ring.add_attr(ring_transform)
self.add_geom(ring)

def _drawQG(self, qgs: list=None):
'''
画棋盘上各个棋格的圆圈
:param qgs:
:return: None
'''
for num,qg in enumerate(qgs):
for i in range(len(qg)):
if qg[i] != 0:
ring = rendering.make_circle(radius=self.getSize(i),
res = 50,
filled=False)
r, g, b = self.getColor(qg[i])
ring.set_color(r, g, b)
ring_transform = rendering.Transform(translation=self.pos2loc(num))
ring.add_attr(ring_transform)
self.add_geom(ring)

def getQG(self, qg: list=None):
'''
将len=27的list转换为[[],[],...]
:param qg: (27,1)的list
:return: (9,1)的list
'''
qgs = []
for x in range(3):
for y in range(3):
tmp = []
for z in range(3):
tmp.append(qg[9*x+3*y+z])
qgs.append(tmp)
self._drawQG(qgs)


class Testenv(gym.Env):
# 如果你不想改参数,下面可以不用写
metadata = {
'render.modes': ['human', 'rgb_array'],
'video.frames_per_second': 2
}

def __init__(self):
self.viewer = ringViewer(300, 400) # 600x400 是画板的长和框
self.state:list = []
self.state:list = []

def setState(self, state):
self.state = state

def setNewring(self, newring=None):
self.newring = newring

def render(self, mode='human', close=False):
# 由于没有找到viewer源码中删除组件的代码,于是每次在渲染前 清空上一次geoms和onetime_geoms列表 来达到消除的目的
if self.state.any():
self.viewer.geoms.clear()
self.viewer.onetime_geoms.clear()
self.viewer.getQG(self.state)
if self.newring:
self.viewer.drawNewring(self.newring)

return self.viewer.render(return_rgb_array=mode == 'rgb_array')


if __name__ == '__main__':
v = Testenv()
while True:
v.setState(np.random.randint(0,6,(27)))
v.setNewring([random.randint(0,5) for x in range(3)])
print(v.state)
print(v.newring)
v.render()
time.sleep(2)

△.由于没有找到viewer源码中删除组件的代码,于是每次在渲染前 清空上一次geoms和onetime_geoms列表 来达到消除的目的

效果图如下

ring

深入剖析gym环境构建[转]

由于该博客的代码展示实在太乱,于是重新帮他排版了一下

我们继续讲,从第1小节的尾巴开始。有三个重要的函数:

  • env = gym.make(‘CartPole-v0’)
  • env.reset()
  • env.render()

第一个函数是创建环境,我们会在第3小节具体讲如何创建自己的环境,所以这个函数暂时不讲。第二个函数env.reset()和第三个函数env.render()是每个环境文件都包含的函数。我们以cartpole为例,对这两个函数进行讲解。

Cartpole的环境文件在~你的gym目录/gym/envs/classic_control/cartpole.py.

该文件定义了一个CartPoleEnv的环境类,该类的成员函数有:seed(), step(),reset()和render(). 第1小节调用的就是CartPoleEnv的两个成员函数reset()和render()。下面,我们先讲讲这两个函数,再介绍step()函数

2.1 reset()函数详解

reset()为重新初始化函数。那么这个函数有什么用呢?

在强化学习算法中,智能体需要一次次地尝试,累积经验,然后从经验中学到好的动作。一次尝试我们称之为一条轨迹或一个episode. 每次尝试都要到达终止状态. 一次尝试结束后,智能体需要从头开始,这就需要智能体具有重新初始化的功能。函数reset()就是这个作用。

reset()的源代码为:

1
2
3
4
5
6
7
def _reset()
# 利用均匀随机分布初试化环境的状态
self.state = self.np_random.uniform(low=-0.05, high=0.05, size=(4,))
# 设置当前步数为None
self.steps_beyond_done = None
# 返回环境的初始化状态
return np.array(self.state)

2.2 render()函数详解

render()函数在这里扮演图像引擎的角色。一个仿真环境必不可少的两部分是物理引擎图像引擎。物理引擎模拟环境中物体的运动规律;图像引擎用来显示环境中的物体图像。其实,对于强化学习算法,该函数可以没有。但是,为了便于直观显示当前环境中物体的状态,图像引擎还是有必要的。另外,加入图像引擎可以方便我们调试代码。下面具体介绍gym如何利用图像引擎来创建图像。

我们直接看源代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from gym.envs.classic_control import rendering
# 这一句导入rendering模块,利用rendering模块中的画图函数进行图形的绘制
class myenv(gym.Env)
def _render(self, mode=’human’, close=False):
if close:
pass #省略,直接看关键代码部分
if self.viewer is None:
# 如绘制600*400的窗口函数为:
self.viewer = rendering.Viewer(screen_width, screen_height)
# 其中screen_width=600, screen_height=400
# 创建小车的代码为:
l,r,t,b = -cartwidth/2, cartwidth/2, cartheight/2, -cartheight/2
axleoffset =cartheight/4.0
cart = rendering.FilledPolygon([(l,b), (l,t), (r,t), (r,b)])
# 其中rendering.FilledPolygon为填充一个矩形。

创建完cart的形状,接下来给cart添加平移属性和旋转属性。将车的位移设置到cart的平移属性中,cart就会根据系统的状态变化左右运动。具体代码解释,我已上传到github上面了,gxnk/reinforcement-learning-code 。想深入了解的同学可去下载学习。

2.3 step()函数详解

该函数在仿真器中扮演物理引擎的角色。其输入是动作a,输出是:下一步状态,立即回报,是否终止,调试项。

该函数描述了智能体与环境交互的所有信息,是环境文件中最重要的函数。在该函数中,一般利用智能体的运动学模型和动力学模型计算下一步的状态和立即回报,并判断是否达到终止状态。

我们直接看源代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def _step(self, action):
assert self.action_space.contains(action), "%r (%s) invalid"%(action, type(action))
state = self.state
x, x_dot, theta, theta_dot = state #系统的当前状态
force = self.force_mag if action==1 else -self.force_mag #输入动作,即作用到车上的力
costheta = math.cos(theta) #余弦函数
sintheta = math.sin(theta) #正弦函数
#底下是车摆的动力学方程式,即加速度与动作之间的关系。
temp = (force + self.polemass_length * theta_dot * theta_dot * sintheta) / self.total_mass
thetaacc = (self.gravity * sintheta - costheta* temp) / (self.length * (4.0/3.0 - self.masspole * costheta * costheta / self.total_mass)) #摆的角加速度
xacc = temp - self.polemass_length * thetaacc * costheta / self.total_mass #小车的平移加速
x = x + self.tau * x_dot
x_dot = x_dot + self.tau * xacc
theta = theta + self.tau * theta_dot
theta_dot = theta_dot + self.tau * thetaacc #积分求下一步的状态
self.state = (x,x_dot,theta,theta_dot)

2.4 一个简单的demo

下面,我给出一个最简单的demo,让大家体会一下上面三个函数如何使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import gym
import time
env = gym.make('CartPole-v0')
#创造环境observation = env.reset()
#初始化环境,observation为环境状态
count = 0
for t in range(100):
action = env.action_space.sample()
#随机采样动作
observation, reward, done, info = env.step(action)
#与环境交互,获得下一步的时刻
if done:
break
env.render()
#绘制场景
count+=1
time.sleep(0.2)
#每次等待0.2s
print(count)
#打印该次尝试的步数

第3小节:创建自己的gym环境并利示例qlearning的方法

在上一小节中以cartpole为例子深入剖析了gym环境文件的重要组成。我们知道,一个gym环境最少的组成需要包括reset()函数和step()函数。当然,图像显示函数render()一般也是需要的。这一节,我会以机器人找金币为例给大家演示如何构建一个全新的gym环境,并以此环境为例,示例最经典的强化学习算法qlearning算法。在3.1节中,给出机器人找金币的问题陈述;第3.2节中,给出构建gym环境的过程;第3.3节中,利用qlearning方法实现机器人找金币的智能决策。全部代码已传到github上。

3.1 机器人找金币的问题陈述

img

图1.1 机器人找金币

如图1.1 为机器人在网格世界找金币的示意图。该网格世界一共有8个状态,其中状态6和状态8为死亡区域,状态7为金币区域。机器人的初始位置为网格世界中任意一个状态。机器人从初始状态出发寻找金币。机器人进行一次探索,进入死亡区域或找到金币,本次探测结束。机器人找到金币的回报为1,进入死亡区域回报为-1,机器人在区域1-5之间转换时,回报为0。我们的目标是找到一个策略使得机器人不管处在什么状态(1-5)都能找到金币。对于这个机器人找金币的游戏,我们可以利用强化学习的方法来实现。

构建网格世界的gym环境

该例子的代码,除了本篇博客有以外,OpenAI Gym构建自定义强化学习环境有更仔细和规范的代码贴出

一个gym的环境文件,其主体是个类,在这里我们定义类名为:GridEnv, 其初始化为环境的基本参数,因为机器人找金币的过程是一个马尔科夫过程,我们在强化学习入门课程的第一讲已经介绍过了一个马尔科夫过程应该包括状态空间,动作空间,回报函数,状态转移概率。因此,我们在类GridEnv的初始化时便给出了相应的定义。网格世界的全部代码在gxnk/reinforcement-learning-code,文件名为 grid_mdp.py. 我们看源代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 状态空间为:
self.states = [1,2,3,4,5,6,7,8]
# 动作空间为:
  self.actions = ['n','e','s','w']
# 回报函数为:
  self.rewards = dict(); #回报的数据结构为字典
  self.rewards['1_s'] = -1.0
  self.rewards['3_s'] = 1.0
self.rewards['5_s'] = -1.0
# 状态转移概率为:
  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(a)函数了。这里特别注意的是,step()函数的输入是动作,输出为:下一个时刻的动作,回报,是否终止,调试信息。尤其需要注意的是输出的顺序不要弄错了。对于调试信息,可以为空,但不能缺少,否则会报错,常用{}来代替。我们看源代码:

step函数的建立:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def _step(self, action):
#系统当前状态
state = self.state
#判断系统当前状态是否为终止状态
if state in self.terminate_states:
return state, 0, True, {}
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
if key not in self.rewards:
r = 0.0
else:
r = self.rewards[key]
return next_state, r,is_terminal,{}

step()函数就是这么简单。下面我们重点介绍下如何写render()函数。从图1.1机器人找金币的示意图我们可以看到,网格世界是由一些线和圆组成的。因此,我们可以调用rendering中的画图函数来绘制这些图像。

render函数的建立:

整个图像是一个600*400的窗口,可用如下代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
from gym.envs.classic_control import rendering

self.viewer = rendering.Viewer(screen_width, screen_height)
# 创建网格世界,一共包括11条直线,事先算好每条直线的起点和终点坐标,然后绘制这些直线,代码如下:
#创建网格世界
def render(self):
self.line1 = rendering.Line((100,300),(500,300))
self.line2 = rendering.Line((100, 200), (500, 200))
self.line3 = rendering.Line((100, 300), (100, 100))
self.line4 = rendering.Line((180, 300), (180, 100))
self.line5 = rendering.Line((260, 300), (260, 100))
self.line6 = rendering.Line((340, 300), (340, 100))
self.line7 = rendering.Line((420, 300), (420, 100))
self.line8 = rendering.Line((500, 300), (500, 100))
self.line9 = rendering.Line((100, 100), (180, 100))
self.line10 = rendering.Line((260, 100), (340, 100))
self.line11 = rendering.Line((420, 100), (500, 100))
# 接下来,创建死亡区域,我们用黑色的圆圈代表死亡区域,源代码如下:

# 创建第一个骷髅
self.kulo1 = rendering.make_circle(40)
self.circletrans = rendering.Transform(translation=(140,150))
self.kulo1.add_attr(self.circletrans)
self.kulo1.set_color(0,0,0)
# 创建第二个骷髅
self.kulo2 = rendering.make_circle(40)
self.circletrans = rendering.Transform(translation=(460, 150))
self.kulo2.add_attr(self.circletrans)
self.kulo2.set_color(0, 0, 0)
# 创建金币区域,用金色的圆来表示:
# 创建金条
self.gold = rendering.make_circle(40)
self.circletrans = rendering.Transform(translation=(300, 150))
self.gold.add_attr(self.circletrans)
self.gold.set_color(1, 0.9, 0)

# 创建机器人,我们依然用圆来表示机器人,为了跟死亡区域和金币区域不同,我们可以设置不同的颜色:
# 创建机器人
self.robot= rendering.make_circle(30)
self.robotrans = rendering.Transform()
self.robot.add_attr(self.robotrans)
self.robot.set_color(0.8, 0.6, 0.4)
# 创建完之后,给11条直线设置颜色,并将这些创建的对象添加到几何中代码如下:
self.line1.set_color(0, 0, 0)
self.line2.set_color(0, 0, 0)
self.line3.set_color(0, 0, 0)
self.line4.set_color(0, 0, 0)
self.line5.set_color(0, 0, 0)
self.line6.set_color(0, 0, 0)
self.line7.set_color(0, 0, 0)
self.line8.set_color(0, 0, 0)
self.line9.set_color(0, 0, 0)
self.line10.set_color(0, 0, 0)
self.line11.set_color(0, 0, 0)
# 添加组件到Viewer中
self.viewer.add_geom(self.line1)
self.viewer.add_geom(self.line2)
self.viewer.add_geom(self.line3)
self.viewer.add_geom(self.line4)
self.viewer.add_geom(self.line5)
self.viewer.add_geom(self.line6)
self.viewer.add_geom(self.line7)
self.viewer.add_geom(self.line8)
self.viewer.add_geom(self.line9)
self.viewer.add_geom(self.line10)
self.viewer.add_geom(self.line11)
self.viewer.add_geom(self.kulo1)
self.viewer.add_geom(self.kulo2)
self.viewer.add_geom(self.gold)
self.viewer.add_geom(self.robot)
# 接下来,开始设置机器人的位置。机器人的位置根据其当前所处的状态不同,所在的位置不同。我们事先计算出每个状态处机器人位置的中心坐标,并存储到两个向量中,并在类初始化中给出:
self.x=[140,220,300,380,460,140,300,460]
self.y=[250,250,250,250,250,150,150,150]
# 根据这两个向量和机器人当前的状态,我们就可以设置机器人当前的圆心坐标了即:

if self.state is None: return None

self.robotrans.set_translation(self.x[self.state-1], self.y[self.state- 1])

# 最后还需要一个返回语句:
return self.viewer.render(return_rgb_array=mode == 'rgb_array')

以上便完成了render()函数的建立

reset()函数的建立:

reset()函数常常用随机的方法初始化机器人的状态,即:

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

环境的注册

全部的代码请去github上下载学习。下面重点讲一讲如何将建好的环境进行注册,以便通过gym的标准形式进行调用。其实环境的注册很简单,只需要3步:

第一步:将我们自己的环境文件(我创建的文件名为grid_mdp.py)拷贝到你的gym安装目录/gym/gym/envs/classic_control文件夹中。(拷贝在这个文件夹中因为要使用rendering模块。当然,也有其他办法。该方法不唯一)

第二步:打开该文件夹(第一步中的文件夹)下的__init__.py文件,在文件末尾加入语句:from gym.envs.classic_control.grid_mdp import GridEnv

第三步:进入文件夹你的gym安装目录/gym/gym/envs,打开该文件夹下的__init__.py文件,添加代码:

1
2
3
4
5
6
7
8
register(
# gym.make(‘id’)时的id
id='GridWorld-v0',
# 函数路口
entry_point='gym.envs.classic_control:GridEnv',
max_episode_steps=200,
reward_threshold=100.0,
)

第一个参数id就是你调用gym.make(‘id’)时的id, 这个id你可以随便选取,我取的,名字是GridWorld-v0

第二个参数就是函数路口了。

后面的参数原则上来说可以不必要写。

经过以上三步,就完成了注册。

下面,我们给个简单的demo来测试下我们的环境的效果吧:

我们依然写个终端程序:

1
2
3
4
5
import gym

env = gym.make('GridWorld-v0')
env.reset()
env.render()

Author: Mrli

Link: https://nymrli.top/2019/09/26/OpenAI-Gym使用/

Copyright: All articles in this blog are licensed under CC BY-NC-SA 3.0 unless stating additionally.

< PreviousPost
Python random
NextPost >
Linux安装selenium执行Python程序
CATALOG
  1. 1. OpenAI Gym使用、rendering画图
    1. 1.1. Hello gym
    2. 1.2. 空间(Spaces)
    3. 1.3. Env.render画图
      1. 1.3.1. 画个圆
    4. 1.4. RingViewr
    5. 1.5. 深入剖析gym环境构建[转]
      1. 1.5.1. 2.1 reset()函数详解
      2. 1.5.2. 2.2 render()函数详解
      3. 1.5.3. 2.3 step()函数详解
      4. 1.5.4. 构建网格世界的gym环境
        1. 1.5.4.1. step函数的建立:
        2. 1.5.4.2. render函数的建立:
        3. 1.5.4.3. reset()函数的建立:
        4. 1.5.4.4. 环境的注册