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
- La pérdida como paisaje: del ascenso de colina a la brújula
- La derivada como pendiente y la intuición del gradiente
- Descenso del gradiente paso a paso: un ejemplo de una variable a mano
- La tasa de aprendizaje: demasiado grande, demasiado pequeña
- Regla de la cadena y retropropagación
- Lotes, mini-lotes, épocas y optimizadores (SGD, momento, Adam)
- Regularización específica de redes: parada temprana, dropout, aumento de datos y normalización por lotes
- Código (a): descenso del gradiente desde cero en numpy
- Código (b): entrenamiento completo del MLP de NovaMarket en PyTorch
- Errores Comunes y Consejos
- Ejercicios
- Conclusión
- 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.
- 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.
- 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.
- 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.
- 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érdidaL = −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).
- 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.
- 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.
- 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.233Lectura: 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.
- 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):
| É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 |
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: sinzero_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
BCEWithLogitsLossa 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 modoevalanula 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_grad → backward → step: 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
- Conceptos Fundamentales: Agentes, Entornos y Racionalidad
- Tipos de Inteligencia Artificial
- Los Datos como Materia Prima de la IA
- Ética y Consideraciones en IA
Módulo 3: Algoritmos en IA
- Introducción a los Algoritmos
- Algoritmos de Búsqueda
- Búsqueda con Adversario: Juegos y Minimax
- Algoritmos de Optimización
Módulo 4: Aprendizaje Automático (Machine Learning)
- Conceptos Básicos de Machine Learning
- Tipos de Aprendizaje Automático
- Preparación de Datos y Características
- Algoritmos de Machine Learning
- Evaluación y Validación de Modelos
- Sobreajuste, Regularización y Ajuste de Hiperparámetros
Módulo 5: Redes Neuronales y Deep Learning
- Introducción a las Redes Neuronales
- Arquitectura de Redes Neuronales
- Cómo Aprende una Red: Descenso del Gradiente y Retropropagación
- Deep Learning y sus Aplicaciones
- Transformers, Grandes Modelos de Lenguaje e IA Generativa
Módulo 6: Lógica y Sistemas Expertos
- Lógica en IA
- Sistemas Expertos
- Razonamiento con Incertidumbre: Probabilidad y Redes Bayesianas
- Aplicaciones de Sistemas Expertos
Módulo 7: Herramientas y Lenguajes de Programación en IA
- Lenguajes de Programación para IA
- Python Científico: NumPy, pandas y Matplotlib
- Herramientas y Librerías Populares
- Entornos de Desarrollo
Módulo 8: Proyectos y Casos de Estudio
Módulo 9: Ejercicios y Prácticas
- Ejercicios de Algoritmos
- Prácticas de Machine Learning
- Proyectos de Redes Neuronales
- Proyecto Integrador: de la Idea al Prototipo
