引言:Self-Attention要解决什么问题?
在深入Self-Attention机制之前,我们需要先明确:它是用来处理什么样的任务的?
序列处理的三种典型场景
在自然语言处理和序列建模中,我们经常遇到以下三种输入输出模式:
1. Sequence to Sequence(序列到序列)
- 输入:一排向量(如一个句子的词向量序列)
- 输出:另一排向量(如翻译后的句子)
- 典型任务:机器翻译、文本摘要
- 例子:“我爱机器学习” → “I love machine learning”
2. Sequence Labeling(序列标注)
-
输入:一排向量
-
输出:同样长度的一排标签
-
典型任务:词性标注、命名实体识别、语音识别
-
例子:
输入: ["我", "爱", "机器", "学习"]输出: [代词, 动词, 名词, 名词]
3. Sequence to One(序列到单值)
- 输入:一排向量
- 输出:一个值或类别
- 典型任务:情感分类、文本分类
- 例子:“这部电影太棒了!” → 正面情感
Self-Attention的核心优势
对于Sequence Labeling这类任务,Self-Attention相比RNN有一个关键优势:
- RNN:处理第个词时,只能看到前面到个词的信息(单向RNN)
- Self-Attention:处理第个词时,可以同时看到整个序列的所有词,包括前面的、后面的、甚至自己
举例:在词性标注任务中
- 句子:“我在银行工作”
- 判断”银行”的词性时,Self-Attention可以同时看到”工作”这个词,从而判断这里的”银行”是金融机构而不是河岸
这种全局视野使得Self-Attention能够更好地利用上下文信息,给每个Token打上更准确的标签。
一、从RNN到Self-Attention的动机
RNN的局限性:为什么需要Self-Attention?
想象你在读一篇文章,理解当前这个词的意思时,你需要回忆前面所有相关的词。RNN就像一个人从头到尾依次阅读,每读一个词都要记住之前的内容。这种方式有几个问题:
1. 顺序处理的困境
- 必须按顺序读:RNN像排队一样,必须处理完第1个词,才能处理第2个词,再处理第3个词…
- 无法并行:即使有多个GPU核心,也只能干等着,因为后面的词依赖前面的结果
- 训练慢:处理长文本时,训练时间会非常长
2. 长距离依赖的困难
考虑这个句子:
“那只在公园里追逐蝴蝶的、毛色金黄的、看起来很开心的狗正在奔跑。”
- RNN处理到”奔跑”时,需要记住很久之前的”狗”
- 尽管LSTM/GRU有记忆机制,但信息经过多次传递后仍会衰减
- 就像传话游戏,传的人越多,信息失真越严重
3. 计算效率低
- 现代GPU擅长并行计算,但RNN的顺序特性无法充分利用
- 就像有100个工人,却只能排队干活,而不能同时开工
Self-Attention的革命性突破
Self-Attention提出了一个全新的思路:让序列中的每个位置直接”看到”所有其他位置。
核心思想:“天涯若比邻”
- 并行处理:所有位置可以同时计算,不需要等待
- 直接连接:任意两个位置之间的距离都是O(1),无论它们相隔多远
- 灵活关注:每个位置可以自主决定关注哪些位置,权重由相关性决定
形象比喻
- RNN:像接力赛,信息一棒一棒传递,后面的人要等前面的人跑完
- Self-Attention:像群聊,所有人同时看到所有消息,可以直接回复任何人
加权求和的本质
每个位置的输出 = 对整个序列的加权求和
- 权重由该位置与其他位置的相关性决定
- 相关性高的位置权重大,贡献更多信息
- 相关性低的位置权重小,几乎不贡献信息
二、Self-Attention的基本原理
Query、Key、Value机制
Self-Attention的核心是Q-K-V三元组机制。这三个概念可能一开始听起来很抽象,但其实它们来自我们日常生活中的检索行为。
直观理解:图书馆找书的过程
想象你在图书馆找书:
- Query(查询):你脑海中想要查找的主题
- 比如你想了解”深度学习”
- 这是你的需求,你想要什么信息
- Key(键):每本书的标签/索引/目录
- 每本书的封面、标题、关键词
- 这是书籍的索引信息,用来判断是否匹配你的需求
- Value(值):书的实际内容
- 书里面真正的知识和信息
- 这是你最终要获取的实际内容
- 注意力权重:Query与Key的匹配度
- 如果一本书的标签与你的查询高度匹配,你会花更多时间读它(权重大)
- 如果不匹配,你可能只是扫一眼就跳过(权重小)
在Self-Attention中的对应
在处理序列”我 爱 机器 学习”时:
- 每个词都有三个角色:
- 作为Query:当处理这个词时,它想要从其他词那里获取什么信息?
- 作为Key:当其他词查询时,这个词提供什么索引信息?
- 作为Value:这个词实际包含什么内容?
- 举例:处理”学习”这个词
- Query(“学习”):我想知道学习的是什么?
- 与Key(“机器”)匹配度高 → 权重大
- 与Key(“我”)、Key(“爱”)匹配度低 → 权重小
- 最终输出 = 主要来自Value(“机器”)的信息 + 少量其他词的信息
关键洞察
- Q、K、V都来自同一个输入,只是通过不同的权重矩阵投影得到
- Query决定”我要什么”,Key决定”我有什么”,Value决定”我给什么”
- 注意力权重 = Query和Key的相似度,决定了Value的贡献比例
Self-Attention的运作流程
示例:处理句子”机器 学习”

假设我们要计算”学习”这个词的Self-Attention输出。
第一步:生成Q、K、V向量
对于输入序列中的每个词,通过三个不同的权重矩阵生成Q、K、V:
输入: ["机器", "学习"]
通过权重矩阵投影:- "机器" → Q₁, K₁, V₁- "学习" → Q₂, K₂, V₂关键点:每个词都有自己的Q、K、V三个向量,它们是通过学习得到的权重矩阵从原始词向量转换而来。
第二步:计算注意力分数
现在我们要计算”学习”对所有词(包括自己)的注意力分数:
Query("学习") 与所有Key的点积:- score("学习", "机器") = Q₂ · K₁ = 0.9 (高相关性)- score("学习", "学习") = Q₂ · K₂ = 0.3 (自己对自己)直观理解:
- 点积越大,说明Query和Key越相似,相关性越高
- “学习”的Query与”机器”的Key相关性很高(0.9),说明”学习”这个词需要关注”机器”
- 除以√d_k是为了防止点积值过大,导致Softmax饱和
第三步:归一化得到注意力权重
使用Softmax将分数转换为概率分布(权重和为1):
Softmax归一化:- α("学习", "机器") = exp(0.9) / (exp(0.9) + exp(0.3)) = 0.73- α("学习", "学习") = exp(0.3) / (exp(0.9) + exp(0.3)) = 0.27含义:
- “学习”这个词应该73%关注”机器”,27%关注自己
- 权重和为1,确保输出的尺度稳定
第四步:加权求和得到输出
用注意力权重对所有Value进行加权求和:
输出("学习") = 0.73 × V₁ + 0.27 × V₂ = 0.73 × Value("机器") + 0.27 × Value("学习")最终结果:
- “学习”的输出向量主要包含”机器”的信息(73%)
- 也保留了一些自己的信息(27%)
- 这个输出向量融合了上下文信息,比原始的”学习”向量更丰富
核心洞察
- 每个词都会执行这个过程:不仅”学习”,“机器”也会计算自己的注意力输出
- 并行计算:所有词的注意力可以同时计算,不需要等待
- 动态权重:注意力权重是根据内容动态计算的,不是固定的
- 信息融合:输出向量融合了整个序列的相关信息
数学公式
1. 计算注意力分数
对于位置的Query和位置的Key:
- :位置的Query向量
- :位置的Key向量
- :Key向量的维度
- :缩放因子,防止点积过大
2. 计算注意力权重
使用Softmax归一化,确保权重和为1
3. 计算输出
输出是所有Value的加权和
矩阵形式
为什么需要缩放?
- 当很大时,点积的值可能很大
- 导致Softmax函数进入饱和区,梯度很小
- 除以可以将点积的方差稳定在1左右
矩阵运算视角:并行计算的关键
前面我们通过单个词的视角(For loop逻辑)解释了Self-Attention的计算过程。但在实际实现中,我们不需要对每个词写循环,而是一次性通过矩阵乘法算出整个句子的注意力分布。这就是GPU加速的物理基础。
从向量到矩阵
假设输入序列有个词,每个词的嵌入维度是:
输入矩阵:
- 每一行是一个词的向量
- 例如:(4个词),(每个词512维)
第一步:生成Q、K、V矩阵
通过三个权重矩阵,一次性将所有词转换为Q、K、V:

- :Query和Key的投影矩阵
- :Value的投影矩阵
- 通常
关键点:一次矩阵乘法,所有词的Q、K、V同时计算完成!
第二步:计算注意力分数矩阵


这一步的物理意义:
- :所有Query和所有Key的点积,一次性算出!
- 矩阵的第元素 = 第个词的Query与第个词的Key的相似度
- 例如:表示第2个词对第3个词的注意力分数
可视化:
K₁ K₂ K₃ K₄Q₁ [ a₁₁ a₁₂ a₁₃ a₁₄ ]Q₂ [ a₂₁ a₂₂ a₂₃ a₂₄ ]Q₃ [ a₃₁ a₃₂ a₃₃ a₃₄ ]Q₄ [ a₄₁ a₄₂ a₄₃ a₄₄ ]- 第2行表示:词2对所有词的注意力分数
- 第3列表示:所有词对词3的注意力分数
第三步:Softmax归一化

对矩阵的每一行进行Softmax:
- 每一行的权重和为1
- 第行表示:词对所有词的注意力权重分布
第四步:加权求和得到输出

