Transformers基本原理—Decoder如何进行解码?

一、Transformers整体架构概述Transformers 是一种基于自注意力机制的架构,最初在2017年由Vaswani等人在论文《Attention Is All You Need》中提出。 这种架构彻底改变了自然语言处理(NLP)领域,因为它能够有效地处理序列数据,并且能够捕捉长距离依赖关系。 Transformers整体架构如下:主要架构由左侧的编码器(Encoder)和右侧的解码器(Decoder)构成。

Transformers基本原理—Decoder如何进行解码?

一、Transformers整体架构概述

Transformers 是一种基于自注意力机制的架构,最初在2017年由Vaswani等人在论文《Attention Is All You Need》中提出。这种架构彻底改变了自然语言处理(NLP)领域,因为它能够有效地处理序列数据,并且能够捕捉长距离依赖关系。

Transformers整体架构如下:

图片

主要架构由左侧的编码器(Encoder)和右侧的解码器(Decoder)构成。

对于Decoder主要做了一件什么事儿,可参考之前的文章。

本次我们主要来看解码器如何工作。

二、编码器的输出

假设有这样一个任务,我们要将德文翻译为英文,这个就会使用到Encoder-Decoder整个结构。

德文:ich mochte ein bier

英文:i want a beer

为了更好地模拟真实情况(每一句话长度不可能相同),我们需要将德文进行Padding,填充1个P符号;英文目标增加一个End,表示这句话翻译完了,用E符号表示。那么这两句话就变为

德文:ich mochte ein bier P

英文:i want a beer E

这个Encoder结束后,会给我们一个输出向量,这个向量将会在Decoder结构中会形成K和V进行使用。

Encoder的输出可以当作真实序列的高级表达,或者说是对真实序列的高级拆分。

三、解码器如何工作

在Transformer模型中,解码器(Decoder)的主要作用是通过自回归模型生成目标序列。

自回归的含义是这样的:

在预测 “东方红,太阳升。” 的时候:
会优先传入 “东”,然后预测出来“方”,
接着传入“东方”, 然后预测出来“东方红”,
接着传入“东方红”,然后预测出来“,”,
接着再传入“东方红,”,然后预测出来“太”
...
直到最后预测出来整句话:“东方红,太阳升。”。

所以,自回归就是基于已出现的词预测未来的词

接下来我们先来搞明白解码器具体做了一件什么事儿,忽略如何做的细节。

四、解码器输入

4.1  (Shifted)Outputs

解码器的输入基本上与编码器输入类似,如果忘记了可以去查看:Transformers基本原理—Encoder如何进行编码(1)

4.1.1 如何理解(Shifted)Outputs

(Shifted)Outputs表示输入序列词的偏移。

对于Transformer结构来说,我们的1组数据应包含3个:Encoder输入是1个,Decoder输入是1个,目标是1个。

比如:

Encoder输入enc_inputs为德文   “ich mochte ein bier P”

Decoder输入dec_inputs为英文   “S i want a beer”

优化目标输入target_inputs为英文 “i want a beer E”

对于Decoder来说,就是要通过“S”预测目标“i”,通过“S i”预测目标“want”,通过“S i want”预测“a”,通过“S i want a”预测“beer”,通过“S i want a beer”预测“E”。

S表示给序列一个开始预测的提示,E表示给序列一个停止预测的提示。

4.1.2 如何完成(Shifted)Outputs

这里的处理方法与Encoder是一样,但是一定要注意输入的是带偏移的序列。

我们定义1个大词表,它包含了输入所有的词的位置,如下所示:

tgt_vocab = {'P': 0, 'i': 1, 'want': 2, 'a': 3, 'beer': 4, 'S': 5, 'E': 6}

那对应于 英文 “S i want a beer” ,其编码端的输入就是:

dec_inputs: tensor([[5, 1, 2, 3, 4]])

接着我们将这5个词通过nn.Embedding给映射到高维空间,形成词向量,为了方便展示,我们映射到6维空间(一般映射到2的n次方维度,最高512维)。

此处的计算逻辑与Encoder一模一样,不再赘述,直接附结果。

图片

这样我们就完成了输入文本的词嵌入。

图片

4.2  位置嵌入  

位置嵌入原理与Encoder中一模一样,此处不再赘述,可参考:Transformers基本原理—Encoder如何进行编码(1)?

图片

这样我们就完成了输入文本的位置嵌入。

图片

4.3  融合词嵌入和位置编码

由于两个向量shape都是一样的,因此直接相加即可,结果如下:

图片

图片

五、带掩码的多头注意力机制

在Decoder的多头注意力机制中,QKV的计算方式与Encoder中一模一样,重点只需要关注掩码的方式即可。

由于Encoder自回归的原因,所以,此处的掩码应遵循预测时不看到未来信息且不关注Padding符号的原则。

5.1  不看未来信息

以“S i want a beer”为例:

在通过“S”预测时,不应该注意后面的“i want a beer”;在通过“S  i”预测时,不应该注意后面的“want a beer”。

因此这个注意力矩阵就应该遵循这个规律:将看不到的词的注意力设置为0。

而这个我们恰巧可以通过上三角矩阵来完成,具体代码如下:

def get_attn_subsequent_mask(seq):
    # 取出传进来的向量的三个维度,分别是batch_size,seq_length,d_model
    attn_shape = [seq.size(0), seq.size(1), seq.size(1)]
    # 通过np.triu形成一个上三角矩阵,填充为1
    subsequent_mask = np.triu(np.ones(attn_shape), k=1)
    # 由array转为torch的tensor格式
    subsequent_mask = torch.from_numpy(subsequent_mask).byte()
    return subsequent_mask

图片

5.2  不看Padding的词

这里的处理方式与Encoder一致,主要为了让对Padding的注意力降为0.

以“S i want P P”为例,具体代码如下:

# 需要判断哪些位置是填充的
def get_attn_pad_mask(seq_q, seq_k):  # enc_inputs, enc_inputs 告诉后面的句子后面的层 那些是被pad符号填充的  pad的目的是让batch里面的每一行长度一致
    # 获取seq_q和seq_k的batch_size和长度
    batch_size, len_q = seq_q.size()
    batch_size, len_k = seq_k.size()
    # 判断输入的位置编码是否是0并升维度,0表明这个并不是有效的词,只是填充或者标点等,后续计算自注意力时不关注这些词
    pad_attn_mask = seq_k.data.eq(0).unsqueeze(1)
    # batch_size x 1 x len_k(=len_q), one is maskingtensor([[[False, False, False, False,  True]]])  True代表为pad符号
    # 扩展mask矩阵,使其维度为batch_size x len_q x len_k
    # (1,1,5) ->> (1,5,5)
    return pad_attn_mask.expand(batch_size, len_q, len_k)

图片

如果序列为“S i want a beer”为例,则掩码矩阵如下:

图片

5.3  合并的masked矩阵

显然,我们在对序列进行编码的时候,肯定希望这个序列既不能偷看未来信息,也不能关注Padding信息,因此,需要将两个掩码进行融),得到self-attention的mask。

具体做法为:两个mask矩阵直接求和,大于0的地方说明要么是未来信息,要么是Padding信息。

“S i want P P”的合并mask矩阵就如下所示:

图片

“S i want a beer”的合并mask矩阵就如下所示:

图片

这样,我们就得到了最终的mask矩阵,只需要将融合的mask矩阵的在计算注意力时填充为-1e10即可,这样经过Softmax之后得到的注意力就约等于0,不会去关注这部分信息。

5.4  多头注意力机制

将【4.3  融合词嵌入和位置编码】的结果当作X输入Decoder的多头进行计算即可,只需要在计算自注意力的时候根据合并的masked矩阵进行掩码填充,防止关注不需要关注的信息即可。

第1步:QKV的计算

X 为我们输入的词嵌入,假设此处的shape为(2,4)。

WQ、WK、WV矩阵为优化目标,WQ、WK矩阵的维度一定相同,此处均为(4,3);WV矩阵维度可以相同也可以不同,此处为(4,3)。

Q、K、V经过矩阵运算后,shape均为(2,3)。

图片

第2步:计算最终输出

1、Q矩阵与K矩阵的转置相乘,得到词与词之间的注意力分数矩阵,位于(i,j)位置表示第i个词与第j个词之间的注意力分数。

矩阵运算:(2,3) * (3,2) ->> (2,2)

2、归一化注意力分数后,矩阵shape不会改变,只改变内部数值大小。【根据mask的填充就发生在Softmax前,确保不关注未来信息与padding信息】

矩阵大小:(2,2)

3、与V矩阵相乘,得到最终输出Z矩阵。

矩阵运算:(2,2) * (2,3) ->> (2,3)

图片

Z矩阵就是原始输入经过变换后的输出,且整个过程中只需要优化WQ、WK、WV矩阵,计算量大大减少。

这样,我们就完成了Decoder的第一个多头计算。

图片

六、残差连接和层归一化

6.1  残差连接

为了解决深层网络退化问题,残差连接是经常被使用的,在代码内也就一行:

dec_outputs = output + residual
dec_outputs:最终输出,判断原始输出好还是经过多头后的输出好
output:经过多头后的输出
residual:原始输入

6.2  层归一化

层归一化只需要将最终输出做一下LayerNorm即可,也是一行代码即可完成。

dec_outputs = nn.LayerNorm(output + residual)

这样,我们就完成了残差连接与层归一化,如图所示:

图片

需要注意的就是本层的输出将会作为Q,传递给下一个多头。

七、第二层多头自注意力机制

这一层的数据来自于两个位置:一个是Encoder的输出,一个是Decoder的第一层多头输出。

7.1  Encoder的输出

在编码端,会产生一个对原始输入的高级表达,假设变量名为:enc_outputs。

这个编码端的输出将会被当作K矩阵和V矩阵使用。

其中,enc_outputs既是K又是V,两者完全一样。

同时,还需要将Encoder的原始输入传递给第二层多头,需要根据这个原始输入形成掩码矩阵,让解码时关注K,以确保解码器在生成每个词时,只利用编码器输出中有效的、相关的信息。

7.2  Decoder第一层多头输出

Decoder第一层多头输出的结果就是对原始解码输入的抽象表达,会被当作Q输入第二层多头内。

图片

7.3  Encoder—Decoder逻辑

当Q给多头后,会通过与Encoder的K进行点乘,得到与要查询(Q)的注意力,判断哪些内容与Q类似,最后再与V计算,得到最终的输出。

一个通俗的理解就是你买了一个变形金刚(transformer是变形金刚)的玩具,拿到的东西会有:各个零件、组装说明书。

Encoder的结果就是提供了各个零件的组装说明书;Decoder就是各个零件的查询。

当我们想要将其组装好时,会在Decoder内查询(Q)每个零件的用法,通过组装说明书(Encoder的结果),寻找与当前零件最相似的说明(寻找高注意力的零件:Q · K^T),最后根据零件用法进行组装(输出最后结果)成一个变形金刚。

当然,这里的QKV的流程是人类赋予的,只是与现实中的数据库查表过程类似,与组装变形金刚类似,让我们更加容易接受的一种说法。

个人对Encoder—Decoder的理解:

Encoder就是对原始输入的高阶抽象表达(低级表达的高级抽象),比如不同语言虽然表面上不一样(各种各样的写法),但是在更高维的向量空间是相似的,存在一套大一统高阶向量,只是我们无法用语言来描述。

就像有些数据在二维不可分,但是落在高维空间就可分了一样,我们无法观察更高维的空间,但它们是存在的。

图片

Decoder就是当我们在高维空间查询(Q)时,就能找到相似(注意力强弱)的高级抽象表达(Q · K^T),然后通过V输出为低级表达,让我们能看得懂。

正如作为三维生物的我们无法理解更高维空间,需要一个中介将高维空间转化为低维空间表达;或者我们作为三维生物,如果二维生物可沟通,我们亦可当作中介将高维空间翻译给低维生物,我们就是这个多头机制,我们就是这个中介。

所以,Encoder—Decoder机制更像是一个大的翻译,大的中介,形成了高维与低维通信的方法。

7.4  Decoder第二层多头的掩码机制

由于Q代表当前要生成的词,而K代表编码器中的词,我们要去零件库寻找与Q相似的零件,那么我们希望的肯定是这个零件是有效的,以确保解码器在生成每个词时,只利用编码器输出中有效的、相关的信息。

因此,我们在形成掩码矩阵时,主要根据Encoder的输入序列来形成掩码矩阵,而不是Decoder输入的序列,当然,Q肯定也有Padding,但这里我们不必过度关注。

所以,在第二层多头掩码时,我们更关注K的Padding掩码,确保解码得到的信息也是有效的。

具体代码如下:

def get_attn_pad_mask(seq_q, seq_k):
    batch_size, len_q = seq_q.size()
    batch_size, len_k = seq_k.size()
    # eq(zero) is PAD token
    pad_attn_mask = seq_k.data.eq(0).unsqueeze(1)  # batch_size x 1 x len_k(=len_q), one is masking
    return pad_attn_mask.expand(batch_size, len_q, len_k)  # batch_size x len_q x len_k

比如,输入序列为:[1,2,3,4,0],最后一位是Padding,那么掩码矩阵如下图示例:

图片

7.5  Decoder第二层多头的输出

至此,我们就将Q进行了解码输出,得到了与Q最为相似的表达。

如图所示步骤:

图片

八、残差连接和层归一化

接着就是再次残差连接与层归一化,与上述步骤完全一样。(为了保持连贯性,还是再写一遍)

8.1  残差连接

