動手實踐神經網路1: 單個神經元

本系列寫作動機

在我自己在學習和應用神經網路的過程中,面臨這樣一種狀況:一方面,理論教材(例如周志華教授的機器學習 (豆瓣)以及網路上浩如煙海的資料)從單個神經元開始詳細講解了上述演算法的數學原理;另一方面,在目前流行的深度學習框架,例如TensorFlow中,這些功能已經被良好封裝,只要調用正確的API就可以實現。這樣,書籍資料里的各類演算法原理在框架調用中並不能直觀的感覺到,雖然已經能用神經網路解決一些問題,但是對於演算法基礎原理的理解仍然不夠深入,在工作中遇到問題時Debug思路不明確。

源於此,我開始嘗試用代碼實現神經網路,希望通過這樣的方式加深對神經網路原理性的理解,在今後的工作中更清楚框架(例如TensorFlow)中API的特性與局限。

源代碼

本文中提及的源代碼都在我的Github中SimpleNeuralNetwork Project : NeuralNode.py開放可用

神經元簡介

典型的神經元包括

  • N個輸入節點:X1,X2, ..., XN
  • 每個輸入節點對應一個輸入權值: w1,w2,...,wN
  • 偏置值bias
  • 激活函數f(x)
  • 輸出值 Y,滿足 y = f(sum{w_ix_i}) + bias)

上述結構可以用下圖形象化說明:圖是自己畫的,丑請見諒

這樣一個神經元雖然簡單,但是結合前向計算和誤差反向傳播演算法,加上梯度下降調整,可以實現訓練參數以逼近給定的訓練集數據規律。用大白話說,就是通過訓練數據學習到規律。當然,由於只有一個神經元,其學習能力非常有限,我們只能用很簡單的問題考察它。

軟體設計

在設計中,存在以下假設

  • 神經元與前一級iDims個節點有連接,iDims >=0且iDims為整數
  • 神經元的輸入為1*iDims的向量,記為vec{x}
  • 神經元的激活函數為Sigmoid function

從軟體的角度,神經元可以抽象為一個對象,此對象包含以下成員

  1. 權重向量vec{weight}: shape =1*iDims,用於保存與前一級iDims個連接中每個連接的權重
  2. 偏置量bias:這是一個標量,用於記錄偏置值
  3. 輸入向量vec{x}:shape =1*iDims
  4. 激活函數的輸入值 z: z = vec{x} odot vec{weight} + bias=sum_{i=1}^{iDims}weight_i*x_i + bias之所以要保存這個成員,是為了在誤差反向傳播時避免重複計算

代碼實現

構造函數:對權重、bias等參數完成初始化。這裡需要注意:權重必須使用隨機數初始化,事實上我在注釋代碼中展示了將weight向量初始化為全1,後續訓練數據會展示為什麼這樣不可行。

def __init__(self, inputDim): #當前結點與前一級的連接數目 self.iDims = inputDim #權重向量,Shape = (iDims, ) self.weight = np.random.rand(self.iDims) #self.weight = np.ones(self.iDims) #偏置 self.bias = 1 #激活函數的輸入 self.z = 1 #當前層的殘差 self.delta = 1

前向計算和保存計算結果。其中前向計算可以解釋為給定輸入計算神經元的輸出值。

#forward: 輸入1*iDims向量,計算前向結果 def forward(self, ix): if (ix.shape <> (self.iDims,)): print ("Wrong input shape: x.shape = " + str(ix.shape)) return self.x = ix self.z = np.dot(self.x, self.weight) + self.bias #為了避免計算溢出,對z做最大值和最小值限制 if self.z >1000: self.z = 1000 elif self.z < -1000: self.z = -1000 return sigmoid(self.z)

反向梯度計算:這部分是誤差反相傳播的關鍵,具體演算法參考了知乎上的一份教材CS231n課程筆記翻譯:反向傳播筆記,雖然教材上講解演算法的篇幅很長,但是具體到一個神經元,情況可以簡化到非常簡單的狀況:

#backward: 輸入前一級計算出的梯度,輸出為兩個數組 #第一個數組: dx,iDims*1向量,即當前節點對於前一級每個輸入的梯度 #第二個數組:dw,iDims*1向量,當前節點對於每個權重的梯度 #第三個數組:dbias, 1*1向量,當前節點對於偏置量的梯度 def backward(self, gradient): dz = (1 - self.z) * self.z #Sigmoid函數的求導 self.delta = dz*gradient dw = self.x * self.delta # 回傳到w dbias = self.delta # 回傳到bias return [dw, dbias]

根據計算出的梯度和學習率調整參數完成訓練

#根據學習率和梯度調整weight和bias參數 def adjustWeightAndBias(self, learnRate, dw, dbias): self.weight = self.weight - learnRate*dw self.bias = self.bias - learnRate*dbias

訓練神經元

前文提到,單個神經元的學習能力是非常有限的,因此我們構造一個最簡單的問題來訓練模型。

