La red de 05-02 tiene 497 parámetros aleatorios y dice "0,55" a todos los pedidos. Esta lección explica cómo esos números pasan de aleatorios a útiles, es decir, cómo aprende una red. La idea la anunciamos al cerrar 03-04: aprender es optimizar, y cuando la función de pérdida es suave no hace falta tantear vecinos al azar como en el ascenso de colina, porque podemos calcular la pendiente y bajar por ella. Veremos la pérdida como un paisaje, la derivada como pendiente, el descenso del gradiente paso a paso con un ejemplo de una variable resuelto a mano, el papel de la tasa de aprendizaje, la regla de la cadena que permite repartir el error hacia atrás por las capas (la retropropagación), las variantes por lotes y mini-lotes, los optimizadores (SGD con momento, Adam) y, por fin, las técnicas de regularización específicas de redes que dejamos apuntadas en 04-06: parada temprana, dropout, aumento de datos y normalización por lotes. En código: un descenso del gradiente desde cero en numpy para una neurona y el bucle de entrenamiento completo en PyTorch del MLP de NovaMarket, evaluado con AUC frente a la logística de 04-05. Es importante porque este algoritmo es el que entrena todo, desde nuestra red de 497 parámetros hasta los grandes modelos de lenguaje de 05-05.

Contenido

  1. La pérdida como paisaje: del ascenso de colina a la brújula
  2. La derivada como pendiente y la intuición del gradiente
  3. Descenso del gradiente paso a paso: un ejemplo de una variable a mano
  4. La tasa de aprendizaje: demasiado grande, demasiado pequeña
  5. Regla de la cadena y retropropagación
  6. Lotes, mini-lotes, épocas y optimizadores (SGD, momento, Adam)
  7. Regularización específica de redes: parada temprana, dropout, aumento de datos y normalización por lotes
  8. Código (a): descenso del gradiente desde cero en numpy
  9. Código (b): entrenamiento completo del MLP de NovaMarket en PyTorch
  10. Errores Comunes y Consejos
  11. Ejercicios
  12. Conclusión

  1. La pérdida como paisaje: del ascenso de colina a la brújula

Recuerda 04-01: la función de pérdida mide, con un número, cuánto se equivoca el modelo sobre los datos de entrenamiento con sus parámetros actuales. Para la neurona sigmoide con entropía cruzada binaria (05-02), la pérdida depende de los pesos y el sesgo: cambia los pesos y cambia la pérdida. Imagina un paisaje en el que cada punto del suelo es una combinación de parámetros y la altura es la pérdida: entrenar es bajar al valle más bajo. Con 2 parámetros el paisaje se dibuja; con 497 no, pero la geometría es la misma.

En 03-04 bajábamos "a ciegas": el ascenso de colina probaba vecinos al azar y se movía si mejoraban, y el recocido simulado aceptaba a veces empeorar. Funcionaba para rutas de reparto, donde no hay pendiente definida (permutar dos paradas no es "un poco a la izquierda"). Con parámetros continuos y una pérdida suave tenemos algo mejor: una brújula que señala cuesta abajo, el gradiente. No hace falta probar vecinos: se calcula la dirección de máximo descenso y se da un paso. Repetido miles de veces, ese paso es el descenso del gradiente.

  1. La derivada como pendiente y la intuición del gradiente

La derivada de una función en un punto es su pendiente allí: cuánto sube la función por cada unidad que avanzas. Si f(w) = (w − 3)² + 1 (una parábola con el mínimo en w = 3), su derivada es f'(w) = 2·(w − 3). En w = 0 vale −6 (la función baja empinada hacia la derecha); en w = 2,5 vale −1 (baja suave); en w = 3 vale 0 (llano: el mínimo); en w = 5 vale +4 (sube). No necesitas saber calcular derivadas para este curso: basta con la idea de "pendiente en el punto" y con saber que PyTorch las calcula por ti (sección 5).

Con varios parámetros, el gradiente es el vector de derivadas parciales, una por parámetro: la pendiente de la pérdida "si muevo solo w₁", "si muevo solo w₂", etc. Apunta en la dirección en que la pérdida sube más deprisa; su opuesto, cuesta abajo. La regla del descenso del gradiente para cada parámetro θ es:

θ ← θ − η · ∂L/∂θ

es decir, "resta a cada parámetro su pendiente multiplicada por la tasa de aprendizaje η". Si la pendiente es negativa (la pérdida baja al aumentar θ), la resta lo aumenta; si es positiva, lo disminuye. Compárala con la regla del perceptrón de 05-01, w ← w + η·e·x: es la misma forma, y de hecho veremos que la regla del perceptrón es un caso particular.

  1. Descenso del gradiente paso a paso: un ejemplo de una variable a mano

Minimicemos f(w) = (w − 3)² + 1 desde w = 0 con η = 0,1. En cada iteración: calculamos la pendiente 2(w − 3), y actualizamos w ← w − 0,1 · pendiente.

Iteración w actual f(w) Pendiente 2(w − 3) Paso −0,1 · pendiente w nuevo
1 0 10 −6 +0,6 0,6
2 0,6 6,76 −4,8 +0,48 1,08
3 1,08 4,686 −3,84 +0,384 1,464
4 1,464 3,359 −3,072 +0,307 1,771
5 1,771 2,510 −2,458 +0,246 2,017
6 2,017 1,966 −1,966 +0,197 2,214

Tres cosas que se ven en la tabla y que valen para cualquier red: (1) la pérdida baja en cada paso (10 → 6,76 → 4,69 → ...); (2) los pasos son grandes lejos del mínimo y pequeños cerca, porque la pendiente se suaviza al acercarse al valle, sin que nadie cambie η; (3) no llega exactamente al mínimo (w = 3) sino que se acerca indefinidamente: en la práctica se para cuando la mejora es despreciable o, mejor, cuando lo dice la validación (sección 7). En una red la única diferencia es que en vez de una pendiente hay 497 (una por parámetro) y que la función no es una parábola sino un paisaje con muchos valles; el algoritmo baja al valle en el que cae, que puede no ser el más profundo, exactamente el problema de los óptimos locales de 03-04.

  1. La tasa de aprendizaje: demasiado grande, demasiado pequeña

η es el hiperparámetro más importante del entrenamiento. Con la misma parábola y el mismo punto de partida:

η w tras 1, 2, 3, 4, 5 iteraciones Comportamiento
0,01 0,06; 0,119; 0,176; 0,233; 0,288 Demasiado pequeña: baja, pero tras 5 pasos apenas ha recorrido el 10 % del camino; harían falta cientos
0,1 0,6; 1,08; 1,46; 1,77; 2,02 Adecuada: converge suavemente
0,5 3,0; 3,0; 3,0; 3,0; 3,0 Perfecta para esta parábola (llega en un paso); casualidad de la forma cuadrática, no generalizable
1,1 6,6; −1,32; 8,18; −3,22; 10,47 Demasiado grande: salta de un lado a otro del valle cada vez más lejos; la pérdida sube (10 → 14 → 20 → 28 → 40) y diverge

Si dibujaras la pérdida por iteración: con η pequeña, una curva que baja despacio y se aplana antes de llegar; con η adecuada, una caída rápida que se estabiliza; con η grande, oscilaciones o una curva que sube y explota (en PyTorch aparecen nan). No hay valor universal: depende de la escala de los datos (por eso estandarizamos) y de la arquitectura. Valores típicos con Adam (sección 6): entre 0,0001 y 0,01. En la práctica se prueba con validación (04-06), y a menudo se reduce durante el entrenamiento (learning rate schedule): pasos grandes al principio, finos al final, la misma intuición que la temperatura del recocido simulado.

  1. Regla de la cadena y retropropagación

Para aplicar θ ← θ − η·∂L/∂θ hay que conocer la pendiente de la pérdida respecto a cada peso, y en una red el peso de la primera capa afecta a la pérdida a través de una cadena larga de operaciones. La herramienta es la regla de la cadena: la pendiente de una composición de funciones es el producto de las pendientes de cada eslabón. Si L depende de p, p de z y z de w, entonces ∂L/∂w = (∂L/∂p) · (∂p/∂z) · (∂z/∂w). Intuición: si z sube 2 unidades por cada unidad de w, p sube 0,25 por unidad de z y L baja 3 por unidad de p, entonces L baja 3 × 0,25 × 2 = 1,5 por unidad de w.

Veámoslo en el grafo de cómputo de una neurona sigmoide con pérdida BCE (nuestra regresión logística):

flowchart LR
    x["x (entradas)"] --> Z["z = w·x + b"]
    w["w, b"] --> Z
    Z --> P["p = σ(z)"]
    P --> L["L = −[y·log p + (1−y)·log(1−p)]"]
    y["y (etiqueta)"] --> L
    L -. "∂L/∂p" .-> P
    P -. "∂p/∂z = p(1−p)" .-> Z
    Z -. "∂z/∂w = x,  ∂z/∂b = 1" .-> w

Las flechas continuas son el paso hacia delante (calcular la predicción y la pérdida); las discontinuas, el paso hacia atrás: desde la pérdida, cada nodo recibe la pendiente acumulada, la multiplica por su pendiente local y la pasa a sus entradas. Para la pareja sigmoide + BCE los dos primeros eslabones se simplifican de forma preciosa: ∂L/∂z = p − y (la probabilidad predicha menos la etiqueta). Por tanto ∂L/∂w = (p − y)·x y ∂L/∂b = p − y. Compara con la regla del perceptrón: e·x con e = y − ŷ. Es la misma regla, con la probabilidad en lugar del escalón, y ahora sabemos de dónde sale: es el gradiente de la pérdida.

Ejemplo numérico: pedido x = (0,5; 1,0; 1) (importe, días y cliente nuevo estandarizados), etiqueta y = 1 (se devolvió), pesos w = (0,4; 0,3; 0,9), b = −2.

  • Adelante: z = 0,2 + 0,3 + 0,9 − 2 = −0,6; p = σ(−0,6) = 0,354; pérdida L = −log(0,354) = 1,037. La red da 35 % a algo que ocurrió: mal.
  • Atrás: ∂L/∂z = p − y = −0,646; ∂L/∂w = −0,646 · x = (−0,323; −0,646; −0,646); ∂L/∂b = −0,646.
  • Paso con η = 0,5: w ← (0,4; 0,3; 0,9) − 0,5·(−0,323; −0,646; −0,646) = (0,561; 0,623; 1,223); b ← −2 + 0,323 = −1,677.
  • Comprobación: ahora z = 0,449, p = 0,61, L = 0,494. La pérdida ha bajado de 1,037 a 0,494 para este pedido. Con todos los pedidos, el gradiente que se usa es el promedio de los gradientes individuales.

En una red con capas ocultas la cadena es más larga pero idéntica en espíritu: la pendiente de la pérdida respecto a la salida de la última capa se propaga a la penúltima multiplicando por los pesos y por la pendiente de la activación (1 o 0 en ReLU; ≤ 0,25 en sigmoide, y de ahí el desvanecimiento de 05-02), y así sucesivamente hasta la primera. Ese algoritmo, que reutiliza los cálculos hacia atrás capa a capa en lugar de rehacerlos para cada peso, es la retropropagación (backpropagation), popularizada en 1986 por Rumelhart, Hinton y Williams (01-01), y es lo que permitió entrenar las capas ocultas que resolvían XOR. En PyTorch no lo escribirás nunca: cada tensor recuerda las operaciones que lo produjeron (el grafo de cómputo), y L.backward() recorre ese grafo hacia atrás y deja en p.grad la pendiente de cada parámetro. Es la diferenciación automática, y es la razón de que los frameworks de deep learning existan (07-03).

  1. Lotes, mini-lotes, épocas y optimizadores (SGD, momento, Adam)

¿Sobre cuántos ejemplos calculamos el gradiente antes de dar un paso?

Variante Ejemplos por paso Pasos por época (1.799 pedidos de entrenamiento) Ventajas Inconvenientes
Por lotes (batch) Todos 1 Gradiente exacto, descenso suave Lento por paso; no cabe en memoria con millones de imágenes
Estocástico (SGD puro) 1 1.799 Barato por paso; el ruido ayuda a escapar de óptimos locales pobres Muy ruidoso; no aprovecha el paralelismo de la GPU
Mini-lotes 32-512 (aquí 64) 29 El equilibrio: gradiente razonable, eficiente en GPU, algo de ruido útil Un hiperparámetro más (el tamaño de lote)

Una época es una pasada completa por los datos de entrenamiento (como en el perceptrón). Con mini-lotes de 64 y 1.799 pedidos, cada época son 29 pasos de gradiente; 100 épocas, 2.900 pasos. Los datos se barajan al inicio de cada época para que los lotes cambien.

El optimizador es la regla que convierte gradiente en actualización. La básica es la de la sección 2 (SGD). Dos mejoras casi universales:

  • Momento (momentum): en vez de moverse solo según el gradiente actual, se acumula una "velocidad" (media móvil de los gradientes recientes). Como una bola que rueda: atraviesa pequeños baches, acelera en pendientes largas y amortigua las oscilaciones en valles estrechos. torch.optim.SGD(..., momentum=0.9).
  • Adam (adaptive moment estimation): momento más una tasa de aprendizaje adaptada a cada parámetro (los que reciben gradientes grandes se frenan, los que reciben gradientes pequeños se aceleran). Es robusto a la elección de η y es el optimizador por defecto para empezar. torch.optim.Adam(..., lr=0.001).

En nuestro MLP, la pérdida de validación tras 1, 5, 10, 20 y 30 épocas fue: SGD (η = 0,01) 0,72 → 0,60 → 0,53 → 0,47 → 0,45; SGD con momento 0,55 → 0,45 → 0,42 → 0,34 → 0,33; Adam (η = 0,001) 0,73 → 0,48 → 0,39 → 0,34 → 0,33. Los tres van al mismo sitio; momento y Adam llegan mucho antes.

  1. Regularización específica de redes: parada temprana, dropout, aumento de datos y normalización por lotes

Una red con cientos de parámetros por cada pocos ejemplos sobreajusta con facilidad (04-06). Además de la regularización L2 (que aquí se llama weight decay y es un argumento del optimizador), el deep learning añade cuatro técnicas propias:

Técnica Qué hace Por qué funciona Cuándo
Parada temprana (early stopping) Evaluar la pérdida en un conjunto de validación al final de cada época y detenerse cuando lleva k épocas sin mejorar (paciencia), recuperando los pesos de la mejor época Al principio la red aprende lo general; con más épocas empieza a memorizar el ruido de entrenamiento y la validación empeora Siempre. Es gratis y sustituye a "elegir el número de épocas"
Dropout Durante el entrenamiento, en cada paso, apagar al azar una fracción p (0,1-0,5) de las neuronas de una capa; en predicción se usan todas Cada neurona no puede depender de que otra concreta esté; la red aprende rasgos redundantes y robustos, como un ensamblado de muchas subredes Capas densas grandes; en nuestro MLP pequeño ayuda poco
Aumento de datos Crear ejemplos nuevos transformando los existentes: girar, recortar o cambiar el brillo de fotos; sustituir sinónimos en texto Más ejemplos "gratis" que enseñan las invariancias correctas (un paquete dañado lo está aunque la foto esté girada) Imágenes y audio sobre todo (05-04)
Normalización por lotes (batch norm) Estandarizar la salida de cada capa con la media y desviación del mini-lote (más dos parámetros aprendidos) Mantiene las activaciones en un rango sano capa a capa; permite tasas de aprendizaje mayores y actúa como regularizador suave Redes profundas, sobre todo convolucionales

Con esto queda cerrado lo que 04-06 dejó apuntado. En el código de la sección 9 usaremos parada temprana y dropout; el aumento de datos y la normalización por lotes aparecen en 05-04.

  1. Código (a): descenso del gradiente desde cero en numpy

Entrenamos una neurona sigmoide (regresión logística) sobre 3.000 pedidos de NovaMarket con tres columnas, escribiendo el gradiente a mano según la sección 5:

import numpy as np
from novamarket_ml import generar_pedidos_ml

def sigmoide(z):
    return 1 / (1 + np.exp(-z))

def perdida_bce(p, y):
    eps = 1e-9                                       # evita log(0)
    return -np.mean(y * np.log(p + eps) + (1 - y) * np.log(1 - p + eps))

pedidos = generar_pedidos_ml(3000, 42)
cols = ["importe", "dias_entrega", "cliente_nuevo"]
X = pedidos[cols].values.astype(float)
y = pedidos["devuelto"].values.astype(float)
X = (X - X.mean(axis=0)) / X.std(axis=0)             # estandarizar (imprescindible)

w = np.zeros(3); b = 0.0                             # punto de partida
tasa = 0.5
print(f"{'época':>5} {'pérdida':>8} {'w_importe':>10} {'w_dias':>8} {'w_nuevo':>8} {'b':>7}")
for epoca in range(1, 201):
    z = X @ w + b                                    # 1. adelante: suma ponderada de los 3.000 pedidos
    p = sigmoide(z)                                  #    probabilidades
    grad_z = p - y                                   # 2. atrás: dL/dz = p - y para cada pedido
    grad_w = X.T @ grad_z / len(y)                   #    dL/dw = promedio de (p - y) * x
    grad_b = grad_z.mean()                           #    dL/db = promedio de (p - y)
    w -= tasa * grad_w                               # 3. paso cuesta abajo
    b -= tasa * grad_b
    if epoca in (1, 2, 3, 5, 10, 20, 50, 100, 200):
        L = perdida_bce(sigmoide(X @ w + b), y)
        print(f"{epoca:5d} {L:8.4f} {w[0]:10.3f} {w[1]:8.3f} {w[2]:8.3f} {b:7.3f}")

