生成式深度学习
机器学习模型能够对图像、音乐和故事的统计潜在空间(latent space)进行学习,然后从这个空间中采样(sample),创造出与模型在训练数据中所见到的艺术作品具有相似特征的新作品
使用 LSTM 生成文本生成序列数据
用深度学习生成序列数据的通用方法,就是使用前面的标记作为输入,训练一个网络(通常是循环神经网络或卷积神经网络)来预测序列中接下来的一个或多个标记。例如,给定输入the cat is on the ma,训练网络来预测目标 t,即下一个字符。与前面处理文本数据时一样,标记(token)通常是单词或字符,给定前面的标记,能够对下一个标记的概率进行建模的任何网络都叫作语言模型(language model)。语言模型能够捕捉到语言的潜在空间(latent space),即语言的统计结构
一旦训练好了这样一个语言模型,就可以从中采样(sample,即生成新序列)。向模型中输入一个初始文本字符串[即条件数据(conditioning data)],要求模型生成下一个字符或下一个单词(甚至可以同时生成多个标记),然后将生成的输出添加到输入数据中,并多次重复这一过程。这个循环可以生成任意长度的序列,这些序列反映了模型训练数据的结构,它们与人类书写的句子几乎相同
使用语言模型逐个字符生成文本的过程
采样策略
生成文本时,如何选择下一个字符至关重要。一种简单的方法是贪婪采样(greedy sampling),就是始终选择可能性最大的下一个字符。但这种方法会得到重复的、可预测的字符串,看起来不像是连贯的语言。一种更有趣的方法是做出稍显意外的选择:在采样过程中引入随机性,即从下一个字符的概率分布中进行采样。这叫作随机采样(stochastic sampling,stochasticity 在这个领域中就是“随机”的意思)。在这种情况下,根据模型结果,如果下一个字符是 e 的概率为0.3,那么你会有 30% 的概率选择它
从模型的 softmax 输出中进行概率采样是一种很巧妙的方法,它甚至可以在某些时候采样到不常见的字符,从而生成看起来更加有趣的句子,而且有时会得到训练数据中没有的、听起来像是真实存在的新单词,从而表现出创造性。但这种方法有一个问题,就是它在采样过程中无法控制随机性的大小
为了在采样过程中控制随机性的大小,我们引入一个叫作 softmax 温度(softmax temperature)的参数,用于表示采样概率分布的熵,即表示所选择的下一个字符会有多么出人意料或多么可预测。给定一个 temperature 值,将按照下列方法对原始概率分布(即模型的 softmax 输出)进行重新加权,计算得到一个新的概率分布
import numpy as np def reweight_distribution(original_distribution, temperature=0.5): #original_distribution 是概率值组成的一维 Numpy 数组,这些概率值之和必须等于 1。temperature 是一个因子,用于定量描述输出分布的熵 distribution = np.log(original_distribution) / temperature distribution = np.exp(distribution) return distribution / np.sum(distribution)更高的温度得到的是熵更大的采样分布,会生成更加出人意料、更加无结构的生成数据,而更低的温度对应更小的随机性,以及更加可预测的生成数据
对同一个概率分布进行不同的重新加权。更低的温度 = 更确定,更高的温度 = 更随机
实现字符级的 LSTM 文本生成
首先下载语料,并将其转换为小写。接下来,我们要提取长度为 maxlen 的序列(这些序列之间存在部分重叠),对它们进行one-hot 编码,然后将其打包成形状为 (sequences, maxlen, unique_characters) 的三维Numpy 数组。与此同时,还需要准备一个数组 y,其中包含对应的目标,即在每一个所提取的序列之后出现的字符 ,下一步,构建网络。最后训练语言模型并从中采样
给定一个训练好的模型和一个种子文本片段,我们可以通过重复以下操作来生成新的文本
给定目前已生成的文本,从模型中得到下一个字符的概率分布
根据某个温度对分布进行重新加权
根据重新加权后的分布对下一个字符进行随机采样
将新字符添加到文本末尾
demo
import keras import numpy as np from keras import layers import random import sys 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 = 60 step = 3 sentences = [] next_chars = [] for i in range(0, len(text) - maxlen, step): sentences.append(text[i: i + maxlen]) 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) print('Vectorization...') # 将字符 one-hot 编码为二进制数组 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 # 用于预测下一个字符的单层 LSTM 模型 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) # 模型预测,采样下一个字符的函数 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) probas = np.random.multinomial(1, preds, 1) return np.argmax(probas) for epoch in range(1, 41): print('epoch', epoch) 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) for i in range(400): 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)