第3章
JavaScript深度學習架構
在本章中,我們會介紹用在浏覽器上的三種不同的JavaScript深度學習架構:TensorFlow.js、WebDNN和Keras.js。我們将給每個項目、特性做概覽式介紹,并提供代碼示例運作簡單的分類任務。
在3.1節中,我們将介紹TensorFlow.js,它是 Google貢獻的一個JavaScript深度學習架構。我們将用核心API實作和訓練一個神經網絡來解決 XOR 問題,它以前以Deeplearn.js 的名字為人們熟知。
在3.2節,我們将介紹WebDNN架構,它是由東京大學開發的機器學習庫。我們将轉化一個預訓練的ResNet模型,并在WebDNN架構中加載它來進行圖像分類。
在3.3 節,我們将講解第三個JavaScript深度學習架構Keras.js,它可以将Keras模型運作在浏覽器上。Keras.js是由Leon Chen(MD.ai 的聯合創始人)開發的,通過WebGL 2提供GPU支援。為了比較其他架構,我們将加載預訓練的模型,并執行分類任務。
3.1 TensorFlow.js
TensorFlow.js是在2017年中期公開釋出的一個開源的 JavaScript 機器學習架構,之前的名字是Deeplearn.js。它設計為運作在浏覽器上,利用WebGL加速。雖然它不是第一個利用使用者硬體執行深度神經網絡(WebDNN和TensorFire已經釋出過)的架構,但TensorFlow.js是第一個公開發行的、在浏覽器中提供硬體加速的神經網絡訓練的架構。使用者可以直接在浏覽器中提供資料,進行實時訓練學習,而不用額外安裝軟體。TensorFlow.js也無須用單獨的深度學習架構建構離線的模型。随着浏覽器提供資料(例如,網絡攝像頭、麥克風等)的獨特性,TensorFlow.js開辟了許多新應用,這些會在本章的後續節中介紹。
學習新架構的最好的方法之一,是直接學習一個簡單的例子,通過每行代碼解釋核心概念。TensorFlow.js也不例外。我們将通過一個基本的多層感覺機學習XOR函數的例子來探索TensorFlow.js庫。TensorFlow.js的訓練API 是基于動态神經網絡建立範式,類似TensorFlow 的Eager、Chainer和PyTorch這樣的架構。如果你熟悉這些架構中的任意一個,你将毫不費力地學會TensorFlow.js。即使你不會也不用擔心,接着我們會解釋每個過程。
3.1.1 TensorFlow.js 介紹
TensorFlow.js的官方網站提供了一個線上示範、實驗的神經網絡平台(playgroud),無須安裝任何其他庫。簡單地打開浏覽器,通路TensorFlow.js官方網站。然後點選右上角的“Try it Live!”按鈕。這将會開啟一個Codepen,你可以在命名空間tf導入TensorFlow.js的函數庫。為了介紹,我們将使用TensorFlow.js的線上示範的神經網絡平台運作一個示例。
你可以在TensorFlow.js示例的Github找到示例應用,比如,MNIST分類、Mobilenet模型、情感分析、轉移學習,等等。
3.1.2 XOR 問題
為了學習 TensorFlow.js 的核心概念,我們将訓練一個多層感覺機來學習 XOR 問題。這是一個典型的介紹神經網絡的示例,因為使用線性方法解決不了該問題。
簡單地說,我們要訓練一個分類器去預測兩個二值特征的一個二值結果。如果這兩個二值特征相同,訓練的分類器輸出結果必須為0。反之,它必須預測為1。該函數的真值表如圖3-1所示。

