词嵌入与 Tokenizer 原理

从分词、词表、BPE 到 embedding 查表与位置对齐,建立“文本如何变成模型可计算向量”的完整直觉。

难度

入门

阅读时长

约 95 分钟

更新日期

2026/03/24

主题

Tokenizer / Embedding / 基础原理 / 预训练

先修知识

基础概率与频率统计向量与矩阵基础

学习目标

读完这篇教程后,你应该能回答这些问题:

  1. 为什么大模型不能直接“按字面字符串”读文本,而必须先做 tokenization。
  2. BPE、SentencePiece 这类 tokenizer 到底在做什么,它们为什么比按词切分更稳。
  3. token id、词表大小、embedding 向量之间是什么关系。
  4. 为什么 tokenizer 设计会直接影响训练成本、上下文长度和下游效果。

如果你已经读过 Transformer 注意力机制入门,这篇文章会把“注意力之前发生了什么”补齐。

为什么 Tokenizer 是大模型的第一层接口

人类看到的是文字、句子和语义,但模型本质上只能处理数字张量。要把一句自然语言送进模型,首先要解决一个基础问题:

文本应该被切成哪些最小单位?

这个单位不能太粗,也不能太细:

  • 如果按整词切分,词表会爆炸,未登录词问题严重。
  • 如果按单字符切分,序列会变得很长,模型更难学习词级模式。

Tokenizer 的工作,就是在“词表规模”和“序列长度”之间找到一个可训练的折中。它并不只是一个预处理工具,而是模型感知世界的第一层抽象。

从字符串到 token id:最基础的流程

一个典型文本进入模型前,大致会经历这几步:

  1. 原始文本规范化,例如大小写、空格、Unicode 处理。
  2. 根据 tokenizer 规则切分成 token 序列。
  3. 把每个 token 映射到词表里的整数 id。
  4. 用这些 id 去查 embedding 矩阵,得到向量表示。
  5. 再叠加位置编码,送入 Transformer。

也就是说,模型并不是“看到词”,而是“看到 token 对应的向量”。如果 tokenizer 切分方式不同,后面整个训练轨迹都会变化。

为什么“按词切分”在大模型里不够好

早期 NLP 常用按词切分,但在大规模语料和多领域文本中,这种方式很容易遇到几个问题:

  • 词表过大,参数占用高。
  • 稀有词、拼写变体、专有名词太多,未登录词泛滥。
  • 多语言和代码场景里,“词”的边界并不稳定。

如果你为所有可能的词都保留一个唯一 id,词表规模会迅速膨胀;如果词表太小,又会频繁出现 OOV(out-of-vocabulary)问题。于是行业逐渐转向子词级方法。

子词方法为什么有效

子词方法的核心思想很朴素:

  • 高频模式保留为较大的单位,提高效率。
  • 低频词拆成更小片段,避免 OOV。

比如一个很少见的专业词,模型未必在词表里有整词,但可以被拆成几个常见片段。这让模型至少能“部分理解”这个词,而不是完全陌生。

这也是为什么现代 tokenizer 通常既不像纯按词切,也不像纯按字切,而是工作在两者之间。

BPE:最常见的子词构造方法

BPE(Byte Pair Encoding)可以理解成一种“从小单位开始,不断合并高频相邻片段”的过程。

最初我们把文本切得很细,比如按字符或字节。然后:

  1. 统计语料中最常见的相邻片段对。
  2. 把这个片段对合并成一个新 token。
  3. 重复这个过程很多轮。

经过足够多轮后,词表会逐渐形成:

  • 高频完整词
  • 高频词缀
  • 高频子词片段

例如英文里常见的前后缀、常见词根,会自然成为词表的一部分。

一个最小 BPE 直觉例子

假设语料里有这些词:

  • low
  • lowest
  • newer
  • wider

一开始按字符切:

  • l o w
  • l o w e s t
  • n e w e r
  • w i d e r

如果 lo 很常见,就先合成 lo;之后 low 常见,再合成 low;后续又可能合成 erest 这类后缀。最终词表里就会出现兼具“完整词”和“可复用子词”的结构。

这种方式的好处是,它不是人工写规则,而是从数据里长出来。

SentencePiece 和 BPE 有什么不同

BPE 常常依赖预先定义的分词边界,而 SentencePiece 的一个重要思想是:把空格也作为普通符号的一部分,直接在原始文本流上学习切分。

它的优点包括:

  • 更适合多语言和无显式分词边界的文本。
  • 训练和推理流程更统一。
  • 在工业落地里更方便做端到端 tokenizer 训练。

很多现代模型会用 SentencePiece 的实现形式,哪怕背后的思想和 BPE、Unigram LM 有重叠。对初学者来说,最需要理解的不是库名,而是:

tokenizer 本质上是在学习“哪些片段值得成为稳定单位”。

词表大小到底意味着什么

词表大小是 tokenizer 设计里最重要的超参数之一。它直接影响三件事:

1. embedding 参数量

embedding 矩阵大小约为:

vocab_size × hidden_dim

词表越大,这部分参数越大。

2. 序列长度

词表越小,平均每句话会被切成更多 token,序列更长。

3. 表达粒度

词表越大,越容易保留完整词或长片段;词表越小,则更依赖子词拼装。

因此,词表大小不是越大越好,也不是越小越好,而是和训练语料、语言种类、成本预算一起决定的折中。

token id 到 embedding:查表到底在做什么

一旦文本被切成 token id,模型下一步通常会做 embedding lookup。可以把 embedding 看成一个大表:

  • 行数是词表大小
  • 列数是隐藏维度

例如:

EmbeddingMatrix[vocab_size, d_model]

如果某个 token id 是 1523,模型就取出第 1523 行的向量作为它的初始表示。

这个向量一开始可能是随机初始化的,但在预训练中会被不断更新。最终 embedding 并不是“字典释义”,而是模型为了预测下一个 token 学出来的一种统计语义表示。

为什么 embedding 不等于“真正语义”

很多入门资料会说“embedding 就是词向量语义”,这没错,但容易过度简化。更准确地说:

  • embedding 是模型的初始离散符号映射。
  • 真正进入深层后,每层隐藏状态都会继续被上下文改写。

例如 “bank” 在不同语境里含义不同,静态 embedding 无法完全解决这个问题;真正完成 disambiguation 的,是后续上下文建模。这也是为什么大模型时代,我们更常谈 contextual representation,而不是只谈静态词向量。

为什么 tokenizer 会影响上下文长度和成本

这点在工程上非常重要。假设模型上下文窗口固定为 8k token,那么:

  • tokenizer 切得越碎,同一篇文档占用的 token 就越多。
  • token 越多,训练和推理成本都更高。
  • 长文档场景下,检索和 prompt 组装也会受到影响。

这也是为什么不同 tokenizer 对同一段文本的 token 数差异,会在真实产品里转化成非常具体的成本差异。RAG、长上下文、API 计费,本质上都和 tokenizer 强相关。

代码最小示例:手写一个极简 tokenizer 流程

下面这段 Python 并不是工业实现,只是帮助你把“切分 + 映射 + 查表”这三步串起来:

tokens = ["<pad>", "<unk>", "Trans", "former", "is", "great"]
vocab = {token: idx for idx, token in enumerate(tokens)}

text_pieces = ["Trans", "former", "is", "great"]
token_ids = [vocab.get(piece, vocab["<unk>"]) for piece in text_pieces]

embedding_table = {
    0: [0.0, 0.0, 0.0],
    1: [0.1, 0.1, 0.1],
    2: [0.9, 0.2, 0.1],
    3: [0.8, 0.3, 0.2],
    4: [0.4, 0.7, 0.6],
    5: [0.6, 0.9, 0.5],
}

embeddings = [embedding_table[token_id] for token_id in token_ids]

print(token_ids)
print(embeddings)

这段代码非常简化,但它抓住了核心:文本先被切成离散单位,再被映射成向量。

常见误区

1. 认为 tokenizer 只是前处理细节

实际上,tokenizer 会影响模型视角、上下文长度、显存成本、检索质量,属于系统级设计。

2. 认为词表越大越高级

词表过大虽然能减少切分,但参数更多、泛化不一定更好,未必更划算。

3. 认为 embedding 本身就等于完整语义

embedding 只是初始表示,真正的上下文含义还要靠 Transformer 层内计算不断改写。

4. 忽视多语言和代码对 tokenizer 的影响

英文表现不错的 tokenizer,不一定适合同样规模的中文、代码或混合语料。

练习与思考题

  1. 为什么子词 tokenizer 能缓解 OOV,却不会彻底消除表达损失?
  2. 如果把词表从 32k 提高到 128k,会对 embedding 参数量和平均序列长度产生什么影响?
  3. 为什么同一个 RAG 系统在不同 tokenizer 下,可能会出现“召回内容够了,但放不进上下文”的情况?
  4. 如果你要为“中英混合 + 代码”场景设计 tokenizer,你最担心哪些边界问题?

延伸阅读

相关阅读

从相近主题继续深入,建立连续学习链路。