Wstęp
Mechanizm attention jest sercem architektury Transformer i podstawą działania wszystkich nowoczesnych dużych modeli językowych (LLM). Chociaż teoria stojąca za attention może wydawać się abstrakcyjna, najlepszym sposobem na jej zrozumienie jest przejście przez konkretny przykład obliczeniowy krok po kroku.
W tym wpisie przeprowadzimy kompletny przykład obliczeniowy mechanizmu attention na prostym zdaniu, pokazując wszystkie macierze i obliczenia numeryczne.
Uwaga: Dla przejrzystości pomijamy w tym przykładzie positional encoding oraz końcową projekcję wyjściową \(W_O\). W rzeczywistych Transformerach oba te elementy są niezbędne – positional encoding koduje kolejność tokenów, a \(W_O\) projektuje sklejone wyjścia z wielu głowic do przestrzeni modelu.
Przykład: "cat chases mouse"
Rozważmy proste zdanie składające się z trzech słów (tokenów): cat chases mouse
I następujący słownik o rozmiarze 5:
- token 0: "cat"
- token 1: "chases"
- token 2: "mouse"
- token 3: "quickly"
- token 4: "sleeping"
Krok 1: Embeddingi tokenów
Każdy token reprezentujemy jako wektor embeddingów o wymiarze \(d_{model} = 2\) (w rzeczywistych modelach to zazwyczaj 512, 768 lub więcej).
Każdy z tokenów posiada następujące embeddingi:
gdzie kolumny odpowiadają kolejnym tokenom ze słownika.
Embeddingi:
gdzie:
- \(E[0] = [1.0, 0.0]\) - embedding dla "cat"
- \(E[1] = [0.2, 1.0]\) - embedding dla "chases"
- \(E[2] = [0.8, 0.0]\) - embedding dla "mouse"
Interpretacja wymiarów:
Choć nie kontrolujemy bezpośrednio, co oznacza każdy wymiar, możemy próbować to odkryć post hoc. W sieci można znaleźć wiele przykładów, gdzie np. embeddingi tokenów wujek i ciocia są przesunięte o pewną stałą wartość tak samo jak tokeny król i królowa. Oznacza to, że model zakodował informację o płci w konkretnym kierunku przestrzeni.
W naszym przykładzie można spekulować:
- Pierwszy wymiar — "zwierzęcość" (cat=1.0, mouse=0.8, chases=0.2)
- Drugi wymiar — "akcja/ruch" (chases=1.0, reszta=0.0)
Aczkolwiek to tylko przykład zrobiony pod tezę, w prawdziwych modelach o setkach wymiarów interpretacja jest znacznie trudniejsza i rzadko jednoznaczna.
Krok 2: Macierze wag — Query, Key, Value
Następnie definiujemy trzy macierze wag, które transformują embeddingi na reprezentacje Query, Key i Value.
W standardowej notacji wymiar klucza i wartości oznaczamy jako \(d_k\) i \(d_v\). Przy pojedynczej głowicy (single-head attention) mamy \(d_k = d_v = d_{model}\). W multi-head attention każda głowica operuje na \(d_k = d_v = d_{model} / h\), gdzie \(h\) to liczba głowic.
Macierz wag Query:
Macierz wag Key:
Macierz wag Value:
W tym prostym przykładzie używamy macierzy jednostkowych, ale w prawdziwych modelach są to wyuczone parametry.
Krok 3: Obliczenie Q, K, V
Mnożymy embeddingi przez odpowiednie macierze wag:
Query:
Key:
Value:
Krok 4: Attention Scores
Teraz obliczamy attention scores używając wzoru:
gdzie \(d_k = 2\) (wymiar klucza).
Obliczenie \(QK^T\):
Skalowanie przez \(\sqrt{d_k} = \sqrt{2} \approx 1.414\):
Krok 5: Maskowanie i Softmax
W architekturze decoder-only (np. GPT, Claude) stosujemy maskę przyczynową (causal mask), która sprawia, że tokeny nie widzą tokenów z przyszłości — mogą patrzeć tylko na siebie i na wcześniejsze pozycje. Dzięki temu model może być używany do autoregresyjnej generacji tekstu.
Dla porównania, architektura encoder-only (np. BERT) nie stosuje maski — każdy token widzi całą sekwencję (bidirectional attention).
Po maskowaniu (przed softmax):
Uwaga: Zapis \(a + (-\infty)\) jest matematycznie nieformalny, ale stanowi standardową konwencję w programowaniu. W arytmetyce zmiennoprzecinkowej
-infto konkretna wartość, dla której \(\exp(-\infty) = 0\), co skutecznie zeruje zamaskowane pozycje po softmax.
Aplikujemy funkcję softmax do każdego wiersza (wartości \(-\infty\) dają 0 po softmax):
Każdy wiersz pokazuje, jak bardzo dany token "zwraca uwagę" (attends) na dostępne tokeny:
- Wiersz 0 (cat): widzi tylko siebie (jedyny dostępny token), więc waga = 1.0
- Wiersz 1 (chases): widzi "cat" i siebie; większa uwaga na siebie (0.65) niż na "cat" (0.35)
- Wiersz 2 (mouse): widzi wszystkie trzy tokeny; największa uwaga na "cat" (0.40), następnie na siebie (0.35), najmniejsza na "chases" (0.25)
Krok 6: Output (reprezentacje kontekstowe)
Ostatnim krokiem jest pomnożenie wag attention przez macierz Value:
Ostateczna macierz wyjściowa zawiera kontekstowe reprezentacje (contextualized representations) dla każdego tokenu — każdy wektor jest ważoną kombinacją wektorów Value, gdzie wagi zależą od attention scores.
Co się zmieniło?
- "cat" — pozostał bez zmian \([1.0, 0.0]\), bo przez maskowanie widzi tylko siebie
- "chases" — zmiana z \([0.2, 1.0]\) na \([0.48, 0.65]\): wzrosła "zwierzęcość" (wpływ "cat"), spadła "akcja"
- "mouse" — zmiana z \([0.8, 0.0]\) na \([0.73, 0.25]\): pojawiła się składowa "akcji" (wpływ "chases")
Każdy token wchłonął informację o swoim kontekście. "Mouse" wie teraz, że jest goniona – informacja zakodowana w wymiarze "akcji" będzie kluczowa przy predykcji następnego tokenu.
To uproszczony przykład, ale dokładnie ten sam mechanizm agregacji kontekstu przez ważone sumy zachodzi w powszechnie używanych modelach.
Uwaga: W pełnej architekturze Transformer wyjście jest następnie mnożone przez macierz projekcji \(W_O \in \mathbb{R}^{d_v \times d_{model}}\), co tutaj pomijamy.
Krok 7: Feed-Forward Network (FFN)
Po bloku attention następuje sieć feed-forward (FFN), stosowana niezależnie do każdej pozycji. W oryginalnej architekturze Transformera (Vaswani et al., 2017) składa się z dwóch warstw liniowych z aktywacją ReLU:
Dla uproszczenia użyjemy minimalnego FFN z jedną warstwą liniową, bez aktywacji i bez biasu:
Uwaga: Pomijamy tu LayerNorm i residual connections, które w prawdziwym Transformerze stabilizują uczenie.
Krok 8: Predykcja następnego tokenu
Aby przewidzieć następny token, potrzebna jest reprezentacja ostatniego tokenu:
Warto zauważyć, że na tym etapie wszystkie pozostałe tokeny nie są nam potrzebne. Po etapie attention wszystkie informacje, które niosą, powinny być już zawarte w ostatnim tokenie.
Predykcja na logity (unembedding)
Mając logity możemy obliczyć prawdopodobieństwa:
Wyniki:
| Token | Prawdopodobieństwo |
|---|---|
| cat | 21% |
| chases | 30% |
| mouse | 19% |
| quickly | 18% |
| sleeping | 11% |
Wychodzi na to, że wg. naszego prostego modelu kolejny najbardziej prawdopodobny token to "chases", czyli zdanie brzmi:
Cat chases mouse chases
Co jest totalnie bez sensu?
Dlaczego to (nie) działa?
Bo wagi zostały dobrane nie na drodze treningu, lecz arbitralnie. Początkowo chciałem dobrać wagi tak, aby uzyskać sensowny wynik, po czym stwierdziłem, że większą wartość będzie miało, jeśli znowu przypomnę, że w prawdziwych modelach tych wag są miliony, więc ten prosty przykład nie ma prawa działać poprawnie (tak naprawdę w pierwszej kolejności było to motywowane lenistwem, dopiero potem dorobiłem tę opowieść o większej wartości edukacyjnej).
Gdy jednak uwierzymy, że to wszystko działa, to warto wspomnieć, że jest to zasługa mechanizmu attention, który pozwala każdemu tokenowi "spojrzeć" na dostępne tokeny i zadecydować, które z nich są najważniejsze dla jego reprezentacji.
W architekturze decoder-only z maską przyczynową (jak w naszym przykładzie):
- Każdy token buduje swoją reprezentację tylko na podstawie siebie i poprzednich tokenów
- "mouse" może uwzględnić kontekst z "cat" i "chases", ale nie odwrotnie
- To umożliwia autoregresyjną generację: model przewiduje następny token znając tylko poprzednie
W architekturze encoder-only (bez maski):
- Każdy token widzi całą sekwencję
- "cat" może zwracać uwagę na "mouse" i odwrotnie
- Model uczy się dwukierunkowych relacji między tokenami
Ta elastyczność pozwala Transformerom rozumieć strukturę i semantykę tekstu bez konieczności definiowania reguł gramatycznych.
Kluczowe właściwości
Skalowanie przez \(\sqrt{d_k}\)
Dzielenie przez \(\sqrt{d_k}\) zapobiega temu, aby iloczyny skalarne nie rosły zbyt mocno wraz z wzrostem wymiarowości. Bez tego skalowania softmax mógłby dawać bardzo ekstremalne wartości (bliskie 0 lub 1), co utrudniłoby uczenie poprzez znikające gradienty.
Softmax
Funkcja softmax normalizuje wyniki tak, aby suma wag attention dla każdego tokenu wynosiła 1.0. Dodatkowo wyostrza różnice między wartościami — większe wartości stają się jeszcze bardziej dominujące po softmax.
Macierze Q, K, V
Rozdzielenie na Query, Key i Value daje modelowi elastyczność:
- Query: "czego szukam?"
- Key: "co oferuję jako klucz do dopasowania?"
- Value: "jaką informację przekazuję?"
Token może szukać określonych cech (Q), być wyszukiwany po innych cechach (K) i przekazywać jeszcze inne informacje (V). Ta separacja pozwala na asymetryczne relacje między tokenami.
Multi-Head Attention
W praktyce nowoczesne Transformery używają multi-head attention, gdzie:
- Embeddingi są projektowane do \(h\) różnych podprzestrzeni (głowic)
- Każda głowica ma własne macierze \(W_Q^{(i)}, W_K^{(i)}, W_V^{(i)}\) o wymiarach prowadzących do \(d_k = d_v = d_{model} / h\)
- Attention jest obliczany równolegle w każdej głowicy
- Wyniki są konkatenowane i projektowane przez \(W_O\)
gdzie:
To pozwala modelowi uczyć się różnych typów relacji równocześnie - jedna głowica może śledzić relacje składniowe, inna semantyczne, jeszcze inna pozycyjne.
Podsumowanie
Mechanizm attention to matematycznie elegancki sposób na modelowanie zależności między elementami sekwencji. Kluczowe kroki to:
- Embeddingi — reprezentacja tokenów jako wektorów (+ positional encoding w pełnej architekturze)
- Transformacje Q, K, V — projekcje do przestrzeni query, key i value
- Attention scores — obliczenie \(QK^T/\sqrt{d_k}\)
- Maskowanie — opcjonalna maska przyczynowa (decoder) lub brak maski (encoder)
- Softmax — normalizacja do prawdopodobieństw
- Ważona suma Value — kontekstowe reprezentacje
- Projekcja wyjściowa — mnożenie przez \(W_O\) (w pełnej architekturze)
Ten mechanizm, powtórzony wiele razy w wielu warstwach i głowicach, tworzy potężną architekturę Transformer, która zrewolucjonizowała przetwarzanie języka naturalnego.
Przydatne linki
- Attention is All You Need (oryginalny artykuł)
- The Illustrated Transformer
- Visualizing Attention in Transformer Models