21. Dropout y Batch Normalization

21.1 Introducción

En el tema anterior estudiamos el problema del overfitting y vimos que la regularización es una parte importante del trabajo con redes neuronales.

En este tema nos detendremos en dos herramientas muy conocidas dentro de Deep Learning: Dropout y Batch Normalization.

Ambas aparecen con muchísima frecuencia en modelos reales, pero cumplen papeles distintos. Entender esa diferencia es muy importante para no usarlas como si fueran lo mismo.

21.2 Por qué conviene estudiar estas dos técnicas juntas

Dropout y Batch Normalization suelen aparecer en redes modernas y muchas veces el estudiante escucha sus nombres casi como “palabras obligatorias” del área.

Sin embargo, no basta con saber que existen: hay que comprender qué hacen, por qué ayudan y cómo cambian entre entrenamiento y evaluación.

Además, en PyTorch ambas dependen mucho del modo del modelo: train() y eval().

21.3 Recordatorio: qué es Dropout

Dropout es una técnica que durante el entrenamiento apaga aleatoriamente una parte de las neuronas.

La idea central es evitar que la red dependa demasiado de combinaciones fijas de activaciones.

Esto introduce una forma de ruido controlado que puede ayudar a mejorar la generalización.

21.4 Recordatorio: qué es Batch Normalization

Batch Normalization, a menudo abreviada como BatchNorm, es una técnica que normaliza activaciones intermedias dentro de la red.

En términos simples, intenta mantener las salidas de ciertas capas en rangos más estables durante el entrenamiento.

Eso suele ayudar a que el aprendizaje sea más estable y, en muchos casos, más rápido.

21.5 No cumplen exactamente la misma función

Aunque ambas técnicas suelen mejorar el comportamiento del modelo, no cumplen el mismo rol principal.

  • Dropout está más asociado a regularización.
  • BatchNorm está más asociado a estabilizar y facilitar el entrenamiento.

Esto no significa que una no afecte a la otra. En la práctica, BatchNorm también puede influir indirectamente en la generalización, pero su idea de base no es la misma que la de Dropout.

21.6 Intuición de Dropout

Cuando usamos Dropout, en cada pasada de entrenamiento algunas neuronas quedan temporalmente desactivadas.

Eso obliga a la red a repartir mejor la información y evita que unas pocas neuronas se vuelvan “demasiado indispensables”.

El resultado esperado es una red más robusta, menos inclinada a memorizar patrones demasiado específicos.

21.7 Intuición de Batch Normalization

BatchNorm se basa en una idea distinta: si las activaciones internas cambian demasiado de una etapa a otra del entrenamiento, optimizar la red se vuelve más difícil.

Al normalizar esas activaciones por lote, se busca que las capas siguientes reciban datos en una escala más controlada.

Eso puede hacer que el descenso del gradiente trabaje en condiciones más cómodas.

21.8 Qué significa “normalizar” en este contexto

Normalizar, aquí, significa transformar los valores para que tengan una distribución más estable dentro del lote actual.

En BatchNorm, esto suele implicar usar la media y la desviación estándar del batch para centrar y escalar las activaciones.

Luego, además, BatchNorm introduce parámetros aprendibles que permiten ajustar nuevamente esa escala y ese desplazamiento.

21.9 Por qué BatchNorm puede acelerar el entrenamiento

Si las activaciones internas se vuelven demasiado grandes o demasiado pequeñas, entrenar puede resultar inestable.

BatchNorm ayuda a controlar ese fenómeno, y por eso a menudo permite usar tasas de aprendizaje más cómodas o converger con mayor facilidad.

No siempre “acelera” en tiempo absoluto, pero sí suele hacer más suave y estable el proceso de optimización.

21.10 Dropout durante entrenamiento

Durante el entrenamiento, Dropout actúa de manera aleatoria.

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

Si p=0.3, aproximadamente un 30% de las activaciones de esa capa se anulan en cada paso de entrenamiento.

Por eso dos pasadas sucesivas sobre el mismo lote no serán exactamente iguales mientras el modelo esté en modo entrenamiento.

21.11 Dropout durante evaluación

En evaluación, Dropout debe dejar de apagar neuronas.

Es decir, la red debe usar toda su capacidad disponible para producir predicciones estables.

Por eso necesitamos llamar a model.eval() antes de medir el desempeño del modelo.

21.12 BatchNorm durante entrenamiento

Cuando el modelo está en modo entrenamiento, BatchNorm usa las estadísticas del lote actual.

Eso significa que calcula media y variabilidad a partir de los datos que están entrando en ese momento.

Además, va acumulando una estimación de estadísticas globales para poder usarlas después en evaluación.

21.13 BatchNorm durante evaluación

En modo evaluación, BatchNorm ya no debe depender del batch actual del mismo modo que durante el entrenamiento.

En su lugar, usa las estadísticas acumuladas durante la fase de entrenamiento.

Esto permite que la salida sea más estable y no dependa de forma tan directa de la composición exacta del lote evaluado.

21.14 Por qué train y eval son tan importantes aquí

En muchos temas anteriores, usar model.train() y model.eval() parecía una formalidad útil. Aquí ya no es una formalidad: es esencial.

Con Dropout y BatchNorm, olvidar cambiar el modo del modelo puede alterar seriamente el comportamiento de la red.

Por eso este tema es ideal para entender por qué PyTorch distingue claramente ambas fases.

21.15 Cómo se escribe BatchNorm en PyTorch

La forma exacta depende del tipo de capa y de la estructura de los datos.

En redes totalmente conectadas, una opción común es nn.BatchNorm1d:

self.bn1 = nn.BatchNorm1d(64)

Ese 64 indica el número de características o neuronas que se normalizarán en esa parte de la red.

21.16 Orden típico de capas

Una secuencia común en redes densas puede verse así:

Linear - BatchNorm - ReLU - Dropout

No es la única posibilidad, pero es una organización bastante habitual.

La razón es que primero se transforma linealmente, luego se estabiliza la activación, después se aplica la no linealidad y, por último, si hace falta, se regulariza con Dropout.

21.17 Dropout y BatchNorm pueden convivir

Sí, ambas técnicas pueden usarse juntas.

De hecho, en muchos modelos aparece una combinación de BatchNorm para estabilizar el entrenamiento y Dropout para introducir regularización adicional.

Sin embargo, eso no significa que siempre haya que usar ambas. Depende del problema, del tamaño del dataset y de la arquitectura.

21.18 ¿Siempre conviene usar Dropout?

No siempre.

En algunos problemas pequeños puede ayudar bastante, pero en otros puede volver el entrenamiento más difícil o innecesariamente ruidoso.

Como regla práctica, conviene probarlo con criterio, no agregarlo de forma automática por costumbre.

21.19 ¿Siempre conviene usar BatchNorm?

Tampoco siempre, aunque es una técnica muy extendida.

BatchNorm suele ser especialmente útil en redes profundas o en contextos donde estabilizar activaciones resulta importante.

Pero como cualquier herramienta, debe entenderse en el contexto del problema y no como una receta universal.

21.20 Diferencia conceptual resumida

Podemos resumir la diferencia entre ambas técnicas así:

  • Dropout: introduce ruido controlado para reducir dependencia excesiva entre neuronas.
  • BatchNorm: normaliza activaciones para estabilizar y facilitar el entrenamiento.

Ambas pueden mejorar el resultado final, pero no por exactamente la misma razón.

21.21 Qué errores conceptuales conviene evitar

Al estudiar estas técnicas, algunos errores muy comunes son:

  • Pensar que Dropout y BatchNorm son sinónimos.
  • Creer que ambas solo sirven para “que el modelo sea mejor” sin entender cómo.
  • Olvidar cambiar entre train() y eval().
  • Suponer que más Dropout siempre significa mejor generalización.
  • No revisar qué pasa en validación al introducir estas capas.

21.22 Qué observaremos en la aplicación final

En la aplicación final construiremos tres modelos sobre un problema tabular de clasificación:

  • Un modelo base sin Dropout ni BatchNorm.
  • Un modelo con Dropout.
  • Un modelo con Batch Normalization y Dropout.

La comparación permitirá ver no solo el rendimiento final, sino también las diferencias de comportamiento entre enfoques.

21.23 Por qué elegimos un problema tabular

Un problema tabular permite mantener el foco en las capas y en el flujo de entrenamiento, sin agregar complejidad extra de imágenes o secuencias.

