Transformer模型完全基于注意力机制,没有任何卷积层或循环神经网络层。尽管transformer最初是应⽤于在⽂本数据上的序列到序列学习,但现在已经推广到各种现代的深度学习中,例如语⾔、视觉、语音和强化学习领域。
本文章进行实战:利用Transformer把英语翻译成法语(Pytorch框架)
Transformer系列往期博客链接直达:攻克 Transformer & 注意力机制的查询、键和值 & 有无参数的Nadaraya-Watson核回归
攻克 Transformer && 评分函数(加性注意力、缩放点积注意力)
目录
Transformer作为编码器-解码器架构的⼀个实例,其整体架构图在图1 中展示。
正如所见到的,transformer是由编码器和解码器组成的;transformer的编码器和解码器是基于⾃注意力的模块叠加而成的;源序列和目标序列的嵌⼊层先加上位置编码(positional encoding),再分别输⼊到编码器和解码器中。
图1 Transformer架构
=============Transformer 编码器=============
从宏观角度来看,transformer的编码器是由多个相同的层叠加而成的,每个层都有两个子层。第⼀个⼦层是多头⾃注意力汇聚;第⼆个子层是基于位置的前馈网络。编码器由n个编码块组成,前n-1个编码块的输出作为编码块的输入,具体来说,在计算编码器的⾃注意力时,查询、键和值都来⾃前⼀个编码器层的输出。每个子层都采用了残差连接。在残差连接的加法计算之后,紧接着应用层规范化。因此,输入序列对应的每个位置,transformer 编码器都将输出⼀个 d 维表示向量。
=============Transformer 解码器=============
Transformer解码器也是由多个相同的层叠加而成的,并且层中使用了残差连接和层规范化。除了编码器中描述的两个子层之外,解码器还在这两个子层之间插⼊了第三个子层,称为编码器—解码器注意力层。在编码器-解码器注意力中,查询来自前⼀个解码器层的输出,而键和值来自整个编码器的输出。在解码器自注意力中,查询、键和值都来自上⼀个解码器层的输出。但是,解码器中的每个位置只能考虑该位置之前的所有位置。这种掩蔽注意力保留了自回归(auto-regressive)属性,确保预测仅依赖于已生成的输出词元。
补充:自回归模型_百度百科
自回归模型(英语:Autoregressive model,简称AR模型),是统计上一种处理时间序列的方法,用同一变数例如x的之前各期,亦即x1至xt-1来预测本期xt的表现,并假设它们为一线性关系。因为这是从回归分析中的线性回归发展而来,只是不用x预测y,而是用x预测 x(自己);所以叫做自回归。
Transformer“编码器-解码器”网络模型结构:
EncoderDecoder(编码器
(encoder): TransformerEncoder(嵌入层
(embedding): Embedding(184, 32)位置编码
(pos_encoding): PositionalEncoding(舍弃
(dropout): Dropout(p=0.1, inplace=False)
)
(blks): Sequential(第一个编码器块
(block0): EncoderBlock(多头注意力
(attention): MultiHeadAttention(缩放点击注意力
(attention): DotProductAttention(舍弃
(dropout): Dropout(p=0.1, inplace=False)
)全连接层(线性变换)
(W_q): Linear(in_features=32, out_features=32, bias=False)
(W_k): Linear(in_features=32, out_features=32, bias=False)
(W_v): Linear(in_features=32, out_features=32, bias=False)
(W_o): Linear(in_features=32, out_features=32, bias=False)
)第一个 加&规范化
(addnorm1): AddNorm(
(dropout): Dropout(p=0.1, inplace=False)
(ln): LayerNorm((32,), eps=1e-05, elementwise_affine=True)
)基于位置的前馈神经网络
(ffn): PositionWiseFFN(
(dense1): Linear(in_features=32, out_features=64, bias=True)
(relu): ReLU() 激活函数
(dense2): Linear(in_features=64, out_features=32, bias=True)
)第二个 加&规范化
(addnorm2): AddNorm(
(dropout): Dropout(p=0.1, inplace=False)
(ln): LayerNorm((32,), eps=1e-05, elementwise_affine=True)
)
)第二个编码器块
(block1): EncoderBlock(
(attention): MultiHeadAttention(
(attention): DotProductAttention(
(dropout): Dropout(p=0.1, inplace=False)
)
(W_q): Linear(in_features=32, out_features=32, bias=False)
(W_k): Linear(in_features=32, out_features=32, bias=False)
(W_v): Linear(in_features=32, out_features=32, bias=False)
(W_o): Linear(in_features=32, out_features=32, bias=False)
)
(addnorm1): AddNorm(
(dropout): Dropout(p=0.1, inplace=False)
(ln): LayerNorm((32,), eps=1e-05, elementwise_affine=True)
)
(ffn): PositionWiseFFN(
(dense1): Linear(in_features=32, out_features=64, bias=True)
(relu): ReLU()
(dense2): Linear(in_features=64, out_features=32, bias=True)
)
(addnorm2): AddNorm(
(dropout): Dropout(p=0.1, inplace=False)
(ln): LayerNorm((32,), eps=1e-05, elementwise_affine=True)
))))解码器同编码器类似
(1)多头注意力
在实践中,当给定相同的查询、键和值的集合时,我们希望模型可以基于相同的注意⼒机制学习到不同的⾏为,然后将不同的⾏为作为知识组合起来,捕获序列内各种范围的依赖关系(例如,短距离依赖和长距离依赖关系)。因此,允许注意力机制组合使用查询、键和值的不同子空间表示可能是有益的。
为此,与其只使⽤单独⼀个注意力汇聚,我们可以⽤独⽴学习得到的h组不同的线性投影(linear projections) 来变换查询、键和值。然后,这h组变换后的查询、键和值将并⾏地送到注意⼒汇聚中。最后,将这h个注意力汇聚的输出拼接在⼀起,并且通过另⼀个可以学习的线性投影进⾏变换,以产⽣最终输出,这种设计被称为多头注意力(multihead attention)。对于h个注意力汇聚输出,每⼀个注意⼒汇聚都被称作⼀个头。
图2 展示了使用全连接层来实现可学习的线性变换的多头注意力。
图2 多头注意力:多个头连结然后线性变换
多头注意力代码:(此处选择缩放点积注意力作为注意力汇聚)
class MultiHeadAttention(nn.Module):
"""多头注意力"""
# 100, 100, 100, 100, 5, 0.5
def __init__(self, key_size, query_size, value_size, num_hiddens,
num_heads, dropout, bias=False, **kwargs):
super(MultiHeadAttention, self).__init__(**kwargs)
self.num_heads = num_heads
self.DP_Attention = d2l.DotProductAttention(dropout) # 缩放点积注意力,舍弃50%的神经元参数
# 输入样本的大小、输出样本的大小、偏置设置为False
self.W_q = nn.Linear(query_size, num_hiddens, bias=bias) # [100, 100]
self.W_k = nn.Linear(key_size, num_hiddens, bias=bias) # [100, 100]
self.W_v = nn.Linear(value_size, num_hiddens, bias=bias) # [100, 100]
self.W_o = nn.Linear(num_hiddens, num_hiddens, bias=bias) # [100, 100]
# [2, 4, 100], [2, 6, 100], [2, 6, 100], torch.tensor([3, 2])
def forward(self, queries, keys, values, valid_lens):
# queries,keys,values的形状:(batch_size,查询或者“键-值”对的个数,num_hiddens)
# valid_lens 的形状:(batch_size,)或(batch_size,查询的个数)
# 经过变换后,输出的 queries,keys,values 的形状:
# (batch_size*num_heads,查询或者“键-值”对的个数,num_hiddens/num_heads)
queries = transpose_qkv(self.W_q(queries), self.num_heads)
keys = transpose_qkv(self.W_k(keys), self.num_heads)
values = transpose_qkv(self.W_v(values), self.num_heads)
# print(queries)
if valid_lens is not None:
# 在轴0,将第一项(标量或者矢量)复制num_heads次,
# 然后如此复制第二项,然后诸如此类。
valid_lens = torch.repeat_interleave(valid_lens, repeats=self.num_heads, dim=0)
# print(valid_lens) # tensor([3, 3, 3, 3, 3, 2, 2, 2, 2, 2])
print("valid_lens:", valid_lens.size())
# output的形状:(batch_size*num_heads,查询的个数,num_hiddens/num_heads)
# torch.Size([10, 4, 20])、torch.Size([10, 6, 20])、torch.Size([10, 6, 20])、torch.Size([10])
output = self.DP_Attention(queries, keys, values, valid_lens)
# output_concat的形状:(batch_size,查询的个数,num_hiddens)
output_concat = transpose_output(output, self.num_heads)
return self.W_o(output_concat)
为了能够使多个头并行计算,上面的MultiHeadAttention类将使用下面定义的两个转置函数。具体来说, transpose_output函数反转了transpose_qkv函数的操作。
# [2, 4, 100]/[2, 6, 100], 5
def transpose_qkv(X, num_heads):
"""为了多注意力头的并行计算而变换形状"""
# 输入X的形状:(batch_size,查询或者“键-值”对的个数,num_hiddens)
# 输出X的形状:(batch_size,查询或者“键-值”对的个数,num_heads,num_hiddens/num_heads)
# reshape(-1):首先把张量中的所有元素平铺,然后在变形成指定的形状
X = X.reshape(X.shape[0], X.shape[1], num_heads, -1) # 2*4*100/2*4*5 = 20, -1就代表20
# print(X.size())
# 输出X的形状:(batch_size,num_heads,查询或者“键-值”对的个数, num_hiddens/num_heads)
X = X.permute(0, 2, 1, 3) # 更改矩阵形状 torch.Size([2, 5, 4, 20])
# print(X.size())
# 最终输出的形状:(batch_size*num_heads,查询或者“键-值”对的个数, num_hiddens/num_heads)
output = X.reshape(-1, X.shape[2], X.shape[3]) # 2*5*4*20/4*20 = 10, -1就代表10
print("transpose_qkv:", output.size())
return output
# [10, 4, 20], 5
def transpose_output(X, num_heads):
"""逆转transpose_qkv函数的操作"""
# print(X.size())
X = X.reshape(-1, num_heads, X.shape[1], X.shape[2]) # [2, 5, 4, 20]
X = X.permute(0, 2, 1, 3) # [2, 4, 5, 20]
output = X.reshape(X.shape[0], X.shape[1], -1) # [2, 4, 100]
print("transpose_output:", output.size())
return output
最后,使用键和值相同的例子,测试我们编写的MultiHeadAttention类。
num_hiddens, num_heads = 100, 5
attention = MultiHeadAttention(num_hiddens, num_hiddens, num_hiddens,
num_hiddens, num_heads, 0.5)
attention.eval()
batch_size, num_queries = 2, 4
num_kvpairs, valid_lens = 6, torch.tensor([3, 2])
X = torch.ones((batch_size, num_queries, num_hiddens)) # [2, 4, 100]
Y = torch.ones((batch_size, num_kvpairs, num_hiddens)) # [2, 6, 100]
print("result:", attention(X, Y, Y, valid_lens).shape) # torch.Size([2, 4, 100])
输出:
transpose_qkv: torch.Size([10, 4, 20])
transpose_qkv: torch.Size([10, 6, 20])
transpose_qkv: torch.Size([10, 6, 20])
valid_lens: torch.Size([10])
transpose_output: torch.Size([2, 4, 100])
result: torch.Size([2, 4, 100])
(2)基于位置的前馈网络
基于位置的前馈⽹络对序列中的所有位置的表⽰进⾏变换时使用的是同⼀个多层感知机(MLP),这就是称前馈⽹络是基于位置的原因。
下⾯的例⼦显示,改变张量的最⾥层维度的尺⼨,会改变成基于位置的前馈⽹络的输出尺⼨。因为⽤同⼀个多层感知机对所有位置上的输⼊进⾏变换,所以当所有这些位置的输⼊相同时,它们的输出也是相同的。
class PositionWiseFFN(nn.Module):
"""基于位置的前馈网络"""
def __init__(self, ffn_num_input, ffn_num_hiddens, ffn_num_outputs,
**kwargs):
super(PositionWiseFFN, self).__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):
return self.dense2(self.relu(self.dense1(X)))
# demo
# [2, 3, 4] * [4, 4] * [4, 8] = [2, 3, 8]
ffn = PositionWiseFFN(4, 4, 8)
ffn.eval()
print("基于位置的前馈网络:")
print(ffn(torch.ones((2, 3, 4)))[0])
输出:
基于位置的前馈网络:
tensor([[-0.1039, 0.6010, -0.7257, 0.0406, -0.2380, -0.5354, -1.0672, 0.3957],
[-0.1039, 0.6010, -0.7257, 0.0406, -0.2380, -0.5354, -1.0672, 0.3957],
[-0.1039, 0.6010, -0.7257, 0.0406, -0.2380, -0.5354, -1.0672, 0.3957]],
grad_fn=<SelectBackward0>)
(3)残差网络后进行层规范化
下面代码由残差连接和紧随其后的层规范化组成,两者都是构建有效的深度架构的关键。
class AddNorm(nn.Module):
"""残差连接后进行层规范化"""
def __init__(self, normalized_shape, dropout, **kwargs):
super(AddNorm, self).__init__(**kwargs)
self.dropout = nn.Dropout(dropout)
self.ln = nn.LayerNorm(normalized_shape)
def forward(self, X, Y):
return self.ln(self.dropout(Y) + X)
# demo
add_norm = AddNorm([3, 4], 0.5)
add_norm.eval()
print("残差连接后进行层规范化:")
print(add_norm(torch.ones((2, 3, 4)), torch.ones((2, 3, 4))).shape)
输出:
残差连接后进行层规范化:
torch.Size([2, 3, 4])
补充:
层规范化和批量规范化的⽬标相同,但层规范化是基于特征维度进⾏规范化。尽管批量规范化在计算机视觉中 被广泛应用,但在自然语言处理任务中(输入通常是变长序列)批量规范化通常不如层规范化的效果好。
以下代码对⽐不同维度的层规范化和批量规范化的效果。
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))
(4)transformer编码器块
有了组成transformer编码器的基础组件,现在可以先实现编码器中的一个层。下⾯的EncoderBlock类包含两个⼦层:多头⾃注意力和基于位置的前馈⽹络,这两个子层都使用了残差连接和紧随的层规范化。
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):
super(EncoderBlock, self).__init__(**kwargs)
# 多头注意力
self.attention = d2l.MultiHeadAttention(key_size, query_size, value_size,
num_hiddens, num_heads, dropout, use_bias)
# 加 & 规范化(包含残差连接)
self.addnorm1 = AddNorm(norm_shape, dropout)
# 逐位前馈网络
self.ffn = PositionWiseFFN(ffn_num_input, ffn_num_hiddens, num_hiddens)
# 加 & 规范化(包含残差连接)
self.addnorm2 = AddNorm(norm_shape, dropout)
# 多头注意力>>>加&规范化>>>逐位前馈网络>>>加&规范化
def forward(self, X, valid_lens):
Y = self.addnorm1(X, self.attention(X, X, X, valid_lens))
return self.addnorm2(Y, self.ffn(Y))
# demo
X = torch.ones((2, 100, 24))
valid_lens = torch.tensor([3, 2])
encoder_blk = EncoderBlock(24, 24, 24, 24, [100, 24], 24, 48, 8, 0.5)
encoder_blk.eval()
print("transformer编码器块:")
print(encoder_blk(X, valid_lens).shape)
输出:(输入的张量尺寸和输出的张量尺寸大小相同)
transformer编码器块:
torch.Size([2, 100, 24])
(5)transformer编码器
在实现下⾯的transformer编码器的代码中,我们堆叠了num_layers个EncoderBlock类的实例。由于我们使⽤的是值范围在−1和1之间的固定位置编码,因此通过学习得到的输⼊的嵌⼊表⽰的值需要先乘以嵌⼊维度的平⽅根进⾏重新缩放,然后再与位置编码相加。
下面的代码指定了超参数来创建⼀个两层的transformer编码器。
class TransformerEncoder(d2l.Encoder):
"""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, use_bias=False, **kwargs):
super(TransformerEncoder, self).__init__(**kwargs)
self.num_hiddens = num_hiddens
# 嵌入层
self.embedding = nn.Embedding(vocab_size, num_hiddens)
# 位置编码
self.pos_encoding = d2l.PositionalEncoding(num_hiddens, dropout)
# 添加模块(num_layers个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和1之间,因此嵌入值乘以嵌入维度的平方根进行缩放,然后再与位置编码相加。
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
# demo
encoder = TransformerEncoder(200, 24, 24, 24, 24, [100, 24], 24, 48, 8, 2, 0.5)
encoder.eval()
print("transformer编码器:")
print(encoder(torch.ones((2, 100), dtype=torch.long), valid_lens).shape)
输出:
transformer编码器:
torch.Size([2, 100, 24])
(6)transformer解码器块
transformer解码器也是由多个相同的层组成。在DecoderBlock类中实现的每个层包含了三个⼦层:解码器自注意力、“编码器-解码器”注意力和基于位置的前馈网络。这些⼦层也都被残差连接和紧随的层规范化围绕。
关于序列到序列模型(sequence-to-sequence model),在训练阶段,其输出序列的所有位置(时间步)的词元都是已知的;然而,在预测阶段,其输出序列的词元是逐个生成的。因此,在任何解码器时间步中,只有⽣成的词元才能⽤于解码器的自注意力计算中。
为了在解码器中保留自回归的属性,其掩蔽自注意力设定了参数dec_valid_lens,以便任何查询都只会与解码器中所有已经生成词元的位置(即直到该查询位置为止)进⾏注意力计算。
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):
super(DecoderBlock, self).__init__(**kwargs)
self.i = i
# 多头注意力
self.attention1 = d2l.MultiHeadAttention(
key_size, query_size, value_size, num_hiddens, num_heads, dropout)
# 加 & 规范化
self.addnorm1 = AddNorm(norm_shape, dropout)
# 多头注意力
self.attention2 = d2l.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):
enc_outputs, enc_valid_lens = state[0], state[1]
# 训练阶段,输出序列的所有词元都在同一时间处理,
# 因此state[2][self.i]初始化为None。
# 预测阶段,输出序列是通过词元一个接着一个解码的,
# 因此state[2][self.i]包含着直到当前时间步第i个块解码的输出表示
if state[2][self.i] is None:
key_values = X
else:
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的开头:(batch_size,num_steps),
# 其中每一行是[1,2,...,num_steps]
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)
# 编码器-解码器注意力。
# enc_outputs的开头:(batch_size,num_steps,num_hiddens)
Y2 = self.attention2(Y, enc_outputs, enc_outputs, enc_valid_lens)
Z = self.addnorm2(Y, Y2)
return self.addnorm3(Z, self.ffn(Z)), state
# demo
decoder_blk = 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("解码器中第i个块:")
print(decoder_blk(X, state)[0].shape)
输出:
解码器中第i个块:
torch.Size([2, 100, 24])
(7)transformer解码器
现在我们构建了由num_layers个DecoderBlock实例组成的完整的transformer解码器。最后,通过⼀个全连接层计算所有vocab_size个可能的输出词元的预测值。解码器的⾃注意力权重和编码器解码器注意力权重都被存储下来,⽅便日后可视化的需要。
class TransformerDecoder(d2l.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):
super(TransformerDecoder, self).__init__(**kwargs)
self.num_hiddens = num_hiddens
self.num_layers = num_layers
# 嵌入层
self.embedding = nn.Embedding(vocab_size, num_hiddens)
# 位置编码
self.pos_encoding = d2l.PositionalEncoding(num_hiddens, dropout)
# 添加模块(num_layers个解码器块)
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):
return [enc_outputs, enc_valid_lens, [None] * self.num_layers]
def forward(self, X, state):
# 位置编码+嵌入层
X = self.pos_encoding(self.embedding(X) * math.sqrt(self.num_hiddens))
# 定义二维列表,存放 解码器自注意力权重 和 “编码器=解码器”自注意力权重
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 # 最后把参数传入到全连接层之中
def attention_weights(self):
return self._attention_weights
(8)训练
依照transformer架构来实例化编码器-解码器模型。在这⾥,指定transformer的编码器和解码器都是2层, 都使⽤4头注意⼒。
为了进⾏序列到序列的学习,我们在“英语-法语”机器翻译数据集上训练transformer模型。
# # 训练
# 隐藏层层数,编码器/解码器块数量,舍弃比率,批量大小,num_steps
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
# 查询,键,值
query_size, key_size, value_size = 32, 32, 32
# AddNorm(规范化的维度)
norm_shape = [32]
# 加载文字数据集(英语翻译成法语)
# train_iter:数据集信息
# src_vocab:源(英语)词汇表
# tgt_vocab:法语词汇表
train_iter, src_vocab, tgt_vocab = d2l.load_data_nmt(batch_size, num_steps)
encoder = 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)
# print("编码器返回的结果(Transformer编码器的结构):")
# print(encoder)
decoder = 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)
# print("解码器返回的结果(Transformer解码器的结构):")
# print(decoder)
# Transformer“编码器-解码器”网络模型
net = d2l.EncoderDecoder(encoder, decoder)
print("Transformer“编码器-解码器”网络模型结构:")
print(net)
# 训练一个序列到序列的模型
# 网络模型,数据集信息,学习率,迭代次数,法语词汇表(标志输出),设备选取
d2l.train_seq2seq(net, train_iter, lr, num_epochs, tgt_vocab, device)
图3 Epoch-Loss曲线图
数据集截图:
(9)测试
训练结束后,使⽤transformer模型将⼀些英语句⼦翻译成法语,并且计算它们的BLEU分数。
- BLEU的全名为:bilingual evaluation understudy,即:双语互译质量评估辅助工具。它是用来评估机器翻译质量的工具。
- BLEU的设计思想:机器翻译结果越接近专业人工翻译的结果,则越好。BLEU算法实际上就是在判断两个句子的相似程度。
- 想知道一个句子翻译前后的表示是否意思一致,直接的办法是拿这个句子的标准人工翻译与机器翻译的结果作比较,如果它们是很相似的,说明我的翻译很成功。
- 因此,BLUE将机器翻译的结果与其相对应的几个参考翻译作比较,算出一个综合分数。这个分数越高说明机器翻译得越好。注意BLEU算法是句子之间的比较,不是词组,也不是段落。
# # 测试
engs = ['go .', "i lost .", 'he\'s calm .', 'i\'m home .']
fras = ['va !', 'j\'ai perdu .', 'il est calme .', 'je suis chez moi .']
# 多维列表
dec_attention_weight_seq = []
for eng, fra in zip(engs, fras):
# 网络模型、单个英语词汇、英语词汇集、法语词汇集、num_steps,设备选取
translation, dec_attention_weight_seq = d2l.predict_seq2seq(
net, eng, src_vocab, tgt_vocab, num_steps, device, save_attention_weights=True)
print(f'{eng} => {translation}, ',
f'bleu {d2l.bleu(translation, fra, k=2):.3f}') # 评分函数(翻译结果同数据集相比较)
enc_attention_weights = torch.cat(net.encoder.attention_weights, 0).reshape((num_layers, num_heads, -1, num_steps))
print(enc_attention_weights.shape)
输出:
go . => va !, bleu 1.000
i lost . => je vous en prie ., bleu 1.000
he's calm . => il court ., bleu 0.000
i'm home . => je suis chez moi ., bleu 1.000
torch.Size([2, 4, 10, 10])
在编码器的⾃注意⼒中,查询和键都来⾃相同的输⼊序列。因为填充词元是不携带信息的,因此通过指定输⼊序列的有效⻓度可以避免查询与使⽤填充词元的位置计算注意力。接下来,将逐⾏呈现两层多头注意力的权重。每个注意⼒头都根据查询、键和值的不同的表⽰子空间来表⽰不同的注意⼒。
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))
plt.show()
图4 最后⼀个英语到法语的句子翻译的可视化transformer的注意力权重
(10)可视化权重
为了可视化解码器的⾃注意⼒权重和“编码器-解码器”的注意⼒权重,我们需要完成更多的数据操作⼯作。 例如,我们⽤零填充被掩蔽住的注意⼒权重。值得注意的是,解码器的⾃注意⼒权重和“编码器-解码器” 的注意⼒权重都有相同的查询:即以序列开始词元(beginning-of-sequence,BOS)打头,再与后续输出的词元共同组成序列。
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)
print(dec_self_attention_weights.shape, dec_inter_attention_weights.shape)
# 与编码器的自注意力的情况类似,通过指定输入序列的有效长度,
# [输出序列的查询不会与输入序列中填充位置的词元进行注意力计算]。
# 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))
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))
1. 由于解码器⾃注意⼒的⾃回归属性,查询不会对当前位置之后的“键-值”对进⾏注意⼒计算。
由上图可以观察到:查询位置之后(主对角线上方)的注意力权重为零,即没有进行注意力计算!
2. 与编码器的⾃注意⼒的情况类似,通过指定输⼊序列的有效⻓度,输出序列的查询不会与输⼊序列中填充位置的词元进⾏注意⼒计算。
由上图可以观察到:指定序列长度后,大于序列长度的位置的注意力权重为零,没有进行注意力计算!
(11)总结
- transformer是编码器-解码器架构的⼀个实践,尽管在实际情况中编码器或解码器可以单独使⽤。
- 在transformer中,多头⾃注意力⽤于表⽰输⼊序列和输出序列,不过解码器必须通过掩蔽机制来保留自回归属性。
- transformer中的残差连接和层规范化是训练⾮常深度模型的重要⼯具。
- transformer模型中基于位置的前馈⽹络使⽤同⼀个多层感知机,作用是对所有序列位置的表示进行转换。
(12)完整代码
import math
import pandas as pd
import torch
from matplotlib import pyplot as plt
from torch import nn
from d2l import torch as d2l
class PositionWiseFFN(nn.Module):
"""基于位置的前馈网络"""
def __init__(self, ffn_num_input, ffn_num_hiddens, ffn_num_outputs,
**kwargs):
super(PositionWiseFFN, self).__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):
return self.dense2(self.relu(self.dense1(X)))
# demo
# [2, 3, 4] * [4, 4] * [4, 8] = [2, 3, 8]
ffn = PositionWiseFFN(4, 4, 8)
ffn.eval()
print("基于位置的前馈网络:")
print(ffn(torch.ones((2, 3, 4)))[0])
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))
class AddNorm(nn.Module):
"""残差连接后进行层规范化"""
def __init__(self, normalized_shape, dropout, **kwargs):
super(AddNorm, self).__init__(**kwargs)
self.dropout = nn.Dropout(dropout)
self.ln = nn.LayerNorm(normalized_shape)
def forward(self, X, Y):
return self.ln(self.dropout(Y) + X)
# demo
add_norm = AddNorm([3, 4], 0.5)
add_norm.eval()
print("残差连接后进行层规范化:")
print(add_norm(torch.ones((2, 3, 4)), torch.ones((2, 3, 4))).shape)
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):
super(EncoderBlock, self).__init__(**kwargs)
# 多头注意力
self.attention = d2l.MultiHeadAttention(key_size, query_size, value_size,
num_hiddens, num_heads, dropout, use_bias)
# 加 & 规范化(包含残差连接)
self.addnorm1 = AddNorm(norm_shape, dropout)
# 逐位前馈网络
self.ffn = PositionWiseFFN(ffn_num_input, ffn_num_hiddens, num_hiddens)
# 加 & 规范化(包含残差连接)
self.addnorm2 = AddNorm(norm_shape, dropout)
# 多头注意力>>>加&规范化>>>逐位前馈网络>>>加&规范化
def forward(self, X, valid_lens):
Y = self.addnorm1(X, self.attention(X, X, X, valid_lens))
return self.addnorm2(Y, self.ffn(Y))
# demo
X = torch.ones((2, 100, 24))
valid_lens = torch.tensor([3, 2])
encoder_blk = EncoderBlock(24, 24, 24, 24, [100, 24], 24, 48, 8, 0.5)
encoder_blk.eval()
print("transformer编码器块:")
print(encoder_blk(X, valid_lens).shape)
class TransformerEncoder(d2l.Encoder):
"""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, use_bias=False, **kwargs):
super(TransformerEncoder, self).__init__(**kwargs)
self.num_hiddens = num_hiddens
# 嵌入层
self.embedding = nn.Embedding(vocab_size, num_hiddens)
# 位置编码
self.pos_encoding = d2l.PositionalEncoding(num_hiddens, dropout)
# 添加模块(num_layers个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和1之间,因此嵌入值乘以嵌入维度的平方根进行缩放,然后再与位置编码相加。
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
# demo
encoder = TransformerEncoder(200, 24, 24, 24, 24, [100, 24], 24, 48, 8, 2, 0.5)
encoder.eval()
print("transformer编码器:")
print(encoder(torch.ones((2, 100), dtype=torch.long), valid_lens).shape)
# =================================================================== #
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):
super(DecoderBlock, self).__init__(**kwargs)
self.i = i
# 多头注意力
self.attention1 = d2l.MultiHeadAttention(
key_size, query_size, value_size, num_hiddens, num_heads, dropout)
# 加 & 规范化
self.addnorm1 = AddNorm(norm_shape, dropout)
# 多头注意力
self.attention2 = d2l.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):
enc_outputs, enc_valid_lens = state[0], state[1]
# 训练阶段,输出序列的所有词元都在同一时间处理,
# 因此state[2][self.i]初始化为None。
# 预测阶段,输出序列是通过词元一个接着一个解码的,
# 因此state[2][self.i]包含着直到当前时间步第i个块解码的输出表示
if state[2][self.i] is None:
key_values = X
else:
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的开头:(batch_size,num_steps),
# 其中每一行是[1,2,...,num_steps]
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)
# 编码器-解码器注意力。
# enc_outputs的开头:(batch_size,num_steps,num_hiddens)
Y2 = self.attention2(Y, enc_outputs, enc_outputs, enc_valid_lens)
Z = self.addnorm2(Y, Y2)
return self.addnorm3(Z, self.ffn(Z)), state
# demo
decoder_blk = 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("解码器中第i个块:")
print(decoder_blk(X, state)[0].shape)
class TransformerDecoder(d2l.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):
super(TransformerDecoder, self).__init__(**kwargs)
self.num_hiddens = num_hiddens
self.num_layers = num_layers
# 嵌入层
self.embedding = nn.Embedding(vocab_size, num_hiddens)
# 位置编码
self.pos_encoding = d2l.PositionalEncoding(num_hiddens, dropout)
# 添加模块(num_layers个解码器块)
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):
return [enc_outputs, enc_valid_lens, [None] * self.num_layers]
def forward(self, X, state):
# 位置编码+嵌入层
X = self.pos_encoding(self.embedding(X) * math.sqrt(self.num_hiddens))
# 定义二维列表,存放 解码器自注意力权重 和 “编码器=解码器”自注意力权重
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 # 最后把参数传入到全连接层之中
def attention_weights(self):
return self._attention_weights
# =================================================================== #
# # 训练
# 隐藏层层数,编码器/解码器块数量,舍弃比率,批量大小,num_steps
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
# 查询,键,值
query_size, key_size, value_size = 32, 32, 32
# AddNorm(规范化的维度)
norm_shape = [32]
# 加载文字数据集(英语翻译成法语)
# train_iter:数据集信息
# src_vocab:源(英语)词汇表
# tgt_vocab:法语词汇表
train_iter, src_vocab, tgt_vocab = d2l.load_data_nmt(batch_size, num_steps)
encoder = 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)
# print("编码器返回的结果(Transformer编码器的结构):")
# print(encoder)
decoder = 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)
# print("解码器返回的结果(Transformer解码器的结构):")
# print(decoder)
# Transformer“编码器-解码器”网络模型
net = d2l.EncoderDecoder(encoder, decoder)
print("Transformer“编码器-解码器”网络模型结构:")
print(net)
# 训练一个序列到序列的模型
# 网络模型,数据集信息,学习率,迭代次数,法语词汇表(标志输出),设备选取
d2l.train_seq2seq(net, train_iter, lr, num_epochs, tgt_vocab, device)
# =================================================================== #
# # 测试
engs = ['go .', "i lost .", 'he\'s calm .', 'i\'m home .']
fras = ['va !', 'j\'ai perdu .', 'il est calme .', 'je suis chez moi .']
# 多维列表
dec_attention_weight_seq = []
for eng, fra in zip(engs, fras):
# 网络模型、单个英语词汇、英语词汇集、法语词汇集、num_steps,设备选取
translation, dec_attention_weight_seq = d2l.predict_seq2seq(
net, eng, src_vocab, tgt_vocab, num_steps, device, save_attention_weights=True)
print(f'{eng} => {translation}, ',
f'bleu {d2l.bleu(translation, fra, k=2):.3f}') # 评分函数(翻译结果同数据集相比较)
enc_attention_weights = torch.cat(net.encoder.attention_weights, 0).reshape((num_layers, num_heads, -1, num_steps))
print(enc_attention_weights.shape)
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))
plt.show()
# =================================================================== #
# # 可视化权重
print(len(dec_attention_weight_seq))
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)
print(dec_self_attention_weights.shape, dec_inter_attention_weights.shape)
# 与编码器的自注意力的情况类似,通过指定输入序列的有效长度,
# [输出序列的查询不会与输入序列中填充位置的词元进行注意力计算]。
# 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))
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))
>>>如有疑问,欢迎评论区一起探讨