{"metadata":{"kernelspec":{"name":"python3","display_name":"Python 3","language":"python"},"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,"isSourceIdPinned":false,"sourceType":"competition"},{"sourceId":675637,"sourceType":"modelInstanceVersion","isSourceIdPinned":false,"modelInstanceId":512217,"modelId":526861},{"sourceId":675838,"sourceType":"modelInstanceVersion","isSourceIdPinned":true,"modelInstanceId":512366,"modelId":527008},{"sourceId":676864,"sourceType":"modelInstanceVersion","isSourceIdPinned":true,"modelInstanceId":513241,"modelId":527876}],"dockerImageVersionId":31192,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"from torch.utils.data import Dataset\nfrom PIL import Image\nimport torch\nfrom sklearn.model_selection import train_test_split\nfrom torchvision import transforms\nfrom sklearn.preprocessing import StandardScaler\nimport torch.nn as nn\nfrom torchvision import models\nfrom torch.utils.data import DataLoader\nimport matplotlib.pyplot as plt\nimport torch.optim as optim \nimport seaborn as sns\nfrom sklearn.multioutput import MultiOutputRegressor\nfrom sklearn.linear_model import Ridge\nfrom sklearn.metrics import r2_score\nimport pandas as pd\nimport numpy as np\nimport os","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-08T18:21:44.252645Z","iopub.execute_input":"2025-12-08T18:21:44.253138Z","iopub.status.idle":"2025-12-08T18:21:48.087393Z","shell.execute_reply.started":"2025-12-08T18:21:44.253099Z","shell.execute_reply":"2025-12-08T18:21:48.086604Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"BASE_DIR = \"/kaggle/input/csiro-biomass/\"\nEFFNET_DIR = \"/kaggle/input/efficientnet/pytorch/default/1/\"\nRESTNET_DIR = \"/kaggle/input/restnet/pytorch/default/1/\"\nMODELS = \"/kaggle/input/modelos-finales/pytorch/default/1/\"","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-08T18:24:38.549939Z","iopub.execute_input":"2025-12-08T18:24:38.550685Z","iopub.status.idle":"2025-12-08T18:24:38.554100Z","shell.execute_reply.started":"2025-12-08T18:24:38.550659Z","shell.execute_reply":"2025-12-08T18:24:38.553274Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"test_df = pd.read_csv(BASE_DIR + \"test.csv\")\ntrain_df = pd.read_csv(BASE_DIR + \"train.csv\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-08T18:21:51.405833Z","iopub.execute_input":"2025-12-08T18:21:51.406116Z","iopub.status.idle":"2025-12-08T18:21:51.421764Z","shell.execute_reply.started":"2025-12-08T18:21:51.406095Z","shell.execute_reply":"2025-12-08T18:21:51.421063Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_df.head(15)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-08T18:21:52.090257Z","iopub.execute_input":"2025-12-08T18:21:52.090527Z","iopub.status.idle":"2025-12-08T18:21:52.106986Z","shell.execute_reply.started":"2025-12-08T18:21:52.090508Z","shell.execute_reply":"2025-12-08T18:21:52.106215Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_df.info()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-08T18:21:53.861437Z","iopub.execute_input":"2025-12-08T18:21:53.862126Z","iopub.status.idle":"2025-12-08T18:21:53.872688Z","shell.execute_reply.started":"2025-12-08T18:21:53.862100Z","shell.execute_reply":"2025-12-08T18:21:53.872008Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_df[\"State\"].value_counts()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-08T18:21:54.910786Z","iopub.execute_input":"2025-12-08T18:21:54.911498Z","iopub.status.idle":"2025-12-08T18:21:54.917656Z","shell.execute_reply.started":"2025-12-08T18:21:54.911472Z","shell.execute_reply":"2025-12-08T18:21:54.916900Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_df[\"Species\"].value_counts().head(10)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-08T18:21:55.295783Z","iopub.execute_input":"2025-12-08T18:21:55.296107Z","iopub.status.idle":"2025-12-08T18:21:55.302612Z","shell.execute_reply.started":"2025-12-08T18:21:55.296081Z","shell.execute_reply":"2025-12-08T18:21:55.302015Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_df = train_df[[\"sample_id\", \"image_path\", \"target_name\", \"target\"]]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-08T18:21:56.383420Z","iopub.execute_input":"2025-12-08T18:21:56.383985Z","iopub.status.idle":"2025-12-08T18:21:56.388641Z","shell.execute_reply.started":"2025-12-08T18:21:56.383962Z","shell.execute_reply":"2025-12-08T18:21:56.387681Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_df.isna().sum()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-08T18:21:56.796432Z","iopub.execute_input":"2025-12-08T18:21:56.797161Z","iopub.status.idle":"2025-12-08T18:21:56.803373Z","shell.execute_reply.started":"2025-12-08T18:21:56.797130Z","shell.execute_reply":"2025-12-08T18:21:56.802671Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"BASE_TARGETS = [\"Dry_Clover_g\", \"Dry_Dead_g\", \"Dry_Green_g\", \"Dry_Total_g\" , \"GDM_g\"]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-08T18:21:57.486514Z","iopub.execute_input":"2025-12-08T18:21:57.487006Z","iopub.status.idle":"2025-12-08T18:21:57.490500Z","shell.execute_reply.started":"2025-12-08T18:21:57.486985Z","shell.execute_reply":"2025-12-08T18:21:57.489807Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def config_df(df):\n    df[\"base_id\"] = df[\"sample_id\"].str.split(\"__\").str[0]\n    \n    df_pivot = df.pivot_table(\n        index=\"base_id\",\n        columns=\"target_name\",\n        values=\"target\"\n    ).reset_index()\n    \n    meta_cols = [\n        \"image_path\",\n    ]\n    \n    df_meta = (\n        df.groupby(\"base_id\")[meta_cols]\n          .first()\n          .reset_index()\n    )\n    \n    df_final = df_meta.merge(df_pivot, on=\"base_id\")\n    df_final['image_path'] = BASE_DIR + df_final['image_path'] \n    return df_final\ndf_final = config_df(train_df)\ndf_final = df_final[[\"base_id\", \"image_path\"] + BASE_TARGETS]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-08T18:21:58.658899Z","iopub.execute_input":"2025-12-08T18:21:58.659599Z","iopub.status.idle":"2025-12-08T18:21:58.679705Z","shell.execute_reply.started":"2025-12-08T18:21:58.659577Z","shell.execute_reply":"2025-12-08T18:21:58.679024Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Análisis Exploratorio de la Biomasa\n\nAntes de entrenar los modelos se realizó un análisis exploratorio para entender mejor la\nestructura de los datos y la naturaleza de las variables objetivo. Al observar la\ndistribución de la biomasa (la suma de Clover, Dead, Green, Total y GDM), se encontró\nuna distribución fuertemente sesgada hacia valores pequeños. La mayoría de los puntos se\nconcentran entre 0 y 30 gramos, mientras que los valores altos (>100 g) son menos\nfrecuentes. Esto indica un problema de regresión con colas largas y una fuerte asimetría.\n\nSe revisaron las correlaciones entre las cinco variables de biomasa. En general, existe\nuna relación positiva moderada, especialmente entre Dry_Green_g, Dry_Dead_g y Dry_Total_g,\nreflejando que las partes de la planta tienden a crecer juntas. Sin embargo, algunas\nrelaciones mostraron correlaciones débiles o incluso negativas, lo cual indica que cada\ncomponente tiene comportamiento propio y no todas pueden predecirse entre sí.\n\nUna verificación adicional confirmó que Dry_Total_g es efectivamente la suma de las tres\npartes (Green + Dead + Clover), ya que al comparar la suma manual contra el valor\nreportado, los puntos se alinearon perfectamente sobre la línea diagonal. Esto sirvió\npara validar la consistencia del dataset.\n\nEn conjunto, el análisis exploratorio mostró un conjunto altamente desbalanceado con\nsesgos fuertes hacia biomasa baja, correlaciones parciales y comportamiento no lineal,\nlo cual justifica el uso de modelos basados en imágenes en lugar de enfoques tabulares\no lineales.\n","metadata":{}},{"cell_type":"code","source":"plt.figure(figsize=(10,6))\nsns.histplot(train_df[\"target\"], bins=50)\nplt.title(\"Distribución general de la biomasa (g)\")\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-08T18:22:00.401551Z","iopub.execute_input":"2025-12-08T18:22:00.402136Z","iopub.status.idle":"2025-12-08T18:22:00.666381Z","shell.execute_reply.started":"2025-12-08T18:22:00.402110Z","shell.execute_reply":"2025-12-08T18:22:00.665694Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"corr_matrix = df_final[BASE_TARGETS].corr()\n\nplt.figure(figsize=(8, 6))\nsns.heatmap(corr_matrix, annot=True, cmap='coolwarm', vmin=-1, vmax=1, fmt=\".2f\")\nplt.title(\"Mapa de Correlación entre Componentes de Biomasa\")\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-08T18:22:01.880794Z","iopub.execute_input":"2025-12-08T18:22:01.881445Z","iopub.status.idle":"2025-12-08T18:22:02.117608Z","shell.execute_reply.started":"2025-12-08T18:22:01.881418Z","shell.execute_reply":"2025-12-08T18:22:02.116995Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df_final['Suma_Partes'] = df_final['Dry_Green_g'] + df_final['Dry_Dead_g'] + df_final['Dry_Clover_g']\n\ndf_final['Error_Suma'] = df_final['Dry_Total_g'] - df_final['Suma_Partes']\n\nplt.figure(figsize=(6, 6))\nsns.scatterplot(x='Suma_Partes', y='Dry_Total_g', data=df_final, alpha=0.5)\nplt.plot([0, df_final['Dry_Total_g'].max()], [0, df_final['Dry_Total_g'].max()], 'r--') # Línea de identidad\nplt.title(\"Verificación: Suma de Partes vs Total Reportado\")\nplt.xlabel(\"Green + Dead + Clover\")\nplt.ylabel(\"Total Reportado\")\nplt.grid(True)\nplt.show()\n\nprint(df_final['Error_Suma'].describe())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-08T18:22:17.092111Z","iopub.execute_input":"2025-12-08T18:22:17.092797Z","iopub.status.idle":"2025-12-08T18:22:17.275649Z","shell.execute_reply.started":"2025-12-08T18:22:17.092772Z","shell.execute_reply":"2025-12-08T18:22:17.275040Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"sns.pairplot(df_final[BASE_TARGETS], diag_kind='kde', corner=True)\nplt.suptitle(\"Relaciones entre todas las variables\", y=1.02)\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-08T18:22:19.233929Z","iopub.execute_input":"2025-12-08T18:22:19.234203Z","iopub.status.idle":"2025-12-08T18:22:21.898114Z","shell.execute_reply.started":"2025-12-08T18:22:19.234185Z","shell.execute_reply":"2025-12-08T18:22:21.897240Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df_final[\"stratify_bins\"] = pd.qcut(df_final[\"Dry_Total_g\"], q=10, labels=False, duplicates='drop')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-08T18:22:24.475509Z","iopub.execute_input":"2025-12-08T18:22:24.476138Z","iopub.status.idle":"2025-12-08T18:22:24.482401Z","shell.execute_reply.started":"2025-12-08T18:22:24.476112Z","shell.execute_reply":"2025-12-08T18:22:24.481789Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class PastureDataset(Dataset):\n    def __init__(self, df, y, transform=None):\n        self.df = df\n        self.y = torch.tensor(y, dtype=torch.float32)\n        self.transform = transform\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        img_path = self.df.iloc[idx][\"image_path\"]\n        image = Image.open(img_path).convert(\"RGB\")\n\n        if self.transform:\n            image = self.transform(image)\n\n        target = self.y[idx]\n\n        return image, target","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-08T18:22:25.770395Z","iopub.execute_input":"2025-12-08T18:22:25.771069Z","iopub.status.idle":"2025-12-08T18:22:25.775906Z","shell.execute_reply.started":"2025-12-08T18:22:25.771046Z","shell.execute_reply":"2025-12-08T18:22:25.775010Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df_train, df_val = train_test_split(\n    df_final,\n    test_size=0.2,\n    stratify=df_final[\"stratify_bins\"],\n    random_state=42\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-08T18:22:28.776800Z","iopub.execute_input":"2025-12-08T18:22:28.777362Z","iopub.status.idle":"2025-12-08T18:22:28.784211Z","shell.execute_reply.started":"2025-12-08T18:22:28.777338Z","shell.execute_reply":"2025-12-08T18:22:28.783484Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df_final[df_final['base_id'] == 'ID1370004842']","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-08T18:22:29.295264Z","iopub.execute_input":"2025-12-08T18:22:29.295785Z","iopub.status.idle":"2025-12-08T18:22:29.306636Z","shell.execute_reply.started":"2025-12-08T18:22:29.295761Z","shell.execute_reply":"2025-12-08T18:22:29.305899Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df_train","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-08T18:22:30.727437Z","iopub.execute_input":"2025-12-08T18:22:30.728037Z","iopub.status.idle":"2025-12-08T18:22:30.741398Z","shell.execute_reply.started":"2025-12-08T18:22:30.728011Z","shell.execute_reply":"2025-12-08T18:22:30.740780Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def build_targets(df, fit=False, y_scaler=None):\n    y = df[BASE_TARGETS].values\n    y = np.log1p(y)\n    if fit:\n        y_scaler = StandardScaler()\n        y = y_scaler.fit_transform(y)\n    else:\n        y = y_scaler.transform(y)\n\n    return y.astype(\"float32\"), y_scaler","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-08T18:22:37.130934Z","iopub.execute_input":"2025-12-08T18:22:37.131193Z","iopub.status.idle":"2025-12-08T18:22:37.135503Z","shell.execute_reply.started":"2025-12-08T18:22:37.131175Z","shell.execute_reply":"2025-12-08T18:22:37.134766Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# TARGETS\ny_train, y_scaler = build_targets(df_train, fit=True)\ny_val, _ = build_targets(df_val, y_scaler=y_scaler)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-08T18:22:38.851754Z","iopub.execute_input":"2025-12-08T18:22:38.852025Z","iopub.status.idle":"2025-12-08T18:22:38.858736Z","shell.execute_reply.started":"2025-12-08T18:22:38.852006Z","shell.execute_reply":"2025-12-08T18:22:38.858093Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df_train = df_train.drop(columns=[\"stratify_bins\"])\ndf_val = df_val.drop(columns=[\"stratify_bins\"])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-08T18:22:39.330767Z","iopub.execute_input":"2025-12-08T18:22:39.331030Z","iopub.status.idle":"2025-12-08T18:22:39.336110Z","shell.execute_reply.started":"2025-12-08T18:22:39.331012Z","shell.execute_reply":"2025-12-08T18:22:39.335221Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"y_train","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-08T18:22:40.715548Z","iopub.execute_input":"2025-12-08T18:22:40.716297Z","iopub.status.idle":"2025-12-08T18:22:40.721138Z","shell.execute_reply.started":"2025-12-08T18:22:40.716267Z","shell.execute_reply":"2025-12-08T18:22:40.720404Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"mean = [0.485, 0.456, 0.406]\nstd  = [0.229, 0.224, 0.225]\n\ntrain_tfms = transforms.Compose([\n    transforms.Resize((224,224)),\n    transforms.RandomHorizontalFlip(),\n    transforms.RandomRotation(15),\n    transforms.ColorJitter(brightness=0.2, contrast=0.2),\n    transforms.ToTensor(),\n    transforms.Normalize(mean=mean, std=std)\n])\n\nval_tfms = transforms.Compose([\n    transforms.Resize((224,224)),\n    transforms.ToTensor(),\n    transforms.Normalize(mean=mean, std=std)\n])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-08T18:22:41.356413Z","iopub.execute_input":"2025-12-08T18:22:41.357031Z","iopub.status.idle":"2025-12-08T18:22:41.362139Z","shell.execute_reply.started":"2025-12-08T18:22:41.357005Z","shell.execute_reply":"2025-12-08T18:22:41.361354Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df_train","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-08T18:22:42.572834Z","iopub.execute_input":"2025-12-08T18:22:42.573101Z","iopub.status.idle":"2025-12-08T18:22:42.586244Z","shell.execute_reply.started":"2025-12-08T18:22:42.573083Z","shell.execute_reply":"2025-12-08T18:22:42.585555Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_ds = PastureDataset(\n    df_train,\n    y_train,\n    transform=train_tfms\n)\n\nval_ds = PastureDataset(\n    df_val,\n    y_val,\n    transform=val_tfms\n)\n\ntrain_loader = DataLoader(train_ds, batch_size=32, shuffle=True)\nval_loader = DataLoader(val_ds, batch_size=32, shuffle=False)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-08T18:22:46.369830Z","iopub.execute_input":"2025-12-08T18:22:46.370543Z","iopub.status.idle":"2025-12-08T18:22:46.375232Z","shell.execute_reply.started":"2025-12-08T18:22:46.370518Z","shell.execute_reply":"2025-12-08T18:22:46.374663Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Modelo 1 — Baseline CNN (sin Optuna)\n\nEl primer modelo entrenado fue una red basada en ResNet18 con pesos preentrenados y sin\nningún tipo de optimización automática. Esta arquitectura sirvió como punto de partida\npara medir el rendimiento base del sistema únicamente con imágenes.\n\nDurante el entrenamiento, la curva de pérdida mostró una disminución progresiva tanto en\nentrenamiento como en validación, aunque con cierta variabilidad. El modelo logró\naprender patrones relevantes, pero también se observaron señales de sobreajuste leve, ya\nque las curvas comenzaron a separarse después de varias épocas.\n\nEste baseline demostró que una arquitectura simple es capaz de capturar información útil\nde las imágenes, pero también reveló la necesidad de optimizar hiperparámetros y buscar\nconfiguraciones más eficientes.\n","metadata":{}},{"cell_type":"code","source":"class PastureModel(nn.Module):\n    def __init__(self):\n        super().__init__()\n\n        # CNN\n        path_to_weights = RESTNET_DIR + \"resnet18-f37072fd.pth\"\n        if (not os.path.exists(path_to_weights)):\n            self.cnn = models.resnet18(weights=ResNet18_Weights.IMAGENET1K_V1)\n        else:\n            self.cnn = models.resnet18(weights=None)\n            \n            state_dict = torch.load(path_to_weights)\n            self.cnn.load_state_dict(state_dict)\n        self.feat_dim = 512\n        self.cnn.fc = nn.Identity()\n\n        # Regresor multi-output\n        self.regressor = nn.Sequential(\n            nn.Linear(self.feat_dim, 128),\n            nn.ReLU(),\n            nn.Linear(128, 5)\n        )\n\n    def forward(self, x):\n        img_feat = self.cnn(x)\n        return self.regressor(img_feat)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-08T18:35:44.089445Z","iopub.execute_input":"2025-12-08T18:35:44.089740Z","iopub.status.idle":"2025-12-08T18:35:44.095480Z","shell.execute_reply.started":"2025-12-08T18:35:44.089717Z","shell.execute_reply":"2025-12-08T18:35:44.094861Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def train_one_epoch(model, loader, optimizer, criterion, device):\n    model.train()\n    running_loss = 0.0\n\n    for images, targets in loader:\n        images = images.to(device)\n        targets = targets.to(device)\n\n        optimizer.zero_grad()\n\n        outputs = model(images)\n        loss = criterion(outputs, targets)\n\n        loss.backward()\n        optimizer.step()\n\n        running_loss += loss.item() * images.size(0)\n\n    return running_loss / len(loader.dataset)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-08T18:22:55.211520Z","iopub.execute_input":"2025-12-08T18:22:55.211787Z","iopub.status.idle":"2025-12-08T18:22:55.216989Z","shell.execute_reply.started":"2025-12-08T18:22:55.211767Z","shell.execute_reply":"2025-12-08T18:22:55.216150Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"@torch.no_grad()\ndef validate(model, loader, criterion, device, weights, y_scaler=None):\n    model.eval()\n    running_loss = 0.0\n    \n    # Listas para guardar todo\n    y_preds_log = []\n    y_targets_log = []\n\n    for images, targets in loader:\n        images = images.to(device)\n        targets = targets.to(device)\n\n        outputs = model(images)\n        \n        loss = criterion(outputs, targets)\n        running_loss += loss.item() * images.size(0)\n\n        y_preds_log.append(outputs.cpu())\n        y_targets_log.append(targets.cpu())\n\n    # Concatenar todos los batches\n    y_preds_log = torch.cat(y_preds_log, dim=0)\n    y_targets_log = torch.cat(y_targets_log, dim=0)\n\n    if y_scaler is not None:\n        y_preds_np = y_scaler.inverse_transform(y_preds_log.numpy())\n        y_targets_np = y_scaler.inverse_transform(y_targets_log.numpy())\n        y_preds_log = torch.from_numpy(y_preds_np)\n        y_targets_log = torch.from_numpy(y_targets_np)\n\n    y_preds_real = torch.expm1(y_preds_log)\n    y_targets_real = torch.expm1(y_targets_log)\n\n    y_preds_real = torch.clamp(y_preds_real, min=0.0)\n    \n    r2_weighted = weighted_r2_score(y_targets_real, y_preds_real, weights)\n    \n    val_loss = running_loss / len(loader.dataset)\n\n    return val_loss, r2_weighted.item()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-08T18:23:22.689552Z","iopub.execute_input":"2025-12-08T18:23:22.690331Z","iopub.status.idle":"2025-12-08T18:23:22.696349Z","shell.execute_reply.started":"2025-12-08T18:23:22.690305Z","shell.execute_reply":"2025-12-08T18:23:22.695648Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"TARGET_WEIGHTS = {\n    \"Dry_Clover_g\": 0.1,\n    \"Dry_Dead_g\": 0.1,\n    \"Dry_Green_g\": 0.1,\n    \"Dry_Total_g\": 0.5,\n    \"GDM_g\": 0.2\n}\n\ntarget_cols = list(TARGET_WEIGHTS.keys())\n\nweights = np.array([TARGET_WEIGHTS[t] for t in target_cols], dtype=np.float32)\nweights = torch.tensor(weights)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-08T18:23:23.130001Z","iopub.execute_input":"2025-12-08T18:23:23.130307Z","iopub.status.idle":"2025-12-08T18:23:23.135373Z","shell.execute_reply.started":"2025-12-08T18:23:23.130282Z","shell.execute_reply":"2025-12-08T18:23:23.134499Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def weighted_r2_score(y_true, y_pred, weights):\n    \"\"\"\n    Calcula el R2 Global Ponderado (Estilo Kaggle/DataFrame).\n    Replica la lógica de: 1 - RSS_weighted / TSS_weighted\n    usando una media global ponderada.\n    \"\"\"\n    weights = weights.to(y_true.device)\n\n    y_true_flat = y_true.reshape(-1)\n    y_pred_flat = y_pred.reshape(-1)\n\n    num_samples = y_true.shape[0]\n    \n    w_flat = weights.unsqueeze(0).expand(num_samples, -1).reshape(-1)\n\n    y_mean = torch.sum(w_flat * y_true_flat) / torch.sum(w_flat)\n\n    rss = torch.sum(((y_true_flat - y_pred_flat)**2) * w_flat)\n\n    tss = torch.sum(((y_true_flat - y_mean)**2) * w_flat)\n\n    r2_global = 1 - (rss / (tss + 1e-8))\n\n    return r2_global","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-08T18:23:29.990942Z","iopub.execute_input":"2025-12-08T18:23:29.991588Z","iopub.status.idle":"2025-12-08T18:23:29.996422Z","shell.execute_reply.started":"2025-12-08T18:23:29.991563Z","shell.execute_reply":"2025-12-08T18:23:29.995755Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-08T18:23:30.425643Z","iopub.execute_input":"2025-12-08T18:23:30.426160Z","iopub.status.idle":"2025-12-08T18:23:30.448486Z","shell.execute_reply.started":"2025-12-08T18:23:30.426135Z","shell.execute_reply":"2025-12-08T18:23:30.447892Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_loader = DataLoader(\n    train_ds,\n    batch_size=32,\n    shuffle=True,\n    num_workers=4,\n    pin_memory=True\n)\n\nval_loader = DataLoader(\n    val_ds,\n    batch_size=32,\n    shuffle=False,\n    num_workers=4,\n    pin_memory=True\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-08T18:23:30.953032Z","iopub.execute_input":"2025-12-08T18:23:30.953285Z","iopub.status.idle":"2025-12-08T18:23:30.957512Z","shell.execute_reply.started":"2025-12-08T18:23:30.953268Z","shell.execute_reply":"2025-12-08T18:23:30.956729Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def train_model(\n    model,\n    train_loader,\n    val_loader,\n    optimizer,\n    criterion,\n    device,\n    y_scaler,\n    weights,\n    scheduler=None,\n    epochs=25,\n    patience=5,\n    type_model = 1\n):\n    train_losses = []\n    val_losses = []\n    val_r2_ = []\n    best_val_loss = float('inf')\n    counter = 0\n\n    if not scheduler:\n        scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(\n            optimizer, mode='min', factor=0.1, patience=3\n        )\n    for epoch in range(epochs):\n        train_loss = train_one_epoch(\n            model, train_loader, optimizer, criterion, device\n        )\n\n        val_loss, val_r2 = validate(\n            model, val_loader, criterion,\n            device, weights\n        )\n\n        train_losses.append(train_loss)\n        val_losses.append(val_loss)\n        val_r2_.append(val_r2)\n        scheduler.step(val_loss)\n        print(\n            f\"Epoch {epoch+1}/{epochs} | \"\n            f\"Train MSE: {train_loss:.4f} | \"\n            f\"Val MSE: {val_loss:.4f} | \"\n            f\"Weighted R²: {val_r2:.4f}\"\n        )\n\n        # Early stopping\n        if val_loss < best_val_loss:\n            best_val_loss = val_loss\n            counter = 0\n            best_model_state = model.state_dict()\n            torch.save(best_model_state, f\"model{type_model}.pth\")\n        else:\n            counter += 1\n            if counter >= patience:\n                print(f\"⏹️ Early stopping en epoch {epoch+1}\")\n                model.load_state_dict(best_model_state)  # Restaurar mejores pesos\n                break\n\n    return train_losses, val_losses,val_r2_","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-08T18:23:33.640245Z","iopub.execute_input":"2025-12-08T18:23:33.641059Z","iopub.status.idle":"2025-12-08T18:23:33.647798Z","shell.execute_reply.started":"2025-12-08T18:23:33.641028Z","shell.execute_reply":"2025-12-08T18:23:33.646942Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"model = PastureModel()\nmodel = model.to(device)\n\nfor p in model.cnn.parameters():\n    p.requires_grad = True\n\nfor p in model.cnn.layer4.parameters():\n    p.requires_grad = True\n\ncriterion = torch.nn.MSELoss()\n\noptimizer = optim.AdamW([\n    {\"params\": model.cnn.parameters(), \"lr\": 1e-4},\n    {\"params\": model.regressor.parameters(), \"lr\": 1e-3},\n], weight_decay=1e-4)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-08T18:23:37.338829Z","iopub.execute_input":"2025-12-08T18:23:37.339564Z","iopub.status.idle":"2025-12-08T18:23:37.683909Z","shell.execute_reply.started":"2025-12-08T18:23:37.339541Z","shell.execute_reply":"2025-12-08T18:23:37.683324Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train = train = not os.path.exists(MODELS + \"model1.pth\")","metadata":{},"outputs":[],"execution_count":null},{"cell_type":"code","source":"if train: \n    train_losses, val_losses, val_r2_ = train_model(model, train_loader, val_loader, optimizer, criterion, device, y_scaler,weights, None,30, 10)\nelse:\n    model.load_state_dict(torch.load(MODELS + \"model1.pth\"))","metadata":{},"outputs":[],"execution_count":null},{"cell_type":"code","source":"if train: \n    plt.plot(train_losses, label=\"Train MSE\")\n    plt.plot(val_losses, label=\"Val MSE\")\n    plt.plot(val_r2_, label=\"Val R2 MSE\")\n    plt.xlabel(\"Epoch\")\n    plt.ylabel(\"Loss\")\n    plt.legend()\n    plt.show()","metadata":{},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class PastureTestDataset(Dataset):\n    def __init__(self, df, transform):\n        self.df = df.reset_index(drop=True)\n        self.transform = transform\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        img_id = self.df.iloc[idx][\"image_path\"]\n        img_path = BASE_DIR + img_id\n\n        image = Image.open(img_path).convert(\"RGB\")\n        image = self.transform(image)\n\n        return image, img_id","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-08T18:25:12.940245Z","iopub.execute_input":"2025-12-08T18:25:12.940919Z","iopub.status.idle":"2025-12-08T18:25:12.945300Z","shell.execute_reply.started":"2025-12-08T18:25:12.940894Z","shell.execute_reply":"2025-12-08T18:25:12.944466Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def build_tabular_features_test(test_df, ref_columns):\n    return pd.DataFrame(\n        np.zeros((len(test_df), len(ref_columns))),\n        columns=ref_columns,\n        dtype=\"float32\"\n    )\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-08T18:25:13.644647Z","iopub.execute_input":"2025-12-08T18:25:13.645024Z","iopub.status.idle":"2025-12-08T18:25:13.648990Z","shell.execute_reply.started":"2025-12-08T18:25:13.645000Z","shell.execute_reply":"2025-12-08T18:25:13.648374Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"test_unique = test_df.drop_duplicates(\"image_path\").reset_index(drop=True)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-08T18:25:15.130271Z","iopub.execute_input":"2025-12-08T18:25:15.130994Z","iopub.status.idle":"2025-12-08T18:25:15.135518Z","shell.execute_reply.started":"2025-12-08T18:25:15.130968Z","shell.execute_reply":"2025-12-08T18:25:15.134678Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"test_ds = PastureTestDataset(\n    test_df,\n    transform=val_tfms\n)\n\ntest_loader = DataLoader(test_ds, batch_size=32, shuffle=False)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-08T18:25:15.655936Z","iopub.execute_input":"2025-12-08T18:25:15.656212Z","iopub.status.idle":"2025-12-08T18:25:15.660609Z","shell.execute_reply.started":"2025-12-08T18:25:15.656191Z","shell.execute_reply":"2025-12-08T18:25:15.659909Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def make_submission(model):\n    model.eval()\n    preds = []\n    image_ids = []\n\n    print(\"Iniciando inferencia...\")\n    \n    with torch.no_grad():\n        for imgs, img_names in test_loader:\n            imgs = imgs.to(device)            \n            outputs = model(imgs)\n            preds.append(outputs.cpu().numpy())\n            image_ids.extend(img_names)\n    \n    preds = np.vstack(preds)\n\n    if y_scaler is not None:\n        preds = y_scaler.inverse_transform(preds)  \n\n    preds = np.expm1(preds) \n\n    preds = np.clip(preds, 0, None)\n\n    sample_ids = []\n    for img_id in image_ids:\n        clean_id = os.path.basename(str(img_id)).split('.')[0]\n        sample_ids.extend([\n            f\"{clean_id}__Dry_Clover_g\",\n            f\"{clean_id}__Dry_Dead_g\",\n            f\"{clean_id}__Dry_Green_g\",\n            f\"{clean_id}__Dry_Total_g\",\n            f\"{clean_id}__GDM_g\",\n        ])\n\n    targets_flat = preds.ravel()\n    \n    submission = pd.DataFrame({\n        \"sample_id\": sample_ids,\n        \"target\": targets_flat\n    }).iloc[:5]\n\n    submission.to_csv(\"submission.csv\", index=False)\n    print(f\"Submission generado. Filas totales: {len(submission)}\")\n    return submission","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-08T18:25:19.316895Z","iopub.execute_input":"2025-12-08T18:25:19.317394Z","iopub.status.idle":"2025-12-08T18:25:19.323322Z","shell.execute_reply.started":"2025-12-08T18:25:19.317373Z","shell.execute_reply":"2025-12-08T18:25:19.322650Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"make_submission(model)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-08T18:25:21.053679Z","iopub.execute_input":"2025-12-08T18:25:21.054366Z","iopub.status.idle":"2025-12-08T18:25:21.528019Z","shell.execute_reply.started":"2025-12-08T18:25:21.054340Z","shell.execute_reply":"2025-12-08T18:25:21.527343Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Modelo 2 — CNN Optimizada con Optuna\n\nEl segundo modelo utilizó la misma arquitectura base que el baseline, pero ahora se\nintegró Optuna para realizar búsqueda automática de hiperparámetros. Optuna exploró\ndiferentes combinaciones de tasas de aprendizaje, tamaño de batch, regularización y\ndropout.\n\nLos resultados mostraron una mejora consistente respecto al baseline. Las curvas de\nentrenamiento y validación fueron más estables y la pérdida final en validación disminuyó\nde manera significativa. La configuración sugerida por Optuna produjo un modelo con\nmejor capacidad de generalización y menores oscilaciones durante el aprendizaje.\n\n## Justificación\nOptuna permitió encontrar una combinación de hiperparámetros que hubiera sido difícil\ndescubrir de manera manual. Esto redujo tanto el error final como la variabilidad entre\népocas, convirtiendo este modelo en una mejora clara sobre el baseline.\n","metadata":{}},{"cell_type":"code","source":"class PastureModelOptuna(nn.Module):\n    def __init__(self, reg_hidden, dropout_p):\n        super().__init__()\n        path_to_weights = RESTNET_DIR + \"resnet18-f37072fd.pth\"\n        state_dict = torch.load(path_to_weights)\n        if (not os.path.exists(path_to_weights)):\n            self.cnn = models.resnet18(weights=ResNet18_Weights.IMAGENET1K_V1)\n        else:\n            self.cnn = models.resnet18(weights=None)\n            self.cnn.load_state_dict(state_dict)\n\n        self.cnn.fc = nn.Identity()  # -> 512\n\n        self.regressor = nn.Sequential(\n            nn.Linear(512, reg_hidden),\n            nn.ReLU(),\n            nn.Dropout(dropout_p),\n            nn.Linear(reg_hidden, 5)\n        )\n\n    def forward(self, x):\n        feat = self.cnn(x)\n        return self.regressor(feat)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-08T18:36:48.143059Z","iopub.execute_input":"2025-12-08T18:36:48.143597Z","iopub.status.idle":"2025-12-08T18:36:48.148755Z","shell.execute_reply.started":"2025-12-08T18:36:48.143572Z","shell.execute_reply":"2025-12-08T18:36:48.147986Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def objective(trial):\n\n    reg_hidden = trial.suggest_categorical(\"reg_hidden\", [256, 512, 1024])\n\n    # lr = trial.suggest_float(\"lr\", 1e-5, 3e-3, log=True)\n    lr = trial.suggest_float(\"lr\", 5e-4, 5e-3, log=True)\n    weight_decay = trial.suggest_float(\"weight_decay\", 1e-6, 1e-3, log=True)\n    # dropout_p = trial.suggest_float(\"dropout\", 0.0, 0.5)\n    dropout_p = trial.suggest_float(\"dropout\", 0.3, 0.6)\n\n    # batch_size = trial.suggest_categorical(\"batch_size\", [16, 32])\n    batch_size = trial.suggest_categorical(\"batch_size\", [32, 64])\n\n    # 🔹 Modelo\n    model = PastureModelImageOnly(\n        reg_hidden=reg_hidden,\n        dropout_p=dropout_p\n    ).to(device)\n\n\n    # 🔹 Congelar CNN (importante para estabilidad)\n    for p in model.cnn.parameters():\n        p.requires_grad = False\n\n    optimizer = torch.optim.Adam(\n        filter(lambda p: p.requires_grad, model.parameters()),\n        lr=lr,\n        weight_decay=weight_decay\n    )\n\n    criterion = nn.MSELoss()\n\n    # 🔹 Entrenamiento corto (CLAVE)\n    EPOCHS = 10\n\n    for _ in range(EPOCHS):\n        train_one_epoch(\n            model,\n            train_loader,\n            optimizer,\n            criterion,\n            device\n        )\n\n    # 🔹 Evaluación\n    _, val_r2 = validate(\n        model,\n        val_loader,\n        criterion,\n        device,\n        weights\n    )\n\n    return val_r2","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-08T18:25:36.303874Z","iopub.execute_input":"2025-12-08T18:25:36.304390Z","iopub.status.idle":"2025-12-08T18:25:36.310519Z","shell.execute_reply.started":"2025-12-08T18:25:36.304364Z","shell.execute_reply":"2025-12-08T18:25:36.309618Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Búsqueda de hiperparámetros\n\nPara identificar la mejor configuración del modelo, se probaron diferentes combinaciones\nde hiperparámetros: tasa de aprendizaje, optimizador, tamaño del batch, dropout.\n\n## Justificación\nLa búsqueda de hiperparámetros es necesaria porque la arquitectura sola no garantiza\nbuen desempeño. Cambios pequeños en LR, regularización o batch size pueden influir\nsignificativamente en la estabilidad del entrenamiento y en la capacidad del modelo\nde generalizar.\n\nSe realizaron múltiples experimentos comparando configuraciones y seleccionando aquella\nque ofreciera el menor error de validación y la menor variabilidad entre folds.","metadata":{}},{"cell_type":"code","source":"train = train = not os.path.exists(MODELS + \"model2.pth\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-08T18:25:45.654899Z","iopub.execute_input":"2025-12-08T18:25:45.655400Z","iopub.status.idle":"2025-12-08T18:25:45.660334Z","shell.execute_reply.started":"2025-12-08T18:25:45.655377Z","shell.execute_reply":"2025-12-08T18:25:45.659766Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import optuna\nif train:\n    study = optuna.create_study(direction=\"maximize\")\n    study.optimize(objective, n_trials=30)\n    \n    print(\"✅ Best trial:\")\n    print(study.best_trial.value)\n    print(study.best_trial.params)\n    best_params = study.best_trial.params\nelse:\n    best_params = {'reg_hidden': 256, 'lr': 0.003057874654673807, 'weight_decay': 4.8872993770944615e-06, 'dropout': 0.3134593350555373, 'batch_size': 64}","metadata":{},"outputs":[],"execution_count":null},{"cell_type":"code","source":"model_optuna = PastureModelOptuna(\n    reg_hidden=best_params[\"reg_hidden\"],\n    dropout_p=best_params[\"dropout\"]\n).to(device)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-08T18:36:51.909882Z","iopub.execute_input":"2025-12-08T18:36:51.910338Z","iopub.status.idle":"2025-12-08T18:36:52.145041Z","shell.execute_reply.started":"2025-12-08T18:36:51.910315Z","shell.execute_reply":"2025-12-08T18:36:52.144450Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_loader = DataLoader(\n    train_ds,\n    batch_size=best_params[\"batch_size\"],\n    shuffle=True,\n    num_workers=4,\n    pin_memory=True\n)\n\nval_loader = DataLoader(\n    val_ds,\n    batch_size=best_params[\"batch_size\"],\n    shuffle=False,\n    num_workers=4,\n    pin_memory=True\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-08T18:26:49.756009Z","iopub.execute_input":"2025-12-08T18:26:49.756727Z","iopub.status.idle":"2025-12-08T18:26:49.760640Z","shell.execute_reply.started":"2025-12-08T18:26:49.756703Z","shell.execute_reply":"2025-12-08T18:26:49.759879Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"head_lr = best_params['lr']\ncnn_lr  = head_lr / 100\n\noptimizer = torch.optim.AdamW([\n    # Grupo 1: La CNN (aprende muy lento para no olvidar lo básico)\n    {'params': model_optuna.cnn.parameters(), 'lr': cnn_lr},\n    \n    # Grupo 2: Tu MLP tabular y Regresor (aprenden rápido con el LR de Optuna)\n    {'params': model_optuna.regressor.parameters(), 'lr': head_lr}\n], weight_decay=best_params['weight_decay'])\n\nscheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, mode='min', factor=0.1, patience=3)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-08T18:26:54.931276Z","iopub.execute_input":"2025-12-08T18:26:54.931907Z","iopub.status.idle":"2025-12-08T18:26:54.936662Z","shell.execute_reply.started":"2025-12-08T18:26:54.931882Z","shell.execute_reply":"2025-12-08T18:26:54.935899Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"for p in model_optuna.cnn.parameters():\n    p.requires_grad = True","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-08T18:27:05.983886Z","iopub.execute_input":"2025-12-08T18:27:05.984176Z","iopub.status.idle":"2025-12-08T18:27:05.988709Z","shell.execute_reply.started":"2025-12-08T18:27:05.984155Z","shell.execute_reply":"2025-12-08T18:27:05.987723Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"if train: \n    train_losses, val_losses, val_r2_ = train_model(\n        model_optuna,\n        train_loader,\n        val_loader,\n        optimizer,\n        criterion,\n        device,\n        y_scaler,\n        weights,\n        scheduler,\n        epochs=50,\n        patience = 10,\n        type_model = 2\n    )\nelse:\n    model_optuna.load_state_dict(torch.load(MODELS + \"model2.pth\"))","metadata":{},"outputs":[],"execution_count":null},{"cell_type":"code","source":"if train:\n    plt.plot(train_losses, label=\"Train MSE\")\n    plt.plot(val_losses, label=\"Val MSE\")\n    plt.plot(val_r2_, label=\"Val MSE\")\n    plt.xlabel(\"Epoch\")\n    plt.ylabel(\"Loss\")\n    plt.legend()\n    plt.show()","metadata":{},"outputs":[],"execution_count":null},{"cell_type":"code","source":"make_submission(model_optuna)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-08T18:27:35.201140Z","iopub.execute_input":"2025-12-08T18:27:35.201769Z","iopub.status.idle":"2025-12-08T18:27:35.485192Z","shell.execute_reply.started":"2025-12-08T18:27:35.201745Z","shell.execute_reply":"2025-12-08T18:27:35.484471Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Modelo 3 — CNN con Validación Cruzada (k-Fold)\n\nEl tercer modelo aplicó la misma arquitectura del baseline pero entrenada usando\nvalidación cruzada de 5 folds. Este enfoque permitió evaluar la estabilidad de la\narquitectura bajo distintas particiones del dataset.\n\nLos resultados mostraron un comportamiento interesante. En varios folds se observaron\népocas iniciales con pérdidas extremadamente altas o explosiones temporales de la función\nde error, seguidas de una estabilización posterior. En los mejores casos, el modelo logró\nalcanzar valores de validación similares o incluso mejores que el baseline sencillo,\naunque con una variabilidad considerable entre folds.\n\nEl mejor fold obtuvo una pérdida de validación notablemente baja, mientras que otros\nfolds exhibieron oscilaciones amplias e incluso valores negativos de R² en las primeras\népocas debido a la naturaleza log-transformada de las salidas.\n\n## Justificación\nLa validación cruzada permitió identificar que la arquitectura tiene un desempeño\naceptable pero con alta sensibilidad a la partición de los datos. Esta variabilidad puede\nser explicada por el desbalance del dataset, la fuerte asimetría en los valores altos de\nbiomasa y la dificultad inherente del problema. Aun así, la técnica permitió obtener una\nestimación más robusta del rendimiento real del modelo.\n","metadata":{}},{"cell_type":"code","source":"class PastureModelEfficient(nn.Module):\n    def __init__(self, reg_hidden, dropout_p=0.3):\n        super().__init__()\n            \n        path_to_weights = EFFNET_DIR + \"efficientnet_b0_rwightman-7f5810bc.pth\"\n        if (not os.path.exists(path_to_weights)):\n            self.backbone = efficientnet_b0(weights=EfficientNet_B0_Weights.DEFAULT)\n        else:\n            self.backbone = models.efficientnet_b0(weights=None)\n            state_dict = torch.load(path_to_weights)\n            self.backbone.load_state_dict(state_dict)\n             \n        self.backbone_out = self.backbone.classifier[1].in_features \n        \n        # 3. Eliminar el clasificador original (lo sustituimos por Identidad)\n        self.backbone.classifier = nn.Identity()\n\n        # 5. Regresor Combinado\n        self.regressor = nn.Sequential(\n            nn.Linear(self.backbone_out, reg_hidden),\n            nn.ReLU(),\n            nn.Dropout(dropout_p),\n            nn.Linear(reg_hidden, 5) \n        )\n\n    def forward(self, image):\n        # El backbone ahora devuelve [Batch, 1280] porque quitamos el clasificador\n        img_feat = self.backbone(image) \n\n        return self.regressor(img_feat)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-08T18:37:01.536599Z","iopub.execute_input":"2025-12-08T18:37:01.537217Z","iopub.status.idle":"2025-12-08T18:37:01.542738Z","shell.execute_reply.started":"2025-12-08T18:37:01.537190Z","shell.execute_reply":"2025-12-08T18:37:01.542026Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from sklearn.model_selection import KFold\nfrom torch.utils.data import Subset\nimport copy","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-08T18:27:48.320309Z","iopub.execute_input":"2025-12-08T18:27:48.321226Z","iopub.status.idle":"2025-12-08T18:27:48.324542Z","shell.execute_reply.started":"2025-12-08T18:27:48.321188Z","shell.execute_reply":"2025-12-08T18:27:48.323863Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train = all(\n    not os.path.exists(MODELS + f\"model_fold_{i}.pth\")\n    for i in range(5)\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-08T18:27:52.189745Z","iopub.execute_input":"2025-12-08T18:27:52.190030Z","iopub.status.idle":"2025-12-08T18:27:52.194136Z","shell.execute_reply.started":"2025-12-08T18:27:52.190009Z","shell.execute_reply":"2025-12-08T18:27:52.193359Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"N_FOLDS = 5\nPATIENCE = 5\nEPOCHS = 25\n\nif train:\n    metric_weights = weights\n    \n    print(metric_weights)\n    kf = KFold(n_splits=N_FOLDS, shuffle=True, random_state=42)\n    \n    # Usamos df_train para el split para evitar el IndexError\n    for fold, (train_idx, val_idx) in enumerate(kf.split(df_train)):\n        print(f\"\\n{'='*10} 🚀 Iniciando Fold {fold+1}/{N_FOLDS} {'='*10}\")\n        \n        # Subsets\n        train_sub = Subset(train_ds, train_idx)\n        val_sub   = Subset(train_ds, val_idx)\n        \n        train_loader_fold = DataLoader(train_sub, batch_size=32, shuffle=True)\n        val_loader_fold   = DataLoader(val_sub, batch_size=32, shuffle=False)\n        \n        # Modelo\n        model_cv = PastureModelEfficient(\n            reg_hidden=best_params['reg_hidden'],\n            dropout_p=best_params['dropout']\n        ).to(device)\n        \n        # Optimizador y Scheduler\n        criterion = nn.MSELoss()\n        optimizer = torch.optim.AdamW(model_cv.parameters(), lr=best_params['lr'], weight_decay=best_params['weight_decay'])\n        scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, mode='min', factor=0.1, patience=3)\n        \n        best_val_loss = float('inf')\n        counter = 0\n        best_model_state = None\n    \n        for epoch in range(EPOCHS):\n            # -- ENTRENAMIENTO --\n            model_cv.train()\n            train_loss = 0\n            for imgs, targets in train_loader_fold:\n                imgs, targets = imgs.to(device), targets.to(device)\n                optimizer.zero_grad()\n                outputs = model_cv(imgs)\n                loss = criterion(outputs, targets)\n                loss.backward()\n                torch.nn.utils.clip_grad_norm_(model_cv.parameters(), max_norm=1.0)\n                optimizer.step()\n                train_loss += loss.item()\n            \n            train_loss /= len(train_loader_fold)\n    \n            # -- VALIDACIÓN (Usando tu función personalizada) --\n            val_loss, val_r2 = validate(\n                model_cv, val_loader_fold, criterion, device, \n                weights=metric_weights, \n                y_scaler=y_scaler\n            )\n            \n            # Step del Scheduler\n            scheduler.step(val_loss)\n    \n            # -- IMPRIMIR ESTADÍSTICAS --\n            print(\n                f\"Fold {fold+1} | Epoch {epoch+1}/{EPOCHS} | \"\n                f\"Train MSE (Log): {train_loss:.4f} | \"\n                f\"Val MSE (Log): {val_loss:.4f} | \"\n                f\"Weighted R² (Real): {val_r2:.4f}\"\n            )\n            \n            # -- EARLY STOPPING & SAVE --\n            if val_loss < best_val_loss:\n                best_val_loss = val_loss\n                counter = 0\n                best_model_state = copy.deepcopy(model_cv.state_dict())\n                torch.save(best_model_state, f\"model_fold_{fold}.pth\")\n            else:\n                counter += 1\n                if counter >= PATIENCE:\n                    print(f\"⏹️ Early stopping en epoch {epoch+1} del Fold {fold+1}\")\n                    break\n        \n        print(f\"✅ Fold {fold+1} terminado. Mejor Val MSE: {best_val_loss:.4f}\")","metadata":{},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def make_ensemble_submission(test_loader, n_folds=5):\n    fold_predictions = []\n    image_ids = []\n    \n    for _, ids in test_loader:\n        image_ids.extend(ids)\n    for fold in range(n_folds):\n        model_path = MODELS + f\"model_fold_{fold}.pth\"\n        if not os.path.exists(model_path):\n            print(f\"⚠️ Alerta: No se encontró {model_path}, saltando...\")\n            continue\n            \n        print(f\" -> Cargando {model_path}...\")\n        \n        model_ef = PastureModelEfficient(\n            reg_hidden=best_params['reg_hidden'],\n            dropout_p=best_params['dropout']\n        ).to(device)\n        \n        model_ef.load_state_dict(torch.load(model_path))\n        model_ef.eval()\n        \n        preds_log_list = []\n        with torch.no_grad():\n            for imgs, _ in test_loader:\n                imgs = imgs.to(device)\n                \n                output = model_ef(imgs)\n                preds_log_list.append(output.cpu().numpy())\n        \n        preds_fold_log = np.vstack(preds_log_list)\n        \n        if y_scaler is not None:\n            preds_fold_log = y_scaler.inverse_transform(preds_fold_log)\n            \n        preds_fold_real = np.expm1(preds_fold_log)\n        \n        preds_fold_real = np.clip(preds_fold_real, 0, None)\n        \n        fold_predictions.append(preds_fold_real)\n\n    ensemble_array = np.array(fold_predictions)\n    avg_preds = np.mean(ensemble_array, axis=0)\n    \n    pred_0, pred_1, pred_2, pred_3, pred_4 = avg_preds.T\n   \n    targets_flat = avg_preds.ravel() # Aplanamos todo\n\n    sample_ids = []\n    \n    if 'target_cols' not in locals():\n        target_cols = [\"Dry_Clover_g\", \"Dry_Dead_g\", \"Dry_Green_g\", \"Dry_Total_g\", \"GDM_g\"]\n    for img_id in image_ids:\n        base_id = str(img_id).split('/')[-1].split('.')[0] # Limpiar ID\n        for col_name in target_cols:\n            sample_ids.append(f\"{base_id}__{col_name}\")\n\n    # 6. Guardar CSV\n    submission = pd.DataFrame({\n        \"sample_id\": sample_ids,\n        \"target\": targets_flat\n    }).iloc[:5]\n\n    submission.to_csv(\"submission.csv\", index=False)\n    print(f\"✅ Submission guardada con {len(submission)} filas.\")\n    return submission, avg_preds","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-08T18:29:10.208735Z","iopub.execute_input":"2025-12-08T18:29:10.209309Z","iopub.status.idle":"2025-12-08T18:29:10.218069Z","shell.execute_reply.started":"2025-12-08T18:29:10.209274Z","shell.execute_reply":"2025-12-08T18:29:10.217270Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df_sub, preds_matrix = make_ensemble_submission(test_loader, n_folds=N_FOLDS)\ndf_sub.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-08T18:29:10.586259Z","iopub.execute_input":"2025-12-08T18:29:10.586528Z","iopub.status.idle":"2025-12-08T18:29:13.468998Z","shell.execute_reply.started":"2025-12-08T18:29:10.586507Z","shell.execute_reply":"2025-12-08T18:29:13.468291Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"make_submission(model_optuna)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-08T18:29:16.468895Z","iopub.execute_input":"2025-12-08T18:29:16.469446Z","iopub.status.idle":"2025-12-08T18:29:16.744197Z","shell.execute_reply.started":"2025-12-08T18:29:16.469420Z","shell.execute_reply":"2025-12-08T18:29:16.743598Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Comparación Final de los Modelos\n\nLos tres modelos permitieron evaluar el impacto de la optimización y la estabilidad del\nentrenamiento:\n\n- **Modelo 1 (Baseline)**: Estable, sencillo y útil como referencia, pero limitado por\n  la falta de optimización y la variabilidad del dataset.\n\n- **Modelo 2 (Optuna)**: Fue el modelo con mejor rendimiento global. La búsqueda de\n  hiperparámetros redujo la pérdida de validación respecto al baseline y generó curvas\n  de aprendizaje más suaves.\n\n- **Modelo 3 (k-Fold)**: Aunque algunos folds alcanzaron buenos resultados, la\n  variabilidad fue mayor que en los otros modelos, con explosiones de pérdida en\n  distintas épocas. Aun así, permitió estimar de forma más confiable la capacidad real\n  de generalización del modelo.\n\n## Modelo Seleccionado\nEl modelo optimizado con Optuna fue el seleccionado para el envío final, ya que\nrepresentó el mejor equilibrio entre desempeño, estabilidad y capacidad predictiva.\n","metadata":{}},{"cell_type":"markdown","source":"# Resultados en el Leaderboard\n\nhttps://www.kaggle.com/competitions/csiro-biomass/leaderboard","metadata":{}},{"cell_type":"code","source":"","metadata":{},"outputs":[],"execution_count":null}]}