Skip to content

Commit 0c6000d

Browse files
committed
Fix replacing Transformer listener (#277)
* failing test for replace listeners * replace listener attr * add methods for IO of inhouse Tok2VecTransformer * add custom hack to rewrite transformer config * cleanup * to_bytes and from_bytes fails for transformer-based pipeline * simple from_bytes failing test * from_bytes works if we call initialize first * fix test * ensure predictions are still the same after replacing the listener * clean up comments * bump versions * fix initialize nlp default value * add comment
1 parent 913d23d commit 0c6000d

10 files changed

Lines changed: 229 additions & 12 deletions

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
spacy>=3.0.0,<4.0.0
1+
spacy>=3.1.0,<4.0.0
22
transformers>=3.4.0,<4.10.0
33
torch>=1.5.0
44
srsly>=2.4.0,<3.0.0

setup.cfg

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[metadata]
2-
version = 1.0.4
2+
version = 1.0.3
33
description = spaCy pipelines for pre-trained BERT and other transformers
44
url = https://spacy.io
55
author = Explosion
@@ -27,7 +27,7 @@ zip_safe = false
2727
include_package_data = true
2828
python_requires = >=3.6
2929
install_requires =
30-
spacy>=3.0.0,<4.0.0
30+
spacy>=3.1.0,<4.0.0
3131
transformers>=3.4.0,<4.10.0
3232
torch>=1.5.0
3333
srsly>=2.4.0,<3.0.0

spacy_transformers/architectures.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ def transformer_listener_tok2vec_v1(
4242
return model
4343

4444

45+
# Note: when updating, also make sure to update 'replace_listener_cfg' in _util.py
4546
@registry.architectures.register("spacy-transformers.Tok2VecTransformer.v1")
4647
def transformer_tok2vec_v1(
4748
name: str,

spacy_transformers/layers/_util.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
from thinc.api import Model, chain
2+
from .split_trf import split_trf_batch
3+
4+
5+
def replace_listener(model):
6+
return chain(model, split_trf_batch())
7+
8+
9+
def replace_listener_cfg(tok2vec_model_cfg, listener_model_cfg):
10+
result = tok2vec_model_cfg
11+
if (
12+
"TransformerModel" in tok2vec_model_cfg["@architectures"]
13+
and "TransformerListener" in listener_model_cfg["@architectures"]
14+
):
15+
result["@architectures"] = "spacy-transformers.Tok2VecTransformer.v1"
16+
if "pooling" in listener_model_cfg and "pooling" not in result:
17+
result["pooling"] = listener_model_cfg["pooling"]
18+
if "grad_factor" in listener_model_cfg and "grad_factor" not in result:
19+
result["grad_factor"] = listener_model_cfg["pooling"]
20+
21+
return result

spacy_transformers/layers/split_trf.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from thinc.api import Model
1+
from thinc.api import Model, chain
22
from typing import List
33
from ..data_classes import FullTransformerBatch, TransformerData
44

spacy_transformers/layers/transformer_model.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
from typing import List, Tuple, Callable
2+
3+
from spacy_transformers.layers._util import replace_listener, replace_listener_cfg
4+
from thinc.types import ArgsKwargs
25
import torch
36
from spacy.tokens import Doc
4-
from thinc.api import PyTorchWrapper, Model, xp2torch
5-
from thinc.types import ArgsKwargs
6-
from transformers.tokenization_utils import BatchEncoding
7+
from thinc.api import PyTorchWrapper, Model, xp2torch, chain
78
import logging
89

910
from ..data_classes import FullTransformerBatch, WordpieceBatch
1011
from ..util import huggingface_tokenize, huggingface_from_pretrained
1112
from ..util import find_last_hidden, maybe_flush_pytorch_cache
12-
from ..truncate import truncate_oversize_splits
1313
from ..util import log_gpu_memory, log_batch_size
14+
from ..truncate import truncate_oversize_splits
1415
from ..align import get_alignment
1516

1617

@@ -41,6 +42,8 @@ def TransformerModel(
4142
"set_transformer": set_pytorch_transformer,
4243
"has_transformer": False,
4344
"flush_cache_chance": 0.0,
45+
"replace_listener": replace_listener,
46+
"replace_listener_cfg": replace_listener_cfg,
4447
},
4548
)
4649

spacy_transformers/pipeline_component.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -328,7 +328,7 @@ def get_loss(self, docs, golds, scores):
328328
pass
329329

330330
def initialize(
331-
self, get_examples: Callable[[], Iterable[Example]], *, nlp: Optional[Language]
331+
self, get_examples: Callable[[], Iterable[Example]], *, nlp: Optional[Language] = None
332332
):
333333
"""Initialize the pipe for training, using data examples if available.
334334

spacy_transformers/tests/test_pipeline_component.py

Lines changed: 83 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ def test_transformer_pipeline_long_token(simple_nlp):
145145
"""
146146

147147

148-
def test_transformer_pipeline_tagger():
148+
def test_transformer_pipeline_tagger_listener():
149149
"""Test that a pipeline with just a transformer+tagger runs and trains properly"""
150150
orig_config = Config().from_str(cfg_string)
151151
nlp = util.load_model_from_config(orig_config, auto_fill=True, validate=True)
@@ -171,7 +171,8 @@ def test_transformer_pipeline_tagger():
171171
losses = {}
172172
nlp.update(train_examples, sgd=optimizer, losses=losses)
173173

174-
doc = nlp("We're interested at underwater basket weaving.")
174+
text = "We're interested at underwater basket weaving."
175+
doc = nlp(text)
175176
doc_tensor = tagger_trf.predict([doc])
176177
assert_equal(doc._.trf_data.tensors, doc_tensor[0].tensors)
177178

@@ -180,12 +181,23 @@ def test_transformer_pipeline_tagger():
180181
file_path = d / "trained_nlp"
181182
nlp.to_disk(file_path)
182183
nlp2 = util.load_model_from_path(file_path)
183-
doc = nlp2("We're interested at underwater basket weaving.")
184+
doc = nlp2(text)
184185
tagger2 = nlp2.get_pipe("tagger")
185186
tagger_trf2 = tagger2.model.get_ref("tok2vec").layers[0]
186187
doc_tensor2 = tagger_trf2.predict([doc])
187188
assert_equal(doc_tensor2[0].tensors, doc_tensor[0].tensors)
188189

190+
# ensure to_bytes / from_bytes works
191+
nlp_bytes = nlp.to_bytes()
192+
nlp3 = util.load_model_from_config(orig_config, auto_fill=True, validate=True)
193+
nlp3.initialize(lambda: train_examples)
194+
nlp3.from_bytes(nlp_bytes)
195+
doc = nlp3(text)
196+
tagger3 = nlp3.get_pipe("tagger")
197+
tagger_trf3 = tagger3.model.get_ref("tok2vec").layers[0]
198+
doc_tensor3 = tagger_trf3.predict([doc])
199+
assert_equal(doc_tensor3[0].tensors, doc_tensor[0].tensors)
200+
189201

190202
def test_transformer_pipeline_empty():
191203
"""Test that the pipeline doesn't fail with empty input"""
@@ -255,3 +267,71 @@ def test_multiprocessing(simple_nlp, texts):
255267

256268
for doc, expected_doc in zip(docs, expecteds):
257269
assert_docs_equal(doc, expected_doc)
270+
271+
def test_replace_listeners():
272+
orig_config = Config().from_str(cfg_string)
273+
nlp = util.load_model_from_config(orig_config, auto_fill=True, validate=True)
274+
text = "This is awesome"
275+
examples = [Example.from_dict(nlp.make_doc(text), {"tags": ["A", "B", "C"]})]
276+
optimizer = nlp.initialize(lambda: examples)
277+
# verify correct configuration with transformer listener
278+
transformer = nlp.get_pipe("transformer")
279+
tagger = nlp.get_pipe("tagger")
280+
tagger_tok2vec = tagger.model.get_ref("tok2vec")
281+
tagger_listener = tagger_tok2vec.get_ref("listener")
282+
assert isinstance(tagger_listener, TransformerListener)
283+
assert transformer.listener_map["tagger"][0] == tagger_listener
284+
assert (
285+
nlp.config["components"]["transformer"]["model"]["@architectures"]
286+
== "spacy-transformers.TransformerModel.v1"
287+
)
288+
assert (
289+
nlp.config["components"]["tagger"]["model"]["tok2vec"]["@architectures"]
290+
== "spacy-transformers.TransformerListener.v1"
291+
)
292+
# train pipe before replacing listeners
293+
for i in range(2):
294+
losses = {}
295+
nlp.update(examples, sgd=optimizer, losses=losses)
296+
doc = nlp(text)
297+
298+
preds = [t.tag_ for t in doc]
299+
doc_tensor = tagger_tok2vec.predict([doc])
300+
301+
# replace listener and verify predictions are still the same
302+
nlp.replace_listeners("transformer", "tagger", ["model.tok2vec"])
303+
tagger = nlp.get_pipe("tagger")
304+
tagger_tok2vec = tagger.model.get_ref("tok2vec")
305+
assert tagger_tok2vec.layers[0].layers[0].name == "transformer"
306+
assert (
307+
nlp.config["components"]["tagger"]["model"]["tok2vec"]["@architectures"]
308+
== "spacy-transformers.Tok2VecTransformer.v1"
309+
)
310+
doc2 = nlp(text)
311+
assert preds == [t.tag_ for t in doc2]
312+
assert_equal(doc_tensor, tagger_tok2vec.predict([doc2]))
313+
# attempt training with the new pipeline
314+
optimizer = nlp.resume_training()
315+
for i in range(2):
316+
losses = {}
317+
nlp.update(examples, sgd=optimizer, losses=losses)
318+
assert losses["tagger"] > 0.0
319+
320+
321+
def test_replace_listeners_invalid():
322+
orig_config = Config().from_str(cfg_string)
323+
nlp = util.load_model_from_config(orig_config, auto_fill=True, validate=True)
324+
text = "This is awesome"
325+
examples = [Example.from_dict(nlp.make_doc(text), {"tags": ["A", "B", "C"]})]
326+
optimizer = nlp.initialize(lambda: examples)
327+
for i in range(2):
328+
losses = {}
329+
nlp.update(examples, sgd=optimizer, losses=losses)
330+
with pytest.raises(ValueError):
331+
nlp.replace_listeners("invalid", "tagger", ["model.tok2vec"])
332+
with pytest.raises(ValueError):
333+
nlp.replace_listeners("transformer", "parser", ["model.tok2vec"])
334+
with pytest.raises(ValueError):
335+
nlp.replace_listeners("transformer", "tagger", ["model.yolo"])
336+
with pytest.raises(ValueError):
337+
nlp.replace_listeners("transformer", "tagger", ["model.tok2vec", "model.yolo"])

spacy_transformers/tests/test_serialize.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from spacy import Language
2+
13
from spacy_transformers import TransformerData
24
import srsly
35

@@ -7,3 +9,25 @@ def test_serialize_transformer_data():
79
bytes_data = srsly.msgpack_dumps(data)
810
new_data = srsly.msgpack_loads(bytes_data)
911
assert isinstance(new_data["x"], TransformerData)
12+
13+
14+
def test_transformer_tobytes():
15+
nlp = Language()
16+
trf = nlp.add_pipe("transformer")
17+
trf_bytes = trf.to_bytes()
18+
19+
nlp2 = Language()
20+
trf2 = nlp2.add_pipe("transformer")
21+
trf2.from_bytes(trf_bytes)
22+
23+
24+
def test_transformer_model_tobytes():
25+
nlp = Language()
26+
trf = nlp.add_pipe("transformer")
27+
nlp.initialize()
28+
trf_bytes = trf.to_bytes()
29+
30+
nlp2 = Language()
31+
trf2 = nlp2.add_pipe("transformer")
32+
nlp2.initialize()
33+
trf2.from_bytes(trf_bytes)
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import pytest
2+
from spacy.training.example import Example
3+
from spacy.util import make_tempdir
4+
from spacy import util
5+
from thinc.api import Model, Config
6+
from numpy.testing import assert_equal
7+
8+
9+
TRAIN_DATA = [
10+
("I like green eggs", {"tags": ["N", "V", "J", "N"]}),
11+
("Eat blue ham", {"tags": ["V", "J", "N"]}),
12+
]
13+
14+
15+
cfg_string = """
16+
[nlp]
17+
lang = "en"
18+
pipeline = ["tagger"]
19+
20+
[components]
21+
22+
[components.tagger]
23+
factory = "tagger"
24+
25+
[components.tagger.model]
26+
@architectures = "spacy.Tagger.v1"
27+
nO = null
28+
29+
[components.tagger.model.tok2vec]
30+
@architectures = "spacy-transformers.Tok2VecTransformer.v1"
31+
name = "albert-base-v2"
32+
tokenizer_config = {"use_fast": false}
33+
grad_factor = 1.0
34+
35+
[components.tagger.model.tok2vec.get_spans]
36+
@span_getters = "spacy-transformers.strided_spans.v1"
37+
window = 256
38+
stride = 256
39+
40+
[components.tagger.model.tok2vec.pooling]
41+
@layers = "reduce_mean.v1"
42+
"""
43+
44+
45+
def test_transformer_pipeline_tagger_internal():
46+
"""Test that a tagger with internal transformer runs and trains properly"""
47+
orig_config = Config().from_str(cfg_string)
48+
nlp = util.load_model_from_config(orig_config, auto_fill=True, validate=True)
49+
assert nlp.pipe_names == ["tagger"]
50+
tagger = nlp.get_pipe("tagger")
51+
tagger_trf = tagger.model.get_ref("tok2vec").layers[0]
52+
assert isinstance(tagger_trf, Model)
53+
train_examples = []
54+
for t in TRAIN_DATA:
55+
train_examples.append(Example.from_dict(nlp.make_doc(t[0]), t[1]))
56+
for tag in t[1]["tags"]:
57+
tagger.add_label(tag)
58+
59+
optimizer = nlp.initialize(lambda: train_examples)
60+
for i in range(2):
61+
losses = {}
62+
nlp.update(train_examples, sgd=optimizer, losses=losses)
63+
64+
doc = nlp("We're interested at underwater basket weaving.")
65+
doc_tensor = tagger_trf.predict([doc])
66+
67+
# ensure IO goes OK
68+
with make_tempdir() as d:
69+
file_path = d / "trained_nlp"
70+
nlp.to_disk(file_path)
71+
nlp2 = util.load_model_from_config(orig_config, auto_fill=True, validate=True)
72+
nlp2.initialize(lambda: train_examples)
73+
74+
# results are not the same if we don't call from_disk
75+
doc2 = nlp2("We're interested at underwater basket weaving.")
76+
tagger2 = nlp2.get_pipe("tagger")
77+
tagger_trf2 = tagger2.model.get_ref("tok2vec").layers[0]
78+
doc_tensor2 = tagger_trf2.predict([doc2])
79+
with pytest.raises(AssertionError):
80+
assert_equal(doc_tensor2.doc_data[0].tensors, doc_tensor.doc_data[0].tensors)
81+
82+
# results ARE the same if we call from_disk
83+
nlp2.from_disk(file_path)
84+
doc2 = nlp2("We're interested at underwater basket weaving.")
85+
tagger2 = nlp2.get_pipe("tagger")
86+
tagger_trf2 = tagger2.model.get_ref("tok2vec").layers[0]
87+
doc_tensor2 = tagger_trf2.predict([doc2])
88+
assert_equal(doc_tensor2.doc_data[0].tensors, doc_tensor.doc_data[0].tensors)

0 commit comments

Comments
 (0)