《神經網路和深度學習》系列文章七:實現我們的神經網路來分類數字

哈工大SCIR發表於2016-02-01

出處:Michael Nielsen的“神經網路與深層次的書”。本節譯者:哈工大SCIR碩士生鄧文超(https://github.com/dengwc1993  。


目錄

1、使用神經網路識別手寫數字

  • 感知機
  • 乙狀結腸神經元
  • 神經網路的結構
  • 用簡單的網路結構解決手寫數字識別
  • 通過梯度下降法學習引數
  • 實現我們的網路來分類數字
  • 關於深度學習

2,反向傳播演算法是如何工作的

3,改進神經網路的學習方法

4,神經網路能夠計算任意函式的視覺證明

5,為什麼深度神經網路的訓練是困難的


實現我們的神經網路來分類數字

好吧,現在讓我們寫一個學習怎麼樣識別手寫數字的程式,使用隨機梯度下降法和MNIST訓練資料。我們需要做的第一件事情是獲取MNIST資料。如果你是一個git的使用者,那麼你能夠通過克隆這本書的程式碼倉庫獲得資料,


git clone https://github.com/mnielsen/neural-networks-and-deep-learning.git


如果你不使用git的,那麼你能夠在這裡下載資料和程式碼。


順便說一下,當我在之前描述MNIST資料時,我說它分成了60000個訓練影象和萬個測試影象。這是官方的MNIST的描述。實際上,我們將用稍微不同的方法對資料進行分割我們將測試集保護原樣,但將將60,000個影象的MNIST訓練集分成兩個部分:一部分50,000個影象,我們將用訓練我們的神經網路和一個單獨的10,000個影象的驗證集)在本章中我們不使用驗證資料,但是在本書的後面我們將會發現它對於解決如何去設定神經網路中的超引數(超引數) - 。例如學習率等不是被我們的學習演算法直接選擇的引數。 - 是很有用的儘管驗證資料集不是原始MNIST規範的一部分,然而許多人使用以這種方式使用MNIST,並且在神經網路中使用驗證資料是很常見的當我從現在起提到「MNIST訓練資料」,我指的不是原始的60000影象資料集,而是我們 50000影象資料集1。


1如前所述,MNIST資料集是基於NIST(美國國家標準與技術研究院)收集的兩個資料集合。為了構建MNIST,NIST資料集合被Yann LeCun,Corinna Cortes和Christopher JC Burges拆分放入一個更方便的格式。更多細節請看這個連結。我的倉庫中的資料集是在一種更容易在Python的中載入和操縱MNIST資料的形式。我從蒙特利爾大學的LISA機器學習實驗室獲得了這個特殊格式的資料(連結)。


除了MNIST資料之外,我們還需要一個叫做Numpy的用於處理快速線性代數的Python庫。如果你沒有安裝過Numpy,你能夠在這裡獲得。在給出一個完整的清單之前,讓我解釋一下神經網路程式碼的核心特徵,如下核心是一個網路類,我們用來表示一個神經網路這是我們用來初始化一個網路物件的程式碼。


class Network(object):
def __init__(self, sizes):
self.num_layers = len(sizes)
self.sizes = sizes
self.biases = [np.random.randn(y, 1)
for y in sizes[1:]]
self.weights = [np.random.randn(y, x)
for x, y in zip(sizes[:-1], sizes[1:])]


在這段程式碼中,列表。大小包含各層的神經元的數量因此舉個例子,如果我們想建立一個在第一層有2個神經元,第二層有3個神經元,最後層有1個神經元的網路物件,我們應這樣寫程式碼:


net = Network([2,3,1])


網路物件的偏差和權重都是被隨機初始化的,使用numpy的的np.random.randn函式來生成均值為0,標準差為1的高斯分佈。隨機初始化給了我們的隨機梯度下降演算法一個起點。在後面的章節中我們將會發現更好的初始化權重和偏差的方法,但是現在將採用隨機初始化。注意網路初始化程式碼假設第一層神經元是一個輸入層,並對這些神經元不設定任何偏差,因為偏差僅在之後的層中使用。


同樣注意,偏差和權重以列表儲存在numpy的矩陣中。因此例如net.weights [1]是一個儲存著連線第二層和第三層神經元權重的numpy的矩陣(不是第一層和第二層,因為Python列中的索引從0開始。)因此net.weights [1]相當冗長,讓我們就這樣表示矩陣w。矩陣中的wjk是連線第二層的第k神經元和第三層的第j神經元的權重。這種Ĵ和ķ索引的順序可能看著奇怪,當然交換Ĵ和ķ索引會更有意義?使用這種順序的很大的優勢是它意味著第三層神經元的啟用向量是


