Transformer 从零讲透:自注意力到底在算什么

副标题:不背公式、不绕弯子,用「舞台聚光灯」和「工位同事」两个比喻,把自注意力、多头、位置编码、残差、LayerNorm、FFN 一次看明白——附调库版 & 面试手搓版代码

你写模型、调 GPT、刷 BERT 时,一定听过一句话:「Transformer 是大模型的基石」。但真要你讲清楚「自注意力(Self-Attention)到底在算什么」,是不是卡在了那行公式上:

A t t e n t i o n ( Q , K , V ) = s o f t m a x ( Q K T d k ) V Attention(Q, K, V) = softmax\left(\frac{QK^T}{\sqrt{d_k}}\right)V Attention(Q,K,V)=softmax(dk QKT)V

别慌,这几乎是所有人的必经之痛。前面我们讲过 Tensor(第 43 篇)、nn.Module(第 50 篇)、优化器与正则化(第 52~57 篇)——那些是「零件」。而 Transformer,是把这些零件组装成一种全新信息流动方式的范式。

这一篇,我们就把这套机制彻底拆开。我会用两个贯穿全程的比喻——舞台聚光灯工位同事——串起所有概念。


总览:先记住两句话,全文就通了

你大概听说过:Transformer 是 BERT、GPT、ChatGPT 的共同骨架。它凭什么这么强?一句话——

👉 它让一句话里「任意两个词」都能直接对话,而且所有词是同时算、可以并行的。

为了让你后面不迷路,先把整篇的「贯穿比喻」立起来:

💡 比喻 A · 舞台聚光灯:一句话是一支队伍上台。每个词头顶有一束聚光灯,灯光照向谁,谁就被「重点关注」。自注意力,就是大家一起决定:我的灯该打在谁身上

💡 比喻 B · 工位同事:每个词是办公室里一个同事。开会时,每个人同时向全场发问、也听全场回答。谁的问题和你对口,你就多听谁的——这就是「加权融合」。

文章里每个图解,都是把上面这两个比喻「画」出来给你看。下面正式开始。


一、为什么是 Transformer:RNN 的两个老毛病

在 Transformer 之前,处理「一句话 / 一段序列」的主力是 RNN(循环神经网络)。它像一条流水线:看完第 1 个词,才看第 2 个,再看第 3 个……时间步必须 串行

🕳️ 毛病① 算得慢(不能并行):第 2 个词必须等第 1 个词算完。句子越长,排队越久,GPU 的并行算力被白白浪费。

🕳️ 毛病② 记性差(长程依赖丢失):信息要从第 1 个词一路「传递」到第 100 个词,每传一步都打点折扣,传到远处就梯度消失、记不住开头了。所以 RNN 很难理解「一句话开头和结尾的呼应」。

Transformer 的解法是「换思路」:不让词排队,而是让所有词同时坐在一屋子里,两两直接对话。于是:

维度RNN(旧)Transformer(新)
计算方式串行,必须等上一时间步并行,所有位置同时算
长程依赖远处易梯度消失,记不住任意两位置直连,一步可达
复杂度O(n),随长度线性O(n²),随长度平方(吃显存)
位置信息天然按顺序输入无序,需额外加位置编码
现状渐被取代GPT / BERT / 大模型基石

RNN vs Transformer 架构对比

💡 一句话记忆:Transformer 用「平方的算力」换「并行 + 长程直连」,在长序列上完胜 RNN。序列特别长时 O(n²) 成为瓶颈 → 催生 FlashAttention、稀疏注意力等优化。


二、自注意力:一句话的「聚光灯」

核心公式就一行,但每个符号都能翻译成大白话:

A t t e n t i o n ( Q , K , V ) = s o f t m a x ( Q K T d k ) ⏟ 该关注谁 ⋅ V Attention(Q, K, V) = \underbrace{softmax\left(\frac{QK^T}{\sqrt{d_k}}\right)}_{\text{该关注谁}} \cdot V Attention(Q,K,V)=该关注谁 softmax(dk QKT)V

拆成两步就懂:

  1. 第一步 · 打分:每个词拿自己的「提问」去和别人的「标签」比对,算出该关注谁多少(就是中间那个 Q K T QK^T QKT)。
  2. 第二步 · 融合:把分数变成权重(每行加起来 = 1),再按权重把大家的「内容」搅和在一起(最后那个 ⋅ V \cdot V V)。

自注意力四步流程:打分→缩放→Softmax→加权求和

💡 聚光灯版解释:每个词头顶一束灯(Query),灯指向谁,谁就被照亮、被「吸收」进自己的理解里。灯越亮 = 关注越多。上面这张图里,气泡越大 = 被关注越多

🎮 👉 点击在线体验:拖动聚光灯看注意力如何分配

聚光灯注意力交互演示

⚠️ 此刻只是「算出该关注谁」,还没真正改变任何词。 改变发生在第二步融合。


三、Q / K / V:每个词的「三张角色卡」

Q、K、V 都来自同一个输入,只是各乘了一个不同的线性矩阵:

KaTeX parse error: Expected 'EOF', got '#' at position 40: …_Q \quad \text{#̲ Query:我想找什么样的上…

Q/K/V 三张角色卡:同一输入 × 三个矩阵 = 三种角色

💡 工位同事版:开会时,每个人都同时拿着三张卡——

  • Q(提问卡):我现在想搞懂什么?
  • K(名牌卡):我擅长回答哪类问题?
  • V(资料卡):我被问到时,掏出什么真东西给你?

💡 为什么要拆三份:「用来匹配的特征」和「用来传递的内容」往往不同。比如代词「它」的 Q 想找一个「动物名词」,而它真正要搬运的 V 是那个名词的具体含义——拆开 K 和 V,让「找人」和「给料」两件事解耦,表达力更强。

🎮 👉 点击在线体验:调整 Wq/Wk/Wv 看三张卡如何变化

QKV 角色卡交互演示

四、缩放 √d_k:给「吵闹的打分」降噪

为什么点积要除以 d k \sqrt{d_k} dk ?一句话:防止分数方差爆炸,Softmax 变「独热」导致梯度消失。

Q 和 K 每个维度里的值,都是随机初始化、近似独立的小数字。两个 d k d_k dk 维向量做点积,相当于把 d k d_k dk 个这样的小乘积加起来

s c o r e = q ⋅ k = ∣ q ∣ ⋅ ∣ k ∣ ⋅ cos ⁡ θ score = q \cdot k = |q| \cdot |k| \cdot \cos\theta score=qk=qkcosθ

维度 d k d_k dk 越高,点积的方差越大(≈ d k d_k dk 倍)。不处理的话,分数会极端到 ±很大,送进 Softmax 后几乎变成「只选一个、其余全 0」的 one-hot,梯度直接消失,模型学不动

💡 除以 √d_k 做了什么:把方差从 ≈ d k d_k dk 拉回到 ≈1,分数分布变得「温和」,Softmax 才会给多个词都分一点注意力,梯度也健康。这也就是名字 Scaled Dot-Product Attention(缩放点积注意力) 的由来。

💡 面试高频追问:「为什么除 √d_k 而不是别的?」→ 因为要让缩放后点积的方差稳定在 1,√d_k 是方差开根号恰好抵消的那一档。答到「方差稳定」四个字就对了。


五、注意力矩阵:整句的「关注全景」

把每个词当 Query 打一遍分,排成矩阵,再按行做 Softmax:

A = s o f t m a x ( Q K T d k ) , s o f t m a x ( z ) j = e z j / τ ∑ k e z k / τ A = softmax\left(\frac{QK^T}{\sqrt{d_k}}\right), \quad softmax(z)_j = \frac{e^{z_j/\tau}}{\sum_k e^{z_k/\tau}} A=softmax(dk QKT),softmax(z)j=kezk/τezj/τ

以「猫 坐 在 垫子 上」为例,注意力矩阵长这样(每行和为 1,数值越大 = 越关注):

         猫    坐    在   垫子   上     ← Key(被看)
猫    [ 0.50][0.10][0.10][0.20][0.10]
坐    [ 0.30][0.40][0.10][0.10][0.10]
在    [ 0.10][0.10][0.30][0.30][0.20]
垫子  [ 0.40][0.10][0.10][0.30][0.10]
上    [ 0.20][0.10][0.10][0.40][0.20]
  ↑ 每个词当一次 Query(在看)

「猫坐在垫子上」的注意力权重热力图

💡 聚光灯全景版:这就是「谁在看谁」的全局地图。亮带通常对应语法/指代关系,比如「它→猫」「动词→主语」。

💡 温度 τ 的作用:τ→0,权重集中到最相关的那个词(硬选择);τ→∞,权重趋于均匀(平均一切)。训练时 τ 固定为 1(已含在 √d_k 里);推理采样时调它能控制「专注 vs 发散」。

🎮 👉 点击在线体验:调节温度 τ 看注意力分布变化

注意力矩阵交互演示

六、多头注意力:多组聚光灯,各看一面

d d d 维切成 h h h 个低维子空间,每个头独立做一遍注意力,结果拼接后用 W O W_O WO 融合:

h e a d t = A t t e n t i o n ( X W Q t , X W K t , X W V t ) M u l t i H e a d = C o n c a t ( h e a d 1 , … , h e a d h ) ⋅ W O \begin{aligned} head_t &= Attention(XW_Q^t, XW_K^t, XW_V^t)\\ MultiHead &= Concat(head_1, \dots, head_h) \cdot W_O \end{aligned} headtMultiHead=Attention(XWQt,XWKt,XWVt)=Concat(head1,,headh)WO

多头注意力:h 组聚光灯各照一个角度

💡 多组聚光灯版:一个头像一盏聚光灯,只能照一个角度。多头 = 同时打 h 盏不同颜色的灯:有的专照「主谓一致」,有的专照「代词指代」,有的专照「相邻距离」。最后 W O W_O WO 把各盏灯的发现重新混回完整表示。

💡 为什么不是一个大头:单一空间里,「语法对齐」和「语义相关」会互相干扰、挤在同一组夹角里。拆成多头,让每种关系拥有独立子空间、互不打架——实践里这显著提升表达力。

🎮 👉 点击在线体验:切换头数 h 看多头效果

多头注意力交互演示

七、位置编码:给每个词贴「座位号」

正弦编码给每个维度安排一个不同频率:低维转得慢(管粗略位置),高维转得快(管精细位置)——像钟表的时针/分针/粗细分针叠加,组合出独一无二的位置指纹。

KaTeX parse error: Expected 'EOF', got '#' at position 180: …i) \quad \text{#̲ 把座位号加到词向量上} \e…

💡 座位号版:注意力是「对称」的——它只看词与词的内容关系,天然分不清先后。位置编码就是给每个人发一张座位牌,贴进向量里,模型这才知道「谁坐在第几个」。

💡 为什么是 sin/cos:三角函数有界(不会随句子变长而爆炸)、且 P E ( p o s + k ) PE(pos+k) PE(pos+k) 可由 P E ( p o s ) PE(pos) PE(pos) 线性表示,模型容易学会「相对距离」。现代大模型常用 RoPE:把向量旋转一个正比于位置的角度,于是「相对位置」变成「夹角差」——恰好能被下一步的点积感知。

🎮 👉 点击在线体验:拖动滑块看 sin/cos 位置编码波形

位置编码交互演示

八、Encoder Block 四件套(堆叠 N 层)

一个 Block 由四部分组成。理解这四件套,就理解了 90% 的 Transformer:

  1. 多头自注意力:让词与词充分对话(第 2~6 节)。
  2. 残差连接 + LayerNorm:把注意力的结果「小步」加回原值,并归一化(下一节)。
  3. 前馈网络 FFN:每个词独立过一个非线性小网络(第 10 节)。
  4. 残差连接 + LayerNorm:再把 FFN 的结果小步加回。

一个 Block 的数据流(Post-LN 写法):

x = x + Attention(LayerNorm(x))     # 子层1:注意力 + 残差
x = x + FFN(LayerNorm(x))           # 子层2:前馈 + 残差

Encoder Block 四件套数据流

💡 乐高版:把「注意力」和「FFN」想成两块功能不同的乐高,每块外面都裹一层「残差+归一化」的保护壳。把这块乐高重复堆 N 次,就是完整的 Transformer 编码器。

💡 Pre-LN vs Post-LN:上面是原论文的 Post-LN(先注意力后 Norm)。现代大模型多用 Pre-LNx + Attn(LayerNorm(x)) 顺序不变,但 Norm 在子层里面),训练更稳、更深也不易崩。


九、残差 & LayerNorm:让深层网络稳如老狗

残差连接:注意力和 FFN 都不替换输入,而是作为「增量 Δ」加回去:

x ← x + Δ x \leftarrow x + \Delta xx+Δ

💡 便利贴版:残差就像在原件上贴便利贴补充,而不是把整页重写。信息沿一条「残差流」主干一路累加,每层的修改都很小。

💡 为什么不可或缺:没有残差,每层要从零重建表示,几十层叠加会让梯度在反向传播中指数衰减(梯度消失),根本训不动。有了 x + Δ x+\Delta x+Δ,梯度有一条「高速公路」直达浅层——这是深层网络可行的关键。

🎮 👉 点击在线体验:可视化残差流如何穿越深层网络

残差连接交互演示
LayerNorm:对每个向量减均值、除标准差,再用可学习的 γ、β 微调:

L N ( x ) = γ ⋅ x − μ σ + β , μ , σ  为该向量自身的均值/标准差 LN(x) = \gamma \cdot \frac{x - \mu}{\sigma} + \beta, \quad \mu,\sigma \text{ 为该向量自身的均值/标准差} LN(x)=γσxμ+β,μ,σ 为该向量自身的均值/标准差

💡 统一格式版:LayerNorm 抹掉「长度/尺度」,只保留「方向」。把所有点推到半径固定的球面上,后续点积比较只看角度,不会被某个模长过大的点带偏。

