Van neuron tot taalmodel

Hoofdstuk 13 · van tekst naar nummers en terug

Hoe een taalmodel tekst leest

Een neuraal netwerk rekent met getallen. Tekst bestaat uit letters. Daar moet iets tussen zitten — en dat tussenstuk, de tokenizer, bepaalt meer dan je zou denken: waarom Nederlands duurder is dan Engels, waarom modellen niet kunnen tellen hoeveel letters een woord heeft, en waarom een spatie op de verkeerde plek het antwoord verandert.

In één alinea

Een tokenizer knipt tekst in stukjes die vaak samen voorkomen, en geeft elk stukje een nummer. Niet per letter, want dan worden de reeksen te lang. Niet per woord, want dan is de lijst nooit af. Maar per veelvoorkomend brokstuk, geleerd uit de tekst zelf.

Voor dit model zijn dat 151 936 stukjes. "the" is er één van. "onwaarschijnlijk" zijn er zes. Het getal 2026 kost er vijf. Waarom dat zo is, en wat het voor gevolgen heeft, is waar dit hoofdstuk over gaat.

151.936
stukjes in de woordenschat
151.387
samenvoegregels, in volgorde
4,4 → 3,2
bytes per token: Engels tegenover Nederlands
10
tokens die enkel uit cijfers bestaan

Deel 01Het probleem

Een taalmodel is een berg getallen die vermenigvuldigd worden. Er komt nergens een letter aan te pas. Toch typ je tekst in en krijg je tekst terug. Iets moet die vertaling doen.

De voor de hand liggende oplossing is een lijst: a is 1, b is 2, enzovoort. Dat werkt, maar het is een slechte keuze, en om te begrijpen waarom moet je weten wat een token het model kost.

Elke token die het model verwerkt, kost één volledige doorloop van het hele netwerk. Bij ons voorbeeldmodel is dat 494 miljoen vermenigvuldigingen. Per token. Bovendien kijkt elke token naar alle voorgaande, dus de kost loopt sneller op dan lineair. En het contextvenster — hoeveel het model tegelijk kan overzien — wordt niet in letters geteld maar in tokens.

Een token is dus de eenheid van kostprijs. Hoe minder tokens je nodig hebt voor dezelfde inhoud, hoe beter.

Waarom niet per letter

Met 26 letters, cijfers en leestekens kom je uit op een lijstje van misschien honderd. Klein en overzichtelijk. Maar de reeksen worden vier tot vijf keer zo lang, en dus vier tot vijf keer zo duur. Erger nog: het model moet dan zelf leren dat k-a-t samen iets betekent, en dat kost een deel van zijn capaciteit dat het aan nuttiger dingen kan besteden.

Waarom niet per woord

Dan wordt de lijst nooit af. Nieuwe woorden, namen, tikfouten, andere talen — en in het Nederlands bovendien samenstellingen die je onbeperkt kan blijven maken. Wat doet het model met een woord dat niet in de lijst staat? Er is geen goed antwoord op die vraag.

Het woord "onwaarschijnlijk", op drie manieren per letter o n w a a r s c h i j n l i j k 16 kleine lijst, maar zestien keer betalen per woord onwaarschijnlijk 1 goedkoop, maar wat als het woord er niet in staat? ... en "onwaarschijnlijker"? en "onwaarschijnlijkheid"? BPE Ġon wa ars ch ijn lijk 6 lijst is af, en elk woord dat ooit bestaat past erin De derde manier is wat elk modern taalmodel doet. De stukjes zijn niet bedacht maar geleerd: het zijn de brokken die in de trainingstekst het vaakst naast elkaar stonden. Vandaar dat "lijk" één stukje is en "wa" ook.
Dit is de echte opdeling die Qwen2.5 van dit woord maakt. Het teken Ġ aan het begin is de spatie ervoor — waarom dat zo is, komt in deel 4.

Deel 02De stukjes worden niet bedacht, maar geleerd

Het algoritme heet byte-pair encoding, afgekort BPE, en het is verrassend eenvoudig. Het komt uit de compressiewereld van de jaren negentig en werd later hergebruikt voor taalmodellen.

Het recept, om de lijst te leren:

  1. Neem een enorme berg tekst en knip alles in losse tekens.
  2. Tel welk paar naburige tekens het vaakst voorkomt.
  3. Voeg dat paar overal samen tot één nieuw stuk, en noteer die regel.
  4. Herhaal, tot je er genoeg hebt.

Meer is het niet. Er komt geen taalkunde aan te pas, geen woordenboek, geen grammatica. Puur tellen.

De samenvoeglijst leren, op een piepklein taaltje Drie woorden: "kat" (3×), "kater" (2×), "katten" (1×). Tel telkens welk naburig paar het vaakst voorkomt en voeg dat samen. ronde het taaltje na die ronde wat wint nieuwe regel 0 k a t k a t e r k a t t e n het paar k a komt 6× voor ka 1 ka t ka t e r ka t t e n het paar ka t komt 6× voor kat 2 kat kat e r kat t e n het paar kat e komt 2× voor kate 3 kat kate r kat t e n het paar kate r komt 2× voor kater 4 kat kater kat t e n niets komt nog vaker dan 1× voor: stoppen De vier regels in deze volgorde — ka, kat, kate, kater — zíjn de samenvoeglijst. Qwen2.5 heeft er 151 387.
Bij Qwen2.5 is dit 151 387 keer herhaald op een berg tekst in tientallen talen. De volgorde waarin de regels ontstonden wordt bewaard, en die volgorde is straks cruciaal.

Wat eruit komt is een lijst van regels, in de volgorde waarin ze geleerd zijn. Regel 12 is e + n, want dat is een van de allervaakst voorkomende paren. Regel 47 036 is lij + k, veel zeldzamer. Die volgorde heet de rang.

Waarom de volgorde alles bepaalt

Een lage rang betekent: dit paar kwam heel vaak voor, dus voeg het als eerste samen. Bij het toepassen wordt die volgorde exact aangehouden. Dat is de hele truc: de tokenizer voegt niet samen van links naar rechts, maar van vaakst naar zeldzaamst.


Deel 03De lijst toepassen

Nu de andere kant: je hebt de lijst, en er komt tekst binnen. Begin met losse bytes en voeg telkens het paar samen met de laagste rang, tot er niets meer kan.

Hieronder gebeurt dat met het woord " gewichten", stap voor stap, met de echte rangen uit het modelbestand.

Het woord " gewichten", samengevoegd Elke regel is één samenvoeging. Let op de volgorde van de rangen: niet van links naar rechts, maar van laag naar hoog. losse bytes Ġ g e w i c h t e n rang 12 samenvoeging 1 Ġ g e w i c h t en rang 36 samenvoeging 2 Ġ g e w ic h t en rang 86 samenvoeging 3 Ġg e w ic h t en rang 109 samenvoeging 4 Ġg ew ic h t en rang 170 samenvoeging 5 Ġg ew ic ht en rang 4609 samenvoeging 6 Ġg ew icht en rang 22283 samenvoeging 7 Ġgew icht en rang 28442 samenvoeging 8 Ġgew ichten klaar Resultaat: 2 tokens. Het woord bestond uit 10 bytes; er waren 8 samenvoegingen nodig.
Elke regel is één samenvoeging. De gekleurde doos is de nieuwe.

Kijk naar de rangen: 12, 36, 86, 109, 170, 4609, 22283, 28442. Ze lopen op. Kijk dan naar wáár in het woord de samenvoegingen gebeuren: eerst achteraan (en), dan in het midden (ic), dan vooraan (Ġg), dan weer in het midden. Het springt heen en weer.

Dat is het gedrag dat de meeste mensen verrast. Een tokenizer werkt niet als een lezer die van links naar rechts gaat. Hij werkt als iemand die eerst alle veelvoorkomende lettergrepen aan elkaar plakt, en pas daarna de zeldzamere combinaties.

Bij " tokenizer" zie je hetzelfde, maar dan tot het einde doorgezet:

rang 3       Ġ + t        -> Ġt
rang 5       e + r        -> er
rang 12      e + n        -> en
rang 55      Ġt + o       -> Ġto
rang 193     i + z        -> iz
rang 2456    k + en       -> ken
rang 2879    iz + er      -> izer
rang 3694    Ġto + ken    -> Ġtoken
rang 45702   Ġtoken + izer -> Ġtokenizer          één enkel token

Negen samenvoegingen om van tien losse bytes naar één token te komen. Het woord "tokenizer" kwam blijkbaar vaak genoeg voor in de trainingstekst om een eigen plek in de lijst te verdienen.

Waarom dit efficiënt kan

Naïef uitgevoerd zou je bij elke stap de hele reeks moeten aflopen op zoek naar het beste paar. Bij een lange reeks — denk aan tweehonderd spaties achter elkaar — wordt dat kwadratisch en loopt het vast. De implementatie in dit project gebruikt daarom een dubbelgeschakelde lijst plus een prioriteitswachtrij, waarmee de kost logaritmisch blijft.


Deel 04De byte-truc, en dat rare teken Ġ

Tot hier deed ik alsof tekst uit letters bestaat. Dat is ze niet. In een computer is tekst een reeks bytes, en dat verschil is precies wat BPE zo robuust maakt.

Het probleem met werken op letters: er zijn er te veel. Unicode heeft meer dan 150 000 tekens. Zou je die allemaal als startpunt nemen, dan is je woordenschat op voorhand al vol.

De oplossing is om onder het niveau van letters te duiken. Er zijn maar 256 mogelijke bytes. Neem die als startpunt, en elke denkbare tekst — in elke taal, met elk emoji, zelfs een stuk van een fotobestand — kan gecodeerd worden. Er bestaat geen "onbekend woord" meer.

Het woord " café" op weg naar één token wat je typt · c a f é 5 tekens UTF-8 als bytes 20 63 61 66 C3 A9 6 bytes — de é kost er twee, de spatie telt gewoon mee byte-tabel van GPT-2 als zichtbare tekens Ġ c a f à © elke byte krijgt een eigen zichtbaar teken, zodat de spatie niet meer wegvalt 5 samenvoegingen als token Ġcafé één token, nummer 51950 De é ziet er in het midden vreemd uit — é — maar dat is geen fout. Het zijn de twee bytes van de é, elk met hun eigen teken. Bij het terugvertalen komt er weer keurig é uit.
De byte-tabel komt uit GPT-2 en wordt sindsdien door bijna elk model hergebruikt. Ze is willekeurig maar vast: byte 0x20 (spatie) wordt altijd het teken Ġ.

Waarom een spatie een eigen teken nodig heeft

Er zijn 94 bytes die overeenkomen met gewone zichtbare tekens: de letters, cijfers en leestekens. Die blijven zichzelf. Alle andere — spatie, tab, regeleinde, en alles boven de 127 — zouden onzichtbaar zijn of samenvallen met witruimte. Daar zou je bij het opsplitsen last van krijgen.

Daarom krijgt elke van die bytes een eigen zichtbaar plaatsvervanger, ergens in een ongebruikt hoekje van Unicode. De spatie wordt Ġ, het regeleinde wordt Ċ, de tab wordt ĉ.

Enkele echte omzettingen. De laatste twee laten zien dat er niets bijzonders aan meerbyte-tekens is: ze worden gewoon een reeks van meerdere plaatsvervangers.
Je typtUnicodeBytesWordt
spatieU+002020Ġ
regeleindeU+000A0AĊ
tabU+000909ĉ
aU+006161a
éU+00E9C3 A9é
—U+2014E2 80 94âĢĶ
🎉U+1F389F0 9F 8E 89ðŁİī

De spatie hoort bij het volgende woord

Kijk goed naar de tokens: het is Ġthe, niet the. De spatie zit in het token, aan het begin. Dat heeft een merkwaardig gevolg: "strawberry" aan het begin van een zin en " strawberry" middenin een zin zijn voor het model twee verschillende dingen.

Met spatie is het één token. Zonder spatie zijn het er drie: strawberry. Dat is geen curiositeit — het verklaart waarom een prompt die met een spatie eindigt soms slechtere antwoorden geeft.


Deel 05Eerst knippen, dan pas samenvoegen

Er ontbreekt nog een stap. Als je BPE zomaar op een hele zin loslaat, gaat het mis.

Stel dat "hond." vaak voorkomt aan het einde van zinnen. Dan zou BPE daar één token van maken, en zou "hond" mét punt iets anders zijn dan "hond" zonder. Dat wil je niet. Een punt hoort geen deel te zijn van een woord.

Daarom wordt de tekst eerst in brokken geknipt, met een reguliere uitdrukking, en gebeurt het samenvoegen alleen binnen een brok. Over de grens heen wordt nooit samengevoegd.

Voorsplitsen: de zin in brokken knippen vóór er iets samengevoegd wordt In 1815 kostte dat 42,50 fr. — écht waar! 20 brokken — elk brok wordt apart samengevoegd, nooit over de grens heen In · 1 8 1 5 ·kostte ·dat · 4 2 , 5 0 ·fr . ·— ·écht ·waar ! De middelste punt staat voor een spatie: die hoort bíj het volgende woord, niet ertussen. Merk op dat 1815 in vier losse cijfers uiteenvalt, en 42,50 in vijf brokken. Dat is een keuze van Qwen.
De brokken voor Qwen2.5. De regels: letters bij letters, cijfers apart, leestekens bij leestekens, en witruimte hoort bij wat erna komt.

De uitdrukking die dat doet ziet er afschrikwekkend uit, maar bestaat uit zes losse regels achter elkaar:

// 1. Engelse samentrekkingen apart houden
(?:'[sS]|'[tT]|'[rR][eE]|'[vV][eE]|'[mM]|'[lL][lL]|'[dD])
// 2. een reeks letters, eventueel met één teken ervoor (meestal een spatie)
|[^\r\n\p{L}\p{N}]?\p{L}+
// 3. één cijfer
|\p{N}
// 4. leestekens, eventueel met een spatie ervoor
| ?[^\s\p{L}\p{N}]+[\r\n]*
// 5, 6. regeleindes en overige witruimte
|\s*[\r\n]+|\s+(?!\S)|\s+

Regel 3 is de interessante. Er staat \p{N} — één cijfer. Niet \p{N}+. Daardoor valt elk getal uiteen in losse cijfers.

Echte metingen. Een jaartal kost vijf tokens: één voor de spatie en één per cijfer.
TekstTokensOpdeling
20265Ġ2026
18155Ġ1815
12345678910Ġ123456789

Dat lijkt verspilling, maar het is een bewuste keuze. Als "2024" één token zou zijn en "2025" een ander, dan moet het model voor elk getal apart leren hoe groot het is. Met losse cijfers ziet het model de opbouw, en kan het leren rekenen op een manier die overdraagbaar is. Modellen die getallen in brokken van drie knippen — zoals Llama 3 — maken hier een andere afweging.

Waar dit stil misgaat

Het verschil tussen de tokenizer van Qwen en die van Llama 3 is precies één teken: \p{N} tegenover \p{N}{1,3}. Gebruik je per ongeluk de verkeerde, dan werkt alles nog. De tekst gaat er heen en weer doorheen zonder een letter te verliezen, er komt geen enkele foutmelding.

Maar het model krijgt andere nummers dan het geleerd heeft, en antwoordt navenant — vloeiend, grammaticaal correct, en volslagen naast de kwestie. Dit is een van de vervelendste fouten in dit hele vakgebied, precies omdat er niets kapotgaat.


Deel 06Tokens die geen tekst zijn

Achteraan de woordenschat, vanaf nummer 151 643, staan 22 tokens die nooit uit tekst kunnen ontstaan.

Dat zijn de speciale tokens: markeringen waarmee het model weet waar een gespreksbeurt begint, wie er aan het woord is, en waar het moet stoppen. Ze worden herkend vóór de voorsplitser eraan te pas komt, en gaan nooit door BPE.

De 22 speciale tokens van Qwen2.5-0.5B. De vision- en video-tokens zijn er voor de grotere broertjes van dit model die ook beelden aankunnen; hier zijn ze ongebruikt.
NummerTokenWaarvoor
151643<|endoftext|>einde van een document
151644<|im_start|>begin van een gespreksbeurt
151645<|im_end|>einde van een gespreksbeurt — hier stopt het model
151646–51<|object_ref_…|> <|box_…|> <|quad_…|>verwijzen naar dingen in een beeld
151652–56<|vision_…|> <|image_pad|> <|video_pad|>plaats waar beeldgegevens komen
151657–58<tool_call> </tool_call>het model roept een extern hulpmiddel aan
151659–64<|fim_…|> <|repo_name|> <|file_sep|>code aanvullen middenin een bestand

Een gesprek ziet er voor het model zo uit — en dit is letterlijk wat er in de reeks tokens staat:

<|im_start|>system
Je bent een behulpzame assistent.<|im_end|>
<|im_start|>user
Hoeveel is 2 + 2?<|im_end|>
<|im_start|>assistant

Het model vult daarna aan, en stopt zodra het zelf <|im_end|> produceert. Er is niets magisch aan een "systeemopdracht" — het is gewoon tekst tussen twee markeringen, op een plek waar het model geleerd heeft dat daar instructies staan.

Een reden om voorzichtig te zijn

Wat gebeurt er als een gebruiker letterlijk <|im_end|> intypt? Dan hangt het ervan af of de tokenizer speciale tokens herkent in gebruikersinvoer. Doet ze dat, dan kan iemand het gesprek van binnenuit herschrijven en zich voordoen als het systeem.

Daarom heeft de tokenizer in dit project een schakelaar: encode(tekst, false) behandelt zulke reeksen als gewone tekst en knipt ze in zes stukjes. Voor invoer van buiten is dat de juiste stand.


Deel 07En weer terug

Decoderen is bijna te makkelijk om een eigen hoofdstuk te verdienen: zoek elk nummer op, plak de stukjes aan elkaar, zet de plaatsvervangende tekens terug om in bytes, en lees die als UTF-8.

Bijna. Er is één addertje, en dat komt pas boven bij het genereren.

Een token is een reeks bytes, en die reeks hoeft geen heel teken te bevatten. De é bestaat uit twee bytes; er is niets dat verbiedt dat de ene byte in het ene token zit en de andere in het volgende. Wie tijdens het genereren elk token meteen wil tonen, krijgt dan een half teken te zien — het bekende vraagteken-in-een-ruitje.

De oplossing is de bytes te bufferen en pas te tonen wanneer ze samen een geldig teken vormen. In dit project levert tokenBytes(id) daarvoor de ruwe bytes; het bufferen zelf komt in hoofdstuk 15 aan bod — waar precies deze valkuil nog een hoofdrol blijkt te spelen.


Deel 08Wat dit in de praktijk betekent

Vier gevolgen die je merkt zonder ooit een regel code te zien.

1. Nederlands is duurder dan Engels

Dezelfde zin, vertaald, en dan geteld:

Wat dezelfde zin kost, per taal "De kat zat op de mat en keek naar buiten naar de vallende regen." — vertaald, en dan geteld. Korter is goedkoper: minder tokens betekent minder rekenwerk en meer ruimte in het contextvenster. tokens bytes/token Chinees 11 4.1 Engels 14 4.4 Nederlands 20 3.2 Duits 23 3.4 Frans 24 2.9 Spaans 25 2.7 Russisch 27 3.9 Nederlands kost 43 % meer tokens dan Engels voor exact dezelfde inhoud — de woordenschat is nu eenmaal niet op Nederlands gebouwd. Chinees wint, en dat is geen meetfout: één Chinees teken is een heel woord. Per byte gemeten liggen alle talen dicht bij elkaar (rechterkolom).
Gemeten op de zin "De kat zat op de mat en keek naar buiten naar de vallende regen." en zijn vertalingen.

Nederlands kost 43 % meer tokens dan Engels voor exact dezelfde inhoud. Dat betekent: 43 % meer rekentijd, 43 % meer van je contextvenster, en bij betaalde modellen 43 % meer geld. De reden is simpel — er stond nu eenmaal veel meer Engels dan Nederlands in de trainingstekst, dus zijn er meer Engelse brokken in de lijst terechtgekomen.

Je ziet het terug in losse woorden:

Gemeten op Qwen2.5-0.5B. Het Engelse woord is telkens één token; het Nederlandse zelden.
EngelsTokensNederlandsTokens
the1 de1
cat1 kat1
hospital1 ziekenhuis4
strawberry1 aardbei3
—— onwaarschijnlijk6
—— meervoudigepersoonlijkheidsstoornis13

2. Modellen kunnen de letters van een woord niet zien

Vraag een taalmodel hoeveel keer de letter r in "strawberry" voorkomt, en het antwoordt vaak fout. Dat is geen gebrek aan intelligentie maar een gevolg van de tokenizer.

Voor het model is " strawberry" één ondeelbaar nummer: 72600. Het ziet geen s, geen t, geen r. Het heeft ooit geleerd dat dat nummer met aardbeien te maken heeft, maar de spelling zit er niet in — net zomin als jij aan het cijfer 7 kan zien hoeveel streepjes er in het woord "zeven" zitten.

Aan het begin van een zin, zonder spatie ervoor, valt het woord toevallig wél uiteen in strawberry — en dan gaat het soms opeens wel goed. Dat verklaart waarom hetzelfde model dezelfde vraag de ene keer juist en de andere keer fout beantwoordt.

3. Getallen zijn duur

Een jaartal kost vijf tokens, een rekeningnummer dertig. Wie tabellen met cijfers aan een model voert, betaalt daar stevig voor. Een tabel van duizend getallen kan makkelijk vijfduizend tokens kosten — meer dan tien bladzijden gewone tekst.

4. Structuur kost meer dan proza

JSON is voor een tokenizer heel duur: alle accolades, aanhalingstekens en dubbele punten zijn losse tokens. Dezelfde gegevens in gewone zinnen zetten scheelt vaak de helft. Bij dit model kostte een klein JSON-fragment 31 tokens waar de Engelse zin er 14 nodig had, terwijl beide ongeveer even lang waren.


Deel 09De woordenschat in cijfers

Wie de nummers naast de samenvoeglijst legt, ziet iets moois: het zijn niet twee lijsten maar één.

De eerste samenvoegregel is Ġ + Ġ, en het resultaat ĠĠ is token nummer 256. De tweede regel geeft token 257. En zo verder, zonder één uitzondering: voor alle 151 387 regels geldt dat het tokennummer precies de samenvoegrang plus 256 is.

De opbouw van de woordenschat. Gecontroleerd voor elke regel afzonderlijk; nul afwijkingen.
NummersHoeveelWat
0 – 255256de losse bytes, het vangnet waarmee elke tekst codeerbaar is
256 – 151.642151.387de resultaten van de samenvoegregels, in de volgorde waarin ze geleerd zijn
151.643 – 151.66422de speciale tokens
151.665 – 151.935271ongebruikt, opgevuld tot een rond getal

De woordenschat is dus de samenvoeglijst, met de 256 bytes ervoor geplakt. Dat is geen ontwerpkeuze die iemand achteraf gemaakt heeft — het is gewoon wat er uit het leerproces rolt: eerst de bouwstenen, dan alles wat je daaruit hebt samengesteld, in de volgorde van samenstellen.

Geteld over de volledige woordenschat van Qwen2.5-0.5B.
WatAantalWat het zegt
tokens in totaal151.936waarvan 22 speciale
begint met een spatie53.021ruim een derde — de spatie hoort bij het woord
zuiver ASCII94.62962 %, de rest is Chinees, cyrillisch, emoji, accenten
één byte lang256precies alle mogelijke bytes — de vangnetlaag
bestaat alleen uit cijfers10alleen 0 tot en met 9, en geen enkel getal meer
op zich geen geldige tekst1.448halve tekencodes, alleen bruikbaar in combinatie

De lengteverdeling laat zien waar het zwaartepunt ligt. De meeste tokens zijn drie of zes bytes lang — een lettergreep of een kort woord:

 1 byte      256  ▏
 2         5.073  ████████
 3        25.206  ███████████████████████████████████████████
 4        18.359  ███████████████████████████████
 5        15.992  ███████████████████████████
 6        28.818  ██████████████████████████████████████████████████
 7        13.592  ███████████████████████
 8        10.801  ██████████████████
 9        12.688  ██████████████████████
10         5.848  ██████████
11         4.547  ███████
12         4.814  ████████
13         1.786  ███

De piek bij zes bytes is verraderlijk: dat zijn grotendeels Chinese woorden van twee tekens, want elk Chinees teken kost drie bytes in UTF-8.


Deel 10Zelf kijken

Alles in dit hoofdstuk is afgelezen uit een modelbestand dat waarschijnlijk al op je schijf staat.

De volledige broncode zit bij dit boek in de map code/ — hieronder ook als zip — en draait met één JDK (22 of nieuwer). Voer de opdrachten uit vanuit die map. $MODEL staat voor het pad naar het GGUF-bestand — bij ollama de blob die je in hoofdstuk 12 via het manifest vond.

# hoe valt een zin uiteen
java src/Tok.java $MODEL "In 1815 kostte dat 42,50 fr."

# alleen het aantal
java src/Tok.java $MODEL --count "hoeveel tokens is dit?"

# toon ook de voorsplitsing, vóór het samenvoegen
java src/Tok.java $MODEL --chunks "In 1815 kostte dat 42,50 fr."

# en terug
java src/Tok.java $MODEL --decode 641 220 16 23 16 20

De uitvoer toont per token het nummer, de byte-level notatie met haar Ġ's, en hoe het stukje er als gewone tekst uitziet. Onderaan staat of de tekst ongeschonden terugkomt.

Een aardig experiment

Tokeniseer eens dezelfde zin met en zonder spatie ervoor, of met een hoofdletter in plaats van een kleine letter. De opdeling verandert vaker dan je zou verwachten — en daarmee ook wat het model precies te zien krijgt.


SamenvattingHet hele verhaal in acht zinnen

  1. Een taalmodel rekent met getallen, dus tekst moet eerst in genummerde stukjes worden opgedeeld.
  2. Per letter is te duur, per woord is onmogelijk; BPE kiest ertussenin door de stukjes uit de tekst zelf te leren.
  3. Leren gebeurt door telkens het vaakst voorkomende naburige paar samen te voegen en die regel te noteren — 151 387 keer.
  4. Toepassen gebeurt in diezelfde volgorde: van vaakst naar zeldzaamst, niet van links naar rechts.
  5. Er wordt niet met letters gewerkt maar met bytes, en elke byte krijgt een zichtbaar plaatsvervangend teken; daarom is er nooit een onbekend woord.
  6. Vóór het samenvoegen wordt de tekst in brokken geknipt, zodat leestekens niet aan woorden vastgroeien en cijfers los blijven.
  7. Tweeëntwintig tokens zijn geen tekst maar markeringen, voor gespreksbeurten en hulpmiddelen.
  8. De gevolgen zijn merkbaar: Nederlands kost 43 % meer dan Engels, getallen zijn duur, en het model ziet de letters van een woord niet.

Alle opdelingen, rangen, tellingen en percentages in dit hoofdstuk zijn afgelezen uit Qwen2.5-0.5B-Instruct zoals ollama het op schijf bewaart, met de tokenizer uit dit project. Ze zijn niet nagemaakt of vereenvoudigd. De tokenizer is getoetst tegen llama.cpp: 201 van 201 teksten geven hetzelfde aantal tokens en 220 van 220 tokengrenzen liggen op dezelfde plaats.

Het voorbeeld in deel 2 — kat, kater, katten — is wel verzonnen, om de telling met de hand te kunnen volgen.

Lettertypen: IBM Plex Sans en IBM Plex Serif, SIL Open Font License 1.1, ingesloten in de gedeelde stylesheet van dit boek.