M0 / Tokenizer BPE / beta

Zbuduj BPE.
Przejmij flagę M0.

Najpierw przewidź merge. Potem napisz własny rdzeń i wytrenuj tokenizer, który poradzi sobie z nieznanymi polskimi tekstami.

ZACZNIJ TUTAJ / mapa lekcji

Nie musisz jeszcze wiedzieć, jak działa BPE.

Ta misja prowadzi od małego przykładu na kartce do tokenizera działającego na tekstach, których nie widział podczas treningu. Potrzebujesz podstaw Pythona: pętli, list, słowników i funkcji. Nie potrzebujesz GPU ani znajomości sieci neuronowych. Część w przeglądarce działa bez konta; kod uruchomisz na swoim komputerze, a klucz bety będzie potrzebny do indywidualnego wyzwania i oceny.

Nie zaczynaj od wysyłania plików. Najpierw naucz się przewidywać jeden krok algorytmu. Potem przenieś tę samą regułę do kodu. Na końcu zajmij się jakością danych i kompresją. Jeśli coś nie działa, wróć do najmniejszego przykładu, dla którego potrafisz samodzielnie wyliczyć wynik.

Dwie części misji, dwa różne pytania

proof.json odpowiada na pytanie: „Czy mój rdzeń wykonuje dokładnie zdefiniowany algorytm?”. Powstaje z Twojej implementacji i osobistego challenge’u. tokenizer.json odpowiada na inne pytanie: „Czy umiem zbudować użyteczny tokenizer dla różnorodnych polskich tekstów?”. To finalny artefakt o słowniku 8192 tokenów. Do jego treningu wolno użyć biblioteki ze startera. Nie musisz rozbudowywać małego challenge’u do 8192 tokenów.

Wyjaśnienia i małe testy są jawne. Właściwy challenge i ukryta ewaluacja pozostają zadaniem do rozwiązania. Możesz korzystać z AI, ale sprawdzaj jego odpowiedzi: poprawny plik sam w sobie nie dowodzi, że rozumiesz algorytm.

01 / zrozum reprezentację

Model nie widzi liter. Dostaje liczby.

Tokenizer zamienia tekst na sekwencję identyfikatorów tokenów. Token nie musi być słowem: może reprezentować bajt, fragment słowa, całe słowo albo fragment zawierający spację. Słownik opisuje, co oznaczają identyfikatory; zakodowany tekst to lista tych identyfikatorów w odpowiedniej kolejności.

W tej części zaczynamy od bajtów UTF-8. Bajt ma jedną z 256 wartości, od 0 do 255, więc początkowe ID są właśnie tymi wartościami. ASCII a zajmuje jeden bajt, ale polskie ż zajmuje dwa. Liczba widocznych liter, liczba bajtów i liczba tokenów to trzy różne rzeczy.

# Uruchom w Pythonie i porównaj wyniki:
list("a".encode("utf-8"))    # [97]
list("ż".encode("utf-8"))    # [197, 188]
len("żółć")                 # 4 znaki w tym przykładzie
len("żółć".encode("utf-8"))  # 8 bajtów

Dlaczego nie zacząć od listy polskich liter? Bo wejściem może być też emoji, nazwa w innym języku albo kod. Pełny alfabet bajtowy pozwala reprezentować dowolny poprawny tekst UTF-8. Pojedynczy bajt wielobajtowego znaku nie musi być samodzielnie czytelny; dekodujemy dopiero złożoną sekwencję bajtów.

Co zmienia merge?

Jeśli często spotykamy obok siebie 97, 98, możemy nadać całej parze nowe ID 256. To skrót dla bajtów ab. Tekst abab będzie teraz reprezentowany jako [256, 256] zamiast [97, 98, 97, 98]. Tekst się nie zmienił. Skróciła się jego reprezentacja.

Zatrzymaj się: czy tokenizer może zamienić „Łódź” na „łódź”, jeśli ma przejść bezstratny round-trip?
Sprawdź odpowiedź i uzasadnienie

