Tokenizer od nuly

Jak vzniká BPE slovník, speciální tokeny a chat template.

Co se naučíš: Natrénuješ si vlastní BPE slovník a uvidíš, proč se čeština drolí na víc tokenů než angličtina.

12 min čteníNavazuje na:🔤 Tokeny

Kapitola o tokenech říkala, co token je. Tady se podíváme, jak vzniká slovník, proč zrovna takhle a co jsou speciální tokeny a chat template, dvě věci, které v praxi rozbijí víc aplikací než celá architektura dohromady.


BPE: slovník se natrénuje z dat

Byte-Pair Encoding je překvapivě jednoduchý algoritmus. Začni s jednotlivými znaky (nebo bajty) a opakovaně slučuj nejčastější sousední dvojici:

korpus: "nižší nižší nižší nejnižší"

start:  n i ž š í   n i ž š í   n i ž š í   n e j n i ž š í
1. krok: nejčastější dvojice je "ž š"  →  slouč na "žš"
        n i žš í   n i žš í   n i žš í   n e j n i žš í
2. krok: nejčastější je "i žš"        →  slouč na "ižš"
3. krok: "n ižš"                       →  "nižš"
4. krok: "nižš í"                      →  "nižší"
...

Opakuje se to, dokud slovník nedosáhne cílové velikosti (typicky 32 000 až 200 000). Výsledek jsou merge rules: uspořádaný seznam slučování, plus slovník. Při tokenizaci textu se pak tatáž pravidla aplikují ve stejném pořadí.

Důsledek: častá slova skončí jako jeden token, vzácná se rozpadnou na kousky. A protože se slovník trénuje na konkrétním korpusu, jazyky zastoupené v datech málo (třeba čeština) na tom budou hůř.


Trénink tokenizeru je tedy obyčejné počítání četností, žádná neuronová síť. Proto taky tokenizer dokonale odráží data, na kterých vznikl: pokud v nich bylo málo češtiny, česká slova se budou drolit na kousky a stejný text tě vyjde na víc tokenů.


Byte-level: proč nikdy nedostaneš „neznámé slovo“

Starší tokenizery měly speciální token <UNK> pro cokoli neznámého a s ním ztrátu informace. Dnešní modely to řeší byte-level BPE: základní jednotkou není znak, ale bajt. Protože každý myslitelný text je posloupnost bajtů, model dokáže reprezentovat úplně cokoli, emoji, čínštinu, binární balast.

Cena: text mimo trénovací distribuci se rozpadne na jednotlivé bajty a spotřebuje spoustu tokenů. Proto je zpracování neobvyklých znaků drahé.

SentencePiece je alternativní implementace (často s tzv. unigram modelem místo BPE), která navíc pracuje přímo s textem včetně mezer a nepotřebuje předchozí rozdělení na slova. Prakticky se chová podobně.


Natrénuj si BPE sám

Tohle je celý algoritmus. Potřebuješ jen soubor text.txt, žádnou knihovnu:

▶ Spustitelné. Ulož jako bpe.py a pusť python3 bpe.py. Potřebuješ jen soubor text.txt.

"""BPE od nuly: natrénuj slovník na vlastním textu a podívej se, co z něj vypadne."""
from collections import Counter

text = open('text.txt', encoding='utf8').read()
words = [tuple(w) + ('</w>',) for w in text.split()]     # každé slovo = n-tice znaků
vocab = Counter(words)
merges = []

for step in range(300):                                  # 300 sloučení stačí na ukázku
    pairs = Counter()
    for word, freq in vocab.items():
        for a, b in zip(word, word[1:]):
            pairs[(a, b)] += freq
    if not pairs: break
    best = pairs.most_common(1)[0][0]                    # nejčastější sousední dvojice
    merges.append(best)
    new = {}
    for word, freq in vocab.items():
        w, i = [], 0
        while i < len(word):
            if i < len(word) - 1 and (word[i], word[i+1]) == best:
                w.append(word[i] + word[i+1]); i += 2
            else:
                w.append(word[i]); i += 1
        new[tuple(w)] = freq
    vocab = new
    if step < 10 or step in (49, 99, 299):
        print(f'{step+1:3d}. sloučení: {best[0]!r} + {best[1]!r} -> {best[0]+best[1]!r}')

def encode(word):
    w = tuple(word) + ('</w>',)
    for a, b in merges:
        out, i = [], 0
        while i < len(w):
            if i < len(w) - 1 and (w[i], w[i+1]) == (a, b):
                out.append(a + b); i += 2
            else:
                out.append(w[i]); i += 1
        w = tuple(out)
    return w

print()
for word in ['model', 'pravděpodobnost', 'transformer', 'nejneobhospodařovávatelnějšímu']:
    t = encode(word)
    kusy = [x.replace('</w>', '') for x in t if x != '</w>']
    print(f'{word:30s} -> {len(kusy)} tokenů: {" | ".join(kusy)}')

Na textu téhle učebnice vypadl tenhle výstup:

  1. sloučení: 'e' + '</w>' -> 'e</w>'
  2. sloučení: ',' + '</w>' -> ',</w>'
  7. sloučení: 's' + 't' -> 'st'
  8. sloučení: 'e' + 'n' -> 'en'
 50. sloučení: 'm' + 'ě' -> 'mě'
300. sloučení: 'm' + 'at' -> 'mat'

model                          -> 1 tokenů: model
pravděpodobnost                -> 5 tokenů: prav | dě | pod | ob | nost
transformer                    -> 6 tokenů: t | ra | n | s | form | er
nejneobhospodařovávatelnějšímu -> 17 tokenů: nej | ne | ob | h | o | s | pod | a | ř | ov | áv | atel | ně | j | š | í | mu

Přečti si z toho tři věci. Za prvé: první sloučení jsou konce slov, protože nic není častější než konec slova. Za druhé: model je jeden jediný token, protože v tomhle korpusu je to nejčastější slovo. A za třetí: transformer se rozpadl na šest kusů, přestože je to slovo, o kterém je půlka učebnice. Tokenizer nezná význam, zná jen četnosti, a v českém textu se anglické slovo prostě neopakuje dost často na to, aby si zasloužilo vlastní token.

Až budeš někde číst, že model „neumí počítat písmena“, vzpomeň si na tenhle výpis. Ono to ta písmena opravdu nevidí.


Velikost slovníku je kompromis

Menší slovník (32 k)Větší slovník (200 k)
Menší embedding a výstupní vrstvaVětší (významná část parametrů malého modelu)
Delší sekvence, víc tokenů na tentýž textKratší sekvence, levnější kontext
Horší pokrytí neanglických jazykůVýrazně lepší pro vícejazyčné modely

Posun k větším slovníkům v posledních letech je hlavně kvůli vícejazyčnosti a kódu a je to jeden z důvodů, proč novější modely zvládají češtinu lépe i při stejné velikosti.


Speciální tokeny

Kromě běžného textu má slovník tokeny s vyhrazeným významem:

  • BOS / EOS: začátek a konec sekvence. EOS je to, čím model říká „domluvil jsem“.
  • PAD: výplň, aby měly sekvence v dávce stejnou délku.
  • Role a oddělovače zpráv: začátek a konec systémové, uživatelské a asistentovy zprávy.
  • Tokeny pro nástroje: ohraničení volání funkce a jeho výsledku.
  • Tokeny pro uvažování: u reasoning modelů ohraničují skrytou úvahu.

Tyhle tokeny model viděl při tréninku ve specifickém významu, takže na ně reaguje jinak než na tentýž text napsaný ručně.


Chat template: nejčastější zdroj potíží

Model nedostane strukturu zpráv jako JSON. Dostane jeden řetězec, do kterého se zprávy poskládají podle šablony konkrétního modelu:

<|system|>Jsi asistent.<|end|>
<|user|>Ahoj<|end|>
<|assistant|>Ahoj! Jak můžu pomoct?<|end|>
<|user|>Kolik je hodin?<|end|>
<|assistant|>

Poslední řádek je klíčový: řetězec končí otevřenou zprávou asistenta, takže model přirozeně pokračuje jeho odpovědí.

⚠️ Každá rodina modelů má jinou šablonu. Když použiješ špatnou (nebo si ji sestavíš ručně a přehlédneš detail), model se chová divně: nedodržuje role, nezastaví se, opakuje se nebo si vymýšlí repliky uživatele. Při lokálním provozu vždycky použij šablonu, kterou model přináší ve své konfiguraci, ne vlastní improvizaci.


Praktické důsledky

  • Počty tokenů zjišťuj tokenizerem daného modelu, nikdy odhadem ani z jiného modelu.
  • Nepiš speciální tokeny do uživatelského vstupu. Uživatel, který napíše <|system|>, se ti jinak může pokusit vydávat za systém a je to reálný vektor prompt injection. Slušné knihovny to escapují, ale ověř si to.
  • Mezera je součástí tokenu. Prompt končící mezerou umí zhoršit výstup, protože model dostane nezvyklé rozdělení.
  • Fine-tuning musí používat stejnou šablonu jako inference, jinak se model naučí formát, který mu pak nikdo nedává.

Shrnutí

  • BPE staví slovník opakovaným slučováním nejčastějších dvojic; výsledek jsou merge rules.
  • Byte-level varianta zvládne libovolný text bez <UNK>, ale neobvyklé znaky jsou drahé.
  • Speciální tokeny mají naučený význam a chat template určuje, jak se zprávy skládají do řetězce.
  • Špatná šablona nebo speciální tokeny v uživatelském vstupu jsou zdrojem podivného chování i rizik.
Jak se vytváří BPE slovník?

Začne se od jednotlivých znaků nebo bajtů a opakovaně se slučuje nejčastější sousední dvojice, dokud slovník nedosáhne cílové velikosti. Výsledkem je uspořádaný seznam slučovacích pravidel, který se pak stejně aplikuje na každý tokenizovaný text.

Proč moderní tokenizery nemají token pro neznámé slovo?

Protože pracují na úrovni bajtů, každý myslitelný text je posloupnost bajtů, takže se dá vždy reprezentovat. Cenou je, že neobvyklé znaky se rozpadnou na mnoho tokenů, a jsou proto drahé.

Lokální model odpovídá zvláštně: nedodržuje role a někdy si sám dopisuje repliky uživatele. Co zkontrolovat nejdřív?

Chat template. Každá rodina modelů má vlastní formát skládání zpráv se speciálními tokeny; když se použije špatný nebo ručně sestavený, model nepozná hranice rolí. Správná šablona je součástí konfigurace modelu a má se použít přesně ta.