Joye Personal Blog

Back

本文是 MiniMind 学习系列的第3篇,深入解析 Attention(注意力机制)—— Transformer 的核心引擎。我们将用数据库查询的类比,让你彻底理解 Q、K、V 的含义,掌握 Multi-Head Attention 的实现,并澄清 Softmax 与 RMSNorm 的常见混淆。

关于本系列#

MiniMind 是一个简洁但完整的大语言模型训练项目,包含从数据处理、模型训练到推理部署的完整流程。我在学习这个项目的过程中,将核心技术点整理成了 minimind-notes 仓库,并产出了这个4篇系列博客,系统性地讲解 Transformer 的核心组件。

本系列包括:

  1. 归一化机制 - 为什么需要RMSNorm
  2. RoPE位置编码 - 如何让模型理解词序
  3. Attention机制(本篇)- Transformer的核心引擎
  4. FeedForward与完整架构 - 组件如何协同工作

一、引言#

1.1 Transformer 的灵魂#

如果说 Transformer 是一座大厦:

  • **归一化(RMSNorm)**是地基 — 稳定训练
  • **位置编码(RoPE)**是坐标系 — 区分位置
  • Attention 是核心引擎 — 理解语义 ⭐

没有 Attention,Transformer 就不存在。

1.2 本文要解答的问题#

  • Q、K、V 到底是什么?(不是玄学!)
  • 为什么要分成 8 个 Head?
  • Softmax 和 RMSNorm 有什么区别?(常见混淆)
  • Attention 如何与 RoPE 配合工作?
  • Multi-Head 的维度变化是怎样的?

1.3 适合读者#

  • 听说过 Attention 但不理解计算细节
  • 想从代码层面掌握 Multi-Head 机制
  • 准备实现自己的 Transformer
  • 对数学推导不恐惧(本文会详细解释)

二、Attention 的本质:词与词的相关性#

2.1 核心问题#

“在理解一个词时,应该关注句子中的哪些其他词?”

例子

句子: "小明很喜欢他的猫,它总是在窗边睡觉"

当模型理解"它"这个词时:
  "它" ← "小明"  相关性: 0.1  (可能性低,代词通常不指人名)
  "它" ← "喜欢"  相关性: 0.05 (几乎无关)
  "它" ← "猫"    相关性: 0.8  (高度相关!)✅
  "它" ← "窗边"  相关性: 0.05 (几乎无关)

最终理解"它"的表示 = 0.1×[小明] + 0.05×[喜欢] + 0.8×[猫] + 0.05×[窗边]
                      ≈ 主要来自"猫"的信息
plaintext

Attention 做的事情

  1. 计算相关性分数(每两个词之间)
  2. 归一化成概率分布(Softmax,加起来 = 1)
  3. 加权求和(融合上下文)

2.2 输入输出对比#

# 输入:孤立的词向量(每个词不知道上下文)
input = [
    [我的768维向量],     # 不知道后面是"爱"还是"恨"
    [爱的768维向量],     # 不知道主语是谁、宾语是谁
    [编程的768维向量]    # 不知道是被爱还是被恨
]

# Attention 处理

# 输出:融合了上下文的词向量
output = [
    [我的新向量],  # 现在知道:我在"爱"这个动作中是主语
    [爱的新向量],  # 现在知道:连接"我"和"编程"
    [编程的新向量] # 现在知道:在"爱"这个动作中是宾语
]
python

2.3 Self-Attention vs Cross-Attention#

Self-Attention(MiniMind 使用)

# 句子关注"自己内部"的词
sentence = "我爱编程"
# 计算:我 ← → 爱 ← → 编程 的相关性
python

Cross-Attention(翻译模型使用)

# 句子 A 关注句子 B
chinese = "我爱编程"
english = "I love programming"
# 计算:"我" ← "I","爱" ← "love","编程" ← "programming"
python

为什么叫”Self”

  • 因为计算的是同一个句子内部的关系
  • 不是”token 与自己”(虽然也会计算 q_i · k_i)

三、Q、K、V 详解:数据库查询类比#

3.1 经典类比:数据库查询#

理解 Q、K、V 最好的方式是类比 SQL 查询:

SELECT value          ← 返回 Value
FROM memory_bank      ← 记忆库(所有词)
WHERE key MATCHES query  ← Key 匹配 Query
sql

对应关系

SQL 概念Attention 概念作用类比
QueryQuery (Q)“我想查询什么信息?“搜索条件
KeyKey (K)“我这里有什么信息?“索引标签
ValueValue (V)“我的实际内容”数据值

3.2 具体例子:理解”爱”#

句子:“我 爱 编程”

当理解”爱”这个词时

  1. Query(“爱”想知道什么):主语和宾语是谁?我在表达什么动作?

  2. Keys(其他词提供什么信息)

    • Key(“我”) = “我是主语,第一人称代词”
    • Key(“编程”) = “我是宾语,表示活动”
  3. 计算相似度

    • “爱”的Q · “我”的K = 0.6(中等相关)
    • “爱”的Q · “编程”的K = 0.8(高度相关!)
  4. Softmax归一化[0.25, 0.15, 0.60](关注”编程”60%)

  5. 加权求和Value

    • “爱”的新表示 = 0.25×Value(“我”) + 0.15×Value(“爱”) + 0.60×Value(“编程”)
    • 融合了上下文,知道自己连接”我”和”编程”

3.3 Q、K、V 怎么得到?#

关键发现:Q、K、V 都是从同一个输入 X 通过不同权重矩阵变换得到!

# 输入 X: [3, 768](3个词,每个768维)
# 权重矩阵 W_Q, W_K, W_V: [768, 768]

Q = X @ W_Q  # Query: "我想知道什么?"
K = X @ W_K  # Key: "我有什么信息?"
V = X @ W_V  # Value: "我的实际内容"
python

维度相同,含义不同。三个矩阵将输入变换成三个不同”视角”。

3.4 权重矩阵的本质#

常见疑问:“W_Q、W_K、W_V 从哪来?”

答案

  • 是什么:神经网络的可学习参数
  • 怎么来:通过训练数据反向传播学习
  • 存在哪里:保存在模型文件里(.pth, .safetensors)
  • 作用:把输入变换成三个不同”视角”

在 MiniMind 中,它们是三个 nn.Linear 层(q_proj, k_proj, v_proj)。训练后,W_Q 学会提取”查询特征”,W_K 学会提取”索引特征”,W_V 学会提取”内容特征”。


四、Attention 计算流程#

4.1 完整公式#

Attention(Q, K, V) = softmax(Q @ K^T / √d_k) @ V
plaintext

这个公式浓缩了整个 Attention 机制!

4.2 分步骤详解#

步骤 1:计算相似度(点积)

scores = Q @ K.T  # [seq_len, seq_len]
# scores[i, j] = Q[i] · K[j](两个向量越相似,点积越大)
python

步骤 2:缩放(除以 √d_k)

scaled_scores = scores / math.sqrt(head_dim)
python

为什么缩放?维度越大,点积越大。不缩放会导致 Softmax 太”尖锐”,梯度消失。缩放后分布更平滑,梯度更稳定。

步骤 3:Softmax 归一化

attn_weights = softmax(scaled_scores, dim=-1)
python

转换成概率分布:所有权重≥0,每一行加起来=1,可以解释为”关注度”。

步骤 4:加权求和 Value

output = attn_weights @ V
python

例如:“爱”的新表示 = 0.29×Value(“我”) + 0.36×Value(“爱”) + 0.25×Value(“编程”)

4.3 完整代码实现#

def attention(Q, K, V, mask=None):
    head_dim = Q.shape[-1]

    # 1-2. 计算相似度并缩放
    scores = (Q @ K.transpose(-2, -1)) / math.sqrt(head_dim)

    # 3. 应用掩码(可选,用于因果注意力)
    if mask is not None:
        scores = scores.masked_fill(mask == 0, -1e9)

    # 4-5. Softmax + 加权求和
    attn_weights = F.softmax(scores, dim=-1)
    output = attn_weights @ V

    return output, attn_weights
python

五、Multi-Head Attention#

5.1 为什么需要多头?#

单头的局限:只能关注一个方面。

句子:“小明在北京的清华大学学习人工智能”

单头 Attention 可能只关注

  • 主谓宾关系(语法)

但我们希望同时关注

  • 语法结构(主谓宾)
  • 实体关系(小明-清华)
  • 地理位置(清华-北京)
  • 主题领域(人工智能)
  • 语义相关(学习-人工智能)

解决方案:Multi-Head Attention!

5.2 “多副眼镜”的类比#

Head 1: 语法眼镜 👓
  → 关注主谓宾关系、句法结构

Head 2: 实体眼镜 🕶️
  → 关注人名、地名、机构名

Head 3: 语义眼镜 👓
  → 关注同义词、相关概念

Head 4: 长距离依赖眼镜 🕶️
  → 关注距离较远但相关的词

Head 5: 情感眼镜 👓
  → 关注情感词、态度词

...

Head 8: 主题眼镜 🕶️
  → 关注主题和领域词汇

最后:摘下所有眼镜,融合 8 个视角!
plaintext

5.3 Multi-Head 的实现流程#

# MiniMind 配置
hidden_size = 768
num_heads = 8
head_dim = hidden_size // num_heads = 96

# 完整流程
输入 X: [batch, seq_len, 768]

生成 Q, K, V: [batch, seq_len, 768]

拆分成 8 个头: [batch, seq_len, 8, 96]

转置: [batch, 8, seq_len, 96]  # 方便并行计算

每个头独立计算 Attention(并行)

输出: [batch, 8, seq_len, 96]

转回: [batch, seq_len, 8, 96]

合并(reshape): [batch, seq_len, 768]

输出投影: [batch, seq_len, 768]
python

5.4 代码实现#

class MultiHeadAttention(nn.Module):
    def __init__(self, hidden_size=768, num_heads=8):
        super().__init__()
        self.num_heads = num_heads
        self.head_dim = hidden_size // num_heads  # 96

        self.q_proj = nn.Linear(hidden_size, hidden_size, bias=False)
        self.k_proj = nn.Linear(hidden_size, hidden_size, bias=False)
        self.v_proj = nn.Linear(hidden_size, hidden_size, bias=False)
        self.o_proj = nn.Linear(hidden_size, hidden_size, bias=False)

    def forward(self, x, mask=None):
        batch, seq_len, _ = x.shape

        # 1. 生成Q、K、V并拆分成多头
        Q = self.q_proj(x).view(batch, seq_len, self.num_heads, self.head_dim).transpose(1, 2)
        K = self.k_proj(x).view(batch, seq_len, self.num_heads, self.head_dim).transpose(1, 2)
        V = self.v_proj(x).view(batch, seq_len, self.num_heads, self.head_dim).transpose(1, 2)
        # [batch, num_heads, seq_len, head_dim]

        # 2. 计算Attention(8个头并行)
        scores = (Q @ K.transpose(-2, -1)) / math.sqrt(self.head_dim)
        attn_weights = F.softmax(scores, dim=-1)
        output = attn_weights @ V

        # 3. 合并多头并输出投影
        output = output.transpose(1, 2).contiguous().view(batch, seq_len, -1)
        return self.o_proj(output)
python

5.5 维度追踪#

输入: [batch, seq_len, 768]
  → Q、K、V: [batch, seq_len, 768]
  → 拆分+转置: [batch, 8, seq_len, 96]
  → Attention: [batch, 8, seq_len, 96]
  → 合并: [batch, seq_len, 768]
  → 输出投影: [batch, seq_len, 768]

