以下是基于 transformers 5.19.0 源码编写
源代码:
python
def decode(
self,
token_ids: int | list[int] | list[list[int]] | np.ndarray | torch.Tensor,
skip_special_tokens: bool = False,
**kwargs,
) -> str | list[str]:
"""
Converts a sequence of ids into a string, or a list of sequences into a list of strings,
using the tokenizer and vocabulary with options to remove special tokens and clean up
tokenization spaces.
Similar to doing `self.convert_tokens_to_string(self.convert_ids_to_tokens(token_ids))`.
Args:
token_ids (`Union[int, list[int], list[list[int]], np.ndarray, torch.Tensor]`):
A single sequence or a batch (list of sequences) of tokenized input ids. Can be obtained using the
`__call__` method.
skip_special_tokens (`bool`, *optional*, defaults to `False`):
Whether or not to remove special tokens in the decoding.
kwargs (additional keyword arguments, *optional*):
Will be passed to the underlying model specific decode method.
Returns:
`Union[str, list[str]]`: The decoded string for a single sequence, or a list of decoded strings for a
batch of sequences.
"""
# Convert inputs to python lists
token_ids = to_py_obj(token_ids)
# If we received batched input, decode each sequence
if isinstance(token_ids, (list, tuple)) and len(token_ids) > 0 and isinstance(token_ids[0], (list, tuple)):
clean_up_tokenization_spaces = kwargs.pop("clean_up_tokenization_spaces", None)
return [ #发现是 batch,就返回列表
self._decode(
token_ids=seq,
skip_special_tokens=skip_special_tokens,
clean_up_tokenization_spaces=clean_up_tokenization_spaces,
**kwargs,
)
for seq in token_ids
]
return self._decode(
token_ids=token_ids,
skip_special_tokens=skip_special_tokens,
**kwargs,
)
一、decode() 是什么?有什么用?
tokenizer.decode() 定义在 PreTrainedTokenizerBase 中,是整个 Tokenizer 流水线中文本解码的公开入口方法。它的核心职责是:把模型输出的 Token ID(整数)还原成人类可读的字符串。
从源码注释可以看到,它的概念等价于:
pythonself.convert_tokens_to_string(self.convert_ids_to_tokens(token_ids))
也就是说,理论上它做了三步:ID → Token字符串 → 拼接成完整文本。但实际上,为了性能,底层(TokenizersBackend._decode)直接调用了 Rust 后端的解码器,一步完成,不走 Python 层的两步转换。
它的实际用处:
- 模型生成结果的还原 :模型
generate()返回的是 Token ID 序列,必须用decode()转回文本才能看到模型说了什么。 - 调试分词结果:编码后再解码,可以验证 Tokenizer 是否正确保留了原文语义。
- 后处理管道的一环:在文本生成任务(对话、翻译、摘要)中,它是生成链路最后一环。
二、完整函数签名(基于源代码)
PreTrainedTokenizerBase.decode 的实际定义签名如下:
python
def decode(
self,
token_ids: int | list[int] | list[list[int]] | np.ndarray | torch.Tensor,
skip_special_tokens: bool = False,
**kwargs,
) -> str | list[str]:
注意一个重要的细节 :clean_up_tokenization_spaces 并不是 decode() 的显式参数,而是通过 **kwargs 透传下去的。这一点很多文档和教程都写错了。它在 batch_decode() 中才是显式参数(batch_decode 内部再传给 decode)。
三、逐参数详解
token_ids(必填)
这是你要解码的 Token ID。它的类型非常灵活,源码中的类型注解是:
int | list[int] | list[list[int]] | np.ndarray | torch.Tensor
也就是说,它可以是以下几种形式:
- 单个整数 (
int):解码单个 Token ID,返回该 ID 对应的 token 字符串。 - 一维列表/数组 (
list[int]、np.ndarray、torch.Tensor):解码一个完整的 Token 序列,返回拼接后的字符串。 - 二维列表 (
list[list[int]]):多个 Token 序列的批次,返回字符串列表。
源码中第一步就是调用 to_py_obj(token_ids) 把张量/数组转成 Python 原生对象(注意:这里说的"原生对象"不一定是列表------如果输入是标量张量如 torch.tensor(5),to_py_obj 会返回 Python int;如果输入是一维张量,返回 list;如果是二维张量,返回 list[list])。然后判断是否是批次输入(isinstance(token_ids, (list, tuple)) and len(token_ids) > 0 and isinstance(token_ids[0], (list, tuple))),如果是批次就循环调用 _decode,否则单次调用 _decode。
来源 :通常来自 tokenizer.encode() 的返回值、tokenizer().__call__() 返回值中的 input_ids 字段、或模型 generate() 输出的 ID 序列。
补充 :to_py_obj 还会自动处理 GPU 张量的设备转移------如果传入的是 GPU 上的 Tensor,它会先 .cpu() 再转成 NumPy 再转成 Python 列表。这对单条解码影响不大,但在大批量解码时需要注意这个隐式的 CPU 转移开销。
skip_special_tokens(可选,默认False)
控制解码时是否跳过(移除)特殊 Token。
False(默认) :特殊 Token 会被保留在输出中,你会看到类似<bos>你好<eos>这样的结果。True:特殊 Token(如<bos>、<eos>、<pad>、<unk>等)会被过滤掉,输出更干净。
这个参数会被直接传给底层 Rust 后端的解码器(self._tokenizer.decode(token_ids, skip_special_tokens=skip_special_tokens)),在 Rust 层完成过滤,效率很高。
建议 :日常查看生成结果时设为 True;需要完整还原编码结果(如验证 decode(encode(text)) 是否一致)时设为 False。
**kwargs(可选)
这是 Python 的可变关键字参数,会收集所有未显式声明的关键词参数,并原样传给底层 _decode 方法。在实际使用中,你最可能通过它传递的是:
clean_up_tokenization_spaces:控制是否清理分词引入的多余空格。虽然它不是decode()的显式参数,但可以通过**kwargs透传到TokenizersBackend._decode()中生效。
四、clean_up_tokenization_spaces 的重要说明(通过 kwargs 传递)
虽然 clean_up_tokenization_spaces 不是 decode() 的显式参数,但它确实可以通过 kwargs 传递并生效。不过有一个非常重要的坑需要你了解:
在 TokenizersBackend._decode() 的源码中,对于 BPE 类型的分词器(Qwen3.5 就是 BPE),代码有一段特殊处理:
python
if clean_up_tokenization_spaces:
# Skip cleanup for BPE tokenizers --- the cleanup was designed for
# WordPiece tokenizers and is destructive for BPE (it strips
# legitimate spaces before punctuation).
if (
type(self.backend_tokenizer.model).__name__ == "BPE"
and not self.clean_up_tokenization_spaces_for_bpe_even_though_it_will_corrupt_output
):
logger.warning_once(
"Ignoring clean_up_tokenization_spaces=True for BPE tokenizer"
...
)
else:
text = self.clean_up_tokenization(text)
这段话的意思是:
clean_up_tokenization_spaces这个后处理步骤最初是为 WordPiece 分词器(如 BERT)设计的。- 对于 BPE 分词器(如 Qwen3.5),这个清理操作是破坏性的------它会去掉标点符号前面的合法空格(比如 "Hello , world" 中的逗号前空格)。
- 因此,对于 BPE 分词器,默认忽略
clean_up_tokenization_spaces=True,并打印一次警告。 - 如果你执意要清理,需要额外设置
clean_up_tokenization_spaces_for_bpe_even_though_it_will_corrupt_output=True。
另外,self.clean_up_tokenization_spaces 这个实例属性的默认值是 False(在 PreTrainedTokenizerBase.__init__ 中:self.clean_up_tokenization_spaces = kwargs.pop("clean_up_tokenization_spaces", False)),所以默认情况下不会做空格清理。
源代码里有一段:
python
# By default, do not clean up tokenization spaces for both fast and slow tokenizers
self.clean_up_tokenization_spaces = kwargs.pop("clean_up_tokenization_spaces", False)
self.clean_up_tokenization_spaces_for_bpe_even_though_it_will_corrupt_output = kwargs.pop(
"clean_up_tokenization_spaces_for_bpe_even_though_it_will_corrupt_output", False
)
补充:默认值回退链
clean_up_tokenization_spaces 的最终生效值遵循以下回退链:
decode()调用时通过 kwargs 传入的值(如果传了True/False则直接用)- 如果 kwargs 中没传(即
None),回退到self.clean_up_tokenization_spaces实例属性(默认False) - 如果最终值为
True且分词器是 BPE 类型,则进一步回退到"忽略清理并打印警告"(除非设置了clean_up_tokenization_spaces_for_bpe_even_though_it_will_corrupt_output=True)
五、输入与输出到底是什么?
输入(Input)
| 项目 | 说明 |
|---|---|
| 参数名 | token_ids |
| 类型 | int、list[int]、list[list[int]]、np.ndarray、torch.Tensor |
| 维度要求 | 一维序列(单条)或二维序列(批次) |
| 来源 | tokenizer.encode()、tokenizer()、模型 generate() 的输出 |
输出(Output)
| 项目 | 说明 |
|---|---|
| 类型 | str(单条)或 list[str](批次) |
| 内容 | 将 Token IDs 映射回词汇表中的 Token 字符串,再拼接成完整句子 |
| 注意 | 输出不保证与原始输入逐字符完全一致(大小写、空格、标点可能略有差异) |
判断返回单条还是批次的关键代码在 decode() 方法中:
python
if isinstance(token_ids, (list, tuple)) and len(token_ids) > 0 and isinstance(token_ids[0], (list, tuple)):
# 批次输入 → 返回 list[str]
return [self._decode(...) for seq in token_ids]
# 单条输入 → 返回 str
return self._decode(token_ids=token_ids, skip_special_tokens=skip_special_tokens, **kwargs)
六、调用链路(源码视角)
当你调用 tokenizer.decode(ids) 时,实际的调用链是:
tokenizer.decode(ids) # PreTrainedTokenizerBase.decode --- 公开入口
→ to_py_obj(ids) # 张量/数组 → Python原生对象
→ 判断单条还是批次
→ self._decode(...) # 动态绑定到 TokenizersBackend._decode
→ self._tokenizer.decode(...) # Rust 后端解码(最快)
→ clean_up_tokenization_spaces # BPE 分词器默认跳过
→ return str
关键点 :Qwen3_5Tokenizer 自己没有定义 decode 或 _decode,所以全部继承自父类。这里需要补充一个细节:PreTrainedTokenizerBase 中其实也定义了 _decode 方法,但它是一个抽象方法 (直接 raise NotImplementedError),真正提供实现的是 TokenizersBackend._decode(因为 Qwen3_5Tokenizer 继承自 TokenizersBackend)。实际执行解码逻辑的是 TokenizersBackend._decode,它直接调用 Rust 后端的 self._tokenizer.decode(),这是最快的路径。
七、常用操作与代码示例
- 基础解码:还原编码结果
python
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen3.5-2B")
text = "你好世界"
ids = tokenizer.encode(text)
print(ids) # 例如: [151644, ..., 151645](具体ID取决于模型词表)
# 默认保留特殊token
print(tokenizer.decode(ids))
# 例如: <|im_start|>你好世界<|im_end|>
# 注意:encode() 只添加 BOS/EOS 等特殊token,不会应用聊天模板(chat template)
# 跳过特殊token
print(tokenizer.decode(ids, skip_special_tokens=True))
# 例如: 你好世界
tokenizer.encode()只会添加 BOS/EOS 等特殊 token,不会 应用聊天模板(chat template)。聊天模板的内容(如 "system\nYou are a helpful assistant.")只有通过apply_chat_template()才会被插入。
- 解码模型生成的结果(最常见的场景)
python
from transformers import AutoTokenizer, AutoModelForCausalLM
tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen3.5-2B")
model = AutoModelForCausalLM.from_pretrained("Qwen/Qwen3.5-2B")
inputs = tokenizer("你好,请介绍一下你自己", return_tensors="pt")
outputs = model.generate(**inputs, max_new_tokens=50) # 包含 输入内容 和 输出内容
# generate 返回的是 [1, seq_len] 的二维张量,取第一个样本
generated_text = tokenizer.decode(outputs[0], skip_special_tokens=True)
print(generated_text)
- 批次解码(decode 自身就支持,不需要 batch_decode)
源码注释明确说了:
"The
decodemethod now handles batched input natively, so you can usedecodedirectly instead ofbatch_decode."
python
# decode 直接支持批次输入
texts = tokenizer.decode([[1, 2, 3], [4, 5, 6]], skip_special_tokens=True)
# 返回: ["文本1", "文本2"]
- 从 model.generate 的完整输出中解码
python
# model.generate 返回的通常是 [batch_size, seq_len]
output_ids = model.generate(**inputs, max_new_tokens=50)
# 方法一:用 decode(自动识别批次)
decoded = tokenizer.decode(output_ids[0], skip_special_tokens=True)
# 方法二:用 batch_decode(显式批次)
decoded_batch = tokenizer.batch_decode(output_ids, skip_special_tokens=True)
- 解码单个 Token ID
python
# 传入单个整数
token_str = tokenizer.decode(151643)
print(token_str) # 输出该 ID 对应的 token 字符串
- 处理 dict 输入(某些 collator 输出格式)
TokenizersBackend._decode 中有这段代码:
python
if isinstance(token_ids, dict):
token_ids = token_ids["input_ids"]
所以如果你传入的是一个包含 "input_ids" 键的字典,它会自动提取。但这不是推荐用法,建议始终传入 ID 序列。
八、补充:decode() vs batch_decode() 的关系
从源码可以看到,batch_decode 的实现非常简单:
python
def batch_decode(
self,
sequences: list[int] | list[list[int]] | np.ndarray | torch.Tensor,
skip_special_tokens: bool = False,
clean_up_tokenization_spaces: bool | None = None,
**kwargs,
) -> list[str]:
# Forward to decode() which now handles batched input natively,
# so you can use `decode` directly instead of `batch_decode`.
result = self.decode(
token_ids=sequences,
skip_special_tokens=skip_special_tokens,
clean_up_tokenization_spaces=clean_up_tokenization_spaces,
**kwargs,
)
# Ensure we always return a list for backwards compatibility
if isinstance(result, str):
return [result]
return result
batch_decode 内部就是调用 decode,然后确保返回值一定是列表(如果 decode 返回了单字符串,就包一层列表)。所以现在直接用 decode 就行 ,batch_decode 保留只是为了向后兼容。
九、补充:decode() vs convert_ids_to_tokens() 的区别
很多人会混淆这两个方法,关键区别:
convert_ids_to_tokens(ids):只做 ID → Token字符串 的查表转换,返回list[str],不拼接,不处理空格。适合逐 token 调试。decode(ids):调用 Rust 后端解码器,完成 ID → Token → 字符串拼接 → 空格清理的完整流程,返回str。适合最终输出。
用源码的话说,decode 的概念等价于 convert_tokens_to_string(convert_ids_to_tokens(token_ids)),但实际走的是更快的 Rust 路径。
十、注意事项与常见坑
decode(encode(text))不一定等于text:由于 Tokenizer 的规范化(Normalization)规则,解码后的文本可能在大小写、空格、标点等方面与原文有细微差异。只要语义一致即可。- 不要传入二维张量给单条解码 :虽然
decode()现在支持批次输入,但如果你明确是单条序列,传一维列表即可。传二维会被当作批次处理,返回的是列表而不是字符串。 - BPE 分词器的
clean_up_tokenization_spaces默认无效 :Qwen3.5 使用 BPE 分词,所以即使你传clean_up_tokenization_spaces=True,默认也会被忽略并打印警告。这是设计如此,不是 bug。 skip_special_tokens的默认值是False:这意味着默认解码结果会包含特殊 token 字符串。日常使用几乎总是需要传skip_special_tokens=True。- Tokenizer 必须与模型匹配:不同模型的词汇表和 ID 映射不同,用错 Tokenizer 解码会得到乱码。
- GPU 张量的隐式设备转移 :如果你把 GPU 上的 Tensor 直接传给
decode(),内部的to_py_obj()会自动先.cpu()再转换。单次调用影响不大,但在循环中大批量解码时,这个隐式的 GPU→CPU 转移可能成为性能瓶颈。如需优化,可考虑先在 CPU 上准备好数据再批量解码。