第 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 生态无缝配合。
动手实践
- 用 bert-base-uncased 分词器观察:
"don't"、"unhappiness"各被切成几个 token,为什么? - 用
padding=True, truncation=True(不写具体值)对比与padding="longest"的差异。 - 在你的中文语料上训练一个 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 文本。
章末练习
基础
- 用 bert-base-uncased 输出
"The quick brown fox"的 tokens、ids 与 decode 结果。 - 写代码打印 5 个特殊 token 的 id。
提高
- 对一个 batch 用
padding="longest", truncation=True, max_length=16,统计每个样本的 attention_mask 中 0 的个数,验证与「补了多少个 [PAD]」一致。 - 对比
tokenizer.encode(text)与tokenizer(text, return_tensors="pt")返回结构差异。
挑战
- 用
tokenizers库训练一个 300 词表的 BPE 分词器,并让vocab_size、特殊 token、Ġ前缀三个知识点都体现在你的实验记录里;保存tokenizer.json并用PreTrainedTokenizerFast加载验证。
章末自测
attention_mask中 0 表示什么?- BERT 的
[CLS]token 出现在序列什么位置? padding="longest"的作用是什么?truncation=True的作用是什么?tokenizer.encode返回什么类型的数据?Ġ在 ByteLevel 分词中表示什么?
