序列模型 sequence model
在前面已經(jīng)講到不同的神經(jīng)網(wǎng)絡(luò)架構(gòu)在處理不同類型的任務(wù)時具有不同的表現(xiàn),循環(huán)神經(jīng)網(wǎng)絡(luò) Recurrent neural network 在處理帶有前后序列性質(zhì)的數(shù)據(jù)如自然語言處理、機器翻譯、連續(xù)動作識別等應(yīng)用具有非常明顯的先天優(yōu)勢,RNN 之所以如此命名是因為我們會對序列中的每一個元素執(zhí)行同樣的操作,因此可以視作一種循環(huán)機制。
The RNN performs the same task for each element in the sequence.
在 RNN 中,記憶被定義為前序隱藏層的輸出。
Memory is defined as the output of hidden layer neurons, which will serve as additional input to the network during next training step.
符號約定
為了便于統(tǒng)一符號注解,在后續(xù)的課程中,對于一個輸入序列 x ,x<i> 表示其中的第 i 個元素,y<i> 表示對應(yīng)輸出序列的第 i 個元素,在很多論文中也用 h 來表示輸出。在時序數(shù)據(jù)中,由于引入了長度的概念,因此用 Tx 來表示一個輸入序列的長度,用 Ty 來表示一個輸出序列的長度。對于多個序列來說,x(i)<t> 表示第 i 個輸入中的第 t 個元素,相應(yīng)地,對于第 i 個輸入序列的長度用 Tx(i) 來表示,對于第 i 個輸出序列的長度用 Ty(i) 來表示,值得注意的是在實際應(yīng)用中 Tx(i) 與 Ty(i) 不一定相等。
在自然語言相關(guān)的時序數(shù)據(jù)處理中,為了便于統(tǒng)一的表示序列中的數(shù)據(jù),在處理具體的樣本前要先建立一個詞匯表 Vocabulary 或稱字典 Dictionary,再根據(jù)各個單詞在詞匯表中的索引位置來對輸入樣本中的詞匯進行 one-hot encoding,具體的操作過程在前面的 情感分析 的練習(xí)中已經(jīng)有過接觸和體會,這里就不再贅述。
另外需要注意的一點是如果最終在輸入中出現(xiàn)字典中沒有的輸入詞匯,則可以創(chuàng)建一個符號 Token 如 <UNK> 來標識這些未知詞匯,部分應(yīng)用中還會用 <EOS> 來代表句子的結(jié)束。
循環(huán)神經(jīng)網(wǎng)絡(luò)模型建立
在前面的情感分析練習(xí)中已經(jīng)看到使用標準形式的多層神經(jīng)網(wǎng)絡(luò)在處理類似的語言類任務(wù)時表現(xiàn)并不突出,普通形式的神經(jīng)網(wǎng)絡(luò)在處理序列性質(zhì)的數(shù)據(jù)時面臨的主要問題有:
對于不同的輸入和輸出來說,樣本可能會有不同的長度
與在 CNN 中的情形類似,標準的多層神經(jīng)網(wǎng)絡(luò)不同單元之間無法共享在語句的不同位置學(xué)習(xí)到的特征
典型循環(huán)神經(jīng)網(wǎng)絡(luò)的工作機制
假設(shè)我們按照從左到右的方式處理一個語句,對于輸入語句的第一個單詞或稱元素 x<1>,網(wǎng)絡(luò)的第一步會計算相應(yīng)的預(yù)測 ?<1>,此后對于語句中的第二個輸入元素 x<2> 來說,網(wǎng)絡(luò)在預(yù)測 ?<2> 時會接收上一步的激活信息 a<1>,以此類推。由于網(wǎng)絡(luò)遵循從左到右的順序逐個對語句中的元素進行處理,因此不同單詞的處理之間存在一個時間步 time step 的概念,但對于同一層來說不同時間步之間的參數(shù)是相同的,也即圖中不同時間步中的 Wax 為同一套參數(shù)。由于對于下圖所示的循環(huán)神經(jīng)網(wǎng)絡(luò)來說,時間步在后的網(wǎng)絡(luò)只參考時間步在前的信息,因此這些參考信息的流動是單向的,對于雙向傳遞的循環(huán)神經(jīng)網(wǎng)絡(luò) Bidirectional Recurrent Neural Network,BRNN 后續(xù)會做介紹。
循環(huán)神經(jīng)網(wǎng)絡(luò)的前向傳播
在實際的 RNN 實現(xiàn)中,每一個時間步的 RNN 都可以被看作是一個 2 層的全連接的神經(jīng)網(wǎng)絡(luò),在此基礎(chǔ)上除了可以橫向的多個時間步進行傳遞,還可以將輸出沿縱向傳遞給更深的層次,進而實現(xiàn) RNN 的堆疊。
對于第一個單詞的前續(xù)輸入 a<0> 一般設(shè)置為元素全 0 的向量,此后對于輸入的第一個元素有:
- a<1> = g(Waaa<0> + Waxx<1> + ba)
- ?<1> = g'(Wyaa<1> + by)
更一般地,對于第 t 個時間步來說:
- a<t> = g(Waaa<t-1> + Waxx<t> + ba)
- ?<t> = g'(Wyaa<t> + by)
為了使得前向傳播的公式更加簡潔,上式可以改寫為:
- a<t> = g(Wa [a<t-1>, x<t>] + ba)
- ?<t> = g'(Wya<t> + by)
其中 Wa 為將 Waa 和 Wax 水平并列放置的結(jié)果,即 Wa = [Waa | Wax],而 [a<t-1>, x<t>] 為上下放置的結(jié)果,相應(yīng)的在 Numpy 中可以采用 hstack((Waa, Wax)) 和 vstack((a<t-1>, x<t>)) 來實現(xiàn)。
與在 CNN 中常用 ReLU 做激活函數(shù)不同的是在 RNN 中常用的激活函數(shù)是 tanh,對于預(yù)測部分的激活函數(shù)則可以根據(jù)實際應(yīng)用使用 Sigmoid 或 Softmax 等。
循環(huán)神經(jīng)網(wǎng)絡(luò)的反向傳播
在討論反向傳播之前需要針對任意一個時間步定義一個損失函數(shù),在這里仍然采用交叉熵函數(shù):
- L<t>(?<t>, y<t>) = -y<t>log?<t> - (1 - y<t>)log(1 - ?<t>)
相應(yīng)地,對于整個輸入序列來說則可以定義成本函數(shù)如下:
- L(?, y) = ΣL<t>(?<t>, y<t>), t = 1, 2, 3, ... , Tx
具體的計算過程類似前面講到的神經(jīng)網(wǎng)絡(luò)的反向傳播計算,先計算前向傳播(綠色箭頭),并計算各個節(jié)點的損失函數(shù),最終再通過計算圖計算反向傳播(紅色箭頭),通過梯度下降來完成參數(shù)的更新。由于在序列類型的輸入中引入了時間的概念,因此這個過程中的反向傳播稱為 Backpropagation through time。
不同類型的循環(huán)神經(jīng)網(wǎng)絡(luò)
上面討論的 RNN 中 Tx 與 Ty 是等長的,而現(xiàn)實中的情形不總是如此。對于下面的這些不同類型的序列數(shù)據(jù)來說,有些時候輸入僅是一維的,而對應(yīng)的輸出則是多維的,在另一些情形下輸入有些時候是多維的,而輸出則是一維的。
如果將之前的循環(huán)神經(jīng)網(wǎng)絡(luò)稱為 many-to-many 的架構(gòu),那么對于類似電影情感分析類的多個輸入、單個輸出的 RNN 則可以稱為 many-to-one RNN:
反之對于音樂生成類單個輸入、多個輸出的架構(gòu)則可以稱為 one-to-many RNN:
同時對于 many-to-many RNN 也存在輸入和輸出均為多個但又不相等的情形:
語言建模和序列生成
簡單說來語言建模的目的就是希望可以對于多個不同的語句組合給予一個概率評價,從而確定一個最高概率的語句組合。通過 RNN 進行語言模型的構(gòu)建過程如下:
首先需要獲取一個大型的語言信息訓(xùn)練集,這個集合在自然語言處理的語境中常被稱為語料庫 corpus
在此基礎(chǔ)上需要針對訓(xùn)練集中的詞匯通過 one-hot encoding 的形式將其符號化,以此作為網(wǎng)絡(luò)的輸入
通過對于網(wǎng)絡(luò)的訓(xùn)練確定不同語句及組合方式在尋常的表達中出現(xiàn)的概率,此時的輸出激活函數(shù)需要采用 softmax
使用循環(huán)神經(jīng)網(wǎng)絡(luò)進行語言模型訓(xùn)練時的一個具體的細節(jié)是會令 x<t> = y<t-1>,也即不斷的在后續(xù)的節(jié)點告知前面語句的正確輸入信息,在此基礎(chǔ)上計算在已知輸入為某個詞語的條件下語料庫中所有詞匯出現(xiàn)的概率,其本質(zhì)上是通過網(wǎng)絡(luò)計算一系列的條件概率。
抽樣獲取新序列
在訓(xùn)練完成后,可以通過采用 np.random.choice( ) 在詞匯表中隨機選取詞匯構(gòu)成新的序列組合來對模型進行測試,以直觀的了解模型學(xué)到了什么。
這里需要注意的是在構(gòu)建語言模型的時候,可以構(gòu)建基于詞匯水平的模型,也可以構(gòu)建基于字母水平的模型。在構(gòu)建字母水平的模型時由于會對每一個字母進行分拆因此序列會變得非常的長,并且不如詞匯水平的模型對于句子不同位置的詞匯間的相關(guān)關(guān)系的捕捉能力。
RNN 中的梯度消失問題
由于有時在自然語言中前后詞匯之間的相關(guān)關(guān)系經(jīng)常要間隔較長的詞匯才會出現(xiàn),例如對于主語使用復(fù)數(shù)形式時,在一個從句當中其謂語可能會間隔了幾十個單詞才需要出現(xiàn),此時由于深層網(wǎng)絡(luò)存在的梯度消失問題,前面講到的標準形式的 RNN 就比較難將前面的信息傳遞到后面。
Gated RNN Unit, GRU
為了解決長間隔的序列信息傳遞問題,GRU 單元引入了一個記憶單元 memory cell,記做 C 來在處理信息的同時對重要的信息進行記憶,且在第 t 個時間步有 C<t> = a<t>,在網(wǎng)絡(luò)計算中用 ?<t> = tanh(Wc [C<t-1>, x<t>] + bc) 來做為 C<t> 的備選值,在決定是否采用 ?<t> 來對 C<t> 進行更新時,可以設(shè)置一個 Γu = σ(Wu [C<t-1>, x<t>] + bu) 來計算相應(yīng)的概率值,由于這個 Γ 的取值范圍是 (0, 1) ,并且在實際計算中取值基本為 0 或 1,因此對于是否對 C<t> 進行更新的判斷公式可以總結(jié)為 C<t> = Γu * ?<t> + (1 - Γu) * C<t-1>,最后這一步為基于元素的向量乘法 elementwise。
上述 GRU 單元實際上是一個簡化的版本,在實際實施中更為完整的版本如下:
?<t> = tanh(Wc [ ΓrC<t-1>, x<t>] + bc)
Γr = σ(Wr [C<t-1>, x<t>] + br)
Γu = σ(Wu [C<t-1>, x<t>] + bu)
C<t> = Γu * ?<t> + (1 - Γu) * C<t-1>
公式中添加了 Γr 項以確定 c<t-1> 的重要性 relevance。
長短時記憶 LSTM
另一個專門針對間隔信息傳遞的架構(gòu)設(shè)計是長短時記憶 Long short-term memory,其與 GRU 在實現(xiàn)中最顯著的區(qū)別是 C<t> ≠ a<t>,其主要組成部分及判斷邏輯如下:
Forget Gate - 對應(yīng)下圖中最左側(cè)的邏輯門,其通過對前一個時間步傳遞過來的激活信息 a<t-1> 和當前時間步的輸入 x<t> 的組合信息進行一個邏輯激活來生成一個所有元素處于 0 - 1 之間的向量 Γf = σ(Wf [a<t-1>, x<t>] + bf),再通過這個向量來與從前一個時間步傳遞過來的長期記憶 C<t-1> 進行逐個元素的乘積來判斷 C<t-1> 中有哪些是需要忘記的 Γf * C<t-1>,以此完成對于長期記憶的部分更新
Use Gate - 對應(yīng)下圖中最中間部分的由一個 σ 激活判斷和一個 tanh 激活構(gòu)成的邏輯門,其結(jié)合前一個時間步傳遞過來的激活信息 a<t-1> 和當前時間步的輸入信息 x<t> 來判斷如何進一步更新從前一個時間步傳遞過來的長期記憶 C<t-1>,其實現(xiàn)原理為:
根據(jù) ?<t> = tanh(Wc [ a<t-1>, x<t>] + bc) 來計算一個潛在的可以加入長期記憶的儲備信息
再利用 Γu = σ(Wu [a<t-1>, x<t>] + bu) 進一步同 ?<t> 通過元素相乘判斷這個儲備信息中有多少可以被加入到長期記憶中
最后結(jié)合前兩個邏輯門的工作成果,最終此時間步的長期記憶為:C<t> = Γu * ?<t> + Γf * C<t-1>
Output Gate - 對應(yīng)下圖中最右側(cè)部分的由一個 σ 激活判斷和一個 tanh 激活構(gòu)成的邏輯門,其主要職責(zé)為根據(jù)前一個時間步傳遞過來的激活信息 a<t-1> 和當前時間步的輸入信息 x<t> 來判斷本時間步的長期記憶的激活信息 C<t> 有哪些是需要進行輸出的,相應(yīng)的計算公式為:
Γo = σ(Wo [a<t-1>, x<t>] + bo)
a<t> = Γo * tanh(C<t>)
在文獻中 C<t-1> 和 C<t> 常被稱為 cell state 或 long term memory,而上一個時間步的激活結(jié)果 a<t-1> 則被稱為 working memory。
在實際使用中由于 GRU 的實現(xiàn)更加簡潔,因此也更加適合規(guī)模化,但 LSTM 由于采用了 3 個邏輯門因而更加的靈活,并不能簡單的評價哪一個單元優(yōu)于另外一個,需要根據(jù)應(yīng)用具體選擇。
雙向 RNN
前面已經(jīng)講到在詞匯判斷時,單向 RNN 在某一個時間步只能利用其之前的時間步的信息,而這經(jīng)常是不夠的,雙向 RNN 就是為了解決這個問題而出現(xiàn)的。在雙向 RNN 中輸出預(yù)測結(jié)合了前向傳播和后向傳播的激活信息,二者共同參與輸出判斷。
但 BRNN 的一個重要缺陷就是模型需要整個序列的信息才能做出判斷,這在諸如實時語音識別類的應(yīng)用中就不太適用。
Deep RNNs
如同標準的神經(jīng)網(wǎng)絡(luò)一樣,我們可以把前面的標準 RNN 單元、GRU 單元、LSTM 單元和 BRNN 單元層疊在一起構(gòu)成深度更深的 DRNN 網(wǎng)絡(luò),但由于時間序列的存在,一般 3 層的 DRNN 就已經(jīng)算是比較復(fù)雜的架構(gòu)了。
這里Andrew 為了區(qū)分不同的層,在注釋中用 [ ] 添加了層數(shù),與標準 RNN 單元一致的是在同一層中的參數(shù)是相同的。
RNN in TensorFlow
在 TensorFlow 中,在構(gòu)建 RNN 時有兩個函數(shù):tf.nn.rnn 和 tf.nn.dynamic_rnn,這兩個函數(shù)的區(qū)別是 tf.nn.rnn 創(chuàng)建的是一個固定時間步的非展開狀態(tài)的 RNN 計算圖,因此其要求每一個批次的輸入必須具有相同的步長,而 tf.nn.dynamic_rnn 中不同批次的長度則可以不同,因此在可能的情況下盡量選擇后者。
RNN Cells & Wrappers & Layers
RNN 的基本構(gòu)成單元是一系列的 RNN,GRU 和 LSTM 單元,這一部分很多不同的參考來源對于 cell 和 unit 并沒有嚴格的定義。在 TensorFlow 中 cell 本身可以包含多個基本的 LSTM,GRU 或 RNN 單元,例如在 BasicLSTMCell 定義中有一個參數(shù) num_units 就是定義一個大的、處于同一層中的 LSTM cell 里可以包含多少個基本的 LSTM 單元,并列的多個(取決于時間步的多少) LSTM cell 構(gòu)成一個 LSTM 層,在此基礎(chǔ)上可以通過 MultiRNNCell 定義多個 LSTM 層。
-
BasicRNNCell
– A vanilla RNN cell. -
GRUCell
– A Gated Recurrent Unit cell. -
BasicLSTMCell
– An LSTM cell based on Recurrent Neural Network Regularization. No peephole connection or cell clipping. -
LSTMCell
– A more complex LSTM cell that allows for optional peephole connections and cell clipping. -
MultiRNNCell
– A wrapper to combine multiple cells into a multi-layer cell. -
DropoutWrapper
– A wrapper to add dropout to input and/or output connections of a cell.
cell = tf.contrib.rnn.BasicLSTMCell(num_units=64, state_is_tuple=True) # 包含 64 個 LSTM 基本單元
cell = tf.contrib.rnn.DropoutWrapper(cell=cell, output_keep_prob=0.5)
cell = tf.contrib.rnn.MultiRNNCell(cells=[cell] * 4, state_is_tuple=True) # 4 個 LSTM 層
在完成基本單元構(gòu)造后,推薦使用 dynamic_rnn 來構(gòu)造計算圖,其性能要優(yōu)于 static_rnn:
outputs, state = tf.nn.dynamic_rnn(cell=cell, inputs=data, dtype=tf.float32)
#根據(jù) inputs 中的時間步來展開構(gòu)造計算圖
在 dynamic_rnn 中還有一個參數(shù) sequence_length,其需要指定的是在同一批次的輸入中因為長度不同而采用 padding 時,每一個序列在未添加 padding 時的時間步長,這點在文檔中的解釋是含糊的。