Impacto de la Inicialización de Pesos
Comparativa experimental de 6 estrategias de inicialización (ceros, unos, normal, uniforme, Xavier, He) sobre un MLP con FashionMNIST.
Impacto de la Inicialización de Pesos en Redes Neuronales
Comparativa experimental de 6 estrategias de inicialización sobre un MLP con FashionMNIST
¿Por qué importa cómo inicializamos los pesos?
Cuando creamos una red neuronal, todos sus pesos parten de un valor inicial. Podría parecer un detalle menor — al fin y al cabo, el optimizador los irá ajustando durante el entrenamiento. Pero la realidad es muy distinta: la inicialización de pesos determina si la red puede aprender, a qué velocidad lo hace y qué rendimiento final alcanza.
El problema fundamental tiene que ver con cómo se propagan las señales a través de las capas. En un MLP con $L$ capas ocultas, la salida de cada capa $l$ se calcula como:
$$z^{(l)} = W^{(l)} \cdot a^{(l-1)} + b^{(l)}, \qquad a^{(l)} = f(z^{(l)})$$
donde $W^{(l)}$ son los pesos, $a^{(l-1)}$ las activaciones de la capa anterior, $b^{(l)}$ los sesgos y $f$ la función de activación.
Si asumimos que los pesos $W_{ij}$ y las entradas $a_j$ son independientes y de media cero, la varianza de la pre-activación en cada capa es:
$$\text{Var}(z^{(l)}) = n_{l-1} \cdot \text{Var}(W^{(l)}) \cdot \text{Var}(a^{(l-1)})$$
donde $n_{l-1}$ es el número de neuronas de la capa anterior (fan-in). Esto tiene dos consecuencias inmediatas:
| Si $n_{l-1} \cdot \text{Var}(W) > 1$ | Si $n_{l-1} \cdot \text{Var}(W) < 1$ |
|---|---|
| La varianza crece capa a capa | La varianza decrece capa a capa |
| → Explosión de activaciones | → Desvanecimiento de activaciones |
| Gradientes enormes, NaN | Gradientes ~0, no aprende |
El objetivo de una buena inicialización es elegir $\text{Var}(W)$ de forma que la varianza se mantenga estable a lo largo de todas las capas.
Un ejemplo numérico rápido
Para entender la intuición, pensemos en una red con capas de 256 neuronas. Si inicializamos los pesos con una distribución $\mathcal{N}(0, 1)$ (media 0, varianza 1):
$$\text{Var}(z^{(l)}) = 256 \times 1 \times \text{Var}(a^{(l-1)}) = 256 \cdot \text{Var}(a^{(l-1)})$$
Es decir, la varianza se multiplica por 256 en cada capa. Tras 4 capas, la varianza se ha multiplicado por $256^4 \approx 4.3 \times 10^9$. ¡Los valores explotan!
En cambio, con inicialización He ($\text{Var}(W) = 2/n_{in} = 2/256$):
$$\text{Var}(z^{(l)}) = 256 \times \frac{2}{256} \times \text{Var}(a^{(l-1)}) = 2 \cdot \text{Var}(a^{(l-1)})$$
El factor 2 compensa exactamente el hecho de que ReLU anula la mitad de los valores (los negativos), por lo que $\text{Var}(a^{(l)}) \approx \text{Var}(a^{(l-1)})$: la varianza se mantiene estable.
Veamos esto en código:
import numpy as np
np.random.seed(42)
n_neurons = 256
n_layers = 4
# Simulamos la propagación de varianza con distintas inicializaciones
print("=" * 65)
print("Propagación de varianza a través de 4 capas (256 neuronas/capa)")
print("=" * 65)
# Señal de entrada simulada (por ejemplo, píxeles normalizados)
x = np.random.randn(1000, n_neurons) * 0.5 # Var ≈ 0.25
strategies = {
"Normal(0,1)": lambda n_in: np.random.randn(n_in, n_in) * 1.0,
"Uniform[-1,1]": lambda n_in: np.random.uniform(-1, 1, (n_in, n_in)),
"Xavier": lambda n_in: np.random.randn(n_in, n_in) * np.sqrt(2.0 / (n_in + n_in)),
"He/Kaiming": lambda n_in: np.random.randn(n_in, n_in) * np.sqrt(2.0 / n_in),
}
for name, weight_fn in strategies.items():
a = x.copy()
print(f"\n📊 {name}:")
print(f" Entrada → Var = {np.var(a):.6f}")
for l in range(n_layers):
W = weight_fn(n_neurons)
z = a @ W.T
a = np.maximum(0, z) # ReLU
var = np.var(a)
status = "✅" if 0.01 < var < 100 else ("💥 EXPLOTA" if var > 100 else "📉 Se desvanece")
print(f" Capa {l+1} → Var = {var:.6f} {status}")
print("\n" + "=" * 65)
print("💡 Solo He/Kaiming mantiene la varianza estable con ReLU")
print("=" * 65)
================================================================= Propagación de varianza a través de 4 capas (256 neuronas/capa) ================================================================= 📊 Normal(0,1): Entrada → Var = 0.250069 Capa 1 → Var = 21.908540 ✅ Capa 2 → Var = 3054.449559 💥 EXPLOTA Capa 3 → Var = 401112.449012 💥 EXPLOTA Capa 4 → Var = 46439070.003931 💥 EXPLOTA 📊 Uniform[-1,1]: Entrada → Var = 0.250069 Capa 1 → Var = 7.291779 ✅ Capa 2 → Var = 306.495569 💥 EXPLOTA Capa 3 → Var = 14505.010660 💥 EXPLOTA Capa 4 → Var = 667639.630781 💥 EXPLOTA 📊 Xavier: Entrada → Var = 0.250069 Capa 1 → Var = 0.085043 ✅ Capa 2 → Var = 0.039854 ✅ Capa 3 → Var = 0.018639 ✅ Capa 4 → Var = 0.008048 📉 Se desvanece 📊 He/Kaiming: Entrada → Var = 0.250069 Capa 1 → Var = 0.172315 ✅ Capa 2 → Var = 0.169230 ✅ Capa 3 → Var = 0.171975 ✅ Capa 4 → Var = 0.177531 ✅ ================================================================= 💡 Solo He/Kaiming mantiene la varianza estable con ReLU =================================================================
Las 6 estrategias que compararemos
En este notebook vamos a poner a prueba 6 estrategias de inicialización sobre la misma red y el mismo dataset, para ver experimentalmente qué ocurre con cada una:
| # | Estrategia | Distribución | Hipótesis |
|---|---|---|---|
| 1 | Todo ceros | $W_{ij} = 0$ | ❌ Simetría perfecta: todas las neuronas son idénticas |
| 2 | Todo unos | $W_{ij} = 1$ | ❌ Activaciones explotan exponencialmente por capa |
| 3 | Normal estándar | $W_{ij} \sim \mathcal{N}(0, 1)$ | ⚠️ $\text{Var}(z) = n_{in} \cdot 1$ — varianza crece con el ancho |
| 4 | Uniforme [-1, 1] | $W_{ij} \sim \mathcal{U}(-1, 1)$ | ⚠️ $\text{Var}(W) = 1/3$, no se adapta a la arquitectura |
| 5 | Xavier/Glorot | $W_{ij} \sim \mathcal{N}!\left(0,; \frac{2}{n_{in}+n_{out}}\right)$ | ✅ Equilibra forward y backward (ideal para tanh/sigmoid) |
| 6 | He/Kaiming | $W_{ij} \sim \mathcal{N}!\left(0,; \frac{2}{n_{in}}\right)$ | ✅ Corrige Xavier para ReLU (compensa el factor $\frac{1}{2}$) |
Diseño del experimento
Para que la comparativa sea justa y las diferencias sean atribuibles únicamente a la inicialización, fijamos todo lo demás:
| Componente | Elección | Justificación |
|---|---|---|
| Dataset | FashionMNIST (60k train / 10k test) | Más exigente que MNIST clásico: prendas de ropa con texturas similares hacen más evidentes las diferencias entre inicializaciones |
| Modelo | MLP con 4 capas ocultas × 256 neuronas | Profundidad suficiente para amplificar los problemas de varianza. Con 2 capas las diferencias serían mínimas |
| Activación | ReLU | La más usada en la práctica. Anula el 50% de las pre-activaciones, lo que afecta directamente a la propagación de varianza |
| Optimizador | SGD (lr=0.01, momentum=0.9) | Deliberadamente no usamos Adam, porque su tasa de aprendizaje adaptativa enmascara las malas inicializaciones |
| Épocas | 15 | Suficientes para ver convergencia (o falta de ella) |
| Seed | 42 (fija) | Mismo punto de partida estructural para todas las estrategias |
Estructura del notebook
- Importaciones y configuración
- Dataset: carga y visualización de FashionMNIST
- Arquitectura del MLP: definición con hooks para capturar activaciones
- 6 funciones de inicialización: una por estrategia
- Activaciones iniciales: visualización antes de entrenar (¿explotan? ¿se desvanecen?)
- Entrenamiento comparativo: misma red, mismos datos, distinta inicialización
- Curvas de loss y accuracy: evolución temporal
- Ranking final: ¿qué inicialización gana?
- Distribución de pesos: cómo quedaron los pesos después del entrenamiento
- Conclusiones: qué hemos aprendido y reglas prácticas
Empecemos. 👇
1. Importaciones y configuración
Cargamos PyTorch, torchvision (para el dataset), matplotlib (para las gráficas) y fijamos las semillas de aleatoriedad para garantizar la reproducibilidad del experimento.
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader
from torchvision import datasets, transforms
import matplotlib.pyplot as plt
import numpy as np
import copy
import warnings
warnings.filterwarnings('ignore')
# Reproducibilidad
torch.manual_seed(42)
np.random.seed(42)
# Dispositivo
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
print(f"Dispositivo: {device}")
print(f"PyTorch: {torch.__version__}")
Dispositivo: cpu PyTorch: 2.10.0+cu128
2. Dataset: FashionMNIST
Usamos FashionMNIST en lugar del MNIST clásico porque es más exigente (las prendas de ropa son más difíciles de distinguir que los dígitos), lo que hará más evidentes las diferencias entre inicializaciones.
Cada imagen es de 28×28 píxeles en escala de grises (784 features al aplanar).
# Transformación: normalizar a [0,1] y aplanar a vector de 784
transform = transforms.Compose([
transforms.ToTensor(), # [0,255] → [0,1]
])
# Descargar datasets
train_dataset = datasets.FashionMNIST(
root='./data', train=True, download=True, transform=transform
)
test_dataset = datasets.FashionMNIST(
root='./data', train=False, download=True, transform=transform
)
# DataLoaders
BATCH_SIZE = 256
train_loader = DataLoader(train_dataset, batch_size=BATCH_SIZE, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=BATCH_SIZE, shuffle=False)
# Clases
class_names = ['Camiseta', 'Pantalón', 'Jersey', 'Vestido', 'Abrigo',
'Sandalia', 'Camisa', 'Zapatilla', 'Bolso', 'Bota']
print(f"Train: {len(train_dataset)} imágenes")
print(f"Test: {len(test_dataset)} imágenes")
print(f"Clases: {len(class_names)}")
print(f"Forma de una imagen: {train_dataset[0][0].shape}")
100.0% 100.0% 100.0% 100.0%
Train: 60000 imágenes Test: 10000 imágenes Clases: 10 Forma de una imagen: torch.Size([1, 28, 28])
Veamos algunas muestras del dataset:
fig, axes = plt.subplots(2, 8, figsize=(14, 4))
for i, ax in enumerate(axes.flat):
img, label = train_dataset[i]
ax.imshow(img.squeeze(), cmap='gray')
ax.set_title(class_names[label], fontsize=8)
ax.axis('off')
plt.suptitle('Muestras de FashionMNIST', fontsize=12, fontweight='bold')
plt.tight_layout()
plt.show()
3. Definición del MLP
Definimos un MLP con 4 capas ocultas de 256 neuronas y activación ReLU. La profundidad es deliberada: con pocas capas, las diferencias entre inicializaciones son menos evidentes. Con 4 capas ocultas, los problemas de varianza se amplifican y las malas inicializaciones fracasan claramente.
También registramos un hook en cada capa para capturar las activaciones durante el forward pass — esto nos permitirá visualizar cómo se propagan las activaciones con cada inicialización.
class MLP(nn.Module):
"""MLP con capas ocultas y captura de activaciones."""
def __init__(self, input_dim=784, hidden_dim=256, output_dim=10, n_hidden=4):
super().__init__()
layers = []
# Primera capa oculta
layers.append(nn.Linear(input_dim, hidden_dim))
layers.append(nn.ReLU())
# Capas ocultas intermedias
for _ in range(n_hidden - 1):
layers.append(nn.Linear(hidden_dim, hidden_dim))
layers.append(nn.ReLU())
# Capa de salida (sin activación — se usa CrossEntropyLoss)
layers.append(nn.Linear(hidden_dim, output_dim))
self.network = nn.Sequential(*layers)
# Almacén de activaciones para análisis
self.activations = {}
self._register_hooks()
def _register_hooks(self):
"""Registra hooks para capturar activaciones de cada capa ReLU."""
layer_idx = 0
for module in self.network:
if isinstance(module, nn.ReLU):
name = f'relu_{layer_idx}'
module.register_forward_hook(self._make_hook(name))
layer_idx += 1
def _make_hook(self, name):
def hook(module, input, output):
self.activations[name] = output.detach().cpu()
return hook
def forward(self, x):
x = x.view(x.size(0), -1) # Aplanar: [B, 1, 28, 28] → [B, 784]
return self.network(x)
# Verificar arquitectura
model_test = MLP()
print(model_test)
n_params = sum(p.numel() for p in model_test.parameters())
print(f"\nTotal parámetros: {n_params:,}")
MLP(
(network): Sequential(
(0): Linear(in_features=784, out_features=256, bias=True)
(1): ReLU()
(2): Linear(in_features=256, out_features=256, bias=True)
(3): ReLU()
(4): Linear(in_features=256, out_features=256, bias=True)
(5): ReLU()
(6): Linear(in_features=256, out_features=256, bias=True)
(7): ReLU()
(8): Linear(in_features=256, out_features=10, bias=True)
)
)
Total parámetros: 400,906
4. Funciones de inicialización
Definimos las 6 estrategias de inicialización que queremos comparar. Cada función recibe un modelo y modifica sus pesos in-place.
Recordemos qué dice la teoría:
- Ceros / Unos: Problema de simetría — todas las neuronas computan lo mismo.
- Normal estándar ($\sigma=1$): La varianza crece como $n_{in}$ por capa → explosión.
- Uniforme [-1, 1]: Similar problema — la varianza no se adapta al tamaño de la capa.
- Xavier: $\text{Var}(W) = \frac{2}{n_{in}+n_{out}}$ — mantiene la varianza estable con activaciones lineales/tanh/sigmoid.
- He: $\text{Var}(W) = \frac{2}{n_{in}}$ — corrige Xavier para ReLU (que anula la mitad de los valores).
def init_zeros(model):
"""Inicializa todos los pesos a cero."""
for name, param in model.named_parameters():
nn.init.zeros_(param)
return model
def init_ones(model):
"""Inicializa todos los pesos a uno."""
for name, param in model.named_parameters():
nn.init.ones_(param)
return model
def init_normal(model):
"""Inicializa pesos con N(0, 1) — varianza demasiado grande."""
for name, param in model.named_parameters():
if 'weight' in name:
nn.init.normal_(param, mean=0.0, std=1.0)
elif 'bias' in name:
nn.init.zeros_(param)
return model
def init_uniform(model):
"""Inicializa pesos con U(-1, 1) — varianza no adaptada."""
for name, param in model.named_parameters():
if 'weight' in name:
nn.init.uniform_(param, a=-1.0, b=1.0)
elif 'bias' in name:
nn.init.zeros_(param)
return model
def init_xavier(model):
"""Inicialización Xavier/Glorot Normal — ideal para tanh/sigmoid."""
for name, param in model.named_parameters():
if 'weight' in name and param.dim() >= 2:
nn.init.xavier_normal_(param)
elif 'bias' in name:
nn.init.zeros_(param)
return model
def init_he(model):
"""Inicialización He/Kaiming Normal — ideal para ReLU."""
for name, param in model.named_parameters():
if 'weight' in name and param.dim() >= 2:
nn.init.kaiming_normal_(param, mode='fan_in', nonlinearity='relu')
elif 'bias' in name:
nn.init.zeros_(param)
return model
# Diccionario con todas las estrategias
INIT_STRATEGIES = {
'Ceros': init_zeros,
'Unos': init_ones,
'Normal(0,1)': init_normal,
'Uniform[-1,1]':init_uniform,
'Xavier': init_xavier,
'He/Kaiming': init_he,
}
print(f"Estrategias definidas: {list(INIT_STRATEGIES.keys())}")
Estrategias definidas: ['Ceros', 'Unos', 'Normal(0,1)', 'Uniform[-1,1]', 'Xavier', 'He/Kaiming']
5. Visualización de las activaciones iniciales
Antes de entrenar, veamos cómo se propagan las activaciones a lo largo de las 4 capas ReLU con cada inicialización. Esto nos permite ver el efecto inmediato de la inicialización sobre la distribución de valores.
Esperamos:
- Ceros: Todas las activaciones son 0 (ReLU(0)=0).
- Unos: Activaciones enormes que crecen exponencialmente.
- Normal(0,1): Activaciones que explotan o se saturan según la capa.
- Xavier/He: Activaciones razonablemente estables a lo largo de las capas.
def capture_activations(model, data_loader):
"""Pasa un batch por el modelo y devuelve las activaciones capturadas."""
model.eval()
images, _ = next(iter(data_loader))
images = images.to(device)
with torch.no_grad():
_ = model(images)
return {k: v.numpy() for k, v in model.activations.items()}
# Capturar activaciones para cada inicialización
fig, axes = plt.subplots(len(INIT_STRATEGIES), 4, figsize=(16, 14))
for row, (name, init_fn) in enumerate(INIT_STRATEGIES.items()):
# Crear modelo fresco e inicializar
torch.manual_seed(42)
model = MLP().to(device)
init_fn(model)
# Capturar activaciones
acts = capture_activations(model, train_loader)
for col, (layer_name, layer_acts) in enumerate(acts.items()):
ax = axes[row, col]
flat = layer_acts.flatten()
# Limitar rango para visualización
valid = flat[np.isfinite(flat)]
if len(valid) > 0 and np.std(valid) > 0:
clip_val = min(np.percentile(np.abs(valid), 99), 50)
clipped = np.clip(valid, -clip_val, clip_val)
ax.hist(clipped, bins=50, color='#0984E3', alpha=0.7, density=True)
ax.set_title(f'{layer_name}', fontsize=8)
ax.text(0.95, 0.95, f'μ={np.mean(valid):.2f}\nσ={np.std(valid):.2f}',
transform=ax.transAxes, ha='right', va='top', fontsize=7,
bbox=dict(boxstyle='round,pad=0.3', facecolor='white', alpha=0.8))
else:
ax.text(0.5, 0.5, 'Todo ceros\no NaN/Inf',
transform=ax.transAxes, ha='center', va='center', fontsize=9)
ax.set_title(f'{layer_name}', fontsize=8)
if col == 0:
ax.set_ylabel(name, fontsize=9, fontweight='bold')
ax.tick_params(labelsize=6)
plt.suptitle('Distribución de activaciones ANTES del entrenamiento (por capa)',
fontsize=13, fontweight='bold', y=1.01)
plt.tight_layout()
plt.show()
Observaciones clave:
- Ceros: todas las activaciones son exactamente 0. La red no puede aprender porque no hay diferencia entre neuronas (simetría perfecta).
- Unos: las activaciones crecen enormemente con cada capa. Valores extremos que causan inestabilidad numérica.
- Normal(0,1): la distribución se distorsiona mucho — demasiada varianza para 784 (o 256) entradas por capa.
- Uniforme[-1,1]: similar a Normal(0,1) — la varianza no está adaptada al tamaño de la capa.
- Xavier: distribución más concentrada y estable, aunque no es óptima para ReLU.
- He/Kaiming: distribución estable a lo largo de todas las capas — exactamente lo que queremos con ReLU.
6. Entrenamiento comparativo
Ahora entrenamos el MLP durante 15 épocas con cada inicialización, usando exactamente los mismos hiperparámetros:
- Optimizador: SGD con learning rate 0.01 y momentum 0.9
- Loss: CrossEntropyLoss
- Batch size: 256
Usamos SGD (no Adam) deliberadamente: Adam enmascara parcialmente las malas inicializaciones porque adapta el learning rate por parámetro. Con SGD, el efecto de la inicialización es más evidente.
def train_model(model, train_loader, test_loader, epochs=15, lr=0.01):
"""Entrena un modelo y devuelve métricas por época."""
model = model.to(device)
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(model.parameters(), lr=lr, momentum=0.9)
history = {
'train_loss': [],
'test_loss': [],
'train_acc': [],
'test_acc': [],
}
for epoch in range(epochs):
# ── Entrenamiento ──
model.train()
running_loss = 0.0
correct = 0
total = 0
for images, labels in train_loader:
images, labels = images.to(device), labels.to(device)
optimizer.zero_grad()
outputs = model(images)
loss = criterion(outputs, labels)
# Proteger contra NaN
if torch.isnan(loss) or torch.isinf(loss):
history['train_loss'].append(float('nan'))
history['test_loss'].append(float('nan'))
history['train_acc'].append(0.0)
history['test_acc'].append(0.0)
print(f" Epoch {epoch+1}: ⚠️ NaN/Inf detectado — entrenamiento abortado")
return history
loss.backward()
# Gradient clipping para evitar explosión total
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=5.0)
optimizer.step()
running_loss += loss.item() * images.size(0)
_, predicted = torch.max(outputs, 1)
total += labels.size(0)
correct += (predicted == labels).sum().item()
train_loss = running_loss / total
train_acc = 100.0 * correct / total
# ── Evaluación ──
model.eval()
test_loss = 0.0
correct = 0
total = 0
with torch.no_grad():
for images, labels in test_loader:
images, labels = images.to(device), labels.to(device)
outputs = model(images)
loss = criterion(outputs, labels)
test_loss += loss.item() * images.size(0)
_, predicted = torch.max(outputs, 1)
total += labels.size(0)
correct += (predicted == labels).sum().item()
test_loss /= total
test_acc = 100.0 * correct / total
history['train_loss'].append(train_loss)
history['test_loss'].append(test_loss)
history['train_acc'].append(train_acc)
history['test_acc'].append(test_acc)
print(f" Epoch {epoch+1:2d}/{epochs} | "
f"Train Loss: {train_loss:.4f} | Train Acc: {train_acc:.1f}% | "
f"Test Acc: {test_acc:.1f}%")
return history
print("Función de entrenamiento definida ✅")
Función de entrenamiento definida ✅
# ── Entrenar con cada estrategia ──
EPOCHS = 15
results = {}
for name, init_fn in INIT_STRATEGIES.items():
print(f"\n{'='*60}")
print(f"🔧 Inicialización: {name}")
print(f"{'='*60}")
# Crear modelo fresco con la misma seed
torch.manual_seed(42)
model = MLP().to(device)
init_fn(model)
# Verificar distribución de pesos inicial
all_weights = []
for pname, param in model.named_parameters():
if 'weight' in pname:
all_weights.append(param.data.cpu().numpy().flatten())
all_w = np.concatenate(all_weights)
print(f" Pesos iniciales: μ={np.mean(all_w):.4f}, σ={np.std(all_w):.4f}, "
f"min={np.min(all_w):.4f}, max={np.max(all_w):.4f}")
# Entrenar
history = train_model(model, train_loader, test_loader, epochs=EPOCHS)
results[name] = history
print(f"\n{'='*60}")
print("✅ Entrenamiento completado para todas las estrategias")
print(f"{'='*60}")
============================================================ 🔧 Inicialización: Ceros ============================================================ Pesos iniciales: μ=0.0000, σ=0.0000, min=0.0000, max=0.0000 Epoch 1/15 | Train Loss: 2.3027 | Train Acc: 9.8% | Test Acc: 10.0% Epoch 2/15 | Train Loss: 2.3027 | Train Acc: 9.9% | Test Acc: 10.0% Epoch 3/15 | Train Loss: 2.3027 | Train Acc: 9.7% | Test Acc: 10.0% Epoch 4/15 | Train Loss: 2.3027 | Train Acc: 9.7% | Test Acc: 10.0% Epoch 5/15 | Train Loss: 2.3027 | Train Acc: 9.9% | Test Acc: 10.0% Epoch 6/15 | Train Loss: 2.3027 | Train Acc: 9.9% | Test Acc: 10.0% Epoch 7/15 | Train Loss: 2.3027 | Train Acc: 10.1% | Test Acc: 10.0% Epoch 8/15 | Train Loss: 2.3027 | Train Acc: 9.9% | Test Acc: 10.0% Epoch 9/15 | Train Loss: 2.3027 | Train Acc: 9.8% | Test Acc: 10.0% Epoch 10/15 | Train Loss: 2.3027 | Train Acc: 9.9% | Test Acc: 10.0% Epoch 11/15 | Train Loss: 2.3027 | Train Acc: 9.8% | Test Acc: 10.0% Epoch 12/15 | Train Loss: 2.3027 | Train Acc: 9.8% | Test Acc: 10.0% Epoch 13/15 | Train Loss: 2.3027 | Train Acc: 9.9% | Test Acc: 10.0% Epoch 14/15 | Train Loss: 2.3027 | Train Acc: 9.8% | Test Acc: 10.0% Epoch 15/15 | Train Loss: 2.3027 | Train Acc: 9.9% | Test Acc: 10.0% ============================================================ 🔧 Inicialización: Unos ============================================================ Pesos iniciales: μ=1.0000, σ=0.0000, min=1.0000, max=1.0000 Epoch 1/15 | Train Loss: 5700047105.2373 | Train Acc: 10.1% | Test Acc: 10.0% Epoch 2/15 | Train Loss: 6110430329.6512 | Train Acc: 10.1% | Test Acc: 10.0% Epoch 3/15 | Train Loss: 5403961516.5781 | Train Acc: 9.8% | Test Acc: 10.0% Epoch 4/15 | Train Loss: 5466632587.2640 | Train Acc: 10.1% | Test Acc: 10.0% Epoch 5/15 | Train Loss: 6338690893.6875 | Train Acc: 10.1% | Test Acc: 10.0% Epoch 6/15 | Train Loss: 5438692141.7387 | Train Acc: 10.0% | Test Acc: 10.0% Epoch 7/15 | Train Loss: 5612628683.5712 | Train Acc: 10.0% | Test Acc: 10.0% Epoch 8/15 | Train Loss: 5912098270.6859 | Train Acc: 10.0% | Test Acc: 10.0% Epoch 9/15 | Train Loss: 5878740279.1595 | Train Acc: 9.9% | Test Acc: 10.0% Epoch 10/15 | Train Loss: 5930738921.1989 | Train Acc: 9.8% | Test Acc: 10.0% Epoch 11/15 | Train Loss: 5822634862.1824 | Train Acc: 10.1% | Test Acc: 10.0% Epoch 12/15 | Train Loss: 4982192605.0475 | Train Acc: 9.9% | Test Acc: 10.0% Epoch 13/15 | Train Loss: 5382116999.4411 | Train Acc: 10.1% | Test Acc: 10.0% Epoch 14/15 | Train Loss: 5438585349.4613 | Train Acc: 10.0% | Test Acc: 10.0% Epoch 15/15 | Train Loss: 5590234524.6037 | Train Acc: 10.0% | Test Acc: 10.0% ============================================================ 🔧 Inicialización: Normal(0,1) ============================================================ Pesos iniciales: μ=-0.0019, σ=0.9987, min=-4.4376, max=4.3865 Epoch 1/15 | Train Loss: 25387.8072 | Train Acc: 62.5% | Test Acc: 68.7% Epoch 2/15 | Train Loss: 4284.2057 | Train Acc: 75.5% | Test Acc: 74.4% Epoch 3/15 | Train Loss: 2915.9389 | Train Acc: 76.6% | Test Acc: 72.7% Epoch 4/15 | Train Loss: 2150.1975 | Train Acc: 77.4% | Test Acc: 75.7% Epoch 5/15 | Train Loss: 1754.7726 | Train Acc: 77.9% | Test Acc: 75.8% Epoch 6/15 | Train Loss: 1380.5892 | Train Acc: 78.3% | Test Acc: 76.7% Epoch 7/15 | Train Loss: 1159.7362 | Train Acc: 78.5% | Test Acc: 73.5% Epoch 8/15 | Train Loss: 971.4804 | Train Acc: 78.6% | Test Acc: 73.0% Epoch 9/15 | Train Loss: 810.5222 | Train Acc: 78.9% | Test Acc: 76.8% Epoch 10/15 | Train Loss: 666.8215 | Train Acc: 79.2% | Test Acc: 77.5% Epoch 11/15 | Train Loss: 609.3056 | Train Acc: 79.0% | Test Acc: 76.4% Epoch 12/15 | Train Loss: 526.4207 | Train Acc: 79.3% | Test Acc: 73.2% Epoch 13/15 | Train Loss: 451.1011 | Train Acc: 79.4% | Test Acc: 77.9% Epoch 14/15 | Train Loss: 404.5953 | Train Acc: 79.4% | Test Acc: 78.7% Epoch 15/15 | Train Loss: 350.1268 | Train Acc: 79.5% | Test Acc: 77.3% ============================================================ 🔧 Inicialización: Uniform[-1,1] ============================================================ Pesos iniciales: μ=-0.0001, σ=0.5771, min=-1.0000, max=1.0000 Epoch 1/15 | Train Loss: 1353.3067 | Train Acc: 66.9% | Test Acc: 74.5% Epoch 2/15 | Train Loss: 237.6351 | Train Acc: 75.9% | Test Acc: 75.0% Epoch 3/15 | Train Loss: 134.2124 | Train Acc: 77.3% | Test Acc: 75.4% Epoch 4/15 | Train Loss: 87.0338 | Train Acc: 77.9% | Test Acc: 76.8% Epoch 5/15 | Train Loss: 59.9987 | Train Acc: 78.0% | Test Acc: 75.3% Epoch 6/15 | Train Loss: 40.6396 | Train Acc: 78.1% | Test Acc: 77.2% Epoch 7/15 | Train Loss: 25.2148 | Train Acc: 77.6% | Test Acc: 76.0% Epoch 8/15 | Train Loss: 10.4494 | Train Acc: 69.2% | Test Acc: 51.9% Epoch 9/15 | Train Loss: 2.1406 | Train Acc: 26.5% | Test Acc: 21.9% Epoch 10/15 | Train Loss: 2.2811 | Train Acc: 17.8% | Test Acc: 15.6% Epoch 11/15 | Train Loss: 2.1702 | Train Acc: 16.2% | Test Acc: 15.9% Epoch 12/15 | Train Loss: 2.0976 | Train Acc: 19.2% | Test Acc: 25.3% Epoch 13/15 | Train Loss: 1.9032 | Train Acc: 25.5% | Test Acc: 23.2% Epoch 14/15 | Train Loss: 1.8768 | Train Acc: 25.9% | Test Acc: 26.3% Epoch 15/15 | Train Loss: 1.8820 | Train Acc: 25.9% | Test Acc: 26.5% ============================================================ 🔧 Inicialización: Xavier ============================================================ Pesos iniciales: μ=-0.0001, σ=0.0541, min=-0.3419, max=0.2791 Epoch 1/15 | Train Loss: 0.8200 | Train Acc: 71.6% | Test Acc: 81.7% Epoch 2/15 | Train Loss: 0.4551 | Train Acc: 83.9% | Test Acc: 81.9% Epoch 3/15 | Train Loss: 0.4088 | Train Acc: 85.4% | Test Acc: 84.8% Epoch 4/15 | Train Loss: 0.3728 | Train Acc: 86.6% | Test Acc: 86.1% Epoch 5/15 | Train Loss: 0.3588 | Train Acc: 87.1% | Test Acc: 86.3% Epoch 6/15 | Train Loss: 0.3411 | Train Acc: 87.5% | Test Acc: 85.5% Epoch 7/15 | Train Loss: 0.3191 | Train Acc: 88.5% | Test Acc: 86.9% Epoch 8/15 | Train Loss: 0.3065 | Train Acc: 88.7% | Test Acc: 86.8% Epoch 9/15 | Train Loss: 0.2975 | Train Acc: 89.0% | Test Acc: 86.3% Epoch 10/15 | Train Loss: 0.2873 | Train Acc: 89.4% | Test Acc: 86.3% Epoch 11/15 | Train Loss: 0.2756 | Train Acc: 89.9% | Test Acc: 87.6% Epoch 12/15 | Train Loss: 0.2686 | Train Acc: 90.1% | Test Acc: 87.4% Epoch 13/15 | Train Loss: 0.2587 | Train Acc: 90.5% | Test Acc: 87.5% Epoch 14/15 | Train Loss: 0.2588 | Train Acc: 90.3% | Test Acc: 87.6% Epoch 15/15 | Train Loss: 0.2442 | Train Acc: 91.0% | Test Acc: 87.5% ============================================================ 🔧 Inicialización: He/Kaiming ============================================================ Pesos iniciales: μ=-0.0001, σ=0.0718, min=-0.3922, max=0.3877 Epoch 1/15 | Train Loss: 0.6710 | Train Acc: 76.9% | Test Acc: 83.2% Epoch 2/15 | Train Loss: 0.4159 | Train Acc: 85.1% | Test Acc: 82.6% Epoch 3/15 | Train Loss: 0.3780 | Train Acc: 86.4% | Test Acc: 85.5% Epoch 4/15 | Train Loss: 0.3457 | Train Acc: 87.4% | Test Acc: 86.7% Epoch 5/15 | Train Loss: 0.3317 | Train Acc: 87.8% | Test Acc: 86.7% Epoch 6/15 | Train Loss: 0.3181 | Train Acc: 88.3% | Test Acc: 85.7% Epoch 7/15 | Train Loss: 0.2985 | Train Acc: 89.1% | Test Acc: 87.3% Epoch 8/15 | Train Loss: 0.2866 | Train Acc: 89.4% | Test Acc: 87.6% Epoch 9/15 | Train Loss: 0.2758 | Train Acc: 89.8% | Test Acc: 86.8% Epoch 10/15 | Train Loss: 0.2672 | Train Acc: 90.1% | Test Acc: 86.9% Epoch 11/15 | Train Loss: 0.2558 | Train Acc: 90.5% | Test Acc: 87.6% Epoch 12/15 | Train Loss: 0.2467 | Train Acc: 90.8% | Test Acc: 88.1% Epoch 13/15 | Train Loss: 0.2376 | Train Acc: 91.3% | Test Acc: 87.6% Epoch 14/15 | Train Loss: 0.2361 | Train Acc: 91.2% | Test Acc: 88.0% Epoch 15/15 | Train Loss: 0.2255 | Train Acc: 91.6% | Test Acc: 87.7% ============================================================ ✅ Entrenamiento completado para todas las estrategias ============================================================
7. Comparación de curvas de pérdida (loss)
Visualicemos cómo evoluciona la loss de entrenamiento y la loss de test con cada inicialización. Las inicializaciones mal elegidas mostrarán una loss que no baja (ceros), que explota (unos, normal) o que baja mucho más lentamente (uniforme).
# Colores para cada estrategia
colors = {
'Ceros': '#e74c3c', # rojo
'Unos': '#e67e22', # naranja
'Normal(0,1)': '#9b59b6', # púrpura
'Uniform[-1,1]': '#f39c12', # amarillo
'Xavier': '#2ecc71', # verde
'He/Kaiming': '#0984E3', # azul
}
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(15, 5))
for name, hist in results.items():
epochs_range = range(1, len(hist['train_loss']) + 1)
# Filtrar NaN para el plot
train_loss = [v if not np.isnan(v) else None for v in hist['train_loss']]
ax1.plot(epochs_range, train_loss, label=name, color=colors[name],
linewidth=2, marker='o', markersize=3)
test_loss = [v if not np.isnan(v) else None for v in hist['test_loss']]
ax2.plot(epochs_range, test_loss, label=name, color=colors[name],
linewidth=2, marker='o', markersize=3)
ax1.set_title('Loss de Entrenamiento', fontsize=12, fontweight='bold')
ax1.set_xlabel('Época')
ax1.set_ylabel('Cross-Entropy Loss')
ax1.legend(fontsize=8)
ax1.set_ylim(bottom=0)
ax1.grid(True, alpha=0.3)
ax2.set_title('Loss de Test', fontsize=12, fontweight='bold')
ax2.set_xlabel('Época')
ax2.set_ylabel('Cross-Entropy Loss')
ax2.legend(fontsize=8)
ax2.set_ylim(bottom=0)
ax2.grid(True, alpha=0.3)
plt.suptitle('Evolución de la Loss según la Inicialización',
fontsize=14, fontweight='bold', y=1.02)
plt.tight_layout()
plt.show()
8. Comparación de accuracy
La accuracy muestra el impacto práctico de cada inicialización en el rendimiento del clasificador.
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(15, 5))
for name, hist in results.items():
epochs_range = range(1, len(hist['train_acc']) + 1)
ax1.plot(epochs_range, hist['train_acc'], label=name,
color=colors[name], linewidth=2, marker='o', markersize=3)
ax2.plot(epochs_range, hist['test_acc'], label=name,
color=colors[name], linewidth=2, marker='o', markersize=3)
ax1.set_title('Accuracy de Entrenamiento', fontsize=12, fontweight='bold')
ax1.set_xlabel('Época')
ax1.set_ylabel('Accuracy (%)')
ax1.legend(fontsize=8)
ax1.set_ylim(0, 100)
ax1.axhline(y=10, color='gray', linestyle='--', alpha=0.5, label='Azar (10%)')
ax1.grid(True, alpha=0.3)
ax2.set_title('Accuracy de Test', fontsize=12, fontweight='bold')
ax2.set_xlabel('Época')
ax2.set_ylabel('Accuracy (%)')
ax2.legend(fontsize=8)
ax2.set_ylim(0, 100)
ax2.axhline(y=10, color='gray', linestyle='--', alpha=0.5, label='Azar (10%)')
ax2.grid(True, alpha=0.3)
plt.suptitle('Evolución del Accuracy según la Inicialización',
fontsize=14, fontweight='bold', y=1.02)
plt.tight_layout()
plt.show()
9. Resumen: accuracy final de test
Comparemos los resultados finales de cada estrategia en un gráfico de barras:
# Accuracy final de test para cada estrategia
final_accs = {}
for name, hist in results.items():
accs = hist['test_acc']
final_accs[name] = accs[-1] if accs else 0.0
# Ordenar de mayor a menor
sorted_names = sorted(final_accs, key=final_accs.get, reverse=True)
sorted_accs = [final_accs[n] for n in sorted_names]
sorted_colors = [colors[n] for n in sorted_names]
fig, ax = plt.subplots(figsize=(10, 5))
bars = ax.barh(sorted_names, sorted_accs, color=sorted_colors, edgecolor='white',
linewidth=0.5, height=0.6)
# Etiquetas en cada barra
for bar, acc in zip(bars, sorted_accs):
ax.text(bar.get_width() + 0.5, bar.get_y() + bar.get_height()/2,
f'{acc:.1f}%', va='center', fontweight='bold', fontsize=10)
ax.set_xlabel('Test Accuracy (%)', fontsize=11)
ax.set_title('Accuracy Final de Test por Estrategia de Inicialización',
fontsize=13, fontweight='bold')
ax.set_xlim(0, 100)
ax.axvline(x=10, color='gray', linestyle='--', alpha=0.5)
ax.text(11, -0.5, 'Azar', color='gray', fontsize=8)
ax.grid(axis='x', alpha=0.3)
ax.invert_yaxis()
plt.tight_layout()
plt.show()
# Tabla resumen
print("\n" + "="*55)
print(f"{'Estrategia':<18} {'Test Acc':>10} {'Veredicto':>25}")
print("="*55)
for name in sorted_names:
acc = final_accs[name]
if acc < 15:
verdict = '❌ No aprende'
elif acc < 70:
verdict = '⚠️ Aprende mal'
elif acc < 85:
verdict = '🔶 Subóptimo'
else:
verdict = '✅ Buen resultado'
print(f"{name:<18} {acc:>9.1f}% {verdict:>25}")
print("="*55)
======================================================= Estrategia Test Acc Veredicto ======================================================= He/Kaiming 87.7% ✅ Buen resultado Xavier 87.5% ✅ Buen resultado Normal(0,1) 77.3% 🔶 Subóptimo Uniform[-1,1] 26.5% ⚠️ Aprende mal Ceros 10.0% ❌ No aprende Unos 10.0% ❌ No aprende =======================================================
10. Distribución de pesos tras el entrenamiento
Por último, visualicemos cómo quedaron distribuidos los pesos de la primera capa oculta después del entrenamiento. Las inicializaciones malas llevan a distribuciones de pesos problemáticas, mientras que Xavier y He mantienen distribuciones saludables.
# Re-entrenar brevemente para capturar pesos finales
# (ya tenemos los históricos, pero necesitamos los modelos finales para pesos)
final_models = {}
for name, init_fn in INIT_STRATEGIES.items():
torch.manual_seed(42)
model = MLP().to(device)
init_fn(model)
# Entrenar silenciosamente
model.train()
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.9)
for epoch in range(EPOCHS):
for images, labels in train_loader:
images, labels = images.to(device), labels.to(device)
optimizer.zero_grad()
outputs = model(images)
loss = criterion(outputs, labels)
if torch.isnan(loss) or torch.isinf(loss):
break
loss.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=5.0)
optimizer.step()
final_models[name] = model
# Visualizar distribución de pesos de la primera capa
fig, axes = plt.subplots(2, 3, figsize=(15, 8))
for idx, (name, model) in enumerate(final_models.items()):
ax = axes[idx // 3, idx % 3]
# Obtener pesos de la primera capa lineal
first_layer = model.network[0]
weights = first_layer.weight.data.cpu().numpy().flatten()
if np.all(np.isfinite(weights)) and np.std(weights) > 0:
ax.hist(weights, bins=80, color=colors[name], alpha=0.7, density=True)
ax.set_title(f'{name}\nμ={np.mean(weights):.4f}, σ={np.std(weights):.4f}',
fontsize=9, fontweight='bold')
else:
ax.text(0.5, 0.5, 'Pesos no finitos\no sin variación',
transform=ax.transAxes, ha='center', va='center', fontsize=10)
ax.set_title(f'{name}', fontsize=9, fontweight='bold')
ax.tick_params(labelsize=7)
ax.grid(True, alpha=0.2)
plt.suptitle('Distribución de pesos de la 1ª capa oculta DESPUÉS del entrenamiento (15 épocas)',
fontsize=12, fontweight='bold', y=1.01)
plt.tight_layout()
plt.show()
11. Conclusiones
Este experimento confirma las predicciones teóricas sobre la inicialización de pesos:
❌ Inicializaciones que NO funcionan:
| Estrategia | Problema | Resultado |
|---|---|---|
| Todo ceros | Simetría perfecta: todas las neuronas computan lo mismo | La red no aprende nada (~10% = azar) |
| Todo unos | Activaciones explotan exponencialmente por capa | Inestabilidad numérica, NaN en la loss |
⚠️ Inicializaciones subóptimas:
| Estrategia | Problema | Resultado |
|---|---|---|
| Normal(0,1) | $\text{Var}(z) = n_{in} \cdot 1$ — la varianza crece con el tamaño de la capa | Explosión/saturación parcial, entrenamiento inestable |
| Uniform[-1,1] | $\text{Var}(W) = 1/3$, no adaptada a $n_{in}$ | Problema similar a Normal(0,1), peor que Xavier/He |
✅ Inicializaciones recomendadas:
| Estrategia | Principio | Resultado |
|---|---|---|
| Xavier/Glorot | $\text{Var}(W) = \frac{2}{n_{in}+n_{out}}$ — equilibra forward y backward | Buen rendimiento, diseñada para tanh/sigmoid |
| He/Kaiming | $\text{Var}(W) = \frac{2}{n_{in}}$ — corrige para ReLU | Mejor resultado con ReLU, convergencia más rápida |
Lección principal
La inicialización de pesos no es un detalle menor. Con la inicialización correcta (He para ReLU, Xavier para tanh), la red converge rápido y alcanza buen rendimiento. Con una inicialización incorrecta, la misma red con los mismos datos y los mismos hiperparámetros puede no aprender absolutamente nada.
En la práctica, PyTorch ya usa He/Kaiming por defecto para capas Linear y Conv2d, así que a menudo no necesitas especificarlo manualmente. Pero es crucial entender por qué funciona, para poder diagnosticar problemas de entrenamiento y elegir la inicialización adecuada cuando uses activaciones no estándar.