Transformer 系列三:Encoder编码器和Decoder解码器

Transformer使用了"Encoder-Decoder" 编码器-解码器的结构,这种结构被广泛应用于处理序列到序列(seq2seq)的学习任务中。这种结构由编码器和解码器两大部分组成,编码(encoding)是一个模式提取的过程,将输入句子的特征提取出来,存储在一个中间隐状态空间(hidden state);而解码(decoding)则是将encoder提取出来的特征进行重建,解码成为我们期望的输出。如翻译任务:将输入的英语经过编码器编码成中间表示,再使用解码器将这个中间表示解码成中文。


编码器 Encoder

Encoder的主要作用是进行特征提取,这样做是因为原始输入中包含一些无用或干扰信息,这会使模型的性能和泛化性大打折扣。所以在这之前,我们通过Encoder来先对数据进行一次特征提取和挖掘,比如后面会提到Encoder里会有一个自注意力层,正如我们之前文章中提到,自注意力层可以挖掘输入内部元素直接的关系,这是模型直接接受原始输入很难做到

Encoder主要包含以下3个部分:

  • Multi-Head Attention 多头自注意力层
  • Add & Norm 残差 & Layer Normalization 层归一化
  • Position-Wise Fully Connected Feed-Forward Network 前馈神经网络(本质上是个MLP,只是名字比较炫酷)

整体流程是这样的:

  1. 首先输入进Encoder的向量会经过一个自注意力层,自注意力层会输出一个长度与输入一致,但特征维度可能不一致(也可能一致)的新特征向量 z z z
  2. 之后会再进行一个残差连接(Residual Connection)的操作,将输入 x x x z z z相加
  3. 使用Layer Normalization的方法对残差连接得到的结果进行层归一化的操作
  4. 再送入一个前馈神经网络
  5. 同样的,执行2.和3.的操作,进行一次残差和归一化处理后输出

残差连接 Residual Connection

Transformer中的残差连接(Residual Connection)是一种将输入直接加到输出上的连接方式。这种结构有助于缓解深度神经网络在训练过程中可能出现的梯度消失或梯度爆炸问题。通过引入残差结构,模型可以直接传递输入信息到深层网络,使得深层网络能够更容易地学习到输入和输出之间的映射关系。同时,残差结构也有助于提高模型的训练速度和稳定性。


归一化 Normalization

除了层归一化还有一种归一化方式也十分常见:批归一化(Batch Normalization)。这里通过对比两种归一化方式来讲解:

  • BatchNorm:一般用于CV任务,是选取一个batch(N张图片)中同一通道的特征,将其标准化。可以理解为对Batch中的所有图的每一层像素进行标准化。
  • LayerNorm:一般用于NLP任务,是选取batch中的一个样本(一个句子)在当前层的所有特征,将其标准化。可以理解为对一个句子的整体进行标准化。

Batch Norm

BatchNorm把一个batch中同一通道的所有特征(如下图红色区域)视为一个分布(有几个通道就有几个分布),并将其标准化。这意味着:

  • 不同图片的的同一通道的相对关系是保留的,即不同图片的同一通道的特征是可以比较的
  • 同一图片的不同通道的特征则是失去了可比性
这里附一段,关于BN用途的解释:

Layer Norm

LayerNorm把一个样本的所有词义向量(如下图红色部分)视为一个分布(有几个句子就有几个分布),并将其标准化。这意味着:

  • 同一句子中词义向量(上图中的V1, V2, …, VL)的相对大小是保留的,或者也可以说LayerNorm不改变词义向量的方向,只改变它的模。
  • 不同句子的词义向量则是失去了可比性。
  • 用于NLP领域解释:
    考虑两个句子,“教练,我想打篮球!” 和 “老板,我要一打包子。”。通过比较两个句子中 “打” 的词义我们可以发现,词义并非客观存在的,而是由上下文的语义决定的。因此进行标准化时不应该破坏同一句子中不同词义向量的可比性,而LayerNorm是满足这一点的,BatchNorm则是不满足这一点的。且不同句子的词义特征也不应具有可比性,LayerNorm也是能够把不同句子间的可比性消除。

Transformers使用Layer Norm的原因

Transformer使用LayerNorm而不是BatchNorm的主要原因是LayerNorm更加适合处理变长的序列数据。在Transformer中,由于输入序列的长度是可变的,因此每个序列的批量统计信息(如均值和方差)也是不同的。而BatchNorm是基于整个批量数据来计算统计信息的,这可能导致在处理变长序列时性能下降。相比之下,LayerNorm是在每个样本的维度上进行归一化的,因此不受序列长度变化的影响。

在Transformer中,LayerNorm通常被放置在多头注意力机制和前馈神经网络的输出之后。通过对这些层的输出进行归一化,可以使得后续层的输入保持在一个相对稳定的范围内,有助于模型的训练。


BatchNorm 和 LayerNorm 优缺点对比

BatchNorm的优点:

  • 加速训练过程:通过归一化输入,可以使得模型更容易收敛。

  • 提高模型稳定性:减少内部协变量偏移(Internal Covariate Shift),使得模型更加稳定。

  • 提高泛化能力:通过引入一定的正则化效果,有助于防止过拟合。

BatchNorm的缺点:

  • 对小批量数据敏感:当批量大小较小时,BatchNorm的统计信息可能不准确,导致性能下降。

  • 依赖于整个批量数据:在处理变长序列或在线学习任务时可能不适用。

LayerNorm的优点:

  • 不依赖于批次大小:LN对每个样本的所有特征进行归一化,不依赖于批次大小,这使得它在小批次或单个样本的情况下也能很好地工作,尤其适合在线学习和RNN等场景

  • 提高训练稳定性:LN通过减少内部协变量偏移,有助于模型更快地收敛,并且可以提高模型的泛化能力

  • 适用于变长序列:在处理不同长度的序列时,LN能够计算每个时间步的统计量,因此它能够很好地处理变长序列,这是BN难以做到的

  • 训练和测试一致性:LN在训练和测试时使用相同的计算方式,这意味着不需要在测试时使用移动平均统计量,这与BN不同

LayerNorm的缺点:

  • 计算复杂度:LN在计算上可能比BN更复杂,因为它需要对每个样本的特征进行归一化,这在特征维度很高时可能会增加计算负担

  • 可能的过拟合风险:LN的参数(如缩放和平移参数)可能会增加过拟合的风险,尽管这可以通过适当的正则化技术来缓解

  • 效果受限于特定场景:虽然LN在某些情况下表现优于BN,但它可能不适用于所有类型的神经网络,例如在CNN中的效果可能就不如BN


前馈神经网络 Position-Wise Fully Connected Feed Forward Network

这个名字说的十分高大上,但其实就是一个简单的两层全连接网络,用于进一步处理多头注意力机制的输出。这个网络首先通过一个线性变换将输入映射到一个更高维的空间中,然后通过一个非线性激活函数(如ReLU)增加网络的非线性能力,最后再通过另一个线性变换将输出映射回原始维度。


在自注意力层后面增加前馈神经网络层的原因

  • 特征提取:FFN通过两层全连接层(通常是线性层),对来自自注意力层的输出进行进一步的特征提取。这有助于模型学习到更深层次的、非线性的特征表示。

  • 增加模型容量:通过引入额外的参数和非线性激活函数,FFN增加了模型的容量,使得模型能够捕捉更复杂的数据模式和关系。

  • 与自注意力机制互补:自注意力机制擅长捕捉序列内部的长距离依赖关系,而FFN则专注于在给定的表示上进行特征转换。这种组合使得Transformer能够有效地处理各种语言和序列任务。

  • 提高泛化能力:FFN通过增加模型的复杂性,有助于提高模型对未见数据的泛化能力。


解码器 Decoder

Decoder的主要作用是解码,将中间隐状态空间的特征映射到期望的目标空间中,比如翻译任务就是输出期望翻译到的目标语种,而且这里的输出和最开始输入的长度是不一定一样长的,比如中文的你好,在英文中只是一个单词HELLO。解码器的主要结构和模块组成和编码器基本没有差别,除了自注意力层以外还添加了一个新的注意力层——交叉注意力层。但是,其在实际运转上却有许多的变化,包括在Attention层中引入了Mask掩码机制来配合其串行的推理机制,以及将上一个token的输出作为下一个token预测的输入等。

交叉注意力 Cross Attention

解码器模块里多出了一个注意力层——Encoder-Decoder Attention 也就是交叉注意力层(Cross Attention),交叉注意力的区别在于使用解码器的内部输出计算 K ( K e y ) K(Key) K(Key),使用编码器的输出计算 Q ( Q u e r y ) 、 V ( V a l u e ) Q(Query)、V(Value) Q(Query)V(Value) ,其余计算方式和自注意力无区别。


交叉注意力的主要作用

  • 信息融合:交叉注意力允许解码器通过注意力机制关注编码器的输出,这有助于融合来自编码器的信息
  • 上下文建模:交叉注意力使得解码器的每个位置都能够考虑到整个输入序列,从而有效地建模上下文信息。

掩码 MASK

在神经网络中,掩码是一种用于阻止模型使用输入数据中的某些部分的技术,即对模型的输入的一些部分进行置零操作。

Transformer中一共有两种掩码机制:

  • 填充掩码 Padding Mask
  • 序列掩码 Sequence Mask

填充掩码 Padding Sequence

自然语言类任务(序列化数据)的输入长度往往不像图像一般是定长的,当序列数据的长度不一致时,通常需要对短的序列进行填充(padding),以确保所有序列的长度相同,这样才能进行批处理,实现高效的并行化计算。这些填充的部分实际上是没有任何意义的,不应该对模型的学习产生影响。

在Encoder的自主力层中计算attention score时,对于输入序列中的padding部分,可以通过在attention score矩阵中对应的位置加上一个非常大的负数(如-inf),使得这些位置的softmax输出接近于0,从而实现对padding的mask操作。这样在计算加权和时,padding部分的信息就不会被考虑在内。


