編者按:當你面對一個新概念時,你會怎麼學習和實踐它?是耗費大量時間學習整個理論,掌握背後的算法、數學、假設、局限再親身實踐,還是從最簡單的基礎開始,通過具體項目解決一個個難題來提高你對它的整體把握?在這系列文章中,論智將採用第二種方法和讀者一起從頭理解機器學習。
「從零學習」系列第一篇從Python和R理解和編碼神經網絡來自Analytics Vidhya博主、印度資深數據科學開發人員SUNIL RAY。
本文將圍繞神經網絡構建的基礎知識展開,併集中討論網絡的應用方式,用Python和R語言實戰編碼。
神經網絡的基本工作原理
多層感知器及其基礎知識
神經網絡具體步驟詳解
神經網絡工作過程的可視化
如何用Numpy實現NN(Python)
如何用R語言實現NN
反向傳播算法的數學原理
如果你是一名開發者,或曾參與過編程項目,你一定知道如何在代碼中找bug。通過改變輸入和環境,你可以用相應的各種輸出測試bug位置,因為輸出的改變其實是一個提示,它能告訴你應該去檢查哪個模塊,甚至是哪一行。一旦你找到正確的那個它,並反覆調試,你總會得到理想的結果。
神經網絡其實也一樣。它通常需要幾個輸入,在經過多個隱藏層中神經元的處理後,它會在輸出層返回結果,這個過程就是神經網絡的「前向傳播」。
得到輸出後,接下來我們要做的就是用神經網絡的輸出和實際結果做對比。由於每一個神經元都可能增加最終輸出的誤差,所以我們要儘可能減少這個損耗(loss),使輸出更接近實際值。那該怎麼減少loss呢?
在神經網絡中,一種常用的做法是降低那些容易導致更多loss的神經元的權重/權值。因為這個過程需要返回神經元並找出錯誤所在,所以它也被稱為「反向傳播」。
為了在減少誤差的同時進行更少量的迭代,神經網絡也會使用一種名為「梯度下降」(Gradient Descent)的算法。這是一種基礎的優化算法,能幫助開發者快速高效地完成各種任務。
雖然這樣的表述太過簡單粗淺,但其實這就是神經網絡的基本工作原理。簡單的理解有助於你用簡單的方式去做一些基礎實現。
就像原子理論中物質是由一個個離散單元原子所構成的那樣,神經網絡的最基本單位是感知器(Perceptron)。那麼,感知器是什麼?
對於這個問題,我們可以這麼理解:感知器就是一種接收多個輸入並產生一個輸出的東西。如下圖所示:
感知器
示例中的它有3個輸入,卻只有一個輸出,由此我們產生的下一個合乎邏輯的問題就是輸入和輸出之間的對應關係是什麼。讓我們先從一些基本方法入手,再慢慢上升到更複雜的方法。
以下是我列舉的3種創建輸入輸出對應關係的方法:
直接組合輸入並根據閾值計算輸出。例如,我們設x1=0,x2=1,x3=1,閾值為0。如果x1+x2+x3>0,則輸出1;反之,輸出0。可以看到,在這個情景下上圖的最終輸出是1。
接下來,讓我們為各輸入添加權值。例如,我們設x1、x2、x3三個輸入的權重分別為w1、w2、w3,其中w1=2,w2=3,w3=4。為了計算輸出,我們需要將輸入乘以它們各自的權值,即2x1+3x2+4x3,再和閾值比較。可以發現,x3對輸出的影響比x1、x2更大。
接下來,讓我們添加bias(偏置,有時也稱閾值,但和上文閾值有區別)。每個感知器都有一個bias,它其實也是一種加權方式,可以反映感知器的靈活性。bias在某種程度上相當於線性方程y=ax+b中的常數b,可以讓函數上下移動。如果b=0,那分類線就要經過原點(0,0),這樣神經網絡的fit範圍會非常受限。例如,如果一個感知器有兩個輸入,它就需要3個權值,兩個對應給輸入,一個給bias。在這個情景下,上圖輸入的線性形式就是w1x1 + w2x2 + w3x3+1×b。
但是,這樣做之後每一層的輸出還是上層輸入的線性變換,這就有點無聊。於是人們想到把感知器發展成一種現在稱之為神經元的東西,它能將非線性變換(激活函數)用於輸入和loss。
什麼是激活函數(activation function)?
激活函數是把加權輸入(w1x1 + w2x2 + w3x3+1×b)的和作為自變量,然後讓神經元得出輸出值。
在上式中,我們將bias權值1表示為x0,將b表示為w0.
輸入—加權—求和—作為實參被激活函數計算—輸出
它主要用於進行非線性變換,使我們能擬合非線性假設、估計複雜函數,常用的函數有:Sigmoid、Tanh和ReLu。
前向傳播、反向傳播和Epoch
到目前為止,我們已經由輸入計算獲得了輸出,這個過程就是「前向傳播」(Forward Propagation)。但是,如果產出的估計值和實際值誤差太大怎麼辦?其實,神經網絡的工作過程可以被看作是一個試錯的過程,我們能根據輸出值的錯誤更新之前的bias和權值,這個回溯的行為就是「反向傳播」(Back Propagation)。
反向傳播算法(BP算法)是一種通過權衡輸出層的loss或錯誤,將其傳回網絡來發生作用的算法。它的目的是重新調整各項權重來使每個神經元產生的loss最小化,而要實現這一點,我們要做的第一步就是基於最終輸出計算每個節點之的梯度(導數)。具體的數學過程我們會在最後一節「反向傳播算法的數學原理」中詳細探討。
而這個由前向傳播和反向傳播構成的一輪迭代就是我們常說的一個訓練迭代,也就是Epoch。
多層感知器
現在,讓我們繼續回到例子,把注意力放到多層感知器上。截至目前,我們看到的只有一個由3個輸入節點x1、x2、x3構成的單一輸入層,以及一個只包含單個神經元的輸出層。誠然,如果是解決線性問題,單層網絡確實能做到這一步,但如果要學習非線性函數,那我們就需要一個多層感知器(MLP),即在輸入層和輸出層之間插入一個隱藏層。如下圖所示:
圖片中的綠色部分表示隱藏層,雖然上圖只有一個,但事實上,這樣一個網絡可以包含多個隱藏層。同時,需要注意的一點是,MLP至少由三層節點組成,並且所有層都是完全連接的,即每一層中(除輸入層和輸出層)的每一個節點都要連接到前/後層中的每個節點。
理解了這一點,我們就能進入下一個主題,即神經網絡優化算法(誤差最小化)。在這裡,我們主要介紹最簡單的梯度下降。
批量梯度下降和隨機梯度下降
梯度下降一般有三種形式:批量梯度下降法(Batch Gradient Descent)隨機梯度下降法(Stochastic Gradient Descent)和小批量梯度下降法(Mini-Batch Gradient Descent)。由於本文為入門向,我們就先來了解滿批量梯度下降法(Full BGD)和隨機梯度下降法(SGD)。
這兩種梯度下降形式使用的是同一種更新算法,它們通過更新MLP的權值來達到優化網絡的目的。不同的是,滿批量梯度下降法通過反覆更新權值來使誤差降低,它的每一次更新都要用到所有訓練數據,這在數據量龐大時會耗費太多時間。而隨機梯度下降法則只抽取一個或多個樣本(非所有數據)來迭代更新一次,較之前者,它在耗時上有不小的優勢。
讓我們來舉個例子:假設現在我們有一個包含10個數據點的數據集,它有w1、w2兩個權值。
在這一節中,讓我們來看看神經網絡(MLP包含隱藏層,類似上圖架構)的具體構建方法。
如上圖所示,我們的神經網絡共有3層,它的思路可以被大致概括為:
第0步 確定輸入和輸出
我們定義:
第1步 用一個隨機值初始化網絡的權值和bias(只在開始時使用,下一次迭代我們會用更新後的值)
我們定義:
wh為隱藏層的權值矩陣;
bh為隱藏層的bias矩陣;
wout為輸出層的權值矩陣;
bout為輸出層的bias矩陣。
第2步 將輸入矩陣和權值的矩陣點乘積分配給輸入和隱藏層之間的邊緣,再加上隱藏層的bias形成相應輸出,這個過程被稱作線性變換
hiddenlayerinput = matrixdotproduct(X,wh)+bh
第3步 引入激活函數(Sigmoid)執行非線性變換,Sigmoid輸出1/(1+exp(-x))
hiddenlayeractivations = sigmoid(hiddenlayer_input)
第4步 對隱藏層進行線性變換(在原矩陣點乘積的基礎上乘以新權值矩陣並加上輸出層bias),使用激活函數,並預測輸出
outputlayerinput = matrixdotproduct (hiddenlayeractivations × wout ) + bout output = sigmoid(outputlayer_input)
以上步驟即為「前向傳播」。
第5步 將預測值與實際值相比較,並計算誤差E(Actual-Predicted)的梯度,它是一個平方誤差函數((y-t)^2)/2(衡量期望誤差的一個常見做法是採用平方誤差測度)
E=y–output
第6步 計算隱藏層和輸出層神經元的斜率/梯度(對每個神經元每一層的非線性激活函數x求導),Sigmoid的梯度會返回為x(1–x)
slopeoutputlayer = derivativessigmoid(output) slopehiddenlayer = derivativessigmoid(hiddenlayer_activations)
第7步 用誤差E的梯度和輸出層激活函數梯度計算輸出層的變化因子(delta)
doutput = E × slopeoutput_layer
第8步 這時,誤差E已經回到神經網絡中,也就是在隱藏層中。為了計算它的梯度,我們需要用到輸出變化因子delta中的點和隱藏層輸出層之間的權重參數
Errorathiddenlayer = matrixdotproduct(doutput,wout.Transpose)
第9步 計算隱藏層的變化因子(delta),將得到的誤差和隱藏層激活函數導數相乘
dhiddenlayer = Errorathiddenlayer × slopehiddenlayer
第10步 更新輸出層和隱藏層的權值:可以用訓練樣本的誤差更新權值
wout = wout + matrixdotproduct(hiddenlayeractivations.Transpose,doutput)× learning_rate
wh = wh + matrixdotproduct(X.Transpose,dhiddenlayer)× learningrate
學習率(learning rate):權值更新速率由自定義超參數學習率決定。
第11步 更新輸出層和隱藏層的bias:神經網絡中的bias可以由神經元中的累積誤差求導更新
即:
bh = bh + sum(dhiddenlayer, axis=0)×learningrate
bout = bout + sum(doutput, axis=0)×learningrate
我們稱5—11為「反向傳播」。
我們把一個前向傳播和一個反向傳播的迭代成為一個訓練周期。正如我在前文中提到的,如果我們再進行訓練,那更新後的權值和bias就會用於新一次前向傳播。
我會在這一節中用圖表形式重新介紹上一節的內容,以幫助入門者更好地了解神經網絡(MLP)的工作方法。
注意:
為了更好的呈現效果,我只保留了2位或3位小數;
黃色單元格表示當前活動的神經元(單元、節點);
橙色單元格表示用於更新當前單元格值的輸入。
第0步 讀取輸入和輸出
第1步 用隨機值初始化權值和bias
第2步 計算隱藏層輸入
hiddenlayerinput = matrixdotproduct(X,wh)+ bh
第3步 對隱藏層的線性輸入執行非線性變換(激活函數)
hiddenlayeractivations = sigmoid(hiddenlayer_input)
第4步 在輸出層對已經執行了線性、非線性變換的隱藏層使用激活函數
outputlayerinput = matrixdotproduct(hiddenlayeractivations × wout)+ bout output = sigmoid(outputlayer_input)
第5步 計算輸出層誤差E的梯度
E = y-output
第6步 計算輸出層和隱藏層的梯度
Slopeoutputlayer= derivativessigmoid(output) Slopehiddenlayer = derivativessigmoid(hiddenlayer_activations)
第7步 計算輸出層的delta
doutput = E × slopeoutput_layer*lr
第8步 計算隱藏層的誤差
Errorathiddenlayer = matrixdotproduct(doutput, wout.Transpose)
第9步 計算隱藏層的delta
dhiddenlayer = Errorathiddenlayer × slopehiddenlayer
第10步 更新輸出層和隱藏層的權值
wout = wout + matrixdotproduct(hiddenlayeractivations.Transpose, doutput)×learningrate wh = wh+ matrixdotproduct(X.Transpose,dhiddenlayer)×learning_rate
第11步 更新輸出層和隱藏層的bias
bh = bh + sum(dhiddenlayer, axis=0) ×learningrate bout = bout + sum(doutput, axis=0)×learningrate
由表格數據可以看出,由於我們只進行了一次迭代,因此預測值和實際值的誤差還相對過大。如果我們多次訓練模型,那它們最終將趨於靠近。事實上,我之後已經對這個模型進行了上千次迭代,最後得到了和目標值很相近的結果:[[ 0.98032096] [ 0.96845624] [ 0.04532167]]。
import numpy as np
#Input array
X=np.array([[1,0,1,0],[1,0,1,1],[0,1,0,1]])
#Output
y=np.array([[1],[1],[0]])
#Sigmoid Function
def sigmoid (x):
return 1/(1 + np.exp(-x))
#Derivative of Sigmoid Function
def derivatives_sigmoid(x):
return x * (1 - x)
#Variable initialization
epoch=5000 #Setting training iterations
lr=0.1 #Setting learning rate
inputlayer_neurons = X.shape[1] #number of features in data set
hiddenlayer_neurons = 3 #number of hidden layers neurons
output_neurons = 1 #number of neurons at output layer
#weight and bias initialization
wh=np.random.uniform(size=(inputlayer_neurons,hiddenlayer_neurons))
bh=np.random.uniform(size=(1,hiddenlayer_neurons))
wout=np.random.uniform(size=(hiddenlayer_neurons,output_neurons))
bout=np.random.uniform(size=(1,output_neurons))
for i in range(epoch):
#Forward Propogation
hidden_layer_input1=np.dot(X,wh)
hidden_layer_input=hidden_layer_input1 + bh
hiddenlayer_activations = sigmoid(hidden_layer_input)
output_layer_input1=np.dot(hiddenlayer_activations,wout)
output_layer_input= output_layer_input1+ bout
output = sigmoid(output_layer_input)
#Backpropagation
E = y-output
slope_output_layer = derivatives_sigmoid(output)
slope_hidden_layer = derivatives_sigmoid(hiddenlayer_activations)
d_output = E * slope_output_layer
Error_at_hidden_layer = d_output.dot(wout.T)
d_hiddenlayer = Error_at_hidden_layer * slope_hidden_layer
wout += hiddenlayer_activations.T.dot(d_output) *lr
bout += np.sum(d_output, axis=0,keepdims=True) *lr
wh += X.T.dot(d_hiddenlayer) *lr
bh += np.sum(d_hiddenlayer, axis=0,keepdims=True) *lr
print output
# input matrix
X=matrix(c(1,0,1,0,1,0,1,1,0,1,0,1),nrow = 3, ncol=4,byrow = TRUE)
# output matrix
Y=matrix(c(1,1,0),byrow=FALSE)
#sigmoid function
sigmoid<-function(x){
1/(1+exp(-x))
}
# derivative of sigmoid function
derivatives_sigmoid<-function(x){
x*(1-x)
}
# variable initialization
epoch=5000
lr=0.1
inputlayer_neurons=ncol(X)
hiddenlayer_neurons=3
output_neurons=1
#weight and bias initialization
wh=matrix( rnorm(inputlayer_neurons*hiddenlayer_neurons,mean=0,sd=1), inputlayer_neurons, hiddenlayer_neurons)
bias_in=runif(hiddenlayer_neurons)
bias_in_temp=rep(bias_in, nrow(X))
bh=matrix(bias_in_temp, nrow = nrow(X), byrow = FALSE)
wout=matrix( rnorm(hiddenlayer_neurons*output_neurons,mean=0,sd=1), hiddenlayer_neurons, output_neurons)
bias_out=runif(output_neurons)
bias_out_temp=rep(bias_out,nrow(X))
bout=matrix(bias_out_temp,nrow = nrow(X),byrow = FALSE)
# forward propagation
for(i in 1:epoch){
hidden_layer_input1= X%*%wh
hidden_layer_input=hidden_layer_input1+bh
hidden_layer_activations=sigmoid(hidden_layer_input)
output_layer_input1=hidden_layer_activations%*%wout
output_layer_input=output_layer_input1+bout
output= sigmoid(output_layer_input)
# Back Propagation
E=Y-output
slope_output_layer=derivatives_sigmoid(output)
slope_hidden_layer=derivatives_sigmoid(hidden_layer_activations)
d_output=E*slope_output_layer
Error_at_hidden_layer=d_output%*%t(wout)
d_hiddenlayer=Error_at_hidden_layer*slope_hidden_layer
wout= wout + (t(hidden_layer_activations)%*%d_output)*lr
bout= bout+rowSums(d_output)*lr
wh = wh +(t(X)%*%d_hiddenlayer)*lr
bh = bh + rowSums(d_hiddenlayer)*lr
}
output
設Wi為輸入層與隱藏層之間的權值,Wh為隱藏層和輸出層之間的權值。
h=σ(u)= σ (WiX),其中h是u的函數,u是Wi和x的函數。這裡我們將激活函數表示為σ。
Y=σ(u')=σ(Whh),其中Y是u'的函數,u'是Wh和h的函數。
利用上述等式,我們可以計算偏導數。
我們需要計算∂E/∂Wi和∂E/∂Wh,其中前者是改變輸入層、隱藏層之間權值造成的誤差(E)的變化,後者是改變隱藏層、輸出層之間權值造成的誤差(E)的變化。
為了得出這兩個偏導數,我們需要用到鏈式法則,因為E是Y的函數,Y是u'的函數,而u'是Wi的函數。
讓我們結合它們的關係來計算梯度:
∂E/∂Wh = (∂E/∂Y).( ∂Y/∂u』).( ∂u』/∂Wh)……(1)
已知誤差E=((Y-t)^2)/2,可得(∂E/∂Y)= (Y-t)。
由於激活函數σ求導後的形式是σ(1-σ),因此
(∂Y/∂u』)= ∂( σ(u』)/ ∂u』= σ(u』)(1-σ(u』))
但是因為σ(u』)=Y,所以
(∂Y/∂u』)=Y(1-Y)
現在我們就能得到( ∂u』/∂Wh)= ∂( Whh)/ ∂Wh = h
把這個等式帶入式(1)可得
∂E/∂Wh = (Y-t). Y(1-Y).h
現在我們已經得出了隱藏層和輸出層之間的梯度,是時候該計算輸入層和隱藏層之間的梯度了。
∂E/∂Wi =(∂E/∂h). (∂h/∂u).( ∂u/∂Wi)
但是,(∂E/∂h) = (∂E/∂Y).( ∂Y/∂u』).( ∂u』/∂h)也同樣成立,將這個式子帶入上式後,
∂E/∂Wi =[(∂E/∂Y).( ∂Y/∂u』).( ∂u』/∂h)]. (∂h/∂u).( ∂u/∂Wi)……(2)
可能會有人有一位,為什麼我們要先計算隱藏層和輸出層之間的梯度呢?這個問題的解答就在式(2)中。我們可以發現,由於之前我們已經先計算了∂E/∂Y和∂Y/∂u』,在進行之後的計算時,我們無需進行大量重複計算,這大大節約了資源佔用和計算時間,提高了整體效率。這也是這個算法被稱為反向傳播算法的原因。
解下來讓我們計算式(2)中的未知導數。
∂u』/∂h = ∂(Whh)/ ∂h = Wh
∂h/∂u = ∂( σ(u)/ ∂u= σ(u)(1- σ(u))
因為σ(u)=h,所以
(∂Y/∂u)=h(1-h)
我們可以得到∂u/∂Wi = ∂(WiX)/ ∂Wi = X
將它代入式(2)我們可得梯度
∂E/∂Wi = [(Y-t). Y(1-Y).Wh].h(1-h).X
既然我們已經計算得到兩個梯度,那神經網絡的權值就可以更新為
Wh = Wh + η . ∂E/∂Wh
Wi = Wi + η . ∂E/∂Wi
其中η表示學習率。
這裡我們可以再一次回到這個算法的命名上。通過對比∂E/∂Wh和∂E/∂Wi的最終形式我們可以發現,預測值和實際值的誤差(Y-t)在權值更新的過程中被作為了輸入層。
那這些數學計算是怎麼和代碼對應的呢?
hiddenlayer_activations = H
E = Yt
Slope_output_layer = Y(1-Y)
lr =η
slope_hidden_layer = h(1-h)
wout = W<sub>h</sub>
本文以神經網絡中最基礎的兩層神經網絡作為講解對象,雖然內容已經很翔實,但為了更形象地解釋輸入、輸出、權值、bias等概念在圖中的位置,論智君在文末為讀者做一些基礎知識補充。上圖是一個簡單的兩層神經網絡,它由一些圓形和向量組成,其中的圓圈就是我們說的神經元,有時也稱單元(unit)和節點(node),它主要起到計算和儲存作用,因此其實發揮重要作用的是圖中的各條帶箭頭的線。
文章中涉及大量「輸入層隱藏層之間的權值」「隱藏層輸出層之間的權值」的表述,這些都可以從圖片中獲得直觀感受。而我們反覆提起的偏置項bias在上圖中也有所展示。理論上,bias是個儲存值永遠為1的單元,它出現在除了輸入層之外的所有層,與後一層的所有節點都有連接。神經網絡結構圖一般不會把它明確畫出來,但我們應該知道它是存在的。
本文把多處神經網絡都注釋為MLP,並詳細介紹了多層感知器的設計演變。需要注意的是,MLP曾經就指代神經網絡,但是它表示的是兩層神經網絡,即包含一個輸入層、隱藏層、輸出層的神經網絡,一般不能被用來表示層數非常深的神經網絡。
論智也沿用了損失的英文原型loss,區別於error誤差。另外會有人把loss譯為殘差、代價,尤其是在談及loss function的時候,它們其實是一種東西。
最後再提一點閾值和參數。文章在介紹bias時用了它的英文單詞,沒有用它的譯名偏置項、閾值,這是為了防止誤讀,尤其是閾值,它容易與案例中的約束條件混淆。事實上,bias作為閾值最生動的體現是在圖像上,它能限制曲線的峰值區間,具體可以Google相關圖片。而本文儘量避免了「參數」這個詞的使用,因為一般而言,神經網絡參數就是指訓練得到的權值和bias,把這個概念用於任何一方都是不合理的,它和其他機器學習方法中的參數也不是一個概念,在閱讀其他原創、翻譯內容時,希望讀者能擦亮雙眼。
原文地址:www.analyticsvidhya.com/blog/2017/05/neural-network-from-scratch-in-python-and-r/