| """Train and package the KorByte-128K tokenizer.""" |
|
|
| from __future__ import annotations |
|
|
| import hashlib |
| import importlib.metadata |
| import json |
| import platform |
| from datetime import UTC, datetime |
| from pathlib import Path |
| from typing import Any |
|
|
| from tokenizers import Regex, Tokenizer, decoders, pre_tokenizers, processors |
| from tokenizers.models import BPE |
| from tokenizers.trainers import BpeTrainer |
|
|
| from .config import ( |
| CORE_VOCAB_SIZE, |
| DEFAULT_MODEL_MAX_LENGTH, |
| TOTAL_VOCAB_SIZE, |
| Paths, |
| ) |
| from .special_tokens import added_special_tokens, special_token_strings |
|
|
| |
| |
| |
| PRETOKENIZER_PATTERN = ( |
| r" ?[\p{L}\p{M}]+(?i:'s|'t|'re|'ve|'m|'ll|'d)?|" |
| r" ?\p{N}{1,6}| ?[^\s\p{L}\p{N}]+[\r\n/]*|\s*[\r\n]+|" |
| r"\s+(?!\S)|\s+" |
| ) |
|
|
|
|
| def _sha256(path: Path) -> str: |
| digest = hashlib.sha256() |
| with path.open("rb") as handle: |
| for chunk in iter(lambda: handle.read(1024 * 1024), b""): |
| digest.update(chunk) |
| return digest.hexdigest() |
|
|
|
|
| def build_tokenizer() -> Tokenizer: |
| """Construct an untrained, exactly reversible byte-level BPE pipeline.""" |
|
|
| tokenizer = Tokenizer(BPE(unk_token=None, byte_fallback=False, fuse_unk=False)) |
| tokenizer.normalizer = None |
| tokenizer.pre_tokenizer = pre_tokenizers.Sequence( |
| [ |
| pre_tokenizers.Split(Regex(PRETOKENIZER_PATTERN), behavior="isolated", invert=False), |
| pre_tokenizers.ByteLevel( |
| add_prefix_space=False, |
| use_regex=False, |
| trim_offsets=True, |
| ), |
| ] |
| ) |
| tokenizer.post_processor = processors.ByteLevel(trim_offsets=True) |
| tokenizer.decoder = decoders.ByteLevel() |
| return tokenizer |
|
|
|
|
| def _compatibility_files(tokenizer: Tokenizer, root: Path) -> None: |
| tokens = special_token_strings() |
| vocab = tokenizer.get_vocab(with_added_tokens=True) |
| additional = list(tokens[4:]) |
| tokenizer_config: dict[str, Any] = { |
| "add_bos_token": False, |
| "add_eos_token": False, |
| "additional_special_tokens": additional, |
| "bos_token": tokens[0], |
| "clean_up_tokenization_spaces": False, |
| "eos_token": tokens[1], |
| "model_max_length": DEFAULT_MODEL_MAX_LENGTH, |
| "pad_token": tokens[2], |
| "tokenizer_class": "PreTrainedTokenizerFast", |
| "unk_token": tokens[3], |
| } |
| special_tokens_map = { |
| "bos_token": tokens[0], |
| "eos_token": tokens[1], |
| "pad_token": tokens[2], |
| "unk_token": tokens[3], |
| "additional_special_tokens": additional, |
| } |
| added_tokens_map = {token: vocab[token] for token in tokens} |
|
|
| (root / "tokenizer_config.json").write_text( |
| json.dumps(tokenizer_config, ensure_ascii=False, indent=2) + "\n", encoding="utf-8" |
| ) |
| (root / "special_tokens_map.json").write_text( |
| json.dumps(special_tokens_map, ensure_ascii=False, indent=2) + "\n", encoding="utf-8" |
| ) |
| (root / "added_tokens.json").write_text( |
| json.dumps(added_tokens_map, ensure_ascii=False, indent=2) + "\n", encoding="utf-8" |
| ) |
|
|
|
|
| def train_tokenizer(root: Path, *, min_frequency: int = 3) -> dict[str, Any]: |
| """Train from the prepared corpus and write a complete Hub-compatible snapshot.""" |
|
|
| paths = Paths(root) |
| if not paths.corpus.is_file(): |
| raise FileNotFoundError(f"Prepared corpus not found: {paths.corpus}") |
| if not paths.corpus_manifest.is_file(): |
| raise FileNotFoundError(f"Corpus manifest not found: {paths.corpus_manifest}") |
|
|
| tokenizer = build_tokenizer() |
| trainer = BpeTrainer( |
| vocab_size=CORE_VOCAB_SIZE, |
| min_frequency=min_frequency, |
| show_progress=True, |
| special_tokens=[], |
| initial_alphabet=pre_tokenizers.ByteLevel.alphabet(), |
| max_token_length=64, |
| ) |
| tokenizer.train([str(paths.corpus)], trainer) |
| core_size = tokenizer.get_vocab_size(with_added_tokens=False) |
| if core_size != CORE_VOCAB_SIZE: |
| raise RuntimeError(f"Expected {CORE_VOCAB_SIZE} core tokens, trained {core_size}") |
|
|
| added_count = tokenizer.add_special_tokens(added_special_tokens()) |
| if added_count != TOTAL_VOCAB_SIZE - CORE_VOCAB_SIZE: |
| raise RuntimeError(f"Expected 256 added tokens, got {added_count}") |
| total_size = tokenizer.get_vocab_size(with_added_tokens=True) |
| if total_size != TOTAL_VOCAB_SIZE: |
| raise RuntimeError(f"Expected {TOTAL_VOCAB_SIZE} total tokens, got {total_size}") |
|
|
| tokenizer.save(str(root / "tokenizer.json"), pretty=True) |
| tokenizer.model.save(str(root)) |
| _compatibility_files(tokenizer, root) |
|
|
| corpus_manifest = json.loads(paths.corpus_manifest.read_text(encoding="utf-8")) |
| build_manifest = { |
| "schema_version": 1, |
| "created_at": datetime.now(UTC).isoformat(), |
| "algorithm": "byte-level-bpe", |
| "normalizer": None, |
| "pretokenizer_pattern": PRETOKENIZER_PATTERN, |
| "core_vocab_size": core_size, |
| "special_token_count": added_count, |
| "total_vocab_size": total_size, |
| "min_frequency": min_frequency, |
| "max_token_length": 64, |
| "corpus_sha256": corpus_manifest["sha256"], |
| "tokenizer_sha256": _sha256(root / "tokenizer.json"), |
| "python": platform.python_version(), |
| "platform": platform.platform(), |
| "packages": { |
| package: importlib.metadata.version(package) |
| for package in ("datasets", "huggingface-hub", "tokenizers", "transformers") |
| }, |
| } |
| paths.build_manifest.write_text( |
| json.dumps(build_manifest, ensure_ascii=False, indent=2) + "\n", encoding="utf-8" |
| ) |
| return build_manifest |
|
|