章节配图

文章参考自:

写这篇文章的目的是给现有 web 开发的同事提供一些新的开发方向,认识新的 js 开发领域!

# 前端为什么要学习机器学习

学习机器学习并不是为了蹭人工智能的热度,而是为了更好的迎接未来的发展趋势。现有绝大多数开发人员面向的是规则编程

根据规则写固定规则程序。但是需求的复杂性提高就无法再通过编写规则来编程。

传统编程的短板: 比如产品需求要让程序识别猫的图片筛选出来做专题,那么程序猿不可能将所有猫的图片进行编写规则来识别,因为每新增一张图片就意味着要修改代码增加一个判断规则。

学习人工智能,学习机器学习编程就是为了弥补这个短板,让程序能更好的适应未来复杂多变的需求。

# 人工智能、机器学习、Tensorflow 三者关系

人工智能是近几年火热的话题,同时人工智能也是一门很庞大的学科。那么如何实现人工智能?机器学习就是其中一种实现方式。

机器学习又是什么?“机器学习”如同字面意思,让计算机自己学习提供的数据特征,最终达到识别相应的类似数据。

那么如何以编程的形式使程序拥有机器学习能力?Tensorflow 库是一款很好的机器学习的编程库。而本文的 Tensorflow.js 是 Tensorflow 的 JS 版本。除了 JS 版本,Tensorflow 还有 Python、C++、Java 等多种语言版本。

综上所述可以下图总结这三者关系:

章节配图

# 为什么选择 Tensorflow.js

TensorFlow 是 Google 推出的开源机器学习框架,并针对浏览器、移动端、IOT 设备及大型生产环境均提供了相应的扩展解决方案,TensorFlow.js 就是 JavaScript 语言版本的扩展,在它的支持下,前端开发者就可以直接在浏览器环境中来实现深度学习的功能,尝试过配置环境的读者都知道这意味着什么。浏览器环境在构建交互型应用方面有着天然优势,而端侧机器学习不仅可以分担部分云端的计算压力,也具有更好的隐私性,同时还可以借助 Node.js 在服务端继续使用 JavaScript 进行开发,这对于前端开发者而言非常友好。除了提供统一风格的术语和 API,TensorFlow 的不同扩展版本之间还可以通过迁移学习来实现模型的复用(许多知名的深度学习模型都可以找到 python 版本的源代码),或者在预训练模型的基础上来定制自己的深度神经网络,为了能够让开发者尽快熟悉相关知识,TensorFlow 官方网站还提供了一系列有关 JavaScript 版本的教程、使用指南以及开箱即用的预训练模型,它们都可以帮助你更好地了解深度学习的相关知识。对深度学习感兴趣的读者推荐阅读美国量子物理学家 Michael Nielsen 编写的《神经网络与深度学习》(英文原版名为《Neural Networks and Deep Learning》),它对于深度学习基本过程和原理的讲解非常清晰。

本文主要是针对 web 开发者认识机器学习这个领域,而 web 开发者必备的语言技能就是 Javascript。Tensorflow.js 就是以纯 Javascript 来编写机器学习程序的开发库,而且运行环境可以选择浏览器,可以很好的可视化程序的训练过程。这就是本文选择 Tensorflow.js 的最重要原因。

  • 除了这个重要原因,Tensorflow.js 还有如下优势:

1、Tensorflow.js 是开箱即用的开发库,开发者无需花精力去编写基础复杂的数学问题。

2、由于可运行于浏览器,减少服务器的运算,提高服务器资源利用,增强客户端响应运算结果的速度。

3、使用语言就是 Javascript,前端工程师不需要学习其他后端语言,降低入门门槛。

4、由于浏览器的 WebGL 可调用 GPU,所以 Tensorflow.js 会使用 GPU 加速模型的运算,提高运算效率。

5、由于 Node 和 Python 一样都是使用 C++编写的环境,所以在 Node 环境进行运算的速度目前与 Python 速度不相上下。

