Deeplearning4j 實戰 (18):基於DQN的強化學習在自定義迷宮遊戲問題中的建模

Eclipse Deeplearning4j GitChat課程https://gitbook.cn/gitchat/column/5bfb6741ae0e5f436e35cd9f
Eclipse Deeplearning4j 系列博客https://blog.csdn.net/wangongxi
Eclipse Deeplearning4j Githubhttps://github.com/eclipse/deeplearning4j

在之前寫的博客系列中,我們已經有談到關於強化學習的相關內容。具體來講,當時是基於RL4j並結合OpenAI提供的Gym這個開源的強化學習工具來訓練了一個可以玩Cartpole遊戲的DQN模型。由於Cartpole問題中的建模要素都是由Gym來提供的,因此除非debug源碼,否則是無法瞭解這個問題涉及的action space以及state space。進一步,如果想結合自身的業務來使用強化學習模型,則需要自定義這些要素,因此我們用這篇文章的案例來說明下如何基於RL4j來自定義強化學習問題。爲了方便給出最終的效果,我們依然選擇做一個簡單的遊戲,而不是實際業務比如推薦、廣告和搜索這樣相對抽象的應用。我們通過Java Swing構建遊戲的界面,通過RL4j訓練DQN模型來連續預測遊戲中agent採取的每一步從而完成這個遊戲。下面我們就從四個方面來具體說下。

問題描述

我們構建一個類似於迷宮的問題,事先設定好陷阱(用一個地雷的圖片來表示)以及最終的目標(用一個公主的圖片來表示)。我們的勇士(用櫻木花道的圖片來表示)充當的是強化學習中agent的角色。遊戲的目的是勇士可以通過離線的學習來掌握拯救公主的路線,並且一路上不能掉到陷阱中,一旦落入陷阱或者說踩雷了,那麼遊戲就結束了。如果勇士一路上暢通無阻並且最終達到了公主所在的位置,那麼我們就認爲遊戲成功完成了。在玩遊戲的過程中,我們每次可以給勇士不同的初始位置,以此來觀察勇士的在不同初始state下的表現。爲了便於說明問題,我們將遊戲的環境設置成一個10*10的類似棋盤的界面,每次勇士可以上、下、左、右進行移動,每次只能移動一個格子。這就是我們構建的這個勇士拯救公主的簡單遊戲的背景。
在這裏插入圖片描述
需要說明一下的是,我們做這樣的簡單的遊戲並不是爲了應用於實際的生產環境中,畢竟沒有人會喜歡這樣簡單的遊戲。而且從算法實現上看,在已經明確終點和陷阱的前提下,我們基於回溯的思想可以很方便的找到一條甚至是所有的成功路徑,因此成功完成這個遊戲並不是我們的最終目的,我們希望通過這個案例讓大家明確如何基於RL4j這樣的框架自定義一個強化學習問題,而不是隻能依賴於Gym等第三方環境(第三方工具雖然設計得很好,但是不一定契合自身的業務)。另一方面則是可以通過這個例子,讓大家瞭解到強化學習問題中獎賞塑形、action/state space定義過程這些細節,因爲只有明確了這些,纔可以應用到自身的業務中以及直觀地瞭解到強化學習的特點。

Deep Q-learning(DQN)回顧

