From ac24adec73743aff546d08f9d8b91d9970740731 Mon Sep 17 00:00:00 2001 From: Ines Montani Date: Tue, 28 Jul 2020 21:39:42 +0200 Subject: [PATCH 01/16] Small adjustments to Scorer and docs --- spacy/scorer.py | 224 +++++++++++++++++++++---------------- website/docs/api/scorer.md | 175 ++++++++++++++++++++--------- 2 files changed, 251 insertions(+), 148 deletions(-) diff --git a/spacy/scorer.py b/spacy/scorer.py index 2bbf453e7..bb283547e 100644 --- a/spacy/scorer.py +++ b/spacy/scorer.py @@ -1,47 +1,53 @@ +from typing import Optional, Iterable, Dict, Any, Callable, Tuple, TYPE_CHECKING import numpy as np +from .gold import Example +from .tokens import Token from .errors import Errors from .util import get_lang_class from .morphology import Morphology +if TYPE_CHECKING: + # This lets us add type hints for mypy etc. without causing circular imports + from .language import Language # noqa: F401 + + +DEFAULT_PIPELINE = ["senter", "tagger", "morphologizer", "parser", "ner", "textcat"] + class PRFScore: - """ - A precision / recall / F score - """ + """A precision / recall / F score.""" - def __init__(self): + def __init__(self) -> None: self.tp = 0 self.fp = 0 self.fn = 0 - def score_set(self, cand, gold): + def score_set(self, cand: set, gold: set) -> None: self.tp += len(cand.intersection(gold)) self.fp += len(cand - gold) self.fn += len(gold - cand) @property - def precision(self): + def precision(self) -> float: return self.tp / (self.tp + self.fp + 1e-100) @property - def recall(self): + def recall(self) -> float: return self.tp / (self.tp + self.fn + 1e-100) @property - def fscore(self): + def fscore(self) -> float: p = self.precision r = self.recall return 2 * ((p * r) / (p + r + 1e-100)) - def to_dict(self): + def to_dict(self) -> Dict[str, float]: return {"p": self.precision, "r": self.recall, "f": self.fscore} class ROCAUCScore: - """ - An AUC ROC score. - """ + """An AUC ROC score.""" def __init__(self): self.golds = [] @@ -49,7 +55,7 @@ class ROCAUCScore: self.saved_score = 0.0 self.saved_score_at_len = 0 - def score_set(self, cand, gold): + def score_set(self, cand, gold) -> None: self.cands.append(cand) self.golds.append(gold) @@ -70,7 +76,13 @@ class ROCAUCScore: class Scorer: """Compute evaluation scores.""" - def __init__(self, nlp=None, **cfg): + def __init__( + self, + nlp: Optional["Language"] = None, + default_lang: str = "xx", + default_pipeline=DEFAULT_PIPELINE, + **cfg, + ) -> None: """Initialize the Scorer. RETURNS (Scorer): The newly created object. @@ -78,44 +90,39 @@ class Scorer: """ self.nlp = nlp self.cfg = cfg - if not nlp: - # create a default pipeline - nlp = get_lang_class("xx")() - nlp.add_pipe("senter") - nlp.add_pipe("tagger") - nlp.add_pipe("morphologizer") - nlp.add_pipe("parser") - nlp.add_pipe("ner") - nlp.add_pipe("textcat") + nlp = get_lang_class(default_lang)() + for pipe in default_pipeline: + nlp.add_pipe(pipe) self.nlp = nlp - def score(self, examples): + def score(self, examples: Iterable[Example]) -> Dict[str, Any]: """Evaluate a list of Examples. examples (Iterable[Example]): The predicted annotations + correct annotations. RETURNS (Dict): A dictionary of scores. + DOCS: https://spacy.io/api/scorer#score """ scores = {} - if hasattr(self.nlp.tokenizer, "score"): scores.update(self.nlp.tokenizer.score(examples, **self.cfg)) for name, component in self.nlp.pipeline: if hasattr(component, "score"): scores.update(component.score(examples, **self.cfg)) - return scores @staticmethod - def score_tokenization(examples, **cfg): + def score_tokenization(examples: Iterable[Example], **cfg) -> Dict[str, float]: """Returns accuracy and PRF scores for tokenization. - * token_acc: # correct tokens / # gold tokens * token_p/r/f: PRF for token character spans examples (Iterable[Example]): Examples to score - RETURNS (dict): A dictionary containing the scores token_acc/p/r/f. + RETURNS (Dict[str, float]): A dictionary containing the scores + token_acc/p/r/f. + + DOCS: https://spacy.io/api/scorer#score_tokenization """ acc_score = PRFScore() prf_score = PRFScore() @@ -146,16 +153,24 @@ class Scorer: } @staticmethod - def score_token_attr(examples, attr, getter=getattr, **cfg): + def score_token_attr( + examples: Iterable[Example], + attr: str, + *, + getter: Callable[[Token, str], Any] = getattr, + **cfg, + ) -> Dict[str, float]: """Returns an accuracy score for a token-level attribute. examples (Iterable[Example]): Examples to score attr (str): The attribute to score. - getter (callable): Defaults to getattr. If provided, + getter (Callable[[Token, str], Any]): Defaults to getattr. If provided, getter(token, attr) should return the value of the attribute for an individual token. - RETURNS (dict): A dictionary containing the accuracy score under the - key attr_acc. + RETURNS (Dict[str, float]): A dictionary containing the accuracy score + under the key attr_acc. + + DOCS: https://spacy.io/api/scorer#score_token_attr """ tag_score = PRFScore() for example in examples: @@ -173,17 +188,21 @@ class Scorer: gold_i = align.x2y[token.i].dataXd[0, 0] pred_tags.add((gold_i, getter(token, attr))) tag_score.score_set(pred_tags, gold_tags) - return { - attr + "_acc": tag_score.fscore, - } + return {f"{attr}_acc": tag_score.fscore} @staticmethod - def score_token_attr_per_feat(examples, attr, getter=getattr, **cfg): + def score_token_attr_per_feat( + examples: Iterable[Example], + attr: str, + *, + getter: Callable[[Token, str], Any] = getattr, + **cfg, + ): """Return PRF scores per feat for a token attribute in UFEATS format. examples (Iterable[Example]): Examples to score attr (str): The attribute to score. - getter (callable): Defaults to getattr. If provided, + getter (Callable[[Token, str], Any]): Defaults to getattr. If provided, getter(token, attr) should return the value of the attribute for an individual token. RETURNS (dict): A dictionary containing the per-feat PRF scores unders @@ -224,20 +243,26 @@ class Scorer: per_feat[field].score_set( pred_per_feat.get(field, set()), gold_per_feat.get(field, set()), ) - return { - attr + "_per_feat": per_feat, - } + return {f"{attr}_per_feat": per_feat} @staticmethod - def score_spans(examples, attr, getter=getattr, **cfg): + def score_spans( + examples: Iterable[Example], + attr: str, + *, + getter: Callable[[Token, str], Any] = getattr, + **cfg, + ) -> Dict[str, Any]: """Returns PRF scores for labeled spans. examples (Iterable[Example]): Examples to score attr (str): The attribute to score. - getter (callable): Defaults to getattr. If provided, + getter (Callable[[Token, str], Any]): Defaults to getattr. If provided, getter(doc, attr) should return the spans for the individual doc. - RETURNS (dict): A dictionary containing the PRF scores under the - keys attr_p/r/f and the per-type PRF scores under attr_per_type. + RETURNS (Dict[str, Any]): A dictionary containing the PRF scores under + the keys attr_p/r/f and the per-type PRF scores under attr_per_type. + + DOCS: https://spacy.io/api/scorer#score_spans """ score = PRFScore() score_per_type = dict() @@ -257,14 +282,12 @@ class Scorer: # Find all predidate labels, for all and per type gold_spans = set() pred_spans = set() - # Special case for ents: # If we have missing values in the gold, we can't easily tell # whether our NER predictions are true. # It seems bad but it's what we've always done. if attr == "ents" and not all(token.ent_iob != 0 for token in gold_doc): continue - for span in getter(gold_doc, attr): gold_span = (span.label_, span.start, span.end - 1) gold_spans.add(gold_span) @@ -280,38 +303,39 @@ class Scorer: # Score for all labels score.score_set(pred_spans, gold_spans) results = { - attr + "_p": score.precision, - attr + "_r": score.recall, - attr + "_f": score.fscore, - attr + "_per_type": {k: v.to_dict() for k, v in score_per_type.items()}, + f"{attr}_p": score.precision, + f"{attr}_r": score.recall, + f"{attr}_f": score.fscore, + f"{attr}_per_type": {k: v.to_dict() for k, v in score_per_type.items()}, } return results @staticmethod def score_cats( - examples, - attr, - getter=getattr, - labels=[], - multi_label=True, - positive_label=None, - **cfg - ): + examples: Iterable[Example], + attr: str, + *, + getter: Callable[[Token, str], Any] = getattr, + labels: Iterable[str] = tuple(), + multi_label: bool = True, + positive_label: Optional[str] = None, + **cfg, + ) -> Dict[str, Any]: """Returns PRF and ROC AUC scores for a doc-level attribute with a dict with scores for each label like Doc.cats. The reported overall score depends on the scorer settings. examples (Iterable[Example]): Examples to score attr (str): The attribute to score. - getter (callable): Defaults to getattr. If provided, + getter (Callable[[Token, str], Any]): Defaults to getattr. If provided, getter(doc, attr) should return the values for the individual doc. labels (Iterable[str]): The set of possible labels. Defaults to []. multi_label (bool): Whether the attribute allows multiple labels. Defaults to True. positive_label (str): The positive label for a binary task with exclusive classes. Defaults to None. - RETURNS (dict): A dictionary containing the scores, with inapplicable - scores as None: + RETURNS (Dict[str, Any]): A dictionary containing the scores, with + inapplicable scores as None: for all: attr_score (one of attr_f / attr_macro_f / attr_macro_auc), attr_score_desc (text description of the overall score), @@ -320,6 +344,8 @@ class Scorer: for binary exclusive with positive label: attr_p/r/f for 3+ exclusive classes, macro-averaged fscore: attr_macro_f for multilabel, macro-averaged AUC: attr_macro_auc + + DOCS: https://spacy.io/api/scorer#score_cats """ score = PRFScore() f_per_type = dict() @@ -368,64 +394,67 @@ class Scorer: ) ) results = { - attr + "_score": None, - attr + "_score_desc": None, - attr + "_p": None, - attr + "_r": None, - attr + "_f": None, - attr + "_macro_f": None, - attr + "_macro_auc": None, - attr + "_f_per_type": {k: v.to_dict() for k, v in f_per_type.items()}, - attr + "_auc_per_type": {k: v.score for k, v in auc_per_type.items()}, + f"{attr}_score": None, + f"{attr}_score_desc": None, + f"{attr}_p": None, + f"{attr}_r": None, + f"{attr}_f": None, + f"{attr}_macro_f": None, + f"{attr}_macro_auc": None, + f"{attr}_f_per_type": {k: v.to_dict() for k, v in f_per_type.items()}, + f"{attr}_auc_per_type": {k: v.score for k, v in auc_per_type.items()}, } if len(labels) == 2 and not multi_label and positive_label: - results[attr + "_p"] = score.precision - results[attr + "_r"] = score.recall - results[attr + "_f"] = score.fscore - results[attr + "_score"] = results[attr + "_f"] - results[attr + "_score_desc"] = "F (" + positive_label + ")" + results[f"{attr}_p"] = score.precision + results[f"{attr}_r"] = score.recall + results[f"{attr}_f"] = score.fscore + results[f"{attr}_score"] = results[f"{attr}_f"] + results[f"{attr}_score_desc"] = f"F ({positive_label})" elif not multi_label: - results[attr + "_macro_f"] = sum( + results[f"{attr}_macro_f"] = sum( [score.fscore for label, score in f_per_type.items()] ) / (len(f_per_type) + 1e-100) - results[attr + "_score"] = results[attr + "_macro_f"] - results[attr + "_score_desc"] = "macro F" + results[f"{attr}_score"] = results[f"{attr}_macro_f"] + results[f"{attr}_score_desc"] = "macro F" else: - results[attr + "_macro_auc"] = max( + results[f"{attr}_macro_auc"] = max( sum([score.score for label, score in auc_per_type.items()]) / (len(auc_per_type) + 1e-100), -1, ) - results[attr + "_score"] = results[attr + "_macro_auc"] - results[attr + "_score_desc"] = "macro AUC" + results[f"{attr}_score"] = results[f"{attr}_macro_auc"] + results[f"{attr}_score_desc"] = "macro AUC" return results @staticmethod def score_deps( - examples, - attr, - getter=getattr, - head_attr="head", - head_getter=getattr, - ignore_labels=tuple(), - **cfg - ): + examples: Iterable[Example], + attr: str, + *, + getter: Callable[[Token, str], Any] = getattr, + head_attr: str = "head", + head_getter: Callable[[Token, str], Any] = getattr, + ignore_labels: Tuple[str] = tuple(), + **cfg, + ) -> Dict[str, Any]: """Returns the UAS, LAS, and LAS per type scores for dependency parses. examples (Iterable[Example]): Examples to score attr (str): The attribute containing the dependency label. - getter (callable): Defaults to getattr. If provided, + getter (Callable[[Token, str], Any]): Defaults to getattr. If provided, getter(token, attr) should return the value of the attribute for an individual token. head_attr (str): The attribute containing the head token. Defaults to 'head'. - head_getter (callable): Defaults to getattr. If provided, + head_getter (Callable[[Token, str], Any]): Defaults to getattr. If provided, head_getter(token, attr) should return the value of the head for an individual token. ignore_labels (Tuple): Labels to ignore while scoring (e.g., punct). - RETURNS (dict): A dictionary containing the scores: + RETURNS (Dict[str, Any]): A dictionary containing the scores: attr_uas, attr_las, and attr_las_per_type. + + DOCS: https://spacy.io/api/scorer#score_deps """ unlabelled = PRFScore() labelled = PRFScore() @@ -483,10 +512,11 @@ class Scorer: set(item[:2] for item in pred_deps), set(item[:2] for item in gold_deps) ) return { - attr + "_uas": unlabelled.fscore, - attr + "_las": labelled.fscore, - attr - + "_las_per_type": {k: v.to_dict() for k, v in labelled_per_dep.items()}, + f"{attr}_uas": unlabelled.fscore, + f"{attr}_las": labelled.fscore, + f"{attr}_las_per_type": { + k: v.to_dict() for k, v in labelled_per_dep.items() + }, } diff --git a/website/docs/api/scorer.md b/website/docs/api/scorer.md index 8daefd241..987cc308e 100644 --- a/website/docs/api/scorer.md +++ b/website/docs/api/scorer.md @@ -6,10 +6,9 @@ source: spacy/scorer.py --- The `Scorer` computes evaluation scores. It's typically created by -[`Language.evaluate`](/api/language#evaluate). - -In addition, the `Scorer` provides a number of evaluation methods for evaluating -`Token` and `Doc` attributes. +[`Language.evaluate`](/api/language#evaluate). In addition, the `Scorer` +provides a number of evaluation methods for evaluating [`Token`](/api/token) and +[`Doc`](/api/doc) attributes. ## Scorer.\_\_init\_\_ {#init tag="method"} @@ -20,10 +19,10 @@ Create a new `Scorer`. > ```python > from spacy.scorer import Scorer > -> # default scoring pipeline +> # Default scoring pipeline > scorer = Scorer() > -> # provided scoring pipeline +> # Provided scoring pipeline > nlp = spacy.load("en_core_web_sm") > scorer = Scorer(nlp) > ``` @@ -41,16 +40,20 @@ scoring methods provided by the components in the pipeline. The returned `Dict` contains the scores provided by the individual pipeline components. For the scoring methods provided by the `Scorer` and use by the core pipeline components, the individual score names start with the `Token` or `Doc` -attribute being scored: `token_acc`, `token_p/r/f`, `sents_p/r/f`, `tag_acc`, -`pos_acc`, `morph_acc`, `morph_per_feat`, `lemma_acc`, `dep_uas`, `dep_las`, -`dep_las_per_type`, `ents_p/r/f`, `ents_per_type`, `textcat_macro_auc`, -`textcat_macro_f`. +attribute being scored: + +- `token_acc`, `token_p`, `token_r`, `token_f`, +- `sents_p`, `sents_r`, `sents_f` +- `tag_acc`, `pos_acc`, `morph_acc`, `morph_per_feat`, `lemma_acc` +- `dep_uas`, `dep_las`, `dep_las_per_type` +- `ents_p`, `ents_r` `ents_f`, `ents_per_type` +- `textcat_macro_auc`, `textcat_macro_f` > #### Example > > ```python > scorer = Scorer() -> scorer.score(examples) +> scores = scorer.score(examples) > ``` | Name | Type | Description | @@ -58,78 +61,148 @@ attribute being scored: `token_acc`, `token_p/r/f`, `sents_p/r/f`, `tag_acc`, | `examples` | `Iterable[Example]` | The `Example` objects holding both the predictions and the correct gold-standard annotations. | | **RETURNS** | `Dict` | A dictionary of scores. | -## Scorer.score_tokenization {#score_tokenization tag="staticmethod"} +## Scorer.score_tokenization {#score_tokenization tag="staticmethod" new="3"} Scores the tokenization: -- `token_acc`: # correct tokens / # gold tokens -- `token_p/r/f`: PRF for token character spans +- `token_acc`: number of correct tokens / number of gold tokens +- `token_p`, `token_r`, `token_f`: precision, recall and F-score for token + character spans + +> #### Example +> +> ```python +> scores = Scorer.score_tokenization(examples) +> ``` | Name | Type | Description | | ----------- | ------------------- | --------------------------------------------------------------------------------------------- | | `examples` | `Iterable[Example]` | The `Example` objects holding both the predictions and the correct gold-standard annotations. | -| **RETURNS** | `Dict` | A dictionary containing the scores `token_acc/p/r/f`. | +| **RETURNS** | `Dict` | A dictionary containing the scores `token_acc`, `token_p`, `token_r`, `token_f`. | -## Scorer.score_token_attr {#score_token_attr tag="staticmethod"} +## Scorer.score_token_attr {#score_token_attr tag="staticmethod" new="3"} Scores a single token attribute. -| Name | Type | Description | -| ----------- | ------------------- | ----------------------------------------------------------------------------------------------------------------------------- | -| `examples` | `Iterable[Example]` | The `Example` objects holding both the predictions and the correct gold-standard annotations. | -| `attr` | `str` | The attribute to score. | -| `getter` | `callable` | Defaults to `getattr`. If provided, `getter(token, attr)` should return the value of the attribute for an individual `Token`. | -| **RETURNS** | `Dict` | A dictionary containing the score `attr_acc`. | +> #### Example +> +> ```python +> scores = Scorer.score_token_attr(examples, "pos") +> print(scores["pos_acc"]) +> ``` -## Scorer.score_token_attr_per_feat {#score_token_attr_per_feat tag="staticmethod"} +| Name | Type | Description | +| -------------- | ------------------- | ----------------------------------------------------------------------------------------------------------------------------- | +| `examples` | `Iterable[Example]` | The `Example` objects holding both the predictions and the correct gold-standard annotations. | +| `attr` | `str` | The attribute to score. | +| _keyword-only_ | | | +| `getter` | `Callable` | Defaults to `getattr`. If provided, `getter(token, attr)` should return the value of the attribute for an individual `Token`. | +| **RETURNS** | `Dict[str, float]` | A dictionary containing the score `{attr}_acc`. | -Scores a single token attribute per feature for a token attribute in UFEATS +## Scorer.score_token_attr_per_feat {#score_token_attr_per_feat tag="staticmethod" new="3"} + +Scores a single token attribute per feature for a token attribute in +[UFEATS](https://universaldependencies.org/format.html#morphological-annotation) format. -| Name | Type | Description | -| ----------- | ------------------- | ----------------------------------------------------------------------------------------------------------------------------- | -| `examples` | `Iterable[Example]` | The `Example` objects holding both the predictions and the correct gold-standard annotations. | -| `attr` | `str` | The attribute to score. | -| `getter` | `callable` | Defaults to `getattr`. If provided, `getter(token, attr)` should return the value of the attribute for an individual `Token`. | -| **RETURNS** | `Dict` | A dictionary containing the per-feature PRF scores unders the key `attr_per_feat`. | +> #### Example +> +> ```python +> scores = Scorer.score_token_attr_per_feat(examples, "morph") +> print(scores["morph_per_feat"]) +> ``` -## Scorer.score_spans {#score_spans tag="staticmethod"} +| Name | Type | Description | +| -------------- | ------------------- | ----------------------------------------------------------------------------------------------------------------------------- | +| `examples` | `Iterable[Example]` | The `Example` objects holding both the predictions and the correct gold-standard annotations. | +| `attr` | `str` | The attribute to score. | +| _keyword-only_ | | | +| `getter` | `Callable` | Defaults to `getattr`. If provided, `getter(token, attr)` should return the value of the attribute for an individual `Token`. | +| **RETURNS** | `Dict` | A dictionary containing the per-feature PRF scores under the key `{attr}_per_feat`. | + +## Scorer.score_spans {#score_spans tag="staticmethod" new="3"} Returns PRF scores for labeled or unlabeled spans. -| Name | Type | Description | -| ----------- | ------------------- | --------------------------------------------------------------------------------------------------------------------- | -| `examples` | `Iterable[Example]` | The `Example` objects holding both the predictions and the correct gold-standard annotations. | -| `attr` | `str` | The attribute to score. | -| `getter` | `callable` | Defaults to `getattr`. If provided, `getter(doc, attr)` should return the `Span` objects for an individual `Doc`. | -| **RETURNS** | `Dict` | A dictionary containing the PRF scores under the keys `attr_p/r/f` and the per-type PRF scores under `attr_per_type`. | +> #### Example +> +> ```python +> scores = Scorer.score_spans(examples, "ents") +> print(scores["ents_f"]) +> ``` -## Scorer.score_deps {#score_deps tag="staticmethod"} +| Name | Type | Description | +| -------------- | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | +| `examples` | `Iterable[Example]` | The `Example` objects holding both the predictions and the correct gold-standard annotations. | +| `attr` | `str` | The attribute to score. | +| _keyword-only_ | | | +| `getter` | `Callable` | Defaults to `getattr`. If provided, `getter(doc, attr)` should return the `Span` objects for an individual `Doc`. | +| **RETURNS** | `Dict` | A dictionary containing the PRF scores under the keys `{attr}_p`, `{attr}_r`, `{attr}_f` and the per-type PRF scores under `{attr}_per_type`. | + +## Scorer.score_deps {#score_deps tag="staticmethod" new="3"} Calculate the UAS, LAS, and LAS per type scores for dependency parses. +> #### Example +> +> ```python +> def dep_getter(token, attr): +> dep = getattr(token, attr) +> dep = token.vocab.strings.as_string(dep).lower() +> return dep +> +> scores = Scorer.score_deps( +> examples, +> "dep", +> getter=dep_getter, +> ignore_labels=("p", "punct") +> ) +> print(scores["dep_uas"], scores["dep_las"]) +> ``` + | Name | Type | Description | | --------------- | ------------------- | ----------------------------------------------------------------------------------------------------------------------------- | | `examples` | `Iterable[Example]` | The `Example` objects holding both the predictions and the correct gold-standard annotations. | | `attr` | `str` | The attribute containing the dependency label. | -| `getter` | `callable` | Defaults to `getattr`. If provided, `getter(token, attr)` should return the value of the attribute for an individual `Token`. | +| _keyword-only_ | | | +| `getter` | `Callable` | Defaults to `getattr`. If provided, `getter(token, attr)` should return the value of the attribute for an individual `Token`. | | `head_attr` | `str` | The attribute containing the head token. | | `head_getter` | `callable` | Defaults to `getattr`. If provided, `head_getter(token, attr)` should return the head for an individual `Token`. | | `ignore_labels` | `Tuple` | Labels to ignore while scoring (e.g., `punct`). | -| **RETURNS** | `Dict` | A dictionary containing the scores: `attr_uas`, `attr_las`, and `attr_las_per_type`. | +| **RETURNS** | `Dict` | A dictionary containing the scores: `{attr}_uas`, `{attr}_las`, and `{attr}_las_per_type`. | -## Scorer.score_cats {#score_cats tag="staticmethod"} +## Scorer.score_cats {#score_cats tag="staticmethod" new="3"} Calculate PRF and ROC AUC scores for a doc-level attribute that is a dict containing scores for each label like `Doc.cats`. The reported overall score -depends on the scorer settings. +depends on the scorer settings: -| Name | Type | Description | -| ---------------- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `examples` | `Iterable[Example]` | The `Example` objects holding both the predictions and the correct gold-standard annotations. | -| `attr` | `str` | The attribute to score. | -| `getter` | `callable` | Defaults to `getattr`. If provided, `getter(doc, attr)` should return the cats for an individual `Doc`. | -| labels | `Iterable[str]` | The set of possible labels. Defaults to `[]`. | -| `multi_label` | `bool` | Whether the attribute allows multiple labels. Defaults to `True`. | -| `positive_label` | `str` | The positive label for a binary task with exclusive classes. Defaults to `None`. | -| **RETURNS** | `Dict` | A dictionary containing the scores, with inapplicable scores as `None`: 1) for all: `attr_score` (one of `attr_f` / `attr_macro_f` / `attr_macro_auc`), `attr_score_desc` (text description of the overall score), `attr_f_per_type`, `attr_auc_per_type`; 2) for binary exclusive with positive label: `attr_p/r/f`; 3) for 3+ exclusive classes, macro-averaged fscore: `attr_macro_f`; 4) for multilabel, macro-averaged AUC: `attr_macro_auc` | +1. **all:** `{attr}_score` (one of `{attr}_f` / `{attr}_macro_f` / + `{attr}_macro_auc`), `{attr}_score_desc` (text description of the overall + score), `{attr}_f_per_type`, `{attr}_auc_per_type` +2. **binary exclusive with positive label:** `{attr}_p`, `{attr}_r`, `{attr}_f` +3. **3+ exclusive classes**, macro-averaged F-score: `{attr}_macro_f`; +4. **multilabel**, macro-averaged AUC: `{attr}_macro_auc` + +> #### Example +> +> ```python +> labels = ["LABEL_A", "LABEL_B", "LABEL_C"] +> scores = Scorer.score_cats( +> examples, +> "cats", +> labels=labels +> ) +> print(scores["cats_macro_auc"]) +> ``` + +| Name | Type | Description | +| ---------------- | ------------------- | ------------------------------------------------------------------------------------------------------- | +| `examples` | `Iterable[Example]` | The `Example` objects holding both the predictions and the correct gold-standard annotations. | +| `attr` | `str` | The attribute to score. | +| _keyword-only_ | | | +| `getter` | `Callable` | Defaults to `getattr`. If provided, `getter(doc, attr)` should return the cats for an individual `Doc`. | +| labels | `Iterable[str]` | The set of possible labels. Defaults to `[]`. | +| `multi_label` | `bool` | Whether the attribute allows multiple labels. Defaults to `True`. | +| `positive_label` | `str` | The positive label for a binary task with exclusive classes. Defaults to `None`. | +| **RETURNS** | `Dict` | A dictionary containing the scores, with inapplicable scores as `None`. | From c689ae8f0a81041ed3c2eb9cae9926b7842715bc Mon Sep 17 00:00:00 2001 From: Adriane Boyd Date: Wed, 29 Jul 2020 10:39:33 +0200 Subject: [PATCH 02/16] Fix types in Scorer --- spacy/scorer.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/spacy/scorer.py b/spacy/scorer.py index bb283547e..24009aec6 100644 --- a/spacy/scorer.py +++ b/spacy/scorer.py @@ -2,7 +2,7 @@ from typing import Optional, Iterable, Dict, Any, Callable, Tuple, TYPE_CHECKING import numpy as np from .gold import Example -from .tokens import Token +from .tokens import Token, Doc from .errors import Errors from .util import get_lang_class from .morphology import Morphology @@ -49,7 +49,7 @@ class PRFScore: class ROCAUCScore: """An AUC ROC score.""" - def __init__(self): + def __init__(self) -> None: self.golds = [] self.cands = [] self.saved_score = 0.0 @@ -250,14 +250,14 @@ class Scorer: examples: Iterable[Example], attr: str, *, - getter: Callable[[Token, str], Any] = getattr, + getter: Callable[[Doc, str], Any] = getattr, **cfg, ) -> Dict[str, Any]: """Returns PRF scores for labeled spans. examples (Iterable[Example]): Examples to score attr (str): The attribute to score. - getter (Callable[[Token, str], Any]): Defaults to getattr. If provided, + getter (Callable[[Doc, str], Any]): Defaults to getattr. If provided, getter(doc, attr) should return the spans for the individual doc. RETURNS (Dict[str, Any]): A dictionary containing the PRF scores under the keys attr_p/r/f and the per-type PRF scores under attr_per_type. @@ -315,7 +315,7 @@ class Scorer: examples: Iterable[Example], attr: str, *, - getter: Callable[[Token, str], Any] = getattr, + getter: Callable[[Doc, str], Any] = getattr, labels: Iterable[str] = tuple(), multi_label: bool = True, positive_label: Optional[str] = None, @@ -327,7 +327,7 @@ class Scorer: examples (Iterable[Example]): Examples to score attr (str): The attribute to score. - getter (Callable[[Token, str], Any]): Defaults to getattr. If provided, + getter (Callable[[Doc, str], Any]): Defaults to getattr. If provided, getter(doc, attr) should return the values for the individual doc. labels (Iterable[str]): The set of possible labels. Defaults to []. multi_label (bool): Whether the attribute allows multiple labels. From 7a6ac47dc16232b50b5774e6722a98ee05ddaabd Mon Sep 17 00:00:00 2001 From: Adriane Boyd Date: Wed, 29 Jul 2020 10:39:39 +0200 Subject: [PATCH 03/16] Remove keyword-only from Scorer API docs --- website/docs/api/scorer.md | 5 ----- 1 file changed, 5 deletions(-) diff --git a/website/docs/api/scorer.md b/website/docs/api/scorer.md index 987cc308e..1798f293e 100644 --- a/website/docs/api/scorer.md +++ b/website/docs/api/scorer.md @@ -95,7 +95,6 @@ Scores a single token attribute. | -------------- | ------------------- | ----------------------------------------------------------------------------------------------------------------------------- | | `examples` | `Iterable[Example]` | The `Example` objects holding both the predictions and the correct gold-standard annotations. | | `attr` | `str` | The attribute to score. | -| _keyword-only_ | | | | `getter` | `Callable` | Defaults to `getattr`. If provided, `getter(token, attr)` should return the value of the attribute for an individual `Token`. | | **RETURNS** | `Dict[str, float]` | A dictionary containing the score `{attr}_acc`. | @@ -116,7 +115,6 @@ format. | -------------- | ------------------- | ----------------------------------------------------------------------------------------------------------------------------- | | `examples` | `Iterable[Example]` | The `Example` objects holding both the predictions and the correct gold-standard annotations. | | `attr` | `str` | The attribute to score. | -| _keyword-only_ | | | | `getter` | `Callable` | Defaults to `getattr`. If provided, `getter(token, attr)` should return the value of the attribute for an individual `Token`. | | **RETURNS** | `Dict` | A dictionary containing the per-feature PRF scores under the key `{attr}_per_feat`. | @@ -135,7 +133,6 @@ Returns PRF scores for labeled or unlabeled spans. | -------------- | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | | `examples` | `Iterable[Example]` | The `Example` objects holding both the predictions and the correct gold-standard annotations. | | `attr` | `str` | The attribute to score. | -| _keyword-only_ | | | | `getter` | `Callable` | Defaults to `getattr`. If provided, `getter(doc, attr)` should return the `Span` objects for an individual `Doc`. | | **RETURNS** | `Dict` | A dictionary containing the PRF scores under the keys `{attr}_p`, `{attr}_r`, `{attr}_f` and the per-type PRF scores under `{attr}_per_type`. | @@ -164,7 +161,6 @@ Calculate the UAS, LAS, and LAS per type scores for dependency parses. | --------------- | ------------------- | ----------------------------------------------------------------------------------------------------------------------------- | | `examples` | `Iterable[Example]` | The `Example` objects holding both the predictions and the correct gold-standard annotations. | | `attr` | `str` | The attribute containing the dependency label. | -| _keyword-only_ | | | | `getter` | `Callable` | Defaults to `getattr`. If provided, `getter(token, attr)` should return the value of the attribute for an individual `Token`. | | `head_attr` | `str` | The attribute containing the head token. | | `head_getter` | `callable` | Defaults to `getattr`. If provided, `head_getter(token, attr)` should return the head for an individual `Token`. | @@ -200,7 +196,6 @@ depends on the scorer settings: | ---------------- | ------------------- | ------------------------------------------------------------------------------------------------------- | | `examples` | `Iterable[Example]` | The `Example` objects holding both the predictions and the correct gold-standard annotations. | | `attr` | `str` | The attribute to score. | -| _keyword-only_ | | | | `getter` | `Callable` | Defaults to `getattr`. If provided, `getter(doc, attr)` should return the cats for an individual `Doc`. | | labels | `Iterable[str]` | The set of possible labels. Defaults to `[]`. | | `multi_label` | `bool` | Whether the attribute allows multiple labels. Defaults to `True`. | From ca491722ad450d2c88388d5b3a4d70de39b857d5 Mon Sep 17 00:00:00 2001 From: Sofie Van Landeghem Date: Thu, 30 Jul 2020 23:30:54 +0200 Subject: [PATCH 04/16] The Parser is now a Pipe (2) (#5844) * moving syntax folder to _parser_internals * moving nn_parser and transition_system * move nn_parser and transition_system out of internals folder * moving nn_parser code into transition_system file * rename transition_system to transition_parser * moving parser_model and _state to ml * move _state back to internals * The Parser now inherits from Pipe! * small code fixes * removing unnecessary imports * remove link_vectors_to_models * transition_system to internals folder * little bit more cleanup * newlines --- bin/ud/ud_train.py | 2 +- examples/training/conllu.py | 2 +- setup.py | 16 +++--- spacy/cli/debug_data.py | 2 +- spacy/gold/example.pyx | 2 +- .../_parser_model.pxd => ml/parser_model.pxd} | 6 +-- .../_parser_model.pyx => ml/parser_model.pyx} | 17 ++----- spacy/ml/tb_framework.py | 2 +- .../_parser_internals}/__init__.py | 0 .../_parser_internals}/_state.pxd | 15 +++--- .../_parser_internals}/_state.pyx | 0 .../_parser_internals}/arc_eager.pxd | 6 +-- .../_parser_internals}/arc_eager.pyx | 19 +++---- .../_parser_internals}/ner.pxd | 2 - .../_parser_internals}/ner.pyx | 15 +++--- .../_parser_internals}/nonproj.pxd | 0 .../_parser_internals}/nonproj.pyx | 4 +- .../_parser_internals}/stateclass.pxd | 8 +-- .../_parser_internals}/stateclass.pyx | 2 +- .../_parser_internals}/transition_system.pxd | 8 +-- .../_parser_internals}/transition_system.pyx | 12 ++--- spacy/pipeline/dep_parser.pyx | 8 +-- spacy/pipeline/entity_linker.py | 12 ++--- spacy/pipeline/multitask.pyx | 7 ++- spacy/pipeline/ner.pyx | 6 +-- .../__init__.pxd => pipeline/nn_parser.pyx} | 0 spacy/pipeline/pipe.pxd | 2 + spacy/pipeline/pipe.pyx | 4 +- spacy/pipeline/sentencizer.pyx | 6 +++ spacy/pipeline/senter.pyx | 2 +- spacy/pipeline/simple_ner.py | 3 -- spacy/pipeline/textcat.py | 8 +-- spacy/pipeline/tok2vec.py | 3 ++ .../transition_parser.pxd} | 15 +++--- .../transition_parser.pyx} | 50 ++++++++----------- spacy/tests/parser/test_arc_eager_oracle.py | 4 +- spacy/tests/parser/test_ner.py | 2 +- spacy/tests/parser/test_neural_parser.py | 4 +- spacy/tests/parser/test_nonproj.py | 6 +-- 39 files changed, 124 insertions(+), 158 deletions(-) rename spacy/{syntax/_parser_model.pxd => ml/parser_model.pxd} (88%) rename spacy/{syntax/_parser_model.pyx => ml/parser_model.pyx} (97%) rename spacy/{syntax => pipeline/_parser_internals}/__init__.py (100%) rename spacy/{syntax => pipeline/_parser_internals}/_state.pxd (98%) rename spacy/{syntax => pipeline/_parser_internals}/_state.pyx (100%) rename spacy/{syntax => pipeline/_parser_internals}/arc_eager.pxd (65%) rename spacy/{syntax => pipeline/_parser_internals}/arc_eager.pyx (98%) rename spacy/{syntax => pipeline/_parser_internals}/ner.pxd (58%) rename spacy/{syntax => pipeline/_parser_internals}/ner.pyx (98%) rename spacy/{syntax => pipeline/_parser_internals}/nonproj.pxd (100%) rename spacy/{syntax => pipeline/_parser_internals}/nonproj.pyx (98%) rename spacy/{syntax => pipeline/_parser_internals}/stateclass.pxd (95%) rename spacy/{syntax => pipeline/_parser_internals}/stateclass.pyx (97%) rename spacy/{syntax => pipeline/_parser_internals}/transition_system.pxd (91%) rename spacy/{syntax => pipeline/_parser_internals}/transition_system.pyx (97%) rename spacy/{syntax/__init__.pxd => pipeline/nn_parser.pyx} (100%) create mode 100644 spacy/pipeline/pipe.pxd rename spacy/{syntax/nn_parser.pxd => pipeline/transition_parser.pxd} (62%) rename spacy/{syntax/nn_parser.pyx => pipeline/transition_parser.pyx} (95%) diff --git a/bin/ud/ud_train.py b/bin/ud/ud_train.py index ac5987aa4..11ad564ec 100644 --- a/bin/ud/ud_train.py +++ b/bin/ud/ud_train.py @@ -16,7 +16,7 @@ from bin.ud import conll17_ud_eval from spacy.tokens import Token, Doc from spacy.gold import Example from spacy.util import compounding, minibatch, minibatch_by_words -from spacy.syntax.nonproj import projectivize +from spacy.pipeline._parser_internals.nonproj import projectivize from spacy.matcher import Matcher from spacy import displacy from collections import defaultdict diff --git a/examples/training/conllu.py b/examples/training/conllu.py index ecc07ccf2..a398b0ae0 100644 --- a/examples/training/conllu.py +++ b/examples/training/conllu.py @@ -13,7 +13,7 @@ import spacy import spacy.util from spacy.tokens import Token, Doc from spacy.gold import Example -from spacy.syntax.nonproj import projectivize +from spacy.pipeline._parser_internals.nonproj import projectivize from collections import defaultdict from spacy.matcher import Matcher diff --git a/setup.py b/setup.py index 6d962ab59..af4cd0ec6 100755 --- a/setup.py +++ b/setup.py @@ -31,6 +31,7 @@ MOD_NAMES = [ "spacy.vocab", "spacy.attrs", "spacy.kb", + "spacy.ml.parser_model", "spacy.morphology", "spacy.pipeline.dep_parser", "spacy.pipeline.morphologizer", @@ -40,14 +41,14 @@ MOD_NAMES = [ "spacy.pipeline.sentencizer", "spacy.pipeline.senter", "spacy.pipeline.tagger", - "spacy.syntax.stateclass", - "spacy.syntax._state", + "spacy.pipeline.transition_parser", + "spacy.pipeline._parser_internals.arc_eager", + "spacy.pipeline._parser_internals.ner", + "spacy.pipeline._parser_internals.nonproj", + "spacy.pipeline._parser_internals._state", + "spacy.pipeline._parser_internals.stateclass", + "spacy.pipeline._parser_internals.transition_system", "spacy.tokenizer", - "spacy.syntax.nn_parser", - "spacy.syntax._parser_model", - "spacy.syntax.nonproj", - "spacy.syntax.transition_system", - "spacy.syntax.arc_eager", "spacy.gold.gold_io", "spacy.tokens.doc", "spacy.tokens.span", @@ -57,7 +58,6 @@ MOD_NAMES = [ "spacy.matcher.matcher", "spacy.matcher.phrasematcher", "spacy.matcher.dependencymatcher", - "spacy.syntax.ner", "spacy.symbols", "spacy.vectors", ] diff --git a/spacy/cli/debug_data.py b/spacy/cli/debug_data.py index 1ffceeca1..fa6f7a7d5 100644 --- a/spacy/cli/debug_data.py +++ b/spacy/cli/debug_data.py @@ -10,7 +10,7 @@ from thinc.api import Config from ._util import app, Arg, Opt, show_validation_error, parse_config_overrides from ._util import import_code, debug_cli from ..gold import Corpus, Example -from ..syntax import nonproj +from ..pipeline._parser_internals import nonproj from ..language import Language from .. import util diff --git a/spacy/gold/example.pyx b/spacy/gold/example.pyx index 9101cefce..84d9f1622 100644 --- a/spacy/gold/example.pyx +++ b/spacy/gold/example.pyx @@ -10,7 +10,7 @@ from .align import Alignment from .iob_utils import biluo_to_iob, biluo_tags_from_offsets, biluo_tags_from_doc from .iob_utils import spans_from_biluo_tags from ..errors import Errors, Warnings -from ..syntax import nonproj +from ..pipeline._parser_internals import nonproj cpdef Doc annotations2doc(vocab, tok_annot, doc_annot): diff --git a/spacy/syntax/_parser_model.pxd b/spacy/ml/parser_model.pxd similarity index 88% rename from spacy/syntax/_parser_model.pxd rename to spacy/ml/parser_model.pxd index 15befb372..6582b3468 100644 --- a/spacy/syntax/_parser_model.pxd +++ b/spacy/ml/parser_model.pxd @@ -1,8 +1,6 @@ from libc.string cimport memset, memcpy -from libc.stdlib cimport calloc, free, realloc -from ..typedefs cimport weight_t, class_t, hash_t - -from ._state cimport StateC +from ..typedefs cimport weight_t, hash_t +from ..pipeline._parser_internals._state cimport StateC cdef struct SizesC: diff --git a/spacy/syntax/_parser_model.pyx b/spacy/ml/parser_model.pyx similarity index 97% rename from spacy/syntax/_parser_model.pyx rename to spacy/ml/parser_model.pyx index eedd84bac..da937ca4f 100644 --- a/spacy/syntax/_parser_model.pyx +++ b/spacy/ml/parser_model.pyx @@ -1,29 +1,18 @@ # cython: infer_types=True, cdivision=True, boundscheck=False -cimport cython.parallel cimport numpy as np from libc.math cimport exp -from libcpp.vector cimport vector from libc.string cimport memset, memcpy from libc.stdlib cimport calloc, free, realloc -from cymem.cymem cimport Pool -from thinc.extra.search cimport Beam from thinc.backends.linalg cimport Vec, VecVec cimport blis.cy import numpy import numpy.random -from thinc.api import Linear, Model, CupyOps, NumpyOps, use_ops, noop +from thinc.api import Model, CupyOps, NumpyOps -from ..typedefs cimport weight_t, class_t, hash_t -from ..tokens.doc cimport Doc -from .stateclass cimport StateClass -from .transition_system cimport Transition - -from ..compat import copy_array -from ..errors import Errors, TempErrors -from ..util import create_default_optimizer from .. import util -from . import nonproj +from ..typedefs cimport weight_t, class_t, hash_t +from ..pipeline._parser_internals.stateclass cimport StateClass cdef WeightsC get_c_weights(model) except *: diff --git a/spacy/ml/tb_framework.py b/spacy/ml/tb_framework.py index 39d4b0a14..44f125a04 100644 --- a/spacy/ml/tb_framework.py +++ b/spacy/ml/tb_framework.py @@ -1,5 +1,5 @@ from thinc.api import Model, noop, use_ops, Linear -from ..syntax._parser_model import ParserStepModel +from .parser_model import ParserStepModel def TransitionModel(tok2vec, lower, upper, dropout=0.2, unseen_classes=set()): diff --git a/spacy/syntax/__init__.py b/spacy/pipeline/_parser_internals/__init__.py similarity index 100% rename from spacy/syntax/__init__.py rename to spacy/pipeline/_parser_internals/__init__.py diff --git a/spacy/syntax/_state.pxd b/spacy/pipeline/_parser_internals/_state.pxd similarity index 98% rename from spacy/syntax/_state.pxd rename to spacy/pipeline/_parser_internals/_state.pxd index fef4f0c92..0d0dd8c05 100644 --- a/spacy/syntax/_state.pxd +++ b/spacy/pipeline/_parser_internals/_state.pxd @@ -1,15 +1,14 @@ -from libc.string cimport memcpy, memset, memmove -from libc.stdlib cimport malloc, calloc, free +from libc.string cimport memcpy, memset +from libc.stdlib cimport calloc, free from libc.stdint cimport uint32_t, uint64_t from cpython.exc cimport PyErr_CheckSignals, PyErr_SetFromErrno from murmurhash.mrmr cimport hash64 -from ..vocab cimport EMPTY_LEXEME -from ..structs cimport TokenC, SpanC -from ..lexeme cimport Lexeme -from ..symbols cimport punct -from ..attrs cimport IS_SPACE -from ..typedefs cimport attr_t +from ...vocab cimport EMPTY_LEXEME +from ...structs cimport TokenC, SpanC +from ...lexeme cimport Lexeme +from ...attrs cimport IS_SPACE +from ...typedefs cimport attr_t cdef inline bint is_space_token(const TokenC* token) nogil: diff --git a/spacy/syntax/_state.pyx b/spacy/pipeline/_parser_internals/_state.pyx similarity index 100% rename from spacy/syntax/_state.pyx rename to spacy/pipeline/_parser_internals/_state.pyx diff --git a/spacy/syntax/arc_eager.pxd b/spacy/pipeline/_parser_internals/arc_eager.pxd similarity index 65% rename from spacy/syntax/arc_eager.pxd rename to spacy/pipeline/_parser_internals/arc_eager.pxd index a59be716a..e05a34f56 100644 --- a/spacy/syntax/arc_eager.pxd +++ b/spacy/pipeline/_parser_internals/arc_eager.pxd @@ -1,8 +1,6 @@ -from cymem.cymem cimport Pool - from .stateclass cimport StateClass -from ..typedefs cimport weight_t, attr_t -from .transition_system cimport TransitionSystem, Transition +from ...typedefs cimport weight_t, attr_t +from .transition_system cimport Transition, TransitionSystem cdef class ArcEager(TransitionSystem): diff --git a/spacy/syntax/arc_eager.pyx b/spacy/pipeline/_parser_internals/arc_eager.pyx similarity index 98% rename from spacy/syntax/arc_eager.pyx rename to spacy/pipeline/_parser_internals/arc_eager.pyx index 6e63859f0..7db8aae0f 100644 --- a/spacy/syntax/arc_eager.pyx +++ b/spacy/pipeline/_parser_internals/arc_eager.pyx @@ -1,24 +1,17 @@ # cython: profile=True, cdivision=True, infer_types=True -from cpython.ref cimport Py_INCREF from cymem.cymem cimport Pool, Address from libc.stdint cimport int32_t from collections import defaultdict, Counter -import json -from ..typedefs cimport hash_t, attr_t -from ..strings cimport hash_string -from ..structs cimport TokenC -from ..tokens.doc cimport Doc, set_children_from_heads +from ...typedefs cimport hash_t, attr_t +from ...strings cimport hash_string +from ...structs cimport TokenC +from ...tokens.doc cimport Doc, set_children_from_heads +from ...gold.example cimport Example +from ...errors import Errors from .stateclass cimport StateClass from ._state cimport StateC -from .transition_system cimport move_cost_func_t, label_cost_func_t -from ..gold.example cimport Example - -from ..errors import Errors -from .nonproj import is_nonproj_tree -from . import nonproj - # Calculate cost as gold/not gold. We don't use scalar value anyway. cdef int BINARY_COSTS = 1 diff --git a/spacy/syntax/ner.pxd b/spacy/pipeline/_parser_internals/ner.pxd similarity index 58% rename from spacy/syntax/ner.pxd rename to spacy/pipeline/_parser_internals/ner.pxd index 989593a92..2264a1518 100644 --- a/spacy/syntax/ner.pxd +++ b/spacy/pipeline/_parser_internals/ner.pxd @@ -1,6 +1,4 @@ from .transition_system cimport TransitionSystem -from .transition_system cimport Transition -from ..typedefs cimport attr_t cdef class BiluoPushDown(TransitionSystem): diff --git a/spacy/syntax/ner.pyx b/spacy/pipeline/_parser_internals/ner.pyx similarity index 98% rename from spacy/syntax/ner.pyx rename to spacy/pipeline/_parser_internals/ner.pyx index c4125bbdf..2570ccdee 100644 --- a/spacy/syntax/ner.pyx +++ b/spacy/pipeline/_parser_internals/ner.pyx @@ -2,17 +2,14 @@ from collections import Counter from libc.stdint cimport int32_t from cymem.cymem cimport Pool -from ..typedefs cimport weight_t +from ...typedefs cimport weight_t, attr_t +from ...lexeme cimport Lexeme +from ...attrs cimport IS_SPACE +from ...gold.example cimport Example +from ...errors import Errors from .stateclass cimport StateClass from ._state cimport StateC -from .transition_system cimport Transition -from .transition_system cimport do_func_t -from ..lexeme cimport Lexeme -from ..attrs cimport IS_SPACE -from ..gold.iob_utils import biluo_tags_from_offsets -from ..gold.example cimport Example - -from ..errors import Errors +from .transition_system cimport Transition, do_func_t cdef enum: diff --git a/spacy/syntax/nonproj.pxd b/spacy/pipeline/_parser_internals/nonproj.pxd similarity index 100% rename from spacy/syntax/nonproj.pxd rename to spacy/pipeline/_parser_internals/nonproj.pxd diff --git a/spacy/syntax/nonproj.pyx b/spacy/pipeline/_parser_internals/nonproj.pyx similarity index 98% rename from spacy/syntax/nonproj.pyx rename to spacy/pipeline/_parser_internals/nonproj.pyx index 5ccb11f37..8f5fdaa71 100644 --- a/spacy/syntax/nonproj.pyx +++ b/spacy/pipeline/_parser_internals/nonproj.pyx @@ -5,9 +5,9 @@ scheme. """ from copy import copy -from ..tokens.doc cimport Doc, set_children_from_heads +from ...tokens.doc cimport Doc, set_children_from_heads -from ..errors import Errors +from ...errors import Errors DELIMITER = '||' diff --git a/spacy/syntax/stateclass.pxd b/spacy/pipeline/_parser_internals/stateclass.pxd similarity index 95% rename from spacy/syntax/stateclass.pxd rename to spacy/pipeline/_parser_internals/stateclass.pxd index 567982a3f..1d9f05538 100644 --- a/spacy/syntax/stateclass.pxd +++ b/spacy/pipeline/_parser_internals/stateclass.pxd @@ -1,12 +1,8 @@ -from libc.string cimport memcpy, memset - from cymem.cymem cimport Pool -cimport cython -from ..structs cimport TokenC, SpanC -from ..typedefs cimport attr_t +from ...structs cimport TokenC, SpanC +from ...typedefs cimport attr_t -from ..vocab cimport EMPTY_LEXEME from ._state cimport StateC diff --git a/spacy/syntax/stateclass.pyx b/spacy/pipeline/_parser_internals/stateclass.pyx similarity index 97% rename from spacy/syntax/stateclass.pyx rename to spacy/pipeline/_parser_internals/stateclass.pyx index e472e9861..880cf6cc5 100644 --- a/spacy/syntax/stateclass.pyx +++ b/spacy/pipeline/_parser_internals/stateclass.pyx @@ -1,7 +1,7 @@ # cython: infer_types=True import numpy -from ..tokens.doc cimport Doc +from ...tokens.doc cimport Doc cdef class StateClass: diff --git a/spacy/syntax/transition_system.pxd b/spacy/pipeline/_parser_internals/transition_system.pxd similarity index 91% rename from spacy/syntax/transition_system.pxd rename to spacy/pipeline/_parser_internals/transition_system.pxd index 836c08168..ba4c33814 100644 --- a/spacy/syntax/transition_system.pxd +++ b/spacy/pipeline/_parser_internals/transition_system.pxd @@ -1,11 +1,11 @@ from cymem.cymem cimport Pool -from ..typedefs cimport attr_t, weight_t -from ..structs cimport TokenC -from ..strings cimport StringStore +from ...typedefs cimport attr_t, weight_t +from ...structs cimport TokenC +from ...strings cimport StringStore +from ...gold.example cimport Example from .stateclass cimport StateClass from ._state cimport StateC -from ..gold.example cimport Example cdef struct Transition: diff --git a/spacy/syntax/transition_system.pyx b/spacy/pipeline/_parser_internals/transition_system.pyx similarity index 97% rename from spacy/syntax/transition_system.pyx rename to spacy/pipeline/_parser_internals/transition_system.pyx index 17166dcf5..7694e7f34 100644 --- a/spacy/syntax/transition_system.pyx +++ b/spacy/pipeline/_parser_internals/transition_system.pyx @@ -1,19 +1,17 @@ # cython: infer_types=True from __future__ import print_function -from cpython.ref cimport Py_INCREF from cymem.cymem cimport Pool from collections import Counter import srsly -from ..typedefs cimport weight_t -from ..tokens.doc cimport Doc -from ..structs cimport TokenC +from ...typedefs cimport weight_t, attr_t +from ...tokens.doc cimport Doc +from ...structs cimport TokenC from .stateclass cimport StateClass -from ..typedefs cimport attr_t -from ..errors import Errors -from .. import util +from ...errors import Errors +from ... import util cdef weight_t MIN_SCORE = -90000 diff --git a/spacy/pipeline/dep_parser.pyx b/spacy/pipeline/dep_parser.pyx index a952385b4..65ffbbe50 100644 --- a/spacy/pipeline/dep_parser.pyx +++ b/spacy/pipeline/dep_parser.pyx @@ -1,13 +1,13 @@ # cython: infer_types=True, profile=True, binding=True from typing import Optional, Iterable -from thinc.api import CosineDistance, to_categorical, get_array_module, Model, Config +from thinc.api import Model, Config -from ..syntax.nn_parser cimport Parser -from ..syntax.arc_eager cimport ArcEager +from .transition_parser cimport Parser +from ._parser_internals.arc_eager cimport ArcEager from .functions import merge_subtokens from ..language import Language -from ..syntax import nonproj +from ._parser_internals import nonproj from ..scorer import Scorer diff --git a/spacy/pipeline/entity_linker.py b/spacy/pipeline/entity_linker.py index cc4e7b159..742b349e5 100644 --- a/spacy/pipeline/entity_linker.py +++ b/spacy/pipeline/entity_linker.py @@ -222,9 +222,9 @@ class EntityLinker(Pipe): set_dropout_rate(self.model, drop) if not sentence_docs: warnings.warn(Warnings.W093.format(name="Entity Linker")) - return 0.0 + return losses sentence_encodings, bp_context = self.model.begin_update(sentence_docs) - loss, d_scores = self.get_similarity_loss( + loss, d_scores = self.get_loss( sentence_encodings=sentence_encodings, examples=examples ) bp_context(d_scores) @@ -235,7 +235,7 @@ class EntityLinker(Pipe): self.set_annotations(docs, predictions) return losses - def get_similarity_loss(self, examples: Iterable[Example], sentence_encodings): + def get_loss(self, examples: Iterable[Example], sentence_encodings): entity_encodings = [] for eg in examples: kb_ids = eg.get_aligned("ENT_KB_ID", as_string=True) @@ -247,7 +247,7 @@ class EntityLinker(Pipe): entity_encodings = self.model.ops.asarray(entity_encodings, dtype="float32") if sentence_encodings.shape != entity_encodings.shape: err = Errors.E147.format( - method="get_similarity_loss", msg="gold entities do not match up" + method="get_loss", msg="gold entities do not match up" ) raise RuntimeError(err) gradients = self.distance.get_grad(sentence_encodings, entity_encodings) @@ -337,13 +337,13 @@ class EntityLinker(Pipe): final_kb_ids.append(candidates[0].entity_) else: random.shuffle(candidates) - # this will set all prior probabilities to 0 if they should be excluded from the model + # set all prior probabilities to 0 if incl_prior=False prior_probs = xp.asarray( [c.prior_prob for c in candidates] ) if not self.cfg.get("incl_prior"): prior_probs = xp.asarray( - [0.0 for c in candidates] + [0.0 for _ in candidates] ) scores = prior_probs # add in similarity from the context diff --git a/spacy/pipeline/multitask.pyx b/spacy/pipeline/multitask.pyx index 97826aaa6..d85030adb 100644 --- a/spacy/pipeline/multitask.pyx +++ b/spacy/pipeline/multitask.pyx @@ -1,7 +1,7 @@ # cython: infer_types=True, profile=True, binding=True from typing import Optional import numpy -from thinc.api import CosineDistance, to_categorical, to_categorical, Model, Config +from thinc.api import CosineDistance, to_categorical, Model, Config from thinc.api import set_dropout_rate from ..tokens.doc cimport Doc @@ -9,7 +9,7 @@ from ..tokens.doc cimport Doc from .pipe import Pipe from .tagger import Tagger from ..language import Language -from ..syntax import nonproj +from ._parser_internals import nonproj from ..attrs import POS, ID from ..errors import Errors @@ -219,3 +219,6 @@ class ClozeMultitask(Pipe): if losses is not None: losses[self.name] += loss + + def add_label(self, label): + raise NotImplementedError diff --git a/spacy/pipeline/ner.pyx b/spacy/pipeline/ner.pyx index 7ee4448fb..7f4fb8363 100644 --- a/spacy/pipeline/ner.pyx +++ b/spacy/pipeline/ner.pyx @@ -1,9 +1,9 @@ # cython: infer_types=True, profile=True, binding=True from typing import Optional, Iterable -from thinc.api import CosineDistance, to_categorical, get_array_module, Model, Config +from thinc.api import Model, Config -from ..syntax.nn_parser cimport Parser -from ..syntax.ner cimport BiluoPushDown +from .transition_parser cimport Parser +from ._parser_internals.ner cimport BiluoPushDown from ..language import Language from ..scorer import Scorer diff --git a/spacy/syntax/__init__.pxd b/spacy/pipeline/nn_parser.pyx similarity index 100% rename from spacy/syntax/__init__.pxd rename to spacy/pipeline/nn_parser.pyx diff --git a/spacy/pipeline/pipe.pxd b/spacy/pipeline/pipe.pxd new file mode 100644 index 000000000..bb97f79d0 --- /dev/null +++ b/spacy/pipeline/pipe.pxd @@ -0,0 +1,2 @@ +cdef class Pipe: + cdef public str name diff --git a/spacy/pipeline/pipe.pyx b/spacy/pipeline/pipe.pyx index 196cdebdc..1a94905a2 100644 --- a/spacy/pipeline/pipe.pyx +++ b/spacy/pipeline/pipe.pyx @@ -8,7 +8,7 @@ from ..errors import Errors from .. import util -class Pipe: +cdef class Pipe: """This class is a base class and not instantiated directly. Trainable pipeline components like the EntityRecognizer or TextCategorizer inherit from it and it defines the interface that components should follow to @@ -17,8 +17,6 @@ class Pipe: DOCS: https://spacy.io/api/pipe """ - name = None - def __init__(self, vocab, model, name, **cfg): """Initialize a pipeline component. diff --git a/spacy/pipeline/sentencizer.pyx b/spacy/pipeline/sentencizer.pyx index 31208ea2c..be4351212 100644 --- a/spacy/pipeline/sentencizer.pyx +++ b/spacy/pipeline/sentencizer.pyx @@ -203,3 +203,9 @@ class Sentencizer(Pipe): cfg = srsly.read_json(path) self.punct_chars = set(cfg.get("punct_chars", self.default_punct_chars)) return self + + def get_loss(self, examples, scores): + raise NotImplementedError + + def add_label(self, label): + raise NotImplementedError diff --git a/spacy/pipeline/senter.pyx b/spacy/pipeline/senter.pyx index c6eb43661..f826f21de 100644 --- a/spacy/pipeline/senter.pyx +++ b/spacy/pipeline/senter.pyx @@ -109,7 +109,7 @@ class SentenceRecognizer(Tagger): for eg in examples: eg_truth = [] for x in eg.get_aligned("sent_start"): - if x == None: + if x is None: eg_truth.append(None) elif x == 1: eg_truth.append(labels[1]) diff --git a/spacy/pipeline/simple_ner.py b/spacy/pipeline/simple_ner.py index 9b9872b77..44e1182c1 100644 --- a/spacy/pipeline/simple_ner.py +++ b/spacy/pipeline/simple_ner.py @@ -131,8 +131,6 @@ class SimpleNER(Pipe): return losses def get_loss(self, examples: List[Example], scores) -> Tuple[List[Floats2d], float]: - loss = 0 - d_scores = [] truths = [] for eg in examples: tags = eg.get_aligned("TAG", as_string=True) @@ -159,7 +157,6 @@ class SimpleNER(Pipe): if not hasattr(get_examples, "__call__"): gold_tuples = get_examples get_examples = lambda: gold_tuples - labels = _get_labels(get_examples()) for label in _get_labels(get_examples()): self.add_label(label) labels = self.labels diff --git a/spacy/pipeline/textcat.py b/spacy/pipeline/textcat.py index 2c399defc..639ce5514 100644 --- a/spacy/pipeline/textcat.py +++ b/spacy/pipeline/textcat.py @@ -238,8 +238,11 @@ class TextCategorizer(Pipe): DOCS: https://spacy.io/api/textcategorizer#rehearse """ + + if losses is not None: + losses.setdefault(self.name, 0.0) if self._rehearsal_model is None: - return + return losses try: docs = [eg.predicted for eg in examples] except AttributeError: @@ -250,7 +253,7 @@ class TextCategorizer(Pipe): raise TypeError(err) if not any(len(doc) for doc in docs): # Handle cases where there are no tokens in any docs. - return + return losses set_dropout_rate(self.model, drop) scores, bp_scores = self.model.begin_update(docs) target = self._rehearsal_model(examples) @@ -259,7 +262,6 @@ class TextCategorizer(Pipe): if sgd is not None: self.model.finish_update(sgd) if losses is not None: - losses.setdefault(self.name, 0.0) losses[self.name] += (gradient ** 2).sum() return losses diff --git a/spacy/pipeline/tok2vec.py b/spacy/pipeline/tok2vec.py index b147cf177..31643a7d3 100644 --- a/spacy/pipeline/tok2vec.py +++ b/spacy/pipeline/tok2vec.py @@ -199,6 +199,9 @@ class Tok2Vec(Pipe): docs = [Doc(self.vocab, words=["hello"])] self.model.initialize(X=docs) + def add_label(self, label): + raise NotImplementedError + class Tok2VecListener(Model): """A layer that gets fed its answers from an upstream connection, diff --git a/spacy/syntax/nn_parser.pxd b/spacy/pipeline/transition_parser.pxd similarity index 62% rename from spacy/syntax/nn_parser.pxd rename to spacy/pipeline/transition_parser.pxd index 7840ec27a..e594a3098 100644 --- a/spacy/syntax/nn_parser.pxd +++ b/spacy/pipeline/transition_parser.pxd @@ -1,16 +1,15 @@ -from .stateclass cimport StateClass -from .arc_eager cimport TransitionSystem +from cymem.cymem cimport Pool + from ..vocab cimport Vocab -from ..tokens.doc cimport Doc -from ..structs cimport TokenC -from ._state cimport StateC -from ._parser_model cimport WeightsC, ActivationsC, SizesC +from .pipe cimport Pipe +from ._parser_internals.transition_system cimport Transition, TransitionSystem +from ._parser_internals._state cimport StateC +from ..ml.parser_model cimport WeightsC, ActivationsC, SizesC -cdef class Parser: +cdef class Parser(Pipe): cdef readonly Vocab vocab cdef public object model - cdef public str name cdef public object _rehearsal_model cdef readonly TransitionSystem moves cdef readonly object cfg diff --git a/spacy/syntax/nn_parser.pyx b/spacy/pipeline/transition_parser.pyx similarity index 95% rename from spacy/syntax/nn_parser.pyx rename to spacy/pipeline/transition_parser.pyx index a0ee13a0a..b14a55cb4 100644 --- a/spacy/syntax/nn_parser.pyx +++ b/spacy/pipeline/transition_parser.pyx @@ -1,42 +1,32 @@ # cython: infer_types=True, cdivision=True, boundscheck=False -cimport cython.parallel +from __future__ import print_function +from cymem.cymem cimport Pool cimport numpy as np from itertools import islice -from cpython.ref cimport PyObject, Py_XDECREF -from cpython.exc cimport PyErr_CheckSignals, PyErr_SetFromErrno -from libc.math cimport exp from libcpp.vector cimport vector -from libc.string cimport memset, memcpy +from libc.string cimport memset from libc.stdlib cimport calloc, free -from cymem.cymem cimport Pool -from thinc.backends.linalg cimport Vec, VecVec -from thinc.api import chain, clone, Linear, list2array, NumpyOps, CupyOps, use_ops -from thinc.api import get_array_module, zero_init, set_dropout_rate -from itertools import islice import srsly + +from ._parser_internals.stateclass cimport StateClass +from ..ml.parser_model cimport alloc_activations, free_activations +from ..ml.parser_model cimport predict_states, arg_max_if_valid +from ..ml.parser_model cimport WeightsC, ActivationsC, SizesC, cpu_log_loss +from ..ml.parser_model cimport get_c_weights, get_c_sizes + +from ..tokens.doc cimport Doc +from ..errors import Errors, Warnings +from .. import util +from ..util import create_default_optimizer + +from thinc.api import set_dropout_rate import numpy.random import numpy import warnings -from ..tokens.doc cimport Doc -from ..typedefs cimport weight_t, class_t, hash_t -from ._parser_model cimport alloc_activations, free_activations -from ._parser_model cimport predict_states, arg_max_if_valid -from ._parser_model cimport WeightsC, ActivationsC, SizesC, cpu_log_loss -from ._parser_model cimport get_c_weights, get_c_sizes -from .stateclass cimport StateClass -from ._state cimport StateC -from .transition_system cimport Transition -from ..util import create_default_optimizer, registry -from ..compat import copy_array -from ..errors import Errors, Warnings -from .. import util -from . import nonproj - - -cdef class Parser: +cdef class Parser(Pipe): """ Base class of the DependencyParser and EntityRecognizer. """ @@ -107,7 +97,7 @@ cdef class Parser: @property def tok2vec(self): - '''Return the embedding and convolutional layer of the model.''' + """Return the embedding and convolutional layer of the model.""" return self.model.get_ref("tok2vec") @property @@ -138,13 +128,13 @@ cdef class Parser: raise NotImplementedError def init_multitask_objectives(self, get_examples, pipeline, **cfg): - '''Setup models for secondary objectives, to benefit from multi-task + """Setup models for secondary objectives, to benefit from multi-task learning. This method is intended to be overridden by subclasses. For instance, the dependency parser can benefit from sharing an input representation with a label prediction model. These auxiliary models are discarded after training. - ''' + """ pass def use_params(self, params): diff --git a/spacy/tests/parser/test_arc_eager_oracle.py b/spacy/tests/parser/test_arc_eager_oracle.py index 77e142215..fd1880030 100644 --- a/spacy/tests/parser/test_arc_eager_oracle.py +++ b/spacy/tests/parser/test_arc_eager_oracle.py @@ -4,8 +4,8 @@ from spacy import registry from spacy.gold import Example from spacy.pipeline import DependencyParser from spacy.tokens import Doc -from spacy.syntax.nonproj import projectivize -from spacy.syntax.arc_eager import ArcEager +from spacy.pipeline._parser_internals.nonproj import projectivize +from spacy.pipeline._parser_internals.arc_eager import ArcEager from spacy.pipeline.dep_parser import DEFAULT_PARSER_MODEL diff --git a/spacy/tests/parser/test_ner.py b/spacy/tests/parser/test_ner.py index 4a6bf73a5..013ae6b7e 100644 --- a/spacy/tests/parser/test_ner.py +++ b/spacy/tests/parser/test_ner.py @@ -5,7 +5,7 @@ from spacy.lang.en import English from spacy.language import Language from spacy.lookups import Lookups -from spacy.syntax.ner import BiluoPushDown +from spacy.pipeline._parser_internals.ner import BiluoPushDown from spacy.gold import Example from spacy.tokens import Doc from spacy.vocab import Vocab diff --git a/spacy/tests/parser/test_neural_parser.py b/spacy/tests/parser/test_neural_parser.py index feae52f7f..6594c7e78 100644 --- a/spacy/tests/parser/test_neural_parser.py +++ b/spacy/tests/parser/test_neural_parser.py @@ -3,8 +3,8 @@ import pytest from spacy import registry from spacy.gold import Example from spacy.vocab import Vocab -from spacy.syntax.arc_eager import ArcEager -from spacy.syntax.nn_parser import Parser +from spacy.pipeline._parser_internals.arc_eager import ArcEager +from spacy.pipeline.transition_parser import Parser from spacy.tokens.doc import Doc from thinc.api import Model from spacy.pipeline.tok2vec import DEFAULT_TOK2VEC_MODEL diff --git a/spacy/tests/parser/test_nonproj.py b/spacy/tests/parser/test_nonproj.py index 496ec7e03..5bdebd0ca 100644 --- a/spacy/tests/parser/test_nonproj.py +++ b/spacy/tests/parser/test_nonproj.py @@ -1,7 +1,7 @@ import pytest -from spacy.syntax.nonproj import ancestors, contains_cycle, is_nonproj_arc -from spacy.syntax.nonproj import is_nonproj_tree -from spacy.syntax import nonproj +from spacy.pipeline._parser_internals.nonproj import ancestors, contains_cycle, is_nonproj_arc +from spacy.pipeline._parser_internals.nonproj import is_nonproj_tree +from spacy.pipeline._parser_internals import nonproj from ..util import get_doc From 901801b33bc64508b6bc36d49fecf821c5352517 Mon Sep 17 00:00:00 2001 From: Adriane Boyd Date: Fri, 31 Jul 2020 10:55:44 +0200 Subject: [PATCH 05/16] Fix default arguments in DependencyParser.score --- spacy/pipeline/dep_parser.pyx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/spacy/pipeline/dep_parser.pyx b/spacy/pipeline/dep_parser.pyx index 65ffbbe50..a022d04d6 100644 --- a/spacy/pipeline/dep_parser.pyx +++ b/spacy/pipeline/dep_parser.pyx @@ -34,7 +34,7 @@ DEFAULT_PARSER_MODEL = Config().from_str(default_model_config)["model"] @Language.factory( "parser", - assigns=["token.dep", "token.is_sent_start", "doc.sents"], + assigns=["token.dep", "token.head", "token.is_sent_start", "doc.sents"], default_config={ "moves": None, "update_with_oracle_cut_size": 100, @@ -120,7 +120,8 @@ cdef class DependencyParser(Parser): return dep results = {} results.update(Scorer.score_spans(examples, "sents", **kwargs)) - results.update(Scorer.score_deps(examples, "dep", getter=dep_getter, - ignore_labels=("p", "punct"), **kwargs)) + kwargs.setdefault("getter", dep_getter) + kwargs.setdefault("ignore_label", ("p", "punct")) + results.update(Scorer.score_deps(examples, "dep", **kwargs)) del results["sents_per_type"] return results From 9b509aa87f61ca4ea994b6e6ec6594ad9b6bdfa4 Mon Sep 17 00:00:00 2001 From: Adriane Boyd Date: Fri, 31 Jul 2020 11:02:17 +0200 Subject: [PATCH 06/16] Move Language.evaluate scorer config to new arg Move `Language.evaluate` scorer config from `component_cfg` to separate argument `scorer_cfg`. --- spacy/language.py | 7 ++++++- spacy/tests/pipeline/test_textcat.py | 2 +- website/docs/api/language.md | 1 + 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/spacy/language.py b/spacy/language.py index e415869b3..77f4be8bb 100644 --- a/spacy/language.py +++ b/spacy/language.py @@ -1099,6 +1099,7 @@ class Language: batch_size: int = 256, scorer: Optional[Scorer] = None, component_cfg: Optional[Dict[str, Dict[str, Any]]] = None, + scorer_cfg: Optional[Dict[str, Any]] = None, ) -> Dict[str, Union[float, dict]]: """Evaluate a model's pipeline components. @@ -1109,6 +1110,8 @@ class Language: will be created. component_cfg (dict): An optional dictionary with extra keyword arguments for specific components. + scorer_cfg (dict): An optional dictionary with extra keyword arguments + for the scorer. RETURNS (Scorer): The scorer containing the evaluation results. DOCS: https://spacy.io/api/language#evaluate @@ -1126,8 +1129,10 @@ class Language: raise TypeError(err) if component_cfg is None: component_cfg = {} + if scorer_cfg is None: + scorer_cfg = {} if scorer is None: - kwargs = component_cfg.get("scorer", {}) + kwargs = dict(scorer_cfg) kwargs.setdefault("verbose", verbose) kwargs.setdefault("nlp", self) scorer = Scorer(**kwargs) diff --git a/spacy/tests/pipeline/test_textcat.py b/spacy/tests/pipeline/test_textcat.py index d5a549f13..c18d00a00 100644 --- a/spacy/tests/pipeline/test_textcat.py +++ b/spacy/tests/pipeline/test_textcat.py @@ -118,7 +118,7 @@ def test_overfitting_IO(): # Test scoring scores = nlp.evaluate( - train_examples, component_cfg={"scorer": {"positive_label": "POSITIVE"}} + train_examples, scorer_cfg={"positive_label": "POSITIVE"} ) assert scores["cats_f"] == 1.0 assert scores["cats_score"] == 1.0 diff --git a/website/docs/api/language.md b/website/docs/api/language.md index 7e25106d1..377852a69 100644 --- a/website/docs/api/language.md +++ b/website/docs/api/language.md @@ -302,6 +302,7 @@ Evaluate a model's pipeline components. | `batch_size` | int | The batch size to use. | | `scorer` | `Scorer` | Optional [`Scorer`](/api/scorer) to use. If not passed in, a new one will be created. | | `component_cfg` | `Dict[str, dict]` | Optional dictionary of keyword arguments for components, keyed by component names. Defaults to `None`. | +| `scorer_cfg` | `Dict[str, Any]` | Optional dictionary of keyword arguments for the `Scorer`. Defaults to `None`. | | **RETURNS** | `Dict[str, Union[float, dict]]` | A dictionary of evaluation scores. | ## Language.use_params {#use_params tag="contextmanager, method"} From 160f1a5f947d65c2b8cb4de60953ac3372442dd4 Mon Sep 17 00:00:00 2001 From: Ines Montani Date: Fri, 31 Jul 2020 13:26:39 +0200 Subject: [PATCH 07/16] Update docs [ci skip] --- website/docs/usage/processing-pipelines.md | 2 + website/docs/usage/training.md | 113 ++++++--- website/docs/usage/transformers.md | 3 +- website/src/components/copy.js | 24 +- website/src/components/icon.js | 4 +- website/src/components/quickstart.js | 252 +++++++++++++++------ website/src/images/icons/download.svg | 4 + website/src/styles/quickstart.module.sass | 51 +++++ website/src/widgets/quickstart-install.js | 2 +- website/src/widgets/quickstart-training.js | 118 ++++++++++ 10 files changed, 454 insertions(+), 119 deletions(-) create mode 100644 website/src/images/icons/download.svg create mode 100644 website/src/widgets/quickstart-training.js diff --git a/website/docs/usage/processing-pipelines.md b/website/docs/usage/processing-pipelines.md index 56ade692a..2bdd560da 100644 --- a/website/docs/usage/processing-pipelines.md +++ b/website/docs/usage/processing-pipelines.md @@ -489,6 +489,8 @@ All other settings can be passed in by the user via the `config` argument on [`@Language.factory`](/api/language#factory) decorator also lets you define a `default_config` that's used as a fallback. + + ```python ### With config {highlight="4,9"} import spacy diff --git a/website/docs/usage/training.md b/website/docs/usage/training.md index 12785b6de..635b52c89 100644 --- a/website/docs/usage/training.md +++ b/website/docs/usage/training.md @@ -3,7 +3,8 @@ title: Training Models next: /usage/projects menu: - ['Introduction', 'basics'] - - ['CLI & Config', 'cli-config'] + - ['Quickstart', 'quickstart'] + - ['Config System', 'config'] - ['Transfer Learning', 'transfer-learning'] - ['Custom Models', 'custom-models'] - ['Parallel Training', 'parallel-training'] @@ -29,12 +30,13 @@ ready-to-use spaCy models. -## Training CLI & config {#cli-config} +### Training CLI & config {#cli-config} The recommended way to train your spaCy models is via the -[`spacy train`](/api/cli#train) command on the command line. +[`spacy train`](/api/cli#train) command on the command line. You can pass in the +following data and information: 1. The **training and evaluation data** in spaCy's [binary `.spacy` format](/api/data-formats#binary-training) created using @@ -68,38 +70,22 @@ workflows, from data preprocessing to training and packaging your model. - +## Quickstart {#quickstart} -When you train a model using the [`spacy train`](/api/cli#train) command, you'll -see a table showing metrics after each pass over the data. Here's what those -metrics means: +> #### Instructions +> +> 1. Select your requirements and settings. The quickstart widget will +> auto-generate a recommended starter config for you. +> 2. Use the buttons at the bottom to save the result to your clipboard or a +> file `config.cfg`. +> 3. TOOD: recommended approach for filling config +> 4. Run [`spacy train`](/api/cli#train) with your config and data. - +import QuickstartTraining from 'widgets/quickstart-training.js' -| Name | Description | -| ---------- | ------------------------------------------------------------------------------------------------- | -| `Dep Loss` | Training loss for dependency parser. Should decrease, but usually not to 0. | -| `NER Loss` | Training loss for named entity recognizer. Should decrease, but usually not to 0. | -| `UAS` | Unlabeled attachment score for parser. The percentage of unlabeled correct arcs. Should increase. | -| `NER P.` | NER precision on development data. Should increase. | -| `NER R.` | NER recall on development data. Should increase. | -| `NER F.` | NER F-score on development data. Should increase. | -| `Tag %` | Fine-grained part-of-speech tag accuracy on development data. Should increase. | -| `Token %` | Tokenization accuracy on development data. | -| `CPU WPS` | Prediction speed on CPU in words per second, if available. Should stay stable. | -| `GPU WPS` | Prediction speed on GPU in words per second, if available. Should stay stable. | + -Note that if the development data has raw text, some of the gold-standard -entities might not align to the predicted tokenization. These tokenization -errors are **excluded from the NER evaluation**. If your tokenization makes it -impossible for the model to predict 50% of your entities, your NER F-score might -still look good. - - - ---- - -### Training config files {#config} +## Training config {#config} > #### Migration from spaCy v2.x > @@ -237,7 +223,70 @@ compound = 1.001 - +### Metrics, training output and weighted scores {#metrics} + +When you train a model using the [`spacy train`](/api/cli#train) command, you'll +see a table showing the metrics after each pass over the data. The available +metrics **depend on the pipeline components**. Pipeline components also define +which scores are shown and how they should be **weighted in the final score** +that decides about the best model. + +The `training.score_weights` setting in your `config.cfg` lets you customize the +scores shown in the table and how they should be weighted. In this example, the +labeled dependency accuracy and NER F-score count towards the final score with +40% each and the tagging accuracy makes up the remaining 20%. The tokenization +accuracy and speed are both shown in the table, but not counted towards the +score. + +> #### Why do I need score weights? +> +> At the end of your training process, you typically want to select the **best +> model** – but what "best" means depends on the available components and your +> specific use case. For instance, you may prefer a model with higher NER and +> lower POS tagging accuracy over a model with lower NER and higher POS +> accuracy. You can express this preference in the score weights, e.g. by +> assigning `ents_f` (NER F-score) a higher weight. + +```ini +[training.score_weights] +dep_las = 0.4 +ents_f = 0.4 +tag_acc = 0.2 +token_acc = 0.0 +speed = 0.0 +``` + +The `score_weights` don't _have to_ sum to `1.0` – but it's recommended. When +you generate a config for a given pipeline, the score weights are generated by +combining and normalizing the default score weights of the pipeline components. +The default score weights are defined by each pipeline component via the +`default_score_weights` setting on the +[`@Language.component`](/api/language#component) or +[`@Language.factory`](/api/language#factory). By default, all pipeline +components are weighted equally. + + + + + +| Name | Description | +| -------------------------- | ----------------------------------------------------------------------------------------------------------------------- | +| **Loss** | The training loss representing the amount of work left for the optimizer. Should decrease, but usually not to `0`. | +| **Precision** (P) | Should increase. | +| **Recall** (R) | Should increase. | +| **F-Score** (F) | The weighted average of precision and recall. Should increase. | +| **UAS** / **LAS** | Unlabeled and labeled attachment score for the dependency parser, i.e. the percentage of correct arcs. Should increase. | +| **Words per second** (WPS) | Prediction speed in words per second. Should stay stable. | + + + +Note that if the development data has raw text, some of the gold-standard +entities might not align to the predicted tokenization. These tokenization +errors are **excluded from the NER evaluation**. If your tokenization makes it +impossible for the model to predict 50% of your entities, your NER F-score might +still look good. + + ## Transfer learning {#transfer-learning} diff --git a/website/docs/usage/transformers.md b/website/docs/usage/transformers.md index bab1b82d3..81bd45f58 100644 --- a/website/docs/usage/transformers.md +++ b/website/docs/usage/transformers.md @@ -88,7 +88,8 @@ The recommended workflow for training is to use spaCy's [`spacy train`](/api/cli#train) command. The training config defines all component settings and hyperparameters in one place and lets you describe a tree of objects by referring to creation functions, including functions you register -yourself. +yourself. For details on how to get started with training your own model, check +out the [training quickstart](/usage/training#quickstart). diff --git a/website/src/components/copy.js b/website/src/components/copy.js index 4392273e2..f8013c5f1 100644 --- a/website/src/components/copy.js +++ b/website/src/components/copy.js @@ -3,21 +3,23 @@ import React, { useState, useRef } from 'react' import Icon from './icon' import classes from '../styles/copy.module.sass' +export function copyToClipboard(ref, callback) { + const isClient = typeof window !== 'undefined' + if (ref.current && isClient) { + ref.current.select() + document.execCommand('copy') + callback(true) + ref.current.blur() + setTimeout(() => callback(false), 1000) + } +} + const CopyInput = ({ text, prefix }) => { const isClient = typeof window !== 'undefined' const supportsCopy = isClient && document.queryCommandSupported('copy') const textareaRef = useRef() const [copySuccess, setCopySuccess] = useState(false) - - function copyToClipboard() { - if (textareaRef.current && isClient) { - textareaRef.current.select() - document.execCommand('copy') - setCopySuccess(true) - textareaRef.current.blur() - setTimeout(() => setCopySuccess(false), 1000) - } - } + const onClick = () => copyToClipboard(textareaRef, setCopySuccess) function selectText() { if (textareaRef.current && isClient) { @@ -37,7 +39,7 @@ const CopyInput = ({ text, prefix }) => { onClick={selectText} /> {supportsCopy && ( - )} diff --git a/website/src/components/icon.js b/website/src/components/icon.js index 8c917d13d..00b237795 100644 --- a/website/src/components/icon.js +++ b/website/src/components/icon.js @@ -22,6 +22,7 @@ import { ReactComponent as SearchIcon } from '../images/icons/search.svg' import { ReactComponent as MoonIcon } from '../images/icons/moon.svg' import { ReactComponent as ClipboardIcon } from '../images/icons/clipboard.svg' import { ReactComponent as NetworkIcon } from '../images/icons/network.svg' +import { ReactComponent as DownloadIcon } from '../images/icons/download.svg' import classes from '../styles/icon.module.sass' @@ -46,7 +47,8 @@ const icons = { search: SearchIcon, moon: MoonIcon, clipboard: ClipboardIcon, - network: NetworkIcon + network: NetworkIcon, + download: DownloadIcon, } const Icon = ({ name, width, height, inline, variant, className }) => { diff --git a/website/src/components/quickstart.js b/website/src/components/quickstart.js index fe73658c7..f1d3616a5 100644 --- a/website/src/components/quickstart.js +++ b/website/src/components/quickstart.js @@ -1,4 +1,4 @@ -import React, { Fragment, useState, useEffect } from 'react' +import React, { Fragment, useState, useEffect, useRef } from 'react' import PropTypes from 'prop-types' import classNames from 'classnames' import { window } from 'browser-monads' @@ -6,6 +6,7 @@ import { window } from 'browser-monads' import Section from './section' import Icon from './icon' import { H2 } from './typography' +import { copyToClipboard } from './copy' import classes from '../styles/quickstart.module.sass' function getNewChecked(optionId, checkedForId, multiple) { @@ -14,10 +15,41 @@ function getNewChecked(optionId, checkedForId, multiple) { return [...checkedForId, optionId] } -const Quickstart = ({ data, title, description, id, children }) => { +function getRawContent(ref) { + if (ref.current && ref.current.childNodes) { + // Select all currently visible nodes (spans and text nodes) + const result = [...ref.current.childNodes].filter(el => el.offsetParent !== null) + return result.map(el => el.textContent).join('\n') + } + return '' +} + +const Quickstart = ({ + data, + title, + description, + copy, + download, + id, + setters = {}, + hidePrompts, + children, +}) => { + const contentRef = useRef() + const copyAreaRef = useRef() + const isClient = typeof window !== 'undefined' + const supportsCopy = isClient && document.queryCommandSupported('copy') + const showCopy = supportsCopy && copy const [styles, setStyles] = useState({}) const [checked, setChecked] = useState({}) const [initialized, setInitialized] = useState(false) + const [copySuccess, setCopySuccess] = useState(false) + const [otherState, setOtherState] = useState({}) + const setOther = (id, value) => setOtherState({ ...otherState, [id]: value }) + const onClickCopy = () => { + copyAreaRef.current.value = getRawContent(contentRef) + copyToClipboard(copyAreaRef, setCopySuccess) + } const getCss = (id, checkedOptions) => { const checkedForId = checkedOptions[id] || [] @@ -32,7 +64,7 @@ const Quickstart = ({ data, title, description, id, children }) => { if (!initialized) { const initialChecked = Object.assign( {}, - ...data.map(({ id, options }) => ({ + ...data.map(({ id, options = [] }) => ({ [id]: options.filter(option => option.checked).map(({ id }) => id), })) ) @@ -48,7 +80,7 @@ const Quickstart = ({ data, title, description, id, children }) => { return !data.length ? null : (
-
+
{title && (

{title} @@ -57,82 +89,154 @@ const Quickstart = ({ data, title, description, id, children }) => { {description &&

{description}

} - {data.map(({ id, title, options = [], multiple, help }) => ( -
- -
- {title} - {help && ( - - {' '} - - - )} -
-
- {options.map(option => { - const optionType = multiple ? 'checkbox' : 'radio' - const checkedForId = checked[id] || [] - return ( - - { - const newChecked = { - ...checked, - [id]: getNewChecked( - option.id, - checkedForId, - multiple - ), + {data.map( + ({ + id, + title, + options = [], + dropdown = [], + defaultValue, + multiple, + other, + help, + }) => { + // Optional function that's called with the value + const setterFunc = setters[id] || (() => {}) + return ( +
+ +
+ {title} + {help && ( + + {' '} + + + )} +
+
+ {!!dropdown.length && ( + + )} + {other && otherState[id] && ( + setterFunc(target.value)} + /> + )} + {options.map(option => { + const optionType = multiple ? 'checkbox' : 'radio' + const checkedForId = checked[id] || [] + return ( + + { + const newChecked = { + ...checked, + [id]: getNewChecked( + option.id, + checkedForId, + multiple + ), + } + setChecked(newChecked) + setStyles({ + ...styles, + [id]: getCss(id, newChecked), + }) + setterFunc(newChecked[id]) + }} + type={optionType} + className={classNames( + classes.input, + classes[optionType] + )} + name={id} + id={`quickstart-${option.id}`} + value={option.id} + checked={checkedForId.includes(option.id)} + /> + - - ) - })} -
-
- ))} + {option.title} + {option.meta && ( + + {option.meta} + + )} + {option.help && ( + + {' '} + + + )} + +
+ ) + })} +
+
+ ) + } + )}
-                    
+                    
                         {children}
                     
+
+                    
+                        {showCopy && (
+                            
+                        )}
+                        {download && (
+                            
+                                
+                            
+                        )}
+                    
                 
+ {showCopy &&