3.2 Transformer架构 - 从零开始的完全指南
📖 目录
- 🎯 开篇:为什么要学Transformer?
- 第一部分:预备知识
- 第二部分:Self-Attention机制
- 第三部分:多头注意力
- 第四部分:Transformer完整架构
- 第五部分:原始Attention的三大缺陷
- 第六部分:Gated Attention改进
- 第七部分:数学推导和实现细节
- 🧪 自我检验:25道思考题
- 📚 核心要点总结
- 🎓 下一步学习
🎯 开篇:为什么要学Transformer?
你可能会问:Transformer到底是什么?
12岁版解释
想象你在看一本书,理解一个句子时,你不是一个字一个字地看,而是会同时关注整个句子中的所有字,看它们之间的关系。比如”我爱吃苹果”,你会同时看到”我”、“爱”、“吃”、“苹果”,并理解它们的关系。
Transformer就是这样一个模型:它能同时看到所有信息,并找出它们之间的关系。
为什么Transformer这么重要?
- GPT系列(ChatGPT、GPT-4):基于Transformer
- BERT:基于Transformer
- 所有现代大语言模型:都基于Transformer
一句话:不懂Transformer,就无法理解现代AI。
第一部分:预备知识 - RNN的困境
这一部分解决什么问题?
背景问题:在Transformer出现之前(2017年之前),处理文本序列主要用RNN(循环神经网络)。但RNN有两个致命缺陷。
RNN的问题:
- 记忆衰减:处理长序列时,前面的信息会逐渐被遗忘
- 无法并行:必须按顺序一个一个处理,训练速度极慢
为什么需要Transformer:彻底解决这两个问题,让模型能同时看到所有位置的信息,并且可以并行计算。
1.1 什么是序列到序列任务?
12岁版解释
把一串东西变成另一串东西。
例子:
- 机器翻译:中文 → 英文
- 文本摘要:长文章 → 短摘要
- 对话系统:问题 → 回答
1.2 RNN的问题是什么?
在Transformer之前,大家用**RNN(循环神经网络)**处理序列。
RNN的工作方式:
一个字 → 处理 → 下一个字 → 处理 → 下一个字 → ...
问题:
- 慢:必须一个一个处理,无法并行
- 记不住远处的信息:处理第100个字时,第1个字的信息已经忘了
类比
就像你读一本书,只能一个字一个字地读,而且读到后面就忘了前面讲了什么。
1.3 Transformer的核心思想
一句话:同时看所有信息,找出它们之间的关系。
类比
- RNN:像用手电筒照路,一次只能看一小块
- Transformer:像开灯,一次看到整个房间
关键机制:Self-Attention(自注意力)
第二部分:Self-Attention机制 - Transformer的核心突破
这一部分解决什么问题?
核心问题:RNN只能顺序处理,无法同时看到所有位置。如何让模型”同时看到”整个序列,并计算任意两个位置之间的关系?
解决方案:Self-Attention机制 —— 通过Q、K、V三个矩阵,计算每个位置对其他所有位置的注意力权重。
突破点:
- 打破了顺序处理的限制,可以并行计算
- 任意两个位置的距离都是1(直接连接),解决了长距离依赖问题
- 计算复杂度是O(n²),但可以并行,实际速度比RNN快很多
局限:O(n²)的复杂度意味着序列长度翻倍,计算量变4倍。这就是为什么后来有了各种改进(如Gated Attention)。
2.1 什么是Attention?
12岁版解释:查字典
想象你在查字典:
- 你有一个要查的字(Query,查询)
- 字典里有很多字(Key,键)
- 每个字都有解释(Value,值)
你的过程:
- 拿着要查的字,和字典里的每个字比较相似度
- 找到最相似的字
- 读取那个字的解释
Attention就是这个过程!
2.2 Query、Key、Value是什么?
这是Attention机制的三个核心概念。
Query(查询)
12岁版解释
你要查的东西
在Transformer中:当前位置的词,想知道”我应该关注哪些其他词?”
例子: 句子:“我爱吃苹果”
- 当处理”吃”这个字时,Query就是”吃”
- “吃”想知道:我应该关注哪些词?
Key(键)
12岁版解释
字典里的索引
在Transformer中:所有位置的词,用来和Query比较
例子:
- Key包括:“我”、“爱”、“吃”、“苹果”
- 用来和Query(“吃”)比较相似度
Value(值)
12岁版解释
字典里的解释
在Transformer中:所有位置的词的实际内容
例子:
- Value也是:“我”、“爱”、“吃”、“苹果”
- 但这是实际要提取的信息
为什么Key和Value不一样?
虽然它们来自同样的词,但:
- Key:用来匹配(“这个词和我相关吗?”)
- Value:用来提取信息(“给我这个词的信息”)
它们通过不同的线性变换得到,所以数学上是不同的。
2.3 Attention的计算过程
步骤1:计算相似度(Q·K^T)
目标:看Query和每个Key有多相似
数学:
相似度 = Query · Key^T
例子
Query: "吃" Keys: ["我", "爱", "吃", "苹果"] 相似度: - "吃" vs "我": 0.1 - "吃" vs "爱": 0.3 - "吃" vs "吃": 1.0 ← 自己和自己最相似 - "吃" vs "苹果": 0.8 ← "吃"和"苹果"很相关!
步骤2:缩放(除以√d)
公式:
缩放后的相似度 = (Q·K^T) / √d
其中d是向量的维度。
为什么要除以√d?
问题:如果向量维度很大(比如d=512),点积的结果会很大。
例子:
d=2时:Q·K^T 可能是 [0.1, 0.3, 1.0, 0.8] d=512时:Q·K^T 可能是 [50, 150, 500, 400] ← 太大了!后果:数值太大,Softmax会出问题(后面会讲)。
解决:除以√d,把数值缩小到合理范围。
类比
就像你用温度计测体温,如果刻度太大,就看不清细微差别了。
步骤3:Softmax
目标:把相似度变成概率分布(加起来等于1)
公式:
Attention权重 = Softmax(Q·K^T / √d)
Softmax是什么?
12岁版解释
把一堆数字变成百分比
数学定义:
例子
输入: [0.1, 0.3, 1.0, 0.8] 步骤1:取指数 exp([0.1, 0.3, 1.0, 0.8]) = [1.11, 1.35, 2.72, 2.23] 步骤2:除以总和 总和 = 1.11 + 1.35 + 2.72 + 2.23 = 7.41 Softmax = [1.11/7.41, 1.35/7.41, 2.72/7.41, 2.23/7.41] = [0.15, 0.18, 0.37, 0.30] 检查:0.15 + 0.18 + 0.37 + 0.30 = 1.0 ✓
为什么用Softmax?
- 归一化:所有权重加起来等于1
- 突出重点:大的值变得更大,小的值变得更小
- 可微分:可以用梯度下降训练
步骤4:加权求和(·V)
目标:根据权重,提取Value的信息
公式:
输出 = Attention权重 · V
12岁版解释
你查字典时,找到了相关的字,现在要读取它们的解释。但不是只读一个,而是按照相关程度,读取多个解释的加权平均。
例子
Attention权重: [0.15, 0.18, 0.37, 0.30] Values: "我" → [1, 0, 0] "爱" → [0, 1, 0] "吃" → [0, 0, 1] "苹果" → [0.5, 0, 0.5] 输出 = 0.15×[1,0,0] + 0.18×[0,1,0] + 0.37×[0,0,1] + 0.30×[0.5,0,0.5] = [0.15+0.15, 0.18, 0.37+0.15] = [0.30, 0.18, 0.52]解释:
- “吃”最关注自己(0.37)和”苹果”(0.30)
- 所以输出主要包含”吃”和”苹果”的信息
2.4 完整的数学公式
Self-Attention的完整公式:
其中:
- (Query权重矩阵)
- (Key权重矩阵)
- (Value权重矩阵)
- :输入(所有词的embedding)
- :可学习的参数矩阵
完整流程:
输入X →
→ Q = X·W_Q
→ K = X·W_K
→ V = X·W_V
→ 相似度 = Q·K^T
→ 缩放 = 相似度 / √d_k
→ 权重 = Softmax(缩放)
→ 输出 = 权重·V
2.5 手算一个例子
句子:“我爱吃”(3个词)
假设:
- 每个词用2维向量表示(实际是512维)
- 简化计算
输入X:
"我": [1, 0]
"爱": [0, 1]
"吃": [1, 1]
权重矩阵(简化,实际是可学习的):
W_Q = [[1, 0], [0, 1]] (单位矩阵,简化)
W_K = [[1, 0], [0, 1]]
W_V = [[1, 0], [0, 1]]
步骤1:计算Q, K, V
Q = X·W_Q = X = [[1,0], [0,1], [1,1]]
K = X·W_K = X = [[1,0], [0,1], [1,1]]
V = X·W_V = X = [[1,0], [0,1], [1,1]]
步骤2:计算相似度矩阵
Q·K^T = [[1,0], [0,1], [1,1]] · [[1,0,1], [0,1,1]]
= [[1, 0, 1],
[0, 1, 1],
[1, 1, 2]]
步骤3:缩放
d_k = 2, √d_k = 1.41
缩放后 ≈ [[0.71, 0, 0.71],
[0, 0.71, 0.71],
[0.71, 0.71, 1.41]]
步骤4:Softmax(对每一行)
第1行: [0.71, 0, 0.71] → Softmax: [0.40, 0.20, 0.40]
第2行: [0, 0.71, 0.71] → Softmax: [0.21, 0.39, 0.39]
第3行: [0.71, 0.71, 1.41] → Softmax: [0.24, 0.24, 0.52]
步骤5:加权求和
输出 = Softmax · V
第1行输出("我"的表示):
= 0.40×[1,0] + 0.20×[0,1] + 0.40×[1,1]
= [0.80, 0.60]
第2行输出("爱"的表示):
= [0.60, 0.78]
第3行输出("吃"的表示):
= [0.76, 0.76]
结果解释
- “我”的新表示:[0.80, 0.60] - 主要关注自己和”吃”
- “爱”的新表示:[0.60, 0.78] - 关注”爱”和”吃”
- “吃”的新表示:[0.76, 0.76] - 最关注自己(权重0.52)
关键发现:每个词的新表示都融合了其他词的信息!这就是Self-Attention的魔力。
第三部分:多头注意力(Multi-Head Attention)
这一部分解决什么问题?
核心问题:单个Attention只能学习一种关系模式。比如”it”指代”animal”,单个Attention可能只学会主谓关系,无法同时学习其他关系。
解决方案:多头注意力 —— 并行运行多个Attention,每个head学习不同的关系模式。
突破点:
- 不同head可以关注不同的语言现象(语法、语义、位置等)
- 所有head并行计算,不增加时间成本
- 通过拼接和线性变换,融合多个视角的信息
局限:增加了参数量(每个head都有独立的W_Q、W_K、W_V),但这是值得的。
3.1 为什么需要多头?
问题
一个Attention只能学习一种关系
例子
句子:“The animal didn’t cross the street because it was too tired”
“it”指的是什么?
- 可能是”animal”(动物累了)
- 也可能是”street”(街道太累?不对)
一个Attention head:可能只学会了一种关系(比如主谓关系)
多个Attention heads:可以学习多种关系
- Head 1:主谓关系
- Head 2:动宾关系
- Head 3:修饰关系
- …
3.2 多头的实现
核心思想:并行运行多个Attention,然后拼接
公式:
其中:
参数:
- :head的数量(通常是8)
- 每个head有自己的
流程:
输入X →
→ Head 1: Attention(Q1, K1, V1) → 输出1
→ Head 2: Attention(Q2, K2, V2) → 输出2
→ ...
→ Head 8: Attention(Q8, K8, V8) → 输出8
→ 拼接: [输出1, 输出2, ..., 输出8]
→ 线性变换: Concat · W_O
→ 最终输出
12岁版解释
就像你看一本书,用不同的角度看:
- 第1遍:看情节
- 第2遍:看人物关系
- 第3遍:看写作手法
- …
每一遍都能发现不同的东西,最后综合起来,理解更深刻。
3.3 多头的好处
- 学习多种关系:不同head关注不同的模式
- 增强表达能力:多个视角,理解更全面
- 并行计算:所有head可以同时计算,速度快
实验发现
- 有些head学会了语法关系(主谓、动宾)
- 有些head学会了语义关系(同义词、反义词)
- 有些head学会了位置关系(相邻词)
第四部分:Transformer完整架构
4.1 整体结构
Transformer = Encoder + Decoder
- Encoder:理解输入
- Decoder:生成输出
12岁版解释
- Encoder:读懂中文句子
- Decoder:翻译成英文句子
4.2 Encoder
结构:
输入 →
→ 位置编码
→ Multi-Head Attention
→ Add & Norm(残差连接 + Layer Normalization)
→ Feed-Forward Network
→ Add & Norm
→ 输出
重复N次(原论文N=6)
每一层做什么?
- Multi-Head Attention:找词之间的关系
- Add & Norm:稳定训练
- Feed-Forward Network:对每个词独立处理,增加非线性
- Add & Norm:再次稳定
4.3 Decoder
结构:
输入 →
→ 位置编码
→ Masked Multi-Head Attention(只能看之前的词)
→ Add & Norm
→ Multi-Head Attention(看Encoder的输出)
→ Add & Norm
→ Feed-Forward Network
→ Add & Norm
→ 输出
关键区别:
- Masked Attention:生成第i个词时,只能看前i-1个词
- Cross Attention:Decoder看Encoder的输出
为什么要Mask?
例子:翻译”我爱吃苹果”
生成"I"时:只能看"我" 生成"love"时:只能看"我爱" 生成"eating"时:只能看"我爱吃" ...如果能看到后面的词,就是”作弊”了!
4.4 位置编码
问题
Attention没有位置信息
例子
- “我爱吃苹果”
- “苹果吃爱我”
对Attention来说,这两个句子是一样的!(因为只看词之间的关系,不看顺序)
解决:加入位置编码
公式:
12岁版解释
给每个位置一个独特的”身份证号”,让模型知道词的顺序。
为什么用sin/cos?
- 有界:值在[-1, 1]之间
- 周期性:可以处理任意长度的序列
- 相对位置:sin和cos的性质让模型容易学习相对位置
4.5 残差连接和Layer Norm
残差连接(Residual Connection)
公式:
12岁版解释
走楼梯时,既可以走楼梯(F(x)),也可以坐电梯(x)。如果楼梯太难走,就直接坐电梯。
作用:
- 缓解梯度消失:深层网络训练更容易
- 保留原始信息:即使F(x)学不好,至少还有x
Layer Normalization
公式:
作用:
- 稳定训练:把数值归一化到合理范围
- 加速收敛:训练更快
4.6 Feed-Forward Network
结构:
12岁版解释
两层全连接网络,中间加个ReLU激活函数。
作用:
- 增加非线性:ReLU是非线性函数
- 独立处理每个词:不像Attention那样看所有词
为什么需要FFN?
Attention是线性的(后面会详细讲),FFN引入非线性,增强表达能力。
4.7 从文本到向量的完整流程
你可能会问
一句话是如何变成Transformer输入的向量的?
第一步:分词(Tokenization)
输入:原始文本
"今天天气很好"
输出:Token序列
["今天", "天气", "很好"]
常见分词方法:
- 字符级:[“今”, “天”, “天”, “气”, “很”, “好”]
- 词级:[“今天”, “天气”, “很”, “好”]
- 子词级(现代常用):[“今天”, “天气”, “很好”]
12岁版解释
就像把一句话切成小块,每一块叫一个token。
子词分词算法:
- BPE (Byte Pair Encoding):GPT系列使用
- WordPiece:BERT使用
- SentencePiece:通用工具
第二步:Token Embedding(词嵌入)
Embedding = 查表
# Embedding矩阵(可学习参数)
embedding_matrix = 随机初始化(vocab_size, d_model)
= 随机初始化(50000, 512)
# 查表
token_id = 1234 # "今天"的ID
embedding = embedding_matrix[1234] # 取第1234行
= [0.23, -0.45, 0.67, ..., 0.12] # 512维向量和Word2Vec的区别
- Word2Vec:预训练 → 固定 → 静态(一个词一个向量)
- Transformer Embedding:端到端训练 → 可更新 → 动态(通过Attention考虑上下文)
Embedding如何学习?
- 初始化:随机初始化,没有意义
- 前向传播:查表获取embedding
- 计算损失:预测下一个词的损失
- 反向传播:计算梯度(只有被使用的token有梯度)
- 梯度下降:更新embedding矩阵
# 更新规则
embedding[token_id] = embedding[token_id] - lr × ∂loss/∂embedding[token_id]为什么相似的词有相似的embedding?
- 共现关系:经常一起出现的词,embedding会变得相似
- 上下文相似:出现在相似上下文的词,embedding相似
- 任务驱动:为了预测下一个词,embedding必须学到语义
第三步:位置编码(Positional Encoding)
为什么需要?
Attention机制本身没有位置信息!
对于Attention来说,这两个句子是一样的:
"今天天气很好"
"很好天气今天"
方法1:固定的正弦位置编码(原始Transformer)
方法2:可学习的位置编码(GPT等,更常用)
# 位置编码矩阵(可学习参数)
positional_embedding = nn.Embedding(max_seq_len, d_model)
= 随机初始化(2048, 512)
# 查表
PE[0] = positional_embedding[0] # 位置0的编码
PE[1] = positional_embedding[1] # 位置1的编码第四步:相加得到最终输入
# Token Embedding (3, 512)
token_emb = [[0.23, -0.45, ..., 0.12], # "今天"
[0.56, 0.78, ..., 0.45], # "天气"
[-0.12, 0.34, ..., -0.23]] # "很好"
# Positional Encoding (3, 512)
pos_enc = [[0.00, 1.00, ..., 1.00], # 位置0
[0.84, 0.54, ..., 0.99], # 位置1
[0.91, -0.42, ..., 0.98]] # 位置2
# 相加(逐元素)
X = token_emb + pos_enc # (3, 512)这就是Transformer的输入!
为什么相加而不是拼接?
- 相加更高效,维度不变
- 实验表明效果和拼接差不多
- 参数量更少
第五部分:原始Attention的三大缺陷
这一部分解决什么问题?
背景:前面讲的Self-Attention和Multi-Head Attention是Transformer的核心,但它们并不完美。研究人员在实际使用中发现了三个严重问题。
三大缺陷:
- 低秩瓶颈:输出投影W_O被”吸收”,表达能力受限
- Attention Sink:第一个token的注意力权重异常高(46.7%)
- 注意力熵崩塌:长文本生成时,注意力分布变得极度集中
为什么重要:理解这些缺陷,才能理解后续的改进(如Gated Attention)是如何突破瓶颈的。
为什么要讲这个?
理解原始Attention的问题,才能理解后面Gated Attention的改进!
5.1 缺陷1:低秩瓶颈(线性瓶颈)
什么是秩?
12岁版解释
秩 = 独立方向的数量
想象你在一个房间里:
- 1维空间(秩=1):只能前后移动(一条线)
- 2维空间(秩=2):可以前后、左右移动(一个平面)
- 3维空间(秩=3):可以前后、左右、上下移动(整个空间)
秩越高 = 独立方向越多 = 表达能力越强
矩阵的秩例子:
A = [1 2]
[2 4]
[3 6]
观察:第2列 = 2 × 第1列
第2行 = 2 × 第1行
第3行 = 3 × 第1行
所有行都可以用第1行表示
→ 只有1个独立方向
→ 秩 = 1
原始Attention为什么是线性的?
原始Attention的公式:
分解:
步骤1:计算注意力权重
A = Softmax(Q·K^T / √d) ← 这是权重矩阵
步骤2:加权求和
z = A · V ← 线性操作!
步骤3:输出投影
o = z · W_O ← 又是线性操作!
关键问题
z = A · V是线性的(矩阵乘法)o = z · W_O也是线性的- 两个线性操作的组合还是线性!
数学证明:
其中
结论
输出投影可以被”吸收”到中,没有增加表达能力!
低秩瓶颈的数学证明
线性代数定理:
具体到Attention:
V的维度:n × d_k (比如 n × 64)
W_O的维度:d_k × d_model (比如 64 × 512)
V·W_O的维度:n × d_model (比如 n × 512)
但是:rank(V·W_O) ≤ min(n, d_k) ≤ 64
12岁版解释
就像你有一个2维的影子,无论怎么旋转,它还是2维的,无法变成3维。
后果:
- 表达能力受限:无法学习复杂的非线性关系
- 参数冗余:的作用有限
- 性能瓶颈:模型能力受限于
5.2 缺陷2:Attention Sink(注意力陷阱)
什么现象?
实验发现
在长文本生成时,第一个token的注意力权重异常高!
数据:
原始Attention:
- 第1个token的平均注意力权重:46.7%
- 其他token的平均权重:53.3% / (n-1)
例如n=100时:
- 第1个token:46.7%
- 其他99个token:平均每个0.54%
12岁版解释
就像你看一本书,总是盯着第一个字看,其他字都不怎么看。这显然不对!
为什么会这样?
理论解释:
- Softmax的特性:必须把权重分配给所有token
- 无关信息的处理:当某个位置不需要关注任何信息时,Softmax还是要分配权重
- 第一个token成为”垃圾桶”:模型学会把不需要的注意力都扔给第一个token
类比
就像你做选择题,必须选一个答案,即使所有选项都不对。你只能随便选一个(通常是A)。
为什么是第一个token(
1. 总是可见:在因果注意力中,<BOS>对所有位置都可见
- Token 0 (<BOS>): 可以看 [0]
- Token 1: 可以看 [0, 1]
- Token 2: 可以看 [0, 1, 2]
- ...
→ <BOS>是唯一对所有位置都可见的token
2. 没有语义:<BOS>只是个标记,不包含实际信息
→ 把注意力扔给它不会影响其他有用信息
3. 训练中涌现:模型自己发现了这个"技巧"
有什么危害?
- 信息失真:第一个token的信息被过度放大
- 长文本不稳定:扩展到更长序列时,问题更严重
- 浪费计算:大量注意力集中在一个无关的token上
5.3 缺陷3:缺乏稀疏性
Softmax的特性
公式:
特性
所有权重都大于0!
例子
输入:[-100, -100, 10, -100] Softmax:[0.0000...001, 0.0000...001, 0.9999..., 0.0000...001] 即使输入是-100(非常小),Softmax输出也不是0!
为什么无法真正”忽略”信息?
问题:
- 即使某个token完全无关,Softmax也会给它一个小权重
- 所有token的信息都会被混合进来
- 无法真正”关闭”某个head或某个token
12岁版解释
就像你听音乐,即使某个乐器的声音很小,你还是能听到一点点。无法完全静音。
为什么需要稀疏性?
- 提高效率:不需要的head可以关闭,节省计算
- 增强可解释性:知道哪些head在工作,哪些没用
- 防止过拟合:减少冗余信息
第六部分:Gated Attention改进
超级简单的解决方案
在Attention输出后,乘以一个sigmoid门控!
6.1 门控机制的设计
核心思想
公式对比:
原始Attention:
门控Attention:
其中:
- :门控参数(每个head一个标量)
- :sigmoid门控分数,范围
- :逐元素乘法
12岁版解释
就像给每个Attention head加一个开关,可以调节它的”音量”。
实现细节
每个head一个门控参数:
假设有8个head:
g = [g₁, g₂, g₃, g₄, g₅, g₆, g₇, g₈]
每个gᵢ是一个可学习的标量
Sigmoid的作用:
特性:
- 输出范围:
- 很大时:(门打开)
- 很小时:(门关闭)
- 时:(半开)
12岁版解释
就像一个音量旋钮,可以从0(静音)调到1(最大音量)。
门控分数的分布
实验发现
训练后的门控分数分布: - 大部分head:0.7-0.9(基本打开) - 少数head:0.1-0.3(基本关闭) - 极少数head:0.4-0.6(半开)
解释:
- 模型自动学会了哪些head有用(打开)
- 哪些head没用(关闭)
- 实现了自动稀疏化!
6.2 如何突破低秩瓶颈
非线性的力量
原始Attention(线性):
Linear → Linear = Linear
门控Attention(非线性):
Linear → NonLinear → Linear ≠ Linear
关键
sigmoid在中间插入了非线性!和无法合并了!
为什么非线性能增加秩?
线性操作不能增加秩:
非线性操作可以增加秩:
12岁版解释
就像做菜,如果只是把食材混合(线性),味道有限。但如果中间加热(非线性),就能产生新的味道!
验证Sigmoid是非线性:
例子:
- ✓
实验验证
秩的测量:
原始Attention:
平均秩 = 127.3(接近128的瓶颈)
Gated Attention:
平均秩 = 156.8(突破了128的瓶颈!)
提升:156.8 / 127.3 = 1.23(提升23%)
性能提升:
原始Attention:PPL = 6.026
Gated Attention:PPL = 5.761
提升:(6.026 - 5.761) / 6.026 = 4.4%
6.3 如何消除Attention Sink
门控的解决方案
原始Attention的问题:
Softmax约束:Σ α_ij = 1(必须分配注意力)
↓
模型有时不想关注任何位置
↓
但必须分配注意力
↓
把不想要的注意力扔给第一个token(<BOS>)
↓
第一个token获得46.7%注意力
门控Attention的解决:
门控值可以接近0:sigmoid(g) → 0
↓
整个head的输出接近0
↓
模型可以"不关注"
↓
不需要把注意力扔给<BOS>了!
实验结果
F-Attr(First-token Attention Ratio):
模型 F-Attr
---------------------------
原始Attention 46.7% ← 异常高!
门控Attention 4.8% ← 正常!
改进:降低了90%!
计算方法:
发现
门控机制让第一个token的注意力从46.7%降到4.8%,几乎消除了Attention Sink!
6.4 如何实现稀疏性
Sigmoid可以接近0
Softmax的问题:
Softmax(x) = exp(x) / Σ exp(x)
→ 输出恒 > 0
→ 无法真正为0
Sigmoid的优势:
σ(x) = 1 / (1 + exp(-x))
→ 当x → -∞时,σ(x) → 0
→ 可以接近0!
实际例子:
σ(-10) = 0.000045 ≈ 0
σ(-20) = 0.0000000002 ≈ 0
软稀疏 vs 硬稀疏
硬稀疏:直接设为0
weights = [0.5, 0, 0.3, 0, 0.2]
软稀疏:接近0但不完全为0
weights = [0.92, 0.03, 0.88, 0.05, 0.02]
门控实现的是软稀疏:
- 不需要的head:门控值≈0(如0.03)
- 有用的head:门控值≈1(如0.92)
- 可微分,可以训练
稀疏性的好处
- 计算效率:门控值<0.1的head可以跳过计算
- 可解释性:知道哪些head在工作
- 防止过拟合:减少冗余信息
6.5 实验结果详解
主要实验(15B MoE模型)
不同激活函数对比:
| 激活函数 | PPL | 相对改进 |
|---|---|---|
| 无(原始) | 6.026 | - |
| ReLU | 6.005 | 0.3% |
| GELU | 5.998 | 0.5% |
| Sigmoid | 5.761 | 4.4% ✅ |
分析:
- 任何非线性都有帮助
- Sigmoid效果最好
- 验证了非线性增益的重要性
门控位置对比
| 位置 | 描述 | PPL | 改进 |
|---|---|---|---|
| Baseline | 无门控 | 6.026 | - |
| G₂ | Value层后 | 5.820 | 3.4% |
| G₁ | SDPA输出后 | 5.761 | 4.4% ✅ |
| G₅ | 最终输出后 | 6.017 | 0.1% |
结论:G₁位置(SDPA输出后)效果最好!
长文本性能
扩展到128k tokens:
| 序列长度 | 原始PPL | 门控PPL | 改进 |
|---|---|---|---|
| 4k | 10.20 | 9.75 | 4.4% |
| 16k | 10.85 | 10.12 | 6.7% |
| 64k | 11.52 | 10.48 | 9.0% |
| 128k | 12.31 | 10.89 | 11.5% |
发现
- 序列越长,门控的优势越明显
- 原始Attention在长序列上退化严重
- 门控Attention更鲁棒
6.6 Sigmoid门控 vs 原始Attention
两层不同的机制
重要!Softmax和Sigmoid作用在不同的层面:
层面1:Token层面的注意力(Softmax)
# 在每个head内部,对token分配注意力
attention_weights = softmax(Q @ K.T / sqrt(d))
# 例如:处理"今天天气很好"
attention_weights = [0.1, 0.3, 0.4, 0.2] # 对4个token的注意力
# 今天 天气 很 好Softmax的作用:
- 决定当前位置应该关注哪些token
- 权重和为1
- 原始Attention有,Gated Attention也有
层面2:Head层面的门控(Sigmoid)
# 在多个head之间,控制哪些head工作
gate_values = [sigmoid(g_1), sigmoid(g_2), ..., sigmoid(g_8)]
# 例如:8个head的门控值
gate_values = [0.95, 0.03, 0.88, 0.05, 0.02, 0.91, 0.04, 0.06]
# 工作 关闭 工作 关闭 关闭 工作 关闭 关闭Sigmoid的作用:
- 决定哪些head应该工作
- 每个head独立控制
- 原始Attention没有,Gated Attention新增
乐队类比
想象一个乐队演奏
原始Attention(没有门控):
乐队有8个乐手:
- 钢琴手:演奏旋律A(用心分配注意力到每个音符)✅
- 小提琴手:演奏旋律B(用心分配注意力)✅
- 大提琴手:演奏旋律C(用心分配注意力)✅
- 鼓手:乱打(虽然也在"用心",但打的是噪声)❌
- 吉他手:乱弹(虽然也在"用心",但弹的是噪声)❌
问题:所有人都在演奏,包括那些制造噪声的乐手
Softmax的作用:每个乐手决定如何演奏自己的部分(注意力分配)
Gated Attention(有门控):
乐队有8个乐手 + 1个指挥(门控机制):
- 钢琴手:音量100%(σ(g₁)=0.95)✅
- 小提琴手:音量5%(σ(g₂)=0.05)← 几乎静音
- 大提琴手:音量90%(σ(g₃)=0.90)✅
- 鼓手:音量3%(σ(g₄)=0.03)← 几乎静音
- 吉他手:音量2%(σ(g₅)=0.02)← 几乎静音
改进:指挥控制每个乐手的音量,噪声被压制
Softmax的作用:每个乐手还是决定如何演奏(没变) Sigmoid的作用:指挥控制每个乐手的音量(新增)
6.7 为什么以前没人用门控?
你可能会问
Sigmoid函数很早就有了,为什么大家以前不在每个head后面加Sigmoid?
原因1:问题没被发现
Attention Sink问题是最近才被重视的
时间线:
2017年:Transformer提出,大家关注"Attention真好用!"
2018-2020年:BERT、GPT-2、GPT-3,大家忙着扩大模型
2021-2023年:开始有人注意到"第1个token获得异常高的注意力"
2024年:这篇论文系统性地解决这个问题
类比:
- 就像你买了新手机,一开始只关注”好用”
- 用了几年后才发现”电池不耐用”
- 然后才有人研究”如何优化电池”
原因2:计算资源限制
以前的模型太小,问题不明显
| 时期 | 模型规模 | Head数量 | 问题严重性 |
|---|---|---|---|
| 2017-2018 | BERT-base | 12个head | 问题不明显 |
| 2019-2020 | GPT-2 | 12-48个head | 开始出现 |
| 2021-2023 | GPT-3/4 | 96-128个head | 问题严重 |
| 2024+ | 大模型 | 更多head | 必须解决 |
当head数量少时:
- 即使有几个head是冗余的,影响不大
- 计算成本还能接受
当head数量多时:
- 冗余head浪费大量计算
- 必须找方法优化
原因3:研究惯性
“Transformer已经很好了,为什么要改?“
研究者的思维:
2017-2020年:Transformer效果好,不要乱改
2021-2022年:开始有人尝试各种改进(但很多失败了)
2023-2024年:积累了足够的理解,找到了有效的改进方法
原因4:细节很重要
很多人可能试过类似的想法,但没成功
可能尝试过的失败方案:
- 直接在head输出上加sigmoid → 破坏了数值范围
- 用固定的门控值 → 没有学习能力
- 用其他激活函数(ReLU、Tanh)→ 效果不如sigmoid
- 门控参数初始化不当 → 训练不稳定
这篇论文的贡献:
- 找到了正确的设计(独立的可学习参数)
- 找到了正确的位置(SDPA输出后)
- 找到了正确的初始化方法(从0开始)
- 证明了确实有效(大规模实验验证)
第七部分:数学推导和实现细节
7.1 Attention的复杂度分析
时间复杂度
Self-Attention的计算步骤:
# 假设:序列长度n,embedding维度d
# 1. 计算Q, K, V
Q = X @ W_Q # (n, d) @ (d, d_k) = (n, d_k)
K = X @ W_K # (n, d) @ (d, d_k) = (n, d_k)
V = X @ W_V # (n, d) @ (d, d_v) = (n, d_v)
# 2. 计算注意力分数
scores = Q @ K.T # (n, d_k) @ (d_k, n) = (n, n)
# 3. Softmax
attention = Softmax(scores) # (n, n)
# 4. 加权求和
output = attention @ V # (n, n) @ (n, d_v) = (n, d_v)复杂度分析:
| 步骤 | 操作 | 复杂度 |
|---|---|---|
| 计算Q, K, V | 3次矩阵乘法 | |
| 计算scores | ||
| Softmax | 对每行归一化 | |
| 加权求和 | attention·V |
总时间复杂度:
关键观察:
- 当时:主导(短序列)
- 当时:主导(长序列)
长序列的问题
对于长序列(如n=10000),项会变得非常大!
例如:
- n=1000, d=512: 操作
- n=10000, d=512: 操作(增加100倍!)
空间复杂度
需要存储的矩阵:
| 矩阵 | 形状 | 空间 |
|---|---|---|
| Q, K, V | (n, d_k) × 3 | |
| scores | (n, n) | |
| attention | (n, n) | |
| output | (n, d_v) |
总空间复杂度:
关键问题:的注意力矩阵!
具体例子
序列长度n=1024,d=512: - Q, K, V: 3 × 1024 × 64 = 196K参数 - attention矩阵: 1024 × 1024 = 1M参数 序列长度n=10240,d=512: - Q, K, V: 3 × 10240 × 64 = 1.96M参数 - attention矩阵: 10240 × 10240 = 105M参数(增加100倍!)
7.2 为什么Transformer比RNN快?
RNN的计算方式
# RNN必须顺序计算
h_0 = initial_state
h_1 = RNN(x_1, h_0) # 依赖h_0
h_2 = RNN(x_2, h_1) # 依赖h_1
h_3 = RNN(x_3, h_2) # 依赖h_2
...
h_n = RNN(x_n, h_{n-1}) # 依赖h_{n-1}关键问题:
- ❌ 必须等待前一步完成
- ❌ 无法并行
- ❌ 时间复杂度:步,每步
Transformer的计算方式
# Transformer可以并行计算
Q = X @ W_Q # 所有位置同时计算
K = X @ W_K # 所有位置同时计算
V = X @ W_V # 所有位置同时计算
scores = Q @ K.T # 一次矩阵乘法
attention = Softmax(scores) # 并行
output = attention @ V # 一次矩阵乘法关键优势:
- ✅ 所有位置同时计算
- ✅ 完全并行
- ✅ 时间复杂度:步(在GPU上)
并行度对比
| 模型 | 顺序操作数 | 并行度 | GPU利用率 |
|---|---|---|---|
| RNN | 低 | 低(10-30%) | |
| Transformer | 高 | 高(80-95%) |
实际速度对比
在相同硬件上:
- RNN处理1000个token:约1秒
- Transformer处理1000个token:约0.1秒
Transformer快10倍!
为什么RNN不能并行?
依赖关系:
RNN的计算图:
x_1 → h_1 → h_2 → h_3 → ... → h_n
↓ ↓ ↓ ↓
y_1 y_2 y_3 y_n
每个h_i都依赖h_{i-1},必须顺序计算
Transformer的计算图:
x_1 ↘
x_2 → Attention → y_1, y_2, y_3, ..., y_n
x_3 ↗
所有x_i同时输入,所有y_i同时输出
7.3 Embedding如何学习?
你可能会问
Embedding矩阵是随机初始化的,它是如何学到有用的表示的?
初始状态(训练前)
创建Embedding矩阵:
import torch.nn as nn
# 假设:词表大小50000,embedding维度512
vocab_size = 50000
d_model = 512
# 创建Embedding层(可学习参数)
embedding = nn.Embedding(vocab_size, d_model)
# 初始化:随机,没有意义
print(embedding.weight[1234]) # "今天"的embedding
# tensor([0.023, -0.015, 0.031, ..., -0.008]) # 512个随机数初始状态的特点:
- ❌ 完全随机,没有任何意义
- ❌ 相似的词没有相似的向量
- ❌ 无法表达语义关系
训练过程(如何学习)
完整训练循环:
# 1. 前向传播
token_ids = [1234, 5678, 9012] # "今天 天气 很好"
embeddings = embedding(token_ids) # 查表
X = embeddings + positional_encoding
output = transformer(X)
logits = output_projection(output)
# 2. 计算损失
target_ids = [5678, 9012, 1] # "天气 很好 <EOS>"
loss = CrossEntropyLoss(logits, target_ids)
# 3. 反向传播(计算梯度)
loss.backward()
# 关键:只有被使用的token有梯度!
print(embedding.weight.grad[1234]) # "今天":有梯度 ✓
# tensor([0.0012, -0.0034, 0.0056, ..., -0.0023])
print(embedding.weight.grad[9999]) # 未使用的词:无梯度 ✗
# tensor([0., 0., 0., ..., 0.])
# 4. 梯度下降(更新参数)
optimizer.step()
# 更新规则
embedding.weight[1234] = embedding.weight[1234] - lr × grad[1234]为什么只有被使用的token有梯度?
Loss → Output → Transformer → Embedding → Token IDs
只有被查表的token,才会有梯度传回来!
为什么Embedding会学到语义?
原因1:共现关系(Co-occurrence)
# 训练数据中经常出现:
"今天天气很好"
"今天天气不错"
"今天天气真棒"
# 模型学到:
# - "今天"和"天气"经常一起出现
# - 它们的embedding应该相似
# - 这样预测下一个词时更准确数学上:
如果"今天"和"天气"经常一起出现
→ 它们的embedding相似时,loss更小
→ 梯度下降会让它们的embedding变得相似
原因2:上下文相似(Distributional Hypothesis)
语言学原理
“You shall know a word by the company it keeps” (通过一个词的伙伴,你就能了解这个词)
# 训练数据:
"今天天气很好"
"明天天气很好"
# 模型学到:
# - "今天"和"明天"出现在相似的上下文中
# - 它们的embedding应该相似原因3:任务驱动(Task-driven)
# 语言模型任务:预测下一个词
# 输入:"今天天气很___"
# 目标:预测"好"
# 如果embedding学到了语义:
# - "今天"的embedding包含"时间"信息
# - "天气"的embedding包含"气候"信息
# - 模型更容易预测出"好"
# 如果embedding是随机的:
# - 模型无法理解"今天"和"天气"的关系
# - 预测会很差训练过程的变化
初始状态(Epoch 0):
# Token 1234("今天")的embedding
embedding.weight[1234]
# tensor([0.023, -0.015, 0.031, ..., -0.008]) # 随机
# Token 5678("天气")的embedding
embedding.weight[5678]
# tensor([0.012, 0.045, -0.023, ..., 0.019]) # 随机
# 相似度(余弦相似度)
similarity = cosine_similarity(
embedding.weight[1234],
embedding.weight[5678]
)
print(similarity) # 0.03(几乎不相关)训练后(Epoch 10000):
# Token 1234("今天")的embedding
embedding.weight[1234]
# tensor([0.456, 0.789, -0.234, ..., 0.678]) # 学到了有用的表示
# Token 5678("天气")的embedding
embedding.weight[5678]
# tensor([0.423, 0.812, -0.201, ..., 0.645]) # 学到了有用的表示
# 相似度
similarity = cosine_similarity(
embedding.weight[1234],
embedding.weight[5678]
)
print(similarity) # 0.87(高度相关!)
# 因为"今天"和"天气"经常一起出现,模型学到了它们的关系可视化:Embedding的学习过程
假设embedding只有2维(实际是512维):
初始状态(随机):
y
|
香蕉 •
|
| • 苹果
|
------+------• 橙子---- x
|
|
没有任何规律,完全随机。
训练后(Epoch 10000):
y
|
| •• 苹果橙子(重叠)
|
------+-------------- x
|
|
香蕉 •
|
明显的聚类:相似的词embedding相似。
和Word2Vec的对比
| 维度 | Word2Vec | Transformer Embedding |
|---|---|---|
| 训练方式 | 独立预训练 | 端到端训练 |
| 训练任务 | 预测上下文词 | 预测下一个token |
| 更新时机 | 只在预训练时 | 整个训练过程中 |
| 训练后 | 固定,不再更新 | 固定,不再更新 |
| 推理时 | 固定 | 固定 |
| 上下文感知 | 静态(无上下文) | 动态(通过Attention) |
关键区别:
# Word2Vec(预训练,固定)
word2vec = load_pretrained("word2vec.bin")
embedding = word2vec["今天"] # 固定的向量
# Transformer Embedding(端到端训练,可更新)
embedding_matrix = nn.Embedding(vocab_size, d_model) # 可学习参数
embedding = embedding_matrix[token_id] # 训练过程中会更新7.4 Transformer的局限性
局限1:二次复杂度
问题:
- 时间复杂度:
- 空间复杂度:
- 长序列时计算量和内存消耗爆炸
具体例子:
序列长度n=1024:
- 注意力矩阵:1M参数
- 可以处理
序列长度n=10240:
- 注意力矩阵:105M参数
- 内存不够!
改进方法:
- Sparse Attention(稀疏注意力)
- Linear Attention(线性注意力)
- Flash Attention(优化内存访问)
局限2:无位置信息
问题:
- Attention本身不知道词的顺序
- 必须手动添加位置编码
例子:
# 对于Attention来说,这两个句子是一样的:
"今天天气很好"
"很好天气今天"改进方法:
- 相对位置编码(Relative Positional Encoding)
- 旋转位置编码(RoPE)
- ALiBi(Attention with Linear Biases)
局限3:长距离依赖衰减
问题:
- 虽然理论上可以关注任意距离
- 但实际上远距离的注意力权重会很小
原因:
# Softmax的特性
scores = [10, 5, 1, 0.1, 0.01] # 距离越远,分数越小
attention = Softmax(scores)
# [0.88, 0.11, 0.01, 0.00, 0.00] # 远距离权重接近0改进方法:
- 分段注意力(Segmented Attention)
- 记忆机制(Memory Mechanism)
局限4:训练不稳定
问题:
- 深层Transformer训练困难
- 容易梯度消失或梯度爆炸
改进方法:
- Pre-Norm(Layer Norm放在前面)
- 更好的初始化方法
- 梯度裁剪(Gradient Clipping)
局限5:缺乏归纳偏置
问题:
- Transformer没有内置的结构假设
- 需要大量数据才能学到模式
对比:
| 模型 | 归纳偏置 | 数据需求 |
|---|---|---|
| CNN | 局部性、平移不变性 | 中等 |
| RNN | 顺序性、时间依赖 | 中等 |
| Transformer | 无 | 大量 |
改进方法:
- 加入卷积层(Conformer)
- 加入循环结构(Universal Transformer)
🧪 自我检验:25道思考题
基础题(检验概念理解)
-
Attention机制的核心思想是什么?用自己的话解释。
-
Query、Key、Value分别是什么?它们有什么区别?
-
为什么要除以?如果不除会怎样?
-
Softmax的作用是什么?为什么不用简单的归一化?
-
Self-Attention的输出是什么?它和输入有什么关系?
-
为什么需要多头注意力?一个head不够吗?
-
位置编码为什么重要?如果没有位置编码会怎样?
中等题(检验原理理解)
-
Encoder和Decoder有什么区别?为什么Decoder需要Mask?
-
残差连接解决了什么问题?
-
Feed-Forward Network的作用是什么?为什么Attention之后还需要FFN?
-
为什么Transformer可以并行,而RNN不行?
-
原始Attention有哪三大缺陷?分别是什么?
-
什么是低秩瓶颈?为什么会限制模型的表达能力?
-
什么是Attention Sink?为什么第一个token会获得异常高的注意力?
-
门控机制是如何工作的?用自己的话解释。
-
为什么用Sigmoid而不是ReLU作为门控函数?
进阶题(检验应用理解)
-
手算一个简单的Attention例子(2个词,2维向量)。
-
证明:两个线性变换的组合还是线性变换。
-
计算Self-Attention的时间复杂度和空间复杂度。
-
门控机制如何突破低秩瓶颈?非线性起了什么作用?
-
门控机制如何消除Attention Sink?实验结果是什么?
-
Sigmoid门控和Softmax注意力有什么区别?它们作用在哪个层面?
-
为什么以前没人用门控机制?列举至少3个原因。
-
Gated Attention的实验结果如何?在长文本上的表现怎么样?
-
Transformer的主要局限性是什么?如何改进?
📚 参考答案
基础题答案
-
Attention机制的核心思想 Attention让模型能”同时看到”所有位置的信息,并计算每个位置对其他位置的关注程度。就像查字典:用Query(要查的字)和Key(字典索引)比较相似度,然后提取Value(字典解释)。
-
Query、Key、Value的区别
- Query:当前位置想知道”我应该关注哪些位置”
- Key:所有位置用来和Query比较相似度
- Value:所有位置的实际内容,用于提取信息 虽然都来自同样的输入,但通过不同的线性变换得到,数学上是不同的。
-
为什么要除以√d 防止点积结果过大。当维度d很大时,Q·K
-
Softmax的作用 把任意实数转换成概率分布(和为1,都是正数)。简单归一化(除以总和)无法处理负数,也无法突出重要的值。Softmax通过exp函数放大差异,让重要的值更突出。
-
Self-Attention的输出 输出是输入的加权组合。每个位置的输出是所有位置Value的加权和,权重由Query和Key的相似度决定。输出维度和输入相同。
-
为什么需要多头注意力 单个head只能学习一种关系模式。多头可以让不同head学习不同的语言现象:有的学语法关系(主谓、动宾),有的学语义关系(同义、反义),有的学位置关系。就像从多个角度看同一本书。
-
位置编码的重要性 Attention本身不知道词的顺序,“今天天气很好”和”很好天气今天”对它来说是一样的。位置编码告诉模型每个词在句子中的位置,让模型能理解顺序关系。
中等题答案
-
Encoder和Decoder的区别
- Encoder:双向Attention,可以看到所有位置,用于理解输入
- Decoder:单向Attention(带Mask),只能看到当前位置之前的内容,用于生成输出 Decoder需要Mask是因为生成时不能”偷看”未来的词。
-
残差连接的作用 解决深层网络的梯度消失问题。通过”输出=F(x)+x”,即使F(x)学不好,至少还有x的直接通路,梯度可以直接传回去。就像走楼梯和坐电梯的双重选择。
-
Feed-Forward Network的作用 Attention是线性的(矩阵乘法),FFN引入非线性(ReLU激活函数),增强模型的表达能力。FFN对每个位置独立处理,而Attention关注位置之间的关系,两者互补。
-
Transformer可以并行的原因 Transformer的所有位置可以同时计算Q、K、V,然后一次矩阵乘法得到所有位置的输出。RNN必须顺序计算,因为h_i依赖h_{i-1},无法并行。在GPU上,Transformer比RNN快10倍以上。
-
原始Attention的三大缺陷
- 低秩瓶颈:输出投影W_O被”吸收”到V中,表达能力受限于d_k(如64维)
- Attention Sink:第一个token获得异常高的注意力(46.7%),成为”垃圾桶”
- 注意力熵崩塌:长文本生成时,注意力分布变得极度集中,失去多样性
-
低秩瓶颈的含义 秩表示独立方向的数量。原始Attention中,z=A·V和o=z·W_O都是线性操作,组合后还是线性,W_O被”吸收”到V中。导致输出的秩≤min(n, d_k),通常受限于d_k(如64),无法充分利用d_model(如512)的表达能力。
-
Attention Sink的原因 Softmax必须把权重分配给所有token。当某个位置不需要关注任何信息时,模型学会把不需要的注意力都扔给第一个token(
),因为它对所有位置都可见,且没有实际语义,不会影响其他有用信息。 -
门控机制的工作原理 在每个Attention head的输出后,乘以一个Sigmoid门控值σ(g)。当σ(g)接近1时,head正常工作;当σ(g)接近0时,head被抑制。模型自动学习哪些head应该工作,哪些应该关闭。
-
为什么用Sigmoid而不是ReLU
- Sigmoid输出在[0,1]之间,可以解释为”开关程度”
- Sigmoid可以接近0但不会完全为0,实现软稀疏
- ReLU输出在[0,∞),无界,不适合做门控
- ReLU在负数时完全为0,太硬,梯度消失
进阶题答案
-
手算Attention例子 假设2个词,2维向量:
Q = [[1, 0], [0, 1]] K = [[1, 0], [0, 1]] V = [[2, 3], [4, 5]] 步骤1:Q·K^T = [[1, 0], [0, 1]](相似度矩阵) 步骤2:除以√2 = [[0.71, 0], [0, 0.71]] 步骤3:Softmax = [[1, 0], [0, 1]](对角矩阵) 步骤4:乘以V = [[2, 3], [4, 5]](输出) -
证明线性组合还是线性 设f(x)=Ax,g(x)=Bx都是线性变换。 组合:h(x)=g(f(x))=B(Ax)=(BA)x=Cx,其中C=BA h(x)=Cx还是线性变换。所以两个线性变换的组合还是线性。
-
Self-Attention的复杂度
- 时间复杂度:O(n²·d),其中n是序列长度,d是维度
- Q·K
- Softmax:O(n²)
- 乘以V:O(n²·d)
- 空间复杂度:O(n²),存储注意力矩阵
- 时间复杂度:O(n²·d),其中n是序列长度,d是维度
-
门控如何突破低秩瓶颈 Sigmoid是非线性函数,σ(2x)≠2·σ(x)。非线性操作可以增加秩,打破线性瓶颈。实验显示,门控后平均秩从127提升到157(提升23%),突破了d_k=128的限制。
-
门控如何消除Attention Sink 门控值可以接近0,让整个head的输出接近0,模型可以”不关注”任何位置,不需要把注意力扔给
了。实验结果:第一个token的注意力从46.7%降到4.8%,降低了90%。 -
Sigmoid门控和Softmax注意力的区别
- Softmax:作用在token层面,决定”关注哪些token”,输出和为1
- Sigmoid:作用在head层面,决定”哪些head工作”,每个head独立控制 就像乐队:Softmax决定每个乐手如何演奏,Sigmoid决定每个乐手的音量。
-
为什么以前没人用门控
- 问题没被发现:Attention Sink是最近才被重视的(2021-2023年)
- 计算资源限制:以前模型小,head少,问题不明显
- 研究惯性:“Transformer已经很好了,为什么要改?”
- 需要积累理解:需要足够的实验和理论才能找到有效的改进方法
-
Gated Attention的实验结果
- PPL提升4.4%(6.026→5.761)
- 长文本(128k)性能提升11.5%
- 第一个token注意力降低90%(46.7%→4.8%)
- 平均秩提升23%(127→157)
- 参数增加<2M(每个head只增加1个标量)
-
Transformer的主要局限性
- 二次复杂度:O(n²),长序列时计算量爆炸
- 无位置信息:必须手动添加位置编码
- 长距离依赖衰减:远距离的注意力权重很小
- 训练不稳定:深层网络容易梯度消失/爆炸
- 缺乏归纳偏置:需要大量数据才能学到模式 改进方法:Sparse Attention、Flash Attention、RoPE、Pre-Norm等。
📚 核心要点总结
一句话总结
Transformer通过Self-Attention机制,让每个词同时关注所有其他词,找出它们之间的关系。Gated Attention通过引入门控机制,突破了原始Attention的低秩瓶颈、Attention Sink和稀疏性问题。
关键概念
基础Transformer:
- Self-Attention:找相似度,Softmax归一化,加权求和
- Multi-Head:多个视角,学习多种关系
- Encoder-Decoder:理解输入,生成输出
- 位置编码:告诉模型词的顺序
- 残差连接:缓解梯度消失,保留原始信息
Gated Attention改进:
- 突破低秩瓶颈:Sigmoid引入非线性,打破秩限制(127→157,提升23%)
- 消除Attention Sink:门控抑制第一个token的异常注意力(46.7%→4.8%)
- 实现稀疏性:Sigmoid可以接近0,实现软稀疏,提高效率
- 参数极少:每个head只增加1个标量参数,总共<2M参数
- 效果显著:PPL提升4.4%,长文本(128k)性能提升11.5%
核心公式
原始Attention:
Multi-Head Attention:
Gated Attention:
其中:
- :Sigmoid门控函数
- :逐元素相乘(Hadamard积)
- :可学习的门控参数
Transformer的优势
- 并行计算:所有位置同时处理,比RNN快10倍
- 长距离依赖:直接建模任意两个词的关系
- 灵活性:可以处理各种序列任务
- 可扩展性:容易扩展到大规模模型
Transformer的局限
原始Attention的三大缺陷:
- 低秩瓶颈:线性变换限制表达能力,秩≤
- Attention Sink:第一个token获得46.7%的异常注意力
- 缺乏稀疏性:Softmax无法产生真正的0,所有head都在工作
其他局限:
- 二次复杂度:,长序列时计算量大
- 内存消耗大:注意力矩阵,长序列时内存不够
- 需要位置编码:本身没有位置信息
- 训练不稳定:深层网络容易梯度消失
Gated Attention如何解决
| 缺陷 | 原始Attention | Gated Attention | 改进效果 |
|---|---|---|---|
| 低秩瓶颈 | 秩≤128 | 秩可以>128 | 秩提升23% |
| Attention Sink | 46.7%注意力在第1个token | 4.8%注意力在第1个token | 降低90% |
| 缺乏稀疏性 | 所有权重>0 | 门控值可接近0 | 实现软稀疏 |
| 参数开销 | - | 每个head +1参数 | <2M参数 |
| 性能提升 | PPL 6.026 | PPL 5.761 | 提升4.4% |
| 长文本性能 | PPL 12.31 (128k) | PPL 10.89 (128k) | 提升11.5% |
为什么Gated Attention有效?
核心机制:
原始Attention(线性):
Linear(V) → Linear(W_O) = Linear
→ 秩受限,无法突破
Gated Attention(非线性):
Linear(V) → Sigmoid(g) ⊙ → Linear(W_O) ≠ Linear
→ 非线性打破秩限制
两层机制:
- Token层面(Softmax):决定关注哪些token
- Head层面(Sigmoid):决定哪些head工作
类比:
- Softmax:每个乐手决定如何演奏(注意力分配)
- Sigmoid:指挥控制每个乐手的音量(门控)
🎓 下一步学习
现在你已经理解了Transformer的基础和Gated Attention的改进,可以:
-
阅读原论文:
- “Attention is All You Need” (Vaswani et al., 2017) - Transformer原论文
- “Gated Attention” (Qwen团队, 2024) - 门控注意力改进
-
深入理解:
- 实现一个简单的Transformer
- 实现Gated Attention机制
- 在小数据集上训练和测试
-
探索变体:
- BERT:双向Encoder
- GPT:单向Decoder
- T5:Encoder-Decoder统一框架
- Llama:现代大语言模型架构
-
优化技术:
- Flash Attention:优化内存访问
- Sparse Attention:降低复杂度
- Linear Attention:线性复杂度
- RoPE:旋转位置编码
-
用费曼学习法讲给我听:检验你的理解
- 用自己的话解释Self-Attention
- 解释为什么需要多头注意力
- 解释Gated Attention如何改进原始Attention
准备好了吗?
你现在已经掌握了:
- ✅ Transformer的完整架构
- ✅ Self-Attention的数学原理
- ✅ 从文本到向量的完整流程
- ✅ Embedding的学习机制
- ✅ 原始Attention的三大缺陷
- ✅ Gated Attention的改进方法
- ✅ 实验结果和性能提升
这是理解现代大语言模型的坚实基础!
📎 附录A:Gated Attention深度答疑
本附录回答常见问题
这些问题来自学习过程中的常见疑惑,帮助你更深入理解Gated Attention。
A.1 PPL是什么?
PPL = Perplexity(困惑度)
12岁版解释
想象你在玩猜词游戏:
- 我说”今天天气很___“,你猜”好”
- 如果你猜对了,说明你对语言的理解很好(困惑度低)
- 如果你完全猜不到,说明你很困惑(困惑度高)
PPL就是衡量模型”有多困惑”的指标。
技术定义:
通俗解释:
- :给定前面的词,预测第i个词的概率
- 如果模型总是能准确预测下一个词(概率接近1),PPL就低
- 如果模型经常预测错(概率很小),PPL就高
实际例子:
| 模型 | PPL | 含义 |
|---|---|---|
| GPT-3 | 20 | 平均每次在20个词中选择 |
| 差模型 | 100 | 平均每次在100个词中选择 |
| 完美模型 | 1 | 每次都能准确预测 |
为什么叫"困惑度"
PPL=20意味着模型平均在20个词之间”困惑”,不知道选哪个。 PPL越低,模型越”不困惑”,预测越准确。
重要:PPL越低越好!
A.2 46.7%的数字是如何得出的?
来源:论文的实验统计
实验方法:
- 选择数据集:在大规模语料库上训练模型
- 统计注意力权重:
- 对每个样本,记录第1个token获得的注意力权重
- 对所有样本求平均
- 得出结论:第1个token平均获得46.7%的注意力
如何复现这个数字?
# 伪代码
total_first_token_attention = 0
num_samples = 0
for batch in dataset:
attention_weights = model.get_attention(batch) # shape: [batch, heads, seq_len, seq_len]
# 提取第1个token获得的注意力(所有位置对第1个token的注意力)
first_token_attn = attention_weights[:, :, :, 0].mean() # 对所有head和位置求平均
total_first_token_attention += first_token_attn
num_samples += 1
average = total_first_token_attention / num_samples
print(f"第1个token平均注意力: {average:.1%}") # 输出: 46.7%A.3 为什么第一个token成为”垃圾桶”?
核心原因:Softmax的归一化约束
问题的本质:
Softmax要求所有注意力权重加起来必须等于1:
但有时候,模型不想关注任何位置!
类比
想象你在考试:
- 题目:“下列哪个是水果?A.石头 B.铁块 C.木头 D.塑料”
- 你的想法:“都不是啊!但我必须选一个…”
- 你随便选了A(虽然A完全不对)
模型也是这样:
- 当前token不需要关注任何其他token
- 但Softmax强制要求”必须把注意力分配给某个位置”
- 模型学会了:把不需要的注意力都扔给第1个token
为什么是第1个token,而不是其他位置?
三个原因:
-
第1个token总是可见
- 因果注意力(causal attention)中,每个位置只能看到自己和之前的token
- 第1个token对所有位置都可见
- 其他位置可能看不到(比如第100个token看不到第101个)
-
第1个token通常是特殊token
- 很多模型用
<BOS>(Begin of Sequence)作为第1个token - 这个token本身没有实际语义
- 模型学会了:反正它没用,就把垃圾注意力扔给它
- 很多模型用
-
训练中的自发涌现
- 模型在训练中自己发现了这个”技巧”
- 一旦开始这样做,就会强化这个模式
- 最终形成稳定的”垃圾桶”行为
A.4 稀疏性与过拟合的关系
你的理解是对的
过拟合的常见原因:
- ✅ 训练轮次太多
- ✅ 参数量过大(相对于数据量)
- ✅ 模型过于复杂
稀疏性如何防止过拟合?
关键:减少”有效参数量”
类比:学生做题
场景A:没有稀疏性(过拟合风险高)
- 学生有100个解题技巧
- 每道题都用上所有100个技巧
- 结果:把训练题的细节都记住了(包括噪声)
- 测试时:遇到新题就懵了
场景B:有稀疏性(过拟合风险低)
- 学生有100个解题技巧
- 每道题只用最相关的5个技巧
- 结果:学到了核心规律,忽略了噪声
- 测试时:能灵活应对新题
技术解释:
原始多头注意力:
- 8个head全部工作
- 即使某些head学到的是噪声,也会影响输出
- 模型容易记住训练数据的特殊模式
Gated Attention(稀疏):
- 门控机制可以关闭不重要的head
- 只有真正有用的head参与计算
- 减少了模型”记忆噪声”的能力
数学角度:
假设模型有H个head:
- 无稀疏性:有效参数 = H × (参数/head)
- 有稀疏性:有效参数 = k × (参数/head),其中k < H
有效参数减少 → 模型复杂度降低 → 过拟合风险降低
A.5 增强可解释性是什么意思?
可解释性 = 理解模型为什么做出某个决策
12岁版解释:老师批改作文
不可解释的情况:
- 你问老师:“为什么我的作文得了80分?”
- 老师说:“因为综合考虑了很多因素…”
- 你还是不知道具体哪里好、哪里不好
可解释的情况:
- 老师说:“你的开头很好(+20分),结构清晰(+30分),但结尾太仓促(-10分),有3个错别字(-20分)”
- 你清楚地知道每个部分的贡献
在Gated Attention中的体现:
原始多头注意力(不可解释):
# 8个head都在工作
head_outputs = [h1, h2, h3, h4, h5, h6, h7, h8]
final_output = concat(head_outputs) @ W_O
# 问题:你不知道哪个head真正有用Gated Attention(可解释):
# 门控值
g = [0.9, 0.05, 0.8, 0.02, 0.1, 0.85, 0.03, 0.06]
# ↑ ↓ ↑ ↓ ↓ ↑ ↓ ↓
# 工作 关闭 工作 关闭 关闭 工作 关闭 关闭
# 只有3个head真正工作
active_heads = [h1, h3, h6] # g > 0.5分析容易:
- 清楚知道head 1, 3, 6在工作
- 可以单独分析这3个head在关注什么
- 理解模型为什么做出这个预测
📎 附录B:Gated Attention流程图解
本附录提供完整的流程图
用图示彻底说清楚:原始Attention vs Gated Attention 的完整流程。
B.1 原始Multi-Head Attention完整流程
输入:X (batch_size, seq_len, d_model)
例如:X = "今天天气很好" 的embedding
shape: (1, 4, 512)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
第1步:对每个head计算Q、K、V
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Head 1:
X ──×W_Q1──> Q1 (1, 4, 64)
X ──×W_K1──> K1 (1, 4, 64)
X ──×W_V1──> V1 (1, 4, 64)
Head 2:
X ──×W_Q2──> Q2 (1, 4, 64)
X ──×W_K2──> K2 (1, 4, 64)
X ──×W_V2──> V2 (1, 4, 64)
... (共8个head)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
第2步:对每个head计算注意力
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Head 1:
scores1 = Q1 @ K1^T / sqrt(64)
attn_weights1 = softmax(scores1) # (1, 4, 4)
head_output1 = attn_weights1 @ V1 # (1, 4, 64)
Head 2:
scores2 = Q2 @ K2^T / sqrt(64)
attn_weights2 = softmax(scores2)
head_output2 = attn_weights2 @ V2 # (1, 4, 64)
... (共8个head)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
第3步:拼接所有head
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
concat_output = concat([head_output1, head_output2, ..., head_output8])
# shape: (1, 4, 512) ← 8个head × 64维 = 512维
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
第4步:输出投影
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
output = concat_output @ W_O # (1, 4, 512)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
结束:输出
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
关键点:
- ✅ 所有8个head都参与计算
- ✅ 每个head的输出直接拼接
- ❌ 没有门控机制
B.2 Gated Attention完整流程
输入:X (batch_size, seq_len, d_model)
例如:X = "今天天气很好" 的embedding
shape: (1, 4, 512)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
第1步:对每个head计算Q、K、V(和原来一样)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Head 1:
X ──×W_Q1──> Q1 (1, 4, 64)
X ──×W_K1──> K1 (1, 4, 64)
X ──×W_V1──> V1 (1, 4, 64)
Head 2:
X ──×W_Q2──> Q2 (1, 4, 64)
X ──×W_K2──> K2 (1, 4, 64)
X ──×W_V2──> V2 (1, 4, 64)
... (共8个head)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
第2步:对每个head计算注意力(和原来一样)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Head 1:
scores1 = Q1 @ K1^T / sqrt(64)
attn_weights1 = softmax(scores1)
head_output1 = attn_weights1 @ V1 # (1, 4, 64)
Head 2:
scores2 = Q2 @ K2^T / sqrt(64)
attn_weights2 = softmax(scores2)
head_output2 = attn_weights2 @ V2 # (1, 4, 64)
... (共8个head)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
第3步:【新增】应用门控机制
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
门控参数(可学习的标量):
g = [g1, g2, g3, g4, g5, g6, g7, g8]
例如训练后的值:
g = [4.5, -3.2, 5.1, -2.8, -3.5, 4.8, -2.9, -3.1]
对每个head应用门控:
Head 1:
gate1 = sigmoid(g1) = sigmoid(4.5) = 0.989
gated_output1 = gate1 × head_output1
= 0.989 × head_output1 # (1, 4, 64)
Head 2:
gate2 = sigmoid(g2) = sigmoid(-3.2) = 0.039
gated_output2 = gate2 × head_output2
= 0.039 × head_output2 # (1, 4, 64)
Head 3:
gate3 = sigmoid(g3) = sigmoid(5.1) = 0.994
gated_output3 = gate3 × head_output3
= 0.994 × head_output3 # (1, 4, 64)
... (共8个head)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
第4步:拼接所有gated head
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
concat_output = concat([gated_output1, gated_output2, ..., gated_output8])
# shape: (1, 4, 512)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
第5步:输出投影
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
output = concat_output @ W_O # (1, 4, 512)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
结束:输出
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
关键点:
- ✅ 前两步和原始Attention完全一样
- ✅ 第3步新增门控:每个head乘以sigmoid(g_i)
- ✅ gate值接近1的head贡献大,接近0的head贡献小
B.3 对比图:关键区别在哪里?
原始Attention:
┌─────────────┐
│ Head 1 │──> head_output1 (1, 4, 64) ──┐
└─────────────┘ │
┌─────────────┐ │
│ Head 2 │──> head_output2 (1, 4, 64) ──┤
└─────────────┘ │
┌─────────────┐ ├──> Concat ──> W_O ──> Output
│ Head 3 │──> head_output3 (1, 4, 64) ──┤
└─────────────┘ │
... │
┌─────────────┐ │
│ Head 8 │──> head_output8 (1, 4, 64) ──┘
└─────────────┘
所有head等权重拼接
Gated Attention:
┌─────────────┐ g1=4.5
│ Head 1 │──> head_output1 ──×σ(g1)=0.989──┐
└─────────────┘ │
┌─────────────┐ g2=-3.2 │
│ Head 2 │──> head_output2 ──×σ(g2)=0.039──┤
└─────────────┘ │
┌─────────────┐ g3=5.1 ├──> Concat ──> W_O ──> Output
│ Head 3 │──> head_output3 ──×σ(g3)=0.994──┤
└─────────────┘ │
... │
┌─────────────┐ g8=-3.1 │
│ Head 8 │──> head_output8 ──×σ(g8)=0.043──┘
└─────────────┘
每个head有独立的门控权重
B.4 门控参数如何学习?
初始化(训练开始):
# 所有门控参数初始化为0
g = [0, 0, 0, 0, 0, 0, 0, 0]
# sigmoid(0) = 0.5
gate_values = [0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5]
# 所有head初始贡献50%训练过程(梯度下降):
第1轮训练:
前向传播:
- 计算所有head的输出
- 应用门控:gated_output_i = sigmoid(g_i) × head_output_i
- 拼接、投影、得到最终输出
- 计算损失:loss = CrossEntropy(output, target)
反向传播:
- 计算损失对g的梯度:∂loss/∂g_i
- 如果head_i有用:梯度为正,g_i增加
- 如果head_i无用:梯度为负,g_i减少
更新参数:
g_i = g_i - learning_rate × ∂loss/∂g_i
第2轮训练:
g的值开始分化:
g = [0.2, -0.1, 0.3, -0.15, -0.2, 0.25, -0.18, -0.12]
gate_values = [0.55, 0.48, 0.57, 0.46, 0.45, 0.56, 0.45, 0.47]
... 继续训练 ...
第1000轮训练:
g的值明显分化:
g = [4.5, -3.2, 5.1, -2.8, -3.5, 4.8, -2.9, -3.1]
gate_values = [0.989, 0.039, 0.994, 0.057, 0.029, 0.992, 0.052, 0.043]
结果:Head 1, 3, 6有用(gate≈1),其他head无用(gate≈0)
相关笔记
📖 参考文献
- Vaswani, A., et al. (2017). “Attention is All You Need”. NeurIPS.
- Qwen Team (2024). “Gated Attention: Improving Transformer Efficiency”.
- Devlin, J., et al. (2018). “BERT: Pre-training of Deep Bidirectional Transformers”. NAACL.
- Radford, A., et al. (2018). “Improving Language Understanding by Generative Pre-Training”. OpenAI.