零基礎入門深度學習(3) - 神經網絡和反向傳播算法

往期回顧

在上一篇文章中,咱們已經掌握了機器學習的基本套路,對模型、目標函數、優化算法這些概念有了必定程度的理解,並且已經會訓練單個的感知器或者線性單元了。在這篇文章中,咱們將把這些單獨的單元按照必定的規則相互鏈接在一塊兒造成神經網絡,從而奇蹟般的得到了強大的學習能力。咱們還將介紹這種網絡的訓練算法:反向傳播算法。最後,咱們依然用代碼實現一個神經網絡。若是您能堅持到本文的結尾,將會看到咱們用本身實現的神經網絡去識別手寫數字。如今請作好準備,您即將雙手觸及到深度學習的大門。node

 

神經元

神經元和感知器本質上是同樣的,只不過咱們說感知器的時候,它的激活函數是階躍函數;而當咱們說神經元時,激活函數每每選擇爲sigmoid函數或tanh函數。以下圖所示:python

計算一個神經元的輸出的方法和計算一個感知器的輸出是同樣的。假設神經元的輸入是向量,權重向量是(偏置項是),激活函數是sigmoid函數,則其輸出git

 

 

 

sigmoid函數的定義以下:程序員

 

 

 

 

將其帶入前面的式子,獲得github

 

 

 

 

sigmoid函數是一個非線性函數,值域是(0,1)。函數圖像以下圖所示算法

sigmoid函數的導數是:編程

 

 

令則

 

能夠看到,sigmoid函數的導數很是有趣,它能夠用sigmoid函數自身來表示。這樣,一旦計算出sigmoid函數的值,計算它的導數的值就很是方便。數組

 

神經網絡是啥

神經網絡其實就是按照必定規則鏈接起來的多個神經元。上圖展現了一個全鏈接(full connected, FC)神經網絡,經過觀察上面的圖,咱們能夠發現它的規則包括:性能優化

  • 神經元按照層來佈局。最左邊的層叫作輸入層,負責接收輸入數據;最右邊的層叫輸出層,咱們能夠從這層獲取神經網絡輸出數據。輸入層和輸出層之間的層叫作隱藏層,由於它們對於外部來講是不可見的。
  • 同一層的神經元之間沒有鏈接。
  • 第N層的每一個神經元和第N-1層的全部神經元相連(這就是full connected的含義),第N-1層神經元的輸出就是第N層神經元的輸入。
  • 每一個鏈接都有一個權值。

上面這些規則定義了全鏈接神經網絡的結構。事實上還存在不少其它結構的神經網絡,好比卷積神經網絡(CNN)、循環神經網絡(RNN),他們都具備不一樣的鏈接規則。服務器

 

計算神經網絡的輸出

神經網絡實際上就是一個輸入向量到輸出向量的函數,即:

 

 

 

 

根據輸入計算神經網絡的輸出,須要首先將輸入向量的每一個元素的值賦給神經網絡的輸入層的對應神經元,而後根據式1依次向前計算每一層的每一個神經元的值,直到最後一層輸出層的全部神經元的值計算完畢。最後,將輸出層每一個神經元的值串在一塊兒就獲得了輸出向量

接下來舉一個例子來講明這個過程,咱們先給神經網絡的每一個單元寫上編號。

如上圖,輸入層有三個節點,咱們將其依次編號爲一、二、3;隱藏層的4個節點,編號依次爲四、五、六、7;最後輸出層的兩個節點編號爲八、9。由於咱們這個神經網絡是全鏈接網絡,因此能夠看到每一個節點都和上一層的全部節點有鏈接。好比,咱們能夠看到隱藏層的節點4,它和輸入層的三個節點一、二、3之間都有鏈接,其鏈接上的權重分別爲。那麼,咱們怎樣計算節點4的輸出值呢?

爲了計算節點4的輸出值,咱們必須先獲得其全部上游節點(也就是節點一、二、3)的輸出值。節點一、二、3是輸入層的節點,因此,他們的輸出值就是輸入向量自己。按照上圖畫出的對應關係,能夠看到節點一、二、3的輸出值分別是。咱們要求輸入向量的維度和輸入層神經元個數相同,而輸入向量的某個元素對應到哪一個輸入節點是能夠自由決定的,你偏非要把賦值給節點2也是徹底沒有問題的,但這樣除了把本身弄暈以外,並無什麼價值。

一旦咱們有了節點一、二、3的輸出值,咱們就能夠根據式1計算節點4的輸出值

 

 

 

 

上式的是節點4的偏置項,圖中沒有畫出來。而分別爲節點一、二、3到節點4鏈接的權重,在給權重編號時,咱們把目標節點的編號放在前面,把源節點的編號放在後面。

