LECCIÓN 9 · CONSTRUIR Y ENTRENAR

Entrenamiento completo

Juntamos todo lo aprendido en un entrenamiento de verdad: datos, lotes, bucle, validación para vigilar el overfitting, y cómo guardar y recargar el modelo entrenado.

1. Las piezas que ya tenemos

Un entrenamiento real combina todo el bloque intermedio:

2. El código completo (regresión real)

Predeciremos y = 2·x₁ − 3·x₂ + 1 (con ruido) a partir de 200 ejemplos, 160 para entrenar y 40 para validar:

# datos: 200 ejemplos con 2 features
X = torch.randn(200, 2)
y = (2*X[:,0] - 3*X[:,1] + 1).reshape(-1, 1)
Xtr, Ytr = X[:160], y[:160]      # entrenamiento
Xva, Yva = X[160:], y[160:]      # validación

tr_dl = DataLoader(TensorDataset(Xtr, Ytr), batch_size=32, shuffle=True)

net = nn.Sequential(nn.Linear(2, 16), nn.ReLU(), nn.Linear(16, 1))
opt = torch.optim.Adam(net.parameters(), lr=0.05)
loss_fn = nn.MSELoss()

for epoca in range(1, 41):
    net.train()                          # modo entrenamiento
    for xb, yb in tr_dl:                  # bucle de lotes
        opt.zero_grad()
        loss = loss_fn(net(xb), yb)
        loss.backward()
        opt.step()

    net.eval()                           # modo evaluación
    with torch.no_grad():                  # sin gradientes al validar
        val_loss = loss_fn(net(Xva), Yva)
salida real (pérdidas por época)epoca 1: train_loss=4.3737 val_loss=8.4813 epoca 5: train_loss=0.3235 val_loss=0.7114 epoca 10: train_loss=0.0511 val_loss=0.0980 epoca 20: train_loss=0.0179 val_loss=0.0147 epoca 40: train_loss=0.0134 val_loss=0.0127
Ambas pérdidas bajan juntas de ~8 a ~0.013: la red aprendió la relación y generaliza (val_loss baja igual que train_loss, sin dispararse → no hay overfitting). Esa vigilancia del val_loss es exactamente la lección de generalización aplicada en PyTorch.

3. Dos detalles nuevos: train() y eval()

LlamadaCuándoQué hace
net.train()antes de entrenarActiva dropout y BatchNorm en modo entrenamiento
net.eval()antes de validar/predecirLos pone en modo evaluación (comportamiento determinista)
🔑 Combina siempre net.eval() con with torch.no_grad(): al validar o predecir: eval() ajusta capas como dropout, y no_grad() ahorra memoria al no construir el grafo. Olvidar eval() da resultados peores e inconsistentes.

4. Guardar y cargar el modelo entrenado

Entrenar cuesta tiempo; no quieres repetirlo cada vez. Se guarda el state_dict (un diccionario con todos los pesos):

# guardar
torch.save(net.state_dict(), 'modelo.pt')

# cargar (hay que recrear la MISMA arquitectura primero)
net2 = nn.Sequential(nn.Linear(2,16), nn.ReLU(), nn.Linear(16,1))
net2.load_state_dict(torch.load('modelo.pt'))

¿Qué hay dentro del state_dict? Una entrada por cada peso y bias de cada capa:

salida real['fc1.weight', 'fc1.bias', 'fc2.weight', 'fc2.bias']

Tras cargar, el modelo predice igual que el original:

salida realpredicción = 0.7903 valor real = 0.9180
💾 Se guarda el state_dict (solo los pesos), no el objeto entero. Por eso al cargar debes recrear la arquitectura idéntica y luego inyectarle los pesos. Es la práctica recomendada en PyTorch.

5. Resumen — ¡ya sabes entrenar de verdad!

Un entrenamiento completo = datos en DataLoader + modelo + pérdida + optimizador + bucle de épocas/lotes, vigilando el val_loss para detectar overfitting, alternando train()/eval(), y guardando con state_dict. Con esto cierras el nivel intermedio: ya puedes construir y entrenar redes funcionales. El nivel avanzado (GPU, regularización, CNNs, transfer learning) son mejoras sobre esta misma base.