{"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":112509,"databundleVersionId":14254895,"sourceType":"competition"},{"sourceId":13727488,"sourceType":"datasetVersion","datasetId":8733721},{"sourceId":13964417,"sourceType":"datasetVersion","datasetId":8901900}],"dockerImageVersionId":31192,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"**🧭 Plan de Trabajo – Competencia de Predicción de Biomasa (Kaggle)**","metadata":{}},{"cell_type":"code","source":"import pandas as pd\nimport numpy as np\nimport os\nimport matplotlib.pyplot as plt\nimport seaborn as sns\nimport seaborn.objects as so\nimport cv2\nfrom tqdm import tqdm\nfrom PIL import Image\nimport random\nimport warnings\nfrom sklearn.preprocessing import LabelEncoder\nfrom torch.utils.data import Dataset, DataLoader\nfrom torchvision import transforms\nfrom torchvision.models import vit_b_16, ViT_B_16_Weights\nimport torch\nfrom tqdm import tqdm\nimport torch.nn as nn\nfrom sklearn.model_selection import GroupKFold\nimport torch.optim as optim\nfrom torch.utils.data import Dataset, DataLoader\nfrom sklearn.model_selection import GroupKFold\nfrom torch.utils.data import TensorDataset, DataLoader\nimport pickle\nimport torchvision.transforms as T\nimport joblib","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T21:18:33.711679Z","iopub.execute_input":"2025-12-03T21:18:33.712378Z","iopub.status.idle":"2025-12-03T21:18:41.889380Z","shell.execute_reply.started":"2025-12-03T21:18:33.712352Z","shell.execute_reply":"2025-12-03T21:18:41.888742Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =========================================================\n#  Información general del dataset\n# =========================================================\n# ---------------------------------------------------------\n#  Cargar los datos CSV\n# ---------------------------------------------------------\ntrain_path = \"/kaggle/input/csiro-biomass/train.csv\"\ntest_path = \"/kaggle/input/csiro-biomass/test.csv\"\n\ntrain_df = pd.read_csv(train_path)\ntest_df = pd.read_csv(test_path)\n\n\n# Número de filas y columnas\nprint(f\"\\n📏 Dimensiones del dataset: {train_df.shape}\")\n\n# Mostrar las primeras filas del dataset\ndisplay(train_df.head())\ndisplay(test_df.head())\ntrain_df.groupby('target_name')['target'].describe()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T21:18:41.890374Z","iopub.execute_input":"2025-12-03T21:18:41.890814Z","iopub.status.idle":"2025-12-03T21:18:41.972471Z","shell.execute_reply.started":"2025-12-03T21:18:41.890782Z","shell.execute_reply":"2025-12-03T21:18:41.971838Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =========================================================\n# 🌾 Distribución de las variables objetivo (sin warnings)\n# =========================================================\n\n# Ignorar warnings de seaborn/pandas\nwarnings.filterwarnings(\"ignore\", category=FutureWarning)\nwarnings.filterwarnings(\"ignore\", category=UserWarning, message=\"The figure layout has changed to tight\")\n# Lista de variables objetivo\ntarget_vars = ['Dry_Green_g', 'Dry_Dead_g', 'Dry_Clover_g', 'GDM_g', 'Dry_Total_g']\n\nplt.figure(figsize=(15, 10))\n\nfor i, var in enumerate(target_vars, 1):\n    plt.subplot(2, 3, i)\n    \n    # Filtrar datos del target actual\n    subset = train_df[train_df['target_name'] == var].copy()\n    \n    # Reemplazar infinitos por NaN y eliminar filas nulas\n    subset['target'] = subset['target'].replace([np.inf, -np.inf], np.nan)\n    subset = subset.dropna(subset=['target'])\n    \n    sns.histplot(subset['target'], kde=True, bins=30, color='green')\n    plt.title(f'Distribución de {var}')\n    plt.xlabel('Gramos')\n    plt.ylabel('Frecuencia')\n\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T21:18:41.973046Z","iopub.execute_input":"2025-12-03T21:18:41.973259Z","iopub.status.idle":"2025-12-03T21:18:43.240538Z","shell.execute_reply.started":"2025-12-03T21:18:41.973243Z","shell.execute_reply":"2025-12-03T21:18:43.239668Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =========================================================\n# 📦 Visualización tipo Boxplot de las variables objetivo\n# =========================================================\nwarnings.filterwarnings(\"ignore\")\n\n# Lista de variables objetivo\ntarget_vars = ['Dry_Green_g', 'Dry_Dead_g', 'Dry_Clover_g', 'GDM_g', 'Dry_Total_g']\n\n# Crear una lista vacía para almacenar los datos\ndata = []\n\n# Extraer los valores de cada variable objetivo\nfor var in target_vars:\n    subset = train_df[train_df['target_name'] == var].copy()\n    subset['target'] = subset['target'].replace([np.inf, -np.inf], np.nan)\n    subset = subset.dropna(subset=['target'])\n    subset['variable'] = var\n    data.append(subset[['variable', 'target']])\n\n# Combinar todos los subconjuntos en un solo DataFrame\ncombined_df = pd.concat(data)\n\n# Crear la figura\nplt.figure(figsize=(12, 6))\nsns.boxplot(x='variable', y='target', data=combined_df, palette='Spectral')\n\n# Añadir detalles\nplt.title('Boxplot de las variables objetivo (gramos)', fontsize=14)\nplt.xlabel('Variable objetivo')\nplt.ylabel('Valor (g)')\nplt.grid(True, linestyle='--', alpha=0.6)\n\n# Mostrar el gráfico\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T21:18:43.242234Z","iopub.execute_input":"2025-12-03T21:18:43.242950Z","iopub.status.idle":"2025-12-03T21:18:43.460973Z","shell.execute_reply.started":"2025-12-03T21:18:43.242926Z","shell.execute_reply":"2025-12-03T21:18:43.460307Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =========================================================\n# 📈 1️⃣ Análisis de correlación entre las variables objetivo\n# =========================================================\n\n# Ignorar warnings innecesarios\nwarnings.filterwarnings(\"ignore\", category=FutureWarning)\n\n# ---------------------------------------------------------\n# 🧩 Crear una copia del dataframe original\n# ---------------------------------------------------------\ndf_corr = train_df.copy()\n\n# ---------------------------------------------------------\n# 🧹 Limpieza básica\n# ---------------------------------------------------------\n# Reemplazar infinitos por NaN\ndf_corr[\"target\"] = df_corr[\"target\"].replace([np.inf, -np.inf], np.nan)\n\n# Eliminar filas con valores faltantes en columnas clave\ndf_corr = df_corr.dropna(subset=[\"target\", \"target_name\", \"sample_id\"])\n\n# ---------------------------------------------------------\n# 🧠 Reestructurar el dataset (una fila por muestra)\n# ---------------------------------------------------------\n# Extraer el identificador base (algunos sample_id incluyen el target)\ndf_corr[\"base_id\"] = df_corr[\"sample_id\"].apply(lambda x: str(x).split(\"_\")[0])\n\n# Pivotear: columnas = tipos de biomasa (target_name), filas = muestra (base_id)\ndf_pivot = df_corr.pivot_table(\n    index=\"base_id\",\n    columns=\"target_name\",\n    values=\"target\",\n    aggfunc=\"mean\"\n)\n\n# Eliminar filas incompletas\ndf_pivot = df_pivot.dropna(how=\"any\")\n\n# ---------------------------------------------------------\n# 📊 Calcular matriz de correlación (Pearson)\n# ---------------------------------------------------------\ncorr_matrix = df_pivot.corr(method=\"pearson\")\n\nprint(\"\\n📈 Matriz de correlación entre variables objetivo (Pearson):\\n\")\nprint(corr_matrix.round(2))\n\n# ---------------------------------------------------------\n# 🎨 Visualización de la correlación\n# ---------------------------------------------------------\nplt.figure(figsize=(12, 10))\nsns.heatmap(\n    corr_matrix,\n    annot=True,\n    fmt=\".2f\",\n    cmap=\"YlGnBu\",\n    linewidths=0.5,\n    cbar_kws={\"shrink\": 0.8}\n)\nplt.title(\"Matriz de correlación entre variables objetivo\", fontsize=14, fontweight=\"bold\")\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T21:18:43.461850Z","iopub.execute_input":"2025-12-03T21:18:43.462094Z","iopub.status.idle":"2025-12-03T21:18:43.783094Z","shell.execute_reply.started":"2025-12-03T21:18:43.462077Z","shell.execute_reply":"2025-12-03T21:18:43.782509Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Copia de trabajo\ndfx = train_df.copy()\n\n# Seleccionar columnas relevantes\ndfx = dfx[['image_path', 'sample_id', 'Sampling_Date', 'State', 'Species', 'Pre_GSHH_NDVI', 'Height_Ave_cm', 'target_name', 'target']]\n\nprint(\"\\n📘 Información general:\")\ndfx.info()\n\n# Número de filas y columnas\nprint(f\"\\n📏 Dimensiones del dataset: {dfx.shape}\")\n\n# Mostrar las primeras filas del dataset\ndisplay(dfx.head())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T21:18:43.783857Z","iopub.execute_input":"2025-12-03T21:18:43.784069Z","iopub.status.idle":"2025-12-03T21:18:43.808520Z","shell.execute_reply.started":"2025-12-03T21:18:43.784052Z","shell.execute_reply":"2025-12-03T21:18:43.807658Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ================================================================\n# 1) CARGAR TRAIN Y CONVERTIR A FORMATO ANCHO\n# ================================================================\n\ntrain_df = pd.read_csv(\"/kaggle/input/csiro-biomass/train.csv\")\n\ndfx = train_df.copy()\n\n# Quitar el sufijo \"__target\"\ndfx[\"sample_id\"] = dfx[\"sample_id\"].str.replace(r\"__.*\", \"\", regex=True)\n\n# Pivotear TRAIN\ndf_wide_train = dfx.pivot_table(\n    index=[\n        \"image_path\", \"sample_id\", \"Sampling_Date\", \"State\", \"Species\",\n        \"Pre_GSHH_NDVI\", \"Height_Ave_cm\"\n    ],\n    columns=\"target_name\",\n    values=\"target\"\n).reset_index()\n\ndf_wide_train.columns.name = None\n\nprint(\"TRAIN ancho:\", df_wide_train.shape)\ndisplay(df_wide_train.head())\n\n# Agregar columna con el ID real de la imagen sin .jpg\ndf_wide_train[\"image_id\"] = df_wide_train[\"image_path\"].apply(\n    lambda x: x.split(\"/\")[-1].replace(\".jpg\", \"\")\n)\n\nprint(\"✔ df_wide_train listo.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T21:51:20.389157Z","iopub.execute_input":"2025-12-03T21:51:20.389501Z","iopub.status.idle":"2025-12-03T21:51:20.427953Z","shell.execute_reply.started":"2025-12-03T21:51:20.389466Z","shell.execute_reply":"2025-12-03T21:51:20.427211Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pandas as pd\nimport numpy as np\nfrom sklearn.preprocessing import StandardScaler\n\ndf = df_wide_train.copy()\n\n# ==========================================================\n# 1. CODIFICACIÓN SEN/COS DE LA FECHA\n# ==========================================================\ndf[\"Sampling_Date\"] = pd.to_datetime(df[\"Sampling_Date\"])\ndf[\"DayOfYear\"] = df[\"Sampling_Date\"].dt.dayofyear\n\ndf[\"Day_sin\"] = np.sin(2 * np.pi * df[\"DayOfYear\"] / 365)\ndf[\"Day_cos\"] = np.cos(2 * np.pi * df[\"DayOfYear\"] / 365)\n\ndf.drop(columns=[\"Sampling_Date\", \"DayOfYear\"], inplace=True)\n\n\n# ==========================================================\n# 2. SPECIES → LABEL ENCODER\n# ==========================================================\nfrom sklearn.preprocessing import LabelEncoder\nle_species = LabelEncoder()\ndf[\"Species_idx\"] = le_species.fit_transform(df[\"Species\"])\n\n\n# ==========================================================\n# 3. STATE → ONE-HOT ENCODING\n# ==========================================================\nif \"State\" in df.columns:\n    df = pd.get_dummies(df, columns=[\"State\"], prefix=\"State\")\nelse:\n    print(\"⚠️ OJO: La columna 'State' NO existe. Verifica df.\")\n\n\n# ==========================================================\n# 4. DETECCIÓN AUTOMÁTICA DE COLUMNAS OHE DE STATE\n# ==========================================================\nstate_cols = [col for col in df.columns if col.startswith(\"State_\")]\nfor col in state_cols:\n    df[col] = df[col].astype(int)\n\n\n# ==========================================================\n# 5. NORMALIZAR VARIABLES NUMÉRICAS\n# ==========================================================\nnum_cols = [\"Pre_GSHH_NDVI\", \"Height_Ave_cm\"]\nscaler = StandardScaler()\ndf[num_cols] = scaler.fit_transform(df[num_cols])\n\n\n# ==========================================================\n# 6. ELIMINAR COLUMNAS INÚTILES\n# ==========================================================\nfor col in [\"Species\"]:\n    if col in df.columns:\n        df.drop(columns=[col], inplace=True)\n\n\n# ==========================================================\n# 7. ACTUALIZAR df_wide_train (PASO CRUCIAL)\n# ==========================================================\ndf_wide_train = df.copy()\nprint(\"✔ df_wide_train actualizado. Columnas:\", df_wide_train.columns)\nprint(\"✔ Shape final:\", df_wide_train.shape)\ndisplay(df_wide_train.head())\n\ndf_wide_train.to_csv(\"df_wide_train.csv\", index=False)\nprint(\"✔ df_wide_train.csv guardado correctamente\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T21:51:25.139965Z","iopub.execute_input":"2025-12-03T21:51:25.140285Z","iopub.status.idle":"2025-12-03T21:51:25.184931Z","shell.execute_reply.started":"2025-12-03T21:51:25.140263Z","shell.execute_reply":"2025-12-03T21:51:25.184299Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"image_path = \"/kaggle/input/csiro-biomass/train/\"  # cambia según tu ruta\nimage_files = [os.path.join(image_path, f) for f in os.listdir(image_path) if f.endswith('.jpg')]\nprint(f\"Total de imágenes: {len(image_files)}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T21:18:43.887606Z","iopub.execute_input":"2025-12-03T21:18:43.887861Z","iopub.status.idle":"2025-12-03T21:18:43.898440Z","shell.execute_reply.started":"2025-12-03T21:18:43.887844Z","shell.execute_reply":"2025-12-03T21:18:43.897780Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ==========================================================\n# 1) CONFIGURACIÓN GENERAL\n# ==========================================================\n\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n\ntransform = transforms.Compose([\n    transforms.Resize(232),\n    transforms.CenterCrop(224),\n    transforms.ToTensor(),\n    transforms.Normalize(\n        mean=[0.485, 0.456, 0.406],\n        std=[0.229, 0.224, 0.225])\n])\n\n\n# ==========================================================\n# 2) DATASET PARA FORMATO ANCHO (1 FILA = 1 IMAGEN)\n# ==========================================================\n\nclass ImageDatasetWide(Dataset):\n    def __init__(self, df, img_dir, transform):\n        self.df = df\n        self.img_dir = img_dir\n        self.transform = transform\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n\n        filename = row[\"image_path\"].split(\"/\")[-1]  # ejemplo: ID123.jpg\n        full_path = os.path.join(self.img_dir, filename)\n\n        img = Image.open(full_path).convert(\"RGB\")\n        img = self.transform(img)\n\n        return img, row[\"image_id\"]  # devolvemos SOLO image_id\n\n\n# ==========================================================\n# 3) FUNCIÓN PARA EXTRAER EMBEDDINGS\n# ==========================================================\n\ndef extract_embeddings(df, img_dir, model, batch_size=32):\n    dataset = ImageDatasetWide(df, img_dir, transform)\n    loader = DataLoader(dataset, batch_size=batch_size, shuffle=False)\n\n    embs = []\n    ids = []\n\n    model.eval()\n    with torch.no_grad():\n        for imgs, image_ids in tqdm(loader, desc=f\"Extrayendo embeddings de {img_dir}\"):\n            imgs = imgs.to(device)\n            emb = model(imgs).cpu().numpy()\n            embs.append(emb)\n            ids.extend(image_ids)\n\n    return np.vstack(embs), np.array(ids)\n\n\n# ==========================================================\n# 4) CARGAR MODELO VIT-B/16 DESDE PESOS LOCALES\n# ==========================================================\n\nweights_path = \"/kaggle/input/vit-b16-weights/vit_b_16_weights.pth\"\nstate_dict = torch.load(weights_path, map_location=device)\n\nmodel = vit_b_16(weights=None).to(device)\nmodel.load_state_dict(state_dict)\nmodel.heads = torch.nn.Identity()\nmodel.eval()\n\n\n# ==========================================================\n# 5) EXTRAER EMBEDDINGS DEL TRAIN (df_wide_train)\n# ==========================================================\n\ntrain_img_dir = \"/kaggle/input/csiro-biomass/train\"\n\nemb_train, ids_train = extract_embeddings(\n    df_wide_train,\n    train_img_dir,\n    model\n)\n\nnp.save(\"embeddings_train.npy\", emb_train)\nnp.save(\"ids_train.npy\", ids_train)\n\nprint(\"✔ Embeddings TRAIN:\", emb_train.shape)   # (357, 768)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T21:18:43.900373Z","iopub.execute_input":"2025-12-03T21:18:43.900563Z","iopub.status.idle":"2025-12-03T21:19:14.274453Z","shell.execute_reply.started":"2025-12-03T21:18:43.900548Z","shell.execute_reply":"2025-12-03T21:19:14.273626Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ================================================================\n# 1) IMPORTS Y CONFIGURACIÓN\n# ================================================================\nimport os\nimport torch\nimport numpy as np\nfrom PIL import Image\nfrom torch.utils.data import Dataset, DataLoader\nfrom torchvision import transforms\nfrom torchvision.models import vit_b_16\nfrom sklearn.model_selection import train_test_split\nimport torch.nn as nn\nimport torch.optim as optim\n\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n\n\n# ================================================================\n# 2) DATASET\n# ================================================================\nclass PastureDataset(Dataset):\n    def __init__(self, df, img_dir, transform):\n        self.df = df.reset_index(drop=True)\n        self.img_dir = img_dir\n        self.transform = transform\n        self.targets = self.df[[\"Dry_Clover_g\", \"Dry_Dead_g\", \n                                \"Dry_Green_g\", \"Dry_Total_g\", \"GDM_g\"]].values.astype(\"float32\")\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        img_id = row[\"image_id\"]\n        path = f\"{self.img_dir}/{img_id}.jpg\"\n\n        img = Image.open(path).convert(\"RGB\")\n        img = self.transform(img)\n\n        y = self.targets[idx]\n        return img, y\n\n\n# ================================================================\n# 3) TRANSFORMACIONES\n# ================================================================\ntransform_train = transforms.Compose([\n    transforms.Resize(256),\n    transforms.CenterCrop(224),\n    transforms.RandomHorizontalFlip(),\n    transforms.RandomRotation(5),\n    transforms.ColorJitter(brightness=0.1, contrast=0.1, saturation=0.1),\n    transforms.ToTensor(),\n    transforms.Normalize(\n        mean=[0.485, 0.456, 0.406],\n        std=[0.229, 0.224, 0.225]\n    )\n])\n\ntransform_val = transforms.Compose([\n    transforms.Resize(256),\n    transforms.CenterCrop(224),\n    transforms.ToTensor(),\n    transforms.Normalize(\n        mean=[0.485, 0.456, 0.406],\n        std=[0.229, 0.224, 0.225]\n    )\n])\n\n\n# ================================================================\n# 4) TRAIN / VALIDATION SPLIT\n# ================================================================\ntrain_df, val_df = train_test_split(df_wide_train, test_size=0.2, random_state=42)\n\ntrain_dataset = PastureDataset(train_df, \"/kaggle/input/csiro-biomass/train\", transform_train)\nval_dataset   = PastureDataset(val_df,   \"/kaggle/input/csiro-biomass/train\", transform_val)\n\ntrain_loader = DataLoader(train_dataset, batch_size=16, shuffle=True, num_workers=2)\nval_loader   = DataLoader(val_dataset,   batch_size=16, shuffle=False, num_workers=2)\n\n\n# ================================================================\n# 5) CARGAR ViT-B/16 DESDE PESOS LOCALES\n# ================================================================\nvit = vit_b_16(weights=None)\nstate_dict = torch.load(\"/kaggle/input/vit-b16-weights/vit_b_16_weights.pth\", map_location=device)\nvit.load_state_dict(state_dict)\n\n# Reemplazar la cabeza por regresión (5 targets)\nvit.heads = nn.Linear(vit.heads.head.in_features, 5)\nvit = vit.to(device)\n\n\n# ================================================================\n# 6) FUNCIÓN DE EVALUACIÓN\n# ================================================================\ndef evaluate(model, loader):\n    model.eval()\n    total = 0.0\n    with torch.no_grad():\n        for imgs, targets in loader:\n            imgs, targets = imgs.to(device), targets.to(device)\n            preds = model(imgs)\n            loss = criterion(preds, targets)\n            total += loss.item()\n    return total / len(loader)\n\n\n# ================================================================\n# 7) ENTRENAMIENTO — FASE 1 (HEAD ONLY)\n# ================================================================\nfor name, param in vit.named_parameters():\n    if not name.startswith(\"heads\"):\n        param.requires_grad = False\n\ncriterion = nn.MSELoss()\noptimizer = optim.Adam(vit.heads.parameters(), lr=1e-4)\nscheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=5)\n\nbest_val = float(\"inf\")\nbest_model = None\n\nepochs = 5\nfor epoch in range(epochs):\n\n    vit.train()\n    total_loss = 0.0\n\n    for imgs, targets in train_loader:\n        imgs, targets = imgs.to(device), targets.to(device)\n\n        preds = vit(imgs)\n        loss = criterion(preds, targets)\n\n        optimizer.zero_grad()\n        loss.backward()\n        optimizer.step()\n\n        total_loss += loss.item()\n\n    val_loss = evaluate(vit, val_loader)\n    scheduler.step()\n\n    print(f\"[HEAD] Epoch {epoch+1}/{epochs} - Train Loss: {total_loss/len(train_loader):.2f} | Val Loss: {val_loss:.2f}\")\n\n    if val_loss < best_val:\n        best_val = val_loss\n        best_model = vit.state_dict().copy()\n\n\n# ================================================================\n# 8) ENTRENAMIENTO — FASE 2 (DESCONGELAR ÚLTIMOS 2 BLOQUES)\n# ================================================================\nfor name, param in vit.named_parameters():\n    if \"encoder.layers.10\" in name or \"encoder.layers.11\" in name:\n        param.requires_grad = True\n\noptimizer = optim.Adam(filter(lambda p: p.requires_grad, vit.parameters()), lr=5e-5)\nscheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=3)\n\nepochs_ft = 20\nfor epoch in range(epochs_ft):\n\n    vit.train()\n    total_loss = 0.0\n\n    for imgs, targets in train_loader:\n        imgs, targets = imgs.to(device), targets.to(device)\n\n        preds = vit(imgs)\n        loss = criterion(preds, targets)\n\n        optimizer.zero_grad()\n        loss.backward()\n        optimizer.step()\n\n        total_loss += loss.item()\n\n    val_loss = evaluate(vit, val_loader)\n    scheduler.step()\n\n    print(f\"[FT] Epoch {epoch+1}/{epochs_ft} - Train Loss: {total_loss/len(train_loader):.2f} | Val Loss: {val_loss:.2f}\")\n\n    if val_loss < best_val:\n        best_val = val_loss\n        best_model = vit.state_dict().copy()\n\n\n# ================================================================\n# 9) GUARDAR EL MEJOR MODELO\n# ================================================================\ntorch.save(best_model, \"vit_finetuned.pth\")\nprint(\"✔ Modelo fine-tuned MEJORADO guardado como vit_finetuned.pth\")\n\n# ================================================================\n# 10) EXTRAER EMBEDDINGS FINE-TUNED (VERSIÓN COMPATIBLE CON KAGGLE)\n# ================================================================\n\n# Dataset para inferencia: solo imagen + image_id\nclass ImageOnlyDataset(Dataset):\n    def __init__(self, df, img_dir, transform):\n        self.df = df.reset_index(drop=True)\n        self.img_dir = img_dir\n        self.transform = transform\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        img_id = row[\"image_id\"]\n        path = f\"{self.img_dir}/{img_id}.jpg\"\n\n        img = Image.open(path).convert(\"RGB\")\n        img = self.transform(img)\n\n        return img, img_id\n\n\n# Transform sin augmentación\ntransform_extract = transforms.Compose([\n    transforms.Resize(256),\n    transforms.CenterCrop(224),\n    transforms.ToTensor(),\n    transforms.Normalize(\n        mean=[0.485, 0.456, 0.406],\n        std=[0.229, 0.224, 0.225]\n    )\n])\n\n\ndef extract_embeddings(df, model, img_dir, batch_size=32):\n    dataset = ImageOnlyDataset(df, img_dir, transform_extract)\n    loader = DataLoader(dataset, batch_size=batch_size, shuffle=False, num_workers=2)\n\n    all_embs = []\n    all_ids = []\n\n    model.eval()\n    with torch.no_grad():\n        for imgs, id_batch in loader:\n            imgs = imgs.to(device)\n\n            # 👉 forward() ya devuelve el embedding porque heads = Identity\n            feats = model(imgs)            # shape: (batch, 768)\n\n            all_embs.append(feats.cpu().numpy())\n            all_ids.extend(id_batch)\n\n    return np.vstack(all_embs), np.array(all_ids)\n\n\n# ================================================================\n# Cargar modelo fine-tuned correctamente\n# ================================================================\nvit_finetuned = vit_b_16(weights=None)\n\n# Reemplazar la cabeza EXACTA usada en el entrenamiento\nvit_finetuned.heads = nn.Linear(vit_finetuned.heads.head.in_features, 5)\n\n# Cargar pesos fine-tuned\nstate_dict = torch.load(\"/kaggle/input/vit-finetuned/vit_finetuned.pth\", map_location=device)\nvit_finetuned.load_state_dict(state_dict)\nprint(\"✔ Modelo fine-tuned cargado con éxito.\")\n\n# Reemplazar cabeza por Identity para obtener únicamente embeddings\nvit_finetuned.heads = nn.Identity()\nvit_finetuned = vit_finetuned.to(device)\nvit_finetuned.eval()\n\nprint(\"✔ Modelo listo para extraer embeddings.\")\n\n\n# ================================================================\n# EXTRAER LOS EMBEDDINGS PARA TODO df_wide_train\n# ================================================================\nemb_ft, ids_ft = extract_embeddings(\n    df_wide_train,\n    vit_finetuned,\n    \"/kaggle/input/csiro-biomass/train\"\n)\n\nprint(\"✔ Embeddings FT shape:\", emb_ft.shape)\n\n# Guardar\nnp.save(\"embeddings_finetuned.npy\", emb_ft)\nnp.save(\"ids_finetuned.npy\", ids_ft)\n\nprint(\"✔ Embeddings fine-tuned guardados correctamente.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T21:19:14.275404Z","iopub.execute_input":"2025-12-03T21:19:14.275661Z","iopub.status.idle":"2025-12-03T21:24:31.185286Z","shell.execute_reply.started":"2025-12-03T21:19:14.275636Z","shell.execute_reply":"2025-12-03T21:24:31.184410Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ================================================================\n# 0) CONFIGURACIÓN GENERAL\n# ================================================================\nimport torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom torch.utils.data import Dataset, DataLoader\nfrom torchvision import transforms\nfrom torchvision.models import vit_b_16\nfrom PIL import Image\n\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nprint(\"✔ Device:\", device)\n\n# ================================================================\n# 1) DATASET MULTIMODAL PARA FINE-TUNING CONTEXTUAL\n# ================================================================\nclass PastureContextualFTDataset(Dataset):\n    def __init__(self, df, img_dir, transform):\n        self.df = df.reset_index(drop=True)\n        self.img_dir = img_dir\n        self.transform = transform\n\n        # Columnas tabulares\n        self.tab_cols = [\n            \"Pre_GSHH_NDVI\", \"Height_Ave_cm\",\n            \"Day_sin\", \"Day_cos\",\n            \"State_NSW\", \"State_Tas\", \"State_Vic\", \"State_WA\"\n        ]\n        self.X_tab = self.df[self.tab_cols].values.astype(\"float32\")\n\n        # Species_idx\n        self.species_idx = self.df[\"Species_idx\"].values.astype(\"int64\")\n\n        # Targets\n        self.targets = self.df[[\n            \"Dry_Clover_g\",\"Dry_Dead_g\",\"Dry_Green_g\",\n            \"Dry_Total_g\",\"GDM_g\"\n        ]].values.astype(\"float32\")\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n\n        img_path = f\"{self.img_dir}/{row['image_id']}.jpg\"\n        img = Image.open(img_path).convert(\"RGB\")\n        img = self.transform(img)\n\n        x_tab = torch.tensor(self.X_tab[idx])\n        sp_idx = torch.tensor(self.species_idx[idx])\n        y = torch.tensor(self.targets[idx])\n\n        return img, x_tab, sp_idx, y\n\n\n# ================================================================\n# 2) MODELO MULTIMODAL — ViT + Tabular + Species\n# ================================================================\nclass ViTWithContext(nn.Module):\n    def __init__(self, vit_model, num_species):\n        super().__init__()\n\n        # Backbone ViT\n        self.vit = vit_model\n\n        # Imagen (CLS)\n        self.img_fc = nn.Linear(768, 512)\n\n        # Tabular MLP\n        self.tab_fc = nn.Sequential(\n            nn.Linear(8, 64),\n            nn.ReLU(),\n            nn.Linear(64, 128)\n        )\n\n        # Species embedding\n        self.sp_emb = nn.Embedding(num_species, 16)\n\n        # Fusión final\n        self.fusion = nn.Sequential(\n            nn.Linear(512 + 128 + 16, 256),\n            nn.ReLU(),\n            nn.Linear(256, 5)\n        )\n\n    def forward(self, img, x_tab, sp_idx):\n        # Imagen → ViT → CLS (768)\n        cls_emb = self.vit(img)\n        img_emb = self.img_fc(cls_emb)  # (B,512)\n\n        # Tabular\n        tab_emb = self.tab_fc(x_tab)\n\n        # Species embedding\n        sp_emb = self.sp_emb(sp_idx)\n\n        # Fusión\n        fused = torch.cat([img_emb, tab_emb, sp_emb], dim=1)\n        out = self.fusion(fused)\n\n        return out, img_emb   # embedding contextualizado\n\n\n# ================================================================\n# 3) CARGAR EL ViT FINE-TUNED (solo imagen)\n# ================================================================\nvit_base = vit_b_16(weights=None)\nvit_base.heads = nn.Linear(vit_base.heads.head.in_features, 5)\n\n# CARGA TU MODELO FINE-TUNED\nstate_dict = torch.load(\"/kaggle/input/vit-finetuned/vit_finetuned.pth\", map_location=device)\nvit_base.load_state_dict(state_dict)\n\n# Quitamos la cabeza para usar CLS\nvit_base.heads = nn.Identity()\nvit_base = vit_base.to(device)\n\n\n# ================================================================\n# 4) PREPARAR EL MODELO CONTEXTUAL\n# ================================================================\nNUM_SPECIES = df_wide_train[\"Species_idx\"].nunique()\nmodel_ctx = ViTWithContext(vit_base, num_species=NUM_SPECIES).to(device)\n\n# Congelar todo excepto los últimos 2 bloques y nuevas capas\nfor name, param in model_ctx.vit.named_parameters():\n    if not (\"encoder.layers.10\" in name or \"encoder.layers.11\" in name):\n        param.requires_grad = False\n\ncriterion = nn.MSELoss()\noptimizer = optim.Adam(\n    filter(lambda p: p.requires_grad, model_ctx.parameters()),\n    lr=5e-5\n)\n\n# ================================================================\n# 5) DATA LOADERS\n# ================================================================\ntransform_ctx = transforms.Compose([\n    transforms.Resize(256),\n    transforms.CenterCrop(224),\n    transforms.ToTensor(),\n    transforms.Normalize(\n        mean=[0.485, 0.456, 0.406],\n        std=[0.229, 0.224, 0.225]\n    )\n])\n\ntrain_df, val_df = train_test_split(df_wide_train, test_size=0.2, random_state=42)\n\ntrain_ds = PastureContextualFTDataset(train_df, \"/kaggle/input/csiro-biomass/train\", transform_ctx)\nval_ds   = PastureContextualFTDataset(val_df,   \"/kaggle/input/csiro-biomass/train\", transform_ctx)\n\ntrain_loader = DataLoader(train_ds, batch_size=16, shuffle=True)\nval_loader   = DataLoader(val_ds, batch_size=16, shuffle=False)\n\n\n# ================================================================\n# 6) FUNCIÓN DE VALIDACIÓN\n# ================================================================\ndef evaluate_ctx(model, loader):\n    model.eval()\n    total = 0\n    with torch.no_grad():\n        for img, tab, sp, y in loader:\n            img, tab, sp, y = img.to(device), tab.to(device), sp.to(device), y.to(device)\n            pred, _ = model(img, tab, sp)\n            loss = criterion(pred, y)\n            total += loss.item()\n    return total / len(loader)\n\n\n# ================================================================\n# 7) ENTRENAMIENTO DEL MODELO CONTEXTUAL\n# ================================================================\n# ================================================================\n# 7) ENTRENAMIENTO DEL MODELO CONTEXTUAL (MEJORADO)\n# ================================================================\nfrom torch.optim.lr_scheduler import ReduceLROnPlateau\nimport numpy as np\n\nmax_epochs = 20          # antes eran 4\npatience   = 5           # early stopping\nbest_val   = float(\"inf\")\nbest_state = None\nno_improve = 0\n\nscheduler = ReduceLROnPlateau(\n    optimizer,\n    mode=\"min\",\n    factor=0.5,        # reduce LR a la mitad\n    patience=2,        # si 2 épocas seguidas no mejora val_loss\n    verbose=True\n)\n\nfor ep in range(1, max_epochs + 1):\n    model_ctx.train()\n    train_losses = []\n\n    for img, tab, sp, y in train_loader:\n        img, tab, sp, y = img.to(device), tab.to(device), sp.to(device), y.to(device)\n\n        pred, _ = model_ctx(img, tab, sp)\n        loss = criterion(pred, y)\n\n        optimizer.zero_grad()\n        loss.backward()\n        optimizer.step()\n\n        train_losses.append(loss.item())\n\n    # ---- Validación ----\n    val_loss = evaluate_ctx(model_ctx, val_loader)\n    train_loss = float(np.mean(train_losses))\n\n    print(f\"Epoch {ep}/{max_epochs} | Train {train_loss:.3f} | Val {val_loss:.3f}\")\n\n    # LR scheduler en función de validación\n    scheduler.step(val_loss)\n\n    # Guardar mejor modelo\n    if val_loss < best_val - 1e-3:   # pequeña tolerancia\n        best_val = val_loss\n        best_state = model_ctx.state_dict()\n        no_improve = 0\n        print(f\"  ✔ Nueva mejor Val Loss: {best_val:.3f}\")\n    else:\n        no_improve += 1\n        print(f\"  ⚠ Sin mejora en val durante {no_improve} época(s)\")\n\n    # Early stopping\n    if no_improve >= patience:\n        print(f\"⛔ Early stopping en epoch {ep}. Mejor Val Loss: {best_val:.3f}\")\n        break\n\n# Cargar el mejor modelo antes de extraer embeddings\nif best_state is not None:\n    model_ctx.load_state_dict(best_state)\n\nprint(\"✔ Entrenamiento contextual terminado. Mejor Val Loss:\", best_val)\n\n\n# ================================================================\n# 8) EXTRAER EMBEDDINGS CONTEXTUALIZADOS\n# ================================================================\nemb_ctx = []\nids_ctx = []\n\nmodel_ctx.eval()\nfull_ds = PastureContextualFTDataset(df_wide_train, \"/kaggle/input/csiro-biomass/train\", transform_ctx)\nfull_loader = DataLoader(full_ds, batch_size=32, shuffle=False)\n\nwith torch.no_grad():\n    for img, tab, sp, y in full_loader:\n        img, tab, sp = img.to(device), tab.to(device), sp.to(device)\n        _, img_emb = model_ctx(img, tab, sp)\n        emb_ctx.append(img_emb.cpu().numpy())\n\nemb_ctx = np.vstack(emb_ctx)\n\nnp.save(\"embeddings_contextual.npy\", emb_ctx)\nprint(\"✔ Embeddings contextualizados guardados como embeddings_contextual.npy\")\nprint(\"✔ Embeddings FT contextualizados:\", emb_ctx.shape)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T21:24:31.186549Z","iopub.execute_input":"2025-12-03T21:24:31.186852Z","iopub.status.idle":"2025-12-03T21:32:30.120573Z","shell.execute_reply.started":"2025-12-03T21:24:31.186818Z","shell.execute_reply":"2025-12-03T21:32:30.119848Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\n\n\n#import umap.umap_ as umap  # esta es la API típica\n# Embeddings (ya generados en pasos anteriores)\nemb_base = np.load(\"embeddings_train.npy\")         # ViT general\nemb_ft   = np.load(\"embeddings_finetuned.npy\")     # ViT fine-tuned\nemb_ctx  = np.load(\"embeddings_contextual.npy\")    # ViT contextual (512)\n\nprint(\"Base:\", emb_base.shape)\nprint(\"FT:  \", emb_ft.shape)\nprint(\"CTX: \", emb_ctx.shape)\n\n# Etiquetas para colorear (por ejemplo Species_idx)\nlabels_species = df_wide_train[\"Species_idx\"].values\nlabels_Dry_Total_g = df_wide_train[\"Dry_Total_g\"].values\nlabels_gdm = df_wide_train[\"GDM_g\"].values\nlabels_Dry_Clover_g = df_wide_train[\"Dry_Clover_g\"].values\nlabels_Dry_Green_g = df_wide_train[\"Dry_Green_g\"].values\nlabels_Dry_Dead_g = df_wide_train[\"Dry_Dead_g\"].values\n\nscaler_base = StandardScaler()\nscaler_ft   = StandardScaler()\nscaler_ctx  = StandardScaler()\n# Si no los tienes importados en esta celda:\nfrom sklearn.preprocessing import StandardScaler\n\nemb_base_norm = scaler_base.fit_transform(emb_base)\nemb_ft_norm   = scaler_ft.fit_transform(emb_ft)\nemb_ctx_norm  = scaler_ctx.fit_transform(emb_ctx)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T21:32:30.121368Z","iopub.execute_input":"2025-12-03T21:32:30.121590Z","iopub.status.idle":"2025-12-03T21:32:30.155794Z","shell.execute_reply.started":"2025-12-03T21:32:30.121572Z","shell.execute_reply":"2025-12-03T21:32:30.154957Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import numpy as np\nfrom sklearn.decomposition import PCA\nfrom sklearn.neighbors import NearestNeighbors\nimport matplotlib.pyplot as plt\n\ndef mini_umap(X, n_neighbors=15, min_dist=0.1, n_components=2):\n    # 1) PCA preprocessing\n    pca = PCA(n_components=min(50, X.shape[1]))\n    X_pca = pca.fit_transform(X)\n\n    # 2) k-NN graph\n    knn = NearestNeighbors(n_neighbors=n_neighbors)\n    knn.fit(X_pca)\n    distances, indices = knn.kneighbors(X_pca)\n\n    # 3) Initialize embedding with PCA\n    Y = PCA(n_components=n_components).fit_transform(X)\n\n    # 4) Simple attractive-repulsive optimization (UMAP-like)\n    lr = 1.0\n    for epoch in range(200):\n        for i in range(X.shape[0]):\n            for j in indices[i, 1:]:  # neighbors\n                diff = Y[i] - Y[j]\n                dist = np.linalg.norm(diff) + 1e-6\n                grad = diff / dist * (dist - min_dist)\n                Y[i] -= lr * grad\n                Y[j] += lr * grad\n\n    return Y\n\ndef plot_mini_umap(X, labels, title):\n    Y = mini_umap(X)\n    plt.figure(figsize=(8,6))\n    plt.scatter(Y[:,0], Y[:,1], c=labels, cmap=\"tab20\", s=20)\n    plt.title(title)\n    plt.colorbar()\n    plt.show()\n    return Y\n\n\numap_base = plot_mini_umap(emb_base_norm, labels_Dry_Total_g, \"MiniUMAP – ViT Base\")\numap_ft   = plot_mini_umap(emb_ft_norm,   labels_Dry_Total_g, \"MiniUMAP – ViT Fine-Tuned\")\numap_ctx  = plot_mini_umap(emb_ctx_norm,  labels_Dry_Total_g, \"MiniUMAP – ViT Contextual\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T21:32:30.156757Z","iopub.execute_input":"2025-12-03T21:32:30.157118Z","iopub.status.idle":"2025-12-03T21:33:14.875535Z","shell.execute_reply.started":"2025-12-03T21:32:30.157090Z","shell.execute_reply":"2025-12-03T21:33:14.874873Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def plot_umap_color_by_value(X_2d, values, title):\n    plt.figure(figsize=(8,6))\n    sc = plt.scatter(\n        X_2d[:,0], X_2d[:,1],\n        c=values,\n        cmap=\"viridis\",\n        s=35,\n        alpha=0.85\n    )\n    plt.colorbar(sc)\n    plt.title(title)\n    plt.show()\n\nplot_umap_color_by_value(emb_ctx_norm, df_wide_train[\"Dry_Total_g\"], \"UMAP – Contextual (Dry_Total_g)\")\nplot_umap_color_by_value(emb_ctx_norm, df_wide_train[\"GDM_g\"], \"UMAP – Contextual (GDM_g)\")\nplot_umap_color_by_value(emb_ctx_norm, df_wide_train[\"Dry_Clover_g\"], \"UMAP – Contextual (Dry_Clover_g)\")\nplot_umap_color_by_value(emb_ctx_norm, df_wide_train[\"Dry_Green_g\"], \"UMAP – Contextual (Dry_Green_g)\")\nplot_umap_color_by_value(emb_ctx_norm, df_wide_train[\"Dry_Dead_g\"], \"UMAP – Contextual (Dry_Dead_g)\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T21:33:14.876426Z","iopub.execute_input":"2025-12-03T21:33:14.876758Z","iopub.status.idle":"2025-12-03T21:33:16.145385Z","shell.execute_reply.started":"2025-12-03T21:33:14.876735Z","shell.execute_reply":"2025-12-03T21:33:16.144757Z"}},"outputs":[],"execution_count":null}]}