注意力机制
注意力机制(Attention Mechanism)并非Transformer提出的,它在Transformer诞生之前就已经是一个非常热门、甚至可以说是救命稻草般的研究方向。注意力机制是连接古典深度学习和现代生成式AI的核心桥梁。
学习路线: Seq2Seq瓶颈 → Bahdanau Attention → Luong Attention → Self-Attention → Scaled Dot-Product → Multi-Head Attention
从固定向量瓶颈说起
在传统NLP笔记中我们已经看到,Seq2Seq架构把整个源句子压缩成一个固定长度的上下文向量 \(c = h_T^{\text{enc}}\)。这个瓶颈带来的问题是:
"Le chat noir est assis sur le tapis rouge dans le salon"(黑猫坐在客厅的红地毯上)
↓
整个句子 → 压缩为一个向量 c
↓
"The black cat is sitting on the red carpet in the living room"
12个词的全部信息被塞进一个几百维的向量里。句子越长,信息损失越严重。
Attention的核心思想:解码器在生成每个目标词时,不再只看一个压缩向量,而是可以动态地关注源句子中不同位置的信息,并为每个位置分配不同的权重。
Bahdanau Attention(加性注意力,2014)
"Neural Machine Translation by Jointly Learning to Align and Translate" — Bahdanau et al., 2014
这是注意力机制的开山之作。
架构
在传统Seq2Seq中,解码器只接收最终隐藏状态。Bahdanau Attention的改进是:让解码器在每一步都能访问编码器的所有隐藏状态。
Encoder (BiRNN) Decoder
┌────────────┐ ┌──────────┐
│ h₁ h₂ h₃ h₄│ α₁ α₂ α₃ α₄ │ │
│ ↓ ↓ ↓ ↓ │──→ 加权求和 ──→ │ cₜ + sₜ │ → yₜ
└────────────┘ = cₜ └──────────┘
↑
每一步t都重新计算注意力权重α
数学推导
Step 1:计算对齐分数(Alignment Score)
解码器第 \(t\) 步的隐藏状态 \(s_{t-1}\) 与编码器第 \(j\) 个隐藏状态 \(h_j\) 之间的"相关性":
其中 \(W_a, U_a, v_a\) 都是可学习参数。这个评分函数被称为加性注意力(Additive Attention),因为它把两个向量先分别线性变换再相加。
Step 2:计算注意力权重(Attention Weights)
对所有源位置的分数做Softmax归一化,得到概率分布:
\(\alpha_{tj}\) 表示"解码器在生成第 \(t\) 个目标词时,对源句子第 \(j\) 个词的关注程度"。所有 \(\alpha_{tj}\) 之和为1。
Step 3:计算上下文向量(Context Vector)
对编码器隐藏状态做加权求和:
注意:这里每一步 \(t\) 都会生成一个不同的 \(c_t\)!不再是一个固定向量。翻译"cat"时,\(c_t\) 可能主要关注"chat";翻译"red"时,\(c_t\) 可能主要关注"rouge"。
Step 4:解码
将上下文向量 \(c_t\) 与解码器当前状态结合,预测下一个词:
直觉理解
Bahdanau Attention就像一个翻译官在翻译时,不是试图记住整篇原文,而是翻译每个词时都回头翻阅原文,找到最相关的部分重点参考。
Luong Attention(乘性注意力,2015)
"Effective Approaches to Attention-based Neural Machine Translation" — Luong et al., 2015
Luong Attention简化了Bahdanau Attention,并提出了三种评分函数:
| 方式 | 公式 | 说明 |
|---|---|---|
| dot(点积) | \(e_{tj} = s_t^T h_j\) | 最简单,无额外参数 |
| general(一般) | \(e_{tj} = s_t^T W_a h_j\) | 加一个可学习矩阵 |
| concat(拼接) | \(e_{tj} = v_a^T \tanh(W_a [s_t; h_j])\) | 类似Bahdanau |
点积注意力在后来的Transformer中被采用,因为它计算效率最高(矩阵乘法可以高度并行化)。
Bahdanau vs Luong 的关键区别:
- Bahdanau使用 \(s_{t-1}\)(前一步隐藏状态)计算注意力,Luong使用 \(s_t\)(当前步)
- Bahdanau使用双向RNN编码器,Luong使用单向
- Luong的点积评分函数更简洁高效
Self-Attention(自注意力)
前面的Attention都是跨序列注意力(Cross-Attention):解码器关注编码器。Self-Attention则是一个序列内部自己关注自己。
从Cross-Attention到Self-Attention
| Cross-Attention | Self-Attention | |
|---|---|---|
| Query来源 | 解码器 | 自身序列 |
| Key/Value来源 | 编码器 | 自身序列 |
| 目的 | 对齐两个序列 | 捕捉序列内部的依赖关系 |
核心思想
对于输入序列中的每个词,Self-Attention让它"询问"序列中所有其他词:"你跟我有多相关?"然后根据相关性加权聚合信息。
示例:"The animal didn't cross the street because it was too tired."
- "it"指代什么?人类一看就知道是"animal"
- Self-Attention让"it"能够直接关注到"animal",无论它们之间隔了多少个词
这就是Attention相比RNN的核心优势:任意两个词之间的路径长度为 \(O(1)\),而RNN中是 \(O(n)\)。
Q, K, V 的来源
在Self-Attention中,Q、K、V都来自同一个输入序列,只是通过不同的线性变换得到:
其中 \(X \in \mathbb{R}^{n \times d_{\text{model}}}\) 是输入序列的嵌入矩阵(\(n\) 个词,每个词 \(d_{\text{model}}\) 维),\(W^Q, W^K, W^V \in \mathbb{R}^{d_{\text{model}} \times d_k}\) 是可学习的投影矩阵。
直觉理解 Q, K, V:
可以把Self-Attention想象成一个信息检索系统:
- Query(查询):"我在找什么信息?" — 当前词想要获取的信息
- Key(键):"我能提供什么信息?" — 每个词用来被匹配的标签
- Value(值):"我实际包含什么信息?" — 每个词的实际内容
类比图书馆:你带着一个问题(Query)去图书馆,每本书都有一个标签(Key),你根据问题和标签的匹配程度决定读哪些书,最后综合这些书的内容(Value)得到答案。
为什么需要三个不同的矩阵?
如果Q = K = V = X,那么每个词最关注的总是自己(自己和自己最相似)。通过不同的投影矩阵,模型可以学习: - 在Q空间中,"it"表达的是"我在找一个名词性的先行词" - 在K空间中,"animal"表达的是"我是一个名词,可以被代词指代" - 在V空间中,"animal"提供它的语义信息
Scaled Dot-Product Attention
这是Transformer中使用的具体注意力计算方式。
完整数学推导
输入: - \(Q \in \mathbb{R}^{n \times d_k}\)(Query矩阵) - \(K \in \mathbb{R}^{n \times d_k}\)(Key矩阵) - \(V \in \mathbb{R}^{n \times d_v}\)(Value矩阵)
Step 1:计算注意力分数
\(S_{ij}\) 是第 \(i\) 个Query和第 \(j\) 个Key的点积,表示它们的"相似度"。这一步生成一个 \(n \times n\) 的注意力矩阵。
Step 2:缩放(Scaling)
为什么要除以 \(\sqrt{d_k}\)? 这是一个关键的细节:
假设 \(Q\) 和 \(K\) 的每个元素都是独立的均值为0、方差为1的随机变量。那么它们的点积 \(q \cdot k = \sum_{i=1}^{d_k} q_i k_i\) 的方差为 \(d_k\)(每项方差为1,共 \(d_k\) 项独立相加)。
当 \(d_k\) 很大时(比如64或512),点积的值会很大,导致Softmax的输入落入梯度极小的饱和区:
几乎所有注意力都集中在一个位置上,梯度接近于零,模型无法学习。除以 \(\sqrt{d_k}\) 将方差重新归一化为1,保持Softmax在有梯度的区间工作。
Step 3:Softmax归一化
对每一行做Softmax,使得每个Query对所有Key的注意力权重之和为1。
Step 4:加权求和
完整计算流程示意:
Q ──→ ┐
├──→ QKᵀ ──→ ÷√dₖ ──→ (Mask) ──→ Softmax ──→ × V ──→ Output
K ──→ ┘ ↑
(可选:Decoder中的
因果掩码Causal Mask)
V ────────────────────────────────────────────────→ ┘
数值示例
假设句子 "I love cats",\(d_k = 2\)(极度简化):
Step 1: \(QK^T = \begin{bmatrix} 1 & 0 & 0.5 \\ 0 & 1 & 0.5 \\ 1 & 1 & 1 \end{bmatrix}\)
Step 2: \(\frac{QK^T}{\sqrt{2}} = \begin{bmatrix} 0.71 & 0 & 0.35 \\ 0 & 0.71 & 0.35 \\ 0.71 & 0.71 & 0.71 \end{bmatrix}\)
Step 3: 对每行Softmax → \(A \approx \begin{bmatrix} 0.43 & 0.21 & 0.30 \\ 0.21 & 0.43 & 0.30 \\ 0.33 & 0.33 & 0.33 \end{bmatrix}\)
Step 4: \(\text{Output} = A \cdot V\),每一行是V各行的加权平均
"I"(第一行)最关注自己(0.43),"cats"其次(0.30),"love"最少(0.21)。
Multi-Head Attention(多头注意力)
动机
一组 \(W^Q, W^K, W^V\) 只能学习一种注意力模式。但自然语言中的关系是多维度的:
- 语法关系:主语 → 动词("猫"→"坐")
- 指代关系:代词 → 先行词("它"→"猫")
- 修饰关系:形容词 → 名词("黑色的"→"猫")
- 局部关系:相邻词之间的搭配
Multi-Head Attention让模型同时从多个角度关注不同的关系模式。
数学定义
将 \(d_{\text{model}}\) 维的Q、K、V分别投影到 \(h\) 个不同的低维子空间,各自独立做注意力,最后拼接:
其中每个头:
参数维度:
- 输入维度:\(d_{\text{model}}\)(例如512)
- 头的数量:\(h\)(例如8)
- 每个头的维度:\(d_k = d_v = d_{\text{model}} / h\)(例如64)
- 投影矩阵:\(W_i^Q, W_i^K \in \mathbb{R}^{d_{\text{model}} \times d_k}\),\(W_i^V \in \mathbb{R}^{d_{\text{model}} \times d_v}\)
- 输出投影:\(W^O \in \mathbb{R}^{h \cdot d_v \times d_{\text{model}}}\)
计算流程
┌→ head₁: Attention(QW₁Q, KW₁K, VW₁V) ─┐
│ │
Input Q,K,V ────→ ├→ head₂: Attention(QW₂Q, KW₂K, VW₂V) ─┼→ Concat → W^O → Output
│ │
├→ ... │
│ │
└→ headₕ: Attention(QWₕQ, KWₕK, VWₕV) ─┘
参数量分析
单头注意力的参数量(如果用全维度 \(d_{\text{model}}\)):
多头注意力(\(h=8\))的参数量:
等等,更精确地计算:\(W_i^Q, W_i^K, W_i^V\) 各为 \(d_{\text{model}} \times d_k\),共 \(h\) 个头:
加上输出投影 \(W^O\):\(d_{\text{model}}^2\),总计 \(4 \times d_{\text{model}}^2\)。
关键洞察:多头注意力的参数量与单头全维度注意力相当(因为每个头的维度 \(d_k = d_{\text{model}}/h\) 降低了),但表达能力更强,因为它能同时捕捉多种关系模式。
多头注意力的可视化
不同的头会学到不同的注意力模式。下面是一个概念性的示例:
句子: "The cat sat on the mat because it was tired"
Head 1(语法关系): Head 2(指代关系):
"sat" ←──── "cat" "it" ←──── "cat"
"sat" ←──── "mat" "was" ←── "it"
Head 3(局部关系): Head 4(长距离):
"the" ←──── "cat" "tired" ←── "cat"
"the" ←──── "mat" "sat" ←──── "tired"
每个头都像一个"专家",从不同角度分析句子。最后拼接所有头的输出,汇总所有角度的信息。
注意力的掩码机制(Masking)
在实际使用中,注意力计算常需要配合掩码:
Padding Mask
由于batch中句子长度不同,短句需要padding补齐。Padding位置的注意力权重应为0:
\(-\infty\) 经过Softmax后变为0,确保模型不关注padding位置。
Causal Mask(因果掩码 / Look-ahead Mask)
在Decoder中,生成第 \(t\) 个词时不能看到 \(t\) 之后的词(否则就是"作弊")。用一个上三角矩阵掩盖未来位置:
这保证了自回归生成的因果性:每个位置只能关注自己和之前的位置。
复杂度分析
| Self-Attention | RNN | CNN | |
|---|---|---|---|
| 每层计算量 | \(O(n^2 \cdot d)\) | \(O(n \cdot d^2)\) | \(O(k \cdot n \cdot d^2)\) |
| 顺序操作数 | \(O(1)\) | \(O(n)\) | \(O(1)\) |
| 最大路径长度 | \(O(1)\) | \(O(n)\) | \(O(\log_k n)\) |
- \(n\):序列长度,\(d\):模型维度,\(k\):卷积核大小
关键权衡:
- Self-Attention的计算量与序列长度呈二次方关系(\(n^2\)),这是它的主要瓶颈。处理10万token的长文档非常昂贵。
- 但Self-Attention的顺序操作数为 \(O(1)\)——所有位置可以并行计算,而RNN必须一步一步来。这使得Attention在GPU上的实际训练速度远快于RNN。
- Self-Attention的最大路径长度为 \(O(1)\)——任意两个位置之间只需一步即可直接交互,不像RNN需要经过 \(O(n)\) 步的信息传递。这是Attention能捕捉长距离依赖的根本原因。
从Attention到Transformer
注意力机制的发展脉络:
RNN Encoder-Decoder(2014)
↓ 固定向量瓶颈
Bahdanau Attention(2014)— 让Decoder回头看Encoder
↓ 简化评分函数
Luong Attention(2015)— 点积评分,更高效
↓ 序列内部自注意力
Self-Attention — 一个序列内部互相关注
↓ 多角度并行
Multi-Head Attention — 多个注意力头同时工作
↓ 完全抛弃RNN
Transformer(2017)— "Attention is All You Need"
Transformer的核心贡献不是发明了注意力机制,而是证明了:只用注意力机制(加上一些简单的前馈网络和残差连接),不需要任何循环或卷积结构,就能在序列建模任务上达到最优性能。
下一步:→ Transformer架构(详解完整的Encoder-Decoder架构)