确认运行慢的原因;排查迭代 10W 次效果反而变差原因

1 确认运行缓慢的原因

训练 QLearning 的时候,发现越往后速度越慢。直观地以为是 snake 的 head 基于随机数的方式在空余空间减少的情况下效率越来越低的问题。

因此从两个方面来做后续的事情:

  1. 查看大家的 snake head 的生成方法
  2. 了解 cProfile 来定位慢的确切原因

首先看的1,结果发现在 Github 里高 star 的,都是基于随机的方式。看来似乎问题不大啊?

特别是,在搜索 snake 的时候, 发下了 snakeviz 这个仓库,说是做 cProfile 文件的可视化的。于是暂时停下搜索的脚本,开始做 profile.

cProfile 是 Python 里做性能瓶颈分析的工具。使用实在是方便, 直接增加 -m cProfile -o xxx.out 就可以把耗时分析放到 xxx.out中了。 这个文件是一个二进制,官网上是搭配 pstats 库来用的。然而,显然这种会有第三方库来做一些 GUI 的支持。

搜索一下 python cProfile visualization, 想不到第一个就是 snakeviz, 还真是巧啊。使用也是太简单了!开箱即用。

耗时分析的结果大大出乎意料——竟然是 np.count_nonzero 这个函数累计耗时最大。这个操作是在打印 debug 信息时查看 QTable 填充率调用的。 虽然每次调用 np.count_nonzero 耗时不算长,但是因为调用太多,耗时达到了 360+s, 而整个程序也没跑到 400s.

这时有两个方法来避免:一个是增加一个 debug print 的 interval, 也就是降低计算频次。另一个就是在不需要打印的时候,避免计算这个。 两个方法并不冲突。考虑到当前日志级别是 INFO, debug 不会打出来,没必要调用;其次,增加 interval 又要增加参数。因此,采用第二种方法避免计算。

改动之后果然快了很多…… 因而,我也把 Q-Learning 的迭代轮次放到了 10W 次; 然而不幸的是,迭代轮次从 1万 增加到 10万:

  1. Q-Table 覆盖率,只从 14% -> 19%, 增幅不明显
  2. 更致命的,在 1.2 W 迭代之后,训练结果出现大幅下滑,分数基本都是0/1, 且在后面的近 9W 次迭代中没有变好

加载 10W 次训练出来的模型,立刻就撞墙了…… 感觉哪里出了问题。

这可又遇到了大麻烦。

2 排查 10W 次迭代效果反而变差的原因

最简单的,是不是出现了NaN ? 把 Q-Table 检查了下,并没有。

简单看下分数的分布:

  • 1475 / 20W 个格子 是正数,最大是48
  • 21575 / 20W 个格子 是负数,最小是 -533004
  • 其余都是 0

大部分都是负数,说白了负反馈太多。如果状态没有弄错,大概率,还是我们的 reward 设置有问题。

  • 对 step 持续增加但没有吃到 food 的惩罚项可能太大了,甚至超过了撞墙的惩罚

此外,

  • reward 是否应该加入: 基于 snake 到 food 距离来做 reward ? 之所以没加这个 reward

    1. 这个 reward 不是游戏本身有的,加入是否合适?
    2. 因为距离做了离散化,即使距离变近了,但离散后的距离可能是一样的,也就是状态一样,这个时候如果加入 reward,是否有问题?

      • 应该不会:
      • 虽然状态没变,但是 Action 是不同的, 那么靠近、远离的分数,可以让 QTable 学会减少离 food 的距离 —— 即使距离有离散,应该也是没问题的。
  • 当前的状态设计,是否有问题?

    1. 当前状态表示头部到上下左右障碍(自身、边界)的距离 和 食物的距离。
    2. 通过离散距离压缩的空间大小,理论上是必要的:否则,完全不压缩,空间太大了!

      • w * w * h * h * w * h, 不压缩就是这样的空间大小(当然精细化的话,上面的估计过大了,但量级不会差太远)
      • 压缩后,是否会因为位置变化但状态重叠,导致学坏了?
      • 如果遵循压缩逻辑,那离 food 的距离,是否可以直接压缩为 3 个值: 正、负、0 ? 只需要表示方位就基本足够了?
      • 到周边障碍的距离是否也可以压缩得更小? 不,保留一定的空间来区别距离更合理。

想来,应该还是 step 持续但没有吃到 food 带来的惩罚 是导致这个问题的直接原因,它让损失具有了一定的随机性, 破坏了 snake 基于 reward 变化向 food 正确靠拢的可行性。

然而,不加这个损失也不行——它会不停地打转导致死循环……

—— 发现个问题,我们之所以没有用“方向”作为状态,是因为方向已经隐含在到周围障碍的距离中了。但是,这个距离是否真的足够表达方向呢? 似乎也是不够的——假设 snake 在向右走,body 也在上面,那 left, up 都是 1,但其实这个 1 的含义是不同的。 因为当前 snake 的设置,是拒绝为行动的反方向响应的。所以,反方向的1,和靠近障碍的1,采取的行为所得到的反馈是不同的。这就会导致问题!

总结来看,感觉要再修正状态!

  1. 至少增加一个当前运行方向的轴向状态。(比直接增加方向,少2个状态;配合距离,应该足够了)
  2. 死循环的 reward, 不能那么严苛,不应该超过撞墙的 reward 的绝对值.

最后,可以加一下基于 snake 到 food 的距离的 reward 的尝试。理论上,应该可以让 snake 快速学会基础动作吧。不然,这也太难收敛了 —— 太傻了。这种 reward, 属于手工特征,我认为也是 OK 的。至少,通过这个 reward,我们可以快速验证当前的设置是否真的可行……