Spacy Matcher匹配优先级配置 实现最长匹配、去重叠匹配
spaCy物理量模式匹配问题修复方案
问题复现
基于spaCy构建物理模式库,目标是实现时间、速度两类物理量的灵活检测,初始编写的匹配规则与执行代码如下:
import spacy from spacy.matcher import Matcher nlp = spacy.load("en_core_web_sm") time_pattern = [ [ {'LIKE_NUM': True, 'OP': '?'}, {'LOWER':{'IN': ['time', 's','h','min']}}, {'LOWER': {"IN": ['maximum','minimum','min','max']}, 'OP':'?'} ] ] speed_pattern = [ [ {'LIKE_NUM': True, 'OP': '?'}, {'LOWER':{"IN": ['km', 'm']}}, {'IS_PUNCT': True}, {'LOWER' : {"IN": ['h','hour','s','min']}} ] ] matcher=Matcher(nlp.vocab, validate =True) matcher.add("SPEED", speed_pattern) matcher.add("TIME", time_pattern) doc=nlp("a certain time, more about 23 min, can't get above 25 km/h") for id_match, start, end in matcher(doc): match_label=nlp.vocab[id_match].text print(match_label, '<--', doc[start:end])
对测试文本a certain time, more about 23 min, can't get above 25 km/h运行后,得到不符合预期的匹配结果:
- TIME <-- time
- TIME <-- 23 min
- TIME <-- min
- SPEED <-- 25 km/h
- SPEED <-- km/h
- TIME <-- h
核心问题共两点:
- 短匹配未被过滤:同一语义片段同时触发长、短两个匹配时,短匹配被保留,例如
23 min片段同时命中长度为2的完整匹配和长度为1的min短匹配 - 重叠重复匹配:已被长规则命中的片段内token,被其他规则重复识别,例如速度匹配
25 km/h中的h,又被时间规则识别为时间量
预期效果为:同一语义片段仅匹配一次,优先匹配最长符合规则的片段,已被匹配的token不再参与后续匹配。
修复方案
spaCy原生Matcher默认返回所有命中的候选片段,不会自动做最长优先和去重,按以下两步调整即可达到要求:
第一步:优化匹配规则,减少无意义裸单位匹配
原规则将数值前缀设为可选,会大量触发不带数值的裸单位匹配(如单独的min/h/km/h),调整规则逻辑:
- 带单位的物理量匹配要求数值必填,将数值匹配的
OP参数从?(可选)改为+(至少匹配1个) - 泛指的
time概念单独拆为一条规则,避免和单位匹配逻辑冲突 - 规则添加时优先加高优先级规则:速度规则优先级高于时间规则(速度模式本身包含时间单位,优先匹配更长的速度语义)
调整后的规则代码:
time_pattern = [ [ # 带数值的时间量,数值必填 {'LIKE_NUM': True, 'OP': '+'}, {'LOWER':{'IN': ['s','h','min','second','minute','hour']}}, {'LOWER': {"IN": ['maximum','minimum','min','max']}, 'OP':'?'} ], # 单独匹配泛指的time概念 [{'LOWER': 'time'}] ] speed_pattern = [ [ {'LIKE_NUM': True, 'OP': '+'}, {'LOWER':{"IN": ['km', 'm', 'kilometer', 'meter']}}, {'IS_PUNCT': True}, {'LOWER' : {"IN": ['h','hour','s','min','second','minute']}} ] ] matcher=Matcher(nlp.vocab, validate =True) # 先添加高优先级的速度规则 matcher.add("SPEED", speed_pattern) matcher.add("TIME", time_pattern)
第二步:增加最长非重叠过滤逻辑
拿到Matcher返回的所有候选匹配后,加一层过滤逻辑:
- 所有候选按匹配长度从长到短排序,长度相同的保持原有规则优先级
- 维护已占用token的位置集合,遍历排序后的候选:如果当前匹配的所有token都未被占用,则保留该匹配,同时标记对应token为已占用;如果存在任意token已被占用,直接丢弃当前匹配
过滤逻辑代码:
all_matches = [] for match_id, start, end in matcher(doc): label = nlp.vocab[match_id].text # 存储格式:(负匹配长度, 起始位置, 结束位置, 标签),负长度用于升序排序时自动把长匹配排前面 all_matches.append( (-(end-start), start, end, label) ) # 按长度降序排序,同长度匹配保持规则添加时的优先级 all_matches.sort() used_tokens = set() final_matches = [] for _, start, end, label in all_matches: token_range = set(range(start, end)) # 仅当当前匹配和已匹配片段无重叠时保留 if token_range.isdisjoint(used_tokens): final_matches.append( (label, doc[start:end]) ) used_tokens.update(token_range) # 输出最终匹配结果 for label, span in final_matches: print(label, '<--', span)
运行后得到符合预期的输出:
TIME <-- time TIME <-- 23 min SPEED <-- 25 km/h
无短片段重复匹配,无跨规则重叠识别,长匹配优先保留。
内容的提问来源于stack exchange,提问作者Shmou
相关产品推荐
相关产品推荐

