前言
關于 rnn 與 lstm 模型本文不做介紹,詳情去查閱資料過着去看上面的 blog 連結,講的很清楚啦。這篇文章主要是偏向實戰,來自己動手建構 lstm 模型。
資料集來自于外文版《安娜卡列妮娜》書籍的文本文檔(本文後面會提供整個 project 的 git 連結)。
工具介紹
語言:python 3
包:tensorflow 及其它資料處理包(見代碼中)
編輯器:jupyter notebook
線上 gpu:floyd
正文部分
正文部分主要包括以下四個部分:
- 資料預處理:加載資料、轉換資料、分割資料 mini-batch
- 模型建構:輸入層,lstm 層,輸出層,訓練誤差,loss,optimizer
- 模型訓練:設定模型參數對模型進行訓練
- 生成新文本:訓練新的文本
主題:整個文本将基于《安娜卡列妮娜》這本書的英文文本作為 lstm 模型的訓練資料,輸入為單個字元,通過學習整個英文文檔的字元(包括字母和标點符号等)來進行文本生成。在開始模組化之前,我們首先要明确我們的輸入和輸出。即輸入是字元,輸出是預測出的新字元。
一. 資料預處理
在開始模型之前,我們首先要導入需要的包:
這一部分主要包括了資料的轉換與 mini-batch 的分割步驟。
首先我們來進行資料的加載與編碼轉換。由于我們是基于字元(字母和标點符号等單個字元串,以下統稱為字元)進行模型建構,也就是說我們的輸入和輸出都是字元。舉個栗子,假如我們有一個單詞 “hello”,我們想要基于這個單詞建構 lstm,那麼希望的到的結果是,輸入 “h”,預測下一個字母為 “e”;輸入 “e” 時,預測下一個字母為 “l”,等等。
是以我們的輸入便是一個個字母,下面我們将文章進行轉換。
上面的代碼主要完成了下面三個任務:
- 得到了文章中所有的字元集合 vocab
- 得到一個字元 - 數字的映射 vocab_to_int
- 得到一個數字 - 字元的映射 int_to_vocab
- 對原文進行轉碼後的清單 encoded
完成了前面的資料預處理操作,接下來就是要劃分我們的資料集,在這裡我們使用 mini-batch 來進行模型訓練,那麼我們要如何劃分資料集呢?在進行 mini-batch 劃分之前,我們先來了解幾個概念。
假如我們目前手裡有一個序列 1-12,我們接下來以這個序列為例來說明劃分 mini-batch 中的幾個概念。首先我們回顧一下,在 dnn 和 cnn 中,我們都會将資料分 batch 輸入給神經網絡,加入我們有 100 個樣本,如果設定我們的 batch_size=10,那麼意味着每次我們都會向神經網絡輸入 10 個樣本進行訓練調整參數。同樣的,在 lstm 中,batch_size 意味着每次向網絡輸入多少個樣本,在上圖中,當我們設定 batch_size=2 時,我們會将整個序列劃分為 6 個 batch,每個 batch 中有兩個數字。
然而由于 rnn 中存在着 “記憶”,也就是循環。事實上一個循環神經網絡能夠被看做是多個相同神經網絡的疊加,在這個系統中,每一個網絡都會傳遞資訊給下一個。上面的圖中,我們可以看到整個 rnn 網絡由三個相同的神經網絡單元疊加起來的序列。那麼在這裡就有了第二個概念 sequence_length(也叫 steps),中文叫序列長度。上圖中序列長度是 3,可以看到将三個字元作為了一個序列。
有了上面兩個概念,我們來規範一下後面的定義。我們定義一個 batch 中的序列個數為 n(即 batch_size),定義單個序列長度為 m(也就是我們的 num_steps)。那麼實際上我們每個 batch 是一個n×m的數組,相當于我們的每個 batch 中有n×m個字元。在上圖中,當我們設定 n=2, m=3 時,我們可以得到每個 batch 的大小為 2 x 3 = 6 個字元,整個序列可以被分割成 12 / 6 = 2 個 batch。
基于上面的分析,我們下面來進行 mini-batch 的分割:
上面的代碼定義了一個 generator,調用函數會傳回一個 generator 對象,我們可以擷取一個 batch。
經過上面的步驟,我們已經完成了對資料集的預處理。下一步我們開始構模組化型。
二. 模型建構
模型建構部分主要包括了輸入層,lstm 層,輸出層,loss,optimizer 等部分的建構,我們将一塊一塊來進行實作。
1. 輸入層
在資料預處理階段,我們定義了 mini-batch 的分割函數,輸入層的 size 取決于我們設定 batch 的 size(n_seqs × n_steps),下面我們首先建構輸入層。
2.lstm 層
lstm 層是整個神經網絡的關鍵部分。tensorflow 中,tf.contrib.rnn 子產品中有 basiclstmcell 和 lstmcell 兩個包,它們的差別在于:
basiclstmcell does not allow cell clipping, a projection layer, and does not use peep-hole connections: it is the basic baseline.(來自 tensorflow 官網)
在這裡我們僅使用基本子產品 basiclstmcell。
上面的代碼中,我并沒有使用 tf.contrib.rnn 子產品,是因為我在使用遠端 floyd 的 gpu 運作代碼時候告訴我找不到這個子產品,可以用 tf.nn.run_cell.basiclstmcell 替代。建構好 lstm cell 後,為了防止過拟合,在它的隐層添加了 dropout 正則。
3. 輸出層
到目前為止,我們的輸入和 lstm 層都已經建構完畢。接下來就要構造我們的輸出層,輸出層采用 softmax,它與 lstm 進行全連接配接。對于每一個字元來說,它經過 lstm 後的輸出大小是1×l(l 為 lstm cell 隐層的結點數量),我們上面也分析過輸入一個 n x m 的 batch,我們從 lstm 層得到的輸出為n×m×l,要将這個輸出與 softmax 全連接配接層建立連接配接,就需要對 lstm 的輸出進行重塑,變成( n * m ) × l 的一個 2d 的 tensor。softmax 層的結點數應該是 vocab 的大小(我們要計算機率分布)。是以整個 lstm 層到 softmax 層的大小為l×vocab_size。
将資料重塑後,我們對 lstm 層和 softmax 層進行連接配接。并計算 logits 和 softmax 後的機率分布。
4. 訓練誤差計算
5.optimizer
我們知道 rnn 會遇到梯度爆炸(gradients exploding)和梯度彌散(gradients disappearing) 的問題。lstm 解決了梯度彌散的問題,但是 gradients 仍然可能會爆炸,是以我們采用 gradient clippling 的方式來防止梯度爆炸。即通過設定一個門檻值,當 gradients 超過這個門檻值時,就将它重置為門檻值大小,這就保證了梯度不會變得很大。
6. 模型組合
經過上面五個步驟,我們完成了所有的子產品設定。下面我們來将這些部分組合起來,建構一個類。
我們使用 tf.nn.dynamic_run 來運作 rnn 序列。
三. 模型訓練
在模型訓練之前,我們首先初始化一些參數,我們的參數主要有:
batch_size: 單個 batch 中序列的個數
num_steps: 單個序列中字元數目
lstm_size: 隐層結點個數
num_layers: lstm 層個數
learning_rate: 學習率
keep_prob: 訓練時 dropout 層中保留結點比例
參數設定完畢後,離運作整個 lstm 就差一步啦,下面我們來運作整個模型。
我這裡設定的疊代次數為 20 次,并且在代碼運作中我們設定了結點的儲存,設定了每運作 200 次進行一次變量儲存,這樣的好處是有利于我們後面去直覺地觀察在整個訓練過程中文本生成的結果是如何一步步 “進化” 的。
四. 文本生成
經過漫長的模型訓練,我們得到了一系列訓練過程中儲存下來的參數,可以利用這些參數來進行文本生成啦。當我們輸入一個字元時,它會預測下一個,我們再将這個新的字元輸入模型,就可以一直不斷地生成字元,進而形成文本。
為了減少噪音,每次的預測值我會選擇最可能的前 5 個進行随機選擇,比如輸入 h,預測結果機率最大的前五個為[o,i,e,u,b],我們将随機從這五個中挑選一個作為新的字元,讓過程加入随機因素會減少一些噪音的生成。
代碼封裝了兩個函數來做文本生成,具體請參看文章尾部的 git 連結中的源碼。
訓練步數:200
當訓練步數為 200 的時候,lstm 生成的文本大概長下面這個樣子:
看起來像是字元的随機組合,但是可以看到有一些單詞例如 hat,her 等已經出現,并且生成了成對的引号。
訓練步數:1000
當訓練步數到達 1000 的時候,已經開始有簡單的句子出現,并且單詞看起來似乎不是那麼亂了。
訓練步數:2000
當訓練步數達到 2000 的時候,單詞和句子看起來已經有所規範。
訓練步數:3960
當訓練結束時(本文僅訓練了 3960 步),生成的文本已經有小部分可以讀的比較通順了,而且很少有單詞拼寫的錯誤。
五. 總結
整個文章通過建構 lstm 模型完成了對《安娜卡列甯娜》文本的學習并且基于學習成果生成了新的文本。
通過觀察上面的生成文本,我們可以看出随着訓練步數的增加,模型的訓練誤差在持續減少。本文僅設定了 20 次疊代,嘗試更大次數的疊代可能會取得更好的效果。
個人覺得 lstm 對于文本的學習能力還是很強,後面可能将針對中文文本構造一些學習模型,應該會更有意思!
我對 rnn 也是在不斷地探索與學習中,文中不免會有一些錯誤和謬誤,懇請各位指正,非常感謝!
----------------------------------------------------------------------------------------------------------
如果覺得有用,請叫上全村兒的小夥伴兒幫我 star 一下,不勝感激!
====================================分割線================================
本文作者:ai研習社