1.前記
LSTM系列的前面兩篇文章《LSTM反向傳播詳解Part1》《LSTM反向傳播詳解Part2》將LSTM的理論梳理了一遍,本文主要着重用代碼實現LSTM,其實也是對之前兩篇文章的驗證。文末貼出下載鏈接。
2.代碼內容
代碼一共分爲三個文件,分別爲樣本生成文件,採用tensorflow訓練LSTM參數的文件,自編寫LSTM參數訓練文件。
2.1樣本生成
樣本生成文件主要含有LSTM的前向傳播,其核心代碼如下:
def forward(x_pre, h_pre, c_pre, Wi, Wc, Wf, Wo, Bi, Bc, Bf, Bo): # 傳入行矩陣
xh = np.column_stack((x_pre, h_pre)) # 行矩陣合併爲行
# ft = xh.dot(Wf) + Bf
# ft = ft+forget_bias
# ft = sigmoid(ft)
ft = sigmoid(xh.dot(Wf) + Bf + forget_bias)#forget_bias主要是tensorflow中的LSTM模型中有這個參數
it = sigmoid(xh.dot(Wi) + Bi)
ot = sigmoid(xh.dot(Wo) + Bo)
ct_ = tanh(xh.dot(Wc) + Bc)
ct = np.multiply(ft, c_pre) + np.multiply(it, ct_)
ht = np.multiply(ot, tanh(ct))
return ct, ht
每個時刻都調用一次forward()函數,並依次遞歸。
h_pre = np.zeros((1, num_units))
c_pre = h_pre
x_pre = np.array([X[i, 0, :]])
c1, h1 = forward(x_pre, h_pre, c_pre, Wi, Wc, Wf, Wo, Bi, Bc, Bf, Bo)
x_pre = np.array([X[i, 1, :]])
c2, h2 = forward(x_pre, h1, c1, Wi, Wc, Wf, Wo, Bi, Bc, Bf, Bo)
在此回答《LSTM反向傳播詳解Part2》展望部分問題4,“我該如何“自編”樣本數據,Part1文中使用one-hot分類標籤(最後通過層分類)能得到正確結果嗎?”,一開始編寫代碼的時候是將最後時刻的輸出通過後,然後根據其最大值所在的下標爲1生成one-hot輸出標籤。但是這樣子訓練的結果很難接近我自定義的矩陣參數。
想一想原因或許是這樣子的,比如你生成樣本時,softmax序列輸出爲,其標籤標記爲,而通過偏導數公式(在本實驗中相當於爲單位矩陣),當你的訓練結果標籤爲正確值時,依舊有殘差值。這樣子就很難準確訓練到與原定義模型的參數值。因此代碼中採用作爲數據輸出值,訓練模型採用最小誤差平方和做數據迴歸。話說這是我第一次覺得二範數這麼有用,因爲各種書籍中描述的方法都大量說最小二範數誤差有各種問題(比如臺大的機器學習在將LR迴歸時,最小二範數誤差在偏離目標點的初始點時,訓練會很慢,在比如adaboost方法中損失函數採用最小二範數同樣不可取,見《統計學習基礎 Trevor Hastie著》10.6節-損失函數和健壯性)。個人認爲平方誤差損失函數效果好,是因爲本實驗是驗證自定義的精準數學模型。而在實際項目中,是很少有標準的迴歸模型的,比如判斷是不是貓打標籤肯定是0或1而很難說0.65是貓。
2.2通過tensorflow訓練
訓練代碼很簡單,主要圍繞以下兩句核心代碼展開:
tf.nn.rnn_cell.BasicLSTMCell(num_units=h_dimens, forget_bias=forget_bias, state_is_tuple=True)
tf.nn.dynamic_rnn(cell=cell, dtype=tf.float64, inputs=inputx)
定義的損失函數爲最小二範數誤差:
tf.reduce_mean(tf.square(y_label - last_h))
參數訓練方法採用tf.train.AdamOptimizer(lr),採用梯度下降方法訓練時,沒有得到正確的結果。通過訓練,查看LSTM中的模型參數與樣本生成時定義的模型參數結果是一致的。
2.3自編寫LSTM訓練代碼
自編寫的訓練代碼,其思路完全按照系列文章的前面兩篇完成。能夠從代碼中查找到對應的一個個公式。本文不做過多解釋,僅通過代碼講解一下《LSTM反向傳播詳解Part2》文中提到的幾個問題。
2.3.1 問題1
1.自編寫LSTM訓練代碼.Part1《LSTM反向傳播詳解Part1》中維度顯然爲,本文中應該爲,否則表達式將會出錯。這是怎麼一回事?
回答: 很簡單,直接截斷即可。在代碼中其實現爲:
dh = np.dot(Wo.T, (tanh(ct) * (1 - ot) * ot * dh))
dh = dh + np.dot(Wf.T, ct_1 * (1 - ft) * ft * dc)
dh = dh + np.dot(Wi.T, ct_ * (1 - it) * it * dc)
dh = dh + np.dot(Wc.T, it * (1 - ct_ * ct_) * dc)
dh = dh[0:h_dimens] # 公式(10_12)
上述代碼就是參考文獻《LSTM反向傳播詳解Part1》中的公式(10_12),最後一行代碼就是截斷原來對的偏導數爲對的偏導數。在代碼中也能找到Part1文中公式(13_14)dc = dc * ft + dh * ot_1 * (1 - tanh(ct_1) * tanh(ct_1)) # 公式(13_14)
2.3.2 問題2
2.本文公式中出現大量等變量,在程序中要怎麼實現?
回答: 在實現反向傳播前,其實是需要利用當前的參數做前向傳播,並且記錄所有時刻的各個變量值。具體代碼如下:
def batch_forward(X): # X[batch,steps,x_dimens]
Xt = np.transpose(X, (0, 2, 1)) # 轉換爲[batch,x_dimens,steps]
for i in range(batchs):
h_pre = np.zeros((h_dimens, 1))
c_pre = h_pre
for j in range(time_steps):
x_pre = Xt[i, :, j]
x_pre = x_pre.reshape((len(x_pre), 1))
xh = np.row_stack((h_pre, x_pre))
ft = sigmoid(Wf.dot(xh) + Bf + forget_bias)
it = sigmoid(Wi.dot(xh) + Bi)
ot = sigmoid(Wo.dot(xh) + Bo)
ct_ = tanh(Wc.dot(xh) + Bc)
ct = np.multiply(ft, c_pre) + np.multiply(it, ct_)
ht = np.multiply(ot, tanh(ct))
F[i, :, j] = ft.reshape(-1)
I[i, :, j] = it.reshape(-1)
O[i, :, j] = ot.reshape(-1)
C_[i, :, j] = ct_.reshape(-1)
C[i, :, j] = ct.reshape(-1)
H[i, :, j] = ht.reshape(-1)
h_pre = ht
c_pre = ct
HX = np.column_stack((H, Xt))
return F, I, O, C_, C, H, HX
注意樣本生成代碼中採用的前向傳播和這裏的代碼稍微有點點不一樣,樣本生成時爲行向量,這裏爲列向量,且在前。之所以有兩種不同的排列,是因爲前一種方法是tensorflow中的排列順序,而我在公式推導中還是習慣列向量,代碼爲了與前面兩文中公式對應,就採用了列向量的方式。
代碼中的如下矩陣,存儲了當前批量樣本根據目前的矩陣參數生成的各個時刻的值。
F[i, :, j] = ft.reshape(-1)
I[i, :, j] = it.reshape(-1)
O[i, :, j] = ot.reshape(-1)
C_[i, :, j] = ct_.reshape(-1)
C[i, :, j] = ct.reshape(-1)
H[i, :, j] = ht.reshape(-1)
2.3.3 問題3
3.本文的公式都是針對單個樣本推導的,當每個batch中有大量樣本時,怎麼辦?不同時刻的的偏導數都有對模型參數的鏈接,要如何實現?
回答: 每次都是計算批量樣本範圍內的樣本誤差平方和的平均,因此多個樣本時,將每個樣本得到的偏導數求平均即可。至於不同時刻都有對矩陣變量的偏導數,因此需要計算每個時刻時的對這些矩陣變量的偏導數求和。代碼如下所示:
dbf = np.sum(dBf, 0) # batch合一
dbf = np.sum(dbf, 1) # time_steps合一
dbf = dbf / batchs
dbf = dbf.reshape((len(dbf), 1))
2.3.4 問題4
4.如果理論一切正常,你相信你能得到正確的結果嗎?比如參數更新採用梯度下降還是Adam?我該如何“自編”樣本數據,Part1文中使用ont-hot分類標籤(最後通過層分類)能得到正確結果嗎?
回答: 在tensorflow中訓練都沒法正確的通過梯度下降方法得到模型參數,自編寫的代碼就更沒信心了。本代碼是通過採用Adam方法訓練參數。參考花書《深度學習》8.5.3節Adam算法中僞代碼實現。
def Adamgrad(dB, t, m, v, lr=learn_rate, beta1=0.9, beta2=0.999, epsilon=1e-08):
m = beta1 * m + (1 - beta1) * dB
v = beta2 * v + (1 - beta2) * (dB ** 2)
mb = m / (1 - beta1 ** t) # t is step number
vb = v / (1 - beta2 ** t)
detB = lr * mb / (np.sqrt(vb) + epsilon)
return m, v, detB
最後一問在本文的2.1節已經闡述過,這裏不再重複。
3.總結
最終通過代碼得到了生成樣本時定義的模型參數,不過速度比tensorflow的訓練方法慢的多,結果的精度當然也沒有那麼高,我發現大部分模型參數誤差大約在0.4%左右,誤差最大的一個參數在Bi的最後一項,誤差爲1.82%
注意前面所述,樣本生成與反向傳播推導時前後順序不一致,所以訓練結果中與部分和部分對應的參數要對調一下才能與樣本生成時一致。
一開始的想法就是想嘗試推導一下LSTM,後來想想如果不通過代碼實現又怎麼能夠說明自己推導的是對的了。至少通過實驗表明結果還是令人滿意的。
4.附加說明
其實代碼中沒使用什麼過多的語言技巧,只要照着前面兩篇文章中的公式閱讀代碼還是比較簡單的。畢竟不是工程代碼所以註釋什麼的確實也比較簡單,見諒。
代碼鏈接:LSTM反向傳播代碼實現(通過tensorflow和自編寫代碼實現)
generateSamples.py爲樣本生成代碼
train.py 爲通過tensorflow訓練代碼
LstmBP.py 爲根據系列文章編寫的反向傳播的實現代碼