RNN vanilla: stato nascosto e srotolamento della sequenza
Imparerete a implementare manualmente una cella RNN a un passaggio, srotolarla su una sequenza breve e visualizzare come lo stato nascosto accumuli il contesto.
RNN vanilla: stato nascosto e srotolamento della sequenza è una lezione Machine Learning Academy gratuita su CoddyKit. Questa è la lezione 1 di 4. Puoi leggere la lezione completa qui gratuitamente — poi esercitati direttamente nel browser con un editor di codice integrato e un tutor IA disponibile 24/7. Fa parte del percorso di apprendimento Machine Learning Academy, e i tuoi progressi si sincronizzano tra il web e l'app CoddyKit. Il corso Machine Learning Academy include 4 lezioni in totale.
Perché servono le reti ricorrenti
Le reti standard feedforward trattano ogni input in modo indipendente: non conservano memoria degli input precedenti. Tuttavia, molti problemi del mondo reale coinvolgono dati sequenziali, nei quali il contesto passato è importante: prevedere la parola successiva di una frase, stimare il prezzo azionario di domani a partire dai dati storici o classificare un gesto da una sequenza di fotogrammi video. Le Recurrent Neural Networks (RNNs) risolvono questo problema mantenendo uno stato nascosto che trasporta le informazioni attraverso i diversi istanti temporali.
# Examples of sequential data:
sequences = {
'NLP': 'The cat sat on the ___ (predict next word)',
'Time Series': '[1.2, 1.5, 1.3, 1.8, ???]',
'Speech': '[audio_t0, audio_t1, ..., audio_tN]',
'Video': '[frame_1, frame_2, ..., frame_T]',
'DNA': 'ATCGATCG... (biological sequence)',
}
for name, example in sequences.items():
print(f'{name}: {example}')La cella RNN vanilla
Una cella RNN vanilla riceve due input: l'input corrente x_t e lo stato nascosto precedente h_{t-1}. Produce lo stato nascosto successivo h_t usando la formula: h_t = tanh(W_hh * h_{t-1} + W_xh * x_t + b). Le stesse matrici dei pesi W_hh e W_xh vengono usate a ogni istante temporale: questa è la condivisione dei pesi nel tempo, analoga alla condivisione dei pesi nello spazio tipica delle CNN. Lo stato nascosto conserva la memoria della rete relativa a tutti gli input passati.
import torch
def rnn_cell(x_t, h_prev, W_xh, W_hh, b):
'''One step of a vanilla RNN cell'''
# x_t: (batch, input_size)
# h_prev: (batch, hidden_size)
h_t = torch.tanh(
x_t @ W_xh.T + # input contribution
h_prev @ W_hh.T + # hidden-to-hidden contribution
b # bias
)
return h_t
# Example: input_size=4, hidden_size=8
batch = 3
x_t = torch.randn(batch, 4)
h_prev = torch.zeros(batch, 8)
W_xh = torch.randn(8, 4) * 0.01
W_hh = torch.randn(8, 8) * 0.01
b = torch.zeros(8)
h_t = rnn_cell(x_t, h_prev, W_xh, W_hh, b)
print(h_t.shape) # (3, 8)Srotolare la RNN nel tempo
Per elaborare una sequenza di lunghezza T, la cella RNN viene applicata T volte in un ciclo: questa operazione è chiamata srotolamento (o unfolding) nel tempo. Lo stato nascosto del passo t-1 viene passato al passo t, collegando tutti gli istanti temporali. Durante la retropropagazione, i gradienti devono scorrere a ritroso attraverso ogni istante temporale: questa procedura è chiamata Backpropagation Through Time (BPTT). La profondità del grafo srotolato corrisponde alla lunghezza della sequenza, creando difficoltà per le sequenze lunghe.
import torch
import torch.nn as nn
# Unroll RNN manually over a sequence
batch_size, seq_len, input_size, hidden_size = 4, 10, 8, 16
rnn_cell = nn.RNNCell(input_size, hidden_size)
sequence = torch.randn(seq_len, batch_size, input_size)
h = torch.zeros(batch_size, hidden_size) # initial hidden state
hidden_states = []
for t in range(seq_len):
x_t = sequence[t] # (batch, input_size)
h = rnn_cell(x_t, h) # apply RNN cell
hidden_states.append(h)
print('Final hidden state:', h.shape) # (4, 16)
print('All hidden states:', len(hidden_states), 'steps')Usare nn.RNN: la versione come modulo
nn.RNN gestisce automaticamente lo srotolamento. Riceve input con forma (seq_len, batch, input_size) oppure (batch, seq_len, input_size) con batch_first=True e restituisce tutti gli stati nascosti e lo stato nascosto finale. Parametri principali: num_layers impila più livelli RNN; bidirectional=True elabora la sequenza in entrambe le direzioni; dropout applica il dropout tra i livelli nelle RNN multilivello.
import torch
import torch.nn as nn
rnn = nn.RNN(
input_size=16,
hidden_size=32,
num_layers=2,
batch_first=True, # input: (batch, seq, features)
dropout=0.2 # between layers
)
X = torch.randn(8, 20, 16) # batch=8, seq=20, features=16
output, h_n = rnn(X)
print('Output shape:', output.shape) # (8, 20, 32) all steps
print('h_n shape:', h_n.shape) # (2, 8, 32) last hiddenInizializzazione dello stato nascosto
Lo stato nascosto iniziale h_0 viene passato come secondo argomento a nn.RNN. Se non viene fornito, viene impostato per impostazione predefinita a zero. Nella classificazione di sequenze, lo stato nascosto finale h_n riassume l'intera sequenza. Nei task sequence-to-sequence vengono usati tutti gli stati nascosti intermedi contenuti in output. Inizializzare h_0 a zero è lo standard; alcune applicazioni apprendono lo stato iniziale come parametro per migliorare le prestazioni sulle sequenze brevi.
import torch
import torch.nn as nn
rnn = nn.RNN(8, 16, batch_first=True)
X = torch.randn(4, 10, 8) # batch=4, seq=10, input=8
# Default: h_0 = zeros
output, h_n = rnn(X)
print('h_n with zero init:', h_n.shape) # (1, 4, 16)
# Custom initial hidden state
h_0 = torch.randn(1, 4, 16) # (num_layers, batch, hidden)
output, h_n = rnn(X, h_0)
print('h_n with custom init:', h_n.shape) # (1, 4, 16)Classificazione di sequenze con lo stato nascosto finale
Un'applicazione comune delle RNN è la classificazione di sequenze: data una sequenza, si predice un'unica etichetta di classe. L'approccio standard consiste nell'usare solo lo stato nascosto finale h_n come input per un classificatore lineare, poiché ha ricevuto l'intera sequenza fino a quel punto. In una RNN bidirezionale, si concatenano gli stati nascosti finali della direzione in avanti e di quella all'indietro per catturare sia il contesto passato sia quello futuro.
import torch
import torch.nn as nn
class SequenceClassifier(nn.Module):
def __init__(self, input_size, hidden_size, num_classes):
super().__init__()
self.rnn = nn.RNN(input_size, hidden_size,
batch_first=True)
self.fc = nn.Linear(hidden_size, num_classes)
def forward(self, x):
_, h_n = self.rnn(x)
# h_n shape: (1, batch, hidden) -> squeeze to (batch, hidden)
h_n = h_n.squeeze(0)
return self.fc(h_n)
model = SequenceClassifier(16, 32, 5)
X = torch.randn(8, 20, 16)
logits = model(X)
print(logits.shape) # (8, 5) -- 8 samples, 5 classesMany-to-Many: etichettatura delle sequenze
Nelle attività di etichettatura di sequenze (tagging POS, riconoscimento di entità nominate, rilevamento di anomalie nelle serie temporali), è necessaria una previsione a ogni passo temporale, non solo all'ultimo. Utilizzi l'intero tensore output di nn.RNN (forma: batch x seq_len x hidden_size) e applichi un livello lineare a ogni passo temporale in modo indipendente. I pesi del livello lineare sono condivisi tra i passi temporali: un altro esempio di condivisione dei pesi nei modelli sequenziali.
import torch
import torch.nn as nn
class SequenceLabeler(nn.Module):
def __init__(self, input_size, hidden_size, num_classes):
super().__init__()
self.rnn = nn.RNN(input_size, hidden_size,
batch_first=True)
self.fc = nn.Linear(hidden_size, num_classes)
def forward(self, x):
output, _ = self.rnn(x) # (batch, seq, hidden)
return self.fc(output) # (batch, seq, num_classes)
model = SequenceLabeler(8, 16, 3)
X = torch.randn(4, 10, 8) # 4 seqs of 10 timesteps
logits = model(X)
print(logits.shape) # (4, 10, 3) per-step labelsRNN impilate e bidirezionali
Le RNN impilate (num_layers > 1) forniscono l'output di un livello RNN come input al successivo, apprendendo rappresentazioni temporali progressivamente più astratte. Le RNN bidirezionali elaborano la sequenza simultaneamente in avanti (da sinistra a destra) e all'indietro (da destra a sinistra), quindi concatenano gli stati nascosti. L'elaborazione bidirezionale consente a ogni output di incorporare il contesto sia passato sia futuro, una caratteristica utile per comprendere le frasi, in cui le parole future chiariscono il significato di quelle precedenti.
import torch
import torch.nn as nn
# Stacked bidirectional RNN
brnn = nn.RNN(
input_size=16,
hidden_size=32,
num_layers=3, # 3 stacked layers
batch_first=True,
bidirectional=True # forward + backward
)
X = torch.randn(4, 10, 16)
output, h_n = brnn(X)
# Output: (batch, seq, hidden*2) because bidirectional
print('Output shape:', output.shape) # (4, 10, 64)
# h_n: (num_layers*2, batch, hidden) -- 2 dirs x 3 layers
print('h_n shape:', h_n.shape) # (6, 4, 32)Backpropagation attraverso il tempo (BPTT)
La backpropagation attraverso il tempo srotola la RNN e applica la backpropagation standard al grafo srotolato. Per una sequenza di lunghezza T, i gradienti vengono calcolati a ogni passo temporale e propagati all'indietro. Il gradiente della loss rispetto ai parametri implica prodotti della stessa matrice dei pesi W_hh moltiplicata T volte. Quando il raggio spettrale di W_hh è minore di 1, questi prodotti svaniscono; quando è maggiore di 1, esplodono: questa è la difficoltà fondamentale nell'addestramento delle RNN su sequenze lunghe.
import torch
import torch.nn as nn
# Demonstrate gradient flow through different sequence lengths
rnn = nn.RNNCell(4, 8)
h = torch.zeros(1, 8, requires_grad=True)
# Short sequence: gradients flow back relatively well
for t in range(5):
x = torch.randn(1, 4)
h = rnn(x, h)
loss = h.sum()
loss.backward()
print('h.grad (seq=5):', h.grad.norm().item())
# For long sequences (T=100+), vanilla RNN gradients
# typically vanish (near zero) or explode (very large)
# This is why LSTM/GRU were inventedGestione delle sequenze di lunghezza variabile
Nella pratica, le sequenze di un batch hanno lunghezze diverse, ad esempio frasi con un numero diverso di parole. PyTorch gestisce questo caso con le sequenze impacchettate: torch.nn.utils.rnn.pack_padded_sequence rimuove il padding da un batch e impacchetta le sequenze in modo efficiente. Dopo la RNN, pad_packed_sequence ripristina il formato con padding. Senza l'impacchettamento, la RNN elabora inutilmente i token di padding e può contaminare lo stato nascosto con informazioni prive di significato provenienti dal padding.
import torch
import torch.nn as nn
from torch.nn.utils.rnn import pack_padded_sequence, pad_packed_sequence
rnn = nn.RNN(4, 8, batch_first=True)
# Padded sequences: length 5, 3, 2
X = torch.zeros(3, 5, 4) # (batch=3, max_seq=5, features=4)
X[0, :, :] = torch.randn(5, 4) # full seq
X[1, :3, :] = torch.randn(3, 4) # length 3
X[2, :2, :] = torch.randn(2, 4) # length 2
lengths = torch.tensor([5, 3, 2])
packed = pack_padded_sequence(X, lengths, batch_first=True)
out_packed, h_n = rnn(packed)
out, _ = pad_packed_sequence(out_packed, batch_first=True)
print(out.shape) # (3, 5, 8) -- back to padded formQuando utilizzare le RNN vanilla
Le RNN vanilla sono usate raramente nella pratica perché soffrono gravemente del problema dei gradienti che svaniscono su sequenze più lunghe di 10-20 passi temporali. Sono utili soprattutto per scopi didattici e per sequenze molto brevi. Per qualsiasi applicazione reale con sequenze più lunghe di 20 passi temporali, utilizzi LSTM o GRU, dotate di meccanismi di gating progettati specificamente per preservare le informazioni su lunghe distanze. Per le sequenze in cui l'ordine è meno importante, le architetture Transformer spesso superano entrambe.
# When to use each sequence model:
use_cases = {
'Vanilla RNN (nn.RNN)': 'Short sequences (<20 steps), learning/demos',
'LSTM': 'Long sequences, NLP, time-series (general)',
'GRU': 'Similar to LSTM but faster, fewer params',
'Transformer': 'Parallelisable, long documents, state-of-art NLP',
'Temporal Conv (TCN)': 'Long sequences, strong parallelism, audio',
}
for model, use in use_cases.items():
print(f'{model}: {use}')Verifica rapida
Verifichi la Sua comprensione dei concetti di Machine Learning con Python presentati in questa lezione.
Riepilogo della lezione
In questa lezione ha imparato che: le RNN vanilla mantengono uno stato nascosto che conserva la memoria tra i passi temporali usando la formula h_t = tanh(W_xh * x_t + W_hh * h_{t-1} + b), che lo srotolamento applica la stessa cella a ogni passo temporale con pesi condivisi e che la BPTT propaga i gradienti all'indietro attraverso il grafo srotolato, causando problemi di gradienti che svaniscono o esplodono nelle sequenze lunghe. Nel prossimo argomento esamineremo in dettaglio il problema dei gradienti che svaniscono e capiremo perché LSTM è stata progettata per risolverlo.
Impara Python con un tutor IA — gratis
Scrivi ed esegui vero codice nel tuo browser, ricevi aiuto istantaneo da un tutor IA disponibile 24/7, e riprendi da dove hai lasciato sul web o nell'app.
- Corsi
- 30
- Lezioni
- 120
Domande Frequenti
La lezione «RNN vanilla: stato nascosto e srotolamento della sequenza» è gratuita?
Sì — il testo completo di «RNN vanilla: stato nascosto e srotolamento della sequenza» è gratuito qui sul web. Per esercitarvi in modo interattivo (un editor di codice integrato e un tutor IA 24/7) e sbloccare il resto del corso Machine Learning Academy, passa a CoddyKit PRO. Il corso Machine Learning Academy include 4 lezioni in totale.
Cosa imparerò in «RNN vanilla: stato nascosto e srotolamento della sequenza»?
Imparerete a implementare manualmente una cella RNN a un passaggio, srotolarla su una sequenza breve e visualizzare come lo stato nascosto accumuli il contesto. Eserciti Machine Learning Academy con codice pratico che esegui direttamente nel browser, e un tutor IA 24/7 risponde alle tue domande mentre lavori sulla lezione.
Ho bisogno di esperienza per iniziare Machine Learning Academy?
Non è richiesta alcuna esperienza precedente. Machine Learning Academy su CoddyKit è strutturato per principianti e studenti avanzati, quindi puoi iniziare da qui o dall'inizio e procedere al tuo ritmo. Questa è la lezione 1 di 4.
Quanto tempo richiede la lezione «RNN vanilla: stato nascosto e srotolamento della sequenza»?
La maggior parte delle lezioni CoddyKit richiede circa 5–10 minuti. Ogni lezione è breve e interattiva, quindi fai progressi costanti e riprendi esattamente da dove hai lasciato su web e app.
Posso scrivere ed eseguire codice in questa lezione Machine Learning Academy?
Sì. Ogni lezione Machine Learning Academy include un editor di codice integrato, quindi scrivi ed esegui codice reale direttamente nel tuo browser e ricevi feedback istantaneo dall'IA — nessuna configurazione locale necessaria.
Tutte le lezioni di questo corso
- RNN vanilla: stato nascosto e srotolamento della sequenza
- Il problema del vanishing gradient nei passaggi temporali profondi
- Cella LSTM: gate di input, forget e output
- Sequence-to-One: analisi del sentiment con una LSTM