基于RNN&LSTM的古诗词生成神经网络实现

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/sinat_33741547/article/details/77684888

一、前言

1、图解RNN

2、Tensorflow中RNN实现的正确打开方式

二、实战

1、训练数据处理

(1)文字转为向量


 
 
  1. def _get_poetry(self):
  2. with open(self.poetry_file, "r", encoding= 'utf-8') as f:
  3. poetry_list = [line for line in f]
  4. return poetry_list
  5. def _gen_poetry_vectors(self):
  6. words = sorted(set( ''.join(self.poetry_list)+ ' '))
  7. # 每一个字符分配一个索引 为后续诗词向量化做准备
  8. int_to_word = {i: word for i, word in enumerate(words)}
  9. word_to_int = {v: k for k, v in int_to_word.items()}
  10. to_int = lambda word: word_to_int.get(word)
  11. poetry_vectors = [list(map(to_int, poetry)) for poetry in self.poetry_list]
  12. return poetry_vectors, word_to_int, int_to_word
在这里将训练数据中所有的字生成了一个"文字==>数字"的词袋,并将所有的诗词按行分割转化为数字表示。

(2)生成器


 
 
  1. def batch(self):
  2. # 生成器
  3. start = 0
  4. end = self.batch_size
  5. for _ in range(self.chunk_size):
  6. batches = self.poetry_vectors[start:end]
  7. # 输入数据 按每块数据中诗句最大长度初始化数组,缺失数据补全
  8. x_batch = np.full((self.batch_size, max(map(len, batches))), self.word_to_int[ ' '], np.int32)
  9. for row in range(self.batch_size): x_batch[row, :len(batches[row])] = batches[row]
  10. # 标签数据 根据上一个字符预测下一个字符 所以这里y_batch数据应为x_batch数据向后移一位
  11. y_batch = np.copy(x_batch)
  12. y_batch[:, : -1], y_batch[:, -1] = x_batch[:, 1:], x_batch[:, 0]
  13. yield x_batch, y_batch
  14. start += self.batch_size
  15. end += self.batch_size
x_batch作为输入,y_batch为标签。诗词生成模型根据上一个字符生成下一个字符,所以这里的标签数据应该是和输入数据的shape一致,但序列字符后移一位。y_batch的最后一位,理论上来说应该是本行诗词的下一行的第一个字,简单起见,这里用本行的第一个字代替。

2、RNN模型

(1)模型初始化


 
 
  1. class PoetryModel:
  2. def __init__(self):
  3. # 诗歌生成
  4. self.poetry = Poetry()
  5. # 单个cell训练序列个数
  6. self.batch_size = self.poetry.batch_size
  7. # 所有出现字符的数量
  8. self.word_len = len(self.poetry.word_to_int)
  9. # 隐层的数量
  10. self.rnn_size = 128
rnn_size代表RNN模型中隐层的数量,这个概念要和RNN模型中time_step的数量分开,这里time_step数量指的是每一行诗词的文字数量,即x。batch_size数量指每次训练时,使用的诗词行数。

(2)变量定义


 
 
  1. # 输入句子长短不一致 用None自适应
  2. inputs = tf.placeholder(tf.int32, shape=(self.batch_size, None), name= 'inputs')
  3. # 输出为预测某个字后续字符 故输出也不一致
  4. targets = tf.placeholder(tf.int32, shape=(self.batch_size, None), name= 'targets')
  5. # 防止过拟合
  6. keep_prob = tf.placeholder(tf.float32, name= 'keep_prob')
