反向傳播演算法(BackPropagation)

限量版 愛發表於2017-11-22

你可以在這裡閱讀 上一篇

我是薛銀亮,感謝英文原版線上書籍,這是我學習機器學習過程中感覺非常適合新手入門的一本書。鑑於知識分享的精神,我希望能將其翻譯過來,並分享給所有想了解機器學習的人,本人翻譯水平有限,歡迎讀者提出問題和發現錯誤,更歡迎大牛的指導。因為篇幅較長,文章將會分為多個部分進行,感興趣的可以關注我的文集,文章會持續更新。


在上一篇文章中,我們看到了神經網路如何通過梯度下降演算法學習調整它的權重和b值,但是我們省略瞭如何計算損失函式的梯度。在這一篇文章中,我將會介紹一種計算梯度的快速演算法:反向傳播演算法(BackPropagation)。

這篇文章會涉及很多數學,如果你覺得沒有興趣或者很迷惑,可以跳過,那樣你可以直接把反向傳播當成一個黑盒來使用。但是有什麼值得我們花時間來學習這些細節呢?

理由當然是:理解。反向傳播的核心是通過改變對於網路中的權重w和偏移b,計算損失函式C的偏導數∂C/∂w,從而求出損失變化的極值(如果這句話看不懂,我建議你去先看看微積分)。這個表示式告訴我們當我們改變權重w和偏移b,損失函式變化的速度。雖然它有時很複雜,但是它可以對每個元素有一個自然的,直觀的解釋,它實際給了我們一個詳細的修改權重和b值來改變網路整體行為的辦法。這是非常值得研究的。

熱身:一種基於矩陣的快速計算神經網路輸出的方法


在討論反向傳播之前,讓我們先用一種基於矩陣的快速傳播方法來預測一個神經網路的輸出。這有助於我們熟悉反向傳播中的符號。

定義符號:

這個符號代表第(l-1)層中第K個神經元和第l層中第j個神經元之間的權重。例如下圖:

定義符號:

代表第 l 層中第 j 個神經元的偏移值b。

代表第 l 層中第 j 個神經元的輸出啟用a(activation)。如下圖所示:

由以上所有符號得出:第l層第j個神經元的啟用等於:

上式括號中表示的是:第(l-1)層(前一層)中的所有k個神經元到第(l)層(後一層)第j個神經元的權重和第(l-1)層所有k個神經元的輸出乘積的和,加上第l層第j個神經元的偏移值(閥值)。相信看到這裡,這些符號的意義你都會很明確了。

我們向量化公式(23),用σ(v)表示。例如,方程式f(x)=x^2,向量化就是:

向量化的f會對每一個向量進行求平方操作。

所以公式(23)被向量化可以表示為下式:

這個表示式給我們提供了一個更為全域性的思考方法:一層中的啟用與前一層中的啟用如何相關:我們只是將權重矩陣應用於前一層啟用,然後新增偏移向量(b),最後應用到σ函式。 比我們現在採用的神經元 - 神經元更簡單,更簡潔(至少應用了更少的索引!)。 該表示式在實踐中也是有用的,因為大多數矩陣庫提供了實現矩陣乘法,向量加法和向量化的快速方法。 實際上,上一章中的程式碼隱含地使用這個表示式來計算網路的行為。

我們定義公式:

為第l層神經元的加權輸入。如果將其寫成分量樣式:

即代表第j層神經元的啟用函式對第l層的加權輸入。

關於損失函式的兩個假設


反向傳播的目標是通過計算損失函式C相對於網路中的權重w或偏差b的偏導數∂C/∂w和∂C/∂b來實現的。 為此,我們需要對損失函式的形式做兩個假設。 在闡述這些假設之前,我們參照一個損失函式示例。 我們將使用上一節中的二次損失函式(c.f. Equation(6))。 在上一節的符號中,二次損失函式的形式是:

其中:n是訓練樣例的總數; x是每一個訓練樣本;y(x)是期望輸出;L是網路層數,a^L(x)是當x輸入是網路的輸出。

第一個假設是:用損失函式的平均數表示在每一個訓練樣例上的損失。

我們做這個假設的原因是,我們求偏導數∂Cx/∂w 和 ∂Cx/∂b是針對每一個樣例進行的。

第二個假設是將神經網路的輸出表示成損失函式:

例如二次損失函式可以滿足這個要求,因為二次損失函式對於單一的輸入x可以寫成:

Hadamard 積,s⊙t


反向傳播要基於常見的線性代數運算,像矩陣的加、乘等等。但是有一種運算是不常見的。例如,s和t是兩個相同維度的向量,我們使用s⊙t表示兩個向量按元素的積(elementwise product),如:

可用如下形式表示:

這種按元素相乘的積叫做Hadamard乘積。

反向傳播背後重要的四個方程


