前面學習了搭建網絡模型的各個層級與結構,想要訓練得到1個良好的網絡模型,正確的權值初始化方法可以加快模型的收斂,相反,不恰當的權值初始化可能導致梯度爆炸或消失,最終導致模型無法訓練。因此,本節主要從3方面來分析了解權值初始化:(1)分析不恰當的權值初始化是如何引發梯度消失與爆炸的?(2)學習常用的Xavier與Kaiming權值初始化方法;(3)學習Pytorch中10種權值初始化方法。
梯度爆炸和消失
一、理論

根據計算圖,列寫出計算圖的前向傳播計算公式,關注第2個隱藏層權值矩陣 \(\ W_2\) 的梯度如何求取?

要求 \(\ W_2\) 權值矩陣的梯度,從上述求解過程中可以看出,\(\ H_1\)是上一層神經元的輸出值,\(\ W_2\) 的梯度依賴於上一層的輸出:①如果\(\ H_1\)輸出值非常小,趨近於0,那么\(\ W_2\) 的梯度也就趨於0,從而導致梯度消失;②同理,如果\(\ H_1\)輸出值非常大,趨近於無窮大,\(\ W_2\) 的梯度也就趨於無窮大,從而導致梯度爆炸。一旦發生梯度爆炸或消失,就會引發模型無法訓練的問題。
結論:
從公式求導(求梯度)的角度,要避免梯度消失和爆炸的產生,就要嚴格控制網絡輸出層(輸出值)的尺度范圍,也就是要求每一層網絡的輸出值不能太大或者太小。
二、實驗
(1)實驗1——引發梯度爆炸,出現nan現象
代碼分析:
這里采用layer_nums=100層全連接網絡,每一層神經元個數為neural_num=256,輸入數據batch_size=16,構建MLP模型。
(1)init 函數:采用Modulelist和列表生成式,通過for循環,循環構建網絡層;又由於Modulelist不能自動前向傳播,因此將構建好的Modulelist賦值給linear屬性;
(2)forward函數:模型模塊Modulelist構建好后,拼接子模塊在forward中實現前向傳播,只需要利用for循環依次從linear中獲取每個全連接層,對全連接層實現前向傳播,就可以返回輸出值x。
(3)initialize初始化:對每一個模塊進行for循環判斷是否為線性層linear,如果是,采用標准正態分布(0均值、1標准差)對權值\(\ W\) 進行初始化。
構建好全連接網絡后,再構建1個0均值、1標准差的隨機輸入input,然后輸入進net中觀察其輸出output。
class MLP(nn.Module):
def __init__(self, neural_num, layers):
super(MLP, self).__init__()
self.linears = nn.ModuleList([nn.Linear(neural_num, neural_num, bias=False) for i in range(layers)])
self.neural_num = neural_num
def forward(self, x):
for (i, linear) in enumerate(self.linears):
x = linear(x)
return x
def initialize(self):
for m in self.modules():
if isinstance(m, nn.Linear):
nn.init.normal_(m.weight.data) # normal: mean=0, std=1
flag = 1
if flag:
layer_nums = 100
neural_nums = 256
batch_size = 16
net = MLP(neural_nums, layer_nums)
net.initialize()
inputs = torch.randn((batch_size, neural_nums)) # normal: mean=0, std=1
output = net(inputs)
print(output)
實驗結果

結果表明:
Output的每一個值都為nan
即數據非常大或者非常小,已經超出了當前精度可表示的范圍。
結果分析:
到forward中觀察,什么時候數據變化到了nan
,這里采用標准差來衡量數據的尺度范圍。
打印網絡每一層的標准差std,設置判斷if,判斷x的標准差為nan時,模型停止向前傳播。
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()))
if torch.isnan(x.std()):
print("output is nan in {} layers".format(i))
break
return x
實驗結果


結果表明:
從實驗結果可以看出,第31層數據的標准差出現了nan
,std可能已經達到 \(\ 10 ^{38}\) 或\(\ 10 ^{39}\);就數據tensor而言,出現了非常大或者非常小的數據即正無窮inf或負無窮-inf,再向前傳播,當前精度已經無法表示非常大或者非常小的數據。就標准差而言,std逐層變大,從15.95到256.623再到4107.245...
結果分析:為什么出現了nan現象?以及如何抑制nan出現?
下面根據公式推導分析,為什么模型網絡輸出層的標准差會逐層變大?

抑制nan現象出現:**由圖4所示,得到1個重要結論,想要控制網絡層輸出值尺度不變,始終為1,那么有:
(2)實驗2:保持網絡層輸出值尺度不變的初始化
采用0均值,標准差為 \(\sqrt{\frac{1}{n}} \quad\) 的分布初始化權值\(W\),再觀察網絡層輸出值標准差的特點。
def initialize(self):
for m in self.modules():
if isinstance(m, nn.Linear):
nn.init.normal_(m.weight.data, std=np.sqrt(1/self.neural_num)) ## normal: mean=0, std=1/n
實驗結果:

結果表明:
根據輸出結果可以看出,每層數據標准差都能維持在1左右,采用恰當的權值初始化方法可以實現多層全連接網絡輸出值尺度維持在一定范圍內。 通過以上實例,我們知道需要保持每個網絡層輸出數據的方差為1,但目前還未考慮激活函數的存在,下面學習具有激活函數的權值初始化。
Xavier和Kaiming方法
一、Xavier初始化
實驗:具有激活函數的權值初始化—引發梯度消失
def forward(self, x):
for (i, linear) in enumerate(self.linears):
x = linear(x)
x = torch.tanh(x)
在forward中,每個linear后進行1個tanh()激活函數
print("layer:{}, std:{}".format(i, x.std()))
if torch.isnan(x.std()):
print("output is nan in {} layers".format(i))
break
return x
實驗結果:

