Faza 04 · lecția 02
Convoluții de la zero
Scopul lecției: O convoluție este un strat dens minuscul pe care îl glisați peste o imagine, folosind aceleași ponderi în fiecare poziție.
Versiunea curentă AlexBred.com: primele 100 de lecții ale programului în limba română.
Cuprinsul lecției
- Obiective de învățare
- Problema
- Conceptul
- Un nucleu care glisează
- Formula dimensiunii ieșirii
- Completarea
- Pasul
- Mai multe canale de intrare
- Trucul im2col
- Câmpul receptiv
- Construiți
- Pasul 1: completați un array
- Pasul 2: convoluție 2D cu bucle imbricate
- Pasul 3: verificați cu un nucleu proiectat manual
- Pasul 4: im2col
- Pasul 5: convoluție rapidă prin im2col + înmulțire de matrice
- Pasul 6: o bancă de nuclee proiectate manual
- Utilizați
- Livrați
- Exerciții
- Termeni-cheie
- Lecturi suplimentare
O convoluție este un strat dens minuscul pe care îl glisați peste o imagine, folosind aceleași ponderi în fiecare poziție.
Tip: Construcție Limbaje: Python Cerințe preliminare: Faza 3 (Nucleul învățării profunde), faza 4, lecția 01 (Fundamentele imaginilor) Timp: ~75 de minute
Obiective de învățare
- Implementați convoluția 2D de la zero folosind numai NumPy, inclusiv versiunea cu bucle imbricate și o versiune vectorizată
im2col - Calculați dimensiunea spațială a ieșirii pentru orice combinație între dimensiunea intrării, dimensiunea nucleului, completare și pas și justificați formula
(H - K + 2P) / S + 1 - Proiectați manual nuclee (muchii, estompare, accentuare, Sobel) și explicați de ce fiecare produce tiparul de activări observat
- Stivuiți convoluții într-un extractor de caracteristici și legați adâncimea stivei de dimensiunea câmpului receptiv
Problema
Un strat complet conectat aplicat unei imagini RGB de 224x224 ar necesita 224 * 224 * 3 = 150.528 de ponderi de intrare pentru fiecare neuron. Un singur strat ascuns cu 1.000 de unități are deja 150 de milioane de parametri — înainte de a fi învățat ceva util. Mai rău, acel strat nu are nicio noțiune că un câine din colțul stânga-sus și unul din colțul dreapta-jos reprezintă același tipar. Tratează fiecare poziție de pixel ca fiind independentă, ceea ce este exact abordarea greșită pentru imagini: deplasarea unei pisici cu trei pixeli nu ar trebui să oblige rețeaua să reînvețe conceptul.
Cele două proprietăți de care are nevoie un model pentru imagini sunt echivarianța la translație (ieșirea se deplasează când intrarea se deplasează) și partajarea parametrilor (același detector de caracteristici rulează pretutindeni). Straturile dense nu vă oferă niciuna. Convoluția vi le oferă pe amândouă fără cost suplimentar.
Notă tehnică a traducerii: Echivarianța exactă a unei convoluții discrete se referă la deplasări cu un număr întreg de pixeli și presupune tratarea compatibilă a marginilor. Sub-eșantionarea, inclusiv convoluțiile cu pas mai mare decât 1, poate încălca echivarianța la deplasări arbitrare.
Convoluția nu a fost inventată pentru învățarea profundă. Este aceeași operație care stă la baza compresiei JPEG, a estompării gaussiene din Photoshop, a detectării muchiilor în viziunea industrială și a fiecărui filtru audio livrat vreodată. Motivul pentru care CNN-urile au dominat ImageNet din 2012 până în 2020 este că convoluția oferă biasul inductiv potrivit pentru date în care valorile apropiate sunt corelate, iar același tipar poate apărea oriunde.
Notă tehnică a traducerii: Afirmația despre JPEG este o simplificare incorectă. Codarea JPEG cu pierderi este bazată în principal pe transformata cosinus discretă (DCT) aplicată blocurilor, nu pe aceeași operație de convoluție spațială descrisă în această lecție. Convoluția discretă descrie filtre liniare, invariante în timp (LTI), în special FIR; filtrele audio IIR, neliniare sau variabile în timp nu sunt toate o astfel de convoluție.
Conceptul
Un nucleu care glisează
O convoluție 2D ia o matrice mică de ponderi, numită nucleu (sau filtru), o glisează peste intrare și, în fiecare poziție, calculează suma produselor element cu element. Acea sumă devine un pixel al ieșirii.
Un exemplu concret 3x3 aplicat unei intrări 5x5 (fără completare, pas 1):
Intrarea X (5 x 5): Nucleul W (3 x 3):
1 2 0 1 2 1 0 -1
0 1 3 1 0 2 0 -2
2 1 0 2 1 1 0 -1
1 0 2 1 3
2 1 1 0 1
Nucleul glisează peste fiecare fereastră validă de 3 x 3. Ieșirea Y este 3 x 3:
Y[0,0] = sum( W * X[0:3, 0:3] )
Y[0,1] = sum( W * X[0:3, 1:4] )
Y[0,2] = sum( W * X[0:3, 2:5] )
Y[1,0] = sum( W * X[1:4, 0:3] )
... și așa mai departe
Acea singură formulă — ponderi partajate, localitate, fereastră glisantă — reprezintă întreaga idee. Tot restul ține de gestionarea detaliilor.
Formula dimensiunii ieșirii
Pentru dimensiunea spațială a intrării H, dimensiunea nucleului K, completarea P și pasul S:
H_out = floor( (H - K + 2P) / S ) + 1
Memorați formula. O veți calcula de zeci de ori pentru fiecare arhitectură.
| Scenariu | H | K | P | S | H_out |
|---|---|---|---|---|---|
| Convoluție validă, fără completare | 32 | 3 | 0 | 1 | 30 |
Convoluție same (păstrează dimensiunea) |
32 | 3 | 1 | 1 | 32 |
| Sub-eșantionare cu factorul 2 | 32 | 3 | 1 | 2 | 16 |
| Pooling 2x2 | 32 | 2 | 0 | 2 | 16 |
| Câmp receptiv mare | 32 | 7 | 3 | 2 | 16 |
„Completarea same” înseamnă să alegeți P astfel încât H_out == H când S == 1. Pentru K impar, aceasta este P = (K - 1) / 2. De aceea domină nucleele 3x3 — sunt cele mai mici nuclee impare care au totuși un centru.
Completarea
Fără completare, fiecare convoluție micșorează harta de caracteristici. Stivuiți 20 de astfel de convoluții, iar imaginea de 224x224 devine 184x184, ceea ce irosește calculul efectuat la margine și complică acele conexiuni reziduale care necesită forme identice.
Completare cu zerouri (P = 1) pentru o intrare 5 x 5:
0 0 0 0 0 0 0
0 1 2 0 1 2 0
0 0 1 3 1 0 0
0 2 1 0 2 1 0 Acum nucleul poate fi centrat pe pixelul
0 1 0 2 1 3 0 (0, 0) și poate avea în continuare trei rânduri și
0 2 1 1 0 1 0 trei coloane de valori pentru înmulțire.
0 0 0 0 0 0 0
Moduri pe care le întâlniți în practică: zero (cel mai frecvent), reflect (reflectă marginea și evită contururile dure în modelele generative), replicate (copiază marginea), circular (revine la capătul opus, folosit în probleme toroidale).
Pasul
Pasul (stride) este dimensiunea deplasării la fiecare glisare. stride=1 este valoarea implicită. stride=2 înjumătățește dimensiunile spațiale și reprezintă metoda clasică de sub-eșantionare într-un CNN fără un strat separat de pooling — fiecare arhitectură modernă (ResNet, ConvNeXt, MobileNet) folosește undeva convoluții cu pas în locul max-poolingului.
Pas 1 pentru o intrare 5 x 5 și un nucleu 3 x 3:
începuturi: (0,0) (0,1) (0,2) -> rândul 0 al ieșirii
(1,0) (1,1) (1,2) -> rândul 1 al ieșirii
(2,0) (2,1) (2,2) -> rândul 2 al ieșirii
Ieșire: 3 x 3
Pas 2 pentru aceeași intrare:
începuturi: (0,0) (0,2) -> rândul 0 al ieșirii
(2,0) (2,2) -> rândul 1 al ieșirii
Ieșire: 2 x 2
Mai multe canale de intrare
Imaginile reale au trei canale. O convoluție 3x3 aplicată unei intrări RGB este, de fapt, un volum 3x3x3: câte o secțiune 3x3 pentru fiecare canal de intrare. În fiecare poziție spațială, înmulțiți și însumați valorile din toate cele trei secțiuni, apoi adăugați un bias.
Intrare: (C_in, H, W) 3 x 5 x 5
Nucleu: (C_in, K, K) 3 x 3 x 3 (un nucleu)
Ieșire: (1, H', W') hartă 2D
Pentru un strat care produce C_out canale de ieșire, stivuiți C_out nuclee:
Pondere: (C_out, C_in, K, K) de exemplu, 64 x 3 x 3 x 3
Ieșire: (C_out, H', W') 64 x 3 x 3
Număr de parametri: C_out * C_in * K * K + C_out (+ C_out reprezintă biasurile)
Ultima linie este cea pe care o veți calcula când planificați un model. O convoluție 3x3 cu 64 de canale, aplicată unei intrări cu 3 canale, are 64 * 3 * 3 * 3 + 64 = 1.792 de parametri. Puțini.
Trucul im2col
Buclele imbricate sunt ușor de citit, dar lente. GPU-urile preferă înmulțiri de matrice mari. Iată trucul: aplatizați fiecare fereastră a câmpului receptiv din intrare într-o coloană a unei matrice mari, aplatizați nucleul într-un rând, iar întreaga convoluție devine o singură înmulțire de matrice.
Fiecare implementare de producție a convoluției este o variantă a acestei metode, la care se adaugă trucuri de împărțire în dale pentru cache (convoluție directă, Winograd, convoluție FFT pentru nuclee mari). Dacă înțelegeți im2col, înțelegeți mecanismul de bază.
Notă tehnică a traducerii:
im2colurmat de GEMM este o implementare importantă, dar nu una universală. Bibliotecile de producție folosesc și algoritmi direct/implicit GEMM, FFT și Winograd; abordarea implicit GEMM nici măcar nu materializează neapărat matricea de coloane.
Câmpul receptiv
O singură convoluție 3x3 examinează 9 pixeli de intrare. Stivuiți două convoluții 3x3, iar un neuron din al doilea strat examinează 5x5 pixeli de intrare. Trei convoluții 3x3 oferă 7x7. În general:
CR după L convoluții K x K stivuite (pas 1) = 1 + L * (K - 1)
Cu pași: CR crește multiplicativ cu pasul de-a lungul fiecărui strat.
Notă tehnică a traducerii: Pentru pași diferiți, relația exactă este recurentă: dacă
j_lreprezintă saltul dintre pozițiile receptive ale stratuluil, atuncij_l = j_(l-1) * S_l, iarRF_l = RF_(l-1) + (K_l - 1) * j_(l-1). Prin urmare, câmpul receptiv nu se obține doar înmulțind dimensiunea sa cu pasul fiecărui strat.
Întregul motiv pentru care funcționează abordarea „3x3 până la capăt” (VGG, ResNet, ConvNeXt) este că două convoluții 3x3 văd aceeași zonă de intrare ca o convoluție 5x5, dar au mai puțini parametri și o neliniaritate suplimentară între ele.
convolution-kernel
Construiți
Pasul 1: completați un array
Începeți cu cea mai mică primitivă: o funcție care adaugă zerouri în jurul unui array H x W.
import numpy as np
def pad2d(x, p):
if p == 0:
return x
h, w = x.shape[-2:]
out = np.zeros(x.shape[:-2] + (h + 2 * p, w + 2 * p), dtype=x.dtype)
out[..., p:p + h, p:p + w] = x
return out
x = np.arange(9).reshape(3, 3)
print(x)
print()
print(pad2d(x, 1))
Trucul axelor finale x.shape[:-2] înseamnă că aceeași funcție lucrează cu (H, W), (C, H, W) sau (N, C, H, W) fără modificări.
Pasul 2: convoluție 2D cu bucle imbricate
Implementarea de referință — lentă, dar lipsită de ambiguități. Aceasta este, în principiu, operația efectuată de torch.nn.functional.conv2d.
def conv2d_naive(x, w, b=None, stride=1, padding=0):
c_in, h, w_in = x.shape
c_out, c_in_w, kh, kw = w.shape
assert c_in == c_in_w
x_pad = pad2d(x, padding)
h_out = (h + 2 * padding - kh) // stride + 1
w_out = (w_in + 2 * padding - kw) // stride + 1
out = np.zeros((c_out, h_out, w_out), dtype=np.float32)
for oc in range(c_out):
for i in range(h_out):
for j in range(w_out):
hs = i * stride
ws = j * stride
patch = x_pad[:, hs:hs + kh, ws:ws + kw]
out[oc, i, j] = np.sum(patch * w[oc])
if b is not None:
out[oc] += b[oc]
return out
Patru bucle imbricate (canalul de ieșire, rândul, coloana, plus suma implicită peste C_in, kh, kw). Acesta este adevărul de referință (ground truth) cu care veți compara fiecare implementare mai rapidă.
Pasul 3: verificați cu un nucleu proiectat manual
Construiți un nucleu Sobel vertical, aplicați-l unei imagini sintetice cu salt și observați cum se evidențiază muchia verticală.
def synthetic_step_image():
img = np.zeros((1, 16, 16), dtype=np.float32)
img[:, :, 8:] = 1.0
return img
sobel_x = np.array([
[[-1, 0, 1],
[-2, 0, 2],
[-1, 0, 1]]
], dtype=np.float32)[None]
x = synthetic_step_image()
y = conv2d_naive(x, sobel_x, padding=1)
print(y[0].round(1))
Așteptați-vă la valori pozitive mari pe coloana 7 (creșterea luminozității de la stânga la dreapta) și la zerouri peste tot în rest. Această singură afișare este verificarea de bun-simț care confirmă corectitudinea calculelor.
Notă tehnică a traducerii: Cu indexare de la zero, codul afișează răspunsuri pozitive pe coloanele 7 și 8, deoarece nucleul 3x3 se suprapune peste salt în ambele poziții. Completarea cu zerouri produce și un răspuns negativ pe coloana 15, la marginea dreaptă a imaginii; valorile de pe primul și ultimul rând au magnitudine mai mică din cauza aceleiași completări.
Pasul 4: im2col
Transformați fiecare fereastră din intrare, cu dimensiunea nucleului, într-o coloană a unei matrice. Pentru C_in=3, K=3, fiecare coloană conține 27 de numere.
def im2col(x, kh, kw, stride=1, padding=0):
c_in, h, w = x.shape
x_pad = pad2d(x, padding)
h_out = (h + 2 * padding - kh) // stride + 1
w_out = (w + 2 * padding - kw) // stride + 1
cols = np.zeros((c_in * kh * kw, h_out * w_out), dtype=x.dtype)
col = 0
for i in range(h_out):
for j in range(w_out):
hs = i * stride
ws = j * stride
patch = x_pad[:, hs:hs + kh, ws:ws + kw]
cols[:, col] = patch.reshape(-1)
col += 1
return cols, h_out, w_out
Există încă o buclă Python, dar acum calculul dificil va fi efectuat printr-o singură înmulțire de matrice vectorizată.
Pasul 5: convoluție rapidă prin im2col + înmulțire de matrice
Înlocuiți bucla cvadruplă cu o singură înmulțire de matrice.
def conv2d_im2col(x, w, b=None, stride=1, padding=0):
c_out, c_in, kh, kw = w.shape
cols, h_out, w_out = im2col(x, kh, kw, stride, padding)
w_flat = w.reshape(c_out, -1)
out = w_flat @ cols
if b is not None:
out += b[:, None]
return out.reshape(c_out, h_out, w_out)
Verificarea corectitudinii: rulați ambele implementări și comparați-le.
rng = np.random.default_rng(0)
x = rng.normal(0, 1, (3, 16, 16)).astype(np.float32)
w = rng.normal(0, 1, (8, 3, 3, 3)).astype(np.float32)
b = rng.normal(0, 1, (8,)).astype(np.float32)
y_naive = conv2d_naive(x, w, b, padding=1)
y_im2col = conv2d_im2col(x, w, b, padding=1)
print(f"max abs diff: {np.max(np.abs(y_naive - y_im2col)):.2e}")
max abs diff ar trebui să fie aproximativ 1e-5 — diferența provine din ordinea acumulării în virgulă mobilă, nu dintr-o eroare.
Pasul 6: o bancă de nuclee proiectate manual
Cinci filtre care arată ce poate exprima un singur strat convoluțional înaintea oricărei antrenări.
KERNELS = {
"identity": np.array([[0, 0, 0], [0, 1, 0], [0, 0, 0]], dtype=np.float32),
"blur_3x3": np.ones((3, 3), dtype=np.float32) / 9.0,
"sharpen": np.array([[0, -1, 0], [-1, 5, -1], [0, -1, 0]], dtype=np.float32),
"sobel_x": np.array([[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]], dtype=np.float32),
"sobel_y": np.array([[-1, -2, -1], [0, 0, 0], [1, 2, 1]], dtype=np.float32),
}
def apply_kernel(img2d, kernel):
x = img2d[None].astype(np.float32)
w = kernel[None, None]
return conv2d_im2col(x, w, padding=1)[0]
Aplicate oricărei imagini în tonuri de gri, estomparea netezește, accentuarea face muchiile mai clare, Sobel-x evidențiază muchiile verticale, iar Sobel-y pe cele orizontale. Acestea sunt exact tiparele pe care primul strat convoluțional antrenat din AlexNet și VGG a ajuns să le învețe — deoarece un model de imagini bun are nevoie de detectoare de muchii și pete indiferent de sarcina care urmează.
Utilizați
nn.Conv2d din PyTorch încapsulează aceeași operație și adaugă autograd, nuclee CUDA și optimizare cu cuDNN. Semantica formelor este identică.
import torch
import torch.nn as nn
conv = nn.Conv2d(in_channels=3, out_channels=64, kernel_size=3, stride=1, padding=1)
print(conv)
print(f"weight shape: {tuple(conv.weight.shape)} # (C_out, C_in, K, K)")
print(f"bias shape: {tuple(conv.bias.shape)}")
print(f"param count: {sum(p.numel() for p in conv.parameters())}")
x = torch.randn(8, 3, 224, 224)
y = conv(x)
print(f"\ninput shape: {tuple(x.shape)}")
print(f"output shape: {tuple(y.shape)}")
Înlocuiți padding=1 cu padding=0, iar ieșirea scade la 222x222. Înlocuiți stride=1 cu stride=2, iar aceasta scade la 112x112. Este aceeași formulă pe care ați memorat-o mai sus.
Livrați
Această lecție produce:
outputs/prompt-cnn-architect.md— un prompt care, primind dimensiunea intrării, bugetul de parametri și câmpul receptiv țintă, proiectează o stivă de straturiConv2dcu valorile K/S/P corecte la fiecare pas.outputs/skill-conv-shape-calculator.md— o abilitate care parcurge specificația unei rețele strat cu strat și întoarce forma ieșirii, câmpul receptiv și numărul de parametri pentru fiecare bloc.
Exerciții
- (Ușor) Pentru o intrare în tonuri de gri de 128x128 și o stivă
[Conv3x3(s=1,p=1), Conv3x3(s=2,p=1), Conv3x3(s=1,p=1), Conv3x3(s=2,p=1)], calculați manual dimensiunea spațială a ieșirii și câmpul receptiv la fiecare strat. Verificați folosind unnn.SequentialPyTorch cu convoluții fictive. - (Mediu) Extindeți
conv2d_naiveșiconv2d_im2colpentru a accepta un argumentgroups. Arătați căgroups=C_in=C_outreproduce o convoluție depthwise și că numărul său de parametri esteC * K * Kîn loc deC * C * K * K. - (Dificil) Implementați manual propagarea înapoi pentru
conv2d_im2col: primind gradientul ieșirii, calculați gradientul luixși al luiw. Verificați față detorch.autograd.gradpe aceleași intrări și ponderi. Trucul: gradientul luiim2colestecol2im, iar acesta trebuie să acumuleze ferestrele suprapuse.
Termeni-cheie
| Termen | Cum i se spune | Ce înseamnă de fapt |
|---|---|---|
| Convoluție | „Glisarea unui filtru” | Un produs scalar antrenabil aplicat în fiecare poziție spațială cu ponderi partajate; din punct de vedere matematic, este o corelație încrucișată, dar toată lumea îi spune convoluție |
| Nucleu / filtru | „Detectorul de caracteristici” | Un tensor mic de ponderi, cu forma (C_in, K, K), al cărui produs scalar cu o fereastră a intrării produce un pixel al ieșirii |
| Pas | „Cât de departe săriți” | Dimensiunea deplasării între poziționări consecutive ale nucleului; pasul 2 înjumătățește fiecare dimensiune spațială |
| Completare | „Zerouri pe margini” | Valori suplimentare adăugate în jurul intrării pentru ca nucleul să poată fi centrat pe pixelii de margine; completarea same păstrează dimensiunea ieșirii egală cu cea a intrării |
| Câmp receptiv | „Cât vede neuronul” | Patch-ul intrării originale de care depinde o anumită activare de ieșire și care crește odată cu adâncimea și pasul |
| im2col | „Trucul GEMM” | Rearanjarea fiecărei ferestre receptive în coloane, astfel încât convoluția să devină o singură înmulțire mare de matrice — mecanismul central al multor nuclee convoluționale rapide |
| Convoluție depthwise | „Un nucleu per canal” | O convoluție cu groups == C_in, care calculează fiecare canal de ieșire numai din canalul de intrare corespunzător; coloana vertebrală a MobileNet și ConvNeXt |
| Echivarianță la translație | „Deplasați intrarea, se deplasează ieșirea” | Proprietatea prin care deplasarea intrării cu k pixeli deplasează ieșirea cu k pixeli; rezultă din ponderile partajate, în condițiile precizate mai sus |
Lecturi suplimentare
- A guide to convolution arithmetic for deep learning (Dumoulin & Visin, 2016) — diagramele de referință pentru completare, pas și dilatare, pe care fiecare curs le preia discret
- CS231n: Convolutional Neural Networks for Visual Recognition — notițele de curs canonice, inclusiv explicația originală a metodei
im2col - The Annotated ConvNet (fast.ai) — un notebook care vă conduce de la convoluția manuală la un clasificator de cifre antrenat
- Receptive Field Arithmetic for CNNs (Dang Ha The Hien) — o explicație interactivă, la nivelul unei lucrări științifice, a calculelor câmpului receptiv
Sursă: Originalul în limba engleză
Navigare: ← Lecția 04.01 — Fundamentele imaginilor — pixeli, canale și spații de culoare · Faza 4 — Viziune computerizată · Catalog complet · Lecția 04.03 — CNN-uri — de la LeNet la ResNet →