题注:若是喜欢咱们的文章别忘了点击关注阿里南京技术专刊呦~ 本文转载自 阿里南京技术专刊-知乎,欢迎大牛小牛投递阿里南京前端/后端开发等职位,详见 阿里南京诚邀前端小伙伴加入~。php
关键字:Tensorflow,JavaScript,AI,前端开发,人工智能,神经网络,遗传算法html
T-Rex Runner 是隐藏在 Chrome 中的彩蛋游戏,最近我用刚推出的 TensorFlow.js 开发了一个彻底独立运行于浏览器环境下的 AI 程序,以下图所示 AI 能够轻松控制暴龙(T-Rex)避开障碍物。前端
AI 在尝试 3 次后逐渐学会了如何控制暴龙避让障碍物git
引入遗传算法后,尝试 2 次后 AI 便可学会控制github
查看在线演示算法
下载或收藏我在 Github 上的源代码spring
做为 Chrome 浏览器死忠,你或许早已发现隐藏在 Chrome 浏览器“没法链接到互联网”报错页面中的彩蛋“T-Rex Runner”游戏。chrome
若是你尚未玩儿过 T-Rex Runner,能够按照下面几个步骤开启彩蛋:编程
你的任务就是在不碰到仙人掌和空中的翼龙的状况下保持前行,坚持的时间越久则分数越高,难度也随之愈来愈大。后端
做为深度学习界的当红炸子鸡——TensorFlow 开源组织终于在 2018 年 3 月推出了首个 JavaScript 版本。TensorFlow.js 能够在浏览器端完成模型训练、执行和再训练等基本任务,而且借助 WebGL 技术,能够和 Python、C++ 版本同样可以经过 GPU 硬件加速完成计算过程。
目前网上关于 TensorFlow.js 的教程寥寥无几,基本上就是官方示例的解析,本文但愿能从实例出发,给你们补充一些学习的动力!
本文的目标是基于 TensorFlow.js 在浏览器端构建人工神经网络,经过反复训练让 AI 学会如何控制暴龙成功避开障碍物。本文的结构以下:
T-Rex Runner 的源代码能够在 Chromium 的代码仓库中找到,可是这个小游戏是在 2014 年编写的,使用的都是 ES5 时代的技术,更糟糕的是因为缺乏模块化,整个游戏的源代码都放在同一个文件中,这很大程度上增长了理解和修改源代码的难度。
所以,我先花了一个下午的时间,用 ES6/ES7 + LESS + Webpack 等现代化前端技术栈重写了 t-rex-runner 项目,而且引入 ESLint 来保障代码质量。
除此外,我还移除了声效、鼠标控制、移动端支持和 GameOver 画面等相关代码,而且为了后面运用遗传算法,我还为游戏加入了多人模式(Multiplayer Mode,即游戏中同一局,有多只暴龙同时出现)。
有关代码已上传至 Github,详细请见 src/game 目录中。
t-rex-runner 是一个很是标准的面向对象编程游戏程序,事实上你也能够将它做为 HTML5 游戏开发入门的经典示例。重构后的 t-rex-runner 项目,主要包含如下类型:
Runner 类:这是游戏的核心,掌管整个游戏的生命周期,主要类成员包括:
Canvas
、Horizon
、DistanceMeter
、TRexGroup
等类的实例,而且首次触发 restart()
和 update()
。requestAnimationFrame()
调用,大约为 60 帧每秒(由 Runtime.getFPS() 方法决定)。Trex 类:表明一只 T-Rex,即暴龙,主要类成员包括:
true
)。Runner.restart()
在游戏重启前调用TrexGroup 类:表明包含 n 个暴龙的种群,这在原先代码中是没有的,之因此要有种群的概念是为了支持多玩家模式,即同时有 n 只暴龙以各自独立的方式玩同一局游戏。除拥支持 Trex 类大多数方法外,还包括:
Obstacle 类:表明障碍物,例如各类高度、宽度的仙人掌和空中的翼龙等,主要类成员包括:
除以上核心类型外,其余还包括:
为了让 AI 替代人类参与到游戏中,咱们除了须要有 Trex.startJump() 这样的输出类方法外,还需在 Runner 类中提供必要的事件做为输入:
update()
后触发该事件,事件的返回值将被做为 action
,当 action
为 1
时表示将执行跳跃,0
则表示保持不变。 能够利用该事件实现对游戏状态的监控,同时命令暴龙在特定的时机改变跳跃状态。下面是一个示例程序,基于以上生命周期事件:
let runner = null;
// 排名
let rankList = [];
// 初始化游戏。
function setup() {
// 建立游戏运行时
runner = new Runner(
'.game', // HTML 中对应的游戏 DIV 容器
{
T_REX_COUNT: 10, // 每一局同时有 10 只暴龙
onReset: handleReset,
onRunning: handleRunning,
onCrash: handleCrash
}
);
// 初始化
runner.init();
}
let firstTime = true;
// 每次游戏从新开始前会调用此方法。
// tRexes 参数表示当前的暴龙种群。
function handleReset({ tRexes }) {
if (firstTime) {
firstTime = false;
tRexes.forEach((tRex) => {
// 随机初始化每一只暴龙的模型
// minDistance 在本例中表明可容忍的障碍物最小间距
tRex.model = { minDistance: Math.random() * 50 };
});
} else {
// 打印排名
rankList.forEach(
(tRex, i) => console.info(i + 1, tRex.model.minDistance)
);
// 清空排名
rankList.splice(0);
}
}
// 在游戏运行中,活着的暴龙会持续调用此方法来询问是否要跳跃。
// tRex 参数表示当前上下文中的暴龙。
// state 参数中,obstacleX 表示距离最近的障碍物的横坐标,obstacleWidth
// 表示障碍物宽度,speed 表示当前游戏全局速度。
// 方法返回 1 表示跳跃,2 则表示不变。
function handleRunning({ tRex, state }) {
if (state.obstacleX <= tRex.model.minDistance &&
!tRex.jumping) {
// 这里咱们直接用一个“人工【的】智能”,即:
// 当前障碍物距离到达阈值,则命令暴龙跳跃
return 1;
}
return 0;
}
const deadTrexes = [];
// 当暴龙“crash”时,会调用此方法来通知。
function handleCrash({ tRex }) {
// 记录排名,最后 crashed 暴龙排在最前面
rankList.unshift(tRex);
}
// 订购 DOMContentLoaded 事件以触发 setup() 方法
document.addEventListener('DOMContentLoaded', setup);
复制代码
“算法模型”一词对于刚接触 AI 的前端同窗来讲,可能听上去有些高不可测,其实否则,让咱们先合上教科书,来一块儿看看下面这个初中就学过的简单公式:
公式中,x
是一输入项(inputs) ,y 是输出项(outputs),而 f(x)
就是模型 (model)的核心函数。例如:
weight
、bias
等参数,举一个例子,听说知乎文章中美多一个公式,就会少 n 个读者,这就是一个典型的线性模型**;**事实上 AI 决定当前是否须要跳跃也是一个线性模型,用一个线性函数表示就是:
obstacleX
和obstacleWidth
是输入项,它们来自于handleRunning()
方法的state
参数,该参数中: -obstacleX
表示距离最近的障碍物的横坐标 -obstacleWidth
表示障碍物宽度 -speed
表示当前游戏全局速度。 当y
输出的值小于0
时,则表示须要“跳跃”。
其中 w1
、w2
分别表示 obstacleX
和 obstacleWidth
的权重(weight), b
是偏移量(bias),它们都是该线性模型的参数。
与初中数学有所不一样的是,这里的输入和输出一般都是向量(vector),而不像前面的例子中都是标量**,**而且多为线性运算。千万不要被线性数学和公式吓跑,“算法”不彻底是“数学”,更不是“算数”,请接着往下看。
预测 Prediction
在机器学习中,已知输入项 x
和模型求 y
时,被称为**预测(predict)**过程。
训练 Training
经过已知输入项 x
和输出项 y
来调节模型中 w1
、w2
和 b
参数直到“最佳效果”的过程,被称为训练(train)过程,而 y
由于是已知的输出项,又被称为标签(label),多组x
和 y
在一块儿被称为**训练数据集(training data set)。**训练一般须要反复执行不少次,才能达到“最佳效果”。
评价 Evaluation
在训练过程当中,将训练数据集中的 x
做为输入项,执行预测过程,将预测结果与标签 y
的实际结果进行对比,并经过一个函数获得一个分值用以表示当前模型的拟合能力,被称为评价(evaluatie)过程,这个函数被称为评价函数或损失函数(loss function)。
机器学习就是一个不断训练、评价迭代的模型训练过程,训练得越好,则将来预测得越准确。
2.1 和 2.2 这两节中的内容均为笔者本身多年工做实践的总结,与教科书不免有差别还请谅解,有关术语定义请以教科书为准。
在正式进入到 AI 算法实现环节以前,咱们还须要定义一个通用的面向对象 AI 模型—— Model 抽象类,其成员主要包括:
inputX
预测出 y
值并将其返回。train()
方法,中止的条件能够是执行到必定的次数,也能够是当 loss()
方法返回的均方差小于一个阀值。上述inputX
、inputs
和 labels
等都是用**向量(vector)**来表示的,能够用数组来表示,在 TensorFlow.js 中则用 tf.Tensor 表示。
在本项目中,Model
抽象类是全部算法模型的基类,让咱们来看一个最简单的模型——随机模型的源代码:
import Model from '../Model';
// 随机模型继承自 Model
export default class RandomModel extends Model {
// weights 和 biases 是 RandomModel 的模型参数
weights = [];
biases = [];
init() {
// 初始化就是随机的过程
this.randomize();
}
predict(inputXs) {
// 最简单的线性模型
const inputX = inputXs[0];
const y =
this.weights[0] * inputX[0] +
this.weights[1] * inputX[1]+
this.weights[2] * inputX[2] +
this.biases[0];
return y < 0 ? 1 : 0;
}
train(inputs, labels) {
// 随机模型还要啥训练,直接随机!
this.randomize();
}
randomize() {
// 随机生成全部模型参数
this.weights[0] = random();
this.weights[1] = random();
this.weights[2] = random();
this.biases[0] = random();
}
}
function random() {
return (Math.random() - 0.5) * 2;
}
复制代码
做者注:千万不要小看这个模型,经过遗传算法,随机模型也能控制暴龙避开障碍物,只是学习效率略低,请在桌面版 Chrome 上观看 Demo。
输入项
简单来讲,咱们首先将 1.3 节中 handleRunning()
方法获得的 state
JSON 参数转换成一个 3 维向量,即一个 3 维数组,并进行归一化处理,所谓**归一化(Normalize)**能够理解成将一个标量变成 0 - 1 之间的值的函数。相关代码以下:
function handleRunning({ state }) {
const inputs = convertStateToVector(state);
...
}
function convertStateToVector(state) {
if (state) {
// 生成一个包含 3 个数字的数组,即向量
// 数字被归一后,值域为 0 到 1
// 如 [0.1428, 0.02012, 0.00549]
return [
state.obstacleX / CANVAS_WIDTH, // 障碍物离暴龙的距离
state.obstacleWidth / CANVAS_WIDTH, // 障碍物宽度
state.speed / 100 // 当前游戏全局速度
];
}
return [0, 0, 0];
}
复制代码
输出项
接下来咱们来定义输出项,最简单的方法是一个 2 维向量,其中第一维表明暴龙保持状态不变的可能性,而第二维度表明跳跃的可能性。例如:
[0, 1]
表示跳跃
;[0.2158, 0.8212]
表示跳跃
;[0.998, 0.997]
则表示保持不变
,继续前行;f([0.1428, 0.02012, 0.00549]) = [0.2158, 0.8212]
表示预测结果为跳跃
;state
为 { obstacleX: 0.1428, obstacleWidth: 0.02012, speed: 0.00549 }
,暴龙跳跃后 crash 了,则能够在训练过程当中经过将 [0.1428, 0.02012, 0.00549]
对应于 [1, 0]
标签,来告诉 AI 下一次碰到这种状况不要再跳跃
了,而应该 保持不变
。受限于篇幅,实在没法将神经网络的原理在此复述。如下内容摘自 Wikipedia:
人工神经网络(英语:artificial neural network,缩写 ANN),简称神经网络(neural network,缩写 NN)或类神经网络,在机器学习和认知科学领域,是一种模仿生物神经网络(动物的中枢神经系统,特别是大脑)的结构和功能的数学模型或计算模型,用于对函数进行估计或近似。神经网络由大量的人工神经元联结进行计算。大多数状况下人工神经网络能在外界信息的基础上改变内部结构,是一种自适应系统。[来源请求]现代神经网络是一种非线性统计性数据建模工具。典型的神经网络具备如下三个部分: 结构(Architecture)结构指定了网络中的变量和它们的拓扑关系。例如,神经网络中的变量能够是神经元链接的权重(weights)和神经元的激励值(activities of the neurons)。 **激励函数(Activity Rule)**大部分神经网络模型具备一个短期尺度的动力学规则,来定义神经元如何根据其余神经元的活动来改变本身的激励值。通常激励函数依赖于网络中的权重(即该网络的参数)。 **学习规则(Learning Rule)**学习规则指定了网络中的权重如何随着时间推动而调整。这通常被看作是一种长时间尺度的动力学规则。通常状况下,学习规则依赖于神经元的激励值。它也可能依赖于监督者提供的目标值和当前权重的值。例如,用于手写识别的一个神经网络,有一组输入神经元。输入神经元会被输入图像的数据所激发。在激励值被加权并经过一个函数(由网络的设计者肯定)后,这些神经元的激励值被传递到其余神经元。这个过程不断重复,直到输出神经元被激发。最后,输出神经元的激励值决定了识别出来的是哪一个字母。
常见的多层结构的神经网络由三部分组成: 输入层(Input layer),众多神经元(Neuron)接受大量非线形输入消息。输入的消息称为输入向量。 输出层(Output layer),消息在神经元连接中传输、分析、权衡,造成输出结果。输出的消息称为输出向量。 隐藏层(Hidden layer),简称“隐层”,是输入层和输出层之间众多神经元和连接组成的各个层面。隐层能够有多层,习惯上会用一层。隐层的节点(神经元)数目不定,但数目越多神经网络的非线性越显著,从而神经网络的强健性(robustness)(控制系统在必定结构、大小等的参数摄动下,维持某些性能的特性。)更显著。习惯上会选输入节点1.2至1.5倍的节点。
如上图所示,在本节中咱们将搭建一个两层神经网络(Neural Network,简称 NN),输入项为一个三维向量组成的矩阵,输出则为一个二维向量组成的矩阵,隐含层中包含 6 个神经元,激励函数为 sigmoid
。
下面为 NNModel 的源代码:
import * as tf from '@tensorflow/tfjs';
import { tensor } from '../../utils';
import Model from '../Model';
/**
* 神经网络模型
*/
export default class NNModel extends Model {
weights = [];
biases = [];
constructor({
inputSize = 3,
hiddenLayerSize = inputSize * 2,
outputSize = 2,
learningRate = 0.1
} = {}) {
super();
this.hiddenLayerSize = hiddenLayerSize;
this.inputSize = inputSize;
this.outputSize = outputSize;
// 咱们使用 ADAM 做为优化器
this.optimizer = tf.train.adam(learningRate);
}
init() {
// 隐藏层
this.weights[0] = tf.variable(
tf.randomNormal([this.inputSize, this.hiddenLayerSize])
);
this.biases[0] = tf.variable(tf.scalar(Math.random()));
// 输出层tput layer
this.weights[1] = tf.variable(
tf.randomNormal([this.hiddenLayerSize, this.outputSize])
);
this.biases[1] = tf.variable(tf.scalar(Math.random()));
}
predict(inputXs) {
const x = tensor(inputXs);
// 预测的是指
const prediction = tf.tidy(() => {
const hiddenLayer = tf.sigmoid(x.matMul(this.weights[0]).add(this.biases[0]));
const outputLayer = tf.sigmoid(hiddenLayer.matMul(this.weights[1]).add(this.biases[1]));
return outputLayer;
});
return prediction;
}
train(inputXs, inputYs) {
// 训练的过程其实就是将带标签的数据交给内置的 optimizer 进行优化
this.optimizer.minimize(() => {
const predictedYs = this.predict(inputXs);
// 计算损失值,优化器的目标就是最小化该值
return this.loss(predictedYs, inputYs);
});
}
}
复制代码
若是你此前使用过 Python 版的 TensorFlow,不难发现上面的代码就是将线性数学公式或者 Python 翻译成了 JavaScript 代码。与 Python 版本不一样的是,因为 JavaScript 缺乏 Python 符号重载(operation overloading)的语言特性,所以在公式表达方面比较繁琐,例如数学公式:
用 Python 表示可直接表示为:
y = tf.sigmoid(tf.matmul(x, Weights) + biases)
复制代码
而 JavaScript 因为缺乏加号符号重载,所以要写成:
y = tf.sigmoid(tf.matMul(x, weights).add(biases));
复制代码
在第 2 章中咱们重构了 T-Rex Runner 的代码结构,并暴露出生命周期事件以便 AI 截获并控制暴龙的行为,在第 3 章结尾,基于 TensorFlow.js 咱们用 50 行代码就构建了一个神经网络,如今咱们只须要将二者进行有机的结合,就能实现 AI 玩游戏,具体步骤以下:
handleRunning()
事件处理中,调用模型的 predict()
方法,根据当前 state
决定是否须要跳跃;handleCrash()
事件处理中,若是暴龙是由于“跳跃”而 crash 的,就在训练数据集中记录标签为“保持不变”,反之则记录为“跳跃”,这就是咱们在教育中所谓“吸收教训”、“矫枉过正”的过程;handleReset()
事件处理中,执行模型的 fit()
方法,根据最新训练数据集进行反复训练。具体代码片断以下:
let firstTime = true;
function handleReset({ tRexes }) {
// 因为当前模型中咱们只有一只暴龙,所以只须要第一只就够了
const tRex = tRexes[0];
if (firstTime) {
// 首次初始化模型
firstTime = false;
tRex.model = new NNModel();
tRex.model.init();
tRex.training = {
inputs: [],
labels: []
};
} else {
// 根据最新收集的训练数据从新训练
tRex.model.fit(tRex.training.inputs, tRex.training.labels);
}
}
function handleRunning({ tRex, state }) {
return new Promise((resolve) => {
if (!tRex.jumping) {
let action = 0;
const prediction = tRex.model.predictSingle(convertStateToVector(state));
// tensor.data() 方法是对 tensor 异步求值的过程,返回一个 Promise 对象:
prediction.data().then((result) => {
if (result[1] > result[0]) {
// 应该跳跃
action = 1;
// 记录最后“跳跃”时的状态,以备 handleCrash() 复盘时使用
tRex.lastJumpingState = state;
} else {
// 保持不变,并记录最后“保持不变”的状态值,以备 handleCrash() 复盘时使用
tRex.lastRunningState = state;
}
resolve(action);
});
} else {
resolve(0);
}
});
}
function handleCrash({ tRex }) {
let input = null;
let label = null;
if (tRex.jumping) {
// 跳错了,应该保持不变!下次记住了!
input = convertStateToVector(tRex.lastJumpingState);
label = [1, 0];
} else {
// 不该该保守的,应该跳跃才对!下次记住了!
input = convertStateToVector(tRex.lastRunningState);
label = [0, 1];
}
tRex.training.inputs.push(input);
tRex.training.labels.push(label);
}
复制代码
基于人工神经网络的 AI 模型运行效果请在桌面版 Chrome 上观看 Demo。
若是你观察过这个线上 Demo不难发现,一般在 4-5 次 crash 后,AI 逐渐学会了跳跃障碍的时间和技巧,可是有的时候“运气很差”的话可能须要 10 次以上,那么有没有什么办法能够优化算法呢?答案是确定的:
本文经过 AI 玩转 T-Rex Runner 的实例,介绍了如何重构游戏代码、利用 TensorFlow.js 快速搭建人工神经网络的过程。
关于将来,本项目计划经过 CNN 卷积神经网络来直接经过捕获 HTML Canvas 中的图像信息,分析 handleRunning()
中的 state
状态。若是你对本项目有兴趣,请在 Github 上关注本项目,我还会继续持续更新。
或许你已经发现,这个项目采用了相似**测试台(Test Bench)**的运行模式,没错,你也能够本身设计新的算法模型并进行测试。
欢迎在下方的留言区中与我交流。
最后请关注咱们的专栏: