20. Regularización y overfitting en Deep Learning

20.1 Introducción

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.

20.2 Qué es el overfitting

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.

20.3 Una analogía simple

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.

20.4 Por qué este problema es tan importante

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.

20.5 Señales típicas de overfitting

Una situación típica de overfitting se observa así:

  • La pérdida de entrenamiento baja mucho.
  • La accuracy de entrenamiento sube bastante.
  • La pérdida de validación deja de mejorar o incluso empeora.
  • La accuracy de validación se estanca o baja.

En otras palabras, el modelo sigue mejorando sobre entrenamiento, pero no mejora sobre datos nuevos.

20.6 Diferencia entre overfitting y underfitting

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í:

  • Underfitting: el modelo es insuficiente o está mal entrenado.
  • Overfitting: el modelo se ajusta demasiado al entrenamiento.

20.7 Por qué las redes neuronales pueden sobreajustar

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.

20.8 Factores que favorecen el overfitting

Algunas situaciones hacen más probable el overfitting:

  • Usar pocos datos de entrenamiento.
  • Entrenar durante demasiadas épocas.
  • Tener una red demasiado grande para el problema.
  • Trabajar con datos ruidosos.
  • No usar técnicas de regularización.

Estas condiciones no garantizan overfitting, pero sí aumentan el riesgo.

20.9 Qué es la regularización

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.

20.10 Intuición de la regularización

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”.

20.11 Tipos comunes de regularización

En Deep Learning existen varias técnicas de regularización. Entre las más conocidas están:

  • L2 o weight decay.
  • Dropout.
  • Early stopping.
  • Data augmentation.
  • Reducir la complejidad del modelo.

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.

20.12 Regularización L2 o weight decay

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.

20.13 Cómo se ve L2 en PyTorch

En PyTorch, una forma muy habitual de usar regularización L2 es mediante el parámetro weight_decay del optimizador.

optimizador = optim.Adam(modelo.parameters(), lr=0.01, weight_decay=0.001)

Ese valor no elimina el overfitting mágicamente, pero puede ayudar a controlar el crecimiento excesivo de los parámetros.

20.14 Dropout

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.

20.15 Intuición de Dropout

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.

20.16 Cómo se usa Dropout en PyTorch

En PyTorch, Dropout se agrega como una capa más del modelo:

self.dropout = nn.Dropout(p=0.3)

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.

20.17 Dropout en entrenamiento y evaluación

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.

20.18 Early stopping

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.

20.19 Data augmentation

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.

20.20 Reducir la complejidad del modelo

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.

20.21 Regularización no significa perfección

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.

20.22 Cómo detectar overfitting en la práctica

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:

  • Pérdida de entrenamiento.
  • Pérdida de validación.
  • Accuracy de entrenamiento.
  • Accuracy de validación.

Cuando ambas curvas se separan demasiado, suele ser una señal de advertencia.

20.23 Un patrón típico en las curvas

En muchos casos, el comportamiento típico es este:

entrenamiento: sigue mejorando
validacion: mejora al principio, luego se estanca o empeora

Ese patrón sugiere que el modelo sigue aprendiendo detalles del conjunto de entrenamiento, pero ya no está mejorando su capacidad de generalización.

20.24 Qué haremos en la aplicación del final

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:

  • Generar un dataset sintético sobre rendimiento académico.
  • Separarlo en entrenamiento y validación.
  • Entrenar un modelo sin regularización.
  • Entrenar otro modelo con Dropout y weight decay.
  • Comparar ambos resultados.

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.

20.25 Por qué usaremos un dataset de entrenamiento y otro de validación

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.

20.26 Qué esperamos ver

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.

20.27 Código completo para ejecutar

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.

20.28 Errores comunes al estudiar regularización

Al comenzar, es frecuente caer en algunas confusiones:

  • Creer que más épocas siempre significan mejor modelo.
  • Pensar que una accuracy muy alta en entrenamiento prueba que el modelo es bueno.
  • Olvidar usar model.eval() cuando hay Dropout.
  • Aplicar regularización sin mirar datos de validación.
  • Usar un modelo demasiado grande para un problema pequeño.

Entender estas trampas conceptuales ahorra mucho tiempo y evita interpretar mal los resultados.

20.29 Buenas prácticas para estudiantes

Si estás empezando, estas prácticas suelen ayudarte mucho:

  • Separar siempre entrenamiento y validación cuando quieras medir generalización.
  • Observar entrenamiento y validación al mismo tiempo.
  • No sacar conclusiones por una sola época aislada.
  • Probar cambios pequeños en la regularización.
  • Usar arquitecturas simples antes de pasar a modelos grandes.

Estas costumbres forman una base sólida para trabajar después con problemas más reales y más complejos.

20.30 Qué debes recordar de este tema

  • El overfitting ocurre cuando el modelo se ajusta demasiado al entrenamiento.
  • El objetivo real es generalizar bien a datos nuevos.
  • La regularización busca reducir el riesgo de sobreajuste.
  • weight_decay implementa una forma habitual de regularización L2.
  • Dropout apaga neuronas aleatoriamente durante el entrenamiento.
  • model.train() y model.eval() son especialmente importantes cuando hay Dropout.
  • Comparar entrenamiento y validación ayuda a detectar overfitting.
  • Una red demasiado compleja para pocos datos puede sobreajustar con facilidad.

20.31 Cierre conceptual

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.