ML Atlas

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.
Z kursu ML, lekcja 1.6: Spadek gradientu: nauka małymi krokami Co gradient mówi algorytmowi w każdym kroku?

Pokaż odpowiedź

Poprawna odpowiedź: W którą stronę i jak stromo rośnie strata w bieżącym punkcie

Gradient wskazuje w górę, więc krok w przeciwną stronę obniża stratę, ale opisuje tylko najbliższe otoczenie bieżącego punktu.

Z kursu ML, lekcja 7.1: Sieci neuronowe: wiele prostych elementów razem Co liczy propagacja wsteczna?

Pokaż odpowiedź

Poprawna odpowiedź: Ile każda waga dołożyła do błędu, żeby gradient mógł ją poprawić

Propagacja wsteczna stosuje regułę łańcuchową od wyjścia wstecz i daje gradient dla każdej wagi.

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

Przewidź, potem sprawdź Neuron ma dwa wejścia, x₁ = 1,5 i x₂ = −0,5, wagi w₁ = 0,5 i w₂ = 0,8, bias 0,2 i sigmoidę na wyjściu. Cel to 1, a strata to (a − 1)². Gradient ∂L/∂w₁ jest dodatni czy ujemny? Czyli: zwiększenie w₁ obniży stratę czy ją podniesie?
Jak bardzo jesteś pewny(-a)?

Eksperyment

Ten interaktywny eksperyment działa w przeglądarce z włączonym JavaScriptem. Tekst wokół opisuje, co w nim widać. W eksperymencie: graf obliczeń jednego neuronu L = (σ(w₁x₁ + w₂x₂ + b) − y)² liczony krok po kroku: najpierw wartości w przód, potem gradienty wstecz z regułą łańcuchową; przy w₁ = 0,5 wychodzi ∂L/∂w₁ = −0,255, tyle samo co z różnicy skończonej.

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.

1. Co liczy propagacja wsteczna?
Pokaż odpowiedź

Poprawna odpowiedź: Pochodną straty po każdej wadze, regułą łańcuchową od straty do wag

Propagacja wsteczna daje gradient; wagi zmienia dopiero optymalizator.

2. Gradient z prawej to ∂L/∂z = −0,170. Węzeł mnożenia liczy w₁·x₁, a x₁ = 1,5. Ile wynosi ∂L/∂w₁?
Pokaż odpowiedź

Poprawna odpowiedź: −0,255, bo mnożenie mnoży gradient przez drugi czynnik, x₁

Pochodna w₁·x₁ po w₁ to x₁, więc gradient z prawej mnożymy przez 1,5.

3. Przy w₁ = −3 strata wynosi 0,982, a gradient tylko −0,0266. Dlaczego tak mały?
Pokaż odpowiedź

Poprawna odpowiedź: Bo sigmoida jest nasycona: jej pochodna 0,009 dusi gradient, choć błąd jest duży

Gradient płynie przez pochodną sigmoidy; na płaskim końcu krzywej jest ona bliska zera.

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 jak h².
  • 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.

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: