文/泽白
本文基于 FlappyLearning 仓库,对 神经网络进化(Neuroevolution) 进行了初步了解,以便后续深入。选择这个仓库的原因是因为该仓库代码体积小(只有 game.js 和 Neuroevolution.js 两个脚本),没有外部依赖。并且作为一个游戏切入,会更有趣味性。
首先可以尝试下 FlappyLearning Demo,对整体结果有一个认识,然后可以了解下 神经网络进化(Neuroevolution) 的 wiki 概念,对该项目用到的神经网络模型有一个初步了解,对后续文章中提到的代码实现部分的理解,也会比较有帮助。
仓库整体实现可以用下方这个图(下文称为 图一)概括,通过 game.js 和 Neuroevolution.js 两个脚本实现。其中 game.js 负责游戏逻辑的实现,Neuroevolution.js 负责进化神经网络模型和遗传算法的实现。
game 脚本用来承载游戏逻辑代码,主要看图中左侧部分。其中划分了游戏场景中常用的 sprites,并且定义了一个 Game 对象作为导演,用来控制游戏的开始,运行以及重新来一局,游戏部分通过 canvas 来实现。首先,实例化 Neuroevolution 对象(图一的右上角 init 部分),其中定义了每代鸟的数量,以及网络模型中每一层的神经元数量。一般神经网络具有分层结构,实例化的 network 分别定义了输入层(input layer)、隐藏层(hidden layer)和输出层(output layer)中的神经元(neurons)数量,神经元也叫感知器,可以看Perceptrons - the most basic form of a neural network这篇文章的详细介绍,了解感知器这一概念。
图一的步骤1中实例化了 Game 对象并调用了 start 方法,此时通过 Neuvol 实例创造了第一代50个 network,每个 network 对应1只鸟,他们的权重值均是随机生成。
图一的步骤2即 update 方法中的逻辑,首先遍历每一只鸟,计算出当前鸟针对 netowrk 需要的输入(input),即两个参数。第1个参数是鸟的 y 坐标与 canvas 高度的比值,第2个参数是下一个上管道的 height 与 canvas 高度的比值,将两个参数作为入参传给 network compute,即图一的步骤3。
network 通过计算,将输出层(output layer)的结果返回,然后对该结果做简单的判断,得出该鸟是向上飞还是伴随重力自由降落。
network compute 方法中,首先将入参赋值给输入层(input layer),然后再传递给隐藏层(hidden layer),层层传递下去,将最终的结果放入输出层(output layer)并返回。这也是一种比较简单的神经网络拓扑结构:前馈网络,信号仅沿一个方向流动,上一层的 value 作为下一层的输入。
此时,已知了当前鸟下一帧是向上还是向下后,即可判断该鸟是否 dead,如果 dead 则将该鸟当前的 network 状态与当前游戏的 score 绑定,按 score 降序排列作为基因存起来(addGenome 方法),即图一的步骤4。
将当前存活的鸟遍历完毕,若依然有存活的鸟,则重复上述过程,否则重新开始新的一代,即图一的步骤5。
每一只鸟 dead 后都将触发 addGenome 方法,一局结束,则会存储50只鸟的 network,即下图中的右侧部分。
开始新的游戏即需要产生新的一代鸟,为了让下一代能够比前一代在游戏中得到更高的分数,这里的进化算法主要分为了三部分。第一部分:精英主义,即直接取前一代分数排名前十的基因,复制出新一代的10只鸟。第二部分:随机行为,取排名前十的基因,加上随机因子,产生新一代的10只鸟。第三部分:繁衍,优先取分数高的两个基因进行组合,加上随机因子作为突变,产生新一代中剩余的鸟。
经过进化算法一代代的迭代,network 的表现也会越来越符合预期,大概在第30代即可在游戏中无敌。issue 列表中也有人讨论如何优化算法,做到通过2代即达到了无敌。
学海无涯,借助这个仓库作为尝试,拓展自己深度学习的道路。