

这是我开了一个新坑——经典算法复现的第一站:Transformer 算法复现。我将在这篇博客中对 Transformer 的架构和底层逻辑进行记录,也作为我个人学习的一个分享。算法复现的 PyTorch 源码会托管在我的仓库中。
什么是Transformer?
Transformer 模型本质上是预训练语言模型,大都采用自监督学习(Self-supervised Learning)的方式在大量语料上进行训练,并根据下游任务有根据地微调参数,从而获得良好表现。
下面是NLP领域两种经典常用的预训练任务:
下一词预测
- 基于句子的前n个词对下一词进行预测,输出依赖于过去和当前的输入,故又称为因果语言建模(Casual language modeling)。
- 比如 GPT
上下文预测
- 基于上下文推测句子中被遮盖的词语(Masked word),故又称为遮盖语言建模(Masked language modeling)
- 比如 Bert
具有 Transformer 架构的模型往往分为三类
- 纯 Encoder 模型,又称为自编码模型(Auto-encoding Transformer)
- 纯 Decoder 模型,又称为自回归模型(Auto-regressive Transformer)
- Encoder-Decoder模型,又称Seq2Seq模型(Sequence-to-sequence Transformer)
Transformer 模型往往采用 预训练 + 微调 训练范式,可以大大减少模型预训练过程中产生的开销,使用少量数据即可获取不错的性能。
模型解构 | Transformer from scratch
在这一小节,我们具体讨论谷歌在2017年发表的经典作 Attention Is All You Need 中提出的 Transformer 架构。
Simplified version from illustrated-transformer:
Transformer 是具有编码器-解码器结构的 Encoder-Decoder 模型,编码组件由六个编码器构成,而解码组件则由相同数量的解码器构成。编码器组在结构上相同,但不共享权重,每一个编码器分为两个子层: 自注意力层(Self-Attention Layer)与前馈神经网络层(Feed Forward Neaural Network Layer)。
其中,自注意力层帮助编码器在对特定单词进行编码时查看输入句子的其他单词,帮助单词捕捉序列中的长距离依赖关系。前馈神经网络层将每个词收集到的上下文信息进行处理与整合。
解码器在具有编码器两个子层的基础上,在这两个子层之间加入了一个注意力层,帮助解码器关注输入句子的相关部分。
下面,我们将对这几个层进行拆解,分别解释每一个层的组成、实现和功能。
自注意力 | Self-attention
计算自注意力的第一步时从每个编码器的输入向量中创建三个向量。对于每一个单词,我们创建一个查询向量(Query),一个键向量(Key)和一个值向量(Value)。通过将词嵌入向量乘以三个矩阵线性变换得到(即输入词向量通过一个全连接层)。
多头自注意力是在编码器输入/输出向量维度的基础上进行切分,这使得多头自注意力:
- 使模型可以同时关注多个重点区域。
- 为模型的注意力层提供了多个表示子空间。
- 维持计算复杂度的相对稳定性。
自注意力得分计算Query向量与Key向量的点积,为了避免过大的点积结果将Softmax函数推向梯度很小的位置,导致梯度消失,我们同时要对点积的结果进行缩放,将缩放后的结果传递到Softmax函数产生归一化得分。
接下来,我们将每一个值向量乘以Softmax分数得到注意力层的输出:
位置编码 | Positional Encoding
Transformer 使用位置编码表示序列的顺序和位置信息,其与词嵌入向量有着相同的维度,这使得他们可以通过相加进行信息聚合。在 Transformer 论文中,使用不同频率的正余弦函数进行位置编码:
- 上一行公式计算偶数位置上的位置嵌入。
- 下一行公式计算奇数位置上的位置嵌入。
和 分别代表偶数和奇数位置上位置嵌入的第i维值。 - pos 表示当前位置的索引。
表示模型的维度大小,即输出向量的长度。
这种编码方式确保了不同的位置会有不同的向量表示,并且在连续的位置之间,位置嵌入的变化体现出一种平滑过渡和周期性的特性,理论上可以覆盖任意长度的序列。
其作用体现在如下几个方面:
- 捕捉次序信息:理解输入词顺序,提高依赖关系分析、句法分析性能。
- 防止重复使用相同输入:提高泛化性。
- 提高鲁棒性:更好地处理噪声值和异常值。
层归一化 | Layer Normalization
归一化操作是深度学习领域的经典数据处理方法,带来更好的数据分布稳定性、尺度不变性与更平滑的优化地形,因此可以有效提高训练效率。
首先,在 CV 领域中有一个与层归一化相似的概念,叫做批量归一化,这种处理方法可选地应用于神经网络的各个层。由于在神经网络前向传播可能导致变量分布的漂移,且更深(更复杂)的网络更容易过拟合,批量归一化便可以充当正则化手段,同时也带来了预处理、数值稳定性的好处。
对批量归一化作了介绍之后,我们再回过头看层归一化。虽然这两者的命名有些相似,归一化逻辑也有共通之处,但与批量归一化对神经网络中间层产生的批量输出作归一化不同的是,层归一化对每一层的输出进行归一化。
这么说可能难以理解,但是我们可以想象一下,以上文编码器产生的输出来看,其输出是形状为[batch_size, sequnce_length, input_dim]的三维张量,如果按照批量归一化的逻辑,将会对相同 input_dim 下,对 batch_size, sequence_length 两个维度做归一化处理,相当于我说了十句话,然后模型将这十句话的第一个词串联起来进行归一化,然后又将第二词串联起来再进行归一化,以此类推,这很明显是反直觉的。
实际上,我们可以将层归一化理解为:对于 sequence_length 每一维嵌入的词向量,将该词向量的每一维 input_dim 串联起来进行归一化,相当于我说十句话,然后模型将每一句话的所有词串联起来进行归一化,这样便很好地保留了整句话的空间逻辑与语义信息,同时稳定了同一句话的量纲分布。
层归一化计算公式:
可以看到,层归一化在对激活进行标准化的同时,引入两个可学习参数(
残差连接 | Residual Connection
在编码器结构中,每个 Encoder 的子层(自注意力,ffnn)周围都有一个残差链接,紧跟层归一化步骤。
残差连接适用于规避在训练更深的网络中的梯度消失问题,这使得 Transformer 得以很好的训练,从而获得不错的性能表现。
我们同样可以在解码器的子层中找到残差连接。
前向传播 | Feed Forward
除了注意力子层,每一个编码器 / 解码器结构都能看到一个前向传播神经网络(FFNN),这是一个带有一个隐藏层的全连接神经网络,它涵盖了两层线性变换,并在第一层输出加入ReLU激活函数引入非线性特征。
FFNN主要的作用是接收来自注意力层的输出,并对其做进一步的线性变换,以捕获更复杂的特征和表示。该子层的输入向量和输出向量都是
编码器-解码器 | Encoder-decoder
编码器-解码器架构是 Transformer 的核心架构。编码器、解码器分别由N(一般为6)个编码器单元、解码器单元构成,每个单元都是对上述模型机制的排列组合。
编码器层由一个多头自注意力层、一个前馈神经网络层构成。
数据输入自注意力层后分为两支,一支进入多头自注意力网络计算自注意力矩阵,另一只被一个残差连接 skip 到归一化层前,与自注意力层的输出(会经过drop out)加和后馈入归一化层进行层归一化。
自注意力层的输出进入前馈神经网络层也分为两支,一支进入 ffn 进行进一步特征提取,另一支被一个残差连接 skip 到归一化层前,与 ffn 的输出 (会经过drop out)加和后,输出在线性变换后会作为k、v输入解码器层,与解码器输入q一并计算cross attention。
解码器层由两个多头自注意力层、一个前馈神经网络层构成,数据的流动方式和编码器层大同小异。
不同点在于,第一个多头自注意力层加入了掩码,这是为了防止 Transformer 关注到它不应该看到的东西。Transformer 推理时是一个一个词预测,而训练时会把所有的结果一次性给到 Transformer,但效果等同于一个一个词给,而之所以可以达到该效果,就是因为在 attention score 矩阵中加入了掩码,防止其看到后面的信息,也就是不要让前面的字具备后面字的上下文信息。
第二个多头自注意力层引入了交叉注意力机制,输入的词向量将作为 quary 和编码器的输出经过线性变换产生的key、value 进行注意力计算,从而找到和 key 寓意更接近的 value。
残差连接等机制则和编码器层一致。
至此,我们便完成了对编码器-解码器架构的理解,可以开始 coding。
Transformer 的工作模式
相信很多人都和我一样,在看完论文后,仅仅是知道了 Transformer 是什么,大概在做怎样一件事,但对于其具体的工作模式,往往知之甚少。
在复现 Transformer 的过程中,我不仅尝试复现了模型解构,也对基于 Transformer 的机器翻译实战做了初步探索,事实也证明了实践出真知的道理,通过实践,我大概明白了 Transformer 的推理流程。
首先放上一张 Illustrat Transformer
我们可以发现,当 Transformer 执行对第 n 个词汇概率分布的推理时,它会将前 n - 1 词输入 Decoder 架构并计算自注意力,然后,它又与 Encoder 的输出执行交叉注意力,最后经过线性层和一个 Softmax 归一化输出下一次概率。
我在看论文的时候就有点想不明白,模型的解码器是如何实现从第一个词开始,动态改变输入进行词序列预测的,后面写代码的时候发现其实也很简单,只需要设置一个 for 循环,设置输入起始值为 start token,下一词预测如果是 end token 的话跳出,否则将其加入 sentence token 序列,继续输入 decoder 进行下一词预测就好了。
可见,有的时候纸面上写的很玄虚的东西,实际操作一下往往会发现也就这么个事,动手实践的好处在这里得到了很好的体现。
写在最后
我的文笔不好,总结也比较混乱,写这篇博客的目的更多的是记录自己的学习经历和一些问题、经验。如果在读这篇博客的你能够从中获得一些帮助,便是我极大的荣幸与快乐。
深入浅出 Transformer, 完。
参考文献
[1] Attention Is All You Need
[2] Illustrate Transformer
[3] Youtube Vedio | Transformers from scratch
[4] MultiHead-Attention和Masked-Attention的机制和原理