Hoppa till innehÄllet
7/30Kapitel 7 av 30

Bygg en BPE Tokenizer: DÀrför kan din modell inte rÀkna r:en

TrĂ€na en byte-pair encoder pĂ„ 60 rader, se den upptĂ€cka ordet ”the” sjĂ€lv och mĂ€t varför ett stycke kostar 39 % mer pĂ„ spanska.

PÄ den hÀr sidan

FrÄga en modell som kan klara en advokatexamen hur mÄnga bokstaven r det finns i strawberry, och chansen Àr rÀtt god att den svarar tvÄ.

Den vanliga förklaringen Ă€r att sprĂ„kmodeller Ă€r ”dĂ„liga pĂ„ att rĂ€kna” eller ”inte riktigt förstĂ„r”. BĂ„da Ă€r omöjliga att motbevisa, och ingen av dem Ă€r orsaken. Orsaken Ă€r mekanisk, den uppstĂ„r innan modellen körs, och du kan se den pĂ„ en rad:

TEXT
'strawberry'  ->  3 tokens  [496, 675, 15717]  ['str', 'aw', 'berry']

Modellen tittar inte pĂ„ tio bokstĂ€ver. Den tittar pĂ„ tre tal. För att rĂ€kna r:en skulle den behöva veta, enbart frĂ„n identiteten hos token 496, hur mĂ„nga r som finns inuti en strĂ€ng den inte kan se — och sedan göra samma sak för 675 och 15717 och addera dem. Den fĂ„r en frĂ„ga om en representation som den inte har tillgĂ„ng till.

Det hÀr kapitlet bygger det som producerar de tre talen. Det tar ungefÀr sextio rader, det Àr samma algoritm som alla stora modeller anvÀnder, och nÀr du vÀl har skrivit den faller ett dussin till synes orelaterade egenheter tillbaka pÄ en enda orsak.

Varför inte bokstÀver, och varför inte ord

LÀnk till avsnittet: Varför inte bokstÀver, och varför inte ord

Det finns tvÄ uppenbara sÀtt att mata text till ett nÀtverk, och bÄda misslyckas av skÀl som Àr vÀrda att förstÄ, eftersom misslyckandet formar lösningen.

Ord. Dela pĂ„ mellanslag, ge varje ord ett tal. Engelska har hundratusentals ordformer och modellen behöver en embedding-rad för var och en, sĂ„ vokabulĂ€ren — och utmatningslagret, som mĂ„ste producera ett vĂ€rde för varje post — blir enorm. VĂ€rre Ă€r vad som hĂ€nder vid inference: ett ord som modellen aldrig sĂ„g under trĂ€ning har inget tal. Det Ă€r problemet med ord utanför vokabulĂ€ren, och den vanliga nödlösningen Ă€r att mappa allt okĂ€nt till en enda <UNK> token, vilket kastar bort informationen. Dessutom Ă€r ”ord” inte ett vĂ€ldefinierat begrepp: kinesiska och japanska sĂ€tter inte mellanslag mellan ord, och tyska kan sammansĂ€tta substantiv med andra substantiv i all oĂ€ndlighet.

Tecken. Inget problem med ord utanför vokabulĂ€ren, och en vokabulĂ€r pĂ„ drygt hundra symboler. Men sekvenserna blir vĂ€ldigt lĂ„nga, och kapitel 9 kommer att visa att attention-kostnaden vĂ€xer kvadratiskt med sekvenslĂ€ngden. Ett dokument pĂ„ 1000 ord Ă€r omkring 5000 tecken — en sekvens som Ă€r fyra till fem gĂ„nger lĂ€ngre Ă€n den behöver vara, till ett kvadratiskt pris. Och varje tecken bĂ€r nĂ€stan ingen mening pĂ„ egen hand, sĂ„ de första lagren gĂ„r Ă„t till att sĂ€tta ihop ord som tokenizer kunde ha lĂ€mnat över intakta.

Svaret ligger mellan dem: subwords. Vanliga ord blir en token, sÀllsynta ord delas i delar, och inget Àr nÄgonsin okÀnt eftersom delarna i botten Àr enskilda bytes. Det intressanta Àr att ingen designar uppdelningen. Tokenizer trÀnas, pÄ samma typ av data som modellen, och den lÀr sig vilka bytesekvenser som förtjÀnar ett eget tal genom att rÀkna hur ofta de förekommer tillsammans.

