KorByte-128K / src /korbyte /train.py
DongHyeok-Seo
Release KorByte-128K v2 tokenizer
5a98e33
Raw
History Blame Contribute Delete
5.81 kB
"""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
# Keep a leading ASCII space with the following unit, but do not bind arbitrary
# punctuation to a word. Six-digit number chunks avoid needless fragmentation
# of dates, standards, and common Korean financial notation.
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