4.4 I. ellenőrző pont: matematika és kód megfelelése

4.4.1 Cél, előismeret és induló becslés

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=500m=500, d=1000d=1000 méretű affin réteg belső Jacobi-mátrixa 500⋅1000=5⋅105500\cdot 1000=5\cdot 10^5 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 zz-gradiens m=500m=500 eleme, az xx-gradiens d=1000d=1000 eleme, tehát nagyságrendileg 10310^3, nem 10510^5.)

4.4.2 A gradiens alakja skalár, vektor és tensor bemenetre

A kiinduló konvenciót egyszer rögzítjük.

Definíció (gradiens alakja). Ha L:ℝa1×⋯×ak→ℝL:\mathbb R^{a_1\times\cdots\times a_k}\to\mathbb R differenciálható valós értékű függvény, akkor ∇XL\nabla_X L az XX-szel azonos alakú tensor, és (∇XL)i1⋯ik=∂L∂Xi1⋯ik.(\nabla_X L)_{i_1\cdots i_k}=\frac{\partial L}{\partial X_{i_1\cdots i_k}}. Speciálisan x∈ℝdx\in\mathbb R^d esetén ∇xL∈ℝd\nabla_x L\in\mathbb R^d; W∈ℝm×dW\in\mathbb R^{m\times d} esetén ∇WL∈ℝm×d\nabla_W L\in\mathbb R^{m\times d}.

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.

4.4.3 A kiválasztott kis gráf

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 x∈ℝdx\in\mathbb R^d a bemenet, W∈ℝm×dW\in\mathbb R^{m\times d} a súly, b∈ℝmb\in\mathbb R^m az eltolás. Definiáljuk:

z=Wx+b,yj=ϕ(zj),j=1,…,m,L=12∥y∥22=12∑j=1myj2.z=Wx+b,\qquad y_j=\phi(z_j),\quad j=1,\ldots,m,\qquad L=\frac12\lVert y\rVert_2^2=\frac12\sum_{j=1}^m y_j^2.

Itt ϕ:ℝ→ℝ\phi:\mathbb R\to\mathbb R folytonosan differenciálható elemenkénti függvény, Φ(z)=(ϕ(z1),…,ϕ(zm))⊤\Phi(z)=(\phi(z_1),\ldots,\phi(z_m))^\top. A veszteség a checklistben bevezetett kivételes 12∑y2\tfrac12\sum y^2 alak, amelyre ∇yL=y\nabla_y L=y; ez nem MSE és nem a regularizálás λ2∥W∥F2\tfrac\lambda2\lVert W\rVert_F^2 tagja.

4.4.4 Lemma: a teljes gráf visszaterjesztési képletei

Lemma. A fenti gráfban legyen g=∇zLg=\nabla_z L. Ekkor ∇yL=y,g=y⊙Φ′(z),∇xL=W⊤g,∇WL=gx⊤,∇bL=g,\nabla_y L=y,\qquad g=y\odot\Phi'(z),\qquad \nabla_x L=W^\top g,\qquad \nabla_W L=g\,x^\top,\qquad \nabla_b L=g, ahol ⊙\odot elemenkénti szorzás és Φ′(z)=(ϕ′(z1),…,ϕ′(zm))⊤\Phi'(z)=(\phi'(z_1),\ldots,\phi'(z_m))^\top.

