Deeplearning4j源碼研習(1): BP算法原理及源碼實現

對於神經網絡來講,訓練的過程是在更新網絡權重和偏重的值,採取的方法有梯度下降、牛頓法等。由於深度學習通常有較多的網絡層數,參數較多,而且二階的優化算法本身就非常消耗內存,因此,實際應用中,梯度下降運用較多。梯度下降更新模型參數的公式:

式子中的代表網絡中的某一個需要訓練的權重參數,K代表第K次迭代,代表學習率/步長。注意,每一層網絡的參數的學習率可以不同。表示損失函數對該權重的梯度。注意,該公式不帶任何正則化項和諸如ADAM等帶有動量的更新策略,是原始梯度下降的寫法,在下面的例子中,也採用該標準形式。

對於該公式,每次迭代前的權重值和學習率都是確定的(學習率一般可以固定,作爲人工設定的超參數之一)。而梯度的計算通常依賴於誤差反向傳播的思想,即BP算法。需要澄清的是,BP算法並非是更新網絡權重的直接算法,而是提供了計算梯度的一種策略。這一點,在Goodfellow《Deep Learning》一書中的第6章中有更爲明確的描述。有興趣的同學可以參考相關內容。就個人理解而言,BP算法是希望每一次迭代時的輸出結果和期望結果誤差的值可以直接作用於權重的更新。雖然在某個具體的問題上,這個誤差的大小由損失函數的形式和學習率等多種因素共同決定,但可以在權重更新中有所體現或者說產生一定的影響非常重要。從純粹計算的角度來看,神經網絡可以認爲是一個複合函數。在定義好損失函數後,梯度就可以基於鏈式法則進行求導計算。在此基礎上,就可以完成一次權重的迭代更新。因此也有很多觀點認爲,反向傳播的核心就是鏈式法則。這裏不討論BP的核心思想,下面就結合XOR的例子來給出BP算法的整個計算過程,並且以此爲例來分析Deeplearning4j的源碼實現。

XOR問題源於一種可以進行異或計算的門電路。其輸入輸出的對應關係可見下圖:

X1 X2 Y
0 0 0
0 1 1
1 0 1
1 1 0

從機器學習的角度看,X1X2表示兩個特徵,Y表示標註。我們可以用分類或者回歸的思想來解決XOR問題。這篇博客採用的是迴歸的方法。爲了簡化後面公式的推導,我們構建一個只含有一層隱藏層的神經網絡。如果用Deeplearning4j建模,則代碼如下:

        int seed = 1234567;
        int iterations = 1;
        MultiLayerConfiguration conf = new NeuralNetConfiguration.Builder()
                                .seed(seed)
                                .iterations(iterations)
                                .learningRate(0.01)
                                .miniBatch(false)
                                .useDropConnect(false)
                                .weightInit(WeightInit.XAVIER)
                                .optimizationAlgo(OptimizationAlgorithm.STOCHASTIC_GRADIENT_DESCENT)
                                .updater(Updater.SGD)
                                .list()
                                .layer(0, new DenseLayer.Builder()
                                                        .nIn(2)
                                                        .nOut(2)
                                                        .activation(Activation.RELU)//Activation.IDENTITY will not work
                                                                                    //since non-linear transformation
                                                                                    //is needed here
                                                        .learningRate(0.01)
                                                        .build())
                                .layer(1, new OutputLayer.Builder(LossFunctions.LossFunction.MSE)
                                                         .activation(Activation.IDENTITY)
                                                         .learningRate(0.01)
                                                         .nIn(2).nOut(1).build())
                                .backprop(true).pretrain(false)
                                .build();
        MultiLayerNetwork model = new MultiLayerNetwork(conf);
        model.init();      

損失函數我們使用的是迴歸問題常用的均方誤差函數(Mean Square Error,MSE)。

在Deeplearning4j中,MSE的定義如下:

其中N是輸出層的維度。優化算法使用的是隨機梯度下降(SGD),同時爲了方便後面公式的推導,我們這裏不使用mini-batch來計算梯度,而是用標準的隨機梯度下降,即利用單條訓練數據來更新權重。每一層的學習率設爲0.01。隱藏層的激勵函數使用RELU。需要注意的是,隨機種子用於初始化權重,固定的種子可以保證每次初始化的權重相同。當然純粹看代碼還未必直觀,下面就直接利用Visio繪出相應的神經網絡結構圖以及初始化後的權重。


圖中的各個組件簡單做下說明:

綠色圓圈:輸入層神經元

藍色圓圈:隱藏層神經元

紅色圓圈:輸出層神經元

紫色方框:激勵函數/非線性變換函數

黑色帶前向箭頭的線:神經元間的連接





從圖中看出,一共有9個參數需要訓練,其中6個是權重值,另外3個是偏置。我們可以直接調用Deeplearning4j中summary接口來獲取神經網絡的參數信息:

============================================================================================================================================
LayerName (LayerType)                   nIn,nOut       TotalParams    ParamsShape                   
============================================================================================================================================
0 (DenseLayer)                          2,2            6              b:{1,2}, W:{2,2}              
1 (OutputLayer)                         2,1            3              b:{1,1}, W:{2,1}              
--------------------------------------------------------------------------------------------------------------------------------------------
            Total Parameters:  9
        Trainable Parameters:  9
           Frozen Parameters:  0
============================================================================================================================================

可以發現通過接口獲取的模型參數信息和我們之前說明的是一致的。

爲了方便後續公式的推導,將公式進行進一步展開



這裏以更新權重爲例來解釋BP算法。

根據之前的描述,利用SGD進行權重更新的時候,需要計算損失函數對於每個權重的梯度。對於,我們需要計算以下公式(當前輸入=0,=1):


即梯度計算的結果爲-0.87165

根據梯度下降更新權重的公式,經過本次迭代後被更新爲:


類似的,b10的權重更新如下:



我們用Deeplearning4j已經搭建好的模型來驗證下我們的計算結果:

Before Fit Model Param: 
0_W,[[0.31, -1.25],
 [-0.54, 0.45]]
0_b,[0.00, 0.00]
1_W,[0.43, 0.07]
1_b,0.00
Iter Training Finish: 0
Feature: [0.00, 0.00]
0_W,[[0.31, -1.25],
 [-0.54, 0.45]]
0_b,[0.00, 0.00]
1_W,[0.43, 0.07]
1_b,0.00
Iter Training Finish: 1
Feature: [0.00, 1.00]
0_W,[[0.31, -1.25],
 [-0.54, 0.45]]
0_b,[0.00, 0.00]
1_W,[0.43, 0.08]
1_b,0.02
需要說明的是,第一次迭代兩個輸入都爲0,所以權重沒有改變。我們剛纔的公式推導實際是第二次迭代後的結果。我們看第二次迭代的時候,即輸入分別是0和1的時候,也就是日誌中的1_W中的第二個數值發生了變化,0.07-->0.08。這和我們的手工計算結果0.0787是吻合的。而對於,也就是日誌中的1_b,從0.00-->0.02和我們計算的結果0.019也是吻合的。由此通過運行程序來驗證我們之前的推導。
最後推導一下的更新過程。


的推導略微複雜些,其中還包括了對Relu函數的求導。但由於的輸入值是0,所以本次迭代的值不發生改變。這個從上面我們日誌的信息中也可以驗證。

通過將中間變量不斷展開的方式來計算損失函數對於每個權重變量偏導數的方式雖然比較直觀,但在實際編碼的時候,我們希望通過張量的形式來落地。我們不妨將這9個變量梯度的計算形式基於鏈式法則都寫出來,這樣可以從中發現一些規律:


爲了方便下面源碼的實現,我們將部分求導的中間結果重新定義:


1.:殘差,可以認爲是在沒有進行非線性變換前各層輸出值對誤差的敏感程度

2.:殘差計算可以重複利用的中間結果。

注意,公式中的用大圓黑點表示的是Hadmard乘積,即對應元素相乘。

我們把求取梯度的結果用張量形式表示:

對於輸出層的權重


對於隱藏層的權重和偏置



在明確了損失函數對於每一個權重和偏置的梯度之後,結合學習率以及迭代前各個變量的值就可以計算出迭代的結果。到此我們完成了對XOR問題一次迭代的手工計算以及計算形式的張量化。在此我們做下小結:神經網絡基於BP算法將誤差由輸出層反向傳播,通過計算損失函數對於權重和偏置的梯度並結合學習率等固定的超參數,在隨機梯度下降算法的基礎上,進行一次權重的更新迭代。這就是BP算法的大致流程。下面就以上XOR的例子,我們來看下Deeplearning4j底層的源碼實現。

首先我們給出一次迭代的算法過程描述:

(1). 根據用戶設置的參數判斷是否進行BP算法。若是進入(2),否則退出
(2) 根據用戶參數選擇優化器(本例中是SGD)以及獲取用戶設置的迭代次數。若尚未完成全部迭代,則進入(3),否則進入(4)
(3). 計算梯度以及損失函數的值
      (a).計算輸出層的輸入張量
      (b).基於BP算法計算輸出層各權重、偏置的梯度、殘差以及後一層的值。
      (c).從倒數第二層開始至倒數第一層,根據前一層計算的的值計算當前層的梯度和殘差以及以及後一層的值
      (d).循環操作步驟(c)直至結束
      (e).計算損失函數的值
(4). 根據(3)中計算的所有梯度值,結合超參數學習率、更新機制等對所有權重和偏置進行更新

在以上描述的計算流程中,部分的邏輯用以下時序圖來表示:


最後,我挑選一些主要的源碼片段來做下說明:

1.MultiLayerNetwork.fit

    public void fit(INDArray features, INDArray labels, INDArray featuresMask, INDArray labelsMask) {
        setInput(features);  // 設置訓練數據
        setLabels(labels);   //設置訓練標註
        if (featuresMask != null || labelsMask != null) {
            this.setLayerMaskArrays(featuresMask, labelsMask);
        }
        update(TaskUtils.buildTask(features, labels));

        if (layerWiseConfigurations.isPretrain()) {
            pretrain(features);
        }

        if (layerWiseConfigurations.isBackprop()) {  //根據用戶設置的參數判斷是否要進行BP反向傳播算法
            if (layerWiseConfigurations.getBackpropType() == BackpropType.TruncatedBPTT) {
                doTruncatedBPTT(features, labels, featuresMask, labelsMask);
            } else {   
                if (solver == null) {   //獲取優化器
                    solver = new Solver.Builder().configure(conf()).listeners(getListeners()).model(this).build();
                }

                solver.optimize();    //模型訓練/優化
            }
        }

        if (featuresMask != null || labelsMask != null) {
            clearLayerMaskArrays();
        }
    }
這一部分代碼是上面算法流程(1)和(2)中的邏輯。其中有一些關於掩碼(maskArray)的判斷,這裏可以先跳過。由於我們設置的優化器是SGD,所以下面會進入StochasticGradientDescent這個類中的optimize方法。顧名思義,就是進行模型的優化了。具體看下面的代碼


2.StochasticGradientDescent.optimize,SGD優化器的優化方法

    public boolean optimize() {
        for (int i = 0; i < conf.getNumIterations(); i++) {

            Pair<Gradient, Double> pair = gradientAndScore();  //計算梯度和損失函數的值
            Gradient gradient = pair.getFirst();

            INDArray params = model.params();
            stepFunction.step(params, gradient.gradient());   //更新參數
            //Note: model.params() is always in-place for MultiLayerNetwork and ComputationGraph, hence no setParams is necessary there
            //However: for pretrain layers, params are NOT a view. Thus a setParams call is necessary
            //But setParams should be a no-op for MLN and CG
            model.setParams(params);  //重置模型參數

            int iterationCount = BaseOptimizer.getIterationCount(model);
            for (IterationListener listener : iterationListeners)
                listener.iterationDone(model, iterationCount);

            checkTerminalConditions(pair.getFirst().gradient(), oldScore, score, i);

            BaseOptimizer.incrementIterationCount(model, 1);
        }
        return true;
    }
這一部分代碼實際上涵蓋了(3)和(4)兩大部分的邏輯。其中,(3)中還有很多的具體的操作,比如計算殘差的值等等。這裏就不一一詳述了,如果有需要的話,可以自行在IDE中進行調試。
最後,我這邊給出經過500輪次訓練過程的可視化信息圖片。Web UI的地址是本地:localhost:9000/train/overview


左上角的第一張圖是Loss Score vs Iteration。也就是說每經過一次迭代後,損失函數的值。可以明顯看出,Loss是振盪下降最後直到收斂。

右上角第一張圖是模型的參數信息。

下面的兩張圖是訓練過程中梯度的相關信息。

補充說明一點:UI頁面需要引入相應的依賴並且JDK版本需要1.8以上:

		<dependency>
        	<groupId>org.deeplearning4j</groupId>
        	<artifactId>deeplearning4j-ui_${scala.binary.version}</artifactId>
        	<version>${dl4j.version}</version>
    	</dependency>
在最後做下小結:我們通過構建一個含有一層隱藏層的全連接神經網絡來解決XOR的問題,推導了基於隨機梯度下降算法的神經網絡中各個參數更新的公式,並結合Deeplearning4j的部分源碼進行分析以及建模驗證。需要說明的是,選擇XOR問題,一方面是因爲該問題的訓練數據少,容易解釋BP算法,同時,XOR問題也是經典的需要通過非線性特徵變換纔可以解決的問題。換句話說,如果將隱藏層中的Relu函數換成Identity,那麼模型將不會收斂。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章