axay28's picture
Add model source
c407ed8 verified
Raw
History Blame Contribute Delete
6.31 kB
from __future__ import annotations
from collections import Counter
from dataclasses import dataclass
from typing import Protocol
class Tokenizer(Protocol):
@property
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]: ...
@dataclass(frozen=True)
class CharTokenizer:
stoi: dict[str, int]
itos: dict[int, str]
@classmethod
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)
@property
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}
@classmethod
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)
@dataclass(frozen=True)
class BytePairTokenizer:
stoi: dict[str, int]
itos: dict[int, str]
merges: list[tuple[str, str]]
@classmethod
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)
@property
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,
}
@classmethod
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