python機器學習——BP(反向傳播)神經網絡算法

背景與原理:

BP神經網絡通常指基於誤差反向傳播算法的多層神經網絡,BP算法由信號的前向傳播和反向傳播兩個過程組成,在前向傳播的過程中,輸入從輸入層進入網絡,經過隱含層逐層傳遞到達輸出層輸出,如果輸出結果與預期不符那麼轉至誤差反向傳播過程,否則結束學習過程。在反向傳播過程中,誤差會基於梯度下降原理分配給各層神經元,修正各個神經元的權值。

考慮一個經典的分類問題,假設我們有一組數據形如$(x_{1},...,x_{n},y)$,其中$y$爲這個數據所屬的類別,不妨設有$k$個取值$(C_{1},...,C_{k})$,那麼重複之前的描述:我們實際上要求的是給定了一個輸入$X$,求這個輸入屬於某一類的概率,也就是說我們可以理解爲輸入一個$n$維向量,希望預測一個$k$維向量,每一維表示這個輸入屬於這一類的概率,而最後我們的預測結果就是概率最大的那一個維度。

考慮一個基礎的邏輯迴歸,我們構造了一個線性函數$z=w^{T}x+b$,然後令$g(z)=\dfrac{1}{1+e^{-z}}$,這樣$g(z)$就可以用來表示一個0-1二分類問題中屬於類別1的概率。

而這個函數的意義,一方面在於將$R$光滑對稱地映射到了$(0,1)$區間能有效地表示概率,另一方面,linear的東西始終只是linear的,當然我們可以引入多項式特徵解決一定的問題,但這樣我們要面對的問題就是——究竟引入多少纔算合適?而這個sigmoid函數能夠把linear的東西變成non-linear的,這就是一個進步。

而如果從神經網絡的生物學背景來看,我們會看到一個神經元一般有兩個狀態,即靜息狀態和興奮狀態,而我們的人工神經網絡從背景上來說是對人類神經系統行爲的一種模擬,因此一個取值在$[0,1]$之間的函數可以看做神經元狀態的一個反映——0代表靜息狀態,1代表興奮狀態,所以在神經網絡中,類似於sigmoid這樣的函數是很常用的,它們被稱作激活函數。

那麼說了這麼多,神經網絡究竟是在幹什麼呢?

從一定的意義上來說,神經網絡實際上實現的是函數的不斷複合——雖然即使引入了一個sigmoid函數,linear的東西可能還是無法擺脫linear,那我們就再設法複合上一層,這樣它就沒有那麼linear了,以此類推,當我們複合次數足夠多的時候,我們得到的最終結果將能夠擬合出這個“客觀正確”的預測模型。

這個過程是怎麼實現的呢?

首先神經網絡被分爲三個部分:輸入層、隱藏層和輸出層,每層有若干個節點,每個節點稱爲一個神經元,輸入層有$n$個節點,對應於輸入特徵的維度,輸出層有$k$個節點,對應於要預測的東西的維度。

而我們首先從單隱藏層的結構開始:假設我們只有一個隱藏層,這個隱藏層上有$n_{1}$個節點,那麼第$i$個節點要處理的輸入上一層的所有輸入,對於這第一個隱藏層就是$x_{1},...,x_{n}$,而後計算出$z_{i}=w_{i}^{T}x+b$(即輸入的一個線性函數),然後用自己的激活函數處理這個信號併發出,即第$i$個節點的輸出是$f_{i}(z_{i})$

那麼這個過程實際上就是對人類神經元活動的一種模擬:神經元接受上一個神經元發來的信號——神經元處理信號產生一個神經衝動傳遞給下一個神經元

那麼我們假設只有一個隱藏層,那麼這個隱藏層的輸出就是$f_{1}(z_{1}),...,f_{n_{1}}(z_{n_{1}})$,而下一個就到了輸出層,輸出層不需要激活函數,那麼輸出層得到的就是這些輸出的一個線性組合,即輸出層的第$j$個神經元輸出的就是$\omega_{j}^{T}f(z)+\beta_{j}$(無需在意符號,只是爲了區分各個層的參數)

那麼非常自然地,如果我們有很多個隱藏層,對於第二個隱藏層而言,其接受的輸入就是第一個隱藏層的輸出,以此類推,每個神經元還是按照自己的參數將上一層的輸出線性組合後扔進自己的激活函數裏輸出給下一層,直到到達輸出層爲止。

這樣的就構成了一個神經網絡,如果每一個神經元的激活函數選的足夠好,參數也訓練的足夠好,再忽略可能的過擬合問題,那麼這個神經網絡的表現應該是很不錯的。

但是...等等,這樣的神經網絡要怎麼訓練啊!

這個問題是巨大的——假設我們的神經網絡每層有10個神經元,而我們一共有10個隱藏層,這樣就需要100個神經元,而每個神經元要對前一層10個神經元的輸出做一個線性組合,這就需要11個參數,也就是說這樣一個“簡陋”的神經網絡我們就要訓練足足1100個參數(粗略估計,並不嚴格)!

那麼如果沒有一個合適的方法,訓練神經網絡的開銷將是災難性的。

幸運的是,回憶一開始的梯度下降方法,我們知道如果我們希望讓一個函數儘快到達最低點,我們只需按照一定的步長沿下降最快的方向前進即可,那麼在這裏是不是也可以引入類似的方法呢?

在神經網絡中我們需要的參數主要有$w,b$,那麼我們定義一個損失函數$J(w,b)$,那如果我們能求出一個梯度$\dfrac{\partial J}{\partial w_{i}}$,那我們當然可以按照梯度下降的方法更新$\hat{w_{i}}=w_{i}-\alpha \dfrac{\partial J}{\partial w}$

但是很遺憾的是,由於函數複合的複雜性,我們想要計算出一個損失函數對某個具體的$w_{i}$的偏導數是相當困難的,因爲這個$w_{i}$可能前面複合了一堆東西,後面又複合上了一堆東西,想要直接計算出這個偏導數是另一場災難。

幸運的是,我們可以從另一個角度來思考這個問題:我們從輸出層向前來考慮,假設輸入爲$y$,而這個$y$滿足$y=w_{d}^{T}f_{d}+b_{d}$(即最後一次線性組合),那麼我們可以很自然地看到:

$\dfrac{\partial J}{\partial w_{di}}=\dfrac{\partial J}{\partial y} \dfrac{\partial y}{\partial w_{di}}=\dfrac{\partial J}{\partial y} f_{di}$

那$f_{di}$是什麼呢?是前一層第$i$個神經元輸出給這個神經元的東西!

好像有眉目了,我們再往前推一層:

如果我們設$f_{di}=f_{di}(z_{di})$,其中$f_{di}(z)$是一個激活函數,而$z_{di}=w_{(d-1)i}^{T}f_{(d-1)}+b_{(d-1)i}$,那麼我們有:

$\dfrac{\partial J}{\partial w_{(d-1)ij}}= \dfrac{\partial J}{\partial y} \dfrac{\partial y}{\partial f_{di}} \dfrac{\partial f_{di}}{\partial z_{di}} \dfrac{\partial z_{di}}{\partial w_{(d-1)ij}}$

這一坨是什麼玩意?

我們觀察下這個式子:$\dfrac{\partial J}{\partial y}$沒啥好說的直接算就行,而$\dfrac{\partial y}{\partial f_{di}}=w_{di}$是直觀的,同時$\dfrac{\partial f_{di}}{\partial z_{di}}$就是對這個激活函數求的導數嘛,這也很容易,而由線性組合過程我們很容易得到$\dfrac{\partial z_{di}}{\partial w_{(d-1)ij}}=f_{(d-1)j}$,即上一層第$j$個神經元的輸出嘛!

那麼順着這個過程一路向前推,可以發現:某一層上對某個參數的偏導數應該是上一層對應神經元的輸出*本層對激活函數求的導數*損失函數回傳到這一層的梯度!

所謂損失函數回傳到這一層的梯度,可以這樣看待:我們要計算的是第$d$層第$i$個神經元中第$j$個參數的梯度,那麼這個梯度實際上就是:

$\dfrac{\partial J}{\partial y} \dfrac{\partial y}{\partial  f_{di}} \dfrac{\partial  f_{di}}{\partial z_{di}} \dfrac{\partial z_{di}}{\partial w_{dij}}$(下標可能與上式不太一致,理解一下就好)

 而最後一項是上一個神經元的輸入,倒數第二項是這個神經元對應激活函數的導數,而前面那項是什麼呢?

前面那項應該是$\dfrac{\partial J}{\partial y}\sum_{i=1}^{n_{1}}\dfrac{\partial y}{\partial f_{Di}}\sum_{j=1}^{n_{2}}\dfrac{\partial f_{Di}}{\partial f_{(D-1)j}}...$

那麼我們對每一層維護好這一坨東西乘到自己這個神經元的值,這樣就可以直接回傳給上一層的神經元了。

那麼我們總結一下BP神經網絡的流程:

首先要確定輸入、輸出和損失函數,然後選取神經網絡的結構(層數、每層的神經元個數、每層的激活函數、學習率),然後按照上述過程訓練至收斂就好。

代碼實現:

import numpy as np
from sklearn import datasets
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
from sklearn.svm import LinearSVC
import matplotlib.pyplot as plt
import pylab as plt
from sklearn.naive_bayes import GaussianNB
from sklearn.model_selection import train_test_split
import tensorflow as tf
from sklearn.neural_network import MLPClassifier


X,y=datasets.make_classification(n_samples=1000,n_features=20,n_informative=2,n_redundant=2)
X_train,X_test,Y_train,Y_test=train_test_split(X,y,test_size=0.25)

lr=LogisticRegression()
svc=LinearSVC(C=1.0)
rfc=RandomForestClassifier(n_estimators=100)#森林中樹的個數

lr=lr.fit(X_train,Y_train)
score1=lr.score(X_test,Y_test)
print(score1)

svc=svc.fit(X_train,Y_train)
score2=svc.score(X_test,Y_test)
print(score2)

rfc=rfc.fit(X_train,Y_train)
score3=rfc.score(X_test,Y_test)
print(score3)

gnb=GaussianNB()
gnb=gnb.fit(X_train,Y_train)
score4=gnb.score(X_test,Y_test)
print(score4)


sc=[]
siz=[]
i=5
while i<=100:
    clf = MLPClassifier(solver='lbfgs', alpha=1e-5,activation='logistic',
                    hidden_layer_sizes=(i,i), random_state=1,max_iter=500000)
    clf.fit(X_train, Y_train)
    sc.append(clf.score(X_test,Y_test))
    siz.append(i)
    i+=5
plt.plot(siz,sc,c='r')
plt.show()

這段代碼使用sklearn自帶的簡易神經網絡分類器來訓練一個模型,其中solver這個參數指定了梯度下降的方式,alpha指定了正則化參數,activation指定了激活函數,hidden_layer_sizes是一個元組,每一項對應於一個隱藏層中神經元的個數,max_iter決定了神經網絡最大的迭代次數,超過這個次數的話會返回一個錯誤

這是這個兩個隱藏層的神經網絡在這組數據上的表現隨每層神經元個數變化的圖像

import numpy as np
from sklearn import datasets
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
from sklearn.svm import LinearSVC
import matplotlib.pyplot as plt
import pylab as plt
from sklearn.naive_bayes import GaussianNB
from sklearn.model_selection import train_test_split
import tensorflow.compat.v1 as tf
tf.disable_v2_behavior()
from sklearn.neural_network import MLPClassifier


X,y=datasets.make_classification(n_samples=1000,n_features=20,n_informative=2,n_redundant=2)
X_train,X_test,Y_train,Y_test=train_test_split(X,y,test_size=0.25)

Y_train=np.array(Y_train).reshape(750,1)
Y_test=np.array(Y_test).reshape(250,1)

def add_layer(input, in_size, out_size, activation_function=None):
    w = tf.Variable(tf.random_normal([in_size, out_size]))
    b = tf.Variable(tf.zeros([1, out_size]) + 0.1)
    Z = tf.matmul(input, w) + b
    if activation_function == None:
        output = Z
    else:
        output = activation_function(Z)
    return output


xs = tf.compat.v1.placeholder(tf.float32, [None, 20])
ys = tf.compat.v1.placeholder(tf.float32, [None, 1])


hidden_layer1 = add_layer(xs, 20, 10, activation_function=tf.nn.relu)
hidden_layer2 = add_layer(hidden_layer1,10,10,activation_function=tf.nn.relu)

prediction = add_layer(hidden_layer2, 10, 1, activation_function=None)


loss = tf.reduce_mean(tf.reduce_sum(tf.square(ys - prediction), reduction_indices=[1]))

train_step = tf.train.GradientDescentOptimizer(0.1).minimize(loss)

init = tf.global_variables_initializer()
sess = tf.Session()
sess.run(init)
for i in range(1000):
    sess.run(train_step, feed_dict={xs: X_train, ys: Y_train})
    if i % 100 == 0:
        print(sess.run(loss, feed_dict={xs: X_test, ys: Y_test}))

sess.close()

這是用tensorflow搭建神經網絡的一個方法,首先我們需要自定義一個函數叫做add_layer,這個函數輸入的參數是而上一層是誰,上一層的神經元個數,本層神經元個數,和本層的激活函數

而每層的初始值可以設成隨機值,這裏的$w$設計成了一個矩陣,因爲對於上一層的第$i$個神經元和本層的第$j$個神經元而言對應的$w$值其實就是$w_{ij}$,而如果我們把一整層看做一個向量的話那麼兩層之間的線性變換過程實際就是一個矩陣乘法,最後根據激活函數進行下激活就完成了這一層的任務。

而初始我們需要定義兩個佔位符xs和ys表示輸入和輸出,第一維是None,第二維表示維度

接下來我們可以定義一些隱藏層和一個輸出層,以及一個損失函數,這裏按照參考格式寫就好。

而反向傳播的過程不需要我們來寫,TensorFlow提供一個train的包,可以決定按什麼方法進行優化,而參數是學習率,後面的minimize是我們要最小化的那個損失函數。

最後我們用tensorflow裏的一個叫Session的對象run這個神經網絡,就可以實現訓練的功能了。

小結與提高:

反向傳播神經網絡是最簡單的神經網絡,在很多任務上表現優越,但其也有很多問題,比如模型的可解釋性很差——事實上整個神經網絡雖然原理清晰,但真實過程就像一個黑盒,比如上面我們會看到模型的表現隨神經元個數在震盪,這樣的現象並不容易找到一個簡單的解釋。同時由於超參數很多,如何選取就成了一個大問題,同時由於模型的複雜性導致很容易出現過擬合的現象,同時模型的複雜還會帶來訓練成本的大幅上升,這些都是神經網絡所要面對的大問題。

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