官术网_书友最值得收藏!

6.4 DQN應用于Pong游戲

在討論代碼之前,需要進行一些介紹。我們的例子變得越來越具有挑戰性、越來越復雜了,這不足為奇,因為要解決的問題的復雜性也在增加。這些示例會盡可能簡單明了,但有些代碼很可能一開始就很難理解。

還需要注意性能。前面針對FrozenLake或CartPole的示例中性能沒有那么重要,因為觀察值很少,NN參數很小,在訓練循環中節省額外的時間并不那么重要。但是,從現在開始,情況不再如此了。Atari環境中的一個觀察有10萬個值,這些值必須重新縮放,轉換為浮點數并存儲在回放緩沖區中。復制此數據可能會降低訓練速度,即使是使用最快的圖形處理單元(GPU),需要的也不再是幾秒或幾分鐘,而是數小時。

NN訓練循環也可能成為瓶頸。當然,RL模型不像最新的ImageNet模型那樣龐大,但即使是2015年的DQN模型也具有超過150萬個參數,這對GPU來說是一個很大的壓力。因此,總而言之,性能很重要,尤其是在嘗試使用超參數并且需要等待不止單個模型而是數十個模型的情況下。

PyTorch的表達能力很強,因此,與經過優化的TensorFlow圖相比,高效處理代碼似乎不那么神秘,但是仍可能執行緩慢及出現錯誤。例如,DQN損失計算的簡單版本(該版本遍歷每個批次樣本)的運行速度大約是并行版本的1/3。但是,數據批的復制可能會使同一代碼的速度變為原來的1/14,這是非常顯著的。

根據長度、邏輯結構和可重用性,該示例代碼分為三個模塊,如下所示:

  • Chapter06/lib/wrappers.py:Atari環境包裝程序,主要來自OpenAI Baselines項目。
  • Chapter06/lib/dqn_model.py:DQN NN層,其結構與Nature雜志論文中的DeepMind DQN相同。
  • Chapter06/02_dqn_pong.py:主模塊,包括訓練循環、損失函數計算和經驗回放緩沖區。

6.4.1 包裝器

從資源的角度來看,使用RL處理Atari游戲的要求是很高的。為了使處理速度變快,DeepMind的論文中對Atari平臺交互進行了幾種轉換。有些轉換僅影響性能,但是有些改變了Atari平臺的特性,使學習時間變長且變得不穩定。轉換通常以各種OpenAI Gym包裝器的形式實現,并且在不同的源中都有相同包裝器的多種實現。我個人最喜歡的是OpenAI Baselines倉庫,它是在TensorFlow中實現的一組RL方法和算法,并應用于流行的基準以建立比較方法的共同基礎。該倉庫可從https://github.com/openai/baselines獲得,而包裝器可從文件https://github.com/openai/baselines/blob/master/baselines/common/atari_wrappers.py中獲取。

