TensorFlow 实战(四)(4)https://developer.aliyun.com/article/1522808
10.3 测量生成文本的质量
性能监控已经成为我们在每一章节模型之旅中不可或缺的一部分。在这里也不例外。性能监控是我们语言模型的一个重要方面,我们需要找到适合语言模型的度量标准。自然地,考虑到这是一个分类任务,你可能会想,“准确率不是一个很好的度量标准吗?”嗯,在这个任务中不完全是这样。
例如,如果语言模型得到了句子“I like my pet dog”,然后当要求预测给定“我喜欢我的宠物 ____”时缺失的单词,模型可能会预测“猫”,准确率为零。但这是不正确的;在这个例子中,“猫”和“狗”一样有意义。这里有更好的解决方案吗?
这就是困惑度!直观地,困惑度 衡量了模型看到前一个词序列后看到目标词时的“惊讶程度”。在理解困惑度之前,你需要了解“熵”是什么意思。
熵 是由著名的克劳德·香农创造的一个术语,他被认为是信息论之父。熵度量了事件的惊奇程度/不确定性/随机性。事件可以由概率分布生成所有可能的结果之一。例如,如果你考虑抛硬币(以概率 p 出现正面)是一个事件,如果 p = 0.5,那么你将有最大的熵,因为这是抛硬币最不确定的情况。如果 p = 1 或 p = 0,则熵最小,因为在抛硬币之前你知道结果是什么。
熵的原始解释是发送信号或消息通知事件所需的位数的期望值。位是内存的单位,可以是 1 或 0。例如,你是一支与 A 国和 B 国交战的军队的指挥官。现在有四种可能性:A 和 B 都投降,A 赢了 B 输了,A 输了 B 赢了,以及 A 和 B 都赢了。如果所有这些事件发生的可能性都是相等的,你需要两位来发送消息,其中每一位表示那个国家是否获胜。随机变量 X 的熵由方程式量化
其中 x 是 X 的一个结果。信不信由你,每次我们使用分类交叉熵损失时,我们都在不知不觉中使用了这个方程式。分类交叉熵的关键就在于这个方程式。回到困惑度度量,困惑度就是简单地
困惑度 = 2^(H(X))
由于困惑度是熵的一个函数,它衡量了模型看到目标词时的惊讶程度/不确定性,考虑了前一个词序列。困惑度也可以被认为是给定信号的所有可能组合的数量。例如,假设你发送一个带有两位的消息,所有事件都是等可能的;那么熵 = 2,这意味着困惑度 = 2² = 4。换句话说,两位可以有四种组合:00、01、10 和 11。
从建模角度来看,你可以将困惑度理解为在给定一系列前序词的情况下,模型认为有多少不同的目标适合作为下一个词的空白。这个数字越小越好,因为这意味着模型试图从更小的子集中找到一个词,表明语言理解的迹象。
要实现困惑度,我们将定义一个自定义指标。计算非常简单。我们计算分类交叉熵,然后对其进行指数化以获得困惑度。分类交叉熵简单地是熵的扩展,用于在具有两个以上类别的分类问题中测量熵。对于输入示例(x[i],y[i]),它通常定义为
其中y[i]表示真实类别示例所属的独热编码向量,ŷ[i]是C个元素的预测类别概率向量,其中ŷ[i,c]表示示例属于类别c的概率。请注意,在实践中,计算更快的是使用指数(自然)基数,而不是使用 2 作为基数。以下清单描述了这个过程。
10.5 实现困惑度指标
import tensorflow.keras.backend as K class PerplexityMetric(tf.keras.metrics.Mean): def __init__(self, name='perplexity', **kwargs): super().__init__(name=name, **kwargs) self.cross_entropy = tf.keras.losses.SparseCategoricalCrossentropy( from_logits=False, reduction='none' ) def _calculate_perplexity(self, real, pred): ❶ loss_ = self.cross_entropy(real, pred) ❷ mean_loss = K.mean(loss_, axis=-1) ❸ perplexity = K.exp(mean_loss) ❹ return perplexity def update_state(self, y_true, y_pred, sample_weight=None): perplexity = self._calculate_perplexity(y_true, y_pred) super().update_state(perplexity)
❶ 定义一个函数来计算给定真实和预测目标的困惑度。
❷ 计算分类交叉熵损失。
❸ 计算损失的均值。
❹ 计算均值损失的指数(困惑度)。
我们正在做的事情非常简单。首先,我们对 tf.keras.metrics.Mean 类进行子类化。tf.keras.metrics.Mean 类将跟踪传递给其 update_state()函数的任何输出指标的均值。换句话说,当我们对 tf.keras.metrics.Mean 类进行子类化时,我们不需要手动计算累积困惑度指标的均值,因为训练继续进行。这将由该父类自动完成。我们将定义我们将在 self.cross_entropy 变量中使用的损失函数。然后,我们编写函数 _calculate_perplexity(),该函数接受模型的真实目标和预测。我们计算逐样本损失,然后计算均值。最后,为了得到困惑度,我们对均值损失进行指数化。有了这个,我们可以编译模型:
model.compile( loss='sparse_categorical_crossentropy', optimizer='adam', metrics=['accuracy', PerplexityMetric()] )
在本节中,我们学习了用于评估语言模型的性能指标,如熵和困惑度。此外,我们实现了一个自定义的困惑度指标,用于编译最终模型。接下来,我们将在准备好的数据上训练我们的模型,并评估生成文本的质量。
练习 3
想象一个有三个输出的分类问题。有两种不同预测的情况:
情景 A:标签 [0, 2, 1]
预测:[[0.6, 0.2, 0.2], [0.1, 0.1, 0.8], [0.3, 0.5, 0.2]]
情景 B:标签 [0, 2, 1]
预测:[[0.3, 0.3, 0.4], [0.4, 0.3, 0.3], [0.3, 0.3, 0.4]]
哪一个会有最低的困惑度?
10.4 训练和评估语言模型
在这一部分中,我们将训练模型。在训练模型之前,让我们使用之前实现的 get_tf_pipeline()函数来实例化训练和验证数据集。我们将只使用前 50 个故事(共 98 个)作为训练集,以节省时间。我们每次取 100 个二元组作为一个序列,通过移动窗口来跳过故事,每次移动 25 个二元组。这意味着单个故事序列的起始索引是 0、25、50 等等。我们将使用批大小为 128:
n_seq = 100 train_ds = get_tf_pipeline( train_data_seq[:50], n_seq, stride=25, batch_size=128 ) valid_ds = get_tf_pipeline( val_data_seq, n_seq, stride=n_seq, batch_size=128 )
要训练模型,我们将像以前一样定义回调。我们将定义
- 一个 CSV 记录器,将在训练期间记录性能
- 一个学习率调度器,当性能达到平台期时会减小学习率
- 如果性能没有提高,则使用早期停止回调来终止训练。
os.makedirs('eval', exist_ok=True) csv_logger = ➥ tf.keras.callbacks.CSVLogger(os.path.join('eval','1_language_modelling. ➥ log')) monitor_metric = 'val_perplexity' mode = 'min' print("Using metric={} and mode={} for EarlyStopping".format(monitor_metric, mode)) lr_callback = tf.keras.callbacks.ReduceLROnPlateau( monitor=monitor_metric, factor=0.1, patience=2, mode=mode, min_lr=1e-8 ) es_callback = tf.keras.callbacks.EarlyStopping( monitor=monitor_metric, patience=5, mode=mode, ➥ restore_best_weights=False )
最后,是时候训练模型了。我想知道我能从训练好的模型中挤出什么样的酷故事:
model.fit(train_ds, epochs=50, validation_data = valid_ds, ➥ callbacks=[es_callback, lr_callback, csv_logger])
注意 在配有 NVIDIA GeForce RTX 2070 8 GB 的 Intel Core i5 机器上,训练大约需要 1 小时 45 分钟运行 25 个 epochs。
训练模型后,您将看到接近 9.5 的验证困惑度。换句话说,这意味着对于给定的单词序列,模型认为可能有 9.5 个不同的下一个单词是正确的单词(不完全准确,但是这是一个足够接近的近似值)。困惑度需要仔细判断,因为其好坏倾向于主观。例如,随着词汇量的增加,这个数字可能会上升。但这并不一定意味着模型不好。数字之所以上升是因为模型看到了更多适合场合的词汇,与词汇量较小时相比。
我们将在测试数据上评估模型,以了解我们的模型可以多大程度地预测一些未见的故事,而不会感到惊讶:
batch_size = 128 test_ds = get_tf_pipeline( test_data_seq, n_seq, shift=n_seq, batch_size=batch_size ) model.evaluate(test_ds)
这将给您约
61/61 [==============================] - 2s 39ms/step - loss: 2.2620 - ➥ accuracy: 0.4574 - perplexity: 10.5495
与我们看到的验证性能相当。最后,保存模型
os.makedirs('models', exist_ok=True) tf.keras.models.save_model(model, os.path.join('models', '2_gram_lm.h5'))
在本节中,您学习了如何训练和评估模型。您在训练数据集上训练了模型,并在验证和测试集上评估了模型。在接下来的部分中,您将学习如何使用训练好的模型生成新的儿童故事。然后,在接下来的部分中,您将学习如何使用我们刚刚训练的模型生成文本。
练习 4
假设您想使用验证准确性(val_accuracy)而不是验证困惑度(val_perplexity)来定义早期停止回调。您将如何更改以下回调?
es_callback = tf.keras.callbacks.EarlyStopping( monitor=’val_perlexity’, patience=5, mode=’min’, ➥ restore_best_weights=False )
10.5 从语言模型生成新文本:贪婪解码
语言模型最酷的一点是其具有的生成特性。这意味着模型可以生成新数据。在我们的情况下,语言模型可以利用从训练阶段获取的知识生成新的儿童故事。
但要这么做,我们必须付出额外的努力。文本生成过程与训练过程不同。训练期间我们拥有完整的序列,可以一次性处理任意长度的序列。但是在生成新的文本时,你没有一个可用的文本序列;事实上,你正在尝试生成一个。你从一个随机的词开始,得到一个输出词,然后递归地将当前输出作为下一个输入来生成新的文本。为了促进这个过程,我们需要定义训练模型的一个新版本。让我们更详细地阐述生成过程。图 10.5 比较了训练过程和生成/推理过程。
- 定义一个初始单词wt。
- 定义一个初始状态向量ht。
- 定义一个列表 words,用于保存预测的单词,并将其初始化为初始单词。
- 对于 t 从 1 到 n:
- 从模型中获取下一个单词(w[t+1])和状态向量(h[t+1])并分别赋值给w[t]和h[t],这样就创建了一个递归过程,使我们能够生成尽可能多的单词。
- 将新单词添加到 words 中。
图 10.5 训练时间和推断/解码阶段的语言模型比较。在推断阶段,我们逐个时间步预测。在每个时间步中,我们将预测的单词作为输入,将新的隐藏状态作为下一个时间步的先前隐藏状态。
我们将使用 Keras 的函数式 API 构建这个模型,如下一个列表所示。首先,让我们定义两个输入。
列表 10.6 是推断/解码语言模型的实现。
inp = tf.keras.layers.Input(shape=(None,)) ❶ inp_state = tf.keras.layers.Input(shape=(1024,)) ❷ emb_layer = tf.keras.layers.Embedding( input_dim=n_vocab+1, output_dim=512, input_shape=(None,) ) ❸ emb_out = emb_layer(inp) ❹ gru_layer = tf.keras.layers.GRU( 1024, return_state=True, return_sequences=True ) gru_out, gru_state = gru_layer(emb_out, initial_state=inp_state) ❺❻ dense_layer = tf.keras.layers.Dense(512, activation='relu') ❼ dense_out = dense_layer(gru_out) ❼ final_layer = tf.keras.layers.Dense(n_vocab, name='final_out') ❽ final_out = final_layer(dense_out) ❽ softmax_out = tf.keras.layers.Activation(activation='softmax')(final_out) ❽ infer_model = tf.keras.models.Model( inputs=[inp, inp_state], outputs=[softmax_out, gru_state] ) ❾
❶ 定义一个能够接受任意长度的单词 ID 序列的输入。
❷ 定义另一个输入,将上一个状态输入进去。
❸ 定义一个嵌入层。
❹ 从输入的单词 ID 中获取嵌入向量。
❺ 定义一个 GRU 层,返回输出和状态。但要注意,对于 GRU 来说它们是相同的。
❻ 从模型中获取 GRU 输出和状态。
❼ 计算第一个全连接层的输出。
❽ 定义一个与词汇表大小相同的最终层,并获取模型的最终输出。
❾ 定义最终模型,该模型接受一个输入和一个状态向量作为输入,并产生下一个单词预测和新的状态向量作为输出。
在定义模型之后,我们必须执行一个重要的步骤。我们必须将训练模型的权重转移到新定义的推断模型中。为此,我们必须识别具有可训练权重的层,从训练模型中获取这些层的权重,并将它们分配给新模型:
# Copy the weights from the original model emb_layer.set_weights(model.get_layer('embedding').get_weights()) gru_layer.set_weights(model.get_layer('gru').get_weights()) dense_layer.set_weights(model.get_layer('dense').get_weights()) final_layer.set_weights(model.get_layer('final_out').get_weights())
要获取训练模型中特定层的权重,可以调用
model.get_layer(<layer name>).get_weights()
将返回一个带有权重的 NumPy 数组。接下来,为了将这些权重分配给一个层,调用
layer.set_weights(<weight matrix>)
现在我们可以递归地调用新定义的模型来生成任意数量的 bigram。我们将更详细地讨论如何进行这个过程。我们将不再从一个随机单词开始,而是从一段文本序列开始。我们将使用 Tokenizer 将文本转换为 bigrams,然后再转换为单词 ID:
text = get_ngrams( "CHAPTER I. Down the Rabbit-Hole Alice was beginning to get very tired ➥ of sitting by her sister on the bank ,".lower(), ngrams ) seq = tokenizer.texts_to_sequences([text])
接下来,让我们重置模型的状态(这在这里不是必需的,因为我们是从头开始,但了解我们可以这样做很好)。我们将定义一个全零的状态向量:
# Reset the state of the model initially model.reset_states() # Defining the initial state as all zeros state = np.zeros(shape=(1,1024))
然后,我们将递归地对 seq 变量中的每个 bigram 进行预测,以更新 GRU 模型的状态。一旦我们遍历整个序列,我们将得到最终预测的 bigram(它将成为我们的第一个预测的 bigram),并将其附加到原始的 bigram 序列中:
# Recursively update the model by assining new state to state for c in seq[0]: out, state = infer_model.predict([np.array([[c]]), state]) # Get final prediction after feeding the input string wid = int(np.argmax(out[0],axis=-1).ravel()) word = tokenizer.index_word[wid] text.append(word)
我们将使用上一个预测的最后一个单词的 ID 来定义一个新的输入 x:
# Define first input to generate text recursively from x = np.array([[wid]])
现在开始有趣的部分。我们将使用之前讨论的方法来预测 500 个 bigram(即 1,000 个字符)。在每次迭代中,我们使用输入 x 和状态向量 state,通过 infer_model 来预测一个新的 bigram 和一个新的状态。然后,我们将这些新的输出递归地替换 x 和 state 变量(请参见下一个列表)。
列表 10.7 使用先前的单词作为输入递归预测新单词
for _ in range(500): out, state = infer_model.predict([x, state]) ❶ out_argsort = np.argsort(out[0], axis=-1).ravel() ❷ wid = int(out_argsort[-1]) ❷ word = tokenizer.index_word[wid] ❷ if word.endswith(' '): ❸ if np.random.normal()>0.5: width = 3 ❹ i = np.random.choice( ❹ list(range(-width,0)), p=out_argsort[-width:]/out_argsort[-width:].sum() ) wid = int(out_argsort[i]) ❹ word = tokenizer.index_word[wid] ❹ text.append(word) ❺ x = np.array([[wid]]) ❻
❶ 获取下一个输出和状态。
❷ 从输出中获取单词 ID 和单词。
❸ 如果单词以空格结尾,我们引入了一点随机性来打破重复文本。
❹ 根据它们的可能性,从该时间步的前三个输出中选择一个输出。
❺ 累积地将预测附加到文本中。
❻ 递归地将当前预测作为下一个输入。
请注意,需要一些工作才能得到 x 的最终值,因为模型预测的是一个概率预测(赋值给 out),而不是一个单词 ID。此外,我们将使用一些附加的逻辑来提高生成文本中的随机性(或者可以说是熵),通过从前三个单词中随机选择一个单词。但我们不以相等的概率选择它们。相反,让我们使用它们的预测概率来预测单词。为了确保我们不会获得过多的随机性,并避免在单词中间获得随机调整,只有当最后一个字符是空格字符时,才这样做。最终的单词 ID(可以是具有最高概率的单词或随机选择的单词)被赋值给变量 x。这个过程将重复进行 500 步,到最后,你将拥有一个酷炫的机器生成的故事。你可以打印出最终的文本查看其效果。要做到这一点,只需将 bigrams 按照下面的方式连接在文本序列中:
# Print the final output print('\n') print('='*60) print("Final text: ") print(''.join(text))
这将显示
Final text: chapter i. down the rabbit-hole alice was beginning to get very tired of ➥ sitting by her sister on the bank , and then they went to the shore , ➥ and then the princess was so stilling that he was a little girl , ... it 's all right , and i 'll tell you how young things would n't be able to ➥ do it . i 'm not goin ' to think of itself , and i 'm going to be sure to see you . i 'm sure i can notice them . i 'm going to see you again , and i 'll tell you what i 've got , '
对于简单的单层 GRU 模型来说,这当然不算差。 大多数情况下,模型会输出实际单词。 但是偶尔会出现拼写错误和更频繁的语法错误困扰文本。 我们能做得更好吗? 在下一节中,我们将学习一种称为光束搜索的新技术,用于生成文本。
练习 5
假设您有以下代码,该代码选择下一个单词时没有随机性。 您运行此代码并意识到结果非常糟糕:
for _ in range(500): out, new_s = infer_model.predict([x, s]) out_argsort = np.argsort(out[0], axis=-1).ravel() wid = int(out_argsort[-1]) word = tokenizer.index_word[wid] text.append(word) x = np.array([[wid]])
你认为性能不佳的原因是什么?
10.6 光束搜索:增强序列模型的预测能力
我们可以比贪婪解码做得更好。 光束搜索是一种流行的解码算法,用于像这样的序列/时间序列任务中生成更准确的预测。 光束搜索背后的思想非常简单。 与贪婪解码不同,贪婪解码预测单个时间步长,而光束搜索预测多个时间步长。 在每个时间步长,您获得前 k 个预测并从中分支出。 光束搜索具有两个重要参数:光束宽度和光束深度。 光束宽度控制每个步骤考虑的候选项数,而光束深度确定要搜索的步骤数。 例如,对于光束宽度为 3 和光束深度为 5,可能的选项数为 3⁵ = 243。 图 10.6 进一步说明了光束搜索的工作原理。
图 10.6 光束搜索的示例。 光束搜索会预测未来几步以进行预测,从而导致更好的解决方案。 在这里,我们正在执行光束搜索,光束宽度为 3,光束深度为 5。
首先,让我们定义一个函数,该函数将接受模型、输入和状态,并返回输出和新状态:
def beam_one_step(model, input_, state): """ Perform the model update and output for one step""" output, new_state = model.predict([input_, state]) return output, new_state
然后,使用这个函数,我们将定义一个递归函数(recursive_fn),该函数将从预定义深度(由 beam_depth 定义)递归地预测前一次预测的下一个单词。在每个时间步长,我们考虑从中分支出的前 k 个候选项(由 beam_width 定义)。递归函数将填充一个名为 results 的变量。results 将包含一个元组列表,其中每个元组表示搜索中的单个路径。具体来说,每个元组包含
- 路径中的元素
- 该序列的联合对数概率
- 传递给 GRU 的最终状态向量
这个函数在下面的列表中概述。
将束搜索实现为递归函数的列表 10.8
def beam_search( model, input_, state, beam_depth=5, beam_width=3, ignore_blank=True ): ❶ """ Defines an outer wrapper for the computational function of beam ➥ search """ def recursive_fn(input_, state, sequence, log_prob, i): ❷ """ This function performs actual recursive computation of the long ➥ string""" if i == beam_depth: ❸ """ Base case: Terminate the beam search """ results.append((list(sequence), state, np.exp(log_prob))) ❹ return sequence, log_prob, state ❹ else: """ Recursive case: Keep computing the output using the ➥ previous outputs""" output, new_state = beam_one_step(model, input_, state) ❺ # Get the top beam_widht candidates for the given depth top_probs, top_ids = tf.nn.top_k(output, k=beam_width) ❻ top_probs, top_ids = top_probs.numpy().ravel(), ➥ top_ids.numpy().ravel() ❻ # For each candidate compute the next prediction for p, wid in zip(top_probs, top_ids): ❼ new_log_prob = log_prob + np.log(p) ❼ if len(sequence)>0 and wid == sequence[-1]: ❽ new_log_prob = new_log_prob + np.log(1e-1) ❽ sequence.append(wid) ❾ _ = recursive_fn( np.array([[wid]]), new_state, sequence, new_log_prob, i+1❿ ) sequence.pop() results = [] sequence = [] log_prob = 0.0 recursive_fn(input_, state, sequence, log_prob, 0) ⓫ results = sorted(results, key=lambda x: x[2], reverse=True) ⓬ return results
❶ 为光束搜索的计算函数定义一个外部包装器。
❷ 定义一个内部函数,该函数递归调用以找到光束路径。
❸ 定义递归终止的基本情况。
❹ 将终止时得到的结果追加到结果中,以便稍后使用。
❺ 在递归过程中,通过调用模型获取输出单词和状态。
❻ 获取该步骤的前 k 个候选项。
❼ 对于每个候选项,计算联合概率。 为了具有数值稳定性,我们将在对数空间中执行此操作。
❽ 每当相同的符号重复时,惩罚联合概率。
❾ 将当前候选项追加到维护当前搜索路径的序列中。
❿ 递归调用函数以找到下一个候选项。
⓫ 调用递归函数以触发递归。
⓬ 根据对数概率对结果进行排序。
最后,我们可以使用这个 beam_search 函数如下:我们将使用 7 的束深度和 2 的束宽。直到 for 循环之前,事情都与我们使用贪婪解码时完全相同。在 for 循环中,我们得到结果列表(按联合概率从高到低排序)。然后,类似于我们以前做的,我们将基于它们的可能性作为下一个预测从前 10 个预测中随机获取下一个预测。以下清单详细说明了这样做的代码。
清单 10.9 实现束搜索解码以生成新故事
text = get_ngrams( "CHAPTER I. Down the Rabbit-Hole Alice was beginning to get very tired ➥ of sitting by her sister on the bank ,".lower(), ngrams ) ❶ seq = tokenizer.texts_to_sequences([text]) ❷ state = np.zeros(shape=(1,1024)) for c in seq[0]: out, state = infer_model.predict([np.array([[c]]), state ❸ wid = int(np.argmax(out[0],axis=-1).ravel()) ❹ word = tokenizer.index_word[wid] ❹ text.append(word) ❹ x = np.array([[wid]]) for i in range(100): ❺ result = beam_search(infer_model, x, state, 7, 2) ❻ n_probs = np.array([p for _,_,p in result[:10 ❼ p_j = np.random.choice(list(range( n_probs.size)), p=n_probs/n_probs.sum()) ❼ best_beam_ids, state, _ = result[p_j] ❽ x = np.array([[best_beam_ids[-1]]]) ❽ text.extend([tokenizer.index_word[w] for w in best_beam_ids]) print('\n') print('='*60) print("Final text: ") print(''.join(text))
❶ 从初始文本序列中定义一系列 n 元组。
❷ 将二元组转换为单词 ID。
❸ 使用给定字符串建立模型状态。
❹ 处理序列后获取预测的单词。
❺ 预测 100 个时间步长。
❻ 从束搜索中获取结果。
❼ 基于它们的可能性获取前 10 个结果中的一个。
❽ 用计算出的新值替换 x 和状态。
运行代码清单 10.9,你应该会得到类似以下的文本:
Final text: chapter i. down the rabbit-hole alice was beginning to get very tired of ➥ sitting by her sister on the bank , and there was no reason that her ➥ father had brought him the story girl 's face . `` i 'm going to bed , '' said the prince , `` and you can not be able ➥ to do it . '' `` i 'm sure i shall have to go to bed , '' he answered , with a smile ➥ . `` i 'm so happy , '' she said . `` i do n't know how to bring you into the world , and i 'll be sure ➥ that you would have thought that it would have been a long time . there was no time to be able to do it , and it would have been a ➥ little thing . '' `` i do n't know , '' she said . ... `` what is the matter ? '' `` no , '' said anne , with a smile . `` i do n't know what to do , '' said mary . `` i 'm so glad you come back , '' said mrs. march , with
用束搜索生成的文本读起来比我们用贪婪解码看到的文本要好得多。当文本是用束搜索生成时,语法更好,拼写错误更少。
多样化的束搜索
随着时间的推移,出现了各种不同的束搜索替代方案。其中一种流行的替代方案称为多样化束搜索,介绍在 Vijayakumar 等人的论文“Diverse Beam Search: Decoding Diverse Solutions from Neural Sequence Models”中(arxiv.org/pdf/1610.02424.pdf
)。多样化束搜索克服了普通束搜索的一个关键局限性。也就是说,如果你分析束搜索提出的最优候选序列,你会发现它们之间仅有少数元素的差异。这也可能导致缺乏变化的重复文本。多样化束搜索提出了一个优化问题,在搜索过程中激励所提出的候选者的多样性。你可以在论文中进一步了解这一点。
这就结束了我们对语言建模的讨论。在下一章中,我们将学习一种称为序列到序列问题的新型 NLP 问题。让我们总结一下本章的重点。
练习 6
你使用了行 result = beam_search(infer_model, x, state, 7, 2) 来执行束搜索。你希望每次考虑五个候选项,并且只在搜索空间中搜索三层深度。你会如何更改这行?
总结
- 语言建模是在给定一系列单词的情况下预测下一个单词的任务。
- 语言建模是一些领域中表现最佳的模型的核心工作,例如 BERT(一种基于 Transformer 的模型)。
- 为了限制词汇表的大小并避免计算问题,可以使用 n-gram 表示。
- 在 n-gram 表示中,文本被分割为固定长度的标记,而不是进行字符级或词级的标记化。然后,一个固定大小的窗口在文本序列上移动,以生成模型的输入和目标。在 TensorFlow 中,您可以使用 tf.data.Dataset.window() 函数来实现这种功能。
- 门控循环单元(GRU)是一种顺序模型,其操作类似于 LSTM,它在生成每个时间步的状态的同时跳转到序列中的下一个输入。
- GRU 是 LSTM 模型的紧凑版本,它维护单个状态和两个门,但提供了与之相当的性能。
- 困惑度量衡量模型看到目标词时对输入序列的惊讶程度。
- 困惑度量的计算受信息论的启发,其中熵度量用于量化代表事件的随机变量的不确定性,其中结果是根据某些潜在的概率分布生成的。
- 训练后的语言模型可以用于生成新文本。有两种流行的技术——贪婪解码和束搜索解码:
- 贪婪解码一次预测一个词,其中预测的词被用作下一个时间步的输入。
- 束搜索解码预测未来的几个步骤,并选择给出最高联合概率的序列。
练习答案
练习 1
ds = tf.data.Dataset.from_tensor_slices(x) ds = ds.window(5,shift=5).flat_map( lambda window: window.batch(5, drop_remainder=True) ) ds = ds.map(lambda xx: (xx[:-2], xx[2:]))
练习 2
model = tf.keras.models.Sequential([ tf.keras.layers.Embedding( input_dim=n_vocab+1, output_dim=512,input_shape=(None,) ), tf.keras.layers.GRU(1024, return_state=False, return_sequences=True), tf.keras.layers.GRU(512, return_state=False, return_sequences=True), tf.keras.layers.Dense(n_vocab, activation=’softmax’, name='final_out'), ])
练习 3
场景 A 将具有最低的困惑度。
练习 4
es_callback = tf.keras.callbacks.EarlyStopping( monitor=’val_accuracy’, patience=5, mode=’max’, restore_best_weights=False )
练习 5
该行 out, new_s = infer_model.predict([x, s]) 是错误的。在推断模型中,状态没有递归更新。这将导致一个工作模型,但性能较差。应该更正为 out, s = infer_model.predict([x, s])。
练习 6
result = beam_search(infer_model, x, state, 3, 5)