考慮輸入向量維度iDims = 2,輸入向量vec{x} = (2,2),目標輸出值為target =frac{1}{1+e}

損失函數定義為loss = (predictValue - target)^2,即訓練的目標是使得loss取值儘可能小。

從上述數據和神經網路結構可以看出這個問題有無窮多組解滿足loss = 0, 當w1*x1 + w2*x2 + bias = -1時即可,例如w1=-1, w2 =1,bias=1就能夠滿足。

接下來我們來看一下單個神經元在梯度下降法的幫助下實際表現如何

訓練代碼如下

#測試神經元訓練,使用梯度下降法訓練參數def unitTest_naiveTrain(): print "In unitTest_naiveTrain" n1 = NeuralNode(2) n1.printParam(); prevWeight = n1.weight x = np.ones(2) x[0] = 2 x[1] = 2 target = 1/(1+np.exp(1)) counter = 0 for i in range(1000000): counter=i #print "Round",i fowardResult = n1.forward(x) #print "Forward Result:",fowardResult loss = (fowardResult-target)*(fowardResult-target) #print "Loss=",loss dLossdvalue = 2*(target-fowardResult) grad = n1.backward(dLossdvalue) #print "grad=",grad n1.adjustWeightAndBias(0.001, grad[0], grad[1]) if np.sum(np.abs(prevWeight - n1.weight)) < 1e-7: break prevWeight = n1.weight #n1.printParam() #print "" n1.printParam() return [counter, loss, n1.weight, n1.bias]

當第一次訓練時,參數和Loss函數取值如下

Weight = [ 0.74034163 0.8361381 ] Bias = 1

Round 0

Loss= 0.512060424436

訓練了1183次之後,兩次迭代之間的權重參數變化幾乎為0(即上述代碼中np.sum(np.abs(prevWeight - n1.weight)) < 1e-7),Loss函數取值如下

Round 1181

Loss= 0.213557212057

Round 1182

Loss= 0.213557170922

Weight = [ 0.03968992 0.13548639] Bias = 0.649674145712

從上述結果可以看到

  1. 目前的神經元實現可以調整權重,使得損失函數向loss降低的方向調整
  2. 在足夠多次迭代後,參數總是收斂到如下結果。在這個位置,梯度已經接近於0,無法進一步調整。但我們明確知道這個位置不是最優解。這有兩種可能,一種是只有一條訓練數據,訓練樣本數量不足;另一種,根據梯度下降調整參數的演算法只實現了最基本的功能,當搜索到局部極值點之後由於梯度消失無法跳出來找到更好的極值點,其他演算法(例如我在Tensorflow中常用的tf.train.AdamOptimizer)或許能解決這個問題

對於沒有訓練出最優解的問題,本文暫且擱置,我會在下一篇文章中予以說明。

為什麼Weight參數不能用相同的值初始化?

在前文介紹構造函數時,我特別提到了「權重必須使用隨機數初始化」,為什麼呢?

這裡我不通過數學推導解釋此問題,只通過實驗說明使用相同的值初始化會有什麼問題

假定我們這樣初始化weight向量

self.weight = np.ones(self.iDims)

採用與上一節同樣的訓練程序,得到結果如下

Weight = [ 1. 1.] Bias = 1

Round 0

Loss= 0.524705707475

Weight = [ 0.94205074 0.94205074] Bias = 0.971025370892

Round 1

Loss= 0.521845875091

Weight = [ 0.89084476 0.89084476] Bias = 0.945422377553

Round 2

Loss= 0.518640311338

Weight = [ 0.84527119 0.84527119] Bias = 0.922635594173

從上述Log可以看到,每一輪訓練中Weight參數的兩個維度總是相同的。根據斯坦福大學反向傳播講義,這種情況稱之為對稱。神經網路中的權值初始化必須是隨機的,否則神經網路的所有隱層節點都會學習到輸入層的同樣的函數。使用隨機初始化可以實現打破對稱(symmetry breaking). 原文如下

it is important to initialize the parameters randomly, rather than to all 0』s. If all the parameters start off at identical values, then all the hidden layer units will end up learning the same function of the input (more formally, will be the same for all values of i, so that for any input x). The random initialization serves the purpose of symmetry breaking.

後續工作

  1. 嘗試實現AdamOptimiser的演算法,觀察是否能夠幫助單個神經元收斂到最優解
  2. 實現由多層神經元組成的網路
  3. 軟體方面:還沒有實現batch訓練和調整。在TensorFlow中,一次訓練的是一個batch的數據,即輸入X={vec{x_i},i=1,2,...N},批量計算結果、梯度並調整權值。目前的代碼只能一次處理一個輸入向量。

更多資料

這個系列整理自我的技術博客:王堯的技術博客。博客中的內容體系性不如在知乎整理的清楚,但會隨時記錄工作中的技術問題和發現,如有興趣歡迎圍觀。


推薦閱讀:

TAG:深度學習DeepLearning | 機器學習 |