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
  • 核心规则
    1. ID 分配规则:新增 token 的 ID 从基础词汇表长度开始分配,避免与基础词汇表的 ID 冲突
    2. 对象封装规则:自动将输入的 token 封装为 AddedToken 对象,统一管理 token 的属性(如是否为特殊 token、是否保留左右空格、是否需要归一化等)
    3. Trie 树更新规则:新增 token 后会更新内部 Trie 树,保证在分词阶段能优先识别并拆分新增 token,避免被分词器拆分为子词
    4. 去重规则:跳过空 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 转换流程
    1. 调用 convert_tokens_to_ids 公共方法
    2. 优先查询 _added_tokens_encoder,若 token 存在则直接返回对应的 ID
    3. 若不存在,则调用子类实现的 _convert_token_to_id 方法,查询基础词汇表的映射关系
    4. 若基础词汇表中也不存在,则返回 unk_token 对应的 ID
  • ID → Token 转换流程
    1. 调用 convert_ids_to_tokens 公共方法
    2. 优先查询 _added_tokens_decoder,若 ID 存在则返回对应的 AddedToken 对象的内容
    3. 若不存在,则调用子类实现的 _convert_id_to_token 方法,查询基础词汇表的映射关系
    4. 若基础词汇表中也不存在,则返回 unk_token 对应的 token

(4)编码与解码核心逻辑

  • 编码核心(_encode_plus / _batch_encode_plus
    1. 接收输入文本或文本对,调用 tokenize 方法完成分词
    2. 将分词结果转换为对应的 ID 列表
    3. 调用 build_inputs_with_special_tokens 方法,拼接特殊 token(如 BERT 的 [CLS]、[SEP])
    4. 执行截断(truncation)和填充(padding)操作,生成模型可接受的固定长度输入
    5. 生成 attention_masktoken_type_ids 等辅助张量,返回 BatchEncoding 对象
  • 解码核心(_decode
    1. 接收 ID 列表,调用 convert_ids_to_tokens 方法转换为 token 列表
    2. 过滤特殊 token(可选,通过 skip_special_tokens 参数控制)
    3. 拼接 token 列表:新增/特殊 token 直接拼接,普通 token 调用 convert_tokens_to_string 方法拼接
    4. 清理 tokenization 过程中产生的冗余空格,返回最终文本

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 方法)

_tokenizeBertTokenizer 的核心方法,实现了 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, 我爱中国!"

  1. 基础分词阶段BasicTokenizer 处理后得到 ["hello", ",", "我", "爱", "中", "国", "!"](自动执行小写转换、中文单字拆分)
  2. 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) 批量处理文本,转换为模型输入 ✅ 间接触发

五、 使用场景建议

  1. 生产环境/大批量数据处理:优先使用 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
    )
    
  2. 自定义分词逻辑/调试场景:使用 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
    
  3. 接口兼容性场景:使用 AutoTokenizer 自动加载分词器,其会优先选择快分词器(若存在),保证代码的通用性和兼容性
    from transformers import AutoTokenizer
    tokenizer = AutoTokenizer.from_pretrained("bert-base-uncased")
    print(tokenizer.is_fast)  # 输出 True,表示加载的是快分词器
    

六、 核心设计总结

  1. 设计模式:采用模板方法模式,父类 PreTrainedTokenizer / PreTrainedTokenizerFast 定义通用流程框架,子类 BertTokenizer / BertTokenizerFast 实现个性化的 _tokenize 等钩子方法,既保证接口统一,又支持灵活扩展
  2. 核心分层:慢分词器与快分词器分层设计,接口完全对齐,可无缝切换,满足不同场景下的性能和定制需求
  3. 新增 token 管理:通过 _added_tokens_encoder / _added_tokens_decoder 统一管理新增 token,ID 分配规则清晰,保证新增 token 不会与基础词汇表冲突
  4. 分词逻辑分层BertTokenizer 采用两级分词架构,基础分词负责粗粒度拆分,WordPiece 分词负责细粒度拆分,兼顾不同语言的分词需求(如中文单字拆分、英文子词拆分)
posted @ 2026-01-07 16:21  玉米面手雷王  阅读(2)  评论(0)    收藏  举报