Nella lezione precedente abbiamo messo una CNN davanti a una LSTM: la CNN riduce ogni frame a un vettore, la LSTM legge i vettori in ordine. Funziona, ma ha un costo nascosto: nel momento in cui il frame diventa un vettore, la geometria (dove sta cosa) è schiacciata in una lista di numeri senza forma. La ConvLSTM, proposta da Shi e colleghi nel 2015 per prevedere le immagini radar della pioggia nelle ore successive, fa un'altra scelta: tiene la memoria come una mappa, con la stessa forma dell'immagine, e sostituisce ogni prodotto fra matrici dentro la cella con una convoluzione. Le porte, la memoria e l'uscita sono le stesse della LSTM; cambia solo che ogni numero è diventato una piccola immagine. In questa lezione scrivi le equazioni, implementi la cella da zero, la verifichi pixel per pixel, misuri quanto risparmia in parametri e scopri due proprietà che la rendono adatta ai video: l'equivarianza alla traslazione e un limite di velocità che è facile violare senza accorgersene.
Le parole di questa lezione
Questa lezione riusa le definizioni di cella di memoria, porta, sigmoide e prodotto elemento per elemento dalla lezione sulla LSTM, e quelle di convoluzione, kernel e mappa delle caratteristiche dalla lezione precedente. Qui aggiungiamo cinque termini.
Il padding «same» aggiunge zeri attorno all'immagine prima della convoluzione, in modo che l'uscita abbia le stesse dimensioni dell'ingresso: con un kernel 3×3 si aggiunge un pixel di zeri per lato. Il campo recettivo è l'insieme dei pixel di ingresso che possono influenzare un certo pixel di uscita; con un solo kernel 3×3 è una finestra 3×3, e impilando strati o ripetendo nel tempo cresce. L'equivarianza alla traslazione dice che, se sposti l'ingresso di k pixel, l'uscita si sposta di k pixel con lo stesso contenuto: una rete che la possiede riconosce lo stesso motivo ovunque si trovi.
Le connessioni a spioncino (peephole) sono una variante storica della LSTM in cui le porte guardano anche lo stato di cella; la ConvLSTM originale le contiene, la maggior parte delle implementazioni moderne le omette, e noi facciamo lo stesso. Infine la previsione di fenomeni spazio-temporali è il compito per cui la cella è nata: dato un film di mappe (radar, traffico, densità di folla), prevederne le mappe successive.
L'analogia: da un solo meteorologo a una squadra di osservatori locali
Con la CNN + LSTM, un solo meteorologo guarda tutta la mappa radar, scrive un riassunto di tre righe e lo passa al collega del giorno dopo, che ne scrive un altro. La mappa si è persa: nel riassunto «una perturbazione a nord-est» non si può più ritrovare l'esatta posizione dei bordi della pioggia. Con la ConvLSTM l'organizzazione è diversa: una griglia di osservatori, uno per ogni punto della mappa. Ognuno tiene un quaderno personale (la memoria locale) e a ogni istante aggiorna il quaderno guardando solo tre cose: cosa vede nel proprio punto adesso, cosa c'era scritto nel suo quaderno, e cosa c'è scritto nei quaderni dei vicini immediati. Tutti gli osservatori usano le stesse regole di aggiornamento (i pesi del kernel, condivisi su tutta la mappa). Una perturbazione che si sposta viene «passata di mano» da un osservatore al vicino: la memoria si sposta insieme al fenomeno.
Il problema: la LSTM classica su immagini
La LSTM a connessione piena (la «FC-LSTM», dove FC sta per fully connected) richiederebbe di appiattire ogni frame in un vettore di H × W numeri e avere uno stato con altrettante componenti, collegate fra loro tutte con tutte. Contiamo con una mappa 16×16 e uno stato di memoria equivalente a 8 canali (cioè 8 × 16 × 16 = 2048 numeri).
# Parameters: a fully connected LSTM that has the SAME "memory size" as the ConvLSTM
H = W = 16
hidden_ch = 8
n_state = hidden_ch * H * W # 2048 numbers of state, as in the ConvLSTM
n_input = 1 * H * W # 256 pixels per frame
fc_lstm = 4 * n_state * (n_state + n_input + 2)
conv_lstm = 4 * hidden_ch * (1 + hidden_ch) * 3 * 3 + 4 * hidden_ch
print("FC-LSTM :", f"{fc_lstm:,}") # 18,890,752
print("ConvLSTM :", f"{conv_lstm:,}") # 2,624
print("ratio :", round(fc_lstm / conv_lstm))
FC-LSTM : 18,890,752
ConvLSTM : 2,624
ratio : 7199
Spiegazione. Per la FC-LSTM, ognuna delle quattro trasformazioni ha una matrice di stato × (stato + ingresso) pesi: 4 · 2048 · (2048 + 256 + 2) = 18.890.752. Per la ConvLSTM, ognuna delle quattro trasformazioni è un kernel 3×3 che collega 1 + 8 canali di ingresso a 8 canali di uscita: 4 · 8 · (1 + 8) · 9 pesi più 4 · 8 bias = 2.624. Il rapporto è 7.199 volte. Il risparmio non viene da un trucco di compressione ma da due ipotesi sulla struttura dei dati: (1) un pixel è influenzato soprattutto dai vicini (località), (2) la regola che lega un pixel ai suoi vicini è la stessa in tutta l'immagine (condivisione dei pesi). Se le ipotesi sono giuste per il tuo problema, l'architettura le sfrutta; se sono sbagliate (ad esempio quando gli oggetti lontani si influenzano a vicenda in modo essenziale), no.
La matematica: dalla LSTM alla ConvLSTM
Ricorda le equazioni della LSTM, con W · [h, x] che indica il prodotto di una matrice con il vettore concatenato:
f = σ( W_f · [h_{t-1}, x_t] + b_f ) i = σ( W_i · [h_{t-1}, x_t] + b_i )
g = tanh( W_g · [h_{t-1}, x_t] + b_g ) o = σ( W_o · [h_{t-1}, x_t] + b_o )
c_t = f ⊙ c_{t-1} + i ⊙ g h_t = o ⊙ tanh(c_t)
La ConvLSTM è esattamente la stessa struttura con tre cambiamenti. Primo: x_t, h_t e c_t non sono più vettori ma tensori (canali, altezza, larghezza). Secondo: ogni prodotto matrice-vettore W · v diventa una convoluzione W ∗ V con un kernel. Terzo: ⊙ resta il prodotto pixel per pixel. In forma compatta, senza connessioni a spioncino:
f_t = σ( W_xf ∗ X_t + W_hf ∗ H_{t-1} + b_f )
i_t = σ( W_xi ∗ X_t + W_hi ∗ H_{t-1} + b_i )
g_t = tanh( W_xg ∗ X_t + W_hg ∗ H_{t-1} + b_g )
o_t = σ( W_xo ∗ X_t + W_ho ∗ H_{t-1} + b_o )
C_t = f_t ⊙ C_{t-1} + i_t ⊙ g_t
H_t = o_t ⊙ tanh( C_t )
Il simbolo ∗ è la convoluzione. Nell'articolo originale compaiono anche termini W_ci ⊙ C_{t-1} (le connessioni a spioncino); le omettiamo perché le implementazioni più diffuse non le hanno e i risultati sono simili. Due convoluzioni sommate (una sull'ingresso, una sullo stato precedente) equivalgono a una convoluzione sulla concatenazione dei due lungo l'asse dei canali: è il trucco usato nel codice per calcolare le quattro porte con una sola chiamata.
Come leggere il disegno, confrontandolo con quello della LSTM. La linea verde in alto è sempre la memoria, ma ora ogni «casella» è un'intera mappa per canale. Il prodotto con il forget gate e la somma con il nuovo contenuto avvengono pixel per pixel: in ogni punto della mappa, la porta decide quanto tenere e quanto scrivere. La riga in basso rappresenta ora la concatenazione di X(t) e H(t−1) sui canali, su cui una convoluzione 3×3 produce, per ogni pixel, i valori delle quattro porte guardando la finestra di nove pixel attorno.
Codice: la cella da zero
Scriviamo la cella in PyTorch. Il cuore è un'unica nn.Conv2d che prende i canali di ingresso più quelli dello stato e produce quattro volte i canali dello stato (uno per porta).
import torch
import torch.nn as nn
class ConvLSTMCell(nn.Module):
"""LSTM cell whose gates are computed with convolutions. State = maps (B, hidden, H, W)."""
def __init__(self, in_ch, hidden_ch, kernel=3):
super().__init__()
self.hidden_ch = hidden_ch
# ONE convolution for the four gates, applied to [x, h] stacked on the channel axis
self.conv = nn.Conv2d(in_ch + hidden_ch, 4 * hidden_ch, kernel, padding=kernel // 2)
with torch.no_grad():
self.conv.bias[hidden_ch:2 * hidden_ch] = 1.0 # forget-gate bias = 1, as in the LSTM lesson
def forward(self, x, state):
h, c = state # each (B, hidden, H, W)
z = self.conv(torch.cat([x, h], dim=1)) # (B, 4*hidden, H, W)
i, f, g, o = torch.chunk(z, 4, dim=1)
i, f, o, g = torch.sigmoid(i), torch.sigmoid(f), torch.sigmoid(o), torch.tanh(g)
c = f * c + i * g # the additive memory update, per pixel
h = o * torch.tanh(c)
return h, c
class ConvLSTM(nn.Module):
def __init__(self, in_ch, hidden_ch, kernel=3):
super().__init__()
self.cell = ConvLSTMCell(in_ch, hidden_ch, kernel)
self.hidden_ch = hidden_ch
def forward(self, seq): # seq: (B, T, C, H, W)
B, T, _, H, W = seq.shape
h = seq.new_zeros(B, self.hidden_ch, H, W)
c = seq.new_zeros(B, self.hidden_ch, H, W)
outs = []
for t in range(T):
h, c = self.cell(seq[:, t], (h, c))
outs.append(h)
return torch.stack(outs, dim=1), (h, c) # (B, T, hidden, H, W)
torch.manual_seed(0) # fixed seed: same weights, same numbers on every run
layer = ConvLSTM(in_ch=1, hidden_ch=8)
x = torch.randn(2, 5, 1, 16, 16)
out, (h, c) = layer(x)
print(out.shape, h.shape, c.shape) # (2, 5, 8, 16, 16) (2, 8, 16, 16) (2, 8, 16, 16)
print(sum(p.numel() for p in layer.parameters())) # 4 * 8 * (1 + 8) * 3 * 3 + 4 * 8 = 2624
Spiegazione riga per riga. Nel costruttore, nn.Conv2d(in_ch + hidden_ch, 4 * hidden_ch, kernel, padding=kernel // 2) è la convoluzione unica: input = canali di x più canali di h concatenati, output = quattro blocchi di hidden_ch canali. Il padding di kernel // 2 (1 per un kernel 3×3) mantiene la dimensione spaziale. Il bias del forget gate è messo a 1, come nella lezione sulla LSTM, per partire con una cella che tende a ricordare. Nel forward, torch.cat([x, h], dim=1) concatena sull'asse dei canali; torch.chunk(z, 4, dim=1) divide il risultato nei quattro blocchi; poi seguono sigmoide e tanh e le due righe dell'aggiornamento, identiche a quelle della LSTM ma su tensori. La classe ConvLSTM ripete la cella per T frame, parte da stati nulli con la forma giusta e impila le uscite in (B, T, hidden, H, W). L'ultima riga conta i parametri: 4·8·(1+8)·3·3 + 4·8 = 2624, esattamente il valore del conto manuale.
torch.Size([2, 5, 8, 16, 16]) torch.Size([2, 8, 16, 16]) torch.Size([2, 8, 16, 16])
2624
Verifica pixel per pixel: la convoluzione è davvero una somma di 81 prodotti
Per essere sicuri di aver capito cosa fa quella singola riga self.conv(...), ricalcoliamo a mano un solo valore: il valore pre-attivazione della porta di input (canale 0) nel pixel in riga 5, colonna 7, con la formula scritta come somma esplicita. Per ogni pixel, la porta guarda 9 canali (1 di x e 8 di h) e per ognuno la finestra 3×3 attorno al pixel: 81 prodotti, più il bias.
import numpy as np
# Check ONE gate value at ONE pixel against the formula written out with explicit sums.
torch.manual_seed(1)
cell = layer.cell
x1 = torch.randn(1, 1, 16, 16)
h0 = torch.randn(1, 8, 16, 16)
z = cell.conv(torch.cat([x1, h0], dim=1)) # (1, 32, 16, 16): [i | f | g | o] pre-activations
gate_i_lib = z[0, 0, 5, 7].item() # input-gate unit 0 at pixel (5, 7), before the sigmoid
W = cell.conv.weight.detach().numpy() # (32, 1+8, 3, 3): output ch, input ch, ky, kx
b = cell.conv.bias.detach().numpy()
inp = torch.cat([x1, h0], dim=1)[0].numpy() # (9, 16, 16)
total = b[0]
for ch in range(9): # sum over input channels (1 from x, 8 from h)
for ky in range(3):
for kx in range(3):
total += W[0, ch, ky, kx] * inp[ch, 5 + ky - 1, 7 + kx - 1] # the 3x3 neighbourhood of (5, 7)
print(gate_i_lib, total, abs(gate_i_lib - total)) # identical up to float precision
-0.7144114375114441 -0.7144115 5.9604645e-08
Il primo numero è ciò che calcola la libreria, il secondo la nostra somma esplicita su 9 canali × 3 × 3 pesi, il terzo la differenza: 6·10⁻⁸, cioè precisione numerica a 32 bit. Ora la convoluzione non è più una scatola nera: ogni porta di ogni pixel è una somma pesata dei 81 numeri vicini. L'indice 5 + ky − 1 con ky da 0 a 2 percorre le righe 4, 5, 6 attorno alla riga 5; lo stesso per le colonne.
Due proprietà che si possono misurare
Il campo recettivo cresce di un raggio per passo
Un pixel di uscita al tempo t dipende dal proprio vicinato 3×3 al tempo t e dallo stato precedente, che a sua volta dipendeva dal vicinato del passo prima. Quindi, andando indietro nel tempo, il vicinato si allarga di un pixel per lato a ogni passo. Misuriamolo: calcoliamo il gradiente di un pixel dell'uscita finale (il centro) rispetto a tutti i pixel dei frame di ingresso; dove il gradiente non è zero, quel pixel di ingresso può influenzarlo.
# How far does one input pixel spread, and what happens if the picture moves?
torch.manual_seed(0)
layer = ConvLSTM(1, 4)
# (1) Receptive field: which pixels of the FIRST frame influence ONE output pixel (the centre) at the end?
T = 4
x = torch.zeros(1, T, 1, 21, 21, requires_grad=True)
out, _ = layer(x)
out[0, -1, :, 10, 10].sum().backward() # the centre pixel of the last output map
reach = (x.grad[0, 0, 0] != 0) # frame 0: which input pixels have non-zero gradient?
print("frame-0 pixels that influence the centre (row 10):", int(reach[10].sum()), "wide")
print("same for the LAST frame (t=T-1):", int((x.grad[0, T - 1, 0] != 0)[10].sum()), "wide")
# (2) Translation equivariance: shift the input by 2 pixels, the output shifts by 2 pixels (away from borders)
img = torch.zeros(1, 3, 1, 21, 21)
img[:, :, :, 8:11, 8:11] = torch.rand(1, 3, 1, 3, 3) # a small blob far from the borders
shifted = torch.roll(img, shifts=2, dims=4) # move right by 2
o1, _ = layer(img); o2, _ = layer(shifted)
a = torch.roll(o1, shifts=2, dims=4)[..., 6:-6, 6:-6] # compare only the interior: borders break the symmetry
b = o2[..., 6:-6, 6:-6]
print("interior: max |shift(output) - output(shift)| =", f"{(a - b).abs().max().item():.1e}")
frame-0 pixels that influence the centre (row 10): 9 wide
same for the LAST frame (t=T-1): 3 wide
interior: max |shift(output) - output(shift)| = 0.0e+00
Con T = 4 frame, i pixel del primo frame che influenzano il centro dell'ultima uscita formano una finestra larga 9 (raggio 4), mentre per l'ultimo frame la finestra è larga 3 (raggio 1): proprio il raggio 1 per ognuno dei passi indietro. Nel codice, out[0, -1, :, 10, 10].sum().backward() sceglie un solo pixel d'uscita; x.grad[0, t, 0] != 0 è la maschera dei pixel del frame t con gradiente non nullo; contiamo quanti sono nella riga centrale.
La conseguenza pratica è un limite di velocità. Se nel tuo video un oggetto si sposta di più pixel per frame di quanti il kernel ne copra (raggio 1 per un 3×3), la memoria non riesce a «tenere il passo» con l'oggetto: l'informazione si propaga al massimo di un raggio per passo. Rimedi: kernel più grandi (5×5, 7×7), più strati impilati, o ridurre la risoluzione spaziale (pooling, convoluzioni con stride) in modo che lo stesso movimento corrisponda a meno pixel. Nei dati reali questo si traduce in una regola di progetto: stima la velocità massima in pixel per frame e dimensiona il kernel di conseguenza.
Equivarianza alla traslazione
Nella seconda parte dell'esperimento prendiamo una piccola macchia, la spostiamo di 2 pixel a destra e confrontiamo: spostare l'uscita deve dare lo stesso risultato di calcolare l'uscita dell'ingresso spostato. Si confronta solo l'interno ([6:-6]), perché ai bordi il padding con zeri rompe la simmetria. Differenza massima: 0.0. La cella riconosce lo stesso fenomeno ovunque si trovi, perché usa gli stessi pesi in ogni posizione: una FC-LSTM dovrebbe imparare il fenomeno separatamente per ogni posizione.
Dal blocco al modello: come si usa in pratica
- Pila di strati. Si impilano più ConvLSTM: l'uscita
Hdel primo strato è la sequenza di ingresso del secondo. Più strati aumentano il campo recettivo e l'astrazione. - Encoder-decoder per la previsione. Uno schema classico: un encoder legge i frame passati e ne produce lo stato finale; un decoder parte da quello stato e produce i frame futuri. Nell'articolo originale per le previsioni radar si usa proprio questa struttura.
- Testa finale. Dopo la ConvLSTM, una convoluzione 1×1 riduce i canali di
Ha quelli dell'immagine da prevedere (ad esempio 1), con una sigmoide se i pixel sono in [0, 1]. - Varianti. La ConvGRU applica la stessa idea alla GRU; i kernel possono essere diversi per ingresso e stato; si può aggiungere normalizzazione per strato.
I quattro punti compongono un modello completo nell'ordine in cui li usi: la pila costruisce l'astrazione, lo schema encoder-decoder decide se leggi o anche generi, la testa riporta i canali a quelli dell'immagine, le varianti sono rifiniture da provare solo dopo una versione base che funziona. Qui non abbiamo addestrato nessun modello completo: lo facciamo nella lezione successiva, con una sola ConvLSTM.
Trade-off
- Memoria di calcolo. Gli stati sono mappe. Una stima (non misurata): con batch 16, 20 frame da 64×64 e 64 canali, un solo tensore delle porte ha
16·20·64·64·64 ≈ 84 milionidi numeri; durante l'addestramento ne vanno conservati diversi per strato (le quattro porte,c,he intermedie), circa 7, cioè dell'ordine di 2.3 gigabyte per un solo strato in virgola mobile a 32 bit. Il vincolo reale delle ConvLSTM è spesso la memoria della GPU, non il numero di parametri. - Sequenzialità. Come ogni RNN, la lettura nel tempo non si parallelizza.
- Campo recettivo limitato. Capacità di legare pixel lontani solo tramite molti passi o molti strati; per interazioni a lungo raggio un'attenzione globale può servire di più.
Le tre voci sono costi diversi della stessa scelta: la memoria cresce con canali × altezza × larghezza × passi, la sequenzialità impedisce di nascondere quel costo con il parallelismo, e il campo recettivo limita ciò che la memoria può collegare. Se il tuo problema ha immagini piccole e movimenti lenti il conto è favorevole; se ha immagini grandi e interazioni a distanza, conviene misurare prima di scegliere.
Trappole reali
La prima trappola è violare il limite di velocità senza accorgersene: il modello addestra bene su movimenti lenti e fallisce su quelli veloci, e il campione di validazione sembra buono se contiene soprattutto movimenti lenti, quindi misura la distribuzione delle velocità. La seconda riguarda i bordi: il padding con zeri crea un «bordo» che il modello può imparare e usare, e quando lo spostamento porta un oggetto vicino al bordo il comportamento cambia. La terza è dimenticare il bias del forget gate: come per la LSTM, un valore iniziale 0 rende la memoria labile all'inizio.
La quarta è sbagliare le forme degli stati iniziali, che devono essere (B, hidden, H, W) e non un semplice vettore, un errore tipico quando si adatta il codice della LSTM. La quinta è l'ordine dei canali concatenati: la convoluzione sulla concatenazione dipende dall'ordine [x, h], e se lo scambi tra addestramento e inferenza i pesi non corrispondono più. L'ultima è pensare che la ConvLSTM «veda» il movimento senza essere addestrata a farlo: impara ciò che il compito richiede, e un modello addestrato a minimizzare l'errore pixel per pixel su immagini lente impara spesso a ricopiare l'ultimo frame, un effetto che vedremo nella prossima lezione.
Domande per verificarti (con risposta)
Una ConvLSTM ha 3 canali di ingresso, 16 canali di stato e kernel 5×5. Quanti parametri (un solo bias per porta e canale)? 4 · 16 · (3 + 16) · 25 + 4 · 16 = 30400 + 64 = 30464.
Dopo 6 passi con kernel 3×3, quanto è largo il campo recettivo (rispetto al primo frame) sul pixel finale? Raggio 6, cioè una finestra larga 13, coerente con la misura (9 per T = 4, cioè 2T + 1).
Un oggetto si muove di 4 pixel per frame. Un kernel 3×3 basta? No: il raggio è 1 pixel per passo. Serve un kernel con raggio almeno 4 (9×9), oppure ridurre la risoluzione di un fattore 4 prima.
Perché due convoluzioni sommate si possono scrivere come una sola sulla concatenazione? Perché una convoluzione su canali concatenati è la somma delle convoluzioni sui singoli gruppi di canali, ognuno con i suoi pesi.
Un esempio svolto a mano: il limite di velocità, in una dimensione
Per vedere «la memoria che si sposta insieme al fenomeno» e il suo limite, usiamo una versione a una dimensione. Lo stato è una riga di 8 caselle, con un oggetto (valore 1) nella casella 1: [0, 1, 0, 0, 0, 0, 0, 0]. Il «kernel» che aggiorna lo stato guarda soltanto la casella a sinistra: nuovo[i] = vecchio[i − 1] (è il kernel [1, 0, 0] di una convoluzione di raggio 1). Applicandolo a ogni passo, l'oggetto scorre verso destra di una casella per passo: dopo 1 passo è in 2, dopo 2 in 3, dopo 3 in 4.
Ora l'oggetto reale si muove di due caselle per passo: dopo 3 passi è in 7. La memoria, con un kernel di raggio 1, può seguirlo di una sola casella per passo e dopo 3 passi è in 4: il ritardo (lag) cresce di una casella a ogni passo, 3 caselle dopo tre passi. Con un kernel di raggio 2 ([0, 0, 0, 0, 1] su 5 caselle: guarda due caselle a sinistra) la memoria arriverebbe esattamente a 7. Questo è il limite di velocità: la memoria può viaggiare al massimo di un raggio del kernel per passo. Per un oggetto che si muove di v pixel per frame serve un raggio ≥ v, oppure uno stato a risoluzione più bassa in cui lo stesso movimento corrisponde a meno pixel, oppure più strati.
Questo è l'esempio più semplice possibile di ciò che nella lezione abbiamo misurato con i gradienti: con T = 4 passi e kernel 3×3, il pixel centrale dell'ultima uscita dipendeva da una finestra larga 9 del primo frame (raggio 4 = passi × raggio del kernel).
Domande frequenti
Una ConvLSTM può imparare un'operazione «globale» (ad esempio contare quanti oggetti ci sono)? Con kernel piccoli e pochi passi ogni pixel vede solo un vicinato; l'informazione globale deve essere portata da molti passi o strati, oppure con un'operazione di aggregazione finale (una media su tutti i pixel). Non è il suo punto forte.
Quanti canali di stato servono? Dipende dalla varietà dei fenomeni: 8-64 sono tipici. Più canali = più capacità e più memoria (cresce linearmente per le attivazioni, e quadraticamente per i pesi del kernel).
Posso impilare strati con kernel diversi? Sì, e si fa: ad esempio un primo strato con kernel grande per catturare il movimento rapido e uno successivo con kernel piccolo per i dettagli.
È meglio una ConvLSTM o una CNN + LSTM? Se i dettagli spaziali contano nella memoria (previsione di immagini, mappe), la ConvLSTM; se serve solo una decisione sul video intero (classificazione), la CNN + LSTM è più economica e spesso basta. Non l'abbiamo misurato sul medesimo compito.
Il bias del forget gate vale anche qui? Sì: la stessa ragione (partire dalla memoria che si conserva), con lo stesso effetto sul flusso del gradiente. Nella cella implementata l'abbiamo posto a 1.
Come inizializzo gli stati iniziali? A zero con la forma (B, canali, H, W); in alternativa si possono imparare come parametri o ottenere da un encoder dei frame di contesto.
Cosa portarti via
La ConvLSTM è una LSTM in cui ogni numero è diventato una mappa e ogni prodotto matrice-vettore una convoluzione. Con questa sostituzione la memoria conserva la geometria, i parametri crollano (2.624 contro 18.890.752 nel nostro esempio, un fattore 7.199) e la cella acquisisce l'equivarianza alla traslazione (verificata: differenza 0.0 nell'interno). Il prezzo è un campo recettivo che cresce di un raggio per passo, quindi un limite di velocità da rispettare, e una memoria di calcolo proporzionale a canali × altezza × larghezza. Nella prossima lezione la mettiamo al lavoro su un compito vero: prevedere il frame successivo di cifre MNIST in movimento.
Nella serie, la lezione precedente è Architetture ibride: una CNN per lo spazio e una LSTM per il tempo; la successiva è ConvLSTM in pratica: prevedere il frame successivo di cifre in movimento.
Fonti
- X. Shi et al., Convolutional LSTM Network: A Machine Learning Approach for Precipitation Nowcasting (arXiv:1506.04214) - l'origine della ConvLSTM e l'applicazione alle previsioni radar della pioggia, scheda raggiunta il 2026-11-12
- PyTorch: documentazione di torch.nn.Conv2d - pesi
(out, in, kH, kW), padding e conteggio dei parametri usati nella verifica pixel per pixel, consultata il 2026-11-12 - PyTorch: documentazione di torch.nn.LSTM - ordine delle porte
[i | f | g | o]e conto dei parametri della LSTM a connessione piena, consultata il 2026-11-12
Commenti
Caricamento commenti...
Accedi per commentare