6、Tensorflow.js 的模型可以跟 Python 等其他语言模型进行互转。就是 js 写了一个机器模型可以转换模型到 Python 环境下使用。

7、浏览器可以很好可视化机器训练过程,同时浏览器可调用设备的摄像头、麦克风等增加机器学习的应用场景,让机器学习跟接近用户。

  • 有优势比如有它的劣势:

1、部署在浏览器,js 就是公开的,那么训练模型就是公开的,商业保密性低。

2、浏览器端不适合部署体积过大的训练模型,不然用户加载页面会相当耗时。

3、在 Node 环境目前无法做到分布式训练,使用多台服务器对一个模型进行大规模训练。

4、Tensorflow.js 的社区活跃度、资源等方面都不如 Python 社区,但是可以从 Python 社区去寻找资源运用到 js 平台。

综合上面优劣,选择 Tensorflow.js 是为了进入机器学习领域,让 web 开发者体验到机器学习编程和传统的规则编程两者之间的不同。深入学习之后可以平滑过度到其他人工智能开发的开发库。

# Tensor(张量)

Tensor(张量) 是 TensorFlow 中的基本数据结构,它是向量和矩阵向更高维度的推广,从编程的角度来看,它的核心数据不过就是多维数组。

Tensor数据类型可以很方便地构造各种维度的张量,支持切片、变形、合并分割等结构操作,同时也定义了各类线性代数运算的操作符,这样做的好处是可以将开发者在应用层编写的程序和不同平台的底层实现之间解耦。这样,神经网络中的信息传递就通过张量(Tensor)的流动(Flow)表现出来了。

在 2018 年 Google I/O 大会上,TensorFlow.js小组的工程师就介绍了该框架分层的结构设计,除了最底层为了解决编程语言和平台差异的层次外,为了对不同的工作性质的开发者实现更好地支。

章节配图

TensorFlow.js在应用层还提供了两种不同的 API:高阶 API 被称为Keras APIKeras是一个 python 编写的开源人工神经网络库)或Layer API,用于快速实现深度学习模型的构建、训练、评估和应用,软件和应用开发者大多情况下会使用它;低阶 API 也被称为 Core API,通常用于支持研究人员对神经网络实现更底层的细节定制,使用起来难度也更高。

TensorFlow.js的工作依然是围绕神经网络展开的,基本的工作过程包含了如下几个典型步骤:

章节配图

下面我们将通过 TensorFlow.js 官方网站提供的数据拟合的示例来了解整个流程。

Define 阶段是使用 TensorFlow.js 的第一步,这个阶段中需要初始化神经网络模型,你可以在 TensorFlow 的 tf.layers 对象上找到具备各种功能和特征的隐藏层,通过模型实例的 add 方法将其逐层添加到神经网络中,从而实现张量变形处理、卷积神经网络、循环神经网络等复杂模型,当内置模型无法满足需求时,还可以自定义模型层,TensorFlow 的高阶 API 可以帮助开发者以声明式的编码来完成神经网络的结构搭建,示例代码如下:

/*创建模型*/
function createModel() {
  const model = tf.sequential();
  model.add(tf.layers.dense({ inputShape: [1], units: 1, useBias: true }));
  model.add(tf.layers.dense({ units: 1, useBias: true }));
  return model;
}

Compile阶段需要对训练过程进行一些参数预设,你可以先温习一下上一章中介绍过的 BP 神经网络的工作过程,然后再来理解下面的示例代码:

model.compile({
  optimizer: tf.train.adam(),
  loss: tf.losses.meanSquaredError,
  metrics: ['mse'],
});

loss(损失)用于定义损失函数,它是神经网络的实际输出和期望输出之间偏差的量化评估标准,最常用的损失函数就是均方差损失(tf.losses.meanSquaredError),其他损失函数可以在TensorFlow的 API 文档中进行查看;optimizer(优化器)是指误差反向传播结束后,神经网络进行权重调整时所使用的的算法。权重调整的目的就是为了使损失函数达到极小值,所以通常采用“梯度下降”的思想来进行逼近,梯度方向是指函数在某一点变化最显著的方向,但实际的情况往往并没有这么简单,假设下图是一个神经网络的损失函数曲线:

章节配图

可以看到损失函数的形态、初始参数的位置以及优化过程的步长等都可能对训练过程和训练结果产生影响,这就需要在optimizer配置项中指定优化算法来达到较好的训练效果;metrics配置项用于指定模型的度量指标,大多数情况下可以直接使用损失函数来作为度量标准。

Fit 阶段执行的是模型训练的工作(fit 本身是拟合的意思),通过调用模型的 fit 方法就可以启动训练循环,官方示例代码如下(fit 方法接收的参数分别为输入张量集、输出张量集和配置参数):

const batchSize = 32;
const epochs = 50;
await model.fit(inputs, labels, {
  batchSize,
  epochs,
  shuffle: true,
  callbacks: tfvis.show.fitCallbacks({ name: 'Training Performance' }, ['loss', 'mse'], { height: 200, callbacks: ['onEpochEnd'] }),
});

相关参数说明如下(其他参数可参考官方开发文档):

  • batchSize(批大小)指每个循环中使用的样本数,通常取值为 32~512
  • epochs 指定整个训练集上的数据的总循环次数
  • shuffle 指是否在每个 epochs 中打乱训练样本的次序
  • callbacks 指定了训练过程中的回调函数

神经网络的训练是循环进行的,假设总训练样本大小为 320 个,那么上面的示例代码所描述的训练过程是:先使用下标为 0^31 的样本来训练神经网络,然后使用 optimizer 来更新一次权重,再使用下标为 32^63 的样本进行训练,再更新权重,直到总样本中所有数据均被使用过一次,上述过程被称为一个 epoch,接着打乱整个训练样本的次序,再重复共计 50 轮,callbacks 回调函数参数直接关联了 tfvis 库,它是 TensorFlow 提供的专用可视化工具模块。

Evaluate阶段需要对模型的训练结果进行评估,调用模型实例的 evaluate 方法就可以使用测试数据来获得损失函数和度量标准的数值。你可能已经注意到 TensorFlow 在定制训练过程时更加关注如何使用样本数据,而并没有将“度量指标小于给定阈值”作为训练终止的条件(例如 brain.js 中就可以通过设置 errorthresh 参数),在复杂神经网络的构建和设计中,开发者很可能需要一边构建一边进行非正式的训练测试,度量指标最终并不一定能够降低到给定的阈值以下,以此作为训练终止条件很可能会使训练过程陷入无限循环,所以使用固定的训练次数配合可视化工具来观察训练过程就更为合理。

Predict阶段是使用神经网络模型进行预测的阶段,这也是前端工程师参与度最高的部分,毕竟模型输出的结果只是数据,如何利用这些预测结果来制作一些更有趣或者更加智能化的应用或许才是前端工程师更应该关注的问题。从前文的过程中不难看出,TensorFlow.js 提供的能力是围绕神经网络模型展开的,应用层很难直接使用,开发者通常都需要借助官方模型仓库中提供的预训练模型或者使用其他基于 TensorFlow.js 构建的第三方应用,例如人脸识别框架 face-api.js(它可以在浏览器端和 Node.js 中实现快速的人脸追踪和身份识别),语义化更加明确的机器学习框架 ml5.js(可以直接调用 API 来实现图像分类、姿势估计、人物抠图、风格迁移、物体识别等更加具体的任务),可以实现手部跟踪的 handtrack.js 等等,如果 TensorFlow 的相关知识让你觉得过于晦涩,也可以先尝试使用这些更高层的框架来构建一些有趣的程序。

# 使用 TensorFlow.js 构建卷积神经网络

卷积神经网络(Convolutional Neural Networks,简称 CNN) 是计算视觉领域应用非常广泛的深度学习模型,它在处理图片或其他具有网格状特征的数据时具有非常好的表现。在信息处理时,卷积神经网络会先保持像素的行列空间结构,通过多个数学计算层来进行特征提取,然后再将信号转换为特征向量将其接入传统神经网络的结构中,经过特征提取的图像所对应的特征向量在提供给传统神经网络时体积更小,需要训练的参数数量也会相应减少。卷积神经网络的基本工作原理图如下(图中各个层的数量并不是固定的):

章节配图

为了搞清楚卷积网络的工作流程,需要先了解卷积池化这两个术语的含义。

卷积层需要对输入信息进行卷积计算,它使用一个网格状的窗口区(也被称为卷积核或过滤器)对输入图像进行遍历加工,过滤器的每个窗口单元通常都具有自己的权重,从输入图像的左上角开始,将权重和窗口覆盖区域的数值相乘并累加后得到一个新的结果,这个结果就是该区域映射后的值,接着将过滤器窗口向右滑动固定的距离(通常为 1 个像素),然后重复前面的过程,当过滤器窗口的右侧和输入图像的右边界重合后,窗口向下移动同样的距离,再次从左向右重复前面的过程,直到所有的区域遍历完成后就可以得到新的行列数据。每将一个不同的过滤器应用于输入图像后,卷积层就会增加一个输出,真实的深度网络中可能会使用多个过滤器,所以在卷积神经网络的原理图中通常会看到卷积层有多个层叠的图像。不难计算,对于一个输入尺寸为 MM 的图像,使用 NN 的过滤器处理后,新图像的单边尺寸为 M-N+1。例如一个输入尺寸是 88 的灰度图,使用 33 过滤器对其进行卷积计算后,就会得到一个 6*6 的新图片,如下图所示:

章节配图

不同的过滤器可以识别出图像中不同的微小特征,例如上图中的过滤器,对于一个 33 大小的纯色区域,卷积计算的结果均为 0,假设现在有一个上白下黑的边界,那么过滤器中上侧的计算结果会非常小,而中间一行和下面一行的结果都接近 0,卷积计算的累加结果也会映射为一个很小的负数,相当于过滤器将一个 33 区域内的典型特征记录在 1 个像素中,也就达到了特征提取的目的,很明显,如果将上面的过滤器旋转 90°,就可以用来识别图像中的垂直边界。由于卷积计算会将一个区域内的特征缩小到一个点上,所以卷积层的输出信息也被称为特征映射图。本章的代码仓中笔者基于 canvas 实现了一个简单的卷积计算程序,你可以在源码中修改过滤器的参数来观察处理后的图像,这就好像是在给图片添加各种有趣的滤镜一样:

章节配图

上图分别展示了水平边缘检测、垂直边缘检测和斜线边缘检测处理后的效果。

再来看看池化层(也被称为混合层、合并层或下采样层),它通常紧接着卷积层之后来使用。图像中相邻像素的值通常比较接近,这会导致卷积层输出结果的产生大量信息冗余,比如一个水平边缘在卷积层中周围的像素可能也检测到了水平边缘,但事实上它们表示的是原图中的同一个特征,池化层的目的是就是简化卷积层的输出信息,它输出的每个单元可以被认为概括了前一层中一个区域的特征,常用的最大池化层就是在区域内选取一个最大值来作为整个区域在池化层的映射(这并不是唯一的池化计算方法),假设前文示例中的 66 的卷积层输出后紧接着一个使用 22 大小的窗口来进行区域映射的最大池化层,那么最终将得到一个 3*3 的图像输出,过程如下图所示:

章节配图

可以看到,在不考虑深度影响时,示例中 88 的输入图像经过卷积层和池化层的处理后已经变成 33 大小了,对于后续的全连接神经网络而言,输入特征的数量已经大幅减少了。本章代码仓库中也提供了经过“卷积层+最大池化层”处理后图像变化的可视化示例,直观效果其实就是图片缩放,可以看到缩放后的图片仍然保持了池化前的典型特征:

章节配图

在对复杂画面进行分析时,“卷积+池化”的模式可能会在网络中进行多次串联,以便可以从图像中逐级提取特征。在实际开发过程中,为了解决具体的计算视觉问题,开发者很可能需要自己去查阅相关学术论文并搭建相关的深度学习网络,它们通常使用非常简洁的符号来表示,下一节中我们将以经典的 LeNet-5 模型为例来学习相关的知识。

