1. 程式人生 > 其它 >用Python從零開始構建反向傳播演算法

用Python從零開始構建反向傳播演算法

反向傳播演算法是經典的前饋人工神經網路。

這項技術也被用來訓練大型的深度學習網路。

在本教程中,你將探索如何使用Python從零開始構建反向傳播演算法。

完成本教程後,你將知道:

  • 如何正向傳播輸入以計算輸出。
  • 如何反向傳播誤差並訓練網路。
  • 如何將反向傳播演算法應用於現實世界的預測建模問題。

讓我們開始吧。

照片來源:NICHD.https://www.flickr.com/photos/nichd/21086425615/,保留部分權利

描述

本節簡要介紹反向傳播演算法和本教程中將使用的小麥種子資料集。

反向傳播演算法

在人工神經網路領域,反向傳播演算法是多層前饋網路中的監督學習方法。

前饋神經網路接受一個或多個神經元處理後的資訊作為輸入激勵。神經元通過它的樹突來接受輸入訊號,樹突將電訊號傳遞給細胞體。軸突將訊號傳遞給突觸(突觸是細胞軸突與其他細胞樹突連線的部位)。

反向傳播演算法的原理是通過修改輸入訊號的內部權重產生預期的輸出訊號來擬合給定的函式。系統使用監督學習的方法進行訓練,系統的輸出和已知的預期輸出之間的誤差將被提供給系統來修改其內部狀態。

從技術上來講,反向傳播演算法是多層前饋神經網路訓練權重的方法。因此,需要定義一個單層或多層的網路結構,其中的每一層與下一層完全連線。標準的網路結構由一個輸入層,一個隱藏層和一個輸出層構成。

反向傳播即可以用於分類問題中,也可以用在迴歸問題當中,在本教程中我們關注其在分類問題上的應用。

在分類問題當中,最理想的結果是輸出層中的每一個神經元對應著一個類別的值。舉例來說,假設有一個二分類問題,兩個類別為A和B。此時預期的類別輸出必定可以轉化為一列數值,每一行的值代表著其屬於該類的概率,比如說A => 1, 0, B => 0, 1,這種編碼方式也被稱作One-hot編碼。

小麥種子資料集

根據小麥種子資料集中不同種類小麥種子樣本的觀測值,其可以用於預測小麥種子的所屬品種。

資料集中有201條樣本記錄和7個數值輸入變數。這個分類問題有三個可能的輸出類別。每個輸入變數的變化範圍是不同的,所以可能需要進行歸一化處理,尤其像反向傳播演算法一樣需要賦予輸入值權重的演算法。

下面給出資料集中前五行的樣本。

15.26,14.84,0.871,5.763,3.312,2.221,5.22,1

14.88,14.57,0.8811,5.554,3.333,1.018,4.956,1

14.29,14.09,0.905,5.291,3.337,2.699,4.825,1

13.84,13.94,0.8955,5.324,3.379,2.259,4.805,1

16.14,14.99,0.9034,5.658,3.562,1.355,5.175,1

使用零規則演算法,預測結果為資料集中最常見的樣本類別,得到基線預測正確率為28.095%。

你可以從UCI機器學習資料庫中下載資料集,瞭解與其相關的更多資訊。

將種子資料集下載到當前的工作目錄後重命名為seeds_dataset.csv。下載的資料集使用製表符作為分割符,所以你必須使用文字編輯器或者電子表格程式將其轉換為CSV。

教程

本教程分為6個部分:

  1. 初始化網路。
  2. 前向傳播。
  3. 誤差反向傳播。
  4. 訓練網路。
  5. 預測。
  6. 種子資料集案例研究。

這些步驟將為你從頭開始實施反向傳播演算法並將其應用於你自己的預測建模問題提供所需基礎。

1.初始化網路

讓我們從簡單的事情開始,首先建立一個新的網路以供訓練。

每個神經元都有一組需要被維護的權重值。神經元間每個連線都需要一個權重值,除此之外每個神經元還有一個額外的偏置值需要維護。在訓練過程中,我們需要儲存神經元的這些附帶屬性,因此我們使用字典來表示每個神經元,並用weight作為鍵名來儲存權重值。

網路會以層級的形式來組織。輸入層實際上就是來自訓練資料集中的一行資料輸入,因此真正的第一層輸入位於隱藏層,隱藏層之後是輸出層,其中每一個類別的分值輸出都對應著輸出層中的一個神經元。

我們將這些層以字典陣列的方式組織起來,即將整個網路當作不同層元素構成的陣列來看待。

網路的權重值初始化為小的隨機數為宜,考慮這個因素,我們使用0-1範圍內的隨機數做權重的初始化。

下面的initialize_network()函式可以新建用於訓練的神經網路。它接受三個引數值:輸入層神經元數,隱藏層神經元數和輸出層神經元數。

從程式碼中可以看出我們將建立n_hidden個隱藏層神經元,每個隱藏層神經元有n_input + 1個權重值需要維護,對應著n_input個輸入神經元的連線權重值和一個偏置值。

同樣地,你也可以看到與隱藏層連線的輸出層有n_output個神經元,每個神經元有n_hidden + 1個權重值需要維護,這意味著輸出層中任一個神經元都維護著隱含層每一個神經元的連線(權重值)。

# 初始化網路
def initialize_network(n_inputs, n_hidden, n_outputs):
    network = list()
    hidden_layer = [{'weights':[random() for i in range(n_inputs + 1)]} for i in range(n_hidden)]
    network.append(hidden_layer)
    output_layer = [{'weights':[random() for i in range(n_hidden + 1)]} for i in range(n_outputs)]
    network.append(output_layer)
    return network

讓我們來測試一下這個函式。下面是建立一個小型網路的完整樣例。

from random import seed
from random import random
# 初始化網路
def initialize_network(n_inputs, n_hidden, n_outputs):
    network = list()
    hidden_layer = [{'weights':[random() for i in range(n_inputs + 1)]} for i in range(n_hidden)]
    network.append(hidden_layer)
    output_layer = [{'weights':[random() for i in range(n_hidden + 1)]} for i in range(n_outputs)]
    network.append(output_layer)
    return network

seed(1)
network = initialize_network(2, 1, 2)
for layer in network:
    print(layer)

執行這個樣例,可以看到程式碼會依次列印各層的資訊。可以看出隱藏層有一個神經元,神經元有兩個輸入權重和一個偏置值。輸出層有兩個神經元,每個神經元有一個權重值和一個偏置值。

[{'weights': [0.13436424411240122, 0.8474337369372327, 0.763774618976614]}]
[{'weights': [0.2550690257394217, 0.49543508709194095]}, {'weights': [0.4494910647887381, 0.651592972722763]}]

現在我們知道了如何建立和初始化一個網路。下面讓我們看看如何使用它來計算輸出。

2.前向傳播

我們讓輸入訊號依次通過每一層直至輸出層,最後輸出的值即我們要計算的輸出值。

這個過程我們稱為前向傳播。

這是我們在訓練過程中網路預測的方式,其輸出還需要進一步的糾正,訓練完成的網路對新資料的預測也是利用前向傳播實現的。

我們可以把傳播過程分解成三部分:

  1. 神經元的啟用過程。
  2. 神經元的傳遞過程。
  3. 前向傳播。

2.1. 神經元的啟用過程

第一步是計算在給定輸入情況下神經元的啟用值(或活化值,表徵神經元的啟用程度)。

對於隱藏層來說,輸入可以是訓練資料集中的一行。對於輸出層來說,輸入是隱藏層中每個神經元的輸出。

神經元的啟用值可以通過計算輸入的加權和得到,與線性迴歸十分相似。

activation = sum(weight_i * input_i) + bias

weight代表網路權重,input代表輸入,i為權重或輸入的編號,bias是一個特殊的權重(偏置),它不需要與任何輸入相乘(你也可以理解為與它相乘的輸入值恆為1.0)。

下面是 activate() 函式的實現,從中可以看出這個函式假定權重列表中的最後一個權重值為偏置。這個假設可以提升這裡和後面程式碼的可讀性。

# 計算給定輸入情況下神經元的啟用值
def activate(weights, inputs):
    activation = weights[-1]
    for i in range(len(weights)-1):
        activation += weights[i] * inputs[i]
    return activation

現在,讓我們看看如何使用神經元的啟用值。

2.2. 神經元的傳遞過程

每當神經元被啟用,我們就需要轉換它的啟用值來看一下神經元的輸出到底是什麼。

我們可以使用不同的傳遞函式。傳統的方法是使用sigmoid啟用函式,但是你也可以使用tanh(雙曲正切)函式來作為傳輸函式。在最近的大型深度學習網路中,整流傳遞函式(Relu函式等)的使用更為普遍。

Sigmoid函式曲線呈S形,也被稱為邏輯函式。它可以接受任意的輸入值並在S曲線上對映產生0-1之間的輸出值。選擇這個函式也為我們後續計算反向傳播誤差所需導數(斜率)提供了便利。

我們可以使用sigmoid函式來傳遞啟用值,數學形式如下:

output = 1 / (1 + e^(-activation))

其中e是自然對數的底數(尤拉數)。

下面是 transfer() 函式,它實現了sigmoid函式。

# 傳遞神經元的啟用值
def transfer(activation):
    return 1.0 / (1.0 + exp(-activation))

現在我們有了這些東西,下面讓我們看看如何使用他們。

2.3. 前向傳播

前向傳播直接接受輸入值。

通過計算當前層中所有神經元的輸出,訊號可以依次通過網路中的每一層。每一層的輸出將稱為下一層的輸入。