Algoritmen Àr frÄn 1994, och den var en komprimeringsalgoritm. Philip Gage publicerade den i C Users Journal som ett sÀtt att krympa filer genom att upprepade gÄnger ersÀtta det vanligaste paret av intilliggande bytes med en byte som inte förekommer i datan.1 DÀr lÄg den i tjugotvÄ Är, tills Sennrich, Haddow och Birch 2016 anvÀnde om den för maskinöversÀttning för att lösa problemet med ord utanför vokabulÀren.2 I dag Àr det i praktiken sÄ varje stor sprÄkmodell lÀser.

TrÀningsloopen Àr fyra steg som upprepas:

Koda trĂ€ningstexten som UTF-8. Varje bytevĂ€rde 0–255 Ă€r en token. VokabulĂ€rstorlek: 256.

GÄ igenom sekvensen och rÀkna hur ofta varje par av angrÀnsande tokens förekommer.

Ta vinnaren, skapa ett nytt token id för det och ersÀtt varje förekomst i sekvensen. VokabulÀren vÀxer med ett; sekvensen blir kortare.

Lagra paret och det id det blev, i ordning. Den ordnade listan Ă€r tokenizer — den Ă€r allt som behövs för att koda ny text senare.

HÀr Àr hela trÀnaren:

bpe.pyPYTHON
def get_stats(ids):
    counts = {}
    for a, b in zip(ids, ids[1:]):
        counts[(a, b)] = counts.get((a, b), 0) + 1
    return counts


def merge(ids, pair, idx):
    out, i = [], 0
    while i < len(ids):
        if i < len(ids) - 1 and ids[i] == pair[0] and ids[i + 1] == pair[1]:
            out.append(idx)
            i += 2
        else:
            out.append(ids[i])
            i += 1
    return out


class BPE:
    def __init__(self):
        self.merges = {}
        self.vocab = {i: bytes([i]) for i in range(256)}

    def train(self, text, vocab_size):
        ids = list(text.encode("utf-8"))
        for i in range(vocab_size - 256):
            stats = get_stats(ids)
            if not stats:
                break
            pair = max(stats, key=stats.get)      
            idx = 256 + i                         
            ids = merge(ids, pair, idx)           
            self.merges[pair] = idx               
            self.vocab[idx] = self.vocab[pair[0]] + self.vocab[pair[1]]
        return ids

Kör den pÄ 151 191 bytes engelsk prosa och skriv ut de första tolv sammanslagningarna nÀr de sker. Det hÀr Àr delen som Àr vÀrd att lÀsa lÄngsamt, eftersom ingen berÀttade nÄgot om engelska för algoritmen:

TEXT
merge   1: b'e' + b' '   -> b'e '     (occurred 4433 times)
merge   2: b' ' + b't'   -> b' t'     (occurred 3302 times)
merge   3: b'\xe2' + b'\x80' -> b'\xe2\x80'  (occurred 3247 times)
merge   4: b' ' + b'a'   -> b' a'     (occurred 2335 times)
merge   5: b' t' + b'h'  -> b' th'    (occurred 2253 times)
merge   6: b'i' + b'n'   -> b'in'     (occurred 2011 times)
merge   7: b't' + b' '   -> b't '     (occurred 1904 times)
merge   8: b'e' + b'r'   -> b'er'     (occurred 1813 times)
merge   9: b'd' + b' '   -> b'd '     (occurred 1703 times)
merge  10: b'o' + b'u'   -> b'ou'     (occurred 1554 times)
merge  11: b' ' + b's'   -> b' s'     (occurred 1467 times)
merge  12: b' th' + b'e '-> b' the '  (occurred 1270 times)

Tre saker i den listan Àr vÀrda att peka pÄ.

Sammanslagning 12 Ă€r ordet ”the” — med mellanslaget före och mellanslaget efter, som en enda enhet, upptĂ€ckt pĂ„ tolfte iterationen av en loop som rĂ€knar par. Ingen levererade en ordbok. Den finns dĂ€r eftersom de fem bytesen förekommer tillsammans oftare Ă€n nĂ„gra andra fem i engelska.

Sammanslagning 3 Ă€r inte text alls. \xe2\x80 Ă€r de tvĂ„ första bytesen i UTF-8-kodningen av typografisk interpunktion — tankstrecket, de böjda citattecknen. Algoritmen har ingen aning om att UTF-8 finns, och den har precis Ă„terupptĂ€ckt en del av dess struktur, eftersom flerbyteskodningar per konstruktion Ă€r sekvenser av bytes som alltid upptrĂ€der tillsammans.

De flesta tidiga sammanslagningar innehÄller ett mellanslag, och mellanslaget ligger oftast till vÀnster. Det Àr ursprunget till ett av de mest förvirrande beteendena i praktiken, som vi Äterkommer till strax.

Varje sammanslagning gör sekvensen kortare och vokabulĂ€ren större. Hur lĂ„ngt man ska driva det Ă€r ett verkligt beslut, och det gĂ„r att mĂ€ta — hĂ€r pĂ„ samma 151 191 bytes:

vokabulÀrstorlekresulterande tokenskomprimering (bytes per token)
300101 0651,50
51268 2492,22
1 02450 3693,00
2 04839 3063,85
4 09630 7574,92

Avtagande avkastning, tydligt. En fördubbling frĂ„n 512 till 1024 köper 0,78 bytes per token; en fördubbling frĂ„n 2048 till 4096 köper 1,07 — bĂ€ttre hĂ€r bara för att korpusen Ă€r liten nog för att lĂ€ngre sammanslagningar fortfarande ska löna sig. PĂ„ en verklig korpus planar kurvan ut kraftigt.

Och kostnaden för en större vokabulĂ€r Ă€r inte bara minne. Varje token behöver en embedding-rad, och — dyrare — modellens utmatningslager mĂ„ste producera ett vĂ€rde för varje post i vokabulĂ€ren vid varje steg, sĂ„ den sista matrismultiplikationen skalar med vokabulĂ€rstorleken. Verkliga modeller ligger mellan 32 000 och 200 000: GPT-2 anvĂ€nde 50 257, GPT-4:s cl100k anvĂ€nder 100 277, GPT-4o:s o200k ungefĂ€r fördubblar det. Trenden gĂ„r uppĂ„t, och skĂ€let finns i nĂ€sta avsnitt.

HÀr Àr samma stycke, översatt, mÀtt med de verkliga tokenizers som OpenAI levererar:

sprÄkteckentokens (cl100k)tokens (o200k)tokens/teckenpÄslag jÀmfört med engelska
Engelska16431310,189—
Spanska16943360,254+39 %
Ryska17878430,438+152 %
Japanska7279581,097+155 %

Samma innehĂ„ll, samma betydelse, och med cl100k förbrukar den ryska versionen tvĂ„ och en halv gĂ„nger sĂ„ mĂ„nga tokens. Eftersom API:er fakturerar per token och context windows mĂ€ts i tokens Ă€r det inte en lingvistisk kuriositet — det Ă€r en rad i en budget, en kortare effektiv context window och ett lĂ„ngsammare svar, alla tre pĂ„ en gĂ„ng, för alla som inte arbetar pĂ„ engelska.

Mekanismen Ă€r trĂ€ningsdatan. En tokenizer som frĂ€mst trĂ€nats pĂ„ engelska lĂ€gger sin sammanslagningsbudget pĂ„ engelska bytesekvenser. Spanska delar det latinska alfabetet och fĂ„r dĂ€rför fortfarande viss nytta; ryska fĂ„r nĂ€stan ingen, eftersom kyrilliska tecken tar tvĂ„ bytes i UTF-8 och fĂ„ av de paren var tillrĂ€ckligt vanliga i trĂ€ningskorpusen för att förtjĂ€na en sammanslagning. Japanska Ă€r Ă€nnu vĂ€rre: tre bytes per tecken, och 72 tecken blir 79 tokens — fler tokens Ă€n tecken.

Kolumnen o200k visar att det hÀr Àr ett lösbart problem och att det hÄller pÄ att lösas. Att fördubbla vokabulÀren och balansera om trÀningsdatan sÀnker pÄslaget för spanska frÄn +39 % till +16 %, och för ryska frÄn +152 % till +39 %. Det Àr den verkliga anledningen till att vokabulÀrerna fortsÀtter att vÀxa: inte komprimering för sin egen skull, utan att den föregÄende generationen i tysthet tog extra betalt av en stor del av vÀrlden.

Kodning, och varför sammanslagningarnas ordning spelar roll

LÀnk till avsnittet: Kodning, och varför sammanslagningarnas ordning spelar roll

TrĂ€ningen producerade en ordnad lista med sammanslagningar. Kodning av ny text spelar upp den igen — och den mĂ„ste spela upp den i samma ordning, eftersom sammanslagning 12 kombinerar resultaten av sammanslagning 5 och 1. TillĂ€mpa dem i en annan ordning och du fĂ„r en annan, felaktig tokenisering som inte matchar nĂ„got modellen sĂ„g under trĂ€ningen.

bpe.py (continued)PYTHON
    def encode(self, text):
        ids = list(text.encode("utf-8"))
        while len(ids) >= 2:
            stats = get_stats(ids)
            # the pair whose merge came FIRST during training wins   
            pair = min(stats, key=lambda p: self.merges.get(p, float("inf")))   
            if pair not in self.merges:
                break
            ids = merge(ids, pair, self.merges[pair])
        return ids

    def decode(self, ids):
        return b"".join(self.vocab[i] for i in ids).decode("utf-8", errors="replace")

Avkodning Ă€r trivialt i jĂ€mförelse: slĂ„ upp varje id:s bytes, konkatenera och avkoda som UTF-8. Notera errors="replace": en modell kan mata ut en token-sekvens som slutar mitt i ett tecken, och det Ă€r inte hypotetiskt — det Ă€r vad som hĂ€nder nĂ€r ett streamat svar kapas mitt i en emoji, vilket Ă€r varför streaming-API:er buffrar partiella bytes i stĂ€llet för att avkoda token för token.

Rundresan fungerar pÄ vad som helst, vilket Àr löftet med byte-level BPE:

TEXT
'strawberry'                            -> 6 tokens, decode == original: True
'Alice was beginning to get very tired' -> 14 tokens, decode == original: True
'cafĂ© — naĂŻve — æ—„æœŹèȘž'                   -> 23 tokens, decode == original: True

NÀr mekanismen vÀl Àr tydlig visar sig en uppsÀttning till synes orelaterade klagomÄl vara samma klagomÄl.

Aritmetik. Tal delas inte pÄ nÄgot konsekvent sÀtt:

TEXT
1234     -> 2 tokens  ['123', '4']
12345    -> 2 tokens  ['123', '45']
1000000  -> 3 tokens  ['100', '000', '0']
3.14159  -> 4 tokens  ['3', '.', '141', '59']
2024     -> 2 tokens  ['202', '4']

För att addera 1234 och 12345 mĂ„ste modellen först rĂ€kna ut att ['123','4'] och ['123','45'] Ă€r tal vars siffror linjerar pĂ„ ett visst sĂ€tt — och linjeringen skiljer sig för varje par av tal. Siffrorna i ett tal finns inte pĂ„ samma platser frĂ„n ett tal till nĂ€sta. Vissa nyare tokenizers tvingar siffror att delas i konsekventa grupper om tre just för att ta bort detta hinder, och modeller som trĂ€nats med dem Ă€r mĂ€tbart bĂ€ttre pĂ„ aritmetik.

Python-indrag.

TEXT
'    x = 1'      -> 5 tokens  ['   ', ' x', ' =', ' ', '1']
'        x = 1'  -> 5 tokens  ['       ', ' x', ' =', ' ', '1']
'\tx = 1'        -> 4 tokens  ['\tx', ' =', ' ', '1']

Fyra mellanslag och Ă„tta mellanslag Ă€r olika enskilda tokens, och en tab Ă€r sammansmĂ€lt med tecknet efter den. Indrag, som i Python Ă€r syntax, representeras inkonsekvent — vilket Ă€r en stor del av varför modeller brukade producera Python med subtilt felaktiga indrag, och varför kodfokuserade tokenizers lĂ€gger till explicita tokens för vanliga indrag.

