CNN: Rețele convoluționale de la zero la producție
Cum „vede” un computer o pisică într-o fotografie? Cum deosebești un semn de circulație de o față umană? Răspunsul constă în Rețele neuronale convoluționale (CNN), arhitectura de deep learning care a revoluționat viziunea computerizată. De la mașini cu conducere autonomă până la diagnosticarea imagistică medicală, CNN-urile sunt motorul invizibil din spatele milioanelor de aplicații pe care le folosim în fiecare zi.
În acest prim articol al seriei Viziune pe computer cu învățare profundă, vom construi înțelegerea dvs. despre CNN-urile de la zero: ce sunt acestea, cum funcționează filtrele convoluționale, care arhitecturi au făcut istorie și cum să implementați și să instruiți un CNN complet în PyTorch. La finalizare, veți avea abilitățile de a clasifica imagini reale și de a aduce modelul în producție.
Prezentare generală a seriei
| # | Articol | Concentrează-te |
|---|---|---|
| 1 | Sunteți aici - CNN: Rețele convoluționale | Arhitectură, instruire, implementare |
| 2 | Transfer de învățare și reglare fină | Modele pre-antrenate, adaptare domeniului |
| 3 | Detectarea obiectelor cu YOLO | Detectarea obiectelor în timp real |
| 4 | Segmentarea semantică | Clasificare la nivel de pixeli |
| 5 | Generare de imagini cu GAN și difuzie | Generarea de imagini sintetice |
| 6 | Implementare și optimizare Edge | Modele pe dispozitive încorporate |
Ce vei învăța
- Cum reprezintă un computer imaginile (pixeli, canale RGB, tensori)
- Operația de convoluție: nucleu, hărți de caracteristici, fereastră glisantă
- Componentele fundamentale ale unui CNN: convoluții, pooling, activări
- Evoluția arhitecturilor: de la LeNet-5 la ConvNeXt
- ResNet și ignorați conexiunile: cum să rezolvați gradientul care dispare
- Implementare completă în PyTorch: CNN pentru clasificarea CIFAR-10
- Transfer de învățare: reutilizarea modelelor pre-instruite pe ImageNet
- Măsuri de evaluare: acuratețe, precizie, reamintire, matrice de confuzie
- Implementare: de la model antrenat la inferență de producție (ONNX, TorchScript)
1. De la pixeli la caracteristici: cum vede un computer imaginile
Pentru noi, oamenii, a privi o fotografie este un act firesc. Creierul nostru recunoaște instantaneu forme, culori, contururi și obiecte. Dar pentru un computer, o imagine este doar o grilă de numere. Fiecare pixel este reprezentat de valori numerice care indică intensitatea luminii.
1.1 O imagine ca o matrice de numere
O imagine în tonuri de gri este o matrice 2D în care fiecare celulă conține o valoare între 0 (negru)
și 255 (alb). O imagine color este formată din trei canale (rosu, verde, albastru),
fiecare o matrice separată. O imagine color de 224x224 este, prin urmare, un tensor de dimensiune
3 x 224 x 224, sau 150.528 de valori numerice.
Immagine 4x4 (scala di grigi, valori 0-255):
+-----+-----+-----+-----+
| 10 | 20 | 30 | 40 |
+-----+-----+-----+-----+
| 50 | 120 | 180 | 60 |
+-----+-----+-----+-----+
| 70 | 200 | 220 | 80 |
+-----+-----+-----+-----+
| 90 | 100 | 110 | 130 |
+-----+-----+-----+-----+
Immagine a colori (3 canali RGB):
Canale R: [[10, 20, ...], ...] --> tonalita di rosso
Canale G: [[30, 40, ...], ...] --> tonalita di verde
Canale B: [[50, 60, ...], ...] --> tonalita di blu
Tensore finale: shape = (3, H, W) --> (canali, altezza, larghezza)
1.2 de ce rețelele neuronale dense nu sunt suficiente
Prima abordare care mi-ar veni în minte este să aplatizați imaginea într-un vector 1D și să o treceți prin la o rețea neuronală complet conectată (densă). Pentru o imagine de 224x224x3, asta ar însemna un strat de intrare cu 150.528 de neuroni. Cu un strat ascuns de 1.000 de neuroni, ai avea deja 150 de milioane de parametri numai în primul strat.
Probleme ale rețelelor dense pentru imagini
- Parametru explozie: Milioane de greutăți deja la primul strat, prohibitive din punct de vedere computațional
- Fără invarianță spațială: Dacă o pisică se mișcă cu 10 pixeli spre dreapta, rețeaua nu o mai recunoaște
- Pierderea structurii 2D: Aplatizarea imaginii distruge relațiile spațiale dintre pixelii vecini
- Supramontare: Prea mulți parametri cu prea puține date duc la memorare, nu la generalizare
CNN-urile rezolvă toate aceste probleme valorificând trei perspective cheie: localitate (modele vizuale sunt locale), împărțirea poverii (același filtru funcționează peste tot în imagine) e invarianta de traducere (o muchie este o muchie indiferent de locul în care se află).
2. Operația de convoluție
La convoluţie este operația matematică din inima CNN-urilor. Un mic filtru (a spus nucleu) derulează peste imaginea de intrare, calculând o sumă la fiecare poziție ponderarea pixelilor acoperiți. Rezultatul este o nouă matrice numită hărți de caracteristici, care evidențiază un model specific (margine verticală, margine orizontală, colț, textură).
2.1 Kernel și fereastră glisantă
Un nucleu este o matrice mică de greutăți (de obicei 3x3 sau 5x5) care este derulată (diapozitive) pe întreaga imagine de intrare. La fiecare poziţie, valorile nucleului sunt înmulțit element cu element cu pixelii de bază și adunați împreună pentru a produce un singur valoare în harta caracteristicilor de ieșire.
Input (5x5): Kernel (3x3):
+---+---+---+---+---+ +----+----+----+
| 1 | 2 | 3 | 0 | 1 | | -1 | 0 | 1 |
+---+---+---+---+---+ +----+----+----+
| 0 | 1 | 2 | 3 | 1 | | -1 | 0 | 1 |
+---+---+---+---+---+ +----+----+----+
| 1 | 0 | 1 | 2 | 0 | | -1 | 0 | 1 |
+---+---+---+---+---+ +----+----+----+
| 2 | 1 | 0 | 1 | 3 | (Filtro per bordi verticali)
+---+---+---+---+---+
| 0 | 1 | 2 | 1 | 0 |
+---+---+---+---+---+
Posizione (0,0): applica kernel ai pixel evidenziati [*]
[*1][*2][*3] 0 1
[*0][*1][*2] 3 1 Calcolo:
[*1][*0][*1] 2 0 (-1x1)+(0x2)+(1x3)+(-1x0)+(0x1)+(1x2)+(-1x1)+(0x0)+(1x1)
2 1 0 1 3 = -1 + 0 + 3 + 0 + 0 + 2 - 1 + 0 + 1 = 4
0 1 2 1 0
Output feature map (3x3):
+---+---+---+
| 4 | . | . | <-- Il 4 appena calcolato
+---+---+---+
| . | . | . | Il kernel scorre e calcola ogni cella
+---+---+---+
| . | . | . |
+---+---+---+
Tipuri clasice de kernel
| Nucleu | Domeniul de aplicare | Valori (3x3) |
|---|---|---|
| Chenar vertical | Detectează tranzițiile verticale | [-1, 0, 1] repetat |
| Chenar orizontal | Detectează tranzițiile orizontale | [-1,-1,-1], [0,0,0], [1,1,1] |
| Ascutire | Mărește claritatea | [0,-1,0], [-1,5,-1], [0,-1,0] |
| estompare gaussiană | estompare gaussiană | [1,2,1], [2,4,2], [1,2,1] / 16 |
Diferența cheie dintre CNN-uri și procesarea tradițională a imaginilor este aceea pe CNN-uri valorile nucleului nu sunt stabilite manual. Rețeaua învață automat filtrele optime în timpul antrenamentului prin retropropagare. Primele straturi învață să detecteze marginile și texturile simple, în timp ce straturile cele mai profunde combină aceste caracteristici în modele din ce în ce mai complexe (ochi, roți, fețe).
3. Componentele unui CNN
Un CNN este format din diferite tipuri de straturi, fiecare cu un rol specific. Înțelegerea a ceea ce face fiecare componentă este esențială pentru proiectarea unor arhitecturi eficiente.
3.1 Stratul convoluțional
Stratul convoluțional aplică mai multe filtre la intrare, producând o hartă de caracteristici pentru fiecare filtru. Dacă aplicați 32 de filtre 3x3 unei imagini RGB, obțineți 32 de hărți de caracteristici, fiecare evidențiind un model diferit. Parametrii cheie sunt:
Parametrii stratului convoluțional
| Parametru | Descriere | Valori tipice |
|---|---|---|
| Dimensiunea nucleului | Dimensiunea filtrului (lățime x înălțime) | 3x3, 5x5, 7x7 |
| Pas | Pasul de defilare a filtrului | 1, 2 |
| Captuseala | Pixeli adăugați la marginile intrării | 0 (valid), 1 (la fel pentru 3x3) |
| Numărul de filtre | Câte hărți de caracteristici în ieșire | 32, 64, 128, 256, 512 |
Formula dimensione output:
output_size = (input_size - kernel_size + 2 * padding) / stride + 1
Esempio: input 32x32, kernel 3x3
Stride=1, Padding=0: (32 - 3 + 0) / 1 + 1 = 30x30 (si riduce)
Stride=1, Padding=1: (32 - 3 + 2) / 1 + 1 = 32x32 (same padding)
Stride=2, Padding=1: (32 - 3 + 2) / 2 + 1 = 16x16 (dimezza)
Stride=1, Padding=1 ("same"):
Input: [A B C D E] Output: [A B C D E] --> stessa dimensione
(con zero-padding ai bordi)
Stride=2 (dimezza la risoluzione):
Input: [A B C D E F] Output: [A C E] --> meta dimensione
(salta un pixel ad ogni passo)
3.2 Funcția de activare (ReLU)
După fiecare convoluție se aplică o funcție de activare neliniară. Cel mai comun
este acolo ReLU (Unitate liniară rectificată): f(x) = max(0, x).
ReLU resetează toate valorile negative și le lasă neschimbate pe cele pozitive. Fără neliniaritate,
o succesiune de convoluții ar fi echivalentă cu o singură transformare liniară,
făcând rețeaua incapabilă să învețe tipare complexe.
ReLU: f(x) = max(0, x) Semplice, veloce, standard
LeakyReLU: f(x) = x se x>0, 0.01x altrimenti Evita "dying ReLU"
GELU: f(x) = x * Phi(x) Usata nei Transformers, smooth
Swish: f(x) = x * sigmoid(x) Usata in EfficientNet
Input feature map: Dopo ReLU:
+----+-----+----+ +----+----+----+
| -3 | 5 | -1 | | 0 | 5 | 0 |
+----+-----+----+ +----+----+----+
| 2 | -7 | 4 | | 2 | 0 | 4 |
+----+-----+----+ +----+----+----+
| -2 | 1 | -5 | | 0 | 1 | 0 |
+----+-----+----+ +----+----+----+
3.3 Straturi de grupare
Gruparea reduce dimensiunile spațiale ale hărților de caracteristici, scăzând numărul de parametri și făcând rețeaua mai robustă la mici modificări ale poziției caracteristicilor. Cele două tipuri principalele sunt Pooling maxim (ia valoarea maximă în fiecare fereastră) e Pooling mediu (calculați media).
Input (4x4): Output (2x2):
+----+----+----+----+ +----+----+
| 12 | 20| 30| 0 | | 20 | 30 | max(12,20,0,8)=20
+----+----+----+----+ --> +----+----+ max(30,0,2,14)=30
| 0 | 8 | 2 | 14 | | 15 | 16 |
+----+----+----+----+ +----+----+
| 15 | 2 | 3 | 16 |
+----+----+----+----+ Riduce 4x4 --> 2x2
| 1 | 6 | 7 | 8 | (dimezza altezza e larghezza)
+----+----+----+----+
Max Pooling: prende il massimo --> preserva le feature più forti
Avg Pooling: calcola la media --> effetto di smoothing
Global Average Pooling: media su tutta la feature map --> un singolo valore per canale
3.4 Normalizarea loturilor
La Normalizare lot (BatchNorm) normalizează rezultatul fiecărui strat astfel încât să aibă medie zero și varianță unitară. Acest lucru stabilizează antrenamentul, permite rate de învățare mai mari și acționează ca un regulator de lumină. În practică, se potrivește un strat BatchNorm după fiecare convoluție, înainte de activare.
4. Arhitectură tipică CNN
Un CNN standard urmează un model recurent: blocuri de extracție a caracteristicilor (convoluție + activare + pooling) urmat de straturi complet conectate pentru clasificarea finală. Odată cu adâncimea, hărțile de caracteristici scad în dimensiune spațială, dar cresc în număr de canale, captând modele din ce în ce mai abstracte.
Input Immagine (3 x 32 x 32)
|
v
[Conv2d 3->32, 3x3, pad=1] --> [BatchNorm] --> [ReLU] --> [MaxPool 2x2]
| Feature maps: 32 x 16 x 16
v
[Conv2d 32->64, 3x3, pad=1] --> [BatchNorm] --> [ReLU] --> [MaxPool 2x2]
| Feature maps: 64 x 8 x 8
v
[Conv2d 64->128, 3x3, pad=1] --> [BatchNorm] --> [ReLU] --> [MaxPool 2x2]
| Feature maps: 128 x 4 x 4
v
[Flatten] --> Vettore di 128 * 4 * 4 = 2048 valori
|
v
[Linear 2048 -> 256] --> [ReLU] --> [Dropout 0.5]
|
v
[Linear 256 -> 10] --> Output: 10 classi (es. CIFAR-10)
|
v
[Softmax] --> Probabilità per ogni classe: [0.02, 0.01, 0.85, ...]
Flusso delle dimensioni:
(3, 32, 32) -> (32, 16, 16) -> (64, 8, 8) -> (128, 4, 4) -> (2048) -> (256) -> (10)
[immagine] [bordi, texture] [parti] [oggetti] [decisione] [classe]
Ierarhia caracteristicilor învățate
- Stratul 1-2 (nivel scăzut): Borduri, degrade de culoare, texturi simple
- Stratul 3-5 (nivel mediu): Colțuri, contururi, părți ale obiectelor (ochi, roți)
- Stratul 6+ (nivel înalt): Obiecte complete, scene, concepte abstracte
Această ierarhie apare automat în timpul antrenamentului. Nu e nevoie pentru a spune rețelei ce să caute: filtrele se adaptează la date.
5. Evoluția arhitecturilor CNN
Istoria CNN-urilor este marcată de arhitecturi revoluționare, fiecare dintre ele introdusă idei care au schimbat domeniul. Cunoașterea acestei evoluții este fundamentală pentru înțelegere alegeri arhitecturale moderne.
Cronologia arhitecturii CNN
| An | Arhitectură | Inovație cheie | Top-1 ImageNet |
|---|---|---|---|
| 1998 | LeNet-5 | Primul CNN practic (recunoaștere cifre) | N / A |
| 2012 | AlexNet | Antrenament GPU, ReLU, abandon | 63,3% |
| 2014 | VGGNet | Rețele profunde cu filtre uniforme 3x3 | 74,5% |
| 2014 | GoogleLeNet/Inception | Module de inițiere, paralele multi-scale | 74,8% |
| 2015 | ResNet | Omite conexiuni, peste 152 de rețele de nivel | 78,6% |
| 2019 | EfficientNet | Scalare compusă (adâncime+lățime+rezoluție) | 84,4% |
| 2022 | ConvNext | CNN modernizat, inspirat de Vision Transformers | 87,8% |
5.1 LeNet-5 (1998) - Pionierul
Proiectat de Yann LeCun pentru recunoașterea cifrelor scrise de mână (MNIST), LeNet-5 și primul CNN care a avut practic succes. Cu doar 5 straturi și 60.000 de parametri, s-a dovedit că circumvoluțiile ar putea învăța trăsături discriminatorii din imagini. A fost folosit pentru citește automat cecurile bancare.
5.2 AlexNet (2012) - Revoluția
AlexNet a câștigat competiția ImageNet 2012 cu o marjă uriașă, reducând eroarea de la 26% la 16%. Inovațiile cheie: antrenament GPU (două NVIDIA GTX 580), funcție Activare ReLU în loc de sigmoid, Dropout pentru regularizare și creșterea datelor. Acest rezultat a convins mediul academic și industria că învățarea profundă a funcționat.
5.3 VGGNet (2014) - Profunzimea contează
VGG a demonstrat că rețelele mai profunde produc rezultate mai bune. Ideea lui cheie și radical în simplitate: utilizați numai filtre stivuite 3x3. Două straturi consecutive de 3x3 au același câmp receptiv ca un singur strat de 5x5, dar cu parametri din ce în ce mai puțini neliniaritate. VGG-16 are 16 straturi și 138 de milioane de parametri.
5.4 EfficientNet (2019) - Scalare inteligentă
EfficientNet a introdus scalare compusă: în loc să crească doar adâncimea (cum ar fi VGG) sau lățimea, scalează toate cele trei dimensiuni în mod uniform (adâncime, lățime, rezoluție de intrare) cu coeficienți echilibrați. EfficientNet-B0 atinge o precizie de 77,1% pe ImageNet cu doar 5,3 milioane de parametri, un raport parametri de precizie fără precedent.
5.5 ConvNeXt (2022) - CNN modernizat
ConvNeXt demonstrează că CNN-urile, modernizate cu tehnici inspirate de Vision Transformers, pot concura cu (și depăși) arhitecturile transformatoarelor. Inovațiile includ: Miezuri separabile în adâncime 7x7, LayerNorm în loc de BatchNorm, activare GELU, și un design „izomorf” cu etape de mărime crescândă. ConvNeXt V2, în versiune E-ConvNeXt-Tiny, atinge 80,6% Top-1 cu doar 2,0 GFLOP, excelent pentru implementare eficient.
6. ResNet și Skip Connections
ResNet (rețele reziduale), propus de He et al. in 2015, s-a rezolvat una dintre problemele fundamentale ale învățării profunde: cel problema de degradare. Înainte de ResNet, adăugarea de straturi la o rețea profundă a făcut ca rezultatele să fie mai proaste decât mai bune, chiar și pe platoul de antrenament. Soluția este pe cât de elegantă, pe atât de simplă.
6.1 Problema gradientului care dispare
În timpul propagării inverse, gradienții sunt înmulțiți în mod repetat straturile rețelei. Dacă aceste înmulțiri produc valori mai mici de 1, gradienții ele „se estompează” exponențial pe măsură ce se propagă spre straturile inițiale. Cu 50 sau mai multe straturi, gradienții devin atât de mici încât primele straturi se opresc a invata. Studii recente confirmă că, fără săriți conexiuni, L2 a gradienților scad drastic în straturile inițiale, în timp ce cu conexiunile de salt rămân uniforme în întreaga rețea.
Rete profonda SENZA skip connections (50 layer):
Layer 50 Layer 49 Layer 48 ... Layer 2 Layer 1
grad=1.0 * 0.8 * 0.8 ... * 0.8 * 0.8
Gradiente al layer 1: 0.8^49 = 0.00001 --> quasi zero!
I primi layer NON apprendono.
Rete profonda CON skip connections (ResNet):
Il gradiente ha un "percorso diretto" attraverso le skip connections.
Non viene moltiplicato ripetutamente per valori piccoli.
Gradiente al layer 1: ~0.5 --> i layer apprendono normalmente!
6.2 Soluția: Învățare reziduală
Ideea genială a ResNet este simplă: în loc să faci un bloc, învață transformarea
completă H(x), îl faci să învețe doar pe diferenţă (rezidual)
în ceea ce privește intrarea: F(x) = H(x) - x. Ieșirea blocului devine
y = F(x) + x, Unde x este intrarea care „sare” blocul
unul omite conexiunea (sau conexiune rapidă).
Blocco Standard: Residual Block:
x x ------+
| | |
v v | (skip connection)
[Conv 3x3] [Conv 3x3] |
| | |
[BatchNorm] [BatchNorm] |
| | |
[ReLU] [ReLU] |
| | |
[Conv 3x3] [Conv 3x3] |
| | |
[BatchNorm] [BatchNorm] |
| | |
v v |
H(x) = output F(x) + x <--+
|
[ReLU]
|
v
output
Se F(x) = 0, l'output è semplicemente x (identità).
La rete può "saltare" un blocco se non serve.
Questo rende il training di reti profonde stabile.
pentru că funcționează
Învățarea reziduului F(x) = 0 (adică „nu face nimic”) este mult mai ușor
decât a învăţa o transformare completă a identităţii. Dacă un strat nu este util, rețeaua
pur si simplu invata F(x) = 0 și trece intrarea neschimbată. Acest lucru permite
pentru a construi rețele cu sute de straturi fără degradarea performanței.
7. Antrenarea unui CNN
Antrenarea unui CNN înseamnă găsirea valorilor optime pentru toate nucleele (filtre) și greutățile straturilor complet conectate. Acest lucru se întâmplă printr-un proces iterativ trecerea înainte, calculul pierderilor și propagarea inversă a gradientului.
7.1 Funcția de pierdere
Pentru clasificarea imaginilor, funcția standard de pierdere este Pierdere de entropie încrucișată. Măsoară cât de mult se abate probabilitățile prezise de rețea de la etichetele reale. O predicție perfectă produce pierdere = 0; o predicție complet greșită produce pierdere tinde spre infinit.
Etichetta reale (one-hot): [0, 0, 1, 0, 0, 0, 0, 0, 0, 0] (classe "gatto" = indice 2)
Predizione buona: [0.02, 0.03, 0.85, 0.02, 0.01, 0.02, 0.01, 0.02, 0.01, 0.01]
Loss = -log(0.85) = 0.16 --> Loss bassa, predizione corretta
Predizione cattiva: [0.30, 0.25, 0.05, 0.10, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05]
Loss = -log(0.05) = 3.00 --> Loss alta, predizione sbagliata
7.2 Optimizatori
Optimizatorul actualizează greutățile rețelei în direcția care reduce pierderea. Cele mai utilizate sunt:
Comparația Optimizer
| Optimizator | Caracteristici | Când să-l folosești |
|---|---|---|
| SGD + Momentum | Convergență simplă, robustă, stabilă | Antrenament lung, precizie finală maximă |
| Adam | Rată de învățare adaptivă, convergență rapidă | Prototipări, rețele mici/medii |
| AdamW | Adam cu scăderea corectă a greutății | Standard modern, recomandat pentru CNN |
7.3 Mărirea datelor
La mărirea datelor este o tehnică fundamentală pentru a preveni supraadaptarea și îmbunătățirea generalizării rețelei. Constă în aplicarea transformărilor aleatorii la antrenamentul imaginilor (rotații, răsturnări, decupări, modificări de luminozitate) pentru a crea variații sintetice fără a colecta date noi.
Immagine Originale: Trasformazioni:
+-------+ [Flip orizzontale] --> Immagine specchiata
| Gatto | [Rotazione +-15 gradi]--> Leggera rotazione
| --o-- | [Random Crop] --> Ritaglio casuale
| /|\ | [Color Jitter] --> Variazione colori
+-------+ [Gaussian Noise] --> Aggiunta rumore
[Cutout/Erasing] --> Maschera rettangolare casuale
[MixUp] --> Media pesata di 2 immagini
[CutMix] --> Porzione di un'immagine sovrapposta
Effetto: da 50.000 immagini di training, ogni epoca vede variazioni
diverse, come se avessi milioni di immagini uniche.
8. Implementarea completă în PyTorch
Să trecem de la teorie la practică. Vom implementa un CNN complet pentru clasificare imaginile setului de date CIFAR-10 (60.000 de imagini 32x32 în 10 clase: avion, mașină, pasăre, pisică, căprioară, câine, broască, cal, navă, camion).
8.1 Configurare și set de date
import torch
import torch.nn as nn
import torch.optim as optim
import torchvision
import torchvision.transforms as transforms
from torch.utils.data import DataLoader
# Trasformazioni con data augmentation per il training
train_transform = transforms.Compose([
transforms.RandomHorizontalFlip(p=0.5),
transforms.RandomCrop(32, padding=4),
transforms.ColorJitter(brightness=0.2, contrast=0.2),
transforms.ToTensor(),
transforms.Normalize(
mean=[0.4914, 0.4822, 0.4465], # Media CIFAR-10
std=[0.2470, 0.2435, 0.2616] # Std CIFAR-10
),
])
# Trasformazioni per il test (nessuna augmentation)
test_transform = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize(
mean=[0.4914, 0.4822, 0.4465],
std=[0.2470, 0.2435, 0.2616]
),
])
# Caricamento dataset
train_dataset = torchvision.datasets.CIFAR10(
root='./data', train=True, download=True, transform=train_transform
)
test_dataset = torchvision.datasets.CIFAR10(
root='./data', train=False, download=True, transform=test_transform
)
# DataLoader con batching e shuffling
train_loader = DataLoader(train_dataset, batch_size=128, shuffle=True, num_workers=4)
test_loader = DataLoader(test_dataset, batch_size=128, shuffle=False, num_workers=4)
CLASSES = ['aereo', 'auto', 'uccello', 'gatto', 'cervo',
'cane', 'rana', 'cavallo', 'nave', 'camion']
8.2 Definirea Modelului
class ResidualBlock(nn.Module):
"""Blocco residuale con skip connection."""
def __init__(self, in_channels: int, out_channels: int, stride: int = 1):
super().__init__()
self.conv1 = nn.Conv2d(
in_channels, out_channels, kernel_size=3,
stride=stride, padding=1, bias=False
)
self.bn1 = nn.BatchNorm2d(out_channels)
self.conv2 = nn.Conv2d(
out_channels, out_channels, kernel_size=3,
stride=1, padding=1, bias=False
)
self.bn2 = nn.BatchNorm2d(out_channels)
self.relu = nn.ReLU(inplace=True)
# Skip connection: se le dimensioni cambiano, usa conv 1x1
self.shortcut = nn.Sequential()
if stride != 1 or in_channels != out_channels:
self.shortcut = nn.Sequential(
nn.Conv2d(in_channels, out_channels, kernel_size=1,
stride=stride, bias=False),
nn.BatchNorm2d(out_channels)
)
def forward(self, x: torch.Tensor) -> torch.Tensor:
identity = self.shortcut(x)
out = self.relu(self.bn1(self.conv1(x)))
out = self.bn2(self.conv2(out))
out = out + identity # Skip connection
out = self.relu(out)
return out
class CIFAR10CNN(nn.Module):
"""CNN con blocchi residuali per classificazione CIFAR-10."""
def __init__(self, num_classes: int = 10):
super().__init__()
# Primo layer convoluzionale
self.conv1 = nn.Conv2d(3, 32, kernel_size=3, padding=1, bias=False)
self.bn1 = nn.BatchNorm2d(32)
self.relu = nn.ReLU(inplace=True)
# Blocchi residuali con profondità crescente
self.layer1 = self._make_layer(32, 64, num_blocks=2, stride=1)
self.layer2 = self._make_layer(64, 128, num_blocks=2, stride=2)
self.layer3 = self._make_layer(128, 256, num_blocks=2, stride=2)
# Classificatore finale
self.global_avg_pool = nn.AdaptiveAvgPool2d((1, 1))
self.dropout = nn.Dropout(0.3)
self.fc = nn.Linear(256, num_classes)
def _make_layer(
self, in_ch: int, out_ch: int, num_blocks: int, stride: int
) -> nn.Sequential:
layers = [ResidualBlock(in_ch, out_ch, stride)]
for _ in range(1, num_blocks):
layers.append(ResidualBlock(out_ch, out_ch, stride=1))
return nn.Sequential(*layers)
def forward(self, x: torch.Tensor) -> torch.Tensor:
x = self.relu(self.bn1(self.conv1(x))) # (B, 32, 32, 32)
x = self.layer1(x) # (B, 64, 32, 32)
x = self.layer2(x) # (B, 128, 16, 16)
x = self.layer3(x) # (B, 256, 8, 8)
x = self.global_avg_pool(x) # (B, 256, 1, 1)
x = x.view(x.size(0), -1) # (B, 256)
x = self.dropout(x)
x = self.fc(x) # (B, 10)
return x
8.3 Bucle de antrenament
def train_model(
model: nn.Module,
train_loader: DataLoader,
test_loader: DataLoader,
epochs: int = 50,
lr: float = 0.01
) -> dict:
"""Addestra il modello e restituisce la storia del training."""
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = model.to(device)
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(
model.parameters(), lr=lr, momentum=0.9, weight_decay=1e-4
)
scheduler = optim.lr_scheduler.OneCycleLR(
optimizer, max_lr=lr, epochs=epochs,
steps_per_epoch=len(train_loader)
)
history = {'train_loss': [], 'test_loss': [], 'test_acc': []}
for epoch in range(epochs):
# --- Training ---
model.train()
running_loss = 0.0
for images, labels in train_loader:
images, labels = images.to(device), labels.to(device)
optimizer.zero_grad()
outputs = model(images)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
scheduler.step()
running_loss += loss.item()
avg_train_loss = running_loss / len(train_loader)
# --- Evaluation ---
model.eval()
test_loss = 0.0
correct = 0
total = 0
with torch.no_grad():
for images, labels in test_loader:
images, labels = images.to(device), labels.to(device)
outputs = model(images)
loss = criterion(outputs, labels)
test_loss += loss.item()
_, predicted = outputs.max(1)
total += labels.size(0)
correct += predicted.eq(labels).sum().item()
avg_test_loss = test_loss / len(test_loader)
accuracy = 100.0 * correct / total
history['train_loss'].append(avg_train_loss)
history['test_loss'].append(avg_test_loss)
history['test_acc'].append(accuracy)
print(
f"Epoch [{epoch+1}/{epochs}] "
f"Train Loss: {avg_train_loss:.4f} | "
f"Test Loss: {avg_test_loss:.4f} | "
f"Accuracy: {accuracy:.2f}%"
)
return history
# Esecuzione
model = CIFAR10CNN(num_classes=10)
history = train_model(model, train_loader, test_loader, epochs=50, lr=0.1)
# Output atteso dopo 50 epoche con OneCycleLR:
# Test Accuracy: ~92-93%
Parametrii de instruire recomandați pentru CIFAR-10
| Parametru | Valoare | Motivația |
|---|---|---|
| Dimensiunea lotului | 128 | Echilibru între viteză și stabilitatea gradientului |
| Rata de învățare | 0.1 (cu OneCycleLR) | SGD cu programare realizează o precizie superioară |
| Scăderea greutății | 1e-4 | Regularizare L2 pentru a preveni supraadaptarea |
| Epoci | 50 | Cu OneCycleLR, 50 de epoci sunt suficiente pentru a converge |
| Abandonările | 0,3 | Regularizare suplimentară înainte de stratul final |
9. Transfer de învățare
În practică, rar antrenezi un CNN de la zero. The transfer de învăţare vă permite să reutilizați modele pre-antrenate pe seturi de date mari (cum ar fi ImageNet cu 1,2 milioane de imagini și 1.000 de clase) și adaptați-le la problema dvs. specifică. Aceasta reduce reduce drastic timpul de antrenament, cantitatea de date necesare și îmbunătățește performanța.
9.1 Extragerea caracteristicilor vs reglaj fin
Două strategii de învățare prin transfer
| Strategie | Cum funcționează | Când să-l folosești |
|---|---|---|
| Extragerea caracteristicilor | Îngheață toate straturile pre-antrenate, antrenează doar clasificatorul final | Date puține (<1.000 de imagini), domeniu similar cu ImageNet |
| Reglaj fin | Dezghețați ultimele N straturi și reantrenați-vă cu o rată scăzută de învățare | Mai multe date disponibile, alt domeniu decât ImageNet |
import torchvision.models as models
def create_transfer_model(
num_classes: int,
freeze_backbone: bool = True
) -> nn.Module:
"""Crea un modello con transfer learning da ResNet-18."""
# Carica ResNet-18 pre-addestrato su ImageNet
model = models.resnet18(weights=models.ResNet18_Weights.DEFAULT)
# Strategia 1: Feature Extraction (congela il backbone)
if freeze_backbone:
for param in model.parameters():
param.requires_grad = False
# Sostituisci il classificatore finale
num_features = model.fc.in_features # 512 per ResNet-18
model.fc = nn.Sequential(
nn.Dropout(0.3),
nn.Linear(num_features, 256),
nn.ReLU(),
nn.Dropout(0.2),
nn.Linear(256, num_classes)
)
return model
# Feature extraction: solo il classificatore viene addestrato
feature_model = create_transfer_model(num_classes=10, freeze_backbone=True)
# Fine-tuning: tutto il modello viene riaddestrato
finetune_model = create_transfer_model(num_classes=10, freeze_backbone=False)
# Per il fine-tuning, usa un learning rate più basso
optimizer = optim.AdamW([
{'params': finetune_model.layer4.parameters(), 'lr': 1e-4},
{'params': finetune_model.fc.parameters(), 'lr': 1e-3},
], weight_decay=1e-2)
Guida al Transfer Learning:
Dati simili a ImageNet Dati diversi da ImageNet
(oggetti, animali, scene) (medico, satellite, micro)
+---------------------------+---------------------------+
Pochi dati | Feature Extraction | Feature Extraction |
(<1.000 img) | Congela tutto, addestra FC | + Aumenta data augment. |
+---------------------------+---------------------------+
Molti dati | Fine-tuning ultimi layer | Fine-tuning completo |
(>5.000 img) | LR basso per il backbone | o training da zero |
+---------------------------+---------------------------+
10. Măsuri de evaluare
Numai precizia nu este suficientă pentru a înțelege dacă un CNN funcționează bine. Cu seturi de date dezechilibrate (de exemplu, 95% clasa A, 5% clasa B), un model care prezice întotdeauna „clasa A” are o precizie de 95% dar este complet inutil. Sunt necesare valori mai detaliate.
Valori pentru clasificarea imaginilor
| Metric | Ce măsoară | Formula |
|---|---|---|
| Precizie | Procentul de predicții corecte din total | TP + TN / Total |
| Precizie | Dintre predicțiile pozitive, câte sunt corecte? | TP / (TP + FP) |
| Amintiți-vă | Dintre aspectele pozitive reale, câte au fost găsite? | TP / (TP + FN) |
| Scoruri F1 | Mijloc armonic de precizie și reamintire | 2 * (P * R) / (P + R) |
Confusion Matrix (predizioni vs realta):
Predetto: aereo auto uccel gatto cervo cane rana caval nave camion
Reale:
aereo [92] 1 2 0 0 0 1 0 3 1
auto 0 [95] 0 0 0 0 0 0 1 4
uccello 3 0 [85] 3 2 2 3 1 1 0
gatto 0 1 2 [78] 1 12 3 2 0 1
cervo 1 0 3 2 [88] 1 2 3 0 0
Interpretazione:
- Diagonale = predizioni corrette (più alto = meglio)
- Fuori diagonale = errori (confusioni tra classi)
- gatto vs cane: 12 gatti classificati come cani --> classi "confuse"
from sklearn.metrics import (
classification_report,
confusion_matrix
)
import numpy as np
def evaluate_model(
model: nn.Module,
test_loader: DataLoader,
device: torch.device,
class_names: list[str]
) -> dict:
"""Valuta il modello e restituisce metriche dettagliate."""
model.eval()
all_preds = []
all_labels = []
with torch.no_grad():
for images, labels in test_loader:
images = images.to(device)
outputs = model(images)
_, predicted = outputs.max(1)
all_preds.extend(predicted.cpu().numpy())
all_labels.extend(labels.numpy())
preds_array = np.array(all_preds)
labels_array = np.array(all_labels)
# Report dettagliato per classe
report = classification_report(
labels_array, preds_array,
target_names=class_names, output_dict=True
)
# Confusion matrix
cm = confusion_matrix(labels_array, preds_array)
# Accuracy globale
accuracy = np.mean(preds_array == labels_array) * 100
print(f"Accuracy globale: {accuracy:.2f}%")
print(classification_report(
labels_array, preds_array, target_names=class_names
))
return {'accuracy': accuracy, 'report': report, 'confusion_matrix': cm}
11. Implementare: de la model instruit la producție
Pregătirea unui model este doar jumătate din muncă. Pentru a-l aduce în producție trebuie să îl exportați într-un format optimizat, creați un API de inferență și containerizați totul pentru implementare scalabilă.
11.1 Exportarea modelului
Formate de export
| Format | Utilizare | Avantaje |
|---|---|---|
| TorchScript | Deducere PyTorch fără Python | Fără dependențe Python, serializare completă |
| ONNX | Format universal, multi-cadru | Compatibil cu TensorRT, OpenVINO, CoreML |
| TensorRT | Inferență optimizată pentru GPU-uri NVIDIA | Până la 5 ori mai rapid decât PyTorch nativ |
import torch.onnx
def export_model(model: nn.Module, export_path: str) -> None:
"""Esporta il modello in TorchScript e ONNX."""
model.eval()
dummy_input = torch.randn(1, 3, 32, 32)
# --- TorchScript ---
scripted_model = torch.jit.trace(model, dummy_input)
scripted_model.save(f"{export_path}/model_scripted.pt")
print("TorchScript salvato.")
# --- ONNX ---
torch.onnx.export(
model,
dummy_input,
f"{export_path}/model.onnx",
input_names=['image'],
output_names=['prediction'],
dynamic_axes={
'image': {0: 'batch_size'},
'prediction': {0: 'batch_size'}
},
opset_version=17
)
print("ONNX salvato.")
export_model(model, './exports')
11.2 Inferență API cu FastAPI
# inference_api.py
from fastapi import FastAPI, UploadFile
from PIL import Image
import torch
import torchvision.transforms as transforms
import io
app = FastAPI(title="CNN Image Classifier")
# Carica il modello TorchScript
model = torch.jit.load("./exports/model_scripted.pt")
model.eval()
CLASSES = ['aereo', 'auto', 'uccello', 'gatto', 'cervo',
'cane', 'rana', 'cavallo', 'nave', 'camion']
preprocess = transforms.Compose([
transforms.Resize((32, 32)),
transforms.ToTensor(),
transforms.Normalize(
mean=[0.4914, 0.4822, 0.4465],
std=[0.2470, 0.2435, 0.2616]
),
])
@app.post("/predict")
async def predict(file: UploadFile):
"""Classifica un'immagine caricata."""
image_bytes = await file.read()
image = Image.open(io.BytesIO(image_bytes)).convert("RGB")
tensor = preprocess(image).unsqueeze(0)
with torch.no_grad():
outputs = model(tensor)
probabilities = torch.softmax(outputs, dim=1)
confidence, predicted = probabilities.max(1)
return {
"class": CLASSES[predicted.item()],
"confidence": round(confidence.item() * 100, 2),
"all_probabilities": {
name: round(prob.item() * 100, 2)
for name, prob in zip(CLASSES, probabilities[0])
}
}
# Esecuzione: uvicorn inference_api:app --host 0.0.0.0 --port 8000
11.3 Containerizare cu Docker
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY exports/ ./exports/
COPY inference_api.py .
EXPOSE 8000
CMD ["uvicorn", "inference_api:app", "--host", "0.0.0.0", "--port", "8000"]
# Build: docker build -t cnn-classifier .
# Run: docker run -p 8000:8000 cnn-classifier
# Test: curl -X POST -F "file=@cat.jpg" http://localhost:8000/predict
Pipeline: Training --> Export --> Container --> Deploy
[Training] [Export] [Container] [Deploy]
PyTorch + GPU --> TorchScript/ONNX --> Docker --> Kubernetes
50 epoche ~10 MB file FastAPI Auto-scaling
~92% accuracy Ottimizzato Health checks Load balancer
No dipendenza Python GPU opzionale Monitoring
Alternativa serverless:
Export ONNX --> AWS Lambda + ONNX Runtime --> API Gateway
Pro: Pay-per-use, zero infrastruttura da gestire
Contro: Cold start (~2s), limite 250MB package
Concluzii și pașii următori
În acest articol am construit o înțelegere cuprinzătoare a rețelelor neuronale convoluționale, pornind de la elementele fundamentale (cum vede un computer imaginile, operația de convoluție) în sus la implementare practică (CNN cu blocuri reziduale în PyTorch) și implementare în producție (TorchScript, ONNX, FastAPI, Docker).
Am văzut cum evoluția arhitecturilor, de la LeNet-5 în 1998 la ConvNeXt în 2022, și-a îmbunătățit progresiv performanța datorită unor idei precum ignorarea conexiunilor (ResNet), scalare compusă (EfficientNet) și design inspirat de transformator (ConvNeXt).
Puncte cheie de reținut
- CNN-urile exploatează localitate, împărțirea poverii e invarianta spatiala pentru a procesa imaginile eficient
- Arhitectura standard urmează modelul: Conv + BatchNorm + ReLU + Pooling, repetat cu adâncimea crescândă
- Le sări peste conexiuni (ResNet) sunt esențiale pentru antrenarea rețelelor profunde fără gradienți care dispar
- Il transfer de învăţare este aproape întotdeauna de preferat antrenamentului de la zero, mai ales cu puține date
- La mărirea datelor este esenţială pentru generalizare şi costuri zero în ceea ce priveşte colectarea datelor
- Pentru producție, exportați în ONNX o TorchScript și containerizați cu Docker
În următorul articol al seriei vom aprofunda mai mult Transfer de învățare și reglare fină: cum să alegeți modelul potrivit pre-antrenat, strategii progresive de reglare fină, adaptarea domeniului și tehnici avansate, cum ar fi distilarea cunoștințelor. În al treilea articol ne vom confrunta cu Detectarea obiectelor cu YOLO, sistemul de detectare a obiectelor cel mai folosit timp real în industrie.
Resurse suplimentare
- Hârtie ResNet originală: „Învățare reziduală profundă pentru recunoașterea imaginilor” (He et al., 2015)
- Paper ConvNext: „Un ConvNet pentru anii 2020” (Liu et al., 2022)
- Paper EfficientNet: „EfficientNet: Regândirea scalarii modelelor pentru CNN” (Tan & Le, 2019)
- Documentația PyTorch: Tutoriale pe CNN și Torchvision
- CS231n Stanford: Rețele neuronale convoluționale pentru recunoașterea vizuală (curs online)
- ONNX Runtime: Documentație optimizată de inferență multiplatformă







