BP神經網絡原理及實踐

一、神經元模型

神經網絡: 目前使用最廣泛的定義是由適應性的簡單單元組成的廣泛並行互連的網絡,它的組織能夠模擬生物神經系統對真實世界物體所做出的交互反應。我們在機器學習中談論神經網絡時指的是神經網絡學習,或者說,是機器學習與神經網絡這兩個學科領域的交叉部分。

神經網絡中最基本的成分是神經元模型,一直沿用近日的是 “M-P神經元模型”,如下圖所示。這個模型中神經元接收到來自 n 個其他神經元傳遞過來的輸入信號,這些輸入信號通過帶權重的連接進行傳遞,神經元接收到的總輸入值將與神經元的閾值進行比較,然後通過激活函數處理以產生神經元的輸出。

理想中激活函數是階躍函數,如下圖 a 所示,將輸入值映射爲輸出 0 和 1,1 對應神經元的興奮,0 對應神經元的抑制,但由於階躍函數具有不連續、不光滑不太好的性質。實際中常用 Sigmoid 函數,如圖b所示。它將較大範圍內變化的輸入值擠壓到(0,1)輸出值範圍內,有時也稱“擠壓函數”。

                                     (a)                                                                 (b)

把上述許多個這樣的神經元按一定的層次結構連接起來,就得到了神經網絡。

二、感知機與多層網絡

感知機由兩層神經元組成,輸出層是“M-P神經元”,亦稱“閾值邏輯單元”。感知機能實現邏輯與、或、非運算。感知機的學習規則非常簡單,對訓練樣例(x,y),若當前感知機的輸出爲 \hat{y} ,則感知機的權重調整爲:

w_{i}\leftarrow w_{i}+\Delta w_{i};\Delta w_{i}=\eta (y-\hat{y})x_{i}

其中 \eta \in (0,1) 稱爲學習率,若感知機對訓練樣例(x,y)預測正確,即 y=\hat{y} ,則感知機不發生變化,否則將根據錯誤的程度進行權重調整。由於感知機只有輸出層神經元進行激活函數處理,其學習能力非常有限,只能處理線性可分問題。要處理非線性可分問題,需要考慮使用多層功能神經元。例如簡單的兩層感知機就可以解決異或的問題,在輸入和輸出有一層神經元,被稱爲隱含層。隱含層和輸出層都具有激活函數的功能神經元。

一般的神經網絡如下圖所示的單隱層前饋網絡,還有雙隱層前饋網絡等等,只要包含隱含層就爲多層網絡。每層神經元與下層神經元全連接,神經元之間不存在同層連接,也不存在跨層連接,這樣的神經元稱爲“多層前饋神經網絡”。輸入層只接受輸入不處理函數,隱含層和輸出層包含功能函數。

神經網絡的學習過程就是訓練數據來調整神經元之間的“連接權”以及每個功能神經元的閾值,即神經網絡的學習蘊含在連接權和閾值中。

三、BP算法

迄今最成功的神經網絡學習算法是誤差逆傳播算法(BP),那BP算法究竟是怎樣的? 下面一起來看

下圖是一個擁有 d 個輸入神經元,l 個輸出神經元,q 個隱含層神經元的多層前饋網絡結構。其中輸出層第j個神經元的閾值表示爲 \theta _{j},隱含層第 h 個神經元的閾值用 \gamma _{h} 表示,輸入層第 i 個神經元與隱含層第 h 個神經元之間的連接權用 v_{ih} 表示,隱含層第 h 個神經元與輸出層第 j 個神經元之間的連接權爲 w_{hj} 表示。假設隱含層和輸出層神經元都使用 sigmoid 函數。

學習率\eta \in (0,1)控制着算法每一輪迭代中的更新步長,若太大則容易震盪,太小收斂速度又會過慢。一般地常把 η 設置爲 0.1,有時更新權重時會將輸出層與隱含層設置爲不同的學習率。

下面給出 BP 算法的工作流程,此流程是針對單個 E_{k} 推導的,每次更新只針對單個樣例,即標準 BP 算法

但 BP 算法的目的是要最小化訓練集 D 上的累積誤差:

E=\frac{1}{m}\sum_{k=1}^{m}E_{k}

所以基於累積誤差最小化的更新準則,就得到累積誤差逆傳播算法,即累計 BP 算法,他在讀取整個訓練集 D 一遍後纔對參數進行更新,其參數更新的頻率低得多。在很多任務中,累積誤差下降到一定程度,進一步下將會非常緩慢,這是標準 BP 往往會更快獲得較好的解,尤其在訓練集 D 非常大時。

由於BP神經網絡的強大,往往會過擬合,其訓練誤差持續降低,但測試誤差會持續上升。有兩種方法緩減過擬合:

  1. 早停:若訓練集誤差降低但驗證集誤差升高,則停止訓練,同時返回具有驗證集誤差的連接權和閾值。
  2. 正則化:其基本思想是在誤差目標函數中增加一個用於描述網絡複雜度的部分,例如連接權和閾值的平方和。其中          λ∈(0,1)用於對累積經驗誤差與網絡複雜度這兩項進行折中,常通過交叉驗證法來估計。

                                               E=\lambda \frac{1}{m}\sum_{k=1}^{m}E_{k}+(1-\lambda )\sum_{i}w_{i}^{2}

在後面將進行BP算法的實踐

四、全局最小和局部最小

神經網絡的訓練過程可看作一個參數尋優的過程,即在參數空間中,尋找一組最優參數使得E最小。BP算法試圖通過最速下降來尋找使得累積經驗誤差最小的權值與閾值,在談到最優時,一般會提到局部極小和全局最小。

局部最小解是參數空間中的某個點,其領域點的誤差函數值均小於該點的函數值;

全局最小解之參數空間中所有點的誤差函數值均小於該點的誤差函數值。兩者對應的E就是誤差函數的局部極小值和全局極小值。局部極小可以有多個,而全局最小隻有一個。全局最小一定是局部極小,但局部最小卻不一定是全局最小。顯然在很多機器學習算法中,都試圖找到目標函數的全局最小。梯度下降法的主要思想就是沿着負梯度方向去搜索最優解,負梯度方向是函數值下降最快的方向,若迭代到某處的梯度爲0,則表示達到一個局部最小,參數更新停止。然而,如果誤差函數具有多個局部極小,則不能保證找到的就爲全局最小,對於這種情況我們稱參數尋優陷入局部極小人們常採用以下策略儘可能地去接近全局最小,即跳出局部極小:

  1. 以多組不同參數值初始化多個神經網絡,按標準方法訓練,迭代停止後,取其中誤差最小的解作爲最終參數。
  2. 使用“模擬退火”技術,在每一步都以一定的概率接受比當前解更差的結果,從而有助於跳出局部極小。
  3. 使用隨機梯度下降,即在計算梯度時加入了隨機因素,使得在局部最小時,計算的梯度仍可能不爲零,這樣有機會跳出局部極小繼續搜索。

此外,遺傳算法也常用來訓練神經網絡以更好地逼近全局最小。上述跳出局部極小的技術大多是啓發式,理論上尚缺乏保障。

五、深度學習

參數越多的模型複雜度越高,複雜模型的訓練效率低,易陷入過擬合,隨着大數據、雲計算的時代到來,以深度學習爲代表的複雜模型開始受到人們的關注。

神經網絡增加模型複雜度的方法有兩種:

  1. 增加隱含層的數目,隱含層多了相應的神經元連接權、閾值等參數就會更多;
  2. 單純增加隱含層神經元的數目。

但從增加模型複雜度的角度來看,增加隱含層的數目比增加隱含層神經元的數目更有效,因爲增加隱含層不僅增加擁有激活函數的神經元數目,還增加了激活函數嵌套的層數。然而,多層神經網絡難以直接用經典算法(例如標準BP算法)進行訓練,因爲誤差在多隱含層內逆傳播時,往往會發散而不能收斂到穩定狀態。這裏的多隱含層是指三個以上的隱含層,深度學習模型通常有八九層甚至更多層。

所以要有效的訓練多隱含層網絡通常有兩種有效手段:

  1. 無監督逐層訓練:每次訓練一層隱節點,把上一層隱節點的輸出當作輸入來訓練,本層隱結點訓練好後,輸出再作爲下一層的輸入來訓練,這稱爲預訓練。預訓練完成後,再對整個網絡進行微調訓練。例如深度信念網絡(deep belief network,簡稱DBN)。“預訓練+微調“可以視爲把大量的參數進行分組,先找出每組較好的設置,再基於這些局部最優的結果來進行全局尋優。
  2. 權共享:讓一組神經元使用完全相同的連接權,例子是卷積神經網絡(Convolutional Neural Network,簡稱CNN)。這樣做可以大大減少需要訓練的參數數目。以CNN進行手寫數字識別任務爲例,如下圖所示,CNN複合多個卷積層和採樣層對輸入信號進行加工,然後在連接層實現與輸出目標之間的映射。

 

我們從另一個角度理解深度學習:通過多層處理,逐漸將初始的”低層”特徵表示轉化爲“高層”特徵表示後,用“簡單模型”即可完成複雜的分類等學習任務,由此可將深度學習理解爲進行特徵學習或表示學習。

六、BP神經網絡的訓練

對神經網絡的學習大致包括以下步驟:

  • 初始化參數,包括權重、偏置、網絡層結構、激活函數等。
  • 循環計算
  • 正向傳播,計算誤差
  • 反向傳播,調整參數
  • 返回最終的神經網絡模型
# -*- coding: utf-8 -*-
"""
Created on Wed Mar 20 09:58:16 2019

@author: 2018061801
"""
import numpy as np
from math import sqrt

def load_data(file_name):
    '''導入數據
    input:  file_name(string):文件的存儲位置
    output: feature_data(mat):特徵
            label_data(mat):標籤
            n_class(int):類別的個數
    '''
    # 1、獲取特徵
    f = open(file_name)  # 打開文件
    feature_data = []
    label_tmp = []
    for line in f.readlines():
        feature_tmp = []
        lines = line.strip().split("\t")
        for i in range(len(lines) - 1):
            feature_tmp.append(float(lines[i]))
        label_tmp.append(int(lines[-1]))      
        feature_data.append(feature_tmp)
    f.close()  # 關閉文件
    
    # 2、獲取標籤
    m = len(label_tmp)
    n_class = len(set(label_tmp))  # 得到類別的個數
    
    label_data = np.mat(np.zeros((m, n_class)))
    for i in range(m):
        label_data[i, label_tmp[i]] = 1
    
    return np.mat(feature_data), label_data, n_class

def sig(x):
    '''Sigmoid函數
    input:  x(mat/float):自變量,可以是矩陣或者是任意實數
    output: Sigmoid值(mat/float):Sigmoid函數的值
    '''
    return 1.0 / (1 + np.exp(-x))

def partial_sig(x):
    '''Sigmoid導函數的值
    input:  x(mat/float):自變量,可以是矩陣或者是任意實數
    output: out(mat/float):Sigmoid導函數的值
    '''
    m, n = np.shape(x)
    out = np.mat(np.zeros((m, n)))
    for i in range(m):
        for j in range(n):
            out[i, j] = sig(x[i, j]) * (1 - sig(x[i, j]))
    return out

def hidden_in(feature, w0, b0):
    '''計算隱含層的輸入
    input:  feature(mat):特徵
            w0(mat):輸入層到隱含層之間的權重
            b0(mat):輸入層到隱含層之間的偏置
    output: hidden_in(mat):隱含層的輸入
    '''
    m = np.shape(feature)[0]
    hidden_in = feature * w0
    for i in range(m):
        hidden_in[i, ] += b0
    return hidden_in

def hidden_out(hidden_in):
    '''隱含層的輸出
    input:  hidden_in(mat):隱含層的輸入
    output: hidden_output(mat):隱含層的輸出
    '''
    hidden_output = sig(hidden_in)
    return hidden_output;

