DL4J原始碼閱讀(四):梯度計算
computeGradientAndScore方法呼叫backprop()做梯度計算和誤差反傳。
backprop()呼叫calcBackpropGradients()方法。calcBackpropGradients()方法再呼叫initGradientsView()方法。在initGradientsView()方法裡,先初始化一個數組flattenedGradients = Nd4j.zeros(new int[] {1, paramLength}, 'f')。這個陣列長度是網路所有引數個數,包括每層的權重引數和偏移引數。本例中是102。然後呼叫layers[i].setBackpropGradientsViewArray(thisLayerGradView)
calcBackpropGradients()方法中currPair = outputLayer.backpropGradient(null)計算輸出層的梯度。本例中會呼叫BaseOutputLayer中的backpropGradient()
computeGradient()方法中呼叫output=activationFn.getActivation(preOutput.dup(), true)計算輸出。本例中輸出層的啟用函式是ActivationSoftmax,其計算公式是類的註釋f_i(x) = exp(x_i - shift) / sum_j exp(x_j - shift) where shift = max_i(x_i)。我寫了一個實現驗證了一下:
public static void main(String[] args) throws Exception {
double[] x = { 0.01, 0.02 };
double shift = Math.max(x[0], x[1]);
double sum = Math.exp(x[0] - shift) + Math.exp(x[1] - shift);
System.out.println(Math.exp(x[0] - shift) / sum);
System.out.println(Math.exp(x[1] - shift) / sum);
}
當x = { 0.01, 0.02 },列印的是0.497500020833125、0.5024999791668749。當x = { -0.08, 0.33},列印的是0.3989121211516303、0.6010878788483698。和DL4J計算的一致(四捨五入之後)。公式中有自然數e的指數運算,這是為了求導方便。資料經過ActivationSoftmax處理後,輸出減去標籤,計算誤差grad = output.subi(labels)。
getGradientsAndDelta()方法中得到誤差後呼叫Nd4j.gemm(input, delta, weightGradView, true, false, 1.0, 0.0),計算輸出層的輸入矩陣的轉置和誤差矩陣的乘積,放入weightGradView變數中。將這個作為輸出層的梯度,矩陣計算公式如下:
delta.sum(biasGradView, 0)語句計算誤差矩陣的和作為偏移的梯度。backpropGradient()方法得到Pair<Gradient, INDArray> pair的值後,用輸出層權重矩陣乘以delta矩陣的轉置,得到一個叫epsilonNext的矩陣。這個epsilonNext矩陣是用來計算上一層誤差的。
現在流程回到calcBackpropGradients方法中。currPair中First儲存了輸出層權重和偏移的梯度,Second儲存了epsilonNext。下面的迴圈將輸出層的權重和偏移梯度以Triple 型別儲存到gradientList變數中。Triple是個三元組,權重的梯度形如:Triple(first=1_W, second=[[3.90, 0.03...,偏移的梯度形如:Triple(first=1_b, second=[4.03, -4.03], third=null)。
for (int j = layerFrom; j >= 0; j--)語句開始處理隱藏層的梯度。這裡需要注意一下,這個迴圈是個倒序。其用意是在方法返回的結果中放入的currPair.getSecond()是第0層的epsilonNext。currPair = currLayer.backpropGradient(currPair.getSecond())語句計算隱藏層梯度。可以看到輸入引數是currPair.getSecond(),也就是輸出層計算梯度時返回的epsilonNext。隱藏層的啟用函式ActivationReLU的backprop()方法中也看到,先求輸出的導數矩陣,然後導數矩陣乘以epsilonNext矩陣,求出本層的誤差矩陣dLdz.muli(epsilon)。這個裡要注意的是這個矩陣相乘,不是一般的矩陣相乘,要求第一個矩陣的列數與第二個矩陣的行數相等。這個是Hadamard product(哈達瑪積),要求相乘的兩個矩陣的行和列相等,本例是都是50行20列。
backpropGradient呼叫的是BaseLayer裡的backpropGradient()方法。這個方法呼叫了INDArray z = preOutput(true)。隱藏層的preOutput方法在前面的訊號前傳中就呼叫過了,這裡重複呼叫了一層。其實矩陣計算還是比較耗資源的。preOutput方法的計算結果應該儲存起來比較好。這樣,這裡就不用再算一次了。backpropGradient中其它的步驟和輸出層的差不多。
流程回到calcBackpropGradients方法中的處理也和輸出層的差不多。有一個地方:LinkedList<Triple<String, INDArray, Character>> tempList = new LinkedList<>()。這個語句新建了一個tempList變數儲存Triple,然後再將每個Triple放到gradientList中。我沒看出來tempList 的必要性。直接將隱藏層的Triple放到gradientList中不可以嗎?
calcBackpropGradients方法最後將gradientList中值儲存到gradient變數中。這裡我又看不懂了,不要gradientList這個中間變數,直接將梯度資訊儲存到gradient裡不可以嗎?方法返回的結果是一個Pair,First是全網各層的梯度,Second是第0層的epsilonNext。