PARL原始碼走讀——使用策略梯度演算法求解迷宮尋寶問題

kosora曹發表於2019-02-24

前不久,百度釋出了基於PaddlePaddle的深度強化學習框架PARL。git傳送門

作為一個強化學習小白,本人懷著學習的心態,安裝並執行了PARL裡的quick-start。不體驗不知道,一體驗嚇一跳,不愧是 NeurIPS 2018 冠軍團隊的傑作,程式碼可讀性良好,函式功能非常清晰,模組之間耦合度低、內聚性強。不僅僅適合零基礎的小白快速搭建DRL環境,也十分適合科研人員復現論文結果。

廢話不多說,我們從強化學習最經典的例子——迷宮尋寶(俗稱格子世界GridWorld)開始,用策略梯度(Policy-Gradient)演算法體驗一把PARL。

 

模擬環境

強化學習適合解決智慧決策問題。如圖,給定如下迷宮,黑色方格代表牆,黃色代表寶藏,紅色代表機器人;一開始,機器人處於任意一個位置,由於走一步要耗電,撞牆後需要修理,所以我們需要訓練一個模型,來告訴機器人如何避免撞牆、並給出尋寶的最優路徑。

迷宮尋寶

接下來,定義強化學習環境所需的各種要素:狀態state、動作action、獎勵reward等等。

state就是機器人所處的位置,用(行、列)這個元組來表示,同時可以表示牆:

self.wallList=[(2,0),(3,2),(1,3),(4,4)]
self.start=(0,4)
self.end=(4,0)

使用random-start策略實現reset功能,以增加初始狀態的隨機性:

    def reset(self):
        for _ in range(0,1024):
            i=np.random.randint(self.row)
            j=np.random.randint(self.col)
            if (i,j) not in self.wallList and (i,j)!=self.end:
                self.pos=(i,j)
                break
        return self.pos 

定義動作action,很顯然,機器人可以走上下左右四個方向:

action_dim=4
dRow=[0,0,-1,1]
dCol=[1,-1,0,0]

定義獎勵reward,到達終點獎勵為10,走其他格子需要耗電,獎勵為-1:

    def reward(self, s):
        if s == self.end:
            return 10.0
        else:
            return -1.0

另外,越界、撞牆需要給較大懲罰:

    if not checkBounds(nextRow, nextCol) :
        #越界
        return self.pos, -5.0, False, {'code':-1,'MSG':'OutOfBounds!'}
    nextPos=(nextRow,nextCol)
    if meetWall(self.wallList, nextPos):
        #撞牆
        return self.pos, -10.0, False, {'code':-1,'MSG':'MeetWall!'}

至此,強化學習所需的狀態、動作、獎勵均定義完畢。接下來簡單推導一下策略梯度演算法的原理。

 

策略梯度(Policy-Gradient)演算法是什麼?

我們知道,強化學習的目標是給定一個馬爾可夫決策過程,尋找出最優策略。所謂策略是指狀態到動作的對映,常用符號\pi表示,它是指給定狀態 s 時,動作集上的一個分佈,即:

                                                                           \large \pi (a|s)=p[A_{t}=a|S_{t}=s]

策略梯度的做法十分直截了當,它直接對求解最優策略進行引數化建模,策略p(a|s)將從一個概率集合變成一個概率密度函式p(a|s,θ),即:

                                                                           \large \pi _{\theta }=p[a|s,\theta ]

這個策略函式表示,在給定狀態s和引數θ的情況下,採取任何可能動作的概率,它是一個概率密度函式,在實際運用該策略的時候,是按照這個概率分佈進行動作action的取樣的,這個分佈可以是離散(如伯努利分佈),也可以說是連續(如高斯分佈)。最直觀的方法,我們可以使用一個線性模型表示這個策略函式:

                                                                           \large \pi _{\theta }=\phi (s)*\theta

其中,\phi(s)表示對狀態s的特徵工程,θ是需要訓練的引數。這樣建模有什麼好處呢?其實最大的好處就是能時時刻刻學到一些隨機策略,增強探索性exploration。

為什麼可以增加探索性呢?

