Skip to content

注意力机制

注意力机制(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\) 之间的"相关性":

\[ e_{tj} = a(s_{t-1}, h_j) = v_a^T \tanh(W_a \cdot s_{t-1} + U_a \cdot h_j) \]

其中 \(W_a, U_a, v_a\) 都是可学习参数。这个评分函数被称为加性注意力(Additive Attention),因为它把两个向量先分别线性变换再相加

Step 2:计算注意力权重(Attention Weights)

对所有源位置的分数做Softmax归一化,得到概率分布:

\[ \alpha_{tj} = \frac{\exp(e_{tj})}{\sum_{k=1}^{T_x} \exp(e_{tk})} \]

\(\alpha_{tj}\) 表示"解码器在生成第 \(t\) 个目标词时,对源句子第 \(j\) 个词的关注程度"。所有 \(\alpha_{tj}\) 之和为1。

Step 3:计算上下文向量(Context Vector)

对编码器隐藏状态做加权求和:

\[ c_t = \sum_{j=1}^{T_x} \alpha_{tj} \cdot h_j \]

注意:这里每一步 \(t\) 都会生成一个不同的 \(c_t\)!不再是一个固定向量。翻译"cat"时,\(c_t\) 可能主要关注"chat";翻译"red"时,\(c_t\) 可能主要关注"rouge"。

Step 4:解码

将上下文向量 \(c_t\) 与解码器当前状态结合,预测下一个词:

\[ s_t = f(s_{t-1}, y_{t-1}, c_t) \]
\[ P(y_t | y_{<t}, x) = \text{softmax}(W_o \cdot [s_t; 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都来自同一个输入序列,只是通过不同的线性变换得到:

\[ Q = X W^Q, \quad K = X W^K, \quad V = X W^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 = Q K^T \in \mathbb{R}^{n \times n} \]

\(S_{ij}\) 是第 \(i\) 个Query和第 \(j\) 个Key的点积,表示它们的"相似度"。这一步生成一个 \(n \times n\) 的注意力矩阵。

Step 2:缩放(Scaling)

\[ S_{\text{scaled}} = \frac{Q K^T}{\sqrt{d_k}} \]

为什么要除以 \(\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的输入落入梯度极小的饱和区:

\[ \text{softmax}([100, 1, 1]) \approx [1.0, 0.0, 0.0] \]

几乎所有注意力都集中在一个位置上,梯度接近于零,模型无法学习。除以 \(\sqrt{d_k}\) 将方差重新归一化为1,保持Softmax在有梯度的区间工作。

Step 3:Softmax归一化

\[ A = \text{softmax}\left(\frac{Q K^T}{\sqrt{d_k}}\right) \]

对每一行做Softmax,使得每个Query对所有Key的注意力权重之和为1。

Step 4:加权求和

\[ \text{Attention}(Q, K, V) = A \cdot V = \text{softmax}\left(\frac{Q K^T}{\sqrt{d_k}}\right) V \]

完整计算流程示意:

Q ──→ ┐
      ├──→ QKᵀ ──→ ÷√dₖ ──→ (Mask) ──→ Softmax ──→ × V ──→ Output
K ──→ ┘                        ↑
                          (可选:Decoder中的
                           因果掩码Causal Mask)
V ────────────────────────────────────────────────→ ┘

数值示例

假设句子 "I love cats",\(d_k = 2\)(极度简化):

\[ Q = \begin{bmatrix} 1 & 0 \\ 0 & 1 \\ 1 & 1 \end{bmatrix}, \quad K = \begin{bmatrix} 1 & 0 \\ 0 & 1 \\ 0.5 & 0.5 \end{bmatrix}, \quad V = \begin{bmatrix} 1 & 2 \\ 3 & 4 \\ 5 & 6 \end{bmatrix} \]

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\) 个不同的低维子空间,各自独立做注意力,最后拼接:

\[ \text{MultiHead}(Q, K, V) = \text{Concat}(\text{head}_1, \ldots, \text{head}_h) \cdot W^O \]

其中每个头:

\[ \text{head}_i = \text{Attention}(Q W_i^Q, K W_i^K, V W_i^V) \]

参数维度:

  • 输入维度:\(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}}\)):

\[ 3 \times d_{\text{model}}^2 = 3 \times 512^2 = 786,432 \]

多头注意力\(h=8\))的参数量:

\[ h \times 3 \times d_{\text{model}} \times d_k + d_{\text{model}}^2 = 8 \times 3 \times 512 \times 64 + 512^2 = 786,432 + 262,144 = 1,048,576 \]

等等,更精确地计算:\(W_i^Q, W_i^K, W_i^V\) 各为 \(d_{\text{model}} \times d_k\),共 \(h\) 个头:

\[ \text{投影参数} = h \times 3 \times d_{\text{model}} \times d_k = 3 \times d_{\text{model}} \times (h \times d_k) = 3 \times d_{\text{model}}^2 \]

加上输出投影 \(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:

\[ S_{ij} = \begin{cases} q_i \cdot k_j & \text{if position } j \text{ is valid} \\ -\infty & \text{if position } j \text{ is padding} \end{cases} \]

\(-\infty\) 经过Softmax后变为0,确保模型不关注padding位置。

Causal Mask(因果掩码 / Look-ahead Mask)

在Decoder中,生成第 \(t\) 个词时不能看到 \(t\) 之后的词(否则就是"作弊")。用一个上三角矩阵掩盖未来位置:

\[ \text{Mask} = \begin{bmatrix} 0 & -\infty & -\infty & -\infty \\ 0 & 0 & -\infty & -\infty \\ 0 & 0 & 0 & -\infty \\ 0 & 0 & 0 & 0 \end{bmatrix} \]

这保证了自回归生成的因果性:每个位置只能关注自己和之前的位置。


复杂度分析

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架构)


评论 #