进神经网络的学习方式(译文)----中
過匹配和規(guī)范化
諾貝爾獎得主美籍意大利裔物理學(xué)家恩里科·費(fèi)米曾被問到他對一個同僚提出的嘗試解決一個重要的未解決物理難題的數(shù)學(xué)模型。模型和實驗非常匹配,但是費(fèi)米卻對其產(chǎn)生了懷疑。他問模型中需要設(shè)置的自由參數(shù)有多少個。答案是“4”。費(fèi)米回答道:“我記得我的朋友約翰·馮·諾依曼過去常說,有四個參數(shù),我可以模擬一頭大象,而有五個參數(shù),我還能讓他卷鼻子。”
這里,其實是說擁有大量的自由參數(shù)的模型能夠描述特別神奇的現(xiàn)象。即使這樣的模型能夠很好的擬合已有的數(shù)據(jù),但并不表示是一個好模型。因為這可能只是因為模型中足夠的自由度使得它可以描述幾乎所有給定大小的數(shù)據(jù)集,不需要對現(xiàn)象的本質(zhì)有創(chuàng)新的認(rèn)知。所以發(fā)生這種情形時,模型對已有的數(shù)據(jù)會表現(xiàn)的很好,但是對新的數(shù)據(jù)很難泛化。對一個模型真正的測驗就是它對沒有見過的場景的預(yù)測能力。
費(fèi)米和馮·諾依曼對有四個參數(shù)的模型就開始懷疑了。我們用來對 MNIST 數(shù)字分類的 $$30$$ 個隱藏神經(jīng)元神經(jīng)網(wǎng)絡(luò)擁有將近 $$24,000$$ 個參數(shù)!當(dāng)然很多。我們有 $$100$$ 個隱藏元的網(wǎng)絡(luò)擁有將近 $$80,000$$ 個參數(shù),而目前最先進(jìn)的深度神經(jīng)網(wǎng)絡(luò)包含百萬級或者十億級的參數(shù)。我們應(yīng)當(dāng)信賴這些結(jié)果么?
讓我們將問題暴露出來,通過構(gòu)造一個網(wǎng)絡(luò)泛化能力很差的例子。我們的網(wǎng)絡(luò)有 $$30$$ 個隱藏神經(jīng)元,共 $$23,860$$ 個參數(shù)。但是我們不會使用所有 $$50,000$$ 幅訓(xùn)練圖像。相反,我們只使用前 $$1000$$ 幅圖像。使用這個受限的集合,會讓泛化的問題突顯。按照同樣的方式,使用交叉熵代價函數(shù),學(xué)習(xí)率設(shè)置為 $$\eta=0.5$$ 而 minibatch 大小設(shè)置為 $$10$$。不過這里我們訓(xùn)練回合設(shè)置為 $$400$$,比前面的要多很多,因為我們只用了少量的訓(xùn)練樣本。我們現(xiàn)在使用 network2 來研究代價函數(shù)改變的情況:
>>> 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)使用上面的結(jié)果,我們可以畫出代價函數(shù)變化的情況:
這看起來令人振奮,因為代價函數(shù)有一個光滑的下降,跟我們預(yù)期一致。注意,我只是展示了 $$200$$ 到 $$399$$ 回合的情況。這給出了很好的近距離理解訓(xùn)練后期的情況,這也是出現(xiàn)有趣現(xiàn)象的地方。
讓我們看看分類準(zhǔn)確度在測試集上的表現(xiàn):
這里我還是聚焦到了后面的過程。 在前 $$200$$ 回合(圖中沒有顯示)準(zhǔn)確度提升到了 82%。然后學(xué)習(xí)逐漸變緩。最終,在 $$280$$ 回合左右分類準(zhǔn)確度就停止了增長。后面的回合,僅僅看到了在 $$280$$ 回合準(zhǔn)確度周圍隨機(jī)的震蕩。將這幅圖和前面的圖進(jìn)行對比,和訓(xùn)練數(shù)據(jù)相關(guān)的代價函數(shù)持續(xù)平滑下降。如果我們只看哪個代價,我們會發(fā)現(xiàn)模型的表現(xiàn)變得“更好”。但是測試準(zhǔn)確度展示了提升只是一種假象。就像費(fèi)米不大喜歡的那個模型一樣,我們的網(wǎng)絡(luò)在 $$280$$ 回合后就不在能夠繁華到測試數(shù)據(jù)上。所以這種學(xué)習(xí)不大有用。也可以說網(wǎng)絡(luò)在 $$280$$ 后就過匹配(或者過度訓(xùn)練)了。
你可能想知道這里的問題是不是由于我們看的是訓(xùn)練數(shù)據(jù)的代價,而對比的卻是測試數(shù)據(jù)上的分類準(zhǔn)確度導(dǎo)致的。換言之,可能我們這里在進(jìn)行蘋果和橙子的對比。如果我們比較訓(xùn)練數(shù)據(jù)上的代價和測試數(shù)據(jù)上的代價,會發(fā)生什么,我們是在比較類似的度量么?或者可能我們可以比較在兩個數(shù)據(jù)集上的分類準(zhǔn)確度啊?實際上,不管我們使用什么度量的方式盡管,細(xì)節(jié)會變化,但本質(zhì)上都是一樣的。
讓我們來看看測試數(shù)據(jù)集上的代價變化情況:
我們可以看到測試集上的代價在 $$15$$ 回合前一直在提升,隨后越來越差,盡管訓(xùn)練數(shù)據(jù)機(jī)上的代價表現(xiàn)是越來越好的。這其實是另一種模型過匹配的標(biāo)志。盡管,這里帶來了關(guān)于我們應(yīng)當(dāng)將 $$15$$ 還是 $$280$$ 回合當(dāng)作是過匹配占主導(dǎo)的時間點(diǎn)的困擾。從一個實踐角度,我們真的關(guān)心的是提升測試數(shù)據(jù)集上的分類準(zhǔn)確度,而測試集合上的代價不過是分類準(zhǔn)確度的一個反應(yīng)。所以更加合理的選擇就是將 $$280$$ 看成是過匹配開始占統(tǒng)治地位的時間點(diǎn)。
另一個過匹配的信號在訓(xùn)練數(shù)據(jù)上的分類準(zhǔn)確度上也能看出來:
準(zhǔn)確度一直在提升接近 100%。也就是說,我們的網(wǎng)絡(luò)能夠正確地對所有 $$1000$$ 幅圖像進(jìn)行分類!而在同時,我們的測試準(zhǔn)確度僅僅能夠達(dá)到 82.27%。所以我們的網(wǎng)絡(luò)實際上在學(xué)習(xí)訓(xùn)練數(shù)據(jù)集的特例,而不是能夠一般地進(jìn)行識別。我們的網(wǎng)絡(luò)幾乎是在單純記憶訓(xùn)練集合,而沒有對數(shù)字本質(zhì)進(jìn)行理解能夠泛化到測試數(shù)據(jù)集上。
過匹配是神經(jīng)網(wǎng)絡(luò)的一個主要問題。這在現(xiàn)代網(wǎng)絡(luò)中特別正常,因為網(wǎng)絡(luò)權(quán)重和偏差數(shù)量巨大。為了高效地訓(xùn)練,我們需要一種檢測過匹配是不是發(fā)生的技術(shù),這樣我們不會過度訓(xùn)練。并且我們也想要找到一些技術(shù)來降低過匹配的影響。
檢測過匹配的明顯方法是使用上面的方法——跟蹤測試數(shù)據(jù)集合上的準(zhǔn)確度隨訓(xùn)練變化情況。如果我們看到測試數(shù)據(jù)上的準(zhǔn)確度不再提升,那么我們就停止訓(xùn)練。當(dāng)然,嚴(yán)格地說,這其實并非是過匹配的一個必要現(xiàn)象,因為測試集和訓(xùn)練集上的準(zhǔn)確度可能會同時停止提升。當(dāng)然,采用這樣的方式是可以阻止過匹配的。
實際上,我們會使用這種方式變形來試驗。記得之前我們載入 MNIST 數(shù)據(jù)的時候有:
>>> import mnist_loader >>> training_data, validation_data, test_data = \ ... mnist_loader.load_data_wrapper()到現(xiàn)在我們一直在使用 training_data 和 test_data,沒有用過 validation_data。validation_data 中包含了 $$10,000$$ 幅數(shù)字圖像,這些圖像和訓(xùn)練數(shù)據(jù)集中的 $$50,000$$ 幅圖像以及測試數(shù)據(jù)集中的 $$10,000$$ 幅都不相同。我們會使用 validation_data 來防止過匹配。我們會使用和上面應(yīng)用在 test_data 的策略。我們每個回合都計算在 validation_data 上的分類準(zhǔn)確度。一旦分類準(zhǔn)確度已經(jīng)飽和,就停止訓(xùn)練。這個策略被稱為 提前停止(Early stopping)。當(dāng)然,實際應(yīng)用中,我們不會立即知道什么時候準(zhǔn)確度會飽和。相反,我們會一直訓(xùn)練知道我們確信準(zhǔn)確度已經(jīng)飽和。
這里需要一些判定標(biāo)準(zhǔn)來確定什么時候停止。在我前面的圖中,將 $$280$$ 回合看成是飽和的地方。可能這有點(diǎn)太悲觀了。因為神經(jīng)網(wǎng)絡(luò)有時候會訓(xùn)練過程中處在一個平原期,然后又開始提升。如果在 $$400$$ 回合后,性能又開始提升(也許只是一些少量提升),那我也不會詫異。所以,在提前停止中采取一點(diǎn)激進(jìn)的策略也是可以的。
為何要使用 validation_data 來替代 test_data 防止過匹配問題?實際上,這是一個更為一般的策略的一部分,這個一般的策略就是使用 validation_data 來衡量不同的超參數(shù)(如訓(xùn)練回合,學(xué)習(xí)率,最好的網(wǎng)絡(luò)架構(gòu)等等)的選擇的效果。我們使用這樣方法來找到超參數(shù)的合適值。因此,盡管到現(xiàn)在我并沒有提及這點(diǎn),但其實本書前面已經(jīng)稍微介紹了一些超參數(shù)選擇的方法。
當(dāng)然,這對于我們前面關(guān)于 validation_data 取代 test_data 來防止過匹配的原因仍舊沒有回答。實際上,有一個更加一般的問題,就是為何用validation_data 取代 test_data 來設(shè)置更好的超參數(shù)?為了理解這點(diǎn),想想當(dāng)設(shè)置超參數(shù)時,我們想要嘗試許多不同的超參數(shù)選擇。如果我們設(shè)置超參數(shù)是基于 test_data 的話,可能最終我們就會得到過匹配于 test_data 的超參數(shù)。也就是說,我們可能會找到那些 符合 test_data 特點(diǎn)的超參數(shù),但是網(wǎng)絡(luò)的性能并不能夠泛化到其他數(shù)據(jù)集合上。我們借助 validation_data 來克服這個問題。然后一旦獲得了想要的超參數(shù),最終我們就使用 test_data 進(jìn)行準(zhǔn)確度測量。這給了我們在 test_data 上結(jié)果是一個網(wǎng)絡(luò)泛化能力真正的度量方式的信心。換言之,你可以將驗證集看成是一種特殊的訓(xùn)練數(shù)據(jù)集能夠幫助我們學(xué)習(xí)好的超參數(shù)。這種尋找好的超參數(shù)的觀點(diǎn)有時候被稱為 hold out 方法,因為 validation_data 是從訓(xùn)練集中保留出來的一部分。
在實際應(yīng)用中,甚至在衡量了測試集上的性能后,我們可能也會改變想法并去嘗試另外的方法——也許是一種不同的網(wǎng)絡(luò)架構(gòu)——這將會引入尋找新的超參數(shù)的的過程。如果我們這樣做,難道不會產(chǎn)生過匹配于 test_data 的困境么?我們是不是需要一種潛在無限大的數(shù)據(jù)集的回歸,這樣才能夠確信模型能夠泛化?去除這樣的疑惑其實是一個深刻而困難的問題。但是對實際應(yīng)用的目標(biāo),我們不會擔(dān)心太多。相反,我們會繼續(xù)采用基于 training_data, validation_data, and test_data 的基本 hold out 方法。
我們已經(jīng)研究了在使用 $$1,000$$ 幅訓(xùn)練圖像時的過匹配問題。那么如果我們使用所有的訓(xùn)練數(shù)據(jù)會發(fā)生什么?我們會保留所有其他的參數(shù)都一樣($$30$$ 個隱藏元,學(xué)習(xí)率 $$0.5$$,mini-batch 規(guī)模為 $$10$$),但是訓(xùn)練回合為 $$30$$ 次。下圖展示了分類準(zhǔn)確度在訓(xùn)練和測試集上的變化情況。注意我們使用的測試數(shù)據(jù),而不是驗證集合,為了讓結(jié)果看起來和前面的圖更方便比較。
如你所見,測試集和訓(xùn)練集上的準(zhǔn)確度相比我們使用 $$1,000$$ 個訓(xùn)練數(shù)據(jù)時相差更小。特別地,在訓(xùn)練數(shù)據(jù)上的最佳的分類準(zhǔn)確度 97.86% 只比測試集上的 95.33% 準(zhǔn)確度高一點(diǎn)點(diǎn)。而之前的例子中,這個差距是 17.73%!過匹配仍然發(fā)生了,但是已經(jīng)減輕了不少。我們的網(wǎng)絡(luò)從訓(xùn)練數(shù)據(jù)上更好地泛化到了測試數(shù)據(jù)上。一般來說,最好的降低過匹配的方式之一就是增加訓(xùn)練樣本的量。有了足夠的訓(xùn)練數(shù)據(jù),就算是一個規(guī)模非常大的網(wǎng)絡(luò)也不大容易過匹配。不幸的是,訓(xùn)練數(shù)據(jù)其實是很難或者很昂貴的資源,所以這不是一種太切實際的選擇。
規(guī)范化
增加訓(xùn)練樣本的數(shù)量是一種減輕過匹配的方法。還有其他的一下方法能夠減輕過匹配的程度么?一種可行的方式就是降低網(wǎng)絡(luò)的規(guī)模。然而,大的網(wǎng)絡(luò)擁有一種比小網(wǎng)絡(luò)更強(qiáng)的潛力,所以這里存在一種應(yīng)用冗余性的選項。
幸運(yùn)的是,還有其他的技術(shù)能夠緩解過匹配,即使我們只有一個固定的網(wǎng)絡(luò)和固定的訓(xùn)練集合。這種技術(shù)就是規(guī)范化。本節(jié),我會給出一種最為常用的規(guī)范化手段——有時候被稱為權(quán)重下降(weight decay)或者 L2 規(guī)范化。L2 規(guī)范化的想法是增加一個額外的項到代價函數(shù)上,這個項叫做 規(guī)范化 項。下面是規(guī)范化交叉熵:
其中第一個項就是常規(guī)的交叉熵的表達(dá)式。第二個現(xiàn)在加入到就是所有權(quán)重的平方的和。然后使用一個因子 $$\lambdas/2n$$ 進(jìn)行量化調(diào)整,其中 $$\lambda > 0$$ 可以成為 規(guī)范化參數(shù),而 $$n$$ 就是訓(xùn)練集合的大小。我們會在后面討論 $$\lambdas$$ 的選擇策略。需要注意的是,規(guī)范化項里面并不包含偏差。這點(diǎn)我們后面也會在講述。
當(dāng)然,對其他的代價函數(shù)也可以進(jìn)行規(guī)范化,例如二次代價函數(shù)。類似的規(guī)范化的形式如下:
兩者都可以寫成這樣:
其中 $$C_0$$ 是原始的代價函數(shù)。
直覺地看,規(guī)范化的效果是讓網(wǎng)絡(luò)傾向于學(xué)習(xí)小一點(diǎn)的權(quán)重,其他的東西都一樣的。大的權(quán)重只有能夠給出代價函數(shù)第一項足夠的提升時才被允許。換言之,規(guī)范化可以當(dāng)做一種尋找小的權(quán)重和最小化原始的代價函數(shù)之間的折中。這兩部分之前相對的重要性就由 $$\lambda$$ 的值來控制了:$$\lambda$$ 越小,就偏向于最小化原始代價函數(shù),反之,傾向于小的權(quán)重。
現(xiàn)在,對于這樣的折中為何能夠減輕過匹配還不是很清楚!但是,實際表現(xiàn)表明了這點(diǎn)。我們會在下一節(jié)來回答這個問題。但現(xiàn)在,我們來看看一個規(guī)范化的確減輕過匹配的例子。
為了構(gòu)造這個例子,我們首先需要弄清楚如何將隨機(jī)梯度下降算法應(yīng)用在一個規(guī)范化的神經(jīng)網(wǎng)絡(luò)上。特別地,我們需要知道如何計算偏導(dǎo)數(shù) $$\partial C/\partial w$$ 和 $$\partial C/\partial b$$。對公式(87)進(jìn)行求偏導(dǎo)數(shù)得:
$$\partial C_0/\partial w$$ 和 $$\partial C_0/\partial b$$ 可以通過反向傳播進(jìn)行計算,和上一章中的那樣。所以我們看到其實計算規(guī)范化的代價函數(shù)的梯度是很簡單的:僅僅需要反向傳播,然后加上 $$\frac{\lambda}{n} w$$ 得到所有權(quán)重的偏導(dǎo)數(shù)。而偏差的偏導(dǎo)數(shù)就不要變化,所以梯度下降學(xué)習(xí)規(guī)則不會發(fā)生變化:
權(quán)重的學(xué)習(xí)規(guī)則就變成:
這其實和通常的梯度下降學(xué)習(xí)規(guī)則相同歐諾個,除了乘了 $$1-\frac{\eta\lambda}{n}$$ 因子。這里就是權(quán)重下降的來源。粗看,這樣會導(dǎo)致權(quán)重會不斷下降到 $$0$$。但是實際不是這樣的,因為如果在原始代價函數(shù)中造成下降的話其他的項可能會讓權(quán)重增加。
好的,這就是梯度下降工作的原理。那么隨機(jī)梯度下降呢?正如在沒有規(guī)范化的隨機(jī)梯度下降中,我們可以通過平均 minibatch 中 $$m$$ 個訓(xùn)練樣本來估計 $$\partial C_0/\partial w$$。因此,為了隨機(jī)梯度下降的規(guī)范化學(xué)習(xí)規(guī)則就變成(參考 方程(20))
其中后面一項是對 minibatch 中的訓(xùn)練樣本 $$x$$ 進(jìn)行求和,而 $$C_x$$ 是對每個訓(xùn)練樣本的(無規(guī)范化的)代價。這其實和之前通常的隨機(jī)梯度下降的規(guī)則是一樣的,除了有一個權(quán)重下降的因子 $$1-\frac{\eta\lambda}{n}$$。最后,為了完備性,我給出偏差的規(guī)范化的學(xué)習(xí)規(guī)則。這當(dāng)然是和我們之前的非規(guī)范化的情形一致了(參考公式(32))
這里也是對minibatch 中的訓(xùn)練樣本 $$x$$ 進(jìn)行求和的。
讓我們看看規(guī)范化給網(wǎng)絡(luò)帶來的性能提升吧。這里還會使用有 $$30$$ 個隱藏神經(jīng)元、minibatch 為 $$10$$,學(xué)習(xí)率為 $$0.5$$,使用交叉熵的神經(jīng)網(wǎng)絡(luò)。然而,這次我們會使用規(guī)范化參數(shù)為 $$\lambda = 0.1$$。注意在代碼中,我們使用的變量名字為 lmbda,這是因為在 Python 中 lambda 是關(guān)鍵字,尤其特定的作用。我也會使用 test_data,而不是 validation_data。不過嚴(yán)格地講,我們應(yīng)當(dāng)使用 validation_data的,因為前面已經(jīng)講過了。這里我這樣做,是因為這會讓結(jié)果和非規(guī)范化的結(jié)果對比起來效果更加直接。你可以輕松地調(diào)整為 validation_data,你會發(fā)現(xiàn)有相似的結(jié)果。
>>> 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)訓(xùn)練集上的代價函數(shù)持續(xù)下降,和前面無規(guī)范化的情況一樣的規(guī)律:
但是這里測試集上的準(zhǔn)確度是隨著回合次數(shù)持續(xù)增加的:
顯然,規(guī)范化的使用能夠解決過匹配的問題。而且,準(zhǔn)確度相當(dāng)搞了,最高處達(dá)到了 87.1%,相較于之前的 82.27%。因此,我們幾乎可以確信持續(xù)訓(xùn)練會有更加好的結(jié)果。實驗起來,規(guī)范化讓網(wǎng)絡(luò)具有更好的泛化能力,顯著地減輕了過匹配的效果。
如果我們換成全部的訓(xùn)練數(shù)據(jù)進(jìn)行訓(xùn)練呢?當(dāng)然,我們之前已經(jīng)看到過匹配在大規(guī)模的數(shù)據(jù)上其實不是那么明顯了。那規(guī)范化能不能起到相應(yīng)的作用呢?保持超參數(shù)和之前一樣。不過我們這里需要改變規(guī)范化參數(shù)。原因在于訓(xùn)練數(shù)據(jù)的大小已經(jīng)從 $$n=1,000$$ 改成了 $$n=50,000$$,這個會改變權(quán)重下降因子 $$1-\frac{\eta\lambda}{n}$$。如果我們持續(xù)使用 $$\lambda = 0.1$$ 就會產(chǎn)生很小的權(quán)重下降,因此就將規(guī)范化的效果降低很多。我們通過將 $$\lambda = 5.0$$ 來補(bǔ)償這種下降。
好了,來訓(xùn)練網(wǎng)絡(luò),重新初始化權(quán)重:
>>> 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)我們得到:
這個結(jié)果很不錯。第一,我們在測試集上的分類準(zhǔn)確度在使用規(guī)范化后有了提升,從 95.49% 到 96.49%。這是個很大的進(jìn)步。第二,我們可以看到在訓(xùn)練數(shù)據(jù)和測試數(shù)據(jù)上的結(jié)果之間的差距也更小了。這仍然是一個大的差距,不過我們已經(jīng)顯著得到了本質(zhì)上的降低過匹配的進(jìn)步。
最后,我們看看在我們使用 $$100$$ 個隱藏元和規(guī)范化參數(shù)為 $$\lambda = 5.0$$ 相應(yīng)的測試分類準(zhǔn)確度。我不會給出詳細(xì)分析,就為了有趣,來看看我們使用一些技巧(交叉熵函數(shù)和 $$L2$$ 規(guī)范化)能夠達(dá)到多高的準(zhǔn)確度。
>>> 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)最終在驗證集上的準(zhǔn)確度達(dá)到了 97.92%。這是比 $$30$$ 個隱藏元的較大飛躍。實際上,稍微改變一點(diǎn),$$60$$ 回合 $$\eta=0.1$$ 和 $$\lambda = 5.0$$。我們就突破了 98%,達(dá)到了 98.04% 的分類準(zhǔn)確度。對于 $$152$$ 行代碼這個效果還真不錯!
我們討論了作為一種減輕過匹配和提高分類準(zhǔn)確度的方式的規(guī)范化技術(shù)。實際上,這不是僅有的好處。實踐表明,在使用不同的(隨機(jī))權(quán)重初始化進(jìn)行多次 MNIST 網(wǎng)絡(luò)訓(xùn)練的時候,我發(fā)現(xiàn)無規(guī)范化的網(wǎng)絡(luò)會偶然被限制住,明顯困在了代價函數(shù)的局部最優(yōu)值處。結(jié)果就是不同的運(yùn)行會給出相差很大的結(jié)果。對比看來,規(guī)范化的網(wǎng)絡(luò)能夠提供更容易復(fù)制的結(jié)果。
為何會這樣子?從經(jīng)驗上看,如果代價函數(shù)是無規(guī)范化的,那么權(quán)重向量的長度可能會增長,而其他的東西都保持一樣。隨著時間的推移,這個會導(dǎo)致權(quán)重向量變得非常大。所以會使得權(quán)重向困在差不多方向上,因為由于梯度下降的改變當(dāng)長度很大的時候僅僅會在那個方向發(fā)生微小的變化。我相信這個現(xiàn)象讓學(xué)習(xí)算法更難有效地探索權(quán)重空間,最終導(dǎo)致很難找到代價函數(shù)的最優(yōu)值。
為何規(guī)范化可以幫助減輕過匹配
我們已經(jīng)看到了規(guī)范化在實踐中能夠減少過匹配了。這是令人振奮的,不過,這背后的原因還不得而知!通常的說法是:小的權(quán)重在某種程度上,意味著更低的復(fù)雜性,也就給出了一種更簡單卻更強(qiáng)大的數(shù)據(jù)解釋,因此應(yīng)該優(yōu)先選擇。這雖然很簡短,不過暗藏了一些可能看起來會令人困惑的因素。讓我們將這個解釋細(xì)化,認(rèn)真地研究一下。現(xiàn)在給一個簡單的數(shù)據(jù)集,我們?yōu)槠浣⒛P?#xff1a;
這里我們其實在研究某種真實的現(xiàn)象,$$x$$ 和 $$y$$ 表示真實的數(shù)據(jù)。我們的目標(biāo)是訓(xùn)練一個模型來預(yù)測 $$y$$ 關(guān)于 $$x$$ 的函數(shù)。我們可以使用神經(jīng)網(wǎng)絡(luò)來構(gòu)建這個模型,但是我們先來個簡單的:用一個多項式來擬合數(shù)據(jù)。這樣做的原因其實是多項式相比神經(jīng)網(wǎng)絡(luò)能夠讓事情變得更加清楚。一旦我們理解了多項式的場景,對于神經(jīng)網(wǎng)絡(luò)可以如法炮制。現(xiàn)在,圖中有十個點(diǎn),我們就可以找到唯一的 $$9$$ 階多項式 $$y=a_0x^9 + a_1x^8 + ... + a_9$$ 來完全擬合數(shù)據(jù)。下面是多項式的圖像:
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$$ 得到一個好的擬合效果:
哪個是更好的模型?哪個更可能是真的?還有哪個模型更可能泛化到其他的擁有同樣現(xiàn)象的樣本上?
這些都是很難回答的問題。實際上,我們?nèi)绻麤]有關(guān)于現(xiàn)象背后的信息的話,并不能確定給出上面任何一個問題的答案。但是讓我們考慮兩種可能的情況:(1)$$9$$ 階多項式實際上是完全描述了真實情況的模型,最終它能夠很好地泛化;(2)正確的模型是 $$y=2x$$,但是存在著由于測量誤差導(dǎo)致的額外的噪聲,使得模型不能夠準(zhǔn)確擬合。
先驗假設(shè)無法說出哪個是正確的(或者,如果還有其他的情況出現(xiàn))。邏輯上講,這些都可能出現(xiàn)。并且這不是易見的差異。在給出的數(shù)據(jù)上,兩個模型的表現(xiàn)其實是差不多的。但是假設(shè)我們想要預(yù)測對應(yīng)于某個超過了圖中所有的 $$x$$ 的 $$y$$ 的值,在兩個模型給出的結(jié)果之間肯定有一個極大的差距,因為 $$9$$ 階多項式模型肯定會被 $$x^9$$ 主導(dǎo),而線性模型只是線性的增長。
在科學(xué)中,一種觀點(diǎn)是我們除非不得已應(yīng)該追隨更簡單的解釋。當(dāng)我們找到一個簡單模型似乎能夠解釋很多數(shù)據(jù)樣本的時候,我們都會激動地認(rèn)為發(fā)現(xiàn)了規(guī)律!總之,這看起來簡單的解決僅僅會是偶然出現(xiàn)的不大可能。我們懷疑模型必須表達(dá)出某些關(guān)于現(xiàn)象的內(nèi)在的真理。如上面的例子,線性模型加噪聲肯定比多項式更加可能。所以如果簡單性是偶然出現(xiàn)的話就很令人詫異。因此我們會認(rèn)為線性模型加噪聲表達(dá)除了一些潛在的真理。從這個角度看,多項式模型僅僅是學(xué)習(xí)到了局部噪聲的影響效果。所以盡管多是對于這些特定的數(shù)據(jù)點(diǎn)表現(xiàn)得很好。模型最終會在未知數(shù)據(jù)上的泛化上出現(xiàn)問題,所以噪聲線性模型具有更強(qiáng)大的預(yù)測能力。
讓我們從這個觀點(diǎn)來看神經(jīng)網(wǎng)絡(luò)。假設(shè)神經(jīng)網(wǎng)絡(luò)大多數(shù)有很小的權(quán)重,這最可能出現(xiàn)在規(guī)范化的網(wǎng)絡(luò)中。更小的權(quán)重意味著網(wǎng)絡(luò)的行為不會因為我們隨便改變了一個輸入而改變太大。這會讓規(guī)范化網(wǎng)絡(luò)學(xué)習(xí)局部噪聲的影響更加困難。將它看做是一種讓單個的證據(jù)不會影響網(wǎng)絡(luò)輸出太多的方式。相對的,規(guī)范化網(wǎng)絡(luò)學(xué)習(xí)去對整個訓(xùn)練集中經(jīng)常出現(xiàn)的證據(jù)進(jìn)行反應(yīng)。對比看,大權(quán)重的網(wǎng)絡(luò)可能會因為輸入的微小改變而產(chǎn)生比較大的行為改變。所以一個無規(guī)范化的網(wǎng)絡(luò)可以使用大的權(quán)重來學(xué)習(xí)包含訓(xùn)練數(shù)據(jù)中的噪聲的大量信息的復(fù)雜模型。簡言之,規(guī)范化網(wǎng)絡(luò)受限于根據(jù)訓(xùn)練數(shù)據(jù)中常見的模式來構(gòu)造相對簡單的模型,而能夠抵抗訓(xùn)練數(shù)據(jù)中的噪聲的特性影響。我們的想法就是這可以讓我們的網(wǎng)絡(luò)對看到的現(xiàn)象進(jìn)行真實的學(xué)習(xí),并能夠根據(jù)已經(jīng)學(xué)到的知識更好地進(jìn)行泛化。
所以,傾向于更簡單的解釋的想法其實會讓我們覺得緊張。人們有時候?qū)⑦@個想法稱為“奧卡姆剃刀原則”,然后就會熱情地將其當(dāng)成某種科學(xué)原理來應(yīng)用這個法則。但是,這就不是一個一般的科學(xué)原理。也沒有任何先驗的邏輯原因來說明簡單的解釋就比更為負(fù)責(zé)的解釋要好。實際上,有時候更加復(fù)雜的解釋其實是正確的。
讓我介紹兩個說明復(fù)雜正確的例子。在 $$1940$$ 年代,物理學(xué)家 Marcel Schein 發(fā)布了一個發(fā)現(xiàn)新粒子的聲明。而他工作的公司,GE,非常歡喜,就廣泛地推廣這個發(fā)現(xiàn)。但是物理學(xué)及 Hans Bethe 就有懷疑。Bethe 訪問了 Schein,看著那些展示 Schein 的新粒子的軌跡的盤子。但是在每個 plate 上,Bethe 都發(fā)現(xiàn)了某個說明數(shù)據(jù)需要被去除的問題。最后 Schein 展示給 Bethe 一個看起來很好的 plate。Bethe 說這可能就是一個統(tǒng)計上的僥幸。Schein 說,“使得,但是這個可能就是統(tǒng)計學(xué),甚至是根據(jù)你自己的公式,也就是 $$1/5$$ 的概率。” Bethe 說:“但我們已經(jīng)看過了這 $$5$$ 個plate 了”。最終,Schein 說:“但是在我的plate中,每個好的plate,每個好的場景,你使用了不同的理論(說它們是新的粒子)進(jìn)行解釋,而我只有一種假設(shè)來解釋所有的 plate。” Bethe 回答說,“在你和我的解釋之間的唯一差別就是你的是錯的,而我所有的觀點(diǎn)是正確的。你單一的解釋是錯誤的,我的多重解釋所有都是正確的。”后續(xù)的工作證實了,Bethe 的想法是正確的而 Schein 粒子不再正確。
注意:這一段翻譯得很不好,請參考原文
第二個例子,在 $$1859$$ 年,天文學(xué)家 Urbain Le Verrier 觀察到水星并沒有按照牛頓萬有引力給出的軌跡進(jìn)行運(yùn)轉(zhuǎn)。與牛頓力學(xué)只有很小的偏差,那時候一些解釋就是牛頓力學(xué)需要一些微小的改動了。在 $$1916$$ 年愛因斯坦證明偏差用他的廣義相對論可以解釋得更好,這是一種和牛頓重力體系相差很大的理論,基于更復(fù)雜的數(shù)學(xué)。盡管引入了更多的復(fù)雜性,現(xiàn)如今愛因斯坦的解釋其實是正確的,而牛頓力學(xué)即使加入一些調(diào)整,仍舊是錯誤的。這部分因為我們知道愛因斯坦的理論不僅僅解釋了這個問題,還有很多其他牛頓力學(xué)無法解釋的問題也能夠完美解釋。另外,令人印象深刻的是,愛因斯坦的理論準(zhǔn)確地給出了一些牛頓力學(xué)沒能夠預(yù)測到的顯現(xiàn)。但是這些令人印象深刻的現(xiàn)象其實在先前的時代是觀測不到的。如果一個人僅僅通過簡單性作為判斷合理模型的基礎(chǔ),那么一些牛頓力學(xué)的改進(jìn)理論可能會看起來更加合理一些。
從這些故事中可以讀出三點(diǎn)。第一,確定兩種解釋中哪個“更加簡單”其實是一件相當(dāng)微妙的工作。第二,即使我們可以做出這樣一個判斷,簡單性也是一個使用時需要相當(dāng)小心的指導(dǎo)!第三,對模型真正的測試不是簡單性,而是它在新場景中對新的活動中的預(yù)測能力。
所以,我們應(yīng)當(dāng)時時記住這一點(diǎn),規(guī)范化的神經(jīng)網(wǎng)絡(luò)常常能夠比非規(guī)范化的泛化能力更強(qiáng),這只是一種實驗事實(empirical fact)。所以,本書剩下的內(nèi)容,我們也會頻繁地使用規(guī)范化技術(shù)。我已經(jīng)在上面講過了為何現(xiàn)在還沒有一個人能夠發(fā)展出一整套具有說服力的關(guān)于規(guī)范化可以幫助網(wǎng)絡(luò)泛化的理論解釋。實際上,研究者們不斷地在寫自己嘗試不同的規(guī)范化方法,然后看看哪種表現(xiàn)更好,嘗試?yán)斫鉃楹尾煌挠^點(diǎn)表現(xiàn)的更好。所以你可以將規(guī)范化看做某種任意整合的技術(shù)。盡管其效果不錯,但我們并沒有一套完整的關(guān)于所發(fā)生情況的理解,僅僅是一些不完備的啟發(fā)式規(guī)則或者經(jīng)驗。
這里也有更深的問題,這個問題也是有關(guān)科學(xué)的關(guān)鍵問題——我們?nèi)绾畏夯R?guī)范化能夠給我們一種計算上的魔力幫助神經(jīng)網(wǎng)絡(luò)更好地泛化,但是并不會帶來原理上理解的指導(dǎo),甚至不會告訴我們什么樣的觀點(diǎn)才是最好的。
這個問題要追溯到歸納問題,最先由蘇格蘭哲學(xué)家大衛(wèi) 休謨在 "An Enquiry Concerning Human Understanding" (1748) 中提出。在現(xiàn)代機(jī)器學(xué)習(xí)領(lǐng)域中歸納問題被 David Wolpert 和 William Macready 描述成無免費(fèi)午餐定理。
這實在是令人困擾,因為在日常生活中,我們?nèi)祟愒诜夯媳憩F(xiàn)很好。給一個兒童幾幅大象的圖片,他就能快速地學(xué)會認(rèn)識其他的大象。當(dāng)然,他們偶爾也會搞錯,很可能將一只犀牛誤認(rèn)為大象,但是一般說來,這個過程會相當(dāng)準(zhǔn)確。所以我們有個系統(tǒng)——人的大腦——擁有超大量的自由變量。在受到僅僅少量的訓(xùn)練圖像后,系統(tǒng)學(xué)會了在其他圖像的推廣。某種程度上,我們的大腦的規(guī)范化做得特別好!怎么做的?現(xiàn)在還不得而知。我期望若干年后,我們能夠發(fā)展出更加強(qiáng)大的技術(shù)來規(guī)范化神經(jīng)網(wǎng)絡(luò),最終這些技術(shù)會讓神經(jīng)網(wǎng)絡(luò)甚至在小的訓(xùn)練集上也能夠?qū)W到強(qiáng)大的泛化能力。
實際上,我們的網(wǎng)絡(luò)已經(jīng)比我們預(yù)先期望的要好一些了。擁有 $$100$$ 個隱藏元的網(wǎng)絡(luò)會有接近 $$80,000$$ 個參數(shù)。我們的訓(xùn)練集僅僅有 $$50,000$$ 幅圖像。這好像是用一個 $$80,000$$ 階的多項式來擬合 $$50,000$$ 個數(shù)據(jù)點(diǎn)。我們的網(wǎng)絡(luò)肯定會過匹配得很嚴(yán)重。但是,這樣的網(wǎng)絡(luò)實際上卻泛化得很好。為什么?這一點(diǎn)并沒有很好滴理解。這里有個猜想:梯度下降學(xué)習(xí)的動態(tài)有一種自規(guī)范化的效應(yīng)。這真是一個意料之外的巧合,但也帶來了對于這種現(xiàn)象本質(zhì)無知的不安。不過,我們還是會在后面依照這種實踐的觀點(diǎn)來應(yīng)用規(guī)范化技術(shù)的。
神經(jīng)網(wǎng)絡(luò)也是由于這點(diǎn)表現(xiàn)才更好一些。
現(xiàn)在我們回到前面留下來的一個細(xì)節(jié):L2 規(guī)范化沒有限制偏差,以此作為本節(jié)的結(jié)論。當(dāng)然了,對規(guī)范化的過程稍作調(diào)整就可以對偏差進(jìn)行規(guī)范了。實踐看來,做出這樣的調(diào)整并不會對結(jié)果改變太多,所以,在某種程度上,對不對偏差進(jìn)行規(guī)范化其實就是一種習(xí)慣了。然而,需要注意的是,有一個大的偏差并不會像大的權(quán)重那樣會讓神經(jīng)元對輸入太過敏感。所以我們不需要對大的偏差所帶來的學(xué)習(xí)訓(xùn)練數(shù)據(jù)的噪聲太過擔(dān)心。同時,允許大的偏差能夠讓網(wǎng)絡(luò)更加靈活——因為,大的偏差讓神經(jīng)元更加容易飽和,這有時候是我們所要達(dá)到的效果。所以,我們通常不會對偏差進(jìn)行規(guī)范化。
規(guī)范化的其他技術(shù)
除了 L2 外還有很多規(guī)范化技術(shù)。實際上,正是由于數(shù)量眾多,我這里也不回將所有的都列舉出來。在本節(jié),我簡要地給出三種減輕過匹配的其他的方法:L1 規(guī)范化、dropout 和人工增加訓(xùn)練樣本。我們不會像上面介紹得那么深入。其實,目的只是想讓讀者熟悉這些主要的思想,然后來體會一下規(guī)范化技術(shù)的多樣性。
L1 規(guī)范化:這個方法其實是在代價函數(shù)上加上一個權(quán)重絕對值的和:
直覺上看,這和 L2 規(guī)范化相似,懲罰大的權(quán)重,傾向于讓網(wǎng)絡(luò)的權(quán)重變小。當(dāng)然,L1 規(guī)范化和 L2 規(guī)范化并不相同,所以我們不應(yīng)該期望 L1 規(guī)范化是進(jìn)行同樣的行為。讓我們來看看試著理解使用 L1 規(guī)范化和 L2 規(guī)范化所不同的地方。
首先,我們會研究一下代價函數(shù)的偏導(dǎo)數(shù)。對(95)求導(dǎo)我們有:
其中 $$sgn(w)$$ 就是 $$w$$ 的正負(fù)號。使用這個表達(dá)式,我們可以輕易地對反向傳播進(jìn)行修改從而使用基于 L1 規(guī)范化的隨機(jī)梯度下降進(jìn)行學(xué)習(xí)。對 L1 規(guī)范化的網(wǎng)絡(luò)進(jìn)行更新的規(guī)則就是
其中和往常一樣,我們可以用 minibatch 的均值來估計 $$\partial C_0/\partial w$$。對比公式(93)的 L2 規(guī)范化,
在兩種情形下,規(guī)范化的效果就是縮小權(quán)重。這和我們想要讓權(quán)重不會太大的直覺目標(biāo)相符。在 L1 規(guī)范化中,權(quán)重按照一個接近 $$0$$ 的常量進(jìn)行縮小。在 L2 規(guī)范化中,權(quán)重同按照一個和 $$w$$ 成比例的量進(jìn)行縮小的。所以,當(dāng)一個特定的權(quán)重絕對值 $$|w|$$很大時,L1 規(guī)范化縮小得遠(yuǎn)比 L2 規(guī)范化要小得多。而一個特定的權(quán)重絕對值 $$|w|$$很小時,L1 規(guī)范化權(quán)值要比 L2 規(guī)范化縮小得更大。最終的結(jié)果就是:L1 規(guī)范化傾向于聚集網(wǎng)絡(luò)的權(quán)重在相對少量的高重要度連接上,而其他權(quán)重就會被驅(qū)使向 $$0$$ 接近。
我在上面的討論中其實忽略了一個問題——在 $$w=0$$ 的時候,偏導(dǎo)數(shù) $$\partial C/\partial w$$ 未定義。原因在于函數(shù) $$|w|$$ 在 $$w=0$$ 時有個直角,事實上,導(dǎo)數(shù)是不存在的。不過也沒有關(guān)系。我們下面要做的就是應(yīng)用無規(guī)范化的通常的梯度下降的規(guī)則在 $$w=0$$ 處。這應(yīng)該不會有什么問題,直覺上看,規(guī)范化的效果就是縮小權(quán)重,顯然,不能對一個已經(jīng)是 $$0$$ 的權(quán)重進(jìn)行縮小了。更準(zhǔn)確地說,我們將會使用方程(96)(97)并約定 $$sgn(0)=0$$。這樣就給出了一種緊致的規(guī)則來進(jìn)行采用 L1 規(guī)范化的隨機(jī)梯度下降學(xué)習(xí)。
Dropout :Dropout 是一種相當(dāng)激進(jìn)的技術(shù)。和 L1、L2 規(guī)范化不同,dropout 并不依賴對代價函數(shù)的變更。而是,在 dropout 中,我們改變了網(wǎng)絡(luò)本身。讓我在給出為何工作的原理之前描述一下 dropout 基本的工作機(jī)制和所得到的結(jié)果。
假設(shè)我們嘗試訓(xùn)練一個網(wǎng)絡(luò):
特別地,假設(shè)我們有一個訓(xùn)練數(shù)據(jù) $$x$$ 和 對應(yīng)的目標(biāo)輸出 $$y$$。通常我們會通過在網(wǎng)絡(luò)中前向傳播 $$x$$ ,然后進(jìn)行反向傳播來確定對梯度的共現(xiàn)。使用 dropout,這個過程就改了。我們會從隨機(jī)(臨時)地刪除網(wǎng)絡(luò)中的一半的神經(jīng)元開始,讓輸入層和輸出層的神經(jīng)元保持不變。在此之后,我們會得到最終的網(wǎng)絡(luò)。注意那些被 dropout 的神經(jīng)元,即那些臨時性刪除的神經(jīng)元,用虛圈表示在途中:
我們前向傳播輸入,通過修改后的網(wǎng)絡(luò),然后反向傳播結(jié)果,同樣通過這個修改后的網(wǎng)絡(luò)。在 minibatch 的若干樣本上進(jìn)行這些步驟后,我們對那些權(quán)重和偏差進(jìn)行更新。然后重復(fù)這個過程,首先重置 dropout 的神經(jīng)元,然后選擇新的隨機(jī)隱藏元的子集進(jìn)行刪除,估計對一個不同的minibatch的梯度,然后更新權(quán)重和偏差。
通過不斷地重復(fù),我們的網(wǎng)絡(luò)會學(xué)到一個權(quán)重和偏差的集合。當(dāng)然,這些權(quán)重和偏差也是在一般的隱藏元被丟棄的情形下學(xué)到的。當(dāng)我們實際運(yùn)行整個網(wǎng)絡(luò)時,是指兩倍的隱藏元將會被激活。為了補(bǔ)償這個,我們將從隱藏元出去的權(quán)重減半了。
這個 dropout 過程可能看起來奇怪和ad hoc。為什么我們期待這樣的方法能夠進(jìn)行規(guī)范化呢?為了解釋所發(fā)生的事,我希望你停下來想一下沒有 dropout 的訓(xùn)練方式。特別地,想象一下我們訓(xùn)練幾個不同的神經(jīng)網(wǎng)絡(luò),使用的同一個訓(xùn)練數(shù)據(jù)。當(dāng)然,網(wǎng)絡(luò)可能不是從同一初始狀態(tài)開始的,最終的結(jié)果也會有一些差異。出現(xiàn)這種情況時,我們可以使用一些平均或者投票的方式來確定接受哪個輸出。例如,如果我們訓(xùn)練了五個網(wǎng)絡(luò),其中三個被分類當(dāng)做是 $$3$$,那很可能它就是 $$3$$。另外兩個可能就犯了錯誤。這種平均的方式通常是一種強(qiáng)大(盡管代價昂貴)的方式來減輕過匹配。原因在于不同的網(wǎng)絡(luò)可能會以不同的方式過匹配,平均法可能會幫助我們消除那樣的過匹配。
那么這和 dropout 有什么關(guān)系呢?啟發(fā)式地看,當(dāng)我們丟掉不同的神經(jīng)元集合時,有點(diǎn)像我們在訓(xùn)練不同的神經(jīng)網(wǎng)絡(luò)。所以,dropout 過程就如同大量不同網(wǎng)絡(luò)的效果的平均那樣。不同的網(wǎng)絡(luò)以不同的方式過匹配了,所以,dropout 的網(wǎng)絡(luò)會減輕過匹配。
一個相關(guān)的啟發(fā)式解釋在早期使用這項技術(shù)的論文中曾經(jīng)給出:“因為神經(jīng)元不能依賴其他神經(jīng)元特定的存在,這個技術(shù)其實減少了復(fù)雜的互適應(yīng)的神經(jīng)元。所以,強(qiáng)制要學(xué)習(xí)那些在神經(jīng)元的不同隨機(jī)子集中更加健壯的特征。”換言之,如果我們就愛那個神經(jīng)網(wǎng)絡(luò)看做一個進(jìn)行預(yù)測的模型的話,我們就可以將 dropout 看做是一種確保模型對于證據(jù)丟失健壯的方式。這樣看來,dropout 和 L1、L2 規(guī)范化也是有相似之處的,這也傾向于更小的權(quán)重,最后讓網(wǎng)絡(luò)對丟失個體連接的場景更加健壯。
當(dāng)然,真正衡量 dropout 的方式在提升神經(jīng)網(wǎng)絡(luò)性能上應(yīng)用得相當(dāng)成功。原始論文介紹了用來解決很多不同問題的技術(shù)。對我們來說,特別感興趣的是他們應(yīng)用 dropout 在 MNIST 數(shù)字分類上,使用了一個和我們之前介紹的那種初級的前向神經(jīng)網(wǎng)絡(luò)。這篇文章關(guān)注到最好的結(jié)果是在測試集上去得到 98.4% 的準(zhǔn)確度。他們使用dropout 和 L2 規(guī)范化的組合將其提高到了 98.7%。類似重要的結(jié)果在其他不同的任務(wù)上也取得了一定的成效。dropout 已經(jīng)在過匹配問題尤其突出的訓(xùn)練大規(guī)模深度網(wǎng)絡(luò)中。
人工擴(kuò)展訓(xùn)練數(shù)據(jù):我們前面看到了 MNIST 分類準(zhǔn)確度在我們使用 $$1,000$$ 幅訓(xùn)練圖像時候下降到了 $$80$$ 年代的準(zhǔn)確度。這種情況并不奇怪,因為更少的訓(xùn)練數(shù)據(jù)意味著我們的網(wǎng)絡(luò)所接觸到較少的人類手寫的數(shù)字中的變化。讓我們訓(xùn)練 $$30$$ 個隱藏元的網(wǎng)絡(luò),使用不同的訓(xùn)練數(shù)據(jù)集,來看看性能的變化情況。我們使用 minibatch 大小為 $$10$$,學(xué)習(xí)率是 $$\eta=0.5$$,規(guī)范化參數(shù)是 $$\lambda=5.0$$,交叉熵代價函數(shù)。我們在全部訓(xùn)練數(shù)據(jù)集合上訓(xùn)練 30 個回合,然后會隨著訓(xùn)練數(shù)據(jù)量的下降而成比例變化回合數(shù)。為了確保權(quán)重下降因子在訓(xùn)練數(shù)據(jù)集上相同,我們會在全部訓(xùn)練集上使用規(guī)范化參數(shù)為 $$\lambda = 5.0$$,然后在使用更小的訓(xùn)練集的時候成比例地下降 $$\lambda$$ 值。
This and the next two graph are produced with the program more_data.py.
如你所見,分類準(zhǔn)確度在使用更多的訓(xùn)練數(shù)據(jù)時提升了很大。根據(jù)這個趨勢的話,提升會隨著更多的數(shù)據(jù)而不斷增加。當(dāng)然,在訓(xùn)練的后期我們看到學(xué)習(xí)過程已經(jīng)進(jìn)入了飽和狀態(tài)。然而,如果我們使用對數(shù)作為橫坐標(biāo)的話,可以重畫此圖如下:
這看起來到了后面結(jié)束的地方,增加仍舊明顯。這表明如果我們使用大量更多的訓(xùn)練數(shù)據(jù)——不妨設(shè)百萬或者十億級的手寫樣本——那么,我們可能會得到更好的性能,即使是用這樣的簡單網(wǎng)絡(luò)。
獲取更多的訓(xùn)練樣本其實是很重要的想法。不幸的是,這個方法代價很大,在實踐中常常是很難達(dá)到的。不過,還有一種方法能夠獲得類似的效果,那就是進(jìn)行人工的樣本擴(kuò)展。假設(shè)我們使用一個 $$5$$ 的訓(xùn)練樣本,
將其進(jìn)行旋轉(zhuǎn),比如說 $$15$$°:
這還是會被設(shè)別為同樣的數(shù)字的。但是在像素層級這和任何一幅在 MNIST 訓(xùn)練數(shù)據(jù)中的圖像都不相同。所以將這樣的樣本加入到訓(xùn)練數(shù)據(jù)中是很可能幫助我們學(xué)習(xí)有關(guān)手寫數(shù)字更多知識的方法。而且,顯然,我們不會就只對這幅圖進(jìn)行人工的改造。我們可以在所有的 MNIST 訓(xùn)練樣本上通過和多小的旋轉(zhuǎn)擴(kuò)展訓(xùn)練數(shù)據(jù),然后使用擴(kuò)展后的訓(xùn)練數(shù)據(jù)來提升我們網(wǎng)絡(luò)的性能。
這個想法非常強(qiáng)大并且已經(jīng)被廣發(fā)應(yīng)用了。讓我們看看一些在 MNIST 上使用了類似的方法進(jìn)行研究成果。其中一種他們考慮的網(wǎng)絡(luò)結(jié)構(gòu)其實和我們已經(jīng)使用過的類似——一個擁有 800 個隱藏元的前驅(qū)神經(jīng)網(wǎng)絡(luò),使用了交叉熵代價函數(shù)。在標(biāo)準(zhǔn)的 MNIST 訓(xùn)練數(shù)據(jù)上運(yùn)行這個網(wǎng)絡(luò),得到了 98.4% 的分類準(zhǔn)確度,其中使用了不只是旋轉(zhuǎn)還包括轉(zhuǎn)換和扭曲。通過在這個擴(kuò)展后的數(shù)據(jù)集上的訓(xùn)練,他們提升到了 98.9% 的準(zhǔn)確度。然后還在“彈性扭曲(elastic distortion)”的數(shù)據(jù)上進(jìn)行了實驗,這是一種特殊的為了模仿手部肌肉的隨機(jī)抖動的圖像扭曲方法。通過使用彈性扭曲擴(kuò)展的數(shù)據(jù),他們最終達(dá)到了 99.3% 的分類準(zhǔn)確度。他們通過展示訓(xùn)練數(shù)據(jù)的所有類型的變體來擴(kuò)展了網(wǎng)絡(luò)的經(jīng)驗。
Best Practices for Convolutional Neural Networks Applied to Visual Document Analysis, by Patrice Simard, Dave Steinkraus, and John Platt (2003).
這個想法的變體也可以用在提升手寫數(shù)字識別之外不同學(xué)習(xí)任務(wù)上的性能。一般就是通過應(yīng)用反映真實世界變化的操作來擴(kuò)展訓(xùn)練數(shù)據(jù)。找到這些方法其實并不困難。例如,你要構(gòu)建一個神經(jīng)網(wǎng)絡(luò)來進(jìn)行語音識別。我們?nèi)祟惿踔量梢栽谟斜尘霸肼暤那闆r下識別語音。所以你可以通過增加背景噪聲來擴(kuò)展你的訓(xùn)練數(shù)據(jù)。我們同樣能夠?qū)ζ溥M(jìn)行加速和減速來獲得相應(yīng)的擴(kuò)展數(shù)據(jù)。所以這是另外的一些擴(kuò)展訓(xùn)練數(shù)據(jù)的方法。這些技術(shù)并不總是有用——例如,其實與其在數(shù)據(jù)中加入噪聲,倒不如先對數(shù)據(jù)進(jìn)行噪聲的清理,這樣可能更加有效。當(dāng)然,記住可以進(jìn)行數(shù)據(jù)的擴(kuò)展,尋求應(yīng)用的機(jī)會還是相當(dāng)有價值的一件事。
練習(xí)
- 正如上面討論的那樣,一種擴(kuò)展 MNIST 訓(xùn)練數(shù)據(jù)的方式是用一些微小的旋轉(zhuǎn)。如果我們允許過大的旋轉(zhuǎn),則會出現(xiàn)什么狀況呢?
大數(shù)據(jù)的旁白和對分類準(zhǔn)確度的影響:讓我們看看神經(jīng)網(wǎng)絡(luò)準(zhǔn)確度隨著訓(xùn)練集大小變化的情況:
假設(shè),我們使用別的什么方法來進(jìn)行分類。例如,我們使用 SVM。正如第一章介紹的那樣,不要擔(dān)心你熟不熟悉 SVM,我們不進(jìn)行深入的討論。下面是 SVM 模型的準(zhǔn)確度隨著訓(xùn)練數(shù)據(jù)集的大小變化的情況:
可能第一件讓你吃驚的是神經(jīng)網(wǎng)絡(luò)在每個訓(xùn)練規(guī)模下性能都超過了 SVM。這很好,盡管你對細(xì)節(jié)和原理可能不太了解,因為我們只是直接從 scikit-learn 中直接調(diào)用了這個方法,而對神經(jīng)網(wǎng)絡(luò)已經(jīng)深入講解了很多。更加微妙和有趣的現(xiàn)象其實是如果我們訓(xùn)練 SVM 使用 $$50,000$$ 幅圖像,實際上 SVM 已經(jīng)能夠超過我們使用 $$5,000$$ 幅圖像的準(zhǔn)確度。換言之,更多的訓(xùn)練數(shù)據(jù)可以補(bǔ)償不同的機(jī)器學(xué)習(xí)算法的差距。
還有更加有趣的現(xiàn)象也出現(xiàn)了。假設(shè)我們試著用兩種機(jī)器學(xué)習(xí)算法去解決問題,算法 $$A$$ 和算法 $$B$$。有時候出現(xiàn),算法 $$A$$ 在一個訓(xùn)練集合上超過 算法 $$B$$,卻在另一個訓(xùn)練集上弱于算法 $$B$$。上面我們并沒有看到這個情況——因為這要求兩幅圖有交叉的點(diǎn)——這里并沒有。對“算法 A 是不是要比算法 $$B$$ 好?”正確的反應(yīng)應(yīng)該是“你在使用什么訓(xùn)練集合?”
在進(jìn)行開發(fā)時或者在讀研究論文時,這都是需要記住的事情。很多論文聚焦在尋找新的技術(shù)來給出標(biāo)準(zhǔn)數(shù)據(jù)集上更好的性能。“我們的超贊的技術(shù)在標(biāo)準(zhǔn)測試集 $$Y$$ 上給出了百分之 $$X$$ 的性能提升。”這是通常的研究聲明。這樣的聲明通常比較有趣,不過也必須被理解為僅僅在特定的訓(xùn)練數(shù)據(jù)機(jī)上的應(yīng)用效果。那些給出基準(zhǔn)數(shù)據(jù)集的人們會擁有更大的研究經(jīng)費(fèi)支持,這樣能夠獲得更好的訓(xùn)練數(shù)據(jù)。所以,很可能他們由于超贊的技術(shù)的性能提升其實在更大的數(shù)據(jù)集合上就喪失了。換言之,人們標(biāo)榜的提升可能就是歷史的偶然。所以需要記住的特別是在實際應(yīng)用中,我們想要的是更好的算法和更好的訓(xùn)練數(shù)據(jù)。尋找更好的算法很重,不過需要確保你在此過程中,沒有放棄對更多更好的數(shù)據(jù)的追求。
問題
- 研究問題:我們的機(jī)器學(xué)習(xí)算法在非常大的數(shù)據(jù)集上如何進(jìn)行?對任何給定的算法,其實去定義一個隨著訓(xùn)練數(shù)據(jù)規(guī)模變化的漸近的性能是一種很自然的嘗試。一種簡單粗暴的方法就是簡單地進(jìn)行上面圖中的趨勢分析,然后將圖像推進(jìn)到無窮大。而對此想法的反駁是曲線本身會給出不同的漸近性能。你能夠找到擬合某些特定類別曲線的理論上的驗證方法嗎?如果可以,比較不同的機(jī)器學(xué)習(xí)算法的漸近性能。
總結(jié):我們現(xiàn)在已經(jīng)介紹完了過匹配和規(guī)范化。當(dāng)然,我們重回這個問題。正如我們前面講過的那樣,尤其是計算機(jī)越來越強(qiáng)大,我們有訓(xùn)練更大的網(wǎng)絡(luò)的能力時。過匹配是神經(jīng)網(wǎng)絡(luò)中一個主要的問題。我們有迫切的愿望來開發(fā)出強(qiáng)大的規(guī)范化技術(shù)來減輕過匹配,所以,這也是當(dāng)前極其熱門的研究方向之一。
===============================================================================================================================
權(quán)重初始化
創(chuàng)建了神經(jīng)網(wǎng)絡(luò)后,我們需要進(jìn)行權(quán)重和偏差的初始化。到現(xiàn)在,我們一直是根據(jù)在第一章中介紹的那樣進(jìn)行初始化。提醒你一下,之前的方式就是根據(jù)獨(dú)立的均值為 $$0$$,標(biāo)準(zhǔn)差為 $$1$$ 的高斯隨機(jī)變量隨機(jī)采樣作為權(quán)重和偏差的初始值。這個方法工作的還不錯,但是非常 ad hoc,所以我們需要尋找一些更好的方式來設(shè)置我們網(wǎng)絡(luò)的初始化權(quán)重和偏差,這對于幫助網(wǎng)絡(luò)學(xué)習(xí)速度的提升很有價值。
結(jié)果表明,我們可以比使用正規(guī)化的高斯分布效果更好。為什么?假設(shè)我們使用一個很多的輸入神經(jīng)元,比如說 $$1000$$。假設(shè),我們已經(jīng)使用正規(guī)化的高斯分布初始化了連接第一隱藏層的權(quán)重。現(xiàn)在我將注意力集中在這一層的連接權(quán)重上,忽略網(wǎng)絡(luò)其他部分:
我們?yōu)榱撕喕?#xff0c;假設(shè),我們使用訓(xùn)練樣本 x 其中一半的神經(jīng)元值為 $$0$$,另一半為 $$1$$。下面的觀點(diǎn)也是可以更加廣泛地應(yīng)用,但是你可以從特例中獲得背后的思想。讓我們考慮帶權(quán)和 $$z=\sum_j w_j x_j + b$$ 的隱藏元輸入。其中 $$500$$ 個項消去了,因為對應(yīng)的輸入 $$x_j=0$$。所以 $$z$$ 是 $$501$$ 個正規(guī)化的高斯隨機(jī)變量的和,包含 $$500$$ 個權(quán)重項和額外的 $$1$$ 個偏差項。因此 $$z$$ 本身是一個均值為 $$0$$ 標(biāo)準(zhǔn)差為 $$\sqrt{501}\approx 22.4$$ 的分布。$$z$$ 其實有一個非常寬的高斯分布,不是非常尖的形狀:
尤其是,我們可以從這幅圖中看出 $$|z|$$ 會變得非常的大,比如說 $$z\gg1$$ 或者 $$z\ll 1$$。如果是這樣,輸出 $$\sigma(z)$$ 就會接近 $$1$$ 或者 $$0$$。也就表示我們的隱藏元會飽和。所以當(dāng)出現(xiàn)這樣的情況時,在權(quán)重中進(jìn)行微小的調(diào)整僅僅會給隱藏元的激活值帶來極其微弱的改變。而這種微弱的改變也會影響網(wǎng)絡(luò)中剩下的神經(jīng)元,然后會帶來相應(yīng)的代價函數(shù)的改變。結(jié)果就是,這些權(quán)重在我們進(jìn)行梯度下降算法時會學(xué)習(xí)得非常緩慢。這其實和我們前面討論的問題差不多,前面的情況是輸出神經(jīng)元在錯誤的值上飽和導(dǎo)致學(xué)習(xí)的下降。我們之前通過代價函數(shù)的選擇解決了前面的問題。不幸的是,盡管那種方式在輸出神經(jīng)元上有效,但對于隱藏元的飽和卻一點(diǎn)作用都沒有。
我已經(jīng)研究了第一隱藏層的權(quán)重輸入。當(dāng)然,類似的論斷也對后面的隱藏層有效:如果權(quán)重也是用正規(guī)化的高斯分布進(jìn)行初始化,那么激活值將會接近 $$0$$ 或者 $$1$$,學(xué)習(xí)速度也會相當(dāng)緩慢。
還有可以幫助我們進(jìn)行更好地初始化么,能夠避免這種類型的飽和,最終避免學(xué)習(xí)速度的下降?假設(shè)我們有一個有 $$n_{in}$$ 個輸入權(quán)重的神經(jīng)元。我們會使用均值為 $$0$$ 標(biāo)準(zhǔn)差為 $$1/\sqrt{n_{in}}$$ 的高斯分布初始化這些權(quán)重。也就是說,我們會向下擠壓高斯分布,讓我們的神經(jīng)元更不可能飽和。我們會繼續(xù)使用均值為 $$0$$ 標(biāo)準(zhǔn)差為 $$1$$ 的高斯分布來對偏差進(jìn)行初始化,后面會告訴你原因。有了這些設(shè)定,帶權(quán)和 $$z=\sum_j w_j x_j + b$$ 仍然是一個均值為 $$0$$ 不過有很陡的峰頂?shù)母咚狗植肌<僭O(shè),我們有 $$500$$ 個值為 $$0$$ 的輸入和$$500$$ 個值為 $$1$$ 的輸入。那么很容證明 $$z$$ 是服從均值為 $$0$$ 標(biāo)準(zhǔn)差為 $$\sqrt{3/2} = 1.22$$ 的高斯分布。這圖像要比以前陡得多,所以即使我已經(jīng)對橫坐標(biāo)進(jìn)行壓縮為了進(jìn)行更直觀的比較:
這樣的一個神經(jīng)元更不可能飽和,因此也不大可能遇到學(xué)習(xí)速度下降的問題。
練習(xí)
- 驗證 $$z=\sum_j w_j x_j + b$$ 標(biāo)準(zhǔn)差為 $$\sqrt{3/2}$$。下面兩點(diǎn)可能會有幫助:(a) 獨(dú)立隨機(jī)變量的和的方差是每個獨(dú)立隨即便方差的和;(b)方差是標(biāo)準(zhǔn)差的平方。
我在上面提到,我們使用同樣的方式對偏差進(jìn)行初始化,就是使用均值為 $$0$$ 標(biāo)準(zhǔn)差為 $$1$$ 的高斯分布來對偏差進(jìn)行初始化。這其實是可行的,因為這樣并不會讓我們的神經(jīng)網(wǎng)絡(luò)更容易飽和。實際上,其實已經(jīng)避免了飽和的問題的話,如何初始化偏差影響不大。有些人將所有的偏差初始化為 $$0$$,依賴梯度下降來學(xué)習(xí)合適的偏差。但是因為差別不是很大,我們后面還會按照前面的方式來進(jìn)行初始化。
讓我們在 MNIST 數(shù)字分類任務(wù)上比較一下新舊兩種權(quán)重初始化方式。同樣,還是使用 $$30$$ 個隱藏元,minibatch 的大小為 $$30$$,規(guī)范化參數(shù) $$\lambda=5.0$$,然后是交叉熵代價函數(shù)。我們將學(xué)習(xí)率從 $$\eta=0.5$$ 調(diào)整到 $$0.1$$,因為這樣會讓結(jié)果在圖像中表現(xiàn)得更加明顯。我們先使用舊的初始化方法訓(xùn)練:
>>> 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)我們也使用新方法來進(jìn)行權(quán)重的初始化。這實際上還要更簡單,因為 network2's 默認(rèn)方式就是使用新的方法。這意味著我們可以丟掉 net.large_weight_initializer() 調(diào)用:
>>> 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)將結(jié)果用圖展示出來,就是:
兩種情形下,我們在 96% 的準(zhǔn)確度上重合了。最終的分類準(zhǔn)確度幾乎完全一樣。但是新的初始化技術(shù)帶來了速度的提升。在第一種初始化方式的分類準(zhǔn)確度在 87% 一下,而新的方法已經(jīng)幾乎達(dá)到了 93%。看起來的情況就是我們新的關(guān)于權(quán)重初始化的方式將訓(xùn)練帶到了一個新的境界,讓我們能夠更加快速地得到好的結(jié)果。同樣的情況在 $$100$$ 個神經(jīng)元的設(shè)定中也出現(xiàn)了:
在這個情況下,兩個曲線并沒有重合。然而,我做的實驗發(fā)現(xiàn)了其實就在一些額外的回合后(這里沒有展示)準(zhǔn)確度其實也是幾乎相同的。所以,基于這些實驗,看起來提升的權(quán)重初始化僅僅會加快訓(xùn)練,不會改變網(wǎng)絡(luò)的性能。然而,在第四張,我們會看到一些例子里面使用 $$1/\sqrt{n_{in}}$$ 權(quán)重初始化的長期運(yùn)行的結(jié)果要顯著更優(yōu)。因此,不僅僅能夠帶來訓(xùn)練速度的加快,有時候在最終性能上也有很大的提升。
$$1/\sqrt{n_{in}}$$ 的權(quán)重初始化方法幫助我們提升了神經(jīng)網(wǎng)絡(luò)學(xué)習(xí)的方式。其他的權(quán)重初始化技術(shù)同樣也有,很多都是基于這個基本的思想。我不會在這里給出其他的方法,因為 $$1/\sqrt{n_{in}}$$ 已經(jīng)可以工作得很好了。如果你對另外的思想感興趣,我推薦你看看在 $$2012$$ 年的 Yoshua Bengio 的論文的 $$14$$ 和 $$15$$ 頁,以及相關(guān)的參考文獻(xiàn)。
Practical Recommendations for Gradient-Based Training of Deep Architectures, by Yoshua Bengio (2012).
問題
- 將規(guī)范化和改進(jìn)的權(quán)重初始化方法結(jié)合使用 L2 規(guī)范化有時候會自動給我們一些類似于新的初始化方法的東西。假設(shè)我們使用舊的初始化權(quán)重的方法。考慮一個啟發(fā)式的觀點(diǎn):(1)假設(shè)$$\lambda$$ 不太小,訓(xùn)練的第一回合將會幾乎被權(quán)重下降統(tǒng)治。;(2)如果 $$\eta\lambda \ll n$$,權(quán)重會按照因子 $$exp(-\eta\lambda/m)$$ 每回合下降;(3)假設(shè) $$\lambda$$ 不太大,權(quán)重下降會在權(quán)重降到 $$1/\sqrt{n}$$ 的時候保持住,其中 $$n$$ 是網(wǎng)絡(luò)中權(quán)重的個數(shù)。用論述這些條件都已經(jīng)滿足本節(jié)給出的例子。
再看手寫識別問題:代碼
讓我們實現(xiàn)本章討論過的這些想法。我們將寫出一個新的程序,network2.py,這是一個對第一章中開發(fā)的 network.py 的改進(jìn)版本。如果你沒有仔細(xì)看過 network.py,那你可能會需要重讀前面關(guān)于這段代碼的討論。僅僅 $$74$$ 行代碼,也很易懂。
和 network.py 一樣,主要部分就是 Network 類了,我們用這個來表示神經(jīng)網(wǎng)絡(luò)。使用一個 sizes 的列表來對每個對應(yīng)層進(jìn)行初始化,默認(rèn)使用交叉熵作為代價 cost 參數(shù):
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 方法,使用了我們新式改進(jìn)后的初始化權(quán)重方法。如我們已經(jīng)看到的,使用了均值為 $$0$$ 而標(biāo)準(zhǔn)差為 $$1/\sqrt{n}$$,$$n$$ 為對應(yīng)的輸入連接個數(shù)。我們使用均值為 $$0$$ 而標(biāo)準(zhǔn)差為 $$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 就是進(jìn)行線性代數(shù)運(yùn)算的 Numpy 庫。我們在程序的開頭會 import Numpy。同樣我們沒有對第一層的神經(jīng)元的偏差進(jìn)行初始化。因為第一層其實是輸入層,所以不需要引入任何的偏差。我們在 network.py 中做了完全一樣的事情。
作為 default_weight_initializer 的補(bǔ)充,我們同樣包含了一個 large_weight_initializer 方法。這個方法使用了第一章中的觀點(diǎn)初始化了權(quán)重和偏差。代碼也就僅僅是和default_weight_initializer差了一點(diǎn)點(diǎn)了:
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 方法包含進(jìn)來的原因也就是使得跟第一章的結(jié)果更容易比較。我并沒有考慮太多的推薦使用這個方法的實際情景。
初始化方法 __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)讓我們分解一下。第一個看到的是:即使使用的是交叉熵,數(shù)學(xué)上看,就是一個函數(shù),這里我們用 Python 的類而不是 Python 函數(shù)實現(xiàn)了它。為什么這樣做呢?答案就是代價函數(shù)在我們的網(wǎng)絡(luò)中扮演了兩種不同的角色。明顯的角色就是代價是輸出激活值 $$a$$ 和目標(biāo)輸出 $$y$$ 差距優(yōu)劣的度量。這個角色通過 CrossEntropyCost.fn 方法來扮演。(注意,np.nan_to_num 調(diào)用確保了 Numpy 正確處理接近 $$0$$ 的對數(shù)值)但是代價函數(shù)其實還有另一個角色。回想第二章中運(yùn)行反向傳播算法時,我們需要計算網(wǎng)絡(luò)輸出誤差,$$\delta^L$$。這種形式的輸出誤差依賴于代價函數(shù)的選擇:不同的代價函數(shù),輸出誤差的形式就不同。對于交叉熵函數(shù),輸出誤差就如公式(66)所示:
所以,我們定義了第二個方法,CrossEntropyCost.delta,目的就是讓網(wǎng)絡(luò)知道如何進(jìn)行輸出誤差的計算。然后我們將這兩個組合在一個包含所有需要知道的有關(guān)代價函數(shù)信息的類中。
類似地,network2.py 還包含了一個表示二次代價函數(shù)的類。這個是用來和第一章的結(jié)果進(jìn)行對比的,因為后面我們幾乎都在使用交叉函數(shù)。代碼如下。QuadraticCost.fn 方法是關(guān)于網(wǎng)絡(luò)輸出 $$a$$ 和目標(biāo)輸出 $$y$$ 的二次代價函數(shù)的直接計算結(jié)果。由 QuadraticCost.delta 返回的值就是二次代價函數(shù)的誤差。
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)現(xiàn)在,我們理解了 network2.py 和 network.py 兩個實現(xiàn)之間的主要差別。都是很簡單的東西。還有一些更小的變動,下面我們會進(jìn)行介紹,包含 L2 規(guī)范化的實現(xiàn)。在講述規(guī)范化之前,我們看看 network2.py 完整的實現(xiàn)代碼。你不需要太仔細(xì)地讀遍這些代碼,但是對整個結(jié)構(gòu)尤其是文檔中的內(nèi)容的理解是非常重要的,這樣,你就可以理解每段程序所做的工作。當(dāng)然,你也可以隨自己意愿去深入研究!如果你迷失了理解,那么請讀讀下面的講解,然后再回到代碼中。不多說了,給代碼:
"""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 規(guī)范化。盡管這是一個主要的概念上的變動,在實現(xiàn)中其實相當(dāng)簡單。對大部分情況,僅僅需要傳遞參數(shù) lmbda 到不同的方法中,主要是 Network.SGD 方法。實際上的工作就是一行代碼的事在 Network.update_mini_batch 的倒數(shù)第四行。這就是我們改動梯度下降規(guī)則來進(jìn)行權(quán)重下降的地方。盡管改動很小,但其對結(jié)果影響卻很大!
其實這種情況在神經(jīng)網(wǎng)絡(luò)中實現(xiàn)一些新技術(shù)的常見現(xiàn)象。我們花費(fèi)了近千字的篇幅來討論規(guī)范化。概念的理解非常微妙困難。但是添加到程序中的時候卻如此簡單。精妙復(fù)雜的技術(shù)可以通過微小的代碼改動就可以實現(xiàn)了。
另一個微小卻重要的改動是隨機(jī)梯度下降方法的幾個標(biāo)志位的增加。這些標(biāo)志位讓我們可以對在代價和準(zhǔn)確度的監(jiān)控變得可能。這些標(biāo)志位默認(rèn)是 False 的,但是在我們例子中,已經(jīng)被置為 True 來監(jiān)控 Network 的性能。另外,network2.py 中的 Network.SGD 方法返回了一個四元組來表示監(jiān)控的結(jié)果。我們可以這樣使用:
>>> 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$$ 個元素的列表其中包含了每個回合在驗證集合上的代價函數(shù)值。這種類型的信息在理解網(wǎng)絡(luò)行為的過程中特別有用。比如,它可以用來畫出展示網(wǎng)絡(luò)隨時間學(xué)習(xí)的狀態(tài)。其實,這也是我在前面的章節(jié)中展示性能的方式。然而要注意的是如果任何標(biāo)志位都沒有設(shè)置的話,對應(yīng)的元組中的元素就是空列表。
另一個增加項就是在 Network.save 方法中的代碼,用來將 Network 對象保存在磁盤上,還有一個載回內(nèi)存的函數(shù)。這兩個方法都是使用 JSON 進(jìn)行的,而非 Python 的 pickle 或者 cPickle 模塊——這些通常是 Python 中常見的保存和裝載對象的方法。使用 JSON 的原因是,假設(shè)在未來某天,我們想改變 Network 類來允許非 sigmoid 的神經(jīng)元。對這個改變的實現(xiàn),我們最可能是改變在 Network.__init__ 方法中定義的屬性。如果我們簡單地 pickle 對象,會導(dǎo)致 load 函數(shù)出錯。使用 JSON 進(jìn)行序列化可以顯式地讓老的 Network 仍然能夠 load。
其他也還有一些微小的變動。但是那些只是 network.py 的微調(diào)。結(jié)果就是把程序從 $$74$$ 行增長到了 $$152$$ 行。
問題
- 更改上面的代碼來實現(xiàn) L1 規(guī)范化,使用 L1 規(guī)范化使用 $$30$$ 個隱藏元的神經(jīng)網(wǎng)絡(luò)對 MNIST 數(shù)字進(jìn)行分類。你能夠找到一個規(guī)范化參數(shù)使得比無規(guī)范化效果更好么?
- 看看 network.py 中的 Network.cost_derivative 方法。這個方法是為二次代價函數(shù)寫的。怎樣修改可以用于交叉熵代價函數(shù)上?你能不能想到可能在交叉熵函數(shù)上遇到的問題?在 network2.py 中,我們已經(jīng)去掉了 Network.cost_derivative 方法,將其集成進(jìn)了 CrossEntropyCost.delta 方法中。請問,這樣是如何解決你已經(jīng)發(fā)現(xiàn)的問題的?
總結(jié)
以上是生活随笔為你收集整理的进神经网络的学习方式(译文)----中的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 开始使用WebRTC
- 下一篇: 亲手打造自己的 Linux 桌面环境