138 lines
5.0 KiB
Python
138 lines
5.0 KiB
Python
"""Evidence for the upstream `spacy/lang/fa/syntax_iterators.py` bug.
|
|
|
|
The shipped Persian `noun_chunks` iterator matches these dependency labels:
|
|
|
|
nsubj, dobj, nsubjpass, pcomp, pobj, dative, appos, attr, ROOT
|
|
|
|
`dobj`, `nsubjpass`, `pobj`, `dative` and `attr` are ClearNLP/English labels. They do not
|
|
exist in Universal Dependencies, and every Persian treebank is UD. So on a UD-trained `fa`
|
|
pipeline the iterator only ever fires on `nsubj`, `appos`, `ROOT` and `conj`.
|
|
|
|
It also expands each chunk to `word.left_edge`, which is wrong for Persian: ezafe
|
|
constructions put modifiers to the RIGHT of the head noun (`رئیس انجمن جراحان قلب ایران`),
|
|
so left-only expansion truncates the phrase to the head.
|
|
|
|
This script quantifies both problems against the dev corpus and prints a side-by-side
|
|
comparison with the proposed UD implementation (see docs/upstream/fa-noun-chunks.md).
|
|
|
|
Usage:
|
|
.venv/bin/python scripts/check_noun_chunks.py training/core/model-best \\
|
|
corpus/merged/fa_perdt-ud-dev.spacy
|
|
"""
|
|
|
|
import sys
|
|
from collections import Counter
|
|
|
|
import spacy
|
|
from spacy.lang.fa.syntax_iterators import noun_chunks as shipped_noun_chunks
|
|
from spacy.symbols import NOUN, PRON, PROPN
|
|
from spacy.tokens import DocBin
|
|
|
|
# Labels a UD-trained parser can actually emit, as heads of a base noun phrase.
|
|
UD_LABELS = [
|
|
"nsubj",
|
|
"nsubj:pass",
|
|
"obj",
|
|
"iobj",
|
|
"obl",
|
|
"obl:arg",
|
|
"nmod",
|
|
"appos",
|
|
"vocative",
|
|
"ROOT",
|
|
]
|
|
# Relations that continue a Persian noun phrase to the right of its head.
|
|
UD_POST_MODIFIERS = ["nmod", "nmod:poss", "amod", "flat", "flat:name", "flat:num",
|
|
"fixed", "compound", "det", "nummod"]
|
|
|
|
|
|
def proposed_noun_chunks(doclike):
|
|
"""UD-label implementation with right-side ezafe expansion."""
|
|
doc = doclike.doc
|
|
if not doc.has_annotation("DEP"):
|
|
raise ValueError("requires a dependency parse")
|
|
np_deps = {doc.vocab.strings.add(label) for label in UD_LABELS}
|
|
np_modifs = {doc.vocab.strings.add(label) for label in UD_POST_MODIFIERS}
|
|
np_label = doc.vocab.strings.add("NP")
|
|
conj = doc.vocab.strings.add("conj")
|
|
adp_pos = doc.vocab.strings.add("ADP")
|
|
cconj_pos = doc.vocab.strings.add("CCONJ")
|
|
prev_end = -1
|
|
for word in doclike:
|
|
if word.pos not in (NOUN, PROPN, PRON):
|
|
continue
|
|
if word.left_edge.i <= prev_end:
|
|
continue
|
|
head_dep = word.dep
|
|
if head_dep == conj:
|
|
head = word.head
|
|
while head.dep == conj and head.head.i < head.i:
|
|
head = head.head
|
|
if head.dep not in np_deps:
|
|
continue
|
|
elif head_dep not in np_deps:
|
|
continue
|
|
# Expand right through ezafe / modifier chains.
|
|
right = word
|
|
for child in word.rights:
|
|
if child.dep in np_modifs:
|
|
right = child.right_edge
|
|
else:
|
|
break
|
|
start, end = word.left_edge.i, max(word.i, right.i) + 1
|
|
# Mirror fr/es: a leading preposition or coordinator is not part of the NP.
|
|
while start < word.i and doc[start].pos in (adp_pos, cconj_pos):
|
|
start += 1
|
|
if end <= prev_end:
|
|
continue
|
|
prev_end = end - 1
|
|
yield start, end, np_label
|
|
|
|
|
|
def spans(doc, iterator):
|
|
return [doc[s:e].text for s, e, _ in iterator(doc)]
|
|
|
|
|
|
def main():
|
|
model, corpus = sys.argv[1], sys.argv[2]
|
|
nlp = spacy.load(model)
|
|
docs = list(DocBin().from_disk(corpus).get_docs(nlp.vocab))
|
|
|
|
shipped_total = proposed_total = 0
|
|
shipped_tokens = proposed_tokens = 0
|
|
dep_hist = Counter()
|
|
for doc in docs:
|
|
for t in doc:
|
|
if t.pos in (NOUN, PROPN, PRON):
|
|
dep_hist[t.dep_] += 1
|
|
for s, e, _ in shipped_noun_chunks(doc):
|
|
shipped_total += 1
|
|
shipped_tokens += e - s
|
|
for s, e, _ in proposed_noun_chunks(doc):
|
|
proposed_total += 1
|
|
proposed_tokens += e - s
|
|
|
|
print(f"gold dev corpus: {len(docs)} docs")
|
|
print(f"shipped noun_chunks: {shipped_total:>6} chunks, "
|
|
f"{shipped_tokens / max(shipped_total, 1):.2f} tokens/chunk")
|
|
print(f"proposed noun_chunks: {proposed_total:>6} chunks, "
|
|
f"{proposed_tokens / max(proposed_total, 1):.2f} tokens/chunk")
|
|
print("\ndeprels on NOUN/PROPN/PRON tokens in dev (top 15):")
|
|
for dep, n in dep_hist.most_common(15):
|
|
reachable = "shipped" if dep in {"nsubj", "appos", "conj", "ROOT", "root"} else "-"
|
|
print(f" {dep:<14}{n:>7} {reachable}")
|
|
|
|
print("\nside by side on live text:")
|
|
for text in [
|
|
"رئیس انجمن جراحان قلب ایران تأکید کرد که امکانات پیشرفته در ایران وجود دارد.",
|
|
"دانشگاه تهران بزرگترین دانشگاه ایران است.",
|
|
]:
|
|
doc = nlp(text)
|
|
print(f"\n {text}")
|
|
print(f" shipped : {spans(doc, shipped_noun_chunks)}")
|
|
print(f" proposed: {spans(doc, proposed_noun_chunks)}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|