Od prostych neuronów do pamięci – ewolucja modeli językowych
Jun 17, 2026 - ⧖ 26 minPublished as part of 'zrozumiec-llm' series.
Tip
Chcesz uruchomić ten kod samemu? Wszystkie przykłady z tego posta (perceptron, RNN, LSTM) znajdziesz w gotowym notatniku Jupyter: od-prostych-neuronow-do-pamieci.ipynb
W poprzednim wpisie przeszliśmy całą drogę od cięcia tekstu na tokeny, przez liczenie słów (TF-IDF), łańcuchy Markowa, aż po Word2Vec i wektory znaczeń. I na koniec pojawiło się jedno fundamentalne pytanie...
Mamy wektory słów. Super. Ale jak z nich zbudować coś, co rozumie zdanie? Albo cały paragraf? Albo książkę?
Bo przecież "Dog bites man" to nie jest po prostu "dog" + "bites" + "man". To jest sekwencja - słowa następują po sobie w określonym porządku i ten porządek zmienia znaczenie. "Dog bites man" i "Man bites dog" to te same słowa, ale zupełnie inne historie :D
A Word2Vec? Word2Vec patrzy na słowa pojedynczo. Nie ma pojęcia kolejności. Nie wie, że "nie" przed "lubie" zmienia wszystko.
Więc potrzebujemy czegoś, co potrafi przetwarzać sekwencje. Coś, co czyta tekst w kolejności i buduje zrozumienie krok po kroku.
I tu zaczyna się fascynująca historia - bo rozwiązanie tego problemu nie pojawiło się z dnia na dzień. To była ewolucja, która trwała kilkadziesiąt lat. Od jednego, prostego neuronu, do skomplikowanych komórek pamięci. I każda generacja rozwiązywała problem poprzedniej, ale tworzyła nowy.
To jest czwarty wpis z serii "zrozumiec LLM". Dzisiaj idziemy głębiej w architekturę sieci neuronowych - ale spokojnie, dalej bez wzorów, których nie da się zrozumieć ;-) Zamiast tego będzie dużo metafor, diagramów i przykładów z życia.
timeline
title Ewolucja architektur neuronowych
1943 : McCulloch & Pitts<br/>model matematyczny neuronu
1958 : Rosenblatt<br/>perceptron
1986 : Rumelhart et al.<br/>backpropagation + MLP
1986 : RNN<br/>sieci rekurencyjne
1997 : Hochreiter & Schmidhuber<br/>LSTM
2017 : Vaswani et al.<br/>Transformer (cliffhanger!)
Narracja dzisiejszego wpisu: "Od jednej komórki do pamięci - i dlaczego to wciąż nie wystarczyło." Zaczynamy od podstaw.
Neuron biologiczny vs sztuczny - skąd w ogóle ten pomysł?
Zanim wejdziemy w architektury, szybko załatwmy jedno pytanie, które pewnie sobie zadajecie: dlaczego w ogóle nazywa się to "siecią neuronową"? Co to ma wspólnego z mózgiem?
Odpowiedź: i dużo, i mało ;-) Na początku była inspiracja biologią. Spójrzcie:
graph LR
BD["🌿 Dendryty<br/><i>odbierają sygnały</i>"] --> BS["🧫 Soma<br/><i>przetwarza</i>"]
BS --> BA["⚡ Akson<br/><i>przekazuje dalej</i>"]
graph LR
SD["📥 Wejścia x₁, x₂, x₃<br/><i>liczby</i>"] --> SS["➕ Suma ważona<br/><i>w₁x₁ + w₂x₂ + w₃x₃ + b</i>"]
SS --> SA["📤 Wyjście<br/><i>aktywacja(suma)</i>"]
Pierwszy diagram - prawdziwy neuron. Ma dendryty (odbierają sygnały od innych neuronów), somę (ciało komórki - decyduje czy "odpalić"), i akson (przekazuje sygnał dalej).
Drugi diagram - sztuczny neuron. Ma wejścia (liczby), sumę ważoną (mnoży każde wejście przez jego "ważność" i dodaje wszystko), i wyjście (wynik przepuszczony przez funkcję aktywacji).
Podobieństwo jest... luźne. Biologiczny neuron jest niewyobrażalnie bardziej skomplikowany. Ale inspiracja była ważna - Warren McCulloch i Walter Pitts w 1943 roku pokazali, że taki uproszczony model neuronu potrafi realizować podstawowe operacje logiczne (AND, OR, NOT). A to oznaczało, że sieć takich neuronów mogłaby teoretycznie obliczać cokolwiek.
Note
Czy LLM "myśli" jak mózg? Nie. Neuron sztuczny to matematyczna abstrakcja, nie model biologiczny. Biologiczny neuron używa impulsów elektrycznych, neuroprzekaźników, ma tysiące połączeń i dynamikę, której nie da się sprowadzić do w₁x₁ + w₂x₂. Ale inspiracja była punktem wyjścia - i to ważny punkt.
OK, to tyle biologii. Przejdźmy do tego, co z tego wynikło ;-)
Perceptron - najprostsza decyzja świata
W 1958 roku Frank Rosenblatt stworzył perceptron - pierwszy algorytm, który potrafił się uczyć na podstawie danych. To był kamień milowy.
Perceptron działa banalnie prosto. Wyobraźcie sobie, że decydujecie, czy wyjść na spacer:
graph TD
S["☀️ Słonecznie?"] -->|"w = +5"| SUM["➕ SUMA"]
D["🌧️ Pada deszcz?"] -->|"w = -8"| SUM
T["🌡️ Temperatura > 20°C?"] -->|"w = +3"| SUM
BIAS["⚙️ Próg (bias) = 0"] --> SUM
SUM --> DEC{"Suma >= próg?<br/>(próg = 0)"}
DEC -->|"Tak"| Y["✅ Wychodzę na spacer!"]
DEC -->|"Nie"| N["❌ Zostaję w domu"]
style S fill:#ffcc99,color:#000
style D fill:#9999ff,color:#fff
style T fill:#ff9999,color:#000
style SUM fill:#99ff99,color:#000
style Y fill:#66cc66,color:#fff
style N fill:#cc6666,color:#fff
Mechanika jest prosta:
- Każde wejście ma swoją wagę (importance) - słońce ma wagę +5, deszcz -8, temperatura +3
- Perceptron mnoży każde wejście przez jego wagę i dodaje wszystko (to jest ta "suma ważona")
- Jeśli suma jest większa lub równa progowi (w naszym przypadku próg = 0) - odpala "tak". Inaczej - "nie".
Przykład:
| Sytuacja | Słońce (+5) | Deszcz (-8) | Temp >20° (+3) | Suma | Decyzja |
|---|---|---|---|---|---|
| Słonecznie, ciepło, bez deszczu | 1 | 0 | 1 | 5 + 0 + 3 = 8 | ✅ Wychodzę |
| Pada, zimno | 0 | 1 | 0 | 0 - 8 + 0 = -8 | ❌ Zostaję |
| Pochmurno i pada, ale ciepło | 0 | 1 | 1 | 0 - 8 + 3 = -5 | ❌ Zostaję |
I - najważniejsze - perceptron potrafi uczyć się tych wag. Pokazujecie mu 100 przykładów "wyszedłem / nie wyszedłem" i on stopniowo dostosowuje wagi, żeby jego decyzje pasowały do waszych.
Genialne w swojej prostocie, prawda?
Ale perceptron ma jeden wielki problem...
Perceptron potrafi narysować jedną prostą linię dzielącą dane na dwie grupy. Co jest super, jeśli dane da się tak podzielić:
⚫ ⚫ ⚫ │ ⚪ ⚪ ⚪
⚫ ⚫ ⚫ │ ⚪ ⚪ ⚪
↑
jedna linia dzieli je ✅
Ale co jeśli dane wyglądają tak?
⚪ ⚫
⚫ ⚪
To jest słynny problem XOR (exclusive OR). Wyjście jest "prawdziwe" tylko wtedy, gdy dokładnie jedno wejście jest prawdziwe - nie oba i nie żadne. I nie da się narysować jednej prostej linii, która by to oddzieliła.
Ten problem zatrzymał rozwój sieci neuronowych na prawie 20 lat. Serio. Badacze powiedzieli: "no dobra, perceptron jest fajny, ale skoro nie radzi sobie z czymś tak prostym jak XOR, to po co to w ogóle?" Ten okres nazywa się "AI Winter" (zima AI).
Warning
Dlaczego XOR był taki ważny? Bo pokazał, że pojedynczy perceptron jest ograniczony do problemów liniowo separowalnych. A prawdziwy świat rzadko jest liniowy. Język na pewno nie jest.
I wtedy ktoś powiedział: a co jeśli połączymy kilka perceptronów razem?
MLP - zespół, który potrafi więcej niż jednostka
Multilayer Perceptron (MLP) to sieć złożona z wielu warstw neuronów. Zamiast jednej warstwy decyzyjnej, mamy:
graph LR
subgraph "Warstwa wejściowa"
X1["x₁"]
X2["x₂"]
X3["x₃"]
end
subgraph "Warstwa ukryta 1"
H1["h₁<br/><i>proste cechy</i>"]
H2["h₂"]
H3["h₃"]
H4["h₄"]
end
subgraph "Warstwa ukryta 2"
G1["g₁<br/><i>kombinacje</i>"]
G2["g₂"]
G3["g₃"]
end
subgraph "Warstwa wyjściowa"
Y1["y₁<br/><i>decyzja</i>"]
Y2["y₂"]
end
X1 --> H1 & H2 & H3 & H4
X2 --> H1 & H2 & H3 & H4
X3 --> H1 & H2 & H3 & H4
H1 --> G1 & G2 & G3
H2 --> G1 & G2 & G3
H3 --> G1 & G2 & G3
H4 --> G1 & G2 & G3
G1 --> Y1 & Y2
G2 --> Y1 & Y2
G3 --> Y1 & Y2
style X1 fill:#ff9999,color:#000
style X2 fill:#ff9999,color:#000
style X3 fill:#ff9999,color:#000
style H1 fill:#ffcc99,color:#000
style H2 fill:#ffcc99,color:#000
style H3 fill:#ffcc99,color:#000
style H4 fill:#ffcc99,color:#000
style G1 fill:#99ff99,color:#000
style G2 fill:#99ff99,color:#000
style G3 fill:#99ff99,color:#000
style Y1 fill:#9999ff,color:#fff
style Y2 fill:#9999ff,color:#fff
Każdy neuron w jednej warstwie jest połączony z każdym neuronem w warstwie następnej (dlatego to się nazywa "fully connected" albo "dense").
Metafora, która mi najbardziej pasuje:
Wyobraźcie sobie firmę. Analitycy (warstwa 1) patrzą na surowe dane i wyłapują proste wzorce. Menedżerowie (warstwa 2) łączą te wzorce w coś bardziej sensownego. Dyrektor (warstwa wyjściowa) podejmuje ostateczną decyzję. Żadna z tych osób nie rozumie pełnego obrazu sama - ale razem tworzą kaskadę abstrakcji.
I właśnie to rozwiązuje problem XOR! Pierwsza warstwa rysuje dwie linie. Druga warstwa łączy je w jeden obszar. Nagle nieliniowy problem staje się rozwiązywalny.
Ale uwaga - jest haczyk!
Dodanie warstw ukrytych samo w sobie nic nie daje. Dlaczego?
Bo jeśli każda warstwa robi tylko suma = wagi × wejścia + bias, to dwie warstwy takiej operacji to nadal... jedna wielka operacja liniowa. Pokażę na liczbach:
Warstwa 1: y = 2·x + 1
Warstwa 2: z = 3·y + 2
Podstawiamy pierwsze do drugiego:
z = 3·(2·x + 1) + 2
z = 6·x + 3 + 2
z = 6·x + 5 ← jedna operacja, tylko z innymi liczbami
Dwie warstwy złożyły się w jedną. Zamiast dwóch transformacji, dostajecie jedną - tylko z innymi współczynnikami. To jakbyście zamiast dwóch filtrów do kawy użyli jednego, ale dwa razy grubszego. Efekt ten sam.
Więc potrzebujemy czegoś nieliniowego pomiędzy warstwami. I tu wkracza...
Funkcja aktywacji - nieoceniony bohater deep learningu
Funkcja aktywacji to taka "niestandardowa przetwórnia", którą przepuszczamy wynik sumy. Najpopularniejsza dziś to ReLU (Rectified Linear Unit):
def relu (x ):
if x > 0 :
return x
else :
return 0
To wszystko. Jeśli wartość jest dodatnia - zostaw. Jeśli ujemna - wyzeruj.
Brzmi banalnie? Jest banalnie. Ale ta jedna prosta operacja łamie liniowość.
Wracamy do naszego przykładu z liczbami. Bez ReLU dwie warstwy złożyły się w jedną (z = 6·x + 5). Ale teraz wstawmy ReLU między nimi:
Warstwa 1: y = ReLU(2·x + 1)
Warstwa 2: z = ReLU(3·y + 2)
Sprawdźmy dla trzech wartości x:
| x | 2·x + 1 | y (po ReLU) | 3·y + 2 | po ReLU | Bez ReLU (6·x + 5) |
|---|---|---|---|---|---|
| -2 | -3 | 0 ⬅ wyzerowane! | 2 | 2 | -7 |
| 0 | 1 | 1 | 5 | 5 | 5 |
| 1 | 3 | 3 | 11 | 11 | 11 |
Bez ReLU wynik zmienia się liniowo (-7, 5, 11 - stały przyrost). Z ReLU nagle dla x = -2 dostajemy 2 zamiast -7. Ta relacja nie jest już prostą linią. Wyzerowanie ujemnych wartości jest czymś, czego nie da się wyrazić jako a·x + b - i właśnie dlatego warstwy przestają się sklejać w jedną.
Analogia: wyobraź sobie żaluzje w oknie. Liniowość to przezroczysta szyba - przepuszcza wszystko, lekko przyciemnione. ReLU to żaluzje, które całkowicie blokują światło poniżej pewnego kąta. Nie da się symulować "całkowitego zablokowania" przez "mocniejsze przyciemnienie szyby". To jakościowo inna operacja.
Dzięki ReLU każda warstwa robi coś innego niż poprzednia. I nagle sieć z wielu warstw staje się potężna - potrafi modelować nieliniowe, skomplikowane relacje w danych.
Important
ReLU w pigułce: Bez funkcji aktywacji (nieliniowej) między warstwami, sieć z 100 warstwami jest równie ekspresywna jak sieć z 1 warstwą. ReLU (i jego kuzyni: sigmoid, tanh, GELU) to składnik, który sprawia, że głębokość ma sens.
Inne popularne funkcje aktywacji:
| Funkcja | Wzór (w uproszczeniu) | Zakres | Gdzie używana |
|---|---|---|---|
| ReLU | max(0, x) | [0, +∞) | Warstwy ukryte (standard) |
| Sigmoid | 1 / (1 + e⁻ˣ) | (0, 1) | Bramki LSTM, output binarny |
| Tanh | (eˣ - e⁻ˣ) / (eˣ + e⁻ˣ) | (-1, 1) | Bramki LSTM, hidden states |
| Softmax | eˣⁱ / Σeˣʲ | (0, 1), suma = 1 | Warstwa wyjściowa (klasyfikacja) |
Tip
Eksperyment dla was: Otwórzcie Python i odpalcie to:
import numpy as np
def relu (x ):
return np .maximum (0 , x )
x = np .array ([- 3 , - 1 , 0 , 2 , 5 ])
print (f"Wejście: { x } " )
print (f"Po ReLU: { relu ( x ) } " )
Zobaczycie jak ReLU "odcina" wszystko, co jest poniżej zera. Ta prosta operacja pozwala sieci "wybierać", które cechy są aktywne, a które ignorować.
Mini-quiz: liniowe czy nie?1
- Zwykła regresja liniowa (y = ax + b) - liniowa czy nieliniowa?
- Perceptron z funkcją step (0 lub 1) - liniowy czy nieliniowy?
- MLP bez funkcji aktywacji między warstwami - liniowy czy nieliniowy?
Problem: MLP nie rozumie kolejności
OK, mamy MLP. Potrafi modelować nieliniowe relacje. Super. Ale jest jeden fundamentalny problem, który blokuje nas przed używaniem MLP do języka.
MLP widzi wszystkie cechy naraz. Nie ma pojęcia kolejności.
Weźmy klasyczny angielski przykład. Dwa zdania z dokładnie tymi samymi słowami, ale w innej kolejności:
| Zdanie | dog | bites | man |
|---|---|---|---|
| "Dog bites man" | 1 | 1 | 1 |
| "Man bites dog" | 1 | 1 | 1 |
Identyczny wektor. A znaczenie? Pierwsze to nudna wiadomość. Drugie to sensacja w gazecie :D
Dla MLP oba zdania to dokładnie to samo - bo MLP dostaje ten sam wektor i nie ma pojęcia kolejności.
Note
A co z polskim? Polski jest tu podchwytliwy - przez fleksję (7 przypadków!) rzadko mamy dwa zdania z dokładnie tymi samymi formami słów. "Pies gryzie człowieka" vs "Człowiek gryzie psa" to różne formy (pies→psa, człowieka→człowiek). Ale angielski, z minimalną odmianą, idealnie pokazuje ten problem. I właśnie dlatego przykłady z bag-of-words często używają angielskiego ;-)
To jest problem permutacji - MLP nie odróżnia [A, B, C] od [C, B, A]. A w języku kolejność jest wszystkim. "Nie lubię" vs "Lubię nie" - same słowa, zupełnie inne znaczenie.
Można by pomyśleć: "OK, to dodajmy pozycję słowa jako cechę". Ale to nie rozwiązuje problemu - MLP nadal nie rozumie, że słowo na pozycji 1 ma związek ze słowem na pozycji 5. Każda pozycja to po prostu kolejna liczba na wejściu.
Więc potrzebujemy czegoś, co przetwarza dane krok po kroku, w kolejności, i buduje zrozumienie sekwencyjnie. Tak jak my czytamy książkę - zdanie po zdaniu, słowo po słowie.
I tu wkracza RNN.
RNN - "czyta tekst po kolei"
Recurrent Neural Network (RNN) to sieć, która ma coś, czego MLP nie ma: pamięć. No... jako tako ;-)
Wyobraźcie sobie, że czytacie książkę. Nie czytacie całej strony naraz (jak robiłby MLP). Czytacie słowo po słowie, zdanie po zdaniu. I przy każdym nowym słowie aktualizujecie swoje zrozumienie tego, co się dzieje.
Dokładnie to robi RNN. Ma hidden state (ukryty stan) - to jest jego "rozumienie tekstu do tej pory". Przy każdym nowym słowie:
- Bierze nowe słowo (x_t)
- Bierze swoje dotychczasowe zrozumienie (h_{t-1})
- Łączy je i tworzy nowe zrozumienie (h_t)
graph LR
subgraph "RNN czyta: 'Kot siedzi na macie'"
X1["📖 'Kot'"] --> RNN1["🔄 RNN<br/>h₁"]
RNN1 --> |"rozumie:<br/>'mowa o kocie'"| RNN2["🔄 RNN<br/>h₂"]
X2["📖 'siedzi'"] --> RNN2
RNN2 --> |"rozumie:<br/>'kot coś robi'"| RNN3["🔄 RNN<br/>h₃"]
X3["📖 'na'"] --> RNN3
RNN3 --> RNN4["🔄 RNN<br/>h₄"]
X4["📖 'macie'"] --> RNN4
RNN4 --> |"rozumie:<br/>'kot siedzi na macie'"| OUT["✅ Pełne zrozumienie"]
end
style X1 fill:#ffcc99,color:#000
style X2 fill:#ffcc99,color:#000
style X3 fill:#ffcc99,color:#000
style X4 fill:#ffcc99,color:#000
style RNN1 fill:#9999ff,color:#fff
style RNN2 fill:#9999ff,color:#fff
style RNN3 fill:#9999ff,color:#fff
style RNN4 fill:#9999ff,color:#fff
style OUT fill:#66cc66,color:#fff
Ten diagram to jest właśnie unrolling (rozwinięcie w czasie). W rzeczywistości to ten sam neuron RNN - ale "odwijamy" go w czasie, żeby pokazać, jak przetwarza kolejne słowa. Zauważcie: strzałka idzie od lewej do prawej - informacja płynie sekwencyjnie.
To jest genialne w swojej prostocie. RNN "czyta" tekst tak jak my - po kolei, budując zrozumienie krok po kroku.
RNN w kodzie - minimalistycznie
Żeby to naprawdę poczuć, napiszmy najprostszy możliwy RNN. Bez PyTorcha, bez TensorFlow - sam NumPy:
import numpy as np
def simple_rnn (word_vectors , hidden_size = 4 ):
words , dim = word_vectors .shape
np .random .seed (42 )
W_h = np .random .randn (hidden_size , hidden_size ) * 0.01
W_x = np .random .randn (hidden_size , dim ) * 0.01
bias = np .zeros (hidden_size )
h = np .zeros (hidden_size )
for t , word in enumerate (word_vectors ):
h = np .tanh (W_h @ h + W_x @ word + bias )
print (f" Krok { t + 1 } : hidden state = { np . round ( h , 2 ) } " )
return h
kot = np .array ([1.0 , 0.0 ])
siedzi = np .array ([0.0 , 1.0 ])
na = np .array ([0.3 , 0.3 ])
macie = np .array ([0.8 , 0.2 ])
sentence = np .array ([kot , siedzi , na , macie ])
print ("Przetwarzam: 'Kot siedzi na macie'" )
final = simple_rnn (sentence )
print (f"\nFinalny hidden state: { np . round ( final , 2 ) } " )
Uruchomcie to! Zobaczycie, jak przy każdym słowie hidden state się zmienia. Ostatni hidden state to jest "rozumienie" całego zdania przez nasz prosty RNN.
Tip
Kluczowa intuicja: Zauważcie linijkę h = np.tanh(W_h @ h + W_x @ word + bias). To jest serce RNN.
Dwie części tej linijki:
W_h @ h + W_x @ word + bias— to jest ta sama "suma ważona" co w perceptronie i MLP. Tyle że teraz sumuje dwa źródła: poprzedni hidden state (h) i nowe słowo (word).tanh(...)— to jest funkcja aktywacji, dokładnie ta sama rodzina co ReLU z poprzedniej sekcji. Zamiast "odcinać poniżej zera", tanh ściska wszystko do zakresu (-1, 1). Ale cel jest ten sam: złamać liniowość.
Nowy hidden state zależy od dwóch rzeczy: poprzedniego hidden state (h) i nowego słowa (word). To jest dokładnie to, co robimy, czytając tekst - łączymy to, co już wiemy, z tym, co właśnie przeczytaliśmy.
Ale RNN ma jeden wielki problem...
I tu dochodzimy do tematu, który zmienił wszystko.
Problem złotej rybki - dlaczego RNN zapomina
RNN ma pamięć, tak. Ale to jest pamięć złotej rybki.
Wyobraźcie sobie to zdanie:
"To jest Jan i ma 30 lat. Lubi chodzić po górach, programować w Pythonie i grać na gitarze. Jego ulubionym kolorem jest zielony. Kiedyś chciał zostać astronautą, ale potem odkrył, że (...) [tutaj 50 słów o różnych rzeczach] (...) i dlatego ___ poszedł do szkoły muzycznej."
Jaki ma być "___"? Jan. Ale żeby to wiedzieć, RNN musi pamiętać imię z początku zdania. I tu pojawia się problem: po 50+ słowach, sygnał z pierwszego słowa jest tak rozwodniony, że RNN go praktycznie nie pamięta.
To jest słynny problem vanishing gradient (zanikający gradient).
Metafora: głuchy telefon. Gracie w głuchy telefon - pierwsza osoba szepcze wiadomość drugiej, ta trzeciej, itd. Po 10 osobach wiadomość jest trochę zniekształcona. Po 50 osobach - kompletnie nieczytelna. Po 100 osobach - ktoś mówi "kup mleko", a ostatnia osoba słyszy "łap chleb".
W RNN gradient (sygnał uczący) przepływa przez sieć dokładnie tak samo - krok po kroku. Przy każdym kroku jest mnożony przez jakieś wagi. Jeśli te wagi są mniejsze niż 1, to po wielu mnożeniach gradient zanika do zera. Sieć przestaje się uczyć z odległych słów.
A czasami jest odwrotnie - wagi są większe niż 1 i gradient rośnie eksponencjalnie (exploding gradient). Sieć "oszalała" i wartości lecą w kosmos.
graph LR
G1["Krok 1<br/>gradient = 1.0"] -->|"×0.7"| G2["Krok 2<br/>0.7"]
G2 -->|"×0.7"| G3["Krok 3<br/>0.49"]
G3 -->|"×0.7"| G4["Krok 4<br/>0.34"]
G4 -->|"..."| G5["Krok 20<br/>0.001"]
G5 -->|"..."| G6["Krok 50<br/>≈ 0"]
style G1 fill:#66cc66,color:#fff
style G2 fill:#99cc66,color:#000
style G3 fill:#cccc66,color:#000
style G4 fill:#cc9966,color:#000
style G5 fill:#cc6666,color:#fff
style G6 fill:#993333,color:#fff
Warning
Dlaczego to jest takie ważne? Bo język jest pełen długich zależności. "The cat, which already ate all the fish in the fridge and then slept on the couch for three hours, was happy." - żeby połączyć "cat" z "was happy", model musi przeskoczyć kilkanaście słów. RNN tego nie potrafi.
I wtedy w 1997 roku Sepp Hochreiter i Jürgen Schmidhuber powiedzieli: a gdyby tak dać sieci explicite mechanizm pamięci?
LSTM - sieć z notatnikiem
Long Short-Term Memory (LSTM) to RNN na sterydach. Zamiast jednego prostego hidden state, LSTM ma komórkę pamięci (cell state) i trzy bramki (gates), które decydują, co z tą pamięcią zrobić.
Metafora, która najlepiej działa: LSTM to uczeń z notatnikiem i trzema zasadami.
Bramka forget (zapomnij) - "wyrzuć stare notatki"
Na początku każdego kroku LSTM pyta: "co z dotychczasowej pamięci jest już nieaktualne i mogę wyrzucić?"
Jak czytacie nowy rozdział książki, to nie potrzebujecie pamiętać, co jedli bohaterowie na śniadanie trzy rozdziały temu. Zapominacie nieistotne detale, żeby zrobić miejsce na nowe.
W LSTM: forget gate przepuszcza każdy element pamięci przez sigmoidę (wartości 0-1). Blisko 0 = "zapomnij". Blisko 1 = "zatrzymaj".
Bramka input (zapamiętaj) - "zapisz to, to jest ważne"
Potem LSTM pyta: "co z nowego słowa jest warte zapisania?"
Kiedy czytacie "Mam na imię Jan" - macie refleks: "OK, to jest ważne, muszę to zapamiętać". Nie zapamiętujecie każdego słowa dosłownie, ale wybieracie to, co istotne.
W LSTM: input gate decyduje, które nowe informacje przepuścić do cell state.
Bramka output (wyeksponuj) - "pokaż mi, co teraz potrzebuję"
Na koniec LSTM pyta: "co z mojej pamięci jest teraz istotne dla tego, co robię?"
Jak ktoś was pyta "Jak ma na imię ta postać, o której właśnie czytaliśmy?" - sięgacie do notatnika i wyciągacie konkretne informacje. Nie pokazujecie całego notatnika - tylko to, co jest potrzebne teraz.
graph TD
subgraph "LSTM - komórka pamięci"
CS_IN["📥 Cell state (wejście)<br/><i>taśma z informacjami</i>"] --> MUL1["✖️ × forget gate"]
FG["🗑️ Forget gate<br/><i>co zapomnieć?</i>"] --> MUL1
MUL1 --> ADD["➕ + nowa informacja"]
NEW["📝 Nowa informacja<br/><i>input gate × kandydat</i>"] --> ADD
IG["📥 Input gate<br/><i>co zapisać?</i>"] --> NEW
ADD --> CS_OUT["📤 Cell state (wyjście)<br/><i>zaktualizowana taśma</i>"]
CS_OUT --> TANH["tanh"]
TANH --> MUL2["✖️ × output gate"]
OG["📤 Output gate<br/><i>co pokazać?</i>"] --> MUL2
MUL2 --> HS["🧠 Hidden state<br/><i>to, co 'myśli' teraz</i>"]
end
X["📖 Nowe słowo (x_t)"] --> FG & IG & OG
HS_PREV["🧠 Poprzedni hidden state (h_{t-1})"] --> FG & IG & OG
style CS_IN fill:#ffcc99,color:#000
style CS_OUT fill:#ffcc99,color:#000
style FG fill:#ff6666,color:#fff
style IG fill:#66cc66,color:#fff
style OG fill:#6666ff,color:#fff
style HS fill:#cc99ff,color:#000
style X fill:#ffcc99,color:#000
Kluczowa rzecz: cell state to jest ta "taśma produkcyjna" (conveyor belt). Informacja na niej może płynąć prawie niezmieniona przez wiele kroków - bramki decydują tylko, co przepuścić, co dodać i co usunąć. To rozwiązuje problem vanishing gradient, bo informacja nie musi być "mnożona" na każdym kroku - może po prostu przepłynąć.
Note
RNN vs LSTM w jednym zdaniu: RNN próbuje pamiętać wszystko, ale szybko zapomina. LSTM jest selektywne - decyduje co zachować, co zaktualizować i co pokazać. To jest różnica między "próbą zapamiętania całego wykładu słowo w słowo" a "robieniem notatek z najważniejszymi punktami".
Dla chętnych: matematyka bramek LSTM
Jeśli chcecie zobaczyć wzory, oto one (nie musicie ich zapamiętywać, ale warto zobaczyć, że to nie jest czarna magia):
Forget gate: $$f_t = \sigma(W_f \cdot [h_{t-1}, x_t] + b_f)$$
Input gate: $$i_t = \sigma(W_i \cdot [h_{t-1}, x_t] + b_i)$$
Kandydat do cell state: $$\tilde{C}t = \tanh(W_c \cdot [h{t-1}, x_t] + b_c)$$
Aktualizacja cell state: $$C_t = f_t \odot C_{t-1} + i_t \odot \tilde{C}_t$$
Output gate: $$o_t = \sigma(W_o \cdot [h_{t-1}, x_t] + b_o)$$
Hidden state: $$h_t = o_t \odot \tanh(C_t)$$
Gdzie $\sigma$ to sigmoid (wgniata wartości do 0-1), $\odot$ to mnożenie element-po-elemencie, a $[h_{t-1}, x_t]$ to połączenie poprzedniego hidden state z nowym wejściem.
Eksperyment dla was
Spróbujcie sami. Odpalcie ChatGPT (albo Claude, Gemini - co macie) i wyślijcie ten prompt:
"Pola na imieniu miała rzadką zdolność do zapamiętywania cyfr. Jej ulubioną liczbą było 42. Lubiła też longboarding, malowanie akwarelami i granie w szachy. Jej kot wabił się Mruczka. (...) [tutaj wklejcie 3-4 akapity jakiegokolwiek tekstu - przepis na naleśniki, opis pogody, cokolwiek] (...) Mimo wszystko, to właśnie ___ postanowiła wziąć udział w konkursie matematycznym."
Zobaczcie, czy model wypełni lukę poprawnie (Pola). LSTM by sobie z tym poradził. Zwykły RNN - prawdopodobnie nie. A dlaczego? Bo LSTM ma mechanizm, który mówi "zapamiętaj imię Pola - to może być ważne później". RNN po prostu... zapomniałby ;-)
Dlaczego LSTM wciąż nie wystarczyło
OK, więc LSTM rozwiązało problem pamięci. Przynajmniej częściowo. Ale pojawiły się nowe problemy, które okazały się fundamentalne.
Problem 1: Nawet LSTM ma swoje granice
LSTM rozwiązało vanishing gradient w dużej mierze, ale nie całkowicie. Gdy odległość między powiązanymi słowami rośnie do setek lub tysięcy słów, nawet LSTM zaczyna gubić nić narracji. Badacze Bengio et al. (1994) pokazali, że sieci rekurencyjne - nawet z bramkami - wciąż mają problem z podtrzymaniem użytecznych gradientów na bardzo długich dystansach.
Czyli: zamiast zapominać po 10 słowach (RNN), LSTM zapomina po... 100? 200? Lepiej, ale wciąż nie idealnie.
Problem 2: Sekwencyjność = wąskie gardło
I to jest najważniejszy problem.
RNN i LSTM przetwarzają dane krok po kroku. Krok 2 musi poczekać na krok 1. Krok 3 na krok 2. I tak dalej.
graph LR
S1["Krok 1"] --> S2["Krok 2"]
S2 --> S3["Krok 3"]
S3 --> S4["Krok 4"]
S4 --> S5["..."]
S5 --> S6["Krok 1000"]
A teraz pomyślcie o GPU. GPU to jest maszynka, która uwielbia robić wiele rzeczy naraz (parallel processing). Tysiące rdzeni pracujących równocześnie.
Ale RNN/LSTM mówi GPU: "nie, nie, poczekaj. Najpierw skończmy krok 1, dopiero potem zacznij krok 2". GPU płacze.
To jest jak książka z 10 000 rozdziałów, gdzie każdy rozdział musi być przeczytany po poprzednim. Nie możecie przeczytać rozdziału 500, dopóki nie skończycie 499. Nie da się tego zrównoleglić. Przy krótkich tekstach to nie problem, ale przy trenowaniu modelu na miliardach słów? To jest koszmar.
LSTM ma dodatkowo 4x więcej parametrów niż prosty RNN (trzy bramki + cell state = cztery zestawy wag na każdy neuron). Więc trenuje się wolniej i zużywa więcej pamięci.
Problem 3: "Black box"
LSTM (jak większość modeli deep learning) jest trudny do interpretacji. Model może generować poprawne wyniki, ale trudno zrozumieć, dlaczego podjął taką a nie inną decyzję. W zastosowaniach takich jak medycyna czy finanse, gdzie interpretowalność jest kluczowa, to jest poważny problem.
Podsumowanie ewolucji - co rozwiązało co
| Architektura | Rok | Rozwiązuje... | Ale nie potrafi... |
|---|---|---|---|
| Perceptron | 1958 | Klasyfikacja liniowa | XOR, nieliniowość |
| MLP | 1986 | Nieliniowe relacje | Rozumieć kolejność słów |
| RNN | 1986 | Przetwarzać sekwencje krok po kroku | Pamiętać długie zależności |
| LSTM | 1997 | Pamiętać dłużej dzięki bramkom | Przetwarzać równolegle, bardzo długie teksty |
graph LR
P["Perceptron<br/>✅ klasyfikuje<br/>❌ tylko liniowo"] -->|"dodaj warstwy"| MLP
MLP["MLP<br/>✅ nieliniowość<br/>❌ nie widzi kolejności"] -->|"dodaj rekurencję"| RNN
RNN["RNN<br/>✅ sekwencje<br/>❌ zapomina"] -->|"dodaj bramki"| LSTM
LSTM["LSTM<br/>✅ dłuższa pamięć<br/>❌ sekwencyjny = wolny"] -->|"???"| Q["<b>???</b>"]
style P fill:#ff9999,color:#000
style MLP fill:#ffcc99,color:#000
style RNN fill:#99ccff,color:#000
style LSTM fill:#cc99ff,color:#000
style Q fill:#ff6666,color:#fff
Każda generacja rozwiązywała problem poprzedniej, ale tworzyła nowy. To jest jak gra w whack-a-mole - uderzasz jednego kreta, wyskakuje następny.
I wtedy w 2017 roku ktoś zadał szalone pytanie...
"A co jeśli przestaniemy czytać po kolei?"
Metafora, która zmienia wszystko.
Wyobraźcie sobie, że czytacie powieść. Teraz macie trzy sposoby na to:
- RNN - pamiętacie ostatnie kilka zdań. Reszta się zaciera.
- LSTM - zatrzymujecie główną fabułę, zapominacie małe detale. Lepiej, ale wciąż czytacie liniowo.
- ??? - zamiast czytać słowo po słowie, patrzycie na całą stronę naraz. Wasza uwaga skacze do kluczowych postaci, ważnych momentów fabularnych, nieoczekiwanych zwrotów akcji. Nie czytacie po kolei - rozumiecie całość jednocześnie!
W 2017 roku grupa badaczy z Google opublikowała artykuł ze skromnym tytułem: "Attention Is All You Need". Ich szalona idea? A co jeśli w ogóle przestaniemy czytać po kolei? A co jeśli każde słowo będzie mogło "zobaczyć" wszystkie inne słowa naraz?
I nastąpiła rewolucja. Bo jeśli każde słowo patrzy na każde inne jednocześnie (nie czekając na swoją kolej), to:
- Nie ma sekwencyjnego wąskiego gardła - wszystko dzieje się równolegle
- GPU jest wniebowzięte - tysiące rdzeni pracujących naraz
- Długie zależności? Nie ma problemu - słowo nr 1 i słowo nr 1000 "widzą" się bezpośrednio, bez 999 kroków pośrednich
Ta architektura nazywa się Transformer. I to na niej są zbudowane ChatGPT, Claude, Gemini i wszystkie LLM, które znacie.
Podsumowanie - cała ewolucja w jednym miejscu
| Architektura | Metafora | Co potrafi | Czego nie umie? |
|---|---|---|---|
| Perceptron | Linijka | Rysuje jedną linię, klasyfikuje na 2 grupy | XOR, nieliniowość, cokolwiek złożonego |
| MLP | Zespół analityków | Modeluje nieliniowe relacje, "głębokość" | Nie rozumie kolejności, widzi wszystko naraz |
| RNN | Czytelnik słowo po słowie | Przetwarza sekwencje, ma hidden state | Krótka pamięć (vanishing gradient) |
| LSTM | Uczeń z notatnikiem i bramkami | Selekrewna pamięć, trzy bramki kontroli | Sekwencyjny = wolny, brak parallelizacji |
| Transformer | Cała strona naraz | Równoległość, uwaga na wszystko | ...do odkrycia w następnych wpisach... |
graph TD
subgraph "Od neuronu do LLM - droga, którą przeszliśmy"
P["Perceptron 1958<br/>liniowa klasyfikacja"] --> MLP
MLP["MLP 1986<br/>nieliniowość + warstwy"] --> RNN
RNN["RNN 1986<br/>sekwencje + hidden state"] --> LSTM
LSTM["LSTM 1997<br/>pamięć z bramkami"] --> T
T["Transformer 2017<br/>uwaga na wszystko naraz"]
end
style P fill:#ff9999,color:#000
style MLP fill:#ffcc99,color:#000
style RNN fill:#99ccff,color:#000
style LSTM fill:#cc99ff,color:#000
style T fill:#ff6666,color:#fff
Quiz końcowy: dopasuj architekturę2
- Chcesz przewidzieć cenę domu na podstawie metrażu, liczby pokoi i dzielnicy - jaka architektura wystarczy?
- Musisz przetworzyć zdanie 50-słowowe i określić jego sentyment (pozytywny/negatywny) - co wybierasz?
- Trenujesz model na tekście 10 000 słów i każde słowo musi "widzieć" każde inne - RNN, LSTM, czy coś innego?
- Chcesz klasyfikować punkty na płaszczyźnie na dwie grupy, ale są ułożone w kształt X (XOR) - pojedynczy perceptron wystarczy?
Ten wpis jest dość długi, ale czułem, że ta ewolucja od perceptronu do LSTM zasługuje na pełną, opowiedzianą historię.
Jeśli coś jest niejasne - napiszcie w komentarzach, postaram się wyjaśnić.
Która architektura was najbardziej zaskoczyła? Czy wiedzieliście, że "AI Winter" był spowodowany czymś tak "prostym" jak XOR?
Co w następnym wpisie? Wchodzimy w Transformer - architekturę, która zmieniła wszystko. Attention, self-attention, positional encoding, multi-head attention - wszystko to, co sprawiło, że ChatGPT w ogóle istnieje. Stay tuned!
Do następnego!
Źródła i ciekawe linki:
Jeśli chcecie wejść głębiej, oto materiały, z których korzystałem:
- Multilayer Perceptrons — d2l.ai - doskonałe, wizualne wprowadzenie do MLP: warstwy, funkcje aktywacji, przejście od regresji liniowej do sieci wielowarstwowych
- Multilayer perceptron — Wikipedia - definicje, rysunki sieci, timeline historii od 1943 do współczesności
- Multilayer Perceptron (MLP): A Practical Way to Understand Neural Networks — dev.to - świeży, bardzo intuicyjny artykuł z metaforami i porównaniem MLP vs CNN vs Transformer
- Perceptron Explained — PlainEnglish - perceptron jako "cegiełka" sieci, komponenty, intuicja geometryczna, ograniczenia
- Multi-Layer Perceptron Learning in TensorFlow — GeeksforGeeks - przejrzyste diagramy, forward propagation, backpropagation z implementacją
- Introduction to Long Short Term Memory — GeeksforGeeks - przystępne wprowadzenie do LSTM: komórka pamięci, bramki, problem long-term dependencies
- Drawbacks of LSTM Algorithm: A Case Study — SSRN - wady LSTM: złożoność obliczeniowa, overfitting, brak parallelizacji, wrażliwość na hiperparametry
- Understanding Transformer Model Types: The Evolution from RNN to Modern AI — dev.to - narracyjny opis przejścia od RNN do Transformerów, typy encoder/decoder
- LLM: Large Language Models Evolution — YouTube - film opowiadający linię rozwoju od modeli sekwencyjnych do architektur opartych na attention
-
Odpowiedzi do mini-quizu: 1) Liniowa - y = ax + b to funkcja liniowa. 2) Nieliniowy - funkcja step "łamie" liniowość. 3) Liniowy! Bez aktywacji nieliniowej, MLP z 100 warstwami = jedna wielka transformacja liniowa. To jest właśnie powód, dla którego funkcje aktywacji są niezbędne. ↩
-
Moje odpowiedzi: 1) Wystarczy prosty MLP - dane tabelaryczne, brak sekwencji. 2) LSTM - sekwencja o umiarkowanej długości, LSTM sobie poradzi. 3) Transformer - 10 000 słów to za dużo nawet dla LSTM. Transformer z self-attention pozwala każdemu słowu "widzieć" każde inne bez czekania. 4) Nie - pojedynczy perceptron nie rozwiąże XOR. Trzeba minimum MLP (2 warstwy). ↩