难度
入门
从分词、词表、BPE 到 embedding 查表与位置对齐,建立“文本如何变成模型可计算向量”的完整直觉。
难度
入门
阅读时长
约 95 分钟
更新日期
2026/03/24
主题
Tokenizer / Embedding / 基础原理 / 预训练
读完这篇教程后,你应该能回答这些问题:
如果你已经读过 Transformer 注意力机制入门,这篇文章会把“注意力之前发生了什么”补齐。
人类看到的是文字、句子和语义,但模型本质上只能处理数字张量。要把一句自然语言送进模型,首先要解决一个基础问题:
文本应该被切成哪些最小单位?
这个单位不能太粗,也不能太细:
Tokenizer 的工作,就是在“词表规模”和“序列长度”之间找到一个可训练的折中。它并不只是一个预处理工具,而是模型感知世界的第一层抽象。
一个典型文本进入模型前,大致会经历这几步:
也就是说,模型并不是“看到词”,而是“看到 token 对应的向量”。如果 tokenizer 切分方式不同,后面整个训练轨迹都会变化。
早期 NLP 常用按词切分,但在大规模语料和多领域文本中,这种方式很容易遇到几个问题:
如果你为所有可能的词都保留一个唯一 id,词表规模会迅速膨胀;如果词表太小,又会频繁出现 OOV(out-of-vocabulary)问题。于是行业逐渐转向子词级方法。
子词方法的核心思想很朴素:
比如一个很少见的专业词,模型未必在词表里有整词,但可以被拆成几个常见片段。这让模型至少能“部分理解”这个词,而不是完全陌生。
这也是为什么现代 tokenizer 通常既不像纯按词切,也不像纯按字切,而是工作在两者之间。
BPE(Byte Pair Encoding)可以理解成一种“从小单位开始,不断合并高频相邻片段”的过程。
最初我们把文本切得很细,比如按字符或字节。然后:
经过足够多轮后,词表会逐渐形成:
例如英文里常见的前后缀、常见词根,会自然成为词表的一部分。
假设语料里有这些词:
lowlowestnewerwider一开始按字符切:
l o wl o w e s tn e w e rw i d e r如果 lo 很常见,就先合成 lo;之后 low 常见,再合成 low;后续又可能合成 er、est 这类后缀。最终词表里就会出现兼具“完整词”和“可复用子词”的结构。
这种方式的好处是,它不是人工写规则,而是从数据里长出来。
BPE 常常依赖预先定义的分词边界,而 SentencePiece 的一个重要思想是:把空格也作为普通符号的一部分,直接在原始文本流上学习切分。
它的优点包括:
很多现代模型会用 SentencePiece 的实现形式,哪怕背后的思想和 BPE、Unigram LM 有重叠。对初学者来说,最需要理解的不是库名,而是:
tokenizer 本质上是在学习“哪些片段值得成为稳定单位”。
词表大小是 tokenizer 设计里最重要的超参数之一。它直接影响三件事:
embedding 矩阵大小约为:
vocab_size × hidden_dim
词表越大,这部分参数越大。
词表越小,平均每句话会被切成更多 token,序列更长。
词表越大,越容易保留完整词或长片段;词表越小,则更依赖子词拼装。
因此,词表大小不是越大越好,也不是越小越好,而是和训练语料、语言种类、成本预算一起决定的折中。
一旦文本被切成 token id,模型下一步通常会做 embedding lookup。可以把 embedding 看成一个大表:
例如:
EmbeddingMatrix[vocab_size, d_model]
如果某个 token id 是 1523,模型就取出第 1523 行的向量作为它的初始表示。
这个向量一开始可能是随机初始化的,但在预训练中会被不断更新。最终 embedding 并不是“字典释义”,而是模型为了预测下一个 token 学出来的一种统计语义表示。
很多入门资料会说“embedding 就是词向量语义”,这没错,但容易过度简化。更准确地说:
例如 “bank” 在不同语境里含义不同,静态 embedding 无法完全解决这个问题;真正完成 disambiguation 的,是后续上下文建模。这也是为什么大模型时代,我们更常谈 contextual representation,而不是只谈静态词向量。
这点在工程上非常重要。假设模型上下文窗口固定为 8k token,那么:
这也是为什么不同 tokenizer 对同一段文本的 token 数差异,会在真实产品里转化成非常具体的成本差异。RAG、长上下文、API 计费,本质上都和 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)
这段代码非常简化,但它抓住了核心:文本先被切成离散单位,再被映射成向量。
实际上,tokenizer 会影响模型视角、上下文长度、显存成本、检索质量,属于系统级设计。
词表过大虽然能减少切分,但参数更多、泛化不一定更好,未必更划算。
embedding 只是初始表示,真正的上下文含义还要靠 Transformer 层内计算不断改写。
英文表现不错的 tokenizer,不一定适合同样规模的中文、代码或混合语料。
从相近主题继续深入,建立连续学习链路。