Salida:

época  pérdida  w_importe   w_dias  w_nuevo       b
    1   0.6250      0.064    0.031    0.056  -0.168
    2   0.5727      0.120    0.058    0.104  -0.315
    3   0.5322      0.170    0.082    0.147  -0.444
    5   0.4753      0.252    0.124    0.220  -0.659
   10   0.4040      0.399    0.202    0.351  -1.031
   20   0.3568      0.577    0.311    0.516  -1.436
   50   0.3310      0.811    0.490    0.751  -1.900
  100   0.3266      0.934    0.603    0.880  -2.132
  200   0.3261      0.987    0.653    0.936  -2.233

Lectura: con w = 0 la pérdida inicial es ln 2 = 0,693 (la red dice 0,5 a todo); en 10 épocas ha bajado a 0,40 y luego se aplana en 0,326: hemos llegado al fondo del valle (esta pérdida es convexa, así que es el mínimo global). Los pesos convergen a (0,99; 0,65; 0,94) y b = −2,23; LogisticRegression sin regularización sobre los mismos datos da (0,996; 0,661; 0,945) y −2,248: hemos reproducido fit con veinte líneas. Es descenso por lotes (los 3.000 pedidos en cada paso); con tasa = 0.01 tras 200 épocas la pérdida seguía en 0,51 (demasiado lenta), y con tasa = 20 oscila entre 0,47 y 0,66 sin converger (demasiado grande), tal como predice la sección 4.

  1. Código (b): entrenamiento completo del MLP de NovaMarket en PyTorch

Ahora la red 21 → 16 → 8 → 1 de 05-02, con todo lo aprendido: mini-lotes, Adam, dropout, parada temprana con conjunto de validación y evaluación final con AUC en el mismo test de 04-05.

9.1 Datos: entrenamiento, validación y test

import numpy as np, torch, torch.nn as nn
from torch.utils.data import TensorDataset, DataLoader
from sklearn.model_selection import train_test_split
from sklearn.metrics import roc_auc_score
from novamarket_ml import generar_pedidos_ml, ensuciar_pedidos, preparar_pedidos, crear_preparacion

X, y = preparar_pedidos(ensuciar_pedidos(generar_pedidos_ml(3000, 42), 42))
Xtr, Xte, ytr, yte = train_test_split(X, y, test_size=0.25, random_state=42, stratify=y)   # test: como en 04-05
Xtr2, Xval, ytr2, yval = train_test_split(Xtr, ytr, test_size=0.2, random_state=42, stratify=ytr)  # validación
prep = crear_preparacion().fit(Xtr2)                 # el preprocesado se ajusta SOLO con entrenamiento

def a_tensor(Xd, yd):
    return (torch.tensor(prep.transform(Xd), dtype=torch.float32),
            torch.tensor(yd.values, dtype=torch.float32).unsqueeze(1))   # y como columna (n x 1)

Xtr_t, ytr_t = a_tensor(Xtr2, ytr2)
Xval_t, yval_t = a_tensor(Xval, yval)
Xte_t, yte_t = a_tensor(Xte, yte)
print(Xtr_t.shape, Xval_t.shape, Xte_t.shape)        # torch.Size([1799, 21]) torch.Size([450, 21]) torch.Size([750, 21])

El test son los mismos 750 pedidos de 04-05 (misma semilla), así que el AUC será comparable con el 0,844 de la logística. La validación (450 pedidos) sirve para la parada temprana; el test se mira una sola vez al final (04-06).

9.2 Red, pérdida, optimizador y bucle

def crear_red(dropout=0.2):
    return nn.Sequential(
        nn.Linear(21, 16), nn.ReLU(), nn.Dropout(dropout),
        nn.Linear(16, 8),  nn.ReLU(), nn.Dropout(dropout),
        nn.Linear(8, 1))                              # SIN sigmoide: la pone BCEWithLogitsLoss

