本教程将带领读者使用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上存放数据的数据结构,inputArray
和targetArray
是Array1D
的列表。我们在将每个输入存入到一个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
,它会打乱输入数据的顺序(数据包括inputArray
和targetArray
两个列表)。 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)中间层节点的模型应该就足够了。
权重初始化问题
现在,预测的互补色某一通道在整个训练期间可能始终为0。例如,在下面的截图中,蓝色通道一直为0。
这种现象的出现取决于初始化权重对训练是否开始产生多大的影响。
有时,通道值卡在0的情况可能会随着时间的推移而变化。而有时,这个问题可能需要刷新页面才能解决。
End
结语
目录中的代码和注释提供了一个简单的例子,希望对你了解如何使用deeplearn.js有所帮助。