写点什么

deeplearn.js:在浏览器上训练神经网络

2018 年 1 月 18 日

来自德国的软件工程师 Robin Wieruch 将带我们一起体验如何使用 deaplearn.js 在浏览器上训练神经网络。原文链接: Neural Networks in JavaScript with deeplearn.js

最近我写了一些文章,介绍如何使用 JavaScript 实现基础的机器学习算法。我基于 Node 的 math.js 从头开始实现了这些算法,但我仍然觉得使用 JavaScript 实现机器学习算法是一件非常复杂的事情。目前我正在自学神经网络,想找一些现成的库可以帮我完成一些任务。谷歌最近发布了 deeplearn.js ,可以用来训练神经网络,于是我就试用了一把。在这篇文章里,我将分享如何使用 deeplearn.js 训练神经网络,并用它解决真实世界的 Web 访问性问题。

神经网络有什么用?

这篇文章里实现的神经网络可用于改进 Web 访问性,它会根据背景色选择恰当的字体颜色。比如,深蓝色背景的字体应该是白色的,而浅黄色背景的字体应该是黑色的。你或许会想:为什么要在这里使用神经网络?通过编程的方式来给字体设置恰当的颜色并不难啊,不是吗?我很快从 Stack Overflow 上找到了一个解决方案,并根据我的需求进行了修改。

复制代码
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 上有一个动画演示了最终效果,从动画上你可以很直观地看到这篇文章将会教你做一个什么样的东西。

如果你熟悉机器学习,可能已经注意到,这其实是一个分类问题。它需要一个算法,根据输入数据(背景色)输出一个二元结果(字体颜色:白色或黑色)。在使用神经网络对算法进行训练之后,最终会根据输入的背景色输出恰当的字体颜色。

下面将告诉你如何从头开始搭建这个神经网络。

使用 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 颜色值。矩阵中的每一行表示一种颜色,每一列表示颜色的一个特征,特征就是 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 };
}

我们还可以进行特征缩放(Feature Scaling),我们把 RGB 值固定在 0 到 1 之间。因为我们都知道它的最大值是多少,所以可以直接规范化每个 RGB 值。

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

在 JavaScript 中搭建神经网络模型

现在进入最激动人心的部分,我们将要使用 JavaScript 来实现一个神经网络。在开始之前,需要先安装 deeplearn.js 库。deeplearn.js 是一个 JavaScript 神经网络框架,据官方的说法,“它是一个开源的框架,将机器学习带到 Web 上,可以在浏览器上训练神经网络或者在推理模式下运行预训练的模型”。这个框架为我们带来了两大好处:

首先,它利用了本地机器的 GPU 来加速机器学习算法的矢量运算。这些运算与图形计算类似,所以使用 GPU 来计算效率更高。

其次,deeplearn.js 与 TensorFlow 类似,它们都是由谷歌开发,不过后者使用了 Python。所以,就算以后你要转向 Python 机器学习,deeplearn.js 也会是一个很好的起点。

现在让我们回到项目上。如果你使用了 npm,那么可以直接通过命令行来安装 deeplearn.js,否则的话请参考 deeplearn.js 的官方文档,按照文档指示来安装它。

npm install deeplearn因为我也没有太多创建神经网络的经验,所以我就按照常规的做法,使用面向对象的方式。在 JavaScript 里,我们可以使用 ES6 里的类。我们通过类的属性和方法来定义神经网络,比如规范化颜色值的函数其实就是类的一个方法:

复制代码
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;

然后声明用于创建会话的方法,这个方法接收训练集作为参数。接下来,会话会初始化一个空的图,这个图会在后面反映出你的神经网络结构。至于要不要定义所有的属性,完全取决于你自己。

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

第四步,以张量的形式定义输入数据点和输出数据点的形状。张量是一组数值(或数值数组),这些数值可以有多个维度。张量可以是一个 vector、一个矩阵或一个多维矩阵。神经网络将这些张量作为输入和输出。我们的这个例子有三个输入单元(每种颜色通道就是一个输入单元)和两个输出单元(二元分类:白颜色或黑颜色)。

复制代码
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({1}
  graph,
  inputLayer,
  layerIndex,
  units,
 ) {
  ...
 }
 ...
}
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({1}
  graph,
  inputLayer,
  layerIndex,
  units,
 ) {
  return graph.layers.dense(
   `fully_connected_${layerIndex}`,
   inputLayer,
   units
  );
 }
 ...
}
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({1}
  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;

第七步,声明一个成本张量。在这里,它就是均方误差。它使用训练集的目标张量(标签)和来自已训练算法的预测张量进行成本计算,以此来优化算法。

复制代码
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 的上下文里执行。当然,这不是必需的,不用回调函数也能完成计算。

复制代码
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;

第三,输入和输出数组被搅乱。deeplearn.js 提供的搅乱器(Shuffler)在搅乱数组时会保持它们之间的同步。每次训练迭代都会进行搅乱操作,输入被分批填充进神经网络。我们通过搅乱来改进训练算法,因为这更像是通过避免过拟合来实现泛化。

复制代码
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;

最后,填充项就成为神经网络训练阶段的最终输入。

复制代码
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)都包含一个前向和一个后向的数据点批次。以批次的形式来训练神经网络有两个好处。首先,它不会造成密集的计算,因为训练算法使用更少的数据点。其次,权重是根据批次来调整的,而不是根据整个训练集来调整。

训练阶段

搭建阶段已经完成了,接下来是训练阶段。首先,可以在类的一个方法里对训练阶段进行定义。训练将会在 deeplearn.js 的 math 上下文中运行。另外,我们将会使用神经网络实例所有的预定义属性来训练算法。

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

训练方法只能运行在某个时间点上,所以需要在外部进行多次迭代调用才能进行神经网络训练,而且它进行的是批次训练。为了进行多个批次的算法训练,需要多次迭代运行这个训练方法。

基本的训练就是这样的,不过我们可以通过调整学习速率来改进训练。学习速率在刚开始时可以设置得高一些,不过随着算法不断收敛,可以逐步降低学习速率。

复制代码
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 可视化神经网络

因为这个例子是关于颜色预测,而且使用了 deeplearn.js,所以如果能够对训练阶段和推理阶段进行可视化会很有意思。

可视化的方式有很多,可以使用 canvas 和 requestAnimationFrame API 来实现,不过在这里,我选择使用 React.js。

先创建一个 create-react-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 Component 的构造函数里生成数据集(训练集和测试集),启动神经网络会话,传入训练集,并定义组件的初始状态。随着训练的进行,成本值和迭代次数会在某个地方展示出来。

复制代码
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 的挂载组件生命周期钩子里循环调用训练方法。

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

不过,在训练过程中无法对输出进行渲染,因为运行中的训练会阻塞 JavaScript 线程,导致 React 组件无法重新渲染。这个时候可以使用 requestAnimationFrame。我们不需要定义循环语句,因为每个动画帧请求都可以用来运行训练方法。

复制代码
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 步计算一次成本。

复制代码
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 值,字体颜色与目标颜色一样。

复制代码
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]})`

最好玩的是推理表格里的预测颜色值。它也使用了上述的 ColorBox,只是显示的属性不一样。

复制代码
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>

输入颜色仍然是在测试集中定义的颜色,但目标颜色不是。目标颜色是使用神经网络预测出来的,它接收输入颜色,并经过训练预测出目标颜色。

最后,启动应用程序,就可以看到神经网络是怎么运行的。React 部分的动画也可以在 GitHub 仓库上看到。

2018 年 1 月 18 日 17:052572
用户头像

发布了 321 篇内容, 共 108.1 次阅读, 收获喜欢 101 次。

关注

评论

发布
暂无评论
发现更多内容

对话 CTO | 听快看漫画 CTO 李润超讲重塑漫画产业的技术推动力

ONES 王颖奇

研发管理 CTO 动画 文化

前端有未来吗?

欧雷

前端 前端开发

C语言运算符

C语言技术网-码农有道

C语言 运算符

如何让团队产生“多米诺骨牌”效应?

Yanel 说敏捷产品

项目管理 敏捷 敏捷开发 敏捷精髓

Try-Catch包裹的代码异常后,竟然导致了产线事务回滚!

码大叔

Java spring 事务

追光逐影:读《我们这一代》

北风

Linux学习-2020.05.11

Flychen

接口限流算法有哪些,看完这篇又能和面试官互扯了~

不才陈某

Java 分布式 后端

Python程序性能分析和火焰图

ElvinYang

C语言输入和输出

C语言技术网-码农有道

C语言 输入 输出

当前的经济形势,如何让自己免于风险?

鼎玉谷

给应届毕业生们的七点建议

Neco.W

大学生日常 工作 应届毕业

Using R for everything: 方差分解(Variation partition)变量筛选与显著性标注

洗衣机用户不会用洗衣机

数据分析 R

ShedLock:一个轻量级的定时任务协调组件

kk

定时任务 shedlock

JavaScript 学习笔记——数据类型

zjlulsum

Java 学习 前端 类型推断 入门

你真的懂"看板文化"么?

Yanel 说敏捷产品

敏捷 敏捷开发 敏捷精髓

从技术层面理解对于区块链技术的10.24集体学习讲话

MaxHu

区块链 智能合约 以太坊 加密货币 去中心化网络

每个人都应该知道的性能参数

ElvinYang

对话 CTO | 喜茶也有 CTO?听陈霈霖讲讲茶饮中的技术甜度

ONES 王颖奇

研发管理 CTO 零售

工具集系列 02|还在为海报设计、LOGO 设计发愁?这些在线工具值得收藏

一尘观世界

效率工具 设计 海报 课程封面 知识付费

认识数据产品经理(二 数据产品经理的稀缺性)

马踏飞机747

大数据 互联网 数据分析 产品经理

DDD 实践手册(6. Bounded Context - 限界上下文)

Joshua

企业架构 设计模式 领域驱动设计 DDD 架构模式

工具集系列|值得收藏的几个免费在线学习国外网站

一尘观世界

学习 工具 网站 提升

C语言常量、变量和关键字

C语言技术网-码农有道

C语言 常量 变量 关键字

危机过后,「表格文档协同」需要具备什么能力?

Geek_Willie

前端开发 开发者工具 Excel

Python网络编程socket 简易聊天窗

Flychen

“随大流”的你是不会成功的

小天同学

个人成长 思考 写作平台 感悟 坚持

目光聚集之处,金钱必将追随

Tom

学习 个人成长 思考 读书

如何高效阅读

ElvinYang

【解析+示例】2种方法,通过SpreadJS在前端实现甘特图

Geek_Willie

前端开发 甘特图 SpreadJS 表格控件

NIO 看破也说破(三)—— 不同的IO模型

小眼睛聊技术

Java 学习 深度思考 程序员 架构

InfoQ 极客传媒开发者生态共创计划线上发布会

InfoQ 极客传媒开发者生态共创计划线上发布会

deeplearn.js:在浏览器上训练神经网络-InfoQ