from collections.abc import Mapping, Sequence
from os import PathLike
from pathlib import Path
from typing import TYPE_CHECKING, Literal
from ._lib import BpeTrainer_Character_CharIdx, BpeTrainer_u8_Idx, WordCounter
if TYPE_CHECKING:
from .model import BpeModel
Unit = Literal["byte", "unicode"]
FileFormat = Literal["unitoken", "gpt2"]
InitialAlphabet = Literal["raw", "byte_level"]
TieBreak = Literal["smallest_pair_id", "largest_content"]
def _validate_unit(unit: str) -> None:
if unit not in ("byte", "unicode"):
raise ValueError(f"Unknown unit: {unit}")
def _resolve_format(unit: Unit, format: FileFormat | None) -> FileFormat:
_validate_unit(unit)
resolved_format = format or ("unitoken" if unit == "unicode" else "gpt2")
if resolved_format not in ("gpt2", "unitoken"):
raise ValueError(f"Unknown format: {resolved_format}")
if unit == "unicode" and resolved_format == "gpt2":
raise ValueError('format="gpt2" is not compatible with unit="unicode"')
return resolved_format
class BpeTrainer:
def __init__(
self,
special_tokens: Sequence[str],
*,
unit: Unit = "byte",
initial_alphabet: InitialAlphabet | None = None,
tie_break: TieBreak | None = None,
parallel_merge_min_occurs_in: int | None = None,
) -> None:
_validate_unit(unit)
self._unit = unit
if unit == "unicode":
self._trainer = BpeTrainer_Character_CharIdx(
special_tokens=special_tokens,
initial_alphabet=initial_alphabet,
tie_break=tie_break,
parallel_merge_min_occurs_in=parallel_merge_min_occurs_in,
)
elif unit == "byte":
self._trainer = BpeTrainer_u8_Idx(
special_tokens=special_tokens,
initial_alphabet=initial_alphabet,
tie_break=tie_break,
parallel_merge_min_occurs_in=parallel_merge_min_occurs_in,
)
@property
def vocab_size(self) -> int:
return self._trainer.vocab_size()
@property
def unit(self) -> Unit:
return self._unit
@property
def vocab(self) -> dict[bytes, int]:
return dict(self._trainer.get_vocab().items())
def add_words(self, words: Mapping[str, int] | Sequence[tuple[str, int]]) -> None:
if isinstance(words, Mapping):
words = list(words.items())
self._trainer.add_words(words)
def add_word_counter(self, counter: WordCounter) -> None:
self._trainer.add_word_counter(counter)
def init_training(self) -> None:
self._trainer.init_training()
def train(self, vocab_size: int) -> None:
self._trainer.train_until(vocab_size)
def step(self) -> int:
return self._trainer.step()
def validate_model(self) -> "BpeModel":
from .model import BpeModel
return BpeModel(self._trainer.validate_model())
def save(self, name: str, *, outdir: str | PathLike = ".", format: FileFormat | None = None) -> None:
vocab_path = Path(outdir) / f"vocab.{name}[{self.unit}].json"
merges_path = Path(outdir) / f"merges.{name}[{self.unit}].txt"
self.save_files(vocab_path, merges_path, format=format)
def save_files(
self,
vocab_path: str | PathLike,
merges_path: str | PathLike,
*,
format: FileFormat | None = None,
) -> None:
resolved_format = _resolve_format(self.unit, format)
self.validate_model().save_files(vocab_path, merges_path, format=resolved_format)