深入剖析神經網路的執行機理及實現
隨著大資料和機器硬體水平的提升,神經網路特別是深度神經網路現在是大火特火。因為目前的深度學習模型都是基於神經網路進行的改進和加深,所以要想對深度學習有一些較深入的研究,先熟悉和了解人工神經網路是非常有幫助的。
本文基於神經網路實現一個手寫體數字識別模型,此處使用的資料集為sklearn自帶的digit資料,只要裝了sklearn就可以直接獲得。
1、手寫體人工神經網路模型
圖(一),mnist手寫體數字識別網路結構,見【參考一】
神經網路是一個判別模型,它會利用訓練集學到一個從輸入到輸出的對映關係,結構上可以分為輸入層、隱藏層和輸出層,如上圖。輸入層用於接收資料的輸入,通過隱藏層的處理,最後經輸出層轉換得到輸出。
上圖為基於mnist資料集畫的一個神經網路模型,因為mnist一張圖片為28*28=784,故輸入層有784個神經元。而digit的圖片為8*8=64,故digit資料集的輸入層有64個神經元,也就是說我們將要實現的神經網路輸入層有64個神經元,要簡單很多。
神經網路的效能如何,隱層的設計非常關鍵,隱藏層是設計用來自動學習特徵的,通過這些學到的特徵來進行最後一層的分類任務,那它會學到什麼東西呢?在手寫體數字識別中,大概會學到這樣的特徵:
圖(二)
再具體一點就是,該圖中的隱層我們共設定了15個神經元,每一個神經元儲存的都是學來的特徵,假設前四個神經元是用來考察手寫數字是否滿足以下這四個特徵,
圖(三)
有了隱層學到的這些東西,那麼對它進行組合判斷就很容易得到輸出了,例如發現上面的四個特徵均被啟用,則如我們所知,其有很大的概率表示數字0。
2、執行機理及實現
在有監督學習中,模型會分為訓練階段和預測階段,在訓練階段將模型中的待定引數學習出來,然後用在預測階段,就好像我們初中求解帶參方程
在神經網路的訓練階段,主要包括以下幾步:
(1)載入訓練集;
(2)前向傳導,將資訊傳遞給輸出層;
(3)利用標註資訊和代價函式來計算代價;
(4)通過反向傳播代價函式梯度來更新每一層中的引數
其簡單實現的整體程式碼如下:
#coding=utf-8
'''
Created on Jul 20, 2016
'''
import numpy as np
import random
from sklearn import datasets
class Network(object):
def __init__(self,sizes):
'''
parameters:
sizes中儲存了神經網路各層神經元個數
functions:
對神經網路層與層之間的連線引數進行初始化
'''
#權重矩陣
self.weights = [np.random.randn(y,x) for x,y in zip(sizes[:-1],sizes[1:])]
#偏置矩陣
self.biases = [np.random.randn(x) for x in sizes[1:]]
def init_parameters(self,parameters):
'''
functions:初始化模型引數
parameters主要包括:
epochs:迭代次數
mini_batch_size:批處理大小
eta:學習率
nnLayers_size:神經網路層數
'''
self.epochs = parameters.get("epochs")
self.mini_batch_size = parameters.get("mini_batch_size")
self.eta = parameters.get("eta")
self.nnLayers_size = parameters.get("nnLayers_size")
def load_data(self):
'''
functions:載入資料集,這裡使用的是sklearn自帶的digit手寫體資料集
'''
digits = datasets.load_digits()
return digits.data, digits.target
def feed_forword(self,data):
'''
parameters:
data:輸入的圖片表示資料,是一個一維向量
functions:前向傳導,將輸入傳遞給輸出,y = w*x + b
return:傳遞到輸出層的結果
'''
for w, b in zip(self.weights, self.biases):
z = np.dot(w,data) + b
data = self.sigmoid(z)
return data
def sigmoid(self,z):
'''
functions:sigmoid函式
'''
return 1.0/(1.0+np.exp(-z))
def crossEntrop(self,a, y):
'''
parameters:
a:預測值
y:真實值
functions:交叉熵代價函式f=sigma(y*log(1/a))
'''
return np.sum(np.nan_to_num(-y*np.log(a)-(1-y)*np.log(1-a)))
def delta_crossEntrop(self,z,a,y):
'''
parameters:
z:啟用函式變數
a:預測值
y:真實值
'''
return self.sigmoid(z) - y
def SGD(self,data):
'''
function:隨即梯度下降演算法來對引數進行更新
parameters:
data:資料集
'''
#資料集大小
data_len = len(list(data))
for _ in range(self.epochs):
#將資料集按照指定大小劃分成小的batch進行梯度訓練,mini_batchs中的每個元素相當於一個小的樣本集
mini_batchs = [data[k:k+self.mini_batch_size] for k in range(0,data_len,self.mini_batch_size)]
for mini_batch in mini_batchs:
#batch中的每個樣本都會被用來更新引數
self.update_parameter_by_mini_batch(mini_batch)
def update_parameter_by_mini_batch(self,mini_batch):
'''
functions:按照梯度下降法批量對引數更新
'''
#首先初始化每個引數的偏導數
nabla_w = [np.zeros(w.shape) for w in self.weights]
nabla_b = [np.zeros(b.shape) for b in self.biases]
#將每個樣本計算得到的引數偏導數進行累加
for mini_x, mini_y in mini_batch:
#每個樣本通過後向傳播得到兩個導數張量,表示對w,b的導數
delta_nabla_w,delta_nabla_b = self.derivative_by_backpropagate(mini_x, mini_y)
nabla_w = [nw+dnw for nw,dnw in zip(nabla_w,delta_nabla_w)]
nabla_b = [nb+dnb for nb,dnb in zip(nabla_b,delta_nabla_b)]
self.weights = [w - self.eta * nw for w,nw in zip(self.weights,nabla_w)]
self.biases = [b - self.eta * nb for b,nb in zip(self.biases,nabla_b)]
def derivative_by_backpropagate(self,x,y):
'''
functions:通過後向傳播演算法來計算每個引數的梯度值
'''
#首先初始化每個引數的偏導數
nabla_w = [np.zeros(w.shape) for w in self.weights]
nabla_b = [np.zeros(b.shape) for b in self.biases]
#啟用值列表,元素為經過神經元后的啟用輸出,也即下一層的輸入,此處記錄下來用於計算梯度
activations = [x]
#線性組合值列表,元素為未經過神經元前的線性組合,z=w*x+b
zs = []
#初始輸入
activation = x
#首先通過迴圈得到求導所需要的中間值
for w, b in zip(self.weights,self.biases):
z = np.dot(w,activation) + b
zs.append(z)
activation = self.sigmoid(z)
activations.append(activation)
#倒數第一層的導數計算,有交叉熵求導得來
delta = self.delta_crossEntrop(zs[-1],activations[-1], y)
nabla_w[-1] = np.dot(delta.reshape(len(delta),1), activations[-2].reshape(1,len(activations[-2])))
nabla_b[-1] = delta
#倒數第二層至正數第一層間的導數計算,有sigmoid函式求導得來
for i in range(2,self.nnLayers_size):
z = zs[-i]
delta = np.dot(self.weights[-i+1].transpose(),delta.reshape(len(delta),1))
delta_z = self.derivative_sigmoid(z)
delta = np.multiply(delta, delta_z.reshape(len(delta_z),1))
nabla_w[-i] = np.dot(np.transpose(delta),activations[-i].reshape(len(activations[-i]),1))
delta = delta.reshape(len(delta))
nabla_b[-i] = delta
return (nabla_w,nabla_b)
def derivative_sigmoid(self,z):
'''
functions:對sigmoid求導的結果
'''
return self.sigmoid(z) *(1-self.sigmoid(z))
def evaluation(self,data):
'''
functions:效能評估函式
'''
result=[]
right = 0
for (x,y) in data:
output = self.feed_forword(x)
result.append((np.argmax(output),np.argmax(y)))
for i,j in result:
if(i == j):
right += 1
print("test data's size:",len(data))
print("count of right prediction",right)
print("the accuracy:",right/len(result))
return right/len(result)
def suffle(self,data):
'''
parameters:
data:元組資料
functions:對資料進行打亂重組
'''
new_data = list(data)
random.shuffle(new_data)
return np.array(new_data)
def transLabelToList(self,data_y):
'''
functions:將digit資料集中的標籤轉換成一個10維的列表,方便做交叉熵求導的計算
'''
data = []
for y in data_y:
item = [0,0,0,0,0,0,0,0,0,0]
item[y] = 1
data.append(item)
return data
if __name__=="__main__":
#神經網路的層數及各層神經元
nnLayers = [64,15,10]
nn=Network(nnLayers)
parameters = {"epochs":50,"mini_batch_size":10,"eta":0.01,"nnLayers_size":len(nnLayers)}
nn.init_parameters(parameters)
#載入資料集
data_x,data_y=nn.load_data()
#將標籤轉換成一個10維列表表示,如1表示成[0,1,0,0,0,0,0,0,0,0]
data_y = nn.transLabelToList(data_y)
#將資料打包成元組形式
data = zip(data_x,data_y)
#將有序資料打亂
data = nn.suffle(data)
#將資料集劃分為訓練集和測試集
train_data = data[:1500]
test_data = data[1500:]
nn.SGD(train_data)
print(nn.evaluation(test_data))
接下來,我們會按照神經網路的實現過程來對神經網路進行分析。
第一步,初始化一個神經網路模型
#nnLayers表示神經網路有三層結構,每層的神經元個數分別為64,15,10
nnLayers = [64,15,10]
nn=Network(nnLayers)
#引數詞典
parameters = {"epochs":50,"mini_batch_size":10,"eta":0.01,"nnLayers_size":len(nnLayers)}
#初始化引數函式
nn.init_parameters(parameters)
def init_parameters(self,parameters):
'''
functions:初始化模型引數
parameters主要包括:
epochs:迭代次數
mini_batch_size:批處理大小
eta:學習率
nnLayers_size:神經網路層數
'''
self.epochs = parameters.get("epochs")
self.mini_batch_size = parameters.get("mini_batch_size")
self.eta = parameters.get("eta")
self.nnLayers_size = parameters.get("nnLayers_size")
我們還要對神經網路中所有的邊的權值進行初始化,如下:
def __init__(self,sizes):
'''
parameters:
sizes中儲存了神經網路各層神經元個數
functions:
對神經網路層與層之間的連線引數進行初始化
'''
#權重矩陣
self.weights = [np.random.randn(y,x) for x,y in zip(sizes[:-1],sizes[1:])]
#偏置矩陣
self.biases = [np.random.randn(x) for x in sizes[1:]]
第二步,載入資料集
要想訓練一個模型,資料集是肯定少不了的。手寫體數字識別最出名的資料集當屬Lecun提供的mnist資料集,但其資料集不能直接拿來用,且我們只是打算訓練一個最簡單的三層神經網路,所以使用sklearn自帶的digit資料集就非常合適。
載入資料集函式如下:
#載入資料集
data_x,data_y=nn.load_data()
#將標籤轉換成一個10維列表表示,如1表示成[0,1,0,0,0,0,0,0,0,0]
data_y = nn.transLabelToList(data_y)
#將資料打包成元組形式
data = zip(data_x,data_y)
#將有序資料打亂
data = nn.suffle(data)
#將資料集劃分為訓練集和測試集
train_data = data[:1500]
test_data = data[1500:]
def load_data(self):
'''
functions:載入資料集,這裡使用的是sklearn自帶的digit手寫體資料集
'''
digits = datasets.load_digits()
return digits.data, digits.target
該函式返回兩個list,分別儲存每張手寫體的數字化表示和對應的標籤。
def transLabelToList(self,data_y):
'''
functions:將digit資料集中的標籤轉換成一個10維的列表,方便做交叉熵求導的計算
'''
data = []
for y in data_y:
item = [0,0,0,0,0,0,0,0,0,0]
item[y] = 1
data.append(item)
return data
該函式把原資料集中的標籤進行了改寫,以方便後續使用。
def suffle(self,data):
'''
parameters:
data:元組資料
functions:對資料進行打亂重組
'''
new_data = list(data)
random.shuffle(new_data)
return np.array(new_data)
因為直接載入後的資料集是按照0-9順序存放的,我們要通過suffle函式將資料集打亂,並劃分為訓練集和測試集。
第三步,訓練模型引數
這一步是模型的關鍵,我們需要通過訓練集把模型中的引數訓練出來,因為裡面夾雜了很多矩陣運算和求導運算,為方便說明,這裡給出一個簡單的三層神經網路,並將裡面的引數和變數標註出來,該圖如下:
圖(四)
訓練直接從呼叫SGD函式開始,
nn.SGD(train_data)
def SGD(self,data):
'''
function:隨即梯度下降演算法來對引數進行更新
parameters:
data:資料集
'''
#資料集大小
data_len = len(list(data))
for _ in range(self.epochs):
#將資料集按照指定大小劃分成小的batch進行梯度訓練,mini_batchs中的每個元素相當於一個小的樣本集
mini_batchs = [data[k:k+self.mini_batch_size] for k in range(0,data_len,self.mini_batch_size)]
for mini_batch in mini_batchs:
#batch中的每個樣本都會被用來更新引數
self.update_parameter_by_mini_batch(mini_batch)
為了加快訓練速度,在神經網路中並不是一次將全部資料都拿來訓練,而是通過批處理進行逐步更新引數的。所以在SGD函式中,首先將訓練資料集劃分成小塊資料送update_parameter_by_mini_batch函式進行引數更新操作。
def update_parameter_by_mini_batch(self,mini_batch):
'''
functions:按照梯度下降法批量對引數更新
'''
#首先初始化每個引數的偏導數
nabla_w = [np.zeros(w.shape) for w in self.weights]
nabla_b = [np.zeros(b.shape) for b in self.biases]
#將每個樣本計算得到的引數偏導數進行累加
for mini_x, mini_y in mini_batch:
#每個樣本通過後向傳播得到兩個導數張量,表示對w,b的導數
delta_nabla_w,delta_nabla_b = self.derivative_by_backpropagate(mini_x, mini_y)
nabla_w = [nw+dnw for nw,dnw in zip(nabla_w,delta_nabla_w)]
nabla_b = [nb+dnb for nb,dnb in zip(nabla_b,delta_nabla_b)]
#梯度下降法更新引數
self.weights = [w - self.eta * nw for w,nw in zip(self.weights,nabla_w)]
self.biases = [b - self.eta * nb for b,nb in zip(self.biases,nabla_b)]
在該函式中,我們會通過反向傳播代價來更新引數,在mini_batch中,我們會把batch中每對樣本對各引數的偏導數進行累加作為梯度下降法中的梯度,它不需要每個樣本過來都要使用梯度下降法計算一次,這也是使用mini_batch速度能夠加快速度的原因。
在上面的函式中,用到了derivative_by_backpropagate來進行反向傳播,這個函式是整個模型的關鍵。
def derivative_by_backpropagate(self,x,y):
'''
functions:通過後向傳播演算法來計算每個引數的梯度值
'''
#首先初始化每個引數的偏導數
nabla_w = [np.zeros(w.shape) for w in self.weights]
nabla_b = [np.zeros(b.shape) for b in self.biases]
#啟用值列表,元素為經過神經元后的啟用輸出,也即下一層的輸入,此處記錄下來用於計算梯度
activations = [x]
#線性組合值列表,元素為未經過神經元前的線性組合,z=w*x+b
zs = []
#初始輸入
activation = x
#首先通過迴圈得到求導所需要的中間值
for w, b in zip(self.weights,self.biases):
z = np.dot(w,activation) + b
zs.append(z)
activation = self.sigmoid(z)
activations.append(activation)
#倒數第一層的導數計算,有交叉熵求導得來
delta = self.delta_crossEntrop(activations[-1], y)
nabla_w[-1] = np.dot(delta.reshape(len(delta),1), activations[-2].reshape(1,len(activations[-2])))
nabla_b[-1] = delta
#倒數第二層至正數第一層間的導數計算,有sigmoid函式求導得來
for i in range(2,self.nnLayers_size):
z = zs[-i]
delta = np.dot(self.weights[-i+1].transpose(),delta.reshape(len(delta),1))
delta_z = self.derivative_sigmoid(z)
delta = np.multiply(delta, delta_z.reshape(len(delta_z),1))
nabla_w[-i] = np.dot(np.transpose(delta),activations[-i].reshape(len(activations[-i]),1))
delta = delta.reshape(len(delta))
nabla_b[-i] = delta
return (nabla_w,nabla_b)
def test():
#nothing
在該函式中,我們定義兩個列表分別來儲存前向傳導過程中計算的中間變數,它在後向傳播計算偏導的時候會被用到,其中,zs列表儲存的是圖(四)神經元左側的變數z,
前向傳導到輸出層,這裡我們用交叉熵來作為模型的代價函式,交叉熵求導過程如下(見【參考二】):
其中倒數第二行的變換用到了:
因為我們在mini_batch中是一個樣本一個樣本進行反向傳播計算的,所以上面公式中的求和求平均都可以去掉,且對w和b的求導項中均包括
def delta_crossEntrop(self,z,a,y):
'''
parameters:
z:啟用函式變數
a:預測值
y:真實值
'''
return self.sigmoid(z) - y
對最後一層的引數求導與其他層的引數求導過程不一樣,因此要分兩步來計算,但均屬於求導的鏈式法則,如
第四步,測試模型的分類能力
這裡使用前面劃分的測試集對已經訓練好的神經網路進行測試,
def evaluation(self,data):
'''
functions:效能評估函式
'''
result=[]
right = 0
for (x,y) in data:
output = self.feed_forword(x)
result.append((np.argmax(output),np.argmax(y)))
for i,j in result:
if(i == j):
right += 1
print("test data's size:",len(data))
print("count of right prediction",right)
print("the accuracy:",right/len(result))
return right/len(result)
def feed_forword(self,data):
'''
parameters:
data:輸入的圖片表示資料,是一個一維向量
functions:前向傳導,將輸入傳遞給輸出,y = w*x + b
return:傳遞到輸出層的結果
'''
for w, b in zip(self.weights, self.biases):
z = np.dot(w,data) + b
data = self.sigmoid(z)
return data
訓練好模型後,引數w,b就可以拿來使用了,通過前向傳導得到輸出,然後計算預測準確率,使用以上程式碼跑的一個結果如下: