Ez az ellenőrző pont a 4.1.–4.3. láncát zárja le: egyetlen kis számítási gráfon keresztülmegyünk a jelöléstől a kézi visszaterjesztésen át a véges differenciás kódellenőrzésig. A cél nem egy új rétegtípus bevezetése, hanem az, hogy a matematikai képlet és a kód közötti megfelelést típus szerint ellenőrizni tudjuk. Az előismeret a 4.3. Jacobi-mátrix, láncszabály és VJP láncszabály-, VJP- és affinréteg-anyaga; a 3.4. Sajátérték, SVD és spektrális határ normafogalmát csak a numerikus hibabecslés egy pontján idézzük vissza.
Értékelés. A közös 0–4 rubrikát, a PASS-szabályt és a hibatípusonkénti visszautakat az 1.4. Tanulási diagnózis és kísérleti napló „Tanulási főút és ellenőrző pontok” szakasza adja meg; itt csak az eltérés: a PASS (a) pontja itt a kézi backward és a központi differencia egyezése a fejezet toleranciáján belül; a súlypont a D1 és a D2.
Induló becslés: egy , méretű affin réteg belső Jacobi-mátrixa skalár. A backpropagation viszont nem ezt a mátrixot tárolja, hanem a kimeneti gradienst és a súlymátrixszal vett szorzatokat. Becsülje meg, hány skalárt kell egyszerre tárolnia a VJP-alapú visszaterjesztésnek ennél a rétegnél! (Várható válasz: a -gradiens eleme, az -gradiens eleme, tehát nagyságrendileg , nem .)
A kiinduló konvenciót egyszer rögzítjük.
Definíció (gradiens alakja). Ha differenciálható valós értékű függvény, akkor az -szel azonos alakú tensor, és Speciálisan esetén ; esetén .
Tehát a „gradiens alakja megegyezik a paraméter alakjával” konvenciót
használjuk mindvégig. Ez összhangban van azzal, ahogyan az
autograd-rendszerek a .grad tenzort visszaadják; az
autograd mechanizmusának hivatalos hátteréhez lásd [D05], de a jelen
levezetés és kód saját, NumPy-alapú ellenőrzés.
A teljes gráf három lépésből áll: affin réteg, elemenkénti sima nemlinearitás, majd egy olyan skalár veszteség, amelynek gradiense kellemesen egyszerű.
Legyen a bemenet, a súly, az eltolás. Definiáljuk:
Itt folytonosan differenciálható elemenkénti függvény, . A veszteség a checklistben bevezetett kivételes alak, amelyre ; ez nem MSE és nem a regularizálás tagja.
Lemma. A fenti gráfban legyen . Ekkor ahol elemenkénti szorzás és .
Bizonyítás. A bizonyítást elemenként végezzük, és minden lépésnél ellenőrizzük az alakot.
, ezért tehát .
Az elemenkénti műveletben csak -t befolyásolja, ezért a láncszabállyal A vektoralak , ahol mindkét tényező -beli.
Az affin rész: . Az bemeneti koordináta minden -ben a együtthatóval jelenik meg, tehát Az eredmény és szorzata, azaz . Itt a szorzó a VJP-irány: a láncban a -gradienst visszük át a bemenetre.
A súly szerinti parciális deriváltban csak függ -től, és , ezért Az outer szorzat és tényezőkből mátrixot ad, azonosat alakjával.
A eltolásra , így
Ezzel a gráf minden élén megkaptuk a visszaterjesztett gradienst, és minden lépésben feltüntettük a tenzoralakot. A batchos eset 4.3. Jacobi-mátrix, láncszabály és VJP alfejezetben a és alakú általánosítás; az itteni eset az outer szorzatos formula.
Legyen , , , és
Előrehaladás:
Mivel , a visszaterjesztés:
Ezután
Az ellenőrző laborhoz egy második, kisebb gradiensű pontot is rögzítünk, hogy a hibamérce nullközeli értékeken is értelmes legyen.
A forward:
A backward:
Az analitikus gradienst középső differenciával hasonlítjuk össze, mert ennek csonkolási hibája rendű, szemben az előredifferencia rendű hibájával. Egy irányú perturbációra
Ha az iránymenti harmadik deriváltak korlátosak, a Taylor-sorral
ahol a gépi pontosság f64 esetén (), a második tag pedig a két közeli függvényérték kivonásának kerekítési hibája. Az első tag akkor kicsi, ha az ellenőrzési pont sima: a ReLU nullpontjában nincs klasszikus derivált, ezért a véges differenciás ellenőrzést törésponttól távol vagy sima -re végezzük.
A két hibaforrás ellentétesen függ -tól: a csonkolás -tel csökken, a kerekítés viszont -nal nő. Az optimális lépésköz ezért nagyságrendileg . A P0 példában , ezért
A laborban f64 lépésközt használunk; ezzel a hibabecslés körülbelül nagyságrendű. Az ellenőrzést a stabil hibaképlettel végezzük:
ahol az abszolút, a relatív tűrés. A nullközeli gradiensnél az abszolút tag dominál, ezért a P1 pont kis gradienseinél is van értelmes elfogadási küszöb.
A labor egyetlen fájl a lab/n016/ munkakönyvtárban. A
futtatás CPU-n történik, float64 NumPy dtype-pal; GPU vagy PyTorch nem
szükséges. A futtatási parancs:
python n016_gradcheck.py --seed 20261020Az alábbi kód a P0 és P1 pontot rögzített várható értékekkel ellenőrzi, majd egy seedelt véletlen ponton is lefuttatja ugyanazt az ellenőrzést. A P0/P1-hez tartozó kimenetet a fenti kézi számítás adja; az alábbi várt sor jelölt számított kimenet, nem ebben az előkészítésben rögzített futási jegyzőkönyv.
"""n016_gradcheck.py – teljes kis gráf gradiensellenőrzése.
A gráf: z = Wx + b, y = z**2, L = 0.5 * sum(y**2).
A kód f64-ben, középső differenciával ellenőrzi x, W és b gradiensét,
majd egy normalizált v irányban a VJP/JVP dualitást is.
"""
import argparse
import numpy as np
def phi(z):
return np.square(z)
def phi_prime(z):
return 2.0 * z
def loss(x, W, b):
return 0.5 * np.sum(np.square(W @ x + b))
def backward(x, W, b):
z = W @ x + b
y = phi(z)
g_z = y * phi_prime(z)
g_x = W.T @ g_z
g_W = np.outer(g_z, x)
g_b = g_z
return g_x, g_W, g_b
def close(a, b, atol=1e-7, rtol=1e-7):
diff = np.abs(a - b)
scale = np.maximum(np.abs(a), np.abs(b))
return bool(np.all(diff <= atol + rtol * scale))
def finite_diff_all(x, W, b, eps=1e-6):
num_x = np.zeros_like(x)
for i in range(x.shape[0]):
xp, xm = x.copy(), x.copy()
xp[i] += eps
xm[i] -= eps
num_x[i] = (loss(xp, W, b) - loss(xm, W, b)) / (2 * eps)
num_W = np.zeros_like(W)
for i in range(W.shape[0]):
for j in range(W.shape[1]):
Wp, Wm = W.copy(), W.copy()
Wp[i, j] += eps
Wm[i, j] -= eps
num_W[i, j] = (loss(x, Wp, b) - loss(x, Wm, b)) / (2 * eps)
num_b = np.zeros_like(b)
for i in range(b.shape[0]):
bp, bm = b.copy(), b.copy()
bp[i] += eps
bm[i] -= eps
num_b[i] = (loss(x, W, bp) - loss(x, W, bm)) / (2 * eps)
return num_x, num_W, num_b
def check_point(name, x, W, b, eps=1e-6):
g_x, g_W, g_b = backward(x, W, b)
n_x, n_W, n_b = finite_diff_all(x, W, b, eps=eps)
ok_x = close(g_x, n_x)
ok_W = close(g_W, n_W)
ok_b = close(g_b, n_b)
v = np.array([0.3, -0.6, 0.2], dtype=np.float64)
v = v / np.linalg.norm(v)
dir_num = (loss(x + eps * v, W, b) - loss(x - eps * v, W, b)) / (2 * eps)
dir_ana = float(v @ g_x)
ok_dir = close(dir_ana, dir_num)
print(f"{name}: x={ok_x}, W={ok_W}, b={ok_b}, iranymenti={ok_dir}")
print(f" max|x-hiba|={np.max(np.abs(g_x - n_x)):.2e}, "
f"max|W-hiba|={np.max(np.abs(g_W - n_W)):.2e}, "
f"max|b-hiba|={np.max(np.abs(g_b - n_b)):.2e}")
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--seed", type=int, default=20261020)
args = ap.parse_args()
rng = np.random.default_rng(args.seed)
# P0: a szövegben kézzel kiszámolt pont
x0 = np.array([1.0, 2.0, -1.0], dtype=np.float64)
W0 = np.array([[0.5, -1.0, 2.0],
[1.0, 0.5, -1.0]], dtype=np.float64)
b0 = np.array([0.2, -0.3], dtype=np.float64)
check_point("P0", x0, W0, b0)
# P1: második, kis gradiensű kontrollpont
x1 = np.array([0.5, -2.0, 1.5], dtype=np.float64)
W1 = np.array([[2.0, 0.0, -1.0],
[0.0, 1.0, 1.0]], dtype=np.float64)
b1 = np.array([0.1, 0.0], dtype=np.float64)
check_point("P1", x1, W1, b1)
# Seedelt új pont: a kimenet ezen a gépen reprodukálható
xr = rng.standard_normal(3)
Wr = rng.standard_normal((2, 3))
br = rng.standard_normal(2)
check_point("random_seed", xr, Wr, br)
if __name__ == "__main__":
main()A P0 pontra a levezetett analitikus gradiens és a fenti lépésköz
mellett a maximális eltérés a
–
nagyságrendbe eső értéket vesz fel; a close függvény ezért
mellett zöld eredményt ad. Ugyanez P1-re a kisebb függvényérték miatt
még kedvezőbb. A seedelt véletlen pont egy független harmadik
ellenőrzés; önmagában azonban nem helyettesíti a levezetést, csak
numerikus hibakeresési bizonyítékot ad.
Tipikus hiba lenne azt állítani, hogy „a gradienskód helyes, mert egy ponton megegyezett a véges differenciával”. Ez logikailag nem következik. Ellenpélda a valós függvények szintjén:
A pontban és , tehát érték és gradiens is egyezik; ennek ellenére , míg , tehát a két függvény máshol különbözik. Ugyanígy egy hibás backward megvalósítás lehet, hogy P1-en a kis gradiensek és a laza tűrés miatt átmegy, P0-on viszont eltér. Az F4 feladat erre épít.
F1 (fogalmi, nehézség 1). Magyarázza el, miért tárol el kevesebb számot a VJP-alapú backpropagation, mint a teljes Jacobi-mátrix, ha és . Adja meg mindkét tárigényt skalárok számában, és mondja ki a gradiens alakjára vonatkozó konvenciót.
F2 (levezetési, nehézség 2). Tekintse ugyanazt a gráfot, de a veszteség legyen
Vezesse le, hogy , és adja meg ezzel az új képletét. Ellenőrizze, hogy visszaadja a lemmabeli -t.
F3 (implementációs, nehézség 2). Írjon egy
safe_close(a, b, atol, rtol) NumPy-függvényt a stabil
hibaképlethez, és egészítse ki a labor kódját úgy, hogy minden
check_point hívás kilépőkódot adjon, ha az ellenőrzés nem
teljesül. Adjon egy kis tesztet, amelyben a nullközeli értékre az
abszolút tűrés dominál.
F4 (hibakeresési, nehézség 2). Az alábbi hibás backward
def backward_bad(x, y, W):
g_z = y # hiba: phi'(z) kimaradt
return W.T @ g_z, np.outer(g_z, x), g_za P1 ponton , szemben a korrekt értékkel. Mutassa meg, hogy a maximális abszolút eltérés , tehát egy tűrésű teszt zöldet adna. Mi a hiba következménye egy tanítási lépésben, és milyen két módosítással deríthető ki megbízhatóan?
F5 (kísérleti/transzfer, nehézség 3). Tervezzen ellenőrzési protokollt arra a kérdésre, hogy a fenti gradiensképletek float32 betanítási főúton is elfogadhatóan működnek-e. Adjon meg legalább egy lehetséges tervet: hány seedelt pont, milyen , milyen baseline, milyen tűrés és milyen következtetési korlát. Ne feltételezzen előre megadott győztest.
F1. A teljes mátrix skalár. A VJP-úton a -gradiens , a bemeneti gradiens , tehát a köztes tárigény nagyságrendileg és a mátrixszorzások a irányban végzik el ugyanazt az információáramlást. A konvenció: alakja megegyezik alakjával; parciális deriváltja minden -indexhez egy skalár.
F2. Legyen . Ekkor
mert . Ezért ; a lánc második lépése változatlan,
Az esetben , tehát visszakapjuk a lemmabeli formulát. Alakellenőrzés: , , tehát ; az elemenkénti szorzat is .
F3. Egy teljes minimális függvény:
def safe_close(a, b, atol=1e-7, rtol=1e-7):
diff = np.abs(np.asarray(a, dtype=np.float64)
- np.asarray(b, dtype=np.float64))
scale = np.maximum(np.abs(np.asarray(a, dtype=np.float64)),
np.abs(np.asarray(b, dtype=np.float64)))
return bool(np.all(diff <= atol + rtol * scale))A kilépőkódos ellenőrzés a check_point végén
if not (ok_x and ok_W and ok_b and ok_dir): raise SystemExit(1).
Nullközeli teszt: a=np.array([1e-12]),
b=np.array([0.0]) esetén a relatív tag gyakorlatilag nulla,
de |a-b|=10^{-12}<\tau_a=10^{-7}, tehát a vizsgálat az
abszolút ágon megy át; ez a kívánt viselkedés, mert nullközeli
gradiensnél a relatív önmagában túl szigorú lenne.
F4. A hiba következménye: a faktor elhagyása azt jelzi a tanítónak, mintha a veszteség gradiensét a nemlinearitás meredeksége nélkül terjesztenénk vissza; a súlyfrissítés iránya és nagysága is torzul, ezért a tanulás lassulhat vagy rossz irányba lép. Két javítás: (1) a tűrést szigorítani értékre, és (2) az ellenőrzést legalább három, eltérő nagyságrendű ponton (P0, P1, seedelt véletlen pont) elvégezni, nemcsak egyetlen eseten. A P0 pont azért is fontos, mert ott az eltérés körüli, ami azonnal kimutatja a hibát.
F5. Egy lehetséges terv. Kérdés: a f32 változat gradienshibája elfogadható-e mellett. Baseline: a jelen f64 P0/P1 analitikus értékei. Változó tényező: f32 tárolás és számítás. Lépések: nyolc, rögzített seedű véletlen pont; minden ponton f64 analitikus gradiens, f32 forward/backward, valamint f64 középső differencia referencia. A f32 verzióhoz véges differencia, mert a gépi pontosság ott ; rögzítjük a nyolc pont maximális eltérését. Elfogadási feltétel: minden pontban . Következtetési korlát: az eredmény csak ennek a kis gráfnak és ennyi pontnak a numerikus viselkedését mutatja; nem bizonyít általános tanítási konvergenciát, és a seedek közti szórás külön vizsgálatot igényel.
Egy kis gráfon — affin réteg, elemenkénti sima , majd veszteség — teljes visszaterjesztést bizonyítottunk elemenként. A gradiensek: , , , . Két kézi ponton ellenőriztük az alakokat, majd NumPy-kódot adtunk, amely középső differenciával, f64 lépésközzel és stabil abszolút/relatív hibaképlettel vizsgál. A döntő módszertani állítás: egyetlen pontbeli egyezés nem bizonyítja az implementációt, ezért több pont, iránymenti VJP/JVP dualitás és szigorú tűrés szükséges.
Az 5.1. Esemény, feltétel és véletlen változó alfejezetben a valószínűségi nyelvre térünk át. A gradiens és VJP-ellenőrzés eszközei a későbbi veszteségfüggvények likelihood-alapú levezetésében és azok numerikus kontrolljában térnek vissza.