Tensorflow.js(二):深度学习如何在前端发挥作用
A) 机器学习 & 深度学习
这两个概念非常容易混淆,在开始之前,需要简单介绍一下这两个概念,简单来说,机器学习需要提前设置算法模型进行约束,而深度学习不需要。
a) 机器学习
这是机器学习训练模型的步骤
- 选择一个模型结构
- 将训练数据输入模型
- 在大量运算后,机器输出最优模型(即具有使训练错误最小化的特定参数的模型)
线性回归,逻辑回归,决策树,支持向量机,贝叶斯模型,正则化模型...这些都是机器学习发展所带来的预测模型。这些预测模型中的每一个都基于特定的算法结构,参数都是可调的。
每个模型对不同的实际场景,所发挥的效果也不尽相同,如果选错模型,会使结果差强人意,会出现“过拟合”和“欠拟合”的情况,
简单来说,当模型较为复杂(复杂模型具有丰富的参数并且可以适应广泛的数据形状),但是可供训练的数据量较少时,会出现过拟合现象,过拟合是指形状过分紧密贴合训练数据,这种情况下,模型对于以往的事情会体现出高准确率的预测值,但是对于未来,会体现出极不理想的预测值。
欠拟合则是指选定的模型不足以适应训练数据,通常是指模型过于简单,而实际情况较为复杂。举个例子:用线性模型(y=ax+b)去拟合含有二次方关系的数据(y=5x^2),效果会非常不理想。为了缓解“不适应的问题”,人们通常会运用他们的“领域知识”来提出“输入特征”,上面的例子,通过创建一个特征z = x ^ 2, 就可以通过线性回归模型进行拟合了(y=5z)
b) 深度学习
对机器学习来说,特征提取并不简单,往往需要大量的时间去优化,而深度学习便机器可以自动学习特征和任务之间的关联,还能从简单特征中提取复杂的特征。深度学习的概念源于人工神经网络的研究,而人工神经网络又来源于对生物神经网络的研究。
当人们受到外界刺激时,会通过神经末梢传导电信号到神经元,神经元会开始联结,产生特定的记忆。在下次遇到同样刺激时,会根据神经元的联结做出相应反馈。

上个世纪六十年代,"人造神经元"模型被提出,叫做"感知器"(perceptron),直到今天还在用。比如下图中的感知器,上面三个外部输入就是因素,最后感知器的输出就是结果。根据因素的权重和整体的阈值,可以影响输出。
一个感知器可以解决较为简单的事情,现实生活的事情往往要比这复杂很多。这时,我们可以将多个感知器连接,以处理更复杂的事情。这样一来,我们就有了层的概念。
通过不断改变神经元的权重与阈值,来观察与样本数据的误差,并通过反向传播优化,这就是所谓的训练了
总体而言,人工神经网络靠的是正向和反向传播来更新神经元, 从而形成一个理想的神经网络, 本质上, 这是一个能让计算机处理和优化问题的数学模型. 而生物神经网络是通过刺激, 产生新的联结, 让信号能够通过新的联结传递而形成反馈,两者有很大的不同。
B) 基本单位
无论是 tfjs 还是 tfpy,无论是在浏览器还是在系统内执行,都是在通过同一套规范进行运作。在开始之前,对于机器学习的基本单位 Tensor,我们需要重温一下它的概念。
就像上一期 tensorflow 系列所介绍的那样,标量(0阶)如下图,它的形状(shape)是[]

向量(1阶)如下图,它的形状是[3]

矩阵(2阶)如下图,它的形状是[3, 2]

来到3阶及以上,我们称为n阶张量。下面是一个三阶张量,它有着不同的表现形式,它的形状是[3, 2, 5]



四阶张量可以理解为多个三阶张量的结合,下面这个4阶张量的形状是[3, 2, 4, 5]

