首页/Agent 核心/注意力机制与 Transformer 架构
Agent 核心 · 第 03 章

注意力机制与 Transformer 架构

第 2 章搭好的神经网络已经就位:它有了嵌入表和可训练的权重。但一个先天缺陷还没解决——嵌入表是一词一向量,静态不变的。"苹果发布了新手机"和"我吃了个苹果"里,"苹果"查的是嵌入表的同一行,含义却天差地别。本章讲 Tr…

约 20 分钟8,056 字6 个代码示例

第 2 章搭好的神经网络已经就位:它有了嵌入表和可训练的权重。但一个先天缺陷还没解决——嵌入表是一词一向量,静态不变的。"苹果发布了新手机"和"我吃了个苹果"里,"苹果"查的是嵌入表的同一行,含义却天差地别。本章讲 Transformer 的核心突破——让词与词互相看见:每个 token 根据上下文吸收信息、动态修正自己的含义,这就是注意力机制(Attention),Transformer 名字里的 T 就是它。我们先拆透这台发动机,再装上多头、前馈、残差、位置编码这些传动和底盘,拼出一张完整的大模型架构图。读完这章,GPT、Claude、Qwen、DeepSeek 在你眼里不再是黑盒,你还能顺手回答一个高频面试题:为什么主流大模型清一色是 decoder-only 结构。

1. 问题:静态向量不够用

第 1 章的嵌入表有一个先天缺陷:一词一向量,静态不变。但语言的精妙恰恰在于语境:

  • "苹果发布了新手机" → 公司
  • "我吃了半个苹果" → 水果
  • "她的脸颊红得像苹果" → 比喻

理解"苹果"需要看它周围的词。所以在进入深度网络之前,必须有一个机制让每个 token 的向量融合上下文信息、按需更新自己。

早期方案(RNN 时代)是让信息沿着序列一步步传递,但序列一长,早期信息就被"稀释"或"遗忘"了,而且无法并行计算。2017 年《Attention Is All You Need》给出了激进的答案:干脆只用注意力,扔掉循环结构。

2. 直觉:一场信息撮合会

把一句话里的 token 想象成一场交流会的成员。每个成员(比如"发布")想弄清楚自己该吸收谁的信息,于是发生三件事:

  1. 我需要什么信息? ——它提出一个"查询"(Query):比如"我在找'发布'的主语,谁是个组织?"
  2. 我能提供什么信息? ——每个成员亮出自己的"标签"(Key):苹果亮出"名词/可能公司/可能水果"
  3. 实际给什么? ——匹配上的成员交付"内容"(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 胜出的原因,工程逻辑大于算法逻辑:

  1. 训练任务即产品形态:预测下一个词,和聊天/生成/Agent 的使用方式天然同构——训练目标就是"持续生成"
  2. 规模化效率最高:因果掩码让每个位置的标签(下一个词)都免费获得监督信号,一句一万个词的语料就是一万个训练样本(第 1 章自监督的完全体)
  3. 工程统一:训练和推理的计算模式完全一致,K-V 缓存(第 4 章)等优化直接可用
  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) —— 架构有了,参数怎么来?为什么同一个基座,经过后训练后从"补全机器"变成"贴心助手"?