這是一篇水貨寫的筆記,但願路過的大牛能夠指出其中的錯誤,帶蒟蒻飛啊~git
1、 梯度消失/梯度爆炸的問題github
首先來講說梯度消失問題產生的緣由吧,雖然是已經被各大牛說爛的東西。不如先看一個簡單的網絡結構,算法
能夠看到,若是輸出層的值僅是輸入層的值與權值矩陣W的線性組合,那麼最終網絡最終的輸出會變成輸入數據的線性組合。這樣很明顯沒有辦法模擬出非線性的狀況。記得神經網絡是能夠擬合任意函數的。好了,既然須要非線性函數,那乾脆加上非線性變換就行了。通常會使用sigmoid函數,獲得,這個函數會把數據壓縮到開區間(0,1),函數的圖像以下api
能夠看到,函數的兩側很是平滑,並且無限的接近0和1,僅僅是中間部分函數接近一條直線,順便說一下,這個函數的導數最值居然真的是1啊,也就是x=0的位置,於是稱這個函數是雙端飽和的,並且它是到處可導。那麼,爲何要選用它呢?看到有資料說是模擬神經學科的,但我不太懂這個,我的認爲是由於(1)能夠引入非線性(2)容易求導(3)能夠把數據壓縮,這樣數據不容易發散。網絡
另外,有一個函數與sigmoid函數很相似,就是tanh()函數,它能夠把數據壓縮到(-1,1)之間。app
可是,咱們要講的是梯度的消失問題哦,要知道,神經網絡訓練的方法是BP算法(不知道還有沒有其餘的訓練方法。。。)。BP算法的基礎其實就是導數的鏈式法則,這個估計不須要細說了,就是有不少乘法會鏈接在一塊兒。在看看sigmoid函數的圖像就知道了,導數最大是1,並且大多數值都被推向兩側飽和的區域,這些區域的導數但是很小的呀~~~~能夠預見到,隨着網絡的加深,梯度後向傳播到淺層網絡時,就呵呵了,基本不能引發數值的擾動,這樣淺層的網絡就學習不到新的特徵了。dom
那麼怎麼辦?我暫時看到了四種解決問題的辦法,僅僅是根據我本身看的論文總結的,並不是權威的說法。第一種很明顯,能夠經過使用別的激活函數;第二種可使用層歸一化;第三種是在權重的初始化上下功夫,第四種是構建新的網絡結構~。但暫時不寫,我還想記錄一下看到的梯度消失/爆炸問題在另外一個經典網絡的出現。ide
======================函數
噔噔噔噔,對的,就是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
2、 選擇其餘激活函數
激活函數有不少其餘的選擇,常看見的有ReLU、Leaky ReLU函數。下面就說一說這兩個函數,對於ReLU,函數爲,觀察一下它的函數圖像
能夠看到,它在負數的一段永遠是0啊。爲何要使用這個函數呢?聽說它是與神經學科有關的,這是由於稀疏激活的,這表如今負數端是抑制狀態,正數興奮激活。並且有理論也代表,稀疏的網絡更準確,在googLeNet的實現中就是利用了神經網絡的稀疏性。並且,在正數端導數永遠爲1,這就很好地解決了梯度消失的問題了。但是,它沒有把數據壓縮,這會使得數據的範圍可能很大。
此外還有Leaky ReLU函數,這個我是在YOLO看到的,其實和ReLU差很少,就是在負數端不徹底抑制了。圖像以下:
3、 層歸一化
這裏記錄的是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/
4、 權值初始化
爲了讓信息能夠更好的在網絡中流動(不必定是梯度消失的問題),可使用xavier的初始化方法。主要能夠看知乎專欄。爲了避免重複別人的工做,我簡單總結一下算了。注意一個問題,xavier的初始化方法的前提假設是,激活函數是線性的(其實歸一化後,可能把數據集中在了一處,就好像將BN的那張圖同樣)。
若是輸入數據x和權值w都知足均值爲0,標準差爲,(x能夠經過歸一化、白化實現)並且各數據是獨立同分布的。這樣,輸出爲
,根據機率公式,z 的均值依然爲0,方差爲
。
經過遞推公式能夠獲得,則
。因而,方差的計算公式爲
。這裏又出現了連乘,仍是按照以前與1比較的討論,那麼,最好是可讓方差保持一致啦,這樣數值的幅度就不會相差太大,就好像上面BN說的那樣,能夠收斂的更快。那麼就是讓連乘內的每一項都爲1了,則能夠推出權值的初始化爲
。
上面說的是前向的,那麼後向呢?後向傳播時,若是可讓方差保持一致,一樣地會有前向傳播的效果,梯度能夠更好地在網絡中流動。因爲假設是線性的,那麼迴流的梯度公式是,能夠寫成
。令迴流的方差不變,那麼權值又能夠初始化成
。注意一個是前向,一個是後向的,二者的n 是不一樣的。取平均
。
最後,是使用均勻分佈來初始化權值的,獲得初始化的範圍。
另一種MSRA的初始化的方法,能夠學習http://blog.csdn.net/shuzfan/article/details/51347572,實驗效果表現要好一些,但貌似xavier用的要多一些。
5、 調整網絡的結構
解決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的參數個數就明白了,以下圖
例如第1、二行,輸入3個Feature,有64個卷積核但卻有64個輸出,是怎麼作到的呢?看它的權值的個數的計算時(3*3*3)*64,也就是說,實際上這64個卷積覈實際上是有3維的通道的。對應於ResNet的64個輸入,一樣卷積核也是有64個channel的。
結語
還請大牛能夠指正不足以及思考不周的地方,指點學習的方向啊!!!!!!