关键不变量:num_heads × head_dim = 768
plaintext

5.6 为什么拆分后要拼接?#

拆分:让每个头专注不同方面

# Head 1 学会关注语法
# Head 2 学会关注实体
# ...
python

拼接:融合所有视角的信息

# 类比:8 个专家分别分析同一个案例
# 每个专家写一份 96 字的报告
# 最后拼成一份 768 字的综合报告
python

为什么不是简单平均?

  • 拼接保留了所有信息(768 维)
  • 平均会丢失信息(还是 96 维)
  • 后续的 FFN 可以学习如何融合这些信息

六、常见混淆:Softmax vs RMSNorm#

6.1 很多人的疑问#

“Attention 里的 Softmax 和 Transformer Block 里的 RMSNorm 都是归一化,有什么区别?”

这是个常见的混淆!

6.2 核心区别#

特性Softmax(Attention 内部)RMSNorm(Block 之间)
位置Attention 计算内部Attention/FFN 之前
归一化对象相似度分数(分数矩阵的每一行)词向量(每个向量的大小)
目的变成概率分布稳定数值,防止梯度爆炸
输入任意分数(-∞ 到 +∞)768 维向量
输出0-1 之间,和为 1归一化向量(方向不变)
公式exp(x_i) / Σexp(x_j)x / sqrt(mean(x²))
作用范围每一行独立归一化每个向量独立归一化

6.3 在代码中的位置#

# Transformer Block
def forward(self, x):
    # ========== RMSNorm ==========
    residual = x
    x = self.input_norm(x)  # ← RMSNorm:归一化词向量

    # ========== Attention 内部 ==========
    Q, K, V = self.q_proj(x), self.k_proj(x), self.v_proj(x)

    # 拆分多头...

    scores = Q @ K.T
    weights = F.softmax(scores, dim=-1)  # ← Softmax:归一化分数
    output = weights @ V

    # ==========  残差连接 ==========
    x = residual + output

    return x
python

6.4 详细对比示例#

Softmax 示例

# Attention 分数矩阵的一行
scores = torch.tensor([2.5, 1.3, 3.7, 0.8])

# Softmax 归一化
weights = F.softmax(scores, dim=-1)
print(weights)
# 输出: tensor([0.1722, 0.0518, 0.5678, 0.0082])
# 特点:
# - 所有值在 [0, 1]
# - 加起来 = 1
# - 大的更大(3.7 → 0.5678,占56.78%)
python

RMSNorm 示例

# 一个词向量
x = torch.tensor([2.5, 1.3, 3.7, 0.8])

# RMSNorm 归一化
rms = torch.sqrt((x ** 2).mean())
x_norm = x / rms
print(x_norm)
# 输出: tensor([1.0698, 0.5563, 1.5833, 0.3424])
# 特点:
# - 值可以是任意正负数
# - RMS ≈ 1
# - 方向不变(只缩放大小)
python

6.5 记忆口诀#

Softmax: 归一化"分数分布" → 变成概率权重
RMSNorm: 归一化"向量大小" → 稳定训练

完全不同的归一化!
位置不同,用途不同,公式不同!
plaintext

七、RoPE 在 Attention 中的应用#

7.1 应用位置#

RoPE 在生成 Q、K 之后,计算 Attention 之前施加:

def forward(self, x, position_embeddings):
    # 1. 生成 Q、K、V
    Q = self.q_proj(x)
    K = self.k_proj(x)
    V = self.v_proj(x)

    # 2. 拆分多头
    Q = Q.view(batch, seq_len, num_heads, head_dim)
    K = K.view(batch, seq_len, num_heads, head_dim)
    V = V.view(batch, seq_len, num_heads, head_dim)

    # 3. 转置
    Q = Q.transpose(1, 2)  # [batch, num_heads, seq_len, head_dim]
    K = K.transpose(1, 2)

    # 4. ⭐ 应用 RoPE(只对 Q、K)
    cos, sin = position_embeddings
    Q, K = apply_rotary_pos_emb(Q, K, cos, sin)

    # 5. 计算 Attention
    scores = Q @ K.transpose(-2, -1) / sqrt(head_dim)
    attn = softmax(scores, dim=-1)
    output = attn @ V

    return output
