從感知機到神經網絡(前向計算、反向傳播、python實現)

1 回顧感知機

廢話不多說,就不從什么模擬人類的神經元開始了,在感知機(Perceptron)中我們已經說過:感知機模型是神經網絡和支持向量機的基礎,現在我們終于講到神經網絡了,先來復習一下作為基礎的感知機。

感知機是一個通過分類超平面來對線性可分數據進行二分類的簡單模型,可以表示為:

f(x)=sign(θ \cdot {x})

sign(x)= \begin{cases} -1& {x<0}\\ 1& {x\geq 0} \end{cases}

其中,sign函數是一個階躍函數(step function),θ \cdot {x}=0為分類超平面,我們可以以下圖來描述一個感知機:

感知機結構

感知機還可以實現簡單的布爾運算,比如“and”、“or”、“not”,我們只知道感知機是個線性分類器,怎么還能進行布爾運算呢?看下圖相信就可以理解了,與或非運算其實也可以看成線性分類的問題(如下左圖),因此可以使用感知機,這同時也是感知機不能進行“異或”運算的原因,“異或”并不是線性可分的(如下右圖)。

由上圖可知,要解決非線性可分的問題,只用一個感知機是不夠的,比如異或問題,其實使用三個感知機組成一個兩層的模型就可以解決,相當于把非線性的分類邊界拆解成了多個線性的分類邊界,然后在下一層組合起來:

2 多層感知機、神經網絡

綜上所述,我們用多個、多層的感知機結構可以逼近非線性函數,所以我們就得到了多層感知機(MLP)的概念,也就是所謂的神經網絡,其中一個感知機我們稱為一個神經元,一個神經元的結構跟感知機是一樣的,不過其激活函數一般是一個非線性函數(原因后文再說),經常使用sigmod函數、tanh函數、relu函數等,如下圖所示:

神經元結構

將多個神經元按層次連接,像異或問題的解決方案中那樣,本層的輸出作為下一層的輸入,就得到了一個神經網絡,如下圖表示一個全連接(full connected, FC)神經網絡

神經網絡分為最左邊的輸入層、中間的隱藏層和最右邊的輸出層,我們總結一下全連接神經網絡的結構規則:

  • 同一層的神經元之間沒有連接;
  • 第N層的每個神經元和第N-1層的所有神經元相連(full connected),第N-1層神經元的輸出就是第N層神經元的輸入;
  • 每個連接都有一個權值。

神經網絡本質上就是以這種組合結構來逼近各種復雜的函數,所以其實神經網絡也相當于是一個函數:

\vec{y}=f_{NN}(\vec{x} )

理論上來說,神經網絡可以以任意小誤差近似定義在有限維空間的任意連續函數,這是神經網絡的普遍性定理,可以簡單的理解一下:兩個感知機構建的單層網絡可以表示一個脈沖函數,通過類似積分的思想,使用很多個脈沖函數來無限逼近任意函數曲線:

注意我們上面說過神經元的激活函數一般是非線性函數,這是為什么呢?因為如果使用線性激活函數或者不使用激活函數,那么無論神經網絡有多少層,其實都是一直在做線性運算而已,其組合還是線性函數,是無法近似更復雜的函數的

3 神經網絡的前向計算

前向計算:從網絡的輸入層開始,依次逐層往前計算,直到計算出輸出層的結果,即求\vec{y}=f_{NN}(\vec{x} )的過程。

我們先定義清楚神經網絡權重上表示不同層、不同神經元的角標,記w^l_{jk}為第l?1層第k個神經元到第l層第j個神經元的權重,b^l_j為第l層第j個神經元的偏置,a^l_j為第l層第j個神經元的輸出,如下圖中w^3_{24}表示第2層的第4個神經元與第3層第2個神經元之間連接的權重。

因此,每層神經元的輸出值可表示為:

a^l_j=σ(\sum_k w^l_{jk}a^{l-1}_k+b^l_j)

在實際使用時,為了提高效率,這些計算都是以矩陣形式進行的,上面的計算過程的矩陣形式為:

a^l=\sigma{(w^l a^{l-1} +b^l)}

舉個例子,對下圖中的三層神經網絡,使用矩陣形式來進行前向計算:

\vec{a}_2=\sigma{(\vec{w}_2\vec{x} +\vec{b}_2)}

\vec{y}=\sigma{(\vec{w}_3\vec{a}_2 +\vec{b}_3)}

下面是矩陣形式前向計算的代碼,很簡單:

def feedforward(self, a):
        for b, w in zip(self.bias, self.weights):
            a = self.sigmod(np.dot(w, a) + b)  
        return a

4 神經網絡的訓練——反向傳播

4.1 為什么要用反向傳播

對算法的訓練我們都很熟悉,無非就是根據真實y值和預測y值之間的差別來更新算法中的參數,而且我們早就掌握了一個訓練神器——梯度下降,那么神經網絡的訓練是否可以按這個思路走下去呢?

假設代價函數是C,目標是min(C),使用梯度下降:

w_{ji}= w_{ji} - \eta\frac{\partial C}{\partial w_{ji}}

首先我們需要對代價函數進行求偏導,這就產生了第一個困難,不同于傳統的機器學習算法,對于神經網絡\vec{y}=f_{NN}(\vec{x} )來說,我們并不知道其具體的函數表達式,故無法直接根據損失函數求參數w導,不過這難不倒我們,我們可以直接根據導數的定義來進行求導:

我們可以給每個權重w一個微小的改變\Delta w,然后前向計算得到預測y,再得到損失函數的變化量\Delta L來計算導數,看起來似乎計算簡單很可行的樣子,然而要注意到,假設有一個 [ 200, 100, 1 ] 的三層全連接網絡,那么其參數共201\times100+101\times1=20201個參數,那么需要20201次前向計算才能得到所有參數的偏導數,這可太難了,如果網絡復雜的話,一般的計算能力做不到了啊,這就是第二個困難:計算效率,這時候我們需要另一大神器:反向傳播。

4.2 反向傳播理解與推導

考慮到神經網絡的復雜結構,要計算參數對代價函數的偏導數,首先要對參數變化影響代價函數過程有所理解。對于NN的分層結構,每一層可以看做上一層的輸出,因此可以認為每層都根據其輸出的誤差來調整參數,我們引入中間量\delta_j^l=\frac{\partial{C}}{\partial z_j^l},z_j^l為神經元j的帶權輸入,這個我們稱為在 l 層第j 個神經元上的誤差項,\delta_j^l是誤差的一種度量,代表了代價函數對帶權輸入的變化率。設想下圖中紅色的小鬼專門負責調整帶權輸入,他調整\Delta z_j^l,則代價函數變化\delta_j^l\Delta z_j^l,當他選擇與\delta_j^l相反的方向調整z_j^l時,代價函數就會不斷減小,直到\delta_j^l趨近于0,這就是在NN中使用梯度下降的方法,現在我們知道,重點是獲得每一層的誤差項\delta_j^l。(其實誤差項定義成對激活值什么的偏導也行,不過使用帶權輸入比較便于計算)

反向傳播給我們提供了計算每層\delta_j^l的方法,其基礎是導數的鏈式法則(不懂鏈式法則的可以看文末的附錄),可以把反向傳播理解為:像一根鏈子(或繩子),誤差項先在尾部得到,然后沿著鏈子(或繩子)依次向前傳播誤差項,就像下圖的甩繩一樣:

圖片來自網絡

1)輸出層的誤差項

我們先來計算尾部輸出層的誤差項,這是反向傳播的起點:

\delta_j^L=\frac{\partial C}{\partial z_j^L}=\frac{\partial C}{\partial a_j^L}\frac{\partial a_j^L}{\partial z_j^L}=\frac{\partial C}{\partial a_j^L}\sigma'(z_j^L)

這沒什么好說的,鏈式法則而已,其中\sigma是激活函數。將其寫成矩陣運算的形式:

\delta^L=\nabla_aC \odot \sigma'(z^L)

式中⊙ 為Hadamard積,即矩陣的點積。

2)隱藏層的誤差項

誤差項傳播的起點有了,接下來就要往前(隱藏層)傳播了。要注意到,對于一個隱藏層結點,其下游(即下一層與之直接相連的結點)一般會有多個結點,這個隱藏層結點的帶權輸入的變化會對全部的下游結點產生影響,因此我們定義l層第j個結點的下游結點集合為Downstream(j),隱藏層誤差項的計算如下:

\begin{align} \delta^l=\frac{\partial{C}}{\partial{z^l_j}}&=\sum_{k\in Downstream(j)}\frac{\partial C}{\partial z_k^{l+1}}\frac{\partial{z_k^{l+1}}}{\partial{z_j^l}}\\ &=\sum_{k\in Downstream(j)}\delta_k^{l+1}\frac{\partial{z_k^{l+1}}}{\partial{z_j^l}}\\ &=\sum_{k\in Downstream(j)}\delta_k^{l+1}\frac{\partial{z_k^{l+1}}}{\partial{a_j^{l}}}\frac{\partial{a_j^l}}{\partial{z_j^l}}\\ &=\sum_{k\in Downstream(j)}\delta_k^{l+1}w_{kj}^{l+1}\frac{\partial a_j^l}{\partial{z_j^l}}\\ &=\sum_{k\in Downstream(j)}\delta_k^{l+1}w_{kj}^{l+1}\sigma'(z_j^l) \end{align}

同樣可以寫成矩陣運算的形式:

\delta^{l}=(W^{l+1})^T\delta^{l+1}\odot\sigma'(z^l)

3)代價函數對參數的偏導

得到誤差項(代價函數對帶權輸入的偏導)的計算方法,別忘了我們的最終目標:求出代價函數對參數的偏導,使用梯度下降來更新模型參數,下面分別是代價函數對偏置b和權重w的偏導計算方法:

\frac{\partial C}{\partial b_j^l}=\frac{\partial C}{\partial z_j^l}\frac{\partial z_j^l}{\partial b_j^l}=\frac{\partial C}{\partial z_j^l}=\delta_j^l

\frac{\partial C}{\partial w_{jk}^l}=\frac{\partial C}{\partial z_j^l}\frac{\partial z_j^l}{\partial w_{jk}^l}=\frac{\partial C}{\partial z_j^l}a_k^{l-1}=a_k^{l-1}\delta_j^l

有了這兩個公式,直接代入梯度下降中去就行了,這一步就跟其他算法沒什么區別了,不再贅述。

4.3 反向傳播python實現

矩陣運算形式的反向傳播代碼,就是把以上我們推導的四個公式寫成代碼而已:

def backprop(self, x, y):
        b_tmp = [np.zeros(b.shape) for b in self.bias]
        w_tmp = [np.zeros(w.shape) for w in self.weights]
        # 前向,與feedforward函數的區別是,每一層的輸出a和加權輸入z需要記錄下來
        activation = x
        activations = [x]
        zs = []
        for w, b in zip(self.weights, self.bias):
            z = np.dot(w, activation) + b
            zs.append(z)
            activation = self.sigmod(z)
            activations.append(activation)
        # 后向,根據前向記錄的結果得到delta
        delta = self.cost_derivative(activations[-1], y) * self.sigmod_derivative(zs[-1])
        b_tmp[-1] = delta
        w_tmp[-1] = np.dot(delta, activations[-2].transpose())  # activations[-2]層的輸出,就是輸出層的輸入x
        # 對np.array各個緯度的轉變需要弄清楚
        # 接下來對隱含層
        for l in range(2, self.layer_count):
            z = zs[-l]
            delta = np.dot(self.weights[-l + 1].transpose(), delta) * self.sigmod_derivative(z)
            b_tmp[-l] = delta
            w_tmp[-l] = np.dot(delta, activations[-l - 1].transpose())
        return b_tmp, w_tmp

5 總結

本文介紹了神經網絡的一些基本概念和原理,神經網絡在當下的火爆程度不必多說,反向傳播居功甚偉,理解了全連接網絡的反向傳播,接下來再去理解CNN、RNN等特殊結構的神經網絡就容易多了。

附錄

導數的鏈式法則



主要參考

感知機
《Neural Network and Deep Learning》- Michael Nielsen

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。