Stavning och baklÀngesvÀndning. Samma orsak som nÀr man rÀknar r:en: att be en modell vÀnda strawberry baklÀnges Àr att be den ordna om bokstÀver inuti tre ogenomskinliga id:n. Modeller gör det genom att ha memorerat stavningar under trÀningen snarare Àn genom att titta, vilket Àr varför de gör det bra för vanliga ord och dÄligt för sÀllsynta.

Glitch tokens. Det mest slĂ„ende fallet Ă€r SolidGoldMagikarp och en uppsĂ€ttning liknande strĂ€ngar som fick GPT-2 och GPT-3 att bete sig bisarrt — vĂ€gra upprepa dem, producera orelaterad output, ibland förolĂ€mpa anvĂ€ndaren. Förklaringen Ă€r vardaglig och följer direkt av att tokenizer trĂ€nas separat frĂ„n modellen: de strĂ€ngarna var vanliga i tokenizerns trĂ€ningskorpus (de var Reddit-anvĂ€ndarnamn), sĂ„ de fick en egen token, men var sĂ€llsynta eller frĂ„nvarande i modellens trĂ€ningskorpus. Resultatet Ă€r en embedding-rad som initialiserades slumpmĂ€ssigt och nĂ€stan aldrig uppdaterades. Modellen har en symbol den i praktiken aldrig har sett, och dess beteende dĂ€r blir vad den slumpmĂ€ssiga initialiseringen rĂ„kade ge.

WordPiece, som anvĂ€nds av BERT, skiljer sig frĂ„n BPE i urvalsregeln: i stĂ€llet för att slĂ„ ihop det mest frekventa paret slĂ„r den ihop det par som ökar sannolikheten för trĂ€ningsdatan mest — vilket normaliserar efter hur vanliga delarna redan Ă€r, sĂ„ ett par av tvĂ„ sĂ€llsynta delar kan slĂ„ ett par av tvĂ„ vanliga.

Unigram, frÄn Kudo, arbetar baklÀnges: börja med en stor kandidatvokabulÀr och ta bort de delar vars borttagning skadar korpusens sannolikhet minst. Den ger ocksÄ en sannolikhet till varje segmentering, vilket gör det möjligt att sampla olika tokeniseringar av samma strÀng som regularisering.

SentencePiece Àr den implementation som de flesta icke-engelska modeller anvÀnder. Dess bidrag Àr att behandla input som en rÄ ström utan nÄgon pre-tokenization alls och koda mellanslaget som ett synligt tecken, vilket betyder att den fungerar likadant för sprÄk som inte separerar ord med mellanslag. Den kan köra antingen BPE eller Unigram under ytan.

En tokenizer Àr ett förlustbringande grÀnssnitt mellan text och tal, och varje mÀrkligt beteende i det hÀr kapitlet Àr grÀnssnittet som lyser igenom. Det Àr vÀrt att vara tydlig med att avvÀgningen Àr avsiktlig: byte-level BPE betyder att ingen input nÄgonsin Àr omöjlig att representera, sekvenserna Àr fyra till fem gÄnger kortare Àn tecken skulle vara, och vanliga ord anlÀnder intakta.

Priset Àr att modellens atomer inte Àr vÄra atomer. Den resonerar om text den inte kan stava, i enheter valda av en frekvensrÀkning över en korpus den inte sÄg, med en kostnad per sprÄk som ingen förhandlade om.

Du har nu en sekvens av heltal. Det Àr input-formatet för allt i resten av del II.

Det du inte har Ă€r nĂ„gon anledning till att ett heltal ska följa ett annat. NĂ€sta kapitel introducerar mĂ„let som varje sprĂ„kmodell trĂ€nas pĂ„, och det Ă€r förbluffande enkelt: givet hittillsvarande tokens, förutsĂ€g nĂ€sta. Det enda mĂ„let — inga etiketter, ingen annotering, bara text med sin egen framtid som target — Ă€r vad som gör hela internet till trĂ€ningsdata, och det Ă€r dĂ€r modellens första verkliga representationer kommer ifrĂ„n.