一樣,咱們能夠繼續計算出節點五、六、7的輸出值。這樣,隱藏層的4個節點的輸出值就計算完成了,咱們就能夠接着計算輸出層的節點8的輸出值

 

 

 

 

同理,咱們還能夠計算出的值。這樣輸出層全部節點的輸出值計算完畢,咱們就獲得了在輸入向量時,神經網絡的輸出向量。這裏咱們也看到,輸出向量的維度和輸出層神經元個數相同。

 

神經網絡的矩陣表示

神經網絡的計算若是用矩陣來表示會很方便(固然逼格也更高),咱們先來看看隱藏層的矩陣表示。

首先咱們把隱藏層4個節點的計算依次排列出來:

 

 

 

 

接着,定義網絡的輸入向量和隱藏層每一個節點的權重向量。令

 

 

 

 

代入到前面的一組式子,獲得:

 

 

 

 

如今,咱們把上述計算的四個式子寫到一個矩陣裏面,每一個式子做爲矩陣的一行,就能夠利用矩陣來表示它們的計算了。令

 

 

 

 

帶入前面的一組式子,獲得

 

 

 

在式2中,是激活函數,在本例中是函數;是某一層的權重矩陣;是某層的輸入向量;是某層的輸出向量。式2說明神經網絡的每一層的做用實際上就是先將輸入向量左乘一個數組進行線性變換,獲得一個新的向量,而後再對這個向量逐元素應用一個激活函數。

每一層的算法都是同樣的。好比,對於包含一個輸入層,一個輸出層和三個隱藏層的神經網絡,咱們假設其權重矩陣分別爲,每一個隱藏層的輸出分別是,神經網絡的輸入爲,神經網絡的輸入爲,以下圖所示:

則每一層的輸出向量的計算能夠表示爲:

 

 

 

 

這就是神經網絡輸出值的計算方法。

 

神經網絡的訓練

如今,咱們須要知道一個神經網絡的每一個鏈接上的權值是如何獲得的。咱們能夠說神經網絡是一個模型,那麼這些權值就是模型的參數,也就是模型要學習的東西。然而,一個神經網絡的鏈接方式、網絡的層數、每層的節點數這些參數,則不是學習出來的,而是人爲事先設置的。對於這些人爲設置的參數,咱們稱之爲超參數(Hyper-Parameters)。

接下來,咱們將要介紹神經網絡的訓練算法:反向傳播算法。

 

反向傳播算法(Back Propagation)

咱們首先直觀的介紹反向傳播算法,最後再來介紹這個算法的推導。固然讀者也能夠徹底跳過推導部分,由於即便不知道如何推導,也不影響你寫出來一個神經網絡的訓練代碼。事實上,如今神經網絡成熟的開源實現多如牛毛,除了練手以外,你可能都沒有機會須要去寫一個神經網絡。

咱們以監督學習爲例來解釋反向傳播算法。在零基礎入門深度學習(2) - 線性單元和梯度降低一文中咱們介紹了什麼是監督學習,若是忘記了能夠再看一下。另外,咱們設神經元的激活函數函數(不一樣激活函數的計算公式不一樣,詳情見反向傳播算法的推導一節)。

咱們假設每一個訓練樣本爲,其中向量是訓練樣本的特徵,而是樣本的目標值。

首先,咱們根據上一節介紹的算法,用樣本的特徵,計算出神經網絡中每一個隱藏層節點的輸出,以及輸出層每一個節點的輸出

而後,咱們按照下面的方法計算出每一個節點的偏差項

  • 對於輸出層節點
 

 

 

其中,是節點的偏差項,是節點的輸出值,是樣本對應於節點的目標值。舉個例子,根據上圖,對於輸出層節點8來講,它的輸出值是,而樣本的目標值是,帶入上面的公式獲得節點8的偏差項應該是:

 

 

 

 

  • 對於隱藏層節點,
 

 

 

其中,是節點的輸出值,是節點到它的下一層節點的鏈接的權重,是節點的下一層節點的偏差項。例如,對於隱藏層節點4來講,計算方法以下:

 

 

 

 

最後,更新每一個鏈接上的權值:

 

 

 

其中,是節點到節點的權重,是一個成爲學習速率的常數,是節點的偏差項,是節點傳遞給節點的輸入。例如,權重的更新方法以下:

 

 

 

 

相似的,權重的更新方法以下:

 

 

 

 

偏置項的輸入值永遠爲1。例如,節點4的偏置項應該按照下面的方法計算:

 

 

 

 

