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:
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?
Legyen a vizsgált paramétervektor, és legyen a veszteségfüggvény. Jelölje a -edik egységvektort, pedig a perturbációt. A centrális differencia a -edik koordináta mentén:
A forward differencia ettől csak egyoldali:
A kettő nem ugyanaz: az egyoldali változat hibája , a centrálisé , 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 pontban az egyezés lehet véletlen vagy szimmetriahiba következménye; 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:
ahol
az analitikus,
a numerikus gradiens egy koordinátája,
pedig abszolút védőszint (például
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 legalább kétszer folytonosan differenciálható a vizsgált pont egy környezetében. Az affin konvolúciós rétegre épülő veszteség a paraméterekben polinomiális, tehát ; 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ó
diszkretizációs hibából és a lebegőpontos kerekítésből származó
zajból, ahol
a gépi epszilon. float32-ben
,
így
körül is már a kerekítési zaj dominálhat; float64-ben
,
ezért
biztonságos egyensúlyt ad.
A tesztelt művelet pontos részhalmaza a következő. Legyen a bemeneti batch , a kernel , a bias . A forward a [D10] szerinti cross-correlation:
ahol a stride, a szimmetrikus zero padding, a dilation. A csatornacsoportok értéke ; 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
ezért a kimeneti gradiens egyszerűen ; 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:
ahol a valid, 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 a pont egy sugarú környezetében, akkor minden koordinátára
valamely közbülső ponttal, így a diszkretizációs tag , szemben a forward differencia tagjával. A kerekítési tag alsó korlátja .
Levezetés. A Taylor-formula a harmadik taggal:
A kivonás után a és az tag kiesik, és
ahol az utolsó lépés a folytonosságból következő közbülsőérték-tétel. A hibatag tehát . A lebegőpontos kerekítésből származó tag abból következik, hogy a két -kiértékelés külön-külön abszolút hibájú, és ezt -nal osztjuk.
Ennek a numerikus kontrollnak a bizonyítási státusza
korlátozott: az egyezés véges sok rácspontban, véges
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.
Legyen , , , stride , padding , dilation . Bemenet és kernel:
A forward :
A veszteség . Az input gradiens analitikus képlete full cross-correlation az eredeti kernelrel:
Ellenőrzés centrális differenciával mellett, perturbálásával:
A két érték az adott pontosság mellett pontosan egyezik. Ugyanezt a súlygradiensre is elvégezhetjük: , és a centrális differencia perturbálásával ugyancsak -ö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 -öt adna, ami hibás a cross-correlation főúton.
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
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.
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 -t és -t, ahol a tiszta képlet nullához közeli gradiensnél félrevezető értéket adna.
F2 (levezetési, nehézség 2). Legyen , , . Számítsa ki a forward és a centrális differencia értékét, és hasonlítsa mindkettőt a pontos értékhez. Melyik hibája arányos -nal, és melyiké -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 veszteségre a fenti bemenet és kernel esetén kiszámítja a centrális differenciát az koordinátára, és ellenőrzi, hogy az eredmény 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,
,
és a háló egy ReLU-t is tartalmaz, amelynek pre-aktivációja
a vizsgált pontban pontosan
.
Az ellenőrzés relatív hibája
.
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 , dilation és padding mellett fut, , , , bemenet , . Határozza meg: hány rácspontot választ, mekkora -t, milyen toleranciát, és milyen korlátok miatt nem tekinthető az egyezés teljes bizonyításnak.
F1. A forward differencia egyoldali:
;
hibája
.
A centrális kétoldali:
;
a szimmetria miatt a másodrendű tag kiesik, hibája
.
A stabil nevező azért kell, mert ha
és
,
akkor a tiszta relatív hiba
(100%), noha az abszolút eltérés
,
ami float64-ben zajszint. Például
-nél
közelében ez előfordul; a
védőszint megakadályozza a hamis riasztást.
F2. Pontos derivált: . Forward: ; hiba , mert a Taylor-maradék . Centrális: ; hiba , mert a maradék . 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
választás miatt a kerekítési zaj
nagyságrendű, így a
tűrés biztonságos. Alakellenőrzés: a gradiens skalár, a bemenet
,
a kimenet
,
minden összegzés határa a kerneldimenziókból következik.
F4. Hibaforrások: (1) float32-ben a
kerekítési zaj
relatív hibát okoz, ami a megfigyelt érték nagyságrendjébe esik; javítás
float64-re és
-ra.
(2) A forward differencia
hibája
-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
nagyságrenddel távolabb van. A
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,
,
tolerancia
relatív hiba stabil nevezővel. A rácspontok száma indoklás: a
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
,
alakra érvényes; más batchméretre a biasgradiens összegzési hibája nem
jelenik meg; a véges
miatt a
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.
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 é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
példa kimutatja: az eredeti kernel ad
-öt,
a flipped
-ö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.
A centrális differencia másodrendű Taylor-maradékát levezettük, és
megmutattuk, hogy a diszkretizációs hiba
,
szemben a forward differencia
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
é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.
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.