纸上得来终觉浅,绝知此事要躬行
上一章文章关于机器学习的基础概念搞清楚了,最快的学习方式就是实践,所以这里用原生 JS 不使用任何 XNN 的库完成了一个简易版的深度学习库,完成 Chrome Dino 游戏的通关。
直接上 Demo
直接上源码:github.com/Cowboy-Jr/a…
麻雀虽小五脏俱全,包含了正向传播、逆向传播、梯度下降、损失函数、拟合等等
前置准备
按照典型的学习方法论,需要理解监督学习的运作模式,也就是
收集数据 -> 获取特征 -> 推理 -> 输出结果 -> 验证是否正确
^ |
| |
|_______________________________________V
所以对应在 Chrome Dino 这个游戏里就是
跑 -> 获取障碍物信息 -> 推理 -> 是否跳 -> 验证是否正确
^ |
| |
|_______________________________________V
有了这个思路就可以工程化项目,但是如何运用数学知识完成机器学习逻辑是个比较麻烦的事情,这个后面继续讲解,我们先从上往下的讲解如何运行这个 AI 游戏。
工程
仓库目录大致如下
- src
|- ai-deno.ts // 入口文件
|- game // Chrome Dino 游戏
|- jnn
|- fm.ts // 深度学习库
|- legend.ts // 图例
Game 仓库
Chrome Dino 的游戏库是从网上 Fork 出来的,没什么特色之处,中规中矩,唯一之处就是为了能收集特征数据,新增了几个状态的回调。
ai-deno.ts
主入口文件,结合 Game 和 JNN 和 legend 运行游戏,里面重要是几个游戏状态和对应的逻辑
dino = new Runner('.game', {
DINO_COUNT: 1,
// 第一次执行,用于初始化状态
onFirstTime: handleFirstTime,
// 重新运行,用于训练模型
onReset: handleReset,
// 每次游戏结束,用于收集失败时的特征数据
onCrash: handleCrash,
// 跑的时候,用于推理是否需要跳起躲避障碍物
onRunning: handleRunning
});
分析
初始化状态 onFirstTime
从前面的所提到回调逻辑,可以看出我们需要初始化一些状态,比如说整个 NN 框架的状态,每个 Layer、每个 neuron 的参数,另外就是训练数据的初始化值。
let trainingData = {
input: [],
output: [],
};
这里的训练数据 input 包含了每次游戏失败恐龙离障碍物的距离、速度、障碍物的大小,Output 存储的是每次游戏失败时,Dino 的上一个的状态,例如是跳起还是跑步。
跑步 onRunning
我们知道在恐龙运行的时候,需要根据当前 Dino 的状态,获取特征数据放入框架中推理他是否应该跳起。
const handleRunning = async (dino, state) => {
const input = convertStateToVector(state);
let action = 0;
// running
if (dino.jumping === false) {
const [output0, output1] = nn.predict(input);
if (output1 > output0) {
// need jump
action = 1;
dino.lastJumpingState = state;
} else {
// keep running
action = 0;
dino.lastRunningState = state;
}
}
// jumping
await sleep(10)
updateLegend(nn, input);
return action;
};
state 包含了,当前恐龙的状态。但是,这些状态只并不能直接使用,我们必须要把数据进行归一化,具体为什么需要归一化,可以网上自行查找。
然后我们拿着归一化数据放入框架中进行推理,判断他是否需要跳起。并把当前的推断存储起来用于游戏失败时的状态保存,这样的话,我们在训练模型时可以知道上一次到底是对的还是错的。
游戏失败 onCrash
每次游戏失败时,会把恐龙的状态存储起来,归一化处理后,放入训练特征数据中,并且吧,应该输出的值也保存在 output 中。
const handleCrash = async (dino) => {
let input = null;
let output = null;
if (dino.jumping) {
// 获取最后一次跳跃状态
input = convertStateToVector(dino.lastJumpingState);
// 不跳
output = [1, 0];
} else {
// 获取当前行走状态
input = convertStateToVector(dino.lastRunningState);
// 跳
output = [0, 1];
}
trainingData.input.push(input);
trainingData.output.push(output);
};
游戏重新开始 onReset
游戏开始后,我们拿着训练特征数据放到框架中进行反向传播训练,训练框架中的参数也就是所谓的模型,具体细节下一篇文章会展开讲解
const handleReset = async () => {
await sleep(1000);
console.log(trainingData);
// training data
nn.fit(trainingData.input, trainingData.output, {
async onEpochFinish(trainData) {
// updateLegend(nn, trainData);
}
});
};
Forever
后续的流程就是反复地进行游戏、推理、收集特征数据、训练模型。
后续
具体学习框架的细节,下一篇文章会展开讲解是如何设计的。敬请期待。