3. Transformer 核心原理:从 Scaled Dot-Product Attention 到完整结构

Transformer 这个东西,现在做多模态的人几乎天天都在用。但说实话,很多人只是调包调习惯了,真问到里面那几个核心公式为什么这么设计,反而答不上来。我当年刚接触时也踩过不少坑,尤其是位置编码那块,理解偏了后面做多模态对齐就会出问题。

这一章咱们就把 Transformer 的底裤扒干净。从最基础的注意力计算,到多头机制,再到位置编码和整体结构,一个一个来。

3.1 Scaled Dot-Product Attention:为什么非要除根号 d_k?

先看最核心的公式:

Attention(Q, K, V) = softmax(Q × K^T / √d_k) × V

Q、K、V 分别是 Query、Key、Value 矩阵。d_k 是 Key 的维度。

很多人会问:为什么非要除这个 √d_k?

我解释一下。Q 和 K 做点积,结果是一个标量。如果 d_k 很大,比如 512 甚至 1024,点积的方差就会很大。方差大了,softmax 的输入值就会往两端跑——有的特别大,有的特别小。softmax 对极端值特别敏感,梯度会变得非常小,模型就学不动了。

除以 √d_k 相当于把方差拉回到 1 左右。这样 softmax 的梯度就能保持稳定。说白了,这是个数值稳定性技巧。

我的经验: 有一次我在做图文匹配任务,自己手写了一个简化版注意力,忘了除 √d_k。训练 loss 死活降不下去,折腾了两天才发现是这个问题。嗯,从那以后我写注意力代码第一行就把这个除号加上。

计算流程其实很简单:

  1. Q 和 K^T 做矩阵乘法,得到注意力分数矩阵
  2. 每个元素除以 √d_k
  3. 对每一行做 softmax,得到注意力权重
  4. 权重矩阵乘以 V,得到加权后的输出

用代码实现就是:

import torch
import torch.nn.functional as F

def scaled_dot_product_attention(Q, K, V):
    d_k = Q.size(-1)
    scores = torch.matmul(Q, K.transpose(-2, -1)) / torch.sqrt(torch.tensor(d_k, dtype=torch.float32))
    attn_weights = F.softmax(scores, dim=-1)
    output = torch.matmul(attn_weights, V)
    return output, attn_weights

3.2 多头注意力机制:让模型从不同角度去看

单头注意力有个问题:它只能学习一种注意力模式。但实际场景中,一个词可能跟多个不同位置的词都有关系。比如 "苹果" 这个词,在 "苹果很好吃" 里跟 "好吃" 相关,在 "苹果公司发布了新手机" 里跟 "公司" 和 "手机" 相关。

多头注意力就是让模型同时学习多个不同的注意力模式。每个头关注不同的子空间,最后把结果拼起来。

具体做法:

  1. 把 Q、K、V 分别线性投影到 h 组不同的子空间
  2. 每组独立做 Scaled Dot-Product Attention
  3. 把 h 个头的输出拼接起来
  4. 再做一次线性变换,得到最终输出

公式长这样:

MultiHead(Q, K, V) = Concat(head_1, ..., head_h) × W_O
其中 head_i = Attention(Q × W_Q_i, K × W_K_i, V × W_V_i)

W_Q_i、W_K_i、W_V_i 是每个头的投影矩阵,W_O 是输出投影矩阵。

关键点: 每个头的维度通常是 d_model / h。比如 d_model=512,h=8,每个头就是 64 维。总计算量和单头 512 维差不多,但表达能力更强。

我建议你记住一个数字:8 个头是最常见的配置。BERT base 用 12 个头, 用 12 个头,ViT 用 12 个头。但也不是越多越好,头太多每个头分到的维度太少,反而学不到什么东西。

3.3 位置编码:给序列注入顺序信息

Transformer 没有循环结构,也没有卷积。它一次性看到整个序列,但不知道哪个词在前哪个词在后。如果不加位置信息,"我打你" 和 "你打我" 在模型眼里是一样的。

位置编码就是来解决这个问题的。

最经典的是正弦余弦位置编码:

PE(pos, 2i)   = sin(pos / 10000^(2i / d_model))
PE(pos, 2i+1) = cos(pos / 10000^(2i / d_model))

其中 pos 是位置索引,i 是维度索引。

为什么用正弦余弦?有几个好处:

  • 值域在 [-1, 1] 之间,稳定
  • 不同位置的编码可以通过线性变换近似得到,模型能学到相对位置关系
  • 可以外推到比训练时更长的序列
避坑指南: 我曾经在一个视频理解项目里,直接把位置编码加到视觉特征上,结果效果很差。后来发现视觉特征和位置编码的尺度不匹配。我的建议是:位置编码的初始化方差要和特征保持一致,或者用 LayerNorm 归一化后再相加。

除了正弦余弦,现在更常用的是可学习位置编码。BERT、 系列用的都是可学习的。每个位置对应一个可训练的向量,让模型自己学出位置信息。好处是更灵活,坏处是不能外推到更长的序列。

还有一个变种是相对位置编码,比如 Transformer-XL 和 T5 用的那种。它不直接编码位置,而是编码两个 之间的相对偏移。在做长序列任务时,相对位置编码往往效果更好。

3.4 Transformer 整体结构:Encoder-Decoder 的骨架

把上面这些组件拼起来,就是完整的 Transformer 了。

标准的 Transformer 是 Encoder-Decoder 结构。Encoder 把输入序列映射成连续表示,Decoder 根据这个表示生成输出序列。

每个 Encoder 层包含两个子层:

  • 多头自注意力
  • 前馈神经网络(FFN)

每个 Decoder 层包含三个子层:

  • 掩码多头自注意力(防止看到未来信息)
  • 交叉注意力(关注 Encoder 的输出)
  • 前馈神经网络

每个子层后面都跟着残差连接和 LayerNorm。这个设计很巧妙——残差连接让梯度能直接流过,LayerNorm 稳定训练过程。

下面这张图展示了 Transformer 的核心结构:

Transformer 核心结构图 输入嵌入 + 位置编码 多头自注意力 残差连接 + LayerNorm 前馈神经网络 (FFN) 残差连接 + LayerNorm 输出嵌入 + 位置编码 掩码多头自注意力 残差连接 + LayerNorm 交叉注意力 (Q来自Decoder, K,V来自Encoder) 残差连接 + LayerNorm 前馈神经网络 (FFN) 残差连接 + LayerNorm K, V 线性层 + Softmax 说明: Encoder 和 Decoder 各层可以堆叠 N 次(通常 N=6 或 12)。 每个子层都包含残差连接和 LayerNorm,这是训练深层 Transformer 的关键。

你可能会问:Encoder 和 Decoder 之间怎么交互?答案是交叉注意力。Decoder 的 Query 来自自身,Key 和 Value 来自 Encoder 的最后一层输出。这样 Decoder 在生成每个词时,都能"看到"完整的输入序列。

工程要点: 实际应用中,很多模型只用 Encoder(如 BERT)或只用 Decoder(如 )。多模态融合通常用 Encoder 结构,因为我们需要同时处理多种模态的输入,而不是生成序列。

3.5 关键参数速查表

下面这个表是我平时做工程时常用的参数配置,你可以直接参考:

参数 符号 常见取值 说明
模型维度 d_model 512, 768, 1024 所有子层的统一维度
头数 h 8, 12, 16 d_model 必须能被 h 整除
每头维度 d_k = d_v 64, 64, 64 通常固定为 64
FFN 中间维度 d_ff 2048, 3072, 4096 通常是 d_model 的 4 倍
层数 N 6, 12, 24 越深能力越强,但训练难度也越大
Dropout - 0.1 防止过拟合
我的建议: 刚开始做多模态项目时,先用小模型试水。d_model=512, h=8, N=6 这个配置基本够用。等验证了思路再放大。我见过太多人一上来就搞 24 层 1024 维,结果训练一周发现效果还不如小模型。

好了,Transformer 的核心原理就这些。Scaled Dot-Product Attention 解决了注意力计算的稳定性问题,多头机制让模型能从不同角度理解关系,位置编码补上了序列顺序信息,整体结构把这几块拼成了一个可训练的端到端系统。