CNN: Konvoluční sítě od nuly po produkci
Jak počítač "vidí" kočku na fotografii? Jak rozeznáte dopravní značku od lidské tváře? Odpověď spočívá v Konvoluční neuronové sítě (CNN), architektura hlubokého učení který způsobil revoluci v počítačovém vidění. Od samořídících aut po lékařskou zobrazovací diagnostiku, CNN jsou neviditelným motorem milionů aplikací, které používáme každý den.
V tomto prvním článku seriálu Počítačové vidění s hlubokým učením, postavíme vaše chápání CNN od nuly: co to je, jak fungují konvoluční filtry, které architektury tvořily historii a jak implementovat a trénovat kompletní CNN v PyTorch. Po dokončení budete mít dovednosti klasifikovat skutečné obrázky a uvést model do výroby.
Přehled série
| # | Položka | Soustředit |
|---|---|---|
| 1 | Nacházíte se zde - CNN: Convolutional Networks | Architektura, školení, nasazení |
| 2 | Přenos učení a jemné ladění | Předtrénované modely, adaptace domény |
| 3 | Detekce objektů pomocí YOLO | Detekce objektů v reálném čase |
| 4 | Sémantická segmentace | Klasifikace na úrovni pixelů |
| 5 | Generování obrazu pomocí GAN a difúze | Syntetické generování obrazu |
| 6 | Edge Deployment a optimalizace | Modely na vestavěných zařízeních |
Co se naučíte
- Jak počítač reprezentuje obrázky (pixely, kanály RGB, tenzory)
- Operace konvoluce: jádro, mapy funkcí, posuvné okno
- Základní součásti CNN: konvoluce, sdružování, aktivace
- Vývoj architektur: od LeNet-5 po ConvNeXt
- ResNet a přeskočení připojení: jak vyřešit mizející gradient
- Kompletní implementace v PyTorch: CNN pro klasifikaci CIFAR-10
- Přenos učení: Opětovné použití předem vyškolených modelů na ImageNet
- Metriky hodnocení: přesnost, přesnost, zapamatovatelnost, matice zmatků
- Nasazení: od trénovaného modelu po produkční odvození (ONNX, TorchScript)
1. Od pixelů k funkcím: Jak počítač vidí obrázky
Pro nás lidi je pohled na fotku přirozeným úkonem. Náš mozek to okamžitě rozpozná tvary, barvy, obrysy a předměty. Ale pro počítač je obrázek jen mřížkou čísel. Každý pixel je reprezentován číselnými hodnotami, které udávají intenzitu světla.
1.1 Obrázek jako matice čísel
Obrázek ve stupních šedi je 2D pole, kde každá buňka obsahuje hodnotu mezi 0 (černá)
a 255 (bílá). Barevný obrázek se skládá z tři kanály (červená, zelená, modrá),
každá samostatná matrice. Barevný obrázek 224x224 je tedy tenzor rozměrů
3 x 224 x 224nebo 150 528 číselných hodnot.
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 Proč husté neuronové sítě nestačí
První přístup, který by mě napadl, je zploštění obrázku do 1D vektoru a jeho průchod do plně propojené (husté) neuronové sítě. Pro obrázek 224x224x3 by to znamenalo vstupní vrstva s 150 528 neuronů. Se skrytou vrstvou 1000 neuronů, jen v první vrstvě byste již měli 150 milionů parametrů.
Problémy hustých sítí pro obrázky
- Exploze parametrů: Miliony závaží již v první vrstvě, výpočetně neúnosné
- Žádná prostorová invariance: Pokud se kočka posune o 10 pixelů doprava, síť ji již nerozpozná
- Ztráta 2D struktury: Zploštění obrazu ničí prostorové vztahy mezi sousedními pixely
- Převybavení: Příliš mnoho parametrů s příliš malým množstvím dat vede k zapamatování, nikoli zobecnění
CNN řeší všechny tyto problémy využitím tří klíčových poznatků: lokalita (vizuální vzory jsou místní), sdílení zátěže (stejný filtr funguje všude na obrázku) e invariance překladu (hrana je hrana bez ohledu na to, kde se nachází).
2. Operace konvoluce
La konvoluce je to matematická operace v srdci CNN. Malý filtr (řekl jádro) roluje přes vstupní obrázek a na každé pozici vypočítá součet vážení zakrytých pixelů. Výsledkem je nové pole nazvané mapy funkcí, který zvýrazní konkrétní vzor (svislý okraj, vodorovný okraj, roh, textura).
2.1 Jádro a posuvné okno
Jádro je malá matice vah (obvykle 3x3 nebo 5x5), která se posouvá (diapozitivy) přes celý vstupní obrázek. Na každé pozici jsou hodnoty jádra násobí prvek po prvku se základními pixely a sčítají se, aby vznikl jeden hodnotu ve výstupní mapě prvků.
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
+---+---+---+
| . | . | . |
+---+---+---+
Klasické typy jádra
| Jádro | Rozsah | Hodnoty (3x3) |
|---|---|---|
| Vertikální hranice | Detekce vertikálních přechodů | [-1, 0, 1] se opakuje |
| Horizontální hranice | Detekce vodorovných přechodů | [-1,-1,-1], [0,0,0], [1,1,1] |
| Ostření | Zvyšuje ostrost | [0,-1,0], [-1,5,-1], [0,-1,0] |
| Gaussovské rozostření | Gaussovské rozostření | [1,2,1], [2,4,2], [1,2,1] / 16 |
Klíčový rozdíl mezi CNN a tradičním zpracováním obrazu je v tom na CNN hodnoty jádra se nenastavují ručně. Síť učí automaticky optimální filtry během tréninku zpětné šíření. První vrstvy se učí detekovat jednoduché hrany a textury, zatímco vrstvy hlubší kombinují tyto rysy do stále složitějších vzorců (oči, kola, obličeje).
3. Komponenty CNN
CNN se skládá z různých typů vrstev, z nichž každá má specifickou roli. Pochopení toho, co jednotlivé komponenty dělají, je zásadní pro navrhování efektivních architektur.
3.1 Konvoluční vrstva
Konvoluční vrstva aplikuje na vstup několik filtrů a vytváří pro ně mapu prvků každý filtr. Pokud na obrázek RGB použijete 32 filtrů 3x3, získáte 32 map funkcí, každý zvýrazňuje jiný vzor. Klíčové parametry jsou:
Parametry konvoluční vrstvy
| Parametr | Popis | Typické hodnoty |
|---|---|---|
| Velikost jádra | Velikost filtru (šířka x výška) | 3x3, 5x5, 7x7 |
| Krok | Krok posouvání filtru | 1, 2 |
| Vycpávka | Pixely přidané k okrajům vstupu | 0 (platí), 1 (stejné pro 3x3) |
| Počet filtrů | Kolik map prvků ve výstupu | 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 Aktivační funkce (ReLU)
Po každé konvoluci je aplikována nelineární aktivační funkce. Nejběžnější
je to tam ReLU (Rectified Linear Unit): f(x) = max(0, x).
ReLU resetuje všechny záporné hodnoty a ponechá kladné beze změny. Bez nelinearity,
sekvence konvolucí by byla ekvivalentní jedné lineární transformaci,
dělat síť neschopnou učit se složité vzorce.
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 Sdružování vrstev
Sdružování snižuje prostorové rozměry map objektů a snižuje počet parametrů a zvýšení odolnosti sítě vůči malým změnám polohy prvků. Dva typy hlavní jsou Max Pooling (v každém okně nabývá maximální hodnoty) e Průměrné sdružování (vypočítejte průměr).
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 Dávková normalizace
La Dávková normalizace (BatchNorm) normalizuje výstup každé vrstvy takže má nulový střední a jednotkový rozptyl. Tím se trénink stabilizuje, umožní vyšší rychlost učení a působí jako lehký regularizátor. V praxi to sedí vrstva BatchNorm po každé konvoluci, před aktivací.
4. Typická architektura CNN
Standardní CNN se řídí opakujícím se vzorem: bloky extrakce prvků (konvoluce + aktivace + sdružování) následované plně propojenými vrstvami pro konečnou klasifikaci. S hloubkou se zmenšuje prostorová velikost map prvků, ale jejich počet se zvyšuje kanálů, zachycujících stále abstraktnější vzory.
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]
Hierarchie naučených funkcí
- Vrstva 1-2 (nízká úroveň): Hranice, barevné přechody, jednoduché textury
- Vrstva 3-5 (střední úroveň): Rohy, obrysy, části předmětů (oči, kola)
- Vrstva 6+ (vysoká úroveň): Kompletní objekty, scény, abstraktní pojmy
Tato hierarchie vzniká automaticky během tréninku. Není potřeba sdělit síti, co má hledat: filtry se přizpůsobí datům.
5. Evoluce CNN Architectures
Historie CNN je poznamenána revolučními architekturami, z nichž každá představila myšlenky, které změnily obor. Znalost tohoto vývoje je základem porozumění moderní architektonické možnosti.
Časová osa CNN Architectures
| Rok | Architektura | Klíčová inovace | Top-1 ImageNet |
|---|---|---|---|
| 1998 | LeNet-5 | První praktická CNN (rozpoznávání číslic) | N/A |
| 2012 | AlexNet | Školení GPU, ReLU, Dropout | 63,3 % |
| 2014 | VGGNet | Hluboké sítě s jednotnými 3x3 filtry | 74,5 % |
| 2014 | GoogleLeNet/Počátek | Počáteční moduly, vícestupňové paralelní | 74,8 % |
| 2015 | ResNet | Přeskočit připojení, sítě s více než 152 vrstvami | 78,6 % |
| 2019 | EfficientNet | Složené škálování (hloubka+šířka+rozlišení) | 84,4 % |
| 2022 | ConvNeXt | Modernizovaná CNN inspirovaná Vision Transformers | 87,8 % |
5.1 LeNet-5 (1998) – The Pioneer
Navržen Yann LeCun pro rozpoznávání ručně psaných číslic (MNIST), LeNet-5 a první CNN, která byla prakticky úspěšná. S pouhými 5 vrstvami a 60 000 parametry se osvědčil že konvoluce by se mohly naučit rozlišovací rysy z obrázků. Bylo použito pro automaticky číst bankovní šeky.
5.2 AlexNet (2012) – Revoluce
AlexNet vyhrál soutěž ImageNet 2012 s obrovským náskokem a snížil tak chybu z 26 % na 16 %. Klíčové novinky: školení GPU (dvě NVIDIA GTX 580), funkce Aktivace ReLU místo sigmatu, Dropout pro regularizaci a rozšíření dat. Tento výsledek přesvědčil akademickou obec a průmysl, že hluboké učení funguje.
5.3 VGGNet (2014) – Na hloubce záleží
VGG ukázala, že hlubší sítě poskytují lepší výsledky. Jeho klíčová myšlenka a radikální v jednoduchosti: používejte pouze 3x3 skládané filtry. Dvě po sobě jdoucí vrstvy 3x3 mají stejné receptivní pole jako jedna vrstva 5x5, ale s méně a více parametry nelinearita. VGG-16 má 16 vrstev a 138 milionů parametrů.
5.4 EfficientNet (2019) – Inteligentní škálování
EfficientNet představil složené škálování: místo pouhého zvyšování hloubka (jako VGG) nebo šířka, měří všechny tři rozměry jednotně (hloubka, šířka, vstupní rozlišení) s vyváženými koeficienty. EfficientNet-B0 dosahuje 77,1% přesnosti na ImageNet s pouhými 5,3 miliony parametrů, uvádí zpráva bezprecedentní parametry přesnosti.
5.5 ConvNeXt (2022) – Modernizovaná CNN
ConvNeXt ukazuje, že CNN, modernizované technikami inspirovanými Vision Transformers, mohou konkurovat (a překonávat) architektury transformátorů. Mezi inovace patří: 7x7 hloubkově oddělitelná jádra, LayerNorm místo BatchNorm, aktivace GELU, a "izomorfní" design s fázemi rostoucí velikosti. ConvNeXt V2, ve verzi E-ConvNeXt-Tiny, dosahuje 80,6 % Top-1 s pouhými 2,0 GFLOP, vynikající pro nasazení efektivní.
6. ResNet a Skip Connections
ResNet (zbytkové sítě), navržený He et al. v roce 2015 se to vyřešilo jeden ze základních problémů hlubokého učení: problém degradace. Před ResNet přidání vrstev do hluboké sítě výsledky spíše zhoršilo než zlepšilo, i na tréninkové sestavě. Řešení je stejně elegantní jako jednoduché.
6.1 Problém mizejícího gradientu
Během zpětného šíření se gradienty opakovaně násobí vrstvy sítě. Pokud tato násobení vytvoří hodnoty menší než 1, gradienty exponenciálně „slábnou“, jak se šíří směrem k počátečním vrstvám. S 50 nebo více vrstvami se přechody stanou tak malé, že se prvních několik vrstev zastaví učit se. Nedávné studie potvrzují, že bez přeskočení připojení je L2 gradientů drasticky klesá v počátečních vrstvách, zatímco s přeskočením spojení zůstávají jednotné v celé síti.
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 Řešení: Zbytkové učení
Geniální myšlenka ResNetu je jednoduchá: místo vytváření bloku se naučte transformaci
kompletní H(x), přimějete ho naučit se pouze to rozdíl (zbytek)
s ohledem na vstup: F(x) = H(x) - x. Výstupem bloku se stává
y = F(x) + x, Kde x je to vstup, který blok "přeskočí".
jeden přeskočit připojení (nebo zkratkové připojení).
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.
protože to funguje
Učení zbytku F(x) = 0 (tj. „nedělat nic“) je mnohem jednodušší
než se naučit kompletní transformaci identity. Pokud vrstva není užitečná, síť
prostě se učit F(x) = 0 a předá vstup beze změny. To umožňuje
budovat sítě se stovkami vrstev bez snížení výkonu.
7. Školení CNN
Trénovat CNN znamená najít optimální hodnoty pro všechna jádra (filtry) a hmotnosti plně spojených vrstev. To se děje prostřednictvím iterativního procesu dopředný průchod, výpočet ztrát a zpětné šíření gradientu.
7.1 Ztrátová funkce
Pro klasifikaci snímků je standardní ztrátová funkce Ztráta křížové entropie. Měří, jak moc se pravděpodobnosti předpovězené sítí liší od skutečných štítků. Dokonalá předpověď produkuje ztrátu = 0; vytváří zcela chybnou předpověď ztráta směřující do nekonečna.
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 Optimalizátory
Optimalizátor aktualizuje váhy sítě ve směru, který snižuje ztráty. Nejpoužívanější jsou:
Porovnání optimalizátoru
| Optimalizátor | Charakteristika | Kdy jej použít |
|---|---|---|
| SGD + Momentum | Jednoduchá, robustní, stabilní konvergence | Dlouhý trénink, maximální výsledná přesnost |
| Adame | Adaptivní rychlost učení, rychlá konvergence | Prototypování, malé/střední sítě |
| AdamW | Adam se správným váhovým úbytkem | Moderní standard, doporučený pro CNN |
7.3 Rozšíření dat
La augmentace dat je to základní technika, jak zabránit nadměrnému vybavení a zlepšit zobecnění sítě. Spočívá v aplikaci náhodných transformací k cvičným obrázkům (rotace, převrácení, oříznutí, změny jasu) k vytvoření variací syntetické bez sběru nových dat.
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. Kompletní implementace v PyTorch
Přejděme od teorie k praxi. Zavedeme úplnou klasifikaci CNN obrázky datové sady CIFAR-10 (60 000 obrázků 32x32 v 10 třídách: letadlo, auto, pták, kočka, jelen, pes, žába, kůň, loď, nákladní automobil).
8.1 Nastavení a datová sada
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 Definice modelu
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 Tréninkové smyčky
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%
Doporučené tréninkové parametry pro CIFAR-10
| Parametr | Hodnota | Motivace |
|---|---|---|
| Velikost dávky | 128 | Rovnováha mezi rychlostí a stabilitou gradientu |
| Míra učení | 0,1 (s OneCycleLR) | SGD s plánováním dosahuje vynikající přesnosti |
| Úbytek hmotnosti | 1e-4 | Regulace L2, aby se zabránilo nadměrnému vybavení |
| éry | 50 | S OneCycleLR stačí ke sblížení 50 epoch |
| Výpadky | 0,3 | Dodatečná regularizace před finální vrstvou |
9. Přenos učení
V praxi málokdy trénujete CNN od nuly. The přenos učení umožňuje znovu použít předem připravené modely na velkých souborech dat (jako ImageNet s 1,2 mil obrázků a 1 000 tříd) a přizpůsobte je svému konkrétnímu problému. Tím se snižuje drasticky snižuje dobu školení, množství potřebných dat a zlepšuje výkon.
9.1 Extrakce funkcí vs jemné ladění
Dvě strategie transferového učení
| Strategie | Jak to funguje | Kdy jej použít |
|---|---|---|
| Extrakce funkcí | Zmrazit všechny předem trénované vrstvy, trénovat pouze finální klasifikátor | Málo dat (<1 000 obrázků), podobná doména jako ImageNet |
| Jemné doladění | Rozmrazte posledních N vrstev a znovu trénujte s nízkou rychlostí učení | K dispozici více dat, jiná doména než 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. Metriky hodnocení
Samotná přesnost nestačí k pochopení toho, zda CNN funguje dobře. S nevyváženými datovými sadami (např. 95 % třída A, 5 % třída B), model, který vždy předpovídá „třídu A“, má 95% přesnost ale je to úplně zbytečné. Je zapotřebí podrobnějších metrik.
Metriky pro klasifikaci obrázků
| Metrický | Co měří | Vzorec |
|---|---|---|
| Přesnost | Procento správných předpovědí z celkového počtu | TP + TN / Celk |
| Přesnost | Kolik z pozitivních předpovědí je správných? | TP / (TP + FP) |
| Odvolání | Kolik se jich našlo mezi skutečnými pozitivy? | TP / (TP + FN) |
| Výsledky F1 | Harmonický průměr přesnosti a zapamatování | 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. Nasazení: Od vycvičeného modelu k výrobě
Výcvik modelky je jen polovina práce. Chcete-li jej uvést do výroby, musíte jej vyvézt v optimalizovaném formátu vytvořte inferenční API a vše kontejnerizujte pro škálovatelné nasazení.
11.1 Export modelu
Exportovat formáty
| Formát | Použití | Výhody |
|---|---|---|
| TorchScript | Vyvodit PyTorch bez Pythonu | Žádné závislosti na Pythonu, plná serializace |
| ONNX | Univerzální formát, multi-framework | Kompatibilní s TensorRT, OpenVINO, CoreML |
| TensorRT | Inference optimalizovaná pro GPU NVIDIA | Až 5x rychlejší než nativní PyTorch |
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 Inference API s 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 Kontejnerizace pomocí Dockeru
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
Závěry a další kroky
V tomto článku jsme vybudovali komplexní pochopení konvolučních neuronových sítí, počínaje od základů (jak počítač vidí obrázky, operace konvoluce) nahoru k praktické implementaci (CNN se zbytkovými bloky v PyTorch) a nasazení ve výrobě (TorchScript, ONNX, FastAPI, Docker).
Viděli jsme, jak se vývoj architektur, od LeNet-5 v roce 1998 po ConvNeXt v roce 2022, má progresivně lepší výkon díky nápadům, jako je přeskakování připojení (ResNet), složené škálování (EfficientNet) a design inspirovaný transformátorem (ConvNeXt).
Klíčové body k zapamatování
- CNN využívají lokalita, sdílení zátěže e prostorová invariance efektivně zpracovávat obrázky
- Standardní architektura se řídí vzorem: Conv + BatchNorm + ReLU + Pooling, opakuje se s rostoucí hloubkou
- Le přeskočit spojení (ResNet) jsou nezbytné pro trénování hlubokých sítí bez mizejících gradientů
- Il přenos učení je téměř vždy výhodnější než trénovat od nuly, zvláště s malým množstvím dat
- La augmentace dat je nezbytný pro zobecnění a náklady na sběr dat jsou nulové
- Pro výrobu, export do ONNX o TorchScript a kontejnerizovat pomocí Dockeru
V dalším článku seriálu se budeme hlouběji zabývat Přenos učení a jemné ladění: jak vybrat správný předtrénovaný model, progresivní dolaďovací strategie, adaptace domény a pokročilé techniky, jako je znalostní destilace. Ve třetím článku budeme čelit Detekce objektů pomocí YOLO, systém detekce objektů nejpoužívanější v reálném čase v průmyslu.
Další zdroje
- Originální papír ResNet: „Hluboké zbytkové učení pro rozpoznávání obrazu“ (He et al., 2015)
- Paper ConvNeXt: „ConvNet pro 2020“ (Liu et al., 2022)
- Paper EfficientNet: „EfficientNet: Rethinking Model Scaling for CNN“ (Tan & Le, 2019)
- Dokumentace PyTorch: Návody na CNN a Torchvision
- CS231n Stanford: Konvoluční neuronové sítě pro vizuální rozpoznávání (online kurz)
- ONNX Runtime: Optimalizovaná dokumentace odvození napříč platformami







