Transformer 结构介绍

王 茂南 2022年10月24日07:58:14
评论
6683字阅读22分16秒
摘要本文会介绍 Transformer 的结构,主要包含 Encoder 和 Decoder 部分,其中包含的知识点有 self-attention,cross-attention,layer norm 等。

简介

在前面的内容中,我们了解了自注意力和位置编码注意力分数多头注意力。在有了所有这些知识之后,我们就可以开始学习「Transformer 的结构」了。

Seq2Seq 一样,Transformer 仍然是使用 EncoderDecoder 的结构;但是与 Seq2Seq 不一样的是,这里不使用 RNN 的架构,而是全部使用 Attention 的结构;下图展示了「Transformer 的结构」,下文会对其进行详细的分析。

Transformer 结构介绍

参考资料

 

Encoder 部分解释

首先我们对 TransformerEncoder 的部分进行说明。Encoder 部分总体来说如下所是:

Transformer 结构介绍

Positionwise Feed-Forward Networks

该层在本质上就是一个全连接层。原本的输入大小为 (batch size, sequence length, feature dimension),在经过两层的全连接层之后,输出的大小会转换为 (batch size, sequence length, ffn_num_outputs)只有最后一个维度发生了变化,相当于对每个字的信息进行转换。于是我们可以写出下面的代码:

  1. class PositionWiseFFN(nn.Module):
  2.     """Positionwise feed-forward network. 
  3.     输入是三维, 只对最后一维度作处理.
  4.     """
  5.     def __init__(self, ffn_num_hiddens, ffn_num_outputs):
  6.         super().__init__()
  7.         self.dense1 = nn.LazyLinear(ffn_num_hiddens)
  8.         self.relu = nn.ReLU()
  9.         self.dense2 = nn.LazyLinear(ffn_num_outputs)
  10.     def forward(self, X):
  11.         return self.dense2(self.relu(self.dense1(X)))

我们实际跑一个例子,下面 input_x 的大小是 (2,3,4),经过 Positionwise Feed-Forward Networks 之后输出的大小为 (2,3,8)

  1. ffn = PositionWiseFFN(4, 8) # 将 feature dimension 转换为 8
  2. ffn.eval()
  3. input_x = torch.ones((2, 3, 4))
  4. output_x = ffn(input_x)
  5. print(f'Input Shape, {input_x.shape}; \nOutput Shape, {output_x.shape};')
  6. """
  7. Input Shape, torch.Size([2, 3, 4]); 
  8. Output Shape, torch.Size([2, 3, 8]);
  9. """

 

Batch Norm 和 Layer Norm 介绍

因为在 Transformer 中会用到 Layer Norm 的相关内容,故我们在这里做一个简单的介绍。

  • Batch Norm 是将每个特征(一列),变为均值为 0,方差为 1;
  • Layer Norm 是将每个样本,变为均值为 0,方差为 1。相当于是对特征进行标准化;

下面看一个具体的数值例子。

  1. ln = nn.LayerNorm(2)
  2. bn = nn.LazyBatchNorm1d()
  3. X = torch.tensor([[1, 2], [2, 3]], dtype=torch.float32)
  4. # Compute mean and variance from X in the training mode
  5. print('layer norm:', ln(X), '\nbatch norm:', bn(X))
  6. """
  7. layer norm: tensor([[-1.0000,  1.0000],
  8.         [-1.0000,  1.0000]], grad_fn=<NativeLayerNormBackward0>) 
  9. batch norm: tensor([[-1.0000, -1.0000],
  10.         [ 1.0000,  1.0000]], grad_fn=<NativeBatchNormBackward0>)
  11. """

可以看到使用 Layer Norm 则一个样本的均值是 0,而使用 Batch Norm 则是一个特征的均值为 0,也就是一列。

 

AddNorm 介绍

