2018-11-30 19:16:14 +00:00
|
|
|
# flake8: noqa
|
|
|
|
"""Train for CONLL 2017 UD treebank evaluation. Takes .conllu files, writes
|
2018-03-10 22:41:55 +00:00
|
|
|
.conllu format for development data, allowing the official scorer to be used.
|
2018-11-30 19:16:14 +00:00
|
|
|
"""
|
2018-03-10 22:41:55 +00:00
|
|
|
from __future__ import unicode_literals
|
2018-11-30 19:16:14 +00:00
|
|
|
|
2018-03-10 22:41:55 +00:00
|
|
|
import plac
|
|
|
|
from pathlib import Path
|
|
|
|
import re
|
|
|
|
import json
|
2019-12-16 12:12:19 +00:00
|
|
|
import tqdm
|
2018-03-10 22:41:55 +00:00
|
|
|
|
|
|
|
import spacy
|
|
|
|
import spacy.util
|
2019-10-03 12:48:45 +00:00
|
|
|
from bin.ud import conll17_ud_eval
|
2019-03-20 00:19:34 +00:00
|
|
|
from spacy.tokens import Token, Doc
|
2020-06-26 17:34:12 +00:00
|
|
|
from spacy.gold import Example
|
2020-08-18 14:10:36 +00:00
|
|
|
from spacy.util import compounding, minibatch
|
|
|
|
from spacy.gold.batchers import minibatch_by_words
|
2020-07-30 21:30:54 +00:00
|
|
|
from spacy.pipeline._parser_internals.nonproj import projectivize
|
2019-03-20 00:19:34 +00:00
|
|
|
from spacy.matcher import Matcher
|
|
|
|
from spacy import displacy
|
2019-11-11 16:35:27 +00:00
|
|
|
from collections import defaultdict
|
2018-03-10 22:41:55 +00:00
|
|
|
|
|
|
|
import random
|
|
|
|
|
2019-03-20 00:19:34 +00:00
|
|
|
from spacy import lang
|
|
|
|
from spacy.lang import zh
|
|
|
|
from spacy.lang import ja
|
2018-03-10 22:41:55 +00:00
|
|
|
|
2018-09-13 22:54:59 +00:00
|
|
|
try:
|
|
|
|
import torch
|
|
|
|
except ImportError:
|
|
|
|
torch = None
|
|
|
|
|
2018-03-10 22:41:55 +00:00
|
|
|
|
|
|
|
################
|
|
|
|
# Data reading #
|
|
|
|
################
|
|
|
|
|
2018-11-30 19:16:14 +00:00
|
|
|
space_re = re.compile("\s+")
|
2018-03-10 22:41:55 +00:00
|
|
|
|
2018-11-30 19:16:14 +00:00
|
|
|
|
|
|
|
def split_text(text):
|
|
|
|
return [space_re.sub(" ", par.strip()) for par in text.split("\n\n")]
|
|
|
|
|
|
|
|
|
|
|
|
def read_data(
|
|
|
|
nlp,
|
|
|
|
conllu_file,
|
|
|
|
text_file,
|
|
|
|
raw_text=True,
|
|
|
|
oracle_segments=False,
|
|
|
|
max_doc_length=None,
|
|
|
|
limit=None,
|
|
|
|
):
|
2019-11-11 16:35:27 +00:00
|
|
|
"""Read the CONLLU format into Example objects. If raw_text=True,
|
2018-03-10 22:41:55 +00:00
|
|
|
include Doc objects created using nlp.make_doc and then aligned against
|
|
|
|
the gold-standard sequences. If oracle_segments=True, include Doc objects
|
2018-11-30 19:16:14 +00:00
|
|
|
created from the gold-standard segments. At least one must be True."""
|
2018-03-10 22:41:55 +00:00
|
|
|
if not raw_text and not oracle_segments:
|
|
|
|
raise ValueError("At least one of raw_text or oracle_segments must be True")
|
|
|
|
paragraphs = split_text(text_file.read())
|
|
|
|
conllu = read_conllu(conllu_file)
|
|
|
|
# sd is spacy doc; cd is conllu doc
|
|
|
|
# cs is conllu sent, ct is conllu token
|
|
|
|
docs = []
|
|
|
|
golds = []
|
|
|
|
for doc_id, (text, cd) in enumerate(zip(paragraphs, conllu)):
|
|
|
|
sent_annots = []
|
|
|
|
for cs in cd:
|
|
|
|
sent = defaultdict(list)
|
|
|
|
for id_, word, lemma, pos, tag, morph, head, dep, _, space_after in cs:
|
2018-11-30 19:16:14 +00:00
|
|
|
if "." in id_:
|
2018-03-10 22:41:55 +00:00
|
|
|
continue
|
2018-11-30 19:16:14 +00:00
|
|
|
if "-" in id_:
|
2018-03-10 22:41:55 +00:00
|
|
|
continue
|
2018-11-30 19:16:14 +00:00
|
|
|
id_ = int(id_) - 1
|
|
|
|
head = int(head) - 1 if head != "0" else id_
|
|
|
|
sent["words"].append(word)
|
|
|
|
sent["tags"].append(tag)
|
2020-06-29 12:33:00 +00:00
|
|
|
sent["morphs"].append(_compile_morph_string(morph, pos))
|
2018-11-30 19:16:14 +00:00
|
|
|
sent["heads"].append(head)
|
|
|
|
sent["deps"].append("ROOT" if dep == "root" else dep)
|
|
|
|
sent["spaces"].append(space_after == "_")
|
2020-06-26 17:34:12 +00:00
|
|
|
sent["entities"] = ["-"] * len(sent["words"]) # TODO: doc-level format
|
2018-11-30 19:16:14 +00:00
|
|
|
sent["heads"], sent["deps"] = projectivize(sent["heads"], sent["deps"])
|
2018-03-10 22:41:55 +00:00
|
|
|
if oracle_segments:
|
2018-11-30 19:16:14 +00:00
|
|
|
docs.append(Doc(nlp.vocab, words=sent["words"], spaces=sent["spaces"]))
|
2020-06-26 17:34:12 +00:00
|
|
|
golds.append(sent)
|
2020-06-29 12:33:00 +00:00
|
|
|
assert golds[-1]["morphs"] is not None
|
2018-03-10 22:41:55 +00:00
|
|
|
|
|
|
|
sent_annots.append(sent)
|
|
|
|
if raw_text and max_doc_length and len(sent_annots) >= max_doc_length:
|
|
|
|
doc, gold = _make_gold(nlp, None, sent_annots)
|
2020-06-29 12:33:00 +00:00
|
|
|
assert gold["morphs"] is not None
|
2018-03-10 22:41:55 +00:00
|
|
|
sent_annots = []
|
|
|
|
docs.append(doc)
|
|
|
|
golds.append(gold)
|
|
|
|
if limit and len(docs) >= limit:
|
2019-11-11 16:35:27 +00:00
|
|
|
return golds_to_gold_data(docs, golds)
|
2018-03-10 22:41:55 +00:00
|
|
|
|
|
|
|
if raw_text and sent_annots:
|
|
|
|
doc, gold = _make_gold(nlp, None, sent_annots)
|
|
|
|
docs.append(doc)
|
|
|
|
golds.append(gold)
|
|
|
|
if limit and len(docs) >= limit:
|
2019-11-11 16:35:27 +00:00
|
|
|
return golds_to_gold_data(docs, golds)
|
|
|
|
return golds_to_gold_data(docs, golds)
|
|
|
|
|
2018-03-10 22:41:55 +00:00
|
|
|
|
2020-06-29 12:33:00 +00:00
|
|
|
def _compile_morph_string(morph_string, pos):
|
2018-09-25 19:32:24 +00:00
|
|
|
if morph_string == '_':
|
2020-06-29 12:33:00 +00:00
|
|
|
return f"POS={pos}"
|
|
|
|
return morph_string + f"|POS={pos}"
|
2018-03-10 22:41:55 +00:00
|
|
|
|
2019-11-11 16:35:27 +00:00
|
|
|
|
2018-03-10 22:41:55 +00:00
|
|
|
def read_conllu(file_):
|
|
|
|
docs = []
|
|
|
|
sent = []
|
|
|
|
doc = []
|
|
|
|
for line in file_:
|
2018-11-30 19:16:14 +00:00
|
|
|
if line.startswith("# newdoc"):
|
2018-03-10 22:41:55 +00:00
|
|
|
if doc:
|
|
|
|
docs.append(doc)
|
|
|
|
doc = []
|
2018-11-30 19:16:14 +00:00
|
|
|
elif line.startswith("#"):
|
2018-03-10 22:41:55 +00:00
|
|
|
continue
|
|
|
|
elif not line.strip():
|
|
|
|
if sent:
|
|
|
|
doc.append(sent)
|
|
|
|
sent = []
|
|
|
|
else:
|
2018-11-30 19:16:14 +00:00
|
|
|
sent.append(list(line.strip().split("\t")))
|
2018-03-10 22:41:55 +00:00
|
|
|
if len(sent[-1]) != 10:
|
|
|
|
print(repr(line))
|
|
|
|
raise ValueError
|
|
|
|
if sent:
|
|
|
|
doc.append(sent)
|
|
|
|
if doc:
|
|
|
|
docs.append(doc)
|
|
|
|
return docs
|
|
|
|
|
|
|
|
|
2018-05-07 13:52:47 +00:00
|
|
|
def _make_gold(nlp, text, sent_annots, drop_deps=0.0):
|
2018-03-10 22:41:55 +00:00
|
|
|
# Flatten the conll annotations, and adjust the head indices
|
2020-06-26 17:34:12 +00:00
|
|
|
gold = defaultdict(list)
|
2018-05-07 13:52:47 +00:00
|
|
|
sent_starts = []
|
2018-03-10 22:41:55 +00:00
|
|
|
for sent in sent_annots:
|
2020-06-26 17:34:12 +00:00
|
|
|
gold["heads"].extend(len(gold["words"])+head for head in sent["heads"])
|
2020-06-29 12:33:00 +00:00
|
|
|
for field in ["words", "tags", "deps", "morphs", "entities", "spaces"]:
|
2020-06-26 17:34:12 +00:00
|
|
|
gold[field].extend(sent[field])
|
2018-05-07 13:52:47 +00:00
|
|
|
sent_starts.append(True)
|
2018-11-30 19:16:14 +00:00
|
|
|
sent_starts.extend([False] * (len(sent["words"]) - 1))
|
2018-03-10 22:41:55 +00:00
|
|
|
# Construct text if necessary
|
2020-06-26 17:34:12 +00:00
|
|
|
assert len(gold["words"]) == len(gold["spaces"])
|
2018-03-10 22:41:55 +00:00
|
|
|
if text is None:
|
2018-11-30 19:16:14 +00:00
|
|
|
text = "".join(
|
2020-06-26 17:34:12 +00:00
|
|
|
word + " " * space for word, space in zip(gold["words"], gold["spaces"])
|
2018-11-30 19:16:14 +00:00
|
|
|
)
|
2018-03-10 22:41:55 +00:00
|
|
|
doc = nlp.make_doc(text)
|
2020-06-26 17:34:12 +00:00
|
|
|
gold.pop("spaces")
|
|
|
|
gold["sent_starts"] = sent_starts
|
2020-06-29 12:33:00 +00:00
|
|
|
for i in range(len(gold["heads"])):
|
2018-05-07 13:52:47 +00:00
|
|
|
if random.random() < drop_deps:
|
2020-06-26 17:34:12 +00:00
|
|
|
gold["heads"][i] = None
|
|
|
|
gold["labels"][i] = None
|
2018-05-07 13:52:47 +00:00
|
|
|
|
2018-03-10 22:41:55 +00:00
|
|
|
return doc, gold
|
|
|
|
|
2018-11-30 19:16:14 +00:00
|
|
|
|
2018-03-10 22:41:55 +00:00
|
|
|
#############################
|
|
|
|
# Data transforms for spaCy #
|
|
|
|
#############################
|
|
|
|
|
2018-11-30 19:16:14 +00:00
|
|
|
|
2019-11-11 16:35:27 +00:00
|
|
|
def golds_to_gold_data(docs, golds):
|
2020-06-26 17:34:12 +00:00
|
|
|
"""Get out the training data format used by begin_training"""
|
2019-11-11 16:35:27 +00:00
|
|
|
data = []
|
2018-03-10 22:41:55 +00:00
|
|
|
for doc, gold in zip(docs, golds):
|
2020-06-29 12:33:00 +00:00
|
|
|
example = Example.from_dict(doc, dict(gold))
|
2019-11-11 16:35:27 +00:00
|
|
|
data.append(example)
|
|
|
|
return data
|
2018-03-10 22:41:55 +00:00
|
|
|
|
|
|
|
|
|
|
|
##############
|
|
|
|
# Evaluation #
|
|
|
|
##############
|
|
|
|
|
2018-11-30 19:16:14 +00:00
|
|
|
|
2018-03-10 22:41:55 +00:00
|
|
|
def evaluate(nlp, text_loc, gold_loc, sys_loc, limit=None):
|
2018-11-30 19:16:14 +00:00
|
|
|
if text_loc.parts[-1].endswith(".conllu"):
|
2018-05-08 11:47:45 +00:00
|
|
|
docs = []
|
2019-10-29 12:16:55 +00:00
|
|
|
with text_loc.open(encoding="utf8") as file_:
|
2018-05-08 11:47:45 +00:00
|
|
|
for conllu_doc in read_conllu(file_):
|
|
|
|
for conllu_sent in conllu_doc:
|
|
|
|
words = [line[1] for line in conllu_sent]
|
|
|
|
docs.append(Doc(nlp.vocab, words=words))
|
|
|
|
for name, component in nlp.pipeline:
|
|
|
|
docs = list(component.pipe(docs))
|
|
|
|
else:
|
2018-11-30 19:16:14 +00:00
|
|
|
with text_loc.open("r", encoding="utf8") as text_file:
|
2018-05-08 11:47:45 +00:00
|
|
|
texts = split_text(text_file.read())
|
|
|
|
docs = list(nlp.pipe(texts))
|
2018-11-30 19:16:14 +00:00
|
|
|
with sys_loc.open("w", encoding="utf8") as out_file:
|
2018-03-10 22:41:55 +00:00
|
|
|
write_conllu(docs, out_file)
|
2018-11-30 19:16:14 +00:00
|
|
|
with gold_loc.open("r", encoding="utf8") as gold_file:
|
2018-03-10 22:41:55 +00:00
|
|
|
gold_ud = conll17_ud_eval.load_conllu(gold_file)
|
2018-11-30 19:16:14 +00:00
|
|
|
with sys_loc.open("r", encoding="utf8") as sys_file:
|
2018-03-10 22:41:55 +00:00
|
|
|
sys_ud = conll17_ud_eval.load_conllu(sys_file)
|
|
|
|
scores = conll17_ud_eval.evaluate(gold_ud, sys_ud)
|
Improve label management in parser and NER (#2108)
This patch does a few smallish things that tighten up the training workflow a little, and allow memory use during training to be reduced by letting the GoldCorpus stream data properly.
Previously, the parser and entity recognizer read and saved labels as lists, with extra labels noted separately. Lists were used becaue ordering is very important, to ensure that the label-to-class mapping is stable.
We now manage labels as nested dictionaries, first keyed by the action, and then keyed by the label. Values are frequencies. The trick is, how do we save new labels? We need to make sure we iterate over these in the same order they're added. Otherwise, we'll get different class IDs, and the model's predictions won't make sense.
To allow stable sorting, we map the new labels to negative values. If we have two new labels, they'll be noted as having "frequency" -1 and -2. The next new label will then have "frequency" -3. When we sort by (frequency, label), we then get a stable sort.
Storing frequencies then allows us to make the next nice improvement. Previously we had to iterate over the whole training set, to pre-process it for the deprojectivisation. This led to storing the whole training set in memory. This was most of the required memory during training.
To prevent this, we now store the frequencies as we stream in the data, and deprojectivize as we go. Once we've built the frequencies, we can then apply a frequency cut-off when we decide how many classes to make.
Finally, to allow proper data streaming, we also have to have some way of shuffling the iterator. This is awkward if the training files have multiple documents in them. To solve this, the GoldCorpus class now writes the training data to disk in msgpack files, one per document. We can then shuffle the data by shuffling the paths.
This is a squash merge, as I made a lot of very small commits. Individual commit messages below.
* Simplify label management for TransitionSystem and its subclasses
* Fix serialization for new label handling format in parser
* Simplify and improve GoldCorpus class. Reduce memory use, write to temp dir
* Set actions in transition system
* Require thinc 6.11.1.dev4
* Fix error in parser init
* Add unicode declaration
* Fix unicode declaration
* Update textcat test
* Try to get model training on less memory
* Print json loc for now
* Try rapidjson to reduce memory use
* Remove rapidjson requirement
* Try rapidjson for reduced mem usage
* Handle None heads when projectivising
* Stream json docs
* Fix train script
* Handle projectivity in GoldParse
* Fix projectivity handling
* Add minibatch_by_words util from ud_train
* Minibatch by number of words in spacy.cli.train
* Move minibatch_by_words util to spacy.util
* Fix label handling
* More hacking at label management in parser
* Fix encoding in msgpack serialization in GoldParse
* Adjust batch sizes in parser training
* Fix minibatch_by_words
* Add merge_subtokens function to pipeline.pyx
* Register merge_subtokens factory
* Restore use of msgpack tmp directory
* Use minibatch-by-words in train
* Handle retokenization in scorer
* Change back-off approach for missing labels. Use 'dep' label
* Update NER for new label management
* Set NER tags for over-segmented words
* Fix label alignment in gold
* Fix label back-off for infrequent labels
* Fix int type in labels dict key
* Fix int type in labels dict key
* Update feature definition for 8 feature set
* Update ud-train script for new label stuff
* Fix json streamer
* Print the line number if conll eval fails
* Update children and sentence boundaries after deprojectivisation
* Export set_children_from_heads from doc.pxd
* Render parses during UD training
* Remove print statement
* Require thinc 6.11.1.dev6. Try adding wheel as install_requires
* Set different dev version, to flush pip cache
* Update thinc version
* Update GoldCorpus docs
* Remove print statements
* Fix formatting and links [ci skip]
2018-03-19 01:58:08 +00:00
|
|
|
return docs, scores
|
2018-03-10 22:41:55 +00:00
|
|
|
|
|
|
|
|
|
|
|
def write_conllu(docs, file_):
|
2019-11-11 15:25:03 +00:00
|
|
|
if not Token.has_extension("get_conllu_lines"):
|
|
|
|
Token.set_extension("get_conllu_lines", method=get_token_conllu)
|
|
|
|
if not Token.has_extension("begins_fused"):
|
|
|
|
Token.set_extension("begins_fused", default=False)
|
|
|
|
if not Token.has_extension("inside_fused"):
|
|
|
|
Token.set_extension("inside_fused", default=False)
|
|
|
|
|
2018-03-10 22:41:55 +00:00
|
|
|
merger = Matcher(docs[0].vocab)
|
2018-11-30 19:16:14 +00:00
|
|
|
merger.add("SUBTOK", None, [{"DEP": "subtok", "op": "+"}])
|
2018-03-10 22:41:55 +00:00
|
|
|
for i, doc in enumerate(docs):
|
2019-09-27 14:20:38 +00:00
|
|
|
matches = []
|
|
|
|
if doc.is_parsed:
|
|
|
|
matches = merger(doc)
|
2018-11-30 19:16:14 +00:00
|
|
|
spans = [doc[start : end + 1] for _, start, end in matches]
|
2019-03-09 00:41:34 +00:00
|
|
|
seen_tokens = set()
|
2019-02-15 09:29:44 +00:00
|
|
|
with doc.retokenize() as retokenizer:
|
|
|
|
for span in spans:
|
2019-03-09 00:41:34 +00:00
|
|
|
span_tokens = set(range(span.start, span.end))
|
|
|
|
if not span_tokens.intersection(seen_tokens):
|
|
|
|
retokenizer.merge(span)
|
|
|
|
seen_tokens.update(span_tokens)
|
|
|
|
|
2018-03-10 22:41:55 +00:00
|
|
|
file_.write("# newdoc id = {i}\n".format(i=i))
|
|
|
|
for j, sent in enumerate(doc.sents):
|
|
|
|
file_.write("# sent_id = {i}.{j}\n".format(i=i, j=j))
|
|
|
|
file_.write("# text = {text}\n".format(text=sent.text))
|
|
|
|
for k, token in enumerate(sent):
|
2018-09-13 22:54:59 +00:00
|
|
|
if token.head.i > sent[-1].i or token.head.i < sent[0].i:
|
2018-11-30 19:16:14 +00:00
|
|
|
for word in doc[sent[0].i - 10 : sent[0].i]:
|
2018-09-13 22:54:59 +00:00
|
|
|
print(word.i, word.head.i, word.text, word.dep_)
|
|
|
|
for word in sent:
|
|
|
|
print(word.i, word.head.i, word.text, word.dep_)
|
2018-11-30 19:16:14 +00:00
|
|
|
for word in doc[sent[-1].i : sent[-1].i + 10]:
|
2018-09-13 22:54:59 +00:00
|
|
|
print(word.i, word.head.i, word.text, word.dep_)
|
2018-11-30 19:16:14 +00:00
|
|
|
raise ValueError(
|
|
|
|
"Invalid parse: head outside sentence (%s)" % token.text
|
|
|
|
)
|
|
|
|
file_.write(token._.get_conllu_lines(k) + "\n")
|
|
|
|
file_.write("\n")
|
2018-03-10 22:41:55 +00:00
|
|
|
|
|
|
|
|
|
|
|
def print_progress(itn, losses, ud_scores):
|
|
|
|
fields = {
|
2018-11-30 19:16:14 +00:00
|
|
|
"dep_loss": losses.get("parser", 0.0),
|
2019-03-07 00:31:23 +00:00
|
|
|
"morph_loss": losses.get("morphologizer", 0.0),
|
2018-11-30 19:16:14 +00:00
|
|
|
"tag_loss": losses.get("tagger", 0.0),
|
|
|
|
"words": ud_scores["Words"].f1 * 100,
|
|
|
|
"sents": ud_scores["Sentences"].f1 * 100,
|
|
|
|
"tags": ud_scores["XPOS"].f1 * 100,
|
|
|
|
"uas": ud_scores["UAS"].f1 * 100,
|
|
|
|
"las": ud_scores["LAS"].f1 * 100,
|
2019-03-07 00:31:23 +00:00
|
|
|
"morph": ud_scores["Feats"].f1 * 100,
|
2018-03-10 22:41:55 +00:00
|
|
|
}
|
2019-03-07 00:31:23 +00:00
|
|
|
header = ["Epoch", "P.Loss", "M.Loss", "LAS", "UAS", "TAG", "MORPH", "SENT", "WORD"]
|
2018-03-10 22:41:55 +00:00
|
|
|
if itn == 0:
|
2018-11-30 19:16:14 +00:00
|
|
|
print("\t".join(header))
|
2019-03-07 00:31:23 +00:00
|
|
|
tpl = "\t".join((
|
|
|
|
"{:d}",
|
|
|
|
"{dep_loss:.1f}",
|
|
|
|
"{morph_loss:.1f}",
|
|
|
|
"{las:.1f}",
|
|
|
|
"{uas:.1f}",
|
|
|
|
"{tags:.1f}",
|
|
|
|
"{morph:.1f}",
|
|
|
|
"{sents:.1f}",
|
|
|
|
"{words:.1f}",
|
|
|
|
))
|
2018-03-10 22:41:55 +00:00
|
|
|
print(tpl.format(itn, **fields))
|
|
|
|
|
2018-11-30 19:16:14 +00:00
|
|
|
|
|
|
|
# def get_sent_conllu(sent, sent_id):
|
2018-03-10 22:41:55 +00:00
|
|
|
# lines = ["# sent_id = {sent_id}".format(sent_id=sent_id)]
|
|
|
|
|
2018-11-30 19:16:14 +00:00
|
|
|
|
2018-03-10 22:41:55 +00:00
|
|
|
def get_token_conllu(token, i):
|
|
|
|
if token._.begins_fused:
|
|
|
|
n = 1
|
|
|
|
while token.nbor(n)._.inside_fused:
|
|
|
|
n += 1
|
2018-11-30 19:16:14 +00:00
|
|
|
id_ = "%d-%d" % (i, i + n)
|
|
|
|
lines = [id_, token.text, "_", "_", "_", "_", "_", "_", "_", "_"]
|
2018-03-10 22:41:55 +00:00
|
|
|
else:
|
|
|
|
lines = []
|
|
|
|
if token.head.i == token.i:
|
|
|
|
head = 0
|
|
|
|
else:
|
|
|
|
head = i + (token.head.i - token.i) + 1
|
2019-03-08 17:54:25 +00:00
|
|
|
features = list(token.morph)
|
2018-09-26 19:02:42 +00:00
|
|
|
feat_str = []
|
2019-03-07 00:31:23 +00:00
|
|
|
replacements = {"one": "1", "two": "2", "three": "3"}
|
2018-09-26 19:02:42 +00:00
|
|
|
for feat in features:
|
2020-07-19 09:10:31 +00:00
|
|
|
if "=" in feat:
|
|
|
|
feat_str.append(feat)
|
|
|
|
elif not feat.startswith("begin") and not feat.startswith("end"):
|
2019-03-09 00:20:11 +00:00
|
|
|
key, value = feat.split("_", 1)
|
2018-09-26 19:02:42 +00:00
|
|
|
value = replacements.get(value, value)
|
2019-03-07 00:31:23 +00:00
|
|
|
feat_str.append("%s=%s" % (key, value.title()))
|
2018-09-26 19:02:42 +00:00
|
|
|
if not feat_str:
|
2019-03-07 00:31:23 +00:00
|
|
|
feat_str = "_"
|
2018-09-26 19:02:42 +00:00
|
|
|
else:
|
2019-03-07 00:31:23 +00:00
|
|
|
feat_str = "|".join(feat_str)
|
2018-09-26 19:02:42 +00:00
|
|
|
fields = [str(i+1), token.text, token.lemma_, token.pos_, token.tag_, feat_str,
|
2019-03-07 00:31:23 +00:00
|
|
|
str(head), token.dep_.lower(), "_", "_"]
|
2018-11-30 19:16:14 +00:00
|
|
|
lines.append("\t".join(fields))
|
|
|
|
return "\n".join(lines)
|
|
|
|
|
2019-09-27 14:20:38 +00:00
|
|
|
|
2018-03-10 22:41:55 +00:00
|
|
|
##################
|
|
|
|
# Initialization #
|
|
|
|
##################
|
|
|
|
|
|
|
|
|
2018-04-29 13:49:32 +00:00
|
|
|
def load_nlp(corpus, config, vectors=None):
|
2018-11-30 19:16:14 +00:00
|
|
|
lang = corpus.split("_")[0]
|
2018-03-10 22:41:55 +00:00
|
|
|
nlp = spacy.blank(lang)
|
|
|
|
if config.vectors:
|
2018-11-30 19:16:14 +00:00
|
|
|
if not vectors:
|
|
|
|
raise ValueError(
|
|
|
|
"config asks for vectors, but no vectors "
|
|
|
|
"directory set on command line (use -v)"
|
|
|
|
)
|
2018-04-29 13:49:32 +00:00
|
|
|
if (Path(vectors) / corpus).exists():
|
2018-11-30 19:16:14 +00:00
|
|
|
nlp.vocab.from_disk(Path(vectors) / corpus / "vocab")
|
|
|
|
nlp.meta["treebank"] = corpus
|
2018-03-10 22:41:55 +00:00
|
|
|
return nlp
|
2018-11-30 19:16:14 +00:00
|
|
|
|
2018-03-10 22:41:55 +00:00
|
|
|
|
2019-11-11 16:35:27 +00:00
|
|
|
def initialize_pipeline(nlp, examples, config, device):
|
2019-03-09 00:20:11 +00:00
|
|
|
nlp.add_pipe(nlp.create_pipe("tagger", config={"set_morphology": False}))
|
2019-03-07 00:31:23 +00:00
|
|
|
nlp.add_pipe(nlp.create_pipe("morphologizer"))
|
2018-11-30 19:16:14 +00:00
|
|
|
nlp.add_pipe(nlp.create_pipe("parser"))
|
2018-03-10 22:41:55 +00:00
|
|
|
if config.multitask_tag:
|
2018-11-30 19:16:14 +00:00
|
|
|
nlp.parser.add_multitask_objective("tag")
|
2018-03-10 22:41:55 +00:00
|
|
|
if config.multitask_sent:
|
2018-11-30 19:16:14 +00:00
|
|
|
nlp.parser.add_multitask_objective("sent_start")
|
2020-06-26 17:34:12 +00:00
|
|
|
for eg in examples:
|
2020-06-29 12:33:00 +00:00
|
|
|
for tag in eg.get_aligned("TAG", as_string=True):
|
2018-03-10 22:41:55 +00:00
|
|
|
if tag is not None:
|
|
|
|
nlp.tagger.add_label(tag)
|
2018-09-13 22:54:59 +00:00
|
|
|
if torch is not None and device != -1:
|
2018-11-30 19:16:14 +00:00
|
|
|
torch.set_default_tensor_type("torch.cuda.FloatTensor")
|
2018-11-29 14:54:47 +00:00
|
|
|
optimizer = nlp.begin_training(
|
2019-11-11 16:35:27 +00:00
|
|
|
lambda: examples,
|
2018-11-30 19:16:14 +00:00
|
|
|
device=device,
|
|
|
|
subword_features=config.subword_features,
|
|
|
|
conv_depth=config.conv_depth,
|
|
|
|
bilstm_depth=config.bilstm_depth,
|
|
|
|
)
|
2018-11-29 14:54:47 +00:00
|
|
|
if config.pretrained_tok2vec:
|
|
|
|
_load_pretrained_tok2vec(nlp, config.pretrained_tok2vec)
|
|
|
|
return optimizer
|
|
|
|
|
|
|
|
|
|
|
|
def _load_pretrained_tok2vec(nlp, loc):
|
2019-10-02 08:37:39 +00:00
|
|
|
"""Load pretrained weights for the 'token-to-vector' part of the component
|
2018-11-29 14:54:47 +00:00
|
|
|
models, which is typically a CNN. See 'spacy pretrain'. Experimental.
|
|
|
|
"""
|
2019-10-29 12:16:55 +00:00
|
|
|
with Path(loc).open("rb", encoding="utf8") as file_:
|
2018-11-29 14:54:47 +00:00
|
|
|
weights_data = file_.read()
|
|
|
|
loaded = []
|
|
|
|
for name, component in nlp.pipeline:
|
2020-02-27 17:42:27 +00:00
|
|
|
if hasattr(component, "model") and component.model.has_ref("tok2vec"):
|
|
|
|
component.get_ref("tok2vec").from_bytes(weights_data)
|
2018-11-29 14:54:47 +00:00
|
|
|
loaded.append(name)
|
|
|
|
return loaded
|
2018-03-10 22:41:55 +00:00
|
|
|
|
|
|
|
|
|
|
|
########################
|
|
|
|
# Command line helpers #
|
|
|
|
########################
|
|
|
|
|
2018-11-30 19:16:14 +00:00
|
|
|
|
2018-03-10 22:41:55 +00:00
|
|
|
class Config(object):
|
2018-11-30 19:16:14 +00:00
|
|
|
def __init__(
|
|
|
|
self,
|
|
|
|
vectors=None,
|
|
|
|
max_doc_length=10,
|
|
|
|
multitask_tag=False,
|
|
|
|
multitask_sent=False,
|
|
|
|
multitask_dep=False,
|
|
|
|
multitask_vectors=None,
|
|
|
|
bilstm_depth=0,
|
|
|
|
nr_epoch=30,
|
|
|
|
min_batch_size=100,
|
|
|
|
max_batch_size=1000,
|
|
|
|
batch_by_words=True,
|
|
|
|
dropout=0.2,
|
|
|
|
conv_depth=4,
|
|
|
|
subword_features=True,
|
|
|
|
vectors_dir=None,
|
|
|
|
pretrained_tok2vec=None,
|
|
|
|
):
|
2018-09-13 16:05:48 +00:00
|
|
|
if vectors_dir is not None:
|
|
|
|
if vectors is None:
|
|
|
|
vectors = True
|
|
|
|
if multitask_vectors is None:
|
|
|
|
multitask_vectors = True
|
2018-03-11 00:26:45 +00:00
|
|
|
for key, value in locals().items():
|
2018-03-10 23:59:39 +00:00
|
|
|
setattr(self, key, value)
|
2018-11-30 19:16:14 +00:00
|
|
|
|
2018-03-10 22:41:55 +00:00
|
|
|
@classmethod
|
2018-09-13 16:05:48 +00:00
|
|
|
def load(cls, loc, vectors_dir=None):
|
2018-11-30 19:16:14 +00:00
|
|
|
with Path(loc).open("r", encoding="utf8") as file_:
|
2018-03-10 22:41:55 +00:00
|
|
|
cfg = json.load(file_)
|
2018-09-13 16:05:48 +00:00
|
|
|
if vectors_dir is not None:
|
2018-11-30 19:16:14 +00:00
|
|
|
cfg["vectors_dir"] = vectors_dir
|
2018-03-10 22:41:55 +00:00
|
|
|
return cls(**cfg)
|
|
|
|
|
|
|
|
|
|
|
|
class Dataset(object):
|
|
|
|
def __init__(self, path, section):
|
|
|
|
self.path = path
|
|
|
|
self.section = section
|
|
|
|
self.conllu = None
|
|
|
|
self.text = None
|
|
|
|
for file_path in self.path.iterdir():
|
|
|
|
name = file_path.parts[-1]
|
2018-11-30 19:16:14 +00:00
|
|
|
if section in name and name.endswith("conllu"):
|
2018-03-10 22:41:55 +00:00
|
|
|
self.conllu = file_path
|
2018-11-30 19:16:14 +00:00
|
|
|
elif section in name and name.endswith("txt"):
|
2018-03-10 22:41:55 +00:00
|
|
|
self.text = file_path
|
|
|
|
if self.conllu is None:
|
|
|
|
msg = "Could not find .txt file in {path} for {section}"
|
|
|
|
raise IOError(msg.format(section=section, path=path))
|
|
|
|
if self.text is None:
|
|
|
|
msg = "Could not find .txt file in {path} for {section}"
|
2018-11-30 19:16:14 +00:00
|
|
|
self.lang = self.conllu.parts[-1].split("-")[0].split("_")[0]
|
2018-03-10 22:41:55 +00:00
|
|
|
|
|
|
|
|
|
|
|
class TreebankPaths(object):
|
|
|
|
def __init__(self, ud_path, treebank, **cfg):
|
2018-11-30 19:16:14 +00:00
|
|
|
self.train = Dataset(ud_path / treebank, "train")
|
|
|
|
self.dev = Dataset(ud_path / treebank, "dev")
|
2018-03-10 22:41:55 +00:00
|
|
|
self.lang = self.train.lang
|
|
|
|
|
|
|
|
|
|
|
|
@plac.annotations(
|
|
|
|
ud_dir=("Path to Universal Dependencies corpus", "positional", None, Path),
|
Generalize handling of tokenizer special cases (#4259)
* Generalize handling of tokenizer special cases
Handle tokenizer special cases more generally by using the Matcher
internally to match special cases after the affix/token_match
tokenization is complete.
Instead of only matching special cases while processing balanced or
nearly balanced prefixes and suffixes, this recognizes special cases in
a wider range of contexts:
* Allows arbitrary numbers of prefixes/affixes around special cases
* Allows special cases separated by infixes
Existing tests/settings that couldn't be preserved as before:
* The emoticon '")' is no longer a supported special case
* The emoticon ':)' in "example:)" is a false positive again
When merged with #4258 (or the relevant cache bugfix), the affix and
token_match properties should be modified to flush and reload all
special cases to use the updated internal tokenization with the Matcher.
* Remove accidentally added test case
* Really remove accidentally added test
* Reload special cases when necessary
Reload special cases when affixes or token_match are modified. Skip
reloading during initialization.
* Update error code number
* Fix offset and whitespace in Matcher special cases
* Fix offset bugs when merging and splitting tokens
* Set final whitespace on final token in inserted special case
* Improve cache flushing in tokenizer
* Separate cache and specials memory (temporarily)
* Flush cache when adding special cases
* Repeated `self._cache = PreshMap()` and `self._specials = PreshMap()`
are necessary due to this bug:
https://github.com/explosion/preshed/issues/21
* Remove reinitialized PreshMaps on cache flush
* Update UD bin scripts
* Update imports for `bin/`
* Add all currently supported languages
* Update subtok merger for new Matcher validation
* Modify blinded check to look at tokens instead of lemmas (for corpora
with tokens but not lemmas like Telugu)
* Use special Matcher only for cases with affixes
* Reinsert specials cache checks during normal tokenization for special
cases as much as possible
* Additionally include specials cache checks while splitting on infixes
* Since the special Matcher needs consistent affix-only tokenization
for the special cases themselves, introduce the argument
`with_special_cases` in order to do tokenization with or without
specials cache checks
* After normal tokenization, postprocess with special cases Matcher for
special cases containing affixes
* Replace PhraseMatcher with Aho-Corasick
Replace PhraseMatcher with the Aho-Corasick algorithm over numpy arrays
of the hash values for the relevant attribute. The implementation is
based on FlashText.
The speed should be similar to the previous PhraseMatcher. It is now
possible to easily remove match IDs and matches don't go missing with
large keyword lists / vocabularies.
Fixes #4308.
* Restore support for pickling
* Fix internal keyword add/remove for numpy arrays
* Add test for #4248, clean up test
* Improve efficiency of special cases handling
* Use PhraseMatcher instead of Matcher
* Improve efficiency of merging/splitting special cases in document
* Process merge/splits in one pass without repeated token shifting
* Merge in place if no splits
* Update error message number
* Remove UD script modifications
Only used for timing/testing, should be a separate PR
* Remove final traces of UD script modifications
* Update UD bin scripts
* Update imports for `bin/`
* Add all currently supported languages
* Update subtok merger for new Matcher validation
* Modify blinded check to look at tokens instead of lemmas (for corpora
with tokens but not lemmas like Telugu)
* Add missing loop for match ID set in search loop
* Remove cruft in matching loop for partial matches
There was a bit of unnecessary code left over from FlashText in the
matching loop to handle partial token matches, which we don't have with
PhraseMatcher.
* Replace dict trie with MapStruct trie
* Fix how match ID hash is stored/added
* Update fix for match ID vocab
* Switch from map_get_unless_missing to map_get
* Switch from numpy array to Token.get_struct_attr
Access token attributes directly in Doc instead of making a copy of the
relevant values in a numpy array.
Add unsatisfactory warning for hash collision with reserved terminal
hash key. (Ideally it would change the reserved terminal hash and redo
the whole trie, but for now, I'm hoping there won't be collisions.)
* Restructure imports to export find_matches
* Implement full remove()
Remove unnecessary trie paths and free unused maps.
Parallel to Matcher, raise KeyError when attempting to remove a match ID
that has not been added.
* Switch to PhraseMatcher.find_matches
* Switch to local cdef functions for span filtering
* Switch special case reload threshold to variable
Refer to variable instead of hard-coded threshold
* Move more of special case retokenize to cdef nogil
Move as much of the special case retokenization to nogil as possible.
* Rewrap sort as stdsort for OS X
* Rewrap stdsort with specific types
* Switch to qsort
* Fix merge
* Improve cmp functions
* Fix realloc
* Fix realloc again
* Initialize span struct while retokenizing
* Temporarily skip retokenizing
* Revert "Move more of special case retokenize to cdef nogil"
This reverts commit 0b7e52c797cd8ff1548f214bd4186ebb3a7ce8b1.
* Revert "Switch to qsort"
This reverts commit a98d71a942fc9bca531cf5eb05cf89fa88153b60.
* Fix specials check while caching
* Modify URL test with emoticons
The multiple suffix tests result in the emoticon `:>`, which is now
retokenized into one token as a special case after the suffixes are
split off.
* Refactor _apply_special_cases()
* Use cdef ints for span info used in multiple spots
* Modify _filter_special_spans() to prefer earlier
Parallel to #4414, modify _filter_special_spans() so that the earlier
span is preferred for overlapping spans of the same length.
* Replace MatchStruct with Entity
Replace MatchStruct with Entity since the existing Entity struct is
nearly identical.
* Replace Entity with more general SpanC
* Replace MatchStruct with SpanC
* Add error in debug-data if no dev docs are available (see #4575)
* Update azure-pipelines.yml
* Revert "Update azure-pipelines.yml"
This reverts commit ed1060cf59e5895b5fe92ad5b894fd1078ec4c49.
* Use latest wasabi
* Reorganise install_requires
* add dframcy to universe.json (#4580)
* Update universe.json [ci skip]
* Fix multiprocessing for as_tuples=True (#4582)
* Fix conllu script (#4579)
* force extensions to avoid clash between example scripts
* fix arg order and default file encoding
* add example config for conllu script
* newline
* move extension definitions to main function
* few more encodings fixes
* Add load_from_docbin example [ci skip]
TODO: upload the file somewhere
* Update README.md
* Add warnings about 3.8 (resolves #4593) [ci skip]
* Fixed typo: Added space between "recognize" and "various" (#4600)
* Fix DocBin.merge() example (#4599)
* Replace function registries with catalogue (#4584)
* Replace functions registries with catalogue
* Update __init__.py
* Fix test
* Revert unrelated flag [ci skip]
* Bugfix/dep matcher issue 4590 (#4601)
* add contributor agreement for prilopes
* add test for issue #4590
* fix on_match params for DependencyMacther (#4590)
* Minor updates to language example sentences (#4608)
* Add punctuation to Spanish example sentences
* Combine multilanguage examples for lang xx
* Add punctuation to nb examples
* Always realloc to a larger size
Avoid potential (unlikely) edge case and cymem error seen in #4604.
* Add error in debug-data if no dev docs are available (see #4575)
* Update debug-data for GoldCorpus / Example
* Ignore None label in misaligned NER data
2019-11-13 20:24:35 +00:00
|
|
|
parses_dir=("Directory to write the development parses", "positional", None, Path),
|
2018-11-30 19:16:14 +00:00
|
|
|
corpus=(
|
Generalize handling of tokenizer special cases (#4259)
* Generalize handling of tokenizer special cases
Handle tokenizer special cases more generally by using the Matcher
internally to match special cases after the affix/token_match
tokenization is complete.
Instead of only matching special cases while processing balanced or
nearly balanced prefixes and suffixes, this recognizes special cases in
a wider range of contexts:
* Allows arbitrary numbers of prefixes/affixes around special cases
* Allows special cases separated by infixes
Existing tests/settings that couldn't be preserved as before:
* The emoticon '")' is no longer a supported special case
* The emoticon ':)' in "example:)" is a false positive again
When merged with #4258 (or the relevant cache bugfix), the affix and
token_match properties should be modified to flush and reload all
special cases to use the updated internal tokenization with the Matcher.
* Remove accidentally added test case
* Really remove accidentally added test
* Reload special cases when necessary
Reload special cases when affixes or token_match are modified. Skip
reloading during initialization.
* Update error code number
* Fix offset and whitespace in Matcher special cases
* Fix offset bugs when merging and splitting tokens
* Set final whitespace on final token in inserted special case
* Improve cache flushing in tokenizer
* Separate cache and specials memory (temporarily)
* Flush cache when adding special cases
* Repeated `self._cache = PreshMap()` and `self._specials = PreshMap()`
are necessary due to this bug:
https://github.com/explosion/preshed/issues/21
* Remove reinitialized PreshMaps on cache flush
* Update UD bin scripts
* Update imports for `bin/`
* Add all currently supported languages
* Update subtok merger for new Matcher validation
* Modify blinded check to look at tokens instead of lemmas (for corpora
with tokens but not lemmas like Telugu)
* Use special Matcher only for cases with affixes
* Reinsert specials cache checks during normal tokenization for special
cases as much as possible
* Additionally include specials cache checks while splitting on infixes
* Since the special Matcher needs consistent affix-only tokenization
for the special cases themselves, introduce the argument
`with_special_cases` in order to do tokenization with or without
specials cache checks
* After normal tokenization, postprocess with special cases Matcher for
special cases containing affixes
* Replace PhraseMatcher with Aho-Corasick
Replace PhraseMatcher with the Aho-Corasick algorithm over numpy arrays
of the hash values for the relevant attribute. The implementation is
based on FlashText.
The speed should be similar to the previous PhraseMatcher. It is now
possible to easily remove match IDs and matches don't go missing with
large keyword lists / vocabularies.
Fixes #4308.
* Restore support for pickling
* Fix internal keyword add/remove for numpy arrays
* Add test for #4248, clean up test
* Improve efficiency of special cases handling
* Use PhraseMatcher instead of Matcher
* Improve efficiency of merging/splitting special cases in document
* Process merge/splits in one pass without repeated token shifting
* Merge in place if no splits
* Update error message number
* Remove UD script modifications
Only used for timing/testing, should be a separate PR
* Remove final traces of UD script modifications
* Update UD bin scripts
* Update imports for `bin/`
* Add all currently supported languages
* Update subtok merger for new Matcher validation
* Modify blinded check to look at tokens instead of lemmas (for corpora
with tokens but not lemmas like Telugu)
* Add missing loop for match ID set in search loop
* Remove cruft in matching loop for partial matches
There was a bit of unnecessary code left over from FlashText in the
matching loop to handle partial token matches, which we don't have with
PhraseMatcher.
* Replace dict trie with MapStruct trie
* Fix how match ID hash is stored/added
* Update fix for match ID vocab
* Switch from map_get_unless_missing to map_get
* Switch from numpy array to Token.get_struct_attr
Access token attributes directly in Doc instead of making a copy of the
relevant values in a numpy array.
Add unsatisfactory warning for hash collision with reserved terminal
hash key. (Ideally it would change the reserved terminal hash and redo
the whole trie, but for now, I'm hoping there won't be collisions.)
* Restructure imports to export find_matches
* Implement full remove()
Remove unnecessary trie paths and free unused maps.
Parallel to Matcher, raise KeyError when attempting to remove a match ID
that has not been added.
* Switch to PhraseMatcher.find_matches
* Switch to local cdef functions for span filtering
* Switch special case reload threshold to variable
Refer to variable instead of hard-coded threshold
* Move more of special case retokenize to cdef nogil
Move as much of the special case retokenization to nogil as possible.
* Rewrap sort as stdsort for OS X
* Rewrap stdsort with specific types
* Switch to qsort
* Fix merge
* Improve cmp functions
* Fix realloc
* Fix realloc again
* Initialize span struct while retokenizing
* Temporarily skip retokenizing
* Revert "Move more of special case retokenize to cdef nogil"
This reverts commit 0b7e52c797cd8ff1548f214bd4186ebb3a7ce8b1.
* Revert "Switch to qsort"
This reverts commit a98d71a942fc9bca531cf5eb05cf89fa88153b60.
* Fix specials check while caching
* Modify URL test with emoticons
The multiple suffix tests result in the emoticon `:>`, which is now
retokenized into one token as a special case after the suffixes are
split off.
* Refactor _apply_special_cases()
* Use cdef ints for span info used in multiple spots
* Modify _filter_special_spans() to prefer earlier
Parallel to #4414, modify _filter_special_spans() so that the earlier
span is preferred for overlapping spans of the same length.
* Replace MatchStruct with Entity
Replace MatchStruct with Entity since the existing Entity struct is
nearly identical.
* Replace Entity with more general SpanC
* Replace MatchStruct with SpanC
* Add error in debug-data if no dev docs are available (see #4575)
* Update azure-pipelines.yml
* Revert "Update azure-pipelines.yml"
This reverts commit ed1060cf59e5895b5fe92ad5b894fd1078ec4c49.
* Use latest wasabi
* Reorganise install_requires
* add dframcy to universe.json (#4580)
* Update universe.json [ci skip]
* Fix multiprocessing for as_tuples=True (#4582)
* Fix conllu script (#4579)
* force extensions to avoid clash between example scripts
* fix arg order and default file encoding
* add example config for conllu script
* newline
* move extension definitions to main function
* few more encodings fixes
* Add load_from_docbin example [ci skip]
TODO: upload the file somewhere
* Update README.md
* Add warnings about 3.8 (resolves #4593) [ci skip]
* Fixed typo: Added space between "recognize" and "various" (#4600)
* Fix DocBin.merge() example (#4599)
* Replace function registries with catalogue (#4584)
* Replace functions registries with catalogue
* Update __init__.py
* Fix test
* Revert unrelated flag [ci skip]
* Bugfix/dep matcher issue 4590 (#4601)
* add contributor agreement for prilopes
* add test for issue #4590
* fix on_match params for DependencyMacther (#4590)
* Minor updates to language example sentences (#4608)
* Add punctuation to Spanish example sentences
* Combine multilanguage examples for lang xx
* Add punctuation to nb examples
* Always realloc to a larger size
Avoid potential (unlikely) edge case and cymem error seen in #4604.
* Add error in debug-data if no dev docs are available (see #4575)
* Update debug-data for GoldCorpus / Example
* Ignore None label in misaligned NER data
2019-11-13 20:24:35 +00:00
|
|
|
"UD corpus to train and evaluate on, e.g. UD_Spanish-AnCora",
|
2018-11-30 19:16:14 +00:00
|
|
|
"positional",
|
|
|
|
None,
|
|
|
|
str,
|
|
|
|
),
|
2018-09-13 12:24:08 +00:00
|
|
|
config=("Path to json formatted config file", "option", "C", Path),
|
2018-03-27 09:53:35 +00:00
|
|
|
limit=("Size limit", "option", "n", int),
|
2018-09-13 22:54:59 +00:00
|
|
|
gpu_device=("Use GPU", "option", "g", int),
|
2018-05-08 11:47:45 +00:00
|
|
|
use_oracle_segments=("Use oracle segments", "flag", "G", int),
|
2018-11-30 19:16:14 +00:00
|
|
|
vectors_dir=(
|
2019-10-02 08:37:39 +00:00
|
|
|
"Path to directory with pretrained vectors, named e.g. en/",
|
2018-11-30 19:16:14 +00:00
|
|
|
"option",
|
|
|
|
"v",
|
|
|
|
Path,
|
|
|
|
),
|
2018-03-10 22:41:55 +00:00
|
|
|
)
|
2018-11-30 19:16:14 +00:00
|
|
|
def main(
|
|
|
|
ud_dir,
|
|
|
|
parses_dir,
|
|
|
|
corpus,
|
|
|
|
config=None,
|
|
|
|
limit=0,
|
|
|
|
gpu_device=-1,
|
|
|
|
vectors_dir=None,
|
|
|
|
use_oracle_segments=False,
|
|
|
|
):
|
2019-11-04 19:31:26 +00:00
|
|
|
Token.set_extension("get_conllu_lines", method=get_token_conllu)
|
|
|
|
Token.set_extension("begins_fused", default=False)
|
|
|
|
Token.set_extension("inside_fused", default=False)
|
2019-09-09 14:32:11 +00:00
|
|
|
|
2018-03-27 09:53:35 +00:00
|
|
|
spacy.util.fix_random_seed()
|
2018-03-23 10:36:38 +00:00
|
|
|
lang.zh.Chinese.Defaults.use_jieba = False
|
|
|
|
lang.ja.Japanese.Defaults.use_janome = False
|
2018-11-30 19:16:14 +00:00
|
|
|
|
2018-09-13 12:24:08 +00:00
|
|
|
if config is not None:
|
2018-09-13 16:05:48 +00:00
|
|
|
config = Config.load(config, vectors_dir=vectors_dir)
|
2018-09-13 12:24:08 +00:00
|
|
|
else:
|
2018-09-13 16:05:48 +00:00
|
|
|
config = Config(vectors_dir=vectors_dir)
|
2018-03-10 22:41:55 +00:00
|
|
|
paths = TreebankPaths(ud_dir, corpus)
|
|
|
|
if not (parses_dir / corpus).exists():
|
|
|
|
(parses_dir / corpus).mkdir()
|
|
|
|
print("Train and evaluate", corpus, "using lang", paths.lang)
|
2018-04-29 13:49:32 +00:00
|
|
|
nlp = load_nlp(paths.lang, config, vectors=vectors_dir)
|
2018-03-10 22:41:55 +00:00
|
|
|
|
2019-11-11 16:35:27 +00:00
|
|
|
examples = read_data(
|
2018-11-30 19:16:14 +00:00
|
|
|
nlp,
|
2019-11-11 16:35:27 +00:00
|
|
|
paths.train.conllu.open(encoding="utf8"),
|
|
|
|
paths.train.text.open(encoding="utf8"),
|
2018-11-30 19:16:14 +00:00
|
|
|
max_doc_length=config.max_doc_length,
|
|
|
|
limit=limit,
|
|
|
|
)
|
2018-03-10 22:41:55 +00:00
|
|
|
|
2019-11-11 16:35:27 +00:00
|
|
|
optimizer = initialize_pipeline(nlp, examples, config, gpu_device)
|
2018-03-10 22:41:55 +00:00
|
|
|
|
2018-09-13 12:24:08 +00:00
|
|
|
batch_sizes = compounding(config.min_batch_size, config.max_batch_size, 1.001)
|
2018-05-16 18:11:59 +00:00
|
|
|
beam_prob = compounding(0.2, 0.8, 1.001)
|
2018-03-10 22:41:55 +00:00
|
|
|
for i in range(config.nr_epoch):
|
2019-11-11 16:35:27 +00:00
|
|
|
examples = read_data(
|
2018-11-30 19:16:14 +00:00
|
|
|
nlp,
|
2019-10-29 12:16:55 +00:00
|
|
|
paths.train.conllu.open(encoding="utf8"),
|
|
|
|
paths.train.text.open(encoding="utf8"),
|
2018-11-30 19:16:14 +00:00
|
|
|
max_doc_length=config.max_doc_length,
|
|
|
|
limit=limit,
|
|
|
|
oracle_segments=use_oracle_segments,
|
|
|
|
raw_text=not use_oracle_segments,
|
|
|
|
)
|
2019-11-11 16:35:27 +00:00
|
|
|
random.shuffle(examples)
|
2018-09-13 12:24:08 +00:00
|
|
|
if config.batch_by_words:
|
2019-11-11 16:35:27 +00:00
|
|
|
batches = minibatch_by_words(examples, size=batch_sizes)
|
2018-09-13 12:24:08 +00:00
|
|
|
else:
|
2019-11-11 16:35:27 +00:00
|
|
|
batches = minibatch(examples, size=batch_sizes)
|
2018-03-10 22:41:55 +00:00
|
|
|
losses = {}
|
2020-06-29 12:33:00 +00:00
|
|
|
n_train_words = sum(len(eg.predicted) for eg in examples)
|
2018-03-10 22:41:55 +00:00
|
|
|
with tqdm.tqdm(total=n_train_words, leave=False) as pbar:
|
|
|
|
for batch in batches:
|
2020-06-29 12:33:00 +00:00
|
|
|
pbar.update(sum(len(ex.predicted) for ex in batch))
|
2018-11-30 19:16:14 +00:00
|
|
|
nlp.parser.cfg["beam_update_prob"] = next(beam_prob)
|
|
|
|
nlp.update(
|
2019-11-11 16:35:27 +00:00
|
|
|
batch,
|
2018-11-30 19:16:14 +00:00
|
|
|
sgd=optimizer,
|
|
|
|
drop=config.dropout,
|
|
|
|
losses=losses,
|
|
|
|
)
|
|
|
|
|
|
|
|
out_path = parses_dir / corpus / "epoch-{i}.conllu".format(i=i)
|
2018-03-10 22:41:55 +00:00
|
|
|
with nlp.use_params(optimizer.averages):
|
2018-05-08 11:47:45 +00:00
|
|
|
if use_oracle_segments:
|
|
|
|
parsed_docs, scores = evaluate(nlp, paths.dev.conllu,
|
2018-09-26 19:02:42 +00:00
|
|
|
paths.dev.conllu, out_path)
|
2018-05-08 11:47:45 +00:00
|
|
|
else:
|
|
|
|
parsed_docs, scores = evaluate(nlp, paths.dev.text,
|
2018-09-26 19:02:42 +00:00
|
|
|
paths.dev.conllu, out_path)
|
|
|
|
print_progress(i, losses, scores)
|
Improve label management in parser and NER (#2108)
This patch does a few smallish things that tighten up the training workflow a little, and allow memory use during training to be reduced by letting the GoldCorpus stream data properly.
Previously, the parser and entity recognizer read and saved labels as lists, with extra labels noted separately. Lists were used becaue ordering is very important, to ensure that the label-to-class mapping is stable.
We now manage labels as nested dictionaries, first keyed by the action, and then keyed by the label. Values are frequencies. The trick is, how do we save new labels? We need to make sure we iterate over these in the same order they're added. Otherwise, we'll get different class IDs, and the model's predictions won't make sense.
To allow stable sorting, we map the new labels to negative values. If we have two new labels, they'll be noted as having "frequency" -1 and -2. The next new label will then have "frequency" -3. When we sort by (frequency, label), we then get a stable sort.
Storing frequencies then allows us to make the next nice improvement. Previously we had to iterate over the whole training set, to pre-process it for the deprojectivisation. This led to storing the whole training set in memory. This was most of the required memory during training.
To prevent this, we now store the frequencies as we stream in the data, and deprojectivize as we go. Once we've built the frequencies, we can then apply a frequency cut-off when we decide how many classes to make.
Finally, to allow proper data streaming, we also have to have some way of shuffling the iterator. This is awkward if the training files have multiple documents in them. To solve this, the GoldCorpus class now writes the training data to disk in msgpack files, one per document. We can then shuffle the data by shuffling the paths.
This is a squash merge, as I made a lot of very small commits. Individual commit messages below.
* Simplify label management for TransitionSystem and its subclasses
* Fix serialization for new label handling format in parser
* Simplify and improve GoldCorpus class. Reduce memory use, write to temp dir
* Set actions in transition system
* Require thinc 6.11.1.dev4
* Fix error in parser init
* Add unicode declaration
* Fix unicode declaration
* Update textcat test
* Try to get model training on less memory
* Print json loc for now
* Try rapidjson to reduce memory use
* Remove rapidjson requirement
* Try rapidjson for reduced mem usage
* Handle None heads when projectivising
* Stream json docs
* Fix train script
* Handle projectivity in GoldParse
* Fix projectivity handling
* Add minibatch_by_words util from ud_train
* Minibatch by number of words in spacy.cli.train
* Move minibatch_by_words util to spacy.util
* Fix label handling
* More hacking at label management in parser
* Fix encoding in msgpack serialization in GoldParse
* Adjust batch sizes in parser training
* Fix minibatch_by_words
* Add merge_subtokens function to pipeline.pyx
* Register merge_subtokens factory
* Restore use of msgpack tmp directory
* Use minibatch-by-words in train
* Handle retokenization in scorer
* Change back-off approach for missing labels. Use 'dep' label
* Update NER for new label management
* Set NER tags for over-segmented words
* Fix label alignment in gold
* Fix label back-off for infrequent labels
* Fix int type in labels dict key
* Fix int type in labels dict key
* Update feature definition for 8 feature set
* Update ud-train script for new label stuff
* Fix json streamer
* Print the line number if conll eval fails
* Update children and sentence boundaries after deprojectivisation
* Export set_children_from_heads from doc.pxd
* Render parses during UD training
* Remove print statement
* Require thinc 6.11.1.dev6. Try adding wheel as install_requires
* Set different dev version, to flush pip cache
* Update thinc version
* Update GoldCorpus docs
* Remove print statements
* Fix formatting and links [ci skip]
2018-03-19 01:58:08 +00:00
|
|
|
|
|
|
|
|
|
|
|
def _render_parses(i, to_render):
|
2018-11-30 19:16:14 +00:00
|
|
|
to_render[0].user_data["title"] = "Batch %d" % i
|
2019-10-29 12:16:55 +00:00
|
|
|
with Path("/tmp/parses.html").open("w", encoding="utf8") as file_:
|
2018-11-30 19:16:14 +00:00
|
|
|
html = displacy.render(to_render[:5], style="dep", page=True)
|
Improve label management in parser and NER (#2108)
This patch does a few smallish things that tighten up the training workflow a little, and allow memory use during training to be reduced by letting the GoldCorpus stream data properly.
Previously, the parser and entity recognizer read and saved labels as lists, with extra labels noted separately. Lists were used becaue ordering is very important, to ensure that the label-to-class mapping is stable.
We now manage labels as nested dictionaries, first keyed by the action, and then keyed by the label. Values are frequencies. The trick is, how do we save new labels? We need to make sure we iterate over these in the same order they're added. Otherwise, we'll get different class IDs, and the model's predictions won't make sense.
To allow stable sorting, we map the new labels to negative values. If we have two new labels, they'll be noted as having "frequency" -1 and -2. The next new label will then have "frequency" -3. When we sort by (frequency, label), we then get a stable sort.
Storing frequencies then allows us to make the next nice improvement. Previously we had to iterate over the whole training set, to pre-process it for the deprojectivisation. This led to storing the whole training set in memory. This was most of the required memory during training.
To prevent this, we now store the frequencies as we stream in the data, and deprojectivize as we go. Once we've built the frequencies, we can then apply a frequency cut-off when we decide how many classes to make.
Finally, to allow proper data streaming, we also have to have some way of shuffling the iterator. This is awkward if the training files have multiple documents in them. To solve this, the GoldCorpus class now writes the training data to disk in msgpack files, one per document. We can then shuffle the data by shuffling the paths.
This is a squash merge, as I made a lot of very small commits. Individual commit messages below.
* Simplify label management for TransitionSystem and its subclasses
* Fix serialization for new label handling format in parser
* Simplify and improve GoldCorpus class. Reduce memory use, write to temp dir
* Set actions in transition system
* Require thinc 6.11.1.dev4
* Fix error in parser init
* Add unicode declaration
* Fix unicode declaration
* Update textcat test
* Try to get model training on less memory
* Print json loc for now
* Try rapidjson to reduce memory use
* Remove rapidjson requirement
* Try rapidjson for reduced mem usage
* Handle None heads when projectivising
* Stream json docs
* Fix train script
* Handle projectivity in GoldParse
* Fix projectivity handling
* Add minibatch_by_words util from ud_train
* Minibatch by number of words in spacy.cli.train
* Move minibatch_by_words util to spacy.util
* Fix label handling
* More hacking at label management in parser
* Fix encoding in msgpack serialization in GoldParse
* Adjust batch sizes in parser training
* Fix minibatch_by_words
* Add merge_subtokens function to pipeline.pyx
* Register merge_subtokens factory
* Restore use of msgpack tmp directory
* Use minibatch-by-words in train
* Handle retokenization in scorer
* Change back-off approach for missing labels. Use 'dep' label
* Update NER for new label management
* Set NER tags for over-segmented words
* Fix label alignment in gold
* Fix label back-off for infrequent labels
* Fix int type in labels dict key
* Fix int type in labels dict key
* Update feature definition for 8 feature set
* Update ud-train script for new label stuff
* Fix json streamer
* Print the line number if conll eval fails
* Update children and sentence boundaries after deprojectivisation
* Export set_children_from_heads from doc.pxd
* Render parses during UD training
* Remove print statement
* Require thinc 6.11.1.dev6. Try adding wheel as install_requires
* Set different dev version, to flush pip cache
* Update thinc version
* Update GoldCorpus docs
* Remove print statements
* Fix formatting and links [ci skip]
2018-03-19 01:58:08 +00:00
|
|
|
file_.write(html)
|
2018-03-10 22:41:55 +00:00
|
|
|
|
|
|
|
|
2018-11-30 19:16:14 +00:00
|
|
|
if __name__ == "__main__":
|
2018-03-10 22:41:55 +00:00
|
|
|
plac.call(main)
|