强化学习从入门到放弃 —— 跟着 OpenAI 学强化学习

39 阅读43分钟

核心概念解析

简而言之,强化学习是关于智能体(agent)以及它们如何通过试错来学习的研究。它将这样一种理念形式化:对智能体的行为进行奖励或惩罚,会使它在未来更有可能重复或放弃该行为。

强化学习能做什么?

强化学习方法近年来在多个领域取得了广泛的成功。例如:

  • 它被用于教计算机在模拟环境中控制机器。
  • 也能在现实世界中控制机器
  • 它还因创造出在复杂策略游戏中具有突破性的人工智能而闻名,最著名的是围棋和 Dota
  • 在大模型的 post-training 过程中被用来调整模型输出"对齐"人类的喜好

核心概念和术语

强化学习的主要角色是智能体和环境。环境是智能体所生活和交互的世界。在每一步交互中,智能体看到世界状态的(可能是部分的)观察,然后决定采取的行动。当智能体对环境采取行动时,环境会发生变化,但环境本身也可能自行变化。

智能体还能从环境中感知到奖励信号,这是一个告诉它当前世界状态好坏的数字。智能体的目标是最大化其累积奖励,称为回报。强化学习方法是智能体学习实现其目标的行为的方式。

状态与观察

状态ss是对世界状态的完整描述。关于世界的信息没有任何是隐藏在状态之外的。观察oo是对状态的部分描述,可能会遗漏信息。

在深度强化学习中,我们几乎总是用实值向量、矩阵或更高阶的张量来表示状态和观察。例如,视觉观察可以用其像素值的 RGB 矩阵表示;机器人的状态可以用其关节角度和速度表示。

当智能体能够观察到环境的完整状态时,我们说环境是完全可观察的。当智能体只能看到部分观察时,我们说环境是部分可观察的。

动作空间

不同的环境允许不同种类的动作。在给定环境中所有有效动作的集合通常称为动作空间。有些环境,如雅达利(Atari)和围棋,具有离散的动作空间,其中智能体只有有限数量的移动可用。其他环境,如智能体在物理世界中控制机器人的环境,具有连续的动作空间。在连续空间中,动作是实值向量。

这种区别对深度强化学习中的方法有相当深远的影响。有些算法家族只能直接应用于一种情况,而要用于另一种情况则必须进行大量修改。

策略

策略是智能体用来决定采取什么动作的规则。它可以是确定性的,在这种情况下通常用μ\mu表示:

at=μ(st)a_t = \mu(s_t)

或者它可能是随机的,在这种情况下通常用π\pi表示:

atπ(st)a_t \sim \pi(\cdot | s_t)

因为策略本质上是智能体的 “大脑”,用 “策略” 代替 “智能体” 这个词并不少见,例如说 “策略正试图最大化奖励”。

在深度强化学习中,我们处理参数化的策略:其输出是可计算函数的策略,这些函数依赖于一组参数(例如神经网络的权重和偏置),我们可以通过一些优化算法来调整这些参数以改变行为。

我们通常用θ\thetaϕ\phi表示这种策略的参数,然后将其作为下标写在策略符号上以突出这种联系:

at=μθ(st)a_t = \mu_\theta(s_t)atπθ(st)a_t \sim \pi_\theta(\cdot | s_t)

确定性策略

示例:确定性策略

这是一个使用torch.nn包在 PyTorch 中为连续动作空间构建简单确定性策略的代码片段:

pi_net = nn.Sequential(
              nn.Linear(obs_dim, 64),
              nn.Tanh(),
              nn.Linear(64, 64),
              nn.Tanh(),
              nn.Linear(64, act_dim)
            )

这构建了一个多层感知器(MLP)网络,具有两个大小为 64 的隐藏层和 tanh 激活函数。如果obs是包含一批观察的 Numpy 数组,则可以使用pi_net获得一批动作,如下所示:

obs_tensor = torch.as_tensor(obs, dtype=torch.float32)


actions = pi_net(obs_tensor)
随机策略

深度强化学习中最常见的两种随机策略是分类策略和对角高斯策略。

分类策略可用于离散动作空间,而对角高斯策略用于连续动作空间。

使用和训练随机策略有两个关键计算至关重要:

  • 从策略中采样动作,
  • 计算特定动作的对数似然,logπθ(as)\log \pi_\theta(a|s)

下面,我们将描述如何对分类策略和对角高斯策略进行这些计算。

分类策略

分类策略就像对离散动作的分类器。你为分类策略构建神经网络的方式与为分类器构建神经网络的方式相同:输入是观察,然后有一个最终的线性层,给出每个动作的 logits。

采样:给定每个动作的概率,像 PyTorch 和 Tensorflow 这样的框架都有内置的采样工具。例如,请参见 PyTorch 中分类分布的文档,torch.multinomialtf.distributions.Categoricaltf.multinomial

对数似然:将最后一层的概率表示为Pθ(s)P_\theta(s)。它是一个向量,其条目数量与动作数量相同,因此我们可以将动作视为该向量的索引。动作aa的对数似然可以通过索引到该向量中获得:

logπθ(as)=log[Pθ(s)a]\log \pi_\theta(a|s) = \log [P_\theta(s)_a]

对角高斯策略

多元高斯分布(或者如果你愿意,也可以叫多元正态分布)由均值向量μ\mu和协方差矩阵Σ\Sigma描述。对角高斯分布是协方差矩阵仅在对角线上有条目的特殊情况。因此,我们可以用一个向量来表示它。

对角高斯策略总是有一个神经网络,将观察映射到平均动作μθ(s)\mu_\theta(s)。协方差矩阵通常有两种不同的表示方式。

第一种方式:有一个单一的对数标准差向量logσ\log \sigma,它不是状态的函数:logσ\log \sigma是独立的参数。(你应该知道:我们对 VPG、TRPO 和 PPO 的实现就是这样做的。)

第二种方式:有一个神经网络将状态映射到对数标准差logσθ(s)\log \sigma_\theta(s)。它可以选择与均值网络共享一些层。

请注意,在这两种情况下,我们输出的是对数标准差,而不是直接输出标准差。这是因为对数标准差可以取(,)(-\infty, \infty)中的任何值,而标准差必须是非负的。如果你不必强制执行这类约束,训练参数会更容易。通过对对数标准差取指数,可以立即得到标准差,因此我们以这种方式表示它们不会丢失任何信息。

采样:给定平均动作μθ(s)\mu_\theta(s)和标准差σθ(s)\sigma_\theta(s),以及来自球形高斯的噪声向量zzzN(0,I)z \sim \mathcal{N}(0, I)),动作样本可以通过以下方式计算:

a=μθ(s)+σθ(s)za = \mu_\theta(s) + \sigma_\theta(s) \odot z

其中\odot表示两个向量的元素相乘。标准框架有内置的生成噪声向量的方法,如torch.normaltf.random_normal。或者,你可以构建分布对象,例如通过torch.distributions.Normaltf.distributions.Normal,并使用它们生成样本。(后一种方法的优点是这些对象也可以为你计算对数似然。)

对数似然:对于具有均值μ=μθ(s)\mu = \mu_\theta(s)和标准差σ=σθ(s)\sigma = \sigma_\theta(s)的对角高斯分布,kk维动作aa的对数似然由下式给出:

logπθ(as)=12(i=1k((aiμi)2σi2+2logσi)+klog2π)\log \pi_\theta(a|s) = -\frac{1}{2} \left( \sum_{i=1}^k \left( \frac{(a_i - \mu_i)^2}{\sigma_i^2} + 2 \log \sigma_i \right) + k \log 2\pi \right)

轨迹

轨迹τ\tau是世界中状态和动作的序列:

τ=s0,a0,s1,a1,,sT,aT\tau = s_0, a_0, s_1, a_1, \ldots, s_T, a_T

世界的第一个状态s0s_0是从起始状态分布中随机采样的,有时用ρ0\rho_0表示:

s0ρ0()s_0 \sim \rho_0(\cdot)

状态转换(在时间tt的状态sts_t和时间t+1t+1的状态st+1s_{t+1}之间世界发生的情况)由环境的自然规律支配,并且仅取决于最近的动作ata_t。它们可以是确定性的:

st+1=f(st,at)s_{t+1} = f(s_t, a_t)

或者是随机的:

st+1P(st,at)s_{t+1} \sim P(\cdot | s_t, a_t)

动作来自智能体,根据其策略。

你应该知道

轨迹也经常被称为情节(episodes)或展开(rollouts)。

奖励与回报

奖励函数RR在强化学习中至关重要。它取决于世界的当前状态、刚刚采取的动作和世界的下一个状态:

rt=R(st,at,st+1)r_t = R(s_t, a_t, s_{t+1})

尽管通常这会简化为仅依赖于当前状态rt=R(st)r_t = R(s_t),或状态 - 动作对rt=R(st,at)r_t = R(s_t, a_t)

智能体的目标是最大化轨迹上的某种累积奖励,但这实际上可能有几种含义。我们将用R(τ)R(\tau)表示所有这些情况,从上下文应该可以清楚地知道指的是哪种情况,或者它可能并不重要(因为相同的方程适用于所有情况)。

一种回报是有限 horizon 无折扣回报,它只是在固定步骤窗口中获得的奖励总和:

