沒有博士學位,照樣玩轉TensorFlow深度學習
本文內容由機器之心編譯自谷歌開發者博客的 Codelabs 項目。據介紹,Google Developers Codelabs 提供了有引導的、教程式的和上手式的編程體驗。大多數 Codelabs 項目都能幫助你了解開發一個小應用或為一個已有的應用加入新功能的過程。這些應用涉及到很多主題,包括 Android Wear、Google Compute Engine、Project Tango、和 iOS 上的 Google API。
本項目的原文可參閱:https://codelabs.developers.google.com/codelabs/cloud-tensorflow-mnist/#13
1、概述
在 codelab 項目中,你將學習如何構建并訓練出能夠識別手寫數字的神經網絡。在這過程中,當這個神經網絡的準確度提升至 99%時,你還會發現深度學習專業人士用來有效訓練模型的貿易工具。
這個 codelab 項目使用的是 MNIST 數據集,這個包含 60,000 個有標記數字的集合是幾屆博士努力近二十年的成果。你將會用不到 100 行的 Python/TensorFlow 代碼來解決上述問題。
你將學到:
①神經網絡的定義及如何訓練神經網絡
②如何使用 TensorFlow 構建基本的 1 層神經網絡
③如何添加多層神經網絡
④訓練提示和技巧:過擬合、dropout、學習速率衰減等...
⑤如何解決深度神經網絡的問題
⑥如何構建卷積網絡
對此,你將需要:
①Python 2 或 3(建議使用 Python 3)
②TensorFlow
③Matplotlib(Python 的可視化庫)
安裝說明會在下一步中給出。
2. 準備:安裝 TensorFlow,獲取示例代碼
在你的計算機上安裝必要軟件:Python、TensorFlow 和 Matplotlib。完整的安裝說明如下:INSTALL.txt
克隆 GitHub 存儲庫:
$ git clone https://github.com/martin-gorner/tensorflow-mnist-tutorial
這個庫包含了多個文件,而你將只在mnist_1.0_softmax.py中操作。其它文件是用于加載數據和可視化結果的解決方案或支持代碼。
當你啟動初始python腳本時,應當能夠看到訓練過程的實時可視化:
$ python3 mnist_1.0_softmax.py
疑難解答:如果無法運行實時可視化,或者如果你只想要使用文本輸出,則可以通過注釋掉一行并取消另一行的注釋來禁用可視化。請參閱文件底部的說明。
為 TensorFlow 構建的可視化工具是 TensorBoard,其主要目標比我們在這里所需的更宏大。它能使你能夠跟蹤你在遠程服務器上的分布式 TensorFlow 工作。而對于我們的這個實驗,matplotlib 將作為替代,并且我們還有額外收獲——實時動畫。但是如果你使用 TensorFlow 進行嚴謹的工作,你一定要試試 TensorBoard。
3、理論:訓練一個神經網絡
我們首先來觀察一個正在訓練的神經網絡。其代碼會在下一節解釋,所以現在不必查看。
我們的神經網絡可以輸入手寫數字并對它們進行分類,即將它們識別為 0、1、2……9。它基于內部變量(「權重(weights)」和「偏差(bias)」,會在后面進行解釋),需要有一個正確的值來分類才能正常工作。這個「正確的值」通過訓練過程進行學習,這也將在后面詳細解釋。你現在需要知道的是,訓練回路看起來像這樣:
Training digits => updates to weights and biases => better recognition (loop)
讓我們逐個通過可視化的六個面板,了解訓練神經網絡需要什么。
你可以看到訓練數字每次 100 個被送入訓練回路;也可以看到當前訓練狀態下的神經網絡是已將數字正確識別(白色背景)還是誤分類(紅色背景,左側印有正確的標示,每個數字右側印有計算錯誤的標示)。
此數據集中有 50,000 個訓練數字。我們在每次迭代(iteration)中將 100 個數字送入訓練循環中,因此系統將在 500 次迭代之后看到所有訓練數字一次。我們稱之為一個「epoch」。
為了測試在現實條件下的識別質量,我們必須使用系統在訓練期間從未看過的數字。否則,它可能記住了所有的訓練數字,卻仍無法識別我剛才寫的「8」。MNIST 數據集包含了 10,000 個測試數字。此處你能看到每個數字對應的大約 1000 種書寫形式,其中所有錯誤識別的數字列在頂部(有紅色背景)。左邊的刻度會給你一個粗略的分辨率精確度(正確識別的百分比)。
為了驅動訓練,我們來定義損失函數,即一個展示出系統數字識別能力有多糟的值,并且系統會盡力將其最小化。損失函數(loss function,此處為「交叉熵」)的選擇稍后會做出解釋。你會看到,隨著訓練的進行,訓練和測試數據的損失會減少,而這個現象是好的,意味著神經網絡正在學習。X 軸表示了學習過程中的迭代。
這個準確度只是正確識別的數字的百分比,是在訓練和測試集上計算出的。如果訓練順利,它便會上升。
最后的兩幅圖表說明了內部變量所取的所有值的擴展,即隨訓練進行而變化的權重和偏置。比如偏置從 0 開始,且最終得到的值大致均勻地分布在-1.5 和 1.5 之間。如果系統不能很好地收斂,那么這些圖可能有用。倘若你發現權重和偏差擴展到上百或上千,那么就可能有問題了。
圖中的條帶為百分數。此處有 7 條帶,所以每條帶是所有值的 100/7,也就是 14%。
用于可視化 GUI 的鍵盤快捷鍵
1 ......... display 1st graph only 僅顯示第 1 張圖
2 ......... display 2nd graph only 僅顯示第 2 張圖
3 ......... display 3rd graph only 僅顯示第 3 張圖
4 ......... display 4th graph only 僅顯示第 4 張圖
5 ......... display 5th graph only 僅顯示第 5 張圖
6 ......... display 6th graph only 僅顯示第 6 張圖
7 ......... display graphs 1 and 2 顯示 1 和 2 圖
8 ......... display graphs 4 and 5 顯示 4 和 5 圖
9 ......... display graphs 3 and 6 顯示 3 和 6 圖
ESC or 0 .. back to displaying all graphs 返回,顯示所有圖
空格 ..... pause/resume 暫停/繼續
O ......... box zoom mode (then use mouse) 框縮放模式(然后使用鼠標)
H ......... reset all zooms 重置所有縮放
Ctrl-S .... save current image 保存當前圖像
什么是“權重”和“偏置”?“交叉熵”又是如何被計算的?訓練算法究竟是如何工作的?請到下一部分一探究竟。
4、理論 : 單層神經網絡
MNIST 數據集中,手寫數字是 28x28 像素的灰度圖像。將它們進行分類的最簡單的方法就是使用 28x28=784 個像素作為單層神經網絡的輸入。
神經網絡中的每個「神經元」對其所有的輸入進行加權求和,并添加一個被稱為「偏置(bias)」的常數,然后通過一些非線性激活函數來反饋結果。
為了將數字分為 10 類(0 到 9),我們設計了一個具有 10 個輸出神經元的單層神經網絡。對于分類問題,softmax 是一個不錯的激活函數。通過取每個元素的指數,然后歸一化向量(使用任意的范數(norm),比如向量的普通歐幾里得距離)從而將 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。它必須被添加到先前計算的矩陣中的每一行當中。使用一個稱為 "broadcasting" 的魔法,我們將會用一個簡單的加號寫出它。
「Broadcasting」是 Python 和 numpy(Python 的科學計算庫)的一個標準技巧。它擴展了對不兼容維度的矩陣進行正常操作的方式。「Broadcasting add」意味著「如果你因為兩個矩陣維度不同的原因而不能將其相加,那么你可以根據需要嘗試復制一個小的矩陣使其工作。」
我們最終應用 softmax 激活函數并且得到一個描述單層神經網絡的公式,并將其應用于 100 張圖像:
順便說一下,什么是「tensor(張量)」?
「張量(tensor)」像一個矩陣,但是卻有著任意數量的維度。一個 1 維的張量是一個向量。一個二維的張量是一個矩陣。然后你可以有 3, 4, 5 或者更多維的張量。
5、理論:梯度下降
現在我們的神經網絡從輸入圖像中產生預測,我們需要知道它們可以做到什么樣的程度,即在我們知道的事實和網絡的預測之間到底有多大的距離。請記住,我們對于這個數據集中的所有圖像都有一個真實的標簽。
任何一種定義的距離都可以進行這樣的操作,普通歐幾里得距離是可以的,但是對于分類問題,被稱為「交叉熵(cross-entropy)」的距離更加有效。
「one-hot」編碼意味著你使用一個 10 個值的向量,其中除了第 6 個值為 1 以外的所有值都是 0。這非常方便,因為這樣的格式和我們神經網絡預測輸出的格式非常相似,同時它也作為一個 10 值的向量。
「訓練」一個神經網絡實際上意味著使用訓練圖像和標簽來調整權重和偏置,以便最小化交叉熵損失函數。它是這樣工作的。
交叉熵是一個關于權重、偏置、訓練圖像的像素和其已知標簽的函數。
如果我們相對于所有的權重和所有的偏置計算交叉熵的偏導數,我們就得到一個對于給定圖像、標簽和當前權重和偏置的「梯度」。請記住,我們有 7850 個權重和偏置,所以計算梯度需要大量的工作。幸運的是,TensorFlow 可以來幫我們做這項工作。
梯度的數學意義在于它指向「上(up)」。因為我們想要到達一個交叉熵低的地方,那么我們就去向相反的方向。我們用一小部分的梯度更新權重和偏置并且使用下一批訓練圖像再次做同樣的事情。我們希望的是,這可以使我們到達交叉熵最小的凹點的低部。
在這副圖片當中,交叉熵被表示為一個具有兩個權重的函數。事實上,還有更多。梯度下降算法遵循著一個最陡的坡度下降到局部最小值的路徑。訓練圖像在每一次迭代中同樣會被改變,這使得我們向著一個適用于所有圖像的局部最小值收斂。
「學習率(learning rate)」: 在整個梯度的長度上,你不能在每一次迭代的時候都對權重和偏置進行更新。這就會像是你穿著七里靴卻試圖到達一個山谷的底部。你會直接從山谷的一邊到達另一邊。為了到達底部,你需要一些更小的步伐,即只使用梯度的一部分,通常在 1/1000 區域中。我們稱這個部分為「學習率(Learning rate)」。
總結一下,以下是訓練過程的步驟:
Training digits and labels => loss function => gradient (partial derivatives) => steepest descent => update weights and biases => repeat with next mini-batch of training images and labels
訓練數字和標簽 => 損失函數 => 梯度(部分偏導數)=> 最陡的梯度 => 更新權重和偏置 => 使用下一個 mini-batch 的圖像和標簽重復這一過程
為什么使用 100 個圖像和標簽的 mini-batch?
你當然也可以只在一個示例圖像中計算你的梯度并且立即更新權重和偏置(這在科學文獻中被稱為「隨機梯度下降(stochastic gradient descent)」)。在 100 個樣本上都這樣做可以得到一個更好地表示由不同樣本圖像施加約束的梯度并且可能更快地朝著解決方案收斂。mini-batch 的大小是可調整的參數。還有一個更加技術化的原因:使用批處理也意味著使用較大的矩陣,而這些通常更容易在 GPU 上優化。
常見問題
為什么交叉熵是在分類問題中合適的定義距離?
解答鏈接:https://jamesmccaffrey.wordpress.com/2013/11/05/why-you-should-use-cross-entropy-error-instead-of-classification-error-or-mean-squared-error-for-neural-network-classifier-training/
6、實驗:讓我們來看看代碼
單層神經網絡的代碼已經寫好了。請打開 mnist_1.0_softmax.py 文件并按說明進行操作。
你在本節的任務是理解開始代碼,以便稍后對其改進。
你應該看到,在文檔中的說明和啟動代碼只有微小的差別。它們對應于可視化的函數,并且在注釋中被標記。此處可忽略。
mnist_1.0_softmax.py:
https://github.com/martin-gorner/tensorflow-mnist-tutorial/blob/master/mnist_1.0_softmax.py
import tensorflow as tf
X = tf.placeholder(tf.float32, [None, 28, 28, 1])
W = tf.Variable(tf.zeros([784, 10]))
b = tf.Variable(tf.zeros([10]))init = tf.initialize_all_variables()
我們首先定義 TensorFlow 的變量和占位符。變量是你希望訓練算法為你確定的所有的參數。在我們的例子中參數是權重和偏差。
占位符是在訓練期間填充實際數據的參數,通常是訓練圖像。持有訓練圖像的張量的形式是 [None, 28, 28, 1],其中的參數代表:
-
28, 28, 1: 圖像是 28x28 每像素 x 1(灰度)。最后一個數字對于彩色圖像是 3 但在這里并非是必須的。
-
None: 這是代表圖像在小批量(mini-batch)中的數量。在訓練時可以得到。
mnist_1.0_softmax.py:
https://github.com/martin-gorner/tensorflow-mnist-tutorial/blob/master/mnist_1.0_softmax.py
# model
Y = tf.nn.softmax(tf.matmul(tf.reshape(X, [-1, 784]), W) + b)
# placeholder for correct labels
Y_ = tf.placeholder(tf.float32, [None, 10])
# loss functioncross_entropy = -tf.reduce_sum(Y_ * tf.log(Y))
# % of correct answers found in batch
is_correct = tf.equal(tf.argmax(Y,1), tf.argmax(Y_,1))
accuracy = tf.reduce_mean(tf.cast(is_correct, tf.float32))
第一行是我們單層神經網絡的模型。公式是我們在前面的理論部分建立的。tf.reshape 命令將我們的 28×28 的圖像轉化成 784 個像素的單向量。在 reshape 中的「-1」意味著「計算機,計算出來,這只有一種可能」。在實際當中,這會是圖像在小批次(mini-batch)中的數量。
然后,我們需要一個額外的占位符用于訓練標簽,這些標簽與訓練圖像一起被提供。
現在我們有模型預測和正確的標簽,所以我們計算交叉熵。tf.reduce_sum 是對向量的所有元素求和。
最后兩行計算了正確識別數字的百分比。這是留給讀者的理解練習,使用 TensorFlow API 參考。你也可以跳過它們。
mnist_1.0_softmax.py:
https://github.com/martin-gorner/tensorflow-mnist-tutorial/blob/master/mnist_1.0_softmax.py)
optimizer = tf.train.GradientDescentOptimizer(0.003)
train_step = optimizer.minimize(cross_entropy)
才是 TensorFlow 發揮它力量的地方。你選擇一個適應器(optimiser,有許多可供選擇)并且用它最小化交叉熵損失。在這一步中,TensorFlow 計算相對于所有權重和所有偏置(梯度)的損失函數的偏導數。這是一個形式衍生( formal derivation),并非是一個耗時的數值型衍生。
梯度然后被用來更新權重和偏置。學習率為 0.003。
最后,是時候來運行訓練循環了。到目前為止,所有的 TensorFlow 指令都在內存中準備了一個計算圖,但是還未進行計算。
TensorFlow 的 “延遲執行(deferred execution)” 模型:TensorFlow 是為分布式計算構建的。它必須知道你要計算的是什么、你的執行圖(execution graph),然后才開始發送計算任務到各種計算機。這就是為什么它有一個延遲執行模型,你首先使用 TensorFlow 函數在內存中創造一個計算圖,然后啟動一個執行 Session 并且使用 Session.run 執行實際計算任務。在此時,圖形無法被更改。
由于這個模型,TensorFlow 接管了分布式運算的大量運籌。例如,假如你指示它在計算機 1 上運行計算的一部分 ,而在計算機 2 上運行另一部分,它可以自動進行必要的數據傳輸。
計算需要將實際數據反饋進你在 TensorFlow 代碼中定義的占位符。這是以 Python 的 dictionary 的形式給出的,其中的鍵是占位符的名稱。
mnist_1.0_softmax.py:
https://github.com/martin-gorner/tensorflow-mnist-tutorial/blob/master/mnist_1.0_softmax.py
sess = tf.Session()sess.run(init)for i in range(1000):
# load batch of images and correct answers
batch_X, batch_Y = mnist.train.next_batch(100)
train_data={X: batch_X, Y_: batch_Y}
# train
sess.run(train_step, feed_dict=train_data)
在這里執行的 train_step 是當我們要求 TensorFlow 最小化交叉熵時獲得的。這是計算梯度和更新權重和偏置的步驟。
最終,我們還需要一些值來顯示,以便我們可以追蹤我們模型的性能。
在訓練回路中使用該代碼來計算準確度和交叉熵(例如每 10 次迭代):
# success ?a,c = sess.run([accuracy, cross_entropy], feed_dict=train_data)
通過在饋送 dictionary 中提供測試而不是訓練數據,可以對測試數據進行同樣的計算(例如每 100 次迭代計算一次。有 10,000 個測試數字,所以會耗費 CPU 一些時間):
# success on test data ?
test_data={X: mnist.test.images, Y_: mnist.test.labels}
a,c = sess.run([accuracy, cross_entropy], feed=test_data)
TensorFlow 和 Numpy 是朋友:在準備計算圖時,你只需要操縱 TensorFlow 張量和命令,比如 tf.matmul, tf.reshape 等。
然而,只要執行 Session.run 命令,它的返回值就是 Numpy 張量,即 Numpy 可以使用的 numpy.ndarray 對象以及基于它的所有科學計算庫。這就是使用 matplotlib(基于 Numpy 的標準 Python 繪圖庫)為本實驗構建實時可視化的方法。
這個簡單的模型已經能識別 92% 的數字了。這不錯,但是你現在要顯著地改善它。
7、實驗:增加層
為了提高識別的準確度,我們將為神經網絡增加更多的層。第二層神經元將計算前一層神經元輸出的加權和,而非計算像素的加權和。這里有一個 5 層全相連的神經網絡的例子:
我們繼續用 softmax 來作為最后一層的激活函數,這也是為什么在分類這個問題上它性能優異的原因。但在中間層,我們要使用最經典的激活函數:sigmoid:在這一節中你的任務是為你的模型增加一到兩個中間層以提高它的性能。
答案可以在 mnist_2.0_five_layers_sigmoid.py 中找到。只有當你實在想不出來的時候再使用它!為了增加一個層,你需要為中間層增加一個額外的權重矩陣和一個額外的偏置向量:
W1 = tf.Variable(tf.truncated_normal([28*28, 200] ,stddev=0.1))
B1 = tf.Variable(tf.zeros([200]))
W2 = tf.Variable(tf.truncated_normal([200, 10], stddev=0.1))
B2 = tf.Variable(tf.zeros([10]))
對,就這么做。通過 2 個中間層以及例子中 200 個和 100 個神經元,你現在應該能夠把你的神經網絡的準確度推高到 97% 了。
8、實驗:深度網絡需要特別注意的地方
隨著層數的增加,神經網絡越來越難以收斂。但現在我們知道如何控制它們的行為了。這里是一些只用 1 行就可以實現的改進,當你看到準確度曲線出現如下情況的時候,這些小技巧會幫到你:
修正線性單元(ReLU)激活函數
在深度網絡里,sigmoid 激活函數確實能帶來很多問題。它把所有的值都擠到了 0 到 1 之間,而且當你重復做的時候,神經元的輸出和它們的梯度都歸零了。值得一提的是,出于歷史原因,一些現代神經網絡使用了 ReLU(修正線性單元),它大致是如下這個樣子:
升級 1/4:用 RELU 替換你所有的 sigmoid,然后你會得到一個更快的初始收斂并且當我們繼續增加層的時候也避免了一些后續問題的產生。僅僅在代碼中簡單地用 tf.nn.relu 來替換 tf.nn.sigmoid 就可以了。
一個更好的優化器
在一個特別多維的空間里,就像當前這個情況——我們有 10K 量級的權值和偏置值——「鞍點 (saddle points)」會頻繁出現。這些點不是局部最小值點,但它的梯度卻是零,那么梯度降的優化會卡在這里。TensorFlow 有一系列可以用的優化器,包括一些帶有一定的慣性,能夠安全越過鞍點的優化器。
升級 2/4:現在將你的 tf.train.GradientDescentOptimiser 替換為 tf.train.AdamOptimizer。
隨機初始化
準確性一直卡在 0.1?你把你的權值初始化成隨機值了沒?對于偏置值,如果用 ReLU 的話,最好的辦法就是把它們都初始化成小的正值,這樣神經元一開始就會工作在 ReLU 的非零區域內。
W = tf.Variable(tf.truncated_normal([K, L] ,stddev=0.1))
B = tf.Variable(tf.ones([L])/10)
升級 3/4:現在檢查是否你所有的權值和偏置值都被初始化好了。上圖所示的 0.1 會作為偏置值。
不定值(NaN)
如果你看到你的精確曲線陡然下滑并且調試口輸出的交叉熵是 NaN,不用感到頭疼,你其實是正在嘗試計算 log(0),而這肯定是個不定值(NaN)。還記得嗎,交叉熵的計算涉及到對 softmax 層的輸出取對數。鑒于 softmax 基本上是一個指數,它肯定不是 0,我們如果用 32 位精度的浮點運算就還好,exp(-100) 基本上可以算作是 0 了。
很幸運,TensorFlow 有一個非常方便的函數可以在單步內計算 softmax 和交叉熵,它是以一種數值上較為穩定的方式實現的。如果要使用它,你需要在應用 softmax 之前將原始的權重和加上你最后一層的偏置隔離開來(在神經網絡的術語里叫「logits」)。
如果你模型的最后一行是這樣的:
Y = tf.nn.softmax(tf.matmul(Y4, W5) + B5)
你需要把它替換成:
Ylogits = tf.matmul(Y4, W5) + B5
Y = tf.nn.softmax(Ylogits)
并且你現在能以一種安全的方式計算交叉熵了:
cross_entropy = tf.nn.softmax_cross_entropy_with_logits(Ylogits, Y_)
同樣加上下面這行代碼使得測試和訓練的交叉熵能夠同框顯示:
cross_entropy = tf.reduce_mean(cross_entropy)*100
升級 4/4:請把 tf.nn.softmax_cross_entropy_with_logits 加到你的代碼里。你也可以跳過這一步,等你真在你的輸出里看到 NaN 以后再來做這步。現在,你已經準備好實現「深度」了。
9、實驗:學習速率衰退
通過兩個、三個或者四個中間層,你現在可以將準確度提升至接近 98%,當然,你的迭代次數要達到 5000 次以上。不過你會發現你并不總是會得到這樣的結果。
這些曲線很嘈雜,看看測試精確度吧:它在全百分比范圍內跳上跳下。這意味著即使 0.003 的學習率我們還是太快了。但我們不能僅僅將學習率除以十或者永遠不停地做訓練。一個好的解決方案是開始很快隨后將學習速率指數級衰減至比如說 0.0001。
這個小改變的影響是驚人的。你會看到大部分的噪聲消失了并且測試精確度持續穩定在 98% 以上。
再看看訓練精確度曲線。在好多個 epoch 里都達到了 100%(一個 epoch=500 次迭代=全部訓練圖片訓練一次)。第一次我們能很好地識別訓練圖片了。
請把學習率衰退加到你的代碼里。為了把一個不同的學習率在每次迭代時傳給 AdamOptimizer,你需要定義一個新的占位符(placeholder)并在每次迭代時通過 feed_dict 賦給它一個新的參數。
這里是一個指數級衰減的方程:lr = lrmin+(lrmax-lrmin)*exp(-i/2000) 答案可以在這個文件里找到:mnist_2.1_five_layers_relu_lrdecay.py。如果你被卡住了可以用它。
10、實驗:dropout、過擬合
你可能已經注意到在數千次迭代之后,測試和訓練數據的交叉熵曲線開始不相連。學習算法只是在訓練數據上做工作并相應地優化訓練的交叉熵。它再也看不到測試數據了,所以這一點也不奇怪:過了一會兒它的工作不再對測試交叉熵產生任何影響,交叉熵停止了下降,有時甚至反彈回來。
它不會立刻影響你模型對于真實世界的識別能力,但是它會使你運行的眾多迭代毫無用處,而且這基本上是一個信號——告訴我們訓練已經不能再為模型提供進一步改進了。這種無法連接通常會被標明「過擬合(overfitting)」,而且當你看到這個的時候,你可以嘗試采用一種規范化(regularization)技術,稱之為「dropout」。
在 dropout 里,在每一次訓練迭代的時候,你可以從網絡中隨機地放棄一些神經元。你可以選擇一個使神經元繼續保留的概率 pkeep,通常是 50% 到 75% 之間,然后在每一次訓練的迭代時,隨機地把一些神經元連同它們的權重和偏置一起去掉。在一次迭代里,不同的神經元可以被一起去掉(而且你也同樣需要等比例地促進剩余神經元的輸出,以確保下一層的激活不會移動)。當測試你神經網絡性能的時候,你再把所有的神經元都裝回來 (pkeep=1)。
TensorFlow 提供一個 dropout 函數可以用在一層神經網絡的輸出上。它隨機地清零一些輸出并且把剩下的提升 1/pkeep。這里是如何把它用在一個兩層神經網絡上的例子。
# feed in 1 when testing, 0.75 when training
pkeep = tf.placeholder(tf.float32)
Y1 = tf.nn.relu(tf.matmul(X, W1) + B1)
Y1d = tf.nn.dropout(Y1, pkeep)
Y = tf.nn.softmax(tf.matmul(Y1d, W2) + B2)
你現在可以在網絡中每個中間層以后插入 dropout。如果你沒時間深入閱讀的話,這是本項目里的可選步驟。
該解決方案可以在 mnist_2.2_five_layers_relu_lrdecay_dropout.py:
(https://github.com/martin-gorner/tensorflow-mnist-tutorial/blob/master/mnist_2.2_five_layers_relu_lrdecay_dropout.py)
里找到。如果你被難住了,可以用它。
你會看到測試損失已經被搞回來了,已經在可控范圍內了,不過至少在這個例子中噪聲重新出現了(如果你知道 dropout 的工作原理的話,這一點也不奇怪)。測試的準確度依然沒變,這倒是有點小失望。這個「過擬合」一定還有其它原因。在我們繼續進行下一步之前,我們先扼要重述一下我們到目前為止用過的所有工具:
無論我們做什么,我們看上去都不可能很顯著地解決 98% 的障礙,而且我們的損失曲線依然顯示「過擬合」無法連接。什么是真正的「過擬合」?過擬合發生在該神經網絡學得「不好」的時候,在這種情況下該神經網絡對于訓練樣本做得很好,對真實場景卻并不是很好。有一些像 dropout 一樣的規范化技術能夠迫使它學習得更好,不過過擬合還有更深層的原因。
基本的過擬合發生在一個神經網絡針對手頭的問題有太多的自由度的時候。想象一下我們有如此多的神經元以至于所組成的網絡可以存儲我們所有的訓練圖像并依靠特征匹配來識別它們。它會在真實世界的數據里迷失。一個神經網絡必須有某種程度上的約束以使它能夠歸納推理它在學習中所學到的東西。
如果你只有很少的訓練數據,甚至一個很小的網絡都能夠用心學習它。一般來說,你總是需要很多數據來訓練神經網絡。
最后,如果你已經做完了所有的步驟,包括實驗了不同大小的網絡以確保它的自由度已經約束好了、采用了 dropout、并且訓練了大量的數據,你可能會發現你還是被卡在了當前的性能層次上再也上不去了。這說明你的神經網絡在它當前的形態下已經無法從你提供的數據中抽取到更多的信息了,就像我們這個例子這樣。
還記得我們如何使用我們的圖像嗎?是所有的像素都展平到一個向量里么?這是一個很糟糕的想法。手寫的數字是由一個個形狀組成的,當我們把像素展平后我們會丟掉這些形狀信息。不過,有一種神經網絡可以利用這些形狀信息:卷積網絡(convolutional network)。讓我們來試試。
11、理論:卷積網絡
在卷積網絡層中,一個「神經元」僅對該圖像上的一個小部分的像素求加權和。然后,它通常會添加一個偏置單元,并且將得到的加權和傳遞給激活函數。與全連接網絡相比,其最大的區別在于卷積網絡的每個神經元重復使用相同的權重,而不是每個神經元都有自己的權重。
在上面的動畫中,你可以看到通過連續修改圖片上兩個方向的權重(卷積),能夠獲得與圖片上的像素點數量相同的輸出值(盡管在邊緣處需要填充(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。下一節中,我們將給出該網絡的具體實現。
12、實現:一個卷積網絡
為了將我們的代碼轉化為卷積模型,我們需要為卷積層定義適當的權重張量,然后將該卷積層添加到模型中。我們已經理解到卷積層需要以下形式的權重張量。下面代碼是用 TensorFlow 語法來對其初始化:
W = tf.Variable(tf.truncated_normal([4, 4, 3, 2], stddev=0.1))
B = tf.Variable(tf.ones([2])/10) # 2 is the number of output channels
在 TensorFlow 中,使用 tf.nn.conv2d 函數實現卷積層,該函數使用提供的權重在兩個方向上掃描輸入圖片。這僅僅是神經元的加權和部分,你需要添加偏置單元并將加權和提供給激活函數。
stride = 1 # output is still 28x28
Ycnv = tf.nn.conv2d(X, W, strides=[1, stride, stride, 1], padding='SAME')
Y = tf.nn.relu(Ycnv + B)
不要過分在意 stride 的復雜語法,查閱文檔就能獲取完整的詳細信息。這里的填充(padding)策略是為了復制圖片的邊緣的像素。所有的數字都在一個統一的背景下,所以這僅僅是擴展了背景,并且不應該添加不需要的任何樣式。
現在該你了。修改你的模型并將其轉化為卷積模型。你可以使用上圖中的值來修改它,你可以減小你的學習速率但是務必先移除 dropout。
你的模型的準確率應該會超過 98%,并且最終達到約 99%。眼看目標就要實現,我們不能停止!看看測試的交叉熵曲線。在你的頭腦中,此時,是否解決方案正在形成?
13、99% 準確率的挑戰
調整你的神經網絡的一個好方法:先去實現一個限制較多的神經網絡,然后給它更多的自由度并且增加 dropout,使神經網絡避免過擬合。最終你將得到一個相當不錯的神經網絡。
例如,我們在第一層卷積層中僅僅使用了 4 個 patch,如果這些權重的 patch 在訓練的過程中發展成不同的識別器,你可以直觀地看到這對于解決我們的問題是不夠的。手寫數字模式遠多于 4 種基本樣式。
因此,讓我們稍微增加 patch 的數量,將我們卷積層中 patch 的數量從 4,8,12 增加到 6,12,24,并且在全連接層上添加 dropout。它們的神經元重復使用相同的權重,在一次訓練迭代中,通過凍結(限制)一些不會對它們起作用的權重,dropout 能夠有效地工作。
加油吧,去打破 99%的限制。增加 patch 數量和通道的數量,如上圖所示,在卷積層中添加 dropout。
解決方案可以在文件 mnist_3.1_convolutional_bigger_dropout.py 中找到。
使用上圖所示的模型,在 10000 個測試的數字中,結果僅僅錯誤了 72 個。你可以在 MNIST 網站上發現,數字識別準確率的世界紀錄大約為 99.7%,這僅比我們用 100 行 Python/TensorFlow 代碼構建的模型的準確率高 0.4%。
最后,不同的 dropout 使我們能夠訓練更大的卷積網絡。增加神經網絡的額外自由度,使模型的最終準確率從 98.9% 達到 99.1%。向卷積層中增加 dropout 不僅減少了測試誤差,而且使我們模型的準確率突破 99%,甚至達到了 99.3%。
14、恭喜!
你已經建立了你的第一個神經網絡,并且訓練精度達到了 99%。在這個學習過程中,你所學到的技術,并不局限于 MNIST 數據集。實際上,這些技術在訓練神經網絡的過程中被廣泛使用。作為禮物,下面提供的內容可以用來幫助你回憶已經所學的內容。
-
在完成了完全神經網絡和卷積網絡后,你應該學習循環神經網絡:https://www.tensorflow.org/tutorials/recurrent/。
-
在本教程中,你已經學習了如何在矩陣層次構建 TensorFlow 模型。Tensorflow 還有更高級的 API,稱為 tf.learn:https://www.tensorflow.org/tutorials/tflearn/
-
要在云上的分布式框架上訓練,我們提供 Cloud ML 服務:https://cloud.google.com/ml
-
最后,我們希望收到你的反饋。如果你在發現了本實驗中的些許錯誤,或者你認為有什么需要改進的地方,請告訴我們。我們通過 GitHub 處理反饋。反饋鏈接:https://github.com/googlecodelabs/feedback/issues/new?title=[cloud-tensorflow-mnist]:&labels[]=content-platform&labels[]=cloud
來自:http://www.jiqizhixin.com/article/2196