参考资料:
https://zhouyifan.net/2022/11/12/20220925-Transformer/
https://zhuanlan.zhihu.com/p/505105707
Transformer的诞生
总结:机器翻译领域,过去都是用RNN或基于RNN的架构来处理。但是RNN本轮的输入状态取决于上一轮的输出状态,这使RNN的计算必须串行执行。因此,RNN的训练通常比较缓慢。
RNN的发展历史
RNN的循环机制使得上一时间步产生的结果, 能够作为当下时间步输入的一部分,能够很好利用序列之间的关系, 因此针对自然界具有连续性的输入序列, 如人类的语言, 语音等进行很好的处理, 广泛应用于NLP领域的各项任务, 如文本分类, 情感分析, 意图识别, 机器翻译等。
传统RNN的缺陷
这种简单的RNN架构仅适用于输入和输出等长的任务。然而,大多数情况下,机器翻译的输出和输入都不是等长的。因此,人们使用了一种新的架构。前半部分的RNN只有输入,后半部分的RNN只有输出(上一轮的输出会当作下一轮的输入以补充信息)。
传统encoder-decoder的缺陷
但是这种架构存在不足:编码器和解码器之间只通过一个隐状态来传递信息。在处理较长的文章时,这种架构的表现不够理想。
为此,有人提出了基于注意力的架构。这种架构依然使用了编码器和解码器,只不过解码器的输入是编码器的状态的加权和,而不再是一个简单的中间状态。每一个输出对每一个输入的权重叫做注意力,注意力的大小取决于输出和输入的相关关系。这种架构优化了编码器和解码器之间的信息交流方式,在处理长文章时更加有效。
即便如此,因为依赖上一轮的输出状态,还是只能串行,所以RNN的训练通常比较慢。
Transformer的设计动机
- 提升训练的并行度
- 规避RNN的使用,完全使用注意力机制来捕捉序列之间的依赖关系
注意力机制
注意力计算的例子
“注意力“这个名字取得非常不易于理解。这个机制应该叫做“全局信息查询”。
做一次“注意力”计算,其实就跟去数据库了做了一次查询一样。假设,我们现在有这样一个以人名为key(键),以年龄为value(值)的数据库:
{
张三: 18,
张三: 20,
李四: 22,
张伟: 19
}
现在,我们有一个query(查询),问所有叫“张三”的人的年龄平均值是多少。让我们写程序的话,我们会把字符串“张三”和所有key做比较,找出所有“张三”的value,把这些年龄值相加,取一个平均数。这个平均数是(18+20)/2=19。
但是,很多时候,我们的查询并不是那么明确。比如,我们可能想查询一下所有姓张的人的年龄平均值。这次,我们不是去比较key == 张三,而是比较key[0] == 张。这个平均数应该是(18+20+19)/3=19。
或许,我们的查询会更模糊一点,模糊到无法用简单的判断语句来完成。因此,最通用的方法是,把query和key各建模成一个向量。之后,对query和key之间算一个相似度(比如向量内积),以这个相似度为权重,算value的加权和。这样,不管多么抽象的查询,我们都可以把query, key建模成向量,用向量相似度代替查询的判断语句,用加权和代替直接取值再求平均值。“注意力”,其实指的就是这里的权重。
把这种新方法套入刚刚那个例子里。我们先把所有key建模成向量,可能可以得到这样的一个新数据库:
{
[1, 2, 0]: 18, # 张三
[1, 2, 0]: 20, # 张三
[0, 0, 2]: 22, # 李四
[1, 4, 0]: 19 # 张伟
}
假设key[0]==1表示姓张。我们的查询“所有姓张的人的年龄平均值”就可以表示成向量[1, 0, 0]。用这个query和所有key算出的权重是:
dot([1, 0, 0], [1, 2, 0]) = 1
dot([1, 0, 0], [1, 2, 0]) = 1
dot([1, 0, 0], [0, 0, 2]) = 0
dot([1, 0, 0], [1, 4, 0]) = 1
之后,我们该用这些权重算平均值了。注意,算平均值时,权重的和应该是1。因此,我们可以用softmax把这些权重归一化一下,再算value的加权和。
softmax([1, 1, 0, 1]) = [1/3, 1/3, 0, 1/3]
dot([1/3, 1/3, 0, 1/3], [18, 20, 22, 19]) = 19
这样,我们就用向量运算代替了判断语句,完成了数据库的全局信息查询。那三个1/3,就是query对每个key的注意力。
Scaled Dot-Product Attention
Transformer里的注意力就叫Scaled Dot-Product Attention
我们刚刚完成的计算差不多就是Transformer里的注意力,这种计算在论文里叫做放缩点乘注意力(Scaled Dot-Product Attention)。它的公式是:
K就是key向量的数组,也就是:
K = [[1, 2, 0], [1, 2, 0], [0, 0, 2], [1, 4, 0]]
V是value向量数组
刚才的例子只做了一次查询,操作写为:
其中,query q就是[1, 0, 0]了。
实际上,我们可以一次做多组query。把所有
打包成矩阵Q,就得到了公式
论文里把value向量的长度叫做
为什么要用一个和
计算相似度不止点乘这一个方法,另一种常用的注意力函数叫做加性注意力,它用一个单层神经网络来计算两个向量的相似度。相比之下,点乘注意力算起来快一些。出于性能上的考量,论文使用了点乘注意力。
自注意力机制
自注意力也是一种注意力机制,只不过它计算序列中某个词和其他所有词的权重。
这个查询query,不是来自外部(数据库的例子),而是从序列本身出发,所以叫self。
如何产生 b1 这个向量?需要衡量输入向量之间的关联度 α。
计算 attention 的模组算出α(关联度大小),有很多方法,比如 Dot-product 方法和 Additive 方法。
下文主要是讲 Dot-product 方法,这是最常用的方法,也是 Transformer 中用的方法。
这个过程要重复进行,在实际中,每个向量向量和自己、和其他向量都要做一次。
得出所有的
最后还需要一个v来抽取信息。
上面计算b的过程,可以并行计算。
多头注意力机制
一种 relevance 还不够,需要多种 relevance。也即是需要多个 q,k,v
其中h是”头“数,
Transformer 模型架构
残差连接
Transformer使用了和ResNet类似的残差连接,即设模块本身的映射为
。和ResNet不同,Transformer使用的归一化方法是LayerNorm。
另外要注意的是,残差连接有一个要求:输入x和输出F(x)+x的维度必须等长。在Transformer中,包括所有词嵌入在内的向量长度都是
前馈网络
架构图中的前馈网络(Feed Forward)其实就是一个全连接网络。具体来说,这个子网络由两个线性层组成,中间用ReLU作为激活函数。
整体架构与掩码多头注意力
Decoder部分的Outputs例子
Decoder部分还有个输入是Outputs,要分为训练和推理阶段来理解。
1. 训练阶段
任务目标
将“I love you” 翻译为中文 “我爱你”
输入与输出
原始目标序列(Ground Truth)
[
Decoder输入(右移后):
[
Decoder输出(预测目标):
[我, 爱, 你,
处理过程
输入右移:将目标序列的起始符
掩码机制:Decoder的自注意力层通过掩码矩阵(Mask)遮盖未来词,确保预测第 t 个词时只能看到前 t-1 个词。
并行计算:所有位置的词同时处理,但掩码确保模型无法“偷看”未来的词。
意义
对齐训练与推理:模拟推理时的逐步生成过程(如生成“爱”时只能看到“, 我”)。
防止信息泄漏:如果直接输入原始目标序列 [
2. 推理阶段
输入英文 “I love you”,逐步生成中文翻译。
步骤 | Decoder输入 | 预测输出 | 已生成序列 |
---|---|---|---|
1 | [\ |
我 | [我] |
2 | [\ |
爱 | [我, 爱] |
3 | [\ |
你 | [我, 爱, 你] |
4 | [\ |
\ |
[我, 爱, 你, \ |
如果不右移会发生什么
训练阶段:
输入原始目标序列 [\
结果:模型可能直接复制未来词,而非学习“爱”对应“love”的映射,导致训练无效。
推理阶段:
无影响,因为模型只能依赖已生成的词。
技术细节
起始符 \
终止符 \
掩码层
掩码机制实现了两个关键目标:
- 防止模型在训练时“偷看”未来答案(避免信息泄漏)
- 允许整个序列的并行计算(提升训练效率)
掩码注意力机制的数学公式:
掩码矩阵示例(长度为4):
未来位置设置为负无穷(softmax后权重为0),当前位置为0。
这样通过矩阵运行一次性处理整个序列,提升训练效率。
执行流程
输入序列
得到了
接下来,数据还会经过一个多头注意力层。这个层比较特别,它的K,V来自
K,V的值是让
嵌入层
和其他大多数序列转换任务一样,Transformer主干结构的输入输出都是词嵌入序列。词嵌入,其实就是一个把one-hot向量转换成有意义的向量的转换矩阵。在Transformer中,解码器的嵌入层和输出线性层是共享权重的——输出线性层表示的线性变换是嵌入层的逆变换,其目的是把网络输出的嵌入再转换回one-hot向量。如果某任务的输入和输出是同一种语言,那么编码器的嵌入层和解码器的嵌入层也可以共享权重。
“论文中写道:“输入输出的嵌入层和softmax前的线性层共享权重”。这个描述不够清楚。如果输入和输出的不是同一种语言,比如输入中文输出英文,那么共享一个词嵌入是没有意义的。”
上面这段话的意思是:“输入输出的嵌入层和softmax前的线性层共享权重” 这一设计在单语言任务(如文本生成、摘要)中有效,但在跨语言任务(如中译英)中确实不适用。
1. 共享权重的设计背景
适用场景:当输入和输出使用同一种语言的词汇表时(如英文到英文的文本生成),共享权重是合理的。
具体实现:
输入嵌入层(Input Embedding):将输入词(如英文单词)映射为向量。
输出嵌入层(Output Embedding):在生成时,将目标词(同样是英文单词)映射为向量。
Softmax前线性层:将解码器的隐藏状态映射到词汇表空间,生成词的概率分布。
共享权重:输入嵌入矩阵(Embedding Matrix)与Softmax前线性层的权重矩阵(Projection Matrix)共享。
2. 跨语言任务的问题
词汇表差异:
输入语言(如中文)和输出语言(如英文)的词汇表完全不同,共享权重会导致以下问题:
维度不匹配:中文词汇表大小(如50,000词)与英文词汇表大小(如30,000词)不同,矩阵维度无法对齐。
语义不匹配:中文词“苹果”与英文词“apple”虽语义相同,但共享同一嵌入向量会混淆语言特性。
具体矛盾示例:
中文输入词“猫”的嵌入向量被强制映射到英文词“cat”的概率分布,但两者的词汇ID完全不同,导致模型无法学习有效映射。
由于模型要预测一个单词,输出的线性层后面还有一个常规的softmax操作。
位置编码
现在,Transformer的结构图还剩一个模块没有读——位置编码。无论是RNN还是CNN,都能自然地利用到序列的先后顺序这一信息。然而,Transformer的主干网络并不能利用到序列顺序信息,它本身不知道词的顺序。所以必须额外告诉模型每个词的位置,否则它会把“猫→垫子”和“垫子→猫”当成一样的情况(想象你在读一句话:“猫坐在垫子上”。如果把这句话的词打乱成“垫子坐在猫上”,意思就完全反了。)。。因此,Transformer使用了一种叫做“位置编码”的机制,对编码器和解码器的嵌入输入做了一些修改,以向模型提供序列顺序信息。
具体的做法就是给每个词的位置添加一组数字,使用一组正弦和余弦函数来生成位置编码。通过正弦和余弦的交替,模型可以学习到“相对位置”关系。有的维度关注短距离位置(高频变化),有的维度关注长距离位置(低频变化)。
如果使用1,2,3会导致长序列数值过大(如第1000个词的位置编码是1000),影响数值稳定性,从而影响模型训练。
为什么要用自注意力
自注意力层是一种和循环层和卷积层等效的计算单元。它们的目的都是把一个向量序列映射成另一个向量序列,比如说编码器把x映射成中间表示z
。论文比较了三个指标:每一层的计算复杂度、串行操作的复杂度、最大路径长度。
前两个指标很容易懂,第三个指标最大路径长度需要解释一下。最大路径长度表示数据从某个位置传递到另一个位置的最大长度。比如对边长为n的图像做普通卷积操作,卷积核大小3x3,要做
- 可以并行
- 计算复杂度会降低(这里指改进后的,让每个元素查询最近的r个元素)
- 最大路径长度降低,因为是全局查询,可以在
Q,K,V相关问题
什么是Q,K,V
输入数据(原始输入数据经过词嵌入和嵌入位置编码后)经过三种不同功能的线性变化后得到的矩阵。
Q:当前字词计算与其他字词相关性的矩阵
K:其他字词与Q计算相关性的矩阵
V:对Q-K相关性进行加权求和的矩阵
为什么需要Q,K,V三个不同的矩阵,而不是一个或两个
三个矩阵能保证模型表达能力最大化的同时,不会进一步增大计算量。
如果使用两个矩阵
我们可以理解为任意两个矩阵相同。
若Q和K共享矩阵(Q=K),则查询和匹配依据被强行绑定,模型无法区分“需要找什么”和“能被找到的特征”,导致注意力(注意力就是权重)计算失效。
若K和V共享矩阵(K=V),则匹配依据与实际内容被强制一致,模型无法区分“哪些特征用于匹配”和“哪些信息需要传递”。
若Q和V共享矩阵(Q=V),模型无法区分“需要关注什么”(Q)和“实际传递什么”(V)。(从公式角度来说,Q既用于计算权重,又作为被加权的值,导致信息重复压缩。这个和K=V是一样的)
从数学的角度来说,Q,K,V经过线性变化讲输入映射到不同空间,合并矩阵会导致投影空间被压缩,限制模型的表达能力。
如果使用一个矩阵
如果只使用一个矩阵,那么模型退化为普通的前馈网格,无法捕捉长距离依赖和上下文关系。