def evaluar(red, X_t, y_t, perdida_fn):
    red.eval()                                       # modo evaluación: desactiva el dropout
    with torch.no_grad():                            # sin grafo de cómputo: más rápido
        logits = red(X_t)
        L = perdida_fn(logits, y_t).item()
        prob = torch.sigmoid(logits).numpy().ravel() # ahora sí, sigmoide para leer probabilidades
    return L, roc_auc_score(y_t.numpy().ravel(), prob)

def entrenar(red, epocas=300, tasa=0.001, lote=64, paciencia=15, semilla=0):
    torch.manual_seed(semilla)
    cargador = DataLoader(TensorDataset(Xtr_t, ytr_t), batch_size=lote, shuffle=True)  # mini-lotes barajados
    perdida_fn = nn.BCEWithLogitsLoss()               # sigmoide + entropía cruzada binaria, estable
    opt = torch.optim.Adam(red.parameters(), lr=tasa)
    mejor_val, mejor_estado, sin_mejora, historial = float("inf"), None, 0, []
    for ep in range(1, epocas + 1):
        red.train()                                   # modo entrenamiento: dropout activo
        suma = 0.0
        for xb, yb in cargador:                       # 29 lotes por época
            opt.zero_grad()                           # 1. borrar gradientes del paso anterior
            L = perdida_fn(red(xb), yb)               # 2. adelante: predicción y pérdida del lote
            L.backward()                              # 3. atrás: retropropagación -> p.grad de cada parámetro
            opt.step()                                # 4. Adam actualiza los 497 parámetros
            suma += L.item() * len(xb)
        L_train = suma / len(Xtr_t)
        L_val, auc_val = evaluar(red, Xval_t, yval_t, perdida_fn)
        historial.append((ep, L_train, L_val, auc_val))
        if L_val < mejor_val - 1e-4:                  # ¿mejora la validación?
            mejor_val, sin_mejora = L_val, 0
            mejor_estado = {k: v.clone() for k, v in red.state_dict().items()}   # guardar los mejores pesos
        else:
            sin_mejora += 1
            if sin_mejora >= paciencia:               # parada temprana
                print(f"Parada temprana en la época {ep}; mejor época: {ep - paciencia}")
                break
    red.load_state_dict(mejor_estado)                 # recuperar los pesos de la mejor época
    return historial

torch.manual_seed(42)
red = crear_red(dropout=0.2)
historial = entrenar(red)
for ep, ltr, lval, auc in historial:
    if ep in (1, 2, 3, 5, 10, 20, 30, 40, 50, 60, 70, 75, 80, 90):
        print(f"época {ep:3d}  pérdida train {ltr:.4f}  pérdida val {lval:.4f}  AUC val {auc:.3f}")

L_test, auc_test = evaluar(red, Xte_t, yte_t, nn.BCEWithLogitsLoss())
print("Pérdida test:", round(L_test, 4), " AUC test:", round(auc_test, 3))

Salida (varía ligeramente con la semilla y la versión de PyTorch):

Parada temprana en la época 90; mejor época: 75
Época Pérdida train Pérdida val AUC val Comentario
1 0,7508 0,7316 0,544 Punto de partida: casi azar
2 0,7220 0,7087 0,474
3 0,6953 0,6765 0,397 Todavía "sabe" menos que una moneda
5 0,5529 0,5067 0,553 Empieza la bajada rápida
10 0,3909 0,3962 0,740
20 0,3556 0,3581 0,801
30 0,3431 0,3367 0,825
40 0,3385 0,3291 0,833
50 0,3335 0,3286 0,831 Se aplana
60 0,3282 0,3257 0,834
70 0,3185 0,3270 0,833
75 0,3226 0,3241 0,835 Mejor época (mínimo de validación)
80 0,3190 0,3248 0,836 La validación ya no mejora
90 0,3206 0,3248 0,835 15 épocas sin mejorar: parada
Pérdida test: 0.3324  AUC test: 0.834

Explicación de las piezas clave del bucle:

  • DataLoader(..., batch_size=64, shuffle=True) entrega los 1.799 pedidos en 29 mini-lotes distintos en cada época.
  • El trío zero_grad()backward()step() es el corazón: sin zero_grad() los gradientes se acumulan de un lote al siguiente (un error clásico); backward() es la retropropagación de la sección 5, automática; step() aplica la regla de Adam.
  • red.train() / red.eval() conmutan el dropout: apagado aleatorio al entrenar, todas las neuronas al evaluar. Por eso la pérdida de entrenamiento de la tabla es algo pesimista (se calcula con neuronas apagadas) y puede quedar por encima de la de validación en las primeras épocas.
  • La parada temprana ha elegido la época 75 mirando solo la validación; sin ella, ¿qué habría pasado? Lo verás en el ejercicio 2: siguiendo 300 épocas sin dropout, la pérdida de entrenamiento baja a 0,22-0,23 pero la de validación sube a 0,39-0,40 y el AUC de test cae por debajo de 0,78. Sobreajuste de manual.

9.3 Resultado y lectura para NovaMarket

El AUC en test es 0,834; repitiendo con cinco semillas distintas se obtiene entre 0,834 y 0,842 (media 0,838). La regresión logística de 04-05 daba 0,844 y el bosque ajustado de 04-06, 0,838. La red no gana: empata dentro del ruido con más complejidad, menos interpretabilidad y más decisiones que tomar. Marta se lo explica a Diego sin rodeos: para el predictor de devoluciones, datos tabulares con 2.249 ejemplos y una "verdad" bastante lineal, la logística sigue siendo la elección (04-06: a igualdad de rendimiento, el modelo más simple). Esto es lo esperable y conviene saberlo: en tabular pequeño, las redes rara vez baten a los modelos clásicos bien ajustados. La red se justifica donde ellos no llegan, y ahí es adonde vamos en 05-04 y 05-05: las fotos de incidencias y las reseñas.

Errores Comunes y Consejos

  • Olvidar opt.zero_grad(). Los gradientes se suman al paso anterior y el entrenamiento se descontrola. Es el error número uno al escribir el bucle a mano.
  • Sigmoide en la red y BCEWithLogitsLoss a la vez. Doble sigmoide: la red aprende mal y las "probabilidades" quedan comprimidas. O sigmoide + BCELoss, o nada + BCEWithLogitsLoss (recomendado).
  • No conmutar train()/eval(). Evaluar con dropout activo da resultados peores y aleatorios; entrenar en modo eval anula el dropout.
  • Tasa de aprendizaje a ojo. Si la pérdida no baja o da nan, es lo primero que hay que tocar (bajar). Si baja muy despacio, subir o usar Adam. Pruébala con validación como cualquier hiperparámetro.
  • Elegir el número de épocas mirando el test. Contamina la única mirada final (04-06). Para eso está la validación y la parada temprana.
  • Comparar con la logística en distinta división de datos. Nuestro test es exactamente el de 04-05 (misma semilla): solo así el 0,834 se puede comparar con el 0,844.
  • Concluir "las redes son malas" por este resultado. Es un problema tabular pequeño y casi lineal; el veredicto cambia con texto e imágenes (05-04, 05-05).

Ejercicios

Ejercicio 1. Calcula a mano un paso de descenso del gradiente para la neurona de la sección 5 con x = (1; 0; 2), y = 0 (no se devolvió), w = (0,5; −1; 0,8), b = −1 y η = 0,5: obtén z, p, la pérdida, ∂L/∂z, ∂L/∂w, ∂L/∂b, los nuevos parámetros y la nueva pérdida. Comprueba que ha bajado.

Ejercicio 2. Entrena la red de la sección 9 sin dropout (crear_red(0.0)) y sin parada temprana (paciencia=10**6, epocas=300), evaluando en modo eval la pérdida de entrenamiento, la de validación y el AUC de test en las épocas 10, 30, 50, 100, 200 y 300 (con evaluar, y sin recuperar el "mejor estado"). Describe la curva y explica en qué época deberías haber parado.

Ejercicio 3. En el código de la sección 8, cambia tasa a 0,01 y a 5 y a 20, y anota la pérdida en las épocas 1, 5, 20, 50 y 200. Clasifica cada tasa según la tabla de la sección 4 y explica por qué la pérdida con tasa = 20 no baja aunque tampoco explota.

Soluciones

Solución 1. z = 0,5·1 + (−1)·0 + 0,8·2 − 1 = 1,1; p = σ(1,1) = 0,750; pérdida L = −log(1 − 0,75) = 1,386 (da 75 % a una devolución que no ocurrió). ∂L/∂z = p − y = 0,75; ∂L/∂w = 0,75·x = (0,75; 0; 1,5); ∂L/∂b = 0,75. Nuevos parámetros: w = (0,5 − 0,375; −1 − 0; 0,8 − 0,75) = (0,125; −1; 0,05), b = −1 − 0,375 = −1,375. Nueva pasada: z = 0,125 + 0,1 − 1,375 = −1,15, p = 0,240, L = −log(0,76) = 0,275. La pérdida ha bajado de 1,386 a 0,275; el peso de la segunda columna no cambia porque su entrada era 0 (∂L/∂w₂ = 0,75·0).

Solución 2. En nuestra ejecución (los valores exactos varían con la semilla): época 10, train 0,36 / val 0,39 / AUC test 0,78; época 30, 0,30 / 0,33 / 0,83; época 50, 0,29 / 0,32 / 0,83; época 100, 0,27 / 0,33 / 0,82; época 200, 0,24-0,25 / 0,35 / 0,78-0,80; época 300, 0,22-0,23 / 0,39-0,40 / 0,73-0,78. La pérdida de entrenamiento baja sin parar (la red memoriza), la de validación toca fondo hacia la época 50 y luego sube, y el AUC de test la sigue: de 0,83 cae por debajo de 0,78. Habría que haber parado en torno a la época 50, que es justo lo que la parada temprana con paciencia hace sola. Es la curva de sobreajuste de 04-06 vista en el eje "épocas" en lugar de "complejidad".

Solución 3. tasa = 0,01: pérdidas 0,692; 0,686; 0,666; 0,629; 0,507. Baja siempre pero tras 200 épocas está lejos del 0,326: demasiado pequeña. tasa = 5: 0,345; 0,328; 0,326; 0,326; 0,326: converge en 20 épocas, adecuada (incluso mejor que 0,5 para este problema tan suave). tasa = 20: 0,587; 0,470; 0,600; 0,660; 0,609: demasiado grande, salta de un lado a otro del valle sin asentarse. No explota porque la pérdida logarítmica con sigmoide crece despacio (los saltos grandes en w saturan la sigmoide y la pendiente se hace pequeña, lo que frena el siguiente paso), a diferencia de la parábola de la sección 4, cuya pendiente crece con la distancia y sí diverge.

Conclusión

Hemos visto cómo aprende una red. La pérdida es un paisaje y, a diferencia del ascenso de colina de 03-04, ahora conocemos la pendiente: la derivada, y con varios parámetros el gradiente. El descenso del gradiente da pasos θ ← θ − η·∂L/∂θ (lo hemos seguido a mano en una parábola: 10 → 6,76 → 4,69 → ... ), la tasa de aprendizaje decide si el descenso es lento, adecuado o divergente, y la regla de la cadena permite calcular la pendiente de cada peso multiplicando pendientes locales hacia atrás por el grafo de cómputo: eso es la retropropagación, que PyTorch hace sola con backward(). Hemos entendido los mini-lotes y las épocas, qué mejoran el momento y Adam, y hemos cerrado la lista pendiente de 04-06 con la parada temprana, el dropout, el aumento de datos y la normalización por lotes. En código, hemos reproducido fit de la regresión logística con veinte líneas de numpy (pérdida 0,693 → 0,326) y hemos entrenado el MLP de NovaMarket en PyTorch con el bucle zero_gradbackwardstep: AUC en test 0,834, empatado con la logística (0,844) y el bosque (0,838). Para el predictor de devoluciones, Marta se queda con la logística.

Ese empate es la mejor introducción a la siguiente lección. Si las redes no ganan en las tablas de pedidos, ¿dónde ganan, y por qué? En Deep Learning y sus Aplicaciones veremos qué hace "profundo" al deep learning y por qué despegó en 2012, y las arquitecturas que incorporan la estructura de los datos: las convolucionales, que probaremos sobre las fotos de incidencias de NovaMarket (paquete dañado o correcto), las recurrentes para secuencias, los autoencoders para anomalías y la transferencia de aprendizaje, la idea de no entrenar desde cero.

Fundamentos de Inteligencia Artificial (IA)

Módulo 1: Introducción a la Inteligencia Artificial

Módulo 2: Principios Básicos de la IA

Módulo 3: Algoritmos en IA

Módulo 4: Aprendizaje Automático (Machine Learning)

Módulo 5: Redes Neuronales y Deep Learning

Módulo 6: Lógica y Sistemas Expertos

Módulo 7: Herramientas y Lenguajes de Programación en IA

Módulo 8: Proyectos y Casos de Estudio

Módulo 9: Ejercicios y Prácticas

Módulo 10: Recursos Adicionales

© Copyright 2026. Todos los derechos reservados