我正在参加掘金社区游戏创意投稿大赛个人赛,详情请看:游戏创意投稿大赛
前言
说起 Flappy Bird 大家可能并不陌生,这个由越南独立游戏开发者阮河东(Dong Nguyen)开发的休闲小游戏,自 2013 年面市至今已在全球范围内拦获了超 5000 万的下载量,可谓家喻户晓。
虽然 Flappy Bird 游戏规则简单:控制小鸟上下移动,使其尽可能多得通过一个个障碍物,但想必玩过的人都知道,要想操控好这只小鸟,顺利的在各个管道之间穿梭,还是颇为困难的。
下面,本文将从神经网络和遗传变异两方面展开,探索如何让机器从零开始学会操控小鸟,并取得高分。
问题分析
首先,我们从一只鸟的情况开始看起。
结合文章开头的 GIF 图片分析可知:
- 每个时刻,玩家对鸟有两种动作:
啥都不做让鸟往下落
或点击屏幕让鸟向上飞
- 每个时刻,鸟只需要关心下一个通过的障碍物缺口位置,避免撞上管道
- 鸟可以感知到自己距离地面和下一障碍物的距离,以及管道缺口位置和长度
- 每个管道缺口的长度是相同的
- 管道一直在向左匀速运动
- 鸟撞上管道就会死亡
- 鸟存活的时间越长分数越高
转化成机器可以理解的数学模型就是:
根据输入条件(鸟距离地面和下一障碍物的距离,以及管道缺口位置等)不断计算决策输出(鸟的两个动作:
往下落
或向上飞
),使鸟获得更高的分数。
这是一个典型的人工神经网络案例,我们可以使用 Tensorflow 快速搭建一个人工神经网络模型进行求解(炼丹)。
神经网络
上图给出了一个典型的人工神经网络的示意图,关于什么是人工神经网络以及它的实现原理,iykyk,此处不再赘述。
网络结构
// 神经网络
class NeuralNetwork {
// 输入层节点数
inputNodes: number;
// 隐藏层节点数
hiddenNodes: number;
// 输出层节点数
outputNodes: number;
model: tf.Sequential;
// 模型结构
createModel() {
const model = tf.sequential();
const hidden = tf.layers.dense({
units: this.hiddenNodes,
inputDim: this.inputNodes,
activation: "sigmoid",
});
model.add(hidden);
const output = tf.layers.dense({
units: this.outputNodes,
activation: "softmax",
});
model.add(output);
return model;
}
// 预测
predict(inputs: number[]) {
const xs = tf.tensor2d([inputs]);
const ys = this.model.predict(xs) as tf.Tensor<tf.Rank>;
return ys.dataSync();
}
}
控制决策
class Bird {
constructor(brain?: NeuralNetwork) {
if (brain) {
this.brain = brain.copy();
} else {
this.brain = new NeuralNetwork(4, 8, 2);
}
}
think() {
let inputs = [];
// 鸟距地高度
inputs[0] = this.y / height;
// 下一障碍物缺口位置纵坐标
inputs[1] = this.topNextObstacle.y / height;
// 下一障碍物缺口位置横坐标
inputs[2] = this.bottomNextObstacle.x / width;
// 鸟的速度
inputs[3] = this.velocity / 10;
// 根据当前状态作为输入,预测输出下一步的操作
let output = this.brain.predict(inputs);
if (output[0] > output[1]) {
// 是否向上飞
this.goUp();
}
}
}
注意在输入时,所有的参数都进行了归一化处理,使各特征变量缩放到相近的区间范围。
遗传变异
为了使模型快速收敛,选择一个好的遗传变异算法可以事半功倍。
我们知道生物在繁衍过程中,会将自己的一些特征遗传给子代,比如肤色,身高等,另外子代基因也是父代双亲基因交叉变异的结果,如此一来,子代除了可以继承父代优秀的基因,也可以通过局部突变,产生自己独一无二的基因与特征。而这也正是生物界上亿年来,一直繁衍昌盛至今的基础保障。
另外,物竞天择,适者生存,长江后浪推前浪,一代更比一代强,新的世代应该总是比旧的世代更加优秀,适应能力更强,这也意味着旧的世代也在一点点被环境所淘汰。
基于上述生物界的生物繁衍规律,我们可以仿照构建一个支持交叉变异的遗传算法。
var Neuroevolution = function (options) {
var self = this;
self.options = {
// 每轮生成的小鸟数
population: 100,
// 保留上代表现最好的小鸟占比
elitism: 0.2,
// 每代产生的全新基因小鸟占比
randomBehaviour: 0.2,
// 变异率
mutationRate: 0.1,
// 变异程度
mutationRange: 0.5,
// 繁衍子代数量
nbChild: 1,
};
/**
* 基因🧬
*
* 由网络和分数构成,代表每个小鸟的游戏经验
*/
var Genome = function (score, network) {
this.score = score || 0;
this.network = network || null;
};
/**
* 繁衍世代
*
* 由多个独立的小鸟基因构成
*
*/
var Generation = function () {
this.genomes = [];
};
/**
* 根据基因组繁衍后代
*/
Generation.prototype.breed = function (g1, g2, nbChilds) {
var datas = [];
for (var nb = 0; nb < nbChilds; nb++) {
// 克隆基因组1
var data = JSON.parse(JSON.stringify(g1));
for (var i in g2.network.weights) {
// 与基因组2随机生成基因交叉(0.5为交叉因数)
if (Math.random() <= 0.5) {
data.network.weights[i] = g2.network.weights[i];
}
}
// 在基因交叉后,继续进行基因变异
for (var i in data.network.weights) {
if (Math.random() <= self.options.mutationRate) {
// 对部分权重按照一定比例和幅度进行随机调整
data.network.weights[i] +=
Math.random() * self.options.mutationRange * 2 -
self.options.mutationRange;
}
}
datas.push(data);
}
return datas;
};
/**
* 生成下一世代的小鸟
*/
Generation.prototype.generateNextGeneration = function () {
var nexts = [];
for (
var i = 0;
i < Math.round(self.options.elitism * self.options.population);
i++
) {
if (nexts.length < self.options.population) {
// 克隆上一轮分数最高的几个小鸟的基因
nexts.push(JSON.parse(JSON.stringify(this.genomes[i].network)));
}
}
for (
var i = 0;
i < Math.round(self.options.randomBehaviour * self.options.population);
i++
) {
// 随机生成新的基因
var n = JSON.parse(JSON.stringify(this.genomes[0].network));
for (var k in n.weights) {
n.weights[k] = self.options.randomClamped();
}
if (nexts.length < self.options.population) {
nexts.push(n);
}
}
var max = 0;
while (true) {
for (var i = 0; i < max; i++) {
// 剩余的小鸟由之前的各代小鸟繁衍生成,越新的父代繁衍能力越强
var childs = this.breed(
this.genomes[i],
this.genomes[max],
self.options.nbChild > 0 ? self.options.nbChild : 1
);
for (var c in childs) {
nexts.push(childs[c].network);
if (nexts.length >= self.options.population) {
// 当达到目标数量,则停止繁衍
return nexts;
}
}
}
max++;
if (max >= this.genomes.length - 1) {
max = 0;
}
}
};
/**
* 族谱
*
* 由历史世代和当前世代组成
*/
var Generations = function () {
this.generations = [];
var currentGeneration = new Generation();
};
/**
* 重置族谱数据,重新开始基因进化
*/
self.restart = function () {
self.generations = new Generations();
};
self.nextGeneration = function () {
var networks = [];
if (self.generations.generations.length == 0) {
// 创世代
networks = self.generations.firstGeneration();
} else {
// 繁衍进化下一世代
networks = self.generations.nextGeneration();
}
// 从当前生成的新世代创建神经网络
var nns = [];
for (var i in networks) {
var nn = new Network();
nn.setSave(networks[i]);
nns.push(nn);
}
return nns;
};
};
学习结果
第一阶段:无头苍蝇
在最开始时,我们随机初始化了神经网络中各神经元输入参数的权重,所以一开始小鸟像无头苍蝇一样到处乱闯,根本无法稳定飞行,所以大部分的鸟,一上来就落地或飞出边界死掉了。
第二阶段:稳定飞行
剩下的一小批鸟,误打误撞,随机生成的权重刚好使其可以维持飞行稳定,一直向前飞去,所以比那些乱飞的鸟活得更久些。
但是光会稳定飞行是远远不够的,就像只能直线行驶却无法转向的汽车,迟早会撞上障碍物,结束游戏,所以一大批已经学会稳定飞行的鸟也挂掉了。
第三阶段:自由穿梭
幸运的是,随着每代小鸟的不断学习和变异,有的小鸟误打误撞,学会了根据自己当前位置和障碍物缺口位置,去“转向”躲避障碍物,于是它们成了鸟群中的佼佼者,活得更久,由它们繁衍而来的后代也继承了这些经验。
并且,在此基础上有些鸟发生了变异,有的是不利的被淘汰掉,有的是更优秀的基因,使鸟飞的更稳,更灵活,于是鸟儿们的飞行技能越来越熟练,活的时间越来越长,最终整个鸟群就都学会了如何在管道间自由穿梭了。
项目地址
鸣谢
Flappy Learning: xviniette.github.io/FlappyLearn…
FlappyBird_Machine_learning: github.com/atipezda/Fl…