Vai al contenuto
Machine Learning e Reti Neurali

Le reti convoluzionali: convoluzione, equivarianza alle traslazioni, pooling e dimensioni

La convoluzione in due dimensioni, l'equivarianza alle traslazioni dimostrata, pooling, formule delle dimensioni e del numero di parametri, LeNet, AlexNet e ResNet, rete densa contro CNN sulle cifre scritte a mano.

Un’immagine da un megapixel a colori ha tre milioni di valori: una rete densa con mille neuroni nel primo strato avrebbe tre miliardi di pesi solo lì, e dovrebbe imparare separatamente che un gatto in alto a sinistra e un gatto in basso a destra sono la stessa cosa. Le reti convoluzionali (CNN) risolvono entrambi i problemi con due idee prese dall’elaborazione dei segnali: i pesi sono locali (ogni neurone guarda solo una piccola zona) e condivisi (lo stesso filtro scorre su tutta l’immagine). Questa lezione definisce la convoluzione, dimostra la proprietà che rende le CNN adatte alle immagini (l’equivarianza alle traslazioni), ricava le formule delle dimensioni, e confronta una CNN con una rete densa sulle cifre scritte a mano.

La convoluzione

Uno strato convoluzionale applica un filtro (kernel) K di piccole dimensioni, per esempio 3×3, a ogni posizione dell’immagine I:

    \[ (I * K)(i, j) = \sum_{m}\sum_{n}I(i + m,\ j + n)\,K(m, n) \]

(nelle reti neurali si usa questa forma, che a rigore è una correlazione; la convoluzione vera ha il filtro capovolto, ma dato che il filtro si impara la differenza non conta). È lo stesso filtro FIR dell’elaborazione numerica dei segnali, in due dimensioni. Con più canali d’ingresso (i tre colori, o le uscite dello strato precedente) il filtro ha un canale per ognuno e i contributi si sommano; con più filtri si ottengono più mappe di caratteristiche in uscita.

Un filtro 3×3 con valori \begin{pmatrix}-1 & 0 & 1\end{pmatrix} su ogni riga, per esempio, risponde ai bordi verticali. La rete non riceve filtri scritti a mano: li impara con la retropropagazione, e nei primi strati di una CNN addestrata compaiono proprio rivelatori di bordi e di macchie di colore; negli strati successivi, combinazioni sempre più complesse (angoli, texture, parti di oggetti).

La convoluzione in due dimensioni
Il filtro 3 per 3, arancione, scorre sull’immagine d’ingresso; in ogni posizione la somma dei prodotti tra i pesi del filtro e i pixel sotto di esso dà un valore della mappa di uscita, in blu. Lo stesso filtro, con gli stessi pesi, è usato in tutte le posizioni.

Perché funziona: l’equivarianza alle traslazioni

Teorema. La convoluzione commuta con le traslazioni: se si sposta l’immagine, la mappa di uscita si sposta della stessa quantità. Con T_{a,b} la traslazione (T_{a,b}I)(i, j) = I(i - a, j - b): (T_{a,b}I) * K = T_{a,b}(I * K).

Dimostrazione. \big((T_{a,b}I) * K\big)(i, j) = \sum_{m,n}I(i - a + m,\ j - b + n)\,K(m, n) = (I * K)(i - a,\ j - b) = \big(T_{a,b}(I * K)\big)(i, j). ∎

Un rivelatore di occhi imparato in un angolo dell’immagine funziona automaticamente ovunque: non bisogna impararlo di nuovo per ogni posizione. Questo pregiudizio induttivo (una conoscenza a priori sul problema, inserita nella struttura della rete) riduce enormemente i parametri e i dati necessari. Per ottenere anche una parziale invarianza (stessa risposta anche se l’oggetto si sposta) si aggiunge il pooling: il max-pooling 2×2 sostituisce ogni blocco di 2×2 valori con il massimo, dimezzando le dimensioni e rendendo la rete poco sensibile a spostamenti piccoli.

Le dimensioni

Per un ingresso di lato n, filtro di lato k, padding p (bordo di zeri aggiunto) e passo (stride) s, il lato dell’uscita è

    \[ n_{out} = \Big\lfloor\frac{n + 2p - k}{s}\Big\rfloor + 1 \]

