河内机器人 基于 HuggingFace Tokenizers 训练自定义分词器
tokenizers 是 HuggingFace 用 Rust 实现核心、Python 绑定的高性能分词库,训练和推理速度比纯
Python 实现快几个数量级,且支持多线程训练。当前最新稳定版为 0.22.x(pip show tokenizers 显
示 0.22.2)[citation:web:2.3];核心训练 API(Tokenizer / BpeTrainer / WordPieceTrainer / UnigramTrainer)
从 0.13 到 0.19 乃至更新版本都保持稳定,老代码基本可直接运行[citation:web:2.14]。
---
一、为什么需要自定义分词器
预训练模型的通用分词器在面对特定领域语料(医学、法律、代码、某种语言)时会退化:
领域专业词被切成大量碎片 token,浪费序列长度;
中低频字词覆盖率低,OOV 比例高;
词表里塞了大量与你任务无关的 token。
自己训练可以让词表、合并规则与你的语料分布对齐,显著提升下游模型效率。
---
二、先理解「分词流水线」
tokenizers 把一个分词器拆成 5 个可替换组件,训练时按顺序作用于文本:
原始文本
→ Normalizer 规范化(Unicode 归一化 / 大小写 / 去重音)
→ PreTokenizer 预分词(按空格 / 字节级 / 正则切分)
→ Model 核心子词模型(BPE / WordPiece / Unigram)
→ PostProcessor 后处理(加 [CLS]/[SEP] 等特殊 token)
→ Decoder 解码回文本
训练时只需关心前三个;PostProcessor 和 Decoder 在训练后补齐。
---
三、三种核心算法对比
| 算法 | 思路 | 特点 | 典型代表 |
|------|------|------|----------|
| BPE | 从字符起步,反复合并最高频相邻对 | 直观、易实现、无歧义 | GPT 系列 |
| WordPiece | 类似 BPE,但合并依据是最大化似然增益 | 贪心、效果略优 | BERT |
| Unigram | 从大词表起步,逐步删减低概率子词 | 概率模型、可出多个候选 | T5、SentencePiece |
选择建议:英文/一般场景 BPE 最稳;复现 BERT 用 WordPiece;中文、日文等无空格语言优先 Unigram(配 Metaspace 预处理,等价 SentencePiece 行为)。
---
四、完整训练代码(以 BPE 为例)
准备语料
语料用纯文本文件,每行一个句子/文档即可(corpus.txt),越大越干净越好。
训练脚本
from tokenizers import Tokenizer
from tokenizers.models import BPE
from tokenizers.trainers import BpeTrainer
from tokenizers.pre_tokenizers import ByteLevel
from tokenizers.normalizers import NFKC, Sequence
from tokenizers.decoders import ByteLevel as ByteLevelDecoder
① 声明核心模型:BPE,unk 用 [UNK]
tokenizer = Tokenizer(BPE(unk_token="[UNK]"))
② 规范化:NFKC 统一全半角、兼容字符
tokenizer.normalizer = Sequence([NFKC()])
③ 预分词:字节级(对 OOV/多语言最鲁棒,GPT 风格)
tokenizer.pretokenizer = ByteLevel(addprefix_space=True)
④ 训练器
trainer = BpeTrainer(
vocab_size=30000, # 词表大小
min_frequency=2, # 最小频次,低于此不参与合并
special_tokens=["[UNK]", "[CLS]", "[SEP]", "[PAD]", "[MASK]"],
show_progress=True,
)
⑤ 训练(files 传路径列表;也支持 trainfromiterator)
tokenizer.train(files=["corpus.txt"], trainer=trainer)
⑥ 解码器(与 ByteLevel 预分词配套)
tokenizer.decoder = ByteLevelDecoder()
⑦ 保存
tokenizer.save("mybpetokenizer.json")
训练 WordPiece(BERT 风格)
from tokenizers import Tokenizer
from tokenizers.models import WordPiece
from tokenizers.trainers import WordPieceTrainer
from tokenizers.pre_tokenizers import Whitespace
tokenizer = Tokenizer(WordPiece(unk_token="[UNK]"))
tokenizer.pre_tokenizer = Whitespace() # BERT 按空格预分词
trainer = WordPieceTrainer(
vocab_size=30000,
special_tokens=["[UNK]", "[CLS]", "[SEP]", "[PAD]", "[MASK]"],
)
tokenizer.train(files=["corpus.txt"], trainer=trainer)
tokenizer.save("mywptokenizer.json")
训练 Unigram(中文/无空格语言推荐)
from tokenizers import Tokenizer
from tokenizers.models import Unigram
from tokenizers.trainers import UnigramTrainer
from tokenizers.pre_tokenizers import Metaspace
tokenizer = Tokenizer(Unigram())
tokenizer.pre_tokenizer = Metaspace() # SentencePiece 风格,▁ 表示词首
trainer = UnigramTrainer(
vocab_size=30000,
special_tokens=["[UNK]", "[CLS]", "[SEP]", "[PAD]", "[MASK]"],
unk_token="[UNK]",
)
tokenizer.train(files=["corpus.txt"], trainer=trainer)
tokenizer.save("myunigramtokenizer.json")
---
五、加载与使用
from tokenizers import Tokenizer
tok = Tokenizer.fromfile("mybpe_tokenizer.json")
编码
enc = tok.encode("Hello, how are you?")
print(enc.ids) # token id 序列
print(enc.tokens) # 子词片段(如 ['Hello', ',', 'Ġhow', ...],Ġ 表示前有空格)
print(enc.attention_mask)
解码
print(tok.decode(enc.ids)) # 'Hello, how are you?'
---
六、接入 Transformers 生态
训练好的 tokenizer.json 可以无缝接进 transformers,配合任意模型使用:
from transformers import PreTrainedTokenizerFast
fast_tok = PreTrainedTokenizerFast(
tokenizerfile="mybpe_tokenizer.json",
# 若需对齐 BERT 格式的特殊 token
unk_token="[UNK]",
pad_token="[PAD]",
cls_token="[CLS]",
sep_token="[SEP]",
mask_token="[MASK]",
)
fasttok.savepretrained("./mytokenizerdir")
也可以直接用 tokenizer_object 传入内存中的 Tokenizer 对象。
---
七、关键注意事项
预处理与解码必须配套:用了 ByteLevel 预分词,解码也要用 ByteLevel 解码器,否则会多出 Ġ 前缀或丢失空格。
addprefixspace 一致性:训练和推理时的这个参数要相同,否则首个 token 的前导空格处理会不一致。
语料质量 > 词表大小:脏数据、短文本碎片多,再大的词表也没用;先做清洗(去重、去 HTML、去乱码)。
minfrequency 与 vocabsize 的权衡:语料小时,minfrequency 调高能避免学到噪声合并;vocabsize 过大反而留下大量低频无用 token。
trainfromiterator 处理大语料:文件太大无法一次性读入时,用生成器逐条喂数据,避免内存爆炸:
def batchiterator(path, batchsize=1000):
with open(path, "r", encoding="utf-8") as f:
batch = []
for line in f:
batch.append(line.strip())
if len(batch) == batch_size:
yield batch
batch = []
if batch:
yield batch
tokenizer.trainfromiterator(batch_iterator("corpus.txt"), trainer=trainer)
tokenizers 与 transformers 版本锁:transformers 导入时会校验 tokenizers 版本,装了过新/过旧的 tokenizers 可能触发 UserWarning 甚至报错[citation:web:2.12];两者一起 pip install 时让 pip 自动解析依赖最稳妥。
---
需要的话,我可以再展开:怎么评估一个分词器的好坏(fertility、覆盖率、压缩率等指标),或者给一个中文语料训练 SentencePiece 风格分词器的完整可运行脚本。