1. 程式人生 > >一篇讀完 Python神經網路程式設計 make your own neuralnetwork

一篇讀完 Python神經網路程式設計 make your own neuralnetwork

Python 神經網路程式設計 

 make your own neural network

非常適合入門神經網路程式設計的一本書,主要是三部分: 介紹神經網路的基本原理和知識;用Python寫一個神經網路訓練識別手寫數字;對識別手寫數字的程式的一些優化。

神經網路如何工作

神經網路的大的概括就是:給定輸入,經過一些處理,得到輸出。當不知道具體的運算處理方式時,嘗試使用模型來估計其運作方式,在這個過程中可以基於模型輸出和已知真實例項之間的比較來得到誤差、調整引數

常見的神經網路模型包括分類器和預測器。通俗而言,分類器是將已有資料分開;預測是根據給定輸入,給出預測的輸出。本質上沒有太大差別。在分類過程中其實就是要找到線分開各組資料,關鍵就是確定這條線,也就是確定斜率。在書中給的直線分類器的例子中,就是隨機一個斜率,然後用一組組正確的資料來逐步修正這個斜率:

delt A = E/x,其中A是斜率。但是這樣直線的斜率只會取決於最後一個樣本而不會顧及到前面的樣本。為了改進,每次只要變化delt A 的一部分,這也就是學習率的由來。調節學習率,單一的訓練樣本就不能主導整個學習過程,也可以減少噪聲的影響。複雜的分類器也是這樣的思想。

神經網路,正如名字所示,靈感來自動物的神經元細胞,畢竟大腦真的很神奇。神經元的結構包括樹突和軸突,電訊號從一端沿著軸突再傳到樹突,這樣就從一個神經元傳到另一個神經元。簡單說,生物的神經元也是接受電訊號輸入,再輸出另一個電訊號。不過生物神經元與簡單的線性函式不一樣,不能簡單的對輸入做出反應。可以這樣認為,在產生輸出之前,輸入必須達到一個閾值

。直觀上,神經元不希望傳遞微小的噪聲訊號,而只傳遞有意識的明顯的訊號。 我們用啟用函式來表達這種方式。

下圖所示為S函式,也就是sigmoid函式,比較平滑,更自然更接近現實。

S函式簡單,並且實際上非常常見。S函式也稱為邏輯函式:

繼續考慮對神經元進行建模。生物的神經元是可以接受許多輸入的,而不僅僅是一個輸入。對這些輸入,我們只需要 相加,得到總和,作為S函式的輸入,然後輸出結果。這就是神經元的工作機制。 如果組合訊號不夠強大,那麼S函式的效果是抑制輸出訊號;足夠大時則激發神經元。生物神經元中每個神經元都接受來自之前的多個神經元的輸入,也可能同時提供訊號給多個神經元。

那麼模仿時,可以構建多層神經元,每層神經元都與其前後的神經元互相連線。如圖:

可以不斷調整節點之間的連線強度。之所以沒有弄成其他奇怪的構造而採用完全的連線,是因為這樣的連線方式可以相對容易的編碼成計算機指令;神經網路的學習過程將會弱化那些實際上不需要的連線。

接下來書中用2*2 3*3的簡單例子,給了一些資料,來實際的體驗神經網路的計算過程。

隨機一些權重,輸入*權重之後求和,得到下一層每個節點的輸入;經過啟用函式,得到該層的輸出;然後繼續向下一層傳播,直到輸出。

這個過程可以用矩陣簡潔方便的計算。比如2*2的神經網路的計算過程可以表示為:

三層的也是如此。

從第一層(輸入層)到第二層(隱藏層):

X是結果,W是權重矩陣,I是第一層的輸入;

然後經過啟用函式sigmoid得到輸出:

然後繼續向下一層傳播:

在經過一層sigmoid得到最後的輸出

Winput_hidden的維度是 hidden_layer * input_layer;

Whidden_output的維度是output_layer*hidden_layer

記住是剛好相反的,行數是下一層的節點數目,列數是本層的節點數

以上是整個神經網路從接受輸入,然後向前傳播的過程。

那麼如何進行學習呢?仍然是使用誤差值,也就是前向傳過來得到的答案與所知正確答案之間的差值。

那麼如何根據誤差來更顯連結的權重呢?是將誤差按連結的權重分配,反向傳播回來,進行修正。比如誤差是4,有兩條連結(0.1 0.3)指向這個節點,那麼分別傳回去的誤差就分別是1和3.

