spacy-fa-pipeline/scripts/check_noun_chunks.py

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()