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

SumwaiLiu發表於2020-09-07

《神經網路的梯度推導與程式碼驗證》之LSTM的前向傳播和反向梯度推導  中,我們學習了LSTM的前向傳播和反向梯度求導,但知識仍停留在紙面。本篇章將基於深度學習框架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))

 

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

 

下面開始實現前向傳播:

我們先來看如果拿tensorflow快速實現前向傳播是什麼樣的:

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

inputs = np.random.random([1, 3, 4]).astype(np.float32)
# 初始化c和h狀態: 第一個是h狀態,第二個是c
init_state = [tf.constant(np.random.random((inputs.shape[0], 2)).astype(np.float32)),
              tf.constant(np.random.random((inputs.shape[0], 2)).astype(np.float32))]
# 定義LSTM layer
lstm_cell = tf.keras.layers.LSTMCell(2)
lstm = tf.keras.layers.RNN(lstm_cell, return_sequences=True, return_state=True)
lstm_seq, last_h, last_c = lstm(inputs=inputs, initial_state=init_state)
# fnn layer
dense = tf.keras.layers.Dense(3)
# 最後得到y
output_seq = tf.math.softmax(dense(lstm_seq))

 

我們隨便造一條樣本(inputs, y_ture),跟vanilla RNN的程式碼驗證一樣,輸入inputs是一條步長為3的有4維特徵的資料;標籤資料步長也為3,每個步長上是長度為3的概率向量。

inputs.shape
Out[3]: (1, 3, 4)
y_true.shape
Out[4]: (1, 3, 3)

 

至於初始狀態init_state,區別於vanilla RNN,LSTM的內部狀態不僅有h,還有c狀態。

init_state
Out[5]: 
[<tf.Tensor: shape=(1, 2), dtype=float32, numpy=array([[0.56804454, 0.92559665]], dtype=float32)>,
 <tf.Tensor: shape=(1, 2), dtype=float32, numpy=array([[0.07103606, 0.0871293 ]], dtype=float32)>]

 

它們的dim都是2,意味著接下來定義LSTM layer的時候,units等於2,也就是第10行程式碼。

 

程式碼12行輸出了inputs經過LSTM layer的h狀態序列,並返回了最後一個time step的h和c 狀態:

lstm_seq
Out[6]: 
<tf.Tensor: shape=(1, 3, 2), dtype=float32, numpy=
array([[[0.0734415 , 0.0337011 ],
        [0.12446897, 0.04911498],
        [0.08435698, 0.1005574 ]]], dtype=float32)>
last_h
Out[7]: <tf.Tensor: shape=(1, 2), dtype=float32, numpy=array([[0.08435698, 0.1005574 ]], dtype=float32)>
last_c 
Out[8]: <tf.Tensor: shape=(1, 2), dtype=float32, numpy=array([[0.4424333 , 0.25549415]], dtype=float32)>

 

顯然lstm_seq[0, -1, :] 就是last_h,這點在上面這段程式碼得到了驗證。

lstm_seq經過全連線最後得到shape和y_true一樣的輸出序列:

output_seq 
Out[9]: 
<tf.Tensor: shape=(1, 3, 3), dtype=float32, numpy=
array([[[0.33866003, 0.33178926, 0.32955068],
        [0.34256846, 0.3297772 , 0.3276543 ],
        [0.3379959 , 0.3387031 , 0.32330096]]], dtype=float32)>

 


 

通過呼叫上面幾行程式碼,我們似乎能夠實現上圖的操作。但尚未確定程式碼lstm = tf.keras.layers.RNN(lstm_cell, return_sequences=True, return_state=True)是否按正真按照LSTM的前傳公式進行計算的。

從下面tensorflow的LSTM原始碼上看其實不太好對應上前向傳播公式,因為kernel和bias實際上是包含了四個門的kernel和bias的大tensor,如果想要對照著前傳公式並配合原始碼一起消化的話,可以參考這篇博文的原始碼解讀。

  def step(cell_inputs, cell_states):
    """Step function that will be used by Keras RNN backend."""
    h_tm1 = cell_states[0]  # previous memory state
    c_tm1 = cell_states[1]  # previous carry state

    z = K.dot(cell_inputs, kernel)
    z += K.dot(h_tm1, recurrent_kernel)
    z = K.bias_add(z, bias)

    z0, z1, z2, z3 = array_ops.split(z, 4, axis=1)

    i = nn.sigmoid(z0)
    f = nn.sigmoid(z1)
    c = f * c_tm1 + i * nn.tanh(z2)
    o = nn.sigmoid(z3)

    h = o * nn.tanh(c)
    return h, [h, c]

 

但我們最好還是動手實操一下,這樣印象可以更加深刻,所以接下來我們手動按照LSTM的前傳公式實現一遍前向傳播看跟tensorflow給出的輸出結果是否一致。

 

下面這段程式碼看似很長,但實際上只是反覆做同樣的事情而已,它實現了LSTM在時間上展開3步的前向操作,並計算了loss,所以真正的程式碼量你可以認為大約只有1/3這樣。

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

 1 # 提取lstm的變數
 2 kernel = lstm.weights[0]
 3 recurrent_kernel = lstm.weights[1]
 4 bias = lstm.weights[2]
 5 
 6 with tf.GradientTape(persistent=True) as t:
 7     h0, c0 = init_state
 8     # --------stpe1-----------
 9     z1 = tf.matmul(inputs[:, 0, :], kernel)
10     z1 += tf.matmul(init_state[0], recurrent_kernel)
11     z1 += bias
12 
13     i1 = tf.math.sigmoid(z1[:, 0:2])
14     f1 = tf.math.sigmoid(z1[:, 2:4])
15     c1 = f1 * init_state[1] + i1 * tf.math.tanh(z1[:, 4:6])
16     t.watch(c1)
17     o1 = tf.math.sigmoid(z1[:, 6:8])
18 
19     h1 = o1 * tf.math.tanh(c1)
20     t.watch(h1)
21     out1 = dense(h1)
22     t.watch(out1)
23     a1 = tf.math.softmax(out1)
24     t.watch(a1)
25     # --------stpe2-------------
26     z2 = tf.matmul(inputs[:, 1, :], kernel)
27     z2 += tf.matmul(h1, recurrent_kernel)
28     z2 += bias
29 
30     i2 = tf.math.sigmoid(z2[:, 0:2])
31     f2 = tf.math.sigmoid(z2[:, 2:4])
32     c2 = f2 * c1 + i2 * tf.math.tanh(z2[:, 4:6])
33     t.watch(c2)
34     o2 = tf.math.sigmoid(z2[:, 6:8])
35 
36     h2 = o2 * tf.math.tanh(c2)
37     t.watch(h2)
38     out2 = dense(h2)
39     t.watch(out2)
40     a2 = tf.math.softmax(out2)
41     t.watch(a2)
42     # ---------step3---------------
43     z3 = tf.matmul(inputs[:, 2, :], kernel)
44     z3 += tf.matmul(h2, recurrent_kernel)
45     z3 += bias
46 
47     i3 = tf.math.sigmoid(z3[:, 0:2])
48     f3 = tf.math.sigmoid(z3[:, 2:4])
49     c3 = f3 * c2 + i3 * tf.math.tanh(z3[:, 4:6])
50     t.watch(c3)
51     o3 = tf.math.sigmoid(z3[:, 6:8])
52 
53     h3 = o3 * tf.math.tanh(c3)
54     t.watch(c3)
55     out3 = dense(h3)
56     t.watch(out3)
57     a3 = tf.math.softmax(out3)
58     t.watch(a3)
59     # ---------loss----------------
60     my_seqout = tf.stack([a1, a2, a3], axis=1)
61     my_loss = get_crossentropy(y_pred=my_seqout, y_true=y_true)
62     # -------L(t)---------
63     loss_1 = get_crossentropy(y_pred=my_seqout[:, 0, :], y_true=y_true[:, 0, :])
64     loss_2 = get_crossentropy(y_pred=my_seqout[:, 1, :], y_true=y_true[:, 1, :])
65     loss_3 = get_crossentropy(y_pred=my_seqout[:, 2, :], y_true=y_true[:, 2, :])

 

為方便結合公式理解,下面是LSTM前傳的核心公式:

 

前向傳播過程在每個時間步$t$上發生的順序為:

1)更新遺忘門輸出:

$\boldsymbol{f}^{(t)} = \sigma\left( {\boldsymbol{W}_{f}\boldsymbol{h}^{(t - 1)} + \boldsymbol{U}_{f}\boldsymbol{x}^{(t)} + \boldsymbol{b}_{f}} \right)$

2)更新輸入門和其控制物件:

$\boldsymbol{i}^{(t)} = \sigma\left( {\boldsymbol{W}_{i}\boldsymbol{h}^{(t - 1)} + \boldsymbol{U}_{i}\boldsymbol{x}^{(t)} + \boldsymbol{b}_{i}} \right)$

$\boldsymbol{a}^{(t)} = tanh\left( {\boldsymbol{W}_{a}\boldsymbol{h}^{(t - 1)} + \boldsymbol{U}_{a}\boldsymbol{x}^{(t)} + \boldsymbol{b}_{a}} \right)$

3)更新細胞狀態,從而$\left. \boldsymbol{C}^{(t - 1)}\longrightarrow\boldsymbol{C}^{(t)} \right.$:

$\boldsymbol{C}^{(t)} = \boldsymbol{C}^{(t - 1)}\bigodot\boldsymbol{f}^{(t)} + \boldsymbol{a}^{(t)}\bigodot\boldsymbol{i}^{(t)}$

4)更新輸出門和其控制物件,從而$\left. \boldsymbol{h}^{(t - 1)}\longrightarrow\boldsymbol{h}^{(t)} \right.$:

$\boldsymbol{o}^{(t)} = \sigma\left( {\boldsymbol{W}_{o}\boldsymbol{h}^{(t - 1)} + \boldsymbol{U}_{o}\boldsymbol{x}^{(t - 1)} + \boldsymbol{b}_{o}} \right)$

$\boldsymbol{h}^{(t)} = \boldsymbol{o}^{(t)}\bigodot tanh\left( \boldsymbol{C}^{(t)} \right)$

5)得到當前時間步$t$的預測輸出:

${\hat{\boldsymbol{y}}}^{(t)} = \sigma\left( {\boldsymbol{V}\boldsymbol{h}^{(t)} + \boldsymbol{c}} \right)$ 

 

接下來解釋上述程式碼,首先是看看lstm.weights都有什麼:

lstm.weights
Out[11]: 
[<tf.Variable 'rnn/lstm_cell/kernel:0' shape=(4, 8) dtype=float32, numpy=
 array([[-0.20611948, -0.13917124, -0.4464248 , -0.5220002 , -0.07079256,
         -0.11765444, -0.6348312 , -0.09944093],
        [-0.36945656,  0.49826902, -0.38441902,  0.5155092 ,  0.43278998,
         -0.6451189 ,  0.25279564, -0.5977526 ],
        [-0.16671354, -0.34911466, -0.36100882, -0.441833  ,  0.22657168,
          0.6603723 , -0.64222896,  0.39119774],
        [ 0.50059   , -0.46351027,  0.21986896,  0.5533599 ,  0.34416074,
          0.46849674, -0.6191046 , -0.6988822 ]], dtype=float32)>,
 <tf.Variable 'rnn/lstm_cell/recurrent_kernel:0' shape=(2, 8) dtype=float32, numpy=
 array([[ 0.0145992 , -0.7155518 , -0.02687099, -0.21511525, -0.5779975 ,
          0.2309799 , -0.06609378, -0.22130255],
        [-0.06615384, -0.62732273, -0.27503067,  0.35276616,  0.4759796 ,
         -0.19590497, -0.18337685,  0.3216232 ]], dtype=float32)>,
 <tf.Variable 'rnn/lstm_cell/bias:0' shape=(8,) dtype=float32, numpy=array([0., 0., 1., 1., 0., 0., 0., 0.], dtype=float32)>]

 

跟vanilla RNN類似,也是有3類變數。但注意這裡3類變數的shape中都有一個維度等於8,因為這是4種門的kernel,recurrent_kernel和bias拼在一起的結果,這樣方便進行批量矩陣乘法。

 

回到程式碼,程式碼9~11實現的就是上面前傳公式組中的:

$\boldsymbol{W}_{f}\boldsymbol{h}^{(t - 1)} + \boldsymbol{U}_{f}\boldsymbol{x}^{(t)} + \boldsymbol{b}_{f}$

$\boldsymbol{W}_{i}\boldsymbol{h}^{(t - 1)} + \boldsymbol{U}_{i}\boldsymbol{x}^{(t)} + \boldsymbol{b}_{i}$

$\boldsymbol{W}_{a}\boldsymbol{h}^{(t - 1)} + \boldsymbol{U}_{a}\boldsymbol{x}^{(t)} + \boldsymbol{b}_{a}$

$\boldsymbol{W}_{o}\boldsymbol{h}^{(t - 1)} + \boldsymbol{U}_{o}\boldsymbol{x}^{(t - 1)} + \boldsymbol{b}_{o}$

只不過這四組運算的結果都合一塊變成了z1,如果要專門提取某個門的z結果,需要對z1進行索引操作。例如i1 = tf.math.sigmoid(z1[:, 0:2])在做的就是:

$\boldsymbol{i}^{(t)} = \sigma\left( {\boldsymbol{W}_{i}\boldsymbol{h}^{(t - 1)} + \boldsymbol{U}_{i}\boldsymbol{x}^{(t)} + \boldsymbol{b}_{i}} \right)$

其他3個門的計算以此類推。

 

c1 = f1 * init_state[1] + i1 * tf.math.tanh(z1[:, 4:6])實現的是c狀態的遞推,即:

$\boldsymbol{C}^{(t)} = \boldsymbol{C}^{(t - 1)}\bigodot\boldsymbol{f}^{(t)} + \boldsymbol{a}^{(t)}\bigodot\boldsymbol{i}^{(t)}$

 

h1 = o1 * tf.math.tanh(c1)則實現了上述公式組的h狀態的計算,即:

$\boldsymbol{h}^{(t)} = \boldsymbol{o}^{(t)}\bigodot tanh\left( \boldsymbol{C}^{(t)} \right)$

 

21+23行程式碼則實現了LSTM的輸出計算,即:

${\hat{\boldsymbol{y}}}^{(t)} = \sigma\left( {\boldsymbol{V}\boldsymbol{h}^{(t)} + \boldsymbol{c}} \right)$

其中desne layer的kernel和bias分別對應$\boldsymbol{V}$和$\boldsymbol{c}$

為突出重點這裡不會去驗證前面定義好了的dense layer是否真的按照FNN的前向傳播公式在做,這部分驗證可參考FNN(DNN)前向和反向傳播過程的程式碼驗證

 

至此,我們完成了c和h狀態在時間步上的遞進,也完成了當前time step的輸出,剩下的程式碼就是繼續迴圈再進行多兩次。注意在計算time step2的c和h狀態時,遞推公式用到的就是上一個time step的c和h狀態init_state了。

 

最後我們看看3個time step上的前向輸出跟tensorflow給出rnn layer的輸出output_seq 是否一致:

output_seq
Out[12]: 
<tf.Tensor: shape=(1, 3, 3), dtype=float32, numpy=
array([[[0.33866003, 0.33178926, 0.32955068],
        [0.34256846, 0.3297772 , 0.3276543 ],
        [0.3379959 , 0.3387031 , 0.32330096]]], dtype=float32)>
my_seqout
Out[13]: 
<tf.Tensor: shape=(1, 3, 3), dtype=float32, numpy=
array([[[0.33866003, 0.33178926, 0.32955068],
        [0.34256846, 0.3297772 , 0.3276543 ],
        [0.3379959 , 0.3387031 , 0.32330096]]], dtype=float32)>

 

看來並沒有問題。

 

--------反向傳播驗證---------

我們先將4個門的recurrent_kernel,即前傳公式中組中的$\boldsymbol{W}_{i}$,$\boldsymbol{W}_{f}$,$\boldsymbol{W}_{a}$和$\boldsymbol{W}_{o}$分別提取出來:

w_i = recurrent_kernel[:, 0:2]
w_f = recurrent_kernel[:, 2:4]
w_a = recurrent_kernel[:, 4:6]
w_o = recurrent_kernel[:, 6:8]

 

因為是BPTT,所以首先我們驗證$\boldsymbol{\delta}_{h}^{(T)}$和$\boldsymbol{\delta}_{C}^{(T)}$,根據公式,它們滿足:

$\boldsymbol{\delta}_{h}^{(T)} = \boldsymbol{V}^{T}\left( {{\hat{\boldsymbol{y}}}^{(T)} - \boldsymbol{y}^{(T)}} \right)$

$\boldsymbol{\delta}_{C}^{(T)} = \left( \frac{\partial\boldsymbol{h}^{(T)}}{\partial\boldsymbol{C}^{(T)}} \right)^{T}\frac{\partial L}{\partial\boldsymbol{h}^{(T)}} = \boldsymbol{\delta}_{h}^{(T)}\bigodot\boldsymbol{o}^{(T)}\bigodot{tanh}^{'}\left( \boldsymbol{C}^{(T)} \right)$

 

下面是對比結果,其中delta_hT = t.gradient(my_loss, h3)表示這是通過tensorflow自動微分工具求得的$\boldsymbol{\delta}_{h}^{(T)}$,而帶my_字首的則是根據LSTM的前向傳播和反向梯度推導   中的公式(就是上面的式子)手動實現的結果。後續的符號同樣沿用這樣的命名規則。

(.transpose()的作用和意義見FNN(DNN)前向和反向傳播過程的程式碼驗證 給出的解釋,這裡不再贅述)

# --------to validate delta_hT---------
delta_hT = t.gradient(my_loss, h3)
my_delta_hT = tf.matmul((a3 - y_true[:, 2, :]), tf.transpose(dense.kernel))

# --------to validate delta_cT---------
delta_cT = t.gradient(my_loss, c3)
my_delta_cT = delta_hT * o3 * (1 - tf.math.tanh(c3)**2)

 

delta_hT
Out[14]: <tf.Tensor: shape=(1, 2), dtype=float32, numpy=array([[-0.07147076,  0.0525395 ]], dtype=float32)>
my_delta_hT
Out[15]: <tf.Tensor: shape=(1, 2), dtype=float32, numpy=array([[-0.07147078,  0.05253952]], dtype=float32)>
delta_cT
Out[16]: <tf.Tensor: shape=(1, 2), dtype=float32, numpy=array([[-0.01199877,  0.01980529]], dtype=float32)>
my_delta_cT
Out[17]: <tf.Tensor: shape=(1, 2), dtype=float32, numpy=array([[-0.01199877,  0.01980529]], dtype=float32)>

 

 

驗證無誤,公式是正確的。

 

接下來是驗證兩個重要的遞推公式:

 

第一個遞推公式,從$\boldsymbol{\delta}_{C}^{(t + 1)}$和$\boldsymbol{\delta}_{h}^{(t + 1)}$推得$\boldsymbol{\delta}_{h}^{(t)}$:

$\boldsymbol{\delta}_{h}^{(t)} = \frac{\partial l\left( t \right)}{\partial\boldsymbol{h}^{(t)}} + \left( \frac{\partial\boldsymbol{C}^{(t + 1)}}{\partial\boldsymbol{h}^{(t)}} \right)^{T}\boldsymbol{\delta}_{C}^{(t + 1)} + \left( {\frac{\partial\boldsymbol{h}^{(t + 1)}}{\partial\boldsymbol{o}^{(t)}}\frac{\partial\boldsymbol{o}^{(t + 1)}}{\partial\boldsymbol{h}^{(t)}}} \right)^{T}\boldsymbol{\delta}_{h}^{(t + 1)}$

其中,

$\frac{\partial\boldsymbol{C}^{(t + 1)}}{\partial\boldsymbol{h}^{(t)}} = diag\left( {\boldsymbol{C}^{(t)}\bigodot\boldsymbol{f}^{({t + 1})}\bigodot\left( {1 - \boldsymbol{f}^{({t + 1})}} \right)} \right)\boldsymbol{W}_{f} + diag\left( {\boldsymbol{a}^{({t + 1})}\bigodot\boldsymbol{i}^{({t + 1})}\bigodot\left( {1 - \boldsymbol{i}^{({t + 1})}} \right)} \right)\boldsymbol{W}_{i} + diag\left( {\boldsymbol{i}^{({t + 1})}\bigodot\left( {1 - {\boldsymbol{a}^{({t + 1})}}^{2}} \right)} \right)\boldsymbol{W}_{a}$

 $\frac{\partial\boldsymbol{h}^{(t + 1)}}{\partial\boldsymbol{o}^{(t)}}\frac{\partial\boldsymbol{o}^{(t + 1)}}{\partial\boldsymbol{h}^{(t)}} = diag\left( {tanh\left( \boldsymbol{C}^{({t + 1})} \right)\bigodot\boldsymbol{o}^{({t + 1})}\bigodot\left( {1 - \boldsymbol{o}^{({t + 1})}} \right)} \right)$

驗證程式碼如下:

# -----------from delta_c2 and delta_h2 to delta_h1----------
a_2 = tf.math.tanh(z2[:, 4:6])
delta_h2 = t.gradient(my_loss, h2)
delta_c2 = t.gradient(my_loss, c2)
dc2_dh1 = tf.matmul(w_f, np.diag(tf.squeeze(c1 * f2 * (1 - f2)))) + \
             tf.matmul(w_i, np.diag(tf.squeeze(a_2 * i2 * (1 - i2)))) + \
             tf.matmul(w_a, np.diag(tf.squeeze(i2 * (1 - a_2**2))))

delta_h1 = t.gradient(my_loss, h1)
my_delta_h1 = tf.matmul((a1 - y_true[:, 0, :]), tf.transpose(dense.kernel)) + \
             tf.matmul(delta_c2, tf.transpose(dc2_dh1)) + \
             tf.matmul(delta_h2, tf.transpose(tf.matmul(w_o, np.diag(tf.squeeze(tf.math.tanh(c2) * o2 * (1 - o2))))))

 

 

delta_h1
Out[18]: <tf.Tensor: shape=(1, 2), dtype=float32, numpy=array([[ 0.0437731 , -0.09992676]], dtype=float32)>
my_delta_h1
Out[19]: <tf.Tensor: shape=(1, 2), dtype=float32, numpy=array([[ 0.0437731 , -0.09992676]], dtype=float32)>

 

沒有問題,遞推公式正確。

 

再是另外一個遞推公式, 從$\boldsymbol{\delta}_{h}^{(t)}$和$\boldsymbol{\delta}_{C}^{(t + 1)}$推得$\boldsymbol{\delta}_{C}^{(t)}$:

$\boldsymbol{\delta}_{C}^{(t)} = \left( \frac{\partial\boldsymbol{h}^{(t)}}{\partial\boldsymbol{c}^{(t)}} \right)^{T}\boldsymbol{\delta}_{h}^{(t)} + \left( \frac{\partial\boldsymbol{c}^{(t + 1)}}{\partial\boldsymbol{c}^{(t)}} \right)^{T}\boldsymbol{\delta}_{C}^{(t + 1)} = \boldsymbol{o}^{(t)} \odot \left( {1 - {tanh}^{2}\left( \boldsymbol{C}^{(t)} \right)} \right)^{2}{\odot \boldsymbol{\delta}}_{h}^{(t)} + \boldsymbol{f}^{({t + 1})} \odot \boldsymbol{\delta}_{C}^{(t + 1)}$

 

驗證程式碼如下:

 

# --------------from delta_h1 and delta_c2 to delta_c1---------
delta_c1 = t.gradient(my_loss, c1)
my_delta_c1 = o1 * (1 - tf.math.tanh(c1)**2) * delta_h1 + f2 * delta_c2

 

delta_c1
Out[20]: <tf.Tensor: shape=(1, 2), dtype=float32, numpy=array([[ 0.01076444, -0.01738559]], dtype=float32)>
my_delta_c1
Out[21]: <tf.Tensor: shape=(1, 2), dtype=float32, numpy=array([[ 0.01076444, -0.01738559]], dtype=float32)>

 

沒問題+1。

 

最後是驗證$\frac{\partial L}{\partial\boldsymbol{W}_{f}}$,根據公式,它滿足:

$\frac{\partial L}{\partial\boldsymbol{W}_{f}} = {\sum\limits_{t = 1}^{T}\left\lbrack {\boldsymbol{\delta}_{C}^{(t)} \odot \boldsymbol{C}^{(t - 1)} \odot \boldsymbol{f}^{(t)} \odot \left\lbrack {1 - \boldsymbol{f}^{(t)}} \right\rbrack} \right\rbrack}\left( \boldsymbol{h}^{(t - 1)} \right)^{T}$

 

程式碼驗證如下:

# -------------to validate dl_dW_f---------------
dl_dW_f = t.gradient(my_loss, recurrent_kernel)[:, 2:4]
my_dl_dW_f = tf.matmul(tf.transpose(h2), (delta_cT * c2 * f3 * (1 - f3))) + \
             tf.matmul(tf.transpose(h1), (delta_c2 * c1 * f2 * (1 - f2))) + \
             tf.matmul(tf.transpose(h0), (delta_c1 * c0 * f1 * (1 - f1)))

 

dl_dW_f
Out[22]: 
<tf.Tensor: shape=(2, 2), dtype=float32, numpy=
array([[-6.2376050e-05, -2.1518332e-05],
       [ 1.0945055e-04, -1.8339025e-04]], dtype=float32)>
my_dl_dW_f
Out[23]: 
<tf.Tensor: shape=(2, 2), dtype=float32, numpy=
array([[-6.2376050e-05, -2.1518332e-05],       [ 1.0945055e-04, -1.8339025e-04]], dtype=float32)>

 

沒問題+1。

至此,LSTM的所有前向和部分反向傳播公式已驗證完,剩下的反向傳播公式例如$\frac{\partial L}{\partial\boldsymbol{W}_{i}}$和$\frac{\partial L}{\partial\boldsymbol{W}_{o}}$等,都是類似這樣的操作罷了。

 

如果本文對您有所幫助的話,還請點下“推薦”讓它能幫到更多的人,謝謝。


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

相關文章