强化学习小白的第一个demo

我挑选的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值和最大值的差别会比较小。

通过这个实验我意识到奖励的设置与结果的好坏有很大关系。

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值