1. 程式人生 > >深入剖析神經網路的執行機理及實現

深入剖析神經網路的執行機理及實現

隨著大資料和機器硬體水平的提升,神經網路特別是深度神經網路現在是大火特火。因為目前的深度學習模型都是基於神經網路進行的改進和加深,所以要想對深度學習有一些較深入的研究,先熟悉和了解人工神經網路是非常有幫助的。

本文基於神經網路實現一個手寫體數字識別模型,此處使用的資料集為sklearn自帶的digit資料,只要裝了sklearn就可以直接獲得。

1、手寫體人工神經網路模型


這裡寫圖片描述
圖(一),mnist手寫體數字識別網路結構,見【參考一】

神經網路是一個判別模型,它會利用訓練集學到一個從輸入到輸出的對映關係,結構上可以分為輸入層、隱藏層和輸出層,如上圖。輸入層用於接收資料的輸入,通過隱藏層的處理,最後經輸出層轉換得到輸出。

上圖為基於mnist資料集畫的一個神經網路模型,因為mnist一張圖片為28*28=784,故輸入層有784個神經元。而digit的圖片為8*8=64,故digit資料集的輸入層有64個神經元,也就是說我們將要實現的神經網路輸入層有64個神經元,要簡單很多。

神經網路的效能如何,隱層的設計非常關鍵,隱藏層是設計用來自動學習特徵的,通過這些學到的特徵來進行最後一層的分類任務,那它會學到什麼東西呢?在手寫體數字識別中,大概會學到這樣的特徵:


這裡寫圖片描述
圖(二)

再具體一點就是,該圖中的隱層我們共設定了15個神經元,每一個神經元儲存的都是學來的特徵,假設前四個神經元是用來考察手寫數字是否滿足以下這四個特徵,

這裡寫圖片描述這裡寫圖片描述
圖(三)

有了隱層學到的這些東西,那麼對它進行組合判斷就很容易得到輸出了,例如發現上面的四個特徵均被啟用,則如我們所知,其有很大的概率表示數字0。

2、執行機理及實現

在有監督學習中,模型會分為訓練階段和預測階段,在訓練階段將模型中的待定引數學習出來,然後用在預測階段,就好像我們初中求解帶參方程a

x+b=y一樣,首先通過已知條件把方程中的引數給求解出來,然後再利用求出來的引數計算給定x下的y值。

在神經網路的訓練階段,主要包括以下幾步:
(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,zt+1=wtat+bt,activations列表儲存的是圖(四)神經元右側的變數a,at=sigmoid(zt)

前向傳導到輸出層,這裡我們用交叉熵來作為模型的代價函式,交叉熵求導過程如下(見【參考二】):


這裡寫圖片描述

這裡寫圖片描述

其中倒數第二行的變換用到了:
這裡寫圖片描述

這裡寫圖片描述

因為我們在mini_batch中是一個樣本一個樣本進行反向傳播計算的,所以上面公式中的求和求平均都可以去掉,且對w和b的求導項中均包括(σ(z)y),我們可以定義交叉熵的求導函式為:

    def delta_crossEntrop(self,z,a,y):
        '''
            parameters:
                z:啟用函式變數
                a:預測值
                y:真實值
        '''
        return self.sigmoid(z) - y

對最後一層的引數求導與其他層的引數求導過程不一樣,因此要分兩步來計算,但均屬於求導的鏈式法則,如at+1wt求導,則先會對zt+1求導,再乘以zt+1wt的求導。對每個樣本通過反向傳播計算完偏導後返回給mini_batch,最後統一通過梯度下降法來對引數進行一次更新,然後進入下一次mini_batch的計算。

第四步,測試模型的分類能力

這裡使用前面劃分的測試集對已經訓練好的神經網路進行測試,

    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就可以拿來使用了,通過前向傳導得到輸出,然後計算預測準確率,使用以上程式碼跑的一個結果如下:


**這裡寫圖片描述**

3、參考