deeplearn.js : チュートリアル : ML 非専門家のためのガイド
作成 : (株)クラスキャット セールスインフォメーション
日時 : 08/22/2017
* 本ページは、github.io の deeplearn.js サイトの Tutorials – Guide for non-ML experts を翻訳した上で
適宜、補足説明したものです:
* サンプルコードの動作確認はしておりますが、適宜、追加改変しています。
* ご自由にリンクを張って頂いてかまいませんが、sales-info@classcat.com までご一報いただけると嬉しいです。
NDArray, Tensor, そして number
数学的な tensor
数学的には、”tensor” は線形代数の最も基本的なオブジェクトで、数字、ベクトル、そして行列の一般化です。ベクトルは数字の 1-次元リストとして考えられます; 行列は数字の 2-次元リストです。tensor は単純にその概念を数字の n-次元リストに一般化します。それは任意の多次元矩形配列に配置された構成要素の数字 (あるいは文字列や他のデータ型でさえ) の任意の配置です。
tensor は幾つかの属性を持ちます :
- それは type (型) を持ちます、それはその構成要素、例えば integer, float, etc の各々の型を表します。deeplearn.js は今のところは float32 のみをサポートしています。
- それは shape (形状) を持ちます、整数のリストでこれはコンポーネントの矩形 (= rectangular) 配列の形状を表します。行列が “4 by 4 ” と言う時、行列の shape を表しています。
- それは rank (ランク) を持ちます、それはその shape の長さです; 構成要素の配列の次元です。ベクトルは rank 1 です; 行列は rank 2 を持ちます。
サンプル tensor | Type | Shape | Rank |
Scalar: 3.0 | float | [] | 0 |
Vector: (1, 5, -2) | int | [3] | 1 |
2×2 Matrix | int | [2, 2] | 2 |
number[], NDArray, Tensor: 数学的な tensor のための3つのデータ型
数学者が “tensor” と呼ぶこの同じオブジェクトは deeplearn.js では3つの異なる方法で表現されます。上の議論 (rank, shape, そして type)、これらのすべてに適用されますが、しかしこれらは異なりそれらをストレートに保持することが重要です :
- number[] は数字の配列に相当する基本的な javascript 型です。実際に 0-rank tensor のためにはそれは number で、1-rank tensor のためには number[]、2-rank のためには number[][]、etc. deeplearn.js 内ではそれをそれほど使わないでしょう、しかし deeplearn.js へ何かを出し入れする方法です。
- NDArray は deeplearn.js のよりパワフルな実装です。NDArray を伴う計算はクライアント GPU 上で実行可能で、これは deeplearn.js の基本的な優位点です。最終的に計算が発生する時これがもっとも実際的な tensor データが内在するフォーマットです。Session.eval を呼び出す時、例えばですが、これが返されるものです (そして FeedEntry 入力が在るべきものです)。NDArray.new(number[]) と NDArray.get([indices]) を使用して NDArray と number[] 間を変換できます。
- Tensor は空のバッグです; それは実際のデータを内部に持ちません。Graph が構築される時に使用されるのは placeholder で、これは最終的に内部に収まるデータの shape と型を記録します。それはその構成要素に実際の値は含みません。ちょうど shape と型を知ることにより、けれども、それは Graph 構築時に重要なエラー・キャッシングを行なうことができます ; 2×3 行列に 10×10 行列を乗算することを望む場合、入力データを与える前に、ノードを作成する時に graph は貴方に大声をあげる (= yell) ことができます。Tensor と NDArray または number[] の間で直接変換することは意味がありません; 貴方がそれを行なうことを試みることを見出す場合、次の一つが多分真実です :
- 静的な NDArray を持っています、貴方はそれをグラフで使用することを望みます; constant Tensor ノードを作成するために graph.constant() を使用しましょう。
- NDArray を持っています、貴方はそれをグラフへの入力として供給することを望みます; graph 内で graph.placeholder により Placeholder を作成しましょう、そして FeedEntry で graph に入力を送ります。
- 出力 tensor を持っています、そして貴方は session にその値を評価して返すことを望みます。Session.eval(tensor) を呼び出しましょう。
一般的に、自動微分 (訓練) を望む時には Graph のみを使用すべきです。forward モード推論、または一般的な数値計算だけのためにライブラリを使用することを望む場合には、NDArrayMath で NDArray を使うことで十分でしょう。
もし訓練に興味があるのであれば、Graph を使用しなければなりません。graph を構築した時には Tensor と共同作業するでしょう、そして Session.eval でそれを実行する時、結果は NDArray になるでしょう。
Forward モード推論 / 数値計算
NDArray 上で数値演算を実行することを望むだけならば、単に貴方のデータで NDArray を構築して、そして NDArrayMath オブジェクトでその上で演算を実行できます。
例として、GPU 上で行列にベクトルを掛ける計算をすることを望む場合 :
const math = new NDArrayMathGPU(); math.scope((keep, track) => { const matrixShape = [2, 3]; // 2 rows, 3 columns. const matrix = track(Array2D.new(matrixShape, [10, 20, 30, 40, 50, 60])); const vector = track(Array1D.new([0, 1, 2])); const result = math.matrixTimesVector(matrix, vector); console.log("result shape:", result.shape); console.log("result", result.getValues()); });
NDArrayMath, keep, そして track 上の更なる情報は、チュートリアル : 序説 を参照してください。
NDArray/NDArrayMath 層は NumPy への相似として考えることができます。
訓練: 遅延実行 (delayed execution), Graph, そして Session
deeplearn.js における訓練 (自動微分) について理解すべき最も重要なことはそれは遅延実行モデル (delayed execution model) を使用することです。貴方のコードは2つの分離した段階を含むでしょう : 最初に Graph を構築します、実行したい計算を表すオブジェクトです、それから後で graph を実行して結果を得るでしょう。
殆どの時間、貴方の Graph はある入力(群)をある出力(群)に変換します。一般的に、Graph のアーキテクチャは固定されたままですが、自動的に更新されるパラメータを含むでしょう。
Graph を実行する時、2つのモードがあります: 訓練、そして推論です。
推論は Graph に出力を生成するために入力を提供する行為です。
graph を訓練することは Graph にラベル付けされた入力/出力のペアの多くのサンプルを提供することを伴い、そして自動的に Graph のパラメータを更新してその結果、入力を評価 (推論) する時 Graph の出力がラベル付けされた出力に近くなります。
ラベル付けされた出力が生成された出力にどのくらい近いかを表す Scalar を与える関数は “目的関数 (cost function)” と呼ばれます (“損失関数 (loss function)” としてもまた知られます)。モデルが上手く動作する時、損失関数はゼロ近くを出力すべきです。訓練する時には目的関数を提供しなければなりません。
deeplearn.js は TensorFlow に非常に類似して構造化されています、これは Google の python ベースの機械学習言語です。もし TensorFlow を知っているのであれば、Tensor, Graph, そして Session の概念は殆どすべて同じです、けれどもここでは TensorFlow の知識は仮定しません。
関数としての Graph
違いは標準の JavaScript コードへの類推により理解されます。このチュートリアルの残りについて、この二次方程式を扱っていきます :
// y = a * x^2 + b * x + c const x = 4; const a = Math.random(); const b = Math.random(); const c = Math.random(); const order2 = a * Math.pow(x, 2); const order1 = b * x; const y = order2 + order1 + c;
この元のコードでは、数値計算は各行でそれが処理される時に直ちに評価されます。
次のコードでは、deeplearn.js Graph 推論がどのように動作するかへの類推で、対照的です。
function graph(x, a, b, c) { const order2 = a * Math.pow(x, 2); const order1 = b * x; return order2 + order1 + c; } const a = Math.random(); const b = Math.random(); const c = Math.random(); const y = graph(4, a, b, c);
このコードは2つのブロックから成ります: 最初に graph 関数をセットアップし、それからそれを呼び出します。最初の数行で graph 関数をセットアップするコードは、関数が最後の行で呼び出されるまでどのような実際の数値演算も行ないません。セットアップの間、計算はまだ実行されていなくても基本的なコンパイラ type-safety エラーは捕らえることができます。
これは Graph が deeplearn.js でどのように動作するかへの正確な類推です。貴方のコードの最初のパートは graph のセットアップで、以下を記述します :
- 入力、私たちのケースでは “x” です。入力は placeholder として表されます (e.g. graph.placeholder())。
- 出力、私たちのケースでは “order1”, “order2”, そして最後の出力 “y” です。
- 出力を生成する演算子、私たちのケースでは二次の分解された関数です (x^2, 乗算, 加算)。
- 更新可能なパラメータ、私たちのケースでは “a”, “b”, “c” です。更新可能なパラメータは variable として表されます (e.g. graph.variable())。
そしてコードの後半のパートで幾つかの入力上で graph の関数を “call” (Session.eval) して、そして Session.train で幾つかのデータに対して “a”, “b”, そして “c” のための値を学習するでしょう。
上の関数類推と deeplearn.js Graph の間の一つの小さな違いは Graph はその出力を指定しないことです。代わりに、Graph の呼出し側はどの tensor を返すことを望むかを指定します。これは同じ Graph にその異なるパートを実行させるという異なる呼出しを可能にします。呼出し側により要求された結果を得るために必要なパートだけが評価されます。
Graph の推論と訓練は Session オブジェクトにより駆動されます。このオブジェクトはランライム状態、重み、活性化、そして勾配 (導関数) を含み、その一方で Graph オブジェクトは接続性情報のみを保持しています。
従って上の関数は deeplearn.js では以下のように実装されるでしょう :
const graph = new Graph(); // Make a new input in the graph, called 'x', with shape [] (a Scalar). const x: Tensor = graph.placeholder('x', []); // Make new variables in the graph, 'a', 'b', 'c' with shape [] and random // initial values. const a: Tensor = graph.variable('a', Scalar.new(Math.random())); const b: Tensor = graph.variable('b', Scalar.new(Math.random())); const c: Tensor = graph.variable('c', Scalar.new(Math.random())); // Make new tensors representing the output of the operations of the quadratic. const order2: Tensor = graph.multiply(a, graph.square(x)); const order1: Tensor = graph.multiply(b, x); const y: Tensor = graph.add(graph.add(order2, order1), c); // When training, we need to provide a label and a cost function. const yLabel: Tensor = graph.placeholder('y label', []); // Provide a mean squared cost function for training. cost = (y - yLabel)^2 const cost: Tensor = graph.meanSquaredCost(y, yLabel); // At this point the graph is set up, but has not yet been evaluated. // **deeplearn.js** needs a Session object to evaluate a graph. const math = new NDArrayMathGPU(); const session = new Session(graph, math); math.scope((keep, track) => { /** * Inference */ // Now we ask the graph to evaluate (infer) and give us the result when // providing a value 4 for "x". // NOTE: "a", "b", and "c" are randomly initialized, so this will give us // something random. const result: NDArray = session.eval(y, [{tensor: x, data: track(Scalar.new(4))}]); /** * Training */ // Now let's learn the coefficients of this quadratic given some data. // To do this, we need to provide examples of x and y. // The values given here are for values a = 3, b = 2, c = 1, with random // noise added to the output so it's not a perfect fit. const xs: Scalar[] = [ track(Scalar.new(0)), track(Scalar.new(1)), track(Scalar.new(2)), track(Scalar.new(3)) ]; const ys: Scalar[] = [ track(Scalar.new(1.1)), track(Scalar.new(5.9)), track(Scalar.new(16.8)), track(Scalar.new(33.9)) ]; // When training, it's important to shuffle your data! const shuffledInputProviderBuilder = new InCPUMemoryShuffledInputProviderBuilder([xs, ys]); const [xProvider, yProvider] = shuffledInputProviderBuilder.getInputProviders(); // Training is broken up into batches. const NUM_BATCHES = 5; const BATCH_SIZE = xs.length; // Before we start training, we need to provide an optimizer. This is the // object that is responsible for updating weights. The learning rate param // is a value that represents how large of a step to make when updating // weights. If this is too big, you may overstep and oscillate. If it is too // small, the model may take a long time to train. const LEARNING_RATE = .001; const optimizer = new SGDOptimizer(LEARNING_RATE); for (let i = 0; i < NUM_BATCHES; i++) { // Train takes a cost tensor to minimize; this call trains one batch and // returns the average cost of the batch as a Scalar. const costValue = session.train( cost, // Map input providers to Tensors on the graph. [{tensor: x, data: xProvider}, {tensor: y, data: yProvider}], BATCH_SIZE, optimizer, CostReduction.MEAN); console.log('average cost: ' + costValue.get()); } });
モデルを訓練した後、与えられた "x" に対して "y" の値を得るために graph を通して再度推論できます。
もちろん、実際には、Scalar 値を使うことだけを望むのではないでしょう。deeplearn.js はパワフルな hardware-accelerated 線形代数を提供します、これは画像認識、テキスト生成等あらゆるものに利用できます。より詳しくは他のチュートリアルを見てください。
以上