必要的背景知识
主要涉及的数学学科:
- 线性代数(矩阵乘法、softmax 归一化)
- 概率论(分布、加权平均)
- (可选)信息论(熵、互信息,用于理解注意力分布本身携带了多少信息)
常用的前置知识点:
- 《微积分基础》中的多元离散微积分——用于理解反向传播如何更新注意力的参数;
- 《智械起源》中的神经网络、反向传播、卷积网络与流形假设。
如果你已经读完《智械起源》,本文是它最自然的下一篇:在那里我们用 CNN 完成了图像分类;在这里,我们要让模型学会"关注重点",并由此引出统治了现代人工智能的 Transformer。
序列建模的困境
在《智械起源》里,我们用卷积网络(CNN)处理了一张张彼此独立的图像。但现实世界里大量的数据是序列:语言、语音、代码、时间序列、分子结构……它们的关键信息往往不在于单个元素,而在于元素之间的关系与顺序。
当我们把神经网络推向序列时,两种经典结构各自碰到了天花板。
卷积为何不够
CNN 在图像上大放异彩,靠的是两条设计:
- 局部感受野:每个神经元只看输入的一小块邻域;
- 权值共享:同一组卷积核滑过整张图。
这两条在图像上很合理(局部像素相关、平移不变),但搬到序列上却成了短板:
- 要看到"远处"的信息,必须一层层地把感受野堆大,路径随之变长;
- 权值共享让卷积对"位置"不敏感,而序列里的位置往往就是语义本身("我爱你"和"你爱我"完全不同)。
循环网络为何遇瓶颈
循环神经网络(RNN)及其改进版 LSTM、GRU 是序列建模的早期主力。它们引入"隐状态",在时刻 t 把输入与上一时刻的隐状态融合:
这条公式很优雅,但它把整段历史压缩进一个固定大小的向量 h_t,于是三个老问题挥之不去:
- 无法并行:第 t 步必须等第 t-1 步算完,训练速度被序列长度锁死;
- 梯度消失/爆炸:长序列上误差信号来回传递,h_t 对早先输入的记忆迅速衰减;
- 信息瓶颈:无论序列多长,所有历史都被塞进同一个定长向量。
一个核心问题
两种结构都在"绕路":CNN 靠堆叠扩大视野,RNN 靠顺序压缩历史。我们真正想要的是——
能不能让序列里的任意一个位置,直接、同时地与所有其他位置对话,并且还能并行计算?
注意力的时代,正是为了回答这个问题而到来。
注意力机制
核心直觉:软寻址
把注意力想象成一次"带权重的字典查询":
- 你手里有一句话(Query,查询),想知道自己该关注哪些内容;
- 句子里每个词都挂着一张"标签"(Key,键)和一段"内容"(Value,值);
- 你拿自己的 Query 去和每个 Key 比对相似度,越相似越值得关注;
- 最后不是"硬选一个词",而是按相似度加权地取回所有 Value 的混合。
这正是"软寻址(soft addressing)":不像普通数组那样按下标精确取值,而是按相关性软性地聚合信息。
数学定义:缩放点积注意力
最经典的形态是 缩放点积注意力(Scaled Dot-Product Attention):
逐项拆解:
- Q\in\mathbb{R}^{n\times d_k}、K\in\mathbb{R}^{n\times d_k}、V\in\mathbb{R}^{n\times d_k} 分别是查询、键、值矩阵;
- QK^{\top} 计算每个 Query 与每个 Key 的相似度(点积越大越相关);
- /\sqrt{d_k} 是缩放:当 d_k 很大时点积方差随之增大,softmax 会落入梯度极小的饱和区,除以 \sqrt{d_k} 把数值拉回稳定区间;
- \text{softmax}(\cdot) 把分数归一化成一组和为 1 的注意力权重;
- 权重乘以 V,得到按注意力聚合后的输出。
自注意力
"自注意力(Self-Attention)"指 Q,K,V全部来自同一个序列(只是各自经过不同的线性变换)。于是每个位置都用自身的 Query 去和全序列所有位置的 Key 比对,再聚合全序列的 Value。
这带来一个决定性优势——任意两个位置之间的路径长度都是 O(1):它们之间只有一条直接的加权边,不需要像 RNN 那样绕 O(n) 步、或像 CNN 那样叠 O(\log n) 层。长程依赖因此不再被距离"稀释"。
多头注意力
单一注意力只能学到一种"关系视角"。多头注意力(Multi-Head Attention) 把 Q,K,V 投影到 h 个不同的子空间,并行地各算一次注意力,再把结果拼接、线性融合:
不同头可以自发地学习语法依赖、指代关系、局部相邻等不同模式——模型于是拥有了"多维并行关注"的能力。
Transformer 架构
2017 年,Google 的论文《Attention Is All You Need》彻底移除了 RNN/CNN 的循环与卷积,仅用注意力搭出了完整的序列模型——Transformer。它证明:只要配上合适的结构与位置信息,注意力本身足以撑起整个网络。
总体结构
编码器(Encoder)把输入序列压缩成一组上下文化的表示;解码器(Decoder)在生成时,一边用带掩码的自注意力防止"偷看未来",一边用交叉注意力从编码器输出中检索所需信息。
位置编码
注意力本身对输入顺序是置换不变的:打乱词序,注意力权重只是跟着重排,输出形态不变。但"我爱你"和"你爱我"显然不同,因此必须显式注入位置信息。
Transformer 采用正弦位置编码,把位置直接加进词向量:
其中 pos 是位置、i 是维度下标。不同频率的正弦/余弦让邻近位置相似、远处位置可区分,且能泛化到比训练时更长的序列。后来的模型(如 RoPE、可学习位置嵌入)在此基础上做了大量演进,但"位置信息不可或缺"这一点始终成立。
残差与归一化
每个子层(注意力、FFN)外都套着残差连接 + 层归一化(Add & Norm):
残差让梯度可以"抄近路"回传,深层网络才训得动;层归一化稳定了每一层的激活分布。这套组合是 Transformer 能堆到成百上千层、进而支撑今天的大模型的根本保障之一。
代价与局限
注意力不是银弹,它的核心代价写在公式里:
- 计算与显存随序列长度平方增长:自注意力的复杂度约为 O(n^2 d),序列翻倍,开销约翻四倍。处理超长文本、高分辨率图像时这成了主要瓶颈;
- 位置信息需额外注入:如前所述,注意力本身是顺序无关的,少了位置编码就会退化;
- 数据是平的:标准注意力把所有位置一视同仁,缺少 CNN 那种天然的局部归纳偏置,往往需要更多数据才能学出局部规律。
也正因如此,后续研究才有了一大波工作:稀疏注意力、线性注意力、FlashAttention、状态空间模型(如 Mamba)等,本质上都是在"保留全局关注能力"与"压低 O(n^2) 代价"之间做权衡。
为什么是"注意力的时代"
把《智械起源》里的那条闭环——观察 → 表示 → 预测 → 决策 → 行动 → 反馈 → 更新——套到 Transformer 上,会发现它恰好补上了序列表示这一环,并带来了四个历史性优势:
- 可并行:去掉了 RNN 的顺序依赖,训练效率碾压循环网络,GPU 算力被彻底释放;
- 长程依赖:任意两位置 O(1) 直连,远距离关系不再随距离衰减;
- 可扩展:结构规整、易于堆叠,直接铺向了"规模即能力"的 Scaling Law,催生出 GPT、BERT 等大模型;
- 通用与可迁移:同一套架构既能读文本、也能看图像、听语音、下棋、写代码,"预训练 + 微调/上下文学习"的范式由此统一。
从机器翻译到 ChatGPT,从多模态到科学发现,Transformer 用"注意力"这一个朴素思想,重新定义了什么是可能的。这正是我们称之为"注意力的时代"的原因。
代码实现(PyTorch)
下面用一个可独立运行的极简 Transformer,演示自注意力如何学会关注序列中的关键信息。任务很简单:判断一个 0/1 序列里 1 的个数是否过半。我们训练一个小模型,并把最后一层的注意力热力图可视化出来。
import torchimport torch.nn as nnimport torch.nn.functional as Fimport matplotlibmatplotlib.use("Agg")import matplotlib.pyplot as plt# ========== 1. 缩放点积注意力 ==========class ScaledDotProductAttention(nn.Module): def __init__(self, d_k): super().__init__() self.d_k = d_k def forward(self, Q, K, V, mask=None): # Q,K,V: (batch, n_heads, seq_len, d_k) scores = torch.matmul(Q, K.transpose(-2, -1)) / (self.d_k ** 0.5) if mask is not None: scores = scores.masked_fill(mask == 0, -1e9) attn = F.softmax(scores, dim=-1) out = torch.matmul(attn, V) return out, attn# ========== 2. 多头注意力 ==========class MultiHeadAttention(nn.Module): def __init__(self, d_model, n_heads): super().__init__() assert d_model % n_heads == 0 self.d_k = d_model // n_heads self.n_heads = n_heads self.W_q = nn.Linear(d_model, d_model) self.W_k = nn.Linear(d_model, d_model) self.W_v = nn.Linear(d_model, d_model) self.W_o = nn.Linear(d_model, d_model) self.attn = ScaledDotProductAttention(self.d_k) def forward(self, x, mask=None): B, L, D = x.size() Q = self.W_q(x).view(B, L, self.n_heads, self.d_k).transpose(1, 2) K = self.W_k(x).view(B, L, self.n_heads, self.d_k).transpose(1, 2) V = self.W_v(x).view(B, L, self.n_heads, self.d_k).transpose(1, 2) out, attn = self.attn(Q, K, V, mask) out = out.transpose(1, 2).contiguous().view(B, L, D) out = self.W_o(out) return out, attn# ========== 3. 正弦位置编码 ==========class PositionalEncoding(nn.Module): def __init__(self, d_model, max_len=512): super().__init__() pe = torch.zeros(max_len, d_model) pos = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1) div = torch.exp(torch.arange(0, d_model, 2).float() * (-torch.log(torch.tensor(10000.0)) / d_model)) pe[:, 0::2] = torch.sin(pos * div) pe[:, 1::2] = torch.cos(pos * div) self.register_buffer("pe", pe.unsqueeze(0)) def forward(self, x): return x + self.pe[:, :x.size(1)]# ========== 4. Transformer 编码器块 ==========class TransformerEncoderLayer(nn.Module): def __init__(self, d_model, n_heads, d_ff): super().__init__() self.attn = MultiHeadAttention(d_model, n_heads) self.ffn = nn.Sequential( nn.Linear(d_model, d_ff), nn.ReLU(), nn.Linear(d_ff, d_model), ) self.norm1 = nn.LayerNorm(d_model) self.norm2 = nn.LayerNorm(d_model) def forward(self, x): a, attn = self.attn(x) x = self.norm1(x + a) x = self.norm2(x + self.ffn(x)) return x, attnclass TinyTransformer(nn.Module): def __init__(self, vocab_size, d_model, n_heads, d_ff, n_layers, max_len=64): super().__init__() self.embed = nn.Embedding(vocab_size, d_model) self.pos = PositionalEncoding(d_model, max_len) self.layers = nn.ModuleList( [TransformerEncoderLayer(d_model, n_heads, d_ff) for _ in range(n_layers)]) self.pool = nn.AdaptiveAvgPool1d(1) self.head = nn.Linear(d_model, 2) def forward(self, x): x = self.embed(x) x = self.pos(x) attns = [] for layer in self.layers: x, attn = layer(x) attns.append(attn) x = x.transpose(1, 2) x = self.pool(x).squeeze(-1) return self.head(x), attns# ========== 5. 合成数据:判断序列中 1 是否过半 ==========def make_data(n, L, seed=0): g = torch.Generator().manual_seed(seed) x = torch.randint(0, 2, (n, L), generator=g) y = (x.sum(dim=1) > L // 2).long() return x, y# ========== 6. 训练 ==========DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")L, VOCAB, D_MODEL, N_HEADS, D_FF, N_LAYERS, EPOCHS, LR, BATCH = 16, 2, 32, 4, 64, 2, 12, 1e-3, 64model = TinyTransformer(VOCAB, D_MODEL, N_HEADS, D_FF, N_LAYERS, max_len=L).to(DEVICE)crit = nn.CrossEntropyLoss()opt = torch.optim.Adam(model.parameters(), lr=LR)Xtr, ytr = make_data(2000, L, seed=1)Xte, yte = make_data(500, L, seed=2)for epoch in range(1, EPOCHS + 1): model.train() perm = torch.randperm(Xtr.size(0)) run_loss, correct, total = 0.0, 0, 0 for i in range(0, Xtr.size(0), BATCH): idx = perm[i:i + BATCH] xb, yb = Xtr[idx].to(DEVICE), ytr[idx].to(DEVICE) opt.zero_grad() logits, _ = model(xb) loss = crit(logits, yb) loss.backward() opt.step() run_loss += loss.item() * xb.size(0) correct += logits.argmax(1).eq(yb).sum().item() total += yb.size(0) # 测试 model.eval() with torch.no_grad(): logits, _ = model(Xte.to(DEVICE)) te_acc = 100. * logits.argmax(1).eq(yte.to(DEVICE)).sum().item() / yte.size(0) print(f"Epoch {epoch:02d} | Loss {run_loss/total:.4f} | " f"Train Acc {100.*correct/total:.2f}% | Test Acc {te_acc:.2f}%")# ========== 7. 可视化注意力热力图 ==========model.eval()sample = Xte[:1].to(DEVICE)with torch.no_grad(): _, attns = model(sample)last_layer = attns[-1][0, 0].detach().cpu().numpy() # 第 0 个样本、第 0 个头fig, ax = plt.subplots(figsize=(6, 5))im = ax.imshow(last_layer, cmap="viridis")ax.set_title("Self-Attention Map (head 0, last layer)")ax.set_xlabel("Key position")ax.set_ylabel("Query position")fig.colorbar(im, ax=ax)plt.savefig("attention_map.png", dpi=120)plt.show()print("注意力热力图已保存至 attention_map.png")将以上代码保存为 attention_demo.py 直接运行即可:模型通常只需两三个 epoch 就能在测试集上达到接近 100% 的准确率,并在 attention_map.png 中看到每个位置如何与其他位置建立关注权重。这短短几十行,正是《智械起源》里那条"观察 → 表示 → 预测"闭环在序列世界中的现代形态。
结语
从 CNN 的局部感受野,到 RNN 的顺序压缩,再到注意力的全局直连——我们走过的,是一条不断缩短"信息路径"、不断释放"并行算力"的路。Transformer 用"注意力"这一种机制统一了表示学习,也把机器学习真正带入了一个属于序列、属于规模、属于通用智能的时代。
注意力不是终点,而是范式:它让我们第一次相信,一个足够大的、会"关注重点"的模型,可以学会几乎任何事。
评论