Introduzione: Come le CNN Vedono il Mondo
Le Reti Neurali Convoluzionali (CNN) hanno rivoluzionato la computer vision, rendendo possibile il riconoscimento automatico di immagini con accuratezza sovrumana. A differenza delle reti fully-connected, le CNN sfruttano la struttura spaziale dei dati: pixel vicini tendono ad essere correlati, e pattern locali (bordi, texture) si ripetono in diverse posizioni dell'immagine.
L'idea chiave delle CNN e l'operazione di convoluzione: un filtro (kernel) scorre sull'immagine estraendo feature locali. Strato dopo strato, la rete costruisce una gerarchia di feature sempre più astratte: da bordi semplici a forme complesse fino a oggetti completi.
Cosa Imparerai
- L'operazione di convoluzione: kernel, stride, padding
- Pooling: riduzione dimensionale e invarianza alla posizione
- Architetture storiche: LeNet, AlexNet, VGG, ResNet
- Skip connections e il problema del vanishing gradient nelle reti profonde
- Transfer learning: riutilizzare modelli pre-addestrati su ImageNet
- Data augmentation per migliorare la robustezza del modello
- Implementazione completa in PyTorch con training e valutazione
L'Operazione di Convoluzione
Il cuore di una CNN e il layer convoluzionale. Un piccolo filtro (kernel), tipicamente di dimensione 3x3 o 5x5, scorre sull'immagine calcolando il prodotto elemento per elemento tra il filtro e la porzione di immagine sottostante. Il risultato e una feature map che evidenzia la presenza di un pattern specifico in ogni posizione.
Due parametri controllano il comportamento della convoluzione:
- Stride: il passo con cui il kernel si muove. Stride=1 produce feature map della stessa dimensione, stride=2 dimezza le dimensioni
- Padding: aggiunta di zeri ai bordi dell'immagine. "Same" padding mantiene le dimensioni originali, "valid" padding le riduce
import torch
import torch.nn as nn
# Convoluzione 2D: 3 canali input (RGB), 16 filtri output, kernel 3x3
conv_layer = nn.Conv2d(in_channels=3, out_channels=16, kernel_size=3,
stride=1, padding=1)
# Input: batch di 4 immagini RGB 32x32
x = torch.randn(4, 3, 32, 32)
output = conv_layer(x)
print(f"Input shape: {x.shape}") # [4, 3, 32, 32]
print(f"Output shape: {output.shape}") # [4, 16, 32, 32]
# Con stride=2 e padding=1
conv_stride2 = nn.Conv2d(3, 16, kernel_size=3, stride=2, padding=1)
output_s2 = conv_stride2(x)
print(f"Stride 2 output: {output_s2.shape}") # [4, 16, 16, 16]
Pooling: Riduzione Dimensionale
Dopo la convoluzione, i layer di pooling riducono le dimensioni spaziali delle feature map, diminuendo il numero di parametri e il costo computazionale. Il pooling introduce anche una forma di invarianza alla traslazione: piccoli spostamenti dell'oggetto nell'immagine non cambiano la feature estratta.
I due tipi principali sono:
- Max Pooling: seleziona il valore massimo in ogni finestra. Preserva le feature più prominenti ed e il tipo più usato
- Average Pooling: calcola la media in ogni finestra. Produce feature più smooth, usato tipicamente prima dell'output
perchè le CNN Funzionano Cosi Bene
Le CNN sfruttano tre proprietà fondamentali delle immagini: localita (feature importanti sono locali), condivisione dei pesi (lo stesso filtro si applica ovunque, riducendo drasticamente i parametri) e invarianza alla traslazione (un gatto e un gatto sia al centro che in un angolo dell'immagine). Queste proprietà rendono le CNN ordini di grandezza più efficienti delle reti fully-connected per dati con struttura spaziale.
Architetture Storiche: Da LeNet a ResNet
LeNet-5 (1998)
Progettata da Yann LeCun per il riconoscimento di cifre scritte a mano, LeNet-5 e la prima CNN di successo. Con soli 5 layer (2 convoluzioni + 3 fully connected), dimostro che le reti convoluzionali potevano battere i metodi tradizionali di feature engineering.
VGG (2014)
VGGNet dimostro che la profondità conta: usando esclusivamente kernel 3x3 impilati in 16 o 19 layer, raggiunse performance eccellenti su ImageNet. Due filtri 3x3 in sequenza coprono lo stesso campo recettivo di un filtro 5x5, ma con meno parametri e più non-linearita.
ResNet (2015)
ResNet (Residual Network) risolse il problema del training di reti molto profonde con le skip connections: invece di imparare una trasformazione diretta F(x), ogni blocco impara la differenza residua F(x) + x. Questo permette al gradiente di fluire direttamente attraverso i layer, rendendo possibile l'addestramento di reti con 152+ layer.
import torch
import torch.nn as nn
class ResidualBlock(nn.Module):
"""Blocco residuo base di ResNet"""
def __init__(self, channels):
super().__init__()
self.conv1 = nn.Conv2d(channels, channels, kernel_size=3, padding=1)
self.bn1 = nn.BatchNorm2d(channels)
self.conv2 = nn.Conv2d(channels, channels, kernel_size=3, padding=1)
self.bn2 = nn.BatchNorm2d(channels)
self.relu = nn.ReLU(inplace=True)
def forward(self, x):
identity = x # Skip connection
out = self.relu(self.bn1(self.conv1(x)))
out = self.bn2(self.conv2(out))
out += identity # Residual: F(x) + x
return self.relu(out)
class SimpleCNN(nn.Module):
"""CNN per classificazione CIFAR-10"""
def __init__(self, num_classes=10):
super().__init__()
self.features = nn.Sequential(
nn.Conv2d(3, 32, kernel_size=3, padding=1),
nn.BatchNorm2d(32),
nn.ReLU(inplace=True),
nn.MaxPool2d(2, 2), # 32x32 -> 16x16
nn.Conv2d(32, 64, kernel_size=3, padding=1),
nn.BatchNorm2d(64),
nn.ReLU(inplace=True),
nn.MaxPool2d(2, 2), # 16x16 -> 8x8
nn.Conv2d(64, 128, kernel_size=3, padding=1),
nn.BatchNorm2d(128),
nn.ReLU(inplace=True),
nn.AdaptiveAvgPool2d((1, 1)) # 8x8 -> 1x1
)
self.classifier = nn.Linear(128, num_classes)
def forward(self, x):
x = self.features(x)
x = x.view(x.size(0), -1)
return self.classifier(x)
model = SimpleCNN()
x = torch.randn(8, 3, 32, 32)
print(f"Output: {model(x).shape}") # [8, 10]
Transfer Learning: Riutilizzare Modelli Pre-Addestrati
Addestrare una CNN da zero su grandi dataset richiede enormi risorse computazionali. Il transfer learning risolve questo problema: si prende un modello pre-addestrato su un dataset enorme (tipicamente ImageNet con 14 milioni di immagini) e lo si adatta al proprio task specifico.
La strategia più comune prevede due fasi:
- Feature extraction: si congelano i pesi del modello pre-addestrato e si sostituisce solo il classificatore finale
- Fine-tuning: si scongelano alcuni layer superiori e si ri-addestrano con un learning rate molto basso
import torchvision.models as models
# Caricare ResNet50 pre-addestrato su ImageNet
model = models.resnet50(weights=models.ResNet50_Weights.DEFAULT)
# Congelare tutti i parametri
for param in model.parameters():
param.requires_grad = False
# Sostituire il classificatore finale per 5 classi
num_features = model.fc.in_features
model.fc = nn.Sequential(
nn.Dropout(0.3),
nn.Linear(num_features, 256),
nn.ReLU(),
nn.Dropout(0.2),
nn.Linear(256, 5) # 5 classi custom
)
# Solo i parametri del nuovo classificatore saranno addestrati
trainable = sum(p.numel() for p in model.parameters() if p.requires_grad)
total = sum(p.numel() for p in model.parameters())
print(f"Trainable: {trainable:,} / {total:,} parametri")
Data Augmentation: Robustezza attraverso le Trasformazioni
La data augmentation e una tecnica di regolarizzazione che aumenta artificialmente la diversità del dataset di training applicando trasformazioni casuali alle immagini. Rotazioni, ritagli, flip orizzontali e variazioni di colore insegnano alla rete ad essere invariante a queste trasformazioni.
from torchvision import transforms
# Pipeline di data augmentation per training
train_transform = transforms.Compose([
transforms.RandomResizedCrop(224, scale=(0.8, 1.0)),
transforms.RandomHorizontalFlip(p=0.5),
transforms.RandomRotation(15),
transforms.ColorJitter(brightness=0.2, contrast=0.2,
saturation=0.2, hue=0.1),
transforms.RandomAffine(degrees=0, translate=(0.1, 0.1)),
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406],
[0.229, 0.224, 0.225]),
transforms.RandomErasing(p=0.1)
])
# Per validation/test: solo resize e normalize
val_transform = transforms.Compose([
transforms.Resize(256),
transforms.CenterCrop(224),
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406],
[0.229, 0.224, 0.225])
])
Prossimi Passi nella Serie
- Nel prossimo articolo esploreremo le Reti Neurali Ricorrenti (RNN) e LSTM per l'elaborazione di sequenze
- Vedremo come LSTM risolvono il vanishing gradient e modellano dipendenze temporali
- Implementeremo un modello di sentiment analysis e text generation







