spacy-fa-pipeline/scripts/assemble_core.py

134 lines
4.8 KiB
Python

"""Merge the separately-trained NER component into the UD pipeline and write full metadata.
The two components cannot be trained together: the UD components come from
UD_Persian-PerDT (CC BY-SA 4.0, edited prose) and `ner` comes from ParsTwiNER (MIT,
tweets). `ner` was therefore configured with its own embedded tok2vec (see
configs/fa_ner_sm.cfg) so it can be sourced into another pipeline without a dangling
Tok2VecListener — the same design as en_core_web_sm.
Run it twice: once right after training (no metrics yet), and again after
`spacy benchmark accuracy` has produced metrics/*.json so `meta.json["performance"]`
reflects the assembled pipeline on the held-out test sets.
Usage:
.venv/bin/python scripts/assemble_core.py training/core/model-best training/ner/model-best \\
training/fa_core_news_sm --version 3.8.0 \\
[--ud-metrics metrics/ud-test.json] [--ner-metrics metrics/ner-test.json]
"""
import argparse
import json
from pathlib import Path
import spacy
DESCRIPTION = (
"Persian pipeline optimized for CPU. Components: tok2vec, tagger, morphologizer, "
"trainable_lemmatizer, parser, ner."
)
SOURCES = [
{
"name": "UD_Persian-PerDT (PerUDT v1.0)",
"url": "https://github.com/UniversalDependencies/UD_Persian-PerDT",
"author": (
"Mohammad Sadegh Rasooli, Pegah Safari, Amirsaeid Moloodi, Alireza Nourian"
),
"license": "CC BY-SA 4.0",
},
{
"name": "ParsTwiNER",
"url": "https://github.com/overfit-ir/parstwiner",
"author": "MohammadMahdi Aghajani, AliAkbar Badri, Hamid Beigy et al. (Overfit-IR)",
"license": "MIT",
},
{
"name": "spaCy lang/fa language data (stop words originally from HAZM)",
"url": "https://github.com/explosion/spaCy/tree/master/spacy/lang/fa",
"author": "Explosion and spaCy contributors",
"license": "MIT",
},
]
# CC BY-SA 4.0 on the treebank propagates to anything derived from it.
NOTES = (
"The tagger, morphologizer, trainable_lemmatizer and parser are trained on "
"UD_Persian-PerDT, which is licensed CC BY-SA 4.0; this pipeline is therefore "
"distributed under CC BY-SA 4.0 with attribution to the treebank authors. "
"The ner component is trained on ParsTwiNER (MIT), a Twitter corpus, so entity "
"recognition is weaker on formal/edited prose than on social media text. "
"Multiword tokens in the treebank (pronominal clitics, enclitic copulas) were merged "
"with `spacy convert --merge-subtokens`, so a small number of XPOS tags are composite "
"(e.g. N_IANM_PR_JOPER) and ~1.5% of lemmas contain a space."
)
def load_metrics(path):
if not path:
return {}
p = Path(path)
if not p.exists():
print(f" (no metrics at {p}, skipping)")
return {}
return json.loads(p.read_text(encoding="utf8"))
def main():
ap = argparse.ArgumentParser()
ap.add_argument("core", help="trained UD pipeline (training/core/model-best)")
ap.add_argument("ner", help="trained NER pipeline (training/ner/model-best)")
ap.add_argument("output", help="destination directory for the assembled pipeline")
ap.add_argument("--version", default="3.8.0")
ap.add_argument("--ud-metrics", default=None)
ap.add_argument("--ner-metrics", default=None)
args = ap.parse_args()
nlp = spacy.load(args.core)
ner_nlp = spacy.load(args.ner)
if "ner" in nlp.pipe_names:
nlp.remove_pipe("ner")
nlp.add_pipe("ner", source=ner_nlp)
print(f"pipeline: {nlp.pipe_names}")
print(f"ner labels: {sorted(nlp.get_pipe('ner').labels)}")
ud = load_metrics(args.ud_metrics)
ner = load_metrics(args.ner_metrics)
performance = dict(nlp.meta.get("performance", {}))
for key in ("token_acc", "token_p", "token_r", "token_f", "tag_acc", "pos_acc",
"morph_acc", "lemma_acc", "dep_uas", "dep_las", "sents_p", "sents_r",
"sents_f"):
if key in ud:
performance[key] = ud[key]
for key in ("ents_p", "ents_r", "ents_f"):
if key in ner:
performance[key] = ner[key]
if "ents_per_type" in ner:
performance["ents_per_type"] = ner["ents_per_type"]
if "dep_las_per_type" in ud:
performance["dep_las_per_type"] = ud["dep_las_per_type"]
nlp.meta.update(
{
"lang": "fa",
"name": "core_news_sm",
"version": args.version,
"description": DESCRIPTION,
"author": "",
"email": "",
"url": "",
"license": "CC BY-SA 4.0",
"sources": SOURCES,
"notes": NOTES,
"performance": performance,
}
)
out = Path(args.output)
nlp.to_disk(out)
print(f"wrote {out}")
print(json.dumps(performance, indent=2, ensure_ascii=False)[:1200])
if __name__ == "__main__":
main()