☰
Transformer核心机制解析:词元化、自注意力与位置编码工程实践
2026/10/17 7:25:39 网站建设 项目流程

1. 从原始文本到词元:Transformer 的输入到底长什么样

很多初学者第一次接触 Transformer 时,看结构图感觉特别宏大:Encoder、Decoder、多头注意力、位置编码……但是有个很基础的问题容易被忽略——模型拿到的"输入"到底是什么?是纯文本字符串?是 Unicode 字符?还是别的什么东西?

都不是。Transformer 本质是一个数值计算系统,它只认识数字。你给它看"猫追老鼠"这五个字,它看到的是五个整数 ID,比如 [312, 5089, 771, 216, 102]。从原始文本到这一串整数,中间经历的过程就是词元化(Tokenization)。这一步虽然在结构图上往往只画成一个"Input Embedding"小方块,但它在实际工程里踩的坑最多。

1.1 词元化的三种粒度:字符级、词级和子词级

早期 NLP 系统喜欢用词级或者字符级分词。词级分词就是按空格把句子切开,英文还能凑合用,中文没有天然空格,必须先做分词,分错了后面全都错。字符级倒是不会分错,但序列长度会暴涨,一个句子可能变成几十个字符,训练效率非常低。

现在主流方案是子词级(Subword),最常用的算法是 BPE(Byte Pair Encoding)和 WordPiece。它的思想很有意思:从字符开始,统计训练语料里相邻字符对的出现频率,把最高频的字符对合并成一个新词元,然后反复执行这个过程,直到词表达到目标大小。

我给个具体例子。假设语料里反复出现 low、lower、newest,初期字符对lo出现频率极高,就会把l和o合并成lo;接着low也高频,就再合并成low。最终你会发现,low成了一个词元,lower被拆成low+er,newest则可能是new+est或者干脆整体出一个newest词元。这样带来的好处是什么?词表不用无限膨胀,就能覆盖几乎所有可能出现的新词——遇到lowest这种没见过的词,词元化器也能拆成low+est,不会直接丢给模型一个<UNK>。

1.2 词表构建和那些"神秘"的特殊符号

做完 BPE 之后,我们会得到一张词表(Vocabulary),里面每个词元对应一个数字 ID。常见模型词表大小在 3 万到 10 万左右,中文大模型一般会更大一些,因为汉字信息密度高,词元化器会把常用汉字和常见词都收进去。

词表里除了真实词语,还有几个特殊 Token,很多人第一次看代码时经常被它们弄晕。常见的包括:

  • <s>或[CLS]:序列开头标记,类似"这句话从这里开始"。
  • </s>或[SEP]:序列结束标记,也常用来分隔两段不同的句子,比如问答任务里问题和回答的分界。
  • <pad>:补齐用的占位符。一个 batch 里有长有短,要把短的补到和最长的一样长,padding 就用它。
  • <unk>:未知词元的兜底。
  • <mask>:掩码词元,预训练任务里把某个词盖住让模型猜测时用。

我建议你把词元化器的源码打印出来,亲手对一个中文句子跑一次,看看每个词元分别对应什么 ID。这个"表面功夫"做透了,后面的注意力、位置编码才不会觉得飘。

1.3 从文本到 ID 的完整链路

现在我们把整个过程串起来。一条原始文本进入模型前要经过这几步:

  1. 归一化:统一大小写、全半角转换、去除多余空白。中文场景这块基本不用动,英文场景要注意。
  2. 预分词:用规则粗切一刀,比如英文按空白和标点切,中文按字切。
  3. BPE/WordPiece 分词:把粗切的结果继续按子词规则拆解。
  4. 映射 ID:查词表,把每个子词转成整数 ID。
  5. 添加特殊 Token:在开头和结尾分别补上<s>和</s>。
  6. 构造注意力掩码(Attention Mask):标记哪些位置是真实词元、哪些位置是 padding。

到这一步,输入才真正从"人读得懂的文字"变成了"模型读得懂的张量"。张量形状一般是(batch_size, seq_len),里面全是整数。

实际写代码时,不需要自己从头实现 BPE——主流的 huggingface transformers 库的 tokenizer 已经封装好了,你只需要选好词元化器,调一个tokenizer(text)就能拿到 input_ids 和 attention_mask。但如果你想真正搞懂 Transformer,我强烈建议你读一遍分词器的配置文件和词表文件,知道每个字段是干什么用的,这会在后面排查生成异常时省下很多时间。

2. 嵌入(Embedding)与位置编码:让离散 ID 成为空间中的向量

输入拿到手,是一串整数 ID。如果直接拿整数往模型里送,会出现一个严重问题:整数 0 和整数 1 的关系(相邻)跟词汇之间的关系("苹果"和"香蕉"相关,"苹果"和"宇宙飞船"不相关)没有对应性。模型不知道 312 和 313 是否语义相近。这时候就需要嵌入层登场。

2.1 Embedding 的作用:从"编号"到"语义向量"

Embedding 本质是一张可学习的查找表,形状是(vocab_size, hidden_dim)。每个词元 ID 在表里查一行向量,这个向量就是一个低维稠密的数值表示。

这里的 hidden_dim(也叫 d_model)是 Transformer 里的核心超参数之一。经典 BERT base 用 768,大一点的用 1024,常见的 7B 规模大模型内部维度一般是 4096。维度越大,能容纳的语义信息越丰富,但参数量和计算量也在涨。

关键点是:嵌入向量不是先验设定好的,而是在预训练过程中学出来的。训练完成后你会发现,语义相近的词在向量空间里距离很近,比如"猫"和"猫咪"几乎挨在一起,而同义词/反义词之间的关系也常常可以用向量运算表达——"国王 - 男 + 女"的结果会落在"女王"附近。这就是嵌入层最有魅力的属性。

2.2 为什么必须有位置编码:自注意力不分先后顺序

这一节可以说是理解 Transformer 设计动机最关键的一块。我在带新人时经常问一个问题:如果句子是"猫追老鼠"和"老鼠追猫"——词元完全一样,只是顺序不同,模型的输出能不同吗?

在纯自注意力机制下,答案是:不能区分。我们把输入词元集合放进模型,注意力矩阵的计算本质是在做词与词之间的"关联打分",它对集合内的元素是对称的,也就是说如果你对输入做任意置换,输出张量的内容不变。这种性质叫置换不变性。对很多任务来说这很致命:语言是极度依赖顺序的。

RNN 没有这个问题,因为它是按时间步逐个处理输入的,先天带有顺序。CNN 也没有,因为卷积核有局部的空间感受野。但 Transformer 抛弃了循环结构,全部并行计算,所以必须显式地把"位置信息"揉进输入里。

2.3 位置编码的主流方案:从正弦编码到相对位置编码

最初论文里用的是三角函数编码,公式长这样:位置 p 的第 i 维编码 = sin(p / 10000^(2i/d)) 或 cos(p / 10000^(2i/d))。

这个设计的巧妙之处在于:不同维度拥有不同的波长,这就可以用三角恒等式让模型在任意固定偏移量 k 的条件下,通过线性变换从位置 p 的位置向量推导出位置 p+k 的位置向量。翻译成大白话:它能告诉模型"第 5 个词和第 8 个词之间差了 3 个位置",这种相对位置信息在很多任务里比绝对位置更有用。

后来实践证明,直接用可学习的位置嵌入(也就是把位置 ID 也查表变成向量)效果也差不多,甚至在某些任务上更好。BERT、GPT 这类模型都用的是可学习位置嵌入。再新一点的方案,比如旋转位置编码(RoPE)和 ALiBi,思路是把位置信息直接注入注意力打分过程,而不是加在输入向量上。RoPE 现在是很多大模型的标准配置,它对长文本泛化能力比绝对位置编码强,推理时也更容易做长度外推。

实操中,除非你自己从头搭建模型,否则位置编码通常不用手工实现。huggingface transformers 和各类大模型框架(如 DeepSpeed、vLLM)都内置好了。但你要注意 max_position_embeddings 这个参数——它限制了模型能处理的序列长度,比如 512 位置的 BERT 就没法直接处理 1024 个词的输入。这也是为什么长文本场景需要 RoPE 这类可外推的位置方案。

2.4 增强输入:隐藏层向量拼接

词嵌入加上位置编码之后,得到的就是 Transformer 真正使用的输入向量了。这里有个工程细节值得单独说:有的模型会把 token_type_ids(句子类型标记,区分第一句和第二句)也加进来,不过新一代模型大多已经不再需要这个了。整体输入向量的维度还是 hidden_dim,但它是"词义 + 位置"两个信息的叠加。

顺便提一个常见的困惑:既然位置信息加在 embedding 上,那是不是会"污染"词向量?实际不会,因为模型在训练过程中会自动学会"哪部分维度主要编码语义、哪部分维度主要编码位置",这种解耦虽然不像手写规则那么干净,但对下游任务来说完全够用。

3. 自注意力机制:Transformer 的心脏怎么跳动

前面都是准备工作。真正让 Transformer 在不同领域里都能大放异彩的核心,就是自注意力(Self-Attention)机制。后面各种改进模型——不管叫什么名字——绝大多数都绕不开这一步。你如果把这个模块吃透,去看其他变体都是小菜一碟。

3.1 Q、K、V 到底在干嘛:一个信息检索的比喻

很多人学 QKV 时被公式吓住,其实它的思想特别接地气。你可以想象一个档案检索场景:

  • Query(查询)是你在检索框里输入的问题。
  • Key(键)是每份档案的标签索引。
  • Value(值)是档案的实际内容。

系统做的事是:拿你的 Query 去跟每个 Key 做相似度匹配,算出每个档案跟你的问题有多相关,然后按相关性权重把 Value 加权求和,得到最终输出。

Transformer 里的自注意力完全一致:输入序列里每个词元会生成三个向量——Query、Key、Value。比如"猫在沙发上睡觉"这句话里,"猫"这个词元的 Query 会去跟"沙发"的 Key 做匹配,如果匹配度高(猫和沙发有语义关联),那么"猫"对应的输出向量里就会注入来自"沙发"的信息。

这三个向量怎么来?很简单,用三个可学习的权重矩阵 WQ、WK、WV,对输入向量做矩阵乘法:Q = X·WQ,K = X·WK,V = X·WV。

3.2 缩放点积注意力的计算细节与"为什么除以根号 d_k"

有了 Q、K、V,下一步就是计算注意力权重。过程分三步:

  1. 计算 Q 和所有 K 的点积:得分越高,代表关联越强。
  2. 对得分做缩放:除以 K 维度 d_k 的平方根。
  3. 过 Softmax:归一化成和为 1 的注意力权重。
  4. 用权重对所有 V 加权求和:得到该位置的最终输出。

我重点讲一下那个缩放为什么要做。假设 Q 和 K 的每个元素都是均值 0 方差 1 的随机变量,那么点积的结果方差就是 d_k(因为 d_k 个独立随机变量求和)。d_k 越大,点积的数值范围就越大,Softmax 会趋于饱和——就是一个权重几乎为 1,其他接近 0。梯度到了饱和区会非常小,训练基本停滞。除以根号 d_k 的作用就是让点积方差恢复到 1 左右,Softmax 曲线不至于太陡,梯度就能正常回传。

这也是训练 Transformer 时一个经典调参经验:如果你发现模型训练前期损失降得很慢,或者梯度消失,先看看你用的缩放因子对不对。

3.3 多头注意力:让模型"多视角"看输入

单个注意力机制学到的匹配模式是单一的。比如它可能只顾语义关联,忽略了"这个位置离得很近"这种局部的语法关系。多头注意力(Multi-Head Attention)的解决思路很直接:把 Q、K、V 分别切成多个头(典型是 8 或 16 个),每个头独立做注意力计算,最后把所有头的输出拼接起来,再过一层线性变换。

每个头可以学到不同的关系模式。有一篇经典的可视化文章展示过:有的头负责捕捉句法依赖(比如名词和修饰它的形容词),有的头负责捕捉指代关系(比如代词指向的实体),有的头负责捕捉位置距离模式。这种多视角机制就是多头注意力的本质优势。

工程上要注意:多头并不是加大维度,而是把原来的 hidden_dim 按头数拆分,比如 d_model=768,8 个头时每个头是 96 维。所以多头注意力的参数量和计算量不会线性增加太多,但表达能力显著增强。

3.4 前馈网络、残差连接与层归一化

自注意力层输出后,还要过两层全连接网络(FFN,前馈网络),中间夹一个激活函数,常见配置是FFN(x) = max(0, xW1 + b1)W2 + b2,即 GELU 或 ReLU 激活。这个 FFN 做的事有点像"对注意力聚合后的信息再做非线性加工",把每个位置的向量独立变换一遍,起到局部特征提炼的作用。Transformer 里大约三分之二的参数都集中在 FFN 里,它虽然看起来不起眼,但能力不小。

同时每个子层外面都套着两个标准部件:残差连接(Residual Connection)和层归一化(Layer Normalization)。

残差连接是output = sublayer(x) + x,解决深层网络梯度回传困难的问题,让信息可以直接从浅层跳跃传到深层。

层归一化把向量分布拉回均值 0 方差 1,稳定训练。这里有个细节值得记录:像 BERT 这种早期模型用 Post-LN(先过子层再过归一化),而很多新模型用 Pre-LN(先归一化再过子层)。Pre-LN 训练更稳定,可以用更大的学习率,但火候没有 Post-LN 好,需要调参。我实际训练小模型时,默认会用 Pre-LN 保证稳定,再慢慢把学习率拉上去。

到这里,一个完整的 Transformer Encoder 层就串起来了:输入 -> 多头自注意力 -> 残差 -> 层归一化 -> 前馈网络 -> 残差 -> 层归一化 -> 输出。这个块反复堆叠,比如 BERT 堆 12 层,更大的模型堆 24 层甚至 48 层。

4. Decoder 与完整流程:从编码到生成

Encoder 理解输入,Decoder 才是真正负责生成的那部分。理解 Encoder 和 Decoder 的区别,对推理场景极其重要。

4.1 Encoder-only、Decoder-only 与 Encoder-Decoder 的选型

Transformer 最初是为机器翻译设计的,结构是 Encoder-Decoder 对称架构。但后来大家发现,很多任务只需要其中一半:Bert 是 Encoder-only,适合理解任务(分类、抽取、匹配);GPT 是 Decoder-only,适合生成任务(对话、续写、翻译);而最初的翻译模型本身就是 Encoder-Decoder。

选型经验:做分类和序列标注用 Encoder-only,做文本生成用 Decoder-only,如果要做类似翻译这种"读一段、写一段"的任务,Encoder-Decoder 通常更稳。不过现阶段大语言模型基本都是 Decoder-only,它可以靠统一的 next-token prediction 目标处理几乎所有任务。

4.2 Decoder 里的三个子层:掩码自注意力、交叉注意力与前馈网络

Decoder 比 Encoder 多一个交叉注意力(Cross-Attention)子层。它长在 Encoder 的顶层输出和 Decoder 的掩码自注意力之间,Query 来自 Decoder,Key 和 Value 来自 Encoder 的输出。这样解码器在生成每个词时,都能有选择地"回看"输入序列的对应部分。比如翻译英文句子到中文,生成到"猫"这个词时,交叉注意力会把"cat"以及"猫"附近上下文的信息加权聚拢进来。

Decoder 的自注意力是带掩码的——每个位置只能看到当前位置及其左边的词元,不能偷看右边。这叫因果掩码(Causal Mask),保证训练时模拟的生成行为跟真实推理一致。

对比一下就很清楚了:Encoder 的自注意力可以被任意两个位置的词互相看到(双向),Decoder 的只能从左往右看(单向)。这也是为什么 BERT 双向能做好"完形填空",而 GPT 单向适合续写。

4.3 输出层:从隐藏向量到下一个词的分布

Decoder 最后一层输出的隐藏向量,形状是(batch_size, seq_len, hidden_dim)。它还不是词语,需要过一个线性层把 hidden_dim 映射到词表大小,得到 logits,然后接 Softmax 变成概率分布:每个词在所有词元上的概率,概率最高的就是模型认为最可能的下一个词。

