深度强化学习实践(原书第2版)
上QQ阅读APP看书,第一时间看更新

4.3 交叉熵方法在CartPole中的应用

这个示例的完整代码在Chapter04/01_cartpole.py,下面展示的是最重要的部分。模型的核心部分是有1个隐藏层的NN,带有整流线性函数(Rectified Linear Unit,ReLU)以及128个隐藏层神经元(128是任意设置的)。其他超参也基本是随机设置的,并没有调优过,因为这个方法本身鲁棒性很好,并且收敛得很快。

082-01

我们将常量放在文件的最上面,它们包含了隐藏层中神经元的数量、在每次迭代中训练的片段数(16),以及用来过滤“精英”片段的奖励边界百分位。这里使用70作为奖励边界,这意味着会留下按奖励排序后前30%的片段。

082-02

NN并没有什么特别之处,它将从环境中得到的单个观察结果作为输入向量,并输出一个数字作为可以执行的动作。NN的输出是动作的概率分布,所以一个比较直接的方式是在最后一层使用一个非线性的softmax。但是,在前面的NN中,我们不使用softmax来增加训练过程的数值稳定性。比起先计算softmax(使用了幂运算)再计算交叉熵损失(使用了对数概率),我们使用PyTorch的nn.CrossEntropyLoss类,它将softmax和交叉熵合二为一,能提供更好的数值稳定性。CrossEntropyLoss要求参数是NN中的原始、未归一化的值(也称为logits)。它的缺点是需要记得每次从NN的输出中获得概率分布时,都需加上一次softmax计算。

083-01

在这,我们定义了两个命名元组类型的帮助类,来自标准库中的collections包:

  • EpisodeStep:这会用于表示智能体在片段中执行的一步,同时它会保存来自环境的观察以及智能体采取了什么动作。在“精英”片段的训练中会用到它们。
  • Episode:这是单个片段,它保存了总的无折扣奖励以及EpisodeStep集合。

我们看一下用片段生成批的函数:

083-02

上述函数接受环境(Gym库中的Env类实例)、NN以及每个迭代需要生成的片段数作为输入。batch变量会在累积批(它是Episode实例的列表)的时候使用。我们也为当前的片段声明了一个奖励计数器以及一个步骤(EpisodeStep对象)列表。然后,重置了环境以获得第一个观察,创建一层softmax(用来将NN的输出转换成动作的概率分布)。准备部分结束了,可以开始环境循环了。

083-03

在每次迭代中,将当前的观察转换成PyTorch张量,并将其传入NN以获得动作概率分布。这里有几件事需要注意:

  • 所有PyTorch中的nn.Module实例都接受一批数据,对于NN也是一样的,所以我们将观察(在CartPole中为一个由4个数字组成的向量)转换成1×4大小的张量(为此,将观察放入单元素的列表中)。
  • 由于没有在NN的输出使用非线性函数,它会输出一个原始的动作分数,因此需要将其用softmax函数处理。
  • NN和softmax层都返回包含了梯度的张量,所以我们需要通过访问tensor.data字段来将其数据取出来,然后将张量转换成NumPy数组。该数组和输入一样,有同样的二维结构,0轴是批的维度,所以我们需要获取第一个元素,这样才能得到动作概率的一维向量。
084-01

既然有了动作的概率分布,只需使用NumPy的random.choice()函数对分布进行采样,就能获得当前步骤该选择的动作。然后,将动作传给环境来获得下一个观察、奖励以及片段是否结束的标记。

084-02

奖励被加入当前片段的总奖励,片段的步骤列表也添加了一个(observation, action)对。注意,保存的是用来选择动作的观察,而不是动作执行后从环境返回的观察。这些都是需要牢记的微小但很重要的细节。

084-03

这就是处理片段结束情况的方式(在CartPole的例子中,即使我们很努力了,如果木棒掉落,那么片段就结束了)。将结束的片段加入批中,保存总奖励(片段已经结束,已累积了所有的奖励)以及执行过的步骤。然后重置总奖励累加器,清空步骤列表。最后,重置环境以重新开始。

