基于 PyTorch 2.x 的大模型开发与微调专题【左扬精讲】—— Transformer 三大核心:Self-Attention、Multi-Head Attention 与 Position Encoding

基于 PyTorch 2.x 的大模型开发与微调专题【左扬精讲】—— Transformer 三大核心:Self-Attention、Multi-Head Attention 与 Position Encoding

Transformer 架构的核心,是把"序列里每一个 token 都能直接看见所有其他 token"这一件事做成了可并行计算的矩阵运算

本文围绕这一核心,给 Transformer 三件事做一份新手也能直接读懂的拆解Self-Attention 本身在做什么Multi-Head Attention 为什么要把 Self-Attention 并行做多次Position Encoding 为什么要单独加上去。读完这三件事,再看 Transformer 论文中那张"Attention Is All You Need" 图,就能直接把每个符号对应到公式与代码

Transformer Self-Attention Multi-Head Attention Position Encoding Q / K / V Scaled Dot-Product Sinusoidal

学习重点提示

必须掌握

    • 必须掌握:Self-Attention 的 Q / K / V 三向量从哪来、点积代表什么、为什么要除以 √dk
    • 必须掌握:Multi-Head Attention 的"切头并行 + 拼接"两步流程到底切的是什么
    • 必须掌握:Position Encoding 解决的是什么问题、Sinusoidal 与 Learned 两种方式的本质区别
    • 必须掌握:从 Self-Attention 到 Multi-Head Attention 的参数总量为何不变

了解即可

    • 了解即可:注意力掩码(Mask)在不同场景下的作用差异
    • 了解即可:为什么论文里选 Sinusoidal 的设计动机

👉 直接跳到 第一章 Self-Attention第二章 Multi-Head Attention第三章 Position Encoding

第一章 Self-Attention:让每一个 token 都能"看见"所有 token

What — Self-Attention 是什么?

Self-Attention(自注意力)是 Transformer 一层 Block 中执行"序列内信息聚合"的子模块。它的输入是一个矩阵(通常是这一层所有 token 的表征),输出是同样形状的矩阵。每个输出向量都是"对所有输入向量按相关性加权求和"的结果。从输出维度看,每个位置都聚合了"整条序列里和它相关"的信息——这是一种任一位置到全序列的直接连接,路径长度恒为 1。

三个在公式中反复出现的核心符号:

  • Query(Q):当前 token 提供的"查询向量",用于和其他 token 比较相关性
  • Key(K):每个 token 提供的"索引向量",用于被 Q 查询
  • Value(V):每个 token 提供的"内容向量",用于在相关性确定后被加权聚合

Self-Attention 的计算就是:先由 Q 和 K 算"谁跟谁相关",再用相关程度当权重对 V 做加权平均。

Why — 为什么需要 Self-Attention?

问题:RNN / LSTM 看不到远距离的上下文

在 Transformer 出现之前,处理序列的主流方式是 RNN / LSTM。这类模型必须"按顺序"处理 token,第 1 个词的信息要经过第 2、3、4 ... n 个时间步才能传到第 n 个词。当序列一长(几百、上千个 token),早期的关键信息会被冲淡,模型很难"记住"远距离的依赖关系。这就是所谓的长距离依赖问题

Self-Attention 的解法:让任意两个位置直接相连

Self-Attention 在计算第 i 个位置的输出时,会同时"看"序列里所有 j 个位置(包括 i 自己),并计算 i 与 j 之间的相关性。这意味着,序列里任意两个位置之间的"路径长度"都是 1 步——物理距离再远也不会衰减。

没有 Self-Attention 会发生什么?

  • 长文档中,开头的设定与结尾的呼应,模型难以学到
  • 代码补全场景中,开括号和闭括号之间隔着几十行,模型难以关联
  • 多轮对话中,用户在第 1 轮提出的约束,第 10 轮很难再被想起

1.1 三步走:Q / K / V 从哪来

Self-Attention 的输入是一个矩阵 X,形状 (n, d_model),其中 n 是序列长度,d_model 是每个 token 的表征维度。Q / K / V 三个矩阵都由 X 乘以三个可学习的权重矩阵得到:

  import torch
  import torch.nn as nn
  
  # 假设:n=4 个 token,每个 token 用 8 维向量表示
  n = 4
  d_model = 8
  X = torch.randn(n, d_model)   # (4, 8)
  
  # 三个可学习权重矩阵
  W_Q = nn.Linear(d_model, d_model, bias=False)
  W_K = nn.Linear(d_model, d_model, bias=False)
  W_V = nn.Linear(d_model, d_model, bias=False)
  
  Q = W_Q(X)   # (n, d_model)  Query
  K = W_K(X)   # (n, d_model)  Key
  V = W_V(X)   # (n, d_model)  Value
  

这一段就一句话:三个 W 是模型训练出来的,不是固定的。模型通过反向传播学会"什么样的 Q、K 配对能反映真正的语义相关性"。

1.2 计算注意力分数:Q 点积 K

下一步是算"谁跟谁相关"。做法是:把第 i 个 Query 和所有 Key 做点积,得到 i 对所有位置的"相关性分数"。

  # Q: (n, d_model), K: (n, d_model)
  # Q @ K.T: (n, n)  每一行是某个 Query 对所有 Key 的分数
  scores = Q @ K.T              # (n, n)
  print(scores.shape)          # torch.Size([4, 4])
  

得到的 scores 矩阵(n×n)就是"注意力分数矩阵"。第 i 行的第 j 列,代表"位置 i 的 token 觉得位置 j 的 token 有多重要"。点积越大,两个向量越"同向",相关性越强。

1.3 缩放(Scale):为什么除以 √dk

在得到分数后,Transformer 论文做了一步关键操作:除以 √dk,其中 dk 是 Q / K 的维度(即 d_model)。这一步叫做 Scaled Dot-Product Attention,公式如下:

Attention(Q, K, V) = softmax( Q·KT / √dk ) · V

为什么一定要除?原因:点积的数值会随着维度 dk 增长而变大。假设 Q、K 的每个分量都是均值 0、方差 1 的独立随机变量,那么 Q·K 的方差就是 dk。如果 dk 较大(比如 64、128),点积的绝对值会偏大,softmax 的输出会极度趋近于 one-hot 分布——也就是"只把注意力压在一个位置上",梯度极小。论文作者通过 √dk 这一缩放,让 softmax 的输入方差稳定在 1 附近,梯度更平滑。

新手提醒

这里的"除以 √dk"是论文 Section 3.2.1 写明的步骤,并非可选项。原论文明确指出:"We suspect that for large values of dk, the dot products grow large in magnitude, pushing the softmax function into regions where it has extremely small gradients"——这就是需要缩放的原因。PyTorch 的 torch.nn.functional.scaled_dot_product_attention 内部默认就包含这一缩放。

1.4 加权求和:softmax 分数乘 V

把分数做 softmax 归一化(让每行变成 0~1 的概率分布),再乘 V,就得到每个位置的新表征:

  import math
  
  d_k = Q.size(-1)            # Q/K 的维度
  attn = torch.softmax(scores / math.sqrt(d_k), dim=-1)   # (n, n)
  out = attn @ V              # (n, d_model)
  

这段代码就是 Transformer 论文里 Attention 公式的逐行翻译。Self-Attention 的全部复杂度就浓缩在这 4 行里:Q / K / V 投影 → 点积 → 缩放 → softmax → 加权求和。

1.5 完整 PyTorch 最小实现

  class SelfAttention(nn.Module):
      def __init__(self, d_model):
          super().__init__()
          self.W_Q = nn.Linear(d_model, d_model, bias=False)
          self.W_K = nn.Linear(d_model, d_model, bias=False)
          self.W_V = nn.Linear(d_model, d_model, bias=False)
  
      def forward(self, X):
          Q = self.W_Q(X)
          K = self.W_K(X)
          V = self.W_V(X)
          scores = Q @ K.transpose(-2, -1) / math.sqrt(Q.size(-1))
          attn = torch.softmax(scores, dim=-1)
          return attn @ V
  

Self-Attention 四个必背要点

  • 1 个动作:用 Q 和 K 算相关性,用相关性当权重对 V 加权求和
  • 3 个矩阵:Q / K / V 都由输入 X 经过可学习的线性映射得到
  • 1 步缩放:除以 √dk 防止 softmax 数值饱和(论文里的必备步骤)
  • 1 个输出:每个位置的输出向量,已经"混入了所有位置的相关信息"

第二章 Multi-Head Attention:把一个注意力"分身"成多个子空间

What — Multi-Head Attention 是什么?

Multi-Head Attention(多头注意力)就是:让 Self-Attention 并行做 h 次,每次只看一部分维度,最后把结果拼回来。每一份"只看一部分维度"的注意力,被称为一个"头"(Head)。

论文 Section 3.2.2 的公式是:

MultiHead(Q, K, V) = Concat(head1, head2, ..., headh) · WO

headi = Attention(Q·WiQ, K·WiK, V·WiV)

其中 h 是头数(论文 base 模型 h = 8),每个头的 Q / K / V 维度是 dk = dmodel / h。

Why — 为什么非要"多头"?单一 Self-Attention 不够吗?

问题 1:单一 Self-Attention 的"相关性"维度被合并

Self-Attention 用一个 dmodel 维度的 Q / K 去算"谁跟谁相关"。论文 Section 3.2.2 原文指出:"Multi-head attention allows the model to jointly attend to information from different representation subspaces at different positions. With a single attention head, averaging inhibits this."——单头注意力把这多种维度的相关性平均到同一个 dmodel 维度的空间里,难以区分。

  • 主语和谓语之间的"语法关系"
  • 代词和前文名词之间的"指代关系"
  • 同义词之间的"语义相似关系"
  • 相邻词之间的"搭配关系"

Multi-Head Attention 的目标就是让不同的头分别建模不同类型的关系。

问题 2:每个头看不全所有维度

Multi-Head Attention 的核心技巧是:把 dmodel 维度切成 h 份,每份独立做 Self-Attention。这样每个头只需要在较低的维度(dmodel/h)里学一种"相关性模式"。最后的输出再拼接、WO 投影回原维度。

没有多头会发生什么?

  • 同一句话里"主谓搭配"和"代词指代"挤在同一个 Q / K 通道里,互相干扰
  • 模型对"远距离语法依赖"和"近距离语义搭配"难以分别建模
  • 表达能力受限,在大模型规模上"力不从心"

2.1 三步走:切头 → 并行注意力 → 拼接投影

Multi-Head Attention 的实现流程可以用三步概括:

  class MultiHeadAttention(nn.Module):
      def __init__(self, d_model, num_heads):
          super().__init__()
          assert d_model % num_heads == 0, "d_model 必须能被 num_heads 整除"
          self.num_heads = num_heads
          self.d_k = d_model // num_heads
  
          # 四个独立的线性层,参数尺寸均为 d_model × d_model
          self.W_Q = nn.Linear(d_model, d_model, bias=False)
          self.W_K = nn.Linear(d_model, d_model, bias=False)
          self.W_V = nn.Linear(d_model, d_model, bias=False)
          self.W_O = nn.Linear(d_model, d_model, bias=False)
  
      def forward(self, X):
          n, d_model = X.size()
          # 1) 切头:把 (n, d_model) 拆成 (n, num_heads, d_k)
          Q = self.W_Q(X).view(n, self.num_heads, self.d_k)
          K = self.W_K(X).view(n, self.num_heads, self.d_k)
          V = self.W_V(X).view(n, self.num_heads, self.d_k)
  
          # 2) 并行注意力:每个头独立算分数和加权
          scores = (Q @ K.transpose(-2, -1)) / math.sqrt(self.d_k)  # (n, h, n)
          attn = torch.softmax(scores, dim=-1)
          out = attn @ V                                           # (n, h, d_k)
  
          # 3) 拼接投影:把 h 个头拼回 (n, d_model)
          #    transpose 后内存非连续,先 contiguous 再 view
          out = out.transpose(0, 1).contiguous().view(n, d_model)
          return self.W_O(out)
  

注意第 1 步里的 view(n, num_heads, d_k):这一步就是"切头"。原来一个 dmodel 维度的表征,被物理上切成了 h 份低维表征,每份独立做 Self-Attention。

2.2 参数总量:单头 vs 多头几乎一样

新手最容易误解的点:以为多头会让"参数变成 h 倍"。其实不是。

看代码里四个权重矩阵:WQ、WK、WV、WO 每一个都是 d_model × d_model。无论 h = 1 还是 h = 8,这四个矩阵的尺寸都不变。

那"多头"切出来的"多个 WiQ"在哪里?答案是它们共享同一个 WQ 线性层(参数尺寸 dmodel×dmodel),但通过 step 1 后的 view(n, num_heads, d_k) 把同一组参数"切"成 h 份子空间使用。训练时模型通过反向传播学到的,是这同一个矩阵在不同子空间下的协同表达。最终"多头"和"单头"的参数总量一致(仍为 4 × dmodel2),但模型获得了"在不同子空间并行学习多种关系"的能力。

训练小贴士

当 dmodel 不能被 h 整除时,计算会直接报错。常见组合:dmodel = 512、h = 8(论文原版);dmodel = 768、h = 12(BERT-base);dmodel = 1024、h = 16(BERT-large)。所有这些都满足 dmodel 能被 h 整除。

2.3 多头 vs 单头的实际差异

维度单头 Self-Attention多头 Multi-Head Attention
WQ / WK / WV 参数量 3 × dmodel2 3 × dmodel2(合并存储)
单头 Q / K 维度 dmodel dmodel / h
学到的"相关性"种类 1 种(平均) h 种(独立)
总计算量 1 × Q·KT · V h × (Q·KT · V) / h ≈ 1 × Q·KT · V

虽然 h 个头看起来"算 h 次",但每个头的维度是 dmodel/h,矩阵乘的复杂度与维度平方成正比,所以总计算量大致不变。论文原版实验设置就是 h = 8、dmodel = 512,得到 dk = dv = 64。

Multi-Head Attention 四个必背要点

  • 1 个动作:把 1 个 Self-Attention 切成 h 个并行子空间,再拼接回来
  • 1 个关键变换:dmodel → (h, dmodel/h),每个头独立做注意力
  • 参数近似不变:所有 W 矩阵尺寸保持 dmodel×dmodel,多头只是"重用"
  • 多视角学习:不同头天然学到不同的关系(语法、指代、搭配等)

第三章 Position Encoding

What — Position Encoding 是什么?

Position Encoding(位置编码)是把"位置 0、位置 1、位置 2 ..."这种顺序信息,叠加到 token 表征上的一种方法。

原版 Transformer 论文使用的是 Sinusoidal Position Encoding(正弦位置编码)。它的公式是:

PE(pos, 2i) = sin(pos / 100002i / dmodel)

PE(pos, 2i+1) = cos(pos / 100002i / dmodel)

其中 pos 是 token 在序列中的位置(0、1、2 ...),i 是维度索引(0、1、2 ... dmodel/2 - 1)。最终 PE 是一个 (n, dmodel) 的矩阵,与 token embedding X 直接相加。

Why — 为什么 Self-Attention 之后还要单独加 Position Encoding?

问题:Self-Attention 本身是"无视顺序"的

把句子"我吃苹果"打乱成"苹果吃我",WQ、WK、WV 是同一组参数,token 的表征也跟着位置走(即 token 仍然对应同一行),所以 Q / K / V 矩阵的"行集合"不变;但行顺序变了。Self-Attention 对这种行置换有 equivariance 性质——数学上的严格结论是:输出矩阵同样按 token 位置做置换(而不是"完全相同"),也就是说模型对"位置是否被打乱"这件事没有任何感知。换句话说,Self-Attention 在数学上是 permutation equivariant(置换等变)的,它真的不知道也不在乎谁先谁后。

但自然语言是强顺序的:"不爽了我"和"我不爽了"意思截然不同。所以必须给词向量补充"这是第 0 个 token,那是第 1 个 token"的位置信息。这就是 Position Encoding 的存在意义。

Position Encoding 的解法:把位置信息"加"进 token 表征

常见做法是:构造一个只与位置有关的向量 PE(pos),与 token 的 embedding X 逐元素相加。这样 X 里就同时包含了"词义"和"位置"两种信息,再丢给 Self-Attention 时,模型就能感知到当前 token 处在哪个位置。

没有 Position Encoding 会发生什么?

  • "狗咬人"和"人咬狗"在模型眼里完全一样
  • "我喜欢你"和"你喜欢我"无法区分
  • 代码生成场景下,a = b;b = a; 这种顺序差异被忽略

3.1 Sinusoidal 公式逐项拆解

论文里的 PE 公式看起来吓人,其实只有两个关键点:

  • 奇偶维度交替:偶数维度用 sin,奇数维度用 cos
  • 频率随维度递减:i 越大,sin / cos 的波长越长(频率越低)

直觉解释:不同维度捕获不同"尺度"的位置信息。低 i(小维度)捕获相邻位置之间的细微差异,高 i(大维度)捕获远距离位置之间的整体趋势。这样模型既能区分相邻 token,也能区分章节首末。

3.2 PyTorch 最小实现

import math
  
  def sinusoid_position_encoding(n, d_model):
      pe = torch.zeros(n, d_model)
      pos = torch.arange(0, n).float().unsqueeze(1)        # (n, 1)
      div = torch.exp(torch.arange(0, d_model, 2).float()
                      * (-math.log(10000.0) / d_model))    # (d_model/2,)
      pe[:, 0::2] = torch.sin(pos * div)   # 偶数维
      pe[:, 1::2] = torch.cos(pos * div)   # 奇数维
      return pe
  
  # 用法:与 token embedding 相加
  token_emb = nn.Embedding(vocab_size, d_model)(input_ids)   # (n, d_model)
  pe = sinusoid_position_encoding(n, d_model)                 # (n, d_model)
  X = token_emb + pe                                          # 位置信息已注入
  

实现里用了一个数学等价变换:把 100002i / d_model 写成 exp(2i * (-log(10000) / d_model)),避免显式调用指数运算,结果完全一致。

3.3 Sinusoidal vs Learned:两种主流派别

Transformer 后续工作中,主流的 Position Encoding 有两派:

方案原理代表模型优点缺点
Sinusoidal(固定) 按 sin / cos 公式硬编码 原始 Transformer 无需学习、可外推到训练时未见过的长度 表达位置信息的能力受限于 sin / cos 形式
Learned(可学习) 每个位置一个可学习的向量 BERT、GPT-2 早期版本 数据驱动、对训练数据自适应 无法外推到训练长度之外

"Sinusoidal 与 Learned 孰优"历来没有定论。Sinusoidal 的最大优势是可外推:训练时用了 512 长度,推理时给 1024 也大致能跑。Learned 在训练长度内往往表现稳定,但超过训练长度上限后该位置没有训练值,效果会明显下降。后续工作如 RoPE、ALiBi 等则是改造注意力机制本身,让位置信息以更优雅的方式参与到 Q / K 的点积中。

新手提醒

不要把"位置编码"和"位置嵌入(Positional Embedding)"混淆。两者的关系:位置编码是"如何计算位置向量"的策略(如 Sinusoidal);位置嵌入是"位置向量本身"的名字。在 PyTorch 实际代码里,nn.Embedding(max_len, d_model) 表示的就是 Learned 位置嵌入。

3.4 验证 PE 有效性的方法

把 PE 加到 X 前后做一次对比,能直观看出"顺序信息有没有被注入":

  # 没用 PE:两个句子的表征完全相同
  X_no_pe = token_emb
  attn_no_pe = SelfAttention()(X_no_pe)
  
  # 用了 PE:两个句子的表征不同
  X_pe = token_emb + pe
  attn_pe = SelfAttention()(X_pe)
  
  print(torch.allclose(attn_no_pe[[0, 1]], attn_no_pe[[1, 0]]))  # True
  print(torch.allclose(attn_pe[[0, 1]], attn_pe[[1, 0]]))        # False
  

Without PE,两句话可以互换位置结果不变;With PE,互换位置后结果不同——这就是 PE 生效的直观证据。

Position Encoding 四个必背要点

  • 1 个问题:Self-Attention 本身无视 token 顺序
  • 1 个解法:构造 PE(pos) 与 token embedding 逐元素相加
  • 1 个公式:偶数维 sin、奇数维 cos,频率随维度递减
  • 两大派别:Sinusoidal(固定、可外推)vs Learned(可学习、不可外推)

第四章 三者协同:Transformer 一层 Block 的完整视野

我从公式中看到的三件事

第一章到第三章看似是三个独立概念,实际上在 Transformer 一层 Block 里是串行接力的关系。把它们合起来看,Transformer 的"一层"完整视野是:

视角一:先加位置,再做多头注意力

输入的 token embedding 出来后,第一步是和 PE 相加,让 X 同时携带"词义"和"位置"信息。第二步才是 Multi-Head Attention,让每个位置的 token 看着"全局 + 顺序"去做加权聚合。如果跳过第一步,Multi-Head Attention 拿到的是"无序"的 X,再怎么切头也学不出"主谓在前 vs 主谓在后"的差异。

视角二:Multi-Head 是 Self-Attention 的"工程实现版"

从公式上严格说,Transformer 一层 Block 里只可能出现 Multi-Head Attention,不会单独出现"Single-Head Self-Attention"——因为输入维度 dmodel 固定被切成 h 份,每个头拿到的是 dmodel/h。但理解时应先理解单头 Self-Attention,再把单头扩展到多头。论文里先讲 Scaled Dot-Product Attention,再讲 Multi-Head Attention,就是这个原因。

视角三:残差 + LayerNorm 是必杀技,但与本三件事无关

Transformer 实际 Block 里在 Multi-Head Attention 之后还会有残差连接 + LayerNorm + Feed-Forward + 残差 + LayerNorm。这些模块是为了训练稳定性服务的,与 Self-Attention / Multi-Head / Position Encoding 这三件事并不直接相关,本文不展开。

三件事的核心流程口诀:

  • Step 1:加位置(PE)——给 X 注入顺序信息
  • Step 2:切头——把 dmodel 维度切成 h 份
  • Step 3:算注意力——每头独立做 QK√dk → softmax → @V
  • Step 4:拼头——h 份结果拼回 dmodel,做 WO 投影

避坑提醒(新手视角):

  • 不要先做 Self-Attention 再加 PE:位置信息必须在注意力之前进入,否则模型已经做完"无序聚合"了
  • 不要把每头维度算错:是 dmodel / h,不是 dmodel × h,更不是 dmodel
  • 不要遗漏 √dk 缩放:这是论文的必备步骤,不是优化项

一张图看完 Transformer 一层 Block

  输入 token ids
       │
       ▼
  Token Embedding (vocab_size → d_model)
       │
       ▼
  + Positional Encoding (Sinusoidal / Learned)        ← 第三章
       │
       ▼
  X = Embedding + PE  (n, d_model)
       │
       ▼
  Multi-Head Attention                                 ← 第一、二章
    │
    ├─ Q = X·W_Q, K = X·W_K, V = X·W_V
    ├─ 切头: (n, d_model) → (n, h, d_model/h)
    ├─ 每头: scores = Q·K^T / √d_k
    ├─ softmax → 注意力权重
    ├─ 加权求和: attn @ V
    └─ 拼头 + W_O 投影
       │
       ▼
  Add & LayerNorm (残差 + 归一化)
       │
       ▼
  Position-wise Feed-Forward (论文 Section 3.3)
       │
       ▼
  Add & LayerNorm
       │
       ▼
  下一层 Block 入口
  

FAQ 常见问题(20 组)

Q1. Self-Attention 能不能不用 softmax?

可以用其他归一化方式,但 softmax 是事实标准。softmax 的优势是输出可解释为概率分布,所有权重非负且和为 1,便于梯度回传。也有工作研究用 ReLU 等替代(如 Performer 用 FAVOR+ 随机特征近似 softmax-attention),但这些多属于"加速"或"近似"方案。原始 Transformer 论文 Section 3.2.1 用的是 softmax 配合 √dk 缩放。

Q2. Q / K / V 三个矩阵为什么不能合并?

可以合并存储,但不能合并语义。实现上,Q / K / V 三个部分可以共享同一段逻辑代码,PyTorch 的 nn.MultiheadAttention 内部就是用 4 个独立的 nn.Linear(Q / K / V / O 各一个)分别投影,再调用底层的 scaled_dot_product_attention。但语义上 Q、K、V 必须分别计算,否则点积退化为"被查询向量与自身的相似度",失去了"跨位置、跨 token 比较相关性"的设计目标。

Q3. Multi-Head Attention 中不同头真的能学到不同模式吗?

可以,原论文用可视化验证过。原论文在 Attention Visualizations 一节(对应 Figure 3 / Figure 4 / Figure 5)中展示了多个 head 的注意力分布:Figure 3 展示 head 关注远距离依赖("making ... more difficult" 这种长距离关系);Figure 4 展示 head 参与指代消解("its" 指向哪个名词);Figure 5 展示 head 与句子结构(语法)相关。这种"自然分工"是模型自主学出来的,不是人工指定的。

Q4. Self-Attention 的计算复杂度是多少?

O(n2 · d_model),其中 n 是序列长度。Q·KT 是 n×d 乘 d×n,得到 n×n 的分数矩阵。这意味着 Self-Attention 的时间和空间复杂度是序列长度的平方级——这就是 Transformer 处理超长文本时显存吃紧的根源。后续工作(如 FlashAttention、Linear Attention)就是要解决这个 n2 问题。

Q5. Attention 分数矩阵为什么是对称的?

一般不对称。在 Encoder 的 Self-Attention 里,Q = WQX 且 K = WKX,但 WQ 和 WK 是两个独立学习的参数矩阵,一般并不相等,所以 Q·KT 不对称。Decoder 的 Cross-Attention 同样不对称,因为 Q 来自 Decoder、K 来自 Encoder,对应不同的来源 X;这种情况的分数矩阵天然非对称。

Q6. Position Encoding 必须加在 Embedding 后面吗?

原版 Transformer 是这样做的(相加),但不是唯一做法。也有工作把位置信息直接注入到 Q / K 的点积里(如 RoPE 把位置编码作为旋转矩阵作用于 Q / K,ALiBi 把线性偏置加到注意力分数上)。相加是最简单、也是原论文采用的具体方案。其他方案各有优缺点,但不在本文范围。

Q7. PE 加在哪里最安全?

把"加在 Embedding 之后、进入注意力之前"作为默认起点,再视模型选择具体注入位置。原版 Transformer 与 BERT 选用相加方案;后续模型对"位置信息注入位置"做了不同选择:LLaMA 系列把位置信息通过 RoPE 注入到 Q / K 的点积里;ALiBi 则把位置偏置加到注意力分数上。具体取舍视任务与训练稳定性而定。本文讨论范围限定在原版 Transformer 的相加方案。

Q8. Sinusoidal PE 的 10000 这个数字有什么讲究?

是论文中"能让不同频率覆盖不同尺度"的超参数。10000 是论文作者选的值,能让最高频(i=0)的波长是 2π、最低频(i=d/2-1)的波长大约是 10000 × 2π。具体值是论文的工程选择,没有强理论依据;多数后续工作(包括 BERT、RoPE 等)也沿用或继承了这一数量级的设定。

Q9. 如果只用 Learned PE 不行吗?

行,训练数据足够时可学习 PE 往往表现稳定。BERT、GPT-2 等早期版本就只用 Learned PE。但 Learned PE 的"位置索引"是离散的,超出训练时设置的最大长度(例如 512)时该位置的 embedding 没有训练值,效果会明显下降。Sinusoidal 没有这个限制,可以平滑外推到更长序列。

Q10. Sinusoidal PE 加完之后值范围会变大吗?

会变,但大多在 [-1, 1] 范围内。sin / cos 的取值范围本身就是 [-1, 1],与 embedding(一般初始化为均值为 0、方差较小的随机值)相加后,X 的数值范围变化不大。原版 Transformer 后续接 LayerNorm 进行归一化,可进一步稳定数值。

Q11. Multi-Head Attention 的 h 怎么选?

h 是超参数,常见取值在 8~25 之间,没有"最优解"。原论文 base 模型 h = 8;BERT-base h = 12、BERT-large h = 16;GPT-2 系列按尺寸递增:small h = 12、medium h = 16、large h = 20、xl h = 25。h 过大会让 dk = dmodel/h 太小(每头没空间学东西);h 过小则失去多视角意义。具体取值与模型规模、训练数据共同决定。

Q12. Self-Attention 怎么用在 Decoder 里?

Decoder 用 Causal Self-Attention(因果自注意力),保证预测时不偷看未来。解码第 t 个 token 时,模型只能看到位置 0..t-1 的信息,要把位置 t..n-1"屏蔽"掉。实现上常用 torch.triu(torch.ones(n, n), diagonal=1) 生成掩码,把分数矩阵的右上三角(含对角线之上)置为 -∞,softmax 后这些位置权重为 0,从而保证当前位置只能关注已生成的位置。也可以直接用 PyTorch 的 is_causal=True 参数触发相同行为。

Q13. Cross-Attention 和 Self-Attention 有什么区别?

Cross-Attention 的 Q 来自 Decoder,K / V 来自 Encoder。Self-Attention 中 Q、K、V 来自同一输入;Cross-Attention 中 Q = WQXdecoder、K = WKXencoder、V = WVXencoder。这是 Encoder-Decoder 架构中"Decoder 去查询 Encoder 上下文"的核心机制。

Q14. Self-Attention 和 CNN 有什么关系?

两者在"加权聚合"的形式上相似,但权重来源不同。CNN 用固定卷积核在局部滑动提取特征;Self-Attention 用 Q / K 动态算出的"权重"在全局做加权求和。把注意力范围限制到局部 k 邻域、并固定权重时,Self-Attention 的形式与 CNN 在数学上接近;但一般用法中 Self-Attention 的权重是动态学出来的,作用范围是全序列。

Q15. Self-Attention 怎么并行?不像 RNN 那样必须顺序吗?

Self-Attention 天然可并行。Q / K / V 都是矩阵乘,Q·KT 也是矩阵乘,整条链路没有"必须等前一个位置算完"的依赖。GPU 可以一次性把整个序列的注意力分数矩阵算出来。这就是 Transformer 训练速度远快于 RNN 的关键原因。

Q16. 为什么 Self-Attention 里不打 attention 到自己?

可以,且默认会。Self-Attention 算 Q·KT 时,Q 第 i 行和自己 K 第 i 列的点积自然落在分数矩阵的第 (i, i) 个位置,softmax 之后这个位置也会得到一个合法权重。模型学到的注意力分布里,"自己投自己"通常会拿到一份不小的权重——这是正常的,因为当前 token 自身的向量本身就包含最有用的信息。

Q17. Self-Attention 中注意力权重的和一定是 1 吗?

是,每行(即每个 Query)的权重和为 1。softmax 的输出是合法概率分布,每行都是非负且和为 1。这意味着 Self-Attention 的输出是"当前 Query 对所有 Key 的加权平均",不会因为序列长度变化而数值漂移。

Q18. Self-Attention 的输出维度是多少?

和输入维度一致,都是 (n, d_model)。V 经过 WV 投影后是 (n, d_model),分数矩阵 (n, n) 乘 V 还是 (n, d_model)。这一点对 Transformer Block 的堆叠至关重要——每一层的输入输出维度相同,下一层 Block 才能直接接收上一层的输出。

Q19. 一定要用 Scaled Dot-Product Attention 吗?

不一定,原论文 Section 3.2.1 同时讨论了两种主流方案。原论文对比的是 Additive Attention(用一个小型单隐层前馈网络算分数)与 Dot-Product Attention。论文原文指出:两者在理论复杂度上相似,但 Dot-Product "is much faster and more space-efficient in practice, since it can be implemented using highly optimized matrix multiplication code"——因为矩阵乘可以通过高度优化的 BLAS / cuBLAS 实现。所以原版 Transformer 选用 Scaled Dot-Product Attention。

Q20. 这三件事的优先级怎么理解?

Self-Attention 是底层机制、Multi-Head Attention 是并行实现、Position Encoding 用于补回顺序信息。从重要性看,三者缺一不可;从理解顺序看,Self-Attention → Multi-Head → Position Encoding 是论文的逻辑顺序;从实现顺序看,PE 需要先算好(与 embedding 相加),再进入 Multi-Head Attention。

全篇总纲

  • 1 个底层机制(Self-Attention):Q / K / V → 点积 → 缩放 → softmax → 加权求和
  • 1 个工程实现(Multi-Head Attention):把 dmodel 切成 h 份并行做 Self-Attention,再拼回来
  • 1 个顺序信息源(Position Encoding):用 Sinusoidal / Learned PE 给无序的 Self-Attention 注入位置信息
  • 1 条链路:Token Embedding + PE → Multi-Head Self-Attention → 下一层 Block

Roadmap 后续预告

  • 下篇预告:Transformer Block 完整结构 —— 残差 + LayerNorm + Feed-Forward 三件套
  • 相关阅读:从零实现 nanoGPT —— 用 PyTorch 2.x 完整手写一个可训练的小型 Transformer
  • 扩展阅读:RoPE / ALiBi 位置编码变体 —— 把位置信息注入到注意力机制本身

posted @ 2026-07-25 07:41  左扬  阅读(19)  评论(0)    收藏  举报