hsn-rl-algo-py-merge-1

65 阅读1小时+

Python 强化学习算法实用指南(二)

原文:annas-archive.org/md5/e3819a6747796b03b9288831f4e2b00c

译者:飞龙

协议:CC BY-NC-SA 4.0

第六章:学习随机优化与 PG 优化

到目前为止,我们已经探讨并开发了基于价值的强化学习算法。这些算法通过学习一个价值函数来找到一个好的策略。尽管它们表现良好,但它们的应用受限于一些内在的限制。在本章中,我们将介绍一类新的算法——策略梯度方法,它们通过从不同的角度处理强化学习问题来克服基于价值方法的限制。

策略梯度方法基于学习到的参数化策略来选择动作,而不是依赖于价值函数。在本章中,我们还将详细阐述这些方法背后的理论和直觉,并在此基础上开发出最基本版本的策略梯度算法,称为REINFORCE

REINFORCE 由于其简单性存在一些不足,但这些不足只需要稍加努力就可以得到缓解。因此,我们将展示 REINFORCE 的两个改进版本,分别是带基线的REINFORCE演员-评论员AC)模型。

本章将涵盖以下主题:

  • 策略梯度方法

  • 理解 REINFORCE 算法

  • 带基线的 REINFORCE

  • 学习 AC 算法

策略梯度方法

到目前为止,学习和开发的算法都是基于价值的,它们的核心是学习一个价值函数,V(s),或动作价值函数,Q(s, a)。价值函数是一个定义从给定状态或状态-动作对所能累积的总奖励的函数。然后,可以基于估计的动作(或状态)值来选择一个动作。

因此,贪婪策略可以定义如下:

![]

当结合深度神经网络时,基于价值的方法可以学习非常复杂的策略,从而控制在高维空间中操作的智能体。尽管这些方法有着很大的优点,但在处理具有大量动作或连续动作空间的问题时,它们会遇到困难。

在这种情况下,最大化操作是不可行的。策略梯度PG)算法在这种背景下展现了巨大的潜力,因为它们可以很容易地适应连续的动作空间。

PG 方法属于更广泛的基于策略的方法类,其中包括进化策略,这将在第十一章《理解黑盒优化算法》中进一步探讨。PG 算法的独特性在于它们使用策略的梯度,因此得名策略梯度

相较于第三章《使用动态规划解决问题》中报告的强化学习算法分类,下面的图示展示了一种更简洁的分类方式:

策略梯度方法的例子有REINFORCEAC,将在接下来的章节中介绍。

策略的梯度

强化学习的目标是最大化一个轨迹的期望回报(总奖励,无论是否折扣)。目标函数可以表示为:

其中θ是策略的参数,例如深度神经网络的可训练变量。

在 PG 方法中,目标函数的最大化是通过目标函数的梯度![]来实现的。通过梯度上升,我们可以通过将参数朝着梯度的方向移动来改善![],因为梯度指向函数增大的方向。

我们必须沿着梯度的方向前进,因为我们的目标是最大化目标函数(6.1)。

一旦找到最大值,策略*π[θ]*将产生回报最高的轨迹。从直观上看,策略梯度通过增加好策略的概率并减少差策略的概率来激励好的策略。

使用方程(6.1),目标函数的梯度定义如下:

通过与前几章的概念相关联,在策略梯度方法中,策略评估是回报的估计,。而策略改进则是参数的优化步骤。因此,策略梯度方法必须协同进行这两个阶段,以改进策略。

策略梯度定理

在查看方程(6.2)时遇到一个初始问题,因为在其公式中,目标函数的梯度依赖于策略的状态分布;即:

我们会使用该期望的随机逼近方法,但为了计算状态的分布,,我们仍然需要一个完整的环境模型。因此,这种公式不适用于我们的目的。

策略梯度定理在这里提供了解决方案。它的目的是提供一个分析公式,用来计算目标函数相对于策略参数的梯度,而无需涉及状态分布的导数。形式上,策略梯度定理使我们能够将目标函数的梯度表示为:

策略梯度定理的证明超出了本书的范围,因此未包含。然而,你可以在 Sutton 和 Barto 的书中找到相关内容 (incompleteideas.net/book/the-book-2nd.htmlor),或者通过其他在线资源查找。

现在目标函数的导数不涉及状态分布的导数,可以通过从策略中采样来估计期望值。因此,目标函数的导数可以近似如下:

这可以用来通过梯度上升法生成一个随机更新:

请注意,由于目标是最大化目标函数,因此使用梯度上升来将参数朝梯度的方向移动(与梯度下降相反,梯度下降执行)。

方程 (6.5) 背后的思想是增加未来重新提出好动作的概率,同时减少坏动作的概率。动作的质量由通常的标量值传递,这给出了状态-动作对的质量。

计算梯度

只要策略是可微的,它的梯度就可以很容易地计算,借助现代自动微分软件。

在 TensorFlow 中,我们可以定义计算图并调用 tf.gradient(loss_function,variables) 来计算损失函数(loss_function)相对于variables可训练参数的梯度。另一种方法是直接使用随机梯度下降优化器最大化objective函数,例如,调用 tf.train.AdamOptimizer(lr).minimize(-objective_function)

以下代码片段是计算公式 (6.5) 中近似值所需步骤的示例,使用的是env.action_space.n维度的离散动作空间策略:

pi = policy(states) # actions probability for each action
onehot_action = tf.one_hot(actions, depth=env.action_space.n) 
pi_log = tf.reduce_sum(onehot_action * tf.math.log(pi), axis=1)

pi_loss = -tf.reduce_mean(pi_log * Q_function(states, actions))

# calculate the gradients of pi_loss with respect to the variables
gradients = tf.gradient(pi_loss, variables)

# or optimize directly pi_loss with Adam (or any other SGD optimizer)
# pi_opt = tf.train.AdamOptimizer(lr).minimize(pi_loss) #

tf.one_hot生成actions动作的独热编码。也就是说,它生成一个掩码,其中 1 对应动作的数值,其他位置为 0

然后,在代码的第三行中,掩码与动作概率的对数相乘,以获得actions动作的对数概率。第四行按如下方式计算损失:

最后,tf.gradient计算pi_loss的梯度,关于variables参数,如公式(6.5)所示。

策略

如果动作是离散且数量有限,最常见的方法是创建一个参数化策略,为每个动作生成一个数值。

请注意,与深度 Q 网络(Deep Q-Network)算法不同,这里策略的输出值不是 Q(s,a) 动作值。

然后,每个输出值会被转换成概率。此操作是通过 softmax 函数执行的,函数如下所示:

softmax 值被归一化以使其总和为 1,从而产生一个概率分布,其中每个值对应于选择给定动作的概率。

接下来的两个图表展示了在应用 softmax 函数之前(左侧的图)和之后(右侧的图)的五个动作值预测示例。实际上,从右侧的图中可以看到,经过 softmax 计算后,新值的总和为 1,并且它们的值都大于零:

右侧的图表表示,动作 0、1、2、3 和 4 将分别以 0.64、0.02、0.09、0.21 和 0.02 的概率被选择。

为了在由参数化策略返回的动作值上使用 softmax 分布,我们可以使用计算梯度部分给出的代码,只需做一个改动,以下代码片段中已做突出显示:

pi = policy(states) # actions probability for each action
onehot_action = tf.one_hot(actions, depth=env.action_space.n) 

pi_log = tf.reduce_sum(onehot_action * tf.nn.log_softmax(pi), axis=1) # instead of tf.math.log(pi)

pi_loss = -tf.reduce_mean(pi_log * Q_function(states, actions))
gradients = tf.gradient(pi_loss, variables)

在这里,我们使用了tf.nn.log_softmax,因为它比先调用tf.nn.softmax,再调用tf.math.log更稳定。

按照随机分布选择动作的一个优势在于动作选择的内在随机性,这使得环境的动态探索成为可能。这看起来像是一个副作用,但拥有能够自主调整探索程度的策略非常重要。

在 DQN 的情况下,我们不得不用手工调整的!变量来调整整个训练过程中的探索,使用线性!衰减。现在,探索已经内建到策略中,我们最多只需在损失函数中添加一个项(熵),以此来激励探索。

策略性 PG

策略梯度算法的一个非常重要的方面是它们是策略性算法。它们的策略性特征来自公式(6.4),因为它依赖于当前的策略。因此,与 DQN 等非策略性算法不同,策略性方法不允许重用旧的经验。

这意味着,一旦策略发生变化,所有使用给定策略收集的经验都必须被丢弃。作为副作用,策略梯度算法的样本效率较低,这意味着它们需要获取更多的经验才能达到与非策略性算法相同的表现。此外,它们通常会稍微泛化得较差。

理解 REINFORCE 算法

策略梯度算法的核心已经介绍过了,但我们还有一个重要的概念需要解释。我们还需要看一下如何计算动作值。

我们已经在公式(6.4)中看到了:

我们能够通过直接从经验中采样来估计目标函数的梯度,该经验是通过遵循**![]*策略收集的。

唯一涉及的两个项是![]的值和策略对数的导数,这可以通过现代深度学习框架(如 TensorFlow 和 PyTorch)获得。虽然我们已经定义了![],但我们尚未解释如何估计动作值函数。

首次由 Williams 在 REINFORCE 算法中提出的更简单方法是使用蒙特卡洛MC)回报来估计回报。因此,REINFORCE 被认为是一个 MC 算法。如果你还记得,MC 回报是通过给定策略运行的采样轨迹的回报值。因此,我们可以重写方程(6.4),将动作值函数![]替换为 MC 回报![]:

回报是通过完整的轨迹计算得出的,这意味着 PG 更新只有在完成步骤后才可用,其中是轨迹中的总步骤数。另一个后果是,MC 回报仅在情节性问题中定义良好,在这种问题中,最大步骤数有一个上限(这与我们之前学习的其他 MC 算法得出的结论相同)。

更实际一点,时间点的折扣回报,也可以称为未来回报,因为它只使用未来的回报,如下所示:

这可以递归地重写如下:

该函数可以按相反的顺序实现,从最后一个回报开始,如下所示:

def discounted_rewards(rews, gamma):
    rtg = np.zeros_like(rews, dtype=np.float32)
    rtg[-1] = rews[-1]
    for i in reversed(range(len(rews)-1)):
        rtg[i] = rews[i] + gamma*rtg[i+1]
    return rtg

在这里,首先创建一个 NumPy 数组,并将最后一个回报的值分配给rtg变量。之所以这样做,是因为在时间点。然后,算法使用后续值反向计算rtg[i]

REINFORCE 算法的主要循环包括运行几个周期,直到收集到足够的经验,并优化策略参数。为了有效,算法必须在执行更新步骤之前完成至少一个周期(它需要至少一个完整轨迹来计算回报函数())。REINFORCE 的伪代码总结如下:

Initialize  with random weight

for episode 1..M do
    Initialize environment 
    Initialize empty buffer

    *> Generate a few episodes*
    for step 1..MaxSteps do
        *> Collect experience by acting on the environment*

        if :

       *     > Compute the reward to go* 
             # for each t
            *> Store the episode in the buffer*
             # where  is the length of the episode
    *> REINFORCE update step using all the experience in ![](https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/ac2413e0e89d47148131a65a0173a4e6~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1770718756&x-signature=PdncfyayK2%2FoINppoIdodDeMEaw%3D) following formula (6.5)
*    

实现 REINFORCE

现在是时候实现 REINFORCE 了。在这里,我们仅提供算法的实现,而不包括调试和监控过程。完整实现可以在 GitHub 仓库中找到。所以,务必检查一下。

代码分为三个主要函数和一个类:

  • REINFORCE(env_name, hidden_sizes, lr, num_epochs, gamma, steps_per_epoch):这是包含算法主要实现的函数。

  • Buffer:这是一个类,用于临时存储轨迹。

  • mlp(x, hidden_layer, output_size, activation, last_activation):这是用来在 TensorFlow 中构建多层感知器的。

  • discounted_rewards(rews, gamma):该函数计算折扣奖励。

我们首先将查看主要的 REINFORCE 函数,然后实现补充的函数和类。

REINFORCE 函数分为两个主要部分。在第一部分,创建计算图;而在第二部分,运行环境并循环优化策略,直到满足收敛标准。

REINFORCE 函数以 env_name 环境的名称作为输入参数,包含隐藏层大小的列表—hidden_sizes,学习率—lr,训练周期数—num_epochs,折扣因子—gamma,以及每个周期的最小步骤数—steps_per_epoch。正式地,REINFORCE 的函数头如下:

def REINFORCE(env_name, hidden_sizes=[32], lr=5e-3, num_epochs=50, gamma=0.99, steps_per_epoch=100):

REINFORCE(..) 开始时,TensorFlow 默认图被重置,环境被创建,占位符被初始化,策略被创建。策略是一个全连接的多层感知器,每个动作对应一个输出,且每一层的激活函数为 tanh。多层感知器的输出是未归一化的动作值,称为 logits。所有这些操作都在以下代码片段中完成:

def REINFORCE(env_name, hidden_sizes=[32], lr=5e-3, num_epochs=50, gamma=0.99, steps_per_epoch=100):

    tf.reset_default_graph()

    env = gym.make(env_name) 
    obs_dim = env.observation_space.shape
    act_dim = env.action_space.n 

    obs_ph = tf.placeholder(shape=(None, obs_dim[0]), dtype=tf.float32, name='obs')
    act_ph = tf.placeholder(shape=(None,), dtype=tf.int32, name='act')
    ret_ph = tf.placeholder(shape=(None,), dtype=tf.float32, name='ret')

    p_logits = mlp(obs_ph, hidden_sizes, act_dim, activation=tf.tanh)

接着,我们可以创建一个操作,用来计算损失函数,并优化策略。代码与我们之前在 The policy 部分看到的代码类似。唯一的不同是现在通过 tf.random.multinomial 来采样动作,该函数根据策略返回的动作分布来选择动作。此函数从类别分布中抽取样本。在我们的例子中,它选择一个单一的动作(根据环境,也可能选择多个动作)。

以下代码片段是 REINFORCE 更新的实现:

 act_multn = tf.squeeze(tf.random.multinomial(p_logits, 1))
 actions_mask = tf.one_hot(act_ph, depth=act_dim)
 p_log = tf.reduce_sum(actions_mask * tf.nn.log_softmax(p_logits), axis=1)
 p_loss = -tf.reduce_mean(p_log*ret_ph)
 p_opt = tf.train.AdamOptimizer(lr).minimize(p_loss)

在与环境交互过程中,创建一个针对选择的动作的掩码,并与 log_softmax 相乘,以便计算 。然后,计算完整的损失函数。注意—在 tf.reduce_sum 前面有一个负号。我们关注的是目标函数的最大化。但因为优化器需要一个最小化的函数,所以我们必须传递一个损失函数。最后一行使用 AdamOptimizer 优化 PG 损失函数。

我们现在准备开始一个会话,重置计算图的全局变量,并初始化一些稍后会用到的变量:

    sess = tf.Session()
    sess.run(tf.global_variables_initializer())
    step_count = 0
    train_rewards = []
    train_ep_len = []

然后,我们创建两个内部循环,这些循环将与环境交互以收集经验并优化策略,并打印一些统计数据:

    for ep in range(num_epochs):
        obs = env.reset()
        buffer = Buffer(gamma)
        env_buf = []
        ep_rews = []

        while len(buffer) < steps_per_epoch:

            # run the policy 
            act = sess.run(act_multn, feed_dict={obs_ph:[obs]})
            # take a step in the environment
            obs2, rew, done, _ = env.step(np.squeeze(act))

            env_buf.append([obs.copy(), rew, act])
            obs = obs2.copy()
            step_count += 1
            ep_rews.append(rew)

            if done:
                # add the full trajectory to the environment
                buffer.store(np.array(env_buf))
                env_buf = []
                train_rewards.append(np.sum(ep_rews))
                train_ep_len.append(len(ep_rews))
                obs = env.reset()
                ep_rews = []

        obs_batch, act_batch, ret_batch = buffer.get_batch()
        # Policy optimization
        sess.run(p_opt, feed_dict={obs_ph:obs_batch, act_ph:act_batch, ret_ph:ret_batch})

        # Print some statistics
        if ep % 10 == 0:
            print('Ep:%d MnRew:%.2f MxRew:%.1f EpLen:%.1f Buffer:%d -- Step:%d --' % (ep, np.mean(train_rewards), np.max(train_rewards), np.mean(train_ep_len), len(buffer), step_count))
            train_rewards = []
            train_ep_len = []
    env.close()

这两个循环遵循通常的流程,唯一的例外是,当轨迹结束时,与环境的交互会停止,并且临时缓冲区有足够的转移。

现在我们可以实现一个Buffer类,用于包含轨迹数据:

class Buffer():
    def __init__(self, gamma=0.99):
        self.gamma = gamma
        self.obs = []
        self.act = []
        self.ret = []

    def store(self, temp_traj):
        if len(temp_traj) > 0:
            self.obs.extend(temp_traj[:,0])
            ret = discounted_rewards(temp_traj[:,1], self.gamma)
            self.ret.extend(ret)
            self.act.extend(temp_traj[:,2])

    def get_batch(self):
        return self.obs, self.act, self.ret

    def __len__(self):
        assert(len(self.obs) == len(self.act) == len(self.ret))
        return len(self.obs)

最后,我们可以实现一个函数,创建一个具有任意数量隐藏层的神经网络:

def mlp(x, hidden_layers, output_size, activation=tf.nn.relu, last_activation=None):
for l in hidden_layers:
    x = tf.layers.dense(x, units=l, activation=activation)
    return tf.layers.dense(x, units=output_size, activation=last_activation)

这里,activation是应用于隐藏层的非线性函数,而last_activation是应用于输出层的非线性函数。

使用 REINFORCE 着陆航天器

算法已经完成,但最有趣的部分还没有解释。在本节中,我们将应用 REINFORCE 到LunarLander-v2,这是一个周期性的 Gym 环境,目标是让月球着陆器着陆。

以下是游戏初始位置的截图,以及一个假设的成功最终位置:

这是一个离散问题,着陆器必须在坐标(0,0)处着陆,如果远离该点则会受到惩罚。着陆器从屏幕顶部移动到底部时会获得正奖励,但当它开启引擎减速时,每一帧会损失 0.3 分。

此外,根据着陆条件,它会获得额外的-100 或+100 分。游戏被认为在获得 200 分时解决。每局游戏最多进行 1,000 步。

出于这个原因,我们将至少收集 1,000 步的经验,以确保至少完成了一个完整的回合(这个值由steps_per_epoch超参数设置)。

通过调用带有以下超参数的函数来运行 REINFORCE:

REINFORCE('LunarLander-v2', hidden_sizes=[64], lr=8e-3, gamma=0.99, num_epochs=1000, steps_per_epoch=1000)

分析结果

在整个学习过程中,我们监控了许多参数,包括p_loss(策略的损失)、old_p_loss(优化阶段前的策略损失)、总奖励和每局的长度,以便更好地理解算法,并合理调整超参数。我们还总结了一些直方图。要了解更多关于 TensorBoard 汇总的内容,请查看书籍仓库中的代码!

在下图中,我们绘制了训练过程中获得的完整轨迹的总奖励的均值:

从这张图中,我们可以看到,它在大约 500,000 步时达到了 200 的平均分数,或者稍微低一点;因此,在能够掌握游戏之前,约需要 1,000 个完整的轨迹。

在绘制训练性能图时,请记住,算法可能仍在探索中。要检查是否如此,可以监控动作的熵。如果熵大于 0,意味着算法对于所选动作不确定,并且它会继续探索——选择其他动作,并遵循它们的分布。在这种情况下,经过 500,000 步后,智能体仍在探索环境,如下图所示:

带基线的 REINFORCE

REINFORCE 具有一个很好的特性,即由于 MC 回报,它是无偏的,这提供了完整轨迹的真实回报。然而,无偏估计会以方差为代价,方差随着轨迹的长度增加而增大。为什么?这种效应是由于策略的随机性。通过执行完整的轨迹,你会知道它的真实奖励。然而,分配给每个状态-动作对的值可能并不正确,因为策略是随机的,重新执行可能会导致不同的状态,从而产生不同的奖励。此外,你会看到,轨迹中动作的数量越多,系统中引入的随机性就越大,因此,最终会得到更高的方差。

幸运的是,可以在回报估计中引入基线,,从而减少方差,并提高算法的稳定性和性能。采用这种策略的算法称为带基线的 REINFORCE,其目标函数的梯度如下所示:

引入基线的这个技巧之所以可行,是因为梯度估计器在偏差上仍然保持不变:

与此同时,为了使这个方程成立,基线必须对动作保持常数。

我们现在的任务是找到一个合适的基线。最简单的方法是减去平均回报。

如果你想在 REINFORCE 代码中实现这一点,唯一需要更改的是Buffer类中的get_batch()函数:

    def get_batch(self):
        b_ret = self.ret - np.mean(self.ret)
        return self.obs, self.act, b_ret

尽管这个基线减少了方差,但它并不是最佳策略。因为基线可以根据状态进行条件化,一个更好的想法是使用值函数的估计:

请记住,值函数平均值是通过策略获得的回报。

