項目地址:https://github.com/ZMpursue/LSTM_Write_Poetrypython
本教程將經過一個示例對LSTM進行介紹。經過搭建訓練LSTM網絡,咱們將訓練一個模型來生成唐詩。本文將對該實現進行詳盡的解釋,並闡明此模型的工做方式和緣由。並不須要過多專業知識,可是可能須要新手花一些時間來理解的模型訓練的實際狀況。爲了節省時間,請儘可能選擇GPU進行訓練。git
下載安裝命令 ## CPU版本安裝命令 pip install -f https://paddlepaddle.org.cn/pip/oschina/cpu paddlepaddle ## GPU版本安裝命令 pip install -f https://paddlepaddle.org.cn/pip/oschina/gpu paddlepaddle-gpu
1 簡介
本項目基於paddlepaddle2.0,結合長短時間記憶(Long short-term memory, LSTM),以唐詩爲數據集經過監督學習的方式,訓練生成唐詩。github
參考文檔:http://colah.github.io/posts/2015-08-Understanding-LSTMs/網絡
1.1 LSTM解決什麼問題?
RNN突出的優勢之一就是能夠用來鏈接先前的信息到當前的任務上,有時候,咱們僅僅須要知道先前的信息來執行當前的任務。例如,咱們有一個語言模型用來基於先前的詞來預測下一個詞。若是試着預測 「the clouds are in the ___ 」 最後的詞,咱們並不須要任何其餘的上下文下一個詞顯然就應該是sky。在這樣的場景中,相關的信息和預測的詞位置之間的間隔是很是小的,RNN 能夠充分使用先前信息來預測。app
可是一樣會有一些更加複雜的場景。假設咱們試着去預測「I grew up in France… I speak fluent French」最後的詞。當前的信息建議下一個詞多是一種語言的名字,可是若是咱們須要弄清楚是什麼語言,咱們是須要先前提到的離當前位置很遠的 France 的上下文的。這說明相關信息和當前預測位置之間的間隔就確定變得至關的大,在這個間隔不斷增大時,RNN 會喪失學習到鏈接如此遠的信息的能力。svg
LSTM的出現爲了解決長序列訓練過程當中的梯度消失和梯度爆炸問題。簡單來講,相比普通的RNN,LSTM可以在更長的序列中有更好的表現。函數
1.2 什麼是LSTM?
Long Short Term 網絡,通常就叫作 LSTM ——是一種 RNN 特殊的類型,能夠學習長期依賴信息。LSTM 由Hochreiter & Schmidhuber (1997)提出,並在近期被Alex Graves進行了改良和推廣。在不少問題,LSTM 都取得至關巨大的成功,並獲得了普遍的使用。
LSTM 經過專門的設計來避免長期依賴問題。
全部 RNN 都具備一種重複神經網絡模塊的鏈式的形式。在標準的 RNN 中,這個重複的模塊只有一個很是簡單的結構,例如一個 tanh 層。post
LSTM 一樣是這樣的結構,可是重複的模塊擁有一個不一樣的結構。不一樣於 單一神經網絡層,這裏是有四個,以一種很是特殊的方式進行交互。學習
如今,咱們先來熟悉一下圖中使用的各類元素的圖標。優化
在上面的圖例中,每一條黑線傳輸着一整個向量,從一個節點的輸出到其餘節點的輸入。粉色的圈表明 pointwise 的操做,諸如向量的和,而黃色的矩陣就是學習到的神經網絡層。合在一塊兒的線表示向量的鏈接,分開的線表示內容被複制,而後分發到不一樣的位置。
1.3 LSTM核心思想
LSTM 的關鍵就是細胞狀態,水平線在圖上方貫穿運行。細胞狀態相似於傳送帶。直接在整個鏈上運行,只有一些少許的線性交互。信息在上面流傳保持不變會很容易。
LSTM 有經過精心設計的稱做爲「門」的結構來去除或者增長信息到細胞狀態的能力。門是一種讓信息選擇式經過的方法。他們包含一個 sigmoid 神經網絡層和一個 pointwise 乘法操做。Sigmoid 層輸出 0 到 1 之間的數值,描述每一個部分有多少許能夠經過。0 表明「不準任何量經過」,1 就指「容許任意量經過」!
LSTM 擁有三個門,來保護和控制細胞狀態。
1.4 LSTM詳解
在咱們 LSTM 中的第一步是決定咱們會從細胞狀態中丟棄什麼信息。這個決定經過一個稱爲遺忘門層完成。該門會讀取 h t − 1 h_{t-1} ht−1和 x t x_t xt,輸出一個在 0 到 1 之間的數值給每一個在細胞狀態 C t − 1 C_{t-1} Ct−1中的數字。1 表示「徹底保留」,0 表示「徹底捨棄」。讓咱們回到語言模型的例子中來基於已經看到的預測下一個詞。在這個問題中,細胞狀態可能包含當前主語的性別,所以正確的代詞能夠被選擇出來。當咱們看到新的主語,咱們但願忘記舊的主語。
下一步是肯定什麼樣的新信息被存放在細胞狀態中。這裏包含兩個部分。第一,sigmoid 層稱輸入門層決定什麼值咱們將要更新。而後,一個 tanh 層建立一個新的候選值向量[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-gg7pFr6E-1607831223631)(https://latex.codecogs.com/svg.latex?\tilde{C}t)]會被加入到狀態中。下一步,咱們會講這兩個信息來產生對狀態的更新。在咱們語言模型的例子中,咱們但願增長新的主語的性別到細胞狀態中,來替代舊的須要忘記的主語。
如今是時候去更新上一個狀態值 C t − 1 C_{t−1} Ct−1了,將其更新爲 C t C_t Ct。前面的步驟以及決定了應該作什麼,咱們只需實際執行便可。
咱們將上一個狀態值乘以 f t f_t ft,以此表達期待忘記的部分。以後咱們將獲得的值加上 i t ∗ C ~ t i_t*\tilde{C}_t it∗C~t。這個獲得的是新的候選值, 按照咱們決定更新每一個狀態值的多少來衡量.
在語言模型的例子中,對應着實際刪除關於舊主題性別的信息,並添加新信息,正如在以前的步驟中描述的那樣。
最後,咱們須要決定要輸出的內容。此輸出將基於咱們的單元狀態,但將是過濾後的版本。首先,咱們運行一個Sigmoid層,該層決定要輸出的單元狀態的哪些部分。而後,咱們經過激活函數Tanh(將值規範化介於−1和1之間)並乘以Sigmoid的輸出,這樣咱們就只輸出咱們決定的部分。
對於語言模型示例,因爲它只是看到一個主語,所以可能要輸出與動詞相關的信息,以防萬一。例如,它可能輸出主語是單數仍是複數,以便咱們知道若是接下來是動詞,則應將動詞以哪一種形式組合。
2 定義超參數
from paddle.io import Dataset import paddle.fluid as fluid import numpy as np import paddle class Config(object): num_layers = 3 # LSTM層數 data_path = 'work/tang_poem.npz' # 詩歌的文本文件存放路徑 lr = 1e-3 # 學習率 use_gpu = True # 是否使用GPU epoch = 20 batch_size = 4 # mini-batch大小 maxlen = 125 # 超過這個長度的以後字被丟棄,小於這個長度的在前面補空格 plot_every = 1000 # 隔batch 可視化一次 max_gen_len = 200 # 生成詩歌最長長度 model_path = "work/checkpoints/model.params.50" # 預訓練模型路徑 prefix_words = '欲窮千里目,更上一層樓' # 不是詩歌的組成部分,用來控制生成詩歌的意境 start_words = '老夫聊發少年狂,' # 詩歌開始 model_prefix = 'work/checkpoints/model.params' # 模型保存路徑 embedding_dim = 256 # 詞向量維度 hidden_dim = 512 # LSTM hidden層維度
3.定義DataLoader
數據集由唐詩組成,包含唐詩57580首125字(不足和多餘125字的都被補充或者截斷)、ix2word以及word2ix共三個字典存儲爲npz格式
paddle.enable_static() datas = np.load(Config.data_path,allow_pickle=True) data = datas['data'] #加載映射表 ix2word = datas['ix2word'].item() word2ix = datas['word2ix'].item() class dataset(Dataset): def __init__(self, data): super(dataset,self).__init__() self.data = data def __getitem__(self, idx): poem = data[idx] return poem def __len__(self): return len(self.data) train_dataset = dataset(data) poem = paddle.static.data(name='poem', shape=[None,125], dtype='float32') if Config.use_gpu: device = paddle.set_device('gpu') places = paddle.CUDAPlace(0) else: device = paddle.set_device('cpu') places = paddle.CPUPlace() paddle.disable_static(device) train_loader = paddle.io.DataLoader( train_dataset, places=places, feed_list = [poem], batch_size=Config.batch_size, shuffle=True, num_workers=2, use_buffer_reader=True, use_shared_memory=False, drop_last=True, )
4.定義網絡
網絡由一層Embedding層和三層LSTM層再經過全鏈接層組成
- input:[seq_len,batch_size]
- 通過embedding層,embeddings(input)
- output:[batch_size,seq_len,embedding_size]
- 通過LSTM,lstm(embeds, (h_0, c_0)),輸出output,hidden
- output:[batch, seq_len, hidden_size]
- Reshape再進過Linear層判別
- output:[batch*seq_len, vocabsize]
import paddle.fluid class Peom(paddle.nn.Layer): def __init__(self,vocab_size,embedding_dim,hidden_dim): super(Peom, self).__init__() self.embeddings = paddle.nn.Embedding(vocab_size,embedding_dim) self.lstm = paddle.nn.LSTM( input_size=embedding_dim, hidden_size=hidden_dim, num_layers=Config.num_layers, ) self.linear = paddle.nn.Linear(in_features=hidden_dim,out_features=vocab_size) def forward(self,input_,hidden=None): seq_len, batch_size = paddle.shape(input_) embeds = self.embeddings(input_) if hidden is None: output,hidden = self.lstm(embeds) else: output,hidden = self.lstm(embeds,hidden) output = paddle.reshape(output,[seq_len*batch_size,Config.hidden_dim]) output = self.linear(output) return output,hidden
5.訓練過程
- 輸入的input爲(batch_size,seq_len)
- 經過input_,target = data_[:,:-1],data_[:,1:]將每句話分爲前n-1個字做爲真正的輸入,後n-1個字做爲label,size都是(batch_size, seq_len-1)
- 通過網絡,得出output:((seq_len-1)*batch, vocab_size)
- 經過label通過reshape將target變成((seq_len-1)*batch)
損失函數爲:crossEntropy,優化器爲:Adam
loss_= [] def train(): model = Peom( len(word2ix), embedding_dim = Config.embedding_dim, hidden_dim = Config.hidden_dim ) # state_dict = paddle.load(Config.model_path) # model.set_state_dict(state_dict) optim = paddle.optimizer.Adam(parameters=model.parameters(),learning_rate=Config.lr) lossf = paddle.nn.CrossEntropyLoss() for epoch in range(Config.epoch): for li,data in enumerate(train_loader()): optim.clear_grad() data = data[0] #data = paddle.transpose(data,(1,0)) x = paddle.to_tensor(data[:,:-1]) y = paddle.to_tensor(data[:,1:],dtype='int64') y = paddle.reshape(y,[-1]) y = paddle.to_tensor(y,dtype='int64') output,hidden = model(x) loss = lossf(output,y) loss.backward() optim.step() loss_.append(loss.numpy()[0]) if li % Config.plot_every == 0: print('Epoch ID={0}\t Batch ID={1}\t Loss={2}'.format(epoch, li, loss.numpy()[0])) results = list(Config.start_words) start_words_len = len(Config.start_words) # 第一個詞語是<START> input = paddle.to_tensor([word2ix['<START>']]) input = paddle.reshape(input,[1,1]) hidden = None # 如有風格前綴,則先用風格前綴生成hidden if Config.prefix_words: # 第一個input是<START>,後面就是prefix中的漢字 # 第一個hidden是None,後面就是前面生成的hidden for word in Config.prefix_words: output, hidden = model(input, hidden) input = paddle.to_tensor([word2ix[word]]) input = paddle.reshape(input,[1,1]) # 開始真正生成詩句,若是沒有使用風格前綴,則hidden = None,input = <START> # 不然,input就是風格前綴的最後一個詞語,hidden也是生成出來的 for i in range(Config.max_gen_len): output, hidden = model(input, hidden) # print(output.shape) # 若是還在詩句內部,輸入就是詩句的字,不取出結果,只爲了獲得 # 最後的hidden if i < start_words_len: w = results[i] input = paddle.to_tensor([word2ix[w]]) input = paddle.reshape(input,[1,1]) # 不然將output做爲下一個input進行 else: # print(output.data[0].topk(1)) _,top_index = paddle.fluid.layers.topk(output[0],k=1) top_index = top_index.numpy()[0] w = ix2word[top_index] results.append(w) input = paddle.to_tensor([top_index]) input = paddle.reshape(input,[1,1]) if w == '<EOP>': del results[-1] break results = ''.join(results) print(results) paddle.save(model.state_dict(), Config.model_prefix) train()
6.Loss變化過程
import matplotlib.pyplot as plt plt.figure(figsize=(15, 6)) x = np.arange(len(loss_)) plt.title('Loss During Training') plt.xlabel('Number of Batch') plt.plot(x,np.array(loss_)) plt.savefig('work/Loss During Training.png') plt.show()
7.生成唐詩
7.1 模式一 <首句續寫唐詩>
例如:」老夫聊發少年狂「
老夫聊發少年狂,嫁得雙鬟梳似桃。
青絲長髮嬌且紅,輭舞臉低時未昏。
嬌嚬欲盡一雙轉,笑語千里相競言。
朝陽上去花欲盡,花時且落花前過。
穠雨霏霏滿地曉,紅妝白鳥飛下郭。
燈前織女嫁新租,袖裏垂綸舞袖舞。
羅袖焰揚簷下櫻,一宿十二花綿綿。
西施夾道春風暖,嫩粉縈絲弄金蘂。
7.2 模式二<藏頭詩>
例如:」夜月一簾幽夢春風十里柔情「
夜半星初洽,月明星未稀。
一緘瓊燭動,簾外玉環飛。
幽匣光華溢,夢中形影微。
春風吹蕙笏,風緒拂莓苔。
十月涵金井,裏塵氛祲微。
柔荑暎肌骨,情酒圍脣肌。
# 給定首句生成詩歌 def generate(model, start_words, prefix_words=None): results = list(start_words) start_words_len = len(start_words) # 第一個詞語是<START> input = paddle.to_tensor([word2ix['<START>']]) input = paddle.reshape(input,[1,1]) hidden = None # 如有風格前綴,則先用風格前綴生成hidden if prefix_words: # 第一個input是<START>,後面就是prefix中的漢字 # 第一個hidden是None,後面就是前面生成的hidden for word in prefix_words: output, hidden = model(input, hidden) input = paddle.to_tensor([word2ix[word]]) input = paddle.reshape(input,[1,1]) # 開始真正生成詩句,若是沒有使用風格前綴,則hidden = None,input = <START> # 不然,input就是風格前綴的最後一個詞語,hidden也是生成出來的 for i in range(Config.max_gen_len): output, hidden = model(input, hidden) # print(output.shape) # 若是還在詩句內部,輸入就是詩句的字,不取出結果,只爲了獲得 # 最後的hidden if i < start_words_len: w = results[i] input = paddle.to_tensor([word2ix[w]]) input = paddle.reshape(input,[1,1]) # 不然將output做爲下一個input進行 else: # print(output.data[0].topk(1)) _,top_index = paddle.fluid.layers.topk(output[0],k=1) top_index = top_index.numpy()[0] w = ix2word[top_index] results.append(w) input = paddle.to_tensor([top_index]) input = paddle.reshape(input,[1,1]) if w == '<EOP>': del results[-1] break results = ''.join(results) return results # 生成藏頭詩 def gen_acrostic(model, start_words, prefix_words=None): result = [] start_words_len = len(start_words) input = paddle.to_tensor([word2ix['<START>']]) input = paddle.reshape(input,[1,1]) # 指示已經生成了幾句藏頭詩 index = 0 pre_word = '<START>' hidden = None # 存在風格前綴,則生成hidden if prefix_words: for word in prefix_words: output, hidden = model(input, hidden) input = paddle.to_tensor([word2ix[word]]) input = paddle.reshape(input,[1,1]) # 開始生成詩句 for i in range(Config.max_gen_len): output, hidden = model(input, hidden) _,top_index = paddle.fluid.layers.topk(output[0],k=1) top_index = top_index.numpy()[0] w = ix2word[top_index] # 說明上個字是句末 if pre_word in { '。', ',', '?', '!', '<START>'}: if index == start_words_len: break else: w = start_words[index] index += 1 # print(w,word2ix[w]) input = paddle.to_tensor([word2ix[w]]) input = paddle.reshape(input,[1,1]) else: input = paddle.to_tensor([top_index]) input = paddle.reshape(input,[1,1]) result.append(w) pre_word = w result = ''.join(result) return result #讀取訓練好的模型 model = Peom( len(word2ix), embedding_dim = Config.embedding_dim, hidden_dim = Config.hidden_dim ) state_dict = paddle.load('work/checkpoints/model.params.50') model.set_state_dict(state_dict) print('[*]模式一:首句續寫唐詩:') #生成首句續寫唐詩(prefix_words爲生成意境的詩句) print(generate(model, start_words="春江潮水連海平,", prefix_words="滾滾長江東逝水,浪花淘盡英雄。")) print('[*]模式二:藏頭詩:') #生成藏頭詩 print(gen_acrostic(model, start_words="夜月一簾幽夢春風十里柔情", prefix_words="落花人獨立,微雨燕雙飛。"))
下載安裝命令 ## CPU版本安裝命令 pip install -f https://paddlepaddle.org.cn/pip/oschina/cpu paddlepaddle ## GPU版本安裝命令 pip install -f https://paddlepaddle.org.cn/pip/oschina/gpu paddlepaddle-gpu
本文同步分享在 博客「Redflashing」(CSDN)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。