PARL原始碼走讀——使用策略梯度演算法求解迷宮尋寶問題
前不久,百度釋出了基於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)演算法是什麼?
我們知道,強化學習的目標是給定一個馬爾可夫決策過程,尋找出最優策略。所謂策略是指狀態到動作的對映,常用符號表示,它是指給定狀態 s 時,動作集上的一個分佈,即:
策略梯度的做法十分直截了當,它直接對求解最優策略進行引數化建模,策略p(a|s)將從一個概率集合變成一個概率密度函式p(a|s,θ),即:
這個策略函式表示,在給定狀態s和引數θ的情況下,採取任何可能動作的概率,它是一個概率密度函式,在實際運用該策略的時候,是按照這個概率分佈進行動作action的取樣的,這個分佈可以是離散(如伯努利分佈),也可以說是連續(如高斯分佈)。最直觀的方法,我們可以使用一個線性模型表示這個策略函式:
其中,(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,θ)的係數。
現在考慮模型的輸出(a|s,θ),它表示動作的概率分佈,我們知道,智慧體每執行完一輪episode,就會形成一個完整的軌跡Trajectory,,其中,狀態和引數θ無關,狀態轉移概率P(s'|s,a)是由環境所決定的,和引數θ也無關。所以,我們的目標簡化為:優化引數θ,使得每個動作概率的乘積達到最大,即使得這個累乘概率達到最大,可用如下公式表示:
這顯然是我們熟悉的極大似然估計問題,轉化為對數似然函式:
乘以權重 f(s,a),構建如下目標函式,這個目標函式和我們平時見到的損失函式正好相反,它需要使用梯度上升的方法求一個極大值:
注意到,這裡的aTrue就是標籤y-Target,表示agent在狀態時真實採取的動作,可以根據軌跡trajectory取樣得到。
學過機器學習的同學都知道,一般用目標函式的均值代替求和,作為新的目標函式:
均值,就是數學期望,所以目標函式也可以表示為:
有了目標函式,梯度就很容易計算了,由於對於θ來說是係數,故梯度公式如下:
那麼,策略具體的表現形式如何?前文提到,策略可以是離散的,也可以是連續的,不妨考慮離散的策略。由於我們需要求解最大值問題,也就是梯度上升問題,自然而然就想到把梯度上升問題轉化為梯度下降問題,這樣才能使得目標函式的相反數達到最小,而什麼樣的函式可以將梯度下降和對數函式關聯起來呢?顯然是我們熟悉的交叉熵,所以最終的損失函式確定為:
連續策略的推導與離散策略類似,有興趣的讀者可以參考相關文獻。
自此,公式推導可以告一段落。策略梯度的基本演算法就是Reinforce,也稱為蒙特卡洛策略梯度,簡稱MCPG,PARL的官方policy-gradient就是基於以下演算法框架實現的:
PARL原始碼結構
在搭建模型之前,我們先分析一下PARL的主要模組:
- env:環境,在這裡,我們的環境就是迷宮尋寶
- model:模型,可以是簡單的線性模型,也可以是CNN、RNN等深度學習模型
- algorithm:演算法,對model層進行封裝,並利用模型進行predict(預測),同時構建損失函式進行learn(學習);具體實現形式可以是DQN、PG、DDPG等等
- agent:智慧體,對algorithm層進行封裝,一般也包含predict、learn兩個函式;同時,由於智慧體要同時進行探索exploration-利用exploitation,還經常包含一個sample函式,用於決定到底是randomSelect(隨機選擇或者根據分佈函式選擇動作),還是argmax(100%貪心,總是選擇可能性最大的動作)
- train:訓練和測試,用於實現agent和環境的互動,當模型收斂後,可以測試智慧體的準確性
- utils:其他輔助功能
以下的架構示意圖,可以幫助我們更好的理解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)
其中,滑動平均可以選擇任意一個公式,無偏估計表示真實的均值,有偏估計更加接近收斂後的平均獎勵:
無偏估計:
有偏估計:,α是學習率,取0.1、0.01等等
其他程式碼都是輔助功能,如記錄log、畫圖、渲染環境等等。
執行程式並觀察結果
執行TrainMaze,可以看到如下輸出。
1、訓練之前,機器人並不知道如何尋寶,所以越界、撞牆次數非常多,也繞了很多彎路,平均獎勵比較低
ErrorCountBeforeTrain:25052 #越界+撞牆次數
平均獎勵曲線:
2、訓練模型。迭代十萬個episode,觀察如下學習曲線,縱軸表示平均獎勵,可以看到,模型已經收斂了:
3、測試模型的準確性。測試階段,我們迭代128輪,智慧體幾乎沒有任何越界或者撞牆行為,由於是random-start,所以平均獎勵有少許波動,但穩定在5-7之間。
ErrorCountAfterTrain:0 #沒有任何撞牆或者越界
訓練後的平均獎勵:
如下動畫展示了訓練成果,可以看到,無論從哪個位置開始,機器人都能輕鬆繞過障礙物,並以較快的速度找到寶藏,我們的agent終於有了智慧決策能力!
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.
相關文章
- 走迷宮
- 使用A*演算法解迷宮最短路徑問題演算法
- 迷宮問題
- 3090 走迷宮
- [SDOI2012] 走迷宮 題解
- 【ybtoj】【BFS】【例題1】走迷宮
- 強化學習入門之智慧走迷宮-策略迭代演算法強化學習演算法
- 回溯法求迷宮問題
- POJ3984-迷宮問題
- 迷宮問題——最短程式碼,不到70行
- c++迷宮問題回溯法遞迴演算法C++遞迴演算法
- 藍橋杯-走迷宮(BFS)
- 解密迷宮問題:三種高效演算法Java實現,讓你輕鬆穿越未知迷宮解密演算法Java
- POJ3984 迷宮問題【BFS】
- dfs深度優先搜尋解決迷宮類問題(遍歷)
- 自動走迷宮小遊戲~遊戲
- 尋路者華為雲:在產業AI迷宮裡走直線產業AI
- 基於RL(Q-Learning)的迷宮尋路演算法演算法
- hdu 1728 逃離迷宮 搜尋
- 簡單介紹Python迷宮生成和迷宮破解演算法Python演算法
- Canal 原始碼走讀原始碼
- 洛谷 p1605 迷宮問題 詳解
- 【dawn·資料結構】迷宮問題(C++)資料結構C++
- 用Q-learning演算法實現自動走迷宮機器人演算法機器人
- 【ybt高效進階1-5-1】走迷宮
- 基於深度強化學習(DQN)的迷宮尋路演算法強化學習演算法
- 粒子群演算法求解帶約束最佳化問題 原始碼實現演算法原始碼
- 強化學習入門之智慧走迷宮-價值迭代演算法強化學習演算法
- 用python深度優先遍歷解迷宮問題Python
- spring篇(三)龍宮尋寶Spring
- (C++)資料結構實驗二——迷宮問題C++資料結構
- 回溯和遞迴實現迷宮問題(C語言)遞迴C語言
- 1744 迷宮
- 509迷宮
- 拼多多2018暑期實習招聘線上程式設計題:迷宮尋路程式設計
- 3089 探索迷宮
- 遺傳演算法求解TSP問題(python版)演算法Python
- 分治演算法-求解棋盤覆蓋問題演算法