TensorFlow 实战(五)(2)https://developer.aliyun.com/article/1522831
11.2.5 编译模型
准备好模型进行培训的最后一件事是编译模型。我们将使用稀疏分类交叉熵损失,Adam 优化器和准确度作为指标:
from tensorflow.keras.metrics import SparseCategoricalAccuracy final_model.compile( loss='sparse_categorical_crossentropy', optimizer='adam', metrics=['accuracy'] )
最后,让我们打印模型摘要
final_model.summary()
将输出
Model: "final_seq2seq" ___________________________________________________________________________ Layer (type) Output Shape Param # ➥ Connected to =========================================================================== d_input (InputLayer) [(None, 1)] 0 ___________________________________________________________________________ d_vectorizer (Functional) (None, 20) 0 ➥ d_input[0][0] ___________________________________________________________________________ e_input_final (InputLayer) [(None, 1)] 0 ___________________________________________________________________________ d_embedding (Embedding) (None, 20, 128) 319872 ➥ d_vectorizer[0][0] ___________________________________________________________________________ encoder (Functional) (None, 256) 484864 ➥ e_input_final[0][0] ___________________________________________________________________________ d_gru (GRU) (None, 20, 256) 296448 ➥ d_embedding[0][0] ➥ encoder[0][0] ___________________________________________________________________________ d_dense_1 (Dense) (None, 20, 512) 131584 ➥ d_gru[0][0] ___________________________________________________________________________ d_dense_final (Dense) (None, 20, 2499) 1281987 ➥ d_dense_1[0][0] =========================================================================== Total params: 2,514,755 Trainable params: 2,514,755 Non-trainable params: 0 ___________________________________________________________________________
在下一节中,我们将学习如何使用准备好的数据训练我们刚刚定义的模型。
习题 2
与教师强制相反,另一种定义编码器-解码器模型的技术是定义一个模型,其中
- 编码器接收英语标记序列
- 解码器在时间轴上重复上下文向量作为输入,以便将相同的上下文向量馈送到解码器的每个时间步
您已提供以下编码器。定义一个具有 GRU 层和两个全连接隐藏层的解码器,以及以 en_inp 开头并生成最终预测的最终模型:
en_inp = tf.keras.Input(shape=(1,), dtype=tf.string, name='e_input') en_vectorized_out = en_vectorizer(inp) en_emb_layer = tf.keras.layers.Embedding( en_vocab+2, 128, mask_zero=True, name='e_embedding' ) en_emb_out = emb_layer(vectorized_out) en_gru_layer = tf.keras.layers.GRU(256, name='e_gru') en_gru_out = gru_layer(emb_out)
您可以使用 tf.keras.layers.RepeatVector 层重复上下文向量任意次数。例如,如果您将一个 [None, 32] 大小的张量传递给 tf.keras.layers.RepeatVector(5) 层,则通过在时间维度上五次重复 [None, 32] 张量,返回一个 [None, 5, 32] 大小的张量。
11.3 训练和评估模型
您已经定义了一个可以接受原始文本并生成翻译的端到端模型。接下来,您将在之前准备的数据上训练此模型。您将使用训练集来训练模型,并使用验证集来监视其训练性能。最后,模型将在测试数据上进行测试。为了评估模型,我们将使用两个指标:准确率和 BLEU。BLEU 是序列到序列问题中常用的指标,用于衡量输出序列(例如,翻译)的质量。
我们已经定义并编译了模型。现在,我们将在训练数据上训练模型,并在验证和测试数据上评估其性能,涵盖几个指标。我们将使用称为 BLEU 的性能指标来衡量模型的性能。它不是 Keras 提供的标准指标,将使用标准的 Python/NumPy 函数实现。因此,我们将编写自定义的训练/评估循环来分别训练和评估模型。
为了方便模型的训练和评估,我们将创建几个辅助函数。首先,我们将创建一个函数,从我们在开头定义的 Python DataFrame 对象中创建输入和目标(请参阅下一个列表)。
列表 11.6 准备用于模型训练和评估的训练/验证/测试数据
def prepare_data(train_df, valid_df, test_df): """ Create a data dictionary from the dataframes containing data """ data_dict = {} ❶ for label, df in zip( ['train', 'valid', 'test'], [train_df, valid_df, test_df] ): ❷ en_inputs = np.array(df["EN"].tolist()) ❸ de_inputs = np.array( df["DE"].str.rsplit(n=1, expand=True).iloc[:,0].tolist() ❹ ) de_labels = np.array( df["DE"].str.split(n=1, expand=True).iloc[:,1].tolist() ❺ ) data_dict[label] = { 'encoder_inputs': en_inputs, ❻ 'decoder_inputs': de_inputs, 'decoder_labels': de_labels } return data_dict
❶ 定义一个包含训练/验证/测试数据的字典。
❷ 遍历 train、valid 和 test 数据框。
❸ 将编码器输入定义为英文文本。
❹ 将解码器输入定义为除最后一个令牌外的所有德语文本。
❺ 将解码器输出定义为除第一个令牌外的所有德语文本。
❻ 更新字典,包括编码器输入、解码器输入和解码器输出。
此函数接受三个数据框 train_df、valid_df 和 test_df,并对它们进行一些转换,以返回一个包含三个键的字典:train、valid 和 test。在每个键下,您将找到以下内容:
- 编码器输入(即,英文单词序列)
- 解码器输入(即,德语单词序列)
- 解码器输出(即,德语单词序列)
正如我们之前所述,我们正在使用一种称为 teacher forcing 的技术来提升模型的性能。因此,解码器的目标变成了在给定前一个单词的情况下预测下一个单词。例如,对于例句(“I want a piece of chocolate cake”,“Ich möchte ein Stück Schokoladenkuchen”),编码器输入、解码器输入和解码器输出变为以下内容:
- [“I”, “want”, “a”, “piece”, “of”, “chocolate”, “cake”](编码器输入)
- [“Ich”, “möchte”, “ein”, “Stück”](解码器输入)
- [“möchte”, “ein”, “Stück”, “Schokoladenkuchen”](解码器输出)
可以看到,在每个时间步长,解码器都在根据前一个单词预测下一个单词。prepare_data(…) 函数会执行此操作,下一个列表将显示。然后,我们将编写一个函数来洗牌数据。此函数将用于在训练期间的每个时代开始时对数据进行洗牌。
列表 11.7 对训练数据进行洗牌
def shuffle_data(en_inputs, de_inputs, de_labels, shuffle_indices=None): """ Shuffle the data randomly (but all of inputs and labels at ones)""" if shuffle_indices is None: shuffle_indices = np.random.permutation(np.arange(en_inputs.shape[0])) ❶ else: shuffle_indices = np.random.permutation(shuffle_indices) ❷ return ( en_inputs[shuffle_indices], de_inputs[shuffle_indices], de_labels[shuffle_indices] ❸ ), shuffle_indices
❶ 如果未传递 shuffle_indices,则自动生成洗牌索引。
❷ 对提供的 shuffle_indices 进行洗牌。
❸ 返回洗牌数据。
shuffle_data() 函数接受由 prepare_data 函数输出的数据(即编码器输入、解码器输入和解码器输出)。可选地,它接受数据索引的洗牌表示。我们允许将洗牌索引传递给函数,这样,通过对已经洗牌的索引进行洗牌,您可以得到数据顺序的新排列。这在训练期间在每个时代生成不同的洗牌配置时非常有用。
如果未传递 shuffle_indices,shuffle_data() 函数将生成数据索引的随机排列。数据索引是由 np.arange(en_ inputs.shape[0]) 生成的,它从 0 到 en_inputs 中的示例数量创建了一个有序数字序列。可以通过调用 np.random.permutation() 函数对给定数组生成给定数组的随机排列。如果已将数组传递给 shuffle_indices 参数,则将在 shuffle_indices 中传递的数组进行洗牌,生成新的洗牌数据配置。最后,我们根据 shuffle_indices 数组确定的顺序返回编码器输入(en_inputs)、解码器输入(de_inputs)和解码器输出(de_labels)进行洗牌。
接下来,我们将编写一个函数来评估模型。在此函数中,我们使用定义的 batch_size 对给定数据上的给定模型进行评估。特别地,我们使用三个指标评估机器翻译模型:
- 交叉熵损失 ——在预测概率和真实目标之间计算的标准多类交叉熵损失。
- 准确度 ——标准准确度,根据模型在给定时间步长上是否预测与真实目标相同的单词进行测量。换句话说,预测必须与真实目标完全匹配,从单词到单词。
- BLEU 分数 ——比准确率更强大的度量标准,基于精确度,但考虑了许多不同值的 n 克隆。
双语评估干预(BLEU)
BLEU 是一种度量标准,用于通过衡量翻译与给定的真实文本(或翻译对应多个真实文本,因为相同的内容可以以不同的语言表达)的相似度来衡量生成文本序列(例如翻译)的质量。它是由 Papineni 等人在论文 “BLEU: A Method for Automatic Evaluation of Machine Translation” 中引入的(www.aclweb.org/anthology/P02-1040.pdf
)。BLEU 分数是一种精度度量标准的变体,用于计算候选文本(即预测)与多个参考翻译(即真实文本)之间的相似度。
要了解 BLEU 度量,让我们考虑以下候选项和参考文献:
候选项 1 (C1):the cat was on the red mat
候选项 2 (C2):the cat the cat the cat the
参考文献 1:the cat is on the floor
参考文献 2:there was a cat on the mat
对于候选项 1 和 2 的精度可以计算为
精度 = 匹配任何参考文献的单词数/候选项中的单词数
意味着
精度(C1) = 6/7,精度(C2) = 7/7
这与直觉相矛盾。显然,C1 是与参考文献更匹配的选择。但是精度却讲述了另一个故事。因此,BLEU 引入了一个修改后的精度。在修改后的精度中,对于候选项中的每个唯一单词,您计算该单词在任何一个参考文献中出现的次数,并取其中的最大值。然后您对候选文本中所有唯一单词的这个值求和。例如,对于 C1 和 C2,修改后的单字精度是
修改后的精度(C1) = (2 + 1 + 1 + 2 + 0 + 1) /7 = 5/7,修改后的精度(C2) = (2 + 1)/7 = 3/7
这好多了:C1 的精度比 C2 高,这正是我们想要的。BLEU 将修改后的单字精度扩展到修改后的 n-gram 精度,并为多个 n-gram(例如,单字、二字、三字等)计算修改后的精度。通过在许多不同的 n-gram 上计算修改后的精度,BLEU 可以偏爱具有与参考文献匹配的更长子序列的翻译或候选。
我们将定义一个名为 BLEUMetric 的对象,它将计算给定预测批次和目标的 BLEU 分数,如下列表所示。
列表 11.8 定义 BLEU 度量以评估机器翻译模型
class BLEUMetric(object): def __init__(self, vocabulary, name='perplexity', **kwargs): """ Computes the BLEU score (Metric for machine translation) """ super().__init__() self.vocab = vocabulary ❶ self.id_to_token_layer = StringLookup( vocabulary=self.vocab, invert=True, num_oov_indices=0 ) ❷ def calculate_bleu_from_predictions(self, real, pred): """ Calculate the BLEU score for targets and predictions """ pred_argmax = tf.argmax(pred, axis=-1) ❸ pred_tokens = self.id_to_token_layer(pred_argmax) ❹ real_tokens = self.id_to_token_layer(real) ❹ def clean_text(tokens): """ Clean padding and [SOS]/[EOS] tokens to only keep meaningful words """ t = tf.strings.strip( ❺ tf.strings.regex_replace( ❻ tf.strings.join( ❼ tf.transpose(tokens), separator=' ' ), "eos.*", ""), ) t = np.char.decode(t.numpy().astype(np.bytes_), encoding='utf-8')❽ t = [doc if len(doc)>0 else '[UNK]' for doc in t ] ❾ t = np.char.split(t).tolist() ❿ return t 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, smooth=False) ⓭ return bleu
❶ 从拟合的 TextVectorizer 中获取词汇表。
❷ 定义一个 StringLookup 层,它可以将标记 ID 转换为单词。
❸ 获取预测的标记 ID。
❹ 使用词汇表和 StringLookup 将标记 ID 转换为单词。
❺ 剥离字符串中的任何额外空格。
❻ 用空白替换 EOS 标记之后的所有内容。
❼ 将每个序列中的所有标记连接为一个字符串。
❽ 将字节流解码为字符串。
❾ 如果字符串为空,则添加一个 [UNK] 标记。否则,可能会导致数值错误。
❿ 将序列拆分为单独的标记。
⓫ 获取预测和真实序列的干净版本。
⓬ 我们必须将每个真实序列包装在列表中,以利用第三方函数来计算 BLEU。
⓭ 获取给定批次目标和预测的 BLEU 值。
首先,我们定义一个 init(…) 函数和该类的几个属性,例如 vocab,它将由 TextVectorization 层返回的解码器词汇表。接下来,我们定义一个 TensorFlow StringLookup 层,它可以返回给定令牌 ID 的字符串令牌,反之亦然。StringLookup 函数所需的仅仅是解码器 TextVectorization 层的词汇表。默认情况下,StringLookup 层将给定的字符串令牌转换为令牌 ID。设置 invert=true 意味着此层将把给定的令牌 ID 转换为字符串令牌。我们还需要说明我们不希望此层自动为词汇表外的单词添加表示。为此,我们将 num_oov_indices=0。
接下来,我们定义一个名为 calculate_bleu_from_predictions(…) 的函数,它接受一个真实目标的批次和模型给出的预测概率的批次,以计算该批次的 BLEU 分数。首先,它通过获取每个时间步的概率向量的最大索引来计算预测的令牌 ID:
pred_argmax = tf.argmax(pred, axis=-1)
接下来,使用之前定义的 StringLookup 层生成字符串令牌:
pred_tokens = self.id_to_token_layer(pred_argmax) real_tokens = self.id_to_token_layer(real)
具体来说,我们将令牌 ID 矩阵(预测和目标)传递给 StringLookup 层。例如,如果
real = [ [4,1,0], [8,2,21] ] vocabulary = ['', '[UNK]', 'sos', 'eos', 'tom', 'ich', 'nicht', 'ist', 'du', 'sie']
然后
real_tokens = tf.Tensor([ [b'tom' b'[UNK]' b''] [b'du' b'sos' b'[UNK]'] ], shape=(2, 3), dtype=string)
之后,我们定义一个函数来执行一些清理工作。定义的函数将截断预测,使得所有 EOS 令牌之后的内容都被移除(包括),并将句子标记为单词列表。该函数的输入是一个张量,其中每一行都是一个令牌列表(即,pred_tokens)。让我们利用这个机会来磨练我们对 TensorFlow 字符串操作的理解。TensorFlow 有一个名为 tf.strings 的命名空间(mng.bz/gw7E
),提供各种基本的字符串操作功能:
def clean_text(tokens): """ Clean padding and [SOS]/[EOS] tokens to only keep meaningful words """ # 3\. Strip the string of any extra white spaces 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.*", "" ), ) # Decode the byte stream to a string translations = np.char.decode( translations_in_bytes.numpy().astype(np.bytes_), encoding='utf-8' ) # If the string is empty, add a [UNK] token # Otherwise get a Division by zero error translations = [sent if len(sent)>0 else '[UNK]' for sent in translations ] # Split the sequences to individual tokens translations = np.char.split(translations).tolist() return translations
TensorFlow 实战(五)(4)https://developer.aliyun.com/article/1522833