自回归推理方式

Transformer整体上是一个自回归架构,解码器一次只预测一个token的结果。在预测完当前token的结果后,会将这个结果作为下一次预测的输入,和编码器的结果一起送入解码器中。由于编码器输出的向量已经包含了每个token与其他token的上下文联系,所以在使用解码器预测的时候,仅使用当前及之前的信息作为输入。这里举一个例子:

Shifted Right

额外在这里提一下Shifted Right操作。Shifted Right 实质上是给输出添加起始符/结束符,方便预测第1个Token/结束预测过程。这里描述一下前几步的预测过程:

第一步:

  • 输入:“Ja” Encoder Vector + “</s>” Previous Output
  • 输出:I

第二步:

  • 输入:“Ja” + “suis” Encoder Vector + “</s>” + “I” Previous Output
  • 输出:am

第三步:

  • 输入:“Ja” + “suis” + “etudiant” Encoder Vector + “</s>” + “I” + "am"Previous Output
  • 输出:a student

序列掩码 Sequence Mask

序列编码被用于解码器的自注意力层训练过程中。训练时,因为output的全部token已经知道了 ,所以在将output作为解码器输入的时就会造成信息泄露,相当于告诉了模型的最终答案是什么。所以我们需要在每个时间步,隐藏掉当前token之后的output数据,具体的做法是产生一个上三角矩阵,上三角的值全为0。把这个矩阵作用在每个序列上,就可以达到我们的目的。


Encoder - Decoder 架构

在分别介绍完编码器和解码器后,我们来宏观的了解一下Transformer的完整架构。可以看到Transformer主要由Encoder 和 Decoder 两部分组成,但是分别在两侧两个 N × N \times N×,这代表这两部分都是由多个Encoder Block和 Decoder Block 堆叠而成的。

实际内部的图是这样的:


最终的Linear + Softmax层

这里的作用是将编码器最终输出的向量转换成自然语言。首先通过一个线性层将编码器输出映射到跟词表一样大的一个logits向量中,在对这个向量做softmax从中找出概率最大的一个index,在将这个index对应到词表里,就可以得到我们想要的最终的输出了。
在这里插入图片描述


推荐阅读

  • 20
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
TransformerDecoder解码器Transformer模型中的一个重要组件,用于生成目标序列。它由多个Transformer Block组成,每个Block包含自注意力机制和前馈神经网络。Decoder的输入是编码器的输出和目标序列的嵌入表示。 下面是一个示例代码,演示了如何使用TransformerDecoder解码器生成目标序列[^1]: ```python import torch import torch.nn as nn class TransformerDecoder(nn.Module): def __init__(self, num_layers, embed_size, num_heads, feed_forward_dim, dropout_rate): super(TransformerDecoder, self).__init__() self.num_layers = num_layers self.decoder_layers = nn.ModuleList([ TransformerDecoderLayer(embed_size, num_heads, feed_forward_dim, dropout_rate) for _ in range(num_layers) ]) def forward(self, enc_output, target_seq): output = target_seq for layer in self.decoder_layers: output = layer(output, enc_output) return output class TransformerDecoderLayer(nn.Module): def __init__(self, embed_size, num_heads, feed_forward_dim, dropout_rate): super(TransformerDecoderLayer, self).__init__() self.self_attention = nn.MultiheadAttention(embed_size, num_heads, dropout=dropout_rate) self.encoder_attention = nn.MultiheadAttention(embed_size, num_heads, dropout=dropout_rate) self.feed_forward = nn.Sequential( nn.Linear(embed_size, feed_forward_dim), nn.ReLU(), nn.Linear(feed_forward_dim, embed_size) ) self.layer_norm1 = nn.LayerNorm(embed_size) self.layer_norm2 = nn.LayerNorm(embed_size) self.layer_norm3 = nn.LayerNorm(embed_size) self.dropout = nn.Dropout(dropout_rate) def forward(self, target_seq, enc_output): # Self-attention self_attention_output, _ = self.self_attention(target_seq, target_seq, target_seq) self_attention_output = self.dropout(self_attention_output) self_attention_output = self.layer_norm1(target_seq + self_attention_output) # Encoder attention encoder_attention_output, _ = self.encoder_attention(self_attention_output, enc_output, enc_output) encoder_attention_output = self.dropout(encoder_attention_output) encoder_attention_output = self.layer_norm2(self_attention_output + encoder_attention_output) # Feed forward feed_forward_output = self.feed_forward(encoder_attention_output) feed_forward_output = self.dropout(feed_forward_output) output = self.layer_norm3(encoder_attention_output + feed_forward_output) return output # 使用示例 num_layers = 6 embed_size = 512 num_heads = 8 feed_forward_dim = 2048 dropout_rate = 0.1 decoder = TransformerDecoder(num_layers, embed_size, num_heads, feed_forward_dim, dropout_rate) enc_output = torch.randn(batch_size, max_token_count, embed_size) target_seq = torch.randn(batch_size, target_seq_length, embed_size) output = decoder(enc_output, target_seq) ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值