Skip to content

Commit eb2d546

Browse files
authored
Merge branch 'master' into fix/listener_ref
2 parents a0e54df + 847be6e commit eb2d546

10 files changed

Lines changed: 112 additions & 42 deletions

File tree

azure-pipelines.yml

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,15 @@ jobs:
1212
Python36Linux:
1313
imageName: 'ubuntu-16.04'
1414
python.version: '3.6'
15-
Python36Mac:
16-
imageName: 'macos-10.14'
17-
python.version: '3.6'
18-
Python37Linux:
19-
imageName: 'ubuntu-16.04'
20-
python.version: '3.7'
2115
Python37Mac:
2216
imageName: 'macos-10.14'
2317
python.version: '3.7'
18+
Python38Linux:
19+
imageName: 'ubuntu-16.04'
20+
python.version: '3.8'
21+
Python38Mac:
22+
imageName: 'macos-10.14'
23+
python.version: '3.8'
2424
maxParallel: 4
2525
pool:
2626
vmImage: $(imageName)
@@ -32,15 +32,22 @@ jobs:
3232
architecture: 'x64'
3333

3434
- script: |
35-
python -m pip install --upgrade pip wheel
35+
python -m pip install --upgrade pip setuptools
3636
pip install -r requirements.txt
3737
displayName: 'Install dependencies'
3838
3939
- script: python setup.py sdist
4040
displayName: 'Build sdist'
4141

42+
- script: |
43+
pip freeze > installed.txt
44+
pip uninstall -y -r installed.txt
45+
displayName: "Uninstall all packages"
46+
4247
- script: pip install dist/*.tar.gz
4348
displayName: 'Install from sdist'
4449

45-
- script: python -m pytest spacy_transformers --cov=spacy_transformers
50+
- script: |
51+
pip install -r requirements.txt
52+
python -m pytest spacy_transformers --cov=spacy_transformers
4653
displayName: 'Run tests'

requirements.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
spacy-nightly>=3.0.0a39,<3.1.0
2-
transformers>=3.0.0,<3.1.0
2+
transformers>=3.0.0,<4.3.0
33
torch>=1.5.0
44
torchcontrib>=0.0.2,<0.1.0
55
srsly>=2.0.1,<3.0.0
66
ftfy>=5.0.0,<6.0.0
77
dataclasses>=0.6; python_version < "3.7"
88
importlib_metadata>=0.20; python_version < "3.8"
9-
pytokenizations>=0.2.0
9+
spacy-alignments>=0.7.2,<1.0.0
1010
# Testing
1111
pytest>=5.0.1,<5.1.0
1212
pytest-cov>=2.7.0,<2.8.0

setup.cfg

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[metadata]
2-
version = 1.0.0rc0
2+
version = 1.0.0rc1
33
description = spaCy pipelines for pre-trained BERT and other transformers
44
url = https://spacy.io
55
author = Explosion
@@ -28,14 +28,14 @@ include_package_data = true
2828
python_requires = >=3.6
2929
install_requires =
3030
spacy-nightly>=3.0.0a39,<3.1.0
31-
transformers>=3.0.0,<3.1.0
31+
transformers>=3.0.0,<4.3.0
3232
torch>=1.5.0
3333
torchcontrib>=0.0.2,<0.1.0
3434
srsly>=2.0.1,<3.0.0
3535
ftfy>=5.0.0,<6.0.0
3636
dataclasses>=0.6; python_version < "3.7"
3737
importlib_metadata>=0.20; python_version < "3.8"
38-
pytokenizations>=0.2.0
38+
spacy-alignments>=0.7.2,<1.0.0
3939

4040
setup_requires =
4141
setuptools
@@ -53,6 +53,14 @@ cuda92 =
5353
cupy-cuda92>=5.0.0b4
5454
cuda100 =
5555
cupy-cuda100>=5.0.0b4
56+
cuda101 =
57+
cupy-cuda101>=5.0.0b4
58+
cuda102 =
59+
cupy-cuda102>=5.0.0b4
60+
cuda110 =
61+
cupy-cuda110>=5.0.0b4
62+
cuda111 =
63+
cupy-cuda111>=5.0.0b4
5664

5765
[options.entry_points]
5866
spacy_factories =

spacy_transformers/align.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import numpy
2-
from typing import cast, Dict, List, Tuple, Callable, Set
3-
import tokenizations
2+
from typing import cast, Dict, List, Tuple, Callable, Set, Optional
3+
from spacy_alignments.tokenizations import get_alignments
44
from spacy.tokens import Span, Token
55
from thinc.api import Ops
66
from thinc.types import Ragged, Floats2d, Ints1d
@@ -100,7 +100,7 @@ def get_alignment_via_offset_mapping(spans: List[Span], token_data) -> Ragged:
100100
return align
101101

102102

103-
def get_alignment(spans: List[Span], wordpieces: List[List[str]]) -> Ragged:
103+
def get_alignment(spans: List[Span], wordpieces: List[List[str]], special_tokens: Optional[List[str]] = None) -> Ragged:
104104
"""Compute a ragged alignment array that records, for each unique token in
105105
`spans`, the corresponding indices in the flattened `wordpieces` array.
106106
For instance, imagine you have two overlapping spans:
@@ -135,14 +135,24 @@ def get_alignment(spans: List[Span], wordpieces: List[List[str]]) -> Ragged:
135135
"""
136136
if len(spans) != len(wordpieces):
137137
raise ValueError("Cannot align batches of different sizes.")
138+
if special_tokens is None:
139+
special_tokens = []
138140
# Tokens can occur more than once, and we need the alignment of each token
139141
# to its place in the concatenated wordpieces array.
140142
token_positions = get_token_positions(spans)
141143
alignment: List[Set[int]] = [set() for _ in range(len(token_positions))]
142144
wp_start = 0
143145
for i, (span, wp_toks) in enumerate(zip(spans, wordpieces)):
144146
sp_toks = [token.text for token in span]
145-
span2wp, wp2span = tokenizations.get_alignments(sp_toks, wp_toks)
147+
wp_toks_filtered = wp_toks
148+
# In the case that the special tokens do not appear in the text, filter
149+
# them out for alignment purposes so that special tokens like "<s>" are
150+
# not aligned to the character "s" in the text. (If the special tokens
151+
# appear in the text, it's not possible to distinguish them from the
152+
# added special tokens, so they may be aligned incorrectly.)
153+
if not any([special in span.text for special in special_tokens]):
154+
wp_toks_filtered = [tok if tok not in special_tokens else "" for tok in wp_toks]
155+
span2wp, wp2span = get_alignments(sp_toks, wp_toks_filtered)
146156
for token, wp_js in zip(span, span2wp):
147157
position = token_positions[token]
148158
alignment[position].update(wp_start + j for j in wp_js)

spacy_transformers/architectures.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
@registry.architectures.register("spacy-transformers.TransformerListener.v1")
1111
def transformer_listener_tok2vec_v1(
12-
pooling: Model[Ragged, Floats2d], grad_factor: float = 1.0
12+
pooling: Model[Ragged, Floats2d], grad_factor: float = 1.0, upstream: str = "*"
1313
) -> Model[List[Doc], List[Floats2d]]:
1414
"""Create a 'TransformerListener' layer, which will connect to a Transformer
1515
component earlier in the pipeline.
@@ -30,8 +30,13 @@ def transformer_listener_tok2vec_v1(
3030
them upstream. You can set this to 0 to "freeze" the transformer weights
3131
with respect to the component, or use it to make some components more
3232
significant than others. Leaving it at 1.0 is usually fine.
33+
upstream (str): A string to identify the 'upstream' Transformer
34+
to communicate with. The upstream name should either be the wildcard
35+
string '*', or the name of the `Transformer` component. You'll almost
36+
never have multiple upstream Transformer components, so the wildcard
37+
string will almost always be fine.
3338
"""
34-
listener = TransformerListener("transformer")
39+
listener = TransformerListener(upstream_name=upstream)
3540
model = chain(listener, trfs2arrays(pooling, grad_factor))
3641
model.set_ref("listener", listener)
3742
return model

spacy_transformers/data_classes.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -91,21 +91,21 @@ class FullTransformerBatch:
9191
tokens: BatchEncoding
9292
tensors: List[torch.Tensor]
9393
align: Ragged
94-
_doc_data: Optional[List[TransformerData]] = None
94+
cached_doc_data: Optional[List[TransformerData]] = None
9595

9696
@classmethod
9797
def empty(cls, nr_docs) -> "FullTransformerBatch":
9898
spans = [[] for i in range(nr_docs)]
9999
doc_data = [TransformerData.empty() for i in range(nr_docs)]
100100
align = Ragged(numpy.zeros((0,), dtype="i"), numpy.zeros((0,), dtype="i"))
101-
return cls(spans=spans, tokens={}, tensors=[], align=align, _doc_data=doc_data)
101+
return cls(spans=spans, tokens={}, tensors=[], align=align, cached_doc_data=doc_data)
102102

103103
@property
104104
def doc_data(self) -> List[TransformerData]:
105105
"""The outputs, split per spaCy Doc object."""
106-
if self._doc_data is None:
107-
self._doc_data = self.split_by_doc()
108-
return self._doc_data
106+
if self.cached_doc_data is None:
107+
self.cached_doc_data = self.split_by_doc()
108+
return self.cached_doc_data
109109

110110
def unsplit_by_doc(self, arrays: List[List[Floats3d]]) -> "FullTransformerBatch":
111111
"""Return a new FullTransformerBatch from a split batch of activations,

spacy_transformers/layers/transformer_model.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ def forward(
116116
# if "offset_mapping" in token_data and hasattr(token_data, "char_to_token"):
117117
# align = get_alignment_via_offset_mapping(flat_spans, token_data)
118118
# else:
119-
align = get_alignment(flat_spans, token_data["input_texts"])
119+
align = get_alignment(flat_spans, token_data["input_texts"], model.attrs["tokenizer"].all_special_tokens)
120120
output = FullTransformerBatch(
121121
spans=nested_spans, tokens=token_data, tensors=tensors, align=align
122122
)
@@ -147,6 +147,8 @@ def _convert_transformer_inputs(model, tokens: BatchEncoding, is_train):
147147

148148
def _convert_transformer_outputs(model, inputs_outputs, is_train):
149149
_, tensors = inputs_outputs
150+
if hasattr(tensors, "to_tuple"):
151+
tensors = tensors.to_tuple()
150152

151153
def backprop(d_tensors: List[torch.Tensor]) -> ArgsKwargs:
152154
return ArgsKwargs(args=(tensors,), kwargs={"grad_tensors": d_tensors})

spacy_transformers/tests/test_pipeline_component.py

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,22 @@ def component(vocab):
3232
return Transformer(Vocab(), DummyTransformer())
3333

3434

35+
@pytest.fixture(scope="module")
36+
def simple_nlp():
37+
nlp = Language()
38+
nlp.add_pipe("transformer")
39+
train_examples = []
40+
for t in TRAIN_DATA:
41+
train_examples.append(Example.from_dict(nlp.make_doc(t[0]), t[1]))
42+
43+
optimizer = nlp.initialize()
44+
for i in range(2):
45+
losses = {}
46+
nlp.update(train_examples, sgd=optimizer, losses=losses)
47+
48+
return nlp
49+
50+
3551
def test_init(component):
3652
assert isinstance(component.vocab, Vocab)
3753
assert isinstance(component.model, Model)
@@ -83,22 +99,19 @@ def test_listeners(component, docs):
8399
]
84100

85101

86-
def test_transformer_pipeline_simple():
102+
def test_transformer_pipeline_simple(simple_nlp):
87103
"""Test that a simple pipeline with just a transformer at least runs"""
88-
nlp = Language()
89-
nlp.add_pipe("transformer")
90-
train_examples = []
91-
for t in TRAIN_DATA:
92-
train_examples.append(Example.from_dict(nlp.make_doc(t[0]), t[1]))
93-
94-
optimizer = nlp.initialize()
95-
for i in range(2):
96-
losses = {}
97-
nlp.update(train_examples, sgd=optimizer, losses=losses)
98-
doc = nlp("We're interested at underwater basket weaving.")
104+
doc = simple_nlp("We're interested at underwater basket weaving.")
99105
assert doc
100106

101107

108+
def test_transformer_pipeline_long_token(simple_nlp):
109+
"""Test that a simple pipeline raises an error on a text that exceeds the
110+
model max length."""
111+
with pytest.raises(ValueError):
112+
doc = simple_nlp("https://example.com/" + "a/" * 1000)
113+
114+
102115
cfg_string = """
103116
[nlp]
104117
lang = "en"
@@ -116,12 +129,14 @@ def test_transformer_pipeline_simple():
116129
[components.tagger.model.tok2vec]
117130
@architectures = "spacy-transformers.TransformerListener.v1"
118131
grad_factor = 1.0
132+
upstream = ${components.transformer.name}
119133
120134
[components.tagger.model.tok2vec.pooling]
121135
@layers = "reduce_mean.v1"
122136
123137
[components.transformer]
124138
factory = "transformer"
139+
name = "custom_upstream"
125140
"""
126141

127142

@@ -135,6 +150,7 @@ def test_transformer_pipeline_tagger():
135150
tagger_trf = tagger.model.get_ref("tok2vec").layers[0]
136151
assert isinstance(transformer, Transformer)
137152
assert isinstance(tagger_trf, TransformerListener)
153+
assert tagger_trf.upstream_name == "custom_upstream"
138154
train_examples = []
139155
for t in TRAIN_DATA:
140156
train_examples.append(Example.from_dict(nlp.make_doc(t[0]), t[1]))

spacy_transformers/tests/util.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,21 @@ def __init__(self):
1616
self.int2str = {}
1717
self.start_symbol = "<s>"
1818
self.end_symbol = "</s>"
19+
self.model_max_length = 512
1920

20-
def batch_encode_plus(
21+
@property
22+
def all_special_tokens(self):
23+
return [self.start_symbol, self.end_symbol]
24+
25+
def __call__(
2126
self,
2227
texts,
2328
add_special_tokens=True,
2429
max_length=None,
2530
stride: int = 0,
2631
truncation_strategy="longest_first",
27-
pad_to_max_length=False,
32+
padding=False,
33+
truncation=False,
2834
is_pretokenized=False,
2935
return_tensors=None,
3036
return_token_type_ids=None,
@@ -48,11 +54,14 @@ def batch_encode_plus(
4854
output["attention_mask"].append(mask)
4955
output["token_type_ids"].append(type_ids)
5056
output["offset_mapping"].append(offsets)
51-
output = self._pad(output)
57+
if padding:
58+
output = self._pad(output)
5259
if return_tensors == "pt":
5360
output["input_ids"] = torch.tensor(output["input_ids"]) # type: ignore
5461
output["attention_mask"] = torch.tensor(output["attention_mask"]) # type: ignore
5562
output["token_type_ids"] = torch.tensor(output["token_type_ids"]) # type: ignore
63+
if return_length:
64+
output["length"] = torch.tensor([len(x) for x in output["input_ids"]]) # type: ignore
5665
return output
5766

5867
def convert_ids_to_tokens(self, ids: Union[List[int], torch.Tensor]) -> List[str]:

spacy_transformers/util.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,19 +38,32 @@ def huggingface_from_pretrained(source: Union[Path, str], config: Dict):
3838

3939
def huggingface_tokenize(tokenizer, texts: List[str]) -> BatchEncoding:
4040
"""Apply a Huggingface tokenizer to a batch of texts."""
41-
token_data = tokenizer.batch_encode_plus(
41+
token_data = tokenizer(
4242
texts,
4343
add_special_tokens=True,
4444
return_attention_mask=True,
4545
return_length=True,
4646
return_offsets_mapping=isinstance(tokenizer, PreTrainedTokenizerFast),
4747
return_tensors="pt",
4848
return_token_type_ids=None, # Sets to model default
49-
pad_to_max_length=True,
49+
padding="longest",
5050
)
5151
token_data["input_texts"] = [
5252
tokenizer.convert_ids_to_tokens(list(ids)) for ids in token_data["input_ids"]
5353
]
54+
# Because of padding=longest all sequences contain the same number of
55+
# tokens, so simply check whether the first sequence is too long.
56+
if (
57+
len(token_data["length"]) > 0
58+
and int(token_data["length"][0]) > tokenizer.model_max_length
59+
):
60+
for text, tokens in zip(texts, token_data["input_texts"]):
61+
# The longest text(s) in the batch will have another symbol (the end
62+
# symbol) rather than the pad token as the final token in the
63+
# sequence. Raise an error on the first such text found.
64+
if tokens[-1] != tokenizer.pad_token:
65+
msg = f"The following input span is too long for the model max length {tokenizer.model_max_length}. Preprocess the texts to break up long tokens or if you're training a new model, you may want to modify the spaCy tokenizer or the @span_getters in the model config:\n\n{text}"
66+
raise ValueError(msg)
5467
return token_data
5568

5669

0 commit comments

Comments
 (0)