| import json |
| import re |
| from collections import Counter |
| from pathlib import Path |
|
|
| SPECIALS = ["<pad>", "<bos>", "<eos>", "<|user|>", "<|assistant|>"] |
| N_SPECIAL = len(SPECIALS) |
| BYTE_OFFSET = N_SPECIAL |
| MERGE_OFFSET = N_SPECIAL + 256 |
|
|
| PAT = re.compile(r"""'(?:[sdmt]|ll|ve|re)| ?[^\W\d_]+| ?\d+| ?[^\s\w]+|\s+""") |
| SPECIAL_PAT = re.compile("(" + "|".join(re.escape(s) for s in SPECIALS) + ")") |
|
|
|
|
| class Tokenizer: |
| def __init__(self): |
| self.merges = [] |
| self.special_to_id = {s: i for i, s in enumerate(SPECIALS)} |
| self.id_to_special = {i: s for i, s in enumerate(SPECIALS)} |
| self.pair_rank = {} |
| self.decode_pair = {} |
|
|
| @property |
| def vocab_size(self): |
| return MERGE_OFFSET + len(self.merges) |
|
|
| def _build_maps(self): |
| self.pair_rank = {tuple(p): r for r, p in enumerate(self.merges)} |
| self.decode_pair = {MERGE_OFFSET + r: tuple(p) for r, p in enumerate(self.merges)} |
|
|
| @staticmethod |
| def _merge_word(word, pair, new_id): |
| a, b = pair |
| out = [] |
| i = 0 |
| while i < len(word): |
| if i < len(word) - 1 and word[i] == a and word[i + 1] == b: |
| out.append(new_id) |
| i += 2 |
| else: |
| out.append(word[i]) |
| i += 1 |
| return tuple(out) |
|
|
| def _corpus_words(self, text): |
| counts = Counter() |
| for seg in SPECIAL_PAT.split(text): |
| if not seg or seg in self.special_to_id: |
| continue |
| for chunk in PAT.findall(seg): |
| word = tuple(BYTE_OFFSET + b for b in chunk.encode("utf-8")) |
| counts[word] += 1 |
| return counts |
|
|
| def train(self, text, vocab_size, verbose=True): |
| words = dict(self._corpus_words(text)) |
| n_merges = vocab_size - MERGE_OFFSET |
| self.merges = [] |
| for step in range(n_merges): |
| pairs = Counter() |
| for word, freq in words.items(): |
| for a, b in zip(word, word[1:]): |
| pairs[(a, b)] += freq |
| if not pairs: |
| break |
| best = max(pairs, key=lambda p: (pairs[p], p)) |
| new_id = MERGE_OFFSET + len(self.merges) |
| self.merges.append([best[0], best[1]]) |
| words = {self._merge_word(w, best, new_id): c for w, c in words.items()} |
| if verbose and (step + 1) % 500 == 0: |
| print(f" merge {step + 1}/{n_merges} pair={best} count={pairs[best]}") |
| self._build_maps() |
|
|
| def _encode_chunk(self, bts): |
| word = [BYTE_OFFSET + b for b in bts] |
| while len(word) >= 2: |
| best = None |
| best_rank = None |
| for a, b in zip(word, word[1:]): |
| r = self.pair_rank.get((a, b)) |
| if r is not None and (best_rank is None or r < best_rank): |
| best_rank = r |
| best = (a, b) |
| if best is None: |
| break |
| word = list(self._merge_word(tuple(word), best, MERGE_OFFSET + best_rank)) |
| return word |
|
|
| def encode(self, text): |
| ids = [] |
| for seg in SPECIAL_PAT.split(text): |
| if not seg: |
| continue |
| if seg in self.special_to_id: |
| ids.append(self.special_to_id[seg]) |
| continue |
| for chunk in PAT.findall(seg): |
| ids.extend(self._encode_chunk(chunk.encode("utf-8"))) |
| return ids |
|
|
| def _expand(self, i): |
| if i in self.decode_pair: |
| a, b = self.decode_pair[i] |
| return self._expand(a) + self._expand(b) |
| return [i - BYTE_OFFSET] |
|
|
| def decode(self, ids): |
| out = [] |
| buf = [] |
| for i in ids: |
| if i in self.id_to_special: |
| if buf: |
| out.append(bytes(buf).decode("utf-8", errors="replace")) |
| buf = [] |
| out.append(self.id_to_special[i]) |
| else: |
| buf.extend(self._expand(i)) |
| if buf: |
| out.append(bytes(buf).decode("utf-8", errors="replace")) |
| return "".join(out) |
|
|
| def save(self, path): |
| Path(path).write_text( |
| json.dumps({"specials": SPECIALS, "merges": self.merges}), |
| encoding="utf-8", |
| ) |
|
|
| @classmethod |
| def load(cls, path): |
| data = json.loads(Path(path).read_text(encoding="utf-8")) |
| tok = cls() |
| tok.merges = [list(m) for m in data["merges"]] |
| tok._build_maps() |
| return tok |
|
|