Python深度学习-Keras应用-文本生成

使用 LSTM 生成文本

1.如何生成序列数据

用深度学习生成序列数据的通用方法,就是使用前面的标记作为输入,训练一个网络(通常是循环神经网络或卷积神经网络)来预测序列中接下来的一个或多个标记。例如,给定输入the cat is on the ma,训练网络来预测目标 t,即下一个字符。与前面处理文本数据时一样,标记(token)通常是单词或字符,给定前面的标记,能够对下一个标记的概率进行建模的任何网络都叫作语言模型(language model)。语言模型能够捕捉到语言的潜在空间(latent space),即语言的统计结构。

一旦训练好了这样一个语言模型,就可以从中采样(sample,即生成新序列)。向模型中输入一个初始文本字符串[即条件数据(conditioning data)],要求模型生成下一个字符或下一个单词(甚至可以同时生成多个标记),然后将生成的输出添加到输入数据中,并多次重复这一过程(见下图)。这个循环可以生成任意长度的序列,这些序列反映了模型训练数据的结构,它们与人类书写的句子几乎相同。在本节的示例中,我们将会用到一个 LSTM 层,向其输入从文本语料中提取的 N 个字符组成的字符串,然后训练模型来生成第 N+1 个字符。模型的输出是对所有可能的字符做 softmax,得到下一个字符的概率分布。这个 LSTM 叫作字符级的神经语言模
型(character-level neural language model)。
Python深度学习-Keras应用-文本生成_第1张图片

2.采样策略的重要性

生成文本时,如何选择下一个字符至关重要。一种简单的方法是贪婪采样(greedy sampling),就是始终选择可能性最大的下一个字符。但这种方法会得到重复的、可预测的字符串,看起来不像是连贯的语言。一种更有趣的方法是做出稍显意外的选择:在采样过程中引入随机性,即从下一个字符的概率分布中进行采样。这叫作随机采样(stochastic sampling,stochasticity 在这个领域中就是“随机”的意思)。在这种情况下,根据模型结果,如果下一个字符是 e 的概率为0.3,那么你会有 30% 的概率选择它。注意,贪婪采样也可以被看作从一个概率分布中进行采样,即某个字符的概率为 1,其他所有字符的概率都是 0。

从模型的 softmax 输出中进行概率采样是一种很巧妙的方法,它甚至可以在某些时候采样到不常见的字符,从而生成看起来更加有趣的句子,而且有时会得到训练数据中没有的、听起来像是真实存在的新单词,从而表现出创造性。但这种方法有一个问题,就是它在采样过程中无法控制随机性的大小。

为了在采样过程中控制随机性的大小,我们引入一个叫作 softmax 温度(softmax temperature)的参数,用于表示采样概率分布的熵,即表示所选择的下一个字符会有多么出人意料或多么可预测。给定一个 temperature 值,将按照下列方法对原始概率分布(即模型的 softmax 输出)进行重新加权,计算得到一个新的概率分布。

对于不同的 softmax 温度,对概率分布进行重新加权

import numpy as np
#original_distribution 是概率值组成的一维 Numpy 数组,这些概率值之和必须等于 1。temperature 是一个因子,用于定量描述输出分布的熵
def reweight_distribution(original_distribution, temperature=0.5): 
    distribution = np.log(original_distribution) / temperature
    distribution = np.exp(distribution)
    #返回原始分布重新加权后的结果。distribution 的求和可能不再等于 1,因此需要将它除以求和,以得到新的分布
    return distribution / np.sum(distribution)

更高的温度得到的是熵更大的采样分布,会生成更加出人意料、更加无结构的生成数据,而更低的温度对应更小的随机性,以及更加可预测的生成数据。

3.实现字符级的 LSTM 文本生成

3.1 准备数据

import keras
import numpy as np
path = keras.utils.get_file(
    'nietzsche.txt',
    origin='https://s3.amazonaws.com/text-datasets/nietzsche.txt')
text = open(path).read().lower()
print('Corpus length:', len(text))

接下来,我们要提取长度为 maxlen 的序列(这些序列之间存在部分重叠),对它们进行one-hot 编码,然后将其打包成形状为 (sequences, maxlen, unique_characters) 的三维Numpy 数组。与此同时,我们还需要准备一个数组 y,其中包含对应的目标,即在每一个所提取的序列之后出现的字符(已进行 one-hot 编码)。

3.2 将字符序列向量化

# 提取 60 个字符组成的序列
maxlen = 60
# 每 3 个字符采样一个新序列
step = 3
# 保存所提取的序列
sentences = []
# 保存目标(即下一个字符)
next_chars = []
for i in range(0, len(text) - maxlen, step):
    #[0,59]
    sentences.append(text[i: i + maxlen])
    #[60]
    next_chars.append(text[i + maxlen])
print('Number of sequences:', len(sentences))
# 语料中唯一字符组成的列表
chars = sorted(list(set(text)))
print('Unique characters:', len(chars))
# 一个字典,将唯一字符映射为它在列表 chars 中的索引
char_indices = dict((char, chars.index(char)) for char in chars)
#将字符 one-hot 编码为二进制数组
print('Vectorization...')
x = np.zeros((len(sentences), maxlen, len(chars)), dtype=np.bool)
y = np.zeros((len(sentences), len(chars)), dtype=np.bool)
for i, sentence in enumerate(sentences):
    for t, char in enumerate(sentence):
        x[i, t, char_indices[char]] = 1
    y[i, char_indices[next_chars[i]]] = 1

3.3 构建网络

from keras import layers
model = keras.models.Sequential()
model.add(layers.LSTM(128, input_shape=(maxlen, len(chars))))
model.add(layers.Dense(len(chars), activation='softmax'))
optimizer = keras.optimizers.RMSprop(lr=0.01)
model.compile(loss='categorical_crossentropy', optimizer=optimizer)
model.summary()

3.4 训练语言模型并从中采样

给定一个训练好的模型和一个种子文本片段,我们可以通过重复以下操作来生成新的文本。

  • (1) 给定目前已生成的文本,从模型中得到下一个字符的概率分布。
  • (2) 根据某个温度对分布进行重新加权。
  • (3) 根据重新加权后的分布对下一个字符进行随机采样。
  • (4) 将新字符添加到文本末尾。

下列代码将对模型得到的原始概率分布进行重新加权,并从中抽取一个字符索引[采样函数(sampling function)]。

3.5 给定模型预测,采样下一个字符的函数

def sample(preds, temperature=1.0):
    preds = np.asarray(preds).astype('float64')
    preds = np.log(preds) / temperature
    exp_preds = np.exp(preds)
    preds = exp_preds / np.sum(exp_preds)
    # 第一个参数为:Number of experiments
    probas = np.random.multinomial(1, preds, 1)
    return np.argmax(probas)

3.6 文本生成循环

import random
import sys
for epoch in range(1, 60):
    print('epoch', epoch)
    #将模型训练 60 轮
    #将模型在数据上拟合一次
    model.fit(x, y,
              batch_size=128,
              epochs=1)
    # 随机选择一个文本种子
    start_index = random.randint(0, len(text) - maxlen - 1)
    generated_text = text[start_index: start_index + maxlen]
    print('--- Generating with seed: "' + generated_text + '"')
    #尝试一系列不同的采样温度
    for temperature in [0.2, 0.5, 1.0, 1.2]:
        print('------ temperature:', temperature)
        sys.stdout.write(generated_text)
        # 从种子文本 开 始,生成 400个字符
        for i in range(400):
            #对目前生成的字符进行one-hot 编码
            sampled = np.zeros((1, maxlen, len(chars)))
            for t, char in enumerate(generated_text):
                sampled[0, t, char_indices[char]] = 1.
            #对下一个字符进行采样
            preds = model.predict(sampled, verbose=0)[0]
            next_index = sample(preds, temperature)
            next_char = chars[next_index]
            generated_text += next_char
            generated_text = generated_text[1:]
            sys.stdout.write(next_char)
            sys.stdout.flush()
        print()

可见,较小的温度值会得到极端重复和可预测的文本,但局部结构是非常真实的,特别是所有单词都是真正的英文单词(单词就是字符的局部模式)。随着温度值越来越大,生成的文本也变得更有趣、更出人意料,甚至更有创造性,它有时会创造出全新的单词,听起来有几分可信(比如 eterned 和 troveration)。对于较大的温度值,局部模式开始分解,大部分单词看起来像是半随机的字符串。毫无疑问,在这个特定的设置下,0.5 的温度值生成的文本最为有趣。一定要尝试多种采样策略!在学到的结构与随机性之间,巧妙的平衡能够让生成的序列非常有趣。

你可能感兴趣的:(深度学习)