反向傳播(BP)算法理解以及Python實現


全文參考《機器學習》-周志華中的5.3節-誤差逆傳播算法;整體思路一致,敘述方式有所不同;

使用如上圖所示的三層網絡來講述反向傳播算法;

首先需要明確一些概念,

假設數據集\(X=\{x^1, x^2, \cdots, x^n\}, Y=\{y^i, y^2, \cdots, y^n\}\),反向傳播算法使用數據集中的每一個樣本執行前向傳播,之后根據網絡的輸出與真實標簽計算誤差,利用誤差進行反向傳播,更新權重;

使用一個樣本\((x, y)\),其中\(x=(x_1, x_2, \cdots, x_d)\)

輸入層:

  有\(d\)個輸入結點,對應着樣本\(x\)\(d\)維特征,\(x_i\)表示輸入層的第\(i\)個結點;

隱藏層:

  有\(q\)個結點,\(b_h\)表示隱藏層的第\(h\)個結點;

輸出層:
  有\(l\)個輸出結點,\(y_j\)表示輸出層的第\(j\)個結點;

權重矩陣:

  兩個權重矩陣\(V, W\),分別是位於輸入層和隱藏層之間的\(V\in R^{d\times q}\),其中\(v_{ih}\)表示連接結點\(x_i\)與結點\(b_h\)之間的權重;以及位於隱藏層與輸出層之間的\(W\in R^{q\times l}\),其中\(w_{hj}\)表示連接結點\(b_h\)與結點\(y_j\)的權重;

激活函數:

  激活函數使用sigmoid函數;

\[f(x) = \dfrac{1}{1+e^{-x}} \]

其導數為:

