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 defecto
1e-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.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
# 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
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
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
# 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.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
# 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
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
# 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.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
# 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
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
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
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.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
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)
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)
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.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)
# 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
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.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
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.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
"""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.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
# 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
# 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
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
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
nn.Conv2d(64, 128, 3, padding=1, bias=True), # Valor predeterminado True
nn.BatchNorm2d(128),
Forma correcta
nn.Conv2d(64, 128, 3, padding=1, bias=False), # ← bias=False
nn.BatchNorm2d(128),
Error 3: Usar BatchNorm con batch size=1
Forma incorrecta
# 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
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
# 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
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
# 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
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 |