Módulo 5 · Redes neuronales

Lección 4 de 7

Loop de entrenamiento

En esta páginaEl problema

El problema

Tienes 200 puntos generados con make_moons(n_samples=200, noise=0.2, random_state=0). Cada punto tiene dos características y una clase, 0 o 1. Estas son las primeras cuatro filas:

puntox1x2clase
P-10.740.460
P-21.66−0.631
P-3−0.160.261
P-4−1.09−0.400

Separas 150 puntos para entrenamiento y 50 para validación. El modelo es un MLP 2 → 16 → 2 con ReLU, y la pérdida es CrossEntropyLoss. Con los pesos iniciales, los logits no tienen relación con las clases.

En Tensores y autograd calculaste un gradiente y cambiaste un peso una vez. Ahora ese cálculo se repite cientos de veces, sobre grupos de puntos, para todos los parámetros del MLP. Necesitas un loop que lo haga y una forma de comprobar que el modelo aprende, tanto en los 150 puntos de entrenamiento como en los 50 que nunca usa para actualizar pesos.

Batches con DataLoader

Un batch es un grupo de ejemplos que el modelo procesa junto. TensorDataset empareja tensores fila por fila: el elemento i es la tupla (X[i], y[i]). DataLoader recorre ese dataset y entrega un batch en cada iteración. Un recorrido completo por el dataset es una época.

Con shuffle=True, el DataLoader baraja los ejemplos. El argumento generator recibe un generador de números aleatorios con semilla fija.

La celda plegada define recorrer, que recorre el cargador una vez, es decir, una época, y devuelve cada batch como una lista de números.

Mostrar códigoOcultar códigoPython
Python 3 · ejemplo reproducible
def recorrer(cargador):
    return [batch[0].tolist() for batch in cargador]

Pregunta. Un dataset de 10 números se recorre con batch_size=4. ¿Cuántos batches hay por época y de qué tamaño es el último? ¿La segunda época repite el orden de la primera? ¿Un segundo DataLoader con la misma semilla repite el orden del primero?

Python 3 · ejemplo reproducible
import torch
from torch.utils.data import DataLoader, TensorDataset

numeros = TensorDataset(torch.arange(10))

cargador_a = DataLoader(
    numeros, batch_size=4, shuffle=True, generator=torch.Generator().manual_seed(0)
)
epoca_1 = recorrer(cargador_a)
epoca_2 = recorrer(cargador_a)
print(epoca_1)
print(epoca_2)

cargador_b = DataLoader(
    numeros, batch_size=4, shuffle=True, generator=torch.Generator().manual_seed(0)
)
print(recorrer(cargador_b) == epoca_1)
Salida esperada

La salida esperada está oculta.

Ahora el dataset real. La celda plegada genera los 200 puntos con make_moons y los separa con train_test_split: 50 para validación y el resto para entrenamiento, con la misma proporción de clases en las dos partes gracias a stratify=y.

Mostrar códigoOcultar códigoPython
Python 3 · ejemplo reproducible
from sklearn.datasets import make_moons
from sklearn.model_selection import train_test_split

X, y = make_moons(n_samples=200, noise=0.2, random_state=0)
X_ent, X_val, y_ent, y_val = train_test_split(
    X, y, test_size=50, random_state=0, stratify=y
)

X_ent y X_val son arreglos de NumPy con dos coordenadas por punto. y_ent y y_val guardan la clase de cada punto, 0 o 1. X_ent tiene forma (150, 2) y X_val, forma (50, 2). La media de y_ent y la de y_val valen 0.5: la mitad de los puntos de cada parte es de clase 1.

torch.tensor convierte los arreglos en tensores. Las características pasan a float32 con dtype=torch.float32, el tipo de los pesos de nn.Linear. Las etiquetas quedan como enteros, el tipo que pide CrossEntropyLoss. crear_cargador devuelve un DataLoader nuevo con su propio generador semillado, así que cada llamada recorre los batches en el mismo orden.

