LLM-02进阶16 min

Attention 机制:Q/K/V 矩阵运算的直观理解

Self-Attention 是 Transformer 的心脏,但大多数教程只给公式不给直觉。本文用矩阵运算的视角,配合 Python 代码,让你真正理解"每个 token 是怎么看到其他 token"的。

AttentionQKV矩阵Transformer代码实现

为什么 Attention值得单独理解

Self-Attention 是 Transformer 架构的核心,也是 GPT、BERT 等主流模型的基础。但大多数人学 Attention 时,止步于"查询(Query)、键(Key)、值(Value)"这三个名字,然后被矩阵乘法的公式劝退。

本文的目标是让你建立真正的直觉:Attention 本质上是在做什么,为什么这样做,以及在跨境电商场景里它能帮你做什么。

从"查字典"理解 Q/K/V

Q、K、V是什么

每个输入 token有一个原始向量(embedding)。Attention 层的做法是:用三个独立的线性投影,把同一个向量变成三个不同的向量——Query、Key、Value。

想象你在图书馆:Query 是你要找的信息(比如"运动鞋"),Key 是每本书的索引卡,Value 是书的实际内容。Attention 做的是:拿 Query 和每张 Key 卡做匹配,找到最相关的书,然后取出它的 Value。

qkv_projection.py
import numpy as np

np.random.seed(42)

# 假设 embedding_dim = 4, hidden_dim = 4(简化)
embedding_dim = 4
hidden_dim = 4

# 三个投影矩阵 Wq, Wk, Wv(真实模型有数百万参数)
Wq = np.random.randn(embedding_dim, hidden_dim) * 0.1
Wk = np.random.randn(embedding_dim, hidden_dim) * 0.1
Wv = np.random.randn(embedding_dim, hidden_dim) * 0.1

# 两个 token 的 embedding:["运动鞋", "跑步"]
x = np.array([
    [0.8, 0.3, -0.2, 0.1],  # "运动鞋" embedding
    [0.2, 0.9, 0.1, -0.3],  # "跑步" embedding
])

# 投影得到 Q, K, V
Q = x @ Wq  # shape: (2, 4)
K = x @ Wk  # shape: (2, 4)
V = x @ Wv  # shape: (2, 4)

print(f"Q (Query):\n{Q}")
print(f"K (Key):\n{K}")
print(f"V (Value):\n{V}")
# Q 矩阵的每一行是一个 token 的 Query 向量
# K 矩阵的每一行是一个 token 的 Key 向量
# V 矩阵的每一行是一个 token 的 Value 向量

Attention Score:Query 和 Key 的匹配

下一步是计算每个 Query 对所有 Key 的相似度。公式是 Q × K^T(Q乘以 K 的转置)。结果是一个 (seq_len, seq_len) 的矩阵——每个元素 (i, j) 表示第 i 个 token 的 Query 对第 j 个 token 的 Key 的匹配程度。

相似度用点积(dot product)计算:两个向量越"对齐",点积越大。在向量空间中,如果两个向量方向一致,点积就高;如果垂直,点积接近零。

attention_score.py
import numpy as np

# 继续用上面的 Q, K
# Q, K shape: (2, 4)
# Q @ K.T shape: (2, 2)

scores = Q @ K.T  # Attention scores
print("Attention Score Matrix:")
print(scores)
print(f"shape: {scores.shape}")

# 第0行 = "运动鞋" 对 ["运动鞋", "跑步"] 的匹配分数
# 第1行 = "跑步" 对 ["运动鞋", "跑步"] 的匹配分数
# 分数越高 = 相关性越强

为什么要除以 √d

原始论文《Attention Is All You Need》对 scores 做了缩放:scores = (Q × K^T) / √d。原因是点积的值会随着维度 d 变大而变大,如果不缩放,Softmax 会把结果压到接近 one-hot 的极端分布,梯度变得极小,训练困难。

除以 √d 后,点积的量级被控制在合理范围,Softmax 输出更平滑,梯度更稳定。

scaled_dot_product.py
import numpy as np

def scaled_dot_product_attention(Q, K, V, d_k=None):
    """Scaled Dot-Product Attention"""
    d_k = Q.shape[-1] if d_k is None else d_k
    scores = Q @ K.T                     # (seq, seq)
    scaled = scores / np.sqrt(d_k)       # 缩放:防止点积过大
    attn_weights = softmax(scaled, axis=-1)  # Softmax:归一化成概率
    output = attn_weights @ V            # (seq, seq) @ (seq, d_v) = (seq, d_v)
    return output, attn_weights

def softmax(x, axis=-1):
    exp_x = np.exp(x - np.max(x, axis=axis, keepdims=True))
    return exp_x / exp_x.sum(axis=axis, keepdims=True)

# 测试:两个 token 的 Attention
output, weights = scaled_dot_product_attention(Q, K, V)
print("Attention weights (谁关注谁):")
print(np.round(weights, 3))
# 如果 weights[0, 1] 高,说明"运动鞋"很关注"跑步"
print("\n加权后的输出:")
print(np.round(output, 3))

Multi-Head 的意义:真实模型用 Multi-Head Attention——把 Q/K/V 投影到 h 个不同的低维空间,各自独立做 Attention,最后拼接。这样每个 head 可以关注不同类型的关系:语法、语义、位置、领域知识等。GPT-4 有 96 个 heads,每个 head 的维度是 128。

Softmax:分数变权重

Attention score 是一个原始分数矩阵,值可以是任意实数。Softmax 把每一行变成概率分布——所有分数归一化成加起来等于 1 的权重。每个 Query 对所有 Key 的权重加起来是 1。

这个权重的含义是:第 i 个 token 在生成表示时,应该从第 j 个 token 的 Value 向量里取多少信息。

softmax_weights.py
import numpy as np

def softmax(x, axis=-1):
    exp_x = np.exp(x - np.max(x, axis=axis, keepdims=True))
    return exp_x / exp_x.sum(axis=axis, keepdims=True)

# 模拟跨境电商产品标题的 token
tokens = ["运动鞋", "跑步", "轻便", "透气"]
seq_len = len(tokens)

# 假设 scores 已经计算好了
scores = np.array([
    [0.9, 0.4, 0.2, 0.1],  # "运动鞋" 的 Query 对各 Key 的分数
    [0.3, 0.8, 0.5, 0.6],  # "跑步" 的 Query 对各 Key 的分数
    [0.1, 0.3, 0.7, 0.2],  # "轻便" 的 Query 对各 Key 的分数
    [0.0, 0.4, 0.1, 0.8],  # "透气" 的 Query 对各 Key 的分数
])

weights = softmax(scores, axis=-1)
print("Attention weights (行归一化后):")
print(np.round(weights, 3))
# 每行加起来 = 1.0

print("\n解读:")
for i, token in enumerate(tokens):
    top_idx = np.argsort(weights[i])[::-1][:2]
    top_vals = weights[i][top_idx]
    print(f"  '{token}' 主要关注: {tokens[top_idx[0]]}({top_vals[0]:.2f}), "
          f"{tokens[top_idx[1]]}({top_vals[1]:.2f})")

Value 加权求和:信息的聚合

拿到权重矩阵后,最后一步是把所有 Value 向量按权重加权求和。对于第 i 个 token,它的输出是所有 token 的 Value 向量的加权和,权重就是 Attention weights 的第 i 行。

这就是 Self-Attention 的输出:每个 token 的新表示,是整个序列信息的动态加权聚合。"动态"是因为权重取决于 token之间的语义关系,而不是固定规则。

weighted_value_sum.py
import numpy as np

def softmax(x, axis=-1):
    exp_x = np.exp(x - np.max(x, axis=axis, keepdims=True))
    return exp_x / exp_x.sum(axis=axis, keepdims=True)

def attention(Q, K, V):
    d_k = Q.shape[-1]
    scores = Q @ K.T / np.sqrt(d_k)
    weights = softmax(scores, axis=-1)
    return weights @ V, weights

# 三个 token 的 Q, K, V
Q = np.array([[0.1, 0.2], [0.3, 0.4], [0.5, 0.6]])
K = np.array([[0.1, 0.2], [0.3, 0.4], [0.5, 0.6]])
V = np.array([[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]])

output, weights = attention(Q, K, V)
print("Attention weights:")
print(np.round(weights, 3))
print("\n加权输出 (Attention后的表示):")
print(np.round(output, 3))

# 第0个 token 的输出 = 0.53*[1,2] + 0.27*[3,4] + 0.20*[5,6]
# 如果所有 token 完全平等关注自己,weights ≈ I(单位矩阵)

跨境电商的实操影响:在电商产品标题上跑 Attention,你会发现"运动鞋"会高权重关注"跑步""减震""透气"——这些是它真正的语义邻居。如果你用 BERT 对产品标题做 Embedding,好的 Attention 权重能帮你识别标题中的核心卖点和属性关系,比简单的词频统计精确得多。

为什么 Attention 在电商场景很有用

电商数据有几个特点:产品标题短但信息密度高("Nike Air Max 270 跑步鞋男透气轻便"),用户搜索词短且模糊,评论文本长且噪声多。Attention机制能帮你解决这些问题:

语义匹配:用户搜"透气跑鞋",Attention 能让"透气"和"跑鞋"的 Query/Key 匹配上,即使标题里没有完全相同的词。这是向量检索(Embedding Search)的底层机制。

产品属性识别:用 BERT 或 GPT 对标题做 Attention 分析,可以自动识别出颜色、尺码、适用场景、品牌等属性。这是构建产品属性图谱的基础。

评论摘要:评论文本很长,直接塞进 LLM 会浪费 token。可以用 Attention 找出每个句子对"整体评价"的贡献权重,优先抽取高权重的句子做摘要,节省 token 成本。

完整 Multi-Head Attention 代码

multihead_attention.py
import numpy as np

def softmax(x, axis=-1):
    exp_x = np.exp(x - np.max(x, axis=axis, keepdims=True))
    return exp_x / exp_x.sum(axis=axis, keepdims=True)

class MultiHeadAttention:
    def __init__(self, d_model, num_heads):
        self.d_model = d_model
        self.num_heads = num_heads
        self.d_k = d_model // num_heads

        # 简化:实际是 (d_model, d_model)投影
        self.Wq = np.random.randn(d_model, d_model) * 0.1
        self.Wk = np.random.randn(d_model, d_model) * 0.1
        self.Wv = np.random.randn(d_model, d_model) * 0.1
        self.Wo = np.random.randn(d_model, d_model) * 0.1

    def split_heads(self, x, batch_size):
        """把 d_model 维分成 num_heads 个 heads"""
        x = x.reshape(batch_size, -1, self.num_heads, self.d_k)
        return x.transpose(0, 2, 1, 3)  # (batch, heads, seq, d_k)

    def forward(self, Q, K, V):
        batch_size = Q.shape[0]
        seq_len = Q.shape[1]

        # 线性投影
        Q = Q @ self.Wq  # (batch, seq, d_model)
        K = K @ self.Wk
        V = V @ self.Wv

        # 分成多个 heads
        Q = self.split_heads(Q, batch_size)
        K = self.split_heads(K, batch_size)
        V = self.split_heads(V, batch_size)

        # Scaled dot-product attention per head
        d_k = Q.shape[-1]
        scores = Q @ K.transpose(0, 1, 3, 2) / np.sqrt(d_k)
        attn_weights = softmax(scores, axis=-1)
        context = attn_weights @ V

        # 合并 heads: (batch, heads, seq, d_k) -> (batch, seq, d_model)
        context = context.transpose(0, 2, 1, 3).reshape(batch_size, seq_len, self.d_model)

        return context @ self.Wo, attn_weights

# 测试
mha = MultiHeadAttention(d_model=8, num_heads=2)
x = np.random.randn(1, 3, 8)  # batch=1, seq=3, d_model=8
out, weights = mha.forward(x, x, x)
print(f"输入: {x.shape}")
print(f"输出: {out.shape}")  # (1, 3, 8)

关键结论

·Attention 本质是"软性查找":每个 token 从整个序列中按相关性加权提取信息
· Q/K/V 是三个投影:把同一个 embedding 变成查询、索引、内容三个向量
· Softmax 把分数变概率:归一化后每个 Query 对所有 Key 的权重和为 1
· Multi-Head 让模型同时关注多种关系:不同 head 可以捕捉语法、语义、领域等不同维度的依赖
· 实操价值:理解 Attention 才能理解向量检索原理、构建好的 RAG pipeline、优化 token 用量

理解了 Attention,下一步就是理解 Embedding:这些高维向量空间到底是怎么让语义"可计算"的。