下面是 forward_propagate() 函式的實現,它實現了從單行輸入資料在神經網路中的前向傳播。

從程式碼中可以看到神經元的輸出被儲存在neuronoutput屬性下,我們使用 new_input 陣列來儲存當前層的輸出值,它將作為下一層的 input (輸入)繼續向前傳播。

該函式在最後一層計算完成後輸出返回值,最後一層也被稱作輸出層。

# 從網路輸入到網路輸出的前向傳播過程
def forward_propagate(network, row):
    inputs = row
    for layer in network:
        new_inputs = []
        for neuron in layer:
            activation = activate(neuron['weights'], inputs)
            neuron['output'] = transfer(activation)
            new_inputs.append(neuron['output'])
        inputs = new_inputs
    return inputs

讓我們把上面所有的程式碼片段放在一起並測試一下本節我們完成的前向傳播函式。

我們用內聯定義的方式定義我們的測試網路,假定網路的預期輸入值為兩個,輸出層有兩個神經元。

from math import exp
def activate(weights, inputs):
    activation = weights[-1]
    for i in range(len(weights)-1):
        activation += weights[i] * inputs[i]
    return activation

def transfer(activation):
    return 1.0 / (1.0 + exp(-activation))

# 從網路輸入到網路輸出的前向傳播
def forward_propagate(network, row):
    inputs = row
    for layer in network:
        new_inputs = []
        for neuron in layer:
            activation = activate(neuron['weights'], inputs)
            neuron['output'] = transfer(activation)
            new_inputs.append(neuron['output'])
        inputs = new_inputs
    return inputs

# 前向傳播測試
network = [[{'weights': [0.13436424411240122, 0.8474337369372327, 0.763774618976614]}],
[{'weights': [0.2550690257394217, 0.49543508709194095]}, {'weights': [0.4494910647887381,0.651592972722763]}]]
row = [1, 0, None]
output = forward_propagate(network, row)
print(output)

執行示例程式碼將向網路中傳入1, 0作為輸入併產生相應的輸出。因為輸出層有兩個神經元,所以我們得到了包含兩個元素的輸出列表。

現在網路的輸出值並沒有任何實際意義,不過下面我們將學習如何得到使網路中的權重更有價值。

[0.6629970129852887, 0.7253160725279748]

3.誤差反向傳播

反向傳播演算法的名字是由它訓練權重的方式得來的。

誤差是通過預期輸出和網路前向傳播輸出值計算得到的。這些誤差通過網路從輸出層反向傳播至隱藏層,分配誤差並即時更新權重。

反向傳播誤差的數學形式源於微積分,但本節我們將以高層次的角度來關注它計算了什麼以及怎樣實現它特定的計算形式而不是為何選取這種形式。

本節分為兩部分。

  1. 傳遞導數。
  2. 誤差反向傳播。

3.1. 傳遞導數

給定了神經元的輸出值,我們需要計算它的斜率。

我們選取的傳遞函式是sigmoid函式,其導數可以通過下面的公式計算:

derivative = output * (1.0 - output)

下面的 transfer_derivative() 函式實現了這個公式:

# 計算神經元輸出的導數
def transfer_derivative(output):
    return output * (1.0 - output)

現在,讓我們看看如何使用它。

3.2. 誤差反向傳播

第一步是計算每個輸出神經元的誤差,這將為我們提供網路反向傳播所需的誤差訊號(輸入)。

對於給定的神經元,它的誤差可以通過下式計算得到:

error = (expected - output) * transfer_derivative(output)

except為神經元的預期輸出值,output為神經元的實際輸出值,transfer_derivative() 是我們上面用於計算神經元輸出值斜率的函式。

誤差計算將應用於輸出層。對於輸出層來說,預期輸出即樣本真實的類別。在隱含層中則會複雜一些。

隱藏層中神經元的誤差訊號將通過輸出層中每個神經元誤差的加權誤差計算得到。我們將誤差通過輸出層權重傳遞給隱藏層中的每一個神經元。

反向傳播積累的誤差訊號將用於確定隱藏層中神經元的誤差:

error = (weight_k * error_j) * transfer_derivative(output)

其中,error_j是輸出層中第j個神經元的誤差訊號,weight_k是第k個神經元到當前神經元的連線權重,output是當前神經元的輸出。

下面的 backward_propagate_error() 函式實現了這個過程。

可以看到每個神經元計算得到的誤差訊號將儲存在其delta屬性下。可以看到,網路的各層將以反向的順序迭代,從輸出層開始反向傳播。這確保了輸出層的神經元首先計算delta值以供隱藏層的神經元可以在後續的迭代中使用。我使用delta作為屬性名來反映這是這是神經元上誤差的變化(例:weight delta)。

可以看到,隱藏層中的誤差訊號是從輸出層神經元中積累的,其中隱藏層的神經元序號j也是輸出層神經元權重的索引 neuron'weight'

# 誤差的反向傳播以及在神經元中的儲存
def backward_propagate_error(network, expected):
    for i in reversed(range(len(network))):
        layer = network[i]
        errors = list()
        if i != len(network)-1:
            for j in range(len(layer)):
                error = 0.0
                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(expected[j] - neuron['output'])
        for j in range(len(layer)):
            neuron = layer[j]
            neuron['delta'] = errors[j] * transfer_derivative(neuron['output'])

讓我們把所有的程式碼段放在一起,看看它們如何工作。

我們定義一個具有設定輸出值的固定神經網路,然後用預期的輸出實現反向傳播。完整的程式碼樣例如下所示:

# 計算神經元輸出的導數
def transfer_derivative(output):
    return output * (1.0 - output)

# 誤差的反向傳播以及在神經元中的儲存
def backward_propagate_error(network, expected):
    for i in reversed(range(len(network))):
        layer = network[i]
        errors = list()
        if i != len(network)-1:
            for j in range(len(layer)):
                error = 0.0
                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(expected[j] - neuron['output'])
        for j in range(len(layer)):
            neuron = layer[j]
            neuron['delta'] = errors[j] * transfer_derivative(neuron['output'])

# 測試誤差反向傳播
network = [[{'output': 0.7105668883115941, 'weights': [0.13436424411240122, 0.8474337369372327,0.763774618976614]}],
[{'output': 0.6213859615555266, 'weights': [0.2550690257394217, 0.49543508709194095]}, {'output':0.6573693455986976, 'weights': [0.4494910647887381, 0.651592972722763]}]]
expected = [0, 1]
backward_propagate_error(network, expected)
for layer in network:
    print(layer)

執行程式碼,在誤差反向傳播完成後會列印輸出網路資訊,可以看到輸出層和隱藏層的神經元的誤差都已經被計算並儲存在了對應的神經元當中。

[{'output': 0.7105668883115941, 'weights': [0.13436424411240122, 0.8474337369372327, 0.763774618976614], 'delta': -0.0005348048046610517}]
[{'output': 0.6213859615555266, 'weights': [0.2550690257394217, 0.49543508709194095], 'delta': -0.14619064683582808}, {'output': 0.6573693455986976, 'weights': [0.4494910647887381, 0.651592972722763], 'delta': 0.0771723774346327}]

現在讓我們使用誤差反向傳播來訓練網路。

4.訓練網路

使用隨機梯度下降演算法訓練網路。

這涉及了網路在訓練資料集上的多次迭代,對於每一行資料則包括了輸入資料的前向傳播,誤差的反向傳播以及權重的更新。

這節分為兩部分:

  1. 更新權重。
  2. 訓練網路。

4.1. 更新權重

當我們通過上述的反向傳播方法計算得到了每個神經元的誤差之後,就可以用它們來更新權重值。

網路權重更新公式如下:

weight = weight + learning_rate * error * input

weight為給定的權重,learning_rate為手動指定的超引數,error為神經元通過反向傳播計算得到的誤差,input為產生誤差的輸入值。

偏置權重的更新公式也是一樣的,只是沒有輸入項或者說輸入值永遠為1.0而已。

學習率控制著糾正誤差時權重的變化大小。舉例來說,0.1的學習率將更新可能需要更新權重量的10%。小的學習率會導致更大的訓練批次和更慢的學習速率。這增加了網路在所有層上尋找到一組最優的權重的可能性而不是更快地得到一組最小化誤差的次優權重(Premature convergence

,這種現象也稱為過早收斂,早熟收斂)

下面是 update_weights() 函式,在給定輸入資料和學習率且前向傳播和反向傳播進行完畢時對網路權重進行更新。

請記住,輸出層的輸入是隱藏層輸出的集合。

# 根據誤差更新網路權重
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]
            neuron['weights'][-1] += l_rate * neuron['delta']

現在我們知道了如何更新網路權重,下面讓我們看看我們如何重複以上過程。

4.2. 訓練網路

如前所述,使用隨機梯度下降演算法來更新網路。

該過程需要完成指定批次數量的迴圈過程,在每個訓練批次中需要根據訓練資料集中對應的輸入來更新網路權重。

因為每個訓練樣本的輸入都會導致網路的更新,這種學習方式被稱作線上學習。如果每個批次中的訓練樣本不止一個,即在每次更新前都有誤差的積累過程,這種學習方式稱為批量學習或者批量梯度下降(Batch gradient descent)。

下面的函式實現了給定訓練資料集,學習率,epochs(批次數),預期輸出和初始化網路時網路的訓練過程。

訓練資料集中的預期輸出是類別經過One-hot編碼後的輸出,為列向量。我們將使用這個二值向量來與網路的輸出進行比對,這是計算輸出層誤差必需的過程。

從程式碼中還可以看到,預期輸出和網路輸出間的平方誤差會在每個訓練批次(epoch)中積累,在每個訓練批次結束後會列印輸出誤差,這有助於我們觀察網路在訓練中學習和提升的過程。