# 体验 Tensorflow.js 开箱即用编程

(一)安装和引用 Tensorflow.js

在 html 页面中可直接引用一个 js 文件即可。cdn 地址是:

https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@1.0.0/dist/tf.min.js

npm 安装使用:npm i @tensorflow/tfjs即可。

以上是浏览器端的安装方式,如果运行在 node 环境可选择安装 node 版本,

npm 安装:

npm i @tensorflow/tfjs-node

因为 node 版本是运行 c++环境,所以在执行这个命令前需要下载很多 c++环境的安装包。

(二)第一个 TF 程序:

<script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@1.0.0/dist/tf.min.js " type="text/javascript"></script>
<script type="text/javascript">
  var a = tf.tensor([1, 2]);
  a.print();
  console.log(a);
</script>

就是这么简单,跟使用 jQuery 这些库是一样的。

(三)体验机器学习编程思想,初识 Tensorflow.js 的 API

章节配图

上图代码:

const model = tf.sequential();
model.add(
  tf.layers.dense({
    units: 4,
    inputShape: [2],
    activation: 'relu',
  })
);
model.add(
  tf.layers.dense({
    units: 1,
    activation: 'sigmoid',
  })
);
model.compile({
  loss: tf.losses.logLoss,
  optimizer: tf.train.adam(0.1),
});
const inputs = tf.tensor(data.map((p) => [p.x, p.y]));
const labels = tf.tensor(data.map((p) => p.label));
await model.fit(inputs, labels, {
  epochs: 10,
});
window.predict = (form) => {
  const pred = model.predict(tf.tensor([[form.x.value * 1, form.y.value * 1]]));
  alert(`预测结果:${pred.dataSync()[0]}`);
};

上面代码是使用 Tensorflow.js 分析 XOR 数据集的部分代码。代码描述了从创建神经网络到训练神经网络最终进行预测。

从代码中可以看出,没有写一句 IF 判断语句,全部都是在使用 Tensorflow.js 提供的 API 进行构建神经网络。所以机器学习的编程是通过构建神经网络来实现程序,而不是通过规则判断来编写程序。

于此同时可以看出使用 Tensorflow.js 构建神经网络相当容易,只需要调用 API 设置你想要的构建元素即可完成。无需编写过多的数学理论方法。比如激活函数 sigmoid,你无需重新编写实现 sigmoid 数学函数,TF 已经提供了该函数,设置调用即可。

途中的注释中描述了很多名词,比如:神经元、损失函数、优化器、神经层、激活函数等等,这些名词都是学习机器学习的一些基础知识。本文不对这些基础知识做一一详解,如有机会再详细对这些基础知识做一次整合讲解。

# 使用 Tensorflow.js 解决问题

下面我们看下 Tensorflow.js 如何进行机器学习编程的实例,除了上面介绍需要 npm 安装@tensorflow/tfjs 这个包外,还需要额外安装@tensorflow/tfjs-vis 可视化包。这个包不影响机器训练,功能是为了将训练过程可视化到浏览器当中。

(一)线性回归问题:

已知 x 轴值 1、2、3、4 对应 y 轴值为 1、3、5、7,可得如下坐标图:

章节配图

传统的规则编程也可轻松完成这样的线性回归函数,并且还能准确预测,比方说 x 轴为 100.5 时对应的 y 值是多少。下面我们看下在 Tensorflow.js 中如何用机器学习的编程方式实现。

