閱讀105 返回首頁    go 阿裏雲 go 技術社區[雲棲]


神經網絡基礎知識筆記

神經網絡表示

神經元模型

神經網絡從大腦的工作原理得到啟發,可用於解決通用的學習問題。神經網絡的基本組成單元是**神經元(neuron)**。每個神經元具有一個軸突和多個樹突。每個連接到本神經元的樹突都是一個輸入,當所有輸入樹突的興奮水平之和超過某一閾值,神經元就會被激活。激活的神經元會沿著其軸突發射信號,軸突分出數以萬計的樹突連接至其他神經元,並將本神經元的輸出並作為其他神經元的輸入。數學上,神經元可以用**感知機**的模型表示。

neuron.png

一個神經元的數學模型主要包括以下內容:

名稱 符號 說明
輸入 (input) $x$ 列向量
權值 (weight) $w$ 行向量,維度等於輸入個數
偏置 (bias) $b$ 標量值,是閾值的相反數
帶權輸入 (weighted input) $z$ $z=w · x + b$ ,激活函數的輸入值
激活函數 (activation function) $σ$ 接受帶權輸入,給出激活值。
激活值 (activation) $a$ 標量值,$a = σ(\vec{w}·\vec{x}+b)$
激活函數表達式

$$
a
= \sigma(
\left[ \begin{matrix}
w_{1} & ⋯ & w_{n} \
\end{matrix}\right] ·
\left[ \begin{array}{x} x_1 \ ⋮ \ ⋮ \ x_n \end{array}\right] +
b
)
$$

激活函數通常使用S型函數,又稱為sigmoid或者logsig,因為該函數具有良好的特性:**光滑可微**,形狀接近感知機所使用的硬極限傳輸函數,函數值與**導數值計算方便**。
$$
σ(z) = \frac 1 {1+e^{-z}}
$$

$$
σ'(z) = σ(z)(1-σ(z))
$$

也有一些其他的激活函數,例如:硬極限傳輸函數(hardlim),對稱硬極限函數(hardlims),線性函數(purelin) , 對稱飽和線性函數(satlins) ,對數-s形函數(logsig) ,正線性函數(poslin),雙曲正切S形函數(tansig),競爭函數(compet),有時候為了學習速度或者其他原因也會使用,表過不提。

單層神經網絡模型

可以並行操作的神經元組成的集合,稱為神經網絡的一層。

現在考慮一個具有$n$個輸入,$s$個神經元(輸出)的單層神經網絡,則原來單個神經元的數學模型可擴展如下:

名稱 符號 說明
輸入 $x$ 同層所有神經元共用輸入,故輸入保持不變,仍為$(n×1)$列向量
權值 $W$ 由$1 × n$行向量,變為$s × n$矩陣,每一行表示一個神經元的權值信息
偏置 $b$ 由$1 × 1$標量變為$s × 1$列向量
帶權輸入 $z$ 由$1 × 1$標量變為$s × 1$列向量
激活值 $a$ 由$1 × 1$標量變為$s × 1$列向量
激活函數向量表達式

$$
\left[ \begin{array}{a} a_1 \ ⋮ \ a_s \end{array}\right]
= \sigma(
\left[ \begin{matrix}
w_{1,1} & ⋯ & w_{1,n} \
⋮ & ⋱ & ⋮ \
w_{s,1} & ⋯ & w_{s,n} \
\end{matrix}\right] ·
\left[ \begin{array}{x} x_1 \ ⋮ \ ⋮ \ x_n \end{array}\right] +
\left[ \begin{array}{b} b_1 \ ⋮ \ b_s \end{array}\right]
)
$$

單層神經網絡能力有限,通常都會將多個單層神經網絡的輸出和輸入相連,組成多層神經網絡。

多層神經網絡模型

  • 多層神經網絡的層數從1開始計數,第一層為**輸入層**,第$L$層為**輸出層**,其它的層稱為**隱含層**。
  • 每一層神經網絡都有自己的參數$W,b,z,a,⋯$,為了區別,使用上標區分:$W^2,W^3,⋯$。
  • 整個多層網絡的輸入,即為輸入層的激活值$x=a^1$,整個網絡的輸出,即為輸出層的激活值:$y'=a^L$。
  • 因為輸入層沒有神經元,所以該層所有參數中隻有激活值$a^1$作為網絡輸入值而存在,沒有$W^1,b^1,z^1$等。

現在考慮一個$L​$層的神經網絡,其各層神經元個數依次為:$d_1,d_2,⋯,d_L​$。則該網絡的數學模型可擴展如下:

名稱 符號 說明
輸入 $x$ 輸入仍然保持不變,為$(d_1×1)$列向量
權值 $W$ 由$s × n$矩陣擴展為$L-1$個矩陣組成的列表:$W^2_{d_2 × d_1},⋯,W^L_{d_L × d_{L-1}}$
偏置 $b$ 由$s × 1$列向量擴展為$L-1$個列向量組成的列表:$b^2_{d_2},⋯,b^L_{d_L}$
帶權輸入 $z$ 由$s × 1$列向量擴展為$L-1$個列向量組成的列表:$z^2_{d_2},⋯,z^L_{d_L}$
激活值 $a$ 由$s × 1$列向量擴展為**$L$個列向量**組成的列表:$a^1_{d_1},a^2_{d_2},⋯,a^L_{d_L}$
激活函數矩陣表達式

$$
\left[ \begin{array}{a} a^l_1 \ ⋮ \ a^l_{d_l} \end{array}\right]
= \sigma(
\left[ \begin{matrix}
w^l_{1,1} & ⋯ & w^l_{1,d_{l-1}} \
⋮ & ⋱ & ⋮ \
w^l_{d_l,1} & ⋯ & w^l_{d_l,d_{l-1}} \
\end{matrix}\right] ·
\left[ \begin{array}{x} a^{l-1}_1 \ ⋮ \ ⋮ \ a^{l-1}_{d_{l-1}} \end{array}\right] +
\left[ \begin{array}{b} b^l_1 \ ⋮ \ b^l_{d_l} \end{array}\right])
$$

權值矩陣的涵義

多層神經網絡的權值由一係列權值矩陣表示

  • 第$l$層網絡的權值矩陣可記作$W^l$,表示前一層($l-1$)到本層($l$ )的連接權重
  • $W^l$的第$j$行可記作$W^l_{j*}$ ,表示從$l-1$層所有$d_{l-1}$個神經元出發,到達$l$ 層$j$號神經元的連接權重
  • $W^l$的第$k$列可記作$W^l_{*k}$ ,表示從$l-1$層第$k$號神經元出發,到達$l$ 層所有$d_l$個神經元的連接權重
  • $W^l$的$j$行$k$列可記作$W^l_{jk}$,表示從$l-1$層$k$號神經元出發,到達$l$ 層$j$神經元的連接權重
  • 如圖,$w^3_{24}$表示從2層4號神經元到3層2號神經元的連接權值:

    nn-weight.png

隻要記住,權值矩陣$W$的**行標表示本層神經元**的標號,**列標表示上層神經元**的標號即可。

神經網絡推斷

前饋(feed forward)是指神經網絡接受輸入,產生輸出的一次計算過程。又稱為一次**推斷(inference)**。

計算過程如下:
$$
\begin{align}
a^1 &= x \
a^2 &= σ(W^2a^1 + b^2) \
a^3 &= σ(W^3a^2 + b^3) \
⋯ \
a^L &= σ(W^La^{L-1} + b^L) \
y &= a^L \
\end{align}
$$

推斷實際上就是一係列矩陣乘法與向量運算,一個訓練好的神經網絡可以高效地使用各種語言實現。神經網絡的功能是通過推斷而體現的。推斷實現起來很簡單,但如何**訓練神經網絡**才是真正的難點。

神經網絡訓練

神經網絡的訓練,是調整網絡中的權值參數與偏置參數,從而提高網絡工作效果的過程。

通常使用**梯度下降(Gradient Descent)**的方法來調整神經網絡的參數,首先要定義一個**代價函數(cost function)**用以衡量神經網絡的誤差,然後通過梯度下降方法計算合適的參數修正量,從而**最小化**網絡誤差。

代價函數

代價函數是用於衡量神經網絡工作效果的函數,是定義在一個或多個樣本上的實值函數,通常應滿足以下條件:

  1. 誤差是非負的,神經網絡效果越好,誤差越小
  2. 代價可以寫成神經網絡輸出的函數
  3. 總體代價等於個體樣本代價的均值:$C=\frac{1}{n} \sum_x C_x$

最常用的一個簡單的代價函數是:**二次代價函數**,又稱為**均方誤差(MeanSquareError)**
$$
C(w,b) = \frac{1}{2n} \sum_x{{|y(x)-a|}^2}
$$
前麵的係數$\frac 1 2$是為了求導後簡潔的形式而添加的,$n$是使用樣本的數量,這裏$y$和$x$都是已知的樣本數據。

理論上任何可以反映網絡工作效果的指標都可以作為代價函數。但之所以使用MSE,而不是諸如“正確分類圖像個數”的指標,是因為隻有一個**光滑可導**的代價函數才可以使用**梯度下降**(Gradient Descent)調整參數。

樣本的使用

代價函數的計算需要一個或多個訓練樣本。當訓練樣本非常多時,如果每輪訓練都要重新計算網絡整個訓練集上所有樣本的誤差函數,開銷非常大,速度難以接受。若隻使用總體的一小部分,計算就能快很多。不過這樣做依賴一個假設:**隨機樣本的代價,近似等於總體的代價。**

按照使用樣本的方式,梯度下降又分為:

  • 批量梯度下降法(Batch GD):最原始的形式,更新每一參數都使用所有樣本。可以得到全局最優解,易於並行實現,但當樣本數量很多時,訓練速度極慢。

  • 隨機梯度下降法(Stochastic GD):解決BGD訓練慢的問題,每次隨機使用一個樣本。訓練速度快,但準確度下降,且並不是全局最優,也不易於並行實現。

  • 小批量梯度下降法(MiniBatch GD):在每次更新參數時使用b個樣本(例如每次10個樣本),在BGD與SGD中取得折中。

每次隻使用一個樣本時,又稱為在線學習或遞增學習。

當訓練集的所有樣本都被使用過一輪,稱為完成一輪**迭代**。

梯度下降算法

若希望通過調整神經網絡中的某個參數來減小整體代價,則可以考慮微分的方法。因為每層的激活函數,以及最終的代價函數都是光滑可導的。所以最終的代價函數$C$對於某個我們感興趣的參數$w,b$也是光滑可導的。輕微撥動某個參數的值,最終的誤差值也會發生連續的輕微的變化。不斷地沿著參數的梯度方向,輕微調整每個參數的值,使得總誤差值向下降的方向前進,最終達到極值點。就是梯度下降法的核心思想。

梯度下降的邏輯

現在假設代價函數$C$為兩個變量$v_1,v_2$的可微函數,梯度下降實際上就是選擇合適的$Δv$,使得$ΔC$為負。由微積分可知:
$$
ΔC ≈ \frac{∂C}{∂v_1} Δv_1 + \frac{∂C}{∂v_2} Δv_2
$$
這裏$Δv$是向量:$Δv = \left[ \begin{array}{v} Δv_1 \ Δv_2 \end{array}\right]$,$ΔC$是梯度向量$\left[ \begin{array}{C} \frac{∂C}{∂v_1} \ \frac{∂C}{∂v_2} \end{array} \right]$,於是上式可重寫為
$$
ΔC ≈ ∇C \cdot Δv
$$
怎樣的$Δv$才能令代價函數的變化量為負呢?一種簡單辦法是令即$Δv$取一個與梯度$∇C$共線反向的小向量,此時$Δv = -η∇C$ ,則損失函數變化量$ΔC ≈ -η{∇C}^2$,可以確保為負值。按照這種方法,通過不斷調整$v$:$v → v' = v -η∇C$,使得$C$最終達到極小值點。

這即梯度下降的涵義所在:**所有參數都會沿著自己的梯度(導數)方向不斷進行輕微下降,使得總誤差到達極值點。**

對於神經網絡,學習的參數實際上是權重$w$與偏置量$b$。原理是一樣的,不過這裏的$w,b$數目非常巨大
$$
w →w' = w-η\frac{∂C}{∂w} \
b → b' = b-η\frac{∂C}{∂b}
$$

真正棘手的問題在於梯度$∇C_w,∇C_b$的計算方式。如果使用微分的方法,通過$\frac {C(p+ε)-C} {ε}$來求參數的梯度,那麼網絡中的每一個參數都需要進行一次前饋和一次$C(p+ε)$的計算,在神經網絡汪洋大海般的參數麵前,這樣的辦法是行不通的。

反向傳播(Back propagation)算法可以解決這一問題。通過巧妙的簡化, 可以在一次前饋與一次反傳中,高效地計算整個網絡中所有參數梯度。

反向傳播

反向傳播算法接受一個打標樣本$(x,y)$作為輸入,給出網絡中所有參數$(W,b)$的梯度。

反向傳播誤差δ

反向傳播算法需要引入一個新的概念:誤差$δ$。誤差的定義源於這樣一種樸素的思想:如果輕微修改某個神經元的帶權輸入$z$,而最終代價$C$已不再變化,則可認為$z$已經到達極值點,調整的很好了。於是損失函數$C$對某神經元帶權輸入$z$的偏導$\frac {∂C}{∂z}$可以作為該神經元上誤差$δ$的度量。故定義第$l$層的第$j^{th}$個神經元上的誤差$δ^l_j$為:
$$
δ^l_j ≡ \frac{∂C}{∂z^l_j}
$$
與激活值$a$,帶權輸入$z$一樣,誤差也可以寫作向量。第$l$層的誤差向量記作$δ^l$。雖然看上去差不多,但之所以使用帶權輸入$z$而不是激活值輸出$a$來定義本層的誤差,有著形式上巧妙的設計。

引入反向傳播誤差的概念,是為了通過誤差向量來計算梯度$∇C_w,∇C_b$。

反向傳播算法一言蔽之:計算出**輸出層誤差**,通過遞推方程逐層回算出**每一層的誤差**,再由每一層的誤差算出**本層的權值梯度與偏置梯度**。

這需要解決四個問題:

  1. 遞推首項:如何計算輸出層的誤差:$δ^L$
  2. 遞推方程:如何根據後一層的誤差$δ^{l+1}$計算前一層誤差$δ^l$
  3. 權值梯度:如何根據本層誤差$δ^l$計算本層權值梯度$∇W^l$
  4. 偏置梯度:如何根據本層誤差$δ^l$計算本層偏置梯度$∇b^l$

這四個問題,可以通過四個反向傳播方程得到解決。

反向傳播方程

方程 說明 編號
$δ^L = ∇C_a ⊙ σ'(z^L)$ 輸出層誤差計算公式 BP1
$δ^l = (W^{l+1})^T δ^{l+1} ⊙ σ'(z^l)$ 誤差傳遞公式 BP2
$∇C_{W^l} = δ^l × {(a^{l-1})}^T $ 權值梯度計算公式 BP3
$∇C_b = δ^l$ 偏置梯度計算公式 BP4

當誤差函數取MSE:$C = \frac 1 2 |\vec{y} -\vec{a}|^2= \frac 1 2 [(y_1 - a_1)^2 + \cdots + (y_{d_L} - a_{d_L})^2]$,激活函數取sigmoid時:

計算方程 說明 編號
$δ^L = (a^L - y) ⊙(1-a^L)⊙ a^L$ 輸出層誤差需要$a^L$和$y$ BP1
$δ^l = (W^{l+1})^T δ^{l+1} ⊙(1-a^l)⊙ a^l $ 本層誤差需要:後層權值$W^{l+1}$,後層誤差$δ^{l+1}$,本層輸出$a^l$ BP2
$∇C_{W^l} = δ^l × {(a^{l-1})}^T $ 權值梯度需要:本層誤差$δ^l$,前層輸出$a^{l-1}$ BP3
$∇C_b = δ^l$ 偏置梯度需要:本層誤差$δ^l$ BP4

反向傳播方程的證明


BP1:輸出層誤差方程

輸出層誤差方程給出了根據網絡輸出$a^L$與標記結果$y$計算輸出層誤差$δ$的方法:
$$
δ^L = (a^L - y) ⊙(1-a^L)⊙ a^L
$$

證明

因為$a^L = σ(z^L)$,本方程可以直接從反向傳播誤差的定義,通過**$a^L$作為中間變量鏈式求導**推導得出:
$$
\frac{∂C}{∂z^L} = \frac{∂C}{∂a^L} \frac{∂a^L}{∂z^L} = ∇C_a σ'(z^L)
$$
而因為誤差函數$C = \frac 1 2 |\vec{y} -\vec{a}|^2= \frac 1 2 [(y_1 - a_1)^2 + ⋯ + (y_{d_L} - a_{d_L})^2]$,方程兩側對某個$a_j$取偏導則有:

$$
\frac {∂C}{∂a^L_j} = (a^L_j-y_j)
$$

因為誤差函數中,其他神經元的輸出不會影響到誤差函數對神經元$j$輸出的偏導,係數也正好平掉了。寫作向量形式即為:$ (a^L - y) $。另一方麵,易證$σ'(z^L) = (1-a^L)⊙ a^L$。

QED


BP2:誤差傳遞方程

誤差傳遞方程給出了根據後一層誤差計算前一層誤差的方法:

$$
δ^l = (W^{l+1})^T δ^{l+1} ⊙ σ'(z^l)
$$

證明

本方程可以直接從反向傳播誤差的定義,以後一層所有神經元的帶權輸入$z^{l+1}$作為中間變量進行鏈式求導推導出:

$$
δ^l_j = \frac {∂C}{∂z^l_j}
= \sum_{k=1}^{d_{l+1}} \frac{∂C}{∂z^{l+1}_k} \frac{∂z^{l+1}_k}{∂z^{l}_j}
= \sum_{k=1}^{d_{l+1}} (δ^{l+1}_k \frac{∂z^{l+1}_k}{∂z^{l}_j})
$$

通過鏈式求導,引入後一層帶權輸入作為中間變量,從而在方程右側引入後一層誤差的表達形式。現在要解決的就是$\frac{∂z^{l+1}_k}{∂z^{l}_j}​$ 是什麼的問題。由帶權輸入的定義$z = wx + b​$可知:

$$
z^{l+1}_k = W^{l+1}_{k,*} ·a^l + b^{l+1}_k = W^{l+1}_{k,*} · σ(z^l) + b^{l+1}_k
= \sum_{j=1}^{d_{l}}(w_{kj}^{l+1} σ(z^l_j)) + b^{l+1}_k
$$

兩邊同時對$z^{l}_j$求導可以得到:

$$
\frac{∂z^{l+1}_k}{∂z^{l}_j} = w^{l+1}_{kj} σ'(z^l)
$$

回代則有:

$$
\begin{align}
δ^l_j & = \sum_{k=1}^{d_{l+1}} (δ^{l+1}_k \frac{∂z^{l+1}_k}{∂z^{l}_j}) \
& = σ'(z^l) \sum_{k=1}^{d_{l+1}} (δ^{l+1}_k w^{l+1}_{kj}) \
& = σ'(z^l) ⊙ [(δ^{l+1}) · W^{l+1}_{*.j}] \
& = σ'(z^l) ⊙ [(W^{l+1})^T_{j,*} · (δ^{l+1}) ]\
\end{align}
$$

這裏,對後一層所有神經元的誤差權值之積求和,可以改寫為兩個向量的點積:

  • 後一層$k$個神經元的誤差向量
  • 後一層權值矩陣的第$j$列,即所有從本層$j$神經元出發前往下一層所有$k$個神經元的權值。

又因為向量點積可以改寫為矩陣乘法:以行向量乘以列向量的方式進行,所以將權值矩陣轉置,原來拿的是列,現在則拿出了行向量。這時候再改寫回向量形式為:

$$
δ^l = σ'(z^l) ⊙ (W^{l+1})^Tδ^{l+1}
$$

QED


BP3:權值梯度方程

每一層的權值梯度$∇C_{W^l}$可以根據本層的誤差向量(列向量),與上層的輸出向量(行向量)的外積得出。

$$
∇C_{W^l} = δ^l × {(a^{l-1})}^T
$$

證明

由誤差的定義,以$w^l_{jk}$作為中間變量求偏導可得:

$$
\begin{align}
δ^l_j & = \frac{∂C}{∂z^l_j}
= \frac{∂C}{∂w^l_{jk}} \frac{∂ w_{jk}}{∂ z^l_j}
= ∇C_{w^l_{jk}} \frac{∂w_{jk}}{∂ z^l_j}
\end{align}
$$

由定義可得,第$l$層第$j$個神經元的帶權輸入$z^l_j$:

$$
z^l_j = \sum_k w^l_{jk} a^{l-1}_k + b^l_j
$$

兩側對$w_{jk}^l$求導得到:

$$
\frac{\partial z_j}{\partial w^l_{jk}} = a^{l-1}_k
$$

代回則有:
$$
∇C_{w^l_{jk}} = δ^l_j \frac{∂ z^l_j}{∂w_{jk}} = δ^l_j a^{l-1}_k
$$
觀察可知,向量形式是一個外積:
$$
∇C_{W^l} = δ^l × {(a^{l-1})}^T
$$

  • 本層誤差行向量:$δ^l$,維度為($d_l \times 1$)

  • 上層激活列向量:$(a^{l-1})^T​$,維度為($1 \times d_{l-1}​$)

QED


BP4:偏置梯度方程

$$
∇C_b = δ^l
$$

證明

由定義可知:

$$
δ^l_j = \frac{∂C}{∂z^l_j}
= \frac{∂C}{∂b^l_j} \frac{∂b_j}{∂z^l_j}
= ∇C_{b^l_{j}} \frac{∂b_j}{∂z^l_j}
$$

因為$z^l_j = W^l_{*,j} \cdot a^{l-1} + b^l_j$,兩側對$z_j^l$求導得到$1=\frac{∂b_j}{∂z^l_j}$。於是回代得到:$∇C_{b^l_{j}} =δ^l_j $ ,

QED


至此,四個方程均已證畢。隻要將其轉換為代碼即可工作。

神經網絡的實現

作為概念驗證,這裏給出了MNIST手寫數字分類神經網絡的Python實現。

# coding: utf-8
# author: vonng(fengruohang@outlook.com)
# ctime: 2017-05-10

import random
import numpy as np

class Network(object):
    def __init__(self, sizes):
        self.sizes = sizes
        self.L = len(sizes)
        self.layers = range(0, self.L - 1)
        self.w = [np.random.randn(y, x) for x, y in zip(sizes[:-1], sizes[1:])]
        self.b = [np.random.randn(x, 1) for x in sizes[1:]]

    def feed_forward(self, a):
        for l in self.layers:
            a = 1.0 / (1.0 + np.exp(-np.dot(self.w[l], a) - self.b[l]))
        return a

    def gradient_descent(self, train, test, epoches=30, m=10, eta=3.0):
        for round in range(epoches):
            # generate mini batch
            random.shuffle(train)
            for batch in [train_data[k:k + m] for k in xrange(0, len(train), m)]:
                x = np.array([item[0].reshape(784) for item in batch]).transpose()
                y = np.array([item[1].reshape(10) for item in batch]).transpose()
                n, r, a = len(batch), eta / len(batch), [x]

                # forward & save activations
                for l in self.layers:
                    a.append(1.0 / (np.exp(-np.dot(self.w[l], a[-1]) - self.b[l]) + 1))

                # back propagation
                d = (a[-1] - y) * a[-1] * (1 - a[-1])   #BP1
                for l in range(1, self.L):  # l is reverse index since last layer
                    if l > 1:   #BP2
                        d = np.dot(self.w[-l + 1].transpose(), d) * a[-l] * (1 - a[-l])
                    self.w[-l] -= r * np.dot(d, a[-l - 1].transpose()) #BP3
                    self.b[-l] -= r * np.sum(d, axis=1, keepdims=True) #BP4

            # evaluate
            acc_cnt = sum([np.argmax(self.feed_forward(x)) == y for x, y in test])
            print "Round {%d}: {%s}/{%d}" % (round, acc_cnt, len(test_data))


if __name__ == '__main__':
    import mnist_loader

    train_data, valid_data, test_data = mnist_loader.load_data_wrapper()
    net = Network([784, 100, 10])
    net.gradient_descent(train_data, test_data, epoches=100, m=10, eta=2.0)

數據加載腳本:mnist_loader.py 。輸入數據為二元組列表:(input(784,1), output(10,1))

$ python net.py
Round {0}: {9136}/{10000}
Round {1}: {9265}/{10000}
Round {2}: {9327}/{10000}
Round {3}: {9387}/{10000}
Round {4}: {9418}/{10000}
Round {5}: {9470}/{10000}
Round {6}: {9469}/{10000}
Round {7}: {9484}/{10000}
Round {8}: {9509}/{10000}
Round {9}: {9539}/{10000}
Round {10}: {9526}/{10000}

一輪迭代後,網絡在測試集上的分類準確率就達到90%,最終收斂至96%左右。

對於五十行代碼,這個效果是值得驚歎的。然而96%的準確率在實際生產中恐怕仍然是無法接受的。想要達到更好的效果,就需要對神經網絡進行優化。

神經網絡優化

神經網絡的基礎知識也就這麼多,但改善其表現卻是一個無盡的挑戰。每一種優化的手段,都可以當做一個進階的Topic深入研究。優化手段也是八仙過海,有數學,有科學,有工程學,也有哲學,還有玄學……

改進神經網絡的學習效果有幾種主要的方法:

  • 選取**更好的代價函數**:例如**交叉熵(cross-entropy)**
  • 規範化(regularization):**L2規範化**、棄權、L1規範化
  • 采用其他的**激活神經元**:線性修正神經元(ReLU),雙曲正切神經元(tansig)
  • 修改神經網絡的輸出層:**柔性最大值(softmax)**
  • 修改神經網絡輸入的組織方式:遞歸神經網絡(Recurrent NN),卷積神經網絡(Convolutional NN)。
  • 添加層數:深度神經網絡(Deep NN)
  • 通過嚐試,選擇合適的**超參數(hyper-parameters)**,按照迭代輪數或評估效果動態調整超參數。
  • 采用其他的梯度下降方法:基於動量的梯度下降
  • 使用更好的**初始化權重**
  • 人為擴展已有訓練數據集

這裏介紹兩種方法,**交叉熵代價函數**與**L2規範化**。因為它們:

  • 實現簡單,修改一行代碼即可實現,還減小了計算開銷。
  • 效果立竿見影,將分類錯誤率從4%降低到2%以下。

代價函數:交叉熵

MSE是一個不錯的代價函數,然而它存在一個很尷尬的問題:學習速度。

MSE輸出層誤差的計算公式為:
$$
δ^L = (a^L - y)σ'(z^L)
$$
sigmoid又稱為邏輯斯蒂曲線,其導數$σ'$是一個鍾形曲線。所以當帶權輸入$z$從大到小或從小到大時,梯度的變化會經曆一個“小,大,小”的過程。學習的速度也會被導數項拖累,存在一個“慢,快,慢”的過程。

MSE Cross Entropy
mse.png cross-entropy.png

若采用**交叉熵(cross entropy)**誤差函數:
$$
C = - \frac 1 n \sum_x [ y ln(a) + (1-y)ln(1-a)]
$$
對於單個樣本,即
$$
C = - [ y ln(a) + (1-y)ln(1-a)]
$$
雖然看起來很複雜,但輸出層的誤差公式變得異常簡單,變為:$δ^L = a^L - y$

比起MSE少掉了導數因子,所以誤差直接和(預測值-實際值)成正比,不會遇到學習速度被激活函數的導數拖慢的問題,而且計算起來更簡單了!

證明

$C$對網絡輸出值$a$求導,則有:
$$
∇C_a = \frac {∂C} {∂a^L} = - [ \frac y a - \frac {(1-y)} {1-a}] = \frac {a - y} {a (1-a)}
$$
反向傳播的四個基本方程裏,與誤差函數$C$相關的隻有BP1:即輸出層誤差的計算方式。
$$
δ^L = ∇C_a ⊙ σ'(z^L)
$$
現在$C$換了計算方式,將新的誤差函數$C$對輸出值$a^L$的梯度$\frac {∂C} {∂a^L}$帶回BP1,即有:
$$
δ^L = \frac {a - y} {a (1-a)}× a(1-a) = a-y
$$

規範化

擁有大量的自由參數的模型能夠描述特別神奇的現象。

費米說:"With four parameters I can fit an elephant, and with five I can make him wiggle his trunk"。神經網絡這種動輒百萬的參數的模型能擬合出什麼奇妙的東西是難以想象的。

一個模型能夠很好的擬合已有的數據,可能隻是因為模型中足夠的自由度,使得它可以描述幾乎所有給定大小的數據集,而不是真正洞察數據集背後的本質。發生這種情形時,**模型對已有的數據表現的很好,但是對新的數據很難泛化**。這種情況稱為**過擬合(overfitting)**。

例如用3階多項式擬合一個帶隨機噪聲的正弦函數,看上去就還不錯;而10階多項式,雖然完美擬合了數據集中的所有點,但實際預測能力就很離譜了。它擬合的更多地是數據集中的噪聲,而非數據集背後的潛在規律。

x, xs = np.linspace(0, 2 * np.pi, 10), np.arange(0, 2 * np.pi, 0.001)
y = np.sin(x) + np.random.randn(10) * 0.4
p1,p2 = np.polyfit(x, y, 10), np.polyfit(x, y, 3)
plt.plot(xs, np.polyval(p1, xs));plt.plot(x, y, 'ro');plt.plot(xs, np.sin(xs), 'r--')
plt.plot(xs, np.polyval(p2, xs));plt.plot(x, y, 'ro');plt.plot(xs, np.sin(xs), 'r--')
3階多項式 10階多項式
overfit-3.png overfit-10.png

一個模型真正的測驗標準,是它對沒有見過的場景的預測能力,稱為**泛化能力(generalize)**。

如何避免過擬合?按照奧卡姆剃刀原理:**兩個效果相同的解釋,選擇簡單的那一個。**

當然這個原理隻是我們抱有的一種信念,並不是真正的定理鐵律:這些數據點真的由擬合出的十階多項式產生,也不能否認這種可能…

總之,如果出現非常大的權重參數,通常就意味著過擬合。例如擬合所得十階多項式係數就非常畸形:

 -0.001278386964370502
 0.02826407452052734
 -0.20310716176300195
 0.049178327509096835
 7.376259706365357
 -46.295365250182925
 135.58265224859255
 -211.767050023543
 167.26204130954324
 -50.95259728945658
 0.4211227089756039

通過添加權重衰減項,可以有效遏製過擬合。例如$L2$規範化為損失函數添加了一個$\frac λ 2 w^2$的懲罰項:
$$
C = -\frac{1}{n} \sum_{xj} \left[ y_j \ln a^L_j+(1-y_j) \ln
(1-a^L_j)\right] + \frac{\lambda}{2n} \sum_w w^2
$$
所以,權重越大,損失值越大,這就避免神經網絡了向擬合出畸形參數的方向發展。

這裏使用的是交叉熵損失函數。但無論哪種損失函數,都可以寫成:
$$
C = C_0 + \frac {λ}{2n} \sum_w {w^2}
$$
其中原始的代價函數為$C_0$。那麼,原來損失函數對權值的偏導,就可以寫成:
$$
\frac{∂C}{∂w} = \frac{ ∂C_0}{∂w}+\frac{λ}{n} w
$$
因此,引入$L2$規範化懲罰項在計算上的唯一變化,就是在處理權值梯度時首先要乘一個衰減係數:

$$
w → w' = w\left(1 - \frac{ηλ}{n} \right)- η\frac{∂C_0}{∂ w}
$$

注意這裏的$n$是所有的訓練樣本數,而不是一個小批次使用的訓練樣本數。

改進實現

# coding: utf-8
# author: vonng(fengruohang@outlook.com)
# ctime: 2017-05-10

import random
import numpy as np

class Network(object):
    def __init__(self, sizes):
        self.sizes = sizes
        self.L = len(sizes)
        self.layers = range(0, self.L - 1)
        self.w = [np.random.randn(y, x) / np.sqrt(x) for x, y in zip(sizes[:-1], sizes[1:])]
        self.b = [np.random.randn(x, 1) for x in sizes[1:]]

    def feed_forward(self, a):
        for l in self.layers:
            a = 1.0 / (1.0 + np.exp(-np.dot(self.w[l], a) - self.b[l]))
        return a

    def gradient_descent(self, train, test, epoches=30, m=10, eta=0.1, lmd=5.0):
        n = len(train)
        for round in range(epoches):
            random.shuffle(train)
            for batch in [train_data[k:k + m] for k in xrange(0, len(train), m)]:
                x = np.array([item[0].reshape(784) for item in batch]).transpose()
                y = np.array([item[1].reshape(10) for item in batch]).transpose()
                r = eta / len(batch)
                w = 1 - eta * lmd / n

                a = [x]
                for l in self.layers:
                    a.append(1.0 / (np.exp(-np.dot(self.w[l], a[-1]) - self.b[l]) + 1))

                d = (a[-1] - y)  # cross-entropy    BP1
                for l in range(1, self.L):
                    if l > 1:   # BP2
                        d = np.dot(self.w[-l + 1].transpose(), d) * a[-l] * (1 - a[-l])
                    self.w[-l] *= w  # weight decay
                    self.w[-l] -= r * np.dot(d, a[-l - 1].transpose())  # BP3
                    self.b[-l] -= r * np.sum(d, axis=1, keepdims=True)  # BP4

            acc_cnt = sum([np.argmax(self.feed_forward(x)) == y for x, y in test])
            print "Round {%d}: {%s}/{%d}" % (round, acc_cnt, len(test_data))


if __name__ == '__main__':
    import mnist_loader
    train_data, valid_data, test_data = mnist_loader.load_data_wrapper()
    net = Network([784, 100, 10])
    net.gradient_descent(train_data, test_data, epoches=50, m=10, eta=0.1, lmd=5.0)
Round {0}: {9348}/{10000}
Round {1}: {9538}/{10000}
Round {2}: {9589}/{10000}
Round {3}: {9667}/{10000}
Round {4}: {9651}/{10000}
Round {5}: {9676}/{10000}
...
Round {25}: {9801}/{10000}
Round {26}: {9799}/{10000}
Round {27}: {9806}/{10000}
Round {28}: {9804}/{10000}
Round {29}: {9804}/{10000}
Round {30}: {9802}/{10000}

可見隻是簡單的變更,就使準確率有了顯著提高,最終收斂至98%。

修改Size為[784,128,64,10]添加一層隱藏層,可以進一步提升測試集準確率至98.33%,驗證集至98.24%。

對於MNIST數字分類任務,目前最好的準確率為99.79%,那些識別錯誤的case,恐怕人類想要正確識別也很困難。神經網絡的分類效果最新進展可以參看這裏:classification_datasets_results

本文是tensorflow官方推薦教程:Neural Networks and Deep Learning的筆記整理,原文Github Page

最後更新:2017-05-17 00:45:59

  上一篇:go  從一到六談起,聊聊六款值得購買的遊戲
  下一篇:go  IT人=格子衫?讓快時尚品牌助你穿出剪裁感