Spaces:
Sleeping
Sleeping
| from __future__ import annotations | |
| from collections import Counter | |
| from dataclasses import dataclass | |
| from typing import Protocol | |
| class Tokenizer(Protocol): | |
| def vocab_size(self) -> int: ... | |
| def encode(self, text: str) -> list[int]: ... | |
| def decode(self, ids: list[int]) -> str: ... | |
| def id_to_token(self, idx: int) -> str: ... | |
| def to_dict(self) -> dict[str, object]: ... | |
| class CharTokenizer: | |
| stoi: dict[str, int] | |
| itos: dict[int, str] | |
| def train(cls, text: str) -> "CharTokenizer": | |
| if not text: | |
| raise ValueError("Cannot train a tokenizer on empty text") | |
| chars = sorted(set(text)) | |
| stoi = {char: idx for idx, char in enumerate(chars)} | |
| itos = {idx: char for char, idx in stoi.items()} | |
| return cls(stoi=stoi, itos=itos) | |
| def vocab_size(self) -> int: | |
| return len(self.stoi) | |
| def encode(self, text: str) -> list[int]: | |
| try: | |
| return [self.stoi[char] for char in text] | |
| except KeyError as exc: | |
| char = exc.args[0] | |
| raise ValueError(f"Character {char!r} is not in the tokenizer vocabulary") from exc | |
| def decode(self, ids: list[int]) -> str: | |
| try: | |
| return "".join(self.itos[idx] for idx in ids) | |
| except KeyError as exc: | |
| idx = exc.args[0] | |
| raise ValueError(f"Token id {idx!r} is not in the tokenizer vocabulary") from exc | |
| def id_to_token(self, idx: int) -> str: | |
| try: | |
| return self.itos[idx] | |
| except KeyError as exc: | |
| raise ValueError(f"Token id {idx!r} is not in the tokenizer vocabulary") from exc | |
| def to_dict(self) -> dict[str, object]: | |
| return {"type": "char", "stoi": self.stoi, "itos": self.itos} | |
| def from_dict(cls, payload: dict[str, dict[str, int] | dict[int | str, str]]) -> "CharTokenizer": | |
| stoi = {str(char): int(idx) for char, idx in payload["stoi"].items()} | |
| itos = {int(idx): str(char) for idx, char in payload["itos"].items()} | |
| return cls(stoi=stoi, itos=itos) | |
| class BytePairTokenizer: | |
| stoi: dict[str, int] | |
| itos: dict[int, str] | |
| merges: list[tuple[str, str]] | |
| def train(cls, text: str, vocab_size: int = 256) -> "BytePairTokenizer": | |
| if not text: | |
| raise ValueError("Cannot train a tokenizer on empty text") | |
| if vocab_size <= 0: | |
| raise ValueError("vocab_size must be positive") | |
| base_vocab = sorted(set(text)) | |
| sequences = [[char for char in text]] | |
| merges: list[tuple[str, str]] = [] | |
| vocab = set(base_vocab) | |
| while len(vocab) < vocab_size: | |
| pair_counts = _count_pairs(sequences) | |
| if not pair_counts: | |
| break | |
| pair, count = pair_counts.most_common(1)[0] | |
| merged = "".join(pair) | |
| if count < 2 or merged in vocab: | |
| break | |
| sequences = [_merge_pair(sequence, pair, merged) for sequence in sequences] | |
| merges.append(pair) | |
| vocab.add(merged) | |
| tokens = sorted(vocab, key=lambda token: (len(token), token)) | |
| stoi = {token: idx for idx, token in enumerate(tokens)} | |
| itos = {idx: token for token, idx in stoi.items()} | |
| return cls(stoi=stoi, itos=itos, merges=merges) | |
| def vocab_size(self) -> int: | |
| return len(self.stoi) | |
| def encode(self, text: str) -> list[int]: | |
| unknown = sorted(set(text) - {token for token in self.stoi if len(token) == 1}) | |
| if unknown: | |
| raise ValueError(f"Characters {unknown!r} are not in the tokenizer vocabulary") | |
| pieces = list(text) | |
| for left, right in self.merges: | |
| pieces = _merge_pair(pieces, (left, right), left + right) | |
| return [self.stoi[piece] for piece in pieces] | |
| def decode(self, ids: list[int]) -> str: | |
| try: | |
| return "".join(self.itos[idx] for idx in ids) | |
| except KeyError as exc: | |
| idx = exc.args[0] | |
| raise ValueError(f"Token id {idx!r} is not in the tokenizer vocabulary") from exc | |
| def id_to_token(self, idx: int) -> str: | |
| try: | |
| return self.itos[idx] | |
| except KeyError as exc: | |
| raise ValueError(f"Token id {idx!r} is not in the tokenizer vocabulary") from exc | |
| def to_dict(self) -> dict[str, object]: | |
| return { | |
| "type": "bpe", | |
| "stoi": self.stoi, | |
| "itos": self.itos, | |
| "merges": self.merges, | |
| } | |
| def from_dict(cls, payload: dict[str, object]) -> "BytePairTokenizer": | |
| raw_stoi = payload["stoi"] | |
| raw_itos = payload["itos"] | |
| raw_merges = payload["merges"] | |
| if not isinstance(raw_stoi, dict) or not isinstance(raw_itos, dict): | |
| raise ValueError("Invalid BPE tokenizer payload") | |
| if not isinstance(raw_merges, list): | |
| raise ValueError("Invalid BPE merge payload") | |
| stoi = {str(token): int(idx) for token, idx in raw_stoi.items()} | |
| itos = {int(idx): str(token) for idx, token in raw_itos.items()} | |
| merges = [(str(left), str(right)) for left, right in raw_merges] | |
| return cls(stoi=stoi, itos=itos, merges=merges) | |
| def tokenizer_from_dict(payload: dict[str, object]) -> Tokenizer: | |
| tokenizer_type = payload.get("type", "char") | |
| if tokenizer_type == "char": | |
| return CharTokenizer.from_dict(payload) # type: ignore[arg-type] | |
| if tokenizer_type == "bpe": | |
| return BytePairTokenizer.from_dict(payload) | |
| raise ValueError(f"Unknown tokenizer type: {tokenizer_type!r}") | |
| def _count_pairs(sequences: list[list[str]]) -> Counter[tuple[str, str]]: | |
| counts: Counter[tuple[str, str]] = Counter() | |
| for sequence in sequences: | |
| counts.update(zip(sequence, sequence[1:])) | |
| return counts | |
| def _merge_pair(sequence: list[str], pair: tuple[str, str], merged: str) -> list[str]: | |
| out: list[str] = [] | |
| idx = 0 | |
| while idx < len(sequence): | |
| if idx < len(sequence) - 1 and (sequence[idx], sequence[idx + 1]) == pair: | |
| out.append(merged) | |
| idx += 2 | |
| else: | |
| out.append(sequence[idx]) | |
| idx += 1 | |
| return out | |