- 基于语言模型的文本生成
基于深度学习方法的文本生成

$$P(w_1, w_2,...,w_n)$$ 也即语言模型描述了这些单词组成这个语言序列句子的概率分布。
首先,由链式法则 chain rule 可以得到:
$$P(w_1, w_2,...,w_n) = P(w_1)P(w_2|w_1)···P(w_n|w_1,...,w_{n-1})$$
马尔科夫假设,当前词只依赖前面 n-1 个词。
$$P(w_i|w_1,w_2,...,w_{i-1}) = P(w_i|w_{i-n+1},...,w_{i-1})$$
基于上式,定义 n-gram 语言模型如下:
n=1 unigram: $$P(w_1,w_2,...,w_n) = \prod_{i=1}^{n}P(w_i)$$
n=2 bigram: $$P(w_1,w_2,...,w_n) = \prod_{i=1}^{n}P(w_i|w_{i-1})$$
n=3 trigram: $$P(w_1,w_2,...,w_n) = \prod_{i=1}^{n}P(w_i|w_{i-2},w_{i-1})$$
$$P(w_i|w_{i-1}) = \frac{P(w_iw_{i-1})}{P(w_{i-1})} = \frac{count(w_iw_{i-1})}{count(w_{i-1})}$$
应用场景:
- 候选句打分:常应用在文本纠错、语音识别等场景中。比如: $P(I\;am\;happy) > P(I\;am\;happen)$
- 文本生成:比如广告文案、机器人写作、机器翻译。
- 文本到文本的生成
- 结构化数据生成文本
- 图像到文本的生成
基于深度学习的文本生成,常用的是 Seq2Seq 模型 $(Encoder-Decoder)$。利用 Attention 机制的 Seq2Seq 模型可以加强单词和特征之间的对齐,在生成文字的时候,模拟人类注意力转移的过程,生成更加复合人类习惯的文本。
一旦训练好了这样一个语言模型,就可以从中采样$(sample,即生成新序列)$。向模型中输入一个初始文本字符串$(conditioning data, 即条件数据)$,要求模型生成下一个字符或下一个单词(甚至可以同时生成多个标记),然后将生成的输出添加到输入数据中,并多次重复这一过程。这个循环可以生成任意长度的序列,这些序列反应了模型训练数据的结构,它们与人类书写的句子几乎相同。

$Argmax\;Decoding$:
主要包括:$beam\;search$, $class-factored\;softmax$ 等。
$Stochastic\;Decoding$:
主要包括 $temperature\;sampling$, $topK\;sampling$ 等。
在 $Seq2Seq$ 模型中,$RNN\;Encoder$ 对输入句子进行编码,生成一个大小固定的 $hidden\;state\;h_c$;基于输入句子的 $hidden\;state\;h_c$ 和先前生成的第 $1$ 到 $t-1$ 个词 $x_{1 : t−1}$,$RNN\;Decoder$ 会生成当前第 $t$ 个词的 $hidden\;state\;h_t$,最后通过 $softmax$ 函数得到第 $t$ 个词 $x_t$ 的 $vocabulary\;probability\;distribution\; P(x|x_{1:t−1})$。
两类 $decoding\;strategy$ 的主要区别就在于,如何从$vocabulary\;probability\;distribution\; P(x|x_{1:t−1})$ 中选取一个词 $x_t$ :
- $Argmax\;Decoding$ 的做法是选择词表中 $probability$ 最大的词,即 $x_t = argmaxP(x|x_{1:t−1})$;
- $Stochastic\;Decoding$ 则是基于概率分布 $P(x|x_{1:t−1})$ 随机 $sample$ 一个词 $x_t$,即 $x_t∼P(x|x_{1:t−1})$。
在做 $seq\;predcition$ 时,需要根据假设模型每个时刻 $softmax$ 的输出概率来 $sample$ 单词,合适的$sample$ 方法可能会获得更有效的结果。
核心思想:每一步取当前最大可能性的结果,作为最终结果。
具体方法:获得新生成的词是vocab中各个词的概率,取 $argmax$ 作为需要生成的词向量索引,继而生成后一个词。
核心思想: $beam\;search$尝试在广度优先基础上进行进行搜索空间的优化(类似于剪枝)达到减少内存消耗的目的。
具体方法:在 $decoding$ 的每个步骤,我们都保留着 $top-K$ 个可能的候选单词,然后到了下一个步骤的时候,我们对这 $K$ 个单词都做下一步 $decoding$,分别选出 $top K$,然后对这 $K^2$ 个候选句子再挑选出 $top K$ 个句子。以此类推一直到 $decoding$ 结束为止。当然 $Beam\;Search$ 本质上也是一个 $greedy\;decoding$ 的方法,所以我们无法保证自己一定可以得到最好的 $decoding$ 结果。
$Greedy\;Search$ 和 $Beam\;Search$ 存在的问题:
- 容易出现重复的、可预测的词;
- 句子/语言的连贯性差。
核心思想: 根据单词的概率分布随机采样。
具体方法:在 $softmax$ 中引入一个 $temperature$ 来改变 $vocabulary\;probability\;distribution$,使其更偏向 $high\;probability\;words$:
另一种表示:假设 $p(x)$ 为模型输出的原始分布,给定一个 $temperature$ 值,将按照下列方法对原始概率分布(即模型的 $softmax$ 输出) 进行重新加权,计算得到一个新的概率分布。
当 $temperature→0$,就变成 $greedy\;search$;当 $temperature→\infty$,就变成均匀采样$(uniform\;sampling)$。详见论文:The Curious Case of Neural Text Degeneration
可以缓解生成罕见单词的问题。比如说,我们可以每次只在概率最高的 $50$ 个单词中按照概率分布做采样。我只保留 $topk$ 个 $probability$ 的单词,然后在这些单词中根据概率做 $sampling$。
核心思想:对概率进行降序排序,然后对第k个位置之后的概率转换为 $0$。
具体方法:在 $decoding$ 过程中,从 $P(x|x_{1:t−1})$ 中选取 $probability$ 最高的前 $k$ 个 $tokens$,把它们的 $probability$ 加总得到 $p′=∑P(x|x_{1:t−1})$ ,然后将 $P(x|x_{1:t−1})$ 调整为 $P′(x|x_{1:t−1})=\frac{P(x|x_{1:t−1})}{p′}$ ,其中 $x∈V(k)!$ ,最后从 $P′(x|x_{1:t−1})$ 中 $sample$ 一个 $token$ 作为$output\;token$。详见论文:Hierarchical Neural Story Generation
但 $Top-k\;Sampling$ 存在的问题是,常数 $k$ 是提前给定的值,对于长短大小不一,语境不同的句子,我们可能有时需要比 $k$ 更多的 $tokens$。
核心思想:通过对概率分布进行累加,然后当累加的值超过设定的阈值 $p$,则对之后的概率进行置 $0$。
具体方法:提出了 $Top-p\;Sampling$ 来解决 $Top-k\;Sampling$ 的问题,基于 $Top-k\;Sampling$,它将 $p′=∑P(x|x_{1:t−1})$ 设为一个提前定义好的常数 $p′∈(0,1)$ ,而$selected\;tokens$ 根据句子 $history\;distribution$ 的变化而有所不同。详见论文:The Curious Case of Neural Text Degeneration
本质上 $Top-p\;Sampling$ 和 $Top-k\;Sampling$ 都是从 $truncated\;vocabulary\;distribution$ 中 $sample\;token$,区别在于置信区间的选择。
- 生成的句子容易不连贯,上下文比较矛盾。
- 容易生成奇怪的句子,出现罕见词。
在采样的过程中引入随机性,即从下一个字符的概率分布中进行采样。这叫作随机采样$(stochastic\;sampling)$,在这种情况下,根据模型结果,如果下一个字符是 e 的概率为 0.3,那么就有 30% 的概率选择它。假设 $p(x)$ 为模型输出的原始分布,则基于 softmax 采样得到的新分布:
$$\pi(x_k) = \frac{e^{log(p(x_k))}}{\sum_{i=1}^{n}e^{log(p(x_i))}}$$
考虑一个极端的例子——纯随机采样: 即从均匀概率分布中抽取下一个字符,其中每个字符的概率相同。这种方案具有最大的随机性,换句话说,这种概率分布具有最大的熵。当然,他不会生成任何有趣的内容。
再来看另一个极端——贪婪采样:贪婪采样也不会生成任何有趣的内容,它没有任何随机性,即相应的概率分布具有最小的熵。从"真实概率分布"(即模型 Softmax 函数输出的分布)中进行采样,是这两个极端之间的一个中间点。但是,还有许多其他中间点具有更大或更小的熵,你可能都想研究一下。更小的熵可以让生成的序列具有更加可预测的结构,因此看起来更真实。而更大的熵会得到更加出人意料且更有创造性的序列。从生成式模型中进行采样时,在生成过程中探索不同的随机性大小总是好的做法。我们人类是生成数据是否有趣的最终判断者,所以有趣是非常主观的,我们无法提前知道最佳熵的位置。
import numpy as np
def reweight_distribution(original_distribution, temperature=0.5):
# original_distribution 是概率值组成的一维 Numpy 数组。temperature 是一个因子,用于定量描述输出分布的熵。
distribution = np.log(original_distribution) / temperature
distribution = np.exp(distribution)
# 返回原始分布重新加权后的结果。distribution 的求和可能不再等于1,因此需要归一化
return distribution / np.sum(distribution)

