機器學習入門學習筆記:(一)BP神經網絡原理推導及程序實現


   機器學習中,神經網絡算法可以說是當下使用的最廣泛的算法。神經網絡的結構模仿自生物神經網絡,生物神經網絡中的每個神經元與其他神經元相連,當它“興奮”時,想下一級相連的神經元發送化學物質,改變這些神經元的電位;如果某神經元的電位超過一個閾值,則被激活,否則不被激活。誤差逆傳播算法(error back propagation)是神經網絡中最有代表性的算法,也是使用最多的算法之一。

誤差逆傳播算法理論推導

  誤差逆傳播算法(error back propagation)簡稱BP網絡算法。而一般在說BP網絡算法時,默認指用BP算法訓練的多層前饋神經網絡。

  下面是一個簡單的BP神經網絡示意圖。其擁有一個輸入層,一個隱含層,一個輸出層。推導中采用這種簡單的三層的神經網絡。

 

  定義相關的一些變量如下:

  1. 假設有 d 個輸入神經元,有 l 個輸出神經元,q 個隱含層神經元;
  2. 設輸出層第 j 個神經元的閾值為 θj
  3. 設隱含層第 h 個神經元的閾值為 γh
  4. 輸入層第 i 個神經元與隱含層第 h 個神經元之間的連接權為 Vih 
  5. 隱含層第 h 個神經元與輸出層第 j 個神經元之間的連接權為 Whj 
  6. 記隱含層第 h 個神經元接收到來自於輸入層的輸入為 αh:

                              

  7.  記輸出層第 j 個神經元接收到來自於隱含層的輸入為 βj:   

              ,其中 bh 為隱含層第 h 個神經元的輸出

  理論推導:

  在神經網絡中,神經元接收到來自來自其他神經元的輸入信號,這些信號乘以權重累加到神經元接收的總輸入值上,隨后與當前神經元的閾值進行比較,然后通過激活函數處理,產生神經元的輸出。

 

  激活函數:

  理想的激活函數是階躍函數,“0”對應神經元抑制,“1”對應神經元興奮。然而階躍函數的缺點是不連續,不可導,且不光滑,所以常用sigmoid函數作為激活函數代替階躍函數。如下圖分別是階躍函數和sigmoid函數。

  階躍函數

     

 

  sigmoid函數:

 

  對於一個訓練例(xk, yk),假設神經網絡的輸出為 Yk ,則輸出可表示為:

    

f(***)表示激活函數,默認全部的激活函數都為sigmoid函數。

  則可以計算網絡上,(xk, yk)的均方差誤差為:

    

  乘以1/2是為了求導時能正好抵消掉常數系數。

  現在,從隱含層的第h個神經元看,輸入層總共有 d 個權重傳遞參數傳給他,它又總共有 l 個權重傳遞參數傳給輸出層, 自身還有 1 個閾值。所以在我們這個神經網絡中,一個隱含層神經元有(d+l+1)個參數待確定。輸出層每個神經元還有一個閾值,所以總共有 l 個閾值。最后,總共有(d+l+1)*q+l 個待定參數。

首先,隨機給出這些待定的參數,后面通過BP算法的迭代,這些參數的值會逐漸收斂於合適的值,那時,神經網絡也就訓練完成了。

  任意權重參數的更新公式為:

    

  下面以隱含層到輸出層的權重參數 whj 為例說明:

  我們可以按照前面給出的公式求出均方差誤差 Ek ,期望其為0,或者為最小值。而BP算法基於梯度下降法(gradient descent)來求解最優解,以目標的負梯度方向對參數進行調整,通過多次迭代,新的權重參數會逐漸趨近於最優解。對於誤差 Ek ,給定學習率(learning rate)即步長 η ,有:

    

  再看一下參數的傳遞方向,首先 whj 影響到了輸出層神經元的輸入值 β,然后影響到輸出值 Yj,然后再影響到誤差 Ek ,所以可以列出如下關系式:

    

  根據輸出層神經元的輸入值 β的定義:

    

  得到:

    

  對於激活函數(sigmoid函數):

    

  很容易通過求導證得下面的性質:

    

  使用這個性質進行如下推導:

  令:

    

  又由於:

    

  所以:

     

  由前面的定義有:

    

  所以:

    

  把這個結果結合前面的幾個式子代入:

    ,  ,  

  得到:

    

  所以:

    

   OK,上面這個式子就是梯度了。通過不停地更新即梯度下降法就可實現權重更新了。

    

  推導到這里就結束了,再來解釋一下式子中各個元素的意義。

     

  η 為學習率,即梯度下降的補償;為神經網絡輸出層第 j 個神經元的輸出值;為給出的訓練例(xk, yk)的標志(label),即訓練集給出的正確輸出;為隱含層第 h 個神經元的輸出。

  

  類似可得:

    

  其中,

    

  這部分的解法與前面的推導方法類似,不做贅述。

 

   接下來是代碼部分:

  這段代碼網上也有不少地方可以看到,后面會簡單介紹一下程序。

  完整程序:文件名“NN_Test.py”

# _*_ coding: utf-8 _*_

import numpy as np


def tanh(x):
    return np.tanh(x)


def tanh_derivative(x):
    return 1 -  np.tanh(x) * np.tanh(x)

# sigmod函數
def logistic(x):
    return 1 / (1 + np.exp(-x))

# sigmod函數的導數
def logistic_derivative(x):
    return logistic(x) * (1 - logistic(x))


class NeuralNetwork:
    def __init__ (self, layers, activation = 'tanh'):
        if activation == 'logistic':
            self.activation = logistic
            self.activation_deriv = logistic_derivative
        elif activation == 'tanh':
            self.activation = tanh
            self.activation_deriv = tanh_derivative
        
        # 隨機產生權重值
        self.weights = []
        for i in range(1, len(layers) - 1):     # 不算輸入層,循環
            self.weights.append((2 * np.random.random( (layers[i-1] + 1, layers[i] + 1)) - 1) * 0.25 )  
            self.weights.append((2 * np.random.random( (layers[i] + 1, layers[i+1])) - 1) * 0.25 )
            #print self.weights
        
    def fit(self, x, y, learning_rate=0.2, epochs=10000):
        x = np.atleast_2d(x) 
        temp = np.ones([x.shape[0], x.shape[1]+1])
        temp[:, 0:-1] = x
        x = temp
        y = np.array(y)
        
        for k in range(epochs): # 循環epochs次
            i = np.random.randint(x.shape[0])   # 隨機產生一個數,對應行號,即數據集編號  
            a = [x[i]]  # 抽出這行的數據集
            
            # 迭代將輸出數據更新在a的最后一行
            for l in range(len(self.weights)):
                a.append(self.activation(np.dot(a[l], self.weights[l])))
            
            # 減去最后更新的數據,得到誤差
            error = y[i] - a[-1]
            deltas = [error * self.activation_deriv(a[-1])]
        
            # 求梯度
            for l in range(len(a) - 2, 0, -1):
                deltas.append(deltas[-1].dot(self.weights[l].T) * self.activation_deriv(a[l]) )
            
            #反向排序
            deltas.reverse()   
            
           # 梯度下降法更新權值
            for i in range(len(self.weights)):
                layer = np.atleast_2d(a[i])
                delta = np.atleast_2d(deltas[i])
                self.weights[i] += learning_rate * layer.T.dot(delta)
            
    def predict(self, x):
        x = np.array(x)
        temp = np.ones(x.shape[0] + 1)
        temp[0:-1] = x   
        a = temp
        for l in range(0, len(self.weights)):
            a = self.activation(np.dot(a, self.weights[l]))
        return a     

簡要說明:

def tanh(x):
    return np.tanh(x)


def tanh_derivative(x):
    return 1 -  np.tanh(x) * np.tanh(x)

# sigmod函數
def logistic(x):
    return 1 / (1 + np.exp(-x))

# sigmod函數的導數
def logistic_derivative(x):
    return logistic(x) * (1 - logistic(x))

分別表示兩種激活函數,tanh函數和sigmoid函數以及其的導數,有關激活函數前文有提及。

 

        if activation == 'logistic':
            self.activation = logistic
            self.activation_deriv = logistic_derivative
        elif activation == 'tanh':
            self.activation = tanh
            self.activation_deriv = tanh_derivative

 “activation”參數決定了激活函數的種類,是tanh函數還是sigmoid函數。

 

     self.weights = []
        for i in range(1, len(layers) - 1):     # 不算輸入層,循環
            self.weights.append((2 * np.random.random( (layers[i-1] + 1, layers[i] + 1)) - 1) * 0.25 )  
            self.weights.append((2 * np.random.random( (layers[i] + 1, layers[i+1])) - 1) * 0.25 )
            #print self.weights    

以隱含層前后層計算產生權重參數,參數初始時隨機,取值范圍是[-0.25, 0.25]

 

        x = np.atleast_2d(x) 
        temp = np.ones([x.shape[0], x.shape[1]+1])
        temp[:, 0:-1] = x
        x = temp
        y = np.array(y)

創建並初始化要使用的變量。

 

for k in range(epochs): # 循環epochs次
            i = np.random.randint(x.shape[0])   # 隨機產生一個數,對應行號,即數據集編號  
            a = [x[i]]  # 抽出這行的數據集
            
            # 迭代將輸出數據更新在a的最后一行
            for l in range(len(self.weights)):
                a.append(self.activation(np.dot(a[l], self.weights[l])))
            
            # 減去最后更新的數據,得到誤差
            error = y[i] - a[-1]
            deltas = [error * self.activation_deriv(a[-1])]
        
            # 求梯度
            for l in range(len(a) - 2, 0, -1):
                deltas.append(deltas[-1].dot(self.weights[l].T) * self.activation_deriv(a[l]) )
            
            #反向排序
            deltas.reverse()   
            
           # 梯度下降法更新權值
            for i in range(len(self.weights)):
                layer = np.atleast_2d(a[i])
                delta = np.atleast_2d(deltas[i])
                self.weights[i] += learning_rate * layer.T.dot(delta)

進行BP神經網絡的訓練的核心部分,在代碼中有相應注釋。

 

    def predict(self, x):
        x = np.array(x)
        temp = np.ones(x.shape[0] + 1)
        temp[0:-1] = x   
        a = temp
        for l in range(0, len(self.weights)):
            a = self.activation(np.dot(a, self.weights[l]))
        return a     

這段是預測函數,其實就是將測試集的數據輸入,然后正向走一遍訓練好的網絡最后再返回預測結果。

 

 測試驗證函數:

# _*_ coding: utf-8 _*_

from NN_Test import NeuralNetwork
import numpy as np

nn = NeuralNetwork([2, 2, 1], 'tanh')
x = np.array([[0, 0], [0, 1], [1, 0], [1, 1]])
y = np.array([0, 1, 1, 0])
nn.fit(x, y)
for i in [[0, 0], [0, 1], [1, 0], [1, 1]]:
    print(i, nn.predict(i))

程序中測試的是異或關系,下面是運行結果:

([0, 0], array([-0.01628435]))
([0, 1], array([ 0.99808061]))
([1, 0], array([ 0.99808725]))
([1, 1], array([-0.03867579]))

顯然與標准異或關系近似。

 

  

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM