1.模型对比
-
RNN(马尔科夫链式编码)
- 通过递归计算逐个处理 token,当前编码结果 h t h_t ht仅依赖前一步的隐藏状态 h t − 1 h_{t-1} ht−1和当前输入 x t x_t xt
- 局限性:序列建模需严格串行,无法并行;长距离依赖易丢失(梯度消失/爆炸)
- 例:双向 LSTM 需正向+反向两次遍历才能捕获上下文,但仍是局部传递
-
CNN(局部窗口编码)
- 使用固定尺寸的卷积核(如窗口为3)聚合局部上下文信息
- 局限性:单层仅能捕获窗口内的局部特征,需多层堆叠扩大感受野
- 例:深度 CNN 需多层级联才能建模长距离依赖,信息传递路径长
-
Attention(全局交互编码)
- 通过 Query-Key-Value 矩阵计算,直接建立任意两个 token 间的关联
- 核心优势:单层即可全局交互,每个位置的编码融合了序列中所有 token 的信息
- 例:Self-Attention 中,每个词与整个序列计算相似度权重(如公式中的 Q K T / d QK^T/\sqrt{d} QKT/d)
2.Attention的实现方式
1.Attention的常见实现方式
-
加性 Attention (Additive Attention / Bahdanau Attention)
- 公式:
Score ( Q , K i ) = v T tanh ( W q Q + W k K i ) \text{Score}(Q, K_i) = v^T \tanh(W_q Q + W_k K_i) Score(Q,Ki)=vTtanh(WqQ+WkKi) - 特点:通过可学习的参数矩阵 W q , W k W_q, W_k Wq,Wk和向量 v v v计算注意力权重,适用于 Query 和 Key 维度不同的场景。
- 公式:
-
点积 Attention (Dot-Product Attention / Luong Attention)
- 公式:
Score ( Q , K i ) = Q ⋅ K i \text{Score}(Q, K_i) = Q \cdot K_i Score(Q,Ki)=Q⋅Ki - 特点:计算高效(矩阵乘法),但需 Query 和 Key 维度相同;当维度较高时,点积结果可能过大,导致 Softmax 梯度消失。
- 公式:
-
缩放点积 Attention (Scaled Dot-Product Attention)
- 改进点:在点积基础上引入缩放因子 d k \sqrt{d_k} dk,缓解高维点积结果过大的问题。
- 公式:
Attention ( Q , K , V ) = softmax ( Q K T d k ) V \text{Attention}(Q, K, V) = \text{softmax}\left(\frac{QK^T}{\sqrt{d_k}}\right)V Attention(Q,K,V)=softmax(dkQKT)V
-
其他变体
- 局部 Attention:仅关注序列的局部窗口,降低计算复杂度。
- 多头 Attention (Multi-Head):将 Q/K/V 投影到多个子空间,并行计算多个注意力头,最后拼接结果。
2.Scaled Dot-Product Attention 详解
核心思想
-
输入矩阵:
- Query Q Q Q:形状为 ( B , seq_len , d k ) (B, \text{seq\_len}, d_k) (B,seq_len,dk),代表需要计算注意力的目标序列。
- Key K K K和 Value V V V:形状均为 ( B , seq_len , d k ) (B, \text{seq\_len}, d_k) (B,seq_len,dk)和 ( B , seq_len , d v ) (B, \text{seq\_len}, d_v) (B,seq_len,dv),代表源序列信息。
- 其中 B B B为批量大小, d k d_k dk和 d v d_v dv为维度。
-
计算步骤:
- 点积:计算 Q Q Q和 K K K的相似度矩阵 Q K T QK^T QKT。
- 缩放:除以 d k \sqrt{d_k} dk,防止高维点积结果过大导致 Softmax 梯度消失。
- Softmax:沿 Key 的维度归一化得到注意力权重。
- 加权和:用权重对 V V V加权求和,得到最终编码。
代码实现 (PyTorch)
import torch
import torch.nn as nn
import torch.nn.functional as F
class ScaledDotProductAttention(nn.Module):
def __init__(self, d_k):
super().__init__()
self.d_k = d_k # Query和Key的维度
def forward(self, Q, K, V, mask=None):
# Q: (B, seq_len_q, d_k)
# K: (B, seq_len_k, d_k)
# V: (B, seq_len_k, d_v)
# mask: (B, seq_len_q, seq_len_k)
# 计算点积注意力分数
scores = torch.matmul(Q, K.transpose(-2, -1)) # (B, seq_len_q, seq_len_k)
# 缩放
scores = scores / (self.d_k ** 0.5)
# 可选:应用mask(如遮挡未来词)
if mask is not None:
scores = scores.masked_fill(mask == 0, -1e9)
# Softmax归一化
attn_weights = F.softmax(scores, dim=-1) # (B, seq_len_q, seq_len_k)
# 加权和
output = torch.matmul(attn_weights, V) # (B, seq_len_q, d_v)
return output, attn_weights
代码解析
-
矩阵乘法:
torch.matmul(Q, K.transpose(-2, -1))
计算 Q K T QK^T QKT,得到形状为 ( B , seq_len_q , seq_len_k ) (B, \text{seq\_len\_q}, \text{seq\_len\_k}) (B,seq_len_q,seq_len_k)的相似度矩阵。
-
缩放:
- 除以 d k \sqrt{d_k} dk防止点积结果过大,稳定训练。
-
Mask 机制(可选):
- 在解码器中,为避免模型看到未来信息,需将未来位置的注意力分数设为极小值(如
-1e9
),Softmax 后这些位置权重接近 0。
- 在解码器中,为避免模型看到未来信息,需将未来位置的注意力分数设为极小值(如
-
Softmax 归一化:
- 沿最后一个维度(
dim=-1
,即 Key 的序列方向)归一化,确保每个 Query 位置的权重和为 1。
- 沿最后一个维度(
-
加权求和:
- 用注意力权重对 Value 矩阵加权,输出形状为 ( B , seq_len_q , d v ) (B, \text{seq\_len\_q}, d_v) (B,seq_len_q,dv)。
3.使用示例
# 参数设置
batch_size = 2
seq_len = 5
d_k = 64 # Query和Key的维度
d_v = 128 # Value的维度
# 随机生成输入
Q = torch.randn(batch_size, seq_len, d_k)
K = torch.randn(batch_size, seq_len, d_k)
V = torch.randn(batch_size, seq_len, d_v)
# 初始化Attention模块
attention = ScaledDotProductAttention(d_k=d_k)
# 前向计算
output, attn_weights = attention(Q, K, V)
print("Output shape:", output.shape) # (2, 5, 128)
print("Attention weights shape:", attn_weights.shape) # (2, 5, 5)
3.Multi-Head Attention的实现方式
1.Multi-Head Attention 原理
核心思想
-
特征空间映射:
- 将输入的
Q, K, V
分别通过线性投影映射到h
个不同的子空间(即h
个头),每个头关注不同的语义模式。 - 投影后的维度:每个头的
Q/K
维度为d_k
,V
维度为d_v
,且满足h * d_k = d_model
(输入总维度)。
- 将输入的
-
并行计算:
- 在每个子空间中独立计算 Scaled Dot-Product Attention,得到
h
个头的输出。
- 在每个子空间中独立计算 Scaled Dot-Product Attention,得到
-
结果拼接:
- 将
h
个头的输出拼接后,通过线性层融合回d_model
维度,形成最终结果。
- 将
公式表示
MultiHead
(
Q
,
K
,
V
)
=
Concat
(
head
1
,
…
,
head
h
)
W
O
\text{MultiHead}(Q, K, V) = \text{Concat}(\text{head}_1, \dots, \text{head}_h)W^O
MultiHead(Q,K,V)=Concat(head1,…,headh)WO
head
i
=
Attention
(
Q
W
i
Q
,
K
W
i
K
,
V
W
i
V
)
\text{head}_i = \text{Attention}(QW_i^Q, KW_i^K, VW_i^V)
headi=Attention(QWiQ,KWiK,VWiV)
2.PyTorch 代码实现
完整代码
import torch
import torch.nn as nn
import torch.nn.functional as F
class MultiHeadAttention(nn.Module):
def __init__(self, d_model, h, d_k, d_v):
super().__init__()
self.d_model = d_model # 输入维度(总维度)
self.h = h # 注意力头数量
self.d_k = d_k # 每个头的Query/Key维度
self.d_v = d_v # 每个头的Value维度
# 定义线性投影矩阵
self.W_q = nn.Linear(d_model, h * d_k) # 将Q投影到h个头的d_k维度
self.W_k = nn.Linear(d_model, h * d_k)
self.W_v = nn.Linear(d_model, h * d_v)
self.W_o = nn.Linear(h * d_v, d_model) # 输出融合矩阵
def forward(self, Q, K, V, mask=None):
batch_size = Q.size(0)
# 1. 线性投影:分头
q_proj = self.W_q(Q).view(batch_size, -1, self.h, self.d_k) # (B, seq_len, h, d_k)
k_proj = self.W_k(K).view(batch_size, -1, self.h, self.d_k)
v_proj = self.W_v(V).view(batch_size, -1, self.h, self.d_v) # (B, seq_len, h, d_v)
# 2. 调整维度,用于并行计算多个头
q = q_proj.transpose(1, 2) # (B, h, seq_len, d_k)
k = k_proj.transpose(1, 2)
v = v_proj.transpose(1, 2)
# 3. 计算 Scaled Dot-Product Attention(每个头独立计算)
scores = torch.matmul(q, k.transpose(-2, -1)) / (self.d_k ** 0.5) # (B, h, seq_len, seq_len)
if mask is not None:
scores = scores.masked_fill(mask.unsqueeze(1) == 0, -1e9) # 扩展mask到所有头
attn_weights = F.softmax(scores, dim=-1)
output = torch.matmul(attn_weights, v) # (B, h, seq_len, d_v)
# 4. 拼接多头结果并融合
output = output.transpose(1, 2).contiguous() # (B, seq_len, h, d_v)
output = output.view(batch_size, -1, self.h * self.d_v) # (B, seq_len, h*d_v)
output = self.W_o(output) # (B, seq_len, d_model)
return output, attn_weights
3.代码解析
1. 线性投影与分头
- 输入处理:
Q/K/V
形状为(B, seq_len, d_model)
。- 通过
W_q, W_k, W_v
将输入投影到h
个头的子空间:q_proj
形状变为(B, seq_len, h, d_k)
。view
操作将投影后的张量分割为h
个头。
- 维度调整:
- 使用
transpose(1, 2)
将头维度h
移到第1维,得到(B, h, seq_len, d_k)
,便于并行计算。
- 使用
2. 并行计算注意力
- 矩阵乘法:
q @ k.transpose(-2, -1)
计算每个头的相似度矩阵,形状为(B, h, seq_len, seq_len)
。
- Mask 处理:
- 若提供
mask
(如解码器遮挡未来词),需扩展维度mask.unsqueeze(1)
以匹配多头形状。
- 若提供
- Softmax 与加权和:
- 计算注意力权重后,与
v
加权求和,输出形状为(B, h, seq_len, d_v)
。
- 计算注意力权重后,与
3. 结果拼接与融合
- 维度还原:
transpose(1, 2)
将头维度移回,形状变为(B, seq_len, h, d_v)
。view
操作拼接所有头的输出,得到(B, seq_len, h*d_v)
。
- 线性融合:
- 通过
W_o
将拼接后的结果映射回d_model
维度,保持输入输出维度一致。
- 通过
4.使用示例
# 参数设置
batch_size = 2
seq_len = 10
d_model = 512 # 输入维度
h = 8 # 注意力头数量
d_k = 64 # 每个头的Q/K维度(d_model / h = 512/8=64)
d_v = 64 # 每个头的V维度
# 随机生成输入(Q/K/V相同则为Self-Attention)
Q = torch.randn(batch_size, seq_len, d_model)
K = torch.randn(batch_size, seq_len, d_model)
V = torch.randn(batch_size, seq_len, d_model)
# 初始化多头注意力模块
multihead_attn = MultiHeadAttention(d_model, h, d_k, d_v)
# 前向计算
output, attn_weights = multihead_attn(Q, K, V)
print("Output shape:", output.shape) # (2, 10, 512)
print("Attention weights shape:", attn_weights.shape) # (2, 8, 10, 10)
4.疑问
1.为什么使用 d k \sqrt{d_k} dk?
假设我们有两个向量
q
q
q和
k
k
k,它们的维度都是
d
k
d_k
dk。它们的点积定义为:
q
⋅
k
=
∑
i
=
1
d
k
q
i
k
i
q \cdot k = \sum_{i=1}^{d_k} q_i \, k_i
q⋅k=i=1∑dkqiki
通常我们假设:
- 向量 q q q和 k k k的各个分量都是相互独立的,
- 每个分量的均值为 0,
- 每个分量的方差为 1。
这里的“独立”假设意味着不同维度之间没有相关性,“方差为1”是为了让数值在不同维度上处于相似的尺度。需要注意的是,假如数据呈现正态分布,那这组假设会更加严格且易于分析,但关键并不在于分布必须正态,只要满足均值 0、方差为 1 以及相互独立,结论依然成立。实际上,很多情况下我们更多的是使用“零均值、单位方差且相互独立”作为一种简化假设。
由于
q
i
q_i
qi和
k
i
k_i
ki独立,且均值为 0,根据随机变量的性质,乘积
q
i
k
i
q_i \, k_i
qiki的均值也为 0。计算每一项的方差,我们有:
Var
(
q
i
k
i
)
=
Var
(
q
i
)
⋅
Var
(
k
i
)
=
1
⋅
1
=
1
\operatorname{Var}(q_i \, k_i) = \operatorname{Var}(q_i) \cdot \operatorname{Var}(k_i) = 1 \cdot 1 = 1
Var(qiki)=Var(qi)⋅Var(ki)=1⋅1=1
因为每一项
q
i
k
i
q_i k_i
qiki的方差都是 1,且不同项之间相互独立,所以整个点积的方差就是各项方差的和:
Var
(
∑
i
=
1
d
k
q
i
k
i
)
=
∑
i
=
1
d
k
Var
(
q
i
k
i
)
=
d
k
\operatorname{Var}\left(\sum_{i=1}^{d_k} q_i \, k_i\right) = \sum_{i=1}^{d_k} \operatorname{Var}(q_i \, k_i) = d_k
Var(i=1∑dkqiki)=i=1∑dkVar(qiki)=dk
由于点积的方差大约为 d k d_k dk,点积的标准差就是 d k \sqrt{d_k} dk。在 softmax 操作之前对点积进行归一化,目的是将数值尺度保持在一个合理的范围内,避免数值过大导致 softmax 输出非常尖锐,从而使得梯度过小,训练变得不稳定。
归一化操作写成:
Attention
(
q
,
k
)
=
softmax
(
q
⋅
k
d
k
)
\text{Attention}(q, k) = \text{softmax}\left(\frac{q \cdot k}{\sqrt{d_k}}\right)
Attention(q,k)=softmax(dkq⋅k)
这样可以使得经过归一化后的点积的标准差稳定在 1 左右。
2.Attention的计算方法让Q、K、V成为了Query-Key-Value三种不同的向量吗?
在注意力机制中,Q、K、V三个向量各自承担着不同的角色,它们的命名和顺序来源于它们在计算流程中的功能,而不是随意排列的。下面详细解释这种设计和它背后的逻辑:
1.向量的各自含义和职责
- Q (Query) - 查询向量
这个向量代表“提问”的部分。对于每个要产生输出的元素,它会构造一个查询,用来“询问”序列中哪些信息与自己相关。 - K (Key) - 键向量
键向量用于标识存储在输入中的信息。当计算注意力时,每个输入的位置都可以被看作有一个“存储”了信息的单元,而这个单元通过键向量来标识。 - V (Value) - 值向量
值向量包含了实际需要传递的信息。当查询和键匹配后,模型便会根据匹配程度(注意力权重)从值向量中加权获取信息,产生最终的输出。
2.为什么顺序是 Query-Key-Value 而不是 Value-Query-Key
- 计算流程决定了顺序:
在计算注意力时,核心步骤是首先计算查询向量与键向量之间的相似度:
Attention Score = Q ⋅ K T d k \text{Attention Score} = \frac{Q \cdot K^T}{\sqrt{d_k}} Attention Score=dkQ⋅KT
这个得分决定了每个输入元素在最终输出中所占的权重。随后,使用这些权重对值向量进行加权求和,形成输出:
Output = softmax ( Q K T d k ) V \text{Output} = \text{softmax}\left(\frac{QK^T}{\sqrt{d_k}}\right)V Output=softmax(dkQKT)V
因此,先有查询再有键,而值则在最后起到信息传递的作用。如果顺序颠倒,例如变成 Value-Query-Key,就无法进行这种逻辑上明确的分离:首先需要查询,然后需要用键来匹配,再依靠值传递具体的信息。 - 语义一致性:
使用 “Query-Key-Value”的顺序准确反映了实际操作过程:- Query(查询):发出问题或请求。
- Key(键):充当回答的“关键词”,帮助确定哪些位置的信息与查询相关。
- Value(值):提供具体的信息作为回答。
倒置为 “Value-Query-Key” 则会混淆这种因果关系,即不知道哪个部分是发问者、哪个部分用于匹配以及哪个部分包含答案的信息。
- 模型训练和实现的角度:
在训练过程中,每个权重矩阵 ( W^Q )、( W^K ) 和 ( W^V ) 是独立学习的。它们专门承担着不同的数据变换任务。如果改变顺序,不仅计算公式会变得不合理,而且这些权重更新时所承载的语义也会混乱,导致模型难以学习到正确的关系。
3.总结
因此,Q、K、V 的命名和顺序(Query-Key-Value)是严格按照它们在注意力计算中承担的角色确定的: - Query 用于“提出问题”
- Key 用于“找到相关信息的关键词”
- Value 用于“提供实际信息”
这样的设计使得自注意力机制可以先通过查询对输入中的各个位置进行匹配,再利用匹配结果对实际信息进行加权求和,最终得到输出。将顺序改为 Value-Query-Key 不仅在数学上无法对应实际计算流程,也会使语义和函数目的混乱,从而破坏注意力机制的正常工作。