Det krÀver ocksÄ att kedjeregeln för sannolikhet frÄn kapitel 2 blir exakt rÀtt, eftersom pÄstÄendet att förutsÀga en token i taget Àr samma sak som att modellera hela dokument Àr en faktorisering, inte en metafor.

Kapitel 8 handlar om det autoregressiva mÄlet, embeddings och den första plats dÀr en modell lÀr sig nÄgot som ingen lade dit.


Kudo, T. Subword Regularization: Improving Neural Network Translation Models with Multiple Subword Candidates (arXiv:1804.10959) introducerar Unigram-modellen; Kudo och Richardson, SentencePiece: A simple and language independent subword tokenizer and detokenizer for Neural Text Processing (arXiv:1808.06226) Àr den implementation som de flesta flersprÄkiga modeller anvÀnder; Schuster och Nakajima, Japanese and Korean Voice Search (ICASSP 2012) Àr ursprunget till WordPiece. Andrej Karpathys Let's build the GPT Tokenizer och det tillhörande karpathy/minbpe-repositoryt Àr de direkta föregÄngarna till koden i det hÀr kapitlet och gÄr betydligt lÀngre, inklusive GPT-4-regexen och hantering av special-token. Kapitel 6 i Hugging Face LLM Course tÀcker de tre algoritmerna sida vid sida med genomarbetade exempel.

  1. Gage, P. A New Algorithm for Data Compression. The C Users Journal 12(2), s. 23–38 (1994). Byte-pair encoding som komprimeringsmetod, tjugotvĂ„ Ă„r innan nĂ„gon anvĂ€nde den för sprĂ„kmodeller. ↩

  2. Sennrich, R., Haddow, B. och Birch, A. Neural Machine Translation of Rare Words with Subword Units. arXiv:1508.07909 (2015; ACL 2016). Artikeln som tog BPE till NLP, motiverad av ord utanför vokabulĂ€ren i översĂ€ttning. ↩

  3. Radford, A., Wu, J., Child, Luan, D., Amodei, D. och Sutskever, I. Language Models are Unsupervised Multitask Learners (2019). Avsnitt 2.2 introducerar byte-level BPE med den pre-tokenization-regex som diskuteras ovan. ↩


Skapad av

David Vicente Campos

Grundare av NeuraLIA Labs och medgrundare av MyRealFood

Jag Àr dataingenjör frÄn Universitetet i León. Jag var med och grundade MyRealFood, dÀr jag som CTO byggde appen som miljontals mÀnniskor har anvÀnt för att Àta bÀttre, och jag grundade NeuraLIA Labs, dÀr jag bygger AI-produkter. HÀr skriver jag om det jag har behövt förstÄ lÀngs vÀgen, sÄ som jag önskar att nÄgon hade förklarat det för mig.

Mer om författaren

Publicerad av NeuraLIA Labs.

FÄ nya inlÀgg i din inkorg

AI-nyheter, guider och produktuppdateringar — ett kort mejl nĂ€r vi publicerar nĂ„got som Ă€r vĂ€rt din tid.

Kursindex

Abstract software decision engine with branching paths, probability nodes, and glowing gates.
jevLĂ€stid 11 min

Jevs AI-modell Àr byggd för beslut, inte prosa

TypeSafe AI:s Jev vÀcker uppmÀrksamhet eftersom den behandlar mjukvaruintelligens som ett sannolikhetsproblem: vÀlj rÀtt gren, lÀgg till konfidens och undvik att betala en LLM för att skriva text nÀr koden behöver ett beslut.

Abstract agent runtime sorting documents, memory blocks and pointer nodes inside a bounded context frame.
context-engineeringLĂ€stid 11 min

Kontextteknik för AI-agenter med lÄng horisont

LÄngkörande agenter misslyckas inte bara för att fönstret Àr litet. De misslyckas nÀr filer, verktygsutdata och gammal historik trÀnger undan uppgiften agenten skulle slutföra.

Redo att lÄta LIA vÀlja Ät dig?

Bygg med alla AI-modeller pĂ„ ett stĂ€lle – kom igĂ„ng gratis i dag.