\[f'(x) = f(x)(1-f(x)) \]

其他:

  在隱藏層,結點\(b_h\)在執行激活函數前為\(\alpha_h\),即隱藏層的輸入;所以有:

\[\alpha_h = \sum_{i=1}^{d}v_{ih}x_i \]

之后經過sigmoid函數:

\[b_h = sigmoid(\alpha_h) \]

  在輸出層,結點\(y_j\)在執行激活函數前為\(\beta_j\),即輸出層的輸入;所以有:

\[\beta_j = \sum_{h=1}^{q}w_{hj}b_h \]

之后經過sigmoid函數:

\[\hat{y}_j = sigmoid(\beta_j) \]

前向傳播

  所以,根據上面一系列的定義,前向傳播的過程為:由輸入層的結點\((x_1, x_2, \cdots, x_i, \cdots, x_d)\),利用權重矩陣\(V\)計算得到\((\alpha_1, \alpha_2, \cdots, \alpha_h, \cdots, \alpha_q)\),經過激活函數sigmoid得到\((b_1, b_2, \cdots, b_h, \cdots, b_q)\),這就得到了隱藏層的輸出;之后,利用權重矩陣\(W\)計算得到\((\beta_1, \beta_2, \cdots, \beta_j, \cdots, \beta_l)\),經過激活函數sigmoid得到\((\hat{y}_1,\hat{y}_1, \cdots, \hat{y}_j , \cdots, \hat{y}_l )\),也就是最后的輸出;

步驟:

**Step 1: **輸入層\(x \in R^{1\times d}\),計算隱藏層輸出\(b = sigmoid(x\times V), \quad b\in R^{1\times q}\)

**Step 2: ** 輸出層輸出\(\hat{y} = sigmoid(b \times W), \quad \hat{y}\in R^{1\times l}\)

  注意,在前向傳播的過程中,記錄每一層的輸出,為反向傳播做准備,因此,需要保存的是\(x, b, \hat{y}\)

前向傳播還是比較簡單的,下面來看反向傳播吧;

反向傳播

  想一下為什么要有反向傳播過程呢?其實目的就是為了更新我們網絡中的參數,也就是上面我們所說的兩個權重矩陣\(V, W\),那么如何來更新呢?

《機器學習》周志華

BP是一個迭代算法,在迭代的每一輪中采用廣義的感知機學習規則對參數進行更新估計,任意參數v的更新估計式為:

\[v \leftarrow v + \Delta v \]

BP算法基於梯度下降策略,以目標的負梯度方向對參數進行調整;

我們如何來更新參數呢?也就是如何更新\(V, W\)這兩個權重矩陣;以\(W\)中的某個參數\(w_{hj}\)舉例,更新它的方式如下:

\[w_{hj} \leftarrow w_{hj} + \Delta w_{hj} \]

那么,如何計算\(\Delta w_{hj}\)的呢?計算如下:

\[\Delta w_{hj} = -\eta \dfrac{\partial{E}}{\partial{w_{hj}}} \]

其中,\(E_k\)表示誤差,也就是網絡的輸出\(\hat{y}\)與真實標簽\(y\)的均方誤差;\(\eta\)表示學習率;負號則表示沿着負梯度方向更新;

\[E = \dfrac{1}{2}\sum_{j=1}^{l}(\hat{y}_j - y_j)^2 \]

也就是說,我們想要對哪一個參數進行更新,則需要計算當前網絡輸出與真實標簽的均方誤差對該參數的偏導數,即\(\dfrac{\partial{E}}{\partial{w_{hj}}}\),之后再利用學習率進行更新;

在這個三層的網絡結構中,有兩個權重矩陣\(V, W\),我們該如何更新其中的每一個參數呢?

就以權重矩陣\(W\)中的參數\(w_{hj}\)來進行下面的解釋,

那么根據上面所敘述的,更新\(w_{hj}\)得方式為:

\[w_{hj} \leftarrow w_{hj} + \Delta w_{hj} \]

\[\Delta w_{hj} = -\eta \dfrac{\partial{E}}{\partial{w_{hj}}} \]

那么如何來計算\(\dfrac{\partial{E}}{\partial{w_{hj}}}\)呢?

這里就需要用到鏈式法則了,如果不熟悉的,建議查找再學習一下;

\[f(x) = g(x)+x \]

\[g(x) = h(x) + x^2 \]

\[h(x) = x^3 + 1 \]

想一下是怎么求\(\dfrac{df(x)}{dx}\)的;

如果對上文中講述的網絡結構,能夠將其完整的呈現的在腦海中的話,對於下面的推導應該不會很困難。

再回顧一遍前向傳播:

所以,根據上面一系列的定義,前向傳播的過程為:由輸入層的結點\((x_1, x_2, \cdots, x_i, \cdots, x_d)\),利用權重矩陣\(V\)計算得到\((\alpha_1, \alpha_2, \cdots, \alpha_h, \cdots, \alpha_q)\),經過激活函數sigmoid得到\((b_1, b_2, \cdots, b_h, \cdots, b_q)\),這就得到了隱藏層的輸出;之后,利用權重矩陣\(W\)計算得到\((\beta_1, \beta_2, \cdots, \beta_j, \cdots, \beta_l)\),經過激活函數sigmoid得到\((\hat{y}_1,\hat{y}_1, \cdots, \hat{y}_j , \cdots, \hat{y}_l )\),也就是最后的輸出;



那么如何來計算\(\dfrac{\partial{E}}{\partial{w_{hj}}}\)呢?

我們想一下在網絡的均方誤差\(E\)與參數\(w_{hj}\)之間有哪些過程,也就是說需要想明白參數\(w_{hj}\)是怎么對誤差\(E\)產生影響的;

\(w_hj\)是連接隱藏層結點\(b_h\)與輸出層結點\(\hat{y}_j\)的權重,因此過程是:\(b_h \rightarrow \beta_j \rightarrow \hat{y}_j \rightarrow E\)

那么根據鏈式法則就可以有:

\[\dfrac{\partial E}{\partial w_{hj}} = \dfrac{\partial E}{\partial \hat{y}_j}\dfrac{\partial \hat{y}_j}{\partial \beta_j} \dfrac{\partial \beta_j}{\partial w_{hj}} \]

分別來求解\(\dfrac{\partial E}{\partial \hat{y}_j}\), \(\dfrac{\partial \hat{y}_j}{\partial \beta_j}\), $ \dfrac{\partial \beta_j}{\partial w_{hj}}$這三項;

(1)第一項:\(\dfrac{\partial E}{\partial \hat{y}_j}\)

想一下\(E\)\(\hat{y}_j\)之間有什么關系,即:

\[E = \dfrac{1}{2}\sum_{j=1}^{l}(\hat{y}_j-y_j)^2 = \dfrac{1}{2}[(\hat{y}_1-y_1)^2+\cdots +(\hat{y}_j-y_j)^2+\cdots+(\hat{y}_l-y_l)^2] \]

那么,\(E_k\)\(\hat{y}_j\)求偏導:

\[\dfrac{\partial E}{\partial \hat{y}_j} = -(\hat{y}_j-y_j) \]

(2)第二項:\(\dfrac{\partial \hat{y}_j}{\partial \beta_j}\)

再想一下\(\hat{y}_j\)\(\beta_j\)之間有什么關系呢,即

\[\hat{y}_j = sigmoid(\beta_j) \]

那么,\(\hat{y}_j\)\(\beta_j\)求偏導,即:

\[\dfrac{\partial \hat{y}_j}{\partial \beta_j} = \hat{y}_j(1-\hat{y}_j) \]

(3)第三項:$ \dfrac{\partial \beta_j}{\partial w_{hj}}$

再想一下\(\beta_j\)\(w_{hj}\)之間又有什么關系呢,即:

\[\beta_j = \sum_{h=1}^{q}w_{hj}b_h= w_{1j}b_1 + \cdots+w_{hj}b_h+\cdots+w_{qj}b_q \]

所以從上式中能夠看清\(\beta_j\)\(w_{hj}\)之間的關系了吧,其實再想一下,\(\beta_j\)是輸出層的第\(j\)個結點,而\(w_{hj}\)是連接隱藏層結點\(b_h\)與結點\(\beta_j\)的權重;

那么\(\beta_j\)\(w_{hj}\)的偏導數,即:

\[\dfrac{\partial \beta_j}{\partial w_{hj}} = b_h \]

上面三個偏導數都求出來了,那么就有:

\[\dfrac{\partial E}{\partial w_{hj}} = \dfrac{\partial E}{\partial \hat{y}_j}\dfrac{\partial \hat{y}_j}{\partial \beta_j} \dfrac{\partial \beta_j}{\partial w_{hj}} =-(\hat{y}_j-y_j)\hat{y}_j(1-\hat{y}_j)b_h \]

那么更新參數\(w_{hj}\)

\[w_{hj} \leftarrow w_{hj}+\Delta w_{hj} \]

\[\Delta w_{hj} = -\eta \dfrac{\partial E}{\partial w_{hj}} = -\eta(-(\hat{y}_j-y_j)\hat{y}_j(1-\hat{y}_j)b_h) \]

即:

\[w_{hj} = w_{hj}+\Delta w_{hj} = w_{hj} -\eta(-(\hat{y}_j-y_j)\hat{y}_j(1-\hat{y}_j)b_h) \]

從上式可以看出,想要對參數\(w_{hj}\)進行更新,我們需要知道上一次更新后的參數值,輸出層的第\(j\)個結點\(\hat{y}_j\),以及隱藏層的第\(h\)個結點\(b_h\)其實想一下,也就是需要知道參數\(w_{hj}\)連接的兩個結點對應的輸出;那么這里就提醒我們一點,在網絡前向傳播的時候需要記錄每一層網絡的輸出,即經過sigmoid函數之后的結果;



現在我們知道如何對權重矩陣\(W\)中的每一個參數\(w_{hj}\)進行更新,那么如何對權重矩陣\(V\)中的參數\(v_{ih}\)進行更新呢?其中,\(v_{ih}\)是連接輸入層結點\(x_i\)與隱藏層結點\(b_h\)之間的權重;

同樣是利用網絡的輸出誤差\(E_k\)對參數\(v_{ih}\)的偏導,即:

\[v_{ih} \leftarrow v_{ih} + \Delta v_{ih} \]

\[\Delta v_{ih} = -\eta \dfrac{\partial{E}}{\partial{v_{ih}}} \]

那么如何來計算\(\dfrac{\partial{E}}{\partial{v_{ih}}}\)呢?想一下是\(E\)\(v_{ih}\)之間有什么關系,過程為:

\[v_{ih} \rightarrow \alpha_h \rightarrow b_h \rightarrow \beta \rightarrow \hat{y} \rightarrow E \]

同樣是利用鏈式求導法則,有:

\[\dfrac{\partial{E}}{\partial{v_{ih}}} = \dfrac{\partial E}{\partial b_h}\dfrac{\partial b_h}{\partial \alpha_h}\dfrac{\partial \alpha_h}{\partial v_{ih}} \]

同樣地,分別來求解\(\dfrac{\partial E}{\partial b_h}\),\(\dfrac{\partial b_h}{\partial \alpha_h}\),\(\dfrac{\partial \alpha_h}{\partial v_{ih}}\)這三項;

(1)第一項:\(\dfrac{\partial E}{\partial b_h}\)

與上述思路相同,想一下\(E_k\)\(b_h\)之間的關系,又可以分解為:

\[\dfrac{\partial E}{\partial b_h} =\sum_{j=1}^{l}\dfrac{\partial E}{\partial \beta_j} \dfrac{\partial \beta_j}{\partial b_h} \]

其中,

\[\dfrac{\partial E}{\partial \beta_j} = \dfrac{\partial E}{\partial \hat{y}_j}\dfrac{\partial \hat{y}_j}{\partial \beta_j} = -(\hat{y}_j-y_j)\hat{y}_j(1-\hat{y}_j) \]

另外,\(\dfrac{\partial \beta_j}{\partial b_h}\),想一下\(\beta_j\)\(b_h\)的關系:

\[\dfrac{\partial \beta_j}{\partial b_h} = w_{hj} \]

所以,就有:

\[\dfrac{\partial E}{\partial b_h} =\sum_{j=1}^{l}\dfrac{\partial E}{\partial \beta_j} \dfrac{\partial \beta_j}{\partial b_h} = \sum_{j=1}^{l}-(\hat{y}_j-y_j)\hat{y}_j(1-\hat{y}_j) w_{hj} \]

(2)第二項:\(\dfrac{\partial b_h}{\partial \alpha_h}\)

同樣地,\(b_h\)\(\alpha_h\)之間的關系,有:

\[b_h = sigmoid(\alpha_h) \]

那么有:

\[\dfrac{\partial b_h}{\partial \alpha_h} = b_h (1-b_h) \]

(3)第三項:\(\dfrac{\partial \alpha_h}{\partial v_{ih}}\)

同樣地,\(\alpha_h\)\(v_{ih}\)之間的關系,有:

\[\alpha_h = \sum_{i=1}^{d}v_{ih}x_i= v_{1h}x_1 + \cdots+v_{ih}x_i+\cdots+v_{dh}x_d \]

因此,\(\alpha_h\)\(v_{ih}\)的偏導數為:

\[\dfrac{\partial \alpha_h}{\partial v_{ih}} = x_i \]

綜合上面三項,有:

\[\dfrac{\partial{E}}{\partial{v_{ih}}} = \dfrac{\partial E}{\partial b_h}\dfrac{\partial b_h}{\partial \alpha_h}\dfrac{\partial \alpha_h}{\partial v_{ih}} = \sum_{j=1}^{l}-(\hat{y}_j-y_j)\hat{y}_j(1-\hat{y}_j) w_{hj} b_h (1-b_h) x_i \]



我們來對比一下\(\dfrac{\partial{E}}{\partial{v_{ih}}}\)\(\dfrac{\partial E}{\partial w_{hj}}\),兩者分別為:

\[\dfrac{\partial E}{\partial w_{hj}} =-(\hat{y}_j-y_j)\hat{y}_j(1-\hat{y}_j)b_h \]

\[\dfrac{\partial{E}}{\partial{v_{ih}}} = \sum_{j=1}^{l}-(\hat{y}_j-y_j)\hat{y}_j(1-\hat{y}_j) w_{hj} b_h (1-b_h) x_i \]

稍微換一種形式,將負號放進去:

\[\dfrac{\partial E}{\partial w_{hj}} =(y_j- \hat{y}_j)\hat{y}_j(1-\hat{y}_j)b_h \]

\[\dfrac{\partial{E}}{\partial{v_{ih}}} = \sum_{j=1}^{l}(y_j - \hat{y}_j)\hat{y}_j(1-\hat{y}_j) w_{hj} b_h (1-b_h) x_i \]

這里我們是對單個參數\(w_{hj}, v_{ih}\)進行更新,如何對\(W, V\)整體進行更新呢?

我們再明確一下幾個定義:
\(x\)表示輸入層的輸出, \(x\in R^{1\times d }\)

\(b\)表示隱藏層的輸出,\(b\in R^{1\times q }\)

\(\hat{y}\)表示輸出層的輸出,\(\hat{y}\in R^{1\times l}\)

\(sigmoid\_deriv()\)表示\(sigmoid\)的導數,\(sigmoid\_deriv(\hat{y}) = \hat{y}(1-\hat{y})\)

將輸出層的輸出與ground-truth之間的差值記為:\(eroor = y-\hat{y}\)

可以得到

\[\dfrac{\partial E}{\partial W} = b' \cdot error \cdot sigmoid\_deriv(\hat{y}) \]

\[\dfrac{\partial E}{\partial V}= x' \cdot error \cdot sigmoid\_deriv(\hat{y}) \cdot W' \cdot sigmoid\_deriv(b) \]

在反向傳播的過程中,我們記:

\[D[0] =error \cdot sigmoid\_deriv(\hat{y}) \]

\[D[1]= error \cdot sigmoid\_deriv(\hat{y}) \cdot W' \cdot sigmoid\_deriv(b) \]

當將每一個權重矩陣的\(D[?]\)計算出來,得到一個列表后,再對所有的權重矩陣進行更新;之所以這樣做,是為方便代碼實現;

Python實現前向傳播與反向傳播

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time : 19-5-7

"""
get started implementing backpropagation.
"""

__author__ = 'Zhen Chen'

# import the necessaty packages
import numpy as np


class NeuralNetwork:
    def __init__(self, layers, alpha=0.1):
        # 初始化權重矩陣、層數、學習率
        # 例如:layers=[2, 3, 2],表示輸入層兩個結點,隱藏層3個結點,輸出層2個結點
        self.W = []
        self.layers = layers
        self.alpha = alpha

		# 隨機初始化權重矩陣,如果三層網絡,則有兩個權重矩陣;
        # 在初始化的時候,對每一層的結點數加1,用於初始化訓練偏置的權重;
        # 由於輸出層不需要增加結點,因此最后一個權重矩陣需要單獨初始化;
        for i in np.arange(0, len(layers)-2):
            w = np.random.randn(layers[i] + 1, layers[i + 1] + 1)
            self.W.append(w / np.sqrt(layers[i]))

        # 初始化最后一個權重矩陣
        w = np.random.randn(layers[-2] + 1, layers[-1])
        self.W.append(w / np.sqrt(layers[-2]))

    def __repr__(self):
        # 輸出網絡結構
        return "NeuralNetwork: {}".format(
            "-".join(str(l) for l in self.layers)
        )

    def sigmoid(self, x):
        # sigmoid激活函數
        return 1.0 / (1 + np.exp(-x))

    def sigmoid_deriv(self, x):
        # sigmoid的導數
        return x * (1 - x)

    def fit(self, X, y, epochs=1000, display=100):
        # 訓練網絡
        # 對訓練數據添加一維值為1的特征,用於同時訓練偏置的權重
        X = np.c_[X, np.ones(X.shape[0])]

        # 迭代的epoch
        for epoch in np.arange(0, epochs):
            # 對數據集中每一個樣本執行前向傳播、反向傳播、更新權重
            for (x, target) in zip(X, y):
                self.fit_partial(x, target)

            # 打印輸出
            if epoch == 0 or (epoch + 1) % display == 0:
                loss = self.calculate_loss(X, y)
                print("[INFO] epoch={}, loss={:.7f}".format(
                    epoch + 1, loss
                ))

    def fit_partial(self, x, y):
        # 構造一個列表A,用於保存網絡的每一層的輸出,即經過激活函數的輸出
        A = [np.atleast_2d(x)]

        # ---------- 前向傳播 ----------
        # 對網絡的每一層進行循環
        for layer in np.arange(0, len(self.W)):
            # 計算當前層的輸出
            net = A[layer].dot(self.W[layer])
            out = self.sigmoid(net)

            # 添加到列表A
            A.append(out)

        # ---------- 反向傳播 ----------
        # 計算error
        error = A[-1] - y

        # 計算最后一個權重矩陣的D[?]
        D = [error * self.sigmoid_deriv(A[-1])]

        # 計算前面的權重矩陣的D[?]
        for layer in np.arange(len(A)-2, 0, -1):
            # 參見上文推導的公式
            delta = D[-1].dot(self.W[layer].T)
            delta = delta * self.sigmoid_deriv(A[layer])
            D.append(delta)

        # 列表D是從后往前記錄,下面更新權重矩陣的時候,是從輸入層到輸出層
        # 因此,在這里逆序
        D = D[::-1]

        # 迭代更新權重
        for layer in np.arange(0, len(self.W)):
            # 參考上文公式
            self.W[layer] += -self.alpha * A[layer].T.dot(D[layer])

    def predict(self, X, addBias=True):
        # 預測
        p = np.atleast_2d(X)

        # check to see if the bias column should be added
        if addBias:
            # insert a column of 1's as the last entry in the feature
            # matrix (bias)
            p = np.c_[p, np.ones((p.shape[0]))]

        # loop over our layers int the network
        for layer in np.arange(0, len(self.W)):
            # computing the output prediction is as simple as taking
            # the dot product between the current activation value 'p'
            # and the weight matrix associated wieth the current layer,
            # then passing this value through a nonlinear activation
            # function
            p = self.sigmoid(np.dot(p, self.W[layer]))

        # return the predicted value
        return p

    def calculate_loss(self, X, targets):
        # make predictions for the input data points then compute
        # the loss
        targets = np.atleast_2d(targets)
        predictions = self.predict(X, addBias=False)
        loss = 0.5 * np.sum((predictions - targets) ** 2)

        # return the loss
        return loss


nn = NeuralNetwork([2, 2, 1])
print(nn.__repr__())


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM