frontend9 / fe9-library

九部知识库
1.94k stars 138 forks source link

Tensorflow.js科普 #46

Open rdmclin2 opened 6 years ago

rdmclin2 commented 6 years ago

之前看到一个有意思的文章 前端人工智能?TensorFlow.js 学会游戏通关,使用tensorflow.js训练模型玩google无网页面的彩蛋T-Rex Runner 。看了下代码,恩...果断看不懂。先看下演示效果: Genetic Algorithm - T-Rex Runner,希望大家在读完这篇文章后至少能看懂源码了=。=,原理以后再扯,我也不懂。

image.png | left | 720x479

实际上之前火爆过的flappy bird早就有多种神经网络或是强化学习的算法试验过了。但还是觉得很有意思,所以拿过来给大家做科普,顺便可以探讨下前端如何结合人工智能,可以做些什么?


Tensorflow.js?

TensorFlow.js 于3 月 30 日谷歌 TenosrFlow 开发者峰会正式发布,核心改编自deeplearn.js,面向JS提供一套可以在浏览器中运行的机器学习库(应该说是API,类似python)。

亮点是可以用WebGL加速,即可以用GPU加速,一般训练机器学习模型非常的消耗计算资源(比如能做机器学习计算的显卡会非常火爆)。

image.png | left | 747x318


什么是Tensorflow?

image.png | left | 747x267

image.png | left | 747x196


Tensorflow.js 对我们前端工程师而言有什么优势和意义

image.png | left | 747x382

比如:Sketching Interfaces 从原型到代码

image.png | left | 747x393


玩起来!

https://js.tensorflow.org/#getting-started

虽然部分demo可能是之前机器学习玩剩下的,但这些例子算是首次在纯浏览器中训练运行。

image.png | left | 719x243

image.png | left | 558x375

image.png | left | 719x287


快速开始

官网例子解说,目标是能够看懂t-rex-runner的代码

过程: 给定数据,训练拟合函数,输出模型,根据模型预测给定值的输出

<html>
  <head>
    <!-- Load TensorFlow.js -->
    <script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@0.10.0"> </script>

    <!-- Place your code in the script tag below. You can also use an external .js file -->
    <script>
      // 线性回归模型(可以理解为线性方程)
      const model = tf.sequential();
      model.add(tf.layers.dense({units: 1, inputShape: [1]}));

      // 设定损失函数为平均方差和,训练算法为随机梯度下降算法sgd
      model.compile({loss: 'meanSquaredError', optimizer: 'sgd'});

      // 训练数据,X轴和y轴
      const xs = tf.tensor2d([1, 2, 3, 4], [4, 1]);
      const ys = tf.tensor2d([1, 3, 5, 7], [4, 1]);

      // 训练数据拟合后,预测新值的输出
      model.fit(xs, ys).then(() => {
        model.predict(tf.tensor2d([5], [1, 1])).print();
      });
    </script>
  </head>

  <body>
  </body>
</html>

核心概念

Tensor 张量

a set of numerical values shaped into an array of one or more dimensions. 将一系列数值切分到数组或多维中

// 2x3 Tensor
const shape = [2, 3]; // 2 rows, 3 columns
const a = tf.tensor([1.0, 2.0, 3.0, 10.0, 20.0, 30.0], shape);
a.print(); // print Tensor values
// Output: [[1 , 2 , 3 ],
//          [10, 20, 30]]

把一个张量想象成一个n维的数组或列表。看到这玩意让我想到了大学线性代数里的向量和矩阵,后来查到还有一个标量。

标量(单独的数,0维) x

向量: (可以理解为一维数组)

image.png | left | 262x312

矩阵: (可以理解为二维数组)

image.png | left | 410x220

我们可以将标量视为零阶张量,矢量视为一阶张量,那么矩阵就是二阶张量,当然还可以更多阶...

当然对于低阶的张量,tensorflow提供了方便的API来构造:

tf.scalar,  // 标量
tf.tensor1d, // 向量
tf.tensor2d, // 矩阵
tf.tensor3d // 三维数组
tf.tensor4d. // 四维数组

// 比如: 
const c = tf.tensor2d([[1.0, 2.0, 3.0], [10.0, 20.0, 30.0]]);
c.print();
// Output: [[1 , 2 , 3 ],
//          [10, 20, 30]]

// 还有比如
tf.zeros // 全张量初始化为0
tf.ones  // 全张量初始化为1

const zeros = tf.zeros([3, 5]);
// Output: [[0, 0, 0, 0, 0],
//          [0, 0, 0, 0, 0],
//          [0, 0, 0, 0, 0]]

