注意力机制完整指南

一、注意力计算核心规则

1.1 基本公式

1
Attention(Q, K, V) = softmax(QK^T / √dk) · V

其中:

  • Q (Query): 查询向量,表示”我在找什么”
  • K (Key): 键向量,表示”我有什么”
  • V (Value): 值向量,表示”实际要用的信息”
  • dk: 键向量维度,√dk 用于缩放防止点积过大
  • softmax: 将点积结果归一化为概率分布(注意力权重)

1.2 计算步骤

1
2
3
4
5
6
第1步: 计算相似度   score = Q @ K^T           → (batch, seq_q, seq_k)
第2步: 缩放 score /= √dk → (batch, seq_q, seq_k)
第3步: 掩码 score[mask] = -inf → 处理变长序列/因果屏蔽
第4步: 归一化 attn_weights = softmax(score) → (batch, seq_q, seq_k)
第5步: 加权求和 output = attn_weights @ V → (batch, seq_q, d_model)
第6步: 残差+归一化 output = ln(output) + residual

1.3 直观理解

想象你在图书馆找资料:

  • Q = 你的搜索关键词(”深度学习论文”)
  • K = 每本书的书名标签
  • V = 每本书的摘要内容
  • Attention = 根据你的关键词,给每本书打分,分数高的书摘要被更多地采纳

二、9种注意力机制实现

2.1 缩放点积注意力 (Scaled Dot-Product Attention)

最基础、最常用的注意力形式,Transformer 的核心组件。

1
2
Input:  Q ∈ R^(n×dk), K ∈ R^(m×dk), V ∈ R^(m×dv)
Output: R^(n×dv)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import torch
import torch.nn as nn
import torch.nn.functional as F
import math
from typing import Tuple, Optional

class ScaledDotProductAttention(nn.Module):
"""缩放点积注意力

公式: Attention(Q,K,V) = softmax(QK^T / sqrt(dk)) * V
"""
def __init__(self, dropout_p=0.1):
super().__init__()
self.dropout = nn.Dropout(dropout_p)

def forward(self, Q, K, V, mask=None):
"""
Args:
Q: (batch, seq_len_q, d_model)
K: (batch, seq_len_k, d_model)
V: (batch, seq_len_k, d_model)
mask: (batch, 1, seq_len_k), 0表示屏蔽
Returns:
output: (batch, seq_len_q, d_model)
attn_weights: (batch, seq_len_q, seq_len_k)
"""
d_k = Q.size(-1)
scores = torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(d_k)

if mask is not None:
scores = scores.masked_fill(mask == 0, float('-inf'))

attn_weights = F.softmax(scores, dim=-1)
attn_weights = self.dropout(attn_weights)
output = torch.matmul(attn_weights, V)

return output, attn_weights


# 测试
def test_scaled_dot_product():
batch, seq_q, seq_k, d_model = 2, 5, 8, 64
Q = torch.randn(batch, seq_q, d_model)
K = torch.randn(batch, seq_k, d_model)
V = torch.randn(batch, seq_k, d_model)

attn = ScaledDotProductAttention()
output, weights = attn(Q, K, V)

print(f"[1] 缩放点积注意力")
print(f" Q:{Q.shape} K:{K.shape} V:{V.shape}")
print(f" output:{output.shape} weights:{weights.shape}")
print(f" weights sum per head: {weights.sum(dim=-1)[:2]}")
print(f" PASS")

test_scaled_dot_product()

2.2 多头注意力 (Multi-Head Attention)

核心思想:让模型在不同的表示子空间中”联合关注”来自不同位置的信息。

1
2
3
1. 将 Q, K, V 分别投影到 h 个子空间
2. 在每个子空间上独立做缩放点积注意力
3. 将所有头的输出拼接,再做一次线性变换
1
2
MultiHead(Q,K,V) = Concat(head_1, ..., head_h) · W^O
其中 head_i = Attention(QW_i^Q, KW_i^K, VW_i^V)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
class MultiHeadAttention(nn.Module):
"""多头注意力 (Multi-Head Attention)

MultiHead(Q,K,V) = Concat(head_1, ..., head_h) * W^O
其中 head_i = Attention(QW_i^Q, KW_i^K, VW_i^V)
"""
def __init__(self, d_model=512, n_heads=8, dropout_p=0.1):
super().__init__()
assert d_model % n_heads == 0
self.d_model = d_model
self.n_heads = n_heads
self.d_k = d_model // n_heads

self.W_Q = nn.Linear(d_model, d_model)
self.W_K = nn.Linear(d_model, d_model)
self.W_V = nn.Linear(d_model, d_model)
self.W_O = nn.Linear(d_model, d_model)

self.attention = ScaledDotProductAttention(dropout_p)
self.dropout = nn.Dropout(dropout_p)

def forward(self, Q, K, V, mask=None):
"""
Args:
Q: (batch, seq_len_q, d_model)
K: (batch, seq_len_k, d_model)
V: (batch, seq_len_k, d_model)
Returns:
output: (batch, seq_len_q, d_model)
attn_weights: (batch, n_heads, seq_len_q, seq_len_k)
"""
batch_size = Q.size(0)

