GooGooLM / scripts /02_model /train_tokenizer_morfessor.py
XiaoyanLi's picture
Initial upload: code, training data, tokenizers, notes
83112d8 verified
Raw
History Blame Contribute Delete
14.5 kB
"""
่ฎญ็ปƒ 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>"]
# Morfessor ่ถ…ๅ‚
CORPUSWEIGHT = 0.01 # ่ถŠๅฐๅˆ‡ๅˆ†่ถŠๆฟ€่ฟ›๏ผˆ้ป˜่ฎค~1.0 ๅคชไฟๅฎˆ๏ผ‰
MIN_MORPH_LEN = 2 # ๅฝขๆ€็ด ๆœ€็Ÿญ้•ฟๅบฆ๏ผŒ่ฟ‡ๆปค s+it / b+and ็ญ‰ๅ‡้˜ณๆ€ง
MIN_WORD_LEN = 3 # ็ŸญไบŽๆญค้•ฟๅบฆ็š„่ฏไธๅˆ‡ๅˆ†
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
# Step 1: ่ฎญ็ปƒ Morfessor
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
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
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
# Step 2: ้ข„ๅˆ‡ๅˆ†ๆ–‡ๆœฌ
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
_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
# ่ฟ‡ๆปค๏ผšๆ‰€ๆœ‰ๅฝขๆ€็ด ๅฟ…้กป >= MIN_MORPH_LEN
if not all(len(s) >= MIN_MORPH_LEN for s in segments):
return word
# ๆŒ‰ segment ้•ฟๅบฆไปŽๅŽŸๅง‹ core ไธญๅˆ‡็‰‡๏ผˆไฟ็•™ๅคงๅฐๅ†™๏ผ‰
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
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
# Step 3: ๅœจ้ข„ๅˆ‡ๅˆ†ๆ–‡ๆœฌไธŠ่ฎญ็ปƒ BPE
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
def build_and_train_bpe(segmented_file: Path, vocab_size: int) -> Tokenizer:
"""่ฎญ็ปƒ BPE๏ผŒๆžถๆž„ไธŽๆ ‡ๅ‡† tokenizer ๅฎŒๅ…จไธ€่‡ดใ€‚"""
print(f"\nStep 3: ่ฎญ็ปƒ BPE (vocab_size={vocab_size})...")
# Normalizer๏ผˆไธŽๆ ‡ๅ‡† BPE ไธ€่‡ด๏ผ‰
normalizer = Sequence([
Prepend(prepend=" "),
NFKC(),
Replace(Regex(r"\n"), "\n "),
Replace(Regex(r" *\n"), "\n"),
])
# Pre-tokenizer๏ผˆไธŽๆ ‡ๅ‡† BPE ไธ€่‡ด๏ผ‰
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()}")
# Post-processor: ๅฅ้ฆ–ๅŠ  <s>
tokenizer.post_processor = TemplateProcessing(
single="<s> $A",
pair="<s> $A <s> $B",
special_tokens=[("<s>", tokenizer.token_to_id("<s>"))],
)
# ๆ ก้ชŒ็‰นๆฎŠ token ID
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
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
# Step 4: ไฟๅญ˜
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
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)
# BPE tokenizer
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))
# Morfessor ๆจกๅž‹
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())}")
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
# Step 5: ้ชŒ่ฏ & ๅฏนๆฏ”
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
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:
# Morfessor+BPE: ๅ…ˆ้ข„ๅˆ‡ๅˆ†
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()
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
# Main
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
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()
# Step 1: Morfessor
morf_model = train_morfessor_model(input_path, corpusweight=args.corpusweight)
# Step 2: ้ข„ๅˆ‡ๅˆ†
seg_path = output_dir / "_presegmented_train.txt"
output_dir.mkdir(parents=True, exist_ok=True)
presegment_file(input_path, morf_model, seg_path)
# Step 3: BPE
tokenizer = build_and_train_bpe(seg_path, args.vocab_size)
# Step 4: ไฟๅญ˜
save_all(tokenizer, morf_model, output_dir)
# Step 5: ้ชŒ่ฏ
verify(output_dir, morf_model)
# ๅˆ ้™ค้ข„ๅˆ‡ๅˆ†ไธญ้—ดๆ–‡ไปถ
seg_path.unlink()
print(f" ๅทฒๅˆ ้™คไธญ้—ดๆ–‡ไปถ: {seg_path.name}")
print("\nโ•โ•โ• ๅฎŒๆˆ๏ผโ•โ•โ•")
if __name__ == "__main__":
main()