# 訓練網路(手動指定批次數 n_epoch)
def train_network(network, train, l_rate, n_epoch, n_outputs):
    for epoch in range(n_epoch):
    sum_error = 0
    for row in train:
        outputs = forward_propagate(network, row)
        expected = [0 for i in range(n_outputs)]
        expected[row[-1]] = 1
        sum_error += sum([(expected[i]-outputs[i])**2 for i in range(len(expected))])
        backward_propagate_error(network, expected)
        update_weights(network, row, l_rate)
    print('>epoch=%d, lrate=%.3f, error=%.3f' % (epoch, l_rate, sum_error))

我們現在已經完成了訓練網路所需的全部程式碼段,下面讓我們在一個小的資料集上完成一個囊括之前所有內容(網路初始化,網路訓練)的測試吧。

下面是一個設計好的小資料集,我們用它來測試我們神經網路的訓練過程。

X1	X2	Y
2.7810836	2.550537003	0
1.465489372	2.362125076	0
3.396561688	4.400293529	0
1.38807019	1.850220317	0
3.06407232	3.005305973	0
7.627531214	2.759262235	1
5.332441248	2.088626775	1
6.922596716	1.77106367	1
8.675418651	-0.242068655	1
7.673756466	3.508563011	1

下面給出完整的程式碼示例。我們設定隱藏層的神經元數量為2,因為這是一個二分類問題,所以輸出層需要兩個神經元。設定訓練批次數為20,學習率為0.5,學習率設定這麼大的原因是訓練的迭代次數過少。

from math import exp
from random import seed
from random import random

# 初始化網路
def initialize_network(n_inputs, n_hidden, n_outputs):
    network = list()
    hidden_layer = [{'weights':[random() for i in range(n_inputs + 1)]} for i in range(n_hidden)]
    network.append(hidden_layer)
    output_layer = [{'weights':[random() for i in range(n_hidden + 1)]} for i in range(n_outputs)]
    network.append(output_layer)
    return network

# 計算給定輸入情況下神經元的啟用值
def activate(weights, inputs):
    activation = weights[-1]
    for i in range(len(weights)-1):
        activation += weights[i] * inputs[i]
    return activation

# 傳遞神經元的啟用值
def transfer(activation):
    return 1.0 / (1.0 + exp(-activation))

# 從網路輸入到網路輸出的前向傳播
def forward_propagate(network, row):
    inputs = row
    for layer in network:
        new_inputs = []
        for neuron in layer:
            activation = activate(neuron['weights'], inputs)
            neuron['output'] = transfer(activation)
            new_inputs.append(neuron['output'])
        inputs = new_inputs
    return inputs

# 計算神經元輸出值的導數
def transfer_derivative(output):
    return output * (1.0 - output)

# 誤差反向傳播並將誤差儲存在神經元中
def backward_propagate_error(network, expected):
    for i in reversed(range(len(network))):
        layer = network[i]
        errors = list()
        if i != len(network)-1:
            for j in range(len(layer)):
                error = 0.0
                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(expected[j] - neuron['output'])
        for j in range(len(layer)):
            neuron = layer[j]
            neuron['delta'] = errors[j] * transfer_derivative(neuron['output'])

# 根據誤差更新網路權重
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]
            neuron['weights'][-1] += l_rate * neuron['delta']

# 訓練網路(手動指定批次數 n_epoch)
def train_network(network, train, l_rate, n_epoch, n_outputs):
    for epoch in range(n_epoch):
    sum_error = 0
    for row in train:
        outputs = forward_propagate(network, row)
        expected = [0 for i in range(n_outputs)]
        expected[row[-1]] = 1
        sum_error += sum([(expected[i]-outputs[i])**2 for i in range(len(expected))])
        backward_propagate_error(network, expected)
        update_weights(network, row, l_rate)
    print('>epoch=%d, lrate=%.3f, error=%.3f' % (epoch, l_rate, sum_error))

# 測試反向傳播演算法實現的訓練過程
seed(1)
dataset = [[2.7810836,2.550537003,0],
	[1.465489372,2.362125076,0],
	[3.396561688,4.400293529,0],
	[1.38807019,1.850220317,0],
	[3.06407232,3.005305973,0],
	[7.627531214,2.759262235,1],
	[5.332441248,2.088626775,1],
	[6.922596716,1.77106367,1],
	[8.675418651,-0.242068655,1],
	[7.673756466,3.508563011,1]]
n_inputs = len(dataset[0]) - 1
n_outputs = len(set([row[-1] for row in dataset]))
network = initialize_network(n_inputs, 2, n_outputs)
train_network(network, dataset, 0.5, 20, n_outputs)
for layer in network:
    print(layer)

執行程式碼,首相將看到每個訓練批次結束時列印的平方和誤差。整體來看誤差呈下降趨勢。

