hf tokenizer
Hugging Face Transformers 分词器核心类笔记
一、 核心类关系与定位
1. 继承关系
graph TD
PreTrainedTokenizerBase --> PreTrainedTokenizer
PreTrainedTokenizerBase --> PreTrainedTokenizerFast
PreTrainedTokenizer --> BertTokenizer
PreTrainedTokenizerFast --> BertTokenizerFast
2. 类功能定位
| 类名 | 类型 | 核心定位 |
|---|---|---|
PreTrainedTokenizerBase |
顶层抽象基类 | 定义所有分词器的通用接口规范,是整个分词器体系的根接口,所有分词器类都间接继承于此 |
PreTrainedTokenizer |
慢分词器基类 | 纯 Python 实现,封装慢分词器通用逻辑,定义抽象钩子方法,为所有慢分词器提供统一的骨架 |
BertTokenizer |
BERT 慢分词器 | 基于 WordPiece 算法,实现 BERT 特有分词逻辑与特殊 token 处理,是 BERT 模型专用的慢分词器 |
PreTrainedTokenizerFast |
快分词器基类 | 封装 Rust 底层 tokenizers 库,实现性能优化,支持 offset_mapping 等高级功能,为所有快分词器提供统一接口 |
BertTokenizerFast |
BERT 快分词器 | 适配 BERT 特殊 token 与输入格式,无缝替换慢分词器,兼具高性能与 BERT 兼容性 |
3. 慢分词器 vs 快分词器核心差异
| 维度 | PreTrainedTokenizer(慢) | PreTrainedTokenizerFast(快) |
|---|---|---|
| 底层实现 | 纯 Python 代码编写,无外部依赖 | 封装 Rust 语言实现的 tokenizers 库,调用底层 Rust 接口 |
| 性能 | 较低,尤其是在大批量文本处理场景下效率差 | 极高,Rust 原生加速特性,批量处理场景下性能提升 10-100 倍 |
| 核心依赖 | 无额外依赖,仅依赖 Python 标准库和 Transformers 内部模块 | 必须安装 tokenizers 库,否则无法初始化 |
| 扩展能力 | 易修改,直接修改 Python 源码即可调整分词逻辑 | 难修改,如需调整核心逻辑需修改 Rust 代码并重新编译 |
| 功能支持 | 仅支持基础分词、编码、解码功能,无 offset_mapping 等高级功能 |
支持完整功能,包含 offset_mapping(token 与原文本字符偏移映射)、overflowing_tokens(超长文本滑动窗口拆分)等 |
| 初始化方式 | 仅支持从词汇表文件(如 vocab.txt)加载初始化 | 支持多源初始化,包括从慢分词器转换、分词器序列化文件、GGUF 格式文件等 |
二、 PreTrainedTokenizer 基类核心逻辑
1. 类定位
- 所有慢分词器的基类,
is_fast属性固定为False,标识其为慢分词器类型 - 定义了慢分词器的通用框架,包含新增 token 管理、特殊 token 处理、批量编码/填充/截断等核心通用能力
- 通过抽象钩子方法,将模型专属的个性化分词逻辑交由子类实现,保证接口统一性
2. 核心初始化逻辑
def __init__(self, **kwargs):
self.tokens_trie = Trie() # 构建 Trie 树,用于高效拆分新增/特殊 token,避免被分词器误切分
# 初始化新增 token 映射字典:_added_tokens_encoder 存储 {token: id},_added_tokens_decoder 存储 {id: AddedToken 对象}
if not hasattr(self, "_added_tokens_decoder"):
self._added_tokens_decoder: dict[int, AddedToken] = {}
self._added_tokens_decoder.update(kwargs.pop("added_tokens_decoder", {}))
self._added_tokens_encoder: dict[str, int] = {k.content: v for v, k in self._added_tokens_decoder.items()}
super().__init__(**kwargs) # 调用父类 PreTrainedTokenizerBase 的初始化方法
# 自动补充未加入的特殊 token 到新增 token 列表,确保特殊 token 不被拆分
self._add_tokens(
[token for token in self.all_special_tokens_extended if token not in self._added_tokens_encoder],
special_tokens=True,
)
self._decode_use_source_tokenizer = False
3. 核心功能模块
(1)新增 Token 管理
- 核心方法:
_add_tokens(new_tokens: Union[list[str], list[AddedToken]], special_tokens: bool = False) -> int - 核心规则
- ID 分配规则:新增 token 的 ID 从基础词汇表长度开始分配,避免与基础词汇表的 ID 冲突
- 对象封装规则:自动将输入的 token 封装为
AddedToken对象,统一管理 token 的属性(如是否为特殊 token、是否保留左右空格、是否需要归一化等) - Trie 树更新规则:新增 token 后会更新内部 Trie 树,保证在分词阶段能优先识别并拆分新增 token,避免被分词器拆分为子词
- 去重规则:跳过空 token 和已存在于新增 token 映射中的 token,避免重复添加
(2)分词流程(tokenize 方法)
tokenize 是用户调用的公共分词入口方法,封装了完整的分词流程,核心步骤如下:
flowchart LR
A[输入文本] --> B[文本预处理:调用 prepare_for_tokenization 方法,执行小写转换、清理空格等操作]
B --> C[Trie 树拆分:利用 tokens_trie 拆分出文本中的新增/特殊 token,避免被误切分]
C --> D[空格规则处理:根据 AddedToken 对象的 lstrip/rstrip 属性,处理 token 前后空格]
D --> E{判断 token 类型}
E -->|新增/特殊 token| F[直接保留该 token,不进行子词拆分]
E -->|普通文本 token| G[调用子类实现的 _tokenize 方法,执行模型专属分词逻辑]
F & G --> H[合并所有 token,返回最终分词结果列表]
(3)Token 与 ID 转换规则
- Token → ID 转换流程
- 调用
convert_tokens_to_ids公共方法 - 优先查询
_added_tokens_encoder,若 token 存在则直接返回对应的 ID - 若不存在,则调用子类实现的
_convert_token_to_id方法,查询基础词汇表的映射关系 - 若基础词汇表中也不存在,则返回
unk_token对应的 ID
- 调用
- ID → Token 转换流程
- 调用
convert_ids_to_tokens公共方法 - 优先查询
_added_tokens_decoder,若 ID 存在则返回对应的AddedToken对象的内容 - 若不存在,则调用子类实现的
_convert_id_to_token方法,查询基础词汇表的映射关系 - 若基础词汇表中也不存在,则返回
unk_token对应的 token
- 调用
(4)编码与解码核心逻辑
- 编码核心(
_encode_plus/_batch_encode_plus)- 接收输入文本或文本对,调用
tokenize方法完成分词 - 将分词结果转换为对应的 ID 列表
- 调用
build_inputs_with_special_tokens方法,拼接特殊 token(如 BERT 的 [CLS]、[SEP]) - 执行截断(truncation)和填充(padding)操作,生成模型可接受的固定长度输入
- 生成
attention_mask、token_type_ids等辅助张量,返回BatchEncoding对象
- 接收输入文本或文本对,调用
- 解码核心(
_decode)- 接收 ID 列表,调用
convert_ids_to_tokens方法转换为 token 列表 - 过滤特殊 token(可选,通过
skip_special_tokens参数控制) - 拼接 token 列表:新增/特殊 token 直接拼接,普通 token 调用
convert_tokens_to_string方法拼接 - 清理 tokenization 过程中产生的冗余空格,返回最终文本
- 接收 ID 列表,调用
4. 子类必须实现的抽象钩子方法
| 抽象方法 | 功能要求 |
|---|---|
vocab_size |
作为属性方法,返回基础词汇表的大小(不含新增 token) |
_tokenize(self, text, split_special_tokens=False) |
实现模型专属的分词逻辑,是分词器的核心个性化方法 |
_convert_token_to_id(self, token) |
实现基础词汇表中 token 到 ID 的映射逻辑 |
_convert_id_to_token(self, index) |
实现基础词汇表中 ID 到 token 的映射逻辑 |
三、 BertTokenizer 核心实现
1. 类定位
- 继承
PreTrainedTokenizer,是 BERT 模型专用的慢分词器 - 核心架构采用基础分词(BasicTokenizer) + WordPiece 分词(WordPieceTokenizer) 两级拆分策略
- 完全适配 BERT 模型的输入格式要求,处理 [CLS]、[SEP]、[PAD]、[MASK]、[UNK] 等特殊 token
2. 初始化逻辑
def __init__(
self,
vocab_file,
do_lower_case=True,
do_basic_tokenize=True,
never_split=None,
unk_token="[UNK]",
sep_token="[SEP]",
pad_token="[PAD]",
cls_token="[CLS]",
mask_token="[MASK]",
tokenize_chinese_chars=True,
strip_accents=None,
clean_up_tokenization_spaces=True,
**kwargs,
):
# 1. 校验词汇表文件是否存在,不存在则抛出异常
if not os.path.isfile(vocab_file):
raise ValueError(
f"Vocabulary file {vocab_file} not found. Please supply a valid path to the vocabulary file."
)
# 2. 加载 vocab.txt 文件,构建基础词汇表映射
self.vocab = load_vocab(vocab_file) # 格式:{token: id}
self.ids_to_tokens = collections.OrderedDict([(ids, tok) for tok, ids in self.vocab.items()]) # 格式:{id: token}
# 3. 初始化基础分词器 BasicTokenizer:负责粗粒度拆分,如按空格、标点拆分,中文按单字拆分
self.do_basic_tokenize = do_basic_tokenize
if do_basic_tokenize:
self.basic_tokenizer = BasicTokenizer(
do_lower_case=do_lower_case,
never_split=never_split,
tokenize_chinese_chars=tokenize_chinese_chars,
strip_accents=strip_accents,
)
# 4. 初始化 WordPiece 分词器:负责细粒度拆分,将基础分词结果拆分为子词
self.wordpiece_tokenizer = WordpieceTokenizer(vocab=self.vocab, unk_token=str(unk_token))
# 5. 调用父类初始化方法,完成特殊 token 等配置
super().__init__(**kwargs)
3. 核心分词逻辑(_tokenize 方法)
_tokenize 是 BertTokenizer 的核心方法,实现了 BERT 特有的两级分词逻辑,代码及流程如下:
def _tokenize(self, text, split_special_tokens=False):
split_tokens = []
if self.do_basic_tokenize:
# 第一步:调用 BasicTokenizer 执行基础分词
for token in self.basic_tokenizer.tokenize(
text, never_split=self.all_special_tokens if not split_special_tokens else None
):
# 若 token 属于不拆分列表(如特殊 token),直接加入结果
if token in self.basic_tokenizer.never_split:
split_tokens.append(token)
# 否则调用 WordPieceTokenizer 执行子词拆分
else:
split_tokens += self.wordpiece_tokenizer.tokenize(token)
else:
# 跳过基础分词,直接执行 WordPiece 拆分(极少使用)
split_tokens = self.wordpiece_tokenizer.tokenize(text)
return split_tokens
分词示例:输入文本 "Hello, 我爱中国!"
- 基础分词阶段:
BasicTokenizer处理后得到["hello", ",", "我", "爱", "中", "国", "!"](自动执行小写转换、中文单字拆分) - WordPiece 拆分阶段:对每个普通 token 执行子词拆分,英文 token
"hello"在词汇表中存在,直接保留;中文 token 无进一步拆分,最终结果与基础分词结果一致
4. BERT 特有功能实现
(1)构建带特殊 token 的输入序列
build_inputs_with_special_tokens 方法实现了 BERT 模型的标准输入格式,支持单序列和双序列两种场景:
def build_inputs_with_special_tokens(self, token_ids_0: list[int], token_ids_1: Optional[list[int]] = None) -> list[int]:
# 单序列场景:[CLS] + 序列0 + [SEP]
if token_ids_1 is None:
return [self.cls_token_id] + token_ids_0 + [self.sep_token_id]
# 双序列场景(如文本匹配任务):[CLS] + 序列0 + [SEP] + 序列1 + [SEP]
cls = [self.cls_token_id]
sep = [self.sep_token_id]
return cls + token_ids_0 + sep + token_ids_1 + sep
示例:
- 单序列 ID 列表
[2769, 4263, 704, 1744]→ 拼接后[101, 2769, 4263, 704, 1744, 102](假设 [CLS]=101,[SEP]=102) - 双序列 ID 列表
[2769, 4263]和[704, 1744]→ 拼接后[101, 2769, 4263, 102, 704, 1744, 102]
(2)Token 拼接还原文本
convert_tokens_to_string 方法用于将 WordPiece 拆分后的 token 列表还原为文本,核心逻辑是移除 token 中的 ## 前缀:
def convert_tokens_to_string(self, tokens):
out_string = " ".join(tokens).replace(" ##", "").strip()
return out_string
示例:输入 token 列表 ["play", "##ing"] → 拼接后得到 "play ##ing" → 替换 ## 后得到最终文本 "playing"
(3)特殊 token 掩码生成
get_special_tokens_mask 方法生成特殊 token 的掩码列表,用于模型区分特殊 token 和普通 token,掩码值 1 表示特殊 token,0 表示普通 token:
def get_special_tokens_mask(self, token_ids_0: list[int], token_ids_1: Optional[list[int]] = None, already_has_special_tokens: bool = False) -> list[int]:
if already_has_special_tokens:
return super().get_special_tokens_mask(token_ids_0, token_ids_1, already_has_special_tokens)
# 双序列场景掩码:[1, 0,0,..., 1, 0,0,..., 1]
if token_ids_1 is not None:
return [1] + ([0] * len(token_ids_0)) + [1] + ([0] * len(token_ids_1)) + [1]
# 单序列场景掩码:[1, 0,0,..., 1]
return [1] + ([0] * len(token_ids_0)) + [1]
(4)词汇表保存
save_vocabulary 方法将当前词汇表保存为 vocab.txt 格式文件,按 ID 升序排列:
def save_vocabulary(self, save_directory: str, filename_prefix: Optional[str] = None) -> tuple[str]:
if os.path.isdir(save_directory):
vocab_file = os.path.join(save_directory, (filename_prefix + "-" if filename_prefix else "") + VOCAB_FILES_NAMES["vocab_file"])
else:
vocab_file = (filename_prefix + "-" if filename_prefix else "") + save_directory
# 按 ID 升序写入词汇表
with open(vocab_file, "w", encoding="utf-8") as writer:
index = 0
for token, token_index in sorted(self.vocab.items(), key=lambda kv: kv[1]):
if index != token_index:
logger.warning(f"Saving vocabulary to {vocab_file}: vocabulary indices are not consecutive")
index = token_index
writer.write(token + "\n")
index += 1
return (vocab_file,)
四、 _tokenize 方法调用链路
1. 核心调用关系
_tokenize 是内部钩子方法,以下划线开头标识其不建议被用户直接调用,而是由父类 PreTrainedTokenizer 的公共方法间接触发,完整调用链路如下:
graph LR
A["用户调用 tokenizer(text)"] --> B["触发 __call__ 方法"]
B --> C["调用 encode / encode_plus 方法"]
C --> D["调用 prepare_for_model 方法"]
D --> E["调用 tokenize 公共方法"]
E --> F["调用 _tokenize 钩子方法"]
2. 直接调用者:父类 tokenize 方法
父类 PreTrainedTokenizer 中的 tokenize 方法是 _tokenize 的直接调用者,核心逻辑如下:
def tokenize(self, text, pair=None, add_special_tokens=False, **kwargs):
# 处理文本对场景(如句子A和句子B)
if pair is not None:
tokens_0 = self._tokenize(text, **kwargs) # 调用子类 _tokenize 方法处理第一个文本
tokens_1 = self._tokenize(pair, **kwargs) # 调用子类 _tokenize 方法处理第二个文本
tokens = tokens_0 + [self.sep_token] + tokens_1
else:
tokens = self._tokenize(text, **kwargs) # 单文本场景,直接调用子类 _tokenize 方法
# 可选:添加特殊 token
if add_special_tokens:
tokens = self.build_inputs_with_special_tokens(tokens)
return tokens
3. 上层触发的公共方法
用户在实际使用中,以下公共方法的调用最终都会触发 _tokenize 方法:
| 公共方法 | 用途 | 是否触发 _tokenize |
|---|---|---|
tokenizer.tokenize(text) |
将文本转换为 token 列表 | ✅ 直接触发 |
tokenizer(text) / tokenizer.__call__(text) |
将文本转换为模型输入的 BatchEncoding 对象 |
✅ 间接触发 |
tokenizer.encode(text) |
将文本转换为 ID 列表 | ✅ 间接触发 |
tokenizer.encode_plus(text) |
将文本转换为包含 ID、掩码等的字典 | ✅ 间接触发 |
tokenizer.batch_encode_plus(text_list) |
批量处理文本,转换为模型输入 | ✅ 间接触发 |
五、 使用场景建议
- 生产环境/大批量数据处理:优先使用
BertTokenizerFast,利用其 Rust 加速特性提升处理效率,同时支持offset_mapping等高级功能,满足复杂任务需求from transformers import BertTokenizerFast tokenizer = BertTokenizerFast.from_pretrained("bert-base-uncased") outputs = tokenizer( ["Hello world!", "I love AI"], return_tensors="pt", padding=True, truncation=True, return_offsets_mapping=True ) - 自定义分词逻辑/调试场景:使用
BertTokenizer,纯 Python 代码易于修改和断点调试,方便开发者调整分词逻辑以适配特殊任务from transformers import BertTokenizer tokenizer = BertTokenizer.from_pretrained("bert-base-uncased") # 自定义修改 WordPiece 分词逻辑 def custom_tokenize(text): tokens = tokenizer.basic_tokenizer.tokenize(text) # 插入自定义处理逻辑 new_tokens = [] for token in tokens: if token.startswith("un"): new_tokens.append("un") new_tokens.append(token[2:]) else: new_tokens.append(token) return new_tokens - 接口兼容性场景:使用
AutoTokenizer自动加载分词器,其会优先选择快分词器(若存在),保证代码的通用性和兼容性from transformers import AutoTokenizer tokenizer = AutoTokenizer.from_pretrained("bert-base-uncased") print(tokenizer.is_fast) # 输出 True,表示加载的是快分词器
六、 核心设计总结
- 设计模式:采用模板方法模式,父类
PreTrainedTokenizer/PreTrainedTokenizerFast定义通用流程框架,子类BertTokenizer/BertTokenizerFast实现个性化的_tokenize等钩子方法,既保证接口统一,又支持灵活扩展 - 核心分层:慢分词器与快分词器分层设计,接口完全对齐,可无缝切换,满足不同场景下的性能和定制需求
- 新增 token 管理:通过
_added_tokens_encoder/_added_tokens_decoder统一管理新增 token,ID 分配规则清晰,保证新增 token 不会与基础词汇表冲突 - 分词逻辑分层:
BertTokenizer采用两级分词架构,基础分词负责粗粒度拆分,WordPiece 分词负责细粒度拆分,兼顾不同语言的分词需求(如中文单字拆分、英文子词拆分)

浙公网安备 33010602011771号