{"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":13885874,"sourceType":"datasetVersion","datasetId":8846951},{"sourceId":285805107,"sourceType":"kernelVersion"}],"dockerImageVersionId":31193,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import numpy as np  # linear algebra\nimport pandas as pd  # data processing, CSV file I/O (e.g. pd.read_csv)\n\n# Load the data\ndf = pd.read_csv(\"/kaggle/input/csiro-biomass/train.csv\")\ndf.head()","metadata":{"_uuid":"5490bef5-e643-4757-9ce5-d1c0226633ca","_cell_guid":"744be1df-07e6-4e47-a32a-8c7a12bffaf4","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2025-12-11T19:49:21.071985Z","iopub.execute_input":"2025-12-11T19:49:21.072861Z","iopub.status.idle":"2025-12-11T19:49:21.358125Z","shell.execute_reply.started":"2025-12-11T19:49:21.072833Z","shell.execute_reply":"2025-12-11T19:49:21.357368Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Preprocessing the sample_id\ndf[\"sample_id\"] = df[\"sample_id\"].str.split(\"__\").str[0]\ngroup_cols = [\n    \"image_path\", \"Sampling_Date\", \"State\", \"Species\",\n    \"Pre_GSHH_NDVI\", \"Height_Ave_cm\"\n]\n\n# Group by and calculate the mean for targets\nmerged_df = df.groupby(group_cols + [\"target_name\"], as_index=False)[\"target\"].mean()\nprint(merged_df)","metadata":{"_uuid":"c9089dff-a9dc-4b86-9d3b-303e5175ac07","_cell_guid":"3b19d0c4-4535-4e43-ae90-d6256cfe0799","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2025-12-11T19:49:21.359341Z","iopub.execute_input":"2025-12-11T19:49:21.359658Z","iopub.status.idle":"2025-12-11T19:49:21.384007Z","shell.execute_reply.started":"2025-12-11T19:49:21.359639Z","shell.execute_reply":"2025-12-11T19:49:21.383308Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Pivot the table to create separate columns for each target\ndf = df.pivot_table(\n    index=[\"sample_id\", \"image_path\", \"Sampling_Date\", \"State\", \"Species\", \"Pre_GSHH_NDVI\", \"Height_Ave_cm\"],\n    columns=\"target_name\",   # becomes new columns: Dry_Clover_g, ...\n    values=\"target\"         # values that fill those columns\n).reset_index()","metadata":{"_uuid":"e8dd705d-3ab8-4ea2-9e64-c17130b838d0","_cell_guid":"1079cda2-e3a8-46ad-bc45-b75e82030196","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2025-12-11T19:49:21.384701Z","iopub.execute_input":"2025-12-11T19:49:21.384915Z","iopub.status.idle":"2025-12-11T19:49:21.408375Z","shell.execute_reply.started":"2025-12-11T19:49:21.384899Z","shell.execute_reply":"2025-12-11T19:49:21.407832Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Checking the new structure of the data\ndf.columns.tolist()\ndf.head()","metadata":{"_uuid":"1b3c3ef8-5c27-49aa-8935-9b4f0aebd5e3","_cell_guid":"0465cc6b-7a95-4968-98c0-03fd8eaaa199","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2025-12-11T19:49:21.409896Z","iopub.execute_input":"2025-12-11T19:49:21.410119Z","iopub.status.idle":"2025-12-11T19:49:21.421093Z","shell.execute_reply.started":"2025-12-11T19:49:21.410101Z","shell.execute_reply":"2025-12-11T19:49:21.420534Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport numpy as np\nimport pandas as pd\nfrom PIL import Image\n\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import Dataset, DataLoader\n\nfrom torchvision import transforms, models\n\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.preprocessing import StandardScaler, OneHotEncoder\nfrom sklearn.compose import ColumnTransformer\nfrom sklearn.pipeline import Pipeline","metadata":{"_uuid":"cc0bb626-c57c-430c-b324-7f1a0945e8ee","_cell_guid":"8800342e-1330-4466-92a5-9b58a8db5c07","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2025-12-11T19:49:21.421744Z","iopub.execute_input":"2025-12-11T19:49:21.421960Z","iopub.status.idle":"2025-12-11T19:49:28.326617Z","shell.execute_reply.started":"2025-12-11T19:49:21.421935Z","shell.execute_reply":"2025-12-11T19:49:28.325829Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Print dataset columns and shape for reference\nprint(\"Columns:\", df.columns.tolist())\nprint(\"Shape:\", df.shape)\n\n# Check for missing values and get a summary\nprint(\"\\nMissing values per column:\")\nprint(df.isna().sum())\n\nprint(\"\\nNumeric summary:\")\nprint(df.describe())","metadata":{"_uuid":"a79a55f1-e223-46e9-a1c8-a96e787dc601","_cell_guid":"63ed6958-cd3e-489c-8f82-da024bc27baa","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2025-12-11T19:49:28.327461Z","iopub.execute_input":"2025-12-11T19:49:28.328357Z","iopub.status.idle":"2025-12-11T19:49:28.350416Z","shell.execute_reply.started":"2025-12-11T19:49:28.328332Z","shell.execute_reply":"2025-12-11T19:49:28.349686Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Split the dataset into training and validation\ntrain_df, val_df = train_test_split(\n    df,\n    test_size=0.2,\n    random_state=42\n)\n\nprint(\"Train size:\", train_df.shape)\nprint(\"Val size:\", val_df.shape)\n\ntarget_cols = [\"Dry_Clover_g\", \"Dry_Dead_g\", \"Dry_Green_g\", \"Dry_Total_g\", \"GDM_g\"]","metadata":{"_uuid":"fb354d8c-5bc4-4d7d-9764-eb828fc04151","_cell_guid":"e0e290f8-91f7-419b-b16e-5fc5899cde55","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2025-12-11T19:49:28.351236Z","iopub.execute_input":"2025-12-11T19:49:28.351518Z","iopub.status.idle":"2025-12-11T19:49:28.357439Z","shell.execute_reply.started":"2025-12-11T19:49:28.351473Z","shell.execute_reply":"2025-12-11T19:49:28.356745Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Define the numeric and categorical features\nnum_features = [\"Pre_GSHH_NDVI\", \"Height_Ave_cm\"]\ncat_features = [\"State\", \"Species\"]\n\n# Column transformer to scale numeric and one-hot encode categorical features\npreprocessor = ColumnTransformer(\n    transformers=[\n        (\"num\", StandardScaler(), num_features),\n        (\"cat\", OneHotEncoder(handle_unknown=\"ignore\"), cat_features),\n    ]\n)\n\n# Fit the preprocessor on the training data only\nX_train_meta = train_df[num_features + cat_features]\nX_val_meta = val_df[num_features + cat_features]\n\npreprocessor.fit(X_train_meta)\n\n# Transform metadata to numpy arrays\nX_train_tab = preprocessor.transform(X_train_meta).toarray()\nX_val_tab = preprocessor.transform(X_val_meta).toarray()\n\nprint(\"Train tabular shape:\", X_train_tab.shape)\nprint(\"Val tabular shape:\", X_val_tab.shape)\n\n# Convert targets to numpy arrays\ny_train = train_df[target_cols].to_numpy(dtype=np.float32)\ny_val = val_df[target_cols].to_numpy(dtype=np.float32)","metadata":{"_uuid":"652629c2-c46e-41b6-882f-4b778f324688","_cell_guid":"09e4920e-deb2-4531-a5c0-f625c1ab4013","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2025-12-11T19:49:28.358207Z","iopub.execute_input":"2025-12-11T19:49:28.358431Z","iopub.status.idle":"2025-12-11T19:49:28.394620Z","shell.execute_reply.started":"2025-12-11T19:49:28.358416Z","shell.execute_reply":"2025-12-11T19:49:28.393784Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Define image transformations for training and validation\nIMAGE_SIZE = 224  # You can change this to 256 or 384 if needed\n\ntrain_transform = transforms.Compose([\n    transforms.Resize((IMAGE_SIZE, IMAGE_SIZE)),\n    transforms.RandomHorizontalFlip(),\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],  # ImageNet means\n        std=[0.229, 0.224, 0.225]    # ImageNet stds\n    ),\n])\n\nval_transform = transforms.Compose([\n    transforms.Resize((IMAGE_SIZE, IMAGE_SIZE)),\n    transforms.ToTensor(),\n    transforms.Normalize(\n        mean=[0.485, 0.456, 0.406],\n        std=[0.229, 0.224, 0.225]\n    ),\n])","metadata":{"_uuid":"26a55170-80de-436a-ba77-45b04a2b8716","_cell_guid":"decbca90-d4c2-43ab-b2ac-0ac7e8c58c75","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2025-12-11T19:49:28.395265Z","iopub.execute_input":"2025-12-11T19:49:28.395425Z","iopub.status.idle":"2025-12-11T19:49:28.400590Z","shell.execute_reply.started":"2025-12-11T19:49:28.395413Z","shell.execute_reply":"2025-12-11T19:49:28.399651Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Define the root directory for image paths\nIMG_ROOT = \"/kaggle/input/csiro-biomass/\"  # Adjust this path as needed\nIMAGE_COL = \"image_path\"","metadata":{"_uuid":"02016dc6-b618-4822-9d18-f787b4d47359","_cell_guid":"bae1c818-05da-49e9-9f0d-a75b71bb05fa","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2025-12-11T19:49:28.402835Z","iopub.execute_input":"2025-12-11T19:49:28.403026Z","iopub.status.idle":"2025-12-11T19:49:28.414301Z","shell.execute_reply.started":"2025-12-11T19:49:28.403012Z","shell.execute_reply":"2025-12-11T19:49:28.413561Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Define the Dataset class for biomass data\nclass BiomassDataset(Dataset):\n    def __init__(self, df, tabular_array, targets_array, transform=None, img_root=\".\", image_col=\"image_path\"):\n        self.df = df.reset_index(drop=True)\n        self.tabular = tabular_array\n        self.targets = targets_array\n        self.transform = transform\n        self.img_root = img_root\n        self.image_col = image_col\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        img_rel_path = row[self.image_col]\n\n        img_path = img_rel_path\n        # If image_path is NOT full path, join with root\n        if not os.path.isabs(img_rel_path):\n            img_path = os.path.join(self.img_root, img_rel_path)\n\n        # Load image\n        image = Image.open(img_path).convert(\"RGB\")\n        if self.transform is not None:\n            image = self.transform(image)\n\n        # Tabular data and targets\n        tab = self.tabular[idx]\n        y = self.targets[idx]\n\n        tab = torch.tensor(tab, dtype=torch.float32)\n        y = torch.tensor(y, dtype=torch.float32)\n\n        return image, tab, y","metadata":{"_uuid":"9d434b7b-df61-45be-a466-7fa20dc62c47","_cell_guid":"e6f91756-c46b-461b-bb15-cf5c76cecd37","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2025-12-11T19:49:28.415091Z","iopub.execute_input":"2025-12-11T19:49:28.415368Z","iopub.status.idle":"2025-12-11T19:49:28.430056Z","shell.execute_reply.started":"2025-12-11T19:49:28.415347Z","shell.execute_reply":"2025-12-11T19:49:28.429351Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Set batch size for training and validation\nBATCH_SIZE = 16\n\n# Create datasets and dataloaders for training and validation\ntrain_dataset = BiomassDataset(\n    df=train_df,\n    tabular_array=X_train_tab,\n    targets_array=y_train,\n    transform=train_transform,\n    img_root=IMG_ROOT,\n    image_col=IMAGE_COL\n)\n\nval_dataset = BiomassDataset(\n    df=val_df,\n    tabular_array=X_val_tab,\n    targets_array=y_val,\n    transform=val_transform,\n    img_root=IMG_ROOT,\n    image_col=IMAGE_COL\n)\n\ntrain_loader = DataLoader(train_dataset, batch_size=BATCH_SIZE, shuffle=True, num_workers=2)\nval_loader = DataLoader(val_dataset, batch_size=BATCH_SIZE, shuffle=False, num_workers=2)","metadata":{"_uuid":"baa432bb-e3a2-4714-acc2-4e23feab7fea","_cell_guid":"a0327456-d273-4a18-adb7-4877c7ddeda0","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2025-12-11T19:49:28.430747Z","iopub.execute_input":"2025-12-11T19:49:28.431005Z","iopub.status.idle":"2025-12-11T19:49:28.444524Z","shell.execute_reply.started":"2025-12-11T19:49:28.430990Z","shell.execute_reply":"2025-12-11T19:49:28.443884Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Set the device (CUDA if available, otherwise CPU)\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nprint(\"Using device:\", device)","metadata":{"_uuid":"1337db1d-fbdb-4932-942c-4a6a65d69395","_cell_guid":"3abfb5b2-dbb4-41b7-b940-0284040d80ed","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2025-12-11T19:49:28.445167Z","iopub.execute_input":"2025-12-11T19:49:28.445419Z","iopub.status.idle":"2025-12-11T19:49:28.510329Z","shell.execute_reply.started":"2025-12-11T19:49:28.445387Z","shell.execute_reply":"2025-12-11T19:49:28.509732Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\nfrom torchvision.models import convnext_tiny, ConvNeXt_Tiny_Weights\nimport torch\nimport torch.nn as nn\n\n# ONE model that works with or without tabular data\nclass BiomassNet(nn.Module):\n    def __init__(self, tabular_dim, num_targets=5):\n        super().__init__()\n\n        # ---- ConvNeXt Backbone ----\n        backbone = convnext_tiny(weights=None)\n        state_dict = torch.load(\"/kaggle/input/convnext-tiny-imagenet1k-v1pt/convnext_tiny_imagenet1k_v1.pt\")\n        backbone.load_state_dict(state_dict)\n\n        self.feature_extractor = nn.Sequential(\n            backbone.features,\n            backbone.avgpool\n        )\n        self.img_features = 768\n\n        # ---- Tabular MLP ----\n        self.tab_mlp = nn.Sequential(\n            nn.Linear(tabular_dim, 64),\n            nn.ReLU(),\n            nn.Linear(64, 32),\n            nn.ReLU()\n        )\n\n        # ---- Shared feature size ----\n        combined_dim = self.img_features + 32\n\n        # ---- Multi-head: one small head per output ----\n        def make_head():\n            return nn.Sequential(\n                nn.Linear(combined_dim, 128),\n                nn.ReLU(),\n                nn.Dropout(0.3),\n                nn.Linear(128, 1)\n            )\n\n        self.heads = nn.ModuleList([make_head() for _ in range(num_targets)])\n\n    def forward(self, x_img, x_tab=None):\n        # --- Image branch ---\n        img_feat = self.feature_extractor(x_img)\n        img_feat = torch.flatten(img_feat, 1)  # (B, 768)\n\n        # --- Tabular fallback ---\n        if x_tab is None:\n            x_tab = torch.zeros(\n                img_feat.shape[0], \n                self.tab_mlp[0].in_features,\n                device=img_feat.device,\n                dtype=img_feat.dtype\n            )\n\n        tab_feat = self.tab_mlp(x_tab)        # (B, 32)\n\n        # --- Combine ---\n        combined = torch.cat([img_feat, tab_feat], dim=1)\n\n        # --- Multi-head outputs ---\n        outputs = [head(combined) for head in self.heads]  # list of (B,1)\n\n        return torch.cat(outputs, dim=1)  # (B, 5)","metadata":{"_uuid":"33096655-13d9-4827-8d20-971f44b2db6c","_cell_guid":"d8f3b296-4e29-4d2d-82f6-3a98284cee9c","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2025-12-11T19:49:28.511164Z","iopub.execute_input":"2025-12-11T19:49:28.511387Z","iopub.status.idle":"2025-12-11T19:49:28.530979Z","shell.execute_reply.started":"2025-12-11T19:49:28.511367Z","shell.execute_reply":"2025-12-11T19:49:28.530284Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Instantiate the model and move it to the selected device\n# Replace your old model with this smart one\ntabular_dim = X_train_tab.shape[1]\nmodel = BiomassNet(tabular_dim=tabular_dim, num_targets=len(target_cols))\nmodel = model.to(device)","metadata":{"_uuid":"1f0e26e1-eaba-4d56-abc2-e03069b014a2","_cell_guid":"b0f85513-5f37-4f1d-b8d8-50eac5862178","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2025-12-11T19:49:28.531665Z","iopub.execute_input":"2025-12-11T19:49:28.531893Z","iopub.status.idle":"2025-12-11T19:49:30.291228Z","shell.execute_reply.started":"2025-12-11T19:49:28.531877Z","shell.execute_reply":"2025-12-11T19:49:30.290231Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Define the training function for one epoch\ndef train_one_epoch(model, dataloader, optimizer, criterion, device):\n    model.train()\n    running_loss = 0.0\n\n    for images, tabs, targets in dataloader:\n        images = images.to(device)\n        tabs = tabs.to(device)\n        targets = targets.to(device)\n\n        optimizer.zero_grad()\n        outputs = model(images, tabs)\n        loss = criterion(outputs, targets)\n        loss.backward()\n        optimizer.step()\n\n        running_loss += loss.item() * images.size(0)\n\n    epoch_loss = running_loss / len(dataloader.dataset)\n    return epoch_loss","metadata":{"_uuid":"e2cbfe86-6a90-4c61-a0dc-b0b131192a01","_cell_guid":"82484dd9-97fa-47af-b7c7-9c34de6e1255","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2025-12-11T19:49:30.292206Z","iopub.execute_input":"2025-12-11T19:49:30.292574Z","iopub.status.idle":"2025-12-11T19:49:30.299775Z","shell.execute_reply.started":"2025-12-11T19:49:30.292552Z","shell.execute_reply":"2025-12-11T19:49:30.299006Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from sklearn.metrics import r2_score\nimport numpy as np\nimport torch\n\ndef evaluate(model, dataloader, criterion, device):\n    model.eval()\n    all_targets = []\n    all_preds = []\n    all_losses = []\n\n    with torch.no_grad():\n        for data in dataloader:\n            images, tabs, targets = data\n            images, tabs, targets = images.to(device), tabs.to(device), targets.to(device)\n\n            # Forward pass\n            outputs = model(images, tabs)\n            loss = criterion(outputs, targets)\n            all_losses.append(loss.item() * images.size(0))  # Accumulate loss\n            \n            all_targets.append(targets.cpu().numpy())\n            all_preds.append(outputs.cpu().numpy())\n\n    # Flatten the lists to 1D arrays for r2_score\n    all_targets = np.concatenate(all_targets)\n    all_preds = np.concatenate(all_preds)\n\n    # Calculate R² score (standard, not weighted)\n    global_r2 = r2_score(all_targets, all_preds)\n\n    # Calculate validation loss\n    val_loss = np.sum(all_losses) / len(dataloader.dataset)\n\n    # Calculate R² score per target (optional)\n    val_r2_per_target = [r2_score(all_targets[:, i], all_preds[:, i]) for i in range(all_targets.shape[1])]\n\n    return val_loss, val_r2_per_target, global_r2","metadata":{"_uuid":"f4cde4d7-89a9-439b-883e-3ffda1b75bc4","_cell_guid":"45e5dc6e-6d29-4a46-9605-bbe60dbd9581","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2025-12-11T19:49:30.301695Z","iopub.execute_input":"2025-12-11T19:49:30.301874Z","iopub.status.idle":"2025-12-11T19:49:30.309895Z","shell.execute_reply.started":"2025-12-11T19:49:30.301860Z","shell.execute_reply":"2025-12-11T19:49:30.308892Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Use Mean Absolute Error (MAE) as the criterion for training\ncriterion = nn.SmoothL1Loss(beta=5.0) # MAE\noptimizer = torch.optim.Adam(model.parameters(), lr=1e-4)","metadata":{"_uuid":"fd6e3346-1ef7-4ae9-89e6-5822fb89186a","_cell_guid":"e3b55b84-3fea-4921-a9af-219ee84aebc1","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2025-12-11T19:49:30.310577Z","iopub.execute_input":"2025-12-11T19:49:30.310833Z","iopub.status.idle":"2025-12-11T19:49:30.335320Z","shell.execute_reply.started":"2025-12-11T19:49:30.310818Z","shell.execute_reply":"2025-12-11T19:49:30.334640Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Training loop (epochs)\nEPOCHS = 70  # start small, see if it works\n\nfor epoch in range(1, EPOCHS + 1):\n    train_loss = train_one_epoch(model, train_loader, optimizer, criterion, device)\n    val_loss, val_r2_per_target, global_r2 = evaluate(model, val_loader, criterion, device)\n\n    print(f\"Epoch {epoch:02d} | \"\n          f\"Train Loss: {train_loss:.4f} | \"\n          f\"Val Loss: {val_loss:.4f} | \"\n          f\"Global R²: {global_r2:.4f}\")\n\n    for i, t in enumerate(target_cols):\n        print(f\"   Val R² {t}: {val_r2_per_target[i]:.4f}\")","metadata":{"_uuid":"de5bc7ef-9d08-44ec-8c54-1ad026ed96db","_cell_guid":"27ca5021-c523-4bae-921a-fc743ca84d6a","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2025-12-11T19:49:30.336218Z","iopub.execute_input":"2025-12-11T19:49:30.336625Z","iopub.status.idle":"2025-12-11T19:49:42.229289Z","shell.execute_reply.started":"2025-12-11T19:49:30.336593Z","shell.execute_reply":"2025-12-11T19:49:42.228310Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ======================================\n#  SYNTHETIC TABULAR PRIORS FOR TEST SET\n# ======================================\n\nmedian_ndvi = train_df[\"Pre_GSHH_NDVI\"].median()\nmedian_height = train_df[\"Height_Ave_cm\"].median()\nmode_state = train_df[\"State\"].mode()[0]\nmode_species = train_df[\"Species\"].mode()[0]\n\nmeta = pd.DataFrame({\n    \"Pre_GSHH_NDVI\": [median_ndvi],\n    \"Height_Ave_cm\": [median_height],\n    \"State\": [mode_state],\n    \"Species\": [mode_species]\n})\n\n# Transform through your pipeline → correct dimension and encoding\nsynthetic_tab = preprocessor.transform(meta).toarray()\n\nprint(\"Synthetic tabular priors (1 row):\")\nprint(meta)\nprint(\"Encoded shape:\", synthetic_tab.shape)\n","metadata":{"_uuid":"85f81e50-1a22-4ae2-8109-f2aac342d4bd","_cell_guid":"b60e2f6f-1254-4df5-a6f7-436a3bc1ea58","trusted":true,"collapsed":false,"execution":{"iopub.status.busy":"2025-12-11T19:59:33.382180Z","iopub.execute_input":"2025-12-11T19:59:33.382830Z","iopub.status.idle":"2025-12-11T19:59:33.396343Z","shell.execute_reply.started":"2025-12-11T19:59:33.382804Z","shell.execute_reply":"2025-12-11T19:59:33.395527Z"},"jupyter":{"outputs_hidden":false}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =======================================================\n#  FINAL WORKING PREDICTION FUNCTION (with tabular priors)\n# =======================================================\n\ndef predict(model, df_rows, device, use_tabular=True):\n    model.eval()\n    \n    # Ensure DataFrame\n    if isinstance(df_rows, pd.Series):\n        df_rows = df_rows.to_frame().T\n\n    # --------------------------------\n    # 1. Load and preprocess images\n    # --------------------------------\n    img_list = []\n    for _, row in df_rows.iterrows():\n        img_path = os.path.join(IMG_ROOT, row[\"image_path\"])\n        img = Image.open(img_path).convert(\"RGB\")\n        img = val_transform(img)\n        img_list.append(img)\n    \n    images = torch.stack(img_list).to(device)\n\n    # --------------------------------\n    # 2. Build tabular tensor\n    # --------------------------------\n    if use_tabular:\n        # If metadata missing in df_rows → use synthetic priors\n        expected_cols = num_features + cat_features\n        \n        if not all(col in df_rows.columns for col in expected_cols):\n            # Repeat synthetic vector for batch size\n            tabular_np = np.repeat(synthetic_tab, len(df_rows), axis=0)\n        else:\n            # Real metadata\n            tabular_np = preprocessor.transform(df_rows[expected_cols]).toarray()\n        \n        tabular_tensor = torch.tensor(tabular_np, dtype=torch.float32).to(device)\n\n    else:\n        tabular_tensor = None\n\n    # --------------------------------\n    # 3. Forward pass\n    # --------------------------------\n    with torch.no_grad():\n        outputs = model(images, x_tab=tabular_tensor)\n\n    # --------------------------------\n    # 4. Return DataFrame\n    # --------------------------------\n    preds = pd.DataFrame(\n        outputs.cpu().numpy(),\n        columns=target_cols,\n        index=df_rows.index\n    )\n    return preds\n","metadata":{"_uuid":"39b3132d-37aa-4c4d-aecc-5cae2b0a562a","_cell_guid":"cb571a87-a7c0-4445-9497-48347d7f3a45","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2025-12-11T19:59:48.261414Z","iopub.execute_input":"2025-12-11T19:59:48.261732Z","iopub.status.idle":"2025-12-11T19:59:48.269163Z","shell.execute_reply.started":"2025-12-11T19:59:48.261710Z","shell.execute_reply":"2025-12-11T19:59:48.268449Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 1. Single sample — WITH tabular data (highest accuracy)\nsingle_pred_full = predict(model, val_df.iloc[0], device, use_tabular=True)\nprint(\"With tabular data (best):\")\nprint(single_pred_full.round(2))","metadata":{"_uuid":"4246422c-dac4-4cd0-97bd-4b877674471c","_cell_guid":"0997dd89-c52b-466c-a876-5f943a4290db","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2025-12-11T20:00:17.684048Z","iopub.execute_input":"2025-12-11T20:00:17.684665Z","iopub.status.idle":"2025-12-11T20:00:17.762248Z","shell.execute_reply.started":"2025-12-11T20:00:17.684636Z","shell.execute_reply":"2025-12-11T20:00:17.761501Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 2. Single sample — IMAGE ONLY\nsingle_img_only = predict(model, val_df.iloc[0], device, use_tabular=False)\nprint(\"Image only:\")\nprint(single_img_only.round(2))","metadata":{"_uuid":"56441267-de0d-4694-b6df-7b9225bd4ef3","_cell_guid":"8dcad54b-5055-4f11-a1b9-d8c165d50ae9","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2025-12-11T20:00:22.605922Z","iopub.execute_input":"2025-12-11T20:00:22.606222Z","iopub.status.idle":"2025-12-11T20:00:22.679586Z","shell.execute_reply.started":"2025-12-11T20:00:22.606202Z","shell.execute_reply":"2025-12-11T20:00:22.678886Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ONE-CELL FINAL SUBMISSION — WORKS FOR ANY NUMBER OF IMAGES\nimport pandas as pd\n\n# Load test file\ntest_df = pd.read_csv(\"/kaggle/input/csiro-biomass/test.csv\")\n\n# Extract sample_id from image path (e.g. \"ID123__Dry_Clover_gtest/ID123.jpg\" → \"ID123\")\ntest_df[\"sample_id\"] = test_df[\"image_path\"].str.split(\"/\").str[1].str.split(\".\").str[0]\n\n# Keep only one row per unique image\ntest_images = test_df.drop_duplicates(\"sample_id\").reset_index(drop=True)\n\nprint(f\"Predicting on {len(test_images)} unique test images...\")\nprint(test_images)","metadata":{"_uuid":"819fe08c-5a50-4b5d-a914-290327ec8faa","_cell_guid":"8ee516a6-3104-4f3c-8c6f-28452af76604","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2025-12-11T20:00:38.462023Z","iopub.execute_input":"2025-12-11T20:00:38.462782Z","iopub.status.idle":"2025-12-11T20:00:38.476337Z","shell.execute_reply.started":"2025-12-11T20:00:38.462753Z","shell.execute_reply":"2025-12-11T20:00:38.475649Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Run image-only prediction (this handles 1 or 10000 images automatically)\ntest_preds = predict(\n    model=model,\n    df_rows=test_images[[\"image_path\"]],\n    device=device,\n    use_tabular=True\n).round(6)\n\nprint(test_preds)","metadata":{"_uuid":"09939357-2bea-4699-aca5-aae67f95d24a","_cell_guid":"bc608702-e1f5-4d6d-a740-142fa577c2c9","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2025-12-11T20:00:44.934660Z","iopub.execute_input":"2025-12-11T20:00:44.935276Z","iopub.status.idle":"2025-12-11T20:00:45.065865Z","shell.execute_reply.started":"2025-12-11T20:00:44.935253Z","shell.execute_reply":"2025-12-11T20:00:45.065187Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Build submission with correct header\nsubmission_list = []\nfor sample_id, pred_row in zip(test_images[\"sample_id\"], test_preds.itertuples(index=False)):\n    for target_name, value in zip(target_cols, pred_row):\n        submission_list.append({\n            \"sample_id\": f\"{sample_id}__{target_name}\",\n            \"target\": float(value)\n        })\n\nsubmission = pd.DataFrame(submission_list)\n\n# Save with correct header\nsubmission.to_csv(\"/kaggle/working/submission.csv\", index=False)\nprint(\"submission.csv created with correct header: sample_id,target\")\nprint(f\"Total rows: {len(submission)} (should be {len(test_images)*5})\")\n\n# Show first 15 lines\nsubmission.head(15)","metadata":{"_uuid":"4781b7b1-952f-40ec-b718-d0d06f254ab5","_cell_guid":"97d075ae-ea8e-491f-8765-fce649a3873e","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2025-12-11T20:00:47.645630Z","iopub.execute_input":"2025-12-11T20:00:47.646178Z","iopub.status.idle":"2025-12-11T20:00:47.662346Z","shell.execute_reply.started":"2025-12-11T20:00:47.646154Z","shell.execute_reply":"2025-12-11T20:00:47.661353Z"}},"outputs":[],"execution_count":null}]}