Paddle2.0讓你成爲詩詞大師-PaddlePoetry

項目地址: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} ht1 x t x_t xt,輸出一個在 0 到 1 之間的數值給每一個在細胞狀態 C t − 1 C_{t-1} Ct1中的數字。1 表示「徹底保留」,0 表示「徹底捨棄」。讓咱們回到語言模型的例子中來基於已經看到的預測下一個詞。在這個問題中,細胞狀態可能包含當前主語的性別,所以正確的代詞能夠被選擇出來。當咱們看到新的主語,咱們但願忘記舊的主語。

下一步是肯定什麼樣的新信息被存放在細胞狀態中。這裏包含兩個部分。第一,sigmoid 層稱輸入門層決定什麼值咱們將要更新。而後,一個 tanh 層建立一個新的候選值向量[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-gg7pFr6E-1607831223631)(https://latex.codecogs.com/svg.latex?\tilde{C}t)]會被加入到狀態中。下一步,咱們會講這兩個信息來產生對狀態的更新。在咱們語言模型的例子中,咱們但願增長新的主語的性別到細胞狀態中,來替代舊的須要忘記的主語。

如今是時候去更新上一個狀態值 C t − 1 C_{t−1} Ct1了,將其更新爲 C t C_t Ct。前面的步驟以及決定了應該作什麼,咱們只需實際執行便可。
咱們將上一個狀態值乘以 f t f_t ft,以此表達期待忘記的部分。以後咱們將獲得的值加上 i t ∗ C ~ t i_t*\tilde{C}_t itC~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源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索