官术网_书友最值得收藏!

2.4 單層神經網絡的Python實現

機器學習算法隨著硬件算力的提升而演化。早期的機器學習以神經元為切入點,產生了簡單的參數調整和擬合算法。然而要模擬較為復雜的計算,顯然不是單個神經元能做到的。我們也看到,神經元的提出是為了模擬人類大腦的神經突觸工作模式,那么在理解單個神經元的工作原理后,下一步自然就是模擬多個神經元了,即神經網絡的算法。

2.4.1 從神經元到神經網絡

前面實現的都是基于圖2-1的感知器結構,利用不同的算法來實現對權重的訓練,重點是如何用梯度下降來修正權重,達到不斷逼近最優解的目的。

但并不能說這是一個真正的神經網絡。一般來說,神經網絡包含輸入層、隱藏層和輸出層,而前面講解的內容,只是隱藏層中一個神經元的權重調整方法而已。

如圖2-4所示,其中的左圖是圖2-1所示的感知器神經元,右圖是一個完整的單層神經網絡。神經元實際上只是神經網絡中的一小部分而已(用粗線標識)。神經網絡的輸出不再由單個神經元決定,而由多個神經元的輸出共同決定。

圖2-4 從神經元到神經網絡

在圖2-4中只設計了一組神經元,我們把這組神經元稱為神經網絡的隱藏層(Hidden Layer)。早期的神經網絡基本上只包括一個隱藏層,少數包括多個隱藏層,如多層感知器(Multilayer Perceptron,MLP),如圖2-5所示。

圖2-5 多層感知器,包括多個隱藏層

如果我們在圖2-5的基礎上再增加層數,則這樣形成的網絡往往被稱為深度網絡(Deep Network),這也是深度學習的由來。我們通常把隱藏層的數量稱為網絡的深度(Depth),把每一層的神經元個數稱為寬度(Width)。于是我們在研究中有一個有趣的問題:對于同樣數量的神經元,是使用更大的寬度、減少深度,還是增加深度、減少寬度?如何達到最佳平衡?這是機器學習中仍然引人深思的問題,目前并沒有標準答案。在實際應用中,我們更多地結合網絡的寬度和深度,通過實驗達到最佳效果,如在Wide & Deep Learning for Recommendation Systems[3]一文中所提及的方式。

2.4.2 單層神經網絡:初始化

和單個神經元的訓練相比,神經網絡確實要復雜許多,但實際上也只是計算次數和參數變得更多,其原理和訓練方式實質上是一樣的。我們下面親手實現一個類似圖2-4的單層神經網絡,看看在引入隱藏層之后,到底是如何進行訓練和預測的。

首先,定義問題。這次不再對正負數進行分類,而是對平面坐標點進行分類,如圖2-6所示。

圖2-6 平面坐標點分類

在圖2-6中,斜線y=x將平面坐標點分為兩類,線上方為0,線下方為1。我們希望設計一個神經網絡來對任意坐標點(x,y)進行自動分類。仿照圖2-4,這里的輸入層包括xy兩個輸入,輸出則包括0和1這兩個類別,因此網絡結構如圖2-7所示。

圖2-7 待實現的單層神經網絡,其中隱藏層的神經元個數可設置

可以看到,圖2-7中神經網絡的輸出是由隱藏層的多個神經元的輸出確定的。同時,因為我們的輸出不再只依靠一個神經元(雖然可以設置隱藏層只用一個神經元),因此除了隱藏層中每個神經元對應的輸入權重都需要計算(Whidden),輸出層中的每個輸出對于每個隱藏層的神經元的輸入也有對應的權重需要計算(Woutput)。

下面看看具體如何實現以上網絡。

因為該分類的數據特點比較明顯,所以我們可以先手工創建兩組數據分別用于訓練與測試:

然后定義輸入和輸出的個數。因為輸入包括xy兩個坐標,輸出包括1和0兩個類別,因此各自都為2。

接著我們就要開始定義神經網絡了,可以通過創建一個initialize_network函數來實現:

在上面的代碼中,我們將網絡模型定義為一個包含兩個數組的list,兩個數組分別對應圖2-7中的兩組權重:WhiddenWoutput

對于隱藏層的權重,參考圖2-7,我們可以看到隱藏層的每個神經元(總數由n_hidden定義)都接收了所有輸入,其數量為n_inputs+1個(其中比輸入多出來的一項為bias,也可將其理解為大多數線性回歸所定義的中的。這里一個全連接層(每個輸出都和所有輸入直接相關)的權重參數總數為(n_inputs+1)·n_hidden。

同樣,對output的權重采用同樣的處理,注意,output層的權重參數總數是(n_hidden+1)·n_output。

2.4.3 單層神經網絡:核心概念

我們接下來開始訓練網絡。怎么訓練呢?其實和前面的流程非常相似。在前面的兩段代碼示例simple_perceptron和linear_regression中,因為代碼過于簡單,所以在訓練過程中沒有明確分離出一些概念的實現。例如在simple_perceptron代碼示例中并沒有定義損失函數和更新權重的單獨方法,而是單獨實現了net_input和predict(實際上相當于激活函數)。而在linear_regression的代碼中強調了損失函數和更新權重update_weight的概念,用于解釋梯度下降優化,卻沒有提及網絡輸入和激活函數。

所以實際上,對于任何模型訓練,其關鍵都是實現如下4個核心函數。

◎ net_input:計算神經元的網絡輸入。

◎ activation:激活函數,將神經元的網絡輸入映射到下一層的輸入空間。

◎ cost_function:計算誤差損失。

◎ update_weights:更新權重。

這里還需要引入兩個概念:前向傳播(Forward Propagation)和反向傳播(Back Propagation)。

◎ 前向傳播:指將數據輸入神經網絡中,每個隱藏層的神經元都接收網絡輸入,通過激活函數進行處理,然后進入下一層或者輸出的過程。在前面linear_regression的例子中,predict方法實際上就是一個簡單的前向傳播方法。

◎ 反向傳播:指對網絡中的所有權重都計算損失函數的梯度,這個梯度會在優化算法中用來更新權值以最小化損失函數。實際上,它指代所有基于梯度下降利用鏈式法則(Chain Rule)來訓練神經網絡的算法,以幫助實現可遞歸循環的形式來有效地計算每一層的權重更新,直到獲得期望的效果。在前面的linear_regression例子中,我們把反向傳播計算梯度的內容和更新權重放在了一起。

了解了以上概念,可以知道實際上我們在前面已經實現過相關內容,只是沒有清晰地對每個環節都進行模塊化。而這里因為不再是單個神經元,所以計算環節更加復雜,對每個關鍵環節都進行模塊化是非常必要的。我們來看看每一步是怎么實現的。

2.4.4 單層神經網絡:前向傳播

首先是每個神經元的網絡輸入:

注意,weights實際上包含了一個類似bias的額外參數,即weights的個數比輸入(inputs)要多一個。因此我們首先使用weights[-1]對total_input賦值,然后添加每個輸入和對應權重的乘積,其形式為total_input = weights[:-2]·inputs + weights[-1]。

然后是激活函數activation:

這里使用了sigmoid激活函數,用于將網絡輸入映射到(-1,1)區間。激活函數有多種形式和算法,在第3章會做詳細解釋,這里不再贅述,只需把它當作一個區間映射的函數即可。

定義完上述兩個函數后,我們便可以定義前向傳播的實現:

可以看到,前向傳播其實就是對于每一層,都把上一層的輸出作為下一層的輸入,進行循環計算。因為這是全連接網絡,所以每個神經元都接收所有inputs,進行相同的net_input處理,將獲得的total_input結果再輸入激活函數activation中,獲得該神經元的最終結果,然后把結果添加到該層的輸出中。我們把當前層的輸出(outputs)作為下一層的輸入(inputs),持續迭代下去,直到最后把輸出返回。

2.4.5 單層神經網絡:反向傳播

可以看到,每個神經元所進行的計算過程都是一樣的,唯一影響結果的就是其中的權重參數(neuron[′weights′])。下面就要進行反向傳播和權重更新的實現,幫助每個神經元都調整自己的相關參數。

這里和前面最大的差別在于,激活函數不再是直接的線性方程,而是使用了sigmoid激活函數,那么我們在計算梯度變化時的求導就需要有所變化。簡單看一下sigmoid激活函數的形式和求導結果:

結合forward_propagation函數的實現,我們可以看到,這里中的輸入z其實就是前一層的輸出(每一層的輸入都是上一層的輸出)。

在本章第1個簡單神經元simple_perceptron及后面的linear_regression實現中,我們看到對權重的調整是這樣的:

當感知器只有一個神經元時,對權重的調整很簡單:

對于沒有隱藏層的單個神經元感知器來說,是有明確的結果(y)和預測結果()的,誤差結果由cost_function(MSE)確定。根據前面Linear Regression中的推導,在將MSE對w求導后,可得到,因此可以進一步概括為

那么問題就變為如何計算

回到要在圖2-7中實現的單層神經網絡,對于其中的輸出層,其處理方式和單神經元感知器類似,因為輸出的也是最終預測值y’,用MSE作為損失函數計算即可。注意,輸出層的輸入并不是原始輸入值xy,而是上一層(隱藏層)的輸出。而對于隱藏層來說,我們并不能直接計算它的輸出誤差,因為并不存在輸出層的真實值y,只能通過鏈式法則來間接推導。換句話說,假設隱藏層有一個權重w,我們希望對其進行修正,那么只能從最后輸出的損失函數計算出的誤差倒推(反向傳播),如圖2-8所示。

圖2-8 鏈式法則示意圖

根據圖2-8的示意,如果要計算最終output_total的誤差和w1的關系,則可以得到這樣一個公式:

其中:

這樣最終可以得到:

隱藏層可以繼續迭代下去,例如對圖2-8中的,可用類似的做法。我們記

實際上,我們沒有必要單獨計算每個,只需沿用前面的運算結果,再和當前神經元的屬性相乘即可。那么我們在循環迭代時,只需保存就可以大幅度提高運算效率,不必從頭計算。這就是反向傳播的核心要點。

這樣就理順了反向傳播中更新權重的全過程,下面看看它是怎么具體實現的。

同樣,首先定義cost_function函數:

需要指出一點:在反向傳播計算中,cost_function函數并不是必需的,根據cost_function函數進行求導才是必需的。但清晰地定義cost_function函數有助于我們理解整個實現。

然后是sigmoid激活函數的導數實現:

二者完成后,便可以完成最重要的反向傳播實現。先來看看下面的實現:

讓我們看看以上代碼都做了什么。

第1行:引入兩個參數,一個是需要更新的網絡模型network(記住,network實際上是一個list,每個item都是一組參數,其中包含了權重和其他屬性);另一個是期望值expected,包含結果的真實分類。

第2~4行:從網絡的最后一層(輸出層)開始計算,獲得當前層(layer)并設置變量errors為一個list,errors將存儲當前層中每個神經元的預測值的誤差。注意,這個誤差是從輸出層開始不斷迭代累積形成的,而不是和真實目標值y的絕對誤差。

第6行:做了一個判斷,對輸出層和隱藏層做了不同的處理。

第7~10行:對最后一層(輸出層)進行處理。對輸出層中的每個神經元(neuron),根據前面定義的公式,將第1部分存入errors中。

第18~20行:在這3行里,如果當前是輸出層,則在當前層的神經元中存儲了,其中,是sigmoid激活函數對輸入值的導數。然后進入倒數第2層(隱藏層)。這樣,邏輯過程就很簡明了,實際上在每個神經元的delta屬性中都會存儲前面所計算的

再回到第12~16行,根據前面推導的公式,我們在這里計算的是前一層的,注意,這里需要將前一層和當前神經元相關的所有輸出誤差全部累加(因為這是全連接網絡,所以意味著當前層的每個神經元的輸出都會作用于下一層的每個神經元的輸入)。

于是,當再次進入第18~20行時,隱藏層乘上了對應sigmoid的導數,這時在neuron[′delta′]中最后存儲的是要獲得最終的,則只需最后再乘以當前神經元的inputs即可(即前一層的output屬性),我們將在更新權重時實現這最后一步,請看下面的代碼:

在update_weights函數中,我們看到,首先仍然是在第2行遍歷網絡的所有層。和前面back_propagate函數不一樣的是,這里不是倒序遍歷,而是順序遍歷(因為初始輸入值在第1層)。

另外,update_weight函數需要在back_propagate函數之后調用。因為在back_propagate函數中,我們在每一層所有神經元的delta屬性里都存儲了,需要乘以該神經元的net_inputs(即前一層的output屬性所存數值,上一部分已經做了詳盡推導,這里不再贅述)。

因此在第3~5行中,初始化inputs為輸入數據row(實際上是一組訓練數據),如果不是輸入層(即第1層輸入),則將輸入換為前一組的輸出。

從第6行開始,對該層的所有神經元都進行遍歷,對該神經元的每一個輸入inputs[j]所對應的權重neuron[′weights′][j]都減去,其中,為在back_propagate函數中計算的neuron[′delta′]·inputs[j],于是就有了第8行的計算:

最后,我們對額外的權重參數bias進行處理,因為其輸入被設定為1,所以在第10行設定最后一個權重為learning_rate·neuron['delta']。

2.4.6 網絡訓練及調整

現在,我們已經定義了所有核心的反向傳播權重調整中所需要的函數,可以來實現具體的訓練代碼了:

在定義好前面的函數后,真正的模型訓練只有短短12行,而且淺顯易懂,如下所述。

第1行:在訓練函數的定義中,我們需要指定網絡模型對象、訓練數據、學習率、訓練次數和輸出類型的數量。

第2行:根據給定的訓練次數n_epoch進行循環訓練。

第3行:sum_error是當前訓練周期(每個周期都使用全部訓練集來訓練)的誤差,這實際上對訓練本身沒有影響,只是檢查一下損失(Loss)。

第4行:遍歷所有訓練數據,取其中一組開始訓練。

第5行:進行前饋計算(前向傳播),獲得最終輸出。注意,這個輸出包括在n_outputs中定義的類別個數,對每個類別都生成一個概率。在本例中輸出的是一個長度為2的一維數組,代表0、1兩個分類。這還不算是最終的預測結果,需要在這兩個分類中選擇概率最大的一個作為預測結果。

第6~7行:做了一個小的技巧性實現,我們需要對每組輸入數據都創建對應的期望輸出(expected)。第6行首先對期望輸出置0;第7行根據輸入數據的最后一個數值(也就是ground truth標簽,表示具體是哪個類別)將期望輸出中的對應位置置1。

第8行:通過cost_function計算誤差。

第9~10行:首先調用back_propagate設置每個神經元的delta屬性,再通過update_weights調整權重。

第11~12行:按照習慣,我們在每個訓練周期結束時都需要顯示一些必要的參數,供查看進度。

那么模型到底是怎么預測的呢?其實和前面的forward_propagate類似,只是最后要把概率最大的分類選出來,其實現如下:

最后,可以運行其代碼:

在上面的代碼中首先調用了initialize_network對網絡模型進行初始化,這里對隱藏層只設了一個神經元;然后調用train_network訓練網絡模型;在訓練完成后,我們用測試數據test_data中的每一組進行驗證,調用predict函數并把預測結果和測試數據的標簽列進行對比。

因為network權重的初始值是隨機的,所以我們運行3次代碼看看結果:

可以看到,前兩次均有一組數據預測錯誤,最后一組數據預測全部正確。怎么提高正確率呢?我們首先可以嘗試增加神經網絡的寬度,在原有的僅有一個神經元的基礎上再加一個,變為兩個神經元,也就是在調用initialize_network時,將n_hidden參數設為2:

這時再連續運行三次,可以看到三次的測試結果都完全正確(結果完全一致,這里省略運行結果展示)。

另一種思路是增加深度,在原有的單層隱藏層上再加一層,這涉及initialize_network的改動,我們來試試:

在上面新改動的initialize_network中,我們把之前的單個hidden_layer改為了hidden_layer1和hidden_layer2,這兩個新的隱藏層的神經元個數一致,都由n_hidden指定。

我們保持n_hidden=1,運行后發現,增加層數后的預測效果反而不如以前:

為什么增加深度后反而效果不好呢?增加深度后,訓練的參數個數和梯度迭代的次數也增加了,是不是需要更多的訓練和調整呢?我們嘗試把n_epoch的次數從20提高到200后看看:

可以看到,訓練次數從20提高到200后效果略好,那么我們再提高到2000呢?

訓練次數提高到2000后,我們發現測試正確率為100%(不再重復展示結果)。

因此我們看到,對于本章例子中的簡單數字分類,增加網絡深度雖然也能提高預測準確率,但同時對計算能力的要求大幅度增加。相對而言,保持一個隱藏層,簡單增加神經元的做法見效更快,而且避免過多增加計算能力需求。

在很長一段時間里,機器學習都停留在強調寬度、增加神經元階段。關于增加深度,沒有太多考慮,向深度神經網絡(Deep Neural Network,DNN)方向發展即可。這是因為算法本身沒有找到合適的突破場景,沒有找到深度神經網絡能夠真正發揮作用的地方;另外,硬件和軟件都沒有提供足夠的計算能力來滿足深度神經網絡在實踐中的需要。

但隨著卷積神經網絡(Convolutional Neural Network,CNN)在圖像分類上的突破,基于深度學習(Deep Learning)的深度神經網絡已經成為當前的主流方案,因此相應誕生了各種開發框架,充分發揮硬件和軟件的作用可幫助深度神經網絡的構建、訓練和應用。第3章將介紹Keras開發框架,方便大家系統了解深度神經網絡開發框架的基本使用方法,為后續進行推薦系統、自然語言處理、圖像識別等方面的學習和應用做好準備。

主站蜘蛛池模板: 永兴县| 明水县| 华安县| 华蓥市| 自治县| 偃师市| 平度市| 建阳市| 丁青县| 沙洋县| 西乌珠穆沁旗| 洪雅县| 上杭县| 石河子市| 德钦县| 开封市| 西乡县| 柘荣县| 桂林市| 乐东| 清苑县| 华阴市| 安福县| 镇原县| 延寿县| 长沙县| 和平县| 墨脱县| 关岭| 宁德市| 凤阳县| 孝感市| 徐闻县| 萨迦县| 德令哈市| 武隆县| 郯城县| 旌德县| 济宁市| 莒南县| 通化县|