Transformers v5 中的分词技术:更简单、更清晰、更模块化

欢迎来到"一起学点什么吧"的合集「NLP知微集」。在这里,我们不愿宏大叙事,只聚焦于自然语言处理领域中那些细微却关键的“齿轮”与“螺丝钉”。我相信,真正深刻的理解,源于对细节的洞察。本期,我将为您拆解的是:Transformer v5中的Tokenizer

转载翻译自 HuggingFace 的blog:https://huggingface.co/blog/tokenizers

Transformers v5 重新设计了分词器(Tokenizers)的工作方式。这次分词器大重构将分词器的设计(架构)与训练好的词表(参数)进行了分离——这非常类似于 PyTorch 将神经网络架构与学习到的权重解耦的方式。其结果是,你现在可以以极低的成本对分词器进行检查自定义以及从头训练

核心摘要(TL;DR): 本文将阐述 Transformers 中分词的工作原理,以及为何 v5 是一次重大的重新设计。新版本拥有更清晰的内部构造、简洁的类继承体系,并统一了高性能后端。对于希望理解、自定义或训练特定模型分词器,而非将其视为“黑盒”的开发者来说,这是一份实用的技术指南。

目录

  • 什么是分词?
  • 分词流水线
  • 分词算法
  • 通过 Transformers 访问分词引擎
  • Transformers 中的分词器类继承体系
  • AutoTokenizer 自动选择正确的分词器类
  • v5:分词器架构与训练词表的分离
  • 总结

专家提示: 如果你已经熟悉相关概念,只想了解 v5 的变化,请直接跳转至“v5:分词器架构与训练词表的分离”章节。

在深入探讨变化之前,让我们先快速回顾一下分词的工作原理以及各组件是如何协同工作的。

什么是分词?

语言模型无法直接读取原始文本。它们处理的是整数序列,通常被称为 Token ID(分词 ID)Input ID。分词(Tokenization)就是将原始文本转换为这些 Token ID 的过程。(你可以尝试分词游乐场来直观地观察分词过程。)

分词是自然语言处理和通用文本处理中的一个广泛概念。本文特别关注使用 transformerstokenizers 库的大语言模型(LLMs)分词技术。

from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained("HuggingFaceTB/SmolLM3-3B")

text = "Hello world"
tokens = tokenizer(text)

print(tokens["input_ids"])
# [9906, 1917]

print(tokenizer.convert_ids_to_tokens(tokens["input_ids"]))
# ['Hello', ' Ġworld']

注:上面的 Ġworld 是一个单一的分词,代表字符序列 " world"(包含空格)。

Token 是模型能看到的最小字符串单元。它可以是一个字符、一个单词或一个子词块(如 “play” 或 “##ing”)。词表(Vocabulary) 负责将每个唯一的 Token 映射到对应的 Token ID。

from transformers import AutoTokenizer

tokenizer = AutoTokenizer.from_pretrained("HuggingFaceTB/SmolLM3-3B")
print(tokenizer.vocab)

# {'ÎĹÎľ': 106502, 'ĠPeel': 89694, '.languages': 91078, ...}

一个优秀的分词器应当能将文本压缩成最少数量的 Token。更少的 Token 意味着在不增加模型规模的情况下,可以利用更长的有效上下文。训练分词器本质上是为你的数据集寻找最佳的压缩规则。例如,如果你处理的是中文语料,有时会发现意想不到的惊喜

分词流水线

分词过程是分阶段进行的,每个阶段在将文本传递给下一级之前都会对其进行转换:

阶段目的示例
归一化器 (Normalizer)标准化文本(转小写、Unicode 规范化、清理空格等)"HELLO World""hello world"
预分词器 (Pre-tokenizer)将文本切分成初步的块"hello world"["hello", " world"]
模型 (Model)应用核心分词算法(BPE, Unigram 等)["hello", " world"][9906, 1917]
后处理器 (Post-processor)添加特殊 Token(BOS, EOS, padding等)[9906, 1917][1, 9906, 1917, 2]
解码器 (Decoder)将 Token ID 还原为文本[9906, 1917]"hello world"

每个组件都是独立的。你可以更换归一化器或更改算法,而无需重写其他部分。

你可以通过 _tokenizer 访问底层基于 Rust 的分词器。我们将在后文深入探讨。

from transformers import AutoTokenizer

tokenizer = AutoTokenizer.from_pretrained("google/gemma-3-270m-it")

print(f"{tokenizer._tokenizer.normalizer=}")
# Replace(...)

print(f"{tokenizer._tokenizer.pre_tokenizer=}")
# Split(...)

print(f"{tokenizer._tokenizer.model=}")
# BPE(...)

print(f"{tokenizer._tokenizer.post_processor=}")
# TemplateProcessing(...)

print(f"{tokenizer._tokenizer.decoder=}")
# Sequence(decoders=[Replace(...), ByteFallback(), Fuse()])

分词算法

现代语言模型的分词器主要由以下算法主导:

  1. 字节对编码 (Byte Pair Encoding, BPE):迭代合并最频繁出现的字符对。该算法是确定性的,应用极其广泛。(了解更多关于 BPE
from transformers import AutoTokenizer

tokenizer = AutoTokenizer.from_pretrained("openai/gpt-oss-20b")
print(tokenizer._tokenizer.model)
# BPE(...)
  1. Unigram:采用概率方法,从庞大的初始词表中选出最可能的分割方式。它比严格的 BPE 更具灵活性。(了解更多关于 Unigram
from transformers import AutoTokenizer

tokenizer = AutoTokenizer.from_pretrained("google-t5/t5-base")
print(tokenizer._tokenizer.model)
# Unigram(...)
  1. WordPiece:类似于 BPE,但使用基于似然性的合并准则。(了解更多关于 WordPiece
from transformers import AutoTokenizer

tokenizer = AutoTokenizer.from_pretrained("bert-base-uncased")
print(tokenizer._tokenizer.model)
# WordPiece(...)

通过 Transformers 访问分词引擎

tokenizers 库是一个基于 Rust 的分词引擎。它速度极快、效率高,且与具体的语言模型无关。该库处理将文本转换为 ID 以及反向还原的底层机制。它是一个实现分词算法的通用工具,但不包含将这些算法连接到特定语言模型的约定。

看看直接将 tokenizersSmolLM3-3B 模型一起使用会发生什么:

from tokenizers import Tokenizer

tokenizer = Tokenizer.from_pretrained("HuggingFaceTB/SmolLM3-3B")
text = "Hello world"
encodings = tokenizer.encode(text)

print(encodings.ids)
# [9906, 1917]
print(encodings.tokens)
# ['Hello', ' Ġworld']

输出的是原始分词结果。你得到了 ID 和对应的字符串碎片,仅此而已。

现在考虑缺失的部分。SmolLM3-3B 是一个对话模型。当你与其交互时,通常会将输入结构化为包含“用户(user)”和“助手(assistant)”角色的对话。语言模型需要特定的格式化 Token 来识别这些角色。原始的 tokenizers 库对此毫无概念。

如何桥接原始分词与模型需求之间的鸿沟?

transformers 库弥补了这一空缺。虽然它主要以模型定义库著称,但它也提供了一个分词器抽象层,封装了原始的 tokenizers 后端,并添加了**模型感知(model-aware)**功能。

以下是使用 transformers 封装后的相同分词过程:

from transformers import AutoTokenizer

tokenizer = AutoTokenizer.from_pretrained("HuggingFaceTB/SmolLM3-3B")

# 使用模型的聊天模板格式化对话
prompt = "Give me a brief explanation of gravity in simple terms."
messages = [{"role": "user", "content": prompt}]
text = tokenizer.apply_chat_template(
    messages,
    tokenize=False,
    add_generation_prompt=True,
)

print(text)

# <|im_start|>system
# ...
# <|im_start|>user
# Give me a brief explanation of gravity in simple terms.<|im_end|>
# <|im_start|>assistant

model_inputs = tokenizer([text], return_tensors="pt")

注意特殊 Token 如 <|im_start|><|im_end|> 是如何在分词前应用到 Prompt 中的。这有助于模型学习序列的开始和结束。

transformers 分词器添加了原始库所缺乏的一切:

  • 聊天模板应用: apply_chat_template 方法根据模型预期的格式格式化对话,插入正确的特殊 Token 和分隔符。
  • 自动插入特殊 Token: 在模型预期的位置添加序列开始(BOS)和序列结束(EOS)Token。
  • 上下文长度截断: 你可以指定 truncation=True,分词器将遵循模型的最大序列长度限制。
  • 带填充的批处理编码: 多个输入可以被填充(Padding)到相同长度,并使用正确的填充 Token 和方向。
  • 返回格式选项: 你可以直接请求 PyTorch 张量(return_tensors="pt")、NumPy 数组等。

transformers 实现了整个机器学习社区最常用的分词 API(encodedecodeconvert_tokens_to_ids 等)。

Transformers 中的分词器类继承体系

transformers 库将分词器组织在一个类继承体系中。顶层是一个定义通用接口的基类。其下,后端类使用不同的引擎处理实际的分词。最底层是模型特定的类,它们为特定模型配置后端。
Transformers 内部的分词器类继承体系

PreTrainedTokenizerBase 定义了所有分词器的通用接口

PreTrainedTokenizerBase 是所有分词器的抽象基类。它定义了每个分词器必须实现的接口。

该基类处理不依赖于具体分词后端的功能:

  • 特殊 Token 属性: 定义了如 bos_tokeneos_tokenpad_tokenunk_token 等属性。这些属性提供对模型用于标记序列边界和处理未知输入的特殊标记的访问。
  • 编码接口: 定义了 __call__encodeencode_plus 方法,返回 token ID、注意力掩码及其他元数据。
  • 解码接口: decodebatch_decode 方法,将 token ID 转换回文本。
  • 序列化: save_pretrainedfrom_pretrained 处理文件下载、信息读取、将 tokenizer 保存到磁盘等。
  • 聊天模板支持: apply_chat_template 方法也存在,根据 tokenizer 配置中存储的 Jinja 模板格式化对话。

transformers 中的每个分词器最终都继承自 PreTrainedTokenizerBase 。基类确保所有分词器的行为一致,无论它们实际使用哪种后端进行分词。

TokenizersBackend 封装了 tokenizers

TokenizersBackend 是大多数现代分词器的主要后端类。它继承自 PreTrainedTokenizerBase 并封装了基于 Rust 的 tokenizers 库。

该类在内部存储 Rust 分词器对象:

class TokenizersBackend(PreTrainedTokenizerBase):
    def __init__(self, tokenizer_object, ...):
        self._tokenizer = tokenizer_object  # Rust 分词器
        ...

当你在TokenizersBackend分词器上调用编码方法时,该类会将实际工作委托给 Rust 后端,由其执行计算密集型工作,而 Python 封装层则在之上添加模型感知的特性。

def _batch_encode_plus(self, batch_text_or_text_pairs, ...):
    encodings = self._tokenizer.encode_batch(batch_text_or_text_pairs, ...)
    ...

许多模型特定的分词器(如 LlamaTokenizerGemmaTokenizer)都继承自 TokenizersBackend

这些特定于模型的类配置后端,为其各自模型提供正确的词汇表、合并规则、特殊标记和规范化设置。

PythonBackend 提供纯 Python 实现

PythonBackend 继承自基类并用纯 Python 实现分词,其别名为 PreTrainedTokenizer

纯 Python 后端的存在的主要原因:

  • 自定义分词逻辑: 某些模型的分词行为无法适应标准的 tokenizers 流水线。
  • 旧版兼容性: 较旧的模型实现可能依赖于 Python 特有的行为。

注意:Python 后端比 Rust 后端慢。在大多数用例中,首选基于 Rust 的 TokenizersBackend

继承自 PythonBackend (或其别名 PreTrainedTokenizer )的特定模型分词器包括一些较旧或专业的模型,例如:

  • CTRLTokenizer
  • CanineTokenizer

SentencePieceBackend 处理 SentencePiece 模型

SentencePieceBackend 继承自 PythonBackend,并集成了 Google 的 SentencePiece 库。许多模型(尤其是 Google 训练的模型)使用该库。

后端封装了一个 SentencePiece 处理器:

class SentencePieceBackend(PythonBackend):
    def __init__(self, vocab_file, ...):
        self.sp_model = spm.SentencePieceProcessor()
        self.sp_model.Load(vocab_file)
        ...

使用 SentencePiece 分词的模型继承于此后端。例如:

  • SiglipTokenizer
  • BartphoTokenizer

SentencePiece 后端继承自 PythonBackend 而不是直接继承自 PreTrainedTokenizerBase ,因为它共享了大部分相同的接口和填充/截断逻辑。

AutoTokenizer 自动选择正确的分词器类

AutoTokenizer 是加载分词器的推荐入口。它会自动判断模型应使用哪个分词器类。

from transformers import AutoTokenizer

tokenizer = AutoTokenizer.from_pretrained("gpt2")

在幕后,AutoTokenizer 会执行:

  1. 下载分词配置from_pretrained 方法从 Hub(或本地目录)获取 tokenizer_config.json

  2. 识别模型类型:配置包含识别模型类型的元数据(例如,“gpt2”、“llama”、“bert”)。

  3. 查表匹配分词器类AutoTokenizer 维护一个名为 TOKENIZER_MAPPING_NAMES 的映射,该映射将模型类型映射到分词器类名:

    TOKENIZER_MAPPING_NAMES = {
        "gpt2": "GPT2Tokenizer",
        "llama": "LlamaTokenizer",
        "bert": "BertTokenizer",
        ...
    }
    
  4. 实例化正确的类AutoTokenizer 导入适当的 tokenizer 类并调用其 from_pretrained 方法。

  5. 返回配置的 tokenizer:将返回一个完全配置的、针对特定模型的 tokenizer。

AutoTokenizer 的好处在于你不需要知道模型使用的是哪个分词器类,统一的接口即可完成一切。无论模型使用的是 LlamaTokenizerGPT2Tokenizer 还是 BertTokenizer ,相同的 AutoTokenizer.from_pretrained("model-name") 调用都能正常工作。

transformers 中的分词系统形成了一种分层架构:

LayerComponentResponsibility
入口点(Entry Point )AutoTokenizer自动选择并实例化正确的分词器类
特定模型(Model-Specific)LlamaTokenizerGPT2Tokenizer 等。配置后端,包括模型特定的架构(如归一化器、预分词器等)、特殊标记和设置
后端(Backend )TokenizersBackend, PythonBackend, SentencePieceBackend使用特定引擎实现实际分词
基本(Base)`PreTrainedTokenizerBase定义了通用接口和共享功能
引擎(Engine)tokenizers (Rust), SentencePiece, 纯 Python执行原始分词

v5:分词器架构与训练词表的分离

Transformers v5 最显著的变化是分词器定义方式的哲学转变分词器现在的工作方式类似于 PyTorch 的 nn.Module:你先定义架构,然后再填充学习到的参数。

v4 的问题:分词器是不透明且紧密耦合的

在 v4 中,分词器被视为与预训练权重文件绑定的“黑盒”。如果你加载了 LlamaTokenizerFast,你无法轻易回答一些基本问题:

  • 它是 BPE 还是 Unigram?
  • 它如何归一化文本?
  • 它使用什么预分词策略?
  • 特殊标记及其位置是什么?

__init__ 方法没有提供任何线索。你必须通过反序列化文件或外部文档来了解分词器实际做了什么。

v4 中的 LlamaTokenizerFast 内部视图

此外,v4 为每个模型维护两套并行实现:

  1. “慢速” Python 分词器(LlamaTokenizer 继承自 PreTrainedTokenizer
  2. “快速” Rust 后端分词器( LlamaTokenizerFast 继承自 PreTrainedTokenizerFast

这意味着:

  • 每个模型两个文件(如 tokenization_llama.pytokenization_llama_fast.py)。
  • 代码冗余
  • 行为差异导致微妙的 Bug,即慢版本和快版本之间的差异。
  • 测试套件不断增长,用于验证慢速和快速分词器产生相同的输出
  • 用户对于应该使用哪个分词器以及何时使用感到困惑

无法创建空的分词器架构。如果你想在自己的数据上训练一个 LLaMA 风格的分词器,没有简单的方法可以先实例化一个“空白”的 LLaMA 分词器,然后再填充词表和合并规则。分词器只作为已加载的检查点存在,而不是可配置的模板。

v5 的解决方案:架构与参数解耦

v5 将分词器架构(归一化器、预分词器、模型类型、后处理器、解码器)与训练好的参数(词表、合并规则)视为不同的个体。这与 PyTorch 将模型架构与学习到的权重分开的方式相呼应。

就像 nn.Module 先定义层级:

from torch import nn

model = nn.Sequential(
    nn.Embedding(vocab_size, embed_dim),
    nn.Linear(embed_dim, hidden_dim),
)
# 架构已定义;权重可随后随机初始化或加载

v5 分词器遵循同样的模式:

from transformers import LlamaTokenizer

# 实例化架构
tokenizer = LlamaTokenizer()

# 在自己的数据上训练以填充词表和合并规则
tokenizer.train(files=["my_corpus.txt"])

现在,分词器类显式声明了其结构。查看 v5 的 LlamaTokenizer,你可以立即看到:

  • 它使用 BPE 作为分词模型。
  • 它可能在文本前添加 前缀空格
  • 它的特殊 Token (unkboseos)位于词表的特定位置。
  • 不对输入文本进行归一化
  • 它的解码器会将元空间字符 替换回空格。

v5 中的 LlamaTokenizer 内部视图

这种透明度在 v4 中是无法实现的,因为这些信息以前都埋藏在序列化文件中。

一个文件、一个后端、一条推荐路径

v5 将双文件系统合并为每个模型一个文件LlamaTokenizer 现在继承自 TokenizersBackend,它封装了以前被称为“快速”实现的 Rust 分词器,且现在将其作为默认项。

前一个“慢速”的 Python 实现明确地位于 PythonBackend 之后,而 SentencePieceBackend 仍然保留用于需要它的模型,但基于 Rust 的分词是首选的默认实现。

这消除了代码冗余和令人困惑的 TokenizerTokenizerFast 命名之争、慢/快之间重复的代码以及专门用于检查快慢一致性的测试套件。

现在用户有一个明确的入口。需要自定义的高级用户仍然可以访问底层组件,但库不再强迫所有人导航两个并行实现。

你现在可以从头训练特定模型的分词器

假设你想要一个行为与 LLaMA 完全一致的分词器(相同的归一化、预分词和 BPE 类型),但要在特定领域(如医疗、法律或新语言)的语料上进行训练。在 v4 中,你需要手动从底层库构建流水线;在 v5 中,直接实例化架构并调用 train 即可:

from transformers import LlamaTokenizer
from datasets import load_dataset

# 初始化空白分词器
tokenizer = LlamaTokenizer()

dataset = load_dataset("wikitext", "wikitext-2-raw-v1", split="train")

def get_training_corpus():
    batch = 1000
    for i in range(0, len(dataset), batch):
        yield dataset[i : i + batch]["text"]

trained_tokenizer = tokenizer.train_new_from_iterator(
    text_iterator=get_training_corpus(),
    vocab_size=32000,
    length=len(dataset),
    show_progress=True,
)

trained_tokenizer.push_to_hub("my_custom_tokenizer")
tokenizer = LlamaTokenizer.from_pretrained("my_custom_tokenizer")

得到的分词器将拥有你自定义的词表,但在处理空格、特殊 Token 约定和解码行为上,与标准 LLaMA 分词器完全一致。

特性V4V5
每个模型的文件数两个 (tokenization_X.py, _fast.py)一个 (tokenization_X.py)
默认后端Python 与 Rust 并行优选 Rust (TokenizersBackend)
架构可见性隐藏在序列化文件中在类定义中显式呈现
从头训练需要手动构建流水线tokenizer.train(files=[...])
组件检查困难、缺乏文档直接访问属性 (tokenizer.normalizer 等)
父类PreTrainedTokenizer(Fast)TokenizersBackend

从“作为加载分词器的 Checkpoint”到“作为可配置的架构”的转变,使库变得更加模块化、透明,并更符合从业者构建机器学习系统的心智模型。

总结

Transformers v5 为分词带来了三项核心改进:

  1. 单模型单文件:取代了以往复杂的双重实现。
  2. 架构可见:你可以直接检查归一化器、预分词器和解码器。
  3. 可训练模板:让你能轻松创建与任何模型设计匹配的自定义分词器。

tokenizers 与 Transformers 之间的封装层依然至关重要,它提供了原始分词器不具备的模型感知、长度管理和聊天模板功能。而 v5 只是让这一层变得更加清晰和可定制。

如果你想了解更多,可以参考以下资源:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

故事挺秃然

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值