实际生成是逐词循环的:模型输入前 N 个词,输出第 N+1 个词的概率分布,选出一个词(或按某种策略采样),把新词追加到序列末尾,再送进模型。这个"自回归生成"过程会一直持续到遇到结束符或者达到 max_new_tokens 上限。

4.4 解码策略:贪心、Beam Search 与采样

生成过程中"怎么选下一个词"并不是直接取概率最高的那个这么简单。实际有几种主流策略:

  • 贪心解码:每次选概率最高的词。速度快,但容易陷入重复——一旦选了某个词,后续概率分布往往导向同样的循环,导致生成结果乏味或死循环。
  • Beam Search:每一步保留概率最高的 top-k 条候选序列,最后选整体概率最高的。在翻译、摘要这类对准确性要求高的任务里表现很好,但计算开销大。
  • 温度采样:把 logits 除以温度 T 再 Softmax,T 大于 1 时分布变平坦,生成更随机;T 小于 1 时变尖锐,生成更保守。T 很低时退化成贪心。
  • Top-K 采样:从概率最高的 K 个候选里采样,避免把低概率词也纳入。
  • Top-P(核采样):从累计概率达到阈值 P 的最小候选集合里采样,比固定 Top-K 更自适应。

我在用大模型做创作、对话时,通常设置 T=0.7~0.9、Top-P=0.9 左右;做代码生成或翻译时 T 调低到 0.2~0.3;做事实性问答有时候直接 T=0.0 让它确定性输出,防止幻觉。这些参数是生成质量的最大杠杆,比改模型权重立竿见影。

5. 工程师视角:完整流程中的性能优化与问题排查

看懂流程图不代表能跑通项目。这节聊聊我在实际训练和推理 Transformer 时踩过的坑,包括显存、速度、质量和稳定性四个方面。

5.1 训练阶段的显存与速度优化

Transformer 显存消耗大头在注意力矩阵。如果序列长度为 L,自注意力分数矩阵就是(batch_size, heads, L, L)。L 翻倍,显存占用直接涨四倍。这也是为什么早期的 BERT 最多只能处理 512 个词。到长文本时代,业界给的方案五花八门,工程上最见效的是:

  • 梯度累积:显存不够时,不一次性用大 batch,而是分几个小 batch 逐步累积梯度,效果等价于大 batch,但省显存。注意要保证梯度归一化的分母是实际累计的总序列数,不然学习率对应不上。
  • 混合精度训练(FP16/BF16):显存减半,速度翻倍。前提是给模型里容易溢出的部分(如 loss 计算)保留 FP32,否则可能会出现 loss 突然变成 NaN 的问题。
  • 激活重计算:不保存中间激活值,反向传播时重新算一遍。省显存但耗算力,适合显存紧张且算力富余的场景。
  • 序列打包:把多个短样本在一个序列里拼接训练,减少 padding 带来的浪费。需要注意别让注意力穿跨样本边界,配合 block-diagonal 掩码。

5.2 训练不稳定:warmup、梯度裁剪与权重初始化

新手训练 Transformer 最常见的失败表现是 loss 一开始就爆掉,或者训着训着变 NaN。我总结的几个典型原因和对应解法:

  1. 学习率设置过高:Transformer 对学习率极其敏感。我的经验是 base model 用 1e-4 到 3e-4,large model 用 1e-5 到 3e-5,而且必须配合 warmup。
  2. 缺少 warmup:前几千步用很小的学习率"预热",让模型先适应数据分布,然后线性升至峰值,之后再衰减。没有 warmup 直接冲,很早就发散。
  3. 梯度裁剪:设置 max_grad_norm 比如 1.0,防止个别样本产生巨大梯度把参数推出正常区域。
  4. 数值溢出:如果用 FP16 训练,logits 很容易溢出。最简单有效的方法是在 loss 层用 FP32 计算 CrossEntropy,或者用 loss scaling 自动调整。

5.3 推理阶段:KV Cache 与量化

推理自回归生成时,你会发现每生成一个新词都要把前面的整段序列重新过一遍模型,效率极低。KV Cache 的思路是把前面已经计算好的 Key 和 Value 缓存下来,每次只计算新位置对应的部分,时间复杂度从 O(L²) 降到 O(L)。绝大多数现代推理框架默认开启。

