上一篇文章中比较了 卷积神经网络(CNN) 、循环神经网络(RNN) 和 自注意力(self-attention) 。值得注意的是,自注意力同时具有并行计算和最短的最大路径长度这两个优势。因此,使用自注意力来设计深度架构是很有吸引力的。对比之前仍然依赖循环神经网络实现输入表示的自注意力模型,Transformer模型完全基于注意力机制,没有任何卷积层或循环神经网络层。尽管Transformer最初是应用于在文本数据上的序列到序列学习,但现在已经推广到各种现代的深度学习中,例如语言、视觉、语音和强化学习领域。
一、模型
Transformer作为编码器-解码器架构的一个实例,其整体架构图在图1中展示。正如所见到的,Transformer是由编码器和解码器组成的。与基于Bahdanau注意力实现的序列到序列的学习相比,Transformer的编码器和解码器是基于自注意力的模块叠加而成的,源(输入)序列和目标(输出)序列的嵌入(embedding)表示将加上位置编码(positional encoding),再分别输入到编码器和解码器中。
图1 transformer架构
上图概述了Transformer的架构。从宏观角度来看,Transformer的编码器是由多个相同的层叠加而成的,每个层都有两个子层(sublayer)。第一个子层是多头自注意力(multi-head self-attention)汇聚;第二个子层是基于位置的前馈网络(positionwise feed-forward network)。具体来说,在计算编码器的自注意力时,查询、键和值都来自前一个编码器层的输出。受残差网络的启发,每个子层都采用了残差连接(residual connection)。在Transformer中,对于序列中任何位置的任何输入 <math xmlns="http://www.w3.org/1998/Math/MathML"> x ∈ R d \mathbf{x} \in \mathbb{R}^d </math>x∈Rd,都要求满足 <math xmlns="http://www.w3.org/1998/Math/MathML"> s u b l a y e r ( x ) ∈ R d sublayer(\mathbf{x}) \in \mathbb{R}^d </math>sublayer(x)∈Rd,以便残差连接满足 <math xmlns="http://www.w3.org/1998/Math/MathML"> x + s u b l a y e r ( x ) ∈ R d \mathbf{x} + sublayer(\mathbf{x}) \in \mathbb{R}^d </math>x+sublayer(x)∈Rd。在残差连接的加法计算之后,紧接着应用层规范化(layer normalization)。因此,输入序列对应的每个位置,Transformer编码器都将输出一个 <math xmlns="http://www.w3.org/1998/Math/MathML"> d d </math>d 维表示向量。
Transformer解码器也是由多个相同的层叠加而成的,并且层中使用了残差连接和层规范化。除了编码器中描述的两个子层之外,解码器还在这两个子层之间插入了第三个子层,称为编码器-解码器注意力(encoder-decoder attention)层。在编码器-解码器注意力中,查询来自前一个解码器层的输出,而键和值来自整个编码器的输出。在解码器自注意力中,查询、键和值都来自上一个解码器层的输出。但是,解码器中的每个位置只能考虑该位置之前的所有位置。这种掩蔽(masked)注意力保留了自回归(auto-regressive)属性,确保预测仅依赖于已生成的输出词元。
二、基于位置的前馈网络
基于位置的前馈网络对序列中的所有位置的表示进行变换时使用的是同一个多层感知机(MLP),这就是称前馈网络是基于位置的(positionwise)的原因。在下面的实现中,输入X的形状(批量大小,时间步数或序列长度,隐单元数或特征维度)将被一个两层的感知机转换成形状为(批量大小,时间步数,ffn_num_outputs)的输出张量。
python
class PositionWiseFFN(nn.Module):
"""基于位置的前馈网络(Position-wise Feed-Forward Network, FFN)
该网络用于 Transformer 结构中的每个位置的前馈计算,不同于传统的全连接网络,
它对序列的每个时间步独立处理,不共享参数。通常由两个线性变换和一个 ReLU 激活函数组成。
"""
def __init__(self, ffn_num_input, ffn_num_hiddens, ffn_num_outputs, **kwargs):
"""
初始化前馈网络层。
参数:
ffn_num_input (int): 输入特征的维度。
ffn_num_hiddens (int): 隐藏层的特征维度。
ffn_num_outputs (int): 输出特征的维度。
"""
super().__init__(**kwargs)
self.dense1 = nn.Linear(ffn_num_input, ffn_num_hiddens)
self.relu = nn.ReLU()
self.dense2 = nn.Linear(ffn_num_hiddens, ffn_num_outputs)
def forward(self, X):
"""
前向传播计算。
参数:
X (Tensor): 输入张量,形状通常为 (batch_size, seq_len, ffn_num_input)。
返回:
Tensor: 输出张量,形状为 (batch_size, seq_len, ffn_num_outputs)。
"""
return self.dense2(self.relu(self.dense1(X)))
下面的例子显示,改变张量的最里层维度的尺寸,会改变成基于位置的前馈网络的输出尺寸。因为用同一个多层感知机对所有位置上的输入进行变换,所以当所有这些位置的输入相同时,它们的输出也是相同的。
python
import torch
from torch import nn
import d2l
ffn = d2l.PositionWiseFFN(4, 4, 8)
ffn.eval()
intput = torch.ones((2, 3, 4))
output = ffn(intput)
print(output.shape) # 输出:torch.Size([2, 3, 8])
print(output[0])
输出:
ini
tensor([[-0.2482, 0.1705, -0.8920, -0.3207, -0.0318, 0.4535, -0.1754, -0.2317],
[-0.2482, 0.1705, -0.8920, -0.3207, -0.0318, 0.4535, -0.1754, -0.2317],
[-0.2482, 0.1705, -0.8920, -0.3207, -0.0318, 0.4535, -0.1754, -0.2317]],
grad_fn=<SelectBackward0>)
三、残差连接和层规范化
现在让我们关注图1中的加法和规范化(add&norm)组件。正如在本文开头所述,这是由残差连接和紧随其后的层规范化组成的。两者都是构建有效的深度架构的关键。
现代卷积神经网络5-理解批量规范化:让深度学习更高效的技术这篇文章中解释了在一个小批量的样本内基于批量规范化对数据进行重新中心化和重新缩放的调整。层规范化和批量规范化的目标相同,但层规范化是基于特征维度进行规范化。尽管批量规范化在计算机视觉中被广泛应用,但在自然语言处理任务中(输入通常是变长序列)批量规范化通常不如层规范化的效果好。
以下代码对比不同维度的层规范化和批量规范化的效果。
python
ln = nn.LayerNorm(2)
bn = nn.BatchNorm1d(2)
X = torch.tensor([[1, 2], [2, 3]], dtype=torch.float32)
# 在训练模式下计算X的均值和方差
print('layer norm:', ln(X), '\nbatch norm:', bn(X))
输出:
lua
layer norm: tensor([[-1.0000, 1.0000],
[-1.0000, 1.0000]], grad_fn=<NativeLayerNormBackward0>)
batch norm: tensor([[-1.0000, -1.0000],
[ 1.0000, 1.0000]], grad_fn=<NativeBatchNormBackward0>)
层规范化(LN) | 批量规范化(BN) | |
---|---|---|
归一化维度 | 在特征维度上(对每个样本独立计算均值和方差) | 在批次维度上(对整个 batch 的相同特征计算均值和方差) |
适用场景 | 适用于 RNN(变长序列)、Transformer(注意力机制)等 | 适用于 CNN(图像)、MLP(全连接网络)等 |
训练 vs 推理 | 训练和推理模式一致 | 训练时计算 batch 统计量,推理时使用移动平均 |
现在可以使用残差连接和层规范化来实现AddNorm类。暂退法也被作为正则化方法使用。
python
class AddNorm(nn.Module):
"""残差连接后进行层规范化(Add & Norm)"""
def __init__(self, normalized_shape, dropout, **kwargs):
"""
初始化 AddNorm 模块。
参数:
normalized_shape (int or tuple): 需要进行层规范化的特征维度。
dropout (float): Dropout 的丢弃率,用于防止过拟合。
"""
super().__init__(**kwargs)
self.dropout = nn.Dropout(dropout) # 施加 Dropout,防止过拟合
self.ln = nn.LayerNorm(normalized_shape) # 进行层规范化
def forward(self, X, Y):
"""
前向传播:执行残差连接(X + dropout(Y))并进行层规范化。
参数:
X (Tensor): 残差连接的原始输入。
Y (Tensor): 需要施加 Dropout 并参与残差连接的输出。
返回:
Tensor: 经过残差连接和层规范化后的结果。
"""
return self.ln(X + self.dropout(Y))
残差连接要求两个输入的形状相同,以便加法操作后输出张量的形状相同。
python
add_norm = d2l.AddNorm(normalized_shape=[3, 4], dropout=0.5)
add_norm.eval()
X = torch.ones((2, 3, 4))
Y = torch.ones((2, 3, 4))
print(add_norm(X, Y).shape) # 输出:torch.Size([2, 3, 4])
normalized_shape
指定了需要计算均值和方差的维度,这些维度上的数据会被归一化,使其均值接近 0,标准差接近 1。- 归一化是针对最后几个维度进行的,这些维度的大小必须与
normalized_shape
匹配。
四、编码器
有了组成Transformer编码器的基础组件,现在可以先实现编码器中的一个层。下面的EncoderBlock
类包含两个子层:多头自注意力 和基于位置的前馈网络,这两个子层都使用了残差连接和紧随的层规范化。
python
class EncoderBlock(nn.Module):
"""Transformer 编码器块
该模块包括多头自注意力机制、残差连接、层归一化以及前馈神经网络。
"""
def __init__(self, key_size, query_size, value_size, num_hiddens,
norm_shape, ffn_num_input, ffn_num_hiddens, num_heads,
dropout, use_bias=False, **kwargs):
"""
参数:
key_size: int,键的维度
query_size: int,查询的维度
value_size: int,值的维度
num_hiddens: int,隐藏层维度
norm_shape: tuple,层归一化的形状
ffn_num_input: int,前馈神经网络的输入维度
ffn_num_hiddens: int,前馈神经网络的隐藏层维度
num_heads: int,多头注意力的头数
dropout: float,Dropout 概率
use_bias: bool,是否在注意力计算时使用偏置
"""
super().__init__(**kwargs)
# 多头自注意力机制
self.attention = MultiHeadAttention(key_size, query_size, value_size, num_hiddens,
num_heads, dropout, use_bias)
# 第一层 AddNorm(用于多头注意力后的归一化)
self.addnorm1 = AddNorm(norm_shape, dropout)
# 前馈神经网络(FFN)
self.ffn = PositionWiseFFN(ffn_num_input, ffn_num_hiddens, num_hiddens)
# 第二层 AddNorm(用于 FFN 之后的归一化)
self.addnorm2 = AddNorm(norm_shape, dropout)
def forward(self, X, valid_lens):
"""
前向传播过程:
1. 进行多头自注意力计算,并通过 AddNorm 归一化
2. 经过前馈神经网络,并通过 AddNorm 归一化
参数:
X: 张量,输入序列,形状为 (batch_size, num_tokens, num_hiddens)
valid_lens: 张量,掩码长度,防止模型关注填充部分
返回:
归一化后的输出张量,形状与 X 相同
"""
# 多头自注意力 + 残差连接 + 归一化
Y = self.addnorm1(X, self.attention(X, X, X, valid_lens))
# 前馈神经网络 + 残差连接 + 归一化
return self.addnorm2(Y, self.ffn(Y))
正如从代码中所看到的,Transformer编码器中的任何层都不会改变其输入的形状。
python
X = torch.ones((2, 100, 24))
valid_lens = torch.tensor([3, 2])
encoder_blk = d2l.EncoderBlock(key_size=24, query_size=24, value_size=24, num_hiddens=24,
norm_shape=[100, 24], ffn_num_input=24, ffn_num_hiddens=48,
num_heads=8, dropout=0.5)
encoder_blk.eval()
print(encoder_blk(X, valid_lens).shape) # 输出:torch.Size([2, 100, 24])
下面实现的Transformer编码器的代码中,堆叠了num_layers
个EncoderBlock
类的实例。由于这里使用的是值范围在 <math xmlns="http://www.w3.org/1998/Math/MathML"> − 1 -1 </math>−1和 <math xmlns="http://www.w3.org/1998/Math/MathML"> 1 1 </math>1之间的固定位置编码,因此通过学习得到的输入的嵌入表示的值需要先乘以嵌入维度的平方根进行重新缩放,然后再与位置编码相加。
python
class TransformerEncoder(Encoder):
"""Transformer 编码器
该编码器由嵌入层(Embedding)、位置编码(Positional Encoding)、
多个 Transformer 编码器块(EncoderBlock)组成。
"""
def __init__(self, vocab_size, key_size, query_size, value_size,
num_hiddens, norm_shape, ffn_num_input, ffn_num_hiddens,
num_heads, num_layers, dropout, use_bias=False, **kwargs):
"""
参数:
vocab_size: int,词汇表大小
key_size: int,键(key)的维度
query_size: int,查询(query)的维度
value_size: int,值(value)的维度
num_hiddens: int,隐藏单元的维度
norm_shape: tuple,层规范化的形状
ffn_num_input: int,前馈神经网络的输入维度
ffn_num_hiddens: int,前馈神经网络的隐藏层维度
num_heads: int,多头注意力的头数
num_layers: int,编码器块的层数
dropout: float,Dropout 概率
use_bias: bool,是否在注意力计算中使用偏置
"""
super(TransformerEncoder, self).__init__(**kwargs)
self.num_hiddens = num_hiddens
# 词嵌入层:将输入索引映射到指定维度的嵌入向量
self.embedding = nn.Embedding(vocab_size, num_hiddens)
# 位置编码:引入位置信息,使模型能够识别序列顺序
self.pos_encoding = PositionalEncoding(num_hiddens, dropout)
# 由多个 Transformer 编码器块组成的序列
self.blks = nn.Sequential()
for i in range(num_layers):
self.blks.add_module(
'block' + str(i),
EncoderBlock(key_size, query_size, value_size, num_hiddens,
norm_shape, ffn_num_input, ffn_num_hiddens,
num_heads, dropout, use_bias)
)
def forward(self, X, valid_lens, *args):
"""
前向传播:
1. 计算嵌入表示,并进行缩放
2. 加入位置编码,使模型具有顺序信息
3. 通过多个 Transformer 编码器块处理输入
4. 记录每层的注意力权重
参数:
X: 张量,输入序列,形状为 (batch_size, num_tokens)
valid_lens: 张量,有效序列长度,用于掩蔽填充部分
返回:
处理后的张量,形状为 (batch_size, num_tokens, num_hiddens)
"""
# 计算词嵌入,并进行缩放,使其数值范围更适合位置编码
# 由于位置编码的值在 [-1, 1] 之间,因此嵌入乘以 sqrt(num_hiddens)
X = self.pos_encoding(self.embedding(X) * math.sqrt(self.num_hiddens))
self.attention_weights = [None] * len(self.blks)
for i, blk in enumerate(self.blks):
X = blk(X, valid_lens)
self.attention_weights[
i] = blk.attention.attention.attention_weights
return X
下面我们指定了超参数来创建一个两层的Transformer编码器。Transformer编码器输出的形状是(批量大小,时间步数目,num_hiddens)
。
python
encoder = d2l.TransformerEncoder(vocab_size=200, key_size=24, query_size=24, value_size=24,
num_hiddens=24, norm_shape=[100, 24], ffn_num_input=24, ffn_num_hiddens=48,
num_heads=8, num_layers=2, dropout=0.5)
encoder.eval()
valid_lens = torch.tensor([3, 2])
print(encoder(torch.ones((2, 100), dtype=torch.long), valid_lens).shape)
# 输出:torch.Size([2, 100, 24])
五、解码器
如图1所示,Transformer解码器也是由多个相同的层组成。在DecoderBlock
类中实现的每个层包含了三个子层:解码器自注意力 、"编码器-解码器"注意力 和基于位置的前馈网络。这些子层也都被残差连接和紧随的层规范化围绕。
正如在本文前面所述,在掩蔽多头解码器自注意力层(第一个子层)中,查询、键和值都来自上一个解码器层的输出。关于序列到序列模型(sequence-to-sequence model),在训练阶段,其输出序列的所有位置(时间步)的词元都是已知的;然而,在预测阶段,其输出序列的词元是逐个生成的。因此,在任何解码器时间步中,只有生成的词元才能用于解码器的自注意力计算中。为了在解码器中保留自回归的属性,其掩蔽自注意力设定了参数dec_valid_lens
,以便任何查询都只会与解码器中所有已经生成词元的位置(即直到该查询位置为止)进行注意力计算。
python
class DecoderBlock(nn.Module):
"""解码器中第 i 个块:包括两次多头注意力和一层前馈神经网络"""
def __init__(self, key_size, query_size, value_size, num_hiddens,
norm_shape, ffn_num_input, ffn_num_hiddens, num_heads,
dropout, i, **kwargs):
"""
初始化解码器块
参数:
key_size, query_size, value_size: 注意力机制中的键、查询和值的维度
num_hiddens: 多头注意力和前馈网络的输出维度
norm_shape: 层归一化的输入形状
ffn_num_input: 前馈神经网络的输入维度
ffn_num_hiddens: 前馈神经网络的隐藏层维度
num_heads: 注意力头的数量
dropout: Dropout 概率
i: 解码器块的索引(用于状态的存取)
"""
super(DecoderBlock, self).__init__(**kwargs)
self.i = i
# 第一个多头注意力:对解码器输入做掩蔽自注意力(masked self-attention)
self.attention1 = MultiHeadAttention(
key_size, query_size, value_size, num_hiddens, num_heads, dropout)
self.addnorm1 = AddNorm(norm_shape, dropout)
# 第二个多头注意力:对编码器输出做注意力(encoder-decoder attention)
self.attention2 = MultiHeadAttention(
key_size, query_size, value_size, num_hiddens, num_heads, dropout)
self.addnorm2 = AddNorm(norm_shape, dropout)
# 前馈网络部分
self.ffn = PositionWiseFFN(ffn_num_input, ffn_num_hiddens, num_hiddens)
self.addnorm3 = AddNorm(norm_shape, dropout)
def forward(self, X, state):
"""
前向传播
参数:
X: 当前解码器的输入,形状 (batch_size, tgt_seq_len, num_hiddens)
state:
state[0]: 编码器输出,用作第二个注意力层的键和值
state[1]: 编码器的有效长度,用于掩蔽 padding
state[2]: 解码器中每个块的缓存 key/value,用于预测时自回归推理
返回:
输出 (batch_size, tgt_seq_len, num_hiddens),以及更新后的 state
"""
enc_outputs, enc_valid_lens = state[0], state[1]
# key_values 缓存自注意力历史信息(用于预测阶段)
if state[2][self.i] is None:
key_values = X # 初始化:当前时间步的输入即为 key 和 value
else:
# 累积历史信息:拼接当前时间步输入和已有 key_values
key_values = torch.cat((state[2][self.i], X), axis=1)
state[2][self.i] = key_values # 更新缓存
if self.training:
# 训练阶段:构造解码器注意力掩码(避免未来信息泄露)
batch_size, num_steps, _ = X.shape
dec_valid_lens = torch.arange(
1, num_steps + 1, device=X.device).repeat(batch_size, 1)
else:
# 推理阶段:逐步解码,因此无需掩蔽
dec_valid_lens = None
# 第一个多头注意力:对解码器当前输入进行掩蔽自注意力
X2 = self.attention1(X, key_values, key_values, dec_valid_lens)
Y = self.addnorm1(X, X2) # 残差连接 + 层归一化
# 第二个多头注意力:解码器对编码器输出进行注意力
Y2 = self.attention2(Y, enc_outputs, enc_outputs, enc_valid_lens)
Z = self.addnorm2(Y, Y2) # 残差连接 + 层归一化
# 前馈网络处理 + 残差连接 + 层归一化
return self.addnorm3(Z, self.ffn(Z)), state
为了便于在"编码器-解码器"注意力中进行缩放点积计算和残差连接中进行加法计算,编码器和解码器的特征维度都是num_hiddens
。
python
decoder_blk = d2l.DecoderBlock(24, 24, 24, 24, [100, 24], 24, 48, 8, 0.5, 0)
decoder_blk.eval()
X = torch.ones((2, 100, 24))
state = [encoder_blk(X, valid_lens), valid_lens, [None]]
print(decoder_blk(X, state)[0].shape) # 输出:torch.Size([2, 100, 24])
现在我们构建了由num_layers
个DecoderBlock
实例组成的完整的Transformer解码器。最后,通过一个全连接层计算所有vocab_size
个可能的输出词元的预测值。解码器的自注意力权重和编码器解码器注意力权重都被存储下来,方便日后可视化的需要。
python
class TransformerDecoder(AttentionDecoder):
"""Transformer 解码器"""
def __init__(self, vocab_size, key_size, query_size, value_size,
num_hiddens, norm_shape, ffn_num_input, ffn_num_hiddens,
num_heads, num_layers, dropout, **kwargs):
"""
初始化 Transformer 解码器
参数说明:
vocab_size: 词表大小
key_size, query_size, value_size: 多头注意力中键、查询、值的维度
num_hiddens: 隐藏单元数,也是词嵌入和位置编码的维度
norm_shape: 层归一化的输入形状
ffn_num_input: 前馈神经网络输入维度
ffn_num_hiddens: 前馈神经网络隐藏层维度
num_heads: 注意力头数
num_layers: 解码器块的数量
dropout: dropout 比例
"""
super(TransformerDecoder, self).__init__(**kwargs)
self.num_hiddens = num_hiddens
self.num_layers = num_layers
# 词嵌入层,将输入 token 转换为向量
self.embedding = nn.Embedding(vocab_size, num_hiddens)
# 位置编码(带 dropout),加入序列位置信息
self.pos_encoding = PositionalEncoding(num_hiddens, dropout)
# 多个解码器块堆叠(每个块是 DecoderBlock)
self.blks = nn.Sequential()
for i in range(num_layers):
self.blks.add_module("block" + str(i),
DecoderBlock(key_size, query_size, value_size, num_hiddens,
norm_shape, ffn_num_input, ffn_num_hiddens,
num_heads, dropout, i))
# 输出层:将隐藏状态映射到词表大小
self.dense = nn.Linear(num_hiddens, vocab_size)
def init_state(self, enc_outputs, enc_valid_lens, *args):
"""
初始化解码器状态
返回值:
- enc_outputs: 编码器输出
- enc_valid_lens: 编码器输入的有效长度,用于掩蔽注意力
- [None] * num_layers: 每个解码器块的自注意力缓存(预测时用)
"""
return [enc_outputs, enc_valid_lens, [None] * self.num_layers]
def forward(self, X, state):
"""
前向传播
参数:
- X: 解码器输入(token ID 序列)
- state: 编码器输出和解码器状态信息
返回:
- 输出 logits: 用于生成词的预测分布
- 更新后的状态
"""
# 嵌入并添加位置信息,然后缩放
X = self.pos_encoding(self.embedding(X) * math.sqrt(self.num_hiddens))
# 初始化注意力权重存储列表(2层,每层包含每个 block 的权重)
self._attention_weights = [[None] * len(self.blks) for _ in range(2)]
# 依次通过每一个解码器块
for i, blk in enumerate(self.blks):
X, state = blk(X, state)
# 记录自注意力权重(用于分析或可视化)
self._attention_weights[0][i] = blk.attention1.attention.attention_weights
# 记录编码器-解码器注意力权重
self._attention_weights[1][i] = blk.attention2.attention.attention_weights
# 通过输出层投影为词表大小,得到每个位置的预测结果
return self.dense(X), state
@property
def attention_weights(self):
"""返回保存的注意力权重"""
return self._attention_weights
六、训练
依照Transformer架构来实例化编码器-解码器模型。在这里,指定Transformer的编码器和解码器都是2层,都使用4头注意力。为了进行序列到序列的学习,下面在"英语-法语"机器翻译数据集上训练Transformer模型。
python
import d2l
num_hiddens, num_layers, dropout, batch_size, num_steps = 32, 2, 0.1, 64, 10
lr, num_epochs, device = 0.005, 200, d2l.try_gpu()
ffn_num_input, ffn_num_hiddens, num_heads = 32, 64, 4
key_size, query_size, value_size = 32, 32, 32
norm_shape = [32]
train_iter, src_vocab, tgt_vocab = d2l.load_data_nmt(batch_size, num_steps)
encoder = d2l.TransformerEncoder(
len(src_vocab), key_size, query_size, value_size, num_hiddens,
norm_shape, ffn_num_input, ffn_num_hiddens, num_heads,
num_layers, dropout)
decoder = d2l.TransformerDecoder(
len(tgt_vocab), key_size, query_size, value_size, num_hiddens,
norm_shape, ffn_num_input, ffn_num_hiddens, num_heads,
num_layers, dropout)
net = d2l.EncoderDecoder(encoder, decoder)
d2l.train_seq2seq(net, train_iter, lr, num_epochs, tgt_vocab, device)

训练结束后,使用Transformer模型将一些英语句子翻译成法语,并且计算它们的BLEU分数。
python
engs = ['go .', "i lost .", 'he\'s calm .', 'i\'m home .']
fras = ['va !', 'j\'ai perdu .', 'il est calme .', 'je suis chez moi .']
for eng, fra in zip(engs, fras):
translation, dec_attention_weight_seq = d2l.predict_seq2seq(
net, eng, src_vocab, tgt_vocab, num_steps, device, True)
print(f'{eng} => {translation}, ',
f'bleu {d2l.bleu(translation, fra, k=2):.3f}')

当进行最后一个英语到法语的句子翻译工作时,让我们可视化Transformer的注意力权重。编码器自注意力权重的形状为(编码器层数,注意力头数,num_steps或查询的数目,num_steps或"键-值"对的数目)
。
python
enc_attention_weights = torch.cat(net.encoder.attention_weights, 0).reshape((num_layers, num_heads,
-1, num_steps))
enc_attention_weights.shape # 输出:torch.Size([2, 4, 10, 10])
在编码器的自注意力中,查询和键都来自相同的输入序列。因为填充词元是不携带信息的,因此通过指定输入序列的有效长度可以避免查询与使用填充词元的位置计算注意力。接下来,将逐行呈现两层多头注意力的权重。每个注意力头都根据查询、键和值的不同的表示子空间来表示不同的注意力。
python
d2l.show_heatmaps(
enc_attention_weights.cpu(), xlabel='Key positions',
ylabel='Query positions', titles=['Head %d' % i for i in range(1, 5)],
figsize=(7, 3.5))

