近期深度強化學習領域日新月異,其中最酷的一件事情莫過於 OpenAI 和 DeepMind 訓練智慧體接收人類的反饋而不是傳統的獎勵訊號。本文作者認為復現論文是提升機器學習技能的最好方式之一,所以選擇了 OpenAI 論文《Deep Reinforcement Learning from Human Preferences》作為 target,雖獲得最後成功,卻未實現初衷。如果你也打算復現強化學習論文,那麼本文經驗也許是你想要的。此外,本文雖對強化學習模型的訓練提供了寶貴經驗,同時也對映出另外一幅殘酷畫面:強化學習依然難免 hype 之嫌;強化學習的成功不在於其真正有效,而是人們故意為之。
瑞士盧加諾大學資訊學碩士 Tim Dettmers 對本文的點評
首先,整體而言,強化學習問題要遠比預期的更為棘手。
主要原因是強化學習本身非常敏感,需要糾正大量的細節,如果不這麼做,後面診斷問題所在會非常難。
例項 1:基本實現完成之後,訓練效果並未跟上。我對問題所在充滿各種想法,但經過數月的苦思冥想,發現問題出現在關鍵階段中的獎勵歸一化和畫素資料。儘管想通了這點,卻仍未搞明白整個問題:畫素資料進入的獎勵探測器網路的準確度剛剛好,我花了很長時間終於明白仔細檢查已預測的獎勵足以發現獎勵歸一化漏洞。一句話,搞明白髮生了什麼問題幾乎是偶然性的,找出最終可以導向正確路徑的微小的不一致性。
例項 2:最後的程式碼清理完成之後,我多少有些錯誤地實現了 dropout。獎勵探測器網路需要一對視訊片段作為輸入,由權重共享的兩個網路同等處理。如果你新增 dropout,並在每個網路中不小心給了它相同的隨機種子,每個網路將獲得不同的 dropout,因此視訊片段將不會被同等處理。正如結果表明完全修正它會破壞訓練,儘管網路的預測準確度看起來一模一樣。
找出被破壞的那一個。沒錯,我也沒找到。
在我印象中這種情況非常普遍(比如《Deep Reinforcement Learning Doesn't Work Yet》)。我的解讀是你要像對待數學問題一樣對待強化學習專案。它不同於程式設計問題,你可以在數天內完成它;它更像是你在解決一個謎題,沒有規律可循,唯一的方法是不斷嘗試,直到靈感出現徹底搞明白。
這需要你不斷嘗試,並對實現過程中的困惑保有最敏銳的嗅覺。
該專案中有很多這樣的點,其中唯一的線索就是那些看起來無關緊要的小事情。比如,某些時候採用不同幀之間的差異作為特徵會更加奏效。通過一些新特徵繼續向前會非常誘人,但我很困惑當時在我工作的簡單環境中會造成如此大的差異。這隻有通過思考這些困惑並意識到採用不同幀之間的差異,才能給正則化問題提供線索。
我不完全確定如何使人在這方面做更多,但我目前最好的猜測是:
學習識別困惑的感覺。「事情不太對的感覺」有很多種,有時是程式碼很醜,有時是擔心浪費時間在錯誤的事情上。但有時是「你看到了一些意料之外的事情」。能夠精確知道令自己不舒服的事情很重要,因此你可以……
培養思考困惑來源的習慣。一些不舒服的原因最好選擇忽略(比如原型設計時的程式碼風格),但困惑並不是。一旦遇到困惑。立即調查其來源對你來說很重要。
無論如何:做好每次卡住數週的準備。(並相信堅持下來就會攻克難關,並留意那些小的細節。)
說到和過去的程式設計經驗的區別,第二個主要學習經驗是觀念模式的區別,即需要長時間的工作迭代。
除錯過程大致涉及 4 個基本步驟:
收集關於問題性質的證據;
基於已有證據對問題作出假設;
選擇最可能成立的假設,實現一個解決辦法,看看結果如何;
重複以上過程直到問題解決。
在我做過的大部分程式設計工作都習慣於快速反饋。如果有程式不工作了,你可以在數秒或數分鐘內做出改變並檢視有沒有奏效。收集證據是很簡單的工作。實際上,在快速反饋的情況下,收集證據可能比作出假設要簡單得多。當你能憑直覺想到解決方案(並收集更多證據)時,為什麼還要花費那麼多時間考慮所有的可能性呢?換句話說,在快速反饋的情況下,你可以通過嘗試而不是仔細考慮並迅速地縮小假設空間。
但當單次執行時間達到 10 小時的時候,嘗試和反饋的策略很容易使你浪費很多的時間。
並行執行多個解決方案會有幫助,如果(a)你有計算機叢集的雲端計算資源;(b)由於上述的強化學習中的各種困難,如果你迭代得太快,可能永遠無法意識到你真正需要的證據。
從「多實驗、少思考」到「少實驗、多思考」的轉變是提高效率的關鍵。當除錯過程需要耗費很長的迭代時間時,你需要傾注大量的時間到建立假設上,即使需要花費很長的時間,比如 30 分鐘甚至 1 小時。在單次實驗中儘可能詳實地檢驗你的假設,找到能最好地區分不同可能性的證據。
轉向「少實驗、多思考」的關鍵是保持細節豐富的工作日誌。當每次實驗的執行時間較少的時候,可以不用日誌,但在實驗時間超過一天的時候,很多東西都容易被忘記。我認為在日誌中應該記錄的有:
日誌 1:你現在所需要的具體輸出;
日誌 2:把你的假設大膽地寫出來;
日誌 3:簡單記錄當前的程式,關於當前執行實驗想要回答的問題;
日誌 4:實驗執行的結果(TensorBoard 圖,任何其它重要觀測),按執行實驗的型別分類(例如按智慧體訓練的環境)。
我起初記錄相對較稀疏的日誌,但到了專案的結束階段,我的態度轉變成了「記錄我頭腦中出現過的所有東西」。這很費時,但也很值得。部分是因為某些除錯過程需要交叉參照結果,這些結果可能是數天前或數週前做出的。部分是因為(至少我認為)思考質量的通常提升方式是從大量的更新到有效的心理 RAM。
典型日誌
為了從所做的實驗中得到最大的效果,我在實驗整個過程中做了兩件事:
首先,持記錄所有可以記錄的指標的態度,以最大化每次執行時收集的證據量。有一些明顯的指標如訓練/驗證準確率,但是在專案開始時花費一點時間頭腦風暴,研究哪些指標對於診斷潛在問題比較重要是很有益的。
我這麼推薦的部分原因是由於事後偏見:我發現哪些指標應該更早記錄。很難提前預測哪些指標有用。可能有用的啟發式方法如下:
對於系統中的每個重要元件,考慮什麼可以被度量。如果是資料庫,那麼度量它的增長速度。如果是佇列,則度量各個項的處理速度。
對於每個複雜步驟,度量其不同部分所花費時間。如果是訓練迴圈,則度量執行每個批次要花費多長時間。如果是複雜的推斷步驟,則度量每個子推斷任務所花費的時間。這些時間對之後的效能 debug 大有裨益,有時候甚至可以檢查出難以發現的 bug。(例如,如果你看到某個部分花費時間很長,那麼它可能出現記憶體洩露。)
類似地,考慮蒐集不同元件的記憶體使用情況。小的記憶體洩露可能揭示所有問題。
另一個策略是檢視別人使用什麼度量指標。在深度強化學習中,John Schulman 在其演講《Nuts and Bolts of Deep RL Experimentation》中給出了一些好主意(視訊地址:https://www.youtube.com/watch?v=8EcdaCk9KaQ;slides 地址:http://joschu.net/docs/nuts-and-bolts.pdf;摘要:https://github.com/williamFalcon/DeepRLHacks)。對於策略梯度方法,我發現策略熵是判斷訓練是否開始的優秀指標,比 per-episode 獎勵更加敏銳。
不健康和健康的策略熵圖示例。失敗模式 1(左):收斂至常數熵(隨機選擇動作子集)。失敗模式 2(中):收斂至零熵(每次選擇相同的動作)。右:成功的 Pong 訓練執行中的策略熵。
如果你在記錄的指標中看到了一些可疑的現象,記得注意混淆,寧可假設它很重要也不要輕視,比如一些資料結構的低效實現。(我因為忽視了每秒幀數的微小而神祕的衰減,導致好幾個月沒找到一個多執行緒 bug。)
如果你能一次性看到所有指標,那麼 debug 就容易多了。我喜歡在 TensorBoard 上有儘可能多的指標。用 TensorFlow 記錄任意指標有點棘手,因此考慮使用 easy-tf-log(https://github.com/mrahtz/easy-tf-log),它提供簡單的 tflog(key, value) 介面,無需任何額外設定。
另一件有助於從執行中獲得更多資訊的事情是,花時間嘗試和提前預測失敗。
多虧了事後偏見,在回顧實驗過程時往往很容易發現失敗原因。但是真正令人挫敗的是在你觀察之前,失敗模式就已經很明顯了。開始執行後,第二天回來一看失敗了,在你開始調查失敗原因之前,你就已經發現:「噢,一定是因為我忘記 frobulator 了」。
簡單的事情是有時你可以提前觸發「半事後觀察」。它需要有意識的努力——在開始執行之前先停下來思考五分鐘哪裡可能出錯。我認為最有用的是:
問問自己:「如果這次執行失敗了,我會有多驚訝?」
如果答案是「不會很驚訝」,那麼想象自己處於未來情境中:執行失敗了,問自己:「哪些地方可能出問題:」
修復想到的問題。
重複以上過程直到問題 1 的答案是「非常驚訝」(或至少是「要多驚訝就多驚訝」)。
總是會有很多你無法預測的失敗,有時你仍然遺漏了一些明顯的事情,但是這個過程至少能夠減少一些因為沒有提前想到而出現的愚蠢失誤。
最後,該專案最令人驚訝的是花費時間,以及所需的計算資源。
前者需要從日曆時間的角度來看。我最初的估計是它作為業餘專案,應該花費 3 個月時間,但它實際上用了 8 個月。(而我一開始預估的時間就已經很消極了!)部分原因是低估了每個階段可能花費的時間,但是最大的低估是沒有預測到該專案之外出現的其他事情。很難說這個規律有多廣泛,但是對於業餘專案來說,把預估時間乘以 2 可能是不錯的方法。
更有趣的是每個階段實際花費的時間。我原本的專案計劃中主要階段時間表基本如下:
寫程式碼不費時,費時的是除錯。事實上,在一個所謂的簡單環境上花費的時間 4 倍於最初的實現。(這是我第一個花了數小時的業餘專案,但所得經驗與過去的機器學習專案相似。)
注:從一開始就仔細設計你認為什麼應該是強化學習的「簡單」環境。尤其是,仔細思考:(a)你的獎勵是否真正傳達解決任務的正確資訊,是的,這很容易弄砸;(b)獎勵是僅依賴之前的觀測結果還是也依賴當前的動作。在你進行任意的獎勵預測時,後者都可能是相關的。
第二個令人驚訝的事情是專案所需的計算時間。我很幸運可以使用學校的機房,雖然只有 CPU 機,但已經很好了。對於需要 GPU 的工作(如在一些小部分上進行快速迭代)或機房太繁忙的時候,我用兩個雲服務進行實驗:谷歌雲端計算引擎的虛擬機器、FloydHub。
谷歌雲端計算引擎挺好的,如果你只想用 shell 訪問 GPU 機器,不過我更多地是在 FloydHub 上進行實驗的。FloydHub 是針對機器學習的雲端計算服務。執行 floyd run python awesomecode.py,FloydHub 會設定一個容器,載入和執行你的程式碼。使 FloydHub 如此強大的兩個關鍵因素是:
GPU 驅動預安裝的容器和常用庫。
每次執行都可以自動存檔。每次執行時使用的程式碼、開始執行的命令、任意命令列輸出和任意資料輸出都可以自動儲存,並通過網頁介面設定索引。
FloydHub 的網頁介面。上方:過去執行的索引,和單次執行的概覽。下方:每次執行所用程式碼和執行的任意資料輸出都可以自動存檔。
第二個功能非常重要。對於任何專案,對嘗試過程的詳細記錄和復現之前實驗的能力都是絕對必要的。版本控制軟體有所幫助,但是 a)管理大量輸出比較困難;b)需要極大的勤勉。(比如,如果你開始一些執行,然後做了一點更改,啟動了另一次執行,當你提交第一批執行的結果時,是否能夠清楚看到使用了哪些程式碼?)你可以仔細記錄或展開自己的系統,但是使用 FloydHub 壓根不需要花費這麼多精力。
我喜歡 FloydHub 的其他原因是:
執行結束時容器自動關閉。無需檢查容器是否關閉、虛擬機器是否關閉。
賬單比雲虛擬機器更加直接。
我認為 FloydHub 的一個痛點在於不能自定義容器。如果你的程式碼中有大量的依賴包,你需要在所有執行啟動前安裝它們。這限制了短期執行上的迭代次數。當然,你可以建立一個「dataset」,其中包含了對檔案系統的安裝依賴包的改變,然後在每次執行起始階段複製該 dataset 的檔案(例如,create_floyd_base.sh)。這很尷尬,但仍比不上處理 GPU 驅動的時候。
FloydHub 相比谷歌雲虛擬機器更貴一些:1.2 美元/小時用一臺 K80 GPU 的機器,對比 0.85 美元/小時用一臺配置相似的虛擬機器。除非你的預算很有限,我認為 FloydHub 帶來的額外便利是值得的。只有在並行執行大量計算的時候,谷歌雲虛擬機器才是更加划算的,因為你可以在單個大型虛擬機器上堆疊。
總的來說,該專案花了:
計算引擎上 150 個小時的 GPU 執行時間和 7700 個小時的(wall time × cores)的 CPU 執行時間。
FloydHub 上 292 個小時的 GPU 執行時間。
大學計算機叢集上的 1500 個小時(wall time, 4 to 16 cores)的 CPU 執行時間。
我驚訝地發現在實現專案的 8 個月期間,總共花費了 850 美元(FloydHub 花了 200 美元,谷歌雲虛擬機器花了 650 美元)。
但是即使花了這麼多的精力,我在專案的最後階段仍然遇到了很大的驚(jing)喜(xia):強化學習可能不太穩定以至於我們需要使用不同的隨機種子重複執行多次以確定效能。
例如當我感覺完成了基本工作,我就會直接在環境上執行端到端的測試。但是即使我一直使用最簡單的環境,我仍然遇到了非常大的問題。因此我重新回到 FloydHub 進行調整並執行了三個副本,事實證明我認為優秀的超引數只在三次測試中成功了一次。
三個隨機種子的兩個出現失敗(紅/藍)是很少見的。
為了讓你確切感受到需要做的計算的量級:
使用 A3C 和 16 個工作站,Pong 需要 10 個小時來訓練;
這是 160 個 CPU 小時;
訓練 3 個隨機種子,則是 480 個 CPU 小時。
至於計算開銷:
對於 8 核機器,FloydHub 大約每小時花費 0.5 美元;
因此 10 小時需要花費 5 美元;
同時執行 3 個隨機種子,則每次執行需要花費 15 美元。
從《Deep Reinforcement Learning Doesn't Work Yet》這篇文章中,我們知道,那些不穩定性是正常的、可接受的。實際上,即使「五個隨機種子(常用的報告指標)也可能不足以得到顯著的結果,因為通過仔細的選擇,你可以得到非重疊的置信區間。」
因此在 OpenAI Scholars programme 中提供 25000 美元的 AWS 信貸實際上並不瘋狂,這可能正是確保你的計算可靠的大致成本。
我要表達的意思是,如果你想要完成一個深度強化學習專案,確保你知道你正趟進的是什麼渾水,確保你已經準備好付出多少時間成本和多少經濟成本。
總之,復現一篇強化學習論文很有趣。但在這之後,回頭看看你有哪些技能真正得到了提升。同時,我也很好奇復現一篇論文是不是對過去數月時間的最佳利用。
一方面,我確實感覺到了機器學習工程能力的提升。我在識別常見的強化學習實現錯誤上更有自信了;我的工作流程在整體上變得更好了;從這篇特定的論文中,我學到了關於分散式 TensorFlow 和非共時設計的很多東西。
另一方面,我並不認為我的機器學習研究能力有很大提高(這才是我當初的真正目的)。和實現不同,研究的更加困難的部分似乎總在有趣但易駕馭、具體的思想,以及你確實花費時間實現並認為得到最高回報的思想之後出現。挖掘有趣的思想似乎取決於(a)豐富的概念詞彙;(b)對思想的良好品味。我認為閱讀有影響力的論文、寫總結,並對它們做嚴謹分析是兼顧兩者的好辦法。
因此,無論你想提高工程技能還是研究技能,深度考慮都是值得的。如果你在某方面比較欠缺,最好啟動一個專案來針對性提高。
如果你想要提高兩者,最好是先閱讀論文,直到你找到真正感興趣的東西,能用簡潔的程式碼進行實現,並嘗試對其進行擴充套件。
如果你希望處理一個強化學習專案,下面是一些更具體的注意內容。
選擇需要復現的論文
分為幾部分查詢論文,並避免需要多個部分協同處理的論文。
強化學習
如果我們的強化學習是作為更大系統中的一個元件,請不要嘗試自己實現強化學習演算法。這是一個很大的挑戰,並且我們也能學習到非常多的東西,但是強化學習目前仍然不夠穩定,我們不能確定到底是大型系統存在問題還是作為系統一部分的強化學習存在問題。
在做任何事前,先要檢視用基線模型在我們的環境上訓練智慧體有多麼困難。
不要忘了歸一化觀察值,因為模型很多地方都要使用這些觀察值。
一旦我們認為模型已經基本好了就直接完成一個端到端的測試,那麼成功的訓練要比我們預期的更加脆弱。
如果我們正在使用 OpenAI Gym 環境,注意在-vo 的環境中,當前動作有 25% 的時間會被忽略,並複製前面的動作以替代,這樣會減少環境的確定性。如果我們不希望增加這種額外的隨機性,那麼就要使用-v4 環境。另外,預設環境只會從模擬器中每隔 4 幀抽取一次,以匹配早期的 DeepMind 論文。如果不希望這種取樣,可以使用 NoFrameSkip 環境控制。結合上面的確定性與不跳過取樣,我們可以使用 PongNoFrameskip-v4。
一般機器學習
由於端到端的測試需要很長時間才能完成,因此如果我們需要做一些重構會浪費大量時間。我們需要在第一次實現就檢查錯誤並試執行,而不是在訓練完後重新編寫程式碼與結構。
初始化模型大概需要花 20s,且因為語法檢測會浪費大量的時間。如果你不喜歡使用 IDE 或只能在伺服器用 shell 訪問與編輯,那麼可以花點時間為編輯器配置 linter。或者每當我們嘗試執行時遇到語法錯誤,可以花點時間令 linter 可以在在未來捕捉它。
不要僅僅使用 Dropout,我們還需要注意網路實現中的權重共享,批歸一化同樣也需要注意這一點。
在訓練過程中看到記憶體佔用有規律地上升?這可能是驗證集過大。
如果使用 Adam 作為優化器發現一些奇怪的現象,那可能是因為 Adam 動量有問題。可以嘗試使用 RMSprop 等不帶動量的優化器,或設定 Adam 的超引數β1 為零。
TensorFlow
如果你想除錯計算圖中某個內部節點,可以使用 tf.Print,這個函式會列印該節點在每一次執行計算圖時的輸入。
如果你僅為推斷過程儲存檢查點,則通過不儲存優化器的引數而節省很多空間。
Session.run() 會出現很大的計算開銷,如果可以的話將一個批量中的多個呼叫分組並執行計算圖。
如果在相同機器上執行多個 TensorFlow 例項,那麼就會得到 GPU 記憶體不足的報錯。這可能是因為其中一個例項嘗試儲存所有的 GPU 記憶體,而不是因為模型過大的原因。這是 TF 的預設選項,而如果需要修改為只儲存模型需要的記憶體,可以檢視 allow_growth 選項。
如果你希望從一次執行的多個模組中訪問計算圖,那麼應該可以從多個執行緒中訪問相同的計算圖,但目前鎖定為只允許單執行緒一次讀取。這看起來與 Python 全域性直譯器鎖不同,TensorFlow 會假定在執行繁重任務前釋放。
在使用 Python 過程中,我們不需要擔心溢位問題,在 TensorFlow 中,我們還需要擔心以下問題:
> a = np.array([255, 200]).astype(np.uint8)
> sess.run(tf.reduce_sum(a))
199
如果 GPU 不可用,注意使用 allow_soft_placement 返回到 CPU。如果你編碼的東西無法在 GPU 執行,那麼可以移動到 CPU 中:
with tf.device("/device:GPU:0"):
a = tf.placeholder(tf.uint8, shape=(4))
b = a[..., -1]
sess = tf.Session(config=tf.ConfigProto(allow_soft_placement=True))
sess.run(tf.global_variables_initializer())
# Seems to work fine. But with allow_soft_placement=False
sess = tf.Session(config=tf.ConfigProto(allow_soft_placement=False))
sess.run(tf.global_variables_initializer())
# we get
# Cannot assign a device for operation 'strided_slice_5':
# Could not satisfy explicit device specification '/device:GPU:0'
# because no supported kernel for GPU devices is available.
我們並不知道有多少運算不能在 GPU 上並行化執行,但為了安全起見,我們可以手動回退 CPU:
gpu_name = tf.test.gpu_device_name()
device = gpu_name if gpu_name else "/cpu:0"
with tf.device(device):
# graph code
心理狀態
講真,不要對 TensorBoard 上癮。不可預測的獎勵是對 TensorBoard 上癮的完美示例:大部分時間你檢測執行的如何,這沒什麼,但在訓練過程,有時檢測中忽然就中了大獎。所以有時非常刺激。如果你開始感覺每分鐘都想要檢查 TensorBoard,那你就需要設定合理的檢查時間了。
以下是強化學習的一些入門資源:
Andrej Karpathy 的《Deep Reinforcement Learning: Pong from Pixels》很好的介紹了強化學習的理論動機與直覺:http://karpathy.github.io/2016/05/31/rl/
更多有關強化學習的理論,檢視 David Silver 的文獻:http://www0.cs.ucl.ac.uk/staff/d.silver/web/Teaching.html。該資源對深度強化學習介紹不多,但卻教授了理解論文時需要的詞彙。
John Schulman 的《Nuts and Bolts of Deep RL Experimentation》課程中包含了很多實踐時的注意點,連結:https://www.youtube.com/watch?v=8EcdaCk9KaQ。
想要了解深度強化學習的現狀,可以檢視以下文章:
Alex Irpan 寫的《Deep Reinforcement Learning Doesn't Work Yet》,機器之心對此文做了中文編譯《變革尚未成功:深度強化學習研究的短期悲觀與長期樂觀》。
Vlad Mnih 的視訊《Deep RL Bootcamp Frontiers Lecture I: Recent Advances, Frontiers and Future of Deep RL》,連結:https://www.youtube.com/watch?v=bsuvM1jO-4w
Sergey Levine 的《Deep Robotic Learning》演講,注重改建機器人泛化與取樣的效率,連結:https://www.youtube.com/watch?v=eKaYnXQUb2g。
Pietel Abbeel 在 NIPS 2017 上的 Keynote,講解了深度強化學習近期的技巧。連結:https://www.youtube.com/watch?v=TyOooJC_bLY。
原文連結:http://amid.fish/reproducing-deep-rl
IJCAI 2018 阿里媽媽國際廣告演算法大賽於 2018 年 2 月正式啟動,獲獎隊伍將有機會前往斯德哥爾摩參加 IJCAI 2018。
報名連結:https://tianchi.aliyun.com/markets/tianchi/ijcai2018?spm=a2c41.11300464.0.0