神經網路-標準神經網路(python3)

神經網路-標準神經網路(python3)

來自專欄「自然語言處理」的不歸路5 人贊了文章

前言

之前閱讀了很多有關神經網路的書籍、論文以及博客,發現大多分為兩種情況。一種是例如《深度學習》這類,裡面堆砌了大量的理論和公式,如果細心研讀的話的確可以將神經網路中的數學原理理解的很透徹。可是,當看完《深度學習》,如果有人讓我做個簡單的神經網路分類器,我可能還是一頭霧水,不知從何下手。第二種是例如《XX實戰》,《XX快速上手》這類,跟著書中的代碼確實可以用神經網路解決實際問題,可是書中又沒有對底層的數學原理進行比較透徹的講解。經常是對數學原理做個簡單介紹,讓讀者對其有個大致了解,然後就開始各種調用現成的包。調用一個包,幾行代碼就搭起一個神經網路,雖然感覺很暢快。可是只會調包又有什麼用呢?

於是,在與同學進行討論後,我們決定自己來總結這樣一套內容。其中既對神經網路中的數學原理進行細緻到每一個步驟的剖析,又能實際搭建一個能夠執行具體任務的神經網路。我們打算採用一種全新的行文構造,來達到一步一剖析(數學原理),一步一實踐(代碼實現)的目標。這種行文構造是:首先提出某種需要解決的實際問題。以解決這個實際問題為出發點,從第一步到最後一步,在解決問題的每一個步驟中,將數學原理展開剖析,然後落實到代碼實現。而在最後,還將會不厭其煩地採用用某些框架調包再次解決該問題。以達到上得廳堂(會用現成的框架快速的解決實際問題)、下得廚房(懂的神經網路每一步的數學原理)的學習效果。

其實我們寫下這樣一套總結首先是為了我們自身的提升,將我們自己所學的知識凝練為文字表述出來,能夠極大的加深我們對所學知識的理解和領悟。其次,當然也是希望能夠為這一領域盡一點綿薄之力,希望能夠幫助到依然對神經網路比較迷茫的同行及學生。最後,常言道,人外有人,天外有天。我們哈爾濱工程大學並不算名牌高校,我們幾個人的水平也著實有限。所以,我們竭誠希望各位大咖能夠為本文提出寶貴的意見和建議。我們十分期望向您學習,並與您共同探討,共同進步。

標準神經網路

標準神經網路是最為普通、常規的神經網路。是其他神經網路如「卷積神經網路(CNNs)」、「遞歸/循環神經網路(RNNs)」的基礎。其他神經網路都是在標準神經網路的基礎上進行改造而來的。

標準神經網路有時也稱為反向傳播神經網路(Back propagation neural network)或簡稱為BP神經網路,有時也叫做前饋反向傳播全連接神經網路。至於何為「反向傳播」,何為「前饋」,以及何為「全連接」在後面會介紹到的。我們先根據圖1,對神經網路的構造有個大體的認識。

圖1:標準神經網路(包含輸入層、隱藏層和輸出層)

圖1所示是一個標準的神經網路構造圖。圓圈代表神經元,圓圈之間的箭頭代表神經元之間的有向連接。我們首先將數據輸入到輸入層。數據有多少個值,輸入層就有多少個神經元。然後輸入層各個神經元內的數據值按著箭頭的方向流向下一層,每個箭頭都有不同的權重,所以數據值在流經不同的箭頭時會與不同的權重相乘。就這樣,將數據輸入到輸入層,就會在輸出層得到我們想要的輸出。如果輸出和我們預期的結果有一定差距,那麼就適當修改箭頭的權重,使神經網路的輸出向更加符合我們預期的方向靠近。

明確任務目標

首先我們需要一個難以進行線性分離的數據集。這裡我們使用螺旋數據集,其通過如下代碼生成。在這裡我們不關注產生螺旋數據集的具體細節,因為這偏離了本文的主題。我們只需要複製粘貼下面這段代碼,即可得到如圖2所示的螺旋數據集:

N = 100 # 每類樣本點的個數D = 2 # 維度(每個樣本點有2個特徵,即橫坐標和縱坐標)K = 3 # 類別數(共三種不同類別的樣本點)X = np.zeros((N*K,D)) # 數據矩陣 (每一行是一個樣本)y = np.zeros(N*K, dtype=uint8) # 類標籤for j in range(K): ix = list(range(N*j,N*(j+1))) r = np.linspace(0.0,1,N) t = np.linspace(j*4,(j+1)*4,N) + np.random.randn(N)*0.2 X[ix] = np.c_[r*np.sin(t), r*np.cos(t)] y[ix] = j# 將數據可視化plt.scatter(X[:, 0], X[:, 1], c=y, s=40, cmap=plt.cm.Spectral)plt.show()

圖2:螺旋數據集

通過觀察我們看到螺旋數據集中有三種不同類型的樣本點,每個樣本點有兩個特徵,分別是橫坐標和縱坐標。於是我們的任務目標就來了:即把這些有兩個特徵的樣本點正確分類。具體來說就是,將一個樣本點的兩個特徵值(橫坐標值與縱坐標值)作為輸入,得到一個關於這個樣本點屬於某個類別的輸出。如果這個輸出符合我們的預期(即輸出的類別就是該樣本點的正確類別),那很完美,如果不符合我們的預期,就對神經網路中的權重進行調整。

預處理

通常我們需要對數據集進行預處理。在本例中,所有特徵值已經很好的介於-1至1之間,因此我們可以跳過此步驟。

初始化輸入層和隱藏層之間的參數

之前提到過,數據有多少個值,輸入層就有多少個神經元。那麼「數據有多少個值」是什麼含義?其實就是指一個樣本點有多少個特徵。在這裡,一個樣本點有兩個特徵,那麼輸入層就有兩個神經元。我們以某個樣本點為例,比如一個坐標為(0.25, 0.50)的綠色樣本點。輸入到輸入層之後如圖3所示:

圖3:將坐標為(0.25,0.50)的樣本點輸入到輸入層

我們看到每個特徵值都與隱藏層的所有神經元相連,這也就是「全連接」一詞的由來。即每一個神經元都與後一層的所有神經元相連接。由圖我們能夠看出,如果隱藏層有兩個神經元,那麼輸入層和隱藏層之間就需要有2×2=4個連接權重。

能夠看到流入到隱藏層第一個神經元的數值應該為 0.25	imes w_{11}+0.50	imes w_{21} ,但其實還應該加上一個稱之為偏置的數值 b ,即 0.25	imes w_{11}+0.50	imes w_{21}+b_1為流入到隱藏層第一個神經元的數值。同理, 0.25	imes w_{12}+0.50	imes w_{22}+b_2 為流入到隱藏層第二個神經元的數值。將其寫為矩陣的形式,即如下:

[0.25, 0.50] left[ egin{matrix} w_{11} & w_{12} \ w_{21} & w_{22} \ end{matrix} 
ight] +[b_1, b_2] =[s_1, s_2]

其中 s_1s_2 分別代表流入隱藏層第一個神經元和二個神經元的數值。

那麼如果我們隱藏層有100個神經元,於是輸入層和隱藏層之間就有2×100=200個連接權重。同理其矩陣乘法應是如下形式:

[0.25, 0.50] left[ egin{matrix} w_{11} & w_{12} & ... & w_{1100} \ w_{21} & w_{22} & ... & w_{2100}\ end{matrix} 
ight] +[b_1, b_2...b_{100}] =[s_1, s_2... s_{100}]

由於最開始我們也不知道這些連接權重以及偏置的值應該是多少,於是我們將這些權重的初始值設置為隨機值。我們將其設置為服從均值為0,標準差為1的正態分布的隨機值,這樣更符合物理世界的一般規律,即靠近0附近的值更多一些。而將偏置初始化為0值:

# 初始化參數W1 = 0.01 * np.random.randn(D, 100) b1 = np.zeros((1,100))

可以試著將權重矩陣列印出來,能夠直觀地看到這是一個2行100列的矩陣。而偏置是一個1行100列的行向量。

從隱藏層流出

根據上述矩陣乘法公式,可以很容易的求出流入到隱藏層神經元的值。然而,流入到隱藏層神經元的值並非原封不動地再從隱藏層神經元流出。而是經過一個稱之為「激活函數」的函數進行激活。簡而言之,就是要把流入到隱藏層神經元的值作為某個函數的輸入,得到該函數的輸出,將該函數的輸出作為流出隱藏層神經元的值。

這裡我們使用ReLu函數作為隱藏層神經元的激活函數,即 max(0,x) 。ReLu函數圖像如圖4所示:

圖4:ReLu函數圖像

顯然,當ReLu函數的輸入小於0時,其輸出為0。當ReLu函數的輸入大於0時,其輸出等於輸入值。由此,可以算出隱藏層的輸出,其代碼如下:

