《神經網路的梯度推導與程式碼驗證》之FNN(DNN)前向和反向傳播過程的程式碼驗證

SumwaiLiu發表於2020-09-02

《神經網路的梯度推導與程式碼驗證》之FNN(DNN)的前向傳播和反向梯度推導中,我們學習了FNN(DNN)的前向傳播和反向梯度求導,但知識仍停留在紙面。本篇章將基於深度學習框架tensorflow驗證我們所得結論的準確性,以便將抽象的數學符號和實際資料結合起來,將知識固化。更多相關內容請見《神經網路的梯度推導與程式碼驗證》系列介紹

 


 

需要用到的庫有tensorflow和numpy,其中tensorflow其實版本>=2.0.0就行

import tensorflow as tf
import numpy as np

 

然後是定義下面兩個要用到的函式,一個是計算mse,另外一個是計算sigmoid的導數:

# mse
def get_mse(y_pred, y_true):
    return 0.5 * tf.reduce_sum((y_pred - y_true)**2)


# sigmoid的導數
def d_sigmoid(x):
    """
    sigmoid(x)的導數 = sigmoid(x) * (1-sigmoid(x))
    :param x:
    :return: sigmoid(x)的導數
    """
    return tf.math.sigmoid(x) * (1 - tf.math.sigmoid(x))

 

接著是隨便產生一條樣本資料:

x = np.array([[1, 2]]).astype(np.float32)
y_true = np.array([[0.3, 0.5, 0.2]]).astype(np.float32)

 

x的是2維的,輸出y是3維的

x.shape
Out[5]: (1, 2)
y_true.shape
Out[6]: (1, 3)

 

有了一條樣本之後,我們開始寫前向傳播的程式碼:

 1 with tf.GradientTape(persistent=True) as t:
 2     # -----hidden l1-------------
 3     # DNN layer1
 4     l1 = tf.keras.layers.Dense(4)
 5 
 6     # 輸入經過DNN layer1得到輸出z_l1
 7     z_l1 = l1(x)
 8     # 跟蹤變數z_l1,用於隨後計算其梯度
 9     t.watch([z_l1])
10     # DNN layer1的輸出再經過啟用函式sigmoid
11     a_l1 = tf.math.sigmoid(z_l1)
12     # 跟蹤變數a_l1 ,用於隨後計算其梯度 
13     t.watch([a_l1])
14     # ------hidden l2------------
15     l2 = tf.keras.layers.Dense(3)
16 
17     z_l2 = l2(a_l1)
18     t.watch([z_l2])
19     a_l2 = tf.math.sigmoid(z_l2)
20     t.watch([a_l2])
21     # -------計算loss-----------
22     loss = get_mse(a_l2, y_true)

上面是一個兩層的FNN(DNN)網路,啟用函式都是sigmoid

這裡 tf.GradientTape(persistent=True) ,t.watch()是用於後面計算變數的導數用的,不太熟悉的可參考tensorflow官方給出的關於這部分的教程(自動微分)

這裡為方便起見我就直接用tf.keras.layers.Dense()來建立DNN層了,tensorflow官方的教程也推薦用這種方法快速定義layer。

如果要看某一層內部的weights和bias也比較容易

l1.kernel
Out[7]: 
<tf.Variable 'dense/kernel:0' shape=(2, 4) dtype=float32, numpy=
array([[-0.96988726, -0.84827805,  0.312042  ,  0.8871379 ],
       [ 0.6567688 , -0.29099226, -0.80029106, -0.15143871]],
      dtype=float32)>
l1.bias
Out[8]: <tf.Variable 'dense/bias:0' shape=(4,) dtype=float32, numpy=array([0., 0., 0., 0.], dtype=float32)>

----------前向傳播的驗證----------

下面來驗證上面程式碼第7+11行程式碼是否符合DNN的前傳規則:

tf.math.sigmoid(tf.matmul(x, l1.kernel) + l1.bias)
Out[14]: <tf.Tensor: shape=(1, 4), dtype=float32, numpy=array([[0.585077  , 0.19305778, 0.2161    , 0.64204717]], dtype=float32)>
a_l1
Out[15]: <tf.Tensor: shape=(1, 4), dtype=float32, numpy=array([[0.585077  , 0.19305778, 0.2161    , 0.64204717]], dtype=float32)>

 

 看來tf.keras.layers.Dense確實是實現了下面的計算公式:

$\left\lbrack \begin{array}{l} \begin{array}{l} a_{1}^{2} \\ a_{2}^{2} \\ \end{array} \\ a_{3}^{2} \\ a_{4}^{2} \\ \end{array} \right\rbrack = \sigma\left( {\left\lbrack \begin{array}{lll} \begin{array}{l} w_{11}^{2} \\ w_{21}^{2} \\ \end{array} & \begin{array}{l} w_{12}^{2} \\ w_{22}^{2} \\ \end{array} & \begin{array}{l} w_{13}^{2} \\ w_{23}^{2} \\ \end{array} \\ w_{31}^{2} & w_{32}^{2} & w_{33}^{2} \\ w_{41}^{2} & w_{42}^{2} & w_{43}^{2} \\ \end{array} \right\rbrack\left\lbrack \begin{array}{l} x_{1} \\ x_{2} \\ x_{3} \\ \end{array} \right\rbrack + \left\lbrack \begin{array}{l} \begin{array}{l} b_{1}^{2} \\ b_{2}^{2} \\ \end{array} \\ b_{3}^{2} \\ b_{4}^{2} \\ \end{array} \right\rbrack} \right)$

這裡l1層啟用函式預設是linear,sigmoid啟用函式被我單獨拿了出來(見前傳部分的程式碼第11行),方便計算梯度的時候好做分解。

----------反向梯度計算的驗證----------

接下來就是驗證反向梯度求導公式的時候了:

 1 # 注意的是,在tensorflow裡,W變數矩陣和數學推導的是互為轉置的關係,所以在驗證的時候,要注意轉置關係的處理
 2 # ------dl_da2------ sigmoid(x)的導數 = sigmoid(x) * (1-sigmoid(x))
 3 dl_da2 = t.gradient(loss, a_l2)
 4 my_dl_da2 = (a_l2 - y_true)
 5 # ------dl_dz2---------
 6 dl_dz2 = t.gradient(loss, z_l2)
 7 my_dl_dz2 = my_dl_da2 * d_sigmoid(z_l2)
 8 # -------dl_dW2--------
 9 dl_dW2 = t.gradient(loss, l2.kernel)
10 my_dl_W2 = np.matmul(a_l1.numpy().transpose(), my_dl_dz2)
11 # -------dl_db2--------
12 dl_db2 = t.gradient(loss, l2.bias)
13 my_dl_db2 = my_dl_dz2
14 # -------dl_dz1---------
15 dl_dz1 = t.gradient(loss, z_l1)
16 my_dl_dz1 = np.matmul(my_dl_dz2, l2.weights[0].numpy().transpose()) * d_sigmoid(z_l1)
17 # -------dl_dW1---------
18 dl_dW1 = t.gradient(loss, l1.kernel)
19 my_dl_dW1 = np.matmul(x.transpose(), my_dl_dz1)
20 # -------dl_db1----------
21 dl_db1 = t.gradient(loss, l1.bias)
22 my_dl_db1 = my_dl_dz1

 

上面反向梯度計算的物件的順序跟前先傳播的順序是正好相反的,因為這樣方便進行梯度計算的時候,靠前的層的引數的梯度能夠用得到靠後的層的梯度計算結果而不必從頭開始計算,這也是反向梯度傳播名字的由來,這點在上面程式碼中也能夠體現出來。

注意:在tensorflow裡,W變數矩陣和數學推導的是互為轉置的關係,所以在驗證的時候,要注意轉置關係的處理。舉個例子,l1.kernel的shape是(2, 4),即(input_dim, output_dim)這樣的格式,說明在tensorflow是按照$\boldsymbol{o}\boldsymbol{u}\boldsymbol{t}\boldsymbol{p}\boldsymbol{u}\boldsymbol{t} = \boldsymbol{x}^{\boldsymbol{T}}\boldsymbol{W}\boldsymbol{~} + \boldsymbol{~}\boldsymbol{b}$這種方式做前傳計算的,即輸入$\boldsymbol{x}^{\boldsymbol{T}}$是一個行向量而非列向量

而我們在數學推導上,習慣寫成$\boldsymbol{o}\boldsymbol{u}\boldsymbol{t}\boldsymbol{p}\boldsymbol{u}\boldsymbol{t} = \boldsymbol{W}^{\boldsymbol{T}}\boldsymbol{x}\boldsymbol{~} + \boldsymbol{~}\boldsymbol{b}$,其中$\boldsymbol{x}$預設是列向量而非行向量。於是在做驗證的時候,需要對一些變數進行一下轉置操作,即上面的 .transpose()操作,但大體並不影響公式的驗證。

 

回到上面的程式碼部分。dl_da2 = t.gradient(loss, a_l2)表示用tensorflow微分工具求得的$\frac{\partial l}{\partial a2}$;而帶my_字首的則是根據《神經網路的梯度推導與程式碼驗證》之FNN(DNN)的前向傳播和反向梯度推導中得到的結論手動計算出來的結果。我們依次對比一下是否有區別。

 

關於$\frac{\partial l}{\partial a2}$,根據我們得到的公式,它滿足:

$\frac{\partial l}{\partial\boldsymbol{a}^{\boldsymbol{L}}} = \boldsymbol{a}^{\boldsymbol{L}} - \boldsymbol{y}$

程式碼驗證的結果是:

t.gradient(loss, a_l2)
Out[17]: <tf.Tensor: shape=(1, 3), dtype=float32, numpy=array([[ 0.1497804 , -0.05124322,  0.23775901]], dtype=float32)>
a_l2 - y_true
Out[18]: <tf.Tensor: shape=(1, 3), dtype=float32, numpy=array([[ 0.1497804 , -0.05124322,  0.23775901]], dtype=float32)>

 

沒有問題,下一個是$\frac{\partial l}{\partial z2}$,根據公式,它滿足:

$d\boldsymbol{a}^{\boldsymbol{L}} = d\sigma\left( \boldsymbol{z}^{L} \right) = \sigma^{'}\left( \boldsymbol{z}^{L} \right) \odot d\boldsymbol{z}^{L} = diag\left( {\sigma^{'}\left( \boldsymbol{z}^{L} \right)} \right)d\boldsymbol{z}^{L}$

程式碼驗證的結果是:

t.gradient(loss, z_l2)
Out[21]: <tf.Tensor: shape=(1, 3), dtype=float32, numpy=array([[ 0.03706735, -0.01267625,  0.05851868]], dtype=float32)>
my_dl_da2 * d_sigmoid(z_l2)
Out[22]: <tf.Tensor: shape=(1, 3), dtype=float32, numpy=array([[ 0.03706735, -0.01267625,  0.05851869]], dtype=float32)>

 

也沒有問題,其中d_sigmoid()函式是前面定義好的,用來求sigmoid導數的。至於上面0.05851868 v.s. 0.05851869的問題,我覺得單純只是兩種程式碼實現過程中呼叫的底層方式不同導致的不同而已。

 

接下來是$\frac{\partial l}{\partial W2}$,根據公式,它滿足:

$\frac{\partial l}{\partial\boldsymbol{W}^{\boldsymbol{L}}} = \frac{\partial l}{\partial\boldsymbol{z}^{\boldsymbol{L}}}\left( \boldsymbol{a}^{\boldsymbol{L} - 1} \right)^{T}$

程式碼驗證的結果是:

t.gradient(loss, l2.kernel)
Out[23]: 
<tf.Tensor: shape=(4, 3), dtype=float32, numpy=
array([[ 0.02168725, -0.00741658,  0.03423793],
       [ 0.00715614, -0.00244725,  0.01129749],
       [ 0.00801025, -0.00273934,  0.01264589],
       [ 0.02379899, -0.00813875,  0.03757175]], dtype=float32)>
np.matmul(a_l1.numpy().transpose(), my_dl_dz2)
Out[24]: 
array([[ 0.02168725, -0.00741658,  0.03423794],
       [ 0.00715614, -0.00244725,  0.01129749],
       [ 0.00801025, -0.00273934,  0.01264589],
       [ 0.02379899, -0.00813875,  0.03757175]], dtype=float32)

 

也沒有問題。剩下的大家可自行對照著《神經網路的梯度推導與程式碼驗證》之FNN(DNN)的前向傳播和反向梯度推導中的得到的公式自行驗證。

 


 

(歡迎轉載,轉載請註明出處。歡迎留言或溝通交流: lxwalyw@gmail.com)

相關文章