Nie. Round-trip oznacza dokładnie decode(encode(tekst)) == tekst. Zmiana wielkości liter traci informację. Tak samo usunięcie podwójnych spacji albo końcowego znaku nowej linii. „Podobnie wygląda” nie znaczy „jest identyczne”.

Dalej: policz pierwszy merge →

02 / policz przed uruchomieniem kodu

Jeden krok BPE, bez magii.

Weź cztery osobne dokumenty: abab, abab, baba, aaaa. Duplikat jest tu celowy: drugi dokument także uczestniczy w zliczaniu. Nie dodajemy par pomiędzy końcem jednego dokumentu a początkiem następnego.

  1. Zapisz każdy dokument jako osobną listę bajtów. Dla abab dostaniesz [97, 98, 97, 98].
  2. Przesuń okno długości dwa o jedną pozycję. W abab zobaczysz ab, ba, ab. To trzy pary, a nie dwie.
  3. Zsumuj częstości ze wszystkich dokumentów. Para ab występuje 5 razy, ba 4 razy, a aa 3 razy.
  4. Wybierz najczęstszą parę. Wygrywa (97, 98). Przy remisie wybierasz numerycznie najmniejszą parę: najpierw porównujesz lewe ID, potem prawe.
  5. Nadaj jej ID 256 i zamień wystąpienia od lewej do prawej. Nie wykorzystuj ponownie pozycji, która już weszła w merge.
  6. Policz pary od nowa. Następna iteracja pracuje na nowych listach tokenów, nie na oryginalnych literach.
Przed:                      Po połączeniu (97, 98) → 256:
[97, 98, 97, 98]            [256, 256]
[97, 98, 97, 98]            [256, 256]
[98, 97, 98, 97]            [98, 256, 97]
[97, 97, 97, 97]            [97, 97, 97, 97]

Nowy słownik jest większy o jeden wpis, a korpus zajmuje mniej tokenów. To istota BPE: kolejne często występujące fragmenty otrzymują własne identyfikatory. Nowe tokeny również mogą być częścią kolejnych par.

Przewidź: jaka para wygra teraz?

(97, 97) ma częstość 3. Para (256, 256) ma częstość 2; pozostałe mają po 1. Nowe ID będzie wynosić 257. Dokument aaaa zmieni się w [257, 257].

Dlaczego „aaaa” daje 3 pary „aa”, ale tylko 2 zamiany?

Podczas zliczania sprawdzasz pozycje (0,1), (1,2), (2,3): trzy wystąpienia. Podczas zamiany łączysz pozycje (0,1), a następnie (2,3). Środkowa para nakłada się na już wykorzystaną pozycję, więc nie może być osobną zamianą. W śladzie zapisujesz częstość 3 sprzed merge’u, nie liczbę zamian 2.

Sprawdź granicę dokumentu: korpus [„a”, „b”]. Ile par?

Zero. Każdy dokument ma tylko jeden token. Sklejenie dokumentów utworzyłoby parę (97,98), której nie ma w wejściu. To częsty błąd implementacji.

Teraz sprawdź to w laboratorium poniżej

  1. Dla domyślnego korpusu wpisz 97,98 i wybierz „Sprawdź i połącz”. Porównaj listy z przykładem powyżej.
  2. Przewidź drugą parę bez podpowiedzi. Dopiero potem odsłoń licznik. Zobacz, że stare ID oraz nowe ID mogą występować obok siebie.
  3. Zastąp korpus jednym wierszem aaaa, kliknij „Uruchom eksperyment” i połącz 97,97. Sprawdź różnicę między częstością a liczbą zamian.
  4. Wpisz dwa wiersze: ab i ac. Obie pary mają częstość 1. Przewidź zwycięzcę regułą remisu.
  5. Wpisz żż. Zaczniesz od czterech bajtów. Sprawdź, czy potrafisz wskazać parę, która odtworzy jedno „ż”.

To ćwiczenie służy zrozumieniu reguł, nie zdobyciu flagi. Rozwiązanie czterech krótkich dokumentów nie zastępuje pełnego challenge’u ani treningu tokenizera.

Rozgrzewka / bez logowania

Przewidź następny merge.

Każdy wiersz jest osobnym dokumentem. Remis wygrywa najmniejsza numerycznie para ID. Zliczamy także nakładające się pary, ale łączymy je od lewej bez nakładania.

Ślad algorytmu

Bajty → tokeny

Znak „ż” zajmuje dwa bajty UTF-8. Kliknij „Uruchom eksperyment” po zmianie tekstu. Rozgrzewka nie zalicza właściwej misji.

Licznik pojawi się po kliknięciu podpowiedzi.
Brak merge’ów.
03B / prawdziwy Python w przeglądarce

Zmień kod. Uruchom. Zobacz, co się zmieniło.

To edytowalny Python, nie animacja gotowego wyniku. Wybierz przykład, przewidź rezultat i kliknij „Uruchom”. Nie potrzebujesz instalacji ani klucza. Silnik Pyodide pobieramy z CDN dopiero przy pierwszym uruchomieniu — może to potrwać kilkanaście sekund.

Gotowe do pierwszego eksperymentu.

Tutaj zobaczysz print(), wyniki testów i błędy Pythona.

Kod wykonuje się na Twoim urządzeniu w osobnym workerze. „Zatrzymaj” przerywa także nieskończoną pętlę. Limit uruchomienia: 30 s; wynik: 20 tys. znaków. Każde uruchomienie ma nową przestrzeń zmiennych. Zmiana przykładu przywraca jego kod. Edycje nie są zapisywane po odświeżeniu — skopiuj je przed wyjściem. Nie wklejaj kluczy ani niezaufanego kodu. Demo nie wysyła rozwiązania do gradera i nie przyznaje flagi.

Materiał z kohorty #1 / 1:28:04

Tokenizacja i BPE od zera

Otwórz archiwum, zaloguj się swoim dostępem do nagrań, a następnie wczytaj wideo tutaj. Klucz gradera i dostęp do archiwum są osobne.

Nagranie wymaga dostępu do archiwum.

04 / oglądaj z pytaniem

Wideo jest pomocą do zadania.

Powyżej jest pełne nagranie kohorty, nie zmontowane krótkie lekcje. Przyciski przenoszą do punktów w nagraniu. Nie musisz oglądać całości przed rozpoczęciem pracy.

05 / przygotuj warsztat

Od pobrania ZIP-a do pierwszego testu.

  1. Pobierz starter i rozpakuj go. Otwórz terminal w katalogu slayer-m0-tokenizer, w którym leżą requirements.txt i bpe_core.py. Samo otwarcie ZIP-a nie wystarczy.
  2. Sprawdź interpreter poleceniem python --version (na części komputerów: python3 --version). Zalecamy Python 3.12. W dalszych komendach używaj tego samego interpretera.
  3. Utwórz osobne środowisko i zainstaluj zależności. Dzięki temu biblioteki misji nie mieszają się z innymi projektami.
# macOS / Linux, z katalogu startera:
python3 -m venv .venv
source .venv/bin/activate
python -m pip install -r requirements.txt
cp manifest.example.json manifest.json
Windows / PowerShell
py -3 -m venv .venv
.venv\Scripts\python.exe -m pip install -r requirements.txt
Copy-Item manifest.example.json manifest.json
# W kolejnych poleceniach zamiast python używaj:
.venv\Scripts\python.exe check_core.py

Nie musisz zmieniać polityki wykonywania skryptów, jeśli uruchamiasz interpreter bezpośrednio ze środowiska.

Co jest gotowe, a co piszesz samodzielnie?

Na tym etapie sukcesem jest uruchomienie python check_core.py. Początkowo zobaczysz wskazówki przy niezaimplementowanych funkcjach. To punkt startowy; nie musisz jeszcze nic wysyłać.

06 / zbuduj rdzeń małymi krokami

Jedna funkcja. Jeden sprawdzalny kontrakt.

A. count_pairs — co występuje obok czego?

Wejściem jest lista dokumentów, a każdy dokument to lista ID. Wynikiem jest licznik, którego kluczami są pary ID. Najpierw obsłuż jeden dokument, potem sumuj wyniki wielu dokumentów. Nie sklejaj ich. Pusty dokument i dokument długości jeden nie wnoszą żadnej pary.

