abyss-api / ml /tokenizer /train.py
KyleEastwood's picture
Upload folder using huggingface_hub
cc15101 verified
Raw History Blame Contribute Delete
4.82 kB
"""
BPETrainer: learn merge rules from raw text.
Note: this recounts all pairs each iteration (clear but 0(merges x tokens)).
Fine for today's small demo. For tomorrow's full TinyStories run we'll add
incremental pair-count updates so it scales.
"""
import os
import json
from collections import Counter, defaultdict
from .regex_split import split
from .tokenizer import merge_seq
from .special_tokens import SPECIAL_TOKENS, special_token_ids
def _pairs(ids):
"""Counter of adjacent id pairs within one word."""
c = Counter()
for a, b in zip(ids, ids[1:]):
c[(a, b)] += 1
return c
class BPETrainer:
def __init__(self, vocab_size=16384, verbose=False):
self.vocab_size = vocab_size
self.verbose = verbose
self.merges = []
self.vocab = {}
# ---- entry points ----
def train(self, text):
"""Train from an in-memory string *used by the test)."""
return self._fit(Counter(split(text)))
def train_from_file(self, path):
"""Stream a (possibly multi-GB) text file line by line so we never
hold the whole corpus in memory."""
word_freqs = Counter()
with open(path, "r", encoding="utf-8") as f:
for i, line in enumerate(f):
if "<|endoftext|>" in line:
line = line.replace("<|endoftext|>", " ")
word_freqs.update(split(line))
if self.verbose and i and i % 200000 == 0:
print(f" read {i:,} lines, {len(word_freqs):,} unique chunks")
if self.verbose:
print(f" done reading: {len(word_freqs):,} unique chunks")
return self._fit(word_freqs)
# ---- core training ----
def _fit(self, word_freqs):
num_merges = self.vocab_size - 256 - len(SPECIAL_TOKENS)
if num_merges < 0:
raise ValueError("vocab_size too small for 256 + specials")
# each unique word -> list of byte ids
splits = {w: list(w.encode("utf-8")) for w in word_freqs}
# running pair counts (weighted by word freq) + which words hold each pair
pair_counts = Counter()
where = defaultdict(set)
for w, freq in word_freqs.items():
for p, c in _pairs(splits[w]).items():
pair_counts[p] += c * freq
where[p].add(w)
merges = []
vocab = {i: bytes([i]) for i in range(256)}
for m in range(num_merges):
if not pair_counts:
break
best = max(pair_counts, key=pair_counts.get)
if pair_counts[best] <= 0:
break
new_id = 256 + m
merges.append(best)
vocab[new_id] = vocab[best[0]] + vocab[best[1]]
# only the words containing 'best' can change
for w in list(where[best]):
freq = word_freqs[w]
old = splits[w]
before = _pairs(old)
new = merge_seq(old, best, new_id)
splits[w] = new
after = _pairs(new)
for p in set(before) | set(after):
delta = (after[p] - before[p]) * freq
if delta:
pair_counts[p] += delta
if after[p] > 0:
where[p].add(w)
else:
where[p].discard(w)
if pair_counts.get(p, 0) <= 0:
pair_counts.pop(p, None)
where.pop(p, None)
if self.verbose and (m < 20 or m% 500 == 0):
piece = vocab[new_id].decode("utf-8", errors="replace")
print(f"merge {m:5d}: {best} -> id {new_id} {piece!r}")
self.merges = merges
self.vocab = vocab
return merges, vocab
# ---- save ----
def save(self, out_dir):
os.makedirs(out_dir, exist_ok=True)
with open(os.path.join(out_dir, "merges.txt"), "w", encoding="utf-8") as f:
for a, b in self.merges:
f.write(f"{a} {b}\n")
# human-readable view (not used for loading, just for inspection)
view = {i: b.decode("utf-8", errors="replace")
for i, b in self.vocab.items()}
for tok, tid in special_token_ids(self.vocab_size).items():
view[tid] = tok
with open(os.path.join(out_dir, "vocab.json"), "w", encoding="utf-8") as f:
json.dump(view, f, ensure_ascii=False, indent=0)
with open(os.path.join(out_dir, "config.json"), "w", encoding="utf-8") as f:
json.dump({"vocab_size": self.vocab_size}, f)