deeplearn.js : サンプル : 補色を予想する
作成 : (株)クラスキャット セールスインフォメーション
日時 : 08/25/2017
* 本ページは、github.io の deeplearn.js サイトの Example: Predicting Complementary Colors を翻訳した上で
適宜、補足説明したものです:
* サンプルコードの動作確認はしておりますが、適宜、追加改変している場合もあります。
* ご自由にリンクを張って頂いてかまいませんが、sales-info@classcat.com までご一報いただけると嬉しいです。
このチュートリアルは補色を予想するモデルのコーディングを読者に体験してもらいます。このモデルのハイパーパラメータは完全には最適化されないかもしれませんが、モデルの構築は deeplearn.js の重要な概念を通り抜けるでしょう。実際に、より多くの層の追加は補色のより近い予想を生成するようです。私たちはハイパーパラメータの最適化に本質的な時間を使いませんでした – その目的に向かっては pull リクエストが望ましいでしょう。
訳注: 補色 (complementary color) とは、色相環で正反対に位置する関係の色の組合せ。相補的な色のことでもあります。 ( Wikipedia )
読者は既に Introduction とおそらくは Guide for non-ML Experts を読み終えているでしょう。このチュートリアルは TypeScript を使用します、JavaScript の知識で十分ですが。
このチュートリアルのためのすべてのコードは demos/complementary-color-predictions ディレクトリにあります。
Stack Overflow 上の Edd の回答 が示すように、補色を計算することはほんの少しのロジックを使います。小さな 順伝播ニューラルネットワーク がそのロジックをどのように上手く学習できるのかを見てみましょう。
(訳注: 以下の画像はデモの訓練開始直後のスナップショットです。)

(そして以下は 4,000 ステップほど訓練した後のスナップショットです。補色の予想が実際の補色に漸近しています。)

入力サンプルを作成する
最初に訓練データを生成します: 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);
}
各入力 (現在 0 と 1 の間の3つの値のリスト) を Array1D 内にストアします、deeplearn.js は GPU 上にデータを置く場所を構築します。inputArray と targetArray は両者とも Array1D のリストです。
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 をビルドします、これは入力データ (これは 2 リストから成ります) をシャッフルします。入力データをシャッフリングする間、ShuffledInputProvider は入力とターゲットの間の関係を維持します (従って両者の配列の要素は同じインデックスにシャッフルされます)。
const shuffledInputProviderBuilder =
new InCPUMemoryShuffledInputProviderBuilder(
[inputArray, targetArray]);
const [inputProvider, targetProvider] =
shuffledInputProviderBuilder.getInputProviders();
provider を使用して、モデルにデータを渡す feed entries を作成します。
this.feedEntries = [
{tensor: this.inputTensor, data: inputProvider},
{tensor: this.targetTensor, data: targetProvider}
];
Graph のセットアップ
このパートはエキサイティングです、何故ならモデルを構築するからです。TensorFlow のように、deeplearn.js は graph-ベースの API です: モデルを実行するために session を使用する前に最初にモデルをデザインします。
Graph オブジェクトと 2 tensor を作成します: 一つは入力カラーのためで一つはターゲット・カラーのためです。ターゲット・カラーは訓練 (そして推論でない時) の間だけ生息して (= populated)、推論の間は、入力カラーが与えられるのみでターゲットを予想します。
上記により、tensor はモデルにデータを渡すために feed entries 内で使用されます。
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]);
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);
}
その関数を使用して、64, 32, そして 16 ノードを持つ3つの完全結合層を作成します。
// 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);
正規化された予想された補色を出力する層を作成します。それは3つの出力を持ちます、各チャネルのために1つです。
this.predictionTensor =
this.createFullyConnectedLayer(graph, fullyConnectedLayer, 3, 3);
損失関数 (mean squared) を指定する cost tensor も追加します。
this.costTensor =
graph.meanSquaredCost(this.predictionTensor, this.targetTensor);
最後に、訓練を実行して推論するための session を作成します。
this.session = new Session(graph, this.math);
訓練して予想する
モデルを訓練するために、optimizer を (0.042 の初期学習率で) 構築します、
this.optimizer = new SGDOptimizer(this.initialLearningRate);
そしてカラーのバッチ上で訓練する関数を書きます。訓練の session の呼出しを math.scope コールバック内にラップすることに注意してください。math.scope の使用はここでは (そしてコードの他のパートで) 必須です、何故ならばそれらが必要でなくなればそれは deeplearn.js に (GPU 上のデータのような) リソースを刈り取る (= reap) ことを可能にするからです。
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;
}
加えて、任意の与えられたカラー上で推論を実行するためのメソッドを書きます。入力カラーをモデルに渡す mapping と呼ばれる FeedEntry を作成します。
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 メソッドの呼出しは訓練を実行してブラウザの viewport のリフレッシュレートと同期した方法でレンダリングします (requestAnimationFrame によります)。4242 ステップ後に訓練を停止します。コンソールに損失をログ出力もします。
幾つかのサンプルカラーに基づけば、64 + 32 + 16 = 112 中間層ノードの私たちのモデルはまずまず良いように見えます。

重みの初期化は重要です
時々、予想される補色のチャネルは訓練を通して 0 にとどまるかもしれません。例えば、下のスクリーンショットでは、青のチャネルが 0 に固着されています。

この挙動は訓練が発生するかに向けた重みの初期化がどのくらい重要であるかに不幸なことに由来します。時々、0 に固着されたチャネルは時間とともに解決するかもしれません。他の時は、ページのリフレッシュが必要かもしれません。
最後に
多分、コードとその内のコメントの精読が learnjs がどのように動作するかの単純なサンプルを提供するでしょう。貴方が追跡する興味深いプロジェクト上で post し続けてください。
以上