1. 程式人生 > >使用Keras和DDPG玩賽車遊戲(自動駕駛)

使用Keras和DDPG玩賽車遊戲(自動駕駛)

為什麼選擇TORCS遊戲

  • 《The Open Racing Car Simulator》(TORCS)是一款開源3D賽車模擬遊戲
  • 看著AI學會開車是一件很酷的事
  • 視覺化並考察神經網路的學習過程,而不是僅僅看最終結果
  • 容易看出神經網路陷入區域性最優
  • 幫助理解自動駕駛中的機器學習技術

安裝執行

  • 基於Ubuntu16.04,python3安裝(Python2也可)
  • 先安裝一些包:
sudo apt-get install xautomation
sudo pip3 install numpy
sudo pip3 install gym
  • 再下載gym_torcs原始碼(建議迅雷+download zip,比較快),解壓壓縮包。
  • 然後將gym_torcs/vtorcs-RL-color/src/modules/simu/simuv2/simu.cpp 中第64行替換為if (isnan((float)(car->ctrl->gear)) || isinf(((float)(car->ctrl->gear)))) car->ctrl->gear = 0;,否則新的gcc會報錯,Ubuntu14可能不用管。
    程式碼修改
  • 然後cdgym_torcsvtorcs-RL-color目錄,執行以下命令:
sudo apt-get install libglib2.0-dev  libgl1-mesa-dev libglu1-mesa-dev  freeglut3-dev  libplib-dev  libopenal-dev libalut-dev libxi-dev libxmu-dev libxrender-dev  libxrandr-dev libpng12-dev 
./configure
make
sudo make install
sudo make datainstall
  • 檢查TORCS是否正確安裝:開啟一個終端,輸入命令torcs,然後會出現圖形介面,然後依次點選Race –> Practice –> New Race –> 會看到一個藍屏輸出資訊“Initializing Driver scr_server1”。此時再開啟一個終端,輸入命令python3 snakeoil3_gym.py可以立刻看到一個演示,則安裝成功。
  • 然後
git clone https://github.com/yanpanlau/DDPG-Keras-Torcs.git #建議下載zip
cd DDPG-Keras-Torcs
cp *.* ../gym_torcs
cd ../gym_torcs
python3 ddpg.py 

作者使用的是python2,所以他將snakeoil3_gym.py檔案做了一些修改。我用的是python3,還需要將snakeoil3_gym.py檔案再改回來,應該是在上面cp命令中不要複製覆蓋snakeoil3_gym.py檔案就對了。如果覆蓋了就將snakeoil3_gym.py檔案中python2的一些語法改成python3的:如print要加個括號,except要改成except socket.error as emsgunicode()改成str()。這樣就可以成功運行了。

背景

  • 在上一篇譯文新手向——使用Keras+卷積神經網路玩小鳥中,展示瞭如何使用深度Q學習神經網路來玩耍FlapyBird。但是,深Q網路的一個很大的侷限性在於它的輸出(是所有動作的Q值列表)是離散的,也就是對遊戲的輸入動作是離散的,而像在賽車遊戲中的轉向動作是一個連續的過程。一個顯而易見的使DQN適應連續域的方法就是簡單地將連續的動作空間離散化。但是馬上我們就會遭遇‘維數災難’問題。比如說,如果你將轉盤從-90度到+90度的轉動劃分為5度一格,然後將將從0km到300km的加速度每5km一劃分,你的輸出組合將是36種轉盤狀態乘以60種速度狀態等於2160種可能的組合。當你想讓機器人進行一些更為專業化的操作時情況會更糟,比如腦外科手術這樣需要精細的行為控制的操作,想要使用離散化來實現需要的操作精度就太naive了。

策略網路

  • 首先,我們將要定義一個策略網路來實現我們的AI-司機。這個網路將接收遊戲的狀態(例如,賽車的速度,賽車和賽道中軸之間的距離等)並且決定我們該做什麼(方向盤向左打向右打,踩油門還是踩剎車)。它被叫做基於策略的強化學習,因為我們直接將策略引數化:
\pi_\theta(s, a) = P [a | s, \theta]

這裡,s是狀態,a是行為/動作,θ是策略網路的模型引數,π是常見的表示策略的符號。我們可以設想策略是我們行為的代理人,即一個從狀態到動作的對映函式。

確定性VS隨機策略

  • 確定性策略: a=μ(s)
  • 隨機策略: π(a∣s)=P[a∣s]

    為什麼在確定性策略之外我們還需要隨機策略呢?理解一個確定性政策是容易的。我看到一個特定的狀態輸入,然後我採取特定的動作。但有時確定性策略不起作用,當你面對的第一個狀態是個類似下面的白板時:

    如果你還使用相同的確定性策略,你的網路將總是把棋子放在一個“特別”的位置,這是一個非常不好的行為,它會使你的對手能夠預測你。在這種情況下,一個隨機策略比確定性策略更合適。

策略目標函式

所以我們怎麼找到π_​θ​​(s,a)呢?實際上,我們能夠使用增強技術來解決它。例如,假設AI正在努力學習如何左轉。在一開始,AI可能根本就不會轉方向盤並撞上路邊,獲得一個負獎勵(懲罰),所以神經網路將調整模型引數θ,避免下一次再撞上路邊。多次嘗試之後,它會發現,“啊哈,如果我把方向盤往更左打一點,我就不會這麼早撞到路邊了”。用數學語言來說,這就是策略目標函式。
未來的總獎勵函式定義為從離散的時間t開始的每一階段的獎勵之和:
R_t = r_t + r_{t+1} + r_{t+2} ... + r_n


上面的函式其實是馬後炮函式,因為事情的總獎勵在事情結束之前是不會確定的,說不定有轉機呢(未來的動作數一般是很多的,也可能是不確定的),所謂俗語:"不到最後一刻絕不罷休"和"蓋棺定論"講得就是這個道理,而且複雜的世界中,同樣的決策它的結果也可能是不一樣的,總有人運氣好,也有人運氣差,"一個人的命運,不光要看個人的奮鬥,還要考慮歷史的行程",也就是說決策的結果可能還受一個不可掌控的未知引數影響。
所以,作為一種提供給當前狀態做判斷的預期,我們構造一個相對簡單的函式,既充分考慮又在一定程度上弱化未來的獎勵(這個未來的獎勵其實是基於經驗得到,也就是訓練的意義所在),得到未來的總折扣獎勵(貼現獎勵)函式:
R_t = r_t + \gamma r_{t+1} + \gamma^{2} r_{t+2} ... + \gamma^{n-t} r_n——\gammaγ是折扣係數,一般取在(0,1)區間中

一個直觀的策略目標函式將是總折扣獎勵的期望:
L(\theta) = E[r_1 + \gamma r_2 + \gamma^{2} r_3 + ... | \pi_\theta(s,a)],這裡暫時取t為1,總獎勵為R


L(\theta) = E_{x\sim p(x|\theta)}[R]

在這裡,總獎勵R的期望是在 由引數θ調整的某一概率分佈p(x∣θ) 下計算的。

這時,又要用到我們的Q函數了,先回想一下上一篇譯文的內容。
由上文的未來總折扣獎勵R_t可以看出它能表示為遞迴的形式:
R_t = r_t + \gamma * R_{t+1},將上文的R_t中的t代換為t+1代入此式即可驗證


而我們的Q函式(在s狀態下選擇動作a的最大貼現獎勵)是
Q(s_t, a_t) = max R_{t+1}

這裡等式左邊的t和右邊的t+1可能看上去有些錯位,因為它是按下面這個圖走的,不用太糾結。

