GRU網絡生成莎士比亞小說

介紹

本文咱們將使用GRU網絡來學習莎士比亞小說,模型經過學習能夠生成與小說風格類似的文本,如圖所示:
圖片描述
雖然有些句子並無實際的意思(目前咱們的模型是基於機率,並非理解語義),可是大多數單詞都是有效的,文本結構也與咱們訓練的文本類似。
因爲項目中使用到了Eager ExecutionGRU,因此咱們先進行簡單介紹:python

Eager Execution

TensorflowEager Execution以前想要評估操做必須經過運行計算圖"sess.run()"的方式來獲取值,而使用Eager Execution能夠當即評估操做。Eager Execution基於python流程控制並可使用python的調試工具進行錯誤報告。git

梯度計算:github

先使用tf.GradientTape記錄而後再計算梯度,示例以下:api

# tfe = tf.contrib.eager
w = tfe.Variable([[1.0]])
with tf.GradientTape() as tape:
  loss = w * w

grad = tape.gradient(loss, w)

經常使用函數:
tfe.gradients_function:返回一個函數,該函數會計算其輸入函數參數相對其參數的導數。
tfe.value_and_gradients_function:除了返回函數還會返回輸入函數的值。性能優化

其它:網絡

在訓練大數據集的時候,Eager Execution 性能與Graph Execution至關,但在小數據集中Eager Execution會慢一些。
Eager Execution勝在開發和調試的便利性,可是在分佈式訓練,性能優化,生產部署方面Graph Execution更好。
在未調用tf.enable_eager_execution(開啓後不能關閉)的狀況下可使用tfe.py_func啓用Eager Executionapp

GRU

GRULSTM的一種變體,它將LSTM的遺忘門,輸入門,輸出門改成更新門(LSTM的遺忘門,輸入門合併),重置門。參數少,收斂快,不過在數據量較大的時候LSTM的表現更好。下圖是GRU網絡結構和前向傳播計算方法。dom

圖片描述

更新門:控制前一時刻的狀態信息被帶入到當前狀態中的程度。
重置門:控制忽略前一時刻的狀態信息,重置門的值越小說明忽略的越多(被寫入的信息越少)。分佈式

GRU訓練:函數

咱們要學習的參數有Wr、Wz、Wh、Wo,其中Wr、Wz、Wh是和ht-1拼接而成,因此須要進行分割:

圖片描述

採用反向傳播對損失函數的各參數求偏導:

圖片描述

中間參數爲:

圖片描述

算出每一個參數的偏導數以後就能夠更新參數了。GRU經過門控機制選擇性的保留特徵,爲長時傳播提供了保證。正由於門控機制的有效,門卷積目前也很受歡迎,感興趣的朋友能夠閱讀相關文獻。

數據導入

import tensorflow as tf
import numpy as np
import os
import re
import random
import time

# 開啓後不能關閉,只能從新啓動新的python會話
tf.enable_eager_execution()

# 獲取數據,你也可使用其餘數據集
path_to_file=tf.keras.utils.get_file('shakespeare.txt', 'https://storage.googleapis.com/download.tensorflow.org/data/shakespeare.txt')
text=open(path_to_file).read()

文字是不能直接放進模型的須要將其轉換爲對應的ID表示:

# 去除重複字符並排序
unique=sorted(set(text))

# enumerate 返回value,index
# 文本轉id 
char2idx={value:idx for idx,value in enumerate(unique)}
# id轉文本
idx2char={idx:value for idx,value in enumerate(unique)}

部分參數配置:

# 每次輸入的最大文本長度,對應GRU模型的‘time_step’
max_length=100
vocab_size=len(unique)
# 詞嵌入維度
embedding_dim=256
hidden_units=1024
BATCH_SIZE=64
BUFFER_SIZE=10000

獲取ID表示的數據並建立標籤

# 標籤的定義方式如:
# data='ming'
# input='min' labels='ing'
input_text=[]
labels_text=[]

# 迭代獲取‘max_length’個數據
for i in range(0,len(text)-max_length,max_length):
    inputs=text[i:i+max_length]
    labels=text[i+1:i+1+max_length]
    
    input_text.append([char2idx[i] for i in inputs])
    labels_text.append([char2idx[i] for i in labels])

dataset讀取數據:

dataset=tf.data.Dataset.from_tensor_slices((input_text,output_text))
# drop_remainder:小於batch_size 是否刪除,默認不刪除
dataset=dataset.batch(BATCH_SIZE,drop_remainder=True)

建立模型

咱們的模型包含三層:Embedding層,GRU層,全鏈接層。

class Model(tf.keras.Model):
    """
    GRU:重置門,更新門 LSTM:遺忘門,輸入門,輸出門
    GRU,參數少,容易收斂,數據量大的時候LSTM表現更好
    """
    def __init__(self,vocab_size,embedding_dim,units,batch_size):
        super(Model, self).__init__()

        self.units=units
        self.batch_size=batch_size
        self.embedding=tf.keras.layers.Embedding(
            input_dim=vocab_size,
            output_dim=embedding_dim
        )
        if tf.test.is_gpu_available:
            # 使用GPU加速訓練
            self.gru=tf.keras.layers.CuDNNGRU(
                units=self.units,
                return_sequences=True,
                return_state=True,
                recurrent_initializer='glorot_uniform'
            )
        else:
            self.gru=tf.keras.layers.GRU(
                units=self.units,
                return_sequences=True,
                return_state=True,
                # 默認激活函數爲:hard_sigmoid
                recurrent_activation='sigmoid',
                recurrent_initializer='glorot_uniform'
            )
        self.fc=tf.keras.layers.Dense(units=vocab_size)
    def __call__(self, x,hidden):
        x=self.embedding(x)
        
        # output:[batch_size,max_length,hidden_size]
        # states:[batch_size,hidden_size]
        output,states=self.gru(x,initial_state=hidden)

        # 轉換至:(batch_size*max_length,hidden_size)
        output=tf.reshape(output,shape=(-1,output.shape[2]))
        
        # output:[batch_size*max_length,vocab_size]
        x=self.fc(output)

        return x,states

爲何要使用Embedding

Embedding將高緯離散向量轉爲低緯稠密的連續向量,而且表現出了向量間的類似性。

圖片描述

如圖所示,one-hot表示只有一個位置是1,其他爲0,當文字較多時維度將會很是的大,而且因爲one-hot編碼後的單詞存在獨立性,致使不能利用類似詞彙進行學習。那麼Embedding又是怎麼作的呢?
圖片描述

使用Embedding的第一步是經過索引對句子進行編碼,而後根據索引建立嵌入矩陣,這樣咱們使用嵌入矩陣替代one-hot編碼向量。每一個單詞向量再也不是由一個獨立向量代替,而是替換成用於查找嵌入矩陣中向量的索引。

模型訓練

# model初始化
model=Model(vocab_size,embedding_dim,hidden_units,BATCH_SIZE)
optimizer=tf.train.AdamOptimizer(learning_rate=0.001)

# 建立損失函數
def loss_fn(lables,preds):
    # 交叉熵損失函數在值域上邊界依然能夠保持較高的激活值
    return tf.losses.sparse_softmax_cross_entropy(
        labels=lables,
        logits=preds
    )

模型保存:

# 讀取checkpoint須要從新定義圖結構
checkpoint_dir = './training_checkpoints'
checkpoint_prefix = os.path.join(checkpoint_dir, "ckpt")
checkpoint = tf.train.Checkpoint(optimizer=optimizer,
                                 model=model)

開始訓練:

EPOCHS = 20

for epoch in range(EPOCHS):
    start = time.time()
    
    # 每迭代完成一次數據集重置hidden-state
    hidden = model.reset_states()
    
    for (batch, (inp, target)) in enumerate(dataset):
          # 使用GradientTape記錄
          with tf.GradientTape() as tape:
              predictions, hidden = model(inp, hidden)
              
              target = tf.reshape(target, (-1,))
              loss = loss_function(target, predictions)
              
          grads = tape.gradient(loss, model.variables)
          # 更新
          optimizer.apply_gradients(zip(grads, model.variables))

          if batch % 100 == 0:
              print ('Epoch {} Batch {} Loss {:.4f}'.format(epoch+1,
                                                            batch,
                                                            loss))
    # 每迭代5次數據集保存一次模型數據
    if (epoch + 1) % 5 == 0:
      checkpoint.save(file_prefix = checkpoint_prefix)

讀取保存的checkpoint文件:

checkpoint.restore(tf.train.latest_checkpoint(checkpoint_dir))

預測

要指定輸入字符以及但願模型生成的文本長度:

# 須要生成的文字長度
num_generate=1000

start_string='Q'
# 將輸入字符轉爲對應ID表示
input_eval=[char2idx[s] for s in start_string]
# 擴展一維 batch_size
input_eval=tf.expand_dims(input_eval,0)

text_generated=''
# hidden state shape:(batch_size,rnn units)
# hidden 初始化
hidden=[tf.zeros((1,hidden_units))]

for i in range(num_generate):
    precit,hidden=model(input_eval,hidden)
    # 注:這裏batch_size == 1
    # 代碼參考,很好理解:
    # output = tf.transpose(output,[1,0,2])
    # last = tf.gather(output,int(output.get_shape()[0]-1)
    predict_id=tf.argmax(predict[-1]).numpy()
    # 將前一時刻的輸出做爲下一時刻的輸入,一直到迭代完成
    input_eval=tf.expand_dims(predict_id,0)
    # 轉換成對應字符
    text_generated+=idx2char[predict_id]
print(start_string+text_generated)

總結

GRU網路做爲LSTM網路的變體,參數少收斂快。Eager模式下代碼簡潔,調試便利雖然比Graph Execution功能遜色,但勝在便利性。RNN如今不少項目都會結合注意力機制使用,效果很好。注意力簡單來講就是對輸入再也不是同等看待,而是根據權重值大小來區別訓練。

本文內容部分參考Yash Katariya,在此表示感謝。

相關文章
相關標籤/搜索