这种变体给系统带来了更多复杂性,因为我们必须设计一个值函数的近似,但它是非常常见的,并且能显著提高算法的性能。

为了学习!,最佳的解决方案是用 MC 估计拟合一个神经网络:

在前面的方程中,是需要学习的神经网络参数。

为了不使符号过于复杂,从现在开始,我们将省略指定策略的部分,因此!将变为!

神经网络在与学习相关的相同轨迹数据上进行训练!,无需与环境进行额外的交互。计算后,MC 估计(例如,使用discounted_rewards(rews, gamma))将成为!目标值,并且神经网络将被优化,以最小化均方误差(MSE)损失——就像你在监督学习任务中做的那样:

这里,是价值函数神经网络的权重,每个数据集元素包含!状态,以及目标值!

实现带基准的 REINFORCE

基准用神经网络逼近的价值函数可以通过在我们之前的代码中添加几行来实现:

  1. 将神经网络、计算 MSE 损失函数的操作和优化过程添加到计算图中:
    ...
    # placeholder that will contain the reward to go values (i.e. the y values)
    rtg_ph = tf.placeholder(shape=(None,), dtype=tf.float32, name='rtg')

    # MLP value function
    s_values = tf.squeeze(mlp(obs_ph, hidden_sizes, 1, activation=tf.tanh))

    # MSE loss function
    v_loss = tf.reduce_mean((rtg_ph - s_values)**2)

    # value function optimization
    v_opt = tf.train.AdamOptimizer(vf_lr).minimize(v_loss)
    ...
  1. 运行s_values,并存储!预测值,因为稍后我们需要计算!。此操作可以在最内层的循环中完成(与 REINFORCE 代码的不同之处用粗体显示):
            ...
            # besides act_multn, run also s_values
            act, val = sess.run([act_multn, s_values], feed_dict={obs_ph:[obs]})
            obs2, rew, done, _ = env.step(np.squeeze(act))

            # add the new transition, included the state value predictions
            env_buf.append([obs.copy(), rew, act, np.squeeze(val)])
            ...

  1. 检索rtg_batch,它包含来自缓冲区的“目标”值,并优化价值函数:
        obs_batch, act_batch, ret_batch, rtg_batch = buffer.get_batch() 
        sess.run([p_opt, v_opt], feed_dict={obs_ph:obs_batch, act_ph:act_batch, ret_ph:ret_batch, rtg_ph:rtg_batch})
  1. 计算奖励目标()和目标值!。此更改在Buffer类中完成。我们需要在该类的初始化方法中创建一个新的空self.rtg列表,并修改storeget_batch函数,具体如下:
    def store(self, temp_traj):
        if len(temp_traj) > 0:
            self.obs.extend(temp_traj[:,0])
            rtg = discounted_rewards(temp_traj[:,1], self.gamma)
            # ret = G - V
            self.ret.extend(rtg - temp_traj[:,3])
            self.rtg.extend(rtg)
            self.act.extend(temp_traj[:,2])

    def get_batch(self):
        return self.obs, self.act, self.ret, self.rtg

你现在可以在任何你想要的环境中测试带基准的 REINFORCE 算法,并将其性能与基本的 REINFORCE 实现进行比较。

学习 AC 算法

简单的 REINFORCE 具有不偏性的显著特点,但它表现出较高的方差。添加基准可以减少方差,同时保持不偏(从渐近的角度来看,算法将收敛到局部最小值)。带基准的 REINFORCE 的一个主要缺点是它收敛得非常慢,需要与环境进行一致的交互。

加速训练的一种方法叫做引导法(bootstrapping)。这是我们在本书中已经多次看到的技巧。它允许从后续的状态值估算回报值。使用这种技巧的策略梯度算法称为演员-评论者(AC)。在 AC 算法中,演员是策略,评论者是价值函数(通常是状态值函数),它对演员的行为进行“批评”,帮助他更快学习。AC 方法的优点有很多,但最重要的是它们能够在非阶段性问题中学习。

无法使用 REINFORCE 解决连续任务,因为要计算奖赏到达目标,它们需要直到轨迹结束的所有奖赏(如果轨迹是无限的,就没有结束)。依靠引导技术,AC 方法也能够从不完整的轨迹中学习动作值。

使用评论者帮助演员学习

使用一步引导法的动作值函数定义如下:

在这里, 是臭名昭著的下一个状态。

因此,使用一个 角色和一个 评论者使用引导法(bootstrapping),我们可以得到一步的 AC 步骤:

这将用一个基准替代 REINFORCE 步骤:

注意 REINFORCE 和 AC 中使用状态值函数的区别。在前者中,它仅作为基准,用来提供当前状态的状态值。在后者示例中,状态值函数用于估算下一个状态的价值,从而只需要当前的奖励来估算 。因此,我们可以说,一步 AC 模型是一个完全在线的增量算法。

n 步 AC 模型

实际上,正如我们在 TD 学习中所看到的,完全在线的算法具有低方差但高偏差,这与 MC 学习相反。然而,通常,介于完全在线和 MC 方法之间的中间策略是首选。为了平衡这种权衡,n 步回报可以替代在线算法中的一步回报。

如果你还记得,我们已经在 DQN 算法中实现了 n 步学习。唯一的区别是 DQN 是一个脱离策略的算法,而理论上,n 步学习只能在在线策略算法中使用。然而,我们展示了通过一个小的 ,性能有所提高。

AC 算法是基于策略的,因此,就性能提升而言,可以使用任意大的  值。在 AC 中集成 n 步是相当直接的;一步返回被  替代,值函数被带入  状态:

这里,  。请注意,如果  是一个最终状态, 

除了减少偏差外,n 步返回还可以更快地传播后续的回报,从而使得学习更加高效。

有趣的是,  量可以看作是优势函数的估计。事实上,优势函数定义如下:

由于  是  的估计,我们得到优势函数的估计。通常,这个函数更容易学习,因为它仅表示在特定状态下一个特定动作相对于其他动作的偏好。它不需要学习该状态的值。

关于评论员权重的优化,它使用一种著名的 SGD 优化方法来进行优化,最小化 MSE 损失:

在前面的方程中,目标值是按如下方式计算的: 

AC 实现

总体而言,正如我们到目前为止所看到的,AC 算法与 REINFORCE 算法非常相似,状态函数作为基准。但为了回顾一下,算法总结如下:

Initialize  with random weight
Initialize environment 
for episode 1..M do
    Initialize empty buffer

    *> Generate a few episodes*
    for step 1..MaxSteps do
        *> Collect experience by acting on the environment*

        if :

            *> Compute the n-step reward to go* 
             # for each t
            *> Compute the advantage values*
             # for each t
            *> Store the episode in the buffer*
             # where  is the lenght of the episode
    *> Actor update step using all the experience in ![](https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/c1ec839f61d944feb081dcc6ed985505~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1770718756&x-signature=wLDEgkCfPFRK%2FLOEulyrv9dzlJo%3D)
*    
    *> Critic update using all the experience in **D***

与 REINFORCE 的唯一区别是 n 步奖励的计算、优势函数的计算,以及主函数的一些调整。

让我们首先看一下折扣奖励的新实现。与之前不同,最后一个last_sv状态的估计值现在传递给输入,并用于引导,如以下实现所示:

def discounted_rewards(rews, last_sv, gamma):
    rtg = np.zeros_like(rews, dtype=np.float32)
    rtg[-1] = rews[-1] + gamma*last_sv    # Bootstrap with the estimate next state value 

    for i in reversed(range(len(rews)-1)):
        rtg[i] = rews[i] + gamma*rtg[i+1]
    return rtg

计算图不会改变,但在主循环中,我们需要注意一些小的但非常重要的变化。

显然,函数的名称已更改为AC,并且cr_lr评论员的学习率作为一个参数被添加进来。

第一个实际的变化涉及环境重置的方式。如果在 REINFORCE 中,偏好在每次主循环迭代时重置环境,那么在 AC 中,我们必须从上一轮迭代的环境状态继续,只有在环境达到最终状态时才重置它。

第二个变化涉及行动价值函数的引导方式,以及如何计算未来的回报。记住,对于每个状态-动作对,除非 是最终状态,否则 。在这种情况下,。因此,我们必须在最后状态时使用0进行引导,并在其他情况下使用 进行引导。根据这些更改,代码如下:

    obs = env.reset()
    ep_rews = []

    for ep in range(num_epochs):
        buffer = Buffer(gamma)
        env_buf = []

        for _ in range(steps_per_env):
            act, val = sess.run([act_multn, s_values], feed_dict={obs_ph:[obs]})
            obs2, rew, done, _ = env.step(np.squeeze(act))

            env_buf.append([obs.copy(), rew, act, np.squeeze(val)])
            obs = obs2.copy()
            step_count += 1
            last_test_step += 1
            ep_rews.append(rew)

            if done:

                buffer.store(np.array(env_buf), 0)
                env_buf = []

                train_rewards.append(np.sum(ep_rews))
                train_ep_len.append(len(ep_rews))
                obs = env.reset()
                ep_rews = []

        if len(env_buf) > 0:
            last_sv = sess.run(s_values, feed_dict={obs_ph:[obs]})
            buffer.store(np.array(env_buf), last_sv)

        obs_batch, act_batch, ret_batch, rtg_batch = buffer.get_batch()
        sess.run([p_opt, v_opt], feed_dict={obs_ph:obs_batch, act_ph:act_batch, ret_ph:ret_batch,         rtg_ph:rtg_batch})
        ...

第三个变化发生在Buffer类的store方法中。实际上,现在我们还需要处理不完整的轨迹。在之前的代码片段中,我们看到估计的 状态值作为第三个参数传递给store函数。事实上,我们使用这些状态值进行引导,并计算"未来回报"。在新版本的store中,我们将与状态值相关的变量命名为last_sv,并将其作为输入传递给discounted_reward函数,代码如下:

    def store(self, temp_traj, last_sv):
        if len(temp_traj) > 0:
            self.obs.extend(temp_traj[:,0])
            rtg = discounted_rewards(temp_traj[:,1], last_sv, self.gamma)
            self.ret.extend(rtg - temp_traj[:,3])
            self.rtg.extend(rtg)
            self.act.extend(temp_traj[:,2])

使用 AC 着陆航天器

我们将 AC 应用于 LunarLander-v2,这与测试 REINFORCE 时使用的环境相同。这是一个回合制的游戏,因此它并没有完全强调 AC 算法的主要特性。尽管如此,它仍提供了一个很好的测试平台,你也可以自由地在其他环境中进行测试。

我们调用AC函数时使用以下超参数:

AC('LunarLander-v2', hidden_sizes=[64], ac_lr=4e-3, cr_lr=1.5e-2, gamma=0.99, steps_per_epoch=100, num_epochs=8000)

结果图显示了训练周期中累计的总回报,图如下:

你可以看到,AC 比 REINFORCE 更快,如下图所示。然而,AC 的稳定性较差,经过大约 200,000 步后,性能有所下降,但幸运的是,之后它继续增长:

在这个配置中,AC 算法每 100 步更新一次演员和评论员。从理论上讲,你可以使用更小的steps_per_epochs,但通常这会让训练变得更不稳定。使用更长的周期可以稳定训练,但演员学习速度较慢。一切都在于找到一个好的平衡点和适合的学习率。

对于本章提到的所有颜色参考,请参见以下链接中的彩色图像包:www.packtpub.com/sites/default/files/downloads/9781789131116_ColorImages.pdf

高级 AC,以及技巧和窍门

AC 算法有许多进一步的进展,还有许多技巧和窍门需要记住,在设计此类算法时要加以注意:

  • 架构设计:在我们的实现中,我们实现了两个不同的神经网络,一个用于评估者,另一个用于演员。也可以设计一个共享主要隐藏层的神经网络,同时保持头部的独立性。这种架构可能更难调整,但总体而言,它提高了算法的效率。

  • 并行环境:减少方差的一个广泛采用的技术是从多个环境中并行收集经验。A3C异步优势演员-评估者)算法异步更新全局参数。而它的同步版本,称为 A2C优势演员-评估者),则在更新全局参数之前等待所有并行的演员完成。智能体的并行化确保了来自环境不同部分的更多独立经验。

  • 批量大小:与其他强化学习算法(尤其是脱离策略算法)相比,政策梯度和 AC 方法需要较大的批量。因此,如果在调整其他超参数后,算法仍然无法稳定,考虑使用更大的批量大小。

  • 学习率:调整学习率本身非常棘手,因此请确保使用更先进的 SGD 优化方法,如 Adam 或 RMSprop。

总结

在本章中,我们学习了一类新的强化学习算法,称为政策梯度。与之前章节中研究的价值函数方法相比,这些算法以不同的方式解决强化学习问题。

PG 方法的简化版本叫做 REINFORCE,这一方法在本章过程中进行了学习、实现和测试。随后,我们提出在 REINFORCE 中加入基准值,以减少方差并提高算法的收敛性。AC 算法不需要使用评估者的完整轨迹,因此我们用 AC 模型解决了同样的问题。

在掌握经典的政策梯度算法的基础上,我们可以进一步深入。在下一章,我们将介绍一些更复杂、前沿的政策梯度算法;即,信任区域策略优化TRPO)和近端策略优化PPO)。这两种算法是基于我们在本章中学习的内容构建的,但它们提出了一个新的目标函数,旨在提高 PG 算法的稳定性和效率。

问题

  1. PG 算法如何最大化目标函数?

  2. 政策梯度算法的核心思想是什么?

  3. 为什么在 REINFORCE 中引入基准值后,算法仍保持无偏?

  4. REINFORCE 属于更广泛的哪类算法?

  5. AC 方法中的评估者与 REINFORCE 中作为基准的价值函数有什么不同?

  6. 如果你需要为一个必须学习移动的智能体开发算法,你会选择 REINFORCE 还是 AC?

  7. 你能将 n 步 AC 算法用作 REINFORCE 算法吗?

进一步阅读

要了解异步版本的演员-评论员算法,请阅读 arxiv.org/pdf/1602.01783.pdf

第七章:TRPO 和 PPO 实现

在上一章中,我们研究了策略梯度算法。它们的独特之处在于解决 强化学习RL)问题的顺序——策略梯度算法朝着奖励增益最大化的方向迈出一步。该算法的简化版本(REINFORCE)具有直接的实现,并且单独使用时能够取得不错的效果。然而,它的速度较慢,且方差较大。因此,我们引入了一个值函数,具有双重目标——批评演员并提供基准。尽管这些演员-评论家算法具有巨大的潜力,但它们可能会受到动作分布中不希望出现的剧烈波动的影响,从而导致访问的状态发生急剧变化,随之而来的是性能的迅速下降,且这种下降可能永远无法恢复。

本章将通过展示如何引入信任区域或剪切目标来解决这一问题,从而减轻该问题的影响。我们将展示两个实际的算法,即 TRPO 和 PPO。这些算法已经证明能在控制模拟行走、控制跳跃和游泳机器人以及玩 Atari 游戏方面取得良好效果。我们将介绍一组新的连续控制环境,并展示如何将策略梯度算法适配到连续动作空间中。通过将 TRPO 和 PPO 应用于这些新环境,您将能够训练一个智能体进行跑步、跳跃和行走。

本章将涵盖以下主题:

  • Roboschool

  • 自然策略梯度

  • 信任区域策略优化

  • 近端策略优化

Roboschool

到目前为止,我们已经处理了离散控制任务,例如 第五章中的 Atari 游戏,深度 Q 网络,以及 第六章中的 LunarLander,学习随机过程和 PG 优化。为了玩这些游戏,只需要控制少数几个离散动作,即大约两个到五个动作。如我们在 第六章 学习随机过程和 PG 优化 中所学,策略梯度算法可以很容易地适应连续动作。为了展示这些特性,我们将在一组新的环境中部署接下来的几种策略梯度算法,这些环境被称为 Roboschool,目标是控制机器人在不同情境下进行操作。Roboschool 由 OpenAI 开发,使用了我们在前几章中使用的著名的 OpenAI Gym 接口。这些环境基于 Bullet Physics 引擎(一个模拟软体和刚体动力学的物理引擎),与著名的 Mujoco 物理引擎的环境类似。我们选择 Roboschool 是因为它是开源的(而 Mujoco 需要许可证),并且它包含了一些更具挑战性的环境。

具体来说,Roboschool 包含 12 个环境,从简单的 Hopper(RoboschoolHopper,左图)到更复杂的人形机器人(RoboschoolHumanoidFlagrun,右图),后者有 17 个连续动作:

图 7.1. 左侧为 RoboschoolHopper-v1 渲染图,右侧为 RoboschoolHumanoidFlagrun-v1 渲染图

在这些环境中的一些,目标是尽可能快速地奔跑、跳跃或行走,以到达 100 米终点,并且在一个方向上移动。其他环境的目标则是移动在三维场地中,同时需要小心可能的外部因素,如被投掷的物体。该环境集合还包括一个多人 Pong 环境,以及一个互动环境,其中 3D 人形机器人可以自由向各个方向移动,并需要朝着旗帜持续移动。除此之外,还有一个类似的环境,其中机器人被不断投掷立方体以破坏平衡,机器人必须建立更强的控制系统来维持平衡。

环境是完全可观察的,这意味着一个智能体能够完全查看其状态,该状态被编码为一个 Box 类,大小可变,约为 10 到 40。正如我们之前提到的,动作空间是连续的,且它由一个 Box 类表示,大小根据环境不同而有所变化。

控制连续系统

本章将实现的策略梯度算法(如 REINFORCE 和 AC,以及 PPO 和 TRPO)都可以与离散和连续动作空间一起使用。从一种动作类型迁移到另一种非常简单。在连续控制中,不是为每个动作计算一个概率,而是通过概率分布的参数来指定动作。最常见的方法是学习正态高斯分布的参数,这是一个非常重要的分布家族,它由均值! 和标准差! 参数化。下图展示了高斯分布及其参数变化的示例:

图 7.2. 三个不同均值和标准差的高斯分布图

关于本章提到的所有颜色参考,请参阅颜色图像包:www.packtpub.com/sites/default/files/downloads/9781789131116_ColorImages.pdf

例如,表示为参数化函数近似(如深度神经网络)的策略可以预测状态功能中正态分布的均值和标准差。均值可以近似为线性函数,通常,标准差是独立于状态的。在这种情况下,我们将表示参数化均值作为状态的函数,记作,标准差作为固定值,记作。此外,代替直接使用标准差,最好使用标准差的对数值。

总结一下,离散控制的参数化策略可以通过以下代码行定义:

p_logits = mlp(obs_ph, hidden_sizes, act_dim, activation=tf.nn.relu, last_activation=None)

mlp是一个函数,用于构建一个多层感知器(也称为全连接神经网络),隐藏层的大小由hidden_sizes指定,输出为act_dim维度,激活函数由activationlast_activation参数指定。这些将成为连续控制的参数化策略的一部分,并将有以下变化:

p_means = mlp(obs_ph, hidden_sizes, act_dim, activation=tf.tanh, last_activation=None)
log_std = tf.get_variable(name='log_std', initializer=np.zeros(act_dim, dtype=np.float32))

这里,p_meanslog_std

此外,如果所有的动作值都在 0 和 1 之间,最好将最后的激活函数设置为tanh

p_means = mlp(obs_ph, hidden_sizes, act_dim, activation=tf.tanh, last_activation=tf.tanh)

然后,为了从这个高斯分布中采样并获得动作,必须将标准差乘以一个噪声向量,该向量遵循均值为 0、标准差为 1 的正态分布,并加到预测的均值上:

这里,z 是高斯噪声向量,,它的形状与相同。这个操作可以通过一行代码实现:

p_noisy = p_means + tf.random_normal(tf.shape(p_means), 0, 1) * tf.exp(log_std)

由于我们引入了噪声,我们不能确定值仍然位于动作的范围内,因此我们必须以某种方式裁剪p_noisy,确保动作值保持在允许的最小值和最大值之间。裁剪操作在以下代码行中完成:

act_smp = tf.clip_by_value(p_noisy, envs.action_space.low, envs.action_space.high)

最终,日志概率通过以下方式计算:

该公式在gaussian_log_likelihood函数中计算,该函数返回日志概率。因此,我们可以按以下方式检索日志概率:

p_log = gaussian_log_likelihood(act_ph, p_means, log_std)

这里,gaussian_log_likelihood在以下代码段中定义:

def gaussian_log_likelihood(x, mean, log_std):
    log_p = -0.5 * (np.log(2*np.pi) + (x-mean)**2 / (tf.exp(log_std)**2 + 1e-9) + 2*log_std)
    return tf.reduce_sum(log_p, axis=-1)

就是这样。现在,你可以在每个 PG 算法中实现它,并尝试各种具有连续动作空间的环境。正如你可能还记得,在上一章,我们在 LunarLander 上实现了 REINFORCE 和 AC。相同的游戏也提供了连续控制版本,叫做LunarLanderContinuous-v2

拥有解决具有连续动作空间固有问题的必要知识后,你现在能够应对更广泛的任务。然而,一般来说,这些任务也更难解决,我们目前所学的 PG 算法过于弱小,无法很好地解决复杂问题。因此,在接下来的章节中,我们将介绍更先进的 PG 算法,从自然策略梯度开始。

