mirror of
https://github.com/explosion/spaCy.git
synced 2025-01-12 10:16:27 +03:00
d33953037e
* Create aryaprabhudesai.md (#2681) * Update _install.jade (#2688) Typo fix: "models" -> "model" * Add FAC to spacy.explain (resolves #2706) * Remove docstrings for deprecated arguments (see #2703) * When calling getoption() in conftest.py, pass a default option (#2709) * When calling getoption() in conftest.py, pass a default option This is necessary to allow testing an installed spacy by running: pytest --pyargs spacy * Add contributor agreement * update bengali token rules for hyphen and digits (#2731) * Less norm computations in token similarity (#2730) * Less norm computations in token similarity * Contributor agreement * Remove ')' for clarity (#2737) Sorry, don't mean to be nitpicky, I just noticed this when going through the CLI and thought it was a quick fix. That said, if this was intention than please let me know. * added contributor agreement for mbkupfer (#2738) * Basic support for Telugu language (#2751) * Lex _attrs for polish language (#2750) * Signed spaCy contributor agreement * Added polish version of english lex_attrs * Introduces a bulk merge function, in order to solve issue #653 (#2696) * Fix comment * Introduce bulk merge to increase performance on many span merges * Sign contributor agreement * Implement pull request suggestions * Describe converters more explicitly (see #2643) * Add multi-threading note to Language.pipe (resolves #2582) [ci skip] * Fix formatting * Fix dependency scheme docs (closes #2705) [ci skip] * Don't set stop word in example (closes #2657) [ci skip] * Add words to portuguese language _num_words (#2759) * Add words to portuguese language _num_words * Add words to portuguese language _num_words * Update Indonesian model (#2752) * adding e-KTP in tokenizer exceptions list * add exception token * removing lines with containing space as it won't matter since we use .split() method in the end, added new tokens in exception * add tokenizer exceptions list * combining base_norms with norm_exceptions * adding norm_exception * fix double key in lemmatizer * remove unused import on punctuation.py * reformat stop_words to reduce number of lines, improve readibility * updating tokenizer exception * implement is_currency for lang/id * adding orth_first_upper in tokenizer_exceptions * update the norm_exception list * remove bunch of abbreviations * adding contributors file * Fixed spaCy+Keras example (#2763) * bug fixes in keras example * created contributor agreement * Adding French hyphenated first name (#2786) * Fix typo (closes #2784) * Fix typo (#2795) [ci skip] Fixed typo on line 6 "regcognizer --> recognizer" * Adding basic support for Sinhala language. (#2788) * adding Sinhala language package, stop words, examples and lex_attrs. * Adding contributor agreement * Updating contributor agreement * Also include lowercase norm exceptions * Fix error (#2802) * Fix error ValueError: cannot resize an array that references or is referenced by another array in this way. Use the resize function * added spaCy Contributor Agreement * Add charlax's contributor agreement (#2805) * agreement of contributor, may I introduce a tiny pl languge contribution (#2799) * Contributors agreement * Contributors agreement * Contributors agreement * Add jupyter=True to displacy.render in documentation (#2806) * Revert "Also include lowercase norm exceptions" This reverts commit70f4e8adf3
. * Remove deprecated encoding argument to msgpack * Set up dependency tree pattern matching skeleton (#2732) * Fix bug when too many entity types. Fixes #2800 * Fix Python 2 test failure * Require older msgpack-numpy * Restore encoding arg on msgpack-numpy * Try to fix version pin for msgpack-numpy * Update Portuguese Language (#2790) * Add words to portuguese language _num_words * Add words to portuguese language _num_words * Portuguese - Add/remove stopwords, fix tokenizer, add currency symbols * Extended punctuation and norm_exceptions in the Portuguese language * Correct error in spacy universe docs concerning spacy-lookup (#2814) * Update Keras Example for (Parikh et al, 2016) implementation (#2803) * bug fixes in keras example * created contributor agreement * baseline for Parikh model * initial version of parikh 2016 implemented * tested asymmetric models * fixed grevious error in normalization * use standard SNLI test file * begin to rework parikh example * initial version of running example * start to document the new version * start to document the new version * Update Decompositional Attention.ipynb * fixed calls to similarity * updated the README * import sys package duh * simplified indexing on mapping word to IDs * stupid python indent error * added code from https://github.com/tensorflow/tensorflow/issues/3388 for tf bug workaround * Fix typo (closes #2815) [ci skip] * Update regex version dependency * Set version to 2.0.13.dev3 * Skip seemingly problematic test * Remove problematic test * Try previous version of regex * Revert "Remove problematic test" This reverts commitbdebbef455
. * Unskip test * Try older version of regex * 💫 Update training examples and use minibatching (#2830) <!--- Provide a general summary of your changes in the title. --> ## Description Update the training examples in `/examples/training` to show usage of spaCy's `minibatch` and `compounding` helpers ([see here](https://spacy.io/usage/training#tips-batch-size) for details). The lack of batching in the examples has caused some confusion in the past, especially for beginners who would copy-paste the examples, update them with large training sets and experienced slow and unsatisfying results. ### Types of change enhancements ## Checklist <!--- Before you submit the PR, go over this checklist and make sure you can tick off all the boxes. [] -> [x] --> - [x] I have submitted the spaCy Contributor Agreement. - [x] I ran the tests, and all new and existing tests passed. - [x] My changes don't require a change to the documentation, or if they do, I've added all required information. * Visual C++ link updated (#2842) (closes #2841) [ci skip] * New landing page * Add contribution agreement * Correcting lang/ru/examples.py (#2845) * Correct some grammatical inaccuracies in lang\ru\examples.py; filled Contributor Agreement * Correct some grammatical inaccuracies in lang\ru\examples.py * Move contributor agreement to separate file * Set version to 2.0.13.dev4 * Add Persian(Farsi) language support (#2797) * Also include lowercase norm exceptions * Remove in favour of https://github.com/explosion/spaCy/graphs/contributors * Rule-based French Lemmatizer (#2818) <!--- Provide a general summary of your changes in the title. --> ## Description <!--- Use this section to describe your changes. If your changes required testing, include information about the testing environment and the tests you ran. If your test fixes a bug reported in an issue, don't forget to include the issue number. If your PR is still a work in progress, that's totally fine – just include a note to let us know. --> Add a rule-based French Lemmatizer following the english one and the excellent PR for [greek language optimizations](https://github.com/explosion/spaCy/pull/2558) to adapt the Lemmatizer class. ### Types of change <!-- What type of change does your PR cover? Is it a bug fix, an enhancement or new feature, or a change to the documentation? --> - Lemma dictionary used can be found [here](http://infolingu.univ-mlv.fr/DonneesLinguistiques/Dictionnaires/telechargement.html), I used the XML version. - Add several files containing exhaustive list of words for each part of speech - Add some lemma rules - Add POS that are not checked in the standard Lemmatizer, i.e PRON, DET, ADV and AUX - Modify the Lemmatizer class to check in lookup table as a last resort if POS not mentionned - Modify the lemmatize function to check in lookup table as a last resort - Init files are updated so the model can support all the functionalities mentioned above - Add words to tokenizer_exceptions_list.py in respect to regex used in tokenizer_exceptions.py ## Checklist <!--- Before you submit the PR, go over this checklist and make sure you can tick off all the boxes. [] -> [x] --> - [X] I have submitted the spaCy Contributor Agreement. - [X] I ran the tests, and all new and existing tests passed. - [X] My changes don't require a change to the documentation, or if they do, I've added all required information. * Set version to 2.0.13 * Fix formatting and consistency * Update docs for new version [ci skip] * Increment version [ci skip] * Add info on wheels [ci skip] * Adding "This is a sentence" example to Sinhala (#2846) * Add wheels badge * Update badge [ci skip] * Update README.rst [ci skip] * Update murmurhash pin * Increment version to 2.0.14.dev0 * Update GPU docs for v2.0.14 * Add wheel to setup_requires * Import prefer_gpu and require_gpu functions from Thinc * Add tests for prefer_gpu() and require_gpu() * Update requirements and setup.py * Workaround bug in thinc require_gpu * Set version to v2.0.14 * Update push-tag script * Unhack prefer_gpu * Require thinc 6.10.6 * Update prefer_gpu and require_gpu docs [ci skip] * Fix specifiers for GPU * Set version to 2.0.14.dev1 * Set version to 2.0.14 * Update Thinc version pin * Increment version * Fix msgpack-numpy version pin * Increment version * Update version to 2.0.16 * Update version [ci skip] * Redundant ')' in the Stop words' example (#2856) <!--- Provide a general summary of your changes in the title. --> ## Description <!--- Use this section to describe your changes. If your changes required testing, include information about the testing environment and the tests you ran. If your test fixes a bug reported in an issue, don't forget to include the issue number. If your PR is still a work in progress, that's totally fine – just include a note to let us know. --> ### Types of change <!-- What type of change does your PR cover? Is it a bug fix, an enhancement or new feature, or a change to the documentation? --> ## Checklist <!--- Before you submit the PR, go over this checklist and make sure you can tick off all the boxes. [] -> [x] --> - [ ] I have submitted the spaCy Contributor Agreement. - [ ] I ran the tests, and all new and existing tests passed. - [ ] My changes don't require a change to the documentation, or if they do, I've added all required information. * Documentation improvement regarding joblib and SO (#2867) Some documentation improvements ## Description 1. Fixed the dead URL to joblib 2. Fixed Stack Overflow brand name (with space) ### Types of change Documentation ## Checklist <!--- Before you submit the PR, go over this checklist and make sure you can tick off all the boxes. [] -> [x] --> - [x] I have submitted the spaCy Contributor Agreement. - [x] I ran the tests, and all new and existing tests passed. - [x] My changes don't require a change to the documentation, or if they do, I've added all required information. * raise error when setting overlapping entities as doc.ents (#2880) * Fix out-of-bounds access in NER training The helper method state.B(1) gets the index of the first token of the buffer, or -1 if no such token exists. Normally this is safe because we pass this to functions like state.safe_get(), which returns an empty token. Here we used it directly as an array index, which is not okay! This error may have been the cause of out-of-bounds access errors during training. Similar errors may still be around, so much be hunted down. Hunting this one down took a long time...I printed out values across training runs and diffed, looking for points of divergence between runs, when no randomness should be allowed. * Change PyThaiNLP Url (#2876) * Fix missing comma * Add example showing a fix-up rule for space entities * Set version to 2.0.17.dev0 * Update regex version * Revert "Update regex version" This reverts commit62358dd867
. * Try setting older regex version, to align with conda * Set version to 2.0.17 * Add spacy-js to universe [ci-skip] * Add spacy-raspberry to universe (closes #2889) * Add script to validate universe json [ci skip] * Removed space in docs + added contributor indo (#2909) * - removed unneeded space in documentation * - added contributor info * Allow input text of length up to max_length, inclusive (#2922) * Include universe spec for spacy-wordnet component (#2919) * feat: include universe spec for spacy-wordnet component * chore: include spaCy contributor agreement * Minor formatting changes [ci skip] * Fix image [ci skip] Twitter URL doesn't work on live site * Check if the word is in one of the regular lists specific to each POS (#2886) * 💫 Create random IDs for SVGs to prevent ID clashes (#2927) Resolves #2924. ## Description Fixes problem where multiple visualizations in Jupyter notebooks would have clashing arc IDs, resulting in weirdly positioned arc labels. Generating a random ID prefix so even identical parses won't receive the same IDs for consistency (even if effect of ID clash isn't noticable here.) ### Types of change bug fix ## Checklist <!--- Before you submit the PR, go over this checklist and make sure you can tick off all the boxes. [] -> [x] --> - [x] I have submitted the spaCy Contributor Agreement. - [x] I ran the tests, and all new and existing tests passed. - [x] My changes don't require a change to the documentation, or if they do, I've added all required information. * Fix typo [ci skip] * fixes symbolic link on py3 and windows (#2949) * fixes symbolic link on py3 and windows during setup of spacy using command python -m spacy link en_core_web_sm en closes #2948 * Update spacy/compat.py Co-Authored-By: cicorias <cicorias@users.noreply.github.com> * Fix formatting * Update universe [ci skip] * Catalan Language Support (#2940) * Catalan language Support * Ddding Catalan to documentation * Sort languages alphabetically [ci skip] * Update tests for pytest 4.x (#2965) <!--- Provide a general summary of your changes in the title. --> ## Description - [x] Replace marks in params for pytest 4.0 compat ([see here](https://docs.pytest.org/en/latest/deprecations.html#marks-in-pytest-mark-parametrize)) - [x] Un-xfail passing tests (some fixes in a recent update resolved a bunch of issues, but tests were apparently never updated here) ### Types of change <!-- What type of change does your PR cover? Is it a bug fix, an enhancement or new feature, or a change to the documentation? --> ## Checklist <!--- Before you submit the PR, go over this checklist and make sure you can tick off all the boxes. [] -> [x] --> - [x] I have submitted the spaCy Contributor Agreement. - [x] I ran the tests, and all new and existing tests passed. - [x] My changes don't require a change to the documentation, or if they do, I've added all required information. * Fix regex pin to harmonize with conda (#2964) * Update README.rst * Fix bug where Vocab.prune_vector did not use 'batch_size' (#2977) Fixes #2976 * Fix typo * Fix typo * Remove duplicate file * Require thinc 7.0.0.dev2 Fixes bug in gpu_ops that would use cupy instead of numpy on CPU * Add missing import * Fix error IDs * Fix tests
593 lines
25 KiB
Cython
593 lines
25 KiB
Cython
# cython: infer_types=True
|
|
# cython: cdivision=True
|
|
# cython: boundscheck=False
|
|
# coding: utf-8
|
|
from __future__ import unicode_literals, print_function
|
|
|
|
from collections import OrderedDict
|
|
import ujson
|
|
import json
|
|
import numpy
|
|
cimport cython.parallel
|
|
import cytoolz
|
|
import numpy.random
|
|
cimport numpy as np
|
|
from cpython.ref cimport PyObject, Py_XDECREF
|
|
from cpython.exc cimport PyErr_CheckSignals, PyErr_SetFromErrno
|
|
from libc.math cimport exp
|
|
from libcpp.vector cimport vector
|
|
from libc.string cimport memset, memcpy
|
|
from libc.stdlib cimport calloc, free
|
|
from cymem.cymem cimport Pool
|
|
from thinc.typedefs cimport weight_t, class_t, hash_t
|
|
from thinc.extra.search cimport Beam
|
|
from thinc.api import chain, clone
|
|
from thinc.v2v import Model, Maxout, Affine
|
|
from thinc.misc import LayerNorm
|
|
from thinc.neural.ops import CupyOps
|
|
from thinc.neural.util import get_array_module
|
|
from thinc.linalg cimport Vec, VecVec
|
|
|
|
from ._parser_model cimport resize_activations, predict_states, arg_max_if_valid
|
|
from ._parser_model cimport WeightsC, ActivationsC, SizesC, cpu_log_loss
|
|
from ._parser_model cimport get_c_weights, get_c_sizes
|
|
from ._parser_model import ParserModel
|
|
from .._ml import zero_init, PrecomputableAffine, Tok2Vec, flatten
|
|
from .._ml import link_vectors_to_models, create_default_optimizer
|
|
from ..compat import json_dumps, copy_array
|
|
from ..tokens.doc cimport Doc
|
|
from ..gold cimport GoldParse
|
|
from ..errors import Errors, TempErrors
|
|
from .. import util
|
|
from .stateclass cimport StateClass
|
|
from ._state cimport StateC
|
|
from .transition_system cimport Transition
|
|
from . cimport _beam_utils
|
|
from . import _beam_utils
|
|
from . import nonproj
|
|
|
|
|
|
cdef class Parser:
|
|
"""
|
|
Base class of the DependencyParser and EntityRecognizer.
|
|
"""
|
|
@classmethod
|
|
def Model(cls, nr_class, **cfg):
|
|
depth = util.env_opt('parser_hidden_depth', cfg.get('hidden_depth', 1))
|
|
subword_features = util.env_opt('subword_features',
|
|
cfg.get('subword_features', True))
|
|
conv_depth = util.env_opt('conv_depth', cfg.get('conv_depth', 4))
|
|
bilstm_depth = util.env_opt('bilstm_depth', cfg.get('bilstm_depth', 0))
|
|
if depth != 1:
|
|
raise ValueError(TempErrors.T004.format(value=depth))
|
|
parser_maxout_pieces = util.env_opt('parser_maxout_pieces',
|
|
cfg.get('maxout_pieces', 2))
|
|
token_vector_width = util.env_opt('token_vector_width',
|
|
cfg.get('token_vector_width', 96))
|
|
hidden_width = util.env_opt('hidden_width', cfg.get('hidden_width', 64))
|
|
embed_size = util.env_opt('embed_size', cfg.get('embed_size', 2000))
|
|
pretrained_vectors = cfg.get('pretrained_vectors', None)
|
|
tok2vec = Tok2Vec(token_vector_width, embed_size,
|
|
conv_depth=conv_depth,
|
|
subword_features=subword_features,
|
|
pretrained_vectors=pretrained_vectors,
|
|
bilstm_depth=bilstm_depth)
|
|
tok2vec = chain(tok2vec, flatten)
|
|
lower = PrecomputableAffine(hidden_width,
|
|
nF=cls.nr_feature, nI=token_vector_width,
|
|
nP=parser_maxout_pieces)
|
|
lower.nP = parser_maxout_pieces
|
|
|
|
with Model.use_device('cpu'):
|
|
upper = zero_init(Affine(nr_class, hidden_width, drop_factor=0.0))
|
|
|
|
cfg = {
|
|
'nr_class': nr_class,
|
|
'hidden_depth': depth,
|
|
'token_vector_width': token_vector_width,
|
|
'hidden_width': hidden_width,
|
|
'maxout_pieces': parser_maxout_pieces,
|
|
'pretrained_vectors': pretrained_vectors,
|
|
'bilstm_depth': bilstm_depth
|
|
}
|
|
return ParserModel(tok2vec, lower, upper), cfg
|
|
|
|
name = 'base_parser'
|
|
|
|
def __init__(self, Vocab vocab, moves=True, model=True, **cfg):
|
|
"""Create a Parser.
|
|
|
|
vocab (Vocab): The vocabulary object. Must be shared with documents
|
|
to be processed. The value is set to the `.vocab` attribute.
|
|
moves (TransitionSystem): Defines how the parse-state is created,
|
|
updated and evaluated. The value is set to the .moves attribute
|
|
unless True (default), in which case a new instance is created with
|
|
`Parser.Moves()`.
|
|
model (object): Defines how the parse-state is created, updated and
|
|
evaluated. The value is set to the .model attribute. If set to True
|
|
(default), a new instance will be created with `Parser.Model()`
|
|
in parser.begin_training(), parser.from_disk() or parser.from_bytes().
|
|
**cfg: Arbitrary configuration parameters. Set to the `.cfg` attribute
|
|
"""
|
|
self.vocab = vocab
|
|
if moves is True:
|
|
self.moves = self.TransitionSystem(self.vocab.strings)
|
|
else:
|
|
self.moves = moves
|
|
if 'beam_width' not in cfg:
|
|
cfg['beam_width'] = util.env_opt('beam_width', 1)
|
|
if 'beam_density' not in cfg:
|
|
cfg['beam_density'] = util.env_opt('beam_density', 0.0)
|
|
cfg.setdefault('cnn_maxout_pieces', 3)
|
|
self.cfg = cfg
|
|
self.model = model
|
|
self._multitasks = []
|
|
|
|
def __reduce__(self):
|
|
return (Parser, (self.vocab, self.moves, self.model), None, None)
|
|
|
|
@property
|
|
def tok2vec(self):
|
|
return self.model.tok2vec
|
|
|
|
@property
|
|
def move_names(self):
|
|
names = []
|
|
for i in range(self.moves.n_moves):
|
|
name = self.moves.move_name(self.moves.c[i].move, self.moves.c[i].label)
|
|
names.append(name)
|
|
return names
|
|
|
|
nr_feature = 8
|
|
|
|
@property
|
|
def labels(self):
|
|
class_names = [self.moves.get_class_name(i) for i in range(self.moves.n_moves)]
|
|
return class_names
|
|
|
|
@property
|
|
def tok2vec(self):
|
|
'''Return the embedding and convolutional layer of the model.'''
|
|
return None if self.model in (None, True, False) else self.model.tok2vec
|
|
|
|
@property
|
|
def postprocesses(self):
|
|
# Available for subclasses, e.g. to deprojectivize
|
|
return []
|
|
|
|
def add_label(self, label):
|
|
resized = False
|
|
for action in self.moves.action_types:
|
|
added = self.moves.add_action(action, label)
|
|
if added:
|
|
resized = True
|
|
if self.model not in (True, False, None) and resized:
|
|
self.model.resize_output(self.moves.n_moves)
|
|
|
|
def add_multitask_objective(self, target):
|
|
# Defined in subclasses, to avoid circular import
|
|
raise NotImplementedError
|
|
|
|
def init_multitask_objectives(self, get_gold_tuples, pipeline, **cfg):
|
|
'''Setup models for secondary objectives, to benefit from multi-task
|
|
learning. This method is intended to be overridden by subclasses.
|
|
|
|
For instance, the dependency parser can benefit from sharing
|
|
an input representation with a label prediction model. These auxiliary
|
|
models are discarded after training.
|
|
'''
|
|
pass
|
|
|
|
def preprocess_gold(self, docs_golds):
|
|
for doc, gold in docs_golds:
|
|
yield doc, gold
|
|
|
|
def use_params(self, params):
|
|
# Can't decorate cdef class :(. Workaround.
|
|
with self.model.use_params(params):
|
|
yield
|
|
|
|
def __call__(self, Doc doc, beam_width=None):
|
|
"""Apply the parser or entity recognizer, setting the annotations onto
|
|
the `Doc` object.
|
|
|
|
doc (Doc): The document to be processed.
|
|
"""
|
|
if beam_width is None:
|
|
beam_width = self.cfg.get('beam_width', 1)
|
|
beam_density = self.cfg.get('beam_density', 0.)
|
|
states = self.predict([doc], beam_width=beam_width,
|
|
beam_density=beam_density)
|
|
self.set_annotations([doc], states, tensors=None)
|
|
return doc
|
|
|
|
def pipe(self, docs, int batch_size=256, int n_threads=2, beam_width=None):
|
|
"""Process a stream of documents.
|
|
|
|
stream: The sequence of documents to process.
|
|
batch_size (int): Number of documents to accumulate into a working set.
|
|
n_threads (int): The number of threads with which to work on the buffer
|
|
in parallel.
|
|
YIELDS (Doc): Documents, in order.
|
|
"""
|
|
if beam_width is None:
|
|
beam_width = self.cfg.get('beam_width', 1)
|
|
beam_density = self.cfg.get('beam_density', 0.)
|
|
cdef Doc doc
|
|
for batch in cytoolz.partition_all(batch_size, docs):
|
|
batch_in_order = list(batch)
|
|
by_length = sorted(batch_in_order, key=lambda doc: len(doc))
|
|
for subbatch in cytoolz.partition_all(8, by_length):
|
|
subbatch = list(subbatch)
|
|
parse_states = self.predict(subbatch, beam_width=beam_width,
|
|
beam_density=beam_density)
|
|
self.set_annotations(subbatch, parse_states, tensors=None)
|
|
for doc in batch_in_order:
|
|
yield doc
|
|
|
|
def predict(self, docs, beam_width=1, beam_density=0.0, drop=0.):
|
|
if isinstance(docs, Doc):
|
|
docs = [docs]
|
|
if not any(len(doc) for doc in docs):
|
|
return self.moves.init_batch(docs)
|
|
if beam_width < 2:
|
|
return self.greedy_parse(docs, drop=drop)
|
|
else:
|
|
return self.beam_parse(docs, beam_width=beam_width,
|
|
beam_density=beam_density, drop=drop)
|
|
|
|
def greedy_parse(self, docs, drop=0.):
|
|
cdef vector[StateC*] states
|
|
cdef StateClass state
|
|
model = self.model(docs)
|
|
batch = self.moves.init_batch(docs)
|
|
weights = get_c_weights(model)
|
|
for state in batch:
|
|
if not state.is_final():
|
|
states.push_back(state.c)
|
|
sizes = get_c_sizes(model, states.size())
|
|
with nogil:
|
|
self._parseC(&states[0],
|
|
weights, sizes)
|
|
return batch
|
|
|
|
def beam_parse(self, docs, int beam_width, float drop=0., beam_density=0.):
|
|
cdef Beam beam
|
|
cdef Doc doc
|
|
cdef np.ndarray token_ids
|
|
model = self.model(docs)
|
|
beams = self.moves.init_beams(docs, beam_width, beam_density=beam_density)
|
|
token_ids = numpy.zeros((len(docs) * beam_width, self.nr_feature),
|
|
dtype='i', order='C')
|
|
cdef int* c_ids
|
|
cdef int nr_feature = self.nr_feature
|
|
cdef int n_states
|
|
model = self.model(docs)
|
|
todo = [beam for beam in beams if not beam.is_done]
|
|
while todo:
|
|
token_ids.fill(-1)
|
|
c_ids = <int*>token_ids.data
|
|
n_states = 0
|
|
for beam in todo:
|
|
for i in range(beam.size):
|
|
state = <StateC*>beam.at(i)
|
|
# This way we avoid having to score finalized states
|
|
# We do have to take care to keep indexes aligned, though
|
|
if not state.is_final():
|
|
state.set_context_tokens(c_ids, nr_feature)
|
|
c_ids += nr_feature
|
|
n_states += 1
|
|
if n_states == 0:
|
|
break
|
|
vectors = model.state2vec(token_ids[:n_states])
|
|
scores = model.vec2scores(vectors)
|
|
todo = self.transition_beams(todo, scores)
|
|
return beams
|
|
|
|
cdef void _parseC(self, StateC** states,
|
|
WeightsC weights, SizesC sizes) nogil:
|
|
cdef int i, j
|
|
cdef vector[StateC*] unfinished
|
|
cdef ActivationsC activations
|
|
memset(&activations, 0, sizeof(activations))
|
|
while sizes.states >= 1:
|
|
predict_states(&activations,
|
|
states, &weights, sizes)
|
|
# Validate actions, argmax, take action.
|
|
self.c_transition_batch(states,
|
|
activations.scores, sizes.classes, sizes.states)
|
|
for i in range(sizes.states):
|
|
if not states[i].is_final():
|
|
unfinished.push_back(states[i])
|
|
for i in range(unfinished.size()):
|
|
states[i] = unfinished[i]
|
|
sizes.states = unfinished.size()
|
|
unfinished.clear()
|
|
|
|
def set_annotations(self, docs, states_or_beams, tensors=None):
|
|
cdef StateClass state
|
|
cdef Beam beam
|
|
cdef Doc doc
|
|
states = []
|
|
beams = []
|
|
for state_or_beam in states_or_beams:
|
|
if isinstance(state_or_beam, StateClass):
|
|
states.append(state_or_beam)
|
|
else:
|
|
beam = state_or_beam
|
|
state = StateClass.borrow(<StateC*>beam.at(0))
|
|
states.append(state)
|
|
beams.append(beam)
|
|
for i, (state, doc) in enumerate(zip(states, docs)):
|
|
self.moves.finalize_state(state.c)
|
|
for j in range(doc.length):
|
|
doc.c[j] = state.c._sent[j]
|
|
self.moves.finalize_doc(doc)
|
|
for hook in self.postprocesses:
|
|
hook(doc)
|
|
for beam in beams:
|
|
_beam_utils.cleanup_beam(beam)
|
|
|
|
def transition_states(self, states, float[:, ::1] scores):
|
|
cdef StateClass state
|
|
cdef float* c_scores = &scores[0, 0]
|
|
cdef vector[StateC*] c_states
|
|
for state in states:
|
|
c_states.push_back(state.c)
|
|
self.c_transition_batch(&c_states[0], c_scores, scores.shape[1], scores.shape[0])
|
|
return [state for state in states if not state.c.is_final()]
|
|
|
|
cdef void c_transition_batch(self, StateC** states, const float* scores,
|
|
int nr_class, int batch_size) nogil:
|
|
cdef int[500] is_valid # TODO: Unhack
|
|
cdef int i, guess
|
|
cdef Transition action
|
|
for i in range(batch_size):
|
|
self.moves.set_valid(is_valid, states[i])
|
|
guess = arg_max_if_valid(&scores[i*nr_class], is_valid, nr_class)
|
|
action = self.moves.c[guess]
|
|
action.do(states[i], action.label)
|
|
states[i].push_hist(guess)
|
|
|
|
def transition_beams(self, beams, float[:, ::1] scores):
|
|
cdef Beam beam
|
|
cdef float* c_scores = &scores[0, 0]
|
|
for beam in beams:
|
|
for i in range(beam.size):
|
|
state = <StateC*>beam.at(i)
|
|
if not state.is_final():
|
|
self.moves.set_valid(beam.is_valid[i], state)
|
|
memcpy(beam.scores[i], c_scores, scores.shape[1] * sizeof(float))
|
|
c_scores += scores.shape[1]
|
|
beam.advance(_beam_utils.transition_state, NULL, <void*>self.moves.c)
|
|
beam.check_done(_beam_utils.check_final_state, NULL)
|
|
return [b for b in beams if not b.is_done]
|
|
|
|
def update(self, docs, golds, drop=0., sgd=None, losses=None):
|
|
if isinstance(docs, Doc) and isinstance(golds, GoldParse):
|
|
docs = [docs]
|
|
golds = [golds]
|
|
if len(docs) != len(golds):
|
|
raise ValueError(Errors.E077.format(value='update', n_docs=len(docs),
|
|
n_golds=len(golds)))
|
|
if losses is None:
|
|
losses = {}
|
|
losses.setdefault(self.name, 0.)
|
|
# The probability we use beam update, instead of falling back to
|
|
# a greedy update
|
|
beam_update_prob = self.cfg.get('beam_update_prob', 1.0)
|
|
if self.cfg.get('beam_width', 1) >= 2 and numpy.random.random() < beam_update_prob:
|
|
return self.update_beam(docs, golds, self.cfg.get('beam_width', 1),
|
|
drop=drop, sgd=sgd, losses=losses,
|
|
beam_density=self.cfg.get('beam_density', 0.0))
|
|
# Chop sequences into lengths of this many transitions, to make the
|
|
# batch uniform length.
|
|
cut_gold = numpy.random.choice(range(20, 100))
|
|
states, golds, max_steps = self._init_gold_batch(docs, golds, max_length=cut_gold)
|
|
states_golds = [(s, g) for (s, g) in zip(states, golds)
|
|
if not s.is_final() and g is not None]
|
|
|
|
# Prepare the stepwise model, and get the callback for finishing the batch
|
|
model, finish_update = self.model.begin_update(docs, drop=drop)
|
|
for _ in range(max_steps):
|
|
if not states_golds:
|
|
break
|
|
states, golds = zip(*states_golds)
|
|
scores, backprop = model.begin_update(states, drop=drop)
|
|
d_scores = self.get_batch_loss(states, golds, scores, losses)
|
|
backprop(d_scores, sgd=sgd)
|
|
# Follow the predicted action
|
|
self.transition_states(states, scores)
|
|
states_golds = [eg for eg in states_golds if not eg[0].is_final()]
|
|
# Do the backprop
|
|
finish_update(golds, sgd=sgd)
|
|
return losses
|
|
|
|
def update_beam(self, docs, golds, width, drop=0., sgd=None, losses=None,
|
|
beam_density=0.0):
|
|
lengths = [len(d) for d in docs]
|
|
states = self.moves.init_batch(docs)
|
|
for gold in golds:
|
|
self.moves.preprocess_gold(gold)
|
|
model, finish_update = self.model.begin_update(docs, drop=drop)
|
|
states_d_scores, backprops, beams = _beam_utils.update_beam(
|
|
self.moves, self.nr_feature, 10000, states, golds, model.state2vec,
|
|
model.vec2scores, width, drop=drop, losses=losses,
|
|
beam_density=beam_density)
|
|
for i, d_scores in enumerate(states_d_scores):
|
|
losses[self.name] += (d_scores**2).sum()
|
|
ids, bp_vectors, bp_scores = backprops[i]
|
|
d_vector = bp_scores(d_scores, sgd=sgd)
|
|
if isinstance(model.ops, CupyOps) \
|
|
and not isinstance(ids, model.state2vec.ops.xp.ndarray):
|
|
model.backprops.append((
|
|
util.get_async(model.cuda_stream, ids),
|
|
util.get_async(model.cuda_stream, d_vector),
|
|
bp_vectors))
|
|
else:
|
|
model.backprops.append((ids, d_vector, bp_vectors))
|
|
model.make_updates(sgd)
|
|
cdef Beam beam
|
|
for beam in beams:
|
|
_beam_utils.cleanup_beam(beam)
|
|
|
|
def _init_gold_batch(self, whole_docs, whole_golds, min_length=5, max_length=500):
|
|
"""Make a square batch, of length equal to the shortest doc. A long
|
|
doc will get multiple states. Let's say we have a doc of length 2*N,
|
|
where N is the shortest doc. We'll make two states, one representing
|
|
long_doc[:N], and another representing long_doc[N:]."""
|
|
cdef:
|
|
StateClass state
|
|
Transition action
|
|
whole_states = self.moves.init_batch(whole_docs)
|
|
max_length = max(min_length, min(max_length, min([len(doc) for doc in whole_docs])))
|
|
max_moves = 0
|
|
states = []
|
|
golds = []
|
|
for doc, state, gold in zip(whole_docs, whole_states, whole_golds):
|
|
gold = self.moves.preprocess_gold(gold)
|
|
if gold is None:
|
|
continue
|
|
oracle_actions = self.moves.get_oracle_sequence(doc, gold)
|
|
start = 0
|
|
while start < len(doc):
|
|
state = state.copy()
|
|
n_moves = 0
|
|
while state.B(0) < start and not state.is_final():
|
|
action = self.moves.c[oracle_actions.pop(0)]
|
|
action.do(state.c, action.label)
|
|
state.c.push_hist(action.clas)
|
|
n_moves += 1
|
|
has_gold = self.moves.has_gold(gold, start=start,
|
|
end=start+max_length)
|
|
if not state.is_final() and has_gold:
|
|
states.append(state)
|
|
golds.append(gold)
|
|
max_moves = max(max_moves, n_moves)
|
|
start += min(max_length, len(doc)-start)
|
|
max_moves = max(max_moves, len(oracle_actions))
|
|
return states, golds, max_moves
|
|
|
|
def get_batch_loss(self, states, golds, float[:, ::1] scores, losses):
|
|
cdef StateClass state
|
|
cdef GoldParse gold
|
|
cdef Pool mem = Pool()
|
|
cdef int i
|
|
is_valid = <int*>mem.alloc(self.moves.n_moves, sizeof(int))
|
|
costs = <float*>mem.alloc(self.moves.n_moves, sizeof(float))
|
|
cdef np.ndarray d_scores = numpy.zeros((len(states), self.moves.n_moves),
|
|
dtype='f', order='C')
|
|
c_d_scores = <float*>d_scores.data
|
|
for i, (state, gold) in enumerate(zip(states, golds)):
|
|
memset(is_valid, 0, self.moves.n_moves * sizeof(int))
|
|
memset(costs, 0, self.moves.n_moves * sizeof(float))
|
|
self.moves.set_costs(is_valid, costs, state, gold)
|
|
cpu_log_loss(c_d_scores,
|
|
costs, is_valid, &scores[i, 0], d_scores.shape[1])
|
|
c_d_scores += d_scores.shape[1]
|
|
if losses is not None:
|
|
losses.setdefault(self.name, 0.)
|
|
losses[self.name] += (d_scores**2).sum()
|
|
return d_scores
|
|
|
|
def create_optimizer(self):
|
|
return create_default_optimizer(self.model.ops,
|
|
**self.cfg.get('optimizer', {}))
|
|
|
|
def begin_training(self, get_gold_tuples, pipeline=None, sgd=None, **cfg):
|
|
if 'model' in cfg:
|
|
self.model = cfg['model']
|
|
if not hasattr(get_gold_tuples, '__call__'):
|
|
gold_tuples = get_gold_tuples
|
|
get_gold_tuples = lambda: gold_tuples
|
|
cfg.setdefault('min_action_freq', 30)
|
|
actions = self.moves.get_actions(gold_parses=get_gold_tuples(),
|
|
min_freq=cfg.get('min_action_freq', 30))
|
|
previous_labels = dict(self.moves.labels)
|
|
self.moves.initialize_actions(actions)
|
|
for action, label_freqs in previous_labels.items():
|
|
for label in label_freqs:
|
|
self.moves.add_action(action, label)
|
|
cfg.setdefault('token_vector_width', 128)
|
|
if self.model is True:
|
|
self.model, cfg = self.Model(self.moves.n_moves, **cfg)
|
|
if sgd is None:
|
|
sgd = self.create_optimizer()
|
|
doc_sample = []
|
|
gold_sample = []
|
|
for raw_text, annots_brackets in cytoolz.take(1000, get_gold_tuples()):
|
|
for annots, brackets in annots_brackets:
|
|
ids, words, tags, heads, deps, ents = annots
|
|
doc_sample.append(Doc(self.vocab, words=words))
|
|
gold_sample.append(GoldParse(doc_sample[-1], words=words, tags=tags,
|
|
heads=heads, deps=deps, ents=ents))
|
|
self.model.begin_training(doc_sample, gold_sample)
|
|
if pipeline is not None:
|
|
self.init_multitask_objectives(get_gold_tuples, pipeline, sgd=sgd, **cfg)
|
|
link_vectors_to_models(self.vocab)
|
|
else:
|
|
if sgd is None:
|
|
sgd = self.create_optimizer()
|
|
self.model.begin_training([])
|
|
self.cfg.update(cfg)
|
|
return sgd
|
|
|
|
def to_disk(self, path, **exclude):
|
|
serializers = {
|
|
'model': lambda p: (self.model.to_disk(p) if self.model is not True else True),
|
|
'vocab': lambda p: self.vocab.to_disk(p),
|
|
'moves': lambda p: self.moves.to_disk(p, strings=False),
|
|
'cfg': lambda p: p.open('w').write(json_dumps(self.cfg))
|
|
}
|
|
util.to_disk(path, serializers, exclude)
|
|
|
|
def from_disk(self, path, **exclude):
|
|
deserializers = {
|
|
'vocab': lambda p: self.vocab.from_disk(p),
|
|
'moves': lambda p: self.moves.from_disk(p, strings=False),
|
|
'cfg': lambda p: self.cfg.update(util.read_json(p)),
|
|
'model': lambda p: None
|
|
}
|
|
util.from_disk(path, deserializers, exclude)
|
|
if 'model' not in exclude:
|
|
path = util.ensure_path(path)
|
|
if self.model is True:
|
|
self.model, cfg = self.Model(**self.cfg)
|
|
else:
|
|
cfg = {}
|
|
with (path / 'model').open('rb') as file_:
|
|
bytes_data = file_.read()
|
|
self.model.from_bytes(bytes_data)
|
|
self.cfg.update(cfg)
|
|
return self
|
|
|
|
def to_bytes(self, **exclude):
|
|
serializers = OrderedDict((
|
|
('model', lambda: (self.model.to_bytes() if self.model is not True else True)),
|
|
('vocab', lambda: self.vocab.to_bytes()),
|
|
('moves', lambda: self.moves.to_bytes(strings=False)),
|
|
('cfg', lambda: json.dumps(self.cfg, indent=2, sort_keys=True))
|
|
))
|
|
return util.to_bytes(serializers, exclude)
|
|
|
|
def from_bytes(self, bytes_data, **exclude):
|
|
deserializers = OrderedDict((
|
|
('vocab', lambda b: self.vocab.from_bytes(b)),
|
|
('moves', lambda b: self.moves.from_bytes(b, strings=False)),
|
|
('cfg', lambda b: self.cfg.update(json.loads(b))),
|
|
('model', lambda b: None)
|
|
))
|
|
msg = util.from_bytes(bytes_data, deserializers, exclude)
|
|
if 'model' not in exclude:
|
|
# TODO: Remove this once we don't have to handle previous models
|
|
if self.cfg.get('pretrained_dims') and 'pretrained_vectors' not in self.cfg:
|
|
self.cfg['pretrained_vectors'] = self.vocab.vectors.name
|
|
if self.model is True:
|
|
self.model, cfg = self.Model(**self.cfg)
|
|
else:
|
|
cfg = {}
|
|
if 'model' in msg:
|
|
self.model.from_bytes(msg['model'])
|
|
self.cfg.update(cfg)
|
|
return self
|