【Bug已解决】XLMRobertaTokenizer.initpasses dict to Unigram(vocab=...) expecting a Sequence 解决方案
一、现象长什么样
初始化XLMRobertaTokenizer(或其相关 tokenizer)时,报:
TypeError: Unigram.__init__() got a dict for vocab=..., expected a Sequence (list)或:
AssertionError: vocab must be a list of strings, got dict最迷惑的是:你"只是正常AutoTokenizer.from_pretrained("xlm-roberta-base")"就想加载,却炸在 tokenizer 的__init__里——说明问题不在你的代码,而在XLMRobertaTokenizer.__init__把参数传给底层的Unigram(SentencePiece 的 unigram 实现)时,类型传错了。
本质:XLMRobertaTokenizer底层用 SentencePiece 的Unigram,它的vocab参数期望一个Sequence(即 list/tuple of token 字符串)。但XLMRobertaTokenizer.__init__在构造时,把vocab当成了一个dict(比如{token: id}形式的映射,或把sp_model之外额外传的 vocab 字典)传给了Unigram(vocab=...)。Unigram 只接受 list,于是TypeError。
二、背景
SentencePiece 的Unigram模型在transformers里的接口大致是:
Unigram(vocab=list_of_tokens, ...)vocab应该是一个token 字符串的列表(有序,索引即 id)。这是Sequence语义。
而XLMRobertaTokenizer的__init__在某些情况下(尤其是自定义构造、或从一个vocabdict 初始化而非从spm模型文件加载)会写出:
# 错误写法 Unigram(vocab=self.vocab, ...) # self.vocab 是 dict {token: id}这里self.vocab是{token: id}字典(从vocab.json或某些转换路径得到),但Unigram要的是 list。于是类型错配。
常见触发场景:
- 你手动
XLMRobertaTokenizer(vocab=some_dict)构造(而非from_pretrained); - 转换脚本把
vocab.json(dict)直接喂给 tokenizer 构造; - 某些版本里
__init__的 vocab 处理逻辑把 list 和 dict 搞混。
下面用可运行代码复现"Unigram 收到 dict 而非 list 报错"。
三、根因
根因一句话:XLMRobertaTokenizer.__init__把vocab以 dict({token: id})形式传给了底层Unigram(vocab=...),而Unigram的vocab参数期望Sequence(list of token 字符串),类型错配导致 TypeError。
三个具体失配:
- vocab 类型错:dict 传给要 list 的
Unigram。 - dict→list 缺失:
__init__没把{token:id}转成有序 token 列表。 - 构造路径混淆:
from_pretrained(走 spm 文件)正常,手动vocab=dict构造才触发。
四、最小可运行复现
用纯 Python 模拟"Unigram 期望 list,收到 dict 报错":
from dataclasses import dataclass from typing import Sequence, Dict, List, Union class Unigram: def __init__(self, vocab: Sequence[str]): if not isinstance(vocab, (list, tuple)): raise TypeError( f"Unigram vocab 期望 Sequence(list),收到 {type(vocab).__name__}" ) self.vocab = list(vocab) def xlm_roberta_init(vocab: Union[Dict, List]): """模拟 XLMRobertaTokenizer.__init__:直接把 vocab 传给 Unigram。""" return Unigram(vocab=vocab) def main(): vocab_dict = {"<s>": 0, "<pad>": 1, "a": 2} # dict 形式 try: xlm_roberta_init(vocab_dict) except TypeError as e: print("复现到报错:", e) # 修复:dict -> 按 id 排序的 token 列表 vocab_list = [t for t, _ in sorted(vocab_dict.items(), key=lambda kv: kv[1])] u = xlm_roberta_init(vocab_list) print("修复后 vocab 列表:", u.vocab) if __name__ == "__main__": main()运行会先打印复现到报错: Unigram vocab 期望 Sequence(list),收到 dict,再打印修复后的列表——正是 dict 误传的本质。
五、解决方案(第一层:最小直接修复)
最立竿见影的修复:在传给Unigram(vocab=...)之前,把 dict 形式的 vocab 转换成"按 id 排序的 token 字符串列表"(dict 的 key 是 token、value 是 id,转 list 时必须按 id 排序以保证索引=id)。**
from typing import Dict, List, Union def vocab_dict_to_list(vocab: Union[Dict[str, int], List[str]]) -> List[str]: """修复:dict {token: id} -> 按 id 升序的 token 列表。""" if isinstance(vocab, dict): return [t for t, _ in sorted(vocab.items(), key=lambda kv: kv[1])] return list(vocab) def build_unigram(vocab): from dataclasses import dataclass @dataclass class Unigram: vocab: list return Unigram(vocab=vocab_dict_to_list(vocab)) def main(): vocab = {"<s>": 0, "a": 2, "<pad>": 1} u = build_unigram(vocab) print("正确 vocab 列表(索引=id):", u.vocab) # ['<s>', '<pad>', 'a'] if __name__ == "__main__": main()第一层修复让Unigram收到的永远是正确的 list,TypeError 消失,且索引与 id 对齐。
六、解决方案(第二层:结构性改进)
把"vocab 归一化为 Unigram 期望的 list"收口成一个VocabNormalizer,在XLMRobertaTokenizer.__init__构造Unigram前统一处理:dict→按 id 排序 list,list→原样,非法类型报错。
from dataclasses import dataclass from typing import Dict, List, Union @dataclass class VocabNormalizer: def to_unigram_vocab(self, vocab: Union[Dict[str, int], List[str]]) -> List[str]: if isinstance(vocab, dict): # 按 id 升序,保证 list 索引 == token id return [t for t, _ in sorted(vocab.items(), key=lambda kv: kv[1])] if isinstance(vocab, (list, tuple)): return list(vocab) raise TypeError(f"vocab 必须是 dict 或 list,收到 {type(vocab)}") def main(): n = VocabNormalizer() for raw in ({"<s>": 0, "a": 2}, ["<s>", "a"]): print("归一化:", n.to_unigram_vocab(raw)) if __name__ == "__main__": main()第二层的关键是VocabNormalizer把"vocab 转 list"固化,并严格校验类型,后续任何Unigram(vocab=...)调用前都过它,杜绝 dict 误传。
七、解决方案(第三层:断言 / CI 守护)
加 pytest 守护:(1) dict vocab 被转成按 id 排序的 list;(2) list vocab 原样保留;(3) 非法类型必须被拒;(4) 转换后list[index]对应原 dict 的 id。
import pytest def to_list(vocab): if isinstance(vocab, dict): return [t for t, _ in sorted(vocab.items(), key=lambda kv: kv[1])] if isinstance(vocab, (list, tuple)): return list(vocab) raise TypeError("bad type") def test_dict_sorted_by_id(): out = to_list({"<s>": 0, "a": 2, "<pad>": 1}) assert out == ["<s>", "<pad>", "a"] def test_list_passthrough(): assert to_list(["x", "y"]) == ["x", "y"] def test_invalid_rejected(): with pytest.raises(TypeError): to_list(123) def test_index_matches_id(): d = {"<s>": 0, "a": 2, "<pad>": 1} out = to_list(d) assert out[0] == "<s>" and out[2] == "a" if __name__ == "__main__": pytest.main([__file__, "-q"])CI 里test_dict_sorted_by_id+test_index_matches_id通过,就能保证 vocab dict 转 list 后索引与 id 对齐,杜绝Unigram收到 dict 的回归。
八、排查清单
XLMRobertaTokenizer.__init__报 vocab 类型错误时,按此顺序查:
- 确认报错在 tokenizer 构造:stack 指向
Unigram(vocab=...)收到 dict。 - 检查你如何构造:是
from_pretrained(通常走 spm 文件,正常)还是手动vocab=dict构造(触发)。 - 第一层修复:手动构造时把
{token:id}dict 转成按 id 排序的 token 列表再传。 - 确认索引=id:转换后
list[i]必须对应原 dict 里 id=i 的 token,否则词表全错。 - 用 VocabNormalizer 兜底:构造
Unigram前统一归一化。 - 优先 from_pretrained:若可用,直接加载 spm 模型文件,绕开 vocab dict 构造路径。
- 升级 transformers:部分版本已修正该
__init__的 vocab 处理。
九、小结
XLMRobertaTokenizer.__init__把 dict 传给Unigram(vocab=...)期望 Sequence 而报错,根因不在 SentencePiece 坏,而在**XLMRobertaTokenizer底层用Unigram,其vocab要 list of token 字符串,但__init__在手动构造时把{token: id}字典直接传了进去,类型错配导致 TypeError**。正常from_pretrained(走 spm 文件)不受影响,手动vocab=dict构造才触发。
修复三层:第一层,构造Unigram前把 dict 转成"按 id 排序的 token 列表"(保证 list 索引=token id);第二层用VocabNormalizer把"dict→list、list→原样、非法报错"固化;第三层用 pytest 断言"dict 按 id 排序转 list、索引与 id 对齐"。记住:Unigram的 vocab 是 list 不是 dict;手动构造时把{token:id}按 id 排成列表,索引才对得上。