【Transformer系列(1)】encoder(编码器)和decoder(解码器)

Transformer详解:编码器与解码器的结构与作用
本文详细介绍了Transformer模型中的编码器和解码器,包括它们的结构、代码实现和在模型中的使用。编码器负责将输入序列压缩成语义向量,解码器则依据编码器的输出生成目标序列。在解码器中,特别提到了Maskedself-attention和Cross-attention机制,以防止解码过程中的信息泄露并结合编码器的上下文信息。文章还探讨了Encoder-Decoder框架在机器翻译等任务中的应用。
该文章已生成可运行项目,

前言

这个专栏我们开始学习transformer,自推出以来transformer在深度学习中占有重要地位,不仅在NLP领域,在CV领域中也被广泛应用,尤其是2021年,transformer在CV领域可谓大杀四方。

在论文的学习之前,我们先来介绍一些专业术语。本篇就让我们先来认识一下encoder和decoder吧!


962f7cb1b48f44e29d9beb1d499d0530.gif​   🍀本人Transformer相关文章导航:

 【Transformer系列(1)】encoder(编码器)和decoder(解码器)

 【Transformer系列(2)】注意力机制、自注意力机制、多头注意力机制、通道注意力机制、空间注意力机制超详细讲解

 【Transformer系列(3)】 《Attention Is All You Need》论文超详细解读(翻译+精读)

【Transformer系列(4)】Transformer模型结构超详细解读


目录

一、encoder

1.1 简介

1.2 代码实现

1.3 transformer中的使用

1.3.1 transformer中encoder的组成

1.3.2 每个Block的组成

1.3.3 每个Block 中的具体实现步骤

二、decoder

2.1 简介

2.2 代码实现

2.3 transformer中的使用

2.3.1transformer中decoder的组成

2.3.2 transformer中encoder和decoder的区别

2.3.3  Masked self attention模块

2.3.4  Cross attetion模块

2.3.5  具体实现步骤

三、encoder-decoder

3.1 简介

3.2 代码实现

3.3 注意问题


 一、encoder

1.1 简介

encoder也就是编码器,负责将输入序列压缩成指定长度的向量,这个向量就可以看成是这个序列的语义,然后进行编码,或进行特征提取(可以看做更复杂的编码)。

简单来说就是机器读取数据的过程,将现实问题转化成数学问题。如下图所示:


1.2 代码实现

在编码器接口中,我们只指定长度可变的序列作为编码器的输入X。 任何继承这个encoder 基类的模型将完成代码实现。

class encoder(nn.Module):
    def __init__(self):
        super(Encoder, self).__init__()
        self.positional_encoding = Positional_Encoding(config.d_model)
        self.muti_atten = Mutihead_Attention(config.d_model,config.dim_k,config.dim_v,config.n_heads)
        self.feed_forward = Feed_Forward(config.d_model)

        self.add_norm = Add_Norm()

    def forward(self,x): # batch_size * seq_len 并且 x 的类型不是tensor,是普通list

        x += self.positional_encoding(x.shape[1],config.d_model)
        # print("After positional_encoding: {}".format(x.size()))
        output = self.add_norm(x,self.muti_atten,y=x)
        output = self.add_norm(output,self.feed_forward)

        return output

Mutihead_Attention():多头注意力机制: 

class Mutihead_Attention(nn.Module):
    def __init__(self,d_model,dim_k,dim_v,n_heads):
        super(Mutihead_Attention, self).__init__()
        self.dim_v = dim_v
        self.dim_k = dim_k
        self.n_heads = n_heads

        self.q = nn.Linear(d_model,dim_k)
        self.k = nn.Linear(d_model,dim_k)
        self.v = nn.Linear(d_model,dim_v)

        self.o = nn.Linear(dim_v,d_model)
        self.norm_fact = 1 / math.sqrt(d_model)

    def generate_mask(self,dim):
        # 此处是 sequence mask ,防止 decoder窥视后面时间步的信息。
        # padding mask 在数据输入模型之前完成。
        matirx = np.ones((dim,dim))
        mask = torch.Tensor(np.tril(matirx))

        return mask==1

    def forward(self,x,y,requires_mask=False):
        assert self.dim_k % self.n_heads == 0 and self.dim_v % self.n_heads == 0
        # size of x : [batch_size * seq_len * batch_size]
        # 对 x 进行自注意力
        Q = self.q(x).reshape(-1,x.shape[0],x.shape[1],self.dim_k // self.n_heads) # n_heads * batch_size * seq_len * dim_k
        K = self.k(x).reshape(-1,x.shape[0],x.shape[1],self.dim_k // self.n_heads) # n_heads * batch_size * seq_len * dim_k
        V = self.v(y).reshape(-1,y.shape[0],y.shape[1],self.dim_v // self.n_heads) # n_heads * batch_size * seq_len * dim_v
        # print("Attention V shape : {}".format(V.shape))
        attention_score = torch.matmul(Q,K.permute(0,1,3,2)) * self.norm_fact
        if requires_mask:
            mask = self.generate_mask(x.shape[1])
            attention_score.masked_fill(mask,value=float("-inf")) # 注意这里的小Trick,不需要将Q,K,V 分别MASK,只MASKSoftmax之前的结果就好了
        output = torch.matmul(attention_score,V).reshape(y.shape[0],y.shape[1],-1)
        # print("Attention output shape : {}".format(output.shape))

        output = self.o(output)
        return output

Feed_Forward() : 两个Linear中连接Relu即可,目的是为模型增添非线性信息,提高模型的拟合能力。

class Feed_Forward(nn.Module):
    def __init__(self,input_dim,hidden_dim=2048):
        super(Feed_Forward, self).__init__()
        self.L1 = nn.Linear(input_dim,hidden_dim)
        self.L2 = nn.Linear(hidden_dim,input_dim)

    def forward(self,x):
        output = nn.ReLU()(self.L1(x))
        output = self.L2(output)
        return output

Add_Norm():残差连接以及LayerNorm

class Add_Norm(nn.Module):
    def __init__(self):
        self.dropout = nn.Dropout(config.p)
        super(Add_Norm, self).__init__()

    def forward(self,x,sub_layer,**kwargs):
        sub_output = sub_layer(x,**kwargs)
        # print("{} output : {}".format(sub_layer,sub_output.size()))
        x = self.dropout(x + sub_output)

        layer_norm = nn.LayerNorm(x.size()[1:])
        out = layer_norm(x)
        return out

1.3 transformer中的使用

1.3.1transformer中encoder的组成

transformer 中 encoder 由 6 个相同的层组成,每个层包含 2 个部分:

  • Multi-Head Self-Attention
  • Position-Wise Feed-Forward Network


1.3.2 每个Block的组成

自注意力机制 + 残差链接 + LayerNorm + FC + 残差链接 + layer Norm此时的输出 = 一个 Block 的输出;


1.3.3 每个Block 中的具体实现步骤

(1)原始的输入向量b 与输出向量a残差相加,得到向量a+b;
【注意】 b是原始的输入向量,下图中输出向量a是考虑整个序列的输入向量得到的结果

 (2)将向量 a+b 通过 Layer Normation 得到向量c ;

也就是下图左边部分:

(3)将向量c 通过 FC layer 得到向量d ;

(4)向量c 与向量d 残差相加 ,得到向量e ;

(5)向量e 通过 Layer Norm 输出 向量f;

(6)此时得到的输出向量f 才是 encoder中每个Block中的一个输出向量;
以上步骤就是下图右边部分:

上述步骤,便是原始论文transformer中encoder的设计啦~


二、decoder

2.1 简介

decoder,也就是解码器,负责根据encoder部分输出的语义向量c来做解码工作。以翻译为例,就是生成相应的译文。

简单来说,就是就数学问题,并转换为现实世界的解决方案。

【注意】生成的序列是不定长的。而且上一时刻的输出通常要作为下一时刻的输入。


2.2 代码实现

在上面1.2encoder的代码实现中,我们已经实现了大部分decoder的模块。

但是encoder和decoder实现还是有区别的:

  • decoder的Muti_head_Attention引入了Mask机制
  • decoder与encoder中模块的拼接方式不同
class Decoder(nn.Module):
    def __init__(self):
        super(Decoder, self).__init__()
        self.positional_encoding = Positional_Encoding(config.d_model)
        self.muti_atten = Mutihead_Attention(config.d_model,config.dim_k,config.dim_v,config.n_heads)
        self.feed_forward = Feed_Forward(config.d_model)
        self.add_norm = Add_Norm()

    def forward(self,x,encoder_output): # batch_size * seq_len 并且 x 的类型不是tensor,是普通list
        # print(x.size())
        x += self.positional_encoding(x.shape[1],config.d_model)
        # print(x.size())
        # 第一个 sub_layer
        output = self.add_norm(x,self.muti_atten,y=x,requires_mask=True)
        # 第二个 sub_layer
        output = self.add_norm(output,self.muti_atten,y=encoder_output,requires_mask=True)
        # 第三个 sub_layer
        output = self.add_norm(output,self.feed_forward)

        return output

2.3 transformer中的使用

2.3.1transformer中decoder的组成

在transformer中decoder 也是由 6 个相同的层组成,每个层包含 3 个部分:

  • Multi-Head Self-Attention
  • Multi-Head Context-Attention
  • Position-Wise Feed-Forward Network


2.3.2 transformer中encoder和decoder的区别

我们先来看看这个图

(1)第一级中: 将self attention 模块加入了Masked模块,变成了 Masked self-attention, 这样以来就只考虑解码器的当前输入和当前输入的左侧部分, 不考虑右侧部分; ( 注意,第一级decoder的key, query, value均来自前一层decoder的输出,但加入了Mask操作,即我们只能attend到前面已经翻译过的输出的词语,因为翻译过程我们当前还并不知道下一个输出词语,这是我们之后才会推测到的。)

(2)第二级中:引入了 Cross attention 交叉注意力模块 在 masked self-attention 和全连接层 之间加入;

(3)Cross attention 交叉注意力模块的输入 Q,K,V 不是来自同一个模块,K,V 来自编码器的输出, Q来自解码器的输出;

【注意】 解码器的输出一个一个产生的


2.3.3  Masked self attention模块

举个栗子吧~以翻译为例:

  • 输入:我是路人贾
  • 输出: I am Jia

由上一节可知,输入“我是路人贾”这一步是在encoder中进行了编码,那么这里我们具体讨论decoder的操作,也就是加了 Masked self attention模块后如何得到输出(“I am Jia”)的过程。

第1步:

  • 初始输入: 起始符</s> + Positional Encoding(位置编码)
  • 中间输入:(我是路人贾)Encoder Embedding
  • 最终输出:产生预测“I”

第2步:

  • 初始输入:起始符</s> + “I”+ Positonal Encoding
  • 中间输入:(我是路人贾)Encoder Embedding
  • 最终输出:产生预测“am”

第3步:

  • 初始输入:起始符</s> + “I”+ “am+ Positonal Encoding
  • 中间输入:(我是路人贾)Encoder Embedding
  • 最终输出:产生预测“Jia”

其实这个原理很简单,主要想表达的就是因为变成了Masked self attention ,所以只考虑输入向量本身, 和输入向量的之前的向量,即左侧向量,而不去考虑后边(右侧)向量。

另外,求相关性的分数时,q,k ,v 同样的也只会考虑当前输入向量的左侧向量部分,而不去考虑输入向量后面的右侧部分。

这里介绍一下论文在Decoder的输入上,对Outputs的Shifted Right操作。

Shifted Right 实质上是给输出添加起始符/结束符,方便预测第1个Token/结束预测过程。

还是看看我们上一个栗子~

正常的输出序列位置关系如下:

  • 0-"I"
  • 1-"am"
  • 2-"Jia"

但在执行的过程中,我们在初始输出中添加了起始符</s>,相当于将输出整体右移1位(Shifted Right),所以输出序列变成如下情况:

  • 0-</s>【起始符】
  • 1-“I”
  • 2-“am”
  • 3-“Jia”

这样我们就可以通过起始符</s>预测“I”,也就是通过起始符预测实际的第1个输出啦。


2.3.4  Cross attetion模块

Cross attetion模块称为交叉注意力模块,是因为向量 q , k , v 不是来自同一个模块

而是将来自解码器的输出向量q 与来自编码器的输出向量 k , v运算。

具体讲来:
向量 q 与向量 k之间相乘求出注意力分数α1 '
注意力分数α1 '再与向量 v 相乘求和,得出向量 b (图中表示为向量 v ) ;

请添加图片描述


 2.3.5  具体实现步骤

(1)经过 Masked self attention:
解码器之前的输出作为当前解码器的输入,并且训练过程中真实标签的也会输入到解码器中,此时这些输入, 通过一个Masked self-attention ,得到输出q向量,注意到这里的q是由解码器产生的;

(2)经过 Cross attention:
将向量q 与来自编码器的输出向量 k , v 运算。具体讲来就是向量 q 与向量 k之间相乘求出注意力分数α1 ',注意力分数α1 '再与向量 v 相乘求和,得出向量 b  ;

(3)经过全连接层:
之后向量 b 便被输入到feed−forward 层, 也即全连接层, 得到最终输出;

上述步骤,便是原始论文transformer中decoder的设计啦~


三、encoder-decoder

刚才已经分别了解了encoder和decoder,接下来我们再来看看encoder-decoder这个框架吧。

3.1 简介

encoder-decoder 模型主要是 NLP 领域里的概念。它并不特值某种具体的算法,而是一类算法的统称encoder-decoder 算是一个通用的框架,在这个框架下可以使用不同的算法来解决不同的任务。

 其实整个过程我们可以看做是一个游戏——《你画我猜》。玩家1从系统中抽取拿到题卡,然后通过画画的方式描述该词。玩家2就通过画来猜出题目中的词是什么东东。我们拿目前应用最深入的机器翻译问题举个栗子:

(毕贾索已上线~)

就酱就酱~大家懂就行~


3.2 代码实现

encoder-decoder框架包含了一个编码器和一个解码器,并且还拥有可选的额外的参数。在前向传播中,编码器的输出用于生成编码状态,这个状态又被解码器作为其输入的一部分。

class EncoderDecoder(nn.Module):
    """
    A standard Encoder-Decoder architecture. Base for this and many 
    other models.
    """
    def __init__(self, encoder, decoder, src_embed, tgt_embed, generator):
        super(EncoderDecoder, self).__init__()
        self.encoder = encoder
        self.decoder = decoder
        self.src_embed = src_embed
        self.tgt_embed = tgt_embed
        self.generator = generator
        
    def forward(self, src, tgt, src_mask, tgt_mask):
        "Take in and process masked src and target sequences."
        return self.decode(self.encode(src, src_mask), src_mask,
                            tgt, tgt_mask)
    
    def encode(self, src, src_mask):
        return self.encoder(self.src_embed(src), src_mask)
    
    def decode(self, memory, src_mask, tgt, tgt_mask):
        return self.decoder(self.tgt_embed(tgt), memory, src_mask, tgt_mask)

3.3 注意问题

  • 不论输入和输出的长度是什么,中间的“向量c”长度都是固定的(这是它的缺陷所在)。
  • 根据不同的任务可以选择不同的编码器和解码器(例如,CNN、RNN、LSTM、GRU等)
  • encoder-decoder的一个显著特征就是:它是一个end-to-end的学习算法。
  • 只要符合这种框架结构的模型都可以统称为encoder-decoder模型。

本文参考:

李宏毅机器学习 

本文章已经生成可运行项目
<think>嗯,用户想了解多头注意力机制,也就是multi-head attention in deep learning。首先,我需要回忆一下多头注意力的基本概念,它是Transformer模型的核心组件之一,对吧?记得最初的Transformer论文里提到过,多头注意力可以让模型同时关注不同位置的信息,捕捉多种依赖关系,比如词与词之间的语法语义联系。 接下来,原理部分。多头注意力其实是对自注意力的扩展,对吧?自注意力机制通过计算查询、键值之间的相关性来分配权重,而多头则是将这个过程拆分成多个“头”,每个头在不同的子空间进行学习。这样每个头可能关注不同的方面,最后再将这些头的输出合并起来。这样的话,模型能更全面地捕捉不同类型的依赖关系,比如有的头关注局部信息,有的关注长距离依赖。 然后是具体实现。多头注意力的实现步骤应该包括线性变换、分割成多个头、计算注意力、合并头最后的线性投影。可能需要用矩阵运算来实现,比如将输入分成h个头,每个头有不同的权重矩阵。然后对每个头分别计算缩放点积注意力,再把结果拼接起来,通过另一个线性层得到最终输出。这里要注意维度的问题,每个头的维度通常是总维度除以头的数量,比如d_model // h。 还有,为什么要用多头而不是单头?可能是因为不同的头可以学习到不同的模式,增强模型的表达能力。比如在机器翻译中,不同的头可能关注不同的语法结构或语义角色,这样组合起来效果更好。另外,多头注意力也增加了模型的并行计算能力,提升训练效率。 用户可能还关心应用场景,比如Transformer在NLP中的各种应用,如BERT、GPT这些模型都用了多头注意力。另外,多头注意力如何处理位置信息?可能需要结合位置编码,因为自注意力本身不考虑序列的顺序,所以需要加上位置编码来注入位置信息。 还有可能需要注意实现中的细节,比如如何分割合并头,是否使用残差连接层归一化。例如,在代码实现中,输入经过线性变换后,需要reshape成(batch_size, seq_len, num_heads, depth),然后转置以进行并行计算。计算注意力分数后,再合并回来,通过线性层得到输出。 可能还需要比较一下多头注意力其他注意力机制的区别,比如Bahdanau注意力是加性注意力,而多头注意力是缩放点积注意力,并且多头结构增加了模型的表达能力。此外,多头注意力在计算时通常会使用mask,特别是在解码器中,防止未来信息泄漏,保证自回归性质。 用户提到的参考引用里有带掩码的多头注意力,这可能是指在训练Transformer解码器时,每个位置只能注意到之前的位置,所以需要掩码来屏蔽后面的位置。这部分在实现时如何处理呢?可能是在计算注意力分数后,加上一个掩码矩阵,将需要屏蔽的位置设置为非常大的负数,这样经过softmax后这些位置的权重就接近零。 总结一下,回答的结构应该包括概念、原理、实现步骤、数学公式、代码示例(如果有的话)、优势应用场景。同时,根据用户提供的引用,可能需要引用相关的文章,比如带掩码的多头注意力,以及其他相关机制如位置编码、自注意力等。 最后,生成相关问题的时候,可以考虑用户可能进一步想了解的内容,比如多头注意力的数学公式、具体实现代码、与自注意力的区别、优势,或者在不同模型中的应用。这样能帮助用户深入学习解决可能遇到的问题。</think>### 多头注意力机制(Multi-head Attention) #### 1. 概念与核心思想 多头注意力是深度学习中的一种注意力机制扩展形式,通过并行计算多个独立的注意力头(attention heads),使模型能够同时捕捉输入序列中不同子空间的特征关联[^1]。其核心思想是: - 将输入映射到多个低维子空间,分别进行注意力计算 - 综合各子空间的学习结果增强模型表达能力 - 克服单头注意力可能存在的模式单一问题 #### 2. 数学原理 给定输入矩阵$X \in \mathbb{R}^{n \times d}$,通过线性变换生成查询(Query)、键(Key)、值(Value)矩阵: $$ Q = XW^Q, \quad K = XW^K, \quad V = XW^V $$ 每个注意力头的计算过程为: $$ \text{Attention}(Q_h,K_h,V_h) = \text{softmax}(\frac{Q_hK_h^T}{\sqrt{d_k}})V_h $$ 最终输出为多个头的拼接与线性变换: $$ \text{MultiHead}(Q,K,V) = \text{Concat}(head_1,...,head_h)W^O $$ 其中$d_k = d/h$为每个头的维度,$h$为头数。 #### 3. 实现步骤(PyTorch示例) ```python import torch import torch.nn as nn class MultiHeadAttention(nn.Module): def __init__(self, d_model, num_heads): super().__init__() self.d_model = d_model self.num_heads = num_heads self.depth = d_model // num_heads self.WQ = nn.Linear(d_model, d_model) self.WK = nn.Linear(d_model, d_model) self.WV = nn.Linear(d_model, d_model) self.WO = nn.Linear(d_model, d_model) def forward(self, x): batch_size = x.size(0) # 线性变换并分割头 Q = self.WQ(x).view(batch_size, -1, self.num_heads, self.depth).transpose(1,2) K = self.WK(x).view(batch_size, -1, self.num_heads, self.depth).transpose(1,2) V = self.WV(x).view(batch_size, -1, self.num_heads, self.depth).transpose(1,2) # 计算缩放点积注意力 scores = torch.matmul(Q, K.transpose(-2, -1)) / torch.sqrt(torch.tensor(self.depth)) attn = torch.softmax(scores, dim=-1) context = torch.matmul(attn, V) # 合并头并输出 context = context.transpose(1,2).contiguous().view(batch_size, -1, self.d_model) return self.WO(context) ``` #### 4. 关键优势 1. **并行学习能力**:多个头可同时关注不同位置特征模式 2. **特征多样性**:各头学习不同的注意力模式(如局部/全局、语法/语义) 3. **维度分解**:将高维空间分解为多个低维子空间,降低计算复杂度 #### 5. 典型应用 - Transformer架构中的编码器-解码器注意力 - BERT等预训练模型的上下文建模 - 机器翻译中的跨语言对齐 - 文本生成中的长距离依赖捕捉
评论 5
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

路人贾'ω'

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值