Salta al contenuto principale

01 - CNN: Reti Convoluzionali da Zero a Production

Guida completa a CNN: Reti Convoluzionali da Zero a Production

Cosa ne pensi di questo articolo?

01 - CNN: Reti Convoluzionali da Zero a Production
Computer VisionFoto:Ruben BoekelooPexels

In Breve

Guida completa a CNN: Reti Convoluzionali da Zero a Production

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
1Sei qui - CNN: Reti ConvoluzionaliArchitettura, training, deployment
2Transfer Learning e Fine-TuningModelli pre-addestrati, domain adaptation
3Object Detection con YOLORilevamento oggetti in tempo reale
4Segmentazione SemanticaClassificazione a livello di pixel
5Image Generation con GAN e DiffusionGenerazione immagini sintetiche
6Edge Deployment e OttimizzazioneModelli 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.

Rappresentazione di un'Immagine 4x4 in Scala di Grigi

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.

Convoluzione 3x3 - Visualizzazione Passo per Passo

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
Effetto di Stride e Padding sulla Dimensione dell'Output

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.

Funzioni di Attivazione a Confronto

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).

Max Pooling 2x2 con Stride 2

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.

Architettura CNN Standard per Classificazione

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.

Vanishing Gradient - Il Problema

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).

Residual Block - Struttura

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.

Cross-Entropy Loss - Esempio

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.

Trasformazioni di Data Augmentation Comuni

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

Caricamento Dataset e Data Augmentation

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

CNN con Residual Blocks per CIFAR-10

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

Training Loop con Validazione e Learning Rate Scheduling

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
Transfer Learning con ResNet-18 Pre-Addestrato

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)
    
Quando Usare Quale Strategia

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 - Esempio per CIFAR-10

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"
    
Calcolo delle Metriche in PyTorch

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
Esportazione in TorchScript e ONNX

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

Servizio 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

Dockerfile per il Servizio di Inference

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 di Deployment Completa

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

Prova Questi Strumenti

Strumenti gratuiti correlati a questo argomento.

Commenti

Caricamento commenti...

Accedi per commentare

Discuti questo articolo nella community

Unisciti al gruppo Telegram per discutere con altri sviluppatori, fare domande e condividere le tue esperienze.

Ti è piaciuto questo articolo?

Esplora altri contenuti sul blog o scopri i miei progetti