deeplearn.js : チュートリアル : 序説
作成 : (株)クラスキャット セールスインフォメーション
日時 : 08/21/2017
* 本ページは、github.io の deeplearn.js サイトの Tutorials – Introduction を翻訳した上で
適宜、補足説明したものです:
* サンプルコードの動作確認はしておりますが、適宜、追加改変しています。
* ご自由にリンクを張って頂いてかまいませんが、sales-info@classcat.com までご一報いただけると嬉しいです。
deeplearn.js はオープンソースの機械知能のための WebGL-accelerated JavaScript ライブラリです。deeplearn.js は高性能な機械学習ビルディング・ブロックを完全にもたらし、ブラウザでニューラルネットワークを訓練したり推論モードで事前訓練されたモデルを実行することを可能にします。それは、直接利用可能な数学関数のセットに加えて微分可能なデータフロー・グラフを構築するための API を提供します。
このチュートリアルを補足するコードは ここで 見つけられます。
それを貴方自身で次のように実行しましょう :
./scripts/watch-demo demos/intro/intro.ts
そして http://localhost:8080/demos/intro/ を訪れましょう。
(訳注: 以下の画像は intro.ts 実行後のブラウザのスナップショットです : )

このドキュメントのためには、TypeScript コード・サンプルを使用します。vanilla JavaScript のためには、const, let, あるいは他の型定義を取り除く必要があるでしょう。
核となる概念
NDArray
deeplearn.js のデータの中心的なユニットは NDArray です。NDArray は任意の次元数の配列に shape された浮動小数点のセットで構成されます。NDArray はそれらの shape を定義する shape 属性を持ちます。ライブラリは低 rank NDArray のための sugar サブクラスを提供します : Scalar, Array1D, Array2D, Array3D and Array4D.
2×3 matrix のサンプル使用方法 :
const shape = [2, 3]; // 2 rows, 3 columns const a = Array2D.new(shape, [1.0, 2.0, 3.0, 10.0, 20.0, 30.0]);
NDArray はデータを GPU 上 WebGLTexture として、ここでは各ピクセルが浮動小数点値をストアし、あるいは CPU 上 vanilla JavaScript TypedArray としてストアします。ほとんどの場合、ユーザはストレージについて考えるべきではありません、それは実装の詳細ですので。
NDArray データが CPU 上にストアされている場合、GPU 数値演算が最初に呼び出された時にデータは texture に自動的にアップロードされます。GPU-resident NDArray 上で NDArray.getValues() を呼び出す場合は、ライブラリは texture を CPU にダウンロードして texture を削除します。
NDArrayMath
ライブラリは NDArrayMath 基底クラスを提供します、これは NDArray 上で動作する数値関数のセットを定義します。
NDArrayMathGPU
NDArrayMathGPU 実装を使用する時、これらの数値演算は shader プログラムを GPU 上で実行されるようにキューに入れます。NDArrayMathCPU とは異なり、これらの演算子はブロックしません、しかし詳細は後述するように、ユーザはNDArray 上で get() または getValues() を呼び出すことにより cpu を gpu と同期することができます。
これらの shader は NDArray により所有される WebGLTexture から読み書きをします。
数値演算を連鎖する時、(演算の間に CPU へダウンロードされずに) texture は GPU メモリに常駐できます 、これはパフォーマンスにとって致命的です。
2つの matrices の間の mean squared difference を取るサンプルです (math.scope, keep, と track の詳細は後述) :
const math = new NDArrayMathGPU();
math.scope((keep, track) => {
const a = track(Array2D.new([2, 2], [1.0, 2.0, 3.0, 4.0]));
const b = track(Array2D.new([2, 2], [0.0, 2.0, 4.0, 6.0]));
// Non-blocking math calls.
const diff = math.sub(a, b);
const squaredDiff = math.elementWiseMul(diff, diff);
const sum = math.sum(squaredDiff);
const size = Scalar.new(a.size);
const average = math.divide(sum, size);
// Blocking call to actually read the values from average. Waits until the
// GPU has finished executing the operations before returning values.
// average is a Scalar so we use .get()
console.log(average.get());
});
NOTE : NDArray.get() と NDArray.getValues() はブロッキング・コールです。連鎖した数値関数を実行する後に callback を登録する必要はありません、CPU & GPU を同期するためには getValues() を呼び出すだけです。
TIP : デバッグしているのでない限り数値 GPU 演算の間に get() や getValues() を呼び出すことは回避してください。これは texture を強制的にダウンロードさせ、そして続く NDArrayMathGPU 呼び出しはデータを新しい texture に再アップロードしなければならないでしょう。
math.scope()
math 演算子を使用する時、上のサンプルで示されるようにそれらを math.scope() 関数クロージャでラップしなければなりません。このスコープの math 演算子の結果はそれらがスコープ内で返される値でない限りはスコープの最後で処理されるでしょう。
2つの関数が関数クロージャに渡されます、keep() と track() です。
keep() は保持するために渡された NDArray がスコープが終わるときに自動的にクリーンアップされないことを保証します。
track() はスコープの内側で直接 construct するかもしれない任意の NDArray を追跡します。スコープが終わる時、任意の手動で track された NDArray はクリーンアップされます。
すべての math.method() 関数の結果、更には多くの他のコアライブラリ関数の結果は自動的にクリーンアップされます、従ってそれらを手動で追跡しなくてよいです。
const math = new NDArrayMathGPU();
let output;
// You must have an outer scope, but don't worry, the library will throw an
// error if you don't have one.
math.scope((keep, track) => {
// CORRECT: By default, math wont track NDArrays that are constructed
// directly. You can call track() on the NDArray for it to get tracked and
// cleaned up at the end of the scope.
const a = track(Scalar.new(2));
// INCORRECT: This is a texture leak!!
// math doesn't know about b, so it can't track it. When the scope ends, the
// GPU-resident NDArray will not get cleaned up, even though b goes out of
// scope. Make sure you call track() on NDArrays you create.
const b = Scalar.new(2);
// CORRECT: By default, math tracks all outputs of math functions.
const c = math.neg(math.exp(a));
// CORRECT: d is tracked by the parent scope.
const d = math.scope(() => {
// CORRECT: e will get cleaned up when this inner scope ends.
const e = track(Scalar.new(3));
// CORRECT: The result of this math function is tracked. Since it is the
// return value of this scope, it will not get cleaned up with this inner
// scope. However, the result will be tracked automatically in the parent
// scope.
return math.elementWiseMul(e, e);
});
// CORRECT, BUT BE CAREFUL: The output of math.tanh will be tracked
// automatically, however we can call keep() on it so that it will be kept
// when the scope ends. That means if you are not careful about calling
// output.dispose() some time later, you might introduce a texture memory
// leak. A better way to do this would be to return this value as a return
// value of a scope so that it gets tracked in a parent scope.
output = keep(math.tanh(d));
});
より技術的な詳細 : WebGL texture が JavaScript のスコープの外へ出るとき、ブラウザのガベージコレクション機構では自動的にクリーンアップされません。これは、GPU-resident な NDArray の扱いが終わった時いつか後で手動で処分されなければなならないことを意味します。NDArray の扱いが終わった時に ndarray.dispose() を手動で呼び出すのを忘れた場合、texture メモリリークをもたらし、これは深刻なパフォーマンス問題を引き起こすでしょう。もし math.scope() を使用する場合、math.method() とスコープを通して結果を返す任意の他のメソッドで作成された任意の NDArray は自動的にクリーンアップされます。
もし手動のメモリ管理を望み math.scope() を使用しないのであれば、NDArrayMath オブジェクトを safeMode = false で construct できます。これは推奨されませんが、NDArrayMathCPU に対しては有用です、何故ならば CPU-resident メモリは JavaScript ガベージコレクタで自動的にクリーンアップされるからです。
NDArrayMathCPU
CPU 実装を使用する時は、これらの数値演算はブロックされて vanilla JavaScript により基礎となる TypedArray 上で直ちに実行されます。
訓練
deeplearn.js の微分可能なデータフロー・グラフはちょうど TensorFlow におけるように、遅延実行モデル (delayed execution model) を使用します。ユーザはグラフを構築してそして FeedEntry を通して入力 NDArray を提供することでその上で訓練して推論します。
NOTE : 推論モードのためには NDArrayMath と NDArray で十分です。訓練を望む場合にのみグラフが必要です。
Graph と Tensor
Graph オブジェクトはデータフロー・グラフを構築するための核となるクラスです。Graph オブジェクトは実際には NDArray データを保持しません、演算間の接続性のみです。
Graph クラスはトップレベルなメンバー関数として微分可能な演算子を持ちます。演算を追加するために graph メソッドを呼び出す時、Tensor オブジェクトが返されます、これは接続性と shape 情報だけを保持します。
入力に variable を乗算するサンプル graph です :
const g = new Graph();
// Placeholders are input containers. This is the container for where we will
// feed an input NDArray when we execute the graph.
const inputShape = [3];
const inputTensor = g.placeholder('input', inputShape);
const labelShape = [1];
const labelTensor = g.placeholder('label', labelShape);
// Variables are containers that hold a value that can be updated from
// training.
// Here we initialize the multiplier variable randomly.
const multiplier = g.variable('multiplier', Array2D.randNormal([1, 3]));
// Top level graph methods take Tensors and return Tensors.
const outputTensor = g.matmul(multiplier, inputTensor);
const costTensor = g.meanSquaredCost(outputTensor, labelTensor);
// Tensors, like NDArrays, have a shape attribute.
console.log(outputTensor.shape);
Session と FeedEntry
Session オブジェクトは Graph の実行を駆動するものです。FeedEntry (TensorFlow feed_dict に類似) は実行のためのデータを供給するもので、与えられた NDArray から Tensor に値を供給します。
バッチング上のクイックノート : deeplearn.js は演算のための outer dimension としてのバッチングをまだ実装してません。このことはすべてのトップレベル graph op、更には math 関数が単一のサンプル上で動作することを意味します。けれども、バッチングは重要でその結果重み更新はバッチに渡る勾配平均上で動作します。deeplearn.js は入力を提供するために NDArray 直接よりも、訓練 FeedEntry 内の InputProvider を使用することによりバッチングをシミュレートします。InputProvider はバッチで各アイテムのために呼び出されます。入力セットをシャッフルしてそれらを同期的に保持するために InMemoryShuffledInputProviderBuilder を提供します。
上記からの Graph オブジェクトによる訓練 :
const learningRate = .00001;
const batchSize = 3;
const math = new NDArrayMathGPU();
const session = new Session(g, math);
const optimizer = new SGDOptimizer(learningRate);
const inputs: Array1D[] = [
Array1D.new([1.0, 2.0, 3.0]),
Array1D.new([10.0, 20.0, 30.0]),
Array1D.new([100.0, 200.0, 300.0])
];
const labels: Array1D[] = [
Array1D.new([4.0]),
Array1D.new([40.0]),
Array1D.new([400.0])
];
// Shuffles inputs and labels and keeps them mutually in sync.
const shuffledInputProviderBuilder =
new InCPUMemoryShuffledInputProviderBuilder([inputs, labels]);
const [inputProvider, labelProvider] =
shuffledInputProviderBuilder.getInputProviders();
// Maps tensors to InputProviders.
const feedEntries: FeedEntry[] = [
{tensor: inputTensor, data: inputProvider},
{tensor: labelTensor, data: labelProvider}
];
const NUM_BATCHES = 10;
for (let i = 0; i < NUM_BATCHES; i++) {
// Wrap session.train in a scope so the cost gets cleaned up automatically.
math.scope(() => {
// Train takes a cost tensor to minimize. Trains one batch. Returns the
// average cost as a Scalar.
const cost = session.train(
costTensor, feedEntries, batchSize, optimizer, CostReduction.MEAN);
console.log('last average cost (' + i + '): ' + cost.get());
});
}
訓練後、graph を通して推論できます :
// Wrap session.eval in a scope so the intermediate values get cleaned up
// automatically.
math.scope((keep, track) => {
const testInput = track(Array1D.new([0.1, 0.2, 0.3]));
// session.eval can take NDArrays as input data.
const testFeedEntries: FeedEntry[] = [
{tensor: inputTensor, data: testInput}
];
const testOutput = session.eval(outputTensor, testFeedEntries);
console.log('---inference output---');
console.log('shape: ' + testOutput.shape);
console.log('value: ' + testOutput.get(0));
});
// Cleanup training data.
inputs.forEach(input => input.dispose());
labels.forEach(label => label.dispose());
以上