# 1. 线性变换
Q = self.W_Q(Q)
K = self.W_K(K)
V = self.W_V(V)

# 2. 维度变换: (batch, seq, d_model) -> (batch, n_heads, seq, d_k)
Q = Q.view(batch_size, -1, self.n_heads, self.d_k).transpose(1, 2)
K = K.view(batch_size, -1, self.n_heads, self.d_k).transpose(1, 2)
V = V.view(batch_size, -1, self.n_heads, self.d_k).transpose(1, 2)

# 3. 广播掩码
if mask is not None:
mask = mask.unsqueeze(1).unsqueeze(1)

# 4. 多头注意力
attn_output, attn_weights = self.attention(Q, K, V, mask=mask)

# 5. 拼接所有头
attn_output = attn_output.transpose(1, 2).contiguous()
attn_output = attn_output.view(batch_size, -1, self.d_model)

# 6. 最终线性变换
output = self.W_O(attn_output)
output = self.dropout(output)

return output, attn_weights


# 测试
def test_multi_head():
batch, seq_len, d_model, n_heads = 2, 10, 64, 8
Q = torch.randn(batch, seq_len, d_model)
K = torch.randn(batch, seq_len, d_model)
V = torch.randn(batch, seq_len, d_model)

mha = MultiHeadAttention(d_model=d_model, n_heads=n_heads)
output, weights = mha(Q, K, V)

print(f"[2] 多头注意力")
print(f" Q:{Q.shape}")
print(f" output:{output.shape}")
print(f" weights:{weights.shape}")
print(f" PASS")

test_multi_head()

2.3 自注意力 (Self-Attention)

Q = K = V,即序列内部的相互关注。编码器中使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class SelfAttention(nn.Module):
"""自注意力: Q=K=V,序列内部相互关注"""
def __init__(self, d_model=512, n_heads=8, dropout_p=0.1):
super().__init__()
self.mha = MultiHeadAttention(d_model, n_heads, dropout_p)

def forward(self, x, mask=None):
return self.mha(x, x, x, mask=mask)


# 测试
def test_self_attention():
batch, seq_len, d_model = 2, 10, 64
x = torch.randn(batch, seq_len, d_model)

sa = SelfAttention(d_model=d_model)
out, w = sa(x)
print(f"[3] 自注意力: input:{x.shape} -> output:{out.shape} weights:{w.shape}")
print(f" PASS")

test_self_attention()

2.4 交叉注意力 (Cross-Attention)

Q 来自一个序列,K 和 V 来自另一个序列。常用于编码器-解码器架构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class CrossAttention(nn.Module):
"""交叉注意力: Q来自decoder, K/V来自encoder"""
def __init__(self, d_model=512, n_heads=8, dropout_p=0.1):
super().__init__()
self.mha = MultiHeadAttention(d_model, n_heads, dropout_p)

def forward(self, query, key, value, mask=None):
return self.mha(query, key, value, mask=mask)


# 测试
def test_cross_attention():
batch = 2
query = torch.randn(batch, 6, 64) # decoder sequence
key = torch.randn(batch, 10, 64) # encoder sequence
value = torch.randn(batch, 10, 64)

ca = CrossAttention(d_model=64)
out, w = ca(query, key, value)
print(f"[4] 交叉注意力: query:{query.shape} key/value:{key.shape} -> output:{out.shape} weights:{w.shape}")
print(f" PASS")

test_cross_attention()

2.5 位置编码 (Positional Encoding)

Transformer 需要位置信息,因为自注意力本身是无序的。

正弦位置编码公式:

1
2
PE(pos, 2i)   = sin(pos / 10000^(2i/d_model))
PE(pos, 2i+1) = cos(pos / 10000^(2i/d_model))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class PositionalEncoding(nn.Module):
"""正弦位置编码

PE(pos, 2i) = sin(pos / 10000^(2i/d_model))
PE(pos, 2i+1) = cos(pos / 10000^(2i/d_model))
"""
def __init__(self, d_model, max_len=5000, dropout_p=0.1):
super().__init__()
self.dropout = nn.Dropout(dropout_p)
pe = torch.zeros(max_len, d_model)
position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1)
div_term = torch.exp(torch.arange(0, d_model, 2).float() *
(-math.log(10000.0) / d_model))
pe[:, 0::2] = torch.sin(position * div_term)
pe[:, 1::2] = torch.cos(position * div_term)
pe = pe.unsqueeze(0)
self.register_buffer('pe', pe)

def forward(self, x):
x = x + self.pe[:, :x.size(1), :]
return self.dropout(x)


# 测试
def test_positional_encoding():
batch, seq_len, d_model = 2, 10, 64
x = torch.randn(batch, seq_len, d_model)

pos_enc = PositionalEncoding(d_model=d_model)
encoded = pos_enc(x)
print(f"[5] 位置编码: input:{x.shape} -> output:{encoded.shape}")
print(f" PASS")

test_positional_encoding()

2.6 分组查询注意力 (GQA)

GPT-3.5 / Llama 2 使用的技术:介于 MHA 和 MQA 之间,减少 KV 缓存大小同时保持性能。

1
2
3
MHA:  8个头各有独立KV → 8组KV缓存
GQA: 8个头分4组,每组共享1组KV → 4组KV缓存
MQA: 所有头共享1组KV → 1组KV缓存
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
class GroupedQueryAttention(nn.Module):
"""GQA: heads分成n_groups组,每组共享一组KV

比MHA节省KV缓存,比MQA保持更好性能
用于GPT-3.5, Llama 2等模型
"""
def __init__(self, d_model=512, n_heads=8, n_groups=4, dropout_p=0.1):
super().__init__()
assert n_heads % n_groups == 0
self.d_model = d_model
self.n_heads = n_heads
self.n_groups = n_groups
self.d_k = d_model // n_heads

self.W_Q = nn.Linear(d_model, d_model)
self.W_K = nn.Linear(d_model, n_groups * self.d_k)
self.W_V = nn.Linear(d_model, n_groups * self.d_k)
self.W_O = nn.Linear(d_model, d_model)
self.dropout = nn.Dropout(dropout_p)

def forward(self, Q, K, V, mask=None):
batch_size = Q.size(0)

q = self.W_Q(Q).view(batch_size, -1, self.n_heads, self.d_k).transpose(1, 2)
k = self.W_K(K).view(batch_size, -1, self.n_groups, self.d_k).transpose(1, 2)
v = self.W_V(V).view(batch_size, -1, self.n_groups, self.d_k).transpose(1, 2)

# 重复k,v以匹配q的head数
repeat_factor = self.n_heads // self.n_groups
k = k.repeat_interleave(repeat_factor, dim=1)
v = v.repeat_interleave(repeat_factor, dim=1)

scores = torch.matmul(q, k.transpose(-2, -1)) / math.sqrt(self.d_k)
if mask is not None:
scores = scores.masked_fill(mask == 0, float('-inf'))
attn_w = F.softmax(scores, dim=-1)
output = torch.matmul(self.dropout(attn_w), v)

output = output.transpose(1, 2).contiguous().view(batch_size, -1, self.d_model)
return self.W_O(output)


# 测试
def test_gqa():
batch, seq_len, d_model = 2, 10, 64
x = torch.randn(batch, seq_len, d_model)

gqa = GroupedQueryAttention(d_model=d_model, n_heads=8, n_groups=4)
out = gqa(x, x, x)
print(f"[6] GQA: input:{x.shape} -> output:{out.shape}")
print(f" PASS")

test_gqa()

2.7 线性注意力 (Linear Attention)

将复杂度从 O(n²) 降到 O(n),适合长序列。

1
2
标准Attention: 先算 n×n 矩阵,再乘 V → O(n²d)
LinearAttn: 先算 K^T·V(d×d),再乘 Q → O(nd²)
1
Attention(Q,K,V) = phi(Q) dot (phi(K)^T dot V)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
class LinearAttention(nn.Module):
"""线性注意力: 复杂度O(n*d)而非O(n^2)

Attention(Q,K,V) = phi(Q) dot (phi(K)^T dot V)
"""
def __init__(self, d_model=512, n_heads=8, dropout_p=0.1, nonlinearity='elu'):
super().__init__()
assert d_model % n_heads == 0
self.n_heads = n_heads
self.d_k = d_model // n_heads

self.W_Q = nn.Linear(d_model, d_model)
self.W_K = nn.Linear(d_model, d_model)
self.W_V = nn.Linear(d_model, d_model)
self.W_O = nn.Linear(d_model, d_model)
self.dropout = nn.Dropout(dropout_p)
self.nonlinearity = nonlinearity

def _feature_map(self, x):
if self.nonlinearity == 'elu':
return F.elu(x) + 1
elif self.nonlinearity == 'relu':
return F.relu(x)
return x

def forward(self, Q, K, V, mask=None):
batch_size = Q.size(0)

q = self.W_Q(Q).view(batch_size, -1, self.n_heads, self.d_k).transpose(1, 2)
k = self.W_K(K).view(batch_size, -1, self.n_heads, self.d_k).transpose(1, 2)
v = self.W_V(V).view(batch_size, -1, self.n_heads, self.d_k).transpose(1, 2)

q = self._feature_map(q)
k = self._feature_map(k)

# K^T @ V: (batch, heads, d_k, d_k)
kv = torch.matmul(k.transpose(-2, -1), v)
# Q @ (K^T @ V): (batch, heads, seq_q, d_k)
output = torch.matmul(q, kv)

# 归一化
denominator = q.sum(dim=-1, keepdim=True).clamp(min=1e-8)
output = output / denominator

output = output.transpose(1, 2).contiguous().view(batch_size, -1, self.d_model)
return self.W_O(output)


# 测试
def test_linear_attention():
batch, seq_len, d_model = 2, 100, 64 # 更长序列
x = torch.randn(batch, seq_len, d_model)

la = LinearAttention(d_model=d_model)
out = la(x, x, x)
print(f"[7] Linear Attn: input:{x.shape} -> output:{out.shape}")
print(f" PASS")

test_linear_attention()

2.8 相对位置注意力 (Relative Position Attention)

加入相对位置信息,使模型能感知 token 之间的距离。

1
score(i,j) = Q_i dot K_j^T / sqrt(d) + B_r(i-j)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
class RelativePositionAttention(nn.Module):
"""相对位置注意力

score(i,j) = Q_i dot K_j^T / sqrt(d) + B_r(i-j)
"""
def __init__(self, d_model=512, n_heads=8, max_rel_pos=20, dropout_p=0.1):
super().__init__()
self.d_model = d_model
self.n_heads = n_heads
self.d_k = d_model // n_heads
self.max_rel_pos = max_rel_pos

self.W_Q = nn.Linear(d_model, d_model)
self.W_K = nn.Linear(d_model, d_model)
self.W_V = nn.Linear(d_model, d_model)
self.W_O = nn.Linear(d_model, d_model)
self.dropout = nn.Dropout(dropout_p)

self.rel_pos_bias = nn.Parameter(torch.randn(2 * max_rel_pos + 1, n_heads) * 0.02)

def _relative_position_index(self, seq_len):
range_q = torch.arange(seq_len)
range_k = torch.arange(seq_len)
rel_pos = range_q.unsqueeze(1) - range_k.unsqueeze(0) + self.max_rel_pos
return rel_pos

def forward(self, Q, K, V, mask=None):
batch_size = Q.size(0)
seq_len = Q.size(1)

q = self.W_Q(Q).view(batch_size, -1, self.n_heads, self.d_k).transpose(1, 2)
k = self.W_K(K).view(batch_size, -1, self.n_heads, self.d_k).transpose(1, 2)
v = self.W_V(V).view(batch_size, -1, self.n_heads, self.d_k).transpose(1, 2)

scores = torch.matmul(q, k.transpose(-2, -1)) / math.sqrt(self.d_k)

# 添加相对位置偏置
rel_idx = self._relative_position_index(seq_len).to(scores.device)
rel_bias = self.rel_pos_bias[rel_idx].permute(2, 0, 1)
rel_bias = rel_bias.unsqueeze(0).expand(batch_size, -1, -1, -1)
scores = scores + rel_bias

if mask is not None:
scores = scores.masked_fill(mask == 0, float('-inf'))

attn_w = F.softmax(scores, dim=-1)
output = torch.matmul(self.dropout(attn_w), v)
output = output.transpose(1, 2).contiguous().view(batch_size, -1, self.d_model)
return self.W_O(output)


# 测试
def test_relative_attention():
batch, seq_len, d_model = 2, 10, 64
x = torch.randn(batch, seq_len, d_model)

rpa = RelativePositionAttention(d_model=d_model, max_rel_pos=10)
out = rpa(x, x, x)
print(f"[8] Relative Pos Attn: input:{x.shape} -> output:{out.shape}")
print(f" PASS")

test_relative_attention()

2.9 注意力掩码 (Masking)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def create_masks(seq_len_q, seq_len_k, batch_size, device='cpu'):
"""创建填充掩码和因果掩码

Returns:
src_mask: (batch, 1, 1, seq_k) 填充掩码
tgt_mask: (batch, 1, seq_q, seq_q) 因果掩码
"""
src_mask = torch.ones(batch_size, 1, 1, seq_len_k, device=device)
nopeak_mask = torch.tril(torch.ones(1, seq_len_q, seq_len_q, device=device))
tgt_mask = nopeak_mask.expand(batch_size, -1, -1, -1)
return src_mask, tgt_mask


# 测试
def test_masks():
batch, seq_len = 2, 10
src_mask, tgt_mask = create_masks(seq_len, seq_len, batch)
print(f"[9] Masks: src:{src_mask.shape} tgt:{tgt_mask.shape}")
print(f" PASS")

test_masks()

2.10 完整 Transformer 注意力块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
class TransformerAttentionBlock(nn.Module):
"""完整的Transformer注意力块 (含残差+LayerNorm)"""
def __init__(self, d_model=512, n_heads=8, dropout_p=0.1, ffn_dim=2048):
super().__init__()

# 多头注意力
self.self_attn = nn.MultiheadAttention(
embed_dim=d_model, num_heads=n_heads,
dropout=dropout_p, batch_first=True
)

# LayerNorm
self.norm1 = nn.LayerNorm(d_model)
self.norm2 = nn.LayerNorm(d_model)

# 前馈网络 (FFN)
self.ffn = nn.Sequential(
nn.Linear(d_model, ffn_dim),
nn.ReLU(),
nn.Linear(ffn_dim, d_model),
nn.Dropout(dropout_p)
)

self.dropout = nn.Dropout(dropout_p)

def forward(self, x, mask=None):
"""
Args:
x: (batch, seq_len, d_model)
mask: (batch, 1, 1, seq_len) 或 (batch, 1, seq_len, seq_len)
Returns:
output: (batch, seq_len, d_model)
"""
# Self-attention + 残差 + LayerNorm
attn_out, _ = self.self_attn(x, x, x, key_padding_mask=mask)
x = x + self.dropout(attn_out)
x = self.norm1(x)

# FFN + 残差 + LayerNorm
ffn_out = self.ffn(x)
x = x + ffn_out
x = self.norm2(x)

return x


# 测试
def test_transformer_block():
batch, seq_len, d_model = 2, 10, 64
x = torch.randn(batch, seq_len, d_model)

block = TransformerAttentionBlock(d_model=d_model)
out = block(x)
print(f"[10] Transformer Block: input:{x.shape} -> output:{out.shape}")
print(f" PASS")

test_transformer_block()

2.11 因果掩码注意力 (Decoder用)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
class CausalMultiHeadAttention(nn.Module):
"""带因果掩码的多头注意力 (用于Decoder)

确保每个位置只能看到它之前的token
"""
def __init__(self, d_model=512, n_heads=8, dropout_p=0.1):
super().__init__()
self.d_model = d_model
self.n_heads = n_heads
self.d_k = d_model // n_heads

self.W_Q = nn.Linear(d_model, d_model)
self.W_K = nn.Linear(d_model, d_model)
self.W_V = nn.Linear(d_model, d_model)
self.W_O = nn.Linear(d_model, d_model)
self.dropout = nn.Dropout(dropout_p)

def forward(self, x, mask=None):
batch_size, seq_len = x.size(0), x.size(1)

Q = self.W_Q(x)
K = self.W_K(x)
V = self.W_V(x)

Q = Q.view(batch_size, -1, self.n_heads, self.d_k).transpose(1, 2)
K = K.view(batch_size, -1, self.n_heads, self.d_k).transpose(1, 2)
V = V.view(batch_size, -1, self.n_heads, self.d_k).transpose(1, 2)

scores = torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(self.d_k)

# 因果掩码: 下三角
causal_mask = torch.tril(torch.ones(seq_len, seq_len, device=x.device)).unsqueeze(0).unsqueeze(0)
scores = scores.masked_fill(causal_mask == 0, float('-inf'))

# 额外掩码
if mask is not None:
scores = scores.masked_fill(mask == 0, float('-inf'))

attn_w = F.softmax(scores, dim=-1)
output = torch.matmul(self.dropout(attn_w), V)

output = output.transpose(1, 2).contiguous().view(batch_size, -1, self.d_model)
return self.W_O(output), attn_w


# 测试
def test_causal_attention():
batch, seq_len, d_model = 2, 10, 64
x = torch.randn(batch, seq_len, d_model)

causal_attn = CausalMultiHeadAttention(d_model=d_model)
out, weights = causal_attn(x)
print(f"[11] Causal MH Attn: input:{x.shape} -> output:{out.shape} weights:{weights.shape}")
print(f" 因果掩码下三角验证: upper triangle sum = {weights[0,0,:5,5:].sum():.4f} (should be 0)")
print(f" PASS")

test_causal_attention()

三、完整对比总结

3.1 9种注意力机制对比

编号 名称 核心思想 复杂度 参数量 适用场景
1 Scaled Dot-Product 基础注意力计算 O(n²d) 无(纯计算) 所有注意力的基础
2 Multi-Head 多子空间联合关注 O(h·n²d) 4d² Transformer核心
3 Self-Attention Q=K=V同序列 O(n²d) 4d² 编码器内部
4 Cross-Attention Q≠K/V跨序列 O(n²d) 4d² 编解码器间
5 Positional Encoding 加位置信息 O(n) 0(buffer) 所有序列模型
6 GQA 分组共享KV O(n²d) 4d²/ng 大模型推理优化
7 Linear Attention 特征映射 O(nd) 4d² 超长序列
8 Relative Position 相对位置偏置 O(n²d) 4d²+n*h 视觉、T5
9 Causal Attention 下三角掩码 O(n²d) 4d² Decoder生成

3.2 Self vs Cross 对比

特性 Self-Attention Cross-Attention
Q/K/V 来源 同一序列 Q来自decoder,K/V来自encoder
输入形状 (batch, n, d) Q:(batch,n,d) K/V:(batch,m,d)
输出形状 (batch, n, d) (batch, n, d)
典型用法 Encoder层内 Decoder看Encoder输出
掩码 可选(填充) 可选(填充+因果)

3.3 MHA vs GQA vs MQA 对比

模型 KV组数 KV缓存大小 精度 推理速度
MHA n_heads 最好 基准
GQA n_groups 中等 接近MHA
MQA 1 最小 略降 最快

3.4 参数量对比 (d_model=512, n_heads=8)

模型 参数量 说明
Scaled Dot-Product 0 纯计算,无参数
Multi-Head ~4M 4个512×512线性层
GQA (4 groups) ~2M W_K/W_V缩小一半
Linear Attn ~4M 同MHA参数量
Relative Pos ~4M+64 多了相对位置偏置

3.5 选择指南

1
2
3
4
5
6
7
8
9
需要注意力机制?
├── 短序列 (< 512 token) → 标准Multi-Head Attention
├── 中等序列 (512-4096) → Multi-Head + Flash Attention
├── 长序列 (> 4096) → Linear Attention 或 GQA
├── 编码器 → Self-Attention
├── 解码器 → Causal Multi-Head Attention
├── 编解码器间 → Cross-Attention
├── 需要位置信息 → 加Positional Encoding或Relative Position
└── 推理速度慢 → GQA/MQA减少KV缓存

四、注意力机制完整实现 (9种)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
import torch
import torch.nn as nn
import torch.nn.functional as F
import math
from typing import Tuple, Optional

# ============================================================
# 1. 缩放点积注意力
# ============================================================
class ScaledDotProductAttention(nn.Module):
"""Attention(Q,K,V) = softmax(QK^T / sqrt(dk)) * V"""
def __init__(self, dropout_p=0.1):
super().__init__()
self.dropout = nn.Dropout(dropout_p)

def forward(self, Q, K, V, mask=None):
d_k = Q.size(-1)
scores = torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(d_k)
if mask is not None:
scores = scores.masked_fill(mask == 0, float('-inf'))
attn_weights = self.dropout(F.softmax(scores, dim=-1))
return torch.matmul(attn_weights, V), attn_weights


# ============================================================
# 2. 多头注意力
# ============================================================
class MultiHeadAttention(nn.Module):
"""MultiHead(Q,K,V) = Concat(head_1,...,head_h) * W^O"""
def __init__(self, d_model=512, n_heads=8, dropout_p=0.1):
super().__init__()
assert d_model % n_heads == 0
self.d_model = d_model
self.n_heads = n_heads
self.d_k = d_model // n_heads
self.W_Q = nn.Linear(d_model, d_model)
self.W_K = nn.Linear(d_model, d_model)
self.W_V = nn.Linear(d_model, d_model)
self.W_O = nn.Linear(d_model, d_model)
self.attention = ScaledDotProductAttention(dropout_p)
self.dropout = nn.Dropout(dropout_p)

def forward(self, Q, K, V, mask=None):
batch_size = Q.size(0)
Q, K, V = self.W_Q(Q), self.W_K(K), self.W_V(V)
Q = Q.view(batch_size, -1, self.n_heads, self.d_k).transpose(1, 2)
K = K.view(batch_size, -1, self.n_heads, self.d_k).transpose(1, 2)
V = V.view(batch_size, -1, self.n_heads, self.d_k).transpose(1, 2)
if mask is not None:
mask = mask.unsqueeze(1).unsqueeze(1)
attn_out, attn_w = self.attention(Q, K, V, mask=mask)
attn_out = attn_out.transpose(1, 2).contiguous().view(batch_size, -1, self.d_model)
return self.dropout(self.W_O(attn_out)), attn_w


# ============================================================
# 3. 自注意力
# ============================================================
class SelfAttention(nn.Module):
"""Q=K=V,序列内部相互关注"""
def __init__(self, d_model=512, n_heads=8, dropout_p=0.1):
super().__init__()
self.mha = MultiHeadAttention(d_model, n_heads, dropout_p)
def forward(self, x, mask=None):
return self.mha(x, x, x, mask=mask)


# ============================================================
# 4. 交叉注意力
# ============================================================
class CrossAttention(nn.Module):
"""Q来自decoder, K/V来自encoder"""
def __init__(self, d_model=512, n_heads=8, dropout_p=0.1):
super().__init__()
self.mha = MultiHeadAttention(d_model, n_heads, dropout_p)
def forward(self, query, key, value, mask=None):
return self.mha(query, key, value, mask=mask)


# ============================================================
# 5. 位置编码
# ============================================================
class PositionalEncoding(nn.Module):
"""PE(pos,2i)=sin(pos/10000^(2i/d)), PE(pos,2i+1)=cos(...)"""
def __init__(self, d_model, max_len=5000, dropout_p=0.1):
super().__init__()
self.dropout = nn.Dropout(dropout_p)
pe = torch.zeros(max_len, d_model)
pos = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1)
div = torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model))
pe[:, 0::2] = torch.sin(pos * div)
pe[:, 1::2] = torch.cos(pos * div)
self.register_buffer('pe', pe.unsqueeze(0))
def forward(self, x):
return self.dropout(x + self.pe[:, :x.size(1), :])


# ============================================================
# 6. 分组查询注意力 (GQA)
# ============================================================
class GroupedQueryAttention(nn.Module):
"""GQA: 分组共享KV,GPT-3.5/Llama2使用"""
def __init__(self, d_model=512, n_heads=8, n_groups=4, dropout_p=0.1):
super().__init__()
assert n_heads % n_groups == 0
self.d_model, self.n_heads, self.n_groups = d_model, n_heads, n_groups
self.d_k = d_model // n_heads
self.W_Q = nn.Linear(d_model, d_model)
self.W_K = nn.Linear(d_model, n_groups * self.d_k)
self.W_V = nn.Linear(d_model, n_groups * self.d_k)
self.W_O = nn.Linear(d_model, d_model)
self.dropout = nn.Dropout(dropout_p)

def forward(self, Q, K, V, mask=None):
bs = Q.size(0)
q = self.W_Q(Q).view(bs, -1, self.n_heads, self.d_k).transpose(1, 2)
k = self.W_K(K).view(bs, -1, self.n_groups, self.d_k).transpose(1, 2)
v = self.W_V(V).view(bs, -1, self.n_groups, self.d_k).transpose(1, 2)
rf = self.n_heads // self.n_groups
k, v = k.repeat_interleave(rf, dim=1), v.repeat_interleave(rf, dim=1)
scores = torch.matmul(q, k.transpose(-2, -1)) / math.sqrt(self.d_k)
if mask is not None:
scores = scores.masked_fill(mask == 0, float('-inf'))
aw = F.softmax(scores, dim=-1)
out = torch.matmul(self.dropout(aw), v)
out = out.transpose(1, 2).contiguous().view(bs, -1, self.d_model)
return self.W_O(out)


# ============================================================
# 7. 线性注意力
# ============================================================
class LinearAttention(nn.Module):
"""复杂度O(n*d),Attention=phi(Q)dot(phi(K)^T dot V)"""
def __init__(self, d_model=512, n_heads=8, dropout_p=0.1, nonlinearity='elu'):
super().__init__()
assert d_model % n_heads == 0
self.d_model = d_model
self.n_heads, self.d_k = n_heads, d_model // n_heads
self.W_Q = nn.Linear(d_model, d_model)
self.W_K = nn.Linear(d_model, d_model)
self.W_V = nn.Linear(d_model, d_model)
self.W_O = nn.Linear(d_model, d_model)
self.dropout = nn.Dropout(dropout_p)
self.nonlinearity = nonlinearity

def _fm(self, x):
return F.elu(x) + 1 if self.nonlinearity == 'elu' else F.relu(x)

def forward(self, Q, K, V, mask=None):
bs = Q.size(0)
q = self._fm(self.W_Q(Q).view(bs, -1, self.n_heads, self.d_k).transpose(1, 2))
k = self._fm(self.W_K(K).view(bs, -1, self.n_heads, self.d_k).transpose(1, 2))
v = self.W_V(V).view(bs, -1, self.n_heads, self.d_k).transpose(1, 2)
kv = torch.matmul(k.transpose(-2, -1), v)
out = torch.matmul(q, kv) / q.sum(dim=-1, keepdim=True).clamp(min=1e-8)
out = out.transpose(1, 2).contiguous().view(bs, -1, self.d_model)
return self.W_O(out)


# ============================================================
# 8. 相对位置注意力
# ============================================================
class RelativePositionAttention(nn.Module):
"""score(i,j) = Q_i dot K_j^T/sqrt(d) + B_r(i-j)"""
def __init__(self, d_model=512, n_heads=8, max_rel_pos=20, dropout_p=0.1):
super().__init__()
self.d_model, self.n_heads, self.max_rel_pos = d_model, n_heads, max_rel_pos
self.d_k = d_model // n_heads
self.W_Q = nn.Linear(d_model, d_model)
self.W_K = nn.Linear(d_model, d_model)
self.W_V = nn.Linear(d_model, d_model)
self.W_O = nn.Linear(d_model, d_model)
self.dropout = nn.Dropout(dropout_p)
self.rel_pos_bias = nn.Parameter(torch.randn(2*max_rel_pos+1, n_heads) * 0.02)

def _rp_idx(self, seq_len):
return (torch.arange(seq_len).unsqueeze(1) - torch.arange(seq_len).unsqueeze(0) + self.max_rel_pos)

def forward(self, Q, K, V, mask=None):
bs, sl = Q.size(0), Q.size(1)
q = self.W_Q(Q).view(bs, -1, self.n_heads, self.d_k).transpose(1, 2)
k = self.W_K(K).view(bs, -1, self.n_heads, self.d_k).transpose(1, 2)
v = self.W_V(V).view(bs, -1, self.n_heads, self.d_k).transpose(1, 2)
scores = torch.matmul(q, k.transpose(-2, -1)) / math.sqrt(self.d_k)
rel_bias = self.rel_pos_bias[self._rp_idx(sl)].permute(2, 0, 1).unsqueeze(0).expand(bs, -1, -1, -1)
scores = scores + rel_bias
if mask is not None:
scores = scores.masked_fill(mask == 0, float('-inf'))
aw = F.softmax(scores, dim=-1)
out = torch.matmul(self.dropout(aw), v)
out = out.transpose(1, 2).contiguous().view(bs, -1, self.d_model)
return self.W_O(out)


# ============================================================
# 9. 因果掩码注意力 (Decoder用)
# ============================================================
class CausalMultiHeadAttention(nn.Module):
"""带因果掩码的多头注意力,确保每个位置只能看到之前的token"""
def __init__(self, d_model=512, n_heads=8, dropout_p=0.1):
super().__init__()
self.d_model, self.n_heads, self.d_k = d_model, n_heads, d_model // n_heads
self.W_Q = nn.Linear(d_model, d_model)
self.W_K = nn.Linear(d_model, d_model)
self.W_V = nn.Linear(d_model, d_model)
self.W_O = nn.Linear(d_model, d_model)
self.dropout = nn.Dropout(dropout_p)

def forward(self, x, mask=None):
bs, sl = x.size(0), x.size(1)
Q, K, V = self.W_Q(x), self.W_K(x), self.W_V(x)
Q = Q.view(bs, -1, self.n_heads, self.d_k).transpose(1, 2)
K = K.view(bs, -1, self.n_heads, self.d_k).transpose(1, 2)
V = V.view(bs, -1, self.n_heads, self.d_k).transpose(1, 2)
scores = torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(self.d_k)
causal = torch.tril(torch.ones(sl, sl, device=x.device)).unsqueeze(0).unsqueeze(0)
scores = scores.masked_fill(causal == 0, float('-inf'))
if mask is not None:
scores = scores.masked_fill(mask == 0, float('-inf'))
aw = F.softmax(scores, dim=-1)
out = torch.matmul(self.dropout(aw), V)
out = out.transpose(1, 2).contiguous().view(bs, -1, self.d_model)
return self.W_O(out), aw


# ============================================================
# 辅助工具
# ============================================================
def create_masks(seq_len_q, seq_len_k, batch_size, device='cpu'):
"""创建填充掩码和因果掩码"""
src_mask = torch.ones(batch_size, 1, 1, seq_len_k, device=device)
nopeak = torch.tril(torch.ones(1, seq_len_q, seq_len_q, device=device))
return src_mask, nopeak.expand(batch_size, -1, -1, -1)


class TransformerAttentionBlock(nn.Module):
"""完整Transformer块: MHA + Residual + LN + FFN + Residual + LN"""
def __init__(self, d_model=512, n_heads=8, dropout_p=0.1, ffn_dim=2048):
super().__init__()
self.self_attn = nn.MultiheadAttention(d_model, n_heads, dropout=dropout_p, batch_first=True)
self.norm1 = nn.LayerNorm(d_model)
self.norm2 = nn.LayerNorm(d_model)
self.ffn = nn.Sequential(nn.Linear(d_model, ffn_dim), nn.ReLU(),
nn.Linear(ffn_dim, d_model), nn.Dropout(dropout_p))
self.dropout = nn.Dropout(dropout_p)

def forward(self, x, mask=None):
x = x + self.dropout(self.self_attn(x, x, x, key_padding_mask=mask)[0])
x = self.norm1(x)
x = self.norm2(x + self.ffn(x))
return x


# ============================================================
# 测试
# ============================================================
def main():
batch, seq_len, d_model = 2, 10, 64
x = torch.randn(batch, seq_len, d_model)
print("=" * 60)
print("注意力机制完整实现 - 测试")
print("=" * 60)

# 1. Scaled Dot-Product
attn = ScaledDotProductAttention()
Q, K, V = torch.randn(batch, 5, d_model), torch.randn(batch, 8, d_model), torch.randn(batch, 8, d_model)
out, w = attn(Q, K, V)
print(f"[1] Scaled Dot-Product: Q:{Q.shape} K:{K.shape} V:{V.shape} -> out:{out.shape} w:{w.shape} OK")

# 2. Multi-Head
mha = MultiHeadAttention(d_model, 8)
out, w = mha(x, x, x)
print(f"[2] Multi-Head: {x.shape} -> out:{out.shape} w:{w.shape} OK")

# 3. Self-Attention
sa = SelfAttention(d_model)
out, w = sa(x)
print(f"[3] Self-Attention: {x.shape} -> out:{out.shape} w:{w.shape} OK")

# 4. Cross-Attention
ca = CrossAttention(d_model)
q = torch.randn(batch, 6, d_model)
out, w = ca(q, x, x)
print(f"[4] Cross-Attention: q:{q.shape} k/v:{x.shape} -> out:{out.shape} w:{w.shape} OK")

# 5. Positional Encoding
pe = PositionalEncoding(d_model)
enc = pe(x)
print(f"[5] Positional Encoding: {x.shape} -> {enc.shape} OK")

# 6. GQA
gqa = GroupedQueryAttention(d_model, 8, 4)
out = gqa(x, x, x)
print(f"[6] GQA: {x.shape} -> {out.shape} OK")

# 7. Linear Attention
la = LinearAttention(d_model)
out = la(x, x, x)
print(f"[7] Linear Attention: {x.shape} -> {out.shape} OK")

# 8. Relative Position
rpa = RelativePositionAttention(d_model, 8, 10)
out = rpa(x, x, x)
print(f"[8] Relative Position: {x.shape} -> {out.shape} OK")

# 9. Causal Attention
ca_attn = CausalMultiHeadAttention(d_model)
out, w = ca_attn(x)
upper_sum = w[0, 0, :5, 5:].sum()
print(f"[9] Causal Attention: {x.shape} -> out:{out.shape} w:{w.shape} upper_tri:{upper_sum:.4f}(should=0) OK")

# Masks
src_m, tgt_m = create_masks(seq_len, seq_len, batch)
print(f" Masks: src:{src_m.shape} tgt:{tgt_m.shape} OK")

# Transformer Block
block = TransformerAttentionBlock(d_model)
out = block(x)
print(f" Transformer Block: {x.shape} -> {out.shape} OK")

# 参数量对比
print("\n" + "=" * 60)
print("参数量对比 (d_model=512, n_heads=8)")
print("=" * 60)
for name, model_cls, kwargs in [
("Multi-Head", MultiHeadAttention, {"d_model": 512}),
("GQA (4 grp)", GroupedQueryAttention, {"d_model": 512, "n_groups": 4}),
("Linear Attn", LinearAttention, {"d_model": 512}),
("Rel Position", RelativePositionAttention, {"d_model": 512}),
("Causal MH", CausalMultiHeadAttention, {"d_model": 512}),
]:
m = model_cls(**kwargs)
p = sum(pp.numel() for pp in m.parameters())
print(f" {name:16s}: {p:>10,} params")

print("\n" + "=" * 60)
print("ALL TESTS PASSED!")
print("=" * 60)


if __name__ == "__main__":
main()