Werkplaats / De werkplaats
In jouw eigen tempo
← Taalmodellen
MODULE 01 · LES 02

Leer herhaalde byteparen samenvoegen

Bouw paartellingen en niet-overlappende merges en train een reproduceerbare byte-BPE op afzonderlijke documenten.

60 min3 checkpoints
DIT GA JE ONTDEKKEN
  • Je telt paren per document en voert merges zonder overlap uit.
  • Je leert een vaste rangorde van merges met een reproduceerbare tie-break.

In tekst komen kleine stukjes vaak terug. Je kunt “ab” telkens met twee byte-ID's bewaren, of een nieuw ID afspreken voor dat terugkerende paar. Byte pair encoding (BPE) leert zulke afspraken stap voor stap uit een trainingscorpus.

Zie een merge als twee aangrenzende kaartjes vervangen door één nieuw kaartje. De oorspronkelijke 256 bytekaartjes blijven beschikbaar. Het subword-paper werkt met tekens; deze onderwijsvariant begint met bytes en gebruikt geen speciale tokens of regex-voorsegmentatie.

1. Tel posities, zonder documenten aan elkaar te plakken

Een paar bestaat uit twee aangrenzende token-ID's in hetzelfde document. In aaaaa staan vier aa-posities: tussen positie0–1,1–2,2–3 en3–4. Die telling mag overlappen; zij zegt nog niet hoeveel vervangingen één merge kan uitvoeren.

def pair_counts(sequences):
    counts = {}
    for tokens in sequences:
        for p in zip(tokens,tokens[1:]):
            counts[p]=counts.get(p,0)+1
    return counts

counts = pair_counts([[97]*5,[98,97]])
print(sorted(counts.items()))
# [((97,97),4),((98,97),1)]

De tweede documentlijst draagt hier één ba-paar bij. Zij begint geen nieuw paar met het laatste token uit het eerste document. Let ook op dezelfde paren in meerdere documenten: hun aantallen moeten worden opgeteld, niet door een latere dictionary overschreven.

Maak pair_counts(sequences), met tupleparen als keys en frequenties als waarden. Verander de aangeleverde lijsten niet. Deze kleine bouwsteen wordt straks na iedere merge opnieuw gebruikt, omdat ook de aangrenzende paren veranderen.

01
CHECKPOINTTel aangrenzende paren binnen iedere tekst

2. Vervang zonder dubbel gebruik van een token

Scan van links naar rechts. Bij een match verbruik je twee oude posities; anders neem je één oud token over. Zo wordt aaaaa bij aa→256 de reeks [256,256,97]: twee vervangingen, ondanks vier getelde posities.

def merge_pair(tokens,pair,new_id):
    result = []; i = 0
    while i < len(tokens):
        match = (i+1 < len(tokens) and
                 (tokens[i],tokens[i+1])
                 == pair)
        if match:
            result.append(new_id)
            i += 2
        else:
            result.append(tokens[i])
            i += 1
    return result

print(merge_pair([97]*5,(97,97),256))
# [256,256,97]

Noem de lengte vóór de stap L_voor en het aantal echte vervangingen m. Iedere vervanging maakt twee tokens tot één, dus de nieuwe lengte is precies m kleiner. De frequentietelling f is geen veilige vervanger voor m wanneer paren overlappen.

Lna=Lvoor−m

Maak merge_pair(tokens,pair,new_id) met een nieuwe resultaatlijst. Ook wanneer geen paar past, mag je geen alias teruggeven die later de oorspronkelijke invoer verandert. Gebruik de visualisatie met aaaaa en vergelijk getelde posities met echte vervangingen.

TEL, KIES EN VOEG SAMENLeer BPE één merge tegelijk

Deze korte ASCII-teksten houden ieder bytefragment leesbaar. De paren worden na iedere stap opnieuw geteld; documentgrenzen blijven staan.

Document 1
bID 98anID 256aID 97anID 256␠ID 32bID 98anID 256aID 97anID 256
Document 2
bID 98anID 256aID 97anID 256
Beginbytes19Huidige tokens13Geleerde regels1

Laatste paar: a + n. Getelde posities: 6; echte vervangingen: 6.

Volgende paar: a + an, 3 keer. Gelijke tellingen worden beslist met het kleinste numerieke ID-paar.

Een vaak geteld paar kan overlappen. In aaaaa zijn er vier aa-posities, maar één merge-stap vervangt er slechts twee.

02
CHECKPOINTVervang links naar rechts, zonder overlap

3. Leer een vaste rangorde

Train op de twee documenten “abab” en “ba”. Ab en ba hebben ieder frequentie 2; onze tie-break kiest het numeriek kleinste paar, dus (97,98). ID256 staat daarna voor ab, en de reeksen worden [256,256] en [98,97].

Na opnieuw tellen staan (256,256) en (98,97) ieder één keer in het corpus. Nu wint (98,97), dat ID257 krijgt; daarna kan (256,256) ID258 worden. De totale lengtes zijn 6,4,3,2, telkens zonder de documentgrens te overschrijden.

seqs = [list(b'abab'),list(b'ba')]
merges = []; lengths = [6]
for rank in range(3):
    counts = pair_counts(seqs)
    key = lambda p:(-counts[p],p)
    pair = min(counts,key=key)
    new_id = 256+rank
    seqs = [merge_pair(s,pair,new_id)
            for s in seqs]
    merges.append((*pair,new_id))
    lengths.append(sum(map(len,seqs)))
print(merges)
# [(97,98,256),(98,97,257),
#  (256,256,258)]
print(lengths)  # [6,4,3,2]

Maak train_bpe(documents,budget=20), met merges en lengths. Kies het hoogste huidige aantal en bij gelijkstand het kleinste numerieke ID-paar; bewaar regels in trainingsvolgorde. Begin lengths vóór training en stop bij het budget of zodra geen paar meer bestaat.

4. Houd dit lesmodel klein en reproduceerbaar

Het budget is een maximum: twee losse documenten “a” en “b” hebben helemaal geen paar. Voor “é” telt de trainingsgrens bytes, niet codepunten. Onderstaande tekst heeft 2048 codepunten maar 4096 UTF-8-bytes.

print(pair_counts([[97],[98]]))  # {}
text = 'é'*2048
nbytes = len(text.encode('utf-8'))
print(len(text),nbytes)
# 2048 4096

Accepteer maximaal 64 documenten van elk 4096 bytes en een exact integerbudget van 0 tot 128; geef anders ValueError. Bewaar de invoervolgorde en tekst. Meer merges voorspellen geen betere taalmodelkwaliteit: we leren hier alleen terugkerende bytepatronen.

03
CHECKPOINTKies een paar, vervang het, tel opnieuw

Wat neem je mee?

  • Paartellingen mogen overlappen, vervangingen gebruiken ieder token hoogstens één keer.
  • Tel na iedere merge opnieuw en respecteer documentgrenzen.
  • Frequentie plus een vaste tie-break maakt de trainingsrang reproduceerbaar.

Hierna gebruik je de geleerde rangorde op nieuwe tekst, zonder opnieuw te trainen.

DE VOLGENDE STAPGebruik de geleerde merges in hun vaste rangorde
JOUW EXPERIMENT

Probeer het zelf

Python
main.py

Python start zodra jij code uitvoert.

UITVOER

Je uitvoer verschijnt hier.
Alles rekent in je eigen browser.

Jouw checkpoints0 / 3
01
Tel aangrenzende paren binnen iedere tekst

Schrijf pair_counts(sequences): een dictionary van tuple(a,b) naar frequentie. sequences is een lijst lijsten met token-ID’s; wijzig ze niet.

Een hint

Loop per document door zip(tokens,tokens[1:]); voeg de documenten niet vooraf samen.

02
Vervang links naar rechts, zonder overlap

Schrijf merge_pair(tokens,pair,new_id), met een nieuwe lijst als resultaat. Laat de oorspronkelijke tokens staan.

Een hint

Gebruik een while-lus met een index. Een gevonden paar verbruikt twee oude tokens.

03
Kies een paar, vervang het, tel opnieuw

Schrijf train_bpe(documents,budget=20) met merges en lengths. Valideer de genoemde grenzen met ValueError. Gebruik UTF-8-bytes en merge_pair; train ieder document apart. Behoud documents en de documentvolgorde.

Een hint

min(counts,key=lambda p:(-counts[p],p)) combineert dalende frequentie met een vaste tie-break.

Vastgelopen? Bekijk de voorbeeldoplossing
def pair_counts(sequences):
    counts={}
    for tokens in sequences:
        for pair in zip(tokens,tokens[1:]):
            counts[pair]=counts.get(pair,0)+1
    return counts

def merge_pair(tokens,pair,new_id):
    result=[]
    i=0
    while i<len(tokens):
        if i+1<len(tokens) and (tokens[i],tokens[i+1])==pair:
            result.append(new_id)
            i+=2
        else:
            result.append(tokens[i])
            i+=1
    return result

def train_bpe(documents,budget=20):
    if type(budget) is not int or not 0<=budget<=128:
        raise ValueError('Gebruik 0 tot 128 merges')
    if not isinstance(documents,list) or len(documents)>64 or any(not isinstance(text,str) or len(text.encode('utf-8'))>4096 for text in documents):
        raise ValueError('Gebruik maximaal 64 teksten van elk 4096 UTF-8-bytes')
    sequences=[list(text.encode('utf-8')) for text in documents]
    merges=[]
    lengths=[sum(map(len,sequences))]
    for rank in range(budget):
        counts=pair_counts(sequences)
        if not counts:
            break
        pair=min(counts,key=lambda p:(-counts[p],p))
        new_id=256+rank
        sequences=[merge_pair(tokens,pair,new_id) for tokens in sequences]
        merges.append((pair[0],pair[1],new_id))
        lengths.append(sum(map(len,sequences)))
    return {'merges':merges,'lengths':lengths}

Voer de code uit en verklaar voor jezelf waarom elke stap werkt.