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.
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().
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.
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.
Aunque ambas técnicas suelen mejorar el comportamiento del modelo, no cumplen el mismo rol principal.
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.
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.
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.
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.
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.
Durante el entrenamiento, Dropout actúa de manera aleatoria.
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.
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.
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.
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.
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.
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:
Ese 64 indica el número de características o neuronas que se normalizarán en esa parte de la red.
Una secuencia común en redes densas puede verse así:
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.
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.
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.
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.
Podemos resumir la diferencia entre ambas técnicas así:
Ambas pueden mejorar el resultado final, pero no por exactamente la misma razón.
Al estudiar estas técnicas, algunos errores muy comunes son:
train() y eval().En la aplicación final construiremos tres modelos sobre un problema tabular de clasificación:
La comparación permitirá ver no solo el rendimiento final, sino también las diferencias de comportamiento entre enfoques.
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.
Durante el entrenamiento observaremos:
Con esos datos podremos comparar si el entrenamiento es estable y si la generalización mejora.
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.
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.
model.eval() al evaluar.Si estás aprendiendo, estas recomendaciones suelen ayudarte:
train() y eval() cambian realmente el comportamiento del modelo.model.train() y model.eval() son fundamentales cuando estas capas están presentes.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.