大家好,我是半虹,这篇文章来讲 Transformer \text{Transformer} Transformer,想要获取更多相关文章,欢迎关注 自然语言处理 专栏
在之前的两篇文章中,我们介绍过序列到序列模型以及注意力机制在序列到序列模型中的应用
所谓序列到序列模型,就是那些输入是序列、输出也是序列的模型,这是从输入输出的角度来定义的
这些模型通常会使用编码器到解码器的架构,编码器用于处理输入序列,解码器用于生成输出序列
由于循环神经网络在处理序列数据时的优势,很多模型都会选择循环神经网络作为编码器和解码器
循环神经网络会逐步遍历序列,并通过隐状态的传递保存序列的历史信息,从而达到理解序列数据的目的
这是循环神经网络能够处理序列数据的关键,同时也是循环神经网络的局限所在
循环神经网络处理序列数据时需要逐步遍历,这是顺序性的,这个过程难以并行
此外,隐状态在长序列间进行传递容易丢失信息,使网络难以对长序列进行建模
另一方面,研究人员发现,注意力机制在序列到序列模型中的应用对性能的提升有很大帮助
后来就有人提出一个开创性的想法,能不能整个序列到序列模型就用注意力机制,不再基于循环神经网络
当然,我们知道这个尝试是成功了,这就是大名鼎鼎的 Transformer \text{Transformer} Transformer, Attention Is All You Need \text{Attention Is All You Need} Attention Is All You Need
Transformer \text{Transformer} Transformer 不仅使训练过程可以并行,而且还能对长序列进行建模!
Transformer \text{Transformer} Transformer 虽然最初是在 NLP \text{NLP} NLP 领域中提出的,但其后来应用到 CV \text{CV} CV 领域也有亮眼的表现
可以说 Transformer \text{Transformer} Transformer 中的注意力机制开创了 RNN \text{RNN} RNN 和 CNN \text{CNN} CNN 之外的一种全新范式
下面将会从总体到细节逐步剖析 Transformer \text{Transformer} Transformer 作为序列到序列模型时的运作方式
从本质上来说, Transformer \text{Transformer} Transformer 是一个编码器到解码器架构,下图是编码器到解码器架构的示意图
图片左半部分是编码器,处理输入序列得到编码信息,右半部分是解码器,根据编码信息生成输出序列
这里需要特别注意的是,解码器一般都是自回归生成,这意味着解码器会将上一步输出作为下一步输入
所以在这里我们能看到,解码器会将偏移一步之后的输出序列作为其输入 ( Teacher Forcing \text{Teacher Forcing} Teacher Forcing )
另外,编码器是由若干个编码层组成的,解码器是由若干个解码层组成的
在 Transformer \text{Transformer} Transformer 出现之前,编码层和解码层大多都是基于循环神经网络进行设计
而 Transformer \text{Transformer} Transformer 另辟蹊径,选择完全基于注意力机制进行设计,下图是其编码层和解码层的实现细节
如图所示,左半部分展示的是编码层的细节,右半部分展示的解码层的细节,二者皆有若干层堆叠
图中相同背景色的矩形框表示相同的子模块,大致上可以分为三种:
黄色背景的是多头注意力机制,绿色背景的是前馈神经网络,蓝色背景的是残差连接以及层正则化
运作步骤如下:
下文会依次解析其中的核心操作,分别是:
序号 | 操作 | 出现步骤 |
---|---|---|
1 | 获取序列向量表示 | 1.1 、2.1 |
2 | 多头注意力机制 | 1.2.1 、2.2.1 、2.2.2 |
3 | 前馈神经网络 | 1.2.2 、2.2.3 |
4 | 残差连接 | 1.2.1 、1.2.2 、2.2.1 、2.2.2 、2.2.3 |
5 | 层正则化 | 1.2.1 、1.2.2 、2.2.1 、2.2.2 、2.2.3 |
我们知道,自然语言是无法直接作为模型输入的,所以第一步首先要把自然语言转化成向量表示
一般来说,只需将序列经过嵌入层得到对应序列中每个词元的可学习向量即可,具体如下图所示:
这样的做法在 RNN \text{RNN} RNN 或 CNN \text{CNN} CNN 架构中没有问题,但 Transformer \text{Transformer} Transformer 的 Attention \text{Attention} Attention 是无法建模位置信息的
为了弥补这个缺陷,在获取序列向量表示时需要额外加入位置信息:
如图所示,每个词元经过词元嵌入层得到语义表示,每个词元的位置经过位置嵌入层得到位置表示
然后对应的语义表示和位置表示通过相加的方式进行融合以得到最终表示: e i = t i + p i , i = 1 , 2 , ⋯ e_{i} = t_{i} + p_{i},\ i = 1, \ 2,\ \cdots ei=ti+pi, i=1, 2, ⋯
当然你也可以用拼接的方法对两个表示进行融合,相加只是论文中的设计
但是需要注意的是,如果用相加的方法进行融合,语义表示和位置表示的维度必须要是一样的
事实上,在论文中为了实现方便,同时也是为了进行残差连接
在编码层和解码层的每个子模块,其输入输出向量的特征维度都是一样的,具体来说特征维度 d = 512 d = 512 d=512
下面是本节的最后一个问题,如何获得位置表示?
最简单的方法就是像语义表示一样,经过嵌入层得到一个可学习的向量,然后通过数据来训练
但是,论文提出可以通过公式直接计算位置表示,具体的计算公式如下:
PE ( p , 2 i ) = sin ( p / 1000 0 2 i / d ) PE ( p , 2 i + 1 ) = cos ( p / 1000 0 2 i / d ) \begin{align} \text{PE}(p, 2i) &= \sin (p\ /\ 10000^{2i/d}) \\ \text{PE}(p, 2i + 1) &= \cos (p\ /\ 10000^{2i/d}) \end{align} PE(p,2i)PE(p,2i+1)=sin(p / 100002i/d)=cos(p / 100002i/d)
对于词元位置 p p p,位置向量的维度为 d d d,向量中的第 2 i 2i 2i 和 2 i + 1 2i+1 2i+1 维计算公式如上所示
一个例子如下:
通过上述方式获得位置表示有以下优点:
多头注意力机制是整个模型的核心,且不说多头是什么操作,我们先来看注意力机制是啥
在上一篇文章中,我们讲过注意力机制在序列到序列模型中的应用,这里还是以翻译为例引入
假设现在我们要进行中译英任务,将 机器学习
翻译成 Machine Learning
当生成 Machine
时,模型其实应该将更多权重放在 机器
二字
当生成 Learning
时,模型其实应该将更多权重放在 学习
二字
注意力机制在本质上是根据当前查询以不同的权重抽取序列中的信息
序列中的每个元素会有一个键向量用于匹配,另有一个值向量表示其蕴含的信息
当给定查询向量时,会通过以下两个步骤得到最终要抽取的信息向量
首先,计算查询向量与每个键向量之间的匹配分数
然后,以匹配分数作为权重对所有值向量加权求和
其中最关键的部分就是打分函数的设计,简单来说就是如何衡量查询向量和键向量之间的匹配程度
在 Transformer \text{Transformer} Transformer 中所使用的打分函数是 scaled dot product \text{scaled dot product} scaled dot product,其计算公式如下: score ( Q , K ) = Q K T d k \text{score}(Q, K) = \frac{QK^T}{\sqrt{d_k}} score(Q,K)=dkQKT
其中, Q Q Q 为查询向量, K K K 为键向量, d k d_k dk 为键向量维度,以下是一些细节的解答
为什么要做 scale \text{scale} scale?
假设 q q q、 k k k 都是均值为 0 0 0、方差为 1 1 1 的独立随机变量,那么二者点积 q ⋅ k q \cdot k q⋅k 的均值为 0 0 0、而方差为 d k d_k dk
如果 d k d_k dk 越大,那么点积方差越大,经过 softmax \text{softmax} softmax 之后,分布变得陡峭,梯度变小
二是为什么设计 scale \text{scale} scale 的值是 1 d k \frac{1}{\sqrt{d_k}} dk1?
假设 q q q、 k k k 都是均值为 0 0 0、方差为 1 1 1 的独立随机变量,那么二者点积 q ⋅ k q \cdot k q⋅k 的均值为 0 0 0、而方差为 d k d_k dk
如果将点积乘上缩放因子 1 d k \frac{1}{\sqrt{d_k}} dk1,可以使得方差稳定在 1 1 1,这是因为 D ( q ⋅ k d k ) = d k ( d k ) 2 = 1 D(\frac{q \cdot k}{\sqrt{d_k}}) = \frac{d_k}{(\sqrt{d_k})^2} = 1 D(dkq⋅k)=(dk)2dk=1
当查询向量来自序列本身时,此时的注意力机制被称为自注意力机制
序列中的每个元素对应三个向量,一是查询向量,二是键向量,三是值向量,那么如何得到这些向量呢
简单的话,可以直接将序列元素的向量表示同时作为上述向量,也可以将其经过不同的线性映射后得到
下面是自注意力机制的运作过程
Self Attention
在自注意力机制中,每个元素都会根据自身查询来获取序列的上下文信息,从而得到对应的向量表示
对比循环神经网络,二者都可用于编码序列,其不同之处在于自注意力机制能够并行地编码序列
这是因为序列中每个元素的注意力计算之间并没有依赖关系,因此可以通过矩阵运算来并行加速
反观循环神经网络则需要通过在序列中逐步传递隐状态来获取序列的上下文信息,这个过程不能并行
回到模型, Transformer \text{Transformer} Transformer 中共有三种注意力机制,对应到运行步骤分别是 1.2.1
、2.2.1
、2.2.2
1.2.1
是编码器自注意力机制,查询向量来自编码器,键向量、值向量也来自编码器
2.2.1
是解码器自注意力机制,查询向量来自解码器,键向量、值向量也来自解码器
2.2.2
是解码器外注意力机制,查询向量来自解码器,键向量、值向量则来自编码器
上述的三种注意力机制都会带有填充掩码 padding mask
这是由于神经网络在进行训练时,一般是以批量为单位的,但是同一批量里的序列通常长短不一
为了进行矩阵运算,需要用预先定义好的占位符,将同一批量里的序列填充到统一长度
然而,在计算注意力权重分布时,我们并不希望给这些占位符分配注意力权重
因此,在查询向量和键向量计算匹配分数后,将占位符的匹配分数设置为一个极小的值是合理的
解码器自注意力机制另有一个独特的掩码 decoder mask
这是因为解码过程是自回归的,就是在生成当前的词元时,只能看到之前的词元,不能看到之后的词元
如果直接使用注意力机制的话,那么在计算当前的词元时,会用自身查询与序列所有词元计算匹配分数
这样做显然是一个作弊的行为,使用什么方法可以避免呢
我们只需要将当前词元与之后词元的匹配分数设置为一个极小的值即可,如此就能保证训练和预测行为一致
至此终于把注意力机制介绍完了,最后再来看一下多头注意力机制是啥
上述介绍的注意力机制,序列中的每个元素都会对应一组查询、键、值向量,然后进行一次注意力计算
为了增强模型表达能力,序列中的每个元素可映射到多组查询、键、值向量,然后分别进行注意力计算
我们希望不同的映射能够在不同的特征空间学习到不同的信息,以此达到增强模型表达能力的目的
最后,多个特征空间中注意力计算的结果将会被拼接起来,然后经过一个线性映射得到最终的输出
前馈神经网络是为了增强模型的表达能力而设计的,将原有的向量表示映射到一个更好的特征空间
在本模型中,前馈神经网络由两个线性映射和一个激活函数组成,具体的公式表达如下:
FFN ( X ) = Linear 2 ( ReLU ( Linear 1 ( X ) ) ) = max ( 0 , X W 1 + b 1 ) W 2 + b 2 \text{FFN}(X) = \text{Linear}2(\text{ReLU}(\text{Linear}1(X))) = \max(0,\ X W_1 + b_1) W_2 + b_2 FFN(X)=Linear2(ReLU(Linear1(X)))=max(0, XW1+b1)W2+b2
其中, Linear \text{Linear} Linear 是线性映射, ReLU \text{ReLU} ReLU 是激活函数, W 1 W_1 W1、 b 1 b_1 b1、 W 2 W_2 W2、 b 2 b_2 b2 都是可学习的参数
输入的 X X X 特征维度是 512 512 512,经过第一个线性映射后变为 2048 2048 2048,经过第二个线性映射后变回 512 512 512
残差连接有助于缓解深层网络在训练时出现的梯度问题,其核心思想非常简单,示意图如下所示:
输入经过某个模块得到输出后,再将原始输入与模块输出相加才得到最终输出
一般来说,正则化的作用是保证数据特征分布稳定,从而加速模型的收敛速度
常用的正则化方法包括以下两种:
BatchNorm
:批正则化,对所有样本的每个特征做正则化,一般用于 CV
领域LayerNorm
:层正则化,对每个样本的所有特征做正则化,一般用于 NLP
领域在本模型中使用的正则化方法是层正则化
上述的描述或许还是有些抽象,下面会用例子对比来讲两种正则化方法的异同
对于二维的情况,给定数据为:[B, F]
BatchNorm
:正则化维度:[1, *]
,表示对每个 F
在所有 [B]
上做正则化LayerNorm
:正则化维度:[*, 1]
,表示对每个 B
在所有 [F]
上做正则化对于三维的情况,给定数据为:[B, S, F]
BatchNorm
:正则化维度:[1, 1, *]
,表示对每个 F
在所有 [B, S]
上做正则化LayerNorm
:正则化维度:[*, 1, 1]
,表示对每个 B
在所有 [S, F]
上做正则化对于三维的情况,给定数据为:[B, H, W, C]
BatchNorm
:正则化维度:[1, 1, 1, *]
,表示对每个 C
在所有 [B, H, W]
上做正则化LayerNorm
:正则化维度:[*, 1, 1, 1]
,表示对每个 B
在所有 [H, W, C]
上做正则化对于一个批量数据,输入序列的长度通常是不相等的
当序列长度变化较大时 ,使用 BatchNorm
训练得到的参数波动会很大
所以在处理序列数据时 ,选择 LayerNorm
是更合适的
好啦,本文到此结束,感谢阅读!
如果你觉得这篇文章还不错的话,欢迎点赞、收藏、关注,你的支持是对我最大的鼓励 (/ω\)