Introduzione: Come i Modelli Trovano i Pesi Migliori
Addestrare un modello di machine learning significa trovare i valori dei parametri che minimizzano una funzione di perdita. Questo processo si chiama ottimizzazione ed e il cuore di tutto il deep learning. L'algoritmo più usato e il gradient descent, con le sue numerose varianti che bilanciano velocità, stabilità e generalizzazione.
Cosa Imparerai
- Gradient Descent vanilla e i suoi limiti
- Stochastic Gradient Descent (SGD) e mini-batch
- Momentum e Nesterov Accelerated Gradient
- Algoritmi adattivi: RMSprop e Adam
- Learning rate scheduling: warmup, cosine annealing
- Convergenza, saddle points e landscape della loss
Gradient Descent: L'Algoritmo Base
Il gradient descent (discesa del gradiente) aggiorna i parametri muovendosi nella direzione opposta al gradiente della loss:
dove \\eta e il learning rate (iperparametro critico) e \\nabla_{\\theta} L e il gradiente calcolato su tutti i dati di training (batch gradient descent).
Analogia: immagina di essere su una collina nella nebbia e voler raggiungere la valle più bassa. Ad ogni passo, senti la pendenza sotto i piedi (gradiente) e cammini nella direzione più ripida verso il basso. Il learning rate e la dimensione del passo: troppo grande e salti oltre la valle, troppo piccolo e ci metti un'eternita.
Il Problema del Learning Rate
La scelta del learning rate e cruciale:
- \\eta troppo grande: la loss oscilla o diverge
- \\eta troppo piccolo: convergenza lentissima
- \\eta giusto: convergenza stabile verso il minimo
import numpy as np
# Funzione obiettivo: f(x) = x^4 - 3x^2 + 2 (ha due minimi)
def f(x):
return x**4 - 3*x**2 + 2
def grad_f(x):
return 4*x**3 - 6*x
# Gradient descent con diversi learning rate
for lr in [0.01, 0.05, 0.1]:
x = 2.0 # punto di partenza
history = [x]
for _ in range(100):
x = x - lr * grad_f(x)
history.append(x)
print(f"lr={lr}: x_finale={x:.6f}, f(x)={f(x):.6f}")
Stochastic Gradient Descent (SGD)
Calcolare il gradiente su tutto il dataset e costoso. SGD usa un singolo campione (o un mini-batch) per stimare il gradiente:
Il gradiente stimato e rumoroso ma in media punta nella direzione giusta. Il rumore ha un beneficio sorprendente: aiuta a sfuggire ai minimi locali e agisce come regolarizzatore implicito.
In pratica si usa il mini-batch SGD con batch di 32-256 campioni, un compromesso tra varianza del gradiente e efficienza computazionale:
Momentum: Accelerare la Convergenza
Il momentum aggiunge una "velocità" all'aggiornamento, accumulando i gradienti passati come una media mobile esponenziale:
dove \\beta \\approx 0.9 controlla quanto "memoria" ha il momentum.
Intuizione: pensa a una palla che rotola giù per una collina. Senza momentum, si ferma ogni volta che incontra una piccola irregolarita. Con momentum, la palla accumula velocità e supera le piccole buche, convergendo più rapidamente verso la valle. Momentum riduce le oscillazioni nelle direzioni con gradienti alternanti e accelera nelle direzioni con gradiente costante.
Nesterov Accelerated Gradient (NAG)
NAG migliora il momentum calcolando il gradiente nella posizione "futura" (look-ahead):
"Prima guardo dove sto andando, poi correggo la direzione." NAG converge più velocemente di momentum standard e rallenta prima dei minimi.
RMSprop: Learning Rate Adattivo
RMSprop adatta il learning rate individualmente per ogni parametro, dividendo per la radice della media dei quadrati dei gradienti passati:
Per parametri con gradienti grandi, il learning rate effettivo si riduce; per parametri con gradienti piccoli, si aumenta. Questo risolve il problema delle scale diverse tra feature.
Adam: Lo State-of-the-Art
Adam (Adaptive Moment Estimation) combina il meglio di momentum e RMSprop, mantenendo sia una media mobile del gradiente (primo momento) che una media mobile del gradiente al quadrato (secondo momento):
Con bias correction per compensare l'inizializzazione a zero:
Aggiornamento finale:
Iperparametri di default raccomandati: \\beta_1 = 0.9, \\beta_2 = 0.999, \\epsilon = 10^{-8}.
import numpy as np
class Adam:
def __init__(self, lr=0.001, beta1=0.9, beta2=0.999, epsilon=1e-8):
self.lr = lr
self.beta1 = beta1
self.beta2 = beta2
self.epsilon = epsilon
self.m = None # Primo momento
self.v = None # Secondo momento
self.t = 0
def update(self, params, grads):
if self.m is None:
self.m = np.zeros_like(params)
self.v = np.zeros_like(params)
self.t += 1
self.m = self.beta1 * self.m + (1 - self.beta1) * grads
self.v = self.beta2 * self.v + (1 - self.beta2) * grads**2
# Bias correction
m_hat = self.m / (1 - self.beta1**self.t)
v_hat = self.v / (1 - self.beta2**self.t)
params -= self.lr * m_hat / (np.sqrt(v_hat) + self.epsilon)
return params
# Test: minimizzare f(x,y) = x^2 + 10*y^2 (landscape ellittico)
def f(params):
return params[0]**2 + 10 * params[1]**2
def grad_f(params):
return np.array([2*params[0], 20*params[1]])
# Confronto SGD vs Adam
params_sgd = np.array([5.0, 5.0])
params_adam = np.array([5.0, 5.0])
optimizer = Adam(lr=0.1)
print("Step | SGD f(x) | Adam f(x)")
for step in range(50):
# SGD
g = grad_f(params_sgd)
params_sgd -= 0.01 * g
# Adam
g = grad_f(params_adam)
params_adam = optimizer.update(params_adam, g)
if step % 10 == 0:
print(f"{step:4d} | {f(params_sgd):8.4f} | {f(params_adam):8.4f}")
Learning Rate Scheduling
Partire con un learning rate fisso non e ottimale. Le strategie di scheduling adattano \\eta durante il training:
Step Decay
dove \\gamma = 0.1 e s e il numero di epoche tra ogni riduzione.
Cosine Annealing
Riduce gradualmente il learning rate seguendo una curva coseno, più aggressivo alla fine.
Warmup + Decay
Usato nei Transformer: si parte con un learning rate basso che cresce linearmente per T_w step (warmup), poi decresce:
import numpy as np
def cosine_annealing(t, T, eta_min=1e-6, eta_max=1e-3):
return eta_min + 0.5 * (eta_max - eta_min) * (1 + np.cos(t * np.pi / T))
def warmup_cosine(t, warmup_steps, total_steps, eta_max=1e-3):
if t < warmup_steps:
return eta_max * t / warmup_steps
else:
progress = (t - warmup_steps) / (total_steps - warmup_steps)
return eta_max * 0.5 * (1 + np.cos(progress * np.pi))
# Visualizzazione (valori)
total_steps = 1000
warmup = 100
print("Step | Cosine LR | Warmup+Cosine LR")
for t in range(0, total_steps, 100):
cos_lr = cosine_annealing(t, total_steps)
warm_lr = warmup_cosine(t, warmup, total_steps)
print(f"{t:4d} | {cos_lr:.6f} | {warm_lr:.6f}")
Saddle Points e Loss Landscape
In spazi ad alta dimensionalità (milioni di parametri), i minimi locali sono rari. Il vero problema sono i saddle points: punti dove il gradiente e zero ma che non sono ne minimi ne massimi. La probabilità di un saddle point cresce esponenzialmente con la dimensionalità.
Fortunatamente, SGD con momentum e Adam riescono a sfuggire ai saddle points grazie al rumore stocastico e al momentum accumulato.
Riepilogo e Connessioni con il ML
Punti Chiave da Ricordare
- Gradient Descent: \\theta \\leftarrow \\theta - \\eta \\nabla L - l'algoritmo base
- SGD: usa mini-batch per efficienza, il rumore aiuta la generalizzazione
- Momentum: accumula velocità, supera le irregolarita della loss surface
- Adam: combina momentum + learning rate adattivo, il default per il DL
- Learning rate scheduling: warmup + cosine decay e lo standard per i Transformer
- Saddle points: più problematici dei minimi locali in alte dimensioni
Nel Prossimo Articolo: esploreremo la teoria dell'informazione. Vedremo entropia, cross-entropy (la loss più usata per la classificazione), KL divergence, e le connessioni profonde con la maximum likelihood.







