电脑基础 · 2023年3月31日

注意力机制 - Transformer

文章目录

  • Transformer
    • 1 - 模型
    • 2 - 基于位置的前馈网络
    • 3 - 残差连接和层规范化
    • 4 - 编码器
    • 5 - 解码器
    • 6 - 训练
    • 7 - 小结

Transformer

注意力同时具有并行计算和最短的最大路径长度这两个优势,因此使用自注意力来设计深度架构是很有吸引力的。对比之前仍然依赖循环神经网络实现输入表示的自注意力模型,transformer模型完全基于注意力机制,没有任何卷积层或循环神经网络层。尽管transformer最初是应用于文本数据上的序列到序列学习,但现在已经推广到各种现代的深度学习中,例如语言、视觉、语音、强化学习领域

1 - 模型

Transformer作为编码器-解码器架构的⼀个实例,其整体架构图在 图10.7.1中展⽰。正如所⻅到的,transformer是由编码器和解码器组成的。与 10.4.1中基于Bahdanau注意⼒实现的序列到序列的学习相⽐,transformer的编码器和解码器是基于⾃注意⼒的模块叠加⽽成的,源(输⼊)序列和⽬标(输出)序列的嵌⼊(embedding)表⽰将加上位置编码(positional encoding),再分别输⼊到编码器和解码器中
注意力机制 - Transformer
注意力机制 - Transformer
注意力机制 - Transformer

import math
import pandas as pd
import torch
from torch import nn
from d2l import torch as d2l

2 - 基于位置的前馈网络

基于位置的前馈网络对序列中的所有位置的表示进行变换时使用的是同一个多层感知机(MLP),这就是称前馈网络是基于位置的(positionwise)的原因。

在下面的实现中,输入X的形状(批量大小,时间步数或序列长度,隐单元数或特征维度)将被一个两层的感知机转换成形状位(批量大小,时间步数,ffn_num_outputs)的输出张量

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)))

下面的例子显示,改变张量的最里层维度的尺寸,会改变基于位置的前馈网络的输出尺寸。因为用同一个多层感知机对所有位置上的输入进行变换,所以当所有这些位置的输入相同时,它们的输出也是相同的

ffn = PositionWiseFFN(4,4,8)
ffn.eval()
ffn(torch.ones((2,3,4)))[0]
tensor([[ 0.0626,  0.1739, -0.3923,  0.2537, -1.2938,  0.0616,  0.6115,  0.4278],
        [ 0.0626,  0.1739, -0.3923,  0.2537, -1.2938,  0.0616,  0.6115,  0.4278],
        [ 0.0626,  0.1739, -0.3923,  0.2537, -1.2938,  0.0616,  0.6115,  0.4278]],
       grad_fn=<SelectBackward0>)

3 - 残差连接和层规范化

现在让我们关注图10.7.1中的“加法和规范化(add&norm)”组件。正如在本节开头所述,这是由残差连接和紧随其后的层规范化组成的。两者都是构建有效的深度架构的关键

以前我们解释了在⼀个⼩批量的样本内基于批量规范化对数据进⾏重新中⼼化和重新缩放的调整。层规范化和批量规范化的⽬标相同,但层规范化是基于特征维度进⾏规范化。尽管批量规范化在计算机视觉中被⼴泛应⽤,但在⾃然语⾔处理任务中(输⼊通常是变⻓序列)批量规范化通常不如层规范化的效果好

以下代码对比不同维度的层规范化和批量规范化的效果

ln = nn.LayerNorm(2)
bn = nn.BatchNorm1d(2)
X = torch.tensor([[1,2],[2,3]],dtype=torch.float32)
# 在训练模式下计算X的均值和方差
print('layer norm:',ln(X),'\n batch norm:',bn(X))
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>)

现在我们可以使用残差连接和层规范化来实现AddNorm类。暂退法也被作为正则化方法使用

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)

残差连接要求两个输入的形状相同,以便加法操作后输出张量的形状相同

add_norm = AddNorm([3, 4], 0.5)
add_norm.eval()
add_norm(torch.ones((2, 3, 4)), torch.ones((2, 3, 4))).shape
torch.Size([2, 3, 4])

4 - 编码器

有了组成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))
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()
encoder_blk(X,valid_lens).shape
torch.Size([2, 100, 24])

在实现下面的transformer编码器的代码实现中,我们堆叠了num_layers个EncoderBlock类的实例。由于我们使用的是值范围在-1和1之间的固定位置编码,因此通过学习得到的输入的嵌入表示的值需要先乘以嵌入维度的平方根进行重新缩放,然后再与位置编码相加

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)
        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

下面我们指定了超参数来创建一个两层的transformer编码器。Transformer编码器输出下形状是(批量大小,时间步数目,num_hiddens)

