課程做業原地址:CS231n Assignment 1
做業及整理:編寫:@土豆 && @郭承坤 && @寒小陽
時間:2018年2月。
出處:http://blog.csdn.net/han_xiaoyang/article/details/79278882html
To-Do:python
[x] 統一全部的數學符號和代碼符號git
[x] 統一全部的術語名稱github
[x] 術語的英文詞彙對應web
[x] 線性模型的名字是?perceptron?算法
[x] 損失函數的計算公式和符號是否嚴格和合適?shell
[x] denominator layout?編程
- [ ] BN梯度的證實
- [x] bn_param[‘running_mean’] = running_mean
bn_param[‘running_var’] = running_var
(我是盜圖大仙,全部圖片資源所有來源於網絡,若侵權望告知~)數組
本文是什麼?緩存
本文以CS231n的Assignment2中的Q1-Q3部分代碼做爲例子,目標是由淺入深得搞清楚神經網絡,同時以圖片分類識別任務做爲咱們一步一步構建神經網路的目標。
本文既適合僅看得懂一點Python代碼、懂得矩陣的基本運算、據說過神經網絡算法這個詞的朋友,也適合準備學習和正在完成CS231n課程做業的朋友。
本文內容涉及:很細節的Python代碼解析 + 神經網絡中矩陣運算的圖像化解釋 + 模塊化Python代碼的流程圖解析
本文是從Python編程代碼的實現角度理解,一層一層撥開神經網絡的面紗,以搞清楚數據在其中到底是怎麼運動和處理的。但願能夠爲小白,尤爲是爲正在學習CS231n課程的朋友,提供一個既淺顯又快捷的觀點,用最直接的方式弄清楚並構建一個神經網絡出來。因此,此文不適合章節跳躍式閱讀。
本文不是什麼?
不涉及艱深的算法原理,忽略絕大多數數學細節,也儘可能不扯任何生澀的專業術語,也不會對算法和優化處理技術作任何橫向對比。
CS231n課程講師Andrej Karpathy在他的博客上寫過一篇文章Hacker’s guide to Neural Networks,其中的精神是我最欣賞的一種教程寫做方式:「My exposition will center around code and physical intuitions instead of mathematical derivations. Basically, I will strive to present the algorithms in a way that I wish I had come across when I was starting out.」
「…everything became much clearer when I started writing code.」
廢話很少說,找個板凳坐好,慢慢聽故事~
俗話說得好:皮褲套棉褲,裏邊有緣故;不是棉褲薄,就是皮褲沒有毛!
咱們的神經網絡是要用來解決某特定問題的,不是家裏閒置的花瓶擺設,模型的構建都有着它的動機。因此,首先讓咱們簡單瞭解下要擺弄的數據集(CIFAR-10),最終的目標是要完成一個圖片樣本數據源的分類問題。
圖像分類數據集:CIFAR-10。
這是一個很是流行的圖像分類數據集是CIFAR-10。這個數據集包含了60000張
的小圖像,單個像素的數值範圍都在0-255之間。每張圖像都對應因而10種分類標籤(label)中的一種。此外,這60000張圖像被分爲包含帶有標籤的50000張圖像的訓練集和包含不帶有標籤的10000張圖像的測試集。
上圖是圖片樣本數據源CIFAR-10中訓練集的一部分樣本圖像,從中你能夠預覽10個標籤類別下的10張隨機圖片。
小結:
在咱們的故事中,只須要記得這個訓練集是一堆
的RGB彩色圖像做爲訓練目標,一個樣本圖像共有
個數據,每一個數據的取值範圍0~255,通常用x
來標記。每一個圖還配有一個標籤值,總共10個標籤,之後咱們都用y
來標記。(悄悄告訴你的是:每一個像素點的3個數據維度是有序的,分別對應紅綠藍(RGB))
下圖是將神經網絡算法以神經元的形式繪製的兩個圖例,想必同志們早已見怪不怪了。
可是,你起碼應該知道的是其中各類約定和定義:
左邊是一個2層神經網絡,一個隱藏層(藍色層)有4個神經元(也可稱爲單元(unit))組成,輸出層(綠色)由2個神經元組成,輸入層(紅色)是3個」神經元」。右邊是一個3層神經網絡,兩個隱藏層,每層分別含4個神經元。注意:層與層之間的神經元是全鏈接的,可是層內的神經元不鏈接(如此就是所謂全鏈接層神經網絡)。
這裏有個小坑:輸入層的每一個圈圈表明的可不是每一張圖片,其實也不是神經元。應該說整個縱向排列的輸入層包含了一張樣本圖片的全部信息,也就是說,每一個圈圈表明的是某樣本圖片對應位置的像素數值。可見對於CIFAR-10數據集來講,輸入層的維數就是 ,共3072個圈圈呢!至於輸出層的神經元數也是依賴數據集的,就CIFAR-10數據集來講,輸出層維數必然是10,即對應數據集的10個標籤。至於中間的隱藏層能夠有多少層,以及每層的神經元個數就均可以任意啦!你說牛不牛?!
在接下來咱們的故事中,要從代碼實現的角度慢慢剖析,先從一個神經元的角度出發,再搞清楚一層神經元們是如何幹活的,而後逐漸的弄清楚一個含有任意神經元個數隱藏層的神經網絡到底是怎麼玩的,在故事的最後將會以CIFAR-10數據集的分類問題爲目標一試身手,看看咱們構造的神經網絡到底是如何工做運轉的。
咱們先僅前向傳播而言,來談談一個神經元到底是作了什麼事情。
前向傳播,這名字起的也是神乎其神的,說白了就是將樣本圖片的數據信息,沿着箭頭正向傳給一個帶參數的神經網絡層中咀嚼一番,而後再吐出來一堆數據再餵給後面的一層吃(如此而已,竟然就叫作了前向/正向傳播了,讓人忍不住吐槽一番)。那麼,對於一個全鏈接層(fully-connected layer) 1
的前向傳播來講,所謂的「帶參數的神經網絡層」通常就是指對輸入數據源(此後用」數據源」這個詞來表示輸入層全部輸入樣本圖片數據整體)先進行一個矩陣乘法,而後加上偏置,獲得數字再運用激活函數」修飾」,最後再反覆迭代罷了(後文都默認使用此線性模型)。
是否是暈了?彆着急,咱們進一步嚼碎了來看看一個神經元(處於第一隱藏層)到底是如何處理輸入層傳來的一張樣本圖片(帶有貓咪標籤)的?
上面提到過,輸入數據源是一張尺寸爲 的RGB彩色圖像,咱們假定輸入數據 的個數是 的話(即 是有 個),那這個 。爲了廣泛意義,下文繼續用大寫字母 來表示一張圖片做爲數據源的維數個數(若是該神經元位於隱藏層,則大寫字母 表示本隱藏層神經元的神經元個數,下一節還會提到)。
顯然,一張圖片中的 個數據 包含了判斷該圖片是一支貓的全部特徵信息,那麼咱們就須要」充分利用」這些信息來給這張樣本圖片」打個分」,來評價一下這張圖像究竟有多像貓。
不能空口套白狼,一張美圖說明問題:
左圖不用看,這個通常是用來裝X用的,並非真的要嚴格類比。雖然最初的神經網絡算法確實是受生物神經系統的啓發,可是如今早已與之分道揚鑣,成爲一個工程問題。關鍵咱們是要看右圖的數學模型(嚴格地說,這就是傳說中的感知器perceptron)。
如右圖中的數學模型所示,咱們爲每個喂進來的數據
都對應的」許配」一個」權重」參數
,再加上一個偏置
,而後一股腦的把他們都加起來獲得一個數(scalar):
換句話說,至關因而有一個神經元坐在某選秀的評委席裏,戴着一款度數爲 雷朋眼鏡,給某一位臺上模仿貓咪的樣本圖片 打了一個分(評價分數)。顯然,得分的高低是不只依賴於臺上的主角 的表現,還嚴重依賴於神經元評委戴着的有色眼鏡(參數 )。固然,咱們已經假定評委的智商(線性模型)是合乎統一要求的。
現現在,參加選秀的人可謂趨之若鶩,一個神經元評委該如何同時的批量化打分,提升效率嗯?
也就是說,一個神經元面對 張圖片該如何給每一張圖片打分的問題。這就是矩陣表達式的優點了,咱們只須要很天然地把上述矩陣表達式縱向延展下便可,以下所示:
然而,關於一個神經元的故事還沒完。
你可能注意到了,上面例子中的美圖中有個函數f,咱們把圖放大仔細看清楚:
在神經元對每張圖片算得的「得分」送給下一個神經元以前都要通過一個函數f的考驗。這就暗示咱們,選秀節目的導演對神經元評委給出的得分還並不滿意,爲了(將來模型訓練的)快捷方便,導演要求對每個得分須要作進一步的「激活」處理(即上圖中的函數
),因而這個叫激活函數(activation)的傢伙會對結果作進一步的處理、好比你們這些年都在用的ReLU就是臨門一腳,要求把得分小於零的都閹割掉,一概給0分(都得負分的了還選什麼秀啊?給0分滾蛋):
因此,總結下來,一個神經元乾的活就是以下所示的公式:
小備註:
這裏最後再多一句嘴:一個神經元在前向傳播中輸出的只是一個數字,另外,神經網絡的訓練過程,訓練的是上述提到的模型參數 。
再多一句嘴,一般咱們在整個神經網絡結構中只使用一種激活函數。而且值得你注意的是,全鏈接層的最後一層就是輸出層,除了這個最後一層,其它的全鏈接層神經元都要包含激活函數。
最最後說再一句,神經網絡的激活函數是非線性的,因此神經網絡是一個非線性分類器。
在正式開始談一層神經元以前,咱們繼續來探討下神經元面對一張圖片還能夠作什麼?
對於一張標籤是貓咪的樣本圖片,咱們光能評價有多麼的像貓咪還不能知足,咱們還須要神經元評價一下其餘9個標籤才行,而後才比如較評判得出最終結論。因而,光用 這 個參數就不夠用了,應該要有 個參數才行。仍是用那個惡搞的例子說明的話,就是說一個神經元評委可不夠用哦,要10個戴着不一樣有色眼鏡的神經元評委分頭去考察10個不一樣標籤,這樣就能夠對每一個樣本圖片給出10個對應不一樣類別的得分。
因此,咱們能夠在最初的矩陣表達式 的基礎上橫向延展成以下矩陣表達式:
爲了便於直觀理解,給你一個數值栗子嚐嚐鮮(和上面的矩陣公式有點區別,但並不影響理解):
能夠看到咱們拿了樣本貓咪圖片中的四個像素做爲一個神經元的輸入數據源,不過上圖只查看了3個標籤(貓/狗/船),且把輸入數據 改爲用列向量表達罷了,這並不影響咱們理解,無非是咱們的矩陣公式改成 表達 。接下來看圖說話,能夠看到在如圖初始化矩陣 和 的狀況下,算出的得分對應於貓咪的分數竟然是最低的2
,這說明 和 的值沒有訓練好啊。那麼究竟該如何訓練出合適的參數呢?難道每次都要肉眼觀察每一個標籤算出的得分再同時對比參數選的究竟好很差?再難道每次都要本身手調參數矩陣的每一個值來觀察得分效果麼?固然不會這麼傻啦,到時候一個叫損失函數的概念就登場了,且繼續聽故事先~
回到上面通過橫向延展後的矩陣表達式:
接下來,推廣到通常的隱藏層神經元們是如何幹活的就易如反掌了!只要充分利用矩陣乘法,就能夠很清楚了。小結以下:
每個隱藏層得出得分 後,都還須要通過激活函數 的處理,要留意的是最後的輸出層並不須要激活函數,給出得分後便可交給損失函數(後文會提到)。
小備註:
值得注意的是:流入每一層神經元們的輸入數據矩陣x和流出每一層神經元們的輸出數據矩陣的行數沒有變化,都是N,即樣本圖片個數。每一層神經元們的參數矩陣w和b都是不一樣的,那麼你就能想象到對於一個」很大很深」神經網絡而言,須要訓練學習的參數個數但是至關多的哦~並且參數矩陣w的維數也頗有特色,其行數H和列數M,分別對應於當前隱藏層神經元個數和下一層神經元的個數。如此一來,就把每一層神經元一層套一層鏈接起來了。
正如本文開頭那句話,
「…everything became much clearer when I started writing code.」
再強大的算法,不管矯情得怎麼解釋,也都不如直接飛代碼來得更清晰,更直接。
那麼任意某一層神經元們在前向傳播中,究竟作了什麼呢?前面總算把數據如何運動的故事說清楚了,如今開始直接用代碼說明一切,第一步,咱們定義面對輸入數據某一層神經元們給出「得分」的函數 affine_forward(x, w, b) = (out, cache)
:
def affine_forward(x, w, b):
""" Inputs: - x: A numpy array containing input data, of shape (N, d_1, ..., d_k) 樣本 - w: A numpy array of weights, of shape (D, M) 權重 - b: A numpy array of biases, of shape (M,) 偏置 Returns a tuple of: - out: output, of shape (N, M) - cache: (x, w, b) """
out = None # 初始化
reshaped_x = np.reshape(x, (x.shape[0],-1)) # 確保x是一個規整的矩陣
out = reshape_x.dot(w) +b # out = w x +b
cache = (x, w, b) # 將該函數的輸入值緩衝儲存起來,以備後面計算梯度時使用
return out, cache
代碼詳解:
np.reshape(x, (x.shape[0],-1))
中的 -1
是指剩餘可填充的維度,因此這段代碼意思就是保證reshape後的矩陣行數是
,剩餘的維度信息都規則的排場一行便可。cache
變量就是把該函數的輸入值
存爲元組(tuple)再輸出出去以備用,它固然不會流到下一層神經元,但其在後面會講到的反向傳播算法中利用到,因而可知咱們是有多麼的老謀深算啊!out = reshape_x.dot(w) +b
這句代碼就表達了某一層神經元中,每一個神經元能夠並行的獨立完成上兩節提到的線性感知器模型,對每一個圖像給出本身的評價分數,與代碼對應一致的內涵可見以下矩陣表達式:
這裏你要清楚的是 reshape_x
中每一行和 w
中的每一列的運算對應於一個神經元面對一張圖片的運算過程。矩陣乘法沒啥可說的,只要把輸入矩陣和要輸出的矩陣的維數掰扯清楚了,就會很簡單。這裏參數w和b都做爲函數的輸入參數參與運算的,可見你想讓神經網絡運做起來,你是須要先初始化全部的神經網絡參數的,那麼究竟如何初始化呢?這仍是門小學問,咱們暫且假定是隨機填了些的參數進來。雖然輸入進來的參數 b 的shape是 (M,)
,但在numpy中,兩個array的」+」相加,是徹底等價於np.add()
函數(詳情可help該函數),這裏體現了numpy的Broadcasting機制:(詳情可查看Python庫numpy中的Broadcasting機制解析)
簡單的說,對兩個陣進行操做時,NumPy逐元素地比較他們的形狀。只有兩種狀況下Numpy會認爲兩個矩陣內的兩個對應維度是兼容的:1. 它們相等; 2. 其中一個是1維。舉個例子:
A (4d array): 8 x 1 x 6 x 1 B (3d array): 7 x 1 x 5 Result (4d array): 8 x 7 x 6 x 5
當任何一個維度是1,那麼另外一個不爲1的維度將被用做最終結果的維度。也就是說,尺寸爲1的維度將延展或「逐個複製」到與另外一個維度匹配。
因此代碼中的偏置b,其shape爲(M,),其實它代表是一個 的行向量,面對另外一個 的矩陣,b 便遇強則強的在弱勢維度上(縱向)被延展成了一個 的矩陣:
雖然,咱們說清楚了 affine_forward(x,w,b)
函數的故事,但要注意的是,在前向傳播中一層神經元要乾的活還沒完哦~ 在隱藏層中獲得的得分結果還須要ReLU激活函數「刺激」一下才算結束。
因而咱們再定義 relu_forward(x) = (out, cache)
函數來完成這一步,其Python代碼就更簡單了:
def relu_forward(x):
""" Computes the forward pass for a layer of rectified linear units (ReLUs). Input: - x: Inputs, of any shape Returns a tuple of: - out: Output, of the same shape as x - cache: x """
out = np.maximum(0, x) # 取x中每一個元素和0作比較
cache = x # 緩衝輸入進來的x矩陣
return out, cache
代碼詳解:
咱們能夠注意到,np.maximum()
函數中的接受的兩個參數同樣用到了剛剛詳解過的Broadcasting機制:前一個參數是隻有一個維度的數值0,被延展成了一個和矩陣x一樣shape的矩陣,而後在對應元素上比大小(至關於矩陣x的全部元素中把比0小的元素都替換成0),取較大元素填在新的同shape形的矩陣out中。
那麼,終於到最後了。一個隱藏層層神經元們在前向傳播中究竟作了什麼呢?那就是下面定義的 affine_relu_forward(x, w, b) = (out, cache)
函數:
def affine_relu_forward(x, w, b):
""" Convenience layer that perorms an affine transform followed by a ReLU Inputs: - x: Input to the affine layer - w, b: Weights for the affine layer Returns a tuple of: - out: Output from the ReLU - cache: Object to give to the backward pass """
a, fc_cache = affine_forward(x, w, b) # 線性模型
out, relu_cache = relu_forward(a) # 激活函數
cache = (fc_cache, relu_cache) # 緩衝的是元組:(x, w, b, (a))
return out, cache
這裏仍是要留個心眼,對於輸出層的神經元們來講,他們只須要用 affine_forward(x, w, b)
函數給出得分便可,無需再被」激活」。
小結一下:
咱們手繪一張圖來講清楚,一隱藏層神經元們在前向傳播的 affine_relu_forward()
函數中,數據變量在模塊代碼中到底是如何流動的:
關於傳說中的反向傳播,首先咱們最重要的是要明白:咱們爲何須要這個反向傳播?
然而,要想弄清楚這點,咱們就須要回頭考察下咱們前向傳播下最後輸出層獲得10個標籤的評分究竟有什麼用。這個問題的答案,將會直接引出故事中的審判官——損失函數。
前情提要:
在前向傳播(從左向右)中,輸入的圖像數據 (以及所對應的正確標籤 )是給定的,固然不可修改,惟一能夠調整的參數是權重矩陣W(大寫字母W表示神經網絡中每層權重矩陣w的集合)和參數B(大寫字母表示神經網絡中每層偏置向量b的集合),即上圖中每一條黑色的線和黑色的圈。因此,咱們但願經過調節參數(W, B),使得最後評分的結果與訓練數據集中圖像的真實類別一致,即輸出層輸出的評分在正確的分類上應當獲得最高的評分。
回到以前那張用來嚐鮮的貓的圖像分類栗子,它有針對「貓」,「狗」,「船」三個類別的分數。咱們看到例子中權重值很是差,由於貓分類的得分很是低(-96.8),而狗(437.9)和船(61.95)比較高。正如上文提到的,究竟該如何讓計算機自動地判別得分的結果與正確標籤之間的差別,而且對神經網絡全部參數給出改進意見呢?
咱們本身僅憑肉眼和肉腦固然是作不了審判官的,可是一個叫作損失函數(Loss Function)(有時也叫代價函數Cost Function或目標函數Objective)的能夠作到!直觀地講,當輸出層的評分給出結果與真實結果之間差別越大,咱們的審判官——損失函數就會給出更加嚴厲的判決!舉起一個寫有很大的判決分數,以表示對其有多麼的不滿!反之差別若越小,損失函數就會給出越小的結果。
咱們這裏請的是交叉熵損失(cross-entropy loss)來做爲最終得分的審判官!廢話少說,直接看代碼!
def softmax_loss(z, y):
""" Computes the loss and gradient for softmax classification. Inputs: - z: Input data, of shape (N, C) where z[i, j] is the score for the jth class for the ith input. - y: Vector of labels, of shape (N,) where y[i] is the label for x[i] and 0 <= y[i] < C Returns a tuple of: - loss: Scalar giving the loss - dz: Gradient of the loss with respect to z """
probs = np.exp(z - np.max(z, axis=1, keepdims=True)) # 1
probs /= np.sum(probs, axis=1, keepdims=True) # 2
N = z.shape[0] # 3
loss = -np.sum(np.log(probs[np.arange(N), y])) / N # 4
dz = probs.copy()
dz[np.arange(N), y] -= 1
dz /= N
return loss, dz
代碼詳解:
softmax_loss(z, y)
函數的輸入數據是shape爲(N, C)的矩陣z和shape爲(N, )的一維array行向量y。因爲損失函數的輸入數據來自神經網絡的輸出層,因此這裏的矩陣z中的N表明是數據集樣本圖片的個數,C表明的是數據集的標籤個數,對應於CIFAR-10的訓練集來講,z矩陣的shape應該爲(50000, 10),其中矩陣元素數值就是CIFAR-10的訓練集數據通過整個神經網絡層到達輸出層,對每一張樣本圖片(每行)打分,給出對應各個標籤(每列)的得分分數。一維array行向量y內的元素數值儲存的是訓練樣本圖片數據源的正確標籤,數值範圍是
,亦即
。probs
變量。首先,np.max(z, axis=1, keepdims=True)
是對輸入矩陣x在橫向方向挑出一個最大值,並要求保持橫向的維度輸出一個矩陣,即輸出爲一個shape爲(N, 1)的矩陣,其每行的數值表示每張樣本圖片得分最高的標籤對應得分;而後,再 np.exp(z - ..)
的操做表示的是對輸入矩陣z的每張樣本圖片的全部標籤得分都被減去該樣本圖片的最高得分,換句話說,將每行中的數值進行平移,使得最大值爲0;再接下來對全部得分取exp函數,而後在每一個樣本圖片中除以該樣本圖片中各標籤的總和(np.sum
),最終獲得一個與矩陣z同shape的(N, C)矩陣probs。上述獲得矩陣probs中元素數值的過程對應的就是softmax函數:
其中,咱們已經取定了
的值:
,且
對應於代碼中的輸出數據矩陣x的第
行、第
列的得分z[i, j]
,其取值僅依賴於從輸出層輸入來的數據矩陣z和參數
,同理,
表示矩陣probs的第
行、第
列的新得分。咱們舉一個簡單3個圖像樣本,4個標籤的輸入數據矩陣x的栗子來講明得分有着怎樣的變化:
$$
z_{ij}
\equiv
\underbrace{\begin{bmatrix}
1 & 1 & 1 & 2 \
2 & 2 & 2 & 3 \
3 & 3 & 3 & 5 \end{bmatrix}}_{3\times 4}
\overset{-\max}{\Longrightarrow }
\underbrace{
\underbrace{
\underbrace{
這個圖是另外一個小例子來講明Softmax函數能夠鑲嵌在輸出層中,至關於其餘隱藏層神經元們中的激活函數同樣,用Softmax函數對輸出層算得的得分進行了一步「激活」操做。
loss = -np.sum(np.log(probs[np.arange(N), y])) / N
,輸出是一個scalar數 loss
。其數學含義是這樣的
其中的 表示的是對每一個圖片正確標籤下的得分所有求和。聽上去很暈是否是?仍是痛快的給個栗子就清楚了:
probs[np.arange(N), y]
,取出了每一個圖片樣本在正確標籤下的得分(紅色數字)。這裏值得留意的是向量y的標籤取值範圍(0~9)恰好是能夠對應於probs矩陣每列的index。
詳解一下:
probs[np.arange(N), y]
在例子中,對probs矩陣確切的切片含義是
probs[np.array([0, 1 ,2]), np.array([2, 0, 1])]
這就像是定義了經緯度同樣,指定了確切的行列數,要求切片出相應的數值。對於上面的例子而已,就是說取出第0行、第2列的值;取出第1行、第0列的值;取出第2行、第1列的值。因而,就獲得了例子中的紅色得分數值。切行數時,
np.arange(N)
至關因而說「我每行都要切一下哦~」,而切列數時,y
向量(array)所存的數值型分類標籤(0~9),恰好能夠對應於probs矩陣每列的index(0~9),若是y = np.array(['cat', 'dog', 'ship'])
,顯然代碼還這麼寫就會出問題了。
再簡單解釋一下上面得到loss損失函數的過程:咱們首先對輸出層輸出的矩陣x作了一個」機率化」的rescale操做,改寫爲同shape的矩陣probs,使得每張樣本圖片的正確得分數值是足夠充分考慮到了其餘標籤得分的(機率化),可見,Softmax分類器爲每種分類都提供了「可能性」;而後針對這個矩陣probs,取出每一個樣本圖片的正確標籤所對應得分,再被單調遞增函數log和sum取平均值操做後,取其負值即爲損失函數的結果了。最後要說一下,公式中負號的存在,並不只僅保證了一個正定的損失函數,還使得若損失函數的結果越小,那麼就意味着咱們最初追求的是正確標籤下得分越高,整個神經網絡模型的參數就訓練得越好,其中的關聯性正是損失函數的數學定義中單調遞增函數exp和log的保證下所實現的。
由此很顯然,在一整套層狀神經網絡框架裏,咱們但願可以獲得讓咱們滿意的模型參數(W, B),只須要使得損失函數最小,就說明咱們的模型參數(W, B)取得好哈!
因此說,找輸出層給出的得分和正確標籤得分差距小的參數(W, B)的問題,就被轉移爲究竟什麼樣的參數(W, B)使得損失函數最小!
別忘了代碼中的 softmax_loss(z, y)
函數最後還有三行哈!它很是重要,是除了評分函數和損失函數以外,體現的是神經網絡算法的第三個關鍵組成部分:最優化Optimization!最優化是尋找能使得損失函數值最小化的參數(W, B)的過程。因爲每當咱們取定的模型參數(W, B)稍微變化一點點的時候,最後算得的損失函數應該也會變化一點點。天然地,咱們就很是但願模型參數每變化一點點的時候,損失函數都恰好能變小一點點,也就是說損失函數老是很乖地向着變小的方向變化,最終達到損失函數的最小值,而後咱們就收穫到理想的模型參數(W, B)。若真如此,不就省下了「踏破鐵鞋無覓處」,反而「得來全不費工夫「!那麼究竟怎麼走才能才能如此省心省事呢?
這時候,就有必要引出梯度這個概念了。由於,咱們但願可以看到損失函數是如何隨着模型參數的變化而變化的,也就是說損失函數與模型參數之間的變化關係,而後纔好進一步順着咱們想走的可持續發展的道路上,」衣食無憂,瓜熟蒂落,奔向小康~」
那麼究竟什麼是梯度呢?
梯度的本意是一個向量(矢量),表示某一函數在該點處的方向導數沿着該方向取得最大值,即函數在該點處沿着該方向(此梯度的方向)變化最快,變化率最大(爲該梯度的模)。一張小圖來解釋梯度怎麼用:
上圖中的曲面是二元函數 在自變量 的圖像。圖上箭頭就表示該點處函數f關於座標參數x,y的梯度啦!從咱們的神經網絡角度去看,二元函數 能夠對應於模型最終算得的損失函數loss,其自變量就是模型的參數(W, B)。若是咱們讓訓練樣本圖片通過一次神經網絡,最後就能夠獲得一個損失值loss,再根據咱們初始選定的模型參數(W, B),就也能夠在上面的(高維)曲面上找到一點對應。倘若咱們同時也知道了該點處loss的梯度,記號爲grad,那麼也就意味着參數(W, B)若是加上這個grad數值,在新參數(W+grad, b+grad)下讓樣本圖片通過神經網絡新計算出來的loss損失值必定會更大一些,這正是梯度的定義所保證的,以下圖:(注:梯度grad是可爲正也可爲負的)
可是不要忘了,咱們的目標是但願獲得損失函數loss最小時的參數(W, B),因此咱們要讓神經網絡的參數(W, B)在每次有樣本圖片通過神經網絡以後,都要讓全部參數減去梯度(加負梯度)的方式來更新全部的參數,這就是所謂的梯度降低(gradient descent)。最終,使得損失函數關於模型參數的梯度達到足夠小,即接近損失函數的最小值,就能夠真正完成咱們神經網絡最優化的目的(更詳細實現梯度降低的故事,咱們留到最後的來講明)。
那麼,如何在每一次有樣本圖片通過神經網絡以後,獲得損失函數關於模型參數的梯度呢?
寒暄到此爲止,咱們再貼一遍 softmax_loss(z, y) = (loss, dx)
函數代碼,來觀察下後三行代碼裏損失函數關於輸出層輸出數據矩陣z的梯度是如何計算出的:
def softmax_loss(z, y):
""" Computes the loss and gradient for softmax classification. Inputs: - z: Input data, of shape (N, C) where z[i, j] is the score for the jth class for the ith input. - y: Vector of labels, of shape (N,) where y[i] is the label for x[i] and 0 <= y[i] < C Returns a tuple of: - loss: Scalar giving the loss - dz: Gradient of the loss with respect to z, of shape (N, C) """
probs = np.exp(z - np.max(z, axis=1, keepdims=True))
probs /= np.sum(probs, axis=1, keepdims=True)
N = z.shape[0]
loss = -np.sum(np.log(probs[np.arange(N), y])) / N
dz = probs.copy() # 1 probs.copy() 表示得到變量probs的副本
dz[np.arange(N), y] -= 1 # 2
dz /= N # 3
return loss, dz
代碼解析:
dz
變量是損失函數關於從輸出層輸入來的數據矩陣z的梯度,其shape與數據矩陣z相同,即(N, C)。其嚴格的數學解析定義(證實過程)是:$$
\left.\begin{matrix}
\begin{align*}
&\frac{\partial L_{ij}}{\partial z_{ij}} = \frac{1}{N}(S_{ij}-1),,,\
&\frac{\partial L_{ij}}{\partial z_{il}} = \frac{1}{N}S_{il},,,,,,,(l\neq j)
\end{align*}
\end{matrix}\right}
\Rightarrow
dL\equiv
\sum_i\Big[\frac{1}{N}(S_{iy_j}-1)dz_{iy_i} +\frac{1}{N}S_{il}dz_{il} \Big],,,,,(l\neq y_i)
$$
不明白數學沒有關係,只須要清楚咱們算得的梯度是損失函數關於從輸出層輸入來的數據矩陣x上的梯度
就足夠了,直接看代碼來弄清楚數據是如何運動的:
$$
\begin{align*}
\left.\begin{matrix}
S_{ij}
\equiv
\underbrace{
\
y_{i}\equiv \underbrace{[201]
\end{matrix}\right} & \overset{dx[\text{np.arange(N), y}] -= 1}{\Longrightarrow }
\underbrace{[0.1750.175−0.8251/2.104−0.8250.1750.1751/2.1040.096−0.9040.0961/1.405]
dz
。嚴格的說,咱們在
softmax_loss(z, y)
函數中輸出的shape爲(N, C)的
dz
矩陣變量對應的是
dLij/dzil
。
小結一下:
在咱們定義的 softmax_loss(z, y)
函數中,不只對神經網絡的輸出層給出的得分矩陣z給出了一個最終打分 loss
——即損失函數,同時還輸出了一個和得分矩陣z相同shape的散度矩陣 dz
,表明的是損失函數關於得分矩陣z的梯度:根據∂Lij∂zij=1N(Sij−1);∂Lij∂zil=1NSil;(l≠yi),有:
$$
\Big[\frac{\partial L_{ij}}{\partial z_{il}}\Big]
\Leftrightarrow
\underbrace{\begin{bmatrix}
\frac{\partial L_{0 0}}{\partial z_{00}} & \cdots & {\color{Red}{\frac{\partial L_{0 y_i}}{\partial z_{0y_i}}}} &\cdots & \cdots\
{\color{Red}{\frac{\partial L_{1y_0}}{\partial z_{1y_0}}}} & \cdots &\frac{\partial L_{1 j}}{\partial z_{1j}} & \cdots & \cdots \
\cdots & {\color{Red}{\frac{\partial L_{iy_i}}{\partial z_{y_i}}}} & \cdots & \frac{\partial L_{ij}}{\partial z_{ij}} &\cdots\
\cdots &\cdots &\cdots & \cdots &\cdots
\end{bmatrix}}_{N\times C}
\Leftrightarrow \frac{1}{N}
\underbrace{[S00⋯S0yi−1⋯⋯S1y0−1⋯S1j⋯⋯⋯Siyj−1⋯Sij⋯⋯⋯⋯⋯⋯]
能夠看到這個損失函數的梯度矩陣 dz
會得出每張樣本圖片(每行)的每一個標籤(每列)下輸出層輸出數據的得分梯度,亦即咱們獲得的是損失函數關於輸出層得分的變化率 ∂L/∂z(後文用 ∂L/∂z 表示損失函數關於輸出層神經元得分數據的梯度,用 ∂L/∂x,∂L/∂y 表示損失函數關於隱藏層神經元輸出數據的梯度)。然而,咱們的故事還遠沒有說完,回憶一下!咱們須要的但是損失函數關於神經網絡中全部參數(W, B)的變化率啊!(即 ∂L/∂W,∂L/∂B ) 而後咱們才能不斷經過損失函數L的反饋來調整神經網絡中的參數(W, B)。
那麼究竟該如何把損失函數關於輸出層得分的變化率 ∂L/∂z 與損失函數關於神經網絡中參數(W, B)的變化率 ∂L/∂W,∂L/∂B 創建起聯繫呢?這時候,傳說中的反向傳播終於要登場了!
目前,咱們已經將損失函數L與輸出層輸出數據矩陣的每個得分創建起了聯繫。只要最後咱們能算得出損失函數的值,就說明咱們已經得到輸出層的輸出數據,進而就能獲得損失函數關於輸出層輸出數據的梯度 ∂L/∂z。
那麼該如何進一步獲得損失函數關於其餘隱藏神經元層的輸出數據的梯度 ∂L/∂x 呢?還有其關於每一隱藏層的參數數據的梯度 ∂L/∂W,∂L/∂B 呢?這時候就要感謝一下偉大的萊布尼茲,感謝他發明複合函數的微積分求導「鏈式法則」就是傳說中的反向傳播算法的核心基礎。
廢話少說,看圖說話,故事仍是要先從一個神經元提及:
圖中正中的函數f至關於輸出層的某一個神經元。綠色箭頭表明的是得分數據的前向傳播 f(x,y)=z(能夠看到輸出層正向來的數據x,y被」激活」過,流出輸出層的數據z並無考慮」激活「),紅色箭頭即表明的是梯度的反向傳播。圖中右側的∂L/∂z 表示損失函數L關於輸出層中來自當前神經元的數據得分z的梯度(scalar)。以上都是咱們已知的,而咱們未知且想知道的是損失函數L關於最後一隱藏層中流入當前神經元的數據得分x和y的梯度,即∂L/∂x,∂L/∂y。
鏈式法則給咱們提供瞭解決方案,那就是經過「局部梯度」將損失函數的梯度傳遞回去:
∂L∂x=∂L∂z∂z∂x;∂L∂y=∂L∂z∂z∂y
在反向傳播的過程當中,咱們只須要給出上面藍色公式所表明的局部梯度 ∂z/∂x,∂z/∂y,便可從損失函數 L 關於輸出層輸出數據z的梯度 ∂L/∂z 獲得 L 關於上一隱藏層輸出數據x,y的梯度 ∂L/∂x,∂L/∂y。如此一來,只要咱們在每一層神經元處都定義好了局部梯度,就能夠很輕鬆的把損失函數 L 關於該層神經元們輸出數據的梯度」搬運」到該層神經元們輸入數據的梯度,如此反覆迭代,就實現了傳說中的反向傳播。。。
關於反向傳播的故事還有一點沒說完:對損失函數的 L 全微分不只會涉及每層神經元給出的得分的微分,也會牽扯到該層參數(w, b)的微分,如此一來就能夠獲得咱們想要的損失函數 L 關於神經網絡模型參數(W, B)的梯度。
整個過程很像是 L 關於數據的梯度在每一層反向傳播,順便地把各層關於參數的梯度也算了出來。
是否是又被說暈了?不要急,直接上代碼,最後奇蹟立現!
def affine_backward(dout, cache):
""" Computes the backward pass for an affine layer. Inputs: - dout: Upstream derivative, of shape (N, M) 上一層的散度輸出 - cache: Tuple of: - z: Input data, of shape (N, d_1, ... d_k) - w: Weights, of shape (D, M) - b: biases, of shape (M,) Returns a tuple of: - dz: Gradient with respect to z, of shape (N, d1, ..., d_k) - dw: Gradient with respect to w, of shape (D, M) - db: Gradient with respect to b, of shape (M,) """
z, w, b = cache
dz, dw, db = None, None, None
reshaped_x = np.reshape(z, (z.shape[0], -1))
dz = np.reshape(dout.dot(w.T), z.shape) # np.dot() 是矩陣乘法
dw = (reshaped_x.T).dot(dout)
db = np.sum(dout, axis=0)
return dz, dw, db
代碼詳解:
affine_backward(dout, cache) = (dz, dw, db)
函數來描述輸出層神經元的反向傳播。其中shape爲(N, M)的 dout
矩陣就是損失函數 L 關於該層在 affine_forward()
函數正向輸出數據 out
的梯度,其對應於咱們上一節定義的 softmax_loss()
函數輸出的 dz
矩陣。梯度在輸出層反向傳播的話,M的大小就等於樣本圖片的標籤數,即 M=10。cache
元組是正向流入輸出層的神經元的數據x和輸出層的參數(w, b),即有 z, w, b = cache
,其中輸出層的輸入數據z是未通過reshaped的一個多維array,shape爲(N,d1,…,dk),權重矩陣W的shape是(D, M),偏置b的shape是(M, )。接下來就是重點了!首先,在前向傳播中,咱們已經清楚該輸出層神經元中數據的流動依據的是一個線形模型,即 形如函數表達式 z(x,w,b)=xw+b。找到函數 z 的局部梯度顯然很簡單:
∂z∂x=w,∂z∂w=x,∂z∂b=1
「矩陣維度匹配」究竟是什麼意思?這實際上是個挺」猥瑣」的方法,故事是這樣的:
首先,咱們要約定好全部損失函數 L 梯度的shape都要與其相關的矩陣變量的shape相同。比方說,上一節損失函數 softmax_loss(z, y) = (loss, dz)
中的梯度 dz
就和數據矩陣 z
的shape相同。因此,在函數 affine_backward(dout, cache) = (dz, dw, db)
中,咱們就要求損失函數 L 關於正向輸入矩陣(x, w, b)的梯度矩陣 (dx, dw, db) 與矩陣 (x, w, b) 維度相同。(嚴格說,咱們的局部梯度求導其實對應於vector-by-vector derivatives,咱們如此約定不過是至關於取定denominator layout)
因而,咱們就能夠按照上面函數 z 的局部梯度規則書寫,只要保證矩陣表達式維度匹配便可:
$$
\begin{align*}
\underbrace{\begin{bmatrix}
& & \
& \text{dx} & \
& &
\end{bmatrix}}_{N\times 32\times32\times3}
\overset{\text{np.reshape}}{\Longleftarrow }
\underbrace{[^dx]
\underbrace{[dw]
dx = np.reshape(dout.dot(w.T), x.shape)
dw = (reshaped_x.T).dot(dout)
db = np.sum(dout, axis=0)
代碼解析:
np.reshape(.., x.shape)
要求輸出的矩陣與多維array輸入數據x的shape同型。np.sum(.., axis=0)
表示每列元素求和,顯然這個矩陣操做等價於咱們在上面寫的矩陣表達式哈!如今,咱們懂得如何用 affine_backward()
函數反向傳播表示損失函數關於輸出層流入先後數據以及參數的梯度,那麼其餘隱藏神經元層也用這個函數反向傳播梯度行不行?
固然不行,千萬別忘了,其餘隱藏層神經元給出得分後還有「激活」的操做。因此咱們再定義一個 relu_backward(dout, cache) = dx
函數表示隱藏層神經元中激活函數部分的梯度的反向傳播,而後就能夠組成一個完整的損失函數 L 關於隱藏層神經元的反向傳播函數 affine_relu_backward(dout, cache) = (dx, dw, db)
。
說這麼多,其實代碼很簡單啦:
def relu_backward(dout, cache):
""" Computes the backward pass for a layer of rectified linear units (ReLUs). Input: - dout: Upstream derivatives, of any shape - cache: Input x, of same shape as dout Returns: - dx: Gradient with respect to x """
dx, x = None, cache
dx = (x > 0) * dout
# 與全部x中元素爲正的位置處,位置對應於dout矩陣的元素保留,其餘都取0
return dx
def affine_relu_backward(dout, cache):
""" Backward pass for the affine-relu convenience layer """
fc_cache, relu_cache = cache # fc_cache = (x, w, b) relu_cache = a
da = relu_backward(dout, relu_cache) # da = (x > 0) * relu_cache
dx, dw, db = affine_backward(da, fc_cache)
return dx, dw, db
代碼詳解:
dx = (x > 0) * dout
,表示損失函數關於在ReLU激活函數處流入流出數據的梯度的反向傳遞。下圖是某層神經元激活函數部分中的數據正反向傳播的示意圖:$$
\begin{align*}
\underbrace{\begin{bmatrix}
{\color{green} {-1 }}& {\color{green} {-1 }} & 3\
{\color{green} {-2}} & (\text{x}) & 4 \
2 & 4 & {\color{green} {-5 }}
\end{bmatrix}}_{N\times H}
&
\overset{\text{forward}}{\Longrightarrow }
\underbrace{[0030(ˆx)4240]
\underbrace{[0.1−0.30.70.4(dout)−0.5−0.20.30.8]
上面的列數H對應的是該隱藏層神經元的個數。激活函數 f(x)=max(0,x) 的局部梯度:
∂f∂x=1,x⩾0,(ˆx>0)∂f∂x=0,x<0,(ˆx=0)
故事講到此,咱們就基本能夠成功的創建起神經網絡的框架了。下面以一個2層的全鏈接神經網絡爲例,從咱們上文全部定義過的模塊化Python代碼函數的角度,總結一下數據在這張神經大網上是如何傳播運動的。
其實,前面講了那麼多故事,定義了那麼多函數,歸根結底就是爲了看懂上面咱們本身手繪的「數據流動走向圖」,以及能夠用Python代碼構造出來咱們的一個超簡易版本的全鏈接神經網絡框架。
數據流動走向圖的代碼詳解:
注:下面代碼在cs231n做業略有簡化修改(無正則化)。
class TwoLayerNet(object): # 咱們的2層全鏈接神經網絡
""" 首先,須要初始化咱們的神經網絡。 畢竟,數據從輸入層第一次流入到神經網絡裏,咱們的參數(W,B)不能爲空, 也不能都太大或過小,由於參數(W,B)的初始化是至關重要的, 對整個神經網絡的訓練影響巨大,但如何proper的初始化參數仍然沒有定論 目前仍有不少paper在專門討論這個話題。 """
def __init__(self ,input_dim=3*32*32 # 每張樣本圖片的數據維度大小 ,hidden_dim=100 # 隱藏層的神經元個數 ,num_classes=10 # 樣本圖片的分類類別個數是 ,weight_scale=1e-3): # 初始化參數的權重尺度(標準誤差)
""" 咱們把須要學習的參數(W,B)都存在self.params字典中, 其中每一個元素都是都是numpy arrays: """
self.params = {}
""" 咱們用標準差爲weight_scale的高斯分佈初始化參數W, 偏置B的初始都爲0: (其中randn函數是基於零均值和標準差的一個高斯分佈) """
self.params["W1"] = weight_scale * np.random.randn(input_dim
,hidden_dim)
self.params["b1"] = np.zeros((hidden_dim,))
self.params["W2"] = weight_scale * np.random.randn(hidden_dim
,num_classes)
self.params["b2"] = np.zeros((num_classes,))
""" 能夠看到, 隱藏層的參數矩陣行數是3*32*32,列數是100; 輸出層的參數矩陣行數是100,列數10. """
# 接下來,咱們最後定義一個loss函數就能夠完成神經網絡的構造
def loss(self, X, y):
""" 首先,輸入的數據X是一個多維的array,shape爲(樣本圖片的個數N*3*32*32), y是與輸入數據X對應的正確標籤,shape爲(N,)。 咱們loss函數目標輸出一個損失值loss和一個grads字典, 其中存有loss關於隱藏層和輸出層的參數(W,B)的梯度值: """
loss, grads = 0, {}
# 數據X在隱藏層和輸出層的前向傳播:
h1_out, h1_cache = affine_relu_forward(X
,self.params["W1"]
,self.params["b1"])
scores, out_cache = affine_forward(h1_out
,self.params["W2"]
,self.params["b2"])
# 輸出層後,結合正確標籤y得出損失值和其在輸出層的梯度:
loss, dout = softmax_loss(scores, y)
# 損失值loss的梯度在輸出層和隱藏層的反向傳播:
dout, dw2, db2 = affine_backward(dout, out_cache)
grads["W2"] = dw2 , grads["b2"] = db2
_, dw1, db1 = affine_relu_backward(dout, h1_cache)
grads["W1"] = dw1 , grads["b1"] = db1
""" 能夠看到圖片樣本的數據梯度dout只起到了帶路的做用, 最終會捨棄掉,咱們只要loss關於參數的梯度, 而後保存在grads字典中。 """
return loss, grads
上面代碼定義的類 TwoLayerNet()
就是一個兩層全連接神經網絡模型,其中富含了咱們以前講到的全部內容。
那麼故事到此,咱們總算是知道怎麼創建一個神經網絡框架了。
感受很簡單是不?是否是覺得咱們的神經網絡如今能夠開始幹活了?
NO~ no~ no~
還遠沒有哦~ 咱們不過是鋪好了一條通往目的地的林蔭小路和各類道路兩旁的設施,尚未真正地上路行駛呢!也就是說,咱們的神經網絡尚未搭建起自我模型最優化的程序,即「訓練」的過程。在後面的故事裏,咱們不只要搞清楚訓練的流程思路,還要進一步強化和改造咱們剛剛搭建的簡易神經網路,把它從一條奔小康的鄉間小路打形成實現四個」現代化」、高速運轉的高速公路!
在前面的故事裏,咱們總算是學會搭建一個全鏈接的神經網絡了,那麼它究竟好用麼?若是你直接去用那個簡易的神經網絡在CIFAR-10數據集上訓練和檢查分類的準確率,你可能會有些失望。
訓練的速度好慢啊!
在測試集上怎麼準確率不高啊?
該怎麼增長神經網絡的深度呢?
。。。
接踵而至的問題和疑惑可能還有不少,可是不用擔憂,在故事談到模型的最優化——「訓練」以前,咱們能夠」過後諸葛亮的」在上述簡易神經網絡的基礎上添磚加瓦,讓它變得更增強大,更加高效運轉起來!
在咱們的故事講圖像樣本數據在神經網絡中如何流動的時候,從每一個神經元層的評價打分,到激活,再到最後的輸出層給出loss損失值,以及最終反向傳播到loss關於參數(W, B)的梯度,咱們的最終目的是爲了獲得最小化的loss損失值,由於它所對應的參數(W, B)正是咱們想要的理想神經網絡參數。
神經網絡的最優化過程,正是咱們上述的反覆迭代逼近最小loss損失值的過程,然而在這個過程當中很容易出現一個問題,那就是「過擬合」。換句話說,在最小化loss損失值時,咱們的神經網絡訓練學習得太好了,都快把訓練集的答案背下來了,可是一旦作測試考試題,就答的一塌糊塗、慘不忍睹,準確率遠比作訓練題低,這就是所謂的「過擬合」。
「背答案」的後果但是很嚴重的,必需要接受懲罰!
因而,咱們要求圖片樣本數據每經過一次神經網絡時,都要對獲得的loss值和參數的梯度加一個懲罰項——正則項,其本質是約束(限制)要優化的參數。以下圖:
上圖是在w1,w2參數平面內,繪製的loss損失函數值的等高線圖。圈圈最中心的點即便loss損失函數最小值所對應的參數w1,w2。而正則化是咱們對於神經網絡中的每一個權重w,向損失函數中增長一個正則項 12λw2 (L2正則化),其中 λ 是正則化強度。因而,在最優化的過程當中,解空間縮小了,解參數w1,w2的可選範圍再也不是整個平面,而是被限制在圖中陰影的圓形區域內。因此,通過懲罰且最小化的loss損失值所對應的最優參數w1,w2,就是距離未經歷懲罰的最小化loss損失值最近,位於圓形區域邊界上的一點。(See more: XX,XX)
廢話說了不少,反映在代碼上其實很簡單,只須要對神經網絡給出的loss和其梯度稍加修改便可:
loss += 0.5 * self.reg * (np.sum(self.params["W1"] ** 2) + \
np.sum(self.params["W2"] ** 2))
dW2 += self.reg * self.params["W2"]
dW1 += self.reg * self.params["W1"]
代碼詳解:
self.reg
是正則化強度,這不是一個要優化的學習參數,須要在訓練以前設置好,取其值爲0就意味着不考慮正則化。np.sum(self.params["W1"] ** 2)
表示對參數矩陣 w1 中的每個元素都取平方,並所有加起來獲得一個scalar數。小注:
咱們的L2正則化能夠直觀理解爲它對於大數值的權重參數進行嚴厲懲罰,傾向於更加分散的權重參數。因爲在每層的神經元中輸入數據和權重參數矩陣之間的乘法操做,這樣就有了一個優良的特性:使得神經網絡網絡更傾向於使用全部輸入數據的特徵,而不是嚴重依賴輸入特徵中某些小部分特徵。
注意:接下來的故事就開始變的很是微妙了,因爲故事的大boss——神經網絡的最優化位於本文的最後一部分(要搞定大boss才能大結局嘛),因此接下來的故事中將會時不時地出現大boss的身影,好比區分訓練模式和測試模式、優化算法等身影。因此不要擔憂以爲太陌生,它會在故事最終大結局時還會露面的。
前面的故事提到過,神經網絡的初始化是一個很棘手的問題。即便到如今,誰也不敢說本身的初始化方案就是最完美、最合適的。不過,咱們有一個接近完美的方案來減輕如何合理初始化神經網絡這個棘手問題帶來的頭痛,這個方案就是批量歸一化(Batch Normalization)。
細說以前,先飛圖兩張來看看它什麼樣子~
上圖是咱們已經再也不陌生的3層全鏈接神經網絡,2個隱藏層神經元中的f表示神經元們給出打分後的激活函數f。
而批量歸一化(Batch Normalization)所作的事情,就是要在神經元們給出打分和拿去作激活之間添加一個步驟,對全部的得分作一個數據預處理,而後再送給激活函數。以下圖所示:
能夠看到,咱們在每一層神經網絡的激活函數前都進行批量歸一化處理,要求數據和梯度在正反向傳播中都要有這一步驟。千萬不要小瞧神經元體內的這一步驟,它的存在乎義重大且深入。這個在每個隱藏層中增長」批量歸一化」做爲數據預處理的方法能夠在將來的學習過程當中,進一步加速收斂,其效果非同小可。它的做用就像高透光的手機貼膜,既能保護手機對外界環境的磨損,又能保證膜內手機屏幕透光性,咱們的手機固然就更健康啦!
下面直接給出前向傳播Batch Normalization(BN)層的 batchnorm_forward(x, gamma, beta, bn_param)=(out,cache)
函數代碼,會更清晰明瞭!
def batchnorm_forward(x, gamma, beta, bn_param):
""" During training the sample mean and (uncorrected) sample variance are computed from minibatch statistics and used to normalize the incoming data. During training we also keep an exponentially decaying running mean of the mean and variance of each feature, and these averages are used to normalize data t test-time. At each timestep we update the running averages for mean and variance using an exponential decay based on the momentum parameter: running_mean = momentum * running_mean + (1 - momentum) * sample_mean running_var = momentum * running_var + (1 - momentum) * sample_var Note that the batch normalization paper suggests a different test-time behavior: they compute sample mean and variance for each feature using a large number of training images rather than using a running average. For this implementation we have chosen to use running averages instead since they do not require an additional estimation step; the torch7 implementation of batch normalization also uses running averages. Input: - x: Data of shape (N, D) - gamma: Scale parameter of shape (D,) - beta: Shift paremeter of shape (D,) - bn_param: Dictionary with the following keys: - mode: 'train' or 'test'; required - eps: Constant for numeric stability - momentum: Constant for running mean / variance. - running_mean: Array of shape (D,) giving running mean of features - running_var Array of shape (D,) giving running variance of features Returns a tuple of: - out: of shape (N, D) - cache: A tuple of values needed in the backward pass """
mode = bn_param['mode']
eps = bn_param.get('eps', 1e-5)
momentum = bn_param.get('momentum', 0.9)
N, D = x.shape
running_mean = bn_param.get('running_mean', np.zeros(D, dtype=x.dtype))
running_var = bn_param.get('running_var', np.zeros(D, dtype=x.dtype))
out, cache = None, None
if mode == 'train': # 訓練模式
sample_mean = np.mean(x, axis=0) # 矩陣x每一列的平均值 shaple: (D,)
sample_var = np.var(x, axis=0) # 矩陣x每一列的方差 shape: (D,)
x_hat = (x - sample_mean) / (np.sqrt(sample_var + eps))
out = gamma * x_hat + beta
cache = (x, sample_mean, sample_var, x_hat, eps, gamma, beta)
running_mean = momentum * running_mean + (1 - momentum) * sample_mean
running_var = momentum * running_var + (1 - momentum) * sample_var
elif mode == 'test': # 測試模式
out = (x - running_mean) * gamma / (np.sqrt(running_var + eps)) + beta
else:
raise ValueError('Invalid forward batchnorm mode "%s"' % mode)
# Store the updated running means back into bn_param
bn_param['running_mean'] = running_mean
bn_param['running_var'] = running_var
return out, cache
代碼詳解:
batchnorm_forward()
函數的輸入矩陣x能夠看作是輸入的樣本數據(或者是通過線性函數的輸出值),shape爲(N, D),N是樣本圖片的個數,D是特徵個數(本例中就是像素點數)。參數(gamma, beta)是咱們新的待優化學習的參數,shape都爲(D, ),這樣一來,咱們的神經網絡模型參數增長到四個(w, b, gamma, beta)。bn_param
是BN算法的參數字典,其中存有 eps
數值變量精度。 是爲了不分母除數爲0的狀況所使用的微小正數。mode
存好了當前神經網絡的狀態。咱們在以前的故事中所談過的數據正向流動和損失函數梯度的反向傳播都已經默認了是 train
模式,由於咱們最終的目標是但願根據樣本圖片訓練咱們的神經網絡模型(中的參數),使得它進入 test
模式下時,輸入沒有正確標籤的樣本圖片能夠給出正確的標籤。參數字典中 momentum
參數是最優化中的」超參數」常數,咱們會在最後說明其中的內涵,在這裏只要知道它會默認取值爲0.9就行了。running_mean, running_var
即移動平均值和移動方差值,會在train階段隨着樣本的輸入不斷變化,train
訓練模式下,sample_mean
和 sample_var
分別是對x的每一列算得平均值和標準差向量,shape都爲(D, )。若這是第一隱藏層中神經元的BN層,這至關因而把每張樣本圖像對應像素維度看做結構化數據的一個」特徵」(共3072個),而後算出全部樣本圖片裏每一個像素特徵下的平均值和標準差。前兩行公式咱們已經談過了,它們分別是 sample_mean
和 sample_var
所對應得分矩陣的每一列xi算得平均值向量和標準差向量(μB,σ2B)。而後,咱們就要利用這兩個信息,對得分矩陣的每一列xi特徵數據進行「歸一化」,即便得每一維特徵均值爲0,標準差爲1,服從標準高斯分佈。又由於歸一化是一個簡單且可求導的操做,因此這是可行的。最後一步更關鍵了,一招驚天地泣鬼神的招式:變換重構,引入了可學習參數γ,β,讓該層神經元能夠學習恢復出原始神經網絡在該層所要學習的特徵分佈。
x_hat = (x - sample_mean) / (np.sqrt(sample_var + eps))
和 out = gamma * x_hat + beta
,仍能夠明顯的看到Broadcasting機制的應用。參數向量(gamma, beta) 都被broadcasting拉成與矩陣x相同shape(N, D)的矩陣,而後在每個對應元素上作如數學公式後兩行所示的運算。在公式中 eps
的存在是必要的,由於數據某一列的標準差 sample_var
爲0是可能的,好比僅一張圖片流入神經網絡的時候。train
模式裏,running_mean, running_var
會在每通過一個BN層進行一次迭代計算~並在離開每個BN層以前保存替換掉原BN參數字典中的 running_mean, runing_var
值。在訓練結束後,這兩個參數將會用於 test
測試模式下的每個BN層中。其實你若留意英文註釋的話,就會明白上面代碼中的操做並非BN算法的做者原始的處理方式,其本意是因爲神經網絡一旦訓練完畢,參數都會基本固定下來,這個時候即便是每批樣本圖片再進入咱們的神經網絡,那麼BN層計算的平均值和標準差都是固定不變的,因此咱們能夠採用這些數值來做爲測試樣本所須要的均值、標準差。然而,咱們上述代碼中所採用的估計整個訓練集的方法,會高效簡潔得多,嚴格地說,這叫一次指數平滑法(Single exponential smoothing):在市場預測中,一次指數平滑法是根據前期的實測數和預測數,以加權因子爲權數,進行加權平均,來預測將來時間趨勢的方法。
詳情可查看這兩個資料:MBAlib:一次指數平滑法 and Wiki: Exponential smoothing
從上述資料中,咱們就能夠知曉:參數momentum對應於平滑係數,或者叫加權因子。其值大小的取定是要考慮到每一次BN層算得的平均值和標準差的變化特性所決定的,算是一個經驗超參數:在咱們的神經網絡中,若每層每次算得波動不大,比較平穩,則參數momentum可取0.7~0.9;若具備迅速且明顯的變更傾向,則應取爲0.1~0.4。
關於一次指數平滑法的初值取法也是有講究的。能夠看到咱們上面的代碼就是取第一次算得的平均值或標準差來做爲初始默認值的。
那麼,清楚了神經元層中 batchnorm_forward()
函數乾的活,就能夠很輕鬆的改寫原一層神經元們在前向傳播中的 affine_relu_forward()
函數爲含有Batch Normalization層的 affine_bn_relu_forward(x, w, b, gamma, beta, bn_param) = (out, cache)
函數:
def affine_bn_relu_forward(x, w, b, gamma, beta, bn_param):
""" Inputs: - x: Array of shape (N, D1); input to the affine layer - w, b: Arrays of shape (D2, D2) and (D2,) giving the weight and bias for the affine transform. - gamma, beta: Arrays of shape (D2,) and (D2,) giving scale and shift parameters for batch normalization. - bn_param: Dictionary of parameters for batch normalization. Returns: - out: Output from ReLU, of shape (N, D2) - cache: Object to give to the backward pass. """
a, fc_cache = affine_forward(x, w, b)
a_bn, bn_cache = batchnorm_forward(a, gamma, beta, bn_param) #BN層,注意!它在ReLU層前
out, relu_cache = relu_forward(a_bn) # ReLU層
cache = (fc_cache, bn_cache, relu_cache)
return out, cache
顯然在反向傳播中,損失函數的梯度也須要添加一步Batch Normalization的局部梯度:
其中數學公式有點複雜,證實可見這裏。
看見就想吐就不要看了,仍是直接飛代碼才能清晰咱們的思路,要理清楚損失函數的數據梯度是如何通過這一層的就好:
def batchnorm_backward(dout, cache):
""" Inputs: - dout: Upstream derivatives, of shape (N, D) - cache: Variable of intermediates from batchnorm_forward. Returns a tuple of: - dx: Gradient with respect to inputs x, of shape (N, D) - dgamma: Gradient with respect to scale parameter gamma, of shape (D,) - dbeta: Gradient with respect to shift parameter beta, of shape (D,) """
x, mean, var, x_hat, eps, gamma, beta = cache
N = x.shape[0]
dgamma = np.sum(dout * x_hat, axis=0) # 第5行公式
dbeta = np.sum(dout * 1.0, axis=0) # 第6行公式
dx_hat = dout * gamma # 第1行公式
dx_hat_numerator = dx_hat / np.sqrt(var + eps) # 第3行第1項(未負求和)
dx_hat_denominator = np.sum(dx_hat * (x - mean), axis=0) # 第2行前半部分
dx_1 = dx_hat_numerator # 第4行第1項
dvar = -0.5 * ((var + eps) ** (-1.5)) * dx_hat_denominator # 第2行公式
# Note var is also a function of mean
dmean = -1.0 * np.sum(dx_hat_numerator, axis=0) + \
dvar * np.mean(-2.0 * (x - mean), axis=0) # 第3行公式(部分)
dx_var = dvar * 2.0 / N * (x - mean) # 第4行第2項
dx_mean = dmean * 1.0 / N # 第4行第3項
# with shape (D,), no trouble with broadcast
dx = dx_1 + dx_var + dx_mean # 第4行公式
return dx, dgamma, dbeta
代碼詳解:
test
模式下,咱們並不須要有反向傳播這一步驟,只須要樣本圖片數據通過神經網絡後,在輸出層給出的得分便可。一樣地,咱們能夠改寫原一層神經元們在反向傳播中的 affine_relu_backward()
函數爲含有Batch Normalization層的 affine_bn_relu_backward(dout, cache) = (dx, dw, db, dgamma, dbeta)
函數:
def affine_bn_relu_backward(dout, cache):
""" Backward pass for the affine-batchnorm-relu convenience layer. """
fc_cache, bn_cache, relu_cache = cache
da_bn = relu_backward(dout, relu_cache) # ReLU層
da, dgamma, dbeta = batchnorm_backward(da_bn, bn_cache) # BN層,反向傳播時在ReLU後
dx, dw, db = affine_backward(da, fc_cache)
return dx, dw, db, dgamma, dbeta
小注:
批量歸一化(Batch Normalization)算法在2015年被提出來(原論文),這個方法能夠進一步加速收斂,所以學習率(後文會提到)能夠適當增大,加快訓練速度;過擬合現象能夠獲得必定程度的緩解,因此能夠不用Dropout(後文會提到)或用較低的Dropout,並且能夠減少L2正則化係數,訓練速度又再一次獲得了提高,即Batch Normalization能夠下降咱們對正則化的依賴程度。
現現在,深度神經網絡基本都會用到Batch Normalization。
能夠閱讀原論文了解詳細細節,也能夠參考如下博文:
故事聽到這裏了,想必你也已經能感受到,想要讓咱們的神經網絡提升對樣本圖片的分類能力,最直接粗暴的辦法就是使用更深的網絡和更多的神經元,即所謂deeper and wider。然而,越複雜的網絡越容易過擬合,因而,咱們可讓隱藏層的神經元們也能夠任性一下,不要太賣力的工做,偶爾放個假休息下,這種」勞逸結合」的管理方式就是所謂的Dropout(隨機失活),大部分實驗代表其具備必定的防止過擬合的能力。
來看圖理解一下這種任性的管理方式:
左圖是標準的全鏈接神經元網絡。咱們的管理方式很簡單:在訓練的時候,讓神經元以超參數p的機率被激活或者被設置爲0。右圖就是應用dropout的效果。
訓練過程當中,dropout能夠被認爲是對完整的神經網絡抽樣出一些子集,每次基於激活函數的輸出數據只更新子網絡的參數(然而,數量巨大的子網絡們並非相互獨立的,由於它們都共享參數)。
上圖是前向傳播中,神經元們把得分輸出給激活函數a以後,會通過一個函數m,它會根據一個超參數p機率地讓部分神經元不工做(其輸出置爲0),而且利用生成的隨機失活遮罩(mask)對輸出數據矩陣進行數值範圍調整。在測試過程當中不使用dropout,能夠理解爲是對數量巨大的子網絡們作了模型集成(model ensemble),以此來計算出一個平均的預測。反向傳播保持不變,可是確定須要將對應的遮罩考慮進去。
仍是那句話,再空洞的理論闡述,都不如直接飛代碼來的實際!
下面是在前向傳播中,在dropout層處定義的 dropout_forward(x, dropout_param) = (out, cache)
函數:
def dropout_forward(x, dropout_param):
""" Performs the forward pass for (inverted) dropout. Inputs: - x: Input data, of any shape - dropout_param: A dictionary with the following keys: - p: Dropout parameter. We drop each neuron output with probability p. - mode: 'test' or 'train'. If the mode is train, then perform dropout; if the mode is test, then just return the input. - seed: Seed for the random number generator. Passing seed makes this function deterministic, which is needed for gradient checking but not in real networks. Outputs: - out: Array of the same shape as x. - cache: A tuple (dropout_param, mask). In training mode, mask is the dropout mask that was used to multiply the input; in test mode, mask is None. """
p, mode = dropout_param['p'], dropout_param['mode']
if 'seed' in dropout_param:
np.random.seed(dropout_param['seed'])
mask = None
out = None
# 訓練模式
if mode == 'train':
keep_prob = 1 - p
mask = (np.random.rand(*x.shape) < keep_prob) / keep_prob
out = mask * x
# 測試模式
elif mode == 'test':
out = x
cache = (dropout_param, mask)
out = out.astype(x.dtype, copy=False)
return out, cache
代碼詳解:
輸入數據矩陣x,其shape能夠任意。dropout的參數字典 dropout_param
中存有超參數 p
和 mode
,分別表明每層神經元們被失活的機率和當前 dropout 層是訓練模式,仍是測試模式。該參數字典中的能夠有隨機數生成種子 seed
,要來標記隨機性。
在訓練模式下,首先,代碼 (np.random.rand(*x.shape)
,表示根據輸入數據矩陣x,亦即通過」激活」後的得分,生成一個相同shape的隨機矩陣,其爲均勻分佈的隨機樣本[0,1)。而後將其與可被保留神經元的機率 keep_prob
作比較,就能夠獲得一個隨機真值表做爲隨機失活遮罩(mask)。原始的辦法是:因爲在訓練模式時,咱們丟掉了部分的激活值,數值調整 out = mask * x
後形成總體分佈的指望值的降低,所以在預測時就須要乘上一個機率 1/keep_prob
,才能保持分佈的統一。不過,咱們用一種叫作inverted dropout的技巧,就是如上面代碼所示,直接在訓練模式下多除以一個機率 keep_prob
,那麼在測試模式下就不用作任何操做了,直接讓數據經過dropout層便可。以下圖所示:
在最後,函數輸出通過dropout且與輸入x相同shape的out輸出矩陣和緩衝保留dropout的參數字典 dropout_param
,其中含有該層的失活機率超參數 p
,模式狀態 mode
,和隨機失活遮罩 mask
,以在反向傳播中備用。
在訓練模式下的反向傳播中,損失函數的梯度經過dropout層的 dropout_backward(dout, cache) = dx
函數恐怕是寫起來最簡單的:
def dropout_backward(dout, cache):
""" Perform the backward pass for (inverted) dropout. Inputs: - dout: Upstream derivatives, of any shape - cache: (dropout_param, mask) from dropout_forward. """
dropout_param, mask = cache
mode = dropout_param['mode']
dx = None
if mode == 'train':
dx = mask * dout
elif mode == 'test':
dx = dout
return dx
代碼詳解:
mask
將被遮罩的梯度置零。小注:
最先的Dropout能夠看Hinton的這篇文章 《Improving neural networks by preventing co-adaptation of feature Detectors》,在Dropout發佈後,很快有大量研究爲何它的實踐效果如此之好,以及它和其餘正則化方法之間的關係。若是你感興趣,能夠看看這些文獻:
對於Dropout這樣的操做爲什麼能夠防止訓練過擬合,原做者也沒有給出數學證實,只是有一些直觀的理解或者說猜測,好比:因爲隨機的讓一些神經元不工做了,所以能夠避免某些特徵只在固定組合下才生效,有意識地讓神經網絡去學習一些廣泛的共性(而不是某些訓練樣本的一些特性)。
更多細節能夠閱讀原論文了解,也能夠參考如下博文:
深度學習網絡大殺器之Dropout——深刻解析Dropout
Regularization of Neural Networks using DropConnect
接下來,咱們要作一件很酷的事情,那就是構造一張更增強大的全鏈接神經網絡,不只包含了咱們以前故事裏提到的全部算法和技巧,同時不限定神經網絡的深度(隱藏層的層數)和厚度(隱藏層神經元的個數)。
下面直接一行一行的閱讀代碼,咱們定義一個 FullyConnectedNet()
類,裏面只有一個初始化函數 __init__()
和一個損失函數 loss()
:
class FullyConnectedNet(object):
""" 一個任意隱藏層數和神經元數的全鏈接神經網絡,其中 ReLU 激活函數,sofmax 損失函數,同時可選的 採用 dropout 和 batch normalization(批量歸一化)。那麼,對於一個L層的神經網絡來講,其 框架是: {affine - [batch norm] - relu - [dropout]} x (L - 1) - affine - softmax 其中的[batch norm]和[dropout]是可選非必須的,框架中{...}部分將會重複L-1次,表明 L-1 個隱藏層。 與咱們在上面的故事中定義的 TwoLayerNet() 類保持一致,全部待學習的參數都會存在 self.params 字典中,而且最終會被最優化 Solver() 類訓練學習獲得(後面的故事會談到)。 """
""" """
#1# 第一步是初始化咱們的 FullyConnectedNet() 類:
def __init__(self ,hidden_dims # 一個列表,元素個數是隱藏層數,元素值爲該層神經元數 ,input_dim=3*32*32 # 默認輸入神經元的個數是3072個(匹配CIFAR-10數據集) ,num_classes=10 # 默認輸出神經元的個數是10個(匹配CIFAR-10數據集) ,dropout=0 # 默認不開啓dropout,若取(0,1)表示失活機率 ,use_batchnorm=False # 默認不開啓批量歸一化,若開啓取True ,reg=0.0 # 默認無L2正則化,取某scalar表示正則化的強度 ,weight_scale=1e-2 # 默認0.01,表示權重參數初始化的標準差 ,dtype=np.float64 # 默認np.float64精度,要求全部的計算都應該在此精度下。 ,seed=None): # 默認無隨機種子,如有會傳遞給dropout層。
""" """
# 實例(Instance)中增長變量並賦予初值,以方便後面的 loss() 函數調用:
self.use_batchnorm = use_batchnorm
self.use_dropout = dropout > 0 # 可見,若dropout若爲0時,爲False
self.reg = reg
self.num_layers = 1 + len(hidden_dims) # 在loss()函數裏,
# 咱們用神經網絡的層數來標記規模
self.dtype = dtype
self.params = {} # self.params 空字典保存待訓練學習的參數
""" """
# 定義全部隱藏層的參數到字典 self.params 中:
in_dim = input_dim # Eg: in_dim = D
for i, h_dim in enumerate(hidden_dims): # Eg:(i, h_dim)=(0, H1)、(1, H2)...
# Eg: W1(D, H1)、W2(H1, H2)... 小隨機數爲初始值
self.params["W%d" % (i+1,)] = weight_scale * \
np.random.randn(in_dim
,h_dim)
# Eg: b1(H1,)、b2(H2,)... 0爲初始值
self.params["b%d" % (i+1,)] = np.zeros((h_dim,))
if use_batchnorm: # 如有批量歸一化層
# Eg: gamma1(H1,)、gamma2(H2,)... 1爲初始值
# Eg: beta1(H1,)、beta2(H2)... 0爲初始值
self.params["gamma%d" % (i+1,)] = np.ones((h_dim,))
self.params["beta%d" % (i+1,)] = np.zeros((h_dim,))
in_dim = h_dim # 將該隱藏層的列數傳遞給下一層的行數
""" """
# 定義輸出層的參數到字典 params 中:
self.params["W%d" % (self.num_layers,)] = weight_scale * \
np.random.randn(in_dim
,num_classes)
self.params["b%d" % (self.num_layers,)] = np.zeros((num_classes,))
""" """
""" 當開啓 dropout 時,咱們須要在每個神經元層中傳遞一個相同的 dropout 參數字典 self.dropout_param ,以保證每一層的神經元們 都知曉失活機率p和當前神經網絡的模式狀態mode(訓練/測試)。 """
self.dropout_param = {} # dropout的參數字典
if self.use_dropout: # 若是use_dropout的值是(0,1),即啓用dropout
# 設置mode默認爲訓練模式,取p爲失活機率
self.dropout_param = {'mode': 'train', 'p': dropout}
if seed is not None: # 若是有seed隨機種子,存入seed
self.dropout_param['seed'] = seed
""" """
""" 當開啓批量歸一化時,咱們要定義一個BN算法的參數列表 self.bn_params , 以用來跟蹤記錄每一層的平均值和標準差。其中,第0個元素 self.bn_params[0] 表示前向傳播第1個BN層的參數,第1個元素 self.bn_params[1] 表示前向傳播 第2個BN層的參數,以此類推。 """
self.bn_params = [] # BN算法的參數列表
if self.use_batchnorm: # 若是開啓批量歸一化,設置每層mode默認爲訓練模式
self.bn_params = [{'mode': 'train'} for i in range(self.num_layers - 1)]
# 上面 self.bn_params 列表的元素個數是hidden layers的個數
""" """
# 最後,調整全部的待學習神經網絡參數爲指定計算精度:np.float64
for k, v in self.params.items():
self.params[k] = v.astype(dtype)
""" """
#2# 第二步是定義咱們的損失函數:
def loss(self, X, y=None):
""" 和 TwoLayerNet() 同樣: 首先,輸入的數據X是一個多維的array,shape爲(樣本圖片的個數N*3*32*32), y是與輸入數據X對應的正確標籤,shape爲(N,)。 # 在訓練模式下:# 咱們loss函數目標輸出一個損失值loss和一個grads字典, 其中存有loss關於隱藏層和輸出層的參數(W,B,gamma,beta)的梯度值. # 在測試模式下:# 咱們的loss函數只須要直接給出輸出層後的得分便可。 """
""" """
# 把輸入數據源矩陣X的精度調整一下
X = X.astype(self.dtype)
# 根據正確標籤y是否爲None來調整模式是test仍是train
mode = 'test' if y is None else 'train'
""" """
""" 肯定了當前神經網絡所處的模式狀態後, 就能夠設置 dropout 的參數字典和 BN 算法的參數列表中的mode了, 由於他們在不一樣的模式下行爲是不一樣的。 """
if self.dropout_param is not None: # 若是開啓dropout
self.dropout_param['mode'] = mode
if self.use_batchnorm: # 若是開啓批量歸一化
for bn_param in self.bn_params:
bn_param['mode'] = mode
""" """
scores = None
""" """
""" %前向傳播% 若是開啓了dropout,咱們須要將dropout的參數字典 self.dropout_param 在每個dropout層中傳遞。 若是開啓了批量歸一化,咱們須要指定BN算法的參數列表 self.bn_params[0] 對應前向傳播第一層的參數,self.bn_params[1]對應第二層的參數,以此類推。 """
fc_mix_cache = {} # 初始化每層前向傳播的緩衝字典
if self.use_dropout: # 若是開啓了dropout,初始化其對應的緩衝字典
dp_cache = {}
""" """
# 從第一個隱藏層開始循環每個隱藏層,傳遞數據out,保存每一層的緩衝cache
out = X
for i in range(self.num_layers - 1): # 在每一個hidden層中循環
w, b = self.params["W%d" % (i + 1,)], self.params["b%d" % (i + 1,)]
if self.use_batchnorm: # 若開啓批量歸一化
gamma = self.params["gamma%d" % (i + 1,)]
beta = self.params["beta%d" % (i + 1,)]
out, fc_mix_cache[i] = affine_bn_relu_forward(out, w, b
,gamma
,beta
,self.bn_params[i])
else: # 若未開啓批量歸一化
out, fc_mix_cache[i] = affine_relu_forward(out, w, b)
if self.use_dropout: # 若開啓dropout
out, dp_cache[i] = dropout_forward(out, self.dropout_param)
# 最後的輸出層
w = self.params["W%d" % (self.num_layers,)]
b = self.params["b%d" % (self.num_layers,)]
out, out_cache = affine_forward(out, w, b)
scores = out
""" 能夠看到,上面對隱藏層的每次循環中,out變量實現了自我迭代更新; fc_mix_cache 緩衝字典中順序地存儲了每一個隱藏層的得分狀況和模型參數(其中可內含BN層); dp_cache 緩衝字典中單獨順序地保存了每一個dropout層的失活機率和遮罩; out_cache 變量緩存了輸出層處的信息; 值得留意的是,若開啓批量歸一化的話,BN層的參數列表 self.bn_params[i], 從第一層開始多出'running_mean'和'running_var'的鍵值保存在參數列表的每個元素中, 形如:[{'mode': 'train','running_mean': ***,'running_var':***},{...}] """
""" """
# 接下來開始讓loss函數區分不一樣模式:
if mode == 'test': # 如果測試模式,輸出scores表示預測的每一個分類機率後,函數中止跳出。
return scores
""" """
""" %反向傳播% 既然運行到了這裏,說明咱們的神經網絡是在訓練模式下了, 接下來咱們要計算損失值,而且經過反向傳播,計算損失函數關於模型參數的梯度! """
loss, grads = 0.0, {} # 初始化 loss 變量和梯度字典 grads
loss, dout = softmax_loss(scores, y)
loss += 0.5 * self.reg * np.sum(self.params["W%d" % (self.num_layers,)]**2)
""" 你可能奇怪上面的loss損失值是否是有問題,還有其餘隱藏層的權重矩陣的正則化呢? 彆着急,咱們要loss損失值的求解,跟隨梯度的反向傳播一點一點的算出來~ """
# 在輸出層處梯度的反向傳播,順便把梯度保存在梯度字典 grad 中:
dout, dw, db = affine_backward(dout, out_cache)
grads["W%d" % (self.num_layers,)] = dw + self.reg * \
self.params["W%d" % (self.num_layers,)]
grads["b%d" % (self.num_layers,)] = db
# 在每個隱藏層處梯度的反向傳播,不只順便更新了梯度字典 grad,還迭代算出了損失值loss:
for i in range(self.num_layers - 1):
ri = self.num_layers - 2 - i # 倒數第ri+1隱藏層
loss += 0.5 * self.reg * \ # 迭代地補上每層的正則項給loss
np.sum(self.params["W%d" % (ri+1,)]**2)
if self.use_dropout: # 若開啓dropout
dout = dropout_backward(dout, dp_cache[ri])
if self.use_batchnorm: # 若開啓批量歸一化
dout, dw, db, dgamma, dbeta = affine_bn_relu_backward(dout,
fc_mix_cache[ri])
grads["gamma%d" % (ri+1,)] = dgamma
grads["beta%d" % (ri+1,)] = dbeta
else: # 若未開啓批量歸一化
dout, dw, db = affine_relu_backward(dout, fc_mix_cache[ri])
grads["W%d" % (ri+1,)] = dw + self.reg * self.params["W%d" % (ri+1,)]
grads["b%d" % (ri+1,)] = db
return loss, grads # 輸出訓練模式下的損失值和損失函數的梯度!
雖然註釋已經給的足夠詳細了,但仍是有必要爲這個強大的神經網絡畫一張「數據流動走向圖」才能真的讓咱們心有成竹,媽媽不再用擔憂個人深度學習~
如今,咱們已經會很輕的鬆搭建一個強大且複雜的神經網絡了,而且咱們始終採用模塊化代碼一步一步實現,於是能夠很容易將其轉化成應對不一樣場景環境的神經網絡模型。然而,咱們的故事其實一直在很模糊一個事實:
接下來,咱們的故事就會接觸到這些最優化問題,只要咱們解決掉這個故事中上述的終極大boss,才能真正完美的劇終~
不忘初心,方得始終。
回憶一下,咱們最初構建神經網絡是爲了什麼呢?~~~ 是爲了給圖片分類。當一堆樣本圖片進入到神經網絡中時,全部的神經元都在不斷的打分作評價,最終輸出的是兩個值:損失函數的值和損失函數關於模型參數的梯度。而神經網絡爲了能正確的分辨出每個圖片,就必須有一套」正確的」模型參數才行,其使得損失函數達到(局部)最小值。因而,基於某一批樣本圖片下,如何利用算得的損失函數值及其關於模型參數的梯度,就成爲了對神經網絡最優化的核心問題。
故事在談到反向傳播算法的時候,咱們其實已經接觸到了隨機梯度降低(stochastic gradient descent)的內涵:在負梯度方向上對模型參數進行更新。
雖說的很神乎其神,關鍵必定要明白梯度方向是讓損失函數值增大的方向就好,下面直接定義 sgd(w, dw, config=None) = (w, config)
函數代碼就更清楚了:
def sgd(w, dw, config=None):
""" Performs vanilla stochastic gradient descent. config format: - learning_rate: Scalar learning rate. """
if config is None: config = {}
config.setdefault('learning_rate', 1e-2)
w -= config['learning_rate'] * dw
return w, config
代碼詳解:
w, dw
,分別是樣本圖片通過神經網絡時的某個模型參數和該參數對應的損失函數梯度;config
若是沒有在函數中輸入的話,會默認爲一個字典:{'learning_rate', 1e-2}
,其中 learning_rate
一般叫作學習率,它是一個固定的常量,是一個超參數,即與「模型參數」不一樣,是須要人工經驗在神經網絡外部設置調整的,與待學習的模型參數不一樣。當神經網絡在整個圖片數據集上進行計算時,只要學習率足夠低,老是能在損失函數上獲得非負的進展。w -= config['learning_rate'] * dw
就是隨機梯度降低的算法核心了,只須要在每一次參數更新時,減去以學習率爲倍數的梯度值便可,最後輸出更新後的新模型參數 w
和sgd算法的設置字典 config
。上述就是最簡單最普通的模型參數更新方法了,其核心思想就是要找到損失函數的超曲面上最陡峭的方向,而後投影到模型參數空間中,爲模型參數的更新帶來啓示。以下圖:
不過這種樸素的方法,對於像是峽谷形狀的損失函數曲面上的點來講就效果沒那麼好了:
上圖中的曲線是損失函數的關於模型參數的」等高線」,每一條曲線上的點都有着相同的損失函數值,圓心處對應於損失函數的局部最小值。途中的a和b都是神經網絡在面對每一批不一樣的樣本圖片時所進行的參數更新路線,a很順利的一步步走向」盆地「的最低點,而b的出發點位於一個細窄的」峽谷「處,因而負梯度方向就極可能並非一直指向着盆地最低點,而是會先順着更」陡峭」峽谷走到谷底,再一點一點震盪着靠近盆地最低點。
顯然,運氣要是很差,傳統的梯度降低方法的收斂速度會很糟糕。因而,就有了隨機梯度降低的動量更新方法(stochastic gradient descent with momentum),這個方法在神經網絡上幾乎總能獲得更好的收斂速度。該方法能夠當作是從物理角度上對於最優化問題獲得的啓發。傳統的梯度降低本質上是將損失函數的梯度向量投影在模型參數空間,而後指導每一個模型參數如何更新,每次更新的幅度由learnning_rate來控制;而動量版本的梯度降低方法能夠看做是將初速度爲0的小球從在一個山體表面上放手,不一樣的損失值表明山體表面的海拔高度,那麼模型參數的更新再也不去參考小球所在處的山體傾斜狀況,而是改用小球的速度矢量在模型參數空間的投影來修正更新參數。
如上圖所示,從第一次參數更新以後,每一點的梯度矢量和速度矢量通常是不一樣的。小球所收到的合外力能夠看做是保守力,就是損失函數的負梯度(F=−∇L),再由牛頓定理(F=ma=mdvdt),就能夠獲得動量關於時間的變化關係——亦所謂動量定理(Fdt=mdv),若在單位時間t上兩邊積分,且單位時間t內看做每一次模型參數的迭代更新的話,就能夠給出動量更新的表達式:p1=p0−a∇L (其中,學習率a,動量p=mv,並默認了單位數間t內負梯度是與時間不相關的函數)。與BN算法中的一次指數平滑法相似,咱們能夠在迭代更新動量的過程當中引入第二個超參數μ,以指數衰減的方式」跟蹤」上一次」動量」的更新:p1=μp0−a∇L。最後將這個動量矢量投影到參數空間去更新模型參數。
直接看代碼吧!咱們定義函數 sgd_momentum(w, dw, config) = (next_w, config)
:
def sgd_momentum(w, dw, config=None):
""" Performs stochastic gradient descent with momentum. config format: - learning_rate: Scalar learning rate. - momentum: Scalar between 0 and 1 giving the momentum value. Setting momentum = 0 reduces to sgd. - velocity: A numpy array of the same shape as w and dw used to store a moving average of the gradients. """
if config is None: config = {}
config.setdefault('learning_rate', 1e-2)
config.setdefault('momentum', 0.9)
v = config.get('velocity', np.zeros_like(w))
next_w = None
v = config["momentum"] * v - config["learning_rate"] * dw
next_w = w + v
config['velocity'] = v
return next_w, config
代碼詳解:
(w, dw)
和輸出(next_w, config)
與 sgd()
函數格式以及含義徹底對應一致,只不過 sgd_momentum()
函數的最優化超參數有三個 {'learning_rate': le-2, 'momentum': 0.9, 'velocity': np.zeros_like(w)}
,其中的velocity
並非經驗可調的超參數,其對應於咱們每一次更新時的「動量」,而且在每一次模型參數更新後,都會將更新後的「動量」再保存回 config
字典中。最後值得一提的是,上述定義的兩個最優化函數,都是針對神經網絡模型中的每個參數來更新的,即在參數空間中的某一維度上進行的計算,亦對應於前面談到的梯度矢量或動量矢量在參數空間的投影后,某一參數維度中的運算。
相關的資料能夠參考:
An Overview on Optimization Algorithms in Deep Learning (I)
Advanced tutorials - momentum, activation function, etc
An overview of gradient descent optimization algorithms
小結:
除了上面提到的兩個簡單的參數更新方法外,還有很多更好用的方法,如Nesterov’s Accelerated Momentum,還有逐參數適應學習率方法,如Adagrad,RMSprop,Adam等等。在cs231n課程的講義中,推薦的兩個參數更新方法是SGD+Nesterov動量方法,或者Adam方法。
咱們能夠看到,訓練一個神經網絡會遇到不少超參數設置。關於超參數的調優也會有不少的要點和技巧,不過,咱們的故事並不打算涉及其中,詳情可參考cs231n的講義筆記。
通過前面故事中各類磨難的鍛鍊,經驗的積累,如今終於能夠一試身手,完成最後boss的挑戰——咱們要真正的訓練一個強大的全鏈接的神經網絡,來挑戰10分類的數據源CIFAR-10!
首先在決鬥以前,咱們要作好一些準備工做:加載已經預處理好的數據源爲 data
。
# Load the (preprocessed) CIFAR10 data.
data = get_CIFAR10_data()
for k, v in data.items():
print('%s: ' % k, v.shape)
""" y_train: (49000,) y_test: (1000,) X_val: (1000, 3, 32, 32) y_val: (1000,) X_test: (1000, 3, 32, 32) X_train: (49000, 3, 32, 32) """
能夠看到已經準備好的訓練集樣本圖片有49000張,另有用來調參優化網絡的驗證樣本圖片1000張,最後是考驗咱們的神經模型的測試集樣本圖片1000張。
接下來是最重要的一步,咱們要在見最終分曉以前,起草一個詳細的做戰方案——定義一個 Solve(object)
類。
此時已月黑風高,話很少說,請直接一行一行地閱讀到底,勿忘閱後即焚!
import numpy as np
# 咱們把優化算法的函數 sgd() 和 sgd_momentum() 定義在當前目錄中的 optim.py 文件中
import optim
class Solver(object):
""" 咱們定義的這個Solver類將會根據咱們的神經網絡模型框架——FullyConnectedNet()類, 在數據源的訓練集部分和驗證集部分中,訓練咱們的模型,而且經過週期性的檢查準確率的方式, 以免過擬合。 在這個類中,包括__init__(),共定義5個函數,其中只有train()函數是最重要的。調用 它後,會自動啓動神經網絡模型優化程序。 訓練結束後,通過更新在驗證集上優化後的模型參數會保存在model.params中。此外,損失值的 歷史訓練信息會保存在solver.loss_history中,還有solver.train_acc_history和 solver.val_acc_history中會分別保存訓練集和驗證集在每一次epoch時的模型準確率。 =============================== 下面是給出一個Solver類使用的實例: data = { 'X_train': # training data 'y_train': # training labels 'X_val': # validation data ' y_val': # validation labels } # 以字典的形式存入訓練集和驗證集的數據和標籤 model = FullyConnectedNet(hidden_size=100, reg=10) # 咱們的神經網絡模型 solver = Solver(model, data, # 模型/數據 update_rule='sgd', # 優化算法 optim_config={ # 該優化算法的參數 'learning_rate': 1e-3, # 學習率 }, lr_decay=0.95, # 學習率的衰減速率 num_epochs=10, # 訓練模型的遍數 batch_size=100, # 每次丟入模型訓練的圖片數目 print_every=100) solver.train() =============================== # 神經網絡模型中必需要有兩個函數方法:模型參數model.params和損失函數model.loss(X, y) A Solver works on a model object that must conform to the following API: - model.params must be a dictionary mapping string parameter names to numpy arrays containing parameter values. # - model.loss(X, y) must be a function that computes training-time loss and gradients, and test-time classification scores, with the following inputs and outputs: Inputs: # 全局的輸入變量 - X: Array giving a minibatch of input data of shape (N, d_1, ..., d_k) - y: Array of labels, of shape (N,) giving labels for X where y[i] is the label for X[i]. Returns: # 全局的輸出變量 # 用標籤y的存在與否標記訓練mode仍是測試mode If y is None, run a test-time forward pass and return: # - scores: Array of shape (N, C) giving classification scores for X where scores[i, c] gives the score of class c for X[i]. If y is not None, run a training time forward and backward pass and return a tuple of: - loss: Scalar giving the loss # 損失函數值 - grads: Dictionary with the same keys as self.params mapping parameter names to gradients of the loss with respect to those parameters.# 模型梯度 """
""" """
#1# 初始化咱們的 Slover() 類:
def __init__(self, model, data, **kwargs):
""" Construct a new Solver instance. # 必需要輸入的函數參數:模型和數據 Required arguments: - model: A model object conforming to the API described above - data: A dictionary of training and validation data with the following: 'X_train': Array of shape (N_train, d_1, ..., d_k) giving training images 'X_val': Array of shape (N_val, d_1, ..., d_k) giving validation images 'y_train': Array of shape (N_train,) giving labels for training images 'y_val': Array of shape (N_val,) giving labels for validation images # 可選的輸入參數: Optional arguments: # 優化算法:默認爲sgd - update_rule: A string giving the name of an update rule in optim.py. Default is 'sgd'. # 設置優化算法的超參數: - optim_config: A dictionary containing hyperparameters that will be passed to the chosen update rule. Each update rule requires different hyperparameters (see optim.py) but all update rules require a 'learning_rate' parameter so that should always be present. # 學習率在每次epoch時衰減率 - lr_decay: A scalar for learning rate decay; after each epoch the learning rate is multiplied by this value. # 在訓練時,模型輸入層接收樣本圖片的大小,默認100 - batch_size: Size of minibatches used to compute loss and gradient during training. # 在訓練時,讓神經網絡模型一次全套訓練的遍數 - num_epochs: The number of epochs to run for during training. # 在訓練時,打印損失值的迭代次數 - print_every: Integer; training losses will be printed every print_every iterations. # 是否在訓練時輸出中間過程 - verbose: Boolean; if set to false then no output will be printed during training. """
# 實例(Instance)中增長變量並賦予初值,以方便後面的 train() 函數等調用:
self.model = model # 模型
self.X_train = data["X_train"] # 訓練樣本圖片數據
self.y_train = data["y_train"] # 訓練樣本圖片的標籤
self.X_val, self.y_val = data["X_val"], data["y_val"] # 驗證樣本圖片的數據和標籤
""" 如下是可選擇輸入的類參數,逐漸一個一個剪切打包kwargs參數列表 """
self.update_rule = kwargs.pop('update_rule', 'sgd') # 默認優化算法sgd
self.optim_config = kwargs.pop('optim_config', {}) # 默認設置優化算法爲空字典
self.lr_decay = kwargs.pop('lr_decay', 1.0) # 默認學習率不衰減
self.batch_size = kwargs.pop('batch_size', 100) # 默認輸入層神經元數100
self.num_epochs = kwargs.pop('num_epochs', 10) # 默認神經網絡訓練10遍
#
self.print_every = kwargs.pop('print_every', 10)
self.verbose = kwargs.pop('verbose', True) # 默認打印訓練的中間過程
""" 異常處理:若是kwargs參數列表中除了上述元素外還有其餘的就報錯! """
if len(kwargs) > 0:
extra = ', '.join('"%s"' % k for k in kwargs.keys())
raise ValueError('Unrecognized arguments %s' % extra)
""" 異常處理:若是kwargs參數列表中沒有優化算法,就報錯! 將self.update_rule轉化爲優化算法的函數,即: self.update_rule(w, dw, config) = (next_w, config) """
if not hasattr(optim, self.update_rule): # 若optim.py中沒有寫好的優化算法對應
raise ValueError('Invalid update_rule "%s"' % self.update_rule)
self.update_rule = getattr(optim, self.update_rule)
# 執行_reset()函數:
self._reset()
""" """
#2# 定義咱們的 _reset() 函數,其僅在類初始化函數 __init__() 中調用
def _reset(self):
""" 重置一些用於記錄優化的變量 """
# Set up some variables for book-keeping
self.epoch = 0
self.best_val_acc = 0
self.best_params = {}
self.loss_history = []
self.train_acc_history = []
self.val_acc_history = []
# Make a deep copy of the optim_config for each parameter
self.optim_configs = {}
for p in self.model.params:
d = {k: v for k, v in self.optim_config.items()}
self.optim_configs[p] = d
""" 上面根據模型中待學習的參數,建立了新的優化字典self.optim_configs, 形如:{'b': {'learnning_rate': 0.0005} ,'w': {'learnning_rate': 0.0005}},爲每一個模型參數指定了相同的超參數。 """
""" """
#3# 定義咱們的 _step() 函數,其僅在 train() 函數中調用
def _step(self):
""" 訓練模式下,樣本圖片數據的一次正向和反向傳播,而且更新模型參數一次。 """
# Make a minibatch of training data # 輸入數據準備
num_train = self.X_train.shape[0] # 要訓練的數據集總數
batch_mask = np.random.choice(num_train, self.batch_size)
X_batch = self.X_train[batch_mask] # 隨機取得輸入神經元個數的樣本圖片數據
y_batch = self.y_train[batch_mask] # 隨機取得輸入神經元個數的樣本圖片標籤
""" """
# Compute loss and gradient # 數據經過神經網絡後獲得損失值和梯度字典
loss, grads = self.model.loss(X_batch, y_batch)
self.loss_history.append(loss) # 把本次算得的損失值記錄下來
""" """
# Perform a parameter update # 執行一次模型參數的更新
for p, w in self.model.params.items():
dw = grads[p] # 取出模型參數p對應的梯度值
config = self.optim_configs[p] # 取出模型參數p對應的優化超參數
next_w, next_config = self.update_rule(w, dw, config) # 優化算法
self.model.params[p] = next_w # 新參數替換掉舊的
self.optim_configs[p] = next_config # 新超參數替換掉舊的,如動量v
""" """
#4# 定義咱們的 check_accuracy() 函數,其僅在 train() 函數中調用
def check_accuracy(self, X, y, num_samples=None, batch_size=100):
""" 根據某圖片樣本數據,計算某與之對應的標籤的準確率 Inputs: - X: Array of data, of shape (N, d_1, ..., d_k) - y: Array of labels, of shape (N,) - num_samples: If not None, subsample the data and only test the model on num_samples datapoints. - batch_size: Split X and y into batches of this size to avoid using too much memory. Returns: - acc: Scalar giving the fraction of instances that were correctly classified by the model. """
# Maybe subsample the data
N = X.shape[0] # 樣本圖片X的總數
if num_samples is not None and N > num_samples:
mask = np.random.choice(N, num_samples)
N = num_samples
X = X[mask]
y = y[mask]
# Compute predictions in batches
num_batches = N // batch_size
if N % batch_size != 0:
num_batches += 1
y_pred = []
for i in range(num_batches):
start = i * batch_size
end = (i + 1) * batch_size
scores = self.model.loss(X[start:end])
y_pred.append(np.argmax(scores, axis=1))
y_pred = np.hstack(y_pred)
acc = np.mean(y_pred == y)
return acc
#5# 定義咱們最重要的 train() 函數
def train(self):
""" 首先要肯定下來總共要進行的迭代的次數num_iterations, """
num_train = self.X_train.shape[0] # 所有要用來訓練的樣本圖片總數
iterations_per_epoch = max(num_train // self.batch_size, 1) # 每遍迭代的次數
num_iterations = self.num_epochs * iterations_per_epoch # 總迭代次數
""" 開始迭代循環! """
for t in range(num_iterations):
self._step()
""" 上面完成了一次神經網絡的迭代。此時,模型的參數已經更新過一次, 而且在self.loss_history中添加了一個新的loss值 """
# Maybe print training loss 從self.loss_history中取最新的loss值
if self.verbose and t % self.print_every == 0:
print('(Iteration %d / %d) loss: %f' % (t + 1,
num_iterations, self.loss_history[-1]))
""" """
# At the end of every epoch, increment the epoch counter and
# decay the learning rate.
epoch_end = (t + 1) % iterations_per_epoch == 0
if epoch_end: # 只有當t = iterations_per_epoch-1 時爲True
self.epoch += 1 # 第一遍以後開始,從0自加1爲每遍計數
for k in self.optim_configs: # 第一遍以後開始,每遍給學習率自乘一個衰減率
self.optim_configs[k]['learning_rate'] *= self.lr_decay
""" """
# Check train and val accuracy on the first iteration, the last
# iteration, and at the end of each epoch.
first_it = (t == 0) # 起始的t
last_it = (t == num_iterations - 1) # 最後的t
if first_it or last_it or epoch_end: # 在最開始/最後/每遍結束時
train_acc = self.check_accuracy(self.X_train, self.y_train,
num_samples=1000) # 隨機取1000個訓練圖看準確率
val_acc = self.check_accuracy(self.X_val, self.y_val)
self.train_acc_history.append(train_acc) # 計算所有驗證圖片的準確率
self.val_acc_history.append(val_acc)
""" """
if self.verbose: # 在最開始/最後/每遍結束時,打印準確率等信息
print('(Epoch %d / %d) train acc: %f; val_acc: %f' % (
self.epoch, self.num_epochs, train_acc, val_acc))
""" """
# 在最開始/最後/每遍結束時,比較當前驗證集的準確率和過往最佳驗證集
# Keep track of the best model
if val_acc > self.best_val_acc:
self.best_val_acc = val_acc
self.best_params = {}
for k, v in self.model.params.items():
self.best_params[k] = v.copy() # copy()僅複製值過來
""" 結束迭代循環! """
# At the end of training swap the best params into the model
self.model.params = self.best_params # 最後把獲得的最佳模型參數存入到模型中
Softmax分類器梯度的證實過程:
關於矩陣Lij中第i行、第j列元素的全微分:
dLij=−1Nd(log[Sij])=−1Nd(log[exij∑kexik])=−1N∑kexikexik(exij∑kexikdxij−exijd(∑kexik)∑kexik∑lexil)=−1N(dxij−exijdxij+exikdxik∑lexil)(k≠j)=1N(1−Sij)dxij+1NSikdxik(k≠j)