Bizonyítás. A bizonyítást elemenként végezzük, és minden lépésnél ellenőrizzük az alakot.

  1. L=12∑j=1myj2L=\frac12\sum_{j=1}^m y_j^2, ezért ∂L∂yj=yj,\frac{\partial L}{\partial y_j}=y_j, tehát ∇yL=y∈ℝm\nabla_y L=y\in\mathbb R^m.

  2. Az yj=ϕ(zj)y_j=\phi(z_j) elemenkénti műveletben zjz_j csak yjy_j-t befolyásolja, ezért a láncszabállyal ∂L∂zj=∂L∂yjdyjdzj=yjϕ′(zj).\frac{\partial L}{\partial z_j} =\frac{\partial L}{\partial y_j}\frac{dy_j}{dz_j} =y_j\,\phi'(z_j). A vektoralak g=y⊙Φ′(z)g=y\odot\Phi'(z), ahol mindkét tényező ℝm\mathbb R^m-beli.

  3. Az affin rész: zj=∑k=1dWjkxk+bjz_j=\sum_{k=1}^d W_{jk}x_k+b_j. Az xix_i bemeneti koordináta minden zjz_j-ben a WjiW_{ji} együtthatóval jelenik meg, tehát ∂L∂xi=∑j=1m∂L∂zj∂zj∂xi=∑j=1mgjWji=(W⊤g)i.\frac{\partial L}{\partial x_i} =\sum_{j=1}^m \frac{\partial L}{\partial z_j}\frac{\partial z_j}{\partial x_i} =\sum_{j=1}^m g_j W_{ji}=(W^\top g)_i. Az eredmény W⊤∈ℝd×mW^\top\in\mathbb R^{d\times m} és g∈ℝmg\in\mathbb R^m szorzata, azaz ∇xL∈ℝd\nabla_x L\in\mathbb R^d. Itt a W⊤W^\top szorzó a VJP-irány: a láncban a zz-gradienst visszük át a bemenetre.

  4. A WjiW_{ji} súly szerinti parciális deriváltban csak zjz_j függ WjiW_{ji}-től, és ∂zj/∂Wji=xi\partial z_j/\partial W_{ji}=x_i, ezért ∂L∂Wji=gjxi=(gx⊤)ji.\frac{\partial L}{\partial W_{ji}}=g_j\,x_i=(g\,x^\top)_{ji}. Az outer szorzat g∈ℝmg\in\mathbb R^m és x∈ℝdx\in\mathbb R^d tényezőkből ℝm×d\mathbb R^{m\times d} mátrixot ad, azonosat WW alakjával.

  5. A bjb_j eltolásra ∂zj/∂bj=1\partial z_j/\partial b_j=1, így ∇bL=g∈ℝm.\nabla_b L=g\in\mathbb R^m.

Ezzel a gráf minden élén megkaptuk a visszaterjesztett gradienst, és minden lépésben feltüntettük a tenzoralakot. A batchos X∈ℝN×dX\in\mathbb R^{N\times d} eset 4.3. Jacobi-mátrix, láncszabály és VJP alfejezetben a GWG W és G⊤XG^\top X alakú általánosítás; az itteni N=1N=1 eset az outer szorzatos formula.

4.4.5 Kézi számpélda: P0 pont

Legyen d=3d=3, m=2m=2, ϕ(t)=t2\phi(t)=t^2, és

x=(12−1),W=(0,5−1210,5−1),b=(0,2−0,3).x=\begin{pmatrix}1\\2\\-1\end{pmatrix},\qquad W=\begin{pmatrix}0{,}5 & -1 & 2\\ 1 & 0{,}5 & -1\end{pmatrix},\qquad b=\begin{pmatrix}0{,}2\\-0{,}3\end{pmatrix}.

Előrehaladás: z=Wx+b=(−3,32,7),y=Φ(z)=(10,897,29),L=12(10,892+7,292)=85,8681.z=Wx+b=\begin{pmatrix}-3{,}3\\2{,}7\end{pmatrix},\quad y=\Phi(z)=\begin{pmatrix}10{,}89\\7{,}29\end{pmatrix},\quad L=\tfrac12(10{,}89^2+7{,}29^2)=85{,}8681.

Mivel ϕ′(t)=2t\phi'(t)=2t, a visszaterjesztés:

g=y⊙Φ′(z)=(10,89⋅(−6,6)7,29⋅5,4)=(−71,87439,366).g=y\odot\Phi'(z)=\begin{pmatrix}10{,}89\cdot(-6{,}6)\\7{,}29\cdot 5{,}4\end{pmatrix} =\begin{pmatrix}-71{,}874\\39{,}366\end{pmatrix}.

Ezután

∇xL=W⊤g=(3,42991,557−183,114),\nabla_x L=W^\top g=\begin{pmatrix}3{,}429\\91{,}557\\-183{,}114\end{pmatrix},

∇WL=gx⊤=(−71,874−143,74871,87439,36678,732−39,366),\nabla_W L=g\,x^\top= \begin{pmatrix} -71{,}874 & -143{,}748 & 71{,}874\\ 39{,}366 & 78{,}732 & -39{,}366 \end{pmatrix},

∇bL=g=(−71,87439,366).\nabla_b L=g=\begin{pmatrix}-71{,}874\\39{,}366\end{pmatrix}.

4.4.6 Új pont: P1 kontrollszámítás

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.

x=(0,5−21,5),W=(20−1011),b=(0,10).x=\begin{pmatrix}0{,}5\\-2\\1{,}5\end{pmatrix},\qquad W=\begin{pmatrix}2 & 0 & -1\\0 & 1 & 1\end{pmatrix},\qquad b=\begin{pmatrix}0{,}1\\0\end{pmatrix}.

A forward:

z=(−0,4−0,5),y=(0,160,25),L=0,04405.z=\begin{pmatrix}-0{,}4\\-0{,}5\end{pmatrix},\quad y=\begin{pmatrix}0{,}16\\0{,}25\end{pmatrix},\quad L=0{,}04405.

A backward:

g=(0,16⋅(−0,8)0,25⋅(−1))=(−0,128−0,25),g=\begin{pmatrix}0{,}16\cdot(-0{,}8)\\0{,}25\cdot(-1)\end{pmatrix} =\begin{pmatrix}-0{,}128\\-0{,}25\end{pmatrix},

∇xL=W⊤g=(−0,256−0,25−0,122),\nabla_x L=W^\top g=\begin{pmatrix}-0{,}256\\-0{,}25\\-0{,}122\end{pmatrix},

∇WL=gx⊤=(−0,0640,256−0,192−0,1250,5−0,375),\nabla_W L=g\,x^\top= \begin{pmatrix} -0{,}064 & 0{,}256 & -0{,}192\\ -0{,}125 & 0{,}5 & -0{,}375 \end{pmatrix},

∇bL=(−0,128−0,25).\nabla_b L=\begin{pmatrix}-0{,}128\\-0{,}25\end{pmatrix}.

4.4.7 A véges differenciás ellenőrzés hibamérlege

Az analitikus gradienst középső differenciával hasonlítjuk össze, mert ennek csonkolási hibája ε2\varepsilon^2 rendű, szemben az előredifferencia ε\varepsilon rendű hibájával. Egy xix_i irányú perturbációra

Fi(ε)=L(x+εei)−L(x−εei)2ε.F_i(\varepsilon)=\frac{L(x+\varepsilon e_i)-L(x-\varepsilon e_i)}{2\varepsilon}.

Ha az iránymenti harmadik deriváltak korlátosak, a Taylor-sorral

Fi(ε)=∂L∂xi+O(ε2)+O(u|L|ε),F_i(\varepsilon)=\frac{\partial L}{\partial x_i}+O(\varepsilon^2)+O\!\left(\frac{u\,|L|}{\varepsilon}\right),

ahol uu a gépi pontosság f64 esetén (u≈2,2⋅10−16u\approx 2{,}2\cdot 10^{-16}), 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 ϕ\phi-re végezzük.

A két hibaforrás ellentétesen függ ε\varepsilon-tól: a csonkolás ε2\varepsilon^2-tel csökken, a kerekítés viszont 1/ε1/\varepsilon-nal nő. Az optimális lépésköz ezért nagyságrendileg (u|L|)1/3(u|L|)^{1/3}. A P0 példában L≈86L\approx 86, ezért

(2,2⋅10−16⋅86)1/3≈(1,9⋅10−14)1/3≈2,7⋅10−5.(2{,}2\cdot 10^{-16}\cdot 86)^{1/3} \approx (1{,}9\cdot 10^{-14})^{1/3} \approx 2{,}7\cdot 10^{-5}.

A laborban ε=10−6\varepsilon=10^{-6} f64 lépésközt használunk; ezzel a hibabecslés körülbelül 10−810^{-8} nagyságrendű. Az ellenőrzést a stabil hibaképlettel végezzük:

|a−b|≤τa+τrmax{|a|,|b|},|a-b|\;\le\; \tau_a+\tau_r\max\{|a|,|b|\},

ahol τa\tau_a az abszolút, τr\tau_r 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.

4.4.8 Labor: NumPy forward, backward és véges differencia

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 20261020

Az 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 10−810^{-8}–10−710^{-7} nagyságrendbe eső értéket vesz fel; a close függvény ezért τa=τr=10−7\tau_a=\tau_r=10^{-7} 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.

4.4.9 Gyakori tévedés: egyetlen pont nem általános bizonyíték

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:

f(t)=t2,g(t)=t2+2(t−1)2.f(t)=t^2,\qquad g(t)=t^2+2(t-1)^2.

A t=1t=1 pontban f(1)=g(1)=1f(1)=g(1)=1 és f′(1)=g′(1)=2f'(1)=g'(1)=2, tehát érték és gradiens is egyezik; ennek ellenére f(2)=4f(2)=4, míg g(2)=6g(2)=6, 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.

4.4.10 Önellenőrzés és korábbi fogalom visszahívása

4.4.11 Feladatok

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 d=1000d=1000 és m=500m=500. 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

LA=12y⊤Ay,A∈ℝm×m,A=A⊤.L_A=\frac12\,y^\top A\,y,\qquad A\in\mathbb R^{m\times m},\ A=A^\top.

Vezesse le, hogy ∇yLA=Ay\nabla_y L_A=Ay, és adja meg ezzel az új g=∇zLAg=\nabla_z L_A képletét. Ellenőrizze, hogy A=IA=I visszaadja a lemmabeli yy-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_z

a P1 ponton gbadx=W⊤y=(0,32,0,25,0,09)⊤g_\text{bad}^x=W^\top y=(0{,}32,\ 0{,}25,\ 0{,}09)^\top, szemben a korrekt (−0,256,−0,25,−0,122)⊤(-0{,}256,\ -0{,}25,\ -0{,}122)^\top értékkel. Mutassa meg, hogy a maximális abszolút eltérés 0,5760{,}576, tehát egy τa=1\tau_a=1 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 ε\varepsilon, milyen baseline, milyen tűrés és milyen következtetési korlát. Ne feltételezzen előre megadott győztest.

4.4.12 Megoldások és indoklás

F1. A teljes JfJ_f mátrix m×d=500000m\times d=500\,000 skalár. A VJP-úton a zz-gradiens m=500m=500, a bemeneti gradiens d=1000d=1000, tehát a köztes tárigény nagyságrendileg 1,5⋅1031{,}5\cdot 10^3 és a mátrixszorzások a W⊤gW^\top g irányban végzik el ugyanazt az információáramlást. A konvenció: ∇XL\nabla_X L alakja megegyezik XX alakjával; parciális deriváltja minden XX-indexhez egy skalár.

F2. Legyen LA=12∑i,jyiAijyjL_A=\tfrac12\sum_{i,j}y_iA_{ij}y_j. Ekkor

∂LA∂yk=12∑jAkjyj+12∑iyiAik=12(Ay)k+12(A⊤y)k=(Ay)k,\frac{\partial L_A}{\partial y_k} =\frac12\sum_j A_{kj}y_j+\frac12\sum_i y_iA_{ik} =\frac12(Ay)_k+\frac12(A^\top y)_k =(Ay)_k,

mert A=A⊤A=A^\top. Ezért ∇yLA=Ay\nabla_y L_A=Ay; a lánc második lépése változatlan,

g=∇zLA=(Ay)⊙Φ′(z).g=\nabla_z L_A=(Ay)\odot\Phi'(z).

Az A=IA=I esetben Ay=yAy=y, tehát visszakapjuk a lemmabeli g=y⊙Φ′(z)g=y\odot\Phi'(z) formulát. Alakellenőrzés: A∈ℝm×mA\in\mathbb R^{m\times m}, y∈ℝmy\in\mathbb R^m, tehát Ay∈ℝmAy\in\mathbb R^m; az elemenkénti szorzat is ℝm\mathbb R^m.

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 ϕ′(z)\phi'(z) 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 τa=τr=10−7\tau_a=\tau_r=10^{-7} é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 197197 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 τa=τr=10−4\tau_a=\tau_r=10^{-4} 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 ε=10−6\varepsilon=10^{-6} referencia. A f32 verzióhoz ε=10−3\varepsilon=10^{-3} véges differencia, mert a gépi pontosság ott u≈1,2⋅10−7u\approx 1{,}2\cdot 10^{-7}; rögzítjük a nyolc pont maximális eltérését. Elfogadási feltétel: minden pontban |a−b|≤10−4+10−4max(|a|,|b|)|a-b|\le 10^{-4}+10^{-4}\max(|a|,|b|). 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.

4.4.13 Rövid összefoglaló

Egy kis gráfon — affin réteg, elemenkénti sima ϕ\phi, majd 12∥y∥2\tfrac12\lVert y\rVert^2 veszteség — teljes visszaterjesztést bizonyítottunk elemenként. A gradiensek: g=y⊙ϕ′(z)g=y\odot\phi'(z), ∇xL=W⊤g\nabla_x L=W^\top g, ∇WL=gx⊤\nabla_W L=g\,x^\top, ∇bL=g\nabla_b L=g. 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.

4.4.14 Következő alkalmazás

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.

4.4.15 Források