explosion / spaCy

💫 Industrial-strength Natural Language Processing (NLP) in Python
https://spacy.io
MIT License
29.69k stars 4.36k forks source link

spacy train from prodigy data-to-spacy config with en_core_web_trf yields ValueError: Cannot deserialize model: mismatched structure #11823

Closed cbjrobertson closed 1 year ago

cbjrobertson commented 1 year ago

I'm training a spaCy multi-label text classification model using en_core_web_trf from spaCy transformers. The data and config file I use are generated through a prodigy data-to-spacy call. The issue is that when I try to reload the model using spacy.load("path/to/mod"), it returns: ValueError: Cannot deserialize model: mismatched structure. Based on the prodigy forums, this ought to have been fixed with spacy-transformers version 1.0.6 but I'm running spacy-transformers==1.1.8 so I believe there's still a bug somewhere, likely in spacy-transformers, see #8566. I can't share the data, but I'll do my best to make the issue reproducible.

How to reproduce the behaviour (from a notebook):

from spacy.cli.train import train

out_path = "./model/out/path"

!prodigy data-to-spacy $out_path --textcat-multilabel dataset_name --base-model en_core_web_trf --eval-split 0.2 

train(f"{out_path}/config.cfg", 
      out_path,
      use_gpu = 1,
      overrides={"paths.train" : f"{out_path}train.spacy", 
                 "paths.dev" : f"{out_path}dev.spacy"
                }
     )

nlp = spacy.load(out_path)
>>> ValueError: Cannot deserialize model: mismatched structure

On the basis of this advice, this error can be worked around in either of two ways:

  1. Change the call to spacy.load(out_path) to spacy.load(out_path,disable="tagger,parser,attribute_ruler,lemmatizer,ner")
  2. Edit line 13 of out_path/config.cfg from pipeline = ["transformer","tagger","parser","attribute_ruler","lemmatizer","ner","textcat_multilabel"] to pipeline = ["transformer","textcat_multilabel"]

However, neither of these are ideal. For instance, getting these models to work with spacy-report requires editing the source code of that package. It seems there's still a bug relating to the frozen components in the call to spacy.train!

Here is an example observation from dataset_name:

