A cél: a visszaterjesztést nem „varázslatként”, hanem egy irányított aciklikus számítási gráfon (DAG) végzett gépi eljárásként leírni, és pontosan bizonyítani, miért összeadódnak egy elágazó csúcshoz több útról érkező gradiens-hozzájárulások. Ez az összegzés a későbbi konvolúciós rétegek és reziduális hálók gradiensének is alapja.
Előismeret: a láncszabály és VJP 4.4. I. ellenőrző pont: matematika és kód megfelelése alfejezetből, az aktivációk lokális deriváltja 9.4. Aktivációk és gradiensáramlás alfejezetből; az optimalizáló frissítés fogalma 6.4. Momentum és adaptív optimizálók alfejezetből, a batch-átlagos veszteség skálája 6.1. Objektív, reguláris tag és gradienslépés alfejezetből. Visszautalunk a neuronra 9.1. Neuron és perceptron: ott egyetlen affin score és egy aktiváció alkotta a modellt; itt ezt kötőelemekre bontjuk.
Induló becslés. Legyen , , , és legyen , , . Ha a visszaterjesztéskor csak az -ágat vennénk figyelembe, mekkora lenne a hiba -ben? A teljes megoldás a 37.5 szakaszban: a helyes gradiens , a hiányos , így a hiba , tehát nem kicsi, hanem maga a második ág hozzájárulása.
Definíció (számítási gráf). A modell kiértékelésének számítási gráfja egy irányított gráf, ahol
A csúcsok szerepe szerint megkülönböztetünk input csúcsokat (paraméterek és bemeneti adatok), köztes csúcsokat és veszteség csúcsot, ahol skalár. A kör kizárása szükséges: kör esetén a forward-kiértékelés nem tudna megállni, mert a kiértékeléséhez önmaga értékére lenne szükség.
Definíció (topologikus sorrend). A DAG egy topologikus sorrendje a csúcsok olyan felsorolása, amelyben minden élre . Minden véges DAG-nak van topologikus sorrendje; a forward-kiértékelés egyszerűen ezt a sorrendet követi, a visszaterjesztés pedig a megfordított sorrendet.
Definíció (mentett köztes érték). Egy csúcs mentett köztes értékei (saved intermediates) azok a tenzorok, amelyeket a csúcs forward-számításakor elmentünk, mert a lokális gradiens kiszámításához a backward lépésben szükségesek. Például lokális deriváltjához a bemeneti kell, a szigmoidnál miatt elegendő az output mentése is. Implementációs döntés, hogy mit mentünk; matematikailag a lokális derivált függvénye a csúcs bemenetének.
Legyen a veszteség , és legyen valamely csúcs, amelynek gradiense már ismert: minden csúcsra a tenzort keressük, amelynek alakja megegyezik alakjával (a JELOLESEK oszlop-konvenciója szerint).
Definíció (lokális VJP). Legyen a művelet, ahol , , differenciálható. A művelet lokális VJP-leképezése a hozzárendelés, ahol a Jacobi-mátrix (sor: output-index, oszlop: input-index), és . A eredmény -beli, tehát alakja megegyezik alakjával. A VJP-t az egész Jacobi-mátrix anyagiasítása nélkül számoljuk: minden elemi művelethez zárt, kis költségű VJP-képlet tartozik.
Például az skalár művelet lokális VJP-je , tehát , ahol skalár gradienst szorozzuk az input vektorral.
A visszafelé haladás a topologikus sorrend megfordítottján halad. Amikor a csúcsot feldolgozzuk, a rá érkező összes él lokális VJP-jét kiszámítjuk, és az eredményt a megfelelő csúcs gradiens-mezőjébe hozzáadjuk. Az összeadás a 37.4 lemmából következik, és a PyTorch autograd is ezt a „grad mező +=” mechanizmust valósítja meg; a jelen könyv a [D05] hivatkozást csak kiinduló státusszal közli, a saját kódban az accumulation explicit lista alapján történik.
Elágazás (branching): amikor egy csúcsból legalább két él indul ki, azaz -t több leszármazott művelet használja. A gradiens-felhalmozás (gradient accumulation) ilyenkor kötelező: minden leszármazott hozzájárulását az -hoz rendelt gradiens-mezőben összegezzük.
Lemma (két ág gradiense összeadódik). Legyen , és differenciálható, valamint differenciálható. Definiáljuk Ha az adott pontban mindegyik függvény differenciálható, akkor A két tagot összeadjuk, nem átlagoljuk, és nem választjuk ki közülük a nagyobbat.
Bizonyítás. Definiáljuk az leképezést az oszlopvektorral. Ekkor . A Jacobi-mátrix blokkmátrix alakú: A többváltozós láncszabály (amelyet 4.3. Jacobi-mátrix, láncszabály és VJP alfejezetben az elemi függvényekre felépítettünk) szerint Mivel skalár kimenetű, . Behelyettesítve a blokkmátrixot és használva, hogy partíciója a és koordinátablokkok szerint: Ezzel a lemma állítását megkaptuk.
A lemma tetszőleges véges sok ágra indukcióval terjeszthető ki; a eset a könyv további fejezeteihez is elegendő alap. Az összegzés nem „heurisztika”, hanem a láncszabály folyománya, feltéve a sima differenciálhatóságot a vizsgált pontban. A ReLU nullpontját ezért a 9.4. Aktivációk és gradiensáramlás alfejezetben megadott konvencióval, külön kezeljük a numerikus ellenőrzés során.
Tekintsük az alábbi gráfot:
┌── a = uᵀx₁ ──┐
u ─────┤ ├── L = ½(a²+b²)
└── b = uᵀx₂ ──┘
Legyen A forward: Lokális VJP-k az egyes ágakon:
A lemma szerint Ellenőrzés elemi parciális deriválással: Az induló becslésben kérdezett „csak az első ág”-gradiens , tehát a hiba valóban a második ág teljes hozzájárulása, .
A labor célja annak bemutatása, hogy a helyes
backward egyezik a középső differenciával, a szándékosan
elhagyott ág pedig mérhető hibát okoz. A kód futtatandó a
lab/n037_graph.py fájlból a lab
munkakönyvtárban; a kimeneti blokk a képletből számított érték, nem
végrehajtott mérés jegyzőkönyve.
import numpy as np
rng = np.random.default_rng(20261020) # seed a reprodukálhatósághoz
u = np.array([1.0, -1.0], dtype=np.float64)
x1 = np.array([1.0, 2.0], dtype=np.float64)
x2 = np.array([3.0, 1.0], dtype=np.float64)
def forward(u, x1, x2):
"""A 37.5 gráf forward-ja; visszaadja L-et és a mentett értékeket."""
a = u @ x1
b = u @ x2
L = 0.5 * (a * a + b * b)
return L, (a, b, x1, x2)
def backward_both(cache):
"""Helyes backward: mindkét ág hozzájárulását összeadjuk."""
a, b, x1, x2 = cache
return a * x1 + b * x2 # dL/du
def backward_missing_second(cache):
"""Hibás kód: a második ág hozzájárulását elhagyjuk."""
a, b, x1, x2 = cache
return a * x1 # hiányzik: b*x2
def central_diff(f, u, i, eps=1e-6):
up = u.copy(); up[i] += eps
um = u.copy(); um[i] -= eps
return (f(up)[0] - f(um)[0]) / (2.0 * eps)
L, cache = forward(u, x1, x2)
num = np.array([central_diff(lambda v: forward(v, x1, x2), u, i)
for i in range(2)])
ana = backward_both(cache)
wrong = backward_missing_second(cache)
def rel_err(a, b):
denom = max(np.abs(a), np.abs(b), 1e-12)
return np.abs(a - b) / denom
print("L =", L)
print("numeric =", num)
print("analytic=", ana)
print("rel_err =", [rel_err(n, a) for n, a in zip(num, ana)])
print("missing =", wrong)
print("err_miss=", wrong - ana)Várható kimenet a képletből számolva, nem mért futtatás:
L = 2.5
numeric = [5.0 0.0] (középső differenciával, eps=1e-6, f64)
analytic= [5. 0.]
rel_err = [~1e-16, ~1e-16] (a középső differencia O(eps²) hibája mellett)
missing = [-1. -2.]
err_miss= [-6. -2.]
Az err_miss pontosan
:
a hiba nem zaj, hanem a hiányzó lokális VJP determinisztikus értéke. A
numerikus és analitikus gradiens közötti egyezést
abszolút tűréssel fogadjuk el; a középső differencia
lépésköz mellett f64-ben
elméleti hibájú, így a tapasztalt
szintű relatív hiba a kerekítésnek megfelelő. A ReLU-szerű töréspont itt
nem szerepel, tehát sima függvényen dolgozunk.
F1 (fogalmi, nehézség 1). Definiálja pontosan a
topologikus sorrendet, és magyarázza meg, miért nem lehet irányított kör
egy érvényes forward-gráfban. Adjon egy három csúcsból álló DAG-példát,
amelyben a u -> v -> w él és a u -> w
közvetlen él egyszerre szerepel; mondja meg, hány topologikus sorrendje
van, és írja fel a lehetséges sorrendeket. Átadandó: definíció,
indoklás, a példagráf és a sorrendek listája.
F2 (levezetési, nehézség 2). Legyen , , , , és legyen , , , továbbá Vezesse le -et a 37.4 lemma három ágra alkalmazásával, és számítsa ki a megadott pontban. Ellenőrizze az alakot: a gradiens -beli. Átadandó: a háromágas képlet, a részleges deriváltak, a numerikus eredmény.
F3 (implementációs, nehézség 1). Egészítse ki a 37.6
labor kódját egy harmadik ággal:
,
,
ahol
.
Írja meg a teljes backward-ot, és ellenőrizze mindkét komponensre
középső differenciával, f64, eps=1e-6 mellett.
Átadandó: futtatható kód, a három analitikus hozzájárulás, a
numeric/analytic összehasonlítás, abszolút tolerancia
.
F4 (hibakeresési, nehézség 2). Az 37.6 labor
backward_missing_second függvénye elhagyja a második ágat.
(a) Nevezze meg a hiba okát a láncszabály szintjén. (b) Mondja meg,
milyen numerikus jel utal erre a hibára, ha az eredményt középső
differenciával vetjük össze. (c) Írja le a javítást és a
visszaellenőrzés pontos tűrését. Átadandó: ok, következmény,
javított sor, a numerikus egyezés kritériuma.
F5 (kísérleti/transzfer, nehézség 3). Tegyük fel,
hogy
helyett
batch-et használunk, és a veszteség
,
ahol
az
-edik
sora. Tervezze meg a backward lépést, és magyarázza meg, hogyan
viszonyul a batch-átlag
skálája a gráf elágazási összegzéséhez. Mi lenne a gradiens, ha a
veszteséget sum reductionnel definiálnánk?
Átadandó: a gradiens képlete, a reduction-választás hatásának
indoklása, legalább egy numerikus kontrollterv a helyes skála
ellenőrzésére.
F1. A topologikus sorrend definíciója a 37.2 szakaszban szerepel. Ciklus esetén a forward-kiértékelés végtelen rekurzióba kerülne, mert egy csúcs értékének kiszámításához saját maga kellene; így a gráf aciklikussága a kiértékelhetőség feltétele. A megadott példa csúcsai , élei , , . Egyetlen érvényes topologikus sorrend van: , mert -nak és előtt, -nek pedig előtt kell állnia, és ez a két feltétel a sorrendet teljesen meghatározza.
F2. Alkalmazzuk a lemmát ágra: ha , akkor Itt , , . A parciálisai: A megadott pontban , , , így Numerikusan , , tehát
F3. A kiegészített kód:
x3 = np.array([0.0, 1.0], dtype=np.float64)
def forward3(u, x1, x2, x3):
a = u @ x1
b = u @ x2
c = 2.0 * (u @ x3)
L = 0.5 * (a*a + b*b + c*c)
return L, (a, b, c, x1, x2, x3)
def backward3(cache):
a, b, c, x1, x2, x3 = cache
return a*x1 + b*x2 + (2.0*c)*x3 # c ág lokális VJP-je 2*I miatt 2c*x3Az pontban: , , . A három hozzájárulás: Összegük . A középső differencia ugyanezt adja abszolút tűrésen belül; a kód a -ágat nem érintő komponensben is ellenőrzi a harmadik ág hatását.
F4. (a) A hiba oka: a teljes deriváltban a
-ágból
származó
tag hiányzik; a kód csak
-t
számol. (b) A numerikus jel: a középső differenciától való eltérés
pontosan
,
nem pedig kerekítési zaj; a két mennyiség közötti különbség nem csökken
az eps finomításával. (c) A javítás a
return a*x1 + b*x2 sor; a visszaellenőrzés kritériuma:
mindkét komponensre
f64 és eps=1e-6 mellett, a
37.6 szakasz rel_err stabil nevezős relatív hibájával
kiegészítve.
F5. A soronkénti elágazás változatlan: minden
sorra a gradiens-hozzájárulás a két ág összege. A batch-átlag miatt
és a teljes paramétergradiens a sorok
összege, vagyis a
alakú
mátrix
-edik
sora a fenti vektor. A
a loss-reductionból fakadó globális skála, az elágazási összeg pedig a
gráf szerkezetéből fakadó hozzájárulás-összeg; a kettő szorzatként
jelenik meg. Ha sum reduction lenne, a
skála eltűnne, és a gradiens
-szeresére
nőne; ez a batch-méret változtatásakor eltérő optimáló lépésköz-igényt
eredményezne. Numerikus kontrollterv: egy
batch esetén a teljes
-et
középső differenciával, soronkénti perturbációval ellenőrizni; a hiba
minden elemre
abszolút tűrés alatt kell maradjon.
Tipikus tévedés. A visszaterjesztést sokan azonosítják az optimalizáló paraméterfrissítésével. A kettő különböző: a backprop kiszámítja a gradienst, az optimalizáló pedig felhasználja azt a 6.4. Momentum és adaptív optimizálók alfejezetben definiált frissítésre. A backprop önmagában nem változtatja a paramétereket.
Ellenpélda. Legyen
skalár, és
.
A backward a
gradienst adja. A backward() hívás után
továbbra is
;
csak az optimalizáló lépése, például
,
módosítja. Ha valaki a backward hívás után azonnal
-et
várna, összekeverné a gradiensszámítást a paraméterfrissítéssel. Ugyanez
a hiba előfordulhat batch-átlag esetén: a gradiensben a
skála a loss definíciójából következik, nem az optimalizáló
beállításából.
Önellenőrzés.
mean reductionje esetén a
megjelenik a gradiensben, míg sum esetén nem?eps és tűrés mellett történt; töréspontos függvényt csak
sima pontban ellenőriztem?Visszahívás. A 4.4. I. ellenőrző pont: matematika és kód megfelelése alfejezetben az affin réteg lokális VJP-jeit bizonyítottuk; ott a lépés ugyanennek a „lokális VJP” gépezetnek egy esete. A 6.1. Objektív, reguláris tag és gradienslépés alfejezetben látott batch-átlag az itteni skálát adja, a 9.1. Neuron és perceptron perceptronjának score-ja pedig egyetlen gráf-él, amelyet a következő egységben kötünk össze.
A számítási gráf egy DAG, amelynek csúcsai tenzorok, élei a felhasználási relációk. A forward a topologikus sorrendben halad, és menti a backwardhoz szükséges köztes értékeket. A visszaterjesztés a megfordított sorrendben minden élre lokális VJP-t számol, alakban. A központi lemma azt mondja ki és bizonyítja a többváltozós láncszabályból, hogy elágazó csúcsnál a leszármazott ágak gradiensei összeadódnak. Kézzel és NumPy-ban megmutattuk, hogy egy ág elhagyása nem kicsi numerikus zajt, hanem a hiányzó tag pontos értékét okozza. A backprop gradiensszámítás, nem paraméterfrissítés.
A következő 10.2. Affín réteg és aktiváció backward egységben az affin réteg és az elemenkénti aktiváció konkrét backward-képleteit vezetjük le ezzel a gépezettel, és az ennek során felépített VJP-készletből tanuló MLP-t építünk.