Comparativa de Optimizadores Adaptativos
Análisis detallado y comparación práctica de SGD, AdaGrad, RMSProp, AdaDelta, Adam, AdamW, NAdam y RAdam en MNIST.
Comparativa de Optimizadores Adaptativos
Analisis detallado y comparacion practica de SGD, AdaGrad, RMSProp, AdaDelta, Adam, AdamW, NAdam y RAdam en MNIST
Motivacion: del learning rate global al aprendizaje adaptativo
En el notebook sobre momentum vimos como acumular gradientes pasados acelera la convergencia. Sin embargo, tanto SGD como SGD+Momentum usan un unico learning rate global para todos los parametros. Esto es suboptimo porque:
- Parametros asociados a features frecuentes reciben gradientes grandes y constantes. Un learning rate alto les basta.
- Parametros asociados a features raras reciben gradientes esporadicos. Necesitan pasos mas grandes cuando aparecen, pero un learning rate alto global causaria inestabilidad en los parametros frecuentes.
Los optimizadores adaptativos resuelven esto manteniendo un learning rate individual por parametro, que se ajusta automaticamente segun el historial de gradientes de cada uno.
Los optimizadores que compararemos
1. SGD + Momentum (referencia)
$$v_t = \beta v_{t-1} + \nabla \mathcal{L}(\theta_t)$$ $$\theta_{t+1} = \theta_t - \eta , v_t$$
Learning rate global fijo. Referencia para comparar.
2. AdaGrad (Duchi et al., 2011)
Acumula el cuadrado de todos los gradientes pasados por parametro:
$$G_t = G_{t-1} + (\nabla \mathcal{L})^2$$ $$\theta_{t+1} = \theta_t - \frac{\eta}{\sqrt{G_t} + \epsilon} \nabla \mathcal{L}$$
Adapta el learning rate por parametro: features frecuentes reciben lr pequeno, raras lr grande. Problema: $G_t$ crece montonamente, haciendo que el lr efectivo decaiga a cero.
3. RMSProp (Hinton, 2012)
Corrige AdaGrad usando una media movil exponencial en lugar de la suma acumulada:
$$E[g^2]t = \gamma , E[g^2]{t-1} + (1 - \gamma)(\nabla \mathcal{L})^2$$ $$\theta_{t+1} = \theta_t - \frac{\eta}{\sqrt{E[g^2]_t} + \epsilon} \nabla \mathcal{L}$$
con $\gamma = 0.99$ tipicamente. El lr efectivo ya no decae a cero.
4. AdaDelta (Zeiler, 2012)
Similar a RMSProp pero elimina la necesidad de especificar un learning rate inicial. Usa la ratio entre la RMS de actualizaciones pasadas y la RMS de gradientes.
5. Adam (Kingma y Ba, 2015)
Combina momentum (media movil del gradiente) con RMSProp (media movil del cuadrado):
$$m_t = \beta_1 m_{t-1} + (1 - \beta_1) \nabla \mathcal{L}, \quad v_t = \beta_2 v_{t-1} + (1 - \beta_2) (\nabla \mathcal{L})^2$$
Con correccion de sesgo: $\hat{m}_t = m_t / (1 - \beta_1^t)$, $\hat{v}_t = v_t / (1 - \beta_2^t)$
$$\theta_{t+1} = \theta_t - \frac{\eta}{\sqrt{\hat{v}_t} + \epsilon} \hat{m}_t$$
6. AdamW (Loshchilov y Hutter, 2019)
Aplica decoupled weight decay en lugar de L2 regularization dentro de Adam. Es el optimizador preferido para Transformers y redes modernas.
7. NAdam (Dozat, 2016)
Incorpora Nesterov momentum en Adam, calculando un paso anticipado.
8. RAdam (Liu et al., 2020)
Resuelve la varianza alta de $v_t$ en las primeras iteraciones de Adam. Degenera a SGD con momentum al inicio y transiciona a Adam cuando la varianza se estabiliza.
Diseno del experimento
| Componente | Eleccion | Justificacion |
|---|---|---|
| Dataset | MNIST (60k train / 10k test) | Problema sencillo para aislar el efecto del optimizador |
| Modelo | MLP (784-512-256-128-10) | Red profunda para evidenciar diferencias |
| Optimizadores | 8 variantes | Cobertura de las principales familias |
| Epocas | 20 | Suficientes para convergencia |
| Batch size | 64 | Estandar |
Estructura del notebook
- Importaciones y configuracion
- Carga de MNIST
- Definicion del MLP
- Configuracion de los 8 optimizadores
- Entrenamiento comparativo
- Comparacion de curvas de loss y accuracy
- Velocidad de convergencia: epocas para alcanzar 95% y 97%
- Ranking final y tiempo de entrenamiento
- Robustez ante distintos learning rates
- Conclusiones y recomendaciones
1. Importaciones y configuracion
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader
from torchvision import datasets, transforms
import matplotlib.pyplot as plt
import numpy as np
import copy
import time
import warnings
warnings.filterwarnings('ignore')
SEED = 42
torch.manual_seed(SEED)
np.random.seed(SEED)
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
print(f"Dispositivo: {device}")
print(f"PyTorch: {torch.__version__}")
Dispositivo: cuda PyTorch: 2.10.0+cu128
2. Dataset: MNIST
transform = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.1307,), (0.3081,)),
])
train_dataset = datasets.MNIST(root='./data', train=True, download=True, transform=transform)
test_dataset = datasets.MNIST(root='./data', train=False, download=True, transform=transform)
train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True, num_workers=0)
test_loader = DataLoader(test_dataset, batch_size=256, shuffle=False, num_workers=0)
print(f"Train: {len(train_dataset):,} imagenes")
print(f"Test: {len(test_dataset):,} imagenes")
100.0% 100.0% 100.0% 100.0%
Train: 60,000 imagenes Test: 10,000 imagenes
3. Definicion del MLP
Usamos una red de 4 capas para tener mas profundidad y evidenciar las diferencias entre optimizadores. Todos comparten exactamente los mismos pesos iniciales.
class MLP(nn.Module):
def __init__(self):
super().__init__()
self.net = nn.Sequential(
nn.Flatten(),
nn.Linear(784, 512),
nn.ReLU(),
nn.Linear(512, 256),
nn.ReLU(),
nn.Linear(256, 128),
nn.ReLU(),
nn.Linear(128, 10),
)
def forward(self, x):
return self.net(x)
torch.manual_seed(SEED)
base_model = MLP()
initial_weights = copy.deepcopy(base_model.state_dict())
n_params = sum(p.numel() for p in base_model.parameters())
print(f"Arquitectura: 784 -> 512 -> 256 -> 128 -> 10")
print(f"Parametros: {n_params:,}")
Arquitectura: 784 -> 512 -> 256 -> 128 -> 10 Parametros: 567,434
4. Configuracion de los 8 optimizadores
Definimos cada optimizador con sus hiperparametros por defecto (segun el paper original). El learning rate base es 0.001, excepto para SGD (que necesita un lr mas alto) y AdaDelta (que no recibe lr como entrada).
def create_optimizers(model):
"""Crea un diccionario de optimizadores para el modelo dado."""
return {
'SGD + Momentum': optim.SGD(model.parameters(), lr=0.01, momentum=0.9),
'AdaGrad': optim.Adagrad(model.parameters(), lr=0.01),
'RMSProp': optim.RMSprop(model.parameters(), lr=0.001, alpha=0.99),
'AdaDelta': optim.Adadelta(model.parameters(), rho=0.9),
'Adam': optim.Adam(model.parameters(), lr=0.001),
'AdamW': optim.AdamW(model.parameters(), lr=0.001, weight_decay=0.01),
'NAdam': optim.NAdam(model.parameters(), lr=0.001),
'RAdam': optim.RAdam(model.parameters(), lr=0.001),
}
# Mostrar la configuracion
dummy_model = MLP()
optimizers_info = create_optimizers(dummy_model)
print(f"{'Optimizador':<18} {'Tipo':>25} {'LR':>8}")
print("=" * 53)
for name, opt in optimizers_info.items():
lr = opt.defaults.get('lr', 'N/A')
lr_str = f"{lr:.4f}" if isinstance(lr, float) else str(lr)
print(f"{name:<18} {type(opt).__name__:>25} {lr_str:>8}")
del dummy_model
Optimizador Tipo LR ===================================================== SGD + Momentum SGD 0.0100 AdaGrad Adagrad 0.0100 RMSProp RMSprop 0.0010 AdaDelta Adadelta 1.0000 Adam Adam 0.0010 AdamW AdamW 0.0010 NAdam NAdam 0.0010 RAdam RAdam 0.0010
5. Entrenamiento comparativo
Entrenamos 8 copias del modelo (mismos pesos iniciales) con cada optimizador. Registramos loss, accuracy y tiempo por epoca.
EPOCHS = 20
def train_model(model, optimizer, train_loader, test_loader, epochs):
"""Entrena un modelo y devuelve historial completo."""
criterion = nn.CrossEntropyLoss()
history = {
'train_loss': [], 'test_loss': [],
'train_acc': [], 'test_acc': [],
'epoch_times': [],
}
for epoch in range(epochs):
start = time.time()
# Entrenamiento
model.train()
total_loss = 0.0
correct = 0
total = 0
for images, labels in train_loader:
images, labels = images.to(device), labels.to(device)
optimizer.zero_grad()
outputs = model(images)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
total_loss += loss.item() * images.size(0)
_, predicted = torch.max(outputs, 1)
total += labels.size(0)
correct += (predicted == labels).sum().item()
history['train_loss'].append(total_loss / total)
history['train_acc'].append(100.0 * correct / total)
# Evaluacion
model.eval()
total_loss = 0.0
correct = 0
total = 0
with torch.no_grad():
for images, labels in test_loader:
images, labels = images.to(device), labels.to(device)
outputs = model(images)
loss = criterion(outputs, labels)
total_loss += loss.item() * images.size(0)
_, predicted = torch.max(outputs, 1)
total += labels.size(0)
correct += (predicted == labels).sum().item()
history['test_loss'].append(total_loss / total)
history['test_acc'].append(100.0 * correct / total)
history['epoch_times'].append(time.time() - start)
return history
# Entrenar con cada optimizador
all_results = {}
opt_names = ['SGD + Momentum', 'AdaGrad', 'RMSProp', 'AdaDelta',
'Adam', 'AdamW', 'NAdam', 'RAdam']
for name in opt_names:
print(f"Entrenando con {name}...", end=' ')
torch.manual_seed(SEED)
model = MLP().to(device)
model.load_state_dict(copy.deepcopy(initial_weights))
# Crear optimizador
opt_dict = create_optimizers(model)
optimizer = opt_dict[name]
history = train_model(model, optimizer, train_loader, test_loader, EPOCHS)
all_results[name] = history
total_time = sum(history['epoch_times'])
print(f"Test Acc: {history['test_acc'][-1]:.2f}% | Tiempo: {total_time:.1f}s")
print("\nEntrenamiento completado.")
Entrenando con SGD + Momentum... Test Acc: 98.41% | Tiempo: 66.7s Entrenando con AdaGrad... Test Acc: 98.41% | Tiempo: 67.7s Entrenando con RMSProp... Test Acc: 98.27% | Tiempo: 67.8s Entrenando con AdaDelta... Test Acc: 98.30% | Tiempo: 68.3s Entrenando con Adam... Test Acc: 98.00% | Tiempo: 68.6s Entrenando con AdamW... Test Acc: 97.73% | Tiempo: 68.9s Entrenando con NAdam... Test Acc: 97.97% | Tiempo: 69.3s Entrenando con RAdam... Test Acc: 97.95% | Tiempo: 69.6s Entrenamiento completado.
6. Comparacion de curvas de loss y accuracy
# Paleta de colores para 8 optimizadores
colors_8 = {
'SGD + Momentum': '#e74c3c',
'AdaGrad': '#e67e22',
'RMSProp': '#f1c40f',
'AdaDelta': '#95a5a6',
'Adam': '#0984E3',
'AdamW': '#00B894',
'NAdam': '#6c5ce7',
'RAdam': '#d63031',
}
epochs_range = range(1, EPOCHS + 1)
fig, axes = plt.subplots(2, 2, figsize=(16, 11))
# Train Loss
ax = axes[0, 0]
for name in opt_names:
ax.plot(epochs_range, all_results[name]['train_loss'],
label=name, color=colors_8[name], linewidth=1.8)
ax.set_title('Train Loss', fontweight='bold')
ax.set_xlabel('Epoca')
ax.set_ylabel('Cross-Entropy Loss')
ax.legend(fontsize=7, ncol=2)
ax.grid(True, alpha=0.3)
# Test Loss
ax = axes[0, 1]
for name in opt_names:
ax.plot(epochs_range, all_results[name]['test_loss'],
label=name, color=colors_8[name], linewidth=1.8)
ax.set_title('Test Loss', fontweight='bold')
ax.set_xlabel('Epoca')
ax.set_ylabel('Cross-Entropy Loss')
ax.legend(fontsize=7, ncol=2)
ax.grid(True, alpha=0.3)
# Train Accuracy
ax = axes[1, 0]
for name in opt_names:
ax.plot(epochs_range, all_results[name]['train_acc'],
label=name, color=colors_8[name], linewidth=1.8)
ax.set_title('Train Accuracy', fontweight='bold')
ax.set_xlabel('Epoca')
ax.set_ylabel('Accuracy (%)')
ax.legend(fontsize=7, ncol=2)
ax.grid(True, alpha=0.3)
# Test Accuracy
ax = axes[1, 1]
for name in opt_names:
ax.plot(epochs_range, all_results[name]['test_acc'],
label=name, color=colors_8[name], linewidth=1.8)
ax.set_title('Test Accuracy', fontweight='bold')
ax.set_xlabel('Epoca')
ax.set_ylabel('Accuracy (%)')
ax.legend(fontsize=7, ncol=2)
ax.grid(True, alpha=0.3)
plt.suptitle('Comparativa de 8 optimizadores (MNIST, MLP 784-512-256-128-10)',
fontweight='bold', fontsize=13, y=1.02)
plt.tight_layout()
plt.show()
7. Velocidad de convergencia
Un aspecto clave es cuantas epocas necesita cada optimizador para alcanzar ciertos umbrales de accuracy. Un optimizador que alcanza 97% en la epoca 3 es mas eficiente que uno que lo logra en la epoca 15, incluso si ambos terminan con la misma accuracy final.
# Epocas para alcanzar umbrales de accuracy en test
thresholds = [90, 95, 97, 98]
print(f"{'Optimizador':<18}", end='')
for t in thresholds:
print(f" {t}% test acc", end='')
print(f" {'Final':>8}")
print("=" * 75)
convergence_data = {}
for name in opt_names:
row = []
print(f"{name:<18}", end='')
for t in thresholds:
epoch_reached = None
for ep, acc in enumerate(all_results[name]['test_acc'], 1):
if acc >= t:
epoch_reached = ep
break
row.append(epoch_reached)
if epoch_reached:
print(f" {'ep ' + str(epoch_reached):>10}", end='')
else:
print(f" {'---':>10}", end='')
convergence_data[name] = row
print(f" {all_results[name]['test_acc'][-1]:>7.2f}%")
Optimizador 90% test acc 95% test acc 97% test acc 98% test acc Final =========================================================================== SGD + Momentum ep 1 ep 1 ep 2 ep 8 98.41% AdaGrad ep 1 ep 1 ep 2 ep 4 98.41% RMSProp ep 1 ep 1 ep 2 ep 6 98.27% AdaDelta ep 1 ep 1 ep 3 ep 6 98.30% Adam ep 1 ep 1 ep 3 ep 6 98.00% AdamW ep 1 ep 1 ep 2 ep 12 97.73% NAdam ep 1 ep 1 ep 4 ep 5 97.97% RAdam ep 1 ep 1 ep 4 ep 6 97.95%
# Visualizar velocidad de convergencia
fig, ax = plt.subplots(figsize=(12, 6))
x = np.arange(len(thresholds))
width = 0.1
offsets = np.arange(len(opt_names)) - len(opt_names)/2 + 0.5
for i, name in enumerate(opt_names):
vals = [v if v is not None else EPOCHS+1 for v in convergence_data[name]]
bars = ax.bar(x + offsets[i]*width, vals, width,
label=name, color=colors_8[name], alpha=0.8)
ax.set_xlabel('Umbral de accuracy en test')
ax.set_ylabel('Epocas necesarias')
ax.set_title('Velocidad de convergencia por optimizador', fontweight='bold')
ax.set_xticks(x)
ax.set_xticklabels([f'{t}%' for t in thresholds])
ax.legend(fontsize=7, ncol=2)
ax.grid(True, alpha=0.3, axis='y')
ax.set_ylim(0, EPOCHS + 2)
plt.tight_layout()
plt.show()
8. Ranking final y tiempo de entrenamiento
# Ranking por accuracy final
ranking = sorted(opt_names, key=lambda n: all_results[n]['test_acc'][-1], reverse=True)
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(15, 6))
# Accuracy final (barras horizontales)
final_accs = [all_results[n]['test_acc'][-1] for n in ranking]
colors_ranked = [colors_8[n] for n in ranking]
bars = ax1.barh(range(len(ranking)), final_accs, color=colors_ranked, alpha=0.8)
ax1.set_yticks(range(len(ranking)))
ax1.set_yticklabels(ranking)
ax1.set_xlabel('Test Accuracy (%)')
ax1.set_title('Ranking por accuracy final', fontweight='bold')
ax1.set_xlim(min(final_accs) - 2, max(final_accs) + 0.5)
for bar, acc in zip(bars, final_accs):
ax1.text(bar.get_width() + 0.1, bar.get_y() + bar.get_height()/2,
f'{acc:.2f}%', va='center', fontsize=9)
ax1.grid(True, alpha=0.3, axis='x')
# Tiempo total de entrenamiento
total_times = [sum(all_results[n]['epoch_times']) for n in ranking]
bars2 = ax2.barh(range(len(ranking)), total_times, color=colors_ranked, alpha=0.8)
ax2.set_yticks(range(len(ranking)))
ax2.set_yticklabels(ranking)
ax2.set_xlabel('Tiempo total (s)')
ax2.set_title('Tiempo de entrenamiento (20 epocas)', fontweight='bold')
for bar, t in zip(bars2, total_times):
ax2.text(bar.get_width() + 0.5, bar.get_y() + bar.get_height()/2,
f'{t:.1f}s', va='center', fontsize=9)
ax2.grid(True, alpha=0.3, axis='x')
plt.suptitle('Comparativa final de optimizadores',
fontweight='bold', y=1.02)
plt.tight_layout()
plt.show()
9. Robustez ante distintos learning rates
Un buen optimizador deberia funcionar razonablemente bien con un rango amplio de learning rates. Entrenemos cada optimizador con 5 learning rates diferentes (exceptuando AdaDelta, que no depende del lr) y veamos como varia la accuracy final.
learning_rates = [0.0001, 0.001, 0.005, 0.01, 0.05]
# Excluimos AdaDelta porque su lr no se usa de la misma manera
opt_for_lr = [n for n in opt_names if n != 'AdaDelta']
EPOCHS_LR = 10 # Menos epocas para este analisis
lr_results = {name: [] for name in opt_for_lr}
for lr in learning_rates:
print(f"\nLR = {lr}:")
for name in opt_for_lr:
torch.manual_seed(SEED)
model = MLP().to(device)
model.load_state_dict(copy.deepcopy(initial_weights))
# Crear optimizador con lr especifico
if name == 'SGD + Momentum':
opt = optim.SGD(model.parameters(), lr=lr, momentum=0.9)
elif name == 'AdaGrad':
opt = optim.Adagrad(model.parameters(), lr=lr)
elif name == 'RMSProp':
opt = optim.RMSprop(model.parameters(), lr=lr, alpha=0.99)
elif name == 'Adam':
opt = optim.Adam(model.parameters(), lr=lr)
elif name == 'AdamW':
opt = optim.AdamW(model.parameters(), lr=lr, weight_decay=0.01)
elif name == 'NAdam':
opt = optim.NAdam(model.parameters(), lr=lr)
elif name == 'RAdam':
opt = optim.RAdam(model.parameters(), lr=lr)
criterion = nn.CrossEntropyLoss()
diverged = False
for epoch in range(EPOCHS_LR):
model.train()
for images, labels in train_loader:
images, labels = images.to(device), labels.to(device)
opt.zero_grad()
outputs = model(images)
loss = criterion(outputs, labels)
if torch.isnan(loss):
diverged = True
break
loss.backward()
opt.step()
if diverged:
break
if diverged:
test_acc = 0.0
else:
model.eval()
correct = total = 0
with torch.no_grad():
for images, labels in test_loader:
images, labels = images.to(device), labels.to(device)
outputs = model(images)
_, predicted = torch.max(outputs, 1)
total += labels.size(0)
correct += (predicted == labels).sum().item()
test_acc = 100.0 * correct / total
lr_results[name].append(test_acc)
print(f" {name:<18} -> {test_acc:.1f}%")
LR = 0.0001: SGD + Momentum -> 89.6% AdaGrad -> 89.6% RMSProp -> 98.1% Adam -> 97.6% AdamW -> 97.7% NAdam -> 97.8% RAdam -> 97.8% LR = 0.001: SGD + Momentum -> 97.0% AdaGrad -> 95.8% RMSProp -> 98.2% Adam -> 98.0% AdamW -> 98.1% NAdam -> 98.2% RAdam -> 97.9% LR = 0.005: SGD + Momentum -> 98.0% AdaGrad -> 98.2% RMSProp -> 96.4% Adam -> 97.2% AdamW -> 97.0% NAdam -> 96.8% RAdam -> 97.0% LR = 0.01: SGD + Momentum -> 98.1% AdaGrad -> 98.4% RMSProp -> 95.4% Adam -> 96.3% AdamW -> 96.3% NAdam -> 96.5% RAdam -> 96.4% LR = 0.05: SGD + Momentum -> 98.0% AdaGrad -> 97.8% RMSProp -> 9.8% Adam -> 21.0% AdamW -> 10.1% NAdam -> 10.1% RAdam -> 10.1%
# Visualizar robustez ante lr
fig, ax = plt.subplots(figsize=(12, 6))
for name in opt_for_lr:
ax.plot(learning_rates, lr_results[name], 'o-', color=colors_8[name],
label=name, linewidth=2, markersize=6)
ax.set_xscale('log')
ax.set_xlabel('Learning Rate')
ax.set_ylabel('Test Accuracy (%) tras 10 epocas')
ax.set_title('Robustez ante distintos learning rates', fontweight='bold')
ax.legend(fontsize=8)
ax.grid(True, alpha=0.3)
ax.set_ylim(0, 100)
plt.tight_layout()
plt.show()
# Tabla resumen
print(f"\n{'Optimizador':<18}", end='')
for lr in learning_rates:
print(f" lr={lr:<7}", end='')
print(f" {'Rango':>8}")
print("=" * 75)
for name in opt_for_lr:
print(f"{name:<18}", end='')
valid = [a for a in lr_results[name] if a > 10]
for acc in lr_results[name]:
print(f" {acc:>7.1f}%", end='')
rango = max(valid) - min(valid) if len(valid) > 1 else 0
print(f" {rango:>7.1f}%")
Optimizador lr=0.0001 lr=0.001 lr=0.005 lr=0.01 lr=0.05 Rango =========================================================================== SGD + Momentum 89.6% 97.0% 98.0% 98.1% 98.0% 8.5% AdaGrad 89.6% 95.8% 98.2% 98.4% 97.8% 8.8% RMSProp 98.1% 98.2% 96.4% 95.4% 9.8% 2.8% Adam 97.6% 98.0% 97.2% 96.3% 21.0% 77.0% AdamW 97.7% 98.1% 97.0% 96.3% 10.1% 88.0% NAdam 97.8% 98.2% 96.8% 96.5% 10.1% 88.1% RAdam 97.8% 97.9% 97.0% 96.4% 10.1% 87.8%
10. Conclusiones y recomendaciones
| Optimizador | Ventaja principal | Desventaja | Cuando usarlo |
|---|---|---|---|
| SGD + Momentum | Mejor generalizacion, simple | Requiere ajuste fino de lr | Cuando se puede hacer HPO del lr y se prioriza generalizacion |
| AdaGrad | Bueno para features dispersas (NLP) | lr decae a cero | Problemas con embeddings dispersos (word2vec) |
| RMSProp | Resuelve el decaimiento de AdaGrad | Dos hiperparametros ($\eta$, $\gamma$) | RNNs, problemas no estacionarios |
| AdaDelta | No requiere lr | Convergencia mas lenta | Cuando no se quiere elegir lr (poco usado en la practica) |
| Adam | Convergencia rapida, robusto | Puede generalizar peor que SGD | Punto de partida por defecto para la mayoria de problemas |
| AdamW | Weight decay correcto | Ligeramente mas lento | Recomendado para redes modernas (Transformers, etc.) |
| NAdam | Lookahead de Nesterov en Adam | Marginal sobre Adam | Cuando NAG mejora sobre momentum en el mismo problema |
| RAdam | Estable en las primeras iteraciones | Complejo conceptualmente | Cuando se observa inestabilidad al inicio del entrenamiento |
Guia practica
- Punto de partida: usar AdamW con lr=0.001 y weight_decay=0.01
- Si AdamW no generaliza bien: probar SGD + Nesterov con lr schedule
- Para NLP con embeddings: considerar AdaGrad o Adam
- Para RNNs: RMSProp o Adam suelen funcionar mejor que SGD
- Para Transformers: AdamW es el estandar de facto
En el siguiente notebook veremos como combinar estos optimizadores con schedulers de learning rate para obtener lo mejor de ambos mundos.