Python 3 · ejemplo reproducible
X_entrenamiento = torch.tensor(X_ent, dtype=torch.float32)
y_entrenamiento = torch.tensor(y_ent)
X_validacion = torch.tensor(X_val, dtype=torch.float32)
y_validacion = torch.tensor(y_val)

datos_entrenamiento = TensorDataset(X_entrenamiento, y_entrenamiento)


def crear_cargador(batch_size=32, semilla=0):
    generador = torch.Generator().manual_seed(semilla)
    return DataLoader(
        datos_entrenamiento, batch_size=batch_size, shuffle=True, generator=generador
    )

next(iter(cargador)) toma el primer batch sin recorrer la época completa.

Pregunta. Con 150 ejemplos y batch_size=32, escribe el número de batches por época, el tamaño de cada uno y la forma y el tipo de xb y yb en el primer batch.

Python 3 · ejemplo reproducible
cargador = crear_cargador()
xb, yb = next(iter(cargador))

print(len(datos_entrenamiento), len(cargador))
print(xb.shape, xb.dtype)
print(yb.shape, yb.dtype)
print([len(yb) for _, yb in cargador])
Salida esperada

La salida esperada está oculta.

Pasos del loop

Un optimizador recibe los parámetros del modelo con modelo.parameters() y sabe cómo cambiarlos. Para cada batch, el loop repite cinco pasos:

  1. optimizador.zero_grad() borra los gradientes guardados
  2. modelo(xb) calcula los logits (forward)
  3. perdida_fn(logits, yb) calcula la pérdida del batch
  4. perdida.backward() suma a .grad el gradiente de cada parámetro
  5. optimizador.step() cambia cada parámetro con su gradiente

torch.optim.SGD recibe además un número lr, el learning rate, que fija cuánto pesa el gradiente en cada cambio. crear_modelo fija la semilla antes de crear las capas, así que cada llamada devuelve los mismos pesos iniciales.

Python 3 · ejemplo reproducible
from torch import nn


def crear_modelo():
    torch.manual_seed(0)
    return nn.Sequential(nn.Linear(2, 16), nn.ReLU(), nn.Linear(16, 2))


perdida_fn = nn.CrossEntropyLoss()
modelo = crear_modelo()
optimizador = torch.optim.SGD(modelo.parameters(), lr=0.1)
xb, yb = next(iter(crear_cargador()))

La celda siguiente aplica los cinco pasos a ese batch. Antes de step() guarda una copia del peso de la primera capa con .detach().clone() y una del gradiente con .clone(). Después recalcula la pérdida del mismo batch dentro de torch.no_grad().

Pregunta. Después de un solo step(), ¿la pérdida del mismo batch sube o baja? ¿El peso nuevo de la primera capa es igual a peso_antes − 0.1 · gradiente?

Python 3 · ejemplo reproducible
optimizador.zero_grad()
logits = modelo(xb)
perdida = perdida_fn(logits, yb)
perdida.backward()

peso_antes = modelo[0].weight.detach().clone()  # copia que step() no modifica
gradiente = modelo[0].weight.grad.clone()
optimizador.step()

with torch.no_grad():
    perdida_despues = perdida_fn(modelo(xb), yb)

print(round(perdida.item(), 4), round(perdida_despues.item(), 4))
print(torch.allclose(modelo[0].weight, peso_antes - 0.1 * gradiente))
Salida esperada

La salida esperada está oculta.

Una época repite esos pasos para cada batch. La función entrenar_epoca devuelve la pérdida media de la época. Cada pérdida de batch se multiplica por el tamaño del batch, así el batch de 22 ejemplos pesa menos que uno de 32. perdida.item() extrae un número de Python y no conserva el registro de operaciones.

Python 3 · ejemplo reproducible
def entrenar_epoca(modelo, cargador, optimizador):
    suma = 0.0
    for xb, yb in cargador:
        optimizador.zero_grad()
        perdida = perdida_fn(modelo(xb), yb)
        perdida.backward()
        optimizador.step()
        suma += perdida.item() * len(yb)
    return suma / len(cargador.dataset)  # entre 150 ejemplos, no entre 5 batches