python

7.2 为什么只旋转 Q 和 K?#

回顾前面提到的内容:

原因

  • Q 和 K 计算相似度 → 需要位置信息
  • V 表示内容 → 不需要位置信息

流程

1. scores = Q @ K.T  ← 计算相似度(需要位置)
2. weights = softmax(scores)
3. output = weights @ V  ← 加权求和内容(不需要位置)
plaintext

类比

  • Q、K 是”地图坐标” → 需要 RoPE
  • V 是”宝藏内容” → 不需要 RoPE

八、动手实验#

完整的学习材料已开源,你可以自己运行验证:

# 克隆代码
git clone https://github.com/joyehuang/minimind-notes
cd minimind-notes/learning_materials

# 实验1:Q、K、V 基础原理
python attention_qkv_explained.py

# 实验2:Multi-Head Attention 实现
python multihead_attention.py

# 实验3:Softmax vs RMSNorm 对比
python softmax_vs_rmsnorm.py
bash

九、总结#

9.1 核心要点#

  • Attention 的本质:计算词与词的相关性,融合上下文
  • Q、K、V:数据库查询类比,不是玄学
  • 权重矩阵:训练学习的参数,存在模型文件里
  • 4 步流程:相似度 → 缩放 → Softmax → 加权求和
  • Multi-Head:8 副眼镜看同一句话,融合多个视角
  • Softmax ≠ RMSNorm:完全不同的归一化,位置和用途都不同
  • RoPE 只用于 Q、K:相似度需要位置,内容不需要

9.2 Attention 的 4 步流程(记忆)#

1. Q @ K.T          → 计算相似度
2. / √d             → 缩放
3. softmax(...)     → 归一化成概率
4. @ V              → 加权求和
plaintext

9.3 Multi-Head 的维度变化(记忆)#

[batch, seq, 768]
  → 生成 Q、K、V
  → 拆分成 8 头: [batch, seq, 8, 96]
  → 转置: [batch, 8, seq, 96]
  → Attention(并行)
  → 转回: [batch, seq, 8, 96]
  → 合并: [batch, seq, 768]
plaintext

9.4 关键代码位置(MiniMind)#

  • Attention 实现:model/model_minimind.py:140-220
  • Q、K、V 投影:model/model_minimind.py:159-161
  • RoPE 应用:model/model_minimind.py:182
  • 学习示例:learning_materials/attention_qkv_explained.py

9.5 延伸思考#

  1. GQA (Grouped Query Attention)

    • MiniMind 使用 GQA(num_key_value_heads=2
    • 节省显存,加快推理
  2. Flash Attention

    • 优化 Attention 的计算和显存访问
    • 训练速度提升 2-3 倍
  3. Sparse Attention

    • 不是所有词都要关注所有词
    • 长文本场景的优化

十、参考资料#

论文

代码

系列其他文章


本文作者:joye 发布日期:2025-12-29 最后更新:2025-12-29 系列文章:MiniMind 学习笔记(3/4)

如果觉得有帮助,欢迎:

  • ⭐ Star 原项目 MiniMind
  • ⭐ Star 我的学习笔记 minimind-notes
  • 💬 留言讨论你的学习心得
  • 🔗 分享给其他学习 LLM 的朋友
深入理解Attention机制:从Q、K、V到Multi-Head
https://astro-pure.js.org/blog/20251218---attention-mechanism/post
Author Joye
Published at 2025年12月18日
Comment seems to stuck. Try to refresh?✨