2020-08-18 17:06:37 +03:00
|
|
|
import warnings
|
2020-08-05 17:00:59 +03:00
|
|
|
from typing import Union, List, Iterable, Iterator, TYPE_CHECKING, Callable
|
2020-09-28 04:03:27 +03:00
|
|
|
from typing import Optional
|
2020-07-29 12:36:42 +03:00
|
|
|
from pathlib import Path
|
2021-04-08 11:08:04 +03:00
|
|
|
import random
|
2020-09-13 15:05:05 +03:00
|
|
|
import srsly
|
2020-07-29 12:36:42 +03:00
|
|
|
|
2020-06-26 20:34:12 +03:00
|
|
|
from .. import util
|
2020-09-28 04:03:27 +03:00
|
|
|
from .augment import dont_augment
|
2020-06-26 20:34:12 +03:00
|
|
|
from .example import Example
|
2020-09-29 23:33:46 +03:00
|
|
|
from ..errors import Warnings, Errors
|
2020-06-26 20:34:12 +03:00
|
|
|
from ..tokens import DocBin, Doc
|
2020-07-29 12:36:42 +03:00
|
|
|
from ..vocab import Vocab
|
|
|
|
|
|
|
|
if TYPE_CHECKING:
|
|
|
|
# This lets us add type hints for mypy etc. without causing circular imports
|
|
|
|
from ..language import Language # noqa: F401
|
2020-06-26 20:34:12 +03:00
|
|
|
|
2020-08-18 17:06:37 +03:00
|
|
|
FILE_TYPE = ".spacy"
|
|
|
|
|
2020-06-26 20:34:12 +03:00
|
|
|
|
2020-08-04 16:09:37 +03:00
|
|
|
@util.registry.readers("spacy.Corpus.v1")
|
|
|
|
def create_docbin_reader(
|
2020-09-29 23:33:46 +03:00
|
|
|
path: Optional[Path],
|
2020-09-28 04:03:27 +03:00
|
|
|
gold_preproc: bool,
|
|
|
|
max_length: int = 0,
|
|
|
|
limit: int = 0,
|
|
|
|
augmenter: Optional[Callable] = None,
|
2020-08-04 16:09:37 +03:00
|
|
|
) -> Callable[["Language"], Iterable[Example]]:
|
2020-09-29 23:33:46 +03:00
|
|
|
if path is None:
|
|
|
|
raise ValueError(Errors.E913)
|
2020-09-29 23:53:14 +03:00
|
|
|
util.logger.debug(f"Loading corpus from path: {path}")
|
2020-09-28 04:03:27 +03:00
|
|
|
return Corpus(
|
|
|
|
path,
|
|
|
|
gold_preproc=gold_preproc,
|
|
|
|
max_length=max_length,
|
|
|
|
limit=limit,
|
|
|
|
augmenter=augmenter,
|
|
|
|
)
|
2020-08-04 16:09:37 +03:00
|
|
|
|
2020-09-15 01:32:49 +03:00
|
|
|
|
2020-10-02 02:36:06 +03:00
|
|
|
@util.registry.readers("spacy.JsonlCorpus.v1")
|
2020-09-13 15:05:05 +03:00
|
|
|
def create_jsonl_reader(
|
2020-09-15 01:32:49 +03:00
|
|
|
path: Path, min_length: int = 0, max_length: int = 0, limit: int = 0
|
2020-09-13 15:05:05 +03:00
|
|
|
) -> Callable[["Language"], Iterable[Doc]]:
|
2020-10-02 02:36:06 +03:00
|
|
|
return JsonlCorpus(path, min_length=min_length, max_length=max_length, limit=limit)
|
2020-09-13 15:05:05 +03:00
|
|
|
|
|
|
|
|
2020-09-30 17:52:27 +03:00
|
|
|
@util.registry.readers("spacy.read_labels.v1")
|
2020-10-01 18:38:17 +03:00
|
|
|
def read_labels(path: Path, *, require: bool = False):
|
2020-09-30 17:52:27 +03:00
|
|
|
# I decided not to give this a generic name, because I don't want people to
|
|
|
|
# use it for arbitrary stuff, as I want this require arg with default False.
|
|
|
|
if not require and not path.exists():
|
|
|
|
return None
|
|
|
|
return srsly.read_json(path)
|
|
|
|
|
|
|
|
|
2020-09-13 15:05:05 +03:00
|
|
|
def walk_corpus(path: Union[str, Path], file_type) -> List[Path]:
|
|
|
|
path = util.ensure_path(path)
|
|
|
|
if not path.is_dir() and path.parts[-1].endswith(file_type):
|
|
|
|
return [path]
|
|
|
|
orig_path = path
|
|
|
|
paths = [path]
|
|
|
|
locs = []
|
|
|
|
seen = set()
|
|
|
|
for path in paths:
|
|
|
|
if str(path) in seen:
|
|
|
|
continue
|
|
|
|
seen.add(str(path))
|
|
|
|
if path.parts and path.parts[-1].startswith("."):
|
|
|
|
continue
|
|
|
|
elif path.is_dir():
|
|
|
|
paths.extend(path.iterdir())
|
|
|
|
elif path.parts[-1].endswith(file_type):
|
|
|
|
locs.append(path)
|
|
|
|
if len(locs) == 0:
|
2020-09-25 16:47:10 +03:00
|
|
|
warnings.warn(Warnings.W090.format(path=orig_path, format=file_type))
|
2020-09-25 20:07:26 +03:00
|
|
|
# It's good to sort these, in case the ordering messes up a cache.
|
|
|
|
locs.sort()
|
2020-09-13 15:05:05 +03:00
|
|
|
return locs
|
|
|
|
|
|
|
|
|
2020-06-26 20:34:12 +03:00
|
|
|
class Corpus:
|
2020-08-04 16:09:37 +03:00
|
|
|
"""Iterate Example objects from a file or directory of DocBin (.spacy)
|
2020-08-06 16:29:44 +03:00
|
|
|
formatted data files.
|
2020-08-04 16:09:37 +03:00
|
|
|
|
|
|
|
path (Path): The directory or filename to read from.
|
|
|
|
gold_preproc (bool): Whether to set up the Example object with gold-standard
|
2020-08-05 17:00:59 +03:00
|
|
|
sentences and tokens for the predictions. Gold preprocessing helps
|
2020-08-04 16:09:37 +03:00
|
|
|
the annotations align to the tokenization, and may result in sequences
|
|
|
|
of more consistent length. However, it may reduce run-time accuracy due
|
|
|
|
to train/test skew. Defaults to False.
|
|
|
|
max_length (int): Maximum document length. Longer documents will be
|
|
|
|
split into sentences, if sentence boundaries are available. Defaults to
|
|
|
|
0, which indicates no limit.
|
|
|
|
limit (int): Limit corpus to a subset of examples, e.g. for debugging.
|
|
|
|
Defaults to 0, which indicates no limit.
|
2020-09-28 04:03:27 +03:00
|
|
|
augment (Callable[Example, Iterable[Example]]): Optional data augmentation
|
|
|
|
function, to extrapolate additional examples from your annotations.
|
2021-04-08 11:08:04 +03:00
|
|
|
shuffle (bool): Whether to shuffle the examples.
|
2020-06-26 20:34:12 +03:00
|
|
|
|
2021-01-30 12:09:38 +03:00
|
|
|
DOCS: https://spacy.io/api/corpus
|
2020-06-26 20:34:12 +03:00
|
|
|
"""
|
|
|
|
|
2020-07-29 12:36:42 +03:00
|
|
|
def __init__(
|
2020-08-05 17:00:59 +03:00
|
|
|
self,
|
2020-08-07 15:30:59 +03:00
|
|
|
path: Union[str, Path],
|
2020-08-05 17:00:59 +03:00
|
|
|
*,
|
|
|
|
limit: int = 0,
|
|
|
|
gold_preproc: bool = False,
|
2020-09-12 22:01:53 +03:00
|
|
|
max_length: int = 0,
|
2020-09-28 04:03:27 +03:00
|
|
|
augmenter: Optional[Callable] = None,
|
2021-04-08 11:08:04 +03:00
|
|
|
shuffle: bool = False,
|
2020-07-29 12:36:42 +03:00
|
|
|
) -> None:
|
2020-08-04 16:09:37 +03:00
|
|
|
self.path = util.ensure_path(path)
|
|
|
|
self.gold_preproc = gold_preproc
|
|
|
|
self.max_length = max_length
|
2020-06-26 20:34:12 +03:00
|
|
|
self.limit = limit
|
2020-09-28 04:03:27 +03:00
|
|
|
self.augmenter = augmenter if augmenter is not None else dont_augment
|
2021-04-08 11:08:04 +03:00
|
|
|
self.shuffle = shuffle
|
2020-06-26 20:34:12 +03:00
|
|
|
|
2020-08-04 16:09:37 +03:00
|
|
|
def __call__(self, nlp: "Language") -> Iterator[Example]:
|
|
|
|
"""Yield examples from the data.
|
|
|
|
|
|
|
|
nlp (Language): The current nlp object.
|
|
|
|
YIELDS (Example): The examples.
|
|
|
|
|
2021-01-30 12:09:38 +03:00
|
|
|
DOCS: https://spacy.io/api/corpus#call
|
2020-08-04 16:09:37 +03:00
|
|
|
"""
|
2020-09-13 15:05:05 +03:00
|
|
|
ref_docs = self.read_docbin(nlp.vocab, walk_corpus(self.path, FILE_TYPE))
|
2021-04-08 11:08:04 +03:00
|
|
|
if self.shuffle:
|
|
|
|
ref_docs = list(ref_docs)
|
|
|
|
random.shuffle(ref_docs)
|
|
|
|
|
2020-08-04 16:09:37 +03:00
|
|
|
if self.gold_preproc:
|
|
|
|
examples = self.make_examples_gold_preproc(nlp, ref_docs)
|
|
|
|
else:
|
2020-09-12 22:01:53 +03:00
|
|
|
examples = self.make_examples(nlp, ref_docs)
|
2020-09-28 04:03:27 +03:00
|
|
|
for real_eg in examples:
|
|
|
|
for augmented_eg in self.augmenter(nlp, real_eg):
|
|
|
|
yield augmented_eg
|
2020-08-04 16:09:37 +03:00
|
|
|
|
2020-07-29 12:36:42 +03:00
|
|
|
def _make_example(
|
|
|
|
self, nlp: "Language", reference: Doc, gold_preproc: bool
|
|
|
|
) -> Example:
|
2020-07-03 13:58:16 +03:00
|
|
|
if gold_preproc or reference.has_unknown_spaces:
|
|
|
|
return Example(
|
|
|
|
Doc(
|
|
|
|
nlp.vocab,
|
|
|
|
words=[word.text for word in reference],
|
2020-07-04 15:23:44 +03:00
|
|
|
spaces=[bool(word.whitespace_) for word in reference],
|
2020-07-03 13:58:16 +03:00
|
|
|
),
|
2020-07-04 15:23:44 +03:00
|
|
|
reference,
|
2020-07-03 13:58:16 +03:00
|
|
|
)
|
|
|
|
else:
|
2020-07-04 15:23:44 +03:00
|
|
|
return Example(nlp.make_doc(reference.text), reference)
|
|
|
|
|
2020-07-29 12:36:42 +03:00
|
|
|
def make_examples(
|
2020-09-12 22:01:53 +03:00
|
|
|
self, nlp: "Language", reference_docs: Iterable[Doc]
|
2020-07-29 12:36:42 +03:00
|
|
|
) -> Iterator[Example]:
|
2020-06-26 20:34:12 +03:00
|
|
|
for reference in reference_docs:
|
2020-07-01 16:16:43 +03:00
|
|
|
if len(reference) == 0:
|
|
|
|
continue
|
2020-09-12 22:01:53 +03:00
|
|
|
elif self.max_length == 0 or len(reference) < self.max_length:
|
2020-07-03 13:58:16 +03:00
|
|
|
yield self._make_example(nlp, reference, False)
|
2021-03-19 11:43:52 +03:00
|
|
|
elif reference.has_annotation("SENT_START"):
|
2020-07-01 16:16:43 +03:00
|
|
|
for ref_sent in reference.sents:
|
|
|
|
if len(ref_sent) == 0:
|
|
|
|
continue
|
2020-09-12 22:01:53 +03:00
|
|
|
elif self.max_length == 0 or len(ref_sent) < self.max_length:
|
2020-07-03 13:58:16 +03:00
|
|
|
yield self._make_example(nlp, ref_sent.as_doc(), False)
|
|
|
|
|
2020-07-29 12:36:42 +03:00
|
|
|
def make_examples_gold_preproc(
|
|
|
|
self, nlp: "Language", reference_docs: Iterable[Doc]
|
|
|
|
) -> Iterator[Example]:
|
2020-06-26 20:34:12 +03:00
|
|
|
for reference in reference_docs:
|
2021-03-19 11:43:52 +03:00
|
|
|
if reference.has_annotation("SENT_START"):
|
2020-06-26 20:34:12 +03:00
|
|
|
ref_sents = [sent.as_doc() for sent in reference.sents]
|
|
|
|
else:
|
|
|
|
ref_sents = [reference]
|
|
|
|
for ref_sent in ref_sents:
|
2020-07-03 13:58:16 +03:00
|
|
|
eg = self._make_example(nlp, ref_sent, True)
|
2020-07-01 16:02:37 +03:00
|
|
|
if len(eg.x):
|
|
|
|
yield eg
|
2020-06-26 20:34:12 +03:00
|
|
|
|
2020-07-29 12:36:42 +03:00
|
|
|
def read_docbin(
|
|
|
|
self, vocab: Vocab, locs: Iterable[Union[str, Path]]
|
|
|
|
) -> Iterator[Doc]:
|
2020-06-26 20:34:12 +03:00
|
|
|
""" Yield training examples as example dicts """
|
|
|
|
i = 0
|
|
|
|
for loc in locs:
|
|
|
|
loc = util.ensure_path(loc)
|
2020-08-18 17:06:37 +03:00
|
|
|
if loc.parts[-1].endswith(FILE_TYPE):
|
2020-08-07 15:30:59 +03:00
|
|
|
doc_bin = DocBin().from_disk(loc)
|
2020-06-26 20:34:12 +03:00
|
|
|
docs = doc_bin.get_docs(vocab)
|
|
|
|
for doc in docs:
|
|
|
|
if len(doc):
|
|
|
|
yield doc
|
|
|
|
i += 1
|
|
|
|
if self.limit >= 1 and i >= self.limit:
|
|
|
|
break
|
2020-09-13 15:05:05 +03:00
|
|
|
|
|
|
|
|
2020-10-02 02:36:06 +03:00
|
|
|
class JsonlCorpus:
|
2020-09-15 01:32:49 +03:00
|
|
|
"""Iterate Doc objects from a file or directory of jsonl
|
2020-09-13 15:05:05 +03:00
|
|
|
formatted raw text files.
|
|
|
|
|
|
|
|
path (Path): The directory or filename to read from.
|
|
|
|
min_length (int): Minimum document length (in tokens). Shorter documents
|
|
|
|
will be skipped. Defaults to 0, which indicates no limit.
|
2020-09-15 01:32:49 +03:00
|
|
|
|
2020-09-13 15:05:05 +03:00
|
|
|
max_length (int): Maximum document length (in tokens). Longer documents will
|
|
|
|
be skipped. Defaults to 0, which indicates no limit.
|
|
|
|
limit (int): Limit corpus to a subset of examples, e.g. for debugging.
|
|
|
|
Defaults to 0, which indicates no limit.
|
|
|
|
|
2021-01-30 12:09:38 +03:00
|
|
|
DOCS: https://spacy.io/api/corpus#jsonlcorpus
|
2020-09-13 15:05:05 +03:00
|
|
|
"""
|
2020-09-15 01:32:49 +03:00
|
|
|
|
2020-09-13 15:05:05 +03:00
|
|
|
file_type = "jsonl"
|
|
|
|
|
|
|
|
def __init__(
|
|
|
|
self,
|
|
|
|
path: Union[str, Path],
|
|
|
|
*,
|
|
|
|
limit: int = 0,
|
|
|
|
min_length: int = 0,
|
|
|
|
max_length: int = 0,
|
|
|
|
) -> None:
|
|
|
|
self.path = util.ensure_path(path)
|
|
|
|
self.min_length = min_length
|
|
|
|
self.max_length = max_length
|
|
|
|
self.limit = limit
|
|
|
|
|
|
|
|
def __call__(self, nlp: "Language") -> Iterator[Example]:
|
|
|
|
"""Yield examples from the data.
|
|
|
|
|
|
|
|
nlp (Language): The current nlp object.
|
2020-09-15 01:32:49 +03:00
|
|
|
YIELDS (Example): The example objects.
|
2020-09-13 15:05:05 +03:00
|
|
|
|
2021-01-30 12:09:38 +03:00
|
|
|
DOCS: https://spacy.io/api/corpus#jsonlcorpus-call
|
2020-09-13 15:05:05 +03:00
|
|
|
"""
|
2020-09-25 16:47:10 +03:00
|
|
|
for loc in walk_corpus(self.path, ".jsonl"):
|
2020-09-13 15:05:05 +03:00
|
|
|
records = srsly.read_jsonl(loc)
|
|
|
|
for record in records:
|
|
|
|
doc = nlp.make_doc(record["text"])
|
|
|
|
if self.min_length >= 1 and len(doc) < self.min_length:
|
|
|
|
continue
|
|
|
|
elif self.max_length >= 1 and len(doc) >= self.max_length:
|
|
|
|
continue
|
|
|
|
else:
|
|
|
|
words = [w.text for w in doc]
|
|
|
|
spaces = [bool(w.whitespace_) for w in doc]
|
|
|
|
# We don't *need* an example here, but it seems nice to
|
|
|
|
# make it match the Corpus signature.
|
|
|
|
yield Example(doc, Doc(nlp.vocab, words=words, spaces=spaces))
|