Files
2026-08-25 12:19:45 -06:00

7.3 KiB
Executable File

Laboratorio resuelto: clasificación tabular con MLP

Esta referencia separa 800/200 de forma estratificada, ajusta el estandarizador sólo en entrenamiento, pondera la clase positiva y elige configuración y umbral con desarrollo. El bloque final de evaluación privada es exclusivamente para el instructor.

In [ ]:
from pathlib import Path
import sys
import numpy as np
import matplotlib.pyplot as plt

candidatos = [Path.cwd().resolve(), Path.cwd().resolve() / 'dist' / 'laboratorio_clasificacion', Path.cwd().resolve().parent]
RAIZ = next((ruta for ruta in candidatos if (ruta / 'lib_modelos.py').exists()), None)
if RAIZ is None:
    raise FileNotFoundError('No se encontró la carpeta laboratorio_clasificacion.')
sys.path.insert(0, str(RAIZ))
print('Raíz del laboratorio:', RAIZ)
from lib_modelos import (cargar_csv, division_estratificada, ajustar_estandarizador, transformar, buscar_hiperparametros, buscar_umbral, metricas, guardar_modelo)

1. Datos y división

In [ ]:
x, y, ids = cargar_csv(RAIZ / 'datos_publicos' / 'train_1000_desbalanceado.csv')
indice_train, indice_dev = division_estratificada(y, proporcion_dev=.20, semilla=31)
media, desviacion = ajustar_estandarizador(x[indice_train])
x_train = transformar(x[indice_train], media, desviacion)
x_dev = transformar(x[indice_dev], media, desviacion)
y_train, y_dev = y[indice_train], y[indice_dev]
print('Train/dev:', len(y_train), len(y_dev))
print('Proporción positiva train/dev:', y_train.mean(), y_dev.mean())
peso_positivo = (y_train == 0).sum() / (y_train == 1).sum()
print('Peso positivo:', peso_positivo)

2. Decisiones para el desbalance

Se usa una BCE ponderada con peso positivo igual a negativos/positivos. Accuracy no basta: un clasificador que siempre predice cero tendría 75% de accuracy y recall nulo. La selección minimiza costo esperado en desarrollo, con costo de falso negativo 5 y costo de falso positivo 1; se reportan también precision, recall y F1.

In [ ]:
configuraciones = [
    {'arquitectura': {'tipo': 'mlp', 'entrada': 4, 'ocultas': [24, 16]},
     'entrenamiento': {'epocas': 100, 'batch_size': 64, 'learning_rate': .003}, 'costo_fn': 5, 'costo_fp': 1},
    {'arquitectura': {'tipo': 'mlp', 'entrada': 4, 'ocultas': [48, 24]},
     'entrenamiento': {'epocas': 100, 'batch_size': 64, 'learning_rate': .002}, 'costo_fn': 5, 'costo_fp': 1},
    {'arquitectura': {'tipo': 'mlp_residual_bn', 'entrada': 4, 'ancho': 24},
     'entrenamiento': {'epocas': 100, 'batch_size': 64, 'learning_rate': .002}, 'costo_fn': 5, 'costo_fp': 1},
    {'arquitectura': {'tipo': 'mlp_residual_bn', 'entrada': 4, 'ancho': 40},
     'entrenamiento': {'epocas': 100, 'batch_size': 64, 'learning_rate': .0015}, 'costo_fn': 5, 'costo_fp': 1},
]
mejor, resultados = buscar_hiperparametros(configuraciones, x_train, y_train, x_dev, y_dev, semilla=41)
In [ ]:
for n, r in enumerate(resultados, 1):
    a = r['configuracion']['arquitectura']
    print(n, a, 'costo=', r['costo'], 'umbral=', round(r['umbral'], 3),
          'F1=', round(r['f1'], 3), 'recall=', round(r['recall'], 3),
          'precision=', round(r['precision'], 3))
print('Selección:', mejor['configuracion']['arquitectura'])

3. Curvas y umbral

In [ ]:
historia = mejor['historia']
epocas = np.arange(1, len(historia['loss_train']) + 1)
fig, ax = plt.subplots(1, 2, figsize=(11, 3.5))
ax[0].plot(epocas, historia['loss_train'], label='train')
ax[0].plot(epocas, historia['loss_dev'], label='dev')
ax[0].set(xlabel='Época', ylabel='BCE ponderada', title='Pérdida'); ax[0].legend()
ax[1].plot(epocas, historia['accuracy_dev'], label='accuracy dev')
ax[1].plot(epocas, historia['f1_dev'], label='F1 dev')
ax[1].set(xlabel='Época', ylabel='Métrica', title='Desarrollo'); ax[1].legend()
plt.show()
In [ ]:
prob_dev = mejor['modelo'].probabilidad(x_dev)
umbral, recorrido = buscar_umbral(y_dev, prob_dev, costo_fn=5, costo_fp=1)
plt.figure(figsize=(6, 3.5))
plt.plot([f['umbral'] for f in recorrido], [f['costo'] for f in recorrido])
plt.axvline(umbral['umbral'], color='crimson', linestyle='--', label=f"umbral={umbral['umbral']:.3f}")
plt.xlabel('Umbral'); plt.ylabel('Costo esperado en desarrollo'); plt.legend(); plt.show()
print(umbral)
print(metricas(y_dev, prob_dev, umbral['umbral']))

4. Exportación

La selección se hizo sólo con desarrollo. El siguiente artefacto contiene los pesos, media, desviación y configuración necesaria para reproducir inferencia.

In [ ]:
guardar_modelo(RAIZ / 'entrega_solucion' / 'modelo_elegido.npz', mejor['modelo'],
               mejor['configuracion']['arquitectura'], media, desviacion)
CONFIGURACION = {
    'ruta_pesos': str(RAIZ / 'entrega_solucion' / 'modelo_elegido.npz'),
    'arquitectura': mejor['configuracion']['arquitectura'],
    'umbral': umbral['umbral'],
}
CONFIGURACION

5. Evaluación privada — instructor

Ejecute este bloque sólo cuando las decisiones estén cerradas. Nunca se devuelve este CSV al estudiantado.

In [ ]:
# BLOQUE DEL INSTRUCTOR
# x_test, y_test, _ = cargar_csv(RAIZ / 'instructor_privado' / 'test_200_balanceado.csv')
# prob_test = mejor['modelo'].probabilidad(transformar(x_test, media, desviacion))
# print(metricas(y_test, prob_test, CONFIGURACION['umbral']))