15.4 Független gradiensellenőrzési protokoll

15.4.1 Cél, előismeret és induló kérdés

A 15.1.–15.2. egységben levezettük a konvolúciós réteg súly- és bemeneti gradiensét, a 15.3. Transposed convolution mint adjungált pedig az adjungált operátort azonosította a mátrixtranszponálttal. Egy levezetés azonban egymagában nem teszi a kódot helyessé: ugyanaz az indexelési hiba könnyen bekerül a képletbe és a referenciakódba is. Ebben az egységben ezért három, egymástól eltérő forrásból származó gradienst hasonlítunk össze:

  1. analitikus képletből származó zárt alakú backward;
  2. véges differenciából származó numerikus közelítés;
  3. a keretrendszer autograd implementációja.

A cél annak a protokollnak a rögzítése, amely megmondja, mikor fogadható el egy numerikus egyezés ellenőrzésnek, és mikor állítható róla, hogy csak részleges bizonyíték. A szükséges előismeret a 15.3. Transposed convolution mint adjungált mátrixos adjungáltja; visszautalunk a 14.4. Csoportos és depthwise konvolúció csoportos operátor korlátozására, a 11.4. Egy teljes PyTorch tanítási iteráció tanítási lépés gradienstárolási szerződésére és a 3.4. Sajátérték, SVD és spektrális határ normakorlátra.

Induló kérdés: miért választunk float64-et és centrális differenciát, ha a forward differencia egyszerűbb?

15.4.2 Definíciók: centrális differencia, paraméterrács, relatív hiba

Legyen θ∈ℝd\theta \in \mathbb{R}^d a vizsgált paramétervektor, és legyen g:ℝd→ℝg:\mathbb{R}^d \to \mathbb{R} a veszteségfüggvény. Jelölje ej∈ℝde_j \in \mathbb{R}^d a jj-edik egységvektort, ε>0\varepsilon > 0 pedig a perturbációt. A centrális differencia a jj-edik koordináta mentén:

∂̃jg(θ;ε)=g(θ+εej)−g(θ−εej)2ε. \tilde{\partial}_j g(\theta; \varepsilon) = \frac{g(\theta + \varepsilon e_j) - g(\theta - \varepsilon e_j)}{2\varepsilon}.

A forward differencia ettől csak egyoldali:

∂̃jfg(θ;ε)=g(θ+εej)−g(θ)ε. \tilde{\partial}_j^{\,f} g(\theta; \varepsilon) = \frac{g(\theta + \varepsilon e_j) - g(\theta)}{\varepsilon}.

A kettő nem ugyanaz: az egyoldali változat hibája O(ε)O(\varepsilon), a centrálisé O(ε2)O(\varepsilon^2), ahogyan az alábbi levezetésben látni fogjuk.

Paraméterrácson (parameter grid) itt nem a konvolúciós hiperparaméterek (stride, padding) rácsát értjük, hanem a veszteség paraméterterének több, függetlenül választott pontját. Egyetlen θ\theta pontban az egyezés lehet véletlen vagy szimmetriahiba következménye; RR darab, különböző skálájú és előjelű pontban való egyezés már függetlenebb bizonyíték. A rácspontokat rögzített seeddel generáljuk.

A numerikus egyezést a stabil relatív hibával mérjük:

Erel(a,n)=|a−n|max{|a|,|n|,δabs}, E_{\mathrm{rel}}(a, n) = \frac{|a - n|}{\max\{|a|, |n|, \delta_{\mathrm{abs}}\}},

ahol aa az analitikus, nn a numerikus gradiens egy koordinátája, δabs\delta_{\mathrm{abs}} pedig abszolút védőszint (például 10−1210^{-12} float64-ben). Nullához közeli gradiens esetén a tiszta relatív hiba zajosan felnagyítódik, ezért szükséges a stabil nevező.

A vizsgált célfüggvény akkor sima, ha gg legalább kétszer folytonosan differenciálható a vizsgált pont egy környezetében. Az affin konvolúciós rétegre épülő L=12∥Y∥F2L = \tfrac12 \|Y\|_F^2 veszteség a paraméterekben polinomiális, tehát C∞C^\infty; a ReLU-t tartalmazó háló viszont a nullpontban nem differenciálható, ezért a gradiensellenőrzést olyan pontban kell végezni, ahol a pre-aktiváció egyetlen koordinátája sem nulla, vagy a ReLU-t a veszteségből el kell hagyni.

A float64 választása nem kényelmi kérdés. A véges differencia teljes hibája két tagból áll: a Taylor-levágásból származó O(ε2)O(\varepsilon^2) diszkretizációs hibából és a lebegőpontos kerekítésből származó O(u/ε)O(u/\varepsilon) zajból, ahol uu a gépi epszilon. float32-ben u≈1,2⋅10−7u \approx 1{,}2\cdot 10^{-7}, így ε≈10−3\varepsilon \approx 10^{-3} körül is már a kerekítési zaj dominálhat; float64-ben u≈2,2⋅10−16u \approx 2{,}2\cdot 10^{-16}, ezért ε=10−6\varepsilon = 10^{-6} biztonságos egyensúlyt ad.

15.4.3 Levezetési kötelezettség: pontos részhalmaz és a numerikus bizonyítás korlátja

A tesztelt művelet pontos részhalmaza a következő. Legyen a bemeneti batch X∈ℝN×Cin×H×WX \in \mathbb{R}^{N \times C_{in} \times H \times W}, a kernel K∈ℝCout×Cin×kh×kwK \in \mathbb{R}^{C_{out} \times C_{in} \times k_h \times k_w}, a bias b∈ℝCoutb \in \mathbb{R}^{C_{out}}. A forward a [D10] szerinti cross-correlation:

Yn,o,i,j=bo+∑c=0Cin−1∑p=0kh−1∑q=0kw−1Xn,c,shi−ph+dhp,swj−pw+dwqKo,c,p,q, Y_{n,o,i,j} = b_o + \sum_{c=0}^{C_{in}-1}\sum_{p=0}^{k_h-1}\sum_{q=0}^{k_w-1} X_{n,c,\, s_h i - p_h + d_h p,\; s_w j - p_w + d_w q}\, K_{o,c,p,q},

ahol sh,sws_h, s_w a stride, ph,pwp_h, p_w a szimmetrikus zero padding, dh,dwd_h, d_w a dilation. A csatornacsoportok értéke g=1g=1; a nem szimmetrikus padding és a csoportos művelet kívül esik ezen az állításon. A veszteség a kézi minta konvenciója szerint

L=12∑n,o,i,jYn,o,i,j2, L = \frac{1}{2}\sum_{n,o,i,j} Y_{n,o,i,j}^2,

ezért a kimeneti gradiens egyszerűen ∇YL=Y\nabla_Y L = Y; ez nem MSE, hanem a kézi ellenőrzést egyszerűsítő választás, amelyet a [JELOLESEK.md] konvenciója rögzít.

A backward analitikus képlete ekkor a 15.1.–15.3. eredménye:

∇KL=X⋆validY,∇bLo=∑n,i,jYn,o,i,j,∇XL=Y⋆fullK, \nabla_K L = X \star_{\mathrm{valid}} Y, \qquad \nabla_b L_o = \sum_{n,i,j} Y_{n,o,i,j}, \qquad \nabla_X L = Y \star_{\mathrm{full}} K,

ahol ⋆valid\star_{\mathrm{valid}} a valid, ⋆full\star_{\mathrm{full}} a full cross-correlation, és az utóbbiban a kernel eredeti orientációjával dolgozunk, mert a forward is cross-correlation; a matematikai konvolúcióhoz tartozó kernelmegfordítás itt nem lép fel.

Állítás (a centrális differencia másodrendű hibája). Ha g∈C2g \in C^2 a θ\theta pont egy ε\varepsilon sugarú környezetében, akkor minden jj koordinátára

∂̃jg(θ;ε)=∂g∂θj(θ)+ε26∂3g∂θj3(ξ)+O(uε), \tilde{\partial}_j g(\theta; \varepsilon) = \frac{\partial g}{\partial \theta_j}(\theta) + \frac{\varepsilon^2}{6} \frac{\partial^3 g}{\partial \theta_j^3}(\xi) + O\!\left(\frac{u}{\varepsilon}\right),

valamely ξ\xi közbülső ponttal, így a diszkretizációs tag O(ε2)O(\varepsilon^2), szemben a forward differencia O(ε)O(\varepsilon) tagjával. A kerekítési tag alsó korlátja O(u/ε)O(u/\varepsilon).

Levezetés. A Taylor-formula a harmadik taggal:

g(θ+εej)=g(θ)+εgj′+ε22gj″+ε36gj‴(ξ+), g(\theta + \varepsilon e_j) = g(\theta) + \varepsilon g_j' + \frac{\varepsilon^2}{2} g_j'' + \frac{\varepsilon^3}{6} g_j'''(\xi_+), g(θ−εej)=g(θ)−εgj′+ε22gj″−ε36gj‴(ξ−). g(\theta - \varepsilon e_j) = g(\theta) - \varepsilon g_j' + \frac{\varepsilon^2}{2} g_j'' - \frac{\varepsilon^3}{6} g_j'''(\xi_-).

A kivonás után a g(θ)g(\theta) és az ε2gj″\varepsilon^2 g_j'' tag kiesik, és

g(θ+εej)−g(θ−εej)2ε=gj′+ε212(gj‴(ξ+)+gj‴(ξ−))=gj′+ε26gj‴(ξ), \frac{g(\theta + \varepsilon e_j) - g(\theta - \varepsilon e_j)}{2\varepsilon} = g_j' + \frac{\varepsilon^2}{12}\bigl(g_j'''(\xi_+) + g_j'''(\xi_-)\bigr) = g_j' + \frac{\varepsilon^2}{6} g_j'''(\xi),

ahol az utolsó lépés a folytonosságból következő közbülsőérték-tétel. A hibatag tehát O(ε2)O(\varepsilon^2). A lebegőpontos kerekítésből származó O(u/ε)O(u/\varepsilon) tag abból következik, hogy a két gg-kiértékelés külön-külön O(u)O(u) abszolút hibájú, és ezt 2ε2\varepsilon-nal osztjuk. ▫\square

Ennek a numerikus kontrollnak a bizonyítási státusza korlátozott: az egyezés véges sok rácspontban, véges ε\varepsilon mellett áll fenn. Nem helyettesíti az analitikus levezetést, nem zárja ki a nullmértékű halmazon jelentkező hibát, és nem igazolja a stride, dilation, padding összes lehetséges kombinációját. Olyan esetben azonban, amikor a három forrás közül kettő egymástól független (például a numerikus differencia és az autograd), a közös hibát valószínűtlenné teszi.

15.4.4 Eredeti kézi példa

Legyen N=Cin=Cout=1N = C_{in} = C_{out} = 1, H=W=3H = W = 3, kh=kw=2k_h = k_w = 2, stride 11, padding 00, dilation 11. Bemenet és kernel:

X=[123456789],K=[0,5−0,510,25]. X = \begin{bmatrix} 1 & 2 & 3 \\ 4 & 5 & 6 \\ 7 & 8 & 9 \end{bmatrix}, \qquad K = \begin{bmatrix} 0{,}5 & -0{,}5 \\ 1 & 0{,}25 \end{bmatrix}.

A forward Y∈ℝ2×2Y \in \mathbb{R}^{2 \times 2}:

Y=[4,756,08,59,75]. Y = \begin{bmatrix} 4{,}75 & 6{,}0 \\ 8{,}5 & 9{,}75 \end{bmatrix}.

A veszteség L=12∑i,jYij2L = \tfrac12 \sum_{i,j} Y_{ij}^2. Az input gradiens analitikus képlete full cross-correlation az eredeti kernelrel:

∇XL[1,1]=Y00K00+Y01K10+Y10K01+Y11K11=4,75⋅0,25+6⋅1+8,5⋅(−0,5)+9,75⋅0,5=7,8125. \nabla_X L[1,1] = Y_{00}K_{00} + Y_{01}K_{10} + Y_{10}K_{01} + Y_{11}K_{11} = 4{,}75 \cdot 0{,}25 + 6 \cdot 1 + 8{,}5 \cdot (-0{,}5) + 9{,}75 \cdot 0{,}5 = 7{,}8125.

Ellenőrzés centrális differenciával ε=0,01\varepsilon = 0{,}01 mellett, X11X_{11} perturbálásával:

X+=X+εe1,1,X−=X−εe1,1, X_+ = X + \varepsilon e_{1,1},\quad X_- = X - \varepsilon e_{1,1},

Y+=[4,75256,018,4959,755],Y−=[4,74755,998,5059,745]. Y_+ = \begin{bmatrix} 4{,}7525 & 6{,}01 \\ 8{,}495 & 9{,}755 \end{bmatrix}, \qquad Y_- = \begin{bmatrix} 4{,}7475 & 5{,}99 \\ 8{,}505 & 9{,}745 \end{bmatrix}.

L+=113,015703125,L−=112,859453125, L_+ = 113{,}015703125, \qquad L_- = 112{,}859453125,

L+−L−2ε=0,156250,02=7,8125. \frac{L_+ - L_-}{2\varepsilon} = \frac{0{,}15625}{0{,}02} = 7{,}8125.

A két érték az adott pontosság mellett pontosan egyezik. Ugyanezt a súlygradiensre is elvégezhetjük: ∇KL[0,0]=Y00X00+Y01X01+Y10X10+Y11X11=99,5\nabla_K L[0,0] = Y_{00}X_{00} + Y_{01}X_{01} + Y_{10}X_{10} + Y_{11}X_{11} = 99{,}5, és a centrális differencia K00K_{00} perturbálásával ugyancsak 99,599{,}5-öt ad. A példa azt is megmutatja, hogy a kernel eredeti orientációjú full cross-correlation adja az input gradienst; a flipped kernel használata 10,312510{,}3125-öt adna, ami hibás a cross-correlation főúton.

15.4.5 Labor: független hármas egyezés és JSON-jelentés

Az alábbi kód a 15.1.–15.2. saját backwardjét használja analitikus referenciaként, a centrális differenciát numerikus kontrollként, és a PyTorch autograd.grad-ját keretrendszer-kontrollként. A kód float64-ben fut, CPU-n, rögzített seeddel, és a hibákat JSON-jelentésbe írja. A kimeneti JSON-minta szemléltetés, nem rögzített futási eredmény; a tényleges futtatást a laborban kell elvégezni.

# munkakönyvtár: lab/
# futtatás: python gradcheck_protocol.py
import json
import torch

torch.manual_seed(20261020)

def conv2d_forward(X, K, b, stride=1, pad=0, dilation=1):
    N, C, H, W = X.shape
    O, _, kH, kW = K.shape
    Ho = (H + 2*pad - dilation*(kH - 1) - 1)//stride + 1
    Wo = (W + 2*pad - dilation*(kW - 1) - 1)//stride + 1
    Y = torch.zeros(N, O, Ho, Wo, dtype=torch.float64)
    for n in range(N):
        for o in range(O):
            for i in range(Ho):
                for j in range(Wo):
                    s = 0.0
                    for c in range(C):
                        for p in range(kH):
                            for q in range(kW):
                                ii = i*stride - pad + p*dilation
                                jj = j*stride - pad + q*dilation
                                if 0 <= ii < H and 0 <= jj < W:
                                    s += X[n, c, ii, jj].item() * K[o, c, p, q].item()
                    Y[n, o, i, j] = s + b[o].item()
    return Y

def conv2d_backward_input(Y, K, stride=1, pad=0, dilation=1, X_shape=None):
    N, O, Ho, Wo = Y.shape
    _, _, kH, kW = K.shape
    N, C, H, W = X_shape
    dX = torch.zeros(N, C, H, W, dtype=torch.float64)
    for n in range(N):
        for c in range(C):
            for i in range(H):
                for j in range(W):
                    s = 0.0
                    for o in range(O):
                        for p in range(kH):
                            for q in range(kW):
                                # Y_{n,o,r,s} járulék, ahol r*stride - pad + p*dilation == i
                                # r = (i + pad - p*dilation)/stride, ha egész
                                if (i + pad - p*dilation) % stride != 0:
                                    continue
                                r = (i + pad - p*dilation)//stride
                                if (j + pad - q*dilation) % stride != 0:
                                    continue
                                s_ = (j + pad - q*dilation)//stride
                                if 0 <= r < Ho and 0 <= s_ < Wo:
                                    s += Y[n, o, r, s_].item() * K[o, c, p, q].item()
                    dX[n, c, i, j] = s
    return dX

def conv2d_backward_weight(X, Y, stride=1, pad=0, dilation=1, K_shape=None):
    N, C, H, W = X.shape
    N, O, Ho, Wo = Y.shape
    O, Cg, kH, kW = K_shape
    dK = torch.zeros(K_shape, dtype=torch.float64)
    db = torch.zeros(O, dtype=torch.float64)
    for o in range(O):
        for c in range(Cg):
            for p in range(kH):
                for q in range(kW):
                    s = 0.0
                    for n in range(N):
                        for i in range(Ho):
                            for j in range(Wo):
                                ii = i*stride - pad + p*dilation
                                jj = j*stride - pad + q*dilation
                                if 0 <= ii < H and 0 <= jj < W:
                                    s += X[n, c, ii, jj].item() * Y[n, o, i, j].item()
                    dK[o, c, p, q] = s
        db[o] = Y[:, o, :, :].sum().item()
    return dK, db

def loss_of(X, K, b, stride, pad, dilation):
    Y = conv2d_forward(X, K, b, stride, pad, dilation)
    return 0.5 * (Y**2).sum()

def finite_difference_flat(X, K, b, stride, pad, dilation, eps=1e-6):
    # a paraméterek lapított vektorán centrális differencia
    flat = torch.cat([X.reshape(-1), K.reshape(-1), b.reshape(-1)]).detach().clone()
    grad = torch.zeros_like(flat)
    for j in range(flat.numel()):
        orig = flat[j].item()
        flat[j] = orig + eps
        Lp = loss_of(flat[:X.numel()].reshape(X.shape),
                     flat[X.numel():X.numel()+K.numel()].reshape(K.shape),
                     flat[-b.numel():], stride, pad, dilation)
        flat[j] = orig - eps
        Lm = loss_of(flat[:X.numel()].reshape(X.shape),
                     flat[X.numel():X.numel()+K.numel()].reshape(K.shape),
                     flat[-b.numel():], stride, pad, dilation)
        flat[j] = orig
        grad[j] = (Lp - Lm)/(2*eps)
    return grad

def rel_err(a, n, atol=1e-12):
    a = float(a); n = float(n)
    return abs(a - n) / max(abs(a), abs(n), atol)

def run_case(shape, kshape, stride, pad, dilation, tag):
    N, C, H, W = shape
    O = kshape[0]
    X = torch.rand(N, C, H, W, dtype=torch.float64) * 2 - 1
    K = (torch.rand(*kshape, dtype=torch.float64) * 2 - 1) / (C * kshape[2] * kshape[3])**0.5
    b = torch.rand(O, dtype=torch.float64) * 0.2 - 0.1

    # 1. analitikus
    Y = conv2d_forward(X, K, b, stride, pad, dilation)
    dX = conv2d_backward_input(Y, K, stride, pad, dilation, X.shape)
    dK, db = conv2d_backward_weight(X, Y, stride, pad, dilation, K.shape)
    anal = torch.cat([dX.reshape(-1), dK.reshape(-1), db.reshape(-1)])

    # 2. numerikus
    num = finite_difference_flat(X, K, b, stride, pad, dilation)

    # 3. autograd
    Xa = X.clone().requires_grad_(True)
    Ka = K.clone().requires_grad_(True)
    ba = b.clone().requires_grad_(True)
    Ya = torch.nn.functional.conv2d(Xa, Ka, ba, stride=stride,
                                     padding=pad, dilation=dilation)
    La = 0.5 * (Ya**2).sum()
    grads = torch.autograd.grad(La, [Xa, Ka, ba])
    auto = torch.cat([g.reshape(-1) for g in grads])

    # relatív hibák
    e_an = max(rel_err(a, n) for a, n in zip(anal, num))
    e_aa = max(rel_err(a, n) for a, n in zip(anal, auto))
    e_na = max(rel_err(n, a) for n, a in zip(num, auto))
    return {"tag": tag, "shape": list(shape), "kernel_shape": list(kshape),
            "stride": stride, "padding": pad, "dilation": dilation,
            "max_rel_anal_vs_numeric": e_an,
            "max_rel_anal_vs_autograd": e_aa,
            "max_rel_numeric_vs_autograd": e_na,
            "grid_points": X.numel() + K.numel() + b.numel()}

cases = [
    ((1, 1, 3, 3), (1, 1, 2, 2), 1, 0, 1, "small_stride1"),
    ((2, 2, 6, 6), (3, 2, 3, 3), 1, 1, 1, "padded_batch"),
    ((1, 1, 7, 7), (1, 1, 3, 3), 2, 0, 1, "stride2"),
    ((1, 1, 6, 6), (1, 1, 2, 2), 1, 0, 2, "dilation2"),
]

report = {"protocol_version": "1.0", "dtype": "float64",
          "finite_difference": "central", "eps": 1e-6,
          "relative_error_formula": "|a-n|/max(|a|,|n|,1e-12)",
          "cases": [run_case(*c) for c in cases]}
report["all_passed_tol_1e-5"] = all(
    max(c[k] for k in ("max_rel_anal_vs_numeric",
                       "max_rel_anal_vs_autograd",
                       "max_rel_numeric_vs_autograd")) < 1e-5
    for c in report["cases"])

with open("gradcheck_protocol.json", "w", encoding="utf-8") as f:
    json.dump(report, f, indent=2, ensure_ascii=False)
print(json.dumps(report, indent=2, ensure_ascii=False)[:1200])

A run_case minden esethez három, egymástól független úton kapott gradienst hasonlít össze. A small_stride1 eset a fenti kézi példa méretosztálya; a padded_batch a biasgradiens batchelt összegzését is teszteli; a stride2 és dilation2 a paraméterrács további pontjai. Az elfogadási küszöb 10−510^{-5} relatív hiba float64 central difference mellett; ennél nagyobb eltérés implementációs hibát vagy nem sima pontot jelez. Az elvárt JSON-jelentés legalább a fenti mezőket tartalmazza, és a all_passed_tol_1e-5 mező true értéke a hármas egyezés indikátora.

15.4.6 Feladatok

F1 (fogalmi, nehézség 1). Fogalmazza meg, mi a különbség a forward és a centrális differencia között, és magyarázza el, miért használunk stabil nevezőt a relatív hibában. Adjon egy konkrét gg-t és ε\varepsilon-t, ahol a tiszta |a−n|/|a||a-n|/|a| képlet nullához közeli gradiensnél félrevezető értéket adna.

F2 (levezetési, nehézség 2). Legyen g(x)=x3g(x) = x^3, x0=2x_0 = 2, ε=0,1\varepsilon = 0{,}1. Számítsa ki a forward és a centrális differencia értékét, és hasonlítsa mindkettőt a pontos g′(2)g'(2) értékhez. Melyik hibája arányos ε\varepsilon-nal, és melyiké ε2\varepsilon^2-tel? A Taylor-sorból indokolja a megfigyelt arányt.

F3 (implementációs, nehézség 2). Írjon teljes Python-függvényt, amely az L=12∑Y2L = \tfrac12 \sum Y^2 veszteségre a fenti 3×33 \times 3 bemenet és 2×22 \times 2 kernel esetén kiszámítja a centrális differenciát az X1,1X_{1,1} koordinátára, és ellenőrzi, hogy az eredmény 7,81257{,}8125 tűrésen belül. A függvény ne használjon PyTorch-autogradot.

F4 (hibakeresési, nehézség 2). Valaki a következő ellenőrzést írta: float32 típus, forward differencia, ε=10−3\varepsilon=10^{-3}, és a háló egy ReLU-t is tartalmaz, amelynek pre-aktivációja a vizsgált pontban pontosan 00. Az ellenőrzés relatív hibája 0,350{,}35. Sorolja fel a legalább három különböző hibaforrást, mindegyik következményét és a javítás módját.

F5 (kísérlettervezési, nehézség 3). Tervezzen gradiensellenőrzési protokollt egy olyan rétegre, amely stride 22, dilation 22 és padding 11 mellett fut, Cin=3C_{in}=3, Cout=5C_{out}=5, kh=kw=3k_h=k_w=3, bemenet N=4N=4, H=W=10H=W=10. Határozza meg: hány rácspontot választ, mekkora ε\varepsilon-t, milyen toleranciát, és milyen korlátok miatt nem tekinthető az egyezés teljes bizonyításnak.

15.4.7 Megoldások és indoklás