咱們已經介紹了神經網絡每一個節點偏差項的計算和權重更新方法。顯然,計算一個節點的偏差項,須要先計算每一個與其相連的下一層節點的偏差項。這就要求偏差項的計算順序必須是從輸出層開始,而後反向依次計算每一個隱藏層的偏差項,直到與輸入層相連的那個隱藏層。這就是反向傳播算法的名字的含義。當全部節點的偏差項計算完畢後,咱們就能夠根據式5來更新全部的權重。

以上就是基本的反向傳播算法,並非很複雜,您弄清楚了麼?

 

反向傳播算法的推導

反向傳播算法其實就是鏈式求導法則的應用。然而,這個如此簡單且顯而易見的方法,倒是在Roseblatt提出感知器算法將近30年以後才被髮明和普及的。對此,Bengio這樣迴應道:

不少看似顯而易見的想法只有在過後才變得顯而易見。

接下來,咱們用鏈式求導法則來推導反向傳播算法,也就是上一小節的式三、式四、式5。

前方高能預警——接下來是數學公式重災區,讀者能夠酌情閱讀,沒必要強求。

按照機器學習的通用套路,咱們先肯定神經網絡的目標函數,而後用隨機梯度降低優化算法去求目標函數最小值時的參數值。

咱們取網絡全部輸出層節點的偏差平方和做爲目標函數:

 

 

 

 

其中,表示是樣本的偏差。

而後,咱們用文章零基礎入門深度學習(2) - 線性單元和梯度降低中介紹的隨機梯度降低算法對目標函數進行優化:

 

 

 

 

隨機梯度降低算法也就是須要求出偏差對於每一個權重的偏導數(也就是梯度),怎麼求呢?

觀察上圖,咱們發現權重僅能經過影響節點的輸入值影響網絡的其它部分,設是節點的加權輸入,即

 

 

 

 

的函數,而的函數。根據鏈式求導法則,能夠獲得:

 

 

 


上式中,是節點傳遞給節點的輸入值,也就是節點的輸出值。

 

對於的推導,須要區分輸出層和隱藏層兩種狀況。

 

輸出層權值訓練

對於輸出層來講,僅能經過節點的輸出值來影響網絡其它部分,也就是說的函數,而的函數,其中。因此咱們能夠再次使用鏈式求導法則:

 

 

 

 

考慮上式第一項:

 

 

 

 

考慮上式第二項:

 

 

 

 

將第一項和第二項帶入,獲得:

 

 

 

 

若是令,也就是一個節點的偏差項是網絡偏差對這個節點輸入的偏導數的相反數。帶入上式,獲得:

 

 

 

 

上式就是式3。

將上述推導帶入隨機梯度降低公式,獲得:

 

 

 

 

上式就是式5。

 

隱藏層權值訓練

如今咱們要推導出隱藏層的

首先,咱們須要定義節點的全部直接下游節點的集合。例如,對於節點4來講,它的直接下游節點是節點八、節點9。能夠看到只能經過影響再影響。設是節點的下游節點的輸入,則的函數,而的函數。由於有多個,咱們應用全導數公式,能夠作出以下推導:

 

 

 

 

由於,帶入上式獲得:

 

 

 

 

上式就是式4。

——數學公式警報解除——

至此,咱們已經推導出了反向傳播算法。須要注意的是,咱們剛剛推導出的訓練規則是根據激活函數是sigmoid函數、平方和偏差、全鏈接網絡、隨機梯度降低優化算法。若是激活函數不一樣、偏差計算方式不一樣、網絡鏈接結構不一樣、優化算法不一樣,則具體的訓練規則也會不同。可是不管怎樣,訓練規則的推導方式都是同樣的,應用鏈式求導法則進行推導便可。

 

神經網絡的實現

完整代碼請參考GitHub: https://github.com/hanbt/learn_dl/blob/master/bp.py (python2.7)

如今,咱們要根據前面的算法,實現一個基本的全鏈接神經網絡,這並不須要太多代碼。咱們在這裏依然採用面向對象設計。

首先,咱們先作一個基本的模型:

如上圖,能夠分解出5個領域對象來實現神經網絡:

  • Network 神經網絡對象,提供API接口。它由若干層對象組成以及鏈接對象組成。
  • Layer 層對象,由多個節點組成。
  • Node 節點對象計算和記錄節點自身的信息(好比輸出值、偏差項等),以及與這個節點相關的上下游的鏈接。
  • Connection 每一個鏈接對象都要記錄該鏈接的權重。
  • Connections 僅僅做爲Connection的集合對象,提供一些集合操做。