《神經網路和深度學習》系列文章七:實現我們的神經網路來分類數字


這個等式有點複雜,所以讓我們一塊一塊地進行理解.A是第二層神經元的啟用向量。為了得到一個',我們用權重矩陣瓦特乘以一個,加上偏差向量B,我們然後對向量WA + b中的每個元素應用函式σ(這被叫做函式σ的向量化(向量化)。)很容易驗證等式(22)給出了跟我們之前的計算一個S形神經元的輸出的等式(4)的結果相同。


練習

寫出等式(22)的組成形式,並驗證它跟我們之前的計算一個sigmoid神經​​元的輸出的等式(4)的結果相同。有了這些,很容易寫從一個網路例項計算輸出的程式碼。我們從定義乙狀結腸函式開始:


def sigmoid(z):
return 1.0/(1.0+np.exp(-z))


注意,當輸入ž是一個向量或者numpy的陣列時,NumPy的自動的應用元素級的S形函式,也就是向量化。


我們然後對網路類新增一個前饋方法,對於網路給定一個輸入一個,返回對應的輸出2.這個方法所做的是對每一層應用等式(22):


假定輸入一個Numpy的n維陣列(n,1),而不是向量(n),這裡,n是輸入到網路的數目,如果你試著用一個向量(n,)作為輸入,將會得到奇怪的結果。雖然使用向量(N,)看上去好像是更自然的選擇,但是使用ñ維陣列(N,1)使它特別容易的修改程式碼來立刻前饋多層輸入,並且有的時候這很方便。


def feedforward(self, a):
"""Return the output of the network if "a" is input."""
for b, w in zip(self.biases, self.weights):
a = sigmoid(np.dot(w, a)+b)
return a


當然,我們想要我們的網路物件做的主要事情是學習。為此我們給它們一個實現隨機梯度下降的SGD方法。下面是這部分的程式碼。在一些地方有一點神祕,我會在下面將它分解。


def SGD(self, training_data, epochs, mini_batch_size, eta, test_data=None):

"""Train the neural network using mini-batch stochastic gradient descent. The "training_data" is a list of tuples "(x, y)" representing the training inputs and the desired outputs. The other non-optional parameters are self-explanatory. If "test_data" is provided then the network will be evaluated against the test data after each epoch, and partial progress printed out. This is useful for tracking progress, but slows things down substantially."""

if test_data: n_test = len(test_data)
n = len(training_data)
for j in xrange(epochs):
random.shuffle(training_data)
mini_batches = [training_data[k:k+mini_batch_size]
for k in xrange(0, n, mini_batch_size)]
for mini_batch in mini_batches:
self.update_mini_batch(mini_batch, eta)
if test_data:
print "Epoch {0}: {1} / {2}".format(j, self.evaluate(test_data), n_test)
else:
print "Epoch {0} complete".format(j)


trainingdata是一個代表著訓練輸入和對應的期望輸出的元組(X,Y)的列表。變數曆元和minibatchsize是你期望的訓練的迭代次數和取樣時所用的小批量塊的大小.eta是學習率η。如果可選引數TESTDATA被提供,那麼程式將會在每次訓練迭代之後評價網路,並輸出我們的區域性進展。這對於跟蹤進展是有用的,但是大幅度降低速度。


程式碼如下工作。在每次迭代,它首先隨機的將訓練資料打亂,然後將它分成適當大小的迷你塊。這是一個簡單的從訓練資料的隨機取樣方法。然後對於每一個minibatch我們應用一次這是通過程式碼self.updateminibatch(minibatch,eta)做,只是使用minibatch上的訓練資料,根據單輪梯度下降更新網路的權重和偏差。這是update_mini_batch方法的程式碼:


def update_mini_batch(self, mini_batch, eta):

"""Update the network's weights and biases by applying gradient descent using backpropagation to a single mini batch. The "mini_batch" is a list of tuples "(x, y)", and "eta" is the learning rate."""

nabla_b = [np.zeros(b.shape) for b in self.biases]
nabla_w = [np.zeros(w.shape) for w in self.weights]
for x, y in mini_batch:
delta_nabla_b, delta_nabla_w = self.backprop(x, y)
nabla_b = [nb+dnb for nb, dnb in zip(nabla_b, delta_nabla_b)]
nabla_w = [nw+dnw for nw, dnw in zip(nabla_w, delta_nabla_w)]
self.weights = [w-(eta/len(mini_batch))*nw for w, nw in zip(self.weights, nabla_w)]
self.biases = [b-(eta/len(mini_batch))*nb for b, nb in zip(self.biases, nabla_b)]


