本文参考自知乎专栏:https://zhuanlan.zhihu.com/p/54356280,并结合自己的一些理解记录下来方便以后查看。
目录
1. Transformer架构
首先,Transformer架构理论来源是《Attention is All You Need》,它主要有编码组件、解码组件以及它们之间的连接组成。
编码组件部分由一堆编码器(encoders)构成(论文中是将6个编码器叠在一起,6个编码器的效果要好于1个编码器,就像CNN中使用多个卷积核效果优于只使用一个),如下图:
2. 编码器
所有的编码器在结构上都是相同的,它们之间没有共享参数(可以理解为它们是6个独立的参数空间);而每个解码器都可以分为自注意力层和前馈网络层:
从编码器输入的句子首先会经过一个自注意力层(self-attention),这层的作用是帮助编码器在对每个单词编码时关注输入句子的其他单词(主要是计算Q、K、V的过程)。
自注意力层的输出会传递到前馈神经网络中(feed-forward),每个位置的单词对应的前馈神经网络都完全一样。
3. 解码器
解码器中也有编码器的自注意力层(self-attention)和前馈层(feed-forward)。除此之外,这两个层之间还有一个编码-解码自注意力层,用来关注输入句子的相关部分。
4. Transformer中的张量
张量,是矢量概念的推广,简单理解为矢量是一阶张量,矩阵是二阶张量。
首先将输入单词通过词嵌入算法转换为词向量:
每个单词都被嵌入512维的向量,简单用上面的方框表示这些向量。
词嵌入过程只发生在最底层的编码器中。所有的编码器都有一个相同的特点,即它们接收一个向量列表,列表中的每个向量大小为512维。在底层编码器中它就是词向量,在其他编码器中,它就是下一层编码器的输出。向量列表大小是我们可以设置的超参数(一般是训练集中最长句子的长度)。
将输入序列进行词嵌入之后,每个单词都会流经编码器中的两个子层。
其中,Transformer的一个核心特性就是,在输入序列中每个位置的单词都有自己独特的路径流入编码器。在自注意力层中,这些路径之间存在依赖关系(各个单词之间存在序列关系,会有位置编码);而前馈层没有这些依赖关系,因此在前馈层可以并行执行各种路径。
5. 编码过程
一个编码器(从最底层的那一个算起)接收向量列表作为输入,接着将向量列中中的向量传递到自注意力层进行处理,然后传递到前馈神经网络中,将输出结果传递到下一个编码器中。
5.1 从宏观上看自注意力机制
举个例子:
下列句子是需要翻译的输入句子:
The animal didn't cross the street because it was too tired
思考:这个 it 在这个句子中是指什么呢? 它指的是street还是animal呢?这对于人类来说是一个简单的问题,但是对于算法则不是。
当模型处理这个单词 it 的时候,自注意力机制会允许 it 与 animal建立联系。随着模型处理输入序列的每个单词,自注意力会关注整个输入序列的所有单词,帮助模型对本单词更好地进行编码。
如RNN网络,它会将已经处理过的前面所有的单词/向量的表示与它正在处理的当前单词/向量结合起来;而自注意力机制会将所有相关单词的理解融入到我们正在处理的单词中。
当我们在编码器#5(栈中最上层编码器)中编码it这个单词时,注意力机制的部分会去关注 The Animal ,将它的表示的一部分编入it的编码中。
5.2 微观上看自注意力机制的计算步骤
通过了解如何使用向量来计算自注意力,然后使用矩阵来实现。
计算自注意力的第一步就是从每个编码器的输入向量(词向量)中生成三个向量,也就说对于每个单词,我们创造一个查询向量、一个键向量和一个值向量。这三个向量是通过词嵌入与三个权重矩阵后相乘创建的。
可以发现这些向量在维度上比词嵌入向量维度更低,它们的维度是64,而词嵌入和编码器的输入/输出向量的维度是512。但实际上不强求维度更小,这只是一种基于架构上的选择,它可以使多头注意力(multiheaded attention)的大部分计算保持不变。
注意: 、 、属于模型中的参数,一开始随机初始化,通过模型逐渐收敛而固定。
与 权重矩阵相乘得到 ,就是与这个单词相关的查询向量。最终使得输入序列的每个词向量创建一个查询向量、一个键向量和一个值向量。
计算自注意力的第二步是计算得分,假设我们在为这个例子中的第一个词 ''Thinking'' 计算自注意力向量,我们需要拿输入句子中的每个单词 对 ''Thinking'' 打分。这些分数决定了在编码单词 ''Thingking'' 的过程中有多重视句子的其他部分。
这些分数是通过打分单词(所有输入句子的单词)的键向量与 ''Thinking'' 的查询向量相点积来计算的。所以如果我们是处理位置最靠前的词的自注意力的话,第一个分数是q1和k1的点积,第二个分数是q1和k2的点积。
第三步和第四步是将分数除以8(8是论文中使用的键向量的维数64的平方分,这会让梯度更稳定),也可以使用其他值,8是默认值;然后通过softmax将所有单词的分数归一化,得到的分数都是正值且和为1.
这个softmax分数决定了每个单词对编辑当下位置(Thinking)的贡献。
第五步是将每个值向量乘以softmax分数(这是为了准备之后将它们求和)。
第六步是对加权值向量求和,然后即得到自注意力层在该位置的输出(例子中是对应第一个单词)。注意:自注意力的另一种解释是在编码某个单词时,将所有单词的值向量进行加权求和,权重是该词的键向量与被编码单词的查询向量的点击并softmax得到。
这六步可以用下图表示:
这样,自注意力的计算就完成了,得到的向量就可以传给前馈神经网络了。
5.3 自注意力的矩阵运算
第一步是生成查询矩阵、键矩阵和值矩阵。为此,我们将输入句子的词嵌入装进矩阵X中(X的维度通常是:batch_size*dimension),将其程以我们训练的权重矩阵( 、 、)。
这里,X中的每一行对应输入句子中一个单词的词向量。q/k/v向量的维度是64维,词向量是512维。
最后,可以将5.2节中的第2到第6步合并为一个公式来计算自注意力层的输出:
6. Transformer中的多头注意力机制
通过增加一种叫做“多头”注意力的机制(multi-headed attention),论文进一步完善了自注意力层,并在两方面提高了注意力层的性能:
- 它扩展了模型专注于不同位置的能力。在上面的例子中,虽然每个编码都在z1中有或多或少的体现,但是它可能被实际的单词本身所支配。如果我们翻译一个句子,比如,"The animal didn't cross the street because it was too tired",我们会想到 "it"指的是哪个词,这时模型的"多头"注意力机制会起作用。
- 它给出了多头注意力层的多个“表示子空间”(representation suspaces)。接下来我们将看到,对于 多头注意力机制,我们有多个查询/键/值权重矩阵集(Transformer中有8个注意力头,所以每个编码器/解码器有8个矩阵集合)。这些集合中的矩阵参数都是随机初始化的,在训练之后,每个集合都被用来将输入词嵌入(除了第一层是词嵌入,其他层来自较低编码器/解码器的向量)投影到不同的表示子空间中。
在多头注意力机制下,我们为每个头保持独立的 查询/键/值权重矩阵,从而产生不同的 查询/键/值矩阵。
如果我们做与5.2节相同的自注意力计算,只需8次不同的权重矩阵运算,我们就会得到8个不同的Z矩阵:
注意:这种计算给我们带来了一点挑战,因为前馈层不需要8个矩阵,它只需要一个矩阵,我们只需要将这些每个头产生的Z矩阵拼接在一起,然后用一个附加的权重矩阵 与它们相乘即可:
下面尝试用一张图概括自注意力的所有计算过程:
这就是自注意力的全部计算过程,还算简单吧,不过思路确实巧妙。
7. 使用位置编码表示序列的顺序
为了解决输入单词顺序的表示问题,Transformer为每个输入的词嵌入添加了一个向量。这些向量遵循模型学习到的特定模式,这有助于确定每个单词的位置,或序列中不同单词之间的距离。这里的解决方法是将位置向量直接添加到词嵌入中使得它们在接下来的运算中,能够更好地表达词与词之间的距离。
如果我们假设词嵌入的维数是4,则实际的位置编码如下:
举个例子:
在下图中,每一行对应一个词向量的位置编码,所以第一行对应着输入序列的第一个词。每行包含512个值,每个值介于-1和1之间。
20字的位置编码实例,词嵌入大小为512.可以看到它从中间分裂成两半,左半部分的值是由一个正弦函数生成;右半部分由余弦函数生成,然后将它们拼接在一起得到每一个位置编码向量。
原始论文里描述了位置编码的公式(第3.5节)。你可以在 get_timing_signal_1d()中看到生成位置编码的代码。这不是唯一可能的位置编码方法。然而,它的优点是能够扩展到未知的序列长度(例如,当我们训练出的模型需要翻译远比训练集里的句子更长的句子时)
8. 残差模块
在编码器的每个子层(包括自注意力、前馈网络层)的周围都有一个残差连接,并且都跟随着一个 “层-归一化”步骤。(Bert中是 Add&Norm 模块)
层-归一化步骤(Add&Norm):
如果我们去可视化这些向量以及这个和自注意力相关联的层-归一化操作,那么看起来就像下面这张描述一样:
解码器的子层也是这样的,比如一个2层的编码-解码结构的transformer:
9. 解码过程中的一些细节
编码器通过处理输入序列开启工作。顶端编码器的输出之后会转化为一个包含向量K(键向量)和V(值向量)的注意力向量集。这些向量将被每个解码器用于自身的“编码-解码注意力层”,而这些层可以帮助解码器关注输入序列哪些位置合适:
在完成编码阶段后,则开始解码阶段。解码阶段的每个步骤都会输出一个输出序列的元素。
接下来的步骤重复了这个过程,直到到达一个特殊的终止符号,它表示transformer的解码器已经完成了它的输出。每个步骤的输出在下一个时间步被提供给低端解码器,并且就像编码器之前做的那样,这些解码器会输出它们的解码结果。另外,就像我们对编码器的输入所做的那样,我们会嵌入并添加位置编码给那些解码器,来表示每个单词的位置。
而那些解码器中的自注意力层表现的模式与编码器不同:在解码器中自注意力层只被允许处理输出序列中更靠前的那些位置,在softmax步骤前,它会把后面的位置给隐去(把它们设置为-inf)。
这个“编码-解码注意力层”工作方式基本就像多头自注意力层一样,只不过它是通过下面的层来创造查询矩阵,并且从编码器的输出中取得键/值矩阵。
10. 最终的线性变换和softmax层
解码组件最后会输出一个实数向量,我们如何把浮点数变成一个单词?这便是线性变换层要做的工作,它之后就是softmax层。
线性变换是一个简单的全连接神经网络,它可以把解码组件产生的向量投射到一个比它大得多的、被称为对数几率(logits)的向量里。
不妨假设我们的模型从训练集中学习一万个不同的英语单词(模型的“输出词表”)。因此对数几率向量为一万个单元格长度的向量-----每个单元格对应一个单词的分数。
接下来的softmax层便会把那些分数变成概率(都为正数,上限是1.0).概率最高的单元格被选中,并且它对应的单词被作为这个时间步的输出。
上图中,以解码器组件产生的输出向量开始,之后会转化为一个输出单词。