从 NLP 基础到 Transformer:我先补的几块地基

整理文本表示的演化路径,以及手写 Transformer 时最容易卡住的九个问题

进入大语言模型之前,我先补两块东西:文本如何变成模型能处理的向量,以及 Transformer 如何用注意力机制建模上下文。这两件事没理顺,后面看 tokenizer、embedding、mask、训练目标和生成过程时很容易断线。

这篇文章分成两部分:先梳理文本表示从统计方法到上下文化表示的演化,再整理手写 Transformer 时最容易混淆的九个实现问题。复习时重点看三条线:张量形状怎样变化、mask 怎样限制信息流、Encoder/Decoder 各自承担什么任务。


一、温故知新:文本表示是如何进化到大模型时代的?

在接触大模型结构之前,先回答一个底层问题:文本怎样被表示成模型可以计算的对象?

1.1 NLP 范式的演进

NLP 的技术路线可以粗略分成三段。每一段都在回答同一个问题:如何把语言现象变成可计算、可学习的形式。

  1. 规则方法(手写模板):早期需要语言学专家人工定义无数规则。
  2. 统计方法(概率模型):利用马尔可夫模型、隐马尔可夫模型等,让模型学习词串在语料库中出现的概率分布。
  3. 深度学习方法(端到端):采用神经网络构建端到端的系统,无需繁杂的特征工程。

1.2 文本表示的“进化链”

大模型能处理长文本和上下文,前提是文本表示已经从稀疏统计特征走到稠密、上下文化向量。复习时我按这条链看:

方法核心思想特点与局限
VSM (词袋模型)将文本表示为高维稀疏向量,基于词频统计。忽略了词语的先后顺序,也完全丢失了语义相似性信息。
N-gram考察连续 n 个词的共现概率。能捕捉一定的局部词序,但面临严重的维度爆炸和数据稀疏问题。
Word2Vec用稠密、低维的向量表示词语,语义相近的词距离更近。静态表示(Static Embedding),每个词具有固定的向量表征,无法解决“一词多义”问题。
ELMo基于双向 LSTM,根据上下文动态生成词向量。实现了动态表示,但受限于 RNN 架构,长距离依赖的建模能力仍然有限。

RNN/LSTM 按时间步递归处理序列,不利于大规模并行,也很难稳定抓住超长距离依赖。Transformer 的自注意力机制改成一次性比较序列中所有 token 之间的关系,所以更适合并行训练和长上下文建模。


二、直面硬核:Transformer 学习过程中的 9 个迷思与解答

学习 Transformer 时,真正容易卡住的不是公式,而是公式落到代码以后:每个张量在哪一层、是什么形状、表示什么语义。下面九个问题都围绕一件事:把结构设计和实现细节对上。

复习时可以把这些问题当成检查表:能回答清楚,就说明注意力、mask、残差、位置编码和编解码器分工已经基本接上。

Q1:关于多头注意力参数的组织方式

为什么通常使用三个“组合特征矩阵”(比如 $W_Q, W_K, W_V$)而不是给每个注意力头分配单独的矩阵?

在实现时,代码经常直接使用 nn.Linear(d_model, 3 * d_model) 或等价组合。这是因为 $X \cdot W_{combined}$ 在数学上完全等价于先分别对每个头进行特征映射然后再将结果拼接起来。使用组合矩阵能够在 GPU 上将所有的计算打包成一次矩阵乘法(matmul),极大提升了计算并行度与效率。

Q2:Causal Mask(因果掩码)的切片逻辑

代码中经常看到 scores = scores + mask[:, :, :seqlen, :seqlen],这样做的意义是什么?

因果掩码用来防止 Decoder 在自回归生成时“偷看未来”。通常这个 mask 张量会被构造成一个上三角全为 $-\infty$、下三角(含对角线)全为 $0$ 的矩阵。 把它加到注意力分数 scores 后再做 softmax,未来位置的权重就会变成 $0$。最后的切片 :seqlen, :seqlen 是为了从预分配的大掩码里,取出当前序列长度真正需要的那一块。

Q3:残差连接到底发生在什么位置