另一个常用优化是量化。把模型权重从 FP16 压到 INT8 或 INT4,显存占用变小,推理速度提升,但精度会有轻微损失。在一些对延迟敏感的产品场景,量化是免不了的选项。我用过主流量化库,在 7B 规模模型上 INT8 量化后困惑度只下降零点几,但显存占用几乎减半,性价比相当高。

5.4 常见问题速查表

现象可能原因排查与解决办法
训练 loss 不降学习率太低或 warmup 太长拉高学习率,检查 warmup 步数与数据集规模的匹配
训练中途 loss 变成 NaN数值溢出、学习率过大启用 FP32 loss 计算、降低学习率、增加梯度裁剪
生成重复文本贪心解码、温度太低改用 Top-P 采样,T=0.8~1.0,或使用 repetition_penalty
生成长度很短就结束max_new_tokens 太小、结束符概率过高调大 max_new_tokens,检查特殊 Token 的生成行为
中文乱码/特殊符号词元化器选错、vocab 不匹配确认分词器文件名和模型匹配,检查特殊 Token ID
位置越界报错输入超过 max_position_embeddings换用 RoPE 模型或做输入截断、滑动窗口
推理显存爆掉没有开 KV Cache、序列过长开 KV Cache 和量化,或减小 batch

6. 我从零手搭 Transformer 的几个复盘体会

6.1 不要直接照搬别人的模型代码

我第一次搭 Transformer 时,直接复制了一份仓库代码,能跑,但改一个超参数就报错,根本不知道内部发生了什么。后来我先从 Embedding 开始,手写了多头注意力和位置编码的完整前向逻辑,再写训练脚本在玩具数据集(比如字符级文本)上训练一个小模型做"句子续写"。那个模型大概只有 2000 万参数,但训练 1 个小时就明显能生成通顺句子。

这种"最小可运行版本"的搭建思路非常建议大家试一次。它逼你把公式里的每一步变成代码,你会非常直观地看到 Q、K、V 三个张量的形状怎么从(batch, seq, dim)变成(batch, heads, seq, head_dim)的,也会搞明白为什么 Reshape 和 Transpose 在注意力实现里如此关键。

6.2 调试注意力权重是最高效的排查方式

模型出现异常行为,比如没有关注该关注的位置、生成的句子上下文不搭,最快定位方式是打开注意力权重热力图。用一个小脚本把某个 Transformer 层在某个输入上的注意力矩阵打印成图片,你能一眼看出哪些位置形成强关联。如果猫在追老鼠的句子里,模型把"猫"和"沙发"关联得很紧但没怎么关注"追",说明训练数据或者位置编码可能有问题。

6.3 常见工程的参数选择参考

以我在中等规模(1-2 亿参数)模型上的实践为参考,列出几个靠谱默认值:

  • hidden_dim:768
  • num_heads:12
  • num_layers:12
  • d_ff(FFN 中间层维度):3072
  • dropout:0.1
  • 学习率:1.5e-4
  • warmup 比率:6%
  • max_grad_norm:1.0

这些数值匹配 Raspberry 单机 8GB 显存以内的训练环境。如果规模更大,建议遵循"维度翻倍时层数减半"这类经验公式,而不是盲目堆大。

6.4 后续扩展方向非常多

Transformer 的框架学习到这里,已经拥有了所有主干的细节。后续你可以往以下方向做深度扩展:注意力机制的稀疏化(如 Longformer)、线性注意力(简化到 O(N))、混合专家模型(MoE)、以及对多模态输入的扩展(图文、语音、视频等)。但无论扩展方向怎么变,从输入嵌入到输出分布的基础链路始终不变。把这层地基打牢,学任何新模型都会快得多。

就我个人经验来说,如果你真想弄懂 Transformer,最好的方式不是反复读论文,而是挑一个玩具数据集,自己从零写出前向过程,再手动实现一次自回归生成。等你亲手把"猫追老鼠"这句话经过 embedding、注意力、前馈网络、softmax,最终输出"狗追猫"的概率分布时,那些曾经着急记不住的概念,就已经变成你心里顺理成章的东西了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询