37.2 Backward és ellenőrzési lefedettség

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

A 37.1. Operátorspecifikáció és kézi referencia egységben rögzítettük a teljes forward-szerződést: a referenciaoperátor NCHW bemenetre, OIHW súlyra, groups=1-gyel számol térbeli keresztkorrelációt szimmetrikus zero paddinggel, stride-dal és dilationnel. Itt azt kérdezzük, mi a helyes VJP (vektor–Jacobi-szorzat) mindhárom tanuló objektumra, azaz XX inputra, KK súlyra és bb biasra. Induló kérdés: ha H=3H=3, k=2k=2, s=1s=1, p=0p=0, akkor a belső X[1,1]X[1,1] pixel vajon egyetlen kimenethez tartozik-e, vagy többhöz is, és ez hogyan látszik a gradiens összegzésében? Becslés: több kimenethez tartozik, tehát a gradiensnak összegeznie kell az átfedő hozzájárulásokat; ezt levezetjük és számpéldán ellenőrizzük.

Előismeret: a forward indexképlete és outputalakja 37.1. Operátorspecifikáció és kézi referencia, a VJP oszlopkonvenciója 4.3. Jacobi-mátrix, láncszabály és VJP, a gradiensellenőrzés alapmérőszámai 10.3. Gradiensellenőrzés és numerikus hibamérce. A dX/dW/dbdX/dW/db jelölés itt mindig a skalár loss szerinti gradiens, nem egy közvetlen mátrixderivált: ∇XL\nabla_X L, ∇KL\nabla_K L, ∇bL\nabla_b L.

37.2.2 Jelölések, alakok és a forward visszahívása

Legyen X∈ℝN×Cin×H×W,K∈ℝCout×Cin×kh×kw,b∈ℝCout,X\in\mathbb R^{N\times C_{in}\times H\times W},\qquad K\in\mathbb R^{C_{out}\times C_{in}\times k_h\times k_w},\qquad b\in\mathbb R^{C_{out}}, a stride ss, a szimmetrikus padding pp, a dilation dd; mind pozitív egész, kivéve p≥0p\ge0. Az output Y∈ℝN×Cout×Hout×WoutY\in\mathbb R^{N\times C_{out}\times H_{out}\times W_{out}} alakú, ahol Hout=⌊H+2p−d(kh−1)−1s⌋+1,H_{out}=\left\lfloor\frac{H+2p-d(k_h-1)-1}{s}\right\rfloor+1, és a szélességre ugyanígy, kwk_w-vel. A forward egyetlen indexképlete a zérussal kiegészített X‾\bar X tenzoron: X‾\bar X alakja N×Cin×(H+2p)×(W+2p)N\times C_{in}\times(H+2p)\times(W+2p), és X‾[n,c,a′,b′]=X[n,c,a′−p,b′−p],\bar X[n,c,a',b'] = X[n,c,a'-p,b'-p], ha a′∈[p,p+H−1]a'\in[p,p+H-1] és b′∈[p,p+W−1]b'\in[p,p+W-1], egyébként 00. Ezzel Y[n,o,i,j]=b[o]+∑c=0Cin−1∑u=0kh−1∑v=0kw−1K[o,c,u,v]X‾[n,c,si+du,sj+dv],Y[n,o,i,j] = b[o]+\sum_{c=0}^{C_{in}-1}\sum_{u=0}^{k_h-1}\sum_{v=0}^{k_w-1} K[o,c,u,v]\,\bar X[n,c,s i+d u,\,s j+d v], \tag{1} ahol n∈[0,N−1]n\in[0,N-1], o∈[0,Cout−1]o\in[0,C_{out}-1], i∈[0,Hout−1]i\in[0,H_{out}-1], j∈[0,Wout−1]j\in[0,W_{out}-1].

Definíció (upstream gradiens). Legyen L:ℝN×Cout×Hout×Wout→ℝL:\mathbb R^{N\times C_{out}\times H_{out}\times W_{out}}\to\mathbb R differenciálható skalár veszteség, és legyen δ=∇YL∈ℝN×Cout×Hout×Wout,\delta=\nabla_Y L\in\mathbb R^{N\times C_{out}\times H_{out}\times W_{out}}, azaz δ[n,o,i,j]=∂L∂Y[n,o,i,j]\delta[n,o,i,j]=\frac{\partial L}{\partial Y[n,o,i,j]}. A projekt kézi konvenciója L=12∑n,o,i,jY[n,o,i,j]2L=\frac12\sum_{n,o,i,j}Y[n,o,i,j]^2, ekkor δ=Y\delta=Y; ez nem MSE és nem cross-entropy, csak a levezetés ellenőrzését egyszerűsíti.

A VJP alakja mindig megegyezik a megfelelő tanuló objektum alakjával:

