NLP基礎學習9(本次結伴學習完結篇,撒花)--TextRNN

終於來到最後一期,也是目前NLP最常用的架構RNN,及其各種變種。具體來看,本次總結的主要內容:

  1. RNN的結構。循環神經網絡的提出背景、優缺點。着重學習RNN的反向傳播、RNN出現的問題(梯度問題、長期依賴問題)、BPTT算法。
  2. 雙向RNN
  3. LSTM、GRU的結構、提出背景、優缺點。
  4. 針對梯度消失(LSTM等其他門控RNN)、梯度爆炸(梯度截斷)的解決方案。
  5. Text-RNN的原理。
  6. 利用Text-RNN模型來進行文本分類

RNN

循環神經網絡(Recurrent Neural Network,RNN)是用來建模序列化數據的一
種主流深度學習模型 [1]。傳統的前饋神經網絡一般的輸入都是一個定長的向量,無法處理變長的序列信息,即使通過一些方法把序列處理成定長的向量,模型也很難捕捉序列中的長距離依賴關係。RNN則通過將神經元串行起來處理序列化的數據。由於每個神經元能用它的內部變量保存之前輸入的序列信息,因此整個序列被濃縮成抽象的表示,並可以據此進行分類或生成新的序列。近年來,得益於計算能力的大幅提升和模型的改進,RNN在很多領域取得了突破性的進展——機器翻譯、序列標註、圖像描述、推薦系統、智能聊天機器人、自動作詞作曲等。
下圖展示一個典型的RNN的結構:
RNN
由圖可見,一個長度爲T的序列用循環神經網絡建模,展開之後可以看作是一
個T層的前饋神經網絡。其中,第t層的隱含狀態hth_t編碼了序列中前t個輸入的信息,可以通過當前的輸入xtx_t和上一層神經網絡的狀態ht1h_{t−1}計算得到;最後一層的狀態hTh_T編碼了整個序列的信息。以此爲基礎的結構可以應用於多種具體任務。例如,在hTh_T後面直接接一個Softmax層,輸出文本所屬類別的預測概率y,就可以實現文本分類。hth_t和y的計算公式爲:
nett=Uxt+Wht1net_t=Ux_t+Wh_{t−1}
ht=f(nett)h_t=f(net_t)
y=g(VhT)y=g(Vh_T)

其中f和g爲激活函數,U爲輸入層到隱含層的權重矩陣,W爲隱含層從上一時刻到
下一時刻狀態轉移的權重矩陣。在文本分類任務中,f可以選取Tanh函數或者ReLU函數,g可以採用Softmax函數。
通過最小化損失誤差(即輸出的y與真實類別之間的距離),我們可以不斷訓
練網絡,使得得到的循環神經網絡可以準確地預測文本所屬的類別,達到分類目
的。相比於卷積神經網絡等前饋神經網絡,循環神經網絡由於具備對序列順序信
息的刻畫能力,往往能得到更準確的結果。

RNN 的變種有很多,從架構上來看,主要可以總結爲一下4種:
RNN變種
循環神經網絡的參數可以通過梯度下降方法來進行學習。循環神經網絡中存在一個遞歸調用的函數f(·),因此其計算參數梯度的方式和前饋神經網絡不太相同。在循環神經網絡中主要有兩種計算梯度的方式:隨時間反向傳播(BPTT)和實時循環學習(RTRL)算法。
隨時間反向傳播(Backpropagation Through Time,BPTT)算法的主要
思想是通過類似前饋神經網絡的錯誤反向傳播算法[2,3] 來進行計算
梯度。BPTT算法將循環神經網絡看作是一個展開的多層前饋網絡,其中“每一
層”對應循環網絡中的“每個時刻”。這樣,循環神經網絡就可以按
按照前饋網絡中的反向傳播算法進行計算參數梯度。在“展開”的前饋網絡中,
所有層的參數是共享的,因此參數的真實梯度是將所有“展開層”的參數梯度
之和。
因爲參數U 和隱藏層在每個時刻k(1 ≤ k ≤ t) 的淨輸入zk=Uhk1+Wxk+bz_k = Uh_{k−1} + Wx_k + b有關,因此第t 時刻損失的損失函數LtL_t 關於參數UijU_{ij} 的梯度爲:
梯度求導
其中+zkUij\frac{\partial^+z_k}{\partial U_{ij}}表示“直接”偏導數,即公式zk=Uhk1+Wxk+bz_k = Uh_{k−1} +Wx_k + b中保持hk1h_{k−1} 不變,對UijU_{ij} 進行求偏導數,得到
直接偏導
其中[hk1]j[h_{k−1}]j爲第k −1 時刻隱狀態的第j 維;IIi(x)II_i(x)除了第i 行值爲x外,其餘都爲0 的向量。定義δt,k=Ltzk\delta_{t,k} = \frac{\partial L_t}{z_k}爲第t 時刻的損失對第k 時刻隱藏神經層的淨輸入zkz_k 的導數,則:
delatatk
公式合併,並寫出矩陣形式得到:
LtU=k=1tδt,khk1T\frac{\partial L_t}{\partial U}=\sum_{k=1}^t \delta_{t,k}h_{k-1}^T
BPTT由圖表示可得:
BPTT
與反向傳播的BPTT算法不同的是,實時循環學習(Real-Time Recurrent
Learning,RTRL)是通過前向傳播的方式來計算梯度,詳細可參考文獻[3]

雙向RNN和LSTM

Bidirectional RNN(雙向RNN)假設當前t的輸出不僅僅和之前的序列有關,並且 還與之後的序列有關,例如:預測一個語句中缺失的詞語那麼需要根據上下文進 行預測;Bidirectional RNN是一個相對簡單的RNNs,由兩個RNNs上下疊加在 一起組成。輸出由這兩個RNNs的隱藏層的狀態決定 [4]。 具體結構如圖所示:
BiRNN
Long Short Term 網絡—— 一般就叫做 LSTM ——是一種 RNN 特殊的類型,可以學習長期依賴信息。LSTM 由Hochreiter & Schmidhuber (1997)提出,並在近期被Alex Graves進行了改良和推廣。在很多問題,LSTM 都取得相當巨大的成功,並得到了廣泛的使用。
LSTM 通過刻意的設計來避免長期依賴問題。記住長期的信息在實踐中是 LSTM 的默認行爲,而非需要付出很大代價才能獲得的能力!所有 RNN 都具有一種重複神經網絡模塊的鏈式的形式。在標準的 RNN 中,這個重複的模塊只有一個非常簡單的結構,例如一個 tanh 層。
LSTM
LSTM 的關鍵就是細胞狀態,水平線在圖上方貫穿運行。
細胞狀態類似於傳送帶。直接在整個鏈上運行,只有一些少量的線性交互。信息在上面流傳保持不變會很容易。LSTM 有通過精心設計的稱作爲“門”的結構來去除或者增加信息到細胞狀態的能力。門是一種讓信息選擇式通過的方法。他們包含一個 sigmoid 神經網絡層和一個 pointwise 乘法操作。
lstm公式
門控循環單元(Gated Recurrent Unit,GRU)網絡是一種比LSTM網絡更加簡單的循環神經網絡。
在LSTM網絡中,輸入門和遺忘門是互補關係,用兩個門比較冗餘。GRU將輸
入門與和遺忘門合併成一個門:更新門。同時,GRU也不引入額外的記憶單元,
直接在當前狀態hth_t 和歷史狀態ht1h_{t−1} 之間引入線性依賴關係。具體結構如下:
GRU

梯度消失與梯度爆炸

循環神經網絡在學習過程中的主要問題是長期依賴問題,
在BPTT算法中,將δt,k\delta_{t,k} 展開得到:
δt,k=i=kt1(diag(f(zi))UT)δt,t\delta_{t,k} = \prod_{i= k}^{t-1} (diag(f'(z_i))U^T)\delta_{t,t}
如果定義γdiag(f(zi))UT\gamma \cong || diag(f'(z_i))U^T||,則:
δt,k=γtkδt,t\delta_{t,k} = \gamma^{t-k}\delta_{t,t}
γ>1\gamma > 1,當tkt−k → \infty時,γtk\gamma^{t−k} → \infty,會造成系統不穩定,稱爲梯度爆炸問
題(Gradient Exploding Problem);相反,若γ<1\gamma < 1,當tkt−k → \infty時,γtk0\gamma^{t−k} → 0,會出現和深度前饋神經網絡類似的梯度消失問題(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:
其基本結構如圖所示:
TextRNN
參考代碼如下:

# 構建模型
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

參考文獻

  1. 諸葛越,《百面機器學習》,人民郵電出版社
  2. Paul J Werbos. Backpropagation through time: what it does and how to do it.
    Proceedings of the IEEE, 78(10):1550–1560,1990.
  3. 邱錫鵬, 《神經網絡和深度學習》
  4. DC童生,深度學習——RNN(2)雙向RNN深度RNN幾種變種,騰訊雲(https://cloud.tencent.com/developer/article/1144238)
  5. wangduo, 理解 LSTM(Long Short-Term Memory, LSTM) 網絡,博客園(https://www.cnblogs.com/wangduo/p/6773601.html?utm_source=itdadao&utm_medium=referral)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章