La segunda función es idéntica, pero omite optimizador.zero_grad().

Python 3 · ejemplo reproducible
def entrenar_epoca_sin_limpiar(modelo, cargador, optimizador):
    suma = 0.0
    for xb, yb in cargador:
        perdida = perdida_fn(modelo(xb), yb)  # sin zero_grad() antes
        perdida.backward()
        optimizador.step()
        suma += perdida.item() * len(yb)
    return suma / len(cargador.dataset)

Cada función entrena 10 épocas con SGD y lr=0.1, desde el mismo modelo inicial y el mismo cargador.

Pregunta. Durante 10 épocas, ¿con cuál de las dos funciones baja la pérdida de forma sostenida? ¿Qué gradiente usa step() en la segunda función a partir del segundo batch?

Ver resultado

Cada fila muestra la pérdida media de entrenamiento de una época. La columna entrenar_epoca corresponde a la función que llama a zero_grad() en cada batch, y la columna entrenar_epoca_sin_limpiar a la que lo omite. Por ejemplo, la fila 6 dice que en la sexta época la pérdida es 0.503 con zero_grad() y 0.430 sin él.

épocaentrenar_epocaentrenar_epoca_sin_limpiar
10.7110.679
20.6340.557
30.5880.435
40.5560.394
50.5270.389
60.5030.430
70.4810.440
80.4610.484
90.4420.456
100.4250.511
Lectura. Con zero_grad(), la pérdida baja en cada época. Sin él, backward() suma cada gradiente nuevo a todos los anteriores, y step() usa esa suma. En las primeras épocas, la suma produce pasos más grandes y la pérdida baja más rápido. Después la suma arrastra gradientes de pesos que ya cambiaron y los pasos quedan demasiado grandes: desde la época 6 la pérdida sube y oscila. Una pérdida que baja al principio no demuestra que el loop esté bien.

Sobreajuste de un batch

Un MLP con 82 parámetros puede memorizar las etiquetas de 8 ejemplos. Si el loop es correcto, entrenar muchas veces sobre ese único batch debe llevar su pérdida cerca de cero. Si no ocurre, el defecto está en el loop o en el modelo, no en la dificultad de los datos. La prueba tarda segundos y se hace antes de entrenar con el dataset completo.

sobreajustar repite los cinco pasos sobre los mismos 8 ejemplos. Al final cuenta los aciertos con argmax(dim=1), que elige en cada fila la clase con el logit mayor.

Pregunta. El código entrena 500 pasos con SGD y lr=0.5 sobre los mismos 8 ejemplos e imprime la pérdida en los pasos 1, 50, 100, 200 y 500. ¿En cuál de esos pasos queda la pérdida por primera vez por debajo de 0.1?

Python 3 · ejemplo reproducible
x_mini, y_mini = X_entrenamiento[:8], y_entrenamiento[:8]


def sobreajustar(modelo, optimizador, pasos=500):
    for paso in range(1, pasos + 1):
        optimizador.zero_grad()
        perdida = perdida_fn(modelo(x_mini), y_mini)
        perdida.backward()
        optimizador.step()
        if paso in (1, 50, 100, 200, 500):
            print(paso, round(perdida.item(), 4))
    with torch.no_grad():
        aciertos = (modelo(x_mini).argmax(dim=1) == y_mini).sum().item()
    print("aciertos:", aciertos, "de 8")


modelo = crear_modelo()
sobreajustar(modelo, torch.optim.SGD(modelo.parameters(), lr=0.5))
Salida esperada

La salida esperada está oculta.

Un error frecuente es crear el optimizador con los parámetros de otro modelo, por ejemplo tras volver a llamar a crear_modelo().

Pregunta. El optimizador recibe los parámetros de otro_modelo, pero el loop calcula la pérdida con modelo. ¿Qué pasa con la pérdida en 500 pasos?

Python 3 · ejemplo reproducible
modelo = crear_modelo()
otro_modelo = crear_modelo()
sobreajustar(modelo, torch.optim.SGD(otro_modelo.parameters(), lr=0.5))
Salida esperada

La salida esperada está oculta.

Optimizador y learning rate

En SGD, el learning rate multiplica el gradiente en cada paso, así que fija el tamaño del cambio de los pesos.

La función entrenar_varias crea el mismo modelo inicial y el mismo cargador para cada configuración. Así, lo único que cambia entre corridas es el optimizador. Recibe una función que crea el optimizador a partir de los parámetros del modelo nuevo, aquí una lambda. resumen toma la pérdida de cuatro épocas. Las listas muestran la pérdida de entrenamiento en las épocas 1, 10, 20 y 30.

Pregunta. Ordena lr = 0.01, 0.1, 1.0 y 10.0 según la pérdida en la época 30. ¿Con cuál sube la pérdida en la primera época en lugar de bajar?

Python 3 · ejemplo reproducible
def entrenar_varias(crear_optimizador, epocas=30):
    modelo = crear_modelo()
    optimizador = crear_optimizador(modelo.parameters())
    cargador = crear_cargador()
    return [entrenar_epoca(modelo, cargador, optimizador) for _ in range(epocas)]


def resumen(perdidas):
    return [round(perdidas[i], 3) for i in (0, 9, 19, 29)]


for lr in (0.01, 0.1, 1.0, 10.0):
    perdidas = entrenar_varias(lambda p: torch.optim.SGD(p, lr=lr))
    print(f"SGD lr={lr}", resumen(perdidas))
Salida esperada

La salida esperada está oculta.

torch.optim.Adam es otro optimizador. Guarda, para cada parámetro, un promedio móvil de sus gradientes y otro de sus cuadrados. Divide el primero entre la raíz del segundo, así que cada parámetro recibe un tamaño de paso propio. El learning rate sigue fijando la escala general. Su valor por defecto es 0.001.

Pregunta. Con el mismo modelo inicial y el mismo cargador, ¿Adam con lr=0.01 termina por encima o por debajo de SGD con lr=0.1? ¿Qué pasa con Adam si el learning rate es 0.001?

Python 3 · ejemplo reproducible
for lr in (0.001, 0.01):
    perdidas = entrenar_varias(lambda p: torch.optim.Adam(p, lr=lr))
    print(f"Adam lr={lr}", resumen(perdidas))
Salida esperada

La salida esperada está oculta.

Schedule del learning rate

Hasta aquí, el learning rate queda fijo durante todo el entrenamiento. Un scheduler lo cambia según un calendario. En PyTorch, los schedulers están en torch.optim.lr_scheduler y reciben el optimizador al crearse. Cada llamada a scheduler.step() avanza el calendario un paso y escribe el learning rate nuevo en el optimizador. scheduler.get_last_lr() devuelve el learning rate actual en una lista, con un valor por grupo de parámetros. Los optimizadores de esta lección tienen un solo grupo, así que se lee el elemento [0].

Los calendarios de esta sección cuentan épocas. Por eso scheduler.step() va una vez por época, al terminar la época de entrenamiento, después de todas las llamadas a optimizador.step() de esa época.

  • StepLR(optimizador, step_size=10, gamma=0.5) multiplica el learning rate por gamma cada step_size épocas
  • CosineAnnealingLR(optimizador, T_max=30) baja el learning rate siguiendo medio ciclo de coseno, desde el valor inicial hasta 0 después de T_max llamadas a step()

La función entrenar_con_scheduler usa SGD con el mismo modelo inicial y el mismo cargador que entrenar_varias. Antes de entrenar cada época, guarda el learning rate que esa época va a usar.

Python 3 · ejemplo reproducible
from torch.optim.lr_scheduler import CosineAnnealingLR, StepLR


def entrenar_con_scheduler(crear_scheduler, lr, epocas=30):
    modelo = crear_modelo()
    optimizador = torch.optim.SGD(modelo.parameters(), lr=lr)
    scheduler = crear_scheduler(optimizador)
    cargador = crear_cargador()
    lrs, perdidas = [], []
    for _ in range(epocas):
        lrs.append(scheduler.get_last_lr()[0])
        perdidas.append(entrenar_epoca(modelo, cargador, optimizador))
        scheduler.step()  # una vez por época, fuera del loop de batches
    return lrs, perdidas

El parámetro crear_scheduler recibe una función que toma el optimizador y devuelve el scheduler. El diccionario schedulers guarda una de esas funciones por calendario.

Python 3 · ejemplo reproducible
schedulers = {
    "StepLR": lambda opt: StepLR(opt, step_size=10, gamma=0.5),
    "coseno": lambda opt: CosineAnnealingLR(opt, T_max=30),
}

Las dos corridas usan SGD con lr=1.0 durante 30 épocas, una con cada scheduler de schedulers.

Pregunta. Con lr=1.0, ¿qué learning rate usa cada scheduler en las épocas 11, 21 y 30? ¿En qué época llega el coseno a 0.5?

Ver resultado

Cada fila muestra el learning rate que usa una época, leído con scheduler.get_last_lr()[0] antes de entrenarla. La columna StepLR corresponde a StepLR(step_size=10, gamma=0.5) y la columna coseno a CosineAnnealingLR(T_max=30). Por ejemplo, la fila 11 dice que la época 11 entrena con 0.500 bajo StepLR y con 0.750 bajo el coseno.

épocaStepLRcoseno
11.0001.000
101.0000.794
110.5000.750
160.5000.500
200.5000.297
210.2500.250
300.2500.003
Lectura. La época 1 usa el learning rate inicial con los dos schedulers, porque scheduler.step() se llama al terminar cada época. StepLR mantiene 1.0 en las épocas 1 a 10, baja a 0.5 en la 11 y a 0.25 en la 21: cambia en escalones. El coseno cambia en cada época: 0.794 en la época 10, 0.5 en la 16, a la mitad del calendario, y 0.003 en la 30.

La comparación usa dos learning rates iniciales: 1.0, el mejor de la sección anterior, y 3.0. La corrida sin scheduler es entrenar_varias con SGD, así que parte del mismo modelo y del mismo cargador.

Pregunta. ¿Con cuál de los dos learning rates baja la pérdida de la época 30 al agregar un scheduler? ¿Con cuál sube?

Ver resultado

Cada fila muestra una corrida de 30 épocas. La columna lr es el learning rate inicial, scheduler es el calendario (ninguno, StepLR o coseno), época 30 es la pérdida de entrenamiento en la última época y máx. 21–30 es la pérdida más alta entre las épocas 21 y 30. Por ejemplo, la fila 3.0, ninguno, 0.233, 0.402 dice que sin scheduler y con lr=3.0 la época 30 termina en 0.233 y alguna de las últimas diez épocas llega a 0.402.

lrschedulerépoca 30máx. 21–30
1.0ninguno0.1420.257
1.0StepLR0.1940.216
1.0coseno0.2050.216
3.0ninguno0.2330.402
3.0StepLR0.1890.226
3.0coseno0.1830.203
Lectura. Con lr=3.0, los dos schedulers bajan la pérdida de la época 30: de 0.233 a 0.189 con StepLR y a 0.183 con el coseno. Sin scheduler, los pasos siguen siendo grandes al final y la pérdida sube hasta 0.402 en alguna de las épocas 21 a 30. Con lr=1.0 pasa lo contrario: sin scheduler la época 30 queda en 0.142, por debajo de 0.194 y 0.205, pero esa misma corrida sube hasta 0.257 en las épocas 21 a 30. Esa oscilación es mayor que la diferencia con las corridas con scheduler, así que una sola época no basta para decidir cuál termina mejor. Con los dos learning rates, el scheduler baja la pérdida más alta de las épocas 21 a 30. Un scheduler no sustituye la elección del learning rate inicial: la combinación se elige comparando corridas.

Validación por época

modelo.train() y modelo.eval() cambian el modo del modelo. El modo se guarda en el atributo training de cada submódulo. Algunas capas calculan distinto según ese atributo. El loop pone modelo.train() antes de entrenar y modelo.eval() antes de evaluar para que el mismo código sirva con esas capas.

Pregunta. Después de modelo.eval(), ¿la salida del modelo sigue registrando operaciones para backward()? ¿Y dentro de torch.no_grad()?

Python 3 · ejemplo reproducible
modelo = crear_modelo()
print(modelo.training)

modelo.eval()
print(modelo.training, modelo[0].training)
print(modelo(X_validacion).requires_grad)

with torch.no_grad():
    print(modelo(X_validacion).requires_grad)

modelo.train()
print(modelo.training)
Salida esperada

La salida esperada está oculta.

La función evaluar aplica modelo.eval() y torch.no_grad() sobre los 50 puntos de validación y devuelve la pérdida y la exactitud. .float() convierte cada acierto en 1 y cada fallo en 0, y .mean() los promedia.

Python 3 · ejemplo reproducible
def evaluar(modelo):
    modelo.eval()
    with torch.no_grad():
        logits = modelo(X_validacion)
        perdida = perdida_fn(logits, y_validacion).item()
        exactitud = (logits.argmax(dim=1) == y_validacion).float().mean().item()
    return perdida, exactitud

El loop completo alterna una época de entrenamiento con una evaluación sobre los 50 puntos de validación. Cada época se guarda en historial. La validación nunca llama a backward() ni a step(), así que no cambia los pesos.

Pregunta. En la época 1, ¿la pérdida de validación queda por encima o por debajo de la de entrenamiento? Piensa en qué momento de la época se mide cada una.

Python 3 · ejemplo reproducible
modelo = crear_modelo()
optimizador = torch.optim.Adam(modelo.parameters(), lr=0.01)
cargador = crear_cargador()
historial = []

for epoca in range(1, 101):
    modelo.train()
    perdida_ent = entrenar_epoca(modelo, cargador, optimizador)
    perdida_val, exactitud_val = evaluar(modelo)
    historial.append((epoca, perdida_ent, perdida_val, exactitud_val))

print("época  ent.   val.   exactitud val.")
for epoca, p_ent, p_val, exac in historial:
    if epoca in (1, 5, 10, 20, 40, 60, 80, 100):
        print(f"{epoca:>5}  {p_ent:.3f}  {p_val:.3f}  {exac:.2f}")
Salida esperada

La salida esperada está oculta.

Explorador

Entrenamiento de un MLP

Reproduce el entrenamiento y observa cómo la frontera se curva mientras bajan las pérdidas de entrenamiento y de validación por época.

Qué probar

Si subes el learning rate a 10, la pérdida de entrenamiento se vuelve inestable y sube y baja de una época a otra.

Parámetros
Conjunto de datos
x₁, x₂8 ReLUsoftmax

120 puntos de entrenamiento y 60 de validación. Cada cambio vuelve a la época 0 con la misma semilla.

Época 0, pesos iniciales: pérdida de entrenamiento 0.94 y exactitud de validación 0.28. Cada época son 8 pasos de SGD, 7 batches de 16 puntos y uno de 8, con learning rate 0.3.

Práctica

Traza guiada

Antes de procesar, escribe el número de batches por época, la forma de xb en cada batch y cuántas veces llamaría a step() un loop de 3 épocas con este cargador. Decide también si cada fila de xb sigue junto a su etiqueta después de reordenar.

Python 3 · ejemplo reproducible
datos_traza = TensorDataset(
    torch.arange(12, dtype=torch.float32).reshape(6, 2),
    torch.tensor([0, 1, 0, 1, 1, 0]),
)
cargador_traza = DataLoader(
    datos_traza, batch_size=4, shuffle=True, generator=torch.Generator().manual_seed(1)
)