def predict_in(hidden_out, w1, b1):
    '''計算輸出層的輸入
    input:  hidden_out(mat):隱含層的輸出
            w1(mat):隱含層到輸出層之間的權重
            b1(mat):隱含層到輸出層之間的偏置
    output: predict_in(mat):輸出層的輸入
    '''
    m = np.shape(hidden_out)[0]
    predict_in = hidden_out * w1
    for i in range(m):
        predict_in[i, ] += b1
    return predict_in
    
def predict_out(predict_in):
    '''輸出層的輸出
    input:  predict_in(mat):輸出層的輸入
    output: result(mat):輸出層的輸出
    '''
    result = sig(predict_in)
    return result

def bp_train(feature, label, n_hidden, maxCycle, alpha, n_output):
    '''計算隱含層的輸入
    input:  feature(mat):特徵
            label(mat):標籤
            n_hidden(int):隱含層的節點個數
            maxCycle(int):最大的迭代次數
            alpha(float):學習率
            n_output(int):輸出層的節點個數
    output: w0(mat):輸入層到隱含層之間的權重
            b0(mat):輸入層到隱含層之間的偏置
            w1(mat):隱含層到輸出層之間的權重
            b1(mat):隱含層到輸出層之間的偏置
    '''
    m, n = np.shape(feature)
    # 1、初始化
    w0 = np.mat(np.random.rand(n, n_hidden))
    w0 = w0 * (8.0 * sqrt(6) / sqrt(n + n_hidden)) - \
     np.mat(np.ones((n, n_hidden))) * \
      (4.0 * sqrt(6) / sqrt(n + n_hidden))
    b0 = np.mat(np.random.rand(1, n_hidden))
    b0 = b0 * (8.0 * sqrt(6) / sqrt(n + n_hidden)) - \
     np.mat(np.ones((1, n_hidden))) * \
      (4.0 * sqrt(6) / sqrt(n + n_hidden))
    w1 = np.mat(np.random.rand(n_hidden, n_output))
    w1 = w1 * (8.0 * sqrt(6) / sqrt(n_hidden + n_output)) - \
     np.mat(np.ones((n_hidden, n_output))) * \
      (4.0 * sqrt(6) / sqrt(n_hidden + n_output))
    b1 = np.mat(np.random.rand(1, n_output))
    b1 = b1 * (8.0 * sqrt(6) / sqrt(n_hidden + n_output)) - \
     np.mat(np.ones((1, n_output))) * \
      (4.0 * sqrt(6) / sqrt(n_hidden + n_output))
    
    # 2、訓練
    i = 0
    while i <= maxCycle:
        # 2.1、信號正向傳播
        # 2.1.1、計算隱含層的輸入
        hidden_input = hidden_in(feature, w0, b0)  # mXn_hidden
        # 2.1.2、計算隱含層的輸出
        hidden_output = hidden_out(hidden_input)
        # 2.1.3、計算輸出層的輸入
        output_in = predict_in(hidden_output, w1, b1)  # mXn_output
        # 2.1.4、計算輸出層的輸出
        output_out = predict_out(output_in)
        
        # 2.2、誤差的反向傳播
        # 2.2.1、隱含層到輸出層之間的殘差
        delta_output = -np.multiply((label - output_out), partial_sig(output_in))
        # 2.2.2、輸入層到隱含層之間的殘差
        delta_hidden = np.multiply((delta_output * w1.T), partial_sig(hidden_input))
        
        # 2.3、 修正權重和偏置       
        w1 = w1 - alpha * (hidden_output.T * delta_output)
        b1 = b1 - alpha * np.sum(delta_output, axis=0) * (1.0 / m)
        w0 = w0 - alpha * (feature.T * delta_hidden)
        b0 = b0 - alpha * np.sum(delta_hidden, axis=0) * (1.0 / m)
        if i % 100 == 0:
            print ("\t-------- iter: ", i, \
            " ,cost: ",  (1.0/2) * get_cost(get_predict(feature, w0, w1, b0, b1) - label))             
        i += 1           
    return w0, w1, b0, b1

def get_cost(cost):
    '''計算當前損失函數的值
    input:  cost(mat):預測值與標籤之間的差
    output: cost_sum / m (double):損失函數的值
    '''
    m,n = np.shape(cost)
    
    cost_sum = 0.0
    for i in range(m):
        for j in range(n):
            cost_sum += cost[i,j] * cost[i,j]
    return cost_sum / m

def get_predict(feature, w0, w1, b0, b1):
    '''計算最終的預測
    input:  feature(mat):特徵
            w0(mat):輸入層到隱含層之間的權重
            b0(mat):輸入層到隱含層之間的偏置
            w1(mat):隱含層到輸出層之間的權重
            b1(mat):隱含層到輸出層之間的偏置
    output: 預測值
    '''
    return predict_out(predict_in(hidden_out(hidden_in(feature, w0, b0)), w1, b1))    

def save_model(w0, w1, b0, b1):
    '''保存最終的模型
    input:  w0(mat):輸入層到隱含層之間的權重
            b0(mat):輸入層到隱含層之間的偏置
            w1(mat):隱含層到輸出層之間的權重
            b1(mat):隱含層到輸出層之間的偏置
    output: 
    '''
    def write_file(file_name, source):   
        f = open(file_name, "w")
        m, n = np.shape(source)
        for i in range(m):
            tmp = []
            for j in range(n):
                tmp.append(str(source[i, j]))
            f.write("\t".join(tmp) + "\n")
        f.close()
    
    write_file("weight_w0", w0)
    write_file("weight_w1", w1)
    write_file("weight_b0", b0)
    write_file("weight_b1", b1)
    
def err_rate(label, pre):
    '''計算訓練樣本上的錯誤率
    input:  label(mat):訓練樣本的標籤
            pre(mat):訓練樣本的預測值
    output: rate[0,0](float):錯誤率
    '''
    m = np.shape(label)[0]
    err = 0.0
    for i in range(m):
        if label[i, 0] != pre[i, 0]:
            err += 1
    rate = err / m
    return rate

if __name__ == "__main__":
    # 1、導入數據
    print ("--------- 1.load data ------------")
    feature, label, n_class = load_data("D:/anaconda4.3/spyder_work/data.txt")
    # 2、訓練網絡模型
    print ("--------- 2.training ------------")
    w0, w1, b0, b1 = bp_train(feature, label, 20, 1000, 0.1, n_class)
    # 3、保存最終的模型
    print ("--------- 3.save model ------------")
    save_model(w0, w1, b0, b1)
    # 4、得到最終的預測結果
    print ("--------- 4.get prediction ------------")
    result = get_predict(feature, w0, w1, b0, b1)
    print ("訓練準確性爲:", (1 - err_rate(np.argmax(label, axis=1), np.argmax(result, axis=1))))
    

結果:

--------- 1.load data ------------
--------- 2.training ------------
        -------- iter:  0  ,cost:  0.32056240323748886
        -------- iter:  100  ,cost:  0.02374088596119964
        -------- iter:  200  ,cost:  0.016247175730753252
        -------- iter:  300  ,cost:  0.014216291822320076
        -------- iter:  400  ,cost:  0.012527987143185957
        -------- iter:  500  ,cost:  0.011411808088174234
        -------- iter:  600  ,cost:  0.010691849370465361
        -------- iter:  700  ,cost:  0.01007772478527919
        -------- iter:  800  ,cost:  0.009571297239877182
        -------- iter:  900  ,cost:  0.009190086607128702
        -------- iter:  1000  ,cost:  0.008898688196057304
--------- 3.save model ------------
--------- 4.get prediction ------------
訓練準確性爲: 0.99

注: 我使用 Python3.5 , 測試部分可以自己去試試

訓練數據鏈接:https://github.com/zhaozhiyong19890102/Python-Machine-Learning-Algorithm/blob/master/Chapter_6%20BP/data.txt

 

參考文獻:趙志勇《python 機器學習算法》(程序)

周志華《機器學習》(原理)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章