LECCIÓN 11 · AVANZADO

Regularización

Las herramientas de PyTorch para combatir el overfitting que estudiaste en teoría. Dropout, weight decay y BatchNorm: cada una "limita" al modelo para que generalice mejor.

1. Recordatorio: el problema es el overfitting

Un modelo con demasiada capacidad memoriza el ruido de los datos de entrenamiento y falla con datos nuevos. La regularización son técnicas que limitan esa memorización. Veamos las tres más usadas, con su efecto real.

2. Dropout: apagar neuronas al azar

Durante el entrenamiento, nn.Dropout(p) pone a cero cada neurona con probabilidad p. Así la red no puede depender de ninguna neurona concreta y aprende representaciones más robustas:

drop = nn.Dropout(p=0.5)
x = torch.ones(8)

drop.train()        # en entrenamiento: apaga ~50%
print(drop(x))

drop.eval()         # en evaluación: NO apaga nada
print(drop(x))
salida realtrain: tensor([2., 2., 2., 2., 0., 2., 2., 0.]) eval : tensor([1., 1., 1., 1., 1., 1., 1., 1.])
Fíjate en dos cosas: (1) en train() apagó 2 de 8 neuronas (las puso a 0), y (2) las que quedaron valen 2 en vez de 1 — las escala para compensar las apagadas. En eval() no apaga nada (usa toda la red). Por esto importa net.train()/net.eval() que vimos en la lección 9.

3. Weight decay (regularización L2): penalizar pesos grandes

Pesos grandes = curvas muy "retorcidas" que sobreajustan. El weight decay añade una penalización por pesos grandes, empujándolos hacia cero (curvas más suaves). En PyTorch es solo un argumento del optimizador:

opt = torch.optim.Adam(net.parameters(),
                       lr=0.01,
                       weight_decay=1e-4)   ← regularización L2
salida realoptimizador con weight_decay=1e-4 creado OK

No hay que cambiar nada más: el optimizador aplica la penalización en cada step(). Valores típicos: 1e-4 a 1e-2.

4. BatchNorm: normalizar las activaciones

nn.BatchNorm1d normaliza la salida de una capa (media ≈ 0, desviación ≈ 1) por cada lote. Estabiliza y acelera el entrenamiento, y tiene un efecto regularizador. Veámoslo:

bn = nn.BatchNorm1d(3)
x = torch.tensor([[1.,2.,3.],
                  [4.,5.,6.],
                  [7.,8.,9.]])
out = bn(x)
salida realmedia por columna ANTES: tensor([4., 5., 6.]) media por columna DESPUÉS: tensor([0., 0., 0.]) std por columna DESPUÉS: tensor([1., 1., 1.])
✍️ A mano, columna 0 = [1, 4, 7] (fórmula: (x − media) / desviación)
1) media:        (1 + 4 + 7) / 3 = 12 / 3 = 4
2) varianza:     [(1-4)² + (4-4)² + (7-4)²] / 3
               = [ 9   +   0   +   9  ] / 3 = 18/3 = 6
3) desviación:   √6 = 2.4495
4) normalizar cada valor:
     (1 − 4) / 2.4495 = -1.2247
     (4 − 4) / 2.4495 = 0.0000
     (7 − 4) / 2.4495 = +1.2247
   → nueva media = 0, nueva desviación = 1  ✓ (las otras columnas, igual)

Cada columna (cada "feature") sale con media 0 y desviación 1, sin importar la escala que tuviera a la entrada. Eso evita que unas features dominen a otras y suaviza el paisaje de la pérdida.

5. ¿Cómo se combinan en una red?

net = nn.Sequential(
    nn.Linear(10, 64),
    nn.BatchNorm1d(64),    # normaliza
    nn.ReLU(),
    nn.Dropout(0.3),       # apaga 30%
    nn.Linear(64, 1),
)
Resumen: Dropout apaga neuronas al azar (robustez); weight_decay penaliza pesos grandes (curvas suaves); BatchNorm normaliza activaciones (estabilidad). Las tres reducen el overfitting de formas distintas y suelen usarse juntas. En la próxima lección vemos una arquitectura especializada: las CNNs para imágenes.