绘制 Transformer 模型的训练和验证损失曲线
我们之前已经看到如何训练 Transformer 模型用于神经机器翻译。在进行训练模型的推断之前,让我们首先探索如何稍微修改训练代码,以便能够绘制在学习过程中生成的训练和验证损失曲线。
训练和验证损失值提供了重要信息,因为它们让我们更好地了解学习性能如何随轮次的变化而变化,并帮助我们诊断任何可能导致模型欠拟合或过拟合的问题。它们还将告诉我们在推断阶段使用训练好的模型权重的轮次。
在本教程中,你将学习如何绘制 Transformer 模型的训练和验证损失曲线。
完成本教程后,你将了解:
-
如何修改训练代码以包括验证和测试划分,除了数据集的训练划分
-
如何修改训练代码以存储计算出的训练和验证损失值,以及训练好的模型权重
-
如何绘制保存的训练和验证损失曲线
通过我的书 《构建具有注意力机制的 Transformer 模型》 启动你的项目。它提供了 自学教程 和 可用代码 来指导你构建一个完全可用的 Transformer 模型。
将句子从一种语言翻译成另一种语言…
让我们开始吧。
绘制 Transformer 模型的训练和验证损失曲线
照片由 Jack Anstey 提供,部分权利保留。
教程概述
本教程分为四部分,它们是:
-
Transformer 架构回顾
-
准备数据集的训练、验证和测试划分
-
训练 Transformer 模型
-
绘制训练和验证损失曲线
先决条件
对于本教程,我们假设你已经熟悉:
Transformer 架构回顾
回忆你已经看到 Transformer 架构遵循编码器-解码器结构。左侧的编码器负责将输入序列映射到一系列连续表示;右侧的解码器接收编码器的输出以及前一个时间步的解码器输出,以生成输出序列。
Transformer 架构的编码器-解码器结构
在生成输出序列时,Transformer 不依赖于递归和卷积。
你已经看到如何训练完整的 Transformer 模型,现在你将看到如何生成和绘制训练和验证损失值,这将帮助你诊断模型的学习性能。
想要开始构建带有注意力机制的 Transformer 模型吗?
现在就参加我的免费 12 天电子邮件速成课程(包含示例代码)。
点击注册,还能获得课程的免费 PDF 电子书版本。
准备数据集的训练、验证和测试拆分
为了能够包括数据的验证和测试拆分,你将通过引入以下代码行来修改准备数据集的代码,这些代码行:
- 指定验证数据拆分的大小。这反过来决定了训练数据和测试数据的大小,我们将把数据分成 80:10:10 的比例,分别用于训练集、验证集和测试集:
Python
self.val_split = 0.1 # Ratio of the validation data split
- 除了训练集外,将数据集拆分为验证集和测试集:
Python
val = dataset[int(self.n_sentences * self.train_split):int(self.n_sentences * (1-self.val_split))]
test = dataset[int(self.n_sentences * (1 - self.val_split)):]
- 通过标记化、填充和转换为张量来准备验证数据。为此,你将把这些操作收集到一个名为
encode_pad
的函数中,如下面的完整代码列表所示。这将避免在对训练数据进行这些操作时代码的过度重复:
Python
valX = self.encode_pad(val[:, 0], enc_tokenizer, enc_seq_length)
valY = self.encode_pad(val[:, 1], dec_tokenizer, dec_seq_length)
- 将编码器和解码器的标记化器保存到 pickle 文件中,并将测试数据集保存到一个文本文件中,以便在推断阶段使用:
Python
self.save_tokenizer(enc_tokenizer, 'enc')
self.save_tokenizer(dec_tokenizer, 'dec')
savetxt('test_dataset.txt', test, fmt='%s')
完整的代码列表现已更新如下:
Python
from pickle import load, dump, HIGHEST_PROTOCOL
from numpy.random import shuffle
from numpy import savetxt
from keras.preprocessing.text import Tokenizer
from keras.preprocessing.sequence import pad_sequences
from tensorflow import convert_to_tensor, int64
class PrepareDataset:
def __init__(self, **kwargs):
super(PrepareDataset, self).__init__(**kwargs)
self.n_sentences = 15000 # Number of sentences to include in the dataset
self.train_split = 0.8 # Ratio of the training data split
self.val_split = 0.1 # Ratio of the validation data split
# Fit a tokenizer
def create_tokenizer(self, dataset):
tokenizer = Tokenizer()
tokenizer.fit_on_texts(dataset)
return tokenizer
def find_seq_length(self, dataset):
return max(len(seq.split()) for seq in dataset)
def find_vocab_size(self, tokenizer, dataset):
tokenizer.fit_on_texts(dataset)
return len(tokenizer.word_index) + 1
# Encode and pad the input sequences
def encode_pad(self, dataset, tokenizer, seq_length):
x = tokenizer.texts_to_sequences(dataset)
x = pad_sequences(x, maxlen=seq_length, padding='post')
x = convert_to_tensor(x, dtype=int64)
return x
def save_tokenizer(self, tokenizer, name):
with open(name + '_tokenizer.pkl', 'wb') as handle:
dump(tokenizer, handle, protocol=HIGHEST_PROTOCOL)
def __call__(self, filename, **kwargs):
# Load a clean dataset
clean_dataset = load(open(filename, 'rb'))
# Reduce dataset size
dataset = clean_dataset[:self.n_sentences, :]
# Include start and end of string tokens
for i in range(dataset[:, 0].size):
dataset[i, 0] = "<START> " + dataset[i, 0] + " <EOS>"
dataset[i, 1] = "<START> " + dataset[i, 1] + " <EOS>"
# Random shuffle the dataset
shuffle(dataset)
# Split the dataset in training, validation and test sets
train = dataset[:int(self.n_sentences * self.train_split)]
val = dataset[int(self.n_sentences * self.train_split):int(self.n_sentences * (1-self.val_split))]
test = dataset[int(self.n_sentences * (1 - self.val_split)):]
# Prepare tokenizer for the encoder input
enc_tokenizer = self.create_tokenizer(dataset[:, 0])
enc_seq_length = self.find_seq_length(dataset[:, 0])
enc_vocab_size = self.find_vocab_size(enc_tokenizer, train[:, 0])
# Prepare tokenizer for the decoder input
dec_tokenizer = self.create_tokenizer(dataset[:, 1])
dec_seq_length = self.find_seq_length(dataset[:, 1])
dec_vocab_size = self.find_vocab_size(dec_tokenizer, train[:, 1])
# Encode and pad the training input
trainX = self.encode_pad(train[:, 0], enc_tokenizer, enc_seq_length)
trainY = self.encode_pad(train[:, 1], dec_tokenizer, dec_seq_length)
# Encode and pad the validation input
valX = self.encode_pad(val[:, 0], enc_tokenizer, enc_seq_length)
valY = self.encode_pad(val[:, 1], dec_tokenizer, dec_seq_length)
# Save the encoder tokenizer
self.save_tokenizer(enc_tokenizer, 'enc')
# Save the decoder tokenizer
self.save_tokenizer(dec_tokenizer, 'dec')
# Save the testing dataset into a text file
savetxt('test_dataset.txt', test, fmt='%s')
return trainX, trainY, valX, valY, train, val, enc_seq_length, dec_seq_length, enc_vocab_size, dec_vocab_size
训练 Transformer 模型
我们将对训练 Transformer 模型的代码进行类似的修改,以:
- 准备验证数据集的批次:
Python
val_dataset = data.Dataset.from_tensor_slices((valX, valY))
val_dataset = val_dataset.batch(batch_size)
- 监控验证损失指标:
Python
val_loss = Mean(name='val_loss')
- 初始化字典以存储训练和验证的损失,并最终将损失值存储在相应的字典中:
Python
train_loss_dict = {}
val_loss_dict = {}
train_loss_dict[epoch] = train_loss.result()
val_loss_dict[epoch] = val_loss.result()
- 计算验证损失:
Python
loss = loss_fcn(decoder_output, prediction)
val_loss(loss)
- 在每个周期保存训练的模型权重。你将在推理阶段使用这些权重来调查模型在不同周期产生的结果差异。在实践中,更高效的做法是包含一个回调方法,该方法根据训练过程中监控的指标停止训练过程,并在此时保存模型权重:
Python
# Save the trained model weights
training_model.save_weights("weights/wghts" + str(epoch + 1) + ".ckpt")
- 最后,将训练和验证损失值保存到 pickle 文件中:
Python
with open('./train_loss.pkl', 'wb') as file:
dump(train_loss_dict, file)
with open('./val_loss.pkl', 'wb') as file:
dump(val_loss_dict, file)
修改后的代码列表现在变为:
Python
from tensorflow.keras.optimizers import Adam
from tensorflow.keras.optimizers.schedules import LearningRateSchedule
from tensorflow.keras.metrics import Mean
from tensorflow import data, train, math, reduce_sum, cast, equal, argmax, float32, GradientTape, function
from keras.losses import sparse_categorical_crossentropy
from model import TransformerModel
from prepare_dataset import PrepareDataset
from time import time
from pickle import dump
# Define the model parameters
h = 8 # Number of self-attention heads
d_k = 64 # Dimensionality of the linearly projected queries and keys
d_v = 64 # Dimensionality of the linearly projected values
d_model = 512 # Dimensionality of model layers' outputs
d_ff = 2048 # Dimensionality of the inner fully connected layer
n = 6 # Number of layers in the encoder stack
# Define the training parameters
epochs = 20
batch_size = 64
beta_1 = 0.9
beta_2 = 0.98
epsilon = 1e-9
dropout_rate = 0.1
# Implementing a learning rate scheduler
class LRScheduler(LearningRateSchedule):
def __init__(self, d_model, warmup_steps=4000, **kwargs):
super(LRScheduler, self).__init__(**kwargs)
self.d_model = cast(d_model, float32)
self.warmup_steps = warmup_steps
def __call__(self, step_num):
# Linearly increasing the learning rate for the first warmup_steps, and decreasing it thereafter
arg1 = step_num ** -0.5
arg2 = step_num * (self.warmup_steps ** -1.5)
return (self.d_model ** -0.5) * math.minimum(arg1, arg2)
# Instantiate an Adam optimizer
optimizer = Adam(LRScheduler(d_model), beta_1, beta_2, epsilon)
# Prepare the training dataset
dataset = PrepareDataset()
trainX, trainY, valX, valY, train_orig, val_orig, enc_seq_length, dec_seq_length, enc_vocab_size, dec_vocab_size = dataset('english-german.pkl')
print(enc_seq_length, dec_seq_length, enc_vocab_size, dec_vocab_size)
# Prepare the training dataset batches
train_dataset = data.Dataset.from_tensor_slices((trainX, trainY))
train_dataset = train_dataset.batch(batch_size)
# Prepare the validation dataset batches
val_dataset = data.Dataset.from_tensor_slices((valX, valY))
val_dataset = val_dataset.batch(batch_size)
# Create model
training_model = TransformerModel(enc_vocab_size, dec_vocab_size, enc_seq_length, dec_seq_length, h, d_k, d_v, d_model, d_ff, n, dropout_rate)
# Defining the loss function
def loss_fcn(target, prediction):
# Create mask so that the zero padding values are not included in the computation of loss
padding_mask = math.logical_not(equal(target, 0))
padding_mask = cast(padding_mask, float32)
# Compute a sparse categorical cross-entropy loss on the unmasked values
loss = sparse_categorical_crossentropy(target, prediction, from_logits=True) * padding_mask
# Compute the mean loss over the unmasked values
return reduce_sum(loss) / reduce_sum(padding_mask)
# Defining the accuracy function
def accuracy_fcn(target, prediction):
# Create mask so that the zero padding values are not included in the computation of accuracy
padding_mask = math.logical_not(equal(target, 0))
# Find equal prediction and target values, and apply the padding mask
accuracy = equal(target, argmax(prediction, axis=2))
accuracy = math.logical_and(padding_mask, accuracy)
# Cast the True/False values to 32-bit-precision floating-point numbers
padding_mask = cast(padding_mask, float32)
accuracy = cast(accuracy, float32)
# Compute the mean accuracy over the unmasked values
return reduce_sum(accuracy) / reduce_sum(padding_mask)
# Include metrics monitoring
train_loss = Mean(name='train_loss')
train_accuracy = Mean(name='train_accuracy')
val_loss = Mean(name='val_loss')
# Create a checkpoint object and manager to manage multiple checkpoints
ckpt = train.Checkpoint(model=training_model, optimizer=optimizer)
ckpt_manager = train.CheckpointManager(ckpt, "./checkpoints", max_to_keep=None)
# Initialise dictionaries to store the training and validation losses
train_loss_dict = {}
val_loss_dict = {}
# Speeding up the training process
@function
def train_step(encoder_input, decoder_input, decoder_output):
with GradientTape() as tape:
# Run the forward pass of the model to generate a prediction
prediction = training_model(encoder_input, decoder_input, training=True)
# Compute the training loss
loss = loss_fcn(decoder_output, prediction)
# Compute the training accuracy
accuracy = accuracy_fcn(decoder_output, prediction)
# Retrieve gradients of the trainable variables with respect to the training loss
gradients = tape.gradient(loss, training_model.trainable_weights)
# Update the values of the trainable variables by gradient descent
optimizer.apply_gradients(zip(gradients, training_model.trainable_weights))
train_loss(loss)
train_accuracy(accuracy)
for epoch in range(epochs):
train_loss.reset_states()
train_accuracy.reset_states()
val_loss.reset_states()
print("\nStart of epoch %d" % (epoch + 1))
start_time = time()
# Iterate over the dataset batches
for step, (train_batchX, train_batchY) in enumerate(train_dataset):
# Define the encoder and decoder inputs, and the decoder output
encoder_input = train_batchX[:, 1:]
decoder_input = train_batchY[:, :-1]
decoder_output = train_batchY[:, 1:]
train_step(encoder_input, decoder_input, decoder_output)
if step % 50 == 0:
print(f'Epoch {epoch + 1} Step {step} Loss {train_loss.result():.4f} Accuracy {train_accuracy.result():.4f}')
# Run a validation step after every epoch of training
for val_batchX, val_batchY in val_dataset:
# Define the encoder and decoder inputs, and the decoder output
encoder_input = val_batchX[:, 1:]
decoder_input = val_batchY[:, :-1]
decoder_output = val_batchY[:, 1:]
# Generate a prediction
prediction = training_model(encoder_input, decoder_input, training=False)
# Compute the validation loss
loss = loss_fcn(decoder_output, prediction)
val_loss(loss)
# Print epoch number and accuracy and loss values at the end of every epoch
print("Epoch %d: Training Loss %.4f, Training Accuracy %.4f, Validation Loss %.4f" % (epoch + 1, train_loss.result(), train_accuracy.result(), val_loss.result()))
# Save a checkpoint after every epoch
if (epoch + 1) % 1 == 0:
save_path = ckpt_manager.save()
print("Saved checkpoint at epoch %d" % (epoch + 1))
# Save the trained model weights
training_model.save_weights("weights/wghts" + str(epoch + 1) + ".ckpt")
train_loss_dict[epoch] = train_loss.result()
val_loss_dict[epoch] = val_loss.result()
# Save the training loss values
with open('./train_loss.pkl', 'wb') as file:
dump(train_loss_dict, file)
# Save the validation loss values
with open('./val_loss.pkl', 'wb') as file:
dump(val_loss_dict, file)
print("Total time taken: %.2fs" % (time() - start_time))
绘制训练和验证损失曲线
为了能够绘制训练和验证损失曲线,你首先需要加载包含训练和验证损失字典的 pickle 文件,这些文件是你在早期训练 Transformer 模型时保存的。
然后你将从各自的字典中检索训练和验证损失值,并在同一图上绘制它们。
代码列表如下,你应该将其保存到一个单独的 Python 脚本中:
Python
from pickle import load
from matplotlib.pylab import plt
from numpy import arange
# Load the training and validation loss dictionaries
train_loss = load(open('train_loss.pkl', 'rb'))
val_loss = load(open('val_loss.pkl', 'rb'))
# Retrieve each dictionary's values
train_values = train_loss.values()
val_values = val_loss.values()
# Generate a sequence of integers to represent the epoch numbers
epochs = range(1, 21)
# Plot and label the training and validation loss values
plt.plot(epochs, train_values, label='Training Loss')
plt.plot(epochs, val_values, label='Validation Loss')
# Add in a title and axes labels
plt.title('Training and Validation Loss')
plt.xlabel('Epochs')
plt.ylabel('Loss')
# Set the tick locations
plt.xticks(arange(0, 21, 2))
# Display the plot
plt.legend(loc='best')
plt.show()
运行上述代码会生成类似下面的训练和验证损失曲线图:
训练和验证损失值在多个训练周期上的折线图
注意,尽管你可能会看到类似的损失曲线,但它们可能不一定与上面的一模一样。这是因为你从头开始训练 Transformer 模型,结果的训练和验证损失值取决于模型权重的随机初始化。
尽管如此,这些损失曲线为我们提供了更好的洞察力,了解学习性能如何随训练周期数变化,并帮助我们诊断可能导致欠拟合或过拟合模型的学习问题。
关于如何使用训练和验证损失曲线来诊断模型的学习表现,您可以参考 Jason Brownlee 的这篇教程。
进一步阅读
本节提供了更多资源,如果你希望深入了解这个话题。
书籍
-
Python 深度学习进阶,2019 年
-
用于自然语言处理的 Transformers,2021 年
文献
- Attention Is All You Need,2017 年
网站
- 如何使用学习曲线诊断机器学习模型性能,
machinelearningmastery.com/learning-curves-for-diagnosing-machine-learning-model-performance/
总结
在本教程中,您学习了如何绘制 Transformer 模型的训练和验证损失曲线。
具体来说,您学到了:
-
如何修改训练代码以包括验证集和测试集分割,除了数据集的训练分割。
-
如何修改训练代码以存储计算的训练和验证损失值,以及训练好的模型权重。
-
如何绘制保存的训练和验证损失曲线。
你有任何问题吗?
在下面的评论中提出你的问题,我会尽力回答。
从头开始了解注意力机制
原文:
machinelearningmastery.com/the-attention-mechanism-from-scratch/
引入注意力机制是为了提高编码器-解码器模型在机器翻译中的性能。注意力机制的想法是允许解码器以灵活的方式利用输入序列中最相关的部分,通过对所有编码的输入向量进行加权组合,其中最相关的向量被赋予最高的权重。
在本教程中,你将发现注意力机制及其实现。
完成本教程后,你将了解:
-
注意力机制如何使用所有编码器隐藏状态的加权和来灵活地将解码器的注意力集中在输入序列中最相关的部分
-
如何将注意力机制推广到信息可能不一定按顺序相关的任务中
-
如何在 Python 中使用 NumPy 和 SciPy 实现通用注意力机制
启动你的项目,可以参考我的书籍 使用注意力构建 Transformer 模型。书中提供了自学教程和可运行的代码,指导你构建一个功能完整的 Transformer 模型
将句子从一种语言翻译成另一种语言…
让我们开始吧。
从头开始了解注意力机制
图片由 Nitish Meena 提供,部分权利保留。
教程概览
本教程分为三个部分;它们是:
-
注意力机制
-
通用注意力机制
-
使用 NumPy 和 SciPy 的通用注意力机制
注意力机制
注意力机制由 Bahdanau 等人 (2014) 引入,以解决使用固定长度编码向量时出现的瓶颈问题,其中解码器对输入提供的信息的访问有限。这在处理长和/或复杂序列时尤为成问题,因为它们的表示维度被强制与较短或较简单序列的维度相同。
注意事项 请注意,Bahdanau 等人的注意力机制被分为对齐分数、权重和上下文向量的逐步计算:
- 对齐分数:对齐模型使用编码的隐藏状态 h i \mathbf{h}_i hi和先前的解码器输出 s t − 1 \mathbf{s}_{t-1} st−1来计算一个分数 e t , i e_{t,i} et,i,该分数表示输入序列的元素与当前位置 t t t的当前输出对齐的程度。对齐模型由一个函数 a ( . ) a(.) a(.)表示,该函数可以通过前馈神经网络实现:
e t , i = a ( s t − 1 , h i ) e_{t,i} = a(\mathbf{s}_{t-1}, \mathbf{h}_i) et,i=a(st−1,hi)
- 权重:权重, α t , i \alpha_{t,i} αt,i,通过对先前计算的对齐分数应用 softmax 操作来计算:
α t , i = softmax ( e t , i ) \alpha_{t,i} = \text{softmax}(e_{t,i}) αt,i=softmax(et,i)
- 上下文向量:在每个时间步骤中,唯一的上下文向量 c t \mathbf{c}_t ct被输入到解码器中。它通过对所有 T T T个编码器隐藏状态的加权和来计算:
c t = ∑ i = 1 T α t , i h i \mathbf{c}_t = \sum_{i=1}^T \alpha_{t,i} \mathbf{h}_i ct=i=1∑Tαt,ihi
Bahdanau 等人实现了一个用于编码器和解码器的 RNN。
然而,注意力机制可以重新公式化为可以应用于任何序列到序列(简称 seq2seq)任务的一般形式,其中信息可能不一定以顺序方式相关。
换句话说,数据库不必由不同步骤的隐藏 RNN 状态组成,而可以包含任何类型的信息。
– 高级深度学习与 Python,2019 年。
一般注意力机制
一般注意力机制使用三个主要组件,即查询, Q \mathbf{Q} Q,键, K \mathbf{K} K,和值, V \mathbf{V} V。
如果你要将这三个组件与 Bahdanau 等人提出的注意力机制进行比较,那么查询将类似于先前的解码器输出, s t − 1 \mathbf{s}_{t-1} st−1,而值将类似于编码的输入, h i \mathbf{h}_i hi。在 Bahdanau 注意力机制中,键和值是相同的向量。
在这种情况下,我们可以将向量 s t − 1 \mathbf{s}_{t-1} st−1视为对键值对数据库执行的查询,其中键是向量,而隐藏状态 h i \mathbf{h}_i hi是值。
– 高级深度学习与 Python,2019 年。
一般注意力机制执行以下计算:
- 每个查询向量 q = s t − 1 \mathbf{q} = \mathbf{s}_{t-1} q=st−1与键的数据库进行匹配,以计算分数值。此匹配操作计算为特定查询与每个键向量 k i \mathbf{k}_i ki的点积:
e q , k i = q ⋅ k i e_{\mathbf{q},\mathbf{k}_i} = \mathbf{q} \cdot \mathbf{k}_i eq,ki=q⋅ki
- 分数通过 softmax 操作生成权重:
α q , k i = softmax ( e q , k i ) \alpha_{\mathbf{q},\mathbf{k}_i} = \text{softmax}(e_{\mathbf{q},\mathbf{k}_i}) αq,ki=softmax(eq,ki)
- 然后,通过对值向量 v k i \mathbf{v}_{\mathbf{k}_i} vki进行加权求和来计算广义注意力,其中每个值向量都与相应的键配对:
attention ( q , K , V ) = ∑ i α q , k i v k i \text{attention}(\mathbf{q}, \mathbf{K}, \mathbf{V}) = \sum_i \alpha_{\mathbf{q},\mathbf{k}_i} \mathbf{v}_{\mathbf{k}_i} attention(q,K,V)=i∑αq,kivki
在机器翻译的背景下,输入句子中的每个词都会被分配自己的查询、键和值向量。这些向量是通过将编码器对特定词的表示与训练过程中生成的三种不同权重矩阵相乘而得到的。
实质上,当广义注意力机制接收到一系列词时,它会将序列中某个特定词的查询向量与数据库中的每个键进行评分。通过这样做,它捕捉到所考虑的词与序列中其他词的关系。然后,它根据注意力权重(从评分中计算得出)对值进行缩放,以保持对与查询相关的词的关注。这样,它会为所考虑的词生成注意力输出。
想要开始构建带有注意力机制的 Transformer 模型吗?
现在就报名参加我的 12 天免费邮件速成课程(附带示例代码)。
点击报名并获取课程的免费 PDF 电子书版本。
使用 NumPy 和 SciPy 的通用注意力机制
本节将探讨如何使用 Python 中的 NumPy 和 SciPy 库实现通用注意力机制。
为了简单起见,你将首先计算四个词序列中第一个词的注意力。然后,你将对代码进行泛化,以矩阵形式计算所有四个词的注意力输出。
因此,让我们首先定义四个不同词的词嵌入,以计算注意力。在实际操作中,这些词嵌入将由编码器生成;然而,在这个例子中,你将手动定义它们。
# encoder representations of four different words
word_1 = array([1, 0, 0])
word_2 = array([0, 1, 0])
word_3 = array([1, 1, 0])
word_4 = array([0, 0, 1])
下一步生成权重矩阵,你最终会将这些矩阵乘以词嵌入,以生成查询、键和值。在这里,你将随机生成这些权重矩阵;然而,在实际操作中,这些权重矩阵将通过训练学习得到。
...
# generating the weight matrices
random.seed(42) # to allow us to reproduce the same attention values
W_Q = random.randint(3, size=(3, 3))
W_K = random.randint(3, size=(3, 3))
W_V = random.randint(3, size=(3, 3))
请注意,这些矩阵的行数等于词嵌入的维度(在本例中为三),以便进行矩阵乘法。
随后,通过将每个词嵌入与每个权重矩阵相乘来生成每个词的查询、键和值向量。
...
# generating the queries, keys and values
query_1 = word_1 @ W_Q
key_1 = word_1 @ W_K
value_1 = word_1 @ W_V
query_2 = word_2 @ W_Q
key_2 = word_2 @ W_K
value_2 = word_2 @ W_V
query_3 = word_3 @ W_Q
key_3 = word_3 @ W_K
value_3 = word_3 @ W_V
query_4 = word_4 @ W_Q
key_4 = word_4 @ W_K
value_4 = word_4 @ W_V
只考虑第一个词的情况下,下一步是使用点积操作对其查询向量与所有键向量进行评分。
...
# scoring the first query vector against all key vectors
scores = array([dot(query_1, key_1), dot(query_1, key_2), dot(query_1, key_3), dot(query_1, key_4)])
评分值随后通过 softmax 操作生成权重。在此之前,通常将评分值除以关键向量维度的平方根(在此案例中为三),以保持梯度稳定。
...
# computing the weights by a softmax operation
weights = softmax(scores / key_1.shape[0] ** 0.5)
最后,通过所有四个值向量的加权总和计算注意力输出。
...
# computing the attention by a weighted sum of the value vectors
attention = (weights[0] * value_1) + (weights[1] * value_2) + (weights[2] * value_3) + (weights[3] * value_4)
print(attention)
[0.98522025 1.74174051 0.75652026]
为了加快处理速度,相同的计算可以以矩阵形式实现,一次生成所有四个词的注意力输出:
from numpy import array
from numpy import random
from numpy import dot
from scipy.special import softmax
# encoder representations of four different words
word_1 = array([1, 0, 0])
word_2 = array([0, 1, 0])
word_3 = array([1, 1, 0])
word_4 = array([0, 0, 1])
# stacking the word embeddings into a single array
words = array([word_1, word_2, word_3, word_4])
# generating the weight matrices
random.seed(42)
W_Q = random.randint(3, size=(3, 3))
W_K = random.randint(3, size=(3, 3))
W_V = random.randint(3, size=(3, 3))
# generating the queries, keys and values
Q = words @ W_Q
K = words @ W_K
V = words @ W_V
# scoring the query vectors against all key vectors
scores = Q @ K.transpose()
# computing the weights by a softmax operation
weights = softmax(scores / K.shape[1] ** 0.5, axis=1)
# computing the attention by a weighted sum of the value vectors
attention = weights @ V
print(attention)
[[0.98522025 1.74174051 0.75652026]
[0.90965265 1.40965265 0.5 ]
[0.99851226 1.75849334 0.75998108]
[0.99560386 1.90407309 0.90846923]]
进一步阅读
本节提供了更多关于该主题的资源,如果你希望深入了解。
书籍
-
使用 Python 的高级深度学习,2019 年。
-
深度学习要点,2018 年。
论文
- 通过联合学习对齐和翻译的神经机器翻译,2014 年。
总结
在本教程中,你了解了注意机制及其实现。
具体来说,你学到了:
-
注意机制如何使用所有编码器隐藏状态的加权总和来灵活地将解码器的注意力集中在输入序列中最相关的部分
-
注意机制如何推广到信息不一定按顺序相关的任务
-
如何使用 NumPy 和 SciPy 实现通用注意机制
你有什么问题吗?
在下面的评论中提出你的问题,我会尽力回答。
Bahdanau 注意力机制
原文:
machinelearningmastery.com/the-bahdanau-attention-mechanism/
用于机器翻译的传统编码器-解码器架构将每个源句编码成一个固定长度的向量,无论其长度如何,解码器将生成一个翻译。这使得神经网络难以处理长句子,实质上导致了性能瓶颈。
Bahdanau 注意力被提出来解决传统编码器-解码器架构的性能瓶颈,相较于传统方法实现了显著改进。
在本教程中,您将了解神经机器翻译的 Bahdanau 注意力机制。
完成本教程后,您将了解:
-
Bahdanau 注意力机制名称的来源及其解决的挑战
-
形成 Bahdanau 编码器-解码器架构的不同组成部分的角色
-
Bahdanau 注意力算法执行的操作
启动您的项目,使用我的书籍使用注意力构建 Transformer 模型。它提供了自学教程和工作代码,指导您构建一个完全工作的 Transformer 模型,可以
将一种语言的句子翻译成另一种语言…
让我们开始吧。
Bahdanau 注意力机制
图片由Sean Oulashin拍摄,部分权利保留。
教程概览
本教程分为两部分;它们是:
-
介绍 Bahdanau 注意力
-
Bahdanau 架构
-
编码器
-
解码器
-
Bahdanau 注意力算法
-
先决条件
对于本教程,我们假设您已经熟悉:
介绍 Bahdanau 注意力
Bahdanau 注意力机制的名称源自其发表论文的第一作者。
它遵循了Cho et al. (2014)和Sutskever et al. (2014)的研究,他们也采用了 RNN 编码器-解码器框架进行神经机器翻译,具体通过将变长的源句子编码成固定长度的向量,然后再解码为变长的目标句子。
Bahdanau et al. (2014)认为,将变长输入编码为固定长度向量会挤压源句子的的信息,无论其长度如何,导致基本的编码器-解码器模型随着输入句子长度的增加而性能迅速恶化。他们提出的方法用变长向量替代固定长度向量,以提高基本编码器-解码器模型的翻译性能。
这种方法与基本的编码器-解码器方法的最重要的区别在于,它不会试图将整个输入句子编码为单一的固定长度向量。相反,它将输入句子编码为向量序列,并在解码翻译时自适应地选择这些向量的子集。
– 通过联合学习对齐和翻译的神经机器翻译,2014 年。
想开始构建带有注意力的 Transformer 模型吗?
立即参加我的免费 12 天电子邮件速成课程(附带示例代码)。
点击注册并获取课程的免费 PDF 电子书版本。
Bahdanau 架构
Bahdanau 编码器-解码器架构使用的主要组件如下:
-
s t − 1 \mathbf{s}_{t-1} st−1是前一时间步 t − 1 t-1 t−1的隐藏解码器状态。
-
c t \mathbf{c}_t ct是时间步 t t t的上下文向量。它在每个解码器步骤中独特生成,以生成目标单词 y t y_t yt。
-
h i \mathbf{h}_i hi是一个注释,它捕捉了构成整个输入句子 { x 1 , x 2 , … , x T } \{ x_1, x_2, \dots, x_T \} {x1,x2,…,xT}的单词中的信息,特别关注第 i i i个单词(共 T T T个单词)。
-
α t , i \alpha_{t,i} αt,i是分配给当前时间步 t t t的每个注释 h i \mathbf{h}_i hi的权重值。
-
e t , i e_{t,i} et,i是由对齐模型 a ( . ) a(.) a(.)生成的注意力分数,用来评分 s t − 1 \mathbf{s}_{t-1} st−1和 h i \mathbf{h}_i hi的匹配程度。
这些组件在 Bahdanau 架构的不同阶段发挥作用,该架构使用双向 RNN 作为编码器,RNN 解码器,并且在两者之间有一个注意力机制:
Bahdanau 架构
编码器
编码器的角色是为输入句子中每个单词 x i x_i xi生成一个注释 h i \mathbf{h}_i hi,输入句子的长度为 T T T个单词。
为了实现这一目的,Bahdanau 等人采用了一个双向 RNN,它首先正向读取输入句子以生成前向隐藏状态 h i → \overrightarrow{\mathbf{h}_i} hi,然后反向读取输入句子以生成后向隐藏状态 h i ← \overleftarrow{\mathbf{h}_i} hi。对于某个特定词 x i x_i xi,其注释将这两个状态连接起来:
h i = [ h i T → ; h i T ← ] T \mathbf{h}_i = \left[ \overrightarrow{\mathbf{h}_i^T} \; ; \; \overleftarrow{\mathbf{h}_i^T} \right]^T hi=[hiT;hiT]T
以这种方式生成每个注释的思想是捕获前面和后面单词的摘要。
通过这种方式,注释 h i \mathbf{h}_i hi 包含了前面单词和后续单词的摘要。
– 神经机器翻译:联合学习对齐和翻译,2014 年。
生成的注释然后传递给解码器以生成上下文向量。
解码器
解码器的作用是通过关注源句子中包含的最相关信息来生成目标词语。为此,它利用了一个注意力机制。
每当提议的模型在翻译中生成一个词时,它(软)搜索源句子中信息最集中的一组位置。然后,基于与这些源位置相关的上下文向量和之前生成的所有目标词语,模型预测目标词语。
– 神经机器翻译:联合学习对齐和翻译,2014 年。
解码器将每个注释与对齐模型 a ( . ) a(.) a(.) 和前一个隐藏解码器状态 s t − 1 \mathbf{s}_{t-1} st−1 一起提供,这生成一个注意力分数:
e t , i = a ( s t − 1 , h i ) e_{t,i} = a(\mathbf{s}_{t-1}, \mathbf{h}_i) et,i=a(st−1,hi)
这里由对齐模型实现的函数将 s t − 1 \mathbf{s}_{t-1} st−1 和 h i \mathbf{h}_i hi 使用加法操作组合起来。因此,Bahdanau 等人实现的注意力机制被称为加性注意力。
这可以通过两种方式实现,要么 (1) 在连接向量 s t − 1 \mathbf{s}_{t-1} st−1 和 h i \mathbf{h}_i hi 上应用权重矩阵 W \mathbf{W} W,要么 (2) 分别对 s t − 1 \mathbf{s}_{t-1} st−1 和 h i \mathbf{h}_i hi 应用权重矩阵 W 1 \mathbf{W}_1 W1 和 W 2 \mathbf{W}_2 W2:
-
a ( s t − 1 , h i ) = v T tanh ( W [ h i ; s t − 1 ] ) a(\mathbf{s}_{t-1}, \mathbf{h}_i) = \mathbf{v}^T \tanh(\mathbf{W}[\mathbf{h}_i \; ; \; \mathbf{s}_{t-1}]) a(st−1,hi)=vTtanh(W[hi;st−1])
-
a ( s t − 1 , h i ) = v T tanh ( W 1 h i + W 2 s t − 1 ) a(\mathbf{s}_{t-1}, \mathbf{h}_i) = \mathbf{v}^T \tanh(\mathbf{W}_1 \mathbf{h}_i + \mathbf{W}_2 \mathbf{s}_{t-1}) a(st−1,hi)=vTtanh(W1hi+W2st−1)
这里, v \mathbf{v} v 是一个权重向量。
对齐模型被参数化为一个前馈神经网络,并与其余系统组件一起进行训练。
随后,对每个注意力分数应用 softmax 函数以获得相应的权重值:
α t , i = softmax ( e t , i ) \alpha_{t,i} = \text{softmax}(e_{t,i}) αt,i=softmax(et,i)
softmax 函数的应用本质上将注释值归一化到 0 到 1 的范围,因此,结果权重可以视为概率值。每个概率(或权重)值反映了 h i \mathbf{h}_i hi 和 s t − 1 \mathbf{s}_{t-1} st−1 在生成下一个状态 s t \mathbf{s}_t st 和下一个输出 y t y_t yt 时的重要性。
直观地说,这在解码器中实现了一个注意力机制。解码器决定要关注源句子的哪些部分。通过让解码器具备注意力机制,我们减轻了编码器必须将源句子中的所有信息编码成固定长度向量的负担。
– 神经机器翻译:通过联合学习对齐和翻译,2014 年。
最终计算上下文向量作为注释的加权和:
c t = ∑ i = 1 T α t , i h i \mathbf{c}_t = \sum^T_{i=1} \alpha_{t,i} \mathbf{h}_i ct=i=1∑Tαt,ihi
Bahdanau 注意力算法
总结来说,Bahdanau 等人提出的注意力算法执行以下操作:
-
编码器从输入句子生成一组注释 h i \mathbf{h}_i hi。
-
这些注释被输入到对齐模型和之前的隐藏解码器状态中。对齐模型使用这些信息生成注意力分数 e t , i e_{t,i} et,i。
-
对注意力分数应用了 softmax 函数,将其有效地归一化为权重值, α t , i \alpha_{t,i} αt,i,范围在 0 到 1 之间。
-
结合先前计算的注释,这些权重用于通过注释的加权和生成上下文向量 c t \mathbf{c}_t ct。
-
上下文向量与之前的隐藏解码器状态和先前输出一起输入解码器,以计算最终输出 y t y_t yt。
-
步骤 2-6 会重复直到序列结束。
Bahdanau 等人对其架构进行了英法翻译任务的测试。他们报告称,他们的模型显著优于传统的编码器-解码器模型,无论句子长度如何。
已经有几个对 Bahdanau 注意力的改进,例如 Luong 等人 (2015) 提出的改进,我们将在单独的教程中回顾。
进一步阅读
本节提供了更多关于该主题的资源,如果你想深入了解。
书籍
- 深入学习与 Python,2019 年。
论文
- 神经机器翻译:通过联合学习对齐和翻译,2014 年。
总结
在本教程中,你发现了 Bahdanau 注意力机制在神经机器翻译中的应用。
具体来说,你学到了:
-
Bahdanau 注意力的名字来源于哪里以及它所解决的挑战。
-
组成 Bahdanau 编码器-解码器架构的不同组件的作用
-
Bahdanau 注意力算法执行的操作
你有什么问题吗?
在下面的评论中提出你的问题,我会尽力回答。
Luong 注意力机制
原文:
machinelearningmastery.com/the-luong-attention-mechanism/
Luong 注意力旨在对 Bahdanau 模型进行若干改进,特别是通过引入两种新的注意力机制:一种是 全局 方法,关注所有源单词,另一种是 局部 方法,只关注在预测目标句子时选择的单词子集。
在本教程中,你将发现 Luong 注意力机制在神经机器翻译中的应用。
完成本教程后,你将了解:
-
Luong 注意力算法执行的操作
-
全局和局部注意力模型如何工作。
-
Luong 注意力与 Bahdanau 注意力的比较
用我的书 《构建带有注意力的 Transformer 模型》 来启动你的项目。它提供了 自学教程 和 可运行的代码,帮助你构建一个完全运行的 Transformer 模型。
将句子从一种语言翻译成另一种语言…
开始吧。
Luong 注意力机制
图片来源 Mike Nahlii,版权所有。
教程概述
本教程分为五部分;它们是:
-
Luong 注意力简介
-
Luong 注意力算法
-
全局注意力模型
-
局部注意力模型
-
与 Bahdanau 注意力的比较
先决条件
在本教程中,我们假设你已经熟悉:
Luong 注意力简介
Luong 等人 (2015) 从先前的注意力模型中汲取灵感,提出了两种注意力机制:
在这项工作中,我们以简洁和有效性为目标,设计了两种新型的基于注意力的模型:一种是全局方法,它总是关注所有源单词,另一种是局部方法,它仅关注一次性选择的源单词子集。
– 基于注意力的神经机器翻译的有效方法,2015 年。
全局 注意力模型类似于 Bahdanau 等人 (2014) 模型,关注 所有 源单词,但旨在在结构上简化它。
局部 注意力模型受到 Xu 等人 (2016) 的硬注意力和软注意力模型的启发,只关注 少量 源位置。
两种注意力模型在预测当前词的许多步骤中是相似的,但主要在于它们计算上下文向量的方式不同。
让我们首先看看整体的 Luong 注意力算法,然后再深入探讨全局和局部注意力模型之间的差异。
想开始构建具有注意力机制的 Transformer 模型吗?
立即参加我的免费 12 天电子邮件速成课程(包含示例代码)。
点击注册,还可以获得课程的免费 PDF 电子书版本。
Luong 注意力算法
Luong 等人的注意力算法执行以下操作:
-
编码器从输入句子中生成一组注释, H = h i , i = 1 , … , T H = \mathbf{h}_i, i = 1, \dots, T H=hi,i=1,…,T。
-
当前的解码器隐藏状态计算公式为: s t = RNN decoder ( s t − 1 , y t − 1 ) \mathbf{s}_t = \text{RNN}_\text{decoder}(\mathbf{s}_{t-1}, y_{t-1}) st=RNNdecoder(st−1,yt−1)。这里, s t − 1 \mathbf{s}_{t-1} st−1 表示先前的隐藏解码器状态,而 y t − 1 y_{t-1} yt−1 是前一个解码器输出。
-
对齐模型 a ( . ) a(.) a(.) 使用注释和当前解码器隐藏状态来计算对齐分数: e t , i = a ( s t , h i ) e_{t,i} = a(\mathbf{s}_t, \mathbf{h}_i) et,i=a(st,hi)。
-
将 softmax 函数应用于对齐分数,有效地将其归一化为介于 0 和 1 之间的权重值: α t , i = softmax ( e t , i ) \alpha_{t,i} = \text{softmax}(e_{t,i}) αt,i=softmax(et,i)。
-
与之前计算的注释一起,这些权重被用于通过加权求和生成上下文向量: c t = ∑ i = 1 T α t , i h i \mathbf{c}_t = \sum^T_{i=1} \alpha_{t,i} \mathbf{h}_i ct=∑i=1Tαt,ihi。
-
基于上下文向量和当前解码器隐藏状态的加权连接计算注意力隐藏状态: s ~ t = tanh ( W c [ c t ; s t ] ) \widetilde{\mathbf{s}}_t = \tanh(\mathbf{W_c} [\mathbf{c}_t \; ; \; \mathbf{s}_t]) s t=tanh(Wc[ct;st])。
-
解码器通过输入加权注意力隐藏状态来生成最终输出: y t = softmax ( W y s ~ t ) y_t = \text{softmax}(\mathbf{W}_y \widetilde{\mathbf{s}}_t) yt=softmax(Wys t)。
-
步骤 2-7 重复直到序列结束。
全局注意力模型
全局注意力模型在生成对齐分数时考虑了输入句子中的所有源词,最终在计算上下文向量时也会考虑这些源词。
全局注意力模型的思想是,在推导上下文向量 c t \mathbf{c}_t ct 时考虑编码器的所有隐藏状态。
– 基于注意力的神经机器翻译的有效方法,2015 年。
为了实现这一点,Luong 等人提出了三种计算对齐分数的替代方法。第一种方法类似于 Bahdanau 的方法。它基于 s t \mathbf{s}_t st 和 h i \mathbf{h}_i hi 的连接,而第二种和第三种方法则实现了 乘法 注意力(与 Bahdanau 的 加法 注意力相对):
-
a ( s t , h i ) = v a T tanh ( W a [ s t ; h i ] ) a(\mathbf{s}_t, \mathbf{h}_i) = \mathbf{v}_a^T \tanh(\mathbf{W}_a [\mathbf{s}_t \; ; \; \mathbf{h}_i]) a(st,hi)=vaTtanh(Wa[st;hi])
-
a ( s t , h i ) = s t T h i a(\mathbf{s}_t, \mathbf{h}_i) = \mathbf{s}^T_t \mathbf{h}_i a(st,hi)=stThi
-
a ( s t , h i ) = s t T W a h i a(\mathbf{s}_t, \mathbf{h}_i) = \mathbf{s}^T_t \mathbf{W}_a \mathbf{h}_i a(st,hi)=stTWahi
在这里, W a \mathbf{W}_a Wa是一个可训练的权重矩阵,类似地, v a \mathbf{v}_a va是一个权重向量。
从直观上讲,乘法注意力中使用点积可以解释为提供了向量 s t \mathbf{s}_t st和 h i \mathbf{h}_i hi之间的相似性度量。
……如果向量相似(即对齐),则乘法结果将是一个大值,注意力将集中在当前的 t,i 关系上。
– 用 Python 进行高级深度学习,2019。
结果对齐向量 e t \mathbf{e}_t et的长度根据源词的数量而变化。
局部注意力模型
在关注所有源词时,全局注意力模型计算开销大,可能会使其在翻译较长句子时变得不切实际。
局部注意力模型试图通过专注于较小的源词子集来生成每个目标词,从而解决这些局限性。为此,它从Xu 等人(2016)的图像描述生成工作中的硬和软注意力模型中获得灵感:
-
软注意力等同于全局注意力方法,其中权重软性地分布在所有源图像区域上。因此,软注意力将整个源图像考虑在内。
-
硬注意力一次关注一个图像区域。
Luong 等人的局部注意力模型通过计算在对齐位置 p t p_t pt中心窗口内注释集 h i \mathbf{h}_i hi上的加权平均来生成上下文向量:
[ p t – D , p t + D ] [p_t – D, p_t + D] [pt–D,pt+D]
虽然 D D D的值是通过经验选择的,但 Luong 等人考虑了计算 p t p_t pt值的两种方法:
-
单调对齐:源句子和目标句子假定是单调对齐的,因此 p t = t p_t = t pt=t。
-
预测对齐:基于可训练的模型参数 W p \mathbf{W}_p Wp和 v p \mathbf{v}_p vp以及源句子长度 S S S对对齐位置进行预测:
p t = S ⋅ sigmoid ( v p T tanh ( W p , s t ) ) p_t = S \cdot \text{sigmoid}(\mathbf{v}^T_p \tanh(\mathbf{W}_p, \mathbf{s}_t)) pt=S⋅sigmoid(vpTtanh(Wp,st))
高斯分布在计算对齐权重时围绕 p t p_t pt中心,以偏好窗口中心附近的源词。
这一次,结果对齐向量 e t \mathbf{e}_t et具有固定长度 2 D + 1 2D + 1 2D+1。
启动你的项目,请参见我的书使用注意力构建 Transformer 模型。它提供了自学教程和有效代码,引导你构建一个完整的 Transformer 模型。
将句子从一种语言翻译成另一种语言……
与 Bahdanau 注意力的比较
Bahdanau 模型和 Luong 等人的全局注意力方法大致相似,但两者之间存在关键差异:
尽管我们的全球注意力方法在精神上类似于 Bahdanau 等人(2015 年)提出的模型,但存在若干关键区别,这些区别反映了我们如何从原始模型中进行简化和概括。
– 基于注意力的神经机器翻译的有效方法,2015 年。
- 最显著的是,Luong 全球注意力模型中对齐得分 e t e_t et 的计算依赖于当前解码器隐藏状态 s t \mathbf{s}_t st,而非 Bahdanau 注意力中的前一个隐藏状态 s t − 1 \mathbf{s}_{t-1} st−1。
Bahdanau 架构(左)与 Luong 架构(右)
摘自 “深入学习 Python”
-
Luong 等人舍弃了 Bahdanau 模型中使用的双向编码器,而是利用编码器和解码器顶部 LSTM 层的隐藏状态。
-
Luong 等人的全球注意力模型研究了使用乘法注意力作为 Bahdanau 加性注意力的替代方案。
进一步阅读
本节提供了更多相关资源,供你深入了解。
书籍
- 深入学习 Python,2019 年。
论文
- 基于注意力的神经机器翻译的有效方法,2015 年。
总结
在本教程中,你了解了 Luong 注意力机制在神经机器翻译中的应用。
具体来说,你学到了:
-
Luong 注意力算法执行的操作
-
全球和局部注意力模型如何工作
-
Luong 注意力与 Bahdanau 注意力的比较
你有任何问题吗?
在下方评论中提出你的问题,我会尽力回答。
Transformer 注意力机制
原文:
machinelearningmastery.com/the-transformer-attention-mechanism/
在引入 Transformer 模型之前,用于神经机器翻译的注意力使用 RNN-based 编码器-解码器架构实现。Transformer 模型通过摒弃循环和卷积,并仅依赖自注意力机制,彻底改变了注意力的实现方式。
在本教程中,我们首先关注 Transformer 注意力机制,随后在另一个教程中回顾 Transformer 模型。
在本教程中,您将了解神经机器翻译的 Transformer 注意力机制。
完成本教程后,您将了解到:
-
Transformer 注意力机制与其前身有何不同
-
Transformer 如何计算缩放点积注意力
-
Transformer 如何计算多头注意力
启动您的项目,阅读我的书 使用注意力构建 Transformer 模型。它提供了带有 工作代码 的 自学教程,引导您构建一个完全可工作的 Transformer 模型,能够
将句子从一种语言翻译成另一种语言…
让我们开始吧。
Transformer 注意力机制
照片由 Andreas Gücklhorn 提供,某些权利保留。
教程概览
本教程分为两部分;它们是:
-
介绍 Transformer 注意力机制
-
Transformer 注意力机制
-
缩放点积注意力
-
多头注意力
-
先决条件
对于本教程,我们假设您已经熟悉:
介绍 Transformer 注意力机制
到目前为止,您已经熟悉了在 RNN-based 编码器-解码器架构中使用注意力机制。其中两个最流行的模型是由 Bahdanau et al. (2014) 和 Luong et al. (2015) 提出的。
Transformer 架构通过摒弃依赖于循环和卷积的方式,彻底改变了注意力的使用。
… 变压器是第一个完全依赖自注意力计算输入和输出表示的转导模型,而无需使用序列对齐的 RNN 或卷积。
– 注意力机制全靠它,2017。
在他们的论文《注意力机制全靠它》中,Vaswani 等人 (2017) 解释了变压器模型如何完全依赖于自注意力机制,其中序列(或句子)的表示是通过关联同一序列中的不同单词来计算的。
自注意力,有时称为内注意力,是一种注意力机制,通过关联单个序列的不同位置来计算该序列的表示。
– 注意力机制全靠它,2017。
变压器注意力机制
变压器注意力机制使用的主要组件如下:
-
q \mathbf{q} q 和 k \mathbf{k} k 分别表示维度为 d k d_k dk 的查询和键向量
-
v \mathbf{v} v 表示维度为 d v d_v dv 的值向量
-
Q \mathbf{Q} Q、 K \mathbf{K} K 和 V \mathbf{V} V 分别表示打包在一起的查询、键和值的矩阵。
-
W Q \mathbf{W}^Q WQ、 W K \mathbf{W}^K WK 和 W V \mathbf{W}^V WV 分别表示用于生成查询、键和值矩阵不同子空间表示的投影矩阵
-
W O \mathbf{W}^O WO 表示用于多头输出的投影矩阵
实质上,注意力函数可以被视为查询与一组键值对之间的映射,得到一个输出。
输出作为值的加权和计算,其中每个值分配的权重由查询与相应键的兼容性函数计算得出。
– 注意力机制全靠它,2017。
Vaswani 等人提出了一种 缩放点积注意力,并在此基础上提出了 多头注意力。在神经机器翻译的背景下,作为这些注意力机制输入的查询、键和值是同一句输入的不同投影。
直观地说,提出的注意力机制通过捕捉同一句子中不同元素(在这种情况下是单词)之间的关系来实现自注意力。
想要开始构建带有注意力机制的变压器模型吗?
立即参加我的免费 12 天电子邮件速成课程(附带示例代码)。
点击以注册并获取课程的免费 PDF 电子书版本。
缩放点积注意力
变压器实现了一种缩放点积注意力,这遵循了你之前见过的 通用注意力机制 的过程。
正如名称所示,缩放点积注意力首先对每个查询 q \mathbf{q} q与所有键 k \mathbf{k} k计算一个点积。随后,它将每个结果除以 d k \sqrt{d_k} dk,并应用 softmax 函数。这样,它获得了用于缩放值 v \mathbf{v} v的权重。
缩放点积注意力
实际上,缩放点积注意力执行的计算可以高效地同时应用于整个查询集。为此,矩阵— Q \mathbf{Q} Q、 K \mathbf{K} K和 V \mathbf{V} V—作为输入提供给注意力函数:
attention ( Q , K , V ) = softmax ( Q K T d k ) V \text{attention}(\mathbf{Q}, \mathbf{K}, \mathbf{V}) = \text{softmax} \left( \frac{QK^T}{\sqrt{d_k}} \right) V attention(Q,K,V)=softmax(dkQKT)V
Vaswani 等人解释说,他们的缩放点积注意力与Luong 等人(2015)的乘法注意力是相同的,唯一的不同是添加了缩放因子 1 d k \tfrac{1}{\sqrt{d_k}} dk1。
引入这个缩放因子的目的是为了抵消当 d k d_k dk的值很大时,点积增长幅度较大的效果,此时应用 softmax 函数会返回极小的梯度,导致著名的梯度消失问题。因此,缩放因子旨在将点积乘法生成的结果拉低,从而防止这个问题。
Vaswani 等人进一步解释说,他们选择乘法注意力而非Bahdanau 等人(2014)的加法注意力是基于前者的计算效率。
…点积注意力在实践中要快得多且空间效率更高,因为它可以使用高度优化的矩阵乘法代码实现。
– Attention Is All You Need, 2017.
因此,计算缩放点积注意力的逐步过程如下:
- 通过将查询矩阵 Q \mathbf{Q} Q中的查询集合与矩阵 K \mathbf{K} K中的键相乘来计算对齐分数。如果矩阵 Q \mathbf{Q} Q的大小为 m × d k m \times d_k m×dk,而矩阵 K \mathbf{K} K的大小为 n × d k n \times d_k n×dk,则结果矩阵的大小将为 m × n m \times n m×n:
$$
\mathbf{QK}^T =
\begin{bmatrix}
e_{11} & e_{12} & \dots & e_{1n} \
e_{21} & e_{22} & \dots & e_{2n} \
\vdots & \vdots & \ddots & \vdots \
e_{m1} & e_{m2} & \dots & e_{mn} \
\end{bmatrix}
$$
- 将每个对齐分数缩放为 1 d k \tfrac{1}{\sqrt{d_k}} dk1:
$$
\frac{\mathbf{QK}^T}{\sqrt{d_k}} =
\begin{bmatrix}
\tfrac{e_{11}}{\sqrt{d_k}} & \tfrac{e_{12}}{\sqrt{d_k}} & \dots & \tfrac{e_{1n}}{\sqrt{d_k}} \
\tfrac{e_{21}}{\sqrt{d_k}} & \tfrac{e_{22}}{\sqrt{d_k}} & \dots & \tfrac{e_{2n}}{\sqrt{d_k}} \
\vdots & \vdots & \ddots & \vdots \
\tfrac{e_{m1}}{\sqrt{d_k}} & \tfrac{e_{m2}}{\sqrt{d_k}} & \dots & \tfrac{e_{mn}}{\sqrt{d_k}} \
\end{bmatrix}
$$
- 然后通过应用 softmax 操作来进行缩放过程,以获得一组权重:
$$
\text{softmax} \left( \frac{\mathbf{QK}^T}{\sqrt{d_k}} \right) =
\begin{bmatrix}
\text{softmax} ( \tfrac{e_{11}}{\sqrt{d_k}} & \tfrac{e_{12}}{\sqrt{d_k}} & \dots & \tfrac{e_{1n}}{\sqrt{d_k}} ) \
\text{softmax} ( \tfrac{e_{21}}{\sqrt{d_k}} & \tfrac{e_{22}}{\sqrt{d_k}} & \dots & \tfrac{e_{2n}}{\sqrt{d_k}} ) \
\vdots & \vdots & \ddots & \vdots \
\text{softmax} ( \tfrac{e_{m1}}{\sqrt{d_k}} & \tfrac{e_{m2}}{\sqrt{d_k}} & \dots & \tfrac{e_{mn}}{\sqrt{d_k}} ) \
\end{bmatrix}
$$
- 最后,将生成的权重应用于矩阵 V \mathbf{V} V 中的值,大小为 n × d v n \times d_v n×dv:
$$
\begin{aligned}
& \text{softmax} \left( \frac{\mathbf{QK}^T}{\sqrt{d_k}} \right) \cdot \mathbf{V} \
=&
\begin{bmatrix}
\text{softmax} ( \tfrac{e_{11}}{\sqrt{d_k}} & \tfrac{e_{12}}{\sqrt{d_k}} & \dots & \tfrac{e_{1n}}{\sqrt{d_k}} ) \
\text{softmax} ( \tfrac{e_{21}}{\sqrt{d_k}} & \tfrac{e_{22}}{\sqrt{d_k}} & \dots & \tfrac{e_{2n}}{\sqrt{d_k}} ) \
\vdots & \vdots & \ddots & \vdots \
\text{softmax} ( \tfrac{e_{m1}}{\sqrt{d_k}} & \tfrac{e_{m2}}{\sqrt{d_k}} & \dots & \tfrac{e_{mn}}{\sqrt{d_k}} ) \
\end{bmatrix}
\cdot
\begin{bmatrix}
v_{11} & v_{12} & \dots & v_{1d_v} \
v_{21} & v_{22} & \dots & v_{2d_v} \
\vdots & \vdots & \ddots & \vdots \
v_{n1} & v_{n2} & \dots & v_{nd_v} \
\end{bmatrix}
\end{aligned}
$$
多头注意力
在其单个注意力函数基础上,接下来构建了一个多头注意力机制,该函数以矩阵 Q \mathbf{Q} Q、 K \mathbf{K} K 和 V \mathbf{V} V 作为输入,正如您刚刚审查的那样,Vaswani 等人还提出了一个多头注意力机制。
多头注意力机制通过 h h h次线性投影来处理查询、键和值,每次使用不同的学习投影。然后,单个注意力机制并行应用于这 h h h个投影中的每一个,以产生 h h h个输出,然后这些输出被串联并再次投影以产生最终结果。
多头注意力
多头注意力的理念是允许注意力函数从不同的表示子空间中提取信息,这在单个注意力头中是不可能的。
多头注意力功能可以表示如下:
multihead ( Q , K , V ) = concat ( head 1 , … , head h ) W O \text{multihead}(\mathbf{Q}, \mathbf{K}, \mathbf{V}) = \text{concat}(\text{head}_1, \dots, \text{head}_h) \mathbf{W}^O multihead(Q,K,V)=concat(head1,…,headh)WO
在这里,每个 head i \text{head}_i headi, i = 1 , … , h i = 1, \dots, h i=1,…,h,实现了一个由自己的学习投影矩阵特征化的单一注意力函数:
head i = attention ( Q W i Q , K W i K , V W i V ) \text{head}_i = \text{attention}(\mathbf{QW}^Q_i, \mathbf{KW}^K_i, \mathbf{VW}^V_i) headi=attention(QWiQ,KWiK,VWiV)
计算多头注意力的逐步过程如下:
-
通过与各自的权重矩阵 W i Q \mathbf{W}^Q_i WiQ、 W i K \mathbf{W}^K_i WiK和 W i V \mathbf{W}^V_i WiV相乘,计算查询、键和值的线性投影版本,每个 head i \text{head}_i headi一个。
-
对每个头应用单一的注意力函数,步骤包括(1)乘以查询和键矩阵,(2)应用缩放和 softmax 操作,以及(3)加权值矩阵以生成每个头的输出。
-
连接头的输出, head i \text{head}_i headi, i = 1 , … , h i = 1, \dots, h i=1,…,h。
-
通过与权重矩阵 W O \mathbf{W}^O WO相乘,将连接的输出进行线性投影,以生成最终结果。
进一步阅读
本节提供了更多关于该主题的资源,供您深入了解。
书籍
- 《深入学习 Python》,2019 年。
论文
-
《Attention Is All You Need》,2017 年。
-
《通过联合学习对齐和翻译的神经机器翻译》,2014 年。
-
《基于注意力的神经机器翻译的有效方法》,2015 年。
总结
在本教程中,你发现了用于神经机器翻译的 Transformer 注意力机制。
具体来说,你学到了:
-
Transformer 注意力与其前身的区别。
-
Transformer 如何计算缩放点积注意力。
-
Transformer 如何计算多头注意力。
你有任何问题吗?
在下方评论中提出你的问题,我会尽力回答。
Transformer 模型
我们已经熟悉了由 Transformer 注意力机制实现的自注意力概念,用于神经机器翻译。现在我们将把焦点转移到 Transformer 架构的细节上,以探索如何在不依赖于递归和卷积的情况下实现自注意力。
在本教程中,您将了解 Transformer 模型的网络架构。
完成本教程后,您将了解:
-
Transformer 架构如何实现编码器-解码器结构而不依赖于递归和卷积
-
Transformer 编码器和解码器的工作原理
-
Transformer 自注意力与使用递归和卷积层的比较
用我的书Building Transformer Models with Attention来启动您的项目。它提供了具有工作代码的自学教程,指导您构建一个完全工作的 Transformer 模型,能够
将一种语言的句子翻译成另一种语言…
让我们开始吧。
Transformer 模型
照片由Samule Sun拍摄,部分权利保留。
教程概述
本教程分为三个部分;它们是:
-
Transformer 架构
-
编码器
-
解码器
-
-
总结:Transformer 模型
-
与递归和卷积层的比较
先决条件
对于本教程,我们假设您已经熟悉:
Transformer 架构
Transformer 架构遵循编码器-解码器结构,但不依赖于递归和卷积以生成输出。
Transformer 架构的编码器-解码器结构
简而言之,Transformer 架构左半部分的编码器的任务是将输入序列映射到一系列连续的表示,然后输入到解码器中。
解码器位于架构的右半部分,接收来自编码器的输出以及前一个时间步的解码器输出,生成一个输出序列。
在每一步中,模型都是自回归的,生成下一个符号时会消耗先前生成的符号作为额外的输入。
– 注意力机制,2017 年。
编码器
Transformer 架构的编码器块
取自“注意力机制“
编码器由 N N N = 6 个相同的层组成,每个层由两个子层组成:
-
第一个子层实现了多头自注意力机制。你已经看到 多头机制实现了 h h h 个头,每个头接收查询、键和值的(不同的)线性投影版本,每个头并行生成 h h h 个输出,然后用于生成最终结果。
-
第二个子层是一个全连接的前馈网络,由两个线性变换组成,中间有 ReLU 激活:
FFN ( x ) = ReLU ( W 1 x + b 1 ) W 2 + b 2 \text{FFN}(x) = \text{ReLU}(\mathbf{W}_1 x + b_1) \mathbf{W}_2 + b_2 FFN(x)=ReLU(W1x+b1)W2+b2
Transformer 编码器的六层将相同的线性变换应用于输入序列中的所有单词,但每层使用不同的权重( W 1 , W 2 \mathbf{W}_1, \mathbf{W}_2 W1,W2)和偏置( b 1 , b 2 b_1, b_2 b1,b2)参数来实现。
此外,这两个子层都有绕它们的残差连接。
每个子层之后还跟着一个标准化层, layernorm ( . ) \text{layernorm}(.) layernorm(.),它对子层输入 x x x 和子层生成的输出 sublayer ( x ) \text{sublayer}(x) sublayer(x) 之间计算的和进行归一化:
layernorm ( x + sublayer ( x ) ) \text{layernorm}(x + \text{sublayer}(x)) layernorm(x+sublayer(x))
需要注意的一点是,Transformer 架构本质上不能捕获序列中单词之间的相对位置信息,因为它不使用递归。此信息必须通过引入位置编码到输入嵌入中来注入。
位置编码向量的维度与输入嵌入相同,使用不同频率的正弦和余弦函数生成。然后,它们简单地与输入嵌入求和,以注入位置信息。
解码器
Transformer 架构的解码器块
取自“注意力机制“
解码器与编码器有几个相似之处。
解码器也由 N N N = 6 个相同的层组成,每层包含三个子层:
- 第一个子层接收解码器堆栈的先前输出,用位置信息增强,并在其上实现多头自注意力。虽然编码器被设计为无论输入序列中单词的位置如何都能关注,解码器修改为只关注前面的单词。因此,在多头注意力机制中(并行实现多个单注意力函数),通过引入一个掩码来阻止由缩放矩阵 Q \mathbf{Q} Q和 K \mathbf{K} K乘法产生的值。这种屏蔽通过抑制矩阵值来实现,否则这些值将对应于非法连接:
$$
\text{mask}(\mathbf{QK}^T) =
\text{mask} \left( \begin{bmatrix}
e_{11} & e_{12} & \dots & e_{1n} \
e_{21} & e_{22} & \dots & e_{2n} \
\vdots & \vdots & \ddots & \vdots \
e_{m1} & e_{m2} & \dots & e_{mn} \
\end{bmatrix} \right) =
\begin{bmatrix}
e_{11} & -\infty & \dots & -\infty \
e_{21} & e_{22} & \dots & -\infty \
\vdots & \vdots & \ddots & \vdots \
e_{m1} & e_{m2} & \dots & e_{mn} \
\end{bmatrix}
$$
在解码器中的多头注意力机制实现了几个掩码的单注意力功能。
取自“注意力机制是你所需要的”
屏蔽使得解码器单向(不像双向编码器)。
– Python 深度学习进阶,2019 年。
-
第二层实现了一种类似于编码器第一子层中实现的多头自注意力机制。在解码器侧,这个多头机制接收来自前一个解码器子层的查询,并从编码器的输出中获取键和值。这使得解码器能够关注输入序列中的所有单词。
-
第三层实现一个全连接的前馈网络,类似于编码器第二子层中实现的网络。
此外,解码器侧的三个子层周围还有残差连接,并且后接一个标准化层。
位置编码也以与编码器相同的方式添加到解码器的输入嵌入中。
想要开始构建带有注意力的 Transformer 模型吗?
现在获取我的免费 12 天电子邮件速成课程(附带示例代码)。
点击注册并获得免费的课程 PDF 电子书版本。
总结:Transformer 模型
Transformer 模型的运行如下:
-
形成输入序列的每个单词都转换为一个 d model d_{\text{model}} dmodel维嵌入向量。
-
每个表示输入词的嵌入向量通过与相同 d model d_{\text{model}} dmodel 长度的位置信息向量逐元素相加,从而将位置信息引入输入。
-
增强的嵌入向量被输入到包含上述两个子层的编码器块中。由于编码器会关注输入序列中的所有词,无论这些词是否在当前考虑的词之前或之后,因此 Transformer 编码器是双向的。
-
解码器在时间步 t – 1 t – 1 t–1 收到其自身预测的输出词作为输入。
-
解码器的输入也通过与编码器侧相同的方式进行位置编码增强。
-
增强的解码器输入被输入到包含上述三个子层的解码器块中。掩蔽被应用于第一个子层,以防止解码器关注后续词。在第二个子层,解码器还接收到编码器的输出,这使得解码器能够关注输入序列中的所有词。
-
解码器的输出最终经过一个全连接层,然后是一个 softmax 层,以生成对输出序列下一个词的预测。
与递归层和卷积层的比较
Vaswani et al. (2017) 解释了他们放弃使用递归和卷积的动机是基于几个因素:
-
自注意力层在处理较短序列长度时比递归层更快,并且对于非常长的序列长度,可以限制只考虑输入序列中的一个邻域。
-
递归层所需的序列操作数是基于序列长度的,而自注意力层的这个数字保持不变。
-
在卷积神经网络中,卷积核的宽度直接影响输入和输出位置对之间可以建立的长期依赖关系。追踪长期依赖关系需要使用大卷积核或卷积层堆栈,这可能会增加计算成本。
进一步阅读
如果你希望深入了解这个话题,本节提供了更多资源。
书籍
论文
- Attention Is All You Need, 2017。
总结
在本教程中,你了解了 Transformer 模型的网络架构。
具体来说,你学到了:
-
Transformer 架构如何在没有递归和卷积的情况下实现编码器-解码器结构
-
Transformer 编码器和解码器如何工作
-
Transformer 自注意力与递归层和卷积层的比较
你有任何问题吗?
在下方评论中提出你的问题,我会尽力回答。
Keras 中的变压器位置编码层,第二部分
原文:
machinelearningmastery.com/the-transformer-positional-encoding-layer-in-keras-part-2/
在第一部分:变压器模型中位置编码的温和介绍中,我们讨论了变压器模型的位置信息编码层。我们还展示了如何在 Python 中自行实现该层及其功能。在本教程中,你将实现 Keras 和 Tensorflow 中的位置编码层。然后,你可以在完整的变压器模型中使用此层。
完成本教程后,你将了解:
-
Keras 中的文本向量化
-
Keras 中的嵌入层
-
如何子类化嵌入层并编写你自己的位置编码层。
启动你的项目,请参阅我的书籍《构建具有注意力机制的变压器模型》。它提供了自学教程和可运行的代码,帮助你构建一个完全可用的变压器模型。
将句子从一种语言翻译成另一种语言…
让我们开始吧。
Keras 中的变压器位置编码层,第二部分
照片由 Ijaz Rafi 提供。保留部分权利
教程概述
本教程分为三个部分;它们是:
-
Keras 中的文本向量化和嵌入层
-
在 Keras 中编写你自己的位置编码层
-
随机初始化和可调的嵌入
-
来自《Attention Is All You Need》的固定权重嵌入
-
-
位置信息编码层输出的图形视图
导入部分
首先,我们来写一段代码以导入所有必需的库:
import tensorflow as tf
from tensorflow import convert_to_tensor, string
from tensorflow.keras.layers import TextVectorization, Embedding, Layer
from tensorflow.data import Dataset
import numpy as np
import matplotlib.pyplot as plt
文本向量化层
让我们从一组已经预处理和清理过的英文短语开始。文本向量化层创建一个单词字典,并用字典中对应的索引替换每个单词。让我们看看如何使用文本向量化层来映射这两个句子:
-
我是一个机器人
-
你也是机器人
请注意,文本已经被转换为小写,并且所有标点符号和文本中的噪声都已被移除。接下来,将这两个短语转换为固定长度为 5 的向量。Keras 的TextVectorization
层需要一个最大词汇量和初始化时所需的输出序列长度。该层的输出是一个形状为:
(句子数量,输出序列长度)
以下代码片段使用adapt
方法生成词汇表。接下来,它创建文本的向量化表示。
output_sequence_length = 5
vocab_size = 10
sentences = [["I am a robot"], ["you too robot"]]
sentence_data = Dataset.from_tensor_slices(sentences)
# Create the TextVectorization layer
vectorize_layer = TextVectorization(
output_sequence_length=output_sequence_length,
max_tokens=vocab_size)
# Train the layer to create a dictionary
vectorize_layer.adapt(sentence_data)
# Convert all sentences to tensors
word_tensors = convert_to_tensor(sentences, dtype=tf.string)
# Use the word tensors to get vectorized phrases
vectorized_words = vectorize_layer(word_tensors)
print("Vocabulary: ", vectorize_layer.get_vocabulary())
print("Vectorized words: ", vectorized_words)
输出
Vocabulary: ['', '[UNK]', 'robot', 'you', 'too', 'i', 'am', 'a']
Vectorized words: tf.Tensor(
[[5 6 7 2 0]
[3 4 2 0 0]], shape=(2, 5), dtype=int64)
想要开始构建具有注意力机制的变压器模型吗?
现在免费参加我的 12 天电子邮件速成课程(附有示例代码)。
单击注册,还可获得课程的免费 PDF 电子书版本。
嵌入层
Keras 的Embedding
层将整数转换为密集向量。此层将这些整数映射到随机数,后者在训练阶段进行调整。但是,您也可以选择将映射设置为一些预定义的权重值(稍后显示)。要初始化此层,您需要指定要映射的整数的最大值,以及输出序列的长度。
词嵌入
看看这一层是如何将vectorized_text
转换为张量的。
output_length = 6
word_embedding_layer = Embedding(vocab_size, output_length)
embedded_words = word_embedding_layer(vectorized_words)
print(embedded_words)
输出已经用一些注释进行了标注,如下所示。请注意,每次运行此代码时都会看到不同的输出,因为权重已随机初始化。
词嵌入。由于涉及到随机数,每次运行代码时,输出都会有所不同。
位置嵌入
您还需要相应位置的嵌入。最大位置对应于TextVectorization
层的输出序列长度。
position_embedding_layer = Embedding(output_sequence_length, output_length)
position_indices = tf.range(output_sequence_length)
embedded_indices = position_embedding_layer(position_indices)
print(embedded_indices)
输出如下:
位置索引嵌入
变换器中位置编码层的输出
在变换器模型中,最终输出是词嵌入和位置嵌入的总和。因此,当设置这两个嵌入层时,您需要确保output_length
对两者都是相同的。
final_output_embedding = embedded_words + embedded_indices
print("Final output: ", final_output_embedding)
输出如下,带有注释。同样,由于随机权重初始化的原因,这将与您的代码运行结果不同。
添加了词嵌入和位置嵌入后的最终输出
子类化 Keras Embedding 层
当实现变换器模型时,您将不得不编写自己的位置编码层。这相当简单,因为基本功能已为您提供。这个Keras 示例展示了如何子类化Embedding
层以实现自己的功能。您可以根据需要添加更多的方法。
class PositionEmbeddingLayer(Layer):
def __init__(self, sequence_length, vocab_size, output_dim, **kwargs):
super(PositionEmbeddingLayer, self).__init__(**kwargs)
self.word_embedding_layer = Embedding(
input_dim=vocab_size, output_dim=output_dim
)
self.position_embedding_layer = Embedding(
input_dim=sequence_length, output_dim=output_dim
)
def call(self, inputs):
position_indices = tf.range(tf.shape(inputs)[-1])
embedded_words = self.word_embedding_layer(inputs)
embedded_indices = self.position_embedding_layer(position_indices)
return embedded_words + embedded_indices
让我们运行这一层。
my_embedding_layer = PositionEmbeddingLayer(output_sequence_length,
vocab_size, output_length)
embedded_layer_output = my_embedding_layer(vectorized_words)
print("Output from my_embedded_layer: ", embedded_layer_output)
输出
Output from my_embedded_layer: tf.Tensor(
[[[ 0.06798736 -0.02821309 0.00571618 0.00314623 -0.03060734
0.01111387]
[-0.06097465 0.03966043 -0.05164248 0.06578685 0.03638128
-0.03397174]
[ 0.06715029 -0.02453769 0.02205854 0.01110986 0.02345785
0.05879898]
[-0.04625867 0.07500569 -0.05690887 -0.07615659 0.01962536
0.00035865]
[ 0.01423577 -0.03938593 -0.08625181 0.04841495 0.06951572
0.08811047]]
[[ 0.0163899 0.06895607 -0.01131684 0.01810524 -0.05857501
0.01811318]
[ 0.01915303 -0.0163289 -0.04133433 0.06810946 0.03736673
0.04218033]
[ 0.00795418 -0.00143972 -0.01627307 -0.00300788 -0.02759011
0.09251165]
[ 0.0028762 0.04526488 -0.05222676 -0.02007698 0.07879823
0.00541583]
[ 0.01423577 -0.03938593 -0.08625181 0.04841495 0.06951572
0.08811047]]], shape=(2, 5, 6), dtype=float32)
变换器中的位置编码:注意力机制是您所需的
注意,上述类创建了一个具有可训练权重的嵌入层。因此,权重被随机初始化并在训练阶段进行调整。Attention Is All You Need的作者指定了一个位置编码方案,如下所示。你可以在本教程的第一部分中阅读详细信息:\begin{eqnarray}
P(k, 2i) &=& \sin\Big(\frac{k}{n^{2i/d}}\Big)\
P(k, 2i+1) &=& \cos\Big(\frac{k}{n^{2i/d}}\Big)
\end{eqnarray}如果你想使用相同的位置编码方案,你可以指定自己的嵌入矩阵,如第一部分中讨论的那样,该部分展示了如何在 NumPy 中创建自己的嵌入。当指定Embedding
层时,你需要提供位置编码矩阵作为权重,并设置trainable=False
。让我们创建一个新的位置嵌入类来完成这一操作。```py
class PositionEmbeddingFixedWeights(Layer):
def init(self, sequence_length, vocab_size, output_dim, **kwargs):
super(PositionEmbeddingFixedWeights, self).init(**kwargs)
word_embedding_matrix = self.get_position_encoding(vocab_size, output_dim)
position_embedding_matrix = self.get_position_encoding(sequence_length, output_dim)
self.word_embedding_layer = Embedding(
input_dim=vocab_size, output_dim=output_dim,
weights=[word_embedding_matrix],
trainable=False
)
self.position_embedding_layer = Embedding(
input_dim=sequence_length, output_dim=output_dim,
weights=[position_embedding_matrix],
trainable=False
)
def get_position_encoding(self, seq_len, d, n=10000):
P = np.zeros((seq_len, d))
for k in range(seq_len):
for i in np.arange(int(d/2)):
denominator = np.power(n, 2*i/d)
P[k, 2*i] = np.sin(k/denominator)
P[k, 2*i+1] = np.cos(k/denominator)
return P
def call(self, inputs):
position_indices = tf.range(tf.shape(inputs)[-1])
embedded_words = self.word_embedding_layer(inputs)
embedded_indices = self.position_embedding_layer(position_indices)
return embedded_words + embedded_indices
接下来,我们设置一切以运行这一层。
```py
attnisallyouneed_embedding = PositionEmbeddingFixedWeights(output_sequence_length,
vocab_size, output_length)
attnisallyouneed_output = attnisallyouneed_embedding(vectorized_words)
print("Output from my_embedded_layer: ", attnisallyouneed_output)
输出
Output from my_embedded_layer: tf.Tensor(
[[[-0.9589243 1.2836622 0.23000172 1.9731903 0.01077196
1.9999421 ]
[ 0.56205547 1.5004725 0.3213085 1.9603932 0.01508068
1.9999142 ]
[ 1.566284 0.3377554 0.41192317 1.9433732 0.01938933
1.999877 ]
[ 1.0504174 -1.4061394 0.2314966 1.9860148 0.01077211
1.9999698 ]
[-0.7568025 0.3463564 0.18459873 1.982814 0.00861763
1.9999628 ]]
[[ 0.14112 0.0100075 0.1387981 1.9903207 0.00646326
1.9999791 ]
[ 0.08466846 -0.11334133 0.23099795 1.9817369 0.01077207
1.9999605 ]
[ 1.8185948 -0.8322937 0.185397 1.9913884 0.00861771
1.9999814 ]
[ 0.14112 0.0100075 0.1387981 1.9903207 0.00646326
1.9999791 ]
[-0.7568025 0.3463564 0.18459873 1.982814 0.00861763
1.9999628 ]]], shape=(2, 5, 6), dtype=float32)
可视化最终嵌入
为了可视化嵌入,我们将选择两个较大的句子:一个技术性的,另一个只是一个引用。我们将设置TextVectorization
层以及位置编码层,看看最终输出的效果。
technical_phrase = "to understand machine learning algorithms you need" +\
" to understand concepts such as gradient of a function "+\
"Hessians of a matrix and optimization etc"
wise_phrase = "patrick henry said give me liberty or give me death "+\
"when he addressed the second virginia convention in march"
total_vocabulary = 200
sequence_length = 20
final_output_len = 50
phrase_vectorization_layer = TextVectorization(
output_sequence_length=sequence_length,
max_tokens=total_vocabulary)
# Learn the dictionary
phrase_vectorization_layer.adapt([technical_phrase, wise_phrase])
# Convert all sentences to tensors
phrase_tensors = convert_to_tensor([technical_phrase, wise_phrase],
dtype=tf.string)
# Use the word tensors to get vectorized phrases
vectorized_phrases = phrase_vectorization_layer(phrase_tensors)
random_weights_embedding_layer = PositionEmbeddingLayer(sequence_length,
total_vocabulary,
final_output_len)
fixed_weights_embedding_layer = PositionEmbeddingFixedWeights(sequence_length,
total_vocabulary,
final_output_len)
random_embedding = random_weights_embedding_layer(vectorized_phrases)
fixed_embedding = fixed_weights_embedding_layer(vectorized_phrases)
现在让我们看看两个短语的随机嵌入是什么样的。
fig = plt.figure(figsize=(15, 5))
title = ["Tech Phrase", "Wise Phrase"]
for i in range(2):
ax = plt.subplot(1, 2, 1+i)
matrix = tf.reshape(random_embedding[i, :, :], (sequence_length, final_output_len))
cax = ax.matshow(matrix)
plt.gcf().colorbar(cax)
plt.title(title[i], y=1.2)
fig.suptitle("Random Embedding")
plt.show()
随机嵌入
固定权重层的嵌入如下图所示。
fig = plt.figure(figsize=(15, 5))
title = ["Tech Phrase", "Wise Phrase"]
for i in range(2):
ax = plt.subplot(1, 2, 1+i)
matrix = tf.reshape(fixed_embedding[i, :, :], (sequence_length, final_output_len))
cax = ax.matshow(matrix)
plt.gcf().colorbar(cax)
plt.title(title[i], y=1.2)
fig.suptitle("Fixed Weight Embedding from Attention is All You Need")
plt.show()
使用正弦位置编码的嵌入
你可以看到,使用默认参数初始化的嵌入层输出随机值。另一方面,使用正弦波生成的固定权重为每个短语创建了一个独特的签名,其中包含了每个单词位置的信息。
你可以根据具体应用尝试可调或固定权重的实现。
进一步阅读
本节提供了更多资源,如果你想深入了解这个话题。
书籍
- 自然语言处理中的 Transformers 作者:Denis Rothman
论文
- Attention Is All You Need,2017 年
文章
总结
在本教程中,您了解了 Keras 中位置编码层的实现。
具体来说,您学到了:
-
Keras 中的文本向量化层
-
Keras 中的位置编码层
-
创建自己的位置编码类
-
为 Keras 中的位置编码层设置自定义权重
在本文中讨论的位置编码有任何问题吗?在下面的评论中提问,我会尽力回答。
视觉 Transformer 模型
随着 Transformer 架构在自然语言处理领域实现了令人鼓舞的结果,计算机视觉领域的应用也只是时间问题。这最终通过视觉 Transformer(ViT)的实现得以实现。
在本教程中,您将发现视觉 Transformer 模型的架构,以及它在图像分类任务中的应用。
完成本教程后,您将了解:
-
ViT 在图像分类中的工作原理。
-
ViT 的训练过程。
-
ViT 与卷积神经网络在归纳偏置方面的比较。
-
ViT 在不同数据集上与 ResNets 的比较表现如何。
-
ViT 如何在内部处理数据以实现其性能。
启动您的项目,可以参考我的书籍 《构建注意力的 Transformer 模型》。它提供了自学教程和工作代码,帮助您构建一个完全可用的 Transformer 模型。
将句子从一种语言翻译成另一种语言……
让我们开始吧。
视觉 Transformer 模型
图片由 Paul Skorupskas 提供,部分权利保留。
教程概述
本教程分为六个部分,它们是:
-
视觉 Transformer(ViT)简介
-
ViT 架构
-
训练 ViT
-
与卷积神经网络相比的归纳偏置
-
ViT 变体与 ResNets 的比较性能
-
数据的内部表示
前提条件
对于本教程,我们假设您已经熟悉:
视觉 Transformer(ViT)简介
我们已经看到,Vaswani 等人(2017)的 Transformer 架构如何革新了注意力的使用,避免了依赖于递归和卷积的早期注意力模型。在他们的工作中,Vaswani 等人将他们的模型应用于自然语言处理(NLP)的具体问题。
然而,在计算机视觉中,卷积架构仍然占据主导地位……
– 图像胜过 16×16 个词:用于大规模图像识别的 Transformers,2021 年。
受到在自然语言处理中的成功启发,Dosovitskiy 等人(2021 年)试图将标准 Transformer 架构应用于图像,我们很快将看到。他们当时的目标应用是图像分类。
想要开始构建带注意力的 Transformer 模型吗?
现在就参加我的免费 12 天电子邮件速成课程(附有示例代码)。
点击注册,还可获得免费 PDF 电子书版本的课程。
ViT 架构
请记住,标准 Transformer 模型接收一维序列的单词嵌入作为输入,因为它最初是为自然语言处理设计的。相反,当应用于计算机视觉中的图像分类任务时,Transformer 模型的输入数据以二维图像的形式提供。
为了以类似自然语言处理(NLP)领域中单词序列的方式结构化输入图像数据(意味着有一个单词序列的序列),输入图像的高度为 H H H,宽度为 W W W,有 C C C个通道,被切割成更小的二维补丁。这导致产生 N = H W P 2 N = \tfrac{HW}{P²} N=P2HW个补丁,每个补丁的分辨率为( P , P P, P P,P)像素。
在将数据馈送到 Transformer 之前,执行以下操作:
-
每个图像补丁被扁平化为长度为 P 2 × C P² \times C P2×C的向量 x p n \mathbf{x}_p^n xpn,其中 n = 1 , … N n = 1, \dots N n=1,…N。
-
通过可训练的线性投影 E \mathbf{E} E,将扁平化的补丁映射到 D D D维度,生成嵌入的图像补丁序列。
-
一个可学习的类别嵌入 x class \mathbf{x}_{\text{class}} xclass被前置到嵌入图像补丁序列中。 x class \mathbf{x}_{\text{class}} xclass的值代表分类输出 y \mathbf{y} y。
-
最终,补丁嵌入向量最终与一维位置嵌入 E pos \mathbf{E}_{\text{pos}} Epos相结合,从而将位置信息引入输入中,该信息在训练期间也被学习。
由上述操作产生的嵌入向量序列如下:
z 0 = [ x class ; x p 1 E ; … ; x p N E ] + E pos \mathbf{z}_0 = [ \mathbf{x}_{\text{class}}; \; \mathbf{x}_p¹ \mathbf{E}; \; \dots ; \; \mathbf{x}_p^N \mathbf{E}] + \mathbf{E}_{\text{pos}} z0=[xclass;xp1E;…;xpNE]+Epos
Dosovitskiy 等人利用了 Vaswani 等人 Transformer 架构的编码器部分。
为了进行分类,他们在 Transformer 编码器的输入处输入 z 0 \mathbf{z}_0 z0,该编码器由 L L L个相同的层堆叠而成。然后,他们继续从编码器输出的第 L th L^{\text{th}} Lth层取 x class \mathbf{x}_{\text{class}} xclass的值,并将其馈送到分类头部。
在预训练阶段,分类头部由具有一个隐藏层的 MLP 实现,在微调阶段则由单一线性层实现。
– 图像价值 16×16 字:大规模图像识别中的 Transformer, 2021.
形成分类头的多层感知机(MLP)实现了高斯误差线性单元(GELU)非线性。
总结来说,ViT 使用了原始 Transformer 架构的编码器部分。编码器的输入是一个嵌入图像块的序列(包括一个附加到序列前面的可学习类别嵌入),并且还增加了位置位置信息。附加在编码器输出上的分类头接收可学习类别嵌入的值,以生成基于其状态的分类输出。所有这些都在下图中进行了说明:
视觉变换器(ViT)的架构
摘自 “一张图片值 16×16 个词:用于大规模图像识别的变换器”
Dosovitskiy 等人提到的另一个注意事项是,原始图像也可以在传递给 Transformer 编码器之前先输入到卷积神经网络(CNN)中。图像块序列将从 CNN 的特征图中获得,而后续的特征图块嵌入、添加类别标记和增加位置位置信息的过程保持不变。
训练 ViT
ViT 在更大的数据集上进行预训练(如 ImageNet、ImageNet-21k 和 JFT-300M),然后对较少的类别进行微调。
在预训练过程中,附加在编码器输出上的分类头由一个具有一个隐藏层和 GELU 非线性函数的 MLP 实现,如前所述。
在微调过程中,MLP 被替换为一个大小为 D × K D \times K D×K 的单层(零初始化)前馈层,其中 K K K 表示与当前任务对应的类别数量。
微调是在比预训练时使用的图像分辨率更高的图像上进行的,但输入图像被切割成的块大小在训练的所有阶段保持不变。这导致在微调阶段的输入序列长度比预训练阶段使用的更长。
输入序列长度更长的含义是,微调需要比预训练更多的位置嵌入。为了解决这个问题,Dosovitskiy 等人通过在二维上插值预训练位置嵌入,根据它们在原始图像中的位置,得到一个与微调过程中使用的图像块数量相匹配的更长序列。
与卷积神经网络的归纳偏差比较
归纳偏差指的是模型为泛化训练数据和学习目标函数所做的任何假设。
在 CNN 中,本地性、二维邻域结构和平移等变性被嵌入到模型的每一层中。
在卷积神经网络(CNNs)中,每个神经元仅与其邻域内的其他神经元连接。此外,由于同一层上的神经元共享相同的权重和偏置值,当感兴趣的特征落在其感受野内时,这些神经元中的任何一个都会被激活。这导致了一个对特征平移等变的特征图,这意味着如果输入图像被平移,则特征图也会相应平移。
Dosovitskiy 等人认为在 ViT 中,只有 MLP 层具有局部性和平移等变性。另一方面,自注意力层被描述为全局的,因为在这些层上进行的计算并不局限于局部的二维邻域。
他们解释说,对于图像的二维邻域结构的偏置仅在以下情况下使用:
-
在模型输入端,每个图像被切割成补丁,从而固有地保留了每个补丁内像素之间的空间关系。
-
在微调过程中,预训练的位置嵌入根据它们在原始图像中的位置进行二维插值,以生成一个更长的序列,这个序列的长度与微调过程中使用的图像补丁数量相匹配。
ViT 变体与 ResNet 的比较性能
Dosovitskiy 等人将三个逐渐增大的 ViT 模型与两个不同尺寸的修改版 ResNet 进行对比。实验结果得出了几个有趣的发现:
-
实验 1 – 在 ImageNet 上进行微调和测试:
-
当在最小的数据集(ImageNet)上进行预训练时,两个较大的 ViT 模型的表现不如其较小的对应模型。所有 ViT 模型的表现普遍低于 ResNet。
-
当在较大的数据集(ImageNet-21k)上进行预训练时,三个 ViT 模型的表现彼此相似,也与 ResNet 的表现相当。
-
当在最大的数据集(JFT-300M)上进行预训练时,较大 ViT 模型的表现超过了较小 ViT 模型和 ResNet 的表现。
-
-
实验 2 – 在 JFT-300M 数据集的随机子集上进行训练,并在 ImageNet 上进行测试,以进一步调查数据集大小的影响:
-
在数据集的较小子集上,ViT 模型的过拟合程度高于 ResNet 模型,并且表现显著较差。
-
在数据集的较大子集上,较大 ViT 模型的表现超过了 ResNet 模型的表现。
-
这一结果加强了这样的直觉:卷积的归纳偏置对较小的数据集是有用的,但对于较大的数据集,从数据中直接学习相关模式是足够的,甚至是有利的。
数据的内部表示
在分析 ViT 中图像数据的内部表示时,Dosovitskiy 等人发现以下内容:
- 初始应用于 ViT 第一层图像补丁的学习嵌入滤波器,类似于能够提取每个补丁内低级特征的基础功能:
学习嵌入滤波器
摘自“一张图像值 16×16 个词:大规模图像识别的变压器”
- 原始图像中空间接近的图像补丁,其学习的位置嵌入相似:
学习位置嵌入
摘自“一张图像值 16×16 个词:大规模图像识别的变压器”
- 在模型最低层的几个自注意力头部已经关注了大部分图像信息(基于它们的注意力权重),展示了自注意力机制在整合整个图像信息方面的能力:
不同自注意力头部关注的图像区域大小
摘自“一张图像值 16×16 个词:大规模图像识别的变压器”
进一步阅读
如果您希望深入了解该主题,本节提供了更多资源。
论文
-
一张图像值 16×16 个词:大规模图像识别的变压器,2021 年。
-
注意力机制就是你所需要的,2017 年。
摘要
在本教程中,您了解了 Vision Transformer 模型的架构及其在图像分类任务中的应用。
具体而言,您学到了:
-
ViT 在图像分类背景下的工作原理。
-
ViT 的训练过程包括哪些内容。
-
ViT 在归纳偏差方面与卷积神经网络的比较。
-
ViT 在不同数据集上与 ResNets 的对比表现如何。
-
ViT 内部如何处理数据以实现其性能。
您是否有任何问题?
在下方评论区提出您的问题,我会尽力回答。
训练 Transformer 模型
原文:
machinelearningmastery.com/training-the-transformer-model/
我们已经整合了 完整的 Transformer 模型,现在我们准备为神经机器翻译训练它。为此,我们将使用一个包含短英语和德语句子对的训练数据集。在训练过程中,我们还将重新审视掩码在计算准确度和损失指标中的作用。
在本教程中,您将了解如何为神经机器翻译训练 Transformer 模型。
完成本教程后,您将了解:
-
如何准备训练数据集
-
如何将填充蒙版应用于损失和准确度计算
-
如何训练 Transformer 模型
用我的书 使用注意力构建 Transformer 模型 快速启动您的项目。它提供了具有 工作代码 的 自学教程,指导您构建一个完全可用的 Transformer 模型,可以…
将句子从一种语言翻译为另一种语言…
让我们开始吧。
训练 Transformer 模型
图片由 v2osk 拍摄,部分权利保留。
教程概览
本教程分为四部分;它们是:
-
Transformer 架构回顾
-
准备训练数据集
-
将填充蒙版应用于损失和准确度计算
-
训练 Transformer 模型
先决条件
对于本教程,我们假设您已经熟悉:
Transformer 架构回顾
回忆 曾见过 Transformer 架构遵循编码器-解码器结构。编码器位于左侧,负责将输入序列映射为连续表示序列;解码器位于右侧,接收编码器的输出以及前一时间步的解码器输出,生成输出序列。
Transformer 架构的编码器-解码器结构
在生成输出序列时,Transformer 不依赖于循环和卷积。
你已经了解了如何实现完整的 Transformer 模型,现在可以开始训练它进行神经机器翻译。
首先准备数据集以进行训练。
想要开始构建带有注意力机制的 Transformer 模型吗?
立即参加我的免费 12 天邮件速成课程(附示例代码)。
点击注册,还可以获得课程的免费 PDF 电子书版。
准备训练数据集
为此,你可以参考之前的教程,了解如何准备文本数据以用于训练。
你还将使用一个包含短的英语和德语句子对的数据集,你可以在这里下载。这个数据集已经过清理,移除了不可打印的、非字母的字符和标点符号,进一步将所有 Unicode 字符归一化为 ASCII,并将所有大写字母转换为小写字母。因此,你可以跳过清理步骤,这通常是数据准备过程的一部分。然而,如果你使用的数据集没有经过预处理,你可以参考这个教程学习如何处理。
让我们通过创建 PrepareDataset
类来实施以下步骤:
- 从指定的文件名加载数据集。
Python
clean_dataset = load(open(filename, 'rb'))
- 从数据集中选择要使用的句子数量。由于数据集很大,你将减少其大小以限制训练时间。然而,你可以考虑使用完整的数据集作为本教程的扩展。
Python
dataset = clean_dataset[:self.n_sentences, :]
- 在每个句子中附加开始()和结束()标记。例如,英语句子
i like to run
现在变为<START> i like to run <EOS>
。这也适用于其对应的德语翻译ich gehe gerne joggen
,现在变为<START> ich gehe gerne joggen <EOS>
。
Python
for i in range(dataset[:, 0].size):
dataset[i, 0] = "<START> " + dataset[i, 0] + " <EOS>"
dataset[i, 1] = "<START> " + dataset[i, 1] + " <EOS>"
- 随机打乱数据集。
Python
shuffle(dataset)
- 根据预定义的比例拆分打乱的数据集。
Python
train = dataset[:int(self.n_sentences * self.train_split)]
- 创建并训练一个分词器,用于处理将输入编码器的文本序列,并找到最长序列的长度及词汇表大小。
Python
enc_tokenizer = self.create_tokenizer(train[:, 0])
enc_seq_length = self.find_seq_length(train[:, 0])
enc_vocab_size = self.find_vocab_size(enc_tokenizer, train[:, 0])
- 对将输入编码器的文本序列进行分词,通过创建一个词汇表并用相应的词汇索引替换每个词。 和 标记也将成为词汇表的一部分。每个序列也会填充到最大短语长度。
Python
trainX = enc_tokenizer.texts_to_sequences(train[:, 0])
trainX = pad_sequences(trainX, maxlen=enc_seq_length, padding='post')
trainX = convert_to_tensor(trainX, dtype=int64)
- 创建并训练一个分词器,用于处理将输入解码器的文本序列,并找到最长序列的长度及词汇表大小。
Python
dec_tokenizer = self.create_tokenizer(train[:, 1])
dec_seq_length = self.find_seq_length(train[:, 1])
dec_vocab_size = self.find_vocab_size(dec_tokenizer, train[:, 1])
- 对将输入解码器的文本序列进行类似的分词和填充处理。
Python
trainY = dec_tokenizer.texts_to_sequences(train[:, 1])
trainY = pad_sequences(trainY, maxlen=dec_seq_length, padding='post')
trainY = convert_to_tensor(trainY, dtype=int64)
完整的代码清单如下(有关详细信息,请参阅 这个之前的教程):
Python
from pickle import load
from numpy.random import shuffle
from keras.preprocessing.text import Tokenizer
from keras.preprocessing.sequence import pad_sequences
from tensorflow import convert_to_tensor, int64
class PrepareDataset:
def __init__(self, **kwargs):
super(PrepareDataset, self).__init__(**kwargs)
self.n_sentences = 10000 # Number of sentences to include in the dataset
self.train_split = 0.9 # Ratio of the training data split
# Fit a tokenizer
def create_tokenizer(self, dataset):
tokenizer = Tokenizer()
tokenizer.fit_on_texts(dataset)
return tokenizer
def find_seq_length(self, dataset):
return max(len(seq.split()) for seq in dataset)
def find_vocab_size(self, tokenizer, dataset):
tokenizer.fit_on_texts(dataset)
return len(tokenizer.word_index) + 1
def __call__(self, filename, **kwargs):
# Load a clean dataset
clean_dataset = load(open(filename, 'rb'))
# Reduce dataset size
dataset = clean_dataset[:self.n_sentences, :]
# Include start and end of string tokens
for i in range(dataset[:, 0].size):
dataset[i, 0] = "<START> " + dataset[i, 0] + " <EOS>"
dataset[i, 1] = "<START> " + dataset[i, 1] + " <EOS>"
# Random shuffle the dataset
shuffle(dataset)
# Split the dataset
train = dataset[:int(self.n_sentences * self.train_split)]
# Prepare tokenizer for the encoder input
enc_tokenizer = self.create_tokenizer(train[:, 0])
enc_seq_length = self.find_seq_length(train[:, 0])
enc_vocab_size = self.find_vocab_size(enc_tokenizer, train[:, 0])
# Encode and pad the input sequences
trainX = enc_tokenizer.texts_to_sequences(train[:, 0])
trainX = pad_sequences(trainX, maxlen=enc_seq_length, padding='post')
trainX = convert_to_tensor(trainX, dtype=int64)
# Prepare tokenizer for the decoder input
dec_tokenizer = self.create_tokenizer(train[:, 1])
dec_seq_length = self.find_seq_length(train[:, 1])
dec_vocab_size = self.find_vocab_size(dec_tokenizer, train[:, 1])
# Encode and pad the input sequences
trainY = dec_tokenizer.texts_to_sequences(train[:, 1])
trainY = pad_sequences(trainY, maxlen=dec_seq_length, padding='post')
trainY = convert_to_tensor(trainY, dtype=int64)
return trainX, trainY, train, enc_seq_length, dec_seq_length, enc_vocab_size, dec_vocab_size
在开始训练 Transformer 模型之前,我们首先来看一下 PrepareDataset
类对应于训练数据集中第一句话的输出:
Python
# Prepare the training data
dataset = PrepareDataset()
trainX, trainY, train_orig, enc_seq_length, dec_seq_length, enc_vocab_size, dec_vocab_size = dataset('english-german-both.pkl')
print(train_orig[0, 0], '\n', trainX[0, :])
Python
<START> did tom tell you <EOS>
tf.Tensor([ 1 25 4 97 5 2 0], shape=(7,), dtype=int64)
(注意:由于数据集已被随机打乱,你可能会看到不同的输出。)
你可以看到,最初,你有一个三词句子(did tom tell you),然后你添加了开始和结束字符串的标记。接着你对其进行了向量化(你可能会注意到 和 标记分别被分配了词汇表索引 1 和 2)。向量化文本还用零进行了填充,使得最终结果的长度与编码器的最大序列长度匹配:
Python
print('Encoder sequence length:', enc_seq_length)
Python
Encoder sequence length: 7
你可以类似地检查输入到解码器的目标数据:
Python
print(train_orig[0, 1], '\n', trainY[0, :])
Python
<START> hat tom es dir gesagt <EOS>
tf.Tensor([ 1 14 5 7 42 162 2 0 0 0 0 0], shape=(12,), dtype=int64)
在这里,最终结果的长度与解码器的最大序列长度相匹配:
Python
print('Decoder sequence length:', dec_seq_length)
Python
Decoder sequence length: 12
应用填充掩码到损失和准确度计算
回顾 看到在编码器和解码器中使用填充掩码的重要性是为了确保我们刚刚添加到向量化输入中的零值不会与实际输入值一起处理。
这对于训练过程也是适用的,其中需要填充掩码,以确保在计算损失和准确度时,目标数据中的零填充值不被考虑。
让我们首先来看一下损失的计算。
这将使用目标值和预测值之间的稀疏分类交叉熵损失函数进行计算,然后乘以一个填充掩码,以确保只考虑有效的非零值。返回的损失是未掩码值的均值:
Python
def loss_fcn(target, prediction):
# Create mask so that the zero padding values are not included in the computation of loss
padding_mask = math.logical_not(equal(target, 0))
padding_mask = cast(padding_mask, float32)
# Compute a sparse categorical cross-entropy loss on the unmasked values
loss = sparse_categorical_crossentropy(target, prediction, from_logits=True) * padding_mask
# Compute the mean loss over the unmasked values
return reduce_sum(loss) / reduce_sum(padding_mask)
计算准确度时,首先比较预测值和目标值。预测输出是一个大小为 (batch_size, dec_seq_length, dec_vocab_size) 的张量,包含输出中令牌的概率值(由解码器端的 softmax 函数生成)。为了能够与目标值进行比较,只考虑每个具有最高概率值的令牌,并通过操作 argmax(prediction, axis=2)
检索其字典索引。在应用填充掩码后,返回的准确度是未掩码值的均值:
Python
def accuracy_fcn(target, prediction):
# Create mask so that the zero padding values are not included in the computation of accuracy
padding_mask = math.logical_not(math.equal(target, 0))
# Find equal prediction and target values, and apply the padding mask
accuracy = equal(target, argmax(prediction, axis=2))
accuracy = math.logical_and(padding_mask, accuracy)
# Cast the True/False values to 32-bit-precision floating-point numbers
padding_mask = cast(padding_mask, float32)
accuracy = cast(accuracy, float32)
# Compute the mean accuracy over the unmasked values
return reduce_sum(accuracy) / reduce_sum(padding_mask)
训练 Transformer 模型
首先定义模型和训练参数,按照 Vaswani 等人(2017) 的规范:
Python
# Define the model parameters
h = 8 # Number of self-attention heads
d_k = 64 # Dimensionality of the linearly projected queries and keys
d_v = 64 # Dimensionality of the linearly projected values
d_model = 512 # Dimensionality of model layers' outputs
d_ff = 2048 # Dimensionality of the inner fully connected layer
n = 6 # Number of layers in the encoder stack
# Define the training parameters
epochs = 2
batch_size = 64
beta_1 = 0.9
beta_2 = 0.98
epsilon = 1e-9
dropout_rate = 0.1
(注意:只考虑两个时代以限制训练时间。然而,您可以将模型训练更多作为本教程的延伸部分。)
您还需要实现一个学习率调度器,该调度器最初会线性增加前warmup_steps
的学习率,然后按步骤数的倒数平方根比例减少它。Vaswani 等人通过以下公式表示这一点:
KaTeX parse error: Expected 'EOF', got '_' at position 15: \text{learning_̲rate} = \text{d…
Python
class LRScheduler(LearningRateSchedule):
def __init__(self, d_model, warmup_steps=4000, **kwargs):
super(LRScheduler, self).__init__(**kwargs)
self.d_model = cast(d_model, float32)
self.warmup_steps = warmup_steps
def __call__(self, step_num):
# Linearly increasing the learning rate for the first warmup_steps, and decreasing it thereafter
arg1 = step_num ** -0.5
arg2 = step_num * (self.warmup_steps ** -1.5)
return (self.d_model ** -0.5) * math.minimum(arg1, arg2)
随后将LRScheduler
类的一个实例作为 Adam 优化器的learning_rate
参数传递:
Python
optimizer = Adam(LRScheduler(d_model), beta_1, beta_2, epsilon)
接下来,将数据集分割成批次,以准备进行训练:
Python
train_dataset = data.Dataset.from_tensor_slices((trainX, trainY))
train_dataset = train_dataset.batch(batch_size)
这之后是创建一个模型实例:
Python
training_model = TransformerModel(enc_vocab_size, dec_vocab_size, enc_seq_length, dec_seq_length, h, d_k, d_v, d_model, d_ff, n, dropout_rate)
在训练 Transformer 模型时,您将编写自己的训练循环,该循环包含先前实现的损失和精度函数。
在 Tensorflow 2.0 中,默认的运行时是急切执行,这意味着操作立即执行。急切执行简单直观,使得调试更容易。然而,它的缺点是不能利用在图执行中运行代码的全局性能优化。在图执行中,首先构建一个图形,然后才能执行张量计算,这会导致计算开销。因此,对于大模型训练,通常建议使用图执行,而不是对小模型训练使用急切执行更合适。由于 Transformer 模型足够大,建议应用图执行来进行训练。
为了这样做,您将如下使用@function
装饰器:
Python
@function
def train_step(encoder_input, decoder_input, decoder_output):
with GradientTape() as tape:
# Run the forward pass of the model to generate a prediction
prediction = training_model(encoder_input, decoder_input, training=True)
# Compute the training loss
loss = loss_fcn(decoder_output, prediction)
# Compute the training accuracy
accuracy = accuracy_fcn(decoder_output, prediction)
# Retrieve gradients of the trainable variables with respect to the training loss
gradients = tape.gradient(loss, training_model.trainable_weights)
# Update the values of the trainable variables by gradient descent
optimizer.apply_gradients(zip(gradients, training_model.trainable_weights))
train_loss(loss)
train_accuracy(accuracy)
添加了@function
装饰器后,接受张量作为输入的函数将被编译为图形。如果@function
装饰器被注释掉,则该函数将通过急切执行运行。
下一步是实现训练循环,该循环将调用上述的train_step
函数。训练循环将遍历指定数量的时代和数据集批次。对于每个批次,train_step
函数计算训练损失和准确度度量,并应用优化器来更新可训练的模型参数。还包括一个检查点管理器,以便每五个时代保存一个检查点:
Python
train_loss = Mean(name='train_loss')
train_accuracy = Mean(name='train_accuracy')
# Create a checkpoint object and manager to manage multiple checkpoints
ckpt = train.Checkpoint(model=training_model, optimizer=optimizer)
ckpt_manager = train.CheckpointManager(ckpt, "./checkpoints", max_to_keep=3)
for epoch in range(epochs):
train_loss.reset_states()
train_accuracy.reset_states()
print("\nStart of epoch %d" % (epoch + 1))
# Iterate over the dataset batches
for step, (train_batchX, train_batchY) in enumerate(train_dataset):
# Define the encoder and decoder inputs, and the decoder output
encoder_input = train_batchX[:, 1:]
decoder_input = train_batchY[:, :-1]
decoder_output = train_batchY[:, 1:]
train_step(encoder_input, decoder_input, decoder_output)
if step % 50 == 0:
print(f'Epoch {epoch + 1} Step {step} Loss {train_loss.result():.4f} Accuracy {train_accuracy.result():.4f}')
# Print epoch number and loss value at the end of every epoch
print("Epoch %d: Training Loss %.4f, Training Accuracy %.4f" % (epoch + 1, train_loss.result(), train_accuracy.result()))
# Save a checkpoint after every five epochs
if (epoch + 1) % 5 == 0:
save_path = ckpt_manager.save()
print("Saved checkpoint at epoch %d" % (epoch + 1))
需要记住的一个重要点是,解码器的输入相对于编码器输入向右偏移一个位置。这种偏移的背后思想,与解码器的第一个多头注意力块中的前瞻遮罩结合使用,是为了确保当前令牌的预测仅依赖于先前的令牌。
这种掩码,结合输出嵌入偏移一个位置的事实,确保了位置 i 的预测只能依赖于位置小于 i 的已知输出。
– Attention Is All You Need,2017。
正因如此,编码器和解码器输入是以以下方式输入到 Transformer 模型中的:
encoder_input = train_batchX[:, 1:]
decoder_input = train_batchY[:, :-1]
汇总完整的代码列表如下:
Python
from tensorflow.keras.optimizers import Adam
from tensorflow.keras.optimizers.schedules import LearningRateSchedule
from tensorflow.keras.metrics import Mean
from tensorflow import data, train, math, reduce_sum, cast, equal, argmax, float32, GradientTape, TensorSpec, function, int64
from keras.losses import sparse_categorical_crossentropy
from model import TransformerModel
from prepare_dataset import PrepareDataset
from time import time
# Define the model parameters
h = 8 # Number of self-attention heads
d_k = 64 # Dimensionality of the linearly projected queries and keys
d_v = 64 # Dimensionality of the linearly projected values
d_model = 512 # Dimensionality of model layers' outputs
d_ff = 2048 # Dimensionality of the inner fully connected layer
n = 6 # Number of layers in the encoder stack
# Define the training parameters
epochs = 2
batch_size = 64
beta_1 = 0.9
beta_2 = 0.98
epsilon = 1e-9
dropout_rate = 0.1
# Implementing a learning rate scheduler
class LRScheduler(LearningRateSchedule):
def __init__(self, d_model, warmup_steps=4000, **kwargs):
super(LRScheduler, self).__init__(**kwargs)
self.d_model = cast(d_model, float32)
self.warmup_steps = warmup_steps
def __call__(self, step_num):
# Linearly increasing the learning rate for the first warmup_steps, and decreasing it thereafter
arg1 = step_num ** -0.5
arg2 = step_num * (self.warmup_steps ** -1.5)
return (self.d_model ** -0.5) * math.minimum(arg1, arg2)
# Instantiate an Adam optimizer
optimizer = Adam(LRScheduler(d_model), beta_1, beta_2, epsilon)
# Prepare the training and test splits of the dataset
dataset = PrepareDataset()
trainX, trainY, train_orig, enc_seq_length, dec_seq_length, enc_vocab_size, dec_vocab_size = dataset('english-german-both.pkl')
# Prepare the dataset batches
train_dataset = data.Dataset.from_tensor_slices((trainX, trainY))
train_dataset = train_dataset.batch(batch_size)
# Create model
training_model = TransformerModel(enc_vocab_size, dec_vocab_size, enc_seq_length, dec_seq_length, h, d_k, d_v, d_model, d_ff, n, dropout_rate)
# Defining the loss function
def loss_fcn(target, prediction):
# Create mask so that the zero padding values are not included in the computation of loss
padding_mask = math.logical_not(equal(target, 0))
padding_mask = cast(padding_mask, float32)
# Compute a sparse categorical cross-entropy loss on the unmasked values
loss = sparse_categorical_crossentropy(target, prediction, from_logits=True) * padding_mask
# Compute the mean loss over the unmasked values
return reduce_sum(loss) / reduce_sum(padding_mask)
# Defining the accuracy function
def accuracy_fcn(target, prediction):
# Create mask so that the zero padding values are not included in the computation of accuracy
padding_mask = math.logical_not(equal(target, 0))
# Find equal prediction and target values, and apply the padding mask
accuracy = equal(target, argmax(prediction, axis=2))
accuracy = math.logical_and(padding_mask, accuracy)
# Cast the True/False values to 32-bit-precision floating-point numbers
padding_mask = cast(padding_mask, float32)
accuracy = cast(accuracy, float32)
# Compute the mean accuracy over the unmasked values
return reduce_sum(accuracy) / reduce_sum(padding_mask)
# Include metrics monitoring
train_loss = Mean(name='train_loss')
train_accuracy = Mean(name='train_accuracy')
# Create a checkpoint object and manager to manage multiple checkpoints
ckpt = train.Checkpoint(model=training_model, optimizer=optimizer)
ckpt_manager = train.CheckpointManager(ckpt, "./checkpoints", max_to_keep=3)
# Speeding up the training process
@function
def train_step(encoder_input, decoder_input, decoder_output):
with GradientTape() as tape:
# Run the forward pass of the model to generate a prediction
prediction = training_model(encoder_input, decoder_input, training=True)
# Compute the training loss
loss = loss_fcn(decoder_output, prediction)
# Compute the training accuracy
accuracy = accuracy_fcn(decoder_output, prediction)
# Retrieve gradients of the trainable variables with respect to the training loss
gradients = tape.gradient(loss, training_model.trainable_weights)
# Update the values of the trainable variables by gradient descent
optimizer.apply_gradients(zip(gradients, training_model.trainable_weights))
train_loss(loss)
train_accuracy(accuracy)
for epoch in range(epochs):
train_loss.reset_states()
train_accuracy.reset_states()
print("\nStart of epoch %d" % (epoch + 1))
start_time = time()
# Iterate over the dataset batches
for step, (train_batchX, train_batchY) in enumerate(train_dataset):
# Define the encoder and decoder inputs, and the decoder output
encoder_input = train_batchX[:, 1:]
decoder_input = train_batchY[:, :-1]
decoder_output = train_batchY[:, 1:]
train_step(encoder_input, decoder_input, decoder_output)
if step % 50 == 0:
print(f'Epoch {epoch + 1} Step {step} Loss {train_loss.result():.4f} Accuracy {train_accuracy.result():.4f}')
# print("Samples so far: %s" % ((step + 1) * batch_size))
# Print epoch number and loss value at the end of every epoch
print("Epoch %d: Training Loss %.4f, Training Accuracy %.4f" % (epoch + 1, train_loss.result(), train_accuracy.result()))
# Save a checkpoint after every five epochs
if (epoch + 1) % 5 == 0:
save_path = ckpt_manager.save()
print("Saved checkpoint at epoch %d" % (epoch + 1))
print("Total time taken: %.2fs" % (time() - start_time))
运行代码会产生类似于以下的输出(你可能会看到不同的损失和准确率值,因为训练是从头开始的,而训练时间取决于你用于训练的计算资源):
Python
Start of epoch 1
Epoch 1 Step 0 Loss 8.4525 Accuracy 0.0000
Epoch 1 Step 50 Loss 7.6768 Accuracy 0.1234
Epoch 1 Step 100 Loss 7.0360 Accuracy 0.1713
Epoch 1: Training Loss 6.7109, Training Accuracy 0.1924
Start of epoch 2
Epoch 2 Step 0 Loss 5.7323 Accuracy 0.2628
Epoch 2 Step 50 Loss 5.4360 Accuracy 0.2756
Epoch 2 Step 100 Loss 5.2638 Accuracy 0.2839
Epoch 2: Training Loss 5.1468, Training Accuracy 0.2908
Total time taken: 87.98s
在仅使用 CPU 的相同平台上,仅使用即时执行需要 155.13 秒来运行代码,这显示了使用图执行的好处。
进一步阅读
本节提供了更多关于此主题的资源,如果你希望更深入地了解。
书籍
论文
网站
- 从头开始在 Keras 中编写训练循环:
keras.io/guides/writing_a_training_loop_from_scratch/
总结
在本教程中,你了解了如何训练 Transformer 模型进行神经机器翻译。
具体来说,你学到了:
-
如何准备训练数据集
-
如何将填充掩码应用于损失和准确率计算
-
如何训练 Transformer 模型
你有任何问题吗?
在下方评论中提出你的问题,我将尽力回答。