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 课程