我們首先來回顧一下強化學習的一些基本知識。區別於監督學習,強化學習是一種支持連續不斷做出決策的算法。一般的,我們可以基於馬爾科夫決策過程(Markov Decision Process, MDP)的框架做多步強化學習的決策,也可以基於一些Bandit算法,如eplison-greedy、UCB以及Tompson採樣等等做單步最大化的強化學習。這一分類方式詳見周志華西瓜書關於強化學習一章的描述。這裏我們重點說的是多步強化學習。
結合MDP的框架,我們說明下強化學習的一些基本要素,主要有Environment、State、Reward以及Action。對於具備做出智能決策的智慧體,具體來講也就是可以做出Action選擇的模型,我們可以稱之爲agent。agent是處在Environment中的一個具體的State下(實際問題中,State和Environment的關係可以是一致的,即State=Environment,當然也可以是獨立或者不完全相等),根據以往經驗,agent在action space中做出選擇。這個具體的action會獲得一定的reward或者說是future reward。與此同時,action被執行後,對Environment會產生一定的影響,State的狀態也會發生遷移。
對於State的轉移如果是已經明確的一個概率分佈,那麼我們可以稱之爲Model-Based的RL問題,否則就是Model-Free的RL問題。在Model-Free的RL問題中,我們可以採用蒙特卡洛算法以及時間差分算法(Temporal-Difference,簡稱TD)。TD算法中比較常見的有Q-learning、Sarsa等等。Deep Q-Network是神經網絡與Q-learning結合的一種新形式,也屬於TD算法的範疇。
Q-learning的核心其實是如何更新State-Action所構成的一張二維表格,也稱之爲Q表。
在這裏插入圖片描述
在初始化的時候,我們可以全部置爲0。那麼接下來的動作,就是通過不斷的選擇action,並且根據拿到的reward來更新這張Q表。這樣就是強化學習學習試錯的過程。我們根據如下算法來更新Q表。
在這裏插入圖片描述
這張圖從周莫煩的強化學習視頻課程中截取的,有興趣的同學可以去看下他的課程,個人覺得還是很通俗易懂的。
經過不斷的更新Q表中的值,我們可能最終可以得到這樣的一張表。
在這裏插入圖片描述
可以看到這時候Q表中的值已經被更新了,不再是初始化時候的狀態了。舉個具體的例子來說,當我們處於State1的狀態的時候,我們採用eplison-greedy算法(這個算法的詳情請自行查閱相關資料)選擇一個action,也就是在Action1和Action2中選擇一個,大概率會選擇Action2,因爲它的Q-value比較大。
我們進一步分析下這個Q表。從另一個角度講,Q表表示的其實是Action Space和State Space這兩個變量的聯合分佈函數,即Q = Q(s,a)。那麼它的優點在於比較直觀,但是缺點其實也有很多。比如,Action Space和State Space的維度可能會很高,那麼Q表的存儲和更新其實就很麻煩了。如果可以通過一種算法自動地學習Q(s,a)這個函數,至少無限逼近它,那麼我們只要存儲這個算法的一些參數和基本結構就可以了,很自然的,神經網絡是一個非常有用的工具。多層神經網絡加上非線性激活函數可以無限逼近任意函數分佈。因此利用神經網絡來代替Q表,Q-learning與DNN的結合,就形成了現在的深度強化學習。當然DNN的選擇可以是MLP、RNN、CNN等等,這裏統稱爲DNN。
在DeepMind最早的關於DQN的論文中,還有一些必須提到的概念,比如experience replay,也是其比較主要的trick。experience replay,所謂的經驗回放,實際上是打破強化學習訓練樣本的一個連續性。該機制將每一步的State, Action, Reward以及Next-State以四元組的數據結構存儲起來,並在訓練的時候從中隨機抽取一個mini-batch數量的四元祖用於更新神經網絡的參數。以下是DQN論文中的算法的核心描述。
在這裏插入圖片描述
那麼在之後DeepMind的一系列論文中,對DQN做出了一系列的改進,包括增加Target網絡(這篇文章發在Nature Letter上,因此也稱爲Nature DQN),以及在此基礎上的DoubDQNDueling-DQN等等這裏就不再展開講述了。有興趣的朋友可行自行查閱相關的論文。

基於RL4j自定義State/Reward/Action

在之前介紹Cartpole的博客中,我們不需要自定義State/Reward/Action,這些數據都可以依賴Gym客戶端來獲取。由於編寫強化學習的環境以及交互是一個非常繁瑣的過程,因此很多的強化學習案例都會以Gym來作爲獲取環境數據的一個源。但是對於實際業務的一些RL問題,明確上下文環境,建立獎賞機制等是必須結合業務來定義的,因此我們需要結合框架來定義自己的RL問題以及試錯學習的過程。我們在第一部分已經描述了需要建模的遊戲。這裏我們就結合這個遊戲給出基於RL4j框架的RL問題的定義過程。
對於使用RL4j定義強化學習問題,我們至少定義兩個類並分別實現org.deeplearning4j.rl4j.mdp.MDP和org.deeplearning4j.rl4j.space.Encodable這兩個接口。MDP這接口定義的是馬爾科夫決策過程的大框架,具體來說就是裏面涉及的一些操作以接口的形式提供。我們來看下下面的截圖。
在這裏插入圖片描述
每當agent選擇一個action並執行後,我們都可以獲取一個Reply,也就是截圖裏的StepReply,其中包含了下一步的state以及獲取的reward。我們來看下如何定義上文提到的遊戲的MDP環境。
在這裏插入圖片描述
我們定義一個GameMDP的類來實現MDP接口,截圖裏包含了我們定義的一些成員變量。其中比較重要的是actionSpace和observationSpace這兩個對象。我們定義遊戲中的agent只能採取上/下/左/右這四種action,因此這是一個典型的離散的action space。我們直接聲明一個維度爲4的DiscreteSpace的對象即可。DiscreteSpace其實是實現了ActionSpace接口的一個實現類,它將具體的action映射成一個具體的整數。比如,上/下/左/右可以分別對應0/1/2/3。observationSpace,或者說是state space是用來定義RL問題中的狀態空間。我們事先需要聲明一個滿足我們遊戲的自定義State,也就是截圖裏的GameState類。這個類的的功能就是記錄當前agent所處的狀態,具體對於我們的遊戲來說,其實就是agent所處的位置或者說座標。我們看下它的實現。

