1. 程式人生 > >【一統江湖的大前端(9)】TensorFlow.js 開箱即用的深度學習工具

【一統江湖的大前端(9)】TensorFlow.js 開箱即用的深度學習工具

示例程式碼託管在:http://www.github.com/dashnowords/blogs

部落格園地址:《大史住在大前端》原創博文目錄

目錄
  • 一. 上手TensorFlow.js
  • 二. 使用TensorFlow.js構建卷積神經網路
    • 卷積神經網路
    • 搭建LeNet-5模型
  • 三. 基於遷移學習的語音指令識別
  • 推薦課程

TensorFlow是Google推出的開源機器學習框架,並針對瀏覽器、移動端、IOT裝置及大型生產環境均提供了相應的擴充套件解決方案,TensorFlow.js就是JavaScript語言版本的擴充套件,在它的支援下,前端開發者就可以直接在瀏覽器環境中來實現深度學習的功能,嘗試過配置環境的讀者都知道這意味著什麼。瀏覽器環境在構建互動型應用方面有著天然優勢,而端側機器學習不僅可以分擔部分雲端的計算壓力,也具有更好的隱私性,同時還可以藉助Node.js在服務端繼續使用JavaScript進行開發,這對於前端開發者而言非常友好。除了提供統一風格的術語和API,TensorFlow的不同擴充套件版本之間還可以通過遷移學習來實現模型的複用(許多知名的深度學習模型都可以找到python版本的原始碼),或者在預訓練模型的基礎上來定製自己的深度神經網路,為了能夠讓開發者儘快熟悉相關知識,TensorFlow官方網站還提供了一系列有關JavaScript版本的教程、使用指南以及開箱即用的預訓練模型,它們都可以幫助你更好地瞭解深度學習的相關知識。對深度學習感興趣的讀者推薦閱讀美國量子物理學家Michael Nielsen編寫的《神經網路與深度學習》(英文原版名為《Neural Networks and Deep Learning》),它對於深度學習基本過程和原理的講解非常清晰。

一. 上手TensorFlow.js

Tensor(張量)是TensorFlow中的基本資料結構,它是向量和矩陣向更高維度的推廣,從程式設計的角度來看,它的核心資料不過就是多維陣列。或許你還記得在【帶著canvas去流浪(9)】粒子動畫一文中為了方便向量計算而定義的二維向量類Vector2,事實上它就可以被看作是Tensor在二維空間的簡化形式。Tensor資料型別可以很方便地構造各種維度的張量,支援切片、變形、合併分割等結構操作,同時也定義了各類線性代數運算的操作符,這樣做的好處是可以將開發者在應用層編寫的程式和不同平臺的底層實現之間解耦。這樣,神經網路中的資訊傳遞就通過張量(Tensor)的流動(Flow)表現出來了。在2018年Google I/O大會上,TensorFlow.js小組的工程師就介紹了該框架分層的結構設計,除了最底層為了解決程式語言和平臺差異的層次外,為了對不同的工作性質的開發者實現更好地支援,TensorFlow.js在應用層還提供了兩種不同的API:高階API被稱為Keras API(Keras是一個python編寫的開源人工神經網路庫)或Layer API,用於快速實現深度學習模型的構建、訓練、評估和應用,軟體和應用開發者大多情況下會使用它;低階API也被稱為Core API,通常用於支援研究人員對神經網路實現更底層的細節定製,使用起來難度也更高。

TensorFlow.js的工作依然是圍繞神經網路展開的,基本的工作過程包含了如下幾個典型步驟:

下面我們將通過TensorFlow.js官方網站提供的資料擬合的示例來了解整個流程。

Define階段是使用TensorFlow.js的第一步,這個階段中需要初始化神經網路模型,你可以在TensorFlow的tf.layers物件上找到具備各種功能和特徵的隱藏層,通過模型例項的add方法將其逐層新增到神經網路中,從而實現張量變形處理、卷積神經網路、迴圈神經網路等複雜模型,當內建模型無法滿足需求時,還可以自定義模型層,TensorFlow的高階API可以幫助開發者以宣告式的編碼來完成神經網路的結構搭建,示例程式碼如下:

/*建立模型*/
function createModel() {
   const model = tf.sequential(); 
   model.add(tf.layers.dense({inputShape: [1], units: 1, useBias: true}));
   model.add(tf.layers.dense({units: 1, useBias: true}));
   return model;
}

Compile階段需要對訓練過程進行一些引數預設,你可以先溫習一下上一章中介紹過的BP神經網路的工作過程,然後再來理解下面的示例程式碼:

model.compile({
   optimizer: tf.train.adam(),
   loss: tf.losses.meanSquaredError,
   metrics: ['mse'],
});

loss(損失)用於定義損失函式,它是神經網路的實際輸出和期望輸出之間偏差的量化評估標準,最常用的損失函式就是均方差損失(tf.losses.meanSquaredError),其他損失函式可以在TensorFlow的API文件中進行檢視;optimizer(優化器)是指誤差反向傳播結束後,神經網路進行權重調整時所使用的的演算法。權重調整的目的就是為了使損失函式達到極小值,所以通常採用“梯度下降”的思想來進行逼近,梯度方向是指函式在某一點變化最顯著的方向,但實際的情況往往並沒有這麼簡單,假設下圖是一個神經網路的損失函式曲線:

可以看到損失函式的形態、初始引數的位置以及優化過程的步長等都可能對訓練過程和訓練結果產生影響,這就需要在optimizer配置項中指定優化演算法來達到較好的訓練效果;metrics配置項用於指定模型的度量指標,大多數情況下可以直接使用損失函式來作為度量標準。

Fit階段執行的是模型訓練的工作(fit本身是擬合的意思),通過呼叫模型的fit方法就可以啟動訓練迴圈,官方示例程式碼如下(fit方法接收的引數分別為輸入張量集、輸出張量集和配置引數):

const batchSize = 32;
const epochs = 50;

await model.fit(inputs, labels, {
   batchSize,
   epochs,
   shuffle: true,
   callbacks: tfvis.show.fitCallbacks(
      { name: 'Training Performance' },
      ['loss', 'mse'], 
      { height: 200, callbacks: ['onEpochEnd'] }
   )
});

相關引數說明如下(其他引數可參考官方開發文件):

  • batchSize(批大小)指每個迴圈中使用的樣本數,通常取值為32~512

  • epochs指定整個訓練集上的資料的總迴圈次數

  • shuffle指是否在每個epochs中打亂訓練樣本的次序

  • callbacks指定了訓練過程中的回撥函式

神經網路的訓練是迴圈進行的,假設總訓練樣本大小為320個,那麼上面的示例程式碼所描述的訓練過程是:先使用下標為031的樣本來訓練神經網路,然後使用optimizer來更新一次權重,再使用下標為3263的樣本進行訓練,再更新權重,直到總樣本中所有資料均被使用過一次,上述過程被稱為一個epoch,接著打亂整個訓練樣本的次序,再重複共計50輪,callbacks回撥函式引數直接關聯了tfvis庫,它是TensorFlow提供的專用視覺化工具模組。

Evaluate階段需要對模型的訓練結果進行評估,呼叫模型例項的evaluate方法就可以使用測試資料來獲得損失函式和度量標準的數值。你可能已經注意到TensorFlow在定製訓練過程時更加關注如何使用樣本資料,而並沒有將“度量指標小於給定閾值”作為訓練終止的條件(例如brain.js中就可以通過設定errorthresh引數),在複雜神經網路的構建和設計中,開發者很可能需要一邊構建一邊進行非正式的訓練測試,度量指標最終並不一定能夠降低到給定的閾值以下,以此作為訓練終止條件很可能會使訓練過程陷入無限迴圈,所以使用固定的訓練次數配合視覺化工具來觀察訓練過程就更為合理。

Predict階段是使用神經網路模型進行預測的階段,這也是前端工程師參與度最高的部分,畢竟模型輸出的結果只是資料,如何利用這些預測結果來製作一些更有趣或者更加智慧化的應用或許才是前端工程師更應該關注的問題。從前文的過程中不難看出,TensorFlow.js提供的能力是圍繞神經網路模型展開的,應用層很難直接使用,開發者通常都需要藉助官方模型倉庫中提供的預訓練模型或者使用其他基於TensorFlow.js構建的第三方應用,例如人臉識別框架face-api.js(它可以在瀏覽器端和Node.js中實現快速的人臉追蹤和身份識別),語義化更加明確的機器學習框架ml5.js(可以直接呼叫API來實現影象分類、姿勢估計、人物摳圖、風格遷移、物體識別等更加具體的任務),可以實現手部跟蹤的handtrack.js等等,如果TensorFlow的相關知識讓你覺得過於晦澀,也可以先嚐試使用這些更高層的框架來構建一些有趣的程式。

二. 使用TensorFlow.js構建卷積神經網路

卷積神經網路

卷積神經網路(Convolutional Neural Networks,簡稱CNN)是計算視覺領域應用非常廣泛的深度學習模型,它在處理圖片或其他具有網格狀特徵的資料時具有非常好的表現。在資訊處理時,卷積神經網路會先保持畫素的行列空間結構,通過多個數學計算層來進行特徵提取,然後再將訊號轉換為特徵向量將其接入傳統神經網路的結構中,經過特徵提取的影象所對應的特徵向量在提供給傳統神經網路時體積更小,需要訓練的引數數量也會相應減少。卷積神經網路的基本工作原理圖如下(圖中各個層的數量並不是固定的):

為了搞清楚卷積網路的工作流程,需要先了解卷積池化這兩個術語的含義。

卷積層需要對輸入資訊進行卷積計算,它使用一個網格狀的視窗區(也被稱為卷積核或過濾器)對輸入影象進行遍歷加工,過濾器的每個視窗單元通常都具有自己的權重,從輸入影象的左上角開始,將權重和視窗覆蓋區域的數值相乘並累加後得到一個新的結果,這個結果就是該區域對映後的值,接著將過濾器視窗向右滑動固定的距離(通常為1個畫素),然後重複前面的過程,當過濾器視窗的右側和輸入影象的右邊界重合後,視窗向下移動同樣的距離,再次從左向右重複前面的過程,直到所有的區域遍歷完成後就可以得到新的行列資料。每將一個不同的過濾器應用於輸入影象後,卷積層就會增加一個輸出,真實的深度網路中可能會使用多個過濾器,所以在卷積神經網路的原理圖中通常會看到卷積層有多個層疊的影象。不難計算,對於一個輸入尺寸為MM的影象,使用NN的過濾器處理後,新影象的單邊尺寸為M-N+1。例如一個輸入尺寸是88的灰度圖,使用33過濾器對其進行卷積計算後,就會得到一個6*6的新圖片,如下圖所示:

不同的過濾器可以識別出影象中不同的微小特徵,例如上圖中的過濾器,對於一個33大小的純色區域,卷積計算的結果均為0,假設現在有一個上白下黑的邊界,那麼過濾器中上側的計算結果會非常小,而中間一行和下面一行的結果都接近0,卷積計算的累加結果也會對映為一個很小的負數,相當於過濾器將一個33區域內的典型特徵記錄在1個畫素中,也就達到了特徵提取的目的,很明顯,如果將上面的過濾器旋轉90°,就可以用來識別影象中的垂直邊界。由於卷積計算會將一個區域內的特徵縮小到一個點上,所以卷積層的輸出資訊也被稱為特徵對映圖。本章的程式碼倉中筆者基於canvas實現了一個簡單的卷積計算程式,你可以在原始碼中修改過濾器的引數來觀察處理後的影象,這就好像是在給圖片新增各種有趣的濾鏡一樣:

上圖分別展示了水平邊緣檢測、垂直邊緣檢測和斜線邊緣檢測處理後的效果。

再來看看池化層(也被稱為混合層、合併層或下采樣層),它通常緊接著卷積層之後來使用。影象中相鄰畫素的值通常比較接近,這會導致卷積層輸出結果的產生大量資訊冗餘,比如一個水平邊緣在卷積層中周圍的畫素可能也檢測到了水平邊緣,但事實上它們表示的是原圖中的同一個特徵,池化層的目的是就是簡化卷積層的輸出資訊,它輸出的每個單元可以被認為概括了前一層中一個區域的特徵,常用的最大池化層就是在區域內選取一個最大值來作為整個區域在池化層的對映(這並不是唯一的池化計算方法),假設前文示例中的66的卷積層輸出後緊接著一個使用22大小的視窗來進行區域對映的最大池化層,那麼最終將得到一個3*3的影象輸出,過程如下圖所示:

可以看到,在不考慮深度影響時,示例中8*8的輸入影象經過卷積層和池化層的處理後已經變成3*3大小了,對於後續的全連線神經網路而言,輸入特徵的數量已經大幅減少了。本章程式碼倉庫中也提供了經過“卷積層+最大池化層”處理後圖像變化的視覺化示例,直觀效果其實就是圖片縮放,可以看到縮放後的圖片仍然保持了池化前的典型特徵:

在對複雜畫面進行分析時,“卷積+池化”的模式可能會在網路中進行多次串聯,以便可以從影象中逐級提取特徵。在實際開發過程中,為了解決具體的計算視覺問題,開發者很可能需要自己去查閱相關學術論文並搭建相關的深度學習網路,它們通常使用非常簡潔的符號來表示,下一節中我們將以經典的LeNet-5模型為例來學習相關的知識。

搭建LeNet-5模型

LeNet-5是一種高效的卷積神經網路模型,幾乎在所有以MNIST手寫數字影象識別為例的教程中都會介紹它,LeNet-5是論文《Gradient-Based Learning Applied to Document Recognition》中提出的,論文中給出的結構示意圖如下:

可以看到模型中一共有7層,其含義和相關解釋如下表所示:

序號 類別 標記 細節
/ 輸入層 INPUT 32X32 輸入為32x32畫素的圖片
C1 卷積層 C1:feature maps 6@28x28 卷積層,輸出特徵圖共6個,每個尺寸為28x28(卷積核尺寸為5x5)
S2 池化層 S2:f.maps6@14x14 池化層,對前一層的輸出進行降取樣,輸出特徵對映圖共6個,每個尺寸14x14(降取樣視窗尺寸為2x2)
C3 卷積層 C3:f.maps16@10x10 卷積層,輸出特徵圖共16個,每個尺寸為10x10(卷積核尺寸為5x5)
S4 池化層 S4:f.maps16@5x5 池化層,對前一層的輸出進行降取樣,輸出特徵對映圖共16個,每個尺寸5x5(降取樣視窗尺寸為2x2)
C5 卷積層 C5:layer 120 卷積層,輸出特徵圖共120個,每個尺寸為1x1(卷積核尺寸為5x5)
F6 全連線層 F6:layer 84 全連線層,使用84個神經元
/ 輸出層 OUTPUT 10 輸出層,10個節點,代表0~9共10個數字

在完成類似的圖片分類任務時,構建的卷積神經網路並不需要完全與LeNet-5模型保持完全一致,只需要根據實際需求對它進行微調或擴充套件即可,例如在TensorFlow.js官方的“利用CNN識別手寫數字”教程中,就在C1層使用了8個卷積核,並去掉了整個F6全連線層,即便這樣依然能夠獲得不錯的識別率。TensorFlow.js提供的layers API可以很方便地生成定製的卷積層和池化層,示例程式碼如下:

model = tf.sequential();

//新增LeNet-5中的 C1層
model.add(tf.layers.conv2d({
   inputShape: [32, 32, 1],//輸入張量的形狀
   kernelSize: 5, //卷積核尺寸
   filters: 6, //卷積核數量
   strides: 1, //卷積核移動步長
   activation: 'relu', //啟用函式
   kernelInitializer: 'varianceScaling' //卷積核權重初始化方式
}));

//生成LeNet-5中的 S2層
model.add(tf.layers.maxPooling2d({
   poolSize: [2, 2],//滑動視窗尺寸
   strides: [2, 2]//滑動視窗移動步長
}));

官方教程提供的示例程式碼使用tfjs-vis庫對訓練過程進行了視覺化,你可以很清楚地看到神經網路的結構、訓練過程中度量指標的變化以及測試資料的預測結果彙總等資訊:

三. 基於遷移學習的語音指令識別

複雜的深度學習模型通常具有上百萬的引數,即便能夠重新搭建起整個神經網路,中小型開發者也沒有足夠的資料和機器資源來從頭訓練它,這就需要開發者將已經在相關任務中訓練過的模型複用到新的模型中,從而降低深度學習模型搭建和訓練的天然門檻,讓更多的應用層開發這可以參與進來。

遷移學習是指一個使用資料集A完成訓練的模型,被用於解決和另一個數據集B相關的任務,這通常需要對模型進行一些調整並使用資料集B重新訓練它。幸運的是,有了A資料集訓練結果的基礎,重新訓練模型時需要的新樣本數和訓練的時間都會大幅減少。調整預訓練模型的基本方法是將它的輸出層替換為自己需要的形式,而保留其他特徵提取網路的部分,對於同類型的任務而言,被保留的部分依然可以完成特徵提取的任務,並對類似的訊號進行分類,但如果資料集A和資料集B的特徵差異過大,新的模型仍有可能無法達到期望的效果,就需要對預訓練模型進行更多的定製和改造(比如調整卷積神經網路中的卷積層和池化層的數量或引數),相關的理論和方法本章中不再展開。TensorFlow.js官方提供了的預訓練模型可以實現影象分類、物件檢測、姿勢估計、面部追蹤、文字惡意檢測、句子編碼、語音指令識別等等非常豐富的功能,本節中就以“語音指令識別”功能為例來了解遷移學習相關的技術。

TensorFlow.js官方語音識別模型speech-commands每次可以針對長度為1秒的音訊片段進行分類,它已經使用近5萬個聲音樣本進行過訓練,直接使用時可以識別英文發音的數字(如zero ~ nine)、方向(up,down,right,left)和一些簡單指令(如yes,no等),在這個預訓練模型的基礎上,只要通過少量的新樣本就可以將它改造為一箇中文指令識別器,是不是很方便?一段音訊訊號在處理時,會先通過快速傅立葉變換將其轉換為頻域訊號,然後提取特徵將其送入深度學習網路進行分析,對於簡易指令的使用場景而言,只需要對若干個聲音指令進行分類就可以了,並不需要計算機進行語種或真實語義分析,所以一個英文指令識別器才可以方便地改造為中文指令識別工具。語音指令功能的本質是對短語音進行分類,例如訓練中將“向左”的聲音片段標記為“右”,訓練後的神經網路在聽到“向左”時就會將其歸類為“右”,使用預訓練模型speech-command實現遷移學習的基本步驟如下:

官方提供的擴充套件庫將具體的實現封裝起來,提供給開發者的應用層API已經非常易用,本章程式碼倉中提供了一個完整的示例,你可以通過採集自己的聲音樣本來生成中文指令,然後重新訓練遷移模型,並嘗試用它來控制《吃豆人》遊戲中的角色:

推薦課程

  • 李巨集毅 《深度學習》課程 (地址: http://speech.ee.ntu.edu.tw/~tlkagk/index.html)

  • 吳恩達 《機器學習》線上教程(地址: https://www.coursera.org/learn/machine-learning)

  • MIT 6.S191《深度學習導論》(地址:http://introtodeeplearning.com/)

  • Stanford CS231.n《卷積神經網路與計算視覺》 (地址http://cs231n.stanford.edu/)