《Flappy Bird》相信大家都玩过或者看过,这款游戏在2014年火遍全球。其操作非常简单,只需要点击屏幕,让主角小鸟顺利地穿过水管之间的缝隙而不碰触任何障碍物。小鸟穿过的水管越多,得到的分数也就越高。
今天我们也来玩一玩这个游戏,不过我们使用强化学习的算法来让主角小鸟自己学会穿过水管躲避障碍,进而魔改环境,制作特殊的三人环境,让游戏进阶为《Flappy Paddle》。别愣着,看下去,看完我们一起划船。
学习这篇文章,你可以做出下面视频中的效果。这里在红黑两支队伍被淘汰之后,结束了录制。因为蓝色的算法可以跑很久,这里只是作为展示,所以没有继续录下去。
飞桨有众多方便好用的开发工具套件,其中PARL就是在强化学习方向的一个高性能、灵活的框架,目前已经在Github上开源。PARL支持大规模并行计算,同样提供了算法的可复现性保证。PARL的框架逻辑清晰,容易上手,从Model到Algorithm再到Agent,逐步构建智能体。同时PARL也提供了一些经典的强化学习算法代码示例,如PG、DDPG、A2C等,方便开发者的调研和验证。不仅如此,PARL还提供了比较完善的算法基类,这使得PARL的扩展性也很好,开发更为轻松快捷。
在我们这个项目中,使用的就是PARL这个开发工具套件。PARL的仓库,针对很多经典的强化学习方法也提供了对应的例子。本项目使用的DQN方法,也是在PARL的实现上的变化。
环境解析
对于强化学习问题,一般是智能体(Agent)和环境(Environment)的一个交互问题。智能体需要对环境或部分环境做出观测(Observation),并根据环境做出动作(Action),而环境对这个动作做出奖惩(Reward)。
我们从《Flappy Bird》这个小游戏开始。我们使用PyGame-Learning-Environment这个环境,你可以在Github上轻松的找到这个仓库。下面来分析一下上述的几个元素。
对于观测值,我们可以通过getGameState函数得到一个观测字典,其中包含了8个字段,包括了玩家(游戏里是一个小鸟)的坐标信息、速度信息、玩家距离下一水管和再下一根水管的位置信息。当然你也可以直接使用getScreenRGB函数得到画面,并以它为观测值。这里为了简单操作,我们以观测字典为例。同时,我们也能发现这个观测值是连续的。
对于动作,我们可以通过getActionSet函数得到环境所支持的动作。在《Flappy Bird》这个游戏里,只有两个动作:1、点击屏幕让小鸟展翅高飞,2、什么都不做让小鸟自由滑翔。由此我们可以知道环境接受的动作是离散有限的。
在奖惩方面,环境是这样定义:reward = 当前帧的总分 - 前一帧的总分。总分的变化有两种情况:1、玩家通过管子,得一分。2、玩家撞天花板、地板,管子则游戏失败,扣五分。
算法选择
前一小节中,我们发现环境的观测是连续的,环境接受的动作是离散有限的。对于这种情况,可以选用Deep Q-Network(以下简称DQN)或是Policy Gradient(以下简称PG)。DQN作为查表法的扩展,把观测值从有限离散扩展到了连续空间,PG也有处理连续空间观测值的能力。两者的区别在于,DQN对观测值对应的每个动作计算Q值,并选择相应的动作;而PG则直接给出动作,省略了中间步骤。
那在这个任务中应该选择DQN还是PG呢?笔者两者都尝试了,而DQN可以轻松的训练出不错的效果,而PG却不能。我觉得可以从以下角度分析。DQN针对每一个观测的每一个动作做出评价,也就是说每一个动作都会有其价值。而在训练PG的时候,需要先跑完一个episode,然后将奖惩回传到这局游戏中的每一个动作上。
对于这个过程中的每一个动作,在这个任务中这种回传不是一个好的反馈方法。举一个例子:现在玩家在第五根管子前,真正影响面对第五根管子时动作的,是经过第四根管子之后的动作(以及第五根第六根的管子的位置,这属于观测值)。而更早之前的经过第一根、第二根、第三根管子的动作是不影响经过第五根管子的,那么这个奖惩回传的方法在这个任务中就很有问题。
搭建DQN及训练
这里再简单的介绍一下DQN。DQN作为Q-learning在连续观测值上的扩展,使用网络来代替传统的表格,增加了泛化的能力。DQN的训练和Q-learning一样,不断的让Q(s,a)逼近TargetQ=r+γmaxQ(s’,*)。这里利用的是神经网络的拟合能力。由于TargetQ在不断变化,DQN中使用了固定Q目标的方法,让算法更新更为平稳。除此之外,DQN中还使用了经验池的方法,提高了样本的利用率。同时这一机制也可以用来打散数据,消除样本之间的关联性。
鉴于PARL清晰的框架结构和完整的基类,我们构建Agent也更加容易。按照先model,再Algorithm,最后定义Agent的步骤来。这个项目的代码都是基于PARL中的DQN的例子的。
这里附上样例链接:https://github.com/PaddlePadd...
首先我们简单地设计一个包含三个隐层的网络,在PARL中的model需要继承parl.Model这样的基类。
class Model(parl.Model):
def __init__(self, act_dim):
hid0_size = 64
hid1_size = 32
hid2_size = 16
self.fc0 = layers.fc(size=hid0_size, act='relu', name="fc0")
self.fc1 = layers.fc(size=hid1_size, act='relu', name="fc1")
self.fc2 = layers.fc(size=hid2_size, act='relu', name="fc2")
self.fc3 = layers.fc(size=act_dim, act=None, name="fc3")
def value(self, obs):
h0 = self.fc0(obs)
h1 = self.fc1(h0)
h2 = self.fc2(h1)
Q = self.fc3(h2)
return Q
有一点动态图构建模型的感觉是不是?所以在模型方面你可以有更多的想法和设计,例如我还设计了以下这种模型:
class catModel(parl.Model):
def __init__(self, act_dim):
hid0_size = 64
hid1_size = 32
hid2_size = 16
self.fc0 = layers.fc(size=hid0_size, act='relu', name="catfc0")
self.fc1 = layers.fc(size=hid1_size, act='relu', name="catfc1")
self.fc2 = layers.fc(size=hid2_size, act='relu', name="catfc2")
self.fc3 = layers.fc(size=act_dim, act=None, name="catfc3")
def value(self, last_obs, obs):
oobs = fluid.layers.concat(input=[last_obs, obs], axis=-1, name='concat')
h0 = self.fc0(oobs)
h1 = self.fc1(h0)
h2 = self.fc2(h1)
Q = self.fc3(h2)
return Q
可以看出来,这里是将last_obs和obs直接concat到一起作为全连接层的输入。这里的last_obs,是上一帧的观测值,obs是当前帧的观测值。也许这种模型的效果并不会更好,但仍是一个值得尝试的想法。
接下来是algorithm,PARL中已有DQN的实现,我们直接使用PARL中提供的DQN类。像样例中一样,我们直接import算法就可以。
from parl.algorithms import DQN
当然这种写法并不适合于我刚才的第二种做法,因为第二种方法的value函数,接受的是last_obs, obs两个参数。所以这里你可以继承基类DQN或是直接重构一个。放心,有了PARL提供的样例,这个过程会非常的简单。基本上重写predict和learn两个成员函数就好。这两个函数也是之后“暴露”给Agent使用的。
predict函数用来拿到模型的输出,也就是所谓的Q值。而learn函数则是根据模型的输出和Agent拿到的经验数据去构建模型的cost,并使用优化器来最小化它,从而达到训练模型的目的。
最后是Agent,如果你使用的是我刚才第一种model,那么你可以直接使用样例中Agent的定义,但如果你使用了第二种,那当然也要修改对应的build_program、sample、predict、learn几个成员函数以能够成功的构建模型并调用Algorithm定义的函数。
接下来就可以训练我们的模型了,大概几百个episode之后,我们的Agent就能够拿到正的分数(其实这个时候,分值已经超过5分了)
修改贴图资源,制作三人环境
现在让我们来划船吧,其实最简单的就是替换一下贴图资源,在PyGame-Learning-Environment
/ple/games/flappybird/assets文件夹中。把这个小bird换成我们的划船选手~
但是一个队伍划船总有一些孤单,能不能让多个Agent在同一环境下一起“比赛”呢?与其说把环境写“死”,每次读取来评判不同的Agent,不如就让他们在同一环境下一起出发,这种方式更加直观。
这个地方需要修改的是PyGame-Learning-Environment/ple/games/flappybird下的__init__.py文件,这个文件中定义了整个游戏的逻辑。
这里就不更具体的说了,因为涉及的更多的是pygame的知识。__init__.py中需要修改的地方大概有:
初始化定义三个player。
为每个player添加score和live属性及每个player对应的得分和死亡处理,以及游戏的score和结束条件。
设计新的actionset, 以能接受三个输入(实际上是一个输入包含三个Agent的三个action)。
设计新的observation。在此之前只返回一个观测值,但现在要针对每个player返回其对应的观测值。
图像绘制。在原来的基础上多绘制两个player。
在仓库中提供了修改好了__init__.py以及图像资源,提供了一些设计环境的想法。
成果
结果已经展示在文章的开篇视频中。这里训练了三个模型,两个隐层的模型,拿到了均分147分;拼接的模型,拿到了157分,而三个隐层的模型,则拿到了2000分左右的平均成绩。当然,针对不同的参数量的模型应该有对应的学习率等超参数层面的调整,这里仅是为了展示,并没有在这方面做更多的探索和优化。
总结
那么在哪里能学到以上酷炫又有趣的知识呢?AI Studio上现有一门课程:《强化学习7日打卡营-世界冠军带你从零实践》,通过学习,你可以对强化学习有一个初步的了解,学到Q-learning、Sarsa、DQN、Policy Gradient等。几个清晰有趣的案例和作业,在充满趣味的同时,加强对算法和代码实现的理解。当然,也可以和我一样扩展思路,魔改环境,开发更多有趣又有技术的项目。
视频预览 :https://www.bilibili.com/vide...
AI Studio项目链接 : https://aistudio.baidu.com/aist