訓練過程一結束,就可以看到網路的輸出,其中包括網路習得的權重。除此之外,網路的輸出和小到可以忽略的delta值也一同列印了出來。如果需要的話,我們可以更新我們的網路訓練函式在訓練結束後刪除這些資料。

>epoch=0, lrate=0.500, error=6.350
>epoch=1, lrate=0.500, error=5.531
>epoch=2, lrate=0.500, error=5.221
>epoch=3, lrate=0.500, error=4.951
>epoch=4, lrate=0.500, error=4.519
>epoch=5, lrate=0.500, error=4.173
>epoch=6, lrate=0.500, error=3.835
>epoch=7, lrate=0.500, error=3.506
>epoch=8, lrate=0.500, error=3.192
>epoch=9, lrate=0.500, error=2.898
>epoch=10, lrate=0.500, error=2.626
>epoch=11, lrate=0.500, error=2.377
>epoch=12, lrate=0.500, error=2.153
>epoch=13, lrate=0.500, error=1.953
>epoch=14, lrate=0.500, error=1.774
>epoch=15, lrate=0.500, error=1.614
>epoch=16, lrate=0.500, error=1.472
>epoch=17, lrate=0.500, error=1.346
>epoch=18, lrate=0.500, error=1.233
>epoch=19, lrate=0.500, error=1.132
[{'output': 0.029980305604426185, 'weights': [-1.4688375095432327, 1.850887325439514, 1.0858178629550297], 'delta': -0.0059546604162323625}, {'output': 0.9456229000211323, 'weights': [0.37711098142462157, -0.0625909894552989, 0.2765123702642716], 'delta': 0.0026279652850863837}]
[{'output': 0.23648794202357587, 'weights': [2.515394649397849, -0.3391927502445985, -0.9671565426390275], 'delta': -0.04270059278364587}, {'output': 0.7790535202438367, 'weights': [-2.5584149848484263, 1.0036422106209202, 0.42383086467582715], 'delta': 0.03803132596437354}]

網路的訓練一經完成,我們就需要用它來實現預測功能。

5.預測

用訓練好的神經網路做出預測是很容易的。

我們已經看到了如何通過前向傳播輸入來獲得輸出。這是我們預測所需的全部過程。我們可以直接將輸出值中每一行的值當作樣本屬於對應類的概率。

我們可以通過選擇具有較大概率的類做為預測結果,這樣輸出就轉換為了一個清晰的類別預測,這樣的結果對我們來說更有幫助。實現這個操作的函式被稱為argmax函式

下面是 predict() 函式,它實現了這個過程。它返回網路輸出中最大概率值的索引。假定類的值已經轉換為從0開始的整數。

# 利用網路的輸出值進行預測
def predict(network, row):
    outputs = forward_propagate(network, row)
    return outputs.index(max(outputs))

我們可以把這個函式和上面實現前向輸入的程式碼放在一起,用我們設計好的小資料集測試訓練得到的網路的預測功能。不過現在,我們先暫時把上面訓練得到的網路硬編碼下來進行測試。

例子的完整程式碼如下所示。

from math import exp

# 計算給定輸入情況下神經元的啟用值
def activate(weights, inputs):
	activation = weights[-1]
	for i in range(len(weights)-1):
		activation += weights[i] * inputs[i]
	return activation

# 傳遞神經元的啟用值
def transfer(activation):
	return 1.0 / (1.0 + exp(-activation))

# 從網路輸入到網路輸出的前向傳播
def forward_propagate(network, row):
	inputs = row
	for layer in network:
		new_inputs = []
		for neuron in layer:
			activation = activate(neuron['weights'], inputs)
			neuron['output'] = transfer(activation)
			new_inputs.append(neuron['output'])
		inputs = new_inputs
	return inputs

# 利用網路的輸出值進行預測
def predict(network, row):
	outputs = forward_propagate(network, row)
	return outputs.index(max(outputs))

# 測試網路的預測功能
dataset = [[2.7810836,2.550537003,0],
	[1.465489372,2.362125076,0],
	[3.396561688,4.400293529,0],
	[1.38807019,1.850220317,0],
	[3.06407232,3.005305973,0],
	[7.627531214,2.759262235,1],
	[5.332441248,2.088626775,1],
	[6.922596716,1.77106367,1],
	[8.675418651,-0.242068655,1],
	[7.673756466,3.508563011,1]]
network = [[{'weights': [-1.482313569067226, 1.8308790073202204, 1.078381922048799]}, {'weights': [0.23244990332399884, 0.3621998343835864, 0.40289821191094327]}],
	[{'weights': [2.5001872433501404, 0.7887233511355132, -1.1026649757805829]}, {'weights': [-2.429350576245497, 0.8357651039198697, 1.0699217181280656]}]]
for row in dataset:
	prediction = predict(network, row)
	print('Expected=%d, Got=%d' % (row[-1], prediction))

