前言
訓練神經網絡模型時,如果訓練樣本較少,為了防止模型過擬合,Dropout可以作為一種trikc供選擇。Dropout是hintion最近2年提出的,源於其文章Improving neural networks by preventing co-adaptation of feature detectors.中文大意為:通過阻止特征檢測器的共同作用來提高神經網絡的性能。本篇博文就是按照這篇論文簡單介紹下Dropout的思想,以及從用一個簡單的例子來說明該如何使用dropout。
基礎知識:
Dropout是指在模型訓練時隨機讓網絡某些隱含層節點的權重不工作,不工作的那些節點可以暫時認為不是網絡結構的一部分,但是它的權重得保留下來(只是暫時不更新而已),因為下次樣本輸入時它可能又得工作了(有點抽象,具體實現看后面的實驗部分)。
按照hinton的文章,他使用Dropout時訓練階段和測試階段做了如下操作:
在樣本的訓練階段,在沒有采用pre-training的網絡時(Dropout當然可以結合pre-training一起使用),hintion並不是像通常那樣對權值采用L2范數懲罰,而是對每個隱含節點的權值L2范數設置一個上限bound,當訓練過程中如果該節點不滿足bound約束,則用該bound值對權值進行一個規范化操作(即同時除以該L2范數值),說是這樣可以讓權值更新初始的時候有個大的學習率供衰減,並且可以搜索更多的權值空間(沒理解)。
在模型的測試階段,使用”mean network(均值網絡)”來得到隱含層的輸出,其實就是在網絡前向傳播到輸出層前時隱含層節點的輸出值都要減半(如果dropout的比例為50%),其理由文章說了一些,可以去查看(沒理解)。
關於Dropout,文章中沒有給出任何數學解釋,Hintion的直觀解釋和理由如下:
1. 由於每次用輸入網絡的樣本進行權值更新時,隱含節點都是以一定概率隨機出現,因此不能保證每2個隱含節點每次都同時出現,這樣權值的更新不再依賴於有固定關系隱含節點的共同作用,阻止了某些特征僅僅在其它特定特征下才有效果的情況。
2. 可以將dropout看作是模型平均的一種。對於每次輸入到網絡中的樣本(可能是一個樣本,也可能是一個batch的樣本),其對應的網絡結構都是不同的,但所有的這些不同的網絡結構又同時share隱含節點的權值。這樣不同的樣本就對應不同的模型,是bagging的一種極端情況。個人感覺這個解釋稍微靠譜些,和bagging,boosting理論有點像,但又不完全相同。
3. native bayes是dropout的一個特例。Native bayes有個錯誤的前提,即假設各個特征之間相互獨立,這樣在訓練樣本比較少的情況下,單獨對每個特征進行學習,測試時將所有的特征都相乘,且在實際應用時效果還不錯。而Droput每次不是訓練一個特征,而是一部分隱含層特征。
4. 還有一個比較有意思的解釋是,Dropout類似於性別在生物進化中的角色,物種為了使適應不斷變化的環境,性別的出現有效的阻止了過擬合,即避免環境改變時物種可能面臨的滅亡。
文章最后當然是show了一大把的實驗來說明dropout可以阻止過擬合。這些實驗都是些常見的benchmark,比如Mnist, Timit, Reuters, CIFAR-10, ImageNet.
實驗過程:
本文實驗時用mnist庫進行手寫數字識別,訓練樣本2000個,測試樣本1000個,用的是matlab的https://github.com/rasmusbergpalm/DeepLearnToolbox,代碼在test_example_NN.m上修改得到。關於該toolbox的介紹可以參考網友的博文【面向代碼】學習 Deep Learning(一)Neural Network。這里我只用了個簡單的單個隱含層神經網絡,隱含層節點的個數為100,所以輸入層-隱含層-輸出層節點依次為784-100-10. 為了使本例子簡單話,沒用對權值w進行規則化,采用mini-batch訓練,每個mini-batch樣本大小為100,迭代20次。權值采用隨機初始化。
實驗結果:
沒用Dropout時:
訓練樣本錯誤率(均方誤差):0.032355
測試樣本錯誤率:15.500%
使用Dropout時:
訓練樣本錯誤率(均方誤差):0.075819
測試樣本錯誤率:13.000%
可以看出使用Dropout后,雖然訓練樣本的錯誤率較高,但是訓練樣本的錯誤率降低了,說明Dropout的泛化能力不錯,可以防止過擬合。
實驗主要代碼及注釋:
test_dropout.m:
%% //導入minst數據並歸一化 load mnist_uint8; train_x = double(train_x(1:2000,:)) / 255; test_x = double(test_x(1:1000,:)) / 255; train_y = double(train_y(1:2000,:)); test_y = double(test_y(1:1000,:)); % //normalize [train_x, mu, sigma] = zscore(train_x);% //歸一化train_x,其中mu是個行向量,mu是個列向量 test_x = normalize(test_x, mu, sigma);% //在線測試時,歸一化用的是訓練樣本的均值和方差,需要特別注意 %% //without dropout rng(0); nn = nnsetup([784 100 10]);% //初步構造了一個輸入-隱含-輸出層網絡,其中包括了 % //權值的初始化,學習率,momentum,激發函數類型, % //懲罰系數,dropout等 opts.numepochs = 20; % //Number of full sweeps through data opts.batchsize = 100; % //Take a mean gradient step over this many samples [nn, L] = nntrain(nn, train_x, train_y, opts); [er, bad] = nntest(nn, test_x, test_y); str = sprintf('testing error rate is: %f',er); disp(str) %% //with dropout rng(0); nn = nnsetup([784 100 10]); nn.dropoutFraction = 0.5; % //Dropout fraction,每一次mini-batch樣本輸入訓練時,隨機扔掉50%的隱含層節點 opts.numepochs = 20; % //Number of full sweeps through data opts.batchsize = 100; % //Take a mean gradient step over this many samples nn = nntrain(nn, train_x, train_y, opts); [er, bad] = nntest(nn, test_x, test_y); str = sprintf('testing error rate is: %f',er); disp(str)
下面來分析與dropout相關的代碼,集中在上面test.m代碼的后面with drop部分。首先在訓練過程中需要將神經網絡結構nn的dropoutFraction設置為一定比例,這里設置為50%:nn.dropoutFraction = 0.5;
然后進入test_dropout.m中的nntrain()函數,沒有發現與dropoutFraction相關的代碼,繼續進入網絡前向傳播函數nnff()函數中,在網絡的隱含層節點激發函數值被計算出來后,有下面的代碼:
if(nn.dropoutFraction > 0) if(nn.testing) nn.a{i} = nn.a{i}.*(1 - nn.dropoutFraction); else nn.dropOutMask{i} = (rand(size(nn.a{i}))>nn.dropoutFraction); nn.a{i} = nn.a{i}.*nn.dropOutMask{i}; end end
由上面的代碼可知,隱含層節點的輸出值以dropoutFraction百分比的幾率被隨機清0(注意此時是在訓練階段,所以是else那部分的代碼),既然前向傳播時有些隱含節點值被清0了,那么在誤差方向傳播時也應該有相應的處理,果然,在反向傳播函數nnbp()中,有下面的代碼:
if(nn.dropoutFraction>0) d{i} = d{i} .* [ones(size(d{i},1),1) nn.dropOutMask{i}]; end
也就是說計算節點誤差那一項時,其誤差項也應該清0。從上面可以看出,使用dropout時,其訓練部分的代碼更改很少。
(有網友發私信說,反向傳播計算誤差項時可以不用乘以dropOutMask{i}矩陣,后面我仔細看了下bp的公式,一開始也感覺不用乘有道理。因為源碼中有為:
for i = 1 : (n - 1) if i+1==n nn.dW{i} = (d{i + 1}' * nn.a{i}) / size(d{i + 1}, 1); else nn.dW{i} = (d{i + 1}(:,2:end)' * nn.a{i}) / size(d{i + 1}, 1); end end
代碼進行權重更新時,由於需要乘以nn.a{i},而nn.a{i}在前向過程中如果被mask清掉的話(使用了dropout前提下),則已經為0了。但其實這時錯誤的,因為對誤差
敏感值作用的是與它相連接的前一層權值,並不是本層的權值,而本層的輸出a只對它的下一層權值更新有效。)
再來看看測試部分,測試部分如hintion論文所說的,采用mean network,也就是說前向傳播時隱含層所有節點的輸出同時減小dropoutFraction百分比,即保留(1- dropoutFraction)百分比,代碼依舊是上面貼出的nnff()函數里滿足if(nn.testing)的部分:
if(nn.dropoutFraction > 0) if(nn.testing) nn.a{i} = nn.a{i}.*(1 - nn.dropoutFraction); else nn.dropOutMask{i} = (rand(size(nn.a{i}))>nn.dropoutFraction); nn.a{i} = nn.a{i}.*nn.dropOutMask{i}; end end
上面只是個簡單的droput實驗,可以用來幫助大家理解dropout的思想和使用步驟。其中網絡的參數都是采用toolbox默認的,並沒有去調整它,如果該實驗將訓練樣本增大,比如6w張,則參數不變的情況下使用了dropout的識別率還有可能會降低(當然這很有可能是其它參數沒調到最優,另一方面也說明在樣本比較少的情況下,droput確實可以防止過擬合),為了體現droput的優勢,這里我只用了2000張訓練樣本。
參考資料:
Hinton, G. E., et al. (2012). "Improving neural networks by preventing co-adaptation of feature detectors." arXiv preprint arXiv:1207.0580.
https://github.com/rasmusbergpalm/DeepLearnToolbox
【面向代碼】學習 Deep Learning(一)Neural Network