Ez az alfejezet az egycsatornás konvolúciós műveletet terjeszti ki több bemeneti és kimeneti csatornára. A 14.1. Stride, padding, dilation és outputméret egységben már definiáltuk a kimeneti méretet stride, padding és dilation mellett; a 13.2. Konvolúció és cross-correlation egységben pedig a keresztkorrelációt mint kernelmegfordítás nélküli szorzatösszeget. Most arra a kérdésre válaszolunk: ha egy 24×24-es szürkeárnyalatos GlyphLab-kép helyett háromcsatornás bemenetünk lenne, hogyan változik a súlytenzor alakja, és pontosan milyen összegzés állítja elő a kimeneti jellemzőtérképeket?
A CNNe-építő programozó számára a fő veszély nem a képlet
bonyolultsága, hanem a tengelyek összekeverése. Ezért minden állítást
NCHW/OIHW alakkal, azaz batch–csatorna–magasság–szélesség, illetve
kimeneti csatorna–bemeneti csatorna–kernelmagasság–kernelszélesség
konvencióban adunk meg. A PyTorch Conv2d dokumentációja
[D10] ugyanezt a súlyalakot specifikálja groups=1 esetén,
és kifejezetten keresztkorrelációról beszél. A CS231n jegyzet [S01] a
kernel bank fogalmát használja ugyanerre az objektumra; az itteni
levezetés és számpélda saját.
Legyen a bemeneti köteg
ahol a batch méret, a bemeneti csatornák száma, és a térbeli dimenziók. A súlytenzor
és a torzításvektor
A kimenet
Minden rögzített kimeneti csatornához tartozó
szeletet kernel banknak nevezzük. Egy bank tehát darab térbeli, -s kernelből áll: minden bemeneti csatornához egy. A teljes súlytenzor darab ilyen bank egymásra helyezése.
Az intuíció a következő. Egy kimeneti jellemzőtérkép egyetlen neuronja nem csak a térbeli szomszédokat, hanem az összes bemeneti csatorna ugyanazon térbeli környezetét is látja. Először minden bemeneti csatornát külön-külön megszűrünk a bank megfelelő kernelével, majd ezeket a szűrt térképeket elemenként összeadjuk. Ez a csatornaösszegzés (channel sum). A torzítás ezután minden kimeneti csatornához egyetlen skalárt ad hozzá, amely térben és a batch mentén is konstans; ezért mondjuk, hogy a bias per output channel értendő.
A levezetéshez a 14.1. Stride, padding, dilation és outputméret egységben bevezetett kimeneti méretet használjuk szimmetrikus zero paddinggel:
és analóg módon. Itt a stride, a padding, a dilation. Feltesszük, hogy és .
Lemma (többcsatornás keresztkorreláció). Legyen , , a fenti alakú, és legyen a fenti feltételű. Ekkor minden , , és indexre a kimenet
Bizonyítás. A bizonyítás a réteg definíciójából indul ki, és nem használja fel a bizonyítandó állítást.
A három összegzés sorrendje tetszőleges, mert véges sok tagot adunk össze; a fenti csoportosítás csak a levezetés átláthatóságát szolgálja.
A négy lépés együtt éppen a lemma képletét adja.
Alakellenőrzés. A jobb oldal minden rögzített indexre egyetlen valós skalár. A bank alakja , tehát a szerinti hármas összegzés pontosan a bank dimenzióit járja be. A indexelése konzisztens a első tengelyével, vagyis darab skalár van. A kimenet első tengelye , így a batch dimenzió az egyenletben csak passzív: ugyanaz a és alkalmazódik minden megfigyelésre.
Megjegyzés a keresztkorrelációról. A fenti képletben
a kernel indexe
növekvő sorrendben halad, és a bemeneti index is
-vel
nő. Ez a programozói főút keresztkorrelációja. Ha valaki matematikai
konvolúciót akar, a 13.2. Konvolúció és
cross-correlation szerint a kernelt 180 fokkal el kell forgatni;
tanult súlyoknál a két konvenció ekvivalens paraméterezést ad, de kézzel
megadott szűrőknél numerikusan különbözik. A D10 dokumentáció a
Conv2d műveletét keresztkorrelációként specifikálja; ezt a
laborban is követjük.
Legyen , , , , , stride , padding , dilation . Ekkor .
A bemenet két csatornája:
A két kernel bank:
A torzítás . Számítsuk ki a , , kimeneti elemet. A csatorna bal felső -es ablaka , így
A csatorna ablaka , így
A csatornaösszeg , a bias hozzáadása után . A többi pozíciót ugyanígy kiszámolva:
Figyeljük meg, hogy a két kimeneti csatorna teljesen más térképet ad, noha ugyanazt a bemenetet látja: a különbséget a bankok és a biasok okozzák. Az alábbi kód ezt a példát is ellenőrzi.
Conv2d, két bemeneti csatornávalA labor a saját NumPy referenciát veti össze a PyTorch
conv2d függvényével float64 pontosságban. A
referencia a 14.1. Stride, padding, dilation és
outputméret alfejezetben bevezetett indexezést használja, de itt
teljes egészében megadjuk, hogy a labor önállóan futtatható legyen.
# munkakönyvtár: lab/n054
# fájl: n054_lab.py
import numpy as np
import torch
import torch.nn.functional as F
def conv2d_numpy(x, weight, bias, stride=1, padding=0, dilation=1):
"""NCHW keresztkorreláció, groups=1, szimmetrikus zero padding."""
N, C_in, H, W = x.shape
C_out, C_in_w, kH, kW = weight.shape
if C_in_w != C_in:
raise ValueError("A suly C_in tengelye nem egyezik a bemenettel")
H_out = (H + 2 * padding - dilation * (kH - 1) - 1) // stride + 1
W_out = (W + 2 * padding - dilation * (kW - 1) - 1) // stride + 1
y = np.zeros((N, C_out, H_out, W_out), dtype=x.dtype)
for n in range(N):
for o in range(C_out):
for i in range(H_out):
for j in range(W_out):
total = 0.0
for c in range(C_in):
for u in range(kH):
for v in range(kW):
xi = i * stride - padding + u * dilation
xj = j * stride - padding + v * dilation
if 0 <= xi < H and 0 <= xj < W:
total += x[n, c, xi, xj] * weight[o, c, u, v]
y[n, o, i, j] = total + bias[o]
return y
# 1) A kezi peld a ellenorzese
x_manual = np.array([[[[1, 2, 3], [4, 5, 6], [7, 8, 9]],
[[9, 8, 7], [6, 5, 4], [3, 2, 1]]]], dtype=np.float64)
w_manual = np.array([[[[1, 0], [0, -1]], [[0, 1], [-1, 0]]],
[[[0, 1], [1, 0]], [[-1, 0], [0, 1]]]], dtype=np.float64)
b_manual = np.array([1.0, -1.0], dtype=np.float64)
y_manual = conv2d_numpy(x_manual, w_manual, b_manual)
print("Kezi pelda, o=0:\n", y_manual[0, 0])
print("Kezi pelda, o=1:\n", y_manual[0, 1])
x_t = torch.from_numpy(x_manual)
w_t = torch.from_numpy(w_manual)
b_t = torch.from_numpy(b_manual)
y_t = F.conv2d(x_t, w_t, b_t, stride=1, padding=0)
print("PyTorch elteres a kezi peldan:", torch.max(torch.abs(y_t - torch.from_numpy(y_manual))).item())
# 2) Veletlen f64 osszevetes ket bemeneti csatornaval
rng = np.random.default_rng(20261020)
N, C_in, H, W = 2, 2, 5, 5
C_out, kH, kW = 3, 3, 3
stride, padding, dilation = 2, 1, 1
x = rng.standard_normal((N, C_in, H, W)).astype(np.float64)
w = rng.standard_normal((C_out, C_in, kH, kW)).astype(np.float64)
b = rng.standard_normal((C_out,)).astype(np.float64)
y_ref = conv2d_numpy(x, w, b, stride, padding, dilation)
y_torch = F.conv2d(torch.from_numpy(x),
torch.from_numpy(w),
torch.from_numpy(b),
stride=stride, padding=padding, dilation=dilation)
err = np.max(np.abs(y_ref - y_torch.numpy()))
print("Alakok: x", x.shape, "w", w.shape, "b", b.shape,
"-> y", y_ref.shape)
print("max abszolut elteres:", f"{err:.3e}")Futtatási parancs:
python n054_lab.pyA kimenet jelölt szemléltetés, mert ezt a kérést író környezet nem futtatott Python-kódot; az értékek a fenti determinisztikus kódból következnek, és a hallgatói futtatás során ellenőrizendők:
Kezi pelda, o=0:
[[-1. -1.]
[-1. -1.]]
Kezi pelda, o=1:
[[1. 3.]
[7. 9.]]
PyTorch elteres a kezi peldan: 0.0
Alakok: x (2, 2, 5, 5) w (3, 2, 3, 3) b (3,) -> y (2, 3, 2, 2)
max abszolut elteres: 0.000e+00
Az első két blokk igazolja a kézi számpéldát. A véletlen esetben a
PyTorch és a NumPy referencia közötti maximális abszolút eltérés
vagy gépi epszilon nagyságrendű, mert mindkét oldal
float64-ben számol, és a referencia ugyanazt az összegzési
sorrendet követi, mint a levezetés. Ha float32-re
váltanánk, a 2.2. Lebegőpont és stabil elemi
számítás szerint nagyobb, de még mindig kicsi eltérésre
számíthatnánk; a pontos egyezés nem várható el automatikusan. A
csatornák számának növelése nem változtatja meg a kimeneti térbeli
alakot, csak a bank méretét:
esetén a súly alakja
lenne.
F1 (fogalmi, nehézség 1). Definiálja a kernel bank fogalmát, és adja meg az alakját , , esetén. Miért van darab bank, és mi a különbség egy bank és egyetlen térbeli kernel között?
F2 (levezetési, nehézség 1). Legyen , stride , padding , dilation , és hanyagoljuk el a biast. Vezesse le a Lemma képletéből, hogy ekkor
Magyarázza el, miért nevezzük ezt pontonkénti csatornakeverésnek, és ellenőrizze a kimeneti alakot.
F3 (implementációs, nehézség 2). Írjon
conv2d_numpy_s1p0 nevű függvényt, amely stride
,
padding
esetén valósítja meg a többcsatornás forwardot, és tesztelje a fenti
kézi példán. Adja meg a függvény teljes kódját importokkal, a bemeneti
tömböket, a várt kimenetet és a toleranciát.
F4 (hibakeresési, nehézség 2). Az alábbi kódrészlet a csatornaösszegzést próbálja NumPy-val elvégezni, de hibás:
def hibas_forward(x, w, b):
N, C, H, W = x.shape
Cout, _, kh, kw = w.shape
Hout = H - kh + 1
Wout = W - kw + 1
y = np.zeros((Cout, Hout, Wout), dtype=x.dtype)
for o in range(Cout):
for i in range(Hout):
for j in range(Wout):
# HIBAS: a 0. tengely a batch, nem a csatorna
y[o, i, j] = np.sum(
x[:, :, i:i+kh, j:j+kw] * w[o, :, :, :],
axis=(0, 2, 3)
) + b[o]
return yLokalizálja a hibát, magyarázza el a következményét, és adja meg a javított sort. Milyen bemeneti alak esetén tűnne a kimenet véletlenül helyesnek?
F5 (kísérleti tervezés, nehézség 3). Tervezzen egy kis kísérletet annak eldöntésére, hogy a bias valóban kimeneti csatornánként egyetlen skalár-e, vagy térbeli pozíciónként változik. Fagyassza a súlyokat nullára, adjon meg ismert biasvektort, és írja le, milyen kimenetet vár a két hipotézis alatt, milyen statisztikával döntene, és milyen korlátai vannak a következtetésnek.
F1 megoldás. A kernel bank a súlytenzor rögzített kimeneti csatornához tartozó szelete: . A feladat adataival az alak . Azért van darab bank, mert minden kimeneti jellemzőtérkép saját szűrőkészletet kap. Egy bank nem egyetlen térbeli kernel, hanem darab kernel együttese: minden bemeneti csatornához külön szűrő tartozik, és ezek kimeneteit összegezzük. Az egycsatornás eset akkor áll elő, ha ; ekkor a bank egyetlen térbeli kernellé redukálódik.
F2 megoldás. A Lemma képletébe , , , helyettesítve az egyetlen kernelindex, és a bemeneti index , illetve hasonlóan. Így
Nincs térbeli szomszédosság, ezért a művelet minden pozícióban külön-külön, a csatornák mentén lineárisan kombinál. A kimeneti alak a 14.1. Stride, padding, dilation és outputméret formulával , ; a kimenet . A tanulható skalárok száma .
F3 megoldás. A függvény a conv2d_numpy
speciális esete:
# fajl: n054_f3.py
import numpy as np
def conv2d_numpy_s1p0(x, weight, bias):
N, C_in, H, W = x.shape
C_out, C_in_w, kH, kW = weight.shape
assert C_in_w == C_in
H_out = H - kH + 1
W_out = W - kW + 1
y = np.zeros((N, C_out, H_out, W_out), dtype=x.dtype)
for n in range(N):
for o in range(C_out):
for i in range(H_out):
for j in range(W_out):
total = 0.0
for c in range(C_in):
total += np.sum(
x[n, c, i:i+kH, j:j+kW] * weight[o, c, :, :]
)
y[n, o, i, j] = total + bias[o]
return y
x = np.array([[[[1, 2, 3], [4, 5, 6], [7, 8, 9]],
[[9, 8, 7], [6, 5, 4], [3, 2, 1]]]], dtype=np.float64)
w = np.array([[[[1, 0], [0, -1]], [[0, 1], [-1, 0]]],
[[[0, 1], [1, 0]], [[-1, 0], [0, 1]]]], dtype=np.float64)
b = np.array([1.0, -1.0], dtype=np.float64)
y = conv2d_numpy_s1p0(x, w, b)
print(y[0, 0])
print(y[0, 1])A várt kimenet a fenti kézi példa eredménye, az ellenőrzés egész
számokon történik, ezért a tolerancia
;
lebegőpontos bemenetnél
np.allclose(y, y_expected, atol=1e-12, rtol=1e-12)
indokolt.
F4 megoldás. A hiba az axis=(0,2,3)
kifejezésben van: a
tengely a batch, nem a bemeneti csatorna. A kód így a batch elemeit is
összegzi, ezért a kimenet (C_out, H_out, W_out) alakú lesz
az elvárt (N, C_out, H_out, W_out) helyett, és különböző
megfigyelések keverednek. A javítás: axis=(1,2,3), azaz a
csatorna- és kerneltengelyeket összegezni. A hiba akkor tűnne véletlenül
helyesnek, ha
,
mert ekkor a batch szerinti összegzés egyetlen elemet ad, és az alak is
egybeeshet, ha a többi dimenzió stimmel; de
esetén azonnal alak- és értékhibát kapunk.
F5 megoldás. Egy lehetséges terv: rögzítünk , , , , kernel , stride , padding . A súlyokat nullára állítjuk, a bias pedig legyen . Ekkor a Lemma szerint , tehát a kimenet minden indexre ugyanaz az csatornában. A hipotézisek: ha a bias per output channel, akkor a kimeneti térkép minden pozíciója konstans; ha térbeli bias lenne, akkor a hossza kellene, és a térkép pozíciónként változna. Döntési statisztika: minden kimeneti csatornára számítsuk ki a pozíciók közötti szórást; per-channel bias esetén ez , térbeli bias esetén általában pozitív. A korlát: ez a kísérlet csak a bias szerkezetét azonosítja, a súlyok csatornaösszegzését nem; továbbá a nulla súly melletti eredmény nem igazolja, hogy a bias a tanítás során is ezt a szerepet tartja meg.
A leggyakoribb tévedés szerint a batch és a csatorna tengely
felcserélhető, mert mindkettő az első két NCHW-dimenzió, és a
reshape vagy transpose azonos elemszámot ad.
Ez hamis. Legyen
és
.
Ugyanahhoz a
súlyhoz
-en
két megfigyelést,
-n
három megfigyelést dolgoz fel a réteg: a
első batch eleme az eredeti első csatorna két batch elemét tartalmazza,
így a kernel azokat a megfigyeléseket csatornaként összegzi, amelyeknek
függetleneknek kellene maradniuk. Konkrét ellenpélda: ha az eredeti
első batch elemének első csatornája csupa
,
második csatornája csupa
,
második batch elemének első csatornája csupa
,
második csatornája csupa
,
akkor az átrendezett
első batch elemének két csatornája
-ek
és
-ak
keveréke; ugyanaz a kernel más kimenetet ad, mint bármely eredeti
megfigyelésre. A megfigyelési egység így sérül, ami az 1.3. Adatfelosztás és a megfigyelési egység szerint
a split és a kiértékelés érvényességét is tönkreteszi.
Ellenőrizze magát: (1) Mi a alakja , , , mellett? (Válasz: .) (2) Hány skalár bias tartozik ehhez a réteghez? (Válasz: .) (3) Ha , mire redukálódik a Lemma képlete? (Egyetlen csatorna összegzésére, azaz a 13.2. Konvolúció és cross-correlation alfejezetbeli egycsatornás kereszttkorrelációra.) (4) Miért nem szabad a csatornaösszegzést a batch tengelyen elvégezni? (Mert a megfigyelések keverednének.)
Visszahívás: a 13.2. Konvolúció és
cross-correlation egység definiálta a keresztkorrelációt és a bias
fogalmát egy csatornára; az itteni Lemma ezt terjeszti ki a
csatornaösszegzéssel. A 10.2. Affín réteg és
aktiváció backward affin rétegénél láttuk, hogy a batch dimenzió
passzív: az
minden sorra ugyanazt a leképezést alkalmazza. Ugyanez a tulajdonság
jelenik meg itt is a
és
batchfüggetlenségében. Végül a 2.2. Lebegőpont és
stabil elemi számítás egység figyelmeztet, hogy a referencia és a
PyTorch közötti numerikus egyezést toleranciával, float64
mellett érdemes ellenőrizni.
A többcsatornás konvolúciós réteg minden kimeneti csatornához egy
kernel bankot rendel, amely
darab
-s
kernelből áll. A forward a bemeneti csatornák szűrt térképeit elemenként
összegzi, majd hozzáadja a kimeneti csatornánkénti skalár biast. A Lemma
ezt a teljes összeget adja meg indexekkel, NCHW/OIHW alakokkal; a
bizonyítás a csatornánkénti keresztkorrelációból és a véges összegek
felcserélhetőségéből áll. A labor f64-ben igazolja, hogy a NumPy
referencia és a PyTorch F.conv2d azonos eredményt ad két
bemeneti csatornával. A batch és a csatorna tengely nem cserélhető fel
azonos elemszám ellenére sem.
A 14.3. Paraméterszám, MAC és aktivációmemória egység a paraméterszámot, a MAC-műveletek számát és az aktivációmemóriát számítja ki ezekből az alakokból. A kernel bank fogalma ott válik költségmodellé: a szorzat a súlyok számát, a kimeneti térbeli mérettel szorozva pedig a számítási igényt adja.