執行程式碼,可以看到訓練資料集中每個樣本的預期輸出以及網路根據輸入作出的預測。

輸出結果表明網路在這個小資料集上達到了100%的準確性。

Expected=0, Got=0
Expected=0, Got=0
Expected=0, Got=0
Expected=0, Got=0
Expected=0, Got=0
Expected=1, Got=1
Expected=1, Got=1
Expected=1, Got=1
Expected=1, Got=1
Expected=1, Got=1

現在我們準備將反向傳播演算法應用到真實世界的資料集中。

6.小麥種子資料集

本節將反向傳播演算法應用於小麥種子資料集。

第一步是載入資料集並將載入的資料轉換為我們可以在我們的神經網路中使用的數值量。為此,我們將使用輔助函式 load_csv() 來載入檔案,用 str_column_to_float() 函式將字串數字轉換為float型別,str_column_to_int() 將整數列轉換為int型別。

各個輸入值的變化範圍大小不同,需要歸一化至0-1的範圍,將輸入值歸一化至傳遞函式的範圍內是一個很好的習慣。在本文中,我們使用的sigmoid函式的輸出區間為0-1,使用 dataset_minmax()normalize_dataset() 輔助函式對輸入值進行歸一化。

我們將使用5次交叉驗證來評估演算法(K次交叉驗證)。這意味著每一份中包含著 40/41 個樣本記錄。後面我們會使用輔助函式 evalute_algotithm() 來對演算法進行交叉驗證,用 accuracy_metric() 函式來計算預測的準確性。

除此之外,我們新開發了 back_propagation() 函式來管理反向傳播演算法的應用,首先初始化網路,然後在訓練資料集上進行訓練,最後使用訓練好的網路在測試資料集上進行預測。

完整的程式碼示例如下所示。

# 在種子資料集上實現反向傳播

from random import seed
from random import randrange
from random import random
from csv import reader
from math import exp

# 載入CSV資料
def load_csv(filename):
	dataset = list()
	with open(filename, 'r') as file:
		csv_reader = reader(file)
		for row in csv_reader:
			if not row:
				continue
			dataset.append(row)
	return dataset

# 將字串的列轉化為float型別
def str_column_to_float(dataset, column):
	for row in dataset:
		row[column] = float(row[column].strip())

# 將字串的列轉化為int型別
def str_column_to_int(dataset, column):
	class_values = [row[column] for row in dataset]
	unique = set(class_values)
	lookup = dict()
	for i, value in enumerate(unique):
		lookup[value] = i
	for row in dataset:
		row[column] = lookup[row[column]]
	return lookup

# 找到每一列的最小值和最大值
def dataset_minmax(dataset):
	minmax = list()
	stats = [[min(column), max(column)] for column in zip(*dataset)]
	return stats

# 將資料集歸一化至0-1的範圍內
def normalize_dataset(dataset, minmax):
	for row in dataset:
		for i in range(len(row)-1):
			row[i] = (row[i] - minmax[i][0]) / (minmax[i][1] - minmax[i][0])

# 將資料集分為k份
def cross_validation_split(dataset, n_folds):
	dataset_split = list()
	dataset_copy = list(dataset)
	fold_size = int(len(dataset) / n_folds)
	for i in range(n_folds):
		fold = list()
		while len(fold) < fold_size:
			index = randrange(len(dataset_copy))
			fold.append(dataset_copy.pop(index))
		dataset_split.append(fold)
	return dataset_split

# 計算準確率
def accuracy_metric(actual, predicted):
	correct = 0
	for i in range(len(actual)):
		if actual[i] == predicted[i]:
			correct += 1
	return correct / float(len(actual)) * 100.0

# 用分割好的資料集來評估演算法
def evaluate_algorithm(dataset, algorithm, n_folds, *args):
	folds = cross_validation_split(dataset, n_folds)
	scores = list()
	for fold in folds:
		train_set = list(folds)
		train_set.remove(fold)
		train_set = sum(train_set, [])
		test_set = list()
		for row in fold:
			row_copy = list(row)
			test_set.append(row_copy)
			row_copy[-1] = None
		predicted = algorithm(train_set, test_set, *args)
		actual = [row[-1] for row in fold]
		accuracy = accuracy_metric(actual, predicted)
		scores.append(accuracy)
	return scores

# 計算給定輸入情況下神經元的啟用值
def activate(weights, inputs):
	activation = weights[-1]
	for i in range(len(weights)-1):
		activation += weights[i] * inputs[i]
	return activation

# 傳遞神經元的啟用值
def transfer(activation):
	return 1.0 / (1.0 + exp(-activation))

# 前向傳播輸入得到輸出
def forward_propagate(network, row):
	inputs = row
	for layer in network:
		new_inputs = []
		for neuron in layer:
			activation = activate(neuron['weights'], inputs)
			neuron['output'] = transfer(activation)
			new_inputs.append(neuron['output'])
		inputs = new_inputs
	return inputs

