当前位置:首页 > 文章列表 > 科技周边 > 人工智能 > Transformers tokenizer offset mapping 的原文区间对齐

Transformers tokenizer offset mapping 的原文区间对齐

来源:17golang原创 2026-10-28 09:51:46 0浏览 收藏

offset_mapping 解决的是“这个 token 在原文的哪一段”问题。使用 Hugging Face Transformers 的 Fast tokenizer,并在调用时设置 return_offsets_mapping=True,就能拿到每个 token 对应的半开字符区间 (start, end),再用 text[start:end] 取回原文片段。

官方文档地址:https://huggingface.co/docs/transformers/main_classes/tokenizer。本文重点是对齐边界:特殊 token 通常没有真实原文区间,空白可能被裁剪,批量和句对还需要额外区分序列。

最稳妥的做法是把 offset_mapping 当成原文字符坐标,而不是 token 文本的“猜测位置”:先确认使用 Fast tokenizer,再跳过无效区间,最后按同一条输入和同一组截断结果做映射。

先确认 offset_mapping 能从哪里拿到

Transformers 文档把 tokenizer 分成普通 Python 实现和 Fast 实现。return_offsets_mapping 只适用于继承 PreTrainedTokenizerFast 的 tokenizer;如果使用慢速 Python tokenizer,调用该参数会抛出 NotImplementedError。因此,第一道门禁不是检查返回数组,而是确认 tokenizer 的实现类型。

from transformers import AutoTokenizer

# 使用 Fast tokenizer,后面才能请求原文字符区间
tokenizer = AutoTokenizer.from_pretrained("bert-base-uncased", use_fast=True)
text = "Transformers maps tokens back to text."

# 同时取 token、ID 和每个 token 的原文半开区间
encoding = tokenizer(
    text,
    add_special_tokens=True,
    return_offsets_mapping=True,
    return_special_tokens_mask=True,
)

print(encoding.tokens())
print(encoding["offset_mapping"])
print(encoding["special_tokens_mask"])

这里的 offset_mapping 与 input_ids 按位置对应:第 i 个区间属于第 i 个 token。加入 [CLS]、[SEP] 等特殊 token 后,数组仍然按编码后的位置排列,不能拿未加特殊 token 的 token 列表直接套索引。

原文字符串、Fast tokenizer、token、input_ids 和 offset_mapping 的静态结构关系图
图1:静态结构说明图。原文先进入 Fast tokenizer,再分别得到 token、input_ids 与 offset_mapping;offset_mapping 负责把编码位置指回原文区间,不表示执行顺序。

用半开区间切回原文片段

区间 (start, end) 使用 Python 常见的半开表示法:包含 start,不包含 end。所以还原片段时直接写 text[start:end] 即可。不要用 token 的字符串长度反推原文位置,因为子词拆分、大小写归一化和空白处理都会让两者长度不同。

def token_spans(text: str, encoding) -> list[dict]:
    """返回有真实原文覆盖范围的 token 片段。"""
    tokens = encoding.tokens()
    offsets = encoding["offset_mapping"]
    special_mask = encoding.get("special_tokens_mask")
    rows = []

    for index, (token, pair) in enumerate(zip(tokens, offsets)):
        start, end = pair
        # 特殊 token 常见为 (0, 0),也用 mask 明确排除
        is_special = special_mask is not None and special_mask[index] == 1
        if is_special or start == end:
            continue
        # 半开区间可以直接切回原文,end 不包含在片段内
        rows.append({"token_index": index, "token": token, "span": [start, end], "text": text[start:end]})

    return rows

这个函数的结果适合做高亮、实体投影或调试日志。它没有把空白区间强行变成 token,也没有因为 token 文本看起来像特殊符号就自行猜测位置;判断依据来自编码结果里的特殊 token 标记和区间本身。

特殊 token 和空白为什么要单独处理

特殊 token 是模型输入格式的一部分,不是原文字符。对 BERT 风格输入来说,开头和结尾可能出现 [CLS]、[SEP],它们通常没有可切回原文的字符范围。把它们当作实体候选,会产生“实体从原文开头开始”的假结果。

空白也有类似陷阱。Fast tokenizer 的对齐方法可能对空白位置返回 None,尤其是在空白被裁剪时;而 offset_mapping 中的零长度区间也不应该被当作有效文本。实践中同时看 special_tokens_mask 和 start 更稳妥。

def valid_token_indices(encoding) -> list[int]:
    offsets = encoding["offset_mapping"]
    special_mask = encoding.get("special_tokens_mask", [0] * len(offsets))
    valid = []

    for index, ((start, end), is_special) in enumerate(zip(offsets, special_mask)):
        # 只保留覆盖至少一个原文字符、且不是格式化 token 的位置
        if not is_special and start 

如果应用必须保留空白,例如做精确文本编辑器标注,就不要依赖“所有空白都会有独立 token”这一假设;应以原文坐标为主,另行维护空白或标点的显示策略。

把实体字符区间映射到 token 索引

很多标注数据先给出原文实体区间,例如 [16, 27),之后才需要传给 token 分类模型。可以用 char_to_token 找一个字符所在的 token,也可以遍历 offset_mapping,选出与实体区间相交的所有 token。后者更适合一个实体被拆成多个子词的情况。

def char_span_to_token_span(encoding, entity_start: int, entity_end: int):
    offsets = encoding["offset_mapping"]
    special_mask = encoding.get("special_tokens_mask", [0] * len(offsets))
    matched = []

    for token_index, ((start, end), is_special) in enumerate(zip(offsets, special_mask)):
        # 两个半开区间相交:token 起点在实体终点前,且 token 终点在实体起点后
        overlaps = start 

区间相交规则不会要求实体边界刚好落在某个 token 边界上。例如一个英文词被拆成两个子词时,两个子词的区间都与实体区间相交,就应该一起保留。char_to_token 找不到空白字符对应的 token 时返回 None,这不是异常,而是提示该字符没有可直接承载的 token。

批量编码、字符区间、char_to_token 和实体 token 范围的静态关系图
图2:静态结构说明图。字符实体区间通过 encoding 的对齐字段落到一个或多个 token 索引;特殊 token 与空白不应被误并入实体。

批量、句对和截断要保持同一坐标系

单条文本时可以把 offset_mapping[i] 理解为第 i 个编码 token 的原文坐标;批量输入时,它先多一层样本索引。句对输入还要区分第一序列和第二序列,不能把两段文本的坐标混在一起。

texts = ["A short document.", "Another document."]

# 批量结果的每个字段都以样本为第一层;不把不同样本的区间拼接
batch = tokenizer(
    texts,
    padding=True,
    truncation=True,
    max_length=16,
    return_offsets_mapping=True,
    return_special_tokens_mask=True,
)

for sample_index, sample_offsets in enumerate(batch["offset_mapping"]):
    sample_text = texts[sample_index]
    # 每个样本都回到自己的原文字符串中切片
    for token_index, (start, end) in enumerate(sample_offsets):
        if start 

开启 truncation 后,超过 max_length 的 token 不会出现在当前编码结果里;原文里对应的尾部区间也就不会有 token 可映射。如果开启 return_overflowing_tokens=True 产生溢出窗口,还要结合返回的样本映射字段,把每个窗口归回原始样本,不能把窗口下标直接当成原文样本下标。

句对场景应使用 sequence_ids() 或 char_to_token(..., sequence_index=0/1) 识别属于哪一段。即使两个序列都从字符 0 开始,它们也是不同的原文坐标空间;对第二序列查找字符时,必须明确传入对应的序列索引。

常见误区与一张速查表

现象原因处理方式
请求 offset_mapping 报未实现使用了慢速 Python tokenizer加载 Fast tokenizer,并确认模型支持该实现
第一个区间是 (0, 0)它可能是 [CLS] 等特殊 token看 special_tokens_mask,并跳过零长度区间
空格查不到 token空白可能被裁剪或没有独立 token把原文坐标和 token 坐标分开维护
实体只命中半个词实体跨越多个子词按半开区间相交收集全部 token
批量结果切片错位复用了另一个样本的原文按 batch 样本索引回到对应文本
截断后实体消失实体所在 token 不在当前窗口提高 max_length 或处理 overflow 窗口映射

可以把整个过程记成四个动作:Fast tokenizer 负责产生对齐信息;offset_mapping 提供原文半开坐标;special_tokens_mask 排除格式化 token;实体映射按字符区间相交,而不是按 token 字符串长度猜测。

offset_mapping 是 token 的字节位置吗?

在 Transformers 的 tokenizer 接口语境中,它返回的是原文字符区间 (char_start, char_end)。使用时应按文档和当前 tokenizer 的编码行为处理,不要把它当成 UTF-8 字节偏移量直接交给字节数组。

慢速 tokenizer 能不能自己补 offset_mapping?

不建议把慢速 tokenizer 的 token 文本用长度累加来补齐。归一化、子词边界和空白裁剪都可能让这种推断失真;需要原文对齐时,优先选择对应的 Fast tokenizer。

为什么实体边界落在空格上时没有 token?

空格可能没有独立 token,或在对齐时被裁剪。实体标注应先规范为覆盖实际字符的半开区间,再根据与 token 区间的相交关系决定是否纳入。

参考:Hugging Face Transformers Tokenizer 官方文档,包含 Fast tokenizer 的字符映射能力、return_offsets_mapping 参数和 BatchEncoding.char_to_token 的说明。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
Hugging Face Datasets unique 怎样列出字段的不同取值Hugging Face Datasets unique 怎样列出字段的不同取值
上一篇
Hugging Face Datasets unique 怎样列出字段的不同取值
PyTorch DistributedSampler 每轮 shuffle 的复现配置
下一篇
PyTorch DistributedSampler 每轮 shuffle 的复现配置
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • PubMedQA数据集详解:生物医学问答基准、功能与应用指南
    PubMedQA
    深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
    754次使用
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    752次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    833次使用
  • 斯坦福HELM:大语言模型Holistic Evaluation整体评估框架详解
    HELM
    深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
    798次使用
  • MMBench详解:多模态大模型基准测试、功能特点与使用指南
    MMBench
    MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
    633次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议 和 隐私政策
返回登录
  • 重置密码