終於來到最後一期,也是目前NLP最常用的架構RNN,及其各種變種。具體來看,本次總結的主要內容:
- RNN的結構。循環神經網絡的提出背景、優缺點。着重學習RNN的反向傳播、RNN出現的問題(梯度問題、長期依賴問題)、BPTT算法。
- 雙向RNN
- LSTM、GRU的結構、提出背景、優缺點。
- 針對梯度消失(LSTM等其他門控RNN)、梯度爆炸(梯度截斷)的解決方案。
- Text-RNN的原理。
- 利用Text-RNN模型來進行文本分類
RNN
循環神經網絡(Recurrent Neural Network,RNN)是用來建模序列化數據的一
種主流深度學習模型 [1]。傳統的前饋神經網絡一般的輸入都是一個定長的向量,無法處理變長的序列信息,即使通過一些方法把序列處理成定長的向量,模型也很難捕捉序列中的長距離依賴關係。RNN則通過將神經元串行起來處理序列化的數據。由於每個神經元能用它的內部變量保存之前輸入的序列信息,因此整個序列被濃縮成抽象的表示,並可以據此進行分類或生成新的序列。近年來,得益於計算能力的大幅提升和模型的改進,RNN在很多領域取得了突破性的進展——機器翻譯、序列標註、圖像描述、推薦系統、智能聊天機器人、自動作詞作曲等。
下圖展示一個典型的RNN的結構:
由圖可見,一個長度爲T的序列用循環神經網絡建模,展開之後可以看作是一
個T層的前饋神經網絡。其中,第t層的隱含狀態編碼了序列中前t個輸入的信息,可以通過當前的輸入和上一層神經網絡的狀態計算得到;最後一層的狀態編碼了整個序列的信息。以此爲基礎的結構可以應用於多種具體任務。例如,在後面直接接一個Softmax層,輸出文本所屬類別的預測概率y,就可以實現文本分類。和y的計算公式爲:
,
,
其中f和g爲激活函數,U爲輸入層到隱含層的權重矩陣,W爲隱含層從上一時刻到
下一時刻狀態轉移的權重矩陣。在文本分類任務中,f可以選取Tanh函數或者ReLU函數,g可以採用Softmax函數。
通過最小化損失誤差(即輸出的y與真實類別之間的距離),我們可以不斷訓
練網絡,使得得到的循環神經網絡可以準確地預測文本所屬的類別,達到分類目
的。相比於卷積神經網絡等前饋神經網絡,循環神經網絡由於具備對序列順序信
息的刻畫能力,往往能得到更準確的結果。
RNN 的變種有很多,從架構上來看,主要可以總結爲一下4種:
循環神經網絡的參數可以通過梯度下降方法來進行學習。循環神經網絡中存在一個遞歸調用的函數f(·),因此其計算參數梯度的方式和前饋神經網絡不太相同。在循環神經網絡中主要有兩種計算梯度的方式:隨時間反向傳播(BPTT)和實時循環學習(RTRL)算法。
隨時間反向傳播(Backpropagation Through Time,BPTT)算法的主要
思想是通過類似前饋神經網絡的錯誤反向傳播算法[2,3] 來進行計算
梯度。BPTT算法將循環神經網絡看作是一個展開的多層前饋網絡,其中“每一
層”對應循環網絡中的“每個時刻”。這樣,循環神經網絡就可以按
按照前饋網絡中的反向傳播算法進行計算參數梯度。在“展開”的前饋網絡中,
所有層的參數是共享的,因此參數的真實梯度是將所有“展開層”的參數梯度
之和。
因爲參數U 和隱藏層在每個時刻k(1 ≤ k ≤ t) 的淨輸入有關,因此第t 時刻損失的損失函數 關於參數 的梯度爲:
其中表示“直接”偏導數,即公式中保持 不變,對 進行求偏導數,得到
其中爲第k −1 時刻隱狀態的第j 維;除了第i 行值爲x外,其餘都爲0 的向量。定義爲第t 時刻的損失對第k 時刻隱藏神經層的淨輸入 的導數,則:
公式合併,並寫出矩陣形式得到:
BPTT由圖表示可得:
與反向傳播的BPTT算法不同的是,實時循環學習(Real-Time Recurrent
Learning,RTRL)是通過前向傳播的方式來計算梯度,詳細可參考文獻[3]
雙向RNN和LSTM
Bidirectional RNN(雙向RNN)假設當前t的輸出不僅僅和之前的序列有關,並且 還與之後的序列有關,例如:預測一個語句中缺失的詞語那麼需要根據上下文進 行預測;Bidirectional RNN是一個相對簡單的RNNs,由兩個RNNs上下疊加在 一起組成。輸出由這兩個RNNs的隱藏層的狀態決定 [4]。 具體結構如圖所示:
Long Short Term 網絡—— 一般就叫做 LSTM ——是一種 RNN 特殊的類型,可以學習長期依賴信息。LSTM 由Hochreiter & Schmidhuber (1997)提出,並在近期被Alex Graves進行了改良和推廣。在很多問題,LSTM 都取得相當巨大的成功,並得到了廣泛的使用。
LSTM 通過刻意的設計來避免長期依賴問題。記住長期的信息在實踐中是 LSTM 的默認行爲,而非需要付出很大代價才能獲得的能力!所有 RNN 都具有一種重複神經網絡模塊的鏈式的形式。在標準的 RNN 中,這個重複的模塊只有一個非常簡單的結構,例如一個 tanh 層。
LSTM 的關鍵就是細胞狀態,水平線在圖上方貫穿運行。
細胞狀態類似於傳送帶。直接在整個鏈上運行,只有一些少量的線性交互。信息在上面流傳保持不變會很容易。LSTM 有通過精心設計的稱作爲“門”的結構來去除或者增加信息到細胞狀態的能力。門是一種讓信息選擇式通過的方法。他們包含一個 sigmoid 神經網絡層和一個 pointwise 乘法操作。
門控循環單元(Gated Recurrent Unit,GRU)網絡是一種比LSTM網絡更加簡單的循環神經網絡。
在LSTM網絡中,輸入門和遺忘門是互補關係,用兩個門比較冗餘。GRU將輸
入門與和遺忘門合併成一個門:更新門。同時,GRU也不引入額外的記憶單元,
直接在當前狀態 和歷史狀態 之間引入線性依賴關係。具體結構如下:
梯度消失與梯度爆炸
循環神經網絡在學習過程中的主要問題是長期依賴問題,
在BPTT算法中,將 展開得到:
如果定義,則:
若,當時,,會造成系統不穩定,稱爲梯度爆炸問
題(Gradient Exploding Problem);相反,若,當時,,會出現和深度前饋神經網絡類似的梯度消失問題(gradient vanishing problem)。
梯度爆炸的問題可以通過梯度裁剪來緩解,即當梯度的範式大於某個給定值
時,對梯度進行等比收縮。而梯度消失問題相對比較棘手,需要對模型本身進行
改進。深度殘差網絡是對前饋神經網絡的改進,通過殘差學習的方式緩解了梯度
消失的現象,從而使得我們能夠學習到更深層的網絡表示;而對於循環神經網絡
來說,長短時記憶模型 LSTM及其變種門控循環單元(Gated recurrent unit,GRU)等模型通過加入門控機制,很大程度上彌補了梯度消失所帶來的損失。
TextRNN
TextRNN的一般流程是1. embeddding layer, 2.Bi-LSTM layer, 3.concat output, 4.FC layer, 5.softmax:
其基本結構如圖所示:
參考代碼如下:
# 構建模型
class BiLSTM(object):
"""
Bi-LSTM 用於文本分類
"""
def __init__(self, config, wordEmbedding):
# 定義模型的輸入
self.inputX = tf.placeholder(tf.int32, [None, config.sequenceLength], name="inputX")
self.inputY = tf.placeholder(tf.float32, [None, 1], name="inputY")
self.dropoutKeepProb = tf.placeholder(tf.float32, name="dropoutKeepProb")
# 定義l2損失
l2Loss = tf.constant(0.0)
# 詞嵌入層
with tf.name_scope("embedding"):
# 利用預訓練的詞向量初始化詞嵌入矩陣
self.W = tf.Variable(tf.cast(wordEmbedding, dtype=tf.float32, name="word2vec") ,name="W")
# 利用詞嵌入矩陣將輸入的數據中的詞轉換成詞向量,維度[batch_size, sequence_length, embedding_size]
self.embeddedWords = tf.nn.embedding_lookup(self.W, self.inputX)
# 定義兩層雙向LSTM的模型結構
with tf.name_scope("Bi-LSTM"):
for idx, hiddenSize in enumerate(config.model.hiddenSizes):
with tf.name_scope("Bi-LSTM" + str(idx)):
# 定義前向LSTM結構
lstmFwCell = tf.nn.rnn_cell.DropoutWrapper(tf.nn.rnn_cell.LSTMCell(num_units=hiddenSize, state_is_tuple=True),
output_keep_prob=self.dropoutKeepProb)
# 定義反向LSTM結構
lstmBwCell = tf.nn.rnn_cell.DropoutWrapper(tf.nn.rnn_cell.LSTMCell(num_units=hiddenSize, state_is_tuple=True),
output_keep_prob=self.dropoutKeepProb)
# 採用動態rnn,可以動態的輸入序列的長度,若沒有輸入,則取序列的全長
# outputs是一個元祖(output_fw, output_bw),其中兩個元素的維度都是[batch_size, max_time, hidden_size],fw和bw的hidden_size一樣
# self.current_state 是最終的狀態,二元組(state_fw, state_bw),state_fw=[batch_size, s],s是一個元祖(h, c)
outputs, self.current_state = tf.nn.bidirectional_dynamic_rnn(lstmFwCell, lstmBwCell,
self.embeddedWords, dtype=tf.float32,
scope="bi-lstm" + str(idx))
# 對outputs中的fw和bw的結果拼接 [batch_size, time_step, hidden_size * 2]
self.embeddedWords = tf.concat(outputs, 2)
# 去除最後時間步的輸出作爲全連接的輸入
finalOutput = self.embeddedWords[:, -1, :]
outputSize = config.model.hiddenSizes[-1] * 2 # 因爲是雙向LSTM,最終的輸出值是fw和bw的拼接,因此要乘以2
output = tf.reshape(finalOutput, [-1, outputSize]) # reshape成全連接層的輸入維度
# 全連接層的輸出
with tf.name_scope("output"):
outputW = tf.get_variable(
"outputW",
shape=[outputSize, 1],
initializer=tf.contrib.layers.xavier_initializer())
outputB= tf.Variable(tf.constant(0.1, shape=[1]), name="outputB")
l2Loss += tf.nn.l2_loss(outputW)
l2Loss += tf.nn.l2_loss(outputB)
self.predictions = tf.nn.xw_plus_b(output, outputW, outputB, name="predictions")
self.binaryPreds = tf.cast(tf.greater_equal(self.predictions, 0.5), tf.float32, name="binaryPreds")
# 計算二元交叉熵損失
with tf.name_scope("loss"):
losses = tf.nn.sigmoid_cross_entropy_with_logits(logits=self.predictions, labels=self.inputY)
self.loss = tf.reduce_mean(losses) + config.model.l2RegLambda * l2Loss
參考文獻
- 諸葛越,《百面機器學習》,人民郵電出版社
- Paul J Werbos. Backpropagation through time: what it does and how to do it.
Proceedings of the IEEE, 78(10):1550–1560,1990. - 邱錫鵬, 《神經網絡和深度學習》
- DC童生,深度學習——RNN(2)雙向RNN深度RNN幾種變種,騰訊雲(https://cloud.tencent.com/developer/article/1144238)
- wangduo, 理解 LSTM(Long Short-Term Memory, LSTM) 網絡,博客園(https://www.cnblogs.com/wangduo/p/6773601.html?utm_source=itdadao&utm_medium=referral)