本文先給出 Q 學習(Q-learning)的基本原理,然後再具體從 DQN 網路的超引數、智慧體、模型和訓練等方面詳細解釋了深度 Q 網路,最後,文章給出了該教程的全部程式碼。
在之前的 Keras/OpenAI 教程中,我們討論了一個將深度學習應用於強化學習環境的基礎案例,它的效果非常顯著。想象作為訓練資料的完全隨機序列(series)。任何兩個序列都不可能高度彼此重複,因為這些都是隨機產生的。然而,成功的試驗之間存在相同的關鍵特徵,例如在 CartPole 遊戲中,當杆往右靠時需要將車向右推,反之亦然。因此,透過在所有這些試驗資料上訓練我們的神經網路,我們提取了有助於成功的共同模式(pattern),並能夠平滑導致其產生獨立故障的細節。
話雖如此,我們認為這次的環境比上次要困難得多,即遊戲:MountainCar。
更復雜的環境
即使看上去我們應該能夠應用與上週相同的技術,但是有一個關鍵特徵使它變得不可能:我們無法生成訓練資料。與簡單的 CartPole 例子不同,採取隨機移動通常只會導致實驗的結果很差(谷底)。也就是說,我們的實驗結果最後都是相同的-200。這用作訓練資料幾乎沒有用。想象一下,如果你無論在考試中做出什麼答案,你都會得到 0%,那麼你將如何從這些經驗中學習?
「MountainCar-v0」環境的隨機輸入不會產生任何對於訓練有用的輸出。
由於這些問題,我們必須找出一種能逐步改進以前實驗的方法。為此,我們使用強化學習最基本的方法:Q-learning!
DQN 的理論背景
Q-learning 的本質是建立一個「虛擬表格」,這個表格包含了當前環境狀態下每個可能的動作能得到多少獎勵。下面來詳細說明:
網路可以想象為內生有電子表格的網路,該表格含有當前環境狀態下可能採取的每個可能的動作的值。
「虛擬表格」是什麼意思?想像一下,對於輸入空間的每個可能的動作,你都可以為每個可能採取的動作賦予一個分數。如果這可行,那麼你可以很容易地「打敗」環境:只需選擇具有最高分數的動作!但是需要注意 2 點:首先,這個分數通常被稱為「Q-分數」,此演算法也由此命名。第二,與任何其它得分一樣,這些 Q-分數在其模擬的情境外沒有任何意義。也就是說,它們沒有確定的意義,但這沒關係,因為我們只需要做比較。
為什麼對於每個輸入我們都需要一個虛擬表格?難道沒有統一的表格嗎?原因是這樣做不和邏輯:這與在谷底談採取什麼動作是最好的,及在向左傾斜時的最高點討論採取什麼動作是最好的是一樣的道理。
現在,我們的主要問題(為每個輸入建立虛擬表格)是不可能的:我們有一個連續的(無限)輸入空間!我們可以透過離散化輸入空間來解決這個問題,但是對於本問題來說,這似乎是一個非常棘手的解決方案,並且在將來我們會一再遇到。那麼,我們如何解決呢?那就是透過將神經網路應用於這種情況:這就是 DQN 中 D 的來歷!
DQN agent
現在,我們現在已經將問題聚焦到:找到一種在給定當前狀態下為不同動作賦值 Q-分數的方法。這是使用任何神經網路時遇到的非常自然的第一個問題的答案:我們模型的輸入和輸出是什麼?本模型中你需要了解的數學方程是以下等式(不用擔心,我們會在下面講解):
如上所述,Q 代表了給定當前狀態(s)和採取的動作(a)時我們模型估計的價值。然而,目標是確定一個狀態價值的總和。那是什麼意思?即從該位置獲得的即時獎勵和將來會獲得的預期獎勵之和。也就是說,我們要考慮一個事實,即一個狀態的價值往往不僅反映了它的直接收益,而且還反映了它的未來收益。在任何情況下,我們會將未來的獎勵折現,因為對於同樣是收到$100 的兩種情況(一種為將來,一種為現在),我會永遠選擇現在的交易,因為未來是會變化的。γ因子反映了此狀態預期未來收益的貶值。
這就是我們需要的所有數學!下面是實際程式碼的演示!
DQN agent 實現
深度 Q 網路為持續學習(continuous learning),這意味著不是簡單地累積一批實驗/訓練資料並將其傳入模型。相反,我們透過之前執行的實驗建立訓練資料,並且直接將執行後建立的資料饋送如模型。如果現在感到好像有些模糊,別擔心,該看看程式碼了。程式碼主要在定義一個 DQN 類,其中將實現所有的演算法邏輯,並且我們將定義一組簡單的函式來進行實際的訓練。
DQN 超引數
首先,我們將討論一些與 DQN 相關的引數。它們大多數是實現主流神經網路的標準引數:
class DQN:
def __init__(self, env):
self.env = env
self.memory = deque(maxlen=2000)
self.gamma = 0.95
self.epsilon = 1.0
self.epsilon_min = 0.01
self.epsilon_decay = 0.995
self.learning_rate = 0.01
讓我們來一步一步地講解這些超引數。第一個是環境(env),這僅僅是為了在建立模型時便於引用矩陣的形狀。「記憶(memory)」是 DQN 的關鍵組成部分:如前所述,我們不斷透過實驗訓練模型。然而與直接訓練實驗的資料不同,我們將它們先新增到記憶體中並隨機抽樣。為什麼這樣做呢,難道僅僅將最後 x 個實驗資料作為樣本進行訓練不好嗎?原因有點微妙。設想我們只使用最近的實驗資料進行訓練:在這種情況下,我們的結果只會學習其最近的動作,這可能與未來的預測沒有直接的關係。特別地,在本環境下,如果我們在斜坡右側向下移動,使用最近的實驗資料進行訓練將需要在斜坡右側向上移動的資料上進行訓練。但是,這與在斜坡左側的情景需決定採取的動作無關。所以,透過抽取隨機樣本,將保證不會偏離訓練集,而是理想地學習我們將遇到的所有環境。
我們現在來討論模型的超引數:gamma、epsilon 以及 epsilon 衰減和學習速率。第一個是前面方程中討論的未來獎勵的折現因子(<1),最後一個是標準學習速率引數,我們不在這裡討論。第二個是 RL 的一個有趣方面,值得一談。在任何一種學習經驗中,我們總是在探索與利用之間做出選擇。這不僅限於電腦科學或學術界:我們每天都在做這件事!
考慮你家附近的飯店。你最後一次嘗試新飯店是什麼時候?可能很久以前。這對應於你從探索到利用的轉變:與嘗試找到新的更好的機會不同,你根據自己以往的經驗找到最好的解決方案,從而最大化效用。對比當你剛搬家時:當時你不知道什麼飯店是好的,所以被誘惑去探索新選擇。換句話說,這時存在明確的學習趨勢:當你不瞭解它們時,探索所有的選擇,一旦你對其中的一些建立了意見,就逐漸轉向利用。以同樣的方式,我們希望我們的模型能夠捕捉這種自然的學習模型,而 epsilon 扮演著這個角色。
Epsilon 表示我們將致力於探索的時間的一小部分。也就是說,實驗的分數 self.epsilon,我們將僅僅採取隨機動作,而不是我們預測在這種情況下最好的動作。如上所述,我們希望在開始時形成穩定評估之前更經常地採取隨機動作:因此開始時初始化ε接近 1.0,並在每一個連續的時間步長中以小於 1 的速率衰減它。
DQN 模型
在上面的 DQN 的初始化中排除了一個關鍵環節:用於預測的實際模型!在原來的 Keras RL 教程中,我們直接給出數字向量形式的輸入和輸出。因此,除了全連線層之外,不需要在網路中使用更復雜的層。具體來說,我們將模型定義為:
def create_model(self):
model = Sequential()
state_shape = self.env.observation_space.shape
model.add(Dense(24, input_dim=state_shape[0],
activation="relu"))
model.add(Dense(48, activation="relu"))
model.add(Dense(24, activation="relu"))
model.add(Dense(self.env.action_space.n))
model.compile(loss="mean_squared_error",
optimizer=Adam(lr=self.learning_rate))
return model
並用它來定義模型和目標模型(如下所述):
def __init__(self, env):
self.env = env
self.memory = deque(maxlen=2000)
self.gamma = 0.95
self.epsilon = 1.0
self.epsilon_min = 0.01
self.epsilon_decay = 0.995
self.learning_rate = 0.01
self.tau = .05
self.model = self.create_model()
# "hack" implemented by DeepMind to improve convergence
self.target_model = self.create_model()
事實上,有兩個單獨的模型,一個用於做預測,一個用於跟蹤「目標值」,這是反直覺的。明確地說,模型(self.model)的作用是對要採取的動作進行實際預測,目標模型(self.target_model)的作用是跟蹤我們想要模型採取的動作。
為什麼不用一個模型做這兩件事呢?畢竟,如果預測要採取的動作,那不會間接地確定我們想要模型採取的模式嗎?這實際上是 DeepMind 發明的深度學習的「不可思議的技巧」之一,它用於在 DQN 演算法中獲得收斂。如果使用單個模型,它可以(通常會)在簡單的環境(如 CartPole)中收斂。但是,在這些更為複雜的環境中並不收斂的原因在於我們如何對模型進行訓練:如前所述,我們正在對模型進行「即時」訓練。
因此,在每個時間步長進行訓練模型,如果我們使用單個網路,實際上也將在每個時間步長時改變「目標」。想想這將多麼混亂!那就如同,開始老師告訴你要完成教科書中的第 6 頁,當你完成了一半時,她把它改成了第 9 頁,當你完成一半的時候,她告訴你做第 21 頁!因此,由於缺乏明確方向以利用最佳化器,即梯度變化太快難以穩定收斂,將導致收斂不足。所以,作為代償,我們有一個變化更慢的網路以跟蹤我們的最終目標,和一個最終實現這些目標的網路。
DQN 訓練
訓練涉及三個主要步驟:記憶、學習和重新定位目標。第一步基本上只是隨著實驗的進行向記憶新增資料:
def remember(self, state, action, reward, new_state, done):
self.memory.append([state, action, reward, new_state, done])
這裡沒有太多的注意事項,除了我們必須儲存「done」階段,以瞭解我們以後如何更新獎勵函式。轉到 DQN 主體的訓練函式。這是使用儲存記憶的地方,並積極從我們過去看到的內容中學習。首先,從整個儲存記憶中抽出一個樣本。我們認為每個樣本是不同的。正如我們在前面的等式中看到的,我們要將 Q-函式更新為當前獎勵之和與預期未來獎勵的總和(貶值為 gamma)。在實驗結束時,將不再有未來的獎勵,所以該狀態的價值為此時我們收到的獎勵之和。然而,在非終止狀態,如果我們能夠採取任何可能的動作,將會得到的最大的獎勵是什麼?我們得到:
def replay(self):
batch_size = 32
if len(self.memory) < batch_size:
return
samples = random.sample(self.memory, batch_size)
for sample in samples:
state, action, reward, new_state, done = sample
target = self.target_model.predict(state)
if done:
target[0][action] = reward
else:
Q_future = max(
self.target_model.predict(new_state)[0])
target[0][action] = reward + Q_future * self.gamma
self.model.fit(state, target, epochs=1, verbose=0)
最後,我們必須重新定位目標,我們只需將主模型的權重複制到目標模型中。然而,與主模型訓練的方法不同,目標模型更新較慢:
def target_train(self):
weights = self.model.get_weights()
target_weights = self.target_model.get_weights()
for i in range(len(target_weights)):
target_weights[i] = weights[i]
self.target_model.set_weights(target_weights)
DQN 動作
最後一步是讓 DQN 實際執行希望的動作,在給定的 epsilon 引數基礎上,執行的動作在隨機動作與基於過去訓練的預測動作之間選擇,如下所示:
def act(self, state):
self.epsilon *= self.epsilon_decay
self.epsilon = max(self.epsilon_min, self.epsilon)
if np.random.random() < self.epsilon:
return self.env.action_space.sample()
return np.argmax(self.model.predict(state)[0])
訓練 agent
現在訓練我們開發的複雜的 agent。將其例項化,傳入經驗資料,訓練 agent,並更新目標網路:
def main():
env = gym.make("MountainCar-v0")
gamma = 0.9
epsilon = .95
trials = 100
trial_len = 500
updateTargetNetwork = 1000
dqn_agent = DQN(env=env)
steps = []
for trial in range(trials):
cur_state = env.reset().reshape(1,2)
for step in range(trial_len):
action = dqn_agent.act(cur_state)
env.render()
new_state, reward, done, _ = env.step(action)
reward = reward if not done else -20
print(reward)
new_state = new_state.reshape(1,2)
dqn_agent.remember(cur_state, action,
reward, new_state, done)
dqn_agent.replay()
dqn_agent.target_train()
cur_state = new_state
if done:
break
if step >= 199:
print("Failed to complete trial")
else:
print("Completed in {} trials".format(trial))
break
完整的程式碼
這就是使用 DQN 的「MountainCar-v0」環境的完整程式碼!
import gym
import numpy as np
import random
from keras.models import Sequential
from keras.layers import Dense, Dropout
from keras.optimizers import Adam
from collections import deque
class DQN:
def __init__(self, env):
self.env = env
self.memory = deque(maxlen=2000)
self.gamma = 0.85
self.epsilon = 1.0
self.epsilon_min = 0.01
self.epsilon_decay = 0.995
self.learning_rate = 0.005
self.tau = .125
self.model = self.create_model()
self.target_model = self.create_model()
def create_model(self):
model = Sequential()
state_shape = self.env.observation_space.shape
model.add(Dense(24, input_dim=state_shape[0], activation="relu"))
model.add(Dense(48, activation="relu"))
model.add(Dense(24, activation="relu"))
model.add(Dense(self.env.action_space.n))
model.compile(loss="mean_squared_error",
optimizer=Adam(lr=self.learning_rate))
return model
def act(self, state):
self.epsilon *= self.epsilon_decay
self.epsilon = max(self.epsilon_min, self.epsilon)
if np.random.random() < self.epsilon:
return self.env.action_space.sample()
return np.argmax(self.model.predict(state)[0])
def remember(self, state, action, reward, new_state, done):
self.memory.append([state, action, reward, new_state, done])
def replay(self):
batch_size = 32
if len(self.memory) < batch_size:
return
samples = random.sample(self.memory, batch_size)
for sample in samples:
state, action, reward, new_state, done = sample
target = self.target_model.predict(state)
if done:
target[0][action] = reward
else:
Q_future = max(self.target_model.predict(new_state)[0])
target[0][action] = reward + Q_future * self.gamma
self.model.fit(state, target, epochs=1, verbose=0)
def target_train(self):
weights = self.model.get_weights()
target_weights = self.target_model.get_weights()
for i in range(len(target_weights)):
target_weights[i] = weights[i] * self.tau + target_weights[i] * (1 - self.tau)
self.target_model.set_weights(target_weights)
def save_model(self, fn):
self.model.save(fn)
def main():
env = gym.make("MountainCar-v0")
gamma = 0.9
epsilon = .95
trials = 1000
trial_len = 500
# updateTargetNetwork = 1000
dqn_agent = DQN(env=env)
steps = []
for trial in range(trials):
cur_state = env.reset().reshape(1,2)
for step in range(trial_len):
action = dqn_agent.act(cur_state)
new_state, reward, done, _ = env.step(action)
# reward = reward if not done else -20
new_state = new_state.reshape(1,2)
dqn_agent.remember(cur_state, action, reward, new_state, done)
dqn_agent.replay() # internally iterates default (prediction) model
dqn_agent.target_train() # iterates target model
cur_state = new_state
if done:
break
if step >= 199:
print("Failed to complete in trial {}".format(trial))
if step % 10 == 0:
dqn_agent.save_model("trial-{}.model".format(trial))
else:
print("Completed in {} trials".format(trial))
dqn_agent.save_model("success.model")
break
if __name__ == "__main__":
main()
原文連結:https://medium.com/towards-data-science/reinforcement-learning-w-keras-openai-dqns-1eed3a5338c