自然策略梯度

REINFORCE 和演员-评论员是非常直观的方法,在中小型 RL 任务中表现良好。然而,它们存在一些问题需要解决,以便我们能调整策略梯度算法,使其适用于更大、更复杂的任务。主要问题如下:

  • 很难选择合适的步长:这个问题源于强化学习的非平稳性特性,意味着数据的分布随着时间的推移不断变化,且随着智能体学习新知识,它会探索不同的状态空间。找到一个总体稳定的学习率非常棘手。

  • 不稳定性:这些算法没有意识到策略会改变的幅度。这也与我们之前提到的问题相关。一次没有控制的更新可能会导致策略发生重大变化,进而剧烈改变动作分布,从而将智能体推向不良的状态空间。此外,如果新状态空间与之前的状态空间差异很大,可能需要很长时间才能恢复。

  • 样本效率低:这个问题几乎所有的在策略算法都会遇到。这里的挑战是,在丢弃策略数据之前,尽可能从中提取更多信息。

本章提出的算法,即 TRPO 和 PPO,尝试通过不同的方式来解决这三个问题,尽管它们有一个共同的背景,稍后将进行解释。此外,TRPO 和 PPO 都是在策略的策略梯度算法,属于无模型家族,如下所示的 RL 分类图:

图 7.3. TRPO 和 PPO 在 RL 算法分类图中的位置

自然策略梯度NPG)是最早提出的解决策略梯度方法不稳定性问题的算法之一。它通过引入策略步长的变化,控制策略的引导方式,从而解决这个问题。不幸的是,它只适用于线性函数逼近,不能应用于深度神经网络。然而,它是更强大算法的基础,如 TRPO 和 PPO。

NPG 背后的直觉

在寻求解决 PG 方法不稳定性的问题之前,让我们先理解它为什么会出现。想象一下,你正在攀登一座陡峭的火山,火山口位于顶部,类似于下图中的函数。我们还假设你唯一的感官是脚下的倾斜度(梯度),并且你看不见周围的世界——你是盲的。我们还假设每一步的步长是固定的(学习率),例如,步长为一米。你迈出了第一步,感知到脚下的倾斜度,并朝着最陡的上升方向移动 1 米。在多次重复这一过程后,你到达了接近火山口的一个点,但由于你是盲人,依然没有意识到这一点。此时,你观察到脚下的倾斜度依旧指向火山口的方向。然而,如果火山的高度仅比你的步长小,那么下一步你将跌落下来。此时,周围的空间对你来说是完全陌生的。在下图所示的情况下,你会很快恢复过来,因为这是一个简单的函数,但通常情况下,它可能复杂得无法预料。作为补救方法,你可以使用更小的步长,但这样你爬山的速度会变得非常慢,并且仍然无法保证能够到达最大值。这个问题不仅仅存在于强化学习(RL)中,但在这里它尤为严重,因为数据并非静态,可能造成的损害比其他领域(如监督学习)更大。让我们看看下图:

图 7.4. 在尝试到达该函数的最大值时,你可能会掉进火山口。

一个可能想到的解决方案,也是 NPG 中提出的解决方案,是在梯度的基础上加入函数的曲率。关于曲率的信息由二阶导数携带。这个信息非常有用,因为高值表示两个点之间的梯度发生了剧烈变化,作为预防,可以采取更小、更谨慎的步伐,从而避免可能的悬崖。通过这种新方法,你可以利用二阶导数来获得更多关于动作分布空间的信息,并确保在剧烈变化的情况下,动作空间的分布不会发生太大变化。在接下来的部分中,我们将看到 NPG 是如何做到这一点的。

一些数学内容

NPG 算法的创新之处在于如何通过结合一阶和二阶导数的步长更新来更新参数。为了理解自然策略梯度步长,我们需要解释两个关键概念:费舍尔信息矩阵FIM)和Kullback-LeiblerKL)散度。但在解释这两个关键概念之前,让我们先看一下更新背后的公式:

 (7.1)

这个更新与传统的策略梯度有所不同,但仅仅通过项 ,它用于增强梯度项。

在这个公式中,  是 FIM,  是目标函数。

正如我们之前提到的,我们希望在分布空间中使得所有步骤的长度相同,无论梯度是什么。这是通过 FIM 的逆来实现的。

FIM 和 KL 散度

FIM 被定义为目标函数的协方差。让我们看看它如何帮助我们。为了限制我们模型的分布之间的距离,我们需要定义一个度量来提供新旧分布之间的距离。最常见的选择是使用 KL 散度。它衡量两个分布之间的差异,并在强化学习(RL)和机器学习中得到广泛应用。KL 散度不是一个真正的度量,因为它不是对称的,但它是一个很好的近似值。两个分布之间的差异越大,KL 散度的值就越高。考虑下图中的曲线。在这个例子中,KL 散度是相对于绿色函数计算的。事实上,由于橙色函数与绿色函数相似,KL 散度为 1.11,接近于 0。相反,很容易看出蓝色和绿色曲线差异较大。这个观察结果得到了它们之间 KL 散度 45.8 的确认。请注意,相同函数之间的 KL 散度始终为 0。

对于有兴趣的读者,离散概率分布的 KL 散度计算公式为 

让我们来看一下以下图示:

图 7.5. 盒子中显示的 KL 散度是测量每个函数与绿色着色函数之间的差异。数值越大,两者之间的差距越大。

因此,利用 KL 散度,我们能够比较两个分布,并获得它们相互关系的指示。那么,我们如何在问题中使用这个度量,并限制两个后续策略分布之间的散度呢?

事实上,FIM 通过使用 KL 散度作为度量,在分布空间中定义了局部曲率。因此,通过将 KL 散度的曲率(二阶导数)与目标函数的梯度(一阶导数)结合(如公式(7.1)中所示),我们可以获得保持 KL 散度距离恒定的方向和步长。因此,根据公式(7.1)得到的更新将在 FIM 较高时更为谨慎(意味着动作分布之间存在较大距离时),沿着最陡的方向小步前进,并在 FIM 较低时采取大步(意味着存在高原且分布变化不大时)。

自然梯度的复杂性

尽管了解自然梯度在 RL 框架中的有用性,其主要缺点之一是涉及计算 FIM 的计算成本。而梯度的计算成本为,自然梯度的计算成本为,其中是参数数量。事实上,在 2003 年的 NPG 论文中,该算法已应用于具有线性策略的非常小的任务。然而,对于具有数十万参数的现代深度神经网络来说,计算的成本太高。尽管如此,通过引入一些近似和技巧,自然梯度也可以用于深度神经网络。

在监督学习中,自然梯度的使用并不像在强化学习中那样必要,因为现代优化器(如 Adam 和 RMSProp)可以以经验方式近似处理二阶梯度。

信任区域策略优化

信任区域策略优化TRPO)是第一个成功利用多种近似方法计算自然梯度的算法,其目标是以更受控且稳定的方式训练深度神经网络策略。从 NPG 中我们看到,对于具有大量参数的非线性函数计算 FIM 的逆是不可能的。TRPO 通过在 NPG 基础上构建,克服了这些困难。它通过引入替代目标函数并进行一系列近似,成功学习复杂策略,例如从原始像素学习步行、跳跃或玩 Atari 游戏。

TRPO 是最复杂的无模型算法之一,虽然我们已经了解了自然梯度的基本原理,但它背后仍然有许多困难的部分。在这一章中,我们只会给出算法的直观细节,并提供主要方程。如果你想深入了解该算法,查阅他们的论文 (arxiv.org/abs/1502.05477),以获得完整的解释和定理证明。

我们还将实现该算法,并将其应用于 Roboschool 环境。然而,我们不会在这里讨论实现的每个组件。有关完整的实现,请查看本书的 GitHub 仓库。

TRPO 算法

从广义的角度来看,TRPO 可以视为 NPG 算法在非线性函数逼近中的延续。TRPO 引入的最大改进是对新旧策略之间的 KL 散度施加约束,形成 信任区域。这使得网络可以在信任区域内采取更大的步伐。由此产生的约束问题表述如下:

(7.2)

这里, 是我们将很快看到的目标代理函数, 是旧策略与 参数之间的 KL 散度,以及新策略之间的 KL 散度。

参数是约束的系数。

目标代理函数的设计方式是,利用旧策略的状态分布最大化新的策略参数。这个过程通过重要性采样来完成,重要性采样估计新策略(期望策略)的分布,同时只拥有旧策略(已知分布)的分布。重要性采样是必要的,因为轨迹是根据旧策略采样的,但我们实际关心的是新策略的分布。使用重要性采样,代理目标函数定义为:

(7.3)

是旧策略的优势函数。因此,约束优化问题等价于以下问题:

(7.4)

这里, 表示在状态条件下的动作分布,

我们接下来要做的是,用一批样本的经验平均值来替代期望,并用经验估计替代 

约束问题难以解决,在 TRPO 中,方程(7.4)中的优化问题通过使用目标函数的线性近似和约束的二次近似来近似求解,使得解变得类似于 NPG 更新:

这里,

现在,可以使用共轭梯度CG)方法来求解原优化问题的近似解,这是一种用于求解线性系统的迭代方法。当我们谈到 NPG 时,我们强调计算对于大参数量而言计算非常昂贵。然而,CG 可以在不形成完整矩阵的情况下近似求解线性问题。因此,使用 CG 时,我们可以按如下方式计算

(7.5)

TRPO 还为我们提供了一种估计步长的方法:

(7.6)

因此,更新变为如下:

(7.7)

到目前为止,我们已经创建了自然策略梯度步骤的一个特例,但要完成 TRPO 更新,我们还缺少一个关键成分。记住,我们通过线性目标函数和二次约束的解来逼近问题。因此,我们只是在求解期望回报的局部近似解。引入这些近似后,我们不能确定 KL 散度约束是否仍然满足。为了在改进非线性目标的同时确保非线性约束,TRPO 执行线搜索以找到满足约束的较高值,。带有线搜索的 TRPO 更新变为如下:

(7.8)

线搜索可能看起来是算法中微不足道的一部分,但正如论文中所展示的,它起着至关重要的作用。没有它,算法可能会计算出过大的步长,从而导致性能灾难性的下降。

在 TRPO 算法中,它使用共轭梯度算法计算搜索方向,以寻找逼近目标函数和约束的解。然后,它使用线搜索来找到最大步长,,从而满足 KL 散度的约束并改进目标。为了进一步提高算法的速度,共轭梯度算法还利用了高效的 Fisher-Vector 乘积(想了解更多,可以查看这篇论文:arxiv.org/abs/1502.05477paper)。

TRPO 可以集成到 AC 架构中,其中评论员被包含在算法中,以为策略(演员)在任务学习中提供额外的支持。这样的算法的高级实现(即 TRPO 与评论员结合)用伪代码表示如下:

Initialize  with random weight
Initialize environment 
for episode 1..M do
    Initialize empty buffer

    *> Generate few trajectories*
    for step 1..TimeHorizon do
        *> Collect experience by acting on the environment*

        if :

            *> Store the episode in the buffer*
             # where  is the length of the episode

    Compute the advantage values  and n-step reward to go 

    > Estimate the gradient of the objective function
         (1)
    > Compute  using conjugate gradient
         (2)
    > Compute the step length 
         (3)

    *> Update the policy using all the experience in ![](https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/a16d16be0b0743a7ad4c963db1c25afa~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1770718756&x-signature=fs6Hmlnk3w5Qz1%2FA4wgQGiW3pVU%3D)* 
    Backtracking line search to find the maximum  value that satisfy the constraint

     (4)

    *> Critic update using all the experience in ![](https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/42184d7163de4b8f88dfae6df9c3f9e8~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1770718756&x-signature=%2FYRrhuiiLqF8apcmDdcKjTvOr0k%3D)*

在对 TRPO 进行概述之后,我们终于可以开始实现它了。

TRPO 算法的实现

在 TRPO 算法的实现部分,我们将集中精力在计算图和优化策略所需的步骤上。我们将省略在前面章节中讨论的其他方面的实现(例如从环境中收集轨迹的循环、共轭梯度算法和线搜索算法)。但是,请务必查看本书 GitHub 仓库中的完整代码。该实现用于连续控制。

首先,让我们创建所有的占位符以及策略(演员)和价值函数(评论员)的两个深度神经网络:

act_ph = tf.placeholder(shape=(None,act_dim), dtype=tf.float32, name='act')
obs_ph = tf.placeholder(shape=(None, obs_dim[0]), dtype=tf.float32, name='obs')
ret_ph = tf.placeholder(shape=(None,), dtype=tf.float32, name='ret')
adv_ph = tf.placeholder(shape=(None,), dtype=tf.float32, name='adv')
old_p_log_ph = tf.placeholder(shape=(None,), dtype=tf.float32, name='old_p_log')
old_mu_ph = tf.placeholder(shape=(None, act_dim), dtype=tf.float32, name='old_mu')
old_log_std_ph = tf.placeholder(shape=(act_dim), dtype=tf.float32, name='old_log_std')
p_ph = tf.placeholder(shape=(None,), dtype=tf.float32, name='p_ph')
# result of the conjugate gradient algorithm
cg_ph = tf.placeholder(shape=(None,), dtype=tf.float32, name='cg')

# Actor neural network
with tf.variable_scope('actor_nn'):
    p_means = mlp(obs_ph, hidden_sizes, act_dim, tf.tanh, last_activation=tf.tanh)
    log_std = tf.get_variable(name='log_std', initializer=np.ones(act_dim, dtype=np.float32))

# Critic neural network
with tf.variable_scope('critic_nn'):
    s_values = mlp(obs_ph, hidden_sizes, 1, tf.nn.relu, last_activation=None)
    s_values = tf.squeeze(s_values) 

这里有几点需要注意:

  1. 带有old_前缀的占位符指的是旧策略的张量。

  2. 演员和评论员被定义在两个独立的变量作用域中,因为稍后我们需要分别选择这些参数。

  3. 动作空间是一个高斯分布,具有对角矩阵的协方差矩阵,并且与状态独立。然后,可以将对角矩阵调整为每个动作一个元素的向量。我们还会使用这个向量的对数。

现在,我们可以根据标准差将正常噪声添加到预测的均值中,对动作进行剪切,并计算高斯对数似然,步骤如下:

p_noisy = p_means + tf.random_normal(tf.shape(p_means), 0, 1) * tf.exp(log_std)

a_sampl = tf.clip_by_value(p_noisy, low_action_space, high_action_space)

p_log = gaussian_log_likelihood(act_ph, p_means, log_std)

然后,我们需要计算目标函数!、评论员的 MSE 损失函数,并为评论员创建优化器,步骤如下:

# TRPO loss function
ratio_new_old = tf.exp(p_log - old_p_log_ph)
p_loss = - tf.reduce_mean(ratio_new_old * adv_ph)

# MSE loss function
v_loss = tf.reduce_mean((ret_ph - s_values)**2)

# Critic optimization
v_opt = tf.train.AdamOptimizer(cr_lr).minimize(v_loss)

接下来的步骤涉及为前面伪代码中给出的(2)、(3)和(4)点创建计算图。实际上,(2)和(3)并不在 TensorFlow 中执行,因此它们不属于计算图的一部分。然而,在计算图中,我们必须处理一些相关的内容。具体步骤如下:

  1. 估计策略损失函数的梯度。

  2. 定义一个过程来恢复策略参数。这是必要的,因为在进行线搜索算法时,我们将优化策略并测试约束条件,如果新策略不满足这些条件,我们将必须恢复策略参数并尝试使用更小的系数。

  3. 计算费舍尔向量积。这是一种有效计算而不形成完整的的方法。

  4. 计算 TRPO 步骤。

  5. 更新策略。

从第 1 步开始,也就是估计策略损失函数的梯度:

def variables_in_scope(scope):    
    return tf.get_collection(tf.GraphKeys.TRAINABLE_VARIABLES, scope)

# Gather and flatten the actor parameters
p_variables = variables_in_scope('actor_nn')
p_var_flatten = flatten_list(p_variables)

# Gradient of the policy loss with respect to the actor parameters
p_grads = tf.gradients(p_loss, p_variables)
p_grads_flatten = flatten_list(p_grads)

由于我们使用的是向量参数,因此必须使用flatten_list将其展平。variable_in_scope返回scope中的可训练变量。此函数用于获取演员的变量,因为梯度计算仅需针对这些变量。

关于步骤 2,策略参数是通过这种方式恢复的:

p_old_variables = tf.placeholder(shape=(None,), dtype=tf.float32, name='p_old_variables')

# variable used as index for restoring the actor's parameters
it_v1 = tf.Variable(0, trainable=False)
restore_params = []

for p_v in p_variables:
    upd_rsh = tf.reshape(p_old_variables[it_v1 : it_v1+tf.reduce_prod(p_v.shape)], shape=p_v.shape)
    restore_params.append(p_v.assign(upd_rsh))
    it_v1 += tf.reduce_prod(p_v.shape)

restore_params = tf.group(*restore_params)

它迭代每一层的变量,并将旧变量的值分配给当前变量。

步骤 3 中的 Fisher-向量积通过计算 KL 散度关于策略变量的二阶导数来完成:

# gaussian KL divergence of the two policies 
dkl_diverg = gaussian_DKL(old_mu_ph, old_log_std_ph, p_means, log_std)

# Jacobian of the KL divergence (Needed for the Fisher matrix-vector product)
dkl_diverg_grad = tf.gradients(dkl_diverg, p_variables)
dkl_matrix_product = tf.reduce_sum(flatten_list(dkl_diverg_grad) * p_ph)

# Fisher vector product
Fx = flatten_list(tf.gradients(dkl_matrix_product, p_variables))

步骤 4 和 5 涉及将更新应用到策略中,其中beta_ph,该值通过公式(7.6)计算,alpha是通过线性搜索找到的缩放因子:

# NPG update
beta_ph = tf.placeholder(shape=(), dtype=tf.float32, name='beta')
npg_update = beta_ph * cg_ph
alpha = tf.Variable(1., trainable=False)

# TRPO update
trpo_update = alpha * npg_update

# Apply the updates to the policy
it_v = tf.Variable(0, trainable=False)
p_opt = []
for p_v in p_variables:
    upd_rsh = tf.reshape(trpo_update[it_v : it_v+tf.reduce_prod(p_v.shape)], shape=p_v.shape)
    p_opt.append(p_v.assign_sub(upd_rsh))
    it_v += tf.reduce_prod(p_v.shape)

p_opt = tf.group(*p_opt)

注意,在没有的情况下,更新可以看作是 NPG 更新。

更新应用到策略的每个变量。此工作由p_v.assign_sub(upd_rsh)完成,它将p_v - upd_rsh的值赋给p_v,即:。减法是因为我们将目标函数转换为损失函数。

现在,让我们简要回顾一下每次迭代更新策略时我们所实现的各个部分是如何协同工作的。我们将在此展示的代码片段应在最内层循环中添加,其中采样了轨迹。但在深入代码之前,让我们回顾一下我们需要做什么:

  1. 获取输出、对数概率、标准差和我们用于采样轨迹的策略参数。这一策略是我们的旧策略。

  2. 获取共轭梯度。

  3. 计算步长,

  4. 执行回溯线性搜索以获得

  5. 运行策略更新。

第一点通过运行一些操作来实现:

    ...    
    old_p_log, old_p_means, old_log_std = sess.run([p_log, p_means, log_std], feed_dict={obs_ph:obs_batch, act_ph:act_batch, adv_ph:adv_batch, ret_ph:rtg_batch})
    old_actor_params = sess.run(p_var_flatten)
    old_p_loss = sess.run([p_loss], feed_dict={obs_ph:obs_batch, act_ph:act_batch, adv_ph:adv_batch, ret_ph:rtg_batch, old_p_log_ph:old_p_log})

共轭梯度算法需要一个输入函数,该函数返回估算的 Fisher 信息矩阵、目标函数的梯度和迭代次数(在 TRPO 中,该值介于 5 到 15 之间):

     def H_f(p):
        return sess.run(Fx, feed_dict={old_mu_ph:old_p_means, old_log_std_ph:old_log_std, p_ph:p, obs_ph:obs_batch, act_ph:act_batch, adv_ph:adv_batch, ret_ph:rtg_batch})

    g_f = sess.run(p_grads_flatten, feed_dict={old_mu_ph:old_p_means,obs_ph:obs_batch, act_ph:act_batch, adv_ph:adv_batch, ret_ph:rtg_batch, old_p_log_ph:old_p_log})
    conj_grad = conjugate_gradient(H_f, g_f, iters=conj_iters)

然后我们可以计算步长,beta_np,以及最大系数,

使用回溯线性搜索算法满足约束条件的best_alpha,并通过将所有值输入到计算图中运行优化:

    beta_np = np.sqrt(2*delta / np.sum(conj_grad * H_f(conj_grad)))

    def DKL(alpha_v):
        sess.run(p_opt, feed_dict={beta_ph:beta_np, alpha:alpha_v, cg_ph:conj_grad, obs_ph:obs_batch, act_ph:act_batch, adv_ph:adv_batch, old_p_log_ph:old_p_log})
        a_res = sess.run([dkl_diverg, p_loss], feed_dict={old_mu_ph:old_p_means, old_log_std_ph:old_log_std, obs_ph:obs_batch, act_ph:act_batch, adv_ph:adv_batch, ret_ph:rtg_batch, old_p_log_ph:old_p_log})
        sess.run(restore_params, feed_dict={p_old_variables: old_actor_params})
        return a_res

    best_alpha = backtracking_line_search(DKL, delta, old_p_loss, p=0.8)
    sess.run(p_opt, feed_dict={beta_ph:beta_np, alpha:best_alpha, cg_ph:conj_grad, obs_ph:obs_batch, act_ph:act_batch, adv_ph:adv_batch, old_p_log_ph:old_p_log})

    ...