Node實現以下:

 
  1. # 節點類,負責記錄和維護節點自身信息以及與這個節點相關的上下游鏈接,實現輸出值和偏差項的計算。
  2. class Node(object):
  3. def __init__(self, layer_index, node_index):
  4. '''
  5. 構造節點對象。
  6. layer_index: 節點所屬的層的編號
  7. node_index: 節點的編號
  8. '''
  9. self.layer_index = layer_index
  10. self.node_index = node_index
  11. self.downstream = []
  12. self.upstream = []
  13. self.output = 0
  14. self.delta = 0
  15. def set_output(self, output):
  16. '''
  17. 設置節點的輸出值。若是節點屬於輸入層會用到這個函數。
  18. '''
  19. self.output = output
  20. def append_downstream_connection(self, conn):
  21. '''
  22. 添加一個到下游節點的鏈接
  23. '''
  24. self.downstream.append(conn)
  25. def append_upstream_connection(self, conn):
  26. '''
  27. 添加一個到上游節點的鏈接
  28. '''
  29. self.upstream.append(conn)
  30. def calc_output(self):
  31. '''
  32. 根據式1計算節點的輸出
  33. '''
  34. output = reduce(lambda ret, conn: ret + conn.upstream_node.output * conn.weight, self.upstream, 0)
  35. self.output = sigmoid(output)
  36. def calc_hidden_layer_delta(self):
  37. '''
  38. 節點屬於隱藏層時,根據式4計算delta
  39. '''
  40. downstream_delta = reduce(
  41. lambda ret, conn: ret + conn.downstream_node.delta * conn.weight,
  42. self.downstream, 0.0)
  43. self.delta = self.output * (1 - self.output) * downstream_delta
  44. def calc_output_layer_delta(self, label):
  45. '''
  46. 節點屬於輸出層時,根據式3計算delta
  47. '''
  48. self.delta = self.output * (1 - self.output) * (label - self.output)
  49. def __str__(self):
  50. '''
  51. 打印節點的信息
  52. '''
  53. node_str = '%u-%u: output: %f delta: %f' % (self.layer_index, self.node_index, self.output, self.delta)
  54. downstream_str = reduce(lambda ret, conn: ret + '\n\t' + str(conn), self.downstream, '')
  55. upstream_str = reduce(lambda ret, conn: ret + '\n\t' + str(conn), self.upstream, '')
  56. return node_str + '\n\tdownstream:' + downstream_str + '\n\tupstream:' + upstream_str

ConstNode對象,爲了實現一個輸出恆爲1的節點(計算偏置項時須要)

 
  1. class ConstNode(object):
  2. def __init__(self, layer_index, node_index):
  3. '''
  4. 構造節點對象。
  5. layer_index: 節點所屬的層的編號
  6. node_index: 節點的編號
  7. '''
  8. self.layer_index = layer_index
  9. self.node_index = node_index
  10. self.downstream = []
  11. self.output = 1
  12. def append_downstream_connection(self, conn):
  13. '''
  14. 添加一個到下游節點的鏈接
  15. '''
  16. self.downstream.append(conn)
  17. def calc_hidden_layer_delta(self):
  18. '''
  19. 節點屬於隱藏層時,根據式4計算delta
  20. '''
  21. downstream_delta = reduce(
  22. lambda ret, conn: ret + conn.downstream_node.delta * conn.weight,
  23. self.downstream, 0.0)
  24. self.delta = self.output * (1 - self.output) * downstream_delta
  25. def __str__(self):
  26. '''
  27. 打印節點的信息
  28. '''
  29. node_str = '%u-%u: output: 1' % (self.layer_index, self.node_index)
  30. downstream_str = reduce(lambda ret, conn: ret + '\n\t' + str(conn), self.downstream, '')
  31. return node_str + '\n\tdownstream:' + downstream_str

Layer對象,負責初始化一層。此外,做爲Node的集合對象,提供對Node集合的操做。

 
  1. class Layer(object):
  2. def __init__(self, layer_index, node_count):
  3. '''
  4. 初始化一層
  5. layer_index: 層編號
  6. node_count: 層所包含的節點個數
  7. '''
  8. self.layer_index = layer_index
  9. self.nodes = []
  10. for i in range(node_count):
  11. self.nodes.append(Node(layer_index, i))
  12. self.nodes.append(ConstNode(layer_index, node_count))
  13. def set_output(self, data):
  14. '''
  15. 設置層的輸出。當層是輸入層時會用到。
  16. '''
  17. for i in range(len(data)):
  18. self.nodes[i].set_output(data[i])
  19. def calc_output(self):
  20. '''
  21. 計算層的輸出向量
  22. '''
  23. for node in self.nodes[:-1]:
  24. node.calc_output()
  25. def dump(self):
  26. '''
  27. 打印層的信息
  28. '''
  29. for node in self.nodes:
  30. print node

