文章目录

    • 10.7. Transformer
      • 10.7.1. 模型
      • 10.7.2. 基于位置的前馈网络
      • 10.7.3. 残差连接和层规范化
      • 10.7.4. 编码器
      • 10.7.5. 解码器
      • 10.7.6. 训练
      • 10.7.7. 小结

10.7. Transformer

transformer模型完全基于注意力机制,没有任何卷积层或循环神经网络层,已经推广到各种现代的深度学习中,例如语言、视觉、语音和强化学习领域。

10.7.1. 模型

Transformer作为编码器-解码器架构的一个实例,其整体架构图在 图中展示

transformer的编码器和解码器是基于自注意力的模块叠加而成的,源(输入)序列和目标(输出)序列的嵌入(embedding)表示将加上位置编码(positional encoding),再分别输入到编码器和解码器中。

从宏观角度来看,transformer的编码器是由多个相同的层叠加而成的,每个层都有两个子层(子层表示为)。
第一个子层是多头自注意力(multi-head self-attention)汇聚;第二个子层是基于位置的前馈网络(positionwise feed-forward network)。
具体来说,在计算编码器的自注意力时,查询、键和值都来自前一个编码器层的输出。

ransformer解码器也是由多个相同的层叠加而成的,并且层中使用了残差连接和层规范化。
除了编码器中描述的两个子层之外,解码器还在这两个子层之间插入了第三个子层,称为编码器-解码器注意力(encoder-decoder attention)层。
在编码器-解码器注意力中,查询来自前一个解码器层的输出,而键和值来自整个编码器的输出。
在解码器自注意力中,查询、键和值都来自上一个解码器层的输出。但是,解码器中的每个位置只能考虑该位置之前的所有位置。
这种掩蔽(masked)注意力保留了自回归(auto-regressive)属性,确保预测仅依赖于已生成的输出词元。

10.7.2. 基于位置的前馈网络

基于位置的前馈网络对序列中的所有位置的表示进行变换时使用的是同一个多层感知机(MLP),这就是称前馈网络是基于位置的(positionwise)的原因。
在下面的实现中,输入X的形状(批量大小,时间步数或序列长度,隐单元数或特征维度)将被一个两层的感知机转换成形状为(批量大小,时间步数,ffn_num_outputs)的输出张量。

#@saveclass 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]# resulttensor([[-1.2386, -0.9917,  0.4708, -0.4516, -0.3069,  0.0447,  0.9740,  0.1313],        [-1.2386, -0.9917,  0.4708, -0.4516, -0.3069,  0.0447,  0.9740,  0.1313],        [-1.2386, -0.9917,  0.4708, -0.4516, -0.3069,  0.0447,  0.9740,  0.1313]],       grad_fn=)

10.7.3. 残差连接和层规范化

“加法和规范化(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), '\nbatch norm:', bn(X))# resultlayer norm: tensor([[-1.0000,  1.0000],        [-1.0000,  1.0000]], grad_fn=)batch norm: tensor([[-1.0000, -1.0000],        [ 1.0000,  1.0000]], grad_fn=)        # 现在我们可以使用残差连接和层规范化来实现AddNorm类。暂退法也被作为正则化方法使用。#@saveclass 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# resulttorch.Size([2, 3, 4])

10.7.4. 编码器

有了组成transformer编码器的基础组件,现在可以先实现编码器中的一个层。下面的EncoderBlock类包含两个子层:多头自注意力和基于位置的前馈网络,这两个子层都使用了残差连接和紧随的层规范化。

#@saveclass 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))        # transformer编码器中的任何层都不会改变其输入的形状。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# resulttorch.Size([2, 100, 24])# 在实现下面的transformer编码器的代码中,我们堆叠了num_layers个EncoderBlock类的实例。由于我们使用的是值范围在和之间的固定位置编码,因此通过学习得到的输入的嵌入表示的值需要先乘以嵌入维度的平方根进行重新缩放,然后再与位置编码相加。#@saveclass 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# resulttorch.Size([2, 100, 24])

10.7.5. 解码器

,transformer解码器也是由多个相同的层组成。在DecoderBlock类中实现的每个层包含了三个子层:解码器自注意力、“编码器-解码器”注意力和基于位置的前馈网络。这些子层也都被残差连接和紧随的层规范化围绕。

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# resulttorch.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

10.7.6. 训练

依照transformer架构来实例化编码器-解码器模型。在这里,指定transformer的编码器和解码器都是2层,都使用4头注意力。

num_hiddens, num_layers, dropout, batch_size, num_steps = 32, 2, 0.1, 64, 10lr, num_epochs, device = 0.005, 200, d2l.try_gpu()ffn_num_input, ffn_num_hiddens, num_heads = 32, 64, 4key_size, query_size, value_size = 32, 32, 32norm_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)# resultloss 0.033, 5993.4 tokens/sec on cuda:0

训练结束后,使用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}')# resultgo . => va !,  bleu 1.000i lost . => je suis avons été battues .,  bleu 0.000he's calm . => il est malade .,  bleu 0.658i'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# resulttorch.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))

为了可视化解码器的自注意力权重和“编码器-解码器”的注意力权重,我们需要完成更多的数据操作工作。例如,我们用零填充被掩蔽住的注意力权重。
值得注意的是,解码器的自注意力权重和“编码器-解码器”的注意力权重都有相同的查询:即以序列开始词元(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# result(torch.Size([2, 4, 6, 10]), torch.Size([2, 4, 6, 10]))# 由于解码器自注意力的自回归属性,查询不会对当前位置之后的“键-值”对进行注意力计算。# Plusonetoincludethebeginning-of-sequencetokend2l.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))

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

10.7.7. 小结

  • transformer是编码器-解码器架构的一个实践,尽管在实际情况中编码器或解码器可以单独使用。

  • 在transformer中,多头自注意力用于表示输入序列和输出序列,不过解码器必须通过掩蔽机制来保留自回归属性。

  • transformer中的残差连接和层规范化是训练非常深度模型的重要工具。

  • transformer模型中基于位置的前馈网络使用同一个多层感知机,作用是对所有序列位置的表示进行转换。