40.1 Összetett levezetési vizsga

40.1.1 1. Cél, előismeret és induló kérdés

Ez az egység a záró vizsga levezetési része. A cél nem új réteg bemutatása, hanem annak ellenőrzése, hogy a hallgató egy korábban nem látott, kis neurális gráfot önállóan definiál, teljes alakhelyes deriváltját levezeti, és a levezetést numerikus kontrollal megerősíti. A feladat a 36.4. IX. ellenőrző pont: lektorálható modellállítás lektorálható csomagjának matematikai komponensét, a 37.4. Rétegből kis tanulómodell integrált saját réteget és a 38.4. Mentett modellből független inference inference-szerződés pontosságigényét egyetlen vizsgapéldában kapcsolja össze.

Előismeret: a 36.4. IX. ellenőrző pont: lektorálható modellállítás hatkomponensű bírálati kerete, a 37.4. Rétegből kis tanulómodell alfejezetben rögzített modellszintű véges differenciás ellenőrzés, a 38.4. Mentett modellből független inference visszatöltési tolerancia fogalma és a 39.4. Alkalmazási riport és átadás alkalmazási riport határkijelölése. Visszautalás: a 28.1. Embedding, távolság és normalizálás normalizálási azonossága, a 36.1. Állításból kísérleti terv hipotézis/kontroll elválasztása és a 39.1. Feladat- és annotációs döntés annotációs konvenció.

Induló kérdés: elegendő-e egy loss.backward() sor sikeres lefutása annak bizonyításához, hogy egy új gráf deriváltját értjük? Becslés: nem. A könyvtári hívás a láncszabály minden lépését elrejti; a vizsga ezért kézzel levezetett, alakellenőrzött és float64 véges differenciával megerősített megoldást vár.

40.1.2 2. A vizsgagráf definíciója

A vizsgafeladat a következő, a könyvben ebben az összeállításban még nem szereplő kompozíciót elemzi. A bemenet X∈ℝN×Cin×LX\in\mathbb R^{N\times C_{in}\times L}, ahol LL a térbeli hossz. Az első lépés 1D keresztkorreláció (cross-correlation, kernelmegfordítás nélkül) szimmetrikus zero paddinggel, stride 1-gyel, dilation 1-gyel, groups=1-gyel:

Zn,o,j=bo+∑i=1Cin∑r=0k−1X̃n,i,j+rKo,i,r, Z_{n,o,j}=b_o+\sum_{i=1}^{C_{in}}\sum_{r=0}^{k-1}\tilde X_{n,i,\,j+r}\,K_{o,i,r},

ahol X̃\tilde X a párnázott bemenet, K∈ℝCout×Cin×kK\in\mathbb R^{C_{out}\times C_{in}\times k}, b∈ℝCoutb\in\mathbb R^{C_{out}}, j=0,…,Lout−1j=0,\ldots,L_{out}-1, és

Lout=L+2p−k+1. L_{out}=L+2p-k+1.

A második lépés elemenkénti ReLU: A=ReLU(Z)A=\operatorname{ReLU}(Z), azaz An,c,j=max(0,Zn,c,j)A_{n,c,j}=\max(0,Z_{n,c,j}). A harmadik lépés globális átlag-pooling (global average pooling, GAP) a térbeli dimenzió felett:

hn,c=1Lout∑j=0Lout−1An,c,j,hn∈ℝCout. h_{n,c}=\frac{1}{L_{out}}\sum_{j=0}^{L_{out}-1}A_{n,c,j},\qquad h_n\in\mathbb R^{C_{out}}.

A negyedik lépés L2-normalizálás:

un=hn∥hn∥2, u_n=\frac{h_n}{\|h_n\|_2},

ahol ∥hn∥2=(∑chn,c2)1/2\|h_n\|_2=(\sum_c h_{n,c}^2)^{1/2}. A veszteség egy rögzített, egységnormájú célvektorra, t∈ℝCoutt\in\mathbb R^{C_{out}}, ∥t∥2=1\|t\|_2=1, NN megfigyelés átlagában:

ℒ=1N∑n=1N(1−un⊤t)+λ2∥K∥F2. \mathcal L=\frac1N\sum_{n=1}^{N}\bigl(1-u_n^\top t\bigr) +\frac{\lambda}{2}\|K\|_F^2.

A regularizáló tag csak a konvolúciós súlyra vonatkozik, a biasra nem; λ≥0\lambda\ge0. A λ2\frac\lambda2 konvenció gradiense λK\lambda K. A ReLU nullponti deriváltját itt 0-nak definiáljuk, ez a PyTorch F.relu viselkedése is; a nullpontot a numerikus ellenőrzésben kifejezetten elkerüljük.

40.1.3 3. A teljes gradiens mint lemma

1. lemma (az új kompozíció teljes gradiense). Tegyük fel, hogy Lout≥1L_{out}\ge1, p≥0p\ge0, k≥1k\ge1, stride=dilation=1, a padding szimmetrikus zero padding, ∥hn∥2>0\|h_n\|_2>0 minden nn-re, és a ReLU nullponti deriváltja 0. Ekkor minden olyan pontban, ahol egyetlen Zn,c,jZ_{n,c,j} sem nulla, a ℒ\mathcal L gradiensei a következők.

  1. Normalizálás és cosinus-veszteség:

∇hnℒ=−1N∥hn∥2(t−(un⊤t)un). \nabla_{h_n}\mathcal L =-\frac{1}{N\|h_n\|_2}\bigl(t-(u_n^\top t)u_n\bigr).

  1. Globális átlag-pooling:

∇An,c,jℒ=1Lout(∇hnℒ)c. \nabla_{A_{n,c,j}}\mathcal L=\frac{1}{L_{out}}(\nabla_{h_n}\mathcal L)_c.

  1. ReLU:

∇Zn,c,jℒ=∇An,c,jℒ⋅𝟏[Zn,c,j>0]. \nabla_{Z_{n,c,j}}\mathcal L =\nabla_{A_{n,c,j}}\mathcal L\cdot \mathbf 1[Z_{n,c,j}>0].

  1. Bias:

∇boℒ=∑n=1N∑j=0Lout−1(∇Zℒ)n,o,j. \nabla_{b_o}\mathcal L=\sum_{n=1}^{N}\sum_{j=0}^{L_{out}-1}(\nabla_Z\mathcal L)_{n,o,j}.

  1. Súlygradiens, r=0,…,k−1r=0,\ldots,k-1:

∇Ko,i,rℒ=∑n=1N∑j=0Lout−1(∇Zℒ)n,o,jX̃n,i,j+r+λKo,i,r. \nabla_{K_{o,i,r}}\mathcal L =\sum_{n=1}^{N}\sum_{j=0}^{L_{out}-1}(\nabla_Z\mathcal L)_{n,o,j}\,\tilde X_{n,i,\,j+r} +\lambda K_{o,i,r}.

A (iv)–(vi) képletekben nincs újabb 1/N1/N szorzó: a batch-átlag 1/N1/N tényezője egyszer, az (i) lépésben lép be, és onnan a ∇A\nabla_A, ∇Z\nabla_Z gradiensekkel öröklődik. Ezért két azonos mintából álló batch (N=2N=2) átlagolt veszteségének súly- és biasgradiense megegyezik az egymintás (N=1N=1) értékkel; ha a (iv)–(v) összegeket még egyszer NN-nel osztanánk, a gradiens N=2N=2-nél a felére esne.

  1. Bemeneti gradiens: először a párnázott X̃\tilde X gradiensét számítjuk a s=0,…,L+2p−1s=0,\ldots,L+2p-1 indexeken,

∇X̃n,i,sℒ=∑o=1Cout∑r=0k−1𝟏[0≤s−r≤Lout−1](∇Zℒ)n,o,s−rKo,i,r, \nabla_{\tilde X_{n,i,s}}\mathcal L =\sum_{o=1}^{C_{out}}\sum_{r=0}^{k-1} \mathbf 1[0\le s-r\le L_{out}-1]\, (\nabla_Z\mathcal L)_{n,o,\,s-r}\,K_{o,i,r},

majd a paddinget levágjuk: ∇Xn,i,ℓℒ=∇X̃n,i,ℓ+pℒ\nabla_{X_{n,i,\ell}}\mathcal L=\nabla_{\tilde X_{n,i,\ell+p}}\mathcal L minden ℓ=0,…,L−1\ell=0,\ldots,L-1 esetén.

Bizonyítás. (i) Jelölje f(x)=x/∥x∥2f(x)=x/\|x\|_2. A hányadosszabályból a parciális derivált:

$$ \frac{\partial f_c}{\partial x_j} =\frac{\delta_{cj}\|x\|_2-x_c x_j/\|x\|_2}{\|x\|_2^2} =\frac{1}{\|x\|_2}\Bigl(\delta_{cj}-\frac{x_c x_j}{\|x\|_2^2}\Bigr). $$

Mivel u=f(x)u=f(x) és δcj\delta_{cj} az II egységmátrix eleme, ezért a Jacobi-mátrix

$$ J_f(x)=\frac{1}{\|x\|_2}\Bigl(I_{C_{out}}-uu^\top\Bigr). $$

A mátrix szimmetrikus, tehát a VJP a 4.3. Jacobi-mátrix, láncszabály és VJP konvenciója szerint ∇xℒ=Jf(x)⊤∇uℒ=Jf(x)∇uℒ\nabla_x\mathcal L=J_f(x)^\top\nabla_u\mathcal L=J_f(x)\nabla_u\mathcal L. A belső veszteségre ∇u(1−u⊤t)=−t\nabla_u(1-u^\top t)=-t. Innen az (i) képlet; az 1/N1/N faktor a batch-átlag lineáris láncszabályából jön. (ii) A GAP lineáris leképezés, minden jj-re ∂hn,c/∂An,c,j=1/Lout\partial h_{n,c}/\partial A_{n,c,j}=1/L_{out}, tehát a VJP a konstans 1/Lout1/L_{out} szorzó. (iii) A ReLU a kimondott konvencióval minden Z≠0Z\ne0 pontban klasszikusan differenciálható, deriváltja 00 vagy 11; a nullpontot a lemma feltétele kizárja. (iv)–(v) A keresztkorreláció a KK elemeiben lineáris, ezért a láncszabály a ZZ gradiensét a X̃\tilde X megfelelő szegmensével súlyozza; a λK\lambda K tag a λ2∥K∥F2\frac\lambda2\|K\|_F^2 deriváltja. (vi) Ugyanaz a linearitás az input irányában: egy adott X̃n,i,s\tilde X_{n,i,s} elem minden olyan (o,j,r)(o,j,r) hármasban szerepel, amelyre s=j+rs=j+r, azaz j=s−rj=s-r. A 𝟏\mathbf 1-feltétel a kimeneti index érvényességét biztosítja; a padding zónákon kívül eső tagok a zero padding miatt eleve nem járulnak hozzá. A padding levágása az utolsó lépés definíció szerint. Alakellenőrzés: ∇hℒ∈ℝN×Cout\nabla_h\mathcal L\in\mathbb R^{N\times C_{out}}, ∇A,∇Z∈ℝN×Cout×Lout\nabla_A,\nabla_Z\in\mathbb R^{N\times C_{out}\times L_{out}}, ∇b∈ℝCout\nabla_b\in\mathbb R^{C_{out}}, ∇K∈ℝCout×Cin×k\nabla_K\in\mathbb R^{C_{out}\times C_{in}\times k}, ∇X∈ℝN×Cin×L\nabla_X\in\mathbb R^{N\times C_{in}\times L}. ◼\blacksquare

Ellenpélda a feltétel szükségességére. Két dolgot kell elkülöníteni. (1) A normalizáló függvény önmagában: f(x)=x/∥x∥2f(x)=x/\|x\|_2 a nullában nem definiált, és nem is terjeszthető ki folytonosan: C=1C=1 esetén f(ε)=sign(ε)f(\varepsilon)=\operatorname{sign}(\varepsilon), így a nulla körüli középső differencia (f(ε)−f(−ε))/(2ε)=1/ε→∞\bigl(f(\varepsilon)-f(-\varepsilon)\bigr)/(2\varepsilon)=1/\varepsilon\to\infty. (2) A teljes gráfban ez a pont nem így érhető el: hn=GAP(ReLU(Zn))h_n=\operatorname{GAP}(\operatorname{ReLU}(Z_n)) minden komponense nemnegatív, és hn=0h_n=0 pontosan akkor, ha minden Zn,c,j≤0Z_{n,c,j}\le0. Ha minden Zn,c,j<0Z_{n,c,j}<0, akkor kis perturbációra is hn=0h_n=0 marad, a veszteség egy egész környezetben csak a könyvtár konvenciójától függ (a PyTorch F.normalize a nullvektorra u=0u=0-t, így 1−u⊤t=11-u^\top t=1-et ad); ugrás nincs, de a gradiens sem informatív. Ugrás csak akkor van, ha a hn=0h_n=0 pontban valamelyik Zn,c,j=0Z_{n,c,j}=0, azaz a ReLU aktív halmaza a perturbációval változik. Példa: Cin=1C_{in}=1, Cout=2C_{out}=2, L=1L=1, p=0p=0, k=1k=1, K1,1,0=1K_{1,1,0}=1, K2,1,0=−1K_{2,1,0}=-1, b=0b=0, t=(1,0)⊤t=(1,0)^\top, X1,1,0=0X_{1,1,0}=0. Ekkor Z=(0,0)Z=(0,0) és h=0h=0. Ha X=ε>0X=\varepsilon>0, akkor h=(ε,0)h=(\varepsilon,0), u=(1,0)u=(1,0) és ℒ=0\mathcal L=0; ha X=−εX=-\varepsilon, akkor h=(0,ε)h=(0,\varepsilon), u=(0,1)u=(0,1) és ℒ=1\mathcal L=1. A középső differencia −1/(2ε)-1/(2\varepsilon), ami ε=10−2,10−4,10−6\varepsilon=10^{-2},10^{-4},10^{-6} mellett −50-50, −5000-5000, −5⋅105-5\cdot10^{5} (float64-ben futtatva), tehát nem konvergál. Itt a két kizárt eset, a ∥hn∥2=0\|h_n\|_2=0 és a Zn,c,j=0Z_{n,c,j}=0, együtt fordul elő; a lemma mindkét feltétele szükséges, és egyik sem technikai részlet.

40.1.4 4. Eredeti kézi kis példa

Legyen N=1N=1, Cin=1C_{in}=1, L=3L=3, Cout=2C_{out}=2, k=2k=2, p=1p=1, λ=0\lambda=0. Bemenet és súly:

X=(121),K=(111−1)(alakja 2×1×2),b=(00). X=\begin{pmatrix}1&2&1\end{pmatrix},\qquad K=\begin{pmatrix}1&1\\1&-1\end{pmatrix}\ \text{(alakja }2\times1\times2\text{)},\qquad b=\begin{pmatrix}0\\0\end{pmatrix}.

A párnázott bemenet X̃=(0,1,2,1,0)\tilde X=(0,1,2,1,0), Lout=4L_{out}=4. A keresztkorreláció:

Z1=(1,3,3,1),Z2=(−1,−1,1,1), Z_1=(1,3,3,1),\qquad Z_2=(-1,-1,1,1),

tehát ZZ alakja (1,2,4)(1,2,4). ReLU után:

A1=(1,3,3,1),A2=(0,0,1,1). A_1=(1,3,3,1),\qquad A_2=(0,0,1,1).

A GAP:

h=(20.5),∥h∥2=172,u=117(41). h=\begin{pmatrix}2\\0.5\end{pmatrix},\qquad \|h\|_2=\frac{\sqrt{17}}2,\qquad u=\frac{1}{\sqrt{17}}\begin{pmatrix}4\\1\end{pmatrix}.

A cél t=(1,0)⊤t=(1,0)^\top. A veszteség:

ℒ=1−417≈0.029858. \mathcal L=1-\frac{4}{\sqrt{17}}\approx 0.029858.

A lemma szerint u⊤t=4/17u^\top t=4/\sqrt{17}, ezért

t−(u⊤t)u=(1/17−4/17),∇hℒ=−217(1/17−4/17)=(−2171781717). t-(u^\top t)u=\begin{pmatrix}1/17\\-4/17\end{pmatrix}, \qquad \nabla_h\mathcal L=-\frac{2}{\sqrt{17}}\begin{pmatrix}1/17\\-4/17\end{pmatrix} =\begin{pmatrix}-\frac{2}{17\sqrt{17}}\\[2mm] \frac{8}{17\sqrt{17}}\end{pmatrix}.

A GAP 1/41/4 osztása után, az A2A_2 két nulláját a ReLU-gradiens lenullázza:

∇Zℒ=(−13417−13417−13417−13417002171721717). \nabla_Z\mathcal L= \begin{pmatrix} -\frac{1}{34\sqrt{17}} & -\frac{1}{34\sqrt{17}} & -\frac{1}{34\sqrt{17}} & -\frac{1}{34\sqrt{17}}\\[2mm] 0 & 0 & \frac{2}{17\sqrt{17}} & \frac{2}{17\sqrt{17}} \end{pmatrix}.

Innen a biasgradiens:

∇bℒ=(−2171741717)≈(−0.028530.05707). \nabla_b\mathcal L= \begin{pmatrix}-\frac{2}{17\sqrt{17}}\\[2mm] \frac{4}{17\sqrt{17}}\end{pmatrix} \approx\begin{pmatrix}-0.02853\\0.05707\end{pmatrix}.

A súlygradiens a X̃\tilde X szegmenseivel:

∇Kℒ=(−21717−217176171721717)≈(−0.02853−0.028530.085590.02853). \nabla_K\mathcal L= \begin{pmatrix} -\frac{2}{17\sqrt{17}} & -\frac{2}{17\sqrt{17}}\\[2mm] \frac{6}{17\sqrt{17}} & \frac{2}{17\sqrt{17}} \end{pmatrix} \approx \begin{pmatrix}-0.02853&-0.02853\\0.08559&0.02853\end{pmatrix}.