Connection對象,主要職責是記錄鏈接的權重,以及這個鏈接所關聯的上下游節點。

 
  1. class Connection(object):
  2. def __init__(self, upstream_node, downstream_node):
  3. '''
  4. 初始化鏈接,權重初始化爲是一個很小的隨機數
  5. upstream_node: 鏈接的上游節點
  6. downstream_node: 鏈接的下游節點
  7. '''
  8. self.upstream_node = upstream_node
  9. self.downstream_node = downstream_node
  10. self.weight = random.uniform(-0.1, 0.1)
  11. self.gradient = 0.0
  12. def calc_gradient(self):
  13. '''
  14. 計算梯度
  15. '''
  16. self.gradient = self.downstream_node.delta * self.upstream_node.output
  17. def get_gradient(self):
  18. '''
  19. 獲取當前的梯度
  20. '''
  21. return self.gradient
  22. def update_weight(self, rate):
  23. '''
  24. 根據梯度降低算法更新權重
  25. '''
  26. self.calc_gradient()
  27. self.weight += rate * self.gradient
  28. def __str__(self):
  29. '''
  30. 打印鏈接信息
  31. '''
  32. return '(%u-%u) -> (%u-%u) = %f' % (
  33. self.upstream_node.layer_index,
  34. self.upstream_node.node_index,
  35. self.downstream_node.layer_index,
  36. self.downstream_node.node_index,
  37. self.weight)

Connections對象,提供Connection集合操做。

 
  1. class Connections(object):
  2. def __init__(self):
  3. self.connections = []
  4. def add_connection(self, connection):
  5. self.connections.append(connection)
  6. def dump(self):
  7. for conn in self.connections:
  8. print conn

Network對象,提供API。

 
  1. class Network(object):
  2. def __init__(self, layers):
  3. '''
  4. 初始化一個全鏈接神經網絡
  5. layers: 二維數組,描述神經網絡每層節點數
  6. '''
  7. self.connections = Connections()
  8. self.layers = []
  9. layer_count = len(layers)
  10. node_count = 0;
  11. for i in range(layer_count):
  12. self.layers.append(Layer(i, layers[i]))
  13. for layer in range(layer_count - 1):
  14. connections = [Connection(upstream_node, downstream_node)
  15. for upstream_node in self.layers[layer].nodes
  16. for downstream_node in self.layers[layer + 1].nodes[:-1]]
  17. for conn in connections:
  18. self.connections.add_connection(conn)
  19. conn.downstream_node.append_upstream_connection(conn)
  20. conn.upstream_node.append_downstream_connection(conn)
  21. def train(self, labels, data_set, rate, iteration):
  22. '''
  23. 訓練神經網絡
  24. labels: 數組,訓練樣本標籤。每一個元素是一個樣本的標籤。
  25. data_set: 二維數組,訓練樣本特徵。每一個元素是一個樣本的特徵。
  26. '''
  27. for i in range(iteration):
  28. for d in range(len(data_set)):
  29. self.train_one_sample(labels[d], data_set[d], rate)
  30. def train_one_sample(self, label, sample, rate):
  31. '''
  32. 內部函數,用一個樣本訓練網絡
  33. '''
  34. self.predict(sample)
  35. self.calc_delta(label)
  36. self.update_weight(rate)
  37. def calc_delta(self, label):
  38. '''
  39. 內部函數,計算每一個節點的delta
  40. '''
  41. output_nodes = self.layers[-1].nodes
  42. for i in range(len(label)):
  43. output_nodes[i].calc_output_layer_delta(label[i])
  44. for layer in self.layers[-2::-1]:
  45. for node in layer.nodes:
  46. node.calc_hidden_layer_delta()
  47. def update_weight(self, rate):
  48. '''
  49. 內部函數,更新每一個鏈接權重
  50. '''
  51. for layer in self.layers[:-1]:
  52. for node in layer.nodes:
  53. for conn in node.downstream:
  54. conn.update_weight(rate)
  55. def calc_gradient(self):
  56. '''
  57. 內部函數,計算每一個鏈接的梯度
  58. '''
  59. for layer in self.layers[:-1]:
  60. for node in layer.nodes:
  61. for conn in node.downstream:
  62. conn.calc_gradient()
  63. def get_gradient(self, label, sample):
  64. '''
  65. 得到網絡在一個樣本下,每一個鏈接上的梯度
  66. label: 樣本標籤
  67. sample: 樣本輸入
  68. '''
  69. self.predict(sample)
  70. self.calc_delta(label)
  71. self.calc_gradient()
  72. def predict(self, sample):
  73. '''
  74. 根據輸入的樣本預測輸出值
  75. sample: 數組,樣本的特徵,也就是網絡的輸入向量
  76. '''
  77. self.layers[0].set_output(sample)
  78. for i in range(1, len(self.layers)):
  79. self.layers[i].calc_output()
  80. return map(lambda node: node.output, self.layers[-1].nodes[:-1])
  81. def dump(self):
  82. '''
  83. 打印網絡信息
  84. '''
  85. for layer in self.layers:
  86. layer.dump()

至此,實現了一個基本的全鏈接神經網絡。能夠看到,同神經網絡的強大學習能力相比,其實現還算是很容易的。

 

梯度檢查

怎麼保證本身寫的神經網絡沒有BUG呢?事實上這是一個很是重要的問題。一方面,千辛萬苦想到一個算法,結果效果不理想,那麼是算法自己錯了仍是代碼實現錯了呢?定位這種問題確定要花費大量的時間和精力。另外一方面,因爲神經網絡的複雜性,咱們幾乎沒法事先知道神經網絡的輸入和輸出,所以相似TDD(測試驅動開發)這樣的開發方法彷佛也不可行。

辦法仍是有滴,就是利用梯度檢查來確認程序是否正確。梯度檢查的思路以下:

對於梯度降低算法:

 

 

 

 

來講,這裏關鍵之處在於的計算必定要正確,而它是偏導數。而根據導數的定義:

 

 

 

 

對於任意的導數值,咱們均可以用等式右邊來近似計算。咱們把看作是的函數,即,那麼根據導數定義,應該等於:

 

 

 

 

若是把設置爲一個很小的數(好比),那麼上式能夠寫成:

 

 

 

咱們就能夠利用式6,來計算梯度的值,而後同咱們神經網絡代碼中計算出來的梯度值進行比較。若是二者的差異很是的小,那麼就說明咱們的代碼是正確的。

下面是梯度檢查的代碼。若是咱們想檢查參數的梯度是否正確,咱們須要如下幾個步驟:

  1. 首先使用一個樣本對神經網絡進行訓練,這樣就能得到每一個權重的梯度。
  2. 加上一個很小的值(),從新計算神經網絡在這個樣本下的
  3. 減上一個很小的值(),從新計算神經網絡在這個樣本下的
  4. 根據式6計算出指望的梯度值,和第一步得到的梯度值進行比較,它們應該幾乎想等(至少4位有效數字相同)。

固然,咱們能夠重複上面的過程,對每一個權重都進行檢查。也可使用多個樣本重複檢查。

 
  1. def gradient_check(network, sample_feature, sample_label):
  2. '''
  3. 梯度檢查
  4. network: 神經網絡對象
  5. sample_feature: 樣本的特徵
  6. sample_label: 樣本的標籤
  7. '''
  8. # 計算網絡偏差
  9. network_error = lambda vec1, vec2: \
  10. 0.5 * reduce(lambda a, b: a + b,
  11. map(lambda v: (v[0] - v[1]) * (v[0] - v[1]),
  12. zip(vec1, vec2)))
  13. # 獲取網絡在當前樣本下每一個鏈接的梯度
  14. network.get_gradient(sample_feature, sample_label)
  15. # 對每一個權重作梯度檢查
  16. for conn in network.connections.connections:
  17. # 獲取指定鏈接的梯度
  18. actual_gradient = conn.get_gradient()
  19. # 增長一個很小的值,計算網絡的偏差
  20. epsilon = 0.0001
  21. conn.weight += epsilon
  22. error1 = network_error(network.predict(sample_feature), sample_label)
  23. # 減去一個很小的值,計算網絡的偏差
  24. conn.weight -= 2 * epsilon # 剛纔加過了一次,所以這裏須要減去2倍
  25. error2 = network_error(network.predict(sample_feature), sample_label)
  26. # 根據式6計算指望的梯度值
  27. expected_gradient = (error2 - error1) / (2 * epsilon)
  28. # 打印
  29. print 'expected gradient: \t%f\nactual gradient: \t%f' % (
  30. expected_gradient, actual_gradient)

至此,會推導、會實現、會抓BUG,你已經摸到深度學習的大門了。接下來還須要不斷的實踐,咱們用剛剛寫過的神經網絡去識別手寫數字。

 

神經網絡實戰——手寫數字識別

針對這個任務,咱們採用業界很是流行的MNIST數據集。MNIST大約有60000個手寫字母的訓練樣本,咱們使用它訓練咱們的神經網絡,而後再用訓練好的網絡去識別手寫數字。

手寫數字識別是個比較簡單的任務,數字只多是0-9中的一個,這是個10分類問題。

 

超參數的肯定

咱們首先須要肯定網絡的層數和每層的節點數。關於第一個問題,實際上並無什麼理論化的方法,你們都是根據經驗來拍,若是沒有經驗的話就隨便拍一個。而後,你能夠多試幾個值,訓練不一樣層數的神經網絡,看看哪一個效果最好就用哪一個。嗯,如今你可能明白爲何說深度學習是個手藝活了,有些手藝很讓人無語,而有些手藝仍是頗有技術含量的。

不過,有些基本道理咱們仍是明白的,咱們知道網絡層數越多越好,也知道層數越多訓練難度越大。對於全鏈接網絡,隱藏層最好不要超過三層。那麼,咱們能夠先試試僅有一個隱藏層的神經網絡效果怎麼樣。畢竟模型小的話,訓練起來也快些(剛開始玩模型的時候,都但願快點看到結果)。

輸入層節點數是肯定的。由於MNIST數據集每一個訓練數據是28*28的圖片,共784個像素,所以,輸入層節點數應該是784,每一個像素對應一個輸入節點。

輸出層節點數也是肯定的。由於是10分類,咱們能夠用10個節點,每一個節點對應一個分類。輸出層10個節點中,輸出最大值的那個節點對應的分類,就是模型的預測結果。

隱藏層節點數量是很差肯定的,從1到100萬均可以。下面有幾個經驗公式:

 

 

隱藏層節點數輸入層節點數輸出層節點數到之間的常數

 

所以,咱們能夠先根據上面的公式設置一個隱藏層節點數。若是有時間,咱們能夠設置不一樣的節點數,分別訓練,看看哪一個效果最好就用哪一個。咱們先拍一個,設隱藏層節點數爲300吧。

對於3層的全鏈接網絡,總共有個參數!神經網絡之因此強大,是它提供了一種很是簡單的方法去實現大量的參數。目前百億參數、千億樣本的超大規模神經網絡也是有的。由於MNIST只有6萬個訓練樣本,參數太多了很容易過擬合,效果反而很差。

 

模型的訓練和評估

MNIST數據集包含10000個測試樣本。咱們先用60000個訓練樣本訓練咱們的網絡,而後再用測試樣本對網絡進行測試,計算識別錯誤率:

 

 

錯誤率錯誤預測樣本數總樣本數

 

咱們每訓練10輪,評估一次準確率。當準確率開始降低時(出現了過擬合)終止訓練。

 

代碼實現

首先,咱們須要把MNIST數據集處理爲神經網絡可以接受的形式。MNIST訓練集的文件格式能夠參考官方網站,這裏不在贅述。每一個訓練樣本是一個28*28的圖像,咱們按照行優先,把它轉化爲一個784維的向量。每一個標籤是0-9的值,咱們將其轉換爲一個10維的one-hot向量:若是標籤值爲,咱們就把向量的第維(從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數據的代碼:

 
  1. #!/usr/bin/env python
  2. # -*- coding: UTF-8 -*-
  3. import struct
  4. from bp import *
  5. from datetime import datetime
  6. # 數據加載器基類
  7. class Loader(object):
  8. def __init__(self, path, count):
  9. '''
  10. 初始化加載器
  11. path: 數據文件路徑
  12. count: 文件中的樣本個數
  13. '''
  14. self.path = path
  15. self.count = count
  16. def get_file_content(self):
  17. '''
  18. 讀取文件內容
  19. '''
  20. f = open(self.path, 'rb')
  21. content = f.read()
  22. f.close()
  23. return content
  24. def to_int(self, byte):
  25. '''
  26. 將unsigned byte字符轉換爲整數
  27. '''
  28. return struct.unpack('B', byte)[0]
  29. # 圖像數據加載器
  30. class ImageLoader(Loader):
  31. def get_picture(self, content, index):
  32. '''
  33. 內部函數,從文件中獲取圖像
  34. '''
  35. start = index * 28 * 28 + 16
  36. picture = []
  37. for i in range(28):
  38. picture.append([])
  39. for j in range(28):
  40. picture[i].append(
  41. self.to_int(content[start + i * 28 + j]))
  42. return picture
  43. def get_one_sample(self, picture):
  44. '''
  45. 內部函數,將圖像轉化爲樣本的輸入向量
  46. '''
  47. sample = []
  48. for i in range(28):
  49. for j in range(28):
  50. sample.append(picture[i][j])
  51. return sample
  52. def load(self):
  53. '''
  54. 加載數據文件,得到所有樣本的輸入向量
  55. '''
  56. content = self.get_file_content()
  57. data_set = []
  58. for index in range(self.count):
  59. data_set.append(
  60. self.get_one_sample(
  61. self.get_picture(content, index)))
  62. return data_set
  63. # 標籤數據加載器
  64. class LabelLoader(Loader):
  65. def load(self):
  66. '''
  67. 加載數據文件,得到所有樣本的標籤向量
  68. '''
  69. content = self.get_file_content()
  70. labels = []
  71. for index in range(self.count):
  72. labels.append(self.norm(content[index + 8]))
  73. return labels
  74. def norm(self, label):
  75. '''
  76. 內部函數,將一個值轉換爲10維標籤向量
  77. '''
  78. label_vec = []
  79. label_value = self.to_int(label)
  80. for i in range(10):
  81. if i == label_value:
  82. label_vec.append(0.9)
  83. else:
  84. label_vec.append(0.1)
  85. return label_vec
  86. def get_training_data_set():
  87. '''
  88. 得到訓練數據集
  89. '''
  90. image_loader = ImageLoader('train-images-idx3-ubyte', 60000)
  91. label_loader = LabelLoader('train-labels-idx1-ubyte', 60000)
  92. return image_loader.load(), label_loader.load()
  93. def get_test_data_set():
  94. '''
  95. 得到測試數據集
  96. '''
  97. image_loader = ImageLoader('t10k-images-idx3-ubyte', 10000)
  98. label_loader = LabelLoader('t10k-labels-idx1-ubyte', 10000)
  99. return image_loader.load(), label_loader.load()

網絡的輸出是一個10維向量,這個向量第個(從0開始編號)元素的值最大,那麼就是網絡的識別結果。下面是代碼實現:

 
  1. def get_result(vec):
  2. max_value_index = 0
  3. max_value = 0
  4. for i in range(len(vec)):
  5. if vec[i] > max_value:
  6. max_value = vec[i]
  7. max_value_index = i
  8. return max_value_index

咱們使用錯誤率來對網絡進行評估,下面是代碼實現:

 
  1. def evaluate(network, test_data_set, test_labels):
  2. error = 0
  3. total = len(test_data_set)
  4. for i in range(total):
  5. label = get_result(test_labels[i])
  6. predict = get_result(network.predict(test_data_set[i]))
  7. if label != predict:
  8. error += 1
  9. 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輪,就把得到參數值保存在磁盤上,以便後續能夠恢復。(代碼略)

 

向量化編程

完整代碼請參考GitHub: https://github.com/hanbt/learn_dl/blob/master/fc.py (python2.7)

在經歷了漫長的訓練以後,咱們可能會想到,確定有更好的辦法!是的,程序員們,如今咱們須要告別面向對象編程了,轉而去使用另一種更適合深度學習算法的編程方式:向量化編程。主要有兩個緣由:一個是咱們事實上並不須要真的去定義Node、Connection這樣的對象,直接把數學計算實現了就能夠了;另外一個緣由,是底層算法庫會針對向量運算作優化(甚至有專用的硬件,好比GPU),程序效率會提高不少。因此,在深度學習的世界裏,咱們總會想法設法的把計算表達爲向量的形式。我相信優秀的程序員不會把本身拘泥於某種(本身熟悉的)編程範式上,而會去學習並使用最爲合適的範式。

下面,咱們用向量化編程的方法,從新實現前面的全鏈接神經網絡。

首先,咱們須要把全部的計算都表達爲向量的形式。對於全鏈接神經網絡來講,主要有三個計算公式。

前向計算,咱們發現式2已是向量化的表達了:

 

 

 

上式中的表示sigmoid函數。

反向計算,咱們須要把式3和式4使用向量來表示:

 

 

式式

 

在式8中,表示第l層的偏差項;表示矩陣的轉置。

咱們還須要權重數組W和偏置項b的梯度計算的向量化表示。也就是須要把式5使用向量化表示:

 

 

 

其對應的向量化表示爲:

 

 

 

更新偏置項的向量化表示爲:

 

 

 

如今,咱們根據上面幾個公式,從新實現一個類: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會帶來更增強大的能力,同時也帶來更多的問題。若是不理解這些問題和它們的解決方案,也不能說你入門了『深度』學習。

目前業界有不少開源的神經網絡實現,它們的功能也要強大的多,所以你並不須要事必躬親的去實現本身的神經網絡。咱們在上半場不斷的從頭髮明輪子,是爲了讓你明白神經網絡的基本原理,這樣你就能很是迅速的掌握這些工具。在下半場的文章中,咱們改變了策略:不會再去從頭開始去實現,而是儘量應用現有的工具。

下一篇文章,咱們介紹不一樣結構的神經網絡,好比鼎鼎大名的卷積神經網絡,它在圖像和語音領域已然創造了諸多奇蹟,在天然語言處理領域的研究也如火如荼。某種意義上說,它的成功大大提高了人們對於深度學習的信心。

好了,同窗們累了吧,奉上美圖一張,放鬆一下心情!

 

參考資料

    1. Tom M. Mitchell, "機器學習", 曾華軍等譯, 機械工業出版社
    2. CS 224N / Ling 284, Neural Networks for Named Entity Recognition
    3. LeCun et al. Gradient-Based Learning Applied to Document Recognition 1998
    4. 轉載自:https://www.zybuluo.com/hanbingtao/note/476663
相關文章
相關標籤/搜索