会“思考”的 Flappy Bird

2,804 阅读6分钟

我正在参加掘金社区游戏创意投稿大赛个人赛,详情请看:游戏创意投稿大赛

前言

说起 Flappy Bird 大家可能并不陌生,这个由越南独立游戏开发者阮河东(Dong Nguyen)开发的休闲小游戏,自 2013 年面市至今已在全球范围内拦获了超 5000 万的下载量,可谓家喻户晓。

虽然 Flappy Bird 游戏规则简单:控制小鸟上下移动,使其尽可能多得通过一个个障碍物,但想必玩过的人都知道,要想操控好这只小鸟,顺利的在各个管道之间穿梭,还是颇为困难的。

下面,本文将从神经网络和遗传变异两方面展开,探索如何让机器从零开始学会操控小鸟,并取得高分。

问题分析

首先,我们从一只鸟的情况开始看起。

结合文章开头的 GIF 图片分析可知:

  1. 每个时刻,玩家对鸟有两种动作:啥都不做让鸟往下落点击屏幕让鸟向上飞
  2. 每个时刻,鸟只需要关心下一个通过的障碍物缺口位置,避免撞上管道
  3. 鸟可以感知到自己距离地面和下一障碍物的距离,以及管道缺口位置和长度
  4. 每个管道缺口的长度是相同的
  5. 管道一直在向左匀速运动
  6. 鸟撞上管道就会死亡
  7. 鸟存活的时间越长分数越高

转化成机器可以理解的数学模型就是:

根据输入条件(鸟距离地面和下一障碍物的距离,以及管道缺口位置等)不断计算决策输出(鸟的两个动作:往下落向上飞),使鸟获得更高的分数。

这是一个典型的人工神经网络案例,我们可以使用 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;
  };
};

学习结果

第一阶段:无头苍蝇

在最开始时,我们随机初始化了神经网络中各神经元输入参数的权重,所以一开始小鸟像无头苍蝇一样到处乱闯,根本无法稳定飞行,所以大部分的鸟,一上来就落地或飞出边界死掉了。

第二阶段:稳定飞行

剩下的一小批鸟,误打误撞,随机生成的权重刚好使其可以维持飞行稳定,一直向前飞去,所以比那些乱飞的鸟活得更久些。

但是光会稳定飞行是远远不够的,就像只能直线行驶却无法转向的汽车,迟早会撞上障碍物,结束游戏,所以一大批已经学会稳定飞行的鸟也挂掉了。

第三阶段:自由穿梭

幸运的是,随着每代小鸟的不断学习和变异,有的小鸟误打误撞,学会了根据自己当前位置和障碍物缺口位置,去“转向”躲避障碍物,于是它们成了鸟群中的佼佼者,活得更久,由它们繁衍而来的后代也继承了这些经验。

并且,在此基础上有些鸟发生了变异,有的是不利的被淘汰掉,有的是更优秀的基因,使鸟飞的更稳,更灵活,于是鸟儿们的飞行技能越来越熟练,活的时间越来越长,最终整个鸟群就都学会了如何在管道间自由穿梭了。

项目地址

github.com/idootop/Fla…

github.com/idootop/Fla…

鸣谢

Flappy Learning: xviniette.github.io/FlappyLearn…

FlappyBird_Machine_learning: github.com/atipezda/Fl…