1. 程式人生 > >從 0 開始機器學習 - 神經網路反向 BP 演算法!

從 0 開始機器學習 - 神經網路反向 BP 演算法!

最近一個月專案好忙,終於擠出時間把這篇 BP 演算法基本思想寫完了,公式的推導放到下一篇講吧。 ## 一、神經網路的代價函式 神經網路可以看做是複雜邏輯迴歸的組合,因此與其類似,我們訓練神經網路也要定義代價函式,之後再使用梯度下降法來最小化代價函式,以此來訓練最優的權重矩陣。 ### 1.1 從邏輯迴歸出發 我們從經典的邏輯迴歸代價函式引出,先來複習下: $$ J(\theta) = \frac{1}{m}\sum\limits_{i = 1}^{m}{[-{y^{(i)}}\log ({h_\theta}({x^{(i)}}))-( 1-{y^{(i)}})\log ( 1 - h_\theta({x^{(i)}}))]} + \frac{\lambda}{2m} \sum\limits_{j=1}^{n}{\theta_j^2} $$ 邏輯迴歸代價函式計算每個樣本的輸入與輸出的誤差,然後累加起來除以樣本數,再加上正則化項,這個我之前的部落格已經寫過了: - [從 0 開始機器學習 - 邏輯迴歸原理與實戰!](https://dlonng.com/posts/logistic-regression) - [從 0 開始機器學習 - 正則化技術原理與程式設計!](https://dlonng.com/posts/regularization) 這裡補充一點對單變數邏輯迴歸代價函式的理解,雖然這一行代價公式很長: $$ cost(i) = -{y^{(i)}}\log ({h_\theta}({x^{(i)}}))-( 1-{y^{(i)}})\log ( 1 - h_\theta({x^{(i)}})) $$ 但是其實可以把它簡單的理解為輸出與輸入的方差,雖然形式上差別很大,但是可以幫助我們理解上面這個公式到底在計算什麼,就是計算輸出與輸入的方差,這樣理解就可以: $$ cost(i) = h_{\theta}(x^{(i)} - y^{(i)})^2 $$ ### 1.2 一步步寫出神經網路代價函式 前面講的簡單邏輯迴歸的只有一個輸出變數,但是在神經網路中輸出層可以有多個神經元,所以可以有很多種的輸出,比如 K 分類問題,神經元的輸出是一個 K 維的向量: ![](https://dlonng.oss-cn-shenzhen.aliyuncs.com/blog/k_output.jpeg) 因此我們需要對每個維度計算預測輸出與真實標籤值的誤差,即對 K 個維度的誤差做一次求和: $$ \sum\limits_{i = 1}^{k}{[-{y_k^{(i)}}\log ({h_\theta}({x^{(i)}}))_k-( 1-{y_k^{(i)}})\log ( 1 - h_\theta({x^{(i)}})_k)]} $$ 然後累加訓練集的 m 個樣本: $$ -\frac{1}{m}[\sum\limits_{i = 1}^{m}\sum\limits_{k = 1}^{k}{[-{y_k^{(i)}}\log ({h_\theta}({x^{(i)}}))_k-( 1-{y_k^{(i)}})\log ( 1 - h_\theta({x^{(i)}})_k)]}] $$ 再加上所有權重矩陣元素的正則化項,注意 $i, j$ 都是從 1 開始的,因為每一層的 $\theta_0$ 是偏置單元,不需要對其進行正則化: $$ \frac{\lambda}{2m}\sum\limits_{i = l}^{L - 1}\sum\limits_{i = 1}^{S_l}\sum\limits_{j = 1}^{S_l + 1}(\theta_{ji}^{(l)})^2 $$ - 最內層求和:迴圈一個權重矩陣所有的行,行數是 $S_l + 1$ 層啟用單元數 - 中間層求和:迴圈一個權重矩陣所有的列,列數是 $S_l$ 層啟用單元數 - 最外層求和:迴圈所有的權重矩陣 這就得到了輸出層為 K 個單元神經網路最終的代價函式: $$ J(\theta) = -\frac{1}{m}[\sum\limits_{i = 1}^{m}\sum\limits_{k = 1}^{k}{[-{y_k^{(i)}}\log ({h_\theta}({x^{(i)}}))_k-( 1-{y_k^{(i)}})\log ( 1 - h_\theta({x^{(i)}})_k)]}] + \frac{\lambda}{2m}\sum\limits_{i = l}^{L - 1}\sum\limits_{i = 1}^{S_l}\sum\limits_{j = 1}^{S_l + 1}(\theta_{ji}^{(l)})^2 $$ 有了代價函式後,就可以通過反向傳播演算法來訓練一個神經網路啦! ## 二、神經網路反向 BP(Back Propagation) 演算法 ### 2.1 BP 演算法簡介 之前寫神經網路基礎的時候,跟大家分享瞭如何用訓練好的神經網路來預測手寫字元:[從 0 開始機器學習 - 神經網路識別手寫字元!](https://dlonng.com/posts/neural-digit),只不過當時我們沒有訓練網路,而是使用已經訓練好的神經網路的權重矩陣來進行前饋預測,那麼我們如何自己訓練神經網路呢? 這就需要學習反向 BP 演算法,這個演算法可以幫助我們求出神經網路權重矩陣中每個元素的偏導數,進而利用梯度下降法來最小化上面的代價函式,你可以聯想簡單的線性迴歸演算法:[從 0 開始機器學習 - 一文入門多維特徵梯度下降法!](https://dlonng.com/posts/ml-multi-feature),也是先求每個引數的偏導數,然後在梯度下降演算法中使用求出的偏導數來迭代下降。 因此訓練神經網路的關鍵就是:**如何求出每個權重係數的偏導數?**,反向 BP 就可以解決這個問題!這裡強烈建議你學習的時候完全搞懂 BP 演算法的原理,最好自己獨立推導一遍公式,因為你以後學習深度學習那些複雜的網路,不管是哪種,最終都要使用反向 BP 來訓練,這個 BP 演算法是最核心的東西,面試也逃不過的,所以既然要學,就要學懂,不然就是在浪費時間。 ### 2.2 BP 演算法基本原理 我先用個例子簡單介紹下 BP 演算法的基本原理和步驟,公式的推導放到下一節,反向 BP 演算法顧名思義,與前饋預測方向相反: - 計算最後一層輸出與實際標籤值的誤差,反向傳播到倒數第二層 - 計算倒數第二層的傳播誤差,反向傳播到倒數第三層 - 以此類推,一層一層地求出各層的誤差 - 直到第二層結束,因為第一層是輸入特徵,不是我們計算的,所以不需要求誤差 以下面這個 4 層的神經網路為例: ![](https://dlonng.oss-cn-shenzhen.aliyuncs.com/blog/bp-example.png) 假如我們的訓練集只有 1 個樣本 $(x^{(1)}, y^{(1)})$,每層所有啟用單元的輸出用 $a^{(i)}$ 向量表示,每層所有啟用單元的誤差用 $\delta^{(i)}$ 向量表示,來看下反向傳播的計算步驟(公式的原理下一節講): 1. 輸出層的誤差為預測值減去真實值:$\delta^{(4)} = a^{(4)} - y^{(1)}$ 2. 倒數第二層的誤差為:$\delta^{(3)} = (W^{(3)})^T \delta^{(4)} * g'(z^{(3)})$ 3. 倒數第三層的誤差為:$\delta^{(2)} = (W^{(2)})^T \delta^{(3)} * g'(z^{(2)})$ 4. 第一層是輸入變數,不需要計算誤差 有了每層所有啟用單元的誤差後,就可以計算代價函式對每個權重引數的偏導數,即每個啟用單元的輸出乘以對應的誤差,這裡不考慮正則化: $$ \frac {\partial}{\partial W_{ij}^{(l)}} J (W) = a_{j}^{(l)} \delta_{i}^{(l+1)} $$ 解釋下這個偏導數的計算: - $l$ 表示目前計算的是第幾層 - $j$ 表示當前層中正在計算的啟用單元下標($j$ 作為列) - $i$ 表示下一層誤差單元的下標($i$ 作為行) 這個計算過程是對一個樣本進行的,網路的輸入是一個特徵向量,所以每層計算的誤差也是向量,但是我們的網路輸入是特徵矩陣的話,就不能用一個個向量來表示誤差了,而是應該也將誤差向量組成誤差矩陣,因為特徵矩陣就是多個樣本,每個樣本都做一個反向傳播,就會計算誤差,所以我們每次都把一個樣本計算的誤差累加到誤差矩陣中: $$ \Delta_{ij}^{(l)} = \Delta_{ij}^{(l)} + a_{j}^{(l)} \delta_{i}^{(l+1)} $$ 然後,我們需要除以樣本總數 $m$,因為上面的誤差是累加了所有 $m$ 個訓練樣本得到的,並且我們還需要考慮加上正則化防止過擬合,注意對偏置單元不需要正則化,這點已經提過好多次了: - 非偏置單元正則化後的偏導數 $j \neq 0$: $$ D_{ij}^{(l)} = \frac {1}{m}\Delta_{ij}^{(l)}+\lambda W_{ij}^{(l)} $$ - 偏置單元正則化後的偏導數 $j = 0$: $$ D_{ij}^{(l)} = \frac{1}{m}\Delta_{ij}^{(l)} $$ 最後計算的所有偏導數就放在誤差矩陣中: $$ \frac {\partial}{\partial W_{ij}^{(l)}} J (W) = D_{ij}^{(l)} $$ 這樣我們就求出了每個權重引數的偏導數,再回想之前的梯度下降法,我們有了偏導數計算方法後,直接送到梯度下降法中進行迭代就可以最小化代價函數了,比如我在 Python 中把上面的邏輯寫成一個正則化梯度計算的函式 `regularized_gradient`,然後再用 `scipy.optimize` 等優化庫直接最小化文章開頭提出的神經網路代價函式,以此來使用反向 BP 演算法訓練一個神經網路: ```python import scipy.optimize as opt res = opt.minimize(fun = 神經網路代價函式, x0 = init_theta, args = (X, y, 1), method = 'TNC', jac = regularized_gradient, options = {'maxiter': 400}) ``` 所以神經網路反向 BP 演算法關鍵就是理解每個權重引數偏導數的計算步驟和方法!關於偏導數計算公式的詳細推導過程,我打算在下一篇文章中單獨分享,本次就不帶大家一步步推導了,否則內容太多,先把基本步驟搞清楚,後面推導公式就容易理解。 ### 2.3 反向 BP 演算法的直觀理解 之前學習前饋預測時,我們知道一個啟用單元是輸入是上一層所有啟用單元的輸出與權重的加權和(包含偏置),計算方向從左到右,計算的是每個啟用單元的輸出,看圖: ![](https://dlonng.oss-cn-shenzhen.aliyuncs.com/blog/forward_prect.png) 其實反向 BP 演算法也是做類似的計算,一個啟用單元誤差的輸入是後一層所有誤差與權重的加權和(可能不包含偏置),只不過這裡計算的反向是從右向左,計算的是每個啟用單元的誤差,對比看圖: ![](https://dlonng.oss-cn-shenzhen.aliyuncs.com/blog/back_bp_delta.png) 你只需要把單個神經元的前饋預測和反向 BP 的計算步驟搞清楚就可以基本理解反向 BP 的基本過程,因為所有的引數都是這樣做的。 ## 三、神經網路程式設計細節 ### 3.1 隨機初始化 每種優化演算法都需要初始化引數,之前的線性迴歸初始化引數為 0 是沒問題的,但是如果把神經網路的初始引數都設定為 0,就會有問題,因為第二層的輸入是要用到權重與啟用單元輸出的乘積: - 如果權重都是 0,則每層網路的輸出都是 0 - 如果權重都是相同的常數 $a$,則每層網路的輸出也都相同,只是不為 0 ![](https://dlonng.oss-cn-shenzhen.aliyuncs.com/blog/random_init.png) 所以為了在神經網路中避免以上的問題,我們採用隨機初始化,把所有的引數初始化為 $[-\epsilon, \epsilon]$ 之間的隨機值,比如初始化一個 10 X 11 的權重引數矩陣: $$ initheta = rand(10, 11) * (2 * \epsilon) - \epsilon $$ ### 3.2 矩陣 <-> 向量 注意上面優化庫的輸入 `X0 = init_theta` 是一個向量,而我們的神經網路每 2 層之間就有一個權重矩陣,所以為了把權重矩陣作為優化庫的輸入,我們必須要把所有的權重引數都組合到一個向量中,也就是實現一個把矩陣組合到向量的功能,但是優化庫的輸出也是一個包含所有權重引數的向量,我們拿到向量後還需要把它轉換為每 2 層之間的權重矩陣,這樣才能進行前饋預測: - 訓練前:初始多個權重矩陣 -> 一個初始向量 - 訓練後:一個最優向量 -> 多個最優權重矩陣 ![](https://dlonng.oss-cn-shenzhen.aliyuncs.com/blog/matrix2vec.png) ### 3.3 梯度校驗 梯度校驗是用來檢驗我們的 BP 演算法計算的偏導數是否和真實的偏導數存在較大誤差,計算以下 2 個偏導數向量的誤差: - 反向 BP 演算法計算的偏導數 - 利用導數定義計算的偏導數 對於單個引數,在一點 $\theta$ 處的導數可由 $[\theta - \epsilon, \theta + \epsilon]$ 表示,這也是導數定義的一種: $$ grad = \frac{J(\theta + \epsilon) - J(\theta - \epsilon)}{2 \epsilon} $$ 如圖: ![](https://dlonng.oss-cn-shenzhen.aliyuncs.com/blog/grad_check.png) 但是我們的神經網路代價函式有很多引數,當我們把引數矩陣轉為向量後,可以對向量裡的每個引數進行梯度檢驗,只需要分別用定義求偏導數即可,比如檢驗 $\theta_1$: $$ \frac {\partial J}{\partial \theta_1} = \frac {J (\theta_1 + \varepsilon_1, \theta_2, \theta_3 ... \theta_n ) - J(\theta_1 - \varepsilon_1, \theta_2, \theta_3 ... \theta_n)}{2 \varepsilon} $$ 以此類推,檢驗 $\theta_n$: $$ \frac {\partial J}{\partial \theta_n} = \frac {J (\theta_1, \theta_2, \theta_3 ... \theta_n + \varepsilon_n) - J(\theta_1, \theta_2, \theta_3 ... \theta_n - \varepsilon_n)}{2 \varepsilon} $$ 求出導數定義的偏導數後,與 BP 演算法計算的偏導數計算誤差,在誤差範圍內認為 BP 演算法計算的偏導數(D_vec)是正確的,梯度檢驗的虛擬碼如下: ```python for i = 1 : n theta_plus = theta theta_plus[i] = theta_plus + epsilon theta_minu = theta theta_minu[i] = theta_minu - epsilon grad = (J(theta_plus) - J(theta_minu)) / (2 * epsilon) end check 誤差: grad 是否約等於 D_vec ``` **注意一點**:梯度檢驗通常速度很慢,在訓練神經網路前先別進行檢驗! 今天就到這,溜了溜了,下篇文章見:) ![](https://dlonng.oss-cn-shenzhen.aliyuncs.com/yingliu_code/yinliu_c