我爱编程

使用deeplearn.js预测互补色(译)

2017-09-16  本文已影响144人  行走的程序猿

本教程将带领读者使用deeplearn.js编写一个预测颜色互补色的模型。

该模型的超参数可能无法完美优化,但通过构建此模型将使我们了解deeplearn.js的重要概念。

事实上,添加更多的层数可以更好地预测互补色。 但这只是为演示而提出一个PR,所以没有花费大量时间优化超参数。

相信读者们应该已经阅读了项目简介给非机器学习专业人员的指南。 尽管原生JavaScript已经足够,本教程选择TypeScript作为编程语言。

本教程所有代码都位于项目demos / complement-color-predictions目录中。

根据Edd在Stack Overflow的回答,我们可以得知,计算颜色的互补色需要很多逻辑。

让我们看看一个小型前馈神经网络能把这个逻辑掌握到什么程度。

创建输入

首先,我们用RGB空间中的随机颜色生成训练数据

const rawInputs = new Array(exampleCount);
for (let i = 0; i < exampleCount; i++) {
  rawInputs[i] = [
    this.generateRandomChannelValue(), this.generateRandomChannelValue(),
    this.generateRandomChannelValue()
  ];
}

接着,使用Edd的方法计算其互补色。

/**
 * This implementation of computing the complementary color came from an
 * answer by Edd https://stackoverflow.com/a/37657940
 */
computeComplementaryColor(rgbColor: number[]): number[] {
  let r = rgbColor[0];
  let g = rgbColor[1];
  let b = rgbColor[2];

  // Convert RGB to HSL
  // Adapted from answer by 0x000f http://stackoverflow.com/a/34946092/4939630
  r /= 255.0;
  g /= 255.0;
  b /= 255.0;
  const max = Math.max(r, g, b);
  const min = Math.min(r, g, b);
  let h = (max + min) / 2.0;
  let s = h;
  const l = h;

  if (max === min) {
    h = s = 0;  // achromatic
  } else {
    const d = max - min;
    s = (l > 0.5 ? d / (2.0 - max - min) : d / (max + min));

    if (max === r && g >= b) {
      h = 1.0472 * (g - b) / d;
    } else if (max === r && g < b) {
      h = 1.0472 * (g - b) / d + 6.2832;
    } else if (max === g) {
      h = 1.0472 * (b - r) / d + 2.0944;
    } else if (max === b) {
      h = 1.0472 * (r - g) / d + 4.1888;
    }
  }

  h = h / 6.2832 * 360.0 + 0;

  // Shift hue to opposite side of wheel and convert to [0-1] value
  h += 180;
  if (h > 360) {
    h -= 360;
  }
  h /= 360;

  // Convert h s and l values into r g and b values
  // Adapted from answer by Mohsen http://stackoverflow.com/a/9493060/4939630
  if (s === 0) {
    r = g = b = l;  // achromatic
  } else {
    const hue2rgb = (p: number, q: number, t: number) => {
      if (t < 0) t += 1;
      if (t > 1) t -= 1;
      if (t < 1 / 6) return p + (q - p) * 6 * t;
      if (t < 1 / 2) return q;
      if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
      return p;
    };

    const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
    const p = 2 * l - q;

    r = hue2rgb(p, q, h + 1 / 3);
    g = hue2rgb(p, q, h);
    b = hue2rgb(p, q, h - 1 / 3);
  }

  return [r, g, b].map(v => Math.round(v * 255));
}

将每个颜色通道除以255来归一化输入。通常,归一化能有助于模型的训练过程。

normalizeColor(rgbColor: number[]): number[] {
  return rgbColor.map(v => v / 255);
}

Array1D是deeplearn.js在GPU上存放数据的数据结构,inputArraytargetArrayArray1D的列表。我们在将每个输入存入到一个Array1D结构中(现在是0到1之间的3个值的列表)。

const inputArray: Array1D[] =
    rawInputs.map(c => Array1D.new(this.normalizeColor(c)));
const targetArray: Array1D[] = rawInputs.map(
    c => Array1D.new(
        this.normalizeColor(this.computeComplementaryColor(c))));

之后,我们构建一个ShuffledInputProvider,它会打乱输入数据的顺序(数据包括inputArraytargetArray两个列表)。 ShuffledInputProvider在打乱输入数据时,保持了输入和目标之间的关系(因此两个数组中的对应元素在打乱顺序后仍保持相同的索引)。

const shuffledInputProviderBuilder =
    new InCPUMemoryShuffledInputProviderBuilder(
        [inputArray, targetArray]);
const [inputProvider, targetProvider] =
    shuffledInputProviderBuilder.getInputProviders();

利用ShuffledInputProvider创建前馈入口,将数据传递到模型。

this.feedEntries = [
  {tensor: this.inputTensor, data: inputProvider},
  {tensor: this.targetTensor, data: targetProvider}
];

Setting Up the Graph设置图表

最有意思的部分来了,在接下来的步骤中,我们将构建模型。deeplearn.js是一个基于图形的API,像TensorFlow一样,运行会话前,先设计一个模型。