正如您所见,backtracking_line_search使用一个名为DKL的函数,该函数返回旧策略和新策略之间的 KL 散度,系数(这是约束值),以及旧策略的损失值。backtracking_line_search的操作是从开始逐步减小该值,直到满足以下条件:KL 散度小于,并且新损失函数已减小。

因此,TRPO 特有的超参数如下:

  • delta,(),旧策略和新策略之间的最大 KL 散度。

  • 共轭迭代次数conj_iters的数量。通常情况下,它是一个介于 5 和 15 之间的数字。

恭喜你走到这一步!那真的很难。

TRPO 的应用

TRPO 的效率和稳定性使我们能够在新的更复杂的环境中进行测试。我们在 Roboschool 上应用了 TRPO。Roboschool 及其 Mujoco 对应物通常用作能够控制具有连续动作的复杂代理的算法的测试平台,例如 TRPO。具体来说,我们在 RoboschoolWalker2d 上测试了 TRPO,代理的任务是尽可能快地学会行走。环境如下图所示。当代理器倒下或者自开始以来已经过去超过 1,000 个时间步长时,环境就会终止。状态以大小为 22 的Box类编码,代理器用范围为的 6 个浮点值进行控制:

图 7.6. RoboschoolWalker2d 环境的渲染

在 TRPO 中,每个 episode 从环境中收集的步数称为time horizon。这个数字还将决定批次的大小。此外,运行多个代理器并行可以收集更具代表性的环境数据。在这种情况下,批次大小将等于时间跨度乘以代理数量。尽管我们的实现不倾向于并行运行多个代理器,但使用比每个 episode 允许的最大步数更长的时间跨度可以达到相同的目标。例如,知道在 RoboschoolWalker2d 中,代理器最多可以进行 1,000 个时间步长以达到目标,通过使用 6,000 的时间跨度,我们可以确保至少运行六个完整的轨迹。

我们使用报告中的以下表格中列出的超参数来运行 TRPO。其第三列还显示了每个超参数的标准范围:

超参数用于 RoboschoolWalker2范围
共轭迭代次数10[7-10]
Delta (δ)0.01[0.005-0.03]
批次大小(时间跨度*代理数量)6000[500-20000]

TRPO(以及在下一节中我们将看到的 PPO)的进展可以通过具体观察每个游戏中累计的总奖励以及由评论者预测的状态值来进行监控。

我们训练了 600 万步,性能结果如图所示。在 200 万步时,它能够达到一个不错的分数 1300,并且能够流畅行走,速度适中。在训练的第一阶段,我们可以注意到一个过渡期,分数略微下降,可能是由于局部最优解。之后,智能体恢复并改进,直到达到 1250 分:

图 7.7. TRPO 在 RoboschoolWalker2d 上的学习曲线

此外,预测的状态值提供了一个重要的指标,帮助我们研究结果。通常,它比总奖励更稳定,也更容易分析。以下图所示,确实证明了我们的假设,因为它显示了一个总体上更平滑的函数,尽管在 400 万和 450 万步之间有几个峰值:

图 7.8. TRPO 在 RoboschoolWalker2d 上由评论者预测的状态值

从这个图表中,我们也更容易看到,在前三百万步之后,智能体继续学习,尽管学习速度非常慢。

正如你所看到的,TRPO 是一个相当复杂的算法,涉及许多活动部分。尽管如此,它证明了将策略限制在信任区域内,以防止策略过度偏离当前分布的有效性。

但我们能否设计一个更简单、更通用的算法,使用相同的基本方法?

近端策略优化(Proximal Policy Optimization)

Schulman 等人的工作表明这是可能的。实际上,它采用了类似于 TRPO 的思想,同时减少了方法的复杂性。这种方法称为近端策略优化PPO),其优势在于仅使用一阶优化,而不会降低与 TRPO 相比的可靠性。PPO 也比 TRPO 更通用、样本效率更高,并支持使用小批量进行多次更新。

快速概述

PPO 的核心思想是在目标函数偏离时进行剪切,而不是像 TRPO 那样约束它。这防止了策略进行过大的更新。其主要目标如下:

 (7.9)

这里, 的定义如下:

 (7.10)

目标所表达的是,如果新旧策略之间的概率比,,高于或低于一个常数,,则应取最小值。这可以防止 超出区间 。取 作为参考点,

PPO 算法

在 PPO 论文中介绍的实用算法使用了 广义优势估计GAE)的截断版本,GAE 是在论文 High-Dimensional Continuous Control using Generalized Advantage Estimation 中首次提出的一个概念。GAE 通过以下方式计算优势:

 (7.11)

它这样做是为了替代常见的优势估计器:

 (7.12)

继续讨论 PPO 算法,在每次迭代中,N 条轨迹来自多个并行演员,并且时间跨度为 T,策略更新 K 次,使用小批量。按照这个趋势,评论员也可以使用小批量进行多次更新。下表包含每个 PPO 超参数和系数的标准值。尽管每个问题都需要特定的超参数,但了解它们的范围(见表格的第三列)仍然是有用的:

超参数符号范围
策略学习率-[1e^(-5), 1e^(-3)]
策略迭代次数K[3, 15]
轨迹数量(等同于并行演员数量)N[1, 20]
时间跨度T[64, 5120]
小批量大小-[64, 5120]
裁剪系数0.1 或 0.2
Delta(用于 GAE)δ[0.9, 0.97]
Gamma(用于 GAE)γ[0.8, 0.995]

PPO 的实现

现在我们已经掌握了 PPO 的基本要素,可以使用 Python 和 TensorFlow 来实现它。

PPO 的结构和实现与演员-评论员算法非常相似,但只多了一些附加部分,我们将在这里解释所有这些部分。

其中一个附加部分是广义优势估计(7.11),它只需要几行代码,利用已实现的 discounted_rewards 函数来计算(7.12):

def GAE(rews, v, v_last, gamma=0.99, lam=0.95):
    vs = np.append(v, v_last)
    delta = np.array(rews) + gamma*vs[1:] - vs[:-1]
    gae_advantage = discounted_rewards(delta, 0, gamma*lam)
    return gae_advantage

GAE 函数在 Buffer 类的 store 方法中使用,当存储一条轨迹时:

class Buffer():
    def __init__(self, gamma, lam):
        ...

    def store(self, temp_traj, last_sv):
        if len(temp_traj) > 0:
            self.ob.extend(temp_traj[:,0])
            rtg = discounted_rewards(temp_traj[:,1], last_sv, self.gamma)
            self.adv.extend(GAE(temp_traj[:,1], temp_traj[:,3], last_sv, self.gamma, self.lam))
            self.rtg.extend(rtg)
            self.ac.extend(temp_traj[:,2])

    def get_batch(self):
        return np.array(self.ob), np.array(self.ac), np.array(self.adv), np.array(self.rtg)

    def __len__(self):
        ...

这里的 ... 代表我们没有报告的代码行。

现在我们可以定义裁剪的替代损失函数(7.9):

def clipped_surrogate_obj(new_p, old_p, adv, eps):
    rt = tf.exp(new_p - old_p) # i.e. pi / old_pi
    return -tf.reduce_mean(tf.minimum(rt*adv, tf.clip_by_value(rt, 1-eps, 1+eps)*adv))

这很直观,不需要进一步解释。

计算图没有什么新东西,但我们还是快速过一遍:

# Placeholders
act_ph = tf.placeholder(shape=(None,act_dim), dtype=tf.float32, name='act')
obs_ph = tf.placeholder(shape=(None, obs_dim[0]), dtype=tf.float32, name='obs')
ret_ph = tf.placeholder(shape=(None,), dtype=tf.float32, name='ret')
adv_ph = tf.placeholder(shape=(None,), dtype=tf.float32, name='adv')
old_p_log_ph = tf.placeholder(shape=(None,), dtype=tf.float32, name='old_p_log')

# Actor
with tf.variable_scope('actor_nn'):
    p_means = mlp(obs_ph, hidden_sizes, act_dim, tf.tanh, last_activation=tf.tanh)
    log_std = tf.get_variable(name='log_std', initializer=np.ones(act_dim, dtype=np.float32))
    p_noisy = p_means + tf.random_normal(tf.shape(p_means), 0, 1) * tf.exp(log_std)
    act_smp = tf.clip_by_value(p_noisy, low_action_space, high_action_space)
    # Compute the gaussian log likelihood
    p_log = gaussian_log_likelihood(act_ph, p_means, log_std)

# Critic 
with tf.variable_scope('critic_nn'):
    s_values = tf.squeeze(mlp(obs_ph, hidden_sizes, 1, tf.tanh, last_activation=None))

# PPO loss function
p_loss = clipped_surrogate_obj(p_log, old_p_log_ph, adv_ph, eps)
# MSE loss function
v_loss = tf.reduce_mean((ret_ph - s_values)**2)

# Optimizers
p_opt = tf.train.AdamOptimizer(ac_lr).minimize(p_loss)
v_opt = tf.train.AdamOptimizer(cr_lr).minimize(v_loss)

与环境交互和收集经验的代码与 AC 和 TRPO 相同。然而,在本书 GitHub 仓库中的 PPO 实现中,你可以找到一个简单的实现,使用了多个智能体。

一旦收集到过渡数据 (其中 N 是运行的轨迹数量,T 是每条轨迹的时间跨度),我们就可以更新策略和评价器。在这两种情况下,优化会多次运行,并且在小批量上进行。但在此之前,我们必须在完整的批量上运行p_log,因为裁剪目标需要旧策略的动作对数概率:

        ...    
        obs_batch, act_batch, adv_batch, rtg_batch = buffer.get_batch()     
        old_p_log = sess.run(p_log, feed_dict={obs_ph:obs_batch, act_ph:act_batch, adv_ph:adv_batch, ret_ph:rtg_batch})
        old_p_batch = np.array(old_p_log)
lb = len(buffer)
        lb = len(buffer)
        shuffled_batch = np.arange(lb) 

        # Policy optimization steps
        for _ in range(actor_iter):
            # shuffle the batch on every iteration
            np.random.shuffle(shuffled_batch)

            for idx in range(0,lb, minibatch_size):
                minib = shuffled_batch[idx:min(idx+batch_size,lb)]
                sess.run(p_opt, feed_dict={obs_ph:obs_batch[minib], act_ph:act_batch[minib], adv_ph:adv_batch[minib], old_p_log_ph:old_p_batch[minib]})

        # Value function optimization steps
        for _ in range(critic_iter):
            # shuffle the batch on every iteration
            np.random.shuffle(shuffled_batch)

            for idx in range(0,lb, minibatch_size):
                minib = shuffled_batch[idx:min(idx+minibatch_size,lb)]
                sess.run(v_opt, feed_dict={obs_ph:obs_batch[minib], ret_ph:rtg_batch[minib]})
        ...

在每次优化迭代时,我们会对批量数据进行打乱,以确保每个小批量与其他批量不同。

这就是 PPO 实现的全部内容,但请记住,在每次迭代的前后,我们还会运行总结信息,稍后我们将使用 TensorBoard 来分析结果并调试算法。再次强调,我们这里不展示代码,因为它总是相同的且较长,但你可以在本书的仓库中查看完整代码。如果你想掌握这些强化学习算法,理解每个图表展示的内容是至关重要的。

PPO 应用

PPO 和 TRPO 是非常相似的算法,我们选择通过在与 TRPO 相同的环境中测试 PPO 来进行比较,即 RoboschoolWalker2d。我们为这两个算法调优时投入了相同的计算资源,以确保比较的公平性。TRPO 的超参数与前一节列出的相同,而 PPO 的超参数则显示在下表中:

超参数
神经网络64, tanh, 64, tanh
策略学习率3e-4
执行者迭代次数10
智能体数量1
时间跨度5,000
小批量大小256
裁剪系数0.2
Delta(用于 GAE)0.95
Gamma(用于 GAE)0.99

以下图示展示了 PPO 和 TRPO 的比较。PPO 需要更多的经验才能起步,但一旦达到这个状态,它会迅速提升,超过 TRPO。在这些特定设置下,PPO 在最终表现上也超过了 TRPO。请记住,进一步调整超参数可能会带来更好的结果,且略有不同:

图 7.9. PPO 和 TRPO 性能比较

一些个人观察:我们发现与 TRPO 相比,PPO 的调优更加困难。其原因之一是 PPO 中超参数的数量较多。此外,演员学习率是最重要的调优系数之一,如果没有正确调节,它会极大地影响最终结果。TRPO 的一个大优点是它没有学习率,并且策略仅依赖于几个易于调节的超参数。而 PPO 的优势则在于其速度更快,且已被证明能在更广泛的环境中有效工作。

总结

在本章中,你学习了如何将策略梯度算法应用于控制具有连续动作的智能体,并使用了一组新的环境,称为 Roboschool。

你还学习并开发了两种高级策略梯度算法:信任域策略优化和近端策略优化。这些算法更好地利用了从环境中采样的数据,并使用技术限制两个后续策略分布之间的差异。具体来说,TRPO(顾名思义)使用二阶导数和基于旧策略与新策略之间 KL 散度的一些约束,围绕目标函数构建了一个信任域。另一方面,PPO 优化的目标函数与 TRPO 相似,但只使用一阶优化方法。PPO 通过在目标函数过大时对其进行裁剪,从而防止策略采取过大的步伐。

PPO 和 TRPO 仍然是基于策略的(与其他策略梯度算法一样),但它们比 AC 和 REINFORCE 更具样本效率。这是因为 TRPO 通过使用二阶导数,实际上从数据中提取了更高阶的信息。而 PPO 的样本效率则来自于其能够在相同的基于策略的数据上执行多次策略更新。

由于其样本效率、鲁棒性和可靠性,TRPO,尤其是 PPO,被广泛应用于许多复杂的环境中,如 Dota(openai.com/blog/openai-five/)。

PPO 和 TRPO,以及 AC 和 REINFORCE,都是随机梯度算法。

在下一章中,我们将探讨两种确定性策略梯度算法。确定性算法是一个有趣的替代方案,因为它们具有一些在我们目前看到的算法中无法复制的有用特性。

问题

  1. 策略神经网络如何控制连续的智能体?

  2. 什么是 KL 散度?

  3. TRPO 背后的主要思想是什么?

  4. KL 散度在 TRPO 中的作用是什么?

  5. PPO 的主要优点是什么?

  6. PPO 如何实现良好的样本效率?

进一步阅读

第八章:DDPG 和 TD3 的应用

在前一章中,我们对所有主要的策略梯度算法进行了全面的概述。由于它们能够处理连续的动作空间,因此被应用于非常复杂和精密的控制系统中。策略梯度方法还可以使用二阶导数,就像 TRPO 中所做的那样,或采用其他策略,以通过防止意外的坏行为来限制策略更新。然而,处理此类算法时的主要问题是它们效率较低,需要大量经验才能有希望掌握任务。这个缺点来源于这些算法的“在策略”(on-policy)特性,这使得每次策略更新时都需要新的经验。在本章中,我们将介绍一种新的“离策略”(off-policy)演员-评论家算法,它在探索环境时使用随机策略,同时学习一个目标确定性策略。由于其学习确定性策略的特点,我们将这些方法称为确定性策略梯度方法。我们将首先展示这些算法是如何工作的,并且还将展示它们与 Q 学习方法的密切关系。然后,我们将介绍两种确定性策略梯度算法:深度确定性策略梯度DDPG),以及它的一个后续版本,称为双延迟深度确定性策略梯度TD3)。通过在新环境中实现和应用这些算法,你将感受到它们的能力。

本章将涵盖以下主题:

  • 将策略梯度优化与 Q 学习结合

  • 深度确定性策略梯度

  • 双延迟深度确定性策略梯度(TD3)

将策略梯度优化与 Q 学习结合

在本书中,我们介绍了两种主要的无模型算法:基于策略梯度的算法和基于价值函数的算法。在第一类中,我们看到了 REINFORCE、演员-评论家、PPO 和 TRPO。在第二类中,我们看到了 Q 学习、SARSA 和 DQN。除了这两类算法学习策略的方式(即,策略梯度算法使用随机梯度上升,朝着估计回报的最陡增量方向前进,而基于价值的算法为每个状态-动作对学习一个动作值,然后构建策略)之外,还有一些关键的差异使我们能够偏好某一类算法。这些差异包括算法的“在策略”或“离策略”特性,以及它们处理大动作空间的倾向。我们已经在前几章讨论了在策略和离策略之间的区别,但理解它们非常重要,这样我们才能真正理解本章将介绍的算法。

离策略学习能够利用以前的经验来改进当前的策略,尽管这些经验来自不同的分布。DQN 通过将智能体在其生命周期中所有的记忆存储在回放缓存中,并从缓存中采样小批量数据来更新目标策略,从中获益。与此相对的是在策略学习,它要求经验来自当前的策略。这意味着不能使用旧的经验,每次更新策略时,必须丢弃旧数据。因此,由于离策略学习可以重复使用数据,它所需的环境交互次数较少。对于那些获取新样本既昂贵又非常困难的情况,这一差异尤为重要,选择离策略算法可能是至关重要的。

第二个因素是动作空间的问题。正如我们在第七章《TRPO 和 PPO 实现》中所看到的,策略梯度算法能够处理非常大且连续的动作空间。不幸的是,Q 学习算法并不具备这一能力。为了选择一个动作,它们必须在整个动作空间中进行最大化,当动作空间非常大或连续时,这种方法是不可行的。因此,Q 学习算法可以应用于任意复杂的问题(具有非常大的状态空间),但其动作空间必须受到限制。

总之,之前的算法中没有哪一个总是优于其他算法,选择算法通常依赖于任务的具体需求。然而,它们的优缺点是相互补充的,因此问题就出现了:是否有可能将两种算法的优点结合成一个单一的算法?

确定性策略梯度

设计一个既是离策略又能够在高维动作空间中学习稳定策略的算法是具有挑战性的。DQN 已经解决了在离策略设置中学习稳定深度神经网络策略的问题。使 DQN 适应连续动作的一种方法是对动作空间进行离散化。例如,如果一个动作的值在 0 和 1 之间,则可以将其离散化为 11 个值(0, 0.1, 0.2, .., 0.9, 1.0),并使用 DQN 预测这些值的概率。然而,这种解决方案对于动作数量较多的情况并不可行,因为可能的离散动作数随着智能体自由度的增加而呈指数增长。此外,这种技术不适用于需要更精细控制的任务。因此,我们需要找到一个替代方案。

一个有价值的想法是学习一个确定性的演员-评论家模型。它与 Q 学习有着密切的关系。如果你还记得,在 Q 学习中,为了最大化所有可能动作中的近似 Q 函数,最佳动作被选择:

这个想法是学习一个确定性的 策略,它逼近 。这克服了在每一步计算全局最大化的问题,并且为将其扩展到非常高维度和连续动作的情况打开了可能性。确定性策略梯度DPG)成功地将这一概念应用于一些简单的问题,例如山地车、摆锤和章鱼臂。DPG 之后,DDPG 扩展了 DPG 的思想,使用深度神经网络作为策略,并采用一些更为细致的设计选择,以使算法更加稳定。进一步的算法,TD3,解决了 DPG 和 DDPG 中常见的高方差和过度估计偏差问题。接下来将解释和发展 DDPG 和 TD3。在我们构建一个分类 RL 算法的图谱时,我们将 DPG、DDPG 和 TD3 放置在策略梯度和 Q 学习算法的交集处,如下图所示。现在,让我们专注于 DPG 的基础以及它是如何工作的:

到目前为止开发的无模型 RL 算法的分类

新的 DPG 算法结合了 Q 学习和策略梯度方法。一个参数化的确定性策略只输出确定性的值。在连续的上下文中,这些值可以是动作的均值。然后,可以通过求解以下方程来更新策略的参数:

是参数化的动作值函数。注意,确定性方法与随机方法的不同之处在于,确定性方法不会向动作中添加额外的噪声。在 PPO 和 TRPO 中,我们是从一个正态分布中采样,具有均值和标准差。而在这里,策略只有一个确定性的均值。回到更新公式(8.1),和往常一样,最大化是通过随机梯度上升来完成的,这将通过小幅更新逐步改进策略。然后,目标函数的梯度可以如下计算:

是遵循 策略的状态分布。这种形式来源于确定性策略梯度定理。它表示,目标函数的梯度是通过链式法则应用于 Q 函数计算的,该 Q 函数是相对于 策略参数来求解的。使用像 TensorFlow 这样的自动微分软件,计算这一梯度非常容易。实际上,梯度是通过从 Q 值开始,沿着策略一直计算梯度,然后只更新后者的参数,如下所示:

DPG 定理的示意图

梯度是从 Q 值开始计算的,但只有策略会被更新。

这是一个更理论性的结果。我们知道,确定性策略不会探索环境,因此它们无法找到好的解决方案。为了使 DPG 成为脱离策略的,我们需要更进一步,定义目标函数的梯度,使得期望符合随机探索策略的分布:

是一种探索策略,也叫做行为策略。该方程给出了脱离策略确定性策略梯度,并给出了相对于确定性策略()的梯度估计,同时生成遵循行为策略()的轨迹。请注意,实际中,行为策略仅仅是加上噪声的确定性策略。

尽管我们之前已经讨论过确定性演员-评论家的问题,但到目前为止,我们只展示了策略学习是如何进行的。实际上,我们同时学习了由确定性策略()表示的演员,以及由 Q 函数()表示的评论家。可微分的动作值函数()可以通过贝尔曼更新轻松学习,从而最小化贝尔曼误差。

(),正如 Q-learning 算法中所做的那样。

深度确定性策略梯度

如果你使用上一节中介绍的深度神经网络实现了 DPG 算法,算法将非常不稳定,且无法学到任何东西。我们在将 Q-learning 与深度神经网络结合时遇到了类似的问题。实际上,为了将 DNN 和 Q-learning 结合在 DQN 算法中,我们不得不采用一些其他技巧来稳定学习。DPG 算法也是如此。这些方法是脱离策略的,像 Q-learning 一样,正如我们很快将看到的,能让确定性策略与 DNN 配合使用的某些因素与 DQN 中使用的因素类似。

DDPG(使用深度强化学习的连续控制 由 Lillicrap 等人:arxiv.org/pdf/1509.02971.pdf)是第一个使用深度神经网络的确定性演员-评论家算法,用于同时学习演员和评论家。这个无模型、脱离策略的演员-评论家算法扩展了 DQN 和 DPG,因为它借用了 DQN 的一些见解,如回放缓冲区和目标网络,使得 DPG 能够与深度神经网络一起工作。

DDPG 算法

DDPG 使用了两个关键思想,均借鉴自 DQN,但已适配到演员-评论家的案例中:

  • 回放缓冲区:在智能体的生命周期内获取的所有过渡数据都会被存储在回放缓冲区中,也叫经验回放。然后,通过从中采样小批量数据,使用它来训练演员和评论员。

  • 目标网络:Q 学习是不稳定的,因为更新的网络也是用来计算目标值的网络。如果你还记得,DQN 通过使用目标网络来缓解这个问题,目标网络每 N 次迭代更新一次(将在线网络的参数复制到目标网络)。在 DDQN 论文中,他们表明,在这种情况下,软目标更新效果更好。通过软更新,目标网络的参数 会在每一步与在线网络的参数 部分更新: 通过 。是的,尽管这可能会减慢学习速度,因为目标网络只部分更新,但它的好处超过了由增加的不稳定性带来的负面影响。使用目标网络的技巧不仅适用于演员,也适用于评论员,因此目标评论员的参数也会在软更新后更新:

请注意,从现在开始,我们将 称为在线演员和在线评论员的参数,将 称为目标演员和目标评论员的参数。

DDPG 从 DQN 继承的一个特点是,能够在每一步环境交互后更新演员和评论员。这源于 DDPG 是一个离策略算法,并且从从回放缓冲区采样的小批量数据中学习。与基于策略的随机策略梯度方法相比,DDPG 不需要等到从环境中收集到足够大的批次数据。

之前,我们看到尽管 DPG 是在学习一个确定性策略,但它是根据一个探索行为策略来执行的。那么,这个探索性策略是如何构建的呢?在 DDPG 中, 策略是通过添加从噪声过程中采样的噪声来构建的():

过程将确保环境被充分探索。

总结一下,DDPG 通过不断循环执行以下三个步骤直到收敛:

  • 行为策略与环境进行交互,通过将观察和奖励存储在缓冲区中,从环境中收集它们。

  • 在每一步中,演员和评论员都会根据从缓冲区采样的迷你批次中的信息进行更新。具体来说,评论员通过最小化在线评论员预测的值()与使用目标策略()和目标评论员()计算得到的目标值之间的均方误差(MSE)损失来更新。相反,演员是按照公式(8.3)进行更新的。

  • 目标网络的参数是按照软更新进行更新的。

整个算法的总结见下列伪代码:

---------------------------------------------------------------------------------
DDPG Algorithm
---------------------------------------------------------------------------------

Initialize online networks  and 
Initialize target networks  and  with the same weights as the online networks
Initialize empty replay buffer 
Initialize environment 

for  do
    > Run an episode
    while not d:

        > Store the transition in the buffer

        > Sample a minibatch 

        > Calculate the target value for every i in b
         (8.4)

        > Update the critic 
         (8.5)

        > Update the policy
         (8.6)

        > Targets update

if :

对算法有了更清晰的理解后,我们现在可以开始实现它了。

DDPG 实现

前面部分给出的伪代码已经提供了该算法的全面视图,但从实现的角度来看,仍有一些值得深入探讨的内容。在这里,我们将展示一些更有趣的特性,这些特性也可能出现在其他算法中。完整代码可在本书的 GitHub 仓库中获取:github.com/PacktPublishing/Reinforcement-Learning-Algorithms-with-Python

具体来说,我们将重点关注以下几个主要部分:

  • 如何构建确定性演员-评论员

  • 如何进行软更新

  • 如何优化一个损失函数,仅针对某些参数

  • 如何计算目标值

我们在一个名为deterministic_actor_critic的函数中定义了一个确定性策略的演员和评论员。这个函数将被调用两次,因为我们需要同时创建在线和目标演员-评论员。代码如下:

def deterministic_actor_critic(x, a, hidden_sizes, act_dim, max_act):
    with tf.variable_scope('p_mlp'):
        p_means = max_act * mlp(x, hidden_sizes, act_dim, last_activation=tf.tanh)
    with tf.variable_scope('q_mlp'):
        q_d = mlp(tf.concat([x,p_means], axis=-1), hidden_sizes, 1, last_activation=None)
    with tf.variable_scope('q_mlp', reuse=True): # reuse the weights
        q_a = mlp(tf.concat([x,a], axis=-1), hidden_sizes, 1, last_activation=None)
    return p_means, tf.squeeze(q_d), tf.squeeze(q_a)

在这个函数内部有三件有趣的事情。首先,我们区分了两种输入类型,都是传递给同一个评论员的。第一种是输入一个状态,策略返回一个p_means确定性动作;第二种是输入一个状态和一个任意动作。做出这种区分是因为,一个评论员将用于优化演员,而另一个将用于优化评论员。尽管这两个评论员有不同的输入,但它们是同一个神经网络,意味着它们共享相同的参数。这种不同的用法是通过为两个评论员实例定义相同的变量作用域,并将第二个实例的reuse=True来实现的。这样可以确保这两个定义的参数是相同的,实际上只创建了一个评论员。

第二个观察是,我们在一个名为p_mlp的变量作用域中定义了演员。这是因为,稍后我们只需要提取这些参数,而不是评论员的参数。

第三个观察结果是,由于策略的最终激活层是tanh函数(将值限制在-1 和 1 之间),但我们的演员可能需要超出这个范围的值,我们必须将输出乘以max_act因子(这假设最小值和最大值是相反的,即,如果最大允许值是 3,最小值是-3)。

很好!现在让我们继续查看计算图的其余部分,在这里我们定义了占位符;创建了在线和目标演员,以及在线和目标评论员;定义了损失函数;实现了优化器;并更新了目标网络。

我们将从创建我们需要的占位符开始,用于观察值、动作和目标值:

obs_dim = env.observation_space.shape
act_dim = env.action_space.shape

obs_ph = tf.placeholder(shape=(None, obs_dim[0]), dtype=tf.float32, name='obs')
act_ph = tf.placeholder(shape=(None, act_dim[0]), dtype=tf.float32, name='act')
y_ph = tf.placeholder(shape=(None,), dtype=tf.float32, name='y')

在之前的代码中,y_ph是目标 Q 值的占位符,obs_ph是观察值的占位符,act_ph是动作的占位符。

然后我们在onlinetarget变量作用域内调用之前定义的deterministic_actor_critic函数,以便区分四个神经网络:

with tf.variable_scope('online'):
    p_onl, qd_onl, qa_onl = deterministic_actor_critic(obs_ph, act_ph, hidden_sizes, act_dim[0], np.max(env.action_space.high))

with tf.variable_scope('target'):
    _, qd_tar, _ = deterministic_actor_critic(obs_ph, act_ph, hidden_sizes, act_dim[0], np.max(env.action_space.high))

评论员的损失是qa_onl在线网络的 Q 值和y_ph目标动作值之间的 MSE 损失:

q_loss = tf.reduce_mean((qa_onl - y_ph)**2)

这将通过 Adam 优化器来最小化:

q_opt = tf.train.AdamOptimizer(cr_lr).minimize(q_loss)

关于演员的损失函数,它是在线 Q 网络的相反符号。在这种情况下,在线 Q 网络的输入是由在线确定性演员选择的动作(如公式(8.6)所示,这在《DDPG 算法》部分的伪代码中定义)。因此,Q 值由qd_onl表示,策略损失函数写作如下:

p_loss = -tf.reduce_mean(qd_onl)

我们取了目标函数的相反符号,因为我们必须将其转换为损失函数,考虑到优化器需要最小化损失函数。

现在,最重要的要记住的是,尽管我们计算了依赖于评论员和演员的p_loss损失函数的梯度,但我们只需要更新演员。实际上,从 DPG 中我们知道!

这通过将p_loss传递给优化器的minimize方法来完成,该方法指定了需要更新的变量。在这种情况下,我们只需要更新在online/m_mlp变量作用域中定义的在线演员的变量:

p_opt = tf.train.AdamOptimizer(ac_lr).minimize(p_loss, var_list=variables_in_scope('online/p_mlp'))

这样,梯度的计算将从p_loss开始,经过评论员的网络,再到演员的网络。最后,只有演员的参数会被优化。

现在,我们需要定义variable_in_scope(scope)函数,它返回名为scope的作用域中的变量:

def variables_in_scope(scope):
    return tf.get_collection(tf.GraphKeys.GLOBAL_VARIABLES, scope)

现在是时候查看目标网络是如何更新的了。我们可以使用variable_in_scope来获取演员和评论员的目标变量和在线变量,并使用 TensorFlow 的assign函数更新目标变量,按照软更新公式进行:

这在以下代码片段中完成:

update_target = [target_var.assign(tau*online_var + (1-tau)*target_var) for target_var, online_var in zip(variables_in_scope('target'), variables_in_scope('online'))]
update_target_op = tf.group(*update_target)

就这样!对于计算图而言,这就是全部。是不是很简单?现在我们可以快速浏览一下主要的循环,其中参数根据有限批次样本的估计梯度进行更新。策略与环境的交互是标准的,唯一的例外是,当前返回的动作是确定性的,因此我们需要添加一定量的噪声,以便充分探索环境。在这里,我们没有提供这部分代码,但你可以在 GitHub 上找到完整的实现。

当积累了足够的经验,并且缓冲区达到了某个阈值时,策略和评估网络的优化就开始了。接下来的步骤是*《DDPG 算法》*部分中提供的 DDPG 伪代码的摘要。步骤如下:

  1. 从缓冲区中采样一个小批量

  2. 计算目标动作值

  3. 优化评估网络

  4. 优化演员网络

  5. 更新目标网络

所有这些操作都在几行代码中完成:

    ... 

    mb_obs, mb_rew, mb_act, mb_obs2, mb_done = buffer.sample_minibatch(batch_size)

    q_target_mb = sess.run(qd_tar, feed_dict={obs_ph:mb_obs2})
    y_r = np.array(mb_rew) + discount*(1-np.array(mb_done))*q_target_mb

    _, q_train_loss = sess.run([q_opt, q_loss], feed_dict={obs_ph:mb_obs, y_ph:y_r, act_ph: mb_act})

    _, p_train_loss = sess.run([p_opt, p_loss], feed_dict={obs_ph:mb_obs})

    sess.run(update_target_op)

     ...

代码的第一行采样了一个batch_size大小的小批量,第二和第三行通过运行评估器和演员目标网络在mb_obs2(包含下一个状态)上来计算目标动作值,正如公式(8.4)所定义的那样。第四行通过输入包含目标动作值、观测和动作的字典来优化评估器。第五行优化演员网络,最后一行通过运行update_target_op更新目标网络。

将 DDPG 应用于 BipedalWalker-v2

现在,我们将 DDPG 应用于一个名为 BipedalWalker-v2 的连续任务,也就是 Gym 提供的一个使用 Box2D(一个 2D 物理引擎)的环境。以下是该环境的截图。目标是让智能体在崎岖的地形上尽可能快地行走。完成任务直到结束会获得 300+的分数,但每次应用马达会消耗少量能量。智能体移动得越高效,消耗的能量就越少。此外,如果智能体摔倒,会获得-100 的惩罚。状态由 24 个浮动数值组成,表示关节、船体的速度和位置,以及 LiDar 测距仪的测量数据。智能体由四个连续的动作控制,动作的范围是[-1,1]。以下是 BipedalWalker 2D 环境的截图:

BipedalWalker2d 环境截图

我们使用以下表格中的超参数来运行 DDPG。在第一行,列出了运行 DDPG 所需的超参数,而第二行列出了在这个特定案例中使用的相应值。让我们参考以下表格:

超参数演员学习率评论家学习率DNN 架构缓冲区大小批量大小Tau
3e-44e-4[64,relu,64,relu]200000640.003

在训练过程中,我们在策略预测的动作中加入了额外的噪声。然而,为了衡量算法的表现,我们每 10 个回合在纯确定性策略(没有额外噪声)上进行 10 局游戏。以下图表展示了在时间步长的函数下,10 局游戏的累计奖励的平均值:

DDPG 算法在 BipedalWalker2d-v2 上的表现

从结果来看,我们可以看到性能相当不稳定,在几千步之后的得分波动范围从 250 到不到-100。众所周知,DDPG 本身是不稳定的,而且对超参数非常敏感,但经过更细致的调优,结果可能会更平滑。尽管如此,我们可以看到,在前 300k 步内,性能有所提升,达到了大约 100 的平均得分,峰值可达 300。

此外,BipedalWalker-v2 是一个非常难以解决的环境。事实上,当代理在 100 个连续回合中获得至少 300 分的平均奖励时,才算解决了这个环境。使用 DDPG 时,我们未能达到这些性能,但我们仍然获得了一个较好的策略,使得代理能够运行得相当快。

在我们的实现中,我们使用了一个恒定的探索因子。如果使用更复杂的函数,可能在更少的迭代中达到更高的性能。例如,在 DDPG 的论文中,他们使用了一个奥恩斯坦-乌伦贝克过程。如果你愿意,可以从这个过程开始。

DDPG 是一个美丽的例子,展示了如何将确定性策略与随机策略对立使用。然而,由于它是第一个解决复杂问题的算法,因此仍然有许多调整可以应用于它。本章中提出的下一个算法,进一步推动了 DDPG 的进步。

双延迟深度确定性策略梯度(TD3)

DDPG 被认为是最具样本效率的演员-评论家算法之一,但已被证明在超参数上非常敏感且易碎。后续的研究尝试通过引入新颖的思路,或者将其他算法的技巧应用于 DDPG,来缓解这些问题。最近,一种新的算法作为 DDPG 的替代方案出现:双延迟深度确定性策略梯度,简称 TD3(论文是Addressing Function Approximation Error in Actor-Critic Methodsarxiv.org/pdf/1802.09477.pdf)。我们在这里使用“替代”一词,是因为它实际上是 DDPG 算法的延续,添加了一些新的元素,使得它更稳定,性能也更优。

TD3 专注于一些在其他脱机算法中也常见的问题。这些问题包括价值估计的高估和梯度估计的高方差。针对前者问题,他们采用了类似 DQN 中使用的解决方案;对于后者问题,他们采用了两种新的解决方案。我们首先考虑高估偏差问题。

解决高估偏差

高估偏差意味着由近似 Q 函数预测的动作值高于实际值。在具有离散动作的 Q 学习算法中,这一问题被广泛研究,通常会导致不良的预测,从而影响最终性能。尽管影响较小,但这个问题在 DDPG 中也存在。

如果你还记得,减少动作值高估的 DQN 变体被称为双 DQN,它提出了两个神经网络;一个用于选择动作,另一个用于计算 Q 值。特别地,第二个神经网络的工作是由一个冻结的目标网络完成的。这个想法很合理,但正如 TD3 论文中所解释的,它在演员-评论员方法中并不有效,因为在这些方法中,策略变化太慢。因此,他们提出了一种变体,称为剪切双 Q 学习,它取两个不同评论员的估计值之间的最小值()。因此,目标值的计算如下:

另一方面,这并不会阻止低估偏差,但它远比高估偏差危害小。剪切双 Q 学习可以在任何演员-评论员方法中使用,并且它遵循这样一个假设:两个评论员会有不同的偏差。

TD3 的实现

为了将此策略转化为代码,我们需要创建两个具有不同初始化的评论员,计算目标动作值,如(8.7)中所示,并优化这两个评论员。

TD3 应用于我们在前一节中讨论的 DDPG 实现。以下代码片段仅是实现 TD3 所需的额外代码的一部分。完整实现可在本书的 GitHub 仓库中找到:github.com/PacktPublishing/Hands-On-Reinforcement-Learning-Algorithms-with-Python

关于双评论员,你只需通过调用deterministic_actor_double_critic两次来创建它们,一次用于目标网络,一次用于在线网络,正如在 DDPG 中所做的那样。代码大致如下:

def deterministic_actor_double_critic(x, a, hidden_sizes, act_dim, max_act):
    with tf.variable_scope('p_mlp'):
        p_means = max_act * mlp(x, hidden_sizes, act_dim, last_activation=tf.tanh)

    # First critic
    with tf.variable_scope('q1_mlp'):
        q1_d = mlp(tf.concat([x,p_means], axis=-1), hidden_sizes, 1, last_activation=None)
    with tf.variable_scope('q1_mlp', reuse=True): # Use the weights of the mlp just defined
        q1_a = mlp(tf.concat([x,a], axis=-1), hidden_sizes, 1, last_activation=None)

    # Second critic
    with tf.variable_scope('q2_mlp'):
        q2_d = mlp(tf.concat([x,p_means], axis=-1), hidden_sizes, 1, last_activation=None)
    with tf.variable_scope('q2_mlp', reuse=True):
        q2_a = mlp(tf.concat([x,a], axis=-1), hidden_sizes, 1, last_activation=None)

    return p_means, tf.squeeze(q1_d), tf.squeeze(q1_a), tf.squeeze(q2_d), tf.squeeze(q2_a)

剪切目标值((8.7))是通过首先运行我们称之为qa1_tarqa2_tar的两个目标评论员,然后计算估计值之间的最小值,最后使用它来估算目标值:

            ...            
            double_actions = sess.run(p_tar, feed_dict={obs_ph:mb_obs2})

            q1_target_mb, q2_target_mb = sess.run([qa1_tar,qa2_tar], feed_dict={obs_ph:mb_obs2, act_ph:double_actions})
            q_target_mb = np.min([q1_target_mb, q2_target_mb], axis=0) 
            y_r = np.array(mb_rew) + discount*(1-np.array(mb_done))*q_target_mb
            ..

接下来,评论员可以像往常一样进行优化:

            ...
            q1_train_loss, q2_train_loss = sess.run([q1_opt, q2_opt], feed_dict={obs_ph:mb_obs, y_ph:y_r, act_ph: mb_act})
            ...

一个重要的观察是,策略是相对于一个近似的 Q 函数进行优化的,在我们的例子中是 。事实上,如果你查看完整代码,你会发现 p_loss 被定义为 p_loss = -tf.reduce_mean(qd1_onl)

解决方差减少问题

TD3 的第二个也是最后一个贡献是方差的减少。为什么高方差是个问题呢?因为它会产生噪声梯度,导致错误的策略更新,从而影响算法的性能。高方差的复杂性体现在 TD 误差中,它通过后续状态估计动作值。

为了缓解这个问题,TD3 引入了延迟的策略更新和目标正则化技术。让我们看看它们是什么,为什么它们如此有效。

延迟的策略更新

由于高方差归因于不准确的评论,TD3 提议将策略更新延迟,直到评论误差足够小为止。TD3 以经验方式延迟更新策略,仅在固定的迭代次数之后才更新策略。通过这种方式,评论有时间学习并稳定自身,然后再进行策略优化。实际上,策略仅在几个迭代中保持固定,通常是 1 到 6 次。如果设置为 1,则与 DDPG 中的情况相同。延迟的策略更新可以通过以下方式实现:

            ...
            q1_train_loss, q2_train_loss = sess.run([q1_opt, q2_opt], feed_dict={obs_ph:mb_obs, y_ph:y_r, act_ph: mb_act})
            if step_count % policy_update_freq == 0:
                sess.run(p_opt, feed_dict={obs_ph:mb_obs})
                sess.run(update_target_op)
            ...

目标正则化

