| """ |
| ่ฎญ็ป Morfessor+BPE Tokenizer |
| |
| ๆ่ทฏ๏ผ |
| 1. ๅจ่ฎญ็ป่ฏญๆไธ่ฎญ็ป Morfessor๏ผๅญฆไน ่ฑ่ฏญๅฝขๆ็ด ่พน็ |
| 2. ็จ Morfessor ๅฏน่ฎญ็ปๆๆฌ้ขๅๅ๏ผๅจๅฝขๆ็ด ่พน็ๆๅ
ฅ็ฉบๆ ผ๏ผ |
| 3. ๅจ้ขๅๅๆๆฌไธ่ฎญ็ป BPE๏ผๆถๆไธๆ ๅ BPE tokenizer ๅฎๅ
จไธ่ด๏ผ |
| |
| ๆๆ๏ผBPE ไธไผ่ทจๅฝขๆ็ด ่พน็ๅๅนถ๏ผไฟ็ WUG/Entity Tracking ๆๅฉ็ๅฝขๆไฟกๆฏ |
| |
| ่พๅ
ฅ: data/8_sample_B/train.txt |
| ่พๅบ: models/tokenizer_morfessor/ |
| โโโ tokenizer.json (BPE tokenizer, HuggingFace ๆ ผๅผ) |
| โโโ tokenizer_config.json |
| โโโ special_tokens_map.json |
| โโโ morfessor.bin (Morfessor ๆจกๅ๏ผๆจ็ๆถ้่ฆ) |
| |
| ็จๆณ: |
| pip install morfessor |
| python scripts/02_model/train_tokenizer_morfessor.py |
| python scripts/02_model/train_tokenizer_morfessor.py --input data/8_sample_C/train.txt |
| """ |
|
|
| import argparse |
| import re |
| import sys |
| from collections import Counter |
| from pathlib import Path |
|
|
| try: |
| import morfessor |
| except ImportError: |
| print("่ฏทๅ
ๅฎ่ฃ
: pip install morfessor") |
| sys.exit(1) |
|
|
| from tokenizers import Tokenizer, Regex |
| from tokenizers.models import BPE |
| from tokenizers.trainers import BpeTrainer |
| from tokenizers.normalizers import Sequence, Prepend, NFKC, Replace |
| from tokenizers.pre_tokenizers import Sequence as PreSeq, Split, ByteLevel |
| from tokenizers.processors import TemplateProcessing |
| from transformers import PreTrainedTokenizerFast |
|
|
| ROOT = Path(__file__).resolve().parent.parent.parent |
| DEFAULT_INPUT = ROOT / "data/8_sample_B/train.txt" |
| DEFAULT_OUT = ROOT / "models/tokenizer_morfessor" |
| SPECIAL_TOKENS = ["<unk>", "<s>", "</s>", "<pad>", "<mask>"] |
|
|
| |
| CORPUSWEIGHT = 0.01 |
| MIN_MORPH_LEN = 2 |
| MIN_WORD_LEN = 3 |
|
|
|
|
| |
| |
| |
|
|
| def train_morfessor_model(input_path: Path, corpusweight: float = CORPUSWEIGHT) -> morfessor.BaselineModel: |
| """ๅจ่ฎญ็ป่ฏญๆไธ่ฎญ็ป Morfessor ๆจกๅใ""" |
| print(f"Step 1: ่ฎญ็ป Morfessor ๆจกๅ (corpusweight={corpusweight})...") |
|
|
| |
| word_counts = Counter() |
| with open(input_path) as f: |
| for line in f: |
| for word in line.split(): |
| clean = word.strip(".,!?;:\"'()-[]{}โฆ""''").lower() |
| if len(clean) >= 2 and clean.isalpha(): |
| word_counts[clean] += 1 |
|
|
| print(f" ๅฏไธ่ฏๆฐ: {len(word_counts):,}") |
| print(f" ๆป่ฏ้ข: {sum(word_counts.values()):,}") |
|
|
| model = morfessor.BaselineModel(corpusweight=corpusweight) |
| training_data = [(count, word) for word, count in word_counts.items()] |
| model.load_data(training_data) |
| model.train_batch() |
|
|
| |
| samples = [ |
| "unhappiness", "running", "walked", "beautiful", "government", |
| "internationally", "darkness", "singer", "swimming", "nationalization", |
| "happiness", "slowly", "governmental", "children", "quickly", |
| ] |
| print("\n ๅๅ็คบไพ:") |
| for w in samples: |
| segs = model.viterbi_segment(w)[0] |
| print(f" {w:25s} โ {' + '.join(segs)}") |
|
|
| return model |
|
|
|
|
| |
| |
| |
|
|
| _WORD_RE = re.compile(r'^([^a-zA-Z]*?)([a-zA-Z]+)([^a-zA-Z]*)$') |
|
|
|
|
| def presegment_word(word: str, morf_model) -> str: |
| """ๅฏนๅ่ฏ็จ Morfessor ๆพๅฝขๆ็ด ่พน็๏ผๅจ่พน็ๅคๆๅ
ฅ็ฉบๆ ผใ |
| |
| ไฟ็ๅๅงๅคงๅฐๅ๏ผ็จ Morfessor ๅๅๅฐๅ็ๆฌ๏ผ |
| ๅๆ segment ้ฟๅบฆๆ ๅฐๅๅๅงๅญ็ฌฆใ |
| |
| ่ฟๆปค่งๅ๏ผๆๆๅฝขๆ็ด ้ฟๅบฆๅฟ
้กป >= MIN_MORPH_LEN๏ผ |
| ๅฆๅ่งไธบๅ้ณๆง๏ผๅฆ s+it, b+and๏ผ๏ผไฟๆๅ่ฏไธๅใ |
| """ |
| m = _WORD_RE.match(word) |
| if not m: |
| return word |
|
|
| prefix, core, suffix = m.groups() |
| if len(core) < MIN_WORD_LEN: |
| return word |
|
|
| segments = morf_model.viterbi_segment(core.lower())[0] |
| if len(segments) <= 1: |
| return word |
|
|
| |
| if not all(len(s) >= MIN_MORPH_LEN for s in segments): |
| return word |
|
|
| |
| parts = [] |
| pos = 0 |
| for seg in segments: |
| n = len(seg) |
| parts.append(core[pos:pos + n]) |
| pos += n |
|
|
| return prefix + ' '.join(parts) + suffix |
|
|
|
|
| def presegment_file(input_path: Path, morf_model, output_path: Path) -> Path: |
| """ๅฏนๆดไธชๆไปถ่ฟ่ก Morfessor ้ขๅๅใ""" |
| print("\nStep 2: ้ขๅๅ่ฎญ็ปๆๆฌ...") |
|
|
| line_count = 0 |
| with open(input_path) as fin, open(output_path, 'w') as fout: |
| for line in fin: |
| if line.strip(): |
| words = line.split() |
| segmented = [presegment_word(w, morf_model) for w in words] |
| fout.write(' '.join(segmented) + '\n') |
| else: |
| fout.write('\n') |
| line_count += 1 |
| if line_count % 200000 == 0: |
| print(f" ๅทฒๅค็ {line_count:,} ่ก...") |
|
|
| print(f" ๆปๅ
ฑๅค็ {line_count:,} ่ก") |
| print(f" ้ขๅๅๆไปถ: {output_path} ({output_path.stat().st_size / 1e6:.1f} MB)") |
|
|
| |
| print("\n ๅฏนๆฏ็คบไพ:") |
| shown = 0 |
| with open(input_path) as f1, open(output_path) as f2: |
| for orig, seg in zip(f1, f2): |
| if orig.strip() != seg.strip() and 30 < len(orig.strip()) < 150: |
| print(f" ๅ: {orig.strip()}") |
| print(f" ๅ: {seg.strip()}") |
| print() |
| shown += 1 |
| if shown >= 5: |
| break |
|
|
| return output_path |
|
|
|
|
| |
| |
| |
|
|
| def build_and_train_bpe(segmented_file: Path, vocab_size: int) -> Tokenizer: |
| """่ฎญ็ป BPE๏ผๆถๆไธๆ ๅ tokenizer ๅฎๅ
จไธ่ดใ""" |
| print(f"\nStep 3: ่ฎญ็ป BPE (vocab_size={vocab_size})...") |
|
|
| |
| normalizer = Sequence([ |
| Prepend(prepend=" "), |
| NFKC(), |
| Replace(Regex(r"\n"), "\n "), |
| Replace(Regex(r" *\n"), "\n"), |
| ]) |
|
|
| |
| GPT4_REGEX = ( |
| r"[^\r\n\p{L}\p{N}]?[\p{Lu}\p{Lt}\p{Lm}\p{Lo}\p{M}]*" |
| r"[\p{Ll}\p{Lm}\p{Lo}\p{M}]+" |
| r"|[^\r\n\p{L}\p{N}]?[\p{Lu}\p{Lt}\p{Lm}\p{Lo}\p{M}]+" |
| r"[\p{Ll}\p{Lm}\p{Lo}\p{M}]*" |
| r"| ?\p{N}" |
| r"| ?[^\s\p{L}\p{N}]+[\r\n/]*" |
| r"|\s*[\r\n]+" |
| r"|\s+(?!\S)" |
| r"|\s+" |
| ) |
| pre_tokenizer = PreSeq([ |
| Split(pattern=Regex(GPT4_REGEX), behavior="isolated"), |
| ByteLevel(add_prefix_space=False, trim_offsets=True, use_regex=False), |
| Split(pattern=Regex(r".{1,24}"), behavior="isolated"), |
| ]) |
|
|
| tokenizer = Tokenizer(BPE(unk_token="<unk>")) |
| tokenizer.normalizer = normalizer |
| tokenizer.pre_tokenizer = pre_tokenizer |
|
|
| trainer = BpeTrainer( |
| vocab_size=vocab_size, |
| special_tokens=SPECIAL_TOKENS, |
| min_frequency=2, |
| show_progress=True, |
| ) |
|
|
| tokenizer.train(files=[str(segmented_file)], trainer=trainer) |
| print(f" ๅฎ้
vocab size: {tokenizer.get_vocab_size()}") |
|
|
| |
| tokenizer.post_processor = TemplateProcessing( |
| single="<s> $A", |
| pair="<s> $A <s> $B", |
| special_tokens=[("<s>", tokenizer.token_to_id("<s>"))], |
| ) |
|
|
| |
| expected = {"<unk>": 0, "<s>": 1, "</s>": 2, "<pad>": 3, "<mask>": 4} |
| for token, eid in expected.items(): |
| aid = tokenizer.token_to_id(token) |
| status = "โ
" if aid == eid else "โ" |
| print(f" {status} {token:10s} expected={eid} actual={aid}") |
| if aid != eid: |
| raise ValueError(f"็นๆฎ token ID ไธไธ่ด: {token}") |
|
|
| return tokenizer |
|
|
|
|
| |
| |
| |
|
|
| def save_all(tokenizer: Tokenizer, morf_model, output_dir: Path): |
| """ไฟๅญ BPE tokenizer + Morfessor ๆจกๅใ""" |
| print(f"\nStep 4: ไฟๅญๅฐ {output_dir}/") |
| output_dir.mkdir(parents=True, exist_ok=True) |
|
|
| |
| raw_path = output_dir / "tokenizer.json" |
| tokenizer.save(str(raw_path)) |
|
|
| fast_tok = PreTrainedTokenizerFast( |
| tokenizer_file=str(raw_path), |
| bos_token="<s>", eos_token="</s>", unk_token="<unk>", |
| sep_token="</s>", pad_token="<pad>", cls_token="<s>", mask_token="<mask>", |
| ) |
| fast_tok.save_pretrained(str(output_dir)) |
|
|
| |
| io = morfessor.MorfessorIO() |
| morf_path = output_dir / "morfessor.bin" |
| io.write_binary_model_file(str(morf_path), morf_model) |
|
|
| print(f" ๆไปถ: {sorted(f.name for f in output_dir.iterdir())}") |
|
|
|
|
| |
| |
| |
|
|
| def verify(output_dir: Path, morf_model): |
| """ๅ ่ฝฝๅนถๅฏนๆฏๆ ๅ BPE vs Morfessor+BPEใ""" |
| print("\nStep 5: ้ช่ฏ & ๅฏนๆฏ...") |
|
|
| fast_tok = PreTrainedTokenizerFast.from_pretrained(str(output_dir)) |
|
|
| std_path = ROOT / "models/tokenizer/tokenizer.json" |
| if std_path.exists(): |
| std_tok = Tokenizer.from_file(str(std_path)) |
| else: |
| std_tok = None |
|
|
| tests = [ |
| "The cat sat on the mat.", |
| "She was running quickly through the forest.", |
| "I don't think he's coming today.", |
| "unhappiness", |
| "running jumped swimming", |
| "The ice is cold and the fire is hot.", |
| "nationalization", |
| "governmental", |
| "The children played happily in the garden.", |
| ] |
|
|
| for t in tests: |
| |
| seg_t = ' '.join(presegment_word(w, morf_model) for w in t.split()) |
| morf_tokens = fast_tok.tokenize(seg_t) |
|
|
| print(f" ๅๆ: {t}") |
| if seg_t != t: |
| print(f" ้ขๅ: {seg_t}") |
| if std_tok: |
| std_tokens = std_tok.encode(t).tokens |
| print(f" ๆ ๅBPE ({len(std_tokens):2d}): {std_tokens}") |
| print(f" Morf+BPE ({len(morf_tokens):2d}): {morf_tokens}") |
| print() |
|
|
|
|
| |
| |
| |
|
|
| def main(): |
| parser = argparse.ArgumentParser(description="่ฎญ็ป Morfessor+BPE Tokenizer") |
| parser.add_argument("--input", default=str(DEFAULT_INPUT), help="่ฎญ็ปๆไปถ่ทฏๅพ") |
| parser.add_argument("--output", default=str(DEFAULT_OUT), help="่พๅบ็ฎๅฝ") |
| parser.add_argument("--vocab_size", default=8192, type=int, help="่ฏ่กจๅคงๅฐ") |
| parser.add_argument("--corpusweight", default=CORPUSWEIGHT, type=float, |
| help=f"Morfessor corpusweight, ่ถๅฐ่ถๆฟ่ฟ (้ป่ฎค{CORPUSWEIGHT})") |
| args = parser.parse_args() |
|
|
| input_path = Path(args.input) |
| output_dir = Path(args.output) |
|
|
| if not input_path.exists(): |
| raise FileNotFoundError(f"่ฎญ็ปๆไปถไธๅญๅจ: {input_path}") |
|
|
| print(f"โโโ ่ฎญ็ป Morfessor+BPE Tokenizer โโโ") |
| print(f" ่พๅ
ฅ: {input_path} ({input_path.stat().st_size / 1e6:.1f} MB)") |
| print(f" ่พๅบ: {output_dir}") |
| print(f" vocab_size: {args.vocab_size}") |
| print() |
|
|
| |
| morf_model = train_morfessor_model(input_path, corpusweight=args.corpusweight) |
|
|
| |
| seg_path = output_dir / "_presegmented_train.txt" |
| output_dir.mkdir(parents=True, exist_ok=True) |
| presegment_file(input_path, morf_model, seg_path) |
|
|
| |
| tokenizer = build_and_train_bpe(seg_path, args.vocab_size) |
|
|
| |
| save_all(tokenizer, morf_model, output_dir) |
|
|
| |
| verify(output_dir, morf_model) |
|
|
| |
| seg_path.unlink() |
| print(f" ๅทฒๅ ้คไธญ้ดๆไปถ: {seg_path.name}") |
|
|
| print("\nโโโ ๅฎๆ๏ผโโโ") |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|