(il filtro parte dalla posizione 0 e può fare \lfloor(n + 2p - k)/s\rfloor spostamenti). Con k = 3, p = 1, s = 1 l’uscita ha le stesse dimensioni dell’ingresso. Uno strato con C_{in} canali d’ingresso e C_{out} filtri ha C_{out}(k^2C_{in} + 1) parametri, indipendentemente dalla dimensione dell’immagine. Il campo recettivo (la zona d’ingresso che influenza un neurone) cresce di k - 1 pixel per ogni strato 3×3 e raddoppia con ogni pooling: impilando strati, i neuroni profondi “vedono” tutta l’immagine.

Le architetture storiche mostrano l’evoluzione: LeNet (1998) leggeva i codici postali; AlexNet (2012) vinse la gara ImageNet con un margine enorme usando le GPU e la ReLU, dando inizio all’era del deep learning; ResNet (2015) aggiunse le connessioni residue \mathbf h_{l+1} = \mathbf h_l + F(\mathbf h_l), che permettono al gradiente di attraversare centinaia di strati senza svanire.

L’esperimento: rete densa contro rete convoluzionale

Sulle 1797 cifre scritte a mano 8×8 di scikit-learn (1347 per l’addestramento, 450 per la prova), due reti addestrate con Adam per 60 epoche:

  • rete densa: 64 ingressi → 64 neuroni ReLU → 10 uscite;
  • CNN: due strati convoluzionali 3×3 (8 e 16 filtri) con ReLU, un max-pooling 2×2, uno strato denso finale.
reteparametriaccuratezza di provacon l’immagine spostata di 1 pixel
densa4 81096,9 %42,9 %
convoluzionale3 81898,2 %65,1 %

La CNN è più precisa con meno parametri, perché la struttura convoluzionale le dice già che pixel vicini vanno guardati insieme. Spostando tutte le immagini di prova di un pixel a destra (un cambiamento minimo per un occhio umano), la rete densa crolla al 43 %: ha imparato “questo pixel acceso significa 7”, non la forma. La CNN regge meglio ma perde comunque molto, perché su immagini 8×8 un pixel è più del 10 % della larghezza e il pooling dà solo una piccola invarianza. In pratica si aggiunge l’aumento dei dati (data augmentation): si addestra anche su copie spostate, ruotate, ingrandite delle immagini.

import numpy as np, torch, torch.nn as nn
from sklearn.datasets import load_digits
from sklearn.model_selection import train_test_split

d = load_digits()
Xa, Xp, ya, yp = train_test_split(d.data / 16.0, d.target, test_size=0.25, random_state=0, stratify=d.target)
Xa, Xp = torch.tensor(Xa, dtype=torch.float32), torch.tensor(Xp, dtype=torch.float32)
ya, yp = torch.tensor(ya), torch.tensor(yp)

def addestra(modello, epoche=60):
    torch.manual_seed(0)
    opt = torch.optim.Adam(modello.parameters(), lr=1e-3)
    for _ in range(epoche):
        perm = torch.randperm(len(Xa))
        for i in range(0, len(Xa), 64):
            idx = perm[i:i + 64]
            perdita = nn.functional.cross_entropy(modello(Xa[idx]), ya[idx])
            opt.zero_grad(); perdita.backward(); opt.step()
    return modello

def accuratezza(modello, X):
    with torch.no_grad():
        return float((modello(X).argmax(1) == yp).float().mean())

torch.manual_seed(0)
densa = nn.Sequential(nn.Linear(64, 64), nn.ReLU(), nn.Linear(64, 10))
torch.manual_seed(0)
cnn = nn.Sequential(nn.Unflatten(1, (1, 8, 8)),                    # da 64 valori a immagine 1x8x8
                    nn.Conv2d(1, 8, 3, padding=1), nn.ReLU(),       # 8 filtri 3x3
                    nn.Conv2d(8, 16, 3, padding=1), nn.ReLU(),      # 16 filtri 3x3x8
                    nn.MaxPool2d(2),                                # 8x8 -> 4x4
                    nn.Flatten(), nn.Linear(16 * 4 * 4, 10))
addestra(densa); addestra(cnn)

spostate = torch.roll(Xp.view(-1, 8, 8), 1, dims=2)                 # tutte le cifre un pixel a destra
spostate[:, :, 0] = 0
spostate = spostate.view(-1, 64)
for nome, m in (("densa", densa), ("convoluzionale", cnn)):
    n = sum(p.numel() for p in m.parameters())
    print(f"{nome:15s}: {n} parametri, prova {accuratezza(m, Xp):.4f}, spostate {accuratezza(m, spostate):.4f}")
Subscribe
Notify of
guest
0 Comments
Oldest
Newest Most Voted
0
Would love your thoughts, please comment.x
()
x