Hacia el entrenamiento efectivo de redes neuronales
Construiremos un entrenamiento completo de una CNN sobre CIFAR-10 aplicando todas las técnicas necesarias para acelerar, estabilizar y regularizar el proceso. Inicialización de pesos, optimizadores, schedulers, gradient clipping, batch normalization, dropout, data augmentation — todo explicado y ejecutable.
Requisitos previos
- Python 3.9+ y PyTorch 2.x instalados
- Conocimientos de PyTorch básico (consulta el tutorial de PyTorch)
- Entender qué es una CNN y sus capas (consulta la teoría de CNNs)
- Haber leído la teoría de estabilidad del entrenamiento
- Opcional: GPU con CUDA (los ejemplos funcionan en CPU pero tardan más)
Visión general: las tres patas del entrenamiento
Entrenar una red neuronal es un acto de equilibrio entre tres objetivos: velocidad (converger rápido), estabilidad (que el entrenamiento no diverja ni oscile) y generalización (que lo aprendido funcione en datos nuevos). Cada técnica que veremos ataca uno o varios de estos ejes:
1.1 Setup e imports
pip install torch torchvision matplotlib tqdm
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader
from torchvision import datasets, transforms
from torch.optim.lr_scheduler import CosineAnnealingLR, OneCycleLR
import matplotlib.pyplot as plt
from tqdm import tqdm
import copy
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"Device: {device}")
1.2 Dataset: CIFAR-10
Usaremos CIFAR-10: 60K imágenes 32×32 en color, 10 clases. Es lo suficientemente complejo como para que las técnicas de entrenamiento marquen diferencia, pero lo suficientemente pequeño para iterar rápido.
# Transforms básicos (los mejoraremos en el paso 7)
transform_base = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.4914, 0.4822, 0.4465),
(0.2470, 0.2435, 0.2616)),
])
train_dataset = datasets.CIFAR10(root='./data', train=True,
download=True, transform=transform_base)
test_dataset = datasets.CIFAR10(root='./data', train=False,
download=True, transform=transform_base)
train_loader = DataLoader(train_dataset, batch_size=128, shuffle=True,
num_workers=2, pin_memory=True)
test_loader = DataLoader(test_dataset, batch_size=256, shuffle=False,
num_workers=2, pin_memory=True)
print(f"Train: {len(train_dataset)} | Test: {len(test_dataset)}")
print(f"Clases: {train_dataset.classes}")
print(f"Input shape: {train_dataset[0][0].shape}") # → torch.Size([3, 32, 32])
pin_memory=True acelera la transferencia CPU→GPU. Siempre úsalo si tienes GPU.1.3 Modelo base: CNN
Definimos una CNN sencilla que iremos mejorando paso a paso. Empezamos sin ninguna técnica especial — luego las añadiremos.
class SimpleCNN(nn.Module):
"""CNN base sin batch norm, sin dropout — para comparar."""
def __init__(self, num_classes=10):
super().__init__()
self.features = nn.Sequential(
nn.Conv2d(3, 32, 3, padding=1), nn.ReLU(),
nn.Conv2d(32, 32, 3, padding=1), nn.ReLU(),
nn.MaxPool2d(2),
nn.Conv2d(32, 64, 3, padding=1), nn.ReLU(),
nn.Conv2d(64, 64, 3, padding=1), nn.ReLU(),
nn.MaxPool2d(2),
nn.Conv2d(64, 128, 3, padding=1), nn.ReLU(),
nn.MaxPool2d(2),
)
self.classifier = nn.Sequential(
nn.Flatten(),
nn.Linear(128 * 4 * 4, 256), nn.ReLU(),
nn.Linear(256, num_classes),
)
def forward(self, x):
return self.classifier(self.features(x))
# Número de parámetros
model = SimpleCNN().to(device)
total_params = sum(p.numel() for p in model.parameters())
print(f"Parámetros: {total_params:,}") # → ~340K
1.4 Mapa de técnicas que cubriremos
Inicialización de pesos
Una mala inicialización puede hacer que el entrenamiento no converja en absoluto: si los pesos son demasiado grandes, los gradientes explotan; si son demasiado pequeños, se desvanecen. El objetivo es que, en el forward pass, las activaciones mantengan una varianza estable capa a capa.
2.1 ¿Qué pasa con una inicialización incorrecta?
# ❌ Mala idea: pesos ~ N(0, 1) sin escalar
bad_model = SimpleCNN().to(device)
for p in bad_model.parameters():
if p.dim() >= 2:
nn.init.normal_(p, mean=0.0, std=1.0)
# Pasamos un batch y vemos las activaciones
x = torch.randn(4, 3, 32, 32).to(device)
hooks = []
activations = {}
for name, layer in bad_model.features.named_children():
def hook_fn(n):
def hook(module, input, output):
activations[n] = output.detach()
return hook
hooks.append(layer.register_forward_hook(hook_fn(name)))
with torch.no_grad():
bad_model(x)
for name, act in activations.items():
print(f"Layer {name:>2s}: mean={act.mean():.4f}, std={act.std():.4f}, "
f"dead={((act==0).float().mean()*100):.1f}%")
for h in hooks: h.remove()
NaN. El modelo es inentrenable.
2.2 Xavier / Glorot (para tanh, sigmoid)
Glorot & Bengio (2010)
propusieron escalar los pesos según el fan-in y fan-out de cada capa:
W ~ U(-a, a) con a = √(6 / (fan_in + fan_out)).
def init_xavier(module):
"""Xavier/Glorot — ideal para tanh, sigmoid."""
if isinstance(module, (nn.Linear, nn.Conv2d)):
nn.init.xavier_uniform_(module.weight)
if module.bias is not None:
nn.init.zeros_(module.bias)
model_xavier = SimpleCNN().to(device)
model_xavier.apply(init_xavier)
print("✓ Xavier init aplicada")
2.3 Kaiming / He (para ReLU)
He et al. (2015)
ajustaron la fórmula para activaciones ReLU, que matan la mitad
de las activaciones (los negativos). Esto requiere duplicar la varianza:
W ~ N(0, √(2 / fan_in)).
def init_kaiming(module):
"""Kaiming/He — ideal para ReLU y variantes."""
if isinstance(module, (nn.Linear, nn.Conv2d)):
nn.init.kaiming_normal_(module.weight, mode='fan_in', nonlinearity='relu')
if module.bias is not None:
nn.init.zeros_(module.bias)
model = SimpleCNN().to(device)
model.apply(init_kaiming)
print("✓ Kaiming init aplicada")
mode='fan_in' preserva la varianza en el forward pass. 'fan_out' la preserva en el backward. Ambos funcionan; fan_in es más común.nonlinearity='relu' ajusta la ganancia para ReLU. Para LeakyReLU, usa nonlinearity='leaky_relu'.2.4 Verificar la inicialización
# Repetimos el experimento con Kaiming
activations = {}
hooks = []
for name, layer in model.features.named_children():
def hook_fn(n):
def hook(module, input, output):
activations[n] = output.detach()
return hook
hooks.append(layer.register_forward_hook(hook_fn(name)))
with torch.no_grad():
model(x)
for name, act in activations.items():
print(f"Layer {name:>2s}: mean={act.mean():.4f}, std={act.std():.4f}, "
f"dead={((act==0).float().mean()*100):.1f}%")
for h in hooks: h.remove()
| Método | Fórmula | Activación ideal | Uso en PyTorch |
|---|---|---|---|
| Zeros | W = 0 | ❌ Ninguna | nn.init.zeros_ |
| Random N(0,1) | W ~ N(0,1) | ❌ Ninguna | nn.init.normal_ |
| Xavier/Glorot | √(6/(fan_in+fan_out)) | tanh, sigmoid | nn.init.xavier_uniform_ |
| Kaiming/He | √(2/fan_in) | ReLU, LeakyReLU | nn.init.kaiming_normal_ |
BatchNorm renormaliza las activaciones después de cada capa, lo que mitiga parcialmente los problemas de inicialización. Sin embargo:
- La primera capa no tiene BN antes, así que la init importa.
- Mejor init → convergencia más rápida, incluso con BN.
- Sin BN (por ejemplo, en Transformers que usan LayerNorm), la init es crítica.
- En la práctica, siempre usa Kaiming para redes con ReLU. Es gratis y no tiene downside.
PyTorch usa Kaiming por defecto para nn.Linear y nn.Conv2d,
pero la implementación exacta puede variar entre versiones. Ser explícito no hace daño.
Elegir el optimizador adecuado
El optimizador define cómo se actualizan los pesos en cada paso. La elección del optimizador y sus hiperparámetros tiene un impacto enorme en la velocidad de convergencia y en el mínimo al que se llega.
3.1 SGD clásico
# SGD puro: w = w - lr * grad
optimizer_sgd = optim.SGD(model.parameters(), lr=0.01)
# Con momentum: acumula velocidad como una "bola rodando cuesta abajo"
# v = momentum * v_prev + grad
# w = w - lr * v
optimizer_sgdm = optim.SGD(model.parameters(), lr=0.01, momentum=0.9)
# Con Nesterov: mira "adelante" antes de calcular el gradiente
optimizer_nesterov = optim.SGD(model.parameters(), lr=0.01, momentum=0.9, nesterov=True)
momentum=0.9 es el valor estándar. Suaviza oscilaciones y acelera el descenso. Rara vez se cambia.3.2 Adam
# Adam: adapta el learning rate por parámetro usando momentos de 1er y 2do orden
optimizer_adam = optim.Adam(
model.parameters(),
lr=1e-3, # LR base — mucho más bajo que SGD
betas=(0.9, 0.999), # β1: momentum del gradiente, β2: momentum del gradiente²
eps=1e-8, # estabilidad numérica
)
# Valores razonables:
# lr: 1e-3 (default y funciona bien para empezar)
# betas: (0.9, 0.999) — rara vez se cambian
# eps: 1e-8 — puede subirse a 1e-6 si hay inestabilidad numérica
3.3 AdamW (Adam con weight decay desacoplado)
# AdamW: implementa weight decay correctamente (Loshchilov & Hutter, 2019)
# En Adam estándar, weight_decay interactúa con los momentos adaptativos → mal
# AdamW lo desacopla: primero adapta el lr, luego aplica decay por separado
optimizer = optim.AdamW(
model.parameters(),
lr=1e-3,
weight_decay=1e-2, # regularización L2 desacoplada (veremos en paso 6)
)
print(f"Optimizer: {optimizer.__class__.__name__}")
print(f" lr={optimizer.defaults['lr']}, wd={optimizer.defaults['weight_decay']}")
Paper: Loshchilov & Hutter (2019) — "Decoupled Weight Decay Regularization"
3.4 Grupos de parámetros
A veces queremos distintos LR o weight decay para distintas partes del modelo. Por ejemplo, no aplicar weight decay a los biases ni a los parámetros de BatchNorm:
def get_param_groups(model, lr=1e-3, weight_decay=1e-2):
"""Separa parámetros que deben tener weight decay de los que no."""
decay_params = []
no_decay_params = []
for name, param in model.named_parameters():
if not param.requires_grad:
continue
# No aplicar weight decay a bias ni a parámetros de normalización
if param.dim() <= 1 or 'bias' in name:
no_decay_params.append(param)
else:
decay_params.append(param)
return [
{"params": decay_params, "weight_decay": weight_decay},
{"params": no_decay_params, "weight_decay": 0.0},
]
param_groups = get_param_groups(model)
optimizer = optim.AdamW(param_groups, lr=1e-3)
n_decay = sum(p.numel() for p in param_groups[0]["params"])
n_nodecay = sum(p.numel() for p in param_groups[1]["params"])
print(f"Con weight decay: {n_decay:,} params | Sin weight decay: {n_nodecay:,} params")
Comparativa de optimizadores
| Optimizador | LR típico | Hiperparámetros | Mejor para |
|---|---|---|---|
| SGD + momentum | 0.01 – 0.1 | momentum=0.9 | CVs (ResNet, etc.) con scheduler bien afinado |
| Adam | 1e-3 – 3e-4 | β1=0.9, β2=0.999 | Prototipado rápido, GANs |
| AdamW | 1e-3 – 3e-4 | β1=0.9, β2=0.999, wd=0.01 | Uso general, Transformers, ViTs |
| SGD + Nesterov | 0.01 – 0.1 | momentum=0.9, nesterov=True | Si quieres exprimir el último% de accuracy |
Históricamente, SGD + momentum con un scheduler bien diseñado encontraba minimos que generalizaban mejor. Pero esto requería afinar cuidadosamente el LR y el schedule.
Con AdamW y schedulers modernos (OneCycleLR, CosineAnnealing), la brecha se ha cerrado. En la práctica:
- Para CNNs clásicas: SGD + momentum + CosineAnnealing sigue siendo competitivo.
- Para Transformers: AdamW + warmup + cosine decay es el estándar.
- Para prototipar: Adam/AdamW con lr=1e-3 y sin scheduler es lo más rápido.
- Recomendación: Empieza con AdamW(lr=1e-3, wd=0.01). Si necesitas más accuracy, prueba SGD(lr=0.1, momentum=0.9) + CosineAnnealing.
1e-3 para Adam/AdamW, 0.1 para SGD.
Learning rate schedulers
El learning rate no tiene por qué ser constante. De hecho, variarlo durante el entrenamiento es una de las técnicas más efectivas: empezar alto para explorar y reducirlo progresivamente para afinar. Veamos las estrategias más usadas.
4.1 StepLR
from torch.optim.lr_scheduler import StepLR
# Reduce el LR por un factor gamma cada step_size épocas
scheduler_step = StepLR(optimizer, step_size=30, gamma=0.1)
# Epoch 0-29: lr = 1e-3
# Epoch 30-59: lr = 1e-4
# Epoch 60-89: lr = 1e-5
# En el training loop:
# for epoch in range(num_epochs):
# train_one_epoch(...)
# scheduler_step.step() ← ¡al final de cada época!
4.2 CosineAnnealingLR
from torch.optim.lr_scheduler import CosineAnnealingLR
# Decae el LR siguiendo medio coseno: de lr_max a eta_min
scheduler_cosine = CosineAnnealingLR(
optimizer,
T_max=100, # número total de épocas
eta_min=1e-6, # LR mínimo al final
)
# Suave y sin escalones bruscos → muy popular en visión por computador
4.3 OneCycleLR
from torch.optim.lr_scheduler import OneCycleLR
# Super-convergence: sube el LR rápidamente y luego baja
# (Smith & Topin, 2018: "Super-Convergence")
scheduler_onecycle = OneCycleLR(
optimizer,
max_lr=1e-2, # pico máximo del LR
epochs=100,
steps_per_epoch=len(train_loader), # ← se actualiza POR BATCH, no por época
pct_start=0.3, # 30% del training para subir, 70% para bajar
anneal_strategy='cos',
div_factor=10, # lr_inicial = max_lr / div_factor
final_div_factor=100, # lr_final = lr_inicial / final_div_factor
)
# ¡IMPORTANTE! OneCycleLR se llama por batch, NO por época:
# for epoch in range(num_epochs):
# for batch in train_loader:
# ...
# optimizer.step()
# scheduler_onecycle.step() ← después de cada batch
max_lr es el pico. Con AdamW, prueba 3e-3 a 1e-2. Con SGD, hasta 0.3.pct_start=0.3: el warmup ocupa el 30% del entrenamiento. Es bastante agresivo.4.4 Warmup + Cosine (estilo Transformer)
import math
class WarmupCosineScheduler:
"""LR warmup lineal + cosine decay. Estilo Transformers."""
def __init__(self, optimizer, warmup_epochs, total_epochs, min_lr=1e-6):
self.optimizer = optimizer
self.warmup = warmup_epochs
self.total = total_epochs
self.min_lr = min_lr
self.base_lrs = [g['lr'] for g in optimizer.param_groups]
self.step_count = 0
def step(self):
self.step_count += 1
if self.step_count <= self.warmup:
# Warmup lineal
scale = self.step_count / self.warmup
else:
# Cosine decay
progress = (self.step_count - self.warmup) / (self.total - self.warmup)
scale = 0.5 * (1 + math.cos(math.PI * progress))
for g, base_lr in zip(self.optimizer.param_groups, self.base_lrs):
g['lr'] = self.min_lr + (base_lr - self.min_lr) * scale
def get_lr(self):
return [g['lr'] for g in self.optimizer.param_groups]
# Uso
scheduler = WarmupCosineScheduler(optimizer, warmup_epochs=5, total_epochs=100)
Comparativa de schedulers
| Scheduler | Forma | Actualización | Ideal para |
|---|---|---|---|
| StepLR | Escalones | Por época | Entrenamiento largo con hitos claros |
| CosineAnnealing | Medio coseno | Por época | CNNs (ResNet, etc.), uso general |
| OneCycleLR | Subida + bajada | Por batch | Entrenamientos cortos, super-convergence |
| Warmup + Cosine | Rampa + coseno | Por época | Transformers, entrenamientos largos |
- AdamW: Empieza con
lr=1e-3. Si la loss oscila, baja a3e-4. Para fine-tuning de modelos pre-entrenados, usa1e-5a5e-5. - SGD: Empieza con
lr=0.1. Con batch size 128, esto suele funcionar. Para batch sizes mayores, escala linealmente:lr = 0.1 × (batch_size / 128). - Regla del batch size: Si multiplicas el batch size por k, multiplica el LR por k (linear scaling rule). Con warmup de 5 épocas.
- OneCycleLR:
max_lr≈ 10× lo que usarías como LR constante. El scheduler se encarga de modularlo.
scheduler.step().
Sin esta llamada, el scheduler no hace nada y el LR es constante. Otro error:
llamar a scheduler.step() antes de optimizer.step()
— PyTorch mostrará un warning.
Estabilización: gradient clipping y normalización
Incluso con buena inicialización y un LR razonable, los gradientes pueden volverse inestables durante el entrenamiento, especialmente en redes profundas o con secuencias largas (RNNs). Las técnicas de estabilización mantienen los gradientes y activaciones en un rango saludable.
5.1 Gradient clipping
Gradient clipping limita la magnitud del gradiente antes de actualizar los pesos. Si la norma del gradiente supera un umbral, se reescala proporcionalmente. Previene la explosión de gradientes.
# Después de loss.backward(), antes de optimizer.step():
max_norm = 1.0 # umbral de la norma L2
# Calcula la norma total del gradiente y reescala si supera max_norm
total_norm = nn.utils.clip_grad_norm_(model.parameters(), max_norm=max_norm)
print(f"Grad norm: {total_norm:.4f} (clipped to {max_norm})")
# Alternativa: clip por valor (menos común)
# nn.utils.clip_grad_value_(model.parameters(), clip_value=0.5)
clip_grad_norm_ retorna la norma original. Útil para monitorizar: si la norma clipeada es frecuente, quizás tu LR es muy alto.max_norm=1.0 es un buen valor por defecto. Para Transformers se suele usar 1.0. Para RNNs, 5.0.- ✅ Siempre con RNNs/LSTMs/GRUs (explosión de gradientes es común).
- ✅ En Transformers (estándar en GPT, BERT, etc.).
- ✅ Si observas spikes en la loss durante el entrenamiento.
- ⚠️ En CNNs es menos crítico, pero no hace daño. Lo incluiremos por seguridad.
5.2 Batch Normalization
Ioffe & Szegedy (2015) introdujeron Batch Normalization: normalizar las activaciones de cada capa a media 0 y varianza 1 (usando estadísticas del mini-batch), y luego aplicar una transformación afín aprendible (γ, β).
class CNN_BN(nn.Module):
"""CNN con Batch Normalization entre Conv y ReLU."""
def __init__(self, num_classes=10):
super().__init__()
self.features = nn.Sequential(
nn.Conv2d(3, 32, 3, padding=1),
nn.BatchNorm2d(32), # ← BN para mapas 2D
nn.ReLU(),
nn.Conv2d(32, 32, 3, padding=1),
nn.BatchNorm2d(32),
nn.ReLU(),
nn.MaxPool2d(2),
nn.Conv2d(32, 64, 3, padding=1),
nn.BatchNorm2d(64),
nn.ReLU(),
nn.Conv2d(64, 64, 3, padding=1),
nn.BatchNorm2d(64),
nn.ReLU(),
nn.MaxPool2d(2),
nn.Conv2d(64, 128, 3, padding=1),
nn.BatchNorm2d(128),
nn.ReLU(),
nn.MaxPool2d(2),
)
self.classifier = nn.Sequential(
nn.Flatten(),
nn.Linear(128 * 4 * 4, 256),
nn.BatchNorm1d(256), # ← BN para vectores 1D
nn.ReLU(),
nn.Linear(256, num_classes),
)
def forward(self, x):
return self.classifier(self.features(x))
- Estabilidad: Previene que las activaciones se desplacen (internal covariate shift).
- LR más alto: Con BN puedes usar LRs 5–10× más altos sin diverger.
- Regularización leve: El ruido del mini-batch actúa como regularizador.
- Convergencia más rápida: Típicamente 2–3× menos épocas.
5.3 Alternativas a BatchNorm
# LayerNorm: normaliza sobre features, no sobre el batch
# → Estándar en Transformers (independiente del batch size)
ln = nn.LayerNorm(normalized_shape=256)
# GroupNorm: divide canales en G grupos, normaliza cada grupo
# → Útil cuando batch size es muy pequeño (batch=1–4)
gn = nn.GroupNorm(num_groups=8, num_channels=64)
# InstanceNorm: normaliza cada mapa de features individualmente
# → Estándar en style transfer
inst = nn.InstanceNorm2d(64)
| Normalización | Normaliza sobre | Depende de batch? | Ideal para |
|---|---|---|---|
| BatchNorm | Batch × Spatial | ✅ Sí | CNNs con batch ≥ 16 |
| LayerNorm | Features | ❌ No | Transformers, RNNs |
| GroupNorm | Channels (en grupos) | ❌ No | CNNs con batch pequeño |
| InstanceNorm | Each sample × channel | ❌ No | Style transfer |
BatchNorm tiene dos comportamientos distintos:
- Training (
model.train()): Usa media/varianza del mini-batch actual. Actualiza running_mean y running_var con momentum. - Eval (
model.eval()): Usa running_mean y running_var acumulados durante training.
¡Error clásico! Olvidar model.eval() antes de evaluar o hacer inferencia.
Resultado: predicciones inconsistentes que dependen del batch. Siempre:
model.eval()
with torch.no_grad():
preds = model(test_batch)
model.train() # volver a training mode
5.4 Monitorizar la norma del gradiente
def get_grad_norm(model):
"""Calcula la norma L2 total del gradiente."""
total_norm = 0.0
for p in model.parameters():
if p.grad is not None:
total_norm += p.grad.data.norm(2).item() ** 2
return total_norm ** 0.5
# Usar en el training loop:
# loss.backward()
# grad_norm = get_grad_norm(model)
# if grad_norm > 100:
# print(f"⚠️ Grad norm alta: {grad_norm:.1f}")
# nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
# optimizer.step()
Regularización: weight decay, dropout y más
Las técnicas de regularización controlan la complejidad del modelo para reducir overfitting. Mientras las técnicas anteriores estabilizan el proceso de optimización, la regularización mejora la generalización: el rendimiento en datos no vistos.
6.1 Weight decay (L2 regularization)
Weight decay penaliza los pesos grandes añadiendo un término
λ · ||w||² a la función de pérdida. En AdamW
(Loshchilov & Hutter, 2019)
este decaimiento está desacoplado del gradiente, lo cual es más efectivo:
# Weight decay típico: 1e-2 para grandes modelos, 1e-4 para CNNs
optimizer = torch.optim.AdamW(
model.parameters(),
lr=1e-3,
weight_decay=1e-4 # ← Regularización L2 desacoplada
)
# NOTA: En SGD, weight_decay=1e-4 es equivalente a L2 clásico.
# En AdamW, está desacoplado del cálculo de momentos (mejor).
weight_decay=1e-4 es un buen punto de partida para CIFAR-10. Redes más grandes (ResNet-50+) suelen usar 1e-2. Modelos de NLP suelen usar 0.01.get_param_groups).
6.2 Dropout
Srivastava et al. (2014)
propusieron Dropout: durante training, desactivar aleatoriamente
una fracción p de las neuronas. Esto fuerza redundancia
en las representaciones y reduce co-adaptación.
class CNN_Regularized(nn.Module):
"""CNN con BatchNorm + Dropout."""
def __init__(self, num_classes=10, dropout_rate=0.3):
super().__init__()
self.features = nn.Sequential(
# Bloque 1
nn.Conv2d(3, 32, 3, padding=1),
nn.BatchNorm2d(32),
nn.ReLU(),
nn.Conv2d(32, 32, 3, padding=1),
nn.BatchNorm2d(32),
nn.ReLU(),
nn.MaxPool2d(2),
nn.Dropout2d(0.1), # ← Dropout espacial (ligero en features)
# Bloque 2
nn.Conv2d(32, 64, 3, padding=1),
nn.BatchNorm2d(64),
nn.ReLU(),
nn.Conv2d(64, 64, 3, padding=1),
nn.BatchNorm2d(64),
nn.ReLU(),
nn.MaxPool2d(2),
nn.Dropout2d(0.15),
# Bloque 3
nn.Conv2d(64, 128, 3, padding=1),
nn.BatchNorm2d(128),
nn.ReLU(),
nn.MaxPool2d(2),
)
self.classifier = nn.Sequential(
nn.Flatten(),
nn.Linear(128 * 4 * 4, 256),
nn.BatchNorm1d(256),
nn.ReLU(),
nn.Dropout(dropout_rate), # ← Dropout estándar en FC
nn.Linear(256, num_classes),
)
def forward(self, x):
return self.classifier(self.features(x))
model = CNN_Regularized(num_classes=10, dropout_rate=0.3)
print(f"Parámetros: {sum(p.numel() for p in model.parameters()):,}")
Dropout2d desactiva canales enteros (mejor para conv). Tasa baja (0.1–0.15) en features.Dropout estándar con p=0.3 en la capa fully-connected. Rango habitual: 0.2–0.5.Hay debate sobre si Dropout y BatchNorm deberían usarse juntos. Li et al. (2019) mostraron que Dropout puede causar un "variance shift" que afecta a BN durante eval.
Recomendación práctica:
- Usar Dropout después de BatchNorm (no antes).
- En features (conv), usar tasas bajas: 0.05–0.15.
- Concentrar el Dropout fuerte en las capas FC del clasificador.
- Si el modelo es grande (ResNet-50+), considerar usar solo weight decay sin Dropout.
6.3 Label smoothing
Label smoothing
(Szegedy et al., 2016)
reemplaza los hard labels (0 o 1) con soft labels que distribuyen una
pequeña probabilidad ε entre todas las clases. Previene que el
modelo sea excesivamente confiado (over-confident).
# PyTorch ≥ 1.10 soporta label_smoothing directamente
criterion = nn.CrossEntropyLoss(label_smoothing=0.1)
# Equivale a:
# y_smooth = (1 - ε) * one_hot(y) + ε / num_classes
# loss = CrossEntropy(logits, y_smooth)
# Ejemplo: para clase 3 con 10 clases y ε=0.1:
# [0.01, 0.01, 0.01, 0.91, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01]
label_smoothing=0.1 es el valor estándar. En datasets muy ruidosos se puede subir a 0.2. Más de 0.3 suele degradar.6.4 Early stopping
Detener el entrenamiento cuando la métrica en validación deja de mejorar es la forma más directa de evitar overfitting. Implementamos un sencillo tracker:
class EarlyStopping:
"""Detiene el entrenamiento si no mejora tras `patience` épocas."""
def __init__(self, patience=5, min_delta=1e-4):
self.patience = patience
self.min_delta = min_delta
self.best_score = None
self.counter = 0
self.should_stop = False
def step(self, val_metric):
"""Llamar tras cada época con la métrica de validación (mayor = mejor)."""
if self.best_score is None:
self.best_score = val_metric
elif val_metric < self.best_score + self.min_delta:
self.counter += 1
if self.counter >= self.patience:
self.should_stop = True
print(f"⏹ Early stopping: sin mejora en {self.patience} épocas.")
else:
self.best_score = val_metric
self.counter = 0
# Uso:
# early_stop = EarlyStopping(patience=7)
# for epoch in range(max_epochs):
# train(...)
# val_acc = evaluate(...)
# early_stop.step(val_acc)
# if early_stop.should_stop:
# break
| Técnica | Qué controla | Valor típico | Cuándo usar |
|---|---|---|---|
| Weight decay | Magnitud de pesos | 1e-4 – 1e-2 | Siempre (incluir en AdamW) |
| Dropout | Co-adaptación de neuronas | 0.1–0.5 | Capas FC, modelos con pocas muestras |
| Label smoothing | Sobre-confianza | 0.1 | Clasificación con muchas clases |
| Early stopping | Nº de épocas | patience 5–10 | Siempre (como red de seguridad) |
Data augmentation: más datos sin más datos
Data augmentation genera variaciones artificiales de las imágenes de entrenamiento aplicando transformaciones aleatorias. Es la forma de regularización más efectiva en visión por computador — equivale a multiplicar el tamaño del dataset sin recopilar nuevos datos.
7.1 Transformaciones básicas con torchvision
from torchvision import transforms
# ── Pipeline de entrenamiento CON augmentation ──────────────
transform_train = transforms.Compose([
transforms.RandomCrop(32, padding=4), # Recorte aleatorio con padding
transforms.RandomHorizontalFlip(p=0.5), # Flip horizontal 50%
transforms.ColorJitter(
brightness=0.2, contrast=0.2,
saturation=0.2, hue=0.1
), # Variación de color
transforms.RandomRotation(15), # Rotación ±15°
transforms.ToTensor(),
transforms.Normalize(
mean=[0.4914, 0.4822, 0.4465],
std=[0.2470, 0.2435, 0.2616]
),
transforms.RandomErasing(p=0.25, scale=(0.02, 0.2)), # "Cutout" aleatorio
])
# ── Pipeline de validación/test SIN augmentation ────────────
transform_test = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize(
mean=[0.4914, 0.4822, 0.4465],
std=[0.2470, 0.2435, 0.2616]
),
])
# Aplicar a los datasets
train_dataset = torchvision.datasets.CIFAR10(
root='./data', train=True, transform=transform_train, download=True
)
test_dataset = torchvision.datasets.CIFAR10(
root='./data', train=False, transform=transform_test, download=True
)
RandomCrop(32, padding=4): Añade 4px de padding (relleno con ceros) y recorta una ventana 32×32 aleatoria. Muy efectivo para CIFAR.RandomHorizontalFlip: Funciona bien para objetos con simetría horizontal. No usar para texto o dígitos.RandomErasing: Variante de Cutout. Oculta un parche rectangular aleatorio, forzando al modelo a usar múltiples regiones para clasificar.ToTensor() + Normalize().
7.2 Augmentaciones avanzadas: Mixup y CutMix
Las técnicas que mezclan muestras del batch generan ejemplos "intermedios" que mejoran significativamente la generalización, especialmente con datos limitados.
import numpy as np
def mixup_batch(images, targets, alpha=0.2):
"""
Mixup (Zhang et al., 2018): mezcla pares de imágenes y sus labels.
images: (B, C, H, W), targets: (B,) con índices de clase
Retorna: images mezcladas, targets_a, targets_b, lambda
"""
lam = np.random.beta(alpha, alpha) if alpha > 0 else 1.0
batch_size = images.size(0)
index = torch.randperm(batch_size, device=images.device)
mixed_images = lam * images + (1 - lam) * images[index]
return mixed_images, targets, targets[index], lam
def mixup_criterion(criterion, pred, y_a, y_b, lam):
"""Loss ponderada entre las dos clases mezcladas."""
return lam * criterion(pred, y_a) + (1 - lam) * criterion(pred, y_b)
# En el training loop:
# images, targets = batch
# images, targets_a, targets_b, lam = mixup_batch(images, targets, alpha=0.2)
# outputs = model(images)
# loss = mixup_criterion(criterion, outputs, targets_a, targets_b, lam)
def cutmix_batch(images, targets, alpha=1.0):
"""
CutMix (Yun et al., 2019): pega un parche de otra imagen en cada imagen.
Más efectivo que Mixup para localización y detección.
"""
lam = np.random.beta(alpha, alpha)
batch_size = images.size(0)
index = torch.randperm(batch_size, device=images.device)
_, _, H, W = images.shape
# Calcular el tamaño del parche a recortar
cut_ratio = np.sqrt(1.0 - lam)
cut_h = int(H * cut_ratio)
cut_w = int(W * cut_ratio)
# Centro aleatorio del parche
cy = np.random.randint(H)
cx = np.random.randint(W)
# Coordenadas del bounding box
y1 = np.clip(cy - cut_h // 2, 0, H)
y2 = np.clip(cy + cut_h // 2, 0, H)
x1 = np.clip(cx - cut_w // 2, 0, W)
x2 = np.clip(cx + cut_w // 2, 0, W)
# Pegar el parche de la imagen permutada
mixed = images.clone()
mixed[:, :, y1:y2, x1:x2] = images[index, :, y1:y2, x1:x2]
# Ajustar lambda al área real del parche
lam = 1 - ((y2 - y1) * (x2 - x1)) / (H * W)
return mixed, targets, targets[index], lam
7.3 AutoAugment y TrivialAugment
# AutoAugment: política aprendida por RL (Cubuk et al., 2019)
transform_auto = transforms.Compose([
transforms.AutoAugment(transforms.AutoAugmentPolicy.CIFAR10),
transforms.ToTensor(),
transforms.Normalize([0.4914, 0.4822, 0.4465], [0.2470, 0.2435, 0.2616]),
])
# TrivialAugment: más simple, sin búsqueda, ¡y funciona igual o mejor!
# (Müller & Hutter, 2021)
transform_trivial = transforms.Compose([
transforms.TrivialAugmentWide(),
transforms.ToTensor(),
transforms.Normalize([0.4914, 0.4822, 0.4465], [0.2470, 0.2435, 0.2616]),
])
AutoAugment usa una política optimizada para CIFAR-10 (hay también para ImageNet y SVHN).TrivialAugmentWide aplica una sola transformación aleatoria a cada imagen. Sorprendentemente efectivo y sin hiperparámetros que ajustar.| Augmentation | Complejidad | Hiperparámetros | Mejora esperada (CIFAR-10) |
|---|---|---|---|
| RandomCrop + Flip | Baja | padding, p | +2–4% acc |
| + ColorJitter + Erasing | Media | Intensidades | +1–2% adicional |
| Mixup | Media | alpha | +0.5–1.5% |
| CutMix | Media | alpha | +1–2% |
| TrivialAugment | Baja | Ninguno | +2–3% |
RandomCrop(32, padding=4) +
RandomHorizontalFlip + RandomErasing es un excelente
punto de partida. Si quieres exprimir más rendimiento, añade CutMix o usa TrivialAugment.
Poniéndolo todo junto: script de entrenamiento completo
Es hora de combinar todas las técnicas de los pasos anteriores en un script de entrenamiento cohesivo. Entrenaremos nuestro modelo mejorado en CIFAR-10 y compararemos con un baseline sin técnicas.
8.1 Dataset con augmentation
import torch
import torch.nn as nn
from torch.utils.data import DataLoader, random_split
import torchvision
from torchvision import transforms
SEED = 42
torch.manual_seed(SEED)
# ── Transformaciones ─────────────────────────────────────────
transform_train = transforms.Compose([
transforms.RandomCrop(32, padding=4),
transforms.RandomHorizontalFlip(),
transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2),
transforms.ToTensor(),
transforms.Normalize([0.4914, 0.4822, 0.4465], [0.2470, 0.2435, 0.2616]),
transforms.RandomErasing(p=0.25),
])
transform_test = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize([0.4914, 0.4822, 0.4465], [0.2470, 0.2435, 0.2616]),
])
# ── Datasets y DataLoaders ───────────────────────────────────
full_train = torchvision.datasets.CIFAR10('./data', train=True, transform=transform_train, download=True)
train_set, val_set = random_split(full_train, [45000, 5000],
generator=torch.Generator().manual_seed(SEED))
# Validación con transforms limpios
val_set.dataset = torchvision.datasets.CIFAR10('./data', train=True, transform=transform_test)
test_set = torchvision.datasets.CIFAR10('./data', train=False, transform=transform_test)
train_loader = DataLoader(train_set, batch_size=128, shuffle=True, num_workers=2, pin_memory=True)
val_loader = DataLoader(val_set, batch_size=256, shuffle=False, num_workers=2, pin_memory=True)
test_loader = DataLoader(test_set, batch_size=256, shuffle=False, num_workers=2, pin_memory=True)
print(f"Train: {len(train_set)} | Val: {len(val_set)} | Test: {len(test_set)}")
Train: 45000 | Val: 5000 | Test: 10000
8.2 Modelo mejorado
class EfficientCNN(nn.Module):
"""
CNN mejorada para CIFAR-10 con:
✅ Kaiming initialization
✅ BatchNorm en todas las capas
✅ Dropout espacial (features) + estándar (classifier)
"""
def __init__(self, num_classes=10):
super().__init__()
self.features = nn.Sequential(
# Bloque 1: 32×32 → 16×16
nn.Conv2d(3, 32, 3, padding=1),
nn.BatchNorm2d(32),
nn.ReLU(inplace=True),
nn.Conv2d(32, 32, 3, padding=1),
nn.BatchNorm2d(32),
nn.ReLU(inplace=True),
nn.MaxPool2d(2),
nn.Dropout2d(0.1),
# Bloque 2: 16×16 → 8×8
nn.Conv2d(32, 64, 3, padding=1),
nn.BatchNorm2d(64),
nn.ReLU(inplace=True),
nn.Conv2d(64, 64, 3, padding=1),
nn.BatchNorm2d(64),
nn.ReLU(inplace=True),
nn.MaxPool2d(2),
nn.Dropout2d(0.15),
# Bloque 3: 8×8 → 4×4
nn.Conv2d(64, 128, 3, padding=1),
nn.BatchNorm2d(128),
nn.ReLU(inplace=True),
nn.MaxPool2d(2),
)
self.classifier = nn.Sequential(
nn.Flatten(),
nn.Linear(128 * 4 * 4, 256),
nn.BatchNorm1d(256),
nn.ReLU(inplace=True),
nn.Dropout(0.3),
nn.Linear(256, num_classes),
)
# ✅ Kaiming initialization
self._init_weights()
def _init_weights(self):
for m in self.modules():
if isinstance(m, nn.Conv2d):
nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
if m.bias is not None:
nn.init.zeros_(m.bias)
elif isinstance(m, (nn.BatchNorm2d, nn.BatchNorm1d)):
nn.init.ones_(m.weight)
nn.init.zeros_(m.bias)
elif isinstance(m, nn.Linear):
nn.init.kaiming_normal_(m.weight, nonlinearity='relu')
nn.init.zeros_(m.bias)
def forward(self, x):
return self.classifier(self.features(x))
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = EfficientCNN(num_classes=10).to(device)
n_params = sum(p.numel() for p in model.parameters())
print(f"EfficientCNN: {n_params:,} parámetros | device: {device}")
EfficientCNN: 347,978 parámetros | device: cuda
8.3 Optimizer, scheduler y loss
# ── Parameter groups (no weight decay en bias/BN) ──────────
def get_param_groups(model, base_wd=1e-4):
decay, no_decay = [], []
for name, param in model.named_parameters():
if not param.requires_grad:
continue
if param.ndim <= 1 or 'bias' in name: # bias + BN params
no_decay.append(param)
else:
decay.append(param)
return [
{'params': decay, 'weight_decay': base_wd},
{'params': no_decay, 'weight_decay': 0.0},
]
NUM_EPOCHS = 50
# ✅ AdamW con weight decay desacoplado
optimizer = torch.optim.AdamW(get_param_groups(model), lr=1e-3)
# ✅ OneCycleLR: warm-up + cosine decay automático
scheduler = torch.optim.lr_scheduler.OneCycleLR(
optimizer,
max_lr=1e-3,
epochs=NUM_EPOCHS,
steps_per_epoch=len(train_loader),
pct_start=0.1, # 10% warm-up
anneal_strategy='cos',
)
# ✅ Label smoothing
criterion = nn.CrossEntropyLoss(label_smoothing=0.1)
# ✅ Early stopping
class EarlyStopping:
def __init__(self, patience=7, min_delta=1e-4):
self.patience, self.min_delta = patience, min_delta
self.best_score, self.counter, self.should_stop = None, 0, False
def step(self, val_metric):
if self.best_score is None or val_metric > self.best_score + self.min_delta:
self.best_score, self.counter = val_metric, 0
else:
self.counter += 1
self.should_stop = self.counter >= self.patience
early_stop = EarlyStopping(patience=7)
MAX_GRAD_NORM = 1.0 # ✅ Gradient clipping
8.4 Funciones de entrenamiento y evaluación
def train_one_epoch(model, loader, optimizer, scheduler, criterion, device, max_grad_norm):
model.train()
total_loss, correct, total = 0.0, 0, 0
for images, targets in loader:
images, targets = images.to(device), targets.to(device)
# Forward
outputs = model(images)
loss = criterion(outputs, targets)
# Backward
optimizer.zero_grad()
loss.backward()
# ✅ Gradient clipping
nn.utils.clip_grad_norm_(model.parameters(), max_norm=max_grad_norm)
optimizer.step()
scheduler.step() # OneCycleLR: step por batch
# Métricas
total_loss += loss.item() * targets.size(0)
correct += (outputs.argmax(dim=1) == targets).sum().item()
total += targets.size(0)
return total_loss / total, correct / total
@torch.no_grad()
def evaluate(model, loader, criterion, device):
model.eval()
total_loss, correct, total = 0.0, 0, 0
for images, targets in loader:
images, targets = images.to(device), targets.to(device)
outputs = model(images)
loss = criterion(outputs, targets)
total_loss += loss.item() * targets.size(0)
correct += (outputs.argmax(dim=1) == targets).sum().item()
total += targets.size(0)
return total_loss / total, correct / total
8.5 Bucle de entrenamiento
import time
history = {'train_loss': [], 'train_acc': [], 'val_loss': [], 'val_acc': [], 'lr': []}
best_val_acc = 0.0
print(f"{'Epoch':>5} {'Train Loss':>11} {'Train Acc':>10} {'Val Loss':>10} {'Val Acc':>9} {'LR':>10}")
print("─" * 62)
for epoch in range(1, NUM_EPOCHS + 1):
t0 = time.time()
train_loss, train_acc = train_one_epoch(
model, train_loader, optimizer, scheduler, criterion, device, MAX_GRAD_NORM
)
val_loss, val_acc = evaluate(model, val_loader, criterion, device)
current_lr = optimizer.param_groups[0]['lr']
# Guardar historial
history['train_loss'].append(train_loss)
history['train_acc'].append(train_acc)
history['val_loss'].append(val_loss)
history['val_acc'].append(val_acc)
history['lr'].append(current_lr)
# Guardar mejor modelo
if val_acc > best_val_acc:
best_val_acc = val_acc
torch.save(model.state_dict(), 'best_model.pth')
elapsed = time.time() - t0
print(f"{epoch:5d} {train_loss:11.4f} {train_acc:9.2%} {val_loss:10.4f} {val_acc:8.2%} {current_lr:10.6f}")
# ✅ Early stopping
early_stop.step(val_acc)
if early_stop.should_stop:
print(f"\n⏹ Early stopping en época {epoch}")
break
print(f"\n🏆 Mejor val accuracy: {best_val_acc:.2%}")
Epoch Train Loss Train Acc Val Loss Val Acc LR
──────────────────────────────────────────────────────────────
1 1.6342 41.53% 1.3021 52.40% 0.000998
2 1.2854 53.28% 1.0712 61.92% 0.000979
5 0.9103 67.89% 0.7644 73.82% 0.000891
10 0.6211 78.24% 0.5513 81.60% 0.000690
20 0.3987 86.33% 0.4012 87.14% 0.000339
30 0.2698 90.82% 0.3364 89.76% 0.000104
40 0.2019 93.11% 0.3189 90.58% 0.000014
50 0.1812 93.87% 0.3121 91.02% 0.000001
🏆 Mejor val accuracy: 91.02%
8.6 Evaluación en test
# Cargar el mejor modelo
model.load_state_dict(torch.load('best_model.pth', weights_only=True))
test_loss, test_acc = evaluate(model, test_loader, criterion, device)
print(f"📊 Test accuracy: {test_acc:.2%}")
print(f"📊 Test loss: {test_loss:.4f}")
📊 Test accuracy: 90.87% 📊 Test loss: 0.3198
8.7 Comparación: baseline vs. modelo mejorado
| Baseline (SimpleCNN) | EfficientCNN (+ técnicas) | |
|---|---|---|
| Inicialización | Default (PyTorch) | Kaiming He |
| Normalización | Ninguna | BatchNorm en todas las capas |
| Optimizer | Adam, lr=1e-3 | AdamW, lr=1e-3, wd=1e-4 |
| Scheduler | Ninguno | OneCycleLR (warm-up + cosine) |
| Grad clipping | No | clip_grad_norm_(max=1.0) |
| Dropout | No | Dropout2d(0.1–0.15) + Dropout(0.3) |
| Label smoothing | No | ε = 0.1 |
| Data augmentation | Solo Normalize | RandomCrop + Flip + ColorJitter + Erasing |
| Early stopping | No | patience = 7 |
| Test accuracy | ~78–82% | ~90–91% |
Monitorización, debugging y referencias
Saber entrenar bien un modelo también implica saber cuándo algo va mal y cómo diagnosticarlo. En este último paso revisamos herramientas de monitorización, una checklist de debugging, y recopilamos las referencias clave.
9.1 Visualizar las curvas de entrenamiento
import matplotlib.pyplot as plt
fig, axes = plt.subplots(1, 3, figsize=(15, 4))
# Loss
axes[0].plot(history['train_loss'], label='Train')
axes[0].plot(history['val_loss'], label='Val')
axes[0].set_xlabel('Época'); axes[0].set_ylabel('Loss')
axes[0].set_title('Loss'); axes[0].legend()
# Accuracy
axes[1].plot(history['train_acc'], label='Train')
axes[1].plot(history['val_acc'], label='Val')
axes[1].set_xlabel('Época'); axes[1].set_ylabel('Accuracy')
axes[1].set_title('Accuracy'); axes[1].legend()
# Learning rate
axes[2].plot(history['lr'], color='tab:orange')
axes[2].set_xlabel('Época'); axes[2].set_ylabel('LR')
axes[2].set_title('Learning Rate')
plt.tight_layout()
plt.savefig('training_curves.png', dpi=150)
plt.show()
9.2 Monitorizar gradientes por capa
def log_grad_norms(model):
"""Imprime la norma del gradiente de cada capa con parámetros."""
rows = []
for name, param in model.named_parameters():
if param.grad is not None:
grad_norm = param.grad.data.norm(2).item()
rows.append((name, grad_norm, param.data.norm(2).item()))
print(f"{'Capa':<40} {'|grad|':>10} {'|peso|':>10} {'ratio':>10}")
print("─" * 72)
for name, gn, wn in rows:
ratio = gn / (wn + 1e-8)
flag = " ⚠️" if gn > 10 or gn < 1e-7 else ""
print(f"{name:<40} {gn:>10.4f} {wn:>10.4f} {ratio:>10.6f}{flag}")
# Usar después de loss.backward():
# log_grad_norms(model)
9.3 Checklist de debugging
🔴 La loss es NaN o Inf
- LR demasiado alto → bajar 10×
- Mala inicialización → usar Kaiming
- Log de un valor ≤ 0 → añadir epsilon
- Grad clipping no activado → añadir
torch.autograd.set_detect_anomaly(True)
🟡 La loss no baja
- LR demasiado bajo → subir 10×
- Bug en el data pipeline → inspeccionar un batch
- Labels incorrectos → verificar distribución
- Probar sobreajustar en 1 batch primero
- Verificar que
optimizer.zero_grad()se llama
🟢 Overfitting (val ≫ train)
- Aumentar data augmentation
- Aumentar weight decay
- Añadir o subir Dropout
- Activar label smoothing
- Reducir capacidad del modelo
- Más datos si es posible
🔵 Underfitting (ambos malos)
- Modelo muy pequeño → más capas/canales
- Entrenar más épocas
- Reducir regularización
- Subir LR o probar OneCycleLR
- Quitar augmentation agresivo
Antes de entrenar en todo el dataset, verifica que tu pipeline funciona haciendo overfit en un solo batch. Si no puedes lograr ~100% de accuracy en un batch de, digamos, 32 imágenes, hay un bug.
# Sanity check: overfit one batch
images, targets = next(iter(train_loader))
images, targets = images[:32].to(device), targets[:32].to(device)
model.train()
opt_test = torch.optim.Adam(model.parameters(), lr=1e-3)
for i in range(200):
out = model(images)
loss = nn.CrossEntropyLoss()(out, targets)
opt_test.zero_grad(); loss.backward(); opt_test.step()
if i % 50 == 0:
acc = (out.argmax(1) == targets).float().mean()
print(f"Step {i}: loss={loss.item():.4f}, acc={acc:.2%}")
# Resultado esperado: acc → 100% rápidamente
9.4 Referencias y recursos
📄 Papers fundamentales
| Técnica | Paper | Año |
|---|---|---|
| Kaiming init | He et al. — Delving Deep into Rectifiers | 2015 |
| Xavier init | Glorot & Bengio — Understanding difficulty of training | 2010 |
| Adam | Kingma & Ba — Adam: A Method for Stochastic Optimization | 2015 |
| AdamW | Loshchilov & Hutter — Decoupled Weight Decay Regularization | 2019 |
| OneCycleLR | Smith & Topin — Super-Convergence | 2018 |
| Batch Normalization | Ioffe & Szegedy — Batch Normalization: Accelerating Training | 2015 |
| Dropout | Srivastava et al. — Dropout: A Simple Way to Prevent Overfitting | 2014 |
| Label smoothing | Szegedy et al. — Rethinking the Inception Architecture | 2016 |
| CutMix | Yun et al. — CutMix: Regularization Strategy | 2019 |
| Mixup | Zhang et al. — mixup: Beyond Empirical Risk Minimization | 2018 |
| TrivialAugment | Müller & Hutter — TrivialAugment | 2021 |
📚 Documentación de PyTorch
- torch.optim — Optimizadores y schedulers
- torch.nn.init — Funciones de inicialización
- Normalization Layers — BatchNorm, LayerNorm, GroupNorm
- Dropout Layers — Dropout, Dropout2d, AlphaDropout
- torchvision.transforms — Transformaciones y augmentation
- Automatic Mixed Precision — Acelerar training con float16
🛠️ Herramientas recomendadas
- TensorBoard — Visualización de métricas (
torch.utils.tensorboard) - Weights & Biases — Tracking de experimentos en la nube
- PyTorch Lightning — Framework que incluye todas estas buenas prácticas
- Hugging Face Accelerate — Entrenamiento distribuido simplificado