🏭 Caso de Uso

Momentum: Acelerador del Descenso por Gradiente

Comparación práctica entre SGD sin momentum, momentum clásico y Nesterov Accelerated Gradient en MNIST.

🐍 Python 📓 Jupyter Notebook

Momentum: Acelerador del Descenso por Gradiente

Comparacion practica entre SGD sin momentum, momentum clasico y Nesterov Accelerated Gradient en MNIST


Por que el SGD estandar es lento

El descenso por gradiente estocastico (SGD) actualiza los pesos en cada paso siguiendo la direccion opuesta al gradiente:

$$\theta_{t+1} = \theta_t - \eta \nabla \mathcal{L}(\theta_t)$$

Este enfoque tiene dos problemas fundamentales:

  1. Oscilacion en valles estrechos: cuando la superficie de loss tiene curvatura muy diferente en distintas direcciones (mal condicionamiento), el gradiente apunta en la direccion de mayor pendiente, no hacia el minimo. Esto causa oscilaciones perpendiculares a la trayectoria optima.

  2. Estancamiento en regiones planas: en zonas con gradiente pequeno (mesetas, puntos de silla), los pasos son minusculos y el progreso se detiene.

La idea del momentum

El momentum se inspira en la fisica: una bola que rueda cuesta abajo acumula velocidad. Matematicamente, se introduce una variable de velocidad $v$ que acumula los gradientes pasados con un factor de decaimiento $\beta$ (tipicamente 0.9):

Momentum clasico (Polyak, 1964)

$$v_{t+1} = \beta , v_t + \eta \nabla \mathcal{L}(\theta_t)$$ $$\theta_{t+1} = \theta_t - v_{t+1}$$

  • Si los gradientes apuntan consistentemente en la misma direccion, $v$ crece y los pasos se hacen mas grandes (aceleracion).
  • Si los gradientes oscilan, las contribuciones positivas y negativas se cancelan parcialmente (amortiguacion).

En el caso limite donde todos los gradientes son iguales, la velocidad converge a $v = \frac{\eta}{1 - \beta} \nabla \mathcal{L}$, es decir, el paso efectivo es $\frac{1}{1-\beta}$ veces mayor que sin momentum. Con $\beta = 0.9$, esto es un factor de 10x.

Nesterov Accelerated Gradient (NAG, 1983)

La variante de Nesterov introduce una correccion sutil pero poderosa: en lugar de calcular el gradiente en la posicion actual $\theta_t$, lo calcula en la posicion anticipada $\theta_t - \beta v_t$ (donde nos llevaria el momentum por si solo):

$$v_{t+1} = \beta , v_t + \eta \nabla \mathcal{L}(\theta_t - \beta , v_t)$$ $$\theta_{t+1} = \theta_t - v_{t+1}$$

Esto permite que el gradiente "mire hacia delante" y corrija la trayectoria antes de sobrepasarse. En la practica, NAG converge mas rapido y oscila menos cerca del minimo.

Diseno del experimento

Componente Eleccion Justificacion
Dataset MNIST (60k train / 10k test) Referencia clasica, 28x28 grayscale, 10 digitos
Modelo MLP (784-256-128-10) Red simple para aislar el efecto del optimizador
Optimizadores SGD sin momentum, SGD + Momentum (0.9), SGD + Nesterov (0.9) Comparacion directa del impacto del momentum
Learning rate 0.01 (fijo para todos) Permite comparacion justa
Epocas 20 Suficientes para ver convergencia y diferencias
Batch size 64 Estandar para MNIST

Estructura del notebook

  1. Importaciones y configuracion
  2. Carga de MNIST
  3. Definicion del MLP
  4. Visualizacion intuitiva del momentum en 2D
  5. Entrenamiento con los 3 optimizadores
  6. Comparacion de curvas de loss y accuracy
  7. Efecto de distintos valores de $\beta$
  8. Analisis de las distribuciones de actualizaciones de pesos
  9. Conclusiones

1. Importaciones y configuracion

[1]
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 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

[2]
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")
print(f"Forma: {train_dataset[0][0].shape}")

# Visualizar muestras
fig, axes = plt.subplots(1, 10, figsize=(14, 1.8))
for i in range(10):
    axes[i].imshow(train_dataset[i][0].squeeze(), cmap='gray')
    axes[i].set_title(str(train_dataset[i][1]), fontsize=10)
    axes[i].axis('off')
plt.suptitle('Muestras de MNIST', fontweight='bold')
plt.tight_layout()
plt.show()
Train: 60,000 imagenes
Test:  10,000 imagenes
Forma: torch.Size([1, 28, 28])
Output

3. Definicion del MLP

Usamos una red fully-connected con dos capas ocultas. La misma arquitectura se usara para los tres optimizadores, con los mismos pesos iniciales para asegurar una comparacion justa.

[3]
class MLP(nn.Module):
    def __init__(self, hidden1=256, hidden2=128):
        super().__init__()
        self.net = nn.Sequential(
            nn.Flatten(),
            nn.Linear(784, hidden1),
            nn.ReLU(),
            nn.Linear(hidden1, hidden2),
            nn.ReLU(),
            nn.Linear(hidden2, 10),
        )

    def forward(self, x):
        return self.net(x)


# Crear modelo base y guardar sus pesos para reutilizar
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 -> 256 -> 128 -> 10")
print(f"Parametros:   {n_params:,}")
print(base_model)
Arquitectura: 784 -> 256 -> 128 -> 10
Parametros:   235,146
MLP(
  (net): Sequential(
    (0): Flatten(start_dim=1, end_dim=-1)
    (1): Linear(in_features=784, out_features=256, bias=True)
    (2): ReLU()
    (3): Linear(in_features=256, out_features=128, bias=True)
    (4): ReLU()
    (5): Linear(in_features=128, out_features=10, bias=True)
  )
)

4. Visualizacion intuitiva del momentum en 2D

Antes de entrenar, visualicemos como afecta el momentum a la trayectoria de optimizacion en una superficie de loss sintetica. Usamos la funcion de Beale, que tiene un valle estrecho con un minimo en $(3, 0.5)$:

$$f(x, y) = (1.5 - x + xy)^2 + (2.25 - x + xy^2)^2 + (2.625 - x + xy^3)^2$$

[4]
def beale(x, y):
    """Funcion de Beale: minimo en (3, 0.5)."""
    return ((1.5 - x + x*y)**2 + (2.25 - x + x*y**2)**2 +
            (2.625 - x + x*y**3)**2)

def beale_grad(x, y):
    """Gradiente de la funcion de Beale."""
    dx = (2*(1.5 - x + x*y)*(-1 + y) +
          2*(2.25 - x + x*y**2)*(-1 + y**2) +
          2*(2.625 - x + x*y**3)*(-1 + y**3))
    dy = (2*(1.5 - x + x*y)*(x) +
          2*(2.25 - x + x*y**2)*(2*x*y) +
          2*(2.625 - x + x*y**3)*(3*x*y**2))
    return np.array([dx, dy])

def optimize_2d(x0, y0, lr, beta, nesterov, n_steps):
    """Simular optimizacion en la funcion de Beale."""
    pos = np.array([x0, y0], dtype=np.float64)
    velocity = np.zeros(2)
    path = [pos.copy()]

    for _ in range(n_steps):
        if nesterov:
            look_ahead = pos - beta * velocity
            grad = beale_grad(look_ahead[0], look_ahead[1])
        else:
            grad = beale_grad(pos[0], pos[1])

        velocity = beta * velocity + lr * grad
        pos = pos - velocity
        path.append(pos.copy())

        # Evitar divergencia
        if np.any(np.abs(pos) > 10):
            break

    return np.array(path)

# Trayectorias desde el mismo punto inicial
x0, y0 = 0.5, 1.5
lr_2d = 0.0001
n_steps = 200

path_sgd = optimize_2d(x0, y0, lr_2d, beta=0.0, nesterov=False, n_steps=n_steps)
path_mom = optimize_2d(x0, y0, lr_2d, beta=0.9, nesterov=False, n_steps=n_steps)
path_nag = optimize_2d(x0, y0, lr_2d, beta=0.9, nesterov=True, n_steps=n_steps)

# Visualizar
xg = np.linspace(-1.5, 4.5, 300)
yg = np.linspace(-1.5, 2.5, 300)
X, Y = np.meshgrid(xg, yg)
Z = beale(X, Y)

fig, axes = plt.subplots(1, 3, figsize=(18, 5))
titles = ['SGD (sin momentum)', 'SGD + Momentum (0.9)', 'SGD + Nesterov (0.9)']
paths = [path_sgd, path_mom, path_nag]
colors = ['#e74c3c', '#0984E3', '#00B894']

for ax, title, path, color in zip(axes, titles, paths, colors):
    ax.contour(X, Y, Z, levels=np.logspace(0, 5, 25), cmap='gray', alpha=0.4)
    ax.plot(path[:, 0], path[:, 1], '-o', color=color, markersize=2,
            linewidth=1, alpha=0.8)
    ax.plot(path[0, 0], path[0, 1], 'ko', markersize=8, label='Inicio')
    ax.plot(3, 0.5, 'r*', markersize=15, label='Minimo (3, 0.5)')
    ax.set_title(f'{title}\n{len(path)} pasos', fontweight='bold', fontsize=10)
    ax.set_xlabel('x')
    ax.set_ylabel('y')
    ax.legend(fontsize=7, loc='upper left')
    ax.set_xlim(-1.5, 4.5)
    ax.set_ylim(-1.5, 2.5)

plt.suptitle('Trayectorias de optimizacion en la funcion de Beale',
             fontweight='bold', y=1.02)
plt.tight_layout()
plt.show()

# Distancia final al minimo
for name, path in zip(titles, paths):
    dist = np.sqrt((path[-1, 0] - 3)**2 + (path[-1, 1] - 0.5)**2)
    print(f"{name}: distancia al minimo = {dist:.4f}")
Output
SGD (sin momentum): distancia al minimo = 2.8166
SGD + Momentum (0.9): distancia al minimo = 2.0276
SGD + Nesterov (0.9): distancia al minimo = 2.0214

La visualizacion muestra como:

  • SGD oscila y avanza lentamente en el valle estrecho
  • Momentum acumula velocidad y se mueve mas rapido en la direccion consistente
  • Nesterov corrige el rumbo anticipandose al paso de momentum, reduciendo oscilaciones

5. Entrenamiento: comparacion de los 3 optimizadores

Entrenamos tres copias del mismo modelo (con pesos iniciales identicos) usando cada variante de SGD. Registramos loss y accuracy en train y test por epoca.

[5]
LR = 0.01
EPOCHS = 20

def train_and_evaluate(model, optimizer, train_loader, test_loader, epochs):
    """Entrena un modelo y devuelve historial de metricas."""
    criterion = nn.CrossEntropyLoss()
    history = {
        'train_loss': [], 'test_loss': [],
        'train_acc': [], 'test_acc': [],
        'grad_norms': [],
    }

    for epoch in range(epochs):
        # --- Entrenamiento ---
        model.train()
        total_loss = 0.0
        correct = 0
        total = 0
        epoch_grad_norms = []

        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()

            # Registrar norma del gradiente
            total_norm = 0.0
            for p in model.parameters():
                if p.grad is not None:
                    total_norm += p.grad.norm(2).item() ** 2
            epoch_grad_norms.append(total_norm ** 0.5)

            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)
        history['grad_norms'].append(np.mean(epoch_grad_norms))

        # --- Evaluacion en test ---
        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)

    return history


# Preparar los tres modelos con los mismos pesos iniciales
optimizers_config = {
    'SGD': {'momentum': 0, 'nesterov': False},
    'SGD + Momentum (0.9)': {'momentum': 0.9, 'nesterov': False},
    'SGD + Nesterov (0.9)': {'momentum': 0.9, 'nesterov': True},
}

