diyclassics Claude Opus 4.8 (1M context) commited on
Commit
b8d6c6c
·
1 Parent(s): f25becd

fix: whitespace-split before subword encoding (full-string tokenization)

Browse files

The HF tokenizer's full-string path char-class-split the entire string
without dropping inter-word spaces (unlike tensor2tensor tokenizer.encode
and gen_berts.py's LatinTokenizer, which split on whitespace first). Every
space was escaped to `\32;_`, injecting 3 spurious subtokens between words
— input the model never trained on. This corrupted masked-word predictions
and broke subword->word alignment for the documented `tokenizer("sentence")`
usage, while the word-by-word case-study path stayed correct (which is why
validation passed). Reported in two independent bug reports.

Fix (slow + root copy + fast): split on whitespace first, then char-class
encode each word. Native full-string output now equals the original
per-word tokenization (verified: slow == fast IDs). Decode renders word
boundaries as spaces (BERT-style; "tres." -> "tres .").

Also fixes a latent fast-tokenizer bug: it ignored do_lower_case (no Rust
normalizer), escaping capitals to codepoints (`Gallia` -> `\71;allia`) —
the same class of bug v1.1.1 fixed for the slow tokenizer. Added a
Lowercase normalizer.

Tests: regenerated golden IDs (previous "reference" values contained the
space escapes), added full-string==per-word parity guards and a new
test_tokenizer_fast.py. Version bump + changelog deferred until cluster
evals confirm case-study metrics on the native path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

src/latincy_latinbert/tokenization_latin_bert.py CHANGED
@@ -187,13 +187,24 @@ class LatinBertTokenizer(PreTrainedTokenizer):
187
  return vocab
188
 
189
  def _tokenize(self, text: str, **kwargs) -> List[str]:
190
- """Tokenize text into subtoken strings."""
 
 
 
 
 
 
 
 
 
 
 
191
  if self.do_lower_case:
192
  text = text.lower()
193
- tokens = _tokenizer_encode(text)
194
  subtokens = []
195
- for token in tokens:
196
- subtokens.extend(self._token_to_subtokens(token))
 
197
  return subtokens
198
 
199
  def _token_to_subtokens(self, token: str) -> List[str]:
@@ -240,16 +251,22 @@ class LatinBertTokenizer(PreTrainedTokenizer):
240
  return self.unk_token
241
 
242
  def convert_tokens_to_string(self, tokens: List[str]) -> str:
243
- """Reverse the tokenization: unescape and join."""
 
 
 
 
 
 
244
  # Filter out special tokens before joining
245
  filtered = [t for t in tokens if t not in SPECIAL_TOKENS]
246
  text = "".join(filtered)
247
- # Remove trailing underscores (word boundary markers)
248
- # and unescape: \\u → _, \\\\ → \\, \\<digits>; → chr
249
- text = re.sub(r"(?<!\\)_", "", text)
250
  text = re.sub(r"\\(\d+);", lambda m: chr(int(m.group(1))), text)
251
  text = text.replace("\\u", "_").replace("\\\\", "\\")
252
- return text
253
 
254
  def build_inputs_with_special_tokens(
255
  self, token_ids_0: List[int], token_ids_1: Optional[List[int]] = None
 
187
  return vocab
188
 
189
  def _tokenize(self, text: str, **kwargs) -> List[str]:
190
+ """Tokenize text into subtoken strings.
191
+
192
+ Splits on whitespace first (like the original gen_berts.py
193
+ LatinTokenizer), then applies character-class splitting within
194
+ each word. This is essential: tensor2tensor's tokenizer.encode()
195
+ drops inter-word spaces, so feeding a full string through
196
+ _tokenizer_encode() directly would escape every space to
197
+ ``\\32;_`` and inject spurious subtokens between words, corrupting
198
+ the input the model was trained on and breaking word alignment.
199
+ Because whitespace-split words contain no spaces, no space escape
200
+ can be produced here.
201
+ """
202
  if self.do_lower_case:
203
  text = text.lower()
 
204
  subtokens = []
205
+ for word in text.split():
206
+ for token in _tokenizer_encode(word):
207
+ subtokens.extend(self._token_to_subtokens(token))
208
  return subtokens
209
 
210
  def _token_to_subtokens(self, token: str) -> List[str]:
 
251
  return self.unk_token
252
 
253
  def convert_tokens_to_string(self, tokens: List[str]) -> str:
254
+ """Reverse the tokenization: unescape and join.
255
+
256
+ Word boundaries are the trailing ``_`` markers, so each is
257
+ rendered as a space (BERT-style; punctuation spacing is lossy,
258
+ e.g. "tres." → "tres ."). Literal underscores are encoded as
259
+ ``\\u`` and are restored afterward.
260
+ """
261
  # Filter out special tokens before joining
262
  filtered = [t for t in tokens if t not in SPECIAL_TOKENS]
263
  text = "".join(filtered)
264
+ # Word-boundary underscores → spaces; then unescape:
265
+ # \\<digits>; → chr, \\u → _, \\\\ → \\
266
+ text = re.sub(r"(?<!\\)_", " ", text)
267
  text = re.sub(r"\\(\d+);", lambda m: chr(int(m.group(1))), text)
268
  text = text.replace("\\u", "_").replace("\\\\", "\\")
269
+ return text.strip()
270
 
271
  def build_inputs_with_special_tokens(
272
  self, token_ids_0: List[int], token_ids_1: Optional[List[int]] = None
tests/test_tokenizer.py CHANGED
@@ -63,23 +63,49 @@ class TestVocab:
63
 
64
 
65
  class TestEncoding:
66
- """Reference IDs from original LatinTokenizer (with +5 offset)."""
 
 
 
 
67
 
68
  def test_gallia(self, tokenizer):
69
  ids = tokenizer.encode("Gallia est omnis divisa in partes tres",
70
  add_special_tokens=False)
71
- # With do_lower_case=True, "Gallia" → "gallia_" (single token)
72
- expected = [6533, 32888, 7735, 13, 15, 32888, 7735, 13, 343,
73
- 32888, 7735, 13, 6773, 32888, 7735, 13, 12, 32888,
74
- 7735, 13, 568, 32888, 7735, 13, 564]
75
  assert ids == expected
76
 
77
  def test_arma(self, tokenizer):
78
  ids = tokenizer.encode("arma virumque cano",
79
  add_special_tokens=False)
80
- expected = [915, 32888, 7735, 13, 18566, 8107, 32888, 7735, 13, 4420]
81
  assert ids == expected
82
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
  def test_uppercase(self, tokenizer):
84
  """Uppercase input should be lowercased, not escaped to codepoints."""
85
  ids = tokenizer.encode("ROMA", add_special_tokens=False)
@@ -106,10 +132,12 @@ class TestRoundtrip:
106
  assert decoded == text
107
 
108
  def test_decode_with_punctuation(self, tokenizer):
 
 
109
  text = "gallia est omnis divisa in partes tres."
110
  ids = tokenizer.encode(text, add_special_tokens=False)
111
  decoded = tokenizer.decode(ids)
112
- assert decoded == text
113
 
114
  def test_decode_uppercase_lossy(self, tokenizer):
115
  """Uppercase input decodes to lowercase (lowercasing is lossy)."""
@@ -138,15 +166,14 @@ class TestLowercasing:
138
  def test_reasonable_expansion_ratio(self, tokenizer):
139
  """With lowercasing, proper nouns should not explode into codepoint escapes.
140
 
141
- Spaces are escaped by tensor2tensor's design (3 tokens each), so we
142
- count only word subtokens, excluding space-escape sequences.
143
  """
144
  text = "Cytherea Camenis Roma Gallia"
145
  tokens = tokenizer.tokenize(text)
146
- # Filter out space-escape tokens (\, 32, ;_)
147
- word_tokens = [t for t in tokens if t not in ("\\", "32", ";_")]
148
- words = text.split()
149
- ratio = len(word_tokens) / len(words)
150
  assert ratio < 2.5, f"Word expansion ratio {ratio:.1f}x is too high"
151
 
152
  def test_do_lower_case_false(self):
 
63
 
64
 
65
  class TestEncoding:
66
+ """Reference IDs from the original LatinTokenizer (with +5 offset).
67
+
68
+ The original whitespace-splits before subword encoding, so there are
69
+ NO inter-word space escapes. One clean subtoken per word here.
70
+ """
71
 
72
  def test_gallia(self, tokenizer):
73
  ids = tokenizer.encode("Gallia est omnis divisa in partes tres",
74
  add_special_tokens=False)
75
+ # do_lower_case=True → each word is a single subtoken; no space escapes
76
+ expected = [6533, 15, 343, 6773, 12, 568, 564]
 
 
77
  assert ids == expected
78
 
79
  def test_arma(self, tokenizer):
80
  ids = tokenizer.encode("arma virumque cano",
81
  add_special_tokens=False)
82
+ expected = [915, 18566, 8107, 4420]
83
  assert ids == expected
84
 
85
+ def test_no_interword_space_escapes(self, tokenizer):
86
+ """Full-string encoding must not inject space escapes between words.
87
+
88
+ The buggy full-string path emitted `\\ 32 ;_` (ids 32888, 7735, 13)
89
+ for every inter-word space — tokens the model never trained on.
90
+ """
91
+ ids = tokenizer.encode("gallia est omnis divisa in partes tres",
92
+ add_special_tokens=False)
93
+ assert 32888 not in ids and 7735 not in ids and 13 not in ids
94
+
95
+ def test_fullstring_matches_per_word(self, tokenizer):
96
+ """Native full-string tokenization == word-by-word tokenization.
97
+
98
+ This is the contract: `tokenizer(full_sentence)` reproduces the
99
+ original per-word LatinTokenizer output, so HF-native usage is
100
+ faithful without requiring is_split_into_words.
101
+ """
102
+ text = "Gallia est omnis divisa in partes tres, quarum unam incolunt Belgae"
103
+ full = tokenizer.tokenize(text)
104
+ per_word = []
105
+ for word in text.split():
106
+ per_word.extend(tokenizer.tokenize(word))
107
+ assert full == per_word
108
+
109
  def test_uppercase(self, tokenizer):
110
  """Uppercase input should be lowercased, not escaped to codepoints."""
111
  ids = tokenizer.encode("ROMA", add_special_tokens=False)
 
132
  assert decoded == text
133
 
134
  def test_decode_with_punctuation(self, tokenizer):
135
+ # Word boundaries decode to spaces (BERT-style), so attached
136
+ # punctuation becomes space-separated: "tres." -> "tres ."
137
  text = "gallia est omnis divisa in partes tres."
138
  ids = tokenizer.encode(text, add_special_tokens=False)
139
  decoded = tokenizer.decode(ids)
140
+ assert decoded == "gallia est omnis divisa in partes tres ."
141
 
142
  def test_decode_uppercase_lossy(self, tokenizer):
143
  """Uppercase input decodes to lowercase (lowercasing is lossy)."""
 
166
  def test_reasonable_expansion_ratio(self, tokenizer):
167
  """With lowercasing, proper nouns should not explode into codepoint escapes.
168
 
169
+ Whitespace-splitting means no inter-word space escapes, so every
170
+ subtoken belongs to a word.
171
  """
172
  text = "Cytherea Camenis Roma Gallia"
173
  tokens = tokenizer.tokenize(text)
174
+ # No space-escape tokens should be present at all
175
+ assert all(t not in ("\\", "32", ";_") for t in tokens)
176
+ ratio = len(tokens) / len(text.split())
 
177
  assert ratio < 2.5, f"Word expansion ratio {ratio:.1f}x is too high"
178
 
179
  def test_do_lower_case_false(self):
tests/test_tokenizer_fast.py ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Tests for LatinBertTokenizerFast (Rust-backed, word_ids support).
2
+
3
+ Guards the two fixes to the fast tokenizer:
4
+ 1. whitespace-split before char-class encoding (no inter-word space
5
+ escapes; correct word_ids alignment)
6
+ 2. lowercasing normalizer (do_lower_case), so capitalized input is not
7
+ escaped to codepoints — matching the slow tokenizer.
8
+ """
9
+
10
+ import importlib.util
11
+ from pathlib import Path
12
+
13
+ import pytest
14
+
15
+ REPO_ROOT = Path(__file__).resolve().parent.parent
16
+ VOCAB_FILE = str(Path(__file__).parent / "latin.subword.encoder")
17
+
18
+
19
+ def _load_fast_module():
20
+ path = REPO_ROOT / "tokenization_latin_bert_fast.py"
21
+ spec = importlib.util.spec_from_file_location("tlbf_under_test", path)
22
+ module = importlib.util.module_from_spec(spec)
23
+ spec.loader.exec_module(module)
24
+ return module
25
+
26
+
27
+ @pytest.fixture(scope="module")
28
+ def fast_module():
29
+ return _load_fast_module()
30
+
31
+
32
+ @pytest.fixture
33
+ def fast(fast_module):
34
+ return fast_module.LatinBertTokenizerFast(vocab_file=VOCAB_FILE)
35
+
36
+
37
+ @pytest.fixture
38
+ def slow():
39
+ from latincy_latinbert import LatinBertTokenizer
40
+ return LatinBertTokenizer(vocab_file=VOCAB_FILE)
41
+
42
+
43
+ class TestFastTokenizer:
44
+ def test_no_interword_space_escapes(self, fast):
45
+ enc = fast("gallia est omnis divisa in partes tres")
46
+ toks = fast.convert_ids_to_tokens(enc["input_ids"])
47
+ assert all(t not in ("\\", "32", ";_") for t in toks)
48
+
49
+ def test_word_ids_alignment(self, fast):
50
+ enc = fast("Gallia est omnis")
51
+ # [CLS]=None, gallia=0, est=1, omnis=2, [SEP]=None
52
+ assert enc.word_ids() == [None, 0, 1, 2, None]
53
+
54
+ def test_lowercases_like_slow(self, fast):
55
+ """Capitalized input must not produce codepoint escapes."""
56
+ enc = fast("Gallia")
57
+ toks = fast.convert_ids_to_tokens(enc["input_ids"])
58
+ assert "gallia_" in toks
59
+ assert all(not t.isdigit() for t in toks)
60
+
61
+ def test_matches_slow_ids(self, fast, slow):
62
+ for text in ["Gallia est omnis divisa in partes tres",
63
+ "arma virumque cano",
64
+ "ROMA",
65
+ "gallia est omnis divisa in partes tres."]:
66
+ s = slow.encode(text, add_special_tokens=False)
67
+ f = fast.encode(text, add_special_tokens=False)
68
+ assert s == f, f"slow/fast mismatch for {text!r}: {s} != {f}"
tokenization_latin_bert.py CHANGED
@@ -187,13 +187,24 @@ class LatinBertTokenizer(PreTrainedTokenizer):
187
  return vocab
188
 
189
  def _tokenize(self, text: str, **kwargs) -> List[str]:
190
- """Tokenize text into subtoken strings."""
 
 
 
 
 
 
 
 
 
 
 
191
  if self.do_lower_case:
192
  text = text.lower()
193
- tokens = _tokenizer_encode(text)
194
  subtokens = []
195
- for token in tokens:
196
- subtokens.extend(self._token_to_subtokens(token))
 
197
  return subtokens
198
 
199
  def _token_to_subtokens(self, token: str) -> List[str]:
@@ -240,16 +251,22 @@ class LatinBertTokenizer(PreTrainedTokenizer):
240
  return self.unk_token
241
 
242
  def convert_tokens_to_string(self, tokens: List[str]) -> str:
243
- """Reverse the tokenization: unescape and join."""
 
 
 
 
 
 
244
  # Filter out special tokens before joining
245
  filtered = [t for t in tokens if t not in SPECIAL_TOKENS]
246
  text = "".join(filtered)
247
- # Remove trailing underscores (word boundary markers)
248
- # and unescape: \\u → _, \\\\ → \\, \\<digits>; → chr
249
- text = re.sub(r"(?<!\\)_", "", text)
250
  text = re.sub(r"\\(\d+);", lambda m: chr(int(m.group(1))), text)
251
  text = text.replace("\\u", "_").replace("\\\\", "\\")
252
- return text
253
 
254
  def build_inputs_with_special_tokens(
255
  self, token_ids_0: List[int], token_ids_1: Optional[List[int]] = None
 
187
  return vocab
188
 
189
  def _tokenize(self, text: str, **kwargs) -> List[str]:
190
+ """Tokenize text into subtoken strings.
191
+
192
+ Splits on whitespace first (like the original gen_berts.py
193
+ LatinTokenizer), then applies character-class splitting within
194
+ each word. This is essential: tensor2tensor's tokenizer.encode()
195
+ drops inter-word spaces, so feeding a full string through
196
+ _tokenizer_encode() directly would escape every space to
197
+ ``\\32;_`` and inject spurious subtokens between words, corrupting
198
+ the input the model was trained on and breaking word alignment.
199
+ Because whitespace-split words contain no spaces, no space escape
200
+ can be produced here.
201
+ """
202
  if self.do_lower_case:
203
  text = text.lower()
 
204
  subtokens = []
205
+ for word in text.split():
206
+ for token in _tokenizer_encode(word):
207
+ subtokens.extend(self._token_to_subtokens(token))
208
  return subtokens
209
 
210
  def _token_to_subtokens(self, token: str) -> List[str]:
 
251
  return self.unk_token
252
 
253
  def convert_tokens_to_string(self, tokens: List[str]) -> str:
254
+ """Reverse the tokenization: unescape and join.
255
+
256
+ Word boundaries are the trailing ``_`` markers, so each is
257
+ rendered as a space (BERT-style; punctuation spacing is lossy,
258
+ e.g. "tres." → "tres ."). Literal underscores are encoded as
259
+ ``\\u`` and are restored afterward.
260
+ """
261
  # Filter out special tokens before joining
262
  filtered = [t for t in tokens if t not in SPECIAL_TOKENS]
263
  text = "".join(filtered)
264
+ # Word-boundary underscores → spaces; then unescape:
265
+ # \\<digits>; → chr, \\u → _, \\\\ → \\
266
+ text = re.sub(r"(?<!\\)_", " ", text)
267
  text = re.sub(r"\\(\d+);", lambda m: chr(int(m.group(1))), text)
268
  text = text.replace("\\u", "_").replace("\\\\", "\\")
269
+ return text.strip()
270
 
271
  def build_inputs_with_special_tokens(
272
  self, token_ids_0: List[int], token_ids_1: Optional[List[int]] = None
tokenization_latin_bert_fast.py CHANGED
@@ -15,7 +15,7 @@ Usage:
15
  "latincy/latin-bert", trust_remote_code=True, use_fast=True
16
  )
17
  enc = tokenizer("Gallia est omnis", return_tensors="pt")
18
- enc.word_ids() # [None, 0, 0, 1, 2, 2, None]
19
  """
20
 
21
  import os
@@ -23,7 +23,7 @@ import re
23
  import unicodedata
24
  from typing import Dict, List, Optional, Tuple
25
 
26
- from tokenizers import Tokenizer, pre_tokenizers, processors
27
  from tokenizers.models import WordPiece
28
  from tokenizers.pre_tokenizers import PreTokenizer
29
  from transformers import PreTrainedTokenizerFast
@@ -86,8 +86,11 @@ def _escape_token(token: str, alphabet: set) -> str:
86
  class _LatinBertPreTokenizer:
87
  """Custom pre-tokenizer: character-class split + escape + append '_'.
88
 
89
- In is_split_into_words mode, each word is processed individually
90
- (char-class split is usually a no-op for single tokens).
 
 
 
91
  """
92
 
93
  def __init__(self, alphabet: set):
@@ -124,7 +127,7 @@ NUM_SPECIAL = 5
124
  VOCAB_FILES_NAMES = {"vocab_file": "latin.subword.encoder"}
125
 
126
 
127
- def _build_backend_tokenizer(vocab_file: str) -> Tokenizer:
128
  """Build a tokenizers.Tokenizer from the SubwordTextEncoder vocab."""
129
  # Load subword vocab
130
  subtoken_strings = []
@@ -159,10 +162,24 @@ def _build_backend_tokenizer(vocab_file: str) -> Tokenizer:
159
  )
160
  tokenizer = Tokenizer(model)
161
 
162
- # Custom pre-tokenizer: char-class split + escape + append '_'
163
- tokenizer.pre_tokenizer = PreTokenizer.custom(
164
- _LatinBertPreTokenizer(alphabet)
165
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
166
 
167
  # BertProcessing: adds [CLS] at start, [SEP] at end
168
  tokenizer.post_processor = processors.BertProcessing(
@@ -195,6 +212,7 @@ class LatinBertTokenizerFast(PreTrainedTokenizerFast):
195
  self,
196
  vocab_file: Optional[str] = None,
197
  tokenizer_object: Optional[Tokenizer] = None,
 
198
  pad_token: str = "[PAD]",
199
  unk_token: str = "[UNK]",
200
  cls_token: str = "[CLS]",
@@ -204,7 +222,10 @@ class LatinBertTokenizerFast(PreTrainedTokenizerFast):
204
  **kwargs,
205
  ):
206
  if tokenizer_object is None and vocab_file is not None:
207
- tokenizer_object = _build_backend_tokenizer(vocab_file)
 
 
 
208
 
209
  # PreTrainedTokenizerFast.__init__ does deepcopy(tokenizer_object),
210
  # which fails for custom Python pre-tokenizers. Bypass by setting
@@ -234,8 +255,13 @@ class LatinBertTokenizerFast(PreTrainedTokenizerFast):
234
 
235
  @staticmethod
236
  def _unescape(text: str) -> str:
237
- """Reverse the t2t escape encoding used by the pre-tokenizer."""
238
- text = re.sub(r"(?<!\\)_", "", text)
 
 
 
 
 
239
  text = re.sub(r"\\(\d+);", lambda m: chr(int(m.group(1))), text)
240
  text = text.replace("\\u", "_").replace("\\\\", "\\")
241
  return text.strip()
 
15
  "latincy/latin-bert", trust_remote_code=True, use_fast=True
16
  )
17
  enc = tokenizer("Gallia est omnis", return_tensors="pt")
18
+ enc.word_ids() # [None, 0, 1, 2, None] (each word maps to its subtokens)
19
  """
20
 
21
  import os
 
23
  import unicodedata
24
  from typing import Dict, List, Optional, Tuple
25
 
26
+ from tokenizers import Tokenizer, normalizers, pre_tokenizers, processors
27
  from tokenizers.models import WordPiece
28
  from tokenizers.pre_tokenizers import PreTokenizer
29
  from transformers import PreTrainedTokenizerFast
 
86
  class _LatinBertPreTokenizer:
87
  """Custom pre-tokenizer: character-class split + escape + append '_'.
88
 
89
+ Applied AFTER a WhitespaceSplit pre-tokenizer (see
90
+ _build_backend_tokenizer), so it receives one whitespace-delimited
91
+ word at a time and splits it at alphanumeric/non-alphanumeric
92
+ boundaries (e.g. "tres," → "tres_", ",_"). It never sees inter-word
93
+ spaces, so no `\\32;_` space-escapes are produced.
94
  """
95
 
96
  def __init__(self, alphabet: set):
 
127
  VOCAB_FILES_NAMES = {"vocab_file": "latin.subword.encoder"}
128
 
129
 
130
+ def _build_backend_tokenizer(vocab_file: str, do_lower_case: bool = True) -> Tokenizer:
131
  """Build a tokenizers.Tokenizer from the SubwordTextEncoder vocab."""
132
  # Load subword vocab
133
  subtoken_strings = []
 
162
  )
163
  tokenizer = Tokenizer(model)
164
 
165
+ # Normalizer: lowercase (the vocab was trained on lowercased text;
166
+ # matches the slow tokenizer's do_lower_case=True). Without this,
167
+ # uppercase chars fall outside the alphabet and get escaped to their
168
+ # codepoints (e.g. 'G' → `\71;`), inflating tokens and producing
169
+ # embeddings the model never saw.
170
+ if do_lower_case:
171
+ tokenizer.normalizer = normalizers.Lowercase()
172
+
173
+ # Pre-tokenizer: whitespace-split FIRST (drops inter-word spaces,
174
+ # keeps punctuation attached), then char-class split + escape + '_'
175
+ # per word. Splitting on whitespace first is essential — otherwise
176
+ # spaces are escaped to `\32;_` and injected between words, corrupting
177
+ # the model input and breaking word_ids() alignment. WhitespaceSplit
178
+ # preserves offsets, so word_ids() maps subtokens to the right words.
179
+ tokenizer.pre_tokenizer = pre_tokenizers.Sequence([
180
+ pre_tokenizers.WhitespaceSplit(),
181
+ PreTokenizer.custom(_LatinBertPreTokenizer(alphabet)),
182
+ ])
183
 
184
  # BertProcessing: adds [CLS] at start, [SEP] at end
185
  tokenizer.post_processor = processors.BertProcessing(
 
212
  self,
213
  vocab_file: Optional[str] = None,
214
  tokenizer_object: Optional[Tokenizer] = None,
215
+ do_lower_case: bool = True,
216
  pad_token: str = "[PAD]",
217
  unk_token: str = "[UNK]",
218
  cls_token: str = "[CLS]",
 
222
  **kwargs,
223
  ):
224
  if tokenizer_object is None and vocab_file is not None:
225
+ tokenizer_object = _build_backend_tokenizer(
226
+ vocab_file, do_lower_case=do_lower_case
227
+ )
228
+ self.do_lower_case = do_lower_case
229
 
230
  # PreTrainedTokenizerFast.__init__ does deepcopy(tokenizer_object),
231
  # which fails for custom Python pre-tokenizers. Bypass by setting
 
255
 
256
  @staticmethod
257
  def _unescape(text: str) -> str:
258
+ """Reverse the t2t escape encoding used by the pre-tokenizer.
259
+
260
+ Word-boundary underscores become spaces (BERT-style decode;
261
+ punctuation spacing is lossy). Literal underscores are encoded
262
+ as ``\\u`` and restored afterward.
263
+ """
264
+ text = re.sub(r"(?<!\\)_", " ", text)
265
  text = re.sub(r"\\(\d+);", lambda m: chr(int(m.group(1))), text)
266
  text = text.replace("\\u", "_").replace("\\\\", "\\")
267
  return text.strip()