张量一旦创建不可改变,但你可以在他们上做操作生成新的张量。很像React的Immutable.js的思想


Variables 变量

变量通过张量初始化,但是值可以改变

const initialValues = tf.zeros([5]);
const biases = tf.variable(initialValues); // initialize biases
biases.print(); // output: [0, 0, 0, 0, 0]

const updatedValues = tf.tensor1d([0, 1, 0, 1, 0]);
biases.assign(updatedValues); // update values of biases
biases.print(); // output: [0, 1, 0, 1, 0]

Operations 操作

张量用于存储数据,操作可以修改这些数据,返回新的张量。用下来像是以张量为基本单位进行的便捷操作,比如:

// 乘方操作
const d = tf.tensor2d([[1.0, 2.0], [3.0, 4.0]]);
const d_squared = d.square();
d_squared.print();
// Output: [[1, 4 ],
//          [9, 16]]

// 张量加减乘法
const e = tf.tensor2d([[1.0, 2.0], [3.0, 4.0]]);
const f = tf.tensor2d([[5.0, 6.0], [7.0, 8.0]]);

const e_plus_f = e.add(f);
e_plus_f.print();
// Output: [[6 , 8 ],
//          [10, 12]]

// 当然还可以链式操作
const sq_sum = e.add(f).square();

模型和层

模型: 给定输入,产生输出,像一个方程。

有的时候方程可能非常复杂,我们只有这些方程的数据,然后需要通过这些数据对这个方程进行拟合(训练),然后给出新的数据,我们就能够通过这个模型进行预测。

from网络: 根据已知数据寻找模型参数的过程就是训练,最终搜索到的映射\hat{f}被称为训练出来的模型

Tensorflow.js 给出了两种创建模型的方式,一种是通过各种操作直接描述模型,比如:

        // 声明标量,方程的常量
      const a = tf.scalar(2); 
      const b = tf.scalar(4);
      const c = tf.scalar(8);

        // 因为js没有操作符重载,所以各种操作不是那么直观。
      function predict(input) {
          // tensor存在GPU内存中,tidy用于清理除了最后返回的张量以外的中间张量,防止内存泄露,另外还有dispose函数用于清除单个张量。
        return tf.tidy(() => {
          // y = a * x ^ 2 + b * x + c
          const x = tf.scalar(input);
          const ax2 = a.mul(x.square());
          const bx = b.mul(x);
          const y = ax2.add(bx).add(c);
          return y;
        })
      }

        predict(2).print();
        // 24

第二种方式是使用tf提供的高阶API ,tf.model,用层来构建模型,什么是层?层是深度学习中的一个重要抽象概念,一个普通的神经网络通常由多个层组成,比如输入层,输出层,隐藏层,深度学习为什么深?就是因为隐藏层比较多(大于2)。

image.png | left | 400x282

例子:

// 不要问我RNN是什么,我也不懂...
const model = tf.sequential(); // 线性模型,每一层的输入依赖于上一层的输出
model.add(
// RNN : 循环神经网络(Recurrent Neural Network) https://zybuluo.com/hanbingtao/note/541458
  tf.layers.simpleRNN({ 
    units: 20,
    recurrentInitializer: 'GlorotNormal',
    inputShape: [80, 4]
  })
);

// SGD: 随机梯度下降算法 https://www.zybuluo.com/hanbingtao/note/448086  暂时也讲不清楚
const optimizer = tf.train.sgd(LEARNING_RATE); // 算法
model.compile({optimizer, loss: 'categoricalCrossentropy'}); // 编译
model.fit({x: data, y: labels)}); // 训练

好吧,如果你一定要知道SGD是啥:

image.png | left | 388x309

RNN是啥:

image.png | left | 747x307

我也没看懂…不班门弄斧了…看懂了再讲…


一个Tensorflow的具体例子: 训练拟合曲线(官方教程)

https://js.tensorflow.org/tutorials/fit-curve.html 快速开始教程(拟合线性方程)的进阶版本,教程都可以在官网找到,了解核心概念后看这些代码应该能大概看懂了。

代码示例: https://github.com/tensorflow/tfjs-examples/tree/master/polynomial-regression-core

运行效果:

image.png | left | 747x277

目标方程: y = ax^3 + bx^2 + cx + d. 参数值为: a: -0.8, b: -0.2, c: 0.9, d: 0.5

运行目标:我们知道函数长这样,但具体的参数值不清楚,猜测a,b,c,d的值。训练的过程就是最小化误差的过程


