一篇文章教你用TensorFlow寫名著
前言
最近看完了 LSTM 的一些外文資料,主要參考了Colah 的 blog以及Andrej Karpathy blog的一些關於 RNN 和 LSTM 的材料,準備動手去實現一個 LSTM 模型。代碼的基礎框架來自於 Udacity 上深度學習納米學位的課程(付費課程)的一個 demo,我剛開始看代碼的時候真的是一頭霧水,很多東西沒有理解,後來反覆查閱資料,並我重新對代碼進行了學習和修改,對步驟進行了進一步的剖析,下面將一步步用 TensorFlow 來構建 LSTM 模型進行文本學習並試圖去生成新的文本。本篇文章比較適合新手去操作,LSTM 層採用的是 BasicLSTMCell。
關於 RNN 與 LSTM 模型本文不做介紹,詳情去查閱資料過著去看上面的 blog 鏈接,講的很清楚啦。這篇文章主要是偏向實戰,來自己動手構建 LSTM 模型。
數據集來自於外文版《安娜卡列妮娜》書籍的文本文檔(本文後面會提供整個 project 的 git 鏈接)。
工具介紹
正文部分
正文部分主要包括以下四個部分:
- 數據預處理:載入數據、轉換數據、分割數據 mini-batch
- 模型構建:輸入層,LSTM 層,輸出層,訓練誤差,loss,optimizer
- 模型訓練:設置模型參數對模型進行訓練
- 生成新文本:訓練新的文本
主題:整個文本將基於《安娜卡列妮娜》這本書的英文文本作為 LSTM 模型的訓練數據,輸入為單個字元,通過學習整個英文文檔的字元(包括字母和標點符號等)來進行文本生成。在開始建模之前,我們首先要明確我們的輸入和輸出。即輸入是字元,輸出是預測出的新字元。
一. 數據預處理
在開始模型之前,我們首先要導入需要的包:
import timeimport numpy as npimport tensorflow as tf
這一部分主要包括了數據的轉換與 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),下面我們首先構建輸入層。
同樣的,輸出層的(因為輸入一個字元,同樣會輸出一個字元)。除了輸入輸出外,我們還定義了 keep_prob 參數用來在後面控制 dropout 的保留結點數。關於 dropout 正則化請參考鏈接。
2.LSTM 層
BasicLSTMCell does not allow cell clipping, a projection layer, and does not use peep-hole connections: it is the basic baseline.(來自 TensorFlow 官網)
在這裡我們僅使用基本模塊 BasicLSTMCell。
後面的MultiRNNCell實現了對基本 LSTM cell 的順序堆疊,它接收的是 cell 對象組成的 list。最後 initial_state 定義了初始 cell state。
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. 訓練誤差計算
至此我們已經完成了整個網路的構建,接下來要定義 train loss 和 optimizer。我們知道從 sotfmax 層輸出的是概率分布,因此我們要對 targets 進行 one-hot 編碼。我們採用softmax_cross_entropy_with_logits交叉熵來計算 loss。
5.Optimizer
我們知道 RNN 會遇到梯度爆炸(gradients exploding)和梯度彌散(gradients disappearing)的問題。LSTM 解決了梯度彌散的問題,但是 gradients 仍然可能會爆炸,因此我們採用 gradient clippling 的方式來防止梯度爆炸。即通過設置一個閾值,當 gradients 超過這個閾值時,就將它重置為閾值大小,這就保證了梯度不會變得很大。
tf.clip_by_global_norm會返回 clip 以後的 gradients 以及 global_norm。整個學習過程採用 AdamOptimizer
6. 模型組合
經過上面五個步驟,我們完成了所有的模塊設置。下面我們來將這些部分組合起來,構建一個類。
三. 模型訓練
在模型訓練之前,我們首先初始化一些參數,我們的參數主要有:
batch_size: 單個 batch 中序列的個數
num_steps: 單個序列中字元數目
lstm_size: 隱層結點個數
num_layers: LSTM 層個數
learning_rate: 學習率
keep_prob: 訓練時 dropout 層中保留結點比例
這是我自己設置的一些參數,具體一些調參經驗可以參考Andrej Karpathy 的 git 上的建議。
參數設置完畢後,離運行整個 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 也是在不斷地探索與學習中,文中不免會有一些錯誤和謬誤,懇請各位指正,非常感謝!
--------------------------------------------------------
整個項目地址已經上傳到個人的GitHub上。
如果覺得有用,請叫上全村兒的小夥伴兒幫我 star 一下,不勝感激!
點擊展開全文
※如何在微服務架構下構建高效的運維管理平台?
※究竟什麼是神經網路?這或許是最簡單有趣的解釋
TAG:唯物 |
※文章 For Youself
※一篇文章教會你 Event loop——瀏覽器和 Node
※一篇文章搞定Markdown
※文章寫作之Introduction
※SCI喜歡的文章該怎麼寫-How to write your abstract
※GCD和Operation/OperationQueue 看這一篇文章就夠了
※每日摘要:兩篇Nature Genetics玉米基因組文章
※轟動性成果,6篇Nature Genetics及2篇Nature Biotec文章同時闡述棉花基因組學進展
※恭喜韓亨達博士文章被Fuel Processing Technolog接收!
※專訪Catherine Otto—從BMJ子刊Heart主編的角度看文章發表
※Vogue編輯Suzy Menkes就其關於DG的評論文章致歉
※這篇文章告訴你iPhone XR與iPhone X該怎麼選
※這篇文章,要給大家介紹一位美女,她叫Paola Antonini
※WordPress文章防複製代碼
※Springer Nature 混合型期刊開放獲取文章影
※一篇文章帶你了解運維重器 Tencent Hub
※Twitter 收購文章精華分享應用 Highly
※Oculus 聯合創始人發表博客文章:Magic Leap 就是一個悲劇
※「CRISPR致非目標突變」文章爭議大,《Nature Methods》終撤稿
※爬蟲培訓更新:增加文章《Headless Chrome起步》