听说你用JavaScript写代码?本文是你的机器学习指南

JavaScript 是一种流行的高级编程语言,它被世界上的绝大多数网站所使用,也被所有主流浏览器所支持。随着深度学习的火热,越来越多开发者开始探索使用 JavaScript 实现人工智能与机器学习算法。近日,来自德国的 Robin Wieruch 发布了一系列使用 JavaScript 构建机器学习的教程,本文将主要介绍使用 JavaScript 实现神经网络的方法。

有网友对此表示:「我本想写一篇激烈的反驳文,其中阐述如果没有 GPU 的支持,这种做法是毫无意义的……但它可以使用 WebGL 来应用 GPU 的能力。而且,这可能比你在本地桌面上安装 TensorFlow 堆栈要简单一万倍。」

近期,原作者发表了一系列有关在 JavaScript 上实现人工智能和机器学习算法的文章,其中包括:

这些机器学习算法的实现是基于 math.js 库的线性代数(如矩阵运算)和微分的,你可以在 GitHub 上找到所有这些算法:

GitHub 链接: https://github.com/javascript-machine-learning

如果你发现其中存在任何缺陷,欢迎对这个资源提出自己的改进,以帮助后来者。我希望不断为 web 开发者们提供更多、更丰富的机器学习算法。

就我个人来说,我发现实现这些算法在某种程度上是一个非常具有挑战性的任务。特别是当你需要在 JavaScript 上实现神经网络的前向和反向传播的时候。由于我自己也在学习神经网络的知识,我开始寻找适用于这种工作的库。希望在不久的将来,我们能够轻松地在 GitHub 上找到相关的基础实现。然而现在,以我使用 JavaScript 的阅历,我选择了谷歌发布的 deeplearn.js 来进行此项工作。在本文中,我将分享使用 deeplearn.js 和 JavaScript 实现神经网络从而解决现实世界问题的方式——在 web 环境上。

首先,我强烈推荐读者先学习一下深度学习著名学者吴恩达的《机器学习》课程。本文不会详细解释机器学习算法,只会展示它在 JavaScript 上的用法。另一方面,该系列课程在算法的细节和解释上有着令人惊叹的高质量。在写这篇文章之前,我自己也学习了相关课程,并试图用 JavaScript 实现来内化课程中的相关知识。

神经网络的目的是什么?

本文实现的神经网络需要通过选择与背景颜色相关的适当字体颜色来改善网页可访问性。比如,深蓝色背景中的字体应该是白色,而浅黄色背景中的字体应该是黑色。你也许会想:首先你为什么需要一个神经网络来完成任务?通过编程的方式根据背景颜色计算可使用的字体颜色并不难,不是吗?我很快在 Stack Overflow 找到了该问题的解决办法,并根据我的需求做了调整,以适应 RGB 空间中的颜色。

function getAccessibleColor(rgb) {
 let [ r, g, b ] = rgb;
 let colors = [r / 255, g / 255, b / 255];
 let c = colors.map((col) => {
   if (col <= 0.03928) {
     return col / 12.92;
   }
   return Math.pow((col + 0.055) / 1.055, 2.4);
 });
 let L = (0.2126 * c[0]) + (0.7152 * c[1]) + (0.0722 * c[2]);
 return (L > 0.179)
   ? [ 0, 0, 0 ]
   : [ 255, 255, 255 ];
}

当已经有一个编程的方法可以解决该问题的时候,使用神经网络对于该现实世界问题价值并不大,没有必要使用一个机器训练的算法。然而,由于可通过编程解决这一问题,所以验证神经网络的性能也变得很简单,这也许能够解决我们的问题。查看该 GitHub 库(https://github.com/javascript-machine-learning/color-accessibility-neural-network-deeplearnjs)中的动图,了解它最终表现如何,以及本教程中你将构建什么。如果你熟悉机器学习,也许你已经注意到这个任务是一个分类问题。算法应根据输入(背景颜色)决定二进制输出(字体颜色:白色或黑色)。在使用神经网络训练算法的过程中,最终会根据输入的背景颜色输出正确的字体颜色。

下文将从头开始指导你设置神经网络的所有部分,并由你决定把文件/文件夹设置中的部分合在一起。但是你可以整合以前引用的 GitHub 库以获取实现细节。

JavaScript 中的数据集生成

机器学习中的训练集由输入数据点和输出数据点(标签)组成。它被用来训练为训练集(例如测试集)之外的新输入数据点预测输出的算法。在训练阶段,由神经网络训练的算法调整其权重以预测输入数据点的给定标签。总之,已训练算法是一个以数据点作为输入并近似输出标签的函数。

该算法经过神经网络的训练后,可以为不属于训练集的新背景颜色输出字体颜色。因此,稍后你将使用测试集来验证训练算法的准确率。由于我们正在处理颜色,因此为神经网络生成输入颜色的样本数据集并不困难。

function generateRandomRgbColors(m) {
 const rawInputs = [];
 for (let i = 0; i < m; i++) {
   rawInputs.push(generateRandomRgbColor());
 }
 return rawInputs;
}
function generateRandomRgbColor() {
 return [
   randomIntFromInterval(0, 255),
   randomIntFromInterval(0, 255),
   randomIntFromInterval(0, 255),
 ];
}
function randomIntFromInterval(min, max) {
 return Math.floor(Math.random() * (max - min + 1) + min);
}

generateRandomRgbColors() 函数创建给定大小为 m 的部分数据集。数据集中的数据点是 RGB 颜色空间中的颜色。每种颜色在矩阵中被表征为一行,而每一列是颜色的特征。特征是 RGB 空间中的 R、G、B 编码值。数据集还没有任何标签,所以训练集并不完整,因为它只有输入值而没有输出值。

由于基于已知颜色生成可使用字体颜色的编程方法是已知的,因此可以使用调整后的功能版本以生成训练集(以及稍后的测试集)的标签。这些标签针对二分类问题进行了调整,并在 RGB 空间中隐含地反映了黑白的颜色。因此,对于黑色,标签是 [0,1];对于白色,标签是 [1,0]。

function getAccessibleColor(rgb) {
 let [ r, g, b ] = rgb;
 let color = [r / 255, g / 255, b / 255];
 let c = color.map((col) => {
   if (col <= 0.03928) {
     return col / 12.92;
   }
   return Math.pow((col + 0.055) / 1.055, 2.4);
 });
 let L = (0.2126 * c[0]) + (0.7152 * c[1]) + (0.0722 * c[2]);
 return (L > 0.179)
   ? [ 0, 1 ] // black
   : [ 1, 0 ]; // white
}

现在你已经准备好一切用于生成(背景)颜色的随机数据集(训练集、测试集),它被分类为黑色或白色(字体)颜色。

function generateColorSet(m) {
 const rawInputs = generateRandomRgbColors(m);
 const rawTargets = rawInputs.map(getAccessibleColor);
 return { rawInputs, rawTargets };
}

使神经网络中底层算法更好的另一步操作是特征缩放。在特征缩放的简化版本中,你希望 RGB 通道的值在 0 和 1 之间。由于你知道最大值,因此可以简单地推导出每个颜色通道的归一化值。

function normalizeColor(rgb) {
 return rgb.map(v => v / 255);
}

你可以把这个功能放在你的神经网络模型中,或者作为单独的效用函数。下一步我将把它放在神经网络模型中。

JavaScript 神经网络模型的设置阶段

现在你可以使用 JavaScript 实现一个神经网络了。在开始之前,你需要先安装 deeplearn.js 库:一个适合 JavaScript 神经网络的框架。官方宣传中说:「deeplearn.js 是一个开源库,将高效的机器学习构造块带到 web 中,允许在浏览器中训练神经网络或在推断模式下运行预训练模型。」本文,你将训练自己的模型,然后在推断模式中运行该模型。使用该库有两个主要优势:

  • 首先,它使用本地电脑的 GPU 加速机器学习算法中的向量计算。这些机器学习计算与图解计算类似,因此使用 GPU 的计算比使用 CPU 更加高效。
  • 其次,deeplearn.js 的结构与流行的 TensorFlow 库类似(TensorFlow 库也是谷歌开发的,不过它使用的是 Python 语言)。因此如果你想在使用 Python 的机器学习中实现飞跃,那么 deeplearn.js 可提供通向 JavaScript 各领域的捷径。

现在回到你的项目。如果你想用 npm 来设置,那么你只需要在命令行中安装 deeplearn.js。也可以查看 deeplearn.js 项目的官方安装说明文档。

npm install deeplearn

我没有构建过大量神经网络,因此我按照构建神经网络的一般实践进行操作。在 JavaScript 中,你可以使用 JavaScript ES6 class 来推进它。该类可以通过定义神经网络特性和类方法为你的神经网络提供完美的容器。例如,你的颜色归一化函数可以在类别中找到一个作为方法的点。

class ColorAccessibilityModel {
 normalizeColor(rgb) {
   return rgb.map(v => v / 255);
 }
}
export default ColorAccessibilityModel;

或许那也是你的函数生成数据集的地方。在我的案例中,我仅将类别归一化作为分类方法,让数据集生成独立于类别之外。你可以认为未来有不同的方法来生成数据集,不应该在神经网络模型中进行定义。不管怎样,这只是一个实现细节。

训练和推断阶段都在机器学习的涵盖性术语会话(session)之下。你可以在神经网络类别中设置会话。首先,你可以输入来自 deeplearn.js 的 NDArrayMathGPU 类别,帮助你以计算高效的方式在 GPU 上进行数学运算。

import {
 NDArrayMathGPU,
} from 'deeplearn';
const math = new NDArrayMathGPU();
class ColorAccessibilityModel {
 ...
}
export default ColorAccessibilityModel;

第二,声明分类方法类设置会话。其函数签名使用训练集作为参数,成为从先前实现的函数中生成训练集的完美 consumer。第三步,会话初始化空的图。之后,图将反映神经网络的架构。你可以随意定义其特性。

import {
 Graph,
 NDArrayMathGPU,
} from 'deeplearn';
class ColorAccessibilityModel {
 setupSession(trainingSet) {
   const graph = new Graph();
 }
 ..
}
export default ColorAccessibilityModel;

第四步,你用张量的形式定义图中输入和输出数据点的形态。张量是具备不同维度的数组,它可以是向量、矩阵,或更高维度的矩阵。神经网络将这些张量作为输入和输出。在我们的案例中,有三个输入单元(每个颜色通道有一个输入单元)和两个输出单元(二分类,如黑白)。

class ColorAccessibilityModel {
 inputTensor;
 targetTensor;
 setupSession(trainingSet) {
   const graph = new Graph();
   this.inputTensor = graph.placeholder('input RGB value', [3]);
   this.targetTensor = graph.placeholder('output classifier', [2]);
 }
 ...
}
export default ColorAccessibilityModel;

第五步,神经网络包含隐藏层。奇迹如何发生目前仍是黑箱。基本上,神经网络提出自己的交叉计算参数(在会话中经过训练)。不过,你可以随意定义隐藏层的维度(每个单元大小、层大小)。

class ColorAccessibilityModel {
 inputTensor;
 targetTensor;
 setupSession(trainingSet) {
   const graph = new Graph();
   this.inputTensor = graph.placeholder('input RGB value', [3]);
   this.targetTensor = graph.placeholder('output classifier', [2]);
   let connectedLayer = this.createConnectedLayer(graph, this.inputTensor, 0, 64);
   connectedLayer = this.createConnectedLayer(graph, connectedLayer, 1, 32);
   connectedLayer = this.createConnectedLayer(graph, connectedLayer, 2, 16);
 }
 createConnectedLayer(
   graph,
   inputLayer,
   layerIndex,
   units,
 ) {
   ...
 }
 ...
}
export default ColorAccessibilityModel;

根据层的数量,你可以变更图来扩展出更多层。创建连接层的分类方法需要图、变异连接层(mutated connected layer)、新层的索引,以及单元数量。图的层属性可用于返回由名称确定的新张量。

class ColorAccessibilityModel {
 inputTensor;
 targetTensor;
 setupSession(trainingSet) {
   const graph = new Graph();
   this.inputTensor = graph.placeholder('input RGB value', [3]);
   this.targetTensor = graph.placeholder('output classifier', [2]);
   let connectedLayer = this.createConnectedLayer(graph, this.inputTensor, 0, 64);
   connectedLayer = this.createConnectedLayer(graph, connectedLayer, 1, 32);
   connectedLayer = this.createConnectedLayer(graph, connectedLayer, 2, 16);
 }
 createConnectedLayer(
   graph,
   inputLayer,
   layerIndex,
   units,
 ) {
   return graph.layers.dense(
     `fully_connected_${layerIndex}`,
     inputLayer,
     units
   );
 }
 ...
}
export default ColorAccessibilityModel;

神经网络中的每一个神经元必须具备一个定义好的激活函数。它可以是 logistic 激活函数。你或许已经从 logistic 回归中了解到它,它成为神经网络中的 logistic 单元。在我们的案例中,神经网络默认使用修正线性单元。

class ColorAccessibilityModel {
 inputTensor;
 targetTensor;
 setupSession(trainingSet) {
   const graph = new Graph();
   this.inputTensor = graph.placeholder('input RGB value', [3]);
   this.targetTensor = graph.placeholder('output classifier', [2]);
   let connectedLayer = this.createConnectedLayer(graph, this.inputTensor, 0, 64);
   connectedLayer = this.createConnectedLayer(graph, connectedLayer, 1, 32);
   connectedLayer = this.createConnectedLayer(graph, connectedLayer, 2, 16);
 }
 createConnectedLayer(
   graph,
   inputLayer,
   layerIndex,
   units,
   activationFunction
 ) {
   return graph.layers.dense(
     `fully_connected_${layerIndex}`,
     inputLayer,
     units,
     activationFunction ? activationFunction : (x) => graph.relu(x)
   );
 }
 ...
}
export default ColorAccessibilityModel;

第六步,创建输出二分类的层。它有两个输出单元,每一个表示一个离散的值(黑色、白色)。

class ColorAccessibilityModel {
 inputTensor;
 targetTensor;
 predictionTensor;
 setupSession(trainingSet) {
   const graph = new Graph();
   this.inputTensor = graph.placeholder('input RGB value', [3]);
   this.targetTensor = graph.placeholder('output classifier', [2]);
   let connectedLayer = this.createConnectedLayer(graph, this.inputTensor, 0, 64);
   connectedLayer = this.createConnectedLayer(graph, connectedLayer, 1, 32);
   connectedLayer = this.createConnectedLayer(graph, connectedLayer, 2, 16);
   this.predictionTensor = this.createConnectedLayer(graph, connectedLayer, 3, 2);
 }
 ...
}
export default ColorAccessibilityModel;

第七步,声明一个代价张量(cost tensor),以定义损失函数。在这个案例中,代价张量是均方误差。它使用训练集的目标张量(标签)和训练算法得到的预测张量来计算代价。

class ColorAccessibilityModel {
 inputTensor;
 targetTensor;
 predictionTensor;
 costTensor;
 setupSession(trainingSet) {
   const graph = new Graph();
   this.inputTensor = graph.placeholder('input RGB value', [3]);
   this.targetTensor = graph.placeholder('output classifier', [2]);
   let connectedLayer = this.createConnectedLayer(graph, this.inputTensor, 0, 64);
   connectedLayer = this.createConnectedLayer(graph, connectedLayer, 1, 32);
   connectedLayer = this.createConnectedLayer(graph, connectedLayer, 2, 16);
   this.predictionTensor = this.createConnectedLayer(graph, connectedLayer, 3, 2);
   this.costTensor = graph.meanSquaredCost(this.targetTensor, this.predictionTensor);
 }
 ...
}
export default ColorAccessibilityModel;

最后但并非不重要的一步,设置架构图的相关会话。之后,你就可以开始准备为训练阶段导入训练集了。

import {
 Graph,
 Session,
 NDArrayMathGPU,
} from 'deeplearn';
class ColorAccessibilityModel {
 session;
 inputTensor;
 targetTensor;
 predictionTensor;
 costTensor;
 setupSession(trainingSet) {
   const graph = new Graph();
   this.inputTensor = graph.placeholder('input RGB value', [3]);
   this.targetTensor = graph.placeholder('output classifier', [2]);
   let connectedLayer = this.createConnectedLayer(graph, this.inputTensor, 0, 64);
   connectedLayer = this.createConnectedLayer(graph, connectedLayer, 1, 32);
   connectedLayer = this.createConnectedLayer(graph, connectedLayer, 2, 16);
   this.predictionTensor = this.createConnectedLayer(graph, connectedLayer, 3, 2);
   this.costTensor = graph.meanSquaredCost(this.targetTensor, this.predictionTensor);
   this.session = new Session(graph, math);
   this.prepareTrainingSet(trainingSet);
 }
 prepareTrainingSet(trainingSet) {
   ...
 }
 ...
}
export default ColorAccessibilityModel;

不过目前在准备神经网络的训练集之前,设置还没完成。首先,你可以在 GPU 数学计算环境中使用回调函数(callback function)来支持计算,但这并不是强制性的,可自主选择。

import {
 Graph,
 Session,
 NDArrayMathGPU,
} from 'deeplearn';
const math = new NDArrayMathGPU();
class ColorAccessibilityModel {
 session;
 inputTensor;
 targetTensor;
 predictionTensor;
 costTensor;
 ...
 prepareTrainingSet(trainingSet) {
   math.scope(() => {
     ...
   });
 }
 ...
}
export default ColorAccessibilityModel;

其次,你可以解构训练集的输入和输出(标签,也称为目标)以将其转换成神经网络可读的格式。deeplearn.js 的数学计算使用内置的 NDArrays。你可以把它们理解为数组矩阵中的简单数组或向量。此外,输入数组的颜色被归一化以提高神经网络的性能。

import {
 Array1D,
 Graph,
 Session,
 NDArrayMathGPU,
} from 'deeplearn';
const math = new NDArrayMathGPU();
class ColorAccessibilityModel {
 session;
 inputTensor;
 targetTensor;
 predictionTensor;
 costTensor;
 ...
 prepareTrainingSet(trainingSet) {
   math.scope(() => {
     const { rawInputs, rawTargets } = trainingSet;
     const inputArray = rawInputs.map(v => Array1D.new(this.normalizeColor(v)));
     const targetArray = rawTargets.map(v => Array1D.new(v));
   });
 }
 ...
}
export default ColorAccessibilityModel;

第三,shuffle 输入和目标阵列。shuffle 的时候,deeplearn.js 提供的 shuffler 将二者保存在 sync 中。每次训练迭代都会出现 shuffle,以馈送不同的输入作为神经网络的 batch。整个 shuffle 流程可以改善训练算法,因为它更可能通过避免过拟合来实现泛化。

import {
 Array1D,
 InCPUMemoryShuffledInputProviderBuilder,
 Graph,
 Session,
 NDArrayMathGPU,
} from 'deeplearn';
const math = new NDArrayMathGPU();
class ColorAccessibilityModel {
 session;
 inputTensor;
 targetTensor;
 predictionTensor;
 costTensor;
 ...
 prepareTrainingSet(trainingSet) {
   math.scope(() => {
     const { rawInputs, rawTargets } = trainingSet;
     const inputArray = rawInputs.map(v => Array1D.new(this.normalizeColor(v)));
     const targetArray = rawTargets.map(v => Array1D.new(v));
     const shuffledInputProviderBuilder = new InCPUMemoryShuffledInputProviderBuilder([
       inputArray,
       targetArray
     ]);
     const [
       inputProvider,
       targetProvider,
     ] = shuffledInputProviderBuilder.getInputProviders();
   });
 }
 ...
}
export default ColorAccessibilityModel;

最后,馈送条目(feed entries)是训练阶段中神经网络前馈算法的最终输入。它匹配数据和张量(根据设置阶段的形态而定义)。

import {
 Array1D,
 InCPUMemoryShuffledInputProviderBuilder
 Graph,
 Session,
 NDArrayMathGPU,
} from 'deeplearn';
const math = new NDArrayMathGPU();
class ColorAccessibilityModel {
 session;
 inputTensor;
 targetTensor;
 predictionTensor;
 costTensor;
 feedEntries;
 ...
 prepareTrainingSet(trainingSet) {
   math.scope(() => {
     const { rawInputs, rawTargets } = trainingSet;
     const inputArray = rawInputs.map(v => Array1D.new(this.normalizeColor(v)));
     const targetArray = rawTargets.map(v => Array1D.new(v));
     const shuffledInputProviderBuilder = new InCPUMemoryShuffledInputProviderBuilder([
       inputArray,
       targetArray
     ]);
     const [
       inputProvider,
       targetProvider,
     ] = shuffledInputProviderBuilder.getInputProviders();
     this.feedEntries = [
       { tensor: this.inputTensor, data: inputProvider },
       { tensor: this.targetTensor, data: targetProvider },
     ];
   });
 }
 ...
}
export default ColorAccessibilityModel;

这样,神经网络的设置就结束了。神经网络的所有层和单元都实现了,训练集也准备好进行训练了。现在只需要添加两个配置神经网络行为的超参数,它们适用于下个阶段:训练阶段。

import {
 Array1D,
 InCPUMemoryShuffledInputProviderBuilder,
 Graph,
 Session,
 SGDOptimizer,
 NDArrayMathGPU,
} from 'deeplearn';
const math = new NDArrayMathGPU();
class ColorAccessibilityModel {
 session;
 optimizer;
 batchSize = 300;
 initialLearningRate = 0.06;
 inputTensor;
 targetTensor;
 predictionTensor;
 costTensor;
 feedEntries;
 constructor() {
   this.optimizer = new SGDOptimizer(this.initialLearningRate);
 }
 ...
}
export default ColorAccessibilityModel;

第一个参数是学习速率(learning rate)。学习速率决定算法的收敛速度,以最小化成本。我们应该假定它的数值很高,但实际上不能太高了。否则梯度下降就不会收敛,因为找不到局部最优值。

第二个参数是批尺寸(batch size)。它定义每个 epoch(迭代)里有多少个训练集的数据点通过神经网络。一个 epoch 等于一批数据点的一次正向传播和一次反向传播。以批次的方式训练神经网络有两个好处:第一,这样可以防止密集计算,因为算法训练时使用了内存中的少量数据点;第二,这样可以让神经网络更快地进行批处理,因为每个 epoch 中权重会随着每个批次的数据点进行调整——而不是等到整个数据集训练完之后再进行改动。

训练阶段

设置阶段结束后就到了训练阶段了。不需要太多实现,因为所有的基础都已在设置阶段完成。首先,训练阶段可以用分类方法来定义。然后在 deeplearn.js 的数学环境中再次执行。此外,它还使用神经网络实例所有的预定义特性来训练算法。

class ColorAccessibilityModel {
 ...
 train() {
   math.scope(() => {
     this.session.train(
       this.costTensor,
       this.feedEntries,
       this.batchSize,
       this.optimizer
     );
   });
 }
}
export default ColorAccessibilityModel;

训练方法是 1 个 epoch 的神经网络训练。因此,从外部调用时,调用必须是迭代的。此外,训练只需要 1 个 epoch。为了多批次训练算法,你必须将该训练方法进行多次迭代运行。

这就是基础的训练阶段。但是根据时间调整学习率可以改善训练。学习率最初很高,但是当算法在每一步过程中逐渐收敛时,学习率会出现下降趋势。

class ColorAccessibilityModel {
 ...
 train(step) {
   let learningRate = this.initialLearningRate * Math.pow(0.90, Math.floor(step / 50));
   this.optimizer.setLearningRate(learningRate);
   math.scope(() => {
     this.session.train(
       this.costTensor,
       this.feedEntries,
       this.batchSize,
       this.optimizer
     );
   }
 }
}
export default ColorAccessibilityModel;

在我们的情况中,学习率每 50 步下降 10%。下面,我们需要获取训练阶段的损失,来验证它是否随着时间下降。损失可在每一次迭代时返回,不过这样会导致较低的计算效率。神经网络每次请求返回损失,就必须通过 GPU 才能实现返回请求。因此,我们在多次迭代后仅要求返回一次损失来验证其是否下降。如果没有请求返回损失,则训练的损失下降常量被定义为 NONE(之前默认设置)。

import {
 Array1D,
 InCPUMemoryShuffledInputProviderBuilder,
 Graph,
 Session,
 SGDOptimizer,
 NDArrayMathGPU,
 CostReduction,
} from 'deeplearn';
class ColorAccessibilityModel {
 ...
 train(step, computeCost) {
   let learningRate = this.initialLearningRate * Math.pow(0.90, Math.floor(step / 50));
   this.optimizer.setLearningRate(learningRate);
   let costValue;
   math.scope(() => {
     const cost = this.session.train(
       this.costTensor,
       this.feedEntries,
       this.batchSize,
       this.optimizer,
       computeCost ? CostReduction.MEAN : CostReduction.NONE,
     );
     if (computeCost) {
       costValue = cost.get();
     }
   });
   return costValue;
 }
}
export default ColorAccessibilityModel;

最后,这就是训练阶段。现在仅需要在训练集上进行会话设置后从外部进行迭代执行。外部的执行取决于训练方法是否返回损失。

推断阶段

最后一个阶段是推断阶段,该阶段使用测试集来验证训练算法的性能。输入是背景颜色中的 RGB 颜色,输出是算法为字体颜色是黑是白进行的 [ 0, 1 ] 或 [ 1, 0 ] 分类预测。由于输入数据点经过归一化,因此不要忘记在这一步也对颜色进行归一化。

class ColorAccessibilityModel {
 ...
 predict(rgb) {
   let classifier = [];
   math.scope(() => {
     const mapping = [{
       tensor: this.inputTensor,
       data: Array1D.new(this.normalizeColor(rgb)),
     }];
     classifier = this.session.eval(this.predictionTensor, mapping).getValues();
   });
   return [ ...classifier ];
 }
}
export default ColorAccessibilityModel;

该方法在数学环境中再次运行性能关键部分,需要定义一个映射,该映射最终可作为会话评估的输入。记住,预测方法不是一定得在训练阶段后运行。它可以在训练阶段中使用,来输出测试集的验证。 至此,神经网络已经经历了设置、训练和推断阶段。

在 JavaScript 中可视化学习神经网络

现在是时候使用神经网络进行训练和验证/测试了。简单的过程为建立一个神经网络,使用一个训练集运行训练阶段,代价函数取得最小值之后,使用一个测试集进行预测。所有的过程只需要使用网页浏览器上的开发者控制台的几个 console.log statements 就可以完成。然而,由于该神经网络是关于颜色预测的,并且 deeplearn.js 是在浏览器上运行,从而可以轻松地对神经网络的训练阶段和测试阶段进行可视化。

至此,你可以自主决定你运行中的神经网络的可视化方式。使用一个 canvas 和 repuestAnimationFrame API 可以使 JavaScript 代码更简单。但就这篇文章来说,我会使用 React.js 进行展示,因为我在博客上写过 React.js。

因此在使用 create-react-app 设置完项目后,App 组件可成为我们可视化的进入点。首先,导入神经网络类别和函数,从你的文件中生成数据集。进而,为训练集大小、测试集大小和训练迭代次数添加若干个常量。

import React, { Component } from 'react';
import './App.css';
import generateColorSet from './data';
import ColorAccessibilityModel from './neuralNetwork';
const ITERATIONS = 750;
const TRAINING_SET_SIZE = 1500;
const TEST_SET_SIZE = 10;
class App extends Component {
 ...
}
export default App;

App 的组件包括生成数据集(训练集和测试集)、通过传递训练集建立神经网络会话、定义组件的初始状态。在训练阶段的时间内,代价函数的值和迭代次数会在控制台上显示,它也表示了组件的状态。

import React, { Component } from 'react';
import './App.css';
import generateColorSet from './data';
import ColorAccessibilityModel from './neuralNetwork';
const ITERATIONS = 750;
const TRAINING_SET_SIZE = 1500;
const TEST_SET_SIZE = 10;
class App extends Component {
 testSet;
 trainingSet;
 colorAccessibilityModel;
 constructor() {
   super();
   this.testSet = generateColorSet(TEST_SET_SIZE);
   this.trainingSet = generateColorSet(TRAINING_SET_SIZE);
   this.colorAccessibilityModel = new ColorAccessibilityModel();
   this.colorAccessibilityModel.setupSession(this.trainingSet);
   this.state = {
     currentIteration: 0,
     cost: -42,
   };
 }
 ...
}
export default App;

接下来,设置了神经网络会话之后,就可以迭代地训练神经网络了。最简单的版本只需要一直运行 React 的一个 for 循环就可以了。

class App extends Component {
 ...
 componentDidMount () {
   for (let i = 0; i <= ITERATIONS; i++) {
     this.colorAccessibilityModel.train(i);
   }
 };
}
export default App;

然而,以上代码不会在 React 的训练阶段提供(render)输出,因为组件不会在神经网络阻塞单个 JavaScript 线程的时候 reRender。这也正是 React 使用 requestAnimationFrame 的时候。与其自己定义一个 for 循环,每一个请求的浏览器的动画帧都可以被用于运行一次训练迭代。

class App extends Component {
 ...
 componentDidMount () {
   requestAnimationFrame(this.tick);
 };
 tick = () => {
   this.setState((state) => ({
     currentIteration: state.currentIteration + 1
   }));
   if (this.state.currentIteration < ITERATIONS) {
     requestAnimationFrame(this.tick);
     this.colorAccessibilityModel.train(this.state.currentIteration);
   }
 };
}
export default App;

此外,代价函数可以每 5 步进行一次计算。如前所述,需要访问 GPU 来检索代价函数。因此需要防止神经网络训练过快。

class App extends Component {
 ...
 componentDidMount () {
   requestAnimationFrame(this.tick);
 };
 tick = () => {
   this.setState((state) => ({
     currentIteration: state.currentIteration + 1
   }));
   if (this.state.currentIteration < ITERATIONS) {
     requestAnimationFrame(this.tick);
     let computeCost = !(this.state.currentIteration % 5);
     let cost = this.colorAccessibilityModel.train(
       this.state.currentIteration,
       computeCost
     );
     if (cost > 0) {
       this.setState(() => ({ cost }));
     }
   }
 };
}
export default App;

一旦组件装载好训练阶段就可以开始运行。现在是使用程序化计算输出和预测输出提供测试集的时候了。经过时间推移,预测输出应该变得和程序化计算输出一样。而训练集本身并未被可视化。

class App extends Component {
 ...
 render() {
   const { currentIteration, cost } = this.state;
   return (
     <div className="app">
       <div>
         <h1>Neural Network for Font Color Accessibility</h1>
         <p>Iterations: {currentIteration}</p>
         <p>Cost: {cost}</p>
       </div>
       <div className="content">
         <div className="content-item">
           <ActualTable
             testSet={this.testSet}
           />
         </div>
         <div className="content-item">
           <InferenceTable
             model={this.colorAccessibilityModel}
             testSet={this.testSet}
           />
         </div>
       </div>
     </div>
   );
 }
}
const ActualTable = ({ testSet }) =>
 <div>
   <p>Programmatically Computed</p>
 </div>
const InferenceTable = ({ testSet, model }) =>
 <div>
   <p>Neural Network Computed</p>
 </div>
export default App;

实际的表格会随着测试集的不断输入不断地展示每一个输入和输出的颜色。测试集包括输入颜色(背景颜色)和输出颜色(字体颜色)。由于生成数据集的时候输出颜色被分类为黑色 [0,1] 和白色 [1,0] 向量,它们需要再次被转换为真实的颜色。

const ActualTable = ({ testSet }) =>
 <div>
   <p>Programmatically Computed</p>
   {Array(TEST_SET_SIZE).fill(0).map((v, i) =>
     <ColorBox
       key={i}
       rgbInput={testSet.rawInputs[i]}
       rgbTarget={fromClassifierToRgb(testSet.rawTargets[i])}
     />
   )}
 </div>
const fromClassifierToRgb = (classifier) =>
 classifier[0] > classifier[1]
   ? [ 255, 255, 255 ]
   : [ 0, 0, 0 ]

ColorBox 组件是一个通用组件,以输入颜色(背景颜色)和目标颜色(字体颜色)为输入。它能简单地用一个矩形展示输入颜色的类型、输入颜色的 RGB 代码字符串,并用字体的 RGB 代码将给定的目标颜色上色。

const ColorBox = ({ rgbInput, rgbTarget }) =>
 <div className="color-box" style={{ backgroundColor: getRgbStyle(rgbInput) }}>
   <span style={{ color: getRgbStyle(rgbTarget) }}>
     <RgbString rgb={rgbInput} />
   </span>
 </div>
const RgbString = ({ rgb }) =>
 `rgb(${rgb.toString()})`
const getRgbStyle = (rgb) =>
 `rgb(${rgb[0]}, ${rgb[1]}, ${rgb[2]})`

最后但重要的是,在推理表格中可视化预测颜色的激动人心的部分。它使用的也是 color box,但提供了一些不同的小道具。

const InferenceTable = ({ testSet, model }) =>
 <div>
   <p>Neural Network Computed</p>
   {Array(TEST_SET_SIZE).fill(0).map((v, i) =>
     <ColorBox
       key={i}
       rgbInput={testSet.rawInputs[i]}
       rgbTarget={fromClassifierToRgb(model.predict(testSet.rawInputs[i]))}
     />
   )}
 </div>

输入颜色仍然是测试集中定义的颜色,但目标颜色并不是测试集中的目标色。任务的关键是利用神经网络的预测方法预测目标颜色——它需要输入的颜色,并应在训练阶段预测目标颜色。

最后,当你开启应用时,你需要观察神经网络是否被启用。而实际的表格从开始就在使用固定测试集,在训练阶段推理表格应该改变它的字体颜色。事实上,当 ActualTable 组件显示实际测试集时,InferenceTable 显示测试集的输入数据点,但输出是使用神经网络预测的。

本文向你展示了如何使用 deeplearn.js 在 JavaScript 上为机器学习构建神经网络,希望对大家有所帮助。如果你有任何改进的建议,欢迎留言并在 GitHub 上做出自己的贡献。React 渲染部分的可视化动图可以在 GitHub 上看到: https://github.com/javascript-machine-learning/color-accessibility-neural-network-deeplearnjs


用户评论
开源开发学习小组列表