Así, el estudiante puede concentrarse mejor en el papel de Dropout y BatchNorm dentro de la arquitectura.

El objetivo es entender el mecanismo, no construir un benchmark sofisticado.

21.24 Qué mediremos

Durante el entrenamiento observaremos:

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

Con esos datos podremos comparar si el entrenamiento es estable y si la generalización mejora.

21.25 Lo importante no es memorizar la receta

Más importante que memorizar el orden exacto de unas pocas líneas es comprender el sentido de cada capa.

Si entiendes qué problema intenta resolver BatchNorm y qué problema intenta resolver Dropout, luego te resultará mucho más fácil leer arquitecturas más complejas.

Ese es el verdadero objetivo de este tema.

21.26 Código completo para ejecutar

El siguiente script genera un problema tabular de clasificación binaria relacionado con rendimiento académico. Luego entrena tres modelos: uno base, uno con Dropout y otro con BatchNorm más Dropout.

import torch
import torch.nn as nn
import torch.optim as optim

# Fijamos la semilla para poder repetir el experimento.
torch.manual_seed(21)

def generar_datos(n):
    # Variables que describen el perfil de un estudiante.
    horas_estudio = 10 * torch.rand(n, 1)
    asistencia = 0.4 + 0.6 * torch.rand(n, 1)
    tareas = torch.rand(n, 1)
    parcial_1 = 10 * torch.rand(n, 1)
    parcial_2 = 10 * torch.rand(n, 1)
    participacion = torch.rand(n, 1)
    distraccion = torch.rand(n, 1)
    sueno = torch.rand(n, 1)

    # Armamos el tensor de entrada normalizando las notas a rango 0..1.
    X = torch.cat([
        horas_estudio / 10.0,
        asistencia,
        tareas,
        parcial_1 / 10.0,
        parcial_2 / 10.0,
        participacion,
        distraccion,
        sueno
    ], dim=1)

    # Regla oculta del problema.
    puntaje = (
        3.0 * (horas_estudio / 10.0) +
        2.8 * asistencia +
        2.5 * tareas +
        2.2 * (parcial_1 / 10.0) +
        2.8 * (parcial_2 / 10.0) +
        1.5 * participacion -
        2.2 * distraccion +
        1.2 * sueno +
        1.4 * (tareas * asistencia) -
        1.0 * (distraccion * sueno)
    )

    ruido = 0.8 * torch.randn(n, 1)
    y = ((puntaje + ruido) > 7.2).float()
    return X, y

# Separamos entrenamiento y validacion.
X_train, y_train = generar_datos(140)
X_val, y_val = generar_datos(500)

class ModeloBase(nn.Module):
    def __init__(self):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(8, 64),
            nn.ReLU(),
            nn.Linear(64, 64),
            nn.ReLU(),
            nn.Linear(64, 1)
        )

    def forward(self, x):
        return self.net(x)

class ModeloConDropout(nn.Module):
    def __init__(self):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(8, 64),
            nn.ReLU(),
            nn.Dropout(p=0.30),
            nn.Linear(64, 64),
            nn.ReLU(),
            nn.Dropout(p=0.30),
            nn.Linear(64, 1)
        )

    def forward(self, x):
        return self.net(x)

class ModeloConBatchNormYDropout(nn.Module):
    def __init__(self):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(8, 64),
            nn.BatchNorm1d(64),
            nn.ReLU(),
            nn.Dropout(p=0.25),
            nn.Linear(64, 64),
            nn.BatchNorm1d(64),
            nn.ReLU(),
            nn.Dropout(p=0.25),
            nn.Linear(64, 1)
        )

    def forward(self, x):
        return self.net(x)

def accuracy_desde_logits(logits, y_real):
    probs = torch.sigmoid(logits)
    pred = (probs >= 0.5).float()
    return (pred == y_real).float().mean().item()

def entrenar_modelo(modelo, nombre):
    criterio = nn.BCEWithLogitsLoss()
    optimizador = optim.Adam(modelo.parameters(), lr=0.01, weight_decay=0.001)

    print(nombre)
    for epoca in range(250):
        # En train(), Dropout se activa y BatchNorm usa estadisticas del batch.
        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:
            # En eval(), Dropout se desactiva y BatchNorm usa estadisticas acumuladas.
            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}")

    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

modelo_base = ModeloBase()
resultado_base = entrenar_modelo(modelo_base, "MODELO BASE")

print()
modelo_dropout = ModeloConDropout()
resultado_dropout = entrenar_modelo(modelo_dropout, "MODELO CON DROPOUT")

print()
modelo_bn_dropout = ModeloConBatchNormYDropout()
resultado_bn_dropout = entrenar_modelo(modelo_bn_dropout, "MODELO CON BATCHNORM Y DROPOUT")

print()
print("RESUMEN FINAL")
print("Base:")
print(f"train loss={resultado_base[0]:.4f} | val loss={resultado_base[1]:.4f} | train acc={resultado_base[2]:.3f} | val acc={resultado_base[3]:.3f}")
print("Con Dropout:")
print(f"train loss={resultado_dropout[0]:.4f} | val loss={resultado_dropout[1]:.4f} | train acc={resultado_dropout[2]:.3f} | val acc={resultado_dropout[3]:.3f}")
print("Con BatchNorm y Dropout:")
print(f"train loss={resultado_bn_dropout[0]:.4f} | val loss={resultado_bn_dropout[1]:.4f} | train acc={resultado_bn_dropout[2]:.3f} | val acc={resultado_bn_dropout[3]:.3f}")

# Probamos el modelo final con estudiantes nuevos no vistos antes.
ejemplos_nuevos = torch.tensor([
    [0.90, 0.95, 0.90, 0.80, 0.85, 0.80, 0.10, 0.75],
    [0.25, 0.60, 0.30, 0.40, 0.35, 0.20, 0.85, 0.30],
    [0.65, 0.88, 0.75, 0.55, 0.70, 0.60, 0.20, 0.80]
], dtype=torch.float32)

modelo_bn_dropout.eval()
with torch.no_grad():
    probs = torch.sigmoid(modelo_bn_dropout(ejemplos_nuevos))
    preds = (probs >= 0.5).float()
    print()
    print("Predicciones del modelo con BatchNorm y Dropout:")
    print(probs)
    print(preds)

Este ejemplo permite observar tres ideas importantes. Primero, que un modelo base puede aprender bien pero no necesariamente generalizar mejor. Segundo, que Dropout puede ayudar a regularizar. Y tercero, que BatchNorm puede volver más estable el entrenamiento y convivir bien con Dropout.

21.27 Errores comunes al usar Dropout y BatchNorm

  • Olvidar llamar a model.eval() al evaluar.
  • Usar Dropout demasiado alto y deteriorar el aprendizaje.
  • Pensar que BatchNorm reemplaza por completo la necesidad de validar.
  • Confundir la función principal de Dropout con la de BatchNorm.
  • Interpretar una sola corrida como verdad definitiva sin comparar resultados.

21.28 Buenas prácticas para estudiantes

Si estás aprendiendo, estas recomendaciones suelen ayudarte:

  • Probar primero un modelo base para tener una referencia.
  • Agregar Dropout y BatchNorm de forma gradual.
  • Observar siempre entrenamiento y validación.
  • Recordar que train() y eval() cambian realmente el comportamiento del modelo.
  • No usar estas técnicas como “decoración”, sino con un propósito claro.

21.29 Qué debes recordar de este tema

  • Dropout y BatchNorm no son lo mismo.
  • Dropout introduce regularización apagando activaciones durante el entrenamiento.
  • Batch Normalization normaliza activaciones para estabilizar el aprendizaje.
  • Ambas técnicas cambian su comportamiento entre entrenamiento y evaluación.
  • model.train() y model.eval() son fundamentales cuando estas capas están presentes.
  • BatchNorm y Dropout pueden combinarse en una misma arquitectura.
  • La validación sigue siendo necesaria para saber si el modelo realmente mejora.

21.30 Cierre conceptual

Dropout y Batch Normalization son dos herramientas muy importantes del Deep Learning moderno, pero solo resultan realmente útiles cuando se entienden en contexto.

Dominar este tema significa empezar a ver la arquitectura de una red no solo como una secuencia de capas, sino como un sistema donde cada componente cumple una función específica dentro del entrenamiento y de la generalización.