第2章:Tokenization基础——分词的基本概念
如果让我选一个在 NLP 里最基础、也最容易被忽视的环节,那一定是分词。说白了,Tokenizer 就是模型的“嘴巴和胃”——它决定了模型能吃什么数据、怎么消化。我见过的不少项目,模型结构调得花里胡哨,结果因为分词没处理好,训练出来的东西就是笑话。
这一章我们就把“分词”这件事扒开揉碎。不扯玄乎的数学,只讲清楚:它是什么?为什么需要它?有哪些主流做法?踩坑的时候该怎么逃?
2.1 什么是 Tokenization?
把一段文本拆成一个个最小语义单元的过程,就是 tokenization。这个最小单元叫 。可以是一个词、一个字符,或者一个子词。
核心定义:Tokenization 是将原始字符串转换成模型能处理的整数索引序列的桥梁。
举个例子:"我爱北京天安门" 如果按字符切,得到 ['我','爱','北','京','天','安','门'];如果按词切,可能得到 ['我','爱','北京','天安门']。不同的切法,模型看到的世界完全不同。
你可能会问:“为什么不直接用词呢?人类不就按词理解吗?”嗯,这里有个坑——自然语言里的词数量太大,英语有几十万,中文更是无穷尽。模型输入层不能无限大,所以我们需要一种折中方案。
2.2 Tokenization 解决了什么痛点?
- 控制词表大小——从百万级压到几万甚至几千。
- 处理未登录词(OOV)——比如“特朗普”没见过?子词可以拼成“特朗”+“普”。
- 平衡语义粒度——字符太散,词太粗,子词刚刚好。
- 提升模型泛化能力——学会子词组合规律,碰到新词也不慌。
我的经验:有一次处理医疗文本,里面全是“肺腺癌”、“EGFR突变”这种专业术语。如果用全词分词,词表爆了;用字符,模型根本学不会语义。最后用 BPE 训练了子词 tokenizer,效果提升一大截。
2.3 三类主流分词方法
| 方法类别 | 代表算法 | 优点 | 缺点 |
|---|---|---|---|
| 词级分词 | Jieba、spaCy | 语义完整,符合直觉 | 词表大、OOV问题严重 |
| 字符级分词 | 按unicode切 | 无OOV,词表极小 | 序列太长,丢失局部语义 |
| 子词分词(Subword) | BPE、WordPiece、SentencePiece | 平衡词表与语义,处理OOV | 训练开销大,需要语料统计 |
现在主流大模型清一色用子词分词。我最早接触这个是在做翻译模型时,当时用 BPE 把中日韩语料统一处理,省了很多人工切词的事。
2.4 子词分词算法详解
2.4.1 BPE(Byte Pair Encoding)
BPE 的思想很简单:从字符开始,反复合并出现频率最高的相邻 对。直到达到目标词表大小。
# 伪代码示例
vocab = list(所有字符)
while len(vocab) < 目标大小:
统计所有相邻pair出现频率
合并频率最高的pair为新的
更新语料
我曾经犯过的错:训练BPE时没有将数字和字母分开处理,结果模型把“2024年”切成了“2”“0”“2”“4”“年”,后来才知道,对数字做单独加固能大幅减少无意义合并。
2.4.2 WordPiece
类似 BPE,但合并标准不是“频率”,而是互信息(MI)。它倾向于合并那些一起出现时收益最大的 pair。BERT 就用这个。
2.4.3 Unigram Language Model
从大词表开始,逐步删除“最不重要”的 ,保留概率最大的那批。SentencePiece 默认支持这种模式。
2.4.4 SentencePiece
它把文本当作原始字节序列处理,连空格都不放过。这样天然支持多语言,不需要预切词。我个人很喜欢它的“直接训练,不用预分词”思路。
2.5 实际应用中的避坑指南
- 词表大小怎么定?——通常 8k~64k。太小碎片化严重,太大模型参数多。我一般先尝试 32k。
- 要不要保留特殊 ?——必须保留 [PAD]、[UNK]、[CLS] 等。忘了留 UNK 的话,模型遇到生僻字直接报错。
- 中文需要额外处理吗?——如果用 BPE 或 SentencePiece,可以直接在字节级别切,不需要先分词。但如果你用 WordPiece,建议先用 jieba 切开再学,效果更稳定。
一句话总结:选 tokenizer = 选模型的“认知粒度”。训练前花半天调好 tokenizer,比后面花三天调模型有效得多。
2.6 动手环节:用 HuggingFace Tokenizers 快速实践
光说不练假把式。下面我们用 HuggingFace 的 tokenizers 库训练一个 BPE 模型。我记得第一次跑这个代码用了不到 5 分钟。
from tokenizers import Tokenizer, models, trainers, pre_tokenizers
# 初始化空模型
tokenizer = Tokenizer(models.BPE())
tokenizer.pre_tokenizer = pre_tokenizers.ByteLevel(add_prefix_space=False)
# 准备训练数据(这里用示例文本)
trainer = trainers.BpeTrainer(vocab_size=30000, special_tokens=["[UNK]","[CLS]","[SEP]","[PAD]","[MASK]"])
files = ["sample_text.txt"] # 你得有自己的语料文件
tokenizer.train(files, trainer)
# 保存
tokenizer.save("my_bpe_tokenizer.json")
我建议:刚开始别贪心,用几百万行文本就够了。语料越杂(多领域、多语言),tokenizer 泛化越好。
训练完成后你可以加载它,看看 "我今天很开" 会被切出什么 ——如果看到“我很”被合并成了一个 ,别奇怪,BPE 觉得它俩经常一起出现。
好了,这一章的内容就到这里。记住,Tokenizer 不是调完了就完事的——它需要根据下游任务反复打磨。下个章节我们会聊模型怎么吃到这些 … 嗯,先卖个关子。