大部分工作通過這行所做


deltanablab,deltanablaw = self.backprop(x,y)


這行呼叫了一個叫做叫反向傳播(反向傳播)的演算法,這是一種快速計算代價函式的梯度的方法。因此update_minibatch的工作僅僅是對minibatch中的每一個訓練樣例計算梯度,然後適當的更新self.weights狀語從句:常self.biases。


我現在不會展示self.backprop的程式碼。我們將在下章中學習反向傳播是怎樣工作的,包括self.backprop的程式碼。現在,就假設它可以如此工作,返回與訓練樣例X相關代價的適當梯度。


讓我們看一下完整的程式,包括我之前忽略的文件註釋。除了self.backprop,程式是不需加以說明的。我們已經討論過,所有重要的部分都在self.SGD和self.updateminibatch中完成。 self.backprop方法利用一些額外的函式來幫助計算梯度,也就是說sigmoidprime是計算σ函式的導數,而我不會在這裡描述self.costderivative。你能夠通過檢視程式碼或文件註釋來獲得這些的要點(以及細節)。我們將在下章詳細的看一下它們。注意,雖然程式顯得很長,但是大部分程式碼是用來使程式碼更容易理解的文件註釋。實際上,程式只包含74行非空行,非註釋程式碼。所有的程式碼可以在GitHub上這裡找到。


""" network.py \~~~~~~~~~~ A module to implement the stochastic gradient descent learning algorithm for a feedforward neural network. Gradients are calculated using backpropagation. Note that I have focused on making the code simple, easily readable, and easily modifiable. It is not optimized, and omits many desirable features. """
#### Libraries
# Standard library
import random
# Third-party libraries
import numpy as npclass Network(object):
def __init__(self, sizes):

"""The list ``sizes`` contains the number of neurons in the respective layers of the network. For example, if the list was [2, 3, 1] then it would be a three-layer network, with the first layer containing 2 neurons, the second layer 3 neurons, and the third layer 1 neuron. The biases and weights for the network are initialized randomly, using a Gaussian distribution with mean 0, and variance 1. Note thatthe first layer is assumed to be an input layer, and by convention we won't set any biases for those neurons, since biases are only ever used in computing the outputs from later layers."""

self.num_layers = len(sizes)
self.sizes = sizes
self.biases = [np.random.randn(y, 1) for y in sizes[1:]]
self.weights = [np.random.randn(y, x) for x, y in zip(sizes[:-1], sizes[1:])]

def feedforward(self, a):

"""Return the output of the network if ``a`` is input."""

for b, w in zip(self.biases, self.weights):
a = sigmoid(np.dot(w, a)+b)
return a

def SGD(self, training_data, epochs, mini_batch_size, eta, test_data=None):

"""Train the neural network using mini-batch stochastic gradient descent. The ``training_data`` is a list of tuples ``(x, y)`` representing the training inputs and the desired outputs. The other non-optional parameters are self-explanatory. If ``test_data`` is provided then the network will be evaluated against the test data after each epoch, and partial progress printed out. This is useful for tracking progress, but slows things down substantially."""

if test_data: n_test = len(test_data)
n = len(training_data)
for j in xrange(epochs):
random.shuffle(training_data)
mini_batches = [training_data[k:k+mini_batch_size] for k in xrange(0, n, mini_batch_size)]
for mini_batch in mini_batches:
self.update_mini_batch(mini_batch, eta)
if test_data:
print "Epoch {0}: {1} / {2}".format(j, self.evaluate(test_data), n_test)
else:
print "Epoch {0} complete".format(j)

def update_mini_batch(self, mini_batch, eta):

"""Update the network's weights and biases by applying gradient descent using backpropagation to a single mini batch. The ``mini_batch`` is a list of tuples ``(x, y)``, and ``eta`` is the learning rate."""

nabla_b = [np.zeros(b.shape) for b in self.biases]
nabla_w = [np.zeros(w.shape) for w in self.weights]
for x, y in mini_batch:
delta_nabla_b, delta_nabla_w = self.backprop(x, y)
nabla_b = [nb+dnb for nb, dnb in zip(nabla_b, delta_nabla_b)]
nabla_w = [nw+dnw for nw, dnw in zip(nabla_w, delta_nabla_w)]
self.weights = [w-(eta/len(mini_batch))*nw for w, nw in zip(self.weights, nabla_w)]
self.biases = [b-(eta/len(mini_batch))*nb for b, nb in zip(self.biases, nabla_b)]