文本序列生成的程序流程:
- 建立 $word2index$,$index2word$ 表。
生成批次(Batch generation)
- 数据格式的预处理:$batch\_generator(data, batch\_size, n\_steps)$.
- 数据的连续采样,要求相邻批次的序列是连续的。需要设置一个批次/周期内的序列个数 $batch\_size$ 以及每个序列中所含字符数 $n\_steps$ ,这样可以得到一个批次/周期数量。
def batch_generator(data, batch_size, n_steps):
data = copy.copy(data)
batch_steps = batch_size * n_steps
n_batches = int(len(data) / batch_steps)
data = data[:batch_size * n_batches]
data = data.reshape((batch_size, -1))
while True:
np.random.shuffle(data)
for n in range(0, data.shape[1], n_steps):
x = data[:, n:n + n_steps]
y = np.zeros_like(x)
y[:, :-1], y[:, -1] = x[:, 1:], x[:, 0]
yield x, y
假设训练集含有 $30$ 个字符,调用 $batch\_generator(data, 2, 6)$.
每个 $batch$ 的行数就是每个周期的句子数,列数就是 $\frac{len(data)}{batch_size}$得到的数值。
这个网络是由多层 $LSTM$ 堆叠组成,然后根据所有时刻的输出 $lstm\_outputs$,对词典中所有词的 $softmax$ 得到概率。
采样策略:贪婪采样,随机采样
采样函数可以选择 $Temperature$ $Sampling$ 或 $TopK$ $Sampling$,分别根据模型得到的原始概率分布进行重新加权或限定 $TopK$ 个概率构建新的概率分布,并从中抽取一个字符索引。
给定一个训练好的模型和一个种子文本片段,可以通过重复以下操作生成新的文本。
- 给定目前已生成的文本,从模型中得到下一个字符的概率。
- 根据某个温度对分布进行重新加权。
- 根据重新加权后的分布对下一个字符进行随机采样。
- 将新字符添加至文本末尾。
如何对生成的文本进行评价也是文本生成研究中重要的一环。
内在评价关注文本的正确性、流畅度和易理解性。常见的内在评价方法又可分为两类:
- 采用 $BLEU$、$NIST$ 和 $ROUGE$ 等进行自动化评价,评估生成文本和参考文本间相似度来衡量生成质量。
- 通过人工评价,从有用性等对文本进行打分。
找出输出句子与参考句子之间的 $n-gram$ 重叠部分并对(比参考句子)更短的输出句子施以惩罚的评价方法。
论文地址:BLEU: a Method for Automatic Evaluation of Machine Translation
它基于 $Ngram$ 的稀缺性对其进行加权。这就意味着对某个稀缺 $Ngram$ 的正确匹配能提高的分数,要多于对某个常见的 $Ngram$ 的正确匹配。
论文地址:Automatic Evaluation of Machine Translation QualityUsing N-gram Co-Occurrence Statistics
由于 $BLEU$ 只考虑了精确率而没有考虑召回率,因此,在 $2004$ 年,Chin-Yew Lin 提出了一种新的评估方法 $ROUGE$,该方法主要是从召回率的角度计算生成文本与参考文本之间的相似性,比较适用于文本摘要任务,因为文本摘要我们更考察生成文本包括了多少参考文本中包含的信息。作者在论文中总共提出了 $4$ 种不同的计算方式:$ROUGE-N$、$ROUGE-L$、$ROUGE-W$、$ROUGE-S$。
它对 $BLEU$ 进行了修改,聚焦于召回率而非准确率。换句话说,该方法看重的是参考翻译句中有多少 $Ngram$ 出现在输出句中,而不是输出句中有多少 $Ngram$ 出现在参考翻译句中。
论文地址:ROUGE: A Package for Automatic Evaluation of Summaries
$BLEU$ 只考虑精确率,$METEOR$ 则同时考虑精确率和召回率,采用加权的 $F$ 值来作为评估指标。
论文地址:Meteor: An Automatic Metric for MT Evaluation with High Levels of Correlation with Human Judgments
外在评价则关注生成文本在实际应用中的可用性。
困惑度(perplexity):是交叉熵的指数形式。给定一个包含 $n$ 个词的测试文本 $W =(w_1, w_2, ..., w_n)$ 和语言模型 $Ngram$:$P(w_1,w_2,...,w_n)=\prod ^n_{i=1}P(w_i|w_{i−N+1},...,w_{i−1})$ 的困惑度 $perplexity$ 可定义为交叉熵的指数形式:
$perplexity$ 和交叉熵一样都可以用来评价语言模型的好坏。 对于测试集其困惑度越小,准确率也就越高,语言模型也就越好。
$perplexity$ 刻画的是语言模型预测一个语言样本的能力,比如已经知道了 $W = (w_1, w_2, ..., w_n)$ 这句话会出现在语料库之中,那么通过语言模型计算得到这句话的概率越高,说明语言模型对这个语料库拟合的越好。
首先下载预料,并将其转换为小写。
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 编码,然后将其打包成形状为 $(sequence, maxlen, unique\_characters)$ 的三维 Numpy 数组。与此同时,我们还需要准备一个数组 y,其中包含对应的目标,即在每一个所提取的序列之后出现的字符(已进行 one-hot 编码)。
maxlen = 60 # 提取 60 个字符组成的序列
step = 3 # 每 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,然后是一个 Dense 分类器和对所有可能字符的 Softmax。值得注意的是,循环神经网络并不是序列数据生成的唯一方法,最近已有研究表明一维卷积神经网络也可以用于序列数据生成。
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'))
目标是经过 one-hot 编码的,所以训练模型需要使用 categorical_crossentropy 作为损失。
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)
最后,下面这个循环将反复训练并生成文本。在每轮过后都是用一系列不同的温度值来生成文本。这样我们可以看到,随着模型收敛,生成的文本如何变化,以及温度对采样的策略的影响。
import random
import sys
for epoch in range(1, 60):
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):
# 对目前生成的字符进行 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)
对于较大的温度值,局部模式开始分解,大部分单词看起来像是半随机的字符串。毫无疑问,在这个特定的设置下, 0.5 的温度值生成的文本最为有趣。一定要尝试多种采样策略,在学到的结构与随机性挚爱金,巧妙的平衡能够让生成的序列非常有趣。
值得注意,利用更多的数据训练一个更大的模型,并且训练时间更长,会让生成的样本看起来更连贯、更真实。但是,不要期待能够生成任何有意义的文本,除非是很偶然的情况。我们所做的一切只是从一个统计模型中对数据进行采样,这个模型是关于字符先后顺序的模型。