{'_input_hash': -849869852,
 '_task_hash': -473741752,
 'answer': 'reject',
 'label': 'MY_LABEL',
 'text': "Foo to the bar.'}

Your Environment

================= Installed pipeline packages (spaCy v3.4.3) =================
ℹ spaCy installation:
/home/coler/anaconda3/envs/prodigy/lib/python3.9/site-packages/spacy

NAME              SPACY            VERSION                            
en_core_web_trf   >=3.4.1,<3.5.0   3.4.1   ✔
en_core_web_lg    >=3.4.0,<3.5.0   3.4.1   ✔
[paths]
train = "./new_mods/corpus/train.spacy"
dev = "./new_mods/corpus/dev.spacy"
vectors = null
init_tok2vec = null

[system]
gpu_allocator = null
seed = 0

[nlp]
lang = "en"
pipeline = ["transformer","tagger","parser","attribute_ruler","lemmatizer","ner","textcat_multilabel"]
disabled = []
before_creation = null
after_creation = null
after_pipeline_creation = null
batch_size = 64
tokenizer = {"@tokenizers":"spacy.Tokenizer.v1"}

[components]

[components.attribute_ruler]
factory = "attribute_ruler"
scorer = {"@scorers":"spacy.attribute_ruler_scorer.v1"}
validate = false

[components.lemmatizer]
factory = "lemmatizer"
mode = "rule"
model = null
overwrite = false
scorer = {"@scorers":"spacy.lemmatizer_scorer.v1"}

[components.ner]
factory = "ner"
incorrect_spans_key = null
moves = null
scorer = {"@scorers":"spacy.ner_scorer.v1"}
update_with_oracle_cut_size = 100

[components.ner.model]
@architectures = "spacy.TransitionBasedParser.v2"
state_type = "ner"
extra_state_tokens = false
hidden_width = 64
maxout_pieces = 2
use_upper = false
nO = null

[components.ner.model.tok2vec]
@architectures = "spacy-transformers.TransformerModel.v3"
name = "roberta-base"
mixed_precision = false

[components.ner.model.tok2vec.get_spans]
@span_getters = "spacy-transformers.strided_spans.v1"
window = 128
stride = 96

[components.ner.model.tok2vec.grad_scaler_config]

[components.ner.model.tok2vec.tokenizer_config]
use_fast = true

[components.ner.model.tok2vec.transformer_config]

[components.parser]
factory = "parser"
learn_tokens = false
min_action_freq = 30
moves = null
scorer = {"@scorers":"spacy.parser_scorer.v1"}
update_with_oracle_cut_size = 100

[components.parser.model]
@architectures = "spacy.TransitionBasedParser.v2"
state_type = "parser"
extra_state_tokens = false
hidden_width = 64
maxout_pieces = 2
use_upper = false
nO = null

[components.parser.model.tok2vec]
@architectures = "spacy-transformers.TransformerModel.v3"
name = "roberta-base"
mixed_precision = false

[components.parser.model.tok2vec.get_spans]
@span_getters = "spacy-transformers.strided_spans.v1"
window = 128
stride = 96

[components.parser.model.tok2vec.grad_scaler_config]

[components.parser.model.tok2vec.tokenizer_config]
use_fast = true

[components.parser.model.tok2vec.transformer_config]

[components.tagger]
factory = "tagger"
neg_prefix = "!"
overwrite = false
scorer = {"@scorers":"spacy.tagger_scorer.v1"}

[components.tagger.model]
@architectures = "spacy.Tagger.v2"
nO = null
normalize = false

[components.tagger.model.tok2vec]
@architectures = "spacy-transformers.Tok2VecTransformer.v3"
name = "roberta-base"
mixed_precision = false
pooling = {"@layers":"reduce_mean.v1"}
grad_factor = 1.0

[components.tagger.model.tok2vec.get_spans]
@span_getters = "spacy-transformers.strided_spans.v1"
window = 128
stride = 96

[components.tagger.model.tok2vec.grad_scaler_config]

[components.tagger.model.tok2vec.tokenizer_config]
use_fast = true

[components.tagger.model.tok2vec.transformer_config]

[components.textcat_multilabel]
factory = "textcat_multilabel"
scorer = {"@scorers":"spacy.textcat_multilabel_scorer.v1"}
threshold = 0.5

[components.textcat_multilabel.model]
@architectures = "spacy.TextCatBOW.v2"
exclusive_classes = false
ngram_size = 1
no_output_layer = false
nO = null

[components.transformer]
factory = "transformer"
max_batch_items = 4096
set_extra_annotations = {"@annotation_setters":"spacy-transformers.null_annotation_setter.v1"}

[components.transformer.model]
@architectures = "spacy-transformers.TransformerModel.v3"
name = "roberta-base"
mixed_precision = false

[components.transformer.model.get_spans]
@span_getters = "spacy-transformers.strided_spans.v1"
window = 128
stride = 96

[components.transformer.model.grad_scaler_config]

[components.transformer.model.tokenizer_config]
use_fast = true

[components.transformer.model.transformer_config]

[corpora]

[corpora.dev]
@readers = "spacy.Corpus.v1"
path = ${paths.dev}
max_length = 0
gold_preproc = false
limit = 0
augmenter = null

[corpora.train]
@readers = "spacy.Corpus.v1"
path = ${paths.train}
max_length = 0
gold_preproc = false
limit = 0
augmenter = null

[training]
train_corpus = "corpora.train"
dev_corpus = "corpora.dev"
seed = ${system:seed}
gpu_allocator = ${system:gpu_allocator}
dropout = 0.1
accumulate_gradient = 3
patience = 5000
max_epochs = 0
max_steps = 20000
eval_frequency = 1000
frozen_components = ["tagger","parser","attribute_ruler","lemmatizer","ner"]
before_to_disk = null
annotating_components = []

[training.batcher]
@batchers = "spacy.batch_by_padded.v1"
discard_oversize = true
get_length = null
size = 2000
buffer = 256

[training.logger]
@loggers = "spacy.ConsoleLogger.v1"
progress_bar = false

[training.optimizer]
@optimizers = "Adam.v1"
beta1 = 0.9
beta2 = 0.999
L2_is_weight_decay = true
L2 = 0.01
grad_clip = 1.0
use_averages = true
eps = 0.00000001

[training.optimizer.learn_rate]
@schedules = "warmup_linear.v1"
warmup_steps = 250
total_steps = 20000
initial_rate = 0.00005

[training.score_weights]
tag_acc = null
dep_uas = null
dep_las = null
dep_las_per_type = null
sents_p = null
sents_r = null
sents_f = null
lemma_acc = null
ents_f = null
ents_p = null
ents_r = null
ents_per_type = null
cats_score = 1.0
cats_score_desc = null
cats_micro_p = null
cats_micro_r = null
cats_micro_f = null
cats_macro_p = null
cats_macro_r = null
cats_macro_f = null
cats_macro_auc = null
cats_f_per_type = null
cats_macro_auc_per_type = null
speed = 0.0

[pretraining]

[initialize]
vectors = ${paths.vectors}
init_tok2vec = ${paths.init_tok2vec}
vocab_data = null
lookups = null
before_init = null
after_init = null

[initialize.components]

[initialize.components.ner]

[initialize.components.ner.labels]
@readers = "spacy.read_labels.v1"
path = "new_mods/corpus/labels/ner.json"
require = false

[initialize.components.parser]

[initialize.components.parser.labels]
@readers = "spacy.read_labels.v1"
path = "new_mods/corpus/labels/parser.json"
require = false

[initialize.components.tagger]

[initialize.components.tagger.labels]
@readers = "spacy.read_labels.v1"
path = "new_mods/corpus/labels/tagger.json"
require = false

[initialize.components.textcat_multilabel]

[initialize.components.textcat_multilabel.labels]
@readers = "spacy.read_labels.v1"
path = "new_mods/corpus/labels/textcat_multilabel.json"
require = false

[initialize.tokenizer]
adrianeboyd commented 1 year ago

Let me first back up a step: your goal is to have a final pipeline with everything from en_core_web_trf + your new textcat component (non-transformer-based) trained on your data from prodigy?

If that's the case, then you can train the textcat model separately and "assemble" the final pipeline as the last step. It would look like this:

prodigy data-to-spacy out/ --textcat-multilabel dataset_name --eval-split 0.2
spacy train out/config.cfg --paths.train out/train.spacy --paths.dev out/dev.spacy -o training/

And then assemble. You can write a config to do with spacy assemble, but it's easier to do it programmatically:

import spacy
nlp = spacy.load("en_core_web_trf")
tcm_nlp = spacy.load("training/model-best")
tcm_nlp.replace_listeners("tok2vec", "textcat_multilabel", ["model.tok2vec"])
nlp.add_pipe("textcat_multilabel", source=tcm_nlp)
nlp.to_disk("/path/to/my_combined_pipeline")

In addition, the prodigy config defaults that you get with data-to-spacy are for the faster BOW-only textcat architecture, when you may see better performance with the ensemble classifier. The config could look like this:

[components.textcat_multilabel]
factory = "textcat_multilabel"
scorer = {"@scorers":"spacy.textcat_multilabel_scorer.v1"}
threshold = 0.5

[components.textcat_multilabel.model]
@architectures = "spacy.TextCatEnsemble.v2"
nO = null

[components.textcat_multilabel.model.linear_model]
@architectures = "spacy.TextCatBOW.v2"
exclusive_classes = false
ngram_size = 1
no_output_layer = false
nO = null

[components.textcat_multilabel.model.tok2vec]
@architectures = "spacy.Tok2VecListener.v1"
width = ${components.tok2vec.model.encode.width}
upstream = "*"

[components.tok2vec]
factory = "tok2vec"

[components.tok2vec.model]
@architectures = "spacy.Tok2Vec.v2"

[components.tok2vec.model.embed]
@architectures = "spacy.MultiHashEmbed.v2"
width = ${components.tok2vec.model.encode.width}
attrs = ["NORM","PREFIX","SUFFIX","SHAPE"]
rows = [5000,1000,2500,2500]
include_static_vectors = false

[components.tok2vec.model.encode]
@architectures = "spacy.MaxoutWindowEncoder.v2"
width = 256
depth = 8
window_size = 1
maxout_pieces = 3

(Ideally you'd be able to generate this with spacy init config -p textcat_multilabel, but the default options only allow for BOW (-o efficiency) or ensemble+static vectors (-o accuracy). The version above is modified by hand from -o accuracy to disable static vectors, which you don't have in the en_core_web_trf pipeline. If you used en_core_web_lg instead, you could keep the static vectors enabled.)

cbjrobertson commented 1 year ago

No, that's not what I want to do. What I was trying to do in the above code was simply re-produce the prodigy training procedure using spaCy. I ran into the bug above and wanted to flag it.

Perhaps this conversion would be better suited to spaCy and/or prodigy forums, but what I intend to do is train a textcat ensemble model which combines TextCatBOW with a transformer-based embedding layer with non-static vectors, i.e. I want to fine tune the transformer embeddings, if that's possible. Whereas your example config.cfg does not use transformer-based embeddings, if I'm reading it correctly. Any advice?

As an aside, given what you've mentioned above, what is the difference between calls to prodigy train textcat when --base-model is set to en_core_web_trf as compared to en_core_web_lg? From your explanation above, it sounds the classification layer is identical (i.e. spacy.TextCatBOW.v2). Is that really true?

Additionally, the code you suggest doesn't work. It fails with:

---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Input In [13], in <cell line: 5>()
      2 nlp = spacy.load("en_core_web_trf")
      3 tcm_nlp = spacy.load(f"./{EXP_NAME}/model/model-best")
----> 5 tcm_nlp.replace_listeners("tok2vec", "textcat_multilabel", ["model.tok2vec"])
      6 nlp.add_pipe("textcat_multilabel", source=tcm_nlp)
      7 nlp.to_disk(f"./{EXP_NAME}/test.cfg")

File ~/anaconda3/envs/prodigy/lib/python3.9/site-packages/spacy/language.py:1969, in Language.replace_listeners(self, tok2vec_name, pipe_name, listeners)
   1962 if tok2vec_name not in self.pipe_names:
   1963     err = Errors.E889.format(
   1964         tok2vec=tok2vec_name,
   1965         name=pipe_name,
   1966         unknown=tok2vec_name,
   1967         opts=", ".join(self.pipe_names),
   1968     )
-> 1969     raise ValueError(err)
   1970 if pipe_name not in self.pipe_names:
   1971     err = Errors.E889.format(
   1972         tok2vec=tok2vec_name,
   1973         name=pipe_name,
   1974         unknown=pipe_name,
   1975         opts=", ".join(self.pipe_names),
   1976     )

ValueError: [E889] Can't replace 'tok2vec' listeners of component 'textcat_multilabel' because 'tok2vec' is not in the pipeline. Available components: textcat_multilabel. If you didn't call nlp.replace_listeners manually, this is likely a bug in spaCy.
adrianeboyd commented 1 year ago

Let me convert this to a discussion...