Skip to content

第 4 章 Tokenizer 与分词 ​

学习目标 ​

  • 理解为什么模型需要 token 编号而不是文本
  • 掌握 encode / decode / convert_ids_to_tokens
  • 掌握特殊 token 与 padding / truncation
  • 掌握批量编码与 attention_mask
  • 会用 tokenizers 库从零训练一个 BPE 分词器

4.1 为什么要分词 ​

神经网络吃的是数字,不是字符串。分词(tokenization) 把文本切成若干token(词元),再把每个 token 映射成整数编号。这一整套由 tokenizer(分词器)完成。

"Hugging Face makes AI easy!"
        ↓ tokenize
['hugging', 'face', 'makes', 'ai', 'easy', '!']
        ↓ ids
[17662, 2227, 3084, 9932, 3733, 999]

注意小写:bert-base-uncased 会先把文本转小写。

4.2 编码与解码 ​

python
from transformers import AutoTokenizer

tokenizer = AutoTokenizer.from_pretrained("google-bert/bert-base-uncased")
print("is_fast:", tokenizer.is_fast, "| 词表大小:", tokenizer.vocab_size)

text = "Hugging Face makes AI easy!"
ids = tokenizer.encode(text)
print("encode:", ids)
print("tokens:", tokenizer.convert_ids_to_tokens(ids))
print("decode:", tokenizer.decode(ids))

输出:

is_fast: True | 词表大小: 30522
encode: [101, 17662, 2227, 3084, 9932, 3733, 999, 102]
tokens: ['[CLS]', 'hugging', 'face', 'makes', 'ai', 'easy', '!', '[SEP]']
decode: [CLS] hugging face makes ai easy! [SEP]

解读:

  • encode 返回编号序列;开头自动加了 101([CLS]),结尾 102([SEP])——这是 BERT 的约定。
  • convert_ids_to_tokens 把编号还原成 token 字符串。
  • decode 把编号还原成文本,用于查看模型输出。

4.3 特殊 token ​

每个分词器有一组特殊 token,在训练/推理中承担固定角色:

python
print(tokenizer.special_tokens_map)
print("pad:", repr(tokenizer.pad_token), tokenizer.pad_token_id)
print("cls:", repr(tokenizer.cls_token), tokenizer.cls_token_id)
print("sep:", repr(tokenizer.sep_token), tokenizer.sep_token_id)
print("unk:", repr(tokenizer.unk_token), tokenizer.unk_token_id)

输出:

special_tokens_map: {'unk_token': '[UNK]', 'sep_token': '[SEP]', 'pad_token': '[PAD]', 'cls_token': '[CLS]', 'mask_token': '[MASK]'}
pad: '[PAD]' 0
cls: '[CLS]' 101
sep: '[SEP]' 102
unk: '[UNK]' 100

含义:

  • [CLS]:分类任务里整个序列的汇总表示。
  • [SEP]:句子分隔/结尾标记。
  • [PAD]:批内对齐长度时补零(填充),编号 0。
  • [UNK]:词表外的未知词。
  • [MASK]:掩码语言建模要预测的位置(第 1 章的 [MASK])。

4.4 批量编码与 padding / truncation ​

批处理要求一个 batch 里所有序列长度一致,于是需要:

  • padding:短的补 [PAD] 到最长。
  • truncation:超长的截断到上限 max_length。
python
batch = tokenizer(
    ["I love transformers.", "Hugging Face makes AI easy!", "short"],
    padding="longest",
    truncation=True,
    max_length=8,
)
print("input_ids:", batch["input_ids"])
print("attention_mask:", batch["attention_mask"])

输出:

input_ids: [[101, 1045, 2293, 19081, 1012, 102, 0, 0],
            [101, 17662, 2227, 3084, 9932, 3733, 999, 102],
            [101, 2460, 102, 0, 0, 0, 0, 0]]
attention_mask: [[1, 1, 1, 1, 1, 1, 0, 0],
                 [1, 1, 1, 1, 1, 1, 1, 1],
                 [1, 1, 1, 0, 0, 0, 0, 0]]

重点理解 attention_mask:1 表示真实 token,0 表示填充。模型在计算注意力时会忽略 0 的位置,避免把 [PAD] 当成真实内容。

直接返回 PyTorch 张量:

python
import torch

enc = tokenizer("I love transformers.", return_tensors="pt")
print(enc.keys())
print("input_ids shape:", enc["input_ids"].shape)

输出:

KeysView({'input_ids': tensor([[101, 1045, 2293, 19081, 1012, 102]]), 'token_type_ids': tensor([[0, 0, 0, 0, 0, 0]]), 'attention_mask': tensor([[1, 1, 1, 1, 1, 1]])})
input_ids shape: torch.Size([1, 6])

return_tensors="pt" 让结果直接是可喂给模型的张量;第 6 章的数据整理会大量使用。

4.5 用 tokenizers 库训练一个 BPE 分词器 ​

预训练分词器可以加载,也可以从自己的语料训练(比如给领域数据定制词表)。tokenizers 库提供底层组件,离线即可运行:

python
from tokenizers import Tokenizer
from tokenizers.models import BPE
from tokenizers.trainers import BpeTrainer
from tokenizers.pre_tokenizers import ByteLevel
from tokenizers.decoders import ByteLevel as ByteLevelDecoder

corpus = [
    "hugging face makes ai easy",
    "i love learning machine learning",
    "transformers are attention models",
    "attention is all you need",
]

tokenizer = Tokenizer(BPE(unk_token="[UNK]"))
tokenizer.pre_tokenizer = ByteLevel(add_prefix_space=False)
tokenizer.decoder = ByteLevelDecoder()

trainer = BpeTrainer(special_tokens=["[UNK]", "[CLS]", "[SEP]", "[PAD]", "[MASK]"],
                     vocab_size=100)
tokenizer.train_from_iterator(corpus * 5, trainer)

print("训练出的词表大小:", tokenizer.get_vocab_size())
out = tokenizer.encode("hugging face makes ai easy")
print("tokens:", out.tokens)
print("ids:", out.ids)

tokenizer.save("/tmp/my_bpe_tokenizer.json")

输出:

训练出的词表大小: 97
tokens: ['hugging', 'Ġface', 'Ġmakes', 'Ġai', 'Ġeasy']
ids: [86, 92, 95, 77, 91]

解读:

  • Ġ 是 ByteLevel 表示「空格前缀」的特殊记号,用来区分词首/词中。
  • 语料太小,词表只学到 97 个词元(接近上限 100)。
  • 训练出的 tokenizer.json 可以被 transformers 包装成标准分词器:
python
from transformers import PreTrainedTokenizerFast

fast = PreTrainedTokenizerFast(tokenizer_file="/tmp/my_bpe_tokenizer.json",
                               unk_token="[UNK]")
print(fast.encode("hugging face makes ai easy"))

输出:

[86, 92, 95, 77, 91]

这样,你从零训练的分词器就能和 transformers 生态无缝配合。

动手实践 ​

  1. 用 bert-base-uncased 分词器观察:"don't"、"unhappiness" 各被切成几个 token,为什么?
  2. 用 padding=True, truncation=True(不写具体值)对比与 padding="longest" 的差异。
  3. 在你的中文语料上训练一个 200 词表的 BPE 分词器,观察中文被切成什么粒度。

常见错误 ​

错误 1:没有 padding 就批量调用,形状不一致报错。

ValueError: Unable to create tensor, you should probably activate truncation and/or padding with 'padding=True' 'truncation=True' to have batched tensors with the same length.

批内文本长度不同,张量无法拼成矩形。批量调用必须显式 padding(第 6 章会再次遇到)。

错误 2:模型没有 pad token 时直接 padding。

GPT-2 这类生成模型默认没有 [PAD],padding=True 会报错。习惯做法:把 eos token 当 pad:

python
tokenizer.pad_token = tokenizer.eos_token

错误 3:decode 时误以为能还原出完全一样的原文。

分词器的规范化(小写、去重音等)是单向的,decode 不保证逐字还原,尤其是 Unicode 文本。

章末练习 ​

基础

  1. 用 bert-base-uncased 输出 "The quick brown fox" 的 tokens、ids 与 decode 结果。
  2. 写代码打印 5 个特殊 token 的 id。

提高

  1. 对一个 batch 用 padding="longest", truncation=True, max_length=16,统计每个样本的 attention_mask 中 0 的个数,验证与「补了多少个 [PAD]」一致。
  2. 对比 tokenizer.encode(text) 与 tokenizer(text, return_tensors="pt") 返回结构差异。

挑战

  1. 用 tokenizers 库训练一个 300 词表的 BPE 分词器,并让 vocab_size、特殊 token、Ġ 前缀三个知识点都体现在你的实验记录里;保存 tokenizer.json 并用 PreTrainedTokenizerFast 加载验证。

章末自测 ​

  1. attention_mask 中 0 表示什么?
  2. BERT 的 [CLS] token 出现在序列什么位置?
  3. padding="longest" 的作用是什么?
  4. truncation=True 的作用是什么?
  5. tokenizer.encode 返回什么类型的数据?
  6. Ġ 在 ByteLevel 分词中表示什么?