深度學習的 JavaScript 基礎:矩陣和向量的表示
摘要:本文總結了在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中的深度學習提供了堅實的基礎。