使用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可能不用管。
程式碼修改 - 然後
cd
進gym_torcs
下vtorcs-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 emsg
,unicode()
改成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個連續的動作。
- 轉方向盤。是一個單元的輸出層,使用
tanh
啟用函式(輸出-1意味著最大右轉,+1表示最大左轉) - 加速。是一個單元的輸出層,使用
sigmoid
啟用函式(輸出0代表不加速,1表示全加速)。 - 剎車。是一個單元的輸出層,也使用
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主程式碼檔案。
它主要做了三件事:
- 接收陣列形式的感測器輸入
- 感測器輸入將被饋入我們的神經網路,然後網路會輸出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])
然而,我們馬上遇到兩個問題。首先,我們如何確定獎勵?其次,我們如何在連續的動作空間探索?
獎勵設計
在原始論文中,他們使用的獎勵函式,等於投射到道路軸向的汽車速度,即Vx*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 | θ | μ | σ |
---|---|---|---|
steering | 0.6 | 0.0 | 0.30 |
acceleration | 1.0 | [0.3-0.6] | 0.10 |
brake | 1.0 | -0.1 | 0.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的稍微困難的賽道,如下圖:
神經網路被訓練了2000個回合,並且令奧恩斯坦 - 烏倫貝克過程在100000幀中線性衰變。(即沒有更多的開發在100000幀後被應用)。然後測試一個新的賽道(3倍長)來驗證我們的神經網路。在其它賽道上測試是很重要的,這可以確認AI是否只是簡單地記憶住了賽道(過擬合),而非學習到通用的策略。
Alpine
測試結果視訊,賽道:Aalborg 與 Alpine。
結果還不錯,但是還不理想,因為它還沒太學會使用剎車。
學習如何剎車
事實證明,要求AI學會如何剎車比轉彎和加速難多了。原因在於當剎車的時候車速降低,因此,獎勵也會下降,AI根本就不會熱心於踩剎車。另外, 如果允許AI在勘探階段同時踩剎車和加速,AI會經常急剎,我們會陷入糟糕的區域性最小解(汽車不動,不會受到任何獎勵)。
所以如何去解決這個問題呢?不要急剎車,而是試著感覺剎車。我們在TORCS中新增隨機剎車的機制:在勘探階段,10%的時間剎車(感覺剎車),90%的時間不剎車。因為只在10%的時間裡剎車,汽車會有一定的速度,因此它不會陷入區域性最小(汽車不動),而同時,它又能學習到如何去剎車。
“隨機剎車”使得AI在直道上加速很快,在快拐彎時適當地剎車。這樣的行為更接近人類的做法。
總結和進一步的工作
我們成功地使用 Keras和DDPG來玩賽車遊戲。儘管DDPG能學習到一個合理的策略,但和人學會開車的複雜機制還是有很大區別的,而且如果是開飛機這種有更多動作組合的問題,事情會複雜得多。
不過,這個演算法還是相當給力的,因為我們有了一個對於連續控制的無模型演算法,這對於機器人是很有意義的。