Ez az egység a Projekt I első állomása: egy saját, kézzel írt konvolúciós reference operátort készítünk, amelynek minden indexét, alakját és érvényességi feltételét pontosan dokumentáljuk. A cél nem egy gyors PyTorch-utánzat, hanem egy olyan specifikáció, amelyhez később a backwardot (37.2. Backward és ellenőrzési lefedettség), az adjungált ellenőrzést (37.3. Linearitás, adjungált és határfeltétel) és a tanuló réteget (37.4. Rétegből kis tanulómodell) kapcsolni lehet. A 16.4. IV. ellenőrző pont: a CNN-operátor teljes megértése IV. ellenőrző pontjából ismert, hogy a numerikus egyezés önmagában nem bizonyíték; ezért itt először a definíciót rögzítjük, és csak utána engedünk kódot mellé.
Szükséges előismeret: a 13.4. Határkezelés és
frekvenciaintuíció alfejezetben bevezetett cross-correlation és zero
padding, a 14.4. Csoportos és depthwise
konvolúció alfejezetben tárgyalt groups paraméter és
annak korlátozása, a 15.4. Független
gradiensellenőrzési protokoll alfejezetben kidolgozott
float64-es gradiensellenőrzési protokoll, valamint a 36.4. IX. ellenőrző pont: lektorálható
modellállítás alfejezetben áttekintett lektorálható állításforma. A
36.1. Állításból kísérleti terv
kísérlettervezési elvét és a 33.1.
Inference-szerződés és modellcsomag inference-szerződését csak
érintőlegesen idézzük vissza, mert itt még nincs betanított modell.
Induló kérdés: adott egy -ös bemenet, egy -as kernel, stride 2, padding 1, dilation 1. Hány térbeli pozícióból áll a kimenet, és miért nem elegendő a „majd a könyvtár tudja” válasz egy referencia specifikációhoz?
Az operátor a PyTorch Conv2d által használt
térbeli keresztkorrelációt számolja: a kernel nem
fordul meg semelyik tengely mentén. A matematikai konvolúció ettől a
kernel
-os
megfordításában tér el; ezt a 13.2. Konvolúció és
cross-correlation pontosan tárgyalta. A jelen specifikáció kizárólag
a következő részhalmazt támogatja:
groups=1, azaz minden kimeneti csatorna minden bemeneti
csatornát lát;float64; a training főút
később float32, de a numerikus kontrollhoz itt szándékosan
a pontosabb típust használjuk;A zero padding azt jelenti, hogy a bemeneti kép szélén kívülre eső pozíciók értéke nulla. Formálisan bevezetjük a paddingelt tömböt
ahol
Ezzel a forward egyetlen indexképlete:
ahol , , és , ha a bias ki van kapcsolva. A stride tehát csak a kimeneti pozíciót lépteti, a dilation pedig a kernel pontjainak bemeneti távolságát növeli: az -adik kernelsor a paddingelt kép sorát olvassa.
Következmény (az output alak). A kimeneti magasság a legnagyobb olyan index plusz egy, amelyre a kernel utolsó, -edik sorának pozíciója még a paddingelt tömbön belül van:
Átrendezve
A legnagyobb ilyen egész a jobb oldal alsó egészrésze, ezért
A szélességre ugyanígy, helyett indexekkel. Ez pontosan a JELOLESEK.md képlete, itt levezetve a paddingelt definícióból. A két térbeli dimenzió független, mert a padding szimmetrikus és a stride/dilation tengelyenként külön adható meg.
Érvényességi feltétel. A kimenet csak akkor értelmes, ha és . Ha a számláló negatív, az operátor hibát dob: a kernel effektív kiterjedése nagyobb, mint a paddingelt bemenet. Ez a feltétel az implementáció bemeneti validációjának része, nem a felhasználó felelőssége.
A definícióban a bias dimenziója , és minden térbeli pozícióhoz ugyanaz a skalár adódik: a bias csatornánként egyetlen paraméter, nem térben változó.
A fenti definícióval a referencia operátor pontosan meghatározott függvény: adott és a paraméterek mellett egyértelmű. A levezetési kötelezettség teljesült: az output alak a paddingelt ablak illeszkedési feltételéből következik, és a képlet minden indexhatárt megad. A bizonyítás státusza: saját, teljes levezetés; nincs átvett nagy tétel.
A specifikáció nem állítja, hogy ez az operátor
minden Conv2d-opciót támogat. Kimarad például az
aszimmetrikus padding, a
padding_mode='reflect'/'replicate'/'circular', a
groups>1, az output_padding, és a
transzponált művelet. A 15.3. Transposed convolution
mint adjungált alfejezetben tárgyalt ConvTranspose2d
adjungált szerepe sem tartozik ide; a D11 forrás ehhez kiinduló URL,
tartalma e kézirat írásakor nem ellenőrzött. A referencia célja a
tanulási főút pontos alapja, nem a keretrendszer
helyettesítése.
Vegyünk egy -as bemenetet, egy nemszimmetrikus -es kernelt, stride 1, padding 0, dilation 1, egyetlen bemeneti és kimeneti csatornát, bias :
A kernel azért nemszimmetrikus, mert a főátlóra tükrözve ; így a keresztkorreláció és a konvolúció eredménye különbözni fog. A definíció szerint például
A teljes kimenet:
Ellenőrzésképp a jobb alsó elem:
Ugyanez a bemenet matematikai konvolúcióval a megfordított kernel alkalmazását jelentené, ami más mátrixot adna; ez az F1 feladatban tér vissza.
A következő minimális kód a referencia függvényt mutatja be, és ezen a kézi példán ellenőrzi az eredményt. A teljes labor-fájl az 5. pontban található; itt csak a függvény vázát adjuk:
import numpy as np
def conv2d_reference(X, K, b=None, stride=(1,1), padding=(0,0), dilation=(1,1)):
# X: (N, C_in, H, W) float64
# K: (C_out, C_in, k_h, k_w) float64
# b: (C_out,) float64 vagy None
...
return Y
# kézi példa
X = np.array([[[[1,2,3],[4,5,6],[7,8,9]]]], dtype=np.float64) # (1,1,3,3)
K = np.array([[[[1,2],[3,4]]]], dtype=np.float64) # (1,1,2,2)
Y = conv2d_reference(X, K, b=np.array([10.0], dtype=np.float64))
print(Y) # várható [[[[47.,57.],[77.,87.]]]]A kód np.pad-et használ konstans nulla üzemmódban, majd
explicit ciklusokkal számol. A ciklusok lassúak, de a referencia célja a
helyesség, nem a sebesség.
A labor két fájlból áll. A reference_conv2d.py a
specifikációt valósítja meg; a test_reference.py a kézi
példát, az alakellenőrzéseket és a hibás bemenetek negatív tesztjeit
futtatja. A labor a lab/n145_reference/ munkakönyvtárban
található. A futtatás:
cd lab/n145_reference
python -m pytest test_reference.py -qA kimenet a kézirat írásakor nem mért, ezért jelölt szemléltetés; a tényleges eredményt a futtatás után a hallgató rögzíti a kísérleti naplóban.
A referencia függvény teljes kódja:
# reference_conv2d.py
import numpy as np
def conv2d_reference(X, K, b=None, stride=(1,1), padding=(0,0), dilation=(1,1)):
"""
Saját NCHW keresztkorrelációs referencia.
Támogatott: groups=1, szimmetrikus zero padding, stride, dilation,
opcionális bias. dtype: float64.
"""
if not (isinstance(X, np.ndarray) and isinstance(K, np.ndarray)):
raise TypeError("X és K NumPy tömb legyen")
if X.dtype != np.float64 or K.dtype != np.float64:
raise TypeError("X és K dtype-ja float64 legyen")
if b is not None and (not isinstance(b, np.ndarray) or b.dtype != np.float64):
raise TypeError("b dtype-ja float64 legyen")
N, C_in, H, W = X.shape
C_out, C_in_k, k_h, k_w = K.shape
if C_in_k != C_in:
raise ValueError(f"K bemeneti csatornája {C_in_k}, de X-é {C_in}")
if b is not None and b.shape != (C_out,):
raise ValueError(f"b alakja {b.shape}, várt ({C_out},)")
s_h, s_w = stride
p_h, p_w = padding
d_h, d_w = dilation
if s_h < 1 or s_w < 1:
raise ValueError("stride >= 1")
if p_h < 0 or p_w < 0:
raise ValueError("padding >= 0")
if d_h < 1 or d_w < 1:
raise ValueError("dilation >= 1")
if k_h < 1 or k_w < 1:
raise ValueError("kernel méret >= 1")
H_out = (H + 2*p_h - (k_h-1)*d_h - 1) // s_h + 1
W_out = (W + 2*p_w - (k_w-1)*d_w - 1) // s_w + 1
if H_out < 1 or W_out < 1:
raise ValueError("A kimeneti méret nem pozitív; ellenőrizze a paramétereket")
# paddingelt tömb
X_pad = np.pad(X, ((0,0),(0,0),(p_h,p_h),(p_w,p_w)), mode='constant')
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):
s = 0.0
for c in range(C_in):
for u in range(k_h):
for v in range(k_w):
s += X_pad[n, c, i*s_h + u*d_h, j*s_w + v*d_w] * K[o, c, u, v]
Y[n, o, i, j] = s
if b is not None:
Y[n, o, :, :] += b[o] # bias csatornánként
return YA tesztfájl a kézi példát és a negatív eseteket tartalmazza:
# test_reference.py
import numpy as np
import pytest
from reference_conv2d import conv2d_reference
def test_manual_nonsymmetric():
X = np.array([[[[1,2,3],[4,5,6],[7,8,9]]]], dtype=np.float64)
K = np.array([[[[1,2],[3,4]]]], dtype=np.float64)
Y = conv2d_reference(X, K, b=np.array([10.0], dtype=np.float64))
expected = np.array([[[[47.0,57.0],[77.0,87.0]]]], dtype=np.float64)
assert np.allclose(Y, expected, rtol=0, atol=1e-12)
def test_wrong_dtype():
X = np.zeros((1,1,3,3), dtype=np.float32)
K = np.zeros((1,1,2,2), dtype=np.float32)
with pytest.raises(TypeError):
conv2d_reference(X, K)
def test_channel_mismatch():
X = np.zeros((1,2,3,3), dtype=np.float64)
K = np.zeros((1,3,2,2), dtype=np.float64)
with pytest.raises(ValueError):
conv2d_reference(X, K)
def test_zero_output():
X = np.zeros((1,1,2,2), dtype=np.float64)
K = np.zeros((1,1,5,5), dtype=np.float64)
with pytest.raises(ValueError):
conv2d_reference(X, K)
def test_bad_stride():
X = np.zeros((1,1,4,4), dtype=np.float64)
K = np.zeros((1,1,2,2), dtype=np.float64)
with pytest.raises(ValueError):
conv2d_reference(X, K, stride=(0,1))A negatív teszt célja annak ellenőrzése, hogy a függvény hibás bemenetnél nem ad csendes, hamis eredményt. Ez a 14.4. Csoportos és depthwise konvolúció alfejezetben jelzett „csendes elnyelés” ellenpontja.
Magyarázza el, mi a különbség a keresztkorreláció és a matematikai konvolúció között a jelen specifikációban. Adjon meg egy -es, nemszimmetrikus kernelt, és a 4. pontban szereplő -as bemenettel számítsa ki mindkét művelet kimenetét stride 1, padding 0, dilation 1, bias nélkül. Elvárt átadandó: a két definíció képlete, a konkrét mátrixok.
Legyen , kernel , stride , padding , dilation . Vezesse le és értékét az output alakképletből, és sorolja fel azokat a bemeneti indexhármasokat, amelyek a elemhez tartozó összegben nemzérus súllyal járulhatnak hozzá. Adja meg a pontos indexfeltételt.
Töltse le vagy írja be a reference_conv2d.py fájlt,
futtassa a test_reference.py tesztet, és rögzítse a
kimenetet. Ezután egészítse ki egy olyan esettel, ahol
,
batchbeli képek
-esek,
kernel
,
stride
,
padding
,
dilation
,
bias bekapcsolva; a bemenetet
np.random.default_rng(20261020).random(...) generálja. A
teszt csak a shape-t és a dtype-ot ellenőrizze, értéket ne (a random
miatt). Elvárt átadandó: a módosított teszt és a futtatás eredménye.
Az alábbi kódrészlet a forwardot számolja, de hibás:
H_out = (H + 2*p_h - k_h*d_h) // s_h + 1
...
Y[n,o,i,j] += X[n,c, i*s_h + u*d_h - p_h, j*s_w + v*d_w - p_w] * K[o,c,u,v]Keresse meg a hibá(ka)t, magyarázza el a következményt a kimeneti méretre és az indexelésre, javítsa a kódot, és a 4. pont kézi példáján igazolja, hogy a javítás után a várt értékek adódnak. Elvárt átadandó: a hibás sorok megnevezése, a javított sor, és a rövid verifikáció.
Tervezzen kísérletet a conv2d_reference és a PyTorch
torch.nn.functional.conv2d kimenetének összehasonlítására.
A terv tartalmazza: a paraméterrácsot (legalább 6 különböző
konfiguráció, beleértve paddinget, stride-ot, dilationt, több csatornát
és bias nélküli esetet), a bemeneti adat generálását rögzített seeddel,
a numerikus toleranciát, és annak indoklását, miért kell a PyTorch
bemenetet is float64-re konvertálni. Elvárt átadandó: a
protokoll szöveges leírása, a lehetséges buktatók, és legalább egy
konfiguráció kézzel számolt várt kimeneti shape-je.
A keresztkorreláció a kernel megfordítása nélkül:
a konvolúció pedig a megfordított kernellel:
A 4. pont kernelével a keresztkorreláció (bias nélkül):
A konvolúcióhoz a kernel megfordítottja , így
A két eredmény különbözik, mert a kernel nem forgásszimmetrikus.
. Hasonlóan . Tehát térbeli alakja .
A elemhez tartozó összeg a kimeneti oszlop miatt a paddingelt kép oszlopait olvassa, esetén: . A paddingelt szélesség , tehát az oszlopindexek érvényesek ( mind <7). A paddingelt oszlopindexekből az eredeti bemeneti oszlopindexek . A magassági rész: , , paddingelt sorok , ebből eredeti sorindexek . Az első sorindex a padding zónába esik, ezért a hozzájárulás nulla; a ténylegesen használt bemeneti sorok: és . Összefoglalva: adott mellett a lehetséges nemzérus bemeneti pozíciók , feltéve, hogy a megfelelő súly nem nulla.
A módosított teszt:
def test_random_shape_dtype():
rng = np.random.default_rng(20261020)
X = rng.random((2,2,6,7), dtype=np.float64)
K = rng.random((3,2,2,2), dtype=np.float64)
b = rng.random((3,), dtype=np.float64)
Y = conv2d_reference(X, K, b=b, stride=(1,2), padding=(0,1), dilation=(1,2))
assert Y.shape == (2,3,5,3) # H_out: (6+0-1-1)//1+1=5; W_out: (7+2-3-1)//2+1=3
assert Y.dtype == np.float64A pytest futtatás után a teszt sikeres, ha a shape és
dtype illeszkedik. A képlet ellenőrzése:
,
.
Az első hibás sorban a k_h*d_h szerepel a
(k_h-1)*d_h helyett: ez eggyel nagyobb effektív
kernelméretet feltételez, így
túl kicsi lehet. A második sorban a - p_h kivonás jó, de
hiányzik annak ellenőrzése, hogy a kivonás után az index
tartományban van-e; a paddingelt tömb használata nélkül a negatív vagy
-n
túli indexek hibás memóriaterületet vagy Python negatív indexelést
okoznak. A helyes megoldás a paddingelt tömb bevezetése, ahogy a
reference_conv2d.py teszi: X_pad-del
számolunk, és a kernel effektív mérete (k_h-1)*d_h+1.
Javított sorok:
H_out = (H + 2*p_h - (k_h-1)*d_h - 1) // s_h + 1
...
s += X_pad[n, c, i*s_h + u*d_h, j*s_w + v*d_w] * K[o, c, u, v]A kézi példán a javított kód a 4. pontban megadott mátrixot adja, abszolút tűréssel egyezően.
Protokollterv:
F.conv2d azonossága a támogatott részhalmazon.np.random.default_rng(20261020).random(shape)
float64-ben; a PyTorch tenzort
torch.from_numpy(...).double()-ként hozzuk létre.F.conv2d bias
argumentumát külön adjuk át, vagy None-t, a referenciával
összhangban.float64, az eltérés zérus legyen; numerikus pontosságból
adódóan rtol=1e-12, atol=1e-12 elfogadható.np.allclose, a maximális abszolút eltérés rögzítése.float32 numerikus eltérést okoz; a csatornák sorrendje NCHW
mindkettőnél; a dilation a PyTorchban ugyanúgy a kernel pontjai közötti
lépés, nem a stride; a padding szimmetrikus, ezért a PyTorch
padding=(p_h,p_w) formája használandó.Példa shape az (a) esetre: bemenet , kernel , s=1, p=1, d=1, így . A kimenet .
„A referencia implementáció minden Conv2d-opciót
támogat.” Ez hamis. A jelen kód például nem támogat
groups>1-et, aszimmetrikus paddinget, más padding
módokat, output_padding-et vagy a transzponált műveletet.
Ellenpélda: ha a felhasználó groups=2-vel hívná meg, a
függvény nem is fogad ilyen paramétert, és a specifikáció ezt
kifejezetten kizárja. Egy részleges referencia csak a deklarált
tartományon érvényes; aki ennél többet állít, az a 13.4. Határkezelés és frekvenciaintuíció
alfejezetben tárgyalt „csendes elnyeléshez” hasonló hibát követ el, csak
éppen a dokumentáció szintjén.
Önellenőrzés. A hallgató tegye fel magának: (1) a
kernel effektív kiterjedését
-ként
számolta-e, nem
-ként?
(2) A paddingelt tömb indexelésekor a padding hozzáadása után a stride
és dilation sorrendje helyes? (3) A bias dimenziója
,
és a kimenet minden térbeli pozíciójához hozzáadódik? (4) A dtype
mindenütt float64, és a hibás bemenetek kivételt dobnak?
(5) A kimeneti méret képlete a floor függvényt használja,
és a nullánál kisebb kimenet hibát ad?
Visszahívás. A 13.4.
Határkezelés és frekvenciaintuíció alfejezetben a zero padding és a
circular padding közötti különbség azt mutatta, hogy a határfeltétel a
definíció része; itt a zero paddingot explicitté tettük a
bevezetésével. A 14.4. Csoportos és depthwise
konvolúció alfejezetben a groups=1 azt jelentette, hogy
a súly alakja
;
ezt most a specifikáció rögzíti.
A referencia operátor specifikációja az NCHW keresztkorrelációt
egyetlen indexképlettel definiálja: a paddingelt bemenetből stride és
dilation szerint olvasott kernelablakok lineáris kombinációja,
csatornánkénti bias hozzáadásával. A kimeneti méret levezetése a kernel
utolsó pozíciójának illeszkedési feltételéből adódik, és a
floor-képletben összegződik. A támogatott tartományt pontosan
rögzítettük: groups=1, szimmetrikus zero padding, stride, dilation,
opcionális bias, float64. A labor egy nemszimmetrikus
kernelű kézi kontrollt és hibás alakokra negatív teszteket tartalmaz. A
következő egység a backward pontos levezetését kezdi, amelyben ugyanezt
a specifikációt használjuk a súly- és bemeneti gradienshez.