🐸 青蛙编程网
首页 / AI / Transformer 原理 / 4. 多头注意力与残差

4. 多头注意力与残差:完整的一层

这是什么:多头注意力(Multi-Head Attention) = 把向量切成几份、各算各的注意力再拼回来;残差连接(Residual) 与 LayerNorm(层归一化) = 让几十层网络算得动、又不会"越算越崩"的两个保命搭档。

为什么用:第 3 章只有"一个聚光灯",每个词只能用一套目光看全句,容易顾此失彼——多头就是几个聚光灯各盯一面(语法一盏、指代一盏、情感一盏)。而几十层往上叠时,靠残差和归一化才立得住。

如何用:本章分别手搓"切头"的维度账和"残差 + LayerNorm"的收尾两步,看清模型的"一层"由哪几块拼成。

第一步:切头——维度对半分

所谓多头,就是把 d_model 维向量平均切成 h 份,每份进一个独立的注意力。范围不受任何影响,只是"同一层能并行换几套眼镜":

import numpy as np

rng = np.random.default_rng(7)
T, d_model, h = 2, 6, 3             # 2 个词、每个词 6 维、3 个注意力头
d_head = d_model // h               # 切分维度账:每头拿走 2 维
X = rng.normal(size=(T, d_model))

heads = []
for _ in range(h):
    W = rng.normal(size=(d_model, d_head))   # 各头有各头的权重
    heads.append(X @ W)
stacked = np.stack(heads, axis=0)   # 把 3 个头摆成一摞
print(stacked.shape)                # (3, 2, 2):3 个头 × 2 词 × 2 维
# 真实现:拼回 (2, 6) 再乘一个 d_model×d_model 的投影矩阵

你会看到:

(3, 2, 2)
代码 作用
d_head = d_model // h 切分公式:6 维 ÷ 3 头 = 每头 2 维,切完总维度不变
for _ in range(h) 各头独立循环算注意力,真实实现里可以并行一块跑
np.stack(heads, axis=0) 把各头的输出摞成 (头数, 词数, 每头维度),真实实现再拼回 (2, 6)
(3, 2, 2) 维度账对上了:3 头 × 2 词 × 每头 2 维 = 12 个数 = 原来 2×6 = 12 个数

第二步:残差——把输入原样加回来

网络叠到几十层,梯度传不回去是老大难。残差连接的办法直白:每层的输出不但给新算的结果,还把输入原样加回来(输出 = 层计算 + 输入),信息永远有"原路直达"的通道:

W2 = rng.normal(size=(d_model, d_model))
hidden = X @ W2                      # 假设这就是这层注意力的输出
residual = hidden + X                # 残差:新结果 + 原输入

这一行 hidden + X 独立看没什么玄乎,妙在几十层照此叠加,每一层都能"抄近道",梯度与信息都走得动。

第三步:LayerNorm——每行拉回标准身材

残差这类加法叠加后,数值容易越滚越大。层归一化(LayerNorm)的法子:对每个词向量独立做"减均值、除标准差",把它拉回均值 0、标准差 1 的标准身材:

mean = residual.mean(axis=1, keepdims=True)
std = residual.std(axis=1, keepdims=True)
normed = (residual - mean) / std

print(np.round(normed.std(axis=1), 2))   # 归一化后每行标准差都是 1;均值也归到 0 附近

你会看到:

[1. 1.]

记忆口诀:六维切三头,各看各的世界;输出加输入,残差开近道;减均值除标准差,LayerNorm 保身材。

算完"多头注意力 →(加残差)→ LayerNorm",这才走完 Transformer 的一层。真实的 GPT 有几十层这样的一层,并且注意力层中间还夹着前馈网络(FFN);第 7 章的迷你版我们只留最关键的两步:位置编码加注意力。

动手改

  1. 把 h 改成 6,让 d_head 变成 1,看 stacked.shape 怎么变;再想想"6 个头、每头 1 维"和"1 个头、6 维"哪个更合理
  2. 在 normed 后面打印 np.round(normed.mean(axis=1), 6),验证每行均值确实在 0 附近
  3. 把 residual = hidden + X 里的 X 换成 hidden 自己,观察标准差输出怎么变化

下一章:位置编码,解决"谁在前谁在后" 🐸

📥 下载本章代码

ZIP 内含本章全部代码文件与说明,解压即用,与教程内容同步生成。