import * as tf from '@tensorflow/tfjs';
import * as tfvis from '@tensorflow/tfjs-vis';
window.onload = async () => {
  const xs = [1, 2, 3, 4];
  const ys = [1, 3, 5, 7];
  tfvis.render.scatterplot({ name: '线性回归训练集' }, { values: xs.map((x, i) => ({ x, y: ys[i] })) }, { xAxisDomain: [0, 5], yAxisDomain: [0, 8] });
  const model = tf.sequential();
  model.add(tf.layers.dense({ units: 1, inputShape: [1] }));
  model.compile({ loss: tf.losses.meanSquaredError, optimizer: tf.train.sgd(0.1) });
  const inputs = tf.tensor(xs);
  const labels = tf.tensor(ys);
  await model.fit(inputs, labels, {
    batchSize: 4,
    epochs: 200,
    callbacks: tfvis.show.fitCallbacks({ name: '训练过程' }, ['loss']),
  });
  const output = model.predict(tf.tensor([5]));
  alert(`如果 x 为 5,那么预测 y 为 ${output.dataSync()[0]}`);
};

以上代码将输入值[1,2,3,4]和输出结果[1,3,5,7]作为训练集,这个训练集只有 4 组个数据,但是通过机器反复学习这 4 个数据集,就能预测出很接近的最终值。

这里使用 sequential 模型构建学习的神经网络,模型损失函数采用均方误差即代码中的 tf.losses.meanSquaredError,优化器使用随机梯度下降即 tf.train.sgd(0.1)。

最后将训练数据集给 model 进行训练,也就是调用了 model.fit 方法。batchSize 是指每次训练数据个数,epochs 是指训练次数。代码中表示每次训练 4 个数据,一共训练 200 次。

运行程序,浏览器中会显示整个训练过程:

章节配图

由图可以看出 loss 也就是训练损失已平滑的曲线不断降低,损失越低表示训练结果越接近真实结果。

最终训练完毕会执行 model.predict 方法进行预测,代码中预测 x 为 5 时 y 值是多少,根据人类的大脑对这个简单函数的预测应该是为 9,而程序实际输出是:

章节配图

无限接近于 9。这虽然没有完全等于 9,但是已经相当有参考价值,因为训练损失是很难降低到 0,而且这段机器学习程序只重复训练 4 个数据集。

(二)解决逻辑回归问题:

章节配图

如图,我们要输入 xy 的坐标轴,然后预测该坐标属于黄色还是蓝色。这张图中,黄色和蓝色之间有少许的杂音而且还有很多空白地区没有显示出对应数据,图表也不可能显示完全部数据,这个时候就无法使用传统的规则编程进行书写。

我们看下 Tensorflow.js 如何解决这逻辑回归问题:

import * as tf from '@tensorflow/tfjs';
import * as tfvis from '@tensorflow/tfjs-vis';
import { getData } from './data.js';
window.onload = async () => {
  const data = getData(400);
  tfvis.render.scatterplot(
    { name: '逻辑回归训练数据' },
    {
      values: [data.filter((p) => p.label === 1), data.filter((p) => p.label === 0)],
    }
  );
  const model = tf.sequential();
  model.add(
    tf.layers.dense({
      units: 1,
      inputShape: [2],
      activation: 'sigmoid',
    })
  );
  model.compile({
    loss: tf.losses.logLoss,
    optimizer: tf.train.adam(0.1),
  });
  const inputs = tf.tensor(data.map((p) => [p.x, p.y]));
  const labels = tf.tensor(data.map((p) => p.label));
  await model.fit(inputs, labels, {
    batchSize: 40,
    epochs: 20,
    callbacks: tfvis.show.fitCallbacks({ name: '训练效果' }, ['loss']),
  });
  window.predict = (form) => {
    const pred = model.predict(tf.tensor([[form.x.value * 1, form.y.value * 1]]));
    alert(`预测结果:${pred.dataSync()[0]}`);
  };
};

这种逻辑回归问题比上面的线性回归问题明显复杂很多,但是使用 Tensorflow.js 进行编程却没有复杂多少,用的 API 是一样的。只是将损失函数从均方误差 meanSquaredError 变成了对数损失函数 logLoss,因为逻辑回归问题无法使用均方误差进行很好的训练(这是一个数学问题,需要理解对数函数)。优化器采用了 adam,当然优化器也可以使用随机梯度下降(至于优化器函数的选择也是一个数学问题)。

除了这两块有明显的区别,其他代码基本和线性回归的使用的 API 没有差别。

