我挑选的demo是书《深入浅出强化学习原理入门》里的一道题,但是没有答案,所以我想自己尝试做一下。
(P.S.我真的对这本书很无感,后来发现豆瓣上基本全是对这本书的吐槽。反正,我一开始看得云里雾里的,全书的逻辑性不强,总之不建议读。想入门的萌新可以看李宏毅老师的RL课程,老师人很可爱,讲得非常通俗易懂,不会让你特别快地放弃,非常适合入门。唯一的不足就是老师讲课中英文夹杂再加上台湾腔,有时要听几遍才懂某些字眼是什么意思,而且偶尔麦克风会发疯,不过瑕不掩瑜,这还是一门好课。)
这道题是一个如下图所示的迷宫,黄色的格子表示出口,另外每一个格子都用数字表示,它是agent的一个状态,每个状态都有上下左右四个动作,只是某些格子的某些动作是无用的,因为它在边界上,所以这时会保持现有状态。
第一步,创建一个agent
-
创建class并完成初始化
需要事先安装好gym这个模块
我建立了一个class,是从gym.Env继承过来的,方便我们在后续调用viewer的一些方法。在初始化函数里放入状态和动作的信息
class grid_env(gym.Env):
def __init__(self):
self.viewer=None
self.states=[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18]
self.terminate_state=18
self.gamma = 0.9
self.state=self.states[int(np.random.random()*len(self.states))]
self.actions=['l','r','u','d']
t是一个字典,记录了所有状态-动作对应的下一个状态,我这里只展示了一小部分
#状态动作转移矩阵,没有赋值的状态动作,说明状态没有发生变化。
#在这个游戏中,在某一状态执行某一动作得到的下一个状态是固定的,不具有随机性
self.t=dict();
self.t['1_r']=2
self.t['1_d']=5
self.t['2_l']=1
self.t['2_r']=3
r同样也是一个字典,记录了部分状态-动作对应的立即奖励
#设置状态-动作的立即奖励,我挑选了离出口较近的几个点,当这几个点向出口走近时,立即奖励为1,到达出口的奖励为10
#远离出口的奖励为-1
self.r=dict();
self.r['9_r']=1.0
self.r['10_r']=10.0
self.r['4_d']=1.0
self.r['8_d']=10.0
self.r['15_u']=10.0
self.r['9_u']=-1.0
self.r['9_d']=-1.0
self.r['10_l']=-1.0
self.r['10_d']=-1.0
self.r['8_u']=-1.0
self.r['15_l']=-1.0
cord这个字典则记录了agent分别在18个状态下的坐标,坐标是相应格子的中心位置,这里只展示了三个状态。
self.cord=dict();
self.cord[1]=[140,460]
self.cord[2]=[220,460]
self.cord[3]=[300,460]
#用pandas的DataFrame来做q_table的数据结构非常合适
self.q_table = pd.DataFrame(columns=self.actions, dtype=np.float64)
for s in self.states:
self.q_table = self.q_table.append(
pd.Series(
[0]*len(self.actions),
index=self.q_table.columns,
name=s,
)
)
还需要编写一下reset函数,让游戏每次开始时随机出现在某个位置,除了np.random.random,还可以用np.random.choice来随机选择状态
def reset(self):
self.state=self.states[int(np.random.random()*len(self.states))]
return self.state
-
初始化之后,开始编写重要的图像引擎函数——render()
窗口的大小是600*600,我设置的迷宫是400*400的(并显示在窗口正中间),每个格子是80*80的。我分别在水平方向和垂直方向画了六条线,形成了一个5*5的格图。然后用rendering.FilledPolygon()渲染出长方体形状的障碍。最后绘制出人(这里我用一个黄色的小圆表示,出口则是一个黑色的大圆),注意人的位置是由self.state来决定的,它是实时移动的。
注意,用rendering函数绘制完图形后,一定要用viewer.add_geom(),将其加入到viewer中,最后才能显示出来。
一开始我没有设置close这个参数,在我不断调用render()时,人以前的位置上也会出现黄色小圆,也就是它不会清除历史信息。所以我就每次调用render()之前,先render(True),将之前的viewer关闭。但是这样会有一个小问题:窗口不断闪现,就是看着有点难受。我对图像渲染这方面一无所知,google也没找到解决方法,所以这个先搁置一下。
def render(self,close=False):
#这个close非常重要,没有它的话,人在以前时刻的轨迹也会被保留
if close:
if self.viewer is not None:
self.viewer.close()
self.viewer = None
return
if self.viewer is None:
self.viewer = rendering.Viewer(600, 600)
for i in range(6):
line=rendering.Line((100+80*i,100),(100+80*i,500))
line.set_color(0,0,0)
self.viewer.add_geom(line)
for i in range(6):
line=rendering.Line((100,100+80*i),(500,100+80*i))
line.set_color(0,0,0)
self.viewer.add_geom(line)
l=340
r=420
b=340
t=500
cart= rendering.FilledPolygon([(l,b), (l,t), (r,t), (r,b)])
self.viewer.add_geom(cart)
l=100
r=260
b=260
t=340
cart= rendering.FilledPolygon([(l,b), (l,t), (r,t), (r,b)])
self.viewer.add_geom(cart)
l=260
r=500
b=100
t=180
cart= rendering.FilledPolygon([(l,b), (l,t), (r,t), (r,b)])
self.viewer.add_geom(cart)
#绘制出口
circle=rendering.make_circle(40)
trans=rendering.Transform(translation=(460,300))
circle.add_attr(trans)
self.viewer.add_geom(circle)
#绘制人所在的位置
[x,y]=self.cord[self.state]
circle=rendering.make_circle(30)
trans=rendering.Transform(translation=(x,y))
circle.add_attr(trans)
circle.set_color(1,0.9,0)
self.viewer.add_geom(circle)
return self.viewer.render()
-
编写step函数
它相当于物理引擎,也就是模拟了环境,将选择的动作与之互动,就返回给agent下一状态,立即奖励和是否终止的信号。在init时,我们只设置了部分状态-动作的奖励,其他的我没规定。最初我把这些没规定的状态-动作对的奖励都设置为0.但是在测试时,我发现这就可能发生问题,人一直碰壁,而不产生任何有效动作。所以我把这些使人徘徊在原地的状态-动作对的奖励设置为-0.5.从另一个角度想,这个负奖励的加入在一定程度上可以促使人摒弃无效动作,在更短步数内走出迷宫。
def _step(self,action) :
state=self.state
if state==self.terminate_state:
return state,0,True
key="%d_%s"%(state,action)
is_terminal=False
#根据状态-动作转移矩阵来更新状态
if key in self.t:
next_state=self.t[key]
else:
next_state=state
self.state=next_state
if next_state==self.terminate_state:
is_terminal=True
#判断这一状态动作
if key in self.r:
r=self.r[key]
elif key not in self.t:
r=-0.5
else:
r=0.0
return next_state,r,is_terminal
以上这些代码就基本上把迷宫这个ENV的class构建好了。主要是step和render两个方法,前者是环境的物理引擎,后者是用来渲染的图像引擎(通常借助于gym的viewer来编写)
第二步 构建agent
在这个例子中,我用的方法是基于蒙特卡洛的q learning方法。所以需要一个函数来随机采样许多回合的数据。
#通过蒙特卡罗收集大量数据
def random_sample(self,num):
state_sample=[]
action_sample=[]
reward_sample=[]
for i in range(num):
tic=time.clock()
s_tmp=[]
r_tmp=[]
a_tmp=[]
s=self.reset()
is_stop=False
while is_stop==False:
s_tmp.append(s)
a=self.actions[int(np.random.random()*len(self.actions))]
s,r,is_stop=self._step(a)
r_tmp.append(r)
a_tmp.append(a)
state_sample.append(s_tmp)
action_sample.append(a_tmp)
reward_sample.append(r_tmp)
toc=time.clock()
print("epoch%d---sampling time is %fs"%(i,(toc-tic)))
return state_sample,action_sample,reward_sample
然后我们对这些数据进行统计分析。首先要把立即奖励转化成累积奖励。也就是在某一状态采取某一动作之后,一直到回合结束(走出迷宫),得到的奖励总和。所以要将每一条回合的数据倒过来处理,注意要从倒数第二条开始处理,最后一条是结束时的状态。
def mc(self,state_sample,action_sample,reward_sample):
nums=dict()
for a in self.actions:
nums[a]=0.0
nums_table=[]
for i in range(len(self.states)):
nums_table.append(nums)
for i in range(len(state_sample)):
G=0.0
tic=time.clock()
for j in range(len(state_sample[i])-1,-1,-1):
G*=self.gamma
G+=reward_sample[i][j]
a=action_sample[i][j]
s=state_sample[i][j]
self.q_table.loc[s,a]+=G
nums_table[s-1][a]+=1
toc=time.clock()
if i%100==0:
print("epoch%d---training time is %fs"%(i,(toc-tic)))
for s in self.states:
for a in self.actions:
self.q_table.loc[s,a]/=nums_table[s-1][a]
return self.q_table
当我们随机采样1000个回合的数据进行mc统计后,得到的q_table如下:
| l | r | u | d |
1 | -0.21225 | -0.12657 | -0.19627 | -0.15027 |
2 | -0.14656 | -0.12296 | -0.17208 | -0.11699 |
3 | -0.11868 | -0.16466 | -0.16666 | -0.08986 |
4 | 0.025052 | 0.021552 | 0.022692 | 0.056406 |
5 | -0.18392 | -0.11075 | -0.15179 | -0.18497 |
6 | -0.13336 | -0.076 | -0.12637 | -0.15179 |
7 | -0.09813 | -0.11197 | -0.11517 | -0.00195 |
8 | 0.026403 | 0.028949 | 0.01265 | 0.070143 |
9 | -0.01863 | 0.19151 | -0.10776 | -0.04913 |
10 | -0.03226 | 0.281304 | 0.061431 | 0.031505 |
11 | -0.11952 | -0.06204 | -0.1215 | -0.10037 |
12 | -0.08944 | 0.015317 | -0.08887 | -0.09643 |
13 | -0.04697 | 0.080801 | 0.013004 | -0.0175 |
14 | 0.001236 | 0.149338 | 0.085545 | 0.049916 |
15 | 0.020852 | 0.075249 | 0.198882 | 0.055839 |
16 | -0.14478 | -0.10038 | -0.09159 | -0.1403 |
17 | -0.1075 | -0.13904 | -0.06664 | -0.13785 |
18 | 0 | 0 | 0 | 0 |
根据这个表格,每个状态-动作对都会有一个最优的动作
状态 | 动作 | 状态 | 动作 |
1 | 右(下) | 10 | 右 |
2 | 下(右) | 11 | 右 |
3 | 下 | 12 | 右 |
4 | 下 | 13 | 右 |
5 | 右 | 14 | 右(上) |
6 | 右 | 15 | 上 |
7 | 下 | 16 | 上(右) |
8 | 下 | 17 | 上 |
9 | 右 | 18 | 结束 |
在1,2,14,16的状态下,最优动作不是唯一,但在我训练的q table中,它有一个自己的偏好,括号里是另一个最优动作。对比q table,可以看到,如果某一状态只存在唯一一个最优动作时,相应的q值要比其他动作的大的多一点;反之,则另外最优动作的q值和最大值的差别会比较小。
通过这个实验我意识到奖励的设置与结果的好坏有很大关系。