A párnázott bemeneti gradiens s=0,…,4s=0,\ldots,4:

∇X̃ℒ=(−13417−1171711717−11717−53417), \nabla_{\tilde X}\mathcal L= \begin{pmatrix} -\frac{1}{34\sqrt{17}}&-\frac{1}{17\sqrt{17}}&\frac{1}{17\sqrt{17}}&-\frac{1}{17\sqrt{17}}&-\frac{5}{34\sqrt{17}} \end{pmatrix},

a padding levágása után:

∇Xℒ=(−1171711717−11717)≈(−0.0142670.014267−0.014267). \nabla_X\mathcal L= \begin{pmatrix} -\frac{1}{17\sqrt{17}}&\frac{1}{17\sqrt{17}}&-\frac{1}{17\sqrt{17}} \end{pmatrix} \approx \begin{pmatrix}-0.014267&0.014267&-0.014267\end{pmatrix}.

Az egzakt törtek a kézi ellenőrzés rögzített várt értékei. A következő kód ugyanezt a levezetést a PyTorch autogradjával veti össze float64-ben.

40.1.5 5. Konkrét labor: önálló levezetés és numerikus kontroll

Munkakönyvtár: n157_lab/. Fájl: derive_check.py.

Futtatás: python n157_lab/derive_check.py. A főút kis helyi CPU, float64, seed rögzített. A kód a fenti gráfot építi fel, kiszámolja az autograd gradienseket, majd saját központi differenciával ellenőrzi a súly-, bias- és bemeneti gradiens minden elemét. Ezután a lemma (i)–(vi) képleteit teljes kézi backwardként (manual_backward) is megvalósítja, és (a) a 4. szakasz példáján, illetve két azonos mintájából álló batch-en, (b) egy független, nem degenerált batch-en (N=3N=3, Cin=2C_{in}=2, Cout=3C_{out}=3, L=5L=5, k=3k=3, p=1p=1, λ=0,03\lambda=0{,}03, nem kollineáris hnh_n és tt) veti össze az autograddal és a központi differenciával. A (b) rész azért kell, mert a 4. szakasz példája N=1N=1, Cin=1C_{in}=1 és λ=0\lambda=0 mellett nem mutatná ki sem a hibás 1/N1/N-et, sem a regularizációs tag hibáját.

import torch
import torch.nn.functional as F

torch.manual_seed(20261020)
dtype = torch.float64
device = torch.device("cpu")
eps = 1.0e-6
atol = 1.0e-7

X = torch.tensor([[[1.0, 2.0, 1.0]]], dtype=dtype, device=device, requires_grad=True)
K = torch.tensor([[[1.0, 1.0]], [[1.0, -1.0]]], dtype=dtype, device=device, requires_grad=True)
b = torch.tensor([0.0, 0.0], dtype=dtype, device=device, requires_grad=True)
t = torch.tensor([1.0, 0.0], dtype=dtype, device=device)

def forward(xv, kv, bv):
    z = F.conv1d(xv, kv, bv, padding=1)      # (1,2,4)
    a = F.relu(z)                            # (1,2,4)
    h = a.mean(dim=2)                        # (1,2)
    return z, a, h

def loss_fn(xv, kv, bv):
    _, _, h = forward(xv, kv, bv)
    u = F.normalize(h.squeeze(0), dim=0)     # (2,)
    return 1.0 - torch.dot(u, t)

Z, A, h = forward(X, K, b)
assert not torch.any(Z == 0), "ReLU-nullpont: a kozponti differencia itt ervenytelen"

loss = loss_fn(X, K, b)
loss.backward()

print("Z=", Z.detach().numpy())
print("h=", h.detach().numpy())
print("loss=", loss.item())
print("gX=", X.grad.detach().numpy())
print("gK=", K.grad.detach().numpy())
print("gb=", b.grad.detach().numpy())

def central_diff(param, grad):
    flat = param.detach().flatten().clone()
    max_abs = 0.0
    for i in range(flat.numel()):
        orig = flat[i].item()
        flat[i] = orig + eps
        if param is K:
            lp = loss_fn(X, flat.view_as(K), b)
        elif param is b:
            lp = loss_fn(X, K, flat.view_as(b))
        else:
            lp = loss_fn(flat.view_as(X), K, b)
        flat[i] = orig - eps
        if param is K:
            lm = loss_fn(X, flat.view_as(K), b)
        elif param is b:
            lm = loss_fn(X, K, flat.view_as(b))
        else:
            lm = loss_fn(flat.view_as(X), K, b)
        flat[i] = orig
        fd = (lp - lm).item() / (2 * eps)
        max_abs = max(max_abs, abs(fd - grad.detach().flatten()[i].item()))
    return max_abs

print("max fd diff K: %.3e" % central_diff(K, K.grad))
print("max fd diff b: %.3e" % central_diff(b, b.grad))
print("max fd diff X: %.3e" % central_diff(X, X.grad))
assert central_diff(K, K.grad) < atol
assert central_diff(b, b.grad) < atol
assert central_diff(X, X.grad) < atol

# A lemma (i)-(vi) teljes kézi backwardja, általános N, C_in, C_out, k, p és lambda mellett.
def manual_backward(xv, kv, bv, tv, lam, pad):
    with torch.no_grad():
        z = F.conv1d(xv, kv, bv, padding=pad)                      # (N,C_out,L_out)
        h = F.relu(z).mean(dim=2)                                  # (N,C_out)
        n_batch, l_out, k = h.shape[0], z.shape[2], kv.shape[2]
        nrm = h.norm(dim=1, keepdim=True)
        u = h / nrm
        g_h = -(tv - (u @ tv)[:, None] * u) / (n_batch * nrm)      # (i), az egyetlen 1/N
        g_z = (g_h[:, :, None] / l_out) * (z > 0)                  # (ii)-(iii)
        x_pad = F.pad(xv, (pad, pad))                              # (N,C_in,L+2p)
        g_b = g_z.sum(dim=(0, 2))                                  # (iv)
        g_k = torch.stack([torch.einsum("noj,nij->oi", g_z, x_pad[:, :, r:r + l_out])
                           for r in range(k)], dim=2) + lam * kv   # (v)
        g_xp = torch.zeros_like(x_pad)
        for r in range(k):                                         # (vi): s = j + r
            g_xp[:, :, r:r + l_out] += torch.einsum("noj,oi->nij", g_z, kv[:, :, r])
        return g_k, g_b, g_xp[:, :, pad:pad + xv.shape[2]]         # padding levágása

def general_loss(xv, kv, bv, tv, lam, pad):
    h = F.relu(F.conv1d(xv, kv, bv, padding=pad)).mean(dim=2)
    u = h / h.norm(dim=1, keepdim=True)
    return (1.0 - u @ tv).mean() + 0.5 * lam * (kv ** 2).sum()

def fd_grad(fun, args, which):
    """Központi differencia az args[which] tenzor minden elemére."""
    base = args[which]
    g = torch.zeros_like(base)
    for i in range(base.numel()):
        plus, minus = base.clone(), base.clone()
        plus.view(-1)[i] += eps
        minus.view(-1)[i] -= eps
        ap, am = list(args), list(args)
        ap[which], am[which] = plus, minus
        g.view(-1)[i] = (fun(*ap) - fun(*am)) / (2 * eps)
    return g

# (a) A 4. szakasz példája (N=1, lambda=0) és két azonos mintája (N=2): a gradiens nem feleződhet.
gK1, gb1, gX1 = manual_backward(X.detach(), K.detach(), b.detach(), t, 0.0, 1)
assert torch.allclose(gK1, K.grad) and torch.allclose(gb1, b.grad) and torch.allclose(gX1, X.grad)
gK2, gb2, gX2 = manual_backward(torch.cat([X.detach()] * 2), K.detach(), b.detach(), t, 0.0, 1)
assert torch.allclose(gK2, gK1) and torch.allclose(gb2, gb1)
assert torch.allclose(gX2, torch.cat([gX1] * 2) / 2)    # mintánként 1/N: a bemeneti gradiens feleződik
print("(a) kezi backward == autograd; N=2 azonos mintaval K, b gradiens valtozatlan")

# (b) Független kontroll: N=3, C_in=2, C_out=3, L=5, k=3, p=1, lambda=0.03, nem kollineáris t és h.
gen = torch.Generator().manual_seed(7)
Xr = torch.randn(3, 2, 5, generator=gen, dtype=dtype) + 1.0
Kr = torch.randn(3, 2, 3, generator=gen, dtype=dtype) * 0.5
br = torch.tensor([0.2, 0.1, 0.3], dtype=dtype)
tr = torch.tensor([0.6, 0.0, 0.8], dtype=dtype)
lam, pad = 0.03, 1
Zr = F.conv1d(Xr, Kr, br, padding=pad)
hr = F.relu(Zr).mean(dim=2)
cos_ht = (hr / hr.norm(dim=1, keepdim=True)) @ tr
print("min|Z| = %.3f, min||h|| = %.3f, cos(h,t) =" % (Zr.abs().min(), hr.norm(dim=1).min()),
      cos_ht.numpy().round(4))
assert Zr.abs().min() > 100 * eps and hr.norm(dim=1).min() > 0      # nincs ReLU-töréspont, h != 0
assert cos_ht.abs().max() < 0.99 and (hr > 0).sum(dim=1).min() >= 2 # nem degenerált irány
args = [Xr.clone().requires_grad_(), Kr.clone().requires_grad_(), br.clone().requires_grad_(), tr, lam, pad]
general_loss(*args).backward()
manual = manual_backward(Xr, Kr, br, tr, lam, pad)                  # (gK, gb, gX)
auto = (args[1].grad, args[2].grad, args[0].grad)
fdiff = (fd_grad(general_loss, [Xr, Kr, br, tr, lam, pad], 1),
         fd_grad(general_loss, [Xr, Kr, br, tr, lam, pad], 2),
         fd_grad(general_loss, [Xr, Kr, br, tr, lam, pad], 0))
for name, m, a, f in zip(("K", "b", "X"), manual, auto, fdiff):
    d_auto, d_fd = (m - a).abs().max().item(), (m - f).abs().max().item()
    print("(b) %s: max|kezi-autograd| = %.1e, max|kezi-fd| = %.1e, max|g| = %.3f"
          % (name, d_auto, d_fd, m.abs().max().item()))
    assert d_auto < 1e-12 and d_fd < atol and m.abs().max() > 1e-3

Futási jegyzőkönyv és értelmezés. A lab-profilban (PyTorch 2.14.1+cpu, float64) a szkript minden assert-en átment. A kimenet a 4. szakasz kézi értékeit reprodukálja: loss =0,0298575=0{,}0298575, gX ≈(−0,014267;0,014267;−0,014267)\approx(-0{,}014267;\ 0{,}014267;\ -0{,}014267), gb ≈(−0,028534;0,057067)\approx(-0{,}028534;\ 0{,}057067). (a) A teljes kézi backward a 4. szakasz példáján egyezik az autograddal; két azonos mintából álló N=2N=2 batch-en a KK és bb gradiense változatlan, tehát nem feleződik, a bemeneti gradiens viszont mintánként a felére esik, mert minden minta XX-e külön változó, és mindegyik 1/N1/N súllyal szerepel. (b) A független kontrollban min|Z|=0,032\min|Z|=0{,}032, minn∥hn∥2=1,923\min_n\|h_n\|_2=1{,}923, cos(hn,t)=0,9113;0,9225;0,9286\cos(h_n,t)=0{,}9113;\ 0{,}9225;\ 0{,}9286; a kézi és az autograd gradiens legnagyobb eltérése 1,2⋅10−161{,}2\cdot10^{-16}, a kézi gradiens és a központi differencia eltérése 6,8⋅10−116{,}8\cdot10^{-11} (KK), 2,9⋅10−112{,}9\cdot10^{-11} (bb) és 4,8⋅10−114{,}8\cdot10^{-11} (XX). A három max fd diff sor az autograd és a központi differencia abszolút eltérése: 6,2⋅10−116{,}2\cdot10^{-11} (KK), 2,1⋅10−112{,}1\cdot10^{-11} (bb) és 1,6⋅10−101{,}6\cdot10^{-10} (XX), mind az atol=10^{-7} korlát alatt.

A kódban a Z==0 ellenőrzés szándékosan megelőzi a numerikus differenciát: a ReLU nullpontjában az autograd által választott 0 meredekség konvenció, a központi differencia viszont az ottani féloldali ugrás miatt nem konvergál a klasszikus deriválthoz. A választott pontban minden ZZ elem ±1\pm1, 33, vagy 11, tehát a vizsgálat sima (l. 10.3. Gradiensellenőrzés és numerikus hibamérce); a (b) részben az assert legalább 100ε100\varepsilon távolságot követel meg a nullától.

40.1.6 6. Feladatok

F1 (fogalmi, nehézség 1). Magyarázza meg, miért nem lineáris a teljes X↦ℒX\mapsto\mathcal L leképezés, és nevezze meg a két pontot, ahol a linearitás elromlik. Adjon konkrét számpéldát: a 4. szakasz XX-ére és 2X2X-ére mi a ℒ\mathcal L érték, és miért cáfolja ez a linearitást? Elvárt átadandó: legfeljebb fél oldal indoklás a két ponttal és a két veszteségértékkel.

F2 (levezetési, nehézség 2). A 4. szakasz numerikus példáján, λ=0\lambda=0 mellett vezesse le a ∇bℒ\nabla_b\mathcal L és ∇Xℒ\nabla_X\mathcal L értékét a lemma (iv) és (vi) képletéből. Adja meg az egzakt törteket és a közelítő tizedes értékeket öt jegyre, majd ellenőrizze az alakokat: ∇b∈ℝ2\nabla_b\in\mathbb R^2, ∇X∈ℝ1×1×3\nabla_X\in\mathbb R^{1\times1\times3}. Elvárt átadandó: a levezetés minden közbülső összeggel.

F3 (implementációs, nehézség 2). Írjon NumPy függvényt, amely a lemmát a N=1N=1, Cin=1C_{in}=1, Cout=2C_{out}=2, k=2k=2, p=1p=1, L=4L=4, λ=0\lambda=0 esetre implementálja: a forward, a ∇h\nabla_h, a ∇Z\nabla_Z, ∇K\nabla_K, ∇b\nabla_b, ∇X\nabla_X kiszámítását. Válasszon olyan bemenetet, amelyen egyetlen ZZ elem sem nulla, és hh nem párhuzamos tt-vel. Hasonlítsa össze a PyTorch autograd gradienseivel azonos bemeneten, 10−1010^{-10} abszolút toleranciával. Degenerációs részfeladat: mutassa meg, hogy Cout=1C_{out}=1 és h>0h>0 esetén u=1u=1, a veszteség lokálisan konstans, és minden gradiens nulla; magyarázza meg, miért nem ellenőrzi ez az eset az implementációt. Elvárt átadandó: futtatható kód, a teszteset és a toleranciaellenőrzés kimenete, valamint a degenerációs eset indoklása.

F4 (hibakeresési, nehézség 2). Az alábbi hibás gradiensszámítás a normalizálási lépést egyszerűsíti:

def wrong_grad_h(h, t):
    return -t / np.linalg.norm(h)

Magyarázza meg, melyik komponensben és miért tér el ez a lemma (i) képletétől, számítsa ki a hibás és a helyes ∇hℒ\nabla_h\mathcal L-t a 4. szakasz adataira, és mutassa meg, hogy a központi differencia a helyes értéket erősíti meg. Elvárt átadandó: ok, következmény, javított képlet és a numerikus összevetés.

F5 (kísérleti/transzfer, nehézség 3). Tervezzen kontrollált kísérletet annak eldöntésére, hogy a GlyphLab 38.4. Mentett modellből független inference checkpointjának embeddingjein az L2-normalizálás javítja-e a jelenetszintű euklideszi retrieval mAP@5-öt a nyers embeddinghez képest. Ne futtassa; adja meg a hipotézist, a kontrollt, a változó tényezőt, a metrikát, a jelenet-alapú splitet és bootstrap egységet, valamint a következtetési korlátokat. Elvárt átadandó: preregisztrált terv a 36.1. Állításból kísérleti terv sablonja szerint.

40.1.7 7. Megoldások és indoklás

F1 megoldás. Egy FF leképezés akkor lineáris, ha F(αx+βy)=αF(x)+βF(y)F(\alpha x+\beta y)=\alpha F(x)+\beta F(y) minden α,β\alpha,\beta és x,yx,y esetén. Két töréspont van. (1) A ReLU: ReLU(−1)=0\operatorname{ReLU}(-1)=0 és ReLU(1)=1\operatorname{ReLU}(1)=1, de ReLU(−1+1)=ReLU(0)=0≠1\operatorname{ReLU}(-1+1)=\operatorname{ReLU}(0)=0\ne1, tehát a szuperpozíció elromlik. (2) Az L2-normalizálás skálainvariáns: f(γh)=f(h)f(\gamma h)=f(h) minden γ>0\gamma>0-ra, ezért X↦uX↦u nem lehet lineáris. A 4. szakaszban ℒ(X)=1−4/17≈0.029858\mathcal L(X)=1-4/\sqrt{17}\approx0.029858. A 2X=(2,4,2)2X=(2,4,2) bemenetre a pozitív ágban ZZ is duplázódik, ezért h=(4,1)⊤h=(4,1)^\top, ami ugyanazt az uu-t adja: ℒ(2X)=0.029858\mathcal L(2X)=0.029858. Ha a leképezés lineáris lenne, ℒ(2X)=2ℒ(X)≠0\mathcal L(2X)=2\mathcal L(X)\ne0 teljesülne; a valós egyenlőség cáfolja a linearitást.

F2 megoldás. A ∇Zℒ\nabla_Z\mathcal L a 4. szakaszban adott. A biasgradiens az oszloponkénti összeg: ∇b1=−4⋅13417=−21717\nabla_{b_1}=-4\cdot\frac{1}{34\sqrt{17}}=-\frac{2}{17\sqrt{17}}; ∇b2=2⋅21717=41717\nabla_{b_2}=2\cdot\frac{2}{17\sqrt{17}}=\frac{4}{17\sqrt{17}}, tehát ∇b=(−0.02853,0.05707)⊤\nabla_b=(-0.02853,0.05707)^\top. A bemeneti gradienshez a párnázott összeget számoljuk az s=j+rs=j+r párokkal. A KK két kernelsorát és a ∇Z\nabla_Z két sorát használva:

s=0:−13417,s=1:−11717,s=2:11717,s=3:−11717,s=4:−53417, s=0:\ -\frac{1}{34\sqrt{17}},\quad s=1:\ -\frac{1}{17\sqrt{17}},\quad s=2:\ \frac{1}{17\sqrt{17}},\quad s=3:\ -\frac{1}{17\sqrt{17}},\quad s=4:\ -\frac{5}{34\sqrt{17}},

ahol például s=2s=2-re a j=1,r=1j=1,r=1 tag −13417⋅1-\frac{1}{34\sqrt{17}}\cdot1 és a j=2,r=0j=2,r=0 tag −13417⋅1+21717⋅1-\frac{1}{34\sqrt{17}}\cdot1+\frac{2}{17\sqrt{17}}\cdot1 összege 3a−a=2a3a-a=2a, ahol a=1/(3417)a=1/(34\sqrt{17}). A padding levágása (s=ℓ+1s=\ell+1) a fenti ∇X\nabla_X hármast adja. Az alakok ellenőrzése: ∇b\nabla_b két koordináta, ∇X\nabla_X egy csatorna, három térbeli index, konzisztens a lemma (iv) és (vi) képletével.

F3 megoldás. A lemmát a konkrét 1D esetre megvalósító NumPy kód:

import numpy as np
import torch
import torch.nn.functional as F

def manual_1d(X, K, b, t):
    # X: (1,1,L); K: (C_out,1,2); b: (C_out,); t: (C_out,), ||t||=1; p=1, N=1, lambda=0
    L, C = X.shape[2], K.shape[0]
    Xp = np.concatenate([np.zeros(1), X[0, 0], np.zeros(1)])   # p=1
    Lout = L + 2 - 2 + 1
    Z = np.array([[b[o] + np.sum(K[o, 0] * Xp[j:j + 2]) for j in range(Lout)] for o in range(C)])
    assert np.all(Z != 0), "ReLU-nullpont"
    h = np.maximum(Z, 0.0).mean(axis=1)                        # (C_out,)
    hnorm = np.linalg.norm(h)
    assert hnorm > 0, "||h|| = 0: a lemma nem alkalmazható"
    u = h / hnorm
    loss = 1.0 - u @ t
    gh = -(t - (u @ t) * u) / hnorm                            # (i), N=1
    gZ = (gh[:, None] / Lout) * (Z > 0)                        # (ii)-(iii)
    gb = gZ.sum(axis=1)                                        # (iv)
    gK = np.array([[[np.sum(gZ[o] * Xp[r:r + Lout]) for r in range(2)]] for o in range(C)])  # (v)
    gXp = np.zeros(L + 2)
    for s in range(L + 2):                                     # (vi)
        for o in range(C):
            for r in range(2):
                if 0 <= s - r < Lout:
                    gXp[s] += gZ[o, s - r] * K[o, 0, r]
    return loss, gXp[1:1 + L][None, None, :], gK, gb

def torch_ref(X, K, b, t):
    Xt, Kt, bt = (torch.tensor(a, requires_grad=True) for a in (X, K, b))
    h = F.relu(F.conv1d(Xt, Kt, bt, padding=1)).mean(dim=2)[0]
    loss = 1.0 - F.normalize(h, dim=0) @ torch.tensor(t)
    loss.backward()
    return loss.item(), Xt.grad.numpy(), Kt.grad.numpy(), bt.grad.numpy()

X = np.array([[[1.0, 2.0, -1.0, 3.0]]])
K = np.array([[[1.0, 0.5]], [[-0.5, 1.0]]])
b = np.array([0.1, 0.2])
t = np.array([0.6, 0.8])
for name, m, a in zip(("loss", "gX", "gK", "gb"), manual_1d(X, K, b, t), torch_ref(X, K, b, t)):
    print(name, "max abs diff = %.1e" % np.max(np.abs(np.asarray(m) - np.asarray(a))))
    assert np.allclose(m, a, rtol=0, atol=1e-10)
gX = manual_1d(X, K, b, t)[1]
assert np.abs(gX).max() > 1e-3                                # nem nulla: a teszt nem triviális

# Degenerációs eset: C_out = 1, h > 0  =>  u = 1, a veszteség lokálisan konstans, minden gradiens 0.
_, gX1, gK1, gb1 = manual_1d(X, K[:1], b[:1], np.array([1.0]))
print("C_out=1: max|grad| =", max(np.abs(g).max() for g in (gX1, gK1, gb1)))

A választott bemeneten Z1=(0,6;2,1;1,6;0,6;3,1)Z_1=(0{,}6;\ 2{,}1;\ 1{,}6;\ 0{,}6;\ 3{,}1), Z2=(1,2;1,7;−1,8;3,7;−1,3)Z_2=(1{,}2;\ 1{,}7;\ -1{,}8;\ 3{,}7;\ -1{,}3), tehát nincs ReLU-nullpont, a második csatornában az aktív halmaz nem teljes; h=(1,6;1,32)h=(1{,}6;\ 1{,}32) és cos(h,t)=0,9719\cos(h,t)=0{,}9719, azaz hh nem párhuzamos tt-vel. A lab-profilban (PyTorch 2.14.1+cpu, float64) a kézi és az autograd érték legnagyobb eltérése 00 (loss), 1,9⋅10−171{,}9\cdot10^{-17} (∇X\nabla_X), 5,6⋅10−175{,}6\cdot10^{-17} (∇K\nabla_K) és 4,2⋅10−174{,}2\cdot10^{-17} (∇b\nabla_b), és max|∇X|=0,0304\max|\nabla_X|=0{,}0304, tehát a teszt nem a nullgradienst hasonlítja össze. A degenerációs esetben Cout=1C_{out}=1, h>0h>0, ezért u=h/|h|=1u=h/|h|=1 minden kis perturbációra, ℒ=1−t=0\mathcal L=1-t=0 konstans, és a lemma (i) szerint ∇h=−(t−(ut)u)/|h|=0\nabla_h=-(t-(ut)u)/|h|=0; a futás is max|∇|=0\max|\nabla|=0-t ad. Egy ilyen teszt bármely, nullát visszaadó hibás implementációt is elfogadna, ezért ellenőrzésre nem alkalmas.

F4 megoldás. A hiba oka: a wrong_grad_h kihagyja a normalizálás Jacobi-mátrixának uu⊤uu^\top vetületi tagját, mintha a célgradiens közvetlenül a normalizálatlan hh-ra hatna. Következmény: a céllal merőleges irányú komponens gradiense eltűnik. A 4. szakaszban a helyes ∇hℒ=(−21717,81717)\nabla_h\mathcal L=(-\frac{2}{17\sqrt{17}},\frac{8}{17\sqrt{17}}), a hibás pedig −t∥h∥2=(−217,0)-\frac{t}{\|h\|_2}=(-\frac{2}{\sqrt{17}},0). Az első komponens közel 17-szeres hibája mellett a második komponens nulla, miközben a központi differencia a h2h_2 perturbációjára 0.114130.11413 értéket ad. A javítás a lemma (i) teljes képlete: ∇h=−1∥h∥(t−(u⊤t)u)\nabla_h=-\frac{1}{\|h\|}(t-(u^\top t)u); ezzel mindkét komponens egyezik a numerikus kontrollal.

F5 megoldásminta. Hipotézis: a GlyphLab test-szettjén a normalizált embedding euklideszi retrieval mAP@5-e magasabb, mint a nyers embeddingé. Adat és egység: test 60 kép, 30 scene, minden scenenek két view-ja; a független bootstrap egység a scene_id. Változó tényező: L2-normalizálás alkalmazása a kinyert embeddingre; a modell súlyai rögzítettek, nincs újratanítás. Metrika: mindkét irányú view-query/gallery átlagolt mAP@5; a per-scene bootstrap CI a 36.2. Ismétlések és eredménybizonytalanság szerint. Döntési szabály: a normalizált mAP CI-ja a nyers CI fölött van. Korlátok: a GlyphLab toy adat, nem természetes képi benchmark; az eredmény csak ehhez az adatszerződéshez és ehhez a checkpointhoz kötött; a 28.1. Embedding, távolság és normalizálás azonossága miatt a koszinusz-rangsor önmagában nem változik, ezért a metrikának kifejezetten euklideszi távolságot kell használnia.

40.1.8 8. Tipikus tévedés, önellenőrzés és visszahívás

Tipikus tévedés. A memorizált API-hívás nem helyettesíti az önálló levezetést. Ha a hallgató a F.normalize hívása után a célgradienst változatlanul továbbítja, azaz ∇hℒ=−t\nabla_h\mathcal L=-t-t ír, a 4. szakaszban ∇h=(−1,0)⊤\nabla_h=(-1,0)^\top-t kapna a helyes (−0.02853,0.11413)⊤(-0.02853,0.11413)^\top helyett. Az első komponens hibája körülbelül 35-szörös, a második komponens pedig teljesen hiányzik. A központi differencia azonnal kimutatja az eltérést; ezért a vizsgán a levezetés és a numerikus kontroll együtt kötelező, nem felcserélhető.

Önellenőrzés. Minden levezetési lépésben: (a) a tenzoralakok egyeznek-e a lemma kimeneti alakjaival; (b) a pp, LoutL_{out} peremfeltételek teljesülnek-e; (c) a ReLU nullpontot elkerültük-e; (d) a λK\lambda K tag csak a súlygradiensbe került-e; (e) a központi differencia a megadott 10−610^{-6} lépésközzel és 10−710^{-7} toleranciával egyezik-e. A nullvektor normalizálása a lemmán kívül eső, nem differenciálható eset; a gyakorlatban a GAP utáni nulla csatornavektort külön esetként kell kezelni.

Visszahívás. A 28.1. Embedding, távolság és normalizálás levezetése szerint egységnormájú vektorokra a koszinusz-hasonlóság és a négyzetes euklideszi távolság rendezése ekvivalens; ezért a jelen feladat cosinus-vesztesége és az euklideszi retrieval a normalizálás után ugyanazt a geometriát használja. A 36.1. Állításból kísérleti terv tanulsága, hogy a kísérleti hipotézist a futás előtt kell rögzíteni; az F5 terv éppen ezt a preregisztrációt gyakorolja. A 39.1. Feladat- és annotációs döntés annotációs konvenciója pedig arra emlékeztet, hogy a cél alakja és az üres eset definíciója a feladat szerves része: itt a target tt egységnormájú vektor, és a ∥h∥=0\|h\|=0 eset kizárása ugyanolyan szerződéses feltétel, mint a bbox üres célja.

40.1.9 Rövid összefoglaló

A vizsga egy új gráfot vezetett le: 1D keresztkorreláció, ReLU, globális átlag-pooling, L2-normalizálás, cosinus-veszteség és L2-súlyregularizálás kompozícióját. Az 1. lemma rögzíti a peremfeltételeket, és lépésenként igazolja a nemlineáris, pooling- és konvolúciós VJP-ket. Az ellenpélda a nulla normájú esetben mutatja a differenciálhatóság határát. A kézi példa egzakt törtekkel, a labor float64 központi differenciával ellenőriz. Az öt feladat a fogalmi cáfolattól a preregisztrált kísérleti tervig terjed, és a memorizált API-hívás helyett a levezetés és a numerikus kontroll együttesét kéri számon.

40.1.10 Következő alkalmazás

A 40.2. Kísérleti és hibakeresési vizsga egység a kísérleti és hibakeresési vizsgát rendezi csomaggá: egy rögzített modellen és adaton kell diagnózist adni, hibás gradiens- vagy adatfolyamot lokalizálni, és a javítást kontrollált újrafuttatással igazolni.

40.1.11 Felhasznált források