Files
frankenstallm/source/tokenizer/train_sp_tokenizer.py

176 lines
5.3 KiB
Python
Raw Permalink Normal View History

#!/usr/bin/env python3
"""
tokenizer/train_sp_tokenizer.py SentencePiece Unigram 한국어 토크나이저 학습.
한국어 1음절(UTF-8 3바이트) = 1토큰이 되도록 Unigram 모델을 사용.
character_coverage=0.9995 한글 11,172 음절 전체 커버.
Usage:
python tokenizer/train_sp_tokenizer.py \
--input "data/raw/namuwiki_ko/*.txt,data/raw/ko_wiki_0000.txt" \
--vocab_size 64000 \
--output_dir tokenizer/korean_sp
Output:
tokenizer/korean_sp/tokenizer.model (SentencePiece 모델)
tokenizer/korean_sp/tokenizer.vocab (어휘 목록)
"""
from __future__ import annotations
import argparse
import glob
import os
import sys
import tempfile
from pathlib import Path
def expand_inputs(input_spec: str) -> list[str]:
"""콤마로 구분된 글로브 패턴들을 실제 파일 경로 목록으로 확장."""
files: list[str] = []
for pattern in input_spec.split(","):
pattern = pattern.strip()
if any(c in pattern for c in ("*", "?", "[")):
matched = sorted(glob.glob(pattern, recursive=True))
if not matched:
print(f"WARNING: 패턴에 일치하는 파일 없음: {pattern!r}", file=sys.stderr)
files.extend(matched)
else:
if Path(pattern).exists():
files.append(pattern)
else:
print(f"WARNING: 파일 없음: {pattern!r}", file=sys.stderr)
return files
def train(
input_files: list[str],
output_dir: Path,
vocab_size: int,
num_threads: int,
input_sentence_size: int,
) -> None:
try:
import sentencepiece as spm
except ImportError:
print(
"ERROR: sentencepiece가 설치되지 않음.\n"
" pip install --break-system-packages sentencepiece",
file=sys.stderr,
)
sys.exit(1)
output_dir.mkdir(parents=True, exist_ok=True)
model_prefix = str(output_dir / "tokenizer")
print(f"입력 파일 수: {len(input_files)}")
for f in input_files[:5]:
print(f" {f}")
if len(input_files) > 5:
print(f" ... 외 {len(input_files) - 5}")
print(f"어휘 크기: {vocab_size:,}")
print(f"출력 경로: {model_prefix}.model / .vocab")
print()
# SentencePiece는 파일 목록을 콤마로 구분된 단일 문자열로 받는다
input_str = ",".join(input_files)
spm.SentencePieceTrainer.train(
input=input_str,
model_prefix=model_prefix,
vocab_size=vocab_size,
model_type="unigram", # BPE보다 한국어에 자연스러움
character_coverage=0.9995, # 한글 11,172 음절 완전 커버
normalization_rule_name="nfkc", # Unicode NFKC 정규화 (한국어 호환문자 통일)
pad_id=0,
bos_id=1,
eos_id=2,
unk_id=3,
pad_piece="<pad>",
bos_piece="<s>",
eos_piece="</s>",
unk_piece="<unk>",
user_defined_symbols=[],
num_threads=num_threads,
input_sentence_size=input_sentence_size,
shuffle_input_sentence=True,
# 학습 안정성
seed_sentencepiece_size=1_000_000,
shrinking_factor=0.75,
max_sentence_length=4096,
)
model_path = Path(f"{model_prefix}.model")
vocab_path = Path(f"{model_prefix}.vocab")
if model_path.exists():
size_mb = model_path.stat().st_size / 1e6
print(f"학습 완료!")
print(f" 모델: {model_path} ({size_mb:.1f} MB)")
print(f" 어휘: {vocab_path}")
print()
print("다음 단계:")
print(f" python tokenizer/convert_sp_to_hf.py \\")
print(f" --model {model_path} \\")
print(f" --output {output_dir}/tokenizer.json")
else:
print("ERROR: 학습 실패 — 출력 파일이 생성되지 않음", file=sys.stderr)
sys.exit(1)
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="SentencePiece Unigram 한국어 토크나이저 학습",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
parser.add_argument(
"--input",
required=True,
help="콤마로 구분된 파일/글로브 패턴 (예: 'data/raw/ko/*.txt,data/raw/wiki.txt')",
)
parser.add_argument(
"--vocab_size",
type=int,
default=64000,
help="어휘 크기",
)
parser.add_argument(
"--output_dir",
type=Path,
default=Path("tokenizer/korean_sp"),
help="모델 저장 디렉토리",
)
parser.add_argument(
"--num_threads",
type=int,
default=64,
help="학습에 사용할 CPU 스레드 수",
)
parser.add_argument(
"--input_sentence_size",
type=int,
default=10_000_000,
help="학습에 사용할 최대 문장 수 (0 = 무제한)",
)
return parser.parse_args()
def main() -> None:
args = parse_args()
input_files = expand_inputs(args.input)
if not input_files:
print("ERROR: 입력 파일이 없습니다.", file=sys.stderr)
sys.exit(1)
train(
input_files=input_files,
output_dir=args.output_dir,
vocab_size=args.vocab_size,
num_threads=args.num_threads,
input_sentence_size=args.input_sentence_size,
)
if __name__ == "__main__":
main()