| from __future__ import annotations |
|
|
| import json |
| from pathlib import Path |
| from typing import Iterable |
|
|
|
|
| class CharacterTokenizer: |
| PAD = "[PAD]" |
| UNK = "[UNK]" |
| BOS = "[BOS]" |
| EOS = "[EOS]" |
|
|
| def __init__(self, vocab: dict[str, int], num_masks: int): |
| self.vocab = dict(vocab) |
| self.id_to_token = {index: token for token, index in self.vocab.items()} |
| self.num_masks = num_masks |
| self.pad_id = self.vocab[self.PAD] |
| self.unk_id = self.vocab[self.UNK] |
| self.bos_id = self.vocab[self.BOS] |
| self.eos_id = self.vocab[self.EOS] |
| self.mask_token_start = 4 |
| self.clean_token_start = 4 + num_masks |
|
|
| @classmethod |
| def build(cls, texts: Iterable[str], num_masks: int = 8) -> "CharacterTokenizer": |
| specials = [cls.PAD, cls.UNK, cls.BOS, cls.EOS] |
| masks = [f"[M{i}]" for i in range(num_masks)] |
| characters = sorted(set("".join(texts))) |
| vocab = { |
| token: index |
| for index, token in enumerate(specials + masks + characters) |
| } |
| return cls(vocab, num_masks) |
|
|
| @property |
| def vocab_size(self) -> int: |
| return len(self.vocab) |
|
|
| @property |
| def clean_vocab_size(self) -> int: |
| return self.vocab_size - self.clean_token_start |
|
|
| def encode(self, text: str, seq_len: int | None = None) -> list[int]: |
| ids = [self.vocab.get(character, self.unk_id) for character in text] |
| if seq_len is not None: |
| ids = ids[:seq_len] |
| ids += [self.pad_id] * (seq_len - len(ids)) |
| return ids |
|
|
| def decode(self, ids: Iterable[int], skip_pad: bool = True) -> str: |
| pieces: list[str] = [] |
| for index in ids: |
| token = self.id_to_token.get(int(index), self.UNK) |
| if skip_pad and token == self.PAD: |
| continue |
| pieces.append(token) |
| return "".join(pieces) |
|
|
| def save(self, path: str | Path) -> None: |
| payload = {"vocab": self.vocab, "num_masks": self.num_masks} |
| path = Path(path) |
| path.parent.mkdir(parents=True, exist_ok=True) |
| with path.open("w", encoding="utf-8") as handle: |
| json.dump(payload, handle, indent=2, ensure_ascii=False) |
|
|
| @classmethod |
| def load(cls, path: str | Path) -> "CharacterTokenizer": |
| with Path(path).open("r", encoding="utf-8") as handle: |
| payload = json.load(handle) |
| return cls(payload["vocab"], payload["num_masks"]) |
|
|