Instructions to use latincy/latin-bert with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use latincy/latin-bert with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("fill-mask", model="latincy/latin-bert")# pip install -U transformers accelerate # Load model directly from transformers import AutoTokenizer, AutoModel tokenizer = AutoTokenizer.from_pretrained("latincy/latin-bert") model = AutoModel.from_pretrained("latincy/latin-bert", device_map="auto") - Notebooks
- Google Colab
- Kaggle
fix: whitespace-split before subword encoding (full-string tokenization)
Browse filesThe 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 +26 -9
- tests/test_tokenizer.py +40 -13
- tests/test_tokenizer_fast.py +68 -0
- tokenization_latin_bert.py +26 -9
- tokenization_latin_bert_fast.py +38 -12
|
@@ -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
|
| 196 |
-
|
|
|
|
| 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 |
-
#
|
| 248 |
-
#
|
| 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
|
|
@@ -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 |
-
#
|
| 72 |
-
expected = [6533,
|
| 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,
|
| 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 ==
|
| 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 |
-
|
| 142 |
-
|
| 143 |
"""
|
| 144 |
text = "Cytherea Camenis Roma Gallia"
|
| 145 |
tokens = tokenizer.tokenize(text)
|
| 146 |
-
#
|
| 147 |
-
|
| 148 |
-
|
| 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):
|
|
@@ -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}"
|
|
@@ -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
|
| 196 |
-
|
|
|
|
| 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 |
-
#
|
| 248 |
-
#
|
| 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
|
|
@@ -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,
|
| 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 |
-
|
| 90 |
-
|
|
|
|
|
|
|
|
|
|
| 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 |
-
#
|
| 163 |
-
tokenizer
|
| 164 |
-
|
| 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(
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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()
|