def backprop(self, x, y):

"""Return a tuple ``(nabla_b, nabla_w)`` representing the gradient for the cost function C_x. ``nabla_b`` and ``nabla_w`` are layer-by-layer lists of numpy arrays, similarto ``self.biases`` and ``self.weights``."""

nabla_b = [np.zeros(b.shape) for b in self.biases]
nabla_w = [np.zeros(w.shape) for w in self.weights]
# feedforward
activation = x
activations = [x]
# list to store all the activations, layer by layer
zs = []
# list to store all the z vectors, layer by layer
for b, w in zip(self.biases, self.weights):
z = np.dot(w, activation)+b
zs.append(z)
activation = sigmoid(z)
activations.append(activation)
# backward pass
delta = self.cost_derivative(activations[-1], y) * \
sigmoid_prime(zs[-1])
nabla_b[-1] = delta
nabla_w[-1] = np.dot(delta, activations[-2].transpose())
# Note that the variable l in the loop below is used a little
# differently to the notation in Chapter 2 of the book. Here,
# l = 1 means the last layer of neurons, l = 2 is the
# second-last layer, and so on. It's a renumbering of the
# scheme in the book, used here to take advantage of the fact
# that Python can use negative indices in lists.
for l in xrange(2, self.num_layers):
z = zs[-l]
sp = sigmoid_prime(z)
delta = np.dot(self.weights[-l+1].transpose(), delta) * sp
nabla_b[-l] = delta
nabla_w[-l]= np.dot(delta, activations[-l-1].transpose())
return (nabla_b, nabla_w)

def evaluate(self, test_data):

"""Return the number of test inputs for which the neural network outputs the correct result. Note that the neural network's output is assumed to be the index of whichever neuron in the final layer has the highest activation."""
test_results = [(np.argmax(self.feedforward(x)), y) for (x, y) in test_data]
return sum(int(x == y) for (x, y) in test_results)

def cost_derivative(self, output_activations, y):
"""Return the vector of partial derivatives \partial C_x / \partial a for the output activations."""

return (output_activations-y)

#### Miscellaneous functionsdef sigmoid(z):
"""The sigmoid function."""

return 1.0/(1.0+np.exp(-z))def sigmoid_prime(z):
"""Derivative of the sigmoid function."""
return sigmoid(z)*(1-sigmoid(z))


程式識別手寫數字的效果如何?好吧,讓我們先載入MNIST資料。我將用下面所描述的一小段輔助程式mnist_loader.py來完成。我們在一個Python shell中執行下面的命令,


>>> import mnist_loader >>> trainingdata,validationdata,testdata = \ ... mnistloader.loaddatawrapper()


當然,這也可以被做成一個單獨的Python程式,但在Python shell執行最方便。在載入完MNIST資料之後,我們將設定一個有30個隱層神經元的網路我們在匯入如上所列的名字為網路的的Python程式後做


>>> import network >>> net = network.Network([784,30,10])


最後,我們將使用隨機梯度下降來從MNISTtraining_data學習超過30次迭代,迷你塊大小為10,學習率η= 3.0


>>> net.SGD(trainingdata,30,10,3.0,testdata = test_data)


注意,如果當你閱讀至此的時候正在執行程式碼,執行將會花費一些時間,對於一個普通的機器(截至2015年),它可能將會花費幾分鐘來執行。我建議你讓它執行,繼續閱讀並定期的檢查一下程式碼的輸出如果你趕時間,你可以通過減少迭代次數,減少隱層神經元次數或僅使用部分訓練資料來提高速度注意,這樣產生的程式碼將會特別快:。這些Python的指令碼的目的是幫助你理解神經網路是如何工作的,而不是高效能的程式碼!而且,當然,一旦我們已經訓練一個網路,它能在幾乎任何的計算平臺上快速的執行。例如,一旦我們對於一個網路學會了一組好的權重集和偏置集,它能很容易的移植到網頁瀏覽器中以的Javascript執行,或者如在移動裝置上的本地應用。在任何情況下,這是一個神經網路訓練執行時的部分輸出文字記錄。記錄顯示了在每輪訓練之後神經網 能正確識別測試影象的數目。正如你所見到,在僅僅一次迭代後,達到了萬中選中9,129個。而且數目還在持續增長


時代0:9129 / 10000Epoch 1:9295 / 10000Epoch 2:9348/10000 ...時代27:9528 / 10000Epoch 28:9542 / 10000Epoch 29:9534/10000