从确定性动作更新的评论往往会在狭窄的峰值中产生过拟合。其后果是方差增加。TD3 提出了一个平滑正则化技术,它在目标动作附近的小区域添加了一个剪切噪声:

该正则化可以通过一个函数实现,该函数接受一个向量和一个比例作为参数:

def add_normal_noise(x, noise_scale):
    return x + np.clip(np.random.normal(loc=0.0, scale=noise_scale, size=x.shape), -0.5, 0.5)

然后,在运行目标策略后,调用 add_normal_noise,如下代码所示(与 DDPG 实现的不同之处已加粗):

            ...            
            double_actions = sess.run(p_tar, feed_dict={obs_ph:mb_obs2})
            double_noisy_actions = np.clip(add_normal_noise(double_actions, target_noise), env.action_space.low, env.action_space.high)

            q1_target_mb, q2_target_mb = sess.run([qa1_tar,qa2_tar], feed_dict={obs_ph:mb_obs2, act_ph:double_noisy_actions})
            q_target_mb = np.min([q1_target_mb, q2_target_mb], axis=0) 
            y_r = np.array(mb_rew) + discount*(1-np.array(mb_done))*q_target_mb
            ..

我们在添加了额外噪声后,剪切了动作,以确保它们不会超出环境设定的范围。

将所有内容结合起来,我们得到了以下伪代码所示的算法:

---------------------------------------------------------------------------------
TD 3 Algorithm
---------------------------------------------------------------------------------

Initialize online networks  and 
Initialize target networks  and  with the same weights as the online networks
Initialize empty replay buffer 
Initialize environment 

for  do
    > Run an episode
    while not d:

        > Store the transition in the buffer

        > Sample a minibatch 

        > Calculate the target value for every i in b

        > Update the critics 

        if iter % policy_update_frequency == 0:
            > Update the policy

            > Targets update

 if :

这就是 TD3 算法的全部内容。现在,你对所有确定性和非确定性策略梯度方法有了清晰的理解。几乎所有的无模型算法都基于我们在这些章节中解释的原则,如果你掌握了它们,你将能够理解并实现所有这些算法。

将 TD3 应用到 BipedalWalker

为了直接比较 TD3 和 DDPG,我们在与 DDPG 相同的环境中测试了 TD3:BipedalWalker-v2。

针对这个环境,TD3 的最佳超参数列在下面的表格中:

超参数Actor 学习率Critic 学习率DNN 架构缓冲区大小批次大小Tau

策略更新频率

|

Sigma

|

4e-44e-4[64,relu,64,relu]200000640.00520.2

结果绘制在下图中。曲线呈平滑趋势,在大约 30 万步之后达到了良好的结果,在训练的 45 万步时达到了顶峰。它非常接近 300 分的目标,但实际上并没有达到:

TD3 算法在 BipedalWalker-v2 上的表现

相比于 DDPG,找到 TD3 的超参数所花费的时间较少。而且,尽管我们只是在一个游戏上比较这两种算法,我们认为这是一个很好的初步观察,帮助我们理解它们在稳定性和性能上的差异。DDPG 和 TD3 在 BipedalWalker-v2 上的表现如下:

DDPG 与 TD3 性能比较

如果你想在更具挑战性的环境中训练算法,可以尝试 BipedalWalkerHardcore-v2。它与 BipedalWalker-v2 非常相似,唯一不同的是它有梯子、树桩和陷阱。很少有算法能够完成并解决这个环境。看到智能体无法通过这些障碍也非常有趣!

相比于 DDPG,TD3 的优越性非常明显,无论是在最终性能、改进速度还是算法稳定性上。

本章中提到的所有颜色参考,请参考此链接中的颜色图像包:www.packtpub.com/sites/default/files/downloads/9781789131116_ColorImages.pdf

总结

在本章中,我们介绍了解决强化学习问题的两种不同方法。第一种是通过估计状态-动作值来选择最佳的下一步动作,这就是所谓的 Q-learning 算法。第二种方法是通过梯度最大化期望奖励策略。事实上,这些方法被称为策略梯度方法。本章中,我们展示了这些方法的优缺点,并证明了它们在许多方面是互补的。例如,Q-learning 算法在样本效率上表现优秀,但无法处理连续动作。相反,策略梯度算法需要更多数据,但能够控制具有连续动作的智能体。接着,我们介绍了结合 Q-learning 和策略梯度技术的 DPG 方法。特别地,这些方法通过预测一个确定性策略,克服了 Q-learning 算法中的全局最大化问题。我们还展示了如何通过 Q 函数的梯度定义 DPG 定理中的确定性策略更新。

我们学习并实现了两种 DPG 算法:DDPG 和 TD3。这两种算法都是脱离策略的演员-评论家算法,可以用于具有连续动作空间的环境。TD3 是 DDPG 的升级版,封装了一些减少方差的技巧,并限制了在 Q-learning 算法中常见的过度估计偏差。

本章总结了无模型强化学习算法的概述。我们回顾了迄今为止已知的所有最佳和最具影响力的算法,从 SARSA 到 DQN,再到 REINFORCE 和 PPO,并将它们结合在如 DDPG 和 TD3 等算法中。这些算法本身在适当的微调和大量数据的支持下,能够实现惊人的成果(参见 OpenAI Five 和 AlphaStar)。然而,这并不是强化学习的全部内容。在下一章中,我们将不再讨论无模型算法,而是展示一种基于模型的算法,其目的是通过学习环境模型来减少学习任务所需的数据量。在随后的章节中,我们还将展示更先进的技术,如模仿学习、新的有用强化学习算法,如 ESBAS,以及非强化学习算法,如进化策略。

问题

  1. Q-learning 算法的主要限制是什么?

  2. 为什么随机梯度算法样本效率低?

  3. DPG 是如何克服最大化问题的?

  4. DPG 是如何保证足够的探索的?

  5. DDPG 代表什么?它的主要贡献是什么?

  6. TD3 提出了哪些问题来最小化?

  7. TD3 使用了哪些新机制?

深入阅读

你可以通过以下链接了解更多:

第三部分:超越无模型算法与改进

在本节中,你将实现基于模型的算法、模仿学习、进化策略,并了解一些可能进一步改进强化学习(RL)算法的思想。

本节包含以下章节:

  • 第九章,基于模型的 RL

  • 第十章,使用 DAgger 算法的模仿学习

  • 第十一章,理解黑箱优化算法

  • 第十二章,开发 ESBAS 算法

  • 第十三章,解决 RL 挑战的实际实现

第九章:基于模型的强化学习

强化学习算法分为两类——无模型方法和基于模型的方法。这两类的区别在于对环境模型的假设。无模型算法仅通过与环境的互动学习策略,而对环境一无所知;而基于模型的算法已经对环境有深入的理解,并利用这些知识根据模型的动态来采取下一步行动。

在本章中,我们将为你提供一个全面的基于模型方法的概述,突出其与无模型方法相比的优缺点,以及当模型已知或需要学习时产生的差异。后者的划分很重要,因为它影响了问题的处理方式和用于解决问题的工具。在这段介绍之后,我们将讨论一些更为复杂的案例,其中基于模型的算法必须处理高维度的观察空间,比如图像。

此外,我们还将讨论一种结合了基于模型和无模型方法的算法类,用于在高维空间中学习模型和策略。我们将深入了解其内部工作原理,并解释为何使用这种方法。然后,为了加深我们对基于模型的算法,特别是结合基于模型和无模型方法的算法的理解,我们将开发一种最先进的算法,叫做模型集成信任区域策略优化ME-TRPO),并将其应用于连续倒立摆问题。

本章将涵盖以下主题:

  • 基于模型的方法

  • 将基于模型与无模型学习结合

  • 将 ME-TRPO 应用到倒立摆问题

基于模型的方法

无模型算法是一种强大的算法类型,能够学习非常复杂的策略并在复杂和多样化的环境中完成目标。正如 OpenAI 的最新工作所展示的(openai.com/five/)和 DeepMind 的工作(deepmind.com/blog/article/alphastar-mastering-real-time-strategy-game-starcraft-ii),这些算法实际上能够在《星际争霸》和《Dota 2》等挑战性游戏中展示长期规划、团队合作以及对意外情况的适应能力。

训练过的智能体已经能够击败顶级职业玩家。然而,最大的问题在于需要进行大量游戏才能训练智能体掌握这些游戏。实际上,为了取得这些结果,算法已经被大规模扩展,允许智能体与自己对战,进行数百年的游戏。但,这种方法到底有什么问题呢?

好吧,直到你为一个模拟器训练一个智能体,你可以收集你想要的任何经验。当你在一个像你生活的世界一样缓慢而复杂的环境中运行智能体时,问题就出现了。在这种情况下,你不能等上几百年才看到一些有趣的能力。那么,我们能否开发出一种与现实环境互动较少的算法?可以。正如你可能还记得的,我们已经在无模型算法中探讨过这个问题。

解决方案是使用脱离策略的算法。然而,收效甚微,对于许多现实世界问题来说,其增益并不显著。

正如你可能预料的那样,答案(或至少一个可能的答案)就在基于模型的强化学习算法中。你已经开发了一种基于模型的算法。你还记得是哪一种吗?在第三章中,使用动态规划解决问题,我们将环境模型与动态规划结合起来,训练智能体在有陷阱的地图上导航。由于动态规划使用了环境模型,因此它被视为一种基于模型的算法。

不幸的是,动态规划(DP)无法应用于中等或复杂的问题。所以,我们需要探索其他类型的基于模型的算法,这些算法能够扩展并在更具挑战性的环境中发挥作用。

基于模型的学习的广泛视角

让我们首先回顾一下什么是模型。模型由环境的转移动态和奖励组成。转移动态是一个从状态 s 和动作 a 映射到下一个状态 s' 的过程。

有了这些信息,环境就可以通过模型完全表示,并且可以用模型代替环境。如果智能体能访问该模型,那么它就有能力预测自己的未来。

在接下来的章节中,我们将看到模型可以是已知的或未知的。在已知模型的情况下,模型直接用来利用环境的动态;也就是说,模型提供了一个表示,用来代替环境。在环境模型未知的情况下,模型可以通过直接与环境互动来学习。但由于在大多数情况下,我们只学到环境的一个近似模型,因此在使用时必须考虑额外的因素。

现在我们已经解释了什么是模型,我们可以看看如何使用模型,以及它如何帮助我们减少与环境的互动次数。模型的使用方式取决于两个非常重要的因素——模型本身以及选择动作的方式。

确实,正如我们刚才提到的,模型可以是已知的或未知的,行动可以通过一个学习到的策略来规划或选择。算法会根据具体情况有所不同,因此让我们首先详细说明在模型已知的情况下使用的方式(即我们已经拥有环境的转移动态和奖励)。

一个已知的模型

当模型已知时,可以用它来模拟完整的轨迹,并计算每条轨迹的回报。然后,选择那些能够带来最高回报的动作。这个过程被称为规划,而环境模型是不可或缺的,因为它提供了生成下一个状态(给定一个状态和一个动作)和回报所需的信息。

规划算法在各个领域都有应用,但我们关注的算法与它们操作的动作空间类型不同。有些算法处理离散动作,其他则处理连续动作。

针对离散动作的规划算法通常是搜索算法,它们构建决策树,例如下面图示的那种:

当前状态是根节点,可能的动作由箭头表示,其他节点是通过一系列动作达到的状态。

你可以看到,通过尝试每一个可能的动作序列,最终会找到最优的那个。不幸的是,在大多数问题中,这个过程是不可行的,因为可能的动作数量呈指数级增长。复杂问题中使用的规划算法采用一些策略,通过依赖有限数量的轨迹来实现规划。

其中一个算法,也被 AlphaGo 采用,叫做蒙特卡洛树搜索(MCTS)。MCTS 通过生成一系列有限的模拟游戏,迭代构建决策树,同时充分探索那些尚未访问的树枝。一旦一个模拟游戏或轨迹达到叶节点(即游戏结束),它会将结果反向传播到访问过的状态,并更新节点所持有的胜/负或回报信息。然后,选择能够带来更高胜/负比或回报的动作。

相对的,处理连续动作的规划算法涉及轨迹优化技术。这些算法比其离散动作的对手更难解决,因为它们需要处理一个无限维的优化问题。

此外,许多算法需要模型的梯度。例如,模型预测控制(MPC)会对有限时间范围进行优化,但它并不执行找到的完整轨迹,而只执行第一步动作。通过这样做,MPC 与其他具有无限时间范围规划的方法相比,响应速度更快。

未知模型

当环境的模型未知时应该怎么办?学习它!到目前为止,我们所见的一切几乎都涉及学习。那么,这是否是最佳方法呢?嗯,如果你确实想使用基于模型的方法,答案是肯定的,稍后我们将看到如何做到这一点。然而,这并不总是最佳的做法。

在强化学习中,最终目标是为给定任务学习一个最优策略。在本章之前,我们提到过基于模型的方法主要用于减少与环境的互动次数,但这总是成立吗?假设你的目标是做一个煎蛋卷。知道鸡蛋的确切断裂点完全没有用;你只需要大致知道如何打破它。因此,在这种情况下,不涉及鸡蛋结构的无模型算法更为合适。

然而,这不应该让你认为基于模型的算法不值得使用。例如,当模型比策略更容易学习时,基于模型的方法在某些情况下优于无模型的方法。

学习一个模型的唯一方式是(不幸的是)通过与环境的互动。这是一个必经步骤,因为它让我们能够获取并创建关于环境的数据集。通常,学习过程是以监督方式进行的,其中一个函数逼近器(如深度神经网络)被训练以最小化损失函数,例如环境获得的转移和预测之间的均方误差损失。以下图示展示了这一过程,其中一个深度神经网络被训练来通过预测下一个状态s'和奖励r,从状态s和动作a来建模环境:

除了神经网络,还有其他选择,如高斯过程和高斯混合模型。特别是,高斯过程的特点是能够考虑到模型的不确定性,并且被认为具有很高的数据效率。事实上,在深度神经网络出现之前,它们是最受欢迎的选择。

然而,高斯过程的主要缺点是它们在处理大型数据集时比较慢。实际上,要学习更复杂的环境(从而需要更大的数据集),更倾向使用深度神经网络。此外,深度神经网络能够学习那些将图像作为观测的环境模型。

有两种主要的学习环境模型的方法;一种是模型一旦学习完毕就固定不变,另一种是在开始时学习模型,但一旦计划或策略发生变化,就重新训练模型。以下图示展示了这两种选择:

在图示的上半部分,展示了一种顺序的基于模型的算法,其中智能体仅在学习模型之前与环境进行互动。在下半部分,展示了一种基于模型的学习的循环方法,其中模型通过来自不同策略的额外数据进行改进。

为了理解算法如何从第二种选择中受益,我们必须定义一个关键概念。为了收集用于学习环境动态的数据集,你需要一个能够让你导航的策略。但在开始时,该策略可能是确定性的或完全随机的。因此,在有限的交互次数下,所探索的空间将非常有限。

这使得模型无法学习到那些用于规划或学习最优轨迹的环境部分。但是,如果模型通过来自更新和更好的策略的新交互进行再训练,它将逐步适应新策略,并捕捉到所有尚未访问的环境部分(从策略角度来看)。这就是数据聚合。

在实践中,在大多数情况下,模型是未知的,并通过数据聚合方法来适应新产生的策略。然而,学习模型可能是具有挑战性的,潜在的问题如下:

  • 模型过拟合:学到的模型在环境的局部区域上过拟合,忽略了它的全局结构。

  • 不准确的模型:在一个不完美的模型上进行规划或学习策略可能会引发一连串的错误,导致潜在的灾难性结论。

优秀的基于模型的算法,能够学习模型,必须处理这些问题。一个潜在的解决方案是使用能够估算不确定性的算法,如贝叶斯神经网络,或通过使用模型集成。

优势和劣势

在开发强化学习算法(各种 RL 算法)时,有三个基本方面需要考虑:

  • 渐近性能:这是指如果算法拥有无限的时间和硬件资源时,它可以达到的最大性能。

  • 实际时间:这是指算法在给定计算能力下,达到特定性能所需的学习时间。

  • 样本效率:这是指与环境交互的次数,以达到给定的性能。

我们已经探讨了在无模型和基于模型的强化学习中样本效率的差异,且发现后者的样本效率要高得多。那么,实际时间和性能呢?其实,基于模型的算法通常具有较低的渐近性能,且训练速度较慢,相比之下,无模型算法的训练速度较快。通常,较高的数据效率往往会以牺牲性能和速度为代价。

基于模型学习性能较低的一个原因可以归因于模型的不准确性(如果是通过学习得到的),这种不准确性会为策略引入额外的误差。较长的学习时钟时间是由于规划算法的缓慢,或者是由于在不准确的学习环境中需要更多的交互才能学习到策略。此外,基于模型的规划算法由于规划的高计算成本,推理时间较慢,仍然需要在每一步进行规划。

总结来说,您必须考虑训练基于模型的算法所需的额外时间,并认识到这些方法的渐近性能较低。然而,当模型比策略本身更容易学习,并且与环境的交互代价较高或较慢时,基于模型的学习是极其有用的。

从两个方面来看,我们有无模型学习和基于模型的学习,它们各自有引人注目的特点,但也有明显的缺点。我们能否从两者中各取所长?

将基于模型的学习与无模型学习结合

我们刚刚看到,规划在训练和运行时都可能计算开销较大,并且在更复杂的环境中,规划算法无法实现良好的性能。我们简要提到的另一种策略是学习策略。策略在推理时无疑要快得多,因为它不需要在每一步进行规划。

一种简单而有效的学习策略的方法是将基于模型的学习与无模型学习相结合。随着无模型算法的最新创新,这种结合方法越来越流行,成为迄今为止最常见的方法。我们将在下一节开发的算法——ME-TRPO,就是这种方法之一。让我们深入探讨这些算法。

一种有用的结合方式

如您所知,无模型学习具有良好的渐近性能,但样本复杂度较高。另一方面,基于模型的学习从数据的角度来看是高效的,但在处理更复杂任务时存在困难。通过结合基于模型和无模型的方法,有可能找到一个平衡点,在保持无模型算法高性能的同时,持续降低样本复杂度。

有很多方法可以将这两个领域结合起来,提出这样的方法的算法之间差异很大。例如,当模型已经给定(如围棋和国际象棋中的模型),搜索树和基于价值的算法可以相互帮助,从而更好地估算行动价值。

另一个例子是将环境和策略的学习直接结合到深度神经网络架构中,以便学习到的动态能够为策略的规划提供帮助。许多算法使用的另一种策略是使用学习到的环境模型生成额外的样本,以优化策略。

换句话说,策略是通过在学习到的模型中进行模拟游戏来训练的。这可以通过多种方式实现,但主要的步骤如下所示:

while not done:
    > collect transitions  from the real environment using a policy 
    > add the transitions to the buffer 
    > learn a model  that minimizes  in a supervised way using data in 
    > (optionally learn )

    repeat K times: 
        > sample an initial state 
        > simulate transitions  from the model using a policy 
        > update the policy  using a model-free RL

这个蓝图涉及两个循环。最外层的循环收集来自真实环境的数据用于训练模型,而最内层的循环中,模型生成的模拟样本用于使用无模型算法优化策略。通常,动态模型是通过监督学习方式训练,以最小化均方误差(MSE)损失。模型的预测越精确,策略就越准确。

在最内层的循环中,可以模拟完整的轨迹或固定长度的轨迹。实际上,为了减轻模型的不完美,后者选项可以被采用。此外,轨迹可以从包含真实转换的缓冲区中随机抽取初始状态,或从初始状态开始。前者在模型不准确时更为偏好,因为这可以防止轨迹与真实轨迹的偏差过大。为了说明这种情况,考虑以下图示。真实环境中收集到的轨迹为黑色,而模拟的轨迹为蓝色:

你可以看到,从初始状态开始的轨迹变得更长,因此,随着不准确模型的误差在随后的预测中传播,它们会更快地发散。

注意,你只进行主循环的一次迭代,并收集所有学习到的环境模型所需的数据也是可以的。然而,基于之前提到的原因,使用迭代数据聚合方法通过新的策略周期性地重新训练模型会更好。

从图像构建模型

到目前为止,结合基于模型和无模型学习的方法,特别设计用于处理低维状态空间。那么,如何处理高维观测空间(如图像)呢?

一种选择是学习潜在空间。潜在空间是高维输入(例如图像)的一种低维表示,也叫做嵌入,g(s)。它可以通过神经网络如自编码器生成。以下图示展示了自编码器的一个例子:

它包括一个编码器,将图像映射到一个小的潜在空间,g(s),以及解码器,将潜在空间映射回重建的图像。通过自编码器的作用,潜在空间应该在一个受限空间内表示图像的主要特征,使得两个相似的图像在潜在空间中也相似。

在强化学习中,自编码器可以被训练来重建输入,S,或者训练来预测下一个帧的观测值,S',(如果需要的话,还包括奖励)。然后,我们可以利用潜在空间来学习动态模型和策略。这个方法的主要好处是由于图像的表示更小,从而大大提高了速度。然而,当自编码器无法恢复正确的表示时,潜在空间中学到的策略可能会出现严重的缺陷。

高维空间的基于模型的学习仍然是一个非常活跃的研究领域。

如果你对从图像观测中学习的基于模型的算法感兴趣,Kaiser 的论文《基于模型的 Atari 强化学习》可能会引起你的兴趣(arxiv.org/pdf/1903.00374.pdf)。

到目前为止,我们已经从更具象征性和理论化的角度讨论了基于模型的学习及其与无模型学习的结合。虽然这些对理解这些范式是不可或缺的,但我们希望将其付诸实践。因此,事不宜迟,让我们专注于第一个基于模型的算法的细节和实现。

ME-TRPO 应用于倒立摆

许多变种的基于模型和无模型的算法存在于有用的组合部分的伪代码中。几乎所有这些变种都提出了不同的方式来处理环境模型的不足之处。

这是一个关键问题,需要解决以达到与无模型方法相同的性能。从复杂环境中学到的模型总是会有一些不准确性。因此,主要的挑战是估计或控制模型的不确定性,以稳定和加速学习过程。

ME-TRPO 提出了使用一个模型集来保持模型不确定性并正则化学习过程。这些模型是具有不同权重初始化和训练数据的深度神经网络。它们共同提供了一个更加稳健的环境通用模型,能够避免在数据不足的区域产生过拟合。

然后,从使用这些模型集模拟的轨迹中学习策略。特别地,选择用来学习策略的算法是信任域策略优化TRPO),该算法在第七章中有详细解释,标题为TRPO 和 PPO 实现

理解 ME-TRPO

在 ME-TRPO 的第一部分,环境的动态(即模型集)被学习。算法首先通过与环境的随机策略互动,,来收集转移数据集,。然后,这个数据集被用来以监督方式训练所有动态模型,。这些模型,,是通过不同的随机权重初始化并使用不同的小批量进行训练的。为了避免过拟合问题,从数据集中创建了一个验证集。此外,当验证集上的损失不再改善时,一种早停机制(在机器学习中广泛使用的正则化技术)会中断训练过程。

在算法的第二部分,策略是通过 TRPO 进行学习的。具体而言,策略是在从已学习模型中收集的数据上进行训练,我们也称之为模拟环境,而不是实际环境。为了避免策略利用单个学习模型的不准确区域,策略,,是通过整个模型集的预测转移来训练的,。特别地,策略是在由模型集中的随机选择的转移组成的模拟数据集上进行训练的,。在训练过程中,策略会不断被监控,一旦性能停止提升,训练过程就会停止。

最后,由这两个部分构成的循环会一直重复,直到收敛。然而,在每次新迭代时,都会通过运行新学习到的策略,,来收集来自实际环境的数据,并将收集到的数据与前几次迭代的数据集进行汇总。ME-TRPO 算法的简要伪代码总结如下:

Initialize randomly policy  and models 
Initialize empty buffer 

while not done:
    > populate buffer  with transitions  from the real environment using policy  (or random)
    > learn models  that minimize  in a supervised way using data in 

    until convergence: 
        > sample an initial state 
        > simulate transitions  using models  and the policy 
        > take a TRPO update to optimize policy 

这里需要特别注意的是,与大多数基于模型的算法不同,奖励并未嵌入到环境模型中。因此,ME-TRPO 假设奖励函数是已知的。

实现 ME-TRPO

ME-TRPO 的代码非常长,在这一部分我们不会给出完整的代码。此外,很多部分并不有趣,所有与 TRPO 相关的代码已经在第七章,TRPO 与 PPO 实现中讨论过。然而,如果你对完整的实现感兴趣,或者想要尝试算法,完整的代码可以在本章的 GitHub 仓库中找到。

在这里,我们将提供以下内容的解释和实现:

  • 内部循环,其中模拟游戏并优化策略

  • 训练模型的函数

剩下的代码与 TRPO 的代码非常相似。

以下步骤将指导我们完成构建和实现 ME-TRPO 核心的过程:

  1. 改变策略:与真实环境的交互过程中唯一的变化是策略。具体来说,策略在第一轮中会随机执行,但在接下来的轮次中,它会从一个标准差随机设定的高斯分布中采样动作,这个标准差在算法开始时就已固定。这个变化是通过用以下代码行替换 TRPO 实现中的act, val = sess.run([a_sampl, s_values], feed_dict=``{obs_ph:[env.n_obs]})来完成的:
...
if ep == 0:
    act = env.action_space.sample()
else:
    act = sess.run(a_sampl, feed_dict={obs_ph:[env.n_obs], log_std:init_log_std})
...
  1. 拟合深度神经网络, :神经网络通过前一步获得的数据集学习环境模型。数据集被分为训练集和验证集,其中验证集通过早停技术来判断是否值得继续训练:
...
model_buffer.generate_random_dataset()
train_obs, train_act, _, train_nxt_obs, _ = model_buffer.get_training_batch()
valid_obs, valid_act, _, valid_nxt_obs, _ = model_buffer.get_valid_batch()
print('Log Std policy:', sess.run(log_std))

for i in range(num_ensemble_models):
train_model(train_obs, train_act, train_nxt_obs, valid_obs, valid_act, valid_nxt_obs, step_count, i)

model_bufferFullBuffer类的一个实例,包含了由环境生成的样本,而generate_random_dataset则会创建用于训练和验证的两个数据集,之后通过调用get_training_batchget_valid_batch返回。

在接下来的代码中,每个模型都通过train_model函数进行训练,传递数据集、当前步骤数以及需要训练的模型索引。num_ensemble_models是集成中模型的总数。在 ME-TRPO 论文中,显示 5 到 10 个模型就足够了。参数i决定了集成中哪个模型需要被优化。

  1. 在模拟环境中生成虚拟轨迹并拟合策略
        best_sim_test = np.zeros(num_ensemble_models)
        for it in range(80):
            obs_batch, act_batch, adv_batch, rtg_batch = simulate_environment(sim_env, action_op_noise, simulated_steps)

            policy_update(obs_batch, act_batch, adv_batch, rtg_batch)

这一过程会重复 80 次,或者至少直到策略继续改进为止。simulate_environment通过在模拟环境中(由学习到的模型表示)执行策略来收集数据集(包括观察、动作、优势、值和回报值)。在我们的例子中,策略由函数action_op_noise表示,给定一个状态时,它返回一个遵循学习到的策略的动作。相反,环境sim_env是环境的一个模型,,在每一步中随机从集成中选择。传递给simulated_environment函数的最后一个参数是simulated_steps,它设定了在虚拟环境中执行的步数。

最终,policy_update函数执行一个 TRPO 步骤,利用在虚拟环境中收集的数据来更新策略。

  1. 实现早停机制并评估策略:早停机制防止策略在环境模型上过拟合。它通过监控策略在每个独立模型上的表现来工作。如果策略改善的模型所占比例超过某个阈值,则终止该周期。这应该能很好地指示策略是否已经开始过拟合。需要注意的是,与训练不同,在测试过程中,策略是一次在一个模型上进行测试的。在训练过程中,每条轨迹都是由所有学习过的环境模型生成的:
            if (it+1) % 5 == 0:
                sim_rewards = []

                for i in range(num_ensemble_models):
                    sim_m_env = NetworkEnv(gym.make(env_name), model_op, pendulum_reward, pendulum_done, i+1)
                    mn_sim_rew, _ = test_agent(sim_m_env, action_op, num_games=5)
                    sim_rewards.append(mn_sim_rew)

                sim_rewards = np.array(sim_rewards)
                if (np.sum(best_sim_test >= sim_rewards) > int(num_ensemble_models*0.7)) \
                    or (len(sim_rewards[sim_rewards >= 990]) > int(num_ensemble_models*0.7)):
                    break
                else:
                  best_sim_test = sim_rewards

策略评估在每五次训练迭代后进行。对于集成中的每个模型,都会实例化一个新的NetworkEnv类对象。它提供了与真实环境相同的功能,但在后台,它返回来自环境学习模型的过渡。NetworkEnv通过继承Gym.wrapper并重写resetstep函数来实现这一点。构造函数的第一个参数是一个真实环境,仅用于获取真实的初始状态,而model_os是一个函数,当给定一个状态和动作时,它会生成下一个状态。最后,pendulum_rewardpendulum_done是返回奖励和完成标志的函数。这两个函数围绕环境的特定功能构建。

  1. 训练动态模型train_model函数优化一个模型以预测未来的状态。这个过程非常简单易懂。我们在步骤 2 中使用了这个函数,当时我们正在训练多个模型的集成。train_model是一个内部函数,接受我们之前看到的参数。在外部循环的每次 ME-TRPO 迭代中,我们会重新训练所有模型,也就是说,我们从它们的随机初始权重开始训练模型;我们不会从之前的优化继续。因此,每次调用train_model并在训练开始之前,我们都会恢复模型的初始随机权重。以下代码片段在执行此操作之前恢复权重并计算训练前后的损失:
    def train_model(tr_obs, tr_act, tr_nxt_obs, v_obs, v_act, v_nxt_obs, step_count, model_idx):
        mb_valid_loss1 = run_model_loss(model_idx, v_obs, v_act, v_nxt_obs)

        model_assign(model_idx, initial_variables_models[model_idx])

        mb_valid_loss = run_model_loss(model_idx, v_obs, v_act, v_nxt_obs)

run_model_loss返回当前模型的损失,model_assign恢复initial_variables_models[model_idx]中的参数。

然后我们训练模型,只要在最后model_iter次迭代中验证集上的损失有所改善。但由于最佳模型可能不是最后一个模型,我们会追踪最佳模型,并在训练结束时恢复其参数。我们还会随机打乱数据集并将其分成小批次。代码如下:

        acc_m_losses = []
        last_m_losses = []
        md_params = sess.run(models_variables[model_idx])
        best_mb = {'iter':0, 'loss':mb_valid_loss, 'params':md_params}
        it = 0

        lb = len(tr_obs)
        shuffled_batch = np.arange(lb)
        np.random.shuffle(shuffled_batch)

        while best_mb['iter'] > it - model_iter:

            # update the model on each mini-batch
            last_m_losses = []
            for idx in range(0, lb, model_batch_size):
                minib = shuffled_batch[idx:min(idx+minibatch_size,lb)]

                if len(minib) != minibatch_size:
                  _, ml = run_model_opt_loss(model_idx, tr_obs[minib], tr_act[minib], tr_nxt_obs[minib])
                  acc_m_losses.append(ml)
                  last_m_losses.append(ml)

            # Check if the loss on the validation set has improved
            mb_valid_loss = run_model_loss(model_idx, v_obs, v_act, v_nxt_obs)
            if mb_valid_loss < best_mb['loss']:
                best_mb['loss'] = mb_valid_loss
                best_mb['iter'] = it
                best_mb['params'] = sess.run(models_variables[model_idx])

            it += 1

        # Restore the model with the lower validation loss
        model_assign(model_idx, best_mb['params'])

        print('Model:{}, iter:{} -- Old Val loss:{:.6f} New Val loss:{:.6f} -- New Train loss:{:.6f}'.format(model_idx, it, mb_valid_loss1, best_mb['loss'], np.mean(last_m_losses)))

run_model_opt_loss是一个函数,它执行具有model_idx索引的模型的优化器。

这就完成了 ME-TRPO 的实现。在下一节中,我们将看到它的表现。

在 RoboSchool 上进行实验

让我们在RoboSchool 倒立摆上测试 ME-TRPO,这是一种与著名的离散控制环境 CartPole 相似的连续倒立摆环境。RoboSchool 倒立摆-v1的截图如下:

目标是通过移动小车保持杆子直立。每当杆子指向上方时,都会获得+1 的奖励。

考虑到 ME-TRPO 需要奖励函数,因此也需要done函数,我们必须为此任务定义两者。为此,我们定义了pendulum_reward,无论观察和动作是什么,它都返回 1:

def pendulum_reward(ob, ac):
    return 1

pendulum_done当杆的角度绝对值大于固定阈值时返回True。我们可以直接从状态中获取角度。实际上,状态的第三和第四个元素分别是角度的余弦和正弦。然后,我们可以任意选择其中一个来计算角度。因此,pendulum_done如下所示:

def pendulum_done(ob):
    return np.abs(np.arcsin(np.squeeze(ob[3]))) > .2

除了 TRPO 的常规超参数外,这些超参数几乎与第七章中使用的保持不变,TRPO 与 PPO 实现,ME-TRPO 还要求以下超参数:

  • 动态模型优化器的学习率,mb_lr

  • 用于训练动态模型的最小批次大小,model_batch_size

  • 每次迭代中执行的模拟步数,simulated_steps(这也是用于训练策略的批次大小)

  • 构成集成的模型数量,num_ensemble_models

  • 如果验证结果没有下降,等待中断model_iter训练的迭代次数

在这个环境中使用的这些超参数值如下:

超参数
学习率(mb_lr1e-5
模型批次大小(model_batch_size50
模拟步数(simulated_steps50000
模型数量(num_ensemble_models10
提前停止迭代次数(model_iter15

RoboSchool 倒立摆的结果

性能图表如下所示:

奖励与与真实环境交互的步数之间的关系。经过 900 步和大约 15 场游戏后,智能体达到了 1000 的最佳性能。策略更新了 15 次,并从 750,000 个模拟步数中学习。从计算角度看,该算法在中端计算机上训练了大约 2 小时。

我们注意到,结果具有很高的变异性,如果使用不同的随机种子进行训练,可能会得到非常不同的性能曲线。这对于无模型算法也是如此,但在这里,差异更加明显。造成这种情况的一个原因可能是实际环境中收集的数据不同。

摘要

在本章中,我们暂时从无模型算法中休息,开始讨论和探索从环境模型中学习的算法。我们分析了激发我们开发这种算法的范式转变背后的关键原因。然后,我们区分了处理模型时可能遇到的两种主要情况:第一种情况是模型已知,第二种情况是模型需要被学习。

此外,我们学习了如何利用模型来规划下一步的动作或学习策略。选择使用其中之一没有固定的规则,但通常与动作和观察空间的复杂性以及推理速度相关。我们随后研究了基于模型和无模型算法的优缺点,并通过将无模型算法与基于模型的学习结合,加深了我们对如何用无模型算法学习策略的理解。这揭示了一种在高维观察空间(如图像)中使用模型的新方式。

最后,为了更好地掌握与基于模型的算法相关的所有材料,我们开发了 ME-TRPO。该方法通过使用模型集成和信任域策略优化来应对模型的不确定性,从而学习策略。所有模型都用于预测下一个状态,从而创建模拟的轨迹,基于这些轨迹学习策略。因此,策略完全基于环境的学习模型进行训练。

本章总结了关于基于模型学习的讨论,在下一章中,我们将介绍新的学习范式。我们将讨论通过模仿学习的算法。此外,我们将开发并训练一个代理,通过跟随专家的行为,能够玩 FlappyBird。

问题

  1. 如果你只有 10 局游戏时间来训练代理玩跳棋,你会选择基于模型的算法还是无模型的算法?

  2. 基于模型的算法有什么缺点?

  3. 如果环境的模型未知,如何学习它?

  4. 为什么要使用数据聚合方法?

  5. ME-TRPO 是如何稳定训练的?

  6. 使用模型集成如何改善策略学习?

进一步阅读

第十章:使用 DAgger 算法的模仿学习

算法仅通过奖励来学习的能力是一个非常重要的特性,这也是我们开发强化学习算法的原因之一。它使得代理可以从零开始学习并改进其策略,而无需额外的监督。尽管如此,在某些情况下,给定环境中可能已经有其他专家代理在工作。模仿学习IL)算法通过模仿专家的行为并从中学习策略来利用专家。

本章重点讲解模仿学习。虽然与强化学习不同,模仿学习在某些环境中提供了巨大的机会和能力,尤其是在具有非常大状态空间和稀疏奖励的环境中。显然,模仿学习只有在可以模仿的更专家代理存在时才可能进行。

本章将重点讲解模仿学习方法的主要概念和特性。我们将实现一个名为 DAgger 的模仿学习算法,并教一个代理玩 Flappy Bird。这将帮助你掌握这一新型算法,并理解其基本原理。

本章的最后部分,我们将介绍逆强化学习IRL)。IRL 是一种通过值和奖励来提取并学习另一个代理行为的方法;也就是说,IRL 学习奖励函数。

本章将涵盖以下主题:

  • 模仿方法

  • 玩 Flappy Bird

  • 理解数据集聚合算法

  • IRL

技术要求

在简要的理论介绍后,我们将实现一个实际的 IL 算法,帮助理解模仿学习算法背后的核心概念。然而,我们只会提供主要和最有趣的部分。如果你对完整实现感兴趣,可以在本书的 GitHub 仓库中找到: github.com/PacktPublishing/Reinforcement-Learning-Algorithms-with-Python

安装 Flappy Bird

接下来,我们将在一个重新设计的著名游戏 Flappy Bird 上运行我们的 IL 算法(en.wikipedia.org/wiki/Flappy_Bird)。在这一部分,我们将提供安装所需的所有命令。

但在安装游戏环境之前,我们需要处理一些额外的库:

  • 在 Ubuntu 中,步骤如下:
$ sudo apt-get install git python3-dev python3-numpy libsdl-image1.2-dev libsdl-mixer1.2-dev libsdl-ttf2.0-dev libsmpeg-dev libsdl1.2-dev libportmidi-dev libswscale-dev libavformat-dev libavcodec-dev libfreetype6-dev
$ sudo pip install pygame
  • 如果你是 Mac 用户,可以通过以下命令安装库:
$ brew install sdl sdl_ttf sdl_image sdl_mixer portmidi 
$ pip install -c https://conda.binstar.org/quasiben pygame
  • 然后,对于 Ubuntu 和 Mac 用户,步骤如下:
  1. 首先,你需要克隆 PLE。克隆可以通过以下代码行完成:
git clone https://github.com/ntasfi/PyGame-Learning-Environment

PLE 是一套环境,也包括 Flappy Bird。因此,通过安装 PLE,你将获得 Flappy Bird。

  1. 然后,你需要进入 PyGame-Learning-Environment 文件夹:
cd PyGame-Learning-Environment
  1. 最后,通过以下命令运行安装:
sudo pip install -e .

现在,你应该能够使用 Flappy Bird 了。

模仿方法

模仿学习(IL)是通过模仿专家来获得新技能的艺术。模仿学习这一特性对学习顺序决策策略来说并不是绝对必要的,但如今,它在许多问题中是不可或缺的。有些任务不能仅仅通过强化学习来解决,从复杂环境中的巨大空间中自举策略是一个关键因素。以下图表展示了模仿学习过程中涉及的核心组件的高层视图:

如果智能体(专家)已经存在于环境中,它们可以为新的智能体(学习者)提供大量关于完成任务和导航环境所需行为的信息。在这种情况下,新的智能体可以在不从零开始学习的情况下更快地学习。专家智能体还可以作为教师,指导并反馈给新的智能体其表现。注意这里的区别,专家既可以作为指导者来跟随,也可以作为监督者来纠正学生的错误。

如果能够提供引导模型或监督者,模仿学习算法可以利用它们。现在你可以理解为什么模仿学习如此重要,也明白为什么我们不能将其排除在本书之外。

驾驶助手示例

为了更好地理解这些关键概念,我们可以用一个青少年学车的例子来说明。假设他们从未坐过车,这是他们第一次看到汽车,而且他们对汽车的工作原理一无所知。学习有三种方法:

  1. 他们拿到钥匙后必须完全独立学习,完全没有监督。

  2. 在获得钥匙之前,他们需要坐在副驾驶座上观察专家驾驶 100 小时,了解在不同天气条件和道路上的驾驶情况。

  3. 他们观察专家驾驶,但更重要的是,他们有机会在专家驾驶时得到反馈。例如,专家可以实时指导如何停车,并对如何保持车道提出直接反馈。

正如你可能已经猜到的,第一个案例是强化学习方法,在这种方法中,智能体只有在不撞车、行人不对其大喊大叫等情况下才会获得稀疏的奖励。

至于第二个案例,这是一种被动的模仿学习方法,通过纯粹复制专家的行为来获得能力。总体而言,它与监督学习方法非常相似。

第三个也是最后一个案例是主动的模仿学习方法,形成了真正的模仿学习方法。在这种情况下,要求在训练阶段,专家对学习者的每一个动作进行指导。

比较模仿学习(IL)和强化学习(RL)

让我们通过强调模仿学习与强化学习之间的差异来更深入地了解模仿学习方法。这种对比非常重要。在模仿学习中,学习者并不意识到任何奖励。这一约束可能会带来非常大的影响。

回到我们的例子,学徒只能尽可能地模仿专家的动作,无论是被动的还是主动的。由于没有来自环境的客观奖励,他们只能受制于专家的主观监督。因此,即使他们想要改进,也无法理解和掌握老师的推理过程。

因此,模仿学习应被视为一种模仿专家动作的方式,但并不理解其主要目标。在我们的例子中,年轻的司机可能很好地模仿了老师的驾驶轨迹,但仍然不知道老师选择这些轨迹背后的动机。没有奖励的意识,经过模仿学习训练的代理无法像强化学习中那样最大化总奖励。

这突出了模仿学习和强化学习之间的主要区别。前者缺乏对主要目标的理解,因此无法超越老师。而后者则缺乏直接的监督信号,在大多数情况下,只能获得稀疏的奖励。这个情况在以下图示中得到了清晰的展示:

左侧的图表示通常的强化学习(RL)循环,而右侧则表示模仿学习(IL)循环。在这里,学习者不会获得任何奖励,只有专家提供的状态和动作。

模仿学习中专家的角色

在讨论模仿学习算法时,专家老师监督者这几个术语指的是相同的概念。它们表示一种可以让新代理(学习者)学习的角色。

从根本上讲,专家可以有各种形式,从真实的人类专家到专家系统。前者更为明显并且被广泛采用。你所做的事情是教算法执行一个人类已经能够做的任务。其优点显而易见,并且可以应用于大量的任务中。

第二种情况可能不太常见。选择新算法并用模仿学习进行训练的有效动机之一,可能是由于技术限制,一个缓慢的专家系统无法得到改进。例如,老师可能是一个准确但缓慢的树搜索算法,在推理时无法以合适的速度运行。这时,可以用深度神经网络来代替它。尽管在树搜索算法的监督下训练神经网络可能需要一些时间,但一旦训练完成,它在运行时的表现会更快。

到现在为止,应该很清楚,从学习者得到的策略质量在很大程度上取决于专家提供的信息质量。教师的表现是学者最终表现的上限。一个糟糕的老师总是会给学习者提供糟糕的数据。因此,专家是设定最终代理质量标准的关键组件。只有在教师强大的情况下,我们才能期望获得好的策略。

IL 结构

现在我们已经解决了模仿学习的所有要素,可以详细说明可以用于设计完整模仿学习算法的算法和方法。

解决模仿问题的最直接方法如下图所示:

前面的图示可以总结为两个主要步骤:

  • 专家从环境中收集数据。

  • 通过监督学习在数据集上学习一个策略。

不幸的是,尽管监督学习是模仿算法的典范,但大多数时候,它并不奏效。

为了理解为什么监督学习方法不是一个好的替代方案,我们必须回顾监督学习的基础。我们主要关注两个基本原则:训练集和测试集应该属于相同的分布,并且数据应该是独立同分布的(i.i.d.)。然而,一个策略应该能够容忍不同的轨迹,并对最终的分布变化具有鲁棒性。

如果一个代理仅通过监督学习方法来训练驾驶汽车,每当它从专家的轨迹中稍微偏离时,它将处于一个前所未见的新状态,这将导致分布不匹配。在这个新状态下,代理对下一步动作会感到不确定。在通常的监督学习问题中,这不会太影响。如果错过了一个预测,这不会对下一个预测产生影响。然而,在模仿学习问题中,算法正在学习一个策略,i.i.d.属性不再成立,因为后续的动作是严格相关的。因此,它们会对所有其他动作产生后果,并且有累积效应。

在我们自驾车的例子中,一旦分布从专家的分布发生变化,正确的路径将变得非常难以恢复,因为错误的动作会积累并导致严重后果。轨迹越长,模仿学习的效果越差。为了更清楚地说明,具有 i.i.d.数据的监督学习问题可以视为长度为 1 的轨迹。对下一步动作没有任何影响。我们刚才提出的范式就是我们之前提到的被动学习。

为了克服由于使用被动模仿而可能对策略造成灾难性影响的分布变化,可以采用不同的技术。有些是巧妙的黑客技术,而另一些则是更具算法变种的方式。以下是两种效果较好的策略:

  • 学习一个在数据上能很好地泛化而不发生过拟合的模型

  • 除了被动模仿,还可以使用主动模仿

因为第一个是更广泛的挑战,我们将集中精力在第二个策略上。

比较主动模仿和被动模仿

在前面的示例中,我们介绍了主动模仿这个术语,通过一个青少年学习开车的例子。具体来说,我们指的是在学习者在专家的额外反馈下进行驾驶的情境。一般来说,主动模仿是指从专家分配的动作中,通过策略数据进行学习。

从输入s(状态或观察)和输出a(动作)的角度来看,在被动学习中,s 和 a 都来自专家。在主动学习中,s 是从学习者那里采样的,a 是专家在状态 s 下应该采取的动作。新手代理的目标是学习一个映射,![]。

使用带有策略数据的主动学习可以让学习者修正那些仅靠被动模仿无法纠正的小偏差。

玩 Flappy Bird

在本章后续部分,我们将开发并测试一个名为 DAgger 的 IL 算法,应用在一个新的环境中。这个环境名为 Flappy Bird,模拟了著名的 Flappy Bird 游戏。在这里,我们的任务是为你提供所需的工具,帮助你使用这个环境实现代码,从接口的解释开始。

Flappy Bird 属于PyGame 学习环境PLE),这是一组模仿街机学习环境ALE)接口的环境。它类似于Gym接口,虽然使用起来很简单,稍后我们会看到它们之间的差异。

Flappy Bird 的目标是让小鸟飞过垂直的管道而不撞到它们。它只通过一个动作来控制,即让小鸟拍打翅膀。如果小鸟不飞,它会按照重力作用沿着下降轨迹前进。下面是环境的截图:

如何使用环境

在接下来的步骤中,我们将看到如何使用这个环境。

  1. 为了在 Python 脚本中使用 Flappy Bird,首先,我们需要导入 PLE 和 Flappy Bird:
from ple.games.flappybird import FlappyBird
from ple import PLE
  1. 然后,我们实例化一个FlappyBird对象,并将其传递给PLE,并传递一些参数:
game = FlappyBird()
p = PLE(game, fps=30, display_screen=False)

在这里,通过display_screen,你可以选择是否显示屏幕。

  1. 通过调用init()方法初始化环境:
p.init()

为了与环境交互并获得环境的状态,我们主要使用四个函数:

    • p.act(act),用来在游戏中执行act动作。act(act)返回执行该动作后获得的奖励。

    • p.game_over(),用于检查游戏是否达到了最终状态。

    • p.reset_game(),将游戏重置为初始状态。

    • p.getGameState(),用于获取当前环境的状态。如果我们想获取环境的 RGB 观察值(即整个屏幕),也可以使用p.getScreenRGB()

  1. 将所有内容整合在一起,一个简单的脚本可以设计成如下代码片段,用于让 Flappy Bird 玩五局。请注意,为了使其工作,您仍然需要定义返回给定状态下动作的get_action(state)函数:
from ple.games.flappybird import FlappyBird
from ple import PLE

game = FlappyBird()
p = PLE(game, fps=30, display_screen=False)
p.init()

reward = 0

for _ in range(5):
    reward += p.act(get_action(p.getGameState()))

    if p.game_over():
        p.reset_game()

这里有几个要点需要指出:

  • getGameState() 返回一个字典,其中包含玩家的位置、速度和距离,以及下一根管道和下下根管道的位置。在将状态传递给我们在此用get_action函数表示的策略制定者之前,字典会被转换为 NumPy 数组并进行标准化。

  • act(action) 如果不需要执行动作,则期望输入为None;如果鸟需要拍打翅膀飞得更高,则输入为119

理解数据集聚合算法

数据集聚合DAgger)是从演示中学习的最成功算法之一。它是一个迭代的策略元算法,在诱发的状态分布下表现良好。DAgger 最显著的特点是,它通过提出一种主动方法来解决分布不匹配问题,在这种方法中,专家教导学习者如何从学习者的错误中恢复。

经典的 IL 算法学习一个分类器,预测专家的行为。这意味着模型拟合一个由专家观察到的训练样本数据集。输入是观察值,输出是期望的动作。然而,根据之前的推理,学习者的预测会影响未来访问的状态或观察,违反了独立同分布(i.i.d.)假设。

DAgger 通过反复迭代从学习者中采样的新数据聚合管道,来处理分布的变化,并利用聚合的数据集进行训练。算法的简单示意图如下所示:

专家填充了分类器所使用的数据集,但根据迭代的不同,环境中执行的动作可能来自专家,也可能来自学习者。

DAgger 算法

具体来说,DAgger 通过迭代以下过程进行。在第一次迭代中,从专家策略创建一个轨迹数据集 D,并用它来训练一个最适合这些轨迹且不发生过拟合的初始策略 。然后,在迭代 i 中,使用学习到的策略 收集新的轨迹,并将其添加到数据集 D 中。接着,使用包含新旧轨迹的聚合数据集 D 来训练一个新的策略,

根据《Dagger 论文》中的报告(arxiv.org/pdf/1011.0686.pdf),有一种活跃的基于策略的学习方法,优于许多其他模仿学习算法,并且在深度神经网络的帮助下,它能够学习非常复杂的策略。

此外,在迭代 i 时,可以修改策略,使专家控制多个动作。该技术更好地利用了专家的能力,并让学习者逐渐掌控环境。

算法的伪代码可以进一步澄清这一点:

Initialize 
Initialize  ( is the expert policy)

for i :
    > Populate dataset  with . States are given by  (sometimes the expert could take the control over it) and actions are given by the expert 

    > Train a classifier  on the aggregate dataset 

DAgger 的实现

代码分为三个主要部分:

  • 加载专家推理函数,以便根据状态预测动作。

  • 为学习者创建计算图。

  • 创建 DAgger 迭代以构建数据集并训练新策略。

在这里,我们将解释最有趣的部分,其他部分留给你个人兴趣。你可以在书籍的 GitHub 仓库中查看剩余的代码和完整版本。

加载专家推理模型

专家应该是一个以状态为输入并返回最佳动作的策略。尽管如此,它可以是任何东西。特别是,在这些实验中,我们使用了一个通过近端策略优化(PPO)训练的代理作为专家。从原则上讲,这没有什么意义,但我们为学术目的采用了这一解决方案,以便与模仿学习算法进行集成。

使用 PPO 训练的专家模型已保存在文件中,因此我们可以轻松地恢复它并使用其训练好的权重。恢复图并使其可用需要三步:

  1. 导入元图。可以通过tf.train.import_meta_graph恢复计算图。

  2. 恢复权重。现在,我们需要将预训练的权重加载到刚刚导入的计算图中。权重已保存在最新的检查点中,可以通过tf.train.latest_checkpoint(session, checkpoint)恢复。

  3. 访问输出张量。恢复的图的张量可以通过graph.get_tensor_by_name(tensor_name)访问,其中tensor_name是图中张量的名称。

以下代码行总结了整个过程:

def expert():
    graph = tf.get_default_graph()
    sess_expert = tf.Session(graph=graph)

    saver = tf.train.import_meta_graph('expert/model.ckpt.meta')
    saver.restore(sess_expert,tf.train.latest_checkpoint('expert/'))

    p_argmax = graph.get_tensor_by_name('actor_nn/max_act:0') 
    obs_ph = graph.get_tensor_by_name('obs:0') 

然后,因为我们只关心一个简单的函数,它会根据状态返回专家动作,我们可以设计 expert 函数,使其返回该函数。因此,在 expert() 内部,我们定义一个名为 expert_policy(state) 的内部函数,并将其作为 expert() 的输出返回:

    def expert_policy(state):
        act = sess_expert.run(p_argmax, feed_dict={obs_ph:[state]})
        return np.squeeze(act)

    return expert_policy

创建学习者的计算图

以下所有代码都位于一个名为 DAgger 的函数内部,该函数接受一些超参数,我们将在代码中看到这些参数。

学习者的计算图非常简单,因为它的唯一目标是构建一个分类器。在我们的案例中,只有两个动作需要预测,一个是做不做动作,另一个是让小鸟拍翅膀。我们可以实例化两个占位符,一个用于输入状态,另一个用于真实标签,即专家的动作。动作是一个整数,表示所采取的动作。对于两个可能的动作,它们分别是 0(什么也不做)或 1(飞行)。

构建这样的计算图的步骤如下:

  1. 创建一个深度神经网络,具体来说,是一个具有 ReLU 激活函数的全连接多层感知器,在隐藏层使用 ReLU 激活函数,在最后一层使用线性激活函数。

  2. 对于每个输入状态,选择具有最高值的动作。这个操作通过 tf.math.argmax(tensor,axis) 函数完成,axis=1

  3. 将动作的占位符转换为 one-hot 张量。这是必要的,因为我们在损失函数中使用的 logits 和标签应该具有维度[batch_size, num_classes]。然而,我们的标签 act_ph 的形状是[batch_size]。因此,我们通过 one-hot 编码将它们转换为所需的形状。tf.one_hot 是 TensorFlow 用于执行这一操作的函数。

  4. 创建损失函数。我们使用 softmax 交叉熵损失函数。这是一个标准的损失函数,适用于具有互斥类别的离散分类问题,就像我们的情况一样。损失函数通过softmax_cross_entropy_with_logits_v2(labels, logits)在 logits 和标签之间进行计算。

  5. 最后,计算 softmax 交叉熵的平均值,并使用 Adam 优化器进行最小化。

这五个步骤在接下来的代码行中实现。

    obs_ph = tf.placeholder(shape=(None, obs_dim), dtype=tf.float32, name='obs')
    act_ph = tf.placeholder(shape=(None,), dtype=tf.int32, name='act')

    p_logits = mlp(obs_ph, hidden_sizes, act_dim, tf.nn.relu, last_activation=None)
    act_max = tf.math.argmax(p_logits, axis=1)
    act_onehot = tf.one_hot(act_ph, depth=act_dim)

    p_loss = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits_v2(labels=act_onehot, logits=p_logits))
    p_opt = tf.train.AdamOptimizer(p_lr).minimize(p_loss)

然后,我们可以初始化会话、全局变量,并定义一个函数 learner_policy(state)。该函数根据给定状态返回学习者选择的具有更高概率的动作(这与我们为专家所做的相同):

    sess = tf.Session()
    sess.run(tf.global_variables_initializer())

    def learner_policy(state):
        action = sess.run(act_max, feed_dict={obs_ph:[state]})
        return np.squeeze(action)

创建 DAgger 循环

现在是设置 DAgger 算法核心的时候了。该概要已经在 The DAgger algorithm 部分的伪代码中定义,但让我们更深入地了解它是如何工作的:

  1. 初始化由两个列表组成的数据集,Xy,其中存储访问过的状态和专家的目标动作。我们还初始化环境:
    X = []
    y = []

    env = FlappyBird()
    env = PLE(env, fps=30, display_screen=False)
    env.init() 
  1. 遍历所有 DAgger 迭代。在每次 DAgger 迭代的开始,我们必须重新初始化学习者的计算图(因为我们在每次迭代时都会在新的数据集上重新训练学习者),重置环境,并执行一系列随机动作。在每个游戏开始时,我们执行一些随机动作,以向确定性环境中添加随机成分。结果将是一个更强健的策略:
    for it in range(dagger_iterations):
        sess.run(tf.global_variables_initializer())
        env.reset_game()
        no_op(env)

        game_rew = 0
        rewards = []
  1. 通过与环境互动收集新数据。正如我们之前所说,第一次迭代中包含了专家,专家必须通过调用expert_policy来选择动作,但在后续迭代中,学习者会逐渐接管控制权。学习到的策略由learner_policy函数执行。数据集通过将当前游戏状态附加到X(输入变量),并将专家在该状态下会采取的动作附加到y(输出变量)来收集。当游戏结束时,游戏将重置,并将game_rew设置为0。代码如下:
        for _ in range(step_iterations):
            state = flappy_game_state(env)

            if np.random.rand() < (1 - it/5):
                action = expert_policy(state)
            else:
                action = learner_policy(state)

            action = 119 if action == 1 else None

            rew = env.act(action)
            rew += env.act(action)

            X.append(state)
            y.append(expert_policy(state)) 
            game_rew += rew

            if env.game_over():
                env.reset_game()
                np_op(env)

                rewards.append(game_rew)
                game_rew = 0

请注意,动作被执行了两次。这是为了将每秒的动作数量从 30 减少到 15,以符合环境的要求。

  1. 在汇总数据集上训练新策略。该流程是标准的。数据集被打乱并分成batch_size长度的小批次。然后,通过在每个小批次上运行p_opt进行多个训练周期(等于train_epochs),重复优化过程。以下是代码:
        n_batches = int(np.floor(len(X)/batch_size))

        shuffle = np.arange(len(X))
        np.random.shuffle(shuffle)
        shuffled_X = np.array(X)[shuffle]
        shuffled_y = np.array(y)[shuffle]

        ep_loss = []
            for _ in range(train_epochs):

                for b in range(n_batches):
                    p_start = b*batch_size
                    tr_loss, _ = sess.run([p_loss, p_opt], feed_dict=
                            obs_ph:shuffled_X[p_start:p_start+batch_size], 
                            act_ph:shuffled_y[p_start:p_start+batch_size]})

                    ep_loss.append(tr_loss)
        print('Ep:', it, np.mean(ep_loss), 'Test:', np.mean(test_agent(learner_policy)))

test_agent在几局游戏中测试learner_policy,以了解学习者的表现如何。

在 Flappy Bird 上的结果分析

在展示模仿学习方法的结果之前,我们想提供一些数据,以便你能将这些与强化学习算法的结果进行比较。我们知道这不是一个公平的比较(这两种算法在非常不同的条件下工作),但无论如何,它们强调了为什么当有专家可用时,模仿学习是值得的。

专家已经使用近端策略优化进行了大约 200 万步的训练,并且在大约 40 万步后,达到了约 138 的停滞分数。

我们在 Flappy Bird 上测试了 DAgger,使用了以下超参数:

超参数变量名
学习者隐藏层hidden_sizes16,16
DAgger 迭代dagger_iterations8
学习率p_lr1e-4
每次 DAgger 迭代的步数step_iterations100
小批次大小batch_size50
训练周期数train_epochs2000

下图展示了 DAgger 性能随步数变化的趋势:

横线代表专家所达到的平均表现。从结果来看,我们可以看到几百步就足以达到专家的表现。然而,与 PPO 训练专家所需的经验相比,这表示样本效率提高了大约 100 倍。

再次强调,这并不是一个公平的比较,因为方法处于不同的背景中,但它突出了一个事实:每当有专家时,建议你使用模仿学习方法(至少可以用来学习一个初始策略)。

IRL

IL 的最大限制之一在于它无法学习其他路径来达到目标,除了从专家那里学到的路径。通过模仿专家,学习者受到教师行为范围的限制。他们并不了解专家试图达成的最终目标。因此,这些方法只有在没有意图超越教师表现的情况下才有用。

IRL 是一种强化学习算法,类似于 IL,使用专家进行学习。不同之处在于 IRL 使用专家来学习其奖励函数。因此,IRL 并不是像模仿学习那样复制示范,而是弄清楚专家的目标。一旦奖励函数被学习,智能体便可以利用它来学习策略。

由于示范仅用于理解专家的目标,智能体不受教师动作的限制,最终可以学到更好的策略。例如,一个通过 IRL 学习的自动驾驶汽车会明白,目标是以最短的时间从 A 点到达 B 点,同时减少对物体和人员的损害。然后,汽车会自行学习一个策略(例如,使用 RL 算法),以最大化这个奖励函数。

然而,IRL 也存在许多挑战,这些挑战限制了其适用性。专家的示范可能并非最优,因此,学习者可能无法充分发挥其潜力,并可能陷入错误的奖励函数中。另一个挑战在于对学习到的奖励函数的评估。

摘要

在这一章中,我们暂时跳出了强化学习算法,探讨了一种新的学习方式——模仿学习。这一新范式的创新之处在于学习的方式;即结果策略模仿专家的行为。这个范式与强化学习的不同之处在于没有奖励信号,并且能够利用专家实体带来的丰富信息源。

我们看到,学习者所学习的数据集可以通过增加额外的状态-动作对来扩展,以增加学习者在新情况中的信心。这个过程叫做数据聚合。此外,新数据可以来自新学习的策略,在这种情况下,我们称之为基于策略的数据(因为它来自同一个已学习的策略)。这种将基于策略的状态与专家反馈结合的做法是一种非常有价值的方法,可以提高学习者的质量。

然后我们探索并开发了最成功的模仿学习算法之一,名为 DAgger,并将其应用于学习 Flappy Bird 游戏。

然而,由于模仿学习算法只是复制专家的行为,这些系统无法做得比专家更好。因此,我们引入了逆向强化学习,它通过推断专家的奖励函数来克服这个问题。通过这种方式,策略可以独立于教师来学习。

在下一章,我们将介绍一组用于解决顺序任务的算法;即进化算法。你将学习这些黑箱优化算法的机制和优势,从而能够在挑战性环境中采用它们。此外,我们将更深入地探讨一种名为进化策略的进化算法,并加以实现。

问题

  1. 模仿学习是否被认为是一种强化学习技术?

  2. 你会使用模仿学习来构建一个在围棋中无法击败的智能体吗?

  3. DAgger 的全名是什么?

  4. DAgger 的主要优点是什么?

  5. 在哪里你会使用逆向强化学习而不是模仿学习?

进一步阅读

  • 要阅读介绍 DAgger 的原始论文,请查看以下论文,将模仿学习和结构化预测归约为无悔在线学习arxiv.org/pdf/1011.0686.pdf

  • 想了解更多关于模仿学习算法的信息,请查看以下论文,模仿学习的全球概述arxiv.org/pdf/1801.06503.pdf

  • 想了解更多关于逆向强化学习的信息,请查看以下调查,逆向强化学习调查:挑战、方法与进展arxiv.org/pdf/1806.06877.pdf