💡 深层含义:正因为 LayerNorm 不断归一化长度,Transformer 里真正承载语义的是向量的方向而非长度——这也解释了为什么分析模型常用余弦相似度。

🎮 👉 点击在线体验:拖动向量看 LayerNorm 如何归一化

LayerNorm 归一化交互演示

十、前馈网络 FFN:把空间「折弯」出非线性

结构是「升维 → 激活 → 降维」,中间层通常是 4 倍宽:

F F N ( x ) = W 2 ⋅ G E L U ( W 1 x + b 1 ) + b 2 , W 1 : d → 4 d ,    W 2 : 4 d → d FFN(x) = W_2 \cdot GELU(W_1 x + b_1) + b_2, \quad W_1: d\rightarrow 4d,\; W_2: 4d\rightarrow d FFN(x)=W2GELU(W1x+b1)+b2,W1:d4d,W2:4dd

💡 折纸版:线性层只能旋转拉伸(保持「直」),唯有激活函数能把空间折弯。每个 ReLU/GELU 神经元像在空间压一道折痕,许多折痕叠加,就能把原本线性不可分的点云揉成可分的形状。

💡 另一种读法(键值记忆):升维后的每个神经元像一个「探测器」,命中就往输出写入一段对应的「知识向量」。研究表明 FFN 正是模型存储事实的地方(「巴黎—法国首都」这类知识就编码在这里),它占了模型约 2/3 的参数量。

🎮 👉 点击在线体验:调整 FFN 维度看空间如何被折弯

FFN 折纸交互演示

十一、堆叠 N 层 → 从混沌到语义

浅层多处理表层/语法关系,深层逐步形成抽象、任务相关的语义。最后取末位 token 的向量,和词表每个词向量做点积,选出最对齐的:

KaTeX parse error: Expected 'EOF', got '#' at position 59: …^T \quad \text{#̲ 又是一次夹角比较!}\\ P…

💡 首尾呼应:输出(Unembedding)又回到了点积——把末位点的方向和词表里每个词向量比夹角,最对齐的方向就是预测的下一个词。从输入到输出,模型自始至终只在反复比较和调整「方向」。

💡 这就是生成:选出一个词后,接到句尾,整流程再跑一遍预测下下个词——如此自回归,逐字写出整段回答。

🎮 👉 点击在线体验:逐层查看语义如何从混沌到清晰

堆叠 N 层演化交互演示

十二、代码实战:调库版 + 面试手搓版

① 调库版:把 MNIST 图片当序列做分类

我们把 28×28 的图看作「28 个时间步、每步 28 维特征」的序列,喂给 Transformer 编码器。真正的重点不是这堆代码,而是后面那张「张量尺寸变化表」——它告诉你数据每一步变成什么样。

import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader
from torchvision import datasets, transforms

# ===== 一、构建模型:继承 nn.Module,实现两个方法 =====
class TransformerModel(nn.Module):
    def __init__(self):
        super().__init__()
        self.embed = nn.Linear(28, 64)                  # 每步28维 → 64维(d_model)
        self.pos = nn.Parameter(torch.randn(1, 28, 64)) # 可学习的位置编码
        layer = nn.TransformerEncoderLayer(
                    d_model=64, nhead=4,               # 4个注意力头
                    dim_feedforward=128,               # FFN 隐藏维度
                    batch_first=True)                  # 形状 [批, 序列, 特征]
        self.encoder = nn.TransformerEncoder(layer, num_layers=2)  # 叠2层
        self.fc = nn.Linear(64, 10)                    # 全连接: 64 → 10类

    def forward(self, x):
        x = self.embed(x) + self.pos     # 投影 + 加位置编码  [批,28,64]
        x = self.encoder(x)              # 过编码器
        x = x.mean(dim=1)                # 序列维平均(池化) [批,64]
        return self.fc(x)                # 得到 10 类得分

# ===== 二、准备数据 / 设备 / 训练循环(套路固定)=====
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = TransformerModel().to(device)
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)

transform = transforms.ToTensor()
train_set = datasets.MNIST('./data', train=True, download=True, transform=transform)
train_loader = DataLoader(train_set, batch_size=64, shuffle=True)

