{"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"}],"dockerImageVersionId":31154,"isInternetEnabled":true,"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-03T17:32:41.385780Z","iopub.execute_input":"2025-11-03T17:32:41.386289Z","iopub.status.idle":"2025-11-03T17:32:58.522969Z","shell.execute_reply.started":"2025-11-03T17:32:41.386263Z","shell.execute_reply":"2025-11-03T17:32:58.522355Z"}},"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-03T17:32:58.524104Z","iopub.execute_input":"2025-11-03T17:32:58.524867Z","iopub.status.idle":"2025-11-03T17:32:58.531222Z","shell.execute_reply.started":"2025-11-03T17:32:58.524846Z","shell.execute_reply":"2025-11-03T17:32:58.530449Z"}},"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        img_path = os.path.join(base_dir, img_path_raw)\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-03T17:32:58.532230Z","iopub.execute_input":"2025-11-03T17:32:58.532475Z","iopub.status.idle":"2025-11-03T17:32:58.550294Z","shell.execute_reply.started":"2025-11-03T17:32:58.532451Z","shell.execute_reply":"2025-11-03T17:32:58.549550Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# quick smoke: load pivoted dataframe as in baseline (expects data/train.csv to be pivotable)\nimport pandas as pd\n\ntrain_df = pd.read_csv('/kaggle/input/csiro-biomass/train.csv')\n# pivot to shape: image_path, Dry_Green_g, Dry_Dead_g, Dry_Clover_g, GDM_g, Dry_Total_g\ndata = train_df[['image_path', 'target_name', 'target']].groupby('image_path')[['image_path', 'target_name', 'target']].apply(lambda x: x).pivot(index='image_path', columns='target_name', values='target').reset_index()\ndata = data[[ 'image_path' ] + TARGET_NAMES]  # ensure order\ndata.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-03T17:32:58.552013Z","iopub.execute_input":"2025-11-03T17:32:58.552626Z","iopub.status.idle":"2025-11-03T17:32:58.906334Z","shell.execute_reply.started":"2025-11-03T17:32:58.552606Z","shell.execute_reply":"2025-11-03T17:32:58.905452Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Example small dataloader (use a small subset for quick debugging)\nfrom sklearn.model_selection import train_test_split\ntrain_df_small, val_df_small = train_test_split(data, test_size=0.1, random_state=42)\ntrain_dataset = DatasetV1(train_df_small, root='data')\nval_dataset = DatasetV1(val_df_small, root='data')\ntrain_loader = DataLoader(train_dataset, batch_size=8, shuffle=True, num_workers=2, pin_memory=True)\nval_loader = DataLoader(val_dataset, batch_size=8, shuffle=False, num_workers=2, pin_memory=True)\nnext(iter(train_loader))[0].shape  # (batch, 4, C, H, W)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-03T17:32:58.907004Z","iopub.execute_input":"2025-11-03T17:32:58.907241Z","iopub.status.idle":"2025-11-03T17:33:00.967635Z","shell.execute_reply.started":"2025-11-03T17:32:58.907223Z","shell.execute_reply":"2025-11-03T17:33:00.966814Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"next(iter(train_loader))[0].shape\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-03T17:33:00.968814Z","iopub.execute_input":"2025-11-03T17:33:00.969164Z","iopub.status.idle":"2025-11-03T17:33:02.639290Z","shell.execute_reply.started":"2025-11-03T17:33:00.969128Z","shell.execute_reply":"2025-11-03T17:33:02.638547Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nimport torch.nn as nn\nimport timm\n\nTARGET_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\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-03T17:33:02.640382Z","iopub.execute_input":"2025-11-03T17:33:02.640931Z","iopub.status.idle":"2025-11-03T17:33:02.650955Z","shell.execute_reply.started":"2025-11-03T17:33:02.640902Z","shell.execute_reply":"2025-11-03T17:33:02.650265Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nimport torch.nn as nn\n\nclass 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()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-03T17:33:02.651865Z","iopub.execute_input":"2025-11-03T17:33:02.652513Z","iopub.status.idle":"2025-11-03T17:33:02.669518Z","shell.execute_reply.started":"2025-11-03T17:33:02.652482Z","shell.execute_reply":"2025-11-03T17:33:02.668880Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class LitMultiPatch(pl.LightningModule):\n    def __init__(self, backbone_name='efficientnet_b2', 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=True)\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}\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-03T17:33:02.670173Z","iopub.execute_input":"2025-11-03T17:33:02.670335Z","iopub.status.idle":"2025-11-03T17:33:02.686734Z","shell.execute_reply.started":"2025-11-03T17:33:02.670322Z","shell.execute_reply":"2025-11-03T17:33:02.686176Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Minimal training example (small subset). Adjust epochs / batch size for real runs.\nimport numpy as np\nnp.random.seed(42)\ntorch.manual_seed(42)\n\nfrom pytorch_lightning.callbacks import ModelCheckpoint\n\ncheckpoint_callback = ModelCheckpoint(\n    monitor='val_weighted_mse', \n    mode='min',                \n    save_top_k=1,               \n    save_weights_only=True,      \n    filename='best-model-{epoch:02d}-{val_weighted_mse:.4f}'\n)\n\n\nmodel = LitMultiPatch(backbone_name='efficientnetv2_rw_m.agc_in1k')\ntrainer = pl.Trainer(\n    max_epochs=30,\n    accelerator='gpu',\n    deterministic=True,\n    log_every_n_steps=10,\n    callbacks=[checkpoint_callback]\n)\n\ntrainer.fit(model, train_loader, val_loader)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-03T17:33:02.688921Z","iopub.execute_input":"2025-11-03T17:33:02.689155Z","iopub.status.idle":"2025-11-03T17:52:51.542747Z","shell.execute_reply.started":"2025-11-03T17:33:02.689133Z","shell.execute_reply":"2025-11-03T17:52:51.541783Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"test_df = pd.read_csv('/kaggle/input/csiro-biomass/test.csv')\ntest_df['target'] = 0\n# pivot to one row per image_path (even though test targets are unknown, keep the structure)\ntest_data = (\n    test_df[['image_path', 'target_name', 'target']]\n    .drop_duplicates()\n    .pivot_table(index='image_path', columns='target_name', values='target')\n    .reset_index()\n)\n\n# ensure same column order as training TARGET_NAMES\ntest_data = test_data[['image_path'] + TARGET_NAMES]\ntest_data","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-03T17:52:51.543822Z","iopub.execute_input":"2025-11-03T17:52:51.544081Z","iopub.status.idle":"2025-11-03T17:52:51.574789Z","shell.execute_reply.started":"2025-11-03T17:52:51.544039Z","shell.execute_reply":"2025-11-03T17:52:51.573915Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"test_dataset = DatasetV1(test_data)\nimg, _ = next(iter(test_dataset))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-03T18:00:34.742623Z","iopub.execute_input":"2025-11-03T18:00:34.742935Z","iopub.status.idle":"2025-11-03T18:00:34.816712Z","shell.execute_reply.started":"2025-11-03T18:00:34.742912Z","shell.execute_reply":"2025-11-03T18:00:34.816100Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!ls /kaggle/working/lightning_logs/version_0/checkpoints","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-03T18:00:44.192378Z","iopub.execute_input":"2025-11-03T18:00:44.193119Z","iopub.status.idle":"2025-11-03T18:00:44.388765Z","shell.execute_reply.started":"2025-11-03T18:00:44.193064Z","shell.execute_reply":"2025-11-03T18:00:44.387995Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\n\n# Put model in evaluation mode\nmodel.eval()\nmodel.to('cuda')  # or 'cpu' if no GPU\n\n# Prepare your image batch (1 sample)\n# img here is patches_stacked of shape (4, C, H, W)\n# Model expects (B, 4, C, H, W)\nx = img.unsqueeze(0).to('cuda')  # shape: (1, 4, 3, H, W)\n\n# Inference (no gradient tracking)\nwith torch.no_grad():\n    preds = model(x)\n\n# preds → tensor of shape (1, 5)\npreds = preds.squeeze(0).cpu().numpy()\n\nprint(\"Predicted values:\")\nfor name, val in zip(TARGET_NAMES, preds):\n    print(f\"{name}: {val:.4f}\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-03T18:10:10.098249Z","iopub.execute_input":"2025-11-03T18:10:10.098789Z","iopub.status.idle":"2025-11-03T18:10:10.269667Z","shell.execute_reply.started":"2025-11-03T18:10:10.098763Z","shell.execute_reply":"2025-11-03T18:10:10.268957Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"sub = pd.read_csv('/kaggle/input/csiro-biomass/sample_submission.csv')\nfor name, val in zip(TARGET_NAMES, preds):\n    sub.loc[sub['sample_id'] == f'ID1001187975__{name}', 'target'] = val\n\nsub.to_csv('submission.csv', index=False)\nsub","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-03T18:06:16.127532Z","iopub.execute_input":"2025-11-03T18:06:16.128200Z","iopub.status.idle":"2025-11-03T18:06:16.144293Z","shell.execute_reply.started":"2025-11-03T18:06:16.128178Z","shell.execute_reply":"2025-11-03T18:06:16.143295Z"}},"outputs":[],"execution_count":null}]}