TensorFlow 实战(五)(3)https://developer.aliyun.com/article/1522832
让我们看看调用几个 tf.string 操作的第一行代码:
translations_in_bytes = tf.strings.strip( # 2\. Replace everything after the eos token with blank tf.strings.regex_replace( # 1\. Join all the tokens to one string in each sequence tf.strings.join(tf.transpose(tokens), separator=' '), "eos.*", "" ), )
它在输入字符串张量上执行一系列转换。首先,对令牌调用 tf.strings.join() 将使用给定的分隔符将所有令牌连接成一列。例如
[ ['a','b','c'], ['d', 'e', 'f'] ]
变为
['ad', 'be', 'cf']
由于我们的句子跨越行,因此我们首先需要转置令牌,使得句子位于列中。接下来,在张量上调用 tf.strings.regex_replace(),其中每个项都是由连接产生的句子。它将移除跟随 EOS 令牌的所有内容。此字符串模式由 eos.* 正则表达式捕获。最后,我们从生成的字符串中去除任何起始和结束空格。
TensorFlow 将字符串保留为字节格式。为了将字符串转换为 UTF-8 编码,我们有一系列的转换步骤,以将其转换为正确的格式:
- 首先,我们必须将数组转换为 NumPy 数组。此数组中的元素将以对象格式存在。
- 接下来,我们通过调用 translations_in_bytes.numpy().astype(np.bytes_)将其转换为字节数组。
- 最后,我们解码字节数组并将其转换为所需的编码(在我们的情况下为 UTF-8)。
其余代码很容易理解,并在代码中被注释说明。
最后,我们对预测和真实令牌张量调用 clean_text()函数,并将最终结果提供给第三方实现的 BLEU 指标:
pred_tokens = clean_text(pred_tokens) real_tokens = [[r] for r in clean_text(real_tokens)] bleu, precisions, bp, ratio, translation_length, reference_length = ➥ compute_bleu(real_tokens, pred_tokens)
clean_text()函数将预测的翻译和真实翻译(有时称为参考)转换为一个令牌列表的列表。在这里,外部列表表示单个示例,而内部列表表示给定示例中的令牌。作为最后一步,我们将每个参考包装在另一个列表结构中,以便 real_tokens 成为一个令牌列表的列表的列表。这是必要的,因为我们将使用第三方实现的 BLEU 指标。此处使用的 compute_bleu()函数是在 TensorFlow 存储库中找到的第三方实现(mng.bz/e7Ev
)。compute_bleu()函数期望两个主要参数:
- 翻译—一个令牌列表的列表。
- 参考—一个令牌列表的列表的列表。换句话说,每个翻译可以有多个参考文本,每个参考文本都是一个令牌列表。
然后返回
- bleu—给定批次的候选参考对的 BLEU 分数。
- 精度—构建最终 BLEU 分数的个别 n-gram 精度。
- bp—短候选项的 BLEU 分数的特殊部分(惩罚短候选项)。
- 比率—候选项长度除以参考文本长度。
- translation_length—批次中候选项的长度总和。
- reference_length—批次中参考文本的长度总和。在候选项有多个参考文本的情况下,选择最小值。
让我们测试 compute_bleu()函数的运行情况。假设有一个翻译和一个参考文本。在第一个情景中,翻译开头出现了[UNK]标记,其余与参考完全匹配。在第二个情景中,我们再次有两个[UNK]标记,但它们出现在开头和中间。让我们看看结果:
translation = [['[UNK]', '[UNK]', 'mÃssen', 'wir', 'in', 'erfahrung', ➥ 'bringen', 'wo', 'sie', 'wohnen']] reference = [[['als', 'mÃssen', 'mÃssen', 'wir', 'in', 'erfahrung', ➥ 'bringen', 'wo', 'sie', 'wohnen']]] bleu1, _, _, _, _, _ = compute_bleu(reference, translation) translation = [['[UNK]', 'einmal', 'mÃssen', '[UNK]', 'in', 'erfahrung', ➥ 'bringen', 'wo', 'sie', 'wohnen']] reference = [[['als', 'mÃssen', 'mÃssen', 'wir', 'in', 'erfahrung', ➥ 'bringen', 'wo', 'sie', 'wohnen']]] bleu2, _, _, _, _, _ = compute_bleu(reference, translation) print("BLEU score with longer correctly predict phrases: {}".format(bleu1)) print("BLEU score without longer correctly predict phrases: ➥ {}".format(bleu2))
这将打印
BLEU score with longer correctly predict phrases: 0.7598356856515925 BLEU score without longer correctly predict phrases: 0.537284965911771
如果你计算翻译与参考之间的逐词准确率,你会得到相同的结果,因为只有两个[UNK]标记不匹配。然而,这两个情况的 BLEU 分数是不同的。这清楚地表明,BLEU 更偏好能够连续正确翻译更多单词的翻译,而不是出现断裂。
我们已经准备好为模型编写训练和评估循环所需的所有功能。让我们首先编写评估循环(请参阅下一个列表),因为它将在训练循环中用于评估模型。
列表 11.9 评估编码器-解码器模型
def evaluate_model( model, vectorizer, en_inputs_raw, de_inputs_raw, de_labels_raw, batch_size ): """ Evaluate the model on various metrics such as loss, accuracy and BLEU """ bleu_metric = BLEUMetric(de_vocabulary) ❶ loss_log, accuracy_log, bleu_log = [], [], [] n_batches = en_inputs_raw.shape[0]//batch_size ❷ print(" ", end='\r') for i in range(n_batches): ❸ print("Evaluating batch {}/{}".format(i+1, n_batches), end='\r') ❹ x = [ en_inputs_raw[i*batch_size:(i+1)*batch_size], ❺ de_inputs_raw[i*batch_size:(i+1)*batch_size] ] y = de_vectorizer(de_labels_raw[i*batch_size:(i+1)*batch_size]) ❺ loss, accuracy = model.evaluate(x, y, verbose=0) ❻ pred_y = model.predict(x) ❼ bleu = bleu_metric.calculate_bleu_from_predictions(y, pred_y) ❽ loss_log.append(loss) ❾ accuracy_log.append(accuracy) ❾ bleu_log.append(bleu) ❾ return np.mean(loss_log), np.mean(accuracy_log), np.mean(bleu_log)
❶ 定义度量标准。
❷ 获取批次数量。
❸ 一次评估一个批次。
❹ 状态更新
❺ 获取输入和目标。
❻ 获取评估指标。
❼ 获取预测以计算 BLEU。
❽ 更新包含损失、准确度和 BLEU 指标的日志。
❾ 计算 BLEU 指标。
evaluate_model() 函数接受几个重要的参数:
- model—我们定义的编码器-解码器模型。
- en_inputs_raw—编码器输入(文本)。这将是一个字符串数组,其中每个字符串都是一个英文句子/短语。
- de_inputs_raw—解码器输入(文本)。这将是一个字符串数组。它将包含每个德语翻译中除最后一个单词之外的所有单词。
- de_labels_raw—解码器标签(文本)。这将是一个字符串数组。它将包含每个德语翻译中除第一个单词之外的所有单词。
- de_vectorizer—解码器向量化器,用于将 decoder_labels_raw(文本)转换为标记 ID。
函数定义了一个我们之前定义的 BLEUMetric 对象。它定义了用于累积给定数据集中每个批次的损失、准确度和 BLEU 分数的占位符。然后它遍历每个数据批次,并执行以下操作:
- 创建批处理输入,作为 en_inputs_raw 和 de_inputs_raw 中对应批次的数据
- 使用 de_labels_raw 创建目标作为标记 ID
- 使用批量输入和目标评估模型,以获取批量的损失和准确度分数
- 使用真实目标和预测计算 BLEU 分数
- 在先前定义的占位符中累积指标
最后,在模型遍历所有数据批次之后,它将返回均值损失、准确度和 BLEU 分数作为数据集的最终评估基准。
有了这个,我们开始定义训练循环(见下一个清单)。我们将定义一个名为 train_model() 的函数,它将执行以下四个核心任务:
- 用训练数据训练模型。
- 用训练数据评估模型。
- 用验证数据评估模型。
- 用测试数据评估模型。
图 11.10 使用自定义训练/评估循环训练模型
def train_model(model, vectorizer, train_df, valid_df, test_df, epochs, batch_size): """ Training the model and evaluating on validation/test sets """ bleu_metric = BLEUMetric(de_vocabulary) ❶ data_dict = prepare_data(train_df, valid_df, test_df) ❷ shuffle_inds = None for epoch in range(epochs): bleu_log = [] ❸ accuracy_log = [] ❸ loss_log = [] ❸ (en_inputs_raw,de_inputs_raw,de_labels_raw), shuffle_inds = ➥ shuffle_data( ❹ data_dict['train']['encoder_inputs'], data_dict['train']['decoder_inputs'], data_dict['train']['decoder_labels'], shuffle_inds ) n_train_batches = en_inputs_raw.shape[0]//batch_size ❺ for i in range(n_train_batches): ❻ print("Training batch {}/{}".format(i+1, n_train_batches), ➥ end='\r') ❼ x = [ ❽ en_inputs_raw[i*batch_size:(i+1)*batch_size], de_inputs_raw[i*batch_size:(i+1)*batch_size] ] y = vectorizer(de_labels_raw[i*batch_size:(i+1)*batch_size]) ❾ model.train_on_batch(x, y) ❿ loss, accuracy = model.evaluate(x, y, verbose=0) ⓫ pred_y = model.predict(x) ⓬ bleu = bleu_metric.calculate_bleu_from_predictions(y, pred_y) ⓭ loss_log.append(loss) ⓮ accuracy_log.append(accuracy) ⓮ bleu_log.append(bleu) ⓮ val_en_inputs = data_dict['valid']['encoder_inputs'] ⓯ val_de_inputs = data_dict['valid']['decoder_inputs'] ⓯ val_de_labels = data_dict['valid']['decoder_labels'] ⓯ val_loss, val_accuracy, val_bleu = evaluate_model( ⓰ model, vectorizer, val_en_inputs, val_de_inputs, val_de_labels, epochs, batch_size ) print("\nEpoch {}/{}".format(epoch+1, epochs)) ⓱ print( "\t(train) loss: {} - accuracy: {} - bleu: {}".format( np.mean(loss_log), np.mean(accuracy_log), np.mean(bleu_log) ) ) print( "\t(valid) loss: {} - accuracy: {} - bleu: {}".format( val_loss, val_accuracy, val_bleu ) ) test_en_inputs = data_dict['test']['encoder_inputs'] test_de_inputs = data_dict['test']['decoder_inputs'] test_de_labels = data_dict['test']['decoder_labels'] test_loss, test_accuracy, test_bleu = evaluate_model( model, vectorizer, test_en_inputs, test_de_inputs, test_de_labels, epochs, batch_size ) print("\n(test) loss: {} - accuracy: {} - bleu: {}".format( test_loss, test_accuracy, test_bleu) )
❶ 定义指标。
❷ 定义数据。
❸ 在每个时期开始时重置指标日志。
❹ 在每个时期开始时洗牌数据。
❺ 获取训练批次的数量。
❻ 一次训练一个批次。
❼ 状态更新
❽ 获取一批输入(英语和德语序列)。
❾ 获取一批目标(德语序列偏移 1)。
❿ 训练一个步骤。
⓫ 评估模型以获取指标。
⓬ 获取最终预测以计算 BLEU。
⓭ 计算 BLEU 指标。
⓮ 更新时期的日志记录的指标。
⓯ 定义验证数据。
⓰ 在验证数据上评估模型。
⓱ 打印每个时期的评估指标。
让我们分析列表 11.10 中的函数,以了解其行为。如果你退一步思考,它所做的只是调用我们之前定义的函数并显示结果。首先,它通过将数据准备(使用 prepare_data()函数)呈现为具有 train、valid 和 test 键的字典来准备数据。接下来,它经历了几个周期的训练。在每个周期中,它会对训练数据进行打乱,并逐批处理数据。对于每个训练批次,模型都会在该批次的数据上进行训练,并对相同批次进行评估。训练数据的评估日志用于计算训练性能,就像我们在 evaluate_model()函数中看到的那样。然后,在训练循环完成后,模型会在验证数据上进行评估。最后,在训练模型结束时,模型会在测试数据上进行评估。你可以像下面这样调用 train_model()函数:
epochs = 5 batch_size = 128 train_model(final_model, de_vectorizer, train_df, valid_df, test_df, ➥ epochs, batch_size)
这将输出类似于以下的结果:
Evaluating batch 39/39 Epoch 1/5 (train) loss: 1.7741597780050375 - accuracy: 0.2443966139585544 - ➥ bleu: 0.0014343267864378607 (valid) loss: 1.4453194752717629 - accuracy: 0.3318057709779495 - ➥ bleu: 0.010740537197906803 Evaluating batch 39/39 ... Epoch 5/5 (train) loss: 0.814081399104534 - accuracy: 0.5280381464041196 - ➥ bleu: 0.1409178724874819 (valid) loss: 0.8876287539800009 - accuracy: 0.514901713683055 - ➥ bleu: 0.1285171513954398 Evaluating batch 39/39 (test) loss: 0.9077589313189188 - accuracy: 0.5076315150811122 - bleu: ➥ 0.12664703414801345
注意 在一台配有 NVIDIA GeForce RTX 2070 8 GB 的 Intel Core i5 机器上,训练大约需要 4 分钟 10 秒来运行五个时期。
我们可以做出的第一个观察是,训练的方向是正确的。随着时间的推移,训练和验证指标都有所提高。模型从 24%的训练准确率和 0.001 的 BLEU 分数开始,最终达到了 52%的准确率和 0.14 的 BLEU 分数。在验证过程中,模型将准确率从 33%提高到了 51%,而 BLEU 分数则从 0.01 提高到了 0.12。与之前一样,让我们保存模型,以便以后可以在现实世界中使用。我们将保存模型以及词汇表:
## Save the model os.makedirs('models', exist_ok=True) tf.keras.models.save_model(final_model, os.path.join('models', 'seq2seq')) import json os.makedirs(os.path.join('models', 'seq2seq_vocab'), exist_ok=True) # Save the vocabulary files with open(os.path.join('models', 'seq2seq_vocab', 'en_vocab.json'), 'w') as f: json.dump(en_vocabulary, f) with open(os.path.join('models', 'seq2seq_vocab', 'de_vocab.json'), 'w') as f: json.dump(de_vocabulary, f)
在训练模型时,我们使用目标(即目标语言标记)作为解码器的输入。但是在翻译时,目标是未知的,因此无法这样做。因此,在接下来的部分中,我们在保持模型参数不变的同时修改我们训练的模型。
练习 3
你有以下用于训练模型的函数。这里,en_inputs_raw 代表编码器输入,de_inputs_raw 代表解码器输入,de_labels_raw 代表解码器标签:
for epoch in range(epochs): bleu_log = [] n_train_batches = en_inputs_raw.shape[0]//batch_size for i in range(n_train_batches): print("Training batch {}/{}".format(i+1, n_train_batches), end='\r') x = [ en_inputs_raw[i*batch_size:(i+1)*batch_size], de_inputs_raw[i*batch_size:(i+1)*batch_size] ] y = vectorizer(de_labels_raw[i*batch_size:(i+1)*batch_size]) model.train_on_batch(x, y) pred_y = model.predict(x) bleu_log.append(bleu_metric.calculate_bleu_from_predictions(y, pred_y)) mean_bleu = np.mean(bleu_log)
如果给定时期的平均训练 BLEU 分数小于上一个时期,你想改变代码以停止训练。你会如何改变代码?
11.4 从训练到推理:定义推理模型
您已经训练了一个序列到序列的机器翻译模型,并计划使用它为一些看不见的英语短语生成德语翻译。它已经使用教师强制进行了训练,这意味着翻译中的单词已经被输入。您意识到这在推理期间是不可能的,因为任务本身就是生成翻译。因此,您打算使用原始模型的训练权重创建一个新的编码器-解码器模型。在此模型中,解码器递归地操作,其中它将其先前的预测作为下一个时间步的输入。解码器从 SOS 标记开始,并以此方式继续,直到输出 EOS 标记。
训练机器翻译器的最终目标是在现实世界中使用它将看不见的源语言句子(例如,英语)翻译成目标语言(例如,德语)。然而,与我们训练的大多数其他模型不同,我们不能直接从中使用它进行推理。需要额外的工作来填补使用训练模型进行推理之间的差距。在提出解决方案之前,让我们先了解潜在的问题。
在模型训练过程中,我们使用教师强制来提高模型的性能。在教师强制中,解码器被提供目标语言输入(例如,德语),并被要求在每个时间步预测序列中的下一个单词。这意味着训练模型依赖于两个输入:英语序列和德语序列。然而,在推理过程中,我们无法访问德语序列。我们的任务是为给定的英语序列生成这些德语序列。因此,我们需要重新利用我们训练过的模型,以便能够生成德语翻译,而不依赖于整个德语序列是否可用。
解决方法是保持编码器不变,并对解码器进行几处修改。我们将使我们的解码器成为一个递归解码器。这意味着解码器将使用其先前预测的单词作为下一个时间步的输入,直到达到序列的末尾(见图 11.4)。具体来说,我们执行以下操作。对于给定的英语序列
- 通过将英语序列输入编码器来获取上下文向量。
- 我们首先将 SOS 标记(x^d[0])(在代码中表示为 start_token)与上下文向量(sid[1])一起输入,并获取解码器的预测(*ŷ*d[1])和输出状态(so^d[1])。
- 直到解码器的预测(ŷ^d[t] [+1])是 EOS(在代码中表示为 end_token)为止
- 在下一个时间步(t + 1)中,将解码器的预测(ŷd[t])和输出状态(sod[t])在时间 t 的输入(x^d[t] [+1])和初始状态(si^d[t] [+1])一起作为输入。
- 在下一个时间步中获取解码器的预测(ŷ^d[t] [+1])和输出状态(so^d[t] [+1])。
图 11.4 使用序列到序列模型进行推理(即,从英语输入生成翻译)
为了实现这个目标,我们必须对已训练的模型进行两个重大的改变:
- 将编码器和解码器分开作为单独的模型。
- 更改解码器,以便将输入令牌和初始状态作为输入,并输出预测的令牌和下一个状态作为输出。
TensorFlow 实战(五)(5)https://developer.aliyun.com/article/1522834