RNN文本生成-想为女朋友写诗(二):验证训练结果

1,785 阅读8分钟

一、亮出效果

世界上美好的事物很多,当我们想要表达时,总是感觉文化底蕴不够。

  • 看到大海时,心情舒畅,顿时感觉激情澎湃,想了很久,说了句:真大啊!
  • 看到鸟巢时,心情舒畅,顿时感觉激情澎湃,想了很久,说了句:真大啊!
  • 看到美女时,心情舒畅,顿时感觉激情澎湃,想了很久,说了句:真大啊!

是的,没有文化底蕴就是这样。

但是,你生在这个数字时代,中华五千年的文化底蕴,你触手可及! 这篇教程就是让人工智能学习大量的诗句,找到作诗的规律,然后你给他几个关键字,他给你一首诗。

看效果

输入的关键词输出的诗句
大海,凉风大海阔苍苍,至月空听音。筒动有歌声,凉风起萧索。
建筑,鸟巢建筑鼓钟催,鸟巢穿梧岸。深语在高荷,栖鸟游何处。
美女美女步寒泉,归期便不住。日夕登高看,吟轩见有情。
我,爱,美,女我意本悠悠,爱菊花相应。美花酒恐春,女娥踏新妇。
老,板,英,明老锁索愁春,板阁知吾事。英闽问旧游,明主佳期晚。

二、实现步骤

上一篇《RNN文本生成-想为女朋友写诗(一)》,我们讲了如何训练数据。

这一篇,我们试一下,如何使用训练好的数据。

2.1 恢复模型

你当初是如何训练的,现在就要恢复当时那个现场。当时有几层楼,有多少人开会,他们都坐在哪里,现在也是一样,只不过是讨论的话题变了。

from numpy.core.records import array
import tensorflow as tf
import numpy as np
import os
import time
import random

# 读取字典
vocab = np.load('vocab.npy')
# 创建从非重复字符到索引的映射
char2idx = {u:i for i, u in enumerate(vocab)}
# 创建从数字到字符的映射
idx2char = np.array(vocab)
# 词集的长度,也就是字典的大小
vocab_size = len(vocab)
# 嵌入的维度,也就是生成的embedding的维数
embedding_dim = 256
# RNN 的单元数量
rnn_units = 1024

def build_model(vocab_size, embedding_dim, rnn_units, batch_size):
    model = tf.keras.Sequential([
    tf.keras.layers.Embedding(vocab_size, embedding_dim,
                              batch_input_shape=[batch_size, None]),
    tf.keras.layers.GRU(rnn_units,
                        return_sequences=True,
                        stateful=True,
                        recurrent_initializer='glorot_uniform'),
    tf.keras.layers.Dense(vocab_size)])
    return model

# 读取保存的训练结果
checkpoint_dir = './training_checkpoints'
tf.train.latest_checkpoint(checkpoint_dir)
model = build_model(vocab_size, embedding_dim, 
                    rnn_units, batch_size=1)
# 当初只保存了权重,现在只加载权重
model.load_weights(tf.train.latest_checkpoint(checkpoint_dir))
# 从历史结果构建起一个model
model.build(tf.TensorShape([1, None]))

最终得到的是一个model,里面包含了输入结构、神经元结构、输出格式等信息,最重要的是它也加载了这些权重,这些权重一开始是随机的,但是经过前期训练,都变成了能够预测结果的有效值,如果调用model.summary()model.summary()打印一下,是如下的结构:

_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
embedding_1 (Embedding)      (1, None, 256)            1377280   
_________________________________________________________________
gru_1 (GRU)                  (1, None, 1024)           3938304   
_________________________________________________________________
dense_1 (Dense)              (1, None, 5380)           5514500   
=================================================================
Total params: 10,830,084
Trainable params: 10,830,084
Non-trainable params: 0
_________________________________________________________________

关于模型

这个模型的序列是三部分,作用是输入文字,经过各个层蹂躏,最终预测出下一个字可能会是什么

graph TD
A[嵌入层 Embedding] --> B[门控循环单元 GRU]--> C[全连接层 Dense]
I((文字输入: 明))-->A
C --> O1(月: 30%)
C --> O2(日: 20%)
C --> O3(龟: 0.5%)
C --> O4(... )
C --> O5(兔: 1%)

2.1.1 嵌入层 Embedding

给文字赋予情感

文字是有情感关系的。比如我们看到落叶,就会悲伤。看到夏雨荷,梦里喊出了皇上。

但是,机器他不知道,他只知道10101010。计算机它真的好惨!

为了解决这个问题,就需要把文字标记成数字,让他通过计算来理解相互之间的关系。

来看颜色是如何用数字来表示的

颜色数值
红色[255,0,0]
绿色[0,255,0]
蓝色[0,0,255]
黄色[255,255,0]
白色[255,255,255]

下面见证一下奇迹,懂色彩学的都知道,红色和绿色掺在一起是什么颜色?

来,跟我一起读:红色+绿色=黄色。

到数字上就是:[255,0,0]+[0,255,0] = [255,255,0]

这很难吗?好像也不难,只要数字标的好,板凳有脚也能跑。

到了文字上也一样,嵌入层干的就是这个。

上面的颜色是用3个维度来表示,而嵌入层有更多个维度,我们代码中嵌入层设置了256个维度。

每一个字词都有256个维度对它进行表示。比如“IT男”和“IT从业者”这两个词的表示。

graph TD
C(IT男) --> O1(IT行业: 100%)
C --> O2(男性: 100%)
C --> O3(格子衫: 20%)
C --> O4(... )
C --> O5(会撩妹: 0.01%)
graph TD
C(IT从业者) --> O1(IT行业: 100%)
C --> O2(男性: 35.1%)
C --> O3(格子衫: 30%)
C --> O4(... )
C --> O5(会撩妹: 5%)

有了这些,当我们问计算IT男和IT从业者之间的差异时,他会计算出来最大的差别体现在是否是男性。

你看,这样计算机就理解了词与词之间的关系了。

这也就是嵌入层做出的贡献。

当我们输入文本时,由于经过前面的训练,这些文本里面是带着这些维度的。

有了词语维度,神经网络才能有条件为预测做出判断。

2.1.2 门控循环单元 GRU

女朋友总是记住这些年惹他生气的事情,细节记得一清二楚

填空题:我坐在马路牙子上正抽烟,远处,一个男人掏出烟,熟练的抽出一根叼进嘴里,摸摸了上衣兜,又拍了拍裤兜,他摇了摇头,向我走来,他说:兄弟,_____。

请问空格处填什么?

来,跟我一起喊出来:借个火。

为什么?有没有问过自己为什么能答出来。

如果这道题是:他摇了摇头,向我走来,他说:兄弟,_____。

你还能答对吗?你能明确知道该怎么回答吗?这又是为什么?

是因为记忆,文本的前后是有关系的,我们叫上下文,代码里你见过叫context。

你首先是能理解文本,另外你也有记忆,所以你能预测出下面该出现什么。

我们上面解决了机器的理解问题,但是它也需要像我们一样拥有记忆。

这个GRU层,就是处理记忆的。

它处理的比较高级,只保存和结果有关的记忆,对于无关的词汇,它统统忽视。

经过它训练和处理之后,估计题目变成这样:

填空题:我抽烟,男人掏出烟,叼进嘴里,他摇了摇头,他说:兄弟,_____。 答案:借个火。

有了记忆,神经网络才有底气和实力为预测做出判断。

2.1.3 全连接层 Dense

弱水三千,只取一瓢。

在这里,它其实是一个分类器。

我们构建它时,代码是这样的Dense(5380)Dense(5380)

他在序列中网络层的结构是这样的:

dense_1 (Dense)              (1, None, 5380)           5514500   

它所做的事情,不管你前面是怎么样,到我这里我要强行转化为固定的通道。

比如数字识别0~9,我有500个神经元参与判断,但是最终输出结果就是10个通道(0,1,2,3,4,5,6,7,8,9)。

