File size: 6,006 Bytes
a3f4798 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 | #!/usr/bin/env python3
"""HuggingFace slow-tokenizer wrapper for our morphological tokenizer, for the BabyLM leaderboard submission.
The official strict harness (`sentence_zero_shot/dataset.py`) calls `tokenizer(text, return_offsets_mapping=True)`
and locates the scored completion by CHARACTER offsets. A pure-Python tokenizer normally can't return offsets;
this subclass overrides the encode path to emit word-granularity `offset_mapping` (via OurTokenizer.encode_with_spans),
which is sufficient because completions always begin at a whitespace boundary.
Ship this file + our_vocab.json (+ the analyzer resources) with the model and load via `trust_remote_code=True`.
Special tokens are NOT auto-added (add_special_tokens is a no-op) to match the harness's no-BOS default for
from-scratch tokenizers.
"""
from __future__ import annotations
import json
from pathlib import Path
from transformers import PreTrainedTokenizer
from transformers.tokenization_utils_base import BatchEncoding
try:
# NOTE: HF's local-dir dynamic-module loader copies only THIS file's one level of relative
# imports (no recursion), so the analyzer must be imported here even though only
# our_tokenizer uses it.
from .morph_tokenizer import load_resources as _hf_copy_probe # noqa: F401
from .our_tokenizer import BOS, EOS, OurTokenizer, PAD, SPECIALS, UNK # noqa: F401
except ImportError: # direct use (importlib evades HF check_imports regex)
import importlib as _il
_m = _il.import_module("our_tokenizer")
BOS, EOS, OurTokenizer, PAD, SPECIALS, UNK = _m.BOS, _m.EOS, _m.OurTokenizer, _m.PAD, _m.SPECIALS, _m.UNK
class OurMorphTokenizer(PreTrainedTokenizer):
"""Slow HF tokenizer backed by OurTokenizer. Emits offset_mapping for the strict harness."""
vocab_files_names = {"vocab_file": "our_vocab.json"}
model_input_names = ["input_ids", "attention_mask"]
def __init__(self, vocab_file=None, char_backoff=True, **kwargs):
self._vocab = json.loads(Path(vocab_file).read_text(encoding="utf-8")) if vocab_file else {s: i for i, s in enumerate(SPECIALS)}
self._ids_to_tokens = {i: t for t, i in self._vocab.items()}
self._ot = OurTokenizer.load(vocab_file, res=None) if vocab_file else None
if self._ot is not None:
self._ot.char_backoff = char_backoff
super().__init__(pad_token=PAD, unk_token=UNK, bos_token=BOS, eos_token=EOS,
char_backoff=char_backoff, **kwargs)
@property
def vocab_size(self) -> int:
return len(self._vocab)
def get_vocab(self) -> dict:
return dict(self._vocab)
def _tokenize(self, text, **kwargs):
return self._ot.tokenize(text)
def _convert_token_to_id(self, token):
return self._vocab.get(token, self._vocab[UNK])
def _convert_id_to_token(self, index):
return self._ids_to_tokens.get(index, UNK)
def convert_tokens_to_string(self, tokens):
return self._ot.decode([self._convert_token_to_id(t) for t in tokens])
def build_inputs_with_special_tokens(self, token_ids_0, token_ids_1=None):
# No auto special tokens (match the harness's no-BOS default); pairs are concatenated.
return token_ids_0 if token_ids_1 is None else token_ids_0 + token_ids_1
def save_vocabulary(self, save_directory, filename_prefix=None):
out = Path(save_directory) / ((filename_prefix + "-" if filename_prefix else "") + "our_vocab.json")
out.write_text(json.dumps(self._vocab, ensure_ascii=False), encoding="utf-8")
return (str(out),)
# -- offset-aware encoding (the reason this wrapper exists) -------------------------------------
def _one(self, text, return_offsets_mapping):
ids, spans = self._ot.encode_with_spans(text)
d = {"input_ids": ids, "attention_mask": [1] * len(ids)}
if return_offsets_mapping:
d["offset_mapping"] = spans
return d
def _encode_plus(self, text, text_pair=None, add_special_tokens=True, return_offsets_mapping=False,
return_attention_mask=None, return_tensors=None, **kwargs):
d = self._one(text, return_offsets_mapping)
if text_pair is not None:
d2 = self._one(text_pair, return_offsets_mapping)
d["input_ids"] += d2["input_ids"]
d["attention_mask"] += d2["attention_mask"]
if return_offsets_mapping:
d["offset_mapping"] += d2["offset_mapping"]
return BatchEncoding(d, tensor_type=return_tensors, prepend_batch_axis=True)
def _batch_encode_plus(self, batch_text_or_pairs, add_special_tokens=True, return_offsets_mapping=False,
return_attention_mask=None, return_tensors=None, padding_strategy=None, **kwargs):
rows = []
for t in batch_text_or_pairs:
text, pair = t if isinstance(t, tuple) else (t, None)
r = self._one(text, return_offsets_mapping)
if pair is not None:
r2 = self._one(pair, return_offsets_mapping)
r["input_ids"] += r2["input_ids"]; r["attention_mask"] += r2["attention_mask"]
if return_offsets_mapping:
r["offset_mapping"] += r2["offset_mapping"]
rows.append(r)
# left-as-lists (harness scores one at a time); pad only if a tensor type is requested
out = {k: [r[k] for r in rows] for k in rows[0]}
if return_tensors is not None:
mx = max(len(r["input_ids"]) for r in rows)
pad_id = self._vocab[PAD]
out["input_ids"] = [r["input_ids"] + [pad_id] * (mx - len(r["input_ids"])) for r in rows]
out["attention_mask"] = [r["attention_mask"] + [0] * (mx - len(r["attention_mask"])) for r in rows]
if return_offsets_mapping:
out["offset_mapping"] = [r["offset_mapping"] + [(0, 0)] * (mx - len(r["offset_mapping"])) for r in rows]
return BatchEncoding(out, tensor_type=return_tensors)
|