天天看點

手撸反向傳播算法(附代碼)

引言

根據上篇文章介紹的反向傳播算法理論,我們今天來手動計算一下。

我們簡化下上篇文章中的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()      

參考

  1. 深度學習入門之反向傳播算法
  2. ​​a-step-by-step-backpropagation-example​​
  3. ​​李宏毅 深度學習​​

繼續閱讀