跳转至

第四章 Tokenizer与Embedding

导读

模型不直接处理文字,它处理的是**整数 id 序列**。把文字变成 id 序列的工具就是 tokenizer。本章解释 tokenizer 的工作原理(BPE / WordPiece / SentencePiece)、词表(vocabulary)的设计、embedding 的查表机制,以及上下文窗口(context window)的含义。

4.1 为什么需要 Tokenizer

神经网络只能处理数值。把自然语言文本变成模型可处理的张量,分两步:

  1. 分词(Tokenization):把字符串切成一个个离散单元(token);
  2. 嵌入(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 等都用其变体)。

BPE训练流程

训练流程

  1. 把语料全部切到字符级(含特殊结束符 </w>);
  2. 统计相邻 pair 频次,找出最高的;
  3. 合并该 pair 为新 token,加入词表;
  4. 重复直到词表达到目标大小。

推理流程

  1. 用训练得到的合并规则,对输入字符串贪心做最长匹配;
  2. 没匹配上的部分回退到字节级——所以任何字符串都能编码,永不产生 <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

这意味着:

  1. 中文用户的 token 成本是英文的 2-3 倍——同样 4K 上下文,英文能塞 3000 词,中文可能只 1500 词;
  2. 代码 token 利用率低——大量符号、数字被切碎;
  3. embedding 矩阵需要更多语言样本——否则小语种 token 学不充分。

这也是为什么中文模型(Qwen、DeepSeek、GLM)会重新设计词表并增加中文 token 比例。

4.6 Embedding:从 id 到向量

得到 token id 后,下一步是查表得到向量。

Token到Embedding流水线

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:每个模型有特定的对话格式,必须严格遵循,否则能力大幅下降:

# Llama-3 格式
prompt = "<|begin_of_text|><|start_header_id|>user<|end_header_id|>\n\n你好<|eot_id|>"

现代 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 必须严格匹配模型格式。

练习题

  1. 解释子词分词相对于字符级与词级的优势。
  2. 用伪代码描述 BPE 的训练流程。
  3. 为什么 BBPE 不会产生 <UNK>
  4. 中文 token 成本是英文的几倍?为什么?
  5. 解释 "Tied Embedding" 节省了多少参数,并说明其原理。
  6. RoPE 相对于绝对位置编码的核心优势是什么?
  7. 一个 4K 上下文的模型,为什么 attention score 矩阵在 L=128K 时会爆显存?写出估算。
  8. 解释 chat template 不匹配会导致什么问题。
  9. 流式输出中文时,为什么需要缓冲多个 token 才能 decode?

下一章:第五章 预训练