为了解决深层网络退化问题,残差连接是经常被使用的,在代码内也就一行:

dec_outputs = output + residual
dec_outputs:最终输出,判断原始输出好还是经过多头后的输出好
output:经过多头后的输出
residual:原始输入

8.2  层归一化

层归一化只需要将最终输出做一下LayerNorm即可,也是一行代码即可完成。

dec_outputs = nn.LayerNorm(output + residual)

这样,我们就完成了残差连接与层归一化,如图所示:

图片

九、前馈神经网络

该前馈神经网络接受来自归一化后的输出,shape为(batch_size, seq_length, dmodel)。

batch_size:批处理大小
seq_length:序列长度,比如64个Token
dmodel:词嵌入映射维度

在Transformer中,由于我们输入的是序列数据,因此Conv1d就被优先考虑使用了,但是这里需要一定的前置知识(点击可看Conv1d原理),才能懂得Transformer中FNN的工作原理。

9.1  Conv1d连接

由于在Pytorch内Conv1d要求维度优先,因此需要对输入数据进行维度转换。

self.conv1 = nn.Conv1d(in_channels=d_model, out_channels=d_ff, kernel_size=1,bias=False)  
self.conv2 = nn.Conv1d(in_channels=d_ff, out_channels=d_model, kernel_size=1,bias=False)  
# 维度优先由6维升维到d_ff维
output = nn.ReLU()(self.conv1(inputs.transpose(1, 2)))  # 这里因为一维卷积需要维度再前所以要交换特征位置
# 再降维到d_model维度,一般为词嵌入维度
output = self.conv2(output).transpose(1, 2)

假设输入的数据为(1, 5 , 6),d_ff为2048,那么整个过程的数据shape流动就为:

(1,5,6) ->转置>> (1,6,5)->卷积>> (1,2048,5)->卷积>>(1,6,5)->转置>>(1,5,6)

经过调整提取之后,输出shape仍为(1,5,6)。

至此,前馈神经网络完成。

图片

十、残差连接和层归一化

接着就是再次残差连接与层归一化,与上述步骤完全一样。(为了保持连贯性,还是再写一遍)

10.1  残差连接

为了解决深层网络退化问题,残差连接是经常被使用的,在代码内也就一行:

dec_outputs = output + residual
dec_outputs:最终输出,判断原始输出好还是经过多头后的输出好
output:经过多头后的输出
residual:原始输入

10.2  层归一化

层归一化只需要将最终输出做一下LayerNorm即可,也是一行代码即可完成。

dec_outputs = nn.LayerNorm(output + residual)

这样,我们就完成了残差连接与层归一化,如图所示:

图片

十一、线性层及Softmax

在得到归一化的输出之后,经过线性层将输出维度映射为指定类别,比如,你的目标词表有1000个词,那么就会通过softmax预测输出的这1000个词哪个词概率最高,最高的即是目标输出。

代码如下:

self.projection = nn.Linear(d_model, tgt_vocab_size, bias=False)
dec_logits = self.projection(dec_outputs).view(-1, dec_logits.size(-1))

可以看到,经过50个epoch,是能够将德文成功翻译为英文的,至此,我们已经将Transformer剖析完成。

图片图片

相关资讯

解决 NLP 任务的 Transformer 为什么可以应用于计算机视觉?

几乎所有的自然语言处理任务,从语言建模和masked词预测到翻译和问答,在2017年Transformer架构首次亮相后都经历了革命性的变化。 Transformer在计算机视觉任务中也表现出色,只用了2-3年的时间。 在这篇文章中,我们探索了两种基础架构,它们使Transformer能够闯入计算机视觉的世界。

OpenAI上线新功能太强了,服务器瞬间被挤爆

让 ChatGPT 服务器宕机,你参与了吗?OpenAI 开发者日上新功能太火爆,服务器都挤爆了。太平洋时间 11 月 8 日上午 6 点左右开始,ChatGPT 服务器宕机超过 90 分钟,用户访问会收到「ChatGPT 目前已满载(ChatGPT is at capacity right now)」的消息。随后,OpenAI 接连发布两次「服务器中断」警告 —— 一次部分中断、一次全线中断,并称正在调查宕机原因,进行修复和监控。最新状态显示:「ChatGPT 和 API 仍然会出现周期性中断。」OpenAI 表

当AI更加理解人类语言可能预示提示工程终结

多年来,大型语言模型(LLM)的兴起要求用户学习一种新技能:提示工程。 为了得到人工智能有用的回应,人们不得不精心设计他们的查询问题,学习人工智能如何理解语言的细微差别。 但这种情况可能正在发生变化。