直接用字符串方式 split(“。“) 来切句,虽然能把句子拆开,但无法和 BERT 模型的 token 位置对应(embedding 用不上)
❗️直接字符串切割的问题:
- 会丢失原始 token 的起止位置(比如第几个 token)
- 无法和 BERT 模型的 token 位置对应(embedding 用不上)
- 遇到标点、空格、英语等语言混合情况,会切不准
这里 无法和 BERT 模型的 token 位置对应
该如何理解,下面详细解释。
“无法和 BERT 模型的 token 位置对应”
这个意思是:
如果你直接用字符串方式 split("。")
来切句,虽然能把句子拆开,但BERT tokenizer 是按子词(subword)来切的,不是按字或句子切的,所以它的 token 和你切出来的句子之间的位置是对不上的。
📘 举个完整的例子
假设我们有如下文本:
text = "我今天很开心。我去公园玩了。"
这句话可以分成两个句子:
- 句子1: “我今天很开心。”
- 句子2: “我去公园玩了。”
🪓 方法一:直接字符串切割
sentences = text.split("。")
print(sentences)
# 输出:["我今天很开心", "我去公园玩了", ""]
这样切割是按标点符号分割,没有考虑模型如何分词,也没有位置信息。
🤖 方法二:用 BERT tokenizer 看 token
from transformers import BertTokenizertokenizer = BertTokenizer.from_pretrained("bert-base-chinese")inputs = tokenizer(text, return_offsets_mapping=True)token_ids = inputs['input_ids']
offsets = inputs['offset_mapping']tokens = tokenizer.convert_ids_to_tokens(token_ids)for t, o in zip(tokens, offsets):print(f"{t}\t{o}")
输出可能类似于:
[CLS] (0, 0)
我 (0, 1)
今天 (1, 3)
很 (3, 4)
开 (4, 5)
心 (5, 6)
。 (6, 7)
我 (7, 8)
去 (8, 9)
公园 (9, 11)
玩 (11, 12)
了 (12, 13)
。 (13, 14)
[SEP] (0, 0)
这些内容解释如下:
Token | 位置(offset) | 字符 |
---|---|---|
我 | (0, 1) | 第1个字符 |
今天 | (1, 3) | 第2~3字符 |
很 | (3, 4) | 第4个字符 |
开心 | (4, 6) | 第5~6字符(两个 subword) |
。 | (6, 7) | 第7个字符 |
我 | (7, 8) | 第8个字符 |
…… | …… | …… |
❗️核心问题来了
如果你用字符串切割法(如 split("。")
),你拿到的句子:
"我今天很开心"
(长度是 6)"我去公园玩了"
(长度是 6)
你不知道这些句子在 tokenizer token 中的具体 token index 区间。你没法知道:
- 第1句在哪些 token 上?是 token[1] 到 token[6] 吗?
- 第2句是哪几号 token?中间有没有 subword 被切断?
所以你也就无法做下面这些操作:
应用 | 你无法做的事 |
---|---|
拿第1个句子的 BERT embedding | 不知道 token 区间 |
句子粒度的嵌入或检索 | 分不清句子 token 边界 |
还原原文字符位置 | 没有 offset 映射 |
✅ 正确做法:用 tokenizer 的 offset_mapping
inputs = tokenizer(text, return_offsets_mapping=True)
这个 offset_mapping
会告诉你每个 token 是原始字符串的哪一段,就可以:
- 精确知道哪个 token 属于哪个句子
- 按 token 精准切句(比如用 “。” 的 token ID 找句号)
- 保留 token 到原文字符的映射(恢复很方便)
📌 总结一句话
如果你只按字符串分句,你得到的是「字符级别」的句子,没有办法和 BERT 的「token 索引」或「subword 切分」对得上,也就没法拿句子级别的 embedding 或用于模型输入。
BERT 的「token 索引」或「subword 切分」分别是指什么
📌 1. 什么是 BERT 的「token 索引」?
简单说,token 索引就是:
BERT 把你原始的文本转成一串「数字 ID」,每个词(或子词)对应一个编号(index),模型是处理这串数字的。
举个例子:
from transformers import BertTokenizertokenizer = BertTokenizer.from_pretrained("bert-base-chinese")
text = "我去上学。"
tokens = tokenizer.tokenize(text)
ids = tokenizer.convert_tokens_to_ids(tokens)
print(tokens) # ['我', '去', '上', '学', '。']
print(ids) # [2769, 1343, 677, 2110, 511]
上面这段的意思是:
token | ID(token 索引) |
---|---|
我 | 2769 |
去 | 1343 |
上 | 677 |
学 | 2110 |
。 | 511 |
这些 token ID 就是所谓的“token 索引”,它是 BERT 处理的输入。
🧩 2. 什么是「subword 切分」?
这才是重点!
BERT 的 tokenizer(特别是 WordPiece tokenizer)不是按「词」切分,而是:
把词分成更小的「子词」(subword),目的是让它能处理「没见过的词」。
比如英语句子:
text = "unbelievable"
tokens = tokenizer.tokenize(text)
print(tokens) # ['un', '##bel', '##ievable']
意思是:
un
:是前缀##bel
:子词,“##” 表示它是前一个词的一部分##ievable
:也是词尾一部分
在中文里也会出现 subword,比如:
text = "开发者"
tokens = tokenizer.tokenize(text)
print(tokens) # ['开发', '##者']
意思是:
token | 是完整词吗? |
---|---|
开发 | 是 |
##者 | 不是,它是“开发”的后缀 |
这就是「subword 切分」的含义。
🚨 如果你直接用字符串切句,会遇到的问题是:
举个复杂例子:
text = "我是开发者。我热爱AI。"
tokens = tokenizer.tokenize(text)
# 输出示例: ['我', '是', '开发', '##者', '。', '我', '热', '爱', 'AI', '。']
现在假设你用 text.split("。")
得到两句:
- 句1 =
"我是开发者"
(你以为这是 token[0:4]) - 句2 =
"我热爱AI"
(你以为这是 token[5:8])
但实际上:
"开发者"
被拆成两个 token:开发
,##者
"AI"
是一个完整 token,但 tokenizer 可能还拆成A
,##I
(看具体模型)
所以你切完的句子和 token 列表对不上号,你不知道:
- 句子的 token 起止 index 是多少?
- 哪些 token 是子词?
- 如何用 token ID 还原原句?
✅ 正确做法是:
inputs = tokenizer(text, return_offsets_mapping=True)
tokens = tokenizer.convert_ids_to_tokens(inputs['input_ids'])
offsets = inputs['offset_mapping']
你就可以得到:
- 每个 token 对应的原始字符起止位置(offset)
- 哪些 token 是在哪个句子里的(通过 offset 判断)
🔚 总结一句话
概念 | 含义 |
---|---|
token 索引 | 原始文本被 tokenizer 转成的一组 token ID(数字序列) |
subword 切分 | 一个词可能被拆成多个子词,尤其是没见过的新词 |
如果你要做句子级别的 embedding / 检索 / 标注任务,一定要用 offset_mapping 对齐 token 和原文字符位置。