當前位置:
首頁 > 最新 > 使用 C#的深度神經網路 IO

使用 C#的深度神經網路 IO

機器學習的許多最新進展(如使用數據進行預測)已通過深度神經網路得到實現。例如,Microsoft Cortana 和 Apple Siri 中的語音識別,以及有助於實現無人駕駛汽車的圖像識別。

深度神經網路 (DNN) 為常規術語,還有多個變體,包括遞歸神經網路 (RNN) 和卷積神經網路 (CNN)。我在本文中介紹的 DNN 最基本形式沒有特殊名稱。因此,我將它稱為 DNN。

本文將介紹 DNN,因此最好有可以試驗的具體演示程序,這將有助於了解有關 DNN 的介紹。我不會提供可直接用於生產系統的代碼,但將提供可擴展以便創建此類系統的代碼(如後所述)。即使從未打算實現 DNN,或許也會對 DNN 的工作原理介紹本身感興趣。

直觀解釋 DNN 是最佳做法。請看一下圖1。深度網路在左側有兩個輸入節點,值分別為 1.0、2.0。在右側有三個輸出節點,值分別為 0.3269、0.3333、0.3398。可以將 DNN 看作是複雜的數學函數:通常接受兩個或多個數字輸入值,並返回一個或多個數字輸出值。

圖 1:基本深度神經網路

所示 DNN 對應於解決一個問題,旨在根據年齡和收入預測人員的政黨派別(「Democrat」、「Republican」或「Other」)。其中,輸入值以某種方式進行了縮放。如果「Democrat」編碼為 (1,0,0),「Republican」編碼為 (0,1,0),而「Other」編碼為 (0,0,1),那麼圖 1 中的 DNN 預測年齡值為 1.0 且收入值為 2.0 的人員的政黨派別為「Other」,因為最後一個輸出值 (0.3398) 最大。

常規神經網路有一個隱藏的節點處理層。DNN 有兩個或多個隱藏層,可以處理非常困難的預測問題。特殊化類型的 DNN(如 RNN 和 CNN)也有多個節點處理層,不同之處在於還有更複雜的連接體系結構。

圖 1 中的 DNN 有三個隱藏的節點處理層。第一個隱藏層有四個節點,第二個和第三個隱藏層有兩個節點。每個從左指向右的長箭頭都表示一個數字常量(稱為「權重」)。如果節點從圖 1 頂部開始以零為基數(即為 [0])開始編製索引,那麼將 input[0] 連接到 hidden[0][0](層 0 的節點 0)的權重值為 0.01,將 input[1] 連接到 hidden[0][3](層 0 的節點 3)的權重值為 0.08,依此類推。有 26 個節點到節點的權重值。

八個隱藏節點和三個輸出節點都有一個表示數字常量(稱為「偏差」)的小箭頭。例如,hidden[2][0] 的偏差值為0.33,output[1] 的偏差值為 0.36。圖中並未標註出所有的權重和偏差值,但由於值是 0.01 到 0.37 之間的順序數,因此可以很容易地就確定未標註的權重或偏差值。

在下面各部分中,我將介紹 DNN 輸入輸出機制的工作原理,以及如何實現此機制。雖然演示程序是使用 C# 進行編碼,但如果願意,可以將此代碼重構為其他語言(如 Python 或 JavaScript),應該不會遇到太多麻煩。演示程序因太長而無法在本文中全部展示,但可以在隨附的代碼下載內容中獲取整個程序。

1

演示程序

了解本文所述觀點的一個好方法是,研究圖 2 中的演示程序屏幕截圖。演示程序對應於圖 1 中展示的 DNN,並通過顯示網路中 13 個節點的值展示了輸入輸出機制。圖 3 展示了生成輸出的演示代碼的開頭。

圖 2:基本深度神經網路演示運行

圖 3:輸出生成代碼的開頭

usingSystem;namespaceDeepNetInputOutput{classDeepInputOutputProgram {staticvoidMain(string[] args) { Console.WriteLine("Begin deep net IO demo"); Console.WriteLine("Creating a 2-(4-2-2)-3 deep network");intnumInput = 2;int[] numHidden =newint[] { 4, 2, 2 };intnumOutput = 3; DeepNet dn =newDeepNet(numInput, numHidden, numOutput);

請注意,演示程序僅使用不含命名空間(System 除外)的純 C#。DNN 的創建方式為,將每層中的節點數量傳遞給 DeepNet 程序定義類構造函數。隱藏層數量 3 作為 numHidden 數組中的項數進行隱式傳遞。備用設計是顯式傳遞隱藏層數量。

26 個權重值和 11 個偏差值的設置如下:

intnw = DeepNet.NumWeights(numInput, numHidden, numOutput);Console.WriteLine("Setting weights and biases to 0.01 to "+ (nw/100.0).ToString("F2") );double[] wts =newdouble[nw];for(inti = 0; i < wts.Length; ++i) wts[i] = (i + 1) * 0.01; dn.SetWeights(wts);

權重和偏差的總數是使用靜態類方法 NumWeights 進行計算。如果回頭看看圖 1,可以發現,由於每個節點都連接到右側層中的所有節點,因此權重數量的計算公式為 (2*4) + (4*2) + (2*2) + (2*3) = 8 + 8 + 4 + 6 = 26。由於每個隱藏節點和輸出節點都有一個偏差值,因此偏差總數的計算公式為 4 + 2 + 2 + 3 = 11。

名為 wts 的數組實例化為 37 個單元,再將值設置為介於 0.01 到 0.37 之間。這些值使用 SetWeights 方法插入 DeepNet 對象。在非演示的實際 DNN 中,權重和偏差值是根據一組包含已知輸入值和已知正確輸出值的數據進行確定。此過程稱為「網路定型」。最常見的定型演算法稱為「反向傳播」。

演示程序的 Main 方法代碼結尾如下:

... Console.WriteLine("Computing output for [1.0, 2.0] ");double[] xValues =newdouble[] { 1.0, 2.0 }; dn.ComputeOutputs(xValues); dn.Dump(false); Console.WriteLine("End demo"); Console.ReadLine(); }// Main}// Class Program

ComputeOutputs 方法接受一組輸入值,再使用輸入輸出機制(我很快就會介紹)計算和存儲輸出節點值。Dump 幫助程序方法顯示 13 個節點值,「false」參數表示不顯示 37 個權重和偏差值。

2

輸入輸出機制

通過具體示例解釋 DNN 的輸入輸出機制是最佳做法。第一步是使用輸入節點值計算第一個隱藏層中的節點值。第一個隱藏層最頂部的隱藏節點值為:

tanh( (1.0)(0.01) + (2.0)(0.05) + 0.27 ) =

tanh(0.38) = 0.3627

用語言描述就是:「對每個輸入節點與其相關權重的乘積進行求和,加上偏差值,再求總和的雙曲正切值」。 雙曲正切函數(縮寫為 tanh)稱為「激活函數」。雙曲正切函數接受從負無窮大到正無窮大的任意值,返回介於 -1.0 到 +1.0 之間的值。重要的備用激活函數包括邏輯 Sigmoid 函數和線性整流 (ReLU) 函數,這兩個函數都不在本文的介紹範圍以內。

剩餘隱藏層中節點值的計算方式完全相同。例如,hidden[1][0] 的節點值計算公式為:

tanh( (0.3627)(0.09) + (0.3969)(0.11) + (0.4301)(0.13) + (0.4621)(0.15) + 0.31 ) =

tanh(0.5115) = 0.4711

hidden[2][0] 的節點值計算公式為:

tanh( (0.4711)(0.17) + (0.4915)(0.19) + 0.33 ) =

tanh(0.5035) = 0.4649

輸出節點值是使用不同的激活函數(即 softmax)進行計算。初步的預激活步驟都是一樣的,即用乘積總和加上偏差值:

預激活 output[0] =

(.4649)(0.21) + (0.4801)(0.24) + 0.35 =

0.5628

預激活 output[1] =

(.4649)(0.22) + (0.4801)(0.25) + 0.36 =

0.5823

預激活 output[2] =

(.4649)(0.23) + (0.4801)(0.26) + 0.37 =

0.6017

對三個任意值 x、y、z 使用 softmax:

softmax(x) = e^x / (e^x + e^y + e^z)

softmax(y) = e^y / (e^x + e^y + e^z)

softmax(z) = e^z / (e^x + e^y + e^z)

其中,e 是歐拉數,約為 2.718282。因此,圖 1 中 DNN 的最終輸出值為:

output[0] = e^0.5628 / (e^0.5628 + e^0.5823 + e^0.6017) = 0.3269

output[1] = e^0.5823 / (e^0.5628 + e^0.5823 + e^0.6017) = 0.3333

output[2] = e^0.6017 / (e^0.5628 + e^0.5823 + e^0.6017) = 0.3398

softmax 激活函數旨在將輸出值總和強制限制為 1.0,這樣就可以將它們解讀為概率並映射到分類值。在此例中,因為第三個輸出值最大,所以編碼為 (0,0,1) 的分類值就是 inputs = (1.0, 2.0) 的預測類別。

3

實現 DeepNet 類

為了創建演示程序,我啟動了 Visual Studio,並選擇了 C# 控制台應用程序模板,同時將它命名為 DeepNetInputOutput。我使用的是 Visual Studio 2015,但由於演示程序並不嚴重依賴 .NET,因此可以使用任意一版 Visual Studio。

在模板代碼載入後,我在「解決方案資源管理器」窗口中右鍵單擊了文件 Program.cs,並將它重命名為更具描述性的名稱(即 DeepNetInputOutputProgram.cs),同時允許 Visual Studio 自動為我重命名類 Program。在編輯器窗口頂部,我刪除了所有不必要的 using 語句,僅留下引用 System 命名空間的語句。

我將演示 DNN 作為 DeepNet 類進行實現。類定義代碼的開頭為:

publicclassDeepNet{publicstaticRandom rnd;publicintnInput;publicint[] nHidden;publicintnOutput;publicintnLayers; ...

為了簡化實現,所有類成員都是通過公共作用域進行聲明。DeepNet 類使用名為 rnd 的靜態隨機對象成員,將權重和偏差初始化為隨機小值(再用介於 0.01 到 0.37 之間的值覆蓋)。成員 nInput 和 nOuput 包含輸入和輸出節點的數量。數組成員 hHidden 包含每個隱藏層中的節點數量,因此隱藏層數量是由數組的 Length 屬性提供(為方便起見,此數量存儲在成員 nLayers 中)。類定義代碼接下來為:

publicdouble[] iNodes;publicdouble[][] hNodes;publicdouble[] oNodes;

深度神經網路實現有多個設計選項。與預期一樣,數組成員 iNodes 和 oNodes 包含輸入和輸出值。二維數組成員 hNodes 包含隱藏節點值。備用設計是,將所有節點存儲在一個二維數組結構 nnNodes 中。在演示網路中,nnNodes[0] 是輸入節點值數組,nnNodes[4] 是輸出節點值數組。

節點到節點權重使用以下數據結構進行存儲:

publicdouble[][] ihWeights;publicdouble[][][] hhWeights;publicdouble[][] hoWeights;

成員 ihWeights 是一個二維數組式矩陣,用於包含輸入節點到第一層隱藏節點的權重。成員 hoWeights 是一個二維數組式矩陣,用於包含最後一層隱藏節點到輸出節點的權重。成員 hhWeights 是一個數組,其中每個單元都指向一個包含隱藏層節點到隱藏層節點權重的二維數組式矩陣。例如,hhWeights[0][3][1] 保留隱藏層 [0] 中隱藏節點 [3] 到隱藏層 [0+1] 中隱藏節點 [1] 的權重。這些數據結構是 DNN 輸入輸出機制的核心,同時也是學習難點。圖 4 就是這些成員的概念圖。

圖 4:權重和偏差數據結構

最後兩個類成員包含隱藏節點偏差和輸出節點偏差:

publicdouble[][] hBiases;publicdouble[] oBiases;

與我使用過的其他任何軟體系統一樣,DNN 有許多備用數據結構設計。編寫輸入輸出代碼時,請務必繪製這些數據結構的草圖。

4

計算權重和偏差的數量

若要設置權重和偏差值,必須先知道有多少個權重和偏差。演示程序實現靜態方法 NumWeights,以計算並返回此數量。回顧一下,2-(4-2-2)-3 演示網路的權重數量為 (2*4) + (4*2) + (2*2) + (2*3) = 26,偏差數量為 4 + 2 + 2 + 3 = 11。方法 NumWeights 中的關鍵代碼如下,可計算輸入節點到隱藏層節點的權重數量、隱藏層節點到隱藏層節點的權重數量,以及隱藏層節點到輸出節點的權重數量:

intihWts = numInput * numHidden[0];inthhWts = 0;for(intj = 0; j < numHidden.Length - 1; ++j) {introws = numHidden[j];intcols = numHidden[j + 1]; hhWts += rows * cols;}inthoWts = numHidden[numHidden.Length - 1] * numOutput;

建議在一個兩單元式整數數組中單獨返回權重和偏差的數量,而不是像 NumWeights 方法一樣返回權重和偏差的總數。

5

設置權重和偏差

非演示 DNN 通常將所有權重和偏差初始化為隨機小值。演示程序使用類方法 SetWeights,將 26 個權重值設置為介於 0.01 至 0.26 之間,將偏差值設置為介於 0.27 至 0.37 之間。定義代碼的開頭如下:

publicvoidSetWeights(double[] wts){intnw = NumWeights(this.nInput,this.nHidden,this.nOutput);if(wts.Length != nw)thrownewException("Bad wts[] length in SetWeights()");intptr = 0;...

輸入參數 wts 包含權重和偏差值,並假定具有正確的 Length。變數 ptr 指向 wts 數組。演示程序幾乎不進行錯誤檢查,以儘可能確保主題明確。輸入節點到第一層隱藏節點的權重設置如下:

for(inti = 0; i < nInput; ++i)for(intj = 0; j < hNodes[0].Length; ++j) ihWeights[i][j] = wts[ptr++];

接下來,隱藏層節點到隱藏層節點的權重設置如下:

for(inth = 0; h < nLayers - 1; ++h)for(intj = 0; j < nHidden[h]; ++j)// Fromfor(intjj = 0; jj < nHidden[h+1]; ++jj)// TohhWeights[h][j][jj] = wts[ptr++];

如果不習慣使用多維數組,可能會覺得索引非常棘手。請務必繪製權重和偏差數據結構圖(我也仍需要這樣做)。最後一層隱藏節點到輸出節點的權重設置如下:

inthi =this.nLayers - 1;for(intj = 0; j

此代碼的依據為,如果有 nLayers 個隱藏層(演示網路中為 3 個),那麼最後一個隱藏層的索引為 nLayers-1。方法 SetWeights 最後設置隱藏節點偏差和輸出節點偏差:

...for(inth = 0; h < nLayers; ++h)for(intj = 0; j

6

計算輸出值

類方法 ComputeOutputs 定義代碼的開頭如下:

publicdouble[] ComputeOutputs(double[] xValues){for(inti = 0; i < nInput; ++i) iNodes[i] = xValues[i];...

數組參數 xValues 包含輸入值。類成員 nInput 包含輸入節點的數量,此成員在類構造函數中進行設置。由於將 xValues 中的前 nInput 個值複製到輸入節點中,因此假定 xValues 在首批單元中至少有 nInput 個值。接下來,將隱藏節點和輸出節點中的當前值清零:

for(inth = 0; h < nLayers; ++h)for(intj = 0; j < nHidden[h]; ++j) hNodes[h][j] = 0.0;for(intk = 0; k < nOutput; ++k) oNodes[k] = 0.0;

這樣做是為了將乘積項的總和直接匯總到隱藏節點和輸出節點,所以對於每個方法調用,都必須將這些節點顯式重置為 0.0。備用方法是聲明和使用包含 hSums[][] 和 oSums[] 等名稱的本地數組。接下來,計算第一個隱藏層中的節點值:

for(intj = 0; j < nHidden[0]; ++j) {for(inti = 0; i < nInput; ++i) hNodes[0][j] += ihWeights[i][j] * iNodes[i]; hNodes[0][j] += hBiases[0][j];// Add the biashNodes[0][j] = Math.Tanh(hNodes[0][j]);// Activation}

此代碼幾乎就是前述機制的一一映射。內置的 Math.Tanh 用於激活隱藏節點。正如之前所提到的,重要的備用函數包括邏輯 Sigmoid 函數和線性整流 (ReLU) 函數,我將在以後的文章中介紹這兩個函數。接下來,計算剩餘的隱藏層節點:

for(inth = 1; h < nLayers; ++h) {for(intj = 0; j < nHidden[h]; ++j) {for(intjj = 0; jj < nHidden[h-1]; ++jj) hNodes[h][j] += hhWeights[h-1][jj][j] * hNodes[h-1][jj]; hNodes[h][j] += hBiases[h][j]; hNodes[h][j] = Math.Tanh(hNodes[h][j]); }}

這是演示程序最為棘手的部分,主要是由於需要多個數組索引。接下來,計算輸出節點的預激活乘積總和:

for(intk = 0; k < nOutput; ++k) {for(intj = 0; j < nHidden[nLayers - 1]; ++j) oNodes[k] += hoWeights[j][k] * hNodes[nLayers - 1][j]; oNodes[k] += oBiases[k];// Add bias}

方法 ComputeOutputs 最後應用 softmax 激活函數,在單獨的數組中返回計算得出的輸出值:

...double[] retResult = Softmax(oNodes);for(intk = 0; k < nOutput; ++k) oNodes[k] = retResult[k];returnretResult; }

Softmax 方法是靜態幫助程序。有關詳情,請參閱隨附的代碼下載內容。請注意,由於 softmax 激活需要將進行激活的所有值(在分母項中),因此一次性計算所有 softmax 值更有效,而不要單獨進行計算。最終的輸出值存儲到輸出節點中,並且也會單獨返回,以便進行調用。

7

總結

在過去幾年裡,開展了大量與深度神經網路相關的研究活動,並取得了許多突破。卷積神經網路、遞歸神經網路、LSTM 神經網路和殘差神經網路等特殊化 DNN 的功能非常強大,但也十分複雜。我認為,了解基本 DNN 的工作原理是掌握更複雜變體的關鍵所在。

在以後的文章中,我將詳細介紹如何使用反向傳播演算法(可以說是機器學習中最著名也是最重要的演算法)定型基本 DNN。反向傳播或此演算法的至少一些形式也用於定型大多數的 DNN 變體。在介紹過程中,我將引入梯度消失概念,這反過來又可以解釋現在用於非常複雜預測系統的許多 DNN 的設計和動機。

衷心感謝以下 Microsoft 技術專家對本文的審閱:Li Deng、Pingjun Hu、Po-Sen Huang、Kirk Li、Alan Liu、Ricky Loynd、Baochen Sun、Henrik Turbell。

喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 微軟中國MSDN 的精彩文章:

TAG:微軟中國MSDN |