执行代码后我们可以看到整个训练过程的损失变化:

章节配图

在页面输入框可以输入我们想要预测的坐标:

章节配图

此例中预测结果接近 0 的是黄点,接近于 1 的是蓝点,如果预测值是 0.5 左右,那么所预测结果应该是两块区域之间的临界点。

(三)卷积神经网络的编写

卷积神经网络是一个伟大的发明,它可以让机器更加高效的学习图片、声音这些文件特征。

我们来看下 Tensorflow.js 如何构建卷积神经网络来识别手写数字的。在编写之前需要在网络上找到 mnist 数据集,这是经典的手写数字数据集,为我们节约了收集手写数字的图片集。下面看下代码的具体实现:

import * as tf from '@tensorflow/tfjs';
import * as tfvis from '@tensorflow/tfjs-vis';
import { MnistData } from './data';
window.onload = async () => {
  const data = new MnistData();
  await data.load();
  const examples = data.nextTestBatch(20);
  const surface = tfvis.visor().surface({ name: '输入示例' });
  for (let i = 0; i < 20; i += 1) {
    const imageTensor = tf.tidy(() => {
      return examples.xs.slice([i, 0], [1, 784]).reshape([28, 28, 1]);
    });
    const canvas = document.createElement('canvas');
    canvas.width = 28;
    canvas.height = 28;
    canvas.style = 'margin: 4px';
    await tf.browser.toPixels(imageTensor, canvas);
    surface.drawArea.appendChild(canvas);
  }
  const model = tf.sequential();
  model.add(
    tf.layers.conv2d({
      inputShape: [28, 28, 1],
      kernelSize: 5,
      filters: 8,
      strides: 1,
      activation: 'relu',
      kernelInitializer: 'varianceScaling',
    })
  );
  model.add(
    tf.layers.maxPool2d({
      poolSize: [2, 2],
      strides: [2, 2],
    })
  );
  model.add(
    tf.layers.conv2d({
      kernelSize: 5,
      filters: 16,
      strides: 1,
      activation: 'relu',
      kernelInitializer: 'varianceScaling',
    })
  );
  model.add(
    tf.layers.maxPool2d({
      poolSize: [2, 2],
      strides: [2, 2],
    })
  );
  model.add(tf.layers.flatten());
  model.add(
    tf.layers.dense({
      units: 10,
      activation: 'softmax',
      kernelInitializer: 'varianceScaling',
    })
  );
  model.compile({
    loss: 'categoricalCrossentropy',
    optimizer: tf.train.adam(),
    metrics: ['accuracy'],
  });
  const [trainXs, trainYs] = tf.tidy(() => {
    const d = data.nextTrainBatch(1000);
    return [d.xs.reshape([1000, 28, 28, 1]), d.labels];
  });
  const [testXs, testYs] = tf.tidy(() => {
    const d = data.nextTestBatch(200);
    return [d.xs.reshape([200, 28, 28, 1]), d.labels];
  });
  await model.fit(trainXs, trainYs, {
    validationData: [testXs, testYs],
    batchSize: 500,
    epochs: 20,
    callbacks: tfvis.show.fitCallbacks({ name: '训练效果' }, ['loss', 'val_loss', 'acc', 'val_acc'], { callbacks: ['onEpochEnd'] }),
  });
  const canvas = document.querySelector('canvas');
  canvas.addEventListener('mousemove', (e) => {
    if (e.buttons === 1) {
      const ctx = canvas.getContext('2d');
      ctx.fillStyle = 'rgb(255,255,255)';
      ctx.fillRect(e.offsetX, e.offsetY, 25, 25);
    }
  });
  window.clear = () => {
    const ctx = canvas.getContext('2d');
    ctx.fillStyle = 'rgb(0,0,0)';
    ctx.fillRect(0, 0, 300, 300);
  };
  clear();
  window.predict = () => {
    const input = tf.tidy(() => {
      return tf.image
        .resizeBilinear(tf.browser.fromPixels(canvas), [28, 28], true)
        .slice([0, 0, 0], [28, 28, 1])
        .toFloat()
        .div(255)
        .reshape([1, 28, 28, 1]);
    });
    const pred = model.predict(input).argMax(1);
    alert(`预测结果为 ${pred.dataSync()[0]}`);
  };
};

