Od prostych neuronów do pamięci – ewolucja modeli językowych

Published 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:

  1. Każde wejście ma swoją wagę (importance) - słońce ma wagę +5, deszcz -8, temperatura +3
  2. Perceptron mnoży każde wejście przez jego wagę i dodaje wszystko (to jest ta "suma ważona")
  3. 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

  1. Zwykła regresja liniowa (y = ax + b) - liniowa czy nieliniowa?
  2. Perceptron z funkcją step (0 lub 1) - liniowy czy nieliniowy?
  3. 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:

  1. Bierze nowe słowo (x_t)
  2. Bierze swoje dotychczasowe zrozumienie (h_{t-1})
  3. Łą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

  1. Chcesz przewidzieć cenę domu na podstawie metrażu, liczby pokoi i dzielnicy - jaka architektura wystarczy?
  2. Musisz przetworzyć zdanie 50-słowowe i określić jego sentyment (pozytywny/negatywny) - co wybierasz?
  3. Trenujesz model na tekście 10 000 słów i każde słowo musi "widzieć" każde inne - RNN, LSTM, czy coś innego?
  4. 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:

  1. 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.

  2. 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).

Komentarze