三步走

  1. 初始化参数为tf变量,初始化为随机值

    const a = tf.variable(tf.scalar(Math.random()));
    const b = tf.variable(tf.scalar(Math.random()));
    const c = tf.variable(tf.scalar(Math.random()));
    const d = tf.variable(tf.scalar(Math.random()));
  2. 创建模型,模型为上述的目标方程:

    function predict(x) {
    // y = a * x ^ 3 + b * x ^ 2 + c * x + d
    return tf.tidy(() => {
    return a.mul(x.pow(tf.scalar(3)))
      .add(b.mul(x.square()))
      .add(c.mul(x))
      .add(d);
    })
    }
  3. 训练,训练以上模型,即学习目标参数值。训练模型需要:

    • 损失函数:
    • 优化函数
    • 训练循环

损失函数

判断拟合程度,一般为均方误差( 平方损失除以样本数)这个值越低,代表拟合越好。评价好坏用。

function loss(prediction, labels) {
// 预测值和真实值的差值平方取平均,比方说完全拟合,结果就是0
const error = prediction.sub(labels).square().mean();
return error;
}

优化函数

随机梯度下降算法(最常见的优化算法),求取目标函数(损失函数)的最小值

// 学习率,每次一小步,逐渐靠近目标值
const learningRate = 0.5;
const optimizer = tf.train.sgd(learningRate);

训练循环

训练多少轮,即调整多少次数值

const numIterations = 75;
async function fit(xs, ys, numIterations) {
for (let iter = 0; iter < numIterations; iter++) {
// train
optimizer.minimize(() => {
const pred = predict(xs);
return loss(pred, ys);
})
}
await tf.nextFrame();
}

回头看 T-Rex-Runner问题

途中有三个恐暴龙是因为作者用的多玩家模式优化算法,通过多只恐暴龙同时训练,达到见多识广的效果(多重影分身)。

image.png | left | 720x442

问题描述: 根据状态(输入)进行是否跳跃的预测predict(输出)。

输入:

return [
      state.obstacleX / CANVAS_WIDTH,      // 障碍物离暴龙的距离
      state.obstacleWidth / CANVAS_WIDTH,  // 障碍物宽度
      state.speed / 100                    // 当前游戏全局速度
 ];

输出:

[0.2158, 0.8212] 
// 其中第一维代表暴龙保持状态不变的可能性,而第二维度代表跳跃的可能性

预测方式: f([0.1428, 0.02012, 0.00549]) = [0.2158, 0.8212]表示预测结果为跳跃


如何训练

训练过程嵌入生命周期,最主要在以下三个函数中嵌入训练和预测过程:

image.png | left | 646x816


onRunning函数(Predict)

跑的过程中判断是否要跳,还是保持不动。

function handleRunning({ tRex, state }) {
  return new Promise((resolve) => {
    if (!tRex.jumping) { // 在跳的过程中不做判断
      let action = 0;
      const prediction = tRex.model.predictSingle(convertStateToVector(state));
      prediction.data().then((result) => {
        if (result[1] > result[0]) { // 不跳
          action = 1;
          tRex.lastJumpingState = state;
        } else { // 跳
          tRex.lastRunningState = state;
        }
        resolve(action);
      });
    } else {
      resolve(0);
    }
  });
}

onCrash(重要训练数据)

crash的时候进行训练数据的收集

function handleCrash({ tRex }) {
  let input = null;
  let label = null;
  if (tRex.jumping) { //  crash的时候在跳
    input = convertStateToVector(tRex.lastJumpingState);
    label = [1, 0]; // 下次遇到这种情况别跳啊
  } else { // crash的时候在跑
    input = convertStateToVector(tRex.lastRunningState);
    label = [0, 1]; // 下次遇到这种情况要跳啊
  }
    // 存下来crash的数据
  tRex.training.inputs.push(input);
  tRex.training.labels.push(label);
}

onReset(训练节点)

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);
  }
}

训练模型(以NN神经网络为例)

和之前的predict不同,这个函数的样子我们未知,使用神经网络模拟。看他的模型,你会发现你也能看得懂了(至少是语法层面上)。

  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) { // 单次训练
    this.optimizer.minimize(() => {
      const predictedYs = this.predict(inputXs);
      return this.loss(predictedYs, inputYs);
    });
  }

  fit(inputXs, inputYs, iterationCount = 100) { // 拟合,训练多次
    for (let i = 0; i < iterationCount; i += 1) {
      this.train(inputXs, inputYs);
    }
  }

  loss(predictedYs, labels) { // 损益度量
    const meanSquareError = predictedYs
      .sub(tensor(labels))
      .square()
      .mean();
    return meanSquareError;
  }

应用场景探索

image.png | left | 512x512

参考资料

chuan92 commented 6 years ago

不错不错,写的通俗易懂

JLraining commented 6 years ago

虽然看不懂。但是6666666666