下面定义 Transformer 用到的 Add&Norm,其实就是包括一个「残差」和一个「Layer Norm」。代码如下所是:

  1. class AddNorm(nn.Module):
  2.     """Residual connection followed by layer normalization."""
  3.     def __init__(self, norm_shape, dropout):
  4.         super().__init__()
  5.         self.dropout = nn.Dropout(dropout)
  6.         self.ln = nn.LayerNorm(norm_shape)
  7.     def forward(self, X, Y):
  8.         """_summary_
  9.         Args:
  10.             X (_type_): 原始的输入
  11.             Y (_type_): Y 是 f(X) 的结果, 这里我们加一个 dropout
  12.         """
  13.         return self.ln(self.dropout(Y) + X)

我们重点看上面的 forward 部分,其中 Y是经过别的层输出的,X 是原始的,我们将其相加(相当于是残差)。最后用一个实际的例子,可以看到 Add&Norm 是可以保持输入的形状的:

  1. # AddNorm 是不会改变「输入」和「输出」的形状。
  2. add_norm = AddNorm(4, 0.5)
  3. output_x = add_norm(torch.ones((2, 3, 4)), torch.ones((2, 3, 4)))
  4. print(f'Output Shape, {output_x.shape}')
  5. # Output Shape, torch.Size([2, 3, 4])

 

Encoder Block 介绍

有了上面知识点的铺垫,我们就可以开始介绍 Encoder Block 了。他的整个数据流向是「Multi-head Attention --> AddNorm --> FFN --> AddNorm」。

下图简单展示了上面所说的数据流向。

Transformer 结构介绍

对于右侧的部分,下图对其进行了详细的说明。(1)首先经过了 Multi-head Attention得到了 b;(2)然后经过 AddNorm,也就是 norm(a+b);(3)接着是FFN;(4)最后对 FFN 输出的结果进行 AddNorm

Transformer 结构介绍

于是我们根据上面的思路,可以写出下面的 Encoder Block 的代码,可以自行查看 forward 部分的代码,与上面的流程是一样的:

  1. class TransformerEncoderBlock(nn.Module):
  2.     """Transformer encoder block."""
  3.     def __init__(self, num_hiddens, ffn_num_hiddens, num_heads, dropout, use_bias=False):
  4.         super().__init__()
  5.         self.attention = MultiHeadAttention(num_hiddens, num_heads, dropout, use_bias)
  6.         self.addnorm1 = AddNorm(num_hiddens, dropout)
  7.         self.ffn = PositionWiseFFN(ffn_num_hiddens, num_hiddens)
  8.         self.addnorm2 = AddNorm(num_hiddens, dropout)
  9.     def forward(self, X, valid_lens):
  10.         # 计算 self attention 就是 query, key, value 都是 X
  11.         self_ttention_result = self.attention(X, X, X, valid_lens) # self attention
  12.         Y = self.addnorm1(X, self_ttention_result) # 残差
  13.         return self.addnorm2(Y, self.ffn(Y))

我们还是找一组实际的数据来进行测试。

  1. encoder_blk = TransformerEncoderBlock(
  2.     num_hiddens=24, ffn_num_hiddens=48,
  3.     num_heads=8, dropout=0.5
  4. )
  5. encoder_blk.eval()
  6. X = torch.ones((2, 100, 24))
  7. valid_lens = torch.tensor([3, 2])
  8. Y = encoder_blk(X, valid_lens)
  9. print(f'Output Shape, {Y.shape}')
  10. # Output Shape, torch.Size([2, 100, 24])

可以看到输入和输出的大小是一样的,都是 (2, 100, 24)。这里输出和输出的大小一定是一样的,因为我们使用了 AddNorm,只有维度一样,才可以进行相加。

 

Transformer Encoder

最后将若干个 Encoder Block 叠加,就得到了 Transformer Encoder。需要注意的是,对于输入部分,我们需要对输入文字进行 「embedding」 和加上「位置编码」,也就是下面 forward 中的第一行。

  1. class TransformerEncoder(Encoder):
  2.     """Transformer encoder."""
  3.     def __init__(self, vocab_size, num_hiddens, ffn_num_hiddens,
  4.                  num_heads, num_blks, dropout, use_bias=False):
  5.         super().__init__()
  6.         self.num_hiddens = num_hiddens
  7.         self.embedding = nn.Embedding(vocab_size, num_hiddens)
  8.         self.pos_encoding = PositionalEncoding(num_hiddens, dropout)
  9.         # 加上多个 block 的数据
  10.         self.blks = nn.Sequential()
  11.         for i in range(num_blks):
  12.             self.blks.add_module(
  13.                 "block"+str(i),
  14.                 TransformerEncoderBlock(num_hiddens, ffn_num_hiddens, num_heads, dropout, use_bias)
  15.             )
  16.     def forward(self, X, valid_lens):
  17.         # Since positional encoding values are between -1 and 1, 
  18.         # the embedding values are multiplied by the square root of the embedding dimension to rescale before they are summed up
  19.         # 使得 embedding 的结果的值和 position embedding 的值差不多大
  20.         X = self.pos_encoding(self.embedding(X) * math.sqrt(self.num_hiddens))
  21.         self.attention_weights = [None] * len(self.blks)
  22.         for i, blk in enumerate(self.blks):
  23.             X = blk(X, valid_lens)
  24.             self.attention_weights[i] = blk.attention.attention.attention_weights
  25.         return X

需要注意,整个 Transformer Encoder 也是不会改变数据的大小,只会改变最后一个维度。例如下面的例子中,我们输入的大小是 (2, 100),输出的大小是 (2, 100, 24),相当于是每个字有 24 维度的特征大小。

  1. encoder = TransformerEncoder(200, 24, 48, 8, 2, 0.5)
  2. X = torch.ones((2, 100), dtype=torch.long)
  3. valid_lens = torch.tensor([3, 2])
  4. Y = encoder(X, valid_lens)
  5. print(f'Output Shape, {Y.shape}') # 最后每个字多一个 24 维度的输出
  6. # Output Shape, torch.Size([2, 100, 24])

 

Decoder 部分解释

Mask Self-Attention

Decoder 部分,我们不能像在 Encoder 的时候一样,看到全部的输入,因此我们每次计算 attention 的时候,需要遮住一部分。

如下图所是,计算「b1」的时候只能使用「a1」;计算「b2」的时候,使用了「a1a2」;计算「b3」的时候,使用了「a1a2a3」;计算「b4」的时候,使用了「a1a2a3a4」;

Transformer 结构介绍

例如计算「b2」的时候,由于只使用了「a1a2」,因此此时的计算流程如下所是:

Transformer 结构介绍

 

Cross Attention

Deocder Block 中,包含两个 Multi-head Attention,其中第一个是 Self-Attention,这个与前面介绍的一样。第二个是 Cross Attention,他的 key-value 是来自 Encoder 的。Cross Attention如下所是:

Transformer 结构介绍

因为有多层的 Encoder LayerDecoder Layer,因此 Cross Attention 可以有不同的形式,也就是不同层之间如何进行 Cross Attention。下图进行了简单的概括:

Transformer 结构介绍

下图举了一个例子来说明 Cross Attention 是如何进行计算的。我们将 qEncoder 的输出计算「注意力分数」,并得到 v,最后将 v 通过全连接层,来输出最终的概率即可:

Transformer 结构介绍

关于 Transformer Decoder 部分的代码与 Encoder 部分是类似的,因此我们就不写在这里了。完整的代码可以查看 Attention-mechanisms-and-transformers。里面涉及了比较多的对数据的处理,例如如何处理 mask 等,可以详细去看一下代码。

  • 微信公众号
  • 关注微信公众号
  • weinxin
  • QQ群
  • 我们的QQ群号
  • weinxin
王 茂南
  • 本文由 发表于 2022年10月24日07:58:14
  • 转载请务必保留本文链接:https://mathpretty.com/15528.html
匿名

发表评论

匿名网友 填写信息

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: