【从0开始学习Transformer】下:Transformer训练与评估
1. 前⾔
在中我们已经描述了 Transformer 的整个模型搭建过程,并逐层逐⾏地解释了其正向传播的原理和细节。接下来,我们将着⼿定义优化训练的⽅式,处理语料,并最终使⽤搭建好的 Transformer 实现⼀个由葡萄⽛语翻译⾄英语的翻译器。
为了训练⼀个由葡萄⽛语翻译⾄英语的翻译器,⾸先来观察如何处理数据从⽽能够正确地输⼊我们已经设计好的 Tranformer 模型:
class Transformer(tf.keras.Model):
...
def call(self, inp, tar, training, enc_padding_mask,韩国电影2018最新r级
look_ahead_mask, dec_padding_mask):
...
只摘取模型的调⽤ call 部分,可以看出 Transformer 需要的输⼊:
inp:输⼊序列,这⾥需要的是源语⾔(葡萄⽛语)的编码表⽰。(嵌⼊表⽰将在编码器中完成)
tar:⽬标序列,这⾥需要的是⽬标语⾔(英语)的编码表⽰。(嵌⼊表⽰将在编码器中完成)
training:布尔量,规定模型是否可以训练。
enc_padding_mask:编码器,填充遮挡。
look_ahead_mask:前瞻遮挡。两个遮挡将在后⾯详细描述。
bodyshots
dec_padding_mask:解码器,填充遮挡。
由此,我们知道,为了达成⽬的,我们需要完成以下⼏个步骤:
1. 创造原训练集(输⼊句⼦和⽬标句⼦)的嵌⼊表⽰
2. 为我们的 Transformer 设计优化器和损失函数
3. 根据情况创造填充遮挡
4. 为了实现⾃回归创建前瞻遮挡
5. 将数据输⼊进⾏训练
6. 最终对训练好的模型进⾏评估
2. 创造原训练集的编码表⽰
2.1. 数据下载与读取
参考 Tensorflow 的官⽅教程,我们同样使⽤ TFDS 来进⾏数据的下载和载⼊。(应⾸先在本机环境或虚拟环境中安装 tensorflow_datasets 模块。
import tensorflow_datasets as tfds
examples, metadata = tfds.load('ted_hrlr_translate/pt_to_en', with_info=True,
爱上离婚的女人djas_supervised=True)
train_examples, val_examples = examples['train'], examples['validation']
第⼀⾏代码会访问⽤户⽬录下(Windows和Unix系系统各有不同,请参考)是否已经下载好了葡萄⽛翻译⾄英⽂翻译器所需的数据集,如果不存在,则会⾃动下载。第⼆⾏,则将其⾃动转换为训练集合和测试集合两个 tf.data.Dataset 实例。
2.2. 创建⼦词分词器
tfds 独⽴于 Tensorflow,是专门⽤来管理和下载⼀些成熟的数据集的Python库。但其中有很多我认为通⽤性很强的函数。⽐如⼦词分词器:
tokenizer_en = SubwordTextEncoder.build_from_corpus(
(en.numpy() for pt, en in train_examples), target_vocab_size=2**13)
tokenizer_pt = SubwordTextEncoder.build_from_corpus(
(pt.numpy() for pt, en in train_examples), target_vocab_size=2**13)
上⽅代码分别创建了两个⼦词分词器,分别读取了训练集合中的全部英⽂和葡萄⽛⽂,并基于这些⼤段的⽂字形成了⼦词分词器。
⼦词分词器的作⽤是将输⼊句⼦中的每⼀个单词编码为⼀个独⼀⽆⼆的数字,如果出现了⼦词分词器不能识别的新单词,那么就将其打散成多个可以识别的⼦词来编码成数字。
同样的,分词器也可以将⽤数字表⽰的句⼦重新转换回原有的句⼦。
sample_string = 'Transformer is awesome.'
tokenized_string = de(sample_string)
print ('Tokenized string is {}'.format(tokenized_string))
# Tokenized string is [7915, 1248, 7946, 7194, 13, 2799, 7877]
original_string = tokenizer_en.decode(tokenized_string)
print ('The original string: {}'.format(original_string))
# The original string: Transformer is awesome.
assert original_string == sample_string
# 分词器转换回的句⼦和原始句⼦⼀定是相同的。
2.3. 数据处理
为了⽅便后期使⽤,编写⼀个将编码后句⼦加上开始标记和结束标记。利⽤ tf.data.Dataset 的 map 功能来批量完成这⼀任务。⾸先需要定义⼀个函数:
def encode(lang1, lang2):
lang1 = [tokenizer_pt.vocab_size] + de(
lang1.numpy()) + [tokenizer_pt.vocab_size+1]
lang2 = [tokenizer_en.vocab_size] + de(
lang2.numpy()) + [tokenizer_en.vocab_size+1]
return lang1, lang2
显然这⾥我们使⽤了原⽣ Python 编写这个函数,这样的函数是不能为 map 所⽤的。我们需要使⽤ tf.py_function 将这个函数转换为计算图。(此函数可以将原⽣ Python 编写的计算过程转换为 Tensorflow 流程控制的计算图,详情请参考 )
def tf_encode(pt, en):
return tf.py_function(encode, [pt, en], [tf.int64, tf.int64])
# 第⼀个参数是包装的函数,第⼆个参数是输⼊的参数列表,第三个参数是输出的数据类型
于是,我们可以给训练数据集和验证数据集中每个句⼦加上开始标记和结束标记:
train_dataset = train_examples.map(tf_encode)
val_dataset = val_examples.map(tf_encode)
为了能够让这个模型较⼩,我们只使⽤句⼦短于 40 个单词的句⼦来作为输⼊数据。这⾥利⽤ tf.data.Dataset 的 filter 过滤器功能来快速筛选出需要的数据。
为了使⽤ filter,⾸先要定义⼀个过滤器函数。这是⼀个布尔函数,如果⼀条数据符合要求,则返回真,否则返回假。显然,对于葡萄⽛句⼦翻译⾄英⽂句⼦数据集,我们要筛选出所有成对相同意思的句⼦,并且两条句⼦都短于 40 个单词(编码后并加上了开始和终结标记后的长度)。
def filter_max_length(x, y, max_length=MAX_LENGTH):
return tf.logical_and(tf.size(x) <= max_length,
tf.size(y) <= max_length)
类似的,对数据集进⾏筛选:
train_dataset = train_dataset.filter(filter_max_length)
我们已经知道, 输⼊给 Transformer 的句⼦通常不会单句地输⼊,⽽是把句⼦叠成⼀批输⼊。将⼀批有长有短的句⼦叠成⼀批,需要将较短的句⼦补 0 使其长度匹配当前⼀批中最长的句⼦。
# 将数据集缓存到内存中以加快读取速度。
train_dataset = train_dataset.cache()
# shuffle 函数定义⼀个随机⽅式,⾸先定义⼀个缓存⼤⼩,取⼀部分数据放⼊缓存(BUFFER_SIZE⼤⼩),然后进⾏随机洗牌,最后从缓存中取。显然,若想实现全数据集的完美随机,需要让缓存的⼤⼩⼤于等于整个数据集。
# ⾸先将数据进⾏随机打散之后,对较短的数据进⾏填充。
王力宏专辑
train_dataset = train_dataset.shuffle(BUFFER_SIZE).padded_batch(
BATCH_SIZE, padded_shapes=([-1], [-1]))
train_dataset = train_dataset.prefetch(perimental.AUTOTUNE)
显然,验证集合也需要进⾏类似的处理操作(验证操作⽆需随机)。
val_dataset = val_dataset.filter(filter_max_length).padded_batch(
BATCH_SIZE, padded_shapes=([-1], [-1]))爱情不是游戏
取出⼀个数据看⼀看:
pt_batch, en_batch = next(iter(val_dataset))
"""
pt_batch:
(<tf.Tensor: id=207688, shape=(64, 40), dtype=int64, numpy=
array([[8214, 1259,    5, ...,    0,    0,    0],
[8214,  299,  13, ...,    0,    0,    0],
[8214,  59,    8, ...,    0,    0,    0],
...,
[8214,  95,    3, ...,    0,    0,    0],
[8214, 5157,    1, ...,    0,    0,    0],
[8214, 4479, 7990, ...,    0,    0,    0]])>,
en_batch:
<tf.Tensor: id=207689, shape=(64, 40), dtype=int64, numpy=
array([[8087,  18,  12, ...,    0,    0,    0],
[8087,  634,  30, ...,    0,    0,    0],
[8087,  16,  13, ...,    0,    0,    0],
...,
[8087,  12,  20, ...,    0,    0,    0],
[8087,  17, 4981, ...,    0,    0,    0],
[8087,  12, 5453, ...,    0,    0,    0]])>)
"""
3. 损失函数设计
损失函数的设计较为简单,需要考虑输出的句⼦和真正的⽬标句⼦是否为同⼀句⼦。只需要使⽤⼀个交叉熵函数。有⼀点需要注意,由上⼀章数据处理可以看出,数据中含有⼤量的填充(补0),这些填充不能作为真正的输⼊来考虑,因此在损失函数的计算中,需要将这些部分屏蔽掉。
loss_object = tf.keras.losses.SparseCategoricalCrossentropy(
from_logits=True, reduction='none')
def loss_function(real, pred):
# 对于mask,如果编码句⼦中出现了值为 0 的数据,则将其置 0
mask = tf.math.logical_not(tf.math.equal(real, 0))
# 输出句⼦和真正的句⼦计算交叉熵
loss_ = loss_object(real, pred)
吴亦凡小g娜事件# 将⽆效的交叉熵删除
mask = tf.cast(mask, dtype=loss_.dtype)
loss_ *= mask
# 返回平均值
duce_mean(loss_)
同时,定义两个指标⽤于展⽰训练过程中的模型变化:
train_loss = ics.Mean(name='train_loss')
train_accuracy = ics.SparseCategoricalAccuracy(
name='train_accuracy')
4. 优化器与学习率
β1β2ϵ10−9
Transformer 使⽤ Adam 优化器,其  为 0.9,  为0.98,  为
。其学习率随着训练的进程变化:
学习率的变化,我们通过继承 tf.keras.optimizers.schedules.LearningRateSchedule来实现。顾名思义,这个类会创建⼀个可序列化的学习率衰减(也可能增加)时间表:
class CustomSchedule(tf.keras.optimizers.schedules.LearningRateSchedule):
def __init__(self, d_model, warmup_steps=4000):
super(CustomSchedule, self).__init__()
self.d_model = d_model
self.d_model = tf.cast(self.d_model, tf.float32)
self.warmup_steps = warmup_steps
def __call__(self, step): # 这个时间表被调⽤时,按照 step 返回学习率
arg1 = tf.math.rsqrt(step)
arg2 = step * (self.warmup_steps ** -1.5)
return tf.math.rsqrt(self.d_model) * tf.math.minimum(arg1, arg2)
优化器便可以⽅便地使⽤这个类的实例改变学习率优化。
learning_rate = CustomSchedule(d_model)
optimizer = tf.keras.optimizers.Adam(learning_rate, beta_1=0.9, beta_2=0.98,
epsilon=1e-9)
5. ⾃回归原理
⾄今位置,我们已经拥有了 Transformer 的完整模型,数据输⼊和优化器。
但显然,Transformer 和 传统的 RNN 按时序依次读取输⼊和输出的训练⽅式“看起来”不同——它⼀次输⼊整个句⼦。⽽ encoder-decoder 架构是⾃回归的:通过上⼀步产⽣的符号和这⼀步的输⼊来预测这⼀步的输出。开始训练之前,需要了解 Transformer 是如何实现⾃回归的。
Tranformer 使⽤导师监督(teacher-forcing)法,即在预测过程中⽆论模型在当前时间步骤下预测出什么,teacher-forcing ⽅法都会将真实的输出传递到下⼀个时间步骤上。
当 transformer 预测每个词时,⾃注意⼒(self-attention)功能使它能够查看输⼊序列中前⾯的单词,
从⽽更好地预测下⼀个单词。为了仅能让其查看输⼊序列中前⾯的单词,则需要前瞻遮挡来屏蔽后⽅的单词。
也就是说,若输⼊⼀个葡萄⽛⽂句⼦,Tranformer 将第⼀次仅预测出英⽂句⼦的第⼀个单词,然后再次基础上依次预测第⼆个,第三个。
⽽训练过程也应该模拟这样的预测过程,每次仅增加⼀个⽬标序列的单词。
因此,我们将⽬标句⼦改写成两种:
原⽬标句⼦:sentence = "SOS A lion in the jungle is sleeping EOS"
改写为:
tar_inp = "SOS A lion in the jungle is sleeping"