為了讓例子看起來更有趣,我們假設兩個特征輸入的取值為-1~1之間的實數值,而不是前面的二值。接着,如果輸入都為正值或者都為負值,分類器預測必須為0。否則,分類器預測為1。圖3-2是顯示的一些樣本輸入,樣本的顔色代表分類。
正如你從圖3-2看到的,沒有單個直線可以将所有的樣本點分為兩個類别。是以上述問題不能用線性方法解決。
3.1.3 解決 XOR 問題
打開TensorFlow.js 線上示範平台,複制下面的代碼到 JavaScript 文本框内并運作。即使你不懂下面的代碼也不用擔心。我們将一行一行地解釋代碼。
運作上述代碼之後,你會注意到控制台輸出分成三部分。
第一部分是一個未訓練的神經網絡在生成的測試資料上的快速測試。生成100個測試樣本點,使用未訓練的神經網絡模型為每個樣本預測結果,并計算準确度。因為這些測試樣本點和網絡模型的權重都是随機生成的,是以你每次看到的結果可能都不同。然而,你将注意到未訓練的網絡模型的準确度為50%左右。這隻是随機猜測每個樣本屬于哪個分類,因為我們沒有輸入訓練資料進行學習。
第二部分顯示神經網絡模型每次疊代的訓練誤差。可以觀察到随着訓練過程的進行,誤差會随之下降。根據你的計算機的計算速度,這步可能需要幾秒鐘。
最後,我們用新訓練好的神經網絡模型重複第一部分的測試。因為我們的資料樣本和初始權重是随機的,是以每次運作的最終準确度的結果稍有不同。但是,大部分情況下你會看到準确度超過80%,也就是說,我們的神經網絡模型确實學習到了XOR函數。這些都是在浏覽器上完成的。
3.1.4 網絡架構
在學習代碼之前,我們來讨論下學習XOR函數的網絡。它是一個樸素的多層感覺機,具有2個隐藏層,隐藏層的神經元數目分别是20和5。XOR函數有兩個輸入和一個二值輸出。這是目前比較常見的神經網絡,每個隐藏層後接着ReLu激活函數。圖3-3顯示該神經網絡的視圖。
訓練神經網絡的過程使用Adam優化算法,學習率為 0.01,batch的大小為20,疊代次數為100。
你可能會想,我們是如何想出這個神經網絡的。對于一個簡單的問題,比如,XOR函數,你可能不需要許多層和神經元。使用樸素的SGD優化算法就可以解決問題,根本不需要Adam優化算法。但是這個例子的目的不是建構最優的網絡模型,而隻是為了介紹TensorFlow.js的概念。一旦對TensorFlow.js 足夠熟悉,你可以根據實際情況進行配置以得到更好的結果。
代碼的前面幾行定義神經網絡模型的常量。
3.1.5 張量
深度學習架構最常見的概念是張量(Tensor),它隻是矩陣的一般化,存儲一個多元數組。在TensorFlow.js 中,張量是處理過程中核心的資料結構。
目前,TensorFlow.js支援0維張量(标量)到 4 維張量。
任意神經網絡處理的資料都要表示為一個張量,比如訓練集、測試集、神經網絡權重等。TensorFlow.js通過張量使得使用者對WebGL着色器程式的使用無感。本質上,TensorFlow.js處理張量資料從CPU(主要的JavaScript線程)到GPU(WebGL 着色器)的來回的轉換,并傳回結果。
TensorFlow.js提供許多公共函數建立張量。在XOR解決方案中,我們使用如下的方式建立張量:
你會注意到前6行代碼都在定義可訓練的變量,它們是帶有兩個隐藏層的多層感覺機将要使用的。每個隐藏層初始化它們的權重和偏置。我們也定義輸出層的權重和偏置(單值,因為我們的模型隻有一個輸出神經元)。
我們使用公共函數tf.randomNormal建立權重張量W1。tf.randomNormal傳回一個給定形狀的張量, 其值從正态分布中随機抽樣。 對于W1,它是一個形狀為 [dimIn, numNeurons1]的2D張量,其中numNeurons1是第一個隐藏層的神經元數量。第一個值dimIn是輸入到神經網絡的輸入數量,對于XOR問題,這裡為2。
這裡要指出,張量是不可變的。但是,對于神經網絡的權重來說,我們需要其随着疊代訓練不斷地更新。因為這個特殊的要求,我們封裝一個tf.variable調用,将張量轉換成一個變量(Variable)。變量是一種特殊的張量,它的值是可變的。
我們使用公共函數tf.zeros建立偏置,該函數類似tf.randomNormal函數。唯一不同的是tf.zeros函數傳回的張量的元素值都初始化為0。
最後,定義了兩個标量張量eps和one。這些常量将用于後面代碼的各種操作中。有一點要注意的是,你不能簡單地将一個JavaScript變量和一個張量進行正常的操作。對常量使用張量操作時,我們必須将這些常量表示成一個張量對象。
3.1.6 張量操作
為了在張量之間處理資料,我們必須進行張量操作 (operation)。 跟前面一樣,TensorFlow.js 提供了各種張量操作。讓我們看下predict函數,該函數傳入一個XOR輸入的2D張量,為每個輸入輸出神經網絡模型的輸出結果。
你可以看到predict函數是如何使用神經網絡的權重和偏置預測一個batch的XOR輸入的batch輸出結果。input是一個形狀為[batchSize, dimIn]的2D張量。
目前可以先忽略tf.tidy函數,第一步是使用下面的張量操作計算第一個隐藏層的輸出:
- 對輸入input和權重張量W1進行矩陣乘法。
- 接着在上一個結果中加上一個偏置b1。
- 再對上面的結果應用 ReLu 激活函數。
上面這些張量操作可以用下面的一行代碼實作。
讓我們來分解上述代碼。
所有的張量對象都有方法來做各種操作。對于第一個矩陣乘法操作,我們調用input張量的matMul方法,并傳入權重張量W1。在本例子中,操作的輸出是一個形狀為[batchSize, 20]的2D張量。
因為張量操作的輸出也是張量對象,是以你可以在處理的過程中簡單地連結下一步,在矩陣乘法的輸出上增加一個偏置,隻需要調用add方法并傳入偏置b1。
最後,連結relu操作,用hidden常量存儲第一個隐藏層的輸出結果。
第二個隐藏層和輸出層的輸出結果的計算方法都與第一個隐藏層相似,除了使用的變量不同。輸出層使用sigmoid激活函數,該激活函數表示XOR的一個二值輸出。然後,你可以使用as1D方法将形狀為[batchSize, 1]的輸出張量轉換成一個[batchSize]的1D張量。這使得損失函數的計算更加友善有效。
注意,對于多分類輸出,你可能需要使用softmax激活函數,保證輸出是一個形狀為[batchSize, dimOut]的2D張量。
對于這個簡單的神經網絡模型來說predict函數就介紹完了。對于更複雜的神經網絡,TensorFlow.js 提供更多的張量操作,比如卷積操作、池化操作和批量歸一化(batch normalization)操作。這些操作在官方文檔中都有描述。
現在我們介紹損失函數。
對于單個二值輸出,常用的誤差名額是對數損失(logarithmic loss)。計算單個預測值與目标值的對數損失的公式如下:
其中y是期望輸出結果($0$ 或者$1$),p是預測機率(取值範圍在$0$~$1$之間)。為了避免取$0$的對數,我們在預測值的結果上增加一個較小的eps。
我們的eoss函數是計算一個batch的預測輸出結果與目标值之間的平均損失。預測值和目标值都是一個形狀為[batchSize]的1D張量。預測值是指predict函數的輸出,目标值是指每個被預測的樣本的實際分類。
注意,eoss函數傳回一個标量張量。這對于後續的訓練過程相當重要。因為該标量張量是優化器要最小化的值。
你會發現損失函數中的tf.add函數不同,這些操作并不需要從一個張量對象的方法中調用。它們是全局tf命名空間中的獨立函數。然而,這些不同的僅僅是在文法上,使得你的代碼更加友善地使用。
下面介紹tf.tidy的相關細節。無論何時我們建立張量對象,TensorFlow.js為每個張量建立相應的WebGL紋理。因為每個張量操作建立一個新的張量對象,像predict和loss函數中的鍊式操作都會建立一串新的中間WebGL紋理。在正常情況下,JavaScript的垃圾回收器釋放不再引用的記憶體是沒有問題的,但是,不幸的是,這對WebGL紋理不适用。在建立WebGL紋理後,我們需要使用張量的dispose方法手動去釋放不再使用的WebGL紋理。這看起來很快會冗長乏味,想象一下每次張量操作後都需要調用dispose方法。
tf.tidy通過跟蹤它的閉包函數建立的所有張量對象來解決上述問題。将所有的張量操作簡單封裝在tf.tidy調用中,所有調用中建立的紋理,不管紋理是否屬于傳回張量,都會在函數末尾一次性釋放。非常友善!
3.1.7 模型訓練
在準備好變量和張量操作後,是時候進行模型訓練了。在程式的開始使用如下代碼定義optimizer變量。
這将傳回一個Optimizer對象,我們可以在train函數中使用,代碼如下。
首先,train函數是異步的。因為模型訓練的過程時間長,確定它運作在獨立于主CPU UI線程之外的一個線程,以防阻塞浏覽器。
train函數的參數有numIterations和done。numIterations是訓練模型的疊代次數。
done參數允許我們在train函數完成主程序疊代後,盡快執行回調函數。
模型疊代訓練由以下步驟組成:
1.生成batchSize個随機樣本點來訓練模型。注意,在其他常見的訓練執行個體中,我們不可能飛快地生成訓練資料。在本例中,你必須根據你的應用抽取訓練batch,常用的技術比如,shuffle訓練集中一個固定集合。
2.在每個batch的訓練集上訓練模型,得到每個樣本點的預測值。
3.計算每個batch的損失值之和。
4.優化器算法在神經網絡中反向傳播,更新神經網絡的變量值。
5.每疊代10次将訓練損失取對數。
6.使用await tf.nextFrame()確定不會阻塞浏覽器渲染任意UI變化。下面關注其中的第 2~4 步。這些步驟被包含在回調中。
優化器Optimizer對象有一個最小化方法minimize,該方法輸入一個函數f,傳回一個标量,在本例中傳回的是predLoss。然後,優化器Optimizer試圖通過計算傳回标量對變量張量(神經網絡的權重和偏置)的梯度,達到最小化該标量。相應地,更新變量張量的值。
我們将布爾型true傳給minimize狀态,f和predLoss傳回的标量配置設定給常量cost。這主要是用來取對數的。
接着把注意力放到第 5 步,執行下面的代碼。
上面這步的唯一目的是對訓練損失值取對數。當我們需要通過一個張量操作抽取一個張量的值時,引入一個重要的概念。
當我們使用GPU時,預設所有的張量操作是非阻塞的、異步執行的。這意味着傳回的張量隻是一個簡單的占位符,JavaScript的主線程沒有立即擷取它的資料。CPU抽取一個張量時,TensorFlow.js提供data和dataSync兩種方法。
第一個方法data是非阻塞的,它傳回一個Promise。當有了張量資料時,該Promise會快速擷取資料。對于所有的Promise,你可以調用它們注冊為一個函數,傳回張量的資料。在本例中,我們使用data列印cost的值。
第二個方法dataSync是data方法的阻塞版。當你需要阻塞直到張量的值合适時,這時調用dataSync函數。一般在實際應用中要避免使用dataSync函數,因為它會阻塞CPU主線程,浏覽器什麼事也做不了。
重點要注意的是,從data或者dataSync函數擷取值時是從GPU下載下傳到CPU,會使得應用的運作變慢。隻有當你确實需要時才建議使用,比如當顯示最終的結果或者調試或者基準測試時。
最後,在每次疊代訓練的末尾,你需要調用await tf.nextFrame()。這是Tensor-Flow.js的公共函數,提供暫停處理、調用requestAnimationFrame和浏覽器調用完成後的
重新啟動的功能。沒有await tf.nextFrame(),動畫隻能在整個疊代模型訓練完成後才能渲染。這在實際應用中将會暫停一下,渲染浏覽器。
前面的代碼使用了不止一次test函數,我們不在這裡詳細讨論了。該函數輸入一個batch的特征(xs)和相應的标(ys),在神經網絡模型中處理,并輸出它們的準确度。此時,你應該知道了所有的概念來了解本例。我們将解密test函數留給讀者作為一個練習。
3.1.8 TensorFlow.js 的生态
當我們深入學習TensorFlow.js時,你可以找到許多資源、代碼倉庫和線上示例。本節将簡單介紹 TensorFlow.js生态。是以我們将介紹Github中tensorflow命名空間組織的tfjs*開頭的所有倉庫。
主要的TensorFlow.js倉庫包括Core和Layer API代碼,用在浏覽器和Node.js。該倉庫也有Core和Layer API的問題跟蹤。
3.1.8.1 Core API(tfjs-core)
Core API實作CPU和GPU的底層張量函數功能,比如,卷積和padding操作、邏輯操作和數學操作,也有優化器代碼。Core API原來開發時的名字是Deeplearn.js。
3.1.8.2 Layers API (tfjs-layers)
Layers API (
https://github.com/tensorflow/tfjs-layers) 是抽象的神經網絡API,類似Keras。該倉庫包含官方API文檔所列的tf.layer.*方法的實作,以及Model的實作。
許多選項、參數和内部狀态的實作和Keras相同。是以,有時比Core API配置參數(卷積操作的padding選項)少很多。Layer API設計為操作一個batch,而不是一個張量。是以,層輸入和輸出的第一維必須定義為batch大小。
下面是一個使用Layer API建構神經網絡的例子。
3.1.8.3 Node.js API (tfjs-node)
Node.js API倉庫包含TensorFlow後端綁定,用來在TensorFlow上運作Node編寫的TensorFlow.js代碼。這些功能還不是用于正式生産環境,然而,這表明TensorFlow的 JavaScript API正在變成一個通用的解決方案。
3.1.8.4 轉換預訓練模型 (tfjs-converter)
TensorFlow.js Converter倉庫實作轉換預訓練的Keras模型,當機執行圖,形成TensorFlow.js 格式。轉換器将操作和參數映射成TensorFlow.js 中間狀态和優化參數塊以達到高效的推斷。
下面的例子是轉換Keras模型的例子。
3.1.8.5 TensorFlow.js模型庫(tfjs-models)
Google提供各種TensorFlow.js格式的模型,比如SqueezeNet、MobileNet、PoseNet等。TensorFlow.js Models倉庫管理着這些模型的代碼。這些模型可以用NPM安裝,也可以直接用CDN直接加載。
下面的代碼是從unpkg.com的cdn中加載MobileNet模型。
你可以在浏覽器或者Node.js中使用導入的模型。
我們讨論了TensorFlow.js周邊的大部分核心概念。官方文檔中有全部的API和一些示例。在這本書的結尾,我們展示了一個用TensorFlow.js建構的實際的應用。你可以用這個應用作為你的應用程式的起點。接着讓我們介紹WebDNN。
3.2 WebDNN
WebDNN是運作在浏覽器上的另外一個深度學習架構。它主要是由東京大學的機器智能實驗室開發的。雖然它沒有TensorFlow.js那麼流行,但是它支援更多種類的深度學習架構,如下:
- TensorFlow
- Keras
- PyTorch
- Chainer
- Cafie
如果你已經有了這些深度學習架構的模型,你可以用WebDNN很容易地導入這些模型。深度學習架構壓縮的訓練模型運作在Web浏覽器上。WebDNN擁有一個優化器管道,它看似一個編譯器。它将一個訓練模型轉換為一個WebDNN的中間表示(intermediate representation ,IR)的格式。在WebDNN優化 IR編寫的中間表達之後,最後優化過的模型生成一個核操作圖。圖3-4顯示一個模型定義如何編譯為WebDNN。
WebDNN,類似TensorFlow.js,也能通過WebGL 的硬體加速提升。優化模型的推斷不僅可以運作在 WebGL上,也可以運作在WebAssembly和WebGPU 上。如果你的浏覽器支援這些API,神經網絡模型可以用各種方法加速。大部分現代浏覽器已經支援WebGL和WebAssembly。是以,你能毫無煩惱地使用這種加速。
TensorFlow.js和WebDNN的一個最主要的不同是WebDNN隻支援任務的推斷階段,而不能用在訓練階段。是以,我們需要導入一個預訓練的模型到WebDNN,是以你必須熟悉前面介紹的一些深度學習架構。可以把WenDNN看作一個優化器,它能重寫預訓練的模型以在Web浏覽器上運作得更快。
讓我們使用WebDNN導入一個SqueezeNet預訓練的模型。SqueezeNet模型是一個深度學習模型,它匹敵AlexNet模型,但是SqueezeNet模型可以用更小的記憶體。适合在移動裝置或者物聯網裝置上運作深度學習的案例。你可以用pip安裝webDNN。
首先,因為webDNN不像TensorFlow.js可以預訓練模型,它不支援訓練功能。
轉換已存儲的網絡模型為優化模型,就像前面在tfjs-converter中使用的。
上面生成的檔案可導入webDNN模型。
根據官方網站的對比,webDNN在WebGPU後端運作時可以運作得更快。WebGPU還不是W3C的一個标準化 API。是以不能期望每個現代浏覽器都支援該API。但是你可以看到WebGPU提高性能的潛力。
3.3 Keras.js
Keras.js是另外一個運作在Web浏覽器上的深度學習架構。Keras.js隻支援Keras生成的模型。但是記住Keras本身支援各種深度學習後端架構。雖然這裡不會深入介紹,這意味着Keras.js間接支援Keras支援的深度學習架構後端。
像TensorFlow.js一樣,Keras.js實作各種核函數,比如WebGL上運作的着色器程式。但是Keras.js同樣不支援訓練階段,是以你需要為 Keras.js準備預訓練模型來建立應用。它可以運作在獨立于主線程外的WebWorker。這會使Keras.js避免阻塞渲染UI。一個深度學習應用給使用者帶來好的使用者體驗至關重要。不幸的是,WebWorker不能通路主線程中的DOM元素,這會限制深度學習預測結果的運用。WebWorker更深層次的細節會在本書後續章節介紹。
Keras.js提供很多試用的例子。
你可以通過
http://localhost:3000和
https://transcranial.github.io/keras-js通路示例。
使用代碼倉庫中的encoder.py将預訓練模型轉換成Keras.js能夠識别的格式。
上面的腳本生成兩個檔案:model-weights.buf和model-metadata.json。除此之外, Keras生成的model.json也需要用來一起建立Keras.js模型。
如果你已經有了預訓練模型,那麼Keras.js和 WebDNN一樣讓你開發深度學習應用更容易。雖然 WebDNN似乎超過Keras.js了,但是Keras和Keras.js的開源社群開發
比WebDNN活躍。這兩個深度學習架構都依賴預訓練的模型。考慮到後續版本後端架構的模型格式的變化,你應該選擇開發活躍的架構應用到你的應用中。
3.4 本章小結
雖然TensorFlow.js對于浏覽器來說是最流行的深度學習實作架構,但是在本章我們也介紹了其他幾個架構。WebDNN的曆史比TensorFlow.js還長,它支援各種後端,比如WebAssembly和WebGPU。雖然這個特性不錯,但是運作在這些平台上的深度學習程式幾乎不存在。Keras.js也有挺長的曆史,與 TensorFlow.js相似。因為其支援更廣泛的模型類型,是以更容易和已有的資源整合。
重要的是記住哪種工具适合哪類問題。深度學習架構還相對年輕、不成熟,但是我們希望本章可以幫你選擇更好的深度學習架構。下一章将會介紹深度學習中需要的JavaScript基礎。