近來正在系統研究一下深度學習,做爲新入門者,爲了更好地理解、交流,準備把學習過程總結記錄下來。最開始的規劃是先學習理論推導;而後學習一兩種開源框架;第三是進階調優、加速技巧。越日後越要帶着工做中的實際問題去作,而不能是空中樓閣式沉迷在理論資料的舊數據中。深度學習領域大牛吳恩達(Andrew Ng)老師的UFLDL教程 (Unsupervised Feature Learning and Deep Learning)提供了很好的基礎理論推導,文辭既系統完備,又明白曉暢,最難得的是能把在斯坦福大學中的教學推廣給全世界,盡心力而廣育人,實教授之典範。
這裏的學習筆記不是爲了重複UFLDL中的推導過程,而是爲了一方面補充我沒有很快看明白的理論要點,另外一方面基於我對課後練習的matlab實現,記錄討論卡殼時間較長的地方。也方便其餘學習者,學習過程當中UFLDL全部留白的代碼模塊全是本身編寫的,沒有查網上的代碼,通過一番調試結果都達到了練習給出的參考標準,並且都按照矩陣化實現(UFLDL稱爲矢量化 vectorization),代碼連接見此https://github.com/codgeek/deeplearning,因此各類matlab實現細節、原因也會比較清楚,此外從實現每次練習的代碼commit中能夠清楚看到修改了那些代碼,歡迎你們一塊兒討論共同進步。php
先上一個經典的自編碼神經網絡結構圖。推導中經常使用的符號表徵見於此git
上圖的神經網絡結構中,輸入單個向量\(\ x\\),逐層根據網絡參數矩陣\(\ W, b\\)計算前向傳播,最後輸出向量$ h_{W,b}(x)$, 這樣單個向量的偏差代價函數是github
\[J{(W,b,x,y)}=\frac{1}{2}{||(h_{W,b}(x)-y||}^2\]算法
對全部數據代價之和即數組
\[\begin{align} J(W,b) &= \frac 1 m \left [\sum_{i=1}^m J{(W,b;x^{(i)},y^{(i)})} \right] +\frac \lambda 2 \sum_{l=1}^{n_l-1}\sum_{i=1}^{s_l}\sum_{j=1}^{s_{l+1}}{W_{ij}^{(l)}}^2 \\ &= \frac 1 m \left [\sum_{i=1}^m{\frac 1 2||(h_{W,b}(x^{(i)})-y^{(i)}||}^2 \right] +\frac \lambda 2 \sum_{l=1}^{n_l-1}\sum_{i=1}^{s_l}\sum_{j=1}^{s_{l+1}}{W_{ij}^{(l)}}^2 \end{align} \]網絡
神經網絡的最基礎的理論是反向傳導算法,反向傳導的主語是偏差代價函數\(\ J_{W,b}\\)對網絡係數(W,b)的偏導數。那爲何要求偏導呢? 這裏就引出了最優化理論,具體的問題通常被建模成許多自變量決定的代價函數,最優化的目標就是找到使代價函數最小化的自變量取值。數學方法告訴咱們令偏導爲0對應的自變量取值就是最小化代價的點,不幸的是多數實際問題對用偏導爲0方程求不出閉合的公式解,神經網絡也不例外。爲了解決這類問題,可使用優化領域的梯度降低法,梯度是對每一個自變量的偏導組成的向量或矩陣,梯度降低直白地說就是沿着對每一個自變量偏導的負方向改變自變量,降低指的是負方向,直到梯度值接近0或代價函數變化很小,即中止查找認爲找到了達到最優化的全部自變量數值。 因此爲了最小化稀疏自編碼偏差代價函數,咱們須要求出J(W,b)對神經網絡各層係數W、b的偏導數組成的梯度矩陣。框架
接下來在反向傳導算法文章中給出了詳細的推導過程,推導的結果即是殘差、梯度值從輸出層到隱藏層的反向傳導公式函數
輸出層的對輸出值\(\ z\\)第i個份量\(\ z_i^{n_l}\\)的偏導爲學習
\[ \delta_i^{(n_l)}= \frac {\partial} {\partial z_i^{(n_l)}} {\frac 1 2||(h_{W,b}(x^{(i)})-y^{(i)}||}^2 = -(y_i - a_i^{(n_l)}) \cdot f'( z_i^{(n_l)}) \]優化
後向傳導時,第l+1層傳導給第l層偏導值爲 \[\delta_i^{(l)} = (\sum_{j=1}^{s_l+1}W_{ij}^{(l)}\delta_j^{(l+1)}) f'( z_i^{(l)}) \]
咱們知道\(\ z_i^{l+1}\\)與\(\ W,b\\)的關係\(\ z_i^{(l+1)}=\sum_{j=1}^{n_l} ({W_{i,j}^{(l)}\cdot{a_j^{(l)}+b}})\\),運用求導鏈式法則可得:
\[\begin{align} \frac {\partial J_{W,b;x,y}}{\partial W_{i,j}^{(l)}} &= \frac {\partial J_{W,b;x,y}}{\partial z_i^{(l+1)}} \cdot \frac {\partial z_i^{(l+1)}}{\partial W_{i,j}^{(l)}} \\ &=\delta_i^{l+1}\cdot a_j^l\end{align}\]
原文中跳過了這一步,基於此,才能獲得偏差函數對參數\(\ W, b\\)每一項的偏導數。
\[ \frac {\partial J_{W,b;x,y}}{\partial W_{i,j}^{(l)}} = \delta_i^{l+1}\cdot a_j^l\]
\[ \frac {\partial J_{W,b;x,y}}{\partial b_{i}^{(l)}} = \delta_i^{l+1}\]
前面的反向傳導是神經網絡的通常形式,具體到稀疏自編碼,有兩個關鍵字「稀疏」、「自」。「自」是指目標值\(\ y\\)和輸入向量\(\ x\\)相等,「稀疏」是指過程要使隱藏層L1的大部分激活值份量接近0,個別份量顯著大於0.因此稀疏自編碼的代價函數中\(\ y\\)直接使用\(\ x\\)的值。同時加上稀疏性懲罰項,詳見稀疏編碼一節。
\[J_sparse(W,b)=J(W,b)+\beta\sum_{j=1}^{S_2}KL(\rho||\hat\rho_j)\]
懲罰項只施加在隱藏層上,對偏導項作對應項添加,也只有隱藏層的偏導增長一項。
\[\delta_i^{(2)} = (\sum_{j=1}^{s_2}W_{ij}^{(2)}\delta_j^{(3)}+\beta (- \frac \rho {\hat \rho_i} + \frac {1-\rho} {1-\hat \rho_i}) ) f'( z_i^{(2)}) \]
後向傳導以及對網絡權值\(\ W, b\\)的梯度公式不變。
在反向傳導的來龍去脈段落,咱們已經知道了要用梯度降低法搜索使代價函數最小的自變量\(\ W, b\\)。實際上梯度降低也不是簡單減梯度,而是有降低速率參數\(\ \alpha\\)的L-BFGS,又牽扯到一個專門的優化。爲了簡化要作的工做量,稀疏自編碼練習中已經幫咱們集成了一個第三方實現,在minFunc文件夾中,咱們只須要提供代價函數、梯度計算函數就能夠調用minFunc實現梯度降低,獲得最優化參數。後續的工做也就是要只須要補上sampleIMAGES.m, sparseAutoencoderCost.m, computeNumericalGradient.m的"YOUR CODE HERE"的代碼段。
UFLDL給你們的學習模式很到家,把周邊的結構性代碼都寫好了matlab代碼與註釋,儘可能給學習者減負。係數自編碼中主m文件是train.m。先用實現好的代價、梯度模塊調用梯度檢驗,而後將上述代價、梯度函數傳入梯度降低優化minFunc。知足迭代次數後退出,獲得訓練好的神經網絡係數矩陣,補全所有待實現模塊的完整代碼見此,https://github.com/codgeek/deeplearning.
其中主要過程是
訓練數據來源是圖片,第一步要在sampleIMAGES.m中將其轉化爲神經網絡\(\ x\\)向量。先看一下訓練數據的樣子吧
sampleIMAGES.m模塊是能夠被後續課程複用的,儘可能寫的精簡通用些。從一幅圖上取patchsize大小的子矩陣,要避免每一個圖上取的位置相同,也要考慮並行化,循環每張圖片取patch效率較低。下文給出的方法是先隨機肯定行起始行列座標,計算出numpatches個這樣的座標值,而後就能每一個patch不關聯地取出來,才能運用parfor作並行循環。
sampleIMAGES.m function patches = sampleIMAGES(patchsize, numpatches) load IMAGES; % load images from disk dataSet=IMAGES; dataSet = IMAGES; patches = zeros(patchsize*patchsize, numpatches); % 初始化數組大小,爲並行循環開闢空間 [row, colum, numPic] = size(dataSet); rowStart = randi(row - patchsize + 1, numpatches, 1);% 從一幅圖上取patchsize大小子矩陣,只須要肯定隨機行起始點,範圍是[1,row - patchsize + 1] columStart = randi(colum - patchsize + 1, numpatches, 1);% 肯定隨機列起始點,範圍是colum - patchsize + 1 randIdx = randperm(numPic); % 肯定從哪一張圖上取子矩陣,打亂排列,防止生成的patch順序排列。 parfor r=1:numpatches % 肯定了起始座標後,每一個patch不關聯,能夠並行循環取 patches(:,r) = reshape(dataSet(rowStart(r):rowStart(r) + patchsize - 1, columStart(r):columStart(r) + patchsize - 1, randIdx(floor((r-1)*numPic/numpatches)+1)),[],1); end patches = normalizeData(patches) end
稀疏自編碼最重要模塊是計算代價、梯度矩陣的sparseAutoencoderCost.m。輸入\(\ W, b\\)與訓練數據data,外加稀疏性因子、稀疏項係數、權重\(\ W\\)係數,值得關注的是後向傳導時隱藏層輸出的殘差delta2是由隱藏層與輸出層的網絡參數\(\ W2\\)和輸出層的殘差delta3決定的,不是輸入層與隱藏層的網絡參數\(\ W1\\),這裏最開始寫錯了,耽誤了一些時間才調試正確。下文結合代碼用註釋的形式解釋每一步具體做用。
sparseAutoencoderCost.m
function [cost,grad] = sparseAutoencoderCost(theta, visibleSize, hiddenSize, ...
lambda, sparsityParam, beta, data)
%% 一維向量重組爲便於矩陣計算的神經網絡係數矩陣。
W1 = reshape(theta(1:hiddenSizevisibleSize), hiddenSize, visibleSize);
W2 = reshape(theta(hiddenSizevisibleSize+1:2hiddenSizevisibleSize), visibleSize, hiddenSize);
b1 = theta(2hiddenSizevisibleSize+1:2hiddenSizevisibleSize+hiddenSize);
b2 = theta(2hiddenSizevisibleSize+hiddenSize+1:end);
%% 前向傳導,W1矩陣爲hiddenSize×visibleSize,訓練數據data爲visibleSize×N_samples,訓練全部數據的過程正好是矩陣相乘$\ W1*data\ $。注意全部訓練數據都共用係數$\ b\ $,而單個向量的每一個份量對用使用$\ b\ $的對應份量,b1*ones(1,m)是將列向量複製m遍,組成和$\ W1*data\ $相同維度的矩陣。 [~, m] = size(data); % visibleSize×N_samples, m=N_samples a2 = sigmoid(W1*data + b1*ones(1,m));% active value of hiddenlayer: hiddenSize×N_samples a3 = sigmoid(W2*a2 + b2*ones(1,m));% output result: visibleSize×N_samples diff = a3 - data; % 自編碼也就意味着將激活值和原始訓練數據作差值。 penalty = mean(a2, 2); % measure of hiddenlayer active: hiddenSize×1 residualPenalty = (-sparsityParam./penalty + (1 - sparsityParam)./(1 - penalty)).*beta; % penalty factor in residual error delta2 % size(residualPenalty) cost = sum(sum((diff.*diff)))./(2*m) + ... (sum(sum(W1.*W1)) + sum(sum(W2.*W2))).*lambda./2 + ... beta.*sum(KLdivergence(sparsityParam, penalty)); % 後向傳導過程,隱藏層殘差須要考慮稀疏性懲罰項,公式比較清晰。 delta3 = -(data-a3).*(a3.*(1-a3)); % visibleSize×N_samples delta2 = (W2'*delta3 + residualPenalty*ones(1, m)).*(a2.*(1-a2)); % hiddenSize×N_samples. !!! => W2'*delta3 not W1'*delta3 % 前面已經推導出代價函數對W2的偏導,矩陣乘法裏包含了公式中l層激活值a向量與1+1層殘差delta向量的點乘。 W2grad = delta3*a2'; % ▽J(L)=delta(L+1,i)*a(l,j). sum of grade value from N_samples is got by matrix product visibleSize×N_samples * N_samples× hiddenSize. so mean value is caculated by "/N_samples" W1grad = delta2*data';% matrix product visibleSize×N_samples * N_samples×hiddenSize b1grad = sum(delta2, 2); b2grad = sum(delta3, 2); % 對m個訓練數據取平均 W1grad=W1grad./m + lambda.*W1; W2grad=W2grad./m + lambda.*W2; b1grad=b1grad./m; b2grad=b2grad./m;% mean value across N_sample: visibleSize ×1 % 矩陣轉列向量 grad = [W1grad(:) ; W2grad(:) ; b1grad(:) ; b2grad(:)]; end
在tarin.m中,將sparseAutoencoderCost.m傳入梯度優化函數minFunc,通過迭代訓練出網絡參數,一般見到的各個方向的邊緣檢測圖表示的是權重矩陣對每一個隱藏層激活值的特徵,當輸入值爲對應特徵值時,每一個激活值會有最大響應,同時的其他隱藏節點處於抑制狀態。因此只須要把W1矩陣每一行的64個向量還原成8*8的圖片patch,也就是特徵值了,每一個隱藏層對應一個,總共100個。結果以下圖。
接下來咱們驗證一下隱藏層對應的輸出是否知足激活、抑制的要求,將上述輸入值用參數矩陣傳導到隱藏層,也就是\(\ W1*W1'\\).可見,每一個輸入對應的隱藏層只有一個像素是白色,表示接近1,其他是暗色調接近0,知足了稀疏性要求,並且激活的像素位置是順序排列的。
繼續改變不一樣的輸入層單元個數,隱藏層單元個數,能夠獲得更多有意思的結果!