Ez az alfejezet a konvolúciós réteg két paraméterének, a kernelnek és a biasnak a gradiensét vezeti le a teljes NCHW cross-correlation definícióból. A központi jelenség a weight sharing (súlymegosztás): ugyanaz a kernel több ezer kimeneti pozícióban szerepel, ezért a gradiensük az összes felhasználási hely lokális gradiensének összege lesz.
Előismeretként feltételezzük a 4.4. I. ellenőrző pont: matematika és kód megfelelése gráf-alapú láncszabályt, a 10.4. Tanítási lépés és tanuló MLP minibatch-gradiens átlagolását, a 13.4. Határkezelés és frekvenciaintuíció határkezelési konvenciókat és a 14.4. Csoportos és depthwise konvolúció csatornaösszegzési szerkezetet. Visszautalunk továbbá a 14.1. Stride, padding, dilation és outputméret kimeneti méretképletre, a 11.1. Tensor és autograd-életciklus autograd-életciklusra és a 3.1. Vektor, norma és skalárszorzat mátrix–vektor szorzás alakra.
Induló becslés. Egy -as kernel bemeneti és kimeneti csatornával, -es bemeneten, stride és nulla padding mellett hány kimeneti pozícióban szerepel? Segítség: a kimeneti méret , tehát a kernel ablakban vesz részt. A gradiens mind a ablak hozzájárulását összegzi.
Tekintsük a teljes NCHW cross-correlationt. A bemeneti tensor
a kernel
a bias , és a kimenet
A és értékét a 14.1. Stride, padding, dilation és outputméret alfejezetben levezetett
formula adja, és analóg módon . Itt a szimmetrikus zero padding, a stride, a dilation. Ebben az alfejezetben -et feltételezünk, hacsak mást nem mondunk; a dilációs általánosítás feladat.
Az output definíciója minden , , és indexre:
A padding eltolás biztosítja a virtuális, nullával kiegészített tartomány indexelését. A kényelem kedvéért bevezetjük a paddingelt bemenet jelölést: a paddingelt képet jelöli, ahol és . Ezzel az output tömörebben:
A veszteség és az upstream gradiens. Legyen egy skalár loss, amely differenciálható függvénye az kimenetnek (például a 4.4. I. ellenőrző pont: matematika és kód megfelelése alfejezetbeli vagy egy cross-entropy a későbbi fejezetből). A visszaterjesztés során ismert az upstream gradiens:
ahol az általános elem . A cél a
kiszámítása. A láncszabály szerint minden paraméter-elemre:
Mivel lineáris a kernelben, a belső parciális derivált éppen az adott bemeneti pixel: . A bias esetén a parciális derivált .
Tétel (súly- és biasgradiens a teljes NCHW cross-correlationhoz). Legyen bemenet, kernel, bias, szimmetrikus zero padding, stride, dilation, és legyen a fenti cross-correlation kimenete. Ha differenciálható függvénye -nak, akkor
és
Ahol a paddingelt bemenetet jelöli.
Bizonyítás. A láncszabály a teljes tenzorgráfon a 4.4. I. ellenőrző pont: matematika és kód megfelelése alfejezetben bemutatott skalár-út-menti érvelés közvetlen kiterjesztése. Mivel az minden elemén keresztül függhet -től, a teljes differenciál:
Az indexre a belső parciális derivált nulla, kivéve ha , mert az -adik kimeneti csatorna csak a kernel -adik kimeneti szeletét használja. Ha , akkor a definícióból közvetlenül:
Ezt behelyettesítve a kívánt formula adódik. A bias esetén , és ha , így
Ez befejezi a bizonyítást. ∎
Intuíció: a batch and spatial sum. A kernel gradiensformulája három összegzést tartalmaz: egyet a batch indexre, kettőt a térbeli kimeneti indexekre. Ez a batch and spatial sum (batch- és térösszeg) elv: minden felhasználási helyen az upstream gradiens és az adott bemeneti pixel szorzatát képezzük, és ezeket összeadjuk. A bias gradiensében nincs pixel-szorzó, csak az upstream gradiens tér- és batchösszege áll.
Összeadott, nem átlagolt gradiensek. A fenti levezetés lineáris rétegként kezeli a konvolúciót, ahol a gradiens a láncszabály szerint összegzés. Nincs benne osztás a felhasználási helyek számával. A minibatch-gradiens átlaga csak a loss definíciójából származik: ha , akkor a batch-átlag faktora az upstream gradiensben jelenik meg, nem a kernel gradiensformulájában. Ezt a különbséget a 10.4. Tanítási lépés és tanuló MLP minibatch-átlagolása és a jelen alfejezet tisztán összegző láncszabálya között fontos megőrizni.
Tekintsünk egy , , bemenetet, -as kernelt, , paddinget és stride-ot. Ekkor , tehát éppen egyetlen térbeli kimeneti pozíció van. A kernel minden eleme pontosan egyszer szerepel, így ez a példa a legegyszerűbb eset: nincs összegzés a térbeli használatok fölött.
Legyen
és . A kimenet egyetlen skalár:
Mivel egyetlen kimeneti elem van, az upstream gradiens is skalár: legyen . A tétel szerint:
Tehát éppen . A bias gradiens .
Numerikus ellenőrzés véges differenciával. A középső differenciás közelítése:
Ahol az a mátrix, amelynek az eleme , a többi . A fenti választással . Ha és -t perturbáljuk, akkor esetén , tehát . Az perturbációval esetén , szintén . A középső differencia minden esetben
ami azt mutatja, hogy a loss a pontban nulla gradiensű, tehát a fenti példa választása rosszul ellenőriz. A helyes teszt a veszteség (azonosítható lineáris) vagy egy olyan , amely nem nulla. Világosabb példához -t válasszuk -nek, ekkor , és . Az upstream gradiens , így és . A véges differencia a biasra:
A kernelre hasonlóan ellenőrizhető, hogy a perturbáció utáni megváltozása lineáris, tehát a véges differencia az adott elemet adja.
A következő kód a levezetés numerikus ellenőrzése a fenti
-as
példán és egy nagyobb,
-es
inputon futtatva. A munkakönyvtár lab/n057/.
# lab/n057/weight_grad_check.py
import numpy as np
def conv2d_forward(X, K, b, stride=1, pad=0):
"""NCHW cross-correlation, groups=1, dilation=1, float64."""
N, C_in, H, W = X.shape
C_out, C_in_k, kh, kw = K.shape
assert C_in == C_in_k
H_out = (H + 2 * pad - kh) // stride + 1
W_out = (W + 2 * pad - kw) // stride + 1
Xp = np.pad(X, ((0, 0), (0, 0), (pad, pad), (pad, pad)))
Y = np.zeros((N, C_out, H_out, W_out), dtype=np.float64)
for n in range(N):
for o in range(C_out):
for i in range(H_out):
for j in range(W_out):
acc = b[o]
for c in range(C_in):
for u in range(kh):
for v in range(kw):
acc += Xp[n, c, i * stride + u,
j * stride + v] * K[o, c, u, v]
Y[n, o, i, j] = acc
return Y
def conv2d_weight_grad(X, K, b, dY, stride=1, pad=0):
"""Weight and bias gradient from the derived theorem."""
N, C_in, H, W = X.shape
C_out, _, kh, kw = K.shape
H_out = dY.shape[2]
W_out = dY.shape[3]
Xp = np.pad(X, ((0, 0), (0, 0), (pad, pad), (pad, pad)))
dK = np.zeros_like(K, dtype=np.float64)
db = np.zeros(C_out, dtype=np.float64)
for o in range(C_out):
for c in range(C_in):
for u in range(kh):
for v in range(kw):
total = 0.0
for n in range(N):
for i in range(H_out):
for j in range(W_out):
total += dY[n, o, i, j] * Xp[
n, c, i * stride + u, j * stride + v]
dK[o, c, u, v] = total
db[o] = np.sum(dY[:, o, :, :])
return dK, db
def finite_diff_check(X, K, b, eps=1e-5):
"""Central difference for one scalar loss L = 0.5 * sum(Y ** 2)."""
N, C_in, H, W = X.shape
C_out, _, kh, kw = K.shape
H_out = (H + 2 * 0 - kh) // 1 + 1
W_out = (W + 2 * 0 - kw) // 1 + 1
def loss(K, b):
Y = conv2d_forward(X, K, b, 1, 0)
return 0.5 * np.sum(Y ** 2)
dK_num = np.zeros_like(K, dtype=np.float64)
for o in range(C_out):
for c in range(C_in):
for u in range(kh):
for v in range(kw):
Kp = K.copy(); Km = K.copy()
Kp[o, c, u, v] += eps
Km[o, c, u, v] -= eps
dK_num[o, c, u, v] = (loss(Kp, b) - loss(Km, b)) / (2 * eps)
db_num = np.zeros(C_out, dtype=np.float64)
for o in range(C_out):
bp = b.copy(); bm = b.copy()
bp[o] += eps; bm[o] -= eps
db_num[o] = (loss(K, bp) - loss(K, bm)) / (2 * eps)
return dK_num, db_num
if __name__ == "__main__":
rng = np.random.default_rng(20261020)
# 3x3 single-position case
X1 = np.array([[[[1.0, 2.0, 1.0],
[0.0, 1.0, 3.0],
[2.0, 0.0, 1.0]]]])
K1 = np.array([[[[1.0, 0.0, 0.0],
[0.0, 1.0, 0.0],
[0.0, 0.0, -1.0]]]])
b1 = np.array([5.0])
Y1 = conv2d_forward(X1, K1, b1, 1, 0)
dY1 = Y1 # since upstream for L = 0.5*sum(Y**2) is Y
dK1, db1 = conv2d_weight_grad(X1, K1, b1, dY1, 1, 0)
dK1_num, db1_num = finite_diff_check(X1, K1, b1)
assert np.allclose(dK1, dK1_num, atol=1e-8, rtol=1e-5), (dK1, dK1_num)
assert np.allclose(db1, db1_num, atol=1e-8, rtol=1e-5), (db1, db1_num)
print("3x3 case: dK =\n", dK1[0,0], "\ndb =", db1)
# 4x4 input with 2 output channels, kernel 3x3, stride 1, no padding
X2 = rng.normal(size=(2, 2, 4, 4))
K2 = rng.normal(size=(2, 2, 3, 3))
b2 = rng.normal(size=(2,))
Y2 = conv2d_forward(X2, K2, b2, 1, 0)
dY2 = Y2
dK2, db2 = conv2d_weight_grad(X2, K2, b2, dY2, 1, 0)
# Ugyanazzal a bemenettel, kernellel és biasszal ellenőrzünk:
dK2_num, db2_num = finite_diff_check(X2, K2, b2)
assert np.allclose(dK2, dK2_num, atol=1e-8, rtol=1e-5)
assert np.allclose(db2, db2_num, atol=1e-8, rtol=1e-5)
print("4x4 case: shapes dK", dK2.shape, "db", db2.shape, "passed.")Futtatás:
cd lab/n057
python weight_grad_check.pyA várt kimenetben az első eset mátrixa a mátrix, a értéke (itt egyetlen térbeli pozíció van). A második esetben a shape-ek és .
Megjegyzés a megosztott helyek számáról. A
-es
inputon
,
tehát minden kernel-elem
térbeli pozícióban szerepel. A gradiens ezek összege: az u
és v hurkokon belüli total változó pontosan
ezt a batch and spatial sum-ot számolja. A numerikus
egyezés
abszolút tűréssel, f64-ben igazolja a képletet. A Python-specifikus
eltérés: a NumPy np.pad alapértelmezésben konstans nulla
paddinget ad, ami a szimmetrikus zero padding konvenciónk.
Gyakori tévedés. Azt gondolni, hogy a kernel gradiensét a felhasználási helyeken átlagolni kell, mert a minibatch-loss átlagot tartalmaz. Ez keveri a loss definíciójából származó faktort a láncszabály összegzési szerkezetével.
Ellenpélda. Legyen a loss , és legyen minden bemeneti pixel , a kernel minden eleme , a bias . Ekkor egyetlen térbeli pozícióban . Ha a gradiens a felhasználások átlaga lenne (itt egyetlen pozíció, tehát nincs osztás), nem látnánk hibát. Tekintsünk most stride -gyel -es bemenetet: , tehát négy pozíció, mindegyik . Az upstream gradiens . A helyes kernel-gradiens: minden bemeneti pixelre, azaz . Ha valaki osztana a pozíciók számával, -et kapna, ami téves. A helyes gondolat: minden kimeneti pozícióban a láncszabály a szorzatot adja, és ezek a hozzájárulások összeadódnak.
Visszahívás 11.1. Tensor és
autograd-életciklus alfejezetből. A PyTorch
backward hívása a .grad tárolóba összead, nem
felülír. Ez pontosan ugyanaz az összegzési szemlélet: a kernel
gradiensének minden felhasználási helyről érkező hozzájárulása
akkumulálódik. Nem szabad tehát a réteg saját backwardjában osztani a
pozíciók számával.
F1 (fogalmi, nehézség 1). Definiálja a weight sharing fogalmát a konvolúciós rétegben. Adjon egy konkrét példát, amelyben ugyanaz a kernel-elem két különböző térbeli pozícióban szerepel, és indokolja, miért nem tekinthetők ezek független paramétereknek. Elvárt eredmény: saját megfogalmazású definíció és egy konkrét index-példa esetén.
F2 (levezetési, nehézség 2). Vezesse le a kernel gradiensét a 14.1. Stride, padding, dilation és outputméret alfejezetben bevezetett paddingelt bemenet jelölésével, de most stride esetén. Kérdés: megváltozik-e a formula szerkezete, vagy csak az indexelés? Elvárt eredmény: a teljes levezetés a láncszabályból, a stride explicit figyelembevételével, és a végső formula.
F3 (implementációs, nehézség 2). Írjon Python/NumPy
függvényt, amely a megadott conv2d_forward kódot használva
kiszámítja a kernel gradiensét, de a forward számítást nem hívja meg
újra. A bemenet
,
,
,
,
,
,
.
Ellenőrizze a kimeneti shape-et és egy véletlen seedű eseten a véges
differenciás kontrollt. Elvárt átadandó eredmény: futtatható kód,
shape-ellenőrzés és egyezési tűrés.
F4 (hibakeresési, nehézség 2). Egy hallgató a következő kódot írja a bias gradiensére:
db[o] = np.sum(dY[np.arange(N), o, :, :]) / (H_out * W_out)Mi a hiba? Milyen következménye van a tanításra? Javítsa és ellenőrizze a tétel formulájával. Elvárt eredmény: a hiba oka, következménye és a javított kód.
F5 (kísérleti/transzfer, nehézség 3). Tervezzen egy kis kísérletet, amely megméri, hogy a kernel gradiensének kiszámítása mennyivel drágább egy -es kernelre, mint egy -asra, azonos bemeneti méret és csatornaszám mellett. Milyen metrikát használna, mit tartana állandóan, és milyen korlátot fogalmazna meg az eredmény általánosításáról? Elvárt átadandó eredmény: írásos kísérleti protokoll az 1.2. Szakmailag megalapozott modellezési állítások szerinti elemekkel, de futtatás nélkül.
F1 megoldás. A weight sharing azt jelenti, hogy a kernel egyetlen eleme a bemenet több térbeli ablakában ugyanazzal a numerikus értékkel szerepel a kimenet kiszámításában. Például az stride-dal, paddinggel vett -as kernel eleme az és kimeneti pozíciókban is a bemenet , illetve pozíciójú pixelével szorzódik. A paraméterek azért nem függetlenek, mert a kimeneti pozíciók mindegyike ugyanazt a kernel-elemet használja; ha függetlenek lennének, a gradiensben nem összegzés, hanem külön-külön frissítés jelenne meg, és megszűnne az eltolási equivariance 16.1. Eltolási equivariance és invariance alfejezetbeli tulajdonsága.
F2 megoldás. A levezetés azonos a tétel bizonyításával, de a paddingelt bemenet indexelésében az és tagok szerepelnek. A stride nem változtatja meg a formula szerkezetét: a kernel gradiens továbbra is
A stride kizárólag abban jelenik meg, hogy a paddingelt bemenet mely koordinátáit mintavételezzük az egyes kimeneti pozíciókhoz. A levezetés minden lépése azonos a esettel, csak a mintavételi rács ritkább.
F3 megoldás. A fenti kód
conv2d_weight_grad függvénye már megvalósítja a levezetett
formulát. A
,
,
,
,
-as
kernel esetén a kimeneti shape
.
A finite_diff_check futtatása ezen a méreten azt mutatja,
hogy a középső differencia
lépésközzel és a fenti tűrésekkel egyezik a levezetett formulával.
F4 megoldás. A hiba a -tal való osztás. A tétel bizonyította, hogy a bias gradiens a batch- és térösszeg, nem pedig annak átlaga. A következmény az, hogy a bias gradiensét a térbeli méret függvényében alulskálázza: nagyobb kimeneti méret esetén a bias túl lassan tanul. A javított kód:
db[o] = np.sum(dY[:, o, :, :])A visszaellenőrzés a tétel formulájával: a éppen a fenti kód, minden osztás nélkül.
F5 megoldásvázlat. A kísérleti kérdés: hogyan skálázódik a kernel gradiensének számítási költsége a kernelmérettel. Protokoll: (1) kérdés: a -es és -as kernel gradiensszámítási idejének aránya; (2) adat: szintetikus véletlen tensor, rögzített seed; (3) megfigyelési egység: egyetlen gradiensszámítás futása; (4) split: nem releváns, nincs tanulás; (5) baseline: a -es kernel; (6) változó tényező: kernelméret; (7) metrika: fal-idő, ismételt méréssel, melegítés után; (8) erőforrás: helyi CPU, f64; (9) korlát: az eredmény nem általánosítható GPU-ra, és a forward overhead miatt a tiszta gradiens-számítás időaránya nem azonos a teljes tanítási idővel.
A konvolúciós réteg kernel- és biasgradiensét a teljes NCHW cross-correlation definícióból vezettük le a láncszabály alkalmazásával. A kernel gradiens minden súlyelemre a batch, a kimeneti magasság és a kimeneti szélesség szerinti összeg: az upstream gradiens és a megfelelő paddingelt bemeneti pixel szorzatát minden felhasználási helyen összeadjuk. A bias gradiens a batch és térbeli indexekre vett upstream-gradiens összeg. A weight sharing miatt a hozzájárulások összeadódnak, nem átlagolódnak. Kézi példán és NumPy-kódban véges differenciával ellenőriztük a formulát f64 tűrésekkel. A leggyakoribb hiba a felhasználási helyek számával való osztás, ami a minibatch-loss átlagolásának félreértéséből származik.
A 15.2. Inputgradiens és átfedő recepciós mezők egységben az input gradiensét vezetjük le; ott az átfedő recepciós mezők miatt a gradiensek hasonlóan összegződnek, de a kernel szerepe megfordul.