2.4 FrozenLake使用cross-entropy方法

FrozenLake是gym的另一个grid world环境。其环境简单的栅格地图,有四种栅格状态,分别用字母SFHG表示,下面是一个地图的例子:

SFFF (S: starting point, safe)
FHFH (F: frozen surface, safe)
FFFH (H: hole, fall to your doom)
HFFG (G: goal, where the frisbee is located)

智能体的环境是一个大小为4×4的栅格地图,可以向四个方向移动:上、下、左、右。智能体总是从左上位置开始,它的目标是到达网格的右下单元格。地图中有些特定位置是洞(Hole),如果智能体进入这些洞,则当前episode就结束了,且奖励为零。如果智能体到达目标栅格(Goal),那么它将获得的奖励为1.0,此时episode结束。

除此之外,FrozenLake环境还有很多不确定性因素,正如其名,智能体要在冰面上移动,移动时会打滑,所以智能体执行action后,其结果并不一定如action一样:它有33%的几率将滑到action的右边或左边。例如,如果智能体向左移动,则会有33%的概率实际是向上或向下移动,当然也会有33%的可能性就是向左移动。这种不确定性给训练带来了麻烦。

我们先看看FrozenLake的基本属性:

In [1]: import gym

In [2]: e = gym.make('FrozenLake-v1')

In [3]: e.observation_space
Out[3]: Discrete(16)

In [4]: e.action_space
Out[4]: Discrete(4)

In [5]: e.reset()
Out[5]: 0

In [6]: e.render()

SFFF
FHFH
FFFH
HFFG

可以发现,观测空间是离散的从0到15的数字,显然,这个数字代表智能体在网格中的位置。动作空间也是离散的从0到3的数字。根据之前控制CartPole的经验,我们也可以使用cross-entropy方法控制FrozenLake环境,只需稍微修改一下代码。最主要的改动是神经网络的输入,CartPole中神经网络的输入是一个状态向量,而FrozenLake的状态是一个从0到16的整数,但是在这个问题中最好不要直接将状态输入神经网络,而应该使用one-hot编码的状态(为什么要用one-hot编码,互联网上已有很多相关讨论)。为了尽可能减少代码的改动,我们使用gym中的ObservationWrapper类实现one-hot编码类。

class DiscreteOneHotWrapper(gym.ObservationWrapper):
    def __init__(self, env: gym.Env) -> None:
        super(DiscreteOneHotWrapper, self).__init__(env)
        assert isinstance(env.observation_space, gym.spaces.Discrete)
        shape = (env.observation_space.n, )
        self.observation_space = spaces.Box(0.0, 1.0, shape=shape, dtype=np.float32)

    def observation(self, observation):
        obs = np.copy(self.observation_space.low)
        obs[observation] = 1.0
        return obs

除此之外还要在main函数中用DiscreteOneHotWrapper类对环境进行处理:

env = DiscreteOneHotWrapper(gym.make('FrozenLake-v1'))

程序运行后在tensorboard中监控训练过程
2.4 FrozenLake使用cross-entropy方法
我们发现不论训练了多少batch,虽然loss下降了,但是智能体的reward_bound总是0,也就是说智能体啥都没学到。为什么cross-entropy算法在CartPole环境中表现良好,但在FrozenLake环境中表现很差呢。

我们需要更深入地研究这两种环境的奖励机制。

在CartPole中,每过一个timestep,智能体都会得到1.0的奖励,直到倒立摆落下的那一刻。所以,智能体平衡倒立摆的时间越长,得到的奖励就越多。由于智能体行为的随机性,不同的episode长度也不同,而不同的奖励服从正态分布。在选择奖励边界后,我们“淘汰”了表现不好的episode,并通过对“优秀”的episode进行训练获得更好的表现。就像下图一样
2.4 FrozenLake使用cross-entropy方法
而在FrozenLake的奖励机制是不同的。只有当我们达到目标(Goal)时,我们才会获得1.0的奖励,而这种奖励并不能说明episode有多棒。它是快速有效的还是瞎猫碰上死耗子一样走到Goal的?我们不知道。我们只知道他成功了。除此之外,episode的奖励分配也存在问题。只有两种奖励:0(失败)或1(成功)。显然,刚开始的时候“失败”占主导地位,所以,我们只考虑“优秀”的episode是不对的(注意在这个环境中“优秀”==“成功到达Goal”,显然智能体很难在随机的policy下到达终点)。这就是训练失败的原因。
2.4 FrozenLake使用cross-entropy方法
这个例子说明了交叉熵方法的局限性

  1. 每个episode必须是有限的,越短越好
  2. 每个episode的奖励应该有足够的区分度,以区分“优秀”和“差劲”的episode
  3. 智能体要么到达终点,要么正在前往终点,而不会在半路上嗝屁(比如半路上掉到洞里)

其实有很多种算法可以用来解决这些限制,但是现在我们专注于解决cross-entropy方法遇到的问题。我们可以做如下调整:

  1. 更大的batchsize: 在CartPole中,每个batch有16个episode就足够了,但FrozenLake至少需要100个以上episode才能获得一些成功的episode。
  2. 对reward施加discount factor:为了让episode的总奖励和它的长度呈正相关,并使reward多样化(而不是只有0和1),我们可以使 γ = 0.99 \gamma=0.99 γ=0.99或 γ = 0.9 \gamma=0.9 γ=0.9。增加了奖励分配的可变性,这有助于避免两极分化严重。
  3. 让“优秀”的episode多训练几次:在“CartPole”中,我们选取较好的episode进行训练,然后把它们扔掉,继续之后的随机采样。但是在FrozenLake中,优秀的episode是稀客,所以我们需要将它们保留下来,多训练几次。
  4. 降低学习率:这个没啥可说的,深度学习基本调参技巧。
  5. 延长训练时间:对于深度学习来说这也是基本技巧,但是针对这个问题本身是有实际意义的,由于好的episode太罕见了,为了获得更多的训练样本就需要采样更多的episode(由此可见训练效率之低下)。

对于代码的调整,我们需要添加一个函数计算折扣奖励并保存历史记录中优秀的episode:

def batch_filter(batch, percentile):
    reward_list = list(map(lambda episode: episode.rewards, batch))
    reward_bound = np.percentile(reward_list, percentile)
    reward_mean = np.mean(reward_list)

    train_observation = []
    train_action = []

    for rewards, steps in batch:
        if rewards < reward_bound:
            continue
        train_observation.extend(map(lambda step: step.observation, steps))
        train_action.extend(map(lambda step: step.action, steps))

    return torch.tensor(train_observation), torch.tensor(train_action), reward_bound, reward_mean

在训练时,保存之前优秀的episode,把它传递给下一次迭代:

    for iter_no, batch in enumerate(batch_iterator(env, net, BATCH_SIZE)):
        reward_mean = np.mean(list(map(lambda step: step.rewards, batch)))
        full_batch, observation, action, reward_bound = batch_filter_with_gamma(full_batch + batch, PERCENTILE, GAMMA)
        if not full_batch:
            continue
        full_batch = full_batch[-500:]

再就是之前提到的降低学习率和增大batch_size。在tensorboard中监控训练过程:

经过一段时间的耐心等待,可以看到,模型的训练在成功率在55%左右停止了改进。有一些深度学习的技巧可以解决这个问题(例如,正则化),我们后面会讨论这些技术。

FrozenLake还有一个没有随机滑动的版本,也就是说智能体执行什么action就会产生相应的结果。只需要更改相应参数即可:

env = gym.make('FrozenLake-v1', is_slippery=True)

这时候训练速度会大大提高,没有了环境随机性的影响,智能体的表现也变得很好了。
2.4 FrozenLake使用cross-entropy方法

上一篇:RIDE: REWARDING IMPACT-DRIVEN EXPLORATION FOR PROCEDURALLY-GENERATED ENVIRONMENTS


下一篇:D435i相机获取某一点深度图像的深度值(ROS实现以及官方API调用)