{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.11","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"gpu","dataSources":[{"sourceId":33679,"databundleVersionId":3212216,"isSourceIdPinned":false,"sourceType":"competition"},{"sourceId":12015253,"sourceType":"datasetVersion","datasetId":7559147},{"sourceId":12023771,"sourceType":"datasetVersion","datasetId":7564770},{"sourceId":12077317,"sourceType":"datasetVersion","datasetId":7526943},{"sourceId":12129797,"sourceType":"datasetVersion","datasetId":7638307},{"sourceId":12150417,"sourceType":"datasetVersion","datasetId":7652308},{"sourceId":12170028,"sourceType":"datasetVersion","datasetId":7664854},{"sourceId":12181783,"sourceType":"datasetVersion","datasetId":7672450},{"sourceId":12313161,"sourceType":"datasetVersion","datasetId":7761186},{"sourceId":12318658,"sourceType":"datasetVersion","datasetId":7764776},{"sourceId":12319039,"sourceType":"datasetVersion","datasetId":7764986},{"sourceId":12319091,"sourceType":"datasetVersion","datasetId":7765021},{"sourceId":12319109,"sourceType":"datasetVersion","datasetId":7765033},{"sourceId":12323674,"sourceType":"datasetVersion","datasetId":7768063},{"sourceId":435708,"sourceType":"modelInstanceVersion","isSourceIdPinned":false,"modelInstanceId":355367,"modelId":376445},{"sourceId":436763,"sourceType":"modelInstanceVersion","isSourceIdPinned":false,"modelInstanceId":356249,"modelId":376445},{"sourceId":453069,"sourceType":"modelInstanceVersion","isSourceIdPinned":true,"modelInstanceId":367543,"modelId":388438}],"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import os\nfrom pathlib import Path\nimport json\nimport shutil\nimport numpy as np\nimport pandas as pd\nfrom collections import Counter\nfrom tqdm import tqdm\nfrom PIL import Image, UnidentifiedImageError, ImageOps\nimport matplotlib.pyplot as plt\nimport seaborn as sns\nimport plotly.express as px\nimport tensorflow as tf\nfrom tensorflow import keras\nfrom tensorflow.keras import regularizers\nfrom tensorflow.keras.applications.efficientnet import preprocess_input\nfrom sklearn import set_config\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.metrics import confusion_matrix, accuracy_score\nimport torchvision.transforms as T\nfrom glob import glob\nfrom torchvision import transforms as T\nfrom PIL import ImageOps\nfrom sklearn.cluster import KMeans\nfrom tensorflow.keras.applications import ConvNeXtBase\nfrom tensorflow.keras.layers import Input, Dense\nfrom tensorflow.keras.models import Model\nfrom tensorflow.keras.applications.convnext import preprocess_input\nfrom sklearn.preprocessing import LabelEncoder\nimport tensorflow as tf\nfrom tensorflow.keras.applications import EfficientNetV2B3\nfrom tensorflow.keras.applications.efficientnet_v2 import preprocess_input\nfrom tensorflow.keras.preprocessing import image_dataset_from_directory\nfrom tensorflow.keras.layers import GlobalAveragePooling2D\nfrom tensorflow.keras.preprocessing import image\nfrom sklearn.preprocessing import StandardScaler\nfrom sklearn.metrics import accuracy_score, f1_score\n\n\nset_config(transform_output=\"pandas\")\nseed_global = 27912","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-06-30T05:59:23.679138Z","iopub.execute_input":"2025-06-30T05:59:23.679394Z","iopub.status.idle":"2025-06-30T05:59:45.787298Z","shell.execute_reply.started":"2025-06-30T05:59:23.679374Z","shell.execute_reply":"2025-06-30T05:59:45.786736Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def build_cluster_dataset_hierarchical(image_paths, y_hier, image_size=(167, 250), training=True):\n    path_ds = tf.data.Dataset.from_tensor_slices(image_paths)\n    hier_ds = tf.data.Dataset.from_tensor_slices(y_hier)\n\n    def load_img(path):\n        img = tf.io.read_file(path)\n        img = tf.image.decode_image(img, channels=3)\n        img = tf.image.resize_with_pad(img, target_height=167, target_width=250)\n        img = tf.keras.applications.convnext.preprocess_input(img)\n        return img\n\n    image_ds = path_ds.map(load_img, num_parallel_calls=tf.data.AUTOTUNE)\n    ds = tf.data.Dataset.zip((image_ds, hier_ds))\n    if training:\n        ds = ds.shuffle(buffer_size=1000)\n    ds = ds.batch(16, drop_remainder=True).prefetch(tf.data.AUTOTUNE)\n    return ds","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-30T05:59:45.788471Z","iopub.execute_input":"2025-06-30T05:59:45.788997Z","iopub.status.idle":"2025-06-30T05:59:45.794282Z","shell.execute_reply.started":"2025-06-30T05:59:45.788976Z","shell.execute_reply":"2025-06-30T05:59:45.793506Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# NIVEL JERÁRQUICO CATEGORÍA","metadata":{}},{"cell_type":"code","source":"train_cluster_df = pd.read_json(\"/kaggle/input/metadatos-aumentados/train_cluster_augmented_df (1).json\")\nval_cluster_df = pd.read_json(\"/kaggle/input/cluster-dataframes/val_cluster_df.json\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-30T05:59:45.795069Z","iopub.execute_input":"2025-06-30T05:59:45.795547Z","iopub.status.idle":"2025-06-30T05:59:50.233103Z","shell.execute_reply.started":"2025-06-30T05:59:45.795522Z","shell.execute_reply":"2025-06-30T05:59:50.232487Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Umbral mínimo de instancias por combinación\numbral_minimo = 750\n\n# Agrupar por cluster y familia\nconteo_cluster_familia = train_cluster_df.groupby([\"cluster\", \"familia\"]).size().reset_index(name=\"count\")\n\n# Filtrar combinaciones por debajo del umbral (pero mayor que 0)\nconteos_bajos_sin_cat = conteo_cluster_familia[\n    (conteo_cluster_familia[\"count\"] > 0) & (conteo_cluster_familia[\"count\"] < umbral_minimo)\n]\n\n# Filtrar combinaciones por debajo del umbral (pero mayor que 0)\nconteos_altos_sin_cat = conteo_cluster_familia[\n    (conteo_cluster_familia[\"count\"] > 0) & (conteo_cluster_familia[\"count\"] > umbral_minimo)\n]\n\n# Número de combinaciones\ntotal_bajos = len(conteos_bajos_sin_cat)\n\ntotal_altos = len(conteos_altos_sin_cat)\n\n# Total de instancias sumadas en esas combinaciones\ntotal_instancias_bajas = conteos_bajos_sin_cat[\"count\"].sum()\ntotal_instancias_altas = conteos_altos_sin_cat[\"count\"].sum()\n\nprint(f\"Total de combinaciones 'cluster + familia' con menos de {umbral_minimo} instancias: {total_bajos}\")\nprint(f\"Estas combinaciones suman {total_instancias_bajas} instancias\")\n\nprint(f\"Total de combinaciones 'cluster + familia' con más de {umbral_minimo} instancias: {total_altos}\")\nprint(f\"Estas combinaciones suman {total_instancias_altas} instancias\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-30T05:59:50.233908Z","iopub.execute_input":"2025-06-30T05:59:50.23413Z","iopub.status.idle":"2025-06-30T05:59:50.306656Z","shell.execute_reply.started":"2025-06-30T05:59:50.234111Z","shell.execute_reply":"2025-06-30T05:59:50.305904Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# definir un umbral mínimo\numbral_minimo = 750\n\n# conteo por cluster + familia + categoría\nconteo_cfc = (\n    train_cluster_df\n    .groupby([\"cluster\", \"familia\", \"categoria\"])\n    .size()\n    .reset_index(name=\"count\")\n)\n\n# obtener las combinaciones cluster+familia bajas (sin categoría)\nconteo_cf = (\n    train_cluster_df\n    .groupby([\"cluster\", \"familia\"])\n    .size()\n    .reset_index(name=\"count\")\n)\nbajas_cf = conteo_cf[\n    (conteo_cf[\"count\"] > 0) &\n    (conteo_cf[\"count\"] < umbral_minimo)\n][[\"cluster\", \"familia\"]]\n\n# filtrar el conteo con categoría para quedarnos sólo con esas parejas bajas\nconteos_bajos_filtrados = conteo_cfc.merge(\n    bajas_cf,\n    on=[\"cluster\", \"familia\"],\n    how=\"inner\"\n)\n\n# ya incluye columna 'categoria' y 'count'\nprint(f\"Total de filas filtradas: {len(conteos_bajos_filtrados)}\")\nprint(conteos_bajos_filtrados.head())\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-30T05:59:50.308838Z","iopub.execute_input":"2025-06-30T05:59:50.309067Z","iopub.status.idle":"2025-06-30T05:59:50.506374Z","shell.execute_reply.started":"2025-06-30T05:59:50.309048Z","shell.execute_reply":"2025-06-30T05:59:50.5057Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Crear un diccionario con clave (cluster, familia) y valor el número de instancias\npares_bajos_dict = {\n    (row[\"cluster\"], row[\"familia\"]): row[\"count\"]\n    for _, row in conteos_bajos_sin_cat.iterrows()\n}\n\nprint(f\"🔧 Diccionario de pares (cluster, familia) con menos de {umbral_minimo} instancias creado.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-30T05:59:50.507055Z","iopub.execute_input":"2025-06-30T05:59:50.507289Z","iopub.status.idle":"2025-06-30T05:59:50.57047Z","shell.execute_reply.started":"2025-06-30T05:59:50.507269Z","shell.execute_reply":"2025-06-30T05:59:50.569913Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"@tf.function\ndef cosine_embedding_loss(y_true, y_pred, label_embeddings):\n    y_true = tf.cast(y_true, tf.int32)\n    y_pred = tf.math.l2_normalize(y_pred, axis=1)\n    target_embeds = tf.gather(label_embeddings, y_true)\n    target_embeds = tf.math.l2_normalize(target_embeds, axis=1)\n    sim = tf.reduce_sum(y_pred * target_embeds, axis=1)\n    sim = tf.clip_by_value(sim, -1.0, 1.0)  # Seguridad adicional\n    return 1 - tf.reduce_mean(sim)\n    \n@tf.function\ndef train_step(x, y, model, label_embeddings, optimizer):\n    with tf.GradientTape() as tape:\n        preds = model(x, training=True)\n        loss = cosine_embedding_loss(y, preds, label_embeddings)\n\n        # Cálculo de predicciones\n        sims = tf.matmul(preds, tf.transpose(label_embeddings))\n        pred_ids = tf.argmax(sims, axis=1)\n        acc = tf.reduce_mean(tf.cast(tf.equal(pred_ids, tf.cast(y, tf.int64)), tf.float32))\n\n    grads = tape.gradient(loss, model.trainable_variables + [label_embeddings])\n    optimizer.apply_gradients(zip(grads, model.trainable_variables + [label_embeddings]))\n\n    return loss, acc\n\n\n@tf.function\ndef val_step(x, y, model, label_embeddings):\n    preds = model(x, training=False)\n    target_embeds = tf.gather(label_embeddings, tf.cast(y, tf.int32))\n    target_embeds = tf.math.l2_normalize(target_embeds, axis=1)\n    val_loss = 1 - tf.reduce_mean(tf.reduce_sum(preds * target_embeds, axis=1))\n    sims = tf.matmul(preds, tf.transpose(label_embeddings))\n    preds_idx = tf.argmax(sims, axis=1)\n    acc = tf.reduce_mean(tf.cast(tf.equal(preds_idx, tf.cast(y, tf.int64)), tf.float32))\n    return val_loss, acc","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-30T05:59:50.571154Z","iopub.execute_input":"2025-06-30T05:59:50.571358Z","iopub.status.idle":"2025-06-30T05:59:50.580741Z","shell.execute_reply.started":"2025-06-30T05:59:50.571341Z","shell.execute_reply":"2025-06-30T05:59:50.580144Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport numpy as np\nimport pandas as pd\nfrom sklearn.utils.class_weight import compute_class_weight\n\ndef get_dataset_por_cluster_familia_(\n    cluster_id,\n    fam_id,\n    df_train,\n    df_val,\n    conteos_bajos,\n    label_col=\"categoria\",\n    training=True,\n    label_map=None\n):\n    # Filtrar por cluster y familia\n    df_tr = df_train[(df_train[\"cluster\"] == cluster_id) & (df_train[\"familia\"] == fam_id)].copy()\n    df_vl = df_val  [(df_val  [\"cluster\"] == cluster_id) & (df_val  [\"familia\"] == fam_id)].copy()\n\n    # Unión de clases en train y val\n    clases_union = sorted(set(df_tr[label_col].unique()) | set(df_vl[label_col].unique()))\n    if training:\n        print(f\"Usando {len(clases_union)} clases de '{label_col}' (train ∪ val)\")\n\n    # Mapas de labels\n    if label_map is None:\n        label_map = {val: idx for idx, val in enumerate(clases_union)}\n    inv_label_map = {idx: val for val, idx in label_map.items()}\n\n    # Añadir ejemplos de conteos bajos solo al train\n    if training:\n        df_extra = conteos_bajos[\n            (conteos_bajos[\"cluster\"] == cluster_id) &\n            (conteos_bajos[\"familia\"] == fam_id) &\n            (conteos_bajos[label_col].isin(clases_union))\n        ]\n        print(f\"Añadiendo {len(df_extra)} ejemplos extra desde conteos_bajos al TRAIN\")\n        df_tr = pd.concat([df_tr, df_extra], ignore_index=True)\n\n    # Filtrar rutas que realmente existen (solo imprime en train)\n    def filter_existing(df, name):\n        mask = df[\"path\"].apply(lambda p: isinstance(p, str) and os.path.exists(p))\n        if training and name == \"TRAIN\":\n            removed = (~mask).sum()\n            print(f\"{removed} rutas inexistentes en {name}, eliminadas\")\n        return df[mask].reset_index(drop=True)\n\n    df_tr = filter_existing(df_tr, \"TRAIN\")\n    df_vl = filter_existing(df_vl, \"VALIDATION\")\n\n    # Elegir subset\n    target_df = df_tr if training else df_vl\n\n    # Mapear etiquetas a índices\n    target_df[\"label\"] = target_df[label_col].map(label_map)\n    paths = target_df[\"path\"].values\n    labels = target_df[\"label\"].values\n\n    # Crear el tf.data.Dataset\n    ds = build_cluster_dataset_hierarchical(\n        image_paths=paths,\n        y_hier=labels,\n        training=training\n    )\n\n    # Calcular y devolver pesos de clase solo para train\n    if training:\n        clases_presentes = np.unique(labels)\n        weights = compute_class_weight(\n            class_weight=\"balanced\",\n            classes=clases_presentes,\n            y=labels\n        )\n        cw = dict(zip(clases_presentes, weights))\n        total = sum(cw.values())\n        cw = {k: v / total for k, v in cw.items()}\n        custom_weights = {i: 1.0 + 0.35 * (cw.get(i, 0) - 1.0) for i in clases_union}\n        return ds, custom_weights, len(clases_union), inv_label_map, label_map\n\n    return ds, None, len(clases_union), inv_label_map, label_map","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-30T05:59:50.581417Z","iopub.execute_input":"2025-06-30T05:59:50.581645Z","iopub.status.idle":"2025-06-30T05:59:50.604286Z","shell.execute_reply.started":"2025-06-30T05:59:50.581627Z","shell.execute_reply":"2025-06-30T05:59:50.603635Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from tensorflow.keras.callbacks import EarlyStopping\nearly_stop = EarlyStopping(\n    monitor='val_macro_f1',\n    mode = 'max',\n    patience=5,\n    restore_best_weights=True,\n    verbose=1\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-30T05:59:50.605079Z","iopub.execute_input":"2025-06-30T05:59:50.605569Z","iopub.status.idle":"2025-06-30T05:59:50.623696Z","shell.execute_reply.started":"2025-06-30T05:59:50.605552Z","shell.execute_reply":"2025-06-30T05:59:50.623102Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import tensorflow as tf\nfrom tensorflow.keras import layers, regularizers, Model\n\nclass L2Normalization(layers.Layer):\n    def __init__(self, axis=1, **kwargs):\n        super().__init__(**kwargs)\n        self.axis = axis\n\n    def call(self, inputs):\n        return tf.math.l2_normalize(inputs, axis=self.axis)\n\n    def get_config(self):\n        config = super().get_config()\n        config.update({\"axis\": self.axis})\n        return config\n\ndef adapt_embedding_model_for_categoria(base_model, embedding_dim=128, extra_trainable_ratio=0.3):\n    # Si base_model es ya el backbone, lo usamos directamente\n    from tensorflow.keras import Model as KerasModel\n    if isinstance(base_model, KerasModel) and 'efficientnet' in base_model.name.lower():\n        backbone = base_model\n    else:\n        backbone = None\n        for layer in base_model.layers:\n            if isinstance(layer, KerasModel) and 'efficientnet' in layer.name.lower():\n                backbone = layer\n                break\n        if backbone is None:\n            raise ValueError(\"No se encontró EfficientNet dentro de base_model\")\n\n    # Congelar capas\n    n = len(backbone.layers)\n    m = int(n * extra_trainable_ratio)\n    for layer in backbone.layers[:-m]:\n        layer.trainable = False\n    for layer in backbone.layers[-m:]:\n        layer.trainable = True\n\n    # Data augmentation\n    augmentation = tf.keras.Sequential([\n        layers.RandomFlip(\"horizontal\"),\n        layers.RandomRotation(0.05),\n        layers.RandomZoom(0.1),\n    ], name=\"augmentation\")\n\n    # Construcción del modelo\n    inputs = layers.Input(shape=(167, 250, 3), name=\"input_image\")\n    x = augmentation(inputs)\n    x = backbone(x)\n    x = layers.GlobalAveragePooling2D(name=\"gap\")(x)\n\n    x = layers.Dense(512, activation=\"relu\", name=\"fc1\")(x)\n    x = layers.BatchNormalization(name=\"bn1\")(x)\n    x = layers.Dropout(0.4, name=\"drop1\")(x)\n\n    x = layers.Dense(256, activation=\"relu\", name=\"fc2\")(x)\n    x = layers.BatchNormalization(name=\"bn2\")(x)\n    x = layers.Dropout(0.4, name=\"drop2\")(x)\n\n    x = layers.Dense(\n        embedding_dim,\n        kernel_regularizer=regularizers.l2(1e-4),\n        name=\"embed_dense\"\n    )(x)\n\n    outputs = L2Normalization(axis=1, name=\"l2_normalize\")(x)\n\n    return Model(inputs=inputs, outputs=outputs, name=\"adapted_embedding_model\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-30T05:59:50.624554Z","iopub.execute_input":"2025-06-30T05:59:50.62507Z","iopub.status.idle":"2025-06-30T05:59:50.640608Z","shell.execute_reply.started":"2025-06-30T05:59:50.625037Z","shell.execute_reply":"2025-06-30T05:59:50.639898Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"cluster_id = 0\ndf_cluster = train_cluster_df[train_cluster_df[\"cluster\"] == cluster_id]\nfamilias_cluster = df_cluster[\"familia\"].unique()\n\n# Lista para guardar combinaciones que no están en conteos_bajos\ncombinaciones_faltantes = []\n\nfor fam_id in familias_cluster:\n    existe = ((conteos_bajos_sin_cat[\"cluster\"] == cluster_id) & (conteos_bajos_sin_cat[\"familia\"] == fam_id)).any()\n    if not existe:\n        combinaciones_faltantes.append((cluster_id, fam_id))\n\nprint(f\"Total combinaciones (cluster={cluster_id}, familia) que NO están en conteos_bajos: {len(combinaciones_faltantes)}\")\nprint(\"Combinaciones faltantes:\")\nfor comb in combinaciones_faltantes:\n    print(comb)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-30T05:59:50.641248Z","iopub.execute_input":"2025-06-30T05:59:50.641538Z","iopub.status.idle":"2025-06-30T05:59:50.704799Z","shell.execute_reply.started":"2025-06-30T05:59:50.641521Z","shell.execute_reply":"2025-06-30T05:59:50.704131Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"tf.debugging.set_log_device_placement(False)\nprint(\"GPUs disponibles:\", tf.config.list_physical_devices('GPU'))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-30T05:59:50.705506Z","iopub.execute_input":"2025-06-30T05:59:50.705694Z","iopub.status.idle":"2025-06-30T05:59:50.783453Z","shell.execute_reply.started":"2025-06-30T05:59:50.705679Z","shell.execute_reply":"2025-06-30T05:59:50.782646Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from keras.config import enable_unsafe_deserialization\nenable_unsafe_deserialization()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-30T05:59:50.784181Z","iopub.execute_input":"2025-06-30T05:59:50.784369Z","iopub.status.idle":"2025-06-30T05:59:50.800055Z","shell.execute_reply.started":"2025-06-30T05:59:50.784354Z","shell.execute_reply":"2025-06-30T05:59:50.799346Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import tensorflow as tf\nimport math\n\nclass CosineDecayWithWarmup(tf.keras.optimizers.schedules.LearningRateSchedule):\n    def __init__(self, initial_lr, warmup_steps, total_steps, alpha=0.0):\n        self.initial_lr = tf.cast(initial_lr, tf.float32)\n        self.warmup_steps = tf.cast(warmup_steps, tf.float32)\n        self.total_steps = tf.cast(total_steps, tf.float32)\n        self.alpha = tf.cast(alpha, tf.float32)\n        self.pi = tf.constant(math.pi, dtype=tf.float32)\n\n    def __call__(self, step):\n        step = tf.cast(step, tf.float32)\n\n        cosine_decay = 0.5 * (1 + tf.cos(self.pi * (step - self.warmup_steps) / (self.total_steps - self.warmup_steps)))\n        decayed = (1 - self.alpha) * cosine_decay + self.alpha\n\n        lr = tf.where(\n            step < self.warmup_steps,\n            self.initial_lr * (step / self.warmup_steps),\n            self.initial_lr * decayed\n        )\n        return lr","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-30T05:59:50.802239Z","iopub.execute_input":"2025-06-30T05:59:50.802774Z","iopub.status.idle":"2025-06-30T05:59:50.814183Z","shell.execute_reply.started":"2025-06-30T05:59:50.802756Z","shell.execute_reply":"2025-06-30T05:59:50.813484Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import gc\nimport json\nimport pickle\nimport numpy as np\nimport tensorflow as tf\nfrom tqdm import tqdm\nfrom sklearn.utils.class_weight import compute_class_weight\n\n\nos.makedirs(\"/kaggle/working/modelos\", exist_ok=True)\ninv_label_cluster_genus = {}\n\nembedding_dim = 128\npatience = 5\nepochs = 30\nclusters = [5]\n\nfor cluster_id in clusters:\n    df_cluster = train_cluster_df[train_cluster_df[\"cluster\"] == cluster_id]\n\n    for fam_id in df_cluster[\"familia\"].unique():\n        df_filtrado = df_cluster[df_cluster[\"familia\"] == fam_id]\n        if ((conteos_bajos_sin_cat[\"cluster\"] == cluster_id) &\n            (conteos_bajos_sin_cat[\"familia\"] == fam_id)).any():\n            continue\n        if df_filtrado[\"categoria\"].nunique() <= 1:\n            print(f\"Descartando cluster={cluster_id}, familia={fam_id}: \"\n                  f\"sólo {df_filtrado['categoria'].nunique()} categoría\")\n            continue\n\n        train_ds, class_weights, num_clases, inv_label_map, label_map = get_dataset_por_cluster_familia_(\n            cluster_id, fam_id,\n            train_cluster_df, val_cluster_df,\n            conteos_bajos_filtrados,\n            training=True\n        )\n        val_ds, _, _, _, _ = get_dataset_por_cluster_familia_(\n            cluster_id, fam_id,\n            train_cluster_df, val_cluster_df,\n            conteos_bajos_filtrados,\n            training=False,\n            label_map=label_map\n        )\n\n        label_embeddings = tf.Variable(\n            tf.random.normal((num_clases, embedding_dim)),\n            trainable=True, name=\"label_embeddings\"\n        )\n\n        backbone = tf.keras.applications.EfficientNetV2B3(\n            include_top=False, weights=\"imagenet\", input_shape=(167, 250, 3)\n        )\n        model = adapt_embedding_model_for_categoria(\n            backbone,\n            embedding_dim=embedding_dim,\n            extra_trainable_ratio=0.6\n        )\n\n        # Scheduler con warmup\n        steps_per_epoch = len(train_ds)\n        total_steps = epochs * steps_per_epoch\n        warmup_steps = int(0.1 * total_steps)\n        lr_schedule = CosineDecayWithWarmup(\n            initial_lr=5e-4,\n            warmup_steps=warmup_steps,\n            total_steps=total_steps,\n            alpha=0.01\n        )\n        optimizer = tf.keras.optimizers.Adam(learning_rate=lr_schedule)\n\n        # Warm-up inicial\n        dummy_x = tf.zeros((1, 167, 250, 3))\n        dummy_y = tf.zeros((1,), dtype=tf.int32)\n        with tf.GradientTape() as tape:\n            preds = model(dummy_x, training=True)\n            loss = cosine_embedding_loss(dummy_y, preds, label_embeddings)\n        grads = tape.gradient(loss, model.trainable_variables + [label_embeddings])\n        optimizer.apply_gradients(zip(grads, model.trainable_variables + [label_embeddings]))\n\n        best_val_acc = 0\n        epochs_no_improve = 0\n\n        print(f\"Dataset de entrenamiento: {steps_per_epoch} batches\")\n        print(f\"Dataset de validación: {len(val_ds)} batches\")\n\n        for epoch in range(epochs):\n            # Obtener el LR actual\n            current_lr = tf.keras.backend.get_value(optimizer.learning_rate)\n            print(f\"Epoch {epoch+1}/{epochs}  LR={current_lr:.2e}\")\n\n            # Entrenamiento\n            train_losses, train_accs = [], []\n            for x_batch, y_batch in tqdm(train_ds, desc=\"Training\"):\n                loss, acc = train_step(x_batch, y_batch, model, label_embeddings, optimizer)\n                train_losses.append(loss.numpy())\n                train_accs.append(acc.numpy())\n            avg_train_loss = np.mean(train_losses)\n            avg_train_acc = np.mean(train_accs)\n\n            # Validación\n            val_losses, val_accs = [], []\n            for x_val, y_val in tqdm(val_ds, desc=\"Validation\"):\n                v_loss, v_acc = val_step(x_val, y_val, model, label_embeddings)\n                val_losses.append(v_loss.numpy())\n                val_accs.append(v_acc.numpy())\n            avg_val_loss = np.mean(val_losses)\n            avg_val_acc = np.mean(val_accs)\n\n            print(f\"Train Loss: {avg_train_loss:.4f} | Train Acc: {avg_train_acc:.4f} | \"\n                  f\"Val Loss: {avg_val_loss:.4f} | Val Acc: {avg_val_acc:.4f}\")\n\n            if avg_val_acc > best_val_acc:\n                best_val_acc = avg_val_acc\n                epochs_no_improve = 0\n\n                save_model_path = (\n                    f\"/kaggle/working/modelos/\"\n                    f\"modelo_categoria_cluster_{cluster_id}_familia_{fam_id}.keras\"\n                )\n                model.save(save_model_path)\n                np.save(\n                    f\"/kaggle/working/modelos/\"\n                    f\"embeddings_categoria_cluster_{cluster_id}_familia_{fam_id}.npy\",\n                    label_embeddings.numpy()\n                )\n                with open(\n                    f\"/kaggle/working/modelos/\"\n                    f\"inv_label_map_categoria_cluster_{cluster_id}_familia_{fam_id}.pkl\", \"wb\"\n                ) as f:\n                    pickle.dump(inv_label_map, f)\n                print(f\"Modelo guardado en {save_model_path}\")\n            else:\n                epochs_no_improve += 1\n                if epochs_no_improve >= patience:\n                    print(\"Early stopping\")\n                    break\n\n        # Liberar recursos\n        del model, train_ds, val_ds, label_embeddings\n        tf.keras.backend.clear_session()\n        gc.collect()\n\n        inv_label_cluster_genus[cluster_id] = inv_label_map","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-30T05:59:50.81492Z","iopub.execute_input":"2025-06-30T05:59:50.815109Z","iopub.status.idle":"2025-06-30T09:45:50.601543Z","shell.execute_reply.started":"2025-06-30T05:59:50.815094Z","shell.execute_reply":"2025-06-30T09:45:50.60094Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# PREDICTS","metadata":{}},{"cell_type":"code","source":"def predict_taxonomy_dual(image_path, feature_extractor, scaler, kmeans,\n                          models_familia, models_genero, models_especie,\n                          le_fam, le_gen, le_esp):\n    \n    # --- 1. Cargamos y preprocesamos cada imagen ---\n    img = tf.io.read_file(image_path)\n    img = tf.image.decode_image(img, channels=3)\n    img = tf.image.resize_with_pad(img, target_height=333, target_width=500)\n    img = tf.keras.applications.convnext.preprocess_input(img)\n    img = tf.expand_dims(img, 0)\n\n    # --- 2. extraemos el vector de características y lo escalamos ---\n    features = feature_extractor.predict(img, verbose=0)\n    features_scaled = scaler.transform(features)\n\n    # --- 3. Obtenemos los dos clusteres más probables para seguir dos predicciones ---\n    distances = kmeans.transform(features_scaled)[0]  # Distancia a cada centro\n    top_clusters = distances.argsort()[:2]  # Los dos más cercanos\n\n    predictions = {}\n\n    # --- 4. CLASIFICACIÓN JERÁRQUICA ---\n    for i, cluster_id in enumerate(top_clusters):\n        label = \"primera\" if i == 0 else \"segunda\"\n\n        # --- FAMILIA ---\n        if cluster_id not in models_familia:\n            predictions[label] = {\"familia\": \"UNKNOWN\", \"genero\": \"UNKNOWN\", \"especie\": \"UNKNOWN\"}\n            continue\n\n        model_fam = models_familia[cluster_id]\n        pred_fam_probs = model_fam.predict(img, verbose=0)[0]\n        pred_fam_id = pred_fam_probs.argmax()\n        pred_fam_conf = float(pred_fam_probs[pred_fam_id]) * 100\n        pred_fam = le_fam.inverse_transform([pred_fam_id])[0]\n\n        # --- GÉNERO ---\n        key_gen = (cluster_id, pred_fam_id)\n        if key_gen in models_genero:\n            model_gen = models_genero[key_gen]\n            pred_gen_probs = model_gen.predict(img, verbose=0)[0]\n            pred_gen_id = pred_gen_probs.argmax()\n            pred_gen_conf = float(pred_gen_probs[pred_gen_id]) * 100\n            pred_gen = le_gen.inverse_transform([pred_gen_id])[0]\n        else:\n            pred_gen, pred_gen_conf = \"UNKNOWN\", 0.0\n            pred_gen_id = None\n\n        # --- ESPECIE ---\n        key_esp = (cluster_id, pred_fam_id, pred_gen_id)\n        if key_esp in models_especie:\n            model_esp = models_especie[key_esp]\n            pred_esp_probs = model_esp.predict(img, verbose=0)[0]\n            pred_esp_id = pred_esp_probs.argmax()\n            pred_esp_conf = float(pred_esp_probs[pred_esp_id]) * 100\n            pred_esp = le_esp.inverse_transform([pred_esp_id])[0]\n        else:\n            pred_esp, pred_esp_conf = \"UNKNOWN\", 0.0\n\n        # Guardar predicción\n        predictions[label] = {\n            \"familia\": f\"{pred_fam} ({pred_fam_conf:.2f}%)\",\n            \"genero\": f\"{pred_gen} ({pred_gen_conf:.2f}%)\",\n            \"especie\": f\"{pred_esp} ({pred_esp_conf:.2f}%)\"\n        }\n\n    return predictions","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-30T09:45:50.602469Z","iopub.execute_input":"2025-06-30T09:45:50.602765Z","iopub.status.idle":"2025-06-30T09:45:50.613112Z","shell.execute_reply.started":"2025-06-30T09:45:50.602736Z","shell.execute_reply":"2025-06-30T09:45:50.61233Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def predict_taxonomy_dual_batch(test_ds, feature_extractor, scaler, kmeans,\n                                 models_familia, models_genero, models_especie,\n                                 le_fam, le_gen, le_esp, batch_size=32):\n    \n    all_preds = []\n    all_true = []\n    \n    for batch in tqdm(test_ds.batch(batch_size)):\n        images, labels = batch\n    \n        # Redimensionar y preprocesar imágenes\n        images_resized = tf.image.resize_with_pad(images, target_height=333, target_width=500)\n        imgs_proc = tf.keras.applications.convnext.preprocess_input(images_resized)\n    \n        # Extraer características\n        features = feature_extractor.predict(imgs_proc, verbose=0)\n        features_scaled = scaler.transform(features)\n\n        # CALCULO DE CLUSTERS, LOS DOS MÁS CERCANOS\n        \n        distances = kmeans.transform(features_scaled)  # shape: (batch, n_clusters)\n        top_clusters = np.argsort(distances, axis=1)[:, :2]  # shape: (batch, 2)\n\n        for i in range(len(images)):\n\n            # SE INICIALIZA UN DICCIONARIO PARA ALMACENAR LAS PREDICCIONES\n            # Y SE ALMACENAN LOS CLUSTERS\n            \n            pred_dict = {}\n            image_features = features[i].reshape(1, -1)\n            cluster_pair = top_clusters[i]\n\n            for j, cluster_id in enumerate(cluster_pair):\n                label = \"primera\" if j == 0 else \"segunda\"\n\n                if cluster_id not in models_familia:\n                    pred_dict[label] = {\"familia\": \"UNKNOWN\", \"genero\": \"UNKNOWN\", \"especie\": \"UNKNOWN\"}\n                    continue\n\n                # FAMILIA\n                model_fam = models_familia[cluster_id]\n                pred_fam_probs = model_fam.predict(images[i:i+1], verbose=0)[0]\n                pred_fam_id = pred_fam_probs.argmax()\n                pred_fam = le_fam.inverse_transform([pred_fam_id])[0]\n\n                # GÉNERO\n                key_gen = (cluster_id, pred_fam_id)\n                if key_gen in models_genero:\n                    model_gen = models_genero[key_gen]\n                    pred_gen_probs = model_gen.predict(images[i:i+1], verbose=0)[0]\n                    pred_gen_id = pred_gen_probs.argmax()\n                    pred_gen = le_gen.inverse_transform([pred_gen_id])[0]\n                else:\n                    pred_gen, pred_gen_id = \"UNKNOWN\", None\n\n                # ESPECIE\n                key_esp = (cluster_id, pred_fam_id, pred_gen_id)\n                if key_esp in models_especie:\n                    model_esp = models_especie[key_esp]\n                    pred_esp_probs = model_esp.predict(images[i:i+1], verbose=0)[0]\n                    pred_esp_id = pred_esp_probs.argmax()\n                    pred_esp = le_esp.inverse_transform([pred_esp_id])[0]\n                else:\n                    pred_esp = \"UNKNOWN\"\n\n                pred_dict[label] = {\n                    \"familia\": pred_fam,\n                    \"genero\": pred_gen,\n                    \"especie\": pred_esp\n                }\n\n            # Ground truth para esta imagen\n            true_labels = {\n                \"familia\": le_fam.inverse_transform([labels[\"familia\"][i].numpy()])[0],\n                \"genero\": le_gen.inverse_transform([labels[\"genero\"][i].numpy()])[0],\n                \"especie\": le_esp.inverse_transform([labels[\"especie\"][i].numpy()])[0],\n            }\n\n            all_preds.append(pred_dict)\n            all_true.append(true_labels)\n\n    return all_preds, all_true","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-30T09:45:50.614022Z","iopub.execute_input":"2025-06-30T09:45:50.61424Z","iopub.status.idle":"2025-06-30T09:45:50.637224Z","shell.execute_reply.started":"2025-06-30T09:45:50.614215Z","shell.execute_reply":"2025-06-30T09:45:50.636604Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# EJEMPLO DE EJECUCIÓN\n\nall_preds, all_true = predict_taxonomy_dual_batch(\n    test_ds=test_ds,\n    feature_extractor=feature_extractor,\n    scaler=scaler,\n    kmeans=kmeans,\n    models_familia=models_familia,\n    models_genero=models_genero,\n    models_especie=models_especie,\n    le_fam=le_fam,\n    le_gen=le_gen,\n    le_esp=le_esp,\n    batch_size=32  # puedes ajustar el tamaño del batch según tu memoria disponible\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-30T09:45:50.637956Z","iopub.execute_input":"2025-06-30T09:45:50.638165Z","iopub.status.idle":"2025-06-30T09:45:50.684448Z","shell.execute_reply.started":"2025-06-30T09:45:50.63814Z","shell.execute_reply":"2025-06-30T09:45:50.683566Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def evaluate_predictions(all_preds, all_true, le_fam, le_gen, le_esp):\n    results = {}\n\n    for nivel in [\"familia\", \"genero\", \"especie\"]:\n        y_true = []\n        y_pred_top1 = []\n        y_pred_top2 = []\n\n        for pred, true in zip(all_preds, all_true):\n            true_label = true[nivel]\n            pred1 = pred[\"primera\"][nivel]\n            pred2 = pred[\"segunda\"][nivel]\n\n            y_true.append(true_label)\n            y_pred_top1.append(pred1)\n\n            # En top-2, si el verdadero label aparece en cualquiera, usamos el correcto. Si no, marcamos el top-1 (para F1 y accuracy)\n            if true_label == pred1 or true_label == pred2:\n                y_pred_top2.append(true_label)  # cuenta como acierto\n            else:\n                y_pred_top2.append(pred1)  # sigue siendo una predicción errónea\n\n        # Convertimos a ids para sklearn\n        le = {\"familia\": le_fam, \"genero\": le_gen, \"especie\": le_esp}[nivel]\n        y_true_ids = le.transform(y_true)\n        y_pred_top1_ids = le.transform(y_pred_top1)\n        y_pred_top2_ids = le.transform(y_pred_top2)\n\n        results[nivel] = {\n            \"accuracy_top1\": accuracy_score(y_true_ids, y_pred_top1_ids),\n            \"f1_macro_top1\": f1_score(y_true_ids, y_pred_top1_ids, average='macro'),\n            \"accuracy_top2\": accuracy_score(y_true_ids, y_pred_top2_ids),\n            \"f1_macro_top2\": f1_score(y_true_ids, y_pred_top2_ids, average='macro'),\n        }\n\n    return results","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-30T09:45:50.684923Z","iopub.status.idle":"2025-06-30T09:45:50.685133Z","shell.execute_reply.started":"2025-06-30T09:45:50.685033Z","shell.execute_reply":"2025-06-30T09:45:50.685042Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"results = evaluate_predictions(all_preds, all_true, le_fam, le_gen, le_esp)\n\nfor nivel, métricas in results.items():\n    print(f\"\\nJerarquía: {nivel.upper()}\")\n    for nombre_metrica, valor in métricas.items():\n        print(f\"{nombre_metrica}: {valor:.4f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-30T09:45:50.686278Z","iopub.status.idle":"2025-06-30T09:45:50.68654Z","shell.execute_reply.started":"2025-06-30T09:45:50.68639Z","shell.execute_reply":"2025-06-30T09:45:50.686398Z"}},"outputs":[],"execution_count":null}]}