{"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,"isSourceIdPinned":false,"sourceType":"competition"},{"sourceId":273202973,"sourceType":"kernelVersion"}],"dockerImageVersionId":31154,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# Imports\nimport os\nimport math\nimport numpy as np\nimport pandas as pd\nfrom PIL import Image\nfrom typing import List\n\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import Dataset, DataLoader\nimport torchvision.transforms as transforms\n\nimport timm\nimport pytorch_lightning as pl\n\n# quick availability check\nDEVICE = 'cuda' if torch.cuda.is_available() else 'cpu'\nprint('Device:', DEVICE)","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-11-13T10:13:45.260712Z","iopub.execute_input":"2025-11-13T10:13:45.261178Z","iopub.status.idle":"2025-11-13T10:14:01.933594Z","shell.execute_reply.started":"2025-11-13T10:13:45.261156Z","shell.execute_reply":"2025-11-13T10:14:01.932884Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Constants: target order must match the pivoted dataframe columns\nTARGET_NAMES = ['Dry_Green_g','Dry_Dead_g','Dry_Clover_g','GDM_g','Dry_Total_g']\n# weights for the competition metric and weighted loss\nTARGET_WEIGHTS = torch.tensor([0.1, 0.1, 0.1, 0.2, 0.5], dtype=torch.float32)\n\n# Mean/std used earlier in baseline - keep consistent\nMEAN = [0.485, 0.456, 0.406]\nSTD = [0.229, 0.224, 0.225]\n\n# patch input size for EfficientNet-B2 (a commonly used input is ~260)\nPATCH_SIZE = 256\n\nbase_dir = '/kaggle/input/csiro-biomass'\n\npatch_transform = transforms.Compose([\n    transforms.Resize((PATCH_SIZE, PATCH_SIZE)),\n    transforms.ToTensor(),\n    transforms.Normalize(MEAN, STD),\n])\n\n# train_transform  = transforms.Compose([\n#     transforms.RandomBrightnessContrast(0.1, 0.1),\n#     transforms.HorizontalFlip(p=0.5),\n#     transforms.VerticalFlip(p=0.5),\n#     transforms.ShiftScaleRotate(shift_limit=0.02, scale_limit=0.05, rotate_limit=15, p=0.7),\n#     transforms.GaussianBlur(p=0.1),\n#     transforms.Normalize(),\n#     ToTensorV2()\n# ])\n\n\n# helper: split a PIL image into 4 quadrants (TL, TR, BL, BR)\ndef split_to_quadrants(img: Image.Image) -> List[Image.Image]:\n    w, h = img.size\n    cx, cy = w // 2, h // 2\n    boxes = [\n        (0, 0, cx, cy),      # top-left\n        (cx, 0, w, cy),      # top-right\n        (0, cy, cx, h),      # bottom-left\n        (cx, cy, w, h),      # bottom-right\n    ]\n    return [img.crop(b) for b in boxes]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-13T10:14:01.934634Z","iopub.execute_input":"2025-11-13T10:14:01.935036Z","iopub.status.idle":"2025-11-13T10:14:01.941505Z","shell.execute_reply.started":"2025-11-13T10:14:01.935017Z","shell.execute_reply":"2025-11-13T10:14:01.940741Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class DatasetV1(Dataset):\n    \n    def __init__(self, df: pd.DataFrame, root: str = 'data', transform = patch_transform, testing=False) -> None:\n        super().__init__()\n        self.df = df.reset_index(drop=True)\n        self.root = root\n        self.transform = transform\n        self.testng = testing\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, index):\n        # assume first column is image_path and the next 5 columns are targets in TARGET_NAMES order\n        row = self.df.iloc[index]\n        img_path_raw = row.iloc[0]\n        # print(img_path_raw)\n        img_path = os.path.join(base_dir, img_path_raw)\n        # print(img_path)\n        img = Image.open(img_path).convert('RGB')\n        patches = split_to_quadrants(img)\n        patch_tensors = [self.transform(p) for p in patches]\n        # stack -> (4, C, H, W)\n        patches_stacked = torch.stack(patch_tensors, dim=0)\n        # targets are the remainder of the row (assumes pivoted df)\n        targets = row.iloc[1:].to_numpy().astype(np.float32)\n        return patches_stacked, torch.from_numpy(targets)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-13T10:14:01.942229Z","iopub.execute_input":"2025-11-13T10:14:01.942904Z","iopub.status.idle":"2025-11-13T10:14:01.967489Z","shell.execute_reply.started":"2025-11-13T10:14:01.942876Z","shell.execute_reply":"2025-11-13T10:14:01.966815Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"TARGET_NAMES = ['Dry_Green_g', 'Dry_Dead_g', 'Dry_Clover_g', 'GDM_g', 'Dry_Total_g']\nTARGET_WEIGHTS = torch.tensor([0.1, 0.1, 0.1, 0.2, 0.5])  # global weights\n\n\nclass MultiPatchEffNet(nn.Module):\n    def __init__(self, backbone_name='efficientnet_b2', pretrained=True):\n        super().__init__()\n        self.backbone = timm.create_model(backbone_name, pretrained=pretrained, num_classes=0, global_pool='avg')\n        feat_dim = getattr(self.backbone, 'num_features', None)\n        if feat_dim is None:\n            _tmp = self.backbone.forward_features(torch.zeros(1, 3, PATCH_SIZE, PATCH_SIZE))\n            feat_dim = _tmp.shape[1]\n        total_dim = feat_dim * 4\n\n        self.heads = nn.ModuleDict({\n            'Dry_Green_g': nn.Sequential(nn.Linear(total_dim, 256), nn.Dropout(0.2), nn.ReLU(), nn.Linear(256, 1)),\n            'Dry_Dead_g': nn.Sequential(nn.Linear(total_dim, 256), nn.Dropout(0.2), nn.ReLU(), nn.Linear(256, 1)),\n            'Dry_Clover_g': nn.Sequential(nn.Linear(total_dim, 256), nn.Dropout(0.2), nn.ReLU(), nn.Linear(256, 1)),\n            'GDM_g': nn.Sequential(nn.Linear(total_dim, 384), nn.Dropout(0.2), nn.ReLU(), nn.Linear(384, 1)),  # higher capacity\n            'Dry_Total_g': nn.Sequential(nn.Linear(total_dim, 512), nn.Dropout(0.2), nn.ReLU(), nn.Linear(512, 1)),  # highest capacity\n        })\n\n    def forward(self, patches):\n        # patches: (B, 4, C, H, W)\n        feats = []\n        for i in range(4):\n            p = patches[:, i, :, :, :]  # (B, C, H, W)\n            f = self.backbone(p)\n            feats.append(f)\n        x = torch.cat(feats, dim=1)  # (B, feat_dim * 4)\n        outs = [self.heads[name](x) for name in TARGET_NAMES]\n        preds = torch.cat(outs, dim=1)\n        return preds","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-13T10:14:01.968936Z","iopub.execute_input":"2025-11-13T10:14:01.969127Z","iopub.status.idle":"2025-11-13T10:14:01.987793Z","shell.execute_reply.started":"2025-11-13T10:14:01.969112Z","shell.execute_reply":"2025-11-13T10:14:01.987026Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class WeightedHybridLoss(nn.Module):\n    \"\"\"\n    Weighted loss that applies MAE for top-weighted targets and RMSE for the rest.\n    Example: top 2 weights → MAE, remaining → RMSE\n    \"\"\"\n    def __init__(self, target_weights, topk_mae=2):\n        super().__init__()\n        self.register_buffer('target_weights', target_weights)\n        self.topk_mae = topk_mae\n        \n        # Determine which indices are top-weighted\n        _, self.top_idx = torch.topk(target_weights, topk_mae)\n        self.bottom_idx = torch.tensor(\n            [i for i in range(len(target_weights)) if i not in self.top_idx],\n            dtype=torch.long\n        )\n\n    def forward(self, preds, targets):\n        \"\"\"\n        preds, targets: (B, num_targets)\n        \"\"\"\n        abs_err = torch.abs(preds - targets)\n        sq_err = (preds - targets) ** 2\n\n        mae_term = abs_err[:, self.top_idx]         # (B, topk)\n        rmse_term = torch.sqrt(sq_err[:, self.bottom_idx] + 1e-8)  # (B, rest)\n        \n        # apply target weights\n        mae_weighted = mae_term * self.target_weights[self.top_idx]\n        rmse_weighted = rmse_term * self.target_weights[self.bottom_idx]\n        \n        # combine\n        loss = torch.cat([mae_weighted, rmse_weighted], dim=1)\n        return loss.mean()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-13T10:14:01.988577Z","iopub.execute_input":"2025-11-13T10:14:01.988829Z","iopub.status.idle":"2025-11-13T10:14:02.007869Z","shell.execute_reply.started":"2025-11-13T10:14:01.988806Z","shell.execute_reply":"2025-11-13T10:14:02.007246Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class LitMultiPatch(pl.LightningModule):\n    def __init__(self, backbone_name='efficientnetv2_rw_m.agc_in1k', pretrained=False, lr_backbone=1e-5, lr_head=1e-4, weight_decay=1e-2):\n        super().__init__()\n        self.save_hyperparameters()\n        self.model = MultiPatchEffNet(backbone_name=backbone_name, pretrained=pretrained)\n        self.register_buffer('target_weights', TARGET_WEIGHTS)\n        self.criterion = WeightedHybridLoss(TARGET_WEIGHTS, topk_mae=2)\n\n\n    def forward(self, x):\n        return self.model(x)\n\n    def training_step(self, batch, batch_idx):\n        patches, targets = batch\n        preds = self(patches)\n        loss = self.criterion(preds, targets)\n        self.log('train_weighted_mse', loss, on_step=True, on_epoch=True, prog_bar=True)\n        return loss\n\n    def validation_step(self, batch, batch_idx):\n        patches, targets = batch\n        preds = self(patches)\n        val_loss = self.criterion(preds, targets)\n        wr2 = self.weighted_r2(preds, targets)\n        self.log('val_weighted_mse', val_loss, on_epoch=True, prog_bar=True)\n        self.log('val_weighted_r2', wr2, on_epoch=True, prog_bar=True)\n        return {'val_loss': val_loss, 'val_wr2': wr2}\n\n    def weighted_r2(self, preds, targets):\n        w = self.target_weights\n        w_exp = w.unsqueeze(0).expand_as(preds)\n        y = targets.reshape(-1)\n        yhat = preds.reshape(-1)\n        w_flat = w_exp.reshape(-1)\n        w_sum = w_flat.sum()\n        if w_sum == 0:\n            return torch.tensor(0.0, device=self.device)\n        y_mean = (w_flat * y).sum() / w_sum\n        sse = (w_flat * (y - yhat) ** 2).sum()\n        sst = (w_flat * (y - y_mean) ** 2).sum()\n        if sst == 0:\n            return torch.tensor(0.0, device=self.device)\n        return 1.0 - (sse / sst)\n\n    def configure_optimizers(self):\n        opt = torch.optim.AdamW([\n            {'params': self.model.backbone.parameters(), 'lr': self.hparams.lr_backbone},\n            {'params': self.model.heads.parameters(), 'lr': self.hparams.lr_head}\n        ], weight_decay=self.hparams.weight_decay)\n        sched = torch.optim.lr_scheduler.CosineAnnealingWarmRestarts(opt, T_0=10)\n        return {\"optimizer\": opt, \"lr_scheduler\": sched}","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-13T10:14:02.008619Z","iopub.execute_input":"2025-11-13T10:14:02.008852Z","iopub.status.idle":"2025-11-13T10:14:02.029787Z","shell.execute_reply.started":"2025-11-13T10:14:02.008836Z","shell.execute_reply":"2025-11-13T10:14:02.029067Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def enforce_constraints(pred_vec, name_to_idx):\n    \"\"\"\n    Enforce physical constraints:\n    - Total = Green + Clover + Dead\n    - GDM = Green + Clover\n    \n    Strategy: Trust the 3 primary predictions (Green, Clover, Dead)\n              and calculate Total and GDM from them.\n    \"\"\"\n    green = pred_vec[name_to_idx['Dry_Green_g']]\n    clover = pred_vec[name_to_idx['Dry_Clover_g']]\n    dead = pred_vec[name_to_idx['Dry_Dead_g']]\n    \n    # Recalculate derived targets\n    gdm = green + clover\n    total = green + clover + dead\n    \n    # Create corrected prediction vector\n    corrected = pred_vec.copy()\n    corrected[name_to_idx['GDM_g']] = gdm\n    corrected[name_to_idx['Dry_Total_g']] = total\n    \n    return corrected","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-13T10:31:03.855261Z","iopub.execute_input":"2025-11-13T10:31:03.855794Z","iopub.status.idle":"2025-11-13T10:31:03.860081Z","shell.execute_reply.started":"2025-11-13T10:31:03.855770Z","shell.execute_reply":"2025-11-13T10:31:03.859432Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport torch\nimport pandas as pd\nimport numpy as np\nfrom PIL import Image\nfrom tqdm import tqdm\nfrom torchvision import transforms\n\n# Load data\ntest_df = pd.read_csv(\"/kaggle/input/csiro-biomass/test.csv\")\nsub = pd.read_csv(\"/kaggle/input/csiro-biomass/sample_submission.csv\")\n\nprint(f\"test df shape: {test_df.shape}\")\n\ndevice = \"cuda\" if torch.cuda.is_available() else \"cpu\"\nckpt_path = \"/kaggle/input/biomass-model-train/lightning_logs/version_0/checkpoints/best-model-epoch=24-val_weighted_mse=2.2038.ckpt\"\nmodel = LitMultiPatch.load_from_checkpoint(ckpt_path, map_location=device)\nmodel.eval().to(device)\nprint(\"model loaded\")\n\n# Define mappings\nname_to_idx = {\n    'Dry_Total_g': 0,\n    'GDM_g': 1,\n    'Dry_Green_g': 2,\n    'Dry_Clover_g': 3,\n    'Dry_Dead_g': 4\n}\n\ndef enforce_constraints(pred_vec, name_to_idx):\n    \"\"\"Enforce physical constraints: Total = G+C+D, GDM = G+C\"\"\"\n    green = pred_vec[name_to_idx['Dry_Green_g']]\n    clover = pred_vec[name_to_idx['Dry_Clover_g']]\n    dead = pred_vec[name_to_idx['Dry_Dead_g']]\n    \n    # Recalculate derived targets\n    gdm = green + clover\n    total = green + clover + dead\n    \n    # Create corrected prediction vector\n    corrected = pred_vec.copy()\n    corrected[name_to_idx['GDM_g']] = gdm\n    corrected[name_to_idx['Dry_Total_g']] = total\n    \n    return corrected\n\ndef predict_image(path):\n    img = Image.open(path).convert(\"RGB\")\n    patches = split_to_quadrants(img)\n    patch_tensors = [patch_transform(p) for p in patches]\n    x = torch.stack(patch_tensors).unsqueeze(0).to(device)\n    with torch.no_grad():\n        pred = model(x).squeeze(0).cpu().numpy()\n    \n    # CRITICAL: Enforce constraints\n    pred = enforce_constraints(pred, name_to_idx)\n    \n    return pred\n\n# Predict\nimage_prediction_cache = {}\npredictions = []\n\nfor idx, row in tqdm(test_df.iterrows(), total=len(test_df)):\n    img_path = os.path.join(\"/kaggle/input/csiro-biomass\", row.image_path)\n    target = row.target_name\n    \n    if img_path not in image_prediction_cache:\n        image_prediction_cache[img_path] = predict_image(img_path)\n    \n    pred_vec = image_prediction_cache[img_path]\n    \n    # Debug first prediction\n    if idx == 0:\n        print(f\"\\n--- First Prediction (After Constraint Enforcement) ---\")\n        print(f\"Green: {pred_vec[name_to_idx['Dry_Green_g']]:.2f}\")\n        print(f\"Clover: {pred_vec[name_to_idx['Dry_Clover_g']]:.2f}\")\n        print(f\"Dead: {pred_vec[name_to_idx['Dry_Dead_g']]:.2f}\")\n        print(f\"GDM: {pred_vec[name_to_idx['GDM_g']]:.2f} (should = Green + Clover)\")\n        print(f\"Total: {pred_vec[name_to_idx['Dry_Total_g']]:.2f} (should = Green + Clover + Dead)\")\n        \n        # Verify\n        g = pred_vec[name_to_idx['Dry_Green_g']]\n        c = pred_vec[name_to_idx['Dry_Clover_g']]\n        d = pred_vec[name_to_idx['Dry_Dead_g']]\n        print(f\"\\nVerification:\")\n        print(f\"Green + Clover = {g + c:.2f}\")\n        print(f\"Green + Clover + Dead = {g + c + d:.2f}\")\n    \n    pred_value = pred_vec[name_to_idx[target]]\n    predictions.append(max(0, float(pred_value)))  # Clip negatives\n\n# Create submission\nsub[\"target\"] = predictions\n\nprint(f\"\\n--- Final Submission ---\")\nprint(sub)\n\nsub.to_csv(\"submission.csv\", index=False)\nprint(\"\\n✓ Submission saved successfully\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-13T10:31:04.139133Z","iopub.execute_input":"2025-11-13T10:31:04.139496Z","iopub.status.idle":"2025-11-13T10:31:05.723311Z","shell.execute_reply.started":"2025-11-13T10:31:04.139473Z","shell.execute_reply":"2025-11-13T10:31:05.722604Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}