hidden_layer = np.maximum(0, np.dot(X, W1) + b1)

其中,hidden_layer 代表隱藏層的輸出。理論上來說這應該是一個1行100列的行向量。但是值得注意的是,我們並非只用單獨一個樣本點的特徵向量做運算,而是用300個樣本點的特徵矩陣 X 同時做運算。300個樣本點的特徵矩陣 X 如下:

left[ egin{matrix} x^{(1)}_1 & x^{(1)}_2 \ ......\ \ x^{(300)}_1 & x^{(300)}_2end{matrix} 
ight]

因此,最終得到的輸出是300行100列的輸出矩陣(儘管偏置向量設置為1行100列的,但是python在執行300行100列的矩陣與1行100列的向量相加時,會自動將1行100列的向量按行複製300份,使偏置成為一個300行100列其中每行都相等的矩陣)。

從輸出層流出

之前我們初始化了輸入層和隱藏層之間的參數,包括2行100列的權重矩陣和1行100列的偏置向量。這次我們用同樣的方法,初始化隱藏層和輸出層之間的參數。我們令輸出層包含3個神經元。輸出層使用3個神經元的原因是樣本點分為三種類型。因此,在輸出層的三個神經元的輸出值中,若第一個神經元的輸出值最大,則可認為該樣本點屬於一類。同理,若第二個神經元的輸出值最大,則可認為該樣本點屬於二類。已知隱藏層的輸出即hidden_layer是300行100列的行向量,因此隱藏層到輸出層之間的權重矩陣應該是100行3列的矩陣。而偏置向量應該是1行3列的行向量。初始化隱藏層到輸出層之間的權重矩陣和偏置向量代碼如下:

W2 = 0.01 * np.random.randn(100,K)b2 = np.zeros((1,K))

然後將隱藏層的輸出值hidden_layer與參數做運算,得到輸出層的輸出:

scores = np.dot(hidden_layer, W2) + b2

我們將輸出層的輸出稱為分數向量,因為輸出的3個值分別代表了樣本點可能屬於每個類別的分數。最終得到的顯然是一個300行3列的分數矩陣。其中每行代表每個樣本點,每列代表對應每個類別的分數。我們可以將分數矩陣直觀地列印出來,如圖5:

圖5:300行3列的分數矩陣

衡量與與真實情況的差距

接下來的一個關鍵因素就是損失函數,我們需要用它來計算我們的損失。那麼何為」損失「?」損失「即預測結果和真實情況相差多少。真實情況和預測結果相差越大,損失值越大。真實情況和預測結果越接近,損失值越小。在這裡,我們希望正確的類別應該比其他類別有更高的分數,若確實如此,則損失應該很低,否則損失應該很高。量化這種直覺的方法有很多種,但在這裡我們使用與交叉熵損失。我們首先用 f 表示一個樣本的類別分數向量(即包含三個數值的向量),於是損失函數如下:

L_i=-log(frac{e^{f_{y_i}}}{sum_je^{f_j}})

我們首先對上述公式進行解讀:首先 L_i 代表樣本 i 的損失值。 y_i 代表樣本 i 的真實類別標籤(如果樣本 i 屬於一類,則類別標籤是1),因此若樣本 i 的類別分數向量為 f=[0.23, 0.32, 0.17],且樣本 i 屬於一類,則 f_{y_i}=0.23 。而 j 代表 f 中元素的索引。故對於樣本分數向量為 f=[0.23, 0.32, 0.17] 的樣本 i 來說,損失值 L_i=-log(frac{e^{0.23}}{e^{0.23}+e^{0.32}+e^{0.17}})

我們能夠看出來, frac{e^{0.23}}{e^{0.23}+e^{0.32}+e^{0.17}} 的值永遠介於0和1之間,我們將這稱為真實類別的歸一化概率。顯然,樣本 i 的真實類別的分數越高,則真實類別的歸一化概率越接近1,因此損失值 L_i 越低。反之,若樣本 i 的真實類別對應的分數較低,則真實類別的歸一化概率越接近0,因此損失值 L_i 越高。

我們在這裡求出300個樣本的平均損失值,以衡量神經網路的性能。我們稱之為平均交叉熵損失:

L=frac1Nsum_iL_i+frac12lambdasum_ksum_lW^2_{k,l}

公式的前半部分自然是300個樣本的交叉熵損失的平均值,即平均交叉熵損失。而後半部分,稱之為正則化損失。何為」正則化損失「?我們通過一個例子來理解正則化損失,比如樣本點 i 的兩個特徵即橫縱坐標為(1.0,1.0)。而假設此時有兩組不同的權重向量 W_1=[1,0],W_2=[0.5,0.5] 。樣本的特徵與兩組不同的權重做內積的結果相等,都為1。可是加上正則化損失後,很明顯 0.5	imes0.5+0.5	imes0.5<1	imes1+0	imes0 。所以選擇 w_2 作為權重,正則化損失更小。正則化損失的作用在於增強泛化能力,去除權重的不確定性。

那麼現在就可以根據公式以及已知的分數矩陣,計算損失了。我們首先將分數矩陣轉換為概率矩陣:

# 得到樣本數量num_examples = X.shape[0]# 得到非歸一化概率exp_scores = np.exp(scores)# 得到每個樣本對應各個類別的歸一化概率probs = exp_scores / np.sum(exp_scores, axis=1, keepdims=True)

現在我們有了一個300行3列的概率矩陣,並且我們已經將每一行的概率都進行了歸一化,使得每一行的三個概率之和為1。現在我們就可以將每個樣本對應真實類別的概率提取出來,做 -log() 映射。

corect_logprobs = -np.log(probs[range(num_examples),y])

於是得到了一個包含300個元素的一維向量,其中每個元素都是相應樣本的交叉熵損失值。接下來計算平均交叉熵損失以及正則化損失,並將二者相加:

# 計算損失:平均交叉熵損失和正則化損失data_loss = np.sum(corect_logprobs)/num_examplesreg_loss = 0.5 * reg * np.sum(W1 * W1) + 0.5 * reg * np.sum(W2 * W2)loss = data_loss + reg_loss

更新參數

我們的參數是隨機初始化的,所以神經網路輸出的結果必然和真實情況有所差距。而我們現在的目標是使這種差距儘可能的小,即找到參數W1、b1、W2、b2取什麼值的時候,損失值最小。我們可以求損失對參數的導數,用梯度下降找到導數為0的點,即為極小值點。這裡我們在 fL_i 之間引入一個中間變數 P ,其含義為一個歸一化概率的向量。於是樣本 i 的損失為:

L_i=-log(p_{y_i})P_k=frac{e^{f_k}}{sum_je^{f_j}}

我們現在想要知道樣本 i 的分數向量 f 中的元素 f_k 如何改變才能減少損失 L_i ,從而減少整體損失 L 。因此,我們需要求出 frac{partial{L_i}}{partial{f_k}} 。而由於在 L_if 之間引入了中間變數 P ,因此 frac{partial{L_i}}{partial{f_k}}=frac{partial{L_i}}{partial{P_{y_i}}}cdot frac{partial{P_{y_i}}}{partial{f_k}} 。容易得出 frac{partial{L_i}}{partial{p_{y_i}}}=-frac{1}{p_{y_i}} 。而對於 frac{partial{P_{y_i}}}{partial{f_k}} ,則分兩種情況:

1)當 k = y_i 時:

frac{partial{P_{y_i}}}{partial{f_k}}=frac{partial}{partial{f_k}}(frac{e^{f_k}}{sum_je^{f_j}})

=frac{(e^{f_k})cdotsum_je^{f_j}-e^{f_k}cdot e^{f_k}}{(sum_je^{f_j})^2}

=frac{e^{f_k}}{sum_je^{f_j}}-frac{e^{f_k}}{sum_je^{f_j}}cdotfrac{e^{f_k}}{sum_je^{f_j}}

因為 k = y_i ,故上式等於:

P_{y_i}cdot(1-P_{y_i})

因此: frac{partial{L_i}}{partial{f_{k}}}=-frac{1}{p_{y_i}}cdot p_{y_i}cdot(1-p_{y_i})=p_{y_i} - 1 (k = y_i)

2)當 k 
eq y_i 時:

frac{partial{P_{y_i}}}{partial{f_k}}=frac{partial}{partial{f_k}}(frac{e^{f_{y_i}}}{sum_je^{f_j}})

=frac{0cdot sum_je^{f_j}-e^{f_{y_i}}cdot e^{f_k}}{(sum_je^{f_j})^2}

=-frac{e^{f_{y_i}}}{sum_je^{f_j}}cdotfrac{e^{f_k}}{sum_je^{f_j}}=-P_{y_i}cdot P_k

因此: frac{partial{L_i}}{partial{f_{k}}}=-frac{1}{P_{y_i}}cdot(-P_{y_i}cdot P_k)=P_k(k 
eq y_i)

故最後求得: frac{partial{L_i}}{partial{f_k}}=p_k - 1(k = y_i)

=p_k(k 
eq y_i)

因此我們能夠得到 L_if 的梯度向量。假設樣本 i 的歸一化概率向量為 P = [0.12, 0.84, 0.04] ,且樣本 i 的真實類別標籤為1,則 L_if 的梯度向量為 [0.12-1, 0.84, 0.04] 。以下代碼為上述過程的實現。其中prob存儲300個樣本的歸一化類別概率矩陣,dscore存儲損失對分數的梯度矩陣:

dscores = probsdscores[range(num_examples),y] -= 1dscores /= num_examples

而由於scores = np.dot(hidden_layer, W2) + b2,因此可知scores對W2的梯度矩陣應該是hidden_layer的轉置,而對b2的梯度矩陣應該所有元素都為1。且有損失對分數的梯度存儲在dscores中,因此根據鏈式求導法則,損失對W2和b2的梯度如下代碼:

# 利用反向傳播求損失對輸出層權重和偏置的梯度dW2 = np.dot(hidden_layer.T, dscores)db2 = np.sum(dscores, axis=0, keepdims=True)

同理,我們要想求損失對W1和b1的梯度,可以先求損失對hidden_layer的梯度,再求hiddenlayer對W1和b1的梯度,再根據鏈式求導法則即可求得損失對W1和b1的梯度。

根據鏈式求導法則,損失對hidden_layer的梯度如下代碼,其中dhidden為損失對hidden_layer的梯度矩陣。

dhidden = np.dot(dscores, W2.T)

而由於隱藏層的激活函數是ReLu函數,即 max(0, x) 。所以隱藏層神經元輸出大於0的,對ReLu函數輸入值的導數為1;隱藏層神經元輸出等於0的,對ReLu函數輸入值的導數為0。因此損失對ReLu函數輸入值即隱藏層輸入值的梯度為:

# 沿ReLu函數進行反向傳播dhidden[hidden_layer <= 0] = 0

現在dhidden中存儲的是損失對隱藏層輸入的梯度矩陣。有了損失對隱藏層輸入的梯度,且有了隱藏層輸入對W1和b1的梯度,根據鏈式法則可以求得損失對W1以及b1的梯度:

# 最終求得損失對與隱藏層神經元相連的權重和偏置的梯度dW1 = np.dot(X.T, dhidden)db1 = np.sum(dhidden, axis=0, keepdims=True)

現在分別有了損失對W1,W2,b1,b2的梯度,即可對參數進行更新了。參數更新的公式如下(以W1中第一行第一列的元素 w_{11} 為例):

w_{11} = w_{11} - eta cdot frac{partial{L}}{partial{w_{11}}}

這就是所謂的通過梯度下降求極小值點。如圖6所示,當 frac{partial{L}}{partial{w_{11}}}>0 時,將 w_{11} 減小一點,就會使 L 更接近極小值。反之,當 frac{partial{L}}{partial{w_{11}}}<0 時,將 w_{11} 增大一點,就會使 L 更接近極小值。

圖6:梯度下降尋找極小值點

對整個W1、W2、b1以及b2梯度矩陣利用上述公式執行參數更新操作:

# 參數更新W1 += -step_size * dW1b1 += -step_size * db1W2 += -step_size * dW2b2 += -step_size * db2

這樣我們就得到了一組新的參數,並且應用這組新參數的神經網路的輸出將會更加符合真實情況。我們將全部上述代碼放在一個循環中,循環執行10000次。這樣,我們的參數就更新了10000次,使得神經網路的輸出向真實情況靠近了一萬步。

測試準確度

# 計算在數據集上的類別預測精度hidden_layer = np.maximum(0, np.dot(X, W) + b)scores = np.dot(hidden_layer, W2) + b2predicted_class = np.argmax(scores, axis=1)print(training accuracy: %.2f % (np.mean(predicted_class == y)))

當我們將300個樣本點的特徵矩陣輸入到神經網路中,神經網路對300個樣本點的所屬類別進行預測。將預測結果與真實情況比對,最終輸出正確率為98%。也就是說300個樣本點中,神經網路正確預測了其中98%個樣本點的類別。

推薦閱讀:

強化學習(十三):logistic regression
機器學習如何在大數據土壤上播種 | 課堂筆記
吳恩達機器學習第六周課後感
直觀理解正則化

TAG:神經網路 | 機器學習 | 深度學習DeepLearning |