深度強(qiáng)化學(xué)習(xí)入門:用TensorFlow構(gòu)建你的第一個(gè)游戲AI
去年,DeepMind 的 AlphaGo 以 4-1 的比分打敗了世界圍棋冠軍李世乭。超過(guò) 2 億的觀眾就這樣看著強(qiáng)化學(xué)習(xí)(reinforce learning)走上了世界舞臺(tái)。幾年前,DeepMind 制作了一個(gè)可以玩 Atari 游戲的機(jī)器人,引發(fā)軒然大波。此后這個(gè)公司很快被谷歌收購(gòu)。
很多研究者相信,強(qiáng)化學(xué)習(xí)是我們創(chuàng)造通用人工智能(Artificial General Intelligence)的最佳手段。這是一個(gè)令人興奮的領(lǐng)域,有著許多未解決的挑戰(zhàn)和巨大的潛能。
強(qiáng)化學(xué)習(xí)起初看似非常有挑戰(zhàn)性,但其實(shí)要入門并不困難。在這篇文章中,我們將創(chuàng)造一個(gè)基于 Keras 的簡(jiǎn)單機(jī)器人,使它能玩 Catch 游戲。
Catch 游戲
原始的 Catch 游戲界面
Catch 是一個(gè)非常簡(jiǎn)單的街機(jī)游戲,你可能在孩提時(shí)代玩過(guò)它。游戲規(guī)則如下:水果從屏幕的頂部落下,玩家必須用一個(gè)籃子抓住它們;每抓住一個(gè)水果,玩家得一分;每漏掉一個(gè)水果,玩家會(huì)被扣除一分。這里的目標(biāo)是讓電腦自己玩 Catch 游戲。不過(guò),我們不會(huì)使用這么漂亮的游戲界面。相反,我們會(huì)使用一個(gè)簡(jiǎn)單的游戲版本來(lái)簡(jiǎn)化任務(wù):
簡(jiǎn)化的 Catch 游戲界面
玩 Catch 游戲時(shí),玩家要決定三種可能的行為。玩家可以將籃子左移、右移或保持不動(dòng)。這個(gè)決定取決于游戲的當(dāng)前狀態(tài)。也就是說(shuō),取決于果子掉落的位置和籃子的位置。我們的目標(biāo)是創(chuàng)造這樣一個(gè)模型:它能在給定游戲屏幕內(nèi)容的情況下,選擇導(dǎo)致得分最高的動(dòng)作。
這個(gè)任務(wù)可以被看做一個(gè)簡(jiǎn)單的分類問(wèn)題。我們可以讓游戲?qū)<叶啻瓮孢@個(gè)游戲,并記錄他們的行為。然后,可以通過(guò)選擇類似于游戲?qū)<业摹刚_」動(dòng)作來(lái)訓(xùn)練模型。
但這實(shí)際上并不是人類學(xué)習(xí)的方式。人類可以在無(wú)指導(dǎo)的情況下,自學(xué)像 Catch 這樣的游戲。這非常有用。想象一下,你如果每次想學(xué)習(xí)像 Catch 一樣簡(jiǎn)單的東西,就必須雇傭一批專家玩這個(gè)游戲上千次!這必然非常昂貴而緩慢。
而在強(qiáng)化學(xué)習(xí)中,模型不會(huì)根據(jù)標(biāo)記的數(shù)據(jù)訓(xùn)練,而是通過(guò)以往的經(jīng)歷。
深度強(qiáng)化學(xué)習(xí)
強(qiáng)化學(xué)習(xí)受行為心理學(xué)啟發(fā)。我們并不為模型提供「正確的」行為,而是給予獎(jiǎng)勵(lì)和懲罰。該模型接受關(guān)于當(dāng)前環(huán)境狀態(tài)的信息(例如計(jì)算機(jī)游戲屏幕)。然后,它將輸出一個(gè)動(dòng)作,就像游戲手柄一樣。環(huán)境將對(duì)這個(gè)動(dòng)作做出回應(yīng),并提供下一個(gè)狀態(tài)和獎(jiǎng)懲行為。
據(jù)此,模型學(xué)習(xí)并尋找最大化獎(jiǎng)勵(lì)的行為。
實(shí)際上,有很多方式能夠做到這一點(diǎn)。下面,讓我們了解一下 Q-Learning。利用 Q-Learning 訓(xùn)練計(jì)算機(jī)玩 Atari 游戲的時(shí)候,Q-Learning 曾引起了轟動(dòng)?,F(xiàn)在,Q-Learning 依然是一個(gè)有重大意義的概念。大多數(shù)現(xiàn)代的強(qiáng)化學(xué)習(xí)算法,都是 Q-Learning 的一些改進(jìn)。
理解 Q-Learning
了解 Q-Learning 的一個(gè)好方法,就是將 Catch 游戲和下象棋進(jìn)行比較。
在這兩種游戲中,你都會(huì)得到一個(gè)狀態(tài) S。在象棋中,這代表棋盤上棋子的位置。在 Catch 游戲中,這代表水果和籃子的位置。
然后,玩家要采取一個(gè)動(dòng)作,稱作 A。在象棋中,玩家要移動(dòng)一個(gè)棋子。而在 Catch 游戲中,這代表著將籃子向左、向右移動(dòng),或是保持在當(dāng)前位置。據(jù)此,會(huì)得到一些獎(jiǎng)勵(lì) R 和一個(gè)新狀態(tài) S’。
Catch 游戲和象棋的一個(gè)共同點(diǎn)在于,獎(jiǎng)勵(lì)并不會(huì)立即出現(xiàn)在動(dòng)作之后。
在 Catch 游戲中,只有在水果掉到籃子里或是撞到地板上時(shí)你才會(huì)獲得獎(jiǎng)勵(lì)。而在象棋中,只有在整盤棋贏了或輸了之后,才會(huì)獲得獎(jiǎng)勵(lì)。這也就是說(shuō),獎(jiǎng)勵(lì)是稀疏分布的(sparsely distributed)。大多數(shù)時(shí)候,R 保持為零。
產(chǎn)生的獎(jiǎng)勵(lì)并不總是前一個(gè)動(dòng)作的結(jié)果。也許,很早之前采取的某些動(dòng)作才是獲勝的關(guān)鍵。要弄清楚哪個(gè)動(dòng)作對(duì)最終的獎(jiǎng)勵(lì)負(fù)責(zé),這通常被稱為信度分配問(wèn)題(credit assignment problem)。
由于獎(jiǎng)勵(lì)的延遲性,優(yōu)秀的象棋選手并不會(huì)僅通過(guò)最直接可見的獎(jiǎng)勵(lì)來(lái)選擇他們的落子方式。相反,他們會(huì)考慮預(yù)期未來(lái)獎(jiǎng)勵(lì)(expected future reward),并據(jù)此進(jìn)行選擇。例如,他們不僅要考慮下一步是否能夠消滅對(duì)手的一個(gè)棋子。他們也會(huì)考慮那些從長(zhǎng)遠(yuǎn)的角度有益的行為。
在 Q-Learning 中,我們根據(jù)最高的預(yù)期未來(lái)獎(jiǎng)勵(lì)選行動(dòng)。我們使用 Q 函數(shù)進(jìn)行計(jì)算。這個(gè)數(shù)學(xué)函數(shù)有兩個(gè)變量:游戲的當(dāng)前狀態(tài)和給定的動(dòng)作。因此,我們可以將其記為 Q(state,action)。在 S 狀態(tài)下,我們將估計(jì)每個(gè)可能的動(dòng)作 A 所帶來(lái)的的回報(bào)。我們假定在采取行動(dòng) A 且進(jìn)入下一個(gè)狀態(tài) S’ 以后,一切都很完美。
對(duì)于給定狀態(tài) S 和動(dòng)作 A,預(yù)期未來(lái)獎(jiǎng)勵(lì) Q(S,A)被計(jì)算為即時(shí)獎(jiǎng)勵(lì) R 加上其后的預(yù)期未來(lái)獎(jiǎng)勵(lì) Q(S’,A’)。我們假設(shè)下一個(gè)動(dòng)作 A’ 是最優(yōu)的。
由于未來(lái)的不確定性,我們用 γ 因子乘以 Q(S’,A’)表示折扣:
Q(S,A) = R + γ * max Q(S’,A’)
象棋高手擅長(zhǎng)在心里估算未來(lái)回報(bào)。換句話說(shuō),他們的 Q 函數(shù) Q(S,A)非常精確。大多數(shù)象棋訓(xùn)練都是圍繞著發(fā)展更好的 Q 函數(shù)進(jìn)行的。玩家使用棋譜學(xué)習(xí),從而了解特定動(dòng)作如何發(fā)生,以及給定的動(dòng)作有多大可能會(huì)導(dǎo)致勝利。但是,機(jī)器如何評(píng)估一個(gè) Q 函數(shù)的好壞呢?這就是神經(jīng)網(wǎng)絡(luò)大展身手的地方了。
最終回歸
玩游戲的時(shí)候,我們會(huì)產(chǎn)生很多「經(jīng)歷」,包括以下幾個(gè)部分:
- 初始狀態(tài),S
- 采取的動(dòng)作,A
- 獲得的獎(jiǎng)勵(lì),R
- 下一狀態(tài),S’
這些經(jīng)歷就是我們的訓(xùn)練數(shù)據(jù)。我們可以將估算 Q(S,A)的問(wèn)題定義為回歸問(wèn)題。為了解決這個(gè)問(wèn)題,我們可以使用神經(jīng)網(wǎng)絡(luò)。給定一個(gè)由 S 和 A 組成的輸入向量,神經(jīng)網(wǎng)絡(luò)需要能預(yù)測(cè) Q(S,A)的值等于目標(biāo):R + γ * max Q(S’,A’)。
如果我們能很好地預(yù)測(cè)不同狀態(tài) S 和不同行為 A 的 Q(S,A),我們就能很好地逼近 Q 函數(shù)。請(qǐng)注意,我們通過(guò)與 Q(S,A)相同的神經(jīng)網(wǎng)絡(luò)估算 Q(S’,A’)。
訓(xùn)練過(guò)程
給定一批經(jīng)歷 <S,A,R,S’>,其訓(xùn)練過(guò)程如下:
- 對(duì)于每個(gè)可能的動(dòng)作 A’(向左、向右、不動(dòng)),使用神經(jīng)網(wǎng)絡(luò)預(yù)測(cè)預(yù)期未來(lái)獎(jiǎng)勵(lì) Q(S’,A’);
- 選擇 3 個(gè)預(yù)期未來(lái)獎(jiǎng)勵(lì)中的最大值,作為 max Q(S’,A’);
- 計(jì)算 r + γ * max Q(S’,A’),這就是神經(jīng)網(wǎng)絡(luò)的目標(biāo)值;
- 使用損失函數(shù)(loss function)訓(xùn)練神經(jīng)網(wǎng)絡(luò)。損失函數(shù)可以計(jì)算預(yù)測(cè)值離目標(biāo)值的距離。此處,我們使用 0.5 * (predicted_Q(S,A)—target)² 作為損失函數(shù)。
在游戲過(guò)程中,所有的經(jīng)歷都會(huì)被存儲(chǔ)在回放存儲(chǔ)器(replay memory)中。這就像一個(gè)存儲(chǔ) <S,A,R,S’> 對(duì)的簡(jiǎn)單緩存。這些經(jīng)歷回放類同樣能用于準(zhǔn)備訓(xùn)練數(shù)據(jù)。讓我們看看下面的代碼:
- class ExperienceReplay(object):
- """
- During gameplay all the experiences < s, a, r, s’ > are stored in a replay memory.
- In training, batches of randomly drawn experiences are used to generate the input and target for training.
- """
- def __init__(self, max_memory=100, discount=.9):
- """
- Setup
- max_memory: the maximum number of experiences we want to store
- memory: a list of experiences
- discount: the discount factor for future experience
- In the memory the information whether the game ended at the state is stored seperately in a nested array
- [...
- [experience, game_over]
- [experience, game_over]
- ...]
- """
- self.max_memory = max_memory
- self.memory = list()
- self.discount = discount
- def remember(self, states, game_over):
- #Save a state to memory
- self.memory.append([states, game_over])
- #We don't want to store infinite memories, so if we have too many, we just delete the oldest one
- if len(self.memory) > self.max_memory:
- del self.memory[0]
- def get_batch(self, model, batch_size=10):
- #How many experiences do we have?
- len_memory = len(self.memory)
- #Calculate the number of actions that can possibly be taken in the game
- num_actions = model.output_shape[-1]
- #Dimensions of the game field
- env_dim = self.memory[0][0][0].shape[1]
- #We want to return an input and target vector with inputs from an observed state...
- inputs = np.zeros((min(len_memory, batch_size), env_dim))
- #...and the target r + gamma * max Q(s’,a’)
- #Note that our target is a matrix, with possible fields not only for the action taken but also
- #for the other possible actions. The actions not take the same value as the prediction to not affect them
- targets = np.zeros((inputs.shape[0], num_actions))
- #We draw states to learn from randomly
- for i, idx in enumerate(np.random.randint(0, len_memory,
- size=inputs.shape[0])):
- """
- Here we load one transition <s, a, r, s’> from memory
- state_t: initial state s
- action_t: action taken a
- reward_t: reward earned r
- state_tp1: the state that followed s’
- """
- state_t, action_t, reward_t, state_tp1 = self.memory[idx][0]
- #We also need to know whether the game ended at this state
- game_over = self.memory[idx][1]
- #add the state s to the input
- inputs[i:i+1] = state_t
- # First we fill the target values with the predictions of the model.
- # They will not be affected by training (since the training loss for them is 0)
- targets[i] = model.predict(state_t)[0]
- """
- If the game ended, the expected reward Q(s,a) should be the final reward r.
- Otherwise the target value is r + gamma * max Q(s’,a’)
- """
- # Here Q_sa is max_a'Q(s', a')
- Q_sa = np.max(model.predict(state_tp1)[0])
- #if the game ended, the reward is the final reward
- if game_over: # if game_over is True
- targets[i, action_t] = reward_t
- else:
- # r + gamma * max Q(s’,a’)
- targets[i, action_t] = reward_t + self.discount * Q_sa
- return inputs, targets
定義模型
現(xiàn)在讓我們定義這個(gè)利用 Q-Learning 學(xué)習(xí) Catch 游戲的模型。我們使用 Keras 作為 Tensorflow 的前端。我們的基準(zhǔn)模型是一個(gè)簡(jiǎn)單的三層密集網(wǎng)絡(luò)。這個(gè)模型在簡(jiǎn)單版的 Catch 游戲當(dāng)中表現(xiàn)很好。你可以在 GitHub 中找到它的完整實(shí)現(xiàn)過(guò)程。
你也可以嘗試更加復(fù)雜的模型,測(cè)試其能否獲得更好的性能。
- num_actions = 3 # [move_left, stay, move_right]
- hidden_size = 100 # Size of the hidden layers
- grid_size = 10 # Size of the playing field
- def baseline_model(grid_size,num_actions,hidden_size):
- #seting up the model with keras
- model = Sequential()
- model.add(Dense(hidden_size, input_shape=(grid_size**2,), activation='relu'))
- model.add(Dense(hidden_size, activation='relu'))
- model.add(Dense(num_actions))
- model.compile(sgd(lr=.1), "mse")
- return model
探索
Q-Learning 的最后一種成分是探索。日常生活的經(jīng)驗(yàn)告訴我們,有時(shí)候你得做點(diǎn)奇怪的事情或是隨機(jī)的手段,才能發(fā)現(xiàn)是否有比日常動(dòng)作更好的東西。
Q-Learning 也是如此。總是做最好的選擇,意味著你可能會(huì)錯(cuò)過(guò)一些從未探索的道路。為了避免這種情況,學(xué)習(xí)者有時(shí)會(huì)添加一個(gè)隨機(jī)項(xiàng),而未必總是用最好的。我們可以將定義訓(xùn)練方法如下:
- def train(model,epochs):
- # Train
- #Reseting the win counter
- win_cnt = 0
- # We want to keep track of the progress of the AI over time, so we save its win count history
- win_hist = []
- #Epochs is the number of games we play
- for e in range(epochs):
- loss = 0.
- #Resetting the game
- env.reset()
- game_over = False
- # get initial input
- input_t = env.observe()
- while not game_over:
- #The learner is acting on the last observed game screen
- #input_t is a vector containing representing the game screen
- input_tm1 = input_t
- #Take a random action with probability epsilon
- if np.random.rand() <= epsilon:
- #Eat something random from the menu
- action = np.random.randint(0, num_actions, size=1)
- else:
- #Choose yourself
- #q contains the expected rewards for the actions
- q = model.predict(input_tm1)
- #We pick the action with the highest expected reward
- action = np.argmax(q[0])
- # apply action, get rewards and new state
- input_t, reward, game_over = env.act(action)
- #If we managed to catch the fruit we add 1 to our win counter
- if reward == 1:
- win_cnt += 1
- #Uncomment this to render the game here
- #display_screen(action,3000,inputs[0])
- """
- The experiences < s, a, r, s’ > we make during gameplay are our training data.
- Here we first save the last experience, and then load a batch of experiences to train our model
- """
- # store experience
- exp_replay.remember([input_tm1, action, reward, input_t], game_over)
- # Load batch of experiences
- inputs, targets = exp_replay.get_batch(model, batch_size=batch_size)
- # train model on experiences
- batch_loss = model.train_on_batch(inputs, targets)
- #sum up loss over all batches in an epoch
- loss += batch_loss
- win_hist.append(win_cnt)
- return win_hist
我將這個(gè)游戲機(jī)器人訓(xùn)練了 5000 個(gè) epoch,結(jié)果表現(xiàn)得很不錯(cuò)!
Catch 機(jī)器人的動(dòng)作
正如你在上述動(dòng)圖中看到的那樣,機(jī)器人可以抓住從天空中掉落的蘋果。為了將這個(gè)模型學(xué)習(xí)的過(guò)程可視化,我繪制了每一個(gè) epoch 的勝利移動(dòng)平均線,結(jié)果如下:
接下來(lái)做什么?現(xiàn)在,你已經(jīng)對(duì)強(qiáng)化學(xué)習(xí)有了初步的直覺(jué)了解。我建議仔細(xì)閱讀該教程的完整代碼。你也可以試驗(yàn)看看。