引數隨機初始化
轉載自:https://zhuanlan.zhihu.com/p/148034113
一:引數初始化類別
引數初始化分為:固定值初始化、預訓練初始化和隨機初始化。
固定初始化:
是指將模型引數初始化為一個固定的常數,這意味著所有單元具有相同的初始化狀態,所有的神經元都具有相同的輸出和更新梯度,並進行完全相同的更新,這種初始化方法使得神經元間不存在非對稱性,從而使得模型效果大打折扣。
預訓練初始化:
是神經網路初始化的有效方式,比較早期的方法是使用 greedy layerwise auto-encoder 做無監督學習的預訓練,經典代表為 Deep Belief Network;而現在更為常見的是有監督的預訓練+模型微調。
隨機初始化:
是指隨機進行引數初始化,但如果不考慮隨機初始化的分佈則會導致梯度爆炸和梯度消失的問題。
我們這裡主要關注隨機初始化的分佈狀態。
二:Naive Initialization
先介紹兩個用的比較多的初始化方法:高斯分佈和均勻分佈。
以均勻分佈為例,通常情況下我們會將引數初始化為,我們來看下效果:
class MLP(nn.Module): def __init__(self, neurals, layers): super(MLP, self).__init__() self.linears = nn.ModuleList( [nn.Linear(neurals, neurals, bias=False) for i in range(layers)]) self.neurals = neurals def forward(self, x): for (i, linear) in enumerate(self.linears): x = linear(x) print("layer:{}, std:{}".format(i+1, x.std())) if torch.isnan(x.std()): break returnx def initialize(self): for m in self.modules(): if isinstance(m, nn.Linear): a = np.sqrt(1/self.neurals) nn.init.uniform_(m.weight.data, -a, a) neural_nums=256 layers_nums=100 batch_size=16 net = MLP(neural_nums, layers_nums) net.initialize() inputs = torch.randn((batch_size, neural_nums)) output = net(inputs)
輸出為:
layer:0, std:0.5743116140365601 layer:1, std:0.3258207142353058 layer:2, std:0.18501722812652588 layer:3, std:0.10656329244375229 ... ... layer:95, std:9.287707510161138e-24 layer:96, std:5.310323679717446e-24 layer:97, std:3.170952429065466e-24 layer:98, std:1.7578611563776362e-24 layer:99, std:9.757115839154053e-25
我們可以看到,隨著網路層數加深,權重的方差越來越小,直到最後超出精度範圍。
我們先通過數學推導來解釋一下這個現象,以第一層隱藏層的第一個單元為例。
首先,我們是沒有啟用函式的線性網路:
其中,n 為輸入層神經元個數。
通過方差公式我們有:(x、w、b相互獨立。各層的權重 w 獨立同分布。且偏置項b方差為0,一般設定為常數)
因為:
所以:
最終,方差公式有:
這裡,我們的輸入樣本x均值為0,方差為1。權重w(前面提及均勻分佈)的均值為0,方差為。所以:
此時,神經元的標準差為。
通過上式進行計算,每一層神經元的標準差都將會是前一層神經元的倍。
我們可以看一下上面列印的輸出,是不是正好驗證了這個規律。
而這種初始化方式合理嗎?有沒有更好的初始化方法?
三:Xavier Initialization
Xavier Glorot 認為:優秀的初始化應該使得各層的啟用值和狀態梯度在傳播過程中的方差保持一致。即方差一致性。
所以我們需要同時考慮正向傳播和反向傳播的輸入輸出的方差相同。
在開始推導之前,我們先引入一些必要的假設:
- x、w、b 相同獨立;
- 各層的權重 w 獨立同分布,且均值為 0;
- 偏置項 b 獨立同分布,且方差為 0;
- 輸入項 x 獨立同分布,且均值為 0;
(一)前向傳播
考慮前向傳播:
我們令輸入的方差等於輸出得到方差:
則有:
(二)反向傳播
此外,我們還要考慮反向傳播的梯度狀態。
反向傳播:
我們也可以得到下一層的方差:
我們取其平均,得到權重的方差為:
此時,均勻分佈為:(方差逆推均勻分佈)
我們來看下實驗部分,只需修改類裡面的初始化函式:
class MLP(nn.Module): ... def initialize(self): a = np.sqrt(3/self.neurals) for m in self.modules(): if isinstance(m, nn.Linear): nn.init.uniform_(m.weight.data, -a, a)
輸出結果:
layer:0, std:0.9798752665519714 layer:1, std:0.9927620887756348 layer:2, std:0.9769216179847717 layer:3, std:0.9821343421936035 ... layer:97, std:0.9224138855934143 layer:98, std:0.9622119069099426 layer:99, std:0.9693211317062378
這便達到了我們的目的,即輸入和輸出的方差保持一致。
(三)使用啟用函式
但在實際過程中,我們還會使用啟用函式,所以我們在 forward 中加入 sigmoid 函式:
class MLP(nn.Module): ... def forward(self, x): for (i, linear) in enumerate(self.linears): x = linear(x) x = torch.sigmoid(x) print("layer:{}, std:{}".format(i, x.std())) if torch.isnan(x.std()): break return x ...
再看下輸出結果:(良好)
layer:0, std:0.21153637766838074 layer:1, std:0.13094832003116608 layer:2, std:0.11587061733007431 ... layer:97, std:0.11739246547222137 layer:98, std:0.11711347848176956 layer:99, std:0.11028502136468887
好像還不錯,也沒有出現方差爆炸的問題。
不知道大家看到這個結果會不會有些疑問:為什麼方差不是 1 了?
這是因為 sigmoid 的輸出都為正數,所以會影響到均值的分佈,所以會導致下一層的輸入不滿足均值為 0 的條件。我們將均值和方差一併打出:
layer:0, mean:0.5062727928161621 layer:0, std:0.20512282848358154 layer:1, mean:0.47972571849823 layer:1, std:0.12843772768974304 ... layer:98, mean:0.5053208470344543 layer:98, std:0.11949671059846878 layer:99, mean:0.49752169847488403 layer:99, std:0.1192963495850563
可以看到,第一層隱藏層(layer 0)的均值就已經變成了 0.5。
這又會出現什麼問題呢?
答案是出現 “zigzag” 現象(只有正數輸出(不是zero-centered))。https://www.zhihu.com/question/50396271?from=profile_question_card
而零均值能避免 zigzag,能提高網路的訓練效率。因此sigmoid 函式再效率上不如零均值的 tanh函式。
為此,我們可以使用,改變 sigmoid 的尺度與範圍,改用 tanh:
tanh 的收斂速度要比 sigmoid 快,這是因為 tanh 的均值更加接近 0,SGD 會更加接近 natural gradient,從而降低所需的迭代次數。
我們使用 tanh 做一下實驗,看下輸出結果:
layer:0, mean:-0.011172479018568993 layer:0, std:0.6305743455886841 layer:1, mean:0.0025750682689249516 layer:1, std:0.4874609708786011 ... layer:98, mean:0.0003803471918217838 layer:98, std:0.06665021181106567 layer:99, mean:0.0013235544320195913 layer:99, std:0.06700969487428665
可以看到,在前向傳播過程中,均值沒有出問題,但是方差一直在減小。
這是因為,輸出的資料經過 tanh 後標準差發生了變換,所以在實際初始化過程中我們還需要考慮啟用函式的計算增益:
class MLP(nn.Module): ... def initialize(self): for m in self.modules(): if isinstance(m, nn.Linear): tanh_gain = nn.init.calculate_gain('tanh') a = np.sqrt(3/self.neurals) a *= tanh_gain nn.init.uniform_(m.weight.data, -a, a)
輸出為:
layer:0, std:0.7603299617767334 layer:1, std:0.6884239315986633 layer:2, std:0.6604527831077576 ... layer:97, std:0.6512776613235474 layer:98, std:0.643700897693634 layer:99, std:0.6490980386734009
此時,方差就被修正過來了。
當然,在實際過程中我們也不需要自己寫,可以直接呼叫現成的函式:
class MLP(nn.Module): ... def initialize(self): a = np.sqrt(3/self.neurals) for m in self.modules(): if isinstance(m, nn.Linear): tanh_gain = nn.init.calculate_gain('tanh') nn.init.xavier_uniform_(m.weight.data, gain=tanh_gain)
在這裡,不知道同學們會不會有一個疑問,為什麼 sigmoid 不會出現 tanh 的情況呢?
這是因為 sigmoid 的資訊增益為 1,而 tanh 的資訊增益為 5/3。----待解決??
tanh 和 sigmoid 有兩大缺點:
- 需要進行指數運算;
- 有軟飽和區域,導致梯度更新速度很慢。
所以我們經常會用到 ReLU,所以我們試一下效果:
class MLP(nn.Module): def __init__(self, neurals, layers): super(MLP, self).__init__() self.linears = nn.ModuleList( [nn.Linear(neurals, neurals, bias=False) for i in range(layers)]) self.neurals = neurals def forward(self, x): for (i, linear) in enumerate(self.linears): x = linear(x) x = torch.relu(x) print("layer:{}, std:{}".format(i, x.std())) return x def initialize(self): for m in self.modules(): if isinstance(m, nn.Linear): tanh_gain = nn.init.calculate_gain('relu') a = np.sqrt(3/self.neurals) a *= tanh_gain nn.init.uniform_(m.weight.data, -a, a)
輸出為:
layer:0, std:1.4423831701278687 layer:1, std:2.3559958934783936 layer:2, std:4.320342540740967 ... layer:97, std:1.3732810130782195e+23 layer:98, std:2.3027095847369547e+23 layer:99, std:4.05964954791109e+23
為什麼 Xavier 突然失靈了呢?
這是因為 Xavier 只能針對類似 sigmoid 和 tanh 之類的飽和啟用函式,而無法應用於 ReLU 之類的非飽和啟用函式。
針對這一問題,何凱明於 2015 年發表了一篇論文《Delving Deep into Rectifiers: Surpassing Human-Level Performance on ImageNet Classification》,給出瞭解決方案。
在介紹 kaiming 初始化之前,這裡補充下飽和啟用函式的概念。
- x 趨於正無窮時,啟用函式的導數趨於 0,則我們稱之為右飽和;
- x 趨於負無窮時,啟用函式的導數趨於 0,則我們稱之為左飽和;
- 當一個函式既滿足右飽和又滿足左飽和時,我們稱之為飽和啟用函式,代表有 sigmoid,tanh;
- 存在常數 c,當 x>c 時,啟用函式的導數恆為 0,我們稱之為右硬飽和,同理左硬飽和。兩者同時滿足時,我們稱之為硬飽和啟用函式,ReLU 則為左硬飽和啟用函式;
- 存在常數 c,當 x>c 時,啟用函式的導數趨於 0,我們稱之為右軟飽和,同理左軟飽和。兩者同時滿足時,我們稱之為軟飽和啟用函式,sigmoid,tanh 則為軟飽和啟用函式;
四:Kaiming Initialization
同樣遵循方差一致性原則。
啟用函式為,所以輸入值的均值就不為 0 了,所以:
注意:這裡是使用kaiming均勻分佈初始化,所以E(w)=0,第一步中可以消去最後項
其中:
我們將其帶入,可以得到:
所以引數服從。(這裡注意,凱明初始化的時候,預設是使用輸入的神經元個數)
我們試一下結果:
class MLP(nn.Module): ... def initialize(self): a = np.sqrt(3/self.neurals) for m in self.modules(): if isinstance(m, nn.Linear): a = np.sqrt(6 / self.neurals) nn.init.uniform_(m.weight.data, -a, a)
輸出為:
layer:0, std:0.8505409955978394 layer:1, std:0.8492708802223206 layer:2, std:0.8718656301498413 ... layer:97, std:0.8371583223342896 layer:98, std:0.7432138919830322 layer:99, std:0.6938706636428833
可以看到,結果要好很多。
再試一下凱明均勻分佈:
class MLP(nn.Module): ... def initialize(self): a = np.sqrt(3/self.neurals) for m in self.modules(): if isinstance(m, nn.Linear): nn.init.kaiming_uniform_(m.weight.data)
輸出為:
layer:0, std:0.8123029470443726 layer:1, std:0.802753210067749 layer:2, std:0.758887529373169 ... layer:97, std:0.2888352870941162 layer:98, std:0.26769548654556274 layer:99, std:0.2554236054420471
那如果啟用函式是 ReLU 的變種怎麼辦呢?
這裡直接給結論:
我們上述介紹的都是以均勻分佈為例,而正態分佈也是一樣的。均值 0,方差也計算出來了,所服從的分佈自然可知。
補充:ReLU 函式和 sigmoid 函式一樣,也是非零均值的,為什麼可以提高效率?
的確,使用 tanh 代替 sigmoid 就是為了提高其迭代效率,可是雖然 ReLU 也是非零均值函式,但是 ReLU 不需要求自然指數呀,因此其計算複雜度要遠遠小於 tanh,就算非零均值,小計算量也可以幫助 ReLU 的梯度快速收斂。總而言之,瑕不掩瑜。
不過,ReLU 函式有一個問題——在負數區硬飽和。當輸入落在小於零的區間時,ReLU 函式將直接輸出零,這種現象被稱作Dead ReLU,但這一特性也被列入 ReLU 的優勢:類似於DropOut正則化。(可以使用變種Relu)