Backpropagation
La retropropagazione applica la regola della catena a ritroso attraverso una rete neurale per calcolare il gradiente di ogni peso in un unico passaggio. Tratta la matematica, il costo e il codice.
La retropropagazione, abbreviazione di «propagazione all'indietro degli errori», è l'algoritmo che calcola quanto ogni peso di una rete neurale abbia contribuito all'errore di una previsione. Partendo dalla funzione di perdita, applica la regola della catena livello per livello, tornando verso l'input, e riutilizza ogni risultato intermedio; una sola passata all'indietro produce così il gradiente di ogni peso, che il modello abbia migliaia o miliardi di parametri. Un ottimizzatore usa poi questi gradienti per aggiornare i pesi, rendendo la retropropagazione il passaggio di apprendimento alla base di quasi tutti i modelli di deep learning. Rumelhart, Hinton e Williams la resero popolare per le reti neurali con l'articolo del 1986 pubblicato su Nature, Learning representations by back-propagating errors.
Come funziona la retropropagazione#
L'addestramento su un batch richiede due passaggi. Il passaggio in avanti invia gli input attraverso ogni livello, memorizza le attivazioni intermedie e termina con la perdita. Il passaggio all'indietro parte dalla perdita e visita i livelli in ordine inverso. Ogni livello riceve il gradiente della perdita rispetto al proprio output, detto gradiente a monte, e lo moltiplica per le proprie derivate locali per produrre due risultati: il gradiente dei pesi del livello, conservato per l'ottimizzatore, e il gradiente rispetto all'input del livello, che diventa il gradiente a monte per il livello precedente.
Poiché ogni livello ha bisogno solo del gradiente a monte e delle attivazioni memorizzate, non viene mai calcolata due volte la stessa derivata. In termini di differenziazione automatica, la retropropagazione è la differenziazione in modalità inversa del grafo computazionale della rete; PyTorch costruisce il grafo durante il passaggio in avanti e lo percorre all'indietro quando viene chiamato loss.backward(). In una rete con due pesi, il risultato di autograd di PyTorch si può verificare con la regola della catena svolta a mano:
import torch
# A two-weight network: prediction = w2 * tanh(w1 * x)
x, target = torch.tensor(1.5), torch.tensor(0.5)
w1 = torch.tensor(0.8, requires_grad=True)
w2 = torch.tensor(-1.2, requires_grad=True)
# Forward pass, keeping the hidden activation h for the backward pass
h = torch.tanh(w1 * x)
loss = (w2 * h - target) ** 2
# Backward pass: autograd applies the chain rule from the loss back to each weight
loss.backward()
# The same chain rule by hand, reusing the upstream gradient dL/dy for both weights
dL_dy = 2 * (w2 * h - target)
dL_dw2 = dL_dy * h
dL_dw1 = dL_dy * w2 * (1 - h**2) * x
print(f"autograd: dL/dw1 = {w1.grad:.4f}, dL/dw2 = {w2.grad:.4f}")
print(f"by hand: dL/dw1 = {dL_dw1:.4f}, dL/dw2 = {dL_dw2:.4f}")Entrambe le righe stampano dL/dw1 = 1.6475, dL/dw2 = -2.5016. La versione scritta a mano calcola dL_dy una sola volta e lo riutilizza per entrambi i pesi; lo stesso riutilizzo consente ad autograd di gestire reti con miliardi di pesi. La retropropagazione nel tempo (BPTT) è lo stesso algoritmo applicato a una rete neurale ricorrente srotolata lungo i suoi passi temporali.
Retropropagazione e ottimizzatore#
La retropropagazione calcola soltanto i gradienti e non modifica i pesi. Un algoritmo di ottimizzazione, come la discesa del gradiente stocastica (SGD) o Adam, converte questi gradienti in aggiornamenti dei pesi, moltiplicandoli per il tasso di apprendimento. PyTorch aggiunge il risultato di ogni passaggio all'indietro ai gradienti già memorizzati nei pesi, così un singolo passo dell'ottimizzatore può combinare più batch:
L'addestramento di Ultralytics YOLO usa questo accumulo dei gradienti per raggiungere un batch nominale di 64 immagini (nbs=64): dopo il riscaldamento, un'esecuzione batch=16 applica la retropropagazione a ogni batch, ma esegue un passo dell'ottimizzatore ogni quattro batch, dopo aver limitato la norma del gradiente combinato a 10.
Gradienti evanescenti ed esplosivi#
La propagazione all’indietro moltiplica le derivate locali strato dopo strato, quindi l’ampiezza del gradiente può diminuire o aumentare geometricamente con la profondità. Quando la maggior parte dei fattori è inferiore a 1, come accade con le unità sigmoid sature, gli strati iniziali ricevono un segnale quasi nullo: è il problema del gradiente evanescente. Quando la maggior parte dei fattori è superiore a 1, il problema del gradiente esplosivo produce aggiornamenti enormi e perdite NaN. Le attivazioni della famiglia ReLU, un’inizializzazione dei pesi scalata, la normalizzazione batch e le connessioni skip delle reti residue mantengono stabili le ampiezze dei gradienti anche su centinaia di strati.
Costo computazionale e di memoria#
Ogni attivazione necessaria per una derivata locale deve rimanere in memoria dal passaggio in avanti finché il passaggio all’indietro non la raggiunge; di conseguenza, la memoria necessaria per l’addestramento cresce con la dimensione del batch, la risoluzione dell’input e la profondità, e l’addestramento richiede molta più memoria dell’inferenza. Il gradient checkpointing conserva solo alcune attivazioni e ricalcola le altre durante il passaggio all’indietro, scambiando un maggiore costo computazionale con un minore uso di memoria; la precisione mista mantiene la maggior parte delle attivazioni in formati a 16 bit, che occupano metà dello spazio dei valori float a 32 bit. Dal punto di vista computazionale, il passaggio all’indietro costa circa il doppio di quello in avanti, perché ogni strato calcola i gradienti sia dei propri pesi sia dei propri input: per questo le stime basate sulle leggi di scalabilità considerano circa 6 operazioni in virgola mobile per parametro per token di addestramento: 2 nel passaggio in avanti e 4 in quello all’indietro.










