TensorFlow 实战(四)(5)

简介: TensorFlow 实战(四)

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 的熵由方程式量化


其中 xX 的一个结果。信不信由你,每次我们使用分类交叉熵损失时,我们都在不知不觉中使用了这个方程式。分类交叉熵的关键就在于这个方程式。回到困惑度度量,困惑度就是简单地

困惑度 = 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)
相关文章
|
7月前
|
机器学习/深度学习 TensorFlow API
TensorFlow与Keras实战:构建深度学习模型
本文探讨了TensorFlow和其高级API Keras在深度学习中的应用。TensorFlow是Google开发的高性能开源框架,支持分布式计算,而Keras以其用户友好和模块化设计简化了神经网络构建。通过一个手写数字识别的实战案例,展示了如何使用Keras加载MNIST数据集、构建CNN模型、训练及评估模型,并进行预测。案例详述了数据预处理、模型构建、训练过程和预测新图像的步骤,为读者提供TensorFlow和Keras的基础实践指导。
499 59
|
2月前
|
机器学习/深度学习 TensorFlow API
机器学习实战:TensorFlow在图像识别中的应用探索
【10月更文挑战第28天】随着深度学习技术的发展,图像识别取得了显著进步。TensorFlow作为Google开源的机器学习框架,凭借其强大的功能和灵活的API,在图像识别任务中广泛应用。本文通过实战案例,探讨TensorFlow在图像识别中的优势与挑战,展示如何使用TensorFlow构建和训练卷积神经网络(CNN),并评估模型的性能。尽管面临学习曲线和资源消耗等挑战,TensorFlow仍展现出广阔的应用前景。
81 5
|
2月前
|
机器学习/深度学习 人工智能 TensorFlow
基于TensorFlow的深度学习模型训练与优化实战
基于TensorFlow的深度学习模型训练与优化实战
109 0
|
5月前
|
机器学习/深度学习 存储 前端开发
实战揭秘:如何借助TensorFlow.js的强大力量,轻松将高效能的机器学习模型无缝集成到Web浏览器中,从而打造智能化的前端应用并优化用户体验
【8月更文挑战第31天】将机器学习模型集成到Web应用中,可让用户在浏览器内体验智能化功能。TensorFlow.js作为在客户端浏览器中运行的库,提供了强大支持。本文通过问答形式详细介绍如何使用TensorFlow.js将机器学习模型带入Web浏览器,并通过具体示例代码展示最佳实践。首先,需在HTML文件中引入TensorFlow.js库;接着,可通过加载预训练模型如MobileNet实现图像分类;然后,编写代码处理图像识别并显示结果;此外,还介绍了如何训练自定义模型及优化模型性能的方法,包括模型量化、剪枝和压缩等。
80 1
|
5月前
|
机器学习/深度学习 数据采集 TensorFlow
使用TensorFlow进行模型训练:一次实战探索
【8月更文挑战第22天】本文通过实战案例详解使用TensorFlow进行模型训练的过程。首先确保已安装TensorFlow,接着预处理数据,包括加载、增强及归一化。然后利用`tf.keras`构建卷积神经网络模型,并配置训练参数。最后通过回调机制训练模型,并对模型性能进行评估。此流程为机器学习项目提供了一个实用指南。
|
4月前
|
机器学习/深度学习 数据挖掘 TensorFlow
解锁Python数据分析新技能,TensorFlow&PyTorch双引擎驱动深度学习实战盛宴
在数据驱动时代,Python凭借简洁的语法和强大的库支持,成为数据分析与机器学习的首选语言。Pandas和NumPy是Python数据分析的基础,前者提供高效的数据处理工具,后者则支持科学计算。TensorFlow与PyTorch作为深度学习领域的两大框架,助力数据科学家构建复杂神经网络,挖掘数据深层价值。通过Python打下的坚实基础,结合TensorFlow和PyTorch的强大功能,我们能在数据科学领域探索无限可能,解决复杂问题并推动科研进步。
77 0
|
5月前
|
API UED 开发者
如何在Uno Platform中轻松实现流畅动画效果——从基础到优化,全方位打造用户友好的动态交互体验!
【8月更文挑战第31天】在开发跨平台应用时,确保用户界面流畅且具吸引力至关重要。Uno Platform 作为多端统一的开发框架,不仅支持跨系统应用开发,还能通过优化实现流畅动画,增强用户体验。本文探讨了Uno Platform中实现流畅动画的多个方面,包括动画基础、性能优化、实践技巧及问题排查,帮助开发者掌握具体优化策略,提升应用质量与用户满意度。通过合理利用故事板、减少布局复杂性、使用硬件加速等技术,结合异步方法与预设缓存技巧,开发者能够创建美观且流畅的动画效果。
97 0
|
5月前
|
安全 Apache 数据安全/隐私保护
你的Wicket应用安全吗?揭秘在Apache Wicket中实现坚不可摧的安全认证策略
【8月更文挑战第31天】在当前的网络环境中,安全性是任何应用程序的关键考量。Apache Wicket 是一个强大的 Java Web 框架,提供了丰富的工具和组件,帮助开发者构建安全的 Web 应用程序。本文介绍了如何在 Wicket 中实现安全认证,
54 0
|
5月前
|
机器学习/深度学习 数据采集 TensorFlow
从零到精通:TensorFlow与卷积神经网络(CNN)助你成为图像识别高手的终极指南——深入浅出教你搭建首个猫狗分类器,附带实战代码与训练技巧揭秘
【8月更文挑战第31天】本文通过杂文形式介绍了如何利用 TensorFlow 和卷积神经网络(CNN)构建图像识别系统,详细演示了从数据准备、模型构建到训练与评估的全过程。通过具体示例代码,展示了使用 Keras API 训练猫狗分类器的步骤,旨在帮助读者掌握图像识别的核心技术。此外,还探讨了图像识别在物体检测、语义分割等领域的广泛应用前景。
49 0
|
6月前
|
机器学习/深度学习 数据挖掘 TensorFlow
解锁Python数据分析新技能,TensorFlow&PyTorch双引擎驱动深度学习实战盛宴
【7月更文挑战第31天】在数据驱动时代,Python凭借其简洁性与强大的库支持,成为数据分析与机器学习的首选语言。**数据分析基础**从Pandas和NumPy开始,Pandas简化了数据处理和清洗,NumPy支持高效的数学运算。例如,加载并清洗CSV数据、计算总销售额等。
67 2