全文參考《機器學習》-周志華中的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函數;
其導數為:
其他:
在隱藏層,結點\(b_h\)在執行激活函數前為\(\alpha_h\),即隱藏層的輸入;所以有:
之后經過sigmoid函數:
在輸出層,結點\(y_j\)在執行激活函數前為\(\beta_j\),即輸出層的輸入;所以有:
之后經過sigmoid函數:
前向傳播
所以,根據上面一系列的定義,前向傳播的過程為:由輸入層的結點\((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的更新估計式為:
BP算法基於梯度下降策略,以目標的負梯度方向對參數進行調整;
我們如何來更新參數呢?也就是如何更新\(V, W\)這兩個權重矩陣;以\(W\)中的某個參數\(w_{hj}\)舉例,更新它的方式如下:
那么,如何計算\(\Delta w_{hj}\)的呢?計算如下:
其中,\(E_k\)表示誤差,也就是網絡的輸出\(\hat{y}\)與真實標簽\(y\)的均方誤差;\(\eta\)表示學習率;負號則表示沿着負梯度方向更新;
也就是說,我們想要對哪一個參數進行更新,則需要計算當前網絡輸出與真實標簽的均方誤差對該參數的偏導數,即\(\dfrac{\partial{E}}{\partial{w_{hj}}}\),之后再利用學習率進行更新;
在這個三層的網絡結構中,有兩個權重矩陣\(V, W\),我們該如何更新其中的每一個參數呢?
就以權重矩陣\(W\)中的參數\(w_{hj}\)來進行下面的解釋,
那么根據上面所敘述的,更新\(w_{hj}\)得方式為:
那么如何來計算\(\dfrac{\partial{E}}{\partial{w_{hj}}}\)呢?
這里就需要用到鏈式法則了,如果不熟悉的,建議查找再學習一下;
想一下是怎么求\(\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 \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_k\)對\(\hat{y}_j\)求偏導:
(2)第二項:\(\dfrac{\partial \hat{y}_j}{\partial \beta_j}\)
再想一下\(\hat{y}_j\)與\(\beta_j\)之間有什么關系呢,即
那么,\(\hat{y}_j\)對\(\beta_j\)求偏導,即:
(3)第三項:$ \dfrac{\partial \beta_j}{\partial w_{hj}}$
再想一下\(\beta_j\)與\(w_{hj}\)之間又有什么關系呢,即:
所以從上式中能夠看清\(\beta_j\)與\(w_{hj}\)之間的關系了吧,其實再想一下,\(\beta_j\)是輸出層的第\(j\)個結點,而\(w_{hj}\)是連接隱藏層結點\(b_h\)與結點\(\beta_j\)的權重;
那么\(\beta_j\)對\(w_{hj}\)的偏導數,即:
上面三個偏導數都求出來了,那么就有:
那么更新參數\(w_{hj}\)
即:
從上式可以看出,想要對參數\(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}\)的偏導,即:
那么如何來計算\(\dfrac{\partial{E}}{\partial{v_{ih}}}\)呢?想一下是\(E\)與\(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 \beta_j}{\partial b_h}\),想一下\(\beta_j\)與\(b_h\)的關系:
所以,就有:
(2)第二項:\(\dfrac{\partial b_h}{\partial \alpha_h}\)
同樣地,\(b_h\)與\(\alpha_h\)之間的關系,有:
那么有:
(3)第三項:\(\dfrac{\partial \alpha_h}{\partial v_{ih}}\)
同樣地,\(\alpha_h\)與\(v_{ih}\)之間的關系,有:
因此,\(\alpha_h\)對\(v_{ih}\)的偏導數為:
綜合上面三項,有:
我們來對比一下\(\dfrac{\partial{E}}{\partial{v_{ih}}}\)與\(\dfrac{\partial E}{\partial w_{hj}}\),兩者分別為:
稍微換一種形式,將負號放進去:
這里我們是對單個參數\(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}\)
可以得到
在反向傳播的過程中,我們記:
當將每一個權重矩陣的\(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__())