当片段的数量已经满足批的要求,用yield将它返回给调用者进行处理。我们的函数是一个生成器,所以每次执行yield时,控制权就转移到了迭代函数的外面,下次会从yield的下一行继续执行。如果你不熟悉Python的生成器函数,请参考Python文档(https://wiki.python.org/moin/Generators)。处理完成后,我们会清除batch

084-04

循环中的最后一个步骤(非常重要)是将从环境中获得的观察赋给当前的观察变量,然后,所有的事情都将无限重复——将观察传给NN,采样动作来执行,让环境处理动作,并且保存处理的返回结果。

在这个函数的逻辑处理中,要理解的一个非常重要的方面是:NN的训练和片段的生成是同时进行的。它们并不是完全并行的,但是每积累了足够(16)的片段后,控制权将转移到调用方,调用方会用梯度下降来训练NN。所以,每当yield返回时,NN都会稍微有点进步(我们希望是这样的)。

我们不需要同步数据,因为训练和数据生成都在同一个线程中执行,但是需要理解从NN训练到其使用之间的不停跳转。

好了,现在我们需要定义另外一个函数,然后就可以切换到训练循环了。

085-01

这个函数是交叉熵方法的核心——根据给定的一批片段和百分位值计算奖励边界,以用于过滤要用于训练的“精英”片段。为了获得奖励边界,我们将使用NumPy的percentile函数,该函数根据给定的值列表和百分位计算百分位的值。然后,再计算平均奖励用于监控。

085-02

然后,过滤片段。针对批中每个片段,检查其总奖励值是否高于边界,如果高于,则将其观察和动作添加到要训练的列表中。

085-03

这是该函数的最后一步,将“精英”片段中的观察和动作转换成张量,并返回一个4元素的元组:观察、动作、奖励边界,以及平均奖励。最后两个值只用来写入TensorBoard,以检验智能体的性能。

现在,还剩下最后一段主要由训练循环组成的代码,它将所有内容拼接在一起:

085-04

一开始,先创建所需的对象:环境、NN、目标函数、优化器,以及TensorBoard的SummaryWriter

有注释的那一行创建了一个监控器,将智能体的性能以视频的方式展现。

085-05

在训练循环中,我们迭代批(Episode对象列表),然后使用filter_batch函数过滤“精英”片段。返回的结果是观察集、执行的动作集、用于过滤的奖励边界和平均奖励。然后,将NN的梯度置为0并将观察集传给NN,获得动作的分数集。这些分数会被传给objective函数,计算NN的输出和智能体真正执行的动作之间的交叉熵。其中的思想是用已经获得较好分数的动作来强化NN。然后,计算损失梯度并让优化器调整NN。

086-01

循环的其余部分主要是监控进度。在控制台中,我们会打印迭代次数、损失、每一批的平均奖励以及奖励边界。我们也将一些值写入TensorBoard,以获取智能体学习效果图。

086-02

循环中最后会检查每一批片段的平均奖励。当它大于199时,停止训练。为什么是199?在Gym中,当最近100个片段的平均奖励大于195时,就可以认为CartPole环境已经被解决了,而我们的方法收敛得很快,通常100个片段就足够了。训练完备的智能体可以将木棒无限平衡下去(获得任意多的分数),但是CartPole的片段长度被限制在了200步(如果你看过CartPole环境的变量,就会发现TimeLimit包装器,它会在200步之后停止片段)。将这些都考虑进去之后,我们在批的平均奖励大于199之后停止训练,因为这已经很好地表明智能体知道如何像一个专家一样平衡木棒了。

我们开始第一次RL训练吧!

086-03

智能体通常用不了50批就能解决环境。我的实验表明在第25~45个片段内就能解决,学习效率非常高(记得,每一个批只需16个片段)。TensorBoard显示智能体不停地进步,基本上每一批都能提高奖励边界(部分时间段是下降的,但是大部分时间都是在提升的),如图4.3和图4.4所示。

087-01

图4.3 训练过程的平均奖励(左)和损失(右)

087-02

图4.4 训练过程的奖励边界

为了检查智能体的动作,可以通过去掉创建环境后面那一行的注释来让Monitor工作。

重启之后(可能需要通过xvfb-run提供一个虚拟的X11显示器),程序将创建一个mon目录并将记录的不同训练阶段的视频放入其中。

088-01

从输出可以看出,它将智能体的活动周期性地输出到不同的视频文件中,这可以让你了解智能体的行为是怎样的(见图4.5)。

089-01

图4.5 CartPole状态的可视化

我们暂停一下并想想发生了什么。NN已经学会如何只根据观察和奖励来与环境交互,而无须对观察值进行任何解释。环境可以不是带木棒的小车,它可以是以产品数量为观察,以赚到的钱为奖励的仓库模型。我们的实现并不依赖于环境的细节。这就是RL模型的迷人之处,下一节,我们将研究如何将完全相同的方法应用于Gym的另一个环境中。