经常看见 Attention 模块里面有 self.wo 和 resid_dropout,它是直接在 Attention 里计算残差吗?

这是一个常见误解。Attention 模块内部通常只做注意力计算、输出投影 self.wo 和 dropout,并不直接把输入 x 加回来。残差连接一般发生在外层的 EncoderLayer 或 DecoderLayer 中,以 x + sublayer_output 或 x + dropout(sublayer_output) 的形式完成。

Q4:Encoder 与 Decoder 到底差在哪

作为整体,它们的分工到底有什么不同?

  • Encoder 负责建模源序列内部的上下文。它是一个“通读全文”的结构,通常不需要因果掩码。
  • Decoder 在生成时必须遵守时间顺序。它先用带掩码的自注意力层(Masked Self-Attention)看自己已经生成的历史,再用交叉注意力层(Cross-Attention)读取 Encoder 的输出,最后通过前馈网络(FFN)得到用于预测下一个词的隐状态。

Q5:Self-Attention 🆚 Cross-Attention

自注意力和交叉注意力的结构区别在哪里?

属性Self-Attention (自注意力)Cross-Attention (交叉注意力)
Q的来源当前序列内部目标序列(Decoder 端)
K/V的来源当前序列内部源序列(Encoder 端)
解决的问题“我这句话里的词之间,谁和谁有什么联系?”“我在生成当前目标词时,应该把注意力放在上一句(源句)的哪几个词上?”

Q6:为什么 Decoder 必须要设计 Cross-Attention

只使用自回归生成不行吗?为什么还要多加一层交叉注意力?

如果只靠 Masked Self-Attention,Decoder 只能看到目标侧已经生成的历史 token,无法直接利用源序列信息。Cross-Attention 让 Decoder 在每个生成位置都用当前目标侧状态作为 Query,去 Encoder 输出中检索相关的 Key/Value。这个机制相当于软对齐:不需要人工指定源词和目标词的对应关系,模型会通过注意力权重自己选择源端信息。

Q7:位置编码(Positional Encoding)的直觉理解

Transformer 摒弃了 RNN 的时序结构,那它是怎么解决词语顺序问题的呢?

注意力机制本身只计算 token 之间的相似度,不天然包含顺序信息。如果不加入位置编码,模型很难区分“猫追狗”和“狗追猫”这类词相同但顺序不同的句子。

位置编码的作用是把位置信息放进 token 表示里。将位置编码向量加到词嵌入后,模型在计算注意力时既能比较“token 内容是什么”,也能利用“token 在哪个位置”。Sin/Cos 绝对位置编码使用不同频率的三角函数表示位置;三角函数有平移关系,所以它也能帮助模型推断相对距离。

Q8:register_buffer 在 PyTorch 实现中的意义

有些模型参数会使用 register_buffer,这是为什么?

register_buffer 用于注册那些随模型保存(会被序列化进入 state_dict)并且会随模型调用 to(device) 自动迁移到 GPU,但不参与梯度更新(不可训练) 的状态张量。比如上面提到的、预先计算好的三角位置编码缓存以及因果掩码矩阵,这就是典型的使用场景。

Q9:view 操作的作用场景

在多头注意力实现中,为什么最后经常要调用 view?

多头注意力需要把隐藏层维度拆成多个头。例如把 [batch, seqlen, dim] 改成 [batch, seqlen, n_heads, head_dim],其中 dim = n_heads * head_dim。view 的作用是重新解释张量形状,不改变元素总数,也不改变底层数据。复习时要记住:view 解决的是形状组织问题,不是新增参数或复制语义。


三、接下来还想继续补上的问题

复习这篇时,我会把问题压成三句:

  1. 文本表示的演化,是从稀疏词频走向上下文化向量。
  2. Transformer 用注意力计算 token 间关系,再用 mask 和位置编码约束信息流。
  3. 手写实现时,最该盯住张量形状、残差所在层级、Encoder/Decoder 的信息来源。

接下来继续往下学时,重点就会自然转向三条路线:BERT 的 Encoder-Only、T5 的 Encoder-Decoder、GPT/LLaMA 的 Decoder-Only。

使用 Hugo 构建
主题 Stack 由 Jimmy 设计