Python 强化学习应用指南(一)

原文:Applied Reinforcement Learning with Python

协议:CC BY-NC-SA 4.0

一、强化学习简介

对于那些从我以前的书籍中返回的人来说,使用 R 1使用 Python 应用自然学习2 很高兴再次成为你们的读者。对新来的人,欢迎!在过去的一年里,深度学习包和技术的持续增长和发展彻底改变了各个行业。毫无疑问,这个领域最令人兴奋的部分之一是强化学习。这本身通常是许多广义人工智能应用的基础,例如学习玩视频游戏或下棋的软件。强化学习的好处是代理可以熟悉大范围的任务,假设问题可以被建模为包含动作、环境、代理的框架。假设,问题的范围可以从解决简单的游戏,到更复杂的 3D 游戏,到教会自动驾驶汽车如何在各种不同的地方接送乘客,以及教会机器人手臂如何抓住物体并将其放在厨房柜台上。

训练有素和部署良好的 RL 算法的意义是巨大的,因为它们更具体地寻求推动人工智能超越我在之前撰写的文章中谈到的一些狭隘的人工智能应用。算法不再是简单地预测一个目标或标签,而是在一个环境中操纵一个代理,这个代理有一组它可以选择的行动来实现一个目标/奖励。投入大量时间研究强化学习的公司和组织的例子有 Deep Mind 和 OpenAI,它们在该领域的突破是领先的解决方案之一。然而,让我们简要概述一下这个领域的历史。

强化学习的历史

强化学习在某种意义上是对最优控制的更名,最优控制是从控制理论延伸出来的概念。最优控制起源于 20 世纪 50 年代和 60 年代,当时它被用来描述一个试图达到某个“最优”标准的问题,以及达到这个目标需要什么样的“控制”法则。通常,我们将最优控制定义为一组微分方程。然后,这些等式定义了一条通向使误差函数值最小的值的路径。最优控制的核心是理查德·贝尔曼工作的顶峰,特别是动态规划。发展于 20 世纪 50 年代的动态规划是一种优化方法,它强调通过将一个大的个体问题分解成更小和更容易解决的部分来解决它。它也被认为是解决随机最优控制问题的唯一可行的方法,而且一般认为所有的最优控制都是强化学习。

贝尔曼对最优控制最显著的贡献是建立了汉密尔顿-雅可比-贝尔曼(HJB)方程。HJB 方程

其中= V w r t 的偏导数,时间变量 ta b =贝尔曼价值函数(未知标量)或从在时间 t 从状态 x 开始并最优控制系统直到时间 TC =标量成本率函数, D =最终效用状态函数, x ( * t * ) =系统状态向量

从这个方程得到的解就是价值函数,或给定动态系统的最小成本。HJB 方程是解决最优控制问题的标准方法。此外,动态规划通常是解决随机最优控制问题的唯一可行的方式或方法。开发动态编程来帮助解决的其中一个问题是马尔可夫决策过程(MDPs)。

MDP 及其与强化学习的关系

我们将 MDPs 描述为离散时间随机控制过程。具体来说,我们将离散时间随机过程定义为一个随机过程,其中指数变量由一组离散的或特定的值来表征(与连续值相反)。MDP 特别适用于结果部分受流程参与者影响,但流程也表现出一定程度的随机性的情况。MDPs 和动态规划因此成为强化学习理论的基础。

简单地说,我们基于马尔可夫特性假设,给定现在,未来独立于过去。除此之外,如果这种状态给我们对未来的描述与我们拥有全部历史信息一样,那么这种状态就被认为是充分的。这实质上意味着当前状态是唯一相关的信息,所有历史信息都不再是必需的。在数学上,一个状态被称为具有马尔可夫性质 iff

马尔可夫过程本身被认为是无记忆的,因为它们是从一个状态到另一个状态的随机转换。此外,我们认为它们是状态空间 S 上的元组(S,P ),其中状态通过转移函数 P 改变,定义如下:

其中 S =马氏状态,St=下一个状态。

这个转移函数描述了一个概率分布,其中该分布是代理可以转移到的所有可能状态。最后,我们从一种状态转移到另一种状态会得到一种回报,我们用数学方法定义如下:

其中 γ =折扣因子, γ ∈ [0,1], G t =总折扣奖励, R =奖励函数。

我们因此定义一个马尔可夫报酬过程(MRP)元组为( SPRγ )。

现在描述了所有这些公式,图 1-1 中的图像是一个可视化马尔可夫决策过程的例子。

img/480225_1_En_1_Fig1_HTML.jpg

图 1-1

马尔可夫决策过程

图 1-1 显示了一个代理人如何以不同的概率从一个状态移动到另一个状态,并获得奖励。最理想的情况是,在给定的环境参数下,在我们失败之前,我们将学会选择在给定事件中积累最多回报的过程。这在本质上是强化学习的一个非常基本的解释。

强化学习发展的另一个重要组成部分是试错学习,这是研究动物行为的一种方法。最具体地说,这被证明有助于理解“强化”不同行为的基本奖励和惩罚机制。然而“强化学习”这个词直到 20 世纪 60 年代才出现。在此期间,“信用分配问题”(cap)的概念被引入,特别是由马文·明斯基提出。明斯基是一位认知科学家,他一生的大部分时间都在研究人工智能,比如他的书 Perceptrons (1969 年)和他描述学分分配问题的论文“迈向人工智能”(1961 年)。cap 询问一个人如何分配成功的“荣誉”,即在实现成功的过程中所做的所有决策。具体来说,许多强化学习算法直接致力于解决这个精确的问题。然而,随着这种说法的提出,试错学习在很大程度上变得不太受欢迎,因为神经网络方法(以及一般的监督学习),如 Bernard Widrow 和 Ted Hoff 提出的创新,占据了人工智能领域的大部分兴趣。然而,在 20 世纪 80 年代,随着 Q 学习的发展,该领域的兴趣复苏是最值得注意的,当时时差(TD)学习真正取得了进展。

具有讽刺意味的是,TD 学习特别受到明斯基指出的动物心理学另一个重要方面的影响。它来自于两个刺激的想法,一个主要强化物与一个次要强化物配对,并随后影响行为。然而,TD 学习本身主要是由 Richard S. Sutton 开发的。他被认为是 RL 领域最有影响力的人物之一,因为他的博士论文引入了时间学分分配的概念。这指的是奖励,尤其是在非常细化的国家行为空间中,是如何被延迟的。例如,赢得一盘棋需要许多动作,然后才能获得赢得比赛的“奖励”。因此,奖励信号对暂时远离的状态没有显著影响。因此,时间信用分配解决了你如何以一种有意义地影响时间上遥远的状态的方式来奖励这些细微的行为。Q 学习,以产生回报的“Q”函数命名,建立在这些创新的基础上,专注于有限马尔可夫决策过程。

对于 Q 学习,这将我们带到了今天,在这里,强化学习的进一步改进正在不断地进行,并代表着人工智能的前沿。然而,随着这个概述的完成,让我们更具体地讨论读者可以期望学到什么。

强化学习算法和强化学习框架

强化学习类似地非常类似于传统机器学习中的监督学习领域,尽管存在关键差异。在监督学习中,有一个客观的答案,即我们正在训练模型,以根据给定观察的输入特征正确预测类别标签或特定值。特征类似于环境的给定状态中的向量,我们通常将这些向量作为一系列状态或者从一个状态到下一个状态单独地馈送给强化学习算法。然而,主要的区别是不一定总是有一个“答案”来解决特定的问题,因为强化学习算法可能有多种方式来成功地解决问题。在这种情况下,我们显然希望选择我们能够最快得出的答案,同时尽可能高效地解决问题。这正是我们选择模型变得至关重要的地方。

在前面对 RL 历史的概述中,我们介绍了几个定理,您将在接下来的章节中详细了解这些定理。然而,由于这是一个应用文本,理论也必须与实例一起提供。因此,我们将在本文中花费大量时间讨论 RL 框架 OpenAI Gym 以及它如何与不同的深度学习框架接口。OpenAI Gym 是一个框架,它允许我们轻松地部署、比较和测试强化学习算法。然而,它确实有很大程度的灵活性,因为我们可以利用深度学习方法和 OpenAI gym,我们将在我们的各种概念证明中这样做。下面显示了一些简单的示例代码,这些代码利用了显示从训练过程中产生的视频的包和情节(图 1-2 )。

img/480225_1_En_1_Fig2_HTML.jpg

图 1-2

手推车杆子视频游戏

import gym

def cartpole():
    environment = gym.make('CartPole-v1')
    environment.reset()
    for _ in range(50):
        environment.render()
        action = environment.action_space.sample()
        observation, reward, done, info = environment.step(action)
        print("Step {}:".format(_))
        print("action: {}".format(action))
        print("observation: {}".format(observation))
        print("reward: {}".format(reward))
        print("done: {}".format(done))
        print("info: {}".format(info))

当审查代码时,我们注意到在使用 gym 时,我们必须初始化一个我们的算法所在的环境。虽然使用软件包提供的环境很常见,但是我们也可以为自定义任务创建自己的环境(比如 gym 没有提供的视频游戏)。但是,接下来,让我们讨论一下终端输出中定义的其他值得注意的变量,如下所示。

action: 1
observation: [-0.02488139  0.00808876  0.0432061   0.02440099]
reward: 1.0
done: False
info: {}

这些变量可以细分如下:

  • 行动–指代理人在一个环境中采取的行动,该行动随后会产生回报

  • 奖励——屈服于代理。表示完成某个目标的行动质量

  • 观察值——由动作产生:指一个动作执行后的环境状态

  • Done–表示环境是否需要重置的布尔值

  • 信息–包含调试用杂项信息的字典

描述这些动作的流程如图 1-3 所示。

img/480225_1_En_1_Fig3_HTML.jpg

图 1-3

RL 算法的流程和环境

为了提供更多的背景信息,图 1-2 显示了一个推车和一根杆子的视频游戏,目标是成功地平衡推车和杆子,使杆子永远不会倾斜。因此,一个合理的目标是训练一些 DL 或 ML 算法,使我们能够做到这一点。然而,我们将在本书的后面处理这个特殊的问题。本节的目的只是简单介绍一下 OpenAI 健身房。

q 学习

我们在引言中简要讨论了 Q 学习;然而,值得强调的是,我们将利用本文的重要部分来讨论这个主题。q 学习的特点是有一些策略,它通知代理在不同的场景中要采取的行动。虽然它不需要模型,但我们可以使用一个模型,并且它通常特别适用于有限马尔可夫决策过程。具体来说,我们将在本文中处理的变体是 Q 学习、深度 Q 学习(DQL)和双 Q 学习(图 1-4 )。

img/480225_1_En_1_Fig4_HTML.jpg

图 1-4

q 学习流程图

我们将在专门引用这些技术的章节中对此进行更深入的讨论;然而,鉴于问题的复杂性,Q 学习和深度 Q 学习各有各自的优势,但两者往往遭受类似的失败。

演员-评论家模型

我们将在本书中探讨的最先进的模型是演员-评论家模型,它由 A2C 和 A3C 组成。这两个模型分别代表优势行动者-批评家模型和异步优势行动者-批评家模型。虽然这两者实际上是相同的,但区别在于后者具有多个彼此并行工作并独立更新参数的模型,而前者同时更新所有模型的参数。这些模型在更细粒度的基础上更新(动作对动作),而不是像许多其他强化学习算法那样以偶发的方式更新。图 1-5 显示了一个演员-评论家模型的例子。

img/480225_1_En_1_Fig5_HTML.jpg

图 1-5

演员-评论家模型可视化

强化学习的应用

在向读者全面介绍了强化学习的概念后,我们将解决多个问题,重点是向读者展示如何部署我们将在云环境中培训和利用的解决方案。

经典控制问题

由于最优控制领域已经存在了大约 60 年,有一些问题我们将首先解决,用户将会看到在其他强化学习文献中经常引用这些问题。其中一个就是大车杆子问题,参考图 1-2 。这是一个游戏,其中要求用户使用一组最佳选项来尝试和平衡一个手推车杆子。另一个如图 1-6 所示,称为冰封湖,其中代理学习如何在不踩冰的情况下穿过冰封的湖,否则会导致代理掉下去。

img/480225_1_En_1_Fig6_HTML.jpg

图 1-6

冰湖显现

超级马里奥兄弟。

有史以来最受欢迎的视频游戏之一被证明是展示如何将人工智能中的强化学习应用于虚拟环境的最佳方式之一。在 py_nes 库的帮助下,我们能够模拟超级马里奥兄弟 (图 1-7 )然后利用游戏中的数据,这样我们就可以训练模型玩关卡了。我们将专门关注一个级别,并将为此应用程序利用 AWS 资源,让读者有机会在此任务中获得经验。

img/480225_1_En_1_Fig7_HTML.jpg

图 1-7

超级马里奥兄弟。

死亡

我们将在这里应用的一个经典强化学习示例是学习玩一个简单级别的视频游戏 Doom (图 1-8 )。最初于 20 世纪 90 年代在 PC 上发布,这款视频游戏的重点是成功杀死你所面对的所有恶魔和/或敌人,同时通过整个关卡。然而,考虑到动作的范围、可用的包以及其他有用的属性,这有助于 Deep Q Learning 的出色应用。

img/480225_1_En_1_Fig8_HTML.jpg

图 1-8

Doom 屏幕快照

强化营销决策

不同自营交易公司的共同策略是通过向参与者提供流动性来赚钱,目标是以任何给定的价格买卖资产。虽然这种策略有既定的技术,但这是一个应用强化学习的绝佳领域,因为目标相对简单,并且是一个数据丰富的领域。我们将使用来自 Lobster 的限价订单数据,这是一个网站,其中包含大量用于此类实验的优秀订单数据。在图 1-9 中,我们可以看到订单簿的示例。

img/480225_1_En_1_Fig9_HTML.jpg

图 1-9

限价订单簿

刺猬索尼克

另一个适合我们使用不同模式的经典视频游戏是刺猬索尼克(图 1-10 )。除了在这一特定的章节,我们将带领读者从头开始创建他们自己的环境,他们可以利用 OpenAI gym 和自定义软件包装一个环境,然后训练他们自己的强化学习算法,然后解决水平问题。这将再次利用 AWS 资源进行培训,借鉴其他视频游戏示例中使用的相同流程,特别是超级马里奥兄弟

img/480225_1_En_1_Fig10_HTML.jpg

图 1-10

刺猬索尼克

结论

本文的目的是让读者熟悉如何在他们工作的各种环境中应用强化学习。读者应该熟悉深度学习框架,如 Tensorflow 和 Keras,我们将从这些框架中部署许多与结合使用的深度学习模型。虽然我们会花时间来解释强化学习理论,并且可能会解释一些与深度学习重叠的理论,但本文的大部分内容将致力于讨论强化学习的理论和应用。话虽如此,让我们从深入讨论强化学习的基础开始。

纽约:匆匆,2018 年。

2

纽约:匆匆,2017 年。

二、强化学习算法

读者应该知道,我们将在本书中利用各种深度学习和强化学习方法。然而,由于我们的重点将转移到讨论实现以及这些算法如何在生产环境中工作,我们必须花一些时间更详细地介绍算法本身。因此,本章的重点将是带领读者通过几个普遍应用的强化学习算法的例子,并在利用 OpenAI gym 解决不同问题的背景下展示它们。

开放 AI 健身房

在我们深入任何具体的例子之前,让我们首先简要地讨论一下读者将在本文的大部分内容中使用的软件。OpenAI 是一个位于旧金山湾区的研究机构。在他们在人工智能领域贡献的许多论文中,他们做出的最大的开源贡献之一是 OpenAI“健身房”。OpenAI gym 是为 python 发布的一个包,它提供了几个环境,用户可以在其中开始利用强化学习算法。我们将把这个包特别用于视频游戏环境,在其中我们可以训练我们的算法;但是,让我们从尝试理解软件包以及如何使用它开始。

健身房的基础是环境。在第一章中,我们讨论了环境,我们定义的各种变量,以及环境的输出。在我们制作的每一个游戏或环境中,往往会观察到它们的不同。本章我们玩的推车杆子游戏将是一个非常小的向量;然而,我们稍后工作的超级马里奥兄弟的环境将会更加复杂。然而,让我们在本章开始时,先看看 cart pole 以及一个新的环境,并尝试理解我们在这个环境中可能想要做些什么来解决这个问题。巴尔托、萨顿和安德森(1983)在“可以解决困难的学习控制问题的神经元样自适应元件”中描述了 cart 极点问题推车杆子问题的目标是保持杆子在推车上平衡。对于杆是垂直的每一帧,我们接收 1 的奖励;然而,如果杆子在任何给定的帧中不再保持垂直,游戏就输了。我们将不再关注他们解决这个问题的方法,而是关注利用策略梯度方法,这是强化学习的基石之一。

基于政策的学习

基于策略的梯度方法关注于直接优化策略函数,而不是试图学习一个价值函数,该价值函数将产生关于给定状态下的预期回报的信息。简而言之,我们选择一个动作与选择利用一个价值函数是分开的。策略分为以下几类:

  • 确定性–将给定状态映射到一个(或多个)操作的策略,具体来说就是所采取的操作“确定”了结果。例如,你在键盘上输入一个 word 文件。当您按下“y”时,您确定字符“y”会出现在屏幕上。

  • 随机-在一系列行动上产生概率分布的策略,因此有可能发生的行动是 而不是 。这特别用于环境是 而不是 确定性的情况,并且是部分可观察马尔可夫决策过程(POMDP)的一个例子。

与基于值的方法相比,基于策略的方法有一些特定的优势,这对于读者在建模过程中要记住是很重要的。首先,与基于价值的方法相比,它们更倾向于收敛于解决方案。这背后的原因是我们正被梯度引导向一个解决方案。直观上,梯度方法指向我们正在微分的最陡函数。当应用于误差函数,并以梯度下降的形式使用时,我们将调整使误差函数值最小化的动作(局部或全局)。因此,我们通常会有一个可行的解决方案。相比之下,基于值的方法可以在最小差异的动作之间产生大得多且更不直观的值范围。具体来说,我们没有相同的趋同保证。

第二,政策梯度特别擅长学习随机过程,而基于价值的函数则不行。虽然不是每个环境都是随机的,但是强化学习有望应用的许多实际例子都是随机的。价值函数在这里失败的原因是它们需要明确定义的环境,其中它们内部的操作将产生特定的结果,这些结果必须是确定的。因此,一个随机的环境不一定会对采取的相同行动产生相同的结果,这使得在这样的环境中基于价值的学习成为一种无效的方法。相比之下,基于策略的方法不需要通过采取相同的动作来探索环境。具体来说,不存在探索/利用的权衡(在结果已知的情况下做什么和尝试结果未知的行动之间进行选择)。第三,基于策略的方法在高维空间中明显更有效,因为它们的计算成本明显更低。基于价值的方法要求我们为每个可能的行为计算一个价值。如果我们有一个有相当多(或无限)行为的空间,这将使收敛到一个解决方案几乎是不可能的。基于策略的方法只是让我们执行一个动作并调整梯度。现在我们已经对基于策略的学习方法有了一个大致的了解,让我们将它应用于 cart pole 问题。

**## 政策梯度的数学解释

对基于策略的方法有了广泛的理解后,让我们首先深入到策略梯度的数学解释中。你应该记得在第一章中,我们简要地介绍了马尔可夫决策过程的概念。我们将 MDP 定义为元组( S,P,R,γ ),使得

定义了报酬和价值函数后,我们现在可以用数学方法来讨论政策。代理无法控制的环境本身;然而,在某种程度上,代理可以控制自己的行为。因此,策略被定义为在环境的给定状态期间所有动作的概率分布。这在数学上描述如下:

其中 π =策略, S =状态空间, A =动作空间, A t =时步 t 的动作,St=时步 t 的状态

现在,我们理解了策略引导我们的代理通过一个环境,其中在我们的环境所处的给定状态下某些动作是可能的。政策梯度究竟如何以及在哪里适用?保单梯度法的目的是在假设代理人有保单的情况下使预期报酬最大化。因此,策略由 θ 参数化,其中轨迹被定义为 τ 。轨迹被广义地定义为当我们遵循一个给定的政策时,我们在一个给定的事件过程中观察到的行动、回报和状态的序列。情节本身指的是在我们达到问题的目标或情节完全失败之前,代理仍在环境中执行一些操作的情况。因此,总报酬在数学上定义为 r ( τ ),即

然后,我们应用标准的机器学习方法,其中我们通过梯度下降找到最佳参数来最大化策略梯度。作为一个简短的回顾,函数的梯度表示函数中增长率最大的点,其大小是图形在该方向上的斜率。梯度通常乘以学习率,学习率决定了函数向最优解收敛的速度。然而,简单地说,梯度通常被定义为给定函数的一阶导数。然而,我们如何利用这一点来优化政策选择呢?

梯度上升在政策优化中的应用

基于梯度下降的优化在不同的机器学习方法中是常见的,例如线性回归以及用于多层感知器中权重优化的反向传播。然而,梯度上升是我们在这里用来优化我们选择的政策。我们不是试图最小化误差,而是试图最大化我们在将使用我们的算法的整个剧集中得到的分数。因此,参数更新应该如下所示:

因此,问题的目标可以表述如下:

口头上,我们试图选择参数值,使在给定状态下采取的行动产生的回报最大化。在我们建模的特定实例中,我们试图为最大化得分的网络选择权重。因此,我们用数学方法将预期总报酬的导数定义如下:

我们采用几何和的原因是,根据第一章中关于马尔可夫决策过程的定理,采取的每个行动都是相互独立的。因此,相关的累积奖励应以类似的方式计算。这个过程在轨迹的长度上重复,轨迹在逻辑上遵循给定情节的长度以及相关的奖励、状态和行动。当我们取总报酬的对数时,我们将其数学定义如下:

分解所有这些,期望回报的对数仅仅是在给定时间,给定状态下,政策从行动中产生的每个个体回报的对数的累积和,在整个轨迹上相加。理解这一点的重要性以及我们在 RL 中使用的通常被称为“无模型”的算法是,这些方程中隐含地显示了这样一个事实,即我们从未对环境建模,因为我们根本不知道状态的分布。事实上,我们唯一要做的就是奖励。现在,在解释了政策梯度的数学基础之后,让我们接下来将它应用于一个经典的控制问题:车杆。

用普通政策梯度解决车杆问题

对于这个问题,我们将利用 Keras,这是一个以能够快速部署神经网络模型而闻名的库。虽然我们将在本章的后面使用 Tensorflow,但是我们将在这里部署的模型将是在“applied _ rl _ python/neural _ networks/models . py”文件中定义的包的一部分。在这里,用户将看到我创建的类,这些类将使在本文内外使用这些解决方案比重复定义这些架构更容易:

class MLPModelKeras():
(Code redacted, please see the source code

    def create_policy_model(self, input_shape):
        input_layer = layers.Input(shape=input_shape)
        advantages = layers.Input(shape=[1])
        hidden_layer = layers.Dense(n_units=self.n_units, activation=self.hidden_activation)(input_layer)
        output_layer = layers.Dense(n_units=self.n_columns, activation=self.output_activation)(hidden_layer)

        def log_likelihood_loss(actual_labels, predicted_labels):
            log_likelihood = backend.log(actual_labels * (actual_labels - predicted_labels) + (1 - actual_labels) * (actual_labels - predicted_labels))
            return backend.mean(log_likelihood * advantages, keepdims=True)

        policy_model = Model(inputs=[input_layer, advantages], outputs=output_layer)
        policy_model.compile(loss=log_likelihood_loss, optimizer=Adam(self.learning_rate))
        model_prediction = Model(input=[input_layer], outputs=output_layer)
        return policy_model, model_prediction

用户应该从这部分代码中学到的是,我们正在定义一个用于策略梯度方法的神经网络,特别是在这里,它可以在其他向前发展的问题中重用和重新定义。Keras 的好处是,它允许您快速创建神经网络模型,如果您使用 Tensorflow,这些模型会非常冗长。这个额外的抽象层自动化并减少了在 Keras 中编写相同的神经网络模型所需的代码量。就该模型用于解决这一特定问题而言,用户应查看图 2-1 来尝试并理解我们试图用该神经网络解决的问题。

img/480225_1_En_2_Fig1_HTML.jpg

图 2-1

求解小车极点问题的神经网络

输入层表示给定状态下的环境及其方向,两个类表示我们可以采取的相应行动的概率。具体来说,我们将选择正确概率最高的动作,因为这被建模为分类问题。

接下来,让我们看看我们将用来解决这个问题的实际代码,在“chapter2/cart_pole_example.py”中可以找到。虽然 gym 经常更新,但这本书是用 gym 0 . 10 . 5 版编写的。在这个特定的版本中,我建议读者总是全局定义环境变量,然后在不同的函数中访问环境的属性。除此之外,在这里定义“environment_dimension”变量最初会重置环境。现在,让我们把注意力放在“cart_pole_game()”函数上,这个例子中的大部分计算都将在这个函数上进行。具体来说,让我们看看在特定的一集内,当我们还没有输掉游戏时,代码的主体继续:

        state = np.reshape(observation, [1, environment_dimension])
        prediction = model_predictions.predict([state])[0]
        action = np.random.choice(range(environment.action_space.n), p=prediction)
        states = np.vstack([states, state])
        actions = np.vstack([actions, action])

        observation, reward, done, info = environment.step(action)
        reward_sum += reward
        rewards = np.vstack([rewards, reward])

从第一章给出的示例文件来看,代码的开头应该是读者熟悉的;但是,也有一些细微的区别。我们在这里定义了一个观察变量,它是每个实验开始时环境的初始化状态。模型产生的预测是概率。我们在这里采取的具体行动是我们可以采取的可能行动的随机样本。然后,状态和动作被附加到一个向量上,我们以后会用到这个向量。像往常一样,我们然后在给定的环境中执行一个动作,产生新的观察结果、当前的奖励,以及我们在该环境中是失败了还是仍然成功的指示。这个过程一直持续到我们输掉游戏,这就把我们带到了“calculated_discounted_reward()”函数。

什么是折扣奖励,我们为什么要使用它们?

如前所述,策略梯度方法的目的是利用基于梯度的优化来选择一组行动,在给定目标的环境中实现最佳结果。我们将在给定状态下可以采取的行动的概率分布定义如下:

其中 π =策略, θ =参数, a =动作, s = 状态

由于这是一个基于梯度的优化问题,我们还想定义成本函数,由下式给出:

上面的等式就是保单得分函数,也就是我们选择的保单的预期/平均回报。因为这是一个基于情节的任务,我们建议用户计算整个情节的折扣奖励。下面的等式给出了如何计算的示例:

其中 k =一集的步数, G =总折扣奖励, γ =折扣调参, R =奖励, V =值。

calculate_discounted_reward()函数为产生的每个给定奖励提供一个折扣奖励向量,然后反转该向量,如下所示:

def calculate_discounted_reward(reward, gamma=gamma):
output = [reward[i] * gamma**i for i in range(0, len(reward))]
return output[::-1]

给定一些调整参数的值,我们在每一步中提高到不同的幂,我们对奖励打折扣,奖励是给定我们在该步骤中采取的行动从环境中产生的。然后,我们对折扣后的奖励向量进行平均,从而得出该集的成本函数输出。

discounted_rewards -= discounted_rewards.mean()
discounted_rewards /= discounted_rewards.std()
discounted_rewards = discounted_rewards.squeeze()

读者将观察到我们对“discounted_rewards”向量执行的以下转换。对于不了解的读者来说,np.array.squeeze()函数采用一个包含多个元素的数组,并将它们连接起来,使得以下情况成立:

[[1, 2], [2, 3]] -> [1, 2, 2, 3]

折扣奖励背后的推理相当简单,因为通过折扣奖励,我们使原本无限的总和变得有限。如果我们不打折扣的奖励,这些奖励的总和将无限增长,因此我们将无法收敛到一个最佳的解决方案。

我们如何计算分数?

在我们的代码中,我们特别利用了“score_model()”函数,该函数使用训练好的模型运行用户指定次数的试验,以得出这些试验次数的平均分数。这允许我们从广义上来看模型是如何表现的,而不是看一个由于偶然因素模型可能表现得更好的试验。我们的得分函数也可以定义如下:

其中 R ( τ ) =预期未来报酬。

这是如何实现的相当简单;但是,让我们解释如下所示的 score_model()函数:

def score_model(model, n_tests, render=render):
(code redacted, please see github)
            state = np.reshape(observation, [1, environment_dimension])
            predict = model.predict([state])[0]
            action = np.argmax(predict)
            observation, reward, done, _ = environment.step(action)
            reward_sum += reward
            if done:
                break
        scores.append(reward_sum)
    environment.close()
    return np.mean(scores)

您会发现,我们不会在每次想要对模型进行评分时都呈现环境标准。我向读者推荐这一点,因为这除了相对缺乏信息之外,还会显著减慢训练过程。如果你真的想渲染这个模型,那么只有当你有了一个你认为已经达到给定问题的基准的模型时,你才应该这么做。

在这个函数中,我们传递一个模型,这个模型是我们之前批量训练的。该模型是专门利用各州及其相应的折扣奖励,以及我们在每个州采取的相应行动来训练的。直观地说,我们正试图训练一个模型,通过在每次迭代中选择一个随机的行动,在预测如何预测会导致一组特定奖励的行动方面变得更加准确。因此,权重将优化,以始终如一地产生给予州的奖励。随着时间的推移,这将产生一个模型,当给定一个特定的状态时,它将理解为了产生一个给定的奖励它将具体做什么。因此,按照问题的框架,我们将最终产生一个模型,该模型将产生我们的得分阈值,因为权重被优化以正确地对状态进行分类,从而随着时间的推移最大化我们的得分。

与所有梯度下降/上升问题一样,我们必须对目标函数进行微分,以便我们可以计算梯度,从而利用梯度来优化权重。因为我们要对概率函数进行微分,所以建议我们使用对数(这就是为什么我们对在 neural_networks/models.py 中后端定义的误差函数使用对数似然损失)。让我们来看看一个似然函数与该函数的对数似然的曲线图(图 2-2 和 2-3 )。

img/480225_1_En_2_Fig3_HTML.jpg

图 2-3

对数似然函数

img/480225_1_En_2_Fig2_HTML.jpg

图 2-2

似然函数

得分函数的导数由下式给出:

(2.8)

由于利用了梯度上升,我们最有可能将参数朝最大化环境产生的回报的方向移动。

在我们用批量训练更新了参数之后,我们必须将状态、动作和奖励向量重新初始化为空。为了总结 cart_pole_game()函数所做的工作,在详细讨论了这一点之后,流程如下:

  1. 初始化变量,这些变量将通过在各自的状态下与环境交互来填充。

  2. 在给定的情节中,执行动作直到游戏失败。给定一个状态,使用模型预测最佳行动。附加状态、在这些状态中采取的行动以及在该状态中产生的奖励。

  3. 计算折扣奖励,然后使用这些奖励训练一批状态,行动和奖励。

  4. 对训练好的模型进行评分,并重复,直到收敛到用户确定的性能阈值。

随着我们的代码被充分解释,我们现在可以执行它并观察结果。当用户执行代码时,应该会看到如图 2-4 和 2-5 所示的结果。

img/480225_1_En_2_Fig5_HTML.jpg

图 2-5

政策梯度问题的误差图

img/480225_1_En_2_Fig4_HTML.jpg

图 2-4

策略梯度问题的输出示例

这个特定的解决方案在多次实验中收敛到大约 5000–6000 集,我们的目标是 190 集。我们现在已经完成了一个情节问题的例子和一个离散问题空间的例子。现在我们知道了我们可以利用普通政策梯度的问题类型,那么在什么情况下我们不能利用政策梯度呢?

政策梯度的缺点

在这个阶段,对强化学习的一个更大的批评是政策梯度和强化学习的抽样效率。采样效率是指我们的算法能够通过仅使用产生最重要的学习信息的状态来更快地学习的程度。具体来说,政策梯度不区分在一个事件中采取的单个行动。也就是说,如果我们在一个事件中采取的行动导致了很高的回报,即使这些行动的一些子集非常次优,我们也可以得出结论,这些行动都是好的。我们只能通过通常迭代非最优策略来学习如何选择最优策略。重要的抽样调查缓解了这一问题;然而,这是一种在非策略学习中使用的技术,我们将在后面讨论。然而,这一缺点并不仅限于政策梯度。除此之外,策略梯度可能倾向于收敛于局部最大值,而不是像许多基于梯度下降的方法那样收敛于全局最大值。这也增加了训练合适模型的难度。为了解决其中的一些问题,我们可以选择在更细粒度的级别上进行更新,而不是像前面所示的在普通策略梯度中采用的零星方案。这就引出了我们的下一个话题,近似策略优化。

近似政策优化(PPO)和行动者批评模型

PPO 专门处理陷入局部最大值的政策梯度趋势,通过对目标函数施加惩罚,然后在这个新改革的梯度下降上利用梯度下降。使得该等式看起来如下:

(2.9)

其中 β =调谐参数,KL = KL 散度, =优势函数。

这种适应性惩罚背后的基本直觉是,我们利用新旧策略之间的 KL 差异,这将在一集内的每次迭代中改变。如果 KL 散度的值高于目标值 δ ,我们缩小调谐参数。然而,如果它低于目标值 δ ,我们扩展我们愿意搜索不同参数的区域。添加惩罚的好处是,它确保我们搜索参数来定义策略的区域明显更小,并基于正确程度在比情节更细粒度的级别上进行调整。这样的话,一个情节中的不好的行为将会直接受到惩罚,而不是被其他可能是好的决定平均对待。这种逐步而非阶段性的变化是行动者-批评家模型的关键组成部分,也是 PPO 的基础。在这种情况下,与 KL 散度相关的调整参数是政策作为参与者的批评家模型。

优势函数将是演员-评论家模型的关键组成部分,我们利用它来代替算法决策过程的价值函数。这里的推理是因为价值函数具有高度可变性,而优势函数更明显是凸函数。梯度优化背后的直觉是,我们的参数将在优势函数大于 0 的方向上优化,并将远离梯度小于 0 的参数选择。接下来,我们定义将使用的优势函数,而不是价值函数:

其中 Q ( sa ) =状态 s 中动作 a 的 Q 值, V ( s ) =状态 s 的平均值

行动者-批评家模型分为两种策略:(1)行动者优势批评家(A2C)和(2)异步优势行动者-批评家(A3C)。这两种算法都像我们简要描述的演员-评论家模型一样工作;然而,唯一的区别是 A3C 不同时(在每次迭代结束时)更新每个 actor 的全局参数,因此有了异步描述。在这种情况下,A2C 的训练会更快。

然而,让我们更仔细地检查这个算法,将它应用到一个比 cart pole,Super Mario Bros 稍微难一点的游戏中,并且更直接地求解这个解。

实现 PPO,解决超级马里奥兄弟。

对于这个模型,我们将利用我创建的一些包中提供的代码以及开源库。虽然游戏是可以改变的,但是用户也应该可以自由地尝试利用其他问题来解决这个问题。由于与 A3C 相关的培训时间,我将利用 A2C。除此之外,我将简要地向用户介绍如何设置用于培训的 Google Cloud 实例,这是任何像这样基于强化学习的任务的推荐方法。

超级马里奥兄弟概述。

超级马里奥兄弟(图 2-6 )是一款相对简单但经典的视频游戏,它允许用户看到强化学习的力量,而不会增加我们将在本书稍后的其他视频游戏环境中看到的一些复杂性。玩家有许多可以利用的动作,列在 https://github.com/Kautenja/gym-super-mario-bros/blob/master/gym_super_mario_bros/actions.py 中。

img/480225_1_En_2_Fig6_HTML.jpg

图 2-6

超级马里奥兄弟。屏幕截图

每个关卡的目标都是一样的:我们试图避开所有的障碍和敌人,这样我们就可以在最后摸到旗杆来赢得关卡。旗杆将永远在关卡的最右端,尽管我们可以获得蘑菇和短暂无敌等其他奖励,但这些都不是首要目标。对于这个例子,我们不会特别担心大多数用户的单独目标,即到达旗帜,因为这可能很难训练一个模型,并且只是一个额外的奖励。

安装环境包

对于这个特定的环境,我们鼓励用户使用 gym-super-mario-bros,可以使用以下命令安装它:

pip3 install gym-super-mario-bros

超级马里奥兄弟不是健身房套餐里提供的标准环境,所以需要创造一个环境。幸运的是,这个开源包完成了这项任务,所以我们可以专注于这个问题的模型架构。这次我们将直接使用 Tensorflow,而不是 Keras,但是将从“neural_networks/models.py”目录中访问一个类。

存储库中代码的结构

与前面的例子不同,从这一点开始,读者应该预料到他们将需要引用模型架构,因为它是在存储库中的不同文件中定义的,例如在“neural_neworks”和“algorithms”目录下。在这个具体的例子中,代码的结构如下:

  • A2C 演员-评论家模型在“models.py”中被定义为一个类。

  • “algorithms/actor _ critic _ utilities”包含 Model 和 Runner 类。包括 ActorCriticModel 在内的这些都是在这个文件中定义的 learn_policy()函数中实例化的。这是大部分计算将在其中结束的函数。

这些类和函数取自 OpenAI 发布的基线库,并稍作修改。这背后的原因是,与其手动操作,不如让读者理解为什么如何这些模型的工作原理,而不是简单地调用它们。因此,让我们首先讨论我们正在使用的模型及其原因。

模型架构

对于这个问题,我们将把它作为一个图像识别问题来处理。因此,我们将使用简单的 LeNet 架构,这是一种卷积神经网络架构。这些设备因图像识别而广受欢迎,最初是由 Yann LeCun 在 20 世纪 80 年代末开发的。图 2-7 显示了典型的 LeNet 架构。

img/480225_1_En_2_Fig7_HTML.jpg

图 2-7

LeNet 体系结构

我们将把每一帧视为一幅图片,对该帧进行卷积以创建特征图,然后不断减少这些特征图的维数,直到我们达到 softmax 编码的输出向量,我们将从该向量中随机选择动作,然后最终以与我们在之前的普通策略梯度示例中相同的方式对该批进行训练。读者现在将会看到详细描述 ActorCriticModel()类的代码,我们已经创建了这个类,它包含了模型架构和相关的属性:

        self.distribution_type = make_pdtype(action_space)
        height, weight, channel = environment.shape
        environment_shape = (height, weight, channel)
        inputs_ = tf.placeholder(tf.float32, [None, environment_shape], name="input")

        self.distribution_type = make_pdtype(action_space)
        height, weight, channel = environment.shape
        environment_shape = (height, weight, channel)
        inputs_ = tf.placeholder(tf.float32, [None, environment_shape], name="input")
        scaled_images = tf.cast(inputs_, tf.float32)/float(255)

        layer1 = tf.layers.batch_normalization(convolution_layer(inputs=scaled_images,
filters=32,
kernel_size=8,
strides=4,
gain=np.sqrt(2)))

(code continued later)

在我们谈论超级马里奥兄弟级别的演员-评论家模型的实现之前,让我们简单地讨论一下我们应该做什么来预处理我们的图像数据,以及它需要如何通过 CNN。图像通常为 256 位,包含 3 个维度。当我们将图像处理成 python 矩阵时,这意味着最初产生的矩阵应该是 m x n x 3 的维度,其中 m 和 n 分别是长度和宽度,矩阵的每个维度代表一个颜色通道。具体来说,我们通常期望颜色通道代表红色、绿色和蓝色。在超级马里奥的例子中,我们期望矩阵如图 2-8 所示。

img/480225_1_En_2_Fig8_HTML.jpg

图 2-8

超级马里奥图像矩阵示例(预处理前)

为了最初降低图像的复杂性,我们将对它们进行灰度化,使得最初的三维矩阵变成一维矩阵。256 位中的每一位都代表颜色的亮度,1 代表黑色,256 代表白色。因为 python 数据结构是按 0 索引的,所以 255 是上限,同样,这也是我们缩放输入图像的依据。既然我们已经关注了如何对数据进行预处理,接下来我们将讨论第一个卷积层。

读者会注意到,我们在这里制作的图层利用了一个函数,该函数在 Tensorflow 固有的卷积图层函数周围使用了一个辅助函数。除此之外,我们在每个卷积层上利用 batch_normalization()。如前所述,我们将创建的特征地图会越来越小。从理论上讲,剩余的数据是对分类目的最有用的像素。现在,我们继续前进,直到我们将所有的特征图展平成一个数组,然后我们用它来计算 V ( s )。该函数的输出以及其他重要值被定义为属性,我们将在该模型的训练过程中调用这些属性。从 ActorCriticModel 开始,让我们讨论 Model()类,其代码如下所示:

class Model(object):
    def __init__(self, policy_model, observation_space, action_space, n_environments,
                 n_steps, entropy_coefficient, value_coefficient, max_grad_norm):

(code redacted, please see github)
        train_model = policy_model(session, observation_space, action_space, n_environments*n_steps, n_steps, reuse=True)
        error_rate = tf.nn.sparse_softmax_cross_entropy_with_logits(logits=train_model.logits, labels=actions_)
        mean_squared_error = tf.reduce_mean(advantages_ * error_rate)

        value_loss = tf.reduce_mean(mse(tf.squeeze(train_model.value_function) ,rewards_))
        entropy = tf.reduce_mean(train_model.distribution_type.entropy())
        loss = mean_squared_error - entropy * entropy_coefficient + value_loss * value_coefficient

(code continued later)

在代码中,我们从“policy_model()”开始,它实际上是我们前面讨论过的 ActorCriticModel()类。在它被实例化并通过这个类之后,我们从单个迭代中获取错误率,就像它在 Model()类中发生一样。读者立即看到的应该是熟悉的利用 Tensorflow 的标准神经网络训练。接下来,让我们检查一下 Runner()类

class Runner(AbstractEnvRunner):

    def __init__(self, environment, model, nsteps, total_timesteps, gamma, _lambda):
        super().__init__(environment=environment, model=model, n_steps=n_steps)

        self.gamma = gamma
        self._lambda = _lambda
        self.total_timesteps = total_timesteps

    def run(self):
        _observations, _actions, _rewards, _values, _dones = [],[],[],[],[]

        for _ in range(self.n_steps):
            actions, values = self.model.step(self.obs, self.dones)
            _observations.append(np.copy(self.observations))
            _actions.append(actions)
            _values.append(values)
            _dones.append(self.dones)
            self.observations[:], rewards, self.dones, _ = self.environment.step(actions)
            _rewards.append(rewards)

(code continued later!)

读者会注意到,我们已经在前面的代码部分定义了一些我们在上一个例子中看到的变量。具体来说,我们定义伽玛,它将被用作一个折扣因子。同样,与网络使用较大值相比,梯度下降使用较小的梯度来优化权重要容易得多。当我们通过在这个环境中允许我们采取的最大数量的步骤进行每个迭代时,我们附加到观察、动作、值、奖励和布尔项,该布尔项确定我们是否已经失败或者仍然在播放当前的剧集。

(code redacted, please see Github)
delta = _rewards[t] + self.gamma * nextvalues * nextnonterminal - _values[t]
            _advantages[t] = last_lambda = delta + self.gamma * self._lambda * nextnonterminal * last_lambda

        _returns = _advantages + _values
        return map(swap_flatten_axes, (_observations, _actions, _returns, _values))

在代码中,我们移动到函数的末尾,在那里我们计算 delta,或者每个单独的步骤之间关于回报、lambda、回报等的差异。这最终将我们引向“train_model()”函数,如下所示:

    model = ActorCriticModel(policy=policy,
           obsevration_space=observation_space,
           action_space=action_space,
           n_environments=n_environments,
n_steps=n+steps,
entropy_coefficient=entropy_coefficient,
value_coefficient=value_coefficient,
max_grad_norm=max_grad_norm)

    model.load("./models/260/model.ckpt")

    runner = Runner(environment,
                    model=model,
                    n_steps=n_steps,
                    n_timesteps=n_timesteps,
                    gamma=gamma,
                    _lambda=_lambda)

 (code redacted please see github)

由于读者已经了解了这些函数,现在,给定我们在文件头和 train_model()函数中定义的超参数,它们将被实例化。从这一点来看,读者看到的过程应该反映了前一个例子中关于训练模型的过程。既然我们已经给出了这个例子的适当概述,那么让我们讨论一下尝试训练这样一个模型的挑战以及我们观察到的结果。

应对更困难的强化学习挑战

推车极点问题和 RL 中的其他经典控制问题相对容易,因为无论选择哪种方法都不会花费过多的时间来收敛到最优解。然而,对于更抽象的问题,尤其是类似于这个例子的问题,任务的训练时间会呈指数增长。例如,A2C 和 A3C 的一些实现已经被应用到刺猬索尼克身上,但它们在 10 个小时后仍然不能完成一个关卡。尽管这个例子中的复杂性在《超级马里奥兄弟》中并不存在,但同样的一点应该牢记在心。因此,对于这样的问题,我们需要使用云解决方案。虽然我们将在稍后讨论 AWS 以及如何使用它,但我认为读者学习其他框架也很重要。正因为如此,我们将与谷歌云合作。作为一个额外的奖励,他们仍然给新用户免费积分,这将使使用这些代码变得非常容易。

任何数据科学家或机器学习工程师都将达到这样一个点,即他们想要制作的解决方案应该利用云资源进行生产和实验。AWS 和 Google Cloud 是读者应该熟悉的两个解决方案,它们不仅会让读者认识到将代码投入生产是有意义的。图 2-9 给出了一个谷歌云仪表板的例子。

img/480225_1_En_2_Fig9_HTML.jpg

图 2-9

谷歌云仪表板示例

读者应该预料到,当点击 SSH 图标时,他们将加载一个(这里假设是 Linux)终端,这将需要一些标准安装(安装 Git、不同的 python 包等)。).用户在这里做的任何事情都不会与他们在本地机器上做的事情有太大的不同;然而,假设您使用的是 Linux,那么会有一些语法差异。

本节的重要部分是理解您应该在云资源上而不是在您的本地机器上培训这样的解决方案。

现在让我们看看实际运行游戏本身的主函数:

def play_super_mario(policy_model=ActorCriticModel, environment=environment):
    (code redacted, please see github)
        observations = environment.reset()
        score, n_step, done = 0, 0, False

        while done == False:

            actions, values = model.step(observations)
            import pdb; pdb.set_trace()

            for action in actions:

                observations, rewards, done, info = environment.step(action)
                score += rewards
                environment.render()
  n_step += 1

        print('Step: %s \nScore: %s '%(n_step, score))
        environment.close()

有了这最后一段代码,我们已经回顾了所有必要的类。这里我们应该讨论的最后一部分是顺利实现培训流程。为此,我建议用户熟悉 docker。

Dockerizing 强化学习实验

当你在训练一个强化学习代理时,你可能不想坐在那里盯着代理通过优化它的策略来熟悉它自己的环境,而且在你用来训练它的许多小时里,你肯定仍然需要你的计算机。因此,这就是我们利用云资源的原因。然而,仅仅在云环境中运行应用程序是不够的。在 AWS 或 Google Cloud 上,如果你不在后台运行这个过程,连接就会中断,原因可能是你的电脑死机、死机等。,您将丢失所有的进度,并且必须从最后一个检查点或者从头开始,这取决于您是否修改了代码以沿着某些检查点保存。因此,使用 docker 容器很重要。

Docker 容器是一个有趣的解决方案,它允许您为从终端运行的应用程序创建一个虚拟环境。简而言之,您可以创建一个虚拟“实例”,快速启动您的应用程序,并从这个虚拟环境中运行它。另一个额外的好处是,docker 包括几个命令,可以帮助您运行这样的进程,并在进程停止时重新启动它。在我们在这里执行的任务的上下文中,一旦我们认为我们已经训练了我们的代理足够长的时间,我们可以终止该过程,之后检查我们的代理的进度,然后如果我们认为有必要,返回训练。首先,让我们看一个 Docker 文件的例子。

img/480225_1_En_2_Fig10_HTML.jpg

图 2-10

Docker 文件示例

图 2-10 是一个虚拟 Docker 文件,我们在其中看到了三个命令,我们将对其进行回顾。具体来说,它们是“从”、“复制”和“运行”“FROM”是我们定义的 python 版本,我们希望这个容器在这个版本中运行。尽管本书中有一些使用 python2 的例子,但所有的例子都应该与 python3 兼容,并且 python2 在 2020 年后将不再受支持。接下来,“COPY”表示存储库中我们想要使用的特定文件。最后,我们开始“运行”,在那里我们专门安装我们需要的 python 包。

值得注意的是,当实例化一个新容器时,必须在 docker 文件中指明所有必需的文件、存储库和 python 模块。如果您不这样做,您的 docker 容器将无法执行代码。

我们通常使用以下命令创建一个容器:

"sudo docker –t build . [container name] . "

假设 docker 已经安装,并且我们正在复制的文件没有丢失,这应该会创建一个指定名称的 docker 容器。在这一步之后,建议用户运行以下命令来启动该文件。

"sudo docker run --dit --restart-unless-stopped python3 –m path.to.file"

实验结果

这主要是为了说明的目的;然而,在解决更困难的强化学习问题时,强调这一点是很有用的—**你必须花大量时间训练你的代理。**与一些更普通的机器学习例子不同,更类似于利用深度学习的困难的自然语言处理问题,训练将需要很长时间才能有效。在这种特殊的情况下,代理通常会耗尽时间,因为它会在相对较早的时候卡在一些障碍物上,如管道,或者它会运气不佳,相对较快地被像古姆巴这样的敌方战斗人员杀死。当代理人被训练了 5 个小时,我们一般观察到它的表现明显更好,最具体的通知是,它现在能够避免死于任何敌人的空间。然而,它确实会被障碍物困住,而且如果它被卡住了,也不太可能原路返回去寻找替代的前进路径。最成功的代理人是那些接受过 12 小时以上培训的人;然而,这个解决方案通常还没有完成,也不一定是完美的。代理的大部分成功似乎往往取决于它在关键点采取的行动,特别是适当的时间跳跃,它倾向于避免杀死敌人,因为它更喜欢尝试不落入关卡的漏洞。在某些场合,这让马里奥赢了;然而,需要注意的是,这是游戏最简单的关卡之一。

结论

在本章之后,读者应该能够自如地应用一些基本的和一种更高级的强化学习算法,这些算法基于情节和时间差分方法。本章的关键要点如下:

  • 了解你正在处理的问题类型-与大多数机器学习问题类似,不同类型的数据有不同的模型。你正在处理一个大的状态空间吗?你的任务是阶段性的吗?如果不是,你真的想要/需要将算法的学习建立在更精细的步骤上吗?在你着手解决之前,花点时间考虑这些问题。

  • 在困难问题上训练 RL 解决方案是耗时的,所以在云资源上训练-类似于一些高级 NLP 问题,读者会观察到本地机器不是训练模型的地方。尽管在本地机器上编写大部分代码显然是有意义的,但是还是要在其他地方使用它们。

随着第一类算法的完成,我们将继续处理不同的基于价值的方法,如 Q 学习和深度 Q 学习。在接下来的章节中,我们将再次采用同样的先例,先处理一个更简单的问题,然后在一个更大的环境中处理一个更复杂的问题。**

三、强化学习算法:Q 学习及其变体

随着对政策梯度和行动者-批评家模型的初步讨论结束,我们现在可以讨论读者可能会发现有用的替代深度学习算法。具体来说,我们将讨论 Q 学习、深度 Q 学习以及深度确定性策略梯度。一旦我们涵盖了这些,我们将足够精通,开始处理更抽象的问题,更具体的领域,这将教用户如何处理不同任务的强化学习。

q 学习

q 学习是无模型学习算法家族的一部分,它通过观察所有可能的动作并评估它们中的每一个来学习策略。在这个算法中,有两个矩阵我们会经常引用:Q 矩阵和 R 矩阵。前者代表与算法同名的算法,包含我们在其中实现策略的环境的累积知识。这个矩阵中的所有条目都被初始化为 0,目标是使产生的奖励最大化。在环境中的每一步,Q 矩阵被更新。R 矩阵是这样一个环境,每行代表一个州,每列代表调到另一个州的奖励。该矩阵的结构类似于相关矩阵,其中每行和每列索引相互镜像。我们在图 3-1 和 3-2 中有一个 Q 和 R 矩阵的可视化。

img/480225_1_En_3_Fig2_HTML.jpg

图 3-2

R 表的可视化

img/480225_1_En_3_Fig1_HTML.jpg

图 3-1

Q 表的可视化

代理可以看到 R 表中它可以采取的即时操作,但看不到其他任何内容。由于这个限制,这正是 Q 表变得重要的地方。前面提到的 Q 表包含了在给定时间段内它所填充的关于环境的所有累积信息。在某种意义上,我们可以把 Q 表想象成地图,把 R 表想象成世界。具体来说,Q 表是如何被更新的由下面给出:

其中Q(stat)=单元格条目, α =学习率, γ =折扣因子,max {Q(st+1a

时间差异学习

在引言一章中,我们简要地谈到了马尔可夫决策过程的主题。更具体地说,MDP 指的是部分随机但也依赖于决策者或受决策者控制的事件。我们将 MDP 定义为以下 4 元组:

其中 S =表示状态的集合, A =表示允许动作的集合, P a =在时间 t 处于状态 s 的动作 a 导致在时间 t + 1 处于状态 s ʹ,ra

*提醒一下,图 3-3 是马尔可夫决策过程的一个例子。

img/480225_1_En_3_Fig3_HTML.jpg

图 3-3

马尔可夫决策过程

正如我们之前所说的,大多数强化学习都围绕着这样的状态,从这些状态中,我们可以执行产生回报的行动。我们试图达到的目标是为决策者选择最优的政策,使产生的回报最大化。我们在引言中简单地提到了时间差异学习,但是现在是详细讨论这个问题的合适时机。

TD 学习被广泛地描述为一种预测依赖于特定信号的未来值的量的方法。它指的是在不同时间步长上预测的“时间差”。TD 学习被设计成使得在当前时间步长的预测被更新为,以便下一个时间步长的后续预测是正确的。q 学习本身就是 TD 学习的一个例子。我们可以解决 TD 学习问题的一种方法是ε贪婪算法,特别是在这里。

ε-贪婪算法

最终,在大量迭代之后,Q 表足够好,可以被代理直接利用。为了达到这一点,我们希望 Q 学习算法利用表中的信息少于它探索的信息。这就是通常所说的勘探-开采权衡,它由ε参数控制。这里的关键是,可能用来达成解决方案的第一条可能路径并不一定是最佳路径。有鉴于此,如果我们不断探索,就不可能总会找到比目前更好的解决办法,因此我们放弃解决问题。为了缓解这个问题,建议使用ε-贪婪算法。

ε-贪婪算法属于多臂土匪问题家族。这被描述为一个问题,我们必须在各种选项中做出选择,最终目标是最大化回报。说明这个问题的经典例子是想象一个赌场,我们有四台机器,每台机器都有不同的未知回报概率。我们将一个伯努利多臂土匪描述为一个集合动作和奖励分别表示在元组< AR >中,其中有 K 个机器,奖励概率为{θ1、…、θK}。每个动作对应于与相应吃角子老虎机的互动,奖励是随机的,因为它们将以概率Q(at)返回,否则为 0。预期回报由以下等式表示:

我们的目标是通过选择最佳行动来最大化累积回报,其中最佳回报概率和损失函数分别由以下等式给出:

虽然有多种方法来解决多臂土匪问题,我们将在这里集中讨论策略。这是一种算法,通过下面的等式来估计动作的质量:img/480225_1_En_3_Figa_HTML.jpg

其中Nt(a)=动作 a 被执行的次数,img/480225_1_En_3_Figb_HTML.jpg =二进制指示函数。

如果ϵ很小,那么我们将探索我们周围的环境。然而,除此之外,我们将采取目前已知的最佳行动。为了说明 Q 学习算法的整体,我们将学习玩一个叫做“冰封湖”的游戏

用 Q 学习解决的冰湖

冰封湖是健身房提供的游戏,玩家试图训练一个代理人从湖的起点走到另一个终点。然而,并不是所有的冰面都是冰冻的,踩上去会导致我们输掉比赛。除了达成目标,我们不接受任何奖励。读者可以想象环境看起来像下图(图 3-4 )。

img/480225_1_En_3_Fig4_HTML.jpg

图 3-4

冰冻湖泊环境

与我们编写的大多数其他文件类似,我们从定义以后可以使用的参数以及环境开始。两个主要函数 populate_q_matrix()和 play_frozen_lake()包含了前面定义的许多辅助函数。让我们从遍历填充 Q 矩阵的函数开始。

def populate_q_table(render=False, n_episodes=n_episodes):

(documentation redacted, please see github)

    for episode in range(n_episodes):
        prior_state = environment.reset()
        _ = 0
        while _ < max_steps:
            if render == True: environment.render()
            action = exploit_explore(prior_state)
            observation, reward, done, info = environment.step(action)

            update_q_matrix(prior_state=prior_state,
                            observation=observation,
                            reward=reward,
                            action=action)
      (CODE TO BE CONTINUED)

浏览代码直到第二个帮助函数 update_q_matrix(),我们看到我们定义了许多剧集,我们将在这些剧集上填充 Q 表。读者可以多加或少加几集,看看表现如何变化,但这里我们选了一万集。我们现在来看第一个助手函数 exploit_explore()。不言而喻,这是执行ε-贪婪探索算法的算法,以确定我们应该采取这两个动作中的哪一个。以下函数对此进行了详细描述。

def exploit_explore(prior_state, epsilon=epsilon):
(documentation redacted, please read github)
    if np.random.uniform(0, 1) < epsilon:
        return environment.action_space.sample()
    else:
        return np.argmax(Q_matrix[prior_state, :])

正如读者可以看到的,我们只探索了一个随机动作,在这个例子中,我们从均匀分布中随机抽取的值是 0。否则,我们会选择给定状态下我们所知道的最佳可能行动。在更大的函数体中向前移动,继续我们在前面的例子中让代理在环境中执行一个动作。这就产生了差异;然而,现在我们必须更新 Q 矩阵。

def update_q_matrix(prior_state, observation, reward, action):
prediction = Q_matrix[prior_state, action]
    actual_label = reward + gamma * np.max(Q_matrix[observation, :])
    Q_matrix[prior_state, action] = Q_matrix[prior_state, action] + learning_rate*(actual_label - prediction)

根据前面的等式,我们更新 Q 矩阵的条目,其中每一列表示要采取的动作,每一行表示不同的状态。我们在每一集里继续这个过程,直到我们达到允许的最大步数,或者掉进冰里。一旦我们达到了最大集数,我们就可以用 Q 表玩游戏了。读者应该观察游戏在终端运行时会出现如图 3-5 所示的画面。

img/480225_1_En_3_Fig5_HTML.jpg

图 3-5

冰湖游戏

当你在某一集赢或输时,终端会输出信息。我们通常使用参数进行多次实验观察,假设代理通常会在 10 集内赢得两到三次,并在大约 20-30 步内达成解决方案。

在某种程度上,Q 学习的主要优点是它不需要模型,并且算法相当透明。很容易解释为什么在给定的时间状态下代理人会选择一个动作。也就是说,这样做的主要缺点是,如果我们要用信息充分填充 Q 矩阵,当我们处理非常大的环境时,获得在给定状态下做什么的知识所需的经验在计算上是非常昂贵的。虽然这个冰冻湖的例子相当有限,但是像更复杂的视频游戏这样的环境可能需要特别长的时间才能得到一个好的 Q 表。为了克服这一限制,设计了深度 Q 学习。

深度学习

深度 Q 学习来自 Q 学习,相当简单,因为这两种方法之间唯一的真正区别是 DQL 近似 Q 表中的值,而不是试图手动填充它们。精确地说,这是如何实现的是ε-贪婪搜索(或替代算法)和动作结果之间的联系。epsilon-greedy 搜索算法解决了我们如何决定是利用还是探索的问题,并且我们反过来基于该状态下的动作值来更新 Q 矩阵。从这个意义上说,我们可以看到,我们希望将达到目标和采取行动之间的损失降至最低。在这个意义上,我们现在有了可以利用梯度下降的东西,它被表示为下面的等式:

其中 μ =行为策略, θ =神经网络参数。

目标标签和 Q 矩阵都由两个独立的神经网络预测。目标网络共享 Q 网络的权重和偏差,但是它们在 Q 网络之后被更新。然而,接下来,让我们讨论一下体验回放的重要性,以及我们在这里如何利用它。如果我们在强化学习的环境中引入全新的数据,神经网络将会覆盖权重。因此,这就是为什么经常有不同的模型被训练用于不同的目的。体验回放是我们如何通过存储观察到的体验来利用它们,然后这有助于减少我们可能观察到的体验之间的相关性。实际上,我们将本章开始时介绍的元组保存在内存中。在训练期间,我们将使用元组计算目标标签,然后应用梯度下降,以便我们具有将在整个环境上很好地推广的权重和偏差。然而,接下来,让我们现在尝试使用深度 Q 学习来解决一个问题,并看看我们的问题的复杂性如何发生了显著变化。

用深度 Q 学习玩毁灭战士

利用 DQL 的一个经典例子是原始的 Doom 视频游戏,如图 3-6 所示,它的环境也是测试各种机器学习算法的一个极好的环境。Doom 是一款第一人称射击游戏,玩家必须在一个三维环境中与敌人战斗。因为这是一个更老的 3D 游戏,玩家在环境中移动的方式与我们许多理论上的代理在 Q 矩阵中移动的方式相同。这将是我们应用强化学习的第一个连续控制问题。

img/480225_1_En_3_Fig6_HTML.jpg

图 3-6

厄运中的一个关卡的例子

简单地说,我们把连续控制系统和离散控制系统区分开来,前者的变量和参数是连续的,后者是离散的。强化学习环境下的连续过程的一个例子是驾驶汽车或教机器人走路。离散控制过程的一个例子是我们处理的第一个问题,车杆,以及“经典控制”中的其他问题,如摆动钟摆。尽管为了理解算法,有许多离散的任务值得分析,但许多用强化学习实现的任务是连续的。这与状态空间的巨大规模一起,使其成为深度 Q 学习的绝佳候选。我们将通过观察简单水平与更困难水平的差异以及算法性能的差异来解决这个问题。

具体到游戏本身,目标相当简单。我们必须在没有死亡的情况下完成关卡,这显然需要在关卡结束前杀死敌方战斗人员。大多数敌人会先发制人地报复,因此算法将主要集中在如何基于此做出反应的训练上。概括地说,我们将通过该算法执行的两个主要过程是:( 1)对环境进行采样并将经验存储在 MDP 元组中;( 2)选择其中的一些作为批量训练示例。让我们首先讨论除了我们将利用什么类型的模型架构之外,我们将如何为这个模型预处理我们的数据。

class DeepQNetwork():

    def __init__(self, n_units, n_classes, n_filters, stride, kernel, state_size, action_size, learning_rate):
    (code redacted, please see github)
        self.input_matrix = tf.placeholder(tf.float32, [None, *state_size])
        self.actions = tf.placeholder(tf.float32, [None])
        self.target_Q = tf.placeholder(tf.float32, [None, *state_size])

        self.network1 = convolution_layer(inputs=self.input_matrix,
                                     filters=self.n_filters,
                                     kernel_size=self.kernel,
                                     strides=self.stride,
                                     activation='elu')

(code redacted please see github)

类似于之前我们定义为图的 TensorFlow 图,我们将从定义几个特定的属性开始。这些将在后面的 doom_example.py 中的“play_doom()”函数中使用,但是我们将在后面解决这些问题。接下来,我们可以看到,类似于我们在《超级马里奥兄弟》中使用的示例,我们将希望使用 LeNet 架构,只是在这种情况下,我们将利用一个接受四维的层,因为我们正在攻击帧。类似地,我们最终将特征地图展平成一个数组,然后通过一个完全连接的 softmax 层输出。从这个 softmax 层,我们将在训练中采样我们的动作。图 3-7 显示了我们将用于 Deep Q 网络的模型架构示例。

img/480225_1_En_3_Fig7_HTML.jpg

图 3-7

深度 Q 网络的示例架构

回到讨论输入数据,在前面的例子中,我们没有堆叠帧,而是原样传递当前和先前状态,作为输入数据的重新格式化矩阵。为什么这很重要,尤其是在三维环境中,背后的原因是因为它让深度 Q 网络了解代理正在诱导的运动。这个方法是 Deep Mind 提出来的。我们通过以下函数对帧进行预处理和堆叠:

def preprocess_frame(frame):
    cropped_frame = frame[30:-10,30:-30]
    normalized_frame = cropped_frame/float(255)
    preprocessed_frame = transform.resize(normalized_frame, [84,84])
    return preprocessed_frame

我们首先利用灰度图像开始,感谢 vizdoom 库以这种形式提供给我们。如果这不是灰度级,用户应该利用像 OpenCV 这样的库来执行预处理。接下来,我们将像素值再次缩放 255 倍,就像我们在超级马里奥的例子中所做的一样,理由也是一样的。然而,一个小的不同是,在这个初始的例子中,我们将裁剪掉框架的顶部,因为《毁灭战士》中的天花板只是为了营造气氛,并不包含任何值得评估的东西。当我们堆叠帧时,我们在这里利用前面的函数:

def stack_frames(stacked_frames, state, new_episode, stack_size=4):

    frame = preprocess_frame(state)

    if new_episode == True:

        stacked_frames = deque([np.zeros((84,84), dtype=np.int) for i in range(stack_size)], maxlen=4)
        for i in range(4):
            stacked_frames.append(frame)

        stacked_state = np.stack(stacked_frames, axis=2)

    else:

        stacked_frames.append(frame)
        stacked_state = np.stack(stacked_frames, axis=2)

    return stacked_state, stacked_frames

与将帧转换为四个堆栈的函数不同,这个函数的重要之处在于它是如何精确地发生的。当第一次调用这个函数时,我们取前四帧。接下来,我们添加最新的帧,同时删除最后一个帧,这样这个过程应该代表一个先进后出(FILO)过程。然而,要记住的是,这个过程不是很现实,因为人类不会看到多个交错的帧,而是一次看到所有帧。除此之外,由于用于存储这些堆叠图像的存储器,这使得训练明显更加困难。当我们在接下来的章节中学习不同的例子时,用户应该记住这一点。接下来,我们将利用稍微复杂一点的贪婪ε策略,其中我们还将利用衰减率,如以下函数所示:

def exploit_explore(session, model, explore_start, explore_stop, decay_rate, decay_step, state, actions):
    exp_exp_tradeoff = np.random.rand()
    explore_probability = explore_stop + (explore_start - explore_stop) * np.exp(-decay_rate * decay_step)

    if (explore_probability > exp_exp_tradeoff):
        action = random.choice(possible_actions)
    else:
        Qs = session.run(model.output, feed_dict = {model.input_matrix: state.reshape((1, * state.shape))})
        choice = np.argmax(Qs)
        action = possible_actions[int(choice)]

这背后的想法是贪婪ε策略本质上与我们在原始 Q 学习示例中看到的相同,除了衰减是指数的,因为随着时间的推移,我们越来越有可能探索更少的内容,迫使算法利用其积累的知识。现在,在解释了助手函数之后,让我们来看一下实际用于训练模型的函数。事不宜迟,让我们来观察在这个级别上训练模型的结果。然后,我们将移动到一个不同的水平,看看模型如何执行。

简单毁灭级别

在这个场景中,玩家处于一个简单的环境中,他们可以向左、向右移动,和/或向敌方战斗人员射击。这个敌方战斗人员不会还击,只是偶尔向左或向右移动。运行代码时,读者应该会看到如图 3-8 和 3-9 所示的输出和脚本。

img/480225_1_En_3_Fig9_HTML.jpg

图 3-9

简单厄运环境示例

img/480225_1_En_3_Fig8_HTML.jpg

图 3-8

培训模式截图

培训和绩效

图 3-10 显示了在不同事件中训练 Q 矩阵的结果以及样本外结果。

img/480225_1_En_3_Fig10_HTML.jpg

图 3-10

训练时深 Q 网络分数

读者应该知道,像这样的任务,正如我们所说的,由于使用了预处理和计算,是相当大的内存密集型任务。除此之外,当神经网络陷入局部最优时,有时它不能恰当地学习采取正确的行动。虽然列出的参数通常产生可接受的样本外解决方案,但有时该神经网络表现不佳。这是局限性之一。

**## 深度 Q 学习的局限性

正如我们之前所展示的,深度 Q 学习并不是没有缺点。但是,除了这个例子之外,这些低效的地方在哪里呢?1993 年,巴斯蒂安·特龙和安东·施瓦茨在他们的论文使用函数逼近进行强化学习中对此进行了更具体的研究。他们发现,深度 Q 网络经常因为高估而学习到非常高的动作值。根据设计,这是由于下面给出的目标标签公式:

在这个等式中,我们可以看到,我们总是选择当时的最大已知值,这可以优先选择我们的网络在这些值可能高得不切实际的阶段学习这些值。这就是函数逼近会导致高估的具体原因。高估,正如这里可能发生的那样,会导致糟糕的政策,并倾向于导致模型中的偏差。正如在毁灭战士的例子中所表现的那样,代理人经常感到被迫射击,而不管它相对于敌人的位置。这个问题能解决的有多精确?

双 Q 学习和双深度 Q 网络

如前面等式中所强调的,在给定环境状态的情况下,max 运算符使用相同的值来选择和评估动作。准确地说,当我们把它分成两个独立的过程(选择和评估)时,我们得到了双 Q 学习。双 Q 学习利用两个价值函数,每个价值函数有两个相应的权重集。其中一个权重集用于确定贪婪利用或探索权衡问题,另一个用于确定给定动作的价值。然后,我们将目标近似值改写如下:

至此,我们可以讨论双 Q 网络以及如何利用它们来克服深 Q 网络的缺点。我们不是添加额外的模型,而是利用目标网络来估计价值,同时利用在线网络来评估勘探-开发决策过程。双 Q 网络的目标函数如下:

结论

Q 学习和深度 Q 学习的例子都完成了,我们建议读者尝试在各种上下文中应用这些算法。在必要的地方,他们可以更改参数并派生/更改现有的代码和模型。无论如何,我建议读者在前进的道路上谨记以下几点:

  • **Q 学习简单明了且易于解释—**该算法的好处是易于理解为什么 Q 值是这样输入的。对于实现算法需要透明性的任务,考虑这样的事情并不是不明智的。

  • Q 学习对大状态空间有限制!–虽然前面的评论适用于简单的问题,但重要的是要认识到,在像 Doom 示例和更复杂的环境中,Vanilla Q Learning 将花费大量的时间来完成。

  • **深度 Q 学习还能陷入局部最优!–**像其他强化学习算法一样,DQN 仍然可以找到局部最优策略,但不能找到全局最优策略。从训练的角度来看,找到这种全局最优可能是彻底的。

  • 尝试实现双 Q 学习和双深度 Q 网络!–Q 学习和 dqn 的局限性已经被越来越先进的技术快速克服。这个起点应该允许您从头开始尝试实现最先进的算法。

完成这些例子后,让我们继续讨论一些我们还没有涉及的其他强化学习算法,并深入讨论这些算法。***

四、通过强化学习做市

除了攻击强化学习中的一些标准问题,因为它们在许多书中都可以找到,作为一个例子,看看答案既不客观也没有完全解决的领域是很好的。在金融领域,尤其是强化学习领域,做市是最好的例子之一。我们将讨论学科本身,提出一些不基于机器学习的基线方法,然后测试几种基于强化学习的方法。

什么是做市?

在金融市场中,利用交易所的人们对流动性有着持续的需求。在任何给定的时刻,试图出售资产的每个人的订单都与想要购买的人完全匹配,这是不可能的。因此,做市商在促进通常希望在不同持续时间长度的金融工具(多头或空头)中持有头寸的人的订单的执行方面起着至关重要的作用。通常,做市被描述为金融市场中的人们能够在金融市场中持续赚钱的少数方式之一,这与押注风险更高但有条件获得更高回报的押注模式相反。现在,让我们试着理解我们正在处理的数据是什么,以及我们可以期待什么。图 4-1 是带有相关订单的订单簿的样本图像。

img/480225_1_En_4_Fig1_HTML.jpg

图 4-1

订单簿示例

图 4-1 是位于订单簿两边的订单示例,代表出价和要价。当有人向交易所发送订单并使用限价订单时,他们试图出售的数量将留在订单簿上,直到订单被执行。虽然交易所专用的履行算法可以从一个到另一个变化,但是它们通常寻求履行它们被接收的订单,使得最近的订单是最后被履行的订单。利用限价单的好处是,它们可以大大减少所谓的“市场影响”简单地说,每当有人想购买大量的东西,它就向市场的其他人发出信号,表明需求很大。这意味着我们可以影响自己获得最佳价格和完成订单的能力。正因为如此,交易者经常分散他们的限价单,以尽可能掩盖他们的意图。

为了给出一个更具体的做市商的例子,让我们假设我们是某个金融交易所,它有大量的客户,这些客户通常希望交换每一笔大额订单。然而,并非所有这些订单都是均匀分布的,每个想买的人都有另一个想卖的大客户。因此,我们决定通过提供非常优惠的费率来激励做市商,以提供流动性,从而促进这些大客户的订单。交易所越能吸引做市商,从而带来更多的流动性,通常交易所对想要交易的人就越有利,特别是机构买家。

做市的基本思想是,有人通常愿意以任何给定的价格购买和/或出售一种工具,这样随着时间的推移,他们的策略会为他们带来回报。做市的主要吸引力在于,一旦确定了可扩展的成功策略,它的有效期通常会比对冲基金和其他交易平台可能采用的传统方向模型长得多。除此之外,与做市相关的风险更低。话虽如此,做市在实际意义上的主要困难在于,根据市场的不同,做市可能需要大量资金。尽管如此,我们将利用强化学习来尝试开发一种更直观的策略开发方式,而不是尝试进行更传统的定量金融研究。

交易健身房

类似于 OpenAI gym,以及我们用来玩各种视频游戏如超级马里奥兄弟末日的那个包的衍生产品,这里的读者将会使用 Trading Gym。这是一个开源项目,其目标是使在交易环境中应用强化学习算法变得容易。在图 4-2 中,你可以看到在环境渲染时通常应该显示的绘图。

img/480225_1_En_4_Fig2_HTML.jpg

图 4-2

交易馆可视化

在这种环境下,读者通常有三种选择:

  1. 购买乐器

  2. 出售乐器

  3. 担任当前职务

Trading Gym 通常允许您处理一个(或多个)产品/金融工具,其中数据的格式为(bid_product1,ask_product1,bid_product2,ask_product2)。我们将出价定义为个人购买产品的最佳可能价格,将要价定义为销售产品的最佳可能价格。我们将向读者介绍如何将他们自己的订单簿数据导入到环境中,但在此之前,让我们首先讨论我们试图解决的问题,并看看解决该问题的更确定的方法。

为什么要针对这个问题进行强化学习?

尽管从交易体操中看不出这一点,但所有的做市都需要使用限价单才能有效。由于完成订单所需的流动性几乎总是有保证的(低于一定额度),市场订单的不利之处在于,交易所通常会收取相当高的费用。因此,利用做市策略的唯一方法是在限价订单簿上下单,并允许它们被执行。如上所述,这会引入如下几个问题:

  1. 应该买什么价位的?

  2. 我应该卖什么价格?

  3. 应该持什么价位?

在机器学习的背景下,所有这些问题都不容易回答。具体来说,我们活动的空间是连续的。如前所述,市场在不断变化,我们在市场上的行为本身会使我们的订单更难兑现或无法兑现。因此,普通的机器学习方法不会考虑这些环境因素,除非我们将它们作为特征包括在内。即便如此,除非我们事先对市场做了大量的研究,否则很难尝试对这些方面进行编码。其次,大多数机器学习方法可能是无效的,因为这是一个时间序列任务。唯一合适的方法是循环神经网络(RNN),特别是因为这项任务的粒度,我们将不得不预测相当数量的序列。这将导致一个模型,在这个模型中,我们持有头寸的平均时间比我们在做市环境中希望的时间长得多。我们想要敏捷性和灵活性,而使用机器学习方法可能会迫使我们在预定的时间内持有头寸,而不是根据市场环境在对我们最有利的时候退出头寸。所有这些原因都证明了基于强化学习的方法是正确的。让我们继续描述代码,以及我们如何创建一个合理的例子。下面是将执行该函数的代码示例:

memory = Memory(max_size=memory_size)

environment = SpreadTrading(spread_coefficients=[1],
                            data_generator=generator,
                            trading_fee=trading_fee,
                            time_fee=time_fee,
history_length=history_length)

state_size = len(environment.reset())

在我们进一步讨论之前,有几个重要的属性是我们为 SpreadTrading()类定义的,我们应该浏览一下。其中一些相当简单,因为金融市场上的所有交易都要花钱在普通交易所进行,所以我们必须设定一个费用。在我们使用的第一个示例中,将合成交易数据,第二个示例将使用真实的订单簿数据。我们将收取象征性的费用,不对应任何特定的交流。我们将 time_fee 设置为 0,因为应该没有成本。然而,最重要的是,我们应该讨论 DataGenerator 类及其作用。

利用交易平台综合订单数据

使用 trading gym 时,我们可以选择直接处理订单数据,也可以综合我们自己的数据。首先,我们将使用 WavySignal 函数,如下所示:

class WavySignal(DataGenerator):
    def _generator(period_1, period_2, epsilon, ba_spread=0):
        i = 0
        while True:
            i += 1
            bid_price = (1 - epsilon) * np.sin(2 * i * np.pi / period_1) + \
                epsilon * np.sin(2 * i * np.pi / period_2)
            yield bid_price, bid_price + ba_spread

对于那些不熟悉生成器函数的人来说,它们通常用于我们需要遍历大量数据的情况,我们已经预先确定了应该从哪里读取这些数据;然而,考虑到我们正在寻找的应用程序的性质,在内存中存储这些数据会太大。相反,对象被存储。然而,向前看,这个生成器将根据前面的逻辑生成假数据。在生成器工作的情况下,我们使用以下命令运行该文件:

"pythonw –m chapter4.market_making_example"

我们应该观察到类似于图 4-3 中的输出。

img/480225_1_En_4_Fig3_HTML.jpg

图 4-3

WavySignal 数据发生器的输出

在这个特殊的例子中,我们利用深度 Q 网络来解决这个问题。正如我们所见,DQN 更喜欢开发环境,而不是强调勘探。除了积累的知识之外,这也让我们获得了比之前更高的分数。因为这是合成数据,所以没有必要继续分析这个问题。这有助于我们将重点放在训练和选择算法上。然而,在现实环境中,我们显然希望解决问题,以便找出我们可以在现实生活场景中部署的解决方案。

使用 Trading Gym 生成订单簿数据

在这种环境下,我们有两个选择:(1)使用假数据或(2)使用真实的市场数据。除了让自己熟悉环境是如何运作的,我不认为假数据有多大效用。因此,我们将开始利用真实数据。这将我们带到了 CSVStreamer()类,如下所示:

class CSVStreamer(DataGenerator):
def _generator(filename, header=False):
        with open(filename, "r") as csvfile:
            reader = csv.reader(csvfile)
            if header:
                next(reader, None)
            for row in reader:
                #assert len(row) % 2 == 0
                yield np.array(row, dtype=np.float)

(code redacted, please see tgym github!)

CSVStreamer 类本质上可以用 _generator()函数来概括,我们在前面的代码中已经展示过了。它只是浏览文件中的每一行,假设第一列是出价,第二列是要价。读者可以从 LOBSTER 下载数据,从而获得不同的订单数据,或者从彭博等供应商那里购买这些数据。可以通过以下 URL 访问这个库: https://lobsterdata.com/

这显然是相当昂贵的,所以它应该留给那些有大量研究预算和/或在已经有彭博终端可用的机构工作的人。我们将在这个例子中使用的“生成器”变量是加载这个存储库中包含的订单簿数据的 CSVStreamer。接下来,让我们从检查本例中执行大部分计算的函数开始:

def train_model(model, environment):
(code redacted, please see github)
            while step < max_steps:

                step += 1; decay_step += 1

                action, explore_probability = exploit_explore(...)
                state, reward, done, info = environment.step(action)

类似于我们在前一章展示的厄运的例子,大部分代码最终都是同质和相似的。我们将以与前面相同的方式迭代环境,除了这里,我们将集中比较多种方法的性能,并评估我们应该使用哪一种。

试验设计

虽然做市商使用什么算法很少公开,但一般来说,我们希望使用一套简单的规则。下面的算法将形成我们的控制组,以及对为什么基于规则的系统优于随机生成的选择集的基本理解。与其他实验一样,控制组的目的是让我们将模型的结果与它进行比较,看看我们是否超过了控制组设定的基准。这套新的方法将组成实验组。我们将根据以下标准评估算法是否成功:

  • 整体报酬

  • 整个实验的平均回报

事不宜迟,让我们讨论一下我们是如何得出对照组/基线算法的。下面列出了我们两个策略的要求:

策略 1(实验组)

  • 随机选择所有选项。

策略二(对照组)

  • 随机选择买入,持有,卖出。

  • 如果头寸很长,出售资产。

  • 如果头寸不足,购买资产。

  • 如果我们持有现金头寸,随机选择一个选项。

这两种策略的代码都将由 baseline_model()函数执行,如下所示:

def baseline_model(n_actions, info, random=True):

    if random == True:
        action = np.random.choice(range(n_actions), p=np.repeat(1/float(n_actions), 3))
        action = possible_actions[action]

    else:

        if len(info) == 0:
            action = np.random.choice(range(n_actions), p=np.repeat(1/float(n_actions), 3))
            action = possible_actions[action]

        elif info['action'] == 'sell':
            action = buy

        else:
            action = sell

    return action

读者现在应该已经熟悉了“info”字典,它显示了来自相关环境的信息。在 Trading Gym 中,信息字典显示最近的动作。如果我们持有现金,字典将是空的。如果我们有一个未结头寸,它会在“操作”键下显示“买入”或“卖出”,有时会显示在我们没有持有现金的情况下最近一次操作的利润。在前面的实验中,我们将在 1000 次试验中重复 100 个单独的交易。最后,在我们训练完我们的模型后,我们将重复同样的方案并比较结果。利用这两种策略,我们从实验中获得了以下结果:

  • 策略 1 平均奖励–30,890

  • 策略 2 平均奖励–62,777

我们有以下与这些实验相关的分布和数据(图 4-4 和 4-5 )。

img/480225_1_En_4_Fig5_HTML.jpg

图 4-5

通过算法选择答案的分数分布

img/480225_1_En_4_Fig4_HTML.jpg

图 4-4

随机选择行动的分数分布

正如之前的 1000 次试验所显示的,当我们的决策背后有一些合理的逻辑时,我们选择的奖励会比在所有这些试验中随机选择行动产生更好的结果。因此,按照这种逻辑,如果我们找到一个模型,能够最优地选择这些结果,而不是像我们之前那样只采用简单的启发式方法,那么我们应该能够进一步提高我们的产量。考虑到这种方法,让我们采取我们提出的解决方案。

RL 方法 1:政策梯度

虽然普通的策略梯度确实有其缺点,但是允许我们轻松地迭代选择的决策数量相对有限。这个空间的负面影响是,我们可能没有捕捉到状态空间的连续元素。既然如此,我们有一个需要解决的紧迫问题,那就是损失函数。当我们第一次使用策略梯度时,我们只有两个类,并且在一个离散的样本空间中操作。因此,我们能够利用对数似然损失。然而,在这个例子中,我们有多个类,并且在一个连续的空间中操作。这些都是我们应该意识到的挑战,我们将在后面研究其结果。

对于这个例子,我们将使用分类交叉熵损失函数以及另一个自定义损失函数。前者源于 Keras,通常用于包含两个以上类的分类方案中。

当我们运行前面设计的实验时,这个实例中的结果都非常糟糕。在许多不同的参数和不同的风格中,利用政策梯度是很不明智的。考虑到这一点,让我们试试深度 Q 网络。

RL 方法 2:深度 Q 网络

对于这个例子,就我们如何构建问题而言,Q 学习肯定是一个极好的选择,但深度 Q 学习最终应该是我们选择的方法。这背后的原因是,状态空间,特别是当考虑到众多的选项时,可能相当大。当我们运行这部分功能时,我们应该注意到类似于图 4-6 的输出。

img/480225_1_En_4_Fig6_HTML.jpg

图 4-6

培训 DQL 模型的示例截图

在几次迭代的训练中,考虑到我们拥有的数据量,我观察到超过一集的训练基本上是不可取的。尽管如此,有时取得的结果非常不一致。在一些迭代中,我观察到结果非常好,模型的一些结果根本不选择任何动作,而一些动作。有几次,我观察到,在这种情况下,做市算法在训练中表现相当好,但这些结果既不稳定也不一致。绝大多数情况下,我注意到我所建议的模型通常表现不佳,并且经常做出不受欢迎的决策。但是,接下来,让我们看看重复样本外实验时的结果(图 4-7 )。

img/480225_1_En_4_Fig7_HTML.jpg

图 4-7

奖励分数分布

上述结果不仅大大优于基线,而且显著优于政策梯度模型,使其成为显而易见的选择。平均奖励为 34,286,348,这绝对是一个可行的解决方案。正如我们在图 4-7 中看到的,我们的分数是令人满意的,我们似乎有一个双峰分布。

结果和讨论

在回顾了所有的结果之后,有理由声明读者既不应该使用深度 Q 学习算法,也不应该使用策略梯度。总之,我们提出这一建议的原因如下:

  • **超过基线算法—**为了证明任何实验方法的合理性,我们必须超过基线。值得检查的是,我们是否适当地对数据进行了采样,或者是否有足够的数据用于这一特殊情况。

  • **有些算法赔钱了—**对我们在这里采用的第一种方法的最客观的批评是,它没有实现其业务目标,即制定一个有利可图的战略。在商业环境中使用这种算法是不可取的,并且最终会超出理论上可行的范围,我们必须选择实际可行的方法。

未来可能的解决方案是阅读这一领域的一些现有文献,尝试解决这个问题。也就是说,许多公开发表的论文同样遇到了算法要么暂时盈利,要么最终不盈利的问题。读者也应该在他们自己的时间内随意尝试和应用 ActorCritic 方法,但是也应该大胆尝试其他现有的解决方案,尝试不同的参数、费用结构和这里没有提到的对策略的不同约束。强化学习研究的困难在于,奖励函数的设计是一个抽象的过程,但也就是说,它是设计好实验的关键组成部分。

结论

下面的例子完成后,我们现在已经到了第一章的末尾,在这一章中,我们从头开始处理强化学习问题,并尝试改进现有的方法。本章的一些关键要点是试图创建一个可部署的解决方案的困难,但向读者提出一个框架,并展示我们如何在样本中连续达到更好的结果,每次都表明我们离答案越来越近。在此之前,我们处理的许多问题都是相对简单或经典的例子,它们的价值在于能够透明地展示算法的力量。现在我们终于到了这个话题的难点,那就是学习推动各种解决方案。对于那些直接从事研究或工业的人来说,这个过程应该很熟悉。如果不是,我强烈建议你开始实现它。也就是说,我们将进入最后一章,在这里我们将重复这一过程,但在一个全新的环境中,我们将引导读者从头开始创建自己的 OpenAI 健身房环境,以便他们可以开始自己进行研究!

五、定制开放式强化学习环境

对于我们的最后一章,我们将关注 OpenAI 的 gym 包,但更重要的是试图理解我们如何创建自己的定制环境,以便我们可以处理更多的典型用例。这一章的大部分内容将集中在我对 OpenAI 编程实践的建议,以及我将如何编写该软件大部分内容的建议上。最后,在我们完成创建环境之后,我们将继续关注解决问题。对于这个实例,我们将集中于尝试创建和解决一个新的视频游戏。

刺猬索尼克概述

对于那些不熟悉的人来说,刺猬索尼克(图 5-1 )是另一款经典游戏,通常被认为是超级马里奥兄弟的对手。游戏的概念是你扮演一只刺猬,从关卡的一边跑到另一边,目的是躲避或杀死敌人并收集戒指。如果玩家受到攻击,他们会失去所有的戒指。如果他们被 0 环攻击,他们会失去一条命。如果他们失去了所有的生命,游戏结束。我们现在不会关注任何有 boss 战的关卡,而是关注一个简单的入门关卡(1 级)。因为它与这个任务有关,我们的目标将是训练代理人成功地通过关卡而不死亡。

img/480225_1_En_5_Fig1_HTML.jpg

图 5-1

刺猬索尼克截图

下载游戏

首先,用户需要创建一个 Steam 帐户,然后将 Steam 下载到他们的本地机器上,如果他们还没有这样做的话。对于那些不熟悉的人来说,Steam 是一种游戏流媒体服务,允许玩家购买和租赁游戏,而不必获得特定的主机。在这种情况下,我们将购买刺猬索尼克(4.99 美元)。用户下载游戏后,一旦登录 Steam 桌面客户端,应该会看到以下屏幕(图 5-2 )。

img/480225_1_En_5_Fig2_HTML.jpg

图 5-2

蒸汽仪表板

安装游戏后,读者应该会看到播放按钮,表明初步设置已经完成。然而,有一些锅炉板,我们需要做的复古图书馆,我们将带领读者通过现在。Retro 是一个专门处理旧视频游戏并使它们与 OpenAI 兼容的库。这将解决我们可能会遇到的许多繁重的工作,并使过程变得更加简单。不管怎样,让我们相应地下载我们需要的文件。首先,用户应该在这个 URL 下载并克隆这个库: https://github.com/openai/retro

克隆这个存储库之后,我们需要创建一个虚拟环境。对于那些不熟悉的人来说,虚拟环境是一种创建特定 python 安装的独立实例以及与之相关的相对依赖性的方式。这样做的好处是,对于孤立的任务或项目,我们可以创建只包含它们所使用的依赖项的 python 安装。一旦安装了 virtualenv,我们可以通过在 bash 终端中输入以下命令来实例化它:

"sudo mkdir virtual_environments && cd virtual_environments"
"virtualenv [environment name]/python3 –m venv [environment name]"

这些命令分别创建虚拟环境目录,将 cd 放入其中,然后创建虚拟环境。完成之后,用户应该用 cd 进入本地克隆的 retro 库所在的目录。之后,他们应该键入以下命令:

"python –m retro.import.sega_classics"

该命令将 sega_classics.py 文件下的游戏各自的 rom 写入我们的本地环境。ROM 指的是只读存储器,通常在这种情况下指的是存储视频游戏的存储器,视频游戏通常通过盒式磁带分发,这是光盘和 DVD 出现之前的标准。现在我们已经下载了游戏及其各自的 rom,让我们继续学习如何使用 retro 和 python 创建自定义环境。

为环境编写代码

当回顾《超级马里奥兄弟》和《毁灭战士》的例子时,读者可以参考这样一个事实,即我们使用了一个利用了一些相同技术的自定义库。首先,让我们分析第五章/create_environment.py 中的功能,并详细描述每个功能的作用。首先,让我们看看如下所示的身体功能:

def create_new_environment(environment_index, n_frames=4):

 (code redacted, please see github!)
    print(dictionary[environment_index]['game'])
    print(dictionary[environment_index]['state'])

    environment = make(game=dictionary[environment_index]['game'],
    state=dictionary[environment_index]['state'],
    bk2dir="./records")

    environment = ActionsDiscretizer(environment)
    environment = RewardScaler(environment)
    environment = PreprocessFrame(environment)
    environment = FrameStack(environment, n_frames)
    environment = AllowBacktracking(environment)
    return environment

创建环境的过程相当简单,我们将参数从“retro_contest”模块传递给 make()函数。这创建了一个环境,然后我们从各种功能中添加结构,直到我们最终返回我们定制和格式化的环境。然而,首先让我们谈谈我们环境中最重要的一个方面,那就是创造和定义我们可以在其中采取的行动。

class PreprocessFrame(gym.ObservationWrapper):

def __init__(self, environment, width, height):

        gym.ObservationWrapper.__init__(self, environment)
        self.width = width
        self.height = height
        self.observation_space = gym.spaces.Box(low=0,

high=255,

shape=(self.height, self.width, 1),

dtype=np.uint8)

    def observation(self, image):
        image = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)
        image = cv2.resize(image, (self.width, self.height), interpolation=cv2.INTER_AREA)
        image = image[:, :, None]
        return image

就像我们在处理 2D 或 3D 视频游戏时遇到的大多数问题一样,我们本质上是在处理一个计算机视觉问题的排列。因此,我们需要从预处理图像开始,这样我们可以减少输入大小或我们将使用的神经网络(或其他方法),然后返回灰度图像的单个一维矩阵。对于前几章的读者来说,其中的大部分应该是熟悉的,但是对于后代,我们从实例化 PreprocessFrame()类开始,该类首先接受 ObservationWrapper 作为其唯一的参数。读者已经在前面的每个例子中使用过这种方法,OpenAI Gym 源代码证明了这一点,如下所示:

class ObservationWrapper(Wrapper):

    def reset(self, **kwargs):
        observation = self.env.reset(**kwargs)
        return self.observation(observation)

    def step(self, action):
        observation, reward, done, info = self.env.step(action)
        return self.observation(observation), reward, done, info

    def observation(self, observation):
        raise NotImplementedError

这是库的核心,我们在这里步进、重置并生成环境的当前状态。回到 PreprocessFrame()类,我们首先定义要输出的图像的环境、宽度和高度。从这三个论点,我们也定义了观察空间,我们将有能力操纵我们的代理人。为此,我们利用体育馆的 Box()类。这被简单地定义为 R n 中欧几里得空间的一个元素。在这种情况下,我们将该框的边界定义为 0 和 255,表示给定像素的白度,其中 0 表示完全没有白色(黑色),255 表示完全没有黑暗(白色)。observation()函数执行单个帧的实际灰度调整并输出,以便我们可以对其进行分析。接下来,让我们进入下一个类 ActionsDiscretizer()创建环境的核心部分。

class ActionsDiscretizer(gym.ActionWrapper):
def __init__(self, env):
        super(ActionsDiscretizer, self).__init__(env)
        buttons = ["B", "A", "MODE", "START", "UP", "DOWN", "LEFT", "RIGHT", "C", "Y", "X", "Z"]
        actions = [['LEFT'], ['RIGHT'], ['LEFT', 'DOWN'], ['RIGHT', 'DOWN'], ['DOWN'],
                   ['DOWN', 'B'], ['B']]
        self._actions = []

从类的实例化开始,读者应该直接看到按钮和动作数组。根据您是为键盘还是为特定的游戏控制台设计环境,按钮会有所不同。这些按钮对应于 Sega Genesis 控制器上所有可能的按钮。

也就是说,不是每个可能的动作都会映射到每个按钮,尤其是在这个版本的刺猬索尼克的情况下。尽管在游戏的更新版本中加入了某些高级功能,但最初的游戏相当标准,因为 Sonic 可以向左或向右行走/奔跑,并可以使用“B”按钮跳跃。接下来,让我们看看如何创造一个特定的行动空间。

      for action in actions:
            _actions = np.array([False] * len(buttons))
            for button in action:
                _actions[buttons.index(button)] = True
            self._actions.append(_actions)
        self.action_space = gym.spaces.Discrete(len(self._actions))

对于动作数组,我们遍历“actions”数组中的每个动作,然后创建一个名为“_actions”的新数组。这应该是一个尺寸为 1 x N 的数组,其中 N 是控制器上按钮的数量,每个索引都是 false。现在,对于动作中的每个按钮,我们希望将其映射到一个数组中,其中一些条目为假,另一些为真。最后,将其作为“self”变量的属性分配给“action_space”。我们已经在其他时候讨论过比例奖励,所以没有必要回顾这个函数。然而,我们应该讨论一个重要的功能,特别是在类似于这个的游戏/环境中。

class AllowBacktracking(gym.Wrapper):
def __init__(self, environment):
        super(AllowBacktracking, self).__init__(environment)
        self.curent_reward = 0
        self.max_reward = 0

    def reset(self, **kwargs):
        self.current_reward = 0
        self.max_reward = 0
        return self.env.reset(**kwargs)

AllowBacktracting()类相当简单,因为对于 2D 环境,我们最终必须通过后退到达该级别的末尾。尽管如此,有时,如果我们偶尔(无论多么微小)回溯我们的步骤,然后选择另一套行动,可能会有更好的道路可走。然而,我们不想鼓励奖励结构过多地这样做,所以我们将以下阶跃函数分配给环境:

    def step(self, action):
        observation, reward, done, info = self.environment.step(action)
        self.current_reward += reward
        reward = max(0, self.current_reward - self.max_reward)
        self.max_reward = max(self.max_reward, self.current_reward)
        return observation, reward, done, info

读者从这个函数中得到的重要信息是,我们将奖励值指定为 0 或 0 以上。在这种情况下,如果它导致一个可怜的回报,我们不会回去。完成所有的样板文件后,让我们继续讨论我们将具体使用什么模型以及为什么。

A3C 演员-评论家

读者会记得,当我们试图训练我们的代理人玩超级马里奥兄弟时,我们利用了这个模型;我们利用了优势演员-评论家模型,简称为 A2C。在图 5-3 中,我们可以看到一个 A3C 网络的可视化。

img/480225_1_En_5_Fig3_HTML.jpg

图 5-3

A3C 图

如前所述,行动者-批评家网络是有效的,因为我们能够使用价值函数来更新政策函数。我们可以逐步评估每个行动,然后相应地改变我们的策略,以获得比使用普通策略梯度更优化、更快速的结果,而不是等待一集结束,然后采取所有行动,而不管每个行动是好是坏。关于 A3C 与 A2C 的比较,A3C 往往不是最优的,因为我们正在训练多个彼此平行的代理,所有这些都是基于一些初始的全局参数。每个代理在探索环境时,将相应地更新参数,其他代理将根据这些参数进行更新。然而,不是所有的代理都会同时更新,因此这个问题是“异步”的。然而,让我们继续讨论我们的实现,因为它包含在 A3CModel()类中。

class A3CNetwork():

    def __init__(self, s_size, a_size, scope, trainer):
        (code redacted)

layer3 = tf.layers.flatten(inputs=layer3)

            output_layer = fully_connected_layer(inputs=layer3,
            units=512,

activation='softmax')

            outputs, cell_state, hidden_state = lstm_layer(input=hidden,

size=s_size,

actions=a_size,

apply_softmax=False)

类似于我们之前部署的 A2C 解决方案,我们首先通过卷积层传递预处理图像。这有助于我们降低维度,并从数据中去除噪声,如前所述。然而,我们将在这里介绍一个新的步骤,这在之前的示例中是没有的,该步骤将通过 LSTM 层传递数据。LSTM 是由 Sepp Hochreiter 和 Jürgen Schmidhuber 在 20 世纪 90 年代发明的模型,长短期记忆单位,或 LSTM。让我们首先想象一下这个模型的样子,如图 5-4 中的图片所示。

img/480225_1_En_5_Fig4_HTML.jpg

图 5-4

LSM 模型

LSTMs 在结构上的区别在于,我们将它们视为块或单元,而不是我们通常看到的神经网络的传统结构。也就是说,同样的原则通常也适用于此。然而,我们对之前讨论的普通 RNN 的隐藏状态进行了改进,我们将开始遍历与 LSTM 相关的公式:

(2.12)

(2.13)

(2.14)

(2.15)

(2.16)

其中 i t 是输入门, f t 是遗忘门, c t 是细胞状态, o t 是输出门, h t

首先,让我们注意模型的图表,特别是中间的 LSTM 单位,并理解与公式相关的方向流。首先,让我们讨论一下记谱法。由矩形img/480225_1_En_5_Figa_HTML.jpg表示的每个块代表一个神经网络层,我们通过它传递值。带箭头的水平线代表数据移动的向量和方向。数据在通过神经网络层之后,通常会被传递给逐点操作对象,用圆圈img/480225_1_En_5_Figb_HTML.jpg表示。在算法初始化时,隐藏和单元状态都被初始化为 0。从程序上讲,与 LSTM 层相关的大部分计算都发生在 Tensorflow 提供的“dynamic_rnn()”函数之下;但是,我们围绕此函数创建了一个体函数,其中前面的单元格、状态和关联变量定义如下:

def lstm_layer(input, size, actions, apply_softmax=False):
      input = tf.expand_dims(input, [0])
      lstm = tf.contrib.rnn.BasicLSTMCell(size, state_is_tuple=True)
      state_size = lstm.state_size
      step_size = tf.shape(input)[:1]
      cell_init = np.zeros((1, state_size.c), np.float32)
      hidden_init = np.zeros((1, state_size.h), np.float32)
      initial_state = [cell_init, hidden_init]
      cell_state = tf.placeholder(tf.float32, [1, state_size.c])
      hidden_state = tf.placeholder(tf.float32, [1, state_size.h])
      input_state = tf.contrib.rnn.LSTMStateTuple(cell_state, hidden_state)
      (code redacted, please see github!)

具体到何时何地使用 LSTM 模型,最常见的是将其应用于基于序列的任务,其中给定的输出依赖于一个以上的输入。例如拼写检查、翻译语言和预测时间序列等任务。因为它与这个特定的任务相关,所以我们对数据进行预处理,以便一次堆叠四个帧。这通常是为了尝试和模拟某种形式的运动,在这种情况下,我们根据先前的观察结果来确定可能采取的最佳行动。在这种情况下,为什么要应用 RNN 的原因很简单。虽然 LSTM 不是必要的,但我们认为向读者展示我们如何结合其他不同类型的机器学习模型来解决这个问题是有用的。接下来,让我们将注意力转回到 A3C 网络本身,并转向功能的后一部分。

            self.policy = slim.fully_connected(output_layer, a_size,
                activation_fn=tf.nn.softmax,
                weights_initializer=normalized_columns_initializer(0.01),
                biases_initializer=None)

            self.value = slim.fully_connected(rnn_out, 1,
                activation_fn=None,
                weights_initializer=normalized_columns_initializer(1.0),
                biases_initializer=None)

随着来自 LSTM 的输出的产生,我们通过一个完全连接的层来传递它,这样我们现在已经定义了我们的策略和值函数,我们将利用它们来生成一个输出矩阵。另外,读者应观察梯度的计算,并更新参数,使其各自相似。然而,正是这种模型的不同之处在于作品的异步性。现在,我们将遍历代码的最后一部分,我们可以称之为 main/master 函数。

    def play_sonic()
(code redacted, please see github!)
wiith tf.device("/cpu:0"):
        master_network = AC_Network(s_size,a_size,'global',None)
        num_workers = multiprocessing.cpu_count()
        workers = []

for i in range(num_workers):

            workers.append(Worker(environment=environment,
                                  name=i,
                                  s_size=s_size,
                                  a_sizse=a_size,
                                  trainer=trainer,
                                  saver=saver,
                                  model_path=model_path))

在下面的代码中,我们首先创建包含全局参数的主网络,并基于可用的 CPU 创建一些 workers。前面显示的方法将确保我们不会使用过多的内存而导致程序崩溃。然后,对于我们打算创建的每个工人,我们在实例化它们之后,将它们附加到一个数组中。然而,向前移动是计算的重要部分发生的地方。

             coord = tf.train.Coordinator()
sess.run(tf.global_variables_initializer())
worker_threads = []

            for worker in workers:

                worker_work = lambda: worker.work(max_episode_length=max_episode_length,
                gamma=gamma,
                master_network=master_network,
                sess=sess,
                coord=coord)

                _thread = threading.Thread(target=(worker_work))
                _thread.start()
                worker_threads.append(_thread)
            coord.join(worker_threads)

读者应该首先注意我们将使用的 tf.train.Coordinator()函数以及线程库。对于 A3C 的实现,理解我们在后端做什么来消除任何潜在的混乱是很重要的。对于那些没有意识到的人来说,线程是一个单独的执行流,这样多线程将允许您在不同的处理器上运行进程。我们通过向变量“_thread”传递一个函数来创建一个线程,在本例中是“worker_work”变量。这是由 worker.work()函数创建的,我们将其定义为以下代码体:

    def work(self,max_episode_length,gamma,sess,coord,saver):
        (code redacted, please see github!)
while self.env.is_episode_finished() == False:
action_dist, value_function,rnn_state = sess.run([self.local_AC.policy,
           self.local_AC.value,
self.local_AC.state_out]...)}

                    action = np.random.choice(action_dist[0], p=action_dist[0])
                    action = np.argmax(action_dist == action)

                    reward = self.env.make_action(self.actions[action]) / 100.0
                    done = self.env.is_episode_finished()
episode_buffer.append([prior_state, action, reward, current_state, done, value[0,0]])
                    episode_values.append(value[0,0])

我们首先通过执行计算图/A3C 模型来实例化几个变量。具体来说,在前面的代码部分中,我们希望从产生的分布中随机选择动作。从这一点来看,从前面的例子来看,其他一切都应该相对熟悉。我们在环境中执行一个动作,它应该产生一些回报和一个价值函数。然而,对于读者来说,新的是我们如何更新工人的主参数。这本身与多线程示例有联系,特别是与 coord.join()函数。理解了线程化以及它与我们所写的 A3C 实现的关系之后,我们终于可以讨论我们之前简单提到的 tf.train.Coodinator()函数了。一旦多个线程都终止了,该函数用于协调它们的终止。这是用 join()函数专门完成的,当我们希望一个线程等待另一个线程完成时,就使用这个函数。这将导致主线程暂停,等待另一个线程完成。 这个 正是 A3C 的异步本质在这个问题中体现出来的地方!

结论

当训练模型 10 小时时,我们观察到合理的性能;然而,一个持续存在的问题是音速需要经常绕着圆形路径运行的部分。因此,建议进行更多的培训。也就是说,最初我们已经注意到代理人击败或避免敌人的能力,以及在收集一些硬币时通过关卡的能力。尽管如此,这也揭示了这个问题的难度。

读者必须意识到强化学习的难度。虽然 RL 仍然是一个深入研究的领域,但是对于那些想要部署这些 for 解决方案的人来说,应该意识到特别是演员-评论家模型可能很难从头开始编写。我们甚至没有处理模型本身,而是花了大量的时间构建样板文件来处理环境。虽然这是一个简单的 2D 游戏,但有相当复杂的环境值得与工程师一起工作,他们完全专注于构建渲染和包装环境的工具。

就解决问题本身而言,花费大量的时间进行培训,而没有准确地指出应该如何解决问题,这可能导致大量的时间浪费。适当地构建你的问题,并准备尝试许多不同的方法,但在强化学习的实例中花费更多的时间来构建问题,而不是尝试不同的方法。此外,当你设计你的环境时,考虑你可以利用的不同的奖励结构。例如,在索尼克的例子中,你想要选择更多的戒指而不是更少,还是更喜欢从摧毁敌人中获得点数?显然,在死亡的情况下,这应该是产生最大的负面奖励,但在你看来死于索尼克从地图上掉下来是最糟糕的,还是死于被随机的敌人杀死更糟糕?所有这些考虑因素都会影响培训,但应该是在问题开始时就应该解决的高级问题。

我们鼓励读者利用这些示例中提供的代码,并在他们认为合适的地方对它们进行改进,可能是通过使某些实现在计算上更高效,以及在适合改进所提出的解决方案的地方。通过合作,我们可以解决难以置信的难题,共同推动这一领域的发展。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值