強化學習小白的第一個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值和最大值的差別會比較小。

通過這個實驗我意識到獎勵的設置與結果的好壞有很大關係。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章