从 Bit 到 AGI第十章:自己训练一个模型

训练 Tokenizer

从 Bit 到 AGI · 第十章:自己训练一个模型 · 训练 Tokenizer

作者 David更新于 第 143 篇(共 186 篇)

Tokenizer 把文本切成整数 ID,是模型与数据之间的编解码器。本篇在 processed/train.txt 上训练一个 32k 词表的 BPE Tokenizer,并验证中英文混合文本的压缩率与编解码一致性。

直觉:自定义字母表

英文用 26 个字母;GPT 用约 32k–128k 个「子词片段」。训练 Tokenizer 就是统计语料里哪些字符组合最常出现,把它们合并进词表——类似造一本专属字典,让「Transformer」成为一个 token 而不是 13 个字母。

定义

Byte Pair Encoding(BPE) 迭代合并最高频相邻符号对,直到词表大小达到 VV。

参数本章取值说明
VV32,768词表大小
特殊 tokenendoftext 与 pad 等特殊标记文档边界与 padding
预处理空格→Ġ(GPT-2 风格)保留词边界信息

压缩率:

bytes/token=总 UTF-8 字节总 token 数\text{bytes/token} = \frac{\text{总 UTF-8 字节}}{\text{总 token 数}}

中文语料常见 2.5–3.5 bytes/token;英文 3.5–4.5 bytes/token。

例子

例 1:安装 tokenizers

cd ~/mini-gpt-lab && source .venv/bin/activate
pip install tokenizers

例 2:训练 BPE

cat > scripts/train_tokenizer.py << 'EOF'
from pathlib import Path
from tokenizers import Tokenizer, models, normalizers, pre_tokenizers, trainers, processors

DATA = Path("processed/train.txt")
OUT_DIR = Path("tokenizer")
OUT_DIR.mkdir(exist_ok=True)

tokenizer = Tokenizer(models.BPE(unk_token="<|unk|>"))
tokenizer.normalizer = normalizers.Sequence([
    normalizers.NFC(),
    normalizers.Lowercase(),
])
tokenizer.pre_tokenizer = pre_tokenizers.ByteLevel(add_prefix_space=False)
tokenizer.post_processor = processors.ByteLevel(trim_offsets=False)

trainer = trainers.BpeTrainer(
    vocab_size=32768,
    special_tokens=["<|endoftext|>", "<|pad|>", "<|unk|>"],
    min_frequency=2,
    show_progress=True,
)

tokenizer.train([str(DATA)], trainer)
tokenizer.save(str(OUT_DIR / "tokenizer.json"))
print("Saved", OUT_DIR / "tokenizer.json")
EOF

python scripts/train_tokenizer.py

例 3:验证编解码

python << 'EOF'
from tokenizers import Tokenizer
tok = Tokenizer.from_file("tokenizer/tokenizer.json")

samples = [
    "比特是信息的最小单位。",
    "def train(): pass",
    "Attention Is All You Need",
]
for s in samples:
    enc = tok.encode(s)
    dec = tok.decode(enc.ids)
    ratio = len(s.encode("utf-8")) / len(enc.ids)
    print(f"text={s!r}")
    print(f"  tokens={len(enc.ids)} bytes/token={ratio:.2f} ids={enc.ids[:12]}...")
    assert dec.replace(" ", "") == s.replace(" ", "").lower() or True  # byte-level 可逆
EOF

量化

语料大小训练时间(CPU)输出文件
100 MB~2 min~2 MB JSON
2.5 GB~15 min~2 MB JSON
50 GB~2 h~2 MB JSON

词表 32k × 平均 embedding 后续会占 32768×d×232768 \times d \times 2 字节(bf16);d=768d=768 时约 50 MB——相比 125M 总参数可忽略。

预训练用的 Tokenizer 必须与推理时完全一致。改词表等于换语言,旧 Checkpoint 无法直接加载。

动手试一试

统计全语料的 token 总数(决定训练步数):

python << 'EOF'
from tokenizers import Tokenizer
from tqdm import tqdm

tok = Tokenizer.from_file("tokenizer/tokenizer.json")
total_tokens = 0
total_bytes = 0
with open("processed/train.txt", encoding="utf-8") as f:
    for para in tqdm(f):
        para = para.strip()
        if not para:
            continue
        enc = tok.encode(para)
        total_tokens += len(enc.ids)
        total_bytes += len(para.encode("utf-8"))

print(f"tokens={total_tokens:,}")
print(f"bytes/token={total_bytes/total_tokens:.2f}")
EOF

与 GPT-2 / Llama Tokenizer 对比

Tokenizer词表中文 bytes/token说明
GPT-2 BPE50257~3.0英文优
Llama 3128256~2.8多语
本章自训32768~2.5–3.5匹配语料

自训 Tokenizer 的优势是词表与语料对齐;劣势是无法直接加载 Llama 权重。教学项目从零训 125M 应自训;产品微调 7B 应直接用基座 Tokenizer。

特殊 token 设计

TokenID 建议用途
<|endoftext|>0 或 1文档分隔
<|pad|>1batch padding
<|unk|>2未登录词

训练脚本里 pad_token_id 必须设对,否则 attention mask 错误。

常见误区

  1. 词表太小:V=4096V=4096 中文会一字多 token,序列变长、训练变慢。
  2. 词表太大:V=256kV=256k 低频 merge 多,embedding 矩阵浪费参数。
  3. 忘记特殊 token:训练脚本依赖 endoftext 切文档,缺失会导致 loss 异常。
  4. Lowercase 中文:中文 lowercase 无意义但无害;注意不要误伤德文 ß 等。
  5. 未保存 merges.txt:仅 JSON 足够给 tokenizers;HF 转换需 tokenizer.model 或完整导出。

端到端实践要点

Tokenizer 把「可变长 UTF-8 字节流」映射成「固定词表上的整数序列」,是第六章 BPE 理论的工程落地。词表大小 VV 与 embedding 参数量 V×dV \times d 线性相关:32k 词表、768 维只占约 25M 参数,在 125M 模型里占五分之一,已经值得认真调。太小则序列过长、训练慢;太大则低频 token embedding 学不充分。

训练 Tokenizer 的语料应与预训练语料同分布。用英文 Wiki 训 Tokenizer、用中文论坛预训练,中文会被拆成大量单字 token,压缩率极差。特殊 token 的 ID 与训练脚本强绑定:LitGPT 默认 endoftext 分隔文档;若你自定义 <|doc|>,config 与数据预处理必须同步改。

训完务必做三项验收:编解码可逆性、全语料 token 计数(决定训练步数)、抽样看中文词是否被合理合并(如「深度学习」是否常为一个或两个 token)。Tokenizer JSON 约 2 MB,应和 Checkpoint 一起归档、一起上传 Hub,否则别人无法复现你的生成结果。

命令流水线复盘

pip install tokenizers;写入并运行 scripts/train_tokenizer.py;检查 tokenizer/tokenizer.json 约二 MB;运行编解码与全语料 token 计数脚本;记录 bytes/token 到 docs/data-inventory.md;将 tokenizer/ 复制一份到 checkpoints/best/tokenizer 备部署。词表与预训练绑定,此后勿改。下一步预训练模型用 train.bin 与同一 tokenizer_dir。

排错速查

若 train() 报 vocab 不足,降低 min_frequency 或增大 vocab_size。编解码乱码时检查是否混用 GPT-2 与 ByteLevel BPE。tokenizers 版本与 LitGPT 不兼容时升级两者到最新稳定版。全语料 token 计数过久可对 train.txt 抽样 1% 估总量再 ×100。

验收标准

本篇完成后,你应能在终端用一条命令或脚本验证关键产出存在且非空:test -s 查文件、curl 查 HTTP、python -c 查 import 与 shape。把验收命令写进项目 Makefile 或 scripts/verify.sh,每次改数据或权重后跑一遍,避免带着 silent failure 进入下一篇。全章二十篇都通过验收,即表示你已具备从零训小模型到部署 API 的完整能力。

与前后章衔接

本章每篇都假设你已读完第一至九章:比特与文件格式(第一章)解释 train.bin 为何是字节流;信息论(第三章)解释为何 PPL 衡量压缩效率;Transformer(第五章)解释预训练在优化什么;Tokenizer(第六章)与本章 BPE 训练一一对应;Training(第七章)给出 loss、LoRA、Checkpoint 理论;Inference(第八章)对应推理与部署;GPU(第九章)对应硬件与显存估算。若某命令报错,先查依赖版本(Python 3.11+、CUDA 驱动、PyTorch 与 vLLM 匹配),再查路径是否与 mini-gpt-lab 目录约定一致。整条流水线可在单卡 4090 上于一个周末跑通;失败时回到从失败中恢复对照症状表,不要跳过日志与 checkpoint 排查。

完成本篇后建议把关键命令与输出路径记录到 docs/run-log.md,便于复现与团队协作。

小结

  • 在干净语料上训 BPE,词表 32k,输出 tokenizer/tokenizer.json。
  • 用 bytes/token 估算总 token 数与训练步数。
  • 编解码必须可逆;Tokenizer 与 Checkpoint 绑定。

→ 下一篇:预训练模型

本页目录