如何在HuggingFace预训练分词器中启用WordPiece前缀适配BERT序列标注
核心场景说明
基于BERT做序列标注任务时,需要识别WordPiece拆分的子词标记,通过求和/平均聚合同一原词的所有子词嵌入,完成子词和原始单词的标签对齐。实际使用中会遇到的共性问题:
- 早期教程里BERT拆分的子词会带可识别的
##前缀,比如New~york会被拆为New、##~、##york;但当前版本的HuggingFace分词器默认输出的解码结果里看不到这类前缀 - 调整WordPiece前缀参数后,调用
decode方法得到的文本依然没有前缀标记,无法直接区分独立词和拆分的子词
常见操作误区
之前尝试的代码看不到前缀,本质是两个操作逻辑不对:
- 混淆了
decode和原始token提取方法的功能注意:
decode方法的设计目标是把token序列还原成人类可读的自然句子,内置了自动剥离子词##前缀、拼接相邻子词的逻辑,不管怎么配置分词器参数,只要调用decode,就会把所有子词标记隐藏掉,这是方法本身的固定行为,不是参数没生效。 - 快速版分词器的初始化方式错误
直接用本地vocab文件路径初始化BertTokenizerFast时,没有加载完整的预训练分词器配置,传入的wordpieces_prefix参数不会真正作用到分词逻辑上,自然得不到预期的输出。
具体实现方法
完全不需要自行训练自定义分词器,按下面的步骤操作就能拿到带前缀标记的子词列表,满足序列标注的对齐需求:
用
convert_ids_to_tokens提取原始带标记的子词
不要依赖decode的输出,拿到input_ids之后,用分词器自带的ID转token方法获取原始子词列表,这个方法会完整保留WordPiece的##前缀:from transformers import BertTokenizer tokenizer = BertTokenizer.from_pretrained('bert-base-cased') batch_sentences = ["hello, i'm testing this efauenufefu"] inputs = tokenizer(batch_sentences, return_tensors="pt") # 输出的token列表会保留所有##前缀的子词标记 token_list = tokenizer.convert_ids_to_tokens(inputs["input_ids"][0]) print(token_list)对应你之前测试的无意义长字符串拆分结果,实际输出的token列表为:
['[CLS]', 'hello', ',', 'i', "'", 'm', 'testing', 'this', 'e', '##fa', '##uen', '##ufe', '##fu', '[SEP]']可以清晰看到拆分出的子词都带
##前缀,满足子词识别需求。正确配置快速版分词器的WordPiece前缀
如果使用BertTokenizerFast,不要直接读本地vocab文件初始化,要从预训练模型加载完整配置后再调整前缀参数:from transformers import BertTokenizerFast # 从预训练源加载完整分词器配置,再传入前缀参数 tokenizer_fast = BertTokenizerFast.from_pretrained( 'bert-base-cased', wordpieces_prefix="##" ) inputs = tokenizer_fast( ["hello, i'm testing this efauenufefu"], return_tensors="pt" ) # 同样用convert_ids_to_tokens拿原始带标记的子词 token_list = tokenizer_fast.convert_ids_to_tokens(inputs["input_ids"][0]) print(token_list)序列标注对齐的更优方案
做子词和原词对齐时,甚至不需要手动识别##前缀:快速版分词器自带get_word_ids()方法,会直接返回每个子词对应的原始单词索引,相同原词拆分出的所有子词会共享同一个word id,直接按id分组聚合嵌入,比手动判断前缀更稳定。# 获取当前批次每个子词对应的原词索引 word_ids = tokenizer_fast.get_word_ids(batch_index=0) print(word_ids) # 同一原词拆分的所有子词会返回相同的id值,可直接用于嵌入聚合和标签对齐
内容的提问来源于stack exchange,提问作者ashered

