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 inputra, súlyra és biasra. Induló kérdés: ha , , , , akkor a belső 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 jelölés itt mindig a skalár loss szerinti gradiens, nem egy közvetlen mátrixderivált: , , .
Legyen a stride , a szimmetrikus padding , a dilation ; mind pozitív egész, kivéve . Az output alakú, ahol és a szélességre ugyanígy, -vel. A forward egyetlen indexképlete a zérussal kiegészített tenzoron: alakja , és ha és , egyébként . Ezzel ahol , , , .
Definíció (upstream gradiens). Legyen differenciálható skalár veszteség, és legyen azaz . A projekt kézi konvenciója , ekkor ; 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 |
|---|---|---|
Tétel (a konvolúciós forward minden VJP-je). Az (1) forwardra, differenciálható skalár mellett:
Biasgradiens, minden -re:
Súlygradiens, minden indexre: Az eredeti -szel ez azon pozíciókra értendő, ahol és , és ott az integrandus ; a padded írásmód a határon kívüli pozíciókat zérussal kezeli.
Inputgradiens a zero-padded tartományon, minden -re: ahol az utolsó összeg azon , párokra megy, amelyek a megadott afin egyenletet teljesítik. Végül a padding levágása:
Bizonyítás. A csak az kimenetekben jelenik meg, mindegyikben együtthatóval, ezért a skalár láncszabály közvetlenül adja (2)-t.
A szintén csak az kimeneti csatorna értékeiben szerepel, de ott minden pozícióban, együtthatóval. Ezért és a parciális derivált éppen a megfelelő -é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 elem azokban az kimenetekben szerepel, amelyekre létezik , hogy és ; ezekben a parciális derivált . 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 eltolt másolata, tehát a megfelelő Jacobi-rész az identitás; a padding tartománybeli gradiens nem tartozik egyetlen -elemhez sem.
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 -súlyokkal súlyozva összegződik. A (3) pedig azt mondja, hogy a súlygradiens a é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.
Legyen , , , , , , , tehát . Az inputok: , . A forward kézzel: A kvadratikus loss miatt .
A (2) képlet szerint , azaz .
A (3) képletet a négy kernelpozícióra alkalmazva mind a négy érték ; ennek egyik tagja például a -hoz az képből a pozíció hozzájárulása , a pozícióé ; a teljes összeg mindkét képen és mind a négy eltolásban e példa szimmetriája miatt . Tehát
Az inputgradiens (4)–(5) szerint scatter-add módon: inicializálunk egy zérus tenzort, és minden esetén hozzáadunk -t a cellához. A belső elemre például két tag esik: és , összegük . A teljes eredmény: A jobb alsó, értékű cellák a 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.
Legyen az 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 Mivel a forward -ban affin, az 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 -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, mellett a relatív hiba tipikusan alá esik; ezért elfogadási korlátnak , bőven elég. A relatív hibát stabil nevezővel számoljuk: 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.
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 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).
F1 (fogalmi, nehézség 1). , , , , , , , . Add meg értékét és a , , , pontos alakját. Mely tengelyek mentén összegzi a biasgradiens a értékeit? Átadandó: az alakok és az összegző tengelyek listája.
F2 (levezetési, nehézség 2). Egydimenziós eset: , , , , , , ; , , , és . Számold ki -t, majd a levezetett képletekkel , , é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
és
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 ittHol 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.
F1 megoldás. tehát . Az alakok: , , , . A biasgradiens a , azaz a , , tengely szerinti összegzéséből áll, a kimeneti csatorna tengelyt megtartva.
F2 megoldás. A forward (1) szerint , tehát . A kvadratikus loss miatt . A (2)–(5) egydimenziós változata: tehát . Az átfedő pozíciók: (a és tagok), . A és 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])) # TrueA slice(None) ág azért kell, mert Pythonban
-0 == 0, ezért a naiv t[:, :, p:-p] a
esetben t[:, :, 0:0] üres tenzort adna. A megoldás
szélsőeset-kontrollja:
shape-azonosság,
é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
végül a legutolsó
tag,
lesz a helyes
helyett,
pedig
a helyes
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,
,
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.
,
,
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.
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
,
,
,
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
-nál
tökéletesen egyezik, de
-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
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.
Önellenőrzéshez: (i) mindhárom gradiens alakja megegyezik a megfelelő objektum alakjával; (ii) mellett a padded és a vágott tenzor azonos; (iii) a F2 1D példában a összege , ami a láncszabály szerint , 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 alakjától függ.
A konvolúciós forward (1) lineáris tagokból áll, ezért mindhárom VJP
véges összeg. A biasgradiens a
batch- és térbeli összege; a súlygradiens a
és az input keresztkorrelációja; az inputgradiens a
-t
a
-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
ü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.
https://docs.pytorch.org/docs/2.14/notes/autograd.html.https://docs.pytorch.org/docs/2.14/generated/torch.nn.Conv2d.html.https://cs231n.github.io/convolutional-networks/.