F1. A forward differencia egyoldali: (g(θ+εej)−g(θ))/ε(g(\theta+ \varepsilon e_j) - g(\theta))/\varepsilon; hibája O(ε)O(\varepsilon). A centrális kétoldali: (g(θ+εej)−g(θ−εej))/(2ε)(g(\theta+\varepsilon e_j) - g(\theta-\varepsilon e_j))/(2\varepsilon); a szimmetria miatt a másodrendű tag kiesik, hibája O(ε2)O(\varepsilon^2). A stabil nevező azért kell, mert ha a=10−14a = 10^{-14} és n=2⋅10−14n = 2 \cdot 10^{-14}, akkor a tiszta relatív hiba 11 (100%), noha az abszolút eltérés 10−1410^{-14}, ami float64-ben zajszint. Például g(x)=10−12x2g(x) = 10^{-12}x^2-nél x0=0x_0 = 0 közelében ez előfordul; a δabs\delta_{\mathrm{abs}} védőszint megakadályozza a hamis riasztást.

F2. Pontos derivált: g′(2)=3⋅22=12g'(2) = 3 \cdot 2^2 = 12. Forward: g(2,1)−g(2)0,1=9,261−80,1=12,61\frac{g(2{,}1) - g(2)}{0{,}1} = \frac{9{,}261 - 8}{0{,}1} = 12{,}61; hiba 0,61=6,1ε0{,}61 = 6{,}1\varepsilon, mert a Taylor-maradék ε2g″(2)=0,12⋅12=0,6\frac{\varepsilon}{2}g''(2) = \frac{0{,}1}{2} \cdot 12 = 0{,}6. Centrális: g(2,1)−g(1,9)0,2=9,261−6,8590,2=12,01\frac{g(2{,}1) - g(1{,}9)}{0{,}2} = \frac{9{,}261 - 6{,}859}{0{,}2} = 12{,}01; hiba 0,01=ε20{,}01 = \varepsilon^2, mert a maradék ε26g‴(2)=0,016⋅6=0,01\frac{\varepsilon^2}{6}g'''(2) = \frac{0{,}01}{6} \cdot 6 = 0{,}01. A megfigyelt hibaarány pontosan a levezetett rendet tükrözi.

F3. Elfogadható megoldás:

# munkakönyvtár: lab/
# futtatás: python f3_hand_check.py
import torch

def conv_3x3(X, K):
    Y = torch.zeros(2, 2, dtype=torch.float64)
    for i in range(2):
        for j in range(2):
            Y[i, j] = (X[i:i+2, j:j+2] * K).sum()
    return Y

X = torch.tensor([[1., 2., 3.], [4., 5., 6.], [7., 8., 9.]],
                  dtype=torch.float64)
K = torch.tensor([[0.5, -0.5], [1.0, 0.25]], dtype=torch.float64)
eps = 1e-6
L_of = lambda A: 0.5 * (conv_3x3(A, K)**2).sum()
Xp = X.clone(); Xp[1, 1] += eps
Xm = X.clone(); Xm[1, 1] -= eps
grad = (L_of(Xp) - L_of(Xm)) / (2 * eps)
assert abs(grad - 7.8125) < 1e-5, f"eltérés: {grad - 7.8125}"
print(f"grad = {grad:.8f}, elvárt 7.8125")

A float64 és ε=10−6\varepsilon = 10^{-6} választás miatt a kerekítési zaj 10−16/10−6=10−1010^{-16}/10^{-6} = 10^{-10} nagyságrendű, így a 10−510^{-5} tűrés biztonságos. Alakellenőrzés: a gradiens skalár, a bemenet 3×33 \times 3, a kimenet 2×22 \times 2, minden összegzés határa a kerneldimenziókból következik.

F4. Hibaforrások: (1) float32-ben a kerekítési zaj 10−7/10−3≈10−410^{-7}/10^{-3} \approx 10^{-4} relatív hibát okoz, ami a megfigyelt érték nagyságrendjébe esik; javítás float64-re és ε=10−6\varepsilon=10^{-6}-ra. (2) A forward differencia O(ε)O(\varepsilon) hibája ε\varepsilon-nal arányos; javítás centrális differenciára. (3) A ReLU nullpontjában a klasszikus derivált nem létezik, a numerikus differencia a kétoldali szelőmeredekséget adja, ami nem egyezik az implementáció egyoldali konvenciójával; javítás: a ReLU-t ki kell hagyni a veszteségből, vagy olyan pontot választani, ahol a pre-aktiváció nullától legalább ε\varepsilon nagyságrenddel távolabb van. A 0,350{,}35 relatív hiba e három tényező együttes következménye lehet.

F5. Tervminta. Kilenc rácspont: három véletlen paraméterpont mind a három független seedből; rögzített seed, float64, ε=10−6\varepsilon = 10^{-6}, tolerancia 10−510^{-5} relatív hiba stabil nevezővel. A rácspontok száma indoklás: a 3×33 \times 3 kernelnél a stride-dilation kombináció indexelési hibái csak több, különböző előjelű pontban mutatkoznak meg. Korlátok: az ellenőrzés csak a választott N=4N=4, H=W=10H=W=10 alakra érvényes; más batchméretre a biasgradiens összegzési hibája nem jelenik meg; a véges ε\varepsilon miatt a 10−510^{-5} alatti egyezés sem teljes bizonyítás, csak numerikus evidenciaszint; a nullmértékű paraméterhalmazon rejtőző hiba nem zárható ki. A protokollt ki kell egészíteni az analitikus backward levezetésének szemantikai ellenőrzésével.

15.4.8 Tipikus tévedés, önellenőrzés és visszahívás

A leggyakoribb tévedés, hogy két azonos hibás implementáció egyezését helyességnek tekintjük. Ha a saját backward és a referenciakód ugyanazt a flipped-kernel hibát tartalmazza, az egyezés tökéletes, mégsem bizonyít semmit. A centrális differencia azért erős kontroll, mert nem ismeri a képletet: csak a forwardot értékeli ki. Ugyanakkor a numerikus egyezés sem általános bizonyítás: véges ε\varepsilon és véges rácspontok mellett csak a tesztelt részhalmazra érvényes. Ezt a kettős korlátot a jelentésben mindig expliciten meg kell nevezni.

Önellenőrzés: a kézi példában a full cross-correlation orientációját érdemes újra levezetni egyetlen koordinátára, mert a flipped kernel használata a leggyakoribb hiba. A fenti X1,1X_{1,1} példa kimutatja: az eredeti kernel ad 7,81257{,}8125-öt, a flipped 10,312510{,}3125-öt.

Visszahívás: a 15.3. Transposed convolution mint adjungált adjungált azonossága garantálta, hogy a bemeneti gradiens a forward operátor transzponáltja; ez a mostani full cross-correlation éppen ennek az indexszintű alakja. A 14.4. Csoportos és depthwise konvolúció groups=1 korlátozása miatt a csatornaösszegzés a fenti képletekben nem bontható blokkokra. A 11.4. Egy teljes PyTorch tanítási iteráció tanítási lépéséből tudjuk, hogy a backward önmagában nem frissíti a paramétereket, ezért a gradiensellenőrzéshez nincs is szükség optimizerre. A 3.4. Sajátérték, SVD és spektrális határ normakorlátja pedig emlékeztet: a gradiensskálát a kernel normája és a bemenet normája együtt határozza meg, ezért a rácspontok skálázását a jelentésben rögzíteni kell.

15.4.9 Rövid összefoglaló

A centrális differencia másodrendű Taylor-maradékát levezettük, és megmutattuk, hogy a diszkretizációs hiba O(ε2)O(\varepsilon^2), szemben a forward differencia O(ε)O(\varepsilon) hibájával. A stabil relatív hiba nullához közeli gradiensnél is értelmezhető marad. A konvolúciós backward ellenőrzése három független úton történik: analitikus képlet, numerikus differencia és keretrendszer-autograd. A labor float64-ben, rögzített seeddel, paraméterrácson fut, és JSON-jelentésbe menti a maximális relatív hibákat. A numerikus egyezés csak a kimondott részhalmazra, véges ε\varepsilon és véges rácspontok mellett érvényes; nem helyettesíti az analitikus levezetést. Két azonos hibás implementáció egyezése önmagában gyenge kontroll.

15.4.10 Következő alkalmazás

A következő, 16.1. Eltolási equivariance és invariance egység az eltolási equivariance és invariance fogalmát vizsgálja. Az itteni gradiensellenőrzési protokoll arra is használható, hogy egy equivariance-állítás numerikus sérülését kimutassuk a határfeltételeknél: a full cross-correlation gradiens és a stride-részcsoport szerinti eltolás kapcsolatát az itt bevezetett float64 mérőszámmal ellenőrizhetjük.

15.4.11 Források