def crear_cargador(X, y, binaria, lote, barajar=False, semilla=42):
objetivo = torch.as_tensor(y, dtype=torch.float32 if binaria else torch.long)
if binaria:
objetivo = objetivo.reshape(-1, 1)
datos = TensorDataset(tensor_x(X), objetivo)
generador = torch.Generator().manual_seed(semilla)
return DataLoader(datos, batch_size=lote, shuffle=barajar,
generator=generador, num_workers=0)
@torch.inference_mode()
def evaluar(modelo, cargador, criterio, binaria):
modelo.eval()
suma, aciertos, n = 0.0, 0, 0
for Xb, yb in cargador:
Xb, yb = Xb.to(dispositivo), yb.to(dispositivo)
logits = modelo(Xb)
suma += criterio(logits, yb).item() * len(yb)
pred = (logits >= 0).to(yb.dtype) if binaria else logits.argmax(dim=1)
aciertos += int((pred == yb).sum().item())
n += len(yb)
return {'loss': suma/n, 'accuracy': aciertos/n}
def entrenar(modelo, Xtr, ytr, Xva, yva, *, binaria=True, lr=0.001,
epocas=500, lote=64, paciencia=40, optimizador='adam', semilla=42):
ajuste = crear_cargador(Xtr, ytr, binaria, lote, True, semilla)
lectura_tr = crear_cargador(Xtr, ytr, binaria, 512)
lectura_va = crear_cargador(Xva, yva, binaria, 512)
criterio = nn.BCEWithLogitsLoss() if binaria else nn.CrossEntropyLoss()
modelo.to(dispositivo)
opt = (torch.optim.Adam(modelo.parameters(), lr=lr) if optimizador == 'adam'
else torch.optim.SGD(modelo.parameters(), lr=lr))
mejor_loss, mejor_epoca, espera = float('inf'), 0, 0
historia = []
for epoca in range(1, epocas+1):
modelo.train()
for Xb, yb in ajuste:
Xb, yb = Xb.to(dispositivo), yb.to(dispositivo)
opt.zero_grad(set_to_none=True)
loss = criterio(modelo(Xb), yb)
loss.backward()
opt.step()
tr = evaluar(modelo, lectura_tr, criterio, binaria)
va = evaluar(modelo, lectura_va, criterio, binaria)
if not np.isfinite([tr['loss'], va['loss']]).all():
raise FloatingPointError('Pérdida no finita: revisar el experimento.')
historia.append({'epoca': epoca, 'train_loss': tr['loss'],
'val_loss': va['loss'], 'train_accuracy': tr['accuracy'],
'val_accuracy': va['accuracy']})
if va['loss'] < mejor_loss:
mejor_loss, mejor_epoca = va['loss'], epoca
mejor_estado = deepcopy(modelo.state_dict())
espera = 0
else:
espera += 1
if espera >= paciencia:
break
modelo.load_state_dict(mejor_estado)
modelo.eval()
restaurada = evaluar(modelo, lectura_va, criterio, binaria)['loss']
assert np.isclose(restaurada, mejor_loss, rtol=1e-5, atol=1e-7)
return pd.DataFrame(historia), mejor_epoca
def dibujar_historia(historia, mejor_epoca, titulo):
fig, axes = plt.subplots(1, 2, figsize=(11, 3.4))
for prefijo, nombre in [('train', 'Entrenamiento'), ('val', 'Validación')]:
axes[0].plot(historia.epoca, historia[prefijo+'_loss'], label=nombre)
axes[1].plot(historia.epoca, historia[prefijo+'_accuracy'], label=nombre)
for ax in axes:
ax.axvline(mejor_epoca, color='black', ls='--', alpha=.6,
label=f'Checkpoint: época {mejor_epoca}')
ax.set_xlabel('Época'); ax.legend(fontsize=8)
axes[0].set(ylabel='Entropía cruzada', title=titulo+' · pérdida')
axes[1].set(ylabel='Exactitud', title=titulo+' · aciertos', ylim=(0, 1.03))
plt.tight_layout(); plt.show()
resultados = []
particiones = []
def registrar_particiones(nombre, ids_por_parte):
for parte, ids in zip(['entrenamiento', 'validacion', 'prueba'], ids_por_parte):
particiones.extend({'experimento': nombre, 'indice': int(i),
'particion': parte} for i in ids)
def informe_final(nombre, modelo, Xtr, ytr, Xva, yva, Xte, yte,
historia, epoca, binaria=True):
filas = []
for parte, X, y in [('Entrenamiento', Xtr, ytr), ('Validación', Xva, yva), ('Prueba', Xte, yte)]:
pred = clases(modelo, X, binaria)
filas.append({'partición': parte, 'n': len(y),
'exactitud': accuracy_score(y, pred),
'exactitud balanceada': balanced_accuracy_score(y, pred),
'F1 macro': f1_score(y, pred, average='macro', zero_division=0)})
tabla = pd.DataFrame(filas)
display(tabla.round(3))
final = tabla.iloc[-1]
resultados.append({'experimento': nombre, 'checkpoint_epoca': int(epoca),
'epocas_recorridas': len(historia), 'n_train': len(ytr), 'n_val': len(yva),
'n_test': len(yte), 'test_accuracy': float(final['exactitud']),
'test_f1_macro': float(final['F1 macro'])})
historia.to_csv(ARTEFACTOS/(nombre+'_historia.csv'), index=False)
return tabla