物理意义:
- 的第行 = 第个词的输出向量
- 它是所有Value向量的加权和,权重由的第行决定
完整的矩阵形式
将上述步骤合并:

GPU加速的本质
为什么矩阵运算这么重要?
- 并行计算:
- 矩阵乘法可以在GPU上高度并行
- 所有词的注意力同时计算,不需要循环
- 硬件优化:
- 现代GPU针对矩阵乘法做了专门优化(如NVIDIA的Tensor Core)
- 一次矩阵乘法比次向量运算快得多
- 计算复杂度:
- 时间复杂度:
- 空间复杂度:(存储注意力矩阵)
- 虽然是,但可以充分利用GPU并行能力
我们不需要对每个词写循环,而是一次性通过矩阵乘法算出整个句子的注意力分布,这就是GPU加速的物理基础。
三、多头注意力(Multi-Head Attention)
动机:为什么需要多个”头”?
单个Self-Attention就像一个人从一个角度看问题,可能会遗漏重要信息。多头注意力就像多个专家从不同角度同时分析。
单头的局限性
假设处理句子”那只在公园里奔跑的狗很可爱”:
- 单个注意力头可能只关注语法关系:“狗”和”奔跑”
- 但可能忽略修饰关系:“狗”和”可爱”
- 也可能忽略位置信息:“狗”和”公园”
多头的优势
多头注意力让模型同时从多个角度理解:
- 头1:关注语法关系(主谓宾)
- 头2:关注修饰关系(形容词-名词)
- 头3:关注位置关系(地点-动作)
- 头4:关注语义相似性
形象比喻:就像一个团队讨论问题,每个人负责不同的视角,最后综合所有人的意见做出决策。
深层原因:相关性的定义不止一种
核心洞察:什么叫”相关”?这个问题没有唯一答案!
例子1:天涯若比邻
考虑句子:“虽然我们相隔万里,但心灵相通”
- 位置上的相关性:“我们”和”万里”(物理距离)
- 如果只有一个头,可能会认为”我们”和”万里”不相关(距离太远)
- 语义上的相关性:“我们”和”心灵相通”(情感联系)
- 另一个头可能会发现”我们”和”心灵相通”高度相关(语义紧密)
矛盾:同样是”我们”这个词,在不同的相关性定义下,应该关注的对象完全不同!
例子2:一词多义
考虑句子:“我在银行工作,银行就在河岸边”
- 第一个”银行”:
- 头1(职业相关):关注”工作” → 金融机构
- 头2(位置相关):关注”河岸” → 可能产生混淆
- 第二个”银行”:
- 头1(职业相关):关注”工作” → 可能产生混淆
- 头2(位置相关):关注”河岸” → 地理位置
解决方案:多个头可以同时捕捉不同类型的相关性,避免单一视角的局限。
子空间投影的数学本质
为什么多头能够学习不同的相关性?关键在于子空间投影。
单头的问题
假设模型维度是512维:
- 所有词都在同一个512维空间中
- 计算相似度时,所有维度混在一起
- 无法区分”语法相关”和”语义相关”
多头的解决方案
使用8个头,每个头64维:
- 头1:将512维投影到第1个64维子空间
- 这个子空间专门学习”语法关系”
- 将输入映射到”语法子空间”
- 头2:将512维投影到第2个64维子空间
- 这个子空间专门学习”语义关系”
- 将输入映射到”语义子空间”
- 头3-8:其他子空间,学习其他类型的相关性
关键点:
- 不同的投影矩阵将输入映射到不同的子空间
- 在不同子空间中,“相关性”的定义不同
- 这样可以避免不同类型的相关性互相干扰
形象比喻:就像把一个复杂的问题分解成多个简单的子问题,每个专家负责一个子问题,最后综合所有专家的答案。
数学定义
1. 多个注意力头
每个头使用不同的权重矩阵,学习不同的表示:
- :第个头的投影矩阵(每个头都不同)
- 每个头独立计算注意力,关注不同的模式
2. 拼接与投影
将所有头的输出拼接起来,再通过一个线性变换:
- :注意力头的数量(通常为8或16)
- :输出投影矩阵,将拼接后的向量映射回原始维度
3. 维度分配
假设模型维度为512,使用8个头:
- 每个头的维度 = 512 / 8 = 64
- 8个头并行计算,每个头处理64维的子空间
- 拼接后恢复到512维
4. 分割与拼接的深层原理
总维度守恒定律:不是增加,而是分散
常见误解:多头注意力是不是增加了8倍的计算量?
实际情况:不是!多头机制的设计遵循**“总维度守恒”**原则。
单头 vs 多头的参数量对比:
假设模型维度 :
单头方案:
- :每个都是
- 总参数量:
8头方案:
- 每个头的维度:
- 每个头的 :
- 8个头的总参数量:
结论:参数量完全相同!
核心思想:
- 多头机制不是为了增加计算量(那样会变慢)
- 而是为了强制模型把计算能力”分散”到不同的子空间
- 就像”不要把鸡蛋放在同一个篮子里”
形象比喻:
- 单头:一个全能专家,什么都看,但可能什么都看不深
- 多头:8个专业专家,每人负责一个领域,分工明确
合并后的”信息搅拌”:W^O的关键作用
拼接不够,还需要混合!
在多头注意力的公式中,有一个容易被忽略但极其重要的矩阵:(输出投影矩阵)。
为什么拼接后还需要W^O?
假设我们只做拼接(Concat),不做W^O:
Head 1 输出: [第1-64维] → 只包含"语法关系"信息Head 2 输出: [第65-128维] → 只包含"位置关系"信息Head 3 输出: [第129-192维] → 只包含"语义关系"信息...拼接结果: [第1-512维]问题:
- 第1-64维永远只包含Head 1的信息
- 第65-128维永远只包含Head 2的信息
- 不同头的发现无法交互!
W^O的作用:信息搅拌
是一个巨大的线性变换矩阵:
效果:
- 输出的每一维都是所有头的加权组合
- Head 1的”语法发现”和Head 2的”位置发现”可以交互
- 就像把8个专家的报告融合成一份综合报告
形象比喻:
- 只拼接:8个专家各写各的报告,装订成一本书,但内容互不相关
- 拼接+W^O:8个专家开会讨论,综合所有观点,写出一份融合报告
数学直觉:
- 拼接: → 简单的”并排放置”
- W^O:矩阵乘法 → 每个输出维度都是所有输入维度的线性组合
- 结果:信息充分混合,不同视角的发现可以相互增强
为什么”浓缩”有效?降维打击的智慧
核心问题:为什么把512维压缩到64维反而更有效?
直观理解:高维的诅咒
高维空间太空旷:
- 在512维空间中,数据点之间的距离都很大
- 就像在一个巨大的仓库里找东西,到处都是空地
- 很难发现数据的规律和模式
低维空间抓特征:
- 把向量投影到64维的”语法子空间”
- 只保留与语法相关的信息
- 主谓宾关系变得清晰可见
- 把向量投影到64维的”位置子空间”
- 只保留与位置相关的信息
- 相邻关系一目了然
形象比喻:
高维空间(512维):
- 就像一个巨大的图书馆,书籍按512种标准分类
- 你想找”语法相关”的书,但书架上混杂着各种主题
- 信号被噪声淹没
低维子空间(64维):
- 就像一个专题书架,只放”语法”相关的书
- 去掉了无关的维度(颜色、大小、价格…)
- 只保留了关键维度(主语、谓语、宾语…)
- 规律清晰可见
数学直觉:
投影到子空间的过程:
- :投影矩阵
- 作用:从512维的”全空间”投影到64维的”语法子空间”
- 效果:过滤掉无关信息,突出关键特征
关键洞察:
- 不是维度越高越好(高维会导致数据稀疏)
- 不是维度越低越好(低维会丢失信息)
- 最优策略:把高维空间分解成多个低维子空间,每个子空间专注一个方面
类比机器学习中的降维:
- PCA:找到数据的主成分(低维表示)
- 多头注意力:找到多个主成分(多个低维子空间)
- 区别:PCA是无监督的,多头注意力是通过训练学习的
完整的计算流程总结
结合以上理解,多头注意力的完整流程是:
- 分割(投影到子空间):
- 每个头独立工作在自己的64维子空间
- 总参数量保持不变(守恒定律)
- 并行计算:
- 8个头同时计算注意力
- 每个头关注不同的模式
- 拼接:
- 简单地把8个64维向量排成一个512维向量
- 信息搅拌(W^O):
- 让不同头的发现交互融合
- 生成最终的512维输出
金句:多头注意力不是”做更多”,而是”做得更专注”——通过分工合作,让每个头在自己的子空间里成为专家。
不同头学到的模式
研究发现,不同的头会自动学习关注不同的语言现象:
头1:局部依赖
- 关注相邻词之间的关系
- 例如:“机器 学习” → “机器”主要关注”学习”
头2:长距离依赖
- 关注句子开头和结尾的呼应
- 例如:“如果…那么…” 结构
头3:语法关系
- 关注主谓宾等语法结构
- 例如:“狗”关注”奔跑”(主语-谓语)
头4:语义相似性
- 关注语义相关的词
- 例如:“深度”关注”学习”(构成专业术语)
关键洞察:这些模式不是人为设计的,而是模型在训练过程中自动学习出来的!
多头注意力 vs 多层注意力
在Transformer架构中,有两个容易混淆的概念:多头注意力(Multi-Head Attention)和多层注意力(Multi-Layer Attention)。它们分别从横向和纵向两个维度扩展模型的能力。
核心区别
| 对比维度 | 多头注意力 (Multi-Head) | 多层注意力 (Multi-Layer) |
|---|---|---|
| 维度 | 横向 (Width) | 纵向 (Depth) |
| 核心逻辑 | 并行 (Parallel):同时看不同的方面 | 串行 (Sequential):一步步深入处理 |
| 数学操作 | 向量切分 → 独立计算 → 拼接 | 向量输入 → 计算 → 输出 → 非线性变换(FFN) → 下一层 |
| 解决痛点 | 解决信息单一性:避免只关注一种关系 | 解决表达能力不足:让模型学会复杂推理 |
| 生物学类比 | 视觉皮层同时处理颜色、形状、运动 | 视网膜 → V1区 → V2区 → … → 大脑皮层决策 |
详细说明
多头注意力(横向扩展)
工作方式:
- 在同一层内,将注意力机制复制多份
- 每个头独立计算,关注不同的模式
- 最后将所有头的输出拼接起来
形象比喻:
- 就像一个团队同时从不同角度分析同一个问题
- 所有人在同一时间工作,最后汇总结果
数学表示:
多层注意力(纵向扩展)
工作方式:
- 将多个Transformer Block堆叠起来
- 每一层的输出作为下一层的输入
- 逐层提取更抽象的特征
形象比喻:
- 就像流水线,每个工人负责一道工序
- 前一个工人完成后,后一个工人才能开始
数学表示:
为什么两者都需要?
多头注意力:
- 提供多样性:从不同角度理解输入
- 类似于”横向思维”:同时考虑多个可能性
多层注意力:
- 提供深度:逐层抽象,学习复杂模式
- 类似于”纵向思维”:从具体到抽象,层层递进
结合效果:
- 横向:每一层都有多个头,提供丰富的视角
- 纵向:多层堆叠,逐步提取高级特征
- 就像一个多层多头的金字塔,既宽又深
实际应用
BERT(12层,12头):
- 12个Transformer Block(纵向)
- 每个Block有12个注意力头(横向)
- 总共144个注意力计算单元
GPT-3(96层,96头):
- 96个Transformer Block(纵向)
- 每个Block有96个注意力头(横向)
- 总共9216个注意力计算单元
关键洞察:多头和多层是正交的设计,分别从宽度和深度两个维度增强模型的表达能力。
四、位置编码(Positional Encoding)
为什么需要位置编码?
Self-Attention有一个严重的问题:它不知道词的顺序!
位置无关性的问题
考虑以下两个句子:
- “狗 咬 人”
- “人 咬 狗”
对于Self-Attention来说:
- 两个句子包含相同的词:{狗, 咬, 人}
- 计算注意力时,只看词与词之间的相关性
- 无论顺序如何,计算结果完全相同!
但显然,这两个句子的意思完全不同:
- “狗咬人”:狗是施动者
- “人咬狗”:人是施动者(这是新闻!)
为什么Self-Attention是位置无关的?
回顾Self-Attention的计算过程:
- 计算Q·K的相似度
- 用Softmax归一化
- 对V加权求和
关键问题:这个过程中,没有任何地方使用了位置信息!
- “狗”在第1个位置还是第3个位置,它的Q、K、V向量都一样
- 就像一个袋子里装着词,不管怎么摇晃,结果都不变(Bag of Words)
解决方案:位置编码
既然Self-Attention不知道位置,我们就人为地告诉它!
核心思想:给每个位置添加一个独特的”位置标记”
- 位置1的词 = 原始词向量 + 位置1的编码
- 位置2的词 = 原始词向量 + 位置2的编码
- 位置3的词 = 原始词向量 + 位置3的编码
这样,即使是相同的词,在不同位置也会有不同的表示。
位置编码方案
1. 正弦位置编码(Sinusoidal)
Transformer原论文使用的方案,使用正弦和余弦函数:
参数说明:
- :位置索引(0, 1, 2, …)
- :维度索引(0, 1, 2, …)
- :模型维度(如512)
为什么用正弦/余弦?
- 不同位置产生不同的编码向量
- 可以处理任意长度的序列(不需要预先设定最大长度)
- 相对位置关系可以通过三角函数性质表示
2. 可学习位置编码
另一种方案是将位置编码作为可训练参数:
- 为每个位置学习一个独特的向量
- 优点:更灵活,可以学习到最适合任务的位置表示
- 缺点:需要预先设定最大序列长度
位置编码的使用
将位置编码加到输入嵌入上:
示例:
原始输入: ["狗", "咬", "人"]词嵌入: [v_狗, v_咬, v_人]
加上位置编码:位置0: v_狗 + PE_0位置1: v_咬 + PE_1位置2: v_人 + PE_2现在,即使”狗”和”人”交换位置,它们的表示也会不同,Self-Attention就能区分”狗咬人”和”人咬狗”了!
关键洞察
- 位置编码是必需的:没有它,Self-Attention就是一个”词袋模型”
- 加法而非拼接:位置编码通过加法融入词向量,而不是拼接
- 一次性添加:位置编码只在输入层添加一次,后续层会自动传播这个信息
深度思考:为什么是相加而不是拼接?
这是一个非常常见的疑问:为什么位置编码要加到词向量上,而不是拼接在一起?
直觉上的困惑
拼接方案看起来更自然:
词向量: [x₁, x₂, ..., x_d] (d维)位置编码: [p₁, p₂, ..., p_d] (d维)拼接结果: [x₁, x₂, ..., x_d, p₁, p₂, ..., p_d] (2d维)相加方案看起来很奇怪:
词向量: [x₁, x₂, ..., x_d] (d维)位置编码: [p₁, p₂, ..., p_d] (d维)相加结果: [x₁+p₁, x₂+p₂, ..., x_d+p_d] (d维)疑问:相加会不会让词向量和位置编码混在一起,无法区分?
数学上的等价性
关键洞察:在经过线性变换后,相加和拼接在数学上是等价的!
拼接方案
假设拼接后的向量是 ,经过权重矩阵 :
我们可以把 分成两块:
其中 对应词向量部分, 对应位置编码部分。
那么:
相加方案
假设相加后的向量是 ,经过权重矩阵 :
对比
- 拼接方案:
- 相加方案:
结论:两种方案的形式完全一样!只是拼接方案用了两个独立的权重矩阵 和 ,而相加方案用了同一个权重矩阵 。
相加方案的优势
既然数学上等价,为什么选择相加?
1. 参数效率
- 拼接:输入维度变成 ,所有权重矩阵的参数量翻倍
- 相加:输入维度保持 ,参数量不变
2. 维度一致性
- 拼接:输入维度 ,后续层的输入输出维度不一致,需要额外处理
- 相加:所有层的维度保持一致,架构更简洁
3. 信息融合
- 拼接:词向量和位置编码在物理上分离,需要通过权重矩阵学习如何融合
- 相加:词向量和位置编码在输入时就已经融合,模型可以直接使用
直觉理解
高维空间的魔法:
- 在高维空间(如512维)中,两个向量相加后,原始信息并不会丢失
- 通过不同的投影矩阵(),模型可以提取出词向量和位置编码的不同组合
- 就像把两种颜料混合,虽然看起来是一种颜色,但通过光谱分析仍然可以分离出原始成分
形象比喻:
- 拼接:把两本书并排放在一起,左边是内容,右边是页码
- 相加:把页码直接印在每一页的内容上,内容和页码融为一体
虽然”相加”看起来会混淆信息,但在高维空间中,通过线性变换可以灵活地提取和组合信息,因此不会造成信息损失。
五、Self-Attention与其他模型的对比
vs RNN:从顺序到并行

| 特性 | RNN/LSTM | Self-Attention |
|---|---|---|
| 并行化 | 不可并行(顺序处理) | 完全并行 |
| 长距离依赖 | O(n)步传播 | O(1)直接连接 |
| 计算复杂度 | O(n·d²) | O(n²·d) |
| 内存需求 | O(n·d) | O(n²) |
| 位置信息 | 隐式编码 | 需要显式编码 |
适用场景
- 短序列:Self-Attention更优(并行化优势)
- 长序列:需要考虑O(n²)的复杂度,可能需要稀疏注意力
- 实时处理:RNN可以流式处理,Self-Attention需要完整序列
vs CNN:从局部到全局

CNN是受限的Self-Attention
核心观点:CNN可以看作是一种受限的Self-Attention,它只关注固定的局部窗口。
感受野的差异
CNN的局部感受野:
- 卷积核大小固定(如3×3)
- 每个位置只能看到固定窗口内的邻居
- 需要堆叠多层才能扩大感受野
- 例如:3层3×3卷积才能看到7×7的范围
Self-Attention的全局感受野:
- 每个位置可以看到整个序列
- 第一层就能建立任意距离的连接
- 感受野是动态的,由注意力权重决定
灵活性的差异
CNN的固定模式:
卷积核: [w₁, w₂, w₃]输出 = w₁·x_{i-1} + w₂·x_i + w₃·x_{i+1}- 权重是固定的,对所有位置使用相同的卷积核
- 无法根据内容调整
Self-Attention的动态模式:
注意力权重: [α₁, α₂, ..., αₙ]输出 = α₁·v₁ + α₂·v₂ + ... + αₙ·vₙ- 权重是动态计算的,根据内容自适应调整
- 可以看作”可学习的卷积核”
形象比喻
- CNN:戴着固定焦距的眼镜,只能看清固定范围内的东西
- Self-Attention:拥有可变焦的镜头,可以自由选择关注远处还是近处
归纳偏置 vs 数据需求
CNN的优势(归纳偏置强):
- 内置了局部性和平移不变性的先验知识
- 参数量少,训练效率高
- 数据量少时表现更好
Self-Attention的优势(上限高):
- 没有强制的归纳偏置,更灵活
- 可以学习到任意复杂的模式
- 数据量大时表现更好
实验结论(Vision Transformer)
根据Vision Transformer(ViT)的研究:
- 小数据集(ImageNet):CNN(如ResNet)表现更好
- 大数据集(JFT-300M):Transformer超越CNN
- 关键因素:数据量是否足够让模型学习到有效的归纳偏置
金句:CNN是人类设计的归纳偏置,Self-Attention是数据驱动的归纳偏置。
vs GNN:从稀疏图到全连接图

Self-Attention就是全连接的GNN
核心观点:Self-Attention可以看作是一个全连接图神经网络(Fully Connected GNN)。
图的视角
将序列看作图:
- 每个Token是一个节点
- 注意力权重是边的权重
- Self-Attention的输出是节点特征的加权聚合
数学形式:
- :第层节点的特征
- :节点到节点的边权重(注意力权重)
- 这正是GNN的消息传递机制!
图结构的差异
GNN的稀疏图:
- 只在有边的节点之间传递信息
- 图结构是预定义的(如社交网络、分子结构)
- 边的数量通常是(稀疏图)
Self-Attention的全连接图:
- 所有节点之间都有边(全连接)
- 图结构是动态的,由注意力权重决定
- 边的数量是(稠密图)
适用场景
GNN适合:
- 图结构已知且稀疏(如社交网络、知识图谱)
- 需要利用图的拓扑结构
Self-Attention适合:
- 图结构未知,需要模型自己学习
- 序列数据(可以看作全连接图)
统一视角
三者的统一:
- CNN:局部连接的图(每个节点只连接邻居)
- GNN:稀疏连接的图(根据预定义结构连接)
- Self-Attention:全连接的图(所有节点互相连接)
灵活性递增:CNN < GNN < Self-Attention 计算复杂度递增:CNN < GNN < Self-Attention 数据需求递增:CNN < GNN < Self-Attention
六、PyTorch实现示例
基础Self-Attention实现
import torchimport torch.nn as nnimport torch.nn.functional as F
class SelfAttention(nn.Module): def __init__(self, embed_dim): super(SelfAttention, self).__init__() self.embed_dim = embed_dim
# Query, Key, Value投影矩阵 self.query = nn.Linear(embed_dim, embed_dim) self.key = nn.Linear(embed_dim, embed_dim) self.value = nn.Linear(embed_dim, embed_dim)
self.scale = embed_dim ** 0.5
def forward(self, x): # x shape: (batch_size, seq_len, embed_dim) batch_size, seq_len, embed_dim = x.size()
# 计算Q, K, V Q = self.query(x) # (batch_size, seq_len, embed_dim) K = self.key(x) # (batch_size, seq_len, embed_dim) V = self.value(x) # (batch_size, seq_len, embed_dim)
# 计算注意力分数 scores = torch.matmul(Q, K.transpose(-2, -1)) / self.scale # scores shape: (batch_size, seq_len, seq_len)
# 计算注意力权重 attention_weights = F.softmax(scores, dim=-1)
# 计算输出 output = torch.matmul(attention_weights, V) # output shape: (batch_size, seq_len, embed_dim)
return output, attention_weights
# 使用示例embed_dim = 128seq_len = 10batch_size = 32
model = SelfAttention(embed_dim)x = torch.randn(batch_size, seq_len, embed_dim)output, attention_weights = model(x)
print(f"输出形状: {output.shape}")print(f"注意力权重形状: {attention_weights.shape}")多头注意力实现
class MultiHeadAttention(nn.Module): def __init__(self, embed_dim, num_heads): super(MultiHeadAttention, self).__init__() assert embed_dim % num_heads == 0, "embed_dim必须能被num_heads整除"
self.embed_dim = embed_dim self.num_heads = num_heads self.head_dim = embed_dim // num_heads
# Q, K, V投影 self.qkv = nn.Linear(embed_dim, embed_dim * 3) # 输出投影 self.out_proj = nn.Linear(embed_dim, embed_dim)
self.scale = self.head_dim ** 0.5
def forward(self, x): batch_size, seq_len, embed_dim = x.size()
# 计算Q, K, V并分割成多个头 qkv = self.qkv(x) # (batch_size, seq_len, embed_dim * 3) qkv = qkv.reshape(batch_size, seq_len, 3, self.num_heads, self.head_dim) qkv = qkv.permute(2, 0, 3, 1, 4) # (3, batch_size, num_heads, seq_len, head_dim) Q, K, V = qkv[0], qkv[1], qkv[2]
# 计算注意力分数 scores = torch.matmul(Q, K.transpose(-2, -1)) / self.scale attention_weights = F.softmax(scores, dim=-1)
# 计算输出 output = torch.matmul(attention_weights, V) # output shape: (batch_size, num_heads, seq_len, head_dim)
# 合并多个头 output = output.transpose(1, 2).contiguous() output = output.reshape(batch_size, seq_len, embed_dim)
# 输出投影 output = self.out_proj(output)
return output, attention_weights
# 使用示例embed_dim = 128num_heads = 8seq_len = 10batch_size = 32
model = MultiHeadAttention(embed_dim, num_heads)x = torch.randn(batch_size, seq_len, embed_dim)output, attention_weights = model(x)
print(f"输出形状: {output.shape}")print(f"注意力权重形状: {attention_weights.shape}")实际应用中的增强:残差连接与层归一化
上面的代码实现了Self-Attention的核心机制,但在实际应用(如Transformer)中,Self-Attention输出后通常会接残差连接(Residual Connection)和层归一化(Layer Normalization),以帮助深层网络训练。
为什么需要这些增强?
1. 残差连接(Residual Connection)
问题:深层网络容易出现梯度消失/爆炸,导致训练困难。
解决方案:将输入直接加到输出上
作用:
- 提供梯度的”高速公路”,让梯度可以直接传播
- 即使Self-Attention学不到有用的信息,至少可以保留输入
- 使得堆叠多层Self-Attention成为可能
2. 层归一化(Layer Normalization)
问题:不同层的激活值分布可能差异很大,导致训练不稳定。
解决方案:对每个样本的特征进行归一化
作用:
- 稳定训练过程
- 加速收敛
- 减少对学习率的敏感性
Transformer Block的标准结构
在Transformer中,一个标准的Self-Attention Block包含:
class TransformerBlock(nn.Module): def __init__(self, embed_dim, num_heads, ff_dim, dropout=0.1): super(TransformerBlock, self).__init__()
# Multi-Head Self-Attention self.attention = MultiHeadAttention(embed_dim, num_heads)
# Layer Normalization self.norm1 = nn.LayerNorm(embed_dim) self.norm2 = nn.LayerNorm(embed_dim)
# Feed-Forward Network self.ffn = nn.Sequential( nn.Linear(embed_dim, ff_dim), nn.ReLU(), nn.Linear(ff_dim, embed_dim) )
# Dropout self.dropout = nn.Dropout(dropout)
def forward(self, x): # Self-Attention + 残差连接 + LayerNorm attn_output, _ = self.attention(x) x = self.norm1(x + self.dropout(attn_output))
# Feed-Forward + 残差连接 + LayerNorm ffn_output = self.ffn(x) x = self.norm2(x + self.dropout(ffn_output))
return x关键点:
- Add & Norm:每个子层(Self-Attention、FFN)后都有残差连接和LayerNorm
- 顺序:有两种常见顺序
- Post-LN:
LayerNorm(x + Sublayer(x))(原始Transformer) - Pre-LN:
x + Sublayer(LayerNorm(x))(更稳定,现代实现常用)
- Post-LN:
为什么这些不属于Self-Attention本身?
- Self-Attention:核心的注意力机制,负责信息聚合
- 残差连接和LayerNorm:训练技巧,帮助深层网络优化
- 它们是正交的:Self-Attention可以单独使用,但在深层网络中需要这些技巧
类比:Self-Attention是引擎,残差连接和LayerNorm是润滑油和冷却系统,让引擎在高负荷下稳定运行。
七、注意力可视化与解释
注意力权重的含义
- 注意力权重矩阵是的,其中是序列长度
- 第行表示位置对其他所有位置的注意力分布
- 权重越大,表示该位置对当前位置的影响越大
常见的注意力模式
- 局部注意力:主要关注相邻位置
- 全局注意力:关注整个序列
- 语法注意力:关注有语法关系的词(如主语-谓语)
八、Self-Attention的变体
1. Masked Self-Attention
- 用于解码器,防止看到未来信息
- 通过掩码矩阵将未来位置的注意力分数设为
2. Cross-Attention
- Query来自一个序列,Key和Value来自另一个序列
- 用于Seq2Seq模型的编码器-解码器连接
3. 稀疏注意力
- 减少O(n²)的复杂度
- 只计算部分位置之间的注意力
九、实践建议
超参数选择
- 注意力头数:通常为8或16
- 头维度:embed_dim / num_heads,通常为64
- Dropout:在注意力权重上应用,防止过拟合
训练技巧
- 学习率预热:开始时使用较小的学习率
- 层归一化:在每个子层之后应用
- 残差连接:帮助梯度传播
常见问题
- 内存不足:减少batch size或序列长度
- 训练不稳定:检查学习率和初始化
- 注意力退化:所有位置的权重都接近均匀分布
十、总结
Self-Attention机制是深度学习领域的重要突破,它:
- 解决了RNN的顺序处理限制
- 提供了更直接的长距离依赖建模
- 成为Transformer架构的核心组件
虽然Self-Attention有O(n²)的复杂度限制,但其并行化能力和建模能力使其在NLP、CV等领域取得了巨大成功。
致谢
本笔记在学习李宏毅老师的深度学习课程(LeeDL)时完成。感谢李宏毅老师对Self-Attention机制的精彩讲解,特别是对Query、Key、Value的直观解释和注意力可视化的展示。
相关资源:
部分信息可能已经过时









