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
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.
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.
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.
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.
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