一個深度學習的例子


主題介紹:使用 TensorFlow,可以將深度機器學習從一個研究領域轉變成一個主流的軟件工程方法。在這個視頻中,Martin Görner 演示了如何構建和訓練一個用於識別手寫數字的神經網絡。在這個過程中,他將描述一些在神經網絡設計中所使用的權衡技巧,最后他將使得其模型的識別准確度超過 99%。本教程的內容適用於各種水平的軟件開發者。即使是經驗豐富的機器學習愛好者,本視頻也能通過卷積網絡等已知的模型來帶你了解 TensorFlow。這是一個技術密集的視頻,是為想要快速上手機器學習的初學者而設計的。

第一部分

 

視頻封面
沒有phd玩深度學習1 - 騰訊視頻
視頻

 

 

本教程將以如何進行手寫數字識別為例進行講解。

首先,Gorner 給出了一個非常簡單的可以分類數字的模型:softmax 分類。對於一張 28×28 像素的數字圖像,其共有 784 個像素(MNIST 的情況)。將它們進行分類的最簡單的方法就是使用 784 個像素作為單層神經網絡的輸入。神經網絡中的每個「神經元」對其所有的輸入進行加權求和,並添加一個被稱為「偏置(bias)」的常數,然后通過一些非線性激活函數(softmax 是其中之一)來反饋結果。

為了將數字分為 10 類(0 到 9),需要設計一個具有 10 個輸出神經元的單層神經網絡。對於分類問題,softmax 是一個不錯的激活函數。通過取每個元素的指數,然后歸一化向量(使用任意的范數(norm,L1 或 L2),比如向量的普通歐幾里得距離)從而將 softmax 應用於向量。

那么為什么「softmax」會被稱為 softmax 呢?指數是一種驟增的函數。這將加大向量中每個元素的差異。它也會迅速地產生一個巨大的值。然后,當進行向量的標准化時,支配范數(norm)的最大的元素將會被標准化為一個接近 1 的數字,其他的元素將會被一個較大的值分割並被標准化為一個接近 0 的數字。所得到的向量清楚地顯示出了哪個是其最大的值,即「max」,但是卻又保留了其值的原始的相對排列順序,因此即為「soft」。

我們現在將使用矩陣乘法將這個單層的神經元的行為總結進一個簡單的公式當中。讓我們直接這樣做:100 個圖像的「mini-batch」作為輸入,產生 100 個預測(10 元素向量)作為輸出。

使用加權矩陣 W 的第一列權重,我們計算第一個圖像所有像素的加權和。該和對應於第一神經元。使用第二列權重,我們對第二個神經元進行同樣的操作,直到第 10 個神經元。然后,我們可以對剩余的 99 個圖像重復操作。如果我們把一個包含 100 個圖像的矩陣稱為 X,那么我們的 10 個神經元在這 100 張圖像上的加權和就是簡單的 X.W(矩陣乘法)。

每一個神經元都必須添加其偏置(一個常數)。因為我們有 10 個神經元,我們同樣擁有 10 個偏置常數。我們將這個 10 個值的向量稱為 b。它必須被添加到先前計算的矩陣中的每一行當中。使用一個稱為「broadcast」的魔法,我們將會用一個簡單的加號寫出它。

是 Python 和 numpy(Python 的科學計算庫)的一個標准技巧。它擴展了對不兼容維度的矩陣進行正常操作的方式。「Broadcasting add」意味着「如果你因為兩個矩陣維度不同的原因而不能將其相加,那么你可以根據需要嘗試復制一個小的矩陣使其工作。」

我們最終應用 softmax 激活函數並且得到一個描述單層神經網絡的公式,並將其應用於 100 張圖像:

在 TensorFlow 中則寫成這樣:

接下來我們需要訓練神經網絡來自己找到我們所需要的權重和偏置。

接下來,Gorner 介紹了如何對神經網絡進行訓練。

要讓神經網絡從輸入圖像中產生預測,我們需要知道它們可以做到什么樣的程度,即在我們知道的事實和網絡的預測之間到底有多大的距離。請記住,我們對於這個數據集中的所有圖像都有一個真實的標簽。

任何一種定義的距離都可以進行這樣的操作,普通歐幾里得距離是可以的,但是對於分類問題,被稱為「交叉熵(cross-entropy)」的距離更加有效。交叉熵是一個關於權重、偏置、訓練圖像的像素和其已知標簽的函數。

這里用到了 one-hot 編碼。「one-hot」編碼意味着你使用一個 10 個值的向量,其中除了第 6 個值為 1 以外的所有值都是 0。這非常方便,因為這樣的格式和我們神經網絡預測輸出的格式非常相似,同時它也作為一個 10 值的向量。

在這里可視化演示了這個動態過程(參見視頻)。

  • 准確度(左上圖):這個准確度只是正確識別的數字的百分比,是在訓練和測試集上計算出的。如果訓練順利,它便會上升。

  • 交叉熵損失(中上圖):為了驅動訓練,需要定義損失函數,即一個展示出系統數字識別能力有多糟的值,並且系統會盡力將其最小化。損失函數(loss function,此處為「交叉熵」)的選擇稍后會做出解釋。你會看到,隨着訓練的進行,訓練和測試數據的損失會減少,而這個現象是好的,意味着神經網絡正在學習。X 軸表示了學習過程中的迭代。

  • 權重(左下圖)和偏置(中下圖):說明了內部變量所取的所有值的擴展,即隨訓練進行而變化的權重和偏置。比如偏置從 0 開始,且最終得到的值大致均勻地分布在-1.5 和 1.5 之間。如果系統不能很好地收斂,那么這些圖可能有用。倘若你發現權重和偏差擴展到上百或上千,那么就可能有問題了。

  • 訓練數字(右上圖):訓練數字每次 100 個被送入訓練回路;也可以看到當前訓練狀態下的神經網絡是已將數字正確識別(白色背景)還是誤分類(紅色背景,左側印有正確的標示,每個數字右側印有計算錯誤的標示)。此數據集中有 50,000 個訓練數字。我們在每次迭代(iteration)中將 100 個數字送入訓練循環中,因此系統將在 500 次迭代之后看到所有訓練數字一次。我們稱之為一個「epoch」。

  • 測試數字(右下圖):為了測試在現實條件下的識別質量,我們必須使用系統在訓練期間從未看過的數字。否則,它可能記住了所有的訓練數字,卻仍無法識別我剛才寫的「8」。MNIST 數據集包含了 10,000 個測試數字。此處你能看到每個數字對應的大約 1000 種書寫形式,其中所有錯誤識別的數字列在頂部(有紅色背景)。左邊的刻度會給你一個粗略的分辨率精確度(正確識別的百分比)。

「訓練」一個神經網絡實際上就是使用訓練圖像和標簽來調整權重和偏置,以便最小化交叉熵損失函數。

那么我們在 TensorFlow 中如何實現它呢?

我們首先定義 TensorFlow 的變量和占位符(placeholder),即權重和偏置。

 

占位符是在訓練期間填充實際數據的參數,通常是訓練圖像。持有訓練圖像的張量的形式是 [None, 28, 28, 1],其中的參數代表:

 

  • 28, 28, 1: 圖像是 28x28 每像素 x 1(灰度)。最后一個數字對於彩色圖像是 3 但在這里並非是必須的。

  • None: 這是代表圖像在小批量(mini-batch)中的數量。在訓練時可以得到。

接下來是定義模型:

第一行是我們單層神經網絡的模型。公式是我們在前面的理論部分建立的。tf.reshape 命令將我們的 28×28 的圖像轉化成 784 個像素的單向量。在 reshape 中的「-1」意味着「計算機,計算出來,這只有一種可能」。在實際當中,這會是圖像在小批次(mini-batch)中的數量。

然后,我們需要一個額外的占位符用於訓練標簽,這些標簽與訓練圖像一起被提供。

現在我們有了模型預測和正確的標簽,所以我們計算交叉熵。tf.reduce_sum 是對向量的所有元素求和。

最后兩行計算了正確識別數字的百分比。

才是 TensorFlow 發揮它力量的地方。你選擇一個適應器(optimiser,有許多可供選擇)並且用它最小化交叉熵損失。在這一步中,TensorFlow 計算相對於所有權重和所有偏置(梯度)的損失函數的偏導數。這是一個形式衍生(formal derivation),並非是一個耗時的數值型衍生。

 

梯度然后被用來更新權重和偏置。學習率為 0.003。

那么梯度和學習率是什么呢?

  • 梯度:如果我們相對於所有的權重和所有的偏置計算交叉熵的偏導數,我們就得到一個對於給定圖像、標簽和當前權重和偏置的「梯度」。請記住,我們有 7850 個權重和偏置,所以計算梯度需要大量的工作。幸運的是,TensorFlow 可以來幫我們做這項工作。梯度的數學意義在於它指向「上(up)」。因為我們想要到達一個交叉熵低的地方,那么我們就去向相反的方向。我們用一小部分的梯度更新權重和偏置並且使用下一批訓練圖像再次做同樣的事情。我們希望的是,這可以使我們到達交叉熵最小的凹點的低部。梯度下降算法遵循着一個最陡的坡度下降到局部最小值的路徑。訓練圖像在每一次迭代中同樣會被改變,這使得我們向着一個適用於所有圖像的局部最小值收斂。

  • 學習率(learning rate): 在整個梯度的長度上,你不能在每一次迭代的時候都對權重和偏置進行更新。這就會像是你穿着七里靴卻試圖到達一個山谷的底部。你會直接從山谷的一邊到達另一邊。為了到達底部,你需要一些更小的步伐,即只使用梯度的一部分,通常在 1/1000 區域中。我們稱這個部分為「學習率」。

接下來該運行訓練循環了。到目前為止,所有的 TensorFlow 指令都在內存中准備了一個計算圖,但是還未進行計算。

TensorFlow 的「延遲執行(deferred execution)」模型:TensorFlow 是為分布式計算構建的。它必須知道你要計算的是什么、你的執行圖(execution graph),然后才開始發送計算任務到各種計算機。這就是為什么它有一個延遲執行模型,你首先使用 TensorFlow 函數在內存中創造一個計算圖,然后啟動一個執行 Session 並且使用 Session.run 執行實際計算任務。在此時,圖無法被更改。

由於這個模型,TensorFlow 接管了分布式運算的大量運籌。例如,假如你指示它在計算機 1 上運行計算的一部分,而在計算機 2 上運行另一部分,它可以自動進行必要的數據傳輸。

計算需要將實際數據反饋進你在 TensorFlow 代碼中定義的占位符。這是以 Python 的 dictionary 的形式給出的,其中的鍵是占位符的名稱。

 

 

在這里執行的 train_step 是當我們要求 TensorFlow 最小化交叉熵時獲得的。這是計算梯度和更新權重和偏置的步驟。

最終,我們還需要一些值來顯示,以便我們可以追蹤我們模型的性能。

通過在饋送 dictionary 中提供測試而不是訓練數據,可以對測試數據進行同樣的計算(例如每 100 次迭代計算一次。有 10,000 個測試數字,所以會耗費 CPU 一些時間)。

最后一行代碼用於在訓練回路中計算准確度和交叉熵(例如每 10 次迭代)。

下面是所有代碼:

 

 

這個簡單的模型已經能識別 92% 的數字了。但這個准確度還不夠好,但是你現在要顯著地改善它。怎么做呢?深度學習就是要深,要更多的層!

 

 

讓我們來試試 5 個全連接層。

 

 

我們繼續用 softmax 來作為最后一層的激活函數,這也是為什么在分類這個問題上它性能優異的原因。但在中間層,我們要使用最經典的激活函數:sigmoid 函數。

下面開始寫代碼。為了增加一個層,你需要為中間層增加一個額外的權重矩陣和一個額外的偏置向量:

 

 

這樣增加多個層:

 

 

但 sigmoid 不是全能的。在深度網絡里,sigmoid 激活函數也能帶來很多問題。它把所有的值都擠到了 0 到 1 之間,而且當你重復做的時候,神經元的輸出和它們的梯度都歸零了。修正線性單元(ReLU)也是一種很常使用的激活函數:

 

 

對比一下在 300 次迭代時 sigmoid 函數(淺色線)和 ReLU(深色線)的效果,可以看到 ReLU 在准確度和交叉熵損失上的表現都顯著更好。

 

 

用 ReLU 替換你所有的 sigmoid,然后你會得到一個更快的初始收斂並且當我們繼續增加層的時候也避免了一些后續問題的產生。僅僅在代碼中簡單地用 tf.nn.relu 來替換 tf.nn.sigmoid 就可以了。

但收斂過快也有問題:

 

 

這些曲線很嘈雜,看看測試精確度吧:它在全百分比范圍內跳上跳下。這意味着即使 0.003 的學習率我們還是太快了。但我們不能僅僅將學習率除以十或者永遠不停地做訓練。一個好的解決方案是開始很快隨后將學習速率指數級衰減至比如說 0.0001。

這個小改變的影響是驚人的。你會看到大部分的噪聲消失了並且測試精確度持續穩定在 98% 以上。

 

 

再看看訓練精確度曲線。在好多個 epoch 里都達到了 100%(一個 epoch=500 次迭代=全部訓練圖片訓練一次)。第一次我們能很好地識別訓練圖片了。

但右邊的圖是什么情況?

 

 

在數千次迭代之后,測試和訓練數據的交叉熵曲線開始不相連。學習算法只是在訓練數據上做工作並相應地優化訓練的交叉熵。它再也看不到測試數據了,所以這一點也不奇怪:過了一會兒它的工作不再對測試交叉熵產生任何影響,交叉熵停止了下降,有時甚至反彈回來。它不會立刻影響你模型對於真實世界的識別能力,但是它會使你運行的眾多迭代毫無用處,而且這基本上是一個信號——告訴我們訓練已經不能再為模型提供進一步改進了。這種情況通常會被稱為「過擬合(overfitting)」。

為了解決這個問題,你可以嘗試采用一種規范化(regularization)技術,稱之為「dropout」。

 

 

在 dropout 里,在每一次訓練迭代的時候,你可以從網絡中隨機地放棄一些神經元。你可以選擇一個使神經元繼續保留的概率 pkeep,通常是 50% 到 75% 之間,然后在每一次訓練的迭代時,隨機地把一些神經元連同它們的權重和偏置一起去掉。在一次迭代里,不同的神經元可以被一起去掉(而且你也同樣需要等比例地促進剩余神經元的輸出,以確保下一層的激活不會移動)。當測試你神經網絡性能的時候,你再把所有的神經元都裝回來 (pkeep=1)。

TensorFlow 提供一個 dropout 函數可以用在一層神經網絡的輸出上。它隨機地清零一些輸出並且把剩下的提升 1/pkeep。你可以在網絡中每個中間層以后插入 dropout。

下面我們集中看一下改進的情況。

當使用 sigmoid 函數,學習率為 0.003 時:

 

 

然后使用 ReLU 替代 sigmoid:

 

 

然后再將學習率衰減到 0.0001:

 

 

增加 dropout:

 

 

解決了過擬合,准確度達到了 98%,但是噪聲又回來了。看起來無論我們做什么,我們看上去都不可能很顯著地解決 98% 的障礙,而且我們的損失曲線依然顯示「過擬合」無法連接。什么是真正的「過擬合」?過擬合發生在該神經網絡學得「不好」的時候,在這種情況下該神經網絡對於訓練樣本做得很好,對真實場景卻並不是很好。有一些像 dropout 一樣的規范化技術能夠迫使它學習得更好,不過過擬合還有更深層的原因。

 

 

基本的過擬合發生在一個神經網絡針對手頭的問題有太多的自由度的時候。想象一下我們有如此多的神經元以至於所組成的網絡可以存儲我們所有的訓練圖像並依靠特征匹配來識別它們。它會在真實世界的數據里迷失。一個神經網絡必須有某種程度上的約束以使它能夠歸納推理它在學習中所學到的東西。

如果你只有很少的訓練數據,甚至一個很小的網絡都能夠用心學習它。一般來說,你總是需要很多數據來訓練神經網絡。

最后,如果你已經做完了所有的步驟,包括實驗了不同大小的網絡以確保它的自由度已經約束好了、采用了 dropout、並且訓練了大量的數據,你可能會發現你還是被卡在了當前的性能層次上再也上不去了。這說明你的神經網絡在它當前的形態下已經無法從你提供的數據中抽取到更多的信息了,就像我們這個例子這樣。