Objektum Gra diens Alak
XX ∇XL\nabla_X L N×Cin×H×WN\times C_{in}\times H\times W
KK ∇KL\nabla_K L Cout×Cin×kh×kwC_{out}\times C_{in}\times k_h\times k_w
bb ∇bL\nabla_b L CoutC_{out}

37.2.3 Tétel: a forward teljes VJP-je

Tétel (a konvolúciós forward minden VJP-je). Az (1) forwardra, differenciálható skalár LL mellett:

  1. Biasgradiens, minden o∈[0,Cout−1]o\in[0,C_{out}-1]-re: ∂L∂b[o]=∑n=0N−1∑i=0Hout−1∑j=0Wout−1δ[n,o,i,j].\frac{\partial L}{\partial b[o]}=\sum_{n=0}^{N-1}\sum_{i=0}^{H_{out}-1}\sum_{j=0}^{W_{out}-1}\delta[n,o,i,j]. \tag{2}

  2. Súlygradiens, minden o,c,u,vo,c,u,v indexre: ∂L∂K[o,c,u,v]=∑n=0N−1∑i=0Hout−1∑j=0Wout−1δ[n,o,i,j]X‾[n,c,si+du,sj+dv].\frac{\partial L}{\partial K[o,c,u,v]}=\sum_{n=0}^{N-1}\sum_{i=0}^{H_{out}-1}\sum_{j=0}^{W_{out}-1}\delta[n,o,i,j]\,\bar X[n,c,s i+d u,s j+d v]. \tag{3} Az eredeti XX-szel ez azon (n,i,j)(n,i,j) pozíciókra értendő, ahol si−p+du∈[0,H−1]s i-p+d u\in[0,H-1] és sj−p+dv∈[0,W−1]s j-p+d v\in[0,W-1], és ott az integrandus X[n,c,si−p+du,sj−p+dv]X[n,c,s i-p+d u,s j-p+d v]; a padded írásmód a határon kívüli pozíciókat zérussal kezeli.

  3. Inputgradiens a zero-padded tartományon, minden n,c,a′,b′n,c,a',b'-re: ∂L∂X‾[n,c,a′,b′]=∑o=0Cout−1∑u=0kh−1∑v=0kw−1∑i,ja′=si+du,b′=sj+dvδ[n,o,i,j]K[o,c,u,v],\frac{\partial L}{\partial \bar X[n,c,a',b']}=\sum_{o=0}^{C_{out}-1}\sum_{u=0}^{k_h-1}\sum_{v=0}^{k_w-1}\sum_{\substack{i,j\\a'=s i+d u,\ b'=s j+d v}}\delta[n,o,i,j]\,K[o,c,u,v], \tag{4} ahol az utolsó összeg azon i∈[0,Hout−1]i\in[0,H_{out}-1], j∈[0,Wout−1]j\in[0,W_{out}-1] párokra megy, amelyek a megadott afin egyenletet teljesítik. Végül a padding levágása: ∂L∂X[n,c,a,b]=∂L∂X‾[n,c,a+p,b+p],a∈[0,H−1],b∈[0,W−1].\frac{\partial L}{\partial X[n,c,a,b]}=\frac{\partial L}{\partial \bar X[n,c,a+p,b+p]},\qquad a\in[0,H-1],\ b\in[0,W-1]. \tag{5}

Bizonyítás. A b[o]b[o] csak az Y[n,o,i,j]Y[n,o,i,j] kimenetekben jelenik meg, mindegyikben 11 együtthatóval, ezért a skalár láncszabály közvetlenül adja (2)-t.

A K[o,c,u,v]K[o,c,u,v] szintén csak az oo kimeneti csatorna értékeiben szerepel, de ott minden (n,i,j)(n,i,j) pozícióban, X‾[n,c,si+du,sj+dv]\bar X[n,c,s i+d u,s j+d v] együtthatóval. Ezért ∂L∂K[o,c,u,v]=∑n,i,jδ[n,o,i,j]∂Y[n,o,i,j]∂K[o,c,u,v],\frac{\partial L}{\partial K[o,c,u,v]}=\sum_{n,i,j}\delta[n,o,i,j]\frac{\partial Y[n,o,i,j]}{\partial K[o,c,u,v]}, és a parciális derivált éppen a megfelelő X‾\bar X-érték, ami (3)-at adja.