encoder = TransformerEncoder(200, 24, 24, 24, 24, [100, 24], 24, 48, 8, 2, 0.5)
encoder.eval()
encoder(torch.ones((2,100),dtype=torch.long),valid_lens).shape
torch.Size([2, 100, 24])

5 - 解码器

如 图10.7.1所⽰,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

为了便于在“编码器-解码器”注意力中进行缩放点积计算和残差连接中进行加法计算,编码器和解码器的特征维度都是num_hiddens

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]]
decoder_blk(X, state)[0].shape
torch.Size([2, 100, 24])

现在我们构建了由num_layers个DecoderBlock实例组成的完整的transformer解码器。最后,通过⼀个全连接层计算所有vocab_size个可能的输出词元的预测值。解码器的⾃注意⼒权重和编码器解码器注意⼒权重都被存储下来,⽅便⽇后可视化的需要

class TransformerDecoder(d2l.AttentionDecoder):
    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)
        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
    @property
    def attention_weights(self):
        return self._attention_weights

6 - 训练

依照transformer架构来实例化编码器-解码器模型。在这⾥,指定transformer的编码器和解码器都是2层,都使⽤4头注意⼒。与 9.7.4节类似,为了进⾏序列到序列的学习,我们在“英语-法语”机器翻译数据集上训练transformer模型

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 = 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 = 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)
loss 0.031, 9433.7 tokens/sec on cuda:0

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tbx6Bthk-1663076050925)(https://www.yuucn.com/wp-content/uploads/2023/03/1680221029-e8de267006956e9.svg)]

训练结束后,使⽤transformer模型将⼀些英语句⼦翻译成法语,并且计算它们的BLEU分数

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}')
go . => va !,  bleu 1.000
i lost . => j'ai perdu .,  bleu 1.000
he's calm . => il est calme .,  bleu 1.000
i'm home . => je suis chez moi .,  bleu 1.000

当进⾏最后⼀个英语到法语的句⼦翻译⼯作时,让我们可视化transformer的注意⼒权重。编码器⾃注意⼒权重的形状为(编码器层数,注意⼒头数,num_steps或查询的数⽬,num_steps或“键-值”对的数⽬)

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])

在编码器的⾃注意⼒中,查询和键都来⾃相同的输⼊序列。因为填充词元是不携带信息的,因此通过指定输⼊序列的有效⻓度可以避免查询与使⽤填充词元的位置计算注意⼒。接下来,将逐⾏呈现两层多头注意⼒的权重。每个注意⼒头都根据查询、键和值的不同的表⽰⼦空间来表⽰不同的注意⼒

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))


[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JHQ03AqK-1663076050925)(https://www.yuucn.com/wp-content/uploads/2023/03/1680221035-e8de267006956e9.svg)]

为了可视化解码器的⾃注意⼒权重和“编码器-解码器”的注意⼒权重,我们需要完成更多的数据操作⼯作。例如,我们⽤零填充被掩蔽住的注意⼒权重。值得注意的是,解码器的⾃注意⼒权重和“编码器-解码器”的注意⼒权重都有相同的查询:即以序列开始词元(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)
dec_self_attention_weights.shape, dec_inter_attention_weights.shape
(torch.Size([2, 4, 6, 10]), torch.Size([2, 4, 6, 10]))

由于解码器⾃注意⼒的⾃回归属性,查询不会对当前位置之后的“键-值”对进⾏注意⼒计算

# 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))


[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Sk8cZFRs-1663076050925)(https://www.yuucn.com/wp-content/uploads/2023/03/1680221041-e8de267006956e9.svg)]

与编码器的⾃注意⼒的情况类似,通过指定输⼊序列的有效⻓度,输出序列的查询不会与输⼊序列中填充位
置的词元进⾏注意⼒计算

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))


[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Zw7NnUJj-1663076050925)(https://www.yuucn.com/wp-content/uploads/2023/03/1680221047-e8de267006956e9.svg)]

管transformer架构是为了“序列到序列”的学习⽽提出的,但正如我们将在本书后⾯提及的那样,transformer编码器或transformer解码器通常被单独⽤于不同的深度学习任务中

7 - 小结

  • transformer是编码器-解码器架构的⼀个实践,尽管在实际情况中编码器或解码器可以单独使⽤
  • 在transformer中,多头⾃注意⼒⽤于表⽰输⼊序列和输出序列,不过解码器必须通过掩蔽机制来保留⾃回归属性
  • transformer中的残差连接和层规范化是训练⾮常深度模型的重要⼯具
  • transformer模型中基于位置的前馈⽹络使⽤同⼀个多层感知机,作⽤是对所有序列位置的表⽰进⾏转换