for epoch in range(5):
    for images, labels in train_loader:
        images = images.squeeze(1).to(device)   # [批,1,28,28]→[批,28,28]
        labels = labels.to(device)
        outputs = model(images)
        loss = criterion(outputs, labels)
        optimizer.zero_grad(); loss.backward(); optimizer.step()
    print(f'Epoch {epoch+1}, Loss: {loss.item():.4f}')

张量尺寸变化追踪(务必看懂)

阶段形状
输入序列[批, 28, 28]
投影 + 位置编码[批, 28, 64]
Encoder 输出[批, 28, 64]
序列平均池化[批, 64]
全连接[批, 10]

② 面试手搓版:缩放点积注意力(必背核心 5 行)

面试说「手搓 Transformer」= 不准用 nn.TransformerEncoder,要自己写出注意力的矩阵运算。重点是默写下面这几行。

import torch.nn.functional as F

def attention(Q, K, V, mask=None):
    # Q,K,V: [批, 头, 序列, d_k]
    d_k = Q.size(-1)
    scores = Q @ K.transpose(-2, -1) / d_k ** 0.5   # ← 缩放点积
    if mask is not None:
        scores = scores.masked_fill(mask == 0, -1e9) # 挡住的位置填 -∞
    attn = F.softmax(scores, dim=-1)                 # 每行归一化
    return attn @ V                                  # 加权求和

🕳️ 面试高频追问预案

  • 为什么除 √d_k → 防点积方差随维度变大、Softmax 梯度消失。
  • 多头的意义 → 不同头关注不同子空间(语法/语义/位置),信息更丰富。
  • Encoder vs Decoder → Decoder 多一层带 causal mask 的自注意力 + 交叉注意力。
  • 手搓 vs 调库 → 手搓 = 自己写 QKᵀ/√d_k 那 5 行;调库 = 写应用。

训练四连(固定套路,所有模型通用)

optimizer.zero_grad()   # 1. 清零
loss.backward()         # 2. 反传
optimizer.step()         # 3. 更新

💡 口诀:清零 → 反传 → 更新。无论 CNN / RNN / LSTM / Transformer,这套训练循环完全一样


核心要点小结

  1. 自注意力 = 聚光灯:每个词同时向全场发问、按相关度融合别人的内容( Q K T / d k → s o f t m a x → ⋅ V QK^T/\sqrt{d_k} \rightarrow softmax \rightarrow \cdot V QKT/dk softmaxV)。
  2. Q/K/V 三张卡:提问卡、名牌卡、资料卡,由同一输入乘三个矩阵得来,「找人」与「给料」解耦。
  3. ÷√d_k 是降噪:把点积方差拉回 1,防止 Softmax 变独热、梯度消失。
  4. 多头 = 多组聚光灯:切分子空间,各看一种关系(语法/指代/距离),再融合。
  5. 位置编码 = 座位号:注意力本身不分先后,必须额外注入顺序信息。
  6. Encoder 四件套:多头注意力 → 残差+LN → FFN → 残差+LN,叠 N 层。
  7. 残差 = 贴便利贴 x ← x + Δ x \leftarrow x + \Delta xx+Δ,只走小步、不推倒重来,深层才训得动。
  8. LayerNorm = 统一格式:抹掉长度只留方向,把点拉回球面,稳定尺度。
  9. FFN = 折纸:激活函数把空间折弯,制造非线性,约占 2/3 参数、存事实。
  10. 它取代了 RNN:用 O(n²) 算力换「并行 + 长程直连」,是 BERT/GPT 的基石。

动手思考题

  1. 把「猫坐在垫子上」改成「垫子在猫坐上」,注意力矩阵会怎么变?位置编码在这里起了什么作用?
  2. 如果去掉 √d_k 缩放,d_k=512 时点积分数可能多大?Softmax 后权重会变成什么样?模型为什么学不动?
  3. 为什么多头用「切分子空间」而不是「用一个更大的头」?结合「语法对齐 vs 语义相关互相干扰」想一想。
  4. 假设只有 1 层、没有残差,深层网络会遇到什么具体问题?用「梯度高速公路」的比喻解释。
  5. 在评论区贴出你第一次看注意力公式时的困惑,我们一起排雷 💬

📚 关于本系列

本文是「AI 学习路线 · 阶段四:PyTorch 深度学习基础」系列中的一篇。完整系列覆盖从 Tensor 基础到模型诊断的全流程,每篇都配有可交互演示与高清配图。

🗺️ 查看完整 AI 学习路线(从 0 到进阶, 持续更新)
👉 bestsdz.xyz

觉得有帮助的话,欢迎去博客点个收藏 ⭐,你的支持是我持续输出的最大动力!

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