這是一篇水貨寫的筆記,希望路過的大牛可以指出其中的錯誤,帶蒟蒻飛啊~
一、 梯度消失/梯度爆炸的問題
首先來說說梯度消失問題產生的原因吧,雖然是已經被各大牛說爛的東西。不如先看一個簡單的網絡結構,
可以看到,如果輸出層的值僅是輸入層的值與權值矩陣W的線性組合,那么最終網絡最終的輸出會變成輸入數據的線性組合。這樣很明顯沒有辦法模擬出非線性的情況。記得神經網絡是可以擬合任意函數的。好了,既然需要非線性函數,那干脆加上非線性變換就好了。一般會使用sigmoid函數,得到,這個函數會把數據壓縮到開區間(0,1),函數的圖像如下
可以看到,函數的兩側非常平滑,而且無限的接近0和1,僅僅是中間部分函數接近一條直線,順便說一下,這個函數的導數最值竟然真的是1啊,也就是x=0的位置,因而稱這個函數是雙端飽和的,而且它是處處可導。那么,為什么要選用它呢?看到有資料說是模擬神經學科的,但我不太懂這個,個人認為是因為(1)可以引入非線性(2)容易求導(3)可以把數據壓縮,這樣數據不容易發散。
另外,有一個函數與sigmoid函數很類似,就是tanh()函數,它可以把數據壓縮到(-1,1)之間。
但是,我們要講的是梯度的消失問題哦,要知道,神經網絡訓練的方法是BP算法(不知道還有沒有其他的訓練方法。。。)。BP算法的基礎其實就是導數的鏈式法則,這個估計不需要細說了,就是有很多乘法會連接在一起。在看看sigmoid函數的圖像就知道了,導數最大是1,而且大多數值都被推向兩側飽和的區域,這些區域的導數可是很小的呀~~~~可以預見到,隨着網絡的加深,梯度后向傳播到淺層網絡時,就呵呵了,基本不能引起數值的擾動,這樣淺層的網絡就學習不到新的特征了。
那么怎么辦?我暫時看到了四種解決問題的辦法,僅僅是根據我自己看的論文總結的,並非權威的說法。第一種很明顯,可以通過使用別的激活函數;第二種可以使用層歸一化;第三種是在權重的初始化上下功夫,第四種是構建新的網絡結構~。但暫時不寫,我還想記錄一下看到的梯度消失/爆炸問題在另一個經典網絡的出現。
======================
噔噔噔噔,對的,就是RNN。
RNN網絡簡單來說,就是把上層的hidden state與輸入數據一同輸入到神經元中進行處理(如左圖),它是與序列相關的。如果把網絡按照時間序列展開,可以得到右圖
假如要求偏導數,可以看到一個連乘的式子,元素是
,假如
大於1,經過k個乘法后會變得異常巨大,畢竟是指數級的,如果小於1,又會變得十分小。這就是RNN中梯度爆炸與消失的問題了。
貼一個RNN的代碼,有注釋,很容易看明白,來自這里

1 import copy, numpy as np 2 np.random.seed(0) 3 4 # compute sigmoid nonlinearity 5 def sigmoid(x): 6 output = 1/(1+np.exp(-x)) 7 return output 8 9 # convert output of sigmoid function to its derivative 10 def sigmoid_output_to_derivative(output): 11 return output*(1-output) 12 13 14 # training dataset generation 15 int2binary = {} 16 binary_dim = 8 17 18 largest_number = pow(2,binary_dim) 19 binary = np.unpackbits( 20 np.array([range(largest_number)],dtype=np.uint8).T,axis=1) 21 for i in range(largest_number): 22 int2binary[i] = binary[i] 23 24 25 # input variables 26 alpha = 0.1 27 input_dim = 2 28 hidden_dim = 16 29 output_dim = 1 30 31 32 # initialize neural network weights 33 synapse_0 = 2*np.random.random((input_dim,hidden_dim)) - 1 34 synapse_1 = 2*np.random.random((hidden_dim,output_dim)) - 1 35 synapse_h = 2*np.random.random((hidden_dim,hidden_dim)) - 1 36 37 synapse_0_update = np.zeros_like(synapse_0) 38 synapse_1_update = np.zeros_like(synapse_1) 39 synapse_h_update = np.zeros_like(synapse_h) 40 41 # training logic 42 for j in range(10000): 43 44 # generate a simple addition problem (a + b = c) 45 a_int = np.random.randint(largest_number/2) # int version 46 a = int2binary[a_int] # binary encoding 47 48 b_int = np.random.randint(largest_number/2) # int version 49 b = int2binary[b_int] # binary encoding 50 51 # true answer 52 c_int = a_int + b_int 53 c = int2binary[c_int] 54 55 # where we'll store our best guess (binary encoded) 56 d = np.zeros_like(c) 57 58 overallError = 0 59 60 layer_2_deltas = list() 61 layer_1_values = list() 62 layer_1_values.append(np.zeros(hidden_dim)) 63 64 # moving along the positions in the binary encoding 65 for position in range(binary_dim): 66 67 # generate input and output 68 X = np.array([[a[binary_dim - position - 1],b[binary_dim - position - 1]]]) 69 y = np.array([[c[binary_dim - position - 1]]]).T 70 71 # hidden layer (input ~+ prev_hidden) 72 layer_1 = sigmoid(np.dot(X,synapse_0) + np.dot(layer_1_values[-1],synapse_h)) 73 74 # output layer (new binary representation) 75 layer_2 = sigmoid(np.dot(layer_1,synapse_1)) 76 77 # did we miss?... if so, by how much? 78 layer_2_error = y - layer_2 79 layer_2_deltas.append((layer_2_error)*sigmoid_output_to_derivative(layer_2)) 80 overallError += np.abs(layer_2_error[0]) 81 82 # decode estimate so we can print it out 83 d[binary_dim - position - 1] = np.round(layer_2[0][0]) 84 85 # store hidden layer so we can use it in the next timestep 86 layer_1_values.append(copy.deepcopy(layer_1)) 87 88 future_layer_1_delta = np.zeros(hidden_dim) 89 90 for position in range(binary_dim): 91 92 X = np.array([[a[position],b[position]]]) 93 layer_1 = layer_1_values[-position-1] 94 prev_layer_1 = layer_1_values[-position-2] 95 96 # error at output layer 97 layer_2_delta = layer_2_deltas[-position-1] 98 # error at hidden layer 99 layer_1_delta = (future_layer_1_delta.dot(synapse_h.T) + layer_2_delta.dot(synapse_1.T)) * sigmoid_output_to_derivative(layer_1) 100 101 # let's update all our weights so we can try again 102 synapse_1_update += np.atleast_2d(layer_1).T.dot(layer_2_delta) 103 synapse_h_update += np.atleast_2d(prev_layer_1).T.dot(layer_1_delta) 104 synapse_0_update += X.T.dot(layer_1_delta) 105 106 future_layer_1_delta = layer_1_delta 107 108 109 synapse_0 += synapse_0_update * alpha 110 synapse_1 += synapse_1_update * alpha 111 synapse_h += synapse_h_update * alpha 112 113 synapse_0_update *= 0 114 synapse_1_update *= 0 115 synapse_h_update *= 0 116 117 # print out progress 118 if(j % 1000 == 0): 119 print "Error:" + str(overallError) 120 print "Pred:" + str(d) 121 print "True:" + str(c) 122 out = 0 123 for index,x in enumerate(reversed(d)): 124 out += x*pow(2,index) 125 print str(a_int) + " + " + str(b_int) + " = " + str(out) 126 print "------------" 127 128
二、 選擇其他激活函數
激活函數有很多其他的選擇,常看見的有ReLU、Leaky ReLU函數。下面就說一說這兩個函數,對於ReLU,函數為,觀察一下它的函數圖像
可以看到,它在負數的一段永遠是0啊。為什么要使用這個函數呢?據說它是與神經學科有關的,這是因為稀疏激活的,這表現在負數端是抑制狀態,正數興奮激活。而且有理論也表明,稀疏的網絡更准確,在googLeNet的實現中就是利用了神經網絡的稀疏性。而且,在正數端導數永遠為1,這就很好地解決了梯度消失的問題了。可是,它沒有把數據壓縮,這會使得數據的范圍可能很大。
此外還有Leaky ReLU函數,這個我是在YOLO看到的,其實和ReLU差不多,就是在負數端不完全抑制了。圖像如下:
三、 層歸一化
這里記錄的是Batch Normalization。主要參考(1)(2)(3)寫的總結,可憐我只是個搬運工啊。
先說一說BN解決的問題,論文說要解決 Internal covariate shift 的問題,covariate shift 是指源空間與目標空間中條件概率一致,但是邊緣概率不同。在深度網絡中,越深的網絡對特征的扭曲就越厲害(應該是這樣說吧……),但是特征本身對於類別的標記是不變的,所以符合這樣的定義。BN通過把輸出層的數據歸一化到mean = 0, var = 1的分布中,可以讓邊緣概率大致相同吧(知乎魏大牛說不可以完全解決,因為均值方差相同不代表分布相同~~他應該是對的),所以題目說是reducing。
那么BN是怎么實現的呢?它是通過計算min batch 的均值與方差,然后使用公式歸一化。例如激活函數是sigmoid,那么輸出歸一化后的圖像就是
中間就是接近線性了,這樣,導數幾乎為常數1,這樣不就可以解決梯度消失的問題了嗎?
但是,對於ReLU函數,這個是否起作用呢?好像未必吧,不過我覺得這個歸一化可以解決ReLU不能把數據壓縮的問題,這樣可以使得每層的數據的規模基本一致了。上述(3)中寫到一個BN的優點,我覺得和我的想法是一致的,就是可以使用更高的學習率。如果每層的scale不一致,實際上每層需要的學習率是不一樣的,同一層不同維度的scale往往也需要不同大小的學習率,通常需要使用最小的那個學習率才能保證損失函數有效下降,Batch Normalization將每層、每維的scale保持一致,那么我們就可以直接使用較高的學習率進行優化。這樣就可以加快收斂了。我覺得還是主要用來減少covariate shift 的。
但是,上述歸一化會帶來一個問題,就是破壞原本學習的特征的分布。那怎么辦?論文加入了兩個參數,來恢復它本來的分布這個帶入歸一化的式子看一下就可以知道恢復原來分布的條件了。但是,如果恢復了原來的分布,那還需要歸一化?我開始也沒想明白這個問題,后來看看別人的解釋,注意到新添加的兩個參數,實際上是通過訓練學習的,就是說,最后可能恢復,也可能沒有恢復。這樣可以增加網絡的capicity,網絡中就存在多種不同的分布了。最后抄一下BP的公式:
那么在哪里可以使用這個BN?很明顯,它應該使用在激活函數之前。而在此處還提到一個優點就是加入BN可以不使用dropout,為什么呢?dropout它是用來正則化增強網絡的泛化能力的,減少過擬合,而BN是用來提升精度的,之所以說有這樣的作用,可能有兩方面的原因(1)過擬合一般發生在數據邊緣的噪聲位置,而BN把它歸一化了(2)歸一化的數據引入了噪聲,這在訓練時一定程度有正則化的效果。對於大的數據集,BN的提升精度會顯得更重要,這兩者是可以結合起來使用的。
最后貼一個算法的流程,以及結構圖,結構圖是來自 http://yeephycho.github.io/2016/08/03/Normalizations-in-neural-networks/
四、 權值初始化
為了讓信息可以更好的在網絡中流動(不一定是梯度消失的問題),可以使用xavier的初始化方法。主要可以看知乎專欄。為了不重復別人的工作,我簡單總結一下算了。注意一個問題,xavier的初始化方法的前提假設是,激活函數是線性的(其實歸一化后,可能把數據集中在了一處,就好像將BN的那張圖一樣)。
如果輸入數據x和權值w都滿足均值為0,標准差為,(x可以通過歸一化、白化實現)而且各數據是獨立同分布的。這樣,輸出為
,根據概率公式,z 的均值依然為0,方差為
。
通過遞推公式可以得到,則
。於是,方差的計算公式為
。這里又出現了連乘,還是按照之前與1比較的討論,那么,最好是可以讓方差保持一致啦,這樣數值的幅度就不會相差太大,就好像上面BN說的那樣,可以收斂的更快。那么就是讓連乘內的每一項都為1了,則可以推出權值的初始化為
。
上面說的是前向的,那么后向呢?后向傳播時,如果可以讓方差保持一致,同樣地會有前向傳播的效果,梯度可以更好地在網絡中流動。由於假設是線性的,那么回流的梯度公式是,可以寫成
。令回流的方差不變,那么權值又可以初始化成
。注意一個是前向,一個是后向的,兩者的n 是不同的。取平均
。
最后,是使用均勻分布來初始化權值的,得到初始化的范圍。
另外一種MSRA的初始化的方法,可以學習http://blog.csdn.net/shuzfan/article/details/51347572,實驗效果表現要好一些,但貌似xavier用的要多一些。
五、 調整網絡的結構
解決RNN的問題,提出了一種LSTM的結構,但我對LSTM還不是太熟悉,就不裝逼了。主要是總結最近看的兩篇文章《Training Very Deep Networks》和《Deep Residual Learning for Image Recognition》。
Highway Network
Highway Network主要解決的問題是,網絡深度加深,梯度信息回流受阻造成網絡訓練困難的問題。先看下面的一張對比圖片,分別是沒有highway 和有highway的。
可以看到,當網絡加深,訓練的誤差反而上升了,而加入了highway之后,這個問題得到了緩解。一般來說,深度網絡訓練困難是由於梯度回流受阻的問題,可能淺層網絡沒有辦法得到調整,或者我自己YY的一個原因是(回流的信息經過網絡之后已經變形了,很可能就出現了internal covariate shift類似的問題了)。Highway Network 受LSTM啟發,增加了一個門函數,讓網絡的輸出由兩部分組成,分別是網絡的直接輸入以及輸入變形后的部分。
假設定義一個非線性變換為,定義門函數
,攜帶函數
。對於門函數取極端的情況0/1會有
,而對應的門函數
使用sigmoid函數
,則極端的情況不會出現。
一個網絡的輸出最終變為,注意這里的乘法是element-wise multiplication。
注意,門函數,轉換
,
與
的維度應該是相同的。如果不足,可以用0補或者用一個卷積層去變化。
在初始化的時候,論文是把偏置 b 初始化為負數,這樣可以讓攜帶函數 C 偏大,這樣做的好處是什么呢?可以讓更多的信息直接回流到輸入,而不需要經過一個非線性轉化。我的理解是,在BP算法時,這一定程度上增大了梯度的回流,而不會被阻隔;在前向流動的時候,把允許原始的信息直接流過,增加了容量,就好像LSTM那樣,可以有long - term temporal dependencies。
Residual Network
ResNet的結構與Highway很類似,如果把Highway的網絡變一下形會得到,而在ResNet中,直接把門函數T(x)去掉,就得到一個殘差函數
,而且會得到一個恆等的映射 x ,對的,這叫殘差網絡,它解決的問題與Highway一樣,都是網絡加深導致的訓練困難且精度下降的問題。殘差網絡的一個block如下:
是的,就是這么簡單,但是,網絡很強大呀。而且實驗證明,在網絡加深的時候,依然很強大。那為什么這么強大呢?我覺得是因為identity map是的梯度可以直接回流到了輸入層。至於是否去掉門函數會更好呢,這個並不知道。在作者的另一篇論文《Identity Mappings in Deep Residual Networks》中,實驗證明了使用identity map會比加入卷積更優。而且通過調整激活函數和歸一化層的位置到weight layer之前,稱為 pre-activation,會得到更優的結果。
對於網絡中的一些虛線層,他們的shortcut就連接了兩個維度不同的feature,這時,有兩種解決辦法(1)在維度減少的部分直接使用 identity 映射,同時對於feature map增加部分用0補齊。(2)通過1*1的卷積變形得到。對於這個1*1的投影是怎么做的,可以參考VGG-16。我開始也很納悶,例如上面的虛線,輸入有64個Feature,輸出是128個Feature,如果是用128個kernel做卷積,應該有64*128個feature啊。糾結很久,看了看VGG的參數個數就明白了,如下圖
例如第一、二行,輸入3個Feature,有64個卷積核但卻有64個輸出,是怎么做到的呢?看它的權值的個數的計算時(3*3*3)*64,也就是說,實際上這64個卷積核其實是有3維的通道的。對應於ResNet的64個輸入,同樣卷積核也是有64個channel的。
結語
還請大牛可以指正不足以及思考不周的地方,指點學習的方向啊!!!!!!