【深度学习|基础算法】初识Transformer-encoder-decoder

时间:2024-04-10 14:17:06

关于transformer的学习

  • 一、前言
  • 二、初识Transformer
    • 2.1 总览
    • 2.2 encoder
    • 2.3 decoder
  • 三. 流程与细节
    • 1、输入
    • 2、self-attention
      • Vector Calculation
      • Matrix Calculation
    • 3、多头self-attention

一、前言

  我本身是从事图像算法行业的,在之前主要是做传统的图像算法,后来接触了基于CNN的神经网络图像算法,包括图像分类,目标检测,图像分割等场景。对于Transformer的了解,我一直停留在其在NLP领域的优异表现,对于图像算法中的应用,知之甚少,因此对于基于该框架的模型,也是一直没有下定决心好好去学习。然而现在在视觉应用中,Transformer也同样大放异彩,逐步的刷新着人们对于智能的认知,尤其是在自动驾驶,机器人领域,大模型的普遍应用,也让我不得不来窥探一下这其中究竟有着怎么样的绝妙的算法设计,虽然道路是艰苦的,但是我想行,只要经过不断的坚持努力,没有啃不下来的硬骨头,Transformer同样也能成为我工作中的攻城利器!


二、初识Transformer

  在网上搜索之后,发现了一篇针对transformer讲解的非常好的一篇博客,是一位国外的大神写的全英文的博客,因此我的笔记内容会将参考他的文章来进行我个人的一个理解总结,由于本人英语水平有限,如果我的理解有什么不到位的地方,希望大家能在评论区帮我指出问题,我们一起探讨一起进步~~

2.1 总览

  我们首先将transformer模型视为一个黑匣子,在机器翻译任务场景下,它将接收一种语言的句子,并以另一种语言输出翻译的结果,如下图所示。
在这里插入图片描述
我们再打开transformer进入内部看一下,我们可以看到它是由一个encoders组件和一个decoders组件组成的,并且两者之间是存在联系的(耦合),如下图所示。
在这里插入图片描述
事实上,encoders组件是一个由多个单独的encoder堆栈组成的一个结构,他们是一个自下而上的顺序流结构,在作者的paper中使用了六个这样的encoder来进行堆叠,当然,6并没有特别的讲究之处,你也可以用不同数量的encoder来堆叠你的encoders组件。同样的,decoders组件用同样个数的decoder进行堆叠,如下图所示。
在这里插入图片描述

2.2 encoder

  每一个encoder在结构上都是相同的,但是他们不共享权重,各自拥有独立的权重,每一个都可以被拆分成两个子部分:self-attentionFeed Forward Neural Network(FFNN)
在这里插入图片描述
encoder的输入首先流过self-attention层,这个层的作用是帮助encoder关注于句子中的特定单词,我们将会在下面的部分详细的介绍自注意力层自注意力层的输出将会作为输入喂到FFNN中作为输入。

自注意力层的输出将会被喂到一个前馈神经网络中去。完全相同的前馈网络被独立的应用在每一个位置(即相同的前馈网络但是不相同的权重)。

2.3 decoder

decoder拥有完全相同的这些层(即self-attention层和FFPN层,仅结构上相同),但是在decoder中介于这两者之间还有一个注意力层来帮助decoder来关注输入句子的相关部分(与seq2seq模型的注意力类似)。
在这里插入图片描述


三. 流程与细节

  在上一节中,我们已经大概了解了transformer的总体架构,接下去我们将带入向量/张量并观察他们是如何在这些组成部分中流动并最终从输入得到我们的输出的。

1、输入

  与一般的NLP应用场景一样,我们先从使用词嵌入算法将每个输入单词转成一个个向量开始(这里以boxes的形式来代替词嵌入后的向量来展示我们的输入):
在这里插入图片描述
这里的每一个单词都被嵌入成了一个长度为512的向量。用4个boxes来表示这个向量。

这样的词嵌入仅仅会发生在最底层的encoder,对于所有的encoder而言,他们的输入都是一个将每一个单词都嵌入成512长度的向量的列表集合,但是不同的是,只有最底层的encoder接受的输入是通过词嵌入算法转化而来的,而其他层的encoder的输入都将是上一个encoder的输出。而这个列表的大小是一个我们可设置的超参数,即我们的训练数据集中最长句子的长度。

将每一个单词嵌入我们的输入序列中之后,每个单词都会分别流经encoder的两个层,得到encoder的输出吗,如下图所示:
在这里插入图片描述
这里,我们开始看到Transformer的一个重要的属性——每个位置上的单词都会有在encoder中拥有自己的流经路径。self-attention层中,这些路径存在互相依赖的关系。而FFNN中的路径并不存在这些依赖关系,因此,在流经FFNN的路径中可以并行处理。接下去我们将以一个更短的句子作为输入,来看看在encoder的内部到底发生了些什么。

正如我们前文所提及的,一个encoder将会接收一个向量组成的列表来作为输入。encoder将这个列表传入到self-attention中来处理其中的词向量,然后将这些词向量输入到FFNN中去,然后将每个输出r1,r2作为下一个encoder的输入:
在这里插入图片描述

2、self-attention

Vector Calculation

假设下面的句子是我们想要翻译的输入句子:
”The animal didn’t cross the street because it was too tired”
问题来了,句子中的it到底代表的是什么意思呢?它是指向street呢还是指向animal呢?这对于人类来说是一个很简单的问题,但是对于算法而言,这一点都不简单。

当模型处理单词it时,self-attention会将它与animal联系在一起。当模型处理每个单词时(句子中的每个位置),self-attention允许它在输入序列的其他位置寻找线索,这些线索可以帮助它更好地编码这个单词。如果你熟悉RNN,那么可以想想我们应该如何去探寻这个隐藏的联系,即将之前处理过的单词/向量的表示与当前正在处理的单词/向量的表示结合起来。self-attention是Transformer用来将对其他相关单词的“理解”转化为我们当前正在处理的单词的方法。
在这里插入图片描述
记得查看Tensor2Tensor笔记本,在那里面您可以加载Transformer模型,并使用这个可视化来查看。

让我们首先来看看它是如何使用self-attention来使用向量,然后进一步解释它是如何使用矩阵来进行计算的。


计算self-attention第一步是根据encoder的每一个输入向量创建三个向量(在这个例子中,每一个输入向量是输入句子中的每一个被词嵌入的单词)。然后针对每一个单词,我们得到一个Query向量,一个key向量,一个value向量。这些向量是根据我们的输入的词嵌入向量乘以我们刚才创建的三个参数可训练的矩阵得到的。

注意到,这些新的向量在维度上是小于词嵌入的向量的,他们的维度是长度为64的向量,而词嵌入或者encoder输入/输出的向量长度是512。这些新的向量没有必要变得更小,这是一种为了让多头注意力的计算变得更加流畅的方式。

在这里插入图片描述

那么什么是q,k,v向量呢?

他们是一种对计算和关联注意力非常有帮助的抽象概念。一旦你继续阅读下面的注意力是如何计算的,你就会知道所有你需要知道的关于这些向量所扮演的角色。(这个作者很喜欢吊胃口~~)


我们现在已经知道了计算self-attention的第一步是先得到q,k,v三个向量。现在,第二步是计算一个分数。假设我们现在正在计算第一个单词“Thinking”的self-attention,我们会得到这个单词的分数,这个分数决定了当我们在句子中的某一个特定的位置经行编码时,我们会投入多少的关注度在输入句子的其他部分。这个分数是通过计算q,k向量的点积求得的。因此,当我们处理第一个位置的单词“Thinking”时,第一个位置的分数将会是q1,k1的点积,当我们处理第二个位置的单词“Machines”时,第二个位置的分数将会是q2,k2的点积,如下图所示:
在这里插入图片描述


第三,四步分别是将每个位置得到的分数除以dk的平方根(正如前文所说的,k向量在文中的定义的一维方向的维度是dk=64,因此平方根为8),在这里,就用每个位置得到的分数再除以8,这一步操作的目的是为了让反向传播时的梯度更加稳定可靠。然后使用softmax将每个位置上的分数进行归一化,因此最后每个分数都是正的,并且相加等于1,如下图所示:
在这里插入图片描述
对于每一个特定的位置,其他位置上的单词都会与它有一个softmax处理后的分数,这个分数反映的也是其他位置与这个特定位置的关联程度,很明显自己位置上的分数肯定是最高的,但是其他位置上单词有时也会获得很高的分数,即句子中与这个特定位置的单词关系较大的单词。


第五步是将每个位置得到的v向量乘以该位置上的softmax分数(准备将所有乘后的v向量进行一个相加得到该位置的z向量)。这里给我们的感觉就是,尽可能保持我们关注的单词的值不变,然后将我们不关注的单词的值尽可能的减小,比如乘以0.001。


第六步是将第五步得到的加权后的==v==向量的值进行相加,这就产生了``self-attention``在这个单词位置上的输出,如下图所示:

在这里插入图片描述

self-attention的计算到此就完成了,接下去就会将输出的这个向量z作为输入传递给FFNN作为输入。在实际的计算中,一般都是以矩阵的形式进行计算,能够比用向量计算有更快的速度,那么,刚才将句子拆分成一个个独立的向量的例子想必已经帮助大家对与self-attention是如何计算的已经有了一个较为深刻的认识,接下去我们将用矩阵计算展开。

Matrix Calculation

有了上面的基础,现在可以简单的快速的讲清楚self-attention中进行矩阵运算时到底发生了些什么。


第一步是计算得到Q,K,V三个矩阵,我们会把我们的输入句子处理成一个矩阵,方式很简单,先将每个位置上的单词进行词嵌入成向量,然后拼接成矩阵的形式X,并且将其分别与权重矩阵WQ,WK,WV(可训练参数)进行相乘,如下图所示:
在这里插入图片描述
这里的话需要说明一下,我们用了4个boxes来表示一个单词的词嵌入后的向量的长度,而实际上是高为句子长度,宽为512的向量,权重矩阵用了长度为3的boxes来表示,实际上是一个高为512,宽为64的矩阵,因此相乘之后得到的是高为句子长度,宽为64的Q,K,V矩阵。


最后,我们将按照上文的相同的方式来处理这些矩阵,唯一的不同是,不像向量最后需要将所有位置上的z进行相加,这里我们已经把z按照矩阵的形式拼接起来了,那么可以得到self-attention的计算公式如下图所示:
在这里插入图片描述

3、多头self-attention

  paper中进一步指出可以通过multi-head的方式来进一步改进self-attention层。这个方法主要在下面两个方面来增强注意力层的性能:

  • a、它扩展了模型关注语句中不同位置的能力。在我们上面提及的例子中,z1包含了一点句子中其他位置单词的编码信息,但是z1依旧是被自身对应的单词的意思所主导的。因此当我们在翻译这句话时“The animal didn’t cross the street because it was too tired”,此时我们更关注它与句子中其他位置的单词关联的意思而并不是“它”本身的含义了。
  • b、多头给注意力层提供了多个可以表示的子空间。正如我们接下去将看到的,对于多头注意力机制,我们不再是仅有一组WQ/WK/WV的权重矩阵了,而是拥有多组的WQ/WK/WV权重矩阵(paper中指出使用了八个注意力头,因此后面的每一个encoder/decoder都会拥有八个注意力头)。每个集合都是随机初始化的,然后经过训练之后,每个Q/K/V的集合用于将输入编码投影到不同的表示子空间中,下图展示了“Thinking Machines”这句话在其中两个注意力头的子空间中经过权重矩阵WQ01/WK01/WV01之后得到的Q01/K01/V01矩阵:

在这里插入图片描述
如果我们按照上述所提及的方式做同样的工作,需要八次用不同的权重矩阵进行相同的操作,我们就可以得到八组表示的子空间z(针对句子中的同一个位置):
在这里插入图片描述
这给我们带来了一点挑战。对于FFNN来说,它并不希望输入八个矩阵,而是希望输入一个矩阵,因此我们需要将这八个矩阵压缩成一个矩阵然后输入给FFNN
我们可以这样做,将八个子空间中的表示在列的方向上拼接成一个大矩阵,然后设计一个额外的权重矩阵WO(可训练的参数)来让这个大矩阵乘以这个权重矩阵,那么可以想到,在我们这个案例中,这个WO矩阵应该是512 * 512的维度:

第一个512是因为最后的输出应该是512维度的词向量(在列方向上),第二个512则是64 * 8 = 512(行方向上):
在这里插入图片描述

以上就是multi head self-attention的全部内容,这里包含了相当多的矩阵运算,我们来将他们放在同一张图中查看一下:
在这里插入图片描述