PyTorch Batch Normalization y Dropout

Batch Normalization (normalización por lotes) y Dropout son dos de las técnicas de regularización y estabilización más importantes en el entrenamiento de redes neuronales profundas. La primera resuelve el problema del desplazamiento covariado interno durante el entrenamiento, haciendo que las redes más profundas sean entrenables; la segunda previene el sobreajuste al descartar neuronas aleatoriamente, mejorando la capacidad de generalización del modelo. Ambas se utilizan normalmente juntas y son componentes estándar de las redes neuronales modernas.


1. Normalización por lotes (Batch Normalization)

1.1 Principios básicos

Durante el entrenamiento de redes profundas, los pequeños cambios en los parámetros de una capa se amplifican a medida que aumenta la profundidad, lo que provoca que la distribución de entrada de las capas posteriores cambie continuamente—este fenómeno se denominadesplazamiento covariado interno (Internal Covariate Shift). Batch Normalization resuelve este problema estandarizando la salida de cada capa y forzando que los valores de activación vuelvan a una distribución estable.

Fórmula de normalización:

\[\hat{x}_i = \frac{x_i - \mu_B}{\sqrt{\sigma_B^2 + \epsilon}}\]

\[y_i = \gamma \hat{x}_i + \beta\]

donde:

  • \(\mu_B\) y \(\sigma_B^2\) son la media y la varianza del batch actual
  • \(\epsilon\) es una constante pequeña para evitar la división por cero (por defecto1e-5)
  • \(\gamma\) y \(\beta\) son parámetros de escala y desplazamiento aprendibles que permiten que la red decida la forma final de la distribución

Beneficios de Batch Normalization:

  • Permite usar una tasa de aprendizaje mayor, acelerando el entrenamiento
  • Reduce la sensibilidad a la inicialización, haciendo el entrenamiento más estable
  • Tiene cierto efecto de regularización, reduciendo la dependencia de Dropout
  • Atenúa la desaparición/explosión del gradiente, permitiendo entrenar redes más profundas

1.2 BatchNorm1d / 2d / 3d

PyTorch ofrece tres versiones según la dimensión de entrada, con exactamente la misma forma de uso; solo cambia la forma de los datos procesados.

BatchNorm1d: para capas totalmente conectadas / datos secuenciales

Ejemplo

import torch
import torch.nn as nn

# Forma de entrada: (N, C) o (N, C, L)
# N=batch_size, C=número de características/canales, L=longitud de secuencia
bn1d = nn.BatchNorm1d(
    num_features=128,    # Número de características/canales
    eps=1e-5,            # Constante pequeña para evitar división por cero (por defecto 1e-5)
    momentum=0.1,        # Momentum de la media móvil (por defecto 0.1)
    affine=True,         # Si aprende gamma y beta (por defecto True)
    track_running_stats=True,  # Si rastrea la media/varianza en tiempo de ejecución (por defecto True)
)

# Se usa después de una capa totalmente conectada
x = torch.randn(32, 128)     # (batch=32, features=128)
out = bn1d(x)
print(out.shape)              # torch.Size([32, 128])

# Datos secuenciales (por ejemplo, después de una convolución 1D)
x_seq = torch.randn(32, 128, 50)   # (batch, channels, seq_len)
out_seq = bn1d(x_seq)
print(out_seq.shape)               # torch.Size([32, 128, 50])

BatchNorm2d: para mapas de características de imágenes en CNN

Ejemplo

# Forma de entrada: (N, C, H, W)
# Normaliza cada canal C de forma independiente
bn2d = nn.BatchNorm2d(num_features=64)  # 64 canales

x = torch.randn(32, 64, 28, 28)    # (batch, channels, height, width)
out = bn2d(x)
print(out.shape)                    # torch.Size([32, 64, 28, 28])

BatchNorm3d: para convoluciones 3D (video/imágenes médicas)

Ejemplo

# Forma de entrada: (N, C, D, H, W)
bn3d = nn.BatchNorm3d(num_features=32)

x = torch.randn(4, 32, 16, 32, 32)    # (batch, channels, depth, h, w)
out = bn3d(x)
print(out.shape)                        # torch.Size([4, 32, 16, 32, 32])

Ver parámetros aprendibles

Ejemplo

bn = nn.BatchNorm2d(64)

