Python 深度强化学习教程(二)
五、函数近似
在前三章中,我们研究了规划和控制的各种方法,首先使用动态规划(DP),然后使用蒙特卡罗方法(MC),最后使用时间差分(TD)方法。在所有这些方法中,我们总是着眼于状态空间和动作都是离散的问题。只有在前一章快结束时,我们才谈到连续状态空间中的 Q 学习。我们使用任意方法离散化状态值,并训练一个学习模型。在这一章中,我们将通过讨论近似的理论基础以及它如何影响强化学习的设置来扩展这种方法。然后,我们将研究近似数值的各种方法,首先是具有良好理论基础的线性方法,然后是专门针对神经网络的非线性方法。深度学习与强化学习相结合的这一方面是最令人兴奋的发展,它将强化学习算法推向了规模。
通常,该方法将在预测/估计设置的上下文中查看一切,其中代理试图遵循给定的策略来学习状态值和/或动作值。接下来将讨论控制,即寻找最优策略。我们将继续处于一个没有模型的世界,在那里我们不知道转变的动力。然后我们将讨论函数近似世界中的收敛性和稳定性问题。到目前为止,在精确和离散的状态空间中,收敛性还不是一个大问题。然而,函数近似带来了新的问题,需要考虑的理论保证和实际的最佳做法。我们还将触及批处理方法,并将它们与本章第一部分讨论的增量学习方法进行比较。
我们将以深度学习、基础理论和使用 PyTorch 和 TensorFlow 构建/训练模型的基础的快速概述来结束这一章。
介绍
强化学习可用于解决具有许多离散状态配置的非常大的问题或具有连续状态空间的问题。考虑双陆棋游戏,它有接近 10 个 20 个离散状态,或者考虑围棋游戏,它有接近 10 个 170 个离散状态。也考虑像自动驾驶汽车、无人机或机器人这样的环境:这些都有一个连续的状态空间。
到目前为止,我们已经看到了状态空间离散且规模较小的问题,例如大约有 100 个状态的网格世界或有 500 个状态的出租车世界。我们如何将目前所学的算法扩展到更大的环境或具有连续状态空间的环境?一直以来,我们都是用一个表格来表示状态值 V ( s )或动作值 Q ( s , a ),每个状态值 s 或状态值 s 和动作值 a 的组合都有一个条目。随着数量的增加,表的大小将变得巨大,使得在表中存储状态或动作值变得不可行。此外,会有太多的组合,这会减慢策略的学习。该算法可能花费太多时间处于在环境的实际运行中概率非常低的状态。
我们现在将采取不同的方法。让我们用下面的函数来表示状态值(或状态-动作值):
(5.1)
它们现在由函数表示,而不是在表中表示值,其中参数 w 取决于代理遵循的策略,其中 s 或( s , a )是状态或状态值函数的输入。我们选择参数∣ * w * ∣的数量比状态∣ * s * ∣的数量或者状态-动作对的数量(∣s∣x∣a∣).)小得多这种方法的结果是存在状态-动作值的状态表示的一般化。当我们基于给定状态 s 的某个更新等式来更新权重向量 w 时,它不仅更新该特定状态 s 或( s , a )的值,而且还更新与更新所针对的原始状态或( s , a )接近的许多其他状态或状态动作的值这取决于函数的几何形状。如前所示, s 附近的其他状态值也将受到这种更新的影响。我们用一个比状态数更受限制的函数来近似这些值。具体来说,我们现在不是直接更新 v ( s )或 q ( s , a ),而是更新函数的参数集 w ,这反过来会影响估计值
。当然,像以前一样,我们使用 MC 或 TD 方法执行 w 更新。函数近似有多种方法。我们可以提供状态向量(表示状态的所有变量的值,例如位置、速度、地点等。)并得到
,或者我们可以输入状态和动作向量并得到
作为输出。在动作是离散的并且来自一个小集合的情况下,另一种非常占优势的方法是馈送状态向量 s 并且得到∣ * A * ∣数
,一个对应一个可能的动作(∣ * A * ∣表示可能动作的数量)。图 5-1 为示意图。
图 5-1
使用函数近似法表示或
的方法。第一个和最后一个是我们在本章中最常用的
有各种方法来构建这样的函数近似器,但我们将探索两种常见的方法:使用平铺的线性近似器和使用神经网络的非线性近似器。
然而,在我们这样做之前,我们需要重温理论基础,看看需要什么操作来使 w 移动,以便我们连续减少目标值和状态或状态动作值的当前估计值之间的误差, v ( s 或 q ( s , a )。
近似理论
函数近似是监督学习领域中广泛研究的主题,其中基于训练数据,我们构建底层模型的一般化。监督学习的大部分理论可以应用于函数近似的强化学习。然而,函数近似的 RL 也带来了新的问题,如如何引导以及它对非平稳性的影响。在监督学习中,当算法正在学习时,生成训练数据的问题/模型不会改变。然而,当涉及到函数近似的 RL 时,目标(在监督学习中标记为输出)的形成方式,会诱发非平稳性,我们需要想出新的方法来处理它。我们所说的非平稳是指我们不知道 v ( s )或 q ( s , a )的实际目标值。我们使用 MC 或 TD 方法来形成评估,然后使用这些评估作为“目标”当我们改进我们对目标值的估计时,我们使用修改后的估计作为新的目标。在监督学习中,情况就不同了;目标在培训期间给出并固定。学习算法对目标没有影响。在强化学习中,我们没有实际的目标,我们使用目标值的估计值。随着这些估计值的改变,学习算法中使用的目标也会改变;即,它们在学习期间不是固定的或静止的。
让我们重温一下 MC(方程 4.2)和 TD(方程 4.4)的更新方程,复制在这里。我们修改了等式,使 MC 和 TD 对当前时间使用相同的下标 t ,对下一时刻使用相同的下标 t + 1。两个等式执行相同的更新以将Vt(s)移动到更接近其目标,在 MC 更新的情况下是Gt(s),而Rt+1+γ∫
(5.2)
(5.3)
这和我们在监督学习,尤其是线性最小二乘回归中所做的事情类似。我们有输出值/目标值 y ( t ),我们有输入特征 x ( t ),统称为训练数据。我们可以选择一个模型模型w[x(t)】像多项式线性模型,决策树,或者支持向量,甚至像神经网络这样的其他非线性模型。训练数据用于最小化模型预测值和训练集中实际输出值之间的误差。称为最小化损失函数,表示如下:
(5.4)
当 J ( w )是一个可微函数时,这将是本书中的情况,我们可以使用梯度下降来调整模型的权重/参数 w ,以最小化误差/损失函数 J ( w )。通常,使用相同的训练数据分批多次进行更新,直到损失停止进一步减少。权重为 w 的模型现在已经学习了从输入 x ( t )到输出 y ( t )的底层映射。执行增量更新的方式在以下等式中给出:
j(w)wrtw∴=∇wj(w)
对于给定的损失函数:。
调整 w ,我们在∈wJ(w)的负方向迈一小步,这样会减少误差。
(5.5)
权重沿着使损失最小化的方向移动,即,实际输出值和预测输出值之间的差异。接下来,让我们花点时间讨论函数近似的各种方法。最常见的方法如下:
-
特征的线性组合。我们结合特征(如速度、速率、位置等。)通过向量 w 加权,并将计算值用作状态值。常见的方法如下:
-
多项式
-
傅立叶基函数
-
径向基函数
-
粗略编码
-
瓦片编码
-
-
非线性但可区分的方法,其中神经网络是最受欢迎和当前趋势的方法。
-
非参数的,基于记忆的方法。
在本书中,我们将主要讨论基于深度学习的神经网络方法,这些方法适合非结构化输入,如代理的视觉系统捕获的图像或使用自然语言处理(NLP)的自由文本。本章的后面部分和下一章将致力于使用基于深度学习的函数近似,我们将看到许多使用 PyTorch 和 TensorFlow 的完整代码示例的变体。但是我们太超前了。让我们首先检查几种常见的线性近似方法,如粗编码和瓦片编码。由于本书的重点是深度学习在强化学习中的应用,我们不会花太多时间讨论其他各种线性近似方法。同样,仅仅因为我们没有把时间花在所有的线性方法上,并不意味着它们缺乏有效性。根据手头的问题,线性近似方法可能是正确的方法;它有效、快速,并且有收敛保证。
粗略编码
我们来看看图 2-2 中讨论过的山地车问题。汽车有一个二维状态,一个位置,和一个速度。假设我们将二维状态空间分成重叠的圆,每个圆代表一个特征。如果状态 S 位于圆内,则该特定特征存在且值为 1;否则,该特征不存在,其值为 0。特征的数量是圆的数量。假设我们有 p 圈;然后,我们将二维连续状态空间转换为 p 维状态空间,其中每个维度可以是 0 或 1。换句话说,每个维度可以属于{0,1}。
Note
{0,1}表示一组可能的值,即 0 或 1。带有常规括号的(0,1)表示值的范围,即从 0 到 1 的任何值,不包括 0 和 1。[0,1]表示 0 到 1 之间的值的范围以及左边的值;即 0 包括在该范围内。
状态 S 所在的圆圈所代表的所有特征将为“开”或等于 1。图 5-2 给出了一个例子。图中显示了两种状态,根据这些点所在的圆圈,相应的功能将被打开,而其他功能将被关闭。概化将取决于圆圈的大小以及圆圈的密集程度。如果用一个椭圆来代替圆,则推广将更多地在伸长的方向上进行。我们也可以选择圆形以外的形状来控制泛化的数量。
图 5-2
使用圆的二维粗编码。概括取决于圆的大小以及圆放在一起的密度
现在考虑大而密集的圆圈的情况。在两个遥远的州连接在一起的地方,一个大圆使最初的概化变宽,因为它们至少落入一个公共圆内。然而,密度(即圆的数量)允许我们控制细粒度的概括。通过使用多个圆,我们可以确保相邻的州之间至少有一个不同的特征。即使每个单独的圆圈都很大,也是如此。借助于具有不同圆形尺寸和圆形数量的实验,可以微调圆形的尺寸和数量,以控制适合于所讨论的问题/领域的一般化。
平铺编码
图块编码是一种可以通过编程进行规划的粗略编码形式。它适用于多维空间,比一般的粗编码有用得多。
让我们考虑一个二维空间,就像我们刚刚谈到的山地车。我们把空间分成覆盖整个空间的不重叠的网格。每一个这样的划分都称为平铺,如图 5-3 左图所示。这里的瓦片是正方形的,并且根据状态 S 在这个 2D 空间上的位置,只有一个瓦片是 1,而所有其他瓦片都是 0。
然后我们有许多这样的平铺相互偏移。假设我们使用 n 个 tilings 那么对于一个状态,每个瓷砖中只有一个瓷砖是开的。换句话说,如果有 n 个镶嵌,那么正好 n 个特征将是 1,n 个镶嵌中的每一个的单个特征。图 5-3 给出了一个例子。
图 5-3
瓷砖编码。如左图所示,我们在一个单幅图块中有 4×4=16 个图块。我们有四个相互重叠的镶嵌,如右图中四种不同的颜色所示。一个状态(绿色圆点)照亮每个瓷砖中的一个瓷砖。泛化由单个分块中的分块数量以及分块总数控制
注意,如果步长的学习率是等式( 5.1 和( 5.2 )中的α(α),我们现在将它替换为,其中 n 是分块的数量。这是为了使算法不受镶嵌数量的限制。由于粗编码和平铺编码都使用二进制特征,数字计算机可以加快计算速度。
概括的性质现在取决于以下因素:
-
单个瓷砖中的瓷砖数量(图 5-3 中的左图)
-
镶嵌的数量(图 5-4 中的右图显示了四种不同颜色的四个镶嵌)
-
偏移的性质,无论是均匀的、对称的还是不对称的
有一些通用的策略来决定前面的数字。考虑这样一种情况,单个瓷砖中的每个瓷砖都是宽度为 w 的正方形。对于一个 k 维的连续空间,它将是一个 k 维的正方形,每边宽 w 。假设我们有 n 个镶嵌,因此这些镶嵌需要在所有维度上相互偏移的距离。这被称为位移矢量。第一个启发式是选择 n 使得n= 2I≥4k。每个方向的位移都是(1,3,5,7,…)的奇数倍。,2k1)的位移矢量
。在我们即将到来的例子中,我们将使用一个库来帮助我们将二维山地汽车状态空间划分成适当的分块。我们将提供 2D 状态向量,库将返回给我们活动的瓦片向量。
近似值的挑战
虽然我们利用了基于监督学习的方法的知识,如前面解释的梯度下降,但我们必须记住两件事,与监督学习相比,基于梯度的方法在强化学习中更难工作。
首先,在监督学习中,训练数据保持不变。数据是从模型中生成的,当我们这样做时,模型不会改变。这是一个给我们的基本事实,我们试图通过使用数据来了解输入到输出的映射方式。提供给训练算法的数据是算法外部的,并且它不以任何方式依赖于算法。它被给定为常数,与学习算法无关。不幸的是,在 RL 中,尤其是在无模型设置中,情况并非如此。用于生成训练样本的数据基于代理所遵循的策略,它并不是底层模型的完整图像。随着我们对环境的探索,我们了解了更多,并生成了一组新的训练数据。我们或者使用基于 MC 的方法观察实际轨迹,或者使用 TD 下的 bootstrap 来形成目标值的估计值 y(t) 。随着我们探索和学习得越来越多,目标 y(t) 会发生变化,这与监督学习的情况不同。这就是所谓的非稳定目标问题。
第二,监督学习基于样本彼此不相关的理论前提,数学上称为 i.i.d .(独立同分布)数据。然而,在 RL 中,我们看到的数据取决于代理生成数据所遵循的策略。在给定的事件中,我们看到的状态取决于代理在该时刻遵循的策略。稍后时间步骤中出现的状态取决于代理先前采取的操作(决策)。换句话说,数据是相关的。我们看到的下一个状态st+1取决于当前状态 s t 和动作atagent 在该状态下采取的动作。
这两个问题使得 RL 设置中的函数近似更加困难。在此过程中,我们将看到为应对这些挑战而采取的各种方法。
对该方法有了大致的了解后,现在是时候开始我们通常的课程了,首先查看值预测/估计以学习可以表示值函数的函数。然后我们将看看控制方面,即代理试图优化策略的过程。它将遵循使用通用策略迭代(GPI)的通常模式,就像前一章中的方法一样。
增量预测:MC,TD,TD(λ)
在本节中,我们将研究预测问题,即如何使用函数近似来估计状态值。
接下来,让我们尝试使用( 5.4 中的损失函数和( 5.5 中的权重更新)将使用由输入和目标组成的训练数据找到模型的监督训练过程扩展到 RL 下的函数近似。如果您比较( 5.4 中的损失函数和( 5.2 和( 5.3 )中的 MC/TD 更新,您可以通过将 MC 和 TD 更新视为操作来进行比较,这些操作试图最小化实际目标vπ(s)和当前估计值v( 我们可以将损失函数表示如下:
(5.6)
遵循与( 5.5 )中相同的推导,并使用随机梯度下降(即,在每个样本用更新替换期望值),我们可以将权重向量的更新方程写成如下:
**
(5.7)
但是,与监督学习不同,我们没有实际/目标输出值Vπ(s);相反,我们使用这些目标的估计值。对于 MC,Vπ(s)的估计/目标是Gt(s), w ,而 TD(0)下的估计/目标是Rt+1因此,在 MC 和 TD(0)下用函数近似的更新可以写成如下。
下面是 MC 更新:
(5.8)
下面是 TD(0)更新:
(5.9)
对于 q 值,可以写出一组类似的等式。我们将在下一节看到这一点。这与我们在前一章中对 MC 和 TD 控制部分所做的是一样的。
让我们首先考虑线性近似的设置,其中状态值可以表示为状态向量 x(s) 和权重向量 w :
(5.10)的点积
相对于 w 的导数现在将只是状态向量 x ( s )。
(5.11)
将( 5.11 )与方程式( 5.7 )结合起来,我们得到如下:
(5.12)
如前所述,我们不知道真实的状态值Vπ(s),因此我们在 MC 方法中使用估计值Gt(s)和估计值Rt+1+γ∫这给出了线性近似情况下 MC 和 TD 的权重更新规则如下。
下面是 MC 更新:
(5.13)
下面是 TD(0)更新:
(5.14)
简单来说,权重的更新,即( 5.14 )右侧的第二项,可以表示如下:
更新=学习率 x 预测误差 x 特征值
让我们把它与上一章看到的离散状态的基于表格的方法联系起来。我们将说明查表是线性方法的一个特例。考虑 x(s)的每个分量或者是 1 或者是 0,并且它们中只有一个可以具有值 1,而所有其余的特征都是 0。 x 表 ( s )是一个大小为 p 的列向量,其中在任何一点只有一个元素的值为 1,其余的元素都等于 0。根据代理所处的状态,相应的元素将是 1。
权重向量包括每个 s = s 1 ,s 2 ,… s p 的状态 v(s)的值。
使用( 5.10 )中的这些表达式,我们得到如下:
我们在线性更新方程( 5.13 )和( 5.14 )中使用该表达式,以从第四章中获得熟悉的更新规则:
下面是 MC 更新:
(5.15)
下面是 TD(0)更新:
(5.16)
前面的推导是将表查找作为更一般的线性函数近似的特例。
还有一点需要注意,我们在推导更新方程时忽略了,MC 中的目标估计点Gt(s)和Rt+1+γ∫Vt(s’举个例子,我们再重温一下( 5.6 )用 TD 目标替换Vπ(s)然后取梯度。
如果我们对 J ( w )对 w 求导,我们实际上会得到两项,一项是由于Vt(s’)的求导; w ),下一个状态,又一个因Vt(s)而衍生的项; w )。这样一种取两者梯度贡献的方法∇vt(s’; w )和∇vt(s; w )恶化学习速度。首先,原因是我们希望目标保持不变,因此我们需要忽略∇vt(s’)的贡献; w )。第二,概念上用梯度下降我们试图拉当前状态的值Vt(s’; w )向着它的目标前进。取第二贡献项∇vt(s’; w )意味着我们正试图将下一个状态S = S’的值移向当前状态 S=s 的值。
综上,我们只取当前状态值的导数Vt(s; w )并忽略下一个状态值的导数Vt(s’; w 。该方法使得值估计看起来类似于利用固定目标的监督学习所使用的方法。这也是为什么有时( 5.8 )和( 5.9 )中使用的梯度下降法也被称为半梯度法的原因。
正如我们前面提到的,算法的收敛性不再得到保证,不像我们在表格设置中由于收缩定理而得到的保证。然而,大多数经过仔细考虑的算法在实践中确实是收敛的。表 5-1 显示了各种预测/估计算法的收敛性。我们将不详细解释这些收敛特性。这样的讨论更适合一本侧重于学习的理论方面的书。我们的是一个实用的,有足够的理论来理解背景和欣赏算法的细微差别,核心焦点是在 PyTorch 或 TensorFlow 中编码这些算法。
表 5-1
预测/估计算法的收敛性
|保单类型
|
算法
|
表格查找
|
线性的
|
非线性的
| | --- | --- | --- | --- | --- | | 政策上的 | 主持人 TD(0)TD(λ) | YYY | YYY | Y 普通普通 | | 不符合政策 | 主持人 TD(0)TD(λ) | YYY | Y 普通普通 | Y 普通普通 |
在后面的章节中,我们将看到自举(例如 TD)、函数近似和偏离策略的组合在一起会对稳定性产生不利影响,除非仔细考虑学习过程。
现在让我们看看控制问题,即如何用函数近似来优化策略。
增量控制
就像上一章一样,我们将遵循类似的方法。我们从函数近似开始来估计 q 值。
(5.17)
像以前一样,我们在目标值和当前值之间形成一个损失函数。
(5.18)
相对于 w 损失最小,进行随机梯度下降:
在哪里,
。
(5.19)
像之前一样,我们可以在与
使用线性近似时简化方程。在前面所示的线性情况下,导数
将变成
。
接下来,由于我们不知道真实的 q 值q【π】(s, a ),我们用使用 MC 或 TD 的估计来替换它,给我们一组等式。
下面是 MC 更新:
(5.20)
下面是 TD(0)更新:
(5.21)
这些等式允许我们进行 q 值估计/预测。这是广义策略迭代的评估步骤,我们执行多轮梯度下降来改进给定策略的 q 值估计,并使其接近实际目标值。
评估之后是贪婪的政策最大化来改进政策。图 5-4 显示了函数近似 GPI 下的迭代过程。
图 5-4
具有函数近似的广义策略迭代
半梯度 N 步 SARSA 控制
让我们将等式( 5.9 )用于 SARSA 政策控制制度中的 TD 目标。代理使用当前策略对环境进行采样,并观察状态、动作、奖励、下一个状态、下一个动作( s t , a t ,rt+1,st+1,a图 5-5 显示了完整的算法。**
Semi-Gradient N-Step SARSA Control (Episodic)
图 5-5
用于事件控制的 n 步半梯度 SARSA
w 更新是等式( 5.20 ,目标是 G ,n 步返回。在我们对山地汽车的 n 步 SARSA 的例子中,我们将使用 tile 编码,一种二进制特征近似器。在我们的设置中:
;其中 x ( S , A )是瓦片编码的特征向量
据此,
在清单 5-1 中,我们有一个类QEstimator来保存权重并执行平铺。函数get_active_features以 S 的连续二维值和离散输入动作 A 作为输入,返回平铺编码的active_feature x ( S , A ),即对于给定( S , A )有效的二进制平铺特征。函数q_predict也将 S , A 作为输入,并返回估计值 = * x * ( S ,A)T。 w 。它在内部调用
get_active_features来首先获得特征,并与权重向量进行点积。图 5-5 中算法末尾所示的权重更新方程是函数q_update所执行的。函数get_eps_greedy_action使用ε贪婪进行动作选择。
另一个函数sarsa_n实现图 5-5 给出的算法,根据需要调用QEstimator中的函数。类似于前一章中的许多例子,我们也有一个助手函数plot_rewards来随着训练的进行绘制每集的奖励。清单 5-1 给出了代码(listing5_1.ipynb)。
class QEstimator:
def __init__(self, step_size, num_of_tilings=8, tiles_per_dim=8, max_size=2048, epsilon=0.0):
self.max_size = max_size
self.num_of_tilings = num_of_tilings
self.tiles_per_dim = tiles_per_dim
self.epsilon = epsilon
self.step_size = step_size / num_of_tilings
self.table = IHT(max_size)
self.w = np.zeros(max_size)
self.pos_scale = self.tiles_per_dim / (env.observation_space.high[0] \
- env.observation_space.low[0])
self.vel_scale = self.tiles_per_dim / (env.observation_space.high[1] \
- env.observation_space.low[1])
def get_active_features(self, state, action):
pos, vel = state
active_features = tiles(self.table, self.num_of_tilings,
[self.pos_scale * (pos - env.observation_space.low[0]),
self.vel_scale * (vel- env.observation_space.low[1])],
[action])
return active_features
def q_predict(self, state, action):
pos, vel = state
if pos == env.observation_space.high[0]: # reached goal
return 0.0
else:
active_features = self.get_active_features(state, action)
return np.sum(self.w[active_features])
# learn with given state, action and target
def q_update(self, state, action, target):
active_features = self.get_active_features(state, action)
q_s_a = np.sum(self.w[active_features])
delta = (target - q_s_a)
self.w[active_features] += self.step_size * delta
def get_eps_greedy_action(self, state):
pos, vel = state
if np.random.rand() < self.epsilon:
return np.random.choice(env.action_space.n)
else:
qvals = np.array([self.q_predict(state, action) for action in range(env.action_space.n)])
return np.argmax(qvals)
########################
def sarsa_n(qhat, step_size=0.5, epsilon=0.0, n=1, gamma=1.0, episode_cnt = 10000):
episode_rewards = []
for _ in range(episode_cnt):
state = env.reset()
action = qhat.get_eps_greedy_action(state)
T = float('inf')
t = 0
states = [state]
actions = [action]
rewards = [0.0]
while True:
if t < T:
next_state, reward, done, _ = env.step(action)
states.append(next_state)
rewards.append(reward)
if done:
T = t+1
else:
next_action = qhat.get_eps_greedy_action(next_state)
actions.append(next_action)
tau = t - n + 1
if tau >= 0:
G = 0
for i in range(tau+1, min(tau+n, T)+1):
G += gamma ** (i-tau-1) * rewards[i]
if tau+n < T:
G += gamma**n * qhat.q_predict(states[tau+n], actions[tau+n])
qhat.q_update(states[tau], actions[tau], G)
if tau == T - 1:
episode_rewards.append(np.sum(rewards))
break
else:
t += 1
state = next_state
action = next_action
return np.array(episode_rewards)
Listing 5-1N-Step SARA Control: Mountain Car
图 5-6 显示了运行此算法训练山地车的结果。我们可以看到,在 50 集内,代理达到一个稳定状态,它能够在大约 110 个时间步长内达到击中山谷右侧旗帜的目标。
图 5-6
带登山车的 n 步半梯度 SARSA
半梯度 SARSA(λ)控制
接下来,我们将研究具有合格轨迹的半梯度 SARSA(λ)算法。SARSA(λ)进一步推广了 n 步 SARSA。当状态或状态-动作值由线性函数近似的二进制特征表示时,就像 tile 编码的山地车一样,我们得到如图 5-7 所示的算法。该算法引入了合格轨迹的概念,该合格轨迹具有与权重向量相同数量的分量。权重向量是对许多情节的长期记忆,从所示的所有例子中进行归纳。合格痕迹是持续时间小于情节长度的短时记忆。它通过影响体重来帮助学习过程。我们将不深入讨论更新规则的详细推导。概念和数学推导的详细解释可以参考 http://incompleteideas.net/book/the-book.html 。
Semi-Gradient SARSA(Λ) Control (Episodic)
图 5-7
半梯度 SARSA(λ)用于当特征是二元的并且值函数是特征向量和权重向量的线性组合时的情节控制
让我们看看在山地车上运行前面的算法。Listing5_2.ipynb有完整的代码。在清单 5-2 中,我们强调了代码的重要部分。像listing5_1.ipynb一样,我们有一个名为QEstimator的类,做了一些小的修改来存储跟踪值,并在权重更新函数q_update中使用跟踪。我们还有两个辅助函数:accumulating_trace和replacing trace来实现对两个轨迹变量的跟踪。函数sarsa_lambda实现图 5-7 中给出的整体学习算法。我们也有一个功能来运行训练有素的代理通过一些插曲,并记录行为。一旦您训练了代理并生成了动画,您就可以运行 MP4 文件并查看代理为达到目标所遵循的策略。
def accumulating_trace(trace, active_features, gamma, lambd):
trace *= gamma * lambd
trace[active_features] += 1
return trace
def replacing_trace(trace, active_features, gamma, lambd):
trace *= gamma * lambd
trace[active_features] = 1
return trace
# code omitted as it largely similar to listing 5-1
# except for adding trace vector to init fn and to q_update fn
class QEstimator:
def sarsa_lambda(qhat, episode_cnt = 10000, max_size=2048, gamma=1.0):
episode_rewards = []
for i in range(episode_cnt):
state = env.reset()
action = qhat.get_eps_greedy_action(state)
qhat.trace = np.zeros(max_size)
episode_reward = 0
while True:
next_state, reward, done, _ = env.step(action)
next_action = qhat.get_eps_greedy_action(next_state)
episode_reward += reward
qhat.q_update(state, action, reward, next_state, next_action)
if done:
episode_rewards.append(episode_reward)
break
state = next_state
action = next_action
return np.array(episode_rewards)
Listing 5-2SARSA (λ) Control: Mountain Car
图 5-8 显示了运行 SARSA(λ)算法训练山地车的结果。我们可以看到结果类似于图 5-6 中的结果。这是一个太小的问题,但对于更大的问题,资格追踪驱动的算法将显示更好和更快的收敛。
图 5-8
半坡 SARSA(λ)带山地车
函数近似的收敛性
让我们通过看一个例子来开始探索收敛。如图 5-9 所示,让我们考虑一个双态转换作为一些 MDP 的一部分。假设我们将使用函数近似,第一个状态的值为 w ,第二个状态的值为 2 w 。这里, w 是一个单一的数字,而不是一个向量。
图 5-9
泛函近似下的两步跃迁
假设 w = 10,代理从第一状态转换到第二状态,即从值为 10 的状态转换到值为 20 的状态。我们还假设从第一个状态到第二个状态的转变是第一个状态中唯一可能的转变,而这个转变的回报每次都是零。设学习率为 α = 0.1。
现在让我们将方程式( 5.14 )应用于前一个。
??
??
i . ew+1=+0.1】w
*我们假设 λ 接近 1,当前权重为 10。更新后的权重会是这样的:wt+1= 10+0.1∫10∫(2 1)= 11。
只要(2γ—1)>为 0,每次更新都会导致权重发散。这表明函数近似会导致发散。这是因为值的泛化,即更新给定州的值也会更新附近或相关州的值。列出了不稳定问题的三个角度。
-
函数近似:一种使用权重对非常大的状态空间进行概括的方法,与状态总数相比,权重是一个较小的向量
-
自举:使用状态值的估计值形成目标值,例如在 TD(0)中,目标是估计值Rt+1+γ∫Vt(s); w
-
非策略学习:使用行为策略训练代理,但学习不同的最优策略
即使在简单的预测/估计场景中,这三个部分的共同存在也显著增加了发散的机会。控制和优化问题分析起来更加复杂。也已经表明,只要这三者不同时存在,就可以避免不稳定性。这就给我们带来了一个问题,我们能放弃这三者中的任何一个,并评估这种放弃的影响吗?
函数近似,尤其是使用神经网络的函数近似,使得 RL 对于大型现实世界的问题是可行的。其他替代方案不实用。自举使流程样本变得高效。通过观看完整集来形成目标的替代方案虽然可行,但不太实用。策略外学习可以用策略内学习来代替,但是为了让 RL 更接近人类的学习方式,我们需要策略外学习,通过探索另一个类似的问题来了解一些问题/情况。因此,对此没有简单的答案。我们不能放弃这三项要求中影响较小的任何一项。那是理论上的一面。在实践中,大多数时候,算法通过一些仔细的监控和调整而收敛。
梯度时间差异学习
用方程( 5.9 )所示的更新方程进行的半梯度 TD 学习不遵循真实梯度。在取损失函数的梯度时,我们保留了对目标的估计,即Rt+1+γ∫Vt(s’; w ),常数。它没有出现在关于重量 w 的导数中。真正的更夫误差是Rt+1+γ∑Vt(s’;w—Vt(s; w ),它的导数理想情况下应该对两个Vt(s)都有梯度项; w )和Vt(s’; w )。
有一种称为梯度时间差异学习的变体,它遵循真实梯度,并在所有查表、线性和非线性函数近似以及符合策略和不符合策略方法的情况下提供收敛。将此添加到算法组合中,可以修改表 5-1 ,如表 5-2 所示。我们不会在这本书里深入讨论它的数学证明,因为这本书的重点是算法的实际实现。
表 5-2
预测/估计算法的收敛性
|保单类型
|
算法
|
表格查找
|
线性的
|
非线性的
| | --- | --- | --- | --- | --- | | 政策上的 | 主持人 TD(0)TD(λ)梯度 TD | YYYY | YYYY | Y 普通普通 Y | | 不符合政策 | 主持人 TD(0)TD(λ)梯度 TD | YYYY | Y 普通普通 Y | Y 普通普通 Y |
接着,我们在表 5-3 中给出了控制算法的收敛性。
表 5-3
控制算法的收敛性
|算法
|
表格查找
|
线性的
|
非线性的
| | --- | --- | --- | --- | | MC 控制 | Y | (Y) | 普通 | | 保单 TD (SARSA) | Y | (Y) | 普通 | | 政策外 Q-learning | Y | 普通 | 普通 | | 梯度 Q 学习 | Y | Y | 普通 |
(Y):在接近最优值函数附近波动。在所有非线性情况下,收敛保证都是无效的。
批量方法(DQN)
到目前为止,我们一直专注于增量算法;也就是说,我们对转换进行采样,然后使用这些值,在随机梯度下降的帮助下更新权重向量 w 。但是这种方法不具有样本效率。我们只使用一次样本就丢弃它。然而,对于非线性函数近似,特别是对于神经网络,我们需要多次通过网络才能使网络权重收敛到真实值。此外,在许多现实生活场景中,如机器人,我们需要两个方面的样本效率:神经网络收敛缓慢,因此需要多次通过,以及在现实生活中生成样本非常慢。在关于批量强化方法的这一节中,我们将带您了解批量方法在深度 Q 网络中的具体使用,深度 Q 网络是非策略 Q 学习的深度网络版本。
像之前一样,我们使用函数近似来估计状态值,如等式( 5.1 ): 所示。
考虑我们以某种方式知道实际状态值v【π】(s),并且我们正试图学习权重向量 w 以达到良好的估计值。我们收集了一批经验。
我们将最小平方损失作为真实值和估计值之间的差的平均值,然后执行梯度下降以最小化误差。我们使用小批量梯度下降来获取过去经验的样本,并使用学习率 α 来移动权重向量。
我们用样本来近似它。
(5.22)
取 LS ( w )相对于 w 的梯度,用负梯度调整 w ,得到方程( 5.23 ,类似于方程( 5.7 )。
(5.23)
像以前一样,我们可以用 q 值进行类似的更新。
(5.24)
但是,我们并不知道真正的值函数,vπ(sI或qπ(sI, a i )。像以前一样,我们使用 MC 或 TD 方法将真实值替换为估计值。现在让我们来看一个叫做 DQN 的版本,Q-learning 的深度学习版本,如第四章所示。在 DQN,一种非策略算法,我们对当前状态进行采样,根据当前行为策略采取步骤,一种使用当前 q 值的ε贪婪策略。我们观察奖励 r 和下一个状态s’。我们用来表示状态**’中可能出现的所有动作**’来形成目标。********
这里我们使用了不同的权重向量wt来计算目标的估计值。本质上,我们有两个网络,一个称为在线网络,其权重为 w ,根据等式 5.24 进行更新,第二个类似的网络称为目标网络,但其权重副本称为w—。权重向量w?? 更新频率较低,比如在线网络权重 w 每更新 100 次。这种方法保持目标网络不变,并允许我们使用监督学习的机制。还请注意,我们使用下标 i 表示小批量中的样品,使用 t 表示权重更新的指数。将所有这些放在一起,最终的更新方程可以写成如下形式:
**(5.25)
简而言之,我们使用ε-greedy 策略在环境中运行代理,并在名为 replay buffer D 的缓冲区中收集经验。我们使用( 5.25 )对在线网络进行权重更新。我们也偶尔更新目标网络权重(比如在每 100 次批量更新 w 之后)。我们使用带有ε-探索的更新的 q 值来将更多的体验添加到重放缓冲器中,并再次执行整个循环。这实质上是 DQN 的方法。在下一章,我们还有很多要说的,这一章完全是关于 DQN 及其变体的。现在,我们把话题留在这里,继续前进。
线性最小二乘法
在批处理方法中使用的经验重放找到最小平方解,最小化使用 TD 或 MC 估计的目标和当前值函数估计之间的误差。但是,需要多次迭代才能收敛。但如果我们对预测用的值函数和控制用的
用线性函数近似,就可以直接求出最小二乘解。先来看预测。
我们从方程( 5.22 ,代入得到这个:
取 LS ( w )相对于 w 的梯度并设为零,我们得到如下:
求解 w 给出如下:
(5.26)
前面的解决方案涉及一个 N N 矩阵的求逆,这需要 O ( N 3 )的计算。但是,使用 Shermann-Morrison,我们可以在O(N2)时间内解决这个问题。和以前一样,我们不知道真实值vπ(sI)。我们使用 MC、TD(0)或 TD(λ)估计值将真实值替换为其估计值,从而为我们提供线性最小二乘 MC (LSMC)、LSTD 或 LSTD(λ)预测算法。
LSD(λ):v*(【我】*)
***所有这些预测算法对于非策略或策略都具有良好的收敛性。
接下来,我们将分析扩展到使用 q 值线性函数近似和 GPI 的控制,其中前面的方法用于 q 值预测,随后是策略改进步骤中的贪婪 q 值最大化。这被称为线性最小二乘策略迭代 (LSPI)。我们通过这些循环迭代预测,然后改进,直到策略收敛,即,直到权重收敛。我们在这里给出线性最小二乘 Q 学习(LSPI)的最终结果,而不经过推导。
下面是预测步骤:
其中( i )是第 i th 样( s i , a i , r i ,s′I)来自经验回放
这是控制步骤:
对于每个状态 s ,我们改变在先前预测步骤中执行的权重更新 w 之后最大化 q 值的策略。
前面,我们讨论了使用函数近似的策略迭代的大多数变体:增量、批处理和线性方法。让我们绕一小段路来介绍 PyTorch 和 TensorFlow 库。
深度学习图书馆
本章的前几节向我们展示了通过使用函数近似方法,我们需要一种有效的方法来计算状态值函数或动作值函数
的导数。如果我们使用神经网络,我们需要使用反向传播来计算网络每一层的导数。这就是 PyTorch 和 TensorFlow 这样的库出现的原因。类似于 NumPy 库,它们也以有效的方式执行向量/矩阵计算。此外,它们在处理张量(二维以上的数组)方面进行了高度优化。
在神经网络中,我们需要能够反向传播误差,以计算相对于各级层权重的误差梯度。这两个库都是高度抽象和优化的,可以在幕后为我们处理这些问题。我们只需要构建正向计算,通过所有的计算获取输入,从而得到最终的输出。这些库跟踪计算图形,并允许我们只通过一次函数调用来对权重进行梯度更新。
为了更新您对这两个库的了解,我们提供了两个 Python 笔记本,带您使用 MNIST 数据集训练一个简单的数字分类模型。listing5_3_pytorch_intro.ipynb是使用 PyTorch 的代码,listing5_4_tensorflow.ipynb使用 TensorFlow 遍历同样的东西。我们在这里不复制文本中的代码,因为这些笔记本只是为了让你更新关于 PyTorch 和 TensorFlow 的知识。
摘要
在这一章中,我们主要关注的是函数近似在非常大或连续的状态空间中的应用,这些空间不能用我们在前面章节中看到的基于表格的学习方法来处理。
我们讨论过用函数近似进行优化意味着什么。我们还展示了监督学习中的训练概念,即训练模型以产生接近目标的值,如何应用于强化学习,对移动目标和 RL 中所示的样本相互依赖进行适当处理。
然后,我们研究了函数近似的各种策略,包括线性和非线性策略。我们还看到了基于表格的方法只是线性近似的特例。随后详细讨论了预测和控制的增量方法。我们看到这些被应用在山地车上,使用 n 步 SARSA 和 SARSA(λ)构建训练代理。
接下来,我们讨论了批处理方法,并探索了 DQN 更新规则的完整推导过程,这是批处理方法家族中的一个流行算法。然后我们研究了预测和控制的线性最小二乘法。在此过程中,我们不断强调一般的融合问题以及正在讨论的特定方法中的融合。
我们最后以 PyTorch 和 TensorFlow 等深度学习框架的简要介绍结束了这一章。**************
六、深度 Q 学习
在这一章中,我们将深入探讨 Q 学习与使用神经网络的函数近似的结合。使用神经网络的深度学习环境中的 Q-learning 也被称为深度 Q 网络 (DQN)。我们将首先总结到目前为止我们所谈论的关于 Q-learning 的内容。然后我们将看看 DQN 在简单问题上的代码实现,然后训练一个代理玩 Atari 游戏。接下来,我们将通过查看可以对 DQN 进行的各种修改来扩展我们的知识,以改进学习,包括一些非常新的和最先进的方法。其中一些方法可能涉及一些数学知识,以理解这些方法的基本原理。然而,我们将努力保持数学最少,只包括所需的细节,以了解背景和推理。本章中的所有例子都将使用 PyTorch 或 TensorFlow 库进行编码。一些代码演练将同时包含 PyTorch 和 TensorFlow 的代码,而其他代码将仅使用 PyTorch 进行讨论。
深度 Q 网络
在第四章中,我们讨论了 Q-learning 作为一种无模型的非策略 TD 控制方法。我们首先看一下在线版本,其中我们使用了一个探索性行为策略(ε-贪婪)来在状态 S 时采取一个步骤(动作 A )。奖励 R 和下一个状态S’然后被用来更新 Q 值 Q ( S , A )。图 4-14 和清单 4-4 详细说明了伪代码和实际实现。这里给出了在这种情况下使用的更新公式。在你继续前进之前,你可能想重温这一点。
(6.1)
我们简要地讨论了最大化偏差和双 Q 学习的方法,其中我们使用了两个 Q 值表。在这一章中,当我们研究双 DQN 时,我们会有更多的内容要说。
接下来,我们研究了多次使用一个样本来将在线 TD 更新转换为批量 TD 更新的方法,从而提高样本效率。它向我们介绍了重放缓冲区的概念。虽然它只是关于离散状态和状态-动作空间上下文中的样本效率,但通过神经网络的函数近似,它几乎成为了使深度学习神经网络收敛的必备条件。我们将再次讨论这一点,当我们谈到优先重放时,我们将看看从缓冲区中采样过渡/体验的其他选项。
接下来,在第五章中,我们看了函数近似的各种方法。我们将瓦片编码视为实现线性函数近似的一种方式。然后,我们讨论了 DQN,即使用神经网络作为函数近似器的批量 Q 学习。我们经历了长时间推导,以得出如等式( 5 中给出的权重(具有神经网络参数)更新等式。25 。此处转载如下:
(6.2)
还请注意,我们使用下标 i 表示小批量中的样品,使用 i 表示权重更新的指数。方程式( 6.2 )是我们将在本章中广泛使用的一个。当我们谈论不同的修改和研究它们的影响时,我们将对这个等式进行各种调整。
我们还讨论了在梯度更新的非线性函数近似下没有收敛的理论保证。在这一章里,我们将对此有更多的论述。Q-learning 方法用于离散状态和动作,其中使用( 6.1 )更新 Q 值,而不是调整 DQN 基于深度学习的方法的权重参数。Q-learning 的案例有收敛的保证,而 DQN 的案例则没有这样的保证。DQN 也是计算密集型的。然而,尽管 DQN 有这些缺点,DQN 使得使用原始图像训练代理成为可能,这在普通的 Q-learning 中是完全不可想象的。现在让我们将等式( 6.2 )付诸实践,以在各种环境中训练 DQN 代理。
让我们再来看一下CartPole问题,它有一个四维连续状态,包含当前购物车位置、速度、杆的角度和杆的角速度的值。动作有两种:向左推车或向右推车,目的是尽可能长时间保持杆子平衡。以下是环境的详细信息:
Observation:
Type: Box(4)
Num Observation Min Max
0 Cart Position -4.8 4.8
1 Cart Velocity -Inf Inf
2 Pole Angle 0.418 rad (-24 deg) 0.418 rad (24 deg)
3 Pole Angular Velocity -Inf Inf
Actions:
Type: Discrete(2)
Num Action
0 Push cart to the left
1 Push cart to the right
我们将建立一个小的神经网络,以 4 为输入维度,三个隐藏层,然后是一个输出层,维度 2 是可能的动作数。图 6-1 为网络图。
图 6-1
简单神经网络
我们将使用 PyTorch 的nn.Module类来构建网络。我们还将实现一些额外的功能。函数get_qvalues取一批状态作为输入,即一个(N×4)维的张量,其中 N 是样本数。它通过网络传递状态值以产生 q 值。输出向量的大小为(n×2);即每个输入一行。每行有两个 q 值,一个用于左推动作,另一个用于右推动作。同一个类中的函数sample_actions接收一批 q 值(N×2)。它使用ε-贪婪策略(等式 4。3 选择一个动作。输出是(N×1)向量。清单 6-2 显示了 PyTorch 中的代码。清单 6-3 显示了 TensorFlow 2.0 的急切执行模式中的相同代码。您可以在文件listing6_1_dqn_pytorch.ipynb中找到 PyTorch 的完整实现,在文件listing6_1_dqn_tensorflow.ipynb中找到 TensorFlow 的完整实现。
注意虽然不是必需的,但是您将从代码讨论中获得更多关于 PyTorch 或 TensorFlow 的知识。你应该能够创建基本网络,定义损失函数,并执行优化的基本训练步骤。TensorFlow 新的急切执行模型类似于 PyTorch。出于这个原因,我们将在两个库中提供有限的示例代码,以帮助您入门。否则,书中的大部分代码都是 PyTorch。
class DQNAgent(nn.Module):
def __init__(self, state_shape, n_actions, epsilon=0):
super().__init__()
self.epsilon = epsilon
self.n_actions = n_actions
self.state_shape = state_shape
state_dim = state_shape[0]
# a simple NN with state_dim as input vector (inout is state s)
# and self.n_actions as output vector of logits of q(s, a)
self.network = nn.Sequential()
self.network.add_module('layer1', nn.Linear(state_dim, 192))
self.network.add_module('relu1', nn.ReLU())
self.network.add_module('layer2', nn.Linear(192, 256))
self.network.add_module('relu2', nn.ReLU())
self.network.add_module('layer3', nn.Linear(256, 64))
self.network.add_module('relu3', nn.ReLU())
self.network.add_module('layer4', nn.Linear(64, n_actions))
#
self.parameters = self.network.parameters
def forward(self, state_t):
# pass the state at time t through the newrok to get Q(s,a)
qvalues = self.network(state_t)
return qvalues
def get_qvalues(self, states):
# input is an array of states in numpy and outout is Qvals as numpy array
states = torch.tensor(states, device=device, dtype=torch.float32)
qvalues = self.forward(states)
return qvalues.data.cpu().numpy()
def sample_actions(self, qvalues):
# sample actions from a batch of q_values using epsilon greedy policy
epsilon = self.epsilon
batch_size, n_actions = qvalues.shape
random_actions = np.random.choice(n_actions, size=batch_size)
best_actions = qvalues.argmax(axis=-1)
should_explore = np.random.choice(
[0, 1], batch_size, p=[1-epsilon, epsilon])
return np.where(should_explore, random_actions, best_actions)
Listing 6-1A Simple DQN Agent in PyTorch
清单 6-2 显示了 TensorFlow 2.x 中使用 Keras 接口的相同代码。我们使用新的急切执行模型,它类似于 PyTorch 采用的方法。TensorFlow 在早期版本中使用了不同的模型,这有点难以概念化,分为两个独立的阶段:一个是构建所有网络操作的符号图,然后是通过将数据作为张量传递到第一阶段的模型构建中来训练模型的第二阶段。
class DQNAgent:
def __init__(self, state_shape, n_actions, epsilon=0):
self.epsilon = epsilon
self.n_actions = n_actions
self.state_shape = state_shape
state_dim = state_shape[0]
self.model = tf.keras.models.Sequential()
self.model.add(tf.keras.Input(shape=(state_dim,)))
self.model.add(tf.keras.layers.Dense(192, activation="relu"))
self.model.add(tf.keras.layers.Dense(256, activation="relu"))
self.model.add(tf.keras.layers.Dense(64, activation="relu"))
self.model.add(tf.keras.layers.Dense(n_actions))
def __call__(self, state_t):
# pass the state at time t through the newrok to get Q(s,a)
qvalues = self.model(state_t)
return qvalues
def get_qvalues(self, states):
# input is an array of states in numpy and outout is Qvals as numpy array
qvalues = self.model(states)
return qvalues.numpy()
def sample_actions(self, qvalues):
# sample actions from a batch of q_values using epsilon greedy policy
epsilon = self.epsilon
batch_size, n_actions = qvalues.shape
random_actions = np.random.choice(n_actions, size=batch_size)
best_actions = qvalues.argmax(axis=-1)
should_explore = np.random.choice(
[0, 1], batch_size, p=[1-epsilon, epsilon])
return np.where(should_explore, random_actions, best_actions)
Listing 6-2A Simple DQN Agent in TensorFlow
重放缓冲区的代码很简单。我们有一个名为self.buffer的缓冲区来保存前面的例子。函数add接收(state, action, reward, next_state, done),即来自代理单步/转换的值,并将其添加到缓冲区。如果缓冲区已经达到最大长度,它会丢弃最早的过渡,为新添加的内容腾出空间。函数sample获取整数batch_size并从缓冲器返回batch_size样本/转换。在这种普通实现中,存储在缓冲器中的每个转换被采样的概率相等。清单 6-3 显示了重放缓冲区的代码。
class ReplayBuffer:
def __init__(self, size):
self.size = size #max number of items in buffer
self.buffer =[] #array to hold samples
self.next_id = 0
def __len__(self):
return len(self.buffer)
def add(self, state, action, reward, next_state, done):
item = (state, action, reward, next_state, done)
if len(self.buffer) < self.size:
self.buffer.append(item)
else:
self.buffer[self.next_id] = item
self.next_id = (self.next_id + 1) % self.size
def sample(self, batch_size):
idxs = np.random.choice(len(self.buffer), batch_size)
samples = [self.buffer[i] for i in idxs]
states, actions, rewards, next_states, done_flags = list(zip(*samples))
return np.array(states),
np.array(actions),
np.array(rewards),
np.array(next_states),
np.array(done_flags)
Listing 6-3Replay Buffer (Same in PyTorch or TensorFlow)
接下来,我们有一个效用函数play_and_store,它接受一个env(例如CartPole)、一个agent(例如DQNAgent)、一个exp_replay ( ReplayBuffer)、代理的start_state和n_steps(即在环境中要采取的步骤/动作的数量)。该功能使agent从初始状态start_state开始走n_steps步数。基于代理使用agent.sample_actions遵循的当前ε-贪婪策略采取这些步骤,并将这些n_steps转换记录在缓冲器中。清单 6-4 显示了代码。
def play_and_record(start_state, agent, env, exp_replay, n_steps=1):
s = start_state
sum_rewards = 0
# Play the game for n_steps and record transitions in buffer
for _ in range(n_steps):
qvalues = agent.get_qvalues([s])
a = agent.sample_actions(qvalues)[0]
next_s, r, done, _ = env.step(a)
sum_rewards += r
exp_replay.add(s, a, r, next_s, done)
if done:
s = env.reset()
else:
s = next_s
return sum_rewards, s
Listing 6-4Implementation of Function play_and_record
接下来,我们看看学习过程。我们首先建立我们想要最小化的损失 L 。它是使用一步 TD 值的当前状态动作的目标值和当前状态值之间的平均平方误差。正如在第五章中所讨论的,我们使用原始神经网络的副本,它具有权重w—(w带上标)。我们使用损失来计算代理(在线/原始)网络的权重 w 的梯度,并在梯度的负方向上采取一个步骤来减少损失。请注意,如第五章所述,我们保持目标网络的权重w—不变,并以较低的频率更新这些权重。引用第五章 DQN 批量方法部分:
这里我们使用了不同的权重向量 wt来计算目标的估计值。本质上,我们有两个网络,一个称为在线网络,权重为“w”,根据等式(5.24)进行更新,第二个类似的网络称为目标网络,但具有权重“w”的副本,称为“**【w。权重向量**w更新频率较低,比如说在线网络每更新 100 次。这种方法保持目标网络不变,并允许我们使用监督学习的机制。
损失函数如下:
(6.3)
我们采用 L 相对于 w 的梯度(导数),然后使用该梯度来更新在线网络的权重 w 。这些等式如下:
(6.4)
(6.5)
结合这两者,我们得到我们熟悉的方程更新( 6.2 )。然而,在 PyTorch 和 TensorFlow 中,我们不通过直接编码来进行更新,因为计算梯度并不容易。这是使用 PyTorch 和 TensorFlow 等软件包的主要原因之一,这些软件包可以根据计算损耗度量 L 的操作自动计算梯度。我们只需要一个函数来计算这个度量。这是通过函数
compute_td_loss.完成的,它接收一批(states, actions, rewards, next_states, done_flags)。它还接受折扣参数 γ 以及代理/在线和目标网络。该函数然后根据等式( 6.3 )计算损耗 L 。清单 6-5 给出 PyTorch 中的实现,清单 6-6 给出 TensorFlow 中的实现。
def compute_td_loss(agent, target_network, states, actions, rewards, next_states,
done_flags, gamma=0.99):
# get q-values for all actions in current states
# use agent network
predicted_qvalues = agent(states)
# compute q-values for all actions in next states
# use target network
predicted_next_qvalues = target_network(next_states)
# select q-values for chosen actions
row_indices= tf.range(len(actions))
indices = tf.transpose([row_indices, actions])
predicted_qvalues_for_actions = tf.gather_nd(predicted_qvalues, indices)
# compute Qmax(next_states, actions) using predicted next q-values
next_state_values = tf.reduce_max(predicted_next_qvalues, axis=1)
# compute "target q-values"
target_qvalues_for_actions = rewards + gamma * next_state_values * (1-done_flags)
# mean squared error loss to minimize
loss = tf.keras.losses.MSE(target_qvalues_for_actions, predicted_qvalues_for_actions)
return loss
Listing 6-6Compute TD Loss in TensorFlow
def compute_td_loss(agent, target_network, states, actions, rewards, next_states, done_flags,
gamma=0.99, device=device):
# convert numpy array to torch tensors
states = torch.tensor(states, device=device, dtype=torch.float)
actions = torch.tensor(actions, device=device, dtype=torch.long)
rewards = torch.tensor(rewards, device=device, dtype=torch.float)
next_states = torch.tensor(next_states, device=device, dtype=torch.float)
done_flags = torch.tensor(done_flags.astype('float32'),device=device,dtype=torch.float)
# get q-values for all actions in current states
# use agent network
predicted_qvalues = agent(states)
# compute q-values for all actions in next states
# use target network
predicted_next_qvalues = target_network(next_states)
# select q-values for chosen actions
predicted_qvalues_for_actions = predicted_qvalues[range(
len(actions)), actions]
# compute Qmax(next_states, actions) using predicted next q-values
next_state_values,_ = torch.max(predicted_next_qvalues, dim=1)
# compute "target q-values"
target_qvalues_for_actions = rewards + gamma * next_state_values * (1-done_flags)
# mean squared error loss to minimize
loss = torch.mean((predicted_qvalues_for_actions -
target_qvalues_for_actions.detach()) ** 2)
return loss
Listing 6-5Compute TD Loss in PyTorch
在这一点上,我们有所有的机器来训练代理平衡杆。首先,我们定义一些超参数,如batch_size,总训练步数total_steps,以及探索ε衰减的速率。它从 1.0 开始,随着代理学习最优策略,慢慢地将探索减少到 0.05。我们还定义了一个optimizer,它可以接受前面列表中创建的损失 L ,并帮助我们采取梯度步骤来调整权重,本质上实现了等式( 6.4 )和( 6.5 )。清单 6-7 给出了 PyTorch 中的训练代码。清单 6-8 给出了 TensorFlow 中相同的代码。
for step in trange(total_steps + 1):
# reduce exploration as we progress
agent.epsilon = epsilon_schedule(start_epsilon, end_epsilon, step, eps_decay_final_step)
# take timesteps_per_epoch and update experience replay buffer
_, state = play_and_record(state, agent, env, exp_replay, timesteps_per_epoch)
# train by sampling batch_size of data from experience replay
states, actions, rewards, next_states, done_flags = exp_replay.sample(batch_size)
with tf.GradientTape() as tape:
# loss = <compute TD loss>
loss = compute_td_loss(agent, target_network,
states, actions, rewards, next_states, done_flags,
gamma=0.99)
gradients = tape.gradient(loss, agent.model.trainable_variables)
clipped_grads = [tf.clip_by_norm(g, max_grad_norm) for g in gradients]
optimizer.apply_gradients(zip(clipped_grads, agent.model.trainable_variables))
Listing 6-8Train the Agent in TensorFlow
for step in trange(total_steps + 1):
# reduce exploration as we progress
agent.epsilon = epsilon_schedule(start_epsilon, end_epsilon, step, eps_decay_final_step)
# take timesteps_per_epoch and update experience replay buffer
_, state = play_and_record(state, agent, env, exp_replay, timesteps_per_epoch)
# train by sampling batch_size of data from experience replay
states, actions, rewards, next_states, done_flags = exp_replay.sample(batch_size)
# loss = <compute TD loss>
loss = compute_td_loss(agent, target_network,
states, actions, rewards, next_states, done_flags,
gamma=0.99,
device=device)
loss.backward()
grad_norm = nn.utils.clip_grad_norm_(agent.parameters(), max_grad_norm)
opt.step()
opt.zero_grad()
###Omitted code here###
### code to periodically evaluate the performance and plot some graphs
Listing 6-7Train the Agent in PyTorch
我们现在有一个训练有素的特工。我们训练代理人,并在我们训练代理人 50,000 步时定期绘制每集的平均奖励。在图 6-2 的左图中,x 轴值 10 对应第 10000步。我们还绘制了每 20 步的 TD 损耗,这就是为什么右侧图中的 x 轴从 0 到 2500,即 0 到 2500x20=50,000 步。与监督学习不同,目标不是固定的。我们在短时间内保持目标网络固定,并通过用在线网络刷新目标网络权重来定期更新它。此外,如所讨论的,具有偏离策略学习(Q 学习)和引导目标(目标网络只是实际值的估计和使用其他 Q 值的当前估计形成的估计)的非线性函数近似(神经网络)没有收敛保证。培训可能会看到损失上升,爆炸或波动。与通常的监督学习中的损失图相比,这个损失图是违反直觉的。图 6-2 显示了来自训练 DQN 的图表。
图 6-2
DQN 的训练曲线
代码笔记本listing6_1_dqn_pytorch.ipynb和listing6_1_dqn_tensorflow.ipynb有更多的代码,以视频文件的形式记录受训代理的行为,然后播放视频来展示行为。
这完成了使用深度学习来训练代理的完整 DQN 的实现。对于这样一个简单的网络来说,使用一个复杂的神经网络可能看起来有些矫枉过正。核心思想是专注于算法,教你如何写一个 DQN 学习代理。我们现在将使用相同的实现,但稍作调整,以便代理可以使用游戏图像像素值作为状态来玩 Atari 游戏。
使用 DQN 的雅达利游戏代理
在 2013 年题为“用深度强化学习玩雅达利”的开创性论文中, 1 作者使用深度学习模型创建了一种基于神经网络的 Q 学习算法。他们将其命名为深度 Q 网络。这正是我们在上一节中实现的。我们现在将简要讨论作者采取的训练代理玩 Atari 游戏的附加步骤。主要要点与上一节相同,但有两个关键区别:使用游戏图像像素值作为需要一些预处理的状态输入,以及在代理内部使用卷积网络,而不是我们在上一节中看到的线性层。计算损耗 L 和进行训练的其余方法与上一节相同。请注意,使用卷积网络进行训练需要大量时间,尤其是在普通 PC/笔记本电脑上。准备好观看训练代码运行几个小时,即使是在中等强大的基于 GPU 的机器上。
您可以在文件listing6_2_dqn_atari_pytorch.ipynb中找到在 PyTorch 中训练代理的完整代码。你可以在listing6_2_dqn_atari_tensorflow.ipynb的 TensorFlow 中找到相同的代码。体育馆图书馆已经在 Atari 图像上实现了许多所需的转换,只要有可能,我们将使用相同的东西。
现在让我们来讨论一下为了将图像像素值输入到深度学习网络而进行的图像预处理。我们将在一个名为 Breakout 的游戏中讨论这个问题,在这个游戏中,底部有一个桨,这个想法是移动桨以确保球不会掉到它下面。我们需要用桨击打并取出尽可能多的砖块。每当球错过球拍,玩家就失去一条生命。玩家有五次生命开始。图 6-3 显示了游戏的三帧画面。
图 6-3
雅达利突破游戏图片
Atari 游戏图像是具有 128 色调色板的 210×160 像素图像。我们将做预处理来修剪图像,使卷积网络运行得更快。我们缩小图像。我们还删除了侧面的一些信息,只保留图像的相关部分用于训练。我们可以再次将图像转换为灰度,以减少输入向量的大小,用一个灰度通道代替 RGB 的三个颜色通道(红、绿、蓝通道)。尺寸为(PyTorch 中的 1×84×84 或 TensorFlow 中的 84×84×1)的预处理单帧图像仅给出静态。球或桨的位置不能告诉我们两者运动的方向。相应地,我们将把几帧连续的游戏图像堆叠在一起,来训练代理。我们将叠加四幅缩小尺寸的灰度图像,这些图像将状态 s 输入神经网络。输入(即状态 s )在 PyTorch 中将是 4×84×84 的大小,在 TensorFlow 中将是 84×84×4 的大小,其中 4 是指游戏图像的四帧,84×84 是每帧的灰度图像大小。将四帧堆叠在一起将允许代理网络推断球和桨的运动方向。我们使用 Gym 的AtariPreprocessing来执行从 210×160×3 大小的彩色图像阵列到 84×84 的灰度图像阵列的图像缩减。该函数还通过设置scale_obs=True.将单个像素值从范围(0,255)缩小到(0.0,1.0)。接下来,我们使用FrameStack将前面讨论的四幅图像叠加在一起。最后,根据最初的方法,我们还将奖励值裁剪为-1 或 1。清单 6-9 给出了执行所有这些转换的代码。
from gym.wrappers import AtariPreprocessing
from gym.wrappers import FrameStack
from gym.wrappers import TransformReward
def make_env(env_name, clip_rewards=True, seed=None):
env = gym.make(env_name)
if seed is not None:
env.seed(seed)
env = AtariPreprocessing(env, screen_size=84, scale_obs=True)
env = FrameStack(env, num_stack=4)
if clip_rewards:
env = TransformReward(env, lambda r: np.sign(r))
return env
Listing 6-9Train the Agent in PyTorch
前面的预处理步骤产生了我们将输入网络的最终状态。这在 PyTorch 中将是 4×84×84 的大小,在 TensorFlow 中将是 84×84×4 的大小,其中 4 是指游戏图像的四帧,84×84 是每帧的灰度图像大小。图 6-4 显示了网络的输入。
图 6-4
经处理的图像将用作神经网络的状态输入
接下来,我们构建神经网络,该网络将接收之前的图像,即状态/观察值 s ,并为这种情况下的所有四个动作产生 q 值。这个游戏的动作是['NOOP', 'FIRE', 'RIGHT', 'LEFT'],用空格键开始,也就是开火,按键盘上的 A 向左移动拨片,按 D 向右移动拨片,最后按 Esc 退出游戏。以下是我们将要构建的网络的规格:
input: tensorflow: [batch_size, 84, 84, 4]
pytorch: [batch_size, 4, 84, 84]
1st hidden layer: 16 nos of 8x8 filters with stride 4 and ReLU activation
2nd hidden layer: 32 nos of 4x4 filters with stride of 2 and ReLU activation
3nd hidden layer: Linear layer with 256 outputs and ReLU activation
output layer: Linear with “n_actions” units with no activation
其余的代码与我们之前的代码相似。清单 6-10 和清单 6-11 分别显示了 PyTorch 和 TensorFlow 中修改后的 DQN 代理的代码。
class DQNAgent:
def __init__(self, state_shape, n_actions, epsilon=0):
super().__init__()
self.epsilon = epsilon
self.n_actions = n_actions
self.state_shape = state_shape
# a simple NN with state_dim as input vector (inout is state s)
# and self.n_actions as output vector of logits of q(s, a)
self.model = tf.keras.models.Sequential()
self.model.add(tf.keras.Input(shape=state_shape))
self.model.add(tf.keras.layers.Conv2D(16, kernel_size=8, strides=4, activation="relu"))
self.model.add(tf.keras.layers.Conv2D(32, kernel_size=4, strides=2, activation="relu"))
self.model.add(tf.keras.layers.Flatten())
self.model.add(tf.keras.layers.Dense(256, activation="relu"))
self.model.add(tf.keras.layers.Dense(n_actions))
def __call__(self, state_t):
# pass the state at time t through the newrok to get Q(s,a)
qvalues = self.model(state_t)
return qvalues
def get_qvalues(self, states):
# input is an array of states in numpy and outout is Qvals as numpy array
qvalues = self.model(states)
return qvalues.numpy()
def sample_actions(self, qvalues):
# sample actions from a batch of q_values using epsilon greedy policy
epsilon = self.epsilon
batch_size, n_actions = qvalues.shape
random_actions = np.random.choice(n_actions, size=batch_size)
best_actions = qvalues.argmax(axis=-1)
should_explore = np.random.choice(
[0, 1], batch_size, p=[1-epsilon, epsilon])
return np.where(should_explore, random_actions, best_actions)
Listing 6-11DQN Agent in TensorFlow
class DQNAgent(nn.Module):
def __init__(self, state_shape, n_actions, epsilon=0):
super().__init__()
self.epsilon = epsilon
self.n_actions = n_actions
self.state_shape = state_shape
state_dim = state_shape[0]
# a simple NN with state_dim as input vector (inout is state s)
# and self.n_actions as output vector of logits of q(s, a)
self.network = nn.Sequential()
self.network.add_module('conv1', nn.Conv2d(4,16,kernel_size=8, stride=4))
self.network.add_module('relu1', nn.ReLU())
self.network.add_module('conv2', nn.Conv2d(16,32,kernel_size=4, stride=2))
self.network.add_module('relu2', nn.ReLU())
self.network.add_module('flatten', nn.Flatten())
self.network.add_module('linear3', nn.Linear(2592, 256)) #2592 calculated above
self.network.add_module('relu3', nn.ReLU())
self.network.add_module('linear4', nn.Linear(256, n_actions))
self.parameters = self.network.parameters
def forward(self, state_t):
# pass the state at time t through the newrok to get Q(s,a)
qvalues = self.network(state_t)
return qvalues
def get_qvalues(self, states):
# input is an array of states in numpy and outout is Qvals as numpy array
states = torch.tensor(states, device=device, dtype=torch.float32)
qvalues = self.forward(states)
return qvalues.data.cpu().numpy()
def sample_actions(self, qvalues):
# sample actions from a batch of q_values using epsilon greedy policy
epsilon = self.epsilon
batch_size, n_actions = qvalues.shape
random_actions = np.random.choice(n_actions, size=batch_size)
best_actions = qvalues.argmax(axis=-1)
should_explore = np.random.choice(
[0, 1], batch_size, p=[1-epsilon, epsilon])
return np.where(should_explore, random_actions, best_actions)
Listing 6-10DQN Agent in PyTorch
您会注意到 PyTorch 和 TensorFlow 在急切执行模式下的代码相似。建议你专注于一个框架并掌握概念。一旦你掌握了一个,将代码移植到另一个框架就很容易了。我们将在本书的大部分例子中使用 PyTorch,并在一些地方使用 TensorFlow 版本。
除了这两个变化,即一些特定问题的预处理和一个适合问题的神经网络,其余代码在CartPole和 Atari 之间保持不变。您还可以使用雅达利版本在雅达利游戏的任何版本上培训代理。此外,除了这两个变化之外,相同的代码可以用于为任何环境训练 DQN 代理。您可以从健身房库文档中查找可用的健身房环境,并尝试修改来自listing6_1_dqn_pytorch.ipynb或listing6_1_dqn_atari_pytorch.ipynb的代码,以便为不同的环境培训代理。
这就完成了 DQN 的实施和培训。现在,我们知道了如何训练 DQN 代理,我们将研究一些问题和各种方法,我们可以采取修改 DQN。正如我们在本章开始时谈到的,我们将看看一些最新的和最先进的变化。
优先重放
在前一章中,我们了解了如何在 DQN 中使用批量版本的更新来解决在线版本中存在的一些关键问题,更新是在每次转换时完成的,而转换会在学习一步后立即被丢弃。以下是在线版本中的关键问题:
-
训练样本(转换)是相关的,打破了独立同分布假设。在网上学习中,我们有一系列相互关联的转变。每个过渡都与前一个过渡相链接。这打破了应用梯度下降所需的内径假设。
-
随着代理学习和丢弃,它可能永远不会访问初始的探索性转换。如果代理走上了一条错误的道路,它将继续从状态空间的那一部分看到例子。它可能会选择一个非常次优的解决方案。
-
对于神经网络,基于单个转换的学习是困难且低效的。对于神经网络来说,将会有太多的变化来学习任何有效的东西。神经网络在成批学习训练样本时效果最佳。
这些问题在 DQN 通过使用存储所有过渡的体验重放得到了解决。每个跃迁都是一个(state, action, reward, next_state, done)元组。随着缓冲区变满,我们丢弃旧的样本来添加新的样本。然后,我们从当前缓冲区中抽样一批,缓冲区中的每个转换在一批中被选中的概率相等。它允许从缓冲区中多次选取罕见的和更具探索性的转换。然而,一个普通的体验回放没有任何方法来选择一些优先的重要过渡。以某种方式为存储在重放缓冲器中的每个过渡分配一个重要性分数,并使用这些重要性分数作为选择的概率从缓冲器中对批次进行采样,为重要的过渡分配较高的选择概率,如它们各自的重要性分数所表示的,这是否会有所帮助?
这就是来自 DeepMind 的论文《优先化体验回放》 2 的作者在 2016 年探索的。我们将遵循本文的主要概念来创建我们自己的体验重放实现,并将其应用于CartPole环境的 DQN 代理。让我们先谈一谈这些重要性分数是如何分配的,以及损失 L 是如何修改的。
本文的主要方法是利用训练样本的时延误差为缓冲区中的训练样本分配重要性分数。当从缓冲区中选取一批样本时,我们计算 TD 误差,作为损耗 L 计算的一部分。TD 误差由下式给出:
(6.6)
它出现在我们计算损失的方程式( 6.3 )中。对所有样本的误差进行平方和平均,以计算权重向量的更新幅度,如等式( 6.4 和( 6.5 )所示。TD 误差的幅度 δ i 表示采样跳变 (i) 对更新的贡献。作者使用这种推理来为每个样本分配重要性分数 p i ,其中 p i 由以下等式给出:
(6.7)
增加一个小常数ε,避免 TD 误差 δ i 为零时 p i 为零的边缘情况。当一个新的转换被添加到缓冲器中时,我们将缓冲器中所有当前转换的最大值 p i 分配给它。当选择一个批次进行训练时,我们计算每个样本的 TD 误差 δ i 作为损失/梯度计算的一部分。然后,这个 TD 误差被用于更新缓冲器中这些样本的重要性分数。
本文还谈到了另一种基于等级的优先级排序方法。使用该方法,,其中等级 ( i )是当基于| * δ * * i * |对重放缓冲器转换进行排序时转换(I)的等级。在我们的代码示例中,我们将使用第一种方法,称为比例优先。
接下来,在采样时,我们通过使用以下等式将 p i 转换成概率:
(6.8)
这里, P ( i )表示缓冲器中的转换(I)被采样并作为训练批次的一部分的概率。这为具有较高 TD 误差的转换分配了较高的采样概率。这里,α是一个超参数,使用网格搜索进行了调整,作者发现 α = 0.6 是我们将要实现的比例变量的最佳值。
先前用某种基于重要性的采样来打破均匀采样的方法引入了偏差。我们需要在计算损耗 L 时修正偏差。在本文中,通过用权重 w i 对每个样本进行加权,然后求和得到修正的损失函数 L ,使用重要性抽样对其进行了修正。计算重量的公式如下:
(6.9)
这里, N 是训练批次中的样本数, P ( i )是前面的表达式中计算出的选择样本的概率。β是另一个超参数,我们将使用论文中的值 0.4。权重由进一步标准化,以确保权重保持在界限内。
(6.10)
有了这些适当的改变,损耗 L 等式也被更新,以用wI对批次中的每个转变进行加权,如下所示:
(6.11)
注意等式中的 w i 。在计算出 L 之后,我们使用损失梯度相对于在线神经网络权重 w 的反向传播来遵循通常的梯度步骤。
请记住,上一个等式中的 TD 误差用于为当前训练批次中的这些转换更新重放缓冲区中的重要性分数。这就完成了对优先重放的理论讨论。我们现在来看看实现。在listing6_3_dqn_prioritized_replay.ipynb中给出了用优先重放训练 DQN 代理的完整代码,它有两种风格,一种在 PyTorch 中,另一种在 TensorFlow 中。然而,从现在开始,我们将只列出 PyTorch 版本。建议您在阅读完下面给出的解释后,详细研究代码和参考文件。跟踪学术论文并将论文中的细节与工作代码相匹配的能力是成为一名优秀实践者的重要组成部分。解释只是让你开始。为了牢固地掌握材料,你应该详细地遵循伴随的代码。如果你在吸收了代码的工作原理后,尝试自己编码,那就更好了。
回到解释上来,我们首先看一下优先重放实现,这是与以前的 DQN 培训笔记相比,代码中的主要变化。清单 6-12 给出了优先重放的代码。大部分代码与我们之前看到的普通代码ReplayBuffer相似。我们现在有一个名为self.priorities的附加数组来保存每个样本的重要性/优先级分数 p i 。修改add函数,将 p i 赋值给正在添加的新样本。它只是数组self.priorities中的最大值。功能sample是变化最大的一个。使用等式( 6.8 )计算第一个概率,然后使用( 6.9 )和( 6.10 )计算权重。该函数现在返回另外两个数组:权重数组np.array(weights)和索引数组np.array(idxs)。索引数组包含批次中采样的缓冲区中样本的索引。这是必需的,以便在丢失步骤中计算 TD 误差之后,我们可以更新缓冲器中的优先级/重要性。功能update_priorities(idxs, new_priorities)正是为了这个目的。
class PrioritizedReplayBuffer:
def __init__(self, size, alpha=0.6, beta=0.4):
self.size = size #max number of items in buffer
self.buffer =[] #array to holde buffer
self.next_id = 0
self.alpha = alpha
self.beta = beta
self.priorities = np.ones(size)
self.epsilon = 1e-5
def __len__(self):
return len(self.buffer)
def add(self, state, action, reward, next_state, done):
item = (state, action, reward, next_state, done)
max_priority = self.priorities.max()
if len(self.buffer) < self.size:
self.buffer.append(item)
else:
self.buffer[self.next_id] = item
self.priorities[self.next_id] = max_priority
self.next_id = (self.next_id + 1) % self.size
def sample(self, batch_size):
priorities = self.priorities[:len(self.buffer)]
probabilities = priorities ** self.alpha
probabilities /= probabilities.sum()
N = len(self.buffer)
weights = (N * probabilities) ** (-self.beta)
weights /= weights.max()
idxs = np.random.choice(len(self.buffer), batch_size, p=probabilities)
samples = [self.buffer[i] for i in idxs]
states, actions, rewards, next_states, done_flags = list(zip(*samples))
weights = weights[idxs]
return (np.array(states), np.array(actions), np.array(rewards),
np.array(next_states), np.array(done_flags), np.array(weights), np.array(idxs))
def update_priorities(self, idxs, new_priorities):
self.priorities[idxs] = new_priorities+self.epsilon
Listing 6-12Prioritized Replay
接下来,我们来看看损失计算。代码几乎类似于我们在清单 6-5 中看到的 TD 损耗计算。有两个变化。第一个是将 TD 误差乘以权重,符合等式( 6.11 )。第二个变化是从函数内部调用update_priorities来更新缓冲区中的优先级。清单 6-13 显示了修改后的TD_loss compute_td_loss_priority_replay计算的代码。
def compute_td_loss_priority_replay(agent, target_network, replay_buffer,
states, actions, rewards, next_states, done_flags, weights, buffer_idxs,
gamma=0.99, device=device):
# convert numpy array to torch tensors
states = torch.tensor(states, device=device, dtype=torch.float)
actions = torch.tensor(actions, device=device, dtype=torch.long)
rewards = torch.tensor(rewards, device=device, dtype=torch.float)
next_states = torch.tensor(next_states, device=device, dtype=torch.float)
done_flags = torch.tensor(done_flags.astype('float32'),device=device,dtype=torch.float)
weights = torch.tensor(weights, device=device, dtype=torch.float)
# get q-values for all actions in current states
# use agent network
predicted_qvalues = agent(states)
# compute q-values for all actions in next states
# use target network
predicted_next_qvalues = target_network(next_states)
# select q-values for chosen actions
predicted_qvalues_for_actions = predicted_qvalues[range(
len(actions)), actions]
# compute Qmax(next_states, actions) using predicted next q-values
next_state_values,_ = torch.max(predicted_next_qvalues, dim=1)
# compute "target q-values"
target_qvalues_for_actions = rewards + gamma * next_state_values * (1-done_flags)
#compute each sample TD error
loss = ((predicted_qvalues_for_actions - target_qvalues_for_actions.detach()) ** 2) * weights
# mean squared error loss to minimize
loss = loss.mean()
# calculate new priorities and update buffer
with torch.no_grad():
new_priorities = predicted_qvalues_for_actions.detach() - target_qvalues_for_actions.detach()
new_priorities = np.absolute(new_priorities.detach().numpy())
replay_buffer.update_priorities(buffer_idxs, new_priorities)
return loss
Listing 6-13TD Loss with Prioritized Replay
训练代码和以前一样。可以看看listing6_3_dqn_prioritized_replay_pytorch.ipynb笔记本看详情。像以前一样,我们训练代理人,我们可以看到代理人学会了用这种方法很好地平衡杆子。图 6-5 显示了训练曲线。
图 6-5
DQN 代理人的训练曲线,具有在钢管上优先重放的经验
这就完成了关于优先重放的部分。建议您参考原始论文和代码笔记本以了解更多详细信息。
双 Q 学习
您在第五章中看到,使用相同的网络来选择最大化行动以及该最大化行动的 q 值会导致高估偏差,进而可能导致次优政策。论文《深度强化学习与双 Q 学习》的作者首先从数学上探讨了这种偏差,然后在 DQN 关于雅达利游戏的背景下进行了探讨。
让我们看看常规 DQN 中的 max 运算。我们计算 TD 目标如下:
我们通过去掉下标( i )以及去掉(1-done)乘数(去掉了终态的第二项)来稍微简化这个等式。我们这样做是为了保持解释的整洁。现在,让我们通过将“ max ”移入来解开这个等式。先前的更新可以等价地写成如下:
我们通过首先采取最大动作,然后采取该最大动作的 q 值,将最大值移到了内部。这类似于直接取最大 q 值。在之前的展开方程中,我们可以清楚地看到,我们使用了相同的网络权重,首先用于选择最佳动作,然后用于获得该动作的 q 值。这就是导致最大化偏差的原因。该论文的作者提出了一种他们称为双 DQN (DDQN)的方法,其中用于选择最佳行动的权重
来自具有权重*t的在线网络,然后具有权重
的目标网络用于选择最佳行动的 q 值。这一变化导致更新的 TD 目标如下:*
*注意,现在用于选择最佳行动的内部网络使用在线权重 w t 。其他一切保持不变。我们像以前一样计算损失,然后使用梯度步长来更新在线网络的权重。我们还定期用来自在线网络的权重更新目标网络权重。我们使用的更新损失函数如下:
(6.12)
作者表明,前面的方法导致高估偏差的显著减少,这反过来导致更好的政策。现在让我们来看看实现细节。与 DQN 实施相比,唯一会改变的是损失的计算方式。我们现在将使用方程 6.12 来计算损失。其他的一切,包括 DQN 代理代码,重放缓冲区,以及通过梯度反向传播进行训练的方式,都将保持不变。清单 6-14 给出了修正的损失函数计算。我们用q_s = agent(states)计算当前的 q 值,然后,对于每一行,选择对应于动作 a i 的 q 值。然后我们使用代理网络来计算下一个状态的 q 值:q_s1 = agent(next_states)。这用于查找每行的最佳动作,然后我们使用具有最佳动作的目标网络来查找目标 q 值。
def td_loss_ddqn(agent, target_network, states, actions, rewards, next_states, done_flags,
gamma=0.99, device=device):
# convert numpy array to torch tensors
states = torch.tensor(states, device=device, dtype=torch.float)
actions = torch.tensor(actions, device=device, dtype=torch.long)
rewards = torch.tensor(rewards, device=device, dtype=torch.float)
next_states = torch.tensor(next_states, device=device, dtype=torch.float)
done_flags = torch.tensor(done_flags.astype('float32'),device=device,dtype=torch.float)
# get q-values for all actions in current states
# use agent network
q_s = agent(states)
# select q-values for chosen actions
q_s_a = q_s[range(
len(actions)), actions]
# compute q-values for all actions in next states
# use agent network (online network)
q_s1 = agent(next_states).detach()
# compute Q argmax(next_states, actions) using predicted next q-values
_,a1max = torch.max(q_s1, dim=1)
#use target network to calclaute the q value for best action chosen above
q_s1_target = target_network(next_states)
q_s1_a1max = q_s1_target[range(len(a1max)), a1max]
# compute "target q-values"
target_q = rewards + gamma * q_s1_a1max * (1-done_flags)
# mean squared error loss to minimize
loss = torch.mean((q_s_a - target_q).pow(2))
return loss
Listing 6-14TD Loss with Double Q-Learning
q_s1 = agent(next_states).detach()
_,a1max = torch.max(q_s1, dim=1)
q_s1_target = target_network(next_states)
q_s1_a1max = q_s1_target[range(len(a1max)), a1max]
在CartPole上运行 DDQN 产生图 6-6 中给出的训练图。您可能没有注意到很大的区别,因为CartPole是一个太简单的问题,无法显示其优势。此外,我们已经运行了少量剧集的训练算法来演示这些算法。要了解该方法的量化优势,您应该查看参考文献。
图 6-6
扁担 DDQN 训练曲线
关于 DDQN 的讨论到此结束。接下来,我们看看决斗 DQN。
决斗 DQN
到目前为止,我们所有的网络都接收状态 S,并为状态 S 中的所有动作 A 产生 Q 值 Q ( S , A )。图 6-1 显示了这种网络的一个例子。然而,很多时候在特定的状态下,采取任何特定的动作都没有影响。考虑这样一种情况,一辆汽车行驶在路中间,而你的汽车周围没有汽车。在这样的场景中,稍微向左或向右,或稍微加速或稍微刹车的动作都没有影响;这些动作都产生相似的 q 值。有没有办法把一个状态的平均值和采取特定行动的优势比那个平均值分开?这就是题为“深度强化学习的决斗网络架构” 4 的论文作者在 2016 年采取的方法。他们表明,这导致了显著的改善,并且随着一个状态中可能的动作数量的增加,改善越大。
让我们来推导决斗 DQN 网络执行的计算。我们在第二章中的方程( 2)中看到了状态值和动作值函数的定义。9 )和( 2。10 ),现转载如下:
然后,在关于函数近似的第五章中,我们看到当我们切换到将状态/动作值表示为参数化函数时,这些方程随着参数 w 的引入而发生了一些变化。
两组方程都向我们展示了 v π 度量的是处于一般状态的值, q π 向我们展示了从状态 S 采取特定动作的值。如果我们从 V 中减去 Q ,我们会得到一个叫做优势 A 的东西。请注意,有一点符号超载。 A 内 Q ( S , A )代表动作,等式左边的A【π代表优势,而不是动作。
(6.13)
作者创建了一个网络,像以前一样接受状态 S 作为输入,在几层网络之后产生两个流,一个给出状态值 V ,另一个给出优势 A ,网络的一部分是单独的层集合,一个用于 V ,一个用于 A 。最后,最后一层结合优势 A 和状态值 V 来恢复 Q 。然而,为了具有更好的稳定性,他们做了一个额外的改变,从 Q ( S , A )的每个输出节点中减去优势值的平均值。神经网络实现的等式如下:
(6.14)
在上式中,权重 w 1 对应网络的初始公共部分, w 2 对应网络预测状态值的部分,最后 w 3 对应网络预测优势
的部分。图 6-7 显示了一个典型的网络架构。
图 6-7
决斗网。网络在初始层有一组公共的权重,然后它分支,一组权重产生值 V ,另一组产生优势 A
作者将这种架构命名为决斗网络,因为它有两个融合在一起的网络和一个初始的公共部分。由于决斗网络处于代理网络级别,因此它独立于其他组件,如重放缓冲器的类型或学习权重的方式(即,简单 DQN 或双 DQN)。因此,我们可以独立于重放缓冲器的类型或学习的类型来使用决斗网络。在我们的演练中,我们将使用一个简单的重放缓冲区,该缓冲区中的每个过渡都有统一的选择概率。此外,我们将使用 DQN 代理。与 DQN 相比,唯一的变化将是网络的构建方式。清单 6-15 显示了决斗代理网络的代码。
class DuelingDQNAgent(nn.Module):
def __init__(self, state_shape, n_actions, epsilon=0):
super().__init__()
self.epsilon = epsilon
self.n_actions = n_actions
self.state_shape = state_shape
state_dim = state_shape[0]
# a simple NN with state_dim as input vector (inout is state s)
# and self.n_actions as output vector of logits of q(s, a)
self.fc1 = nn.Linear(state_dim, 64)
self.fc2 = nn.Linear(64, 128)
self.fc_value = nn.Linear(128, 32)
self.fc_adv = nn.Linear(128, 32)
self.value = nn.Linear(32, 1)
self.adv = nn.Linear(32, n_actions)
def forward(self, state_t):
# pass the state at time t through the newrok to get Q(s,a)
x = F.relu(self.fc1(state_t))
x = F.relu(self.fc2(x))
v = F.relu(self.fc_value(x))
v = self.value(v)
adv = F.relu(self.fc_adv(x))
adv = self.adv(adv)
adv_avg = torch.mean(adv, dim=1, keepdim=True)
qvalues = v + adv - adv_avg
return qvalues
def get_qvalues(self, states):
# input is an array of states in numpy and outout is Qvals as numpy array
states = torch.tensor(states, device=device, dtype=torch.float32)
qvalues = self.forward(states)
return qvalues.data.cpu().numpy()
def sample_actions(self, qvalues):
# sample actions from a batch of q_values using epsilon greedy policy
epsilon = self.epsilon
batch_size, n_actions = qvalues.shape
random_actions = np.random.choice(n_actions, size=batch_size)
best_actions = qvalues.argmax(axis=-1)
should_explore = np.random.choice(
[0, 1], batch_size, p=[1-epsilon, epsilon])
return np.where(should_explore, random_actions, best_actions)
Listing 6-15Dueling Network
我们有两层公共网络(self.fc1和self.fc2)。对于 V 预测,我们在fc1和fc2之上还有另外两层(self.fc_value和self.value)。类似地,为了进行优势评估,我们再次在fc1和fc2之上设置了单独的两层(self.fc_adv和self.adv)。然后将这些输出组合起来,按照公式( 6.14 )给出修正的 q 值。代码的其余部分,如 TD 损失的计算和权重更新的梯度下降,与 DQN 保持相同。图 6-8 显示了在CartPole上训练前一个网络的结果。
图 6-8
决斗网络的训练曲线
就像我们说的,你可以试着用PrioritizedReplayBuffer.代替ReplayBuffer,他们也可以用双 DQN 代替 DQN 作为学习代理。关于决斗 DQN 的讨论到此结束。我们现在在下一节看一个非常不同的变体。
大声公 DQN
我们需要探索部分状态空间。我们一直在使用ε贪婪策略来这样做。在这个探索下,我们采取概率为(1- ε)的最大 q 值动作,我们采取概率为ε的随机动作。最近一篇题为“探索的嘈杂网络”的 2018 年论文的作者, 5 使用了一种不同的方法,将随机扰动添加到线性图层作为参数,像网络权重一样,这些也是学习的。
通常的线性层是仿射变换,如下式所示:
在噪声线性版本中,我们在权重中引入随机扰动,如下所示:
在前面的等式中,μw, σ w , μ b , σ b 是学习到的网络的权值。 ϵ w 和 ϵ b 是引入随机性导致探索的随机噪声。图 6-9 给出了线性层的噪点版本示意图,解释了我们上一段刚刚讲的方程。
图 6-9
噪声线性层。权重和偏差是均值和标准差的线性组合,就像常规线性图层中的权重和偏差一样
我们将实现论文中讨论的分解版本,其中矩阵的每个元素都被分解。假设我们有个 p 单位的投入和个 q 单位的产出。相应地,我们生成一个p-大小的高斯噪声向量ϵ * i 和一个q*-大小的高斯噪声向量ϵ * j * 。每个
和
现在可以写成如下形式:
对于我们正在使用的因式分解网络,我们建议您按如下方式初始化权重:
-
μ w 和 μ b 的每一个元素μIj,其中 p 为输入单元的个数。
-
同样,每个元素σ i , σ w 和 σ b 被初始化为常数
,超参数 σ 0 设置为 0.5。
我们沿着 PyTorch 提供的线性图层创建一个噪波图层。我们通过从 PyTorch 扩展nn.Module来实现。这是一个简单的标准实现,您可以在init函数中创建您的权重向量。然后你写一个forward函数来获取一个输入,并通过一组噪声线性和规则线性图层进行转换。你还需要一些额外的功能。在我们的例子中,我们编写了一个名为reset_noise的函数来生成噪声 ϵ w 和 ϵ b 。这个函数在内部使用了一个叫做_noise的辅助函数。我们还有一个功能reset_parameters来按照前面概述的策略重置参数。我们可以使用一个嘈杂的网络与 DQN,DDQN,决斗 DQN,并优先重播在各种组合。但是,出于演练的目的,我们将重点关注对 DQN 使用常规重放缓冲区。我们也使用常规的 DQN 方法训练,而不是 DDQN。清单 6-16 给出了噪声线性的代码。
class NoisyLinear(nn.Module):
def __init__(self, in_features, out_features, sigma_0 = 0.4):
super(NoisyLinear, self).__init__()
self.in_features = in_features
self.out_features = out_features
self.sigma_0= sigma_0
self.mu_w = nn.Parameter(torch.FloatTensor(out_features, in_features))
self.sigma_w = nn.Parameter(torch.FloatTensor(out_features, in_features))
self.mu_b = nn.Parameter(torch.FloatTensor(out_features))
self.sigma_b = nn.Parameter(torch.FloatTensor(out_features))
self.register_buffer('epsilon_w', torch.FloatTensor(out_features, in_features))
self.register_buffer('epsilon_b', torch.FloatTensor(out_features))
self.reset_noise()
self.reset_params()
def forward(self, x):
if self.training:
w = self.mu_w + self.sigma_w * self.epsilon_w
b = self.mu_b + self.sigma_b * self.epsilon_b
else:
w = self.mu_w
b = self.mu_b
return F.linear(x, w, b)
def reset_params(self):
k = 1/self.in_features
k_sqrt = math.sqrt(k)
self.mu_w.data.uniform_(-k_sqrt, k_sqrt)
self.sigma_w.data.fill_(k_sqrt*self.sigma_0)
self.mu_b.data.uniform_(-k_sqrt, k_sqrt)
self.sigma_b.data.fill_(k_sqrt*self.sigma_0)
def reset_noise(self):
eps_in = self._noise(self.in_features)
eps_out = self._noise(self.out_features)
self.epsilon_w.copy_(eps_out.ger(eps_in))
self.epsilon_b.copy_(self._noise(self.out_features))
def _noise(self, size):
x = torch.randn(size)
x = torch.sign(x)*torch.sqrt(torch.abs(x))
return x
Listing 6-16Noisy Linear Layer in PyTorch
实现的其余部分保持不变。现在唯一的不同是,我们在 DQN 代理的函数sample_actions中没有ε-贪婪选择。我们还有一个reset_noise功能,用于在每批之后重置噪声。这符合论文中去相关的建议。清单 6-17 包含了之前修改过的NoisyDQN版本。其余的实现类似于普通的 DQN 代理。
class NoisyDQN(nn.Module):
def __init__(self, state_shape, n_actions):
super(NoisyDQN, self).__init__()
self.n_actions = n_actions
self.state_shape = state_shape
state_dim = state_shape[0]
# a simple NN with state_dim as input vector (inout is state s)
# and self.n_actions as output vector of logits of q(s, a)
self.fc1 = NoisyLinear(state_dim, 64)
self.fc2 = NoisyLinear(64, 128)
self.fc3 = NoisyLinear(128, 32)
self.q = NoisyLinear(32, n_actions)
def forward(self, state_t):
# pass the state at time t through the newrok to get Q(s,a)
x = F.relu(self.fc1(state_t))
x = F.relu(self.fc2(x))
x = F.relu(self.fc3(x))
qvalues = self.q(x)
return qvalues
def get_qvalues(self, states):
# input is an array of states in numpy and outout is Qvals as numpy array
states = torch.tensor(states, device=device, dtype=torch.float32)
qvalues = self.forward(states)
return qvalues.data.cpu().numpy()
def sample_actions(self, qvalues):
# sample actions from a batch of q_values using greedy policy
batch_size, n_actions = qvalues.shape
best_actions = qvalues.argmax(axis=-1)
return best_actions
def reset_noise(self):
self.fc1.reset_noise()
self.fc2.reset_noise()
self.fc3.reset_noise()
self.q.reset_noise()
Listing 6-17NoisyDQN Agent in PyTorch
在CartPole环境中训练一个 NoisyDQN 产生训练曲线,如图 6-10 所示。我们可能看不出这个变体和 DQN(或者所有的变体)之间有什么显著的区别。原因是我们在用一个简单的问题,对它进行短集的训练。书中的想法是教你一个特定变体的内部细节。对于改进和其他观察的彻底研究,建议你参考原始论文。此外,我们再次强调,您应该详细阅读附带的 Python 笔记本,在掌握了细节之后,您应该尝试重新编写示例代码。
图 6-10
DQN 训练图
你也可以尝试编写一个吵闹版的决斗 DQN。此外,你也可以尝试学习的 DDQN 变体。换句话说,根据我们目前所学,我们可以尝试以下组合:
-
网络
-
DDQN(影响我们的学习方式)
-
决斗 DQN(影响训练架构)
-
决斗 DDQN
-
用优先重放缓冲区替换普通重放缓冲区
-
在任何以前的方法中,用 NoisyNets 代替ε-exploration
-
对 TensorFlow 上的所有组合进行编码
-
尝试许多其他健身房环境,对网络进行适当的更改(如果有的话)
-
在 Atari 上运行其中的一些,尤其是如果你有 GPU 机器的话
分类 51 原子 DQN (C51)
在 2017 年题为“强化学习的分布式视角”的论文中, 6 作者支持 RL 的分布式本质。他们没有看 Q 值这样的期望值,而是看了 Z ,一个期望值为 Q 的随机分布。
到目前为止,我们一直在输出输入状态 s 的 Q ( s , a )值。输出中的单元数量大小为n_action。在某种程度上,输出值是预期的 Q ( s , a ),使用蒙特卡洛技术对多个样本进行平均,以形成对实际预期值E**Q(s, a )的估计![。在分类的 51 原子 DQN 中,对于每个 Q ( s , a ) ( n_action其中),我们现在产生一个 Q ( s , a )值的分布估计:每个 Q ( s , a 的n_atom(准确地说是 51)值网络现在预测建模为分类概率分布的整个分布,而不是仅仅估计 Q ( s , a )的平均值。
pI(s, a )是动作值在( s , a )将为 z i 的概率。
我们现在有n_action * n_atom个输出,即n_action的每个值有n_atom个输出。此外,这些输出是概率。对于一个动作,我们有n_atom个概率,这些是 q 值在V_min到V_max范围内的任意一个n_atom离散值中的概率。你应该参考前面提到的论文了解更多细节。
在分布式 RL 的 C51 版本中,作者在值-10 到 10 上取𝑖为 51 个原子(支持点)。我们将在代码中使用相同的设置。因为这些值在代码中是参数化的,所以欢迎您更改它们并探究其影响。
应用贝尔曼更新后,值会发生变化,可能不会落在 51 个支持点上。有一个投影的步骤,将概率分布带回 51 个原子的支撑点。
损失也从均方误差替换为交叉熵损失。代理使用ε-贪婪策略进行训练,类似于 DQN。整个数学是相当复杂的,这将是一个很好的练习,让你把文章和代码一起过一遍,把每一行代码和文章中的具体细节联系起来。这是你作为 RL 实践者需要具备的一项重要技能。
类似于 DQN 方法,我们有一个类CategoricalDQN,它是神经网络,通过它将状态 s 作为输入来产生 Q 的分布Z(s, a )。有一个计算 TD 损耗的函数:td_loss_categorical_dqn。如前所述,我们需要一个投影步骤将值带回n_atom支持点,这在函数compute_projection中执行。计算损耗计算时,函数compute_projection在td_loss_categorical_dqn内使用。其余的训练和以前一样。
图 6-11 给出了在CartPole环境下运行的训练曲线。
图 6-11
分类 51 原子 DQN (C51)训练图
分位数回归 DQN
在关于 C51 算法的论文于 2017 年年中发表后不久,一些原始作者和其他几位作者(都来自 DeepMind)提出了一种变体,他们称之为分位数回归 DQN (QR-DQN)。在一篇题为“使用分位数回归的分布式强化学习” 7 的论文中,作者使用了一种与最初的 C51 略有不同的方法,但仍然在相同的分布焦点区域 RL 内。
类似于分布 RL 的 C51 方法,QR-DQN 方法也依赖于使用分位数来预测 Q ( s , a )的分布,而不是预测 Q ( s , a )的平均值的估计。C51 和 QR DQN 都是分布式 RL 的变体,由 DeepMind 的科学家制作。
C51 方法将名为 Z π (s,a)的 Q π (s,a)的分布建模为 V min 到 V max 范围内的概率过定点的分类分布。这些点上的概率就是网络所学习到的。这种方法导致在贝尔曼更新之后使用投影步骤,以将新的概率带回到固定支持点n_atoms,该固定支持点在Vmin到Vmax上均匀分布。虽然结果是可行的,但这与推导该算法的理论基础有点脱节。
QR-DQN 的方法略有不同。支撑点仍然是 N,但是现在概率固定为 1/N,这些点的位置由网络学习。引用作者的话:
我们“转置”来自 C51 的参数化:前者使用 N 个固定位置用于其近似分布并调整它们的概率,而我们将固定的、均匀的概率分配给 N 个可调整的位置。
DQ DQN 使用的损失是分位数回归损失与胡伯损失的混合。这叫做分位数胡伯损失;参考文献中的等式 9 和 10 给出了细节。我们没有在这里显示代码清单,因为我们希望您阅读论文,并将论文中的等式与笔记本listing6_8_qr_dqn_pytorch.ipynb中的代码进行匹配。这篇论文充满了数学,除非你对高等数学很熟悉,否则你应该试着把重点放在方法的更高层次的细节上。
图 6-12 为训练曲线。
图 6-12
分位数回归 DQN
马后炮经验回放
在 open ai 2018 年发表的题为“后知之明体验回放”的论文中, 8 作者提出了一种在回报稀少的环境中学习的有效方法。常见的方法是以某种方式塑造奖励函数,以引导代理朝向最优化。这是不能一概而论的。
与从成功的结果中学习的 RL 代理相比,人类似乎不仅从成功的结果中学习,也从不成功的结果中学习。这是被称为后见之明经验回放(她)的后见之明回放方法中提出的想法的基础。虽然她可以结合各种 RL 方法,在我们的代码走查,我们将使用她与决斗 DQN,给我们她-DQN。
在 HER 方法中,在一个情节结束后,比如说一个不成功的情节,我们形成一个次要目标,其中原始目标被终止前的最后一个状态代替,作为该轨迹的目标。
说一集已经打完: s 0 , s 1 ,…。s??T。通常我们在重放缓冲区中存储一个元组( s t , a t , r ,st+1, done )。假设这一集的目标是 g ,这一次无法实现。在 HER 方法中,我们将在重放缓冲区中存储以下内容:
- (s*| |,【a】***
**** (st|g′, a t ,r(st, a t , g 对奖励进行了修改,以显示状态转换st→st+1对于g′的子目标是好是坏。***
**原始文件讨论了形成这些子目标的各种策略。我们将使用一个名为 future 的,它是一个带有 k 个随机状态的重播,这些状态来自与正在重播的过渡相同的一集,并在它之后被观察到。
我们还使用了不同于以往笔记本电脑的环境。我们将使用一个比特翻转实验环境。假设您有一个 n 位向量,每一位都是{0,1}范围内的二进制数。因此,有 2 个 n 个 的组合可能。在重置时,环境以随机选择的 n 位配置开始,目标也随机选择为一些不同的 n 位配置。每个动作都是翻转一点。要翻转的位是代理试图学习的策略π( a | s )。如果代理能够找到与目标匹配的正确配置,或者当代理在一集中用尽了 n 个动作时,一集结束。清单 6-18 显示了环境的代码。完整的代码在笔记本listing6_9_her_dqn_pytorch.ipynb里。
class BitFlipEnvironment:
def __init__(self, bits):
self.bits = bits
self.state = np.zeros((self.bits, ))
self.goal = np.zeros((self.bits, ))
self.reset()
def reset(self):
self.state = np.random.randint(2, size=self.bits).astype(np.float32)
self.goal = np.random.randint(2, size=self.bits).astype(np.float32)
if np.allclose(self.state, self.goal):
self.reset()
return self.state.copy(), self.goal.copy()
def step(self, action):
self.state[action] = 1 - self.state[action] # Flip the bit on position of the action
reward, done = self.compute_reward(self.state, self.goal)
return self.state.copy(), reward, done
def render(self):
print("State: {}".format(self.state.tolist()))
print("Goal : {}\n".format(self.goal.tolist()))
@staticmethod
def compute_reward(state, goal):
done = np.allclose(state, goal)
return 0.0 if done else -1.0, done
Listing 6-18Bit-Flipping Environment
我们已经实现了我们自己的render和step功能,因此我们的环境界面仍然与 Gym 中的界面相似,这样我们就可以使用我们以前开发的机器。我们还有一个自定义函数compute_reward,当输入一个状态和一个目标时,返回reward和done标志。
作者表明,对于常规的 DQN,其中状态(n 位的配置)被表示为深度网络,常规的 DQN 代理几乎不可能学习超过 15 位的组合。然而,结合赫尔-DQN 方法,代理人能够很容易地学习甚至像 50 左右的大数字组合。在图 6-13 中,我们给出了论文中的完整伪代码,并做了一些修改,使其与我们的符号相匹配。
Hindsight Experience Replay (HER)
图 6-13
她使用了未来策略
我们使用决斗 DQN。代码中最有趣的部分是按照图 6-13 中给出的伪代码实现 HER 算法。清单 6-19 是该伪代码的逐行实现。
def train_her(env, agent, target_network, optimizer, td_loss_fn):
success_rate = 0.0
success_rates = []
exp_replay = ReplayBuffer(10**6)
for epoch in range(num_epochs):
# Decay epsilon linearly from eps_max to eps_min
eps = max(eps_max - epoch * (eps_max - eps_min) / int(num_epochs * exploration_fraction), eps_min)
print("Epoch: {}, exploration: {:.0f}%, success rate: {:.2f}".format(epoch + 1, 100 * eps, success_rate))
agent.epsilon = eps
target_network.epsilon = eps
successes = 0
for cycle in range(num_cycles):
for episode in range(num_episodes):
# Run episode and cache trajectory
episode_trajectory = []
state, goal = env.reset()
for step in range(num_bits):
state_ = np.concatenate((state, goal))
qvalues = agent.get_qvalues([state_])
action = agent.sample_actions(qvalues)[0]
next_state, reward, done = env.step(action)
episode_trajectory.append((state, action, reward, next_state, done))
state = next_state
if done:
successes += 1
break
# Fill up replay memory
steps_taken = step
for t in range(steps_taken):
# Usual experience replay
state, action, reward, next_state, done = episode_trajectory[t]
state_, next_state_ = np.concatenate((state, goal)), np.concatenate((next_state, goal))
exp_replay.add(state_, action, reward, next_state_, done)
# Hindsight experience replay
for _ in range(future_k):
future = random.randint(t, steps_taken) # index of future time step
new_goal = episode_trajectory[future][3] # take future next_state from (s,a,r,s',d) and set as goal
new_reward, new_done = env.compute_reward(next_state, new_goal)
state_, next_state_ = np.concatenate((state, new_goal)), np.concatenate((next_state, new_goal))
exp_replay.add(state_, action, new_reward, next_state_, new_done)
# Optimize DQN
for opt_step in range(num_opt_steps):
# train by sampling batch_size of data from experience replay
states, actions, rewards, next_states, done_flags = exp_replay.sample(batch_size)
# loss = <compute TD loss>
optimizer.zero_grad()
loss = td_loss_fn(agent, target_network,
states, actions, rewards, next_states, done_flags,
gamma=0.99,
device=device)
loss.backward()
optimizer.step()
target_network.load_state_dict(agent.state_dict())
success_rate = successes / (num_episodes * num_cycles)
success_rates.append(success_rate)
# print graph
plt.plot(success_rates, label="HER-DQN")
plt.legend()
plt.xlabel("Epoch")
plt.ylabel("Success rate")
plt.title("Number of bits: {}".format(num_bits))
plt.show()
Listing 6-19Hindsight Experience Replay Implementation
在代码中,我们使用之前编码的td_loss_dqn函数来计算 TD 损耗,并采取梯度步骤。我们还从ε=0.2 的非常探索性的行为策略开始,并在训练进行到一半时慢慢将其降低到零。代码的其余部分与图 6-13 中的伪代码逐行匹配。
图 6-14 为训练曲线。对于 50 位的BitFlipping环境,代理和她一起能够 100%成功地解决环境问题。请记住,环境以 50 位的随机组合作为起点,以另一个随机组合作为目标。代理最多有 50 个翻转动作来达到目标组合。彻底的搜索需要代理尝试 2 个 50 个组合中的每一个,除了最初开始的那个。
图 6-14
成功率图:她使用未来策略的比特翻转环境
这让我们结束了对她的讨论,也结束了这一章。
摘要
这是一个相当长的章节,我们看了 DQN 和它的大多数流行和最近的变种。
我们从快速回顾 Q-learning 和 DQN 更新方程的推导开始。然后,我们在 PyTorch 和 TensorFlow 中查看了一个简单的CartPole环境中 DQN 的实现。在这之后,我们研究了雅达利游戏,这是 2013 年的原始灵感,能够在强化学习的背景下使用深度学习。我们研究了额外的预处理步骤和网络从线性到基于卷积层的变化。
接下来,我们讨论了优先重放,其中根据分配给样本的某个重要性分数从缓冲区中选取样本,该分数与 TD 误差的大小成比例。
接下来,我们在 DQN 的背景下重新审视了双 Q 学习,即所谓的双 DQN。这是一种影响学习方式并试图减少最大化偏差的方法。
然后我们看了决斗 DQN,其中使用了两个网络和一个初始共享网络。随后是噪声层,ε贪婪的探索被噪声层所取代。
接下来,我们研究了两种类型的分布 RL,在这两种分布 RL 下,网络产生了 q 值的分布 Z 。它不是产生预期的动作值 Q ( S , A ),而是输出整个分布,特别是分类分布。我们还看到了投影步骤和损失的使用,如交叉熵和分位数 huber 损失。
最后一部分是关于后知之明的经验回放,它解决了在奖励稀少的环境中学习的问题。以前的学习方法集中在只从成功的结果中学习,但是事后诸葛亮也允许我们从不成功的结果中学习。
我们在本章看到的许多算法和方法都是最先进的研究。通过查看原始论文以及逐行浏览代码,您将会收获良多。我们还建议了各种组合,你可以尝试编码,以进一步巩固你脑海中的概念。
本章总结了我们对基于价值的方法的探索,在这种方法中,我们首先通过使用 V 或 Q 函数来学习策略,然后使用这些函数来找到最优策略。在下一章中,我们将切换到基于策略的方法,在这种方法中,我们找到最佳策略,而不需要学习 V /Q 函数这一中间步骤。
Footnotes 1https://arxiv.org/pdf/1312.5602.pdf
2
https://arxiv.org/pdf/1511.05952.pdf
3
https://arxiv.org/pdf/1509.06461v3.pdf
4
https://arxiv.org/pdf/1511.06581.pdf
5
https://arxiv.org/pdf/1706.10295.pdf
6
https://arxiv.org/pdf/1707.06887.pdf
7
https://arxiv.org/pdf/1710.10044.pdf
8
https://arxiv.org/pdf/1707.01495.pdf
***