結果表明: 看到網絡層輸出值的標准差隨着網絡層的前向傳播變得越來越小,說明網絡層輸出數據變得越來越小,從而導致梯度消失。
針對以上具有激活函數權值初始化的問題,2010年Xavier詳細探討了具有激活函數應該如何進行初始化。文獻中,結合 "方差一致性原則:要求每個網絡層輸出值的方差為1",同時針對飽和激活函數sigmoid、Tanh激活函數進行了分析。
Xavier初始化
1、理論
通過 《Understanding the diffculty of training deep feedforward nerual networks》 文章中的公式推導, 權值的方差\(D(W)\)滿足如下公式:其中,\(\ n_i\)為輸入層神經元個數,\(\ n_{i+1}\)為輸出層神經元個數,下式是同時考慮了前向傳播和反向傳播的數據尺度問題得到的:
通常,Xavier采用均勻分布,下面推導均勻分布的上、下限。因為采用0均值,因此,分布的上下限是對稱關系\(\ {W\sim U[-a ,a]}\):
上下聯立:\(\ D(W)=\frac{a^2}{3}=\frac{2}{n_i +n_{i+1}}\),解得:\(\ a=\frac{\sqrt{6}}{\sqrt {n_i+n_{i+1}}}\)
因此,權值分布的上、下限為:
2、驗證實驗
①手動計算,采用Xavier對權值進行初始化再觀察網絡層的輸出。
def initialize(self):
for m in self.modules():
if isinstance(m, nn.Linear):
#計算a的數值大小(由於本例中輸入和輸出神經元個數相同,所以同值)
a = np.sqrt(6 / (self.neural_num + self.neural_num))
#利用Pytorch的內置函數calculate_gain計算tanh增益
tanh_gain = nn.init.calculate_gain('tanh')
a *= tanh_gain
#利用上、下限對權值進行均勻分布初始化
nn.init.uniform_(m.weight.data, -a, a)
實驗結果:

②Pytorch中提供了Xavier,對權值進行初始化,觀察與手動計算的輸出結果有無區別。
def initialize(self):
for m in self.modules():
if isinstance(m, nn.Linear):
nn.init.xavier_uniform_(m.weight.data, gain=tanh_gain)
實驗結果:

該結果印證了Pytorch提供的Xavier初始化方法與手動計算均勻分布上、下限的Xavier初始化方法基本相同,沒有太大區別。雖然2010年針對飽和激活函數提出了有效初始化方法,但是在2010年Alexnet出現以后,非飽和激活函數被廣泛使用,Xavier針對非飽和激活函數不再適用。
Kaiming初始化
1、理論
針對這一問題,2015年何愷明等人發表 《Delving deep into rectifiers:Surpassing human-level performance on ImageNet classification》 提出了解決方法,在文中同樣遵循方差一致性原則:保持數據尺度維持在恰當范圍,即讓每個輸出層方差為1,針對ReLU激活函數(及其變種)。
通過公式推導,權值的方差\(D(W)\)滿足如下公式,其中,\(\ n_i\)為輸入層神經元個數:
進一步,針對ReLU的變種(即負半軸存在斜率)權值的方差\(D(W)\)有,a為負半軸的斜率:
因此,權值矩陣\(std(W)\)有:
2、驗證實驗
下面通過上述公式對權值\(W\)進行初始化,觀察網絡層的輸出。
①手動計算的Kaiming初始化方法
def initialize(self):
for m in self.modules():
if isinstance(m, nn.Linear):
nn.init.normal_(m.weight.data, std=np.sqrt(2 / self.neural_num))
②Pytorch中提供的Kaiming初始化方法
def initialize(self):
for m in self.modules():
if isinstance(m, nn.Linear):
nn.init.kaiming_normal_(m.weight.data)
常用的初始化方法
不良的權值初始化會引起輸出層輸出值過大、過小從而引起梯度爆炸或消失,導致模型無法訓練的問題。為避免這一問題,要控制網絡層輸出值的尺度范圍,使每一個網絡層輸出值的方差盡量為1,不讓方差過大或者過小。
Pytorch提供的10大初始化方法:
1、Xavier均勻分布
2、Xavier標准正態分布
3、Kaiming均勻分布
4、Kaiming標准正態分布
5、均勻分布
6、正態分布
7、常數分布
8、正交矩陣初始化
9、單位矩陣初始化
10、稀疏矩陣初始化
無論選擇哪一種初始化方法都需要遵循方差一致性原則。
實驗——函數 "calculate_gain方差變化尺度"
nn.init.calculate_gain
主要功能:計算激活函數的方差變化尺度
主要參數:nonlinearity-激活函數名稱
param:激活函數的參數,如Leaky ReLU的negative_slop
① 手動計算輸入輸出增益gain
x = torch.randn(10000)
out = torch.tanh(x)
gain = x.std() / out.std()
print('gain:{}'.format(gain))
② Pytorch中提供求增益gain
x = torch.randn(10000)
out = torch.tanh(x)
tanh_gain = nn.init.calculate_gain('tanh')
print('tanh_gain in PyTorch:', tanh_gain)

對於0均值、1標准差的數據x經過tanh后,標准差會減小約1.6倍。