注意力机制与 Transformer 架构
第 2 章搭好的神经网络已经就位:它有了嵌入表和可训练的权重。但一个先天缺陷还没解决——嵌入表是一词一向量,静态不变的。"苹果发布了新手机"和"我吃了个苹果"里,"苹果"查的是嵌入表的同一行,含义却天差地别。本章讲 Tr…
第 2 章搭好的神经网络已经就位:它有了嵌入表和可训练的权重。但一个先天缺陷还没解决——嵌入表是一词一向量,静态不变的。"苹果发布了新手机"和"我吃了个苹果"里,"苹果"查的是嵌入表的同一行,含义却天差地别。本章讲 Transformer 的核心突破——让词与词互相看见:每个 token 根据上下文吸收信息、动态修正自己的含义,这就是注意力机制(Attention),Transformer 名字里的 T 就是它。我们先拆透这台发动机,再装上多头、前馈、残差、位置编码这些传动和底盘,拼出一张完整的大模型架构图。读完这章,GPT、Claude、Qwen、DeepSeek 在你眼里不再是黑盒,你还能顺手回答一个高频面试题:为什么主流大模型清一色是 decoder-only 结构。
1. 问题:静态向量不够用
第 1 章的嵌入表有一个先天缺陷:一词一向量,静态不变。但语言的精妙恰恰在于语境:
- "苹果发布了新手机" → 公司
- "我吃了半个苹果" → 水果
- "她的脸颊红得像苹果" → 比喻
理解"苹果"需要看它周围的词。所以在进入深度网络之前,必须有一个机制让每个 token 的向量融合上下文信息、按需更新自己。
早期方案(RNN 时代)是让信息沿着序列一步步传递,但序列一长,早期信息就被"稀释"或"遗忘"了,而且无法并行计算。2017 年《Attention Is All You Need》给出了激进的答案:干脆只用注意力,扔掉循环结构。
2. 直觉:一场信息撮合会
把一句话里的 token 想象成一场交流会的成员。每个成员(比如"发布")想弄清楚自己该吸收谁的信息,于是发生三件事:
- 我需要什么信息? ——它提出一个"查询"(Query):比如"我在找'发布'的主语,谁是个组织?"
- 我能提供什么信息? ——每个成员亮出自己的"标签"(Key):苹果亮出"名词/可能公司/可能水果"
- 实际给什么? ——匹配上的成员交付"内容"(Value):苹果把自己当前的完整向量交出去
Query 匹配 Key 的程度,决定了 Value 被吸收的比例。Q 是问题,K 是索引,V 是内容——像极了你每天用的搜索:查询词(Q)匹配文档的标题/关键词(K),返回文档正文(V)。
3. 数学:一次软性加权平均
每个 token 的向量 $\mathbf{x}$ 通过三个不同的矩阵,投影出三个角色:
$$ \mathbf{q} = \mathbf{W}_Q \mathbf{x} \qquad \mathbf{k} = \mathbf{W}_K \mathbf{x} \qquad \mathbf{v} = \mathbf{W}_V \mathbf{x} $$
对于某个 token 的查询 $\mathbf{q}_i$,它和所有位置的 $\mathbf{k}_j$ 做点积(点积大 = 匹配强),再经过 softmax 归一化成一组和为 1 的权重,然后对所有的 $\mathbf{v}_j$ 加权平均:
$$ \text{Attention}(\mathbf{q}_i) = \sum_j \underbrace{\text{softmax}\left(\frac{\mathbf{q}_i \cdot \mathbf{k}_j}{\sqrt{d_k}}\right)}_{\text{权重:我有多关注位置 }j} \cdot \mathbf{v}_j $$
三个设计细节值得说透:
① 点积 = 匹配度。两个向量越"同向",点积越大。Q 和 K 的维度故意设计成一致,就是为了能逐对点积算匹配分。
② softmax = 软选择。权重是 0.3 / 0.6 / 0.1 这种"软"分配,不是硬性的二选一。模型可以同时从多个位置吸收信息,比例由匹配度决定。所有权重加起来恰好等于 1(softmax 的性质)。
③ 除以 $\sqrt{d_k}$ = 温度调控。维度越高,点积的数值天然越大,softmax 会被推向"一家独大"(梯度消失)。除以 $\sqrt{d_k}$ 把数值拉回合理区间。这个小动作叫 scaled dot-product attention,是为训练稳定性服务的。
矩阵形式(一次算完所有 token 的注意力):
$$ \text{Attention}(\mathbf{Q}, \mathbf{K}, \mathbf{V}) = \text{softmax}\left(\frac{\mathbf{Q}\mathbf{K}^T}{\sqrt{d_k}}\right)\mathbf{V} $$
4. 动手:手写一遍注意力
用 numpy 实现一遍,规模虽小,但和真实模型里的计算一字不差:
import numpy as np
rng = np.random.default_rng(0)
def softmax(x, axis=-1):
e = np.exp(x - x.max(axis=axis, keepdims=True)) # 减最大值防溢出
return e / e.sum(axis=axis, keepdims=True)
def attention(Q, K, V):
d_k = Q.shape[-1]
scores = Q @ K.T / np.sqrt(d_k) # (n, n) 匹配度矩阵
weights = softmax(scores) # 每行和为 1
return weights @ V, weights # 输出 + 权重(留着可视化)
# 3 个 token,向量维度 4
X = rng.normal(0, 1, (3, 4))
# 三个投影矩阵(真实模型中它们是可训练参数)
Wq, Wk, Wv = (rng.normal(0, 0.5, (4, 4)) for _ in range(3))
Q, K, V = X @ Wq, X @ Wk, X @ Wv
out, weights = attention(Q, K, V)
print("输出形状:", out.shape) # (3, 4) 每个位置一个融合后的向量
print("权重矩阵:\n", np.round(weights, 3))
print("每行权重和:", weights.sum(axis=1)) # 全是 1.0
输出的形状和输入相同:注意力不改变序列长度,它做的是"每个位置的向量,按相关性吸收了全句信息后,被重写了"。这就是从"静态词向量"到"语境化向量"的过程。
5. 因果掩码:只许看过去
聊天模型(decoder-only,见本章第 13 节)训练时要预测下一个词,必须遵守一个规则:预测第 5 个词时,不能偷看第 5 个词之后的内容。但标准注意力会让每个位置看到全句,怎么办?
掩码(mask):把"未来位置"的匹配分数设成负无穷,softmax 后权重自然变成 0。
def causal_attention(Q, K, V):
d_k = Q.shape[-1]
n = Q.shape[0]
scores = Q @ K.T / np.sqrt(d_k)
mask = np.triu(np.ones((n, n)), k=1).astype(bool) # 上三角 = 未来
scores[mask] = -np.inf # 未来不可见
weights = softmax(scores)
return weights @ V
这个上三角禁令就是"因果"二字的由来,也是生成式模型逐 token 输出(第 4 章的推理部分)的伏笔。你现在应该能理解为什么模型的输出是一字一字往外蹦的——它在每个位置都只能基于过去做决策。
6. 多头注意力:多视角并行
一个注意力头只学一种关系模式,但语言里的关系是多维的:语法关系(主谓)、指代关系(它 → 苹果)、语义关系(水果 vs 公司)……所以 Transformer 把注意力做成多头(Multi-Head):
- 并行运行 h 个独立的注意力,各自有自己的 $\mathbf{W}_Q/\mathbf{W}_K/\mathbf{W}_V$
- 每个头维度是 $d/h$(例如 4096 维 32 头,每头 128 维)
- 各头输出拼接后,再过一个线性层融合
# 概念示意:两头注意力
def multi_head(X, Wq, Wk, Wv, Wo, n_heads):
n, d = X.shape
hd = d // n_heads
# 切头:(n, d) → (n_heads, n, hd)
Q = (X @ Wq).reshape(n, n_heads, hd).transpose(1, 0, 2)
K = (X @ Wk).reshape(n, n_heads, hd).transpose(1, 0, 2)
V = (X @ Wv).reshape(n, n_heads, hd).transpose(1, 0, 2)
# 逐头注意力后拼回去
outs = [causal_attention(Q[i], K[i], V[i]) for i in range(n_heads)]
out = np.stack(outs).transpose(1, 0, 2).reshape(n, d)
return out @ Wo
一个不严谨但有用的类比:每个头是一个"专家评委",语法评委、指代评委、主题评委各自打分,最后汇总成对每个词的完整理解。
一个有趣的工程事实:各头的注意力权重模式是可以可视化的,研究者确实观察到了"专门盯主谓关系的头""专门负责相邻位置的头"。但注意:注意力权重 ≠ 模型的解释,它能提示信息流动,但不能当作模型推理依据(第 13 章会再遇到这一点)。
7. 位置编码:注意力不知道顺序
还有一个隐藏问题:注意力对所有位置一视同仁,"猫追狗"和"狗追猫"在它眼里完全相同(纯集合式交互)!而语序显然是语义的一部分。
解法是位置编码(Positional Encoding):给每个位置的向量加上(或拼接)一个"位置签名",让模型能区分"谁在谁前面"。现代主流是旋转位置编码 RoPE(把 q/k 向量按位置旋转角度),细节不影响使用,记住结论:
$$ \text{模型输入} = \text{词嵌入} + \text{位置信息} $$
Agent 开发者遇到的"长文本中间部分被忽略"(lost in the middle)现象,与位置编码的外推能力有关——这是第 13 章的上下文工程要处理的现实问题之一。
8. 一个 Transformer 层的全貌
注意力这台发动机已经造好,现在拼出整辆车。真实模型由 N 层相同的结构堆叠(GPT-3 是 96 层,Qwen 按规模 28~80 层)。每一层的流水线是:
x(本层输入)
│
┌───────▼────────┐
│ 多头注意力 │ token 之间交换信息(本章第 6 节)
└───────┬────────┘
│ ← 残差连接 ①:y = x + Attn(x)
┌───────▼────────┐
│ 层归一化 │ 数值稳定
└───────┬────────┘
│
┌───────▼────────┐
│ 前馈网络 FFN │ 每个 token 独立加工(本章第 9 节)
└───────┬────────┘
│ ← 残差连接 ②:y = y + FFN(y)
┌───────▼────────┐
│ 层归一化 │
└───────┬────────┘
▼
输出(交给下一层)
两个子层各司其职,这个分工值得记住:
- 注意力 = 横向沟通:token 之间按需交换信息("苹果"找"发布"确认自己是公司)
- 前馈网络 = 纵向深加工:每个 token 独立地消化融合来的信息,做非线性变换(可以理解为"查资料后的思考")
一个形象的类比:注意力是开会(信息同步),FFN 是回工位干活(各自消化)。开完会再各自干活,重复几十轮,语义逐层加深。
9. 前馈网络 FFN:两层矩阵 + 激活
FFN 结构出奇地简单——就是第 2 章讲的两层全连接,中间夹激活函数:
$$ \text{FFN}(\mathbf{x}) = \sigma(\mathbf{W}_1\mathbf{x} + \mathbf{b}_1)\mathbf{W}_2 + \mathbf{b}_2 $$
特点是其中间维度被故意放大(通常 4 倍):768 维的模型,FFN 中间层是 3072 维。直觉:先升维(展开成更丰富的特征空间),再降回来(压缩成结论)。
近年研究有一个很有意思的发现:FFN 层的行为像一张巨大的"键值记忆库"——升维后的每个中间神经元像一个记忆槽,触发条件($\mathbf{W}_1$ 的行)和存储内容($\mathbf{W}_2$ 的列)分离,模型的大量事实知识("法国的首都是巴黎")被认为主要存储在 FFN 里,而不是注意力里。这也解释了模型为什么"知道"那么多事——它们被压缩在几百亿个 FFN 参数里。
10. 残差连接:深度的生命线
每一层的输出都写成 x + 子层(x) 而不是 子层(x),这个小改动是 2015 年 ResNet 带来的革命,价值在于让"非常深"成为可能:
- 信息高速公路:输入可以无损地"直通"到深层,每层只需学"增量修正"(学 $F(x) = 目标 - x$ 比直接学目标容易得多)
- 梯度畅通:反向传播时,残差提供了一条无衰减的回传路径,深层网络不再梯度消失
一个不严谨但传神的比喻:残差连接让每层的工作变成"在传阅的文档上补充批注",而不是"重写一份新文档"。100 层下来,第一手信息仍在场。
11. 层归一化 LayerNorm
把每个 token 的向量数值调整到均值 0、方差 1 的稳定区间(再配两个可训练的缩放/平移参数)。作用类似音频设备的"自动增益控制":
- 数值不会逐层爆炸或消失
- 学习率可以设得更激进,训练更快更稳
工程上你只需要知道它存在且不可省略。现代变体(RMSNorm)只是计算更省,思想相同。
12. 动手:拼一个最小 Transformer
把前文的注意力和本节的零件组装起来(省略多头切分,保留完整骨架;真实结构就是它的放大版):
import numpy as np
rng = np.random.default_rng(0)
def softmax(x, axis=-1):
e = np.exp(x - x.max(axis=axis, keepdims=True))
return e / e.sum(axis=axis, keepdims=True)
class TinyTransformerLayer:
def __init__(self, d=16):
w = lambda *s: rng.normal(0, 0.1, s) # 初始化助手
# 注意力投影
self.Wq, self.Wk, self.Wv = w(d, d), w(d, d), w(d, d)
self.Wo = w(d, d)
# FFN:d → 4d → d
self.W1, self.W2 = w(d, 4 * d), w(4 * d, d)
self.d = d
def attention(self, X): # X: (n, d)
Q, K, V = X @ self.Wq, X @ self.Wk, X @ self.Wv
n = X.shape[0]
scores = Q @ K.T / np.sqrt(self.d)
scores[np.triu(np.ones((n, n)), 1).astype(bool)] = -np.inf # 因果掩码
return softmax(scores) @ V
def ffn(self, X):
H = np.maximum(0, X @ self.W1) # 升维 + ReLU
return H @ self.W2 # 降维
def forward(self, X):
# 子层1:注意力 + 残差
X = X + self.attention(X)
# 子层2:FFN + 残差
X = X + self.ffn(X)
return X
# 3 层小模型跑通 5 个 token
d = 16
layers = [TinyTransformerLayer(d) for _ in range(3)]
tokens = rng.normal(0, 1, (5, d)) # 假装这是嵌入后的向量(第 1 章)
h = tokens
for layer in layers:
h = layer.forward(h) # 形状始终 (5, 16)
print("输出形状:", h.shape) # (5, 16)
加上词嵌入和输出层,这就是一个完整可训练的语言模型骨架:
token 编号 → 查嵌入表(第1章) → [Transformer 层 × N] → 投影回词表 → softmax → 下一个词概率
最后一步值得单独说:顶层输出要预测词表里每个 token 作为下一个词的概率。做法是把顶层向量乘一个 (d, V) 的矩阵(很多实现直接复用嵌入表转置,叫 weight tying),得到 V 个分数,softmax 归一化。所以模型每一步的"思考成果",最终是一次对全词表的投票。
13. 为什么 decoder-only 赢了
原始 Transformer(2017)是 encoder-decoder 结构,为翻译设计。此后三年演化出三个流派,最终格局出乎很多人意料:
| 流派 | 代表 | 注意力可见范围 | 擅长 |
|---|---|---|---|
| encoder-only | BERT | 双向(看全文) | 理解类任务:分类、抽取 |
| encoder-decoder | T5、原始翻译模型 | 编码器双向 + 解码器单向 | 翻译、摘要 |
| decoder-only | GPT 系、所有主流 LLM | 单向(只看过去) | 生成;规模化后通吃 |
decoder-only 胜出的原因,工程逻辑大于算法逻辑:
- 训练任务即产品形态:预测下一个词,和聊天/生成/Agent 的使用方式天然同构——训练目标就是"持续生成"
- 规模化效率最高:因果掩码让每个位置的标签(下一个词)都免费获得监督信号,一句一万个词的语料就是一万个训练样本(第 1 章自监督的完全体)
- 工程统一:训练和推理的计算模式完全一致,K-V 缓存(第 4 章)等优化直接可用
- 涌现:规模足够大后,理解能力以 in-context learning 的形式自然涌现,"不会理解"的短板被规模补齐
对 Agent 开发者的直接启示:你调用的模型架构上是"补全机器"——它没有"回答问题"的出厂设置,回答问题是被后训练(第 4 章)塑造出的行为模式。理解这一点,你对模型"为什么有时不听话"会有全新的认识。
14. 参数都花在哪了
以本章拼出的结构盘点一个 d=4096、层数 40、词表 150K 的 7B 级模型(数量级示意):
| 部件 | 计算 | 参数量 |
|---|---|---|
| 嵌入表 | V × d = 150K × 4096 | ~0.6B |
| 注意力(每层 4 个投影) | 4 × d² | ~67M/层 |
| FFN(每层,中间 4d) | 2 × d × 4d | ~134M/层 |
| 单层合计 | ~200M | |
| 40 层 + 嵌入 + 输出层 | ~7-8B |
注意 FFN 是参数大户(约占 2/3),呼应"知识主要存在 FFN"的研究结论。下次看到"72B 模型需要 140GB 显存"(16bit 下 1 参数约 2 字节),你可以直接心算出来。
本章小结
- 注意力的使命:让每个 token 按相关性吸收上下文,把静态向量变成语境化向量
- Q/K/V 三部曲:查询(我要什么)、键(我是什么)、值(我的内容);点积算匹配,softmax 做软加权
- 缩放因子 $\sqrt{d_k}$ 是训练稳定性的关键小动作
- 因果掩码禁止看未来 → 生成模型逐 token 输出的根源
- 多头 = 多个"专家评委"并行分析不同关系模式
- 位置编码补上注意力缺失的顺序感
- 一层 Transformer = 注意力(横向沟通)+ FFN(纵向深加工),各配残差和归一化
- 残差连接是深度的生命线:每层学"增量批注",信息无损耗传递
- FFN 占参数 2/3,是模型事实知识的主要仓库
- 完整流水线:嵌入 → N 层 → 投影回词表 → softmax → 下一个词概率
- decoder-only 胜出的原因:训练任务与生成形态同构、自监督效率最高、规模涌现
- 参数量可手算:注意力 4d²/层,FFN 8d²/层,由此可推算显存需求
下一篇:04 · 预训练、RLHF 与采样(KV Cache) —— 架构有了,参数怎么来?为什么同一个基座,经过后训练后从"补全机器"变成"贴心助手"?