引言
根據上篇文章介紹的反向傳播算法理論,我們今天來手動計算一下。
我們簡化下上篇文章中的NN模型(參數和它類似,這裡把每個偏內插補點設成1),使它的隐藏層隻有1層。它有兩個輸入和兩個輸出。
我們有初始化權重、偏差和訓練用的輸入和輸出值。
反向傳播算法的目的是優化權值來最小化損失函數,進而使NN預測的輸出和實際輸出更比對。
假設給定一個訓練集,它的輸入是,我們想要NN輸出和
正向過程(Forward Pass)
首先我們看看這個NN在初始權值和訓練資料下的表現。我們會計算出每個隐藏神經元的輸入,通過激活函數(這裡用
Sigmoid
函數)轉換成下一層的輸入,直到達到輸出層計算出最終輸出。
先來計算隐藏層的輸入,
然後将它代入激活函數(這裡用
Sigmoid
函數),得到隐藏層的輸出,
,
同理有
可得
這兩個輸出又可作為它們的下一層的輸入,計算輸出層的輸出如下兩個步驟:
輸出層的輸出也是同理:
可以看到,在初始參數上的輸出和我們的目标還是有不小距離的。
接下來,我們計算這不小距離到底有多麼不小。
計算總誤差(Total Error)
我們使用均方誤差來計算總誤差:
其中就是輸出層的實際輸出,而則是它的期望輸出。
比如神經元的期望輸出是,但是實際輸出是,是以誤差是:
使用同樣的過程計算出:
總誤差就是這些誤差之和:
這個距離是真的不小啊。
反向過程
反向傳播算法的目的是更高效的計算梯度,進而更新參數值,使得總誤差更小,也就是使實際輸出更貼近我們期望輸出。它是作為一個整體去更新整個神經網絡的。
反向就是先考慮輸出層,然後再考慮它的上一層,并重複這個過程。是以,我們先從輸出層開始計算。
輸出層
考慮參數,我們想知道的改變會對總誤差有多大的影響,即計算
。
我們需要計算這個等式中的每個式子,首先計算如何影響總誤差
接下來計算
我們知道
是以
最後是最簡單的
最後放到一起得:
通常一般定義
是以
為了減少誤差,通常需要減少目前權值,如下:
學習率一般取值,當然是需要根據實際情況去調整的。
同理可算得:
但還未結束,我們隻是更新了輸出層的參數,還要繼續往前,更新隐藏層的參數。
隐藏層
首先來更新:
我們要用和更新輸出層參數類似的步驟來更新隐藏層的參數,但有一點不同的是,每個隐藏層的神經元都影響了多個輸出層(或下一層)神經元的輸出。同時影響了和,是以計算需要将輸出層的兩個神經元都考慮在内:
從開始:
其實我們上面已經算過了:
并且:
也就是:
同理,可得
是以:
現在我們已經知道了,我們還需要計算和 :
最後,總式子就可以計算了:
接下來,就可以更新了:
同理算得:
終于,我們更新完一輪權值了!接下來用原始輸入值來測試更新權值後的總誤差是多少?經計算誤差是: 還記得未更新前的總誤差嗎?
但是在我們執行10000次更新權值的過程後:
>times=9999, lrate=0.500, error=0.000
[0.011851540581436764, 0.9878060737917571]
總誤差成了,輸出是和
和期望輸出以及比是不是十分接近了。
(上面同理計算可得的地方其實我都是用代碼算的,下面就貼出代碼)
反向傳播代碼
# -*- coding: utf-8 -*
from math import exp
# 計算z
def calculate_z(weights, inputs):
z = weights[-1] # 偏差b # z = x₁w₁ + x₂w₂ + ... + b
for i in range(len(weights) - 1):
z += weights[i] * inputs[i]
return z
# 激活函數Sigmoid: σ(z) = 1 / (1 + e^(-z))
def active(z):
return 1.0 / (1.0 + exp(-z))
# 正向傳播過程
def forward_pass(network, row):
inputs = row
for layer in network:
new_inputs = []
for neuron in layer:
z = calculate_z(neuron['weights'], inputs) # 計算z
neuron['output'] = active(z) # 代入激活函數得到輸出 并儲存到 output key中
new_inputs.append(neuron['output'])
inputs = new_inputs
return inputs
# sigmoid函數的導數 : 𝑑σ/𝑑z = σ(1-σ)
def active_derivative(output):
return output * (1.0 - output)
# 反向傳播過程
def back_pass(network, expected):
for i in reversed(range(len(network))):
layer = network[i] # 從輸出層開始
errors = []
if i != len(network) - 1: # 如果非輸出層
for j in range(len(layer)): # 周遊所有神經元
error = 0.0
# 計算 ∂Etotal/∂a = ∂Eo₁/∂ah₁ + ∂Eo₂/∂ah₁ ...
for neuron in network[i + 1]: # 下一層的神經元
error += (neuron['weights'][j] * neuron['delta']) # 反向傳播
errors.append(error)
else: # 如果是輸出層
for j in range(len(layer)):
neuron = layer[j] # 周遊輸出的神經元
errors.append(neuron['output'] - expected[j]) # -(target - output) 計算∂E/∂a
for j in range(len(layer)):
neuron = layer[j]
neuron['delta'] = errors[j] * active_derivative(neuron['output']) # δ = ∂E/∂zⱼ = ∂E/∂yⱼ * daⱼ/dzⱼ
# 更新參數
def update_weights(network, row, l_rate):
for i in range(len(network)):
inputs = row[:-1] # 去掉資料集中最後的類别标簽
if i != 0:
inputs = [neuron['output'] for neuron in network[i - 1]] # 如果不是輸入層,則更新該層的輸入為上一層的輸出
for neuron in network[i]:
for j in range(len(inputs)): # 入參的數量也就是權值參數的數量
neuron['weights'][j] -= l_rate * neuron['delta'] * inputs[j] # wⱼ = wⱼ - α * δ * xⱼ
neuron['weights'][-1] -= l_rate * neuron['delta'] * 1 # 同時更新了偏差b,如果更新了偏差,結果會更好
def total_error(outputs, expected):
sum_error = 0.0
for i in range(len(expected)):
sum_error += (expected[i] - outputs[i]) ** 2
return sum_error / 2.0
def test():
network = [[{'weights': [1, -2, 1]},
{'weights': [-1, 1, 1]}],
[{'weights': [2, -2, 1]},
{'weights': [-2, -1, 1]}]]
dataset = [[1, -1, None]]
n_inputs = len(dataset[0]) - 1
expected = [0.01, 0.99]
l_rate = 0.5
for times in range(10000):
sum_error = 0.0
for row in dataset:
outputs = forward_pass(network, row)
sum_error += total_error(outputs, expected)
back_pass(network, expected)
update_weights(network, row, l_rate)
print('>times=%d, lrate=%.3f, error=%.3f' % (times, l_rate, sum_error))
outputs = forward_pass(network, dataset[0])
print(outputs)
if __name__ == '__main__':
test()
參考
- 深度學習入門之反向傳播算法
- a-step-by-step-backpropagation-example
- 李宏毅 深度學習