识别字母,就是26个通道。

我们这里训练的文本,7万多个句子,总共有5380类字符,所以是5380个通道。给定一个输入后,输出为每个字的概率。

graph TD
C[明] --下一个字会是--> O1(月: 30%)
C --下一个字会是--> O2(日: 20%)
C --下一个字会是--> O3(龟: 0.5%)
C --下一个字会是--> O4(... )
C --下一个字会是--> O5(兔: 1%)

有了分类层,神经网络才能有方法把预测的结果输出下来。

2.2 预测数据

有了上面的解释,我相信,下面的代码你一定能看明白了。看不明白不是我的读者。

下面是根据一个字预测下一个字的示例。

start_string = "大"
# 将起始字符串转换为数字
input_eval = [char2idx[s] for s in start_string]
print(input_eval) # [1808]
# 训练模型结构一般是多套输入多套输出,要升维
input_eval = tf.expand_dims(input_eval, 0)
print(input_eval) # Tensor([[1808]])

# 获得预测结果,结果是多维的
predictions = model(input_eval)
print(predictions) 
'''
输出的是预测结果,总共输入'明'一个字,输出分别对应的下一个字的概率,总共有5380个字
shape=(1, 1, 5380)
tf.Tensor(
[[[ -3.3992984    2.3124864   -2.7357426  ... -10.154563 ]]])
'''

# 预测结果,删除批次的维度[[xx]]变为[xx]
predictions1 = tf.squeeze(predictions, 0)
# 用分类分布预测模型返回的字符,从5380个字中根据概率找出num_samples个字
predicted_ids = tf.random.categorical(predictions1, num_samples=1).numpy()
print(idx2char[predicted_ids])  # [['名']]

下面是生成藏头诗的示例。

# 根据一段文本,预测下一段文本
def generate_text(model, start_string, num_generate=6):

  # 将起始字符串转换为数字(向量化)
  input_eval = [char2idx[s] for s in start_string]
  # 上面结果是[2,3,4,5]

  # 训练模型结构一般是多套输入多套输出,要升维
  input_eval = tf.expand_dims(input_eval, 0)
  # 上结果变为[[2,3,4,5]]

  # 空字符串用于存储结果
  text_generated = []

  model.reset_states()
  for i in range(num_generate):
      # 获得预测结果,结果是多维的
      predictions = model(input_eval)
      # 预测结果,删除批次的维度[[xx,xx]]变为[xx,xx]
      predictions = tf.squeeze(predictions, 0)
      # 用分类分布预测模型返回的字符
      predicted_id = tf.random.categorical(predictions, num_samples=1)[-1,0].numpy()
      # 把预测字符和前面的隐藏状态一起传递给模型作为下一个输入
      input_eval = tf.expand_dims([predicted_id], 0)
      # 将预测的字符存起来
      text_generated.append(idx2char[predicted_id])

  # 最终返回结果
  return start_string+''.join(text_generated)

#%%
s = "掘金不止"
array_keys = list(s)
all_string = ""
for word in array_keys:
  all_string = all_string +" "+ word
  next_len = 5-len(word)
  print("input:",all_string)
  all_string = generate_text(model, start_string=all_string, num_generate = next_len)
  print("out:",all_string)

print("最终输出:"+all_string)
# %%

'''
input:  掘
out:  掘隼曳骏迟
input:  掘隼曳骏迟 金
out:  掘隼曳骏迟 金马徒自举
input:  掘隼曳骏迟 金马徒自举 不
out:  掘隼曳骏迟 金马徒自举 不言巧言何
input:  掘隼曳骏迟 金马徒自举 不言巧言何 止
out:  掘隼曳骏迟 金马徒自举 不言巧言何 止足知必趣
最终输出: 掘隼曳骏迟 金马徒自举 不言巧言何 止足知必趣

'''

上面例子演示了一个“掘金不止”的藏头诗:

掘金不止·现代·TF男孩
隼曳骏迟
马徒自举
言巧言何
足知必趣