RL研究人員使用的最受歡迎的Atari轉換包括:

  • 將游戲中的一條命轉變為單獨的片段。一般來說,片段包含從游戲開始到屏幕出現“游戲結束”的所有步驟,這可以持續數千個游戲步驟(觀察和動作)。通常,在街機游戲中,玩家被賦予幾條命,可以提供幾次游戲機會。這種轉換將完整的片段分為玩家的每條命對應的片段。并非所有游戲都支持此功能(例如,Pong不支持),但是對于支持的環境,它通常有助于加快收斂,因為片段變得更短。
  • 在游戲開始時,隨機執行(最多30次)無操作的動作。這會跳過一些Atari游戲中與游戲玩法無關的介紹性屏幕。
  • K步做出一個動作決策,其中K通常為4或3。在中間幀上,只需重復選擇的動作。這可以使訓練速度顯著加快,因為使用NN處理每幀是消耗巨大的操作,但是相鄰幀之間的差異通常很小。
  • 取最后兩幀中每個像素的最大值,并將其用作觀察值。由于平臺的限制,某些Atari游戲具有閃爍效果(Atari只能在一幀中顯示有限數量的精靈圖)。對于人眼來說,這種快速變化是不可見的,但是它們會使NN混亂。
  • 在游戲開始時按FIRE。有些游戲(包括Pong和Breakout)要求用戶按下FIRE按鈕才能啟動游戲。否則,環境將成為POMDP,因為從觀察的角度來看,智能體無法知道是否已按下FIRE。
  • 將每幀從具有三個彩色幀的210×160圖像縮小到84×84單色圖像。可以采用不同的方法。例如,DeepMind的論文將這種轉換描述為從YCbCr顏色空間獲取Y顏色通道,然后將整個圖像重新縮小為84×84分辨率。其他一些研究人員則會進行灰度轉換,裁剪圖像的不相關部分,然后按比例縮小。在Baselines倉庫(以及以下示例代碼)中,將使用后一種方法。
  • 將幾個(通常是4個)后續幀堆在一起提供有關游戲對象的動態網絡信息。前面已經討論了這種方法,作為單個游戲幀中缺乏游戲動態信息的快速解決方案。
  • 將獎勵限制為–1、0和1。所獲得的分數在各游戲之間可能會有很大差異。例如,在Pong中,對手每落后一球,可得1分。但是,在某些游戲中,例如KungFuMaster,每殺死一名敵人,可獲得100的獎勵。獎勵值的分散使損失在不同游戲之間有完全不同的比例,這使得為一組游戲找到通用的超參數變得更加困難。要解決此問題,獎勵需被限制在[–1 … 1]范圍內。
  • 將觀察值從無符號字節轉換為float32值。從模擬器獲得的屏幕被編碼為字節張量,其值為0~255,這不是NN的最佳表示。因此,需要將圖像轉換為浮點數并將值重新縮小至[0.0 … 1.0]范圍。

在Pong示例中,我們不需要包裝器(例如將游戲中的命轉換為單獨的片段和獎勵裁剪的包裝器),因此這些包裝器不包含在示例代碼中。但是,大家還是應該知道它們,以防想嘗試其他游戲。有時,當DQN不收斂時,問題可能不是出自代碼,而是出自錯誤的包裝環境。我花了幾天的時間調試由于游戲一開始沒有按FIRE按鈕而導致的收斂問題!

我們來看一下Chapter06/lib/wrappers.py中各個包裝器的實現:

123-01

在要求游戲啟動的環境中,前面的包裝器會按下FIRE按鈕。除了按FIRE外,此包裝器還會檢查某些游戲中存在的幾種極端情況。

124-01

該包裝器組合了K幀中的重復動作和連續幀中的像素。

124-02

該包裝器的目標是將來自模擬器的輸入觀察結果(通常具有RGB彩色通道,分辨率為210×160像素)轉換為84×84灰度圖像。它使用比色灰度轉換(比簡單的平均顏色通道更接近人類的顏色感知),調整圖像大小以及裁剪頂部和底部來進行轉換。

125-01

這個類沿著第一個維度將隨后幾幀疊加在一起,并將其作為觀察結果返回。目的是使網絡了解對象的動態,例如Pong中球的速度和方向或敵人的移動方式。這是非常重要的信息,無法從單個圖像獲得。

125-02

這個簡單的包裝器將觀察的形狀從HWC(高度,寬度,通道)更改為PyTorch所需的CHW(通道,高度,寬度)格式。張量的輸入形狀中顏色通道是最后一維,但是PyTorch的卷積層將顏色通道假定為第一維。

126-01

庫中的最后一個包裝器將觀察數據從字節轉換為浮點數,并將每個像素的值縮小到[0.0 … 1.0]的范圍。

126-02

文件的末尾是一個簡單函數,該函數根據名稱創建環境并將所有必需的包裝器應用到該環境。以上就是包裝器,下面我們來看一下模型。

6.4.2 DQN模型

Nature雜志上發表的模型有三個卷積層,然后是兩個全連接層。所有層均由線性整流函數(Rectified Linear Unit, ReLU)非線性分開。模型的輸出是環境中每個動作的Q值,沒有應用非線性(因為Q值可以有任何值)。與逐個處理Q(s, a)并將觀察值和動作反饋到網絡以獲得動作價值相比,通過網絡一次計算所有Q值的方法有助于顯著提高速度。

該模型的代碼在Chapter06/lib/dqn_model.py中:

126-03

為了能夠以通用方式編寫網絡,將它分成兩部分實現:convolution和sequential。PyTorch沒有可以將3D張量轉換為1D向量的“flatter”層,需要將卷積層輸出到全連接層。這個問題在forward()函數中得到解決,該函數可以將3D張量批處理為1D向量。

另一個小問題是,我們不知道給定輸入形狀的卷積層的輸出值的準確數量,但是需要將此數字傳遞給第一個全連接層構造函數。一種可能的解決方案是對該數字進行硬編碼,該數字是輸入形狀的函數(對于84×84的輸入,卷積層的輸出將有3136個值)。但是,這并不是最好的方法,因為代碼對輸入形狀的變化將變得不那么健壯。更好的解決方案是用一個簡單的函數_get_conv_out()接受輸入形狀并將卷積層應用于這種形狀的偽張量。該函數的結果將等于此應用程序返回的參數數量。這樣會很快,因為此調用將在模型創建時完成,而且,它使代碼更通用。

127-01

模型的最后一部分是forward()函數,該函數接受4D輸入張量。(第一維是批的大小;第二維是顏色通道,由后續幀疊加而成;第三維和第四維是圖像尺寸。)

轉換的應用分兩步完成:首先將卷積層應用于輸入,然后在輸出上獲得4D張量。這個結果被展平為兩個維度:批大小以及該批卷積返回的所有參數(作為一個數字向量)。這是通過張量的view()函數完成的,該函數讓某一維為-1,并作為其余參數的通配符。例如,假設有一個形狀為(2, 3, 4)的張量T,它是由24個元素組成的3D張量,我們可以使用T.view(6, 4)將其重塑為具有6行4列的2D張量。此操作不會創建新的內存對象,也不會在內存中移動數據,它只是改變了張量的高級形狀。可以通過T.view(-1,4)T.view(6,-1)獲得相同的結果,這在張量第一維是批大小時非常方便。最后,將展平的2D張量傳遞到全連接層,以獲取每個批輸入的Q值。

6.4.3 訓練

第三個模塊包含經驗回放緩沖區、智能體、損失函數的計算和訓練循環本身。在討論代碼之前,需要對訓練超參數進行一些說明。DeepMind在Nature發表的論文包含一張表格,其中包含用于在49個Atari游戲中訓練其模型的超參數的所有詳細信息。DeepMind在所有游戲中均讓這些參數保持相同(但為每個游戲訓練了單獨的模型),意在證明該方法足夠強大,可以通過一個模型架構和超參數來解決不同的游戲問題(具有不同的復雜性、動作空間、獎勵結構和其他細節)。但是,我們的目標要簡單得多:只想解決Pong游戲。

與Atari測試集中的其他游戲相比,Pong非常簡單明了,因此論文中的超參數對于該任務來說過多。例如,為了在所有49款游戲中都獲得最佳結果,DeepMind使用了一個百萬觀察值的回放緩沖區,該緩沖區需要大約20GB的RAM,并且要從環境中獲取大量樣本。

對于單個Pong游戲,論文中使用的ε衰減表也不是最好的。在訓練中,DeepMind在從環境獲得的前一百萬幀中,將ε從1.0線性衰減到0.1。但是,筆者自己的實驗表明,對于Pong而言,在前15萬幀中衰減ε然后使其保持穩定就夠了。回放緩沖區也可以小一些,1萬次轉移就足夠了。

以下示例中使用了筆者自己的參數。這些與論文中的參數不同,但是可以使解決Pong的速度快大約10倍。在GeForce GTX 1080 Ti上,以下版本在1~2小時內的平均得分達到19.0,但是使用DeepMind的超參數,至少需要一天的時間。

當然,這種加速是針對特定環境的微調,并且可能破壞其他游戲的收斂性。大家可以自由使用Atari中的選項和其他游戲。

128-01

首先,導入所需的模塊并定義超參數。

128-02

這兩個值設置了訓練的默認環境,以及最后100個片段的獎勵邊界以停止訓練。如果需要,可以使用命令行重新定義環境名稱。

128-03

這些參數定義以下內容:

  • γ值用于Bellman近似(GAMMA)。
  • 從回放緩沖區采樣的批大小(BATCH_SIZE)。
  • 回放緩沖區的最大容量(REPLAY_SIZE)。
  • 開始訓練前等待填充回放緩沖區的幀數(REPLAY_START_SIZE)。
  • 本示例中使用的Adam優化器的學習率(LEARNING_RATE)。
  • 將模型權重從訓練模型同步到目標模型的頻率,該目標模型用于獲取Bellman近似中下一個狀態的價值(SYNC_TARGET_FRAMES)。
129-01

最后一批超參數與ε衰減有關。為了進行適當的探索,在訓練的早期階段以ε = 1.0開始,這就可以隨機選擇所有動作。然后,在前15萬幀期間,ε線性衰減至0.01,這對應于以1%的概率采取隨機動作。最初的DeepMind論文也使用類似的方案,但是衰減的持續時間幾乎是10倍(即在一百萬幀后,ε = 0.01)。

下一部分代碼定義了經驗回放緩沖區,其目的是存儲從環境中獲得的狀態轉移(由觀察、動作、獎勵、完成標志和下一狀態組成的元組)。在環境中每執行一步,都將狀態轉移情況推送到緩沖區中,僅保留固定數量的狀態轉移(本示例中為1萬個)。為了進行訓練,從回放緩沖區中隨機抽取一批狀態轉移樣本,這打破了環境中后續步驟之間的相關性。

129-02

大多數經驗回放緩沖區代碼非常簡單,基本上利用了deque類的功能以在緩沖區中維持給定數量的條目。在sample()方法中,創建了一個隨機索引列表,然后將采樣的條目重新打包到NumPy數組中,以方便進行損失計算。

我們需要的下一個類是Agent,它與環境交互并將交互結果保存到剛剛的經驗回放緩沖區中:

130-01

在智能體初始化期間,需要存儲對環境的引用和經驗回放緩沖區,追蹤當前的觀察結果以及到目前為止累積的總獎勵。

130-02

智能體的主要方法是在環境中執行一個步驟并將其結果存儲在緩沖區中。為此,首先需要選擇動作。利用概率ε(作為參數傳遞)采取隨機動作;否則,將使用過去的模型獲取所有可能動作的Q值,然后選擇最佳值所對應的動作。

130-03

選擇動作后,將其傳遞給環境以獲取下一個觀察結果和獎勵,將數據存儲在經驗回放緩沖區中,然后處理片段結束的情況。如果通過此步驟到達片段末尾,則該函數的返回結果是總累積獎勵,否則為None

現在是時候使用訓練模塊中的最后一個函數了,該函數可以計算采樣批次的損失。該函數可以通過使用向量運算處理所有批樣本,以最大限度地利用GPU并行性,與簡單循環相比,它更難理解。然而,這種優化是有回報的,并行版本比批處理中的顯式循環快兩倍以上。

提醒一下,以下是需要計算的損失表達式(針對片段未結束的步驟):

131-01

最后一步用公式:

131-02
131-03

在參數中,我們傳入了數組元組的批(由經驗緩沖區中的sample()方法重新打包)、正在訓練的網絡以及定期與訓練網絡同步的目標網絡。

第一個模型(作為網絡參數傳遞)用于計算梯度。tgt_net參數用于計算下一個狀態的價值,并且此計算不應影響梯度。為此,使用PyTorch張量的detach()函數(見第3章)來防止梯度流入目標網絡。

131-04

前面的代碼簡單明了,如果在參數中指定了CUDA設備,我們將帶有批數據的NumPy數組包裝在PyTorch張量中,然后將它們復制到GPU。

131-05

在上一行中,我們將觀察結果傳遞給第一個模型,并使用gather()張量操作提取所采取動作的特定Q值。gather()調用的第一個參數是要對其進行收集的維度索引(本示例中,它等于1,對應于動作)。

第二個參數是要選擇的元素的索引張量。需要額外調用unsqueeze()squeeze()來計算索引參數,并擺脫創建的額外維度(索引應具有與正在處理的數據相同的維數)。在圖6.3中,可以看到對gather()情況的示例說明,其中批包含六個條目和四個動作。

132-02

圖6.3 DQN計算損失過程中張量的變化

請記住,將gather()的結果應用于張量是一個微分運算,該運算將使所有梯度都與損失值有關。

131-06

上一行代碼將目標網絡應用于下一個狀態觀察值,并按相同動作維度1來計算最大Q值。函數max()返回最大值和這些值的索引(它同時計算max和argmax),這非常方便。但是,在本例中,我們只對價值感興趣,因此只選結果的第一項。

132-01

在這里,我們提出一個簡單但非常重要的點:如果狀態轉移發生在片段的最后一步,那么動作價值不會獲得下一個狀態的折扣獎勵,因為沒有可從中獲得獎勵的下一個狀態。這看似微不足道,但在實踐中非常重要,沒有這個訓練就不會收斂。

132-03

這行代碼將值與其計算圖分開,以防止梯度流入用于計算下一狀態Q近似值的NN。

這很重要,因為如果不這樣,損失的反向傳播會同時影響當前狀態和下一個狀態的預測。但是,我們并不想影響下一個狀態的預測,因為它們在Bellman方程中用來計算參考Q值。為了阻止梯度流入圖的該分支中,使用張量的detach()方法,該方法會返回與計算歷史不相關聯的張量。

132-04

最后,計算Bellman近似值和均方誤差損失。這樣損失函數的計算就結束了,其余的代碼就是訓練循環。

132-05

首先,創建一個命令行參數解析器。我們的腳本使我們能夠啟用CUDA并在與默認環境不同的環境中進行訓練。

133-01

上述代碼使用所有必需的包裝器、將要訓練的NN和具有相同結構的目標網絡創建了環境。在一開始,使用不同的隨機權重進行初始化,但這并不重要,因為每隔1000幀(大致相當于Pong的一個片段)同步一次。

133-02

然后,我們創建所需大小的經驗回放緩沖區,并將其傳給智能體。epsilon最初初始化為1.0,但會隨著迭代增加而減小。

133-03

在訓練循環之前,我們要做的最后一件事是創建一個優化器、一個完整片段獎勵的緩沖區、一個幀計數器和幾個變量來跟蹤速度以及達到的最佳平均獎勵。每當平均獎勵超過記錄時,就將模型保存在文件中。

133-04

在訓練循環的開始,計算完成的迭代次數,并根據規劃減小epsilonepsilon在給定幀數(EPSILON_DECAY_LAST_FRAME = 150k)內線性下降,然后保持在EPSILON_FINAL = 0.01的水平。

133-05

在這段代碼中,我們讓智能體在環境中執行一步(使用當前網絡和epsilon值)。僅當此步驟是片段的最后一步時,此函數才返回非None結果。

在這種情況下,我們將報告進度。具體來說,是在控制臺和TensorBoard中計算并顯示以下值:

  • 速度,即每秒處理的幀數。
  • 運行的片段數。
  • 最近100個片段的平均獎勵。
  • epsilon的當前值。
134-01

每當最近100個片段的平均獎勵達到最高時,我們就報告此結果并保存模型參數。如果平均獎勵超過了指定邊界,就停止訓練。對于Pong來說,邊界是19.0,這意味著21場比賽中贏得19場以上。

134-02

這段代碼檢查緩沖區是否大到可以進行訓練。在開始時,我們應該積累足夠的數據,在本例中為1萬次狀態轉移。下一個條件是每隔SYNC_TARGET_FRAMES(默認情況下該值為1000)個數的幀將參數從主網絡同步到目標網絡。

134-03

訓練循環的最后一部分代碼非常簡單,但是需要的執行時間最多:將梯度歸零,從經驗回放緩沖區中采樣數據,計算損失,并執行優化步驟以最小化損失。

6.4.4 運行和性能

這個例子對資源要求很高。在Pong中,它需要大約40萬幀才能達到平均獎勵17(這意味著游戲的80%獲勝)。從17提高到19需要相似數量的幀,因為學習進度將趨于飽和,并且模型很難再提高分數。因此,訓練充分的話平均需要100萬幀。在GTX 1080 Ti上,能達到每秒約120幀的速度,大約需要兩個小時的訓練。在CPU上,速度則要慢得多,大約為每秒9幀,大約需要一天半的時間訓練。請記住,這是針對Pong游戲的,它相對容易解決。其他游戲需要數億幀和100倍大的經驗回放緩沖區。

在第8章中,我們將探討研究人員自2015年以來發現的各種方法,這些方法可以幫助提高訓練速度和數據效率。第9章將致力于提高RL方法性能的工程技巧。但是,對于Atari來說,需要資源和耐心。圖6.4顯示了訓練動態圖。

135-01

圖6.4 最近100片段的平均獎勵動態

在訓練開始時:

135-02

在最初的1萬步中,因為沒有進行任何訓練(代碼中花費時間最多的操作),速度非常快。1萬步之后,開始對訓練批次進行采樣,性能顯著下降。

幾百場比賽之后,DQN應該開始弄清楚如何在21場比賽中贏一兩場。由于eps減小,速度降低了,不僅需要將模型用于訓練,還需要將其用于環境步驟:

136-01

最后,經過更多場比賽后,DQN終于可以統治并擊敗(不是非常復雜的)內置的Pong AI對手:

137-01

由于訓練過程中的隨機性,實際動態可能與此處顯示的有所不同。在一些罕見的情況下(根據筆者自己的實驗,每運行10次會出現一次),訓練根本無法收斂,看起來獎勵在很長一段時間都是–21。如果訓練在前10萬~20萬迭代中沒有顯示出任何正向動態,那么應重新啟動。

6.4.5 模型實戰

訓練過程只是整個過程的一半。我們的最終目標不僅僅是訓練模型,我們也希望模型能夠在玩游戲時表現良好。在訓練期間,每次更新最近100場比賽的最大平均獎勵時,都會將該模型保存到文件PongNoFrameskip-v4-best.dat中。在Chapter06/03_dqn_play.py文件中,有一個程序可以加載此模型文件并運行一個片段,以顯示模型的動態。

該代碼非常簡單,但是像魔術一樣神奇,可以看到幾個具有百萬參數的矩陣是如何通過觀察像素來以超人的準確性玩Pong游戲的。

137-02

在一開始,導入熟悉的PyTorch和Gym模塊。FPS(每秒幀數)參數指定了顯示幀的大致速度。

138-01

該腳本接受已保存模型的文件名,并允許指定Gym環境(當然,模型和環境必須匹配)。此外,還可以通過選項-r傳遞不存在目錄名稱,該目錄將用于保存游戲視頻(使用Monitor包裝器)。默認情況下,腳本僅顯示幀,但是如果要將模型的游戲上傳到YouTube,則用-r可能很方便。

138-02

前面的代碼無須注釋也很清楚,它創建環境和模型,然后從傳遞給參數的文件中加載權重。需要將參數map_location傳遞給torch.load()函數,以將加載的張量從GPU映射到CPU。默認情況下,torch會嘗試將張量加載到保存張量的設備上,但是如果將模型從用于訓練的計算機(帶有GPU)復制到沒有GPU的筆記本電腦,則需要重新映射位置。本示例根本沒有使用GPU,因為沒有加速推理也足夠快。

138-03

這段基本是訓練代碼的Agent類的play_step()方法的復制,沒有選擇ε-greedy動作。只是將觀察結果傳遞給智能體,然后選擇具有最大價值的動作。這里唯一的新事物是環境中的render()方法,這是Gym中顯示當前觀察值的標準方法(為此,需要有圖形用戶界面(Graphical User Interface,GUI))。

139-01

其余代碼也很簡單。我們將動作傳遞給環境,計算總獎勵,并在片段結束時停止循環。片段結束后,將顯示總獎勵以及智能體執行動作的次數。

在YouTube播放列表(https://www.youtube.com/playlist?list=PLMVwuZENsfJklt4vCltrWq0KV9aEZ3ylu)中,你可以找到訓練各個階段的游戲記錄。

主站蜘蛛池模板: 仁布县| 封开县| 九台市| 射洪县| 西丰县| 钦州市| 丹寨县| 灵武市| 澜沧| 西城区| 且末县| 凤凰县| 东乌珠穆沁旗| 菏泽市| 锡林郭勒盟| 阳高县| 遵义市| 浮梁县| 九江县| 深水埗区| 佛冈县| 宁波市| 北流市| 萨嘎县| 清远市| 桃源县| 苏尼特左旗| 临高县| 叙永县| 阿勒泰市| 黑龙江省| 凌海市| 宜川县| 沂水县| 普兰店市| 马龙县| 长治县| 三台县| 东安县| 招远市| 庄河市|