En los temas anteriores vimos cómo construir, entrenar y evaluar modelos con PyTorch. A esta altura ya sabemos definir una red, elegir una pérdida, usar un optimizador y medir métricas.
Sin embargo, cuando comenzamos a entrenar modelos de verdad aparece un problema muy importante: el modelo puede aprender demasiado bien los datos de entrenamiento y, aun así, funcionar mal con datos nuevos.
Ese problema se llama overfitting. Comprenderlo bien es fundamental, porque en Deep Learning no alcanza con que la red funcione sobre los ejemplos que ya vio: necesitamos que generalice.
Decimos que un modelo tiene overfitting cuando se ajusta excesivamente a los datos de entrenamiento.
Eso significa que aprende no solo los patrones útiles, sino también detalles accidentales, ruido o particularidades que no representan la regla general del problema.
Como consecuencia, el modelo puede mostrar un rendimiento excelente en entrenamiento y un rendimiento bastante peor en validación o prueba.
Imagina un estudiante que en lugar de comprender una materia se memoriza exactamente las respuestas de un conjunto fijo de ejercicios.
Si en el examen aparecen esas mismas preguntas, le irá muy bien. Pero si cambian un poco, probablemente falle.
Con el overfitting ocurre algo parecido: el modelo “memoriza” demasiado el conjunto de entrenamiento y pierde flexibilidad para responder bien ante casos nuevos.
El objetivo real de un modelo no es repetir correctamente los datos ya vistos, sino comportarse bien con datos nuevos.
Por eso, en Machine Learning y Deep Learning la palabra clave es generalización.
Si un modelo tiene overfitting, puede dar la falsa impresión de ser muy bueno cuando en realidad solo está reproduciendo el conjunto con el que fue entrenado.
Una situación típica de overfitting se observa así:
En otras palabras, el modelo sigue mejorando sobre entrenamiento, pero no mejora sobre datos nuevos.
Es importante no confundir overfitting con underfitting.
El underfitting ocurre cuando el modelo ni siquiera logra aprender bien el conjunto de entrenamiento. En ese caso suele verse un rendimiento pobre tanto en entrenamiento como en validación.
Podemos resumirlo así:
Las redes neuronales tienen mucha capacidad para representar relaciones complejas. Eso es una gran ventaja, pero también implica un riesgo.
Si el modelo tiene muchos parámetros y el conjunto de datos es pequeño o ruidoso, la red puede empezar a capturar detalles que no conviene aprender.
Cuanta más capacidad tiene un modelo, más cuidado necesitamos con la regularización, la validación y el diseño del entrenamiento.
Algunas situaciones hacen más probable el overfitting:
Estas condiciones no garantizan overfitting, pero sí aumentan el riesgo.
La regularización es el conjunto de estrategias que usamos para reducir el riesgo de overfitting.
La idea general es ayudar al modelo a aprender patrones útiles sin volverse excesivamente dependiente del conjunto de entrenamiento.
No se trata de “arruinar” el aprendizaje, sino de volverlo más robusto y más estable.
Podemos pensar la regularización como una forma de imponer cierta disciplina al modelo.
En lugar de permitirle cualquier ajuste posible, le ponemos límites o condiciones para que no elija soluciones demasiado particulares.
Eso suele mejorar su capacidad de generalización, aunque a veces haga que el entrenamiento puro sea un poco menos “perfecto”.
En Deep Learning existen varias técnicas de regularización. Entre las más conocidas están:
En este tema nos concentraremos sobre todo en las más fáciles de entender y aplicar al comenzar: L2, Dropout y la observación del rendimiento de validación.
La regularización L2 agrega una penalización a los pesos grandes del modelo.
La idea intuitiva es que, si el modelo necesita pesos exageradamente grandes para ajustarse a los datos, quizá esté encontrando una solución demasiado específica.
Al penalizar esos pesos grandes, empujamos al modelo hacia soluciones más moderadas y, muchas veces, más generalizables.
En PyTorch, una forma muy habitual de usar regularización L2 es mediante el parámetro weight_decay del optimizador.
Ese valor no elimina el overfitting mágicamente, pero puede ayudar a controlar el crecimiento excesivo de los parámetros.
Otra técnica muy conocida es Dropout. Durante el entrenamiento, Dropout apaga aleatoriamente una parte de las neuronas de una capa.
Eso obliga a la red a no depender demasiado de un camino específico de activaciones.
Como resultado, el modelo suele volverse menos frágil y menos propenso a memorizar detalles particulares.
Imagina que en cada iteración una parte de las neuronas “falta” temporalmente.
La red no puede confiar siempre en las mismas conexiones y se ve obligada a distribuir mejor la información.
Esta idea suele mejorar la robustez del modelo, especialmente en redes con bastante capacidad.
En PyTorch, Dropout se agrega como una capa más del modelo:
Luego, dentro de forward, esa capa se aplica como cualquier otra.
El parámetro p indica la proporción aproximada de neuronas que se apagarán durante el entrenamiento.
Aquí aparece una idea muy importante: Dropout no debe comportarse igual en entrenamiento que en evaluación.
Durante el entrenamiento se apagan neuronas aleatoriamente. Durante la evaluación, en cambio, esa aleatoriedad debe desaparecer.
Por eso es tan importante usar:
model.train() al entrenar.model.eval() al evaluar.Otra estrategia muy usada es early stopping, es decir, detener el entrenamiento cuando el rendimiento de validación deja de mejorar.
La idea es sencilla: si el modelo ya no mejora sobre datos nuevos y sigue mejorando solo sobre entrenamiento, conviene parar.
Esto no reemplaza a otras técnicas, pero suele ser muy útil como mecanismo práctico de control.
En tareas como visión por computadora, una técnica muy común es data augmentation.
Consiste en generar variantes razonables de los datos de entrenamiento: por ejemplo, rotar una imagen, desplazarla, reflejarla o cambiar levemente su brillo.
De ese modo, el modelo ve más variedad y tiene menos oportunidades de memorizar exactamente los ejemplos originales.
A veces la solución no está en agregar más técnicas, sino en hacer el modelo más simple.
Si una red tiene muchas capas o muchas neuronas para un problema pequeño, puede tener capacidad excesiva.
En esos casos, reducir el tamaño del modelo puede ser una forma muy efectiva de combatir el overfitting.
Es importante entender que regularizar no garantiza automáticamente un buen modelo.
Si el dataset está mal construido, si la arquitectura es inadecuada o si los hiperparámetros son malos, la regularización por sí sola no resolverá todo.
La regularización es una ayuda importante, pero debe formar parte de un proceso de diseño y evaluación más amplio.
La forma más habitual de detectar overfitting es comparar entrenamiento y validación a lo largo del tiempo.
Por ejemplo, podemos observar en cada época:
Cuando ambas curvas se separan demasiado, suele ser una señal de advertencia.
En muchos casos, el comportamiento típico es este:
Ese patrón sugiere que el modelo sigue aprendiendo detalles del conjunto de entrenamiento, pero ya no está mejorando su capacidad de generalización.
Para que esta idea no quede solo en teoría, al final del tema veremos una aplicación completa.
En esa aplicación haremos lo siguiente:
El objetivo no es construir un sistema complejo, sino que el estudiante pueda ver con claridad cómo aparece el overfitting y cómo la regularización puede ayudar.
En este tema sí conviene separar los datos, porque justamente queremos observar el problema de generalización.
Si evaluáramos usando exactamente el mismo conjunto con el que entrenamos, sería mucho más difícil detectar el overfitting.
Por eso la aplicación final tendrá dos conjuntos diferentes: uno para aprender y otro para medir qué tanto generaliza la red.
En el modelo sin regularización, es posible que el entrenamiento se vuelva muy bueno, pero que la validación no acompañe del todo.
En el modelo regularizado, en cambio, puede ocurrir que el entrenamiento puro sea un poco menos perfecto, pero que la validación resulte más estable o más cercana.
Esa comparación es justamente la esencia práctica de este tema.
El siguiente script trabaja con un problema más interesante: predecir si un estudiante aprobará o no aprobará una materia a partir de seis variables.
Las variables serán: horas de estudio, asistencia, promedio de tareas, nota del parcial 1, nota del parcial 2 y nivel de distracción con el celular.
Con esos datos se entrenan dos modelos. Uno se entrena sin regularización y el otro con Dropout y weight decay. Al final se comparan pérdidas y accuracies de entrenamiento y validación.
import torch
import torch.nn as nn
import torch.optim as optim
# Fijamos la semilla para poder repetir el experimento.
torch.manual_seed(12)
def generar_datos(n):
# Generamos variables que representan el perfil de cada estudiante.
horas_estudio = 10 * torch.rand(n, 1)
asistencia = 0.5 + 0.5 * torch.rand(n, 1)
tareas = torch.rand(n, 1)
parcial_1 = 10 * torch.rand(n, 1)
parcial_2 = 10 * torch.rand(n, 1)
distraccion = torch.rand(n, 1)
# Armamos la matriz final de entradas normalizando algunas variables.
X = torch.cat([
horas_estudio,
asistencia,
tareas,
parcial_1 / 10.0,
parcial_2 / 10.0,
distraccion
], dim=1)
# Definimos una regla oculta que mezcla varios factores positivos y negativos.
puntaje = (
0.35 * horas_estudio +
4.0 * asistencia +
3.5 * tareas +
0.45 * parcial_1 +
0.55 * parcial_2 -
2.8 * distraccion +
1.2 * (horas_estudio * tareas) -
1.5 * (distraccion * tareas)
)
# Agregamos ruido para que el problema no sea demasiado perfecto.
ruido = 1.8 * torch.randn(n, 1)
y = ((puntaje + ruido) > 9.5).float()
return X, y
# Separamos los datos en entrenamiento y validacion.
X_train, y_train = generar_datos(120)
X_val, y_val = generar_datos(400)
class RedSinRegularizacion(nn.Module):
def __init__(self):
super().__init__()
# Red con bastante capacidad y sin tecnicas explicitas de regularizacion.
self.net = nn.Sequential(
nn.Linear(6, 128),
nn.ReLU(),
nn.Linear(128, 128),
nn.ReLU(),
nn.Linear(128, 64),
nn.ReLU(),
nn.Linear(64, 1)
)
def forward(self, x):
return self.net(x)
class RedConRegularizacion(nn.Module):
def __init__(self):
super().__init__()
# La arquitectura es parecida, pero agregamos Dropout entre capas.
self.net = nn.Sequential(
nn.Linear(6, 128),
nn.ReLU(),
nn.Dropout(p=0.35),
nn.Linear(128, 128),
nn.ReLU(),
nn.Dropout(p=0.35),
nn.Linear(128, 64),
nn.ReLU(),
nn.Dropout(p=0.20),
nn.Linear(64, 1)
)
def forward(self, x):
return self.net(x)
def accuracy_desde_logits(logits, y_real):
# Convertimos logits a probabilidades y luego a clases 0/1.
probs = torch.sigmoid(logits)
pred = (probs >= 0.5).float()
return (pred == y_real).float().mean().item()
def entrenar_modelo(modelo, usar_regularizacion):
criterio = nn.BCEWithLogitsLoss()
# Si activamos regularizacion, usamos weight_decay en el optimizador.
if usar_regularizacion:
optimizador = optim.Adam(modelo.parameters(), lr=0.01, weight_decay=0.002)
else:
optimizador = optim.Adam(modelo.parameters(), lr=0.01)
for epoca in range(350):
# Modo entrenamiento: aqui Dropout queda activo.
modelo.train()
logits_train = modelo(X_train)
loss_train = criterio(logits_train, y_train)
optimizador.zero_grad()
loss_train.backward()
optimizador.step()
if (epoca + 1) % 50 == 0:
# Modo evaluacion: aqui Dropout se desactiva.
modelo.eval()
with torch.no_grad():
logits_train_eval = modelo(X_train)
logits_val = modelo(X_val)
loss_train_eval = criterio(logits_train_eval, y_train).item()
loss_val = criterio(logits_val, y_val).item()
acc_train = accuracy_desde_logits(logits_train_eval, y_train)
acc_val = accuracy_desde_logits(logits_val, y_val)
print(f"Epoca {epoca+1:3d} | train loss={loss_train_eval:.4f} | val loss={loss_val:.4f} | train acc={acc_train:.3f} | val acc={acc_val:.3f}")
# Al final devolvemos una comparacion resumida entre entrenamiento y validacion.
modelo.eval()
with torch.no_grad():
logits_train = modelo(X_train)
logits_val = modelo(X_val)
loss_train = criterio(logits_train, y_train).item()
loss_val = criterio(logits_val, y_val).item()
acc_train = accuracy_desde_logits(logits_train, y_train)
acc_val = accuracy_desde_logits(logits_val, y_val)
return loss_train, loss_val, acc_train, acc_val
print("MODELO SIN REGULARIZACION")
modelo1 = RedSinRegularizacion()
resultados1 = entrenar_modelo(modelo1, usar_regularizacion=False)
print()
print("MODELO CON DROPOUT Y WEIGHT DECAY")
modelo2 = RedConRegularizacion()
resultados2 = entrenar_modelo(modelo2, usar_regularizacion=True)
# Mostramos una comparacion final entre ambos enfoques.
print()
print("RESUMEN FINAL")
print("Sin regularizacion:")
print(f"train loss={resultados1[0]:.4f} | val loss={resultados1[1]:.4f} | train acc={resultados1[2]:.3f} | val acc={resultados1[3]:.3f}")
print("Con regularizacion:")
print(f"train loss={resultados2[0]:.4f} | val loss={resultados2[1]:.4f} | train acc={resultados2[2]:.3f} | val acc={resultados2[3]:.3f}")
# Probamos el modelo regularizado con estudiantes nuevos no vistos en entrenamiento.
ejemplo_nuevo = torch.tensor([
[0.90, 0.95, 0.90, 0.80, 0.90, 0.10],
[0.20, 0.60, 0.30, 0.40, 0.35, 0.85],
[0.65, 0.88, 0.75, 0.55, 0.70, 0.20]
], dtype=torch.float32)
with torch.no_grad():
probs = torch.sigmoid(modelo2(ejemplo_nuevo))
preds = (probs >= 0.5).float()
print()
print("Predicciones con el modelo regularizado:")
print(probs)
print(preds)
Este ejemplo resulta más interesante porque el modelo no toma solo dos variables simples, sino una combinación de factores que se parecen más a un problema tabular real. Aun así, mantiene el objetivo pedagógico central del tema: mostrar cómo la regularización puede mejorar la generalización.
Al comenzar, es frecuente caer en algunas confusiones:
model.eval() cuando hay Dropout.Entender estas trampas conceptuales ahorra mucho tiempo y evita interpretar mal los resultados.
Si estás empezando, estas prácticas suelen ayudarte mucho:
Estas costumbres forman una base sólida para trabajar después con problemas más reales y más complejos.
weight_decay implementa una forma habitual de regularización L2.model.train() y model.eval() son especialmente importantes cuando hay Dropout.Regularización y overfitting son dos ideas inseparables en Deep Learning. Cada vez que entrenamos una red, no solo debemos preguntarnos si aprende, sino también cómo está aprendiendo y si ese aprendizaje sirve fuera del conjunto de entrenamiento.
Comprender esta diferencia marca un paso importante en la formación de cualquier estudiante, porque obliga a mirar al modelo con más criterio y menos ingenuidad.