机器学习·注意力的时代

Attention is all you need.

机器学习# Transformer# 注意力机制# 自注意力# 位置编码# PyTorch

必要的背景知识

主要涉及的数学学科:

  • 线性代数(矩阵乘法、softmax 归一化)
  • 概率论(分布、加权平均)
  • (可选)信息论(熵、互信息,用于理解注意力分布本身携带了多少信息)

常用的前置知识点:

  • 《微积分基础》中的多元离散微积分——用于理解反向传播如何更新注意力的参数;
  • 《智械起源》中的神经网络、反向传播、卷积网络与流形假设。

如果你已经读完《智械起源》,本文是它最自然的下一篇:在那里我们用 CNN 完成了图像分类;在这里,我们要让模型学会"关注重点",并由此引出统治了现代人工智能的 Transformer。

序列建模的困境

在《智械起源》里,我们用卷积网络(CNN)处理了一张张彼此独立的图像。但现实世界里大量的数据是序列:语言、语音、代码、时间序列、分子结构……它们的关键信息往往不在于单个元素,而在于元素之间的关系与顺序。

当我们把神经网络推向序列时,两种经典结构各自碰到了天花板。

卷积为何不够

CNN 在图像上大放异彩,靠的是两条设计:

  • 局部感受野:每个神经元只看输入的一小块邻域;
  • 权值共享:同一组卷积核滑过整张图。

这两条在图像上很合理(局部像素相关、平移不变),但搬到序列上却成了短板:

  • 要看到"远处"的信息,必须一层层地把感受野堆大,路径随之变长;
  • 权值共享让卷积对"位置"不敏感,而序列里的位置往往就是语义本身("我爱你"和"你爱我"完全不同)。

循环网络为何遇瓶颈

循环神经网络(RNN)及其改进版 LSTM、GRU 是序列建模的早期主力。它们引入"隐状态",在时刻 t 把输入与上一时刻的隐状态融合:

h_t = f(W_x x_t + W_h h_{t-1} + b)

这条公式很优雅,但它把整段历史压缩进一个固定大小的向量 h_t,于是三个老问题挥之不去:

  • 无法并行:第 t 步必须等第 t-1 步算完,训练速度被序列长度锁死;
  • 梯度消失/爆炸:长序列上误差信号来回传递,h_t 对早先输入的记忆迅速衰减;
  • 信息瓶颈:无论序列多长,所有历史都被塞进同一个定长向量。

一个核心问题

两种结构都在"绕路":CNN 靠堆叠扩大视野,RNN 靠顺序压缩历史。我们真正想要的是——

能不能让序列里的任意一个位置,直接、同时地与所有其他位置对话,并且还能并行计算?

注意力的时代,正是为了回答这个问题而到来。

注意力机制

核心直觉:软寻址

把注意力想象成一次"带权重的字典查询":

  • 你手里有一句话(Query,查询),想知道自己该关注哪些内容;
  • 句子里每个词都挂着一张"标签"(Key,键)和一段"内容"(Value,值);
  • 你拿自己的 Query 去和每个 Key 比对相似度,越相似越值得关注;
  • 最后不是"硬选一个词",而是按相似度加权地取回所有 Value 的混合。

这正是"软寻址(soft addressing)":不像普通数组那样按下标精确取值,而是按相关性软性地聚合信息。

数学定义:缩放点积注意力

最经典的形态是 缩放点积注意力(Scaled Dot-Product Attention):

\text{Attention}(Q,K,V)=\text{softmax}\!\left(\frac{QK^{\top}}{\sqrt{d_k}}\right)V

逐项拆解:

  • 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 个不同的子空间,并行地各算一次注意力,再把结果拼接、线性融合:

\begin{align} \text{MultiHead}(Q,K,V) &= \text{Concat}(\text{head}_1,\dots,\text{head}_h)W^{O} \\ \text{head}_i &= \text{Attention}(QW_i^{Q},\,KW_i^{K},\,VW_i^{V}) \end{align}

不同头可以自发地学习语法依赖、指代关系、局部相邻等不同模式——模型于是拥有了"多维并行关注"的能力。

Transformer 架构

2017 年,Google 的论文《Attention Is All You Need》彻底移除了 RNN/CNN 的循环与卷积,仅用注意力搭出了完整的序列模型——Transformer。它证明:只要配上合适的结构与位置信息,注意力本身足以撑起整个网络。

总体结构