還記得我們如何使用我們的圖像嗎?是所有的像素都展平到一個向量里么?這是一個很糟糕的想法。手寫的數字是由一個個形狀組成的,當我們把像素展平后我們會丟掉這些形狀信息。不過,有一種神經網絡可以利用這些形狀信息:卷積網絡(convolutional network)。讓我們來試試。

 

 

在卷積網絡層中,一個「神經元」僅對該圖像上的一個小部分的像素求加權和。然后,它通常會添加一個偏置單元,並且將得到的加權和傳遞給激活函數。與全連接網絡相比,其最大的區別在於卷積網絡的每個神經元重復使用相同的權重,而不是每個神經元都有自己的權重。

在上圖中,你可以看到通過連續修改圖片上兩個方向的權重(卷積),能夠獲得與圖片上的像素點數量相同的輸出值(盡管在邊緣處需要填充(padding))。

要產生一個輸出值平面,我們使用了一張 4x4 大小的彩色圖片作為出輸入。在上圖當中,我們需要 4x4x3=48 個權重,這還不夠,為了增加更多自由度,我們還需要選取不同組的權重值重復實驗。

通過向權重張量添加一個維度,能夠將兩組或更多組的權重重寫為一組權重,這樣就給出了一個卷積層的權重張量的通用實現。由於輸入、輸出通道的數量都是參數,我們可以開始堆疊式(stacking)和鏈式(chaining)的卷積層。

最后,我們需要提取信息。在最后一層中,我們僅僅想使用 10 個神經元來分類 0-9 十個不同的數字。傳統上,這是通過「最大池化(max-pooling)」層來完成的。即使今天有許多更簡單的方法能夠實現這分類任務,但是,「最大池化」能夠幫助我們直覺地理解卷積神經網絡是怎么工作的。如果你認為在訓練的過程中,我們的小塊權重會發展成能夠過濾基本形狀(水平線、垂直線或曲線等)的過濾器(filter),那么,提取有用信息的方式就是識別輸出層中哪種形狀具有最大的強度。實際上,在最大池化層中,神經元的輸出是在 2x2 的分組中被處理,最后僅僅保留輸出最大強度的神經元。

這里有一種更簡單的方法:如果你是以一步兩個像素移動圖片上的滑塊而不是以每步一個像素地移動圖片上的滑塊。這種方法就是有效的,今天的卷積網絡僅僅使用了卷積層。

讓我們建立一個用於手寫數字識別的卷積網絡。在頂部,我們將使用 3 個卷積層;在底部,我們使用傳統的 softmax 讀出層,並將它們用完全連接層連接。

 

 

注意,第二與第三卷積層神經元數量以 2x2 為倍數減少,這就解釋了為什么它們的輸出值從 28x28 減少為 14x14,然后再到 7x7。卷積層的大小變化使神經元的數量在每層下降約為:28x28x14≈3000->14x14x8≈1500 → 7x7x12≈500 → 200。下一節中,我們將給出該網絡的具體實現。

那我們如何在 TensorFlow 中實現它呢?為了將我們的代碼轉化為卷積模型,我們需要為卷積層定義適當的權重張量,然后將該卷積層添加到模型中。我們已經理解到卷積層需要以下形式的權重張量。下面代碼是用 TensorFlow 語法來對其初始化:

 

 

然后實現其模型:

 

 

在 TensorFlow 中,使用 tf.nn.conv2d 函數實現卷積層,該函數使用提供的權重在兩個方向上掃描輸入圖片。這僅僅是神經元的加權和部分,你需要添加偏置單元並將加權和提供給激活函數。不要過分在意 stride 的復雜語法,查閱文檔就能獲取完整的詳細信息。這里的填充(padding)策略是為了復制圖片的邊緣的像素。所有的數字都在一個統一的背景下,所以這僅僅是擴展了背景,並且不應該添加不需要的任何樣式。

完成這一步之后,我們應該能達到 99% 的准確度了吧?

 

 

然而並沒有!什么情況?

 

 

還記得前面我們怎么解決這個「過擬合」問題的嗎?使用 dropout。

