jancll.com

Lekce 02 · 14 min čtení

Tokenizace

Proč model nevidí slova ani písmena, ale něco mezi. BPE krok za krokem nad Švejkem.

Úvod

Tokeny, tokeny, tokeny. V poslední době jako bychom o ničem jiném neslyšeli. Měří se v nich délka kontextu, účtuje se podle nich cena za API, sype je z rukávu kdejaký článek o AI. Co to ale token vlastně je?

Nic složitého: token je kousek textu, se kterým model zachází jako se základní jednotkou. Může to být celé slovo, kus slova, nebo jen jeden znak. A jak přesně se text na tyhle kousky krájí, je překvapivě zásadní rozhodnutí. Přesně o tom je tahle lekce.

Na konci té minulé jsme totiž narazili na problém. Náš tokenizér rozsekal text regexem \w+ na slova a tím pádem Švejk, Švejka a Švejkovi byly tři úplně cizí věci. Model samozřejmě neměl ponětí, že spolu nějak souvisí. Jako kdybys neznal žádnou souvislost mezi „pes" a „psovi".

Postavíme si BPE (byte-pair encoding), algoritmus, který text nekrájí ani na slova, ani na písmena, ale na něco mezi. A uvidíš, že to „mezi" je překvapivě chytré.

Švejk krájí proud textu na malé tokeny a skládá je do BPE tokenizéru

Švejk stříhá a slepuje tokeny.

Dva extrémy: znaky vs slova

Než dojdeme k chytrému řešení, podívejme se na ty dvě hloupá. Máš dvě nejjednodušší možnosti, jak text nasekat na tokeny.

Po slovech. Každé slovo = jeden token. Zní to rozumně, ale:

  • Slovník bude obrovský. Čeština má desítky tisíc slov a každý pád, číslo a osobu počítá zvlášť: švejk, švejka, švejkovi, švejkem, švejkové… jedno jméno, pět tokenů.
  • A co slovo, které model nikdy neviděl? švejkologie? Smůla. Padá do koše jako „neznámé slovo" a model je slepý.

Po znacích. Každé písmeno = jeden token. Opačný extrém:

  • Slovník je titěrný, pár desítek znaků a hotovo. Žádné neznámé slovo neexistuje, protože všechno je jen kombinace písmen.
  • Ale sekvence jsou strašně dlouhé. nedotknutelnost je jeden pojem, ale patnáct tokenů. A model se musí učit skládat slova z písmenek od nuly pokaždé znovu. Drahé a pomalé.

Vidíš ten háček? Každá z těch dvou možností je dobrá přesně v tom, v čem je ta druhá mizerná. BPE sedí přesně mezi nimi a sebere si z obou jen to dobré.

BPE: zlatá střední cesta

Princip je jednoduchý:

  1. Začni s nejhloupějším slovníkem: jen jednotlivé znaky.
  2. Najdi nejčastější sousední dvojici tokenů v celém korpusu.
  3. Slep ji do jednoho nového tokenu a přidej do slovníku.
  4. Opakuj kroky 2 a 3 pořád dokola, klidně tisíckrát.

To je celé. Žádná neuronová síť, žádné učení, jen počítání dvojic a slepování. A přesto z toho vypadne něco překvapivě užitečného: časté kousky slov (švejk, předpony, koncovky) se stanou vlastními tokeny, zatímco vzácné slovo se prostě poskládá z menších kousků.

Nejlíp to pochopíme na příkladu. Klikej v playgroundu krok po kroku a sleduj, jak se z písmenek š v e j k postupně slepí jediný token švejk a jak ho pak BPE recykluje napříč všemi pády:

TokenizerPlayground · BPE nad Švejkem

Klikej další merge › a sleduj, jak se nejčastější sousední dvojice slévají do delších tokenů. · značí konec slova.

merge 0 / 12

Začínáme: každé slovo rozsekané na jednotlivé znaky. Žádný token zatím delší než písmeno.

  • 6ךvejk·
  • 4ךvejka·
  • 3ךvejkovi·
  • 2ךvejkem·
  • 3×poslušně·
  • 1×poslušnost·

Naučené tokeny (0)

Zatím žádné — slovník jsou jen samotné znaky.

Všimni si dvou věcí:

  1. Jakmile vznikne token švejk, naskočí naráz hned do několika slov: švejk, švejka, švejkovi, švejkem. To je přesně ta souvislost, která nám v lekci 1 chyběla. Model už nevidí cizí slova, ale jeden společný základ + koncovku.
  2. BPE je hladový (greedy): v každém kroku bere lokálně nejčastější dvojici, nic neplánuje dopředu. Žádná globální optimalizace, žádné triky. A přesto to funguje. Často je ten nejjednodušší (nejhloupější) přístup ten správný.

Matika (ještě kratší než minule)

Tady toho moc není. Jediné, co počítáme, je frekvence dvojic. Pro každou sousední dvojici tokenů (a,b)(a, b) sečteme, kolikrát se v korpusu objeví:

count(a,b)=slovofreq(slovo)[slovo obsahuje ab]\text{count}(a, b) = \sum_{\text{slovo}} \text{freq}(\text{slovo}) \cdot [\text{slovo obsahuje } a\,b]

…což je děsivě vypadající zápis pro „projdi všechna slova a počítej". Pak vezmeme to s nejvyšším číslem a slepíme. Konec matiky.

Jediný vzorec, který stojí za zapamatování, se týká velikosti slovníku. Každý merge přidá do slovníku přesně jeden nový token. Takže:

V=abeceda+Nmerge|V| = |\text{abeceda}| + N_{\text{merge}}

Začneš s ~40 znaky a uděláš třeba 5 000 merge kroků → slovník má ~5 040 tokenů. A to je hezké, protože velikost slovníku si přímo volíš počtem merge kroků. Dnešní modely se pohybují řádově v desítkách až stovkách tisíc tokenů. Zní to málo, ale právě to je kouzlo subwordů: pár desítek tisíc kousků složí libovolné slovo v libovolném jazyce.

Proč zrovna „byte“ pair?

Pravý BPE v moderních LLM nepracuje se znaky, ale s byty (0–255). Tím se elegantně vyřeší, že na světě existují emoji, čínština i ñ. Všechno je jen sekvence bytů a žádné slovo není doopravdy „neznámé". My tu pro názornost slepujeme znaky, ne byty.

V kódu

Pojďme to napsat. Je to kratší, než bys čekal. Celý tréninkový algoritmus se vejde do tří funkcí: spočítej dvojice, najdi nejčastější, slep ji dohromady. Klikni Run.

python · pyodide

Podívejme se na ten výstup pozorně. švejku a vymyšlené švejkologie v tréninku vůbec nebyly, a přesto je tokenizér nasekal s tokenem švejk uvnitř. To je ta síla BPE: nová slova nejsou „neznámá", jen se poskládají z kousků, které model už zná.

Kolik tokenů to vlastně ušetří?

Pojďme si ty dva extrémy z úvodu změřit na číslech. Stejný mini-korpus, dva hloupé způsoby krájení:

python · pyodide

Tady to máme černé na bílém (případně bílé na černém, pokud sis zapnul tmavý režim): znaky = malý slovník, dlouhý text. Slova = velký slovník, krátký text. BPE si to vyladí přesně tak, jak chceš: pár tisíc tokenů ve slovníku a rozumně krátké sekvence. Nejlepší z obou světů.

Mám pro to čerstvý příklad ze života. Můj nick jancll žádný model nikdy neviděl jako jedno slovo, takže se mu při tokenizaci rozpadne na pár cizích útržků. A co model udělá, když dostane něco, co nezná? Hádá. Když jsem si s ním psal, oslovoval mě jednou jančíl, jindy jancl, a párkrát mi rovnou začal říkat Honza. Nesnažil se mě naštvat, jen si z těch střípků skládal něco povědomějšího. Je to přímý důsledek tokenizace: model nevidí jancll, vidí pár kousků, a u vzácných nebo divných řetězců se z nich snadno trefí vedle.

Cvičení

Cvičení · lekce2-cv1

Než začneš klikat, tipni si: po kolikátém merge se z písmen š v e j k slepí jediný token švejk? Pak to v playgroundu ověř.

Cvičení · lekce2-cv2

Slovo se patří v češtině k nejčastějším. Vznikne z něj při BPE vlastní token (zadej 1), nebo zůstane navždy jako dvě písmena s + e (zadej 0)?

Cvičení · lekce2-cv3

Model má slovník 50 304 tokenů a začínal s abecedou 304 znaků. Kolik merge kroků při tréninku proběhlo?

Cvičení · lekce2-cv4

V tréninkovém kódu výš nahraď korpus vlastní rodinou slov (klidně svoje příjmení v pádech, nebo pivo, piva, pivu, pivní, pivovar) a pusť ho. Který subword ti BPE recykluje nejvíc?

Shrnutí

  • Tokenizace je krájení textu na kousky, se kterými model počítá. Základ všeho dalšího.
  • Slova = krátké sekvence, ale obří slovník a slepota vůči novým slovům. Znaky = přesně naopak.
  • BPE je střední cesta: začni se znaky a iterativně slepuj nejčastější dvojice.
  • Každý merge = jeden nový token, takže velikost slovníku si volíš sám: V=abeceda+Nmerge|V| = |\text{abeceda}| + N_{\text{merge}}.
  • Naučené subwordy se sdílí napříč slovy, takže i nové slovo se poskládá z kousků. Žádné „neznámé slovo".

Proč na tom záleží víc, než by se zdálo

Tokenizace vypadá jako nudná příprava dat, ale rozhoduje o spoustě věcí, které budou bolet později. Délka kontextu se počítá v tokenech, ne ve slovech. Cena za API se účtuje v tokenech. A jazyk, který se tokenizuje neefektivně, spotřebuje na stejnou myšlenku víc tokenů než angličtina, takže je i dražší a „kratší" v kontextovém okně. Není to fér, ale je to tak.

A hlavně: tokeny jsou to, co model opravdu vidí. Ne písmena, ne slova, ale tokeny. Když po modelu chceš spočítat písmena ve slově a ono se splete, je to často proto, že to slovo nikdy nevidělo jako písmena, jen jako pár tokenů. Dává to najednou smysl, co?

Co bude příště

Máme tokeny. Ale pořád jsou to jen kousky textu, pro počítač pořád dost neprůhledné symboly. Token švejk a token lukáš jsou pro model zatím jen dvě různá čísla bez jakéhokoliv vztahu.

V Lekci 3 (Embeddings) dáme každému tokenu vektor čísel, tedy souřadnice v prostoru významů. A stane se kouzlo: tokeny s podobným významem skončí blízko sebe. pivo kousek od hospoda, švejk kousek od voják. Poprvé v kurzu se modelu rozsvítí něco, co skoro připomíná porozumění. Skoro.