Wejście: [[97, 97, 97, 97]]
Oczekiwane zliczenie: {(97, 97): 3}
Wejście: [[97], [98]]
Oczekiwane zliczenie: {}

B. select_pair — jednoznaczny wybór

Wybierz największą częstość. Jeśli kilku kandydatów ma ten sam wynik, wybierz najmniejszą parę liczbową. Nie polegaj na kolejności dodawania do słownika. Wartości 98 i 256 porównuj jako liczby, nie napisy. Gdy par już nie ma, pętla treningowa powinna to wykryć przed wywołaniem wyboru.

Częstości: {(98, 97): 2, (97, 99): 2, (97, 98): 2}
Wynik: (97, 98)

C. merge_pair — zamiana bez utraty informacji

Zwróć nowe listy; nie zmieniaj wejścia. Przechodź od lewej. Dopasowanie pary zużywa dwa elementy wejścia i daje jeden element wyjścia. Brak dopasowania przenosi jeden element bez zmiany. Pamiętaj o ostatnim elemencie, który może nie mieć sąsiada.

Wejście: [[97, 97, 97]], para: (97, 97), nowe ID: 256
Wyjście: [[256, 97]]
Nie: [[256, 256]] — taki wynik wykorzystałby środkowy bajt dwa razy.

D. train_bpe — połącz kroki w pętlę

Zamień każdy tekst na jego bajty UTF-8. W każdej iteracji policz pary, wybierz zwycięzcę, zapisz jego częstość przed zamianą, wykonaj merge i przejdź do następnej iteracji. Nowe ID to 256, 257, 258… Nie resetuj tego licznika pomiędzy dokumentami.

# Pierwszy wiersz śladu dla przykładu z lekcji:
{"left_id": 97, "right_id": 98, "result_id": 256, "frequency": 5}

Uruchom python check_core.py. Jeśli test nie przechodzi, porównaj wynik z przykładem ręcznym. Przy błędzie w późniejszym merge’u szukaj pierwszego rozbieżnego kroku — wszystkie kolejne mogą być tylko jego konsekwencją. Małe testy nie stanowią pełnego dowodu poprawności; dodaj własne przypadki.

Samodzielne zadanie: jakie testy jeszcze dopisać?

Sprawdź brak modyfikacji wejścia, pusty korpus, dokument jednoelementowy, remis między ID 98 i 256, powtarzającą się parę złożoną z nowych tokenów i polskie znaki. Dla korpusu, w którym pary się wyczerpały, zakończ trening zamiast wymyślać kolejne merge’e. Serwerowy challenge wymaga pełnej zadanej liczby kroków.

Dopiero teraz pobierz indywidualne wyzwanie

Połącz konto kluczem w panelu poniżej i wybierz „Pobierz mój challenge”. Przenieś pobrany challenge.json do katalogu startera. Nie zmieniaj jego korpusu, ID ani sumy kontrolnej. Następnie uruchom:

python make_proof.py

Oczekiwany wynik dla tej bety to proof.json z 192 merge’ami. Challenge jest ważny 72 godziny; datę ważności widzisz przy pobraniu. Po wygaśnięciu pobierz nowe wyzwanie, wygeneruj nowy dowód i zaktualizuj ID w manifeście. Finalny tokenizer możesz rozwijać niezależnie od tego terminu.

07 / z algorytmu do użytecznego artefaktu

Poprawny BPE to dopiero połowa zadania.

Challenge sprawdza mechanikę algorytmu. Teraz chodzi o to, czego algorytm nauczy się z danych. Tokenizer wytrenowany wyłącznie na jednym rodzaju tekstu może dobrze kompresować ten rodzaj tekstu, a słabo radzić sobie z innymi domenami.

  1. Przygotuj pliki UTF-8 w katalogu data/. Uwzględnij różne typy polskich tekstów: narrację, dialog, treści techniczne, liczby i kod. Zapisz źródło oraz licencję każdego wykorzystanego zbioru. Nie traktuj tekstu dostępnego w internecie automatycznie jako danych do dowolnego użytku.
  2. Oddziel trening od własnej walidacji. Trzymaj walidację poza data/, żeby funkcja zbierająca pliki nie dołączyła jej do treningu. Usuń duplikaty między częściami. Walidacja ma pytać o nowe teksty, nie o zapamiętane przykłady.
  3. Uzupełnij corpus_files. Zwróć stabilnie uporządkowaną listę rzeczywistych, niepustych plików tekstowych. Sprawdź ją przed długim treningiem.
  4. Uzupełnij build_tokenizer. Potrzebujesz modelu BPE, zgodnego byte-level pre-tokenizera i dekodera oraz alfabetu pozwalającego reprezentować każdy bajt. Nie dodawaj stratnej normalizacji. W tej części biblioteka tokenizers jest dozwolona.
  5. Wytrenuj pierwszy wariant. Uruchom python train_tokenizer.py. Powinien powstać samowystarczalny tokenizer.json z dokładnie 8192 wpisami słownika, łącznie z dodanymi tokenami. Zbyt małe lub zbyt jednorodne dane mogą nie wystarczyć do osiągnięcia tej liczby.

Najpierw bezstratność, potem kompresja

Sprawdź zdanie z polskimi znakami, emoji, tabulator, kilka spacji i końcowy newline. Jeśli odkodowany tekst różni się od oryginału, nie poprawiaj jeszcze wyniku kompresji — najpierw napraw reprezentację. Usuwanie znaków nie jest dozwoloną metodą „ulepszania” wyniku.

from tokenizers import Tokenizer
t = Tokenizer.from_file("tokenizer.json")
text = "Łódź  2026\t🙂\n"
ids = t.encode(text, add_special_tokens=False).ids
restored = t.decode(ids, skip_special_tokens=False)
print(repr(text), repr(restored))
assert restored == text

Jak czytać wynik?

Używamy znaków na token: liczba znaków podzielona przez liczbę tokenów. Jeśli 1200 znaków daje 400 tokenów, wynik wynosi 3,00. Jeśli kolejny tokenizer daje 300 tokenów dla tego samego tekstu, wynik wynosi 4,00 — lepiej. Nie myl tego z tokenami na znak: ta odwrotna miara powinna maleć.

Wynik globalny licz z sum znaków i sum tokenów, a nie jako zwykłą średnią wyników dokumentów. Sprawdzaj również każdą domenę osobno. Dobry wynik na długich tekstach literackich może ukryć słaby wynik na krótkich przykładach technicznych.

python grade.py tokenizer.json proof.json challenge.json

Jawny test wypisuje rozmiar słownika, wynik łączny i wyniki domen. Jego progi to 3,30 globalnie i 2,50 dla każdej domeny. Aktualne progi ukrytej oceny pobieramy z serwera w panelu kontraktu poniżej. To inne teksty: wyższy wynik na publicznej próbce nie gwarantuje wyniku na ukrytej.

Prowadź eksperyment, nie serię zgadywanek

Zapisz dla każdego wariantu: skład danych, konfigurację, wynik globalny, najsłabszą domenę i jedną hipotezę. Zmieniaj jedną istotną rzecz naraz, na przykład udział tekstów technicznych. Zachowuj wcześniejsze artefakty. Porównuj na tej samej własnej walidacji i nie dokładaj jej do treningu po nieudanym wyniku.

Czy wystarczy dopisać publiczne testy do danych treningowych?

Nie jest to dowód generalizacji. Wynik na tych przykładach może wzrosnąć, ale przestają być niezależnym testem. Ukryty zbiór sprawdza inne teksty. Celem jest poprawa reprezentacji całego rodzaju danych, nie zapamiętanie pojedynczych zdań.

08 / wyślij, zdiagnozuj, popraw