(3)embedding定义


 
 
  1. def embedding_variable(inputs, rnn_size, word_len):
  2. with tf.variable_scope( 'embedding'):
  3. # 这里选择使用cpu进行embedding
  4. with tf.device( "/cpu:0"):
  5. # 默认使用'glorot_uniform_initializer'初始化,来自源码说明:
  6. # If initializer is `None` (the default), the default initializer passed in
  7. # the variable scope will be used. If that one is `None` too, a
  8. # `glorot_uniform_initializer` will be used.
  9. # 这里实际上是根据字符数量分别生成state_size长度的向量
  10. embedding = tf.get_variable( 'embedding', [word_len, rnn_size])
  11. # 根据inputs序列中每一个字符对应索引 在embedding中寻找对应向量,即字符转为连续向量:[字]==>[1]==>[0,1,0]
  12. lstm_inputs = tf.nn.embedding_lookup(embedding, inputs)
  13. return lstm_inputs
将诗词的文字对应索引分别转化为变量,引入另一个新概念,input_size,即每一个字所对应的变量长度,这里是训练数据中所有字符的数量,所以实际上我们每次训练使用的数据shape应该是(batch_size,time_step,input_size)。

(4)模型计算图定义


 
 
  1. def rnn_graph(self, batch_size, rnn_size, word_len, lstm_inputs, keep_prob):
  2. # cell.state_size ==> 128
  3. # 基础cell 也可以选择其他基本cell类型
  4. lstm = tf.nn.rnn_cell.BasicLSTMCell(num_units=rnn_size)
  5. drop = tf.nn.rnn_cell.DropoutWrapper(lstm, output_keep_prob=keep_prob)
  6. # 多层cell 前一层cell作为后一层cell的输入
  7. cell = tf.nn.rnn_cell.MultiRNNCell([drop] * 2)
  8. # 初始状态生成(h0) 默认为0
  9. # initial_state.shape ==> (64, 128)
  10. initial_state = cell.zero_state(batch_size, tf.float32)
  11. # 使用dynamic_rnn自动进行时间维度推进 且 可以使用不同长度的时间维度
  12. # 因为我们使用的句子长度不一致
  13. lstm_outputs, final_state = tf.nn.dynamic_rnn(cell, lstm_inputs, initial_state=initial_state)
  14. seq_output = tf.concat(lstm_outputs, 1)
  15. x = tf.reshape(seq_output, [ -1, rnn_size])
  16. # softmax计算概率
  17. w, b = self.soft_max_variable(rnn_size, word_len)
  18. logits = tf.matmul(x, w) + b
  19. prediction = tf.nn.softmax(logits, name= 'predictions')
  20. return logits, prediction, initial_state, final_state
(5)权重及偏置定义


 
 
  1. def soft_max_variable(rnn_size, word_len):
  2. # 共享变量
  3. with tf.variable_scope( 'soft_max'):
  4. w = tf.get_variable( "w", [rnn_size, word_len])
  5. b = tf.get_variable( "b", [word_len])
  6. return w, b
RNN与CNN不同的一点在于,RNN的权重及偏置在所有cell中是一样的,这里使用了共享变量。
(6)损失及优化图定义


 
 
  1. @staticmethod
  2. def loss_graph(word_len, targets, logits):
  3. # 将y序列按序列值转为one_hot向量
  4. y_one_hot = tf.one_hot(targets, word_len)
  5. y_reshaped = tf.reshape(y_one_hot, [ -1, word_len])
  6. loss = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(logits=logits, labels=y_reshaped))
  7. return loss
  8. @staticmethod
  9. def optimizer_graph(loss, learning_rate):
  10. grad_clip = 5
  11. # 使用clipping gradients
  12. tvars = tf.trainable_variables()
  13. grads, _ = tf.clip_by_global_norm(tf.gradients(loss, tvars), grad_clip)
  14. train_op = tf.train.AdamOptimizer(learning_rate)
  15. optimizer = train_op.apply_gradients(zip(grads, tvars))
  16. return optimizer

RNN会遇到梯度爆炸(gradients exploding)和梯度弥散(gradients disappearing)的问题。LSTM解决了梯度弥散的问题,但是gradients仍然可能会爆炸,因此我们采用gradient clippling的方式来防止梯度爆炸。即通过设置一个阈值,当gradients超过这个阈值时,就将它重置为阈值大小,这就保证了梯度不会变得很大。

(7)开始训练


 
 
  1. # 开始训练
  2. saver = tf.train.Saver()
  3. sess = tf.Session()
  4. sess.run(tf.global_variables_initializer())
  5. step = 0
  6. new_state = sess.run(initial_state)
  7. for i in range(epoch):
  8. # 训练数据生成器
  9. batches = self.poetry.batch()
  10. # 随模型进行训练 降低学习率
  11. sess.run(tf.assign(learning_rate, 0.001 * ( 0.97 ** i)))
  12. for batch_x, batch_y in batches:
  13. feed = {inputs: batch_x, targets: batch_y, initial_state: new_state, keep_prob: 0.5}
  14. batch_loss, _, new_state = sess.run([loss, optimizer, final_state], feed_dict=feed)
  15. print(datetime.datetime.now().strftime( '%c'), ' i:', i, 'step:', step, ' batch_loss:', batch_loss)
  16. step += 1
  17. model_path = os.getcwd() + os.sep + "poetry.model"
  18. saver.save(sess, model_path, global_step=step)
  19. sess.close()
随着模型的训练,逐步降低学习率。
(8)生成古诗词

 
 
  1. def gen(self, poem_len):
  2. def to_word(weights):
  3. t = np.cumsum(weights)
  4. s = np.sum(weights)
  5. sample = int(np.searchsorted(t, np.random.rand( 1) * s))
  6. return self.poetry.int_to_word[sample]
  7. # 输入
  8. # 句子长短不一致 用None自适应
  9. self.batch_size = 1
  10. inputs = tf.placeholder(tf.int32, shape=(self.batch_size, 1), name= 'inputs')
  11. # 防止过拟合
  12. keep_prob = tf.placeholder(tf.float32, name= 'keep_prob')
  13. lstm_inputs = self.embedding_variable(inputs, self.rnn_size, self.word_len)
  14. # rnn模型
  15. _, prediction, initial_state, final_state = self.rnn_graph(self.batch_size, self.rnn_size, self.word_len, lstm_inputs, keep_prob)
  16. saver = tf.train.Saver()
  17. with tf.Session() as sess:
  18. sess.run(tf.global_variables_initializer())
  19. saver.restore(sess, tf.train.latest_checkpoint( '.'))
  20. new_state = sess.run(initial_state)
  21. # 在所有字中随机选择一个作为开始
  22. x = np.zeros(( 1, 1))
  23. x[ 0, 0] = self.poetry.word_to_int[self.poetry.int_to_word[random.randint( 1, self.word_len -1)]]
  24. feed = {inputs: x, initial_state: new_state, keep_prob: 1}
  25. predict, new_state = sess.run([prediction, final_state], feed_dict=feed)
  26. word = to_word(predict)
  27. poem = ''
  28. while len(poem) < poem_len:
  29. poem += word
  30. x = np.zeros(( 1, 1))
  31. x[ 0, 0] = self.poetry.word_to_int[word]
  32. feed = {inputs: x, initial_state: new_state, keep_prob: 1}
  33. predict, new_state = sess.run([prediction, final_state], feed_dict=feed)
  34. word = to_word(predict)
  35. return poem
在sep-sep模型中,上一个网络的状态输出应该作为下一个网络的状态输入,所以初始状态为零,后续输入状态都是上一次的输出状态。

(9)结果


 
 
  1. 筋烛升玄净,云光理片溪。
  2. 美子相离处,离来下路疏。
  3. 策变因坚薛,灵行一更颇。
  4. 卖上狎城天,穿洄欹笔软。
  5. 授名时依寂,快举即芳眉。
  6. 吊此之当主,长期动世迟。
看起来有模有样的,基本模型是成功了。在这个模型的基础上进行改动,还可以生成小说等。

三、其他

具体源码可以在我的github上找到: https://github.com/lpty/tensorflow_tutorial






  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值