进神经网络的学习方式(译文)----中
過匹配和規范化
諾貝爾獎得主美籍意大利裔物理學家恩里科·費米曾被問到他對一個同僚提出的嘗試解決一個重要的未解決物理難題的數學模型。模型和實驗非常匹配,但是費米卻對其產生了懷疑。他問模型中需要設置的自由參數有多少個。答案是“4”。費米回答道:“我記得我的朋友約翰·馮·諾依曼過去常說,有四個參數,我可以模擬一頭大象,而有五個參數,我還能讓他卷鼻子。”
這里,其實是說擁有大量的自由參數的模型能夠描述特別神奇的現象。即使這樣的模型能夠很好的擬合已有的數據,但并不表示是一個好模型。因為這可能只是因為模型中足夠的自由度使得它可以描述幾乎所有給定大小的數據集,不需要對現象的本質有創新的認知。所以發生這種情形時,模型對已有的數據會表現的很好,但是對新的數據很難泛化。對一個模型真正的測驗就是它對沒有見過的場景的預測能力。
費米和馮·諾依曼對有四個參數的模型就開始懷疑了。我們用來對 MNIST 數字分類的 $$30$$ 個隱藏神經元神經網絡擁有將近 $$24,000$$ 個參數!當然很多。我們有 $$100$$ 個隱藏元的網絡擁有將近 $$80,000$$ 個參數,而目前最先進的深度神經網絡包含百萬級或者十億級的參數。我們應當信賴這些結果么?
讓我們將問題暴露出來,通過構造一個網絡泛化能力很差的例子。我們的網絡有 $$30$$ 個隱藏神經元,共 $$23,860$$ 個參數。但是我們不會使用所有 $$50,000$$ 幅訓練圖像。相反,我們只使用前 $$1000$$ 幅圖像。使用這個受限的集合,會讓泛化的問題突顯。按照同樣的方式,使用交叉熵代價函數,學習率設置為 $$\eta=0.5$$ 而 minibatch 大小設置為 $$10$$。不過這里我們訓練回合設置為 $$400$$,比前面的要多很多,因為我們只用了少量的訓練樣本。我們現在使用 network2 來研究代價函數改變的情況:
>>> import mnist_loader >>> training_data, validation_data, test_data = \ ... mnist_loader.load_data_wrapper() >>> import network2 >>> net = network2.Network([784, 30, 10], cost=network2.CrossEntropyCost) >>> net.large_weight_initializer() >>> net.SGD(training_data[:1000], 400, 10, 0.5, evaluation_data=test_data, ... monitor_evaluation_accuracy=True, monitor_training_cost=True)使用上面的結果,我們可以畫出代價函數變化的情況:
這看起來令人振奮,因為代價函數有一個光滑的下降,跟我們預期一致。注意,我只是展示了 $$200$$ 到 $$399$$ 回合的情況。這給出了很好的近距離理解訓練后期的情況,這也是出現有趣現象的地方。
讓我們看看分類準確度在測試集上的表現:
這里我還是聚焦到了后面的過程。 在前 $$200$$ 回合(圖中沒有顯示)準確度提升到了 82%。然后學習逐漸變緩。最終,在 $$280$$ 回合左右分類準確度就停止了增長。后面的回合,僅僅看到了在 $$280$$ 回合準確度周圍隨機的震蕩。將這幅圖和前面的圖進行對比,和訓練數據相關的代價函數持續平滑下降。如果我們只看哪個代價,我們會發現模型的表現變得“更好”。但是測試準確度展示了提升只是一種假象。就像費米不大喜歡的那個模型一樣,我們的網絡在 $$280$$ 回合后就不在能夠繁華到測試數據上。所以這種學習不大有用。也可以說網絡在 $$280$$ 后就過匹配(或者過度訓練)了。
你可能想知道這里的問題是不是由于我們看的是訓練數據的代價,而對比的卻是測試數據上的分類準確度導致的。換言之,可能我們這里在進行蘋果和橙子的對比。如果我們比較訓練數據上的代價和測試數據上的代價,會發生什么,我們是在比較類似的度量么?或者可能我們可以比較在兩個數據集上的分類準確度啊?實際上,不管我們使用什么度量的方式盡管,細節會變化,但本質上都是一樣的。
讓我們來看看測試數據集上的代價變化情況:
我們可以看到測試集上的代價在 $$15$$ 回合前一直在提升,隨后越來越差,盡管訓練數據機上的代價表現是越來越好的。這其實是另一種模型過匹配的標志。盡管,這里帶來了關于我們應當將 $$15$$ 還是 $$280$$ 回合當作是過匹配占主導的時間點的困擾。從一個實踐角度,我們真的關心的是提升測試數據集上的分類準確度,而測試集合上的代價不過是分類準確度的一個反應。所以更加合理的選擇就是將 $$280$$ 看成是過匹配開始占統治地位的時間點。
另一個過匹配的信號在訓練數據上的分類準確度上也能看出來:
準確度一直在提升接近 100%。也就是說,我們的網絡能夠正確地對所有 $$1000$$ 幅圖像進行分類!而在同時,我們的測試準確度僅僅能夠達到 82.27%。所以我們的網絡實際上在學習訓練數據集的特例,而不是能夠一般地進行識別。我們的網絡幾乎是在單純記憶訓練集合,而沒有對數字本質進行理解能夠泛化到測試數據集上。
過匹配是神經網絡的一個主要問題。這在現代網絡中特別正常,因為網絡權重和偏差數量巨大。為了高效地訓練,我們需要一種檢測過匹配是不是發生的技術,這樣我們不會過度訓練。并且我們也想要找到一些技術來降低過匹配的影響。
檢測過匹配的明顯方法是使用上面的方法——跟蹤測試數據集合上的準確度隨訓練變化情況。如果我們看到測試數據上的準確度不再提升,那么我們就停止訓練。當然,嚴格地說,這其實并非是過匹配的一個必要現象,因為測試集和訓練集上的準確度可能會同時停止提升。當然,采用這樣的方式是可以阻止過匹配的。
實際上,我們會使用這種方式變形來試驗。記得之前我們載入 MNIST 數據的時候有:
>>> import mnist_loader >>> training_data, validation_data, test_data = \ ... mnist_loader.load_data_wrapper()到現在我們一直在使用 training_data 和 test_data,沒有用過 validation_data。validation_data 中包含了 $$10,000$$ 幅數字圖像,這些圖像和訓練數據集中的 $$50,000$$ 幅圖像以及測試數據集中的 $$10,000$$ 幅都不相同。我們會使用 validation_data 來防止過匹配。我們會使用和上面應用在 test_data 的策略。我們每個回合都計算在 validation_data 上的分類準確度。一旦分類準確度已經飽和,就停止訓練。這個策略被稱為 提前停止(Early stopping)。當然,實際應用中,我們不會立即知道什么時候準確度會飽和。相反,我們會一直訓練知道我們確信準確度已經飽和。
這里需要一些判定標準來確定什么時候停止。在我前面的圖中,將 $$280$$ 回合看成是飽和的地方。可能這有點太悲觀了。因為神經網絡有時候會訓練過程中處在一個平原期,然后又開始提升。如果在 $$400$$ 回合后,性能又開始提升(也許只是一些少量提升),那我也不會詫異。所以,在提前停止中采取一點激進的策略也是可以的。
為何要使用 validation_data 來替代 test_data 防止過匹配問題?實際上,這是一個更為一般的策略的一部分,這個一般的策略就是使用 validation_data 來衡量不同的超參數(如訓練回合,學習率,最好的網絡架構等等)的選擇的效果。我們使用這樣方法來找到超參數的合適值。因此,盡管到現在我并沒有提及這點,但其實本書前面已經稍微介紹了一些超參數選擇的方法。
當然,這對于我們前面關于 validation_data 取代 test_data 來防止過匹配的原因仍舊沒有回答。實際上,有一個更加一般的問題,就是為何用validation_data 取代 test_data 來設置更好的超參數?為了理解這點,想想當設置超參數時,我們想要嘗試許多不同的超參數選擇。如果我們設置超參數是基于 test_data 的話,可能最終我們就會得到過匹配于 test_data 的超參數。也就是說,我們可能會找到那些 符合 test_data 特點的超參數,但是網絡的性能并不能夠泛化到其他數據集合上。我們借助 validation_data 來克服這個問題。然后一旦獲得了想要的超參數,最終我們就使用 test_data 進行準確度測量。這給了我們在 test_data 上結果是一個網絡泛化能力真正的度量方式的信心。換言之,你可以將驗證集看成是一種特殊的訓練數據集能夠幫助我們學習好的超參數。這種尋找好的超參數的觀點有時候被稱為 hold out 方法,因為 validation_data 是從訓練集中保留出來的一部分。
在實際應用中,甚至在衡量了測試集上的性能后,我們可能也會改變想法并去嘗試另外的方法——也許是一種不同的網絡架構——這將會引入尋找新的超參數的的過程。如果我們這樣做,難道不會產生過匹配于 test_data 的困境么?我們是不是需要一種潛在無限大的數據集的回歸,這樣才能夠確信模型能夠泛化?去除這樣的疑惑其實是一個深刻而困難的問題。但是對實際應用的目標,我們不會擔心太多。相反,我們會繼續采用基于 training_data, validation_data, and test_data 的基本 hold out 方法。
我們已經研究了在使用 $$1,000$$ 幅訓練圖像時的過匹配問題。那么如果我們使用所有的訓練數據會發生什么?我們會保留所有其他的參數都一樣($$30$$ 個隱藏元,學習率 $$0.5$$,mini-batch 規模為 $$10$$),但是訓練回合為 $$30$$ 次。下圖展示了分類準確度在訓練和測試集上的變化情況。注意我們使用的測試數據,而不是驗證集合,為了讓結果看起來和前面的圖更方便比較。
如你所見,測試集和訓練集上的準確度相比我們使用 $$1,000$$ 個訓練數據時相差更小。特別地,在訓練數據上的最佳的分類準確度 97.86% 只比測試集上的 95.33% 準確度高一點點。而之前的例子中,這個差距是 17.73%!過匹配仍然發生了,但是已經減輕了不少。我們的網絡從訓練數據上更好地泛化到了測試數據上。一般來說,最好的降低過匹配的方式之一就是增加訓練樣本的量。有了足夠的訓練數據,就算是一個規模非常大的網絡也不大容易過匹配。不幸的是,訓練數據其實是很難或者很昂貴的資源,所以這不是一種太切實際的選擇。
規范化
增加訓練樣本的數量是一種減輕過匹配的方法。還有其他的一下方法能夠減輕過匹配的程度么?一種可行的方式就是降低網絡的規模。然而,大的網絡擁有一種比小網絡更強的潛力,所以這里存在一種應用冗余性的選項。
幸運的是,還有其他的技術能夠緩解過匹配,即使我們只有一個固定的網絡和固定的訓練集合。這種技術就是規范化。本節,我會給出一種最為常用的規范化手段——有時候被稱為權重下降(weight decay)或者 L2 規范化。L2 規范化的想法是增加一個額外的項到代價函數上,這個項叫做 規范化 項。下面是規范化交叉熵:
其中第一個項就是常規的交叉熵的表達式。第二個現在加入到就是所有權重的平方的和。然后使用一個因子 $$\lambdas/2n$$ 進行量化調整,其中 $$\lambda > 0$$ 可以成為 規范化參數,而 $$n$$ 就是訓練集合的大小。我們會在后面討論 $$\lambdas$$ 的選擇策略。需要注意的是,規范化項里面并不包含偏差。這點我們后面也會在講述。
當然,對其他的代價函數也可以進行規范化,例如二次代價函數。類似的規范化的形式如下:
兩者都可以寫成這樣:
其中 $$C_0$$ 是原始的代價函數。
直覺地看,規范化的效果是讓網絡傾向于學習小一點的權重,其他的東西都一樣的。大的權重只有能夠給出代價函數第一項足夠的提升時才被允許。換言之,規范化可以當做一種尋找小的權重和最小化原始的代價函數之間的折中。這兩部分之前相對的重要性就由 $$\lambda$$ 的值來控制了:$$\lambda$$ 越小,就偏向于最小化原始代價函數,反之,傾向于小的權重。
現在,對于這樣的折中為何能夠減輕過匹配還不是很清楚!但是,實際表現表明了這點。我們會在下一節來回答這個問題。但現在,我們來看看一個規范化的確減輕過匹配的例子。
為了構造這個例子,我們首先需要弄清楚如何將隨機梯度下降算法應用在一個規范化的神經網絡上。特別地,我們需要知道如何計算偏導數 $$\partial C/\partial w$$ 和 $$\partial C/\partial b$$。對公式(87)進行求偏導數得:
$$\partial C_0/\partial w$$ 和 $$\partial C_0/\partial b$$ 可以通過反向傳播進行計算,和上一章中的那樣。所以我們看到其實計算規范化的代價函數的梯度是很簡單的:僅僅需要反向傳播,然后加上 $$\frac{\lambda}{n} w$$ 得到所有權重的偏導數。而偏差的偏導數就不要變化,所以梯度下降學習規則不會發生變化:
權重的學習規則就變成:
這其實和通常的梯度下降學習規則相同歐諾個,除了乘了 $$1-\frac{\eta\lambda}{n}$$ 因子。這里就是權重下降的來源。粗看,這樣會導致權重會不斷下降到 $$0$$。但是實際不是這樣的,因為如果在原始代價函數中造成下降的話其他的項可能會讓權重增加。
好的,這就是梯度下降工作的原理。那么隨機梯度下降呢?正如在沒有規范化的隨機梯度下降中,我們可以通過平均 minibatch 中 $$m$$ 個訓練樣本來估計 $$\partial C_0/\partial w$$。因此,為了隨機梯度下降的規范化學習規則就變成(參考 方程(20))
其中后面一項是對 minibatch 中的訓練樣本 $$x$$ 進行求和,而 $$C_x$$ 是對每個訓練樣本的(無規范化的)代價。這其實和之前通常的隨機梯度下降的規則是一樣的,除了有一個權重下降的因子 $$1-\frac{\eta\lambda}{n}$$。最后,為了完備性,我給出偏差的規范化的學習規則。這當然是和我們之前的非規范化的情形一致了(參考公式(32))
這里也是對minibatch 中的訓練樣本 $$x$$ 進行求和的。
讓我們看看規范化給網絡帶來的性能提升吧。這里還會使用有 $$30$$ 個隱藏神經元、minibatch 為 $$10$$,學習率為 $$0.5$$,使用交叉熵的神經網絡。然而,這次我們會使用規范化參數為 $$\lambda = 0.1$$。注意在代碼中,我們使用的變量名字為 lmbda,這是因為在 Python 中 lambda 是關鍵字,尤其特定的作用。我也會使用 test_data,而不是 validation_data。不過嚴格地講,我們應當使用 validation_data的,因為前面已經講過了。這里我這樣做,是因為這會讓結果和非規范化的結果對比起來效果更加直接。你可以輕松地調整為 validation_data,你會發現有相似的結果。
>>> import mnist_loader >>> training_data, validation_data, test_data = \ ... mnist_loader.load_data_wrapper() >>> import network2 >>> net = network2.Network([784, 30, 10], cost=network2.CrossEntropyCost) >>> net.large_weight_initializer() >>> net.SGD(training_data[:1000], 400, 10, 0.5, ... evaluation_data=test_data, lmbda = 0.1, ... monitor_evaluation_cost=True, monitor_evaluation_accuracy=True, ... monitor_training_cost=True, monitor_training_accuracy=True)訓練集上的代價函數持續下降,和前面無規范化的情況一樣的規律:
但是這里測試集上的準確度是隨著回合次數持續增加的:
顯然,規范化的使用能夠解決過匹配的問題。而且,準確度相當搞了,最高處達到了 87.1%,相較于之前的 82.27%。因此,我們幾乎可以確信持續訓練會有更加好的結果。實驗起來,規范化讓網絡具有更好的泛化能力,顯著地減輕了過匹配的效果。
如果我們換成全部的訓練數據進行訓練呢?當然,我們之前已經看到過匹配在大規模的數據上其實不是那么明顯了。那規范化能不能起到相應的作用呢?保持超參數和之前一樣。不過我們這里需要改變規范化參數。原因在于訓練數據的大小已經從 $$n=1,000$$ 改成了 $$n=50,000$$,這個會改變權重下降因子 $$1-\frac{\eta\lambda}{n}$$。如果我們持續使用 $$\lambda = 0.1$$ 就會產生很小的權重下降,因此就將規范化的效果降低很多。我們通過將 $$\lambda = 5.0$$ 來補償這種下降。
好了,來訓練網絡,重新初始化權重:
>>> net.large_weight_initializer() >>> net.SGD(training_data, 30, 10, 0.5, ... evaluation_data=test_data, lmbda = 5.0, ... monitor_evaluation_accuracy=True, monitor_training_accuracy=True)我們得到:
這個結果很不錯。第一,我們在測試集上的分類準確度在使用規范化后有了提升,從 95.49% 到 96.49%。這是個很大的進步。第二,我們可以看到在訓練數據和測試數據上的結果之間的差距也更小了。這仍然是一個大的差距,不過我們已經顯著得到了本質上的降低過匹配的進步。
最后,我們看看在我們使用 $$100$$ 個隱藏元和規范化參數為 $$\lambda = 5.0$$ 相應的測試分類準確度。我不會給出詳細分析,就為了有趣,來看看我們使用一些技巧(交叉熵函數和 $$L2$$ 規范化)能夠達到多高的準確度。
>>> net = network2.Network([784, 100, 10], cost=network2.CrossEntropyCost) >>> net.large_weight_initializer() >>> net.SGD(training_data, 30, 10, 0.5, lmbda=5.0, ... evaluation_data=validation_data, ... monitor_evaluation_accuracy=True)最終在驗證集上的準確度達到了 97.92%。這是比 $$30$$ 個隱藏元的較大飛躍。實際上,稍微改變一點,$$60$$ 回合 $$\eta=0.1$$ 和 $$\lambda = 5.0$$。我們就突破了 98%,達到了 98.04% 的分類準確度。對于 $$152$$ 行代碼這個效果還真不錯!
我們討論了作為一種減輕過匹配和提高分類準確度的方式的規范化技術。實際上,這不是僅有的好處。實踐表明,在使用不同的(隨機)權重初始化進行多次 MNIST 網絡訓練的時候,我發現無規范化的網絡會偶然被限制住,明顯困在了代價函數的局部最優值處。結果就是不同的運行會給出相差很大的結果。對比看來,規范化的網絡能夠提供更容易復制的結果。
為何會這樣子?從經驗上看,如果代價函數是無規范化的,那么權重向量的長度可能會增長,而其他的東西都保持一樣。隨著時間的推移,這個會導致權重向量變得非常大。所以會使得權重向困在差不多方向上,因為由于梯度下降的改變當長度很大的時候僅僅會在那個方向發生微小的變化。我相信這個現象讓學習算法更難有效地探索權重空間,最終導致很難找到代價函數的最優值。
為何規范化可以幫助減輕過匹配
我們已經看到了規范化在實踐中能夠減少過匹配了。這是令人振奮的,不過,這背后的原因還不得而知!通常的說法是:小的權重在某種程度上,意味著更低的復雜性,也就給出了一種更簡單卻更強大的數據解釋,因此應該優先選擇。這雖然很簡短,不過暗藏了一些可能看起來會令人困惑的因素。讓我們將這個解釋細化,認真地研究一下。現在給一個簡單的數據集,我們為其建立模型:
這里我們其實在研究某種真實的現象,$$x$$ 和 $$y$$ 表示真實的數據。我們的目標是訓練一個模型來預測 $$y$$ 關于 $$x$$ 的函數。我們可以使用神經網絡來構建這個模型,但是我們先來個簡單的:用一個多項式來擬合數據。這樣做的原因其實是多項式相比神經網絡能夠讓事情變得更加清楚。一旦我們理解了多項式的場景,對于神經網絡可以如法炮制。現在,圖中有十個點,我們就可以找到唯一的 $$9$$ 階多項式 $$y=a_0x^9 + a_1x^8 + ... + a_9$$ 來完全擬合數據。下面是多項式的圖像:
I won't show the coefficients explicitly, although they are easy to find using a routine such as Numpy's polyfit
. You can view the exact form of the polynomial in the source code for the graph if you're curious. It's the function p(x)
defined starting on line 14 of the program which produces the graph.
這給出了一個完美的擬合。但是我們同樣也能夠使用線性模型 $$y=2x$$ 得到一個好的擬合效果:
哪個是更好的模型?哪個更可能是真的?還有哪個模型更可能泛化到其他的擁有同樣現象的樣本上?
這些都是很難回答的問題。實際上,我們如果沒有關于現象背后的信息的話,并不能確定給出上面任何一個問題的答案。但是讓我們考慮兩種可能的情況:(1)$$9$$ 階多項式實際上是完全描述了真實情況的模型,最終它能夠很好地泛化;(2)正確的模型是 $$y=2x$$,但是存在著由于測量誤差導致的額外的噪聲,使得模型不能夠準確擬合。
先驗假設無法說出哪個是正確的(或者,如果還有其他的情況出現)。邏輯上講,這些都可能出現。并且這不是易見的差異。在給出的數據上,兩個模型的表現其實是差不多的。但是假設我們想要預測對應于某個超過了圖中所有的 $$x$$ 的 $$y$$ 的值,在兩個模型給出的結果之間肯定有一個極大的差距,因為 $$9$$ 階多項式模型肯定會被 $$x^9$$ 主導,而線性模型只是線性的增長。
在科學中,一種觀點是我們除非不得已應該追隨更簡單的解釋。當我們找到一個簡單模型似乎能夠解釋很多數據樣本的時候,我們都會激動地認為發現了規律!總之,這看起來簡單的解決僅僅會是偶然出現的不大可能。我們懷疑模型必須表達出某些關于現象的內在的真理。如上面的例子,線性模型加噪聲肯定比多項式更加可能。所以如果簡單性是偶然出現的話就很令人詫異。因此我們會認為線性模型加噪聲表達除了一些潛在的真理。從這個角度看,多項式模型僅僅是學習到了局部噪聲的影響效果。所以盡管多是對于這些特定的數據點表現得很好。模型最終會在未知數據上的泛化上出現問題,所以噪聲線性模型具有更強大的預測能力。
讓我們從這個觀點來看神經網絡。假設神經網絡大多數有很小的權重,這最可能出現在規范化的網絡中。更小的權重意味著網絡的行為不會因為我們隨便改變了一個輸入而改變太大。這會讓規范化網絡學習局部噪聲的影響更加困難。將它看做是一種讓單個的證據不會影響網絡輸出太多的方式。相對的,規范化網絡學習去對整個訓練集中經常出現的證據進行反應。對比看,大權重的網絡可能會因為輸入的微小改變而產生比較大的行為改變。所以一個無規范化的網絡可以使用大的權重來學習包含訓練數據中的噪聲的大量信息的復雜模型。簡言之,規范化網絡受限于根據訓練數據中常見的模式來構造相對簡單的模型,而能夠抵抗訓練數據中的噪聲的特性影響。我們的想法就是這可以讓我們的網絡對看到的現象進行真實的學習,并能夠根據已經學到的知識更好地進行泛化。
所以,傾向于更簡單的解釋的想法其實會讓我們覺得緊張。人們有時候將這個想法稱為“奧卡姆剃刀原則”,然后就會熱情地將其當成某種科學原理來應用這個法則。但是,這就不是一個一般的科學原理。也沒有任何先驗的邏輯原因來說明簡單的解釋就比更為負責的解釋要好。實際上,有時候更加復雜的解釋其實是正確的。
讓我介紹兩個說明復雜正確的例子。在 $$1940$$ 年代,物理學家 Marcel Schein 發布了一個發現新粒子的聲明。而他工作的公司,GE,非常歡喜,就廣泛地推廣這個發現。但是物理學及 Hans Bethe 就有懷疑。Bethe 訪問了 Schein,看著那些展示 Schein 的新粒子的軌跡的盤子。但是在每個 plate 上,Bethe 都發現了某個說明數據需要被去除的問題。最后 Schein 展示給 Bethe 一個看起來很好的 plate。Bethe 說這可能就是一個統計上的僥幸。Schein 說,“使得,但是這個可能就是統計學,甚至是根據你自己的公式,也就是 $$1/5$$ 的概率。” Bethe 說:“但我們已經看過了這 $$5$$ 個plate 了”。最終,Schein 說:“但是在我的plate中,每個好的plate,每個好的場景,你使用了不同的理論(說它們是新的粒子)進行解釋,而我只有一種假設來解釋所有的 plate。” Bethe 回答說,“在你和我的解釋之間的唯一差別就是你的是錯的,而我所有的觀點是正確的。你單一的解釋是錯誤的,我的多重解釋所有都是正確的。”后續的工作證實了,Bethe 的想法是正確的而 Schein 粒子不再正確。
注意:這一段翻譯得很不好,請參考原文
第二個例子,在 $$1859$$ 年,天文學家 Urbain Le Verrier 觀察到水星并沒有按照牛頓萬有引力給出的軌跡進行運轉。與牛頓力學只有很小的偏差,那時候一些解釋就是牛頓力學需要一些微小的改動了。在 $$1916$$ 年愛因斯坦證明偏差用他的廣義相對論可以解釋得更好,這是一種和牛頓重力體系相差很大的理論,基于更復雜的數學。盡管引入了更多的復雜性,現如今愛因斯坦的解釋其實是正確的,而牛頓力學即使加入一些調整,仍舊是錯誤的。這部分因為我們知道愛因斯坦的理論不僅僅解釋了這個問題,還有很多其他牛頓力學無法解釋的問題也能夠完美解釋。另外,令人印象深刻的是,愛因斯坦的理論準確地給出了一些牛頓力學沒能夠預測到的顯現。但是這些令人印象深刻的現象其實在先前的時代是觀測不到的。如果一個人僅僅通過簡單性作為判斷合理模型的基礎,那么一些牛頓力學的改進理論可能會看起來更加合理一些。
從這些故事中可以讀出三點。第一,確定兩種解釋中哪個“更加簡單”其實是一件相當微妙的工作。第二,即使我們可以做出這樣一個判斷,簡單性也是一個使用時需要相當小心的指導!第三,對模型真正的測試不是簡單性,而是它在新場景中對新的活動中的預測能力。
所以,我們應當時時記住這一點,規范化的神經網絡常常能夠比非規范化的泛化能力更強,這只是一種實驗事實(empirical fact)。所以,本書剩下的內容,我們也會頻繁地使用規范化技術。我已經在上面講過了為何現在還沒有一個人能夠發展出一整套具有說服力的關于規范化可以幫助網絡泛化的理論解釋。實際上,研究者們不斷地在寫自己嘗試不同的規范化方法,然后看看哪種表現更好,嘗試理解為何不同的觀點表現的更好。所以你可以將規范化看做某種任意整合的技術。盡管其效果不錯,但我們并沒有一套完整的關于所發生情況的理解,僅僅是一些不完備的啟發式規則或者經驗。
這里也有更深的問題,這個問題也是有關科學的關鍵問題——我們如何泛化。規范化能夠給我們一種計算上的魔力幫助神經網絡更好地泛化,但是并不會帶來原理上理解的指導,甚至不會告訴我們什么樣的觀點才是最好的。
這個問題要追溯到歸納問題,最先由蘇格蘭哲學家大衛 休謨在 "An Enquiry Concerning Human Understanding" (1748) 中提出。在現代機器學習領域中歸納問題被 David Wolpert 和 William Macready 描述成無免費午餐定理。
這實在是令人困擾,因為在日常生活中,我們人類在泛化上表現很好。給一個兒童幾幅大象的圖片,他就能快速地學會認識其他的大象。當然,他們偶爾也會搞錯,很可能將一只犀牛誤認為大象,但是一般說來,這個過程會相當準確。所以我們有個系統——人的大腦——擁有超大量的自由變量。在受到僅僅少量的訓練圖像后,系統學會了在其他圖像的推廣。某種程度上,我們的大腦的規范化做得特別好!怎么做的?現在還不得而知。我期望若干年后,我們能夠發展出更加強大的技術來規范化神經網絡,最終這些技術會讓神經網絡甚至在小的訓練集上也能夠學到強大的泛化能力。
實際上,我們的網絡已經比我們預先期望的要好一些了。擁有 $$100$$ 個隱藏元的網絡會有接近 $$80,000$$ 個參數。我們的訓練集僅僅有 $$50,000$$ 幅圖像。這好像是用一個 $$80,000$$ 階的多項式來擬合 $$50,000$$ 個數據點。我們的網絡肯定會過匹配得很嚴重。但是,這樣的網絡實際上卻泛化得很好。為什么?這一點并沒有很好滴理解。這里有個猜想:梯度下降學習的動態有一種自規范化的效應。這真是一個意料之外的巧合,但也帶來了對于這種現象本質無知的不安。不過,我們還是會在后面依照這種實踐的觀點來應用規范化技術的。
神經網絡也是由于這點表現才更好一些。
現在我們回到前面留下來的一個細節:L2 規范化沒有限制偏差,以此作為本節的結論。當然了,對規范化的過程稍作調整就可以對偏差進行規范了。實踐看來,做出這樣的調整并不會對結果改變太多,所以,在某種程度上,對不對偏差進行規范化其實就是一種習慣了。然而,需要注意的是,有一個大的偏差并不會像大的權重那樣會讓神經元對輸入太過敏感。所以我們不需要對大的偏差所帶來的學習訓練數據的噪聲太過擔心。同時,允許大的偏差能夠讓網絡更加靈活——因為,大的偏差讓神經元更加容易飽和,這有時候是我們所要達到的效果。所以,我們通常不會對偏差進行規范化。
規范化的其他技術
除了 L2 外還有很多規范化技術。實際上,正是由于數量眾多,我這里也不回將所有的都列舉出來。在本節,我簡要地給出三種減輕過匹配的其他的方法:L1 規范化、dropout 和人工增加訓練樣本。我們不會像上面介紹得那么深入。其實,目的只是想讓讀者熟悉這些主要的思想,然后來體會一下規范化技術的多樣性。
L1 規范化:這個方法其實是在代價函數上加上一個權重絕對值的和:
直覺上看,這和 L2 規范化相似,懲罰大的權重,傾向于讓網絡的權重變小。當然,L1 規范化和 L2 規范化并不相同,所以我們不應該期望 L1 規范化是進行同樣的行為。讓我們來看看試著理解使用 L1 規范化和 L2 規范化所不同的地方。
首先,我們會研究一下代價函數的偏導數。對(95)求導我們有:
其中 $$sgn(w)$$ 就是 $$w$$ 的正負號。使用這個表達式,我們可以輕易地對反向傳播進行修改從而使用基于 L1 規范化的隨機梯度下降進行學習。對 L1 規范化的網絡進行更新的規則就是
其中和往常一樣,我們可以用 minibatch 的均值來估計 $$\partial C_0/\partial w$$。對比公式(93)的 L2 規范化,
在兩種情形下,規范化的效果就是縮小權重。這和我們想要讓權重不會太大的直覺目標相符。在 L1 規范化中,權重按照一個接近 $$0$$ 的常量進行縮小。在 L2 規范化中,權重同按照一個和 $$w$$ 成比例的量進行縮小的。所以,當一個特定的權重絕對值 $$|w|$$很大時,L1 規范化縮小得遠比 L2 規范化要小得多。而一個特定的權重絕對值 $$|w|$$很小時,L1 規范化權值要比 L2 規范化縮小得更大。最終的結果就是:L1 規范化傾向于聚集網絡的權重在相對少量的高重要度連接上,而其他權重就會被驅使向 $$0$$ 接近。
我在上面的討論中其實忽略了一個問題——在 $$w=0$$ 的時候,偏導數 $$\partial C/\partial w$$ 未定義。原因在于函數 $$|w|$$ 在 $$w=0$$ 時有個直角,事實上,導數是不存在的。不過也沒有關系。我們下面要做的就是應用無規范化的通常的梯度下降的規則在 $$w=0$$ 處。這應該不會有什么問題,直覺上看,規范化的效果就是縮小權重,顯然,不能對一個已經是 $$0$$ 的權重進行縮小了。更準確地說,我們將會使用方程(96)(97)并約定 $$sgn(0)=0$$。這樣就給出了一種緊致的規則來進行采用 L1 規范化的隨機梯度下降學習。
Dropout :Dropout 是一種相當激進的技術。和 L1、L2 規范化不同,dropout 并不依賴對代價函數的變更。而是,在 dropout 中,我們改變了網絡本身。讓我在給出為何工作的原理之前描述一下 dropout 基本的工作機制和所得到的結果。
假設我們嘗試訓練一個網絡:
特別地,假設我們有一個訓練數據 $$x$$ 和 對應的目標輸出 $$y$$。通常我們會通過在網絡中前向傳播 $$x$$ ,然后進行反向傳播來確定對梯度的共現。使用 dropout,這個過程就改了。我們會從隨機(臨時)地刪除網絡中的一半的神經元開始,讓輸入層和輸出層的神經元保持不變。在此之后,我們會得到最終的網絡。注意那些被 dropout 的神經元,即那些臨時性刪除的神經元,用虛圈表示在途中:
我們前向傳播輸入,通過修改后的網絡,然后反向傳播結果,同樣通過這個修改后的網絡。在 minibatch 的若干樣本上進行這些步驟后,我們對那些權重和偏差進行更新。然后重復這個過程,首先重置 dropout 的神經元,然后選擇新的隨機隱藏元的子集進行刪除,估計對一個不同的minibatch的梯度,然后更新權重和偏差。
通過不斷地重復,我們的網絡會學到一個權重和偏差的集合。當然,這些權重和偏差也是在一般的隱藏元被丟棄的情形下學到的。當我們實際運行整個網絡時,是指兩倍的隱藏元將會被激活。為了補償這個,我們將從隱藏元出去的權重減半了。
這個 dropout 過程可能看起來奇怪和ad hoc。為什么我們期待這樣的方法能夠進行規范化呢?為了解釋所發生的事,我希望你停下來想一下沒有 dropout 的訓練方式。特別地,想象一下我們訓練幾個不同的神經網絡,使用的同一個訓練數據。當然,網絡可能不是從同一初始狀態開始的,最終的結果也會有一些差異。出現這種情況時,我們可以使用一些平均或者投票的方式來確定接受哪個輸出。例如,如果我們訓練了五個網絡,其中三個被分類當做是 $$3$$,那很可能它就是 $$3$$。另外兩個可能就犯了錯誤。這種平均的方式通常是一種強大(盡管代價昂貴)的方式來減輕過匹配。原因在于不同的網絡可能會以不同的方式過匹配,平均法可能會幫助我們消除那樣的過匹配。
那么這和 dropout 有什么關系呢?啟發式地看,當我們丟掉不同的神經元集合時,有點像我們在訓練不同的神經網絡。所以,dropout 過程就如同大量不同網絡的效果的平均那樣。不同的網絡以不同的方式過匹配了,所以,dropout 的網絡會減輕過匹配。
一個相關的啟發式解釋在早期使用這項技術的論文中曾經給出:“因為神經元不能依賴其他神經元特定的存在,這個技術其實減少了復雜的互適應的神經元。所以,強制要學習那些在神經元的不同隨機子集中更加健壯的特征。”換言之,如果我們就愛那個神經網絡看做一個進行預測的模型的話,我們就可以將 dropout 看做是一種確保模型對于證據丟失健壯的方式。這樣看來,dropout 和 L1、L2 規范化也是有相似之處的,這也傾向于更小的權重,最后讓網絡對丟失個體連接的場景更加健壯。
當然,真正衡量 dropout 的方式在提升神經網絡性能上應用得相當成功。原始論文介紹了用來解決很多不同問題的技術。對我們來說,特別感興趣的是他們應用 dropout 在 MNIST 數字分類上,使用了一個和我們之前介紹的那種初級的前向神經網絡。這篇文章關注到最好的結果是在測試集上去得到 98.4% 的準確度。他們使用dropout 和 L2 規范化的組合將其提高到了 98.7%。類似重要的結果在其他不同的任務上也取得了一定的成效。dropout 已經在過匹配問題尤其突出的訓練大規模深度網絡中。
人工擴展訓練數據:我們前面看到了 MNIST 分類準確度在我們使用 $$1,000$$ 幅訓練圖像時候下降到了 $$80$$ 年代的準確度。這種情況并不奇怪,因為更少的訓練數據意味著我們的網絡所接觸到較少的人類手寫的數字中的變化。讓我們訓練 $$30$$ 個隱藏元的網絡,使用不同的訓練數據集,來看看性能的變化情況。我們使用 minibatch 大小為 $$10$$,學習率是 $$\eta=0.5$$,規范化參數是 $$\lambda=5.0$$,交叉熵代價函數。我們在全部訓練數據集合上訓練 30 個回合,然后會隨著訓練數據量的下降而成比例變化回合數。為了確保權重下降因子在訓練數據集上相同,我們會在全部訓練集上使用規范化參數為 $$\lambda = 5.0$$,然后在使用更小的訓練集的時候成比例地下降 $$\lambda$$ 值。
This and the next two graph are produced with the program more_data.py.
如你所見,分類準確度在使用更多的訓練數據時提升了很大。根據這個趨勢的話,提升會隨著更多的數據而不斷增加。當然,在訓練的后期我們看到學習過程已經進入了飽和狀態。然而,如果我們使用對數作為橫坐標的話,可以重畫此圖如下:
這看起來到了后面結束的地方,增加仍舊明顯。這表明如果我們使用大量更多的訓練數據——不妨設百萬或者十億級的手寫樣本——那么,我們可能會得到更好的性能,即使是用這樣的簡單網絡。
獲取更多的訓練樣本其實是很重要的想法。不幸的是,這個方法代價很大,在實踐中常常是很難達到的。不過,還有一種方法能夠獲得類似的效果,那就是進行人工的樣本擴展。假設我們使用一個 $$5$$ 的訓練樣本,
將其進行旋轉,比如說 $$15$$°:
這還是會被設別為同樣的數字的。但是在像素層級這和任何一幅在 MNIST 訓練數據中的圖像都不相同。所以將這樣的樣本加入到訓練數據中是很可能幫助我們學習有關手寫數字更多知識的方法。而且,顯然,我們不會就只對這幅圖進行人工的改造。我們可以在所有的 MNIST 訓練樣本上通過和多小的旋轉擴展訓練數據,然后使用擴展后的訓練數據來提升我們網絡的性能。
這個想法非常強大并且已經被廣發應用了。讓我們看看一些在 MNIST 上使用了類似的方法進行研究成果。其中一種他們考慮的網絡結構其實和我們已經使用過的類似——一個擁有 800 個隱藏元的前驅神經網絡,使用了交叉熵代價函數。在標準的 MNIST 訓練數據上運行這個網絡,得到了 98.4% 的分類準確度,其中使用了不只是旋轉還包括轉換和扭曲。通過在這個擴展后的數據集上的訓練,他們提升到了 98.9% 的準確度。然后還在“彈性扭曲(elastic distortion)”的數據上進行了實驗,這是一種特殊的為了模仿手部肌肉的隨機抖動的圖像扭曲方法。通過使用彈性扭曲擴展的數據,他們最終達到了 99.3% 的分類準確度。他們通過展示訓練數據的所有類型的變體來擴展了網絡的經驗。
Best Practices for Convolutional Neural Networks Applied to Visual Document Analysis, by Patrice Simard, Dave Steinkraus, and John Platt (2003).
這個想法的變體也可以用在提升手寫數字識別之外不同學習任務上的性能。一般就是通過應用反映真實世界變化的操作來擴展訓練數據。找到這些方法其實并不困難。例如,你要構建一個神經網絡來進行語音識別。我們人類甚至可以在有背景噪聲的情況下識別語音。所以你可以通過增加背景噪聲來擴展你的訓練數據。我們同樣能夠對其進行加速和減速來獲得相應的擴展數據。所以這是另外的一些擴展訓練數據的方法。這些技術并不總是有用——例如,其實與其在數據中加入噪聲,倒不如先對數據進行噪聲的清理,這樣可能更加有效。當然,記住可以進行數據的擴展,尋求應用的機會還是相當有價值的一件事。
練習
- 正如上面討論的那樣,一種擴展 MNIST 訓練數據的方式是用一些微小的旋轉。如果我們允許過大的旋轉,則會出現什么狀況呢?
大數據的旁白和對分類準確度的影響:讓我們看看神經網絡準確度隨著訓練集大小變化的情況:
假設,我們使用別的什么方法來進行分類。例如,我們使用 SVM。正如第一章介紹的那樣,不要擔心你熟不熟悉 SVM,我們不進行深入的討論。下面是 SVM 模型的準確度隨著訓練數據集的大小變化的情況:
可能第一件讓你吃驚的是神經網絡在每個訓練規模下性能都超過了 SVM。這很好,盡管你對細節和原理可能不太了解,因為我們只是直接從 scikit-learn 中直接調用了這個方法,而對神經網絡已經深入講解了很多。更加微妙和有趣的現象其實是如果我們訓練 SVM 使用 $$50,000$$ 幅圖像,實際上 SVM 已經能夠超過我們使用 $$5,000$$ 幅圖像的準確度。換言之,更多的訓練數據可以補償不同的機器學習算法的差距。
還有更加有趣的現象也出現了。假設我們試著用兩種機器學習算法去解決問題,算法 $$A$$ 和算法 $$B$$。有時候出現,算法 $$A$$ 在一個訓練集合上超過 算法 $$B$$,卻在另一個訓練集上弱于算法 $$B$$。上面我們并沒有看到這個情況——因為這要求兩幅圖有交叉的點——這里并沒有。對“算法 A 是不是要比算法 $$B$$ 好?”正確的反應應該是“你在使用什么訓練集合?”
在進行開發時或者在讀研究論文時,這都是需要記住的事情。很多論文聚焦在尋找新的技術來給出標準數據集上更好的性能。“我們的超贊的技術在標準測試集 $$Y$$ 上給出了百分之 $$X$$ 的性能提升。”這是通常的研究聲明。這樣的聲明通常比較有趣,不過也必須被理解為僅僅在特定的訓練數據機上的應用效果。那些給出基準數據集的人們會擁有更大的研究經費支持,這樣能夠獲得更好的訓練數據。所以,很可能他們由于超贊的技術的性能提升其實在更大的數據集合上就喪失了。換言之,人們標榜的提升可能就是歷史的偶然。所以需要記住的特別是在實際應用中,我們想要的是更好的算法和更好的訓練數據。尋找更好的算法很重,不過需要確保你在此過程中,沒有放棄對更多更好的數據的追求。
問題
- 研究問題:我們的機器學習算法在非常大的數據集上如何進行?對任何給定的算法,其實去定義一個隨著訓練數據規模變化的漸近的性能是一種很自然的嘗試。一種簡單粗暴的方法就是簡單地進行上面圖中的趨勢分析,然后將圖像推進到無窮大。而對此想法的反駁是曲線本身會給出不同的漸近性能。你能夠找到擬合某些特定類別曲線的理論上的驗證方法嗎?如果可以,比較不同的機器學習算法的漸近性能。
總結:我們現在已經介紹完了過匹配和規范化。當然,我們重回這個問題。正如我們前面講過的那樣,尤其是計算機越來越強大,我們有訓練更大的網絡的能力時。過匹配是神經網絡中一個主要的問題。我們有迫切的愿望來開發出強大的規范化技術來減輕過匹配,所以,這也是當前極其熱門的研究方向之一。
===============================================================================================================================
權重初始化
創建了神經網絡后,我們需要進行權重和偏差的初始化。到現在,我們一直是根據在第一章中介紹的那樣進行初始化。提醒你一下,之前的方式就是根據獨立的均值為 $$0$$,標準差為 $$1$$ 的高斯隨機變量隨機采樣作為權重和偏差的初始值。這個方法工作的還不錯,但是非常 ad hoc,所以我們需要尋找一些更好的方式來設置我們網絡的初始化權重和偏差,這對于幫助網絡學習速度的提升很有價值。
結果表明,我們可以比使用正規化的高斯分布效果更好。為什么?假設我們使用一個很多的輸入神經元,比如說 $$1000$$。假設,我們已經使用正規化的高斯分布初始化了連接第一隱藏層的權重。現在我將注意力集中在這一層的連接權重上,忽略網絡其他部分:
我們為了簡化,假設,我們使用訓練樣本 x 其中一半的神經元值為 $$0$$,另一半為 $$1$$。下面的觀點也是可以更加廣泛地應用,但是你可以從特例中獲得背后的思想。讓我們考慮帶權和 $$z=\sum_j w_j x_j + b$$ 的隱藏元輸入。其中 $$500$$ 個項消去了,因為對應的輸入 $$x_j=0$$。所以 $$z$$ 是 $$501$$ 個正規化的高斯隨機變量的和,包含 $$500$$ 個權重項和額外的 $$1$$ 個偏差項。因此 $$z$$ 本身是一個均值為 $$0$$ 標準差為 $$\sqrt{501}\approx 22.4$$ 的分布。$$z$$ 其實有一個非常寬的高斯分布,不是非常尖的形狀:
尤其是,我們可以從這幅圖中看出 $$|z|$$ 會變得非常的大,比如說 $$z\gg1$$ 或者 $$z\ll 1$$。如果是這樣,輸出 $$\sigma(z)$$ 就會接近 $$1$$ 或者 $$0$$。也就表示我們的隱藏元會飽和。所以當出現這樣的情況時,在權重中進行微小的調整僅僅會給隱藏元的激活值帶來極其微弱的改變。而這種微弱的改變也會影響網絡中剩下的神經元,然后會帶來相應的代價函數的改變。結果就是,這些權重在我們進行梯度下降算法時會學習得非常緩慢。這其實和我們前面討論的問題差不多,前面的情況是輸出神經元在錯誤的值上飽和導致學習的下降。我們之前通過代價函數的選擇解決了前面的問題。不幸的是,盡管那種方式在輸出神經元上有效,但對于隱藏元的飽和卻一點作用都沒有。
我已經研究了第一隱藏層的權重輸入。當然,類似的論斷也對后面的隱藏層有效:如果權重也是用正規化的高斯分布進行初始化,那么激活值將會接近 $$0$$ 或者 $$1$$,學習速度也會相當緩慢。
還有可以幫助我們進行更好地初始化么,能夠避免這種類型的飽和,最終避免學習速度的下降?假設我們有一個有 $$n_{in}$$ 個輸入權重的神經元。我們會使用均值為 $$0$$ 標準差為 $$1/\sqrt{n_{in}}$$ 的高斯分布初始化這些權重。也就是說,我們會向下擠壓高斯分布,讓我們的神經元更不可能飽和。我們會繼續使用均值為 $$0$$ 標準差為 $$1$$ 的高斯分布來對偏差進行初始化,后面會告訴你原因。有了這些設定,帶權和 $$z=\sum_j w_j x_j + b$$ 仍然是一個均值為 $$0$$ 不過有很陡的峰頂的高斯分布。假設,我們有 $$500$$ 個值為 $$0$$ 的輸入和$$500$$ 個值為 $$1$$ 的輸入。那么很容證明 $$z$$ 是服從均值為 $$0$$ 標準差為 $$\sqrt{3/2} = 1.22$$ 的高斯分布。這圖像要比以前陡得多,所以即使我已經對橫坐標進行壓縮為了進行更直觀的比較:
這樣的一個神經元更不可能飽和,因此也不大可能遇到學習速度下降的問題。
練習
- 驗證 $$z=\sum_j w_j x_j + b$$ 標準差為 $$\sqrt{3/2}$$。下面兩點可能會有幫助:(a) 獨立隨機變量的和的方差是每個獨立隨即便方差的和;(b)方差是標準差的平方。
我在上面提到,我們使用同樣的方式對偏差進行初始化,就是使用均值為 $$0$$ 標準差為 $$1$$ 的高斯分布來對偏差進行初始化。這其實是可行的,因為這樣并不會讓我們的神經網絡更容易飽和。實際上,其實已經避免了飽和的問題的話,如何初始化偏差影響不大。有些人將所有的偏差初始化為 $$0$$,依賴梯度下降來學習合適的偏差。但是因為差別不是很大,我們后面還會按照前面的方式來進行初始化。
讓我們在 MNIST 數字分類任務上比較一下新舊兩種權重初始化方式。同樣,還是使用 $$30$$ 個隱藏元,minibatch 的大小為 $$30$$,規范化參數 $$\lambda=5.0$$,然后是交叉熵代價函數。我們將學習率從 $$\eta=0.5$$ 調整到 $$0.1$$,因為這樣會讓結果在圖像中表現得更加明顯。我們先使用舊的初始化方法訓練:
>>> import mnist_loader >>> training_data, validation_data, test_data = \ ... mnist_loader.load_data_wrapper() >>> import network2 >>> net = network2.Network([784, 30, 10], cost=network2.CrossEntropyCost) >>> net.large_weight_initializer() >>> net.SGD(training_data, 30, 10, 0.1, lmbda = 5.0, ... evaluation_data=validation_data, ... monitor_evaluation_accuracy=True)我們也使用新方法來進行權重的初始化。這實際上還要更簡單,因為 network2's 默認方式就是使用新的方法。這意味著我們可以丟掉 net.large_weight_initializer() 調用:
>>> net = network2.Network([784, 30, 10], cost=network2.CrossEntropyCost) >>> net.SGD(training_data, 30, 10, 0.1, lmbda = 5.0, ... evaluation_data=validation_data, ... monitor_evaluation_accuracy=True)將結果用圖展示出來,就是:
兩種情形下,我們在 96% 的準確度上重合了。最終的分類準確度幾乎完全一樣。但是新的初始化技術帶來了速度的提升。在第一種初始化方式的分類準確度在 87% 一下,而新的方法已經幾乎達到了 93%。看起來的情況就是我們新的關于權重初始化的方式將訓練帶到了一個新的境界,讓我們能夠更加快速地得到好的結果。同樣的情況在 $$100$$ 個神經元的設定中也出現了:
在這個情況下,兩個曲線并沒有重合。然而,我做的實驗發現了其實就在一些額外的回合后(這里沒有展示)準確度其實也是幾乎相同的。所以,基于這些實驗,看起來提升的權重初始化僅僅會加快訓練,不會改變網絡的性能。然而,在第四張,我們會看到一些例子里面使用 $$1/\sqrt{n_{in}}$$ 權重初始化的長期運行的結果要顯著更優。因此,不僅僅能夠帶來訓練速度的加快,有時候在最終性能上也有很大的提升。
$$1/\sqrt{n_{in}}$$ 的權重初始化方法幫助我們提升了神經網絡學習的方式。其他的權重初始化技術同樣也有,很多都是基于這個基本的思想。我不會在這里給出其他的方法,因為 $$1/\sqrt{n_{in}}$$ 已經可以工作得很好了。如果你對另外的思想感興趣,我推薦你看看在 $$2012$$ 年的 Yoshua Bengio 的論文的 $$14$$ 和 $$15$$ 頁,以及相關的參考文獻。
Practical Recommendations for Gradient-Based Training of Deep Architectures, by Yoshua Bengio (2012).
問題
- 將規范化和改進的權重初始化方法結合使用 L2 規范化有時候會自動給我們一些類似于新的初始化方法的東西。假設我們使用舊的初始化權重的方法。考慮一個啟發式的觀點:(1)假設$$\lambda$$ 不太小,訓練的第一回合將會幾乎被權重下降統治。;(2)如果 $$\eta\lambda \ll n$$,權重會按照因子 $$exp(-\eta\lambda/m)$$ 每回合下降;(3)假設 $$\lambda$$ 不太大,權重下降會在權重降到 $$1/\sqrt{n}$$ 的時候保持住,其中 $$n$$ 是網絡中權重的個數。用論述這些條件都已經滿足本節給出的例子。
再看手寫識別問題:代碼
讓我們實現本章討論過的這些想法。我們將寫出一個新的程序,network2.py,這是一個對第一章中開發的 network.py 的改進版本。如果你沒有仔細看過 network.py,那你可能會需要重讀前面關于這段代碼的討論。僅僅 $$74$$ 行代碼,也很易懂。
和 network.py 一樣,主要部分就是 Network 類了,我們用這個來表示神經網絡。使用一個 sizes 的列表來對每個對應層進行初始化,默認使用交叉熵作為代價 cost 參數:
class Network(object):def __init__(self, sizes, cost=CrossEntropyCost):self.num_layers = len(sizes)self.sizes = sizesself.default_weight_initializer()self.cost=cost__init__ 方法的和 network.py 中一樣,可以輕易弄懂。但是下面兩行是新的,我們需要知道他們到底做了什么。
我們先看看 default_weight_initializer 方法,使用了我們新式改進后的初始化權重方法。如我們已經看到的,使用了均值為 $$0$$ 而標準差為 $$1/\sqrt{n}$$,$$n$$ 為對應的輸入連接個數。我們使用均值為 $$0$$ 而標準差為 $$1$$ 的高斯分布來初始化偏差。下面是代碼:
def default_weight_initializer(self):self.biases = [np.random.randn(y, 1) for y in self.sizes[1:]]self.weights = [np.random.randn(y, x)/np.sqrt(x) for x, y in zip(self.sizes[:-1], self.sizes[1:])]為了理解這段代碼,需要知道 np 就是進行線性代數運算的 Numpy 庫。我們在程序的開頭會 import Numpy。同樣我們沒有對第一層的神經元的偏差進行初始化。因為第一層其實是輸入層,所以不需要引入任何的偏差。我們在 network.py 中做了完全一樣的事情。
作為 default_weight_initializer 的補充,我們同樣包含了一個 large_weight_initializer 方法。這個方法使用了第一章中的觀點初始化了權重和偏差。代碼也就僅僅是和default_weight_initializer差了一點點了:
def large_weight_initializer(self):self.biases = [np.random.randn(y, 1) for y in self.sizes[1:]]self.weights = [np.random.randn(y, x) for x, y in zip(self.sizes[:-1], self.sizes[1:])]我將 larger_weight_initializer 方法包含進來的原因也就是使得跟第一章的結果更容易比較。我并沒有考慮太多的推薦使用這個方法的實際情景。
初始化方法 __init__ 中的第二個新的東西就是我們初始化了 cost 屬性。為了理解這個工作的原理,讓我們看一下用來表示交叉熵代價的類:
class CrossEntropyCost(object): @staticmethoddef fn(a, y):return np.sum(np.nan_to_num(-y*np.log(a)-(1-y)*np.log(1-a))) @staticmethoddef delta(z, a, y):return (a-y)讓我們分解一下。第一個看到的是:即使使用的是交叉熵,數學上看,就是一個函數,這里我們用 Python 的類而不是 Python 函數實現了它。為什么這樣做呢?答案就是代價函數在我們的網絡中扮演了兩種不同的角色。明顯的角色就是代價是輸出激活值 $$a$$ 和目標輸出 $$y$$ 差距優劣的度量。這個角色通過 CrossEntropyCost.fn 方法來扮演。(注意,np.nan_to_num 調用確保了 Numpy 正確處理接近 $$0$$ 的對數值)但是代價函數其實還有另一個角色。回想第二章中運行反向傳播算法時,我們需要計算網絡輸出誤差,$$\delta^L$$。這種形式的輸出誤差依賴于代價函數的選擇:不同的代價函數,輸出誤差的形式就不同。對于交叉熵函數,輸出誤差就如公式(66)所示:
所以,我們定義了第二個方法,CrossEntropyCost.delta,目的就是讓網絡知道如何進行輸出誤差的計算。然后我們將這兩個組合在一個包含所有需要知道的有關代價函數信息的類中。
類似地,network2.py 還包含了一個表示二次代價函數的類。這個是用來和第一章的結果進行對比的,因為后面我們幾乎都在使用交叉函數。代碼如下。QuadraticCost.fn 方法是關于網絡輸出 $$a$$ 和目標輸出 $$y$$ 的二次代價函數的直接計算結果。由 QuadraticCost.delta 返回的值就是二次代價函數的誤差。
class QuadraticCost(object): @staticmethoddef fn(a, y):return 0.5*np.linalg.norm(a-y)**2 @staticmethoddef delta(z, a, y):return (a-y) * sigmoid_prime(z)現在,我們理解了 network2.py 和 network.py 兩個實現之間的主要差別。都是很簡單的東西。還有一些更小的變動,下面我們會進行介紹,包含 L2 規范化的實現。在講述規范化之前,我們看看 network2.py 完整的實現代碼。你不需要太仔細地讀遍這些代碼,但是對整個結構尤其是文檔中的內容的理解是非常重要的,這樣,你就可以理解每段程序所做的工作。當然,你也可以隨自己意愿去深入研究!如果你迷失了理解,那么請讀讀下面的講解,然后再回到代碼中。不多說了,給代碼:
"""network2.py ~~~~~~~~~~~~~~An improved version of network.py, implementing the stochastic gradient descent learning algorithm for a feedforward neural network. Improvements include the addition of the cross-entropy cost function, regularization, and better initialization of network weights. Note that I have focused on making the code simple, easily readable, and easily modifiable. It is not optimized, and omits many desirable features."""#### Libraries # Standard library import json import random import sys# Third-party libraries import numpy as np#### Define the quadratic and cross-entropy cost functionsclass QuadraticCost(object): @staticmethoddef fn(a, y):"""Return the cost associated with an output ``a`` and desired output``y``."""return 0.5*np.linalg.norm(a-y)**2 @staticmethoddef delta(z, a, y):"""Return the error delta from the output layer."""return (a-y) * sigmoid_prime(z)class CrossEntropyCost(object): @staticmethoddef fn(a, y):"""Return the cost associated with an output ``a`` and desired output``y``. Note that np.nan_to_num is used to ensure numericalstability. In particular, if both ``a`` and ``y`` have a 1.0in the same slot, then the expression (1-y)*np.log(1-a)returns nan. The np.nan_to_num ensures that that is convertedto the correct value (0.0)."""return np.sum(np.nan_to_num(-y*np.log(a)-(1-y)*np.log(1-a))) @staticmethoddef delta(z, a, y):"""Return the error delta from the output layer. Note that theparameter ``z`` is not used by the method. It is included inthe method's parameters in order to make the interfaceconsistent with the delta method for other cost classes."""return (a-y)#### Main Network class class Network(object):def __init__(self, sizes, cost=CrossEntropyCost):"""The list ``sizes`` contains the number of neurons in the respectivelayers of the network. For example, if the list was [2, 3, 1]then it would be a three-layer network, with the first layercontaining 2 neurons, the second layer 3 neurons, and thethird layer 1 neuron. The biases and weights for the networkare initialized randomly, using``self.default_weight_initializer`` (see docstring for thatmethod)."""self.num_layers = len(sizes)self.sizes = sizesself.default_weight_initializer()self.cost=costdef default_weight_initializer(self):"""Initialize each weight using a Gaussian distribution with mean 0and standard deviation 1 over the square root of the number ofweights connecting to the same neuron. Initialize the biasesusing a Gaussian distribution with mean 0 and standarddeviation 1.Note that the first layer is assumed to be an input layer, andby convention we won't set any biases for those neurons, sincebiases are only ever used in computing the outputs from laterlayers."""self.biases = [np.random.randn(y, 1) for y in self.sizes[1:]]self.weights = [np.random.randn(y, x)/np.sqrt(x)for x, y in zip(self.sizes[:-1], self.sizes[1:])]def large_weight_initializer(self):"""Initialize the weights using a Gaussian distribution with mean 0and standard deviation 1. Initialize the biases using aGaussian distribution with mean 0 and standard deviation 1.Note that the first layer is assumed to be an input layer, andby convention we won't set any biases for those neurons, sincebiases are only ever used in computing the outputs from laterlayers.This weight and bias initializer uses the same approach as inChapter 1, and is included for purposes of comparison. Itwill usually be better to use the default weight initializerinstead."""self.biases = [np.random.randn(y, 1) for y in self.sizes[1:]]self.weights = [np.random.randn(y, x)for x, y in zip(self.sizes[:-1], self.sizes[1:])]def feedforward(self, a):"""Return the output of the network if ``a`` is input."""for b, w in zip(self.biases, self.weights):a = sigmoid(np.dot(w, a)+b)return adef SGD(self, training_data, epochs, mini_batch_size, eta,lmbda = 0.0,evaluation_data=None,monitor_evaluation_cost=False,monitor_evaluation_accuracy=False,monitor_training_cost=False,monitor_training_accuracy=False):"""Train the neural network using mini-batch stochastic gradientdescent. The ``training_data`` is a list of tuples ``(x, y)``representing the training inputs and the desired outputs. Theother non-optional parameters are self-explanatory, as is theregularization parameter ``lmbda``. The method also accepts``evaluation_data``, usually either the validation or testdata. We can monitor the cost and accuracy on either theevaluation data or the training data, by setting theappropriate flags. The method returns a tuple containing fourlists: the (per-epoch) costs on the evaluation data, theaccuracies on the evaluation data, the costs on the trainingdata, and the accuracies on the training data. All values areevaluated at the end of each training epoch. So, for example,if we train for 30 epochs, then the first element of the tuplewill be a 30-element list containing the cost on theevaluation data at the end of each epoch. Note that the listsare empty if the corresponding flag is not set."""if evaluation_data: n_data = len(evaluation_data)n = len(training_data)evaluation_cost, evaluation_accuracy = [], []training_cost, training_accuracy = [], []for j in xrange(epochs):random.shuffle(training_data)mini_batches = [training_data[k:k+mini_batch_size]for k in xrange(0, n, mini_batch_size)]for mini_batch in mini_batches:self.update_mini_batch(mini_batch, eta, lmbda, len(training_data))print "Epoch %s training complete" % jif monitor_training_cost:cost = self.total_cost(training_data, lmbda)training_cost.append(cost)print "Cost on training data: {}".format(cost)if monitor_training_accuracy:accuracy = self.accuracy(training_data, convert=True)training_accuracy.append(accuracy)print "Accuracy on training data: {} / {}".format(accuracy, n)if monitor_evaluation_cost:cost = self.total_cost(evaluation_data, lmbda, convert=True)evaluation_cost.append(cost)print "Cost on evaluation data: {}".format(cost)if monitor_evaluation_accuracy:accuracy = self.accuracy(evaluation_data)evaluation_accuracy.append(accuracy)print "Accuracy on evaluation data: {} / {}".format(self.accuracy(evaluation_data), n_data)printreturn evaluation_cost, evaluation_accuracy, \training_cost, training_accuracydef update_mini_batch(self, mini_batch, eta, lmbda, n):"""Update the network's weights and biases by applying gradientdescent using backpropagation to a single mini batch. The``mini_batch`` is a list of tuples ``(x, y)``, ``eta`` is thelearning rate, ``lmbda`` is the regularization parameter, and``n`` is the total size of the training data set."""nabla_b = [np.zeros(b.shape) for b in self.biases]nabla_w = [np.zeros(w.shape) for w in self.weights]for x, y in mini_batch:delta_nabla_b, delta_nabla_w = self.backprop(x, y)nabla_b = [nb+dnb for nb, dnb in zip(nabla_b, delta_nabla_b)]nabla_w = [nw+dnw for nw, dnw in zip(nabla_w, delta_nabla_w)]self.weights = [(1-eta*(lmbda/n))*w-(eta/len(mini_batch))*nwfor w, nw in zip(self.weights, nabla_w)]self.biases = [b-(eta/len(mini_batch))*nbfor b, nb in zip(self.biases, nabla_b)]def backprop(self, x, y):"""Return a tuple ``(nabla_b, nabla_w)`` representing thegradient for the cost function C_x. ``nabla_b`` and``nabla_w`` are layer-by-layer lists of numpy arrays, similarto ``self.biases`` and ``self.weights``."""nabla_b = [np.zeros(b.shape) for b in self.biases]nabla_w = [np.zeros(w.shape) for w in self.weights]# feedforwardactivation = xactivations = [x] # list to store all the activations, layer by layerzs = [] # list to store all the z vectors, layer by layerfor b, w in zip(self.biases, self.weights):z = np.dot(w, activation)+bzs.append(z)activation = sigmoid(z)activations.append(activation)# backward passdelta = (self.cost).delta(zs[-1], activations[-1], y)nabla_b[-1] = deltanabla_w[-1] = np.dot(delta, activations[-2].transpose())# Note that the variable l in the loop below is used a little# differently to the notation in Chapter 2 of the book. Here,# l = 1 means the last layer of neurons, l = 2 is the# second-last layer, and so on. It's a renumbering of the# scheme in the book, used here to take advantage of the fact# that Python can use negative indices in lists.for l in xrange(2, self.num_layers):z = zs[-l]sp = sigmoid_prime(z)delta = np.dot(self.weights[-l+1].transpose(), delta) * spnabla_b[-l] = deltanabla_w[-l] = np.dot(delta, activations[-l-1].transpose())return (nabla_b, nabla_w)def accuracy(self, data, convert=False):"""Return the number of inputs in ``data`` for which the neuralnetwork outputs the correct result. The neural network'soutput is assumed to be the index of whichever neuron in thefinal layer has the highest activation.The flag ``convert`` should be set to False if the data set isvalidation or test data (the usual case), and to True if thedata set is the training data. The need for this flag arisesdue to differences in the way the results ``y`` arerepresented in the different data sets. In particular, itflags whether we need to convert between the differentrepresentations. It may seem strange to use differentrepresentations for the different data sets. Why not use thesame representation for all three data sets? It's done forefficiency reasons -- the program usually evaluates the coston the training data and the accuracy on other data sets.These are different types of computations, and using differentrepresentations speeds things up. More details on therepresentations can be found inmnist_loader.load_data_wrapper."""if convert:results = [(np.argmax(self.feedforward(x)), np.argmax(y))for (x, y) in data]else:results = [(np.argmax(self.feedforward(x)), y)for (x, y) in data]return sum(int(x == y) for (x, y) in results)def total_cost(self, data, lmbda, convert=False):"""Return the total cost for the data set ``data``. The flag``convert`` should be set to False if the data set is thetraining data (the usual case), and to True if the data set isthe validation or test data. See comments on the similar (butreversed) convention for the ``accuracy`` method, above."""cost = 0.0for x, y in data:a = self.feedforward(x)if convert: y = vectorized_result(y)cost += self.cost.fn(a, y)/len(data)cost += 0.5*(lmbda/len(data))*sum(np.linalg.norm(w)**2 for w in self.weights)return costdef save(self, filename):"""Save the neural network to the file ``filename``."""data = {"sizes": self.sizes,"weights": [w.tolist() for w in self.weights],"biases": [b.tolist() for b in self.biases],"cost": str(self.cost.__name__)}f = open(filename, "w")json.dump(data, f)f.close()#### Loading a Network def load(filename):"""Load a neural network from the file ``filename``. Returns aninstance of Network."""f = open(filename, "r")data = json.load(f)f.close()cost = getattr(sys.modules[__name__], data["cost"])net = Network(data["sizes"], cost=cost)net.weights = [np.array(w) for w in data["weights"]]net.biases = [np.array(b) for b in data["biases"]]return net#### Miscellaneous functions def vectorized_result(j):"""Return a 10-dimensional unit vector with a 1.0 in the j'th positionand zeroes elsewhere. This is used to convert a digit (0...9)into a corresponding desired output from the neural network."""e = np.zeros((10, 1))e[j] = 1.0return edef sigmoid(z):"""The sigmoid function."""return 1.0/(1.0+np.exp(-z))def sigmoid_prime(z):"""Derivative of the sigmoid function."""return sigmoid(z)*(1-sigmoid(z))有個更加有趣的變動就是在代碼中增加了 L2 規范化。盡管這是一個主要的概念上的變動,在實現中其實相當簡單。對大部分情況,僅僅需要傳遞參數 lmbda 到不同的方法中,主要是 Network.SGD 方法。實際上的工作就是一行代碼的事在 Network.update_mini_batch 的倒數第四行。這就是我們改動梯度下降規則來進行權重下降的地方。盡管改動很小,但其對結果影響卻很大!
其實這種情況在神經網絡中實現一些新技術的常見現象。我們花費了近千字的篇幅來討論規范化。概念的理解非常微妙困難。但是添加到程序中的時候卻如此簡單。精妙復雜的技術可以通過微小的代碼改動就可以實現了。
另一個微小卻重要的改動是隨機梯度下降方法的幾個標志位的增加。這些標志位讓我們可以對在代價和準確度的監控變得可能。這些標志位默認是 False 的,但是在我們例子中,已經被置為 True 來監控 Network 的性能。另外,network2.py 中的 Network.SGD 方法返回了一個四元組來表示監控的結果。我們可以這樣使用:
>>> evaluation_cost, evaluation_accuracy, ... training_cost, training_accuracy = net.SGD(training_data, 30, 10, 0.5, ... lmbda = 5.0, ... evaluation_data=validation_data, ... monitor_evaluation_accuracy=True, ... monitor_evaluation_cost=True, ... monitor_training_accuracy=True, ... monitor_training_cost=True)所以,比如 evaluation_cost 將會是一個 $$30$$ 個元素的列表其中包含了每個回合在驗證集合上的代價函數值。這種類型的信息在理解網絡行為的過程中特別有用。比如,它可以用來畫出展示網絡隨時間學習的狀態。其實,這也是我在前面的章節中展示性能的方式。然而要注意的是如果任何標志位都沒有設置的話,對應的元組中的元素就是空列表。
另一個增加項就是在 Network.save 方法中的代碼,用來將 Network 對象保存在磁盤上,還有一個載回內存的函數。這兩個方法都是使用 JSON 進行的,而非 Python 的 pickle 或者 cPickle 模塊——這些通常是 Python 中常見的保存和裝載對象的方法。使用 JSON 的原因是,假設在未來某天,我們想改變 Network 類來允許非 sigmoid 的神經元。對這個改變的實現,我們最可能是改變在 Network.__init__ 方法中定義的屬性。如果我們簡單地 pickle 對象,會導致 load 函數出錯。使用 JSON 進行序列化可以顯式地讓老的 Network 仍然能夠 load。
其他也還有一些微小的變動。但是那些只是 network.py 的微調。結果就是把程序從 $$74$$ 行增長到了 $$152$$ 行。
問題
- 更改上面的代碼來實現 L1 規范化,使用 L1 規范化使用 $$30$$ 個隱藏元的神經網絡對 MNIST 數字進行分類。你能夠找到一個規范化參數使得比無規范化效果更好么?
- 看看 network.py 中的 Network.cost_derivative 方法。這個方法是為二次代價函數寫的。怎樣修改可以用于交叉熵代價函數上?你能不能想到可能在交叉熵函數上遇到的問題?在 network2.py 中,我們已經去掉了 Network.cost_derivative 方法,將其集成進了 CrossEntropyCost.delta 方法中。請問,這樣是如何解決你已經發現的問題的?
總結
以上是生活随笔為你收集整理的进神经网络的学习方式(译文)----中的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 开始使用WebRTC
- 下一篇: 杭州5.8万人面临饮水难 一村庄居民一月