本文咱们将使用GRU网络来学习莎士比亚小说,模型经过学习能够生成与小说风格类似的文本,如图所示:
虽然有些句子并无实际的意思(目前咱们的模型是基于几率,并非理解语义),可是大多数单词都是有效的,文本结构也与咱们训练的文本类似。
因为项目中使用到了Eager Execution和GRU,因此咱们先进行简单介绍:python
Tensorflow在Eager 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 Execution。app
GRU是LSTM的一种变体,它将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将高纬离散向量转为低纬稠密的连续向量,而且表现出了向量间的类似性。
如图所示,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,在此表示感谢。