为了可视化解码器的自注意力权重和"编码器-解码器"的注意力权重,我们需要完成更多的数据操作工作。例如用零填充被掩蔽住的注意力权重。值得注意的是,解码器的自注意力权重和"编码器-解码器"的注意力权重都有相同的查询:即以序列开始词元(beginning-of-sequence,BOS)打头,再与后续输出的词元共同组成序列。
python
import pandas as pd
dec_attention_weights_2d = [head[0].tolist()
for step in dec_attention_weight_seq
for attn in step for blk in attn for head in blk]
dec_attention_weights_filled = torch.tensor(
pd.DataFrame(dec_attention_weights_2d).fillna(0.0).values)
dec_attention_weights = dec_attention_weights_filled.reshape((-1, 2, num_layers, num_heads, num_steps))
dec_self_attention_weights, dec_inter_attention_weights = \
dec_attention_weights.permute(1, 2, 3, 0, 4)
dec_self_attention_weights.shape, dec_inter_attention_weights.shape
输出:
css
(torch.Size([2, 4, 6, 10]), torch.Size([2, 4, 6, 10]))
由于解码器自注意力的自回归属性,查询不会对当前位置之后的"键-值"对进行注意力计算。
python
# Plusonetoincludethebeginning-of-sequencetoken
d2l.show_heatmaps(
dec_self_attention_weights[:, :, :, :len(translation.split()) + 1],
xlabel='Key positions', ylabel='Query positions',
titles=['Head %d' % i for i in range(1, 5)], figsize=(7, 3.5))

与编码器的自注意力的情况类似,通过指定输入序列的有效长度,输出序列的查询不会与输入序列中填充位置的词元进行注意力计算。
python
d2l.show_heatmaps(
dec_inter_attention_weights, xlabel='Key positions',
ylabel='Query positions', titles=['Head %d' % i for i in range(1, 5)],
figsize=(7, 3.5))

尽管 Transformer 架构最初是为序列到序列的学习任务而设计的,但在实际应用中,人们发现将 Transformer 编码器或解码器单独使用,也同样适用于各种不同的深度学习任务。
七、小结
- Transformer是编码器-解码器架构的一个实践,尽管在实际情况中编码器或解码器可以单独使用。
- 在Transformer中,多头自注意力 用于表示输入序列和输出序列,不过解码器必须通过掩蔽机制来保留自回归属性。
- Transformer中的残差连接和层规范化是训练非常深度模型的重要工具。
- Transformer模型中基于位置的前馈网络使用同一个多层感知机,作用是对所有序列位置的表示进行转换。