題注:若是喜歡咱們的文章別忘了點擊關注阿里南京技術專刊呦~ 本文轉載自 阿里南京技術專刊-知乎,歡迎大牛小牛投遞阿里南京前端/後端開發等職位,詳見 阿里南京誠邀前端小夥伴加入~。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)**的運行模式,沒錯,你也能夠本身設計新的算法模型並進行測試。
歡迎在下方的留言區中與我交流。
最後請關注咱們的專欄: