介紹
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
提供糖類:Scalar
,Array1D
,Array2D
,Array3D
和Array4D
。
使用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.scope
,keep
,以及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
來模擬批處理,以直接提供輸入,而不是NDArray
。InputProvider
將在批處理中調(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());