七、策略梯度算法
到目前为止,我们一直专注于基于模型和无模型的方法。使用这些方法的所有算法都估计了给定当前策略的动作值。在第二步中,这些估计值被用于通过选择给定状态中的最佳动作来找到更好的策略。这两个步骤反复循环进行,直到观察不到数值的进一步提高。在这一章中,我们将通过直接在策略空间中操作来研究学习最优策略的不同方法。我们将在不明确学习或使用状态或状态行为值的情况下改进策略。
我们还将看到,基于策略的方法和基于价值的方法并不是两种不相交的方法。有些方法将基于价值的方法和基于政策的方法结合起来,如行动者-批评家方法。
本章的核心将是建立定义,并从数学上推导出基于策略的优化的关键部分。基于策略的方法是目前强化学习中解决大规模连续空间问题最流行的方法之一。
介绍
我们首先从简单的基于模型的方法开始我们的旅程,其中我们通过迭代贝尔曼方程来解决小的、离散的状态空间问题。接下来,我们讨论了使用蒙特卡罗和时间差分方法的无模型设置。然后,我们使用函数近似将分析扩展到大的或连续的状态空间。特别是,我们将 DQN 及其许多变体视为政策学习的途径。
所有这些方法的核心思想是首先了解当前政策的价值,然后对政策进行迭代改进以获得更好的回报。这是使用广义政策迭代 (GPI) *的一般框架完成的。*如果你想一想,你会意识到我们的真正目标是学习一个好的政策,我们使用价值函数作为中间步骤来指导我们找到一个好的政策。
这种学习价值函数以改进策略的方法是间接的。与直接学习好的政策相比,学习价值观并不总是那么容易。考虑一下你在慢跑道上遇到一只熊的情况。你首先想到的是什么?你的大脑是否试图评估可能行动的状态(你面前的熊)和行动值(“冻结”、“抚摸熊”、“逃命”或“攻击熊”)?还是几乎确定地“运行”,即遵循概率为 1.0 的action="run"
策略?我确信答案是后者。让我们举另一个玩 Atari Breakout 游戏的例子,我们在前一章的 DQN 例子中使用的那个。考虑球几乎接近你的球拍的右边缘并远离球拍的情况(“状态”)。作为一个人类玩家,你会怎么做?你是否试图评估两个动作的状态动作值 Q ( s , a ),然后决定桨需要向右还是向左移动?还是只看状态,学会右移球拍避免球掉下来?同样,我肯定答案是第二个。在这两个例子中,后一种更容易的选择是学习直接行动,而不是先学习值,然后使用状态值在可能的选择中找到最佳行动。
基于政策的方法的利弊
前面的例子表明,在许多情况下,与学习值函数然后使用它们来学习策略相比,学习策略(在给定状态下采取什么行动)更容易。那么,为什么我们要经历像萨莎、Q-learning、DQN 等价值方法的途径呢?,一点都没有?好吧,政策学习虽然更容易,但也不是一帆风顺的。它有自己的一系列挑战,特别是基于我们目前的知识和可用的算法,这些挑战如下:
-
优势
-
更好的融合
-
在高维连续动作空间有效
-
学习随机政策
-
-
不足之处
-
通常收敛于局部最大值而不是全局最大值
-
政策评估效率低且差异大
-
详细阐述这几点,还记得 DQN 学习曲线吗?我们看到政策的价值在培训中变化很大。在“车杆子”问题中,我们看到分数(如上一章所有训练进度图左图所示)波动很大。对于更好的政策没有稳定的一致意见。基于策略的方法,特别是我们将在本章末尾讨论的一些附加控制,确保我们在学习过程中朝着更好的策略平稳地前进。
我们的行动空间一直是一个可能行动的小集合。即使在函数近似与 DQN 这样的深度学习相结合的情况下,我们的动作空间也仅限于个位数。这些动作是一维的。想象一下,试图一起控制行走机器人的各个关节。我们需要对机器人的每个关节做出决定,在机器人的给定状态下,这些单独的选择合在一起会做出一个完整的动作。此外,每个关节的单独动作不会是离散的。最有可能的是,这些动作,如马达的速度或手臂或腿需要移动的角度,将会在一个连续的范围内。基于策略的方法更适合处理这些操作。
在我们迄今为止看到的所有基于价值的方法中,我们总是学到一种最优策略——一种确定性策略,在这种策略中,我们确切地知道在给定状态下应该采取的最佳行动。实际上,我们不得不引入探索的概念,使用ε-贪婪策略来尝试不同的行动,随着代理人学会采取更好的行动,探索概率会降低。最终的结果总是一个确定的政策。然而,确定性策略并不总是最优的。在有些情况下,最优策略是以某种概率分布采取多个行动,尤其是在多代理环境中。如果你有一些博弈论的经验,你会立即从囚徒困境和相应的纳什均衡中意识到这一点。不管怎样,我们来看一个简单的情况。
你玩过石头剪子布的游戏吗?这是一个双人游戏。在一个回合中,每个玩家必须从剪刀、石头或布三个选项中选择一个。两个玩家同时这样做,同时展示他们的选择。规则规定剪刀打败布是因为剪刀能剪出布,石头打败剪刀是因为石头能砸破剪刀,而布打败石头是因为纸能盖住石头。
什么是最好的政策?没有明显的赢家。如果你总是选择,比如说,摇滚,那么我作为你的对手会利用这些知识,总是选择纸。你能想到任何其他确定性的政策吗(例如,总是从三者中选择一个)?为了避免对手利用你的策略,你必须完全随机地做出选择。你必须以相等的概率随机选择剪刀或石头或布,即随机策略。确定性策略是随机策略的一种特殊形式,其中一个选择的概率为 1.0,所有其他行为的概率为零。随机策略更通用,这就是基于策略的方法所学习的。
以下是确定性策略:
)
换句话说,这是在状态 s 时要采取的具体动作 a 。
这是随机策略:
)
换句话说,这就是给定状态下 s 的动作概率分布。
也有不利之处。基于策略的方法虽然具有良好的收敛性,但可能收敛到局部最大值。第二大缺点是基于策略的方法不学习任何价值函数的直接表示,这使得评估给定策略的价值效率低下。评估策略通常需要使用策略播放代理的多个片段,然后使用这些结果来计算策略值,本质上是 MC 方法,这带来了估计策略值的高方差问题。我们将通过结合基于价值的方法和基于政策的方法这两个领域的优点,找到解决这一问题的方法。这就是所谓的演员兼评论家算法家族。
策略表示
在前一章中,我们讨论了函数近似的无模型设置,我们在方程( 5)中表示了值函数。1 )如下:
)
)
我们有一个权重为 w 的模型(线性模型或神经网络)。我们用由权重 w 参数化的函数来表示状态值 v 和状态动作值 q 。相反,我们现在将直接参数化策略,如下所示:
)
离散案例
对于不太大的离散动作空间,我们实际上会参数化另一个函数 h ( s ,a;θ)用于状态-动作对。概率分布将使用 h 的软最大值形成。
)
值 h ( s ,a;θ)被称为逻辑或动作偏好。这类似于我们在监督分类情况下采用的方法。在监督学习中,我们输入观察值 X ,在 RL 中,我们将状态 S 输入到模型中。在监督情况下,模型的输出是属于不同类别的输入 X 的逻辑值。而在 RL 中,模型的输出是采取该特定动作的动作偏好 ha。
连续案例
在连续动作空间中,策略的高斯表示是自然的选择。假设我们的行动空间是连续和多维的,比如说,维度为 d 。我们的模型将以状态 S 作为输入,并产生多维均值向量∈ R d 。方差σ2Id也可以参数化或者可以保持不变。代理将遵循的策略是具有均值 μ 和方差σ2Id的高斯策略。
)
政策梯度推导
推导基于策略的算法的方法类似于我们在监督学习中所做的。下面是我们提出算法的步骤概要:
-
我们形成一个我们想要最大化的目标,就像监督学习一样。这将是遵循一项政策所获得的全部回报。这将是我们希望最大化的目标。
-
我们将导出梯度更新规则来执行梯度上升。我们正在做梯度上升,而不是梯度下降,因为我们的目标是最大化总平均奖励。
-
我们需要将梯度更新公式重新转换为期望值,以便梯度更新可以使用样本来近似。
-
我们将正式地将更新规则转换成一个算法,该算法可以与 PyTorch 和 TensorFlow 等自动微分库一起使用。
目标函数
让我们从我们想要最大化的目标开始。正如前面列表中第一个项目符号所强调的,它将是策略的价值,即代理通过遵循策略可以获得的奖励。预期回报的表现形式有很多变化。我们将看看其中的一些,并简要讨论何时使用哪种表示法的背景。然而,算法的详细推导将使用其中一个变体来完成,因为其他奖励公式的推导非常相似。奖励函数及其变体如下:
-
未打折的插曲 :
)
-
偶发性打折 :
)
-
无限地平线打折 :
)
-
平均奖励 :
)
我们的大部分推导将遵循一个不确定的奖励结构,只是为了保持数学简单,并专注于推导的关键方面。
我也想让你感受一下折现因子 γ 。折扣用于无限公式中,以保持总和有界。通常,我们使用 0.99 或类似的折扣值来获得理论上有界的总和。在某些公式中,贴现因子还扮演着利息的角色——例如,今天的奖励比明天同样的奖励更有价值。使用一个折扣因子带来了今天有利于奖励的概念。贴现因子也用于通过提供时间范围的软截止来减少估计中的方差。
假设你在每一个时间步都得到 1 的回报,你使用的贴现因子是γ。这个无穷级数的和是)。假设我们有 γ = 0.99。无穷级数和等于 100。因此,你可以认为 0.99 的折扣将你的视野限制在 100 步,在这 100 步中,你每一步都获得 1 英镑的奖励,这样你总共获得 100 英镑。
总的来说,折现率γ意味着时间跨度为)步。
使用γ还可以确保在轨迹的初始阶段改变政策行动的影响比在轨迹的后期阶段决策的影响对政策的整体质量有更大的影响。
回到推导,现在让我们计算用于改进策略的梯度更新。代理遵循由 θ 参数化的策略。
策略由 θ 参数化。
)
(7.1)
代理遵循策略并生成轨迹τ,如下所示:
)
这里, s T 不一定是终点状态,而是某个时间范围 T 直到我们考虑的轨迹。
轨迹的概率τ取决于转移概率p(st+1|st, a t )和政策πθ(at|)它由以下表达式给出:
)
(7.2)
遵循策略π的预期回报由下式给出:
)
(7.3)
我们要找到使期望报酬/回报 J (θ)最大化的θ。换句话说,最优θ=θ∫由以下表达式给出:
)
(7.4)
在我们继续之前,让我们看看我们将如何评估目标 J (θ)。我们将( 7.3 中的期望值转换为样本的平均值;也就是说,我们通过策略多次运行代理,收集 N 条轨迹。我们计算每条轨迹中的总奖励,并对跨越 N 条轨迹的总奖励取平均值。这是期望值的蒙特卡罗(MC)估计。这就是我们谈论评估政策时的意思。我们得到的表达式如下:
)
(7.5)
导数更新规则
继续,让我们试着找到最优θ。为了让记法更容易理解,我们将∑tr(st, a t )替换为 r (τ)。重写( 7.3 ),我们得到如下:
)
(7.6)
我们取上一个表达式相对于θ的梯度/导数。
)
(7.7)
利用线性,我们可以移动积分内的梯度。
)
(7.8)
用对数求导的小技巧,我们知道∇xf(x)=f(x)∇x对数 f ( x )。利用这一点,我们可以把前面的表达式( 7.8 )写成如下:
)
(7.9)
我们现在可以将积分写回期望值,这给出了下面的表达式:
)
(7.10)
让我们从方程( 7.2 )写出 p θ (τ)的完整表达式,从而展开术语∇θlogpθ(τ)。
)
(7.11)
我们知道,项数乘积的对数可以写成项数对数的和。换句话说:
)
(7.12)
将( 7.12 )代入方程( 7.11 ,得到如下结果:
)
(7.13)
( 7.13 )中唯一依赖于θ的项是πθ(at|st)。另外两个术语log p(s1)和 logp(st+1|st, a t )不依赖于θ。相应地,我们可以将前面的表达式( 7.13 )简化如下:
)
(7.14)
将方程( 7.14 )代入方程( 7.10 )中∇ θ J (θ)的表达式,并将 r (τ)展开为∑tr(sta
)
*(7.15)
现在,我们可以用多个轨迹的估计值/平均值替换外部预期,以获得政策目标的梯度的以下表达式:
)
(7.16)
其中上标索引 i 表示Ith轨迹。
为了改进政策,我们朝着∇θj(θ)的方向迈出了+ve 的一步。
)
(7.17)
综上所述,我们设计一个以状态 s 为输入的模型,并产生策略分布π θ ( a | s )作为模型的输出。我们使用由当前模型参数θ确定的策略来生成轨迹,计算每个轨迹的总回报。我们用( 7.16 )计算∇θj(θ),然后用( 7.17 )中的表达式θ = θ + α∇ θ J (θ)改变模型参数θ。
更新规则背后的直觉
让我们开发一些方程式背后的直觉( 7.16 )。让我们用文字来解释这个等式。我们对 N 条轨迹进行平均,这是最外面的和。轨迹的平均值是多少?对于每一条轨迹,我们查看我们在该轨迹中获得的总回报,并将其乘以该轨迹上所有行为的对数概率之和。
现在假设一条轨迹的总回报r(τI)为+ve。第一个内部和中的每个梯度——即),该行为对数概率的梯度——乘以总回报 r (τ * i )。它导致单个梯度对数项被轨迹的总回报放大,在方程( 7.17 )中,它的贡献是将模型参数θ向
)的+ve 方向移动,即增加系统处于状态
)时采取行动
)的概率。但是,如果r*(τI)是一个-ve 量,则方程( 7.16 )和( 7.17 )导致θ向-ve 方向移动,导致系统处于状态
)时采取动作
)的概率降低。
我们可以总结整个解释说,政策优化是所有关于试错。我们推出多种轨迹。对于那些好的轨迹,沿着轨迹的所有动作的概率增加。对于不良轨迹,沿着这些不良轨迹的所有动作的概率都降低了,如图 7-1 所示。
图 7-1
轨迹展开。轨迹 1 是好的,我们希望模型产生更多的轨迹。轨迹 2 不好也不坏,模型不要太担心。轨迹 3 是不好的,我们希望模型能降低它的概率
让我们通过比较( 7.17 )中的表达式和最大似然的表达式来看看同样的解释。如果我们只想对看到我们看到的轨迹的概率进行建模,我们会得到最大似然估计——我们观察到一些数据(轨迹),我们希望建立一个产生观察到的数据/轨迹的概率最高的模型。这是最大似然模型的建立。在这种情况下,我们将得到如下等式:
)
(7.18)
在方程( 7.18 )中,我们只是增加动作的概率来增加轨迹的整体概率。我们在( 7.16 )的政策梯度中做了同样的事情,只是我们用回报来衡量对数概率梯度,以便增加好的轨迹和减少坏的轨迹概率——而不是增加所有轨迹的概率。
在我们结束这一节之前,我们想做的一个观察是关于马尔可夫性质和部分可观测性。在推导过程中,我们并没有真正使用马尔可夫假设。最后方程( 7.16 )只是说增加好东西的概率,减少坏东西的概率。到目前为止,我们还没有使用过贝尔曼方程。政策梯度也适用于非马尔可夫结构。
强化算法
我们现在将方程( 7.16 )转换成策略优化的算法。我们给出了图 7-2 中的基本算法。它被称为强化。
REINFORCE
图 7-2
强化算法
让我们看看一些实现级别的细节。假设您使用神经网络作为模型,该模型将状态值作为输入,并生成在该状态下采取所有可能行动的 logit(对数概率)。图 7-3 显示了这种模型的示意图。
图 7-3
预测政策的神经网络模型
我们使用 PyTorch 或 TensorFlow 等自动分化库。我们不明确地计算微分。等式( 7.16 )给出了∇θj(θ)的表达式。用 PyTorch 或者 TensorFlow,我们需要一个表达式 J (θ)。神经网络模型会以状态 S 为输入,产生πθ(at|St)。我们需要使用这个输出,并执行进一步的计算,以得出 J (θ)的表达式。PyTorch 或 TensorFlow 等自动微分软件包将根据 J (θ)的表达式自动计算梯度∇ θ J (θ)。 J (θ)的正确表达式如下:
)
(7.19)
您可以检查并确认该表达式的梯度将为我们提供∇θj(θ)的正确值,如( 7.16 )所示。
( 7.19 中的表达式被称为伪目标。这是我们需要在 PyTorch 和 TensorFlow 等自动差异库中实现的表达式。我们计算对数概率),用轨迹的总回报
)对概率进行加权,然后计算加权量的负对数似然(NLL,或交叉熵损失),给出我们在( 7.20 中的表达式。这类似于我们在监督学习设置中用于训练多类分类模型的方法。唯一的区别是用轨迹回报来衡量对数概率。这是我们在行动离散时将采取的方法。我们在 PyTorch/TensorFlow 中实现的损失如下:
)
(7.20)
请注意 PyTorch 和 TensorFlow 通过向损失的负方向迈出一步来最小化损失。还要注意( 7.20 )的-ve 梯度是( 7.19 )的+ve 梯度,因为在( 7.20 )中存在-1 的因子。
接下来,我们看看动作连续的情况。如所讨论的,由θ参数化的模型将状态 S 作为输入,并产生多元正态分布的平均值μ。我们考虑的是正态分布的方差已知并固定为某个小值的情况,比如说σ2Id。
)
假设状态为),模型产生的平均值为
)。
)的值由下式给出:
)
)
(7.20)
上式中唯一依赖于模型参数θ的值是)。让我们取一个关于θ的梯度( 7.20 )。我们得到以下结果:
)
为了在 PyTorch 或 TensorFlow 中实现这一点,我们将形成一个修正的均方误差,就像我们对之前的离散操作采取的方法一样。我们用弹道返回来衡量均方误差。我们在 PyTorch 或 TensorFlow 中实现的损耗方程在( 7.21 )中给出。
)
(7.21)
再次注意,使用梯度 L MSE (θ)然后在梯度的-ve 方向上迈出一步将产生以下结果:
)
(7.22)
向−∇θlMSE(θ)方向的一步是向)方向的一步,如( 7.19 )中给出。这是一个尝试增加
)价值的步骤,即最大化保单回报。
总之,PyTorch 或 TensorFlow 中的实现要求我们在离散动作空间中形成交叉熵损失,或者在连续动作空间的情况下形成均方损失,其中每个损失项由对)来自的轨迹的总回报加权。这类似于我们在监督学习中采取的方法,除了额外的通过轨迹返回r(τI)=
)进行加权的步骤。
还请注意,加权交叉熵损失或加权均方损失没有任何意义或重要性。它只是一个方便的表达式,允许我们使用 PyTorch 和 TensorFlow 的 auto-diff 功能,通过反向传播计算梯度,然后采取措施改进策略。相比之下,在监督学习中,损失确实表明了预测的质量。在政策梯度的情况下,没有这样的推论或含义。这就是为什么我们称它们为伪亏损/目标。
带奖励的方差缩减
我们在方程式( 7.16 )中推导出的表达式,如果以目前的形式使用,就有问题。它有很高的方差。我们现在将利用问题的时间性质来做一些方差减少。
当我们推出政策(即根据政策采取行动)产生一条轨迹时,我们计算这条轨迹的总回报r(τI)。接下来,轨迹中动作的每个动作概率项由该轨迹回报加权。
然而,在一个时间步中采取的行动,比如说**’,只能影响我们在那个行动之后看到的回报。我们在时间步 t ‘ 之前看到的奖励不受我们在时间步 t ’ 采取的动作或任何后续动作的影响。原因是世界是因果的。未来的行动不会影响过去的回报。我们将使用该属性删除( 7.16 )中的某些术语,并减少差异。给出了推导修正公式的步骤。请注意,它不是严格的数学证明。**
**我们从方程开始( 7.15 )。
)
我们将奖励项的求和索引从 t 更改为t’,并将第一次求和中的和移到πθ上。这给出了下面的表达式:
)
在指标 t 总和的求和项中,我们去掉了时间 t 之前的奖励项。在时间 t 时,我们采取的行动只能影响在时间 t 及以后到来的奖励。这导致第二个内和从T‘=T变为 T ,而不是从 t ’ = 1 变为 T 。换句话说,开始索引现在是t'= t而不是***t =***1。修改后的表达式如下:
)
内部总和)不再是轨迹的总回报。而是我们从时间 = t 到 T 看到的剩余轨迹的回报。如您所知,这就是 q 值。q 值是我们在状态stttt时,在时间 t 采取一个步骤/动作 a * t 之后,从时间 t 开始直到结束,我们得到的预期奖励。我们也可以称之为赏去*。因为表达式
)只针对一条轨迹,我们表示它是对预期收益的估计。更新的梯度方程如下:
)
)
(7.23)
要在 PyTorch 或 TensorFlow 中使用这个方程,我们只需要做一个小小的修改。不是用总的轨迹回报来衡量每个对数概率项,我们现在用该时间步的剩余回报来衡量它;换句话说,我们用奖励去价值来衡量它。图 7-4 显示了一个使用 reward to go 的修正增强算法。
Reinforce With Reward to go
图 7-4
用奖励去强化算法
本章到目前为止我们已经做了很多理论,数学公式有点超载。我们试图保持它的最小化,如果到目前为止从这一章有什么收获的话,那就是图 7-4 中的加强算法。现在让我们把这个等式付诸实践。我们将从图 7-4 对我们通常的CartPole
问题用连续的状态空间和离散的动作来实现加强。
在此之前,我们先介绍最后一个数学术语。策略梯度算法中对状态-动作空间的探索来自于这样一个事实,即我们学习一个随机策略,该策略为给定状态的所有动作分配一个概率,而不是使用 DQN 选择最佳可能动作。为了确保探索得以维持,并确保π θ ( a | s )不会以高概率崩溃为单个动作,我们引入了一个正则化项,称为熵。分布的熵定义如下:
)
为了保持足够的探索,我们将希望概率具有分散的分布,并且不要让概率分布过早地在单个值或小区域附近达到峰值。分布的扩散越大,分布的熵 H(x)越高。因此,输入 PyTorch/TensorFlow 最小化器的项如下:
)
)
在我们的代码示例中,我们只采用一条轨迹,即 N = 1。但是,我们将对其进行平均,得出平均损失。我们将实际实现的功能如下:
)
)
在哪里,
)
请注意,我们在前面的表达式中重新引入了贴现因子γ。
现在让我们浏览一下实现。您可以在listing7_1_reinforce_pytorch.ipynb
中找到完整的代码清单。我们在listing7_1_reinforce_tensorflow.ipynb
中也有一个 TensorFlow 版本的代码。然而,我们将只浏览 PyTorch 版本。TensorFlow 版本遵循几乎相同的步骤,除了我们定义网络或计算损耗以及逐步通过梯度的方式略有不同。在我们的代码中,我们在急切执行模式下使用了 TensorFlow 2.0。
我们之前解释过环境。它有一个四维连续的状态空间和两个动作的离散动作空间:“左移”和“右移”。让我们首先定义一个简单的策略网络,它有一个 192 个单元的隐藏层和 ReLU 激活。最终输出没有激活。清单 7-1 显示了代码。
model = nn.Sequential(
nn.Linear(state_dim,192),
nn.ReLU(),
nn.Linear(192,n_actions),
)
Listing 7-1Policy Network in PyTorch
接下来,我们定义一个generate_trajectory
函数,该函数采用当前策略来生成一集的(states, actions, rewards)
轨迹。它使用一个助手函数predict_probs
来完成这项工作。清单 7-2 给出了代码。它从初始化环境开始,然后按照当前策略连续采取步骤,返回它展开的轨迹的(states, actions, rewards)
。
def generate_trajectory(env, n_steps=1000):
"""
Play a session and genrate a trajectory
returns: arrays of states, actions, rewards
"""
states, actions, rewards = [], [], []
# initialize the environment
s = env.reset()
#generate n_steps of trajectory:
for t in range(n_steps):
action_probs = predict_probs(np.array([s]))[0]
#sample action based on action_probs
a = np.random.choice(n_actions, p=action_probs)
next_state, r, done, _ = env.step(a)
#update arrays
states.append(s)
actions.append(a)
rewards.append(r)
s = next_state
if done:
break
return states, actions, rewards
Listing 7-2generate_trajectory in PyTorch
我们还有另一个助手函数,根据表达式)将单个步骤的回报
)转换为奖励。清单 7-3 包含了这个函数的实现。
def get_rewards_to_go(rewards, gamma=0.99):
T = len(rewards) # total number of individual rewards
# empty array to return the rewards to go
rewards_to_go = [0]*T
rewards_to_go[T-1] = rewards[T-1]
for i in range(T-2, -1, -1): #go from T-2 to 0
rewards_to_go[i] = gamma * rewards_to_go[i+1] + rewards[i]
return rewards_to_go
Listing 7-3get_rewards_to_go in PyTorch
我们现在准备实施培训。我们构建了损失函数,我们将把它输入 PyTorch 优化器。如前所述,我们将实现以下表达式:
)
清单 7-4 包含损失计算的代码。
#init Optimizer
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
def train_one_episode(states, actions, rewards, gamma=0.99, entropy_coef=1e-2):
# get rewards to go
rewards_to_go = get_rewards_to_go(rewards, gamma)
# 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_to_go = torch.tensor(rewards_to_go, device=device, dtype=torch.float)
# get action probabilities from states
logits = model(states)
probs = nn.functional.softmax(logits, -1)
log_probs = nn.functional.log_softmax(logits, -1)
log_probs_for_actions = log_probs[range(len(actions)), actions]
#Compute loss to be minized
J = torch.mean(log_probs_for_actions*rewards_to_go)
H = -(probs*log_probs).sum(-1).mean()
loss = -(J+entropy_coef*H)
optimizer.zero_grad()
loss.backward()
optimizer.step()
return np.sum(rewards) #to show progress on training
Listing 7-4Training for One Trajectory in PyTorch
我们现在准备进行训练。清单 7-5 展示了我们如何训练代理 10000 步,打印 100 步轨迹训练后的平均剧集奖励。一旦我们达到 300 的平均奖励,我们也停止训练。
total_rewards = []
for i in range(10000):
states, actions, rewards = generate_trajectory(env)
reward = train_one_episode(states, actions, rewards)
total_rewards.append(reward)
if i != 0 and i % 100 == 0:
mean_reward = np.mean(total_rewards[-100:-1])
print("mean reward:%.3f" % (mean_reward))
if mean_reward > 300:
break
env.close()
Listing 7-5Training the Agent in PyTorch
训练结束时,代理已经学会了很好地平衡杆子。您还会注意到,与基于 DQN 的方法相比,该程序实现这一结果所需的迭代次数和时间要少得多。
请注意,加强是一个基于策略的算法。
使用基线进一步减少差异
我们从( 7.15 中的原始政策梯度更新表达式开始,并使用( 7.16 中的平均值将期望值转换为估计值。接下来,我们展示了如何通过考虑奖励而不是全轨迹奖励来减少方差。等式( 7.23 )给出了这个奖励的表达式。
在这一节中,我们来看另一个使政策梯度更加稳定的变化。我们来考虑一下动机。假设您已经按照一个策略完成了三次轨迹的展开。假设奖励是 300,200,100。为了使解释简单,请考虑总报酬和总轨迹概率版本的梯度更新方程的情况,如方程( 7.10 )所示,复制如下:
)
那么,渐变更新会有什么作用呢?它会用 300 衡量第一个轨迹的对数概率的梯度,用 200 衡量第二个轨迹,用 100 衡量第三个轨迹。这意味着三个轨迹中的每一个的概率都增加了不同的量。让我们来看看它的图示,如图 7-5 所示。
图 7-5
具有实际轨迹回报的策略的梯度更新
从图中可以看出,我们用不同的权重因子增加了所有三个轨迹的概率,都是+ve 权重,使得所有轨迹的概率都上升了。理想情况下,我们会喜欢增加奖励为 300 的轨迹的概率,减少奖励为 100 的轨迹的概率,因为它不是一个很好的轨迹。我们希望政策能够改变,这样就不会经常产生奖励 100 的轨迹。然而,使用当前的方法,修正的概率曲线变得更平坦,因为它试图增加所有三个轨迹的概率,并且概率曲线下的总面积必须为 1。
让我们考虑一个场景,从三个回报中减去三个轨迹的平均回报。我们得到 100,0,和-100(300-200;200-200;100-200).让我们使用修正后的轨迹奖励作为权重来进行梯度更新。图 7-6 显示了这种更新的结果。我们可以看到,概率曲线变得越来越窄,越来越陡,因为它沿 x 轴的分布越来越小。
图 7-6
具有减少基线的轨迹奖励的策略的梯度更新
用基线减少奖励会减少更新的方差。在极限中,无论我们使用还是不使用基线,结果都是一样的。基线的引入不会改变最优解。它只是减少了差异,从而加快了学习。我们将从数学上证明基线的引入不会改变梯度更新的期望值。基线可以是跨越所有轨迹和轨迹中所有步骤的固定基线,或者可以是取决于状态的变化量。然而,它不能依赖于行动。让我们先来看看基线是状态函数的推导)。
让我们更新( 7.15 )中的等式,引入一个基线。
)
让我们把b(st)的项分离出来,评估期望会是什么。
)
由于期望的线性性质,我们将第一个内和移出,得到表达式。
)
我们把期望从τ∽pθ(τ)切换到了 at∼πθ(at|st)。这是因为我们将第一个内和移到了期望值之外,之后唯一依赖于概率分布的项就是概率为π的动作atθ(at|st)。
我们只关注内心的期待:)。我们可以把它写成积分,如下所示:
)
)
)
)
)
作为b(st)不依赖于 a t ,我们可以拿出来。同样,由于积分的线性,我们可以交换梯度和积分。现在积分将计算为 1,因为这是使用π θ 曲线的总概率。相应地,我们得到以下结果:
)
)
)
前面的推导告诉我们,减去一个依赖于状态或者可能是常数的基线不会改变期望值。条件是不应该依赖于动作 a t 。
因此,强化基线将经历如下更新:
)
(7.24)
我们可以用基线修改( 7.23 )中给出的奖励,得到以下结果:
)
)
(7.25)
方程 7.25 用基线和奖励来加强。我们使用了两个技巧来减少普通钢筋的差异。我们使用一个时间结构来移除过去的奖励,而不受现在的行为的影响。然后,我们使用基线让坏政策获得-ve 奖励,让好政策获得+ve 奖励,以使政策梯度在我们学习过程中显示较低的变化。
请注意,REINFORCE 及其所有变体都是基于策略的算法。政策权重更新后,我们需要推出新的轨迹。旧的轨迹不再代表旧的政策。这也是为什么像基于价值的政策方法一样,加强也是样本低效的原因之一。我们不能使用早期政策的过渡。我们必须在每次权重更新后丢弃它们并生成新的转换。
演员-评论家方法
在本节中,我们将通过将策略梯度与价值函数相结合来进一步完善算法,以获得所谓的行动者-批评家算法家族(A2C/A3C)。我们先来定义一个术语叫做优势 A ( s , a )。
定义优势
先说方程中的表达式)(7.25)。就是在给定的轨迹( i )和给定的状态 s * t * 中走下去的奖励。
)
为了使用前面的表达式评估)值,我们使用蒙特卡罗模拟。换句话说,我们正在累加从那个时间步 t 直到结束的所有奖励,即,直到 T 。它将再次具有高方差,因为它只是期望值的一个轨迹估计。在前一章无模型策略学习中,我们看到 MC 方法零偏差,但方差很大。相比之下,TD 方法有一些偏差,但方差较低,并且由于方差较低,可以导致更快的收敛。我们能在这里做类似的事情吗?这是什么奖励?表情
)有什么期待?无非是状态-动作对的 q 值( s * t , a t * )。如果我们可以得到 q 值,我们可以用 q 估计值代替个人奖励的总和。
)
(7.26)
让我们将q(st, a t )的值滚动一个时间步长。这类似于我们在第五章中看到的 TD(0)方法。我们可以这样写):
)
(7.27)
这是未打折的展示。正如本章开始时所讨论的,我们将在有限视界未贴现设置的背景下进行所有推导。该分析可以容易地扩展到其他设置。我们将在算法的最终伪代码中切换到更一般的情况,同时将我们的分析限制在未折扣的情况。
再看方程( 7.25 ),你能想到一个可以用的好基线 bIT5(st)吗?用状态值V(st)怎么样?如上所述,我们可以使用任何值作为基线,只要它不依赖于动作 a t 。V(st)就是这样一个依赖于状态 s t 而不依赖于动作*t的量。*
)
*(7.28)
使用前面的表达式:
)
(7.29)
右侧称为优势 A(st, a t )。它是我们在状态 s t 采取步骤 a t 所获得的额外收益/奖励,它给出的奖励是)相对于我们在状态 s * t * 所获得的平均奖励,用 V ( s 表示我们现在可以将方程式( 7.27 )代入( 7.29 )得到如下:
)
)
)
)
(7.30)
优势演员评论家
继续上一节,让我们根据前面的表达式重写等式( 7.25 )中给出的梯度更新。
这是来自方程( 7.25 )的原始梯度更新:
)
代入,bI(st)= V(st)由( 7.28 ),我们得到如下:
)
使用 MC 方法,我们得到:
)
或者,使用 TD(0)方法,我们得到这个:
)
(7.31)
看前面表达式中的内心表达式)。 Q 是使用当前策略遵循特定步骤 a * t 的值。换句话说,“actor”和 V 是下面当前政策的平均值,即“critical”演员试图最大化回报,评论家告诉算法,与平均水平相比,特定步骤是好是坏。行动者-批评者方法是一系列算法,其中行动者改变政策梯度以改进行动,而批评者*告知算法使用当前政策的行动的良好性。
我们可以用优势的形式重写( 7.30 )得到如下:
)
(7.32)
这种表达也是我们称之为优势演员评论家 (A2C)的原因。请注意,演员批评家是一个方法家族,A2C 和 A3C 是其中的两个具体例子。有时在文学中演员评论家也可互换地称为 A2C。与此同时,一些论文将 A2C 称为 A3C 的同步版本,我们将在下一节简要讨论。
我们可以进一步将( 7.32 )与( 7.30 )组合起来表达如下:
)
)
使用 actor critic 的修订更新规则如下:
)
)
(7.33)
我们需要两个网络,一个网络估计由参数 ϕ 参数化的状态值函数 V ( s t )另一个网络输出由 θ参数化的策略πθ(at|st图 7-7 显示了演员评论家的完整伪代码(也称为优势演员评论家 A2C)。
Advantage Actor-Critic Algorithm
图 7-7
优势行动者-批评家算法
请注意,在前面的伪代码中,我们使用了一步不打折返回来获得优势)。
)
折扣后的一步到位版本如下:
)
同样,n 步返回的折扣版本如下:
)
使用直接使用 rewards to go 的 MC 方法,优势如下:
)
这是我们将在代码中实现的版本。
A2C 算法的实现
我们来看看图 7-7 中伪代码的实现细节。我们需要两个网络/模型——一个是带有参数向量θ的政策网络(参与者),另一个是带有参数向量ϕ.的价值评估网络(批评家)在实际设计中,策略网络和价值评估网络可以共享一些初始权重。这类似于我们在前一章看到的决斗网络架构。这实际上是加快收敛的理想设计选择。图 7-8 给出了组合模型的示意图。
图 7-8
初始层具有公共权重的演员评论网络
在我们的代码遍历中,我们将对图 7-7 中给出的 actor-critic 算法进行以下更改:
-
We will use the MC discounted version of the advantage.
)
-
像加强,我们将介绍熵正则化。
-
代替训练第一拟合 V(s)的两个单独的损失训练步骤,然后进行策略梯度,我们将形成单个损失目标,其将与熵正则化器一起执行 V(s)拟合以及策略梯度步骤。
使用具有先前修改的演员评论家的损失如下:
)
)
像加强,我们将进行重量更新后,每个轨迹。因此, N = 1。但是,我们将对其进行平均,得出平均损失。因此,我们将实际实现的函数如下:
)
这是我们将实施的损失。你可以在listing7_2_actor_critic_pytorch.ipynb
中找到 PyTorch 中实现 actor critic 的完整代码。代码库也有一个 TensorFlow 版本,在listing7_2_actor_critic_tensorflow.ipynb
中给出。实施将遵循我们在加强中的相同步骤。只有一些小的变化:网络结构和损耗计算与前面的表达式一样。
先说网络。我们将有一个共享权重的联合网络,一个生成政策行动概率,另一个生成状态值。对于CartPole
,这是一个相当简单的网络,如图 7-9 所示。
图 7-9
南极环境演员-评论家网络
清单 7-6 显示了 PyTorch 中的实现。它是网络的直接实现,如图 7-9 所示。
class ActorCritic(nn.Module):
def __init__(self):
super(ActorCritic, self).__init__()
self.fc1 = nn.Linear(state_dim, 128)
self.actor = nn.Linear(128,n_actions)
self.critic = nn.Linear(128,1)
def forward(self, s):
x = F.relu(self.fc1(s))
logits = self.actor(x)
state_value = self.critic(x)
return logits, state_value
model = ActorCritic()
Listing 7-6Actor-Critic Network in PyTorch
另一个变化是我们为一集实现训练代码的方式。它类似于清单 7-4 中的代码,只是引入了V(st)作为基线值。清单 7-7 给出了train_one_episode
的完整代码。
#init Optimizer
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
def train_one_episode(states, actions, rewards, gamma=0.99, entropy_coef=1e-2):
# get rewards to go
rewards_to_go = get_rewards_to_go(rewards, gamma)
# 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_to_go = torch.tensor(rewards_to_go, device=device, dtype=torch.float)
# get action probabilities from states
logits, state_values = model(states)
probs = nn.functional.softmax(logits, -1)
log_probs = nn.functional.log_softmax(logits, -1)
log_probs_for_actions = log_probs[range(len(actions)), actions]
advantage = rewards_to_go - state_values.squeeze(-1)
#Compute loss to be minized
J = torch.mean(log_probs_for_actions*(advantage))
H = -(probs*log_probs).sum(-1).mean()
loss = -(J+entropy_coef*H)
optimizer.zero_grad()
loss.backward()
optimizer.step()
return np.sum(rewards) #to show progress on training
Listing 7-7train_one_episode for Actor Critic Using MC Rewards to Go in PyTorch
请注意,在多个轨迹上训练的代码与之前相同。当我们运行代码时,我们看到,与强化相比,使用 A2C 的训练进行得更快,朝着更好的策略稳步前进。
请注意,演员评论也是一种政策上的方法,就像加强一样。
异步优势演员评论家
2016 年,论文《深度强化学习的异步方法》作者 1 介绍了 A2C 的异步版本。基本想法很简单。我们有一个全局服务器,它是提供网络参数的“参数”服务器:θ,ϕ.有多个演员-评论家代理并行运行。每个演员-评论家代理从服务器获得参数,进行轨迹滚动,并在 θ 、 ϕ 上进行梯度下降。代理将参数更新回服务器。它允许更快的学习,特别是在我们使用模拟器的环境中,例如机器人环境。我们可以首先在模拟器的多个实例上使用 A3C 训练一个算法。随后的学习将是在真实环境中的物理机器人上进一步微调/训练算法。
图 7-10 显示了 A3C 的高级示意图。请注意,这是对该方法的简单解释。对于实际的实现细节,建议您详细参考参考文献。
图 7-10
异步优势演员评论家
正如所解释的,一些论文将多个代理一起训练的同步版本称为 A3C 的 A2C 版本,即没有异步部分的 A3C。然而,有时有一个经纪人的演员评论家也被称为优势演员评论家 (A2C)。最后,演员评论家是一个算法家族,其中我们一起使用两个网络:一个价值网络来估计 V ( s )和一个政策网络来估计政策πθ(a|st)。我们正在利用两个世界的优势:基于价值的方法和政策梯度方法。
信赖域策略优化算法
到目前为止,我们在本章中详细介绍的方法也被称为标准政策梯度 (VPG)。我们使用 VPG 训练的策略是一个随机策略,它提供了自己的探索,而没有显式地使用ε-贪婪探索。随着培训的进行,策略分布变得更加清晰,以最佳行动为中心。这减少了探索,使算法越来越多地利用它所学到的东西。这会导致策略卡在局部最大值。我们试图通过引入正则化来解决这个问题,但这不是唯一的方法。
正如我们在前面章节的策略梯度方法中看到的,我们通过以下等式给出的小量来更新策略参数:
)
换句话说,在旧的策略参数θ=θ旧的 处评估梯度,然后通过采取由步长 α 确定的小步长来更新梯度。VPG 通过使用学习率α限制策略参数从θ 旧 到θ 新 的变化,试图在参数空间中保持新旧策略彼此接近。但是,仅仅因为策略参数在附近,并不能保证新旧策略(即动作概率分布)实际上是相互接近的。参数 θ 的微小变化可能导致政策概率的巨大差异。理想情况下,我们希望在概率空间而不是参数空间中保持新旧策略彼此接近。这是 2015 年题为“信任区域策略优化”的论文的作者详述的关键见解 2 在深入细节之前,我们先花几分钟时间来讲一个叫做 Kullback-Liebler 散度(KL-divergence)的度量。这是衡量两个概率有多大不同的尺度。它来自信息论领域,深入研究它需要一本自己的书。我们将只试图给出公式及其背后的一些直觉,而不进入数学证明。
假设我们有两个离散的概率分布 P 和 Q 定义在某个值的范围内(称为支持)。假设支持度为“x ”,从 1 到 6。PX(X=X)使用概率分布 P 定义了 X=x 的概率,类似地,我们在相同的支持度上定义了另一个概率分布 Q。作为例子,考虑具有概率分布的六个面的模具。
|x
|
one
|
Two
|
three
|
four
|
five
|
six
|
| — | — | — | — | — | — | — |
| P(x) | 1/6 | 1/6 | 1/6 | 1/6 | 1/6 | 1/6 |
| Q(x) | 2/9 | 1/6 | 1/6 | 1/6 | 1/6 | 1/9 |
骰子 Q 被加载以显示少于 6 而多于 1,而 P 是公平骰子,显示骰子的任何面的概率相等。
P 和 Q 之间的 KL-散度表示如下:
)
(7.34)
我们来计算一下上表的DKL(P‖Q)。
)
)
)
)
)
)
你可以通过放 P = Q 得到DKL(P‖Q)= 0 来满足自己。当两个概率相等时,KL 散度为 0。对于其他任意两个不相等的概率分布,你会得到一个+ve KL 散度。分布越远,KL 散度值越高。有严格的数学证明表明,只有当两个分布相等时,KL 散度才总是+ve 和 0。
还要注意 KL 散度是不对称的。
)
KL 散度是概率空间中两个概率分布之间距离的一种伪测度。连续概率分布的 KL 散度公式如下所示:
)
(7.35)
回到 TRPO,我们希望保持新旧政策在概率空间而不是参数空间上相互接近。这就等于说我们希望 KL-divergence 在每个更新步骤中都是有界的,以确保新旧策略不会偏离太远。
)
这里, θ k 为当前策略参数, θ 为更新后的策略参数。
现在让我们把注意力转向我们试图最大化的目标。我们之前的度量 J (θ)对新旧政策参数没有任何显式的依赖,分别说θk+1和 θ k 。有一个使用重要性抽样的政策目标的替代公式。我们将在没有数学推导的情况下陈述这一点,如下所示:
)
(7.36)
这里,θ是修改/更新策略的参数, θ k 是旧策略的参数。我们正试图采取最大可能的步骤,从旧政策参数 θ k 到具有参数 θ 的修订政策,使得新老政策之间的 KL 差异不会相差太多。换句话说,找到一个最大限度地增加目标而不走出旧政策周围的信任区域的新政策,定义为DKL(θ**θk)≤δ。用数学术语来说,我们可以将最大化问题总结如下:
)
)
)
(7.37)
优势)定义如前:
)
或者,当我们推出一个步骤,并且 v 由另一个具有参数 ϕ 的网络参数化时,下面是等式:
)
这是 TRPO 目标最大化的理论表述。但是利用目标)的泰勒级数展开和 KL 约束DKL(θ**θk)≤δ,再加上拉格朗日对偶利用凸优化,就可以得到一个近似的更新表达式。这种近似可以打破 KL-散度有界的保证,为此在更新规则中增加了回溯线搜索。最后,它涉及一个 *** nxn *** 矩阵的求逆,这不容易计算。在这种情况下,使用共轭梯度算法。此时,我们有了一个使用 TRPO 计算更新的实用算法。
我们不会深入这些推导的细节,也不会给出完整的算法。我们希望您了解基本设置。大多数时候,我们不会自己动手实现这些算法。
近似策略优化算法
近似策略优化(PPO)也是由与 TRPO 相同的问题驱动的。“我们如何在策略参数中采用最大可能的步长,而不会走得太远,导致比更新前的原始策略更差的策略?”
我们将要详述的 PPO-clip 变体没有 KL-divergence。它依赖于裁剪目标函数中的梯度,使得更新没有动机将策略移动得离原始步骤太远。PPO 实现起来更简单,并且经验表明其性能与 TRPO 一样好。详细信息见 2017 年题为“近似策略优化算法”的论文。 3
使用 PPO-clip 变体的目的如下:
)
其中:
)
(7.38)
我们改写一下 J ( θ , θ k ),优势 A 为+ve 时,如下图:
)
当优势为+ve 时,我们要更新参数,使新策略πθ(a|s)高于旧策略。但是,我们没有将其增加太多,而是剪切梯度以确保新策略的增加在旧策略的 1+ε倍以内。
类似地,当优势为-ve 时,我们得到以下结果:
)
换句话说,当优势为-ve 时,我们希望更新参数,从而降低( s , a )对的策略概率。然而,我们不是一直减小,而是剪切梯度,使得新策略概率不会下降到旧策略概率的(1-ε)倍以下。
换句话说,我们剪切梯度以确保策略更新使策略概率分布在旧概率分布的(1-ε)到(1+ε)倍之内。ϵ作为一个正则化。与 TRPO 相比,PPO 很容易实现。对于 A2C,我们可以遵循图 7-7 中给出的相同伪代码,仅做一处修改,将图 7-7 中的目标 J(θ)与( 7.38 中给出的目标互换。
这一次,我们将使用一个库,而不是自己编码。OpenAI 有一个库叫做 Baselines ( https://github.com/openai/baselines
)。它实现了许多流行和最新的算法。还有另一个基于基线的库,叫做稳定基线 3。你可以在 https://stable-baselines3.readthedocs.io/en/master/
了解更多。
我们的代码将遵循与以前相同的模式,只是我们不会显式地定义策略网络。我们也不会自己写计算损失和通过梯度的训练步骤。您可以在listing7_3_ppo_baselines3.ipynb
中找到使用 PPO 训练并记录CartPole
训练表现的完整代码。我们现在将浏览创建代理、在CartPole
上训练它并评估性能的代码片段,如清单 7-8 所示。
from stable_baselines3 import PPO
from stable_baselines3.ppo.policies import MlpPolicy
from stable_baselines3.common.evaluation import evaluate_policy
#create enviroment
env_name = 'CartPole-v1'
env = gym.make(env_name)
# build model
model = PPO(MlpPolicy, env, verbose=0)
# Train the agent for 30000 steps
model.learn(total_timesteps=30000)
# Evaluate the trained agent
mean_reward, std_reward = evaluate_policy(model, env, n_eval_episodes=100)
print(f"mean_reward:{mean_reward:.2f} +/- {std_reward:.2f}")
Listing 7-8PPO Agent for CartPole Using a Stable Baselines 3 Implementation
就是这样。用 PPO 训练代理只需要几行代码。Python 笔记本包含额外的代码,用于记录受训代理的表现并播放视频。我们不打算讨论它的代码细节。建议感兴趣的读者使用之前的链接深入研究 OpenAI 基线和稳定基线。
我们也想利用这个机会再次强调了解流行的 RL 库的重要性。在浏览本书中各种算法的实现时,您应该注意到,熟悉流行的 RL 实现并学会使用它们来满足您的特定需求同样重要。本书附带的代码有助于您更好地理解这些概念。它绝不是生产代码。像 Baselines 这样的库拥有高度优化的代码,能够利用 GPU 和多核并行运行许多代理。
摘要
本章向您介绍了另一种方法,即直接学习策略,而不是先学习状态/动作值,然后使用它们来找到最佳策略。
我们研究了 REINFROCE 的推导,这是最基本的政策梯度方法。在最初的推导之后,我们研究了一些减少方差的技术,比如奖励和基线的使用。
这让我们看到了演员-评论家家族,在这个家族中,我们结合了基于价值的方法来学习使用强化作为基线的状态值。政策网络(actor)与国家价值网络(critical)使我们能够结合基于价值的方法和政策梯度方法的优点。我们简要介绍了异步版本 A3C。
最后,我们看了一些高级策略优化技术,比如信任区域策略优化(TRPO)和邻近策略优化(PPO)。我们讨论了使用这两种技术的主要动机和方法。我们还看到了使用库来训练使用 PPO 的代理。
https://arxiv.org/pdf/1602.01783.pdf
2
https://arxiv.org/pdf/1502.05477.pdf
3
https://arxiv.org/pdf/1707.06347.pdf
****
八、结合策略梯度和 Q 学习
到目前为止,在本书中,在深度学习与强化学习相结合的背景下,我们已经在第六章中查看了深度 Q 学习及其变体,并在第七章中查看了策略梯度。神经网络训练需要多次迭代,Q-learning 是一种非策略方法,它使我们能够多次使用转换,从而提高样本效率。然而,Q-learning 有时会不稳定。此外,这是一种间接的学习方式。我们不是直接学习一个最优策略,而是先学习 q 值,然后用这些动作值来学习最优行为。在第七章中,我们看到了直接学习策略的方法,这给了我们更好的改进保证。然而,我们在第七章看到的所有政策都是政策上的。我们使用策略与环境进行交互,并对策略权重进行更新,以增加好的轨迹/动作的概率,同时减少坏的轨迹/动作的概率。然而,我们在策略上进行学习,因为在更新策略权重之后,先前的转换变得无效。
在这一章中,我们将着眼于结合这两种方法的优点,即非策略学习和直接学习策略。我们将首先讨论 Q 学习与政策梯度方法的权衡。在此之后,我们将研究三种将 Q 学习与策略梯度相结合的流行方法:深度确定性策略梯度(DDPG)、双延迟 DDPG (TD3)和软行动者批评(SAC)。我们将主要遵循 OpenAI Spinning Up 库中记录的符号、方法和示例代码。 1
政策梯度和 Q-Learning 的权衡
在第七章中,我们看了 DQN,深度学习版的 Q-learning。在 Q-learning(一种非策略方法)中,我们从探索性行为策略中收集转换,然后在批量随机梯度更新中使用这些转换来学习 Q 值。当我们学习 q 值时,我们通过取一个状态中所有可能行动的 q 值的最大值来选择最佳行动,从而改进策略。这是我们遵循的等式:
)
(8.1)
请注意最大值。通过取最大值,即),我们正在提高目标值
),这迫使当前状态动作 q 值
)更新权重以达到更高的目标。这就是我们强迫网络权重满足的贝尔曼最优方程。整个学习是偏离策略的,因为无论遵循什么策略,贝尔曼最优方程都适用于最优策略。所有的( s 、 a 、 r 、 s 、 ' )转换都需要满足这一点,不管这些转换是使用哪个策略生成的。我们能够使用重放缓冲区重用转换,这使得学习非常简单高效。然而,Q-learning 也有一些问题。
第一个是关于使用 Q-learning 的当前形式进行持续的行动。请看)。我们在第六章中看到的所有例子都是离散动作的例子。你知道为什么吗?当动作空间是连续和多维的,例如一起移动机器人的多个关节时,你认为你将如何执行 max ?
当动作离散时,很容易取 max 。我们将状态 s 输入到模型中,对于所有可能的动作,我们得到 Q ( s , a )。由于离散动作的数量有限,选择最大值很容易。图 8-1 显示了一个样品模型。
图 8-1
具有离散动作的 DQN 学习的一般模型
现在想象这些动作是连续的!你会如何取最大值?为了找到每个)的
),我们必须运行另一个优化算法来找到最大值。这将是一个昂贵的过程,因为作为政策改进的一部分,需要对批次中的每个过渡执行该过程。
第二个问题是学习错误的目标。我们实际上想要一个最优的政策,但我们不会在 DQN 的直接领导下这样做。我们学习行动值函数,然后使用 max 找到最佳 q 值/最佳行动。
第三个问题是,DQN 有时也不稳定。没有理论上的保证,我们正在尝试使用我们在第五章中谈到的半梯度更新来更新权重。我们基本上是在尝试遵循目标不断变化的监督学习过程。我们所学的会影响新轨迹的生成,这反过来会影响我们的学习质量。我们看到的所有 DQN 平均回报的进度图都没有持续改善。它们非常不稳定,需要仔细调整超参数,以确保算法朝着好的策略发展。
最后,第四个问题是 DQN 学会了确定性政策。我们使用探索性行为策略来生成和探索代理在确定性策略中学习的内容。特别是在机器人领域的实验已经表明,一定量的随机策略更好,因为我们对世界的建模和关节的操作并不总是完美的。我们需要一些随机性来调整不完美的建模或动作值到实际机器人关节运动的转换。此外,确定性策略是随机策略的极限情况。
让我们把注意力转向政策梯度方法。在策略梯度方法中,我们输入状态,得到的输出是离散动作的动作概率或连续动作的概率分布的参数。我们可以看到,策略梯度允许我们学习离散和连续动作的策略。然而,学习连续动作在 DQN 是不可行的。图 8-2 显示了政策梯度中使用的模型。
图 8-2
政策梯度方法的政策网络。在第七章中,我们看到了不连续的动作,但是这个过程对于连续的动作也很有效,就像在那一章中解释的那样
此外,使用策略梯度方法,我们直接学习改进策略,而不是先学习值函数,然后使用它们来寻找最优策略的迂回方式。普通政策梯度确实遭遇了向坏区域的崩溃,我们看到了 TRPO 和 PPO 等方法控制步长以改善政策梯度的保证,从而产生更好的政策。
与 DQN 不同,政策梯度学习随机政策,因此探索是我们试图学习的政策的一部分。然而,政策梯度法的最大缺陷是,它是一种政策性方法。一旦我们使用转换来计算梯度更新,模型就移动到新的策略。在这个更新的政策世界中,早期的转变不再适用。我们需要在更新后丢弃以前的转换,并生成新的轨迹/转换来训练模型。这使得策略学习非常样本低效。
我们确实使用行动者-批评家方法将价值学习作为政策梯度的一部分,其中政策网络是试图学习最佳行动的行动者,而价值网络是告知政策网络行动好坏的批评家。然而,即使使用演员-评论家的方法,学习是在政策上。我们使用批评家来指导参与者,但是在更新策略(和/或价值)网络之后,我们仍然需要丢弃所有的转换。
有没有一种方法可以让我们直接学习策略,但同时利用 Q-learning 来学习非策略?对于连续行动空间,我们能这样做吗?这就是我们将在本章中讨论的内容。我们将把 Q-learning 和策略梯度结合起来,提出不依赖于策略的算法,并很好地用于连续动作。
结合策略梯度和 Q-Learning 的通用框架
我们将着眼于持续行动政策。我们将有两个网络。一个是学习给定状态下的最优行动,即行动者网络。假设策略网络用θ参数化,网络学习到一个策略,这个策略产生动作 a = μ θ ( s ),这个动作最大化 Q ( s , a )。在数学符号中:
)
第二个网络,评论家网络,将再次把状态( s )作为一个输入,并把来自第一个网络的最优动作,μθ(s),作为另一个输入,以产生 q 值q【ϕ(s, μ θ 图 8-3 从概念上展示了网络的相互作用。
图 8-3
结合策略和 Q-learning。我们使用两个网络直接学习策略和 Q,在这两个网络中,来自第一个网络(行动者)的行动输出被馈送到第二个网络(批评家),第二个网络学习 Q ( s , a )
为了确保探索,我们要采取行动 a ,这是探索性的。这类似于我们在 Q-learning 中采用的方法,在 Q-learning 中,我们学习了确定性策略,但从探索性ε-贪婪策略生成了转换。同样,在这里,我们在学习 a 的同时,加入一点随机性 ϵ ~ N (0, σ 2 )并使用 a + ε 动作来探索环境,生成轨迹。
像 Q-learning 一样,我们将使用重放缓冲区来存储转换,并重用以前的转换来学习。这是我们将在本章中看到的所有方法的最大好处之一。它们将使策略学习偏离策略,从而提高样本效率。
在 Q-learning 中,我们必须使用一个目标网络,它是 Q-网络的副本。原因是在学习 q 值时提供某种文具目标。你可以在第五章和第六章重温目标网络的讨论。在这些方法中,目标网络权重定期用在线/代理网络权重更新。这里我们也将使用一个目标网络。然而,用于更新本章中算法的目标网络权重的方法将是 polyak 平均 ( 指数平均)的方法,如下式所示:
)
(8.2)
我们还将使用策略网络的目标网络。这与提供稳定的目标 Q 值的原因相同,这允许我们执行梯度下降的监督学习风格,并将权重调整到近似的 Q ( s , a )。
有了这个背景,我们就可以开始研究我们的第一个算法:深度确定性策略梯度。
深度确定性政策梯度
2016 年,在一篇题为“深度强化学习的连续控制”的论文中,来自 DeepMind 的作者 2 介绍了 DDPG 算法。作者对他们的方法提出了以下几点:
-
而 DQN 求解高维状态空间,只能处理离散的低维动作空间。DQN 不能应用于连续和高维的动作领域,例如,像机器人这样的物理控制任务。
-
由于维数灾难,离散化行动空间不是一个选项。假设你有一个有七个关节的机器人,每个关节可以在范围内移动( k 、 k )。让我们对每个关节进行粗略的离散化,每个关节有三个可能的值{k,0, k }。即使采用这种粗略的离散化,所有七个维度中的离散动作的总组合也达到 3 7 = 2187。相反,如果我们决定将每个关节的离散范围划分为范围内的 10 个可能值( k , k ),我们会得到 10 7 = 10 百万个选项。这是维度的诅咒,其中可能的动作组合集合随着每个新维度/关节呈指数增长。
-
DDPG 是一种如下的算法:
-
型号自由:我们不知道型号。我们从主体与环境的互动中学习。
-
偏离政策 : DDPG 和 DQN 一样,使用探索性政策来产生转变,并学习确定性政策。
-
连续高维动作空间 : DDPG 只对连续动作域有效,对高维动作空间也很有效。
-
行动者-批评家:这意味着我们有一个行动者(政策网络)和批评家,行动-价值(q 值)网络。
-
重放缓冲器:像 DQN 一样,DDPG 使用重放缓冲器来存储过渡,并利用它们来学习。这打破了训练示例的时间依赖性/相关性,否则会搞乱学习。
-
目标网络:和 DQN 一样,它使用目标网络为 q 值学习提供相当稳定的目标。然而,与 DQN 不同,它不通过定期复制在线/代理/主网络的权重来更新目标网络。相反,它使用 polyak/指数平均值在每次更新主网络后将目标网络移动一点点。
-
现在让我们将注意力转向网络架构和我们计算的损耗。首先让我们看看 Q-learning 部分,然后我们将看看政策学习网络。
DDPG 的 Q-Learning(评论家)
在 DQN,我们计算了通过梯度下降最小化的损失。损失由方程式( 6)给出。3 ),转载于此:
)
(8.3)
让我们重写等式。我们将删除 subindex i 和 t 以减少符号的混乱。我们将求和改为期望,以强调我们通常想要的是一个期望,但它是在蒙特卡罗下通过样本的平均值来估计的。最终在代码中,我们得到了和,但是它们是一些期望值的蒙特卡罗估计。我们还将用ϕ 目标 替换目标网络权重)。同样,我们将主要权重 w * t 替换为ϕ.进一步,我们将把权重从函数参数内部移到函数上的子索引,即q*ϕ(…)←q(…)。;ϕ).由于所有这些符号变化,等式( 8.3 )看起来像这样:
)
(8.4)
这仍然是 DQN 公式,我们在状态(s’)中取最大离散动作来得到)。在连续空间中,我们不能取 max ,因此我们有另一个网络(actor)来取输入状态 s 并产生动作,这使
)最大化;即,我们用
)替换
),其中
)是目标策略。更新后的损失表达式如下:
)
(8.5)
这是更新的均方贝尔曼误差(MSBE),我们将在代码中实现,然后进行反向传播,以最小化损失函数。请注意,这只是ϕ的函数,因此 L ( ϕ , D )的梯度是相对于ϕ.的如前所述,在代码中,我们将用样本平均值代替期望值,样本平均值是期望值的 MC 估计值。
接下来我们来看看政策学习部分。
DDPG 的政策学习(演员)
在策略学习部分,我们试图学习 a = μ θ ( s ),一个确定性策略,给出最大化 Q ϕ ( s , a )的动作。由于动作空间是连续的,并且我们假设 Q 函数相对于动作是可微的,我们可以相对于要求解的策略参数执行梯度上升。
)
(8.6)
由于策略是确定性的,( 8.6 )中的期望不依赖于策略,这与我们在前一章的随机梯度中看到的不同。那里的期望算子依赖于策略参数,因为策略是随机的,这反过来影响期望的 q 值。
我们可以取 J 相对于 θ 的梯度,得到如下结果:
)
(8.7)
这是链式法则的直接应用。还请注意,我们在期望中没有得到任何∇ 对数 (…)项,因为对其进行期望的状态 s 来自重放缓冲器,并且它与关于哪个梯度进行期望的参数θ无关。
此外,在 2014 年题为“确定性政策梯度算法”的论文中, 3 作者表明,方程( 8.7 )是政策梯度,即政策绩效的梯度。建议你通读这两篇论文,以便对 DDPG 背后的数学有更深入的理论理解。
如前所述,为了帮助探索,当我们学习确定性策略时,我们将使用所学策略的嘈杂探索版本来探索和生成转换。我们通过向学习策略中添加平均零高斯噪声来做到这一点。
伪代码和实现
至此,我们已经准备好给出完整的伪代码了。请参见图 8-4 进行说明。
Deep Deterministic Policy Gradient
图 8-4
深度确定性策略梯度算法
代码中使用的健身房环境
转向实现,我们将在本章中使用两个环境来运行代码。第一个是称为Pendulum-v0
.
的钟摆摆动环境,这里的状态是给出钟摆角度的三维向量(即,其 cos 和 sin 分量),第三维是角速度(θ点):)。这个动作是一个单一的值,力矩施加在钟摆上。这个想法是尽可能长时间地保持钟摆直立。参见图 8-5 。
图 8-5
来自开放体育馆图书馆的钟摆环境
在我们在这个具有一维动作空间的简单连续动作环境上训练网络之后,我们将研究另一个称为月球着陆器连续环境的环境:LunarLanderContinuous-v2
。在这种环境下,我们试图将登月舱降落在月球的两面旗帜之间。状态向量是八维的:[x_pos, y_pos, x_vel, y_vel, lander_angle, lander_angular_vel, left_leg_ground_contact_flag, right_leg_ground_contact_flag]
。
动作是二维浮动:[main engine, left-right engines]
。
-
主机 : -1…0 表示发动机关闭,范围(0,1)是从 50%到 100%功率的发动机油门。发动机不能在低于 50%的功率下工作。
-
左右 :
range(-1.0, -0.5)
点火左发动机,range(+0.5, +1.0)
点火右发动机,range(-0.5, 0.5)
两个发动机都关。
图 8-6 显示了环境的快照。
图 8-6
月球着陆器连续从开放体育馆图书馆
代码列表
现在让我们把注意力转向实现图 8-4 中给出的 DDPG 伪代码的实际代码。代码来自文件listing8_1_ddpg_pytorch.ipynb.
我们在 TensorFlow 2.0 的文件listing8_1_ddpg_tensorflow.ipynb.
中也有完整的实现,所有的代码遍历都将借用这两个文件中的代码片段。我们将首先讨论 Q 和策略网络,然后是损失计算,然后是训练循环。最后,我们将讨论运行和测试经过培训的代理的性能的代码。
政策网络行动者
首先让我们看看演员/政策网络。清单 8-1 显示了 PyTorch 中的策略网络代码。我们定义了一个简单的神经网络,它有两个大小为 256 的隐藏层,每个层都有 ReLU 激活。如果你查看函数forward,
,你会注意到最后一层(self.actor
)通过了tanh
激活。Tanh
是挤压功能;它将(∞,∞)中的值重新映射到一个压缩范围(-1,1)
。然后,我们将该压缩值乘以动作限值(self.act_limit
),以便MLPActor
的连续输出在环境可接受的动作值的有效范围内。我们通过扩展 PyTorch nn.Module
类来创建我们的网络类,这需要我们定义一个forward
函数,将输入状态 S 作为产生动作值作为网络输出的参数。
class MLPActor(nn.Module):
def __init__(self, state_dim, act_dim, act_limit):
super().__init__()
self.act_limit = act_limit
self.fc1 = nn.Linear(state_dim, 256)
self.fc2 = nn.Linear(256, 256)
self.actor = nn.Linear(256, act_dim)
def forward(self, s):
x = self.fc1(s)
x = F.relu(x)
x = self.fc2(x)
x = F.relu(x)
x = self.actor(x)
x = torch.tanh(x) # to output in range(-1,1)
x = self.act_limit * x
return x
Listing 8-1Policy Network in PyTorch
政策网络参与者(TensorFlow)
清单 8-2 包含了 TensorFlow 2.0 中相同的函数。它非常类似于 PyTorch 实现,除了我们子类化了tf.keras.Model
而不是nn.Module
。我们在一个名为call
的函数中实现网络转发逻辑,而不是在函数forward
中。此外,层的命名方式也有细微的差别,例如dense
与linear
以及层的维度传递方式。
class MLPActor(tf.keras.Model):
def __init__(self, state_dim, act_dim, act_limit):
super().__init__()
self.act_limit = act_limit
self.fc1 = layers.Dense(256, activation="relu")
self.fc2 = layers.Dense(256, activation="relu")
self.actor = layers.Dense(act_dim)
def call(self, s):
x = self.fc1(s)
x = self.fc2(x)
x = self.actor(x)
x = tf.keras.activations.tanh(x) # to output in range(-1,1)
x = self.act_limit * x
return x
Listing 8-2Policy Network in TensorFlow
q-网络评论家实现
接下来我们来看看 Q-network ( 评论家)。这也是一个简单的两层隐藏网络,具有 ReLU 激活,然后是最后一层,输出数量等于 1。最后一层没有任何激活,使网络能够产生任何值作为网络的输出。该网络输出 q 值,因此我们需要一个可能的范围(∞,∞)。
PyTorch
清单 8-3 显示了 PyTorch 中批评家网络的代码,清单 8-4 显示了 TensorFlow 中的代码。它们非常类似于参与者/策略网络的实现,除了前面讨论的一些小的区别。
class MLPQFunction(nn.Module):
def __init__(self, state_dim, act_dim):
super().__init__()
self.fc1 = nn.Linear(state_dim+act_dim, 256)
self.fc2 = nn.Linear(256, 256)
self.Q = nn.Linear(256, 1)
def forward(self, s, a):
x = torch.cat([s,a], dim=-1)
x = self.fc1(x)
x = F.relu(x)
x = self.fc2(x)
x = F.relu(x)
q = self.Q(x)
return torch.squeeze(q, -1)
Listing 8-3Q/Critic Network in PyTorch
TensorFlow
清单 8-4 显示了 TensorFlow 中 critic 网络的代码。
class MLPQFunction(tf.keras.Model):
def __init__(self, state_dim, act_dim):
super().__init__()
self.fc1 = layers.Dense(256, activation="relu")
self.fc2 = layers.Dense(256, activation="relu")
self.Q = layers.Dense(1)
def call(self, s, a):
x = tf.concat([s,a], axis=-1)
x = self.fc1(x)
x = self.fc2(x)
q = self.Q(x)
return tf.squeeze(q, -1)
Listing 8-4Q/Critic Network in TensorFlow
组合的模型-参与者评论实现
一旦这两个网络都定义好了,我们就把它们组合成一个类,这样我们就可以以一种更加模块化的方式来管理在线网络和目标网络。这只是为了更好地组织代码,仅此而已。结合两个网络的类实现为MLPActorCritic
。在这个类中,我们还定义了一个函数get_action
,它接受状态和噪声比例。它通过策略网络传递状态得到μθ(s),然后它加上一个噪声(零均值高斯噪声)给出一个有噪声的动作进行探索。这是实现算法步骤 4 的函数:
)
清单 8-5 展示了 PyTorch 中MLPActorCritic
的实现,清单 8-6 展示了 TensorFlow 版本。实现非常相似,除了个别的细微差别,比如子类化哪个类。另一个区别是,在 PyTorch 中,您需要将 NumPy 数组转换为 Torch 张量,然后才能通过网络传递它们,而在 TensorFlow 中,我们可以直接将 NumPy 数组传递给模型。
class MLPActorCritic(tf.keras.Model):
def __init__(self, observation_space, action_space):
super().__init__()
self.state_dim = observation_space.shape[0]
self.act_dim = action_space.shape[0]
self.act_limit = action_space.high[0]
#build Q and policy functions
self.q = MLPQFunction(self.state_dim, self.act_dim)
self.policy = MLPActor(self.state_dim, self.act_dim, self.act_limit)
def act(self, state):
return self.policy(state).numpy()
def get_action(self, s, noise_scale):
a = self.act(s.reshape(1,-1).astype("float32")).reshape(-1)
a += noise_scale * np.random.randn(self.act_dim)
return np.clip(a, -self.act_limit, self.act_limit)
Listing 8-6MLPActorCritic in TensorFlow
class MLPActorCritic(nn.Module):
def __init__(self, observation_space, action_space):
super().__init__()
self.state_dim = observation_space.shape[0]
self.act_dim = action_space.shape[0]
self.act_limit = action_space.high[0]
#build Q and policy functions
self.q = MLPQFunction(self.state_dim, self.act_dim)
self.policy = MLPActor(self.state_dim, self.act_dim, self.act_limit)
def act(self, state):
with torch.no_grad():
return self.policy(state).numpy()
def get_action(self, s, noise_scale):
a = self.act(torch.as_tensor(s, dtype=torch.float32))
a += noise_scale * np.random.randn(self.act_dim)
return np.clip(a, -self.act_limit, self.act_limit)
Listing 8-5MLPActorCritic in PyTorch
体验回放
像 DQN 一样,我们使用经验回放。这和我们在 DDPG 使用的 PyTorch 和 TensorFlow 来自 DQN 的版本是一样的。它是使用 NumPy 数组实现的,同样的代码适用于 PyTorch 和 TensorFlow。因为它是从 DQN 借来的相同的实现,我们不给出它的代码。要查看ReplayBuffer
的代码,请查看 Jupyter 笔记本中的实现。
q 损耗实现
接下来,我们来看看 Q 损耗的计算。我们实际上是在实现伪代码的步骤 11 和 12 中的等式。
)
)
PyTorch
清单 8-7 给出了 PyTorch 的实现。我们先把这批( s , a , r , s , ' , d )转换成 PyTorch 张量。接下来我们用批( s , a )计算 Q , ϕ ( s , a ),并通过策略网络传递。接下来,我们根据上式计算目标值 y ( r , s ,′, d )。在计算目标时,我们使用with torch.no_grad()
来停止梯度计算,因为我们不想使用 PyTorch 中的 auto-diff 来调整目标网络权重。我们将使用 polyak 平均手动调整目标网络权重。停止计算不需要的梯度可以加快训练速度,还可以确保梯度步长不会对您想要保持冻结或手动调整的权重产生任何意外的副作用。最后,我们计算损失。
)
PyTorch 执行反向传播来计算梯度。我们不需要在代码中明确计算梯度。
def compute_q_loss(agent, target_network, states, actions, rewards, next_states, done_flags,
gamma=0.99):
# convert numpy array to torch tensors
states = torch.tensor(states, dtype=torch.float)
actions = torch.tensor(actions, dtype=torch.float)
rewards = torch.tensor(rewards, dtype=torch.float)
next_states = torch.tensor(next_states, dtype=torch.float)
done_flags = torch.tensor(done_flags.astype('float32'),dtype=torch.float)
# get q-values for all actions in current states
# use agent network
predicted_qvalues = agent.q(states, actions)
# Bellman backup for Q function
with torch.no_grad():
q__next_state_values = target_network.q(next_states, target_network.policy(next_states))
target = rewards + gamma * (1 - done_flags) * q__next_state_values
# MSE loss against Bellman backup
loss_q = ((predicted_qvalues - target)**2).mean()
return loss_q
Listing 8-7Q-Loss Computation in PyTorch
TensorFlow
TensorFlow 版本类似,清单 8-8 列出了完整的实现。与 PyTorch 的主要区别在于,我们没有将 NumPy 数组转换为张量。但是,我们将数据类型转换为float32
,使其与网络权重的默认数据类型兼容。请记住,对于所有 TensorFlow 实现,我们都使用热切执行模式,与 PyTorch 类似,我们使用with tape.stop_recording()
来停止目标网络中的梯度计算。
def compute_q_loss(agent, target_network, states, actions, rewards, next_states, done_flags,
gamma, tape):
# convert numpy array to proper data types
states = states.astype('float32')
actions = actions.astype('float32')
rewards = rewards.astype('float32')
next_states = next_states.astype('float32')
done_flags = done_flags.astype('float32')
# get q-values for all actions in current states
# use agent network
predicted_qvalues = agent.q(states, actions)
# Bellman backup for Q function
with tape.stop_recording():
q__next_state_values = target_network.q(next_states, target_network.policy(next_states))
target = rewards + gamma * (1 - done_flags) * q__next_state_values
# MSE loss against Bellman backup
loss_q = tf.reduce_mean((predicted_qvalues - target)**2)
return loss_q
Listing 8-8Q-Loss Computation in TensorFlow
保单损失执行
接下来,我们按照伪代码的第 13 步计算保单损失。
)
这是一个简单的计算。它只是 PyTorch 和 TensorFlow 中的一个三行代码实现。清单 8-9 包含 PyTorch 版本,清单 8-10 包含 TensorFlow 版本。请注意损失中的-ve
符号。我们的算法需要在策略目标上做梯度上升,但是像 PyTorch 和 TensorFlow 这样的自动微分库实现梯度下降。将政策目标乘以-1.0 会导致亏损,亏损的梯度下降与政策目标的梯度上升相同。
def compute_policy_loss(agent, states, tape):
# convert numpy array to proper data type
states = states.astype('float32')
predicted_qvalues = agent.q(states, agent.policy(states))
loss_policy = - tf.reduce_mean(predicted_qvalues)
return loss_policy
Listing 8-10Policy-Loss Computation in TensorFlow
def compute_policy_loss(agent, states):
# convert numpy array to torch tensors
states = torch.tensor(states, dtype=torch.float)
predicted_qvalues = agent.q(states, agent.policy(states))
loss_policy = - predicted_qvalues.mean()
return loss_policy
Listing 8-9Policy-Loss Computation in PyTorch
一步更新实施
接下来,我们定义一个名为one_step_update
的函数,它获取一批( s , a , r , s ' , d )并计算 Q 损失,随后是反向传播,然后是类似的策略损失计算步骤,随后是梯度步骤。最后,它使用 polyak 平均对目标网络权重进行更新。本质上,这个步骤和前面的两个函数compute_q_loss
和compute_policy_loss
一起实现了伪代码的步骤 11 到 14。
清单 8-11 显示了one_step_update
的 PyTorch 版本。第一步是计算 Q 损失,并对 critic/Q 网络权重执行梯度下降。然后,我们冻结 Q-网络权重,使得策略网络上的梯度下降不会影响 Q-网络的权重。随后计算行动者/策略网络权重的策略损失和梯度下降。我们再次解冻 Q-网络权重。最后,我们使用 polyak 平均来更新目标网络权重。
def one_step_update(agent, target_network, q_optimizer, policy_optimizer,
states, actions, rewards, next_states, done_flags,
gamma=0.99, polyak=0.995):
#one step gradient for q-values
q_optimizer.zero_grad()
loss_q = compute_q_loss(agent, target_network, states, actions, rewards, next_states, done_flags,
gamma)
loss_q.backward()
q_optimizer.step()
#Freeze Q-network
for params in agent.q.parameters():
params.requires_grad = False
#one setep gradient for policy network
policy_optimizer.zero_grad()
loss_policy = compute_policy_loss(agent, states)
loss_policy.backward()
policy_optimizer.step()
#UnFreeze Q-network
for params in agent.q.parameters():
params.requires_grad = True
# update target networks with polyak averaging
with torch.no_grad():
for params, params_target in zip(agent.parameters(), target_network.parameters()):
params_target.data.mul_(polyak)
params_target.data.add_((1-polyak)*params.data)
Listing 8-11One-Step Update in PyTorch
清单 8-12 给出了one_step_update
的 TensorFlow 版本,类似于 PyTorch 实现流程。不同之处在于计算梯度的方式、在每个库中采用梯度步长的方式、冻结和解冻权重的方式以及更新目标网络权重的方式。逻辑是一样的;这只是调用哪个库函数和传递什么参数的区别——基本上是两个库的语法区别。
def one_step_update(agent, target_network, q_optimizer, policy_optimizer,
states, actions, rewards, next_states, done_flags,
gamma=0.99, polyak=0.995):
#one step gradient for q-values
with tf.GradientTape() as tape:
loss_q = compute_q_loss(agent, target_network, states, actions, rewards, next_states, done_flags,
gamma, tape)
gradients = tape.gradient(loss_q, agent.q.trainable_variables)
q_optimizer.apply_gradients(zip(gradients, agent.q.trainable_variables))
#Freeze Q-network
agent.q.trainable=False
#one setep gradient for policy network
with tf.GradientTape() as tape:
loss_policy = compute_policy_loss(agent, states, tape)
gradients = tape.gradient(loss_policy, agent.policy.trainable_variables)
policy_optimizer.apply_gradients(zip(gradients, agent.policy.trainable_variables))
#UnFreeze Q-network
agent.q.trainable=True
# update target networks with polyak averaging
updated_model_weights = []
for weights, weights_target in zip(agent.get_weights(), target_network.get_weights()):
new_weights = polyak*weights_target+(1-polyak)*weights
updated_model_weights.append(new_weights)
target_network.set_weights(updated_model_weights)
Listing 8-12One-Step Update in TensorFlow
DDPG:主循环
最后一步是 DDPG 算法的实现,它使用了前面的one_step_update
函数。它创建优化器并初始化环境。它使用当前的在线策略在环境中不断地步进。最初,对于第一个start_steps=10000
,它采取一个随机动作来探索环境,一旦收集到足够多的转换,它就使用当前的带有噪声的策略来选择动作。转换被添加到ReplayBuffer
,如果缓冲区已满,则从缓冲区中删除最早的一个。update_after
告诉算法仅在update_after=1000
转换被收集到缓冲器后才开始进行梯度更新。代码按照参数epoch=5
的定义多次运行循环。我们使用了epoch=5
进行演示。你可能想要运行更长的时间,比如说 100 个纪元左右。这绝对是推荐给月球着陆器环境的。我们在清单 8-13 中仅给出 PyTorch 版本的清单。
def ddpg(env_fn, seed=0,
steps_per_epoch=4000, epochs=5, replay_size=int(1e6), gamma=0.99,
polyak=0.995, policy_lr=1e-3, q_lr=1e-3, batch_size=100, start_steps=10000,
update_after=1000, update_every=50, act_noise=0.1, num_test_episodes=10,
max_ep_len=1000):
torch.manual_seed(seed)
np.random.seed(seed)
env, test_env = env_fn(), env_fn()
ep_rets, ep_lens = [], []
state_dim = env.observation_space.shape
act_dim = env.action_space.shape[0]
act_limit = env.action_space.high[0]
agent = MLPActorCritic(env.observation_space, env.action_space)
target_network = deepcopy(agent)
# Freeze target networks with respect to optimizers (only update via polyak averaging)
for params in target_network.parameters():
params.requires_grad = False
# Experience buffer
replay_buffer = ReplayBuffer(replay_size)
#optimizers
q_optimizer = Adam(agent.q.parameters(), lr=q_lr)
policy_optimizer = Adam(agent.policy.parameters(), lr=policy_lr)
total_steps = steps_per_epoch*epochs
state, ep_ret, ep_len = env.reset(), 0, 0
for t in range(total_steps):
if t > start_steps:
action = agent.get_action(state, act_noise)
else:
action = env.action_space.sample()
next_state, reward, done, _ = env.step(action)
ep_ret += reward
ep_len += 1
# Ignore the "done" signal if it comes from hitting the time
# horizon (that is, when it's an artificial terminal signal
# that isn't based on the agent's state)
done = False if ep_len==max_ep_len else done
# Store experience to replay buffer
replay_buffer.add(state, action, reward, next_state, done)
state = next_state
# End of trajectory handling
if done or (ep_len == max_ep_len):
ep_rets.append(ep_ret)
ep_lens.append(ep_len)
state, ep_ret, ep_len = env.reset(), 0, 0
# Update handling
if t >= update_after and t % update_every == 0:
for _ in range(update_every):
states, actions, rewards, next_states, done_flags = replay_buffer.sample(batch_size)
one_step_update(
agent, target_network, q_optimizer, policy_optimizer,
states, actions, rewards, next_states, done_flags,
gamma, polyak
)
# End of epoch handling
if (t+1) % steps_per_epoch == 0:
epoch = (t+1) // steps_per_epoch
avg_ret, avg_len = test_agent(test_env, agent, num_test_episodes, max_ep_len)
print("End of epoch: {:.0f}, Training Average Reward: {:.0f}, Training Average Length: {:.0f}".format(epoch, np.mean(ep_rets), np.mean(ep_lens)))
print("End of epoch: {:.0f}, Test Average Reward: {:.0f}, Test Average Length: {:.0f}".format(epoch, avg_ret, avg_len))
ep_rets, ep_lens = [], []
return agent
Listing 8-13DDPG Outer Training Loop in PyTorch
TensorFlow 版本非常相似,除了在调用哪个库函数和如何传递参数上有微小的区别。在 TensorFlow 版本中,我们有一段额外的代码来初始化网络权重,这样我们就可以在开始训练之前冻结目标网络权重。按照我们构建模型的方式,直到训练的第一步才构建模型,因此我们需要在 TensorFlow 中添加额外的代码来强制构建模型。我们不会给出 TensorFlow 版本的代码清单。
剩下的代码是训练代理,然后记录被训练的代理的表现。我们首先为钟摆环境运行该算法,然后为月球着陆器健身房环境运行该算法。这些代码版本读起来很有趣,但是因为它们是我们学习 DDPG 的目标的附带内容,所以我们不会深入这些代码实现的细节。然而,感兴趣的读者可能希望查阅相关的库文档并逐步阅读代码。
这就完成了代码实现演练。我们可以看到,即使经过五个时期的训练,代理人能够在简单的摆环境中表现得非常好,并且能够在更复杂的月球着陆环境中表现得相当好。
接下来,我们将看看孪生延迟 DDPG,也称为 TD3。它有一些其他的增强和技巧来解决一些在 DDPG 看到的稳定性和收敛速度问题。
孪生延迟 DDPG
双延迟 DDPG 于 2018 年在一篇题为“解决演员-评论家方法中的函数近似误差”的论文中提出 4 DDPG 患有我们在第四章的 Q-learning 中看到的高估偏差(在“最大化偏差和双 Q 学习”一节)。我们在第六章中看到了双 DQN 方法,通过解耦最大化动作和最大 q 值来解决偏差。在前面提到的论文中,作者表明 DDPG 也遭受同样的高估偏差。他们提出了一种双 Q 学习的变体,解决了 DDPG 的这种高估偏差。该方法使用以下修改:
-
削波双 Q 学习 : TD3 使用两个独立的 Q 函数,在贝尔曼方程下形成目标时取两者中的最小值,即图 8-4 中 DDPG 伪代码第 11 步中的目标。这种修改就是该算法被称为孪生的原因。
-
延迟策略更新:与 Q 函数更新相比,TD3 更新策略和目标网络的频率较低。该论文建议对于 Q 函数的每两次更新,对策略和目标网络进行一次更新。这意味着在图 8-4 中的 DDPG 伪代码的步骤 13 和 14 中,对于步骤 11 和 12 中的 Q 函数的每两次更新,执行一次更新。这个修改就是把这个算法叫做延迟的原因。
-
目标策略平滑 : TD3 给目标动作增加了噪声,使得策略更难利用 Q 函数估计误差和控制高估偏差。
目标政策平滑
用于计算目标 y ( r , s ' , d )的动作基于目标网络。在 DDPG,我们在图 8-4 的步骤 11 中计算)。然而,在 TD3 中,我们通过向动作添加噪声来执行目标策略平滑。对于确定性动作
),我们添加了一个带有一些剪辑范围的平均零高斯噪声。然后使用 tanh 进一步剪切动作,并乘以
max_action_range
以确保动作值在可接受的动作值范围内。
)
(8.8)
Q-Loss(评论家)
我们使用两个独立的 Q 函数,并从使用两个独立的 Q 函数中的最小值的公共目标学习它们。以数学方式表达目标看起来像这样:
)
(8.9)
我们首先利用方程( 8.8 )来寻找有噪声的目标动作a’(s’)。这又用于计算目标 Q 值:第一和第二 Q 目标网络的 Q 值:)和
)。
( 8.9 )中的共同目标用于查找两个 Q 网络的损耗,如下所示:
)
还有这里:
)
(8.10)
损失加在一起,然后独立地最小化,以训练)和
)网络(即两个在线评论家网络)。
)
(8.11)
保单损失(参与者)
保单损失计算方法保持不变,与 DDPG 使用的方法相同。
)
(8.12)
请注意,我们在等式中只使用了)。和 DDPG 一样,也请注意
-ve
标志。我们需要做梯度上升,但是 PyTorch 和 TensorFlow 做梯度下降。我们使用一个-ve
符号将上升转换为下降。
延迟更新
我们以延迟的方式更新在线策略和代理网络权重,即在线 Q 网络)和
)的每两次更新更新一次。
伪代码和实现
至此,我们已经准备好给出完整的伪代码。请参见图 8-7 。
Twin Delayed DDPG
图 8-7
双延迟 DDPG 算法
代码实现
现在让我们浏览一下代码实现。像 DDPG 一样,我们将在摆锤和月球着陆器上运行该算法。除了我们前面谈到的三处修改,大部分代码与 DDPG 的代码相似。因此,我们将只介绍 PyTorch 和 TensorFlow 版本中这些变化的亮点。您可以在文件listing8_2_td3_pytorch.ipynb
中找到 PyTorch 版本的完整代码,在listing8_2_td3_tensorflow.ipynb
中找到 TensorFlow 版本的完整代码。
组合的模型-参与者评论实现
我们首先来看看代理网络。个人 Q-网络(评论家)MLPQFunction
和政策-网络(演员)MLPActor
与之前相同。然而,我们将演员和评论家结合在一起的代理人,即MLPActorCritic
,看到了一个微小的变化。我们现在有两个 Q 网络与 TD3 的“孪生”部分一致。清单 8-14 包含了MLPActorCritic
的代码,在 PyTorch 和 TensorFlow 中都有。
#################PyTorch#################
class MLPActorCritic(nn.Module):
def __init__(self, observation_space, action_space):
super().__init__()
self.state_dim = observation_space.shape[0]
self.act_dim = action_space.shape[0]
self.act_limit = action_space.high[0]
#build Q and policy functions
self.q = MLPQFunction(self.state_dim, self.act_dim)
self.policy = MLPActor(self.state_dim, self.act_dim, self.act_limit)
def act(self, state):
with torch.no_grad():
return self.policy(state).numpy()
def get_action(self, s, noise_scale):
a = self.act(torch.as_tensor(s, dtype=torch.float32))
a += noise_scale * np.random.randn(self.act_dim)
return np.clip(a, -self.act_limit, self.act_limit)
################# TensorFlow #################
class MLPActorCritic(tf.keras.Model):
def __init__(self, observation_space, action_space):
super().__init__()
self.state_dim = observation_space.shape[0]
self.act_dim = action_space.shape[0]
self.act_limit = action_space.high[0]
#build Q and policy functions
self.q1 = MLPQFunction(self.state_dim, self.act_dim)
self.q2 = MLPQFunction(self.state_dim, self.act_dim)
self.policy = MLPActor(self.state_dim, self.act_dim, self.act_limit)
def act(self, state):
return self.policy(state).numpy()
def get_action(self, s, noise_scale):
a = self.act(s.reshape(1,-1).astype("float32")).reshape(-1)
a += noise_scale * np.random.randn(self.act_dim)
return np.clip(a, -self.act_limit, self.act_limit)
Listing 8-14MPLActorCritic in PyTorch and TensorFlow
q 损耗实现
重放缓冲区保持不变。下一个变化是 Q 损耗的计算方式。我们按照等式( 8.8 )到( 8.11 )实现目标策略平滑和限幅双 Q 学习。清单 8-15 包含 PyTorch 中compute_q_loss
的代码。这次我们没有明确列出 TensorFlow 的代码,可以在文件listing8_2_td3_tensorflow.ipynb
中进一步探究。
def compute_q_loss(agent, target_network, states, actions, rewards, next_states, done_flags,
gamma, target_noise, noise_clip, act_limit, tape):
# convert numpy array to proper data types
states = states.astype('float32')
actions = actions.astype('float32')
rewards = rewards.astype('float32')
next_states = next_states.astype('float32')
done_flags = done_flags.astype('float32')
# get q-values for all actions in current states
# use agent network
q1 = agent.q1(states, actions)
q2 = agent.q2(states, actions)
# Bellman backup for Q function
with tape.stop_recording():
action_target = target_network.policy(next_states)
# Target policy smoothing
epsilon = tf.random.normal(action_target.shape) * target_noise
epsilon = tf.clip_by_value(epsilon, -noise_clip, noise_clip)
action_target = action_target + epsilon
action_target = tf.clip_by_value(action_target, -act_limit, act_limit)
q1_target = target_network.q1(next_states, action_target)
q2_target = target_network.q2(next_states, action_target)
q_target = tf.minimum(q1_target, q2_target)
target = rewards + gamma * (1 - done_flags) * q_target
# MSE loss against Bellman backup
loss_q1 = tf.reduce_mean((q1 - target)**2)
loss_q2 = tf.reduce_mean((q2 - target)**2)
loss_q = loss_q1 + loss_q2
return loss_q
Listing 8-15Q-Loss in PyTorch
保单损失执行
策略损失计算保持不变,除了我们仅使用 q 网络之一,这实际上是第一个具有权重ϕ 1 的网络。
一步更新实施
one_step_update
函数的实现也非常相似,除了我们需要针对作为q_params
传递到函数中的 Q1 和 Q2 网络的组合网络权重来实现 q 损耗的梯度。此外,我们需要冻结和解冻q1
和q2
的网络权重。
清单 8-16 包含 PyTorch 中one_step_update
的实现,清单 8-17 包含 TensorFlow 中的代码。请特别注意权重是如何冻结和解冻的,梯度更新是如何计算的,以及权重是如何更新到目标网络的。这些操作在 PyTorch 代码和 TensorFlow 代码之间有一些细微差别。
def one_step_update(agent, target_network, q_params, q_optimizer, policy_optimizer,
states, actions, rewards, next_states, done_flags,
gamma, polyak, target_noise, noise_clip, act_limit,
policy_delay, timer):
#one step gradient for q-values
q_optimizer.zero_grad()
loss_q = compute_q_loss(agent, target_network, states, actions, rewards, next_states, done_flags,
gamma, target_noise, noise_clip, act_limit)
loss_q.backward()
q_optimizer.step()
# Update policy and target networks after policy_delay updates of Q-networks
if timer % policy_delay == 0:
#Freeze Q-network
for params in q_params:
params.requires_grad = False
#one setep gradient for policy network
policy_optimizer.zero_grad()
loss_policy = compute_policy_loss(agent, states)
loss_policy.backward()
policy_optimizer.step()
#UnFreeze Q-network
for params in q_params:
params.requires_grad = True
# update target networks with polyak averaging
with torch.no_grad():
for params, params_target in zip(agent.parameters(), target_network.parameters()):
params_target.data.mul_(polyak)
params_target.data.add_((1-polyak)*params.data)
Listing 8-16One-Step Update in PyTorch
清单 8-17 显示了一步更新中的 TensorFlow 版本。
def one_step_update(agent, target_network, q_params, q_optimizer, policy_optimizer,
states, actions, rewards, next_states, done_flags,
gamma, polyak, target_noise, noise_clip, act_limit,
policy_delay, timer):
#one step gradient for q-values
with tf.GradientTape() as tape:
loss_q = compute_q_loss(agent, target_network, states, actions, rewards, next_states, done_flags,
gamma, target_noise, noise_clip, act_limit, tape)
gradients = tape.gradient(loss_q, q_params)
q_optimizer.apply_gradients(zip(gradients, q_params))
# Update policy and target networks after policy_delay updates of Q-networks
if timer % policy_delay == 0:
#Freeze Q-network
agent.q1.trainable=False
agent.q2.trainable=False
#one setep gradient for policy network
with tf.GradientTape() as tape:
loss_policy = compute_policy_loss(agent, states, tape)
gradients = tape.gradient(loss_policy, agent.policy.trainable_variables)
policy_optimizer.apply_gradients(zip(gradients, agent.policy.trainable_variables))
#UnFreeze Q-network
agent.q1.trainable=True
agent.q2.trainable=True
# update target networks with polyak averaging
updated_model_weights = []
for weights, weights_target in zip(agent.get_weights(), target_network.get_weights()):
new_weights = polyak*weights_target+(1-polyak)*weights
updated_model_weights.append(new_weights)
target_network.set_weights(updated_model_weights)
Listing 8-17One-Step Update in TensorFlow
TD3 主回路
下一个变化是更新的频率。与 DDPG 不同,在 TD3 中,我们每更新两次 Q 网络就更新一次在线策略和目标权重。这是对 DDPG 规范的一个小改动,因此我们不在这里列出。
我们现在首先为钟摆环境运行 TD3,然后为月球着陆器健身房环境运行 TD3。我们可以看到,在五集之后,钟摆在直立状态下变得很平衡。月球着陆器的训练质量,就像 DDPG 一样,有点平庸。月球着陆器的环境是复杂的,因此我们需要运行更多次数的训练,比如 50 或 100 次。
我们可能也看不出 DDPG 和 TD3 在学习质量上有什么明显的区别。然而,如果我们可以在更复杂的环境中运行它,我们将会看到 TD3 相对于 DDPG 更高的性能。感兴趣的读者可以参考 TD3 的原始论文,查看 TD3 的作者对其他算法所做的基准研究。
很快我们就会看到本章的最后一个算法,一个叫做软演员评论家的算法。在此之前,我们将绕一小段路来理解 SAC 使用的一种叫做的重新参数化技巧。
重新参数化技巧
重新参数化技巧是变分自编码器(VAEs)中使用的变量方法的一个变化。在那里,需要通过随机的节点传播梯度。重新参数化也被用来降低梯度估计的方差。第二个原因是我们将在这里探讨的。这篇深度文章是在戈克尔·埃尔多安 5 的一篇博客文章之后进行的,其中有额外的分析推导和解释。
假设我们有一个随机变量 x ,它遵循正态分布。让分布由 θ 参数化如下:
)
(8.13)
我们从中抽取样本,然后使用这些样本找出下列各项中的最小值:
)
我们使用梯度下降法。我们的重点是找到两种不同的方法来确定导数/梯度∇θj(θ)的估计值。
得分/强化方式
首先,我们将遵循 log 技巧,这是我们在讨论使用策略梯度进行强化时所做的。我们看到它有很高的方差,这就是我们希望为前面显示的简单示例分布所展示的。
我们对 J ( θ )相对于 θ 求导。
)
)
)
)
)
)
)
)
接下来,我们使用蒙特卡罗来使用样本形成∇θj(θ)的估计。
)
将前面的表达式代入pθ(x)并取一个后跟梯度 wrt θ的对数,我们得到如下结果:
)
(8.14)
重新参数化技巧和路径导数
第二种方法是重新参数化技巧。我们将把 x 重新定义为一个常数和一个没有参数 θ 的正态分布的组合。让 x 定义如下:
)
我们可以看到,之前的重新参数化使 x 的分布保持不变。
)
我们来计算一下)。
)
)
)
由于期望值不依赖于θ,我们可以将梯度移入,而不会遇到前面方法中所示的“ log ”问题(即在导数内找到 log)。
)
)
)
)
接下来,我们将期望值转换为 MC 估计值,得到以下结果:
)
(8.15)
实验
我们使用方程( 8.14 )和( 8.15 )通过两种方法计算估计值的均值和方差。我们使用不同的 N 值来计算( 8.14 )和( 8.15 ),并且我们对 N 的每个值重复实验 10,000 次,以计算在( 8.14 )和( 8.15 )中给出的梯度估计的平均值和方差。我们的实验将表明这两个方程的平均值是相同的。换句话说,他们估计的是同一个值,但是( 8.14 )中估计的方差比( 8.15 )中估计的方差高了几乎一个数量级。在我们的例子中,它高出了 21.75 倍。
让我们冻结这个:
)
-
我们为x∽n(θ,1)生成样本,并在等式( 8.14 )中使用这些样本来计算∇θj(θ)的加固估计。
-
我们为ϵ∽n(0,1)生成样本,并在等式( 8.15 )中使用这些样本来计算∇θj(θ)的重新参数化估计。
实验的细节和代码请看listing8_3_reparameterization.ipynb
。在笔记本中,我们还计算了解析解以得出结果,如下所示。
对于使用( 8.14 )的加固梯度,梯度如下:
)
)
对于使用( 8.15 )重新参数化的梯度,梯度如下:
)
)
我们可以看到,在两种方法下,梯度估计具有相同的均值。然而,重新参数化方法的方差要小得多,小了一个数量级。这正是我们的代码运行所确认的。
综上所述,假设我们有一个以状态 s 为输入的策略网络,网络由 θ 参数化。策略网络产生策略的均值和方差,即一个正态分布的随机策略,其均值和方差是网络的输出,如图 8-8 所示。
图 8-8
随机政策网络
我们将动作 a 定义如下:
)
(8.16)
我们重新参数化动作 a 以分解出确定性和随机部分,使得随机部分不依赖于网络参数 θ 。
)
)
(8.17)
与使用非参数化方法相比,重新参数化允许我们计算具有较低方差的策略梯度。此外,重新参数化允许我们通过将随机部分分离为非参数化部分,以另一种方式将梯度流回网络。我们将在软演员-评论家算法中使用这种方法。
熵解释
在我们开始深入研究 SAC 的细节之前,还有一件事:让我们重温一下熵。在前一章中,我们讨论了熵作为正则项作为增强代码遍历的一部分。我们会做一些类似的事情。我们来理解一下熵是什么。
假设我们有一个随机变量 x 遵循某种分布 P ( x )。 x 的熵定义如下:
)
(8.18)
假设我们有一个硬币,硬币的 P ( H ) = ρ 和P(T)= 1ρ。我们针对 ρ ε (0,1)的不同值计算熵 H 。
)
我们可以绘制出 H ( x )对 ρ 的曲线,如图 8-9 所示。
图 8-9
作为 p 的函数的伯努利分布的熵,它是在试验中得到 1 的概率
我们可以看到,熵 H 是 ρ = 0.5 的最大值,也就是说,当我们在得到 1 或 0 之间具有最大不确定性时。换句话说,通过最大化熵,我们可以确保随机行动策略具有广泛的分布,并且不会过早地崩溃到一个尖峰。尖锐的峰会减少探索。
软演员评论家
软演员评论家大约与 TD3 同时出现。像 DDPG 和 TD3 一样,SAC 也使用了一个行动者-批评家结构,通过政策外学习进行连续控制。然而,与 DDPG 和 TD3 不同,SAC 学习随机策略。因此,SAC 在确定性策略算法(如 DDPG 和 TD3)与随机策略优化之间架起了一座桥梁。该算法是在 2018 年发表的一篇题为“软行动者-批评家:随机行动者的非策略最大熵深度强化学习”的论文中引入的。 6
它使用像 TD3 一样的限幅双 Q 技巧,并且由于它的学习随机策略,它间接地受益于目标策略平滑,而不明显需要向目标策略添加噪声。
SAC 的核心特性是使用熵作为最大化的一部分。引用论文作者的话:
“在这个框架中,行动者的目标是同时最大化预期收益和熵;也就是说,在尽可能随机行动的同时成功完成任务。”
SAC 对 TD3
这是两者的相似之处:
-
两者都使用均方贝尔曼误差(MSBE)最小化来达到共同目标。
-
使用目标 Q 网络来计算公共目标,该目标 Q 网络是使用聚丙烯平均获得的。
-
两者都使用限幅双 Q,它至少由两个 Q 值组成,以避免高估。
这就是不同之处:
-
SAC 使用熵正则化,这是 TD3 中没有的。
-
TD3 目标策略用于计算下一个状态的动作,而在 SAC 中,我们使用当前策略来获得下一个状态的动作。
-
在 TD3 中,目标策略通过向动作添加随机噪声来使用平滑。然而,在 SAC 中,学习到的策略是随机的,它提供了平滑效果,而没有任何明显的噪声添加。
熵正则化 q 损失
熵测量分布的随机性。熵越高,分布越平坦。一个尖峰政策的所有概率都集中在那个尖峰附近,因此它将具有低熵。通过熵正则化,策略被训练为最大化期望回报和熵之间的权衡,其中 α 控制该权衡。该策略被训练为最大化预期回报和熵之间的权衡,熵是策略中随机性的度量。
)
(8.19)
在此设置中, V π 被更改为包含每个时间步长的熵。
)
(8.20)
此外, Q π 被更改为包括除第一个时间步之外的每个时间步的熵奖励。
)
(8.21)
有了这些定义, V π 和 Q π 通过以下方式连接:
)
(8.22)
关于Qπ的贝尔曼方程如下:
)
)
(8.23)
右边是我们转换成样本估计的期望值。
)
(8.24)
以上,( s , a , r , s ,′)来自重放缓冲区,)来自在线/代理策略采样。在 SAC 中,我们根本不使用目标网络策略。
像 TD3 一样,SAC 使用限幅双 Q 并最小化均方贝尔曼误差(MSBE)。综上所述,SAC 中 Q 网络的损耗函数如下:
)
(8.25)
其中目标由下式给出:
)
(8.26)
我们将期望值转化为样本平均值。
)
(8.27)
我们将最小化的最终 Q 损耗如下:
)
(8.28)
重新参数化策略的策略损失
政策要选择最大化预期未来收益和未来熵的行动,即V【π】(s)。
)
我们将其改写如下:
)
(8.29)
该论文的作者使用了重新参数化和压缩高斯策略。
)
(8.30)
结合前面的两个等式( 8.29 )和( 8.30 ),并且还注意到我们的策略网络由策略网络权重 θ 参数化,我们得到以下等式:
)
(8.31)
接下来,我们用函数近似器代替 Q,取两个 Q 函数中的最小值。
)
(8.32)
政策目标相应地转变为:
)
(8.33)
像以前一样,我们在 PyTorch/TensorFlow 中使用最小化器。因此,我们引入了一个-ve
符号来将最大化转换为损失最小化。
)
(8.34)
我们还使用样本将期望值转换为估计值,得到以下结果:
)
(8.35)
伪代码和实现
至此,我们已经准备好给出完整的伪代码。请参见图 8-10 。
Soft Actor Critic
图 8-10
软演员评论家算法
代码实现
有了所有的数学推导和伪代码,是时候深入 PyTorch 和 TensorFlow 的实现了。使用 PyTorch 的实现在文件listing_8_4_sac_pytorch.ipynb
中。像以前一样,我们使用 SAC 在钟摆和月球着陆器环境中训练代理。TensorFlow 2.0 实现在文件listing_8_4_sac_tensorflow.ipynb
中。
策略网络-参与者实施
我们先看演员网。这一次,actor 实现将状态作为输入,和以前一样。然而,输出有两个部分。
-
要么是压扁(即通过 tanh 传递动作值)的确定性动作 a ,即μ θ ( s ),要么是来自分布
)的样本动作 a 。采样使用了重新参数化技巧,PyTorch 为您实现了这个技巧,如
distribution.rsample()
。你可以在https://pytorch.org/docs/stable/distributions.html
的“路径导数”主题下阅读 -
The second output is the log probability that we will need for calculating entropy inside the Q-loss as per equation (8.26). Since we are using squashed/tanh transformation, the log probability needs to apply a change of variables for random distribution using the following:
)
代码使用一些技巧来计算数值稳定的版本。你可以在原文中找到更多的细节。
清单 8-18 列出了前面讨论过的SquashedGaussianMLPActor
的代码。神经网络仍然和以前一样:两个大小为 256 单位的隐藏层,具有 ReLU 激活。
LOG_STD_MAX = 2
LOG_STD_MIN = -20
class SquashedGaussianMLPActor(nn.Module):
def __init__(self, state_dim, act_dim, act_limit):
super().__init__()
self.act_limit = act_limit
self.fc1 = nn.Linear(state_dim, 256)
self.fc2 = nn.Linear(256, 256)
self.mu_layer = nn.Linear(256, act_dim)
self.log_std_layer = nn.Linear(256, act_dim)
self.act_limit = act_limit
def forward(self, s, deterministic=False, with_logprob=True):
x = self.fc1(s)
x = F.relu(x)
x = self.fc2(x)
x = F.relu(x)
mu = self.mu_layer(x)
log_std = self.log_std_layer(x)
log_std = torch.clamp(log_std, LOG_STD_MIN, LOG_STD_MAX)
std = torch.exp(log_std)
# Pre-squash distribution and sample
pi_distribution = Normal(mu, std)
if deterministic:
# Only used for evaluating policy at test time.
pi_action = mu
else:
pi_action = pi_distribution.rsample()
if with_logprob
:
# Compute logprob from Gaussian, and then apply correction for Tanh squashing.
# NOTE: The correction formula is a little bit magic. To get an understanding
# of where it comes from, check out the original SAC paper (arXiv 1801.01290)
# and look in appendix C. This is a more numerically-stable equivalent to Eq 21.
# Try deriving it yourself as a (very difficult) exercise. :)
logp_pi = pi_distribution.log_prob(pi_action).sum(axis=-1)
logp_pi -= (2*(np.log(2) - pi_action - F.softplus(-2*pi_action))).sum(axis=1)
else:
logp_pi = None
pi_action = torch.tanh(pi_action)
pi_action = self.act_limit * pi_action
return pi_action, logp_pi
Listing 8-18SquashedGaussianMLPActor in PyTorch
清单 8-19 显示了 TensorFlow 版本。对于重新参数化,我们使用下面的代码行对其进行示例说明:
pi = mu + tf.random.normal(tf.shape(mu)) * std
核心 TensorFlow 包没有计算熵的对数似然的函数。我们实现了自己的小函数gaussian_likelihood
来做这件事。所有这些的一个替代方案是使用 TensorFlow 分布包:tfp.distributions.Distribution
。actor 的其余实现类似于我们在 PyTorch 中看到的。
LOG_STD_MAX = 2
LOG_STD_MIN = -20
EPS = 1e-8
def gaussian_likelihood(x, mu, log_std):
pre_sum = -0.5 * (((x-mu)/(tf.exp(log_std)+EPS))**2 + 2*log_std + np.log(2*np.pi))
return tf.reduce_sum(pre_sum, axis=1)
def apply_squashing_func(mu, pi, logp_pi):
# Adjustment to log prob
# NOTE: This formula is a little bit magic. To get an understanding of where it
# comes from, check out the original SAC paper (arXiv 1801.01290) and look in
# appendix C. This is a more numerically-stable equivalent to Eq 21.
# Try deriving it yourself as a (very difficult) exercise. :)
logp_pi -= tf.reduce_sum(2*(np.log(2) - pi - tf.nn.softplus(-2*pi)), axis=1)
# Squash those unbounded actions!
mu = tf.tanh(mu)
pi = tf.tanh(pi)
return mu, pi, logp_pi
class SquashedGaussianMLPActor(tf.keras.Model):
def __init__(self, state_dim, act_dim, act_limit):
super().__init__()
self.act_limit = act_limit
self.fc1 = layers.Dense(256, activation="relu")
self.fc2 = layers.Dense(256, activation="relu")
self.mu_layer = layers.Dense(act_dim)
self.log_std_layer = layers.Dense(act_dim)
self.act_limit = act_limit
def call(self, s):
x = self.fc1(s)
x = self.fc2(x)
mu = self.mu_layer(x)
log_std = self.log_std_layer(x)
log_std = tf.clip_by_value(log_std, LOG_STD_MIN, LOG_STD_MAX)
std = tf.exp(log_std)
pi = mu + tf.random.normal(tf.shape(mu)) * std
logp_pi = gaussian_likelihood(pi, mu, log_std)
mu, pi, logp_pi = apply_squashing_func(mu, pi, logp_pi)
mu *= self.act_limit
pi *= self.act_limit
return mu, pi, logp_pi
Listing 8-19SquashedGaussianMLPActor in TensorFlow
q-网络、组合模型和经验重放
Q-function 网络MLPQFunction
,它将演员和评论家组合成类MLPActorCritic
中的一个代理,和ReplayBuffer
的实现是相同的,或者至少非常相似。因此,我们不在这里列出代码的这些部分。
q 损失和保单损失执行
接下来我们看看compute_q_loss
和compute_policy_loss
。这是图 8-10 中伪代码的步骤 11 到 13 的直接实现。如果你将这些步骤与图 8-7 中 TD3 的步骤 11 到 14 进行比较,你会发现很多相似之处,除了动作是从在线网络中采样的,SAC 在两个损失中都有一个额外的熵项。这些变化很小,因此我们没有在这里的文本中明确列出代码。
单步更新和 SAC 主循环
同样,one_step_update
的代码和整个训练算法遵循与之前类似的模式。
一旦我们运行并训练了代理,我们会看到类似于 DDPG 和 TD3 的结果。如前所述,我们使用的是简单的环境,因此本章中的三种连续控制算法(DDPG、TD3 和 SAC)都表现良好。请参考本章中引用的各种论文,深入研究这些不同方法的官方性能比较。
这就把我们带到了在演员-评论家环境中持续控制这一章的结尾。到目前为止,我们已经看到了基于模型的策略迭代方法、基于深度学习的 Q 学习(DPN)方法、针对离散动作的策略梯度以及针对连续控制的策略梯度。这涵盖了大多数强化学习的流行方法。在结束我们的旅程之前,我们还有一个主要的主题要考虑:在无模型的世界中使用模型学习,以及在我们知道模型但它太复杂或太庞大而无法彻底探索的环境中进行有效的模型探索。
摘要
在这一章中,我们研究了持续控制的行动者-批评者方法,其中我们将非策略 Q 学习类型与策略梯度相结合,以导出非策略持续控制行动者-批评者方法。
我们首先看了 2016 年推出的深度确定性政策梯度。这是我们第一个连续控制算法。DDPG 是一种具有确定性连续控制策略的行动者-批评家方法。
接下来,我们看了双延迟 DDPG,它于 2018 年问世,解决了 DDPG 存在的一些稳定性和低效率挑战。像 DDPG 一样,它也通过演员-评论家架构学习了非政策环境中的确定性政策。
最后,我们看了软行动者批评家,它把 DDPG 式的学习和使用熵的随机政策优化联系起来。SAC 是一种使用行动者-批评家设置的非策略随机策略优化。我们还看到了重新参数化技巧,以获得梯度的较低方差估计。
https://spinningup.openai.com/
2
https://arxiv.org/pdf/1509.02971.pdf
3
http://proceedings.mlr.press/v32/silver14.pdf
4
https://arxiv.org/pdf/1802.09477.pdf
5
http://gokererdogan.github.io/2016/07/01/reparameterization-trick/
6
https://arxiv.org/abs/1801.01290
和 https://arxiv.org/pdf/1812.05905.pdf