也就是說在向前傳播資料和向後傳播誤差的過程中,都要用到權重。將誤差從輸出向後傳播到網路中的方法就叫做反向傳播

推廣到多個節點的誤差反向傳播時,每個節點的誤差都按比例向後傳播,然後在上一層中重新組合(指向一個節點的所有連結傳回來的誤差的和就是傳到這個節點的誤差)。依據同樣的方式就可以繼續向上一層傳播。用圖來闡述更加清楚:

總的來說,神經網路是根據調整連結權重進行學習,這種方法由誤差主導。在輸出節點中的誤差等於所需值與實際值間的差;而內部節點的誤差是按照連結權重來分配誤差,然後在內部節點中重組這些誤差。

同樣用矩陣來進行計算。嚴格按照上面的思路將是這樣的:

但是這樣並不好處理。觀察上圖,最重要的其實是wij*e,分子越大,分到的權重就應該越大,而這些分數的分母是歸一化的分母,忽略分母之後,比例仍然在。

於是:

就可以寫成:

注意這裡,是原來的連結矩陣的轉置!!

下面就是一個重要問題,到底如何更新權重。為什麼要反向傳播誤差呢?因為我們要用誤差來更新權重。神經網路要處理的問題往往過於複雜,無法像簡單的線性分類器一樣用數學的方式求得引數,暴力的解決不實際,所以最後提出的是梯度下降(gradient descent)的方法。

書中對梯度下降的比喻是摸黑下山,打著手電筒只能看到最近的一小片地方,你就選擇看起來是下坡的方向走。逐步逐步走,不需要知道整個山的地貌,也可以下山。

進一步將其推廣到一個複雜的函式,梯度下降的思想在於,我們不需要知道整個函式的面貌,也可以一點點朝著最低點前進。

如果將複雜的函式換成神經網路的誤差函式,下山找到最小值就是要最小化誤差,這樣才可以改進神經網路。這就要用到上面的梯度下降演算法了。

可以用二維的函式幫助理解:

隨機一個起點,斜率為負的話向右走(x增大),斜率為正的話向左走(x減小)——往相反的方向增加x的值,這樣可以慢慢的向最小值靠近,直到達到附近的位置。

在接近真正的最小值時,斜率會變小,需要調節步長比較小,才不會在最小值附近來回震盪。

當函式有很多引數的時候,這種方法才真正顯出優勢。

不過這種方法也有缺點,可能會終止在某個區域性的最小值。為了避免這種情況,可以進行多次隨機的初始化操作。

總之,梯度下降演算法是求解函式最小值的一種很好的辦法,當函式非常複雜困難,並且不能輕易使用數學代數求解時,這種方法很實用;當函式有很多引數時,這種方法依然適用,還可以容忍不完善資料,偶爾走錯一步也不影響大局。

神經網路本身的輸出函式不是一個誤差函式,但由於誤差是目標訓練值與實際輸出值之間的差值,因此可以很容易的把輸出函式變成誤差函式。

直接相減可能會出現正負誤差相抵消的問題;而絕對值函式的斜率在最小值附近不連續,會讓梯度下降法在v型山谷附近跳來跳去。因此最終選的是差的平方:斜率好計算,誤差函式平滑連續,且越接近最小值,梯度越小。

確定了誤差函式後,需要計算出誤差函式相對於權重的斜率。也就是神經網路的誤差E對網路連結權重wij導數。

右邊只是將E表達了出來,t是真實值,o是神經網路的輸出,所有節點的誤差的平方和。

但是注意到,在節點n的輸出on只取決於連線到這個節點的連結,也就是節點k的輸出ok只取決於權重wjk,即連線到k的連結。換句話說,節點k的輸出不依賴連結wjb(wjb表示由前一層第j個節點連結到該層第b個節點的權重)。也就是說,對wjk而言,我們只需要考慮節點Ok。

所以上式可以改成:

結合鏈式法則:

而前面那個就是個二次函式對ok的求導。

將Ok的表示式列出來,就是sigmoid函式之後得到的輸出

其中oj是上一層的輸出,結合了這之間的連結矩陣後,在本層進行組合(即求和),然後過一個啟用函式。對sigmoid函式進行求導,可以得到:

所以繼續之前的求導過程:

可以去掉前面的2,因為我們只對誤差函式的方向感興趣(想想那個二維的函式)。

所以我們一直努力要得到的答案就是:

這是從輸出層到隱藏層的的誤差的斜率。

套進去我們可以得到從隱藏層到輸入層之間的斜率:

oi其實就是輸入的值。

根據相反方向來修改連結矩陣:

同樣的,我們要轉化成矩陣的運算:

即:

wjk是連線當前層節點j與下一層節點k的連結矩陣。ɑ是學習率,Ok是下一層節點的值,最後一項OjT 是上一層節點的輸出的轉置。

這是正確的點乘順序。

以上是神經網路的原理。接下來有關準備資料要知道的事情。

觀察sigmoid函式:

其輸出值在0-1之間,而x過大時,啟用函式會變得很平緩,也就是斜率很小,這樣不利於學習新的權重。

觀察上面計算更新權重值的表示式,權重的更新取決於啟用函式的梯度,梯度太小限制神經網路的學習能力,也就是所謂的飽和神經網路。

因此我們不應該讓輸入值太大;而計算機處理太小的訊號時,可能會喪失精度。因此也不能過小。

好的建議是調整輸入值在0.0-1.0之間,而輸入0會使輸出也是0(oj),權重更新值也會變為0(上面那個複雜的式子),所以也要避開0;因此最好加上一個小的偏移。

因為我們的啟用函式不能產生大於1的值,因此將訓練目標值設的比較大是不合理的。而且邏輯函式的輸出也無法達到1,只能是接近1;如果目標設定的過大,訓練網路將會驅使更大的權重,以獲得越來越大的輸出,這將使得網路飽和。因此,我們應當調整輸出值符合啟用函式的輸出範圍,一般在0.0-1.0,由於0.0和 1.0也不能,也可以使用0.01-0.99.

除了輸入輸出,同樣的道理也適用於初始權重的設定。同樣要避免大的權重使得網路飽和,可以用-1.0~1.0之間隨機均勻的選擇。另外,如果很多訊號進入一個節點,並且這些訊號已經表現的不錯了,那麼對這些訊號進行組合並應用啟用函式時,應保持這些表現良好的訊號。可以在一個節點傳入連結數量平方根倒數的大致範圍內隨機取樣,初始化權重。

直觀上理解,如果一個節點傳入的連結越多,就有越多的訊號被疊加在一起,因此如果連結越多,而減小連結的範圍是有意義的。實際上就是從均值為0、保準方差等於節點傳入連結數量的平方根倒數的正態分佈中進行取樣。

另外,不管你怎麼做,禁止將權重設定為相同的恆定值,特別是不要設定為0.這樣,網路中每個節點都將收到相同的訊號值,輸出也是一樣的,反向傳播誤差時誤差也是平均分的,這將導致同等量的權重更新,又會出現相同的權重。而0 的話,輸入訊號歸0,取決於輸入訊號的權重更新函式也因此為0.完全喪失更新權重的能力。

Python寫一個神經網路訓練識別手寫數字

具體參考github上的程式碼:

主要是構建了一個神經網路類:neuralNetwok

這個類有初始化函式__init__,設定輸入層節點、隱藏層節點和輸出層節點數,設定學習率,設定啟用函式,用隨機值初始化輸入層-隱藏層和隱藏層-輸出層間的連結矩陣。

查詢部分(query),是輸入輸入值,用訓練好的模型(權重引數),計算得到輸出值,計算過程就是前面神經網路前向傳播的過程,要用到pyhon庫中numpy.dot運算,以及啟用函式。

訓練部分(train)這是整個神經網路類的核心部分。輸入是輸入值和目標值。計算前項傳播的部分和查詢部分是相同的。得到輸出後,與真實的目標值相減得到誤差E,然後用第一部分推出的那個很長的式子來計算delt wij並更新權重。

整個神經網路類的結構就是如此,想要使用這個類,就在main函式中,例項化這個類,然後準備訓練資料和測試資料,用一定的迭代次數來訓練模型,之後用訓練過的這些引數,呼叫query輸入測試資料就可以進行測試了。

這裡對原始的輸入資料的處理是,將,分隔的畫素值分開,處理成合適的輸入範圍,目標值則是一個vector,正確的數字大,其他的數字接近0.

測試資料時對輸出用argmax選出得分最高的那個,就是神經網路判斷的數字。

優化和嘗試

可以通過改變學習率、神經網路各層節點數、修改epoch次數、多次隨機初始化等方式來找到更合適的引數。

這部分還指出了自己手寫數字,然後轉化成同樣的28*28的畫素,用自己訓練得到的模型進行識別。

由於資料集有限,可以通過適當旋轉已有的圖片來得到新的資料。