使用PaddleFluid和TensorFlow训练RNN语言模型
專欄介紹:Paddle Fluid 是用來讓用戶像 PyTorch 和 Tensorflow Eager Execution 一樣執行程序。在這些系統中,不再有模型這個概念,應用也不再包含一個用于描述 Operator 圖或者一系列層的符號描述,而是像通用程序那樣描述訓練或者預測的過程。
本專欄將推出一系列技術文章,從框架的概念、使用上對比分析 TensorFlow 和 Paddle Fluid,為對 PaddlePaddle 感興趣的同學提供一些指導。
在圖像領域,最流行的 building block 大多以卷積網絡為主。上一篇我們介紹了如何在 PaddleFluid 和 TensorFlow 上訓練圖像分類任務。卷積網絡本質上依然是一個前饋網絡,在神經網絡基本單元中循環神經網絡是建模序列問題最有力的工具, 有著非常重要的價值。自然語言天生是一個序列,在自然語言處理領域(Nature Language Processing,NLP)中,許多經典模型都基于循環神經網絡單元。可以說自然語言處理領域是 RNN 的天下。
這一篇以 NLP 領域的 RNN 語言模型(RNN Language Model,RNN LM)為實驗任務,對比如何使用 PaddleFluid 和 TensorFlow 兩個平臺實現序列模型。 這一篇中我們會看到 PaddleFluid 和 TensorFlow 在處理序列輸入時有著較大的差異:PaddleFluid 默認支持非填充的 RNN 單元,在如何組織 mini-batch 數據提供序列輸入上也簡化很多。
如何使用代碼
本篇文章配套有完整可運行的代碼, 請從隨時從 github [1] 上獲取最新代碼。代碼包括以下幾個文件:
注意:在運行模型訓練之前,請首先進入 data 文件夾,在終端運行 sh download.sh 下載訓練數據。?
在終端運行以下命令便可以使用默認結構和默認參數運行 PaddleFluid 訓練 RNN LM。
在終端運行以下命令便可以使用默認結構和默認參數運行 TensorFlow 訓練 RNN LM。
背景介紹
one-hot和詞向量表示法?
計算機如何表示語言是處理 NLP 任務的首要問題。這里介紹將會使用到的 one-hot 和詞向量表示法。?
one-hot 表示方法:一個編碼單元表示一個個體,也就是一個詞。于是,一個詞被表示成一個長度為字典大小的實數向量,每個維度對應字典里的一個詞,除了該詞對應維度上的值是 1,其余維度都是 0。?
詞向量表示法:與 one-hot 表示相對的是 distributed representation ,也就是常說的詞向量:用一個更低維度的實向量表示詞語,向量的每個維度在實數域 RR 取值。?
在自然語言處理任務中,一套好的詞向量能夠提供豐富的領域知識,可以通過預訓練獲取,或者與最終任務端到端學習而來。?
循環神經網絡?
循環神經網絡(Recurrent Neural Network)是一種對序列數據建模的重要單元,模擬了離散時間(這里我們只考慮離散時間)動態系統的狀態演化。“循環” 兩字刻畫了模型的核心:上一時刻的輸出作為下一個時刻的輸入,始終留在系統中如下面的圖 1 所示,這種循環反饋能夠形成復雜的歷史。自然語言是一個天生的序列輸入,RNN 恰好有能力去刻畫詞匯與詞匯之間的前后關聯關系,因此,在自然語言處理任務中占有重要的地位。
▲?圖1. 最簡單的RNN單元
RNN 形成“循環反饋” 的過程是一個函數不斷復合的過程,可以等價為一個層數等于輸入序列長度的前饋神經網絡,如果輸入序列有 100 個時間步,相當于一個 100 層的前饋網絡,梯度消失和梯度爆炸的問題對 RNN 尤為嚴峻。
直覺上大于 1 的數連乘越乘越大,極端時會引起梯度爆炸;小于 1 的數連乘越乘越小,極端時會引起梯度消失。梯度消失也會令在循環神經網絡中,后面時間步的信息總是會”壓過”前面時間步。如果 t 時刻隱層狀態依賴于 t 之前所有時刻,梯度需要通過所有的中間隱層逐時間步回傳,這會形成如圖 2 所示的一個很深的求導鏈。
▲?圖2. t時刻依賴t時刻之前所有時刻
在許多實際問題中時間步之間相互依賴的鏈條并沒有那么長,t 時刻也許僅僅依賴于它之前有限的若干時刻。很自然會聯想到:如果模型能夠自適應地學習出一些如圖 3 所示的信息傳播捷徑來縮短梯度的傳播路徑,是不是可以一定程度減梯度消失和梯度爆炸呢?答案是肯定的,這也就是 LSTM 和 GRU 這類帶有 “門控”思想的神經網絡單元。
▲?圖3. 自適應地形成一些信息傳播的“捷徑”
關于 LSTM 更詳細的介紹請參考文獻 [2],這里不再贅述,只需了解 LSTM/GUR 這些門控循環神經網絡單元提出的動機即可。
RNN LM?
語言模型是 NLP 領域的基礎任務之一。語言模型是計算一個序列的概率,判斷一個序列是否屬于一個語言的模型,描述了這樣一個條件概率,其中是輸入序列中的 T 個詞語,用 one-hot 表示法表示。
言模型顧名思義是建模一種語言的模型,這一過程如圖 4 所示:
▲?圖4. RNN語言模型
RNN LM的工作流程如下:?
1. 給定一段 one-hot 表示的輸入序列 {x1,x2,...,xT},將它們嵌入到實向量空間,得到詞向量表示 :{ω1,ω2,...,ωt}。?
2. 以詞向量序列為輸入,使用 RNN 模型(可以選擇LSTM或者GRU),計算輸入序列到 t 時刻的編碼 ht。?
3. softmax 層以 ht 為輸入,預測下一個最可能的詞的概率。?
4.?,根據和計算誤差信號。
PTB數據集介紹
至此,介紹完 RNN LM 模型的原理和基本結構,下面準備開始分別使用 PaddleFluid 和 TensorFlow 來構建我們的 訓練任務。這里首先介紹這一篇我們使用 Mikolov 與處理過的 PTB 數據,這是語言模型任務中使用最為廣泛的公開數據之一。 PTB 數據集包含 10000 個不同的詞語(包含句子結束符 <eos> ,以及表示 低頻詞的特殊符號 <unk> )。?
通過運行 data 目錄下的 download.sh 下載數據,我們將使用其中的 ptb.train.txt 文件進行訓練,文件中一行是一句話,文本中的低頻詞已經全部被替換為 <unk> 預處理時我們會在 每一行的末尾附加上句子結束符 <e> 。
程序結構
這一節我們首先整體總結一下使用 PaddleFluid 平臺和 TensorFlow 運行自己的神經網絡模型都有哪些事情需要完成。
PaddleFluid?
1. 調用 PaddleFluid API 描述神經網絡模型。PaddleFluid 中一個神經網絡訓練任務被稱之為一段 Fluid Program 。?
2. 定義 Fluid Program 執行設備: place 。常見的有 fluid.CUDAPlace(0) 和 fluid.CPUPlace()?
注:PaddleFluid 支持混合設備運行,一些運算(operator)沒有特定設備實現,或者為了提高全局資源利用率,可以為他們指定不同的計算設備。?
3. 創建 PaddleFluid 執行器(Executor),需要為執行器指定運行設備。
讓執行器執行 fluid.default_startup_program() ,初始化神經網絡中的可學習參數,完成必要的初始化工作。?
5. 定義 DataFeeder,編寫 data reader,只需要關注如何返回一條訓練/測試數據。?
6. 進入訓練的雙層循環(外層在 epoch 上循環,內層在 mini-batch 上循環),直到訓練結束。
TensorFlow?
1. 調用 TensorFlow API 描述神經網絡模型。 TensorFlow 中一個神經網絡模型是一個 Computation Graph。?
2. 創建TensorFlow Session用來執行計算圖。
3. 調用 sess.run(tf.global_variables_initializer()) 初始化神經網絡中的可學習參數。
4. 編寫返回每個 mini-batch 數據的數據讀取腳本。
5. 進入訓練的雙層循環(外層在 epoch 上循環,內層在 mini-batch 上循環),直到訓練結束。
如果不顯示地指定使用何種設備進行訓練,TensorFlow 會對機器硬件進行檢測(是否有 GPU), 選擇能夠盡可能利用機器硬件資源的方式運行。
從以上的總結中可以看到,PaddleFluid 程序和 TensorFlow 程序的整體結構非常相似,使用經驗可以非常容易的遷移。
構建網絡結構及運行訓練
加載訓練數據
PaddleFluid?
定義 輸入data layers
PaddleFluid 模型通過 fluid.layers.data 來接收輸入數據。圖像分類網絡以圖片以及圖片對應的類別標簽作為網絡的輸入:
????????name="current_word",?shape=[1],?dtype="int64",?lod_level=1)
lbl?=?fluid.layers.data(
????????name="next_word",?shape=[1],?dtype="int64",?lod_level=1)
1. 定義 data layer 的核心是指定輸入 Tensor 的形狀( shape )和類型。?
2. RNN LM 使用 one-hot 作為輸入,一個詞用一個和字典大小相同的向量表示,每一個位置對應了字典中的 一個詞語。one-hot 向量僅有一個維度為 1, 其余全部為 0。因此為了節約存儲空間,通常都直接用一個整型數表示給出詞語在字典中的 id,而不是真的創建一個和詞典同樣大小的向量 ,因此在上面定義的 data layer 中 word 和 lbl 的形狀都是 1,類型是 int64 。?
3. 需要特別說明的是,實際上 word 和?lbl 是兩個 [batch_size x 1] 的向量,這里的 batch size 是指一個 mini-batch 中序列中的總詞數。對序列學習任務, mini-batch 中每個序列長度 總是在發生變化,因此實際的 batch_size 只有在運行時才可以確定。 batch size 總是一個輸入 Tensor 的第 0 維,在 PaddleFluid 中指定 data layer 的 shape 時,不需要指定 batch size 的大小,也不需要考慮占位。框架會自動補充占位符,并且在運行時 設置正確的維度信息。因此,上面的兩個 data layer 的 shape 都只需要設置第二個維度,也就是 1。
LoD Tensor和Non-Padding的序列輸入?
與前兩篇文章中的任務相比,在上面的代碼片段中定義 data layer 時,出現了一個新的 lod_level 字段,并設置為 1。這里就要介紹在 Fluid 系統中表示序列輸入的一個重要概念 LoDTensor。?
那么,什么是 LoD(Level-of-Detail) Tensor 呢??
1. Tensor 是 nn-dimensional arry 的推廣,LoDTensor 是在 Tensor 基礎上附加了序列信息。?
2. Fluid 中輸入、輸出,網絡中的可學習參數全部統一使用 LoDTensor(n-dimension array)表示,對非序列數據,LoD 信息為空。一個 mini-batch 輸入數據是一個 LoDTensor。?
3. 在 Fluid 中,RNN 處理變長序列無需 padding,得益于 LoDTensor表示。?
4. 可以簡單將 LoD 理解為:std::vector<std::vector>。
下圖是 LoDTensor 示意圖(圖片來自 Paddle 官方文檔):
▲?圖5. LoD Tensor示意圖
LoD 信息是附著在一個 Tensor 的第 0 維(也就是 batch size 對應的維度),來對一個 batch 中的數據進一步進行劃分,表示了一個序列在整個 batch 中的起始位置。?
LoD 信息可以嵌套,形成嵌套序列。例如,NLP 領域中的段落是一種天然的嵌套序列,段落是句子的序列,句子是詞語的序列。?
LoD 中的 level 就表示了序列信息的嵌套:
圖 (a) 的 LoD 信息 [0, 5, 8, 10, 14] :這個 batch 中共含有 4 條序列。
圖 (b) 的 LoD 信息 [[0, 5, 8, 10, 14] /*level=1*/, [0, 2, 3, 5, 7, 8, 10, 13, 14] /*level=2*/] :這個 batch 中含有嵌套的雙層序列。
有了 LoDTensor 這樣的數據表示方式,用戶不需要對輸入序列進行填充,框架會自動完成 RNN 的并行計算處理。
如何構造序列輸入信息?
明白了 LoD Tensor 的概念之后,另一個重要的問題是應該如何構造序列輸入。在 PaddleFluid 中,通過 DataFeeder 模塊來為網絡中的 data layer 提供數據,調用方式如下面的代碼所示:
????????paddle.reader.shuffle(train_data,?buf_size=51200),
????????batch_size=conf.batch_size)
place?=?fluid.CUDAPlace(0)?if?conf.use_gpu?else?fluid.CPUPlace()
feeder?=?fluid.DataFeeder(feed_list=[word,?lbl],?place=place)
觀察以上代碼,需要用戶完成的僅有:編寫一個實現讀取一條數據的 python 函數: train_data 。 train_data 的代碼非常簡單,我們再來看一下它的具體實現 [3]:
????data_path?=?os.path.join(data_dir,?"ptb.train.txt")
????_,?word_to_id?=?build_vocab(data_path)
????with?open(data_path,?"r")?as?ftrain:
????????for?line?in?ftrain:
????????????words?=?line.strip().split()
????????????word_ids?=?[word_to_id[w]?for?w?in?words]
????????????yield?word_ids[0:-1],?word_ids[1:]
在上面的代碼中:?
1. train_data 是一個 python generator ,函數名字可以任意指定,無需固定。?
2. train_data 打開原始數據數據文件,讀取一行(一行既是一條數據),返回一個 python list,這個 python list 既是序列中所有時間步。具體的數據組織方式如下表所示(其中,f 代表一個浮點數,i 代表一個整數):
3. paddle.batch() 接口用來構造 mini-batch 輸入,會調用 train_data 將數據讀入一個 pool 中,對 pool 中的數據進行 shuffle,然后依次返回每個 mini-batch 的數據。
TensorFlow?
TensorFlow 中使用占位符 placeholder 接收 訓練數據,可以認為其概念等價于 PaddleFluid 中的 data layer。同樣的,我們定義了如下兩個 placeholder 用于接收當前詞與下一個詞語:
????self._inputs?=?tf.placeholder(tf.int32,
??????????????????????????????????[None,?self.max_sequence_length])
????self._targets?=?tf.placeholder(tf.int32,?[None,?self.vocab_size])
1. placeholder 只存儲一個 mini-batch 的輸入數據。與 PaddleFluid 中相同, _inputs 這里接收的是 one-hot 輸入,也就是該詞語在詞典中的 index,one-hot 表示 會進一步通過此詞向量層的作用轉化為實值的詞向量表示。
2. 需要注意的是,TensorFlow 模型中網絡輸入數據需要進行填充,保證一個 mini-batch 中序列長度 相等。也就是一個 mini-batch 中的數據長度都是 max_seq_length ,這一點與 PaddleFluid 非常不同。?
通常做法 是對不等長序列進行填充,在這一篇示例中我們使用一種簡化的做法,每條訓練樣本都按照 max_sequence_length 來切割,保證一個 mini-batch 中的序列是等長的。?
于是, _input 的 shape=[batch_size, max_sequence_length]?。 max_sequence_length 即為 RNN 可以展開長度。
構建網絡結構?
PaddleFluid?RNN LM?
這里主要關注最核心的 LSTM 單元如何定義:
????for?i?in?range(self.num_layers):
????????hidden?=?fluid.layers.fc(
????????????size=self.hidden_dim?*?4,
????????????bias_attr=fluid.ParamAttr(
????????????????initializer=NormalInitializer(loc=0.0,?scale=1.0)),
????????????input=hidden?if?i?else?input)
????????lstm?=?fluid.layers.dynamic_lstm(
????????????input=hidden,
????????????size=self.hidden_dim?*?4,
????????????candidate_activation="tanh",
????????????gate_activation="sigmoid",
????????????cell_activation="sigmoid",
????????????bias_attr=fluid.ParamAttr(
????????????????initializer=NormalInitializer(loc=0.0,?scale=1.0)),
????????????is_reverse=False)
????return?lstm
PaddleFluid 中的所有 RNN 單元(RNN/LSTM/GRU)都支持非填充序列作為輸入,框架會自動完成不等長序列的并行處理。當需要堆疊多個 LSTM 作為輸入時,只需利用 Python 的 for 循環語句,讓一個 LSTM 的輸出成為下一個 LSTM 的輸入即可。在上面的代碼片段中有一點需要特別注意:PaddleFluid 中的 LSTM 單元是由 fluid.layers.fc+ fluid.layers.dynamic_lstm 共同構成的。
▲?圖6. LSTM計算公式
圖 6 是 LSTM 計算公式,圖中用紅色圈起來的計算是每一時刻輸入矩陣流入三個門和 memory cell 的之前的映射。PaddleFluid 將這個四個矩陣運算合并為一個大矩陣一次性計算完畢, fluid.layers.dynamic_lstm 不包含這部分運算。因此:?
1. PaddleFluid 中的 LSTM 單元是由 fluid.layers.fc + fluid.layers.dynamic_lstm 。?
2. 假設 LSTM 單元的隱層大小是 128 維, fluid.layers.fc 和 fluid.layers.dynamic_lstm 的 size 都應該設置為 128 * 4,而不是 128。?
TensorFlow RNN LM?
這里主要關注最核心的 LSTM 單元如何定義:
????def?lstm_cell():
????????return?tf.contrib.rnn.BasicLSTMCell(
????????????self.hidden_dim,?state_is_tuple=True)
????cells?=?[lstm_cell()?for?_?in?range(self.num_layers)]
????cell?=?tf.contrib.rnn.MultiRNNCell(cells,?state_is_tuple=True)
????_inputs?=?self.input_embedding()
????_outputs,?_?=?tf.nn.dynamic_rnn(
????????cell=cell,?inputs=_inputs,?dtype=tf.float32)
????last?=?_outputs[:,?-1,?:]
????logits?=?tf.layers.dense(inputs=last,?units=self.vocab_size)
????prediction?=?tf.nn.softmax(logits)
?tf.nn.rnn_cell.BasicLSTMCell(n_hidden, state_is_tuple=True) : 是最基本的 LSTM 單元。 n_hidden 表示 LSTM 單元隱層大小。 state_is_tuple=True 表示返回的狀態用一個元祖表示。?
?tf.contrib.rnn.MultiRNNCell : 用來 wrap 一組序列調用的 RNN 單元的 wrapper。?
?tf.nn.dynamic_rnn :?通過指定 mini-batch 中序列的長度,可以跳過 padding 部分的計算,減少計算量。這一篇的例子中由于我們對輸入數據進行了處理,將它們都按照 max_sequence_length 切割。?
但是, dynamic_rnn 可以讓不同 mini-batch 的 batch size 長度不同,但同一次迭代一個 batch 內部的所有數據長度仍然是固定的。
運行訓練?
運行訓練任務對兩個平臺都是常規流程,可以參考上文在程序結構一節介紹的流程,以及代碼部分:PaddleFluid vs. TensorFlow,這里不再贅述。
總結
這一篇我們第一次接觸 PaddleFluid 和 TensorFlow 平臺的序列模型。了解 PaddleFluid 和 TensorFlow 在接受序列輸入,序列處理策略上的不同。序列模型是神經網絡模型中較為復雜的一類模型結構,可以衍生出非常復雜的模型結構。?
不論是 PaddleFluid 以及 TensorFlow 都實現了多種不同的序列建模單元,如何選擇使用這些不同的序列建模單元有很大的學問。到目前為止平臺使用的一些其它重要主題:例如多線程多卡,如何利用混合設備計算等我們還尚未涉及。接下來的篇章將會繼續深入 PaddleFluid 和 TensorFlow 平臺的序列模型處理機制,以及更多重要功能如何在兩個平臺之間實現。
參考文獻
[1]. 本文配套代碼
https://github.com/JohnRabbbit/TF2Fluid/tree/master/03_rnnlm
[2].?Understanding LSTM Networks
http://colah.github.io/posts/2015-08-Understanding-LSTMs/
[3].?train_data具體實現
https://github.com/JohnRabbbit/TF2Fluid/blob/master/03_rnnlm/load_data_fluid.py
關于PaperWeekly
PaperWeekly 是一個推薦、解讀、討論、報道人工智能前沿論文成果的學術平臺。如果你研究或從事 AI 領域,歡迎在公眾號后臺點擊「交流群」,小助手將把你帶入 PaperWeekly 的交流群里。
▽ 點擊 |?閱讀原文?| 加入社區刷論文
總結
以上是生活随笔為你收集整理的使用PaddleFluid和TensorFlow训练RNN语言模型的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 直播|百度AI开发者大会深度学习直播课程
- 下一篇: 上海科技大学ACL2018高分论文:混合