From 91ccacea12a46c62ccb5e7f6de891a37cb71e184 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 6 Feb 2022 16:30:30 +0100 Subject: [PATCH 01/23] Auto-format code with black (#10209) * Auto-format code with black * add black requirement to dev dependencies and pin to 22.x * ignore black dependency for comparison with setup.cfg Co-authored-by: explosion-bot Co-authored-by: svlandeg --- requirements.txt | 1 + spacy/language.py | 2 +- spacy/ml/models/multi_task.py | 2 +- spacy/pipeline/spancat.py | 2 +- spacy/pipeline/textcat.py | 4 ++-- spacy/tests/package/test_requirements.py | 1 + 6 files changed, 7 insertions(+), 5 deletions(-) diff --git a/requirements.txt b/requirements.txt index 8d7372cfe..ca4099be5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -35,3 +35,4 @@ mypy==0.910 types-dataclasses>=0.1.3; python_version < "3.7" types-mock>=0.1.1 types-requests +black>=22.0,<23.0 diff --git a/spacy/language.py b/spacy/language.py index fdce34ac4..e8fd2720c 100644 --- a/spacy/language.py +++ b/spacy/language.py @@ -131,7 +131,7 @@ class Language: self, vocab: Union[Vocab, bool] = True, *, - max_length: int = 10 ** 6, + max_length: int = 10**6, meta: Dict[str, Any] = {}, create_tokenizer: Optional[Callable[["Language"], Callable[[str], Doc]]] = None, batch_size: int = 1000, diff --git a/spacy/ml/models/multi_task.py b/spacy/ml/models/multi_task.py index 9e1face63..a7d67c6dd 100644 --- a/spacy/ml/models/multi_task.py +++ b/spacy/ml/models/multi_task.py @@ -85,7 +85,7 @@ def get_characters_loss(ops, docs, prediction, nr_char): target = ops.asarray(to_categorical(target_ids, n_classes=256), dtype="f") target = target.reshape((-1, 256 * nr_char)) diff = prediction - target - loss = (diff ** 2).sum() + loss = (diff**2).sum() d_target = diff / float(prediction.shape[0]) return loss, d_target diff --git a/spacy/pipeline/spancat.py b/spacy/pipeline/spancat.py index 32c1275a6..5d0d8f17e 100644 --- a/spacy/pipeline/spancat.py +++ b/spacy/pipeline/spancat.py @@ -377,7 +377,7 @@ class SpanCategorizer(TrainablePipe): # If the prediction is 0.9 and it's false, the gradient will be # 0.9 (0.9 - 0.0) d_scores = scores - target - loss = float((d_scores ** 2).sum()) + loss = float((d_scores**2).sum()) return loss, d_scores def initialize( diff --git a/spacy/pipeline/textcat.py b/spacy/pipeline/textcat.py index 30a65ec52..7f5510933 100644 --- a/spacy/pipeline/textcat.py +++ b/spacy/pipeline/textcat.py @@ -281,7 +281,7 @@ class TextCategorizer(TrainablePipe): bp_scores(gradient) if sgd is not None: self.finish_update(sgd) - losses[self.name] += (gradient ** 2).sum() + losses[self.name] += (gradient**2).sum() return losses def _examples_to_truth( @@ -315,7 +315,7 @@ class TextCategorizer(TrainablePipe): not_missing = self.model.ops.asarray(not_missing) # type: ignore d_scores = (scores - truths) / scores.shape[0] d_scores *= not_missing - mean_square_error = (d_scores ** 2).sum(axis=1).mean() + mean_square_error = (d_scores**2).sum(axis=1).mean() return float(mean_square_error), d_scores def add_label(self, label: str) -> int: diff --git a/spacy/tests/package/test_requirements.py b/spacy/tests/package/test_requirements.py index 75908df59..e20227455 100644 --- a/spacy/tests/package/test_requirements.py +++ b/spacy/tests/package/test_requirements.py @@ -12,6 +12,7 @@ def test_build_dependencies(): "flake8", "hypothesis", "pre-commit", + "black", "mypy", "types-dataclasses", "types-mock", From 63e1e4e8f637085b6dfa42d2918cf30e149d7474 Mon Sep 17 00:00:00 2001 From: Adriane Boyd Date: Mon, 7 Feb 2022 08:53:30 +0100 Subject: [PATCH 02/23] Fix debug data check for ents that cross sents (#10188) * Fix debug data check for ents that cross sents * Use aligned sent starts to have the same indices for the NER and sent start annotation * Add a temporary, insufficient hack for the case where a sentence-initial reference token is split into multiple tokens in the predicted doc, since `Example.get_aligned("SENT_START")` currently aligns `True` to all the split tokens. * Improve test example * Use Example.get_aligned_sent_starts * Add test for crossing entity --- spacy/cli/debug_data.py | 3 ++- spacy/tests/test_cli.py | 18 +++++++++++++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/spacy/cli/debug_data.py b/spacy/cli/debug_data.py index ab7c20d48..4be749204 100644 --- a/spacy/cli/debug_data.py +++ b/spacy/cli/debug_data.py @@ -603,6 +603,7 @@ def _compile_gold( if nlp.vocab.strings[word] not in nlp.vocab.vectors: data["words_missing_vectors"].update([word]) if "ner" in factory_names: + sent_starts = eg.get_aligned_sent_starts() for i, label in enumerate(eg.get_aligned_ner()): if label is None: continue @@ -612,7 +613,7 @@ def _compile_gold( if label.startswith(("B-", "U-")): combined_label = label.split("-")[1] data["ner"][combined_label] += 1 - if gold[i].is_sent_start and label.startswith(("I-", "L-")): + if sent_starts[i] == True and label.startswith(("I-", "L-")): data["boundary_cross_ents"] += 1 elif label == "-": data["ner"]["-"] += 1 diff --git a/spacy/tests/test_cli.py b/spacy/tests/test_cli.py index 253469909..9d5bdfab2 100644 --- a/spacy/tests/test_cli.py +++ b/spacy/tests/test_cli.py @@ -12,7 +12,7 @@ from spacy.cli._util import is_subpath_of, load_project_config from spacy.cli._util import parse_config_overrides, string_to_list from spacy.cli._util import substitute_project_variables from spacy.cli._util import validate_project_commands -from spacy.cli.debug_data import _get_labels_from_model +from spacy.cli.debug_data import _compile_gold, _get_labels_from_model from spacy.cli.debug_data import _get_labels_from_spancat from spacy.cli.download import get_compatibility, get_version from spacy.cli.init_config import RECOMMENDATIONS, init_config, fill_config @@ -22,6 +22,7 @@ from spacy.lang.en import English from spacy.lang.nl import Dutch from spacy.language import Language from spacy.schemas import ProjectConfigSchema, RecommendationSchema, validate +from spacy.tokens import Doc from spacy.training import Example, docs_to_json, offsets_to_biluo_tags from spacy.training.converters import conll_ner_to_docs, conllu_to_docs from spacy.training.converters import iob_to_docs @@ -692,3 +693,18 @@ def test_get_labels_from_model(factory_name, pipe_name): assert _get_labels_from_spancat(nlp)[pipe.key] == set(labels) else: assert _get_labels_from_model(nlp, factory_name) == set(labels) + + +def test_debug_data_compile_gold(): + nlp = English() + pred = Doc(nlp.vocab, words=["Token", ".", "New", "York", "City"]) + ref = Doc(nlp.vocab, words=["Token", ".", "New York City"], sent_starts=[True, False, True], ents=["O", "O", "B-ENT"]) + eg = Example(pred, ref) + data = _compile_gold([eg], ["ner"], nlp, True) + assert data["boundary_cross_ents"] == 0 + + pred = Doc(nlp.vocab, words=["Token", ".", "New", "York", "City"]) + ref = Doc(nlp.vocab, words=["Token", ".", "New York City"], sent_starts=[True, False, True], ents=["O", "B-ENT", "I-ENT"]) + eg = Example(pred, ref) + data = _compile_gold([eg], ["ner"], nlp, True) + assert data["boundary_cross_ents"] == 1 From 72fece712f2706c3338365fa6eed179b6b7f8848 Mon Sep 17 00:00:00 2001 From: Lj Miranda <12949683+ljvmiranda921@users.noreply.github.com> Date: Mon, 7 Feb 2022 21:55:53 +0800 Subject: [PATCH 03/23] Add shuffle parameter to Corpus API docs (#10220) * Add shuffle parameter to Corpus API docs * Update website/docs/api/corpus.md Co-authored-by: Adriane Boyd Co-authored-by: Adriane Boyd --- website/docs/api/corpus.md | 1 + 1 file changed, 1 insertion(+) diff --git a/website/docs/api/corpus.md b/website/docs/api/corpus.md index 986c6f458..35afc8fea 100644 --- a/website/docs/api/corpus.md +++ b/website/docs/api/corpus.md @@ -79,6 +79,7 @@ train/test skew. | `max_length` | Maximum document length. Longer documents will be split into sentences, if sentence boundaries are available. Defaults to `0` for no limit. ~~int~~ | | `limit` | Limit corpus to a subset of examples, e.g. for debugging. Defaults to `0` for no limit. ~~int~~ | | `augmenter` | Optional data augmentation callback. ~~Callable[[Language, Example], Iterable[Example]]~~ | +| `shuffle` | Whether to shuffle the examples. Defaults to `False`. ~~bool~~ | ## Corpus.\_\_call\_\_ {#call tag="method"} From 42072f4468b353d785214a82b67ace38b728f9b5 Mon Sep 17 00:00:00 2001 From: Lj Miranda <12949683+ljvmiranda921@users.noreply.github.com> Date: Mon, 7 Feb 2022 22:03:36 +0800 Subject: [PATCH 04/23] Add spancat pipeline in spacy debug data (#10070) * Setup debug data for spancat * Add check for missing labels * Add low-level data warning error * Improve logic when compiling the gold train data * Implement check for negative examples * Remove breakpoint * Remove ws_ents and missing entity checks * Fix mypy errors * Make variable name spans_key consistent * Rename pipeline -> component for consistency * Account for missing labels per spans_key * Cleanup variable names for consistency * Improve brevity of conditional statements * Remove unused variables * Include spans_key as an argument for _get_examples * Add a conditional check for spans_key * Update spancat debug data based on new API - Instead of using _get_labels_from_model(), I'm now using _get_labels_from_spancat() (cf. https://github.com/explosion/spaCy/pull10079) - The way information is displayed was also changed (text -> table) * Rename model_labels to ensure mypy works * Update wording on warning messages Use "span type" instead of "entity type" in wording the warning messages. This is because Spans aren't necessarily entities. * Update component type into a Literal This is to make it clear that the component parameter should only accept either 'spancat' or 'ner'. * Update checks to include actual model span_keys Instead of looking at everything in the data, we only check those span_keys from the actual spancat component. Instead of doing the filter inside the for-loop, I just made another dictionary, data_labels_in_component to hold this value. * Update spacy/cli/debug_data.py * Show label counts only when verbose is True Co-authored-by: Adriane Boyd Co-authored-by: Adriane Boyd --- spacy/cli/debug_data.py | 102 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 95 insertions(+), 7 deletions(-) diff --git a/spacy/cli/debug_data.py b/spacy/cli/debug_data.py index 4be749204..a63795148 100644 --- a/spacy/cli/debug_data.py +++ b/spacy/cli/debug_data.py @@ -193,6 +193,70 @@ def debug_data( else: msg.info("No word vectors present in the package") + if "spancat" in factory_names: + model_labels_spancat = _get_labels_from_spancat(nlp) + has_low_data_warning = False + has_no_neg_warning = False + + msg.divider("Span Categorization") + msg.table(model_labels_spancat, header=["Spans Key", "Labels"], divider=True) + + msg.text("Label counts in train data: ", show=verbose) + for spans_key, data_labels in gold_train_data["spancat"].items(): + msg.text( + f"Key: {spans_key}, {_format_labels(data_labels.items(), counts=True)}", + show=verbose, + ) + # Data checks: only take the spans keys in the actual spancat components + data_labels_in_component = { + spans_key: gold_train_data["spancat"][spans_key] + for spans_key in model_labels_spancat.keys() + } + for spans_key, data_labels in data_labels_in_component.items(): + for label, count in data_labels.items(): + # Check for missing labels + spans_key_in_model = spans_key in model_labels_spancat.keys() + if (spans_key_in_model) and ( + label not in model_labels_spancat[spans_key] + ): + msg.warn( + f"Label '{label}' is not present in the model labels of key '{spans_key}'. " + "Performance may degrade after training." + ) + # Check for low number of examples per label + if count <= NEW_LABEL_THRESHOLD: + msg.warn( + f"Low number of examples for label '{label}' in key '{spans_key}' ({count})" + ) + has_low_data_warning = True + # Check for negative examples + with msg.loading("Analyzing label distribution..."): + neg_docs = _get_examples_without_label( + train_dataset, label, "spancat", spans_key + ) + if neg_docs == 0: + msg.warn(f"No examples for texts WITHOUT new label '{label}'") + has_no_neg_warning = True + + if has_low_data_warning: + msg.text( + f"To train a new span type, your data should include at " + f"least {NEW_LABEL_THRESHOLD} instances of the new label", + show=verbose, + ) + else: + msg.good("Good amount of examples for all labels") + + if has_no_neg_warning: + msg.text( + "Training data should always include examples of spans " + "in context, as well as examples without a given span " + "type.", + show=verbose, + ) + else: + msg.good("Examples without ocurrences available for all labels") + if "ner" in factory_names: # Get all unique NER labels present in the data labels = set( @@ -238,7 +302,7 @@ def debug_data( has_low_data_warning = True with msg.loading("Analyzing label distribution..."): - neg_docs = _get_examples_without_label(train_dataset, label) + neg_docs = _get_examples_without_label(train_dataset, label, "ner") if neg_docs == 0: msg.warn(f"No examples for texts WITHOUT new label '{label}'") has_no_neg_warning = True @@ -573,6 +637,7 @@ def _compile_gold( "deps": Counter(), "words": Counter(), "roots": Counter(), + "spancat": dict(), "ws_ents": 0, "boundary_cross_ents": 0, "n_words": 0, @@ -617,6 +682,15 @@ def _compile_gold( data["boundary_cross_ents"] += 1 elif label == "-": data["ner"]["-"] += 1 + if "spancat" in factory_names: + for span_key in list(eg.reference.spans.keys()): + if span_key not in data["spancat"]: + data["spancat"][span_key] = Counter() + for i, span in enumerate(eg.reference.spans[span_key]): + if span.label_ is None: + continue + else: + data["spancat"][span_key][span.label_] += 1 if "textcat" in factory_names or "textcat_multilabel" in factory_names: data["cats"].update(gold.cats) if any(val not in (0, 1) for val in gold.cats.values()): @@ -687,14 +761,28 @@ def _format_labels( return ", ".join([f"'{l}'" for l in cast(Iterable[str], labels)]) -def _get_examples_without_label(data: Sequence[Example], label: str) -> int: +def _get_examples_without_label( + data: Sequence[Example], + label: str, + component: Literal["ner", "spancat"] = "ner", + spans_key: Optional[str] = "sc", +) -> int: count = 0 for eg in data: - labels = [ - label.split("-")[1] - for label in eg.get_aligned_ner() - if label not in ("O", "-", None) - ] + if component == "ner": + labels = [ + label.split("-")[1] + for label in eg.get_aligned_ner() + if label not in ("O", "-", None) + ] + + if component == "spancat": + labels = ( + [span.label_ for span in eg.reference.spans[spans_key]] + if spans_key in eg.reference.spans + else [] + ) + if label not in labels: count += 1 return count From e4625d2fc3c0580dcdd62d9b817554c31fe8b75e Mon Sep 17 00:00:00 2001 From: Kenneth Enevoldsen Date: Tue, 8 Feb 2022 08:32:11 +0100 Subject: [PATCH 05/23] Added Augmenty to universe (#10229) * Added Augmenty to universe * Update website/meta/universe.json Co-authored-by: Adriane Boyd * Update website/meta/universe.json Co-authored-by: Sofie Van Landeghem Co-authored-by: Adriane Boyd Co-authored-by: Sofie Van Landeghem --- website/meta/universe.json | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/website/meta/universe.json b/website/meta/universe.json index b1a61598e..1a67de67b 100644 --- a/website/meta/universe.json +++ b/website/meta/universe.json @@ -953,6 +953,37 @@ "category": ["pipeline"], "tags": ["lemmatizer", "danish"] }, + { + "id": "augmenty", + "title": "Augmenty", + "slogan": "The cherry on top of your NLP pipeline", + "description": "Augmenty is an augmentation library based on spaCy for augmenting texts. Augmenty differs from other augmentation libraries in that it corrects (as far as possible) the token, sentence and document labels under the augmentation.", + "github": "kennethenevoldsen/augmenty", + "pip": "augmenty", + "code_example": [ + "import spacy", + "import augmenty", + "", + "nlp = spacy.load('en_core_web_md')", + "", + "docs = nlp.pipe(['Augmenty is a great tool for text augmentation'])", + "", + "ent_dict = {'ORG': [['spaCy'], ['spaCy', 'Universe']]}", + "entity_augmenter = augmenty.load('ents_replace.v1',", + " ent_dict = ent_dict, level=1)", + "", + "for doc in augmenty.docs(docs, augmenter=entity_augmenter, nlp=nlp):", + " print(doc)" + ], + "thumb": "https://github.com/KennethEnevoldsen/augmenty/blob/master/img/icon.png?raw=true", + "author": "Kenneth Enevoldsen", + "author_links": { + "github": "kennethenevoldsen", + "website": "https://www.kennethenevoldsen.com" + }, + "category": ["training", "research"], + "tags": ["training", "research", "augmentation"] + }, { "id": "dacy", "title": "DaCy", From 836f689cc7b2729d071174629a58fc09f3e12cec Mon Sep 17 00:00:00 2001 From: Peter Baumgartner <5107405+pmbaumgartner@users.noreply.github.com> Date: Tue, 8 Feb 2022 02:35:09 -0500 Subject: [PATCH 06/23] YAML multiline tip for project.yml files (#10187) * MultiHashEmbed vector docs correction * add in multi-line tip * convert to sidebar tip --- website/docs/usage/projects.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/website/docs/usage/projects.md b/website/docs/usage/projects.md index e0e787a1d..57d226913 100644 --- a/website/docs/usage/projects.md +++ b/website/docs/usage/projects.md @@ -213,6 +213,12 @@ format, train a pipeline, evaluate it and export metrics, package it and spin up a quick web demo. It looks pretty similar to a config file used to define CI pipelines. +> #### Tip: Multi-line YAML syntax for long values +> +> YAML has [multi-line syntax](https://yaml-multiline.info/) that can be +> helpful for readability with longer values such as project descriptions or +> commands that take several arguments. + ```yaml %%GITHUB_PROJECTS/pipelines/tagger_parser_ud/project.yml ``` From deb143fa709461ea6b8fddd17006908f7bea7f55 Mon Sep 17 00:00:00 2001 From: Sofie Van Landeghem Date: Tue, 8 Feb 2022 08:35:37 +0100 Subject: [PATCH 07/23] Token sent attributes more consistent (#10164) * remove duplicate line * add sent start/end token attributes to the docs * let has_annotation work with IS_SENT_END * elif instead of if * add has_annotation test for sent attributes * fix typo * remove duplicate is_sent_start entry in docs --- spacy/glossary.py | 1 - spacy/tests/doc/test_doc_api.py | 22 ++++++++++++++++++++++ spacy/tokens/doc.pyx | 2 ++ spacy/tokens/token.pyx | 2 -- website/docs/api/doc.md | 2 +- website/docs/api/token.md | 19 ++----------------- 6 files changed, 27 insertions(+), 21 deletions(-) diff --git a/spacy/glossary.py b/spacy/glossary.py index e45704fc5..57254330f 100644 --- a/spacy/glossary.py +++ b/spacy/glossary.py @@ -310,7 +310,6 @@ GLOSSARY = { "re": "repeated element", "rs": "reported speech", "sb": "subject", - "sb": "subject", "sbp": "passivized subject (PP)", "sp": "subject or predicate", "svp": "separable verb prefix", diff --git a/spacy/tests/doc/test_doc_api.py b/spacy/tests/doc/test_doc_api.py index 10700b787..858c7cbb6 100644 --- a/spacy/tests/doc/test_doc_api.py +++ b/spacy/tests/doc/test_doc_api.py @@ -684,6 +684,7 @@ def test_has_annotation(en_vocab): attrs = ("TAG", "POS", "MORPH", "LEMMA", "DEP", "HEAD", "ENT_IOB", "ENT_TYPE") for attr in attrs: assert not doc.has_annotation(attr) + assert not doc.has_annotation(attr, require_complete=True) doc[0].tag_ = "A" doc[0].pos_ = "X" @@ -709,6 +710,27 @@ def test_has_annotation(en_vocab): assert doc.has_annotation(attr, require_complete=True) +def test_has_annotation_sents(en_vocab): + doc = Doc(en_vocab, words=["Hello", "beautiful", "world"]) + attrs = ("SENT_START", "IS_SENT_START", "IS_SENT_END") + for attr in attrs: + assert not doc.has_annotation(attr) + assert not doc.has_annotation(attr, require_complete=True) + + # The first token (index 0) is always assumed to be a sentence start, + # and ignored by the check in doc.has_annotation + + doc[1].is_sent_start = False + for attr in attrs: + assert doc.has_annotation(attr) + assert not doc.has_annotation(attr, require_complete=True) + + doc[2].is_sent_start = False + for attr in attrs: + assert doc.has_annotation(attr) + assert doc.has_annotation(attr, require_complete=True) + + def test_is_flags_deprecated(en_tokenizer): doc = en_tokenizer("test") with pytest.deprecated_call(): diff --git a/spacy/tokens/doc.pyx b/spacy/tokens/doc.pyx index 5a0db115d..d33764ac9 100644 --- a/spacy/tokens/doc.pyx +++ b/spacy/tokens/doc.pyx @@ -420,6 +420,8 @@ cdef class Doc: cdef int range_start = 0 if attr == "IS_SENT_START" or attr == self.vocab.strings["IS_SENT_START"]: attr = SENT_START + elif attr == "IS_SENT_END" or attr == self.vocab.strings["IS_SENT_END"]: + attr = SENT_START attr = intify_attr(attr) # adjust attributes if attr == HEAD: diff --git a/spacy/tokens/token.pyx b/spacy/tokens/token.pyx index b515ab67b..d14930348 100644 --- a/spacy/tokens/token.pyx +++ b/spacy/tokens/token.pyx @@ -487,8 +487,6 @@ cdef class Token: RETURNS (bool / None): Whether the token starts a sentence. None if unknown. - - DOCS: https://spacy.io/api/token#is_sent_start """ def __get__(self): if self.c.sent_start == 0: diff --git a/website/docs/api/doc.md b/website/docs/api/doc.md index 9836b8c21..c21328caf 100644 --- a/website/docs/api/doc.md +++ b/website/docs/api/doc.md @@ -304,7 +304,7 @@ ancestor is found, e.g. if span excludes a necessary ancestor. ## Doc.has_annotation {#has_annotation tag="method"} -Check whether the doc contains annotation on a token attribute. +Check whether the doc contains annotation on a [`Token` attribute](/api/token#attributes). diff --git a/website/docs/api/token.md b/website/docs/api/token.md index 44a2ea9e8..3c3d12d54 100644 --- a/website/docs/api/token.md +++ b/website/docs/api/token.md @@ -349,23 +349,6 @@ A sequence containing the token and all the token's syntactic descendants. | ---------- | ------------------------------------------------------------------------------------ | | **YIELDS** | A descendant token such that `self.is_ancestor(token)` or `token == self`. ~~Token~~ | -## Token.is_sent_start {#is_sent_start tag="property" new="2"} - -A boolean value indicating whether the token starts a sentence. `None` if -unknown. Defaults to `True` for the first token in the `Doc`. - -> #### Example -> -> ```python -> doc = nlp("Give it back! He pleaded.") -> assert doc[4].is_sent_start -> assert not doc[5].is_sent_start -> ``` - -| Name | Description | -| ----------- | ------------------------------------------------------- | -| **RETURNS** | Whether the token starts a sentence. ~~Optional[bool]~~ | - ## Token.has_vector {#has_vector tag="property" model="vectors"} A boolean value indicating whether a word vector is associated with the token. @@ -465,6 +448,8 @@ The L2 norm of the token's vector representation. | `is_punct` | Is the token punctuation? ~~bool~~ | | `is_left_punct` | Is the token a left punctuation mark, e.g. `"("` ? ~~bool~~ | | `is_right_punct` | Is the token a right punctuation mark, e.g. `")"` ? ~~bool~~ | +| `is_sent_start` | Does the token start a sentence? ~~bool~~ or `None` if unknown. Defaults to `True` for the first token in the `Doc`. | +| `is_sent_end` | Does the token end a sentence? ~~bool~~ or `None` if unknown. | | `is_space` | Does the token consist of whitespace characters? Equivalent to `token.text.isspace()`. ~~bool~~ | | `is_bracket` | Is the token a bracket? ~~bool~~ | | `is_quote` | Is the token a quotation mark? ~~bool~~ | From f939da0bfa7f53fdb8ad1a200a8702e184443694 Mon Sep 17 00:00:00 2001 From: Ryn Daniels <397565+ryndaniels@users.noreply.github.com> Date: Tue, 8 Feb 2022 11:05:35 +0200 Subject: [PATCH 08/23] Add github actions for slow and gpu tests (#10225) * Add github actions for slow and gpu tests * change weekly GPU tests to also run slow tests, and change the time * only run the tests if there were commits in the past day --- .github/workflows/gputests.yml | 19 +++++++++++++++++++ .github/workflows/slowtests.yml | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) create mode 100644 .github/workflows/gputests.yml create mode 100644 .github/workflows/slowtests.yml diff --git a/.github/workflows/gputests.yml b/.github/workflows/gputests.yml new file mode 100644 index 000000000..7c062fe4c --- /dev/null +++ b/.github/workflows/gputests.yml @@ -0,0 +1,19 @@ +on: + schedule: + - cron: '0 1 * * MON' + +jobs: + weekly-gputests: + strategy: + matrix: + branch: [master, develop, v4] + runs-on: ubuntu-latest + steps: + - name: Trigger buildkite build + uses: buildkite/trigger-pipeline-action@v1.2.0 + env: + PIPELINE: explosion-ai/spacy-slow-gpu-tests + BRANCH: ${{ matrix.branch }} + MESSAGE: ":github: Weekly GPU + slow tests - triggered from a GitHub Action" + secrets: + BUILDKITE_API_ACCESS_TOKEN: ${{ secrets.BUILDKITE_SECRET }} diff --git a/.github/workflows/slowtests.yml b/.github/workflows/slowtests.yml new file mode 100644 index 000000000..4d4441679 --- /dev/null +++ b/.github/workflows/slowtests.yml @@ -0,0 +1,32 @@ +on: + schedule: + - cron: '0 0 * * *' + +jobs: + daily-slowtests: + strategy: + matrix: + branch: [master, develop, v4] + runs-on: ubuntu-latest + steps: + - name: Get commits from past 24 hours + id: check_commits + run: | + today=$(date '+%Y-%m-%d %H:%M:%S') + yesterday=$(date -v-1d '+%Y-%m-%d %H:%M:%S') + if git log --after=$yesterday --before=$today | grep commit ; then + echo "::set-output name=run_tests::true" + else + echo "::set-output name=run_tests::false" + fi + + - name: Trigger buildkite build + needs: check_commits + if: needs.check_commits.outputs.run_tests == 'true' + uses: buildkite/trigger-pipeline-action@v1.2.0 + env: + PIPELINE: explosion-ai/spacy-slow-tests + BRANCH: ${{ matrix.branch }} + MESSAGE: ":github: Daily slow tests - triggered from a GitHub Action" + secrets: + BUILDKITE_API_ACCESS_TOKEN: ${{ secrets.BUILDKITE_SECRET }} From a9ee5bff98b40126dce1625a5b48f86e4ffabc77 Mon Sep 17 00:00:00 2001 From: Adriane Boyd Date: Tue, 8 Feb 2022 10:52:46 +0100 Subject: [PATCH 09/23] Support mixed case model package names (#10223) --- spacy/util.py | 1 - 1 file changed, 1 deletion(-) diff --git a/spacy/util.py b/spacy/util.py index 14714143c..2a8b9f5cc 100644 --- a/spacy/util.py +++ b/spacy/util.py @@ -871,7 +871,6 @@ def get_package_path(name: str) -> Path: name (str): Package name. RETURNS (Path): Path to installed package. """ - name = name.lower() # use lowercase version to be safe # Here we're importing the module just to find it. This is worryingly # indirect, but it's otherwise very difficult to find the package. pkg = importlib.import_module(name) From f2c2b97e56f4f6d73e7cede8a98f7ba9668e83b0 Mon Sep 17 00:00:00 2001 From: Ines Montani Date: Tue, 8 Feb 2022 11:46:42 +0100 Subject: [PATCH 10/23] Add spaCy Tailored Pipelines --- README.md | 31 ++++---- .../images/spacy-tailored-pipelines_wide.png | Bin 0 -> 44724 bytes website/meta/sidebars.json | 6 +- website/meta/site.json | 6 +- website/src/components/list.js | 9 ++- website/src/styles/list.module.sass | 10 +++ website/src/widgets/landing.js | 71 +++++++++++++----- 7 files changed, 94 insertions(+), 39 deletions(-) create mode 100644 website/docs/images/spacy-tailored-pipelines_wide.png diff --git a/README.md b/README.md index 57d76fb45..05c912ffa 100644 --- a/README.md +++ b/README.md @@ -32,19 +32,20 @@ open-source software, released under the MIT license. ## ๐Ÿ“– Documentation -| Documentation | | -| -------------------------- | -------------------------------------------------------------- | -| โญ๏ธ **[spaCy 101]** | New to spaCy? Here's everything you need to know! | -| ๐Ÿ“š **[Usage Guides]** | How to use spaCy and its features. | -| ๐Ÿš€ **[New in v3.0]** | New features, backwards incompatibilities and migration guide. | -| ๐Ÿช **[Project Templates]** | End-to-end workflows you can clone, modify and run. | -| ๐ŸŽ› **[API Reference]** | The detailed reference for spaCy's API. | -| ๐Ÿ“ฆ **[Models]** | Download trained pipelines for spaCy. | -| ๐ŸŒŒ **[Universe]** | Plugins, extensions, demos and books from the spaCy ecosystem. | -| ๐Ÿ‘ฉโ€๐Ÿซ **[Online Course]** | Learn spaCy in this free and interactive online course. | -| ๐Ÿ“บ **[Videos]** | Our YouTube channel with video tutorials, talks and more. | -| ๐Ÿ›  **[Changelog]** | Changes and version history. | -| ๐Ÿ’ **[Contribute]** | How to contribute to the spaCy project and code base. | +| Documentation | | +| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| โญ๏ธ **[spaCy 101]** | New to spaCy? Here's everything you need to know! | +| ๐Ÿ“š **[Usage Guides]** | How to use spaCy and its features. | +| ๐Ÿš€ **[New in v3.0]** | New features, backwards incompatibilities and migration guide. | +| ๐Ÿช **[Project Templates]** | End-to-end workflows you can clone, modify and run. | +| ๐ŸŽ› **[API Reference]** | The detailed reference for spaCy's API. | +| ๐Ÿ“ฆ **[Models]** | Download trained pipelines for spaCy. | +| ๐ŸŒŒ **[Universe]** | Plugins, extensions, demos and books from the spaCy ecosystem. | +| ๐Ÿ‘ฉโ€๐Ÿซ **[Online Course]** | Learn spaCy in this free and interactive online course. | +| ๐Ÿ“บ **[Videos]** | Our YouTube channel with video tutorials, talks and more. | +| ๐Ÿ›  **[Changelog]** | Changes and version history. | +| ๐Ÿ’ **[Contribute]** | How to contribute to the spaCy project and code base. | +| spaCy Tailored Pipelines | Get a custom spaCy pipeline, tailor-made for your NLP problem by spaCy's core developers. Streamlined, production-ready, predictable and maintainable. Start by completing our 5-minute questionnaire to tell us what you need and we'll be in touch! **[Learn more →](https://explosion.ai/spacy-tailored-pipelines)** | [spacy 101]: https://spacy.io/usage/spacy-101 [new in v3.0]: https://spacy.io/usage/v3 @@ -60,9 +61,7 @@ open-source software, released under the MIT license. ## ๐Ÿ’ฌ Where to ask questions -The spaCy project is maintained by **[@honnibal](https://github.com/honnibal)**, -**[@ines](https://github.com/ines)**, **[@svlandeg](https://github.com/svlandeg)**, -**[@adrianeboyd](https://github.com/adrianeboyd)** and **[@polm](https://github.com/polm)**. +The spaCy project is maintained by the [spaCy team](https://explosion.ai/about). Please understand that we won't be able to provide individual support via email. We also believe that help is much more valuable if it's shared publicly, so that more people can benefit from it. diff --git a/website/docs/images/spacy-tailored-pipelines_wide.png b/website/docs/images/spacy-tailored-pipelines_wide.png new file mode 100644 index 0000000000000000000000000000000000000000..d1a762ebe06a330ba00c5593803221db300c8bcd GIT binary patch literal 44724 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 literal 0 HcmV?d00001 diff --git a/website/meta/sidebars.json b/website/meta/sidebars.json index 1054f7626..c49b49c73 100644 --- a/website/meta/sidebars.json +++ b/website/meta/sidebars.json @@ -40,7 +40,11 @@ "label": "Resources", "items": [ { "text": "Project Templates", "url": "https://github.com/explosion/projects" }, - { "text": "v2.x Documentation", "url": "https://v2.spacy.io" } + { "text": "v2.x Documentation", "url": "https://v2.spacy.io" }, + { + "text": "Custom Solutions", + "url": "https://explosion.ai/spacy-tailored-pipelines" + } ] } ] diff --git a/website/meta/site.json b/website/meta/site.json index 169680f86..9ecaef74c 100644 --- a/website/meta/site.json +++ b/website/meta/site.json @@ -48,7 +48,11 @@ { "text": "Usage", "url": "/usage" }, { "text": "Models", "url": "/models" }, { "text": "API Reference", "url": "/api" }, - { "text": "Online Course", "url": "https://course.spacy.io" } + { "text": "Online Course", "url": "https://course.spacy.io" }, + { + "text": "Custom Solutions", + "url": "https://explosion.ai/spacy-tailored-pipelines" + } ] }, { diff --git a/website/src/components/list.js b/website/src/components/list.js index e0a3d9b64..d31617487 100644 --- a/website/src/components/list.js +++ b/website/src/components/list.js @@ -6,11 +6,14 @@ import { replaceEmoji } from './icon' export const Ol = props =>
    export const Ul = props =>
      -export const Li = ({ children, ...props }) => { +export const Li = ({ children, emoji, ...props }) => { const { hasIcon, content } = replaceEmoji(children) - const liClassNames = classNames(classes.li, { [classes.liIcon]: hasIcon }) + const liClassNames = classNames(classes.li, { + [classes.liIcon]: hasIcon, + [classes.emoji]: emoji, + }) return ( -
    • +
    • {content}
    • ) diff --git a/website/src/styles/list.module.sass b/website/src/styles/list.module.sass index 588b30ba0..1a352d9dd 100644 --- a/website/src/styles/list.module.sass +++ b/website/src/styles/list.module.sass @@ -36,6 +36,16 @@ box-sizing: content-box vertical-align: top +.emoji:before + content: attr(data-emoji) + padding-right: 0.75em + padding-top: 0 + margin-left: -2.5em + width: 1.75em + text-align: right + font-size: 1em + position: static + .li-icon text-indent: calc(-20px - 0.55em) diff --git a/website/src/widgets/landing.js b/website/src/widgets/landing.js index 74607fd09..b7ae35f6e 100644 --- a/website/src/widgets/landing.js +++ b/website/src/widgets/landing.js @@ -15,9 +15,9 @@ import { } from '../components/landing' import { H2 } from '../components/typography' import { InlineCode } from '../components/code' +import { Ul, Li } from '../components/list' import Button from '../components/button' import Link from '../components/link' -import { YouTube } from '../components/embed' import QuickstartTraining from './quickstart-training' import Project from './project' @@ -25,6 +25,7 @@ import Features from './features' import courseImage from '../../docs/images/course.jpg' import prodigyImage from '../../docs/images/prodigy_overview.jpg' import projectsImage from '../../docs/images/projects.png' +import tailoredPipelinesImage from '../../docs/images/spacy-tailored-pipelines_wide.png' import Benchmarks from 'usage/_benchmarks-models.md' @@ -104,23 +105,45 @@ const Landing = ({ data }) => { - spaCy v3.0 features all new transformer-based pipelines that - bring spaCy's accuracy right up to the current state-of-the-art - . You can use any pretrained transformer to train your own pipelines, and even - share one transformer between multiple components with{' '} - multi-task learning. Training is now fully configurable and - extensible, and you can define your own custom models using{' '} - PyTorch, TensorFlow and other frameworks. The - new spaCy projects system lets you describe whole{' '} - end-to-end workflows in a single file, giving you an easy path - from prototype to production, and making it easy to clone and adapt - best-practice projects for your own use cases. + + spaCy Tailored Pipelines + + + Get a custom spaCy pipeline, tailor-made for your NLP problem by spaCy's + core developers. + +
      +
      +
        +
      • + Streamlined. Nobody knows spaCy better than we do. Send + us your pipeline requirements and we'll be ready to start producing your + solution in no time at all. +
      • +
      • + Production ready. spaCy pipelines are robust and easy + to deploy. You'll get a complete spaCy project folder which is ready to{' '} + spacy project run. +
      • +
      • + Predictable. You'll know exactly what you're going to + get and what it's going to cost. We quote fees up-front, let you try + before you buy, and don't charge for over-runs at our end โ€” all the risk + is on us. +
      • +
      • + Maintainable. spaCy is an industry standard, and we'll + deliver your pipeline with full code, data, tests and documentation, so + your team can retrain, update and extend the solution as your + requirements change. +
      • +
      { - - + + spaCy v3.0 features all new transformer-based pipelines that + bring spaCy's accuracy right up to the current state-of-the-art + . You can use any pretrained transformer to train your own pipelines, and even + share one transformer between multiple components with{' '} + multi-task learning. Training is now fully configurable and + extensible, and you can define your own custom models using{' '} + PyTorch, TensorFlow and other frameworks. Date: Tue, 8 Feb 2022 13:37:27 +0100 Subject: [PATCH 11/23] fix(phrasematcher.pyi): change type annotation of `docs` in `add()` to `List[Doc]` (#10235) https://github.com/explosion/spaCy/issues/10234 --- spacy/matcher/phrasematcher.pyi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spacy/matcher/phrasematcher.pyi b/spacy/matcher/phrasematcher.pyi index 82a194835..68e3386e4 100644 --- a/spacy/matcher/phrasematcher.pyi +++ b/spacy/matcher/phrasematcher.pyi @@ -14,7 +14,7 @@ class PhraseMatcher: def add( self, key: str, - docs: List[List[Dict[str, Any]]], + docs: List[Doc], *, on_match: Optional[ Callable[[Matcher, Doc, int, List[Tuple[Any, ...]]], Any] From 10c77af83d700c78aaf08be793e78b5d79f8550a Mon Sep 17 00:00:00 2001 From: John Boy <2187261+jboynyc@users.noreply.github.com> Date: Wed, 9 Feb 2022 06:04:26 +0000 Subject: [PATCH 12/23] add textnets to spaCy universe (#10216) https://github.com/jboynyc/textnets/issues/38 --- website/meta/universe.json | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/website/meta/universe.json b/website/meta/universe.json index 1a67de67b..4ded8880f 100644 --- a/website/meta/universe.json +++ b/website/meta/universe.json @@ -3769,6 +3769,29 @@ }, "category": ["pipeline"], "tags": ["pipeline", "nlp", "sentiment"] + }, + { + "id": "textnets", + "slogan": "Text analysis with networks", + "description": "textnets represents collections of texts as networks of documents and words. This provides novel possibilities for the visualization and analysis of texts.", + "github": "jboynyc/textnets", + "image": "https://user-images.githubusercontent.com/2187261/152641425-6c0fb41c-b8e0-44fb-a52a-7c1ba24eba1e.png", + "code_example": [ + "import textnets as tn", + "", + "corpus = tn.Corpus(tn.examples.moon_landing)", + "t = tn.Textnet(corpus.tokenized(), min_docs=1)", + "t.plot(label_nodes=True,", + " show_clusters=True,", + " scale_nodes_by=\"birank\",", + " scale_edges_by=\"weight\")" + ], + "author": "John Boy", + "author_links": { + "github": "jboynyc", + "twitter": "jboy" + }, + "category": ["visualizers", "standalone"] } ], From 3877f78ff9f406a148e27a16ee60a7778bc5a551 Mon Sep 17 00:00:00 2001 From: Ryn Daniels <397565+ryndaniels@users.noreply.github.com> Date: Wed, 9 Feb 2022 12:21:20 +0200 Subject: [PATCH 13/23] fix the syntax for the slow/gpu test crons (#10244) --- .github/workflows/gputests.yml | 3 ++- .github/workflows/slowtests.yml | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/gputests.yml b/.github/workflows/gputests.yml index 7c062fe4c..2a5be13ce 100644 --- a/.github/workflows/gputests.yml +++ b/.github/workflows/gputests.yml @@ -1,3 +1,5 @@ +name: Weekly GPU tests + on: schedule: - cron: '0 1 * * MON' @@ -15,5 +17,4 @@ jobs: PIPELINE: explosion-ai/spacy-slow-gpu-tests BRANCH: ${{ matrix.branch }} MESSAGE: ":github: Weekly GPU + slow tests - triggered from a GitHub Action" - secrets: BUILDKITE_API_ACCESS_TOKEN: ${{ secrets.BUILDKITE_SECRET }} diff --git a/.github/workflows/slowtests.yml b/.github/workflows/slowtests.yml index 4d4441679..c3a08e5b5 100644 --- a/.github/workflows/slowtests.yml +++ b/.github/workflows/slowtests.yml @@ -1,3 +1,5 @@ +name: Daily slow tests + on: schedule: - cron: '0 0 * * *' @@ -21,12 +23,10 @@ jobs: fi - name: Trigger buildkite build - needs: check_commits - if: needs.check_commits.outputs.run_tests == 'true' + if: steps.check_commits.outputs.run_tests == 'true' uses: buildkite/trigger-pipeline-action@v1.2.0 env: PIPELINE: explosion-ai/spacy-slow-tests BRANCH: ${{ matrix.branch }} MESSAGE: ":github: Daily slow tests - triggered from a GitHub Action" - secrets: BUILDKITE_API_ACCESS_TOKEN: ${{ secrets.BUILDKITE_SECRET }} From ee662ec38190f2f806ab67309ed9eb160dcaceed Mon Sep 17 00:00:00 2001 From: Peter Baumgartner <5107405+pmbaumgartner@users.noreply.github.com> Date: Thu, 10 Feb 2022 02:15:23 -0500 Subject: [PATCH 14/23] Raise error in spacy package when model name is not a valid python identifier (#10192) * MultiHashEmbed vector docs correction * raise error for invalid identifier as model name * more succinct error message * update success message * permitted package name + double underscore * clarify package name error * clarify underscore run message * tweak language + simplify underscore run * cleanup underscore run warning * spacing correction * Update spacy/tests/test_cli.py Co-authored-by: Adriane Boyd --- spacy/cli/package.py | 37 +++++++++++++++++++++++++++++++++++-- spacy/tests/test_cli.py | 14 +++++++++++++- 2 files changed, 48 insertions(+), 3 deletions(-) diff --git a/spacy/cli/package.py b/spacy/cli/package.py index f9d2a9af2..b8c8397b6 100644 --- a/spacy/cli/package.py +++ b/spacy/cli/package.py @@ -7,6 +7,7 @@ from collections import defaultdict from catalogue import RegistryError import srsly import sys +import re from ._util import app, Arg, Opt, string_to_list, WHEEL_SUFFIX, SDIST_SUFFIX from ..schemas import validate, ModelMetaSchema @@ -109,6 +110,24 @@ def package( ", ".join(meta["requirements"]), ) if name is not None: + if not name.isidentifier(): + msg.fail( + f"Model name ('{name}') is not a valid module name. " + "This is required so it can be imported as a module.", + "We recommend names that use ASCII A-Z, a-z, _ (underscore), " + "and 0-9. " + "For specific details see: https://docs.python.org/3/reference/lexical_analysis.html#identifiers", + exits=1, + ) + if not _is_permitted_package_name(name): + msg.fail( + f"Model name ('{name}') is not a permitted package name. " + "This is required to correctly load the model with spacy.load.", + "We recommend names that use ASCII A-Z, a-z, _ (underscore), " + "and 0-9. " + "For specific details see: https://www.python.org/dev/peps/pep-0426/#name", + exits=1, + ) meta["name"] = name if version is not None: meta["version"] = version @@ -162,7 +181,7 @@ def package( imports="\n".join(f"from . import {m}" for m in imports) ) create_file(package_path / "__init__.py", init_py) - msg.good(f"Successfully created package '{model_name_v}'", main_path) + msg.good(f"Successfully created package directory '{model_name_v}'", main_path) if create_sdist: with util.working_dir(main_path): util.run_command([sys.executable, "setup.py", "sdist"], capture=False) @@ -171,8 +190,14 @@ def package( if create_wheel: with util.working_dir(main_path): util.run_command([sys.executable, "setup.py", "bdist_wheel"], capture=False) - wheel = main_path / "dist" / f"{model_name_v}{WHEEL_SUFFIX}" + wheel_name_squashed = re.sub("_+", "_", model_name_v) + wheel = main_path / "dist" / f"{wheel_name_squashed}{WHEEL_SUFFIX}" msg.good(f"Successfully created binary wheel", wheel) + if "__" in model_name: + msg.warn( + f"Model name ('{model_name}') contains a run of underscores. " + "Runs of underscores are not significant in installed package names.", + ) def has_wheel() -> bool: @@ -422,6 +447,14 @@ def _format_label_scheme(data: Dict[str, Any]) -> str: return md.text +def _is_permitted_package_name(package_name: str) -> bool: + # regex from: https://www.python.org/dev/peps/pep-0426/#name + permitted_match = re.search( + r"^([A-Z0-9]|[A-Z0-9][A-Z0-9._-]*[A-Z0-9])$", package_name, re.IGNORECASE + ) + return permitted_match is not None + + TEMPLATE_SETUP = """ #!/usr/bin/env python import io diff --git a/spacy/tests/test_cli.py b/spacy/tests/test_cli.py index 9d5bdfab2..9d3f1ee71 100644 --- a/spacy/tests/test_cli.py +++ b/spacy/tests/test_cli.py @@ -17,6 +17,7 @@ from spacy.cli.debug_data import _get_labels_from_spancat from spacy.cli.download import get_compatibility, get_version from spacy.cli.init_config import RECOMMENDATIONS, init_config, fill_config from spacy.cli.package import get_third_party_dependencies +from spacy.cli.package import _is_permitted_package_name from spacy.cli.validate import get_model_pkgs from spacy.lang.en import English from spacy.lang.nl import Dutch @@ -695,6 +696,17 @@ def test_get_labels_from_model(factory_name, pipe_name): assert _get_labels_from_model(nlp, factory_name) == set(labels) +def test_permitted_package_names(): + # https://www.python.org/dev/peps/pep-0426/#name + assert _is_permitted_package_name("Meine_Bรคume") == False + assert _is_permitted_package_name("_package") == False + assert _is_permitted_package_name("package_") == False + assert _is_permitted_package_name(".package") == False + assert _is_permitted_package_name("package.") == False + assert _is_permitted_package_name("-package") == False + assert _is_permitted_package_name("package-") == False + + def test_debug_data_compile_gold(): nlp = English() pred = Doc(nlp.vocab, words=["Token", ".", "New", "York", "City"]) @@ -707,4 +719,4 @@ def test_debug_data_compile_gold(): ref = Doc(nlp.vocab, words=["Token", ".", "New York City"], sent_starts=[True, False, True], ents=["O", "B-ENT", "I-ENT"]) eg = Example(pred, ref) data = _compile_gold([eg], ["ner"], nlp, True) - assert data["boundary_cross_ents"] == 1 + assert data["boundary_cross_ents"] == 1 \ No newline at end of file From 2d6cabb23c1b39995fdd8bdaf78f68ac344f7901 Mon Sep 17 00:00:00 2001 From: Ryn Daniels <397565+ryndaniels@users.noreply.github.com> Date: Thu, 10 Feb 2022 13:06:30 +0200 Subject: [PATCH 15/23] Fix the date command and the matrix failure mode (#10254) --- .github/workflows/gputests.yml | 1 + .github/workflows/slowtests.yml | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/gputests.yml b/.github/workflows/gputests.yml index 2a5be13ce..14c1552bf 100644 --- a/.github/workflows/gputests.yml +++ b/.github/workflows/gputests.yml @@ -7,6 +7,7 @@ on: jobs: weekly-gputests: strategy: + fail-fast: false matrix: branch: [master, develop, v4] runs-on: ubuntu-latest diff --git a/.github/workflows/slowtests.yml b/.github/workflows/slowtests.yml index c3a08e5b5..9490b53bd 100644 --- a/.github/workflows/slowtests.yml +++ b/.github/workflows/slowtests.yml @@ -7,15 +7,18 @@ on: jobs: daily-slowtests: strategy: + fail-fast: false matrix: branch: [master, develop, v4] runs-on: ubuntu-latest steps: + - name: Checkout + uses: actions/checkout@v1 - name: Get commits from past 24 hours id: check_commits run: | today=$(date '+%Y-%m-%d %H:%M:%S') - yesterday=$(date -v-1d '+%Y-%m-%d %H:%M:%S') + yesterday=$(date -d "yesterday" '+%Y-%m-%d %H:%M:%S') if git log --after=$yesterday --before=$today | grep commit ; then echo "::set-output name=run_tests::true" else From 7961a0a959e6860bc9b2eb9d487a77de84758ae9 Mon Sep 17 00:00:00 2001 From: Edward <43848523+thomashacker@users.noreply.github.com> Date: Thu, 10 Feb 2022 13:45:46 +0100 Subject: [PATCH 16/23] Fix typo in errors (#10256) --- spacy/errors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spacy/errors.py b/spacy/errors.py index 390612123..b45c4f9db 100644 --- a/spacy/errors.py +++ b/spacy/errors.py @@ -483,7 +483,7 @@ class Errors(metaclass=ErrorsWithCodes): "components, since spans are only views of the Doc. Use Doc and " "Token attributes (or custom extension attributes) only and remove " "the following: {attrs}") - E181 = ("Received invalid attributes for unkown object {obj}: {attrs}. " + E181 = ("Received invalid attributes for unknown object {obj}: {attrs}. " "Only Doc and Token attributes are supported.") E182 = ("Received invalid attribute declaration: {attr}\nDid you forget " "to define the attribute? For example: `{attr}.???`") From bbaf41fb3b1b0123455b93d7b97a9ef5d886f8b1 Mon Sep 17 00:00:00 2001 From: Adriane Boyd Date: Fri, 11 Feb 2022 11:45:26 +0100 Subject: [PATCH 17/23] Set version to v3.2.2 (#10262) --- spacy/about.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spacy/about.py b/spacy/about.py index c253d5052..d01b278c9 100644 --- a/spacy/about.py +++ b/spacy/about.py @@ -1,6 +1,6 @@ # fmt: off __title__ = "spacy" -__version__ = "3.2.1" +__version__ = "3.2.2" __download_url__ = "https://github.com/explosion/spacy-models/releases/download" __compatibility__ = "https://raw.githubusercontent.com/explosion/spacy-models/master/compatibility.json" __projects__ = "https://github.com/explosion/projects" From 9a06a210ec8ef2a6cd93f4572c3dd18c2532ca71 Mon Sep 17 00:00:00 2001 From: Adriane Boyd Date: Fri, 11 Feb 2022 14:22:43 +0100 Subject: [PATCH 18/23] Exclude github workflow edits from CI (#10261) --- azure-pipelines.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 71a793911..8e322f3dd 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -17,6 +17,7 @@ pr: - "*.md" - "website/docs/*" - "website/src/*" + - ".github/workflows/*" jobs: # Perform basic checks for most important errors (syntax etc.) Uses the config From 5adedb8587818741dcd4ee1364ffb3f7d5074e75 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 11 Feb 2022 14:23:01 +0100 Subject: [PATCH 19/23] Auto-format code with black (#10260) Co-authored-by: explosion-bot --- spacy/tests/test_cli.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/spacy/tests/test_cli.py b/spacy/tests/test_cli.py index 9d3f1ee71..fc35ff86e 100644 --- a/spacy/tests/test_cli.py +++ b/spacy/tests/test_cli.py @@ -706,17 +706,27 @@ def test_permitted_package_names(): assert _is_permitted_package_name("-package") == False assert _is_permitted_package_name("package-") == False - + def test_debug_data_compile_gold(): nlp = English() pred = Doc(nlp.vocab, words=["Token", ".", "New", "York", "City"]) - ref = Doc(nlp.vocab, words=["Token", ".", "New York City"], sent_starts=[True, False, True], ents=["O", "O", "B-ENT"]) + ref = Doc( + nlp.vocab, + words=["Token", ".", "New York City"], + sent_starts=[True, False, True], + ents=["O", "O", "B-ENT"], + ) eg = Example(pred, ref) data = _compile_gold([eg], ["ner"], nlp, True) assert data["boundary_cross_ents"] == 0 pred = Doc(nlp.vocab, words=["Token", ".", "New", "York", "City"]) - ref = Doc(nlp.vocab, words=["Token", ".", "New York City"], sent_starts=[True, False, True], ents=["O", "B-ENT", "I-ENT"]) + ref = Doc( + nlp.vocab, + words=["Token", ".", "New York City"], + sent_starts=[True, False, True], + ents=["O", "B-ENT", "I-ENT"], + ) eg = Example(pred, ref) data = _compile_gold([eg], ["ner"], nlp, True) - assert data["boundary_cross_ents"] == 1 \ No newline at end of file + assert data["boundary_cross_ents"] == 1 From 8818a44a39f6e8f5387680e28984897a60baa830 Mon Sep 17 00:00:00 2001 From: Markus Konrad Date: Mon, 14 Feb 2022 07:16:43 +0100 Subject: [PATCH 20/23] add tmtoolkit package to spaCy universe (#10245) --- website/meta/universe.json | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/website/meta/universe.json b/website/meta/universe.json index 4ded8880f..d7eef97e8 100644 --- a/website/meta/universe.json +++ b/website/meta/universe.json @@ -3792,6 +3792,39 @@ "twitter": "jboy" }, "category": ["visualizers", "standalone"] + }, + { + "id": "tmtoolkit", + "slogan": "Text mining and topic modeling toolkit", + "description": "tmtoolkit is a set of tools for text mining and topic modeling with Python developed especially for the use in the social sciences, in journalism or related disciplines. It aims for easy installation, extensive documentation and a clear programming interface while offering good performance on large datasets by the means of vectorized operations (via NumPy) and parallel computation (using Pythonโ€™s multiprocessing module and the loky package).", + "github": "WZBSocialScienceCenter/tmtoolkit", + "code_example": [ + "from tmtoolkit.corpus import Corpus, tokens_table, lemmatize, to_lowercase, dtm", + "from tmtoolkit.bow.bow_stats import tfidf, sorted_terms_table", + "# load built-in sample dataset and use 4 worker processes", + "corp = Corpus.from_builtin_corpus('en-News100', max_workers=4)", + "# investigate corpus as dataframe", + "toktbl = tokens_table(corp)", + "print(toktbl)", + "# apply some text normalization", + "lemmatize(corp)", + "to_lowercase(corp)", + "# build sparse document-token matrix (DTM)", + "# document labels identify rows, vocabulary tokens identify columns", + "mat, doc_labels, vocab = dtm(corp, return_doc_labels=True, return_vocab=True)", + "# apply tf-idf transformation to DTM", + "# operation is applied on sparse matrix and uses few memory", + "tfidf_mat = tfidf(mat)", + "# show top 5 tokens per document ranked by tf-idf", + "top_tokens = sorted_terms_table(tfidf_mat, vocab, doc_labels, top_n=5)", + "print(top_tokens)" + ], + "author": "Markus Konrad / WZB Social Science Center", + "author_links": { + "github": "internaut", + "twitter": "_knrd" + }, + "category": ["scientific", "standalone"] } ], From 23bd103d8940c110e2588e7c93f8e33205e1b3be Mon Sep 17 00:00:00 2001 From: Paul O'Leary McCann Date: Mon, 14 Feb 2022 15:17:25 +0900 Subject: [PATCH 21/23] Add tmtoolkit setup steps --- website/meta/universe.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/website/meta/universe.json b/website/meta/universe.json index d7eef97e8..122281583 100644 --- a/website/meta/universe.json +++ b/website/meta/universe.json @@ -3799,6 +3799,9 @@ "description": "tmtoolkit is a set of tools for text mining and topic modeling with Python developed especially for the use in the social sciences, in journalism or related disciplines. It aims for easy installation, extensive documentation and a clear programming interface while offering good performance on large datasets by the means of vectorized operations (via NumPy) and parallel computation (using Pythonโ€™s multiprocessing module and the loky package).", "github": "WZBSocialScienceCenter/tmtoolkit", "code_example": [ + "# Note: This requires these setup steps:", + "# pip install tmtoolkit[recommended]", + "# python -m tmtoolkit setup en", "from tmtoolkit.corpus import Corpus, tokens_table, lemmatize, to_lowercase, dtm", "from tmtoolkit.bow.bow_stats import tfidf, sorted_terms_table", "# load built-in sample dataset and use 4 worker processes", From f6250015ab4693131bde160ba5659151046cdd1d Mon Sep 17 00:00:00 2001 From: Ryn Daniels <397565+ryndaniels@users.noreply.github.com> Date: Tue, 15 Feb 2022 15:18:36 +0200 Subject: [PATCH 22/23] Fix the datemath for reals (#10294) * add debugging branch and quotes to daily slowtest action * Apparently the quotes fixed it --- .github/workflows/slowtests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/slowtests.yml b/.github/workflows/slowtests.yml index 9490b53bd..3b0f177a7 100644 --- a/.github/workflows/slowtests.yml +++ b/.github/workflows/slowtests.yml @@ -19,7 +19,7 @@ jobs: run: | today=$(date '+%Y-%m-%d %H:%M:%S') yesterday=$(date -d "yesterday" '+%Y-%m-%d %H:%M:%S') - if git log --after=$yesterday --before=$today | grep commit ; then + if git log --after="$yesterday" --before="$today" | grep commit ; then echo "::set-output name=run_tests::true" else echo "::set-output name=run_tests::false" From 22066f4e0fd2a0685932b118bbc7501370c17dd9 Mon Sep 17 00:00:00 2001 From: Adriane Boyd Date: Wed, 16 Feb 2022 13:45:30 +0100 Subject: [PATCH 23/23] Also exclude workflows from non-PR CI runs (#10305) --- azure-pipelines.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 8e322f3dd..4624b2eb2 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -11,8 +11,9 @@ trigger: exclude: - "website/*" - "*.md" + - ".github/workflows/*" pr: - paths: + paths: exclude: - "*.md" - "website/docs/*"