激活函數的實現與梯度檢查<五>
專欄簡介
Numpy是一個非常好用的python科學計算的庫,CNN是現在視覺領域深度學習的基礎之一。雖然好的框架很多,但是自己用Numpy實現一個可以使用的CNN的模型有利於我們加深對CNN的理解。
本文旨在通過這樣一個專欄介紹如何用Numpy從零實現一個可以訓練的CNN簡易網路,同時對深度學習(CNN)的相關基礎知識進行一些複習,也希望能夠給正在入門的學弟學妹們一些簡單的歸納。
看全部代碼請直接踹門去我的github wuziheng
上期回顧 & 內容介紹:
上期[計算圖實現&mnist(new)!<四>]主要介紹了我們如何在我們實現的框架里引入計算圖的概念,並實現了遷移到計算圖之後的代碼,展示了計算圖框架下,自動求值和求導的便利。當然也介紹了未來優化的方向~這是之後的課題了。
本期我們將具體的介紹常見的激活函數,從原理到實現。我們之所以一直拖到計算圖框架實現之後,是因為我們將一次性介紹多種激活函數,並在框架中進行替換與實驗,從而驗證不同激活函數的特性。最後我們將介紹梯度檢查,一種必備的操作定義之後的檢驗手段。注意,本篇開始,原理偏少,試驗偏多。
- 激活函數原理與 系實現
- 及變種的Numpy實現與比較
- 梯度檢查原理 的和實現&測試
按照邏輯上來說梯度檢查的實現與介紹應該放在最早的文章之中,但是實際實現中,我們需要對神經網路有了足夠的了解,才能夠將梯度檢查這個方法應用的得心應手。
激活函數原理與 系實現
激活函數最初設計為為神經網路提供非線性性,提升網路的表達能力。如果不能合理的添加非線性性,無論網路有多少層,都可以規約到輸入線性組合上。這樣的網路幾乎毫無實用的價值。通過設計具有至少如下性質的函數,對網路每一層的輸出進行映射,可以添加非線性並提升表達能力:
- 非線性(目標)
- 可微性(基於梯度的優化方法可以實現)
人們模擬神經元觸發的激活函數設計了非線性的 系列激活函數,極大增強了網路的表達能力。以下兩個是最為常見的 系:
:
特點:
- 對中央區信號增益大,兩側信號增益小,可以在訓練中較好的將重點特徵推向中央區。
- 求導方便,公式簡潔。
缺點:
- 計算量相對較大,每一個數據都涉及exp運算。
- 梯度彌散現象嚴重,例如sigmoid梯度最大值為0.25,幾層下來梯度就消失的差不多了。
最直觀的體現是計算速度,和收斂問題,過深層的神經網路幾乎無法使用 進行訓練,即使是在我們2層的mnist上,我們也能看到,在收斂速度上幾乎是無法接受的。
實現
激活函數也是 ,給出一個完整的激活函數的實現如下,其他的我們將只介紹 部分。
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
可以說是最簡單的 ,同實得益於 ,我們可以方便的實現 系列的激活函數。
系列及對比實驗
全稱修正線性單元(Rectified linear unit,ReLU),是人們從生物學角度模擬出的更精確的腦信號激活模型。他在滿足我們上面提到的非線性,可微的性質上,還有更多優良的品質。
優點:(完美解決了 系的缺點)
- 計算簡單
- 梯度不會消失。
- 稀疏表達,很多神經元輸出為0,減少了參數的相關性,減小了overfit.
由於第二點,我們會發現同樣的學習率, 的收斂速度會遠遠快與 .實際網路收斂的效果也要好於之前。我們給出實現部分的關鍵代碼:
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系列分別實現,並在如下參數的情況下進行了訓練:
上圖明顯可以看出 的收斂速度上有絕對的優勢,而且 也確實比 收斂的要快。最終收斂的結果看,也是 要更勝一籌。(github里提供了一個畫圖的腳本,嘻嘻,大家可以自取~如果開心可以star).
如此的簡單,高效,但也不是沒有改進的空間,實際上關於 的變種,非常之多,我們不可能一一介紹其原理和實現,可以關注的有 ,這些我們都加以實現,並進行了實現,首先我們給出簡單的圖示展示幾種的區別。
具體的實現代碼我們分別給出:
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
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]))
我們同樣測試了:
情況下三種relu的效果,只能說在alpha很小,網路不深的情況下,他們表現沒有什麼太大的區別, 略好一點。
梯度檢驗
這是一個很早就應該開始的章節,第二章中我們就曾提出,但一直擱置。我們可以直到計算圖中,Op的反向傳播運算是我們手動實現的,如何能保證我們實現的Op的反向求導的正確性,至關重要。如果你不去做,later or soon ,一定會付出代價~
梯度檢驗數值演算法的原理來源於偏導數的定義:
實際中,你會去一個非常小的 ,按照上面的式子計算誤差函數關於某個值的偏導數 。同時計算反向傳播的值,得到反向傳播計算出來的 .數值上:
,滿足這個條件即可。
我相信實現激活函數的梯度檢驗是非常簡單的,大家可以先行嘗試一下,如果沒有得出正確的結果,可以去github中激活函數這部分代碼,找到他的梯度檢驗。
這裡我們將用一個複雜一點的例子實現一下:卷積層反向傳播的梯度檢驗。
- 我們聲明了兩個卷積層實例,我們手動調整了使得兩個卷積層的初始化權重參數相等。
- 輸入為任意的一個圖片數據 輸出為 ,我們選取一個損失函數為 ,這樣可以明顯的得出一個結論: ,我們則可以在完成前傳後手動調整 ,代替去寫一個損失函數。
- 設置 ,逐一對 中的數據進行前向,然後完成 的計算。完成梯度檢驗。
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 | 機器學習 |