Backprop ručně

Derivace na jednom neuronu, krok za krokem s čísly.

Co se naučíš: Spočítáš si gradient na jednom neuronu ručně a ověříš si ho numericky.

9 min čtení + cvičeníNavazuje na:⏩ Trénink vs generování

Loss a gradient říká, že „gradient se spočítá jedním průchodem zpět“. To je pravda, ale nic to nevysvětlí. Tady si to spočítáme na jednom neuronu, na papíře, s konkrétními čísly a uvidíš, že backpropagation není algoritmus, který si musíš pamatovat. Je to jen řetízkové pravidlo z derivací, aplikované pozpátku.


Náš neuron

Nejmenší možný příklad. Dva vstupy, dvě váhy, bias, aktivace a jedno číslo na výstupu:

z = x₁·w₁ + x₂·w₂ + b
h = tanh(z)
L = (h − y)²          ← jak moc jsme se spletli oproti cíli y

Dosadíme:

x₁ = 2,0    w₁ =  0,5
x₂ = −1,0   w₂ = −0,3
b  = 0,1     y  =  1,0     ← cíl

Dopředný průchod

z = 2,0 × 0,5  +  (−1,0) × (−0,3)  +  0,1
  = 1,0 + 0,3 + 0,1
  = 1,4

h = tanh(1,4) = 0,885352

L = (0,885352 − 1,0)² = 0,013144

Máme chybu 0,0131. Otázka zní: kterým směrem posunout w₁, w₂ a b, aby byla menší?


Zpětný průchod, krok za krokem

Jdeme od konce a ptáme se pořád na totéž: jak se změní L, když se změní tohle?

1. Jak L závisí na h

L = (h − y)², tedy derivace je 2(h − y):

∂L/∂h = 2 × (0,885352 − 1,0) = −0,229297

Záporné číslo znamená: kdyby h bylo větší, loss by klesla. Dává smysl, jsme pod cílem.

2. Jak h závisí na z

Derivace tanh je 1 − tanh², což je hezké, protože tanh(z) už máme spočítané:

∂h/∂z = 1 − 0,885352² = 0,216152

3. Řetízkové pravidlo: jak L závisí na z

A tady je celý trik backpropu, prostě se to vynásobí:

∂L/∂z = ∂L/∂h × ∂h/∂z = −0,229297 × 0,216152 = −0,049563

Tomuhle číslu se říká „gradient, který dotekl do uzlu z“. Všechno další se z něj odvodí.

4. Rozdělení mezi vstupy

z = x₁w₁ + x₂w₂ + b, takže derivace podle jednotlivých členů jsou triviální:

∂z/∂w₁ = x₁ = 2,0        →  ∂L/∂w₁ = −0,049563 × 2,0  = −0,099126
∂z/∂w₂ = x₂ = −1,0       →  ∂L/∂w₂ = −0,049563 × −1,0 =  0,049563
∂z/∂b  = 1               →  ∂L/∂b  = −0,049563

Všimni si dvou věcí:

  • Gradient váhy je úměrný jejímu vstupu. w₁ má dvakrát větší vstup než w₂, tak dostane dvakrát větší gradient. Proto se vstupy normalizují, jinak by se některé váhy učily mnohem rychleji než jiné.
  • Znaménko se obrátilo u w₂, protože jeho vstup je záporný.

A kdybychom pokračovali dál doleva (v hlubší síti), pošleme gradient i do vstupů:

∂L/∂x₁ = ∂L/∂z × w₁ = −0,049563 × 0,5 = −0,024782

Tohle je přesně to, co se v hluboké síti předává do předchozí vrstvy.


Kontrola: numerická derivace

Nemusíš mi věřit. Derivaci jde ověřit hrubou silou, posuň váhu o kousek a podívej se, co udělá loss:

eps = 1e-6
num = (loss(w1 + eps) - loss(w1 - eps)) / (2 * eps)
analyticky:  ∂L/∂w₁ = −0,099126
numericky:   ∂L/∂w₁ = −0,099126     ✓

Sedí. Tohle je mimochodem standardní test při psaní vlastních vrstev: gradient check. Když se analytický a numerický gradient rozejdou, máš chybu v backwardu.


Krok učení

Gradient říká, kterým směrem loss roste, tak jdeme opačně:

w₁ ← 0,5  − 0,5 × (−0,099126) = 0,5496
w₂ ← −0,3 − 0,5 × ( 0,049563) = −0,3248
b  ← 0,1  − 0,5 × (−0,049563) = 0,1248

A přepočítáme:

z = 1,4744      h = 0,9006      L = 0,007470

Loss klesla z 0,013144 na 0,007470. Přesně o tomhle je celý trénink, jen se to místo tří parametrů dělá pro sedm miliard a místo jednou pro milion kroků.


Proč je to efektivní

Naivní přístup by pro každý parametr zkusil, co udělá jeho změna, u modelu se 7 miliardami parametrů tedy 7 miliard dopředných průchodů na jeden krok učení. Nemyslitelné.

Backprop místo toho spočítá všechny gradienty jedním průchodem zpět, protože každý uzel potřebuje jen dvě věci:

  1. gradient, který k němu dotekl shora,
  2. lokální derivaci vlastní operace.

Vynásobí je a pošle dál. Cena zpětného průchodu je zhruba dvojnásobek dopředného, bez ohledu na to, kolik má síť parametrů.

naivně:  N parametrů → N průchodů
backprop: N parametrů → 1 průchod (2× cena forwardu)

Jak to vypadá v opravdovém modelu

Přesně tyhle dva kroky, jen s maticemi. Podívej se na funkci backward v kapitole postav si vlastní model, každý řádek je „gradient shora × lokální derivace“:

g['Wu'] = h.T @ dlogits        # gradient váhy = vstup × gradient výstupu
dh = dlogits @ P['Wu'].T       # gradient posílaný dál doleva

Je to totéž jako ∂L/∂w₁ = ∂L/∂z × x₁, jen hromadně pro celou matici.


Cvičení

  1. Spočítej gradient pro w₂, kdyby byl cíl y = 0 místo y = 1. Změní se znaménko?
  2. Co se stane s gradientem, když je z velké (třeba 5)? Spočítej 1 − tanh²(5) a zamysli se, proč se tomu říká nasycený neuron.
  3. Proč se pro kontrolu používá (f(w+ε) − f(w−ε)) / 2ε a ne (f(w+ε) − f(w)) / ε?
Náčrt řešení: rozbal, až si cvičení zkusíš sám
  1. Ano, znaménko se otočí: z +0,049563 na −0,382742. Řetízek je dL/dw₂ = 2(h − y) × (1 − tanh²z) × x₂. Prostřední člen 0,216152 je kladný vždy, x₂ = −1 je záporné vždy, takže o znaménku rozhoduje jen (h − y). Při y = 1 model podstřeluje (h < y), při y = 0 přestřeluje. Gradient je navíc při y = 0 skoro osmkrát větší, protože chyba je větší.
  2. 1 − tanh²(5) = 0,000182, oproti 0,216152 při z = 1,4 je to 1190krát méně. Neuron je nasycený: tanh je u pěti prakticky na jedničce a je tam plochý, takže sebevětší změna vah výstupem skoro nehne. Gradient se tím vynásobí a zmizí. Přesně proto se aktivace normalizují a proto se inicializuje malými čísly, aby se model do plochých oblastí vůbec nedostal.
  3. Protože centrální diference má chybu řádu ε², zatímco jednostranná řádu ε. Rozvineš-li f(w ± ε) do Taylorovy řady, u centrální varianty se členy s druhou derivací odečtou a zbyde až třetí derivace. Prakticky to znamená o několik řádů přesnější kontrolu při stejném ε, což je při ověřování gradientů rozdíl mezi „sedí“ a „nevím“.

Shrnutí

  • Backprop = řetízkové pravidlo aplikované od konce: gradient shora × lokální derivace.
  • Gradient váhy je úměrný jejímu vstupu, proto se vstupy normalizují.
  • Analytický gradient se dá ověřit numericky; je to standardní test vlastních vrstev.
  • Jeden zpětný průchod spočítá gradienty všech parametrů za dvojnásobek ceny forwardu.
Proč je gradient váhy úměrný jejímu vstupu?

Protože v součtu z = x·w + … je derivace podle w rovna právě x. Váha, která dostává velký vstup, tak dostane velký gradient a mění se rychleji. Právě proto se vstupy a aktivace normalizují, jinak by se různé části sítě učily nesouměřitelně rychle.

Jak ověříš, že máš správně napsaný backward vlastní vrstvy?

Gradient checkem: spočítáš derivaci numericky jako (f(w+ε) − f(w−ε)) / 2ε a porovnáš s analytickým gradientem. Když se rozcházejí, je chyba v backwardu. Je to standardní test při implementaci nových vrstev.

Proč nejde gradienty počítat prostě tak, že pro každý parametr zkusíme, co udělá jeho změna?

Protože by to znamenalo jeden dopředný průchod na každý parametr, u modelu s miliardami parametrů miliardy průchodů na jediný krok učení. Backprop spočítá všechny gradienty jedním zpětným průchodem, který stojí zhruba dvojnásobek dopředného.