R(τ)=t=0TrtR(\tau) = \sum_{t=0}^T r_t

另一种回报是无限 horizon 折扣回报,它是智能体获得的所有奖励的总和,但会按它们在未来出现的距离进行折扣。这种奖励表述包括一个折扣因子γ(0,1)\gamma \in (0, 1)

R(τ)=t=0γtrtR(\tau) = \sum_{t=0}^\infty \gamma^t r_t

但是,我们为什么会想要折扣因子呢?我们不就是想要获得所有奖励吗?我们确实是,但折扣因子在直观上很有吸引力,并且在数学上很方便。从直观层面来说:现在的现金比以后的现金好。从数学层面来说:无限 horizon 的奖励总和可能不会收敛到一个有限值,并且在方程中很难处理。但是,在合理的条件下,有了折扣因子,这个无限总和会收敛。

强化学习问题

无论选择哪种回报度量(无论是无限 horizon 折扣回报,还是有限 horizon 无折扣回报),也无论选择哪种策略,强化学习的目标都是选择一个策略,当智能体按照该策略行动时,能最大化期望回报。

要谈论期望回报,我们首先必须谈论轨迹上的概率分布。

假设环境转换和策略都是随机的。在这种情况下,TT步轨迹的概率是:

P(τπ)=ρ0(s0)t=0TP(st+1st,at)π(atst)P(\tau | \pi) = \rho_0(s_0) \prod_{t=0}^T P(s_{t+1} | s_t, a_t) \pi(a_t | s_t)

(无论哪种度量的)期望回报,记为J(π)J(\pi),则是:

J(π)=P(τπ)R(τ)dτ=Eτπ[R(τ)]J(\pi) = \int P(\tau | \pi) R(\tau) d\tau = \mathbb{E}_{\tau \sim \pi}[R(\tau)]

强化学习中的核心优化问题可以表示为:

π=argmaxπJ(π)\pi^* = \arg \max_\pi J(\pi)

其中π\pi^*是最优策略。

价值函数

知道一个状态或状态 - 动作对的价值通常是有用的。这里的价值是指如果你从该状态或状态 - 动作对开始,然后永远按照特定策略行动,所获得的期望回报。几乎所有强化学习算法都以这样或那样的方式使用价值函数。

这里有四个主要的值得注意的函数。

在策略价值函数Vπ(s)V^\pi(s),它给出如果你从状态ss开始并始终按照策略π\pi行动的期望回报:

Vπ(s)=Eπ[R(τ)s0=s]V^\pi(s) = \mathbb{E}_\pi[R(\tau) | s_0 = s]

在策略动作价值函数Qπ(s,a)Q^\pi(s, a),它给出如果你从状态ss开始,采取任意动作aa(可能不是来自该策略),然后永远按照策略π\pi行动的期望回报:

Qπ(s,a)=Eπ[R(τ)s0=s,a0=a]Q^\pi(s, a) = \mathbb{E}_\pi[R(\tau) | s_0 = s, a_0 = a]

最优价值函数V(s)V^*(s),它给出如果你从状态ss开始并始终按照环境中的最优策略行动的期望回报:

V(s)=maxπEπ[R(τ)s0=s]V^*(s) = \max_\pi \mathbb{E}_\pi[R(\tau) | s_0 = s]

最优动作价值函数Q(s,a)Q^*(s, a),它给出如果你从状态ss开始,采取任意动作aa,然后永远按照环境中的最优策略行动的期望回报:

Q(s,a)=maxπEπ[R(τ)s0=s,a0=a]Q^*(s, a) = \max_\pi \mathbb{E}_\pi[R(\tau) | s_0 = s, a_0 = a]

价值函数和动作价值函数之间有两个关键的联系经常出现:

Vπ(s)=Eaπ[Qπ(s,a)]V^\pi(s) = \mathbb{E}_{a \sim \pi}[Q^\pi(s, a)]

V(s)=maxaQ(s,a)V^*(s) = \max_a Q^*(s, a)

这些关系直接来自刚才给出的定义:你能证明它们吗?

最优 Q 函数与最优动作

最优动作价值函数Q(s,a)Q^*(s, a)和最优策略选择的动作之间有一个重要的联系。根据定义,Q(s,a)Q^*(s, a)给出从状态ss开始,采取(任意)动作aa,然后永远按照最优策略行动的期望回报。

状态ss下的最优策略将选择能最大化从状态ss开始的期望回报的动作。因此,如果我们有QQ^*,我们可以通过以下方式直接获得最优动作a(s)a^*(s)

a(s)=argmaxaQ(s,a)a^*(s) = \arg \max_a Q^*(s, a)

注意:可能有多个动作能最大化Q(s,a)Q^*(s, a),在这种情况下,所有这些动作都是最优的,最优策略可以随机选择其中任何一个。但总会有一个确定性地选择动作的最优策略。

贝尔曼方程

所有四个价值函数都遵循称为贝尔曼方程的特殊自洽方程。贝尔曼方程背后的基本思想是:

你的起点的价值是你期望从那里获得的奖励,加上你接下来到达的地方的价值。

在策略价值函数的贝尔曼方程是:

Vπ(s)=Eaπ,sP[r(s,a)+γVπ(s)]V^\pi(s) = \mathbb{E}_{a \sim \pi, s' \sim P}[r(s, a) + \gamma V^\pi(s')]

Qπ(s,a)=EsP[r(s,a)+γEaπ[Qπ(s,a)]]Q^\pi(s, a) = \mathbb{E}_{s' \sim P}[r(s, a) + \gamma \mathbb{E}_{a' \sim \pi}[Q^\pi(s', a')]]

其中sPs' \sim PsP(s,a)s' \sim P(\cdot | s, a)的简写,表示下一个状态ss'是从环境的转换规则中采样的;aπa \sim \piaπ(s)a \sim \pi(\cdot | s)的简写;aπa' \sim \piaπ(s)a' \sim \pi(\cdot | s')的简写

强化学习算法种类介绍

强化学习算法的分类

需要先声明的是:为现代强化学习领域的算法绘制一个准确且包罗万象的分类是相当困难的,因为算法的模块性很难用树状结构来表示。此外,为了使内容能在一页内呈现且在介绍性文章中易于理解,我们不得不省略相当多更高级的内容(如探索、迁移学习、元学习等)。尽管如此,我们的目标是:

  • 强调深度强化学习算法在学习内容和学习方式上最基本的设计选择;
  • 揭示这些选择中的权衡;
  • 将一些著名的现代算法置于这些选择的背景下进行阐述。

无模型(Model-Free)与有模型(Model-Based)强化学习

强化学习算法中一个最重要的分支点是智能体是否能够访问(或学习)环境模型。环境模型指的是一个能够预测状态转换和奖励的函数。

拥有模型的主要优势在于,它允许智能体通过前瞻性思考进行规划,查看一系列可能选择的结果,并明确地在这些选项中做出决策。然后,智能体可以将前瞻性规划的结果提炼为一个学习到的策略。一个特别著名的例子是 AlphaZero。当这种方法奏效时,与不使用模型的方法相比,它能显著提高样本效率。

主要的缺点是,智能体通常无法获得环境的真实模型。在这种情况下,如果智能体想要使用模型,就必须纯粹从经验中学习模型,这会带来几个挑战。最大的挑战是模型中的偏差可能会被智能体利用,导致智能体在学习到的模型上表现良好,但在真实环境中却表现不佳(或者非常糟糕)。模型学习本质上是困难的,所以即使投入大量的时间和计算资源,也可能无法取得成效。

使用模型的算法称为有模型方法,不使用模型的算法称为无模型方法。虽然无模型方法放弃了使用模型可能带来的样本效率提升,但它们往往更易于实现和调优。在撰写本介绍时(2018 年 9 月),无模型方法比有模型方法更受欢迎,并且得到了更广泛的开发和测试。

学习内容

强化学习算法另一个关键的分支点是学习内容的问题。常见的学习内容包括:

  • 策略,无论是随机的还是确定性的;
  • 动作价值函数(Q 函数);
  • 价值函数;
  • 和 / 或环境模型。
无模型强化学习中的学习内容

在无模型强化学习中,表示和训练智能体主要有两种方法:

  1. 策略优化(Policy Optimization):这类方法将策略明确表示为πθ(as)\pi_\theta(a|s)。它们通过对性能目标J(π)J(\pi)进行梯度上升直接优化参数θ\theta,或者通过最大化J(π)J(\pi)的局部近似来间接优化。这种优化几乎总是在线策略(on-policy)的,这意味着每次更新只使用在按照最新版本的策略行动时收集的数据。策略优化通常还包括学习在线策略价值函数Vπ(s)V^\pi(s)的近似器Vϕ(s)V_\phi(s),它用于确定如何更新策略。

一些策略优化方法的例子有:

  • A2C/A3C,它通过梯度上升直接最大化性能;
  • PPO,其更新通过最大化一个替代目标函数来间接最大化性能,该替代目标函数对J(π)J(\pi)因更新而产生的变化提供了一个保守估计。
  1. Q 学习(Q-Learning):这类方法学习最优动作价值函数Q(s,a)Q^*(s,a)的近似器Qθ(s,a)Q_\theta(s,a)。通常,它们使用基于贝尔曼方程的目标函数。这种优化几乎总是离线策略(off-policy)的,这意味着每次更新可以使用训练过程中任何时候收集的数据,无论数据是智能体在探索环境时如何选择动作收集的。相应的策略通过 Q 和π\pi^*之间的联系获得:Q 学习智能体采取的动作由a(s)=argmaxaQθ(s,a)a(s)=\arg\max_a Q_\theta(s,a)给出。

Q 学习方法的例子包括:

  • DQN,一个经典的方法,极大地推动了深度强化学习领域的发展;
  • C51,一种变体,它学习回报的分布,其期望为 Q。
  1. 策略优化与 Q 学习之间的权衡:策略优化方法的主要优势在于其原则性,即直接优化我们想要的目标。这往往使它们稳定且可靠。相比之下,Q 学习方法只是通过训练QθQ_\theta满足自洽方程来间接优化智能体性能。这种学习方式有很多失败模式,因此往往不太稳定 [1]。但是,当 Q 学习方法奏效时,它们具有显著更高的样本效率,因为与策略优化技术相比,它们能更有效地重用数据。
  2. 策略优化与 Q 学习之间的结合:幸运的是,策略优化和 Q 学习并非互不相容(在某些情况下,事实证明它们是等价的),并且存在一系列介于这两个极端之间的算法。处于这个范围内的算法能够谨慎地权衡双方的优缺点。例子包括:
  • DDPG,一种同时学习确定性策略和 Q 函数的算法,通过相互利用来改进彼此;
  • SAC,一种变体,它使用随机策略、熵正则化和其他一些技巧来稳定学习,并在标准基准测试中取得比 DDPG 更高的分数。
有模型强化学习中的学习内容

与无模型强化学习不同,有模型强化学习没有少量易于定义的方法集群:使用模型的方式有很多正交的方式。我们将给出一些例子,但这个列表远非详尽无遗。在每种情况下,模型既可以是给定的,也可以是学习到的。

MBMF 的研究探索了将学习到的环境模型与 MPC 结合在一些深度强化学习的标准基准任务上的应用。

专家迭代(Expert Iteration):纯规划的一个直接后续方法涉及使用和学习策略πθ(as)\pi_\theta(a|s)的明确表示。智能体在模型中使用规划算法(如蒙特卡洛树搜索),通过从当前策略中采样来生成计划的候选动作。规划算法产生的动作比单独的策略产生的动作更好,因此相对于该策略而言,它是一个 “专家”。之后,策略会被更新以产生更接近规划算法输出的动作。

ExIt 算法使用这种方法来训练深度神经网络玩 Hex 游戏。AlphaZero 是这种方法的另一个例子。

  • 无模型方法的数据增强(Data Augmentation for Model-Free Methods):使用无模型强化学习算法来训练策略或 Q 函数,但要么 1)在更新智能体时用虚构经验增强真实经验,要么 2)仅使用虚构经验来更新智能体。

MBVE 是用虚构经验增强真实经验的一个例子。World Models 是仅使用虚构经验来训练智能体的一个例子,他们称之为 “在梦中训练”。

  • 将规划循环嵌入策略(Embedding Planning Loops into Policies):另一种方法将规划过程直接作为子程序嵌入策略中,以便完整的计划成为策略的辅助信息,同时使用任何标准的无模型算法训练策略的输出。这个框架中的关键概念是,策略可以学习选择如何以及何时使用这些计划。这减少了模型偏差带来的问题,因为如果模型在某些状态下不适合规划,策略可以简单地学习忽略它。

策略优化入门:从理论到实现

在强化学习领域,策略优化算法是一类重要的方法,它们直接对策略进行参数化并通过梯度上升来最大化预期回报。本文将深入探讨策略优化算法的数学基础,并结合代码示例进行讲解。我们将涵盖策略梯度理论中的三个关键成果:描述策略性能相对于策略参数梯度的最简单方程、允许从表达式中删除无用项的规则,以及允许向表达式中添加有用项的规则。最后,我们将把这些结果结合起来,描述基于优势的策略梯度表达式 —— 这也是我们在 Vanilla Policy Gradient(香草策略梯度)实现中使用的版本。

最简单策略梯度的推导

这里,我们考虑一个随机的、参数化的策略πθ\pi_{\theta}。我们的目标是最大化预期回报J(πθ)=E[R(τ)]J(\pi_{\theta}) = \mathbb{E}[R(\tau)],其中τ\tau是一条轨迹。为了便于推导,我们将R(τ)R(\tau)视为有限 horizon 无折扣回报,但对于无限 horizon 折扣回报的情况,推导过程几乎相同。

我们希望通过梯度上升来优化策略,即:

θk+1=θk+αθJ(πθ)θk\theta_{k+1} = \theta_k + \alpha \nabla_{\theta} J(\pi_{\theta})|_{\theta_k}

策略性能的梯度θJ(πθ)\nabla_{\theta} J(\pi_{\theta})被称为策略梯度,使用这种方式优化策略的算法被称为策略梯度算法(例如 Vanilla Policy Gradient 和 TRPO。PPO 通常也被称为策略梯度算法,尽管这并不完全准确)。

要实际使用该算法,我们需要一个可以数值计算的策略梯度表达式。这涉及两个步骤:1)推导策略性能的解析梯度,结果表明它具有期望值的形式;2)形成该期望值的样本估计,这可以通过有限数量的智能体 - 环境交互步骤的数据来计算。

在本小节中,我们将找到该表达式的最简单形式。在后面的小节中,我们将展示如何改进这个最简单的形式,以得到我们在标准策略梯度实现中实际使用的版本。

我们首先列出一些对推导解析梯度有用的事实:

  1. 轨迹的概率:在动作来自策略πθ\pi_{\theta}的情况下,轨迹τ=(s0,a0,...,sT+1)\tau = (s_0, a_0, ..., s_{T+1})的概率为:

    P(τθ)=ρ0(s0)t=0TP(st+1st,at)πθ(atst)P(\tau|\theta) = \rho_0(s_0) \prod_{t=0}^{T} P(s_{t+1}|s_t, a_t) \pi_{\theta}(a_t|s_t)

  2. 对数导数技巧:对数导数技巧基于微积分中的一个简单规则:log(v)log(v)xx的导数是1vdvdx\frac{1}{v} \frac{dv}{dx}。重新排列并结合链式法则,我们得到:

    θP(τθ)=P(τθ)θlogP(τθ)\nabla_{\theta} P(\tau|\theta) = P(\tau|\theta) \nabla_{\theta} log P(\tau|\theta)

  3. 轨迹的对数概率:轨迹的对数概率为:

    logP(τθ)=logρ0(s0)+t=0T(logP(st+1st,at)+logπθ(atst))log P(\tau|\theta) = log \rho_0(s_0) + \sum_{t=0}^{T} (log P(s_{t+1}|s_t, a_t) + log \pi_{\theta}(a_t|s_t))

  4. 环境函数的梯度:环境与参数θ\theta无关,因此ρ0(s0)\rho_0(s_0)P(st+1st,at)P(s_{t+1}|s_t, a_t)R(τ)R(\tau)的梯度都为零。

  5. 轨迹对数概率的梯度:因此,轨迹对数概率的梯度为:

    θlogP(τθ)=t=0Tθlogπθ(atst)\nabla_{\theta} log P(\tau|\theta) = \sum_{t=0}^{T} \nabla_{\theta} log \pi_{\theta}(a_t|s_t)

综合以上所有内容,我们可以推导出:

基本策略梯度的推导

θJ(πθ)=θEτπθ[R(τ)]=θτP(τθ)R(τ)dτ=τθP(τθ)R(τ)dτ=τP(τθ)θlogP(τθ)R(τ)dτ=Eτπθ[θlogP(τθ)R(τ)]=Eτπθ[t=0Tθlogπθ(atst)R(τ)]\begin{aligned} \nabla_{\theta} J(\pi_{\theta}) &= \nabla_{\theta} \mathbb{E}_{\tau \sim \pi_{\theta}}[R(\tau)] \\ &= \nabla_{\theta} \int_{\tau} P(\tau|\theta) R(\tau) d\tau \\ &= \int_{\tau} \nabla_{\theta} P(\tau|\theta) R(\tau) d\tau \\ &= \int_{\tau} P(\tau|\theta) \nabla_{\theta} log P(\tau|\theta) R(\tau) d\tau \\ &= \mathbb{E}_{\tau \sim \pi_{\theta}}[\nabla_{\theta} log P(\tau|\theta) R(\tau)] \\ &= \mathbb{E}_{\tau \sim \pi_{\theta}}[\sum_{t=0}^{T} \nabla_{\theta} log \pi_{\theta}(a_t|s_t) R(\tau)] \end{aligned}

这是一个期望值,这意味着我们可以用样本均值来估计它。如果我们收集一组轨迹D={τi}i=1..N\mathcal{D} = \{\tau_i\}_{i=1..N},其中每条轨迹都是通过让智能体使用策略πθ\pi_{\theta}在环境中行动而获得的,那么策略梯度可以估计为:

g^=1Ni=1Nt=0Tθlogπθ(ai,tsi,t)R(τi)\hat{g} = \frac{1}{N} \sum_{i=1}^{N} \sum_{t=0}^{T} \nabla_{\theta} log \pi_{\theta}(a_{i,t}|s_{i,t}) R(\tau_i)

这个最后的表达式是我们所期望的可计算表达式的最简单版本。假设我们已经以一种允许我们计算θlogπθ(as)\nabla_{\theta} log \pi_{\theta}(a|s)的方式表示了我们的策略,并且如果我们能够运行该策略在环境中收集轨迹数据集,我们就可以计算策略梯度并进行更新步骤。

实现最简单的策略梯度

预期梯度 - 对数概率引理(Expected Grad-Log-Prob Lemma)

在本小节中,我们将推导一个在策略梯度理论中广泛使用的中间结果。我们将其称为预期梯度 - 对数概率(EGLP)

EGLP 引理:假设PθP_{\theta}是一个关于随机变量xx的参数化概率分布。那么:

ExPθ[θlogPθ(x)]=0\mathbb{E}_{x \sim P_{\theta}}[\nabla_{\theta} log P_{\theta}(x)] = 0

证明

回想所有概率分布都是归一化的:

Pθ(x)dx=1\int P_{\theta}(x) dx = 1

对两边取梯度:

θPθ(x)dx=θ1=0\nabla_{\theta} \int P_{\theta}(x) dx = \nabla_{\theta} 1 = 0

使用对数导数技巧:

0=θPθ(x)dx=Pθ(x)θlogPθ(x)dx=ExPθ[θlogPθ(x)]0 = \int \nabla_{\theta} P_{\theta}(x) dx = \int P_{\theta}(x) \nabla_{\theta} log P_{\theta}(x) dx = \mathbb{E}_{x \sim P_{\theta}}[\nabla_{\theta} log P_{\theta}(x)]

不要让过去干扰你

检查我们最近得到的策略梯度表达式:

θJ(πθ)=Eτπθ[t=0Tθlogπθ(atst)R(τ)]\nabla_{\theta} J(\pi_{\theta}) = \mathbb{E}_{\tau \sim \pi_{\theta}}[\sum_{t=0}^{T} \nabla_{\theta} log \pi_{\theta}(a_t|s_t) R(\tau)]

用这个梯度进行更新会按比例增加每个动作的对数概率,比例为R(τ)R(\tau),即所有获得的奖励之和。但这并不太合理。

智能体实际上应该只根据动作的后果来强化动作。在采取动作之前获得的奖励与该动作的好坏无关:只有之后的奖励才有关系。

事实证明,这种直觉在数学中也有所体现,我们可以证明策略梯度也可以表示为:

θJ(πθ)=Eτπθ[t=0Tθlogπθ(atst)t=tTR(st,at,st+1)]\nabla_{\theta} J(\pi_{\theta}) = \mathbb{E}_{\tau \sim \pi_{\theta}}[\sum_{t=0}^{T} \nabla_{\theta} log \pi_{\theta}(a_t|s_t) \sum_{t'=t}^{T} R(s_{t'}, a_{t'}, s_{t'+1})]

在这种形式中,动作只根据其采取后获得的奖励进行强化。

我们将这种形式称为 “reward-to-go 策略梯度”,因为轨迹中某一点之后的奖励之和,即

Rt=t=tTR(st,at,st+1)R_t = \sum_{t'=t}^{T} R(s_{t'}, a_{t'}, s_{t'+1})

被称为从该点的 reward-to-go,并且这个策略梯度表达式依赖于状态 - 动作对的回报reward-to-go。

##策略梯度中的基线(Baselines in Policy Gradients)

预期梯度对数概率引理(EGLP 引理)有一个直接的推论:对于任何仅依赖于状态的函数bb,都有:

Eπθ[θlogπθ(atst)b(st)]=0\mathbb{E}_{\pi_{\theta}} \left[ \nabla_{\theta} \log \pi_{\theta}(a_t | s_t) b(s_t) \right] = 0

这意味着我们可以在策略梯度的表达式中随意添加或减去这类项,而不会改变其期望值。基于此,策略梯度可以表示为:

θJ(πθ)=Eτπθ[t=0Tθlogπθ(atst)(t=tTR(st,at,st+1)b(st))]\nabla_{\theta} J(\pi_{\theta}) = \underset{\tau \sim \pi_{\theta}}{\mathbb{E}} \left[ \sum_{t=0}^{T} \nabla_{\theta} \log \pi_{\theta}(a_t | s_t) \left( \sum_{t'=t}^{T} R(s_{t'}, a_{t'}, s_{t'+1}) - b(s_t) \right) \right]

在这里,任何以这种方式使用的函数bb都被称为基线(baseline)。

最常用的基线是在策略价值函数Vπ(st)V^{\pi}(s_t)。它表示智能体从状态sts_t开始,之后按照策略π\pi行动所获得的平均回报。

从经验来看,选择b(st)=Vπ(st)b(s_t) = V^{\pi}(s_t)能够有效降低策略梯度样本估计的方差,从而使策略学习更快、更稳定。从概念上讲,这也很合理:如果智能体获得了预期的回报,它应该对此保持 “中立” 态度。

需要注意的是,在实际应用中,Vπ(st)V^{\pi}(s_t)无法精确计算,因此必须进行近似。通常会使用神经网络Vϕ(st)V_{\phi}(s_t)来近似,并且该神经网络会与策略同时更新(以确保价值网络始终近似最新策略的价值函数)。

大多数策略优化算法(包括 VPG、TRPO、PPO 和 A2C)中,学习VϕV_{\phi}的最简单方法是最小化均方误差目标:

ϕk=argminϕEst,Rπk(Vϕ(st)R)2\phi_k = \arg \min_{\phi} \mathbb{E}_{s_t, R \sim \pi_k} \left( V_{\phi}(s_t) - R \right)^2

其中πk\pi_k是第kk个周期的策略。这通常通过从之前的价值参数ϕk1\phi_{k-1}开始,执行一步或多步梯度下降来实现。

策略梯度的其他形式

到目前为止,我们已经了解到策略梯度的一般形式为:

θJ(πθ)=Eτπθ[t=0Tθlogπθ(atst)Φt]\nabla_{\theta} J(\pi_{\theta}) = \underset{\tau \sim \pi_{\theta}}{\mathbb{E}} \left[ \sum_{t=0}^{T} \nabla_{\theta} \log \pi_{\theta}(a_t | s_t) \Phi_t \right]

其中Φt\Phi_t可以是以下几种形式之一:

  • Φt=R(τ)\Phi_t = R(\tau)(整个轨迹的回报)
  • Φt=t=tTR(st,at,st+1)\Phi_t = \sum_{t'=t}^{T} R(s_{t'}, a_{t'}, s_{t'+1})(从时刻tt开始的回报总和,即回报到 - go)
  • Φt=t=tTR(st,at,st+1)b(st)\Phi_t = \sum_{t'=t}^{T} R(s_{t'}, a_{t'}, s_{t'+1}) - b(s_t)(减去基线后的回报到 - go)

这些选择虽然会导致策略梯度的样本估计具有不同的方差,但它们的期望值是相同的。此外,还有另外两种重要的Φt\Phi_t选择值得了解:

  1. 在策略动作价值函数(On-Policy Action-Value Function):选择Φt=Qπ(st,at)\Phi_t = Q^{\pi}(s_t, a_t)也是有效的。有关这一结论的证明(可选)可以参考相关页面。
  2. 优势函数(Advantage Function):动作的优势定义为Aπ(st,at)=Qπ(st,at)Vπ(st)A^{\pi}(s_t, a_t) = Q^{\pi}(s_t, a_t) - V^{\pi}(s_t),它描述了相对于当前策略下的平均水平,该动作是更好还是更差。选择Φt=Aπ(st,at)\Phi_t = A^{\pi}(s_t, a_t)同样有效。这是因为它等价于使用Φt=Qπ(st,at)\Phi_t = Q^{\pi}(s_t, a_t)然后再减去一个价值函数基线,而我们知道添加或减去基线是被允许的。

基于优势函数的策略梯度公式应用极为广泛,不同的算法会采用多种不同的方法来估计优势函数。

Vanilla Policy Gradient:策略梯度方法的基础标杆

在强化学习的策略梯度(Policy Gradients)家族中,Vanilla Policy Gradient(VPG) 扮演着至关重要的基础角色。它是理解更复杂策略梯度算法(如 PPO、A2C 等)的基石,其核心思想简洁而深刻:通过提升带来高回报的动作的概率,降低带来低回报的动作的概率,逐步优化策略直至达到最优。

作为最经典的策略梯度方法之一,VPG 为后续更先进的算法提供了关键的理论和实践基础。许多高级策略梯度算法本质上都是在 VPG 的框架上进行改进,例如通过引入剪辑机制(如 PPO)来稳定训练,或通过多线程并行化(如 A2C)来提高数据效率。因此,深入理解 VPG 的原理、特性和实现细节,是掌握强化学习中策略梯度方法的必要前提。

VPG 的核心特性

1. 在线策略(On-policy)特性

VPG 是一种在线策略算法,这意味着它必须基于当前正在训练的策略所收集的数据来更新策略参数。每次策略更新后,之前收集的数据就会被丢弃,需要用新的策略重新与环境交互以获取新数据。这种特性虽然在数据利用效率上不如离线策略(Off-policy)算法,但保证了训练过程的稳定性,因为更新始终基于最新的策略行为。

2. 适用的动作空间

VPG 具有良好的通用性,既可以应用于离散动作空间,也可以应用于连续动作空间。在离散动作空间中,策略可以直接输出每个动作的概率分布;而在连续动作空间中,策略通常输出高斯分布等连续概率分布的参数(如均值和标准差),通过采样来选择动作。这种灵活性使得 VPG 能够应对多种不同类型的强化学习任务,从简单的游戏(如 Atari 系列)到复杂的机器人控制问题。

3. 并行化支持

OpenAI Spinning Up 提供的 VPG 实现支持基于 MPI 的并行化。通过多进程并行与环境交互,可以在相同时间内收集更多的轨迹数据,从而加速训练过程并提高策略的稳定性。这种并行化能力对于需要大量交互数据的复杂环境尤为重要。

核心公式解析

VPG 的数学基础围绕策略性能的梯度展开,核心是如何计算策略目标函数的梯度并进行优化。

策略目标函数的梯度

πθ\pi_\theta为参数化的策略(θ\theta为参数),J(πθ)J(\pi_\theta)为该策略的期望有限 horizon 无折扣回报。则J(πθ)J(\pi_\theta)的梯度可表示为:

θJ(πθ)=Eτπθ[t=0Tθlogπθ(atst)A^t]\nabla_\theta J(\pi_\theta) = \underset{\tau \sim \pi_\theta}{\mathbb{E}} \left[ \sum_{t=0}^T \nabla_\theta \log \pi_\theta(a_t | s_t) \hat{A}_t \right]

其中:

  • τ\tau表示一条轨迹(trajectory),即状态、动作、奖励的序列;
  • A^t\hat{A}_t为当前策略的优势函数估计,用于衡量在状态sts_t下执行动作ata_t相对于平均水平的优势;
  • 期望Eτπθ\underset{\tau \sim \pi_\theta}{\mathbb{E}}表示对所有由策略πθ\pi_\theta生成的轨迹取平均。

策略更新规则

VPG 采用随机梯度上升来更新策略参数,以最大化目标函数J(πθ)J(\pi_\theta)。更新公式为:

θk+1=θk+αθJ(πθk)\theta_{k+1} = \theta_k + \alpha \nabla_\theta J(\pi_{\theta_k})

其中α\alpha为学习率,控制每次参数更新的步长。

需要注意的是,尽管上述公式基于有限 horizon 无折扣回报,但在实际实现中,策略梯度方法通常使用无限 horizon 折扣回报来计算优势函数估计,以更好地平衡短期和长期回报。

探索与利用的平衡

在强化学习中,智能体需要在探索(Exploration)和利用(Exploitation)之间取得平衡:探索是指尝试新的动作以发现潜在的高回报;利用是指选择已知能带来高回报的动作。

VPG 通过训练随机策略来实现探索与利用的平衡。在训练初期,策略的随机性较强,智能体会通过采样不同的动作进行充分探索,以了解环境的奖励机制。随着训练的进行,策略会逐渐调整,增加高回报动作的概率,减少低回报动作的概率,从而更多地利用已发现的高回报策略。

然而,这种机制也存在一定的风险:如果策略过早收敛到局部最优解,可能会陷入局部最优,无法发现更优的全局策略。因此,在实际应用中,通常需要通过调整策略的初始随机性、学习率等超参数来缓解这一问题。

算法流程(伪代码)

VPG 的算法流程可以概括为以下步骤:

  1. 输入初始策略参数θ0\theta_0和初始价值函数参数ϕ0\phi_0
  2. 对于每个迭代k=0,1,2,...k=0,1,2,...
  • 步骤 3:通过当前策略πk=π(θk)\pi_k = \pi(\theta_k)与环境交互,收集轨迹集合Dk={τi}D_k = \{\tau_i\}
  • 步骤 4-5:计算回报到 go(Rewards-to-go)RtR_t,并基于当前价值函数VϕkV_{\phi_k}计算优势估计A^t\hat{A}_t
  • 步骤 6:估计策略梯度g^k=1DkτDkt=0Tθlogπθ(atst)θkA^t\hat{g}_k = \frac{1}{|D_k|} \sum_{\tau \in D_k} \sum_{t=0}^T \nabla_\theta \log \pi_\theta(a_t | s_t) \big|_{\theta_k} \hat{A}_t
  • 步骤 7:通过梯度上升更新策略参数,如θk+1=θk+αkg^k\theta_{k+1} = \theta_k + \alpha_k \hat{g}_k(或使用 Adam 等优化器)。
  • 步骤 8:通过最小化均方误差来拟合价值函数:ϕk+1=argminϕ1DkTτDkt=0T(Vϕ(st)R^t)2\phi_{k+1} = \arg \min_\phi \frac{1}{|D_k| T} \sum_{\tau \in D_k} \sum_{t=0}^T (V_\phi(s_t) - \hat{R}_t)^2,通常使用梯度下降算法。

实现

import numpy as np
import torch
from torch.optim import Adam
import gym
import time
import spinup.algos.pytorch.vpg.core as core
from spinup.utils.logx import EpochLogger
from spinup.utils.mpi_pytorch import setup_pytorch_for_mpi, sync_params, mpi_avg_grads
from spinup.utils.mpi_tools import (
    mpi_fork,
    mpi_avg,
    proc_id,
    mpi_statistics_scalar,
    num_procs,
)


class VPGBuffer:

    def __init__(self, obs_dim, act_dim, size, gamma=0.99, lam=0.95):
        self.obs_buf = np.zeros(core.combined_shape(size, obs_dim), dtype=np.float32)
        self.act_buf = np.zeros(core.combined_shape(size, act_dim), dtype=np.float32)
        self.adv_buf = np.zeros(size, dtype=np.float32)
        self.rew_buf = np.zeros(size, dtype=np.float32)
        self.ret_buf = np.zeros(size, dtype=np.float32)
        self.val_buf = np.zeros(size, dtype=np.float32)
        self.logp_buf = np.zeros(size, dtype=np.float32)
        self.gamma, self.lam = gamma, lam
        self.ptr, self.path_start_idx, self.max_size = 0, 0, size

    def store(self, obs, act, rew, val, logp):
       
        assert self.ptr < self.max_size  # buffer has to have room so you can store
        self.obs_buf[self.ptr] = obs
        self.act_buf[self.ptr] = act
        self.rew_buf[self.ptr] = rew
        self.val_buf[self.ptr] = val
        self.logp_buf[self.ptr] = logp
        self.ptr += 1

    def finish_path(self, last_val=0):

        path_slice = slice(self.path_start_idx, self.ptr)
        rews = np.append(self.rew_buf[path_slice], last_val)
        vals = np.append(self.val_buf[path_slice], last_val)

        # the next two lines implement GAE-Lambda advantage calculation
        deltas = rews[:-1] + self.gamma * vals[1:] - vals[:-1]
        self.adv_buf[path_slice] = core.discount_cumsum(deltas, self.gamma * self.lam)

        # the next line computes rewards-to-go, to be targets for the value function
        self.ret_buf[path_slice] = core.discount_cumsum(rews, self.gamma)[:-1]

        self.path_start_idx = self.ptr

    def get(self):
        
        assert self.ptr == self.max_size  # buffer has to be full before you can get
        self.ptr, self.path_start_idx = 0, 0
        # the next two lines implement the advantage normalization trick
        adv_mean, adv_std = mpi_statistics_scalar(self.adv_buf)
        self.adv_buf = (self.adv_buf - adv_mean) / adv_std
        data = dict(
            obs=self.obs_buf,
            act=self.act_buf,
            ret=self.ret_buf,
            adv=self.adv_buf,
            logp=self.logp_buf,
        )
        return {k: torch.as_tensor(v, dtype=torch.float32) for k, v in data.items()}


def vpg(
    env_fn,
    actor_critic=core.MLPActorCritic,
    ac_kwargs=dict(),
    seed=0,
    steps_per_epoch=4000,
    epochs=50,
    gamma=0.99,
    pi_lr=1e-3,
    vf_lr=1e-3,
    train_v_iters=80,
    lam=0.97,
    logger_kwargs=dict(),
    save_freq=10,
):
    # Special function to avoid certain slowdowns from PyTorch + MPI combo.
    setup_pytorch_for_mpi()

    # Set up logger and save configuration
    logger = EpochLogger(**logger_kwargs)
    logger.save_config(locals())

    # Random seed
    seed += 10000 * proc_id()
    torch.manual_seed(seed)
    np.random.seed(seed)

    # Instantiate environment
    env = env_fn()
    obs_dim = env.observation_space.shape
    act_dim = env.action_space.shape

    # Create actor-critic module
    ac = actor_critic(env.observation_space, env.action_space, **ac_kwargs)

    # Sync params across processes
    sync_params(ac)

    # Count variables
    var_counts = tuple(core.count_vars(module) for module in [ac.pi, ac.v])
    logger.log("\nNumber of parameters: \t pi: %d, \t v: %d\n" % var_counts)

    # Set up experience buffer
    local_steps_per_epoch = int(steps_per_epoch / num_procs())
    buf = VPGBuffer(obs_dim, act_dim, local_steps_per_epoch, gamma, lam)

    # Set up function for computing VPG policy loss
    def compute_loss_pi(data):
        obs, act, adv, logp_old = data["obs"], data["act"], data["adv"], data["logp"]

        # Policy loss
        pi, logp = ac.pi(obs, act)
        loss_pi = -(logp * adv).mean()

        # Useful extra info
        approx_kl = (logp_old - logp).mean().item()
        ent = pi.entropy().mean().item()
        pi_info = dict(kl=approx_kl, ent=ent)

        return loss_pi, pi_info

    # Set up function for computing value loss
    def compute_loss_v(data):
        obs, ret = data["obs"], data["ret"]
        return ((ac.v(obs) - ret) ** 2).mean()

    # Set up optimizers for policy and value function
    pi_optimizer = Adam(ac.pi.parameters(), lr=pi_lr)
    vf_optimizer = Adam(ac.v.parameters(), lr=vf_lr)

    # Set up model saving
    logger.setup_pytorch_saver(ac)

    def update():
        data = buf.get()

        # Get loss and info values before update
        pi_l_old, pi_info_old = compute_loss_pi(data)
        pi_l_old = pi_l_old.item()
        v_l_old = compute_loss_v(data).item()

        # Train policy with a single step of gradient descent
        pi_optimizer.zero_grad()
        loss_pi, pi_info = compute_loss_pi(data)
        loss_pi.backward()
        mpi_avg_grads(ac.pi)  # average grads across MPI processes
        pi_optimizer.step()

        # Value function learning
        for i in range(train_v_iters):
            vf_optimizer.zero_grad()
            loss_v = compute_loss_v(data)
            loss_v.backward()
            mpi_avg_grads(ac.v)  # average grads across MPI processes
            vf_optimizer.step()

        # Log changes from update
        kl, ent = pi_info["kl"], pi_info_old["ent"]
        logger.store(
            LossPi=pi_l_old,
            LossV=v_l_old,
            KL=kl,
            Entropy=ent,
            DeltaLossPi=(loss_pi.item() - pi_l_old),
            DeltaLossV=(loss_v.item() - v_l_old),
        )

    # Prepare for interaction with environment
    start_time = time.time()
    o, _ = env.reset()
    ep_ret, ep_len = 0, 0

    # Main loop: collect experience in env and update/log each epoch
    for epoch in range(epochs):
        for t in range(local_steps_per_epoch):
            a, v, logp = ac.step(torch.as_tensor(o, dtype=torch.float32))

            next_o, r, d, truncated, _ = env.step(a)
            ep_ret += r
            ep_len += 1

            # save and log
            buf.store(o, a, r, v, logp)
            logger.store(VVals=v)

            # Update obs (critical!)
            o = next_o

            timeout = truncated
            terminal = d or timeout
            epoch_ended = t == local_steps_per_epoch - 1

            if terminal or epoch_ended:
                if epoch_ended and not (terminal):
                    print(
                        "Warning: trajectory cut off by epoch at %d steps." % ep_len,
                        flush=True,
                    )
                # if trajectory didn't reach terminal state, bootstrap value target
                if timeout or epoch_ended:
                    _, v, _ = ac.step(torch.as_tensor(o, dtype=torch.float32))
                else:
                    v = 0
                buf.finish_path(v)
                if terminal:
                    # only save EpRet / EpLen if trajectory finished
                    logger.store(EpRet=ep_ret, EpLen=ep_len)
                o, _ = env.reset()
                ep_ret, ep_len = 0, 0

        # Save model
        if (epoch % save_freq == 0) or (epoch == epochs - 1):
            logger.save_state({"env": env}, None)

        # Perform VPG update!
        update()

        # Log info about epoch
        logger.log_tabular("Epoch", epoch)
        logger.log_tabular("EpRet", with_min_and_max=True)
        logger.log_tabular("EpLen", average_only=True)
        logger.log_tabular("VVals", with_min_and_max=True)
        logger.log_tabular("TotalEnvInteracts", (epoch + 1) * steps_per_epoch)
        logger.log_tabular("LossPi", average_only=True)
        logger.log_tabular("LossV", average_only=True)
        logger.log_tabular("DeltaLossPi", average_only=True)
        logger.log_tabular("DeltaLossV", average_only=True)
        logger.log_tabular("Entropy", average_only=True)
        logger.log_tabular("KL", average_only=True)
        logger.log_tabular("Time", time.time() - start_time)
        logger.dump_tabular()


if __name__ == "__main__":
    import argparse

    parser = argparse.ArgumentParser()
    parser.add_argument("--env", type=str, default="HalfCheetah-v2")
    parser.add_argument("--hid", type=int, default=64)
    parser.add_argument("--l", type=int, default=2)
    parser.add_argument("--gamma", type=float, default=0.99)
    parser.add_argument("--seed", "-s", type=int, default=0)
    parser.add_argument("--cpu", type=int, default=4)
    parser.add_argument("--steps", type=int, default=4000)
    parser.add_argument("--epochs", type=int, default=200)
    parser.add_argument("--exp_name", type=str, default="vpg")
    args = parser.parse_args()

    mpi_fork(args.cpu)  # run parallel code with mpi

    from spinup.utils.run_utils import setup_logger_kwargs

    logger_kwargs = setup_logger_kwargs(args.exp_name, args.seed)

    vpg(
        lambda: gym.make(args.env),
        actor_critic=core.MLPActorCritic,
        ac_kwargs=dict(hidden_sizes=[args.hid] * args.l),
        gamma=args.gamma,
        seed=args.seed,
        steps_per_epoch=args.steps,
        epochs=args.epochs,
        logger_kwargs=logger_kwargs,
    )

Trust Region Policy Optimization(TRPO):强化学习中的信赖域策略优化算法

信赖域策略优化(TRPO)算法通过在满足新旧策略相似度约束的前提下,尽可能采取最大步长来提升策略性能。这种约束通过 KL 散度(Kullback-Leibler Divergence)来表达,KL 散度是一种衡量概率分布之间 “距离”(类似但不完全等同于传统距离)的指标。

这与普通的策略梯度方法有显著区别:传统策略梯度方法通过限制参数空间中新旧策略的距离来保持相似性,但即使参数空间中看似微小的差异,也可能导致策略性能的巨大波动 —— 一步错误的更新就可能使策略性能急剧下降。这使得在使用 vanilla 策略梯度时,采用大步长存在风险,从而降低了样本效率。而 TRPO 巧妙地避免了这种性能崩溃问题,并且能够快速、单调地提升策略性能。

核心特点速览

  • 策略类型:TRPO 是一种 on-policy(同策略)算法。
  • 适用环境:可用于具有离散或连续动作空间的环境。
  • 并行支持:OpenAI Spinning Up 的 TRPO 实现支持通过 MPI 进行并行计算。

关键公式解析

πθ\pi_\theta表示带有参数θ\theta的策略。TRPO 的理论更新公式为:

θk+1=argmaxθL(θk,θ)\theta_{k+1} = \arg\max_\theta L(\theta_k, \theta)

s.t.DKL(πθπθk)δs.t. D_{KL}(\pi_\theta \parallel \pi_{\theta_k}) \leq \delta

其中,L(θk,θ)L(\theta_k, \theta)替代优势函数(surrogate advantage),用于衡量新策略πθ\pi_\theta相对于旧策略πθk\pi_{\theta_k}的性能,其计算基于旧策略收集的数据:

L(θk,θ)=Es,aπθk[πθ(as)πθk(as)Aθk(s,a)]L(\theta_k, \theta) = \mathbb{E}_{s,a \sim \pi_{\theta_k}} \left[ \frac{\pi_\theta(a|s)}{\pi_{\theta_k}(a|s)} A_{\theta_k}(s,a) \right]

DKL(πθπθk)D_{KL}(\pi_\theta \parallel \pi_{\theta_k})是在旧策略访问过的状态上,新旧策略之间的平均 KL 散度:

DKL(πθπθk)=Esπθk[DKL(πθ(s)πθk(s))]D_{KL}(\pi_\theta \parallel \pi_{\theta_k}) = \mathbb{E}_{s \sim \pi_{\theta_k}} \left[ D_{KL}(\pi_\theta(\cdot|s) \parallel \pi_{\theta_k}(\cdot|s)) \right]

由于理论上的 TRPO 更新难以直接计算,TRPO 通过一些近似来快速求解。我们将目标函数和约束条件在θk\theta_k处进行泰勒展开至一阶项:

L(θk,θ)gT(θθk)L(\theta_k, \theta) \approx g^T (\theta - \theta_k)

DKL(πθπθk)12(θθk)TH(θθk)D_{KL}(\pi_\theta \parallel \pi_{\theta_k}) \approx \frac{1}{2} (\theta - \theta_k)^T H (\theta - \theta_k)

由此得到近似的优化问题:

θk+1=argmaxθgT(θθk)\theta_{k+1} = \arg\max_\theta g^T (\theta - \theta_k)

s.t.12(θθk)TH(θθk)δs.t. \frac{1}{2} (\theta - \theta_k)^T H (\theta - \theta_k) \leq \delta

这个近似问题可以通过拉格朗日对偶方法解析求解,得到:

θk+1=θk+αH1ggTH1g2δ\theta_{k+1} = \theta_k + \alpha \frac{H^{-1} g}{\sqrt{\frac{g^T H^{-1} g}{2\delta}}}

如果仅停留于此,该算法就等价于自然策略梯度(Natural Policy Gradient)。但由于泰勒展开引入的近似误差,这个解可能不满足 KL 约束,或者无法提升替代优势函数。因此 TRPO 增加了回溯线搜索(backtracking line search)机制:

θk+1=θk+αjH1ggTH1g2δ\theta_{k+1} = \theta_k + \alpha^j \frac{H^{-1} g}{\sqrt{\frac{g^T H^{-1} g}{2\delta}}}

其中α(0,1)\alpha \in (0,1)是回溯系数,jj是满足 KL 约束且能产生正替代优势的最小非负整数。

最后需要注意的是:当处理具有数千或数百万参数的神经网络策略时,计算和存储矩阵H1H^{-1}的成本极高。TRPO 通过共轭梯度算法(conjugate gradient algorithm)来求解Hx=gHx = g,得到x=H1gx = H^{-1}g,这仅需要计算矩阵 - 向量乘积HxHx,而无需直接计算和存储整个矩阵HH。具体而言,通过符号运算计算:

Hx=θ(θDKL(πθπθk)x)Hx = \nabla_\theta \left( \nabla_\theta D_{KL}(\pi_\theta \parallel \pi_{\theta_k}) \cdot x \right)

即可在不计算完整矩阵的情况下得到正确结果。

探索与利用的平衡

TRPO 以 on-policy 的方式训练随机策略。这意味着它通过根据最新的随机策略采样动作来进行探索。动作选择的随机性大小取决于初始条件和训练过程。在训练过程中,策略的随机性通常会逐渐降低,因为更新规则会鼓励策略利用已发现的奖励。但这也可能导致策略陷入局部最优。

算法伪代码

算法 1:Trust Region Policy Optimization

  1. 输入:初始策略参数θ0\theta_0,初始价值函数参数ϕ0\phi_0
  2. 超参数:KL 散度限制δ\delta,回溯系数α\alpha,最大回溯步数KK
  3. 对于k=0,1,2,...k=0,1,2,...循环:
  • 通过运行策略πk=π(θk)\pi_k = \pi(\theta_k)在环境中收集轨迹集Dk={τi}D_k = \{\tau_i\}
  • 计算回报 - to-go(rewards-to-go)RtR_t
  • 基于当前价值函数VϕkV_{\phi_k}计算优势估计AtA_t(可使用任何优势估计方法)
  • 估计策略梯度:g^k=1DkτDkt=0Tθlogπθ(atst)θkA^t\hat{g}_k = \frac{1}{|D_k|} \sum_{\tau \in D_k} \sum_{t=0}^T \nabla_\theta \log \pi_\theta(a_t|s_t) \big|_{\theta_k} \hat{A}_t
  • 使用共轭梯度算法计算x^kH^k1g^k\hat{x}_k \approx \hat{H}_k^{-1} \hat{g}_k,其中HkH_k是样本平均 KL 散度的 Hessian 矩阵
  • 通过回溯线搜索更新策略:θk+1=θk+αj2δx^kTH^kx^kx^k\theta_{k+1} = \theta_k + \alpha^j \sqrt{\frac{2\delta}{\hat{x}_k^T \hat{H}_k \hat{x}_k}} \hat{x}_k,其中j{0,1,2,...,K}j \in \{0,1,2,...,K\}是满足样本 KL 约束且能提升样本损失的最小值
  • 通过均方误差回归拟合价值函数:ϕk+1=argminϕ1DkTτDkt=0T(Vϕ(st)R^t)2\phi_{k+1} = \arg\min_\phi \frac{1}{|D_k| T} \sum_{\tau \in D_k} \sum_{t=0}^T (V_\phi(s_t) - \hat{R}_t)^2,通常使用梯度下降算法
  • 结束循环

Proximal Policy Optimization(PPO):简单高效的强化学习算法

PPO(Proximal Policy Optimization)的提出源于与 TRPO 相同的问题:如何利用当前拥有的数据,在策略上迈出尽可能大的改进步骤,同时又不会因为步子过大而意外导致性能崩溃。

与 TRPO 采用复杂的二阶方法解决该问题不同,PPO 是一系列一阶方法,它通过一些其他技巧来保持新策略与旧策略的接近性。PPO 方法实现起来明显更简单,并且从经验上看,其性能至少与 TRPO 相当。

PPO 主要有两种变体:PPO - Penalty 和 PPO - Clip。

PPO - Penalty 近似求解类似 TRPO 的 KL 约束更新,但它在目标函数中对 KL 散度进行惩罚,而不是将其作为硬约束,并且在训练过程中自动调整惩罚系数,以使其得到适当的缩放。

PPO - Clip 的目标函数中没有 KL 散度项,也根本没有约束。它依靠目标函数中的专门剪辑来消除新策略远离旧策略的动机。本文将重点介绍 PPO - Clip(这是 OpenAI 主要使用的变体)。

二、快速 facts

  • PPO 是一种 on - policy 算法。
  • PPO 可用于具有离散或连续动作空间的环境。
  • Spinning Up 实现的 PPO 支持通过 MPI 进行并行化。

三、关键方程

PPO - Clip 通过以下方式更新策略:

θk+1=argmaxθE[L(s,a,θk,θ)]\theta_{k + 1}=\arg\max_{\theta}E\left[L(s,a,\theta_{k},\theta)\right],其中s,aπθks,a\sim\pi_{\theta_{k}}

通常会采取多个步骤(通常是小批量)的 SGD 来最大化目标。这里的LL由下式给出:

L(s,a,θk,θ)=min(πθ(as)πθk(as)Aπθk(s,a),g(ϵ,Aπθk(s,a)))L(s,a,\theta_{k},\theta)=\min\left(\frac{\pi_{\theta}(a|s)}{\pi_{\theta_{k}}(a|s)}A^{\pi_{\theta_{k}}}(s,a),g(\epsilon,A^{\pi_{\theta_{k}}}(s,a))\right)

其中,g(ϵ,A)={(1+ϵ)AA0(1ϵ)AA<0g(\epsilon,A)=\begin{cases}(1 + \epsilon)A& A\geq0\\(1-\epsilon)A& A<0\end{cases}

这个表达式相当复杂,乍一看很难理解它在做什么,以及它如何帮助保持新策略与旧策略的接近。事实证明,这个目标有一个相当简化的版本,更容易理解(这也是我们在代码中实现的版本)。

为了理解其背后的直觉,让我们看看单个状态 - 动作对s,as,a,并考虑不同情况。

当优势为正时:假设该状态 - 动作对的优势为正,在这种情况下,它对目标的贡献简化为L(s,a,θk,θ)=min(πθ(as)πθk(as),(1+ϵ))Aπθk(s,a)L(s,a,\theta_{k},\theta)=\min\left(\frac{\pi_{\theta}(a|s)}{\pi_{\theta_{k}}(a|s)},(1 + \epsilon)\right)A^{\pi_{\theta_{k}}}(s,a)

由于优势为正,如果动作变得更有可能(即πθ(as)πθk(as)\frac{\pi_{\theta}(a|s)}{\pi_{\theta_{k}}(a|s)}增大),目标将会增加。但该项中的最小值限制了目标的增加幅度。一旦πθ(as)πθk(as)>(1+ϵ)\frac{\pi_{\theta}(a|s)}{\pi_{\theta_{k}}(a|s)}>(1 + \epsilon),最小值开始起作用,该项达到(1+ϵ)Aπθk(s,a)(1 + \epsilon)A^{\pi_{\theta_{k}}}(s,a)的上限。因此,新策略不会从远离旧策略中获益。

当优势为负时:假设该状态 - 动作对的优势为负,此时它对目标的贡献简化为L(s,a,θk,θ)=max(πθ(as)πθk(as),(1ϵ))Aπθk(s,a)L(s,a,\theta_{k},\theta)=\max\left(\frac{\pi_{\theta}(a|s)}{\pi_{\theta_{k}}(a|s)},(1-\epsilon)\right)A^{\pi_{\theta_{k}}}(s,a)

由于优势为负,如果动作变得更不可能(即πθ(as)πθk(as)\frac{\pi_{\theta}(a|s)}{\pi_{\theta_{k}}(a|s)}减小),目标将会增加。但该项中的最大值限制了目标的增加幅度。一旦πθ(as)πθk(as)<(1ϵ)\frac{\pi_{\theta}(a|s)}{\pi_{\theta_{k}}(a|s)}<(1-\epsilon),最大值开始起作用,该项达到(1ϵ)Aπθk(s,a)(1-\epsilon)A^{\pi_{\theta_{k}}}(s,a)的上限。因此,同样,新策略不会从远离旧策略中获益。

到目前为止,我们已经了解到剪辑通过消除策略发生巨大变化的动机而起到正则化的作用,超参数ϵ\epsilon对应于新策略在仍然能从目标中获利的情况下可以偏离旧策略的程度。

需要知道的是,虽然这种剪辑在很大程度上有助于确保合理的策略更新,但仍然有可能得到一个与旧策略相差太远的新策略,不同的 PPO 实现使用了许多技巧来避免这种情况。在我们的实现中,我们使用一种特别简单的方法:早停。如果新策略与旧策略的平均 KL 散度超过一个阈值,我们就停止采取梯度步骤。

当你对基本的数学和实现细节感到满意时,值得查看其他实现,看看它们是如何处理这个问题的!

四、探索与利用

PPO 以 on - policy 的方式训练随机策略。这意味着它通过根据最新版本的随机策略采样动作来进行探索。动作选择中的随机量取决于初始条件和训练过程。在训练过程中,策略通常会逐渐减少随机性,因为更新规则鼓励它利用已经发现的奖励。这可能会导致策略陷入局部最优。

五、伪代码

Algorithm 1 PPO - Clip

1: 输入:初始策略参数θ0\theta_{0},初始价值函数参数ϕ0\phi_{0}

2: for k=0,1,2,...k = 0,1,2,... do

3: 通过运行策略πk=π(θk)\pi_{k}=\pi(\theta_{k})在环境中收集轨迹集合Dk={τi}\mathcal{D}_{k}=\{\tau_{i}\}

4: 计算未来奖励RtR_{t}

5: 基于当前价值函数VϕkV_{\phi_{k}}计算优势估计AtA_{t}(使用任何优势估计方法)

6: 通过最大化 PPO - Clip 目标来更新策略:θk+1=argmaxθ1DkTτDkt=0Tmin(πθ(atst)πθk(atst)Aπθk(st,at),g(ϵ,Aπθk(st,at)))\theta_{k + 1}=\arg\max_{\theta}\frac{1}{|\mathcal{D}_{k}|T}\sum_{\tau\in\mathcal{D}_{k}}\sum_{t = 0}^{T}min\left(\frac{\pi_{\theta}(a_{t}|s_{t})}{\pi_{\theta_{k}}(a_{t}|s_{t})}A^{\pi_{\theta_{k}}}(s_{t},a_{t}),g(\epsilon,A^{\pi_{\theta_{k}}}(s_{t},a_{t}))\right),通常通过随机梯度上升实现

7: 通过均方误差回归拟合价值函数:ϕk+1=argminϕ1DkTτDkt=0T(Vϕ(st)R^t)2\phi_{k + 1}=\arg\min_{\phi}\frac{1}{|\mathcal{D}_{k}|T}\sum_{\tau\in\mathcal{D}_{k}}\sum_{t = 0}^{T}(V_{\phi}(s_{t})-\hat{R}_{t})^{2},通常通过某种梯度下降算法实现

8: end for

Deep Deterministic Policy Gradient(DDPG):连续动作空间的强化学习算法

深度确定策略梯度(DDPG)算法旨在解决具有连续动作空间环境中的强化学习问题。在传统的策略梯度方法中,策略通常是随机的,即给定状态下,策略输出的是动作的概率分布,然后根据概率采样选择动作。然而,在许多实际应用场景中,如机器人控制、自动驾驶等,动作空间是连续的,从概率分布中采样动作效率较低,并且可能导致策略的不稳定。

DDPG 算法基于确定性策略,直接输出具体的动作值,避免了随机策略中的采样过程,提高了决策效率。此外,DDPG 还结合了深度神经网络强大的表达能力,能够处理复杂的高维状态空间,在连续动作空间的强化学习任务中表现出色。

DDPG 算法借鉴了多个经典算法的思想,它结合了深度 Q 网络(DQN)的经验回放(Experience Replay)和目标网络(Target Network)技术,以及确定性策略梯度(Deterministic Policy Gradient,DPG)算法的理论基础,通过不断学习和优化,使智能体能够在连续动作空间的环境中找到最优策略。

二、快速 facts

  • 算法类型:DDPG 是一种 off - policy 算法,这意味着用于学习策略的数据可以来自于与当前策略不同的策略所产生的轨迹。
  • 适用场景:特别适用于动作空间为连续的强化学习环境,如机器人的关节控制、飞行器的姿态调整等。
  • 网络结构:包含四个深度神经网络,分别为 Actor 网络、Actor 目标网络、Critic 网络、Critic 目标网络。其中 Actor 网络用于输出确定性动作,Critic 网络用于评估动作的价值。
  • 超参数特性:对超参数较为敏感,超参数的选择会显著影响算法的性能和收敛速度 。

三、关键方程

DDPG 算法的核心在于对 Actor 网络和 Critic 网络的更新。

Actor 网络更新

Actor 网络的目标是最大化 Q 值,其更新的梯度为:

θμJˉ1NiaQ(s,a;θQ)s=si,a=μ(si;θμ)θμμ(s;θμ)si\nabla_{\theta^{\mu}}\bar{J} \approx \frac{1}{N}\sum_{i}\nabla_{a}Q(s,a;\theta^{Q})\big|_{s = s^{i},a = \mu(s^{i};\theta^{\mu})}\nabla_{\theta^{\mu}}\mu(s;\theta^{\mu})\big|_{s^{i}}

其中,θμ\theta^{\mu}是 Actor 网络的参数,Jˉ\bar{J}是目标函数,NN是样本数量,Q(s,a;θQ)Q(s,a;\theta^{Q})是 Critic 网络输出的 Q 值,μ(s;θμ)\mu(s;\theta^{\mu})是 Actor 网络根据状态ss输出的动作。该公式的含义是通过计算 Q 值对动作的梯度与 Actor 网络输出动作对其参数的梯度的乘积,来更新 Actor 网络的参数,从而使得 Actor 网络输出的动作能够最大化 Q 值。

Critic 网络更新

Critic 网络的目标是最小化 TD 误差( temporal - difference error),其损失函数为:

L=1Ni(yiQ(si,ai;θQ))2L = \frac{1}{N}\sum_{i}(y^{i} - Q(s^{i},a^{i};\theta^{Q}))^{2}

其中,yiy^{i}是目标 Q 值,计算方式为:

yi=ri+γQ(si,μ(si;θμ);θQ)y^{i} = r^{i} + \gamma Q'(s'^{i},\mu'(s'^{i};\theta^{\mu'}) ;\theta^{Q'})

这里,rir^{i}是在状态sis^{i}执行动作aia^{i}后获得的奖励,γ\gamma是折扣因子,QQ'μ\mu'分别是 Critic 目标网络和 Actor 目标网络,θQ\theta^{Q'}θμ\theta^{\mu'}是它们对应的参数。Critic 网络通过最小化损失函数LL,来不断调整自身参数,以更准确地评估动作的价值。

此外,为了使算法更加稳定,DDPG 采用了目标网络缓慢更新的策略,即:

θQτθQ+(1τ)θQ\theta^{Q'}\leftarrow\tau\theta^{Q}+(1 - \tau)\theta^{Q'}

θμτθμ+(1τ)θμ\theta^{\mu'}\leftarrow\tau\theta^{\mu}+(1 - \tau)\theta^{\mu'}

其中,τ\tau是一个较小的更新系数(通常远小于 1),通过这种软更新方式,使得目标网络的参数变化较为缓慢,从而提高算法的稳定性和收敛性。

四、探索与利用

作为一种 off - policy 算法,DDPG 通过在确定性动作上添加噪声来实现探索与利用的平衡。常见的做法是添加高斯噪声(Gaussian Noise),即在 Actor 网络输出的确定性动作上叠加一个符合高斯分布的随机噪声,使智能体能够在一定程度上探索新的动作。随着训练的进行,噪声的强度可以逐渐衰减,使得智能体更多地利用已经学习到的较好策略。

例如,在训练初期,较大的噪声可以帮助智能体广泛地探索环境,发现潜在的高回报动作;而在训练后期,较小的噪声可以使智能体专注于已经发现的较优策略,进一步优化动作选择,以获得更高的累计奖励。

五、伪代码

Algorithm 1 DDPG

1: 初始化 Actor 网络μ(s;θμ)\mu(s;\theta^{\mu})、Critic 网络Q(s,a;θQ)Q(s,a;\theta^{Q}),以及对应的目标网络μ(s;θμ)\mu'(s;\theta^{\mu'})Q(s,a;θQ)Q'(s,a;\theta^{Q'}),设置参数θμ=θμ\theta^{\mu'}=\theta^{\mu}θQ=θQ\theta^{Q'}=\theta^{Q}

2: 初始化经验回放缓冲区DD

3: for episode = 1, M do

4: 初始化环境状态ss

5: for t = 1, T do

6: 根据当前策略μ(s;θμ)\mu(s;\theta^{\mu})并添加噪声生成动作aa

7: 在环境中执行动作aa,获取下一个状态ss'、奖励rr和是否结束的标志donedone

8: 将(s,a,r,s,done)(s,a,r,s',done)存储到经验回放缓冲区DD

9: 从DD中随机采样一批样本(si,ai,ri,si,donei)(s_{i},a_{i},r_{i},s_{i}',done_{i})

10: 计算目标 Q 值yi=ri+γQ(si,μ(si;θμ);θQ)y_{i}=r_{i}+\gamma Q'(s_{i}',\mu'(s_{i}';\theta^{\mu'}) ;\theta^{Q'}),若doneidone_{i}为 True,则yi=riy_{i}=r_{i}

11: 更新 Critic 网络:通过最小化(yiQ(si,ai;θQ))2(y_{i}-Q(s_{i},a_{i};\theta^{Q}))^{2}来更新θQ\theta^{Q}

12: 更新 Actor 网络:通过计算$\n