Faza 04 · lecția 03

CNN-uri — de la LeNet la ResNet

Scopul lecției: Fiecare CNN importantă din ultimii treizeci de ani folosește aceeași rețetă convoluție–neliniaritate–subeșantionare, la care se adaugă o idee nouă. Învățați ideile în ordine.

Versiunea curentă AlexBred.com: primele 100 de lecții ale programului în limba română.

Curs
AI Engineering from Scratch
Fază
Viziune computerizată
Lectură
19 min.
Verificat
Cuprinsul lecției
  1. Obiective de învățare
  2. Problema
  3. Conceptul
  4. Cele patru idei care au schimbat viziunea computerizată
  5. LeNet-5 (1998)
  6. AlexNet (2012)
  7. VGG (2014)
  8. Inception (2014, în același an)
  9. Problema degradării
  10. ResNet (2015)
  11. De ce sunt importante reziduurile dincolo de viziunea computerizată
  12. Construiți-l
  13. Pasul 1: LeNet-5
  14. Pasul 2: un bloc VGG
  15. Pasul 3: un BasicBlock ResNet
  16. Pasul 4: un ResNet mic
  17. Pasul 5: comparați eficiența parametrilor în raport cu caracteristicile
  18. Utilizați-l
  19. Livrați-l
  20. Exerciții
  21. Termeni-cheie
  22. Lecturi suplimentare

Fiecare CNN importantă din ultimii treizeci de ani folosește aceeași rețetă convoluție–neliniaritate–subeșantionare, la care se adaugă o idee nouă. Învățați ideile în ordine.

Tip: Învățare + construcție Limbaje: Python Cerințe preliminare: Faza 3, Lecția 11 (PyTorch), Faza 4, Lecția 01 (Fundamentele imaginilor), Faza 4, Lecția 02 (Convoluții de la zero) Timp: ~75 de minute

Obiective de învățare

  • Urmăriți filiația arhitecturală LeNet-5 → AlexNet → VGG → Inception → ResNet și precizați singura idee nouă adusă de fiecare familie
  • Implementați în PyTorch LeNet-5, un bloc în stil VGG și un BasicBlock ResNet, fiecare în mai puțin de 40 de linii
  • Explicați de ce conexiunile reziduale transformă o rețea cu 1.000 de straturi dintr-una imposibil de antrenat într-una de ultimă generație
  • Citiți un backbone modern (ResNet-18, ResNet-50) și anticipați forma ieșirii, câmpul receptiv și numărul parametrilor înainte de a consulta sursa

Problema

În 2011, cel mai bun clasificator ImageNet obținea aproximativ 74% acuratețe top-5. În 2012, AlexNet a obținut 85%. În 2015, ResNet a obținut 96%. Fără date noi. Fără o generație nouă de GPU. Câștigurile au provenit din idei arhitecturale. Un inginer practicant în domeniul viziunii trebuie să știe ce idee provine din fiecare lucrare, deoarece fiecare backbone de producție pe care îl livrați în 2026 recombină aceleași componente — și deoarece ideile continuă să fie transferate: convoluțiile grupate au trecut de la CNN-uri la arhitecturi Transformer, conexiunile reziduale au trecut de la ResNet la LLM-uri, iar normalizarea pe lot este folosită în modele de difuzie.

Notă tehnică a traducerii: Procentele sunt aproximări ale acurateții top-5, echivalente cu ratele de eroare raportate în competiții și lucrări. Afirmația „fără o generație nouă de GPU” izolează excesiv arhitectura: între aceste rezultate au evoluat și hardware-ul, software-ul și procedurile de antrenare, chiar dacă inovațiile arhitecturale au fost esențiale.

Studierea acestor rețele în ordine vă protejează și împotriva unei greșeli frecvente: alegerea celui mai mare model disponibil când o rețea de dimensiunea LeNet ar rezolva problema. MNIST nu are nevoie de un ResNet. Cunoașterea curbei de scalare a fiecărei familii vă arată unde să vă poziționați pe ea.

Conceptul

Cele patru idei care au schimbat viziunea computerizată

Диаграмма к уроку «CNN-uri — de la LeNet la ResNet»

Nimic altceva din viziunea computerizată clasică nu a avut un impact la fel de mare ca aceste patru salturi.

LeNet-5 (1998)

Sistemul de recunoaștere a cifrelor al lui Yann LeCun. 60.000 de parametri. Două blocuri convoluție–pooling, două straturi complet conectate, activări tanh. A definit șablonul moștenit de fiecare CNN:

intrare (1, 32, 32)
  conv 5x5 -> (6, 28, 28)
  pooling mediu 2x2 -> (6, 14, 14)
  conv 5x5 -> (16, 10, 10)
  pooling mediu 2x2 -> (16, 5, 5)
  aplatizare -> 400
  dens -> 120
  dens -> 84
  dens -> 10

Tot ceea ce lumea modernă numește CNN — convoluții și subeșantionări alternante care alimentează un mic cap de clasificare — este LeNet cu mai multe straturi, mai multe canale și activări mai bune.

AlexNet (2012)

Trei schimbări care, împreună, au depășit decisiv rezultatele ImageNet:

  1. ReLU în loc de tanh. Gradienții nu mai dispar la fel de ușor. Antrenarea se accelerează de șase ori în comparația raportată de lucrare.
  2. Dropout în capul complet conectat. Regularizarea devine un strat, nu un artificiu.
  3. Profunzime și lățime. Cinci straturi convoluționale, trei straturi dense, 60 de milioane de parametri, antrenate pe două GPU-uri, cu modelul împărțit între ele.

Figura 2 a lucrării încă prezintă împărțirea între GPU-uri drept două fluxuri paralele. Paralelismul era o soluție pentru constrângerile hardware, nu o idee arhitecturală — dar cele trei idei de mai sus se regăsesc încă în modelele folosite astăzi.

VGG (2014)

VGG a întrebat: ce se întâmplă dacă folosiți numai convoluții 3x3 și creșteți profunzimea?

stivă:     conv 3x3 -> conv 3x3 -> pooling 2x2
repetare:  16 sau 19 straturi conv

Două convoluții 3x3 văd aceeași zonă 5x5 din intrare ca o singură convoluție 5x5, dar au mai puțini parametri (2*9*C^2 = 18C^2 față de 25*C^2) și încă un ReLU între ele. VGG a transformat această observație într-o arhitectură completă. Simplitatea — un singur tip de bloc, repetat — a făcut-o punctul de referință pentru tot ceea ce a urmat.

Notă tehnică a traducerii: Denumirile VGG-16 și VGG-19 indică 16, respectiv 19 straturi cu ponderi, nu tot atâtea straturi convoluționale. Configurațiile consacrate au 13, respectiv 16 straturi convoluționale, urmate de trei straturi complet conectate. Structura blocului textual al originalului este păstrată, iar etichetele explicative sunt localizate.

Cost: 138 de milioane de parametri, antrenare lentă, inferență costisitoare.

Inception (2014, în același an)

Răspunsul Google la întrebarea „ce dimensiune de nucleu să folosesc?” a fost: toate, în paralel.

Диаграмма к уроку «CNN-uri — de la LeNet la ResNet»

Fiecare ramură se specializează — 1x1 pentru combinarea canalelor, 3x3 pentru textura locală, 5x5 pentru tipare mai mari, pooling pentru caracteristici invariante la translație — iar concatenarea permite stratului următor să aleagă ramura utilă. Inception v1 a folosit convoluții 1x1 în interiorul fiecărei ramuri drept blocaj pentru a menține rezonabil numărul parametrilor.

Problema degradării

În 2015, VGG-19 funcționa, iar VGG-32 nu. Profunzimea trebuia să ajute, dar după aproximativ 20 de straturi atât pierderea de antrenare, cât și cea de testare se înrăutățeau. Aceasta nu este supraînvățare. Optimizatorul nu reușește să găsească ponderi utile, deoarece gradienții se micșorează multiplicativ prin fiecare strat.

Rețea profundă simplă:
  y = f_L( f_{L-1}( ... f_1(x) ... ) )

Gradient față de stratul inițial:
  dL/dW_1 = dL/dy * df_L/df_{L-1} * ... * df_2/df_1 * df_1/dW_1

Fiecare termen multiplicativ are magnitudinea aproximativă (magnitudinea ponderii) * (câștigul activării).
Stivuiți 100 de asemenea termeni cu câștiguri < 1, iar gradientul devine practic zero.

Originalul afirmă că VGG a funcționat la 19 straturi deoarece normalizarea pe lot, publicată simultan, a păstrat activările bine scalate și că nici aceasta nu a putut salva profunzimi de peste aproximativ 30 de straturi.

Notă tehnică a traducerii: VGG-16/19 original nu folosea BatchNorm; variantele VGG cu normalizare pe lot au apărut ulterior. Mai mult, lucrarea ResNet prezintă degradarea ca dificultate de optimizare distinctă de simpla dispariție a gradienților: rețelele simple mai profunde aveau eroare de antrenare mai mare chiar și cu BatchNorm. Explicația multiplicativă din original este intuitivă, dar incompletă.

ResNet (2015)

He, Zhang, Ren și Sun au propus o singură schimbare care a remediat problema:

bloc standard:   y = F(x)
bloc rezidual:   y = F(x) + x

Termenul + x înseamnă că stratul poate alege să nu facă nimic, împingând F(x) către zero. Originalul afirmă că un ResNet cu 1.000 de straturi este astfel cel mult la fel de slab ca o rețea cu un singur strat, deoarece fiecare bloc suplimentar are o cale trivială de ieșire. Cu această garanție, optimizatorul poate face fiecare bloc puțin util — iar utilitatea mică, acumulată de 100 de ori, produce performanțe de ultimă generație.

Notă tehnică a traducerii: Conexiunea identitate face ca o soluție mai superficială să poată fi reprezentată de rețeaua mai profundă atunci când formele sunt compatibile, dar nu garantează că optimizatorul o va găsi și nici că o rețea cu 1.000 de straturi nu poate performa mai slab. Rezultatul ResNet este unul empiric și de optimizare, nu o bornă matematică universală asupra performanței.

Диаграмма к уроку «CNN-uri — de la LeNet la ResNet»

Două variante ale blocului apar pretutindeni:

  • BasicBlock (ResNet-18, ResNet-34): două convoluții 3x3, cu un salt în jurul amândurora.
  • Bottleneck (ResNet-50, -101, -152): 1x1 pentru reducere, 3x3 la mijloc, 1x1 pentru extindere, cu un salt în jurul celor trei. Este mai ieftin când numărul canalelor este mare.

Când saltul trebuie să traverseze o subeșantionare (stride=2), calea identitate este înlocuită cu o convoluție 1x1 cu stride=2 pentru a potrivi formele.

De ce sunt importante reziduurile dincolo de viziunea computerizată

Ideea nu privea de fapt doar clasificarea imaginilor. Privea transformarea rețelelor profunde din sisteme în care „vă țineți respirația și sperați că gradienții supraviețuiesc” într-un instrument ingineresc fiabil și scalabil. Fiecare Transformer pe care îl veți studia în faza următoare are o conexiune de salt similară în fiecare bloc. Fără ResNet, GPT nu ar exista în forma sa actuală.

pooling

Construiți-l

Pasul 1: LeNet-5

O implementare LeNet minimală și apropiată de original. Activări tanh, pooling mediu. Singura concesie declarată modernității este folosirea ulterioară a nn.CrossEntropyLoss în locul conexiunilor gaussiene originale.

Notă tehnică a traducerii: Implementarea este o aproximare modernă, nu o reproducere complet fidelă. LeNet-5 original folosea straturi de subeșantionare cu coeficient și bias antrenabile, conectivitate parțială în C3 și un strat de ieșire bazat pe distanțe euclidiene/RBF, diferențe care depășesc înlocuirea funcției de pierdere.

import torch
import torch.nn as nn
import torch.nn.functional as F

class LeNet5(nn.Module):
    def __init__(self, num_classes=10):
        super().__init__()
        self.conv1 = nn.Conv2d(1, 6, kernel_size=5)
        self.conv2 = nn.Conv2d(6, 16, kernel_size=5)
        self.pool = nn.AvgPool2d(2)
        self.fc1 = nn.Linear(16 * 5 * 5, 120)
        self.fc2 = nn.Linear(120, 84)
        self.fc3 = nn.Linear(84, num_classes)

    def forward(self, x):
        x = self.pool(torch.tanh(self.conv1(x)))
        x = self.pool(torch.tanh(self.conv2(x)))
        x = torch.flatten(x, 1)
        x = torch.tanh(self.fc1(x))
        x = torch.tanh(self.fc2(x))
        return self.fc3(x)

net = LeNet5()
x = torch.randn(1, 1, 32, 32)
print(f"output: {net(x).shape}")
print(f"params: {sum(p.numel() for p in net.parameters()):,}")

Ieșire așteptată: output: torch.Size([1, 10]), params: 61,706. Acesta este întregul clasificator de cifre care a deschis calea viziunii computerizate moderne.

Pasul 2: un bloc VGG

Un bloc reutilizabil: două convoluții 3x3, ReLU, normalizare pe lot, max pooling.

class VGGBlock(nn.Module):
    def __init__(self, in_c, out_c):
        super().__init__()
        self.conv1 = nn.Conv2d(in_c, out_c, kernel_size=3, padding=1)
        self.bn1 = nn.BatchNorm2d(out_c)
        self.conv2 = nn.Conv2d(out_c, out_c, kernel_size=3, padding=1)
        self.bn2 = nn.BatchNorm2d(out_c)
        self.pool = nn.MaxPool2d(2)

    def forward(self, x):
        x = F.relu(self.bn1(self.conv1(x)))
        x = F.relu(self.bn2(self.conv2(x)))
        return self.pool(x)

class MiniVGG(nn.Module):
    def __init__(self, num_classes=10):
        super().__init__()
        self.stack = nn.Sequential(
            VGGBlock(3, 32),
            VGGBlock(32, 64),
            VGGBlock(64, 128),
        )
        self.head = nn.Sequential(
            nn.AdaptiveAvgPool2d(1),
            nn.Flatten(),
            nn.Linear(128, num_classes),
        )

    def forward(self, x):
        return self.head(self.stack(x))

net = MiniVGG()
x = torch.randn(1, 3, 32, 32)
print(f"output: {net(x).shape}")
print(f"params: {sum(p.numel() for p in net.parameters()):,}")

Trei blocuri VGG pe o intrare de dimensiunea CIFAR, un pooling adaptiv, un strat liniar. Aproximativ 290.000 de parametri. Suficient pentru CIFAR-10.

Pasul 3: un BasicBlock ResNet

Blocul constructiv central al ResNet-18 și ResNet-34.

class BasicBlock(nn.Module):
    def __init__(self, in_c, out_c, stride=1):
        super().__init__()
        self.conv1 = nn.Conv2d(in_c, out_c, kernel_size=3, stride=stride, padding=1, bias=False)
        self.bn1 = nn.BatchNorm2d(out_c)
        self.conv2 = nn.Conv2d(out_c, out_c, kernel_size=3, stride=1, padding=1, bias=False)
        self.bn2 = nn.BatchNorm2d(out_c)
        if stride != 1 or in_c != out_c:
            self.shortcut = nn.Sequential(
                nn.Conv2d(in_c, out_c, kernel_size=1, stride=stride, bias=False),
                nn.BatchNorm2d(out_c),
            )
        else:
            self.shortcut = nn.Identity()

    def forward(self, x):
        out = F.relu(self.bn1(self.conv1(x)))
        out = self.bn2(self.conv2(out))
        out = out + self.shortcut(x)
        return F.relu(out)

bias=False în straturile convoluționale este o convenție pentru normalizarea pe lot — parametrul beta al BN gestionează deja biasul, deci păstrarea biasului convoluției ar fi redundantă. shortcut are nevoie de o convoluție reală numai când se schimbă pasul sau numărul canalelor; altfel este o identitate care nu modifică nimic.

Pasul 4: un ResNet mic

Stivuiți patru grupuri de BasicBlock-uri pentru a obține un ResNet funcțional pentru intrări de dimensiunea CIFAR.

class TinyResNet(nn.Module):
    def __init__(self, num_classes=10):
        super().__init__()
        self.stem = nn.Sequential(
            nn.Conv2d(3, 32, kernel_size=3, stride=1, padding=1, bias=False),
            nn.BatchNorm2d(32),
            nn.ReLU(inplace=True),
        )
        self.layer1 = self._make_group(32, 32, num_blocks=2, stride=1)
        self.layer2 = self._make_group(32, 64, num_blocks=2, stride=2)
        self.layer3 = self._make_group(64, 128, num_blocks=2, stride=2)
        self.layer4 = self._make_group(128, 256, num_blocks=2, stride=2)
        self.head = nn.Sequential(
            nn.AdaptiveAvgPool2d(1),
            nn.Flatten(),
            nn.Linear(256, num_classes),
        )

    def _make_group(self, in_c, out_c, num_blocks, stride):
        blocks = [BasicBlock(in_c, out_c, stride=stride)]
        for _ in range(num_blocks - 1):
            blocks.append(BasicBlock(out_c, out_c, stride=1))
        return nn.Sequential(*blocks)

    def forward(self, x):
        x = self.stem(x)
        x = self.layer1(x)
        x = self.layer2(x)
        x = self.layer3(x)
        x = self.layer4(x)
        return self.head(x)

net = TinyResNet()
x = torch.randn(1, 3, 32, 32)
print(f"output: {net(x).shape}")
print(f"params: {sum(p.numel() for p in net.parameters()):,}")

Patru grupuri a câte două blocuri. Pasul 2 la începutul grupurilor 2, 3 și 4. Numărul canalelor se dublează la fiecare subeșantionare. Aproximativ 2,8 milioane de parametri. Aceasta este rețeta standard care se extinde ordonat până la ResNet-152.

Pasul 5: comparați eficiența parametrilor în raport cu caracteristicile

Treceți aceeași intrare prin toate cele trei rețele și comparați numărul parametrilor.

def summary(name, net, x):
    y = net(x)
    params = sum(p.numel() for p in net.parameters())
    print(f"{name:12s}  input {tuple(x.shape)} -> output {tuple(y.shape)}  params {params:>10,}")

x = torch.randn(1, 3, 32, 32)
summary("LeNet5",     LeNet5(),       torch.randn(1, 1, 32, 32))
summary("MiniVGG",    MiniVGG(),      x)
summary("TinyResNet", TinyResNet(),   x)

Trei modele, trei epoci, trei ordine de mărime ale numărului de parametri. Pentru acuratețea pe CIFAR-10, originalul oferă orientativ: LeNet 60%, MiniVGG 89%, TinyResNet 93% după câteva epoci de antrenare.

Notă tehnică a traducerii: Aceste procente nu sunt rezultate reproductibile fără un protocol complet — preprocesare, augmentare, optimizator, schemă a ratei de învățare, număr exact de epoci și semințe. Tratați-le drept estimări ilustrative, nu drept proprietăți ale arhitecturilor.

Utilizați-l

torchvision.models oferă versiuni preantrenate ale familiilor de mai sus. Semnătura apelului este identică între familii, tocmai acesta fiind scopul abstracției backbone.

from torchvision.models import resnet18, ResNet18_Weights, vgg16, VGG16_Weights

r18 = resnet18(weights=ResNet18_Weights.IMAGENET1K_V1)
r18.eval()

print(f"ResNet-18 params: {sum(p.numel() for p in r18.parameters()):,}")
print(r18.layer1[0])
print()

v16 = vgg16(weights=VGG16_Weights.IMAGENET1K_V1)
v16.eval()
print(f"VGG-16   params: {sum(p.numel() for p in v16.parameters()):,}")

ResNet-18 are 11,7 milioane de parametri. VGG-16 are 138 de milioane. Acuratețe top-1 ImageNet similară (69,8% față de 71,6%). Conexiunile reziduale vă oferă o eficiență a parametrilor de 12 ori mai mare în această comparație. De aceea variantele ResNet au dominat din 2016 până la apariția ViT în 2021 — și domină încă implementările reale în care calculul este constrângerea principală.

