116 KiB
Executable File
116 KiB
Executable File
In [1]:
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)
Raíz del laboratorio: /home/aleleba/projects/cursos/Universidad/Procesamiento de Imagenes y Vision por Computadora/Labs/Lab2
In [2]:
from lib_modelos import (
cargar_csv,
division_estratificada,
ajustar_estandarizador,
transformar,
)
# Único archivo de datos permitido durante el desarrollo.
x, y, ids = cargar_csv(RAIZ / 'datos_publicos' / 'train_1000_desbalanceado.csv')
# División estratificada fija: 800 entrenamiento / 200 desarrollo,
# preservando la proporción 75 % clase 0 / 25 % clase 1 en ambas partes.
SEMILLA_DIVISION = 31
indice_train, indice_dev = division_estratificada(y, proporcion_dev=0.20, semilla=SEMILLA_DIVISION)
# La media y la desviación estándar se ajustan SOLO con entrenamiento,
# para no filtrar información de desarrollo hacia el preprocesamiento.
media_entrenamiento, desviacion_entrenamiento = ajustar_estandarizador(x[indice_train])
x_train = transformar(x[indice_train], media_entrenamiento, desviacion_entrenamiento)
x_dev = transformar(x[indice_dev], media_entrenamiento, desviacion_entrenamiento)
y_train, y_dev = y[indice_train], y[indice_dev]
# Comprobaciones requeridas.
print(len(y_train), len(y_dev))
print(y_train.mean(), y_dev.mean())
800 200 0.25 0.25
In [3]:
# El entrenamiento tiene 75 % clase 0 / 25 % clase 1. Para que la pérdida
# BCE ponderada no ignore la clase minoritaria, el peso positivo se calcula
# como la razón negativos/positivos en entrenamiento (compensa 3:1).
peso_positivo = (y_train == 0).sum() / (y_train == 1).sum()
print('Peso positivo:', peso_positivo)
Peso positivo: 3.0
In [4]:
configuraciones = [
{ # MLP angosto y poco profundo: referencia base.
'arquitectura': {'tipo': 'mlp', 'entrada': 4, 'ocultas': [24, 16]},
'entrenamiento': {'epocas': 100, 'batch_size': 64, 'learning_rate': 0.003},
'costo_fn': 5, 'costo_fp': 1,
},
{ # MLP más ancho: más capacidad, learning rate algo menor.
'arquitectura': {'tipo': 'mlp', 'entrada': 4, 'ocultas': [48, 24]},
'entrenamiento': {'epocas': 100, 'batch_size': 64, 'learning_rate': 0.002},
'costo_fn': 5, 'costo_fp': 1,
},
{ # Residual + BatchNorm angosto.
'arquitectura': {'tipo': 'mlp_residual_bn', 'entrada': 4, 'ancho': 24},
'entrenamiento': {'epocas': 100, 'batch_size': 64, 'learning_rate': 0.002},
'costo_fn': 5, 'costo_fp': 1,
},
{ # Residual + BatchNorm más ancho, learning rate menor para más estabilidad.
'arquitectura': {'tipo': 'mlp_residual_bn', 'entrada': 4, 'ancho': 40},
'entrenamiento': {'epocas': 100, 'batch_size': 64, 'learning_rate': 0.0015},
'costo_fn': 5, 'costo_fp': 1,
},
]
In [5]:
from lib_modelos import buscar_hiperparametros
mejor, resultados = buscar_hiperparametros(
configuraciones, x_train, y_train, x_dev, y_dev, semilla=41,
)
# Tabla compacta: una fila por configuración probada, ordenadas como se declararon.
encabezado = (
f"{'arquitectura':<16}{'capas/ancho':<14}{'lr':>8}{'epocas':>8}"
f"{'costo':>8}{'f1':>7}{'recall':>8}{'precision':>10}{'umbral':>8}"
)
print(encabezado)
for fila in resultados:
arquitectura = fila['configuracion']['arquitectura']
entrenamiento = fila['configuracion']['entrenamiento']
capas_o_ancho = arquitectura.get('ocultas', arquitectura.get('ancho'))
print(
f"{arquitectura['tipo']:<16}{str(capas_o_ancho):<14}"
f"{entrenamiento['learning_rate']:>8}{entrenamiento['epocas']:>8}"
f"{fila['costo']:>8}{fila['f1']:>7.3f}{fila['recall']:>8.3f}"
f"{fila['precision']:>10.3f}{fila['umbral']:>8.3f}"
)
print('Configuración seleccionada:', mejor['configuracion']['arquitectura'])
arquitectura capas/ancho lr epocas costo f1 recall precision umbral
mlp [24, 16] 0.003 100 43 0.715 0.980 0.563 0.050
mlp [48, 24] 0.002 100 43 0.796 0.900 0.714 0.315
mlp_residual_bn 24 0.002 100 55 0.686 0.940 0.540 0.060
mlp_residual_bn 40 0.0015 100 56 0.804 0.820 0.788 0.400
Configuración seleccionada: {'tipo': 'mlp', 'entrada': 4, 'ocultas': [48, 24]}
In [6]:
from lib_modelos import buscar_umbral, metricas
historia = mejor['historia']
epocas_transcurridas = range(1, len(historia['loss_train']) + 1)
figura, ejes = plt.subplots(1, 2, figsize=(10, 3.5))
ejes[0].plot(epocas_transcurridas, historia['loss_train'], label='train')
ejes[0].plot(epocas_transcurridas, historia['loss_dev'], label='dev')
ejes[0].set(xlabel='Época', ylabel='Pérdida BCE ponderada', title='Curvas de pérdida')
ejes[0].legend()
ejes[1].plot(epocas_transcurridas, historia['accuracy_dev'], label='accuracy dev')
ejes[1].plot(epocas_transcurridas, historia['f1_dev'], label='F1 dev')
ejes[1].set(xlabel='Época', ylabel='Métrica', title='Desarrollo')
ejes[1].legend()
plt.show()
# El umbral de "mejor" ya minimiza el costo en desarrollo; recalculamos el
# recorrido completo únicamente para graficar costo vs. umbral.
probabilidades_dev = mejor['modelo'].probabilidad(x_dev)
umbral_elegido, recorrido_umbrales = buscar_umbral(y_dev, probabilidades_dev, costo_fn=5, costo_fp=1)
plt.figure(figsize=(6, 3.5))
plt.plot([f['umbral'] for f in recorrido_umbrales], [f['costo'] for f in recorrido_umbrales])
plt.axvline(umbral_elegido['umbral'], color='crimson', linestyle='--',
label=f"umbral={umbral_elegido['umbral']:.3f}")
plt.xlabel('Umbral'); plt.ylabel('Costo esperado en desarrollo'); plt.legend()
plt.show()
metricas_dev = metricas(y_dev, probabilidades_dev, umbral_elegido['umbral'])
print('Umbral elegido:', umbral_elegido['umbral'])
print('Métricas en desarrollo (incluye matriz de confusión tp/fp/fn/tn):', metricas_dev)
Umbral elegido: 0.31499999999999995
Métricas en desarrollo (incluye matriz de confusión tp/fp/fn/tn): {'tp': 45, 'fp': 18, 'fn': 5, 'tn': 132, 'accuracy': 0.885, 'precision': 0.7142857142857143, 'recall': 0.9, 'especificidad': 0.88, 'f1': 0.7964601769911505}
In [7]:
from lib_modelos import guardar_modelo
# Carpeta propia del estudiante: "entrega", distinta de "entrega_solucion"
# (que es la referencia del instructor y no debe sobrescribirse).
ruta_modelo_elegido = RAIZ / 'entrega' / 'modelo_elegido.npz'
guardar_modelo(
ruta_modelo_elegido,
mejor['modelo'],
mejor['configuracion']['arquitectura'],
media_entrenamiento,
desviacion_entrenamiento,
)
# Este diccionario es el que se copia a inferencia_configurable.ipynb para
# reconstruir exactamente este modelo (arquitectura, pesos y umbral).
configuracion_inferencia = {
'ruta_pesos': ruta_modelo_elegido,
'arquitectura': mejor['configuracion']['arquitectura'],
'umbral': umbral_elegido['umbral'],
}
configuracion_inferencia
Out [7]:
{'ruta_pesos': PosixPath('/home/aleleba/projects/cursos/Universidad/Procesamiento de Imagenes y Vision por Computadora/Labs/Lab2/entrega/modelo_elegido.npz'),
'arquitectura': {'tipo': 'mlp', 'entrada': 4, 'ocultas': [48, 24]},
'umbral': 0.31499999999999995}