Phase 03 - Lesson 09

Programaciones de Tasa de Aprendizaje y Calentamiento

This lesson includes a graded coding exercise that runs in your browser, unlocked with lifetime access.

La tasa de aprendizaje es el hiperparámetro más importante de todos. No es la arquitectura. No es el tamaño del dataset. No es la función de activación. Es la tasa de aprendizaje. Si no ajustas nada más, ajusta esto.

Tipo: Build Lenguajes: Python Prerrequisitos: Lección 03.06 (Optimizadores), Lección 03.08 (Inicialización de Pesos) Tiempo: ~90 minutos

Objetivos de Aprendizaje

  • Implementar desde cero las programaciones de tasa de aprendizaje constante, step decay, cosine annealing, calentamiento + coseno y 1cycle
  • Demostrar los tres modos de fallo en la elección de la tasa de aprendizaje: divergencia (demasiado alta), estancamiento (demasiado baja) y oscilación (sin decaimiento)
  • Explicar por qué el calentamiento es necesario para optimizadores basados en Adam y cómo estabiliza el inicio del entrenamiento
  • Comparar la velocidad de convergencia entre las cinco programaciones en la misma tarea y elegir la más adecuada para un presupuesto de entrenamiento dado

El Problema

Fija la tasa de aprendizaje en 0.1. El entrenamiento diverge: la loss salta al infinito en 3 pasos. Fíjala en 0.0001. El entrenamiento se arrastra: después de 100 épocas, el modelo apenas se movió del estado aleatorio. Fíjala en 0.01. El entrenamiento funciona por 50 épocas, y luego la loss oscila alrededor de un mínimo que nunca puede alcanzar, porque los pasos son demasiado grandes.

La tasa de aprendizaje óptima no es una constante. Cambia durante el entrenamiento. Al principio, quieres pasos grandes para avanzar rápidamente. Al final del entrenamiento, quieres pasos minúsculos para asentarte en un mínimo agudo. La diferencia entre un modelo con 90% de exactitud y uno con 95% de exactitud suele estar justamente en la programación.

Todo modelo importante publicado en los últimos tres años usa una programación de tasa de aprendizaje. Llama 3 usó lr pico=3e-4 con 2000 pasos de calentamiento y decaimiento coseno hasta 3e-5. GPT-3 usó lr=6e-4 con calentamiento a lo largo de 375 millones de tokens. Estas no son elecciones arbitrarias. Son el resultado de barridos extensivos de hiperparámetros que costaron millones de dólares.

Necesitas entender las programaciones porque los valores por defecto no funcionarán para tu problema. Cuando haces fine-tuning de un modelo preentrenado, la programación correcta es distinta a la del entrenamiento desde cero. Cuando aumentas el tamaño del batch, el período de calentamiento debe cambiar. Cuando el entrenamiento se rompe en el paso 10.000, necesitas saber si es un problema de programación o algo más.

El Concepto

Tasa de Aprendizaje Constante

El enfoque más simple. Elige un número y úsalo en cada paso.

lr(t) = lr_0

Rara vez es óptima. O es demasiado alta para el final del entrenamiento (oscilación alrededor del mínimo) o demasiado baja para el comienzo (cómputo desperdiciado en pasos minúsculos). Funciona bien para modelos pequeños y para depuración. Una elección terrible para cualquier cosa que entrene por más de una hora.

Step Decay

El enfoque de la vieja escuela, de la era de ResNet. Corta la tasa de aprendizaje por un factor (generalmente 10x) en épocas fijas.

lr(t) = lr_0 * gamma^(floor(epoch / step_size))

Donde gamma = 0.1 y step_size = 30 significa: la lr cae por 10x cada 30 épocas. ResNet-50 usó esto: lr=0.1, con caída de 10x en las épocas 30, 60 y 90.

El problema: los puntos de decaimiento óptimos dependen del dataset y de la arquitectura. Cambia a un problema distinto y necesitas reajustar cuándo cortar. Las transiciones son abruptas: la loss puede dispararse cuando la tasa cambia de repente.

Cosine Annealing

Decaimiento suave de la tasa de aprendizaje máxima hasta la mínima, siguiendo una curva coseno:

lr(t) = lr_min + 0.5 * (lr_max - lr_min) * (1 + cos(pi * t / T))

Donde t es el paso actual y T es el número total de pasos.

