{"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\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 Ridge\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# Alpha controla la fuerza de regularización L2 de Ridge.\n# Un valor alto penaliza más los coeficientes grandes → modelo más simple.\nALPHA = 10\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# Esta etapa no ayuda a mejorar el score usando Ridge\n\n# def 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\n# train_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 - Ridge\n# ===============================\n# Ridge Regression minimiza: ||y - Xw||² + alpha * ||w||²\n# El término alpha * ||w||² penaliza coeficientes grandes, evitando\n# sobreajuste cuando hay muchas características (1536) y pocas muestras (285).\n#\n# Se usa un modelo por variable objetivo porque Ridge con y 2D entrena\n# internamente un regresor independiente por columna de todas formas,\n# pero el loop hace explícita esa separación para facilitar el análisis\n# individual de cada componente de biomasa.\n\nmodels     = {}\ntest_preds = np.zeros((X_test.shape[0], len(TARGET_COLS)))\n\nprint(f\"\\nEntrenando Ridge (alpha={ALPHA})...\")\n\nfor t_idx, target in enumerate(TARGET_COLS):\n\n    model_r = Ridge(\n        alpha=ALPHA,         # regularización L2: penaliza coeficientes grandes\n        fit_intercept=True   # incluir término independiente en la regresión\n    )\n\n    model_r.fit(X_train, y_train[:, t_idx])\n\n    models[target]        = model_r\n    test_preds[:, t_idx]  = model_r.predict(X_test)\n    print(f\"  {target:<20} → entrenado\")\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-26T00:40:26.198372Z","iopub.execute_input":"2026-04-26T00:40:26.199124Z","iopub.status.idle":"2026-04-26T00:40:56.668993Z","shell.execute_reply.started":"2026-04-26T00:40:26.199091Z","shell.execute_reply":"2026-04-26T00:40:56.668224Z"}},"outputs":[],"execution_count":null}]}