创建一个Graph对象和两个张量(Tensor):一个给输入颜色,另一个给目标颜色。仅在训练阶段填充目标颜色(测试阶段不填充) - 在测试过程中,只能利用所给输入颜色来预测目标颜色。

如上所述,张量(Tensor)用于在前馈入口中将数据传递给模型。

const graph = new Graph();

// This tensor contains the input. In this case, it is a scalar.
this.inputTensor = graph.placeholder('input RGB value', [3]);

// This tensor contains the target.
this.targetTensor = graph.placeholder('output RGB value', [3]);

编写函数createFullyConnectedLayer,用graph.layers.dense生成一个全连接层。

private createFullyConnectedLayer(
    graph: Graph, inputLayer: Tensor, layerIndex: number,
    sizeOfThisLayer: number, includeRelu = true, includeBias = true) {
  return graph.layers.dense(
      'fully_connected_' + layerIndex, inputLayer, sizeOfThisLayer,
      includeRelu ? (x) => graph.relu(x) : undefined, includeBias);
}

createFullyConnectedLayer创建三个全连接层,各有64, 32和16个节点。

// Create 3 fully connected layers, each with half the number of nodes of
// the previous layer. The first one has 16 nodes.
let fullyConnectedLayer =
    this.createFullyConnectedLayer(graph, this.inputTensor, 0, 64);

// Create fully connected layer 1, which has 8 nodes.
fullyConnectedLayer =
    this.createFullyConnectedLayer(graph, fullyConnectedLayer, 1, 32);

// Create fully connected layer 2, which has 4 nodes.
fullyConnectedLayer =
    this.createFullyConnectedLayer(graph, fullyConnectedLayer, 2, 16);

创建一个有三个输出的网络层,每个输出对应一个颜色通道,用于输出归一化后的预测互补色。

this.predictionTensor =
    this.createFullyConnectedLayer(graph, fullyConnectedLayer, 3, 3);

添加成本张量(Cost Tensor)指定损失函数(Loss Function)(均方)。

this.costTensor =
    graph.meanSquaredCost(this.targetTensor, this.predictionTensor);

最后,创建一个运行训练和测试的会话。

this.session = new Session(graph, this.math);

Train and Predict

训练和预测

构建一个初始学习率为0.042优化器训练模型,

this.optimizer = new SGDOptimizer(this.initialLearningRate);

然后遍写一个能训练一批颜色的函数。需要注意的是,我们将在math.scope回调函数中打包训练的会话调用。

在这里使用math.scope是必需的(代码的其他部分也是如此),它允许deeplearn.js回收不再需要的资源(如GPU上的数据)。

还要注意,train1Batch方法接受一个shouldFetchCost参数,让调用train1Batch的外部循环,只能在某些步骤获取成本值。

从GPU获取成本值要从GPU传输数据,会造成延迟,所以我们偶尔才这样做。

train1Batch(shouldFetchCost: boolean): number {
  // Every 42 steps, lower the learning rate by 15%.
  const learningRate =
      this.initialLearningRate * Math.pow(0.85, Math.floor(step / 42));
  this.optimizer.setLearningRate(learningRate);

  // Train 1 batch.
  let costValue = -1;
  this.math.scope(() => {
    const cost = this.session.train(
        this.costTensor, this.feedEntries, this.batchSize, this.optimizer,
        shouldFetchCost ? CostReduction.MEAN : CostReduction.NONE);

    if (!shouldFetchCost) {
      // We only train. We do not compute the cost.
      return;
    }

    // Compute the cost (by calling get), which requires transferring data
    // from the GPU.
    costValue = cost.get();
  });
  return costValue;
}

此外,编写预测任何给定颜色互补色的方法,并创建一个称为映射的前馈入口,将输入颜色传递给模型。

predict(rgbColor: number[]): number[] {
  let complementColor: number[] = [];
  this.math.scope((keep, track) => {
    const mapping = [{
      tensor: this.inputTensor,
      data: Array1D.new(this.normalizeColor(rgbColor)),
    }];
    const evalOutput = this.session.eval(this.predictionTensor, mapping);
    const values = evalOutput.getValues();
    const colors = this.denormalizeColor(Array.prototype.slice.call(values));

    // Make sure the values are within range.
    complementColor = colors.map(
        v => Math.round(Math.max(Math.min(v, 255), 0)));
  });
  return complementColor;
}

更新UI

.ts文件中的其余逻辑主要是用于管理UI。

调用trainAndMaybeRender方法启动循环进行训练和渲染,循环的执行时与浏览器视图刷新率保持同步。

4242步后停止训练,并在控制台中记录损失值。

根据一小部分示例颜色,112个(64 + 32 + 16)中间层节点的模型应该就足够了。

complementary-color-prediction.png

权重初始化问题

现在,预测的互补色某一通道在整个训练期间可能始终为0。例如,在下面的截图中,蓝色通道一直为0。

blue-channel-at-0.png

这种现象的出现取决于初始化权重对训练是否开始产生多大的影响。

有时,通道值卡在0的情况可能会随着时间的推移而变化。而有时,这个问题可能需要刷新页面才能解决。

End
结语

目录中的代码和注释提供了一个简单的例子,希望对你了解如何使用deeplearn.js有所帮助。

上一篇下一篇

猜你喜欢

热点阅读