上面代码中,有一大半是 canvas 画布的操作代码,可以不进行研究。在创建模型 model 之前的代码是将 20 张实例图片挂载到页面显示,也可以不进行研究。而 Tensorflow.js 的实际书写代码比线性回归相比之多了十来行。我们来解析下这部分代码。

这个卷积神经网络构建一共就分了 5 层,2 层 2D 卷积层,2 层池化层,1 层输出层。tf.layers.conv2d 创建的是 2d 卷积层,卷积层之后创建一个 tf.layers.maxPool2d 最大池化层对上一次卷积特征进行优化。进行两次卷积提取特征和池化后使用了 tf.layers.flatten 将立体数据平铺,最后根据平铺数据通过 tf.layers.dense 全链接层作为预测输出。这样一个简单的卷积神经网络就构建完成。

后面是将图片数据读取 1000 张图片转换成 Tensorflow.js 可运算的数据 Tensor(张量),以及随机读取这 1000 张以外的 200 张验证数据。

model.fit 方法进行模型的训练,这里将训练集和验证集都放入训练过程。验证集的目的是为了验证模型训练效果是否偏离了轨道,也就是是否出现过拟合或者欠拟合的情况。

运行代码后的结果如图:

章节配图

我们可以看到 loss 训练损失在平滑下降,acc 是训练准确度。val_loss 和 val_acc 是验证集的损失值和准确度。这里的模型我们只训练了 1000 张图片,然后只训练了 20 次。在如此小的数据集以及训练次数的情况下已经可以达到惊人的效果。

我们在页面画布上可以随意写数字 0-9,模型就可以进行判断我们写的是什么数字。如图效果:

章节配图

案例使用小结:

  • 上面例子只是让大家通过具体案例和代码实现来更加深入了解机器学习是如何编程的,以及 Tensorflow.js 的强大。

  • 机器学习的编程和传统编程有着思维上的不同。传统编程要求程序逻辑缜密,对条件判断做出人为认定。而机器学习编程不在拘泥于规则编写,而是构建神经网络让计算机进行特征的学习。

  • 而 Tensorflow 给我们程序猿封装了很多构建神经网络、训练模型的 API。

# 本篇小结

上述简要介绍了人工智能、机器学习这些 AI 领域的概念,以及让大家感受了一下 Tensorflow.js 在机器学习上是如何编程的。本篇文章主要目的是带大家认识人工智能编程的世界。

Tensorflow.js 并不是最热门最高效的机器学习框架,但是由于它使用的语言是 Javascript 以及开箱即用的 API,这些条件可以让 WEB 开发者们用比较低的学习成本进行人工智能编程领域的学习。

如今很多大厂对人工智能的研发都投入了大量的人力资金,Tensorflow.js 在移动端也出现很多应用,最近比较有名的实时彩妆就是使用 Tensorflow.js 进行编写的小程序。至于未来 AI 的发展是迅猛的,人工智能编程未来很可能是每个程序猿的必修课。所以在此勉励程序猿任重而道远,大家共勉之。

# 其他拓展

人工智能是一门庞大的学科,机器学习只是它的一个分支,它的分支可以用网上的下面这张图来描述:

章节配图

从上图中可以看到机器学习也有庞大的分支,其中 Deep Learning(深度学习)是近年来最火热的课题,深度学习训练的效果会更加智能。Tensorflow 可以进行深度学习的编程。不过深度学习需要庞大的数据量以及强大的计算量,传统的机器学习不需要庞大的数据量。所以两者有着不可分割的关系。

下面是一些有关人工智能的一些学习网站,这些网站不仅限于 Tensorflow.js,更多是关于机器学习的基础知识:

前端入门机器学习 Tensorflow.js 简明教程 (opens new window)