# Jak zbudować duży model językowy od zera? Toktumi 1.0 – Część 1/3

🇬🇧 [**ENGLISH VERSION**](https://sixfingers.hashnode.dev/toktumi-1-en)

## **Opis projektu**

W ramach projektu własnego postanowiłem stworzyć i wytrenować **od podstaw model językowy** typu LLM, działający **w języku polskim**. Projekt ma charakter **edukacyjno-badawczy i niekomercyjny**, a jego głównym celem było zdobycie praktycznej wiedzy na temat tworzenia i trenowania modeli językowych.

Projekt rozpocząłem w 2023 roku. Zdecydowałem się na architekturę **GPT-2**, opracowaną przez OpenAI. To jeden z przełomowych modeli językowych, który **odegrał ważną rolę w rozwoju generatywnej sztucznej inteligencji**. Dzięki publicznej dostępności kodu i wag modelu stanowił dobry punkt wyjścia do eksperymentów w tamtym czasie.

Pomysł był taki, żeby **stworzyć podobny model od A do Z**, choć w mniejszej skali. Czyli samodzielnie **zebrać teksty** (web scraping), przeprowadzić **czyszczenie danych**, przygotować **duży zbiór uczący**, wypracować **optymalny kod uczący**, doprowadzić do końca **proces uczenia na domowym PC**, a następnie znaleźć **najbardziej efektywne metody promptowania** na gotowym modelu.

W projekcie wykorzystałem język programowania **Python** oraz biblioteki **PyTorch** i **Transformers**.

Projekt ostatecznie został zrealizowany **w trzech etapach**:

> **Etap 1 - Toktumi 1.0 (2024):** Polegał na stworzeniu i wytrenowaniu **od podstaw** pierwszej działającej wersji modelu. Wykorzystałem **3 115 002 próbki tekstów** pozyskanych z różnych źródeł internetowych i innych materiałów. Przygotowałem własny tokenizer, przeprowadziłem trening i pierwsze testy jakości odpowiedzi.

> **Etap 2 - Toktumi 2.0 (2025):** Był to ponowny trening modelu **od zera**, z wykorzystaniem poprawionego zbioru uczącego, zawierającego **2 336 162 próbki tekstów**. Na podstawie doświadczeń i wniosków z poprzedniego etapu zmodyfikowałem strukturę i zawartość zbioru treningowego.

> **Etap 3 - Toktumi 3.0 (2026):** **Fine-tuning** modelu Toktumi 2.0 na całkowicie syntetycznym zbiorze liczącym **6 190 420 krótszych rekordów**, składającym się wyłącznie z par typu **pytanie–odpowiedź, instrukcja–odpowiedź** oraz **prostych par dialogowych**. Celem było rozwinięcie wiedzy faktograficznej modelu, poszerzenie jego możliwości językowych i poprawa jakości generowanych odpowiedzi.

Każdy z trzech etapów szczegółowo opisuję **w kolejnych częściach pracy**.

# **ROZDZIAŁ I. Toktumi 1.0. Pierwszy trening.**

# 1\. Wybór rozmiaru modelu

Spośród czterech dostępnych rozmiarów **GPT-2 (XL, Large, Medium, Small)**, zdecydowałem się na rozmiar **Medium**, ponieważ był to rozsądny kompromis pomiędzy posiadanymi zasobami sprzętowymi, a jakością generowanego tekstu. Granicę wyznaczała też ilość posiadanego vram (12 GB) na karcie graficznej NVIDIA RTX 3060. Uczenie modelu GPT-2 Medium w wybranej przeze mnie konfiguracji zużywało około 11 GB. Uznałem to za optymalny wybór.

**Dostępne rozmiary modeli GPT-2**

- GPT-2 Small: 124M, czyli: 124 miliony parametrów
- **GPT-2 Medium: 345M, czyli: 345 milionów parametrów** &lt;&lt; wybrany model
- GPT-2 Large: 762M, czyli: 762 miliony parametrów
- GPT-2 XL: 1,5B, czyli: 1,5 miliarda parametrów

# 2\. Analiza rozwiązań OpenAI (GPT-2)

Zacząłem od przeanalizowania dostępnych informacji o **GPT-2**, udostępnionych przez OpenAI w serwisach [Hugging Face](https://huggingface.co/openai-community/gpt2) oraz [GitHub](https://github.com/openai/gpt-2/tree/master), aby wzorować się na rozwiązaniach zastosowanych przez twórców modelu.

Co warto wiedzieć: oryginalny model **GPT-2, zaprezentowany przez OpenAI w lutym 2019 roku**, był trenowany na zbiorze danych o rozmiarze około **40 GB**, o nazwie **WebText**, [\[źródło: OpenAI\]](https://openai.com/index/better-language-models/), [\[źródło: Hugging Face\]](https://huggingface.co/openai-community/gpt2). Zbiór ten zawierał około **8 milionów próbek tekstu**, zebranych ze stron internetowych. Częściowa lista tych adresów została opublikowana na [GitHubie](https://github.com/openai/gpt-2/blob/master/model_card.md). Samego zbioru jednak firma nie udostępniła. Na podstawie dostępnych źródeł można jednak dowiedzieć się czegoś o jego zawartości, jak i o stosowanej przez Open AI metodologii.

Dodatkową inspiracją były też istniejące wówczas modele GPT-2 w języku polskim, które przygotowali **Sławomir Dadas** [polish-gpt2](https://huggingface.co/sdadas/polish-gpt2-xl/tree/main) oraz **Dariusz Kleczek** [papuGaPT2](https://huggingface.co/dkleczek/papuGaPT2).

# 3\. Zebranie danych

Wybrane źródła:

![003_logos_all](https://cdn.hashnode.com/uploads/covers/6af0eebb5f7d123257967ad7/c060382e-5c79-4736-ba67-d772cd05d878.png align="center")

Do uczenia **Toktumi 1.0** użyłem **3 115 002 próbek tekstów**, zebranych przeze mnie z następujących źródeł: (**Wykop, Wikipedia , Fakt, Pudelek, Polsat News, książki, dowcipy**).

Celem było zgromadzenie tekstów reprezentujących **żywy, potoczny język** i stworzenie modelu, który potrafi **prowadzić konwersację** na różnorodne tematy, używając stylu **mowy potocznej**.

Na tym etapie nie wiedziałem jeszcze, jakie rodzaje tekstów najlepiej sprawdzą się w zbiorze treningowym. Dobór materiałów miał więc częściowo charakter eksperymentalny — wiele decyzji podejmowałem **metodą prób i błędów**, stopniowo poznając możliwości modelu i kierunki jego dalszego rozwoju.

![006_wykres_teksty](https://cdn.hashnode.com/uploads/covers/6af0eebb5f7d123257967ad7/6e8a2532-b0f0-44ce-a1c9-dbc129748e4c.png align="center")

a) [**Wikipedia**](https://pl.wikipedia.org) - wykorzystałem zasoby (data dump) polskiej wersji Wikipedii, udostępniane przez Fundację Wikimedia [do pobrania](https://dumps.wikimedia.org/plwiki/latest/) na licencji Creative Commons CC BY-SA 4.0. Następnie zawartość pliku XML przekonwertowałem do czystego tekstu.

Wyodrębniłem **1 485 976 haseł**, z czego ostatecznie wykorzystałem około połowy z nich **(718 129)** - żeby styl Wikipedii nie 'zdominował' stylu odpowiedzi modelu.

Teksty z Wikipedii mają charakter haseł encyklopedycznych - są bogate w informacje, skupione na faktach, napisane neutralnym językiem, bez emocjonalnego stylu lub przekazywania opinii.

![007_wikitext](https://cdn.hashnode.com/uploads/covers/6af0eebb5f7d123257967ad7/619920c7-866c-4342-a816-5a161b88a53a.png align="center")

b) [**Fakt.pl**](https://www.fakt.pl/) - do pozyskania tekstów wykorzystałem różne metody **web scrapingu**, służące do automatycznego pobierania i przetwarzania zawartości stron internetowych. W tym celu przygotowałem skrypty w języku **Python**, korzystając z biblioteki **Scrapy** oraz dodatku **Scrapy-Splash**, uruchamianego w kontenerze **Docker**.

Skrypty automatycznie pobierały artykuły, wyodrębniały ich treść z kodu HTML i zapisywały każdy tekst do osobnego pliku TXT.

W ten sposób zebrałem **573 445 próbek tekstu**, gotowych do dalszego przetwarzania i trenowania modelu.

Portal Fakt publikuje teksty o różnorodnej tematyce, zarówno artykuły informacyjne i publicystyczne, jak i treści **o bardziej tabloidowym charakterze**. Często mają charakter sensacyjny, emocjonalny i zawierają opinie. Można tu znaleźć [m.in](http://m.in). artykuły, recenzje, teksty poradnikowe, horoskopy, czyli **szeroki przekrój tematów**. Teksty tego rodzaju pomagają modelowi osiągnąć **bardziej potoczny styl** oraz rozszerzyć jego wiedzę o świecie z różnych dziedzin.

Późniejsze testy i doświadczenia pokazały, że jest to **jeden z najcenniejszych zasobów tekstów** w całym projekcie, przede wszystkim ze względu na przystępny język, różnorodność gatunków wypowiedzi i bardzo szeroki zakres poruszanych tematów.

Mechanizm działania Scrapy:

![008_web_scraping](https://cdn.hashnode.com/uploads/covers/6af0eebb5f7d123257967ad7/6182692a-53fa-41a2-87db-37962cd19967.png align="center")

c) [**PolsatNews.pl**](https://www.polsatnews.pl/) - do pozyskania tekstów z tego portalu użyłem tej samej metody (**Scrapy, Splash, Docker**).

Wyzwaniem było ustalenie listy adresów URL do pobrania, ponieważ portal jej nie udostępnia. W tym celu przygotowałem w Pythonie **crawler**, wykorzystujący biblioteki **Requests** i **BeautifulSoup**, który automatycznie wyszukiwał linki do artykułów. Zebrane adresy były podstawą do dalszego scrapowania treści z użyciem Scrapy i Splash. W ten sposób zgromadziłem **126 004** próbek tekstu.

Portal Polsatnews to teksty informacyjne **o bieżących wydarzeniach krajowych i zagranicznych**. Styl newsowy, formalny, rzeczowy. Artykuły zawierają wiadomości z oficjalnych sprawdzonych źródeł. Teksty tego rodzaju pomagają modelowi poszerzyć swoją **wiedzę o świecie** i nauczyć się posługiwania neutralnym językiem, ale innym niż styl Wikipedii.

d) [**Pudelek.pl**](https://www.pudelek.pl/) - do pozyskania tekstów wykorzystałem tę samą metodę web scrapingu (**Scrapy, Splash, Docker**). W ten sposób zebrałem **165 229 próbek tekstu**.

Pudelek zawiera **teksty o charakterze plotkarskim** - krótkie artykuły o celebrytach, gwiazdach i show-biznesie. Pisane charakterystycznym językiem pełnym ironii, złośliwości, często grające na emocjach. Teksty tego rodzaju pomagają modelowi w imitowaniu żywego potocznego języka, a 'pudelkowy' styl od razu łatwo rozpoznać.

Pudelek publikuje też charakterystyczne, często kąśliwe, **komentarze użytkowników**. Komentarze zostały pobrane razem z treścią artykułów. Ostatecznie jednak ich nie użyłem, ponieważ pojawiają się na stronie w sposób losowy, przez co model, ucząc się na ich podstawie, nie zachowywał logicznego związku pomiędzy promptem a odpowiedzią.

e) [**Wykop**](https://wykop.pl/) - to popularny portal, na którym użytkownicy dzielą się swoimi internetowymi „znaleziskami”, publikują posty, a inni użytkownicy komentują je i oceniają. W kontekście trenowania modelu uznałem go za cenne źródło **tekstów w stylu komentarzy internetowych**.

Do pozyskania tekstów wykorzystałem **Python** oraz bibliotekę **Selenium** z narzędziem **GeckoDriver**, umożliwiające automatyzację przeglądarki Firefox. W ten sposób zebrałem **1 413 999 próbek tekstów**, obejmujących publikacje wraz z komentarzami użytkowników.

Ostatecznie okazało się, że **wykorzystanie komentarzy internetowych, podobnie jak tych z Pudelka, nie było dobrym wyborem**. Komentarze często zawierały chaotyczne, obraźliwe lub niezwiązane z tematem wypowiedzi, które negatywnie wpływały na jakość odpowiedzi modelu. Dlatego w kolejnych wersjach Toktumi **całkowicie zrezygnowałem z tego źródła danych**.

Mechanizm działania web scrapingu z Selenium:

![009_Selenium](https://cdn.hashnode.com/uploads/covers/6af0eebb5f7d123257967ad7/03e6e65e-85ff-400b-ba02-f6f348ce1198.png align="center")

f) **Dowcipy** - do zbioru uczącego postanowiłem też dodać teksty dowcipów, zebranych z kilkunastu ogólnodostępnych stron internetowych. Był to eksperyment, który miał sprawdzić, czy model nauczy się generować sensowne teksty humorystyczne.

Okazało się jednak, że nauczenie modelu humoru jest znacznie trudniejsze, niż początkowo zakładałem. Wymaga bowiem uchwycenia ironii, gry słów i różnych subtelności językowych. Niespodziewanie jednak **dowcipy pomogły modelowi przyswoić strukturę pytanie-odpowiedź (QA)**, czyli zasadę, że na zadane pytanie należy udzielić odpowiedzi. Tego rodzaju struktura często występuje w dowcipach, podczas gdy pozostałe teksty w zbiorze treningowym na ogół jej nie zawierały.

Po usunięciu duplikatów zebrałem **88 897 dowcipów**.

Zbiór wymagał również **dodatkowego czyszczenia i filtrowania treści obraźliwych lub nieodpowiednich**. Dowcipy często publikowane są bezpośrednio przez internautów, bez odpowiedniej moderacji. Próby usunięcia takich treści nie zawsze przynosiły zadowalające rezultaty. Pokazało to, jak trudne jest przygotowanie wartościowego zbioru treningowego z tekstów pochodzących z otwartych źródeł internetowych.

Do pobierania tekstów wykorzystałem zautomatyzowaną obsługę przeglądarki (**Firefox, GeckoDriver, Selenium i Python**).

g) **Książki** - do zbioru na koniec dodałem teksty **285 książek** znalezionych w sieci, podzielonych na **29 299 fragmentów**. Były to książki typu: **instrukcje obsługi, poradniki, książki popularnonaukowe, reportaże, książki kucharskie** etc. Celem było dostarczenie dodatkowych, bardziej pogłębionych treści.

Okazało się, że do uczenia nie nadaje się beletrystyka i szeroko rozumiana fikcja: utwory teatralne, poezja, a także napisy do filmów. Jak pokazały testy, model wówczas zaczynał posługiwać się zbyt abstrakcyjnym, wyrwanym z kontekstu, niezrozumiałym językiem, lub mówił stylem powieściowym, który utrudnia swobodną konwersację. Dlatego, przy wyborze tekstów z tej kategorii, ograniczyłem się do twórczości non-fiction i utworów komentujących naszą rzeczywistość.

Teksty książek trzeba było podzielić na mniejsze fragmenty, zachowując przy tym **szerszy kontekst wypowiedzi**. W tym celu zastosowałem metodę przesuwających się okien „z zakładką” (**overlapping sliding window**), w której końcówka jednego fragmentu powtarza się na początku kolejnego. Dzięki temu udało się zachować ciągłość tekstu i ograniczyć utratę kontekstu.

Dzielenie danych metodą overlappingu:

![010_overlapping](https://cdn.hashnode.com/uploads/covers/6af0eebb5f7d123257967ad7/36e3f2d5-1835-4be2-97e1-c59cd172ec9f.png align="center")

# 4\. Stworzenie pliku treningowego (training set)

Każdy tekst zapisywałem **do osobnego pliku .txt**. Wybrałem ten format ze względu na jego prostotę — pliki tekstowe można łatwo przeglądać, edytować i czyścić, bez konieczności stosowania dodatkowej struktury danych, np. JSON. Umożliwia to łatwiejsze zapanowanie nad zbiorem danych. Dzięki temu można było np. uporządkować pliki **według rozmiarów** i usunąć te najmniejsze - czyli puste albo nieprzydatne z punktu widzenia uczenia modelu.

Największe pliki .txt, przekraczające **1024 tokenów**, dzieliłem na mniejsze części metodą overlappingu.

Z kolei krótsze teksty tego samego typu (np. dowcipy, krótsze artykuły) łączyłem w większe 'paczki', liczące sobie w przybliżeniu 1024 tokenów, aby zbiór do uczenia był dobrze zbilansowany. Każdy tekst wewnątrz był oddzielony separatorem.

Łączenie tekstów tego samego rodzaju w większe pakiety:

![011_łączenie](https://cdn.hashnode.com/uploads/covers/6af0eebb5f7d123257967ad7/cde712c8-4536-4ead-a639-20c57e6e1ecb.png align="center")

Każdy ze zbiorów danych należało też **oczyścić**. Był to proces bardzo czasochłonny, składający się z wielu etapów, których nie będę w tym miejscu szczegółowo opisywał. Warto jednak podkreślić, że czyszczenie i przygotowanie danych **zajmuje najwięcej czasu - więcej niż samo pozyskiwanie danych i trenowanie modelu**.

Pokazuje to, jak **ogromnych nakładów pracy wymaga ten etap**, który początkowo może wydawać się mniej istotny, a w rzeczywistości jest jednym z najbardziej czasochłonnych i kluczowych elementów całego procesu uczenia modelu. Warto pamiętać o zasadzie **„Garbage In, Garbage Out”** - jakość danych wejściowych ma fundamentalny wpływ na jakość uzyskiwanych wyników.

Kolejnym krokiem była **redukcja zbioru wykorzystywanych znaków** w zebranych tekstach. W tym celu wszystkie teksty przekonwertowałem z kodowania **UTF-8** do **Windows-1250** (do 256 znaków) - i z powrotem (aby zachować ich zgodność z narzędziami wykorzystywanymi podczas treningu). Przy takiej konwersji i dodatkowym czyszczeniu, znaki spoza tego zestawu zostały zastąpione lub usunięte.

Dzięki temu model GPT-2 nie musiał przetwarzać wielu nietypowych znaków, które nie wnosiły istotnej wartości językowej. Ograniczenie liczby wykorzystywanych znaków miało również na celu **usprawnienie działania tokenizera** (w moim przypadku **rozmiar słownika to 50 000 tokenów**).

Redukcja korpusu znaków podczas konwersji z UTF-8 do ANSI:

![012_ansi](https://cdn.hashnode.com/uploads/covers/6af0eebb5f7d123257967ad7/722e19db-0c3b-4c99-acce-c0e9baace3bf.png align="center")

W następnym etapie wszystkie pliki .txt z tekstami połączyłem w **jeden duży zbiór danych treningowych (training set)**, który następnie został użyty do uczenia modelu. Każda z **ponad 3 milionów** próbek tekstu została oddzielona separatorem `<|endoftext|>` - takim samym, jak zastosowany przez Open AI w oryginalnej wersji modelu GPT-2.

Wydzieliłem też mniejszy **zbiór walidacyjny (validation set)** - zawierający dane w tym samym formacie, ale które **nie powtarzają się** w zbiorze treningowym. Wykorzystałem go później do oceny jakości modelu (do pomiaru **perplexity**).

Metoda podziału tekstów w zbiorze treningowym i walidacyjnym:

![013_train_dataset](https://cdn.hashnode.com/uploads/covers/6af0eebb5f7d123257967ad7/29bae01f-ac02-4673-b7ac-ab6488499e11.png align="center")

### ROZMIARY ZBIORU TRENINGOWEGO TOKTUMI 1.0 (bez separatorów):

> Liczba próbek tekstu: **3 115 002**
>
> Liczba słów: **658 078 092**
>
> Liczba znaków: **4 480 020 887**
>
> Liczba tokenów: **978 054 512**
>
> Rozmiar pliku: **4,46 GB**

# 5\. Przygotowanie tokenizera

**Tokenizer (tokenizator)** to zestaw plików i reguł pomagających modelowi przekształcać tekst na tokeny - czyli mniejsze fragmenty, na które dzieli się tekst. To właśnie na tokenach działa model, zamieniając je **na wartości liczbowe**, a na końcu z powrotem na tekst. Właściwe przygotowanie tokenizera ma kluczowe znaczenie dla spójności danych, jakości generowanych odpowiedzi i ogólnej wydajności modelu. Źle przygotowany tokenizer może znacznie pogorszyć proces uczenia i jakość generowanych tekstów.

Zamiast korzystać z gotowego tokenizera, zdecydowałem się przygotować własny, dopasowany do języka polskiego, na podstawie dużej próbki zebranych wcześniej tekstów.

Pliki najczęściej wchodzące w skład tokenizera: **vocab.json, merges.txt, tokenizer_config.json, special_tokens_map.json, tokenizer.json**.

Zawartość pliku vocab.json:

![014_tokenizer](https://cdn.hashnode.com/uploads/covers/6af0eebb5f7d123257967ad7/0408c30e-57a5-4564-b232-70a925f2d4fc.png align="center")

Poniżej kluczowy fragment **kodu użytego do utworzenia tokenizera**.

Przyjąłem rozmiar słownika **vocab_size=50_000**, zbliżony do oryginalnego tokenizera GPT-2, który zawierał 50 257 tokenów we wszystkich wariantach modelu.

Do utworzenia tokenizera wykorzystałem biblioteki **Tokenizers** oraz **Transformers**.

```python
from tokenizers import ByteLevelBPETokenizer
from transformers import GPT2TokenizerFast

# Utworzenie i trening własnego tokenizera BPE
tokenizer = ByteLevelBPETokenizer()

tokenizer.train(
    files="tokenizer_teksty.txt",
    vocab_size=50_000,
    min_frequency=2,
    special_tokens=[
        "<|endoftext|>",
        "<unk>",
        "[PAD]"
    ]
)

# Zapis słownika i reguł BPE
tokenizer_path = "tokenizer_2025_endoftext"
tokenizer.save_model(tokenizer_path)

# Przygotowanie tokenizera do pracy z Transformers
fast_tokenizer = GPT2TokenizerFast(
    vocab_file=f"{tokenizer_path}/vocab.json",
    merges_file=f"{tokenizer_path}/merges.txt",
    unk_token="<unk>",
    pad_token="[PAD]",
    eos_token="<|endoftext|>"
)

fast_tokenizer.save_pretrained(tokenizer_path)
```

Po utworzeniu tokenizera należało sprawdzić, czy działa prawidłowo. W tym celu przeprowadziłem testy poprawności kodowania i dekodowania tekstu oraz zweryfikowałem, czy token specjalny `<|endoftext|>` jest prawidłowo rozpoznawany jako separator. Wszystkie testy zakończyły się pomyślnie.

Proces tokenizacji i jej weryfikacja:

![018_tokenizer](https://cdn.hashnode.com/uploads/covers/6af0eebb5f7d123257967ad7/794bd289-299b-4be9-872f-8af7b57dfe37.gif align="center")

# 6\. Utworzenie pustego modelu GPT-2 Medium

W kolejnym kroku utworzyłem **pusty model GPT-2**, bez wytrenowanych wag. Konfiguracja modelu (liczba warstw, głowic, wymiar osadzeń, długość kontekstu i rozmiar słownika) są dopasowane do **wariantu GPT-2 Medium** oraz wcześniej przygotowanego tokenizera. Jest to punkt wyjścia do trenowania GPT-2 od zera.

```python
from transformers import GPT2LMHeadModel, GPT2Config

# Konfiguracja architektury GPT-2 Medium
config_medium = GPT2Config(
    vocab_size=50_000,  # Rozmiar słownika
    n_positions=1024,   # Maksymalna długość sekwencji
    n_ctx=1024,         # Długość kontekstu
    n_embd=1024,        # Wymiar osadzeń
    n_layer=24,         # Liczba warstw
    n_head=16,          # Liczba głowic atencji
    bos_token_id=0,
    eos_token_id=0,
    pad_token_id=2
)

# Inicjalizacja modelu z losowymi wagami
model_medium = GPT2LMHeadModel(config_medium)

# Zapis modelu i jego konfiguracji
model_medium.save_pretrained("GPT2-MEDIUM-empty_new")
```

# 7\. Trening modelu

Model trenowałem lokalnie, na karcie **NVIDIA GeForce RTX 3060 (12 GB VRAM)**, z wykorzystaniem **Pythona, PyTorch, Transformers i CUDA**. Jednym z pierwszych wyzwań było dobranie kompatybilnych wersji bibliotek i sterowników — niezgodności często powodowały błędy lub uniemożliwiały wykorzystanie GPU.

Po skonfigurowaniu środowiska należy sprawdzić, czy PyTorch prawidłowo wykrywa CUDA:

```python
import torch

print("Czy CUDA dostępne =", torch.cuda.is_available())
```

**Wykorzystanie GPU znacząco przyspiesza trening modelu** w porównaniu z obliczeniami na samym procesorze (CPU).

### UCZENIE - PODSTAWOWE WYZWANIA:

Poniżej fragment logu z przebiegu **treningu**. Wskaźniki po lewej stronie przedstawiają liczbę wykonanych **kroków** oraz łączną liczbę kroków zaplanowanych w całym treningu (**1140000/1771840**), co oznacza, że wykonano **1 140 000** kroków z planowanych **1 771 840**. Po prawej stronie widoczna jest wartość 'epoch': 6.43, która informuje, że model znajduje się w **szóstej epoce**. Cały proces uczenia został zaplanowany na **10 epok**, więc ogólny postęp na tym etapie wynosi około **64%**.

![015_epoki_Toktumi1](https://cdn.hashnode.com/uploads/covers/6af0eebb5f7d123257967ad7/81063391-741a-4241-ac79-3c9e03bb2196.png align="center")

Kluczowe pojęcia pozwalające monitorować postęp uczenia:

- **Kroki (steps)** określają, ile razy zaktualizowano wagi modelu. Jeden krok obejmuje przetworzenie jednej lub kilku partii danych (batchy), obliczenie błędu i aktualizację parametrów modelu.
- **Epoki (epochs)** wskazują, ile razy został przetworzony cały zbiór treningowy. Jedna epoka to jedno pełne przejście przez cały zbiór danych uczących. Zazwyczaj potrzeba wielu epok, aby model osiągnął dobrą jakość predykcji.

Zanim powstał ostateczny kod do uczenia modelu, trzeba było przeprowadzić wiele tur testów na mniejszych zbiorach danych (pliki tekstowe około 50-100 MB). Jednym z poważniejszych wyzwań była odpowiedź na pytanie: **po ilu epokach zakończyć szkolenie?**

Na początku testowałem kod z mechanizmem **early stopping** i dwoma zbiorami, treningowym (training set) i walidacyjnym (validation set). Automatyczne zatrzymanie uczenia następowało **około 7. epoki**. Na tej podstawie przyjąłem bezpieczny limit - maksymalnie 10 epok - jako wartość końcową w docelowym kodzie. Ostatecznie zrezygnowałem z mechanizmu early stopping, ponieważ okazał się mało praktyczny przy trenowaniu na dużym zbiorze danych, w wielu turach, gdy proces musiał być dzielony na mniejsze etapy (wiele razy wstrzymywany i wznawiany).

W ustaleniu najlepszego momentu na zakończenie uczenia bardziej praktyczne okazało się mierzenie **perpleksji (perplexity)**, o czym bardziej szczegółowo piszę w dalszej części.

Poniżej najważniejsza część **kodu wykorzystanego do treningu Toktumi 1.0**, wraz z **konfiguracją hiperparametrów**.

```python
from transformers import (
    GPT2LMHeadModel, GPT2Tokenizer, TextDataset,
    DataCollatorForLanguageModeling, Trainer, TrainingArguments
)

# Model, tokenizer i dane treningowe
model_path = "GPT2-MEDIUM-empty_new"
tokenizer_path = "tokenizer_2025_endoftext"
train_path = "BIG_UCZ_fin_v2.txt"
output_dir = "GPT2-MEDIUM-Toktumi-1"

model = GPT2LMHeadModel.from_pretrained(model_path)
tokenizer = GPT2Tokenizer.from_pretrained(tokenizer_path)

# Dopasowanie tokenizera do modelu
tokenizer.add_special_tokens({"eos_token": "<|endoftext|>"})
model.resize_token_embeddings(len(tokenizer))

# Przygotowanie danych treningowych
train_dataset = TextDataset(
    tokenizer=tokenizer,
    file_path=train_path,
    block_size=230  # Długość sekwencji dopasowana do pamięci GPU
)

data_collator = DataCollatorForLanguageModeling(
    tokenizer=tokenizer,
    mlm=False  # Uczenie przewidywania kolejnego tokenu
)

# Hiperparametry treningu
training_args = TrainingArguments(
    output_dir=output_dir,
    overwrite_output_dir=True,
    num_train_epochs=10,             # Maksymalna liczba epok
    per_device_train_batch_size=8,   # Wielkość batcha
    gradient_accumulation_steps=3,   # Akumulacja gradientów
    learning_rate=5e-5,              # Współczynnik uczenia
    fp16=True,                       # Mieszana precyzja (oszczędność VRAM)
    save_steps=7500,                 # Zapis checkpointu co 7500 kroków
    save_total_limit=300,            # Limit zapisanych checkpointów
    logging_steps=500,               # Logowanie postępu
    save_safetensors=False
)

# Inicjalizacja treningu
trainer = Trainer(
    model=model,
    args=training_args,
    train_dataset=train_dataset,
    data_collator=data_collator
)

# Trening od zera
trainer.train()

# Alternatywnie: wznowienie z ostatniego checkpointu
# trainer.train(resume_from_checkpoint=True)

# Zapis wytrenowanego modelu i tokenizera
model.save_pretrained(output_dir)
tokenizer.save_pretrained(output_dir)
```

W trakcie procesu uczenia niezwykle przydatna okazała się funkcja **automatycznego zapisywania checkpointów** (w tym przypadku **'save_steps=7500'**, czyli co 7500 kroków).

Katalog z checkpointami:

![016_checkpointy](https://cdn.hashnode.com/uploads/covers/6af0eebb5f7d123257967ad7/7888cab7-4086-4eba-b075-638fe2b8bf27.png align="center")

Każdy katalog z checkpointem zawiera pełny stan modelu, a także niezbędne pliki tymczasowe. Dzięki checkpointom możliwe jest **wznowienie treningu dokładnie od miejsca, w którym został zapisany**. Zapisywanie wielu checkpointów wymaga jednak dużo przestrzeni dyskowej.

Jest to szczególnie istotne w przypadku długiego treningu trwającego wiele dni — kiedy mogą wystąpić przerwy w zasilaniu, konieczność ponownego uruchomienia komputera lub inne nieprzewidziane zdarzenia.

### ZAKOŃCZENIE TRENINGU

Ostatecznie, najbardziej optymalnym punktem zakończenia uczenia okazał się **checkpoint-1140000**, czyli około **6,44 epoki**.

Czas treningu: **około 380 godzin (ponad 15 dni)**.

# 8\. Ocena jakości modelu. Perplexity.

Do oceny jakości wytrenowanego modelu GPT-2 Medium wykorzystałem wskaźnik **perplexity (perpleksja)**. Perplexity mierzy, jak dobrze model przewiduje kolejne słowa w sekwencji — **im niższa wartość perpleksji, tym lepsze dopasowanie** modelu do danych.

Model był oceniany z użyciem wydzielonego wcześniej **zbioru walidacyjnego (validation set)** czyli danych, których model wcześniej 'nie widział'. Wynik jest obliczany na podstawie funkcji trainer.evaluate(), która zwraca wartość straty (loss).

```python
perplexity = math.exp(eval_results["eval_loss"])
```

Ze względu na techniczną złożoność tego zagadnienia, skorzystałem z [gotowych rozwiązań](https://www.comet.com/site/blog/perplexity-for-llm-evaluation/).

Zastosowane rozwiązanie działa następująco: kod przechodzi kolejno **przez zapisane checkpointy** i dla wybranych z nich oblicza wartość **perplexity**. Dzięki temu można oszacować, w którym momencie **dalsze uczenie przestaje być opłacalne**.

Jak widać na poniższym wykresie, w pewnym momencie spadek perplexity wyraźnie wyhamował, a dalsze trenowanie oznaczałoby duże zużycie zasobów obliczeniowych przy bardzo niewielkiej poprawie. Na tej podstawie podjąłem decyzję o wcześniejszym zakończeniu treningu, zanim uczenie osiągnęło zaplanowane 10 epok.

![17_perplexity_Toktumi1](https://cdn.hashnode.com/uploads/covers/6af0eebb5f7d123257967ad7/fc1350cc-e1fa-4aeb-98be-fd51c907b8da.png align="center")

# 9\. Ocena jakości modelu. Prompting.

Drugim etapem oceny jakości modelu była **ewaluacja ludzka**, czyli sprawdzenie, jak Toktumi radzi sobie z konkretnymi promptami.

Do testów wykorzystałem **greedy decoding** — metodę, w której model wybiera najbardziej prawdopodobny kolejny token. Dzięki temu na ten sam prompt otrzymywałem zawsze tę samą odpowiedź, co zapewniało stabilny punkt odniesienia przy porównywaniu checkpointów.

Taki efekt można uzyskać, ustawiając `do_sample=False` lub, tak jak w moich testach, `do_sample=True` **i** `top_k=1`. Pozostałe parametry pozwalają kontrolować długość odpowiedzi i ograniczać powtarzanie fragmentów tekstu.

Poniżej kluczowy fragment kodu wykorzystanego do generowania i testowania odpowiedzi Toktumi 1.0.

```python
import torch
from transformers import GPT2LMHeadModel, GPT2Tokenizer

# Wczytanie modelu Toktumi 1.0 i tokenizera
model_path = "GPT2-MEDIUM-Toktumi-1"
model = GPT2LMHeadModel.from_pretrained(model_path)
tokenizer = GPT2Tokenizer.from_pretrained(model_path)

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model.to(device).eval()

# Przykładowy prompt
prompt = "Co to jest grawitacja?"
input_ids = tokenizer.encode(prompt, return_tensors="pt").to(device)

# Parametry generowania odpowiedzi
generate_args = {
    "max_length": 256,            # Maksymalna długość: prompt + odpowiedź
    "min_new_tokens": 2,          # Minimum 2 nowe tokeny
    "no_repeat_ngram_size": 2,    # Blokowanie powtarzających się par tokenów
    "repetition_penalty": 1.1,    # Kara za powtarzanie tokenów
    "do_sample": True,            # Włączony tryb samplingu
    "top_k": 1,                   # Wybór tylko najbardziej prawdopodobnego tokenu
    "pad_token_id": tokenizer.eos_token_id,
    "eos_token_id": tokenizer.eos_token_id
}

# Generowanie i dekodowanie odpowiedzi
with torch.no_grad():
    output = model.generate(input_ids, **generate_args)

generated_text = tokenizer.decode(output[0], skip_special_tokens=True)
print(generated_text)
```

Model udziela najbardziej prawdopodobnej odpowiedzi.

> **Prompt:** *Co to jest grawitacja?*
>
> **Odpowiedź:** *\- To jest siła, która przyciąga przedmioty.*

Aby znaleźć **najlepszy checkpoint** pod względem jakości odpowiedzi na prompty, przetestowałem wybrane checkpointy na zestawie **150 promptów** o różnej strukturze: **dokończenia zdań, pytania i instrukcje**:

- **Dokończenia zdań**, np. „Warszawa jest stolicą...”, „Ziemia krąży wokół...”. Model na ogół prawidłowo odczytywał, że jest to struktura, w której należy dokończyć zdanie. Często też udzielał prawidłowej lub najbardziej „intuicyjnej” odpowiedzi. Tu zdarzało się najmniej błędów.
- **Pytania**, zakończone znakiem zapytania, np. „Co to jest planeta?”, „Do czego służy ołówek?”. Model powinien udzielić odpowiedzi na pytanie - tak jak robi to człowiek. Często prawidłowo odczytywał sytuację pytanie-odpowiedź. Często jednak miał problem zarówno ze zrozumieniem struktury (na pytanie trzeba odpowiedzieć), jak i z faktografią oraz wiedzą o świecie (odpowiadał, ale niepoprawnie).
- **Instrukcje**, np. „Wymień trzy warzywa”, „Podaj przykład pojazdu”. Warto zauważyć, że instrukcja nie jest pytaniem, ale również wymaga odpowiedniej reakcji — w tym przypadku wykonania polecenia lub udzielenia odpowiedzi. Ten typ promptów sprawiał modelowi największą trudność, ponieważ często nie rozpoznawał ich jako formy wymagającej odpowiedzi.

Pełna lista 150 promptów i odpowiedzi modelu Toktumi 1.0 (w konfiguracji 'greedy decoding') -> [**ZOBACZ**](https://huggingface.co/datasets/miczac/toktumi-reports/raw/main/Toktumi_1.0_150_prompts_checkpoint-1140000.txt).

![020_prompty](https://cdn.hashnode.com/uploads/covers/6af0eebb5f7d123257967ad7/870b69c3-5e1e-4c1b-970d-72a89a242abe.png align="center")

# 10\. Zakończenie treningu

Po analizie **perplexity** i wyników testów na **150 promptach**, za najlepszy uznałem **checkpoint-1140000**, zapisany po około **6,44 epoki**.

Dalsze uczenie nie przynosiło już wystarczającej poprawy jakości odpowiedzi, dlatego zdecydowałem się zakończyć trening na tym etapie.

Wybrany checkpoint zawiera zapisane wagi modelu. To on stanowi podstawę finalnej wersji **Toktumi 1.0**.

![021_model_ready](https://cdn.hashnode.com/uploads/covers/6af0eebb5f7d123257967ad7/a9e0b84a-95ab-4893-a73c-49f1f15e6673.png align="center")

# 11\. Końcowa ewaluacja modelu

Pozostała jeszcze **ewaluacja końcowa** i ustalenie, w jakim stopniu model realizuje przyjęte na początku założenia, czyli **czy potrafi prowadzić konwersację na różnorodne tematy, używając stylu mowy potocznej**.

W tym miejscu ograniczę się tylko do najważniejszych spostrzeżeń. Prezentowane poniżej przykłady były generowane w trybie **greedy decoding**.

### a) Rozpoznanie promptu.

**WNIOSKI:** Model często rozpoznaje strukturę promptu i generuje poprawną odpowiedź **pod względem gramatycznym i składniowym**. Używa przy tym **właściwie dobranego stylu**. Co ciekawe, struktury **Pytanie-Odpowiedź** model nauczył się głównie ze zbioru z dowcipami - wiele dowcipów ma formę pytania, po którym następuje odpowiedź (puenta). Dzięki temu model przyswoił prostą zasadę: **gdy pada pytanie, należy na nie odpowiedzieć**.

> **Prompt:** *Co to jest grawitacja?*
>
> **Odpowiedź:** *\- To jest siła, która przyciąga przedmioty.*

> **Prompt:** *Co robi kucharz?*
>
> **Odpowiedź:** *\- Smaży kotlety.*

> **Prompt:** *Warzywa są zdrowe, bo*
>
> **Odpowiedź:** *Warzywa są zdrowe, bo zawierają dużo witamin i minerałów.*

### b) Podstawowa wiedza o świecie.

**Gdy prompt jest stosunkowo prosty**, dotyczy powszechnie znanych faktów i nie wymaga złożonego rozumowania, **model często udziela poprawnej odpowiedzi**. Posiada więc **ogólną wiedzę o świecie** i znajomość **podstawowych faktów**.

Najlepiej radzi sobie z promptami wymagającymi **dokończenia zdania** - ta forma jest dla niego najprostsza do zdekodowania.

> **Prompt:** *Stolicą Francji jest*
>
> **Odpowiedź:** *\- Stolicą Francji jest Paryż.*

> **Prompt:** *Najwyższą górą na świecie jest*
>
> **Odpowiedź:** *Najwyższą górą na świecie jest Mount Everest, który ma wysokość 720 metrów.*

(choć myli się podając wysokość)

> **Prompt:** *Najdłuższą rzeką w Polsce jest*
>
> **Odpowiedź:** *Najdłuższą rzeką w Polsce jest rzeka Wisła.*

### c) Błędne odpowiedzi.

Zdarza się, że model generuje odpowiedź o poprawnej strukturze, lecz niezgodną z prawdą — sprawiając wrażenie, jakby „zmyślał”. Zjawisko to określa się terminem **halucynacji**. Oznacza ono sytuację, w której model przedstawia nieprawdziwe lub wymyślone informacje tak, jakby były faktami.

Jak pokazują testy, halucynacje częściej pojawiają się w **słabiej wytrenowanych modelach**, zwłaszcza na wczesnych etapach uczenia. Trochę przypomina to **zachowanie człowieka, który nie zna odpowiedzi, ale mimo to próbuje ją odgadnąć**.

> **Prompt:** *Do czego służy ołówek?*
>
> **Odpowiedź:** *\- Do czyszczenia akwariów.*

> **Prompt:** *Z czego robi się mąkę?*
>
> **Odpowiedź:** *\- Z proszku do pieczenia.*

> **Prompt:** *Najszybszym zwierzęciem na lądzie jest*
>
> **Odpowiedź:** *Najszybszym zwierzęciem na lądzie jest koń.*

### d) Brak zrozumienia.

Czasem model niewłaściwie odczytuje strukturę pytania albo udziela odpowiedzi wewnętrznie sprzecznej.

> **Prompt:** *Księżyc świeci w*
>
> **Odpowiedź:** *Księżyc świeci w nocy, a księżyc świeci nocą.*

> **Prompt:** *Co to jest ogień?*
>
> **Odpowiedź:** *\- To jest taki mały, biały, czerwony, świecący, czarny, żółty, zielony, niebieski, pomarańczowy, brązowy, fioletowy.*

> **Prompt:** *Jakie jedzenie przygotowujesz, kiedy masz gości?*
>
> **Odpowiedź:** *\- Na początku tygodnia.*

Dodatkowo: gdy prompt zawiera **instrukcje**, model bardzo często w ogóle **nie potrafi rozpoznać oczekiwanego stylu odpowiedzi**.

> **Prompt:** *Podziel się swoimi planami na weekend.*
>
> **Odpowiedź:** *Horoskop dzienny - Lew. Dzisiejszy dzień przyniesie Ci wiele możliwości do rozwoju osobistego i zawodowego...*

(nieoczekiwanie odpowiada w stylu horoskopu)

> **Prompt:** *Wymień dwie przyprawy.*
>
> **Odpowiedź:** *Wymień dwie przyprawy. To proste!*

(cytuje fragment artykułu z Pudelka lub Faktu)

### e) Sekcja 'Komentarz:'

Jak wspomniałem wcześniej, teksty z serwisu '[Wykop.pl](http://Wykop.pl)' zostały dołączone do zbioru treningowego Toktumi 1.0 razem z komentarzami. Był to najliczniejszy zbiór tekstów w zbiorze treningowym (**1 413 999 próbek**).

Testy pokazały, że komentarze internetowe znacząco pogorszyły jakość generowanych odpowiedzi. Rzadko były na temat, a dodatkowo - pomimo moderacji - nadal zawierały **oceniający, obraźliwy język, powielały szkodliwe stereotypy, a nawet zawierały teorie spiskowe**.

Model nie tyle błędnie interpretuje polecenie, ile **naśladuje styl komentarzy internetowych** — często chaotyczny, oceniający i nieodpowiedni dla roli przyjaznego asystenta.

> **Prompt:** *Jakie znasz środki transportu?*
>
> **Odpowiedź:** *(...)*
>
> **Komentarz:** *Nie znam, ale się wypowiem.*

> **Prompt:** *Co sądzisz o podróżach po Europie?*
>
> **Odpowiedź:** *(...)*
>
> **Komentarz:** *Nie wiem, czy to dobry pomysł. W Polsce jest dużo taniej niż w Niemczech.*

> **Prompt:** *Ziemia krąży wokół*
>
> **Odpowiedź:** *Ziemia krąży wokół Słońca.*
>
> **Komentarz:** *To jest niesamowite, że w XXI wieku ludzie nadal wierzą w takie bzdury. (...) To tak jakby wierzyć, iż Ziemia krąży dookoła Słońca i jest okrągła. Albo, żeby wierzyć w płaską ziemię. No ale przecież ziemia jest płaska, więc ...*

# 12\. Rekomendacje i dalsze kroki

Na podstawie zebranych obserwacji opracowałem krótki zestaw rekomendacji dotyczących dalszego rozwoju modelu. **Dwie pierwsze zrealizowałem podczas tworzenia kolejnej wersji modelu — Toktumi 2.0.**

### REKOMENDACJE:

1. **Usunięcie komentarzy internetowych** ze zbioru treningowego.
2. Przeprowadzanie **fine-tuningu** modelu (**lub ponownego treningu** od zera) na zbiorach typu **pytanie-odpowiedź (QA)** oraz **instrukcja-odpowiedź (instruction fine-tuning)**.
3. Zastosowanie **zaawansowanych technik czyszczenia tekstu**, skuteczniejsze **filtrowanie treści** niepożądanych i szkodliwych.
4. Rozważenie użycia modelu GPT-2 **o większej liczbie parametrów**: **GPT-2 Large (762M)** albo **GPT-2 XL (1,5B)**. Model GPT-2 Medium może mieć zbyt małą pojemność oraz ograniczony mechanizm uwagi (attention), aby skutecznie gromadzić wiedzę i generować bardziej złożone wypowiedzi. Potencjalną barierą pozostają tu jednak ograniczenia sprzętowe.
5. Użycie **modelu o nowszej architekturze**, oferującej większe możliwości niż GPT-2. Barierą mogą być jednak ograniczenia sprzętowe oraz wysokie koszty treningu tych bardziej zaawansowanych modeli.

## Model Toktumi 1.0 w Hugging Face Spaces

Działanie **Toktumi 1.0** można przetestować na platformie **Hugging Face Spaces**:

[**https://huggingface.co/spaces/miczac/toktumi-1.0-polish-gpt2**](https://huggingface.co/spaces/miczac/toktumi-1.0-polish-gpt2)

![51_T1_interface](https://cdn.hashnode.com/uploads/covers/6af0eebb5f7d123257967ad7/fe99cff4-72fc-415d-87e0-f348ef2b9bc6.png align="center")

[**NASTĘPNA CZĘŚĆ**](https://sixfingers.hashnode.dev/toktumi-2-pl)
