【Transformer】Attention is All You Need - 模型理解
背景:为什么要用Self-Attention替代RNN?
在Transformer横空出世之前,自然语言处理领域的主导者是RNN(循环神经网络)及其变体(LSTM、GRU)。要理解Transformer为什么是革命性的,我们得先看看RNN的"死穴"。
RNN的死穴一:无法并行训练。 RNN处理序列的方式非常"老实":第1步读第1个词,算出一个隐状态;第2步用这个隐状态加上第2个词,算出新的隐状态;第3步再用新隐状态加第3个词……如此循环。这意味着,处理一个100词的句子,RNN必须走100步,每一步都依赖前一步的结果。即使在GPU上,这100步也无法同时执行——它们必须排着队,一步一步来。想象一下,你有一本100页的书,RNN只能一页一页地翻,而Transformer可以同时摊开所有100页。
RNN的死穴二:长距离依赖衰减。 当RNN处理长句子时,第1个词的信息要经过几十上百步的传递才能影响到第100个词。每传递一步,信息就会因为连乘操作而稀释——这就是臭名昭著的"梯度消失"问题。结果是,RNN对近距离的词语关系记得很清楚(比如相邻的形容词和名词),但对远距离的依赖关系(比如段落开头提到的人名,在段落结尾用"他"来指代)就力不从心了。
Self-Attention的解法:一步到位。 Self-Attention的核心思想简单而优雅——让句子中的每个词都和所有其他词直接"对话"。处理一个100词的句子时,每个词同时关注其他99个词,计算两两之间的关联度。这一步是矩阵乘法,GPU最擅长的事,可以完全并行。100个词、1000个词、甚至10000个词(当然,复杂度是O(n²),实际有上限),在Self-Attention中都是一次矩阵运算完成。
这个"并行"能力是Transformer能在大规模语料(比如整个互联网的文本)上训练的根本原因。原论文《Attention Is All You Need》中明确表态:Self-Attention不仅计算效率高(每层的计算复杂度远低于RNN在长序列上的表现),而且"可解释性"更好——你可以直接可视化Attention权重,看到"哪个词在关注哪个词"。比如在翻译任务中,你可以清楚地看到,当模型翻译出"cat"时,它正盯着原文中的"猫"。
| 对比维度 | RNN/LSTM | Self-Attention |
|---|---|---|
| 并行能力 | 序列化,无法并行 | 完全并行(矩阵运算) |
| 长距离依赖 | 梯度消失,信息衰减 | 任意两个位置直接交互 |
| 计算复杂度(每层) | O(n·d²) | O(n²·d) |
| 可解释性 | 隐状态难以解读 | Attention权重可直接可视化 |
用简短的话概括整个Transformer从训练到工作的过程:
Q/K/V的检索比喻:像在图书馆查资料一样理解Attention
在深入Transformer的具体流程之前,我们先建立一个直觉——Attention机制的Q/K/V到底在做什么?一个非常形象的比喻是图书馆查资料:
Q(Query,查询):你想查什么? 比如你对"猫的行为习性"感兴趣,这个"查询意图"就是Q。在Transformer中,每个token都会产生自己的Q,相当于每个词都在问:“句子里的哪些词跟我最相关?”
K(Key,索引标签):每本书的标签告诉你它讲什么。 图书馆里每本书的侧脊上都贴着标签——“猫科动物”、“宠物饲养”、“动物行为学"等等。这些标签就是K,让查询者能快速判断这本书大概在讲什么。
V(Value,内容):书的具体内容。 标签对上了,你就可以打开这本书阅读详细内容。V就是这本书真正的知识。
整个过程是这样的:
-
Q和K做点积 → 匹配度打分。 你用你的查询意图Q,和每本书的标签K做比较。Q和K越相似(向量方向越一致),点积值越大,说明这本书越符合你的需求。比如"猫的行为习性"这个查询,和标签为"动物行为学"的书点积得分很高,和标签为"量子力学"的书得分就接近零。
-
Softmax → 把匹配度变成权重。 你对所有书的匹配度做一个Softmax归一化,让它们变成一组加起来等于1的权重。比如最后发现:“动物行为学"这本书占40%的权重,“宠物饲养"占30%,“猫科动物百科"占20%,其他书加起来10%。
-
加权取V → 综合提取信息。 你按这些权重去取每本书的内容(V),权重高的书的内容被提取的比例更大。最终你得到的,是一个综合了所有相关信息、但重点突出了最相关内容的"答案向量”。
这整个过程就是Attention机制的核心公式:
$$\text{Attention}(Q, K, V) = \text{Softmax}\left(\frac{Q K^T}{\sqrt{d_k}}\right) V$$
你可能注意到了公式中有一个 除以√d_k 的操作。这个设计的目的是什么呢?当d_k(K向量的维度)很大时,Q和K的点积值会变得非常大。大的点积值经过Softmax后,会趋于"两极分化”——最大的那个值接近1,其他的都接近0。这会导致Softmax的梯度几乎为零,模型无法有效学习。除以√d_k相当于做了一次"温度调节”,让点积值的分布保持在一个合理的范围内,梯度不会消失。
这个"温度调节"的直觉:Q和K的每个维度都是独立随机变量(假设均值为0,方差为1),那么Q·K的结果的方差就是d_k。除以√d_k后,方差被归一化回1,点积值不会随维度增大而膨胀。
1、训练:
对于Encoder,[修改说明: Transformer是并行处理单个句子内的所有token,而非"大量句子”——将"输入原语言的大量句子,对于每一个句子"微调为"输入原语言的一个句子"]输入原语言的一个句子,首先进行每个token的词义嵌入和位置嵌入,两个叠加变成E。然后通过E*Wq, E*Wk, E*Wv,求得整个句子的Q, K, V。然后叠加到原本的E上面,修改C = LayerNorm(E + ΔE),其中ΔE = Attention(Q, K, V) = Softmax( (Q K^T) / sqrt(d_k) ) V,之后把修改过的E矩阵输出,输出编码矩阵C(不使用Masking)
关于位置编码:为什么要告诉模型"顺序"?
你可能注意到上面提到的"位置嵌入"(Position Embedding),它和词义嵌入叠加后才得到最终的E。为什么需要位置编码呢?
Self-Attention本身是"无序"的。 这是一个非常关键的性质:如果你把句子"我爱你"中三个词的顺序完全打乱——“我你爱”、“你我爱”、“爱你我”、“爱你我”——无论哪种排列,Self-Attention计算出来的词与词之间的相似度得分都是一模一样的。因为在Self-Attention中,每个词只看和谁"内容相关",完全不关心谁在前谁在后。
但语言中顺序显然极其重要——“狗咬人"和"人咬狗"是两种完全不同的新闻。所以我们必须显式地告诉模型,每个token在句子中处于什么位置。
两种主流方案:
| 方案 | 原理 | 代表模型 |
|---|---|---|
| 位置嵌入(Learnable) | 给每个位置编号(0, 1, 2, …),每个编号对应一个可训练的向量,和词嵌入直接相加。模型在训练中自己学会"位置0和第3个位置应该有什么不同”。 | GPT系列 |
| 三角函数编码(Sinusoidal) | 用sin/cos函数根据位置生成固定的编码,不同频率的sin/cos组合能自然地反映出相对位置关系。不需要学习,节省参数。 | 原始Transformer |
原始Transformer论文使用的是三角函数编码(固定公式),而GPT等后续模型更多采用了可学习的位置嵌入。本文描述的架构使用的是位置嵌入方案:在词嵌入向量上直接加一个位置向量,这样同一个词在不同位置会有不同的表示。比如"苹果"这个词出现在句子开头和结尾时,虽然词义嵌入相同,但因为位置嵌入不同,模型实际看到的表示是不一样的。
关于LayerNorm和残差连接:让梯度有一条"高速公路"
在Encoder的流程中,你看到公式 C = LayerNorm(E + ΔE)——这里面藏着一个深层网络能够稳定训练的核心秘诀。
残差连接(Residual Connection):E + ΔE。 “残差"指的是"增量”——不是让网络从头学习输出,而是让网络只学习"需要在E的基础上修改多少"。形式上写成 E + ΔE,其中ΔE = Attention(…)。这个加法的意义非常深远:在反向传播时,梯度可以通过这条加法路径直接传到最底层,不会因为经过多层网络而衰减殆尽。你可以把残差连接想象成一条信息高速公路——没有红绿灯,没有收费站,信息可以直达目的地。
LayerNorm(层归一化):对每个样本的特征维度做归一化。 每经过一次Attention或FFN,数据的分布可能会发生偏移——某些维度变得特别大,某些维度变得特别小。如果不加控制,经过几十层后分布会越来越歪,模型难以收敛。LayerNorm的作用就是对每个样本自身的特征做归一化,让输出分布保持稳定(均值约为0,方差约为1)。
两者配合的威力: 残差连接让信息可以自由跨层流动——既可以在Attention层被"精炼",也可以选择走"旁路"跳过Attention直接传递。LayerNorm则确保从旁路回来的信息和经过Attention处理的信息能和平共处,分布上不会打架。正是这一对黄金组合,让Transformer可以堆叠到几十层(BERT-base是12层,GPT-3是96层)而不会退化崩溃——这在ResNet之前的深度学习中是很难想象的。
类比理解:残差连接就像你修改Word文档时用的"审阅模式"——你不会从头重写整篇文档,而是在原有文档上做增删改。LayerNorm就像排版检查——确保你新增的内容字体、字号和原文一致,不会显得突兀。
多头注意力的分工:让多个"专家"同时看一句话
上面描述的单头Attention只能学到一种关系模式——比如"这个形容词修饰哪个名词"。但语言中的关系是多层次的,一个词可能同时参与多种关系。多头注意力(Multi-Head Attention)的设计就是为了解决这个问题:就像请多个专家同时分析同一句话,每个专家从不同角度给出见解。
具体来说,多头注意力把Q、K、V分别通过不同的投影矩阵映射到不同的"子空间":
- Head 1 可能学会关注**“主谓搭配”**——比如在"猫吃鱼"中,“猫"的Q和"吃"的K匹配置信度很高(因为主语和动词的关系模式很固定)。
- Head 2 可能学会关注**“指代消解”**——“小明说他很好"中,“他"会高度关注"小明”,模型通过这个head理解代词指向。
- Head 3 可能学会关注**“位置相邻”**——前后的词往往构成短语,这个head会关注紧邻的前后词语。
- Head 4 可能学会关注**“否定和情态”**——“不”、“没有”、“可能"这些词如何影响句子中其他词的语义。
每个head都有自己独立的一套投影矩阵:(Wq_head1, Wk_head1, Wv_head1)、(Wq_head2, Wk_head2, Wv_head2)……这些矩阵是独立随机初始化、独立学习的。因为初始化和学习路径不同,它们自然会分化出不同的功能——这正是神经网络最迷人的地方:功能分化不需要人工设计,而是从数据中自动涌现。
最后,把所有head的输出在维度上拼接(Concat)起来,再做一次线性变换,相当于**“综合各专家的意见,给出最终结论”**:
$$\text{MultiHead}(Q, K, V) = \text{Concat}(\text{head}_1, \text{head}_2, …, \text{head}_h) W^O$$
其中每个head的结果是:$\text{head}_i = \text{Attention}(Q W_i^Q, K W_i^K, V W_i^V)$
在实际的Transformer中(以原始论文为例),h=8个head,每个head的维度是d_k = d_model/h = 512/8 = 64。这样总的计算量和单头大维度Attention几乎相同,但学到了8种不同的关系模式。
对于Decoder,他的目的是关注原语句中关键词的部分。它输入目标语言的对应翻译目标序列target,同步输入来自Encoder的编码矩阵C,预测下一个token生成是什么的概率。在训练时,Decoder的输入是右移一位的目标序列(teacher forcing机制,例如输入 token1 token2 …,预测token1 token2 … ),而非直接"对应翻译目标序列target”。这样能并行计算所有位置的预测概率,与真实target计算交叉熵损失。这点在推理阶段才真正自回归生成(逐token预测)。
最后根据和目标序列target的交叉熵来进行梯度下降和反向传播,调整所有W中的参数
2、工作阶段
对于Encoder,通过Self-Attention,和训练状态类似
=>其内部的参数Wq,Wk,Wv目的是让每个token都能变成表达语义的向量
对于Decoder,Decoder由多个层(block)组成,每层包含三种子层:Masked Self-Attention、Cross-Attention和前馈网络。
第一层是Masked Self-Attention,每次的输入是模型自回归预测的目标语言token,输出一个对于预测token的编码C’矩阵(和Encoder类似)
=>其内部参数Wq’, Wk’, Wv’目的是学习目标语言的生成规律(类似词义空间)
第二层是Cross-Attention,输入MSA层的预测矩阵C’作为Q矩阵的生成矩阵,Q = C’Wq’,同时输入来自Encoder的编码C矩阵,得到K = CWk’, V = C*Wv'
=>其内部参数Wq’’, Wk’’, Wv’’ 目的是让Decoder学会在源语言语义空间里找对应的词,了解句子结构和重点
Decoder到这时候,计算完Cross-Attention的QKV之后,输出一个C’’,这里的C’‘最终会通过下面的神经网络转换成分数->概率
第三层是一个前馈神经网络(FFN),FFN可以让模型深度思考每个token的语义,输出的维度进行维度对齐,和Decoder输出的维度一致。它输出预测的向量表示,最后再通过 Linear + Softmax 映射到词表概率。
Linear线性层要做的是:把每个 h_t(维度 d)映射到词汇表的打分空间(维度 V,V是词汇表中的所有词数量)。最后输出一个t x V大小的矩阵,表示当前对于V个词,每个词的分数,然后再转换成概率
=>每层Decoder都有自己独立的一组 Wq, Wk, Wv 参数,不在层间共享。
例子:翻译I have a cat。比如我的Decoder自回归到”我"这个序列,然后自回归输入了Decoder,这时候,Decoder继续通过MSA层把”我"词嵌入,得到一个输入矩阵X’,然后进行CrossAttention,把X’和原本的输出矩阵C进行Cross-Attention操作,最后得到了一个Decoder的输出矩阵C’,然后经过FNN,得到C’’,再进行线性变换,先映射到词表,然后进行Softmax转换成概率分布,最后自回归"我有"
训练与推理的核心区别
理解到这一步,我们可以清晰地总结训练和推理(工作阶段)的本质不同:
| 维度 | 训练阶段 | 推理(工作)阶段 |
|---|---|---|
| 输入 | Encoder输入源语言句子 + Decoder输入完整目标序列(右移一位,Teacher Forcing) | Encoder输入源语言句子 + Decoder输入已生成的部分序列 |
| 并行性 | 完全并行——Decoder一次性看到所有目标token,同时计算所有位置的预测 | 串行(自回归)——每次只预测一个token,把它拼回去再预测下一个 |
| Masking | Masked Self-Attention遮住未来信息,防止"作弊"(看到答案) | 同样使用Mask,但此时是自然地"看不到未来" |
| 计算目标 | 计算交叉熵损失 + 反向传播 + 更新参数 | 只做前向传播,参数冻结,逐token采样 |
| 输出 | 每个位置的概率分布(和真实target对比求loss) | 最后一个位置的预测token(拼回输入继续生成) |
一句话概括:训练时,模型看着"标准答案"(完整的目标序列)来学习翻译;推理时,模型只能一个字一个字地"憋"出来,每憋一个字,都把之前憋的结果重新看一遍。
下图为具体结构示意图