results = {}
for name, opt_params in optimizers_config.items():
    print(f"\nEntrenando: {name}...")
    model = MLP().to(device)
    model.load_state_dict(copy.deepcopy(initial_weights))

    optimizer = optim.SGD(model.parameters(), lr=LR,
                          momentum=opt_params['momentum'],
                          nesterov=opt_params['nesterov'])
    history = train_and_evaluate(model, optimizer, train_loader, test_loader, EPOCHS)
    results[name] = history
    print(f"  Test Acc final: {history['test_acc'][-1]:.2f}%")

print("\nEntrenamiento completado.")
Entrenando: SGD...
  Test Acc final: 97.68%

Entrenando: SGD + Momentum (0.9)...
  Test Acc final: 98.50%

Entrenando: SGD + Nesterov (0.9)...
  Test Acc final: 98.45%

Entrenamiento completado.

6. Comparacion de curvas de loss y accuracy

[6]
colors_dict = {
    'SGD': '#e74c3c',
    'SGD + Momentum (0.9)': '#0984E3',
    'SGD + Nesterov (0.9)': '#00B894',
}

fig, axes = plt.subplots(2, 2, figsize=(14, 10))
epochs_range = range(1, EPOCHS + 1)

# Train Loss
ax = axes[0, 0]
for name, hist in results.items():
    ax.plot(epochs_range, hist['train_loss'], label=name, color=colors_dict[name], linewidth=2)
ax.set_title('Train Loss', fontweight='bold')
ax.set_xlabel('Epoca')
ax.set_ylabel('Cross-Entropy Loss')
ax.legend(fontsize=8)
ax.grid(True, alpha=0.3)

# Test Loss
ax = axes[0, 1]
for name, hist in results.items():
    ax.plot(epochs_range, hist['test_loss'], label=name, color=colors_dict[name], linewidth=2)
ax.set_title('Test Loss', fontweight='bold')
ax.set_xlabel('Epoca')
ax.set_ylabel('Cross-Entropy Loss')
ax.legend(fontsize=8)
ax.grid(True, alpha=0.3)

# Train Accuracy
ax = axes[1, 0]
for name, hist in results.items():
    ax.plot(epochs_range, hist['train_acc'], label=name, color=colors_dict[name], linewidth=2)
ax.set_title('Train Accuracy', fontweight='bold')
ax.set_xlabel('Epoca')
ax.set_ylabel('Accuracy (%)')
ax.legend(fontsize=8)
ax.grid(True, alpha=0.3)

# Test Accuracy
ax = axes[1, 1]
for name, hist in results.items():
    ax.plot(epochs_range, hist['test_acc'], label=name, color=colors_dict[name], linewidth=2)
ax.set_title('Test Accuracy', fontweight='bold')
ax.set_xlabel('Epoca')
ax.set_ylabel('Accuracy (%)')
ax.legend(fontsize=8)
ax.grid(True, alpha=0.3)

plt.suptitle('SGD vs Momentum vs Nesterov (MNIST, MLP, lr=0.01)',
             fontweight='bold', fontsize=13, y=1.02)
plt.tight_layout()
plt.show()

# Resumen numerico
print(f"\n{'Optimizador':<28} {'Test Acc (ep 5)':>15} {'Test Acc (ep 10)':>16} {'Test Acc (final)':>16}")
print("=" * 78)
for name, hist in results.items():
    print(f"{name:<28} {hist['test_acc'][4]:>14.2f}% {hist['test_acc'][9]:>15.2f}% {hist['test_acc'][-1]:>15.2f}%")
Output
Optimizador                  Test Acc (ep 5) Test Acc (ep 10) Test Acc (final)
==============================================================================
SGD                                   94.73%           96.55%           97.68%
SGD + Momentum (0.9)                  97.90%           98.17%           98.50%
SGD + Nesterov (0.9)                  97.89%           98.26%           98.45%
[7]
# Norma media del gradiente por epoca
fig, ax = plt.subplots(figsize=(10, 5))
for name, hist in results.items():
    ax.plot(epochs_range, hist['grad_norms'], label=name,
            color=colors_dict[name], linewidth=2)
ax.set_title('Norma media del gradiente por epoca', fontweight='bold')
ax.set_xlabel('Epoca')
ax.set_ylabel('Norma L2 del gradiente')
ax.legend(fontsize=9)
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
Output

7. Efecto del coeficiente de momentum ($\beta$)

El valor de $\beta$ controla cuanta "memoria" tiene el optimizador. Valores tipicos estan entre 0.8 y 0.99:

  • $\beta$ bajo (ej: 0.5): poca acumulacion, comportamiento cercano a SGD vanilla
  • $\beta$ medio (ej: 0.9): buena aceleracion, valor por defecto habitual
  • $\beta$ alto (ej: 0.99): mucha inercia, puede causar oscilaciones si lr es alto

Entrenemos con distintos valores de $\beta$ para observar el efecto.

[8]
betas = [0.0, 0.5, 0.8, 0.9, 0.95, 0.99]
beta_results = {}

for beta in betas:
    torch.manual_seed(SEED)
    model = MLP().to(device)
    model.load_state_dict(copy.deepcopy(initial_weights))
    optimizer = optim.SGD(model.parameters(), lr=LR, momentum=beta)
    history = train_and_evaluate(model, optimizer, train_loader, test_loader, EPOCHS)
    beta_results[beta] = history
    print(f"beta={beta:.2f} | Test Acc final: {history['test_acc'][-1]:.2f}%")

fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 5))
cmap = plt.cm.viridis(np.linspace(0, 1, len(betas)))

for i, beta in enumerate(betas):
    label = f'beta={beta}'
    ax1.plot(epochs_range, beta_results[beta]['test_loss'],
             label=label, color=cmap[i], linewidth=2)
    ax2.plot(epochs_range, beta_results[beta]['test_acc'],
             label=label, color=cmap[i], linewidth=2)

ax1.set_title('Test Loss para distintos valores de beta', fontweight='bold')
ax1.set_xlabel('Epoca')
ax1.set_ylabel('Cross-Entropy Loss')
ax1.legend(fontsize=8)
ax1.grid(True, alpha=0.3)

ax2.set_title('Test Accuracy para distintos valores de beta', fontweight='bold')
ax2.set_xlabel('Epoca')
ax2.set_ylabel('Accuracy (%)')
ax2.legend(fontsize=8)
ax2.grid(True, alpha=0.3)

plt.suptitle('Efecto de beta (coeficiente de momentum)', fontweight='bold', y=1.02)
plt.tight_layout()
plt.show()

# Tabla resumen
print(f"\n{'beta':>6} {'Test Acc ep 5':>14} {'Test Acc ep 10':>15} {'Test Acc final':>15}")
print("=" * 53)
for beta in betas:
    h = beta_results[beta]
    print(f"{beta:>6.2f} {h['test_acc'][4]:>13.2f}% {h['test_acc'][9]:>14.2f}% {h['test_acc'][-1]:>14.2f}%")
beta=0.00 | Test Acc final: 97.53%
beta=0.50 | Test Acc final: 98.01%
beta=0.80 | Test Acc final: 98.17%
beta=0.90 | Test Acc final: 98.44%
beta=0.95 | Test Acc final: 98.42%
beta=0.99 | Test Acc final: 10.09%
Output
  beta  Test Acc ep 5  Test Acc ep 10  Test Acc final
=====================================================
  0.00         94.76%          96.64%          97.53%
  0.50         96.63%          97.57%          98.01%
  0.80         97.86%          97.78%          98.17%
  0.90         97.93%          98.23%          98.44%
  0.95         97.67%          98.10%          98.42%
  0.99         92.58%          12.42%          10.09%

8. Analisis de las actualizaciones de pesos

Para entender fisicamente que hace el momentum, podemos analizar las actualizaciones efectivas de los pesos (la diferencia $\theta_{t+1} - \theta_t$) a lo largo del entrenamiento. Con momentum, estas actualizaciones deberian ser mas suaves (menos ruidosas) que con SGD vanilla.

