CNN: Reti Convoluzionali da Zero a Production
Come fa un computer a "vedere" un gatto in una foto? Come distingue un segnale stradale da un volto umano? La risposta sta nelle Convolutional Neural Networks (CNN), l'architettura di deep learning che ha rivoluzionato la computer vision. Dalle auto a guida autonoma alla diagnosi medica per immagini, le CNN sono il motore invisibile dietro milioni di applicazioni che usiamo ogni giorno.
In questo primo articolo della serie Computer Vision con Deep Learning, costruiremo la tua comprensione delle CNN partendo da zero: cosa sono, come funzionano i filtri convoluzionali, quali architetture hanno fatto la storia e come implementare e addestrare una CNN completa in PyTorch. Al termine avrai le competenze per classificare immagini reali e portare il modello in produzione.
Panoramica della Serie
| # | Articolo | Focus |
|---|---|---|
| 1 | Sei qui - CNN: Reti Convoluzionali | Architettura, training, deployment |
| 2 | Transfer Learning e Fine-Tuning | Modelli pre-addestrati, domain adaptation |
| 3 | Object Detection con YOLO | Rilevamento oggetti in tempo reale |
| 4 | Segmentazione Semantica | Classificazione a livello di pixel |
| 5 | Image Generation con GAN e Diffusion | Generazione immagini sintetiche |
| 6 | Edge Deployment e Ottimizzazione | Modelli su dispositivi embedded |
Cosa Imparerai
- Come un computer rappresenta le immagini (pixel, canali RGB, tensori)
- L'operazione di convoluzione: kernel, feature maps, sliding window
- I componenti fondamentali di una CNN: convoluzioni, pooling, attivazioni
- L'evoluzione delle architetture: da LeNet-5 a ConvNeXt
- ResNet e le skip connections: come risolvere il vanishing gradient
- Implementazione completa in PyTorch: CNN per classificazione CIFAR-10
- Transfer learning: riutilizzare modelli pre-addestrati su ImageNet
- Metriche di valutazione: accuracy, precision, recall, confusion matrix
- Deployment: da modello addestrato a inference in produzione (ONNX, TorchScript)
1. Da Pixel a Feature: Come un Computer Vede le Immagini
Per noi umani, guardare una foto è un atto naturale. Il nostro cervello riconosce istantaneamente forme, colori, contorni e oggetti. Ma per un computer, un'immagine è solo una griglia di numeri. Ogni pixel è rappresentato da valori numerici che indicano l'intensità luminosa.
1.1 Un'Immagine come Matrice di Numeri
Un'immagine in scala di grigi è una matrice 2D dove ogni cella contiene un valore tra 0 (nero)
e 255 (bianco). Un'immagine a colori è composta da tre canali (Red, Green, Blue),
ciascuno una matrice separata. Un'immagine 224x224 a colori è quindi un tensore di dimensione
3 x 224 x 224, ovvero 150.528 valori numerici.
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 perchè le Reti Neurali Dense Non Bastano
Il primo approccio che verrebbe in mente è appiattire l'immagine in un vettore 1D e passarla a una rete neurale fully connected (densa). Per un'immagine 224x224x3, questo significherebbe un layer di input con 150.528 neuroni. Con un hidden layer da 1.000 neuroni, avresti già 150 milioni di parametri solo nel primo layer.
Problemi delle Reti Dense per le Immagini
- Esplosione dei parametri: Milioni di pesi già al primo layer, computazionalmente proibitivo
- Nessuna invarianza spaziale: Se un gatto si sposta di 10 pixel a destra, la rete non lo riconosce più
- Perdita della struttura 2D: Appiattire l'immagine distrugge le relazioni spaziali tra pixel vicini
- Overfitting: Troppi parametri con pochi dati portano a memorizzazione, non generalizzazione
Le CNN risolvono tutti questi problemi sfruttando tre intuizioni fondamentali: localita (i pattern visivi sono locali), condivisione dei pesi (lo stesso filtro funziona ovunque nell'immagine) e invarianza alla traslazione (un bordo è un bordo indipendentemente da dove si trova).
2. L'Operazione di Convoluzione
La convoluzione è l'operazione matematica al cuore delle CNN. Un piccolo filtro (detto kernel) scorre sull'immagine di input, calcolando ad ogni posizione una somma pesata dei pixel coperti. Il risultato è una nuova matrice chiamata feature map, che evidenzia un pattern specifico (bordo verticale, bordo orizzontale, angolo, texture).
2.1 Kernel e Sliding Window
Un kernel è una piccola matrice di pesi (tipicamente 3x3 o 5x5) che viene fatta scorrere (slide) su tutta l'immagine di input. Ad ogni posizione, i valori del kernel vengono moltiplicati elemento per elemento con i pixel sottostanti e sommati per produrre un singolo valore nella feature map di output.
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
+---+---+---+
| . | . | . |
+---+---+---+
Tipi di Kernel Classici
| Kernel | Scopo | Valori (3x3) |
|---|---|---|
| Bordo verticale | Rileva transizioni verticali | [-1, 0, 1] ripetuto |
| Bordo orizzontale | Rileva transizioni orizzontali | [-1,-1,-1], [0,0,0], [1,1,1] |
| Sharpening | Aumenta la nitidezza | [0,-1,0], [-1,5,-1], [0,-1,0] |
| Gaussian blur | Sfocatura gaussiana | [1,2,1], [2,4,2], [1,2,1] / 16 |
La differenza fondamentale tra le CNN e l'elaborazione tradizionale delle immagini è che nelle CNN i valori del kernel non sono fissati a mano. La rete apprende automaticamente i filtri ottimali durante il training attraverso la backpropagation. I primi layer imparano a rilevare bordi e texture semplici, mentre i layer più profondi combinano queste feature in pattern sempre più complessi (occhi, ruote, volti).
3. Componenti di una CNN
Una CNN è composta da diversi tipi di layer, ognuno con un ruolo specifico. Capire cosa fa ogni componente è fondamentale per progettare architetture efficaci.
3.1 Convolutional Layer
Il layer convoluzionale applica multipli filtri all'input, producendo una feature map per ogni filtro. Se applichi 32 filtri 3x3 a un'immagine RGB, ottieni 32 feature maps, ciascuna che evidenzia un pattern diverso. I parametri chiave sono:
Parametri del Convolutional Layer
| Parametro | Descrizione | Valori Tipici |
|---|---|---|
| Kernel size | Dimensione del filtro (larghezza x altezza) | 3x3, 5x5, 7x7 |
| Stride | Passo di scorrimento del filtro | 1, 2 |
| Padding | Pixel aggiunti ai bordi dell'input | 0 (valid), 1 (same per 3x3) |
| Numero filtri | Quante feature maps in output | 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 Activation Function (ReLU)
Dopo ogni convoluzione si applica una funzione di attivazione non-lineare. La più comune
è la ReLU (Rectified Linear Unit): f(x) = max(0, x).
ReLU azzera tutti i valori negativi e lascia invariati quelli positivi. Senza la non-linearita,
una sequenza di convoluzioni sarebbe equivalente a una singola trasformazione lineare,
rendendo la rete incapace di apprendere pattern complessi.
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 Pooling Layer
Il pooling riduce le dimensioni spaziali delle feature maps, diminuendo il numero di parametri e rendendo la rete più robusta a piccole variazioni nella posizione delle feature. I due tipi principali sono Max Pooling (prende il valore massimo in ogni finestra) e Average Pooling (calcola la 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 Batch Normalization
La Batch Normalization (BatchNorm) normalizza l'output di ogni layer affinche abbia media zero e varianza unitaria. Questo stabilizza il training, permette learning rate più alti e funge da leggero regolarizzatore. In pratica, si inserisce un layer BatchNorm dopo ogni convoluzione, prima dell'attivazione.
4. Architettura CNN Tipica
Una CNN standard segue un pattern ricorrente: blocchi di estrazione feature (convoluzione + attivazione + pooling) seguiti da layer fully connected per la classificazione finale. Con la profondità, le feature maps diminuiscono in dimensione spaziale ma aumentano in numero di canali, catturando pattern sempre più astratti.
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]
Gerarchia delle Feature Apprese
- Layer 1-2 (basso livello): Bordi, gradienti di colore, texture semplici
- Layer 3-5 (medio livello): Angoli, contorni, parti di oggetti (occhi, ruote)
- Layer 6+ (alto livello): Oggetti completi, scene, concetti astratti
Questa gerarchia emerge automaticamente durante il training. Non c'è bisogno di indicare alla rete cosa cercare: i filtri si adattano ai dati.
5. Evoluzione delle Architetture CNN
La storia delle CNN è segnata da architetture rivoluzionarie, ciascuna delle quali ha introdotto idee che hanno cambiato il campo. Conoscere questa evoluzione è fondamentale per capire le scelte architetturali moderne.
Timeline delle Architetture CNN
| Anno | Architettura | Innovazione Chiave | Top-1 ImageNet |
|---|---|---|---|
| 1998 | LeNet-5 | Prima CNN pratica (riconoscimento cifre) | N/A |
| 2012 | AlexNet | GPU training, ReLU, Dropout | 63.3% |
| 2014 | VGGNet | Reti profonde con filtri 3x3 uniformi | 74.5% |
| 2014 | GoogLeNet/Inception | Moduli Inception, multi-scala parallela | 74.8% |
| 2015 | ResNet | Skip connections, reti 152+ layer | 78.6% |
| 2019 | EfficientNet | Compound scaling (profondità+larghezza+risoluzione) | 84.4% |
| 2022 | ConvNeXt | CNN modernizzata ispirata ai Vision Transformers | 87.8% |
5.1 LeNet-5 (1998) - La Pioniera
Progettata da Yann LeCun per il riconoscimento di cifre scritte a mano (MNIST), LeNet-5 e la prima CNN ad avere successo pratico. Con soli 5 layer e 60.000 parametri, ha dimostrato che le convoluzioni potevano apprendere feature discriminanti dalle immagini. Era usata per leggere automaticamente gli assegni bancari.
5.2 AlexNet (2012) - La Rivoluzione
AlexNet ha vinto la competizione ImageNet 2012 con un margine enorme, riducendo l'errore dal 26% al 16%. Le innovazioni chiave: training su GPU (due NVIDIA GTX 580), funzione di attivazione ReLU al posto della sigmoide, Dropout per la regolarizzazione e data augmentation. Questo risultato ha convinto il mondo accademico e industriale che il deep learning funzionava.
5.3 VGGNet (2014) - La Profondità Conta
VGG ha dimostrato che reti più profonde producono risultati migliori. La sua idea chiave e radicale nella semplicità: usare solo filtri 3x3 impilati. Due layer 3x3 consecutivi hanno lo stesso campo recettivo di un singolo layer 5x5, ma con meno parametri e più non-linearita. VGG-16 ha 16 layer e 138 milioni di parametri.
5.4 EfficientNet (2019) - Scaling Intelligente
EfficientNet ha introdotto il compound scaling: invece di aumentare solo la profondità (come VGG) o la larghezza, scala uniformemente tutte e tre le dimensioni (profondità, larghezza, risoluzione dell'input) con coefficienti bilanciati. EfficientNet-B0 raggiunge il 77.1% di accuracy su ImageNet con soli 5.3 milioni di parametri, un rapporto accuracy-parametri senza precedenti.
5.5 ConvNeXt (2022) - La CNN Modernizzata
ConvNeXt dimostra che le CNN, modernizzate con tecniche ispirate ai Vision Transformers, possono competere con (e superare) le architetture transformer. Le innovazioni includono: kernel 7x7 depthwise separabili, LayerNorm al posto di BatchNorm, attivazione GELU, e un design "isomorphic" con stadi di dimensioni crescenti. ConvNeXt V2, nella versione E-ConvNeXt-Tiny, raggiunge l'80.6% Top-1 con soli 2.0 GFLOPs, eccellente per deployment efficiente.
6. ResNet e Skip Connections
ResNet (Residual Networks), proposta da He et al. nel 2015, ha risolto uno dei problemi fondamentali del deep learning: il degradation problem. Prima di ResNet, aggiungere layer a una rete profonda peggiorava i risultati anziche migliorarli, anche sul training set. La soluzione è tanto elegante quanto semplice.
6.1 Il Problema del Vanishing Gradient
Durante la backpropagation, i gradienti vengono moltiplicati ripetutamente attraverso i layer della rete. Se queste moltiplicazioni producono valori minori di 1, i gradienti "svaniscono" esponenzialmente man mano che si propagano verso i layer iniziali. Con 50 o più layer, i gradienti diventano cosi piccoli che i primi layer smettono di apprendere. Studi recenti confermano che senza skip connections, le norme L2 dei gradienti calano drasticamente nei layer iniziali, mentre con le skip connections rimangono uniformi lungo tutta la rete.
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 La Soluzione: Residual Learning
L'idea geniale di ResNet e semplice: invece di far apprendere a un blocco la trasformazione
completa H(x), gli fai apprendere solo la differenza (residuo)
rispetto all'input: F(x) = H(x) - x. L'output del blocco diventa
y = F(x) + x, dove x è l'input che "salta" il blocco tramite
una skip connection (o shortcut connection).
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.
perchè Funziona
Apprendere il residuo F(x) = 0 (cioe "non fare nulla") è molto più facile
che apprendere una trasformazione identità completa. Se un layer non è utile, la rete
semplicemente impara F(x) = 0 e passa l'input invariato. Questo permette
di costruire reti con centinaia di layer senza degradazione delle prestazioni.
7. Training di una CNN
Addestrare una CNN significa trovare i valori ottimali per tutti i kernel (filtri) e i pesi dei layer fully connected. Questo avviene attraverso un processo iterativo di forward pass, calcolo della loss e backpropagation dei gradienti.
7.1 Loss Function
Per la classificazione di immagini, la loss function standard è la Cross-Entropy Loss. Misura quanto le probabilità predette dalla rete si discostano dalle etichette reali. Una predizione perfetta produce loss = 0; una predizione completamente sbagliata produce loss tendente a infinito.
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 Optimizer
L'optimizer aggiorna i pesi della rete nella direzione che riduce la loss. I più usati sono:
Optimizer a Confronto
| Optimizer | Caratteristiche | Quando Usarlo |
|---|---|---|
| SGD + Momentum | Semplice, robusto, convergenza stabile | Training lungo, massima accuracy finale |
| Adam | Learning rate adattivo, convergenza rapida | Prototyping, reti piccole/medie |
| AdamW | Adam con weight decay corretto | Standard moderno, raccomandato per CNN |
7.3 Data Augmentation
La data augmentation è una tecnica fondamentale per prevenire l'overfitting e migliorare la generalizzazione della rete. Consiste nell'applicare trasformazioni casuali alle immagini di training (rotazioni, flip, crop, cambi di luminosita) per creare variazioni sintetiche senza raccogliere nuovi dati.
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. Implementazione Completa in PyTorch
Passiamo dalla teoria alla pratica. Implementeremo una CNN completa per classificare le immagini del dataset CIFAR-10 (60.000 immagini 32x32 in 10 classi: aeroplano, automobile, uccello, gatto, cervo, cane, rana, cavallo, nave, camion).
8.1 Setup e Dataset
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 Definizione del Modello
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 Training Loop
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%
Parametri di Training Consigliati per CIFAR-10
| Parametro | Valore | Motivazione |
|---|---|---|
| Batch size | 128 | Bilanciamento tra velocità e stabilità del gradiente |
| Learning rate | 0.1 (con OneCycleLR) | SGD con scheduling raggiunge accuracy superiore |
| Weight decay | 1e-4 | Regolarizzazione L2 per prevenire overfitting |
| Epoche | 50 | Con OneCycleLR, 50 epoche bastano per convergere |
| Dropout | 0.3 | Regolarizzazione aggiuntiva prima del layer finale |
9. Transfer Learning
Nella pratica, raramente si addestra una CNN da zero. Il transfer learning permette di riutilizzare modelli pre-addestrati su grandi dataset (come ImageNet con 1.2 milioni di immagini e 1.000 classi) e adattarli al proprio problema specifico. Questo riduce drasticamente i tempi di training, la quantità di dati necessaria e migliora le prestazioni.
9.1 Feature Extraction vs Fine-Tuning
Due Strategie di Transfer Learning
| Strategia | Come Funziona | Quando Usarla |
|---|---|---|
| Feature Extraction | Congela tutti i layer pre-addestrati, addestra solo il classificatore finale | Pochi dati (<1.000 immagini), dominio simile a ImageNet |
| Fine-Tuning | Scongela gli ultimi N layer e riaddestra con learning rate basso | Più dati disponibili, dominio diverso da 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. Metriche di Valutazione
L'accuracy da sola non basta per capire se una CNN funziona bene. Con dataset sbilanciati (es. 95% classe A, 5% classe B), un modello che predice sempre "classe A" ha 95% di accuracy ma è completamente inutile. Servono metriche più granulari.
Metriche per la Classificazione di Immagini
| Metrica | Cosa Misura | Formula |
|---|---|---|
| Accuracy | Percentuale di predizioni corrette sul totale | TP + TN / Totale |
| Precision | Tra le predizioni positive, quante sono corrette? | TP / (TP + FP) |
| Recall | Tra i positivi reali, quanti sono stati trovati? | TP / (TP + FN) |
| F1 Score | Media armonica di precision e recall | 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. Deployment: Da Modello Addestrato a Produzione
Addestrare un modello è solo metà del lavoro. Per portarlo in produzione devi esportarlo in un formato ottimizzato, creare un'API di inference e containerizzare il tutto per il deployment scalabile.
11.1 Esportazione del Modello
Formati di Esportazione
| Formato | Uso | Vantaggi |
|---|---|---|
| TorchScript | Inference PyTorch senza Python | Nessuna dipendenza Python, serializzazione completa |
| ONNX | Formato universale, multi-framework | Compatibile con TensorRT, OpenVINO, CoreML |
| TensorRT | Inference ottimizzata per GPU NVIDIA | Fino a 5x più veloce di PyTorch nativo |
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 API di Inference con 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 Containerizzazione con 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
Conclusioni e Prossimi Passi
In questo articolo abbiamo costruito una comprensione completa delle Convolutional Neural Networks, partendo dai fondamenti (come un computer vede le immagini, l'operazione di convoluzione) fino all'implementazione pratica (CNN con residual blocks in PyTorch) e al deployment in produzione (TorchScript, ONNX, FastAPI, Docker).
Abbiamo visto come l'evoluzione delle architetture, da LeNet-5 nel 1998 a ConvNeXt nel 2022, abbia progressivamente migliorato le performance grazie a idee come le skip connections (ResNet), il compound scaling (EfficientNet) e il design ispirato ai transformer (ConvNeXt).
Punti Chiave da Ricordare
- Le CNN sfruttano localita, condivisione dei pesi e invarianza spaziale per processare immagini efficientemente
- L'architettura standard segue il pattern: Conv + BatchNorm + ReLU + Pooling, ripetuto con profondità crescente
- Le skip connections (ResNet) sono essenziali per addestrare reti profonde senza vanishing gradient
- Il transfer learning è quasi sempre preferibile all'addestramento da zero, specialmente con pochi dati
- La data augmentation è fondamentale per la generalizzazione e costa zero in termini di raccolta dati
- Per la produzione, esporta in ONNX o TorchScript e containerizza con Docker
Nel prossimo articolo della serie approfondiremo il Transfer Learning e il Fine-Tuning: come scegliere il modello pre-addestrato giusto, le strategie di fine-tuning progressivo, il domain adaptation e tecniche avanzate come il knowledge distillation. Nel terzo articolo affronteremo la Object Detection con YOLO, il sistema di rilevamento oggetti in tempo reale più utilizzato nell'industria.
Risorse Aggiuntive
- Paper ResNet originale: "Deep Residual Learning for Image Recognition" (He et al., 2015)
- Paper ConvNeXt: "A ConvNet for the 2020s" (Liu et al., 2022)
- Paper EfficientNet: "EfficientNet: Rethinking Model Scaling for CNNs" (Tan & Le, 2019)
- PyTorch Documentation: Tutorials su CNN e torchvision
- CS231n Stanford: Convolutional Neural Networks for Visual Recognition (corso online)
- ONNX Runtime: Documentazione per inference multi-piattaforma ottimizzata







