0
本文作者: AI研習(xí)社 | 2017-06-02 11:17 |
雷鋒網(wǎng)按:本文作者楊浩,原文載于作者個(gè)人博客,雷鋒網(wǎng)已獲授權(quán)。
以下內(nèi)容來源于一次部門內(nèi)部的分享,主要針對(duì)AI初學(xué)者,介紹包括CNN、Deep Q Network以及TensorFlow平臺(tái)等內(nèi)容。由于筆者并非深度學(xué)習(xí)算法研究者,因此以下更多從應(yīng)用的角度對(duì)整個(gè)系統(tǒng)進(jìn)行介紹,而不會(huì)進(jìn)行詳細(xì)的公式推導(dǎo)。
關(guān)于Flappy Bird
Flappy Bird(非官方譯名:笨鳥先飛)是一款2013年鳥飛類游戲,由越南河內(nèi)獨(dú)立游戲開發(fā)者阮哈東(Dong Nguyen)開發(fā),另一個(gè)獨(dú)立游戲開發(fā)商GEARS Studios發(fā)布?!?以上內(nèi)來自《維基百科》
Flappy Bird操作簡單,通過點(diǎn)擊手機(jī)屏幕使Bird上升,穿過柱狀障礙物之后得分,碰到則游戲結(jié)束。由于障礙物高低不等,控制Bird上升和下降需要反應(yīng)快并且靈活,要得到較高的分?jǐn)?shù)并不容易,筆者目前最多得過10分。
本文主要介紹如何通過AI(人工智能)的方式玩Flappy Bird游戲,分為以下四個(gè)部分內(nèi)容:
1. Flappy Bird 游戲展示
2. 模型:卷積神經(jīng)網(wǎng)絡(luò)
3. 算法:Deep Q Network
4. 代碼:TensorFlow實(shí)現(xiàn)
在介紹模型、算法前先來直接看下效果,上圖是剛開始訓(xùn)練的時(shí)候,畫面中的小鳥就像無頭蒼蠅一樣亂飛,下圖展示的是在本機(jī)(后面會(huì)給出配置)訓(xùn)練超過10小時(shí)后(訓(xùn)練步數(shù)超過2000000)的情況,其最好成績已經(jīng)超過200分,人類玩家已基本不可能超越。
訓(xùn)練數(shù)小于10000步(剛開始訓(xùn)練)
訓(xùn)練步數(shù)大于2000000步(10小時(shí)后)
由于本機(jī)配置了 CUDA 以及 cuDNN,采用了 NVIDIA 的顯卡進(jìn)行并行計(jì)算,所以這里提前貼一下運(yùn)行時(shí)的日志輸出。
關(guān)于CUDA以及cuDNN的配置,其中有一些坑包括:安裝CUDA之后循環(huán)登錄,屏幕分辨率無法正常調(diào)節(jié)等等,都是由于NVIDIA驅(qū)動(dòng)安裝的問題,這不是本文要討論的主要內(nèi)容,讀者可自行Google。
● 加載CUDA運(yùn)算庫
加載CUDA運(yùn)算庫
● TensorFlow運(yùn)行設(shè)備 /gpu:0
TensorFlow運(yùn)行設(shè)備/gpu:0
/gpu:0 這是TensorFlow平臺(tái)默認(rèn)的配置方法,表示使用系統(tǒng)中的第一塊顯卡。
本機(jī)軟硬件配置:
系統(tǒng):Ubuntu 16.04
顯卡:NVIDIA GeForce GTX 745 4G
版本:TensorFlow 1.0
軟件包:OpenCV 3.2.0、Pygame、Numpy、…
細(xì)心的朋友可能發(fā)現(xiàn),筆者的顯卡配置并不高,GeForce GTX 745,顯存3.94G,可用3.77G(桌面占用了一部分),屬于入門中的入門。對(duì)于專業(yè)做深度學(xué)習(xí)算法的朋友,這個(gè)顯卡必然是不夠的。知乎上有帖子教大家怎么配置更專業(yè)的顯卡,有興趣的可以移步。
神經(jīng)網(wǎng)絡(luò)算法是由眾多的神經(jīng)元可調(diào)的連接權(quán)值連接而成,具有大規(guī)模并行處理、分布式信息存儲(chǔ)、良好的自組織自學(xué)習(xí)能力等特點(diǎn)。人工神經(jīng)元與生物神經(jīng)元結(jié)構(gòu)類似,其結(jié)構(gòu)對(duì)比如下圖所示。
生物神經(jīng)元
人工神經(jīng)元
人工神經(jīng)元的輸入(x1,x2...xm)類似于生物神經(jīng)元的樹突,輸入經(jīng)過不同的權(quán)值(wk1, wk2, ....wkn),加上偏置,經(jīng)過激活函數(shù)得到輸出,最后將輸出傳輸?shù)较乱粚由窠?jīng)元進(jìn)行處理。
單神經(jīng)元輸出函數(shù)
激活函數(shù)為整個(gè)網(wǎng)絡(luò)引入了非線性特性,這也是神經(jīng)網(wǎng)絡(luò)相比于回歸等算法擬合能力更強(qiáng)的原因。常用的激活函數(shù)包括sigmoid、tanh等,它們的函數(shù)表達(dá)式如下:
sigmoid函數(shù)
tanh雙曲正切函數(shù)
這里可以看出,sigmoid函數(shù)的值域是(0,1),tanh函數(shù)的值域是(-1,1)。
卷積神經(jīng)網(wǎng)絡(luò)起源于動(dòng)物的視覺系統(tǒng),主要包含的技術(shù)是:
1. 局部感知域(稀疏連接);
2. 參數(shù)共享;
3. 多卷積核;
4. 池化。
● 1. 局部感知域(稀疏連接)
全連接網(wǎng)絡(luò)的問題在于:
1. 需要訓(xùn)練的參數(shù)過多,容器導(dǎo)致結(jié)果不收斂(梯度消失),且訓(xùn)練難度極大;
2. 實(shí)際上對(duì)于某個(gè)局部的神經(jīng)元來講,它更加敏感的是小范圍內(nèi)的輸入,換句話說,對(duì)于較遠(yuǎn)的輸入,其相關(guān)性很低,權(quán)值也就非常小。
人類的視覺系統(tǒng)決定了人在觀察外界的時(shí)候,總是從局部到全局。
比如,我們看到一個(gè)美女,可能最先觀察到的是美女身上的某些部位(自己體會(huì))。
因此,卷積神經(jīng)網(wǎng)絡(luò)與人類的視覺類似,采用局部感知,低層的神經(jīng)元只負(fù)責(zé)感知局部的信息,在向后傳輸?shù)倪^程中,高層的神經(jīng)元將局部信息綜合起來得到全局信息。
全連接與局部連接的對(duì)比(圖片來自互聯(lián)網(wǎng))
從上圖中可以看出,采用局部連接之后,可以大大的降低訓(xùn)練參數(shù)的量級(jí)。
● 2. 參數(shù)共享
雖然通過局部感知降低了訓(xùn)練參數(shù)的量級(jí),但整個(gè)網(wǎng)絡(luò)需要訓(xùn)練的參數(shù)依然很多。
參數(shù)共享就是將多個(gè)具有相同統(tǒng)計(jì)特征的參數(shù)設(shè)置為相同,其依據(jù)是圖像中一部分的統(tǒng)計(jì)特征與其它部分是一樣的。其實(shí)現(xiàn)是通過對(duì)圖像進(jìn)行卷積(卷積神經(jīng)網(wǎng)絡(luò)命名的來源)。
可以理解為,比如從一張圖像中的某個(gè)局部(卷積核大小)提取了某種特征,然后以這種特征為探測(cè)器,應(yīng)用到整個(gè)圖像中,對(duì)整個(gè)圖像順序進(jìn)行卷積,得到不同的特征。
卷積過程(圖片來自互聯(lián)網(wǎng))
每個(gè)卷積都是一種特征提取方式,就像一個(gè)篩子,將圖像中符合條件(激活值越大越符合條件)的部分篩選出來,通過這種卷積就進(jìn)一步降低訓(xùn)練參數(shù)的量級(jí)。
● 3. 多卷積核
如上,每個(gè)卷積都是一種特征提取方式,那么對(duì)于整幅圖像來講,單個(gè)卷積核提取的特征肯定是不夠的,那么對(duì)同一幅圖像使用多種卷積核進(jìn)行特征提取,就能得到多幅特征圖(feature map)。
不同的卷積核提取不同的特征(圖片來自互聯(lián)網(wǎng))
多幅特征圖可以看成是同一張圖像的不同通道,這個(gè)概念在后面代碼實(shí)現(xiàn)的時(shí)候用得上。
● 4. 池化
得到特征圖之后,可以使用提取到的特征去訓(xùn)練分類器,但依然會(huì)面臨特征維度過多,難以計(jì)算,并且可能過擬合的問題。從圖像識(shí)別的角度來講,圖像可能存在偏移、旋轉(zhuǎn)等,但圖像的主體卻相同的情況。也就是不同的特征向量可能對(duì)應(yīng)著相同的結(jié)果,那么池化就是解決這個(gè)問題的。
池化過程(圖片來自互聯(lián)網(wǎng))
池化就是將池化核范圍內(nèi)(比如2*2范圍)的訓(xùn)練參數(shù)采用平均值(平均值池化)或最大值(最大值池化)來進(jìn)行替代。
終于到了展示模型的時(shí)候,下面這幅圖是筆者手畫的(用電腦畫太費(fèi)時(shí),將就看吧),這幅圖展示了本文中用于訓(xùn)練游戲所用的卷積神經(jīng)網(wǎng)絡(luò)模型。
卷積神經(jīng)網(wǎng)絡(luò)模型
圖像的處理過程
1. 初始輸入四幅圖像80×80×4(4代表輸入通道,初始時(shí)四幅圖像是完全一致的),經(jīng)過卷積核8×8×4×32(輸入通道4,輸出通道32),步距為4(每步卷積走4個(gè)像素點(diǎn)),得到32幅特征圖(feature map),大小為20×20;
2. 將20×20的圖像進(jìn)行池化,池化核為2×2,得到圖像大小為10×10;
3. 再次卷積,卷積核為4×4×32×64,步距為2,得到圖像5×5×64;
4. 再次卷積,卷積核為3×3×64*64,步距為2,得到圖像5×5×64,雖然與上一步得到的圖像規(guī)模一致,但再次卷積之后的圖像信息更為抽象,也更接近全局信息;
5. Reshape,即將多維特征圖轉(zhuǎn)換為特征向量,得到1600維的特征向量;
6. 經(jīng)過全連接1600×512,得到512維特征向量;
7. 再次全連接512×2,得到最終的2維向量[0,1]和[1,0],分別代表游戲屏幕上的是否點(diǎn)擊事件。
可以看出,該模型實(shí)現(xiàn)了端到端的學(xué)習(xí),輸入的是游戲屏幕的截圖信息(代碼中經(jīng)過opencv處理),輸出的是游戲的動(dòng)作,即是否點(diǎn)擊屏幕。深度學(xué)習(xí)的強(qiáng)大在于其數(shù)據(jù)擬合能力,不需要傳統(tǒng)機(jī)器學(xué)習(xí)中復(fù)雜的特征提取過程,而是依靠模型發(fā)現(xiàn)數(shù)據(jù)內(nèi)部的關(guān)系。
不過這也帶來另一方面的問題,那就是深度學(xué)習(xí)高度依賴大量的標(biāo)簽數(shù)據(jù),而這些數(shù)據(jù)獲取成本極高。
有了卷積神經(jīng)網(wǎng)絡(luò)模型,那么怎樣訓(xùn)練模型?使得模型收斂,從而能夠指導(dǎo)游戲動(dòng)作呢?機(jī)器學(xué)習(xí)分為監(jiān)督學(xué)習(xí)、非監(jiān)督學(xué)習(xí)和強(qiáng)化學(xué)習(xí),這里要介紹的Q Network屬于強(qiáng)化學(xué)習(xí)(Reinforcement Learning)的范疇。在正式介紹Q Network之前,先簡單說下它的光榮歷史。
2014年Google 4億美金收購DeepMind的橋段,大家可能聽說過。那么,DeepMind是如何被Google給盯上的呢?最終原因可以歸咎為這篇論文:
DeepMind團(tuán)隊(duì)通過強(qiáng)化學(xué)習(xí),完成了20多種游戲,實(shí)現(xiàn)了端到端的學(xué)習(xí)。其用到的算法就是Q Network。2015年,DeepMind團(tuán)隊(duì)在《Nature》上發(fā)表了一篇升級(jí)版:
自此,在這類游戲領(lǐng)域,人已經(jīng)無法超過機(jī)器了。后來又有了AlphaGo,以及Master,當(dāng)然,這都是后話了。其實(shí)本文也屬于上述論文的范疇,只不過基于TensorFlow平臺(tái)進(jìn)行了實(shí)現(xiàn),加入了一些筆者自己的理解而已。
回到正題,Q Network屬于強(qiáng)化學(xué)習(xí),那么先介紹下強(qiáng)化學(xué)習(xí)。
強(qiáng)化學(xué)習(xí)模型
這張圖是從UCL的課程中拷出來的,課程鏈接地址(YouTube):
強(qiáng)化學(xué)習(xí)過程有兩個(gè)組成部分:
● 智能代理(學(xué)習(xí)系統(tǒng))
● 環(huán)境
如圖所示,在每步迭代過程中,首先智能代理(學(xué)習(xí)系統(tǒng))接收環(huán)境的狀態(tài)st,然后產(chǎn)生動(dòng)作at作用于環(huán)境,環(huán)境接收動(dòng)作at,并且對(duì)其進(jìn)行評(píng)價(jià),反饋給智能代理rt。不斷的循環(huán)這個(gè)過程,就會(huì)產(chǎn)生一個(gè)狀態(tài)/動(dòng)作/反饋的序列:(s1, a1, r1, s2, a2, r2.....,sn, an, rn),而這個(gè)序列讓我們很自然的想起了:
● 馬爾科夫決策過程
MDP:馬爾科夫決策過程
馬爾科夫決策過程與著名的HMM(隱馬爾科夫模型)相同的是,它們都具有馬爾科夫特性。那么什么是馬爾科夫特性呢?簡單來說,就是未來的狀態(tài)只取決于當(dāng)前的狀態(tài),與過去的狀態(tài)無關(guān)。
HMM(馬爾科夫模型)在語音識(shí)別,行為識(shí)別等機(jī)器學(xué)習(xí)領(lǐng)域有較為廣泛的應(yīng)用。條件隨機(jī)場(chǎng)模型(Conditional Random Field)則用于自然語言處理。兩大模型是語音識(shí)別、自然語言處理領(lǐng)域的基石。
上圖可以用一個(gè)很形象的例子來說明。比如你畢業(yè)進(jìn)入了一個(gè)公司,你的初始職級(jí)是T1(對(duì)應(yīng)圖中的 s1),你在工作上刻苦努力,追求上進(jìn)(對(duì)應(yīng)圖中的a1),然后領(lǐng)導(dǎo)覺得你不錯(cuò),準(zhǔn)備給你升職(對(duì)應(yīng)圖中的r1),于是,你升到了T2;你繼續(xù)刻苦努力,追求上進(jìn)......不斷的努力,不斷的升職,最后升到了sn。當(dāng)然,你也有可能不努力上進(jìn),這也是一種動(dòng)作,換句話說,該動(dòng)作a也屬于動(dòng)作集合A,然后得到的反饋r就是沒有升職加薪的機(jī)會(huì)。
這里注意下,我們當(dāng)然希望獲取最多的升職,那么問題轉(zhuǎn)換為:如何根據(jù)當(dāng)前狀態(tài)s(s屬于狀態(tài)集S),從A中選取動(dòng)作a執(zhí)行于環(huán)境,從而獲取最多的r,即r1 + r2 ……+rn的和最大 ?這里必須要引入一個(gè)數(shù)學(xué)公式:狀態(tài)值函數(shù)。
狀態(tài)值函數(shù)模型
公式中有個(gè)折合因子γ,其取值范圍為[0,1],當(dāng)其為0時(shí),表示只考慮當(dāng)前動(dòng)作對(duì)當(dāng)前的影響,不考慮對(duì)后續(xù)步驟的影響,當(dāng)其為1時(shí),表示當(dāng)前動(dòng)作對(duì)后續(xù)每步都有均等的影響。當(dāng)然,實(shí)際情況通常是當(dāng)前動(dòng)作對(duì)后續(xù)得分有一定的影響,但隨著步數(shù)增加,其影響減小。
從公式中可以看出,狀態(tài)值函數(shù)可以通過迭代的方式來求解。增強(qiáng)學(xué)習(xí)的目的就是求解馬爾可夫決策過程(MDP)的最優(yōu)策略。
策略就是如何根據(jù)環(huán)境選取動(dòng)作來執(zhí)行的依據(jù)。策略分為穩(wěn)定的策略和不穩(wěn)定的策略,穩(wěn)定的策略在相同的環(huán)境下,總是會(huì)給出相同的動(dòng)作,不穩(wěn)定的策略則反之,這里我們主要討論穩(wěn)定的策略。
求解上述狀態(tài)函數(shù)需要采用動(dòng)態(tài)規(guī)劃的方法,而具體到公式,不得不提:
● 貝爾曼方程
貝爾曼方程
其中,π代表上述提到的策略,Q π (s, a)相比于V π (s),引入了動(dòng)作,被稱作動(dòng)作值函數(shù)。對(duì)貝爾曼方程求最優(yōu)解,就得到了貝爾曼最優(yōu)性方程。
狀態(tài)值函數(shù)最優(yōu)解
動(dòng)作值函數(shù)最優(yōu)解
求解該方程有兩種方法:策略迭代和值迭代。
● 策略迭代
策略迭代分為兩個(gè)步驟:策略評(píng)估和策略改進(jìn),即首先評(píng)估策略,得到狀態(tài)值函數(shù),其次,改進(jìn)策略,如果新的策略比之前好,就替代老的策略。
策略迭代
● 值迭代
從上面我們可以看到,策略迭代算法包含了一個(gè)策略估計(jì)的過程,而策略估計(jì)則需要掃描(sweep)所有的狀態(tài)若干次,其中巨大的計(jì)算量直接影響了策略迭代算法的效率。而值迭代每次只掃描一次,更新過程如下:
值迭代
即在值迭代的第k+1次迭代時(shí),直接將能獲得的最大的Vπ(s)值賦給Vk+1。
● Q-Learning
Q-Learning是根據(jù)值迭代的思路來進(jìn)行學(xué)習(xí)的。該算法中,Q值更新的方法如下:
Q值更新方法
雖然根據(jù)值迭代計(jì)算出目標(biāo)Q值,但是這里并沒有直接將這個(gè)Q值(是估計(jì)值)直接賦予新的Q,而是采用漸進(jìn)的方式類似梯度下降,朝目標(biāo)邁近一小步,取決于α,這就能夠減少估計(jì)誤差造成的影響。類似隨機(jī)梯度下降,最后可以收斂到最優(yōu)的Q值。具體算法如下:
Q-Learning算法
如果沒有接觸過動(dòng)態(tài)規(guī)劃的童鞋看上述公式可能有點(diǎn)頭大,下面通過表格來演示下Q值更新的過程,大家就明白了。
Q-Learning算法的過程就是存儲(chǔ)Q值的過程。上表中,橫列為狀態(tài)s,縱列為Action a,s和a決定了表中的Q值。
● 第一步:初始化,將表中的Q值全部置0;
● 第二步:根據(jù)策略及狀態(tài)s,選擇a執(zhí)行。假定當(dāng)前狀態(tài)為s1,由于初始值都為0,所以任意選取a執(zhí)行,假定這里選取了a2執(zhí)行,得到了reward為1,并且進(jìn)入了狀態(tài)s3。根據(jù)Q值更新公式:
Q值更新公式
來更新Q值,這里我們假設(shè)α是1,λ也等于1,也就是每一次都把目標(biāo)Q值賦給Q。那么這里公式變成:
Q值更新公式
所以在這里,就是
本次Q值更新
那么對(duì)應(yīng)的s3狀態(tài),最大值是0,所以
Q值
Q表格就變成:
然后置位當(dāng)前狀態(tài)s為s3。
● 第三步:繼續(xù)循環(huán)操作,進(jìn)入下一次動(dòng)作,當(dāng)前狀態(tài)是s3,假設(shè)選擇動(dòng)作a3,然后得到reward為2,狀態(tài)變成s1,那么我們同樣進(jìn)行更新:
Q值更新
所以Q的表格就變成:
● 第四步: 繼續(xù)循環(huán),Q值在試驗(yàn)的同時(shí)反復(fù)更新,直到收斂。
上述表格演示了具有4種狀態(tài)/4種行為的系統(tǒng),然而在實(shí)際應(yīng)用中,以本文講到的Flappy Bird游戲?yàn)槔?,界面?0*80個(gè)像素點(diǎn),每個(gè)像素點(diǎn)的色值有256種可能。那么實(shí)際的狀態(tài)總數(shù)為256的80*80次方,這是一個(gè)很大的數(shù)字,直接導(dǎo)致無法通過表格的思路進(jìn)行計(jì)算。
因此,為了實(shí)現(xiàn)降維,這里引入了一個(gè)價(jià)值函數(shù)近似的方法,通過一個(gè)函數(shù)表近似表達(dá)價(jià)值函數(shù):
價(jià)值函數(shù)近似
其中,ω 與 b 分別為參數(shù)??吹竭@里,終于可以聯(lián)系到前面提到的神經(jīng)網(wǎng)絡(luò)了,上面的表達(dá)式不就是神經(jīng)元的函數(shù)嗎?
● Q-network
下面這張圖來自論文《Human-level Control through Deep Reinforcement Learning》,其中詳細(xì)介紹了上述將Q值神經(jīng)網(wǎng)絡(luò)化的過程。(感興趣的可以點(diǎn)之前的鏈接了解原文~)
Q-network
以本文為例,輸入是經(jīng)過處理的4個(gè)連續(xù)的80x80圖像,然后經(jīng)過三個(gè)卷積層,一個(gè)池化層,兩個(gè)全連接層,最后輸出包含每一個(gè)動(dòng)作Q值的向量。
現(xiàn)在已經(jīng)將Q-learning神經(jīng)網(wǎng)絡(luò)化為Q-network了,接下來的問題是如何訓(xùn)練這個(gè)神經(jīng)網(wǎng)絡(luò)。神經(jīng)網(wǎng)絡(luò)訓(xùn)練的過程其實(shí)就是一個(gè)最優(yōu)化方程求解的過程,定義系統(tǒng)的損失函數(shù),然后讓損失函數(shù)最小化的過程。
訓(xùn)練過程依賴于上述提到的DQN算法,以目標(biāo)Q值作為標(biāo)簽,因此,損失函數(shù)可以定義為:
DQN損失函數(shù)(來源于論文)
上面公式是s',a'即下一個(gè)狀態(tài)和動(dòng)作。確定了損失函數(shù),確定了獲取樣本的方式,DQN的整個(gè)算法也就成型了!
DQN算法(來源于論文)
值得注意的是這里的D—Experience Replay,也就是經(jīng)驗(yàn)池,就是如何存儲(chǔ)樣本及采樣的問題。
由于玩Flappy Bird游戲,采集的樣本是一個(gè)時(shí)間序列,樣本之間具有連續(xù)性,如果每次得到樣本就更新Q值,受樣本分布影響,效果會(huì)不好。因此,一個(gè)很直接的想法就是把樣本先存起來,然后隨機(jī)采樣如何?這就是Experience Replay的思想。
算法實(shí)現(xiàn)上,先反復(fù)實(shí)驗(yàn),并且將實(shí)驗(yàn)數(shù)據(jù)存儲(chǔ)在D中;存儲(chǔ)到一定程度,就從中隨機(jī)抽取數(shù)據(jù),對(duì)損失函數(shù)進(jìn)行梯度下降。
終于到了看代碼的時(shí)候。首先申明下,當(dāng)筆者從Deep Mind的論文入手,試圖用TensorFlow實(shí)現(xiàn)對(duì)Flappy Bird游戲進(jìn)行實(shí)現(xiàn)時(shí),發(fā)現(xiàn)github已有大神完成demo。思路相同,所以直接以公開代碼為例進(jìn)行分析說明了。
如有源碼需要,請(qǐng)移步github:Using Deep Q-Network to Learn How To Play Flappy Bird。
代碼從結(jié)構(gòu)上來講,主要分為以下幾部分:
● GameState游戲類,frame_step方法控制移動(dòng)
● CNN模型構(gòu)建
● OpenCV-Python圖像預(yù)處理方法
● 模型訓(xùn)練過程
1. GameState游戲類及frame_step方法
通過Python實(shí)現(xiàn)游戲必然要用pygame庫,其包含時(shí)鐘、基本的顯示控制、各種游戲控件、觸發(fā)事件等,對(duì)此有興趣的,可以詳細(xì)了解pygame。frame_step方法的入?yún)閟hape為 (2,) 的ndarray,值域: [1,0]:什么都不做; [0,1]:提升Bird。來看下代碼實(shí)現(xiàn):
if input_actions[1] == 1:
if self.playery > -2 * PLAYER_HEIGHT:
self.playerVelY = self.playerFlapAcc
self.playerFlapped = True
# SOUNDS['wing'].play()
后續(xù)操作包括檢查得分、設(shè)置界面、檢查是否碰撞等,這里不再詳細(xì)展開。
frame_step方法的返回值是:
return image_data, reward, terminal
分別表示界面圖像數(shù)據(jù),得分以及是否結(jié)束游戲。對(duì)應(yīng)前面強(qiáng)化學(xué)習(xí)模型,界面圖像數(shù)據(jù)表示環(huán)境狀態(tài) s,得分表示環(huán)境給予學(xué)習(xí)系統(tǒng)的反饋 r。
2. CNN模型構(gòu)建
該Demo中包含三個(gè)卷積層,一個(gè)池化層,兩個(gè)全連接層,最后輸出包含每一個(gè)動(dòng)作Q值的向量。因此,首先定義權(quán)重、偏置、卷積和池化函數(shù):
# 權(quán)重
def weight_variable(shape):
initial = tf.truncated_normal(shape, stddev=0.01)
return tf.Variable(initial)
# 偏置
def bias_variable(shape):
initial = tf.constant(0.01, shape=shape)
return tf.Variable(initial)
# 卷積
def conv2d(x, W, stride):
return tf.nn.conv2d(x, W, strides=[1, stride, stride, 1], padding="SAME")
# 池化
def max_pool_2x2(x):
return tf.nn.max_pool(x, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding="SAME")
然后,通過上述函數(shù)構(gòu)建卷積神經(jīng)網(wǎng)絡(luò)模型(對(duì)代碼中參數(shù)不解的,可直接往前翻,看上面那張手畫的圖)。
def createNetwork():
# 第一層卷積
W_conv1 = weight_variable([8, 8, 4, 32])
b_conv1 = bias_variable([32])
# 第二層卷積
W_conv2 = weight_variable([4, 4, 32, 64])
b_conv2 = bias_variable([64])
# 第三層卷積
W_conv3 = weight_variable([3, 3, 64, 64])
b_conv3 = bias_variable([64])
# 第一層全連接
W_fc1 = weight_variable([1600, 512])
b_fc1 = bias_variable([512])
# 第二層全連接
W_fc2 = weight_variable([512, ACTIONS])
b_fc2 = bias_variable([ACTIONS])
# 輸入層
s = tf.placeholder("float", [None, 80, 80, 4])
# 第一層隱藏層+池化層
h_conv1 = tf.nn.relu(conv2d(s, W_conv1, 4) + b_conv1)
h_pool1 = max_pool_2x2(h_conv1)
# 第二層隱藏層(這里只用了一層池化層)
h_conv2 = tf.nn.relu(conv2d(h_pool1, W_conv2, 2) + b_conv2)
# h_pool2 = max_pool_2x2(h_conv2)
# 第三層隱藏層
h_conv3 = tf.nn.relu(conv2d(h_conv2, W_conv3, 1) + b_conv3)
# h_pool3 = max_pool_2x2(h_conv3)
# Reshape
# h_pool3_flat = tf.reshape(h_pool3, [-1, 256])
h_conv3_flat = tf.reshape(h_conv3, [-1, 1600])
# 全連接層
h_fc1 = tf.nn.relu(tf.matmul(h_conv3_flat, W_fc1) + b_fc1)
# 輸出層
# readout layer
readout = tf.matmul(h_fc1, W_fc2) + b_fc2
return s, readout, h_fc1
3. OpenCV-Python圖像預(yù)處理方法
在Ubuntu中安裝opencv的步驟比較麻煩,當(dāng)時(shí)也踩了不少坑,各種Google解決。建議安裝opencv3。
這部分主要對(duì)frame_step方法返回的數(shù)據(jù)進(jìn)行了灰度化和二值化,也就是最基本的圖像預(yù)處理方法。
x_t, r_0, terminal = game_state.frame_step(do_nothing)
# 首先將圖像轉(zhuǎn)換為80*80,然后進(jìn)行灰度化
x_t = cv2.cvtColor(cv2.resize(x_t, (80, 80)), cv2.COLOR_BGR2GRAY)
# 對(duì)灰度圖像二值化
ret, x_t = cv2.threshold(x_t, 1, 255, cv2.THRESH_BINARY)
# 四通道輸入圖像
s_t = np.stack((x_t, x_t, x_t, x_t), axis=2)
4. DQN訓(xùn)練過程
這是代碼部分要講的重點(diǎn),也是上述Q-learning算法的代碼化。
i. 在進(jìn)入訓(xùn)練之前,首先創(chuàng)建一些變量:
# define the cost function
a = tf.placeholder("float", [None, ACTIONS])
y = tf.placeholder("float", [None])
readout_action = tf.reduce_sum(tf.multiply(readout, a), axis=1)
cost = tf.reduce_mean(tf.square(y - readout_action))
train_step = tf.train.AdamOptimizer(1e-6).minimize(cost)
# open up a game state to communicate with emulator
game_state = game.GameState()
# store the previous observations in replay memory
D = deque()
在TensorFlow中,通常有三種讀取數(shù)據(jù)的方式:Feeding、Reading from files和Preloaded data。Feeding是最常用也最有效的方法。即在模型(Graph)構(gòu)建之前,先使用placeholder進(jìn)行占位,但此時(shí)并沒有訓(xùn)練數(shù)據(jù),訓(xùn)練是通過feed_dict傳入數(shù)據(jù)。
這里的a表示輸出的動(dòng)作,即強(qiáng)化學(xué)習(xí)模型中的Action,y表示標(biāo)簽值,readout_action表示模型輸出與a相乘后,在一維求和,損失函數(shù)對(duì)標(biāo)簽值與輸出值的差進(jìn)行平方,train_step表示對(duì)損失函數(shù)進(jìn)行Adam優(yōu)化。
賦值的過程為:
# perform gradient step
train_step.run(feed_dict={
y: y_batch,
a: a_batch,
s: s_j_batch}
)
ii. 創(chuàng)建游戲及經(jīng)驗(yàn)池 D
# open up a game state to communicate with emulator
game_state = game.GameState()
# store the previous observations in replay memory
D = deque()
經(jīng)驗(yàn)池 D采用了隊(duì)列的數(shù)據(jù)結(jié)構(gòu),是TensorFlow中最基礎(chǔ)的數(shù)據(jù)結(jié)構(gòu),可以通過dequeue()和enqueue([y])方法進(jìn)行取出和壓入數(shù)據(jù)。經(jīng)驗(yàn)池 D用來存儲(chǔ)實(shí)驗(yàn)過程中的數(shù)據(jù),后面的訓(xùn)練過程會(huì)從中隨機(jī)取出一定量的batch進(jìn)行訓(xùn)練。
變量創(chuàng)建完成之后,需要調(diào)用TensorFlow系統(tǒng)方法tf.global_variables_initializer()添加一個(gè)操作實(shí)現(xiàn)變量初始化。運(yùn)行時(shí)機(jī)是在模型構(gòu)建完成,Session建立之初。比如:
# Create two variables.
weights = tf.Variable(tf.random_normal([784, 200], stddev=0.35),
name="weights")
biases = tf.Variable(tf.zeros([200]), name="biases")
...
# Add an op to initialize the variables.
init_op = tf.global_variables_initializer()
# Later, when launching the model
with tf.Session() as sess:
# Run the init operation.
sess.run(init_op)
...
# Use the model
...
iii. 參數(shù)保存及加載
采用TensorFlow訓(xùn)練模型,需要將訓(xùn)練得到的參數(shù)進(jìn)行保存,不然一關(guān)機(jī),就一夜回到解放前了。TensorFlow采用Saver來保存。一般在Session()建立之前,通過tf.train.Saver()獲取Saver實(shí)例。
saver = tf.train.Saver()
變量的恢復(fù)使用saver的restore方法:
# Create some variables.
v1 = tf.Variable(..., name="v1")
v2 = tf.Variable(..., name="v2")
...
# Add ops to save and restore all the variables.
saver = tf.train.Saver()
# Later, launch the model, use the saver to restore variables from disk, and
# do some work with the model.
with tf.Session() as sess:
# Restore variables from disk.
saver.restore(sess, "/tmp/model.ckpt")
print("Model restored.")
# Do some work with the model
...
在該Demo訓(xùn)練時(shí),也采用了Saver進(jìn)行參數(shù)保存。
# saving and loading networks
saver = tf.train.Saver()
checkpoint = tf.train.get_checkpoint_state("saved_networks")
if checkpoint and checkpoint.model_checkpoint_path:
saver.restore(sess, checkpoint.model_checkpoint_path)
print("Successfully loaded:", checkpoint.model_checkpoint_path)
else:
print("Could not find old network weights")
首先加載CheckPointState文件,然后采用saver.restore對(duì)已存在參數(shù)進(jìn)行恢復(fù)。
在該Demo中,每隔10000步,就對(duì)參數(shù)進(jìn)行保存:
# save progress every 10000 iterations
if t % 10000 == 0:
saver.save(sess, 'saved_networks/' + GAME + '-dqn', global_step=t)
iv. 實(shí)驗(yàn)及樣本存儲(chǔ)
首先,根據(jù)ε 概率選擇一個(gè)Action。
# choose an action epsilon greedily
readout_t = readout.eval(feed_dict={s: [s_t]})[0]
a_t = np.zeros([ACTIONS])
action_index = 0
if t % FRAME_PER_ACTION == 0:
if random.random() <= epsilon:
print("----------Random Action----------")
action_index = random.randrange(ACTIONS)
a_t[random.randrange(ACTIONS)] = 1
else:
action_index = np.argmax(readout_t)
a_t[action_index] = 1
else:
a_t[0] = 1 # do nothing
這里,readout_t是訓(xùn)練數(shù)據(jù)為之前提到的四通道圖像的模型輸出。a_t是根據(jù)ε 概率選擇的Action。
其次,執(zhí)行選擇的動(dòng)作,并保存返回的狀態(tài)、得分。
# run the selected action and observe next state and reward
x_t1_colored, r_t, terminal = game_state.frame_step(a_t)
x_t1 = cv2.cvtColor(cv2.resize(x_t1_colored, (80, 80)), cv2.COLOR_BGR2GRAY)
ret, x_t1 = cv2.threshold(x_t1, 1, 255, cv2.THRESH_BINARY)
x_t1 = np.reshape(x_t1, (80, 80, 1))
# s_t1 = np.append(x_t1, s_t[:,:,1:], axis = 2)
s_t1 = np.append(x_t1, s_t[:, :, :3], axis=2)
# store the transition in D
D.append((s_t, a_t, r_t, s_t1, terminal))
經(jīng)驗(yàn)池D保存的是一個(gè)馬爾科夫序列。(s_t, a_t, r_t, s_t1, terminal)分別表示t時(shí)的狀態(tài)s_t,執(zhí)行的動(dòng)作a_t,得到的反饋r_t,以及得到的下一步的狀態(tài)s_t1和游戲是否結(jié)束的標(biāo)志terminal。
在下一訓(xùn)練過程中,更新當(dāng)前狀態(tài)及步數(shù):
# update the old values
s_t = s_t1
t += 1
重復(fù)上述過程,實(shí)現(xiàn)反復(fù)實(shí)驗(yàn)及樣本存儲(chǔ)。
v. 通過梯度下降進(jìn)行模型訓(xùn)練
在實(shí)驗(yàn)一段時(shí)間后,經(jīng)驗(yàn)池D中已經(jīng)保存了一些樣本數(shù)據(jù)后,就可以從這些樣本數(shù)據(jù)中隨機(jī)抽樣,進(jìn)行模型訓(xùn)練了。這里設(shè)置樣本數(shù)為OBSERVE = 100000.。隨機(jī)抽樣的樣本數(shù)為BATCH = 32。
if t > OBSERVE:
# sample a minibatch to train on
minibatch = random.sample(D, BATCH)
# get the batch variables
s_j_batch = [d[0] for d in minibatch]
a_batch = [d[1] for d in minibatch]
r_batch = [d[2] for d in minibatch]
s_j1_batch = [d[3] for d in minibatch]
y_batch = []
readout_j1_batch = readout.eval(feed_dict={s: s_j1_batch})
for i in range(0, len(minibatch)):
terminal = minibatch[i][4]
# if terminal, only equals reward
if terminal:
y_batch.append(r_batch[i])
else:
y_batch.append(r_batch[i] + GAMMA * np.max(readout_j1_batch[i]))
# perform gradient step
train_step.run(feed_dict={
y: y_batch,
a: a_batch,
s: s_j_batch}
)
s_j_batch、a_batch、r_batch、s_j1_batch是從經(jīng)驗(yàn)池D中提取到的馬爾科夫序列(Java童鞋羨慕Python的列表推導(dǎo)式?。瑈_batch為標(biāo)簽值,若游戲結(jié)束,則不存在下一步中狀態(tài)對(duì)應(yīng)的Q值(回憶Q值更新過程),直接添加r_batch,若未結(jié)束,則用折合因子(0.99)和下一步中狀態(tài)的最大Q值的乘積,添加至y_batch。
最后,執(zhí)行梯度下降訓(xùn)練,train_step的入?yún)⑹莝_j_batch、a_batch和y_batch。差不多經(jīng)過2000000步(在本機(jī)上大概10個(gè)小時(shí))訓(xùn)練之后,就能達(dá)到本文開頭動(dòng)圖中的效果啦。
以上。
雷鋒網(wǎng)相關(guān)閱讀:
圖像風(fēng)格遷移 (Neural Style) 簡史
雷峰網(wǎng)版權(quán)文章,未經(jīng)授權(quán)禁止轉(zhuǎn)載。詳情見轉(zhuǎn)載須知。