摘要:本文總結了在JavaScript如何表達深度學習中非常要的矩陣和向量,藉助於TypedArray和ArrayBuffer,在JS中,我們也可以高效的處理矩陣數據,爲JS中的深度學習提供了堅實的基礎。但實際上TypedArray是類,提供了一種訪問數組中每個元素的方法,其實際數據存儲在ArrayBuffer中。

最近在讀一本《基於瀏覽器的深度學習》,書比較薄,但是涉及的內容很多,因此在讀的過程中不得不再查閱一些資料,以加深理解。我目前從事的本職工作就是瀏覽器研發,對於前端技術並不陌生。但是從前段時間開發微信小程序 識狗君 的過程來看,對JavaScript還是掌握得太少,特別是對一些前端框架以及一些比較新的JavaScript語法和編程模型,瞭解的不夠。在修改tfjs-core源碼時,就體會到這種痛苦。好吧,既然無法避開,那就正面剛吧。

與Java、C++這樣的靜態類型語言不同,JS中的變量似乎沒有類型,在聲明變量時不用指定變量類型。但實際上JS也有字符串、數字、布爾值、對象、數組、未定義等類型,是一種弱類型語言。在深度學習中,矩陣和向量是最基本的數據結構,而高效的矩陣和向量運算是深度學習計算中的關鍵。在C++中,數組可用於表示矩陣或向量,JS中也有這樣的數據結構嗎?

在JS中,提供了一種TypedArray的類,它是幾種數組類型的統稱:

  • Int8Array

  • Uint8Array

  • Uint8ClampedArray

  • Int16Array

  • Uint16Array

  • Int32Array

  • Uint32Array

  • Float32Array

  • Float64Array

前綴中的U表示無符號的值。Uint8Array和Uint8ClampedArray都是保存0 ~ 255之間的值。如果保存的值大於256,Uint8Array會截掉溢出位,而Uint8ClampedArray對值進行限制,大於255的值限定爲255,小於0的值限定爲0。例如:

var arr = new Uint8Array(4);
arr[0] = 256;
console.log(arr);  // [0, 0, 0, 0]

var arrc = new Uint8ClampedArray(4);
arrc[0] = 256;
arrc[1] = -12;
console.log(arrc);  // [256, 0, 0, 0]

在最新的JS規範中,還增加了 BigInt64Array 和 BigUint64Array 兩種類,但並非每個瀏覽器都支持,請謹慎使用。

TypedArray可以以類型安全的方式訪問數據,而不會造成數據複製的開銷。TypedArray使用上有些類似C++中的數組,可以通過 [] 運算符讀取或寫入值。但實際上TypedArray是類,提供了一種訪問數組中每個元素的方法,其實際數據存儲在ArrayBuffer中。

ArrayBuffer

ArrayBuffer代表內存之中的一段二進制數據,是存儲數據的實際數據結構,但它不提供讀取或寫入數據的任何方式。如何解釋這些存放的數據,取決於TypedArray或稍後要講到的DataView。你可以通過不同的TypedArray訪問ArrayBuffer,可以在ArrayBuffer上使用不同的TypedArray,如何解釋二進制數據的任務被委託給TypedArray。

var buf = new ArrayBuffer(4);
var uint8 = new Uint8Array(buf);
var int16 = new Int16Array(buf);

uint8[0] = 1;
uint8[1] = 1;

console.log(uint8);  // [1, 1, 0, 0]
console.log(int16);  // [257, 0]

因爲uint8和int16共享同一個底層ArrayBuffer,這樣一個修改在兩個TypedArray中都反映出來。TypedArray和ArrayBuffer通過避免冗餘數據複製提供了一種訪問內存數據的高效方法,實現了快速數據訪問。

DataView

讀取和寫入ArrayBuffer數據的另一種方式是通過DataView,用TypedArray能做到的事情,一樣可以用DataView完成。DataView在ArrayBuffer上提供了一個更低層次的接口,DataView不管理存儲數據的類型。每次訪問數據時,你需要知道存儲的數據類型。

var buf = new ArrayBuffer(4);
var d = new DataView(buf);

d.setInt8(0, 10);
console.log(d.getInt8(0));  // 10

需要注意的是,在多字節整數存儲上,存在“大端”和“小端”的不同,取決於機器的體系結構,這意味着內存中同樣的一塊內存數據,在不同體系結構的機器上,解釋爲不同的值。DataView提供了一種顯示指定“大端”和“小端”的接口。

var buf = new ArrayBuffer(4);
new DataView(buf).setInt16(0, 127, true);  // 按小端保存
console.log(new Uint8Array(buf));  // [127, 0, 0, 0]

new DataView(buf).setInt16(0, 127, false);  // 按大端保存
console.log(new Uint8Array(buf));  // [0, 127, 0, 0]

通常情況下,我們無需關心大端和小端,但是如果存在數據在GPU和CPU之間傳遞,或者模型用於跨體系結構的機器上的情況時,就需要注意這個問題。

SharedArrayBuffer

深度學習的JavaScript基礎:從callbacks到sync/await 這篇文章中,我們提到JS代碼是以單線程執行的,但這種說法並非完全正確,因爲在HTML5中引入一種新的線程機制web workers。但web workers與其他語言中使用的線程略有不同。默認情況下,它們不共享內存。

這也就意味着,如果你想和其他線程共享數據,那麼你就需要將數據從一個地方複製到另外一個地方。這是通過函數postMessage 完成的。postMessage 將所有輸入的對象序列化,將其發送到另一個web worker,並將其反序列化並放入內存中。

一眼就可以看出,這種方式相當低效。某些情況下,我們需要更高效的並行策略,希望共享內存單元,ShareArrayBuffer正是爲此而生。

通過 ShareArrayBuffer,web worker、不同線程可以在相同的內存塊中讀寫數據。這也意味着你不再需要通過 postMessage 來在不同的線程中通信傳遞數據。不同的 web worker 都有獲取/操作數據的權限。但是這也會帶來一些問題,比如兩個線程在同一時間對數據進行操作。這也是併發需要解決的問題之一。關於SharedArrayBuffer的併發是一個比較大的話題,這裏先不展開討論。

SharedArrayBuffer 顧名思義就是爲線程間共享內存提供了一塊內存緩衝區,你可以通過 postMessage 將線程 A 分配的 SharedArrayBuffer 發送給線程 B,然後兩個線程就可以共同訪問這塊內存。

下面的代碼通過創建 SharedArrayBuffer 來分配一塊共享內存:

var sab = new SharedArrayBuffer(1024);  // 1KiB shared memory

通過 postMessage 發送給另外一個 Worker 線程:

w.postMessage(sab);

Worker 接收 SharedArrayBuffer 對象:

var sab;
onmessage = function (ev) {
sab = ev.data;  // 1KiB shared memory, the same memory as in the parent
}

同樣的,我們可以通過TypedArray或DataView來讀寫SharedArrayBuffer:

const w = new Worker('worker.js'),
buff = new SharedArrayBuffer(1);
var   arr = new Int8Array(buff);
/* setting data */
arr[0] = 9;
/* sending the buffer (copy) to worker */
w.postMessage(buff);
/* changing the data */
arr[0] = 1;

小結

本文總結了在JavaScript如何表達深度學習中非常要的矩陣和向量,藉助於TypedArray和ArrayBuffer,在JS中,我們也可以高效的處理矩陣數據,爲JS中的深度學習提供了堅實的基礎。

相關文章