# 計算神經元輸出值的導數
def transfer_derivative(output):
	return output * (1.0 - output)

# 誤差反向傳播並將誤差儲存在神經元中
def backward_propagate_error(network, expected):
	for i in reversed(range(len(network))):
		layer = network[i]
		errors = list()
		if i != len(network)-1:
			for j in range(len(layer)):
				error = 0.0
				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(expected[j] - neuron['output'])
		for j in range(len(layer)):
			neuron = layer[j]
			neuron['delta'] = errors[j] * transfer_derivative(neuron['output'])

# 根據誤差更新網路權重
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]
			neuron['weights'][-1] += l_rate * neuron['delta']

# 訓練網路(手動指定批次數 n_epoch)
def train_network(network, train, l_rate, n_epoch, n_outputs):
	for epoch in range(n_epoch):
		for row in train:
			outputs = forward_propagate(network, row)
			expected = [0 for i in range(n_outputs)]
			expected[row[-1]] = 1
			backward_propagate_error(network, expected)
			update_weights(network, row, l_rate)


# 初始化網路
def initialize_network(n_inputs, n_hidden, n_outputs):
	network = list()
	hidden_layer = [{'weights':[random() for i in range(n_inputs + 1)]} for i in range(n_hidden)]
	network.append(hidden_layer)
	output_layer = [{'weights':[random() for i in range(n_hidden + 1)]} for i in range(n_outputs)]
	network.append(output_layer)
	return network

# 使用網路進行預測
def predict(network, row):
	outputs = forward_propagate(network, row)
	return outputs.index(max(outputs))

# 使用隨機梯度下降實現反向傳播演算法
def back_propagation(train, test, l_rate, n_epoch, n_hidden):
	n_inputs = len(train[0]) - 1
	n_outputs = len(set([row[-1] for row in train]))
	network = initialize_network(n_inputs, n_hidden, n_outputs)
	train_network(network, train, l_rate, n_epoch, n_outputs)
	predictions = list()
	for row in test:
		prediction = predict(network, row)
		predictions.append(prediction)
	return(predictions)

# 在種子資料集上測試反向傳播
seed(1)

# 匯入準備的資料
filename = 'seeds_dataset.csv'
dataset = load_csv(filename)
for i in range(len(dataset[0])-1):
	str_column_to_float(dataset, i)

# 將類別一類轉換為int型別
str_column_to_int(dataset, len(dataset[0])-1)

# 輸入變數歸一化
minmax = dataset_minmax(dataset)
normalize_dataset(dataset, minmax)

# 評估演算法
n_folds = 5
l_rate = 0.3
n_epoch = 500
n_hidden = 5
scores = evaluate_algorithm(dataset, back_propagation, n_folds, l_rate, n_epoch, n_hidden)
print('Scores: %s' % scores)
print('Mean Accuracy: %.3f%%' % (sum(scores)/float(len(scores))))

我們在程式碼中構建了具有5個神經元的隱藏層和具有3個神經元的輸出層的網路。網路訓練批次數為500,學習率為0.3。這些引數時經過少量試錯後得到的,也許你可以找到更好的組合。

執行程式碼,每一份資料的平均分類準確率和整體的效能將被分別打印出來。

可以看到所選配置下反向傳播演算法的平均分類準確率達到了95.238%,比零規則演算法的準確度提高了28.095%。

Scores: [95.23809523809523, 97.61904761904762, 95.23809523809523, 92.85714285714286, 95.23809523809523]
Mean Accuracy: 95.238%

擴充套件

本節列出了你可能希望探索嘗試的擴充套件功能。

  • 演算法引數調優。選用更大規模或更小規模的網路,調整訓練批次更大或更小。看看能否在種子資料集上獲得更好的效能。
  • 其他方法。試用不同的權重初始化技術(如正態分佈隨機,平均分佈隨機等)和不同的傳遞函式(如tanh)。
  • 更多層。增加對更多隱藏層的支援,訓練方式與本教程中使用的隱藏層相同。
  • 迴歸。使網路輸出層中只有一個神經元,並用它預測實值。選擇一個迴歸資料集來練習。輸出層中的神經元可以使用線性傳遞函式作為傳遞函式,或者將所選資料集的輸出值縮放到0到1之間的值。
  • 批量梯度下降。將訓練過程從線上學習更改為批量梯度下降,並僅在每個epoch結束時更新權重。

如果你嘗試了以上擴充套件,歡迎在下面的評論中分享你的經驗。

總結回顧

在本教程中,你瞭解瞭如何從頭開始構建反向傳播演算法。

具體來說,你瞭解到:

  • 如何前向傳播輸入來計算網路輸出。
  • 如何反向傳播誤差並更新網路權重。
  • 如何將反向傳播演算法應用於真實世界的資料集。