print(f"weight (gamma) shape: {bn.weight.shape}")   # torch.Size([64])
print(f"bias   (beta)  shape: {bn.bias.shape}")     # torch.Size([64])
print(f"running_mean   shape: {bn.running_mean.shape}")  # torch.Size([64])
print(f"running_var    shape: {bn.running_var.shape}")   # torch.Size([64])

# running_mean / running_var no son parámetros entrenables
# Son estadísticas de media móvil utilizadas en inferencia
print(f`"Número de parámetros entrenables: {sum(p.numel() for p in bn.parameters())}"`)  # 128(64×2)

Significado del parámetro momentum

Ejemplo

# El momentum de PyTorch es lo contrario de lo que se suele entender:
# running_mean = (1 - momentum) × running_mean + momentum × batch_mean
# Es decir, cuanto mayor es el momentum, mayor es el peso de las estadísticas del batch actual

# Valor por defecto 0.1: generalmente adecuado
# Para batch size pequeño se recomienda reducirlo: momentum=0.01, para evitar estadísticas inestables
bn = nn.BatchNorm2d(64, momentum=0.01)

# momentum=None: usa media móvil acumulativa (CMA), adecuado para batches muy pequeños
bn = nn.BatchNorm2d(64, momentum=None)

1.3 LayerNorm (normalización de capa)

SobreTodas las características de una sola muestraHace la normalización sin depender del batch, adecuada para escenarios con batch size pequeño o variable.El método de normalización estándar de Transformer。

BatchNorm:在 batch 维度计算均值/方差(跨样本,同通道)
LayerNorm:在特征维度计算均值/方差(同样本,跨特征)

Ejemplo

import torch
import torch.nn as nn

# ── Para totalmente conectadas/Transformer ────────────────────────
# normalized_shape: normaliza las últimas dimensiones
ln = nn.LayerNorm(normalized_shape=512)

x = torch.randn(32, 10, 512)   # (batch, seq_len, embed_dim)
out = ln(x)
print(out.shape)                # torch.Size([32, 10, 512])

# ── Normalizar múltiples dimensiones ───────────────────────────────
ln_2d = nn.LayerNorm([64, 28, 28])   # Normaliza las tres dimensiones C, H, W
x = torch.randn(8, 64, 28, 28)
out = ln_2d(x)

# ── Uso típico en Transformer ──────────────────────
class TransformerBlock(nn.Module):
    def __init__(self, embed_dim, num_heads, ff_dim):
        super().__init__()
        self.attn  = nn.MultiheadAttention(embed_dim, num_heads, batch_first=True)
        self.ff    = nn.Sequential(
            nn.Linear(embed_dim, ff_dim),
            nn.GELU(),
            nn.Linear(ff_dim, embed_dim),
        )
        self.norm1 = nn.LayerNorm(embed_dim)   # ← Normaliza después de cada subcapa
        self.norm2 = nn.LayerNorm(embed_dim)

    def forward(self, x):
        attn_out, _ = self.attn(x, x, x)
        x = self.norm1(x + attn_out)            # Residual + normalización
        x = self.norm2(x + self.ff(x))
        return x

1.4 GroupNorm (normalización de grupo)

Divide los canales en variosgrupogrupos, y normaliza dentro de cada grupo. No depende del batch size; en escenarios con batch pequeño (como detección de objetos, segmentación de imágenes médicas) funciona mejor que BatchNorm.

Ejemplo

# num_groups: en cuántos grupos dividir los canales (debe dividir exactamente a num_channels)
# Caso especial: num_groups=1 equivale a LayerNorm
# num_groups=num_channels equivale a InstanceNorm
gn = nn.GroupNorm(
    num_groups=32,       # Número de grupos (común: 8, 16, 32)
    num_channels=256,    # Número de canales
    eps=1e-5,
    affine=True,
)

x = torch.randn(4, 256, 56, 56)    # batch pequeño (4 imágenes), 256 canales
out = gn(x)
print(out.shape)                    # torch.Size([4, 256, 56, 56])

# GroupNorm no se ve afectado en absoluto cuando batch=1 (por ejemplo, al inferir una sola imagen)
x_single = torch.randn(1, 256, 56, 56)
out_single = gn(x_single)           # Funciona con total normalidad

1.5 InstanceNorm (normalización de instancia)

SobreCada canal de cada muestraSe normaliza de forma independiente, sin involucrar la dimensión de batch. Se usa a menudo entransferencia de estilo de imagen— Conserva la información de estilo de cada imagen, evitando que las estadísticas de otras imágenes del batch interfieran.

Ejemplo

# InstanceNorm2d: normaliza H×W en cada (N, C)
in2d = nn.InstanceNorm2d(
    num_features=64,
    affine=False,    # Por defecto False (no aprende gamma/beta)
)

x = torch.randn(8, 64, 128, 128)
out = in2d(x)
print(out.shape)    # torch.Size([8, 64, 128, 128])

1.6 Comparación de métodos de normalización

Método Dimensión de normalización Dependencia de batch Escenarios de aplicación
BatchNorm Entre batch (mismo canal) Dependencia fuerte Clasificación de imágenes con CNN (batch grande)
LayerNorm Entre características (misma muestra) Sin dependencia Transformer、NLP、RNN
GroupNorm Canales dentro del grupo (misma muestra) Sin dependencia Detección de objetos y segmentación con batch pequeño
InstanceNorm Dimensión espacial (misma muestra y mismo canal) Sin dependencia Transferencia de estilo de imagen, modelos generativos

Referencia rápida de selección

# batch_size >= 16, tareas de imágenes con CNN → BatchNorm2d
# Transformer / BERT / GPT            → LayerNorm
# batch_size < 8, detección de objetos/segmentación → GroupNorm(num_groups=32)
# Transferencia de estilo de imagen / CycleGAN → InstanceNorm2d

2. Dropout

2.1 Principios básicos

Dropout establece aleatoriamente las salidas de algunas neuronas a 0 durante el entrenamiento (con probabilidadp), forzando a la red a no depender de una única neurona, aprendiendo así representaciones de características más robustas y distribuidas, previniendo eficazmente el sobreajuste.

Durante el entrenamiento:

\[y_i = \begin{cases} 0 & \text{con probabilidad } p \\ \dfrac{x_i}{1-p} & \text{con probabilidad } 1-p \end{cases}\]

Las neuronas conservadas se amplifican por \(\frac{1}{1-p}\) para mantener el valor esperado (es decir, Inverted Dropout).

Durante la inferencia: Dropout Se desactiva automáticamente, todas las neuronas participan normalmente en el cálculo, lo que equivale al promedio de un conjunto de múltiples subredes.

训练时示意(p=0.5):
输入:  [1.0,  2.0,  3.0,  4.0,  5.0]
掩码:  [ 1,    0,    1,    0,    1 ]   ← 随机生成
输出:  [2.0,  0.0,  6.0,  0.0, 10.0]  ← 保留的值乘以 1/(1-0.5)=2

推理时:
输出:  [1.0,  2.0,  3.0,  4.0,  5.0]  ← 原样输出

2.2 Dropout / Dropout2d / Dropout3d

Dropout: se usa en capas totalmente conectadas

Ejemplo

import torch
import torch.nn as nn

dropout = nn.Dropout(p=0.5)   # p: probabilidad de poner a cero

x = torch.ones(2, 10)
print("Modo entrenamiento:)
dropout.train()
print(dropout(x))    # Aproximadamente el 50% de los valores son 0, los valores conservados son 2.0

print("\nModo evaluación:")
dropout.eval()
print(dropout(x))    # Todos son 1.0, Dropout desactivado

Ejemplo de salida:

训练模式:
tensor([[2., 0., 2., 0., 2., 0., 0., 2., 2., 0.],
        [0., 2., 0., 2., 0., 2., 2., 0., 0., 2.]])

评估模式:
tensor([[1., 1., 1., 1., 1., 1., 1., 1., 1., 1.],
        [1., 1., 1., 1., 1., 1., 1., 1., 1., 1.]])

Dropout2d: para mapas de características de CNN (descarte de canal completo)

Dropout2dDescarta aleatoriamente por canal, es decir, todas las posiciones espaciales de un canal completo se ponen a cero juntas. Esto es más adecuado para características convolucionales que el Dropout punto a punto, porque las activaciones de píxeles adyacentes están altamente correlacionadas y el descarte punto a punto es menos efectivo.

Ejemplo

dropout2d = nn.Dropout2d(p=0.3)

# Entrada: (N, C, H, W)
x = torch.ones(4, 16, 8, 8)    # 4 imágenes, 16 canales
out = dropout2d(x)
print(out.shape)                # torch.Size([4, 16, 8, 8])

# Verificación: descarte de canal completo
# Estado de descarte de canales de la imagen 0 (distinto de cero = canal conservado, todo ceros = canal descartado)
kept_channels = (out[0].sum(dim=(1, 2)) != 0).sum().item()
print(f"La imagen 0 conservó {kept_channels}/16 canales")

Dropout3d: para convoluciones 3D

Ejemplo

# Entrada: (N, C, D, H, W), todo el volumen de características (D, H, W) se descarta
dropout3d = nn.Dropout3d(p=0.2)
x = torch.ones(2, 8, 16, 16, 16)
out = dropout3d(x)
print(out.shape)   # torch.Size([2, 8, 16, 16, 16])

Recomendaciones para elegir el valor de p

Ejemplo

# Capas totalmente conectadas (capas ocultas anchas): Dropout más alto
nn.Dropout(p=0.5)     # Valor clásico, estándar en capas ocultas

# Capas totalmente conectadas (capas ocultas estrechas) o redes pequeñas: Dropout más bajo
nn.Dropout(p=0.2)     # Evitar el subajuste

# Dropout2d en capas convolucionales: generalmente más bajo
nn.Dropout2d(p=0.1)   # Las capas convolucionales ya tienen cierto efecto de regularización

# Dropout antes de la cabeza de clasificación (por ejemplo, EfficientNet / ViT)
nn.Dropout(p=0.3)     # Normalmente entre 0.2 y 0.5

# Dropout de atención / FFN en Transformer
nn.Dropout(p=0.1)     # Valor por defecto de BERT; en Transformer normalmente es menor

2.3 AlphaDropout

Una variante de Dropout diseñada específicamente parala función de activación SELU,que ajusta automáticamente la media y la varianza tras descartar neuronas, manteniendo la propiedad de autonormalización (self-normalizing).

Ejemplo

# Se usa con SELU (red autonormalizante SELU + AlphaDropout)
class SelfNormalizingNet(nn.Module):
    def __init__(self, in_dim, hidden_dim, num_classes):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(in_dim, hidden_dim),
            nn.SELU(),
            nn.AlphaDropout(p=0.1),   # ← Se usa con SELU
            nn.Linear(hidden_dim, hidden_dim),
            nn.SELU(),
            nn.AlphaDropout(p=0.1),
            nn.Linear(hidden_dim, num_classes),
        )

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

Nota:AlphaDropout solo tiene sentido cuando se usa la función de activación SELU; con otras funciones de activación, use directamente el Dropout normal.


3. Modo de entrenamiento y modo de evaluación

El comportamiento de BatchNorm y Dropout durante el entrenamiento y la evaluaciónes completamente diferente, por lo que debe cambiarse correctamente; este es uno de los errores más comunes entre los principiantes.

Comparación de diferencias de comportamiento

Componente model.train() model.eval()
BatchNorm Normaliza con la media/varianza del batch actual;Actualiza running_mean/var Normaliza con running_mean/var;No actualiza
Dropout Pone a cero aleatoriamente (con probabilidad p) Desactivado, todas las neuronas producen salida normal

Forma correcta de uso

Ejemplo

import torch
import torch.nn as nn

model = nn.Sequential(
    nn.Linear(128, 256),
    nn.BatchNorm1d(256),
    nn.ReLU(),
    nn.Dropout(0.5),
    nn.Linear(256, 10),
)

# ── Fase de entrenamiento ──────────────────────────────────────
model.train()    # ← Cambiar al modo entrenamiento (estado predeterminado)

for inputs, labels in train_loader:
    optimizer.zero_grad()
    outputs = model(inputs)   # BN usa estadísticas del batch, Dropout descarta aleatoriamente
    loss = criterion(outputs, labels)
    loss.backward()
    optimizer.step()

# ── Fase de evaluación/validación ─────────────────────────────────
model.eval()     # ← Cambiar al modo evaluación (¡muy importante!)

with torch.no_grad():              # También se desactiva el cálculo de gradientes
    for inputs, labels in val_loader:
        outputs = model(inputs)    # BN usa estadísticas acumuladas, Dropout desactivado
        # ...

# ── Inferencia de una sola imagen ──────────────────────────────────
model.eval()
with torch.inference_mode():       # Más rápido que no_grad, específico para inferencia
    output = model(single_input.unsqueeze(0))
    pred   = output.argmax(1).item()

Verificar si el cambio de modo es correcto

Ejemplo

# Use este código para verificar si BN y Dropout funcionan en el modo correcto
x = torch.randn(8, 128)

model.train()
out_train_1 = model(x)
out_train_2 = model(x)
print("¿En modo entrenamiento las dos salidas son iguales?", torch.allclose(out_train_1, out_train_2))
# False ← Cada vez la máscara de Dropout es diferente, las salidas difieren (normal)

model.eval()
out_eval_1 = model(x)
out_eval_2 = model(x)
print("¿En modo evaluación las dos salidas son iguales?", torch.allclose(out_eval_1, out_eval_2))
# True ← Dropout desactivado, BN usa estadísticas fijas, la salida es determinista (normal)

4. Ubicación de uso en la red

Ubicación de BatchNorm

Existen dos enfoques para la ubicación de BatchNorm, y ambos se usan en la práctica:

Enfoque 1: Antes de la función de activación (artículo original)

# Conv → BN → ReLU
nn.Sequential(
    nn.Conv2d(32, 64, 3, padding=1),
    nn.BatchNorm2d(64),      # BN antes de la activación
    nn.ReLU(inplace=True),
)

Enfoque 2: Después de la función de activación (algunas investigaciones consideran que funciona mejor)

# Conv → ReLU → BN
nn.Sequential(
    nn.Conv2d(32, 64, 3, padding=1),
    nn.ReLU(inplace=True),
    nn.BatchNorm2d(64),      # BN después de la activación
)

Recomendación práctica:Siga el diseño del artículo que esté consultando; si no hay requisitos especiales, Conv → BN → ReLU es lo más habitual.

Ubicación de Dropout

Red totalmente conectada: después de la función de activación

nn.Sequential(
    nn.Linear(512, 256),
    nn.ReLU(),
    nn.Dropout(0.5),         # Después de ReLU
    nn.Linear(256, 128),
    nn.ReLU(),
    nn.Dropout(0.5),
    nn.Linear(128, 10),      # Normalmente no se agrega Dropout antes de la capa de salida
)

CNN: después de BN (si hay BN, Dropout puede omitirse)

# Nota: BN ya tiene un efecto de regularización; si la CNN ya tiene BN, las capas convolucionales no necesitan Dropout.
# Dropout normalmente solo se agrega en capas totalmente conectadas o antes de la cabeza de clasificación
nn.Sequential(
    nn.Conv2d(64, 128, 3, padding=1),
    nn.BatchNorm2d(128),
    nn.ReLU(inplace=True),
    # Normalmente no se agrega Dropout aquí (BN ya proporciona el efecto de regularización)
)

Transformer: dentro de FFN y de las capas de atención

class FeedForward(nn.Module):
    def __init__(self, d_model, d_ff, dropout=0.1):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(d_model, d_ff),
            nn.GELU(),
            nn.Dropout(dropout),  # Agregar Dropout dentro de FFN
            nn.Linear(d_ff, d_model),
            nn.Dropout(dropout),  # Agregar una vez más antes de la salida
        )
    def forward(self, x):
        return self.net(x)

¿Deben usarse BN y Dropout simultáneamente?

Red totalmente conectada: pueden usarse simultáneamente

nn.Sequential(
    nn.Linear(512, 256),
    nn.BatchNorm1d(256),
    nn.ReLU(),
    nn.Dropout(0.3),         # BN + Dropout coexistiendo, común y efectivo
    nn.Linear(256, 10),
)

Redes convolucionales: normalmente se elige uno de los dos o se usa Dropout solo en la cabeza.

Razón: la operación de normalización de BN y Dropout pueden provocar el problema de "desplazamiento de varianza". Es decir, durante el entrenamiento Dropout cambia la varianza de las activaciones, y durante la inferencia Dropout está desactivado, lo que hace que las estadísticas que BN ve en la inferencia no coincidan con las del entrenamiento.

Práctica recomendada:

Opción A: usar solo BN en las capas convolucionales y solo Dropout en las capas totalmente conectadas/cabeza de clasificación

Ejemplo

class ConvNet(nn.Module):
    def __init__(self, num_classes):
        super().__init__()
        self.features = nn.Sequential(
            nn.Conv2d(3, 64, 3, padding=1),
            nn.BatchNorm2d(64),       # Parte convolucional: solo BN
            nn.ReLU(inplace=True),
            nn.Conv2d(64, 128, 3, padding=1),
            nn.BatchNorm2d(128),
            nn.ReLU(inplace=True),
            nn.AdaptiveAvgPool2d(1),
        )
        self.classifier = nn.Sequential(
            nn.Flatten(),
            nn.Linear(128, 256),
            nn.ReLU(),
            nn.Dropout(0.5),          # Parte totalmente conectada: solo Dropout
            nn.Linear(256, num_classes),
        )

    def forward(self, x):
        return self.classifier(self.features(x))