Az inputgradiensnél először a padded változóra alkalmazzuk a láncszabályt. Egy rögzített X‾[n,c,a′,b′]\bar X[n,c,a',b'] elem azokban az Y[n,o,i,j]Y[n,o,i,j] kimenetekben szerepel, amelyekre létezik u,vu,v, hogy a′=si+dua'=s i+d u és b′=sj+dvb'=s j+d v; ezekben a parciális derivált K[o,c,u,v]K[o,c,u,v]. Ez pontosan (4). Minden összeg véges, ezért az összegzések sorrendje szabadon felcserélhető (a Fubini-tétel véges halmazokra vonatkozó triviális esete; a könyv nem hív mértékelméleti állítást). Az (5) vágás abból következik, hogy a padded tenzor belső része az eredeti XX eltolt másolata, tehát a megfelelő Jacobi-rész az identitás; a padding tartománybeli gradiens nem tartozik egyetlen XX-elemhez sem. ▫\square

A (4) összeg az „overlap” (átfedés) kvantitatív alakja: ugyanaz az input pozíció több kimeneti pozíciót is érint, és a gradiens a közösen használt KK-súlyokkal súlyozva összegződik. A (3) pedig azt mondja, hogy a súlygradiens a δ\delta és az input térbeli keresztkorrelációja, azzal a fontos különbséggel, hogy a kimeneti pozíciók szerint összegzünk, tehát ugyanaz a súly minden pozícióból kap hozzájárulást.

Megjegyzés (adjoinált, nem inverz). A (4)–(5) művelet a forward lineáris operátor adjungáltja a megadott stride/padding/dilation konvencióval; kapcsolódik a ConvTranspose2d-hez, de nem általános inverz, és a kernel megfordítási konvenciója attól függ, hogy a forwardot korrelációnak vagy matematikai konvolúciónak definiáltuk. A részletes adjungált-tulajdonságot 37.3. Linearitás, adjungált és határfeltétel tárgyalja, a könyvtári háttér a [D05] és [D11] körébe tartozik; ebben az egységben a közvetlen (4)–(5) scatter-add formulát használjuk.

37.2.4 Eredeti kézi kis példa

Legyen N=2N=2, Cin=Cout=1C_{in}=C_{out}=1, H=W=3H=W=3, kh=kw=2k_h=k_w=2, s=1s=1, p=0p=0, d=1d=1, tehát Hout=Wout=2H_{out}=W_{out}=2. Az inputok: X[0]=(123456789),X[1]=(987654321),X[0]=\begin{pmatrix}1&2&3\\4&5&6\\7&8&9\end{pmatrix},\qquad X[1]=\begin{pmatrix}9&8&7\\6&5&4\\3&2&1\end{pmatrix}, K=((1,0),(0,1))K=((1,0),(0,1)), b=[0.5]b=[0.5]. A forward kézzel: Y[0]=(6.58.512.514.5),Y[1]=(14.512.58.56.5).Y[0]=\begin{pmatrix}6.5&8.5\\12.5&14.5\end{pmatrix},\qquad Y[1]=\begin{pmatrix}14.5&12.5\\8.5&6.5\end{pmatrix}. A kvadratikus loss miatt δ=Y\delta=Y.

A (2) képlet szerint gb=6.5+8.5+12.5+14.5+14.5+12.5+8.5+6.5=84gb=6.5+8.5+12.5+14.5+14.5+12.5+8.5+6.5=84, azaz gb=[84]gb=[84].

A (3) képletet a négy kernelpozícióra alkalmazva mind a négy gK[u,v]gK[u,v] érték 460460; ennek egyik tagja például a gK[0,0]gK[0,0]-hoz az n=0n=0 képből a (0,0)(0,0) pozíció hozzájárulása 6.5⋅16.5\cdot 1, a (1,1)(1,1) pozícióé 14.5⋅514.5\cdot 5; a teljes összeg mindkét képen és mind a négy eltolásban e példa szimmetriája miatt 460460. Tehát gK=(460460460460).gK=\begin{pmatrix}460&460\\460&460\end{pmatrix}.

Az inputgradiens (4)–(5) szerint scatter-add módon: inicializálunk egy N×1×3×3N\times1\times3\times3 zérus tenzort, és minden (n,i,j,u,v)(n,i,j,u,v) esetén hozzáadunk δ[n,i,j]K[u,v]\delta[n,i,j]K[u,v]-t a (i+u,j+v)(i+u,j+v) cellához. A belső X[0,1,1]=5X[0,1,1]=5 elemre például két tag esik: δ[0,1,1]K[0,0]=14.5\delta[0,1,1]K[0,0]=14.5 és δ[0,0,0]K[1,1]=6.5\delta[0,0,0]K[1,1]=6.5, összegük 2121. A teljes eredmény: gX[0]=(6.58.5012.5218.5012.514.5),gX[1]=(14.512.508.52112.508.56.5).gX[0]=\begin{pmatrix}6.5&8.5&0\\12.5&21&8.5\\0&12.5&14.5\end{pmatrix},\qquad gX[1]=\begin{pmatrix}14.5&12.5&0\\8.5&21&12.5\\0&8.5&6.5\end{pmatrix}. A jobb alsó, 00 értékű cellák a p=0p=0 határt mutatják: kívülről nem érkezik hozzájárulás, de a padding nélküli forwardban ott nincs is inputon kívüli tag.

37.2.5 Véges differencia és autograd: mit ellenőrizzünk?

Legyen θ\theta az X,K,bX,K,b bármely megkötött skalár bejegyzése, és tekintsük a többi változót rögzítettnek. A centrális differencia ΔεL(θ)=L(θ+ε)−L(θ−ε)2ε.\Delta_\varepsilon L(\theta)=\frac{L(\theta+\varepsilon)-L(\theta-\varepsilon)}{2\varepsilon}. Mivel a forward θ\theta-ban affin, az L=12∑Y2L=\frac12\sum Y^2 veszteség pedig kvadratikus, a centrális differencia valós aritmetikában algebrailag pontos; az eltérés csak a lebegőpontos kerekítésből származik. Ez hasznos kontroll: ha a referencia gradiens és a centrális differencia között a hiba az ε\varepsilon-hoz képest sok nagyságrenddel kisebb, akkor a képletek, nem pedig a differencia approximációs hibája érvényesül. A gyakorlatban float64, ε=10−5\varepsilon=10^{-5} mellett a relatív hiba tipikusan 10−1110^{-11} alá esik; ezért elfogadási korlátnak atol=10−8\mathrm{atol}=10^{-8}, rtol=10−6\mathrm{rtol}=10^{-6} bőven elég. A relatív hibát stabil nevezővel számoljuk: erel=|gref−gfd|max(|gref|,|gfd|,η),η=10−12.e_{rel}=\frac{|g_{ref}-g_{fd}|}{\max(|g_{ref}|,|g_{fd}|,\eta)},\qquad \eta=10^{-12}. A normálás nélküli, nullához közeli gradiensű elemeket az abszolút korlát fogja el.

A PyTorch-autograd egy független második kontroll: a saját forwardot float64 leaf tenzorokkal futtatjuk, L.backward() után X.grad, K.grad, b.grad összevethető a referencia (2)–(5) értékeivel. A ReLU-val vagy más törésponttal ellentétben a konvolúciós forward a vizsgált pontokon sima, tehát a véges differenciának nincs konvenciófüggő törésponti eltérése; a nem sima eset kezelését 10.3. Gradiensellenőrzés és numerikus hibamérce rögzíti.

37.2.6 Konkrét labor: f64 gradiensellenőrzés több kis tensoron

A labor CPU-n, torch.float64 tenzorokkal fut. Bemeneti konfigurációk:

Azonosító N C→M H×W kernel s p d
A 2 1→1 3×3 2×2 1 0 1
B 1 2→3 4×5 3×2 2 1 1
C 3 2→2 6×5 3×3 1 1 2

Az A eset az előző kézi példa; B és C a stride, padding és dilation határfeltételeit fedi. Minden esetre a labor kiszámolja a forwardot, a referencia backwardot, a centrális differenciát minden X,K,bX,K,b bejegyzésre, és a PyTorch-autograd gradienseket; a hibákat CSV-be menti.

A teljes, futtatható szkript:

#!/usr/bin/env python3
"""n146 labor: conv2d backward ellenőrzés. CPU, float64.
Nem állítunk már elvégzett futtatást; a szkriptet a hallgató futtatja:
    python check_backward.py --eps 1e-5 --out error_table.csv
"""
import argparse, csv
import torch

def conv2d_forward(X, K, b, s, p, d):
    N, C, H, W = X.shape
    M, Ck, kh, kw = K.shape
    Ho = (H + 2*p - d*(kh-1) - 1)//s + 1
    Wo = (W + 2*p - d*(kw-1) - 1)//s + 1
    Y = torch.empty(N, M, Ho, Wo, dtype=X.dtype)
    for n in range(N):
        for o in range(M):
            for i in range(Ho):
                for j in range(Wo):
                    acc = b[o] + 0.0
                    for c in range(C):
                        for u in range(kh):
                            for v in range(kw):
                                a = s*i - p + d*u
                                bq = s*j - p + d*v
                                if 0 <= a < H and 0 <= bq < W:
                                    acc = acc + X[n, c, a, bq] * K[o, c, u, v]
                    Y[n, o, i, j] = acc
    return Y

def conv2d_backward(X, K, dY, s, p, d):
    N, C, H, W = X.shape
    M, Ck, kh, kw = K.shape
    _, Mo, Ho, Wo = dY.shape
    gb = dY.sum(dim=(0, 2, 3))
    gK = torch.zeros_like(K)
    for n in range(N):
        for o in range(M):
            for i in range(Ho):
                for j in range(Wo):
                    for u in range(kh):
                        for v in range(kw):
                            a = s*i - p + d*u
                            bq = s*j - p + d*v
                            if 0 <= a < H and 0 <= bq < W:
                                gK[o, :, u, v] += dY[n, o, i, j] * X[n, :, a, bq]
    Hp, Wp = H + 2*p, W + 2*p
    gXp = torch.zeros(N, C, Hp, Wp, dtype=X.dtype)
    for n in range(N):
        for o in range(M):
            for i in range(Ho):
                for j in range(Wo):
                    for u in range(kh):
                        for v in range(kw):
                            a = s*i + d*u
                            bq = s*j + d*v
                            if 0 <= a < Hp and 0 <= bq < Wp:
                                gXp[n, :, a, bq] += dY[n, o, i, j] * K[o, :, u, v]
    if p == 0:
        gX = gXp
    else:
        gX = gXp[:, :, p:p+H, p:p+W]
    return gX, gK, gb

def quad_loss(Y):
    return 0.5 * torch.sum(Y * Y)

def fd_all(f, theta, eps):
    theta = theta.detach().clone()
    flat = theta.reshape(-1)
    out = torch.empty_like(flat)
    for q in range(flat.numel()):
        plus = theta.clone()
        minus = theta.clone()
        plus.reshape(-1)[q] = flat[q].item() + eps
        minus.reshape(-1)[q] = flat[q].item() - eps
        out[q] = (f(plus) - f(minus)) / (2.0 * eps)
    return out.reshape(theta.shape)

def max_abs_err(a, b):
    return (a - b).abs().max().item()

def max_rel_err(a, b, eta=1e-12):
    d = (a - b).abs()
    den = torch.maximum(a.abs(), b.abs()).clamp_min(eta)
    return (d / den).max().item()

def check_case_A_hand():
    X = torch.tensor([[[[1,2,3],[4,5,6],[7,8,9]]],
                      [[[9,8,7],[6,5,4],[3,2,1]]]], dtype=torch.float64)
    K = torch.tensor([[[[1,0],[0,1]]]], dtype=torch.float64)
    b = torch.tensor([0.5], dtype=torch.float64)
    Y = conv2d_forward(X, K, b, 1, 0, 1)
    gX, gK, gb = conv2d_backward(X, K, Y, 1, 0, 1)
    exp_gX = torch.tensor(
        [[[[6.5,8.5,0.0],[12.5,21.0,8.5],[0.0,12.5,14.5]]],
         [[[14.5,12.5,0.0],[8.5,21.0,12.5],[0.0,8.5,6.5]]]], dtype=torch.float64)
    assert torch.allclose(gX, exp_gX, atol=1e-12)
    assert torch.allclose(gK, torch.tensor([[[[460,460],[460,460]]]], dtype=torch.float64), atol=1e-12)
    assert torch.allclose(gb, torch.tensor([84.0], dtype=torch.float64), atol=1e-12)
    print("A kézi eset rendben.")

CASES = {
    "A": dict(seed=0, shape=(2,1,1,3,3), kernel=(2,2), stride=1, padding=0, dilation=1),
    "B": dict(seed=1, shape=(1,2,3,4,5), kernel=(3,2), stride=2, padding=1, dilation=1),
    "C": dict(seed=2, shape=(3,2,2,6,5), kernel=(3,3), stride=1, padding=1, dilation=2),
}

def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--eps", type=float, default=1e-5)
    ap.add_argument("--out", type=str, default="error_table.csv")
    args = ap.parse_args()
    check_case_A_hand()
    rows = []
    for case_id, cfg in CASES.items():
        torch.manual_seed(cfg["seed"])
        N, C, M, H, W = cfg["shape"]
        kh, kw = cfg["kernel"]
        s, p, d = cfg["stride"], cfg["padding"], cfg["dilation"]
        X = torch.rand(N, C, H, W, dtype=torch.float64) * 2 - 1
        K = torch.rand(M, C, kh, kw, dtype=torch.float64) * 0.6 - 0.3
        b = torch.rand(M, dtype=torch.float64) * 0.4 - 0.2
        X.requires_grad_(True); K.requires_grad_(True); b.requires_grad_(True)
        Y = conv2d_forward(X, K, b, s, p, d)
        dY = Y  # L = 0.5 * sum(Y**2)
        gX, gK, gb = conv2d_backward(X, K, dY, s, p, d)
        L = quad_loss(Y)
        L.backward()
        for name, ga, gt in [("X", gX, X.grad), ("K", gK, K.grad), ("b", gb, b.grad)]:
            rows.append([case_id, name, "autograd",
                         max_abs_err(ga, gt), max_rel_err(ga, gt)])
        Xd = X.detach().clone(); Kd = K.detach().clone(); bd = b.detach().clone()
        fX = lambda t: quad_loss(conv2d_forward(t, Kd, bd, s, p, d))
        fK = lambda t: quad_loss(conv2d_forward(Xd, t, bd, s, p, d))
        fb = lambda t: quad_loss(conv2d_forward(Xd, Kd, t, s, p, d))
        for name, ga, gf in [("X", gX, fd_all(fX, Xd, args.eps)),
                             ("K", gK, fd_all(fK, Kd, args.eps)),
                             ("b", gb, fd_all(fb, bd, args.eps))]:
            rows.append([case_id, name, "fd",
                         max_abs_err(ga, gf), max_rel_err(ga, gf)])
    with open(args.out, "w", newline="") as fh:
        w = csv.writer(fh)
        w.writerow(["case", "object", "method", "max_abs_err", "max_rel_err"])
        w.writerows(rows)
    print(f"Tábla: {args.out}; sorok: {len(rows)}")

if __name__ == "__main__":
    main()

A labor ellenőrzése: az A eset check_case_A_hand assertje a fenti kézi táblázatot bitközeli pontossággal kéri; a csv minden sorában max_abs_err < 1e-8 és max_rel_err < 1e-6 az elfogadási feltétel. A tábla szerkezete a fenti, a tényleges B és C számértékeket itt nem állítjuk már lefuttatottnak; a hallgató a szkript futtatásával állítja elő őket. Az eredmény értelmezése: a hiba táblázat csak a felsorolt (N,C,M,H,W,k,s,p,d) konfigurációkra ad bizonyítékot, és csak float64 CPU-n; a groups≠1, aszimmetrikus padding és float32 approximáció kívül esik a teszten (36.2. Ismétlések és eredménybizonytalanság gondolata a több teszt és a kimondatlan tartomány viszonyáról).

37.2.7 Feladatok

F1 (fogalmi, nehézség 1). N=4N=4, Cin=3C_{in}=3, Cout=5C_{out}=5, H=W=8H=W=8, kh=kw=3k_h=k_w=3, s=2s=2, p=1p=1, d=1d=1. Add meg Hout,WoutH_{out},W_{out} értékét és a δ\delta, ∇XL\nabla_X L, ∇KL\nabla_K L, ∇bL\nabla_b L pontos alakját. Mely tengelyek mentén összegzi a biasgradiens a δ\delta értékeit? Átadandó: az alakok és az összegző tengelyek listája.

F2 (levezetési, nehézség 2). Egydimenziós eset: N=1N=1, Cin=Cout=1C_{in}=C_{out}=1, H=4H=4, kh=2k_h=2, s=1s=1, p=0p=0, d=1d=1; X=[1,−1,1,0]X=[1,-1,1,0], K=[2,1]K=[2,1], b=[0]b=[0], és L=12∑Y2L=\frac12\sum Y^2. Számold ki YY-t, majd a levezetett képletekkel gXgX, gKgK, gbgb értékét. Jelöld meg azokat a cellákat, ahol két átfedő hozzájárulás összegződik. Átadandó: a négy vektor/skalár és a két átfedő tag kifejtése.

F3 (implementációs, nehézség 2). Írj crop_safe(t, padding, h, w) függvényt a projektben a (5) vágásra úgy, hogy padding==0 esetén ne keletkezzen üres szelet; ellenőrizd p=0p=0 és p=2p=2 mellett egy véletlen float64 tensoron. Átadandó: a függvény kódja, a két hívás shape-je és a torch.equal eredménye.

F4 (hibakeresési, nehézség 2). Az alábbi 1D inputgradiens-kód a F2 feladatot próbálja reprodukálni:

gX = torch.zeros(4)
for i in range(3):
    for u in range(2):
        gX[i + u] = dY[i] * K[u]   # hiba itt

Hol a hiba, mi a numerikus következménye a F2 példában, és hogyan javítod? Átadandó: a hibás és a javított kód, a F2 értékeivel való összevetés.

F5 (kísérleti/transzfer, nehézség 3). Tervezd meg a backward-ellenőrzés konfigurációs mátrixát legalább hat esetre a támogatott operátortartományból: változtasd egyenként a paddinget, stride-ot és dilationt, valamint a csatorna- és batch-méretet; rögzítsd, mely kombinációkat nem teszteled. Add meg a CSV sémáját, az elfogadási korlátokat és a következtetés érvényességi határát. Átadandó: protokoll, hat konfiguráció indoklással és a mentett tábla.

37.2.8 Megoldások és indoklás

F1 megoldás. Hout=⌊8+2−1⋅2−12⌋+1=⌊72⌋+1=3+1=4,H_{out}=\left\lfloor\frac{8+2-1\cdot 2-1}{2}\right\rfloor+1=\left\lfloor\frac72\right\rfloor+1=3+1=4, tehát Hout=Wout=4H_{out}=W_{out}=4. Az alakok: δ∈ℝ4×5×4×4\delta\in\mathbb R^{4\times5\times4\times4}, ∇XL∈ℝ4×3×8×8\nabla_X L\in\mathbb R^{4\times3\times8\times8}, ∇KL∈ℝ5×3×3×3\nabla_K L\in\mathbb R^{5\times3\times3\times3}, ∇bL∈ℝ5\nabla_b L\in\mathbb R^{5}. A biasgradiens a δ\delta (n,i,j)(n,i,j), azaz a 00, 22, 33 tengely szerinti összegzéséből áll, a kimeneti csatorna oo tengelyt megtartva.

F2 megoldás. A forward (1) szerint Y[i]=2X[i]+X[i+1]Y[i]=2X[i]+X[i+1], tehát Y=[2⋅1+(−1),2(−1)+1,2⋅1+0]=[1,−1,2]Y=[2\cdot1+(-1),\,2(-1)+1,\,2\cdot1+0]=[1,-1,2]. A kvadratikus loss miatt δ=Y\delta=Y. A (2)–(5) egydimenziós változata: gb=∑iδ[i]=1−1+2=2,gb=\sum_i\delta[i]=1-1+2=2, gK[u]=∑i=02δ[i]X[i+u],gK[0]=1⋅1+(−1)(−1)+2⋅1=4,gK[1]=1(−1)+(−1)1+2⋅0=−2,gK[u]=\sum_{i=0}^{2}\delta[i]X[i+u],\qquad gK[0]=1\cdot1+(-1)(-1)+2\cdot1=4,\quad gK[1]=1(-1)+(-1)1+2\cdot0=-2, gX[a]=∑i∈[0,2],u∈[0,1]a=i+uδ[i]K[u],gX[a]=\sum_{\substack{i\in[0,2],\,u\in[0,1]\\a=i+u}}\delta[i]K[u], tehát gX=[2,−1,3,2]gX=[2,\,-1,\,3,\,2]. Az átfedő pozíciók: gX[1]=1⋅1+(−1)⋅2=−1gX[1]=1\cdot1+(-1)\cdot2=-1 (a δ0K1\delta_0K_1 és δ1K0\delta_1K_0 tagok), gX[2]=(−1)⋅1+2⋅2=3gX[2]=(-1)\cdot1+2\cdot2=3. A gX[0]=2gX[0]=2 és gX[3]=2gX[3]=2 egy-egy tagból állnak; ez mutatja, hogy a határnál nincs padding-hozzájárulás.

F3 megoldás.

def crop_safe(t, padding, h, w):
    row = slice(padding, padding + h) if padding else slice(None)
    col = slice(padding, padding + w) if padding else slice(None)
    return t[:, :, row, col]

# ellenőrzés
torch.manual_seed(0)
t0 = torch.rand(2, 3, 6, 6, dtype=torch.float64)
print(crop_safe(t0, 0, 6, 6).shape)   # torch.Size([2, 3, 6, 6])
print(torch.equal(crop_safe(t0, 0, 6, 6), t0))   # True
t2 = torch.rand(2, 3, 10, 10, dtype=torch.float64)
out = crop_safe(t2, 2, 6, 6)
print(out.shape)   # torch.Size([2, 3, 6, 6])
print(torch.equal(out, t2[:, :, 2:8, 2:8]))  # True

A slice(None) ág azért kell, mert Pythonban -0 == 0, ezért a naiv t[:, :, p:-p] a p=0p=0 esetben t[:, :, 0:0] üres tenzort adna. A megoldás szélsőeset-kontrollja: p=0p=0 shape-azonosság, p>0p>0 érték-egyezés.

F4 megoldás. A hiba az = hozzárendelés a += helyett. Az átfedő pozíciókat a ciklus többször látogatja, és a hozzárendelés felülírja a korábbi hozzájárulást, tehát a gradiens nem összegződik. A F2 példában a hiba hatására gX[1]gX[1] végül a legutolsó i=1,u=0i=1,u=0 tag, (−1)⋅2=−2(-1)\cdot2=-2 lesz a helyes −1-1 helyett, gX[2]gX[2] pedig 44 a helyes 33 helyett. Javítás:

gX = torch.zeros(4)
for i in range(3):
    for u in range(2):
        gX[i + u] += dY[i] * K[u]   # += : összegzés
# ellenőrzés: gX == [2, -1, 3, 2]

A javított verzió a F2 összes értékét visszaadja, beleértve a kétátfedéses pozíciókat.

F5 megoldásminta. Kérdés: a referencia backward egyezik-e az autograddal és a centrális differenciával a támogatott paramétertartományon? Megfigyelési egység: egy konfiguráció, benne a max_abs_err és max_rel_err mindhárom objektumra, módszerenként. Minimum hat eset:

case változtatott dimenzió N, C→M, H×W, k s p d
A alap 2, 1→1, 3×3, 2×2 1 0 1
B stride+p 1, 2→3, 4×5, 3×2 2 1 1
C padding+dilation 3, 2→2, 6×5, 3×3 1 1 2
A_p2 csak p>0 2, 1→1, 3×3, 2×2 1 2 1
A_s2 csak s=2 2, 1→1, 3×3, 2×2 2 0 1
A_d3 csak d=3, nagyobb H 2, 1→1, 6×6, 2×2 1 0 3

A csatorna- és batch-méretet a B és C esetekben változtatjuk, hogy az összegző tengelyek ne csak szingletonok legyenek. Elfogadás: float64, ε=10−5\varepsilon=10^{-5}, atol=1e-8, rtol=1e-6; minden bejegyzésre véges differencia, nem mintavétel. A csv oszlopai: case, object, method, max_abs_err, max_rel_err. Érvényességi határ: a hat eset nem reprezentatív minta a parametertérből; az állítás pontosan a tesztelt (s,p,d,k) halmazra szól, groups=1-re, négyzetes paddingre, float64 CPU-n. Nem tárgyalt kombinációk (pl. s=3s=3, p=3p=3, aszimmetrikus kernel) további kutatást igényelnek; ez ugyanaz a korlát, mint a 36.2. Ismétlések és eredménybizonytalanság alfejezetben a seedek és a fix tesztkészlet viszonya.

37.2.9 Tipikus tévedés és ellenpélda

A leggyakoribb hiba a „minden rendben” állítás a tesztelt tartomány pontos megnevezése nélkül. Tegyük fel, hogy valaki csak az s=1s=1, p=0p=0, d=1d=1, k=2k=2 esetet futtatja, és a gradiens ott egyezik; ebből nem következik a paddingelt ág helyessége. Konkrét ellenpélda: ha az inputgradiensben a padded koordinátát a kód a = s*i + d*u helyett a = s*i - p + d*u-val számolja a gXp-scatterhez is, akkor p=0p=0-nál tökéletesen egyezik, de p=1p=1-nél a padding miatt a (a=-1) hozzájárulások kívülre kerülnek, és a vágás után rossz gXgX adódik. A kijelentés helyes formája: „a backward a felsorolt hat konfiguráción, float64 CPU-n, a megadott toleranciával egyezik az autograddal és a véges differenciával; más tartományra nincs bizonyítékunk”. Ez a 36.2. Ismétlések és eredménybizonytalanság alfejezetben bevezetett, a fix protokoll és a kimondott tartomány közötti különbség közvetlen alkalmazása.

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

Önellenőrzéshez: (i) mindhárom gradiens alakja megegyezik a megfelelő objektum alakjával; (ii) p=0p=0 mellett a padded és a vágott tenzor azonos; (iii) a F2 1D példában a gXgX összege 2−1+3+2=62-1+3+2=6, ami a láncszabály szerint ∑iδi∑uKu=2⋅3=6\sum_i\delta_i\sum_u K_u=2\cdot3=6, tehát a teljes gradiens konzisztens. Visszahívás: a 37.1. Operátorspecifikáció és kézi referencia forward-egyezménye adja a (1) indexhalmazt; a 33.2. Batch és kis szolgáltatás alfejezetben rögzített sémaellenőrzés logikája itt az ellenőrző tábla oszlopainak formális szerződéseként tér vissza; a 25.2. Multilabel és hierarchikus cél megfigyelését, miszerint a loss mindig skalár, úgy használjuk, hogy a forward VJP-je független a veszteség konkrét választásától, csak δ\delta alakjától függ.

37.2.11 Rövid összefoglaló

A konvolúciós forward (1) lineáris tagokból áll, ezért mindhárom VJP véges összeg. A biasgradiens a δ\delta batch- és térbeli összege; a súlygradiens a δ\delta és az input keresztkorrelációja; az inputgradiens a δ\delta-t a KK-súlyokkal scatter-add módon szétszórja a padded tartományra, majd levágja a paddinget. A centrális differencia a kvadratikus loss miatt valós aritmetikában pontos, így float64 mellett az autograddal együtt két független numerikus kontrollt ad. A lab hat kis konfiguráción, CSV-táblával, 1e-8 abszolút és 1e-6 relatív korláttal ellenőriz; a p=0p=0 üres szelet és az átfedések += összegzése a leggyakoribb implementációs hibák. A következő 37.3. Linearitás, adjungált és határfeltétel egység a linearitást, az adjungált tulajdonságot és a határfeltételeket vizsgálja.

37.2.12 Források