比如迷宮尋寶問題,假設一開始機器人在最左上角的位置,此時p(a|s,θ)可以初始化為[0.25,0.25,0.25,0.25],表明機器人走上、下、左、右、的概率都是0.25。當模型訓練到一定程度的時候,p(a|s,θ)變成了[0.1,0.6,0.1,0.2],此時,向下的概率最大,為0.6,機器人最有可能向下走,這一步表現為利用exploitation;但是,向右走其實也是最優策略,0.2也是可能被選擇的,這一步表現為探索exploration;相對0.6和0.2,向上、向左兩個動作的概率就小很多,但也是有可能被選擇的。如果模型繼續訓練下去,p(a|s,θ)很有可能收斂成[0.05,0.45,0.05,0.45],此時,機器人基本上只走向下或者向右,選擇向上、向左的可能性就極小了。這是最左上角位置(狀態)的情況,其他狀態,隨著模型的訓練,也會收斂到最優解。

有了模型,就想到求梯度,那麼,如何構建損失函式呢?標籤y-Target又是什麼?

一個非常樸素的想法就是:如果一個動作獲得的reward多,那麼就使其出現的概率變大,否則減小,於是,可以構建一個有關狀態-動作的函式 f(s,a) 作為損失函式的權重,這個權重函式可以是長期回報G(t),可以是狀態值函式V(s),也可以是狀態-行為函式Q(s,a),當然也可以是優勢函式A。但是,這個權重函式和引數θ無關,對θ的梯度為0,僅僅作為p(a|s,θ)的係數。

現在考慮模型的輸出\pi(a|s,θ),它表示動作的概率分佈,我們知道,智慧體每執行完一輪episode,就會形成一個完整的軌跡TrajectoryT=[S_{0},a_{0},P(S_{1}|S_{0},a_{0}),S_{1},a_{1},P(S_{2}|S_{1},a_{1}),S_{2}...S_{n-1},a_{n-1},P(S_{n}|S_{n-1},a_{n-1}),S_{n}],其中,狀態S_{0},S_{1}...S_{n}和引數θ無關,狀態轉移概率P(s'|s,a)是由環境所決定的,和引數θ也無關。所以,我們的目標簡化為:優化引數θ,使得每個動作概率的乘積\small p(a_{0})*p(a_{1})*...*p(a_{n})達到最大,即使得\pi (a_{0}|s_{0},\theta)*\pi (a_{1}|s_{1},\theta)*\pi (a_{2}|s_{2},\theta)*...*\pi (a_{n}|s_{n},\theta)這個累乘概率達到最大,可用如下公式表示:

                                                        \large Maximize[arg(\theta )],T=\prod_{t=0}^{N}\pi (a|s_{t},\theta)

這顯然是我們熟悉的極大似然估計問題,轉化為對數似然函式:

                                              \large log(T)=log(\prod_{t=0}^{N}\pi (a|s_{t},\theta))=\sum_{t=0}^{N}log(\pi (a|s_{t},\theta))

乘以權重 f(s,a),構建如下目標函式,這個目標函式和我們平時見到的損失函式正好相反,它需要使用梯度上升的方法求一個極大值:

                                           \large J(\theta )=\sum_{t=0}^{N}log(\pi(a |s_{t},\theta) )*f(s,aTrue)

注意到,這裡的aTrue就是標籤y-Target,表示agent在狀態s_{t}時真實採取的動作,可以根據軌跡trajectory取樣得到。

學過機器學習的同學都知道,一般用目標函式的均值代替求和,作為新的目標函式:

                                          \large J(\theta )=\frac{1}{N}\sum_{t=0}^{N}log(\pi (a|s_{t},\theta ))*f(s_{t},aTrue)

均值,就是數學期望,所以目標函式也可以表示為:

                                          \large J(\theta )=E_{\pi (\theta )}(log(\pi (a|s_{t},\theta ))*f(s_{t},aTrue))

有了目標函式,梯度就很容易計算了,由於\large f(s_{t},a)對於θ來說是係數,故梯度公式如下:

                                       \large \triangledown J(\theta )=E_{\pi(\theta)}(\triangledown log(\pi(a|s_{t},\theta))*f(s_{t},aTrue))

那麼,策略\large \pi具體的表現形式如何?前文提到,策略可以是離散的,也可以是連續的,不妨考慮離散的策略。由於我們需要求解最大值問題,也就是梯度上升問題,自然而然就想到把梯度上升問題轉化為梯度下降問題,這樣才能使得目標函式的相反數達到最小,而什麼樣的函式可以將梯度下降和對數函式關聯起來呢?顯然是我們熟悉的交叉熵,所以最終的損失函式確定為:

                          Minimize[arg(\theta)],J(\theta)=E_{\pi(\theta)}(CrossEntropy(\pi(a|s_{t},\theta),aTrue)*f(s_{t},aTrue))

連續策略的推導與離散策略類似,有興趣的讀者可以參考相關文獻。

自此,公式推導可以告一段落。策略梯度的基本演算法就是Reinforce,也稱為蒙特卡洛策略梯度,簡稱MCPG,PARL的官方policy-gradient就是基於以下演算法框架實現的:

 

PARL原始碼結構

在搭建模型之前,我們先分析一下PARL的主要模組:

  1. env:環境,在這裡,我們的環境就是迷宮尋寶
  2. model:模型,可以是簡單的線性模型,也可以是CNN、RNN等深度學習模型
  3. algorithm:演算法,對model層進行封裝,並利用模型進行predict(預測),同時構建損失函式進行learn(學習);具體實現形式可以是DQN、PG、DDPG等等
  4. agent:智慧體,對algorithm層進行封裝,一般也包含predict、learn兩個函式;同時,由於智慧體要同時進行探索exploration-利用exploitation,還經常包含一個sample函式,用於決定到底是randomSelect(隨機選擇或者根據分佈函式選擇動作),還是argmax(100%貪心,總是選擇可能性最大的動作)
  5. train:訓練和測試,用於實現agent和環境的互動,當模型收斂後,可以測試智慧體的準確性
  6. utils:其他輔助功能

以下的架構示意圖,可以幫助我們更好的理解PARL:

PARL整體架構

 

程式碼實現&原始碼解讀

在理解了框架的各個模組之後,我們就可以按照模板填程式碼了,學過MVC、ORM等框架的同學都知道,這是一件非常輕鬆愉快的事情。

1、MazeEnv。迷宮環境,繼承自gym.Env,實現了reset、step、reward、render四個主要方法,這裡不再贅述

2、MazeModel。模型層,搭建如下全連結神經網路,輸入是狀態state-input,輸出是策略函式action-out,由於策略函式是動作的概率分佈,所以選用softmax作為啟用函式,中間還有若干隱藏層。

神經網路

程式碼實現非常的簡單,讓MazeModel繼承官方的Model類,然後照貓畫虎搭建模型即可:

class MazeModel(Model):
    def __init__(self, act_dim):
        self.act_dim = act_dim
        hid1_size = 32
        hid2_size = 32
        self.fc1 = layers.fc(size=hid1_size, act='tanh')
        self.fc2 = layers.fc(size=hid2_size, act='tanh')
        self.fcOut = layers.fc(size=act_dim,act='softmax')

    def policy(self, obs):
        out = self.fc1(obs)
        out = self.fc2(out)
        out = self.fcOut(out)
        return out

3、policy_gradient。演算法層;官方倉庫提供了大量的經典強化學習演算法,我們無需自己重複寫,可以直接複用演算法庫(parl.algorithms)裡邊的 PolicyGradient 演算法!

簡單分析一下policy_gradient的原始碼實現。

define_predict函式,接收狀態obs,呼叫model的policy方法,輸出狀態所對應的動作:

    def define_predict(self, obs):
        """ use policy model self.model to predict the action probability
        """
        return self.model.policy(obs)

define_learn函式,接收狀態obs、真實動作action、長期回報reward,首先呼叫model的pocliy方法,預測狀態obs所對應的動作概率分佈act_prob,然後使用交叉熵和reward的乘積構造損失函式cost,最後執行梯度下降法,優化器為Adam,完成學習功能:

    def define_learn(self, obs, action, reward):
        """ update policy model self.model with policy gradient algorithm
        """
        act_prob = self.model.policy(obs)
        log_prob = layers.cross_entropy(act_prob, action)
        cost = log_prob * reward
        cost = layers.reduce_mean(cost)
        optimizer = fluid.optimizer.Adam(self.lr)
        optimizer.minimize(cost)
        return cost

4、MazeAgent。智慧體。其中,self.pred_program是對algorithm中define_predict的簡單封裝,self.train_program是對algorithm中define_learn的簡單封裝,我們可以參考官方的CartpoleAgent實現,按照框架模板填入相應的格式程式碼。

這裡,僅僅分析self.pred_program,self.train_program寫法類似:

self.pred_program = fluid.Program()#固定寫法

with fluid.program_guard(self.pred_program):
    obs = layers.data(
                name='obs', shape=[self.obs_dim], dtype='float32')#接收外界傳入的狀態obs
    self.act_prob = self.alg.define_predict(obs)
    #呼叫algorithm的define_predict,self.act_prob為動作的概率分佈

sample函式,注意這句話:

act = np.random.choice(range(self.act_dim), p=act_prob)

這句話表示根據概率分佈隨機選出相應的動作;假設上、下、左、右的概率分別為[0.5,0.3,0.15,0.05],那麼上被選擇的概率是最大的,右被選擇的概率是最小的,所以sample函式既能exploration,又能exploitation,體現了強化學習中的探索-利用的平衡。

predict函式,和sample函式不同的是,它總是貪心的選擇可能性最大的動作,常常用於測試階段:

act = np.argmax(act_prob)

learn函式,接收obs、action、reward,進行批量梯度下降,返回損失函式cost。

5、TrainMaze。讓環境env和智慧體agent進行互動,最主要的部分就是以下程式碼,體現了MCPG過程:


    #迭代十萬個episode

    for i in range(1,100001):

        #取樣

        obs_list, action_list, reward_list = run_train_episode(env, agent)

        #使用滑動平均的方式計算獎勵的期望

        MeanReward=MeanReward+(sum(reward_list)-MeanReward)/i

        batch_obs = np.array(obs_list)

        batch_action = np.array(action_list)

        #通過backup的方式計算G(t),並進行歸一化處理

        batch_reward = calc_discount_norm_reward(reward_list, GAMMA)

        #學習

        agent.learn(batch_obs, batch_action, batch_reward)

其中,滑動平均可以選擇任意一個公式,無偏估計表示真實的均值,有偏估計更加接近收斂後的平均獎勵:

無偏估計:\small E_{X}=E_{X}+(x-E_{X})/N

有偏估計:\small E_{X}=(1-\alpha )E_{X}+(x-E_{X})*\alpha,α是學習率,取0.1、0.01等等

其他程式碼都是輔助功能,如記錄log、畫圖、渲染環境等等。

 

執行程式並觀察結果

執行TrainMaze,可以看到如下輸出。

1、訓練之前,機器人並不知道如何尋寶,所以越界、撞牆次數非常多,也繞了很多彎路,平均獎勵比較低

ErrorCountBeforeTrain:25052   #越界+撞牆次數

平均獎勵曲線:

訓練前的獎勵

2、訓練模型。迭代十萬個episode,觀察如下學習曲線,縱軸表示平均獎勵,可以看到,模型已經收斂了:

3、測試模型的準確性。測試階段,我們迭代128輪,智慧體幾乎沒有任何越界或者撞牆行為,由於是random-start,所以平均獎勵有少許波動,但穩定在5-7之間。

ErrorCountAfterTrain:0    #沒有任何撞牆或者越界

訓練後的平均獎勵:

如下動畫展示了訓練成果,可以看到,無論從哪個位置開始,機器人都能輕鬆繞過障礙物,並以較快的速度找到寶藏,我們的agent終於有了智慧決策能力!

原始碼Git地址 

https://github.com/kosoraYintai/PARL-Sample

參考文獻:

  • CS 294-112 at UC Berkeley,Deep Reinforcement Learning.
  • Deepmind,Silver.D,Reinforcement Learning Open Class.
  • 馮超. 強化學習精要[M]. 北京:電子工業出版社,2018.
  • 郭憲,方勇純. 深入淺出強化學習[M]. 北京:電子工業出版社,2018.

 

相關文章