Una architettura ibrida combina blocchi pensati per scopi diversi, in modo che ciascuno faccia ciò che sa fare meglio. In questo modulo il primo esempio è il più naturale: una rete convoluzionale (CNN) che guarda ogni immagine e ne estrae «cosa c'è e dove», seguita da una rete ricorrente (LSTM) che legge la sequenza di queste descrizioni nell'ordine e ne ricava «come cambia nel tempo». È la struttura di base per classificare un breve video, per riconoscere un gesto, per capire se un oggetto si avvicina o si allontana. In questa lezione la costruisci a mano, capisci perché non basta né una CNN sola né una LSTM sola, e verifichi con un esperimento che togliere la ricorrenza fa perdere informazione in un modo che si può dimostrare.
Le parole di questa lezione
Un'immagine in scala di grigi è una griglia di numeri (altezza × larghezza) con un solo canale; un'immagine a colori ha tre canali (rosso, verde, blu). Un frame è un'immagine che fa parte di un video, e una clip è una sequenza di frame. Una convoluzione usa una piccola griglia di pesi, il kernel (ad esempio 3×3), che si fa scorrere sull'immagine: in ogni posizione si moltiplicano i pesi per i pixel sotto di essi e si sommano i prodotti. Il risultato è una nuova griglia, la mappa delle caratteristiche (feature map), che «si accende» dove nell'immagine c'è il motivo che il kernel cerca, come un bordo o un angolo.
Il pooling riduce la risoluzione di una mappa tenendo, ad esempio, il massimo in ogni blocco 2×2 (max pooling): la rappresentazione diventa più piccola e meno sensibile a spostamenti di un pixel. Un encoder è la parte di una rete che trasforma un ingresso grande e grezzo, come un'immagine, in un vettore compatto che ne riassume il contenuto. Il tensore a cinque assi (B, T, C, H, W) è la forma tipica di un batch di clip: batch (quante clip), tempo (quanti frame), canali, altezza, larghezza. Infine time-distributed significa applicare la stessa rete a ogni frame di una clip, con gli stessi pesi, e raccogliere i risultati in una sequenza: è ciò che per la RNN era la condivisione dei pesi nel tempo.
L'analogia: un analista di fotografie e un narratore
Immagina di dover capire dove va un'auto da una sequenza di sei fotografie. Assumi due persone. La prima, l'analista, guarda una foto alla volta e scrive su un biglietto una descrizione breve: «auto al centro, vicino al semaforo». Usa lo stesso metodo per tutte le foto. Il secondo, il narratore, non vede le foto: riceve i biglietti nell'ordine e li legge uno dopo l'altro, tenendo a mente la storia fin lì, per concludere «l'auto sta andando verso destra». L'analista è la CNN, il narratore è l'LSTM, i biglietti sono i vettori. Se mescoli i biglietti prima di darli al narratore, o se calcoli la «media» delle descrizioni, la storia sparisce: resta l'elenco di dove l'auto è stata, non la direzione in cui si muoveva.
La convoluzione, con numeri veri
Prima di assemblare le parti, vediamo cosa fa il blocco che guarda lo spazio. Prendiamo un'immagine 4×4 con un bordo verticale (a sinistra scuro, a destra chiaro) e un kernel 2×2 che risponde proprio a «sinistra scuro, destra chiaro». Facciamo scorrere il kernel a mano e poi con la libreria.
import numpy as np
import torch
import torch.nn.functional as F
image = np.array([[0, 0, 1, 1],
[0, 0, 1, 1],
[0, 0, 1, 1],
[0, 0, 1, 1]], dtype=float) # dark on the left, bright on the right: a vertical edge
kernel = np.array([[-1, 1],
[-1, 1]], dtype=float) # responds to "left dark, right bright"
# By hand: slide the kernel, multiply element by element, sum
out = np.zeros((3, 3))
for i in range(3):
for j in range(3):
patch = image[i:i + 2, j:j + 2]
out[i, j] = (patch * kernel).sum()
print(out)
# The library: same numbers (note: deep-learning "convolution" is really cross-correlation, no kernel flip)
lib = F.conv2d(torch.tensor(image).view(1, 1, 4, 4), torch.tensor(kernel).view(1, 1, 2, 2))
print(np.abs(lib.view(3, 3).numpy() - out).max())
[[0. 2. 0.]
[0. 2. 0.]
[0. 2. 0.]]
0.0
Spiegazione. Il doppio ciclo prende, per ogni posizione (i, j), la finestra 2×2 dell'immagine (patch), la moltiplica elemento per elemento con il kernel e somma. Nelle posizioni dove la finestra è tutta nera (colonne 0-1) o tutta bianca (colonne 2-3) il risultato è 0: a sinistra (−1)·0 + 1·0 + (−1)·0 + 1·0 = 0. Nella colonna centrale, dove la finestra contiene due pixel scuri a sinistra e due chiari a destra, si ha −0 + 1 − 0 + 1 = 2. La mappa risultante vale 2 esattamente in corrispondenza del bordo: il kernel è un «rilevatore di bordi». Una CNN impara i valori dei kernel dai dati invece di farli scegliere a un ingegnere, e ne impila molti strati. Il secondo blocco del codice dimostra che F.conv2d dà gli stessi numeri (differenza 0.0). Nota: le librerie chiamano «convoluzione» ciò che in matematica si chiama correlazione incrociata (il kernel non viene capovolto); per l'apprendimento è irrilevante, perché la rete impara comunque il kernel che serve.
Perché servono entrambe le parti
- Una CNN da sola vede un frame alla volta. Può dirti «c'è un quadrato in (4, 9)», non «si muove verso destra»: la direzione è una proprietà della differenza fra frame, che nessun frame contiene.
- Un'LSTM da sola sui pixel riceverebbe a ogni passo un vettore di
H × Wnumeri (256 per un'immagine 16×16, 4096 per una 64×64), e dovrebbe imparare da zero anche ciò che una convoluzione fa gratis: che i pixel vicini sono correlati, che lo stesso motivo può comparire ovunque. Troppi parametri e nessun bias utile. - Un MLP sull'intera clip appiattita funziona su problemi minuscoli, ma il numero di pesi cresce con frame × altezza × larghezza (lo misuriamo fra poco), ed è limitato a clip della lunghezza esatta vista in addestramento.
Le tre voci sono tre modi diversi di sbagliare: la CNN sola perde il tempo, l'LSTM sui pixel butta via la geometria, il MLP piatto paga ogni frame con nuovi pesi. L'ibrido risolve il problema dividendo il lavoro: la CNN comprime ogni frame (256 pixel → 32 numeri), l'LSTM, ora con ingressi piccoli e informativi, si concentra sul tempo.
Come leggere il diagramma, con le forme dei tensori. Il batch di clip ha forma (B, T, 1, H, W). La CNN è progettata per ricevere immagini, cioè tensori a quattro assi (N, C, H, W): il trucco time-distributed consiste nel «piegare» il tempo dentro il batch, con clip.reshape(B*T, C, H, W), cioè trattare ogni frame come un'immagine indipendente. La CNN produce (B·T, F), un vettore da F numeri per frame, e un secondo reshape(B, T, F) rimette il tempo al suo posto. L'LSTM riceve così (B, T, F), che è proprio ciò che si aspetta con batch_first=True. Il «piegare» è corretto perché i due assi, batch e tempo, sono adiacenti e in quest'ordine: se invece scambiassi l'ordine con un permute senza rimettere a posto, mescoleresti frame di clip diverse senza alcun errore di forma.
Codice: i dati sintetici
Per avere un risultato controllabile costruiamo un compito dove la risposta dipende soltanto dall'ordine dei frame. Una clip è formata da 6 frame 16×16; in ciascuno c'è un quadrato 3×3 acceso. Da un frame al successivo il quadrato si sposta di un pixel in una di quattro direzioni (destra, sinistra, giù, su); la posizione di partenza è casuale. L'etichetta è la direzione.
import torch
import torch.nn as nn
SIZE, T, SQ = 16, 6, 3 # 16x16 frames, 6 frames per clip, 3x3 square
DIRECTIONS = [(0, 1), (0, -1), (1, 0), (-1, 0)] # (dy, dx): right, left, down, up
def make_clips(batch, seed=None):
"""Each clip: a 3x3 square moves one pixel per frame in one of 4 directions. Label = direction."""
g = torch.Generator().manual_seed(seed) if seed is not None else None
labels = torch.randint(0, 4, (batch,), generator=g)
clips = torch.zeros(batch, T, 1, SIZE, SIZE)
for b in range(batch):
dy, dx = DIRECTIONS[labels[b]]
# choose a start so that the square stays inside the frame for all T frames
y0 = int(torch.randint(1, SIZE - SQ - 1 - T, (1,), generator=g)) + (T if dy < 0 else 0)
x0 = int(torch.randint(1, SIZE - SQ - 1 - T, (1,), generator=g)) + (T if dx < 0 else 0)
if dy == 0: y0 = int(torch.randint(1, SIZE - SQ, (1,), generator=g))
if dx == 0: x0 = int(torch.randint(1, SIZE - SQ, (1,), generator=g))
for t in range(T):
y, x = y0 + dy * t, x0 + dx * t
clips[b, t, 0, y:y + SQ, x:x + SQ] = 1.0
return clips, labels
x, y = make_clips(4, seed=0)
print(x.shape, y.tolist()) # torch.Size([4, 6, 1, 16, 16]) one integer per clip
La funzione make_clips genera un batch al volo. Estrae casualmente le etichette, e per ogni clip sceglie una posizione di partenza abbastanza lontana dai bordi da permettere sei spostamenti senza uscire (se la direzione è «su» o «sinistra», la posizione di partenza viene spostata di T pixel per lasciare spazio al movimento verso il bordo). Poi accende il quadrato in ogni frame. L'argomento seed permette di generare un insieme di test fisso, identico per tutti i modelli: è fondamentale per confrontare in modo equo. L'ultima riga stampa la forma (4, 6, 1, 16, 16) e quattro etichette.
Codice: tre architetture a confronto
Confrontiamo tre modi di affrontare lo stesso compito, tutti addestrati allo stesso modo (600 passi, Adam, learning rate 0.003, batch 32).
class FrameEncoder(nn.Module):
"""A small CNN: one 16x16 frame -> a vector of `dim` numbers (what is where in the frame)."""
def __init__(self, dim=32):
super().__init__()
self.conv = nn.Sequential(
nn.Conv2d(1, 8, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2), # 16 -> 8
nn.Conv2d(8, 16, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2), # 8 -> 4
)
self.fc = nn.Linear(16 * 4 * 4, dim)
def forward(self, frames): # frames: (N, 1, 16, 16)
return self.fc(self.conv(frames).flatten(1))
class CNNPool(nn.Module):
"""Baseline A: encode every frame, AVERAGE over time, classify. The order of frames is lost."""
def __init__(self, dim=32):
super().__init__()
self.enc, self.head = FrameEncoder(dim), nn.Linear(dim, 4)
def forward(self, clip): # clip: (B, T, 1, H, W)
B, T_, C, H, W = clip.shape
feats = self.enc(clip.reshape(B * T_, C, H, W)).reshape(B, T_, -1) # "time-distributed" CNN
return self.head(torch.relu(feats).mean(dim=1))
class CNNLSTM(nn.Module):
"""Hybrid: the CNN encodes each frame, an LSTM reads the sequence of vectors in order."""
def __init__(self, dim=32, hidden=32):
super().__init__()
self.enc = FrameEncoder(dim)
self.lstm = nn.LSTM(dim, hidden, batch_first=True)
self.head = nn.Linear(hidden, 4)
def forward(self, clip):
B, T_, C, H, W = clip.shape
feats = self.enc(clip.reshape(B * T_, C, H, W)).reshape(B, T_, -1)
_, (h_n, _) = self.lstm(torch.relu(feats))
return self.head(h_n[-1])
class FlatMLP(nn.Module):
"""Baseline B: flatten the whole clip (6*16*16 = 1536 numbers) into one big MLP."""
def __init__(self):
super().__init__()
self.net = nn.Sequential(nn.Linear(T * SIZE * SIZE, 64), nn.ReLU(), nn.Linear(64, 4))
def forward(self, clip):
return self.net(clip.flatten(1))
def fit(model, steps=600, seed=0):
torch.manual_seed(seed)
opt = torch.optim.Adam(model.parameters(), lr=3e-3)
for _ in range(steps):
x, y = make_clips(32)
loss = nn.functional.cross_entropy(model(x), y)
opt.zero_grad(); loss.backward(); opt.step()
with torch.no_grad():
x, y = make_clips(1000, seed=123) # a FIXED test set
return (model(x).argmax(-1) == y).float().mean().item()
for name, model in [("CNN + mean over time", CNNPool()), ("CNN + LSTM", CNNLSTM()), ("flat MLP", FlatMLP())]:
n_par = sum(p.numel() for p in model.parameters())
print(f"{name:22s} params {n_par:6d} test accuracy {fit(model):.3f}")
Spiegazione delle tre reti. FrameEncoder è una piccola CNN: due convoluzioni 3×3 con ReLU (la ReLU, max(0, x), è l'attivazione che azzera i valori negativi) e max-pooling che portano il frame da 16×16 a 4×4 con 16 canali, poi un livello lineare che lo riduce a 32 numeri. CNNPool è la baseline A: codifica ogni frame e poi fa la media nel tempo prima di classificare; è l'approccio di chi dice «aggrego le informazioni dei frame» senza pensare all'ordine. CNNLSTM è l'ibrido: stessa CNN, poi un'LSTM che legge i sei vettori in ordine, e la classificazione usa l'ultimo stato nascosto h_n[-1]. FlatMLP è la baseline B: appiattisce tutta la clip in 1536 numeri e usa una rete densa. La funzione fit è il solito ciclo; l'insieme di test è fisso (seed=123) e di 1000 clip.
CNN + mean over time params 9604 test accuracy 0.378
CNN + LSTM params 18052 test accuracy 1.000
flat MLP params 98628 test accuracy 1.000
Cosa dicono i numeri
La media nel tempo non ce la fa. L'accuratezza è circa 0.37: 0.378 nell'esecuzione riportata sopra, 0.372 e 0.355 in altre due esecuzioni dello stesso codice con lo stesso seme (questo modello non è riproducibile al millesimo, mentre CNN + LSTM e MLP piatto hanno dato 1.000 in tutte le esecuzioni). Con 2000 passi invece di 600 e tre seed si ottiene 0.349, 0.372 e 0.389: l'addestramento più lungo non migliora. Non è un difetto di addestramento: è un limite dell'informazione.
Una clip che va verso destra visita le posizioni x₀, x₀+1, …, x₀+5; una clip che va verso sinistra e parte da x₀+5 visita le stesse posizioni in ordine inverso. La media nel tempo dei vettori non distingue un ordinamento dall'altro, quindi destra e sinistra (e analogamente su e giù) producono la stessa media quando le posizioni visitate coincidono. Nel nostro generatore le posizioni di partenza di «sinistra» sono spostate di T pixel, quindi le distribuzioni non coincidono esattamente, e il tetto di circa 0.5 (si indovina l'asse, si tira a sorte il verso) è un'approssimazione del limite teorico, non un valore misurato. Il modello a circa 0.37 è comunque ben sotto: non ha imparato bene nemmeno l'asse.
CNN + LSTM risolve con 18.052 parametri e accuratezza 1.000. L'LSTM vede i vettori in ordine e può costruirsi l'informazione «la posizione cresce».
Il MLP piatto risolve anche lui, ma con 98.628 parametri, circa cinque volte tanti. Su un compito minuscolo con clip fisse, la forza bruta funziona. Il problema è cosa succede quando il compito cresce:
def params_flat_mlp(frames, side, hidden=64, classes=4):
return frames * side * side * hidden + hidden + hidden * classes + classes
def params_cnn_lstm(frames, side, dim=32, hidden=32, classes=4):
conv = (1 * 8 * 3 * 3 + 8) + (8 * 16 * 3 * 3 + 16) # two 3x3 convolutions
fc = 16 * (side // 4) ** 2 * dim + dim # after two 2x2 poolings the map is side/4
lstm = 4 * hidden * (dim + hidden + 2)
head = hidden * classes + classes
return conv + fc + lstm + head # NOT dependent on the number of frames
for frames, side in ((6, 16), (12, 32), (24, 64)):
print(f"{frames:2d} frames of {side}x{side}: flat MLP {params_flat_mlp(frames, side):9,d} CNN+LSTM {params_cnn_lstm(frames, side):8,d}")
6 frames of 16x16: flat MLP 98,628 CNN+LSTM 18,052
12 frames of 32x32: flat MLP 786,756 CNN+LSTM 42,628
24 frames of 64x64: flat MLP 6,291,780 CNN+LSTM 140,932
Le due funzioni contano i parametri con la formula. Nel MLP il primo livello ha frame × lato² × nascosti pesi: raddoppiare i frame e il lato (da 6×16 a 12×32) moltiplica per otto i pesi, e a 24 frame da 64×64 si arriva a 6,3 milioni di pesi per un solo strato da 64 unità. Nel CNN+LSTM i pesi delle convoluzioni e dell'LSTM non dipendono né dal numero di frame né dalle dimensioni del frame; dipende dalle dimensioni solo il livello lineare che riduce la mappa finale, e cresce con il quadrato del lato diviso 4 (non con i frame). A 24 frame da 64×64 il CNN+LSTM ha 140.932 parametri contro 6.291.780: quarantaquattro volte meno. Inoltre un CNN+LSTM accetta clip di lunghezza qualsiasi senza cambiare un peso, mentre il MLP no.
Alternative alla coppia CNN + LSTM
- Convoluzioni 3D (
nn.Conv3d): il kernel scorre anche nel tempo e coglie piccoli movimenti direttamente. Più parametri per kernel e finestra temporale fissa, ma nessuna ricorrenza. - ConvLSTM (prossima lezione): invece di comprimere ogni frame in un vettore prima dell'LSTM, tiene uno stato che è ancora una mappa spaziale e usa convoluzioni nelle porte. Conserva la geometria nella memoria.
- CNN pre-addestrata come encoder: al posto della piccola CNN si può usare una rete già allenata su milioni di immagini (e magari congelarla): l'LSTM impara solo la parte temporale. Utile con pochi dati.
- Attenzione sul tempo: al posto dell'LSTM, un modello ad attenzione sui vettori dei frame. Fuori dall'ambito di questa serie.
Le quattro alternative spostano il costo in punti diversi. La convoluzione 3D e la ConvLSTM fondono spazio e tempo in un solo operatore, la CNN pre-addestrata sposta il costo sull'addestramento già fatto da altri, l'attenzione cambia del tutto il modo di leggere la sequenza. Per questo la coppia CNN + LSTM resta il punto di partenza didattico: ogni pezzo si può isolare, misurare e sostituire.
Trade-off
- Compressione contro dettaglio. Ridurre ogni frame a un solo vettore è economico ma butta via la geometria fine: se serve sapere esattamente dove, un vettore da 32 numeri può non bastare. Questo è il motivo storico della ConvLSTM.
- Costo. La CNN è applicata a
B × Timmagini: la memoria e il tempo crescono con il numero di frame. Con video reali è spesso il vincolo principale. - Un solo encoder per tutti i frame è efficiente (pesi condivisi), ma assume che ogni frame vada descritto allo stesso modo.
I tre compromessi si leggono insieme: un vettore per frame abbassa il costo ma sacrifica la posizione fine, e il costo della CNN cresce comunque con il numero di frame, perché i pesi sono condivisi ma i calcoli no. Se il tuo problema ha pochi frame e bisogna sapere dove, la compressione è il punto debole; se ha molti frame, lo è il conto di B × T immagini.
Trappole reali
La trappola più insidiosa è il reshape sbagliato: clip.reshape(B*T, C, H, W) è corretto solo se la clip ha forma (B, T, C, H, W). Se per errore ha forma (T, B, ...), il codice gira lo stesso e la rete mescola frame di clip diverse; controlla con un test che prende una clip nota e verifica che i frame ricostruiti siano gli stessi. La seconda riguarda BatchNorm con i frame piegati nel batch: le statistiche vengono calcolate su B × T frame correlati fra loro, non su immagini indipendenti, e questo può danneggiare l'addestramento (la tecnica è descritta nel paper di Ioffe e Szegedy citato nelle fonti).
La terza è scegliere un compito che non richiede il tempo: se la risposta si legge da un frame solo, ogni architettura funziona e non hai testato nulla sull'ibrido, mentre il nostro compito è costruito perché la risposta stia nell'ordine. La quarta è concludere troppo dal 100%: un compito sintetico così semplice è risolto dall'MLP come dall'ibrido, quindi dice che l'ibrido può imparare l'ordine e costa molto meno al crescere del problema, non che sarà migliore su video reali. L'ultima è usare un insieme di test diverso per ogni modello: le differenze di accuratezza possono essere rumore, e il test si fissa con un seed, come qui.
Domande per verificarti (con risposta)
Perché la media sul tempo dei vettori non può distinguere destra da sinistra? La media è invariante rispetto all'ordine dei frame; destra e sinistra producono gli stessi frame in ordine inverso, quindi (a parità di posizioni visitate) la stessa media.
Che forma ha il tensore dopo reshape(B*T, C, H, W) se B = 8, T = 6, C = 1, H = W = 16? (48, 1, 16, 16).
Una convoluzione con kernel 3×3 su un'immagine con 1 canale e 8 filtri in uscita: quanti parametri? 8 · (1·3·3) + 8 = 80, cioè i pesi più un bias per filtro.
Se raddoppi il numero di frame, i parametri del CNN+LSTM cambiano? No. Cambiano il tempo di calcolo e la memoria, non i parametri.
Un esempio svolto a mano: le forme e i parametri del nostro modello
Seguiamo un batch di B = 8 clip da T = 6 frame 16×16 attraverso il CNNLSTM, calcolando le forme e contando i parametri.
ingresso (8, 6, 1, 16, 16)
reshape(B·T, C, H, W) (48, 1, 16, 16) 48 immagini indipendenti
Conv2d(1→8, 3×3, pad 1) (48, 8, 16, 16) parametri 1·8·9 + 8 = 80
MaxPool2d(2) (48, 8, 8, 8)
Conv2d(8→16, 3×3, pad 1) (48, 16, 8, 8) parametri 8·16·9 + 16 = 1.168
MaxPool2d(2) (48, 16, 4, 4)
flatten (48, 256) 16·4·4 = 256
Linear(256→32) (48, 32) parametri 256·32 + 32 = 8.224
reshape(B, T, F) (8, 6, 32)
LSTM(32→32) h_n: (1, 8, 32) parametri 4·32·(32+32+2) = 8.448
Linear(32→4) (8, 4) parametri 32·4 + 4 = 132
La somma è 80 + 1.168 + 8.224 + 8.448 + 132 = 18.052, esattamente il numero stampato dal codice. Questo conto a mano ha due utilità: verificare di aver capito le forme (se il conto non torna, qualcosa nel codice non è come credi) e prevedere il costo: i 48 passaggi per la CNN sono il vero costo di calcolo, non l'LSTM.
Una regola per le dimensioni spaziali: con kernel 3×3, padding = 1 e stride 1 la dimensione non cambia; con un MaxPool2d(2) si dimezza (con arrotondamento per difetto). In generale, per una convoluzione l'uscita è ⌊(N + 2·pad − kernel) / stride⌋ + 1: con N = 16, pad = 0, kernel = 3 si ottiene 14.
Domande frequenti
Conv3d o CNN + LSTM? Conv3d è più semplice se i movimenti sono brevi e la lunghezza è fissa; CNN + LSTM gestisce lunghezze variabili e dipendenze lunghe nel tempo. Non le abbiamo confrontate con una misura, quindi non possiamo dire quale vinca in generale.
Posso usare una CNN pre-addestrata? Sì, ed è comune: si sostituisce FrameEncoder con una rete già addestrata su molte immagini (congelandola o regolandola con un learning rate piccolo), e si addestra solo la parte temporale. Serve soprattutto quando hai pochi video.
Che frame rate scelgo? Quello che rende visibile il movimento che ti interessa: se il fenomeno cambia in 0.5 s, campionare a 1 Hz lo salta. Frame troppo ravvicinati, d'altra parte, danno molta ridondanza e poco segnale.
E se il video ha lunghezze diverse? L'LSTM le accetta; vale il discorso del padding (pack/unpack) della lezione 60. Se invece le campioni a lunghezza fissa (ad esempio 16 frame per clip), serve decidere come: uniforme sull'intera durata o una finestra.
Perché l'ultimo stato nascosto e non la media degli stati? L'ultimo stato ha letto tutta la clip; la media ripeterebbe l'errore della CNNPool, perdendo l'ordine (anche se qui sarebbe una media di stati che hanno già codificato l'ordine, quindi meno grave). È una scelta di progetto e si può provare.
Il risultato del 100% è troppo bello? Il compito è volutamente semplice (un solo quadrato, nessuno sfondo, direzioni nette), ed è per questo che un MLP lo risolve. Il punto non è la difficoltà ma il conto dei parametri e la dimostrazione dell'ordine; su video reali ti aspetti prestazioni molto minori, che vanno misurate.
Esercizi con soluzione
Prima di andare avanti, prova a risolvere questi esercizi con carta e matita; le soluzioni (verificate con il codice) sono subito sotto ciascuno.
Esercizio 1. Un'immagine 28×28, convoluzione 5×5 senza padding con stride 1: dimensione dell'uscita? E con stride 2? Soluzione: 28 − 5 + 1 = 24; con stride 2: ⌊(28 − 5)/2⌋ + 1 = 12.
Esercizio 2. Parametri di nn.Conv2d(3, 16, 3) (con bias)? Soluzione: 3·16·9 + 16 = 448.
Esercizio 3. Un MLP piatto su 10 frame 28×28 con 128 unità nascoste, solo il primo strato (con bias): quanti parametri? Soluzione: 10·784·128 + 128 = 1.003.648, più di un milione per un solo strato.
Esercizio 4. Perché la media sul tempo non può distinguere destra da sinistra? Soluzione: perché i frame di una clip verso destra e di una verso sinistra sono gli stessi in ordine inverso, e la media non dipende dall'ordine.
Cosa portarti via
Un'architettura ibrida divide il lavoro: la CNN risponde a «cosa e dove in questo frame», l'LSTM a «come cambia nel tempo». Il trucco tecnico è piegare il tempo dentro il batch per applicare la stessa CNN a tutti i frame e poi rimetterlo a posto. L'esperimento ha mostrato tre cose misurate: la media nel tempo non può superare circa 0.5 perché perde l'ordine (ottenuto circa 0.37), il CNN+LSTM risolve il compito (1.000) con 18 mila parametri, e il MLP piatto lo risolve ma con cinque volte i parametri, un fattore che sale a circa quarantaquattro a 24 frame da 64×64. Nella prossima lezione una variante più elegante: portare le convoluzioni dentro la cella ricorrente.
Nella serie, la lezione precedente è Generare testo carattere per carattere con una LSTM; la successiva è La cella ConvLSTM: memoria che conserva la geometria.
Fonti
- PyTorch: documentazione di torch.nn.Conv2d - forma dell'uscita e conteggio dei parametri (pesi più un bias per filtro), consultata il 2026-11-12
- PyTorch: documentazione di torch.nn.LSTM - forma di
h_ne conteggio dei parametri dell'LSTM, consultata il 2026-11-12 - J. Donahue et al., Long-term Recurrent Convolutional Networks for Visual Recognition and Description (arXiv:1411.4389) - l'architettura CNN più LSTM per sequenze di immagini, scheda raggiunta il 2026-11-12
- S. Ioffe, C. Szegedy, Batch Normalization (arXiv:1502.03167) - la normalizzazione di batch citata nelle trappole, scheda raggiunta il 2026-11-12
- X. Shi et al., Convolutional LSTM Network (arXiv:1506.04214) - la ConvLSTM della lezione successiva, scheda raggiunta il 2026-11-12
Commenti
Caricamento commenti...
Accedi per commentare