En t=0, el término coseno vale 1, así que lr = lr_max. En t=T, el término coseno vale -1, así que lr = lr_min. El decaimiento es suave al principio, acelera en el medio y vuelve a ser suave cerca del final.

Este es el valor por defecto para la mayoría de los entrenamientos modernos. Ningún hiperparámetro que ajustar más allá de lr_max y lr_min. La forma del coseno coincide con la observación empírica de que la mayor parte del aprendizaje ocurre en el medio del entrenamiento: quieres pasos de tamaño razonable durante ese período crítico.

Calentamiento: Por Qué Empiezas Pequeño

Adam y otros optimizadores adaptativos mantienen estimaciones corrientes de la media y la varianza del gradiente. En el paso 0, estas estimaciones se inicializan en cero. Las primeras actualizaciones de gradiente se basan en estadísticas basura. Si tu tasa de aprendizaje es grande durante ese período, el modelo da pasos enormes y mal dirigidos.

El calentamiento corrige esto. Empieza con una tasa de aprendizaje minúscula (a menudo lr_max / warmup_steps o incluso cero) y auméntala linealmente hasta lr_max a lo largo de los primeros N pasos. Para cuando alcanzas la tasa de aprendizaje completa, las estadísticas de Adam ya se estabilizaron.

lr(t) = lr_max * (t / warmup_steps)     for t < warmup_steps

Calentamiento típico: 1-5% del total de pasos de entrenamiento. Llama 3 entrenó por ~1,8 billones de tokens y calentó por 2000 pasos. GPT-3 calentó a lo largo de 375 millones de tokens.

Calentamiento Lineal + Decaimiento Coseno

El valor por defecto moderno. Aumenta linealmente y luego decae con coseno:

if t < warmup_steps:
    lr(t) = lr_max * (t / warmup_steps)
else:
    progress = (t - warmup_steps) / (total_steps - warmup_steps)
    lr(t) = lr_min + 0.5 * (lr_max - lr_min) * (1 + cos(pi * progress))

Esto es lo que usan Llama, GPT, PaLM y la mayoría de los transformers modernos. El calentamiento previene la inestabilidad inicial. El decaimiento coseno asienta el modelo en un buen mínimo.

Política 1cycle

El descubrimiento de Leslie Smith (2018): aumenta la tasa de aprendizaje de un valor bajo a un valor alto en la primera mitad del entrenamiento, y luego redúcela de nuevo en la segunda mitad. Contraintuitivo: ¿por qué aumentarías la tasa de aprendizaje a mitad de camino?

La teoría: una tasa de aprendizaje alta actúa como regularización al agregar ruido a la trayectoria de optimización. El modelo explora más del panorama de la loss durante la fase de subida, encontrando mejores cuencas. La fase de bajada luego refina dentro de la mejor cuenca encontrada.

Phase 1 (0 to T/2):    lr ramps from lr_max/25 to lr_max
Phase 2 (T/2 to T):    lr ramps from lr_max to lr_max/10000

La 1cycle a menudo entrena más rápido que el cosine annealing para un presupuesto de cómputo fijo. El trade-off: debes conocer el número total de pasos de antemano.

Formas de las Programaciones

graph LR
    subgraph "Constante"
        C1["lr"] --- C2["lr"] --- C3["lr"]
    end

    subgraph "Step Decay"
        S1["0.1"] --- S2["0.1"] --- S3["0.01"] --- S4["0.001"]
    end

    subgraph "Cosine Annealing"
        CS1["lr_max"] --> CS2["gradual"] --> CS3["pronunciado"] --> CS4["lr_min"]
    end

    subgraph "Calentamiento + Coseno"
        WC1["0"] --> WC2["lr_max"] --> WC3["coseno"] --> WC4["lr_min"]
    end

Diagrama de Decisión

flowchart TD
    Start["Eligiendo una programación de LR"] --> Know{"¿Conoces el total<br/>de pasos de entrenamiento?"}

    Know -->|"Sí"| Budget{"¿Presupuesto de cómputo?"}
    Know -->|"No"| Constant["Usa LR constante<br/>con decaimiento manual"]

    Budget -->|"Grande (días/semanas)"| WarmCos["Calentamiento + Decaimiento Coseno<br/>(por defecto Llama/GPT)"]
    Budget -->|"Pequeño (horas)"| OneCycle["Política 1cycle<br/>(convergencia más rápida)"]
    Budget -->|"Moderado"| Cosine["Cosine Annealing<br/>(valor por defecto seguro)"]

    WarmCos --> Warmup["Calentamiento = 1-5% de los pasos"]
    OneCycle --> FindLR["Encuentra lr_max con prueba de rango de LR"]
    Cosine --> MinLR["Fija lr_min = lr_max / 10"]

Números Reales de Modelos Publicados

graph TD
    subgraph "Configs de LR Publicadas"
        L3["Llama 3 (405B)<br/>Pico: 3e-4<br/>Calentamiento: 2000 pasos<br/>Programación: Coseno hasta 3e-5"]
        G3["GPT-3 (175B)<br/>Pico: 6e-4<br/>Calentamiento: 375M tokens<br/>Programación: Coseno hasta 0"]
        R50["ResNet-50<br/>Pico: 0.1<br/>Calentamiento: ninguno<br/>Programación: Step decay x0.1 en 30,60,90"]
        B["BERT (340M)<br/>Pico: 1e-4<br/>Calentamiento: 10K pasos<br/>Programación: Decaimiento lineal"]
    end

Constrúyelo

Paso 1: Funciones de Programación

Cada función recibe el paso actual y devuelve la tasa de aprendizaje en ese paso.

import math


def constant_schedule(step, lr=0.01, **kwargs):
    return lr


def step_decay_schedule(step, lr=0.1, step_size=100, gamma=0.1, **kwargs):
    return lr * (gamma ** (step // step_size))


def cosine_schedule(step, lr=0.01, total_steps=1000, lr_min=1e-5, **kwargs):
    if step >= total_steps:
        return lr_min
    return lr_min + 0.5 * (lr - lr_min) * (1 + math.cos(math.pi * step / total_steps))


def warmup_cosine_schedule(step, lr=0.01, total_steps=1000, warmup_steps=100, lr_min=1e-5, **kwargs):
    if total_steps <= warmup_steps:
        return lr * (step / max(warmup_steps, 1))
    if step < warmup_steps:
        return lr * step / warmup_steps
    progress = (step - warmup_steps) / (total_steps - warmup_steps)
    return lr_min + 0.5 * (lr - lr_min) * (1 + math.cos(math.pi * progress))


def one_cycle_schedule(step, lr=0.01, total_steps=1000, **kwargs):
    mid = max(total_steps // 2, 1)
    if step < mid:
        return (lr / 25) + (lr - lr / 25) * step / mid
    else:
        progress = (step - mid) / max(total_steps - mid, 1)
        return lr * (1 - progress) + (lr / 10000) * progress

Paso 2: Visualiza Todas las Programaciones

Imprime un gráfico basado en texto que muestre cómo evoluciona cada programación a lo largo del entrenamiento.

def visualize_schedule(name, schedule_fn, total_steps=500, **kwargs):
    steps = list(range(0, total_steps, total_steps // 20))
    if total_steps - 1 not in steps:
        steps.append(total_steps - 1)

    lrs = [schedule_fn(s, total_steps=total_steps, **kwargs) for s in steps]
    max_lr = max(lrs) if max(lrs) > 0 else 1.0

    print(f"\n{name}:")
    for s, lr_val in zip(steps, lrs):
        bar_len = int(lr_val / max_lr * 40)
        bar = "#" * bar_len
        print(f"  Step {s:4d}: lr={lr_val:.6f} {bar}")

Paso 3: Red de Entrenamiento

Una red simple de dos capas en el dataset del círculo, igual a las lecciones anteriores, pero ahora variamos la programación.

import random


def sigmoid(x):
    x = max(-500, min(500, x))
    return 1.0 / (1.0 + math.exp(-x))


def relu(x):
    return max(0.0, x)


def relu_deriv(x):
    return 1.0 if x > 0 else 0.0


def make_circle_data(n=200, seed=42):
    random.seed(seed)
    data = []
    for _ in range(n):
        x = random.uniform(-2, 2)
        y = random.uniform(-2, 2)
        label = 1.0 if x * x + y * y < 1.5 else 0.0
        data.append(([x, y], label))
    return data


def train_with_schedule(schedule_fn, schedule_name, data, epochs=300, base_lr=0.05, **kwargs):
    random.seed(0)
    hidden_size = 8
    total_steps = epochs * len(data)

    std = math.sqrt(2.0 / 2)
    w1 = [[random.gauss(0, std) for _ in range(2)] for _ in range(hidden_size)]
    b1 = [0.0] * hidden_size
    w2 = [random.gauss(0, std) for _ in range(hidden_size)]
    b2 = 0.0

    step = 0
    epoch_losses = []

    for epoch in range(epochs):
        total_loss = 0
        correct = 0

        for x, target in data:
            lr = schedule_fn(step, lr=base_lr, total_steps=total_steps, **kwargs)

            z1 = []
            h = []
            for i in range(hidden_size):
                z = w1[i][0] * x[0] + w1[i][1] * x[1] + b1[i]
                z1.append(z)
                h.append(relu(z))

            z2 = sum(w2[i] * h[i] for i in range(hidden_size)) + b2
            out = sigmoid(z2)

            error = out - target
            d_out = error * out * (1 - out)

            for i in range(hidden_size):
                d_h = d_out * w2[i] * relu_deriv(z1[i])
                w2[i] -= lr * d_out * h[i]
                for j in range(2):
                    w1[i][j] -= lr * d_h * x[j]
                b1[i] -= lr * d_h
            b2 -= lr * d_out

            total_loss += (out - target) ** 2
            if (out >= 0.5) == (target >= 0.5):
                correct += 1
            step += 1

        avg_loss = total_loss / len(data)
        accuracy = correct / len(data) * 100
        epoch_losses.append(avg_loss)

    return epoch_losses

Paso 4: Compara Todas las Programaciones

Entrena la misma red con cada programación y compara la loss final y el comportamiento de convergencia.

def compare_schedules(data):
    configs = [
        ("Constant", constant_schedule, {}),
        ("Step Decay", step_decay_schedule, {"step_size": 15000, "gamma": 0.1}),
        ("Cosine", cosine_schedule, {"lr_min": 1e-5}),
        ("Warmup+Cosine", warmup_cosine_schedule, {"warmup_steps": 3000, "lr_min": 1e-5}),
        ("1cycle", one_cycle_schedule, {}),
    ]

    print(f"\n{'Schedule':<20} {'Start Loss':>12} {'Mid Loss':>12} {'End Loss':>12} {'Best Loss':>12}")
    print("-" * 70)

    for name, schedule_fn, extra_kwargs in configs:
        losses = train_with_schedule(schedule_fn, name, data, epochs=300, base_lr=0.05, **extra_kwargs)
        mid_idx = len(losses) // 2
        best = min(losses)
        print(f"{name:<20} {losses[0]:>12.6f} {losses[mid_idx]:>12.6f} {losses[-1]:>12.6f} {best:>12.6f}")

Paso 5: LR Demasiado Alta vs Demasiado Baja

Demuestra los tres modos de fallo: demasiado alta (divergencia), demasiado baja (arrastrándose) y en su justa medida.

def lr_sensitivity(data):
    learning_rates = [1.0, 0.1, 0.01, 0.001, 0.0001]

    print("\nLR Sensitivity (constant schedule, 100 epochs):")
    print(f"  {'LR':>10} {'Start Loss':>12} {'End Loss':>12} {'Status':>15}")
    print("  " + "-" * 52)

    for lr in learning_rates:
        losses = train_with_schedule(constant_schedule, f"lr={lr}", data, epochs=100, base_lr=lr)
        start = losses[0]
        end = losses[-1]

        if end > start or math.isnan(end) or end > 1.0:
            status = "DIVERGED"
        elif end > start * 0.9:
            status = "BARELY MOVED"
        elif end < 0.15:
            status = "CONVERGED"
        else:
            status = "LEARNING"

        end_str = f"{end:.6f}" if not math.isnan(end) else "NaN"
        print(f"  {lr:>10.4f} {start:>12.6f} {end_str:>12} {status:>15}")

Úsalo

PyTorch proporciona programadores en torch.optim.lr_scheduler:

import torch
import torch.optim as optim
from torch.optim.lr_scheduler import CosineAnnealingLR, OneCycleLR, StepLR

model = nn.Sequential(nn.Linear(10, 64), nn.ReLU(), nn.Linear(64, 1))
optimizer = optim.Adam(model.parameters(), lr=3e-4)

scheduler = CosineAnnealingLR(optimizer, T_max=1000, eta_min=1e-5)

for step in range(1000):
    loss = train_step(model, optimizer)
    scheduler.step()

Para calentamiento + coseno, usa un programador lambda o el get_cosine_schedule_with_warmup de HuggingFace:

from transformers import get_cosine_schedule_with_warmup

scheduler = get_cosine_schedule_with_warmup(
    optimizer,
    num_warmup_steps=2000,
    num_training_steps=100000,
)

La función de HuggingFace es lo que usa la mayoría de los scripts de fine-tuning de Llama y GPT. En caso de duda, usa calentamiento + coseno con calentamiento = 3-5% del total de pasos. Funciona para casi todo.

Entrégalo

Esta lección produce:

  • outputs/prompt-lr-schedule-advisor.md -- un prompt que recomienda la programación de tasa de aprendizaje correcta y los hiperparámetros para tu configuración de entrenamiento

Ejercicios

  1. Implementa el decaimiento exponencial: lr(t) = lr_0 * gamma^t donde gamma = 0.999. Compara con el cosine annealing en el dataset del círculo.

  2. Implementa la prueba de rango de tasa de aprendizaje (Leslie Smith): entrena por algunos cientos de pasos mientras aumentas exponencialmente la LR de 1e-7 a 1. Grafica la loss vs LR. La LR máxima óptima está justo antes de que la loss empiece a aumentar.

  3. Entrena con calentamiento + coseno pero varía la duración del calentamiento: 0%, 1%, 5%, 10%, 20% del total de pasos. Encuentra el punto ideal donde el entrenamiento es más estable.

  4. Implementa el cosine annealing con warm restarts (SGDR): reinicia la tasa de aprendizaje a lr_max cada T pasos y decae de nuevo. Compara con el coseno estándar en una ejecución de entrenamiento más larga.

  5. Construye un "cirujano de programación" que monitoree la loss de entrenamiento y cambie automáticamente de calentamiento a coseno cuando la loss se estabilice, y reduzca la lr si la loss se estanca por demasiado tiempo.

Términos Clave

Término Lo que dice la gente Lo que realmente significa
Tasa de aprendizaje "Qué tan rápido aprende el modelo" El escalar que multiplica el gradiente para determinar el tamaño de la actualización del parámetro
Programación "Cambiar la LR a lo largo del tiempo" Una función que mapea el paso de entrenamiento a la tasa de aprendizaje, diseñada para optimizar la convergencia
Calentamiento "Empieza con una LR pequeña" Aumentar linealmente la LR de casi-cero hasta el valor objetivo a lo largo de los primeros N pasos para estabilizar las estadísticas del optimizador
Cosine annealing "Decaimiento suave de la LR" Disminuir la LR siguiendo una curva coseno de lr_max a lr_min a lo largo del entrenamiento
Step decay "Bajar la LR en hitos" Multiplicar la LR por un factor (generalmente 0.1) en intervalos fijos de épocas
Política 1cycle "Subir y luego bajar" El método de Leslie Smith de subir y luego bajar la LR en un solo ciclo para una convergencia más rápida
Prueba de rango de LR "Encontrar la mejor tasa de aprendizaje" Entrenar brevemente mientras se aumenta la LR para encontrar el valor donde la loss empieza a divergir
Coseno con warm restarts "Reiniciar y repetir" Reiniciar periódicamente la LR a lr_max y decaer de nuevo (SGDR)
Eta min "El piso de la LR" La tasa de aprendizaje mínima hasta la cual decae la programación
Tasa de aprendizaje pico "La LR máxima" La LR más alta alcanzada durante el entrenamiento, típicamente después del calentamiento

Lectura Adicional

  • Loshchilov & Hutter, "SGDR: Stochastic Gradient Descent with Warm Restarts" (2017) -- introdujo el cosine annealing y los warm restarts
  • Smith, "Super-Convergence: Very Fast Training of Neural Networks Using Large Learning Rates" (2018) -- el artículo de la política 1cycle
  • Touvron et al., "Llama 2: Open Foundation and Fine-Tuned Chat Models" (2023) -- documenta la programación de calentamiento + coseno usada a escala
  • Goyal et al., "Accurate, Large Minibatch SGD: Training ImageNet in 1 Hour" (2017) -- la regla de escalado lineal y el calentamiento para entrenamiento con batch grande
0 lifetime access. Curriculum based on AI Engineering from Scratch by Rohit Ghumare (MIT, used under attribution).