Pentru învățarea prin transfer, rețeta prezentată aici este: încărcați modelul preantrenat, înghețați backbone-ul, înlocuiți capul de clasificare.

Notă tehnică a traducerii: Înghețarea completă a backbone-ului este un punct de pornire, nu o regulă universală. În funcție de dimensiunea și diferența de domeniu a setului-țintă, ajustarea fină parțială sau completă poate oferi rezultate mai bune.

for p in r18.parameters():
    p.requires_grad = False
r18.fc = nn.Linear(r18.fc.in_features, 10)

Trei linii. Aveți acum un clasificator CIFAR cu 10 clase care moștenește reprezentările plătite de ImageNet.

Livrați-l

Această lecție produce:

  • outputs/prompt-backbone-selector.md — un prompt care alege familia CNN potrivită (LeNet/VGG/ResNet/MobileNet/ConvNeXt) în funcție de sarcină, dimensiunea setului de date și bugetul de calcul.
  • outputs/skill-residual-block-reviewer.md — o abilitate care citește un modul PyTorch și semnalează greșeli ale conexiunilor de salt (scurtătură lipsă la schimbarea pasului, ordinea activării pe scurtătură, poziționarea BN față de adunare).

Exerciții

  1. (Ușor) Numărați manual parametrii din TinyResNet, strat cu strat. Comparați cu sum(p.numel() for p in net.parameters()). Unde se consumă cea mai mare parte a bugetului de parametri — în convoluții, BN sau capul de clasificare?
  2. (Mediu) Implementați blocul Bottleneck (1x1 → 3x3 → 1x1 cu salt) și folosiți-l pentru a construi o rețea în stil ResNet-50 pentru CIFAR. Comparați parametrii cu TinyResNet.
  3. (Dificil) Eliminați conexiunea de salt din BasicBlock, antrenați o rețea „simplă” cu 34 de blocuri și un ResNet cu 34 de blocuri pe CIFAR-10 timp de câte 10 epoci. Reprezentați pierderea de antrenare în funcție de epocă pentru ambele. Reproduceți rezultatul din Figura 1 a lucrării He și colaboratorii, în care rețeaua simplă profundă converge la o pierdere mai mare decât perechea sa mai puțin profundă.

Termeni-cheie

Termen Ce spun oamenii Ce înseamnă de fapt
Backbone „Modelul” Stiva de blocuri convoluționale care produce harta de caracteristici furnizată capului sarcinii
Conexiune reziduală „Conexiune de salt” y = F(x) + x; permite optimizatorului să reprezinte identitatea setând F la zero, ceea ce ușurează antrenarea la profunzimi mari
BasicBlock „Două convoluții 3x3 cu un salt” Blocul constructiv ResNet-18/34: conv-BN-ReLU-conv-BN-adunare-ReLU
Bottleneck „1x1 reducere, 3x3, 1x1 extindere” Blocul ResNet-50/101/152; ieftin la un număr mare de canale, deoarece 3x3 rulează pe o lățime redusă
Problema degradării „Mai profund înseamnă mai slab” După aproximativ 20 de straturi convoluționale simple, cresc atât eroarea de antrenare, cât și cea de testare; conexiunile reziduale atenuează problema
Stem „Primul strat” Convoluția inițială care transformă intrarea cu trei canale în lățimea de bază a caracteristicilor; de obicei 7x7 cu pas 2 pentru ImageNet, 3x3 cu pas 1 pentru CIFAR
Cap „Clasificatorul” Straturile de după ultimul bloc backbone: pooling adaptiv, aplatizare, strat sau straturi liniare
Învățare prin transfer „Ponderi preantrenate” Încărcarea unui backbone antrenat pe ImageNet și ajustarea fină a capului sau, după caz, a unei părți mai mari din rețea pentru sarcina vizată

Lecturi suplimentare

Sursă: Originalul în limba engleză

Navigare: ← Lecția 04.02 — Convoluții de la zero · Faza 4 — Viziune computerizată · Catalog complet