| """ |
| tokenization_morpiece.py — HuggingFace wrapper around native MorPiece. |
| |
| Why this exists |
| --------------- |
| lm-eval loads the tokenizer through `AutoTokenizer.from_pretrained(..., trust_remote_code=True)`. |
| The stock HF *WordPiece* export cannot do byte fallback (only BPE/Unigram can, and |
| BPE merge-order does NOT reproduce MorPiece's greedy longest-match, so it would |
| silently perturb eng/nld). This slow `PreTrainedTokenizer` delegates every call |
| to the native MorPiece encoder, so: |
| * byte-level fallback works identically at train time and eval time; |
| * eng / nld / covered-zho segmentation is bit-identical to native MorPiece; |
| * distinct rare hanzi produce distinct UTF-8 byte-token sequences -> minimal |
| pairs (PinyinBench / HanziBench) stop tying -> the 0.0000 collapse is gone. |
| |
| Wiring (so AutoTokenizer picks it up) |
| ------------------------------------- |
| Place this file next to the model on the Hub and set, in tokenizer_config.json: |
| "tokenizer_class": "MorPieceHFTokenizer", |
| "auto_map": {"AutoTokenizer": ["tokenization_morpiece.MorPieceHFTokenizer", null]} |
| and drop the native trie next to it as `morpiece_native.json` |
| (that is exactly what MorPiece.save_pretrained(...) writes). |
| """ |
|
|
| import os |
| import json |
| from typing import List, Optional, Tuple |
|
|
| from transformers import PreTrainedTokenizer |
|
|
| try: |
| |
| |
| |
| |
| |
| |
| |
| from .tokenizer_MorPiece import MorPiece |
| except ImportError: |
| |
| |
| from tokenizer_MorPiece import MorPiece |
|
|
| NATIVE_FILE = "morpiece_native.json" |
|
|
|
|
| class MorPieceHFTokenizer(PreTrainedTokenizer): |
| vocab_files_names = {"native_file": NATIVE_FILE} |
| model_input_names = ["input_ids", "attention_mask"] |
|
|
| def __init__( |
| self, |
| native_file: Optional[str] = None, |
| unk_token="<unk>", |
| pad_token="<pad>", |
| bos_token="<s>", |
| eos_token="</s>", |
| mask_token="<mask>", |
| **kwargs, |
| ): |
| |
| |
| |
| |
| |
| |
| |
| |
| self._mp = MorPiece(ooa=False, use_tokenizers_lib=True, |
| byte_fallback=True) |
| if native_file and os.path.isfile(native_file): |
| self._load_native(native_file) |
| self._id_to_vocab = self._mp.id_to_vocab or {} |
| self._vocab_to_id = self._mp.vocab_to_id or {} |
| self._byte_set = set(getattr(self._mp, "BYTE_TOKENS", [])) |
| super().__init__(unk_token=unk_token, pad_token=pad_token, |
| bos_token=bos_token, eos_token=eos_token, |
| mask_token=mask_token, **kwargs) |
|
|
| |
| def _load_native(self, native_file: str): |
| |
| |
| with open(native_file, "r", encoding="utf-8") as f: |
| data = json.load(f) |
| self._mp.roots = data["roots"] |
| self._mp.vocab_to_id = data.get("vocab", {}) |
| self._mp.id_to_vocab = {v: k for k, v in self._mp.vocab_to_id.items()} |
| sp = data.get("special_token_ids", {}) |
| self._mp.unk_token_id = sp.get("unk", 0) |
| self._mp.pad_token_id = sp.get("pad", 1) |
| self._mp.bos_token_id = sp.get("bos", 2) |
| self._mp.eos_token_id = sp.get("eos", 3) |
| self._mp.mask_token_id = sp.get("mask", 4) |
| |
| |
| |
| self._mp.glue_morphemes = bool( |
| data.get("glue_morphemes", "++" in self._mp.vocab_to_id)) |
| self._mp.glue_cjk_prefer_root = bool(data.get("glue_cjk_prefer_root", True)) |
| |
| |
| |
| |
| |
| |
| |
| self._mp._pipeline = data.get("pipeline", "native") |
| if self._mp._pipeline == "legacy": |
| from tokenizers import normalizers, pre_tokenizers, Regex |
| self._mp.normalizer = normalizers.Sequence([ |
| normalizers.Lowercase(), normalizers.NFKC()]) |
| self._mp.pre_tokenizer = pre_tokenizers.Sequence([ |
| pre_tokenizers.Whitespace(), |
| pre_tokenizers.Split(Regex(".{1,24}"), behavior="isolated")]) |
| self._mp._split_re_n = -1 |
|
|
| |
| @property |
| def vocab_size(self) -> int: |
| return len(self._vocab_to_id) |
|
|
| def get_vocab(self) -> dict: |
| return dict(self._vocab_to_id, **self.added_tokens_encoder) |
|
|
| |
| def _tokenize(self, text: str) -> List[str]: |
| _, tokens = self._mp.encode(text) |
| |
| |
| if tokens and tokens[0] == self._mp.start_of_text_symbol: |
| tokens = tokens[1:] |
| return tokens |
|
|
| def _convert_token_to_id(self, token: str) -> int: |
| return self._vocab_to_id.get(token, self._mp.unk_token_id) |
|
|
| def _convert_id_to_token(self, index: int) -> str: |
| return self._id_to_vocab.get(index, self.unk_token) |
|
|
| def convert_tokens_to_string(self, tokens: List[str]) -> str: |
| """Reconstruct text with word spacing. |
| |
| MorPiece drops whitespace at encode time, so (unlike BPE) there is no |
| space-marker token to invert. Word boundaries live in the root/`++` |
| distinction. Two continuation encodings are supported: |
| * legacy: word-internal pieces are `++X` tokens (attach, strip `++`); |
| * glue_morphemes: a standalone `++` glue token precedes a root piece |
| that reuses its root embedding ("superhero" -> super, ++, hero). |
| A root token gets a leading space unless it is the first piece, follows |
| a `++` glue, or follows a byte run. Byte-fallback runs (`<0xHH>`) fuse |
| into the current word (also correct for CJK). |
| """ |
| out: List[str] = [] |
| glue = False |
| i, n = 0, len(tokens) |
| while i < n: |
| tok = tokens[i] |
| if tok == "++": |
| glue = True; i += 1; continue |
| if tok in self._byte_set: |
| buf = [] |
| while i < n and tokens[i] in self._byte_set: |
| buf.append(int(tokens[i][3:5], 16)); i += 1 |
| out.append(bytes(buf).decode("utf-8", errors="replace")) |
| glue = False; continue |
| if tok.startswith("++"): |
| out.append(tok[2:]); glue = False; i += 1; continue |
| if out and not glue: |
| out.append(" ") |
| out.append(tok); glue = False; i += 1 |
| return "".join(out).replace(self._mp.SPACE_MARK, " ").strip() |
|
|
| |
| def save_vocabulary(self, save_directory: str, |
| filename_prefix: Optional[str] = None) -> Tuple[str]: |
| os.makedirs(save_directory, exist_ok=True) |
| prefix = (filename_prefix + "-") if filename_prefix else "" |
| path = os.path.join(save_directory, prefix + NATIVE_FILE) |
| with open(path, "w", encoding="utf-8") as f: |
| json.dump({ |
| "roots": self._mp.roots, |
| "vocab": self._vocab_to_id, |
| "glue_morphemes": bool(getattr(self._mp, "glue_morphemes", False)), |
| "glue_cjk_prefer_root": bool(getattr(self._mp, "glue_cjk_prefer_root", True)), |
| "pipeline": getattr(self._mp, "_pipeline", "native"), |
| "special_token_ids": { |
| "unk": self._mp.unk_token_id, "pad": self._mp.pad_token_id, |
| "bos": self._mp.bos_token_id, "eos": self._mp.eos_token_id, |
| "mask": self._mp.mask_token_id, |
| }, |
| }, f, ensure_ascii=False) |
| return (path,) |
|
|