Keras+OpenAI強(qiáng)化學(xué)習(xí)實(shí)踐:深度Q網(wǎng)絡(luò)
在之前的 Keras/OpenAI 教程中,我們討論了一個(gè)將深度學(xué)習(xí)應(yīng)用于強(qiáng)化學(xué)習(xí)環(huán)境的基礎(chǔ)案例,它的效果非常顯著。想象作為訓(xùn)練數(shù)據(jù)的完全隨機(jī)序列(series)。任何兩個(gè)序列都不可能高度彼此重復(fù),因?yàn)檫@些都是隨機(jī)產(chǎn)生的。然而,成功的試驗(yàn)之間存在相同的關(guān)鍵特征,例如在 CartPole 游戲中,當(dāng)桿往右靠時(shí)需要將車(chē)向右推,反之亦然。因此,通過(guò)在所有這些試驗(yàn)數(shù)據(jù)上訓(xùn)練我們的神經(jīng)網(wǎng)絡(luò),我們提取了有助于成功的共同模式(pattern),并能夠平滑導(dǎo)致其產(chǎn)生獨(dú)立故障的細(xì)節(jié)。
話雖如此,我們認(rèn)為這次的環(huán)境比上次要困難得多,即游戲:MountainCar。
一、更復(fù)雜的環(huán)境
即使看上去我們應(yīng)該能夠應(yīng)用與上周相同的技術(shù),但是有一個(gè)關(guān)鍵特征使它變得不可能:我們無(wú)法生成訓(xùn)練數(shù)據(jù)。與簡(jiǎn)單的 CartPole 例子不同,采取隨機(jī)移動(dòng)通常只會(huì)導(dǎo)致實(shí)驗(yàn)的結(jié)果很差(谷底)。也就是說(shuō),我們的實(shí)驗(yàn)結(jié)果***都是相同的-200。這用作訓(xùn)練數(shù)據(jù)幾乎沒(méi)有用。想象一下,如果你無(wú)論在考試中做出什么答案,你都會(huì)得到 0%,那么你將如何從這些經(jīng)驗(yàn)中學(xué)習(xí)?
「MountainCar-v0」環(huán)境的隨機(jī)輸入不會(huì)產(chǎn)生任何對(duì)于訓(xùn)練有用的輸出。
由于這些問(wèn)題,我們必須找出一種能逐步改進(jìn)以前實(shí)驗(yàn)的方法。為此,我們使用強(qiáng)化學(xué)習(xí)最基本的方法:Q-learning!
二、DQN 的理論背景
Q-learning 的本質(zhì)是創(chuàng)建一個(gè)「虛擬表格」,這個(gè)表格包含了當(dāng)前環(huán)境狀態(tài)下每個(gè)可能的動(dòng)作能得到多少獎(jiǎng)勵(lì)。下面來(lái)詳細(xì)說(shuō)明:
網(wǎng)絡(luò)可以想象為內(nèi)生有電子表格的網(wǎng)絡(luò),該表格含有當(dāng)前環(huán)境狀態(tài)下可能采取的每個(gè)可能的動(dòng)作的值。
「虛擬表格」是什么意思?想像一下,對(duì)于輸入空間的每個(gè)可能的動(dòng)作,你都可以為每個(gè)可能采取的動(dòng)作賦予一個(gè)分?jǐn)?shù)。如果這可行,那么你可以很容易地「打敗」環(huán)境:只需選擇具有***分?jǐn)?shù)的動(dòng)作!但是需要注意 2 點(diǎn):首先,這個(gè)分?jǐn)?shù)通常被稱(chēng)為「Q-分?jǐn)?shù)」,此算法也由此命名。第二,與任何其它得分一樣,這些 Q-分?jǐn)?shù)在其模擬的情境外沒(méi)有任何意義。也就是說(shuō),它們沒(méi)有確定的意義,但這沒(méi)關(guān)系,因?yàn)槲覀冎恍枰霰容^。
為什么對(duì)于每個(gè)輸入我們都需要一個(gè)虛擬表格?難道沒(méi)有統(tǒng)一的表格嗎?原因是這樣做不和邏輯:這與在谷底談采取什么動(dòng)作是***的,及在向左傾斜時(shí)的***點(diǎn)討論采取什么動(dòng)作是***的是一樣的道理。
現(xiàn)在,我們的主要問(wèn)題(為每個(gè)輸入建立虛擬表格)是不可能的:我們有一個(gè)連續(xù)的(***)輸入空間!我們可以通過(guò)離散化輸入空間來(lái)解決這個(gè)問(wèn)題,但是對(duì)于本問(wèn)題來(lái)說(shuō),這似乎是一個(gè)非常棘手的解決方案,并且在將來(lái)我們會(huì)一再遇到。那么,我們?nèi)绾谓鉀Q呢?那就是通過(guò)將神經(jīng)網(wǎng)絡(luò)應(yīng)用于這種情況:這就是 DQN 中 D 的來(lái)歷!
三、DQN agent
現(xiàn)在,我們現(xiàn)在已經(jīng)將問(wèn)題聚焦到:找到一種在給定當(dāng)前狀態(tài)下為不同動(dòng)作賦值 Q-分?jǐn)?shù)的方法。這是使用任何神經(jīng)網(wǎng)絡(luò)時(shí)遇到的非常自然的***個(gè)問(wèn)題的答案:我們模型的輸入和輸出是什么?本模型中你需要了解的數(shù)學(xué)方程是以下等式(不用擔(dān)心,我們會(huì)在下面講解):
如上所述,Q 代表了給定當(dāng)前狀態(tài)(s)和采取的動(dòng)作(a)時(shí)我們模型估計(jì)的價(jià)值。然而,目標(biāo)是確定一個(gè)狀態(tài)價(jià)值的總和。那是什么意思?即從該位置獲得的即時(shí)獎(jiǎng)勵(lì)和將來(lái)會(huì)獲得的預(yù)期獎(jiǎng)勵(lì)之和。也就是說(shuō),我們要考慮一個(gè)事實(shí),即一個(gè)狀態(tài)的價(jià)值往往不僅反映了它的直接收益,而且還反映了它的未來(lái)收益。在任何情況下,我們會(huì)將未來(lái)的獎(jiǎng)勵(lì)折現(xiàn),因?yàn)閷?duì)于同樣是收到$100 的兩種情況(一種為將來(lái),一種為現(xiàn)在),我會(huì)永遠(yuǎn)選擇現(xiàn)在的交易,因?yàn)槲磥?lái)是會(huì)變化的。γ因子反映了此狀態(tài)預(yù)期未來(lái)收益的貶值。
這就是我們需要的所有數(shù)學(xué)!下面是實(shí)際代碼的演示!
四、DQN agent 實(shí)現(xiàn)
深度 Q 網(wǎng)絡(luò)為持續(xù)學(xué)習(xí)(continuous learning),這意味著不是簡(jiǎn)單地累積一批實(shí)驗(yàn)/訓(xùn)練數(shù)據(jù)并將其傳入模型。相反,我們通過(guò)之前運(yùn)行的實(shí)驗(yàn)創(chuàng)建訓(xùn)練數(shù)據(jù),并且直接將運(yùn)行后創(chuàng)建的數(shù)據(jù)饋送如模型。如果現(xiàn)在感到好像有些模糊,別擔(dān)心,該看看代碼了。代碼主要在定義一個(gè) DQN 類(lèi),其中將實(shí)現(xiàn)所有的算法邏輯,并且我們將定義一組簡(jiǎn)單的函數(shù)來(lái)進(jìn)行實(shí)際的訓(xùn)練。
1. DQN 超參數(shù)
首先,我們將討論一些與 DQN 相關(guān)的參數(shù)。它們大多數(shù)是實(shí)現(xiàn)主流神經(jīng)網(wǎng)絡(luò)的標(biāo)準(zhǔn)參數(shù):
- 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
讓我們來(lái)一步一步地講解這些超參數(shù)。***個(gè)是環(huán)境(env),這僅僅是為了在建立模型時(shí)便于引用矩陣的形狀?!赣洃?memory)」是 DQN 的關(guān)鍵組成部分:如前所述,我們不斷通過(guò)實(shí)驗(yàn)訓(xùn)練模型。然而與直接訓(xùn)練實(shí)驗(yàn)的數(shù)據(jù)不同,我們將它們先添加到內(nèi)存中并隨機(jī)抽樣。為什么這樣做呢,難道僅僅將*** x 個(gè)實(shí)驗(yàn)數(shù)據(jù)作為樣本進(jìn)行訓(xùn)練不好嗎?原因有點(diǎn)微妙。設(shè)想我們只使用最近的實(shí)驗(yàn)數(shù)據(jù)進(jìn)行訓(xùn)練:在這種情況下,我們的結(jié)果只會(huì)學(xué)習(xí)其最近的動(dòng)作,這可能與未來(lái)的預(yù)測(cè)沒(méi)有直接的關(guān)系。特別地,在本環(huán)境下,如果我們?cè)谛逼掠覀?cè)向下移動(dòng),使用最近的實(shí)驗(yàn)數(shù)據(jù)進(jìn)行訓(xùn)練將需要在斜坡右側(cè)向上移動(dòng)的數(shù)據(jù)上進(jìn)行訓(xùn)練。但是,這與在斜坡左側(cè)的情景需決定采取的動(dòng)作無(wú)關(guān)。所以,通過(guò)抽取隨機(jī)樣本,將保證不會(huì)偏離訓(xùn)練集,而是理想地學(xué)習(xí)我們將遇到的所有環(huán)境。
我們現(xiàn)在來(lái)討論模型的超參數(shù):gamma、epsilon 以及 epsilon 衰減和學(xué)習(xí)速率。***個(gè)是前面方程中討論的未來(lái)獎(jiǎng)勵(lì)的折現(xiàn)因子(<1),***一個(gè)是標(biāo)準(zhǔn)學(xué)習(xí)速率參數(shù),我們不在這里討論。第二個(gè)是 RL 的一個(gè)有趣方面,值得一談。在任何一種學(xué)習(xí)經(jīng)驗(yàn)中,我們總是在探索與利用之間做出選擇。這不僅限于計(jì)算機(jī)科學(xué)或?qū)W術(shù)界:我們每天都在做這件事!
考慮你家附近的飯店。你***一次嘗試新飯店是什么時(shí)候?可能很久以前。這對(duì)應(yīng)于你從探索到利用的轉(zhuǎn)變:與嘗試找到新的更好的機(jī)會(huì)不同,你根據(jù)自己以往的經(jīng)驗(yàn)找到***的解決方案,從而***化效用。對(duì)比當(dāng)你剛搬家時(shí):當(dāng)時(shí)你不知道什么飯店是好的,所以被誘惑去探索新選擇。換句話說(shuō),這時(shí)存在明確的學(xué)習(xí)趨勢(shì):當(dāng)你不了解它們時(shí),探索所有的選擇,一旦你對(duì)其中的一些建立了意見(jiàn),就逐漸轉(zhuǎn)向利用。以同樣的方式,我們希望我們的模型能夠捕捉這種自然的學(xué)習(xí)模型,而 epsilon 扮演著這個(gè)角色。
Epsilon 表示我們將致力于探索的時(shí)間的一小部分。也就是說(shuō),實(shí)驗(yàn)的分?jǐn)?shù) self.epsilon,我們將僅僅采取隨機(jī)動(dòng)作,而不是我們預(yù)測(cè)在這種情況下***的動(dòng)作。如上所述,我們希望在開(kāi)始時(shí)形成穩(wěn)定評(píng)估之前更經(jīng)常地采取隨機(jī)動(dòng)作:因此開(kāi)始時(shí)初始化ε接近 1.0,并在每一個(gè)連續(xù)的時(shí)間步長(zhǎng)中以小于 1 的速率衰減它。
2. DQN 模型
在上面的 DQN 的初始化中排除了一個(gè)關(guān)鍵環(huán)節(jié):用于預(yù)測(cè)的實(shí)際模型!在原來(lái)的 Keras RL 教程中,我們直接給出數(shù)字向量形式的輸入和輸出。因此,除了全連接層之外,不需要在網(wǎng)絡(luò)中使用更復(fù)雜的層。具體來(lái)說(shuō),我們將模型定義為:
- 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
并用它來(lái)定義模型和目標(biāo)模型(如下所述):
- 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
- selfself.model = self.create_model()
- # "hack" implemented by DeepMind to improve convergence
- selfself.target_model = self.create_model()
事實(shí)上,有兩個(gè)單獨(dú)的模型,一個(gè)用于做預(yù)測(cè),一個(gè)用于跟蹤「目標(biāo)值」,這是反直覺(jué)的。明確地說(shuō),模型(self.model)的作用是對(duì)要采取的動(dòng)作進(jìn)行實(shí)際預(yù)測(cè),目標(biāo)模型(self.target_model)的作用是跟蹤我們想要模型采取的動(dòng)作。
為什么不用一個(gè)模型做這兩件事呢?畢竟,如果預(yù)測(cè)要采取的動(dòng)作,那不會(huì)間接地確定我們想要模型采取的模式嗎?這實(shí)際上是 DeepMind 發(fā)明的深度學(xué)習(xí)的「不可思議的技巧」之一,它用于在 DQN 算法中獲得收斂。如果使用單個(gè)模型,它可以(通常會(huì))在簡(jiǎn)單的環(huán)境(如 CartPole)中收斂。但是,在這些更為復(fù)雜的環(huán)境中并不收斂的原因在于我們?nèi)绾螌?duì)模型進(jìn)行訓(xùn)練:如前所述,我們正在對(duì)模型進(jìn)行「即時(shí)」訓(xùn)練。
因此,在每個(gè)時(shí)間步長(zhǎng)進(jìn)行訓(xùn)練模型,如果我們使用單個(gè)網(wǎng)絡(luò),實(shí)際上也將在每個(gè)時(shí)間步長(zhǎng)時(shí)改變「目標(biāo)」。想想這將多么混亂!那就如同,開(kāi)始老師告訴你要完成教科書(shū)中的第 6 頁(yè),當(dāng)你完成了一半時(shí),她把它改成了第 9 頁(yè),當(dāng)你完成一半的時(shí)候,她告訴你做第 21 頁(yè)!因此,由于缺乏明確方向以利用優(yōu)化器,即梯度變化太快難以穩(wěn)定收斂,將導(dǎo)致收斂不足。所以,作為代償,我們有一個(gè)變化更慢的網(wǎng)絡(luò)以跟蹤我們的最終目標(biāo),和一個(gè)最終實(shí)現(xiàn)這些目標(biāo)的網(wǎng)絡(luò)。
3. DQN 訓(xùn)練
訓(xùn)練涉及三個(gè)主要步驟:記憶、學(xué)習(xí)和重新定位目標(biāo)。***步基本上只是隨著實(shí)驗(yàn)的進(jìn)行向記憶添加數(shù)據(jù):
- def remember(self, state, action, reward, new_state, done):
- self.memory.append([state, action, reward, new_state, done])
這里沒(méi)有太多的注意事項(xiàng),除了我們必須存儲(chǔ)「done」階段,以了解我們以后如何更新獎(jiǎng)勵(lì)函數(shù)。轉(zhuǎn)到 DQN 主體的訓(xùn)練函數(shù)。這是使用存儲(chǔ)記憶的地方,并積極從我們過(guò)去看到的內(nèi)容中學(xué)習(xí)。首先,從整個(gè)存儲(chǔ)記憶中抽出一個(gè)樣本。我們認(rèn)為每個(gè)樣本是不同的。正如我們?cè)谇懊娴牡仁街锌吹降?,我們要? Q-函數(shù)更新為當(dāng)前獎(jiǎng)勵(lì)之和與預(yù)期未來(lái)獎(jiǎng)勵(lì)的總和(貶值為 gamma)。在實(shí)驗(yàn)結(jié)束時(shí),將不再有未來(lái)的獎(jiǎng)勵(lì),所以該狀態(tài)的價(jià)值為此時(shí)我們收到的獎(jiǎng)勵(lì)之和。然而,在非終止?fàn)顟B(tài),如果我們能夠采取任何可能的動(dòng)作,將會(huì)得到的***的獎(jiǎng)勵(lì)是什么?我們得到:
- 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)
***,我們必須重新定位目標(biāo),我們只需將主模型的權(quán)重復(fù)制到目標(biāo)模型中。然而,與主模型訓(xùn)練的方法不同,目標(biāo)模型更新較慢:
- 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)
4. DQN 動(dòng)作
***一步是讓 DQN 實(shí)際執(zhí)行希望的動(dòng)作,在給定的 epsilon 參數(shù)基礎(chǔ)上,執(zhí)行的動(dòng)作在隨機(jī)動(dòng)作與基于過(guò)去訓(xùn)練的預(yù)測(cè)動(dòng)作之間選擇,如下所示:
- 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])
5. 訓(xùn)練 agent
現(xiàn)在訓(xùn)練我們開(kāi)發(fā)的復(fù)雜的 agent。將其實(shí)例化,傳入經(jīng)驗(yàn)數(shù)據(jù),訓(xùn)練 agent,并更新目標(biāo)網(wǎng)絡(luò):
- def main():
- env = gym.make("MountainCar-v0")
- gamma = 0.9
- epsilon = .95
- trials = 100
- trial_len = 500
- updateTargetNetwork = 1000
- dqn_agent = DQN(envenv=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)
- rewardreward = reward if not done else -20
- print(reward)
- new_statenew_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」環(huán)境的完整代碼!
- 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
- selfself.model = self.create_model()
- selfself.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(envenv=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)
- # rewardreward = reward if not done else -20
- new_statenew_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
【本文是51CTO專(zhuān)欄機(jī)構(gòu)“機(jī)器之心”的原創(chuàng)譯文,微信公眾號(hào)“機(jī)器之心( id: almosthuman2014)”】