{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceType":"competition","sourceId":20561}],"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# COVID-19 CT Image Segmentation\n\n## Integrantes\n\n- Alejandro Adrián García Martínez\n- Marco Iván Flores Villanueva\n- Oswaldo Isaías Hernández Santes\n- Roberto Serna Niño\n\n## 1. Introducción\n\nEste proyecto aborda el problema de segmentación semántica de imágenes médicas CT de pacientes con COVID-19. El objetivo de la competencia es identificar regiones pulmonares correspondientes principalmente a Ground Glass Opacities y Consolidations.\n\nEn un problema de segmentación se busca asignar una clase a cada píxel de la imagen, lo cual permite identificar la presencia de una lesión, su localización, y también su extensión dentro de una imagen CT.\n\nPara desarrollar la solución se tomó como punto de partida el notebook **[PyTorch Baseline for Semantic Segmentation](https://www.kaggle.com/code/maedemaftouni/pytorch-baseline-for-semantic-segmentation)**, de Maede Maftouni, disponible en la sección Code de la competencia. A partir de este baseline se agrega un análisis exploratorio más detallado del dataset y posteriormente se evaluarán modificaciones orientadas a atender el desbalance observado entre las clases.","metadata":{}},{"cell_type":"markdown","source":"## 2. Datos\n\nEl dataset se adjuntó directamente al notebook como input de la competición. Las celdas a continuación identifican la ubicación de los archivos de origen y cargan los arrays de NumPy sin modificar los datos originales.","metadata":{}},{"cell_type":"code","source":"# This Python 3 environment comes with many helpful analytics libraries installed\n# It is defined by the kaggle/python Docker image: https://github.com/kaggle/docker-python\n# For example, here's several helpful packages to load\n\nimport numpy as np # linear algebra\nimport pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)\n\n# Input data files are available in the read-only \"../input/\" directory\n# For example, running this (by clicking run or pressing Shift+Enter) will list all files under the input directory\n\nimport os\nfor dirname, _, filenames in os.walk('/kaggle/input/competitions/'):\n    for filename in filenames:\n        print(os.path.join(dirname, filename))\n\n# You can write up to 20GB to the current directory (/kaggle/working/) that gets preserved as output when you create a version using \"Save & Run All\" \n# You can also write temporary files to /kaggle/temp/, but they won't be saved outside of the current session\n\n# Use the kagglehub client library to attach Kaggle resources like competitions, datasets, and models to your session\n# Learn more about kagglehub: https://github.com/Kaggle/kagglehub/blob/main/README.md\n\nimport kagglehub\n# kagglehub.dataset_download('<owner>/<dataset-slug>')","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2026-09-15T02:12:24.251345Z","iopub.execute_input":"2026-09-15T02:12:24.251784Z","iopub.status.idle":"2026-09-15T02:12:24.271788Z","shell.execute_reply.started":"2026-09-15T02:12:24.251753Z","shell.execute_reply":"2026-09-15T02:12:24.271235Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import kagglehub\n\n# Download latest version\npath = kagglehub.competition_download('covid-segmentation')\n\nprint(\"Path to competition files:\", path)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-15T02:12:24.272844Z","iopub.execute_input":"2026-09-15T02:12:24.273179Z","iopub.status.idle":"2026-09-15T02:12:25.567024Z","shell.execute_reply.started":"2026-09-15T02:12:24.273157Z","shell.execute_reply":"2026-09-15T02:12:25.566325Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### 2.1 Extracción del dataset","metadata":{}},{"cell_type":"code","source":"from pathlib import Path\n\nDATA_DIR = Path(\"/kaggle/input/competitions/covid-segmentation\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-15T02:12:25.568350Z","iopub.execute_input":"2026-09-15T02:12:25.568656Z","iopub.status.idle":"2026-09-15T02:12:25.572134Z","shell.execute_reply.started":"2026-09-15T02:12:25.568630Z","shell.execute_reply":"2026-09-15T02:12:25.571498Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"El dataset se distribuye en archivos `.npy`, por lo que para acceder a ellos se usa NumPy. Se utiliza mapeo de memoria en modo read-only, lo que permite acceder a matrices de gran tamaño sin tener que copiar el conjunto de datos completo en la memoria.","metadata":{}},{"cell_type":"code","source":"images_medseg = np.load(\n    DATA_DIR / \"images_medseg.npy\",\n    mmap_mode=\"r\",\n    allow_pickle=False\n)\n\nmasks_medseg = np.load(\n    DATA_DIR / \"masks_medseg.npy\",\n    mmap_mode=\"r\",\n    allow_pickle=False\n)\n\ntest_images_medseg = np.load(\n    DATA_DIR / \"test_images_medseg.npy\",\n    mmap_mode=\"r\",\n    allow_pickle=False\n)\n\nimages_radiopedia = np.load(\n    DATA_DIR / \"images_radiopedia.npy\",\n    mmap_mode=\"r\",\n    allow_pickle=False\n)\n\nmasks_radiopedia = np.load(\n    DATA_DIR / \"masks_radiopedia.npy\",\n    mmap_mode=\"r\",\n    allow_pickle=False\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-15T02:12:25.573139Z","iopub.execute_input":"2026-09-15T02:12:25.573476Z","iopub.status.idle":"2026-09-15T02:12:25.640790Z","shell.execute_reply.started":"2026-09-15T02:12:25.573453Z","shell.execute_reply":"2026-09-15T02:12:25.640101Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### 2.2 Análisis Exploratorio y Perfilado de Datos\n\nAntes de aplicar cualquier limpieza o transformación, la estructura y calidad de los arreglos originales se tiene que revisar. Esto incluye sus dimensiones, tipos de datos, rangos numéricos, describes, y valores inválidos.\n\nSe revisan las dimensiones `shape` y los tipos de datos `dtype` de cada arreglo para conocer la cantidad de imágenes, su resolución, el número de canales y el formato en el que están almacenados los valores.","metadata":{}},{"cell_type":"code","source":"print(\"MedSeg images:\", images_medseg.shape, images_medseg.dtype)\nprint(\"MedSeg masks:\", masks_medseg.shape, masks_medseg.dtype)\nprint(\"MedSeg test:\", test_images_medseg.shape, test_images_medseg.dtype)\n\nprint(\"Radiopedia images:\", images_radiopedia.shape, images_radiopedia.dtype)\nprint(\"Radiopedia masks:\", masks_radiopedia.shape, masks_radiopedia.dtype)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-15T02:12:25.642187Z","iopub.execute_input":"2026-09-15T02:12:25.642467Z","iopub.status.idle":"2026-09-15T02:12:25.647582Z","shell.execute_reply.started":"2026-09-15T02:12:25.642426Z","shell.execute_reply":"2026-09-15T02:12:25.646884Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Se usa describe_array para encontrar los valores mínimos, máximos y promedio de cada arreglo. También se verifica la existencia de valores faltantes `NaN` o valores infinitos, ya que estos afectarían las etapas posteriores de procesamiento y entrenamiento.","metadata":{}},{"cell_type":"code","source":"def describe_array(name, array):\n    print(f\"--- {name} ---\")\n    print(\"Shape:\", array.shape)\n    print(\"Dimensions:\", array.ndim)\n    print(\"Dtype:\", array.dtype)\n    print(\"Minimum:\", np.min(array))\n    print(\"Maximum:\", np.max(array))\n    print(\"Mean:\", np.mean(array))\n    print(\"NaN values:\", np.isnan(array).sum())\n    print(\"Infinite values:\", np.isinf(array).sum())\n    print()\n\ndescribe_array(\"images_medseg\", images_medseg)\ndescribe_array(\"masks_medseg\", masks_medseg)\n\ndescribe_array(\"images_radiopedia\", images_radiopedia)\ndescribe_array(\"masks_radiopedia\", masks_radiopedia)\n\ndescribe_array(\"test_images_medseg\", test_images_medseg)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-15T02:12:25.648459Z","iopub.execute_input":"2026-09-15T02:12:25.648706Z","iopub.status.idle":"2026-09-15T02:12:29.092409Z","shell.execute_reply.started":"2026-09-15T02:12:25.648673Z","shell.execute_reply":"2026-09-15T02:12:29.091705Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Observaciones\n\n- El conjunto MedSeg contiene 100 imágenes CT de entrenamiento y sus respectivas máscaras\n- El conjunto Radiopedia contiene 829 imágenes CT y sus respectivas máscaras\n- El conjunto de prueba MedSeg contiene 10 imágenes CT\n- Todas las imágenes tienen una resolución de 512 x 512 píxeles y un canal de intensidad\n- Las máscaras contienen cuatro canales booleanos\n- No se detectaron valores NaN o indefinidos en los arreglos analizados, por lo cual no es necesario eliminar ni imputar observaciones","metadata":{}},{"cell_type":"markdown","source":"### 2.3 Validación de Integridad de los Datos\n\nAntes de tocar los datos, se verifica que las imágenes y sus máscaras correspondientes tengan una estructura consistente.\n\nSe comprueba que cada conjunto contenga la misma cantidad de imágenes y máscaras. De igual forma, se verifica que las dimensiones espaciales de cada imagen coincidan con las dimensiones de su máscara correspondiente.","metadata":{}},{"cell_type":"code","source":"print(\n    \"MedSeg image-mask count match:\",\n    images_medseg.shape[0] == masks_medseg.shape[0]\n)\n\nprint(\n    \"Radiopedia image-mask count match:\",\n    images_radiopedia.shape[0] == masks_radiopedia.shape[0]\n)\n\nprint(\n    \"MedSeg spatial dimensions match:\",\n    images_medseg.shape[1:3] == masks_medseg.shape[1:3]\n)\n\nprint(\n    \"Radiopedia spatial dimensions match:\",\n    images_radiopedia.shape[1:3] == masks_radiopedia.shape[1:3]\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-15T02:12:29.093313Z","iopub.execute_input":"2026-09-15T02:12:29.093547Z","iopub.status.idle":"2026-09-15T02:12:29.099265Z","shell.execute_reply.started":"2026-09-15T02:12:29.093525Z","shell.execute_reply":"2026-09-15T02:12:29.098278Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Aquí se confirma que los conjuntos MedSeg y Radiopedia contienen la misma cantidad de imágenes y máscaras correspondientes, así que no se detectó inconsistencia alguna en ese aspecto. Igualmente, las imágenes y sus máscaras presentan las mismas dimensiones espaciales de 512 x 512 pixeles, así que tampoco sería necesario eliminar observaciones debido a incompatibilidades de tamaño.","metadata":{}},{"cell_type":"markdown","source":"### 2.4 Análisis de máscaras de segmentación\n\nLas máscaras se analizan para identificar cómo están codificadas las clases y conocer la distribución de los píxeles entre los diferentes canales.\n\nSe revisan los valores únicos presentes en las máscaras para conocer la forma en la que están representadas las etiquetas.","metadata":{}},{"cell_type":"code","source":"print(\"Unique mask values - MedSeg:\")\nprint(np.unique(masks_medseg))\n\nprint(\"Unique mask values - Radiopedia:\")\nprint(np.unique(masks_radiopedia))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-15T02:12:29.100395Z","iopub.execute_input":"2026-09-15T02:12:29.100672Z","iopub.status.idle":"2026-09-15T02:12:53.087535Z","shell.execute_reply.started":"2026-09-15T02:12:29.100639Z","shell.execute_reply":"2026-09-15T02:12:53.086843Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Las máscaras utilizan valores booleanos:\n\n- `True`: el píxel pertenece a la clase\n- `False`: el píxel no pertenece a la clase","metadata":{}},{"cell_type":"markdown","source":"Se calcula la cantidad de píxeles pertenecientes a cada uno de los cuatro canales para analizar la distribución de las clases.","metadata":{}},{"cell_type":"code","source":"class_names = [\n    \"ground_glass\",\n    \"consolidations\",\n    \"lungs_other\",\n    \"background\"\n]\n\nfor i, class_name in enumerate(class_names):\n    pixels = np.sum(masks_medseg[..., i])\n    print(class_name, pixels)\n\nfor i, class_name in enumerate(class_names):\n    pixels = np.sum(masks_radiopedia[..., i])\n    print(class_name, pixels)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-15T02:12:53.088429Z","iopub.execute_input":"2026-09-15T02:12:53.088775Z","iopub.status.idle":"2026-09-15T02:12:53.988509Z","shell.execute_reply.started":"2026-09-15T02:12:53.088734Z","shell.execute_reply":"2026-09-15T02:12:53.987725Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Los resultados muestran una desproporción entre las clases. La clase `background` contiene una cantidad mayor de píxeles que las clases `ground_glass` y `consolidations`. Este desbalance se deberá considerar posteriormente durante la preparación de los datos, el entrenamiento del modelo y la selección de métricas de evaluación.","metadata":{}},{"cell_type":"markdown","source":"### 2.5 Inspección Visual de los Datos\n\nAdemás del análisis numérico, se realiza una inspección visual para verificar que las imágenes CT y sus máscaras puedan interpretarse correctamente.\n\nSe selecciona una imagen (en este caso index[0]) del conjunto MedSeg para observar su contenido y comprobar que los datos fueron cargados correctamente.","metadata":{}},{"cell_type":"code","source":"import matplotlib.pyplot as plt\n\nindex = 0\n\nplt.figure(figsize=(6, 6))\nplt.imshow(images_medseg[index], cmap=\"gray\")\nplt.title(\"CT image\")\nplt.axis(\"off\")\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-15T02:12:53.989389Z","iopub.execute_input":"2026-09-15T02:12:53.989670Z","iopub.status.idle":"2026-09-15T02:12:54.158324Z","shell.execute_reply.started":"2026-09-15T02:12:53.989646Z","shell.execute_reply":"2026-09-15T02:12:54.157610Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Se visualizan los canales correspondientes a `ground_glass` y `consolidations` para observar las regiones marcadas en la imagen.","metadata":{}},{"cell_type":"code","source":"plt.figure(figsize=(6, 6))\nplt.imshow(masks_medseg[index, :, :, 0], cmap=\"gray\")\nplt.title(\"Ground glass mask\")\nplt.axis(\"off\")\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-15T02:12:54.160759Z","iopub.execute_input":"2026-09-15T02:12:54.161341Z","iopub.status.idle":"2026-09-15T02:12:54.266512Z","shell.execute_reply.started":"2026-09-15T02:12:54.161316Z","shell.execute_reply":"2026-09-15T02:12:54.265742Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"plt.figure(figsize=(6, 6))\nplt.imshow(masks_medseg[index, :, :, 1], cmap=\"gray\")\nplt.title(\"Consolidation mask\")\nplt.axis(\"off\")\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-15T02:12:54.267640Z","iopub.execute_input":"2026-09-15T02:12:54.268040Z","iopub.status.idle":"2026-09-15T02:12:54.373480Z","shell.execute_reply.started":"2026-09-15T02:12:54.268014Z","shell.execute_reply":"2026-09-15T02:12:54.372823Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Se superpone la máscara de `ground_glass` sobre la imagen CT correspondiente para comprobar visualmente su alineación espacial.","metadata":{}},{"cell_type":"code","source":"plt.figure(figsize=(6, 6))\n\nplt.imshow(images_medseg[index], cmap=\"gray\")\nplt.imshow(\n    masks_medseg[index, :, :, 2],\n    alpha=0.35\n)\n\nplt.title(\"CT image + Ground Glass Mask\")\nplt.axis(\"off\")\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-15T02:12:54.374506Z","iopub.execute_input":"2026-09-15T02:12:54.374859Z","iopub.status.idle":"2026-09-15T02:12:54.611580Z","shell.execute_reply.started":"2026-09-15T02:12:54.374832Z","shell.execute_reply":"2026-09-15T02:12:54.611019Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"En el ejemplo analizado, la máscara se encuentra alineada con la imagen CT correspondiente.","metadata":{}},{"cell_type":"markdown","source":"### 2.6 Limpieza y Validación de Calidad\n\nEl análisis exploratorio no identificó valores faltantes, valores infinitos ni inconsistencias entre las dimensiones de las imágenes y sus máscaras correspondientes. Por esta razón, no fue necesario eliminar observaciones ni realizar imputación de valores.\n\nSe decidió conservar las observaciones originales para evitar modificaciones innecesarias al dataset. Las siguientes etapas se concentran en el preprocesamiento y transformación requeridos para preparar los datos para el modelo.","metadata":{}},{"cell_type":"markdown","source":"### 2.7 Traslape entre Clases\n\nLas máscaras originales contienen cuatro canales booleanos. Antes de convertir estos canales a una única etiqueta por píxel, se verifica si existen píxeles asignados simultáneamente a más de una clase.\n\nEsta comprobación es importante porque el baseline utiliza `argmax` para convertir los cuatro canales a una sola clase por píxel.","metadata":{}},{"cell_type":"code","source":"def analyze_mask_overlap(name, masks):\n    channel_sum = np.sum(masks, axis=-1)\n\n    overlapping_pixels = np.count_nonzero(channel_sum > 1)\n    unassigned_pixels = np.count_nonzero(channel_sum == 0)\n\n    gg_consolidation_overlap = np.count_nonzero(\n        masks[..., 0] & masks[..., 1]\n    )\n\n    print(f\"--- {name} ---\")\n    print(\"Píxeles con más de una clase:\", overlapping_pixels)\n    print(\"Píxeles sin clase asignada:\", unassigned_pixels)\n    print(\n        \"Traslape Ground Glass / Consolidation:\",\n        gg_consolidation_overlap\n    )\n    print()\n\n\nanalyze_mask_overlap(\"MedSeg\", masks_medseg)\nanalyze_mask_overlap(\"Radiopedia\", masks_radiopedia)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-15T02:12:54.612385Z","iopub.execute_input":"2026-09-15T02:12:54.612696Z","iopub.status.idle":"2026-09-15T02:13:00.866791Z","shell.execute_reply.started":"2026-09-15T02:12:54.612646Z","shell.execute_reply":"2026-09-15T02:13:00.866038Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### 2.8 Preprocesamiento de Intensidades\n\nSe reproduce la estrategia de preprocesamiento utilizada por el notebook baseline.\n\nLas intensidades de las imágenes CT se limitan al intervalo [-1500, 500]. Posteriormente se calculan la media y desviación estándar utilizando los valores comprendidos entre los percentiles 5 y 95 del conjunto Radiopedia.\n\nLa misma media y desviación estándar se utilizan para normalizar Radiopedia, MedSeg y el conjunto de prueba, manteniendo una transformación consistente entre los conjuntos.","metadata":{}},{"cell_type":"code","source":"def preprocess_images(images_arr, mean_std=None):\n    # Creamos una copia modificable y reducimos la precisión a float32\n    images = np.asarray(images_arr, dtype=np.float32).copy()\n\n    # Limitamos intensidades extremas\n    np.clip(images, -1500, 500, out=images)\n\n    # Si aún no tenemos media y desviación, las calculamos\n    if mean_std is None:\n        min_perc, max_perc = np.percentile(images, [5, 95])\n\n        valid_values = images[\n            (images > min_perc) &\n            (images < max_perc)\n        ]\n\n        mean = valid_values.mean()\n        std = valid_values.std()\n    else:\n        mean, std = mean_std\n\n    # Estandarización z-score\n    images = (images - mean) / std\n\n    print(f\"mean = {mean:.4f}, std = {std:.4f}\")\n\n    return images, (mean, std)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-15T02:13:00.867873Z","iopub.execute_input":"2026-09-15T02:13:00.868410Z","iopub.status.idle":"2026-09-15T02:13:00.873693Z","shell.execute_reply.started":"2026-09-15T02:13:00.868384Z","shell.execute_reply":"2026-09-15T02:13:00.873004Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"images_radiopedia_processed, mean_std = preprocess_images(\n    images_radiopedia\n)\n\nimages_medseg_processed, _ = preprocess_images(\n    images_medseg,\n    mean_std\n)\n\ntest_images_medseg_processed, _ = preprocess_images(\n    test_images_medseg,\n    mean_std\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-15T02:13:00.874675Z","iopub.execute_input":"2026-09-15T02:13:00.874954Z","iopub.status.idle":"2026-09-15T02:13:06.146686Z","shell.execute_reply.started":"2026-09-15T02:13:00.874887Z","shell.execute_reply":"2026-09-15T02:13:06.145976Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(\"Radiopedia:\")\nprint(\n    images_radiopedia_processed.shape,\n    images_radiopedia_processed.dtype,\n    images_radiopedia_processed.min(),\n    images_radiopedia_processed.max()\n)\n\nprint(\"\\nMedSeg:\")\nprint(\n    images_medseg_processed.shape,\n    images_medseg_processed.dtype,\n    images_medseg_processed.min(),\n    images_medseg_processed.max()\n)\n\nprint(\"\\nTest:\")\nprint(\n    test_images_medseg_processed.shape,\n    test_images_medseg_processed.dtype,\n    test_images_medseg_processed.min(),\n    test_images_medseg_processed.max()\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-15T02:13:06.147693Z","iopub.execute_input":"2026-09-15T02:13:06.148331Z","iopub.status.idle":"2026-09-15T02:13:06.324771Z","shell.execute_reply.started":"2026-09-15T02:13:06.148295Z","shell.execute_reply":"2026-09-15T02:13:06.324049Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"#### Resultados del Preprocesamiento\n\nSe identificó una cantidad reducida de píxeles asignados simultáneamente a más de un canal de segmentación. Sin embargo, no se encontró ningún traslape entre las dos clases objetivo, Ground Glass y Consolidation. Por esta razón, es posible seguir la formulación multiclase utilizada por el notebook baseline, representando cada píxel mediante una única etiqueta de clase.\n\nPara la normalización se calculó una media de aprox. -451.65 y una desviación estándar de 458.57 utilizando Radiopedia. Estos mismos parámetros se usaron para MedSeg y para el conjunto de prueba, evitando utilizar información específica del conjunto de prueba para definir su transformación.\n\nDespués del preprocesamiento, los tres conjuntos conservan sus dimensiones originales y utilizan el tipo de dato `float32`.","metadata":{}},{"cell_type":"markdown","source":"### 2.9 Conversión de las Máscaras a Etiquetas Multiclase\n\nEl modelo baseline plantea la segmentación como un problema de clasificación multiclase por píxel. Por lo tanto, las máscaras originales de cuatro canales se convierten a una matriz bidimensional en la que cada píxel contiene el índice de la clase correspondiente.\n\nLas clases se codifican de la siguiente forma:\n\n- `0`: Ground Glass\n- `1`: Consolidation\n- `2`: Lungs Other\n- `3`: Background\n\nLa conversión se realiza utilizando `argmax` sobre la dimensión de los canales.","metadata":{}},{"cell_type":"code","source":"def onehot_to_class_mask(masks):\n    return np.argmax(masks, axis=-1).astype(np.uint8)\n\n\nmasks_medseg_classes = onehot_to_class_mask(masks_medseg)\nmasks_radiopedia_classes = onehot_to_class_mask(masks_radiopedia)\n\nprint(\n    \"MedSeg:\",\n    masks_medseg_classes.shape,\n    masks_medseg_classes.dtype,\n    np.unique(masks_medseg_classes)\n)\n\nprint(\n    \"Radiopedia:\",\n    masks_radiopedia_classes.shape,\n    masks_radiopedia_classes.dtype,\n    np.unique(masks_radiopedia_classes)\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-15T02:13:06.325720Z","iopub.execute_input":"2026-09-15T02:13:06.326500Z","iopub.status.idle":"2026-09-15T02:13:14.441091Z","shell.execute_reply.started":"2026-09-15T02:13:06.326473Z","shell.execute_reply":"2026-09-15T02:13:14.440264Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"index = 0\n\nplt.figure(figsize=(6, 6))\nplt.imshow(masks_medseg_classes[index], cmap=\"viridis\")\nplt.title(\"Máscara multiclase\")\nplt.colorbar(ticks=[0, 1, 2, 3])\nplt.axis(\"off\")\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-15T02:13:14.442168Z","iopub.execute_input":"2026-09-15T02:13:14.442880Z","iopub.status.idle":"2026-09-15T02:13:14.573356Z","shell.execute_reply.started":"2026-09-15T02:13:14.442853Z","shell.execute_reply":"2026-09-15T02:13:14.572720Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### 2.10 Separación de Entrenamiento y Validación\n\nPara reproducir las condiciones del notebook baseline, se utiliza inicialmente la misma separación entre entrenamiento y validación.\n\nLas primeras 24 imágenes de MedSeg se utilizan como conjunto de validación. Las 76 imágenes restantes de MedSeg se combinan con las 829 imágenes de Radiopedia para formar el conjunto de entrenamiento.\n\nEsta separación se conserva durante los experimentos posteriores para permitir una comparación directa entre el modelo baseline y las modificaciones realizadas.","metadata":{}},{"cell_type":"code","source":"VAL_SIZE = 24\n\nval_indexes = np.arange(VAL_SIZE)\ntrain_indexes = np.arange(VAL_SIZE, len(images_medseg_processed))\n\ntrain_images = np.concatenate(\n    (\n        images_medseg_processed[train_indexes],\n        images_radiopedia_processed\n    ),\n    axis=0\n)\n\ntrain_masks = np.concatenate(\n    (\n        masks_medseg_classes[train_indexes],\n        masks_radiopedia_classes\n    ),\n    axis=0\n)\n\nval_images = images_medseg_processed[val_indexes]\nval_masks = masks_medseg_classes[val_indexes]\n\nprint(\"Train images:\", train_images.shape)\nprint(\"Train masks:\", train_masks.shape)\n\nprint(\"Validation images:\", val_images.shape)\nprint(\"Validation masks:\", val_masks.shape)\n\nprint(\"Test images:\", test_images_medseg_processed.shape)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-15T02:13:14.574353Z","iopub.execute_input":"2026-09-15T02:13:14.574633Z","iopub.status.idle":"2026-09-15T02:13:15.013122Z","shell.execute_reply.started":"2026-09-15T02:13:14.574610Z","shell.execute_reply":"2026-09-15T02:13:15.012313Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### 2.11 Aumento de Datos\n\nEl conjunto de entrenamiento contiene 905 imágenes, una cantidad relativamente pequeña para entrenar una red neuronal profunda. Para aumentar artificialmente la variedad de ejemplos disponibles se aplican transformaciones aleatorias durante el entrenamiento.\n\nSe utiliza la librería Albumentations, ya que permite aplicar exactamente la misma transformación geométrica a una imagen y a su máscara correspondiente.\n\nSiguiendo el enfoque del notebook baseline, se consideran rotaciones, recortes aleatorios y reflejos horizontales. Las imágenes de entrenamiento se reducen de 512 x 512 a 256 x 256 píxeles para disminuir el costo computacional del entrenamiento.\n\nLas transformaciones aleatorias se aplican únicamente al conjunto de entrenamiento. Para validación solamente se realiza el cambio de tamaño, con el objetivo de evaluar siempre sobre datos deterministas.","metadata":{}},{"cell_type":"code","source":"import albumentations as A\nimport cv2\n\nprint(\"Albumentations version:\", A.__version__)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-15T02:13:15.014156Z","iopub.execute_input":"2026-09-15T02:13:15.014478Z","iopub.status.idle":"2026-09-15T02:13:15.018967Z","shell.execute_reply.started":"2026-09-15T02:13:15.014443Z","shell.execute_reply":"2026-09-15T02:13:15.018133Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import random\nimport torch\n\nSEED = 42\n\ndef set_seed(seed):\n    random.seed(seed)\n    np.random.seed(seed)\n\n    torch.manual_seed(seed)\n\n    if torch.cuda.is_available():\n        torch.cuda.manual_seed(seed)\n        torch.cuda.manual_seed_all(seed)\n\n    torch.backends.cudnn.deterministic = True\n    torch.backends.cudnn.benchmark = False\n\n\nset_seed(SEED)\n\nprint(\"Seed:\", SEED)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-15T02:13:15.019959Z","iopub.execute_input":"2026-09-15T02:13:15.020340Z","iopub.status.idle":"2026-09-15T02:13:15.031501Z","shell.execute_reply.started":"2026-09-15T02:13:15.020291Z","shell.execute_reply":"2026-09-15T02:13:15.030860Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"SOURCE_SIZE = 512\nTARGET_SIZE = 256\n\ntrain_augs = A.Compose([\n    A.Rotate(\n        limit=360,\n        p=0.9,\n        border_mode=cv2.BORDER_REPLICATE\n    ),\n\n    A.RandomResizedCrop(\n        size=(TARGET_SIZE, TARGET_SIZE),\n        scale=(0.75, 1.0),\n        ratio=(1.0, 1.0),\n        interpolation=cv2.INTER_LINEAR,\n        mask_interpolation=cv2.INTER_NEAREST,\n        p=1.0\n    ),\n\n    A.HorizontalFlip(p=0.5)\n], seed=SEED)\n\nval_augs = A.Compose([\n    A.Resize(\n        TARGET_SIZE,\n        TARGET_SIZE,\n        interpolation=cv2.INTER_LINEAR,\n        mask_interpolation=cv2.INTER_NEAREST\n    )\n])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-15T02:13:15.032329Z","iopub.execute_input":"2026-09-15T02:13:15.032563Z","iopub.status.idle":"2026-09-15T02:13:15.043659Z","shell.execute_reply.started":"2026-09-15T02:13:15.032543Z","shell.execute_reply":"2026-09-15T02:13:15.042923Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"sample_index = 0\n\nsample = train_augs(\n    image=train_images[sample_index],\n    mask=train_masks[sample_index]\n)\n\naug_image = sample[\"image\"]\naug_mask = sample[\"mask\"]\n\nprint(\"Imagen original:\", train_images[sample_index].shape)\nprint(\"Máscara original:\", train_masks[sample_index].shape)\n\nprint(\"Imagen transformada:\", aug_image.shape)\nprint(\"Máscara transformada:\", aug_mask.shape)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-15T02:13:15.044561Z","iopub.execute_input":"2026-09-15T02:13:15.044991Z","iopub.status.idle":"2026-09-15T02:13:15.061782Z","shell.execute_reply.started":"2026-09-15T02:13:15.044958Z","shell.execute_reply":"2026-09-15T02:13:15.061193Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"plt.figure(figsize=(6, 6))\nplt.imshow(aug_image[:, :, 0], cmap=\"gray\")\nplt.title(\"Imagen CT con aumento de datos\")\nplt.axis(\"off\")\nplt.show()\n\nplt.figure(figsize=(6, 6))\nplt.imshow(aug_mask, cmap=\"viridis\")\nplt.title(\"Máscara transformada\")\nplt.colorbar(ticks=[0, 1, 2, 3])\nplt.axis(\"off\")\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-15T02:13:15.062637Z","iopub.execute_input":"2026-09-15T02:13:15.062884Z","iopub.status.idle":"2026-09-15T02:13:15.329448Z","shell.execute_reply.started":"2026-09-15T02:13:15.062856Z","shell.execute_reply":"2026-09-15T02:13:15.328784Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### 2.12 Preparación de los Datos para PyTorch\n\nPara el entrenamiento se utiliza PyTorch, un framework de aprendizaje profundo que permite definir redes neuronales, calcular gradientes automáticamente y ejecutar operaciones en GPU.\n\nLos arreglos NumPy se organizan mediante las clases `Dataset` y `DataLoader` de PyTorch.\n\n`Dataset` define cómo obtener una imagen y su máscara correspondiente. `DataLoader` agrupa posteriormente estas observaciones en minibatches que serán entregados al modelo durante el entrenamiento.","metadata":{}},{"cell_type":"code","source":"import torch\nfrom torch.utils.data import Dataset, DataLoader","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-15T02:13:15.330513Z","iopub.execute_input":"2026-09-15T02:13:15.331150Z","iopub.status.idle":"2026-09-15T02:13:15.334812Z","shell.execute_reply.started":"2026-09-15T02:13:15.331123Z","shell.execute_reply":"2026-09-15T02:13:15.333967Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"device = torch.device(\n    \"cuda\" if torch.cuda.is_available() else \"cpu\"\n)\n\nprint(\"Dispositivo:\", device)\n\nif torch.cuda.is_available():\n    print(\"GPU:\", torch.cuda.get_device_name(0))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-15T02:13:15.336047Z","iopub.execute_input":"2026-09-15T02:13:15.336408Z","iopub.status.idle":"2026-09-15T02:13:15.349082Z","shell.execute_reply.started":"2026-09-15T02:13:15.336384Z","shell.execute_reply":"2026-09-15T02:13:15.348418Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### 2.13 Dataset de PyTorch\n\nSe define una clase personalizada que permite a PyTorch recuperar una imagen y su máscara utilizando un índice.\n\nCuando se solicita una observación:\n\n1. Se recuperan la imagen y máscara correspondientes.\n2. Se aplican las transformaciones de Albumentations.\n3. La imagen se reorganiza del formato `(alto, ancho, canales)` al formato `(canales, alto, ancho)` utilizado por PyTorch.\n4. Tanto la imagen como la máscara se convierten en tensores.","metadata":{}},{"cell_type":"code","source":"class CovidSegmentationDataset(Dataset):\n    def __init__(\n        self,\n        images,\n        masks,\n        augmentations=None,\n        imagenet_normalization=True\n    ):\n        self.images = images\n        self.masks = masks\n        self.augmentations = augmentations\n        self.imagenet_normalization = imagenet_normalization\n\n        self.mean = 0.485\n        self.std = 0.229\n\n    def __len__(self):\n        return len(self.images)\n\n    def __getitem__(self, index):\n        image = self.images[index]\n        mask = self.masks[index]\n\n        if self.augmentations is not None:\n            sample = self.augmentations(\n                image=image,\n                mask=mask\n            )\n\n            image = sample[\"image\"]\n            mask = sample[\"mask\"]\n\n        # HWC -> CHW\n        image = np.transpose(image, (2, 0, 1))\n\n        image = torch.from_numpy(\n            np.ascontiguousarray(image)\n        ).float()\n\n        mask = torch.from_numpy(\n            np.ascontiguousarray(mask)\n        ).long()\n\n        # Normalización adicional utilizada por el baseline\n        # Esta es neccesaria para la comparación final de desempeños\n        if self.imagenet_normalization:\n            image = (image - self.mean) / self.std\n\n        return image, mask","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-15T02:13:15.349993Z","iopub.execute_input":"2026-09-15T02:13:15.350274Z","iopub.status.idle":"2026-09-15T02:13:15.359972Z","shell.execute_reply.started":"2026-09-15T02:13:15.350244Z","shell.execute_reply":"2026-09-15T02:13:15.359352Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_dataset = CovidSegmentationDataset(\n    train_images,\n    train_masks,\n    augmentations=train_augs\n)\n\nval_dataset = CovidSegmentationDataset(\n    val_images,\n    val_masks,\n    augmentations=val_augs\n)\n\nprint(\"Train dataset:\", len(train_dataset))\nprint(\"Validation dataset:\", len(val_dataset))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-15T02:13:15.365950Z","iopub.execute_input":"2026-09-15T02:13:15.366169Z","iopub.status.idle":"2026-09-15T02:13:15.374710Z","shell.execute_reply.started":"2026-09-15T02:13:15.366149Z","shell.execute_reply":"2026-09-15T02:13:15.373869Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"image_tensor, mask_tensor = train_dataset[0]\n\nprint(\"Imagen:\", image_tensor.shape, image_tensor.dtype)\nprint(\"Máscara:\", mask_tensor.shape, mask_tensor.dtype)\n\nprint(\"Clases de la máscara:\", torch.unique(mask_tensor))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-15T02:13:15.375788Z","iopub.execute_input":"2026-09-15T02:13:15.376182Z","iopub.status.idle":"2026-09-15T02:13:15.395075Z","shell.execute_reply.started":"2026-09-15T02:13:15.376146Z","shell.execute_reply":"2026-09-15T02:13:15.394243Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### 2.14 Minibatches con DataLoader\n\nDurante el entrenamiento no se procesan las 905 imágenes simultáneamente. El `DataLoader` divide el dataset en grupos más pequeños llamados minibatches.\n\nEl uso de minibatches permite reducir el consumo de memoria y actualizar los parámetros del modelo varias veces durante cada epoch.\n\nEl conjunto de entrenamiento se mezcla aleatoriamente en cada epoch (`shuffle=True`), mientras que el conjunto de validación conserva un orden fijo.","metadata":{}},{"cell_type":"code","source":"BATCH_SIZE = 24\n\ntrain_generator = torch.Generator()\ntrain_generator.manual_seed(SEED)\n\ntrain_dataloader = DataLoader(\n    train_dataset,\n    batch_size=BATCH_SIZE,\n    shuffle=True,\n    generator=train_generator\n)\n\nval_dataloader = DataLoader(\n    val_dataset,\n    batch_size=BATCH_SIZE,\n    shuffle=False\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-15T02:13:15.396257Z","iopub.execute_input":"2026-09-15T02:13:15.396563Z","iopub.status.idle":"2026-09-15T02:13:15.406999Z","shell.execute_reply.started":"2026-09-15T02:13:15.396540Z","shell.execute_reply":"2026-09-15T02:13:15.406071Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"images_batch, masks_batch = next(\n    iter(train_dataloader)\n)\n\nprint(\"Batch de imágenes:\", images_batch.shape)\nprint(\"Batch de máscaras:\", masks_batch.shape)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-15T02:13:15.408204Z","iopub.execute_input":"2026-09-15T02:13:15.408560Z","iopub.status.idle":"2026-09-15T02:13:15.494765Z","shell.execute_reply.started":"2026-09-15T02:13:15.408535Z","shell.execute_reply":"2026-09-15T02:13:15.494022Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 3. Modelo\n\n### 3.1 Arquitectura U-Net\n\nPara la tarea de segmentación se utiliza una arquitectura U-Net con EfficientNet-B2 como encoder, siguiendo el enfoque del notebook baseline utilizado como punto de partida.\n\nU-Net es una arquitectura diseñada para segmentación de imágenes. Está formada principalmente por dos partes:\n\n- Encoder: reduce progresivamente las dimensiones espaciales de la imagen y extrae características cada vez más complejas.\n- Decoder: recupera progresivamente la resolución espacial para producir una clasificación para cada píxel.\n\nU-Net utiliza además conexiones entre encoder y decoder, conocidas como *skip connections*. Estas conexiones permiten recuperar información espacial de alta resolución que podría perderse durante la reducción de dimensiones.\n\nEn esta implementación, EfficientNet-B2 se utiliza como encoder y la salida del modelo contiene cuatro canales, uno para cada clase de segmentación: Ground Glass, Consolidation, Lungs Other y Background.","metadata":{}},{"cell_type":"code","source":"!pip install -q segmentation-models-pytorch","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-15T02:13:15.495666Z","iopub.execute_input":"2026-09-15T02:13:15.496130Z","iopub.status.idle":"2026-09-15T02:13:18.931379Z","shell.execute_reply.started":"2026-09-15T02:13:15.496096Z","shell.execute_reply":"2026-09-15T02:13:18.930575Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import segmentation_models_pytorch as smp\n\nprint(\n    \"segmentation_models_pytorch:\",\n    smp.__version__\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-15T02:13:18.932713Z","iopub.execute_input":"2026-09-15T02:13:18.933168Z","iopub.status.idle":"2026-09-15T02:13:18.937939Z","shell.execute_reply.started":"2026-09-15T02:13:18.933131Z","shell.execute_reply":"2026-09-15T02:13:18.937191Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### 3.2 Configuración del Modelo\n\nEl modelo utiliza EfficientNet-B2 como encoder de una U-Net.\n\nLa configuración utilizada inicialmente reproduce el baseline:\n\n- 1 canal de entrada, debido a que las imágenes CT son monocromáticas.\n- 4 clases de salida.\n- Encoder EfficientNet-B2.\n- Pesos preentrenados sobre ImageNet.\n- Profundidad de encoder de 5 niveles.\n- Canales del decoder: 256, 128, 64, 32 y 16.\n\nEsta configuración se utiliza como modelo baseline antes de introducir modificaciones, permitiendo comparar posteriormente el efecto de nuestras mejoras bajo condiciones consistentes.","metadata":{}},{"cell_type":"code","source":"set_seed(SEED)\n\nmodel = smp.Unet(\n    encoder_name=\"efficientnet-b2\",\n    encoder_weights=\"imagenet\",\n    in_channels=1,\n    classes=4,\n    activation=None,\n    encoder_depth=5,\n    decoder_channels=(256, 128, 64, 32, 16)\n)\n\nmodel = model.to(device)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-15T02:13:18.938933Z","iopub.execute_input":"2026-09-15T02:13:18.939256Z","iopub.status.idle":"2026-09-15T02:13:19.150240Z","shell.execute_reply.started":"2026-09-15T02:13:18.939212Z","shell.execute_reply":"2026-09-15T02:13:19.149619Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"total_params = sum(\n    p.numel()\n    for p in model.parameters()\n)\n\ntrainable_params = sum(\n    p.numel()\n    for p in model.parameters()\n    if p.requires_grad\n)\n\nprint(f\"Parámetros totales: {total_params:,}\")\nprint(f\"Parámetros entrenables: {trainable_params:,}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-15T02:13:19.151150Z","iopub.execute_input":"2026-09-15T02:13:19.151459Z","iopub.status.idle":"2026-09-15T02:13:19.158218Z","shell.execute_reply.started":"2026-09-15T02:13:19.151424Z","shell.execute_reply":"2026-09-15T02:13:19.157548Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"test_batch = images_batch[:2].to(device)\n\nmodel.eval()\n\nwith torch.no_grad():\n    test_output = model(test_batch)\n\nprint(\"Entrada:\", test_batch.shape)\nprint(\"Salida:\", test_output.shape)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-15T02:13:19.159428Z","iopub.execute_input":"2026-09-15T02:13:19.159730Z","iopub.status.idle":"2026-09-15T02:13:19.242174Z","shell.execute_reply.started":"2026-09-15T02:13:19.159689Z","shell.execute_reply":"2026-09-15T02:13:19.241566Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"test_masks = masks_batch[:2].to(device)\n\nprint(\"Predicción:\", test_output.shape)\nprint(\"Ground truth:\", test_masks.shape)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-15T02:13:19.242929Z","iopub.execute_input":"2026-09-15T02:13:19.243242Z","iopub.status.idle":"2026-09-15T02:13:19.248457Z","shell.execute_reply.started":"2026-09-15T02:13:19.243187Z","shell.execute_reply":"2026-09-15T02:13:19.247693Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 4. Entrenamiento\n\nEl modelo baseline se entrena utilizando Cross-Entropy como función de pérdida y AdamW como optimizador. Se utiliza además un scheduler OneCycleLR para modificar dinámicamente la tasa de aprendizaje durante el entrenamiento.\n\nPara reproducir el baseline se utilizan inicialmente 10 epochs, una tasa máxima de aprendizaje de 0.001, un `weight_decay` de 0.0001 y minibatches de 24 imágenes.\n\nDurante cada epoch se calculan métricas tanto para entrenamiento como para validación. Además de la exactitud por píxel y mIoU utilizadas por el baseline, se calculan IoU y Dice específicamente para Ground Glass y Consolidation, ya que son las dos clases objetivo de la competencia.\n\n### Métricas de Evaluación\n\nPara evaluar los modelos se utilizan principalmente Intersection over Union (IoU) y el coeficiente Dice.\n\nIoU mide la proporción entre la intersección de la región predicha y el ground truth y la unión de ambas regiones. Dice también mide el solapamiento entre la predicción y la anotación real, dando un peso importante a la región compartida.\n\nEstas métricas son pertinentes para segmentación porque evalúan qué tan bien coincide la región predicha con la región real.\n\nDe igual forma, se calcula la exactitud por píxel, aunque esta se utiliza como una métrica secundaria debido al fuerte desbalance identificado durante el análisis exploratorio. Como una gran parte de los píxeles pertenece a Background y Lungs Other, es posible obtener una accuracy elevada incluso cuando Ground Glass o Consolidation presentan un desempeño deficiente.\n\nEs por esto que IoU y Dice sobre Ground Glass y Consolidation se consideran las métricas principales para comparar los modelos.","metadata":{}},{"cell_type":"code","source":"import torch.nn as nn\nimport torch.nn.functional as F\nimport time\nfrom tqdm.auto import tqdm","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-15T02:13:19.250231Z","iopub.execute_input":"2026-09-15T02:13:19.250481Z","iopub.status.idle":"2026-09-15T02:13:19.259323Z","shell.execute_reply.started":"2026-09-15T02:13:19.250460Z","shell.execute_reply":"2026-09-15T02:13:19.258709Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def pixel_accuracy(logits, targets):\n    predictions = torch.argmax(logits, dim=1)\n\n    correct = (predictions == targets).float().mean()\n\n    return correct.item()\n\n\ndef mean_iou(logits, targets, classes=(0, 1, 2, 3), smooth=1e-10):\n    predictions = torch.argmax(logits, dim=1)\n\n    scores = []\n\n    for class_id in classes:\n        predicted_class = predictions == class_id\n        true_class = targets == class_id\n\n        # Igual que el baseline: ignoramos una clase\n        # si no aparece en el ground truth del batch\n        if true_class.sum().item() == 0:\n            continue\n\n        intersection = torch.logical_and(\n            predicted_class,\n            true_class\n        ).sum().float()\n\n        union = torch.logical_or(\n            predicted_class,\n            true_class\n        ).sum().float()\n\n        score = (intersection + smooth) / (union + smooth)\n        scores.append(score.item())\n\n    if len(scores) == 0:\n        return np.nan\n\n    return float(np.mean(scores))\n\n\ndef mean_dice(logits, targets, classes=(0, 1), smooth=1e-10):\n    predictions = torch.argmax(logits, dim=1)\n\n    scores = []\n\n    for class_id in classes:\n        predicted_class = predictions == class_id\n        true_class = targets == class_id\n\n        if true_class.sum().item() == 0:\n            continue\n\n        intersection = torch.logical_and(\n            predicted_class,\n            true_class\n        ).sum().float()\n\n        denominator = (\n            predicted_class.sum().float()\n            + true_class.sum().float()\n        )\n\n        score = (\n            (2 * intersection + smooth)\n            / (denominator + smooth)\n        )\n\n        scores.append(score.item())\n\n    if len(scores) == 0:\n        return np.nan\n\n    return float(np.mean(scores))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-15T02:13:19.260171Z","iopub.execute_input":"2026-09-15T02:13:19.260473Z","iopub.status.idle":"2026-09-15T02:13:19.271315Z","shell.execute_reply.started":"2026-09-15T02:13:19.260422Z","shell.execute_reply":"2026-09-15T02:13:19.270657Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(\n    \"Pixel Accuracy:\",\n    pixel_accuracy(test_output, test_masks)\n)\n\nprint(\n    \"mIoU 4 clases:\",\n    mean_iou(test_output, test_masks)\n)\n\nprint(\n    \"mIoU clases objetivo:\",\n    mean_iou(\n        test_output,\n        test_masks,\n        classes=(0, 1)\n    )\n)\n\nprint(\n    \"Dice clases objetivo:\",\n    mean_dice(\n        test_output,\n        test_masks,\n        classes=(0, 1)\n    )\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-15T02:13:19.272190Z","iopub.execute_input":"2026-09-15T02:13:19.272487Z","iopub.status.idle":"2026-09-15T02:13:19.289591Z","shell.execute_reply.started":"2026-09-15T02:13:19.272454Z","shell.execute_reply":"2026-09-15T02:13:19.288843Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### 4.1 Configuración del Entrenamiento Baseline\n\nEl entrenamiento inicial reproduce los hiperparámetros principales del notebook de referencia. Este experimento se conservará como baseline para poder comparar posteriormente cualquier modificación realizada al modelo o a la estrategia de entrenamiento.","metadata":{}},{"cell_type":"markdown","source":"### Sobre Hardware\n\nEl entrenamiento se realizó utilizando una GPU NVIDIA Tesla T4 proporcionada por Kaggle. Para reducir el costo computacional, las imágenes se procesaron como `float32` y se redimensionaron de 512 x 512 a 256 x 256 píxeles durante entrenamiento y validación; el entrenamiento utiliza minibatches de 24 imágenes.\n\n### Sobre Optimización y Regularización\n\nAdamW se utiliza como optimizador con una tasa máxima de aprendizaje de 0.001 y `weight_decay=0.0001`. El `weight_decay` funciona como una forma de regularización al penalizar parámetros excesivamente grandes.\n\nAdemás, se utiliza OneCycleLR para modificar dinámicamente la tasa de aprendizaje durante el entrenamiento.\n\nNo se utilizó early stopping adicional, ya que ambos experimentos se limitaron a 10 epochs para mantener condiciones comparables.","metadata":{}},{"cell_type":"code","source":"MAX_LR = 1e-3\nEPOCHS = 10\nWEIGHT_DECAY = 1e-4\n\ncriterion = nn.CrossEntropyLoss()\n\noptimizer = torch.optim.AdamW(\n    model.parameters(),\n    lr=MAX_LR,\n    weight_decay=WEIGHT_DECAY\n)\n\nscheduler = torch.optim.lr_scheduler.OneCycleLR(\n    optimizer,\n    max_lr=MAX_LR,\n    epochs=EPOCHS,\n    steps_per_epoch=len(train_dataloader)\n)\n\nprint(\"Epochs:\", EPOCHS)\nprint(\"Batch size:\", BATCH_SIZE)\nprint(\"Batches de entrenamiento:\", len(train_dataloader))\nprint(\"Batches de validación:\", len(val_dataloader))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-15T02:13:19.290528Z","iopub.execute_input":"2026-09-15T02:13:19.291013Z","iopub.status.idle":"2026-09-15T02:13:19.305146Z","shell.execute_reply.started":"2026-09-15T02:13:19.290990Z","shell.execute_reply":"2026-09-15T02:13:19.304459Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def train_model(\n    model,\n    train_loader,\n    val_loader,\n    criterion,\n    optimizer,\n    scheduler,\n    epochs,\n    device,\n    checkpoint_path\n):\n    history = {\n        \"train_loss\": [],\n        \"val_loss\": [],\n        \"train_miou\": [],\n        \"val_miou\": [],\n        \"train_target_miou\": [],\n        \"val_target_miou\": [],\n        \"train_target_dice\": [],\n        \"val_target_dice\": [],\n        \"train_accuracy\": [],\n        \"val_accuracy\": [],\n        \"lr\": []\n    }\n\n    best_val_loss = float(\"inf\")\n\n    for epoch in range(epochs):\n        start_time = time.time()\n\n        # =====================\n        # TRAIN\n        # =====================\n        model.train()\n\n        train_loss = 0.0\n        train_miou = 0.0\n        train_target_miou = 0.0\n        train_target_dice = 0.0\n        train_accuracy = 0.0\n\n        for images, masks in tqdm(\n            train_loader,\n            desc=f\"Epoch {epoch + 1}/{epochs} - Train\"\n        ):\n            images = images.to(device)\n            masks = masks.to(device)\n\n            optimizer.zero_grad(set_to_none=True)\n\n            outputs = model(images)\n\n            loss = criterion(outputs, masks)\n\n            loss.backward()\n\n            optimizer.step()\n            scheduler.step()\n\n            train_loss += loss.item()\n\n            train_miou += mean_iou(\n                outputs,\n                masks\n            )\n\n            train_target_miou += mean_iou(\n                outputs,\n                masks,\n                classes=(0, 1)\n            )\n\n            train_target_dice += mean_dice(\n                outputs,\n                masks,\n                classes=(0, 1)\n            )\n\n            train_accuracy += pixel_accuracy(\n                outputs,\n                masks\n            )\n\n            history[\"lr\"].append(\n                optimizer.param_groups[0][\"lr\"]\n            )\n\n        n_train_batches = len(train_loader)\n\n        train_loss /= n_train_batches\n        train_miou /= n_train_batches\n        train_target_miou /= n_train_batches\n        train_target_dice /= n_train_batches\n        train_accuracy /= n_train_batches\n\n        # =====================\n        # VALIDATION\n        # =====================\n        model.eval()\n\n        val_loss = 0.0\n        val_miou = 0.0\n        val_target_miou = 0.0\n        val_target_dice = 0.0\n        val_accuracy = 0.0\n\n        with torch.no_grad():\n            for images, masks in tqdm(\n                val_loader,\n                desc=f\"Epoch {epoch + 1}/{epochs} - Val\"\n            ):\n                images = images.to(device)\n                masks = masks.to(device)\n\n                outputs = model(images)\n\n                loss = criterion(outputs, masks)\n\n                val_loss += loss.item()\n\n                val_miou += mean_iou(\n                    outputs,\n                    masks\n                )\n\n                val_target_miou += mean_iou(\n                    outputs,\n                    masks,\n                    classes=(0, 1)\n                )\n\n                val_target_dice += mean_dice(\n                    outputs,\n                    masks,\n                    classes=(0, 1)\n                )\n\n                val_accuracy += pixel_accuracy(\n                    outputs,\n                    masks\n                )\n\n        n_val_batches = len(val_loader)\n\n        val_loss /= n_val_batches\n        val_miou /= n_val_batches\n        val_target_miou /= n_val_batches\n        val_target_dice /= n_val_batches\n        val_accuracy /= n_val_batches\n\n        history[\"train_loss\"].append(train_loss)\n        history[\"val_loss\"].append(val_loss)\n\n        history[\"train_miou\"].append(train_miou)\n        history[\"val_miou\"].append(val_miou)\n\n        history[\"train_target_miou\"].append(\n            train_target_miou\n        )\n        history[\"val_target_miou\"].append(\n            val_target_miou\n        )\n\n        history[\"train_target_dice\"].append(\n            train_target_dice\n        )\n        history[\"val_target_dice\"].append(\n            val_target_dice\n        )\n\n        history[\"train_accuracy\"].append(\n            train_accuracy\n        )\n        history[\"val_accuracy\"].append(\n            val_accuracy\n        )\n\n        # Guardar mejor modelo según validation loss\n        if val_loss < best_val_loss:\n            best_val_loss = val_loss\n\n            torch.save(\n                model.state_dict(),\n                checkpoint_path\n            )\n\n            best_marker = \"  <-- mejor modelo\"\n        else:\n            best_marker = \"\"\n\n        elapsed = (time.time() - start_time) / 60\n\n        print(\n            f\"\\nEpoch {epoch + 1}/{epochs}\"\n            f\" | Train Loss: {train_loss:.4f}\"\n            f\" | Val Loss: {val_loss:.4f}\"\n            f\" | Train mIoU: {train_miou:.4f}\"\n            f\" | Val mIoU: {val_miou:.4f}\"\n            f\" | Val Target IoU: {val_target_miou:.4f}\"\n            f\" | Val Target Dice: {val_target_dice:.4f}\"\n            f\" | Val Acc: {val_accuracy:.4f}\"\n            f\" | {elapsed:.2f} min\"\n            f\"{best_marker}\"\n        )\n\n    return history","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-15T02:13:19.306110Z","iopub.execute_input":"2026-09-15T02:13:19.306854Z","iopub.status.idle":"2026-09-15T02:13:19.318586Z","shell.execute_reply.started":"2026-09-15T02:13:19.306831Z","shell.execute_reply":"2026-09-15T02:13:19.317841Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"history_baseline = train_model(\n    model=model,\n    train_loader=train_dataloader,\n    val_loader=val_dataloader,\n    criterion=criterion,\n    optimizer=optimizer,\n    scheduler=scheduler,\n    epochs=EPOCHS,\n    device=device,\n    checkpoint_path=\"/kaggle/working/best_baseline_model.pth\"\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-15T02:13:19.319516Z","iopub.execute_input":"2026-09-15T02:13:19.319817Z","iopub.status.idle":"2026-09-15T02:16:18.613571Z","shell.execute_reply.started":"2026-09-15T02:13:19.319785Z","shell.execute_reply":"2026-09-15T02:16:18.612903Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### 4.2 Resultados del Entrenamiento Baseline\n\nDurante las 10 epochs el modelo baseline mostró una reducción general de la pérdida y una mejora de las métricas de validación.\n\nAunque la exactitud por píxel alcanzó valores elevados, esta métrica debe interpretarse con precaución debido al fuerte desbalance de clases identificado durante el análisis exploratorio.\n\nLas métricas calculadas específicamente sobre Ground Glass y Consolidation muestran un desempeño considerablemente menor que la exactitud global, lo que sugiere que el modelo favorece las clases más frecuentes.\n\nEste comportamiento motiva la evaluación posterior de una función de pérdida que considere explícitamente el solapamiento de las dos clases objetivo.","metadata":{}},{"cell_type":"code","source":"model.load_state_dict(\n    torch.load(\n        \"/kaggle/working/best_baseline_model.pth\",\n        map_location=device,\n        weights_only=True\n    )\n)\n\nmodel = model.to(device)\nmodel.eval()\n\nprint(\"Mejor checkpoint del baseline cargado.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-15T02:16:18.614828Z","iopub.execute_input":"2026-09-15T02:16:18.615164Z","iopub.status.idle":"2026-09-15T02:16:18.778337Z","shell.execute_reply.started":"2026-09-15T02:16:18.615131Z","shell.execute_reply":"2026-09-15T02:16:18.777659Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 5. Resultados del Baseline\n\n### 5.1 Curvas de Entrenamiento y Validación\n\nLas curvas de pérdida y las métricas de segmentación permiten analizar la evolución del aprendizaje y comparar el comportamiento del modelo entre los conjuntos de entrenamiento y validación.","metadata":{}},{"cell_type":"code","source":"plt.figure(figsize=(8, 5))\n\nplt.plot(\n    history_baseline[\"train_loss\"],\n    marker=\"o\",\n    label=\"Train\"\n)\n\nplt.plot(\n    history_baseline[\"val_loss\"],\n    marker=\"o\",\n    label=\"Validation\"\n)\n\nplt.xlabel(\"epoch\")\nplt.ylabel(\"Loss\")\nplt.title(\"Baseline - Training vs Validation Loss\")\nplt.legend()\nplt.grid()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-15T02:16:18.779194Z","iopub.execute_input":"2026-09-15T02:16:18.779482Z","iopub.status.idle":"2026-09-15T02:16:18.931587Z","shell.execute_reply.started":"2026-09-15T02:16:18.779458Z","shell.execute_reply":"2026-09-15T02:16:18.931042Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"plt.figure(figsize=(8, 5))\n\nplt.plot(\n    history_baseline[\"train_miou\"],\n    marker=\"o\",\n    label=\"Train mIoU\"\n)\n\nplt.plot(\n    history_baseline[\"val_miou\"],\n    marker=\"o\",\n    label=\"Validation mIoU\"\n)\n\nplt.xlabel(\"epoch\")\nplt.ylabel(\"mIoU\")\nplt.title(\"Baseline - Training vs Validation mIoU\")\nplt.legend()\nplt.grid()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-15T02:16:18.932760Z","iopub.execute_input":"2026-09-15T02:16:18.933429Z","iopub.status.idle":"2026-09-15T02:16:19.082731Z","shell.execute_reply.started":"2026-09-15T02:16:18.933405Z","shell.execute_reply":"2026-09-15T02:16:19.081964Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"plt.figure(figsize=(8, 5))\n\nplt.plot(\n    history_baseline[\"val_target_miou\"],\n    marker=\"o\",\n    label=\"Validation IoU\"\n)\n\nplt.plot(\n    history_baseline[\"val_target_dice\"],\n    marker=\"o\",\n    label=\"Validation Dice\"\n)\n\nplt.xlabel(\"epoch\")\nplt.ylabel(\"Score\")\nplt.title(\"Baseline - Ground Glass y Consolidation\")\nplt.legend()\nplt.grid()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-15T02:16:19.083741Z","iopub.execute_input":"2026-09-15T02:16:19.084069Z","iopub.status.idle":"2026-09-15T02:16:19.223452Z","shell.execute_reply.started":"2026-09-15T02:16:19.084036Z","shell.execute_reply":"2026-09-15T02:16:19.222762Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"baseline_results = pd.DataFrame({\n    \"Epoch\": np.arange(1, EPOCHS + 1),\n\n    \"Train Loss\":\n        history_baseline[\"train_loss\"],\n\n    \"Validation Loss\":\n        history_baseline[\"val_loss\"],\n\n    \"Train mIoU\":\n        history_baseline[\"train_miou\"],\n\n    \"Validation mIoU\":\n        history_baseline[\"val_miou\"],\n\n    \"Validation Target IoU\":\n        history_baseline[\"val_target_miou\"],\n\n    \"Validation Target Dice\":\n        history_baseline[\"val_target_dice\"],\n\n    \"Validation Accuracy\":\n        history_baseline[\"val_accuracy\"]\n})\n\nbaseline_results.round(4)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-15T02:16:19.224493Z","iopub.execute_input":"2026-09-15T02:16:19.224748Z","iopub.status.idle":"2026-09-15T02:16:19.239562Z","shell.execute_reply.started":"2026-09-15T02:16:19.224725Z","shell.execute_reply":"2026-09-15T02:16:19.238960Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"best_target_epoch = baseline_results.loc[\n    baseline_results[\"Validation Target Dice\"].idxmax()\n]\n\nbest_target_epoch","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-15T02:16:19.240644Z","iopub.execute_input":"2026-09-15T02:16:19.240963Z","iopub.status.idle":"2026-09-15T02:16:19.255131Z","shell.execute_reply.started":"2026-09-15T02:16:19.240929Z","shell.execute_reply":"2026-09-15T02:16:19.254357Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### 5.2 Evaluación por Clase\n\nDebido al desbalance observado en el dataset, se calculan métricas independientes para cada una de las cuatro clases. Esto permite determinar si una métrica global elevada es consecuencia del buen desempeño sobre las clases más frecuentes.","metadata":{}},{"cell_type":"code","source":"def evaluate_per_class(\n    model,\n    dataloader,\n    device,\n    num_classes=4\n):\n    intersections = np.zeros(num_classes)\n    unions = np.zeros(num_classes)\n\n    prediction_sizes = np.zeros(num_classes)\n    target_sizes = np.zeros(num_classes)\n\n    model.eval()\n\n    with torch.no_grad():\n\n        for images, masks in dataloader:\n\n            images = images.to(device)\n            masks = masks.to(device)\n\n            outputs = model(images)\n\n            predictions = torch.argmax(\n                outputs,\n                dim=1\n            )\n\n            for class_id in range(num_classes):\n\n                pred_class = (\n                    predictions == class_id\n                )\n\n                true_class = (\n                    masks == class_id\n                )\n\n                intersection = torch.logical_and(\n                    pred_class,\n                    true_class\n                ).sum().item()\n\n                union = torch.logical_or(\n                    pred_class,\n                    true_class\n                ).sum().item()\n\n                intersections[class_id] += intersection\n                unions[class_id] += union\n\n                prediction_sizes[class_id] += (\n                    pred_class.sum().item()\n                )\n\n                target_sizes[class_id] += (\n                    true_class.sum().item()\n                )\n\n    iou_scores = (\n        intersections\n        / np.maximum(unions, 1)\n    )\n\n    dice_scores = (\n        2 * intersections\n        / np.maximum(\n            prediction_sizes + target_sizes,\n            1\n        )\n    )\n\n    return iou_scores, dice_scores","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-15T02:16:19.256244Z","iopub.execute_input":"2026-09-15T02:16:19.256559Z","iopub.status.idle":"2026-09-15T02:16:19.265938Z","shell.execute_reply.started":"2026-09-15T02:16:19.256536Z","shell.execute_reply":"2026-09-15T02:16:19.265182Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class_names = [\n    \"Ground Glass\",\n    \"Consolidation\",\n    \"Lungs Other\",\n    \"Background\"\n]\n\nclass_iou, class_dice = evaluate_per_class(\n    model,\n    val_dataloader,\n    device\n)\n\nclass_results = pd.DataFrame({\n    \"Class\": class_names,\n    \"IoU\": class_iou,\n    \"Dice\": class_dice\n})\n\nclass_results.round(4)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-15T02:16:19.267014Z","iopub.execute_input":"2026-09-15T02:16:19.267312Z","iopub.status.idle":"2026-09-15T02:16:19.414487Z","shell.execute_reply.started":"2026-09-15T02:16:19.267281Z","shell.execute_reply":"2026-09-15T02:16:19.413709Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### 5.3 Análisis del Desempeño por Clase\n\nLa evaluación por clase confirma el efecto del desbalance identificado durante el análisis exploratorio.\n\nEl baseline obtiene un desempeño elevado sobre Background y Lungs Other, mientras que Ground Glass presenta un desempeño inferior y Consolidation constituye su principal debilidad.\n\nLa diferencia entre las métricas de las clases frecuentes y las clases objetivo explica por qué una exactitud global elevada no implica necesariamente una buena segmentación de las lesiones de interés.\n\nPor esta razón, se propone incorporar Dice Loss para considerar directamente el solapamiento de Ground Glass y Consolidation durante el entrenamiento.","metadata":{}},{"cell_type":"markdown","source":"### 5.4 Comparación Visual de Predicciones\n\nSe comparan las máscaras reales del conjunto de validación con las máscaras generadas por el modelo baseline. Estas visualizaciones permiten identificar errores que no son evidentes únicamente mediante las métricas numéricas.","metadata":{}},{"cell_type":"code","source":"model.eval()\n\nval_images_batch, val_masks_batch = next(\n    iter(val_dataloader)\n)\n\nwith torch.no_grad():\n\n    val_logits = model(\n        val_images_batch.to(device)\n    )\n\n    val_predictions = torch.argmax(\n        val_logits,\n        dim=1\n    ).cpu()\n\nval_images_cpu = val_images_batch.cpu()\nval_masks_cpu = val_masks_batch.cpu()\n\nprint(val_images_cpu.shape)\nprint(val_masks_cpu.shape)\nprint(val_predictions.shape)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-15T02:16:19.415518Z","iopub.execute_input":"2026-09-15T02:16:19.416334Z","iopub.status.idle":"2026-09-15T02:16:19.548838Z","shell.execute_reply.started":"2026-09-15T02:16:19.416301Z","shell.execute_reply":"2026-09-15T02:16:19.548294Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"NUM_EXAMPLES = 4\n\nfor i in range(NUM_EXAMPLES):\n\n    plt.figure(figsize=(6, 6))\n\n    plt.imshow(\n        val_images_cpu[i, 0],\n        cmap=\"gray\"\n    )\n\n    plt.title(f\"Imagen CT - Ejemplo {i + 1}\")\n    plt.axis(\"off\")\n    plt.show()\n\n\n    plt.figure(figsize=(6, 6))\n\n    plt.imshow(\n        val_masks_cpu[i],\n        cmap=\"viridis\",\n        vmin=0,\n        vmax=3\n    )\n\n    plt.title(\n        f\"Ground Truth - Ejemplo {i + 1}\"\n    )\n\n    plt.colorbar(ticks=[0, 1, 2, 3])\n    plt.axis(\"off\")\n    plt.show()\n\n\n    plt.figure(figsize=(6, 6))\n\n    plt.imshow(\n        val_predictions[i],\n        cmap=\"viridis\",\n        vmin=0,\n        vmax=3\n    )\n\n    plt.title(\n        f\"Predicción Baseline - Ejemplo {i + 1}\"\n    )\n\n    plt.colorbar(ticks=[0, 1, 2, 3])\n    plt.axis(\"off\")\n    plt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-15T02:16:19.549660Z","iopub.execute_input":"2026-09-15T02:16:19.549960Z","iopub.status.idle":"2026-09-15T02:16:20.950725Z","shell.execute_reply.started":"2026-09-15T02:16:19.549936Z","shell.execute_reply":"2026-09-15T02:16:20.949762Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"TARGET_CLASSES = {\n    0: \"Ground Glass\",\n    1: \"Consolidation\"\n}\n\nfor image_index in range(4):\n\n    for class_id, class_name in TARGET_CLASSES.items():\n\n        true_mask = (\n            val_masks_cpu[image_index].numpy()\n            == class_id\n        )\n\n        predicted_mask = (\n            val_predictions[image_index].numpy()\n            == class_id\n        )\n\n        plt.figure(figsize=(6, 6))\n\n        plt.imshow(true_mask, cmap=\"gray\")\n\n        plt.title(\n            f\"{class_name} - Ground Truth \"\n            f\"(Ejemplo {image_index + 1})\"\n        )\n\n        plt.axis(\"off\")\n        plt.show()\n\n\n        plt.figure(figsize=(6, 6))\n\n        plt.imshow(predicted_mask, cmap=\"gray\")\n\n        plt.title(\n            f\"{class_name} - Predicción \"\n            f\"(Ejemplo {image_index + 1})\"\n        )\n\n        plt.axis(\"off\")\n        plt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-15T02:16:20.951745Z","iopub.execute_input":"2026-09-15T02:16:20.953099Z","iopub.status.idle":"2026-09-15T02:16:22.306503Z","shell.execute_reply.started":"2026-09-15T02:16:20.953072Z","shell.execute_reply":"2026-09-15T02:16:22.305790Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 6. Modelo Mejorado\n\n### 6.1 Motivación de la Mejora\n\nEl análisis del modelo baseline muestra un fuerte contraste entre el desempeño de las clases frecuentes y las clases objetivo.\n\nMientras que Background y Lungs Other obtienen valores Dice superiores a 0.90, Consolidation obtiene un Dice considerablemente menor. Además, la exactitud global por píxel es elevada a pesar del desempeño limitado sobre las regiones de interés.\n\nPara atender este problema se modifica la función de pérdida. El modelo baseline utiliza únicamente Cross-Entropy, que evalúa la clasificación individual de cada píxel. En el modelo mejorado se combina Cross-Entropy con Dice Loss.\n\nDice Loss considera directamente el solapamiento entre la región predicha y el ground truth. En esta implementación, el componente Dice se calcula específicamente sobre Ground Glass y Consolidation, las dos clases que evalúa la competencia.\n\nEl resto de la configuración principal se mantiene sin cambios para facilitar una comparación consistente con el baseline.","metadata":{}},{"cell_type":"code","source":"class TargetDiceLoss(nn.Module):\n    def __init__(\n        self,\n        target_classes=(0, 1),\n        smooth=1.0\n    ):\n        super().__init__()\n\n        self.target_classes = target_classes\n        self.smooth = smooth\n\n    def forward(self, logits, targets):\n        # Convertimos los logits a probabilidades\n        probabilities = F.softmax(\n            logits,\n            dim=1\n        )\n\n        # Ground truth:\n        # [B, H, W]\n        #\n        # one-hot:\n        # [B, H, W, C]\n        targets_one_hot = F.one_hot(\n            targets,\n            num_classes=logits.shape[1]\n        )\n\n        # PyTorch trabaja con:\n        # [B, C, H, W]\n        targets_one_hot = targets_one_hot.permute(\n            0, 3, 1, 2\n        ).float()\n\n        dice_scores = []\n\n        for class_id in self.target_classes:\n            pred = probabilities[:, class_id]\n            true = targets_one_hot[:, class_id]\n\n            intersection = (\n                pred * true\n            ).sum()\n\n            denominator = (\n                pred.sum()\n                + true.sum()\n            )\n\n            dice = (\n                2 * intersection + self.smooth\n            ) / (\n                denominator + self.smooth\n            )\n\n            dice_scores.append(dice)\n\n        mean_dice = torch.stack(\n            dice_scores\n        ).mean()\n\n        return 1 - mean_dice","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-15T02:16:22.307561Z","iopub.execute_input":"2026-09-15T02:16:22.307879Z","iopub.status.idle":"2026-09-15T02:16:22.314819Z","shell.execute_reply.started":"2026-09-15T02:16:22.307845Z","shell.execute_reply":"2026-09-15T02:16:22.314134Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class CombinedLoss(nn.Module):\n    def __init__(\n        self,\n        ce_weight=0.5,\n        dice_weight=0.5\n    ):\n        super().__init__()\n\n        self.cross_entropy = nn.CrossEntropyLoss()\n\n        self.dice_loss = TargetDiceLoss(\n            target_classes=(0, 1)\n        )\n\n        self.ce_weight = ce_weight\n        self.dice_weight = dice_weight\n\n    def forward(self, logits, targets):\n        ce = self.cross_entropy(\n            logits,\n            targets\n        )\n\n        dice = self.dice_loss(\n            logits,\n            targets\n        )\n\n        total_loss = (\n            self.ce_weight * ce\n            + self.dice_weight * dice\n        )\n\n        return total_loss","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-15T02:16:22.315667Z","iopub.execute_input":"2026-09-15T02:16:22.315979Z","iopub.status.idle":"2026-09-15T02:16:22.330496Z","shell.execute_reply.started":"2026-09-15T02:16:22.315944Z","shell.execute_reply":"2026-09-15T02:16:22.329873Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"set_seed(SEED)\n\nimproved_model = smp.Unet(\n    encoder_name=\"efficientnet-b2\",\n    encoder_weights=\"imagenet\",\n    in_channels=1,\n    classes=4,\n    activation=None,\n    encoder_depth=5,\n    decoder_channels=(\n        256,\n        128,\n        64,\n        32,\n        16\n    )\n)\n\nimproved_model = improved_model.to(device)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-15T02:16:22.331300Z","iopub.execute_input":"2026-09-15T02:16:22.331603Z","iopub.status.idle":"2026-09-15T02:16:22.497278Z","shell.execute_reply.started":"2026-09-15T02:16:22.331580Z","shell.execute_reply":"2026-09-15T02:16:22.496727Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"improved_criterion = CombinedLoss(\n    ce_weight=0.5,\n    dice_weight=0.5\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-15T02:16:22.498157Z","iopub.execute_input":"2026-09-15T02:16:22.498437Z","iopub.status.idle":"2026-09-15T02:16:22.502333Z","shell.execute_reply.started":"2026-09-15T02:16:22.498399Z","shell.execute_reply":"2026-09-15T02:16:22.501446Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"improved_model.eval()\n\nwith torch.no_grad():\n    sanity_logits = improved_model(\n        images_batch[:2].to(device)\n    )\n\nsanity_masks = masks_batch[:2].to(device)\n\nsanity_loss = improved_criterion(\n    sanity_logits,\n    sanity_masks\n)\n\nprint(\"Combined Loss inicial:\", sanity_loss.item())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-15T02:16:22.503349Z","iopub.execute_input":"2026-09-15T02:16:22.503642Z","iopub.status.idle":"2026-09-15T02:16:22.539512Z","shell.execute_reply.started":"2026-09-15T02:16:22.503609Z","shell.execute_reply":"2026-09-15T02:16:22.538985Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"improved_optimizer = torch.optim.AdamW(\n    improved_model.parameters(),\n    lr=MAX_LR,\n    weight_decay=WEIGHT_DECAY\n)\n\nimproved_scheduler = (\n    torch.optim.lr_scheduler.OneCycleLR(\n        improved_optimizer,\n        max_lr=MAX_LR,\n        epochs=EPOCHS,\n        steps_per_epoch=len(\n            train_dataloader\n        )\n    )\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-15T02:16:22.540383Z","iopub.execute_input":"2026-09-15T02:16:22.540652Z","iopub.status.idle":"2026-09-15T02:16:22.548011Z","shell.execute_reply.started":"2026-09-15T02:16:22.540621Z","shell.execute_reply":"2026-09-15T02:16:22.547311Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"history_improved = train_model(\n    model=improved_model,\n    train_loader=train_dataloader,\n    val_loader=val_dataloader,\n    criterion=improved_criterion,\n    optimizer=improved_optimizer,\n    scheduler=improved_scheduler,\n    epochs=EPOCHS,\n    device=device,\n    checkpoint_path=(\n        \"/kaggle/working/\"\n        \"best_improved_model.pth\"\n    )\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-15T02:16:22.549117Z","iopub.execute_input":"2026-09-15T02:16:22.549402Z","iopub.status.idle":"2026-09-15T02:19:23.473432Z","shell.execute_reply.started":"2026-09-15T02:16:22.549372Z","shell.execute_reply":"2026-09-15T02:19:23.472757Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"improved_model.load_state_dict(\n    torch.load(\n        \"/kaggle/working/best_improved_model.pth\",\n        map_location=device,\n        weights_only=True\n    )\n)\n\nimproved_model = improved_model.to(device)\nimproved_model.eval()\n\nprint(\"Mejor checkpoint del modelo mejorado cargado.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-15T02:19:23.474396Z","iopub.execute_input":"2026-09-15T02:19:23.474707Z","iopub.status.idle":"2026-09-15T02:19:23.632329Z","shell.execute_reply.started":"2026-09-15T02:19:23.474681Z","shell.execute_reply":"2026-09-15T02:19:23.631648Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"improved_class_iou, improved_class_dice = evaluate_per_class(\n    improved_model,\n    val_dataloader,\n    device\n)\n\nimproved_class_results = pd.DataFrame({\n    \"Class\": class_names,\n    \"IoU\": improved_class_iou,\n    \"Dice\": improved_class_dice\n})\n\nimproved_class_results.round(4)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-15T02:19:23.633323Z","iopub.execute_input":"2026-09-15T02:19:23.633663Z","iopub.status.idle":"2026-09-15T02:19:23.773976Z","shell.execute_reply.started":"2026-09-15T02:19:23.633638Z","shell.execute_reply":"2026-09-15T02:19:23.773228Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"comparison_results = pd.DataFrame({\n    \"Metric\": [\n        \"Validation mIoU\",\n        \"Target IoU\",\n        \"Target Dice\",\n        \"Validation Accuracy\"\n    ],\n    \"Baseline\": [\n        history_baseline[\"val_miou\"][-1],\n        history_baseline[\"val_target_miou\"][-1],\n        history_baseline[\"val_target_dice\"][-1],\n        history_baseline[\"val_accuracy\"][-1]\n    ],\n    \"Improved\": [\n        history_improved[\"val_miou\"][-1],\n        history_improved[\"val_target_miou\"][-1],\n        history_improved[\"val_target_dice\"][-1],\n        history_improved[\"val_accuracy\"][-1]\n    ]\n})\n\ncomparison_results[\"Absolute Improvement\"] = (\n    comparison_results[\"Improved\"]\n    - comparison_results[\"Baseline\"]\n)\n\ncomparison_results.round(4)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-15T02:19:23.774981Z","iopub.execute_input":"2026-09-15T02:19:23.775290Z","iopub.status.idle":"2026-09-15T02:19:23.786217Z","shell.execute_reply.started":"2026-09-15T02:19:23.775256Z","shell.execute_reply":"2026-09-15T02:19:23.785496Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class_comparison = pd.DataFrame({\n    \"Class\": class_names,\n\n    \"Baseline IoU\": class_iou,\n    \"Improved IoU\": improved_class_iou,\n\n    \"Baseline Dice\": class_dice,\n    \"Improved Dice\": improved_class_dice\n})\n\nclass_comparison[\"IoU Improvement\"] = (\n    class_comparison[\"Improved IoU\"]\n    - class_comparison[\"Baseline IoU\"]\n)\n\nclass_comparison[\"Dice Improvement\"] = (\n    class_comparison[\"Improved Dice\"]\n    - class_comparison[\"Baseline Dice\"]\n)\n\nclass_comparison.round(4)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-15T02:19:23.787064Z","iopub.execute_input":"2026-09-15T02:19:23.787352Z","iopub.status.idle":"2026-09-15T02:19:23.804798Z","shell.execute_reply.started":"2026-09-15T02:19:23.787317Z","shell.execute_reply":"2026-09-15T02:19:23.804207Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"plt.figure(figsize=(8, 5))\n\nplt.plot(\n    history_improved[\"train_miou\"],\n    marker=\"o\",\n    label=\"Train mIoU\"\n)\n\nplt.plot(\n    history_improved[\"val_miou\"],\n    marker=\"o\",\n    label=\"Validation mIoU\"\n)\n\nplt.xlabel(\"epoch\")\nplt.ylabel(\"mIoU\")\nplt.title(\"Modelo Mejorado - Training vs Validation mIoU\")\nplt.legend()\nplt.grid()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-15T02:19:23.805679Z","iopub.execute_input":"2026-09-15T02:19:23.805853Z","iopub.status.idle":"2026-09-15T02:19:23.947353Z","shell.execute_reply.started":"2026-09-15T02:19:23.805834Z","shell.execute_reply":"2026-09-15T02:19:23.946740Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"plt.figure(figsize=(8, 5))\n\nplt.plot(\n    history_improved[\"val_target_miou\"],\n    marker=\"o\",\n    label=\"Validation Target IoU\"\n)\n\nplt.plot(\n    history_improved[\"val_target_dice\"],\n    marker=\"o\",\n    label=\"Validation Target Dice\"\n)\n\nplt.xlabel(\"epoch\")\nplt.ylabel(\"Score\")\nplt.title(\"Modelo Mejorado - Ground Glass y Consolidation\")\nplt.legend()\nplt.grid()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-15T02:19:23.948204Z","iopub.execute_input":"2026-09-15T02:19:23.948546Z","iopub.status.idle":"2026-09-15T02:19:24.081940Z","shell.execute_reply.started":"2026-09-15T02:19:23.948523Z","shell.execute_reply":"2026-09-15T02:19:24.081278Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"plt.figure(figsize=(8, 5))\n\nplt.plot(\n    history_baseline[\"val_target_dice\"],\n    marker=\"o\",\n    label=\"Baseline\"\n)\n\nplt.plot(\n    history_improved[\"val_target_dice\"],\n    marker=\"o\",\n    label=\"Improved\"\n)\n\nplt.xlabel(\"epoch\")\nplt.ylabel(\"Target Dice\")\nplt.title(\"Baseline vs Improved - Validation Target Dice\")\nplt.legend()\nplt.grid()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-15T02:19:24.082858Z","iopub.execute_input":"2026-09-15T02:19:24.083139Z","iopub.status.idle":"2026-09-15T02:19:24.216337Z","shell.execute_reply.started":"2026-09-15T02:19:24.083118Z","shell.execute_reply":"2026-09-15T02:19:24.215663Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"plt.figure(figsize=(8, 5))\n\nplt.plot(\n    history_baseline[\"val_miou\"],\n    marker=\"o\",\n    label=\"Baseline\"\n)\n\nplt.plot(\n    history_improved[\"val_miou\"],\n    marker=\"o\",\n    label=\"Modelo mejorado\"\n)\n\nplt.xlabel(\"epoch\")\nplt.ylabel(\"mIoU\")\nplt.title(\n    \"Baseline vs Modelo Mejorado - \"\n    \"Validation mIoU\"\n)\n\nplt.legend()\nplt.grid()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-15T02:19:24.217239Z","iopub.execute_input":"2026-09-15T02:19:24.217558Z","iopub.status.idle":"2026-09-15T02:19:24.364090Z","shell.execute_reply.started":"2026-09-15T02:19:24.217535Z","shell.execute_reply":"2026-09-15T02:19:24.363421Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### 6.2 Resultados del Modelo Mejorado\n\nLa incorporación de Dice Loss mejora las principales métricas de las clases objetivo respecto al baseline, como muestran las tablas y curvas anteriores.\n\nLa mejora más importante ocurre en Consolidation, que presenta un incremento considerable tanto en IoU como en Dice. Ground Glass también mejora, mientras que Lungs Other y Background conservan un desempeño elevado con un decremento relativamente bajo.\n\nEstos resultados indican que la función de pérdida combinada dirige mayor atención hacia las regiones minoritarias de interés sin producir un deterioro importante del desempeño global.","metadata":{}},{"cell_type":"markdown","source":"### 6.3 Comparación Baseline vs Modelo Mejorado\n\nPara evaluar el efecto de la modificación se mantuvieron sin cambios la arquitectura U-Net con EfficientNet-B2, la separación de datos, las técnicas de aumento, el optimizador, la tasa de aprendizaje, el batch size y el número de epochs.\n\nLa principal diferencia entre ambos experimentos fue la función de pérdida:\n\n- Baseline: Cross-Entropy.\n- Modelo mejorado: 0.5 x Cross-Entropy + 0.5 x Dice Loss.\n\nEsto permite estudiar principalmente el efecto de incorporar una medida explícita de solapamiento sobre Ground Glass y Consolidation.","metadata":{}},{"cell_type":"markdown","source":"### 6.4 Comparación Visual de Segmentaciones\n\nAdemás de las métricas numéricas, se comparan las predicciones del modelo mejorado con las anotaciones reales del conjunto de validación.\n\nSe presta especial atención a Ground Glass y Consolidation, ya que constituyen las clases objetivo de la competencia.","metadata":{}},{"cell_type":"code","source":"improved_model.eval()\n\nwith torch.no_grad():\n    improved_logits = improved_model(\n        val_images_batch.to(device)\n    )\n\n    improved_predictions = torch.argmax(\n        improved_logits,\n        dim=1\n    ).cpu()\n\nprint(improved_predictions.shape)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-15T02:19:24.364840Z","iopub.execute_input":"2026-09-15T02:19:24.365129Z","iopub.status.idle":"2026-09-15T02:19:24.479195Z","shell.execute_reply.started":"2026-09-15T02:19:24.365099Z","shell.execute_reply":"2026-09-15T02:19:24.478646Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"example_index = 9\n\nplt.figure(figsize=(15, 5))\n\nplt.subplot(1, 3, 1)\nplt.imshow(\n    val_masks_cpu[example_index],\n    cmap=\"viridis\",\n    vmin=0,\n    vmax=3\n)\nplt.title(\"Ground Truth\")\nplt.axis(\"off\")\n\nplt.subplot(1, 3, 2)\nplt.imshow(\n    val_predictions[example_index],\n    cmap=\"viridis\",\n    vmin=0,\n    vmax=3\n)\nplt.title(\"Baseline\")\nplt.axis(\"off\")\n\nplt.subplot(1, 3, 3)\nplt.imshow(\n    improved_predictions[example_index],\n    cmap=\"viridis\",\n    vmin=0,\n    vmax=3\n)\nplt.title(\"Modelo Mejorado\")\nplt.axis(\"off\")\n\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-15T02:19:24.480066Z","iopub.execute_input":"2026-09-15T02:19:24.480533Z","iopub.status.idle":"2026-09-15T02:19:25.102306Z","shell.execute_reply.started":"2026-09-15T02:19:24.480500Z","shell.execute_reply":"2026-09-15T02:19:25.101588Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 7. Predicción sobre el Conjunto de Prueba\n\nLas diez imágenes del conjunto de prueba no cuentan con máscaras públicas, ya que estas constituyen las etiquetas que deben ser predichas para la competencia.\n\nPara procesarlas se aplica el mismo preprocesamiento utilizado durante validación: cambio de tamaño a 256 x 256, conversión a tensor y normalización.","metadata":{}},{"cell_type":"code","source":"class CovidTestDataset(Dataset):\n    def __init__(\n        self,\n        images,\n        augmentations=None,\n        imagenet_normalization=True\n    ):\n        self.images = images\n        self.augmentations = augmentations\n        self.imagenet_normalization = imagenet_normalization\n\n        self.mean = 0.485\n        self.std = 0.229\n\n    def __len__(self):\n        return len(self.images)\n\n    def __getitem__(self, index):\n        image = self.images[index]\n\n        if self.augmentations is not None:\n            sample = self.augmentations(\n                image=image\n            )\n\n            image = sample[\"image\"]\n\n        image = np.transpose(\n            image,\n            (2, 0, 1)\n        )\n\n        image = torch.from_numpy(\n            np.ascontiguousarray(image)\n        ).float()\n\n        if self.imagenet_normalization:\n            image = (\n                image - self.mean\n            ) / self.std\n\n        return image","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-15T02:19:25.103359Z","iopub.execute_input":"2026-09-15T02:19:25.103696Z","iopub.status.idle":"2026-09-15T02:19:25.111267Z","shell.execute_reply.started":"2026-09-15T02:19:25.103672Z","shell.execute_reply":"2026-09-15T02:19:25.110548Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"test_dataset = CovidTestDataset(\n    test_images_medseg_processed,\n    augmentations=val_augs\n)\n\ntest_dataloader = DataLoader(\n    test_dataset,\n    batch_size=10,\n    shuffle=False\n)\n\ntest_batch = next(\n    iter(test_dataloader)\n)\n\nprint(test_batch.shape)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-15T02:19:25.112280Z","iopub.execute_input":"2026-09-15T02:19:25.112626Z","iopub.status.idle":"2026-09-15T02:19:25.131617Z","shell.execute_reply.started":"2026-09-15T02:19:25.112599Z","shell.execute_reply":"2026-09-15T02:19:25.130959Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"improved_model.load_state_dict(\n    torch.load(\n        \"/kaggle/working/best_improved_model.pth\",\n        map_location=device,\n        weights_only=True\n    )\n)\n\nimproved_model.eval()\nprint(\"Modelo mejorado listo para inferencia.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-15T02:19:25.132620Z","iopub.execute_input":"2026-09-15T02:19:25.132936Z","iopub.status.idle":"2026-09-15T02:19:25.288631Z","shell.execute_reply.started":"2026-09-15T02:19:25.132884Z","shell.execute_reply":"2026-09-15T02:19:25.288051Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"with torch.no_grad():\n\n    test_logits = improved_model(\n        test_batch.to(device)\n    )\n\n    test_predictions = torch.argmax(\n        test_logits,\n        dim=1\n    ).cpu().numpy()\n\nprint(test_predictions.shape)\nprint(np.unique(test_predictions))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-15T02:19:25.289386Z","iopub.execute_input":"2026-09-15T02:19:25.289656Z","iopub.status.idle":"2026-09-15T02:19:25.361451Z","shell.execute_reply.started":"2026-09-15T02:19:25.289632Z","shell.execute_reply":"2026-09-15T02:19:25.360864Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"test_target_masks = np.stack(\n    [\n        test_predictions == 0,\n        test_predictions == 1\n    ],\n    axis=-1\n).astype(np.uint8)\n\nprint(test_target_masks.shape)\nprint(np.unique(test_target_masks))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-15T02:19:25.362178Z","iopub.execute_input":"2026-09-15T02:19:25.362457Z","iopub.status.idle":"2026-09-15T02:19:25.393328Z","shell.execute_reply.started":"2026-09-15T02:19:25.362428Z","shell.execute_reply":"2026-09-15T02:19:25.392759Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"test_target_masks_original = np.zeros(\n    (10, 512, 512, 2),\n    dtype=np.uint8\n)\n\nfor i in range(10):\n    for channel in range(2):\n\n        test_target_masks_original[\n            i, :, :, channel\n        ] = cv2.resize(\n            test_target_masks[\n                i, :, :, channel\n            ],\n            (512, 512),\n            interpolation=cv2.INTER_NEAREST\n        )","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-15T02:19:25.394196Z","iopub.execute_input":"2026-09-15T02:19:25.394521Z","iopub.status.idle":"2026-09-15T02:19:25.408382Z","shell.execute_reply.started":"2026-09-15T02:19:25.394499Z","shell.execute_reply":"2026-09-15T02:19:25.407831Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"for i in range(3):\n\n    plt.figure(figsize=(6, 6))\n    plt.imshow(\n        test_images_medseg[i, :, :, 0],\n        cmap=\"gray\"\n    )\n    plt.title(f\"Test CT {i + 1}\")\n    plt.axis(\"off\")\n    plt.show()\n\n    plt.figure(figsize=(6, 6))\n    plt.imshow(\n        test_target_masks_original[\n            i, :, :, 0\n        ],\n        cmap=\"gray\"\n    )\n    plt.title(\n        f\"Ground Glass Predicho - {i + 1}\"\n    )\n    plt.axis(\"off\")\n    plt.show()\n\n    plt.figure(figsize=(6, 6))\n    plt.imshow(\n        test_target_masks_original[\n            i, :, :, 1\n        ],\n        cmap=\"gray\"\n    )\n    plt.title(\n        f\"Consolidation Predicha - {i + 1}\"\n    )\n    plt.axis(\"off\")\n    plt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-15T02:19:25.409222Z","iopub.execute_input":"2026-09-15T02:19:25.409536Z","iopub.status.idle":"2026-09-15T02:19:26.512122Z","shell.execute_reply.started":"2026-09-15T02:19:25.409513Z","shell.execute_reply":"2026-09-15T02:19:26.511401Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### 7.1 Generación del Archivo de Submission\n\nLa competencia requiere una predicción binaria para Ground Glass y Consolidation en cada píxel de las diez imágenes de prueba.\n\nLas máscaras generadas tienen dimensiones `(10, 512, 512, 2)`. Para construir el archivo solicitado por Kaggle, los valores se aplanan respetando el mismo orden utilizado por el notebook baseline y se almacenan en las columnas `Id` y `Predicted`.","metadata":{}},{"cell_type":"code","source":"assert test_target_masks_original.shape == (10, 512, 512, 2)\n\nsubmission_values = (\n    test_target_masks_original\n    .ravel()\n    .astype(np.uint8)\n)\n\nsubmission = pd.DataFrame({\n    \"Id\": np.arange(submission_values.size),\n    \"Predicted\": submission_values\n})\n\nsubmission.to_csv(\n    \"/kaggle/working/sub.csv\",\n    index=False\n)\n\nprint(\"Submission shape:\", submission.shape)\nprint(\"Valores únicos:\", submission[\"Predicted\"].unique())\n\nsubmission.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-15T02:19:26.513303Z","iopub.execute_input":"2026-09-15T02:19:26.514005Z","iopub.status.idle":"2026-09-15T02:19:30.101341Z","shell.execute_reply.started":"2026-09-15T02:19:26.513971Z","shell.execute_reply":"2026-09-15T02:19:30.100368Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 8. Conclusiones\n\nEn este proyecto se desarrolló una solución de segmentación semántica para imágenes CT de COVID-19 utilizando una arquitectura U-Net con EfficientNet-B2 como encoder.\n\nEl análisis exploratorio mostró un desbalance entre las clases, especialmente para Ground Glass y Consolidation frente a Background y Lungs Other. Esta característica se reflejó en el modelo baseline: aunque obtuvo una accuracy de validación cercana a 0.937, presentó dificultades importantes para segmentar Consolidation.\n\nPara atender este problema se sustituyó la función de pérdida basada únicamente en Cross-Entropy por una combinación de 0.5 Cross-Entropy y 0.5 Dice Loss orientada a las dos clases objetivo.\n\nLa modificación aumentó el mIoU de validación de aproximadamente 0.570 a 0.640 y el Dice promedio de las clases objetivo de aproximadamente 0.319 a 0.534. La mayor mejora ocurrió en Consolidation, cuyo Dice pasó de aproximadamente 0.0004 a 0.396.\n\nUna fortaleza del enfoque es que la modificación mejora considerablemente las clases minoritarias sin requerir cambios en la arquitectura. Sin embargo, el modelo mejorado presenta una mayor diferencia entre el desempeño de entrenamiento y validación, lo que sugiere una tendencia más fuerte al sobreajuste.\n\nComo trabajo futuro podrían evaluarse diferentes ponderaciones entre Cross-Entropy y Dice Loss, técnicas adicionales de regularización, divisiones de datos a nivel paciente y múltiples ejecuciones con diferentes semillas para analizar la estabilidad de los resultados.\n\nUno de los principales desafíos del proyecto fue el desbalance entre las clases y la diferencia entre una alta exactitud global y un bajo desempeño sobre las lesiones de interés. El análisis por clase permitió identificar este problema y orientar la modificación de la función de pérdida.","metadata":{}},{"cell_type":"markdown","source":"## 9. Uso de Modelos de Lenguaje\n\nDurante el desarrollo del proyecto se utilizó chatGPT como herramienta de apoyo para comprender conceptos relacionados con PyTorch, U-Net, funciones de pérdida nuevas y métricas de segmentación, así como adaptar código del notebook baseline a versiones actuales de las librerías y apoyar en la depuración del pipeline.","metadata":{}},{"cell_type":"markdown","source":"## 10. Referencias\n\n1. Maftouni, M. *PyTorch Baseline for Semantic Segmentation*. Notebook de Kaggle utilizado como punto de partida. [https://www.kaggle.com/code/maedemaftouni/pytorch-baseline-for-semantic-segmentation].\n\n2. Falta U-Net","metadata":{}}]}