Comprueba con print(xb, yb) dentro de un for. La fila [2., 3.] debe aparecer siempre con la etiqueta 1. Si cuentas mal los batches, revisa qué pasa con los ejemplos que no completan un batch.

Tamaño de batch

Añade un parámetro batch_size a entrenar_varias y pásalo a crear_cargador. Repite la corrida de Adam con lr=0.01 y batch_size igual a 8 y a 150. Antes de procesar, predice cuántas llamadas a step() hace cada configuración por época y cuál llega a una pérdida más baja en la época 30. Comprueba el número de batches con assert len(cargador) == …. Relaciona la diferencia con el número de llamadas a step() en 30 épocas.

Loop de tres clases

Genera datos con make_blobs(n_samples=300, centers=3, random_state=0) y sepáralos en 240 de entrenamiento y 60 de validación con stratify. Escribe un cálculo que:

  1. convierta los datos a tensores float32 y etiquetas enteras
  2. cree un MLP 2 → 16 → 3 con torch.manual_seed(0) y un DataLoader con generador semillado
  3. sobreajuste 8 ejemplos y compruebe con assert que la pérdida final queda por debajo de 0.05
  4. entrene un modelo nuevo durante 50 épocas y guarde en historial la pérdida de entrenamiento, la de validación y la exactitud de validación
  5. compruebe con assert len(historial) == 50 y escriba la época con menor pérdida de validación

Reto opcional

Entrena el modelo de «Validación por época» con semillas 0, 1 y 2 en crear_cargador y el mismo modelo inicial. Después añade un parámetro de semilla a crear_modelo, fija el cargador con semilla 0 y cambia solo la semilla del modelo. Compara el rango de la pérdida de validación final en los dos casos y explica cuál de las dos fuentes de azar cambia más el resultado.

Errores que revisar

  • Si la pérdida de la primera época queda por encima de la pérdida inicial, el learning rate es demasiado grande. Divídelo entre 10 y compara.
  • Si la pérdida del batch diminuto no baja, revisa que el optimizador recibió modelo.parameters() del mismo modelo que calcula la pérdida y que step() va después de backward().
  • Si la pérdida baja rápido al principio y luego sube con un learning rate que antes funcionaba, falta optimizador.zero_grad() al inicio de cada batch.
  • Si dos corridas del mismo código dan pérdidas distintas, fija torch.manual_seed antes de crear el modelo y pasa un generator semillado al DataLoader.
  • Si la memoria crece época tras época, revisa que acumulas perdida.item() y no el tensor perdida, que conserva su registro de operaciones.
  • Si el learning rate baja mucho antes de lo previsto, o el coseno vuelve a subir a mitad del entrenamiento, revisa que scheduler.step() esté fuera del loop de batches. Dentro de él avanza una vez por batch: con 5 batches por época, StepLR(step_size=10, gamma=0.5) divide el learning rate entre 2 cada 2 épocas y CosineAnnealingLR(T_max=30) llega a 0 en la época 7 y vuelve a subir.

Cierre

Explica qué comprueba el sobreajuste de un batch de 8 ejemplos y qué información añade la pérdida de validación registrada por época.

Lista de comprobación

  • ¿Cuántas veces se llama optimizador.step() en una época con 150 ejemplos y batch_size=32?
  • ¿Qué gradiente usa step() si se omite optimizador.zero_grad()?
  • ¿Por qué un learning rate demasiado grande hace que la pérdida suba en lugar de bajar?
  • ¿Qué cambia modelo.eval() y qué cambia torch.no_grad()?
  • ¿Por qué la pérdida de entrenamiento de una época puede quedar por encima de la de validación?
  • ¿Qué learning rate usa la época 21 con StepLR(step_size=10, gamma=0.5) y lr=1.0, y qué cambia si scheduler.step() se llama en cada batch?

Documentación oficial

Cargando progreso…