Sobreajuste: cómo detectarlo y qué hacer cuando tu modelo memoriza

Has entrenado tu primer modelo y la pérdida en entrenamiento es baja. Estupendo, ¿no? Depende. La pregunta que importa es otra: ¿ha aprendido algo que sirva con datos nuevos, o se ha limitado a memorizar los que le diste?

Qué significa que tu modelo memorice

Imagina a alguien que se prepara un examen aprendiéndose de memoria las respuestas del examen del año pasado. Si el examen de este año es idéntico, sacará un diez. Si cambia una sola pregunta, se hunde. No ha aprendido la materia: ha memorizado el examen.

Eso es el sobreajuste (overfitting). La documentación de scikit-learn lo plantea con un caso extremo: un modelo que solo repitiera las etiquetas de las muestras que ya ha visto tendría una puntuación perfecta y, aun así, no predeciría nada útil con datos nuevos. La misma página califica de error metodológico aprender los parámetros de una función de predicción y probarla con los mismos datos.

De ahí sale la regla de oro: para saber si generaliza, tienes que medirlo con datos que no ha visto al entrenar. No voy a repetir aquí cómo se ajustan los pesos; si necesitas repasar el descenso de gradiente, lo tienes en la guía de redes neuronales para principiantes. Aquí nos interesa qué hacer cuando ese ajuste se pasa de rosca.

Separar los datos: entrenamiento, validación y test

La práctica habitual es reservar una parte de los datos como conjunto de test, según la guía de validación cruzada de scikit-learn. Pero con eso solo no basta, y el motivo es sutil.

Si pruebas varias configuraciones (número de capas, tasa de aprendizaje, lo que sea) y te quedas con la que mejor puntúa en test, el conocimiento del test se «filtra» al modelo. Sin darte cuenta, has ajustado tu elección a esos datos, y las métricas dejan de reflejar la capacidad real de generalización.

La solución son tres conjuntos:

  • Entrenamiento: con él se ajustan los pesos.
  • Validación: con él se comparan configuraciones y se decide cuándo parar.
  • Test: se usa una sola vez al final, cuando el experimento parece bueno, para la evaluación definitiva.

Un aviso para más adelante: si tus datos vienen agrupados (por ejemplo, varias muestras de cada persona), un modelo lo bastante flexible puede aprender rasgos propios de cada sujeto y fallar con sujetos nuevos. Scikit-learn ofrece GroupKFold, que evita que un mismo grupo aparezca a la vez en entrenamiento y test y permite detectar ese tipo de sobreajuste. Basta con que lo tengas en el radar.

Cómo se ve el sobreajuste en las curvas de pérdida

Mientras entrenas, registra la pérdida en entrenamiento y en validación al final de cada época (una pasada completa por los datos de entrenamiento) y dibuja las dos. El patrón típico es este:

Esquema ilustrativo sin valores reales. La curva de entrenamiento baja durante todas las épocas. La curva de validación baja al principio, alcanza un mínimo hacia la mitad, y después vuelve a subir, de modo que aumenta la distancia entre ambas curvas.mínimo de validaciónépocaspérdidaentrenamientovalidación

Este comportamiento está descrito en un artículo sobre parada temprana: en modelos de alta capacidad, la pérdida de entrenamiento baja durante todo el proceso, mientras que la de test se satura en algún punto y vuelve a aumentar.

Para leer la gráfica fíjate en dos señales:

  1. El punto en que la validación deja de mejorar. A partir de ahí, lo que el modelo sigue ganando en entrenamiento ya no se traduce en nada útil.
  2. La distancia creciente entre las dos curvas. Que haya cierta diferencia es normal; lo que debe ponerte en alerta es que se ensanche mientras la validación se estanca o empeora.

Una precisión honesta: las fuentes consultadas no fijan un umbral numérico de «demasiada diferencia». Es un juicio que se hace mirando la forma de las curvas, no una cifra mágica. Y el dibujo de arriba es un esquema, no datos de un experimento real.

Un ejemplo mínimo para verlo con tus propios ojos

No hace falta una red para ver el fenómeno. Un polinomio de grado alto ajustado a pocos puntos con ruido es un modelo con mucha capacidad, y se comporta igual. Este código usa solo NumPy:

import numpy as np

rng = np.random.default_rng(0)

# Pocos datos: una curva suave más ruido
x = np.linspace(-1, 1, 30)
y = np.sin(3 * x) + rng.normal(0, 0.2, size=x.shape)

# Mezclamos y separamos: 20 para entrenar, 10 para validar
orden = rng.permutation(len(x))
idx_train, idx_val = orden[:20], orden[20:]

def error_cuadratico_medio(coef, idx):
    prediccion = np.polyval(coef, x[idx])
    return np.mean((prediccion - y[idx]) ** 2)

# Más grado = más capacidad para pegarse a los datos
errores_val = {}
for grado in (1, 3, 5, 9, 15):
    coef = np.polyfit(x[idx_train], y[idx_train], grado)
    err_train = error_cuadratico_medio(coef, idx_train)
    err_val = error_cuadratico_medio(coef, idx_val)
    print(f"grado {grado:2d} | train {err_train:.4f} | val {err_val:.4f}")
    errores_val[grado] = err_val
assert errores_val[15] > errores_val[5], "el grado 15 debería sobreajustar"

Paso a paso:

  • np.random.default_rng(0) fija la semilla para que el resultado sea reproducible.
  • Generamos 30 puntos de una curva seno con ruido y los repartimos al azar: 20 para entrenar y 10 para validar. El modelo nunca ajusta con los de validación.
  • np.polyfit encuentra los coeficientes del polinomio que mejor se ajusta a los datos de entrenamiento; np.polyval lo evalúa.
  • El bucle sube el grado, es decir, la capacidad del modelo, y mide el error en las dos particiones.

Ejecútalo y observa. Lo que se espera, de forma cualitativa, es que el error de entrenamiento siga bajando al subir el grado, mientras que el de validación mejore al principio y acabe empeorando con los grados más altos. Con tan pocos datos, el detalle depende de la semilla y del reparto; si no lo ves a la primera, cambia la semilla o reduce los datos de entrenamiento. El modelo con grado 1 es el caso contrario: demasiado simple para captar la curva (infraajuste).

Qué hacer: regularización L1, L2 y dropout

Una defensa habitual es añadir a la función que se minimiza un término que penaliza el vector de parámetros w, normalmente con la norma l1 o l2, y que limita su magnitud, como explican los autores del artículo sobre parada temprana. La idea es que los pesos enormes son la forma típica de pegarse al ruido; si encarecen, el modelo tiende a soluciones más moderadas.

  • L2 suma un coste proporcional al cuadrado de los pesos. En redes neuronales también se llama weight decay, y el tutorial de TensorFlow sobre sobreajuste afirma que es matemáticamente exactamente lo mismo.
  • L1 empuja los pesos hacia exactamente cero y favorece un modelo disperso (sparse). L2 penaliza los pesos sin volverlos dispersos, porque su penalización tiende a cero para pesos pequeños; el tutorial lo cita como una de las razones por las que L2 es más común.

Si usas PyTorch, ojo con esto: torch.optim.Adam tiene weight_decay=0 por defecto, así que no se aplica ninguna penalización L2 a menos que la actives:

import torch

# 'modelo' es tu red; el valor de weight_decay lo eliges tú
optimizador = torch.optim.Adam(modelo.parameters(), lr=1e-3, weight_decay=1e-4)

La documentación indica además que con decoupled_weight_decay=True el optimizador equivale a AdamW. No hay un valor de weight_decay que valga para todo: es un hiperparámetro, y se elige comparando curvas de validación.

Dropout

El dropout ataca el problema desde otro lado. Durante el entrenamiento, pone a cero al azar una parte de las salidas de una capa; la tasa suele fijarse entre 0,2 y 0,5. En el test no se descarta nada, y los valores de salida de la capa se escalan para compensar que hay más unidades activas que durante el entrenamiento.

La idea clave, según el artículo original de Srivastava, Hinton y colaboradores, es que al descartar unidades al azar se evita que se co-adapten demasiado, es decir, que dependan unas de otras de forma rígida. Los autores indican que mejora el rendimiento en tareas supervisadas de visión, reconocimiento de voz, clasificación de documentos y biología computacional.

Tiene un precio: una red con dropout suele tardar de 2 a 3 veces más en entrenarse que una estándar de la misma arquitectura, en gran parte porque las actualizaciones de los parámetros son muy ruidosas. Es una herramienta con ventajas y costes, no un interruptor que arregla el sobreajuste.

Parada temprana: dejar de entrenar a tiempo

Si la validación empieza a empeorar, la solución más directa es dejar de entrenar. La parada temprana (early stopping) detiene el optimizador cuando se cumple un criterio definido por ti. Los autores del artículo citado consideran el «patrón oro» monitorizar la pérdida en un conjunto de validación, con un inconveniente: te deja menos datos efectivos para entrenar.

El criterio más común usa la paciencia (patience): el número de épocas seguidas sin mejora que toleras antes de parar. Un trabajo de 2024 sobre sobreajuste describe que, al parar, se devuelve la época con menor pérdida de validación. La lógica cabe en una función:

def parada_temprana(perdidas_val, paciencia):
    """Recorre las pérdidas de validación época a época.
    Devuelve (época en que se para, época del mejor modelo)."""
    mejor_epoca = 0
    mejor_perdida = float("inf")
    for epoca, perdida in enumerate(perdidas_val):
        if perdida < mejor_perdida:
            mejor_perdida = perdida
            mejor_epoca = epoca
        elif epoca - mejor_epoca >= paciencia:
            return epoca, mejor_epoca
    return len(perdidas_val) - 1, mejor_epoca
print(parada_temprana([1.0, 0.8, 0.7, 0.72, 0.75, 0.74, 0.8], paciencia=3))
assert parada_temprana([1.0, 0.8, 0.7, 0.72, 0.75, 0.74, 0.8], paciencia=3) == (5, 2)

Guarda la mejor pérdida vista y su época; si pasan paciencia épocas sin superarla, corta y te dice con qué época quedarte. En un entrenamiento real, además, guardarías los pesos de esa mejor época para restaurarlos.

La paciencia no es un detalle menor. En el ejemplo de la Figura 2a de ese trabajo, con paciencia de 20 épocas el entrenamiento se detiene en la época 70 y se devuelve la 50; con paciencia de 40 se detiene en la época 140, pero devuelve la 100, que tiene una pérdida menor. Es una curva concreta de ese artículo, no una regla general, pero ilustra que el valor que elijas cambia el modelo que obtienes.

Si trabajas con Keras, ya existe el callback tf.keras.callbacks.EarlyStopping, que el tutorial de TensorFlow usa para evitar entrenamientos largos e innecesarios. El mismo trabajo de 2024 señala que la implementación de TensorFlow es la más extendida y que PyTorch Ignite también incluye la suya.

Errores frecuentes

  • Evaluar con los datos de entrenamiento y celebrar la puntuación. Es justo el error metodológico que describe scikit-learn.
  • Ajustar hiperparámetros mirando el test. Cada decisión basada en él lo contamina; para eso está la validación.
  • Dar por hecho que hay regularización. En torch.optim.Adam, weight_decay vale 0 por defecto.
  • Elegir la paciencia a ciegas. Demasiado corta puede cortar antes de tiempo; demasiado larga, desperdiciar épocas. Mira la curva.
  • Olvidar el coste de la validación. Apartar datos para vigilar la pérdida es útil, pero deja menos para entrenar.
  • Mezclar grupos entre entrenamiento y test. Si varias muestras vienen de la misma persona o fuente, repártelas por grupos (GroupKFold), o tu validación será demasiado optimista.

Para practicar

  1. Coge el ejemplo del polinomio y dibuja con Matplotlib el error de entrenamiento y el de validación frente al grado. Busca el grado donde la validación toca fondo.
  2. Reduce a 12 los puntos de entrenamiento y repite. ¿Se adelanta el sobreajuste?
  3. Si montas una red pequeña, entrénala sin ninguna defensa, guarda las dos pérdidas por época y compara con otra versión que active primero L2 (weight_decay) y después dropout o parada temprana. Compara la forma de las curvas, no solo el último número.
  4. Reto extra: pasa a parada_temprana la lista de pérdidas de validación que hayas guardado, con paciencias de 5, 10 y 20, y mira en qué época se queda el mejor modelo.

Para entender el origen del dropout y la investigación alrededor, el siguiente paso natural son los artículos de Kia, que cuenta lo que se ha descubierto mientras yo me quedo con cómo funciona por dentro. Y si quieres reforzar la base, la introducción al aprendizaje profundo te sitúa en dónde encaja todo esto.

Fuentes

Imagen: geralt en Pixabay

Deja un comentario

Resumen de privacidad

Esta web utiliza cookies para que podamos ofrecerte la mejor experiencia de usuario posible. La información de las cookies se almacena en tu navegador y realiza funciones tales como reconocerte cuando vuelves a nuestra web o ayudar a nuestro equipo a comprender qué secciones de la web encuentras más interesantes y útiles.