反向傳播演算法的目的是通過改變(權重w和偏移b)來改善神經網路損失函式。最終是通過求偏導數(∂C/∂w)和(∂C/∂b)來衡量的。為此我們先定義一箇中間量:δ(l,j),代表 l 層第 j 個神經元的誤差。我們將會利用反向傳播來計算這個誤差。首先為了理解這個中間量,我們想象一下在神經網路中有一個小惡魔:

這個惡魔站在第l層的第j個神經元,當有輸入過來的時候,它改變了一點加權輸入(上面定義的z):

造成了神經元輸出的改變:

最終,造成了損失函式的改變:

現在假設這個惡魔是個好惡魔,試圖幫助你提高輸出的準確率。它嘗試幫你調節∆z,使得輸出誤差最小。由上面的想象,我們通過下式定義第l層第j個神經元的誤差:

作為慣例,把上式中的j去掉,表示第l層的誤差向量(δ^l)。反向傳播為我們提供了計算它的方法,並通過:∂C/∂w 和∂C/∂b將誤差和真實資料結合起來。

反向傳播基於四個基本方程。 這些方程一起給我們提供了一種計算誤差δ和損失函式梯度的方法。 不過要注意的是:你不應該期望一下子就理解這四個方程式。 這樣的期望可能會讓你失望。 因為反向傳播方程非常豐富,以至於深入瞭解它們需要相當長的時間和耐心。

這裡先提供一個預覽的方式,我們將會在後面章節給出更詳細的描述。這裡會給出一些簡單的證明,這有助於我們對他們的理解。

輸出層誤差的等式:

表示式右邊的第一部分表示:第j個神經元輸出改變時損失C改變的速度(斜率)。如果當神經元輸出改變已經不能太顯著地改變C時,那麼誤差就會很小,這正是我們期望的。右邊的第二個式子表示:測量啟用函式σ在加權輸入變化時的變化速率。

將BP1以向量的形勢寫出來就是:

等號右邊第一部分表示一個向量,你可以把它看作損失C對輸出偏導數的向量矩陣,很容易看出來(BP1a)和(BP1)是相等的。因為這個原因,從現在起我們將用BP1代替上式。我們參照在二次均方誤差方程中的例子來理解,其損失函式為:

可推匯出:

所以:

代入方程(BP1a)得出:

正如你看到的,表示式中的每一部分都是向量格式,所以就能很容易的用Numpy來計算。

用(l + 1)層的誤差表示第(l)層的誤差

你可以把它看做:誤差會隨著網路進行傳遞。

與偏差b相關的誤差變化:

通過BP1和BP2,我們能簡單表示BP3:

與權重相關的誤差變化:

簡潔格式:

a(in)可理解成權重w的輸入啟用, δ(out)可理解成權重w輸入時神經元的誤差。圖示:

等式中a的值很小接近0時,公式值就會很小,此時我們說這個權重下學習很慢,意味著它不能很快的改變梯度。

讓我們從輸出層的角度觀察這4個公式,看一下公式BP1,再回想一下上一節中的sigmoid函式的圖,當函式接近0或1的時候就會變的非常水平。這時:

如果輸出神經元是低啟用(≈0)或高啟用(≈1),權重調節就是緩慢學習的。這種情況下就說,輸出神經元飽和,權重停止學習,對於輸出神經元的偏差b,也有類似的說法。這種方法應用在BP2上也是類似的。

總結一下,我們已經知道,如果輸入神經元是低啟用的,或者如果輸出神經元飽和,即高或低啟用,則權重學習效率將會很低。

注意:以上四個公式我希望讀者可以自己證明出來,這能給你帶來更深入的理解。這可能會花很多時間,但是這是挺有必要的。

簡單證明一下上看的式子:

四個式子都是可以用多元微積分的鏈式法則求出來的。如果你熟悉鏈式法則,我希望你可以自己證明。

  • BP1
    我們知道誤差表示式為:

根據微積分鏈式法則:

上式為輸出層k個神經元的和,當然,輸出層第k個神經元的啟用輸出a僅僅依賴於輸出層第j個神經元的加權輸入z(j)。所以除了這一項,其他項都消失了,則:

而a=σ(z),所以:

  • BP2

這是表示相鄰兩層的誤差關係的式子,由式子(36)可得:

由鏈式法則可得:

然後已知的是:

對其求偏導數可得:

代入(42)可得:

這就是式子二的分量形勢。

關於式子三和四我希望讀者可以自己證明,根據鏈式法則這也是相對容易的。

我們從最後一層開始計算誤差向量,然後向後逐層計算誤差。

實際中,因為有很多訓練集,我們經常需要結合隨機梯度下降和反向傳播演算法來計算梯度。例如使用我們第一章提到的mini-batch資料時:

  • 輸入訓練資料
  • 依照下面的步驟,為每個訓練資料設定啟用a(x,1):

    • 為每一層 l=2,3,…,L計算a和z:
      • 反向傳播計算每一層誤差:
  • 梯度下降,對每一層更新w和b:

此外,你需要一個外部迴圈mini-batches遍歷整個訓練資料集。

###反向傳播程式碼

理解了反向傳播的抽象概念,我們來理解一下上節的程式碼。還記得在Network類中有update_mini_batch和backprop方法,這兩個方法是直接翻譯的上面的公式,update_mini_batch方法是通過計算當前訓練集(mini_batch)的梯度更新權重(w)和偏移值(b):

class Network(object):
...
    def update_mini_batch(self, mini_batch, eta):
        """Update the network's weights and biases by applying
        gradient descent using backpropagation to a single mini batch.
        The "mini_batch" is a list of tuples "(x, y)", and "eta"
        is the learning rate."""
        nabla_b = [np.zeros(b.shape) for b in self.biases]
        nabla_w = [np.zeros(w.shape) for w in self.weights]
        for x, y in mini_batch:
            delta_nabla_b, delta_nabla_w = self.backprop(x, y)
            nabla_b = [nb+dnb for nb, dnb in zip(nabla_b, delta_nabla_b)]
            nabla_w = [nw+dnw for nw, dnw in zip(nabla_w, delta_nabla_w)]
        self.weights = [w-(eta/len(mini_batch))*nw 
                        for w, nw in zip(self.weights, nabla_w)]
        self.biases = [b-(eta/len(mini_batch))*nb 
                       for b, nb in zip(self.biases, nabla_b)]複製程式碼

大多數的工作都被delta_nabla_b, delta_nabla_w = self.backprop(x, y)這一行執行。這一行通過反向傳播方法計算b和w相對於C的偏導數。這個方法如下:

class Network(object):
...
   def backprop(self, x, y):
        """Return a tuple "(nabla_b, nabla_w)" representing the
        gradient for the cost function C_x.  "nabla_b" and
        "nabla_w" are layer-by-layer lists of numpy arrays, similar
        to "self.biases" and "self.weights"."""
        nabla_b = [np.zeros(b.shape) for b in self.biases]
        nabla_w = [np.zeros(w.shape) for w in self.weights]
        # feedforward
        activation = x
        activations = [x] # list to store all the activations, layer by layer
        zs = [] # list to store all the z vectors, layer by layer
        for b, w in zip(self.biases, self.weights):
            z = np.dot(w, activation)+b
            zs.append(z)
            activation = sigmoid(z)
            activations.append(activation)
        # backward pass
        delta = self.cost_derivative(activations[-1], y) * \
            sigmoid_prime(zs[-1])
        nabla_b[-1] = delta
        nabla_w[-1] = np.dot(delta, activations[-2].transpose())
        # Note that the variable l in the loop below is used a little
        # differently to the notation in Chapter 2 of the book.  Here,
        # l = 1 means the last layer of neurons, l = 2 is the
        # second-last layer, and so on.  It's a renumbering of the
        # scheme in the book, used here to take advantage of the fact
        # that Python can use negative indices in lists.
        for l in xrange(2, self.num_layers):
            z = zs[-l]
            sp = sigmoid_prime(z)
            delta = np.dot(self.weights[-l+1].transpose(), delta) * sp
            nabla_b[-l] = delta
            nabla_w[-l] = np.dot(delta, activations[-l-1].transpose())
        return (nabla_b, nabla_w)

...

    def cost_derivative(self, output_activations, y):
        """Return the vector of partial derivatives \partial C_x /
        \partial a for the output activations."""
        return (output_activations-y) 

def sigmoid(z):
    """The sigmoid function."""
    return 1.0/(1.0+np.exp(-z))

def sigmoid_prime(z):
    """Derivative of the sigmoid function."""
    return sigmoid(z)*(1-sigmoid(z))複製程式碼

###反向傳播演算法是什麼意思?
讓我們想象一下網路中權重有一點小小的改變:

這個小小的改變會造成相應神經元啟用輸出的改變:

然後會造成下一層所有輸出啟用的改變:

最終,將會影響到損失函式:

通過公式可以表示成:

我們知道一層中啟用改變會造成下一層中輸出改變,這裡先把目光聚焦於下一層中的某一個的改變。

結合公式(48):

可以想象,當有很多層時(每一層假設都有受影響的輸出):

這代表這某個權重改變經過網路某一條路徑最終對誤差的影響,當然網路中有很多的路徑,我們需要計算所有的總和:

結合(47):

計算c相對於某個權重的變化速率,公式可看出每一層的啟用相對於下一層啟用的偏導數都是速率因子。體現在圖上:

這只是提供一種啟發式的思考,希望會對你理解反向傳播有幫助。

如果我的文章對你有幫助,我建議你可以關注我的文集或者打賞,我建議打賞¥5。當然你也可以隨意打賞。

微信
微信

支付寶
支付寶

如果有問題,歡迎跟我聯絡討論space-x@qq.com.我會盡量及時回覆。

相關文章