但是接下來我們並沒有和Q-learning採取同樣的Q值更新策略,重點來了:
我們採用了SARSA —— State-Action-Reward-State-Action代表了狀態-動作-獎勵-狀態-動作。在SARSA中,我們開始於狀態1,執行動作1,然後得到獎勵1,於是我們到了狀態2,在返回並更新在狀態1下執行動作1的Q值之前,我們又執行了另一個動作(動作2)然後得到獎勵2。相反,在Q-learning中,我們開始於狀態1,執行動作1,然後得到獎勵1,接著就是檢視在狀態2中無論做出任一動作的最大可能獎勵,並用這個值來更新狀態1下執行動作1的Q值。所以不同的是未來獎勵被發現的方式。在Q-learning中它只是在狀態2下最可能採取的最有利的動作的最大預期值,而在SARSA中它就是實際執行的動作的獎勵值。
這意味著SARSA考慮到了賽車(遊戲代理)移動的控制策略(由控制策略我們連續地執行了兩步),並整合到它的動作值的更新中,而Q-learning只是假設一個最優策略被執行。不考慮所謂的最優而遵循一定的策略有時會是好事。
於是乎,在連續的情況下,我們使用了SARSA,Q值公式去掉了max,它還是遞迴的,只是去掉了'武斷'的max,而包含了控制策略,不過它並沒有在這個Q值公式裡表現出來,在更新公式的迭代中可以體現出來:
Q(s_t, a_t) = R_{t+1}

Q值的更新公式從Q-learning的
Q-learning更新公式
變為
SARSA更新公式

所以,接著我們可以寫出確定性策略a=μ(s)的梯度:
\frac{\partial L(\theta)}{\partial \theta} = E_{x\sim~p(x|\theta)}[\frac{\partial Q}{\partial \theta}]


然後應用高數中的鏈式法則:

它已經被證明(Silver el at. 2014)是策略梯度,即只要你按照上述的梯度公式來更新你的模型引數,你就會得到最大期望獎勵。

補充

演員-評論家演算法本質上是策略梯度演算法和值函式方法的混合演算法。策略函式被稱為演員,而價值函式被稱為評論家。本質上,演員在當前環境的給定狀態s下產生動作a,而評論家產生一個訊號來批評演員做出的動作。這在人類世界中是相當自然的,其中研究生(演員)做實際工作,導師(評論家)批評你的工作來讓你下一次做得更好:)。在我們的TORCS例子中,我們使用了SARSA作為我們的評論家模型,並使用策略梯度演算法作為我們的演員模型。它們的關係如圖:

關係圖
回到之前的公式,我們將Q做近似代換,其中w是神經網路的權重。所以我們得到深度策略性梯度公式(DDPG):
\frac{\partial L(\theta)}{\partial \theta} = \frac{\partial Q(s,a,w)}{\partial a}\frac{\partial a}{\partial \theta}

其中策略引數θ可以通過隨機梯度上升來更新。
此外,還有我們的損失函式,與SARSA的Q函式迭代更新公式一致:
Loss = [r + \gamma Q (s^{'},a^{'}) - Q(s,a)]^{2}

Q值用於估計當前演員策略的值。
下圖是演員-評論家模型的結構圖:


演員-評論家結構圖

Keras程式碼說明

演員網路

首先我們來看如何在Keras中構建演員網路。這裡我們使用了2個隱藏層分別擁有300和600個隱藏單元。輸出包括3個連續的動作。

  1. 轉方向盤。是一個單元的輸出層,使用tanh啟用函式(輸出-1意味著最大右轉,+1表示最大左轉)
  2. 加速。是一個單元的輸出層,使用sigmoid啟用函式(輸出0代表不加速,1表示全加速)。
  3. 剎車。是一個單元的輸出層,也使用sigmoid啟用函式(輸出0表示不制動,1表示緊急制動)。
    def create_actor_network(self, state_size,action_dim):
        print("Now we build the model")
        S = Input(shape=[state_size])  
        h0 = Dense(HIDDEN1_UNITS, activation='relu')(S)
        h1 = Dense(HIDDEN2_UNITS, activation='relu')(h0)
        Steering = Dense(1,activation='tanh',init=lambda shape, name: normal(shape, scale=1e-4, name=name))(h1)   
        Acceleration = Dense(1,activation='sigmoid',init=lambda shape, name: normal(shape, scale=1e-4, name=name))(h1)   
        Brake = Dense(1,activation='sigmoid',init=lambda shape, name: normal(shape, scale=1e-4, name=name))(h1)   
        V = merge([Steering,Acceleration,Brake],mode='concat')          
        model = Model(input=S,output=V)
        print("We finished building the model")
        return model, model.trainable_weights, S

