.. _sec_transformer: Transformer =========== :numref:`subsec_cnn-rnn-self-attention`\ 中比较了卷积神经网络(CNN)、循环神经网络(RNN)和自注意力(self-attention)。值得注意的是,自注意力同时具有并行计算和最短的最大路径长度这两个优势。因此,使用自注意力来设计深度架构是很有吸引力的。对比之前仍然依赖循环神经网络实现输入表示的自注意力模型 :cite:`Cheng.Dong.Lapata.2016,Lin.Feng.Santos.ea.2017,Paulus.Xiong.Socher.2017`\ ,Transformer模型完全基于注意力机制,没有任何卷积层或循环神经网络层 :cite:`Vaswani.Shazeer.Parmar.ea.2017`\ 。尽管Transformer最初是应用于在文本数据上的序列到序列学习,但现在已经推广到各种现代的深度学习中,例如语言、视觉、语音和强化学习领域。 模型 ---- Transformer作为编码器-解码器架构的一个实例,其整体架构图在 :numref:`fig_transformer`\ 中展示。正如所见到的,Transformer是由编码器和解码器组成的。与 :numref:`fig_s2s_attention_details`\ 中基于Bahdanau注意力实现的序列到序列的学习相比,Transformer的编码器和解码器是基于自注意力的模块叠加而成的,源(输入)序列和目标(输出)序列的\ *嵌入*\ (embedding)表示将加上\ *位置编码*\ (positional encoding),再分别输入到编码器和解码器中。 .. _fig_transformer: .. figure:: ../img/transformer.svg :width: 500px transformer架构 图 :numref:`fig_transformer`\ 中概述了Transformer的架构。从宏观角度来看,Transformer的编码器是由多个相同的层叠加而成的,每个层都有两个子层(子层表示为\ :math:`\mathrm{sublayer}`\ )。第一个子层是\ *多头自注意力*\ (multi-head self-attention)汇聚;第二个子层是\ *基于位置的前馈网络*\ (positionwise feed-forward network)。具体来说,在计算编码器的自注意力时,查询、键和值都来自前一个编码器层的输出。受 :numref:`sec_resnet`\ 中残差网络的启发,每个子层都采用了\ *残差连接*\ (residual connection)。在Transformer中,对于序列中任何位置的任何输入\ :math:`\mathbf{x} \in \mathbb{R}^d`\ ,都要求满足\ :math:`\mathrm{sublayer}(\mathbf{x}) \in \mathbb{R}^d`\ ,以便残差连接满足\ :math:`\mathbf{x} + \mathrm{sublayer}(\mathbf{x}) \in \mathbb{R}^d`\ 。在残差连接的加法计算之后,紧接着应用\ *层规范化*\ (layer normalization) :cite:`Ba.Kiros.Hinton.2016`\ 。因此,输入序列对应的每个位置,Transformer编码器都将输出一个\ :math:`d`\ 维表示向量。 Transformer解码器也是由多个相同的层叠加而成的,并且层中使用了残差连接和层规范化。除了编码器中描述的两个子层之外,解码器还在这两个子层之间插入了第三个子层,称为\ *编码器-解码器注意力*\ (encoder-decoder attention)层。在编码器-解码器注意力中,查询来自前一个解码器层的输出,而键和值来自整个编码器的输出。在解码器自注意力中,查询、键和值都来自上一个解码器层的输出。但是,解码器中的每个位置只能考虑该位置之前的所有位置。这种\ *掩蔽*\ (masked)注意力保留了\ *自回归*\ (auto-regressive)属性,确保预测仅依赖于已生成的输出词元。 在此之前已经描述并实现了基于缩放点积多头注意力 :numref:`sec_multihead-attention`\ 和位置编码 :numref:`subsec_positional-encoding`\ 。接下来将实现Transformer模型的剩余部分。 .. raw:: html
mindsporepytorch
.. raw:: html
.. raw:: latex \diilbookstyleinputcell .. code:: python import math import mindspore import pandas as pd from mindspore import nn from d2l import mindspore as d2l .. raw:: html
.. raw:: html
.. raw:: latex \diilbookstyleinputcell .. code:: python import math import pandas as pd import torch from torch import nn from d2l import torch as d2l .. raw:: html
.. raw:: html
基于位置的前馈网络 ------------------ 基于位置的前馈网络对序列中的所有位置的表示进行变换时使用的是同一个多层感知机(MLP),这就是称前馈网络是\ *基于位置的*\ (positionwise)的原因。在下面的实现中,输入\ ``X``\ 的形状(批量大小,时间步数或序列长度,隐单元数或特征维度)将被一个两层的感知机转换成形状为(批量大小,时间步数,\ ``ffn_num_outputs``\ )的输出张量。 .. raw:: html
mindsporepytorch
.. raw:: html
.. raw:: latex \diilbookstyleinputcell .. code:: python #@save class PositionWiseFFN(nn.Cell): """基于位置的前馈网络""" def __init__(self, ffn_num_input, ffn_num_hiddens, ffn_num_outputs, **kwargs): super(PositionWiseFFN, self).__init__(**kwargs) self.dense1 = d2l.Dense(ffn_num_input, ffn_num_hiddens) self.relu = nn.ReLU() self.dense2 = d2l.Dense(ffn_num_hiddens, ffn_num_outputs) def construct(self, X): return self.dense2(self.relu(self.dense1(X))) .. raw:: html
.. raw:: html
.. raw:: latex \diilbookstyleinputcell .. code:: python #@save 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))) .. raw:: html
.. raw:: html
下面的例子显示,改变张量的最里层维度的尺寸,会改变成基于位置的前馈网络的输出尺寸。因为用同一个多层感知机对所有位置上的输入进行变换,所以当所有这些位置的输入相同时,它们的输出也是相同的。 .. raw:: html
mindsporepytorch
.. raw:: html
.. raw:: latex \diilbookstyleinputcell .. code:: python ffn = PositionWiseFFN(4, 4, 8) ffn.set_train(False) ffn(d2l.ones((2, 3, 4)))[0] .. raw:: latex \diilbookstyleoutputcell .. parsed-literal:: :class: output Tensor(shape=[3, 8], dtype=Float32, value= [[-9.54746604e-01, -9.13365006e-01, 1.70805246e-01 ... -4.93909061e-01, -6.87687397e-01, 2.66076714e-01], [-9.54746604e-01, -9.13365006e-01, 1.70805246e-01 ... -4.93909061e-01, -6.87687397e-01, 2.66076714e-01], [-9.54746604e-01, -9.13365006e-01, 1.70805246e-01 ... -4.93909061e-01, -6.87687397e-01, 2.66076714e-01]]) .. raw:: html
.. raw:: html
.. raw:: latex \diilbookstyleinputcell .. code:: python ffn = PositionWiseFFN(4, 4, 8) ffn.eval() ffn(torch.ones((2, 3, 4)))[0] .. raw:: latex \diilbookstyleoutputcell .. parsed-literal:: :class: output tensor([[-0.1577, 0.0456, -0.3540, -0.1331, 0.0241, -0.3141, -0.0422, 0.1085], [-0.1577, 0.0456, -0.3540, -0.1331, 0.0241, -0.3141, -0.0422, 0.1085], [-0.1577, 0.0456, -0.3540, -0.1331, 0.0241, -0.3141, -0.0422, 0.1085]], grad_fn=) .. raw:: html
.. raw:: html
残差连接和层规范化 ------------------ 现在让我们关注 :numref:`fig_transformer`\ 中的\ *加法和规范化*\ (add&norm)组件。正如在本节开头所述,这是由残差连接和紧随其后的层规范化组成的。两者都是构建有效的深度架构的关键。 :numref:`sec_batch_norm`\ 中解释了在一个小批量的样本内基于批量规范化对数据进行重新中心化和重新缩放的调整。层规范化和批量规范化的目标相同,但层规范化是基于特征维度进行规范化。尽管批量规范化在计算机视觉中被广泛应用,但在自然语言处理任务中(输入通常是变长序列)批量规范化通常不如层规范化的效果好。 以下代码对比不同维度的层规范化和批量规范化的效果。 .. raw:: html
mindsporepytorch
.. raw:: html
.. raw:: latex \diilbookstyleinputcell .. code:: python ln = d2l.LayerNorm((2,)) bn = nn.BatchNorm1d(2, use_batch_statistics=True) X = mindspore.tensor([[1, 2], [2, 3]], dtype=mindspore.float32) # 在训练模式下计算X的均值和方差 print('layer norm:', ln(X), '\nbatch norm:', bn(X)) .. raw:: latex \diilbookstyleoutputcell .. parsed-literal:: :class: output layer norm: [[-0.99998 0.99998] [-0.99998 0.99998]] batch norm: [[-0.99998 -0.99998] [ 0.99998 0.99998]] .. raw:: html
.. raw:: html
.. raw:: latex \diilbookstyleinputcell .. code:: 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)) .. raw:: latex \diilbookstyleoutputcell .. parsed-literal:: :class: output layer 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=) .. raw:: html
.. raw:: html
现在可以使用残差连接和层规范化来实现\ ``AddNorm``\ 类。暂退法也被作为正则化方法使用。 .. raw:: html
mindsporepytorch
.. raw:: html
.. raw:: latex \diilbookstyleinputcell .. code:: python #@save class AddNorm(nn.Cell): """残差连接后进行层规范化。""" def __init__(self, normalized_shape, dropout, **kwargs): super(AddNorm, self).__init__(**kwargs) self.dropout = nn.Dropout(p=dropout) self.ln = d2l.LayerNorm(normalized_shape) def construct(self, X, Y): return self.ln(self.dropout(Y) + X) .. raw:: html
.. raw:: html
.. raw:: latex \diilbookstyleinputcell .. code:: python #@save 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) .. raw:: html
.. raw:: html
残差连接要求两个输入的形状相同,以便加法操作后输出张量的形状相同。 .. raw:: html
mindsporepytorch
.. raw:: html
.. raw:: latex \diilbookstyleinputcell .. code:: python add_norm = AddNorm((3, 4), 0.5) add_norm.set_train(False) add_norm(d2l.ones((2, 3, 4)), d2l.ones((2, 3, 4))).shape .. raw:: latex \diilbookstyleoutputcell .. parsed-literal:: :class: output (2, 3, 4) .. raw:: html
.. raw:: html
.. raw:: latex \diilbookstyleinputcell .. code:: python add_norm = AddNorm([3, 4], 0.5) add_norm.eval() add_norm(torch.ones((2, 3, 4)), torch.ones((2, 3, 4))).shape .. raw:: latex \diilbookstyleoutputcell .. parsed-literal:: :class: output torch.Size([2, 3, 4]) .. raw:: html
.. raw:: html
编码器 ------ 有了组成Transformer编码器的基础组件,现在可以先实现编码器中的一个层。下面的\ ``EncoderBlock``\ 类包含两个子层:多头自注意力和基于位置的前馈网络,这两个子层都使用了残差连接和紧随的层规范化。 .. raw:: html
mindsporepytorch
.. raw:: html
.. raw:: latex \diilbookstyleinputcell .. code:: python #@save class EncoderBlock(nn.Cell): """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 construct(self, X, valid_lens): Y = self.addnorm1(X, self.attention(X, X, X, valid_lens)) return self.addnorm2(Y, self.ffn(Y)) .. raw:: html
.. raw:: html
.. raw:: latex \diilbookstyleinputcell .. code:: python #@save 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)) .. raw:: html
.. raw:: html
正如从代码中所看到的,Transformer编码器中的任何层都不会改变其输入的形状。 .. raw:: html
mindsporepytorch
.. raw:: html
.. raw:: latex \diilbookstyleinputcell .. code:: python X = d2l.ones((2, 100, 24)) valid_lens = mindspore.tensor([3, 2]) encoder_blk = EncoderBlock(24, 24, 24, 24, [100, 24], 24, 48, 8, 0.5) encoder_blk.set_train(False) encoder_blk(X, valid_lens).shape .. raw:: latex \diilbookstyleoutputcell .. parsed-literal:: :class: output (2, 100, 24) .. raw:: html
.. raw:: html
.. raw:: latex \diilbookstyleinputcell .. code:: python 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 .. raw:: latex \diilbookstyleoutputcell .. parsed-literal:: :class: output torch.Size([2, 100, 24]) .. raw:: html
.. raw:: html
下面实现的Transformer编码器的代码中,堆叠了\ ``num_layers``\ 个\ ``EncoderBlock``\ 类的实例。由于这里使用的是值范围在\ :math:`-1`\ 和\ :math:`1`\ 之间的固定位置编码,因此通过学习得到的输入的嵌入表示的值需要先乘以嵌入维度的平方根进行重新缩放,然后再与位置编码相加。 .. raw:: html
mindsporepytorch
.. raw:: html
.. raw:: latex \diilbookstyleinputcell .. code:: python #@save 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 = d2l.Embedding(vocab_size, num_hiddens, embedding_table='normal') self.pos_encoding = d2l.PositionalEncoding(num_hiddens, dropout) self.blks = nn.SequentialCell() for i in range(num_layers): self.blks.append(EncoderBlock(key_size, query_size, value_size, num_hiddens, norm_shape, ffn_num_input, ffn_num_hiddens, num_heads, dropout, use_bias)) def construct(self, X, valid_lens): # 因为位置编码值在-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 return X .. raw:: html
.. raw:: html
.. raw:: latex \diilbookstyleinputcell .. code:: python #@save 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 .. raw:: html
.. raw:: html
下面我们指定了超参数来创建一个两层的Transformer编码器。 Transformer编码器输出的形状是(批量大小,时间步数目,\ ``num_hiddens``\ )。 .. raw:: html
mindsporepytorch
.. raw:: html
.. raw:: latex \diilbookstyleinputcell .. code:: python encoder = TransformerEncoder( 200, 24, 24, 24, 24, [100, 24], 24, 48, 8, 2, 0.5) encoder.set_train(False) encoder(d2l.ones((2, 100), mindspore.int32), valid_lens).shape .. raw:: latex \diilbookstyleoutputcell .. parsed-literal:: :class: output (2, 100, 24) .. raw:: html
.. raw:: html
.. raw:: latex \diilbookstyleinputcell .. code:: python 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 .. raw:: latex \diilbookstyleoutputcell .. parsed-literal:: :class: output torch.Size([2, 100, 24]) .. raw:: html
.. raw:: html
解码器 ------ 如 :numref:`fig_transformer`\ 所示,Transformer解码器也是由多个相同的层组成。在\ ``DecoderBlock``\ 类中实现的每个层包含了三个子层:解码器自注意力、“编码器-解码器”注意力和基于位置的前馈网络。这些子层也都被残差连接和紧随的层规范化围绕。 正如在本节前面所述,在掩蔽多头解码器自注意力层(第一个子层)中,查询、键和值都来自上一个解码器层的输出。关于\ *序列到序列模型*\ (sequence-to-sequence model),在训练阶段,其输出序列的所有位置(时间步)的词元都是已知的;然而,在预测阶段,其输出序列的词元是逐个生成的。因此,在任何解码器时间步中,只有生成的词元才能用于解码器的自注意力计算中。为了在解码器中保留自回归的属性,其掩蔽自注意力设定了参数\ ``dec_valid_lens``\ ,以便任何查询都只会与解码器中所有已经生成词元的位置(即直到该查询位置为止)进行注意力计算。 .. raw:: html
mindsporepytorch
.. raw:: html
.. raw:: latex \diilbookstyleinputcell .. code:: python class DecoderBlock(nn.Cell): """解码器中第 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 construct(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 = d2l.concat((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 = d2l.tile(d2l.arange(1, num_steps + 1), (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 .. raw:: html
.. raw:: html
.. raw:: latex \diilbookstyleinputcell .. code:: 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): 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 .. raw:: html
.. raw:: html
为了便于在“编码器-解码器”注意力中进行缩放点积计算和残差连接中进行加法计算,编码器和解码器的特征维度都是\ ``num_hiddens``\ 。 .. raw:: html
mindsporepytorch
.. raw:: html
.. raw:: latex \diilbookstyleinputcell .. code:: python decoder_blk = DecoderBlock(24, 24, 24, 24, [100, 24], 24, 48, 8, 0.5, 0) decoder_blk.set_train(False) X = d2l.ones((2, 100, 24)) state = [encoder_blk(X, valid_lens), valid_lens, [None]] decoder_blk(X, state)[0].shape .. raw:: latex \diilbookstyleoutputcell .. parsed-literal:: :class: output (2, 100, 24) .. raw:: html
.. raw:: html
.. raw:: latex \diilbookstyleinputcell .. code:: python 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 .. raw:: latex \diilbookstyleoutputcell .. parsed-literal:: :class: output torch.Size([2, 100, 24]) .. raw:: html
.. raw:: html
现在我们构建了由\ ``num_layers``\ 个\ ``DecoderBlock``\ 实例组成的完整的Transformer解码器。最后,通过一个全连接层计算所有\ ``vocab_size``\ 个可能的输出词元的预测值。解码器的自注意力权重和编码器解码器注意力权重都被存储下来,方便日后可视化的需要。 .. raw:: html
mindsporepytorch
.. raw:: html
.. raw:: latex \diilbookstyleinputcell .. code:: python class TransformerDecoder(d2l.Decoder): 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 = d2l.Embedding(vocab_size, num_hiddens, embedding_table='normal') self.pos_encoding = d2l.PositionalEncoding(num_hiddens, dropout) self.blks = nn.SequentialCell() for i in range(num_layers): self.blks.append(DecoderBlock(key_size, query_size, value_size, num_hiddens, norm_shape, ffn_num_input, ffn_num_hiddens, num_heads, dropout, i)) self.dense = d2l.Dense(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 construct(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 .. raw:: html
.. raw:: html
.. raw:: latex \diilbookstyleinputcell .. code:: python 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 .. raw:: html
.. raw:: html
训练 ---- 依照Transformer架构来实例化编码器-解码器模型。在这里,指定Transformer的编码器和解码器都是2层,都使用4头注意力。与 :numref:`sec_seq2seq_training`\ 类似,为了进行序列到序列的学习,下面在“英语-法语”机器翻译数据集上训练Transformer模型。 .. raw:: html
mindsporepytorch
.. raw:: html
.. raw:: latex \diilbookstyleinputcell .. code:: python num_hiddens, num_layers, dropout, batch_size, num_steps = 32, 2, 0.1, 64, 10 lr, num_epochs = 0.005, 200 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) .. raw:: latex \diilbookstyleoutputcell .. parsed-literal:: :class: output loss 0.014, 11063.7 tokens/sec .. figure:: output_transformer_5722f1_120_1.png .. raw:: html
.. raw:: html
.. raw:: latex \diilbookstyleinputcell .. code:: python 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) .. raw:: latex \diilbookstyleoutputcell .. parsed-literal:: :class: output loss 0.031, 33027.8 tokens/sec on cuda:0 .. figure:: output_transformer_5722f1_123_1.svg .. raw:: html
.. raw:: html
训练结束后,使用Transformer模型将一些英语句子翻译成法语,并且计算它们的BLEU分数。 .. raw:: html
mindsporepytorch
.. raw:: html
.. raw:: latex \diilbookstyleinputcell .. code:: 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, True) print(f'{eng} => {translation}, ', f'bleu {d2l.bleu(translation, fra, k=2):.3f}') .. raw:: latex \diilbookstyleoutputcell .. parsed-literal:: :class: output 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 .. raw:: html
.. raw:: html
.. raw:: latex \diilbookstyleinputcell .. code:: 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}') .. raw:: latex \diilbookstyleoutputcell .. parsed-literal:: :class: output 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 .. raw:: html
.. raw:: html
当进行最后一个英语到法语的句子翻译工作时,让我们可视化Transformer的注意力权重。编码器自注意力权重的形状为(编码器层数,注意力头数,\ ``num_steps``\ 或查询的数目,\ ``num_steps``\ 或“键-值”对的数目)。 .. raw:: html
mindsporepytorch
.. raw:: html
.. raw:: latex \diilbookstyleinputcell .. code:: python enc_attention_weights = d2l.concat(net.encoder.attention_weights, 0).reshape((num_layers, num_heads, -1, num_steps)) enc_attention_weights.shape .. raw:: latex \diilbookstyleoutputcell .. parsed-literal:: :class: output (2, 4, 10, 10) .. raw:: html
.. raw:: html
.. raw:: latex \diilbookstyleinputcell .. code:: python enc_attention_weights = torch.cat(net.encoder.attention_weights, 0).reshape((num_layers, num_heads, -1, num_steps)) enc_attention_weights.shape .. raw:: latex \diilbookstyleoutputcell .. parsed-literal:: :class: output torch.Size([2, 4, 10, 10]) .. raw:: html
.. raw:: html
在编码器的自注意力中,查询和键都来自相同的输入序列。因为填充词元是不携带信息的,因此通过指定输入序列的有效长度可以避免查询与使用填充词元的位置计算注意力。接下来,将逐行呈现两层多头注意力的权重。每个注意力头都根据查询、键和值的不同的表示子空间来表示不同的注意力。 .. raw:: html
mindsporepytorch
.. raw:: html
.. raw:: latex \diilbookstyleinputcell .. code:: python d2l.show_heatmaps( enc_attention_weights, xlabel='Key positions', ylabel='Query positions', titles=['Head %d' % i for i in range(1, 5)], figsize=(7, 3.5)) .. figure:: output_transformer_5722f1_147_0.png .. raw:: html
.. raw:: html
.. raw:: latex \diilbookstyleinputcell .. code:: 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)) .. figure:: output_transformer_5722f1_150_0.svg .. raw:: html
.. raw:: html
为了可视化解码器的自注意力权重和“编码器-解码器”的注意力权重,我们需要完成更多的数据操作工作。例如用零填充被掩蔽住的注意力权重。值得注意的是,解码器的自注意力权重和“编码器-解码器”的注意力权重都有相同的查询:即以\ *序列开始词元*\ (beginning-of-sequence,BOS)打头,再与后续输出的词元共同组成序列。 .. raw:: html
mindsporepytorch
.. raw:: html
.. raw:: latex \diilbookstyleinputcell .. code:: python dec_attention_weights_2d = [head[0].asnumpy() for step in dec_attention_weight_seq for attn in step for blk in attn for head in blk] dec_attention_weights_filled = mindspore.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.transpose(1, 2, 3, 0, 4) dec_self_attention_weights.shape, dec_inter_attention_weights.shape .. raw:: latex \diilbookstyleoutputcell .. parsed-literal:: :class: output ((2, 4, 6, 10), (2, 4, 6, 10)) .. raw:: html
.. raw:: html
.. raw:: latex \diilbookstyleinputcell .. code:: python 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 .. raw:: latex \diilbookstyleoutputcell .. parsed-literal:: :class: output (torch.Size([2, 4, 6, 10]), torch.Size([2, 4, 6, 10])) .. raw:: html
.. raw:: html
由于解码器自注意力的自回归属性,查询不会对当前位置之后的“键-值”对进行注意力计算。 .. raw:: html
mindsporepytorch
.. raw:: html
.. raw:: latex \diilbookstyleinputcell .. code:: 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)) .. figure:: output_transformer_5722f1_165_0.png .. raw:: html
.. raw:: html
.. raw:: latex \diilbookstyleinputcell .. code:: 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)) .. figure:: output_transformer_5722f1_168_0.svg .. raw:: html
.. raw:: html
与编码器的自注意力的情况类似,通过指定输入序列的有效长度,输出序列的查询不会与输入序列中填充位置的词元进行注意力计算。 .. raw:: html
mindsporepytorch
.. raw:: html
.. raw:: latex \diilbookstyleinputcell .. code:: 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)) .. figure:: output_transformer_5722f1_174_0.png .. raw:: html
.. raw:: html
.. raw:: latex \diilbookstyleinputcell .. code:: 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)) .. figure:: output_transformer_5722f1_177_0.svg .. raw:: html
.. raw:: html
尽管Transformer架构是为了\ *序列到序列*\ 的学习而提出的,但正如本书后面将提及的那样,Transformer编码器或Transformer解码器通常被单独用于不同的深度学习任务中。 小结 ---- - Transformer是编码器-解码器架构的一个实践,尽管在实际情况中编码器或解码器可以单独使用。 - 在Transformer中,多头自注意力用于表示输入序列和输出序列,不过解码器必须通过掩蔽机制来保留自回归属性。 - Transformer中的残差连接和层规范化是训练非常深度模型的重要工具。 - Transformer模型中基于位置的前馈网络使用同一个多层感知机,作用是对所有序列位置的表示进行转换。 练习 ---- 1. 在实验中训练更深的Transformer将如何影响训练速度和翻译效果? 2. 在Transformer中使用加性注意力取代缩放点积注意力是不是个好办法?为什么? 3. 对于语言模型,应该使用Transformer的编码器还是解码器,或者两者都用?如何设计? 4. 如果输入序列很长,Transformer会面临什么挑战?为什么? 5. 如何提高Transformer的计算速度和内存使用效率?提示:可以参考论文 :cite:`Tay.Dehghani.Bahri.ea.2020`\ 。 6. 如果不使用卷积神经网络,如何设计基于Transformer模型的图像分类任务?提示:可以参考Vision Transformer :cite:`Dosovitskiy.Beyer.Kolesnikov.ea.2021`\ 。 .. raw:: html
mindsporepytorch
.. raw:: html
`讨论 `__ .. raw:: html
.. raw:: html
`讨论 `__ .. raw:: html
.. raw:: html