7.2 自然语言处理实战(一):RNN生成文本
本实例的实现文件是nlp01.py,功能是使用基于字符的 RNN 生成文本。本实例将使用 Andrej Karpathy 在《循环神经网络不合理的有效性》一文中提供的莎士比亚作品数据集,给定此数据集中的一个字符序列 (“Shakespear”),训练一个模型以预测该序列的下一个字符(“e”)。通过重复调用该模型,可以生成更长的文本序列。下面是当训练本实例中的模型30 个周期 (epoch)后,并以字符串 “Q”开头时的输出结果:
QUEENE:
I had thought thou hadst a Roman; for the oracle,
Thus by All bids the man against the word,
Which are so weak of care, by old care done;
Your children were in your holy love,
And the precipitation through the bleeding throne.
BISHOP OF ELY:
Marry, and will, my lord, to weep in such a one were prettiest;
Yet now I was adopted heir
Of the world's lamentable day,
To watch the next way with his father with his face?
ESCALUS:
The cause why then we are all resolved more sons.
VOLUMNIA:
O, no, no, no, no, no, no, no, no, no, no, no, no, no, no, no, no, no, no, no, no, it is no sin it should be dead,
And love and pale as any will to that word.
QUEEN ELIZABETH:
But how long have I heard the soul for this world,
And show his hands of life be proved to stand.
PETRUCHIO:
I say he look'd on, if I must be content
To stay him from the fatal of our country's bliss.
His lordship pluck'd from this sentence then for prey,
And then let us twain, being the moon,
were she such a case as fills m
在上述结果中,虽然有些句子符合语法规则,但是大多数句子没有意义。这个模型尚未学习到单词的含义,但请考虑以下几点:
- 此模型是基于字符的:训练开始时,模型不知道如何拼写一个英文单词,甚至不知道单词是文本的一个单位。
- 输出文本的结构类似于剧本:文本块通常以讲话者的名字开始;而且与数据集类似,讲话者的名字采用全大写字母。
- 此模型由小批次 (batch) 文本训练而成(每批 100 个字符)。即便如此,此模型仍然能生成更长的文本序列,并且结构连贯。
在接下来的内容中,将详细讲解实例文件nlp01.py的具体实现流程。
7.2.1 准备数据集
在开始之前需要先准备数据集,具体实现流程如下所示。
(1)下载数据集
下载莎士比亚数据集,代码如下:
path_to_file = tf.keras.utils.get_file('shakespeare.txt', 'https://storage.googleapis.com/download.tensorflow.org/data/shakespeare.txt')
执行后会输出:
Downloading data from https://storage.googleapis.com/download.tensorflow.org/data/shakespeare.txt
1122304/1115394 [==============================] - 0s 0us/step
(2)读取数据集中的数据,代码如下:
# 读取并为 py2 compat 解码
text = open(path_to_file, 'rb').read().decode(encoding='utf-8')
# 文本长度是指文本中的字符个数
print ('Length of text: {} characters'.format(len(text)))
执行后会输出:
Length of text: 1115394 characters
查看文本中的前 250 个字符,代码如下:
print(text[:250])
执行后会输出:
First Citizen:
Before we proceed any further, hear me speak.
All:
Speak, speak.
First Citizen:
You are all resolved rather to die than to famish?
All:
Resolved. resolved.
First Citizen:
First, you know Caius Marcius is chief enemy to the people.
查看文本中的非重复字符,代码如下:
vocab = sorted(set(text))
print ('{} unique characters'.format(len(vocab)))
执行后会输出:
65 unique characters
7.2.2 向量化处理文本
在训练之前,需要将字符串映射到数字表示值。创建两个查找表格:一个将字符映射到数字,另一个将数字映射到字符。代码如下:
# 创建从非重复字符到索引的映射
char2idx = {u:i for i, u in enumerate(vocab)}
idx2char = np.array(vocab)
text_as_int = np.array([char2idx[c] for c in text])
现在,每个字符都有一个整数表示值,接下来将字符映射至索引 0 至 len(unique)。代码如下:
print('{')
for char,_ in zip(char2idx, range(20)):
print(' {:4s}: {:3d},'.format(repr(char), char2idx[char]))
print(' ...\n}')
执行后会输出:
{
'\n': 0,
' ' : 1,
'!' : 2,
'$' : 3,
'&' : 4,
"'" : 5,
',' : 6,
'-' : 7,
'.' : 8,
'3' : 9,
':' : 10,
';' : 11,
'?' : 12,
'A' : 13,
'B' : 14,
'C' : 15,
'D' : 16,
'E' : 17,
'F' : 18,
'G' : 19,
...
}
通过如下代码显示文本中前13个字符的整数映射,代码如下:
print ('{} ---- characters mapped to int ---- > {}'.format(repr(text[:13]), text_as_int[:13]))
执行后会输出:
'First Citizen' ---- characters mapped to int ---- > [18 47 56 57 58 1 15 47 58 47 64 43 52]
7.2.3 预测任务并创建训练样本和目标
指定一个字符或者一个字符序列,下一个最可能出现的字符是什么?这就是我们训练模型要执行的任务。输入进模型的是一个字符序列,我们训练这个模型来预测输出每个时间步(time step)预测下一个字符是什么。由于 RNN 是根据前面看到的元素维持内部状态,那么,给定此时计算出的所有字符下一个字符是什么?
接下来将文本划分为样本序列,每个输入序列包含文本中的 seq_length 个字符。对于每个输入序列来说,其对应的目标包含相同长度的文本,但是向右顺移一个字符。将文本拆分为长度为 seq_length+1 的文本块。例如,假设 seq_length为4 且文本为 “Hello”, 那么输入序列将为“Hell”,目标序列将为“ello”。
首先使用函数tf.data.Dataset.from_tensor_slices()把文本向量转换为字符索引流,代码如下:
# 设定每个输入句子长度的最大值
seq_length = 100
examples_per_epoch = len(text)//seq_length
# 创建训练样本 / 目标
char_dataset = tf.data.Dataset.from_tensor_slices(text_as_int)
for i in char_dataset.take(5):
print(idx2char[i.numpy()])
执行后会输出:
F
i
r
s
t
使用函数batch()把单个字符转换为所需长度的序列,代码如下:
sequences = char_dataset.batch(seq_length+1, drop_remainder=True)
for item in sequences.take(5):
print(repr(''.join(idx2char[item.numpy()])))
执行后会输出:
'First Citizen:\nBefore we proceed any further, hear me speak.\n\nAll:\nSpeak, speak.\n\nFirst Citizen:\nYou '
'are all resolved rather to die than to famish?\n\nAll:\nResolved. resolved.\n\nFirst Citizen:\nFirst, you k'
"now Caius Marcius is chief enemy to the people.\n\nAll:\nWe know't, we know't.\n\nFirst Citizen:\nLet us ki"
"ll him, and we'll have corn at our own price.\nIs't a verdict?\n\nAll:\nNo more talking on't; let it be d"
'one: away, away!\n\nSecond Citizen:\nOne word, good citizens.\n\nFirst Citizen:\nWe are accounted poor citi'
对于每个序列来说,先使用map 函数复制然后再顺移,以创建输入文本和目标文本。map 方法可以将一个简单的函数应用到每一个批次(batch)。代码如下:
def split_input_target(chunk):
input_text = chunk[:-1]
target_text = chunk[1:]
return input_text, target_text
dataset = sequences.map(split_input_target)
打印输出第一批样本的输入与目标值,代码如下:
for input_example, target_example in dataset.take(1):
print ('Input data: ', repr(''.join(idx2char[input_example.numpy()])))
print ('Target data:', repr(''.join(idx2char[target_example.numpy()])))
执行后会输出:
Input data: 'First Citizen:\nBefore we proceed any further, hear me speak.\n\nAll:\nSpeak, speak.\n\nFirst Citizen:\nYou'
Target data: 'irst Citizen:\nBefore we proceed any further, hear me speak.\n\nAll:\nSpeak, speak.\n\nFirst Citizen:\nYou '
这些向量的每个索引均作为一个时间步来处理。作为时间步 0 的输入,模型接收到 “F” 的索引,并尝试预测 “i” 的索引为下一个字符。在下一个时间步,模型将执行相同的操作,但是 RNN 不仅考虑当前的输入字符,还会考虑上一步的信息。代码如下:
for i, (input_idx, target_idx) in enumerate(zip(input_example[:5], target_example[:5])):
print("Step {:4d}".format(i))
print(" input: {} ({:s})".format(input_idx, repr(idx2char[input_idx])))
print(" expected output: {} ({:s})".format(target_idx, repr(idx2char[target_idx])))
执行后会输出:
Step 0
input: 18 ('F')
expected output: 47 ('i')
Step 1
input: 47 ('i')
expected output: 56 ('r')
Step 2
input: 56 ('r')
expected output: 57 ('s')
Step 3
input: 57 ('s')
expected output: 58 ('t')
Step 4
input: 58 ('t')
expected output: 1 (' ')
7.2.4 创建训练批次
使用tf.data将文本拆分为可管理的序列。但是在把这些数据输送至模型之前需要将数据重新排列(shuffle)并打包为批次。代码如下:
# 批大小
BATCH_SIZE = 64
# 设定缓冲区大小,以重新排列数据集
# (TF 数据被设计为可以处理可能是无限的序列,
# 所以它不会试图在内存中重新排列整个序列。相反,
# 它维持一个缓冲区,在缓冲区重新排列元素。)
BUFFER_SIZE = 10000
dataset = dataset.shuffle(BUFFER_SIZE).batch(BATCH_SIZE, drop_remainder=True)
dataset
执行后会输出:
<BatchDataset shapes: ((64, 100), (64, 100)), types: (tf.int64, tf.int64)>
7.2.5 创建模型
使用tf.keras.Sequential定义模型,在本实例中使用三个层来定义模型:
- tf.keras.layers.Embedding:输入层,一个可训练的对照表,它会将每个字符的数字映射到一个 embedding_dim 维度的向量。
- tf.keras.layers.GRU:一种 RNN 类型,其大小由 units=rnn_units 指定(这里你也可以使用一个 LSTM 层)。
- tf.keras.layers.Dense:输出层,带有 vocab_size 个输出。
代码如下:
# 词集的长度
vocab_size = len(vocab)
# 嵌入的维度
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
model = build_model(
vocab_size = len(vocab),
embedding_dim=embedding_dim,
rnn_units=rnn_units,
batch_size=BATCH_SIZE)