經過訓練的網路給我們一個一個約95%分類正確率為,在峰值時為95.42%(“Epoch 28”)!作為第一次嘗試,這是非常鼓舞人心的。然而我應該提醒你,如果你執行程式碼然後得到的結果不一定和我的完全一樣,因為我們使用了(不同的)隨機權重和偏置來初始化我們的網路。我採用了三次執行中的最優結果作為本章的結果。


讓我們重新執行上面的實驗,將隱層神經元數目改到100.正如前面的情況,如果你一邊閱讀一邊執行程式碼,你應該被警告它將會花費相當長一段時間來執行(在我的機器上,這個實驗每一輪訓練迭代需要幾十秒),因此當程式碼執行時,並行的繼續閱讀是很明智的。


>>> net = network.Network([784,100,10])>>> net.SGD(trainingdata,30,10,3.0,testdata = test_data)


果然,它將結果提升至96.59%。至少在這種情況下,使用更多的隱層神經元幫助我們得到了更好的結果。


讀者的反饋表明本實驗在結果上有相當多的變化,而且一些訓練執行給出的結果相當糟糕。使用第三章所介紹的技術將對我們的網路在不同的訓練執行上大大減少效能變化。


當然,為了獲得這些正確率,我不得不對於訓練的迭代次數,小批量大小和學習率η做了特殊的選擇。正如我上面所提到的,這些在我們的神經網路中被稱為超引數,以區別於通過我們的學習演算法所學到的引數(權重和偏置)。如果我們較差的選擇了超引數,我們會得到較差的結果。假設,例如我們選定學習率為η = 0.001,


>>> net = network.Network([784,100,10])>>> net.SGD(trainingdata,30,10,0.001,testdata = test_data)


結果不太令人激勵,


時代0:1139 / 10000Epoch 1:1136 / 10000Epoch 2:1135/10000 ...時代27:2101 / 10000Epoch 28:2123 / 10000Epoch 29:2142/10000


然而,你能夠看到網路的效能隨著時間的推移在緩慢的變好。這意味著著我們應該增大學習率,例如η= 0.01。如果我們那樣做了,我們會得到更好的結果,意味著我們應該再次增加學習率。(如果改變能夠提高,試著做更多!)如果我們這樣做幾次,我們最終會得到一個像η= 1.0的學習率(或者調整到3.0),這跟我們之前的實驗很接近。因此即使我們最初較差的選擇了超引數,我們至少獲得了足夠的資訊來幫助我們提升對於超參的選擇。一般來說,除錯一個神經網路是具有挑戰性的。甚至有可能某種超引數的選擇所產生的分類結果還不如隨機分類假定我們從之前成功的構建了30個隱層神經元的網路結構,但是學習率被改為η= 100.0。


>>> net = network.Network([784,30,10])>>> net.SGD(trainingdata,30,10,100.0,testdata = test_data)


在這點上,我們實際走的太遠,學習率太高了:


時代0:1009 / 10000Epoch 1:1009 / 10000Epoch 2:1009 / 10000Epoch 3:1009/10000 ...時代27:982 / 10000Epoch 28:982 / 10000Epoch 29:982/10000


現在想象一下,我們第一次遇到這樣的問題。當然,我們從我們之前的實驗中知道正確的做法是減小學習率。但是如果我們第一次遇到這樣的問題,然而沒有太多的輸出來指導我們怎麼做。我們可能不僅關心學習率,還要關心我們的神經網路中的其它每一個部分。我們可能想知道是否選擇了讓網路很難學習的初始化的權重和偏置?或者可能我們沒有足夠的訓練資料來獲得有意義的學習?或者我們沒有進行足夠的迭代次數?或者可能對於這種神經網路的結構,學習識別手寫數字是不可能的?可能學習率太低?或者可能學習率太高?當你第一次遇到某問題,你通常抱有不了把握。


從這得到的教訓是除錯一個神經網路是一個瑣碎的工作,就像日常的程式設計一樣,它是一門藝術。你需要學習除錯的藝術來獲得神經網路更好的結果。更普通的是,我們需要啟發式方法來選擇好的超引數和好的結構。所有關於這些的內容,我們都會在本書中進行討論,包括我之前是怎麼樣選擇超引數的。


下一節我們將介紹“關於深度學習”,敬請關注!


本文來源於哈工大SCIR

原文連結點選即可跳轉

《神經網路和深度學習》系列文章七:實現我們的神經網路來分類數字


相關文章