原文位址:https://www.zybuluo.com/hanbingtao/note/476663
轉載在此的目的是自己做個筆記,日後好複習,如侵權請聯系我!!
在上一篇文章中,我們已經掌握了機器學習的基本套路,對模型、目标函數、優化算法這些概念有了一定程度的了解,而且已經會訓練單個的感覺器或者線性單元了。在這篇文章中,我們将把這些單獨的單元按照一定的規則互相連接配接在一起形成神經網絡,進而奇迹般的獲得了強大的學習能力。我們還将介紹這種網絡的訓練算法:反向傳播算法。最後,我們依然用代碼實作一個神經網絡。如果您能堅持到本文的結尾,将會看到我們用自己實作的神經網絡去識别手寫數字。現在請做好準備,您即将雙手觸及到深度學習的大門。
神經元
神經元和感覺器本質上是一樣的,隻不過我們說感覺器的時候,它的激活函數是階躍函數;而當我們說神經元的時候,激活函數往往選擇為sigmoid函數或者tanh函數,如下圖所示:
![](https://img.laitimes.com/img/__Qf2AjLwojIjJCLyojI0JCLiETPwJWZ3ZCMwcTP39zZuBnLuVzRjVXSq1kMZR0TwUkeORDMD5ENVR1Tw0EVOVTRE90dFRVT0UERNlHMD1EeRpmT5lEVNZXRU1ENFRUT5hzQNhXUq5UeJRVT2NmMiNnSywEd5ITW110MaZHetlVdO1GT0UERNl3YXJGc5kHT20ESjBjUIF2Lc12bj5SYphXa5VWen5WY35iclN3Ztl2Lc9CX6MHc0RHaiojIsJye.png)
sigmoid函數是一個非線性函數,值域是(0,1)。函數圖像如下圖所示:
sigmoid函數的導數是:
可以看到,sigmoid函數的導數非常有趣,它可以用sigmoid函數自身來表示。這樣,一旦計算出sigmoid函數的值,計算它的導數的值就非常友善。
神經網絡是什麼
神經網絡其實就是按照一定規則連接配接起來的多個神經元。上圖展示了一個全連接配接(full connected, FC)神經網絡,通過觀察上面的圖,我們可以發現它的規則包括:
- 神經元按照層來布局。最左邊的層叫做輸入層,負責接收輸入資料;最右邊的層叫輸出層,我們可以從這層擷取神經網絡輸出資料。輸入層和輸出層之間的層叫做隐藏層,因為它們對于外部來說是不可見的。
- 同一層的神經元之間沒有連接配接。
- 第N層的每個神經元和第N-1層的所有神經元相連(這就是full connected的含義),第N-1層神經元的輸出就是第N層神經元的輸入。
- 每個連接配接都有一個權值。
上面這些規則定義了全連接配接神經網絡的結構。事實上還存在很多其它結構的神經網絡,比如卷積神經網絡(CNN)、循環神經網絡(RNN),他們都具有不同的連接配接規則。
神經網絡為什麼能普遍适用
單層的神經網絡能夠模拟一條二維平面上的直線,進而可以完成線性分割任務。而理論證明,兩層神經網絡可以無限逼近任意連續函數。
證明:兩層神經網絡可以無限逼近任意連續函數。
比如下面這張圖,二維平面中有兩類點,紅色的和藍色的,用一條直線肯定不能把二者分開了。
我們使用一個兩層的神經網絡可以得到一個非常近似的結果,使得分類誤差在滿意的範圍之内。而這個真實的連續函數的原型是:
這麼複雜的函數,一個兩層的神經網絡是如何做到的呢?其實從輸入層到隐藏層的矩陣計算,就是對輸入資料進行了空間變換,使其可以被線性可分,然後輸出層畫出了一個分界線。而訓練的過程,就是确定那個空間變換矩陣的過程。是以,多層神經網絡的本質就是對複雜函數的拟合。我們可以在後面的試驗中來學習如何拟合上述的複雜函數的。
為什麼需要激活函數
為什麼我們不能再沒有激活輸入信号的情況下完成神經網絡的學習呢?
如果我們不運用激活函數的話,則輸出信号将僅僅是一個簡單的線性函數。線性函數一個一級多項式。現如今,線性方程是很容易解決的,但是它們的複雜性有限,并且從資料中學習複雜函數映射的能力更小。一個沒有激活函數的神經網絡将隻不過是一個線性回歸模型(Linear regression Model)罷了,它功率有限,并且大多數情況下執行得并不好。我們希望我們的神經網絡不僅僅可以學習和計算線性函數,而且還要比這複雜得多。同樣是因為沒有激活函數,我們的神經網絡将無法學習和模拟其他複雜類型的資料,例如圖像、視訊、音頻、語音等。這就是為什麼我們要使用人工神經網絡技術,諸如深度學習(Deep learning),來了解一些複雜的事情,一些互相之間具有很多隐藏層的非線性問題,而這也可以幫助我們了解複雜的資料。
為什麼我們需要非線性函數?
非線性函數是那些一級以上的函數,而且當繪制非線性函數時它們具有曲率。現在我們需要一個可以學習和表示幾乎任何東西的神經網絡模型,以及可以将輸入映射到輸出的任意複雜函數。神經網絡被認為是通用函數近似器(Universal Function Approximators)。這意味着他們可以計算和學習任何函數。幾乎我們可以想到的任何過程都可以表示為神經網絡中的函數計算。
而這一切都歸結于這一點,我們需要應用激活函數f(x),以便使網絡更加強大,增加它的能力,使它可以學習複雜的事物,複雜的表單資料,以及表示輸入輸出之間非線性的複雜的任意函數映射。是以,使用非線性激活函數,我們便能夠從輸入輸出之間生成非線性映射。
激活函數的另一個重要特征是:它應該是可以區分的。我們需要這樣做,以便在網絡中向後推進以計算相對于權重的誤差(丢失)梯度時執行反向優化政策,然後相應地使用梯度下降或任何其他優化技術優化權重以減少誤差。
計算神經網絡的輸出
接下來,舉一個例子來說明這個過程,我們先給神經網絡的每個單元寫上編号。
如上圖,輸入層有三個節點,我們将其依次編号為1、2、3;隐藏層的4個節點,編号依次為4、5、6、7;最後輸出層的兩個節點編号為8、9。因為我們這個神經網絡是全連接配接網絡,是以可以看到每個節點都和上一層的所有節點有連接配接。比如,我們可以看到隐藏層的節點4,它和輸入層的三個節點1、2、3之間都有連接配接,其連接配接上的權重分别為W41,W42,W43。那麼,我們怎樣計算節點4的輸出值a4呢?
神經網絡的矩陣表示
神經網絡的計算如果用矩陣來表示會很友善(當然逼格也更高),我們先來看看隐藏層的矩陣表示。
首先我們把隐藏層4個節點的計算依次排列出來:
則最後一層的輸出向量的計算可以表示為:
這就是神經網絡輸出值的計算方法。
神經網絡的訓練
現在,我們需要知道一個神經網絡的每個連接配接上的權值是如何得到的。我們可以說神經網絡是一個模型,那麼這些權值就是模型的參數,也就是模型要學習的東西。然而,一個神經網絡的連接配接方式、網絡的層數、每層的節點數這些參數,則不是學習出來的,而是人為事先設定的。對于這些人為設定的參數,我們稱之為超參數(Hyper-Parameters)。
接下來,我們将要介紹神經網絡的訓練算法:反向傳播算法。
反向傳播算法(Back Propagation)
我們首先直覺的介紹反向傳播算法,最後再來介紹這個算法的推導。當然讀者也可以完全跳過推導部分,因為即使不知道如何推導,也不影響你寫出來一個神經網絡的訓練代碼。事實上,現在神經網絡成熟的開源實作多如牛毛,除了練手之外,你可能都沒有機會需要去寫一個神經網絡。
我們已經介紹了神經網絡每個節點誤差項的計算和權重更新方法。顯然,計算一個節點的誤差項,需要先計算每個與其相連的下一層節點的誤差項。這就要求誤差項的計算順序必須是從輸出層開始,然後反向依次計算每個隐藏層的誤差項,直到與輸入層相連的那個隐藏層。這就是反向傳播算法的名字的含義。當所有節點的誤差項計算完畢後,我們就可以根據式5來更新所有的權重。
以上就是基本的反向傳播算法,并不是很複雜,您弄清楚了麼?
反向傳播算法的推導
反向傳播算法其實就是鍊式求導法則的應用。然而,這個如此簡單且顯而易見的方法,卻是在Roseblatt提出感覺器算法将近30年之後才被發明和普及的。對此,Bengio這樣回應道:
很多看似顯而易見的想法隻有在事後才變得顯而易見。
接下來,我們用鍊式求導法則來推導反向傳播算法,也就是上一小節的式3、式4、式5。
按照機器學習的通用套路,我們先确定神經網絡的目标函數,然後用随機梯度下降優化算法去求目标函數最小值時的參數值。
我們取網絡所有輸出層節點的誤差平方和作為目标函數:
然後我們利用随機梯度下降算法對目标函數進行優化
随機梯度下降算法也就是需要求出誤差Ed對于每個權重Wji的偏導數(也就是梯度)
輸出層權值訓練
隐藏層權值訓練
至此,我們已經推導出了反向傳播算法。需要注意的是,我們剛剛推導出的訓練規則是根據激活函數是sigmoid函數、平方和誤差、全連接配接網絡、随機梯度下降優化算法。如果激活函數不同、誤差計算方式不同、網絡連接配接結構不同、優化算法不同,則具體的訓練規則也會不一樣。但是無論怎樣,訓練規則的推導方式都是一樣的,應用鍊式求導法則進行推導即可。
神經網絡的實作
現在,我們要根據前面的算法,實作一個基本的全連接配接神經網絡,這并不需要太多代碼。我們在這裡依然采用面向對象設計。
首先,我們先做一個基本的模型:
如上圖,可以分解出5個領域對象來實作神經網絡:
- Network 神經網絡對象,提供API接口。它由若幹層對象組成以及連接配接對象組成。
- Layer 層對象,由多個節點組成。
- Node 節點對象計算和記錄節點自身的資訊(比如輸出值、誤差項等),以及與這個節點相關的上下遊的連接配接。
- Connection 每個連接配接對象都要記錄該連接配接的權重。
- Connections 僅僅作為Connection的集合對象,提供一些集合操作。
Node實作如下:
# 節點類,負責記錄和維護節點自身資訊以及與這個節點相關的上下遊連接配接,實作輸出值和誤差項的計算。
class Node(object):
def __init__(self, layer_index, node_index):
\'\'\'
構造節點對象。
layer_index: 節點所屬的層的編号
node_index: 節點的編号
\'\'\'
self.layer_index = layer_index
self.node_index = node_index
self.downstream = []
self.upstream = []
self.output = 0
self.delta = 0
def set_output(self, output):
\'\'\'
設定節點的輸出值。如果節點屬于輸入層會用到這個函數。
\'\'\'
self.output = output
def append_downstream_connection(self, conn):
\'\'\'
添加一個到下遊節點的連接配接
\'\'\'
self.downstream.append(conn)
def append_upstream_connection(self, conn):
\'\'\'
添加一個到上遊節點的連接配接
\'\'\'
self.upstream.append(conn)
def calc_output(self):
\'\'\'
根據式1計算節點的輸出
\'\'\'
output = reduce(lambda ret, conn: ret + conn.upstream_node.output * conn.weight, self.upstream, 0)
self.output = sigmoid(output)
def calc_hidden_layer_delta(self):
\'\'\'
節點屬于隐藏層時,根據式4計算delta
\'\'\'
downstream_delta = reduce(
lambda ret, conn: ret + conn.downstream_node.delta * conn.weight,
self.downstream, 0.0)
self.delta = self.output * (1 - self.output) * downstream_delta
def calc_output_layer_delta(self, label):
\'\'\'
節點屬于輸出層時,根據式3計算delta
\'\'\'
self.delta = self.output * (1 - self.output) * (label - self.output)
def __str__(self):
\'\'\'
列印節點的資訊
\'\'\'
node_str = \'%u-%u: output: %f delta: %f\' % (self.layer_index, self.node_index, self.output, self.delta)
downstream_str = reduce(lambda ret, conn: ret + \'\n\t\' + str(conn), self.downstream, \'\')
upstream_str = reduce(lambda ret, conn: ret + \'\n\t\' + str(conn), self.upstream, \'\')
return node_str + \'\n\tdownstream:\' + downstream_str + \'\n\tupstream:\' + upstream_str
constNode對象,為了實作一個輸出恒為1的節點(計算偏置項Wb時需要)
class ConstNode(object):
def __init__(self, layer_index, node_index):
\'\'\'
構造節點對象。
layer_index: 節點所屬的層的編号
node_index: 節點的編号
\'\'\'
self.layer_index = layer_index
self.node_index = node_index
self.downstream = []
self.output = 1
def append_downstream_connection(self, conn):
\'\'\'
添加一個到下遊節點的連接配接
\'\'\'
self.downstream.append(conn)
def calc_hidden_layer_delta(self):
\'\'\'
節點屬于隐藏層時,根據式4計算delta
\'\'\'
downstream_delta = reduce(
lambda ret, conn: ret + conn.downstream_node.delta * conn.weight,
self.downstream, 0.0)
self.delta = self.output * (1 - self.output) * downstream_delta
def __str__(self):
\'\'\'
列印節點的資訊
\'\'\'
node_str = \'%u-%u: output: 1\' % (self.layer_index, self.node_index)
downstream_str = reduce(lambda ret, conn: ret + \'\n\t\' + str(conn), self.downstream, \'\')
return node_str + \'\n\tdownstream:\' + downstream_str
Layer對象,負責初始化一層。此外,作為Node的集合對象,提供對Node集合的操作。
class Layer(object):
def __init__(self, layer_index, node_count):
\'\'\'
初始化一層
layer_index: 層編号
node_count: 層所包含的節點個數
\'\'\'
self.layer_index = layer_index
self.nodes = []
for i in range(node_count):
self.nodes.append(Node(layer_index, i))
self.nodes.append(ConstNode(layer_index, node_count))
def set_output(self, data):
\'\'\'
設定層的輸出。當層是輸入層時會用到。
\'\'\'
for i in range(len(data)):
self.nodes[i].set_output(data[i])
def calc_output(self):
\'\'\'
計算層的輸出向量
\'\'\'
for node in self.nodes[:-1]:
node.calc_output()
def dump(self):
\'\'\'
列印層的資訊
\'\'\'
for node in self.nodes:
print node
Connection對象,主要職責是記錄連接配接的權重,以及這個連接配接所關聯的上下遊節點。
class Connection(object):
def __init__(self, upstream_node, downstream_node):
\'\'\'
初始化連接配接,權重初始化為是一個很小的随機數
upstream_node: 連接配接的上遊節點
downstream_node: 連接配接的下遊節點
\'\'\'
self.upstream_node = upstream_node
self.downstream_node = downstream_node
self.weight = random.uniform(-0.1, 0.1)
self.gradient = 0.0
def calc_gradient(self):
\'\'\'
計算梯度
\'\'\'
self.gradient = self.downstream_node.delta * self.upstream_node.output
def get_gradient(self):
\'\'\'
擷取目前的梯度
\'\'\'
return self.gradient
def update_weight(self, rate):
\'\'\'
根據梯度下降算法更新權重
\'\'\'
self.calc_gradient()
self.weight += rate * self.gradient
def __str__(self):
\'\'\'
列印連接配接資訊
\'\'\'
return \'(%u-%u) -> (%u-%u) = %f\' % (
self.upstream_node.layer_index,
self.upstream_node.node_index,
self.downstream_node.layer_index,
self.downstream_node.node_index,
self.weight)
Connections對象,提供Connection集合操作。
class Connections(object):
def __init__(self):
self.connections = []
def add_connection(self, connection):
self.connections.append(connection)
def dump(self):
for conn in self.connections:
print conn
Network對象,提供API。
class Network(object):
def __init__(self, layers):
\'\'\'
初始化一個全連接配接神經網絡
layers: 二維數組,描述神經網絡每層節點數
\'\'\'
self.connections = Connections()
self.layers = []
layer_count = len(layers)
node_count = 0;
for i in range(layer_count):
self.layers.append(Layer(i, layers[i]))
for layer in range(layer_count - 1):
connections = [Connection(upstream_node, downstream_node)
for upstream_node in self.layers[layer].nodes
for downstream_node in self.layers[layer + 1].nodes[:-1]]
for conn in connections:
self.connections.add_connection(conn)
conn.downstream_node.append_upstream_connection(conn)
conn.upstream_node.append_downstream_connection(conn)
def train(self, labels, data_set, rate, iteration):
\'\'\'
訓練神經網絡
labels: 數組,訓練樣本标簽。每個元素是一個樣本的标簽。
data_set: 二維數組,訓練樣本特征。每個元素是一個樣本的特征。
\'\'\'
for i in range(iteration):
for d in range(len(data_set)):
self.train_one_sample(labels[d], data_set[d], rate)
def train_one_sample(self, label, sample, rate):
\'\'\'
内部函數,用一個樣本訓練網絡
\'\'\'
self.predict(sample)
self.calc_delta(label)
self.update_weight(rate)
def calc_delta(self, label):
\'\'\'
内部函數,計算每個節點的delta
\'\'\'
output_nodes = self.layers[-1].nodes
for i in range(len(label)):
output_nodes[i].calc_output_layer_delta(label[i])
for layer in self.layers[-2::-1]:
for node in layer.nodes:
node.calc_hidden_layer_delta()
def update_weight(self, rate):
\'\'\'
内部函數,更新每個連接配接權重
\'\'\'
for layer in self.layers[:-1]:
for node in layer.nodes:
for conn in node.downstream:
conn.update_weight(rate)
def calc_gradient(self):
\'\'\'
内部函數,計算每個連接配接的梯度
\'\'\'
for layer in self.layers[:-1]:
for node in layer.nodes:
for conn in node.downstream:
conn.calc_gradient()
def get_gradient(self, label, sample):
\'\'\'
獲得網絡在一個樣本下,每個連接配接上的梯度
label: 樣本标簽
sample: 樣本輸入
\'\'\'
self.predict(sample)
self.calc_delta(label)
self.calc_gradient()
def predict(self, sample):
\'\'\'
根據輸入的樣本預測輸出值
sample: 數組,樣本的特征,也就是網絡的輸入向量
\'\'\'
self.layers[0].set_output(sample)
for i in range(1, len(self.layers)):
self.layers[i].calc_output()
return map(lambda node: node.output, self.layers[-1].nodes[:-1])
def dump(self):
\'\'\'
列印網絡資訊
\'\'\'
for layer in self.layers:
layer.dump()
至此,實作了一個基本的全連接配接神經網絡。可以看到,同神經網絡的強大學習能力相比,其實作還算是很容易的。
梯度檢查
怎麼保證自己寫的神經網絡沒有BUG呢?事實上這是一個非常重要的問題。一方面,千辛萬苦想到一個算法,結果效果不理想,那麼是算法本身錯了還是代碼實作錯了呢?定位這種問題肯定要花費大量的時間和精力。另一方面,由于神經網絡的複雜性,我們幾乎無法事先知道神經網絡的輸入和輸出,是以類似TDD(測試驅動開發)這樣的開發方法似乎也不可行。
辦法還是有滴,就是利用梯度檢查來确認程式是否正确。梯度檢查的思路如下:
對于梯度下降算法:
def gradient_check(network, sample_feature, sample_label):
\'\'\'
梯度檢查
network: 神經網絡對象
sample_feature: 樣本的特征
sample_label: 樣本的标簽
\'\'\'
# 計算網絡誤差
network_error = lambda vec1, vec2: \
0.5 * reduce(lambda a, b: a + b,
map(lambda v: (v[0] - v[1]) * (v[0] - v[1]),
zip(vec1, vec2)))
# 擷取網絡在目前樣本下每個連接配接的梯度
network.get_gradient(sample_feature, sample_label)
# 對每個權重做梯度檢查
for conn in network.connections.connections:
# 擷取指定連接配接的梯度
actual_gradient = conn.get_gradient()
# 增加一個很小的值,計算網絡的誤差
epsilon = 0.0001
conn.weight += epsilon
error1 = network_error(network.predict(sample_feature), sample_label)
# 減去一個很小的值,計算網絡的誤差
conn.weight -= 2 * epsilon # 剛才加過了一次,是以這裡需要減去2倍
error2 = network_error(network.predict(sample_feature), sample_label)
# 根據式6計算期望的梯度值
expected_gradient = (error2 - error1) / (2 * epsilon)
# 列印
print \'expected gradient: \t%f\nactual gradient: \t%f\' % (
expected_gradient, actual_gradient)
至此,會推導、會實作、會抓BUG,你已經摸到深度學習的大門了。接下來還需要不斷的實踐,我們用剛剛寫過的神經網絡去識别手寫數字。
神經網絡實戰——手寫數字識别
可以參考部落格:https://www.cnblogs.com/wj-1314/p/9842719.html
針對這個任務,我們采用業界非常流行的MNIST資料集。MNIST大約有60000個手寫字母的訓練樣本,我們使用它訓練我們的神經網絡,然後再用訓練好的網絡去識别手寫數字。
手寫數字識别是個比較簡單的任務,數字隻可能是0-9中的一個,這是個10分類問題。
代碼實作:
首先,我們需要把MNIST資料集處理為神經網絡能夠接受的形式。MNIST訓練集的檔案格式可以參考官方網站,這裡不在贅述。每個訓練樣本是一個28*28的圖像,我們按照行優先,把它轉化為一個784維的向量。每個标簽是0-9的值,我們将其轉換為一個10維的one-hot向量:如果标簽值為n,我們就把向量的第維(從0開始編号)設定為0.9,而其它維設定為0.1。例如,向量[0.1,0.1,0.9,0.1,0.1,0.1,0.1,0.1,0.1,0.1]表示值2。
下面是處理MNIST資料的代碼:
#!/usr/bin/env python
# -*- coding: UTF-8 -*-
import struct
from bp import *
from datetime import datetime
# 資料加載器基類
class Loader(object):
def __init__(self, path, count):
\'\'\'
初始化加載器
path: 資料檔案路徑
count: 檔案中的樣本個數
\'\'\'
self.path = path
self.count = count
def get_file_content(self):
\'\'\'
讀取檔案内容
\'\'\'
f = open(self.path, \'rb\')
content = f.read()
f.close()
return content
def to_int(self, byte):
\'\'\'
将unsigned byte字元轉換為整數
\'\'\'
return struct.unpack(\'B\', byte)[0]
# 圖像資料加載器
class ImageLoader(Loader):
def get_picture(self, content, index):
\'\'\'
内部函數,從檔案中擷取圖像
\'\'\'
start = index * 28 * 28 + 16
picture = []
for i in range(28):
picture.append([])
for j in range(28):
picture[i].append(
self.to_int(content[start + i * 28 + j]))
return picture
def get_one_sample(self, picture):
\'\'\'
内部函數,将圖像轉化為樣本的輸入向量
\'\'\'
sample = []
for i in range(28):
for j in range(28):
sample.append(picture[i][j])
return sample
def load(self):
\'\'\'
加載資料檔案,獲得全部樣本的輸入向量
\'\'\'
content = self.get_file_content()
data_set = []
for index in range(self.count):
data_set.append(
self.get_one_sample(
self.get_picture(content, index)))
return data_set
# 标簽資料加載器
class LabelLoader(Loader):
def load(self):
\'\'\'
加載資料檔案,獲得全部樣本的标簽向量
\'\'\'
content = self.get_file_content()
labels = []
for index in range(self.count):
labels.append(self.norm(content[index + 8]))
return labels
def norm(self, label):
\'\'\'
内部函數,将一個值轉換為10維标簽向量
\'\'\'
label_vec = []
label_value = self.to_int(label)
for i in range(10):
if i == label_value:
label_vec.append(0.9)
else:
label_vec.append(0.1)
return label_vec
def get_training_data_set():
\'\'\'
獲得訓練資料集
\'\'\'
image_loader = ImageLoader(\'train-images-idx3-ubyte\', 60000)
label_loader = LabelLoader(\'train-labels-idx1-ubyte\', 60000)
return image_loader.load(), label_loader.load()
def get_test_data_set():
\'\'\'
獲得測試資料集
\'\'\'
image_loader = ImageLoader(\'t10k-images-idx3-ubyte\', 10000)
label_loader = LabelLoader(\'t10k-labels-idx1-ubyte\', 10000)
return image_loader.load(), label_loader.load()
網絡的輸出是一個10維向量,這個向量第n個(從0開始編号)元素的值最大,那麼n就是網絡的識别結果。下面是代碼實作:
def get_result(vec):
max_value_index = 0
max_value = 0
for i in range(len(vec)):
if vec[i] > max_value:
max_value = vec[i]
max_value_index = i
return max_value_index
我們使用錯誤率來對網絡進行評估,下面是代碼實作:
def evaluate(network, test_data_set, test_labels):
error = 0
total = len(test_data_set)
for i in range(total):
label = get_result(test_labels[i])
predict = get_result(network.predict(test_data_set[i]))
if label != predict:
error += 1
return float(error) / float(total)
最後實作我們的訓練政策:每訓練10輪,評估一次準确率,當準确率開始下降時終止訓練。下面是代碼實作:
def train_and_evaluate():
last_error_ratio = 1.0
epoch = 0
train_data_set, train_labels = get_training_data_set()
test_data_set, test_labels = get_test_data_set()
network = Network([784, 300, 10])
while True:
epoch += 1
network.train(train_labels, train_data_set, 0.3, 1)
print \'%s epoch %d finished\' % (now(), epoch)
if epoch % 10 == 0:
error_ratio = evaluate(network, test_data_set, test_labels)
print \'%s after epoch %d, error ratio is %f\' % (now(), epoch, error_ratio)
if error_ratio > last_error_ratio:
break
else:
last_error_ratio = error_ratio
if __name__ == \'__main__\':
train_and_evaluate()
在我的機器上測試了一下,1個epoch大約需要9000多秒,是以要對代碼做很多的性能優化工作(比如用向量化程式設計)。訓練要很久很久,可以把它上傳到伺服器上,在tmux的session裡面去運作。為了防止異常終止導緻前功盡棄,我們每訓練10輪,就把獲得參數值儲存在磁盤上,以便後續可以恢複。(代碼略)
向量化程式設計
在經曆了漫長的訓練之後,我們可能會想到,肯定有更好的辦法!是的,程式員們,現在我們需要告别面向對象程式設計了,轉而去使用另外一種更适合深度學習算法的程式設計方式:向量化程式設計。主要有兩個原因:一個是我們事實上并不需要真的去定義Node、Connection這樣的對象,直接把數學計算實作了就可以了;另一個原因,是底層算法庫會針對向量運算做優化(甚至有專用的硬體,比如GPU),程式效率會提升很多。是以,在深度學習的世界裡,我們總會想法設法的把計算表達為向量的形式。我相信優秀的程式員不會把自己拘泥于某種(自己熟悉的)程式設計範式上,而會去學習并使用最為合适的範式。
下面,我們用向量化程式設計的方法,重新實作前面的全連接配接神經網絡。
首先,我們需要把所有的計算都表達為向量的形式。對于全連接配接神經網絡來說,主要有三個計算公式。
前向計算,我們發現式2已經是向量化的表達了:
現在,我們根據上面幾個公式,重新實作一個類:FullConnectedLayer。它實作了全連接配接層的前向和後向計算:
# 全連接配接層實作類
class FullConnectedLayer(object):
def __init__(self, input_size, output_size,
activator):
\'\'\'
構造函數
input_size: 本層輸入向量的次元
output_size: 本層輸出向量的次元
activator: 激活函數
\'\'\'
self.input_size = input_size
self.output_size = output_size
self.activator = activator
# 權重數組W
self.W = np.random.uniform(-0.1, 0.1,
(output_size, input_size))
# 偏置項b
self.b = np.zeros((output_size, 1))
# 輸出向量
self.output = np.zeros((output_size, 1))
def forward(self, input_array):
\'\'\'
前向計算
input_array: 輸入向量,次元必須等于input_size
\'\'\'
# 式2
self.input = input_array
self.output = self.activator.forward(
np.dot(self.W, input_array) + self.b)
def backward(self, delta_array):
\'\'\'
反向計算W和b的梯度
delta_array: 從上一層傳遞過來的誤差項
\'\'\'
# 式8
self.delta = self.activator.backward(self.input) * np.dot(
self.W.T, delta_array)
self.W_grad = np.dot(delta_array, self.input.T)
self.b_grad = delta_array
def update(self, learning_rate):
\'\'\'
使用梯度下降算法更新權重
\'\'\'
self.W += learning_rate * self.W_grad
self.b += learning_rate * self.b_grad
上面這個類一舉取代了原先的Layer、Node、Connection等類,不但代碼更加容易了解,而且運作速度也快了幾百倍。
現在,我們對Network類稍作修改,使之用到FullConnectedLayer:
# Sigmoid激活函數類
class SigmoidActivator(object):
def forward(self, weighted_input):
return 1.0 / (1.0 + np.exp(-weighted_input))
def backward(self, output):
return output * (1 - output)
# 神經網絡類
class Network(object):
def __init__(self, layers):
\'\'\'
構造函數
\'\'\'
self.layers = []
for i in range(len(layers) - 1):
self.layers.append(
FullConnectedLayer(
layers[i], layers[i+1],
SigmoidActivator()
)
)
def predict(self, sample):
\'\'\'
使用神經網絡實作預測
sample: 輸入樣本
\'\'\'
output = sample
for layer in self.layers:
layer.forward(output)
output = layer.output
return output
def train(self, labels, data_set, rate, epoch):
\'\'\'
訓練函數
labels: 樣本标簽
data_set: 輸入樣本
rate: 學習速率
epoch: 訓練輪數
\'\'\'
for i in range(epoch):
for d in range(len(data_set)):
self.train_one_sample(labels[d],
data_set[d], rate)
def train_one_sample(self, label, sample, rate):
self.predict(sample)
self.calc_gradient(label)
self.update_weight(rate)
def calc_gradient(self, label):
delta = self.layers[-1].activator.backward(
self.layers[-1].output
) * (label - self.layers[-1].output)
for layer in self.layers[::-1]:
layer.backward(delta)
delta = layer.delta
return delta
def update_weight(self, rate):
for layer in self.layers:
layer.update(rate)
現在,Network類也清爽多了,用我們的新代碼再次訓練一下MNIST資料集吧。
小結
至此,你已經完成了又一次漫長的學習之旅。你現在應該已經明白了神經網絡的基本原理,高興的話,你甚至有能力去動手實作一個,并用它解決一些問題。如果感到困難也不要氣餒,這篇文章是一個重要的分水嶺,如果你完全弄明白了的話,在真正的『小白』和裝腔作勢的『大牛』面前吹吹牛是完全沒有問題的。
作為深度學習入門的系列文章,本文也是上半場的結束。在這個半場,你掌握了機器學習、神經網絡的基本概念,并且有能力去動手解決一些簡單的問題(例如手寫數字識别,如果用傳統的觀點來看,其實這些問題也不簡單)。而且,一旦掌握基本概念,後面的學習就容易多了。
在下半場,我們講介紹更多『深度』學習的内容,我們已經講了神經網絡(Neutrol Network),但是并沒有講深度神經網絡(Deep Neutrol Network)。Deep會帶來更加強大的能力,同時也帶來更多的問題。如果不了解這些問題和它們的解決方案,也不能說你入門了『深度』學習。
目前業界有很多開源的神經網絡實作,它們的功能也要強大的多,是以你并不需要事必躬親的去實作自己的神經網絡。我們在上半場不斷的從頭發明***,是為了讓你明白神經網絡的基本原理,這樣你就能非常迅速的掌握這些工具。在下半場的文章中,我們改變了政策:不會再去從頭開始去實作,而是盡可能應用現有的工具。
下一篇文章,我們介紹不同結構的神經網絡,比如鼎鼎大名的卷積神經網絡,它在圖像和語音領域已然創造了諸多奇迹,在自然語言處理領域的研究也如火如荼。某種意義上說,它的成功大大提升了人們對于深度學習的信心。