[9]
def train_with_updates(model, optimizer, train_loader, n_batches=200):
    """Entrena y registra las actualizaciones de pesos de la primera capa."""
    criterion = nn.CrossEntropyLoss()
    updates = []
    model.train()

    batch_count = 0
    for images, labels in train_loader:
        if batch_count >= n_batches:
            break
        images, labels = images.to(device), labels.to(device)

        # Pesos antes
        w_before = model.net[1].weight.data.clone()

        optimizer.zero_grad()
        outputs = model(images)
        loss = criterion(outputs, labels)
        loss.backward()
        optimizer.step()

        # Pesos despues
        w_after = model.net[1].weight.data.clone()
        delta = (w_after - w_before).flatten().cpu().numpy()
        updates.append(delta)
        batch_count += 1

    return updates


update_records = {}
for name, opt_params in [('SGD', {'momentum': 0}),
                         ('Momentum (0.9)', {'momentum': 0.9}),
                         ('Nesterov (0.9)', {'momentum': 0.9, 'nesterov': True})]:
    torch.manual_seed(SEED)
    model = MLP().to(device)
    model.load_state_dict(copy.deepcopy(initial_weights))
    nesterov = opt_params.get('nesterov', False)
    optimizer = optim.SGD(model.parameters(), lr=LR,
                          momentum=opt_params['momentum'],
                          nesterov=nesterov)
    update_records[name] = train_with_updates(model, optimizer, train_loader, n_batches=200)
    print(f"{name}: {len(update_records[name])} batches registrados")

# Visualizar la norma de las actualizaciones por batch
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 5))

colors_up = ['#e74c3c', '#0984E3', '#00B894']
for i, (name, updates) in enumerate(update_records.items()):
    norms = [np.linalg.norm(u) for u in updates]
    ax1.plot(norms, color=colors_up[i], alpha=0.7, linewidth=1, label=name)

ax1.set_title('Norma de las actualizaciones de pesos (primera capa)', fontweight='bold')
ax1.set_xlabel('Batch')
ax1.set_ylabel('||delta_w||')
ax1.legend(fontsize=9)
ax1.grid(True, alpha=0.3)

# Histograma de actualizaciones individuales (elemento a elemento)
for i, (name, updates) in enumerate(update_records.items()):
    all_deltas = np.concatenate(updates[-50:])  # Ultimos 50 batches
    ax2.hist(all_deltas, bins=80, alpha=0.5, color=colors_up[i], label=name, density=True)

ax2.set_title('Distribucion de actualizaciones (ultimos 50 batches)', fontweight='bold')
ax2.set_xlabel('delta_w')
ax2.set_ylabel('Densidad')
ax2.legend(fontsize=9)
ax2.set_xlim(-0.005, 0.005)
ax2.grid(True, alpha=0.3)

plt.tight_layout()
plt.show()
SGD: 200 batches registrados
Momentum (0.9): 200 batches registrados
Nesterov (0.9): 200 batches registrados
Output

Las actualizaciones con momentum tienen mayor norma (pasos mas grandes en la direccion dominante) pero distribucion mas concentrada (menos ruido). Esto es exactamente el efecto de aceleracion-amortiguacion que describimos en la introduccion.

9. Conclusiones

Aspecto SGD Momentum clasico Nesterov
Formula $\theta_{t+1} = \theta_t - \eta \nabla \mathcal{L}$ Acumula gradientes pasados con factor $\beta$ Evalua gradiente en posicion anticipada
Convergencia Lenta Acelerada (~10x con $\beta=0.9$) Ligeramente mas rapida que momentum clasico
Oscilaciones Muchas en valles estrechos Menos: componentes opuestas se cancelan Menos aun: correccion anticipada
Parametro clave Solo $\eta$ $\eta$ y $\beta$ $\eta$ y $\beta$
Recomendacion Rara vez se usa solo Buena opcion general Preferido sobre momentum clasico
En PyTorch SGD(lr=0.01) SGD(lr=0.01, momentum=0.9) SGD(lr=0.01, momentum=0.9, nesterov=True)

Tanto el momentum clasico como Nesterov son mejoras significativas sobre SGD vanilla, y el coste computacional adicional es insignificante (una operacion vectorial extra por paso). En la practica, los optimizadores adaptativos como Adam combinan la idea del momentum con tasas de aprendizaje adaptativas por parametro, lo cual veremos en el siguiente notebook.