{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Laboratorio resuelto: clasificación tabular con MLP\n", "\n", "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.\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from pathlib import Path\n", "import sys\n", "import numpy as np\n", "import matplotlib.pyplot as plt\n", "\n", "candidatos = [Path.cwd().resolve(), Path.cwd().resolve() / 'dist' / 'laboratorio_clasificacion', Path.cwd().resolve().parent]\n", "RAIZ = next((ruta for ruta in candidatos if (ruta / 'lib_modelos.py').exists()), None)\n", "if RAIZ is None:\n", " raise FileNotFoundError('No se encontró la carpeta laboratorio_clasificacion.')\n", "sys.path.insert(0, str(RAIZ))\n", "print('Raíz del laboratorio:', RAIZ)\n", "from lib_modelos import (cargar_csv, division_estratificada, ajustar_estandarizador, transformar, buscar_hiperparametros, buscar_umbral, metricas, guardar_modelo)\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 1. Datos y división\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "x, y, ids = cargar_csv(RAIZ / 'datos_publicos' / 'train_1000_desbalanceado.csv')\n", "indice_train, indice_dev = division_estratificada(y, proporcion_dev=.20, semilla=31)\n", "media, desviacion = ajustar_estandarizador(x[indice_train])\n", "x_train = transformar(x[indice_train], media, desviacion)\n", "x_dev = transformar(x[indice_dev], media, desviacion)\n", "y_train, y_dev = y[indice_train], y[indice_dev]\n", "print('Train/dev:', len(y_train), len(y_dev))\n", "print('Proporción positiva train/dev:', y_train.mean(), y_dev.mean())\n", "peso_positivo = (y_train == 0).sum() / (y_train == 1).sum()\n", "print('Peso positivo:', peso_positivo)\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 2. Decisiones para el desbalance\n", "\n", "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.\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "configuraciones = [\n", " {'arquitectura': {'tipo': 'mlp', 'entrada': 4, 'ocultas': [24, 16]},\n", " 'entrenamiento': {'epocas': 100, 'batch_size': 64, 'learning_rate': .003}, 'costo_fn': 5, 'costo_fp': 1},\n", " {'arquitectura': {'tipo': 'mlp', 'entrada': 4, 'ocultas': [48, 24]},\n", " 'entrenamiento': {'epocas': 100, 'batch_size': 64, 'learning_rate': .002}, 'costo_fn': 5, 'costo_fp': 1},\n", " {'arquitectura': {'tipo': 'mlp_residual_bn', 'entrada': 4, 'ancho': 24},\n", " 'entrenamiento': {'epocas': 100, 'batch_size': 64, 'learning_rate': .002}, 'costo_fn': 5, 'costo_fp': 1},\n", " {'arquitectura': {'tipo': 'mlp_residual_bn', 'entrada': 4, 'ancho': 40},\n", " 'entrenamiento': {'epocas': 100, 'batch_size': 64, 'learning_rate': .0015}, 'costo_fn': 5, 'costo_fp': 1},\n", "]\n", "mejor, resultados = buscar_hiperparametros(configuraciones, x_train, y_train, x_dev, y_dev, semilla=41)\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "for n, r in enumerate(resultados, 1):\n", " a = r['configuracion']['arquitectura']\n", " print(n, a, 'costo=', r['costo'], 'umbral=', round(r['umbral'], 3),\n", " 'F1=', round(r['f1'], 3), 'recall=', round(r['recall'], 3),\n", " 'precision=', round(r['precision'], 3))\n", "print('Selección:', mejor['configuracion']['arquitectura'])\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 3. Curvas y umbral\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "historia = mejor['historia']\n", "epocas = np.arange(1, len(historia['loss_train']) + 1)\n", "fig, ax = plt.subplots(1, 2, figsize=(11, 3.5))\n", "ax[0].plot(epocas, historia['loss_train'], label='train')\n", "ax[0].plot(epocas, historia['loss_dev'], label='dev')\n", "ax[0].set(xlabel='Época', ylabel='BCE ponderada', title='Pérdida'); ax[0].legend()\n", "ax[1].plot(epocas, historia['accuracy_dev'], label='accuracy dev')\n", "ax[1].plot(epocas, historia['f1_dev'], label='F1 dev')\n", "ax[1].set(xlabel='Época', ylabel='Métrica', title='Desarrollo'); ax[1].legend()\n", "plt.show()\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "prob_dev = mejor['modelo'].probabilidad(x_dev)\n", "umbral, recorrido = buscar_umbral(y_dev, prob_dev, costo_fn=5, costo_fp=1)\n", "plt.figure(figsize=(6, 3.5))\n", "plt.plot([f['umbral'] for f in recorrido], [f['costo'] for f in recorrido])\n", "plt.axvline(umbral['umbral'], color='crimson', linestyle='--', label=f\"umbral={umbral['umbral']:.3f}\")\n", "plt.xlabel('Umbral'); plt.ylabel('Costo esperado en desarrollo'); plt.legend(); plt.show()\n", "print(umbral)\n", "print(metricas(y_dev, prob_dev, umbral['umbral']))\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 4. Exportación\n", "\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.\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "guardar_modelo(RAIZ / 'entrega_solucion' / 'modelo_elegido.npz', mejor['modelo'],\n", " mejor['configuracion']['arquitectura'], media, desviacion)\n", "CONFIGURACION = {\n", " 'ruta_pesos': str(RAIZ / 'entrega_solucion' / 'modelo_elegido.npz'),\n", " 'arquitectura': mejor['configuracion']['arquitectura'],\n", " 'umbral': umbral['umbral'],\n", "}\n", "CONFIGURACION\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 5. Evaluación privada — instructor\n", "\n", "Ejecute este bloque sólo cuando las decisiones estén cerradas. Nunca se devuelve este CSV al estudiantado.\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# BLOQUE DEL INSTRUCTOR\n", "# x_test, y_test, _ = cargar_csv(RAIZ / 'instructor_privado' / 'test_200_balanceado.csv')\n", "# prob_test = mejor['modelo'].probabilidad(transformar(x_test, media, desviacion))\n", "# print(metricas(y_test, prob_test, CONFIGURACION['umbral']))\n" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "name": "python", "version": "3.x" } }, "nbformat": 4, "nbformat_minor": 5 }