我們使用了一個Keras函式Merge來合併三個輸出層(concat引數是將待合併層輸出沿著最後一個維度進行拼接),為什麼我們不使用如下的傳統的定義方式呢:

V = Dense(3,activation='tanh')(h1) 

使用3個不同的Dense()函式允許每個連續動作有不同的啟用函式,例如,對加速使用tanh啟用函式的話是沒有意義的,tanh的輸出是[-1,1],而加速的範圍是[0,1]。
還要注意的是,在輸出層我們使用了μ = 0,σ = 1e-4的正態分佈初始化來確保策略的初期輸出接近0。

評論家網路

評論家網路的構造和上一篇的小鳥深Q網路非常相似。唯一的區別是我們使用了2個300和600隱藏單元的隱藏層。此外,評論家網路同時接受了狀態和動作的輸入。根據DDPG的論文,動作輸入直到網路的第二個隱藏層才被使用。同樣我們使用了Merge函式來合併動作和狀態的隱藏層。

    def create_critic_network(self, state_size,action_dim):
        print("Now we build the model")
        S = Input(shape=[state_size])
        A = Input(shape=[action_dim],name='action2')    
        w1 = Dense(HIDDEN1_UNITS, activation='relu')(S)
        a1 = Dense(HIDDEN2_UNITS, activation='linear')(A)
        h1 = Dense(HIDDEN2_UNITS, activation='linear')(w1)
        h2 = merge([h1,a1],mode='sum')    
        h3 = Dense(HIDDEN2_UNITS, activation='relu')(h2)
        V = Dense(action_dim,activation='linear')(h3)  
        model = Model(input=[S,A],output=V)
        adam = Adam(lr=self.LEARNING_RATE)
        model.compile(loss='mse', optimizer=adam)
        print("We finished building the model")
        return model, A, S 

目標網路

有一個眾所周知的事實,在很多環境(包括TORCS)下,直接利用神經網路來實現Q值函式被證明是不穩定的。Deepmind團隊提出了該問題的解決方法——使用一個目標網路,在那裡我們分別建立了演員和評論家網路的副本,用來計算目標值。這些目標網路的權重通過 讓它們自己慢慢跟蹤學習過的網路 來更新:
\theta^{'} \leftarrow \tau \theta + (1 - \tau) \theta^{'}​​​​


\tauτ << 1。這意味著目標值被限制為慢慢地改變,大大地提高了學習的穩定性。
在Keras中實現目標網路時非常簡單的:
    def target_train(self):
        actor_weights = self.model.get_weights()
        actor_target_weights = self.target_model.get_weights()
        for i in xrange(len(actor_weights)):
            actor_target_weights[i] = self.TAU * actor_weights[i] + (1 - self.TAU)* actor_target_weights[i]
        self.target_model.set_weights(actor_target_weights)

主要程式碼

在搭建完神經網路後,我們開始探索ddpg.py主程式碼檔案。
它主要做了三件事:

  1. 接收陣列形式的感測器輸入
  2. 感測器輸入將被饋入我們的神經網路,然後網路會輸出3個實數(轉向,加速和制動的值)
  3. 網路將被訓練很多次,通過DDPG(深度確定性策略梯度演算法)來最大化未來預期回報。

感測器輸入

名稱範圍 (單位)描述
ob.angle[-π,+π] (rad)汽車方向和道路軸方向之間的夾角
ob.track(0, 200) (m)19個測距儀感測器組成的向量,每個感測器返回200米範圍內的車和道路邊緣的距離
ob.trackPos(-oo, +oo)車和道路軸之間的距離,這個值用道路寬度歸一化了:0表示車在中軸上,大於1或小於-1表示車已經跑出道路了
ob.speedX(-oo, +oo) (km/h)沿車縱向軸線的車速度(good velocity)
ob.speedY(-oo, +oo) (km/h)沿車橫向軸線的車速度
ob.speedZ(-oo, +oo) (km/h)沿車的Z-軸線的車速度
ob.wheelSpinVel(0,+oo) (rad/s)4個感測器組成的向量,表示車輪的旋轉速度
ob.rpm(0,+oo) (rpm)汽車發動機的每分鐘轉速

請注意,對於某些值我們歸一化後再饋入神經網路,並且有些感測器輸入並沒有暴露在gym_torcs中。高階使用者需要修改gym_torcs.py來改變引數。(檢視函式make_observaton()

策略選擇

現在我們可以使用上面的輸入來饋入神經網路。程式碼很簡單:

    for j in range(max_steps):
        a_t = actor.model.predict(s_t.reshape(1, s_t.shape[0]))
        ob, r_t, done, info = env.step(a_t[0])

然而,我們馬上遇到兩個問題。首先,我們如何確定獎勵?其次,我們如何在連續的動作空間探索?

獎勵設計

在原始論文中,他們使用的獎勵函式,等於投射到道路軸向的汽車速度,即V​x*​​cos(θ),如圖:


但是,我發現訓練正如原始論文中說的那樣並不是很穩定。有些時候可以學到合理的策略併成功完成任務,有些時候則不然,並不能習得明智的策略。
我相信原因是,在原始的策略中,AI會嘗試拼命踩油門油來獲得最大的獎勵,然後它會撞上路邊,這輪非常迅速地結束。因此,神經網路陷入一個非常差的區域性最小中。新提出的獎勵函式如下:
R_t = V_x cos(\theta) - V_y sin(\theta) - V_x \mid trackPos \mid

簡單說來,我們想要最大化軸向速度(第一項),最小化橫向速度(第二項),並且我們懲罰AI如果它持續非常偏離道路的中心(第三項)。
這個新的獎勵函式大幅提高了穩定性,降低了TORCS學習時間。

探索演算法的設計

另一個問題是在連續空間中如何設計一個正確的探索演算法。在上一篇文章中,我們使用了ε貪婪策略,即在某些時間片,我們嘗試一個隨機的動作。但是這個方法在TORCS中並不有效,因為我們有3個動作(轉向,加速,制動)。如果我只是從均勻分佈的動作中隨機選取,會產生一些無聊的組合(例如:制動的值大於加速的值,車子根本就不會動)。所以,我們使用奧恩斯坦 - 烏倫貝克(Ornstein-Uhlenbeck)過程新增噪聲來做探索。

Ornstein-Uhlenbeck處理

簡單說來,它就是具有均值迴歸特性的隨機過程。
dx_t = \theta (\mu - x_t)dt + \sigma dW_t


這裡,θ反應變量回歸均值有多快。μ代表平衡或均值。σ是該過程的波動程度。有趣的事,奧恩斯坦 - 烏倫貝克過程是一種很常見的方法,用來隨機模擬利率,外匯和大宗商品價格。(也是金融定量面試的常見問題)。下表展示了在程式碼中使用的建議值。
Actionθμσ
steering0.60.00.30
acceleration1.0[0.3-0.6]0.10
brake1.0-0.10.05

基本上,最重要的引數是加速度μ,你想要讓汽車有一定的初始速度,而不要陷入區域性最小(此時汽車一直踩剎車,不再踩油門)。你可以隨意更改引數來實驗AI在不同組合下的行為。奧恩斯坦的 - 烏倫貝克過程的程式碼儲存在OU.py中。
AI如果使用合理的探索策略和修訂的獎勵函式,它能在一個簡單的賽道上在200回合左右學習到一個合理的策略。

經驗回放

類似於深Q小鳥,我們也使用了經驗回放來儲存所有的階段(s, a, r, s')在一個回放儲存器中。當訓練神經網路時,從其中隨機小批量抽取階段情景,而不是使用最近的,這將大大提高系統的穩定性。

        buff.add(s_t, a_t[0], r_t, s_t1, done)
        # 從儲存回放器中隨機小批量抽取N個變換階段 (si, ai, ri, si+1)
        batch = buff.getBatch(BATCH_SIZE)
        states = np.asarray([e[0] for e in batch])
        actions = np.asarray([e[1] for e in batch])
        rewards = np.asarray([e[2] for e in batch])
        new_states = np.asarray([e[3] for e in batch])
        dones = np.asarray([e[4] for e in batch])
        y_t = np.asarray([e[1] for e in batch])

        target_q_values = critic.target_model.predict([new_states, actor.target_model.predict(new_states)])    #Still using tf
       
        for k in range(len(batch)):
            if dones[k]:
                y_t[k] = rewards[k]
            else:
                y_t[k] = rewards[k] + GAMMA*target_q_values[k]

請注意,當計算了target_q_values時我們使用的是目標網路的輸出,而不是模型自身。使用緩變的目標網路將減少Q值估測的振盪,從而大幅提高學習的穩定性。

訓練

神經網路的實際訓練非常簡單,只包含了6行程式碼:

        loss += critic.model.train_on_batch([states,actions], y_t) 
        a_for_grad = actor.model.predict(states)
        grads = critic.gradients(states, a_for_grad)
        actor.train(states, grads)
        actor.target_train()
        critic.target_train()

首先,我們最小化損失函式來更新評論家。
L = \frac{1}{N} \displaystyle\sum_{i} (y_i - Q(s_i,a_i | \theta^{Q}))^{2}


然後演員策略使用一定樣本的策略梯度來更新
\nabla_\theta J = \frac{\partial Q^{\theta}(s,a)}{\partial a}\frac{\partial a}{\partial \theta}

回想一下,a是確定性策略:a=μ(s∣θ)
因此,它能被寫作:
\nabla_\theta J = \frac{\partial Q^{\theta}(s,a)}{\partial a}\frac{\partial \mu(s|\theta)}{\partial \theta}

最後兩行程式碼更新了目標網路
\theta^{Q^{'}} \leftarrow \tau \theta^{Q} + (1 - \tau) \theta^{Q^{'}} \theta^{\mu^{'}} \leftarrow \tau \theta^{\mu} + (1 - \tau) \theta^{\mu^{'}}

結果

為了測試策略,選擇一個名為Aalborg的稍微困難的賽道,如下圖:

Aalborg
神經網路被訓練了2000個回合,並且令奧恩斯坦 - 烏倫貝克過程在100000幀中線性衰變。(即沒有更多的開發在100000幀後被應用)。然後測試一個新的賽道(3倍長)來驗證我們的神經網路。在其它賽道上測試是很重要的,這可以確認AI是否只是簡單地記憶住了賽道(過擬合),而非學習到通用的策略。
Alpine
測試結果視訊,賽道:AalborgAlpine
結果還不錯,但是還不理想,因為它還沒太學會使用剎車。

學習如何剎車

事實證明,要求AI學會如何剎車比轉彎和加速難多了。原因在於當剎車的時候車速降低,因此,獎勵也會下降,AI根本就不會熱心於踩剎車。另外, 如果允許AI在勘探階段同時踩剎車和加速,AI會經常急剎,我們會陷入糟糕的區域性最小解(汽車不動,不會受到任何獎勵)。
所以如何去解決這個問題呢?不要急剎車,而是試著感覺剎車。我們在TORCS中新增隨機剎車的機制:在勘探階段,10%的時間剎車(感覺剎車),90%的時間不剎車。因為只在10%的時間裡剎車,汽車會有一定的速度,因此它不會陷入區域性最小(汽車不動),而同時,它又能學習到如何去剎車。
“隨機剎車”使得AI在直道上加速很快,在快拐彎時適當地剎車。這樣的行為更接近人類的做法。

總結和進一步的工作

我們成功地使用 Keras和DDPG來玩賽車遊戲。儘管DDPG能學習到一個合理的策略,但和人學會開車的複雜機制還是有很大區別的,而且如果是開飛機這種有更多動作組合的問題,事情會複雜得多。
不過,這個演算法還是相當給力的,因為我們有了一個對於連續控制的無模型演算法,這對於機器人是很有意義的。