{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"gpu","dataSources":[{"sourceType":"competition","sourceId":112509,"databundleVersionId":14254895},{"sourceType":"modelInstanceVersion","sourceId":2652,"databundleVersionId":4910549,"modelInstanceId":1918,"modelId":257},{"sourceType":"modelInstanceVersion","sourceId":6123,"databundleVersionId":7429407,"modelInstanceId":4606,"modelId":2797}],"dockerImageVersionId":31329,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# ============================================================\n# SOLUCIÓN 2 - EfficientNetB3 + Ridge Regression (LOCAL)\n# Incluye: Preprocesamiento, Aumento de Datos,\n#          Extracción de Características, Regresión,\n#          Validación y Calibración\n# ============================================================\n\n# Para manipulación de datos\nimport os\nimport joblib\nimport pandas as pd\nimport numpy as np\nfrom tqdm import tqdm\n\n# Para el aumento de datos\nimport albumentations as A\nimport cv2\n\n# Para el modelo de aprendizaje automático\nfrom sklearn.linear_model import RidgeCV\nfrom sklearn.metrics import mean_squared_error, r2_score\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.preprocessing import StandardScaler\n\n# Para visión por computador\nimport tensorflow as tf\nimport keras_cv\n\n\n# ===============================\n# RUTAS\n# ===============================\n\nBASE_PATH = \"/kaggle/input/competitions/csiro-biomass\"   # carpeta raíz del dataset local\n\nTRAIN_CSV_PATH = os.path.join(BASE_PATH, \"train.csv\")\nTEST_CSV_PATH  = os.path.join(BASE_PATH, \"test.csv\")\n\nTRAIN_IMG_DIR  = BASE_PATH\nTEST_IMG_DIR   = BASE_PATH\n\nSAVE_DIR = \"saved_model_efficientnet\"\nos.makedirs(SAVE_DIR, exist_ok=True)\n\n\n# ===============================\n# CONFIGURACIÓN GENERAL\n# ===============================\n\nIMG_SIZE    = 300       # Tamaño de entrada esperado por EfficientNetB3\nRANDOM_SEED = 999       # Semilla para reproducibilidad\nVAL_SIZE    = 0.2       # 20 % de los datos se reservan para validación\n\nBATCH_SIZE      = 32    # Número de imágenes procesadas simultáneamente en GPU\n# Nota: NO se aplica augmentación ni en entrenamiento ni en test para Ridge.\n# La augmentación provoca data leakage en LOO-CV y no aporta beneficio real\n# a un modelo lineal con extractor de características congelado.\n\n# Valores de alpha que RidgeCV evaluará internamente mediante cross-validation.\n# RidgeCV probará cada uno y seleccionará automáticamente el que minimice\n# el error de validación, sin necesidad de un loop manual de folds.\nALPHA_OPTIONS   = [0.01, 0.1, 1.0, 10.0, 100.0, 1000.0]\n\n\n# ===============================\n# CARGA DE DATOS\n# ===============================\n\ntrain_df = pd.read_csv(TRAIN_CSV_PATH)\ntest_df  = pd.read_csv(TEST_CSV_PATH)\n\n# Convertir de formato largo a ancho para tener una fila por imagen\n# Cada columna corresponde a una variable objetivo (Dry_Clover_g, Dry_Dead_g, etc.)\ntrain_wide_df = train_df.pivot(\n    index=\"image_path\",\n    columns=\"target_name\",\n    values=\"target\"\n).reset_index()\n\nTARGET_COLS = [c for c in train_wide_df.columns if c != \"image_path\"]\n\nprint(\"Targets:\", TARGET_COLS)\nprint(f\"Imágenes de train: {len(train_wide_df)}\")\nprint(f\"Imágenes de test:  {len(test_df['image_path'].unique())}\")\n\n\n# ===============================\n# MÓDULO DE AUMENTO DE DATOS\n# ===============================\n# Se definen transformaciones geométricas y de color sencillas que se\n# aplican aleatoriamente a cada imagen SOLO durante el entrenamiento.\n# El objetivo es aumentar artificialmente la variabilidad del conjunto\n# de entrenamiento para que el modelo generalice mejor.\n\ndef get_train_transforms():\n    \"\"\"\n    Devuelve un pipeline de albumentations con transformaciones simples:\n      - HorizontalFlip : volteo horizontal con probabilidad 0.5\n      - VerticalFlip   : volteo vertical con probabilidad 0.5\n      - RandomRotate90 : rotación aleatoria de 90° con probabilidad 0.5\n      - RandomBrightnessContrast : variación leve de brillo y contraste\n    \"\"\"\n    return A.Compose([\n        A.HorizontalFlip(p=0.5),\n        A.VerticalFlip(p=0.5),\n        A.RandomRotate90(p=0.5),\n        A.RandomBrightnessContrast(\n            brightness_limit=0.2,\n            contrast_limit=0.2,\n            p=0.5\n        ),\n        A.Resize(IMG_SIZE, IMG_SIZE),\n    ], seed=RANDOM_SEED)\n\n\n# Instanciar el pipeline de aumento una sola vez\ntrain_augment = get_train_transforms()\n\n\n# ===============================\n# CARGA DEL MODELO EfficientNetV2B3\n# ===============================\n# Se carga EfficientNetV2B3 usando keras_cv con pesos preentrenados en ImageNet.\n# from_preset descarga y construye automáticamente la arquitectura completa,\n# incluyendo el rescaling interno (include_rescaling=True por defecto), por lo\n# que las imágenes deben pasarse en rango [0, 255] y NO dividirse entre 255.\n#\n# La salida del backbone es un mapa de características espacial (H×W×C).\n# GlobalAveragePooling2D promedia ese mapa sobre las dimensiones espaciales\n# y produce un vector 1-D de 1536 dimensiones por imagen, que es lo que\n# se pasa como entrada al modelo de Ridge.\n\nbase_net = keras_cv.models.EfficientNetV2Backbone.from_preset(\"efficientnetv2_b3\")\nbase_net.trainable = False   # Los pesos preentrenados NO se actualizan\n\n# Construir el extractor: backbone + pooling global\ninputs  = tf.keras.Input(shape=(IMG_SIZE, IMG_SIZE, 3))\nx       = base_net(inputs, training=False)\noutputs = tf.keras.layers.GlobalAveragePooling2D()(x)\n\nfeature_extractor = tf.keras.Model(inputs=inputs, outputs=outputs)\n\nprint(f\"Modelo cargado. Forma del vector de características: {feature_extractor.output_shape}\")\n# Debería imprimir: (None, 1536)\n\n\n# ===============================\n# FUNCIONES DE CARGA Y EXTRACCIÓN\n# ===============================\n\ndef load_image_as_array(img_path):\n    \"\"\"\n    Carga una imagen desde disco con OpenCV y la convierte a RGB.\n    \"\"\"\n    img = cv2.imread(img_path)\n    img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n    img = cv2.resize(img, (IMG_SIZE, IMG_SIZE))\n    return img\n\n\ndef extract_features_batch(img_arrays):\n    \"\"\"\n    Recibe una lista de arrays HxWx3 uint8 y extrae sus vectores de\n    características en un único forward pass por la GPU.\n\n    Procesar en batches en lugar de imagen por imagen es clave para\n    aprovechar el paralelismo de la GPU: en lugar de hacer N llamadas\n    con batch_size=1, se hace una sola llamada con batch_size=N,\n    lo que reduce drásticamente el tiempo de inferencia.\n\n    Parámetros\n    ----------\n    img_arrays : list of np.ndarray\n        Lista de imágenes en formato HxWx3 uint8.\n\n    Retorna\n    -------\n    numpy.ndarray\n        Matriz de shape (N, 1536) con un vector de características por imagen.\n    \"\"\"\n    # Apilar todas las imágenes en un único tensor (N, 300, 300, 3)\n    # keras_cv con from_preset maneja el rescaling internamente,\n    # por lo tanto las imágenes se pasan en rango [0, 255] sin dividir entre 255.\n    batch = np.stack([img.astype(np.float32) for img in img_arrays], axis=0)\n    features = feature_extractor(batch, training=False)\n    return features.numpy()   # shape: (N, 1536)\n\n\n# ===============================\n# DIVISIÓN ENTRENAMIENTO / VALIDACIÓN\n# ===============================\n# Se separa el 20 % de las imágenes como conjunto de validación ANTES\n# de extraer características, para evitar cualquier fuga de información.\n\ntrain_split_df, val_split_df = train_test_split(\n    train_wide_df,\n    test_size=VAL_SIZE,\n    random_state=RANDOM_SEED\n)\n\nprint(f\"Imágenes de entrenamiento : {len(train_split_df)}\")\nprint(f\"Imágenes de validación    : {len(val_split_df)}\")\n\n\n# ===============================\n# EXTRACCIÓN DE CARACTERÍSTICAS - ENTRENAMIENTO\n# ===============================\n# Se extrae un único vector de características por imagen original, SIN augmentación.\n#\n# ¿Por qué no se augmenta en entrenamiento para Ridge?\n#   Aplicar augmentación genera múltiples copias de cada imagen con la misma\n#   etiqueta. Cuando LOO-CV deja fuera una muestra para validarla, todas sus\n#   copias aumentadas siguen en el entrenamiento, provocando data leakage:\n#   el modelo \"ya conoce\" esa imagen en otras formas, por lo que LOO-CV\n#   selecciona un alpha artificialmente bajo (poca regularización) y el modelo\n#   no generaliza bien a imágenes completamente nuevas.\n#   La augmentación tiene sentido para entrenar redes end-to-end, no para\n#   un extractor congelado + modelo lineal como este.\n\nprint(\"\\nExtrayendo features de TRAIN...\")\n\ntrain_imgs, train_labels = [], []\n\nfor _, row in tqdm(train_split_df.iterrows(), total=len(train_split_df)):\n    full_path = os.path.join(TRAIN_IMG_DIR, row[\"image_path\"])\n    img_array = load_image_as_array(full_path)\n    train_imgs.append(img_array)\n    train_labels.append(row[TARGET_COLS].values.astype(np.float32))\n\n# Procesar en batches para aprovechar la GPU\nprint(f\"Procesando {len(train_imgs)} imágenes en batches de {BATCH_SIZE}...\")\ntrain_features = []\n\nfor i in tqdm(range(0, len(train_imgs), BATCH_SIZE)):\n    batch_feats = extract_features_batch(train_imgs[i : i + BATCH_SIZE])\n    train_features.append(batch_feats)\n\ntrain_features = np.vstack(train_features)   # shape: (285, 1536)\ntrain_labels   = np.array(train_labels)\n\nprint(f\"Forma del vector de características: {train_features.shape}\")\n\n\n# ===============================\n# EXTRACCIÓN DE CARACTERÍSTICAS - VALIDACIÓN\n# ===============================\n# Para validación NO se aplica aumento de datos: se evalúa el modelo\n# en imágenes tal cual llegan, sin modificaciones artificiales.\n\nprint(\"\\nExtrayendo features del conjunto de VALIDACIÓN...\")\n\nval_imgs, val_labels = [], []\n\nfor _, row in tqdm(val_split_df.iterrows(), total=len(val_split_df)):\n    full_path = os.path.join(TRAIN_IMG_DIR, row[\"image_path\"])\n    img_array = load_image_as_array(full_path)\n    label     = row[TARGET_COLS].values.astype(np.float32)\n\n    val_imgs.append(img_array)\n    val_labels.append(label)\n\n# Procesar validación en batches (sin aumento de datos)\nval_features = []\nfor i in tqdm(range(0, len(val_imgs), BATCH_SIZE)):\n    batch_feats = extract_features_batch(val_imgs[i : i + BATCH_SIZE])\n    val_features.append(batch_feats)\n\nval_features = np.vstack(val_features)\nval_labels   = np.array(val_labels)\n\nprint(f\"Matriz de características de validación: {val_features.shape}\")\n\n\n# ===============================\n# NORMALIZACIÓN\n# ===============================\n# Ridge Regression es sensible a la escala de los datos.\n# StandardScaler estandariza cada característica a media=0 y desviación=1.\n# IMPORTANTE: el scaler se ajusta SOLO con los datos de entrenamiento y\n# luego se aplica a validación y test, nunca al revés.\n\nscaler  = StandardScaler()\nX_train = scaler.fit_transform(train_features)   # ajusta y transforma\nX_val   = scaler.transform(val_features)          # solo transforma\ny_train = train_labels\ny_val   = val_labels\n\n\n# ===============================\n# EXTRACCIÓN DE CARACTERÍSTICAS - TEST\n# ===============================\n# Al igual que en validación, NO se aplica aumento de datos en test.\n# Se extrae un único vector de características por imagen original.\n\nunique_paths = test_df[\"image_path\"].unique()\nprint(f\"\\nExtrayendo features de TEST...\")\n\ntest_imgs = []\nfor img_path in tqdm(unique_paths):\n    full_path = os.path.join(TEST_IMG_DIR, img_path)\n    test_imgs.append(load_image_as_array(full_path))\n\n# Procesar en batches para aprovechar la GPU\ntest_features = []\nfor i in tqdm(range(0, len(test_imgs), BATCH_SIZE)):\n    batch_feats = extract_features_batch(test_imgs[i : i + BATCH_SIZE])\n    test_features.append(batch_feats)\n\ntest_features = np.vstack(test_features)   # shape: (n_test, 1536)\nX_test        = scaler.transform(test_features)\n\n\n# ===============================\n# ENTRENAMIENTO DEL MODELO - RidgeCV\n# ===============================\n# RidgeCV es una versión de Ridge que incorpora la selección automática\n# del parámetro de regularización alpha mediante cross-validation interna.\n#\n# ¿Por qué RidgeCV en lugar de Ridge + loop manual de folds?\n#   - Con Ridge normal habría que iterar manualmente sobre cada valor de\n#     alpha, entrenar un modelo por cada combinación (fold × alpha × target)\n#     y elegir el mejor. Eso implica mucho código repetitivo y propenso\n#     a errores.\n#   - RidgeCV hace exactamente eso de forma interna y eficiente: prueba\n#     todos los alphas de ALPHA_OPTIONS usando Leave-One-Out CV (por defecto)\n#     o KFold si se especifica cv=, y guarda automáticamente el mejor alpha\n#     en el atributo .alpha_ de cada modelo entrenado.\n#   - El resultado es el mismo, pero con mucho menos código.\n#\n# Parámetros clave de RidgeCV:\n#   - alphas      : lista de valores de regularización a evaluar.\n#                   Un alpha alto → más regularización → modelo más simple.\n#                   Un alpha bajo → menos regularización → puede sobreajustar.\n#   - cv          : número de folds para la validación cruzada interna.\n#                   Si cv=None usa Leave-One-Out (eficiente para datasets pequeños).\n#   - scoring     : métrica usada para comparar alphas. \"neg_mean_squared_error\"\n#                   equivale a minimizar el MSE, que es lo que buscamos.\n#   - store_cv_results : si es True, guarda los errores de cada alpha para\n#                   poder inspeccionarlos después con .cv_results_.\n\nmodels     = {}\ntest_preds = np.zeros((X_test.shape[0], len(TARGET_COLS)))\n\nprint(f\"\\nEntrenando RidgeCV...\")\nprint(f\"Alphas a evaluar: {ALPHA_OPTIONS}\\n\")\n\nfor t_idx, target in enumerate(TARGET_COLS):\n\n    # RidgeCV con cv=None usa Leave-One-Out CV (LOO) de forma eficiente.\n    #\n    # ¿Por qué LOO en lugar de KFold?\n    #   - Con cv=5 (KFold), el dataset se divide en 5 partes: 4 para entrenar\n    #     y 1 para validar, repitiendo 5 veces. Con pocos datos (3135 muestras)\n    #     cada fold de validación tiene solo ~627 muestras, lo que puede dar\n    #     una estimación ruidosa del error.\n    #   - Con cv=None (LOO), cada muestra actúa una vez como validación mientras\n    #     el resto entrena. Es la estimación más precisa posible del error, y\n    #     sklearn la implementa de forma algebraica sin reentrenar N veces,\n    #     por lo que es igual de rápida que KFold.\n    #\n    # Nota importante sobre scoring:\n    #   - Cuando cv=None (LOO), el parámetro scoring se IGNORA internamente.\n    #     RidgeCV siempre usa MSE para comparar alphas en modo LOO, sin importar\n    #     qué se le pase en scoring. Por eso no se especifica scoring aquí.\n    #   - Cuando cv=5 (KFold), scoring por defecto es R², no MSE. En ese caso\n    #     sí habría que pasar scoring=\"neg_mean_squared_error\" para ser consistente.\n    model_cv = RidgeCV(\n        alphas=ALPHA_OPTIONS,   # valores de alpha que LOO-CV comparará internamente\n        cv=None,                # None → Leave-One-Out CV eficiente (recomendado para datasets pequeños)\n        fit_intercept=True      # incluir término independiente en la regresión\n    )\n\n    # Se entrena sobre el conjunto completo de entrenamiento (ya con augmentación).\n    # RidgeCV selecciona el mejor alpha automáticamente durante este fit.\n    model_cv.fit(X_train, y_train[:, t_idx])\n\n    # .alpha_ contiene el mejor valor de alpha encontrado por LOO-CV\n    print(f\"  {target:<20} → mejor alpha seleccionado por LOO-CV: {model_cv.alpha_}\")\n\n    # Guardar el modelo entrenado y generar predicciones en test\n    models[target]         = model_cv\n    test_preds[:, t_idx]   = model_cv.predict(X_test)\n\nprint(\"\\nEntrenamiento completado.\")\n\n\n# ============================================================\n# MÓDULO DE VALIDACIÓN Y CALIBRACIÓN\n# ============================================================\n# Se evalúa el rendimiento del modelo sobre el 20 % de imágenes\n# reservadas al inicio como conjunto de validación.\n#\n# Estas imágenes NO participaron en el entrenamiento ni en la\n# selección interna de alpha de RidgeCV, por lo que las métricas\n# son una estimación honesta del rendimiento del modelo.\n#\n# Métricas utilizadas:\n#   - MSE  (Mean Squared Error / Error Cuadrático Medio):\n#       Promedio de los errores al cuadrado. Penaliza fuertemente\n#       los errores grandes. Unidad: (g/m²)²\n#\n#   - RMSE (Root Mean Squared Error / Raíz del ECM):\n#       Raíz cuadrada del MSE. Está en la misma unidad que la variable\n#       objetivo (g/m²), por lo que es más interpretable que el MSE.\n#\n#   - R²  (Coeficiente de Determinación):\n#       Indica qué proporción de la varianza de los datos es explicada\n#       por el modelo. Va de 0 a 1; un valor cercano a 1 indica un\n#       mejor ajuste.\n# ============================================================\n\nprint(\"\\n\" + \"=\" * 60)\nprint(\"MÓDULO DE VALIDACIÓN Y CALIBRACIÓN\")\nprint(\"=\" * 60)\n\nresults = []\n\nfor t_idx, col in enumerate(TARGET_COLS):\n\n    # Valores reales del conjunto de validación para esta variable\n    y_true_col = y_val[:, t_idx]\n\n    # El modelo de RidgeCV ya entrenado predice sobre el conjunto de validación\n    y_pred_col = models[col].predict(X_val)\n\n    # --- MSE: promedio de (real - predicho)² ---\n    mse  = mean_squared_error(y_true_col, y_pred_col)\n\n    # --- RMSE: raíz del MSE, misma unidad que la biomasa (g/m²) ---\n    rmse = np.sqrt(mse)\n\n    # --- R²: proporción de varianza explicada por el modelo ---\n    r2   = r2_score(y_true_col, y_pred_col)\n\n    results.append({\"Variable objetivo\": col, \"MSE\": mse, \"RMSE\": rmse, \"R²\": r2})\n    print(f\"  {col:<20}  MSE={mse:>10.4f}  RMSE={rmse:>8.4f} g/m²  R²={r2:>6.4f}\")\n\nresults_df = pd.DataFrame(results)\nprint(\"-\" * 60)\nprint(f\"  {'PROMEDIO GLOBAL':<20}  MSE={results_df['MSE'].mean():>10.4f}  \"\n      f\"RMSE={results_df['RMSE'].mean():>8.4f} g/m²  R²={results_df['R²'].mean():>6.4f}\")\nprint(\"=\" * 60 + \"\\n\")\n\n\n# ===============================\n# PREDICCIÓN Y ENVÍO\n# ===============================\n\n# Clamp a 0 para evitar predicciones negativas (biomasa no puede ser negativa)\ntest_preds = np.clip(test_preds, 0, None)\n\n# Armar DataFrame ancho con las predicciones\npredictions_df = pd.DataFrame(test_preds, columns=TARGET_COLS)\npredictions_df[\"image_path\"] = unique_paths\n\n# Convertir de formato ancho a largo (long format requerido para la entrega)\npredictions_long_df = predictions_df.melt(\n    id_vars=[\"image_path\"],\n    value_vars=TARGET_COLS,\n    var_name=\"target_name\",\n    value_name=\"target\"\n)\n\n# Unir con test_df para recuperar el sample_id original\nsubmission_df = pd.merge(\n    test_df[[\"sample_id\", \"image_path\", \"target_name\"]],\n    predictions_long_df,\n    on=[\"image_path\", \"target_name\"]\n)[[\"sample_id\", \"target\"]]\n\nsubmission_df.to_csv(\"submission.csv\", index=False)\nprint(\"Archivo de entrega creado: submission.csv\")\nprint(submission_df.head())\n\n\n# ===============================\n# GUARDAR MODELO Y ARTEFACTOS\n# ===============================\n\n# Guardar el diccionario de modelos RidgeCV (un modelo por target)\njoblib.dump(models, os.path.join(SAVE_DIR, \"ridge_models.pkl\"))\n\n# Guardar el scaler para poder transformar nuevas imágenes en inferencia\njoblib.dump(scaler, os.path.join(SAVE_DIR, \"scaler.pkl\"))\n\n# Guardar los nombres de los targets\njoblib.dump(TARGET_COLS, os.path.join(SAVE_DIR, \"target_cols.pkl\"))\n\n# Guardar los features de entrenamiento (opcional, para análisis futuros)\nnp.save(os.path.join(SAVE_DIR, \"train_features.npy\"), train_features)\n\nprint(f\"Modelo y artefactos guardados en: {SAVE_DIR}\")","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2026-04-25T19:57:19.305409Z","iopub.execute_input":"2026-04-25T19:57:19.306160Z","iopub.status.idle":"2026-04-25T19:57:49.529865Z","shell.execute_reply.started":"2026-04-25T19:57:19.306112Z","shell.execute_reply":"2026-04-25T19:57:49.526687Z"}},"outputs":[],"execution_count":null}]}