Ebben az egységben a konvolúciós réteg bemenet szerinti gradiensét, a tenzort vezetjük le, és megvalósítását a scatter-add művelettel ellenőrizzük. A 15.1. Súly- és biasgradiens egységben a kernel- és biasgradiens képletét már levezettük; most az ellenkező irányú kérdést tesszük fel. Induló becslésként gondoljuk végig egy -as kernel és stride esetét: egy belső input pixel hány kimeneti elem kiszámításában vesz részt? A válasz tipikusan darab, tehát a visszafelé terjedő gradiensből ugyanennyi hozzájárulást kell összegeznünk. Ha ezeket felülírjuk egymást, az inputgradiens hibás lesz.
Előismeret: a 14.2. Több bemeneti és kimeneti csatorna alfejezetben bevezetett többcsatornás forward definíció, a 15.1. Súly- és biasgradiens alfejezetben használt keresztkorrelációs indexelés, valamint a 3.2. Mátrixszorzás és affín réteg alfejezetből a mátrixszorzás inputgradiensének tranzponált szerkezete.
Legyen a bemenet
NCHW alakban, a kernel
,
a bias
.
A konvolúciós réteg kimenete
,
ahol a PyTorch Conv2d keresztkorrelációt számol [D10]. A
veszteség
szerinti gradiense legyen
A cél a
tensor kiszámítása. A forwardban minden kimeneti elem a bemenet egy -s patch-éből készül; a backwardban ezért minden elem a megfelelő patch minden pozíciójához hozzájárul a kernel súllyal szorozva. Mivel a szomszédos kimeneti elemek patch-ei átfednek, ugyanazon input pixel több hozzájárulást kap. A helyes művelet tehát scatter-add: a célkoordinátához tartozó gradienshez hozzáadunk, nem felülírunk.
A stride, padding és dilation szerepe a célkoordináta meghatározásában:
ahol a kimeneti térbeli index, a bemeneti térbeli index, a stride, a padding, a dilation. Csak a és tartományba eső pozíciók kapnak gradienst; a paddingelt virtuális pixelekhez tartozó hozzájárulások kimaradnak.
A következő lemmában a teljes inputgradiens képletet mondjuk ki. A bizonyítás a forward definícióból és a láncszabályból áll, ezért a feltételeket expliciten megadjuk.
Lemma (a konvolúciós réteg inputgradiense). Legyen a 14.2. Több bemeneti és kimeneti csatorna szerinti keresztkorreláció pozitív egészekkel, egész paddinggel, egész stride-dal és egész dilationnel úgy, hogy a kimeneti méretek
pozitívak. Ekkor minden , és esetén
A belső összegzés pontosan azokra az párokra terjed ki, amelyekre az
értékek egészek és a kimeneti tartományba esnek. A többi eleme definíció szerint nulla; a paddingelt virtuális pixelekhez nincs -elem.
Bizonyítás. A forward definícióból, a paddingelt tenzorral:
A láncszabály szerint
Mivel a bias -től független, csak a keresztkorrelációs tag deriváltja marad. A parciális derivált:
feltéve, hogy és ; ha a célkoordináta a paddingre mutat, a derivált nulla. Ezt behelyettesítve és a véges összegeket átrendezve a lemma képlete adódik. A véges összegek felcserélhetősége miatt az átrendezés indokolt.
Következmény (scatter-add megvalósítás). A lemma nem az bemeneti indexekre szervezett összegzést adja, hanem megfordítva: minden elemhez és minden kernelpozícióhoz kiszámítjuk a célkoordinátát ; ha az érvényes bemeneti tartományba esik, akkor
minden
-re.
A += művelet az összeadás, nem a felülírás. Az átfedő
patch-ek miatt ugyanazon
cél több forrásból is kap hozzájárulást, ezért az accumulation a helyes
szemantika.
A stride hatása az indexfeltételben látszik: ha nem osztható -sel, akkor az adott párhoz nincs . Például , , , esetén a pozíciók semmilyen kernelpozícióval sem érhetők el, így azok -e nulla marad, feltéve, hogy a kernel súlya nem nulla.
Legyen , , , , , , . A kernel legyen diagonális:
Az upstream gradients tenzor is diagonális:
A lemma képletéből , , miatt , . A diagonális kernel esetén ad nemnulla hozzájárulást, így
ahol a tenzor kiterjesztése nullákkal a tartományon kívül. A diagonális elemekre:
A többi elem nulla, mert a off-diagonális elemei nullák. Tehát
Megfigyelhető, hogy a középső pixel több kimeneti gradiensből is részesül: például a pixel a , és elemekből kap hozzájárulást a kernel megfelelő súlyával.
Az alábbi labor a lemmát több stride/padding/dilation konfiguráció
mellett ellenőrzi. A referenciakód a scatter-add-öt a fenti következmény
szerint valósítja meg; a PyTorch oldalon az autograd
számítja a
-et.
A kimenet az eltérések maximuma.
# lab/n058_dx_check.py
import numpy as np
import torch
import torch.nn.functional as F
rng = np.random.default_rng(20261020)
def conv2d_output_shape(H, W, kh, kw, stride, padding, dilation):
Hout = (H + 2 * padding - dilation * (kh - 1) - 1) // stride + 1
Wout = (W + 2 * padding - dilation * (kw - 1) - 1) // stride + 1
return Hout, Wout
def conv2d_input_grad_numpy(D, K, H, W, stride, padding, dilation):
# D: (N, Cout, Hout, Wout), K: (Cout, Cin, kh, kw)
N, Cout, Hout, Wout = D.shape
Cout2, Cin, kh, kw = K.shape
assert Cout2 == Cout
dX = np.zeros((N, Cin, H, W), dtype=D.dtype)
for n in range(N):
for o in range(Cout):
for i in range(Hout):
for j in range(Wout):
du = D[n, o, i, j]
for c in range(Cin):
for a in range(kh):
for b in range(kw):
u = i * stride + a * dilation - padding
v = j * stride + b * dilation - padding
if 0 <= u < H and 0 <= v < W:
dX[n, c, u, v] += du * K[o, c, a, b]
return dX
N, Cin, Cout, H, W = 2, 3, 4, 6, 7
kh, kw = 3, 3
for stride in (1, 2):
for padding in (0, 1):
for dilation in (1, 2):
Hout, Wout = conv2d_output_shape(H, W, kh, kw, stride, padding, dilation)
if Hout <= 0 or Wout <= 0:
continue
X_np = rng.standard_normal((N, Cin, H, W)).astype(np.float64)
K_np = rng.standard_normal((Cout, Cin, kh, kw)).astype(np.float64)
b_np = rng.standard_normal(Cout).astype(np.float64)
D_np = rng.standard_normal((N, Cout, Hout, Wout)).astype(np.float64)
dX_np = conv2d_input_grad_numpy(D_np, K_np, H, W, stride, padding, dilation)
X_t = torch.tensor(X_np, dtype=torch.float64, requires_grad=True)
K_t = torch.tensor(K_np, dtype=torch.float64, requires_grad=True)
b_t = torch.tensor(b_np, dtype=torch.float64, requires_grad=True)
Y_t = F.conv2d(X_t, K_t, b_t, stride=stride, padding=padding, dilation=dilation)
D_t = torch.tensor(D_np, dtype=torch.float64)
loss = (Y_t * D_t).sum()
loss.backward()
dX_t = X_t.grad
diff = np.max(np.abs(dX_np - dX_t.detach().numpy()))
print(f"s={stride} p={padding} d={dilation} "
f"Hout,Wout={Hout},{Wout} max_abs_diff={diff:.3e}")A futtatás parancsa a lab munkakönyvtárból:
python n058_dx_check.pyA kimenet számított elvárás, nem a kéziratban elvégzett futás: minden
konfigurációra a max_abs_diff értékének
alatt kell lennie f64 CPU aritmetikával. A
loss = (Y_t * D_t).sum() használata a 15.1. Súly- és biasgradiens alfejezetben bevezetett
ellenőrző konvenció: a skalár loss gradiense a megadott upstream tenzor,
ezért nincs szükség tényleges veszteségfüggvényre.
F1 (fogalmi, nehézség 1). Magyarázza el, miért nem
elegendő a
kiszámításakor az értékadás (=), és miért kell
scatter-addöt (+=) használni. Adjon egy konkrét 1D
ellenpéldát:
,
,
,
,
,
.
Számítsa ki a helyes
második elemét, és a hibás felülíró implementáció által adott értéket,
ha az utolsó kernelpozíció ír utoljára. Átadandó: szöveges indoklás és a
két számérték.
F2 (levezetési, nehézség 2). Tekintse a , , , , , esetet. Határozza meg a lemmából, mely bemeneti magasságindexekre lehet nemnulla, és minden ilyen -ra adja meg az összes hozzájáruló párt. Átadandó: a értéke, a lehetséges -k halmaza, és a hozzájárulás-táblázat.
F3 (implementációs, nehézség 1). Írjon 1D NumPy
referenciát a
kiszámítására a lemmából, majd ellenőrizze PyTorch F.conv1d
autograddal a
,
,
,
,
,
,
,
konfigurációra. Átadandó: teljes kód, seed, dtype, parancs, és a kapott
max_abs_diff.
F4 (hibakeresési, nehézség 2). Az alábbi kódrészlet a scatter-add után a padding levágását próbálja elvégezni:
# HIBA: p=0 esetén a 0:-0 szelet üres.
dX = full[:, :, padding:-padding, padding:-padding]Lokalizálja a hibát esetén: miért üres a szelet, mi a következménye a alakjára és a numerikus összehasonlításra? Javítsa a kódot, és adja meg az ellenőrző teszt várt kimenetét. Átadandó: hiba magyarázata, javított kód, és a javítás utáni shape-ellenőrzés.
F5 (kísérleti/transzfer, nehézség 3). Tervezzen kísérletet annak mérésére, hogy a Python ciklusos scatter-add implementáció futásideje és memóriaigénye hogyan skálázódik a batchmérettel () kis CPU-n, rögzített , , , , mellett. Nem kell ténylegesen futtatnia. Átadandó: hipotézis, mért mennyiségek, mérési protokoll, baseline, és az érvényességi korlát.
F1 megoldás. A forwardban az bemeneti pixel három kimeneti elemhez járul hozzá: -hoz a kernelpozícióval, -hez a pozícióval, és -höz a pozícióval. A megfelelő gradiens-hozzájárulások:
A helyes . A felülíró implementáció, ha a kernelpozíciók ciklusában az utolsó, pár ír a célba, -at eredményez, tehát a három hozzájárulásból kettőt elveszít. Az átfedő patch-ek miatt a gradiens-hozzájárulásokat mindig összegezni kell.
F2 megoldás. A kimeneti méret:
A lehetséges értékek , mellett:
| érvényes? | |||
|---|---|---|---|
| 0 | 0 | nem | |
| 0 | 1 | igen | |
| 0 | 2 | igen | |
| 1 | 0 | igen | |
| 1 | 1 | igen | |
| 1 | 2 | nem |
Tehát a lehetséges -k halmaza . Az ponthoz az és párok, az ponthoz az és párok járulnak hozzá. A magasságindexekre a feltétel nem teljesíthető, ezért azok -e nulla.
F3 megoldás. Az 1D referencia és az ellenőrző kód:
# lab/n058_f3_1d.py
import numpy as np
import torch
import torch.nn.functional as F
rng = np.random.default_rng(20261020)
def conv1d_input_grad_numpy(D, K, H, stride, padding, dilation):
N, Cout, Hout = D.shape
Cout2, Cin, kh = K.shape
assert Cout2 == Cout
dX = np.zeros((N, Cin, H), dtype=D.dtype)
for n in range(N):
for o in range(Cout):
for i in range(Hout):
for c in range(Cin):
for a in range(kh):
u = i * stride + a * dilation - padding
if 0 <= u < H:
dX[n, c, u] += D[n, o, i] * K[o, c, a]
return dX
N, Cin, Cout, H = 1, 2, 2, 5
kh, stride, padding, dilation = 3, 2, 1, 1
Hout = (H + 2 * padding - dilation * (kh - 1) - 1) // stride + 1
X_np = rng.standard_normal((N, Cin, H)).astype(np.float64)
K_np = rng.standard_normal((Cout, Cin, kh)).astype(np.float64)
b_np = rng.standard_normal(Cout).astype(np.float64)
D_np = rng.standard_normal((N, Cout, Hout)).astype(np.float64)
dX_np = conv1d_input_grad_numpy(D_np, K_np, H, stride, padding, dilation)
X_t = torch.tensor(X_np, dtype=torch.float64, requires_grad=True)
K_t = torch.tensor(K_np, dtype=torch.float64, requires_grad=True)
b_t = torch.tensor(b_np, dtype=torch.float64, requires_grad=True)
Y_t = F.conv1d(X_t, K_t, b_t, stride=stride, padding=padding, dilation=dilation)
D_t = torch.tensor(D_np, dtype=torch.float64)
loss = (Y_t * D_t).sum()
loss.backward()
diff = np.max(np.abs(dX_np - X_t.grad.detach().numpy()))
print(f"Hout={Hout} max_abs_diff={diff:.3e}")Futtatás: python n058_f3_1d.py. Várható elvárás f64
CPU-n: max_abs_diff
alatt; a tényleges érték platformfüggő.
F4 megoldás. A hiba oka: Pythonban a
: -0 szelet alsó határa -0, ami
0-val egyenlő. A padding:-padding ezért
0:0-t jelent, ami üres. A következmény:
esetén a
térbeli dimenziói
méretűek lesznek, miközben a várt alak
,
tehát a PyTorch autograddal való összehasonlítás shape-hibát vagy hamis
egyezést adhat. Javítás: padding nélkül nincs mit levágni, mert a
scatter-add eleve csak a valós tartományba ír:
if padding > 0:
dX = full[:, :, padding:-padding, padding:-padding]
else:
dX = fullEllenőrzés: assert dX.shape == (N, Cin, H, W), majd a
max_abs_diff kiszámítása. A javítás után a
esetben a teljes
shape megmarad, és a numerikus eltérés a f64 tűréshatár alá kerül.
F5 megoldás. Hipotézis: a ciklusos referencia
futásideje közelítőleg lineárisan nő
-nel,
mert a batch iterációja független ismétlés; a csúcsmemória szintén
lineárisan nő a
és
tenzorok miatt. Mért mennyiségek: medián futásidő három ismétlésből
time.perf_counter-rel, csúcsmemória
tracemalloc vagy resource modullal. Protokoll:
rögzített seed, f64 dtype, minden
-re
azonos
generálás; minden mérés előtt egy warm-up hívás; az iterációk sorrendjét
véletlenszerűen keverve, hogy az operációs rendszer gyorsítótárazása ne
torzítson. Baseline: az üres ciklushurok futásideje azonos
-re,
vagy a PyTorch backward ideje ugyanazon konfigurációra. Érvényességi
korlát: az eredmény csak a jelen NumPy referencia implementációra és a
helyi CPU-ra vonatkozik; nem vihető át GPU-ra vagy a PyTorch kernelre,
és a kis batchméret tartományán kívül a Python interpreter és
memória-allokátor viselkedése megváltozhat.
A leggyakoribb hiba a scatter-add összeadásának felülírásra cserélése. Az F1 példában láttuk: a helyes helyett a felülíró kód -at ad, ha az utolsó kernelpozíció ír utoljára. A hiba oka az, hogy a forwardban egy input pixel több kimeneti elemhez tartozik; a láncszabály ezeknek a részhozzájárulásoknak az összegét írja elő. A felülírás tönkreteszi a weight sharing dualitását: ahogy a 15.1. Súly- és biasgradiens alfejezetben a kernelgradiensnél a felhasználási helyek összegzése volt a kulcs, itt ugyanaz az összegzés a bemeneti irányban jelenik meg.
Ellenőrizze: a alakja mindig megegyezik az alakjával, függetlenül a stride-tól és paddingtől. A , , , példában a bemeneti pozíciók -e minden kernel és mellett nulla, mert az indexfeltétel nem oldható meg. A bias gradiensre a -ből nincs szükség: mindenképpen a batch- és térbeli összege a 15.1. Súly- és biasgradiens szerint.
Visszahívás: a 3.2. Mátrixszorzás és affín
réteg alfejezetben az affin réteg inputgradiense
volt; a mostani levezetés ugyanezt a tranzponált operátort valósítja meg
keresztkorrelációs formában. A 14.2. Több bemeneti
és kimeneti csatorna alfejezetben a forward csatornaösszeg volt a
kimeneti csatornák felépítésének kulcsa; a
képletében a
szerinti összegzés ennek a csatornastruktúrának a megfordítása. A 11.2. nn.Module és paraméterregisztráció
alfejezetben bevezetett modul-nyilvántartás itt abban érhető tetten,
hogy a PyTorch conv2d autograd számítása a regisztrált
Parameter és a bemeneti Tensor gráfján
automatikusan elvégzi a fenti accumulationt.
A konvolúciós réteg inputgradiense az upstream gradiens és a kernel súlyának szorzatából álló scatter-add. A lemmában levezettük a teljes NCHW képletet, amely minden lehetséges kimeneti pozícióból összegzi a hozzájárulásokat a , célkoordinátákra. A stride oszthatósági feltételt szab, a padding a virtuális pixelek hozzájárulásait zárja ki, a dilation a kernel effektív kiterjedését növeli. Az átfedő patch-ek miatt a hozzájárulásokat összeadni kell; a felülírás hibás gradienst ad. A NumPy-referenciát PyTorch autograddal több stride/padding/dilation konfiguráció mellett hasonlítottuk össze f64 tűréssel.
A 15.3. Transposed convolution mint adjungált
egységben a scatter-add operátort formális lineáris leképezésként
vizsgáljuk: a forward konvolúció adjungáltja a transzponált konvolúció.
A mostani indexfeltételből közvetlenül leolvasható lesz a
ConvTranspose2d bemeneti–kimeneti kapcsolata, és az, hogy
miért nem általános inverz.