GPT od podstaw: Jak działa sztuczna inteligencja, która pisze lepsze maile niż Ty?
Odkryj, jak działa GPT: od imion po mechanizm uwagi. Poznaj architekturę AI w 150 liniach kodu Pythona. Przewodnik dla programistów i ciekawych świata.
Prolog: Słowa, które rodzą się z chaosu
Muszę Ci się do czegoś przyznać. Kiedy pierwszy raz zobaczyłem możliwości ChatGPT, poczułem coś pomiędzy podziwem a niepokojem. Jak maszyna - układ krzemu, miedzi i elektryczności - potrafi składać słowa w coś, co brzmi… ludzko?
Okazuje się, że odpowiedź jest jednocześnie rozczarowująco prosta i oszałamiająco piękna. I kryje się w pewnym kodzie, który pewien genialny facet zmieścił w niecałych 200 liniach.
Andrej Karpathy to jedno z największych nazwisk w świecie sztucznej inteligencji. Pracował w OpenAI (firma od ChatGPT), kierował zespołem AI w Tesli, a w sieci tłumaczy zaawansowaną matematykę tak, że rozumieją ją nawet humaniści po trzech kawach. Pewnego dnia napisał działającą, minimalną implementację architektury GPT - silnika stojącego za ChatGPT - w czystym Pythonie, bez PyTorch, TensorFlow ani żadnej innej biblioteki do uczenia maszynowego.
Co to znaczy „bez gotowych bibliotek"? Wyobraź sobie, że zamiast kupić samochód w salonie, budujesz go od zera. Sam odlewasz silnik, sam wyginasz blachę, sam montujesz hamulce. Nie dlatego, że to praktyczne, ale dlatego, że wtedy naprawdę rozumiesz, jak samochód działa. Właśnie to zrobił Karpathy z GPT.
I właśnie o tym kodzie chcę Ci dziś opowiedzieć. Bez żargonu. Bez udawania, że to proste. Ale z obietnicą, że na końcu zrozumiesz więcej o AI niż 95% ludzi, którzy o niej dyskutują w internecie.
Wyobraź sobie, że stoisz przed ogromną biblioteką. Miliony książek, miliardy słów ułożonych w nieskończone sekwencje. Twoim zadaniem jest przewidzieć, jakie słowo pojawi się jako następne. Brzmi niemożliwie? A jednak to właśnie robią modele językowe takie jak GPT - i to robią z zadziwiającą precyzją.
Rozdział pierwszy: Ziarno w oceanie losowości
Każdy program komputerowy, który ma cokolwiek wspólnego z losowością, potrzebuje czegoś, co informatycy nazywają ziarnem losowości. Brzmi poetycko, prawda? W rzeczywistości chodzi o coś prozaicznego, ale sprytnego.
Komputery tak naprawdę nie potrafią być losowe. Udają. Używają skomplikowanych wzorów matematycznych, które generują ciągi liczb wyglądające na przypadkowe. Ale jeśli podasz im ten sam punkt startowy - to samo ziarno - wygenerują dokładnie ten sam ciąg. Za każdym razem.
Karpathy ustawia ziarno na 42 (fani Autostopem przez Galaktykę wiedzą, o co kaman). Dzięki temu każdy, kto uruchomi jego kod, dostanie identyczne wyniki. To jak przepis na ciasto - jeśli użyjesz tych samych składników w tych samych proporcjach, dostaniesz to samo ciasto. Niezbędne, kiedy chcesz badać proces uczenia się maszyny, a nie tylko się nim zachwycać.
Rozdział drugi: Księga imion
OK, ale czego konkretnie ten model się uczy? Otóż czegoś banalnego: wymyślania imion. Ma listę prawdziwych imion i na ich podstawie uczy się generować nowe - takie, które brzmią jak prawdziwe, ale nigdy nie istniały.
Żeby to zrobić, musi najpierw zamienić litery na liczby. Komputer nie rozumie liter - rozumie tylko liczby. Więc każda litera dostaje swój numer: ‘a’ to 0, ‘b’ to 1, ‘c’ to 2 i tak dalej.
Ale potrzebujemy jeszcze jednego specjalnego znaku - “klamry”, która mówi modelowi “tu zaczyna się nowe imię” i “tu się kończy”. Wyobraź sobie, że czytasz książkę, w której nie ma spacji ani kropek. Skąd wiesz, gdzie kończy się jedno zdanie, a zaczyna drugie? Właśnie od tego jest ten specjalny token - w kodzie nazywa się BOS, od angielskiego Beginning of Sequence, czyli „początek ciągu".
W tej konkretnej implementacji nie ma osobnego tokenu końca (EOS) - ten sam BOS pełni obie role. To trochę tak, jakby drzwi do pokoju działały w obie strony: wchodzisz przez nie i wychodzisz. Takie drzwi wejściowo-wyjściowe.
Kiedy model widzi ten specjalny znak, wie, że ma zacząć generować nowe imię. Kiedy sam go wygeneruje, wie, że skończył swoje dzieło. Proste? I tak, i nie. Bo między tymi dwoma znakami dzieje się cała magia.
Rozdział trzeci: Jak komputer liczy, żeby się uczyć
Teraz dochodzimy do sedna. Sieć neuronowa - serce każdego modelu AI - to w gruncie rzeczy gigantyczny kalkulator. Ale kalkulator z pamięcią.
W kodzie Karpathy’ego istnieje coś o nazwie Value (po angielsku: „wartość"). To maleńki klocek - atom obliczeń. Każdy taki klocek pamięta cztery rzeczy:
- swoją wartość - wynik obliczeń, jakaś liczba,
- swój gradient - zaraz wyjaśnię, co to,
- swoich rodziców - z jakich innych klocków powstał,
- wrażliwość - jak bardzo zmiana każdego rodzica wpłynęłaby na wynik.
„Gradient" brzmi groźnie, ale to naprawdę proste. Wyobraź sobie, że stoisz na wzgórzu we mgle. Nie widzisz szczytu, nie widzisz doliny. Ale czujesz, że grunt pod lewą nogą jest niżej niż pod prawą. Gradient to właśnie ta informacja: „w którą stronę nachylony jest teren i jak bardzo". Maszyna używa gradientów, żeby wiedzieć, w którą stronę „iść", żeby jej wyniki były coraz lepsze.
Weźmy przykład. Model mnoży 3 razy 5 i dostaje 15. Teraz pytamy: co by się stało, gdybyśmy delikatnie podkręcili jedną z tych liczb? Gdyby trójka wzrosła do 3.01, wynik skoczyłby do 15.05 – bo ta mała zmiana została pomnożona przez 5. Gdyby to piątka wzrosła do 5.01, wynik wyniósłby 15.03 – bo zmiana została pomnożona przez 3. Te „mnożniki wpływu" to właśnie gradienty. Każdy element w sieci zna swój mnożnik – wie, jak mocno jego zmiana odbije się na końcowym wyniku.
Albo jeszcze prościej. Wyobraź sobie, że pieczesz ciasto. Wyszło za słone. Chcesz wiedzieć: co zmienić, żeby następne było lepsze? Gradient to odpowiedź na to pytanie dla każdego składnika z osobna. „Soli było za dużo – zmniejsz o łyżeczkę. Cukru było w sam raz – nie ruszaj. Masła było odrobinę za mało – dodaj 10 gramów." Model robi dokładnie to samo: po każdej próbie sprawdza, który parametr i o ile powinien się zmienić, żeby wynik był bliżej ideału.
Miliardy takich klocków, połączonych w warstwy - mnożenia, dodawania, specjalne funkcje - tworzą coś, co zaczyna przypominać rozumienie wzorców w danych.
Rozdział czwarty: Uczenie się przez błędy
Jak ten cały system się uczy? Metoda jest genialnie prosta:
- Model próbuje zgadnąć następną literę.
- Sprawdzamy, czy trafił.
- Jeśli nie trafił (a na początku prawie nigdy nie trafia), wysyłamy informację wstecz przez całą sieć: „hej, pomyliłeś się, oto jak bardzo i w którą stronę".
- Każdy klocek w sieci lekko koryguje swoją wartość.
- Powtarzamy od punktu 1.
To się nazywa propagacja wsteczna (z angielskiego backpropagation) i jest to prawdopodobnie najważniejszy algorytm w historii sztucznej inteligencji. Cała idea polega na tym, że informacja o błędzie wędruje od końca sieci do jej początku, po drodze mówiąc każdemu parametrowi: „musisz się zmienić o tyle i tyle".
Model Karpathy’ego robi to tysiąc razy - a każda runda to jedno imię (jeden dokument treningowy). Duże modele powtarzają ten proces miliony razy, na tysiącach kart graficznych, przez tygodnie. Ale zasada jest ta sama.
Na początku model generuje kompletny bełkot - losowe ciągi liter. Ale z każdym krokiem zaczyna łapać wzorce. „O, po literze ’m’ często jest ‘a’. Po ‘ka’ zwykle następuje ‘r’ albo ’t’. Imiona kończą się na ‘a’, ’n’ albo ’ek’." To nie jest rozumienie w ludzkim sensie. To statystyka. Ale efekt jest zadziwiająco podobny.
Rozdział piąty: Mechanizm uwagi, czyli dlaczego Transformer zmienił wszystko
Teraz najciekawsza część. GPT to skrót od Generative Pre-trained Transformer. Dwa pierwsze słowa to „generatywny" (bo generuje tekst) i „wstępnie wytrenowany" (bo uczy się na ogromnych zbiorach danych, zanim zaczniemy go do czegokolwiek używać). Ale to trzecie słowo - Transformer - jest kluczowe. To nazwa architektury, która w 2017 roku wywróciła świat AI do góry nogami.
Serce Transformera to coś, co nazywa się mechanizmem uwagi (po angielsku attention mechanism). I tu muszę użyć analogii, bo bez niej to nie ma sensu.
Wyobraź sobie, że czytasz zdanie: „Marek poszedł do sklepu, bo chciał kupić mleko." Kiedy docierasz do słowa „chciał", Twój mózg automatycznie wie, że chodzi o Marka. Nie o sklep, nie o mleko - o Marka. Skąd? Bo zwróciłeś uwagę na wcześniejsze słowo.
Mechanizm uwagi w Transformerze robi dokładnie to samo, tylko matematycznie. Każda litera (albo słowo, zależy od modelu) generuje trzy rzeczy:
- Klucz - coś jak etykietka: „oto, czym mogę być przydatna".
- Wartość - coś jak zawartość paczki: „oto, jaką informację ze sobą niosę".
- Zapytanie - coś jak pytanie: „czego teraz szukam?".
Kiedy model przetwarza nową literę, jej „zapytanie" jest porównywane ze wszystkimi „kluczami" liter, które już widział. Im bardziej klucz pasuje do zapytania, tym więcej uwagi model poświęca danej literze. To jak bibliotekarz, który dla każdego pytania błyskawicznie przegląda cały katalog i wybiera najważniejsze pozycje.
I tu jest piękny trik w kodzie Karpathy’ego. W produkcyjnych modelach AI mechanizm uwagi musi mieć specjalną maskę, która zabrania modelowi „podglądania w przyszłość" - bo gdyby podczas nauki widział odpowiedzi, niczego by się nie nauczył (tak jak student, który ma otwarty zeszyt z notatkami na egzaminie). Karpathy elegancko omija ten problem: jego kod przetwarza litery po kolei, jedną za drugą - zarówno podczas treningu, jak i generowania - więc przyszłe litery dosłownie jeszcze nie istnieją w pamięci. Maska jest niepotrzebna, bo przyszłość jeszcze się nie wydarzyła. Proste? Proste.
Rozdział szósty: Narodziny nowych imion
Po tysiącu rund treningowych model jest gotowy do tworzenia. I generuje imiona, których nigdy nie widział - takie, które brzmią prawdziwie, ale nigdy nie istniały.
Nie istnieją, ale mają właściwą strukturę: zaczynają się od spółgłosek, mają samogłoski w środku, kończą się charakterystycznymi końcówkami. Model nie zapamiętał listy - nauczył się stylu.
To jest esencja sztucznej inteligencji generatywnej. Nie kopiowanie. Tworzenie czegoś nowego na podstawie wzorców wyciągniętych z danych.
Jest jeszcze jeden fajny parametr, który kontroluje „osobowość" modelu: temperatura. Niska temperatura (powiedzmy 0.3) sprawia, że model jest ostrożny - wybiera najbardziej prawdopodobne litery, generuje bezpieczne, przewidywalne imiona. Wysoka temperatura (powiedzmy 1.5) daje mu więcej swobody - pojawia się kreatywność, ale też dziwactwa. To jak suwak między rzemieślnikiem a artystą awangardowym. Albo, jeśli wolisz, między księgowym a poetą po trzecim piwie.
Technicznie to działa tak: przed podjęciem decyzji „jaką literę wybrać", model dzieli swoje surowe „oceny" (zwane logitami) przez wartość temperatury. Dzielenie przez małą liczbę wyostrza różnice - faworyt jeszcze bardziej dominuje. Dzielenie przez dużą liczbę je spłaszcza - mniej prawdopodobne opcje dostają szansę.
Rozdział siódmy: 200 linii kontra 175 miliardów
I tu muszę powiedzieć rzecz, która chyba najbardziej mnie zaskakuje. Kod Karpathy’ego ma poniżej 200 linii. Model GPT-3, miał 175 miliardów parametrów i trenował na tysiącach kart graficznych przez miesiące.
Ale zasada działania? Ta sama.
Te same mechanizmy. Ta sama matematyka. Ten sam fundamentalny pomysł: weź dane, znajdź w nich wzorce, naucz się przewidywać, co dalej, poprawiaj się na podstawie błędów. Różnica to skala - jak między budką z lemoniadą a Coca-Colą. Biznes ten sam: rób napoje, które ludzie chcą pić. Tylko rozmach inny.
Możesz uruchomić kod Karpathy’ego na swoim laptopie. Dzisiaj. Za darmo. Możesz patrzeć, jak model uczy się w czasie rzeczywistym, zmieniać parametry, eksperymentować. To jest prawdziwa demokratyzacja sztucznej inteligencji - nie jako mistycznej czarnej skrzynki, której trzeba ufać na słowo, ale jako zrozumiałego mechanizmu, który można rozebrać na części i zbadać.
Ciekawostka dla dociekliwych: Dlaczego ten kod nie potrzebuje karty graficznej?
Profesjonalne biblioteki jak PyTorch operują na tensorach – wielowymiarowych tablicach liczb. Kod Karpathy’ego operuje na skalarach – pojedynczych liczbach opakowywanych w obiekty Value.
- Skalary (ten kod): Każda operacja (mnożenie, dodawanie) tworzy nowy obiekt Pythona z historią obliczeń i pochodnymi. To jak budowanie zamku z pojedynczych ziarenek piasku – precyzyjne, edukacyjne, ale strasznie powolne. Wąskim gardłem jest sam Python: tysiące drobnych obiektów, zarządzanie pamięcią, sprawdzanie typów.
- Tensory (produkcja): Zamiast mnożyć 1000 par liczb jedna po drugiej, GPU dostaje dwa wielkie bloki danych i wykonuje 1000 mnożeń jednocześnie. Karta graficzna ma tysiące małych rdzeni zaprojektowanych właśnie do takich równoległych operacji – stąd jej przewaga nad CPU, który ma kilka mocnych, ale sekwencyjnych rdzeni.
Kod Karpathy’ego wykonuje tysiące operacji na sekundę w czystym Pythonie. Produkcyjne modele wykonują miliardy operacji na sekundę, bo całą „brudną robotę" zrzucają na GPU. Ale matematyka pod spodem? Identyczna.
Epilog: To co, ta AI rozumie czy nie rozumie?
To pytanie za milion dolarów. I uczciwa odpowiedź brzmi: nie wiemy. Model nie „rozumie" imion tak, jak Ty je rozumiesz. Nie wie, że wygenerowane przez niego imię brzmi trochę jak imię greckiego filozofa, a inne mogłoby być bohaterką powieści fantasy. Dla niego to rozkłady prawdopodobieństwa - matematyczne wzorce.
Ale efekt jest taki, jakby rozumiał. A może to wystarczy? Kiedy kalkulator „rozumie" dodawanie, nie zastanawiamy się nad głębią jego wewnętrznego przeżycia. Po prostu działa.
200 linii kodu. Kilkadziesiąt liter alfabetu. Tysiąc rund treningowych. I maszyna, która wymyśla imiona, jakich nikt nigdy nie nosił, ale które brzmią, jakby ktoś mógł.
Jeśli to nie jest fascynujące, to nie wiem, co jest.
Część II: Dla programistów – Architektura GPT od kuchni
O czym jest ta sekcja
Link do repozytorium: gist Karpathy’ego
Kod Andreja Karpathy’ego to minimalny, ale kompletny pipeline modelu językowego: tokenizacja → trening → generowanie tekstu. Wszystko w czystym Pythonie, bez PyTorch, bez TensorFlow, bez NumPy. Poniżej przejdziemy przez każdy element kodu i wyjaśnimy nie tylko jak działa, ale dlaczego został zaprojektowany w ten sposób.
1. Tokenizacja: zamiana liter na liczby
uchars = sorted(set(''.join(docs))) # unikalne znaki: ['a', 'b', ..., 'z']
BOS = len(uchars) # specjalny token "początek/koniec"
vocab_size = len(uchars) + 1 # rozmiar słownika
Komputer nie rozumie liter – rozumie liczby. Tokenizacja to mapowanie: 'a' → 0, 'b' → 1, itd. Tu użyto najprostszej możliwej strategii: każdy znak to osobny token (character-level tokenization).
Produkcyjne modele jak GPT-2 używają tokenizacji BPE (Byte Pair Encoding), gdzie częste sekwencje znaków (np. „the", „ing") stają się jednym tokenem. To efektywniejsze, ale trudniejsze do zaimplementowania od zera.
Token BOS (Beginning of Sequence) pełni podwójną rolę – otacza każde imię z obu stron. Nie ma osobnego tokenu końca (EOS) – jeden token służy jako „drzwi wejściowo-wyjściowe":
tokens = [BOS] + [uchars.index(ch) for ch in doc] + [BOS]
Model widzi BOS na początku i wie, że zaczyna się nowe imię. Kiedy sam wygeneruje BOS, wie, że skończył. W bardziej rozbudowanych systemach to dwa osobne tokeny, ale tu jeden wystarcza.
2. Autograd: automatyczne liczenie gradientów
To jest serce całego uczenia maszynowego w tym kodzie.
class Value:
__slots__ = ('data', 'grad', '_children', '_local_grads')
def __init__(self, data, children=(), local_grads=()):
self.data = data # wartość liczbowa
self.grad = 0 # gradient (wypełniany w backward pass)
self._children = children # z jakich wartości powstałem
self._local_grads = local_grads # pochodne lokalne
Czym jest Value? To opakowanie na liczbę, które pamięta swoją historię. Kiedy napiszesz a * b, wynikiem nie jest zwykła liczba – to nowy obiekt Value, który wie, że powstał z a i b, i zna pochodne cząstkowe tej operacji.
Po co to? Żeby automatycznie obliczyć gradienty. Gradient mówi: „gdybym zmienił tę wartość o odrobinę, jak zmieniłby się końcowy wynik (loss)?". Bez tego musielibyśmy ręcznie pisać wzory na pochodne dla każdej operacji w sieci – przy setkach parametrów byłoby to niewykonalne.
Każda operacja definiuje swoje pochodne lokalne. Na przykład mnożenie a * b:
def __mul__(self, other):
other = other if isinstance(other, Value) else Value(other)
return Value(self.data * other.data, (self, other), (other.data, self.data))
# ↑ d(a*b)/da = b, d(a*b)/db = a
Jak działa backward()?
def backward(self):
# 1. Sortowanie topologiczne (DFS) – ustalenie kolejności
topo = []
visited = set()
def build_topo(v):
if v not in visited:
visited.add(v)
for child in v._children:
build_topo(child)
topo.append(v)
build_topo(self)
# 2. Propagacja gradientów od końca (loss) do początku (parametry)
self.grad = 1
for v in reversed(topo):
for child, local_grad in zip(v._children, v._local_grads):
child.grad += local_grad * v.grad
Buduje graf obliczeniowy (kto od kogo zależy), a potem idzie od końca (loss) do początku (parametry), mnożąc gradienty po drodze. To jest reguła łańcuchowa (chain rule) z analizy matematycznej, zaimplementowana automatycznie.
Jeśli znasz PyTorch: Value to uproszczona wersja torch.Tensor z requires_grad=True, a backward() to odpowiednik loss.backward().
3. Architektura modelu
Model jest inspirowany GPT-2, z kilkoma uproszczeniami:
| Co | GPT-2 | Ten kod | Dlaczego zmiana |
|---|---|---|---|
| Normalizacja | LayerNorm | RMSNorm (bez uczonych parametrów) | Prostsza (bez średniej i parametrów gamma/beta) |
| Aktywacja | GeLU | ReLU | Prostsza (max(0, x) vs skomplikowany wzór) |
| Bias | Tak | Nie | Mniej parametrów |
| Final LayerNorm | Tak (ln_f przed warstwą wyjściową) |
Nie | Uproszczenie |
| Tokenizacja | BPE (~50k tokenów) | Character-level (~27 tokenów) | Uproszczenie |
| Dropout | Tak | Nie | Uproszczenie |
| Warstwy | 12+ | 1 | Uproszczenie |
Embeddingi: nadawanie znaczenia tokenom i pozycjom
tok_emb = state_dict['wte'][token_id] # "co to za litera?" → wektor 16 liczb
pos_emb = state_dict['wpe'][pos_id] # "na której pozycji?" → wektor 16 liczb
x = [t + p for t, p in zip(tok_emb, pos_emb)] # suma obu
Każdy token i każda pozycja mają swój wektor (listę liczb). Token embedding uczy się, że np. 'a' i 'e' mają coś wspólnego (obie są samogłoskami). Pozycyjny embedding uczy się, że pierwsza litera imienia rządzi się innymi regułami niż ostatnia.
Te wektory to parametry modelu – na początku losowe, stopniowo dostrajane przez trening.
Kontekst historyczny: oryginalny Transformer z 2017 roku („Attention Is All You Need") kodował pozycje za pomocą stałych funkcji sinus/cosinus. GPT-2 przeszedł na uczone wektory pozycji – i właśnie to podejście widzimy tu (state_dict['wpe']).
Warto zauważyć, że po zsumowaniu embeddingów stosowana jest normalizacja RMSNorm, jeszcze przed wejściem do warstw transformera. Może się to wydawać nadmiarowe (bo RMSNorm jest też na początku każdego bloku), ale ma sens dzięki połączeniom rezydualnym – gradient przepływa inną ścieżką.
RMSNorm: stabilizacja wartości
def rmsnorm(x):
ms = sum(xi * xi for xi in x) / len(x) # średnia kwadratów
scale = (ms + 1e-5) ** -0.5 # 1 / sqrt(średnia kwadratów)
return [xi * scale for xi in x] # skalowanie
Normalizacja zapobiega temu, żeby wartości w sieci stawały się zbyt duże lub zbyt małe w trakcie treningu (tzw. eksplozja/zanikanie gradientów). RMSNorm dzieli każdy element wektora przez jego normę RMS.
To prostsza wersja LayerNorm – nie odejmuje średniej i nie ma uczonych parametrów gamma/beta. W standardowej implementacji RMSNorm posiada uczony parametr skali (gamma), ale ta implementacja go pomija dla prostoty.
Multi-head Attention: serce Transformera
To najważniejszy fragment kodu. Rozbijmy go na kroki:
# Dla każdego tokenu obliczamy trzy wektory:
q = linear(x, state_dict[f'layer{li}.attn_wq']) # query: "czego szukam?"
k = linear(x, state_dict[f'layer{li}.attn_wk']) # key: "czym jestem?"
v = linear(x, state_dict[f'layer{li}.attn_wv']) # value: "co niosę za informację?"
Model ma 4 head attention (n_head = 4), każda o wymiarze 4 (head_dim = 16 / 4). Każdy jead uczy się zwracać uwagę na inne wzorce – np. jeden może śledzić samogłoski, inny spółgłoski, jeszcze inny pozycje w słowie.
Dla każdego head’a:
# 1. Porównaj query z wszystkimi dotychczasowymi keys (iloczyn skalarny)
attn_logits = [sum(q_h[j] * k_h[t][j] for j in range(head_dim)) / head_dim**0.5
for t in range(len(k_h))]
# 2. Zamień na prawdopodobieństwa (softmax – suma = 1)
attn_weights = softmax(attn_logits)
# 3. Policz ważoną sumę values
head_out = [sum(attn_weights[t] * v_h[t][j] for t in range(len(v_h)))
for j in range(head_dim)]
Dzielenie przez sqrt(head_dim) to scaled dot-product attention – bez tego iloczyny skalarne byłyby zbyt duże dla softmaxa, co prowadziłoby do prawie binarnych wag (cała uwaga na jednym tokenie).
Kluczowy trick: niejawna maska przyczynowa (implicit causal mask). W produkcyjnych Transformerach attention jest obliczany równolegle dla wszystkich pozycji naraz (wydajne, ale wymaga jawnego maskowania przyszłych tokenów – żeby model nie „podglądał odpowiedzi"). Tu problem nie istnieje – kod przetwarza tokeny po kolei (zarówno w treningu, jak i przy generowaniu), a klucze i wartości są dodawane do list przyrostowo:
keys[li].append(k) # dodaj klucz bieżącego tokenu
values[li].append(v) # dodaj wartość bieżącego tokenu
W momencie przetwarzania tokenu na pozycji 5, listy keys i values zawierają tylko tokeny 0–5. Tokeny 6, 7, 8… jeszcze nie istnieją w pamięci. Maska jest niepotrzebna, bo przyszłość dosłownie jeszcze się nie wydarzyła. To potężne uproszczenie dydaktyczne, które jednocześnie daje poprawne wyniki.
Bonus: ten mechanizm to de facto KV-cache – technika, którą produkcyjne modele stosują, żeby przyspieszyć generowanie. Zamiast przeliczać klucze i wartości starych tokenów od nowa przy każdym kroku, przechowujemy je w pamięci.
Residual connections: skrót dla gradientów
x = [a + b for a, b in zip(x, x_residual)]
Po bloku attention i po bloku MLP wynik jest dodawany do wejścia (a nie zastępuje je). Dzięki temu gradienty mają „autostradę" – mogą przepływać bezpośrednio przez dodawanie, omijając skomplikowane transformacje. Bez tego trenowanie głębokich sieci byłoby praktycznie niemożliwe.
Jeśli znasz ResNety – to dokładnie ten sam pomysł.
Blok MLP: prosta sieć feedforward
x = linear(x, state_dict[f'layer{li}.mlp_fc1']) # 16 → 64 (rozszerzenie 4x)
x = [xi.relu() for xi in x] # nieliniowość
x = linear(x, state_dict[f'layer{li}.mlp_fc2']) # 64 → 16 (powrót)
Po attention każdy token przechodzi przez dwuwarstwowy perceptron. Rozszerzenie 4x daje modelowi więcej „przestrzeni obliczeniowej" do uczenia się nieliniowych zależności, a potem wymiar wraca do normy.
ReLU (max(0, x)) to najprostsza nieliniowość – bez niej cała sieć byłaby jedną wielką operacją liniową (mnożenie macierzy × mnożenie macierzy = nadal mnożenie macierzy) i nie mogłaby nauczyć się niczego złożonego.
4. Trening
Funkcja straty: cross-entropy
probs = softmax(logits) # zamień surowe oceny na prawdopodobieństwa
loss_t = -probs[target_id].log() # im większe P(poprawny token), tym mniejszy loss
Model dostaje token i przewiduje rozkład prawdopodobieństwa nad całym słownikiem. Chcemy, żeby prawdziwy następny token miał prawdopodobieństwo jak najbliższe 1. -log(p) daje nam liczbę, która jest bliska 0, gdy model trafił (p ≈ 1), i bardzo duża, gdy model się pomylił (p ≈ 0).
Końcowy loss to średnia z całego imienia:
loss = (1 / n) * sum(losses)
Ważne uproszczenie: każdy krok treningowy przetwarza jedno imię (brak mini-batchy). Produkcyjne modele przetwarzają setki dokumentów jednocześnie, co stabilizuje trening i lepiej wykorzystuje GPU.
Optymalizator Adam
m[i] = beta1 * m[i] + (1 - beta1) * p.grad # momentum
v[i] = beta2 * v[i] + (1 - beta2) * p.grad ** 2 # adaptacyjny learning rate
m_hat = m[i] / (1 - beta1 ** (step + 1)) # korekta biasu
v_hat = v[i] / (1 - beta2 ** (step + 1))
p.data -= lr_t * m_hat / (v_hat ** 0.5 + eps_adam) # aktualizacja parametru
Adam to standard w optymalizacji sieci neuronowych. Zamiast zwykłego SGD (param -= lr * grad), Adam utrzymuje dwa bufory dla każdego parametru:
m(momentum): średnia krocząca gradientów. Wygładza aktualizacje – parametr nie skacze w losowych kierunkach, tylko podąża w ogólnym trendzie.v: średnia krocząca kwadratów gradientów. Adaptuje learning rate – parametry z dużymi gradientami dostają mniejsze kroki, z małymi gradientami większe.
Korekta biasu (m_hat, v_hat) jest potrzebna, bo na początku oba bufory startują od zera i zaniżałyby estymacje.
Warto zauważyć: beta1 = 0.85 jest niestandardowy (typowo 0.9). Learning rate maleje liniowo do zera (lr_t = learning_rate * (1 - step / num_steps)) – to popularna technika stabilizująca końcowe fazy treningu.
5. Inferencja i temperatura
probs = softmax([l / temperature for l in logits])
token_id = random.choices(range(vocab_size), weights=[p.data for p in probs])[0]
Przy generowaniu model produkuje token po tokenie. Temperatura kontroluje „pewność siebie" modelu:
- Temperatura < 1 (tu: 0.5) → wyostrza rozkład. Faworyt dominuje jeszcze bardziej. Wynik przewidywalny.
- Temperatura = 1 → oryginalny rozkład, bez zmian.
- Temperatura > 1 → spłaszcza rozkład. Mniej prawdopodobne opcje dostają szansę. Więcej kreatywności, ale i więcej bełkotu.
Matematycznie: dzielenie logitów przez 0.5 to to samo co mnożenie przez 2, co zwiększa różnice przed softmaxem.
6. Hiperparametry
| Parametr | Wartość | Co robi | Kontekst |
|---|---|---|---|
n_layer |
1 | Głębokość sieci | GPT-2: 12 warstw, GPT-3: 96 |
n_embd |
16 | Wymiarowość wektorów | GPT-2: 768, GPT-3: 12288 |
block_size |
16 | Długość kontekstu | Najdłuższe imię w zbiorze: 15 znaków + BOS |
n_head |
4 | Głowice attention | GPT-2: 12 |
learning_rate |
0.01 | Tempo uczenia | Typowo 1e-4 do 3e-4 dla dużych modeli |
num_steps |
1000 | Kroki treningowe | Duże modele: miliony kroków |
temperature |
0.5 | Kreatywność generowania | < 1 = ostrożny, 1 = standardowy, > 1 = kreatywny |
7. Podsumowanie dla programisty
Cały model to powtórzony wzorzec:
Input → Embedding → [RMSNorm → Attention → Residual → RMSNorm → MLP → Residual] × N → Linear → Softmax
W tym kodzie N = 1. W GPT-2 N = 12. W dużych modelach to kilkadziesiąt i więcej. Ale schemat jest ten sam.
Kluczowe mechanizmy, które warto zapamiętać:
- Autograd (klasa
Value) – automatyczne różniczkowanie przez graf obliczeniowy. Bez tego deep learning byłby niepraktyczny. - Attention – mechanizm „patrzenia" na cały dotychczasowy kontekst, który znacząco łagodzi problem długich zależności (z którym borykały się wcześniejsze architektury jak RNN i LSTM).
- Residual connections – umożliwiają trenowanie głębokich sieci, zapewniając gradientom bezpośrednią ścieżkę przepływu.
- KV-cache – przechowywanie kluczy i wartości z poprzednich kroków przyspiesza generowanie.
- Temperatura – prosty parametr kontrolujący balans między determinizmem a losowością.
Żeby uruchomić kod, wystarczy Python 3 i dostęp do internetu (kod sam pobierze plik z imionami). Trening 1000 kroków na CPU zajmie do kilku minut – wszystko działa na czystym Pythonie, bez optymalizacji GPU.
Podsumowanie
Ten kod to „Hello World" dla GPT. Pokazuje, że za fasadą miliardów parametrów i skomplikowanych frameworków leży zrozumiała, elegancka matematyka. Jeśli rozumiesz ten kod, rozumiesz fundamenty architektury stojącej za ChatGPT i wszystkimi innymi dużymi modelami językowymi.
To jest demokratyzacja AI. Nie jako czarna skrzynka, której trzeba ufać. Ale jako otwarta, zrozumiała technologia, którą każdy może zgłębić, zmodyfikować i ulepszyć.
Kod oryginalny: Andrej Karpathy
Artykuł napisany z myślą o czytelnikach pragnących zrozumieć fundamenty sztucznej inteligencji