谷歌開源DeepLearn.js:可實現硬件加速的機器學習JavaScript庫

 2017-08-08 14:39:03.0


2017-08-08 12:39 谷歌

選自GitHub

參與:蔣思源、路雪


deeplearn.js 是一個可用於機器智能並加速 WebGL 的開源 Java 庫。 deeplearn.js 提供高效的機器學習構建模塊,使我們能夠在瀏覽器中訓練神經網絡或在推斷模式中運行預訓練模型。它提供構建可微數據流圖的 API,以及一系列可直接使用的數學函數。

本文檔中,我們使用 Type 代碼示例。對於 vanilla Java,你可能需要移除 Type 語法,如 const、let 或其他類型定義。


核心概念

NDArrays

deeplearn.js 的核心數據單元是 NDArray。 NDArray 包括一系列浮點值,它們將構建為任意維數的數組。 NDArray 具備一個用來定義形狀的 shape 屬性。該庫為低秩 NDArray 提供糖子類(sugar subclasses):Scalar、Array1D、Array2D、Array3D 和 Array4D。

2x3 矩陣的用法示例:

const shape = [2, 3]; // 2 rows, 3 columnsconst a = Array2D.new(shape, [1.0, 2.0, 3.0, 10.0, 20.0, 30.0]);

NDArray 可作為 WebGLTexture 在 GPU 上儲存數據,每一個像素儲存一個浮點值;或者作為 vanilla Java TypedArray 在 CPU 上儲存數據。大多數時候,用戶不應思考儲存問題,因為這只是一個實現細節。

如果 NDArray 數據儲存在 CPU 上,那麼 GPU 數學操作第一次被調用時,該數據將被自動上傳至一個 texture。如果你在 GPU 常駐內存的 NDArray 上調用 NDArray.getValues(),則該庫將下載該 texture 至 CPU,然後將其刪除。

NDArrayMath

該庫提供一個 NDArrayMath 基類,其為定義在 NDArray 上運行的一系列數學函數。

NDArrayMathGPU

當使用 NDArrayMathGPU 實現時,這些數學運算對將在 GPU 上執行的著色器程序(shader program)排序。和 NDArrayMathCPU 中不同,這些運算不會阻塞,但用戶可以通過在 NDArray 上調用 get() 或 getValues() 使 cpu 和 gpu 同步化,詳見下文。

這些著色器從 NDArray 上的 WebGLTexture 中讀取和寫入。當連接數學運算時,紋理可以停留在 GPU 內存中(不必下載至運算之間的 CPU),這對性能來說非常關鍵。

以兩個矩陣間的均​​方差為例(有關 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. console.log(average.get()); // average is a Scalar so we use .get()});

注:NDArray.get() 和 NDArray.getValues() 是阻塞調用。因此在執行一系列數學函數之後,無需寄存回調函數,只需調用 getValues() 來使 CPU 和 GPU 同步化。

小技巧:避免在 GPU 數學運算之間調用 get() 或 getValues(),除非你在進行調試。因為這會強制下載 texture,然後後續的 NDArrayMathGPU 調用將不得不重新下載數據至新的 texture 中。

math.scope()

當我們進行數學運算時,我們需要像如上所示的案例那樣將它們封裝到 math.scope() 函數的閉包中。該數學運算的結果將在作用域的端點處得到配置,除非該函數在作用域內返回函數值。

有兩種函數可以傳遞到函數閉包中:keep() 和 track()。

keep() 確保了 NDArray 將得到傳遞並保留,它不會在作用域範圍結束後被自動清除。

track() 追踪了我們在作用域內直接構建的 NDArray。當作用域結束時,任何手動追踪的 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 tr​​ack() 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 tr​​ack() 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 textures 在 Java 的作用範圍之外時,它們因為瀏覽器的碎片回收機製而不會被自動清除。這就意味著當我們使用 GPU 常駐內存完成了 NDArray 時,它隨後需要手動地配置。如果我們完成 NDArray 時忘了手動調用 ndarray.dispose(),那就會引起 texture 內存滲漏,這將會導致十分嚴重的性能問題。如果我們使用 math.scope(),任何由 math.method() 創建的 NDArray 或其它通過作用域返回函數值方法創建的 NDArray 都會被自動清除。

如果我們想不使用 math.scope(),並且手動配置內存,那麼我們可以令 safeMode = false 來構建 NDArrayMath 對象。這種方法我們並不推薦,但是因為 CPU 常駐內存可以通過 Java 碎片回收器自動清除,所以它對 NDArrayMathCPU 十分有用。

NDArrayMathCPU

當我們使用 CPU 實現模型時,這些數學運算是封閉的並且可以通過 vanilla Java 在底層 TypedArray 上立即執行。

訓練

在 deeplearn.js 中的可微數據流圖使用的是延遲執行模型,這一點就和 TensorFlow 一樣。用戶可以通過 FeedEntrys 提供的輸入 NDArray 構建一個計算圖,然後再在上面進行訓練或推斷。

注意:NDArrayMath 和 NDArrays 對於推斷模式來說是足夠的,如果我們希望進行訓練,只需要一個圖就行。

圖和張量

Graph 對像是構建數據流圖的核心類別,Graph 對象實際上並不保留 NDArray 數據,它只是在運算中構建連接。

Graph 類像頂層成員函數(member function)一樣有可微分運算。當我們調用一個圖方法來添加運算時,我們就會獲得一個 Tensor 對象,它僅僅保持連通性和形狀信息。

下面是一個將輸入和變量做乘積的計算圖示例:

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 inputTensor = 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 對像是驅動執行計算圖的方法,FeedEntry 對象(和 TensorFlow 中的 feed_dict 類似)將提供運行所需的數據,並從給定的 NDArray 中饋送一個值給 Tensor 對象。

批處理簡單的註釋:deeplearn.js 並沒有執行批處理作為運算的外部維度(outer dimension)。這就意味著每一個頂層圖運算就像數學函數那樣在單個樣本上運算。然而,批處理十分重要,以至於權重的更新依賴於每一個批量的梯度均值。 deeplearn.js 在訓練 FeedEntry 時通過使用 InputerProvider 模擬批處理來提供輸入向量,而不是直接使用 NDArray。因此,每一個批量中的每一項都會調用 InputerProvider。我們同樣提供了 InMemoryShuffledInputProviderBuilder 來清洗一系列輸入並保持它們的同步性。

通過上面的 Graph 對象訓練:

const learningRate = .001;const batchSize = 2;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([ 2.0, 6.0, 12.0]), Array1D.new([20.0, 60.0, 120.0]), Array1D.new([200.0, 600.0, 1200.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}];// 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 // av erage cost as a Scalar. const cost = session.train( costTensor, feedEntries, batchSize, optimizer, CostReduction.MEAN); console.log('last average cost: ' + cost.get());});

在訓練後,我們就可以通過圖進行推斷:

// Wrap session.eval in a scope so the intermediate values get cleaned up// automatically.math.scope((keep, track) => { const testInput = track(Array1D.new([1.0, 2.0, 3.0])) ; // 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(testOutput.shape); console.log(testOutput.getValues());});

詳情請查看文檔教程: https://pair-code.github.io/deeplearnjs/docs/tutorials/

原文地址: https://pair-code.github.io/deeplearnjs/docs/tutorials/intro.html

文章來源:機器之心