Schooling Flappy Bird:強化學習教程
已發表: 2022-03-11在經典編程中,軟件指令是由程序員明確制定的,根本不會從數據中學到任何東西。 相比之下,機器學習是計算機科學的一個領域,它使用統計方法使計算機能夠學習並從數據中提取知識,而無需明確編程。
在這個強化學習教程中,我將展示我們如何使用 PyTorch 來教強化學習神經網絡如何玩 Flappy Bird。 但首先,我們需要介紹一些構建塊。
機器學習算法大致可以分為兩部分:傳統學習算法和深度學習算法。 傳統學習算法的可學習參數通常比深度學習算法少得多,學習能力也少得多。
此外,傳統的學習算法無法進行特徵提取:人工智能專家需要找出一個好的數據表示,然後將其發送給學習算法。 傳統機器學習技術的示例包括 SVM、隨機森林、決策樹和 $k$-means,而深度學習中的核心算法是深度神經網絡。
深度神經網絡的輸入可以是原始圖像,人工智能專家不需要找到任何數據表示——神經網絡會在訓練過程中找到最佳表示。
許多深度學習技術早已為人所知,但硬件方面的最新進展迅速推動了深度學習的研究和開發。 英偉達負責該領域的擴展,因為它的 GPU 已經實現了快速的深度學習實驗。
可學習的參數和超參數
機器學習算法由在訓練過程中調整的可學習參數和在訓練過程之前設置的不可學習參數組成。 在學習之前設置的參數稱為超參數。
網格搜索是尋找最優超參數的常用方法。 這是一種蠻力方法:它意味著在定義的範圍內嘗試所有可能的超參數組合,並選擇最大化預定義指標的組合。
有監督、無監督和強化學習算法
對學習算法進行分類的一種方法是在有監督和無監督算法之間劃清界限。 (但這並不一定那麼簡單:強化學習介於這兩種類型之間。)
當我們談論監督學習時,我們看的是 $ (x_i, y_i) $ 對。 $ x_i $ 是算法的輸入,$ y_i $ 是輸出。 我們的任務是找到一個能正確映射$x_i$到$y_i$的函數。
為了調整可學習的參數,以便它們定義一個將 $ x_i $ 映射到 $ y_i $ 的函數,需要定義一個損失函數和一個優化器。 優化器最小化損失函數。 損失函數的一個示例是均方誤差 (MSE):
\[MSE = \sum_{i=1}^{n} (y_i - \widehat{y_i} )^2\]這裡,$ y_i $ 是真實標籤,$ \widehat{y_i} $ 是預測標籤。 在深度學習中非常流行的一種優化器是隨機梯度下降。 有很多變體試圖改進隨機梯度下降法:Adam、Adadelta、Adagrad 等。
無監督算法試圖在沒有明確提供標籤的情況下在數據中找到結構。 $k$-means 是試圖在數據中找到最優聚類的無監督算法的例子之一。 下面是包含 300 個數據點的圖像。 $k$-means 算法在數據中找到結構並為每個數據點分配一個集群標籤。 每個集群都有自己的顏色。
強化學習使用獎勵:稀疏、時間延遲的標籤。 代理採取行動,改變環境,從中獲得新的觀察和獎勵。 觀察是代理從環境中感知到的刺激。 它可以是代理看到的、聽到的、聞到的等等。
代理在採取行動時會獲得獎勵。 它告訴代理該操作有多好。 通過感知觀察和獎勵,代理學習如何在環境中以最佳方式表現。 我將在下面更詳細地介紹這一點。
主動、被動和逆強化學習
這種技術有幾種不同的方法。 首先,我們在這裡使用的是主動強化學習。 相比之下,有被動強化學習,獎勵只是另一種類型的觀察,而是根據固定策略做出決策。
最後,逆強化學習嘗試在給定動作歷史及其在各種狀態下的獎勵的情況下重建獎勵函數。
泛化、過擬合和欠擬合
參數和超參數的任何固定實例稱為模型。 機器學習實驗通常由兩部分組成:訓練和測試。
在訓練過程中,使用訓練數據調整可學習參數。 在測試過程中,可學習的參數被凍結,任務是檢查模型對以前看不見的數據的預測效果如何。 泛化是學習機器在經歷了一個學習數據集之後,在一個新的、看不見的例子或任務上準確執行的能力。
如果模型在數據方面過於簡單,它將無法擬合訓練數據,並且在訓練數據集和測試數據集上都表現不佳。 在這種情況下,我們說模型是欠擬合的。
如果機器學習模型在訓練數據集上表現良好,但在測試數據集上表現不佳,我們就說它是過擬合的。 過度擬合是模型相對於數據過於復雜的情況。 它可以完美地擬合訓練數據,但它過於適應訓練數據集,以至於它在測試數據上表現不佳——也就是說,它根本無法泛化。
下圖顯示了與整體數據和預測函數之間的平衡情況相比的欠擬合和過擬合。
可擴展性
數據對於構建機器學習模型至關重要。 通常,傳統的學習算法不需要太多的數據。 但是由於它們的容量有限,性能也受到限制。 下圖顯示了與傳統機器學習算法相比,深度學習方法的擴展性如何。
神經網絡
神經網絡由多層組成。 下圖顯示了一個簡單的四層神經網絡。 第一層是輸入層,最後一層是輸出層。 輸入層和輸出層之間的兩層是隱藏層。
如果一個神經網絡有不止一個隱藏層,我們稱之為深度神經網絡。 輸入集合$X$給神經網絡,得到輸出$y$。 學習是使用結合了損失函數和優化器的反向傳播算法完成的。
反向傳播由兩部分組成:前向傳播和後向傳播。 在前向傳遞中,將輸入數據放在神經網絡的輸入上並獲得輸出。 計算基本事實和預測之間的損失,然後在反向傳播中,根據損失調整神經網絡的參數。
卷積神經網絡
一種神經網絡變體是卷積神經網絡。 它主要用於計算機視覺任務。
卷積神經網絡中最重要的層是卷積層(因此得名)。 它的參數由可學習的過濾器組成,也稱為內核。 卷積層對輸入應用卷積運算,將結果傳遞給下一層。 卷積運算減少了可學習參數的數量,起到了一種啟發式的作用,使神經網絡更容易訓練。
下面是卷積層中的一個卷積核是如何工作的。 將內核應用於圖像並獲得卷積特徵。
ReLU 層用於在神經網絡中引入非線性。 非線性很重要,因為我們可以使用它們對所有類型的函數進行建模,而不僅僅是線性函數,從而使神經網絡成為通用函數逼近器。 這使得 ReLU 函數定義如下:
\[ReLU = \max(0, x)\]ReLU 是用於在神經網絡中引入非線性的所謂激活函數的示例之一。 其他激活函數的示例包括 sigmoid 和超正切函數。 ReLU 是最流行的激活函數,因為它表明與其他激活函數相比,它使神經網絡訓練更有效。
下面是 ReLU 函數的圖。
如您所見,此 ReLU 函數只是將負值更改為零。 這有助於防止梯度消失問題。 如果梯度消失,它不會對調整神經網絡的權重產生很大影響。
卷積神經網絡由多個層組成:卷積層、ReLU 層和全連接層。 全連接層將一層中的每個神經元連接到另一層中的每個神經元,如本節開頭圖像中的兩個隱藏層所示。 最後一個全連接層將前一層的輸出映射到,在這種情況下, number_of_actions
值。
應用
深度學習在計算機視覺、語音識別和強化學習等多個機器學習子領域取得了成功並且優於經典機器學習算法。 這些深度學習領域被應用於現實世界的各個領域:金融、醫學、娛樂等。
強化學習
強化學習基於代理。 代理在環境中採取行動並從中獲得觀察和獎勵。 需要訓練代理以最大化累積獎勵。 正如引言中所指出的,對於經典的機器學習算法,機器學習工程師需要進行特徵提取,即創建能夠很好地代表環境並將其輸入機器學習算法的良好特徵。
使用深度學習,可以創建一個端到端系統,該系統接受高維輸入(例如視頻),並從中學習代理採取良好行動的最佳策略。
2013 年,倫敦人工智能初創公司 DeepMind 在學習直接從高維感官輸入控制代理方面取得了重大突破。 他們發表了一篇論文《用深度強化學習玩 Atari》 ,其中展示了他們如何教人工神經網絡通過看屏幕來玩 Atari 遊戲。 他們被谷歌收購,然後在Nature上發表了一篇新論文,有一些改進: Human-level control through deep enhancement learning 。
與其他機器學習範例相反,強化學習沒有監督者,只有獎勵信號。 反饋是延遲的:它不像監督學習算法那樣是即時的。 數據是連續的,代理的行為會影響它接收到的後續數據。
現在,一個代理位於它的環境中,它處於某種狀態。 為了更詳細地描述這一點,我們使用馬爾可夫決策過程,這是一種對強化學習環境進行建模的正式方法。 它由一組狀態、一組可能的動作和從一個狀態轉換到另一個狀態的規則(例如概率)組成。
代理能夠執行操作,改變環境。 我們稱獎勵為$R_t$。 這是一個標量反饋信號,表明代理在步驟 $t$ 處的表現如何。
為了獲得良好的長期績效,不僅要考慮眼前的回報,還要考慮未來的回報。 從時間步 $t$ 開始的一集的總獎勵是 $ R_t = r_t + r_{t+1} + r_{t+2} + \ldots + r_n $。 未來是不確定的,我們在未來走得越遠,未來的預測就越可能出現分歧。 因此,使用折扣的未來獎勵: $ R_t = r_t +\gamma r_{t+1} + \gamma^2r_{t+2} + \ldots + \gamma^{nt}r_n = r_t + \gamma R_{t+1} $。 代理人應該選擇最大化折扣未來獎勵的行動。
深度 Q 學習
$ Q(s, a) $ 函數表示當
未來獎勵的估計由貝爾曼方程給出: $ Q(s, a) = r + \gamma \max_{a'}Q(s', a') $ 。 換句話說,給定狀態 $ s $ 和動作 $ a $ 的最大未來獎勵是立即獎勵加上下一個狀態的最大未來獎勵。
使用非線性函數(神經網絡)逼近 Q 值不是很穩定。 因此,經驗回放用於穩定性。 訓練課程中的情節期間的經驗存儲在重放存儲器中。 使用來自重放內存的隨機小批量,而不是使用最近的轉換。 這打破了後續訓練樣本的相似性,否則會將神經網絡驅動到局部最小值。
關於深度 Q 學習,還有兩個更重要的方面需要提及:探索和利用。 通過利用,可以根據當前信息做出最佳決策。 探索收集更多信息。
當算法執行神經網絡提出的動作時,它就是在進行利用:它利用了神經網絡的學習知識。 相反,算法可以採取隨機動作,探索新的可能性並將潛在的新知識引入神經網絡。
DeepMind 的論文 Playing Atari with Deep Reinforcement Learning 中的“Deep Q-learning algorithm with Experience Replay”如下圖所示。
DeepMind 將使用其方法訓練的捲積網絡稱為深度 Q 網絡 (DQN)。
使用 Flappy Bird 的深度 Q 學習示例
Flappy Bird 是一款流行的手機遊戲,最初由越南視頻遊戲藝術家和程序員 Dong Nguyen 開發。 在其中,玩家控制一隻鳥並試圖在綠色管道之間飛行而不撞到它們。
下面是使用 PyGame 編碼的 Flappy Bird 克隆的屏幕截圖:
克隆已經被分叉和修改:背景、聲音以及不同的鳥和管道樣式已被刪除,代碼已被調整,以便可以輕鬆地與簡單的強化學習框架一起使用。 修改後的遊戲引擎取自這個 TensorFlow 項目:
但我沒有使用 TensorFlow,而是使用 PyTorch 構建了一個深度強化學習框架。 PyTorch 是一個用於快速、靈活實驗的深度學習框架。 它在 Python 中提供具有強大 GPU 加速功能的張量和動態神經網絡。
神經網絡架構與論文Human-level control through deep enhancement learning中使用的 DeepMind 相同。
層 | 輸入 | 過濾器尺寸 | 跨步 | 過濾器數量 | 激活 | 輸出 |
---|---|---|---|---|---|---|
轉換1 | 84x84x4 | 8x8 | 4 | 32 | ReLU | 20x20x32 |
轉換2 | 20x20x32 | 4x4 | 2 | 64 | ReLU | 9x9x64 |
轉換3 | 9x9x64 | 3x3 | 1 | 64 | ReLU | 7x7x64 |
fc4 | 7x7x64 | 512 | ReLU | 512 | ||
fc5 | 512 | 2 | 線性 | 2 |
有三個卷積層和兩個全連接層。 每一層都使用 ReLU 激活,除了最後一層,它使用線性激活。 神經網絡輸出兩個值,代表玩家唯一可能的動作:“飛起來”和“什麼也不做”。

輸入由四個連續的 84x84 黑白圖像組成。 下面是輸入到神經網絡的四個圖像的示例。
您會注意到圖像已旋轉。 那是因為克隆的遊戲引擎的輸出是旋轉的。 但是如果神經網絡被教導然後使用這些圖像進行測試,它不會影響它的性能。
您可能還會注意到圖像已被裁剪,因此地板被省略了,因為它與此任務無關。 代表管道和鳥的所有像素都是白色的,代表背景的所有像素都是黑色的。
這是定義神經網絡的代碼的一部分。 神經網絡的權重被初始化為遵循均勻分佈 $\mathcal{U}(-0.01, 0.01)$。 神經網絡參數的偏差部分設置為 0.01。 嘗試了幾種不同的初始化(Xavier uniform、Xavier normal、Kaiming uniform、Kaiming normal、uniform、normal),但上述初始化使得神經網絡收斂和訓練最快。 神經網絡的大小為 6.8 MB。
class NeuralNetwork(nn.Module): def __init__(self): super(NeuralNetwork, self).__init__() self.number_of_actions = 2 self.gamma = 0.99 self.final_epsilon = 0.0001 self.initial_epsilon = 0.1 self.number_of_iterations = 2000000 self.replay_memory_size = 10000 self.minibatch_size = 32 self.conv1 = nn.Conv2d(4, 32, 8, 4) self.relu1 = nn.ReLU(inplace=True) self.conv2 = nn.Conv2d(32, 64, 4, 2) self.relu2 = nn.ReLU(inplace=True) self.conv3 = nn.Conv2d(64, 64, 3, 1) self.relu3 = nn.ReLU(inplace=True) self.fc4 = nn.Linear(3136, 512) self.relu4 = nn.ReLU(inplace=True) self.fc5 = nn.Linear(512, self.number_of_actions) def forward(self, x): out = self.conv1(x) out = self.relu1(out) out = self.conv2(out) out = self.relu2(out) out = self.conv3(out) out = self.relu3(out) out = out.view(out.size()[0], -1) out = self.fc4(out) out = self.relu4(out) out = self.fc5(out) return out
在構造函數中,您會注意到定義了超參數。 超參數優化不是為了這篇博文的目的而進行的。 相反,超參數主要來自 DeepMind 的論文。 在這裡,一些超參數被縮放到低於 DeepMind 的論文中的水平,因為 Flappy Bird 沒有他們用於調整的 Atari 遊戲那麼複雜。
此外,epsilon 已更改為更適合該遊戲。 DeepMind 使用 1 的 epsilon,但這裡我們使用 0.1。 這是因為較高的 epsilon 迫使鳥兒拍打很多,這將鳥兒推向屏幕的上邊界,最終總是導致鳥兒撞到管道上。
強化學習代碼有兩種模式:訓練和測試。 在測試階段,我們可以看到強化學習算法在玩遊戲方面的學習情況。 但首先,需要訓練神經網絡。 我們需要定義要最小化的損失函數和最小化損失函數的優化器。 我們將使用Adam優化方法和損失函數的均方誤差:
optimizer = optim.Adam(model.parameters(), lr=1e-6) criterion = nn.MSELoss()
遊戲應該被實例化:
game_state = GameState()
重放內存被定義為一個 Python 列表:
replay_memory = []
現在我們需要初始化第一個狀態。 動作是二維張量:
- [1, 0] 表示“什麼都不做”
- [0, 1] 代表“飛起來”
frame_step
方法為我們提供了下一個屏幕、獎勵和關於下一個狀態是否為終端的信息。 每隻鳥在沒有通過管道時沒有死亡的移動獎勵為0.1
,如果鳥成功通過管道,則獎勵為1
,如果鳥墜毀,則獎勵為-1
。
resize_and_bgr2gray
函數裁剪地板,將屏幕大小調整為 84x84 圖像,並將顏色空間從 BGR 更改為黑白。 image_to_tensor
函數將圖像轉換為 PyTorch 張量,如果 CUDA 可用,則將其放入 GPU 內存中。 最後,最後四個連續屏幕連接在一起,準備發送到神經網絡。
action = torch.zeros([model.number_of_actions], dtype=torch.float32) action[0] = 1 image_data, reward, terminal = game_state.frame_step(action) image_data = resize_and_bgr2gray(image_data) image_data = image_to_tensor(image_data) state = torch.cat((image_data, image_data, image_data, image_data)).unsqueeze(0)
使用以下代碼行設置初始 epsilon:
epsilon = model.initial_epsilon
接下來是主無限循環。 註釋寫在代碼中,您可以將代碼與上面編寫的帶有體驗重放算法的深度 Q 學習進行比較。
該算法從重放內存中採樣小批量並更新神經網絡的參數。 使用epsilon 貪婪探索執行操作。 隨著時間的推移,Epsilon 正在被退火。 被最小化的損失函數是 $ L = \frac{1}{2}\left[\max_{a'}Q(s', a') - Q(s, a)\right]^2 $ 。 $ Q(s, a) $ 是使用貝爾曼方程計算的地面真值,$ \max_{a'}Q(s', a') $ 是從神經網絡獲得的。 神經網絡為兩個可能的動作給出兩個 Q 值,算法採用具有最高 Q 值的動作。
while iteration < model.number_of_iterations: # get output from the neural network output = model(state)[0] # initialize action action = torch.zeros([model.number_of_actions], dtype=torch.float32) if torch.cuda.is_available(): # put on GPU if CUDA is available action = action.cuda() # epsilon greedy exploration random_action = random.random() <= epsilon if random_action: print("Performed random action!") action_index = [torch.randint(model.number_of_actions, torch.Size([]), dtype=torch.int) if random_action else torch.argmax(output)][0] if torch.cuda.is_available(): # put on GPU if CUDA is available action_index = action_index.cuda() action[action_index] = 1 # get next state and reward image_data_1, reward, terminal = game_state.frame_step(action) image_data_1 = resize_and_bgr2gray(image_data_1) image_data_1 = image_to_tensor(image_data_1) state_1 = torch.cat((state.squeeze(0)[1:, :, :], image_data_1)).unsqueeze(0) action = action.unsqueeze(0) reward = torch.from_numpy(np.array([reward], dtype=np.float32)).unsqueeze(0) # save transition to replay memory replay_memory.append((state, action, reward, state_1, terminal)) # if replay memory is full, remove the oldest transition if len(replay_memory) > model.replay_memory_size: replay_memory.pop(0) # epsilon annealing epsilon = epsilon_decrements[iteration] # sample random minibatch minibatch = random.sample(replay_memory, min(len(replay_memory), model.minibatch_size)) # unpack minibatch state_batch = torch.cat(tuple(d[0] for d in minibatch)) action_batch = torch.cat(tuple(d[1] for d in minibatch)) reward_batch = torch.cat(tuple(d[2] for d in minibatch)) state_1_batch = torch.cat(tuple(d[3] for d in minibatch)) if torch.cuda.is_available(): # put on GPU if CUDA is available state_batch = state_batch.cuda() action_batch = action_batch.cuda() reward_batch = reward_batch.cuda() state_1_batch = state_1_batch.cuda() # get output for the next state output_1_batch = model(state_1_batch) # set y_j to r_j for terminal state, otherwise to r_j + gamma*max(Q) y_batch = torch.cat(tuple(reward_batch[i] if minibatch[i][4] else reward_batch[i] + model.gamma * torch.max(output_1_batch[i]) for i in range(len(minibatch)))) # extract Q-value q_value = torch.sum(model(state_batch) * action_batch, dim=1) # PyTorch accumulates gradients by default, so they need to be reset in each pass optimizer.zero_grad() # returns a new Tensor, detached from the current graph, the result will never require gradient y_batch = y_batch.detach() # calculate loss loss = criterion(q_value, y_batch) # do backward pass loss.backward() optimizer.step() # set state to be state_1 state = state_1
現在所有部分都已就緒,下面是使用我們的神經網絡的數據流的高級概述:
這是一個帶有訓練有素的神經網絡的短序列。
上面顯示的神經網絡使用高端 Nvidia GTX 1080 GPU 訓練了幾個小時; 相反,如果使用基於 CPU 的解決方案,則此特定任務將需要幾天時間。 遊戲引擎的 FPS 在訓練期間被設置為一個非常大的數字:999…999 — 換句話說,每秒盡可能多的幀數。 在測試階段,FPS 設置為 30。
下圖顯示了最大 Q 值在迭代過程中如何變化。 顯示每 10,000 次迭代。 向下的尖峰意味著對於特定的幀(一次迭代為一幀),神經網絡預測這隻鳥在未來將獲得非常低的獎勵——即它很快就會崩潰。
完整代碼和預訓練模型可在此處獲得。
深度強化學習:2D、3D 甚至現實生活
在這個 PyTorch 強化學習教程中,我展示了計算機如何在沒有任何遊戲知識的情況下學習玩 Flappy Bird,只使用人類第一次遇到遊戲時會使用的試錯方法。
有趣的是,該算法可以使用 PyTorch 框架在幾行代碼中實現。 本博客中的方法所基於的論文相對較舊,並且有許多更新的論文,這些論文進行了各種修改以實現更快的收斂。 從那時起,深度強化學習已被用於玩 3D 遊戲和現實世界的機器人系統。
DeepMind、Maluuba 和 Vicarious 等公司正在集中精力研究深度強化學習。 AlphaGo 使用了這種技術,並擊敗了世界上最好的圍棋選手之一李世石。 當時認為機器至少需要十年時間才能擊敗圍棋中最好的棋手。
對深度強化學習(以及一般人工智能)的大量興趣和投資甚至可能導致潛在的人工智能(AGI)——人類水平的智能(甚至更高),可以以算法和在電腦上模擬。 但 AGI 必須成為另一篇文章的主題。
參考:
- 揭開深度強化學習的神秘面紗
- Flappy Bird 的深度強化學習
- 維基百科上的“卷積神經網絡”
- 維基百科的“強化學習”
- 維基百科上的“馬爾可夫決策過程”
- 倫敦大學學院 RL 課程