thai_indextts2 / tools /tokenizer /train_bpe.py
williampike's picture
Upload folder using huggingface_hub
4d3248c verified
Raw
History Blame Contribute Delete
4.91 kB
#!/usr/bin/env python3
"""
Utility script to train a SentencePiece BPE tokenizer on the Japanese corpus.
Example:
python tools/tokenizer/train_bpe.py \\
--manifest JA_yodas_dataset/ja_yodas_train.jsonl \\
--output-prefix checkpoints/japanese_bpe \\
--vocab-size 12000
"""
import argparse
import json
import os
import sys
import tempfile
from pathlib import Path
import sentencepiece as spm
from indextts.utils.front import TextNormalizer
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Train a Japanese BPE tokenizer with SentencePiece.")
parser.add_argument(
"--manifest",
nargs="+",
required=True,
help="One or more JSONL manifests containing a 'text' field.",
)
parser.add_argument(
"--output-prefix",
type=Path,
default=Path("checkpoints/japanese_bpe"),
help="Output prefix for the tokenizer files (.model/.vocab).",
)
parser.add_argument(
"--vocab-size",
type=int,
default=12000,
help="Desired vocabulary size.",
)
parser.add_argument(
"--character-coverage",
type=float,
default=0.9995,
help="Character coverage for SentencePiece (keep near 1.0 for Japanese).",
)
parser.add_argument(
"--model-type",
choices=["bpe", "unigram"],
default="bpe",
help="SentencePiece model type.",
)
parser.add_argument(
"--input-sentence-size",
type=int,
default=0,
help="Limit the number of sentences sampled for training (0 means use all).",
)
parser.add_argument(
"--byte-fallback",
action="store_true",
default=False,
help="Enable byte fallback to avoid <unk> for unseen characters (Do not enable unless you know what you're doing)",
)
return parser.parse_args()
def iter_texts(manifests: list[Path]) -> tuple[int, int, Path]:
normalizer = TextNormalizer(preferred_language="ja")
normalizer.load()
num_samples = 0
num_empty = 0
tmp_file = tempfile.NamedTemporaryFile(delete=False, suffix=".txt", mode="w", encoding="utf-8")
try:
with tmp_file as fp:
for manifest in manifests:
with open(manifest, "r", encoding="utf-8") as handle:
for line in handle:
if not line.strip():
continue
payload = json.loads(line)
text = payload.get("text", "")
text = normalizer.normalize(text, language="ja")
if not text:
num_empty += 1
continue
fp.write(text + "\n")
num_samples += 1
except Exception:
os.unlink(tmp_file.name)
raise
return num_samples, num_empty, Path(tmp_file.name)
def train_tokenizer(args: argparse.Namespace) -> None:
manifests = [Path(m).expanduser().resolve() for m in args.manifest]
missing = [str(p) for p in manifests if not p.exists()]
if missing:
raise FileNotFoundError(f"Missing manifest(s): {', '.join(missing)}")
output_prefix = args.output_prefix.expanduser().resolve()
output_prefix.parent.mkdir(parents=True, exist_ok=True)
num_samples, num_empty, corpus_path = iter_texts(manifests)
if num_samples == 0:
raise RuntimeError("No non-empty samples found. Cannot train tokenizer.")
spm_kwargs = {
"input": str(corpus_path),
"model_prefix": str(output_prefix),
"vocab_size": args.vocab_size,
"character_coverage": args.character_coverage,
"model_type": args.model_type,
"bos_id": 0,
"eos_id": 1,
"unk_id": 2,
"pad_id": -1,
"input_sentence_size": args.input_sentence_size,
"shuffle_input_sentence": True,
"byte_fallback": args.byte_fallback,
"train_extremely_large_corpus": True,
}
print(f"[Tokenizer] Training on {num_samples} samples (skipped {num_empty}).")
try:
spm.SentencePieceTrainer.train(**spm_kwargs)
finally:
corpus_path.unlink(missing_ok=True)
model_path = output_prefix.with_suffix(".model")
vocab_path = output_prefix.with_suffix(".vocab")
print(f"[Tokenizer] Saved SentencePiece model to: {model_path}")
print(f"[Tokenizer] Saved vocabulary to: {vocab_path}")
def main() -> int:
args = parse_args()
try:
train_tokenizer(args)
except KeyboardInterrupt:
print("Interrupted!", file=sys.stderr)
return 130
return 0
if __name__ == "__main__":
raise SystemExit(main())