Kurs: modele językowe od środka · Lekcja 1 z 12 · 13 min
Tokeny: jak BPE buduje słownik
W skrócie
Model nie czyta liter, tylko numery tokenów. Zobacz, jak BPE skleja najczęstsze pary w słownik i ile to kosztuje: krótsze ciągi, większa macierz osadzeń.
Rozgrzewka Dwa pytania z kursów NN i ML, na których ta lekcja się opiera. Minuta, a lekcji nie blokuje.
Zanim model językowy cokolwiek policzy, tekst zamienia się w ciąg liczb. Każda liczba to numer tokenu: kawałka tekstu ze stałego słownika. Słownik nie jest ustalony ręcznie. Buduje go algorytm BPE (od angielskiego byte-pair encoding) na tekście do nauki, czyli korpusie, a od tego, jak go zbuduje, zależy długość każdego ciągu, który model potem przetwarza, i rozmiar pierwszej macierzy modelu.
Wybierz odpowiedź i pewność.
Odpowiedz, żeby odblokować lab.
Pokaż odpowiedź
Poprawna odpowiedź: Około ćwierci: 5, po jednym na słowo
Po 80 scaleniach każde z pięciu słów jest jednym tokenem, więc z 19 tokenów zostaje 5. To słowa z korpusu: BPE skleja najpierw pary, które w nim występują najczęściej, a te słowa składają się właśnie z takich par.
Spróbuj
- Zostań przy „pies je kość pod stołem” i przesuwaj suwak po jednym scaleniu. Przy którym scaleniu zdanie ma pierwszy raz tyle tokenów, ile słów? To cel labu.
- Patrz na licznik „liter na token” przy 0 i przy 80 scaleniach: o ile krótszy robi się ten sam tekst?
- Przejdź na „psy siedzą na podłodze” przy 80 scaleniach i sprawdź, które kawałki słowa „siedzą” pochodzą z innych słów korpusu.
Pomysł w prostych słowach
BPE zaczyna od pojedynczych liter każdego słowa. Liczy, jak często każda para sąsiednich kawałków występuje w tekście do nauki, skleja najczęstszą parę w nowy token i powtarza. Każde scalenie to jeden nowy token w słowniku. Na 28 zdaniach labu słownik startowy ma 43 symbole (litery, przy czym litera na początku słowa jest osobnym symbolem), a pierwsze scalenie to „i” + „e”: ta para występuje 36 razy. Scaleń może być najwyżej 92, bo dalej żadna para nie występuje co najmniej 2 razy.
Częste słowa szybko stają się jednym tokenem, rzadkie zostają w kawałkach dłużej. W zdaniu z labu ostatnie w całość skleja się „stołem”, które w korpusie występuje tylko 2 razy. Słowo, którego w nauce nie było, tokenizator złoży ze znanych kawałków, w skrajnym razie z pojedynczych liter, ale tylko z liter, które wystąpiły w korpusie. W labie „ż” i „ó” w korpusie nie występują, więc w zdaniu „żółw pływa w akwarium” tokenizator z labu nie miałby dla nich numeru. Dlatego BPE może zaczynać od bajtów zamiast liter: słownik startowy to wtedy 256 możliwych bajtów, każdy znak zapisuje się jednym albo kilkoma bajtami i żaden tekst nie wypada poza słownik [2]. Sam algorytm pochodzi z kompresji danych; jako słownik podsłów zastosowano go w tłumaczeniu maszynowym, żeby rozwiązać problem słów spoza słownika [1].
Dlaczego to działa
Słownik ma dwie strony kosztu. Po 80 scaleniach cały korpus labu to 195 tokenów zamiast 620, czyli ciągi są ponad 3 razy krótsze. To wynik na tekście, na którym tokenizator się uczył: trzy zdania labu spoza korpusu mają po 80 scaleniach 27 tokenów na 55 liter, czyli ok. 2 litery na token. Model, który czyta krótszy ciąg, robi mniej kroków przy generowaniu i mieści więcej tekstu w tym samym oknie kontekstu, czyli w liczbie tokenów, które widzi naraz. Za to każdy token słownika dostaje w modelu własny wektor osadzenia: przy wektorach po 16 liczb macierz osadzeń ma 688 liczb przy 43 symbolach i 1968 liczb przy 123 tokenach. Większy słownik to krótsze ciągi i większa macierz. Rozmiar słownika wybiera się jako kompromis między tymi kosztami.
Ten sam mechanizm ma skutek, który widać w prawdziwych tokenizatorach. Słownik uczy się na tekście do nauki, więc język, którego było w nim mało, kroi się na drobniejsze kawałki. Ten sam tekst przetłumaczony na różne języki może mieć w jednym tokenizatorze nawet 15 razy więcej tokenów w jednym języku niż w innym [3]. Liczby dla polskiego zależą od konkretnego tokenizatora; lab ich nie liczy, bo nie wczytujemy prawdziwych słowników.
Częsty błąd
Prawda czy fałsz: „Model dostaje litery słowa »parapecie«, więc widzi każde jego »r«.” Fałsz dla modelu na tokenach BPE takich jak w labie. Po 80 scaleniach „parapecie” to 1 token, czyli dla modelu jeden numer, a dalej jeden wiersz macierzy osadzeń. Litery wewnątrz tokenu nie są osobnym wejściem: o pisowni tokenu model wie tylko tyle, ile wywnioskował z tekstu w treningu. Model, który czyta pojedyncze litery, dostaje każdą osobno, ale w labie płaci za to ponad 3 razy dłuższymi ciągami: 620 tokenów korpusu zamiast 195.
Quiz
Trzy pytania na sprawdzenie się. Wybierz odpowiedź albo rozwiń odpowiedź pod pytaniem. Z klawiatury: strzałki wybierają, Enter albo spacja zatwierdza.
Najważniejsze wnioski
- Model dostaje numery tokenów, a nie litery; słownik tokenów buduje BPE na tekście do nauki.
- BPE zaczyna od liter, skleja najczęstszą parę sąsiednich kawałków i powtarza; każde scalenie to jeden nowy token.
- Większy słownik daje krótsze ciągi, ale większą macierz osadzeń: rozmiar słownika to kompromis.
- Tekst rzadki w danych do nauki, także inny język, kroi się na więcej tokenów.
Źródła (sprawdzone 26.10.2026)
- arxiv.org/abs/1508.07909: BPE z kompresji danych przeniesione na słownik podsłów w tłumaczeniu maszynowym; rozdział 3.2: znacznik końca słowa, liczba scaleń jako jedyny hiperparametr (Sennrich, Haddow, Birch). (sprawdzone 26.10.2026)
- arxiv.org/abs/1909.03341: BPE na bajtach: słownik startowy to 256 bajtów, więc nie ma tokenów spoza słownika (Wang, Cho, Gu, abstrakt). (sprawdzone 26.10.2026)
- arxiv.org/abs/2305.15425: Ten sam tekst w różnych językach daje w tym samym tokenizatorze bardzo różną liczbę tokenów (Petrov i in., NeurIPS 2023, abstrakt). (sprawdzone 26.10.2026)
Głębiej: wzór, kod, praktyka
Matematyka
Słownik startowy V₀ to litery korpusu. Krok BPE: dla każdej pary sąsiednich symboli (a, b) policz n(a, b) = Σ_słowa f(słowo) · (liczba wystąpień pary w słowie), gdzie f to częstość słowa, i dodaj do słownika ab dla pary z największym n.
- Po
kscaleniach słownik ma|V| = |V₀| + ktokenów. - Macierz osadzeń ma
|V| · dliczb, gdziedto długość wektora tokenu. - Długość zakodowanego tekstu
L(k)nigdy nie rośnie zk: scalenie zamienia dwa symbole na jeden albo niczego nie zmienia. - W praktyce liczbę scaleń, czyli rozmiar słownika, wybiera się z góry: to jedyny hiperparametr algorytmu [1]. Lab kończy, gdy żadna para nie występuje co najmniej 2 razy: taki token skróciłby tylko jedno słowo i nic by nie uogólniał.
- Znak ▁ na początku słowa to wariant zapisu granicy słowa; w artykule o BPE granicę oznacza znacznik na końcu słowa [1].
W kodzie (Python)
BPE na 28 zdaniach labu w czystym Pythonie i numpy: słownik startowy, pierwsze scalenie, cel labu i koszt słownika.
import numpy as np
from collections import Counter
corpus = [
"kot siedzi na parapecie", "kot je rybę na kanapie", "pies siedzi na kanapie", "pies je kość pod stołem",
"koty lubią spać na parapecie", "psy lubią biegać po parku", "mały kot i mały pies śpią razem", "dzieci lubią koty i psy",
"kot śpi na kanapie a pies na podłodze", "dzieci biegają po parku z psem", "mały pies je kość a kot je rybę", "koty i psy śpią razem na kanapie",
"pies biega po parku a kot siedzi na parapecie", "koty lubią rybę a psy lubią kość", "dzieci karmią koty i psy", "mały kot śpi na podłodze",
"pies śpi pod stołem", "dzieci lubią biegać z psem po parku", "kot patrzy na rybę", "mały pies patrzy na kota",
"ptak siedzi na drzewie", "ptaki śpiewają na drzewie", "kot patrzy na ptaki", "babcia gotuje obiad dla dzieci",
"dzieci jedzą obiad przy stole", "babcia karmi ptaki w ogrodzie", "pies biega po ogrodzie", "babcia lubi ptaki",
]
def letters(word): # "kot" -> ["▁k", "o", "t"]: the first letter carries the word-start mark
return ["▁" + word[0]] + list(word[1:])
def merge(syms, a, b):
out, i = [], 0
while i < len(syms):
if i + 1 < len(syms) and syms[i] == a and syms[i + 1] == b:
out.append(a + b); i += 2
else:
out.append(syms[i]); i += 1
return out
def train(sentences):
freq = Counter(w for s in sentences for w in s.split())
vocab = [(letters(w), f) for w, f in freq.items()]
merges = []
while True:
pairs = Counter()
for syms, f in vocab:
for a, b in zip(syms, syms[1:]):
pairs[(a, b)] += f
(a, b), n = max(pairs.items(), key=lambda kv: kv[1]) # ties: the pair met first
if n < 2:
return merges
merges.append((a, b, n))
vocab = [(merge(syms, a, b), f) for syms, f in vocab]
def encode(sentence, merges, k):
out = []
for w in sentence.split():
syms = letters(w)
for a, b, _ in merges[:k]:
syms = merge(syms, a, b)
out += syms
return out
merges = train(corpus)
start = sorted({t for s in corpus for t in encode(s, [], 0)})
a, b, n = merges[0]
print(f"start vocabulary: {len(start)} symbols; merges possible: {len(merges)}, first: {a} + {b} ({n}x)")
s = "pies je kość pod stołem"
fewest = next(k for k in range(len(merges) + 1) if len(encode(s, merges, k)) <= len(s.split()))
print(f"'{s}': {len(encode(s, merges, 0))} tokens at 0 merges, {len(encode(s, merges, 80))} at 80, fewest for <= words: {fewest}")
for k in [0, 80]:
tokens = sum(len(encode(x, merges, k)) for x in corpus)
E = np.zeros((len(start) + k, 16)) # one 16-number embedding per token of the vocabulary
print(f"{k} merges: corpus {tokens} tokens, vocabulary {len(start) + k}, embedding matrix {E.size} numbers")
Wynik
start vocabulary: 43 symbols; merges possible: 92, first: i + e (36x)
'pies je kość pod stołem': 19 tokens at 0 merges, 5 at 80, fewest for <= words: 68
0 merges: corpus 620 tokens, vocabulary 43, embedding matrix 688 numbers
80 merges: corpus 195 tokens, vocabulary 123, embedding matrix 1968 numbers
Wynik jest ten sam co w labie, bo kod robi te same kroki w tej samej kolejności, łącznie z remisami. Uruchomione w numpy 2.1.3.
Pobierz notebook (.ipynb). Otwórz go w Jupyterze albo wgraj do Google Colab; brakujące dane pobierze sam.
Dla praktyka
- Licz długość tekstu, okno kontekstu i koszt w tokenach konkretnego tokenizatora, nie w słowach ani znakach: ten sam tekst w innym tokenizatorze ma inną długość.
- Tokenizator jest częścią modelu: model uczony z jednym słownikiem nie zadziała z innym, bo numery tokenów znaczą co innego.
- Zanim wybierzesz model do tekstów po polsku, policz tokeny na próbce własnych tekstów w jego tokenizatorze; ten sam tekst w różnych językach daje bardzo różną liczbę tokenów [3].
- Gdy zadanie dotyczy pojedynczych liter (liczenie, odwracanie słowa), pamiętaj, że model dostaje tokeny, a nie litery: rozpisz słowo na litery w poleceniu albo daj mu narzędzie.
Więcej w Atlasie
Hasła encyklopedii o pojęciach z tej lekcji: