Kurs: sieci neuronowe od środka · Lekcja 5 z 12 · 13 min
Propagacja wsteczna krok po kroku
W skrócie
Sieć przesuwa wagi tam, gdzie wskazuje gradient. Liczymy go dla jednego neuronu regułą łańcuchową, węzeł po węźle, i sprawdzamy bez wzorów.
Rozgrzewka Dwa pytania z kursu ML, na których ta lekcja się opiera. Minuta, a lekcji nie blokuje.
W kursie ML spadek gradientu schodził po zboczu straty: krok w stronę, w którą strata maleje najszybciej. Do tego trzeba znać gradient, czyli to, jak strata zmieni się, gdy ruszymy każdą wagą osobno. Sieć z lekcji 2 ma 2410 wag. Skąd wie, w którą stronę przesunąć każdą z nich? Odpowiedź to propagacja wsteczna: reguła łańcuchowa stosowana węzeł po węźle, od straty do wag. Zobaczymy ją na jednym neuronie, gdzie każdą liczbę da się sprawdzić.
Wybierz odpowiedź i pewność.
Odpowiedz, żeby odblokować lab.
Pokaż odpowiedź
Poprawna odpowiedź: Ujemny: zwiększenie w₁ obniży stratę
∂L/∂w₁ = −0,255. Różnica skończona, liczona bez reguły łańcuchowej, daje to samo −0,255. Neuron daje 0,634, a cel to 1, więc większa suma ważona przybliża wynik do celu, a x₁ jest dodatnie.
Spróbuj
- Klikaj „krok ▶” i patrz, jak wartości (pod węzłami) idą w prawo, a gradienty (nad węzłami) wracają w lewo.
- Na ostatnim kroku porównaj ∂L/∂w₁ z różnicą skończoną. Przy w₁ = 0,5 obie dają −0,255.
- Przesuwaj suwak w₁ i znajdź miejsce, gdzie gradient jest bliski zera. To cel labu.
- Ustaw w₁ = −3. Strata wynosi 0,982, prawie najwięcej, a gradient tylko −0,0266, ponad 16 razy mniej niż przy w₁ = −0,5. Zobacz na kroku 6, która pochodna go zjada.
Pomysł w prostych słowach
Neuron liczy po kolei: dwa mnożenia, sumę, sigmoidę i stratę. Każdy z tych kroków to węzeł grafu. Przejście w przód liczy wartość każdego węzła i ją zapamiętuje. Przy w₁ = 0,5 suma to z = 0,55, sigmoida daje a = 0,634, strata L = 0,134.
Przejście wstecz idzie od straty do wag. Każdy węzeł zna tylko swoją lokalną pochodną: jak jego wynik zmienia się, gdy ruszy się jego wejście. Gradient, który przyszedł z prawej, węzeł mnoży przez tę lokalną pochodną i podaje dalej w lewo. Strata mówi: ∂L/∂a = 2(a − 1) = −0,732. Sigmoida mnoży to przez swoją pochodną a(1 − a) = 0,232 i daje ∂L/∂z = −0,170. Suma podaje to bez zmian każdemu składnikowi, a mnożenie w₁·x₁ mnoży przez x₁ = 1,5. Wynik: ∂L/∂w₁ = −0,255.
Dlaczego to działa
To reguła łańcuchowa z analizy: pochodna złożenia funkcji to iloczyn pochodnych kolejnych kroków. Graf obliczeń rozpisuje to złożenie na węzły, więc iloczyn można liczyć kawałek po kawałku. Gdy węzeł ma kilka wyjść, gradienty z nich się dodają.
Sprawdzian bez reguły łańcuchowej to różnica skończona: policz stratę przy w₁ trochę większym i trochę mniejszym, odejmij i podziel przez odległość. Przy h = 0,0001 wychodzi ta sama liczba, z różnicą rzędu 10⁻¹⁰. Taki test opłaca się przy kilku wagach, ale nie przy całej sieci: dla 2410 wag to 4820 przejść w przód. Propagacja wsteczna daje wszystkie 2410 pochodnych naraz, za koszt jednego przejścia w przód i jednego wstecz. Biblioteki robią to same: to automatyczne różniczkowanie.
Lab pokazuje też słabość sigmoidy. Przy w₁ = −3 neuron odpowiada 0,009 zamiast 1, a pochodna sigmoidy w tym miejscu to tylko 0,009. Mnożenie przez tak małą liczbę dusi gradient: tam, gdzie neuron myli się najbardziej, uczy się wolno. Gradient bliski zera nie znaczy więc, że jest dobrze. Przy w₁ = 2 i więcej strata jest mała i gradient też, bo nie ma czego poprawiać.
Częsty błąd
Prawda czy fałsz: „Propagacja wsteczna to sposób, w jaki sieć się uczy.” Fałsz. Propagacja wsteczna tylko liczy gradient. Wagi przesuwa optymalizator, i ten sam gradient można podać różnym. W kodzie niżej −0,255 trafia do dwóch reguł kroku. Zwykły spadek gradientu z krokiem 0,5 przesuwa w₁ z 0,5 do 0,627. Adam z krokiem 0,1 przesuwa w₁ do 0,600, bo w pierwszym kroku rusza wagę dokładnie o długość kroku, niezależnie od wielkości gradientu. Wybór kroku i optymalizatora to temat lekcji 6.
Quiz
Trzy pytania na sprawdzenie się. Wybierz odpowiedź albo rozwiń odpowiedź pod pytaniem. Z klawiatury: strzałki wybierają, Enter albo spacja zatwierdza.
Najważniejsze wnioski
- Przejście w przód liczy i zapamiętuje wartość każdego węzła; przejście wstecz liczy gradienty od straty do wag.
- Każdy węzeł mnoży gradient z prawej przez swoją lokalną pochodną: to cała reguła łańcuchowa.
- Różnica skończona sprawdza gradient bez wzorów, ale propagacja wsteczna daje wszystkie pochodne za jeden przebieg tam i z powrotem.
- Propagacja wsteczna liczy gradient; krok robi optymalizator.
Głębiej: wzór, kod, praktyka
Matematyka
Neuron z labu: z = w₁x₁ + w₂x₂ + b, a = σ(z), L = (a − y)². Reguła łańcuchowa:
∂L/∂a = 2(a − y);∂L/∂z = ∂L/∂a · σ′(z), gdzieσ′(z) = a(1 − a);∂L/∂w₁ = ∂L/∂z · x₁,∂L/∂w₂ = ∂L/∂z · x₂,∂L/∂b = ∂L/∂z.- Różnica skończona (centralna):
∂L/∂w₁ ≈ [L(w₁ + h) − L(w₁ − h)] / 2h; jej błąd maleje jakh². - Krok spadku gradientu:
w₁ ← w₁ − η · ∂L/∂w₁. Pierwszy krok Adama:w₁ ← w₁ − η · g / |g|.
W kodzie (Python)
Neuron z labu w numpy: przejście w przód, przejście wstecz, różnica skończona i ten sam gradient w dwóch regułach kroku:
import numpy as np
x1, x2, w2, b, y = 1.5, -0.5, 0.8, 0.2, 1.0
sigmoid = lambda z: 1 / (1 + np.exp(-z))
loss = lambda w1: (sigmoid(w1 * x1 + w2 * x2 + b) - y) ** 2
# przejście w przód: zapamiętujemy wartość każdego węzła
w1 = 0.5
z = w1 * x1 + w2 * x2 + b
a = sigmoid(z)
L = (a - y) ** 2
print(f"forward: z = {z:.3f}, a = {a:.4f}, L = {L:.4f}")
# przejście wstecz: każdy węzeł mnoży gradient z prawej przez swoją lokalną pochodną
dL_da = 2 * (a - y)
dL_dz = dL_da * a * (1 - a)
dL_dw1 = dL_dz * x1
print(f"backward: dL/da = {dL_da:.4f}, dL/dz = {dL_dz:.4f}, dL/dw1 = {dL_dw1:.4f}")
# sprawdzenie bez reguły łańcuchowej: różnica skończona
h = 1e-4
fd = (loss(w1 + h) - loss(w1 - h)) / (2 * h)
print(f"finite difference: {fd:.4f}, gap {abs(fd - dL_dw1):.1e}")
# ten sam gradient, dwie reguły kroku: to robi optymalizator, nie propagacja wsteczna
sgd = w1 - 0.5 * dL_dw1
m, v = 0.1 * dL_dw1, 0.001 * dL_dw1 ** 2 # Adam, krok 1 (beta1 = 0.9, beta2 = 0.999)
adam = w1 - 0.1 * (m / 0.1) / (np.sqrt(v / 0.001) + 1e-8)
print(f"after one step: SGD (lr 0.5) w1 = {sgd:.4f}, Adam (lr 0.1) w1 = {adam:.4f}")
Wynik
forward: z = 0.550, a = 0.6341, L = 0.1339
backward: dL/da = -0.7317, dL/dz = -0.1698, dL/dw1 = -0.2547
finite difference: -0.2547, gap 1.1e-10
after one step: SGD (lr 0.5) w1 = 0.6273, Adam (lr 0.1) w1 = 0.6000
Liczby są te same co w labie przy w₁ = 0,5. Adam w pierwszym kroku dzieli gradient przez jego własną wielkość, więc zostaje sam znak razy długość kroku. Uruchomione w numpy 2.1.3.
Pobierz notebook (.ipynb). Otwórz go w Jupyterze albo wgraj do Google Colab; brakujące dane pobierze sam.
Dla praktyka
- Pisząc własną warstwę albo własną stratę, sprawdź gradient różnicą skończoną na kilku wagach, jak w kodzie wyżej. Różnica o wiele rzędów większa niż tam zwykle oznacza błąd we wzorze.
- W praktyce gradient liczy biblioteka (automatyczne różniczkowanie w PyTorch, JAX czy TensorFlow). Ty wybierasz stratę, budowę sieci i optymalizator.
- Zapamiętane wartości z przejścia w przód zajmują pamięć. To one, a nie same wagi, często kończą pamięć karty graficznej przy dużych paczkach.
- Gradient bliski zera na początku treningu to sygnał ostrzegawczy: sprawdź, czy aktywacje nie są nasycone (lekcja 7).
Więcej w Atlasie
Hasła encyklopedii o pojęciach z tej lekcji: