基于spaCy及Matcher提取作nsubj的NER与对应支配动词的实现咨询
你之前碰到的所有匹配问题,本质是用按文本顺序匹配的普通Token Matcher,去解决需要依赖语法支配关系才能完成的任务。普通Matcher只认token在文本里的前后位置,根本感知不到句子的句法结构,自然适配不了主语在动词后面的场景。spaCy本身自带的依存树结构才是解决这个需求的最优路径,根本不需要靠复杂的顺序匹配规则绕弯。
方案1:不依赖Matcher,直接通过原生依存树提取(准确率最高)
spaCy里每个带有nsubj依存标签的token,都自带.head属性,这个属性直接指向语法上支配它的父节点,也就是我们要找的关联动词。这个逻辑完全不考虑两个元素在文本里的先后顺序,不管主语在动词前、还是在动词后,都能100%基于语法关系匹配,不受中间间隔多少词的影响。
同时spaCy本身集成了句子切分能力,完全可以替代你现在用的NLTK sent_tokenize,避免切分不准导致的跨句错配。
你之前写的普通顺序匹配规则,本质是在找“从一个nsubj实体开始,往后滑到碰到的第一个动词”,根本不检查这个动词是不是语法上真的支配这个实体的节点,所以既适配不了倒序的主语位置,也会把句子里随便碰到的第一个无关动词当成配对结果,这也是为什么会出现"Florent, soulagé"这类和动词完全无关的错配——因为soulagé是形容词,根本没到动词节点就被规则误判为结束了。
对应的可直接运行的实现代码:
import spacy # 加载法语中型模型,自带词性标注、依存分析、命名实体识别、句子切分能力 nlp = spacy.load("fr_core_news_md") def extract_pairs(input_text: str) -> list[dict]: result = [] # 直接遍历模型输出的句子,无跨句问题 doc = nlp(input_text) for sent in doc.sents: for token in sent: # 筛选两个必要条件: # 1. 句法角色是名词性主语nsubj # 2. 这个主语本身是有命名实体标签的实体 if token.dep_ == "nsubj" and token.ent_type_: # 直接通过head属性拿到支配它的动词,不需要做跨度匹配 parent_node = token.head result.append( { "实体主语": token.text, "实体类型": token.ent_type_, "支配动词": parent_node.text, "原句": sent.text } ) return result
用这个方法处理你提到的两个示例:
- Georges and his friends live in Mexico City:遍历到Georges这个token时,它的dep是nsubj,head是动词
live,直接配对成功 - "Hello !", says Mary:遍历到Mary这个token时,它的dep是nsubj,head是动词
says,不管它在句子里的位置在动词后面,都能正确配对
方案2:如果需要用Matcher实现,改用DependencyMatcher适配双向语序
如果必须用Matcher工具完成需求,就替换掉普通的Matcher,改用spaCy自带的DependencyMatcher——这个工具是专门为依存关系匹配设计的,完全基于依存树的节点关系匹配,不受文本顺序影响。
规则调整说明
你拿到的基础参考规则本身已经覆盖了语序无关的匹配,只需要把关系操作符调整得更兼容即可:
REL_OP的作用是定义两个节点的依存支配关系,和文本顺序没有任何关联- 把原来的
<(直接子节点关系)换成>>(任意层级的支配关系),可以兼容法语里的复合时态、助动词带的长依存链路,不会漏配
调整后的可直接用的匹配规则:
from spacy.matcher import DependencyMatcher dep_matcher = DependencyMatcher(nlp.vocab) # 匹配规则的逻辑:找所有「作为nsubj的命名实体」,以及它支配链上的核心动词 pattern = [ # 第一个节点:目标nsubj命名实体 { "RIGHT_ID": "nsubj_entity", "RIGHT_ATTRS": {"DEP": "nsubj", "ENT_TYPE": {"NOT_IN": [""]}} }, # 第二个节点:支配这个nsubj的动词节点,不管文本位置在前还是在后 { "LEFT_ID": "nsubj_entity", "REL_OP": ">>", "RIGHT_ID": "controlling_verb", "RIGHT_ATTRS": {"POS": "VERB"} } ] dep_matcher.add("nsubj_verb_pair", [pattern]) # 调用示例 doc = nlp('"Hello !", says Mary') matches = dep_matcher(doc) # 匹配返回的元组里,第一个位置是实体token的索引,第二个是动词的索引,完全不受顺序影响 for match_id, (ent_idx, verb_idx) in matches: print(f"实体: {doc[ent_idx].text}, 关联动词: {doc[verb_idx].text}")
这个规则可以同时适配两种常见场景:
- 主语在动词前:比如Florent regardait,规则能识别到Florent是regardait的nsubj,正确配对
- 主语在动词后:比如Saget est matinale, dit,规则能识别到Saget是dit的nsubj,不管dit在句子里的位置在Saget后面,都能正确匹配
准确率提升的实用技巧
在不需要追求100%准确的场景下,做几个简单调整就能把准确率从90%拉到95%以上:
- 移除对外部NLTK切句的依赖,统一用spaCy内置的
doc.sents切分,避免不同工具的切分逻辑不一致导致的跨句错配 - 增加一层过滤:只收录head是实义动词的配对,过滤掉支配节点是助动词、形容词的无效匹配,比如之前出现的"Florent accablé"这类错配
- 针对法语语法做适配:法语里的代词式动词(比如s'était arrêté)的依存标签有时候会把代词标成nsubj,只要过滤掉ent_type为空的条目,就不会把这类非实体的主语混进结果
- 做结果去重:同一个句子里如果同一个实体和同一个动词被重复匹配,只保留一次结果,避免输出冗余
- 可以根据自己的语料场景,适当补充命名实体的类型过滤,比如只保留PER(人名)、LOC(地名)类的实体,过滤掉其他类型的无效nsubj
内容的提问来源于stack exchange,提问作者Etienne Armangau