有点晕,该如何理解形状(shape)?其实就是多维数组中每一维的长度。
const tensor = 4 // [null]
// [tensor1d.length] => [3]
const tensor1d = [2, 3, 4]
// [tensor2d.length, tensor2d[0].length] => [3, 2]
const tensor2d = [[1, 2], [3, 4], [5, 6]]
// [tensor3d.length, tensor3d[0].length, tensor3d[0][0].length] => [3, 2, 5]
const tensor3d =
[[[ 0 1 2 3 4]
[ 5 6 7 8 9]]
[[10 11 12 13 14]
[15 16 17 18 19]]
[[20 21 22 23 24]
[25 26 27 28 29]]]
C) 第一例
简单了解了上述内容后,我们就可使开始动手做一些事情了。就从简单的线性回归开始吧。
let x = [1, 2, 3, 4, 5, 6];
let y = [7, 12, 17, 22, 27, 32];
我们需要知道输入输出,以及神经网络的结构。参照 demo-1,我们给出了 x 和 y,分别对应输入和输出,
model.add(
tf.layers.dense({
inputShape: [1],
units: 1,
})
);
我们添加了一个稠密层,内含一个神经元,这对于我们解决线性回归问题,已经足够了。
model.compile({
optimizer: tf.train.adam(0.1),
loss: "meanSquaredError",
});
接下来定义优化器和损失函数,就可以开始训练了。在上一期中,我们已经介绍了优化器和损失函数的作用,在这里不再赘述。
D) 预测鼠标轨迹
鼠标轨迹或许是普通人能轻而易举大量获取的数据了,只需要动动手指而已。
a) 设计模型与训练
我们该如何定义输入和输出呢?我们可以很轻易的知道,输出是一个向量,是一个包含 x 和 y 坐标的数组。那么输入呢?也是一个向量?
当然可以这样,但这显然不是最好的方案。想象一下,我们拿着一个坐标去预测下一个坐标,很容易让人摸不着头脑,一个坐标能让我们知道位置,但是一段时间内的坐标组,能让我们知道路径。我们需要一组坐标作为参考。
let input = [
[
[1, 1], [2, 2], [3, 3] ... [9, 9]
],
[
[1, 2], [3, 4], [5, 6] ... [8, 9]
],
...
]
let output = [
[2, 3],
[4, 5],
...
]
那么输入和输出就已经搞定了,接下来是神经网络的设计问题。在这里,我们引入“时间序列”会更好的解决问题,但是这涉及到递归神经网络的范畴,会带来更多抽象且复杂的概念,所以暂时先不用这个,我们依旧用稠密层来解决问题,顺便加了个激活函数来提升效果。
model.add(
tf.layers.dense({
inputShape: [cont, 2],
units: 80,
})
);
model.add(
tf.layers.dense({
activation: "relu",
units: 40,
})
);
model.add(tf.layers.flatten()); // 降维
model.add(
tf.layers.dense({
units: 2,
})
);
见 demo-2 代码,我们首先收集了1000个鼠标的连续坐标信息,并将每20个连续坐标合并成一个小组,输出是一个坐标。
不出意外的话,训练结果会符合预期,能基本判断出鼠标在10步之后的坐标。
b) 轨迹/用户意图预测能否协助网页优化
现在,网页都是在被动地等待着用户的指令,在收到指令后才开始执行。或许在前一秒,CPU还无所事事,后一秒接到指令就要突然执行大量的运算;亦或是前一秒,页面没有进行网络请求,下一秒接到指令就发起了多条网络请求;再或者前一秒页面无任何动作,后一秒就接到指令加载分包/异步资源。
这种不均衡形成了性能和网络使用率的峰和谷。

不可否认的是,大多数情况下(如网络状态良好,硬件性能良好),这些突如其来的任务并不会过多地影响页面的流畅性,但是或多或少会带来些许卡顿,或者无聊的加载时间。在遇到网络环境差,设备卡顿,或者网页较为特殊时,这种较差的体验会被放大。
常规的优化手段,在我看来,存在着边际效用递减。简单来说,我把加载速度从10秒优化到3秒,经过大刀阔斧的改造后,或许很容易达到目的,但是从3秒优化到1秒,我们要付出成倍的成本和努力,从1秒继续往下优化,会变得十分寸步难行。
但是网络和硬件在这一段时间并不是100%在被使用的,只是大量任务集中在一个时间点发生了而已,我们通过预测用户意图,来进行削峰填谷的优化,不失为一个好的方案。
上述只是观点,具体是否可行,有待考究和验证。
E) 验证码识别
在这里,我们需要引入卷积神经网络的概念。
a) 深度学习如何识别验证码
前面已经提到,图片是由数字组成的,黑白图片只有两种颜色,用 [0-1] 替代,灰度图片由255种颜色组成,用 [0-255] 替代,彩色图片有三个通道(rgb),由 [0-255, 0-255, 0-255] 组成。
就像上图,一个很随意数字“8”,是由一个矩阵组成的,也就是一个二阶张量。
第一步:卷积操作
卷积操作可以帮助我们提取图片的特征,假设有以下图像

这是一个特征检测器,你可以把它想象成一个滤镜

特征检测器会自上而下,从左到右扫描图像,每次输出特征检测器中的数字的乘积之和。注意,卷积操作会保留图片的局部依赖关系。
一般会有多个特征检测器,每个负责提取特定的特征,拥有的特征检测器数量越多,提取的图像特征就越多,我们的神经网络识别看不见的特征的能力就越好。
下图会直观地展示特征检测器是如何工作的
第二步:引入非线性
relu 激活函数将负值变为0,经过 relu 处理过的图像,参考下图
第三步:池化操作
池化操作(polling)也是一种处理手段,它降低了每个特征图的维数,但保留了最重要的信息。空间池可以有不同的类型:Max、Average、Sum 等,下图就是 Max polling。
经过池化处理之后,图像会变成这样:
第四步:全连接层
全连接层不再对图像进行处理,这是会对特征图进行分类。只要有足量的带标签数据,就能增强特征与对应输出的联系。
总览
至此,图像识别就完成了,总流程如下。

验证码是如何被识别的?道理也是一样的。

b) 卷积神经网络与低代码
既然图片可以通过卷积神经网络来识别成相应的物体,还能识别图片中物体的位置,那么,我们是否可以运用到低代码上,实现所见即所得?通过识别成品图片各类元素的位置,来生成基本符合要求的 dom 结构,再导入到某种低代码平台进行手动调整和逻辑编写,听起来很美好。
如果能优雅的自动调整就更美好了。
英伟达推出的 AI painter (传送门)着实让人吃惊,这其中又是另外一个概念“生成对抗网络”了。其给予了“画网页”一种可能性。试想寥寥几笔原型图即可变化为有模有样的网页成品,太令人激动了。
鉴于我对卷积神经网络还不是特别了解,就不多做讨论了,而且这些似乎都是很遥远的事,当前低代码甚至都还没有推广开来。
tensorflow.js 到底带来了什么
前面举了很多例子,扯来扯去,就是在说 tensorFlow.js 赋予了我们在浏览器内进行训练,或是利用模型直接预测的能力。
从结果来看
从前,如果我们想要获得数据的预测结果,则需要从云服务中获取,如果输入是视频,图片,那么实时获取结果的成本会很高,而且会给云服务造成较大的压力,由于返回的延时,造成的体验也不会很好。
现在,我们可以在浏览器内直接进行预测。甚至可以利用个体的个性化数据继续进行训练,以获得更贴近个体的预测结果。
从过程来看
这或许会带动个性化模型的需求,即每个人都可以在浏览器里训练自己的模型。tensorflow.js 摆脱了环境依赖(Python),也就降低了训练成本,将自己的数据留在本地训练,也确保了信息的安全性。如果有一个平台提供了这样的能力,或许它会成为模型界的 github...
不可忽视的是,浏览器是否能为机器学习提供足够的性能,这也是关键所在。
References
ujjwalkarn: An Intuitive Explanation of Convolutional Neural Networks