激活函數的實現與梯度檢查<五>

專欄簡介

Numpy是一個非常好用的python科學計算的庫,CNN是現在視覺領域深度學習的基礎之一。雖然好的框架很多,但是自己用Numpy實現一個可以使用的CNN的模型有利於我們加深對CNN的理解。

本文旨在通過這樣一個專欄介紹如何用Numpy從零實現一個可以訓練的CNN簡易網路,同時對深度學習(CNN)的相關基礎知識進行一些複習,也希望能夠給正在入門的學弟學妹們一些簡單的歸納。

看全部代碼請直接踹門去我的github wuziheng


上期回顧 & 內容介紹:

上期[計算圖實現&mnist(new)!<四>]主要介紹了我們如何在我們實現的框架里引入計算圖的概念,並實現了遷移到計算圖之後的代碼,展示了計算圖框架下,自動求值和求導的便利。當然也介紹了未來優化的方向~這是之後的課題了。

本期我們將具體的介紹常見的激活函數,從原理到實現。我們之所以一直拖到計算圖框架實現之後,是因為我們將一次性介紹多種激活函數,並在框架中進行替換與實驗,從而驗證不同激活函數的特性。最後我們將介紹梯度檢查,一種必備的操作定義之後的檢驗手段。注意,本篇開始,原理偏少,試驗偏多。

  1. 激活函數原理與 Sigmoid 系實現
  2. Relu 及變種的Numpy實現與比較
  3. 梯度檢查原理 (gradient\_check) 的和實現&測試

target

按照邏輯上來說梯度檢查的實現與介紹應該放在最早的文章之中,但是實際實現中,我們需要對神經網路有了足夠的了解,才能夠將梯度檢查這個方法應用的得心應手。


激活函數原理與 Sigmoid 系實現

激活函數最初設計為為神經網路提供非線性性,提升網路的表達能力。如果不能合理的添加非線性性,無論網路有多少層,都可以規約到輸入線性組合上。這樣的網路幾乎毫無實用的價值。通過設計具有至少如下性質的函數,對網路每一層的輸出進行映射,可以添加非線性並提升表達能力:

  1. 非線性(目標)
  2. 可微性(基於梯度的優化方法可以實現)

人們模擬神經元觸發的激活函數設計了非線性的 Sigmoid 系列激活函數,極大增強了網路的表達能力。以下兩個是最為常見的 Sigmoid 系: Sigmoid ,Tanh

Sigmoid :

Sigmoid(x) = frac{1}{1+exp(-x)}

Sigmoid(x) = frac{1}{1+exp(-x)}*frac{exp(-x)}{1+exp(-x)} = Sigmoid(x)*(1-Sigmoid(x))

Tanh:

Tanh(x) = frac{1-exp(-2*x)}{1+exp(-2*x)}

Tanh(x) = (1-Tanh(x)^2)

特點:

  1. 對中央區信號增益大,兩側信號增益小,可以在訓練中較好的將重點特徵推向中央區。
  2. 求導方便,公式簡潔。

Sigmoid系函數激活曲線和導數

缺點:

  1. 計算量相對較大,每一個數據都涉及exp運算。
  2. 梯度彌散現象嚴重,例如sigmoid梯度最大值為0.25,幾層下來梯度就消失的差不多了。

最直觀的體現是計算速度,和收斂問題,過深層的神經網路幾乎無法使用 Sigmoid 進行訓練,即使是在我們2層的mnist上,我們也能看到,在收斂速度上幾乎是無法接受的。

實現

激活函數也是 derived quad Operator ,給出一個完整的激活函數的實現如下,其他的我們將只介紹 forward,backward 部分。

class Sigmoid(Operator): def __init__(self, input_variable=Variable, name=str): self.input_variables = input_variable self.output_variables = Variable(self.input_variables.shape, name=out, scope=name) Operator.__init__(self, name, self.input_variables, self.output_variables) def forward(self): if self.wait_forward: for parent in self.parent: GLOBAL_VARIABLE_SCOPE[parent].eval() # y = 1/(1+exp(-x)) self.output_variables.data = 1.0/(1.0+np.exp(-self.input_variables.data)) self.wait_forward = False return else: pass def backward(self): if self.wait_forward: pass else: for child in self.child: GLOBAL_VARIABLE_SCOPE[child].diff_eval() # eta_x = eta_y * (1-y) * y self.input_variables.diff = self.output_variables.data * ( 1 - self.output_variables.data) * self.output_variables.diff self.wait_forward = True return

可以說是最簡單的 Operator ,同實得益於 np.exp() ,我們可以方便的實現 Sigmoid 系列的激活函數。


Relu 系列及對比實驗

Relu 全稱修正線性單元(Rectified linear unit,ReLU),是人們從生物學角度模擬出的更精確的腦信號激活模型。他在滿足我們上面提到的非線性,可微的性質上,還有更多優良的品質。

Relu(x) = egin{cases} x& 	ext{x>=0}\ 0& 	ext{x<0} end{cases},quad quad Relu(x) = egin{cases} 1& 	ext{x>=0}\ 0& 	ext{x<0} end{cases}

優點:(完美解決了 Sigmoid 系的缺點)

  1. 計算簡單
  2. 梯度不會消失。
  3. 稀疏表達,很多神經元輸出為0,減少了參數的相關性,減小了overfit.

由於第二點,我們會發現同樣的學習率, Relu 的收斂速度會遠遠快與 Sigmoid .實際網路收斂的效果也要好於之前。我們給出實現部分的關鍵代碼:

forward: self.output_variables.data = np.maximum(self.input_variables.data, 0)backward: self.output_variables.diff[self.input_variables.data < 0] = 0

上面提到的優點,我們將relu與sigmoid系列分別實現,並在如下參數的情況下進行了訓練:

learning\_rate=5e-4, batch\_size=64,method=Momentum(0.9)

上圖明顯可以看出 Relu 的收斂速度上有絕對的優勢,而且 tanh 也確實比 Sigmoid 收斂的要快。最終收斂的結果看,也是 Relu 要更勝一籌。(github里提供了一個畫圖的腳本,嘻嘻,大家可以自取~如果開心可以star).

Relu 如此的簡單,高效,但也不是沒有改進的空間,實際上關於 Relu 的變種,非常之多,我們不可能一一介紹其原理和實現,可以關注的有 Leaky-Relu,Elu,PRelu ,這些我們都加以實現,並進行了實現,首先我們給出簡單的圖示展示幾種的區別。

relu series and gradients

具體的實現代碼我們分別給出:

Leaky-Relu

forward: self.output_variables.data = np.maximum(self.input_variables.data, 0) + self.alpha * np.minimum( self.input_variables.data, 0)backward: self.input_variables.diff = self.output_variables.diff self.input_variables.diff[self.input_variables.data <= 0] *= self.alpha

Elu:

forward: self.output_variables.data = np.maximum(self.input_variables.data, 0) + self.alpha * ( np.exp(np.minimum(self.input_variables.data, 0)) - 1)backward: self.input_variables.diff = self.output_variables.diff self.output_variables.diff[self.input_variables.data <= 0] *= ( self.alpha * np.exp(self.input_variables.data[self.input_variables.data <= 0]))

我們同樣測試了:

learning\_rate=5e-4, batch\_size=64,method=Momentum(0.9) 情況下三種relu的效果,只能說在alpha很小,網路不深的情況下,他們表現沒有什麼太大的區別, Leaky-relu 略好一點。

mnist relu series train


梯度檢驗

這是一個很早就應該開始的章節,第二章中我們就曾提出,但一直擱置。我們可以直到計算圖中,Op的反向傳播運算是我們手動實現的,如何能保證我們實現的Op的反向求導的正確性,至關重要。如果你不去做,later or soon ,一定會付出代價~

梯度檢驗數值演算法的原理來源於偏導數的定義:

frac{partial J(	heta)}{partial 	heta^{(i)}} = frac{J(	heta^{(i)}+epsilon)-J(	heta^{(i)}-epsilon)}{2*epsilon},epsilon
ightarrow0

實際中,你會去一個非常小的 epsilon=1e-4 ,按照上面的式子計算誤差函數關於某個值的偏導數 g_1 。同時計算反向傳播的值,得到反向傳播計算出來的 g_2 .數值上:

g_1-g_2sim O(epsilon^2) ,滿足這個條件即可。

我相信實現激活函數的梯度檢驗是非常簡單的,大家可以先行嘗試一下,如果沒有得出正確的結果,可以去github中激活函數這部分代碼,找到他的梯度檢驗。

這裡我們將用一個複雜一點的例子實現一下:卷積層反向傳播的梯度檢驗。

  1. 我們聲明了兩個卷積層實例,我們手動調整了使得兩個卷積層的初始化權重參數相等。
  2. 輸入為任意的一個圖片數據 input=[a,b,b,c] 輸出為 output=[A,B,B,C] ,我們選取一個損失函數為 J = np.sum(output) ,這樣可以明顯的得出一個結論: delta_{output} = np.ones([A,B,B,C]) ,我們則可以在完成前傳後手動調整 output.diff.data = np.ones([A,B,B,C]) ,代替去寫一個損失函數。
  3. 設置 epsilon ,逐一對 input 中的數據進行前向,然後完成 g_1,g_2 的計算。完成梯度檢驗。

import tensor.Variable as varimport tensor.Operator as opimport numpy as npe=1e-3a = var.Variable((1, 128, 128, 3), a)b = var.Variable((1, 128, 128, 3), b)b.data = a.data.copy()a.data[0,0,0,2] += eb.data[0,0,0,2] -= econv1_out = op.Conv2D((3, 3, 3, 3), input_variable=a, name=conv1,padding=VALID).output_variablesconv2_out = op.Conv2D((3, 3, 3, 3), input_variable=b, name=conv2,padding=VALID).output_variablesconv1 = var.GLOBAL_VARIABLE_SCOPE[conv1]conv2 = var.GLOBAL_VARIABLE_SCOPE[conv2]var.GLOBAL_VARIABLE_SCOPE[conv1].weights.data = var.GLOBAL_VARIABLE_SCOPE[conv2].weights.datavar.GLOBAL_VARIABLE_SCOPE[conv1].bias.data = var.GLOBAL_VARIABLE_SCOPE[conv2].bias.dataconv1_out.eval()conv1_out.diff.data = (np.ones(conv1_out.diff.shape))conv2_out.eval()conv2_out.diff.data = (np.ones(conv1_out.diff.shape))a.data[0,0,0,2]-=ea.diff_eval()g1 = np.sum(conv1_out.data-conv2_out.data)/2/eg2 = a.diff[0,0,0,2]print g1,g2

小結:

這篇文章里,我們主要介紹了激活函數的原理,分類和實現,以及在我們的框架上的實驗,當然,同時我們最為重要的是介紹了梯度檢驗的原理和實現。我們將對激活函數操作的梯度檢驗留個讀者自己思考。卷積層的梯度檢驗作為例子,相對複雜,但也更有意思。

最後希望大家都能動手實現梯度檢驗,最為重要的工具之一。同時專欄暫時也要告一段落了,明天或後天再更新一篇。謝謝大家的關注。如果有哪些地方我提到的沒有詳說的內容,恰好你們也感興趣願意與我交流,歡迎在知乎留言、私信或者github上留言。如果發現了錯誤,也希望能夠及時指出,贈人玫瑰~嘿嘿嘿嘿~

當然也歡迎直接去我的github CNN-Numpy 可以看到迄今為止實現的所有內容!~

祝大家新春快樂~在家吃好喝好~


推薦閱讀:

TAG:深度學習DeepLearning | 卷積神經網路CNN | 機器學習 |