Flaga jest wynikiem testu, nie checkboxem.

  1. W manifest.json wpisz handle swojego konta, ID z bieżącego challenge.json, prawdziwe źródła i licencje oraz krótki opis przygotowania danych. Usuń przykładowe wartości; zachowaj schemat i nazwę modułu.
  2. Sprawdź, czy masz tokenizer.json, proof.json i manifest.json. challenge.json jest wejściem do pracy lokalnej — nie wybierasz go zamiast dowodu.
  3. Połącz konto i wybierz te trzy pliki w formularzu poniżej. Nie przesyłaj kodu ani ZIP-a. Klucz uczestnika jest sekretem: nie dodawaj go do repozytorium, manifestu ani zrzutów ekranu.
  4. Poczekaj na ocenę. Przyjęcie do kolejki nie oznacza zaliczenia. Ostateczny status pojawi się w panelu i historii prób.
  5. Po PASS aktualnej wersji oceny zobaczysz flagę w panelu konta, a Twój najlepszy zaliczony wynik może pojawić się na publicznym leaderboardzie pod handlem konta. Nie musisz zgadywać flagi ani wpisywać jej w dodatkowym formularzu.

Jeśli dostaniesz FAIL, wybierz właściwą diagnozę

Dowód BPE jest niepoprawny

Wróć do kroku 06. Sprawdź zgodność challenge’u, regułę remisów, zliczanie nakładających się par oraz granice dokumentów. Zapisuj listy po każdym merge’u i porównaj pierwszy rozbieżny krok z małym ręcznie policzonym przykładem. Więcej danych treningowych nie naprawi błędu w rdzeniu.

Round-trip nie przechodzi lub pojawiają się nieznane tokeny

Wróć do konfiguracji z kroku 07. Sprawdź alfabet bajtowy, dekoder i normalizację. Wypisz repr wejścia oraz wyjścia, żeby zobaczyć niewidoczne spacje i nowe linie. Nie usuwaj problematycznego przykładu z testu.

Słaba kompresja globalna lub jedna słaba domena

Najpierw potwierdź poprawność techniczną. Potem porównaj domeny na własnej walidacji, przejrzyj tokenizację kilku nowych tekstów i zapisz hipotezę o brakujących danych. Zmiana miksu może poprawić jedną domenę kosztem innej — oceniaj cały raport.

Publiczny PASS, ale ukryty FAIL

To możliwy wynik eksperymentu, nie sprzeczność. Publiczna próbka jest mała i jawna; ukryta ma inne przykłady. Poszerz niezależną walidację i popraw generalizację. Beta nie jest jeszcze gwarancją jakości na każdym możliwym tekście.

Problem z kontem, terminem lub serwerem

Po odświeżeniu strony ponownie wpisz klucz — nie zapisujemy go trwale w przeglądarce. Jeśli challenge wygasł, pobierz nowy i przelicz dowód. Błąd infrastruktury nie jest diagnozą jakości tokenizera: zachowaj identyfikator próby i zgłoś go prowadzącemu, bez ujawniania klucza.

Zanim uznasz misję za opanowaną: wyjaśnij bez zaglądania do kodu, dlaczego „aaaa” ma trzy pary, dlaczego dwa dokumenty nie tworzą pary na granicy oraz dlaczego lepszy wynik na danych treningowych nie wystarcza. Flaga potwierdza zaliczenie testów; te odpowiedzi pomagają sprawdzić własne rozumienie.
Kontrakt / kryteria PASS

Co trzeba dostarczyć?

1. proof.json — pełne 192 merge’e z indywidualnego challenge’u.
2. tokenizer.json — dokładnie 8192 tokeny, bezstratny round-trip i zero UNK.
3. manifest.json — Twój handle, ID challenge’u oraz źródła danych.

Ładowanie aktualnych progów…

Kod uruchamiasz u siebie. Tutaj pobierasz challenge, wysyłasz pliki i otrzymujesz wynik. Sam publiczny PASS nie gwarantuje flagi.

python fetch_challenge.py
python make_proof.py
python train_tokenizer.py
python grade.py
Twoje konto bety

Połącz z graderem

Klucz otrzymujesz od prowadzącego betę. Przechowujemy go wyłącznie w pamięci tej karty; po odświeżeniu połącz się ponownie.

Oddaj artefakt / capture the flag

Twój kolejny eksperyment.

Połącz konto, aby wysłać rozwiązanie.

Historia / postęp z serwera

Próby i diagnozy.

Historia pojawi się po połączeniu z kontem.