flowchart LR subgraph ENC["编码器 Encoder (×N)"] E1["多头自注意力<br/>+ 残差 & 层归一化"] E2["前馈网络 FFN<br/>+ 残差 & 层归一化"] E1 --> E2 end subgraph DEC["解码器 Decoder (×N)"] D1["带掩码的多头自注意力<br/>+ 残差 & 层归一化"] D2["交叉注意力<br/>(查询来自解码器, K/V 来自编码器)<br/>+ 残差 & 层归一化"] D3["前馈网络 FFN<br/>+ 残差 & 层归一化"] D1 --> D2 --> D3 end IN["输入序列 + 位置编码"] --> ENC ENC --> D2 OUT["输出序列 + 位置编码"] --> D1 D3 --> P["线性层 + Softmax<br/>生成下一个词"]

编码器(Encoder)把输入序列压缩成一组上下文化的表示;解码器(Decoder)在生成时,一边用带掩码的自注意力防止"偷看未来",一边用交叉注意力从编码器输出中检索所需信息。

位置编码

注意力本身对输入顺序是置换不变的:打乱词序,注意力权重只是跟着重排,输出形态不变。但"我爱你"和"你爱我"显然不同,因此必须显式注入位置信息。

Transformer 采用正弦位置编码,把位置直接加进词向量:

\begin{align} PE_{(pos,2i)} &= \sin\!\left(\frac{pos}{10000^{2i/d_{\text{model}}}}\right) \\ PE_{(pos,2i+1)} &= \cos\!\left(\frac{pos}{10000^{2i/d_{\text{model}}}}\right) \end{align}

其中 pos 是位置、i 是维度下标。不同频率的正弦/余弦让邻近位置相似、远处位置可区分,且能泛化到比训练时更长的序列。后来的模型(如 RoPE、可学习位置嵌入)在此基础上做了大量演进,但"位置信息不可或缺"这一点始终成立。

残差与归一化

每个子层(注意力、FFN)外都套着残差连接 + 层归一化(Add & Norm):

\text{LayerNorm}(x + \text{Sublayer}(x))

残差让梯度可以"抄近路"回传,深层网络才训得动;层归一化稳定了每一层的激活分布。这套组合是 Transformer 能堆到成百上千层、进而支撑今天的大模型的根本保障之一。

代价与局限

注意力不是银弹,它的核心代价写在公式里:

  • 计算与显存随序列长度平方增长:自注意力的复杂度约为 O(n^2 d),序列翻倍,开销约翻四倍。处理超长文本、高分辨率图像时这成了主要瓶颈;
  • 位置信息需额外注入:如前所述,注意力本身是顺序无关的,少了位置编码就会退化;
  • 数据是平的:标准注意力把所有位置一视同仁,缺少 CNN 那种天然的局部归纳偏置,往往需要更多数据才能学出局部规律。

也正因如此,后续研究才有了一大波工作:稀疏注意力、线性注意力、FlashAttention、状态空间模型(如 Mamba)等,本质上都是在"保留全局关注能力"与"压低 O(n^2) 代价"之间做权衡。

为什么是"注意力的时代"

把《智械起源》里的那条闭环——观察 → 表示 → 预测 → 决策 → 行动 → 反馈 → 更新——套到 Transformer 上,会发现它恰好补上了序列表示这一环,并带来了四个历史性优势:

  1. 可并行:去掉了 RNN 的顺序依赖,训练效率碾压循环网络,GPU 算力被彻底释放;
  2. 长程依赖:任意两位置 O(1) 直连,远距离关系不再随距离衰减;
  3. 可扩展:结构规整、易于堆叠,直接铺向了"规模即能力"的 Scaling Law,催生出 GPT、BERT 等大模型;
  4. 通用与可迁移:同一套架构既能读文本、也能看图像、听语音、下棋、写代码,"预训练 + 微调/上下文学习"的范式由此统一。

从机器翻译到 ChatGPT,从多模态到科学发现,Transformer 用"注意力"这一个朴素思想,重新定义了什么是可能的。这正是我们称之为"注意力的时代"的原因。

代码实现(PyTorch)

下面用一个可独立运行的极简 Transformer,演示自注意力如何学会关注序列中的关键信息。任务很简单:判断一个 0/1 序列里 1 的个数是否过半。我们训练一个小模型,并把最后一层的注意力热力图可视化出来。

pythonpython
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 用"注意力"这一种机制统一了表示学习,也把机器学习真正带入了一个属于序列、属于规模、属于通用智能的时代。

注意力不是终点,而是范式:它让我们第一次相信,一个足够大的、会"关注重点"的模型,可以学会几乎任何事。

评论