5. Ejemplo de red completa

Ejemplo 1: Red de clasificación totalmente conectada (BN + Dropout)

Ejemplo

import torch
import torch.nn as nn

class MLPClassifier(nn.Module):
    """
Clasificador totalmente conectado con BatchNorm1d y Dropout
Adecuado para tareas de clasificación de datos tabulares
    """

    def __init__(self, in_dim, hidden_dims, num_classes, dropout=0.5):
        super().__init__()
        layers = []
        prev_dim = in_dim

        for hidden_dim in hidden_dims:
            layers += [
                nn.Linear(prev_dim, hidden_dim),
                nn.BatchNorm1d(hidden_dim),
                nn.ReLU(inplace=True),
                nn.Dropout(dropout),
            ]
            prev_dim = hidden_dim

        layers.append(nn.Linear(prev_dim, num_classes))
        self.net = nn.Sequential(*layers)

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


model = MLPClassifier(
    in_dim=784,
    hidden_dims=[512, 256, 128],
    num_classes=10,
    dropout=0.4
)

# Verificar la forma de salida
x = torch.randn(32, 784)
model.eval()
print(model(x).shape)   # torch.Size([32, 10])

Ejemplo 2: Clasificación de imágenes con CNN (principalmente BN)

Ejemplo

class ConvBNBlock(nn.Module):
    """Bloque convolucional estándar: Conv → BN → ReLU"""
    def __init__(self, in_ch, out_ch, stride=1):
        super().__init__()
        self.block = nn.Sequential(
            nn.Conv2d(in_ch, out_ch, 3, stride=stride, padding=1, bias=False),
            nn.BatchNorm2d(out_ch),    # bias=False: el beta de BN ya cumple la función de sesgo
            nn.ReLU(inplace=True),
        )

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


class SmallCNN(nn.Module):
    """CNN pequeño para CIFAR-10"""
    def __init__(self, num_classes=10):
        super().__init__()
        self.features = nn.Sequential(
            ConvBNBlock(3, 32),               # 32×32 → 32×32
            ConvBNBlock(32, 64, stride=2),    # 32×32 → 16×16
            ConvBNBlock(64, 128, stride=2),   # 16×16 → 8×8
            ConvBNBlock(128, 256, stride=2),  # 8×8 → 4×4
            nn.AdaptiveAvgPool2d(1),          # 4×4 → 1×1
        )
        self.classifier = nn.Sequential(
            nn.Flatten(),
            nn.Linear(256, 128),
            nn.ReLU(),
            nn.Dropout(0.5),                  # Usar Dropout solo en las capas totalmente conectadas
            nn.Linear(128, num_classes),
        )

    def forward(self, x):
        return self.classifier(self.features(x))


model = SmallCNN(num_classes=10)
x = torch.randn(16, 3, 32, 32)
model.eval()
print(model(x).shape)   # torch.Size([16, 10])

Ejemplo 3: Ciclo de entrenamiento completo

Ejemplo

import torch
import torch.nn as nn
import torch.optim as optim
from torch.optim.lr_scheduler import CosineAnnealingLR

DEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
EPOCHS = 50

model     = SmallCNN(num_classes=10).to(DEVICE)
criterion = nn.CrossEntropyLoss(label_smoothing=0.1)
optimizer = optim.AdamW(model.parameters(), lr=1e-3, weight_decay=1e-4)
scheduler = CosineAnnealingLR(optimizer, T_max=EPOCHS)

def run_epoch(model, loader, optimizer=None, train=True):
    if train:
        model.train()    # ← BN usa estadísticas del batch, Dropout activo
    else:
        model.eval()     # ← BN usa running stats, Dropout desactivado

    total_loss, correct = 0.0, 0

    ctx = torch.enable_grad() if train else torch.no_grad()
    with ctx:
        for imgs, labels in loader:
            imgs, labels = imgs.to(DEVICE), labels.to(DEVICE)

            if train:
                optimizer.zero_grad()

            outputs = model(imgs)
            loss    = criterion(outputs, labels)

            if train:
                loss.backward()
                optimizer.step()

            total_loss += loss.item() * imgs.size(0)
            correct    += (outputs.argmax(1) == labels).sum().item()

    n = len(loader.dataset)
    return total_loss / n, correct / n


best_acc = 0.0
for epoch in range(1, EPOCHS + 1):
    train_loss, train_acc = run_epoch(model, train_loader, optimizer, train=True)
    val_loss,   val_acc   = run_epoch(model, val_loader,   train=False)
    scheduler.step()

    print(f"Epoch {epoch:2d}/{EPOCHS} | "
          f"Train {train_loss:.4f}/{train_acc:.4f} | "
          f"Val {val_loss:.4f}/{val_acc:.4f}")

    if val_acc > best_acc:
        best_acc = val_acc
        torch.save(model.state_dict(), 'best.pth')

6. Guía de ajuste de hiperparámetros

Ajuste de parámetros de BatchNorm

Ejemplo

# ── momentum (el parámetro que más a menudo necesita ajuste) ─────────────────
# El valor predeterminado 0.1 funciona en la mayoría de los casos

# batch_size muy pequeño (< 8) → estadísticas inestables → reducir momentum
nn.BatchNorm2d(64, momentum=0.01)

# Muchos pasos de entrenamiento, distribución de datos estable → se puede aumentar el momentum apropiadamente
nn.BatchNorm2d(64, momentum=0.2)

# ── affine=False (no aprender gamma/beta) ──────────────
# Se usa muy raramente, normalmente se mantiene el valor predeterminado True
# A menos que explícitamente no se desee que BN cambie la escala y el desplazamiento de las características
nn.BatchNorm2d(64, affine=False)

# ── track_running_stats=False ──────────────────────
# También usar estadísticas del batch actual en cada inferencia (comportamiento consistente entre entrenamiento e inferencia)
# Adecuado para escenarios especiales como aprendizaje en línea, datos en streaming, etc.
nn.BatchNorm2d(64, track_running_stats=False)

Estrategias de ajuste de Dropout

Ejemplo

# ── Ajustar p según el grado de sobreajuste ───────────────────────────
# acc de entrenamiento mucho mayor que acc de validación (sobreajuste severo) → aumentar p
# acc de entrenamiento y acc de validación cercanas pero ambas bajas (subajuste) → reducir p o eliminar Dropout

# ── Referencia de valores p recomendados ───────────────────────────────
p_values = {
    'Capas ocultas totalmente conectadas grandes (> 1024)': 0.5,
    'Capas ocultas totalmente conectadas medianas (256~1024)': 0.3,
    'Capas ocultas totalmente conectadas pequeñas (< 256)': 0.2,
    'Capas convolucionales CNN (Dropout2d)': 0.1,
    'Antes de la cabeza de clasificación': 0.3,
    'Transformer FFN': 0.1,
    'Atención de Transformer': 0.1,
}

# ── Dropout dinámico (ajustado según el progreso del entrenamiento) ──────────────────
# En algunas prácticas, p es pequeño al inicio del entrenamiento y aumenta después (reforzar la regularización a medida que el modelo se vuelve más complejo)
class DynamicDropoutNet(nn.Module):
    def __init__(self, initial_p=0.1):
        super().__init__()
        self.dropout = nn.Dropout(initial_p)
        self.fc = nn.Linear(256, 10)

    def set_dropout(self, p):
        self.dropout.p = p

    def forward(self, x):
        return self.fc(self.dropout(x))

model = DynamicDropoutNet(initial_p=0.1)
# A mitad del entrenamiento, aumentar Dropout
model.set_dropout(0.4)

7. Errores comunes y precauciones

Error 1: Olvidar cambiar al modo eval()

Forma incorrecta

# Error: no se llamó a model.eval() durante la evaluación
model.train()   # Estado predeterminado
for imgs, labels in val_loader:
    with torch.no_grad():
        outputs = model(imgs)   # ¡Dropout todavía descarta aleatoriamente! ¡BN todavía se está actualizando!
        # Resultados no deterministas, precisión artificialmente baja y contaminación de running_mean/var

Forma correcta

# Correcto: cambiar explícitamente el modo antes de la evaluación
model.eval()
with torch.no_grad():
    outputs = model(imgs)    # Salida determinista

Error 2: La capa Conv tiene bias y además va seguida de BN

Forma incorrecta

# Redundante: el bias de Conv es compensado por el beta de BN, desperdiciando parámetros
nn.Conv2d(64, 128, 3, padding=1, bias=True),   # Valor predeterminado True
nn.BatchNorm2d(128),

Forma correcta

# Correcto: al conectar Conv con BN, desactivar el bias
nn.Conv2d(64, 128, 3, padding=1, bias=False),  # ← bias=False
nn.BatchNorm2d(128),

Error 3: Usar BatchNorm con batch size=1

Forma incorrecta

# Error: con batch=1, BN no puede calcular la varianza, aparecerán NaN o inf durante el entrenamiento
# La inferencia con una sola muestra no tiene problema (usa running stats), pero el entrenamiento con una sola muestra fallará
model.train()
x = torch.randn(1, 64, 32, 32)
out = bn2d(x)    # ← En modo entrenamiento, batch=1 dará error o producirá NaN

Solución

# Solución 1: Usar GroupNorm en su lugar
nn.GroupNorm(num_groups=32, num_channels=64)

# Solución 2: Usar LayerNorm
nn.LayerNorm([64, 32, 32])

# Solución 3: Cambiar a eval() durante la inferencia, no afecta a BN
model.eval()
out = bn2d(x)    # En modo inferencia, batch=1 funciona perfectamente

Error 4: Dropout causa desplazamiento de varianza en BN

Problema

# Problema: Dropout cambia la varianza de las activaciones durante el entrenamiento
# En la inferencia Dropout está desactivado, BN usa running_var del entrenamiento (con influencia de Dropout)
# Las estadísticas no coinciden, los resultados de inferencia tienen sesgo
nn.Sequential(
    nn.Conv2d(64, 128, 3),
    nn.Dropout2d(0.5),        # ← Usar Dropout en capas convolucionales
    nn.BatchNorm2d(128),      # ← La varianza que BN ve es alterada por Dropout
)

Solución

# Opción 1: BN primero, Dropout después
nn.Sequential(
    nn.Conv2d(64, 128, 3),
    nn.BatchNorm2d(128),      # BN primero (no afectado por Dropout)
    nn.ReLU(),
    nn.Dropout2d(0.1),        # Luego Dropout
)

# Opción 2: solo BN en capas convolucionales, solo Dropout en capas totalmente conectadas (la más recomendada)

Error 5: Olvidar cambiar a modo eval en una capa BN congelada

Forma incorrecta

# En aprendizaje por transferencia, al congelar el backbone, las running stats de BN preentrenado no deben actualizarse
# De lo contrario, la distribución de nuevos datos contaminará las estadísticas preentrenadas

# Error: se congelan los parámetros, pero running_mean/var de BN todavía se actualizan
model = models.resnet50(weights='IMAGENET1K_V2')
for param in model.parameters():
    param.requires_grad = False   # Solo se congelan los parámetros aprendibles, las estadísticas de BN siguen actualizándose

Forma correcta

# Solución correcta: cambiar también las capas BN del backbone a modo eval
def freeze_bn(model):
    for module in model.modules():
        if isinstance(module, (nn.BatchNorm1d, nn.BatchNorm2d, nn.BatchNorm3d)):
            module.eval()              # Fijar running_mean/var
            for param in module.parameters():
                param.requires_grad = False

model = models.resnet50(weights='IMAGENET1K_V2')
freeze_bn(model)          # Congelar todos los BN
model.fc = nn.Linear(model.fc.in_features, 10)  # Solo las nuevas capas FC son entrenables

Lista de verificación de diagnóstico rápido

Fenómeno Causa posible Elemento a verificar
La loss de validación sube y baja erráticamente Olvidarmodel.eval() Confirmar llamar a model.eval() antes de la evaluacióneval()
Gran brecha entre acc de entrenamiento/validación Dropout pDemasiado grande Intentar reducirpo eliminar Dropout
NaN/inf en el entrenamiento batch=1 con BatchNorm Cambiar a GroupNorm o LayerNorm
Mal rendimiento en aprendizaje por transferencia running stats de BN contaminadas Llamar a eval() en las capas BN del backboneeval()
Resultados de inferencia del modelo inestables No se cambió a eval() durante la inferenciaeval() Llamar a model.eval() antes de la inferenciamodel.eval()
Demasiados parámetros en Conv No se desactivó bias en Conv+BN Configurarbias=False
Otras extensiones