{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"gpu","dataSources":[{"sourceId":112070,"databundleVersionId":13354591,"sourceType":"competition"},{"sourceId":12693777,"sourceType":"datasetVersion","datasetId":8022148}],"dockerImageVersionId":31090,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"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'):\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","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-08-12T00:42:18.400739Z","iopub.execute_input":"2025-08-12T00:42:18.401075Z","iopub.status.idle":"2025-08-12T00:42:19.164418Z","shell.execute_reply.started":"2025-08-12T00:42:18.401046Z","shell.execute_reply":"2025-08-12T00:42:19.163502Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os, random\nimport numpy as np\nimport pandas as pd\nfrom pathlib import Path\nimport tensorflow as tf\nfrom tensorflow.keras import layers, models\nfrom tensorflow.keras.callbacks import EarlyStopping, ReduceLROnPlateau, ModelCheckpoint\n\n# ========= Config =========\nSEED = 42\nIMG_SIZE = 224\nBATCH_SIZE = 32\nEPOCHS_BASE = 12\nEPOCHS_FT = 6\nVAL_SPLIT = 0.2\n\ntf.random.set_seed(SEED)\nnp.random.seed(SEED)\nrandom.seed(SEED)\n\n# ========= CREAR SUBMISSION DUMMY AL INICIO =========\n# Esto garantiza que siempre exista un archivo submission.csv\nprint(\"🔧 Creando submission.csv inicial...\")\ntry:\n    # Buscar sample_submission para obtener formato\n    sample_paths = list(Path(\"/kaggle/input\").rglob(\"sample_submission.csv\"))\n    if sample_paths:\n        sample_sub = pd.read_csv(sample_paths[0])\n        print(f\"✅ Sample submission encontrado: {sample_paths[0]}\")\n        # Crear copia inicial\n        initial_sub = sample_sub.copy()\n        # Poner valores dummy\n        label_col = sample_sub.columns[1] if len(sample_sub.columns) > 1 else 'label'\n        initial_sub[label_col] = \"placeholder\"\n    else:\n        print(\"⚠️ No se encontró sample_submission.csv, creando formato estándar\")\n        # Crear submission estándar de 50 filas\n        initial_sub = pd.DataFrame({\n            'id': range(50),\n            'label': ['placeholder'] * 50\n        })\n    \n    # Guardar submission inicial\n    initial_sub.to_csv(\"/kaggle/working/submission.csv\", index=False)\n    print(f\"✅ Submission inicial creado: {initial_sub.shape}\")\n    print(f\"📊 Columnas: {list(initial_sub.columns)}\")\nexcept Exception as e:\n    print(f\"❌ Error creando submission inicial: {e}\")\n    # Submission de emergencia\n    pd.DataFrame({'id': range(50), 'label': ['class0'] * 50}).to_csv(\"/kaggle/working/submission.csv\", index=False)\n    print(\"✅ Submission de emergencia creado\")\n\n# ========= Funciones auxiliares =========\ndef has_imgs(p: Path) -> bool:\n    if not p.exists() or not p.is_dir(): \n        return False\n    for ext in (\"*.jpg\", \"*.jpeg\", \"*.png\", \"*.bmp\", \"*.webp\"):\n        if list(p.rglob(ext)): \n            return True\n    return False\n\ndef unwrap(p: Path) -> Path:\n    if p.exists() and p.is_dir():\n        subs = [s for s in p.iterdir() if s.is_dir()]\n        if len(subs) == 1:\n            inner = subs[0]\n            if sum(1 for s in inner.iterdir() if s.is_dir()) >= 2:\n                return inner\n    return p\n\ndef find_train_test_dirs():\n    candidates_train = [\n        Path(\"/kaggle/input/fotosgrupo/Fotos Grupo\"),\n        Path(\"/kaggle/input/fotosgrupo/Fotos_Grupo\"),\n        Path(\"/kaggle/input/fotosgrupo/fotos_grupo\"),\n        Path(\"/kaggle/input/fotosgrupo\"),\n    ]\n    candidates_test = [\n        Path(\"/kaggle/input/facial-recognition-7th-gen/Fotos Grupo\"),\n        Path(\"/kaggle/input/facial-recognition-7th-gen/Fotos_Grupo\"),\n        Path(\"/kaggle/input/facial-recognition-7th-gen/fotos_grupo\"),\n        Path(\"/kaggle/input/facial-recognition-7th-gen\"),\n    ]\n    train_dir = None\n    for ct in candidates_train:\n        ct = unwrap(ct)\n        if has_imgs(ct): \n            train_dir = ct\n            break\n    test_dir = None\n    for ct in candidates_test:\n        ct = unwrap(ct)\n        if has_imgs(ct): \n            test_dir = ct\n            break\n    return train_dir, test_dir\n\ndef gather_images(folder: Path):\n    files = []\n    for ext in (\"*.jpg\", \"*.jpeg\", \"*.png\", \"*.bmp\", \"*.webp\"):\n        files += list(folder.rglob(ext))\n    return sorted(files)\n\n# ========= Detección de rutas =========\ntry:\n    TRAIN_DIR, TEST_DIR = find_train_test_dirs()\n    \n    if TRAIN_DIR is None:\n        raise RuntimeError(\"No se encontró carpeta de entrenamiento\")\n    \n    print(f\"\\n📁 Directorio de entrenamiento: {TRAIN_DIR}\")\n    print(f\"📁 Directorio de test: {TEST_DIR if TEST_DIR else 'No encontrado'}\")\n    \n    # ========= Datasets =========\n    train_ds = tf.keras.utils.image_dataset_from_directory(\n        TRAIN_DIR, \n        labels=\"inferred\", \n        label_mode=\"categorical\",\n        validation_split=VAL_SPLIT, \n        subset=\"training\", \n        seed=SEED,\n        image_size=(IMG_SIZE, IMG_SIZE), \n        batch_size=BATCH_SIZE\n    )\n    \n    val_ds = tf.keras.utils.image_dataset_from_directory(\n        TRAIN_DIR, \n        labels=\"inferred\", \n        label_mode=\"categorical\",\n        validation_split=VAL_SPLIT, \n        subset=\"validation\", \n        seed=SEED,\n        image_size=(IMG_SIZE, IMG_SIZE), \n        batch_size=BATCH_SIZE\n    )\n    \n    class_names = train_ds.class_names\n    num_classes = len(class_names)\n    print(f\"📊 Número de clases: {num_classes}\")\n    print(f\"📊 Clases: {class_names[:10]}...\")  # Mostrar solo las primeras 10\n    \n    AUTOTUNE = tf.data.AUTOTUNE\n    train_ds = train_ds.shuffle(1024).prefetch(AUTOTUNE)\n    val_ds = val_ds.prefetch(AUTOTUNE)\n    \n    # ========= Modelo =========\n    data_augmentation = tf.keras.Sequential([\n        layers.RandomFlip(\"horizontal\"),\n        layers.RandomRotation(0.12),\n        layers.RandomZoom(0.15),\n        layers.RandomContrast(0.15),\n    ])\n    \n    base = tf.keras.applications.MobileNetV2(\n        include_top=False, \n        weights=\"imagenet\",\n        input_shape=(IMG_SIZE, IMG_SIZE, 3), \n        pooling=\"avg\"\n    )\n    base.trainable = False\n    \n    inp = layers.Input((IMG_SIZE, IMG_SIZE, 3))\n    x = tf.keras.applications.mobilenet_v2.preprocess_input(inp)\n    x = data_augmentation(x)\n    x = base(x, training=False)\n    x = layers.Dropout(0.4)(x)\n    out = layers.Dense(num_classes, activation=\"softmax\")(x)\n    \n    model = models.Model(inp, out)\n    \n    model.compile(\n        optimizer=tf.keras.optimizers.Adam(1e-3),\n        loss=\"categorical_crossentropy\", \n        metrics=[\"accuracy\"]\n    )\n    \n    print(f\"📈 Total de parámetros: {model.count_params():,}\")\n    \n    # ========= Entrenamiento =========\n    ckpt = \"mobilenetv2_best.keras\"\n    cbs = [\n        ModelCheckpoint(ckpt, monitor=\"val_accuracy\", save_best_only=True, mode=\"max\"),\n        EarlyStopping(monitor=\"val_accuracy\", patience=5, restore_best_weights=True),\n        ReduceLROnPlateau(monitor=\"val_loss\", factor=0.5, patience=2, verbose=1),\n    ]\n    \n    print(\"\\n🚀 Fase 1: Entrenamiento inicial\")\n    model.fit(train_ds, validation_data=val_ds, epochs=EPOCHS_BASE, callbacks=cbs, verbose=1)\n    \n    # Fine-tuning\n    print(\"\\n🚀 Fase 2: Fine-tuning\")\n    base.trainable = True\n    for layer in base.layers[:-30]:\n        layer.trainable = False\n    \n    model.compile(\n        optimizer=tf.keras.optimizers.Adam(1e-4),\n        loss=\"categorical_crossentropy\", \n        metrics=[\"accuracy\"]\n    )\n    \n    model.fit(train_ds, validation_data=val_ds, epochs=EPOCHS_FT, callbacks=cbs, verbose=1)\n    \n    # ========= Actualizar Submission con Predicciones Reales =========\n    print(\"\\n📝 Actualizando submission con predicciones reales...\")\n    \n    # Leer el submission inicial\n    submission = pd.read_csv(\"/kaggle/working/submission.csv\")\n    \n    if TEST_DIR and TEST_DIR.exists():\n        test_images = gather_images(TEST_DIR)\n        print(f\"📷 Imágenes de test encontradas: {len(test_images)}\")\n        \n        # Crear diccionario de predicciones\n        name_to_pred = {}\n        \n        # Predecir por lotes\n        def build_infer_ds(paths):\n            ds = tf.data.Dataset.from_tensor_slices([str(p) for p in paths])\n            def _load(p):\n                img = tf.io.read_file(p)\n                img = tf.io.decode_image(img, channels=3, expand_animations=False)\n                img = tf.image.resize(img, (IMG_SIZE, IMG_SIZE))\n                img = tf.keras.applications.mobilenet_v2.preprocess_input(tf.cast(img, tf.float32))\n                return img\n            return ds.map(_load, num_parallel_calls=AUTOTUNE).batch(BATCH_SIZE).prefetch(AUTOTUNE)\n        \n        if test_images:\n            infer_ds = build_infer_ds(test_images)\n            predictions = model.predict(infer_ds, verbose=1)\n            \n            # Mapear predicciones\n            for img_path, pred in zip(test_images, predictions):\n                pred_class = class_names[np.argmax(pred)]\n                name_to_pred[img_path.name] = pred_class\n                name_to_pred[img_path.stem] = pred_class\n        \n        # Actualizar submission\n        id_col = submission.columns[0]\n        label_col = submission.columns[1] if len(submission.columns) > 1 else 'label'\n        \n        new_labels = []\n        for idx, row in submission.iterrows():\n            id_value = str(row[id_col])\n            \n            # Buscar predicción\n            pred = None\n            if id_value in name_to_pred:\n                pred = name_to_pred[id_value]\n            elif f\"{id_value}.jpg\" in name_to_pred:\n                pred = name_to_pred[f\"{id_value}.jpg\"]\n            elif f\"{id_value}.jpeg\" in name_to_pred:\n                pred = name_to_pred[f\"{id_value}.jpeg\"]\n            elif f\"{id_value}.png\" in name_to_pred:\n                pred = name_to_pred[f\"{id_value}.png\"]\n            else:\n                # Buscar coincidencia parcial\n                for key in name_to_pred:\n                    if id_value in key or key in id_value:\n                        pred = name_to_pred[key]\n                        break\n            \n            # Si no se encuentra, usar la primera clase\n            if pred is None:\n                pred = class_names[0] if class_names else \"class0\"\n            \n            new_labels.append(pred)\n        \n        submission[label_col] = new_labels\n    else:\n        # Sin imágenes de test, usar predicciones dummy\n        label_col = submission.columns[1] if len(submission.columns) > 1 else 'label'\n        submission[label_col] = class_names[0] if class_names else \"class0\"\n    \n    # Guardar submission actualizado\n    submission.to_csv(\"/kaggle/working/submission.csv\", index=False)\n    print(f\"✅ Submission actualizado: {submission.shape}\")\n    print(\"📊 Primeras 5 filas:\")\n    print(submission.head())\n    \nexcept Exception as e:\n    print(f\"\\n❌ Error durante el entrenamiento: {e}\")\n    print(\"⚠️ Manteniendo submission inicial con valores placeholder\")\n\n# ========= VERIFICACIÓN FINAL =========\nprint(\"\\n\" + \"=\"*50)\nprint(\"📋 VERIFICACIÓN FINAL\")\nprint(\"=\"*50)\n\nif os.path.exists(\"/kaggle/working/submission.csv\"):\n    final_sub = pd.read_csv(\"/kaggle/working/submission.csv\")\n    print(f\"✅ submission.csv existe en /kaggle/working/\")\n    print(f\"📊 Forma: {final_sub.shape}\")\n    print(f\"📊 Columnas: {list(final_sub.columns)}\")\n    print(f\"📊 Valores únicos en columna de predicción: {final_sub.iloc[:, 1].nunique()}\")\n    print(\"\\n📄 Contenido del submission:\")\n    print(final_sub.head(10))\nelse:\n    print(\"❌ ERROR CRÍTICO: submission.csv no existe\")\n    print(\"Creando submission de emergencia...\")\n    emergency_sub = pd.DataFrame({\n        'id': range(50),\n        'label': ['emergency'] * 50\n    })\n    emergency_sub.to_csv(\"/kaggle/working/submission.csv\", index=False)\n    print(\"✅ Submission de emergencia creado\")\n\nprint(\"\\n🎉 Script completado!\")","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}