@Getter
@Setter
public class GameState implements Encodable{
	
	private double x;
	private double y;
	
	public GameState(double x, double y){
		this.x = x;
		this.y = y;
	}
	
	@Override
	public double[] toArray() {
		double[] ret = new double[2];
		ret[0] = x;
		ret[1] = y;
		return ret;
	}

}

這個類的實現很清晰,就是記錄下當前agent的(X,Y)座標。其中toArray的方法用於以數組的形式返回當前的state,其實也就是返回模型輸入的特徵,這個後面會說明。我們再回到上面GameMDP的定義中observationSpace的定義,這個對象並不是用來存儲GameState,它的主要作用是記錄GameState返回的特徵的shape,這裏就是(X,Y)座標構成的長度爲2的向量的shape,也就是1*2。GameMDP中其他的一些成員變量,如:traps是用來存儲陷阱的位置的,curState用來記錄當前的位置,trace主要服務於日誌用於記錄agent移動的軌跡,reward和step分別是到目前爲止獲得回報以及走的步數。下面看下GameMDP一些主要方法的實現。首先是isDone。
在這裏插入圖片描述
isDone的實現直接關係到訓練和預測階段遊戲是否結束。這裏我們考慮兩種狀態會結束遊戲。一個是掉入陷阱,一個是成功救到公主。我們再看reset方法的實現。
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-gdmgDhM4-1576650131879)(…/RL4j/GameMDP-reset-define.jpg)]
reset方法就是重置整個遊戲,具體來說就是當遊戲結束的時候,重新初始化agent的位置。這裏我們通過隨機數生成的方式給agent隨機生成X,Y的座標,因此不管是在訓練還是在實際玩遊戲的時候,每一次遊戲的執行agent的位置都是隨意的。到這裏,我相信是有很多疑問的。比如,爲什麼每次reset都是取隨機的位置,而不是固定一個位置,比如(0,0)這樣一個特殊的起始位置。還有isDone的實現,爲什麼不可以將移動出邊界也算是遊戲結束呢。這裏我們先不多做解釋,在下一個部分會給出我們的看法和解釋。最後我們來看下step方法的實現。
在這裏插入圖片描述
之前說過,step方法需要做的是在agent執行完某一步後,給到的reward和下一步的state。對於算法生成的action(默認採用epsilon-greedy算法),我們執行完之後,可以獲取當前的state,也就是遊戲中agent的座標,並更新curState對象內存儲的座標的具體值。如果執行完這個action後,agent移動出了棋盤,那麼我們人爲做下調整,將其反向移動一個位置。最後,根據最新狀態,判斷是掉入陷阱、救到公主、出界還是一般移動我們給出reward的策略。可以看到,只有救到公主是加分,其他都有不同程度的扣分。最後我們在trace對象中做下記錄,並且更新當前步數然後構建StepReply對象並返回。到此,GameMDP類的實現就基本完成了。

DQN模型訓練

在上面的部分中我們結合遊戲的實際情況定義的RL的相關內容,比如遊戲的狀態,遊戲中agent可以採取的action以及獎賞機制等等。總的來說,這裏的RL問題是基於MDP框架來實現。那麼定義好這些環境相關的信息後,我們給出DQN建模的相關邏輯。

public static QLearning.QLConfiguration QL_CONFIG =
        new QLearning.QLConfiguration(
                123,   	//Random seed
                30,	//Max step Every epoch 批次下最大執行的步數
                100*2000, //Max step            總執行的部署
                100*2000, //Max size of experience replay 記憶數據
                40,    //size of batches
                10,   //target update (hard) 每10次更新一次參數
                0,     //num step noop warmup   步數從0開始
                0.01,  //reward scaling
                0.9,  //gamma
                1.0,  //td-error clipping
                0.1f,  //min epsilon
                100,  //num step for eps greedy anneal
                false   //double DQN
        );

public static DQNFactoryStdDense.Configuration DQN_NET =
        DQNFactoryStdDense.Configuration.builder()
                .updater(new Adam(0.001))
                .numLayer(2)
                .numHiddenNodes(16)
                .build();

以上兩個對象的定義用於確定DQN網絡的結構以及相關的超參數設置。這裏對部分關鍵超參數做一些說明。在QLConfiguration的入參中,有maxEpochStep、maxStep、expRepMaxSize(分別設置成30、100x2000、100x2000)這幾個參數,其中maxEpochStep表示的是在一輪模型訓練中,agent可以執行的step的步數,maxStep表示在整個訓練過程中agent可以執行的總的步數,expRepMaxSize則表示experience reply的總數據量。因此,maxStep / maxEpochStep就是理論上的訓練輪次,實際輪次可能會更多,因爲部分輪次會提前結束。targetDqnUpdateFreq這個參數(我們取的是40)指的是更新目標網絡的頻次。在第二部分回顧DQN的相關內容的時候,我們提到過DQN、Nature DQN、Double DQN以及Dueling DQN這些概念。其中除了DQN,其他都含有target網絡,換言之模型中其實有兩個神經網絡,而DQN只有一個。因此這個參數的作用指的就是更新target網絡的頻次。如果最後一個入參是false,那麼其實這個頻次沒什麼實際作用(雖然底層實現的時候,默認還是會去更新,但是不參與實際的預測了),否則這個參數將決定target網絡的參數情況。第二個對象定義的是神經網絡結構,這裏用的就是一個普通的兩層的每層含有16個神經元的全連接網絡,優化器用的Adam。這個應該沒什麼特別。需要注意的是,如果你用Double DQN,那麼target網絡的結構和它是一模一樣的,只是模型參數不同,底層實現的時候clone一下就可以了。下面看下訓練的邏輯。

public static void learning() throws IOException {

    DataManager manager = new DataManager();

    GameMDP mdp = new GameMDP();

    QLearningDiscreteDense<GameState> dql = new QLearningDiscreteDense<GameState>(mdp, DQN_NET, QL_CONFIG, manager);

    DQNPolicy<GameState> pol = dql.getPolicy();

    dql.train();

    pol.save("game.policy");

    mdp.close();

}

這部分和之前Cartpole問題的建模邏輯基本是一致的,我就不多解釋了,有需要的可以看之前的博客
那麼調用這個方法我們就可以訓練這個DQN模型,並且將模型的相關信息,包括環境參數等信息保存在game.policy這個二進制文件中,具體底層通過序列化來實現。以下是訓練日誌。
在這裏插入圖片描述
下面的章節我們嘗試來玩下這個遊戲。

遊戲效果與說明

在上面一部分建模結束的基礎上,我們編寫了遊戲的一些效果。如果是基於Java Swing做一個遊戲界面,這個在第一部分中就有提及。我們每次會給agent(櫻木花道的圖片)一個隨機的初始位置,然後根據DQN的輸出來決策當前狀態下采取的行動,也就是往上/下/左/右走。執行完這個action後,我們判斷下究竟是掉入了陷阱還是救到了公主還是普通的行走,然後再採取下一步的action,如果掉入陷阱或者救到了公主,那麼我們會提示遊戲結束,並且reset整個遊戲,否則就是agent在遊戲界面上繼續移動。我們給出核心的邏輯。

public static void main(String[] args) throws IOException, InterruptedException {
	//learning();
	GameMDP mdp = initMDP();
	GameBoard board = initGameBoard(mdp);
	//
	boolean success = false, trap = false;
	DQNPolicy<GameState> policy = DQNPolicy.load("game.policy");
	while( true ){
		Point p = playByStep(mdp ,policy);
		board.shiftSoilder(p.getX(), p.getY());
		//
		success = isSuccess(mdp);
		if( success ){
			board.dialog("成功救到公主", "Game Over");
			board = initGameBoard(mdp);
		}
		trap = isTraped(mdp);
		if( trap ){
			board.dialog("掉入陷阱", "Game Over");
			board = initGameBoard(mdp);
		}
		//
		Thread.sleep(1000);
	}
	//
}

這個就是整個遊戲的核心邏輯。我們首先初始化一個MDP環境以及遊戲界面。然後加載預訓練好的DQN模型以及相關環境參數。接着,我們基於DQN給出的決策,也就是在不同state下的action,將agent在界面上進行移動,根據移動後的狀態給出遊戲的狀態。下面給出我錄製的視頻。
視頻文件已經上傳到bilibili

DQN-Maze

視頻裏我已經給出了一些說明,這裏再講解一下。經過訓練之後,agent大體上是可以不斷向着目標,也就是右下角公主的那個座標不斷靠近的。如果不經過訓練,那麼結果很可能是agent在界面隨機上下左右胡亂行動。至於爲什麼依然會有踩到地雷的情況,主要是因爲每次踩到地雷後,整個訓練其實是會結束的,也就是上面提到的isDone方法的那個實現。這個設計的結果是,在訓練過程中,踩到地雷之後訓練就立即結束無法學習到後續的一個情況,因此可以考慮在訓練的時候踩到地雷依然繼續執行,但是預測的時候終止,這樣由於踩到地雷的reward是-1,是比較大的一個懲罰,因此理論上可以讓模型學習到這樣的走法是不好的,從而避開地雷。

調優相關

DQN網絡的調優是一個比較麻煩的事情,因爲其涉及神經網絡本身的調優以及MDP框架下參數的一些調優。

  • 獎賞機制的調優
  • 單輪最大執行次數的調優
  • batchsize的調優
  • agent初始化位置最終效果的影響

大概先列這幾項並做些說明。
第一個是獎賞機制。我們對於掉入陷阱的是-1的激勵,救到公主的是+1的激勵,其餘出界或者正常移動的是-0.2和-0.1的激勵。可以看到,除了就到公主是正激勵,其餘都是大小不等的負激勵。需要特別注意的是,正常移動也是給的負激勵,而不是正激勵,這是因爲我們其實並不鼓勵agent的隨意走動。如果正常移動都給與正激勵,那麼極端情況下agent可以在原地來回走一段時間,這樣不利於模型的收斂,因此正常移動給予負激勵或者0激勵。

第二個單輪最大執行次數。這個參數的主要目的是約束agent在每一輪執行action的次數。如果在試錯的過程中,已經達到甚至超過了這個參數的值,那麼就會結束本輪的訓練。因此極端情況,我們設置一個很小的數值,比如說1,那麼agent永遠到不了目的地,所以這樣的訓練是沒有意義的。而如果設置得過大,那麼容錯的餘地會很大,agent可能會在“閒逛”很長一段時間纔到終點,因此模型的收斂速度會比較慢,但是最終理論上是會收斂的。所以最好設置一個相對合理的值,這裏設置成30,50,100都可以嘗試。

第三個是batchsize的大小。這個參數一開始我設置成8,16這樣比較小的值。但是這樣會有些問題。因爲實際DQN在訓練過程中取的是experience replay機制下存儲的過往的經驗數據,而每次我們的可能會需要移動20甚至30步纔會到終點,因此理論上batchsize中的樣本應該覆蓋這30種情況,即不要小於單輪次的最大步數,取一到兩倍的整數倍即可。這裏取的40 ,就接近於一倍。

第四個是agent初始化位置的問題。實際我們在每一輪訓練或者說每一次play的過程中,初始化位置可以定位在一個固定的位置,比如左上角(0,0)的位置,但我們的選擇是每次隨機初始化一個位置。如果每次固定在左上角,那麼相應的,agent走到終點的步數肯定會增加,這樣單輪最大執行次數如果還是30可能收斂得很慢甚至不收斂,這個參數肯定需要增加。另外,左上角位置的初始化方法相對比較容易掉到陷阱中。

總結

最後我們做下總結。我們基於RL4j自定義了一個簡單遊戲的強化學習過程,具體可以分爲自定義MDP,自定義RL的state以及自定義reward和訓練過程。從最終的效果看,agent確實學習到了如何成功達到目的地的移動方法,但仍然有一定的機率會落到陷阱中。通過這個簡單的案例,希望可以幫助有需要的同學瞭解如何基於RL4j建立和自身業務相關的強化學習算法。這些業務不僅限於遊戲,推薦、廣告、搜索等互聯網場景,工控軟件、工業機器人等製造業場景同樣可以嘗試。
需要注意的是,RL4j 1.0.0-beta5之前的版本中抽象類org.deeplearning4j.rl4j.policy.Policy存在一些問題,所以需要依賴最新的1.0.0-beta6或者1.0.0-SNAPSHOT的版本。我們最後把這個依賴也貼一下。
1.0.0-SNAPSHOT:https://deeplearning4j.org/docs/latest/deeplearning4j-config-snapshots
1.0.0-beta6:

<dependency>
    <groupId>org.deeplearning4j</groupId>
    <artifactId>rl4j-core</artifactId>
    <version>1.0.0-beta6</version>
</dependency>

所有代碼已經託管到Github:https://github.com/AllenWGX/RL4j-Demos.git

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