All The Way North-头像
Transformers 5.19.0 源码阅读:AutoTokenizer.decode() 原生批处理与 BPE 空格清理陷阱封面图

Transformers 5.19.0 源码阅读:AutoTokenizer.decode() 原生批处理与 BPE 空格清理陷阱

以下是基于 transformers 5.19.0 源码编写

源代码:

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(整数)还原成人类可读的字符串。

从源码注释可以看到,它的概念等价于:

self.convert_tokens_to_string(self.convert_ids_to_tokens(token_ids))

也就是说,理论上它做了三步:ID → Token字符串 → 拼接成完整文本。但实际上,为了性能,底层(TokenizersBackend._decode)直接调用了 Rust 后端的解码器,一步完成,不走 Python 层的两步转换。

它的实际用处:

  1. 模型生成结果的还原:模型 generate() 返回的是 Token ID 序列,必须用 decode() 转回文本才能看到模型说了什么。
  2. 调试分词结果:编码后再解码,可以验证 Tokenizer 是否正确保留了原文语义。
  3. 后处理管道的一环:在文本生成任务(对话、翻译、摘要)中,它是生成链路最后一环。

二、完整函数签名(基于源代码)

PreTrainedTokenizerBase.decode 的实际定义签名如下:

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)。


三、逐参数详解

  1. 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 转移开销。

  1. 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。

  1. **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),代码有一段特殊处理:

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)

这段话的意思是:

  1. clean_up_tokenization_spaces 这个后处理步骤最初是为 WordPiece 分词器(如 BERT)设计的。
  2. 对于 BPE 分词器(如 Qwen3.5),这个清理操作是破坏性的——它会去掉标点符号前面的合法空格(比如 “Hello , world” 中的逗号前空格)。
  3. 因此,对于 BPE 分词器,默认忽略 clean_up_tokenization_spaces=True,并打印一次警告。
  4. 如果你执意要清理,需要额外设置 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)),所以默认情况下不会做空格清理。

源代码里有一段:

# 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 的最终生效值遵循以下回退链:

  1. decode() 调用时通过 kwargs 传入的值(如果传了 True/False 则直接用)
  2. 如果 kwargs 中没传(即 None),回退到 self.clean_up_tokenization_spaces 实例属性(默认 False)
  3. 如果最终值为 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() 方法中:

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(),这是最快的路径。


七、常用操作与代码示例

  1. 基础解码:还原编码结果
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() 才会被插入。

  1. 解码模型生成的结果(最常见的场景)
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)
  1. 批次解码(decode 自身就支持,不需要 batch_decode)

源码注释明确说了:

“The decode method now handles batched input natively, so you can use decode directly instead of batch_decode.”

# decode 直接支持批次输入
texts = tokenizer.decode([[1, 2, 3], [4, 5, 6]], skip_special_tokens=True)
# 返回: ["文本1", "文本2"]
  1. 从 model.generate 的完整输出中解码
# 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)
  1. 解码单个 Token ID
# 传入单个整数
token_str = tokenizer.decode(151643)
print(token_str)  # 输出该 ID 对应的 token 字符串
  1. 处理 dict 输入(某些 collator 输出格式)

TokenizersBackend._decode 中有这段代码:

if isinstance(token_ids, dict):
    token_ids = token_ids["input_ids"]

所以如果你传入的是一个包含 "input_ids" 键的字典,它会自动提取。但这不是推荐用法,建议始终传入 ID 序列。


八、补充:decode() vs batch_decode() 的关系

从源码可以看到,batch_decode 的实现非常简单:

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 路径。


十、注意事项与常见坑

  1. decode(encode(text)) 不一定等于 text:由于 Tokenizer 的规范化(Normalization)规则,解码后的文本可能在大小写、空格、标点等方面与原文有细微差异。只要语义一致即可。
  2. 不要传入二维张量给单条解码:虽然 decode() 现在支持批次输入,但如果你明确是单条序列,传一维列表即可。传二维会被当作批次处理,返回的是列表而不是字符串。
  3. BPE 分词器的 clean_up_tokenization_spaces 默认无效:Qwen3.5 使用 BPE 分词,所以即使你传 clean_up_tokenization_spaces=True,默认也会被忽略并打印警告。这是设计如此,不是 bug。
  4. skip_special_tokens 的默认值是 False:这意味着默认解码结果会包含特殊 token 字符串。日常使用几乎总是需要传 skip_special_tokens=True。
  5. Tokenizer 必须与模型匹配:不同模型的词汇表和 ID 映射不同,用错 Tokenizer 解码会得到乱码。
  6. GPU 张量的隐式设备转移:如果你把 GPU 上的 Tensor 直接传给 decode(),内部的 to_py_obj() 会自动先 .cpu() 再转换。单次调用影响不大,但在循环中大批量解码时,这个隐式的 GPU→CPU 转移可能成为性能瓶颈。如需优化,可考虑先在 CPU 上准备好数据再批量解码。

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/qq_62634342/article/details/169067777

文章来源转载

评论

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--