我們該怎么對其進行優化呢?調整你的神經網絡的一個好方法是先去實現一個限制較多的神經網絡,然后給它更多的自由度並且增加 dropout,使神經網絡避免過擬合。最終你將得到一個相當不錯的神經網絡。

 

 

例如,我們在第一層卷積層中僅僅使用了 4 個 patch,如果這些權重的 patch 在訓練的過程中發展成不同的識別器,你可以直觀地看到這對於解決我們的問題是不夠的。手寫數字模式遠多於 4 種基本樣式。

因此,讓我們稍微增加 patch 的數量,將我們卷積層中 patch 的數量從 4,8,12 增加到 6,12,24,並且在全連接層上添加 dropout。它們的神經元重復使用相同的權重,在一次訓練迭代中,通過凍結(限制)一些不會對它們起作用的權重,dropout 能夠有效地工作。

然后模型的准確度就突破 99% 了!

 

 

對比之前的結果可以看到明顯的進步:

 

 

相關資源:

 

 

第二部:建立循環神經網絡

在這一部分 Gorner 講解了如何使用 TensorFlow 建立循環神經網絡。

 

視頻封面
沒有phd玩深度學習2 - 騰訊視頻
視頻
 

 

 

RNN 的結構與訓練

首先,RNN 的結構如下,其中第二層為 softmax 層(讀取出東西)。但特殊的是,中間綠色層的輸出在下一步驟中會返回到輸入中。

下面是循環神經網絡里面的等式。

那么接下來如何訓練 RNN?以自然語言處理為例:輸入通常為字符(character)。如下圖中所示,我們輸入字符,反向傳播通過該神經網絡、反向傳播通過 softmax 層,我們會得到字符的輸出。如果得到的字符不是我們想要的,對比一下得到的與我們想要的,我們就對網絡中的權重進行調整,從而得到更好的結果。

但如果結果是錯的怎么辦?而且不是因為網絡中的權重偏見,而是因為狀態輸入 H-1 是錯的。在此問題中,輸入是連續的,有些無能為力的感覺。在這個問題上卡住了,那么解決方案是什么?

解決方案就是復制該 cell,再次使用同樣的權重。下圖演示了你該如何訓練循環神經網絡,在多次迭代中共享同樣的權重和偏差。

另外,值得一提的是如果你想往深處做,可以堆疊 cells(如下圖)。

而后,Gorner 以句子為例講解了如何使用 TensorFlow 建立循環神經網絡。在以下示例中,我們是用單詞而非字符作為輸入,建立這樣的模型中就有一個典型的問題:長期依存關系。而要把下面的長句輸入,需要非常深的循環神經網絡,而如果網絡太深,訓練時候又不太好收斂。在數學細節上要提到的就是梯度消失的問題,梯度成 0 了。

對此問題的一種解決方案是 LSTM。下圖從數學角度解釋了該解決方案為何有效。

在實踐中,LSTM 有效是因為它基於了門(gates) 的概念:

GRU 的等式:

在 TensorFlow 中實現 RNN 語言模型

接下來就是如何用 TensorFlow 實現語言模型的循環神經網絡了。在教授語言模型預測單詞的下一個字符是什么的例子中,Gorner 使用了 TensorFlow 中更高等級的 API。圖中的 GRUCell 有着多層的循環神經網絡層、兩個門。然后把網絡做的更深,3 個 GRU 堆疊在一起。接下來,展開整個網絡,在 TensorFlow 中,這被稱為動態 RNN 功能。最終得到如下結果。

下圖演示了如何在 TensorFlow 中實現 Softmax 層。

就行正確理解 RNN 的工作原理很難一樣,向它們正確的輸入數據也很難,你會發現里面里面有很多誤差。接下來 Gorner 嘗試了如何做出正確的輸入、得到正確的輸出。依此為例,他講解了所選擇的 batchsize、cellsize 和層(如下)。

各個步驟實現的代碼如下:

在 TensorFlow 中實現語言模型的完整代碼如下:

最后,Gorne 打開 TensorFlow 演示了如何實際建模語言模型,並且演示了 RNN 在文本翻譯、圖像描述等領域的應用。看完之后我決定先下個 TensorFlow。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM