PaddleNLP MBart 分词器深度解析:MBartTokenizer 与 MBart50Tokenizer 的架构原理与实战使用
PaddleNLP MBart 分词器深度解析:MBartTokenizer 与 MBart50Tokenizer 的架构原理与实战使用
MBart 是面向多语言序列到序列任务的预训练模型,其分词器的核心挑战在于:在一个共享的 SentencePiece 词表之上叠加"语言代码"(language code)特殊 Token,从而让同一个模型支撑多语言翻译。本文以 PaddleNLP 仓库中 paddlenlp.transformers.mbart.tokenizer 模块为对象,系统讲解 MBartTokenizer 与 MBart50Tokenizer 两个分词器的词表布局、fairseq 偏移机制、语言代码管理、特殊 Token 拼接规则与完整 API 用法,并给出基于 mbart-large-en-ro、mbart-large-50-one-to-many-mmt 的翻译数据预处理实战示例。读完本文,你将能够独立完成 MBart 系列模型的文本编码、语言切换、decoder 输入构建与分词器持久化。
一、模块定位:MBart 分词器在模型家族中的角色
PaddleNLP 将 MBart 的实现拆分为三个文件,共同构成完整的多语言翻译能力:
- configuration.py:
MBartConfig,定义模型超参数(层数、注意力头数、词表大小等)与各预训练权重的下载地址; - modeling.py:
MBartModel及条件生成、问答、序列分类等变体,以及构建 decoder 输入的关键工具函数shift_tokens_right; - tokenizer.py:
MBartTokenizer与MBart50Tokenizer,负责将多语言文本切分为 subword 并注入语言代码。
其中 tokenizer.py 通过 __all__ = ["MBartTokenizer", "MBart50Tokenizer"] 对外暴露两个类,并在模块级声明了预训练模型的位置编码长度映射:
MBART_PRETRAINED_POSITIONAL_EMBEDDINGS_SIZES = {
"mbart-large-cc25": 1024,
"mbart-large-en-ro": 1024,
}
MBART50_PRETRAINED_POSITIONAL_EMBEDDINGS_SIZES = {
"mbart-large-50-one-to-many-mmt": 1024,
"mbart-large-50-many-to-one-mmt": 1024,
"mbart-large-50-many-to-many-mmt": 1024,
}
也就是说,两个分词器共覆盖 5 个官方 checkpoint:mbart-large-cc25(多语言去噪预训练)、mbart-large-en-ro(英语-罗马尼亚语微调)、以及三个 MBart-50 多语言机器翻译模型。分词器资源文件统一为 sentencepiece.bpe.model,通过 pretrained_resource_files_map 声明,例如 mbart-large-en-ro 对应的词表即下载自该映射中登记的地址(见 tokenizer.py)。
二、核心机制:SentencePiece 词表 + fairseq 偏移的特殊词表布局
MBart 分词器没有为语言代码单独维护一张词表,而是在 SentencePiece 模型之外"追加"一段虚拟词表区间。理解这段布局,是理解 MBart 分词器一切行为的前提。
在 __init__ 中(见 tokenizer.py),初始化顺序如下:
- 加载 SentencePiece 模型
self.sp_model = spm.SentencePieceProcessor(...); - 定义
self.fairseq_offset = 1,这是与 fairseq 保持兼容的关键偏移量; - 定义基础特殊 Token 的固定 id:
{"<s>": 0, "<pad>": 1, "</s>": 2, "<unk>": 3}; - 计算语言代码的 id:
code_id = sp_model_size + i + fairseq_offset,其中i是语言代码在FAIRSEQ_LANGUAGE_CODES中的下标; <mask>的 id 被放在语言代码之后:len(sp_model) + len(lang_code_to_id) + fairseq_offset。
以 mbart-large-en-ro(SentencePiece 词表 250000 条)为例,可以精确推导出测试用例中出现的数字:
| Token | 计算公式 | 结果 |
|---|---|---|
ar_AR |
250000 + 0 + 1 | 250001 |
en_XX |
250000 + 3 + 1 | 250004 |
ro_RO |
250000 + 19 + 1 | 250020 |
<mask> |
250000 + 25 + 1 | 250026 |
这些数值与 tests/transformers/mbart/test_tokenizer.py 中的断言完全吻合。对于 MBart-50(52 个语言代码),<mask> 为 250053,mr_IN 为 250038,同样可以在 tests/transformers/mbart50/test_tokenizer.py 中找到对应断言。
2.1 普通 token 的 id 转换
词表布局的特殊性同样体现在 token 与 id 的互转上(tokenizer.py):
_convert_token_to_id:先在fairseq_tokens_to_ids中查找(覆盖<s>、<pad>、</s>、<unk>、<mask>与全部语言代码);否则调用sp_model.PieceToId(token)得到原始 id 后 加上fairseq_offset(即 +1);若 Piece 不存在则返回unk_token_id;_convert_id_to_token:反向操作,普通 id 需 减去fairseq_offset后再交给sp_model.IdToPiece。
这也是测试中 convert_tokens_to_ids(<a href="https://link.gitcode.com/i/f20d811bd9e0ac9c8d403ebf36f3e4be" target="_blank">"<mask>", "ar_AR"]) 得到 [250026, 250001] 的原因。vocab_size 属性因此定义为 len(sp_model) + len(lang_code_to_id) + fairseq_offset + 1([tokenizer.py),即 SentencePiece 词表、语言代码、<mask> 与偏移量之和。
2.2 与模型配置的词表一致性
分词器 id 布局必须与模型 embedding 矩阵大小严格对应。configuration.py 中 mbart-large-cc25 与 mbart-large-en-ro 的 vocab_size 为 250027,MBart-50 系列为 250054。以 MBart-50 为例:250000(SentencePiece)+ 52(语言代码)+ 1(偏移)+ 1(<mask>)= 250054,两边完全吻合,保证了 convert_ids_to_tokens 得到的每个 id 都能落在 embedding 的有效区间内。
三、MBartTokenizer 与 MBart50Tokenizer:语言覆盖差异
两个分词器类结构几乎完全一致,唯一实质区别在于 FAIRSEQ_LANGUAGE_CODES 的规模:
MBartTokenizer支持 25 种语言代码(tokenizer.py):ar_AR, cs_CZ, de_DE, en_XX, es_XX, et_EE, fi_FI, fr_XX, gu_IN, hi_IN, it_IT, ja_XX, kk_KZ, ko_KR, lt_LT, lv_LV, my_MM, ne_NP, nl_XX, ro_RO, ru_RU, si_LK, tr_TR, vi_VN, zh_CN,服务于mbart-large-cc25与mbart-large-en-ro;MBart50Tokenizer在前 25 种之上追加 27 种(tokenizer.py):af_ZA, az_AZ, bn_IN, fa_IR, he_IL, hr_HR, id_ID, ka_GE, km_KH, mk_MK, ml_IN, mn_MN, mr_IN, pl_PL, ps_AF, pt_XX, sv_SE, sw_KE, ta_IN, te_IN, th_TH, tl_XX, uk_UA, ur_PK, xh_ZA, gl_ES, sl_SI,合计 52 种,服务于三个mbart-large-50-*翻译模型。
由于语言代码 id 由列表下标决定,MBart50Tokenizer 中前 25 种语言的 id 与 MBartTokenizer 保持一致,后 27 种从 250026 开始依次编号。需要特别注意的是:语言代码在类内部以 lang_code_to_id 字典保存,并整体注入 _additional_special_tokens(tokenizer.py),因此所有语言代码都被视为 special token,解码时可通过 skip_special_tokens=True 自动剔除。
四、构造函数与参数详解
两个类的 __init__ 签名完全一致(tokenizer.py):
| 参数 | 默认值 | 说明 |
|---|---|---|
vocab_file |
必填 | sentencepiece.bpe.model 的本地路径或可加载路径 |
src_lang |
None(回退 en_XX) |
源语言代码,决定编码时追加到序列末尾的语言代码 |
tgt_lang |
None |
目标语言代码,供翻译流程与解码使用 |
bos_token |
"<s>" |
序列起始标记,MBart 实际不使用 BOS |
eos_token |
"</s>" |
序列结束标记,id 为 2 |
sep_token |
"</s>" |
分隔标记,与 eos 共用 |
cls_token |
"<s>" |
分类标记,与 bos 共用 |
unk_token |
"<unk>" |
未知词标记,id 为 3 |
pad_token |
"<pad>" |
填充标记,id 为 1 |
mask_token |
"<mask>" |
掩码标记,构造时包装为 AddedToken(mask_token, lstrip=True, rstrip=False),即掩码 Token 左侧不留空格 |
sp_model_kwargs |
None |
透传给 sentencepiece.SentencePieceProcessor 的额外关键字参数(如 keep_accents 等) |
additional_special_tokens |
None |
额外的特殊 Token 列表,仅在不在既有集合中时追加 |
构造完成后立即执行 set_src_lang_special_tokens(self.src_lang)(tokenizer.py),因此 prefix_tokens 与 suffix_tokens 在创建时即按源语言配置完毕。
五、文本编码调用:call 的参数体系与旧参数兼容
MBartTokenizer.__call__(tokenizer.py)在转发给基类 PretrainedTokenizer 之前,做了三层旧参数兼容处理,方便从旧版 PaddleNLP 代码平滑迁移:
pad_to_max_seq_len=True→padding="max_length",否则padding=False;max_seq_len→max_length;truncation_strategy(非"longest_first"时)→truncation。
其余参数语义如下:
| 参数 | 默认值 | 说明 |
|---|---|---|
text / text_pair |
必填 / None |
单条文本或文本对;MBart 通常只处理单条序列,文本对按无分隔符方式拼接 |
max_length |
None |
序列最大长度(配合 truncation 使用) |
stride |
0 |
溢出窗口的步长,配合 return_overflowing_tokens 使用 |
is_split_into_words |
False |
输入是否已按词切分 |
padding |
None(最终解析为 False) |
True / "longest" / "max_length" 等 |
truncation |
"longest_first" |
截断策略,默认从最长序列截断 |
return_position_ids / return_token_type_ids |
False |
是否返回位置 id / token 类型 id,MBart 默认不返回 |
return_attention_mask |
True |
是否返回注意力掩码 |
return_length / return_overflowing_tokens / return_special_tokens_mask |
False |
附加返回项 |
model_input_names = <a href="https://link.gitcode.com/i/fb796bdc3dcd220e2cdf171044ed1cb3" target="_blank">"input_ids"]([tokenizer.py)表明该分词器的标准模型输入为 input_ids。典型调用方式:
from paddlenlp.transformers import MBartTokenizer
tokenizer = MBartTokenizer.from_pretrained("mbart-large-en-ro", src_lang="en_XX", tgt_lang="ro_RO")
inputs = tokenizer(
["UN Chief Says There Is No Military Solution in Syria"],
max_length=64,
padding=True,
truncation=True,
return_tensors="pd",
)
print(inputs["input_ids"].shape)
六、特殊 Token 拼接:两种语言代码摆放格式
MBart 与 MBart-50 系列在特殊 Token 的摆放位置上存在重要差异,这是两个类最核心的行为分叉,分别实现在各自的 set_src_lang_special_tokens / set_tgt_lang_special_tokens 中:
MBartTokenizer(eos + 语言代码后缀)(tokenizer.py):
self.prefix_tokens = []
self.suffix_tokens = [self.eos_token_id, self.cur_lang_code_id]
即编码器输入格式为 X <a href="https://link.gitcode.com/i/f937a4376f1a8f7e5c12e6d130d605c6" target="_blank">eos, src_lang_code],语言代码放在 eos 之后。测试中的 expected_src_tokens 末尾为 [..., 2, EN_CODE]([tests/transformers/mbart/test_tokenizer.py)正是这一格式的直接证据。
MBart50Tokenizer(语言代码前缀 + eos 后缀)(tokenizer.py):
self.prefix_tokens = [self.cur_lang_code_id]
self.suffix_tokens = [self.eos_token_id]
即编码器输入格式为 <a href="https://link.gitcode.com/i/4acce65f0d9acb8611f09b261ee1a0cf" target="_blank">src_lang_code] X [eos],语言代码放在 序列最前面。对应测试断言 expected_src_tokens = [EN_CODE, 8274, ..., 2]([tests/transformers/mbart50/test_tokenizer.py)。
两种格式统一由 build_inputs_with_special_tokens 完成拼接(tokenizer.py):单序列为 prefix_tokens + token_ids_0 + suffix_tokens;文本对则无分隔符地拼接为 prefix_tokens + token_ids_0 + token_ids_1 + suffix_tokens。MBart 系列 从不使用 BOS。get_special_tokens_mask 与之对应,将前缀与后缀位置标记为 1、正文标记为 0(tokenizer.py),build_offset_mapping_with_special_tokens 则为特殊 Token 位置填充 (0, 0) 偏移(tokenizer.py)。
在翻译任务中切换语言只需调用对应方法:
tokenizer.set_src_lang_special_tokens("en_XX")
tokenizer.set_tgt_lang_special_tokens("ro_RO")
七、常用 API 速览
除上述方法外,分词器还提供一组可直接调用的基础能力:
tokenize(text):底层由_tokenize调用sp_model.encode(text, out_type=str)完成 SentencePiece 切分(tokenizer.py),返回带▁前缀的 subword 列表;convert_tokens_to_ids(tokens)/convert_ids_to_tokens(ids):按第二节介绍的偏移规则互转;convert_tokens_to_string(tokens)/convert_ids_to_string(ids):将 subword 序列还原为字符串,核心逻辑是"".join(tokens).replace("▁", " ").strip()(tokenizer.py),即把 SentencePiece 的空格标记▁还原为真实空格并去除首尾空白;get_vocab():返回{token: id}全量词表,并在末尾合并added_tokens_encoder(tokenizer.py);vocab_size:属性,返回含语言代码与<mask>的总词表大小;decode(ids, skip_special_tokens=True):继承自基类,skip_special_tokens=True时可自动剔除语言代码与 eos 等特殊 Token,得到纯净译文文本——集成测试test_enro_tokenizer_decode_ignores_language_codes(tests/transformers/mbart/test_tokenizer.py)专门验证了这一点。
八、翻译数据预处理实战
8.1 用 shift_tokens_right 构建 decoder 输入
训练 MBart 条件生成时,decoder 的输入由标签右移一位得到。与普通 seq2seq 模型不同,MBart 的右移并非简单填充 pad id,而是把最后一列的非 pad Token(即目标语言代码 <LID>)搬运到序列首位,这正是 modeling.py 中 shift_tokens_right 的实现:
def shift_tokens_right(input_ids, pad_token_id):
shifted_input_ids = input_ids.clone()
...
index_of_eos = paddle.cast(shifted_input_ids != pad_token_id, dtype="int32").sum(axis=-1) - 1
decoder_start_tokens = paddle.gather(input_flat, index + index_of_eos.astype(index.dtype))
shifted_input_ids[:, 1:] = shifted_input_ids[:, :-1].clone()
shifted_input_ids[:, 0] = decoder_start_tokens
return shifted_input_ids
测试中 batch<a href="https://link.gitcode.com/i/e82c56c712f87724aba1755b67c60522" target="_blank">"decoder_input_ids"] = shift_tokens_right(labels, tokenizer.pad_token_id)([tests/transformers/mbart/test_tokenizer.py)演示了标准的训练数据组装流程:
from paddlenlp.transformers.mbart.modeling import shift_tokens_right
batch = tokenizer(src_text, padding=True, truncation=True, max_length=3, return_tensors="pd")
targets = tokenizer(tgt_text, padding=True, truncation=True, max_length=10, return_tensors="pd")
labels = targets["input_ids"]
batch["decoder_input_ids"] = shift_tokens_right(labels, tokenizer.pad_token_id)
8.2 用 _build_translation_inputs 准备推理输入
MBart50Tokenizer._build_translation_inputs(tokenizer.py)专为翻译推理设计,其行为被单测完整覆盖(tests/transformers/mbart50/test_tokenizer.py):
inputs = tokenizer._build_translation_inputs(
"A test", return_tensors="pd", src_lang="en_XX", tgt_lang="ar_AR"
)
# input_ids: [[250004, 62, 3034, 2]] (en_XX + 原文 + eos)
# forced_bos_token_id: 250001 (ar_AR)
它完成三件事:校验 src_lang/tgt_lang 必须提供;以 add_special_tokens=True 编码原文并附带源语言代码;将目标语言代码作为 forced_bos_token_id 写入返回结果,从而强制解码器从目标语言代码开始生成。
九、分词器持久化与序列化
MBart 分词器在 pickle 序列化与磁盘保存两方面都做了专门处理:
__getstate__/__setstate__(tokenizer.py):pickle 时把不可序列化的SentencePieceProcessor对象置空,改为保存其serialized_model_proto()二进制原型;反序列化时再通过LoadFromSerializedProto恢复,并兼容旧版本缺失sp_model_kwargs属性的情况;save_resources(tokenizer.py):将vocab_file复制为sentencepiece.bpe.model写入保存目录;若原始词表文件已不存在,则直接写出序列化原型,保证分词器在任何情况下都能完整落盘。
测试 test_special_tokens_unaffacted_by_save_load(tests/transformers/mbart/test_tokenizer.py)验证了 save_pretrained 后再 from_pretrained 加载,fairseq_tokens_to_ids(含语言代码与 <mask> 映射)保持完全一致。
十、测试验证与集成要点
MBart 分词器在仓库中有完整的测试支撑:
- tests/transformers/mbart/test_tokenizer.py:覆盖完整 tokenize/convert 往返、
mbart-large-en-ro集成、截断、mask 与特殊 Token 保存加载; - tests/transformers/mbart50/test_tokenizer.py:覆盖 MBart-50 词表大小(
vocab_size == 1054,基于 fixture 小词表)、_build_translation_inputs与翻译集成。
实践中的关键集成要点可总结为:
- 语言代码必须显式指定:
src_lang默认回退为en_XX,训练/推理前务必通过构造参数或set_src_lang_special_tokens明确源语言; - 两代模型格式不同:
mbart-large-*(cc25/en-ro)语言代码在序列尾部(X [eos, lang]),mbart-large-50-*语言代码在序列头部([lang] X [eos]),切勿混用; - id 对齐以配置为准:分词器的
vocab_size必须与MBartConfig.vocab_size一致(250027 / 250054),自定义训练时两者需同步修改; - decoder 输入用 shift_tokens_right:不要用普通 pad 填充做右移,否则会丢失目标语言代码导致生成质量下降。
通过上述机制,MBart 分词器把"多语言共享词表"与"语言条件控制"无缝融合,是 PaddleNLP 中实现多语言翻译、多语言去噪预训练等任务的基础组件。若需进一步了解模型侧行为,可继续阅读 paddlenlp/transformers/mbart/modeling.py 与 paddlenlp/transformers/mbart/configuration.py。