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

SumwaiLiu發表於2020-09-04

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

 

提醒:

  • 後續會反覆出現$\boldsymbol{\delta}^{l}$這個(類)符號,它的定義為$\boldsymbol{\delta}^{l} = \frac{\partial l}{\partial\boldsymbol{z}^{\boldsymbol{l}}}$,即loss $l$對$\boldsymbol{z}^{\boldsymbol{l}}$的導數
  • 其中$\boldsymbol{z}^{\boldsymbol{l}}$表示第$l$層(DNN,CNN,RNN或其他例如max pooling層等)未經過啟用函式的輸出。
  • $\boldsymbol{a}^{\boldsymbol{l}}$則表示$\boldsymbol{z}^{\boldsymbol{l}}$經過啟用函式後的輸出。

這些符號會貫穿整個系列,還請留意。


 

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

import tensorflow as tf
import numpy as np
np.random.seed(0)

 

然後是定義交叉熵損失函式:

def get_crossentropy(y_pred, y_true):
    return -tf.reduce_sum(y_true * tf.math.log(y_pred))

 

接下來正式進入程式碼主體:

 1 with tf.GradientTape(persistent=True) as t:
 2     # -------input-----------
 3     x = tf.constant(np.random.randn(1, 9, 9, 1).astype(np.float32))
 4     y_true = np.array([[0.3, 0.5, 0.2]]).astype(np.float32)
 5     t.watch(x)
 6     # -----conv2d l1----------
 7     l1 = tf.keras.layers.Conv2D(filters=1, kernel_size=(3, 3), strides=2)
 8 
 9     z_l1 = l1(x)
10     t.watch([z_l1])
11     a_l1 = tf.nn.relu(z_l1)
12     t.watch([a_l1])
13     # -----max pooling--------
14     l2 = tf.keras.layers.MaxPool2D(pool_size=(2, 2))
15 
16     z_l2 = l2(a_l1)
17     t.watch([z_l2])
18     a_l2 = tf.keras.layers.Flatten()(z_l2)
19     t.watch([a_l2])
20     # ---------FNN------------
21     l3 = tf.keras.layers.Dense(3)
22 
23     z_l3 = l3(a_l2)
24     t.watch([z_l3])
25     a_l3 = tf.math.softmax(z_l3)
26     t.watch([a_l3])
27     # ---------loss----------
28     loss = get_crossentropy(y_pred=a_l3, y_true=y_true)

上面這段程式碼實現的是輸入--卷積層--池化層--全連線層--輸出的前向過程。因為是我們們的目的是做驗證,所以沒必要把網路加深,讓這個麻雀五臟俱全即可。

input部分,輸入x是形狀為(1, 9, 9, 1)的張量,可以理解為一張單通道的9*9的圖。標籤y_true是3維的概率向量。在上面的程式碼中我們只考慮一條(x,y_true)樣本。

 

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

我們先驗證下第7+9行程式碼是否在對x做stride=2的(預設valid模式)卷積操作。

(對卷積操作不太熟悉的可參考:https://www.cnblogs.com/itmorn/p/11179448.html

先瞧一瞧這一層的卷積核和偏置:

np.squeeze(l1.kernel.numpy())
Out[4]: 
array([[-0.3447126 , -0.23770776, -0.20545131],
       [ 0.40415084, -0.56749415,  0.13746339],
       [-0.5106965 , -0.36734173, -0.18053415]], dtype=float32)
l1.bias.numpy()
Out[8]: array([0.], dtype=float32)

 

然後再看看輸入x和輸出z_l1:

np.squeeze(x)
Out[6]: 
array([[ 1.7640524 ,  0.4001572 ,  0.978738  ,  2.2408931 ,  1.867558  ,
        -0.9772779 ,  0.95008844, -0.1513572 , -0.10321885],
       [ 0.41059852,  0.14404356,  1.4542735 ,  0.7610377 ,  0.12167501,
         0.44386324,  0.33367434,  1.4940791 , -0.20515826],
       [ 0.3130677 , -0.85409576, -2.5529897 ,  0.6536186 ,  0.8644362 ,
        -0.742165  ,  2.2697546 , -1.4543657 ,  0.04575852],
       [-0.18718386,  1.5327792 ,  1.4693588 ,  0.15494743,  0.37816253,
        -0.88778573, -1.9807965 , -0.34791216,  0.15634897],
       [ 1.2302907 ,  1.2023798 , -0.3873268 , -0.30230275, -1.048553  ,
        -1.420018  , -1.7062702 ,  1.9507754 , -0.5096522 ],
       [-0.4380743 , -1.2527953 ,  0.7774904 , -1.6138978 , -0.21274029,
        -0.89546657,  0.3869025 , -0.51080513, -1.1806322 ],
       [-0.02818223,  0.42833188,  0.06651722,  0.3024719 , -0.6343221 ,
        -0.36274117, -0.67246044, -0.35955316, -0.8131463 ],
       [-1.7262826 ,  0.17742614, -0.40178093, -1.6301984 ,  0.46278226,
        -0.9072984 ,  0.0519454 ,  0.7290906 ,  0.12898292],
       [ 1.1394007 , -1.2348258 ,  0.40234163, -0.6848101 , -0.87079716,
        -0.5788497 , -0.31155252,  0.05616534, -1.1651498 ]],
      dtype=float32)

 

np.squeeze(z_l1)
Out[7]: 
array([[-0.00542112, -0.17352474, -1.3421125 , -1.6447177 ],
       [-1.1239526 ,  1.6031268 ,  1.1616374 , -0.78091574],
       [-0.14451274,  1.5910958 ,  2.1035302 ,  1.1354219 ],
       [-1.1602874 ,  1.0651501 ,  1.8656987 ,  0.4581319 ]],
      dtype=float32)

 

為提高效率,我們就驗證一丟丟就好:

np.sum(np.squeeze(x)[0:3, 0:3] * np.squeeze(l1.kernel.numpy()))
Out[8]: -0.0054210722
np.sum(np.squeeze(x)[2:5, 0:3] * np.squeeze(l1.kernel.numpy()))
Out[9]: -1.1239524
np.sum(np.squeeze(x)[0:3, 2:5] * np.squeeze(l1.kernel.numpy()))
Out[10]: -0.17352472

對比紅色標記的部分,發現我們定義的卷積層確實有乖乖做stride=2的valid卷積操作。

 

接下來再看看pool_size=(2, 2)池化層的操作:

np.squeeze(a_l1)
Out[11]: 
array([[0.       , 0.       , 0.       , 0.       ],
       [0.       , 1.6031268, 1.1616374, 0.       ],
       [0.       , 1.5910958, 2.1035302, 1.1354219],
       [0.       , 1.0651501, 1.8656987, 0.4581319]], dtype=float32)
np.squeeze(z_l2)
Out[12]: 
array([[1.6031268, 1.1616374],
       [1.5910958, 2.1035302]], dtype=float32)

也沒有什麼問題。

接下來的FNN的前向傳播驗證可參考FNN(DNN)前向和反向傳播過程的程式碼驗證,這裡就略過了。

 

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

老樣子,我們從尾向頭開始驗證。

 

先看$\frac{\partial l}{\partial\boldsymbol{z}\_ l3}$:

# -----dl_dz3------
dl_dz3 = t.gradient(loss, z_l3)
my_dl_dz3 = a_l3 - y_true
dl_dz3
Out[13]: <tf.Tensor: shape=(1, 3), dtype=float32, numpy=array([[ 0.20361423, -0.4315467 ,  0.22793245]], dtype=float32)>
my_dl_dz3
Out[14]: <tf.Tensor: shape=(1, 3), dtype=float32, numpy=array([[ 0.20361423, -0.4315467 ,  0.22793244]], dtype=float32)>

上面dl_dz3 = t.gradient(loss, z_l3)表示用tensorflow微分工具求得的$\frac{\partial l}{\partial\boldsymbol{z}\_ l3}$;而帶my_字首的則是根據先前推導得到的結論手動計算出來的結果。後續的符號同樣沿用這樣的命名規則。

$\frac{\partial l}{\partial\boldsymbol{z}\_ l3}$應當滿足:

$\frac{\partial l}{\partial\boldsymbol{z}\_ l3} = \boldsymbol{a}\_ l3 - \boldsymbol{y}_{true}$

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

從結果看來並沒有什麼問題。

 

我們直接跳過FNN層的反向梯度驗證(可參考FNN的程式碼驗證),來到max pooling層:

(跳過FNN的反向梯度驗證意味著此時我們是已經能夠計算得到$\frac{\partial l}{\partial\boldsymbol{z}\_ l2}$)

 

於是我們直接驗證$\frac{\partial l}{\partial\boldsymbol{z}\_ l1}$

下面這是tensorflow自動微分給出的$\frac{\partial l}{\partial\boldsymbol{z}\_ l1}$結果

inverse_mp = np.squeeze(t.gradient(loss, z_l1))

inverse_mp
Out[15]: 
array([[ 0.        ,  0.        ,  0.        ,  0.        ],
       [ 0.        ,  0.08187968, -0.07518297,  0.        ],
       [ 0.        , -0.2259186 ,  0.5417712 ,  0.        ],
       [ 0.        ,  0.        ,  0.        ,  0.        ]],
      dtype=float32)

 

接下來我們手動實現下CNN的前向傳播和反向梯度推導中關於max pooling層的結論:

$\boldsymbol{\delta}_{k}^{l - 1} = upsample\left( \boldsymbol{\delta}_{k}^{l} \right) \odot \sigma^{'}\left( \boldsymbol{z}_{k}^{l - 1} \right)$, 其中$\boldsymbol{\delta}^{l} = \frac{\partial l}{\partial\boldsymbol{z}^{\boldsymbol{l}}}$

 

我們先看看卷積層出來的結果z_l1,因為z_l1是個高維張量不太好直接觀察,所以用squeeze去掉了一些多餘的axis,於是batch_size和channel這兩個為1的維度被我去掉,但並不影響驗證。

flat_z_l1 = np.squeeze(z_l1)

flat_z_l1 
Out[16]: 
array([[-0.00542112, -0.17352474, -1.3421125 , -1.6447177 ],
       [-1.1239526 ,  1.6031268 ,  1.1616374 , -0.78091574],
       [-0.14451274,  1.5910958 ,  2.1035302 ,  1.1354219 ],
       [-1.1602874 ,  1.0651501 ,  1.8656987 ,  0.4581319 ]],
      dtype=float32)

 

# z_l1經過啟用函式relu之後變成了下面的a_l1
flat_a_l1 = np.squeeze(a_l1)

flat_a_l1
Out[17]: 
array([[0.       , 0.       , 0.       , 0.       ],
       [0.       , 1.6031268, 1.1616374, 0.       ],
       [0.       , 1.5910958, 2.1035302, 1.1354219],
       [0.       , 1.0651501, 1.8656987, 0.4581319]], dtype=float32)

 

a_l1池化後得到下面的結果,我們要記住池化前後的max value元素的位置以用於後面up sampling,即逆向subsampling。

# a_l1池化後得到下面的結果,我們要記住池化前後的max value元素的位置以用於後面up sampling
flat_z_l2 = np.squeeze(z_l2)

flat_z_l2
Out[18]: 
array([[1.6031268, 1.1616374],
       [1.5910958, 2.1035302]], dtype=float32)

 

因為前面假設我們已知$\frac{\partial l}{\partial\boldsymbol{z}\_ l2}$,所以這裡直接用tensorflow的自動微分工具給出結果:

# 下面是dl_dz2的結果
dl_dz2 = np.squeeze(t.gradient(loss, z_l2))

dl_dz2
Out[23]: 
array([[ 0.08187968, -0.07518297],
       [-0.2259186 ,  0.5417712 ]], dtype=float32)

 

根據$\frac{\partial l}{\partial\boldsymbol{z}\_ l1} = upsample\left( \frac{\partial l}{\partial\boldsymbol{z}\_ l2} \right) \odot \sigma^{'}\left( {\boldsymbol{z}\_ l1} \right)$我們簡單腦補一下下面這個東西就好,沒有必要寫程式碼實現這個upsampling,因為結果和邏輯都非常pure and simple(upsampling時,四個元素的位置要參考max pooling時四個max value的位置記錄)。

按照公式給出的規則我們腦補出來的結果確實跟下面這個自動微分的結果是一致的。

dl_dz1 = t.gradient(loss, z_l1)

np.squeeze(dl_dz1)
Out[26]: 
array([[ 0.        ,  0.        ,  0.        ,  0.        ],
       [ 0.        ,  0.08187968, -0.07518297,  0.        ],
       [ 0.        , -0.2259186 ,  0.5417712 ,  0.        ],
       [ 0.        ,  0.        ,  0.        ,  0.        ]],
      dtype=float32)

 


 

然後是略為麻煩的,驗證誤差反向經過卷積層時,$\boldsymbol{\delta}$的遞推公式:

$\boldsymbol{\delta}^{l - 1} = \left( \frac{\partial\boldsymbol{z}^{l}}{\partial\boldsymbol{z}^{l - 1}} \right)^{T}\boldsymbol{\delta}^{l} = \boldsymbol{\delta}^{l}*rot180\left( \boldsymbol{W}^{l} \right) \odot \sigma^{'}\left( \boldsymbol{z}^{l - 1} \right)$, 其中$\boldsymbol{\delta}^{l} = \frac{\partial l}{\partial\boldsymbol{z}^{\boldsymbol{l}}}$。

在我們這個“麻雀”小例子中,我們只有一層CNN層,也就意味著,這裡loss對z_l1 = l1(x) 的導數就是上述公式中的$\boldsymbol{\delta}^{l}$,而loss對x的導數則對映著公式中的$\boldsymbol{\delta}^{l-1}$,而且因為x位於輸入層,所以是沒有啟用函式的,或者說啟用函式是線性函式,即$\sigma\left( \boldsymbol{z}^{l - 1} \right)=\boldsymbol{z}^{l - 1}$

於是我們最終要驗證的東西簡化成了:

$\frac{\partial l}{\partial\boldsymbol{x}} = \frac{\partial l}{\partial\boldsymbol{z}\_ l1}*rot180\left( \boldsymbol{W}^{1} \right)$

 

雖說一般情況下,我們不會去求loss對輸入的導數就是了,因為通常我們訓練的物件是神經網路而非樣本資料。但在生成問題下,例如GAN,這種玩法還是很常見的。

 

回到正題,我們現在已經明確要對比什麼了。接下來開始碼程式碼吧。。

現已知loss對z_l1的導數

# 已知dl_dz1
dl_dz1 = t.gradient(loss, z_l1)

 

我們要通過它來求loss對輸入x的導數,也就是下面這個東西,這是tensorflow給出的答案,接下來我們要通過遞推公式得到一個跟這一樣的答案。

# -----dl_dx--------
dl_dx = np.squeeze(t.gradient(loss, x))

np.squeeze(dl_dx)
Out[5]: 
array([[ 0.        ,  0.        ,  0.        ,  0.        , -0.02591492,
         0.0461329 , -0.02298165,  0.        ,  0.        ],
       [ 0.        ,  0.        ,  0.        ,  0.        ,  0.00783426,
        -0.0234191 ,  0.00530995,  0.        ,  0.        ],
       [ 0.        ,  0.        ,  0.        ,  0.        ,  0.05378128,
        -0.02789585,  0.07432017,  0.        ,  0.        ],
       [ 0.        ,  0.        ,  0.        ,  0.        ,  0.        ,
         0.        ,  0.        ,  0.        ,  0.        ],
       [ 0.        ,  0.        ,  0.        ,  0.        ,  0.        ,
         0.        ,  0.02716039, -0.04835004,  0.02408614],
       [ 0.        ,  0.        ,  0.        ,  0.        ,  0.        ,
         0.        , -0.00821077,  0.02454463, -0.00556515],
       [-0.02953471,  0.05257672, -0.02619171,  0.        ,  0.        ,
         0.        , -0.05636601,  0.02923652, -0.07789201],
       [ 0.00892854, -0.02669027,  0.00605165,  0.        ,  0.        ,
         0.        ,  0.        ,  0.        ,  0.        ],
       [ 0.06129343, -0.03179233,  0.08470119,  0.        ,  0.        ,
         0.        ,  0.        ,  0.        ,  0.        ]],
      dtype=float32)

 

我們注意到 l1 = tf.keras.layers.Conv2D(filters=1, kernel_size=(3, 3), strides=2),這是一個stride>1的情況。讓我們回到CNN的前向傳播和反向梯度推導的3.2.2中的那個stride>1的例子。當stride>1時,$\frac{\partial l}{\partial\boldsymbol{z}\_\boldsymbol{l}1}$矩陣的元素是要散開成一個較大的矩陣(空的位置補0,類似upsampling的感覺)然後跟旋轉了180度的卷積核做valid模式的卷積就能完成跨越CNN層的梯度遞推。

下面是關鍵點回顧:

 

  • 前向計算有:

$conv2D\left( {\boldsymbol{a}^{l - 1},\boldsymbol{W}^{l},^{'}valid^{'},~stride = 2} \right) = \left\lbrack \begin{array}{lll} z_{11} & 0 & z_{12} \\ 0 & 0 & 0 \\ z_{21} & 0 & z_{22} \\ \end{array} \right\rbrack\overset{down~sampling}{\rightarrow}\boldsymbol{z}^{l} = \left\lbrack \begin{array}{ll} z_{11} & z_{12} \\ z_{21} & z_{22} \\ \end{array} \right\rbrack$

  • 計算反向梯度時有:

loss $l$對矩陣$\boldsymbol{z}^{l}$的導數,即$\boldsymbol{\delta}^{l}$,它經過上取樣後再跟$rot180\left( \boldsymbol{W}^{l} \right)$進行stride=1的full模式的卷積運算的結果就是上面例子2的最終結果,即:

$\boldsymbol{\delta}^{l} = \left\lbrack \begin{array}{ll} \delta_{11} & \delta_{12} \\ \delta_{21} & \delta_{22} \\ \end{array} \right\rbrack\overset{up~sampling}{\rightarrow}\left\lbrack \begin{array}{lll} \delta_{11} & 0 & \delta_{12} \\ 0 & 0 & 0 \\ \delta_{21} & 0 & \delta_{22} \\ \end{array} \right\rbrack$

$\frac{\partial l}{\partial\boldsymbol{a}^{l - 1}} = conv2D\left( {\left\lbrack \begin{array}{lll} \delta_{11} & 0 & \delta_{12} \\ 0 & 0 & 0 \\ \delta_{21} & 0 & \delta_{22} \\ \end{array} \right\rbrack,rot180\left( \boldsymbol{W}^{l} \right),'full'} \right)$

但是,就算是照著上面這個模式做,自己寫起程式碼來也是有點麻煩的,而tensorflow恰好提供了用於完成上面這種操作的layer,Conv2DTranspose。

inverse_conv2d = tf.keras.layers.Conv2DTranspose(filters=1, kernel_size=(3, 3), strides=2)
inverse_conv2d.build(dl_dz1.shape)

上面兩行程式碼完成了設定和初始化Conv2DTranspose層,它做的事情正好就是:先對輸入做擴張比率為strides的upsampling,然後將它和旋轉了180度的自己的卷積核做full模式的卷積操作。

 

但目前Conv2DTranspose還不能用,因為前傳和反傳期間卷積核應當只是被旋轉,其元素的值是不變的,因此初始化的Conv2DTranspose層的卷積核和偏置還需要被賦值成Conv2D時所用的那些:

inverse_conv2d.kernel = l1.kernel
inverse_conv2d.bias = l1.bias

 

然後就是見證奇蹟的時刻了:

my_dl_dx = np.squeeze(inverse_conv2d(dl_dz1))

my_dl_dx
Out[6]: 
array([[ 0.        ,  0.        ,  0.        ,  0.        , -0.02591492,
         0.0461329 , -0.02298165,  0.        ,  0.        ],
       [ 0.        ,  0.        ,  0.        ,  0.        ,  0.00783426,
        -0.0234191 ,  0.00530995,  0.        ,  0.        ],
       [ 0.        ,  0.        ,  0.        ,  0.        ,  0.05378128,
        -0.02789585,  0.07432017,  0.        ,  0.        ],
       [ 0.        ,  0.        ,  0.        ,  0.        ,  0.        ,
         0.        ,  0.        ,  0.        ,  0.        ],
       [ 0.        ,  0.        ,  0.        ,  0.        ,  0.        ,
         0.        ,  0.02716039, -0.04835004,  0.02408614],
       [ 0.        ,  0.        ,  0.        ,  0.        ,  0.        ,
         0.        , -0.00821077,  0.02454463, -0.00556515],
       [-0.02953471,  0.05257672, -0.02619171,  0.        ,  0.        ,
         0.        , -0.05636601,  0.02923652, -0.07789201],
       [ 0.00892854, -0.02669027,  0.00605165,  0.        ,  0.        ,
         0.        ,  0.        ,  0.        ,  0.        ],
       [ 0.06129343, -0.03179233,  0.08470119,  0.        ,  0.        ,
         0.        ,  0.        ,  0.        ,  0.        ]],
      dtype=float32)

跟前面自動微分工具給出的結果一致,說明CNN的前向傳播和反向梯度推導的結論沒有問題。

 

最後是loss對$W$,$b$的導數的驗證。

根據結論,loss對$W$的導數滿足:

$\frac{\partial l}{\partial\boldsymbol{W}^{l}} = {\sum\limits_{k}{\sum\limits_{l}\delta_{k,l}^{l}}}\sigma\left( z_{k + x,l + y}^{l - 1} \right) = conv2D\left( {\sigma\left( \boldsymbol{z}^{l - 1} \right),\boldsymbol{\delta}^{l}~,'valid'} \right)$

需要注意的是,對於stride>1的情況,如果要求$\frac{\partial\boldsymbol{l}}{\partial\boldsymbol{W}^{\boldsymbol{l}}}$和$\frac{\partial\boldsymbol{l}}{\partial\boldsymbol{b}^{\boldsymbol{l}}}$,我們需要將$\boldsymbol{\delta}^{\boldsymbol{l}}$做擴充套件比率=strides上取樣先。

那我們先看看原本$\boldsymbol{\delta}^{\boldsymbol{l}}$的樣子,為方便檢視依然用np.squeeze去掉多餘的axis。

np.squeeze(dl_dz1)
Out[13]: 
array([[ 0.        ,  0.        ,  0.13701844,  0.        ],
       [ 0.        ,  0.        ,  0.        ,  0.        ],
       [ 0.        ,  0.        ,  0.        , -0.14360355],
       [ 0.15615712,  0.        ,  0.        ,  0.        ]],
      dtype=float32)

 

接下來我們手動給它做up sampling:

new_kernel = np.squeeze(dl_dz1)
col = np.zeros(4)
new_kernel = np.column_stack((new_kernel[:, 0], col, new_kernel[:, 1], col, new_kernel[:, 2], col, new_kernel[:, 3]))
row = np.zeros(7)
new_kernel = np.row_stack((new_kernel[0, :], row, new_kernel[1, :], row, new_kernel[2, :], row, new_kernel[3, :]))

np.squeeze(new_kernel)
Out[15]: 
array([[ 0.        ,  0.        ,  0.        ,  0.        ,  0.13701844,
         0.        ,  0.        ],
       [ 0.        ,  0.        ,  0.        ,  0.        ,  0.        ,
         0.        ,  0.        ],
       [ 0.        ,  0.        ,  0.        ,  0.        ,  0.        ,
         0.        ,  0.        ],
       [ 0.        ,  0.        ,  0.        ,  0.        ,  0.        ,
         0.        ,  0.        ],
       [ 0.        ,  0.        ,  0.        ,  0.        ,  0.        ,
         0.        , -0.14360355],
       [ 0.        ,  0.        ,  0.        ,  0.        ,  0.        ,
         0.        ,  0.        ],
       [ 0.15615712,  0.        ,  0.        ,  0.        ,  0.        ,
         0.        ,  0.        ]], dtype=float32)

從原來4*4擴充套件成了7*7,即4個元素間,放了3個0。

 

完成了對$\boldsymbol{\delta}^{\boldsymbol{l}}$的上取樣後接下來就是計算$\sigma\left( \boldsymbol{z}^{l - 1} \right)$,它是卷積層的輸入,在本例中就是輸入x,所以$\sigma\left( \boldsymbol{z}^{l - 1} \right)=x$。

剩下的工作就是做卷積了。既然有卷積層那就不用我們自己動手寫卷積操作了。我們定義好CNN層,然讓將其卷積核替換成我們事先準備好的那個上取樣後的$\boldsymbol{\delta}^{\boldsymbol{l}}$。

conv2d = tf.keras.layers.Conv2D(filters=1, kernel_size=new_kernel.shape)
conv2d.build(inverse_conv2d(dl_dz1).shape)

new_kernel = new_kernel[:, :, np.newaxis, np.newaxis].astype(np.float32)
conv2d.kernel = new_kernel

 

再就是對比結果了:

dl_dW = np.squeeze(t.gradient(loss, l1.kernel))

dl_dW 
Out[3]: 
array([[-0.10748424, -0.00609609,  0.364539  ],
       [ 0.1810095 , -0.13556898, -0.19335817],
       [-0.135235  , -0.18566257, -0.23072638]], dtype=float32)

 

my_dl_dW = np.squeeze(conv2d(x))

my_dl_dW
Out[4]: 
array([[-0.10748424, -0.00609609,  0.364539  ],
       [ 0.1810095 , -0.13556898, -0.19335817],
       [-0.135235  , -0.18566257, -0.23072638]], dtype=float32)

驗證無誤。

 

剩下就是$\frac{\partial\boldsymbol{l}}{\partial\boldsymbol{b}^{\boldsymbol{l}}}$了。

根據公式,它滿足$\frac{\partial l}{\partial b^{l}} = \frac{\partial l}{\partial b_{x,y}^{l}} = {\sum\limits_{k}{\sum\limits_{l}\delta_{k,l}^{l}}}$,這個簡單,就只是對$\boldsymbol{\delta}^{\boldsymbol{l}}$的所有元素做累加和:

dl_db = np.squeeze(t.gradient(loss, l1.bias))
dl_db
Out[5]: array(0.15956555, dtype=float32)

my_dl_db = np.sum(new_kernel.astype(np.float32))
my_dl_db 
Out[6]: 0.15956555

也沒有問題。

 


 

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

 

 

 

 

 

相關文章