4.4 交叉熵方法在FrozenLake中的应用
我们要用交叉熵方法解决的下一个环境是FrozenLake。它是所谓的网格世界类型的环境,智能体被限制在4×4的网格中时,可以朝4个方向移动:上、下、左、右。如图4.6所示,智能体总是从左上角开始,它的目标是到达网格的右下角。在某些固定的单元格中有洞,如果智能体掉入洞中,片段就结束了并且奖励为0。如果智能体到达了目标单元格,则获得1.0的奖励并且片段结束。
图4.6 FrozenLake环境
为了增加复杂性,假设整个世界都是光滑的(毕竟这是一个结冰的湖面),所以智能体执行的动作不总如预期那样发生——有33%的概率,它会滑到左边或右边。例如,如果希望智能体向左移动,则它有33%的概率确实向左移动,有33%的概率会停在目标单元格的上面,有33%的概率则会停在目标单元格的下面。本节末尾将会显示,这使智能体很难进步。
我们来看该环境在Gym中的表示:
观察空间是离散的,这意味着它就是从0到15(包含0和15)的数字。很明显,这个数字是智能体在网格中的当前位置。动作空间同样是离散的,它可以是从0到3的数字。神经网络来自CartPole示例,需要一个数字向量。为了获得这样的向量,可以对离散的输入进行传统的独热(one-hot)编码,这意味着网络的输入会是16个浮点数,除了要编码的位置是1外,其他位置都是0。使用Gym中的ObservationWrapper
类可以减少代码的修改量,因此可以实现一个DiscreteOneHotWrapper
类:
通过将此包装器应用于环境,观察空间和动作空间已经都100%兼容CartPole了(源代码见Chapter04/02_frozenlake_naive.py
)。然而,运行之后,我们发现它的分数并没有随着时间的推移而提升,如图4.7和图4.8所示。
图4.7 FrozenLake环境的奖励(左)和损失(右)
图4.8 训练过程的奖励边界
要理解发生了什么,我们需要深入研究两个环境的奖励结构。在CartPole中,木棒掉落前的每一步环境都会返回一个1.0的奖励。所以智能体平衡木棒越久,它能获得的奖励就越多。由于智能体行为的随机性,不同的片段有不同的长度,这将产生一个片段奖励的正态分布。通过选择奖励边界,过滤掉不太成功的片段,学习如何复制成功的片段(用成功的片段数据来训练),如图4.9所示。
图4.9 CartPole环境的奖励分布
在FrozenLake环境中,片段和它们的奖励有些不同。只有在到达终点的时候才会获得1.0的奖励,并且这个奖励并不能表示出片段有多好。它是快速并高效的吗?还是说在湖上转了4圈后才偶然进入最终单元格的?我们并不知道这些,只有一个1.0的奖励,仅此而已。片段的奖励分布也是有问题的。只有两种可能的片段(见图4.10):奖励是0的(失败了)以及奖励是1的(成功了),失败的片段显然会在训练的一开始占主导地位。所以,“精英”片段的百分位完全选错了,只提供了一些错误的样本来训练。这就是训练失败的原因。
图4.10 FrozenLake环境的奖励分布
这个示例展示了交叉熵方法存在的限制:
- 对于训练来说,片段必须是有限的、优秀的、简短的。
- 片段的总奖励应该有足够的差异来区分好的片段和差的片段。
- 没有中间值来表明智能体成功了还是失败了。
本书后面会介绍其他能解决这些限制的方法。就目前而言,如果你好奇如何用交叉熵方法解决FrozenLake环境,请参阅对代码的改变(完整示例代码见Chapter04/03_frozenlake_tweaked.py
):
- 每批包含更多的片段:在CartPole中,每个迭代只用16个片段就足够了,但是FrozenLake需要起码100个片段,才能获得一些成功的片段。
- 对奖励使用折扣系数:为了让总奖励能考虑片段的长度,并增加片段的多样性,可以使用折扣因子是0.9或0.95的折扣总奖励。在这种情况下,较短的片段将比那些较长的片段得到更高的奖励。这将增加奖励分布的多样性,有助于避开图4.10所描述的情况。
- 让“精英”片段保持更长的时间:在CartPole训练中,我们从环境中采样片段,训练最好的一些片段,然后将它们丢掉。在FrozenLake环境中,成功的片段十分珍贵,所以需要将它们保留并训练好几次迭代。
- 降低学习率:这给NN机会来平均更多的训练样本。
- 更长的训练时间:由于成功片段的稀有性以及动作结果的随机性,NN会更难决定在某个特定场景下应该执行什么动作。要将片段的成功率提升至50%,起码需要训练5000次迭代。
要将这些内容都合并到代码中,需要改变filter_batch
函数,用它来计算折扣奖励并返回需要保留下去的“精英”片段:
然后,在训练循环中,需要存下之前的“精英”片段,并将它们传给下一次训练迭代的处理函数。
除了学习率降低10倍以及BATCH_SIZE
设置成100,剩下的代码都一样。耐心地等待一小会儿(新版本的代码需要花1个半小时才能完成1万次迭代),当能解决55%的片段时,再训练模型就不会提升了,训练结果如图4.11和图4.12所示。虽然处理这个问题还是有其他办法的(例如,为熵损失增加一个正则项),但是这些方法会在后面的章节再讨论。
图4.11 调整后训练的奖励(左)和损失(右)
图4.12 调整后的奖励边界
FrozenLake环境中值得注意的最后一点是它的光滑度所带来的影响。每个动作都有33%的概率被替换成一个转动了90°的动作(例如,“向上”的动作会有33%的概率是正常的,有33%的概率被替换成“向左”,还有33%的概率被替换成“向右”)。
不光滑的版本见Chapter04/04_frozenlake_nonslippery.py
,唯一的不同之处就在环境创建处(需要看一下Gym的源码,然后用调整后的参数来创建环境的实例)。
效果惊人(见图4.13和图4.14)!只需要120~140个批就能解决不光滑版本的环境,这比有噪声的环境要快100倍:
图4.13 不光滑版本的FrozenLake的奖励(左)和损失(右)
图4.14 不光滑版本的奖励边界