第四章 Tokenizer与Embedding¶
导读¶
模型不直接处理文字,它处理的是**整数 id 序列**。把文字变成 id 序列的工具就是 tokenizer。本章解释 tokenizer 的工作原理(BPE / WordPiece / SentencePiece)、词表(vocabulary)的设计、embedding 的查表机制,以及上下文窗口(context window)的含义。
4.1 为什么需要 Tokenizer¶
神经网络只能处理数值。把自然语言文本变成模型可处理的张量,分两步:
- 分词(Tokenization):把字符串切成一个个离散单元(token);
- 嵌入(Embedding):把每个 token 映射到一个稠密向量。
text = "今天天气不错"
tokens = tokenizer.encode(text) # [1234, 5678, 910]
input_ids = torch.tensor([tokens]) # [1, 3]
embeddings = model.embed_tokens(input_ids) # [1, 3, d_model]
4.2 分词的三个层次¶
| 方法 | 粒度 | 问题 |
|---|---|---|
| 字符级 | 每字符 | 序列太长,模型要学语义单元 |
| 词级 | 整词 | 词表爆炸,OOV(未登录词)多 |
| 子词级 | 字符与词之间 | 平衡——现代 LLM 共识 |
子词级的核心想法:常见词整体作为 1 个 token,罕见词切成多个子词 token。这样:
- 词表大小可控(通常 32K-128K);
- 不会出现
<UNK>,因为可一直回退到字符 / 字节级; - 相似词("play" / "playing" / "plays")共享子词,有泛化性。
4.3 BPE:主流算法¶
Byte Pair Encoding(BPE) 是当前最主流的子词分词算法(GPT-2、Llama、Qwen 等都用其变体)。
训练流程:
- 把语料全部切到字符级(含特殊结束符
</w>); - 统计相邻 pair 频次,找出最高的;
- 合并该 pair 为新 token,加入词表;
- 重复直到词表达到目标大小。
推理流程:
- 用训练得到的合并规则,对输入字符串贪心做最长匹配;
- 没匹配上的部分回退到字节级——所以任何字符串都能编码,永不产生
<UNK>。
变体:
- WordPiece(BERT 用):与 BPE 类似,但合并标准是似然而非频次;
- Unigram(T5 / mT5):从大词表反向"剪枝",最大化语料似然;
- SentencePiece:把空格也当普通字符处理,语言无关——中日韩等不含空格的语言也能统一处理;
- BBPE(Byte-level BPE):以字节为基础单元,绝对不产生 UNK,GPT-2 之后流行。
4.4 词表大小 trade-off¶
| 词表 V | 优点 | 缺点 |
|---|---|---|
| 小(~10K) | embedding 矩阵小、训练快 | 序列变长、注意力算力↑ |
| 中(~32K-64K) | 平衡 | 主流选择 |
| 大(~128K+) | 序列短、多语言友好 | embedding 矩阵大、小数据集训练不充分 |
Llama-2 词表 32K,Llama-3 升到 128K(更好支持多语言与代码),Qwen-2.5 也是 152K。
4.5 中英文与代码的分词差异¶
同一个意思的句子,不同语言消耗 token 数差异巨大:
| 文本 | 大致 token 数 |
|---|---|
| "Hello world" | 2 |
| "你好世界" | 4-6 |
function add(a, b) { return a + b; } |
12-15 |
这意味着:
- 中文用户的 token 成本是英文的 2-3 倍——同样 4K 上下文,英文能塞 3000 词,中文可能只 1500 词;
- 代码 token 利用率低——大量符号、数字被切碎;
- embedding 矩阵需要更多语言样本——否则小语种 token 学不充分。
这也是为什么中文模型(Qwen、DeepSeek、GLM)会重新设计词表并增加中文 token 比例。
4.6 Embedding:从 id 到向量¶
得到 token id 后,下一步是查表得到向量。
class TokenEmbedding(nn.Module):
def __init__(self, vocab_size, d_model):
super().__init__()
self.weight = nn.Parameter(torch.randn(vocab_size, d_model))
def forward(self, input_ids):
return self.weight[input_ids] # [B, L] -> [B, L, d_model]
这就是 embedding 的全部——一个 [V, d_model] 的查找表,每行是一个 token 的向量表示。
Tied Embedding:很多模型把"输入 embedding"与"输出 LM head"共享权重——因为"从 token 到向量"与"从向量到 token 概率"本质是同一映射。可节省 V × d 参数(对 Llama-3 8B 是 ~500M)。
4.7 位置编码¶
embedding 只有 token 信息,没有顺序信息。需要再加位置信息:
| 方法 | 思路 | 用法 |
|---|---|---|
| 绝对位置 sin/cos | 给每个位置一个固定向量 | 原始 Transformer、早期 GPT |
| 可学绝对位置 | 给每个位置一个可学向量 | GPT-2、早期 BERT |
| RoPE | 在 Q/K 上乘旋转矩阵 | Llama、Qwen 等现代主流 |
| ALiBi | 在 attention score 上加位置偏置 | BLOOM、MPT |
RoPE 的关键优势:外推性——训练时只见过 4K 位置,推理时可推到 32K+ 而无需重训(详见第 15 章)。
4.8 上下文窗口¶
上下文窗口(Context Window) = 模型一次能处理的最大 token 数。
- GPT-3(2020):2K
- Llama-2(2023):4K
- Llama-3(2024):8K(标准)/ 1M(长上下文版)
- Qwen-2.5-Turbo:1M
- Gemini 1.5 Pro:2M
为什么有上限?因为标准 Self-Attention 的算力 / 显存随 L² 增长。L=128K 时 attention score 矩阵 = 128K × 128K × 4 bytes ≈ 64 GB(仅一层)——必须用 FlashAttention、稀疏注意力等技巧(第 15 章)。
截断 vs 长上下文:超出窗口的请求要么被截断(前缀保留或后缀保留),要么用 RAG(第 11 章)绕开。
4.9 特殊 token¶
每个 tokenizer 都有特殊 token:
| Token | 用途 |
|---|---|
<s> / <bos> |
序列开始 |
</s> / <eos> |
序列结束(生成时遇到即停) |
<pad> |
填充 |
<unk> |
未登录(BBPE 后基本不用) |
[INST]...[/INST] |
Llama 指令模板 |
<|im_start|> |
Qwen 对话模板 |
chat template:每个模型有特定的对话格式,必须严格遵循,否则能力大幅下降:
现代 tokenizer(HuggingFace 的 apply_chat_template)已自动化这一过程。
4.10 工程视角的常见问题¶
1. token 计费:API 按 token 收费,必须知道不同语言的 token 密度差异;
2. chat template 不匹配:用 Llama 模型配 ChatML 模板,输出会变成乱码;
3. 多模型混用:不同模型词表不兼容,prompt 不能简单复用——要么用同一模型族,要么各自处理;
4. 流式输出:streaming 时 token 是逐个生成的,前端要处理"半个字"——因为中文一字可能是 2-3 token,需要缓冲到能 decode 出完整字符再显示;
5. BOM / 控制字符:看不见的字符也会占 token,是常见 prompt 浪费源。
工程实战要点¶
- 永远用
apply_chat_template,不要手拼 prompt 字符串; - 预估 token 数:中文约 1.5-2 字/token,英文约 0.75 词/token;
- tokenizer 与模型必须配套:
AutoTokenizer.from_pretrained(model_id)自动加载对应版本; - 特殊 token 浪费:检查 prompt 是否含不可见字符、是否多次重复 system prompt;
- API 计费:估算 cost 时按 input + output token 分别计;
- 流式 decode:用
bytes_per_token累积到合法 UTF-8 边界再 decode。
小结¶
- Tokenizer 把字符串切成子词 token,主流算法 BPE(含 BBPE、SentencePiece 变体);
- 词表大小 32K-128K,trade-off 序列长度与 embedding 参数量;
- 中英文 token 密度差异大,影响成本与上下文利用率;
- Embedding 是 [V, d] 查表,可与 LM head 共享权重;
- 位置编码注入顺序信息,RoPE 因外推性成为现代主流;
- 上下文窗口是 L² 算力的硬约束;
- chat template 与特殊 token 必须严格匹配模型格式。
练习题¶
- 解释子词分词相对于字符级与词级的优势。
- 用伪代码描述 BPE 的训练流程。
- 为什么 BBPE 不会产生
<UNK>? - 中文 token 成本是英文的几倍?为什么?
- 解释 "Tied Embedding" 节省了多少参数,并说明其原理。
- RoPE 相对于绝对位置编码的核心优势是什么?
- 一个 4K 上下文的模型,为什么 attention score 矩阵在 L=128K 时会爆显存?写出估算。
- 解释 chat template 不匹配会导致什么问题。
- 流式输出中文时,为什么需要缓冲多个 token 才能 decode?
下一章:第五章 预训练