deeplearn.js教程 - 介紹

介紹

deeplearn.js是用于機(jī)器智能的開源WebGL加速JavaScript庫。 deeplearn.js將高性能的機(jī)器學(xué)習(xí)構(gòu)建塊帶到您的指尖,讓您可以在瀏覽器中訓(xùn)練神經(jīng)網(wǎng)絡(luò)或在推斷模式(inference mode)下運(yùn)行預(yù)先訓(xùn)練的模型。 它提供了一個(gè)用于構(gòu)建可微數(shù)據(jù)流圖的API,以及可以直接使用的一組數(shù)學(xué)函數(shù)。

您可以在這里找到補(bǔ)充本教程的代碼。

運(yùn)行:

./scripts/watch-demo demos/intro/intro.ts

然后訪問http://localhost:8080/demos/intro/

或者直接點(diǎn)擊這里觀看我們的演示。

此文檔將使用TypeScript代碼示例。 對于vanilla JavaScript,您可能需要刪除某些TypeScript類型的注釋或定義。

核心概念

NDArrays

deeplearn.js中的中心數(shù)據(jù)單元是NDArray。 一個(gè)NDArray由一組浮點(diǎn)值組成,它們是一個(gè)任意數(shù)量的數(shù)組。 NDArray有一個(gè)shape屬性來定義它們的形狀。 該庫為低級NDArrays提供糖類:ScalarArray1DArray2DArray3DArray4D

使用2x3矩陣的示例:

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

NDArray可以在GPU上存儲數(shù)據(jù)作為WebGLTexture,其中每個(gè)像素存儲一個(gè)浮點(diǎn)值,或者作為一個(gè)vanilla JavaScript TypedArray在CPU上存儲數(shù)據(jù)。 大多數(shù)時(shí)候,用戶不應(yīng)該考慮存儲,因?yàn)樗且粋€(gè)實(shí)現(xiàn)上的細(xì)節(jié)。

如果NDArray數(shù)據(jù)存儲在CPU上,則首次調(diào)用GPU數(shù)學(xué)運(yùn)算時(shí),數(shù)據(jù)將自動上傳到紋理。 如果在GPU駐留的NDArray上調(diào)用NDArray.getValues(),庫將把紋理下載到CPU并刪除紋理。

NDArrayMath

該庫提供了一個(gè)NDArrayMath基類,它定義了一組對NDArray進(jìn)行操作的數(shù)學(xué)函數(shù)。

NDArrayMathGPU

當(dāng)使用NDArrayMathGPU實(shí)現(xiàn)時(shí),這些數(shù)學(xué)運(yùn)算將著色器程序排列在GPU上執(zhí)行。 與NDArrayMathCPU不同,這些操作并不阻止,但用戶可以通過在NDArray上調(diào)用get()getValues()來同步cpu和gpu,如下所述。

這些著色器從NGArray擁有的WebGLTextures中讀取和寫入。 當(dāng)接入數(shù)學(xué)運(yùn)算時(shí),紋理可以停留在GPU內(nèi)存中(未在操作之間下載到CPU),這對于性能至關(guān)重要。

采取兩個(gè)矩陣之間的均方差的示例(有關(guān)math.scopekeep,以及track的細(xì)節(jié))

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]));

  // 非阻塞數(shù)學(xué)調(diào)用。
  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);

  // 阻止調(diào)用實(shí)際從平均值讀取值。
  // 等待直到GPU返回值之前完成執(zhí)行操作。
  // average是一個(gè)標(biāo)量,所以我們使用.get()
  console.log(average.get());
});

注意:NDArray.get()NDArray.getValues()正在阻止調(diào)用。 執(zhí)行被鏈接的數(shù)學(xué)函數(shù)后無需注冊回調(diào),只需調(diào)用getValues()來同步CPU和GPU。

math.scope()

當(dāng)使用數(shù)學(xué)運(yùn)算時(shí),必須將它們包裝在一個(gè)math.scope()函數(shù)閉包中,如上面的例子所示。 此范圍內(nèi)的數(shù)學(xué)運(yùn)算結(jié)果將被放置在范圍的末尾,除非它們是范圍中返回的值。

兩個(gè)函數(shù)傳遞給函數(shù)閉包,keep()track()

keep()確保在范圍結(jié)束時(shí),傳遞給保留的NDArray將不會自動清除。

track()跟蹤可以在范圍內(nèi)直接構(gòu)造的任何NDArray。 當(dāng)范圍結(jié)束時(shí),任何手動跟蹤的NDArray將被清理。 所有math.method()函數(shù)的結(jié)果以及許多其他核心庫函數(shù)的結(jié)果都會自動清除,因此您不必手動跟蹤它們。

const math = new NDArrayMathGPU();

let output;

// 您必須擁有一個(gè)外部范圍,但不用擔(dān)心,如果沒有該庫,則會導(dǎo)致錯(cuò)誤。
math.scope((keep, track) => {
  // 正確:默認(rèn)情況下,數(shù)學(xué)不會跟蹤直接構(gòu)造的NDArray。
  // 您可以在NDArray上調(diào)用track(),以便在范圍結(jié)束時(shí)進(jìn)行跟蹤和清理。
  const a = track(Scalar.new(2));

  // 錯(cuò)誤:這是紋理泄漏!
  // 數(shù)學(xué)不知道b,所以它不能跟蹤它。 當(dāng)范圍結(jié)束時(shí),GPU駐留的NDArray不會被清理,即使b超出范圍。 
  // 確保您在創(chuàng)建的NDArrays上調(diào)用track()。
  // scope. Make sure you call track() on NDArrays you create.
  const b = Scalar.new(2);

  // 正確:默認(rèn)情況下,數(shù)學(xué)跟蹤數(shù)學(xué)函數(shù)的所有輸出。
  const c = math.neg(math.exp(a));

  // 正確:d由父范圍跟蹤。
  const d = math.scope(() => {
    // 正確:當(dāng)內(nèi)部范圍結(jié)束時(shí),e將被清理。
    const e = track(Scalar.new(3));

    // 正確:
    // 這個(gè)數(shù)學(xué)功能的結(jié)果已經(jīng)被跟蹤。
    // 由于它是該范圍的返回值,它將不會被內(nèi)部范圍清理。
    // 結(jié)果將在父范圍內(nèi)自動跟蹤。
    return math.elementWiseMul(e, e);
  });

  // 正確但是請注意:math.tanh的輸出將被自動跟蹤,但是我們可以在其上調(diào)用keep(),以便在范圍結(jié)束時(shí)保留它。
  // 這意味著如果您稍后調(diào)用output.dispose()時(shí)不小心,可能會引入紋理內(nèi)存泄漏。 
  // 一個(gè)更好的方法是將此值作為范圍的返回值返回,以便在父作用域中進(jìn)行跟蹤。
  output = keep(math.tanh(d));
});

更多技術(shù)細(xì)節(jié):當(dāng)WebGL紋理超出JavaScript范圍時(shí),它們不會被瀏覽器的垃圾收集機(jī)制自動清理。 這意味著當(dāng)你完成一個(gè)GPU駐留的NDArray,它必須在一段時(shí)間后手動放置。 如果您在完成NDArray后忘記手動調(diào)用ndarray.dispose(),那么您將會引入紋理內(nèi)存泄漏,從而導(dǎo)致嚴(yán)重的性能問題。 如果使用math.scope(),則由math.method()創(chuàng)建的任何NDArray和通過范圍返回結(jié)果的任何其他方法將自動被清除。

如果要進(jìn)行手動內(nèi)存管理,而不使用math.scope(),則可以使用safeMode = false構(gòu)造NDArrayMath對象。 這是不推薦的,但對于NDArrayMathCPU是有用的,因?yàn)镃PU駐留的內(nèi)存將被JavaScript垃圾回收器自動清理。

NDArrayMathCPU

當(dāng)使用CPU實(shí)現(xiàn)時(shí),這些數(shù)學(xué)運(yùn)算被阻塞,并使用vanilla JavaScript立即在底層TypedArray上執(zhí)行。

訓(xùn)練

deeplearn.js中的可微數(shù)據(jù)流圖使用延遲執(zhí)行模型,就像在TensorFlow中一樣。 用戶構(gòu)建Graph,然后通過FeedEntry提供輸入NDArray來對其進(jìn)行訓(xùn)練或推斷。

注意:NDArrayMath和NDArrays足以推斷模式。 如果你想訓(xùn)練,你只需要一個(gè)圖形(Graph)。

圖形(Graph)和張量(Tensor)

Graph對象是構(gòu)建數(shù)據(jù)流圖的核心類。Graph對象實(shí)際上并不包含NDArray數(shù)據(jù),只能在操作之間進(jìn)行連接。

Graph類具有可操作的操作作為頂級成員函數(shù)。 當(dāng)您調(diào)用Graph方法來添加操作時(shí),您將返回一個(gè)僅保存連接和形狀信息的Tensor對象。

一個(gè)將輸入與變量相乘的示例圖:

const g = new Graph();

// 占位符是輸入容器。
// 這是在我們執(zhí)行圖形(graph)時(shí)我們將為我們傳送輸入NDArray的容器。
const inputShape = [3];
const inputTensor = g.placeholder('input', inputShape);

const labelShape = [1];
const labelTensor = g.placeholder('label', labelShape);

// 變量是容納可以從培訓(xùn)中更新的值的容器。
// 這里我們隨機(jī)初始化乘數(shù)變量。
const multiplier = g.variable('multiplier', Array2D.randNormal([1, 3]));

// 最高級圖形(graph)方法采用Tensor并返回Tensor。
const outputTensor = g.matmul(multiplier, inputTensor);
const costTensor = g.meanSquaredCost(outputTensor, labelTensor);

// Tensor,像NDArray,有一個(gè)shape屬性。
console.log(outputTensor.shape);

會話(Session)和 FeedEntry

會話對象用于驅(qū)動Graph的執(zhí)行。 FeedEntry(類似于TensorFlow的feed_dict)為運(yùn)行提供數(shù)據(jù),從給定的NDArray向Tensor提供值。

關(guān)于批處理的一個(gè)快速注意事項(xiàng):deeplearn.js尚未實(shí)現(xiàn)批處理作為操作的外部維度。 這意味著每個(gè)最高級圖形(Graph)操作以及數(shù)學(xué)函數(shù)都可以在單個(gè)示例中進(jìn)行操作。 但是,批處理是很重要的,因此權(quán)重更新是根據(jù)批次的平均梯度進(jìn)行操作的。deeplearn.js通過在訓(xùn)練FeedEntry中使用InputProvider來模擬批處理,以直接提供輸入,而不是NDArrayInputProvider將在批處理中調(diào)用每個(gè)項(xiàng)目。 我們提供InMemoryShuffledInputProviderBuilder來混洗一組輸入并保持它們同步。

用上面的Graph對象進(jìn)行訓(xùn)練:

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])
];

// 混合輸入和標(biāo)簽,并保持它們相互同步。
const shuffledInputProviderBuilder =
  new InCPUMemoryShuffledInputProviderBuilder([inputs, labels]);
const [inputProvider, labelProvider] =
  shuffledInputProviderBuilder.getInputProviders();

// 將張量映射到InputProviders。
const feedEntries: FeedEntry[] = [
  {tensor: inputTensor, data: inputProvider},
  {tensor: labelTensor, data: labelProvider}
];

const NUM_BATCHES = 10;
for (let i = 0; i < NUM_BATCHES; i++) {
  // 在會話中包裝session.train,以便自動清除成本。
  math.scope(() => {
    // 訓(xùn)練需要一個(gè)成本張量來最小化。
    // 訓(xùn)練一批。返回平均成本作為標(biāo)量。
    const cost = session.train(
        costTensor, feedEntries, batchSize, optimizer, CostReduction.MEAN);

    console.log('last average cost (' + i + '): ' + cost.get());
  });
}

訓(xùn)練后,我們可以通過Graph推斷:

// 在會話中包含session.eval,以便中間值被自動清理。
math.scope((keep, track) => {
  const testInput = track(Array1D.new([0.1, 0.2, 0.3]));

  // session.eval可以將NDArray作為輸入數(shù)據(jù)。
  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));
});

// 清理訓(xùn)練數(shù)據(jù)。
inputs.forEach(input => input.dispose());
labels.forEach(label => label.dispose());
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容