{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceId":112509,"databundleVersionId":14254895,"sourceType":"competition"}],"dockerImageVersionId":31259,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import os\nimport pandas as pd\nimport torch\nfrom torch.utils.data import Dataset\nfrom PIL import Image\nimport numpy as np\nimport torch.nn as nn\nimport torchvision.models as models\nfrom torch.optim import AdamW\nfrom sklearn.model_selection import train_test_split\nfrom torch.utils.data import DataLoader\nfrom torchvision import transforms","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2026-01-18T08:02:24.556319Z","iopub.execute_input":"2026-01-18T08:02:24.557178Z","iopub.status.idle":"2026-01-18T08:02:36.265347Z","shell.execute_reply.started":"2026-01-18T08:02:24.557131Z","shell.execute_reply":"2026-01-18T08:02:36.264394Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import random\nimport numpy as np\nimport torch\n\ndef seed_everything(seed=42):\n    random.seed(seed)\n    np.random.seed(seed)\n    torch.manual_seed(seed)\n    torch.cuda.manual_seed_all(seed)\n    torch.backends.cudnn.deterministic = False\n    torch.backends.cudnn.benchmark = True\n\nseed_everything(42)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-18T08:02:36.266704Z","iopub.execute_input":"2026-01-18T08:02:36.267093Z","iopub.status.idle":"2026-01-18T08:02:36.280719Z","shell.execute_reply.started":"2026-01-18T08:02:36.267070Z","shell.execute_reply":"2026-01-18T08:02:36.280117Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"TARGET2IDX = {\n    \"Dry_Green_g\": 0,\n    \"Dry_Dead_g\": 1,\n    \"Dry_Clover_g\": 2,\n    \"GDM_g\": 3,\n    \"Dry_Total_g\": 4,\n}\n\nTARGET_WEIGHTS = torch.tensor(\n    [0.1, 0.1, 0.1, 0.2, 0.5], dtype=torch.float32\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-18T08:02:36.281510Z","iopub.execute_input":"2026-01-18T08:02:36.281825Z","iopub.status.idle":"2026-01-18T08:02:36.285770Z","shell.execute_reply.started":"2026-01-18T08:02:36.281804Z","shell.execute_reply":"2026-01-18T08:02:36.285201Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class BiomassDataset(Dataset):\n    def __init__(self, csv_path, img_dir, transform=None):\n        self.df = pd.read_csv(csv_path)\n        self.image_dir = img_dir\n        self.transform = transform\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n\n        img_path = os.path.join(self.image_dir, row[\"image_path\"])\n        image = Image.open(img_path).convert(\"RGB\")\n\n        if self.transform:\n            image = self.transform(image)\n\n        y = torch.zeros(5, dtype=torch.float32)\n        mask = torch.zeros(5, dtype=torch.float32)\n\n        tidx = TARGET2IDX[row[\"target_name\"]]\n        y[tidx] = torch.log1p(torch.tensor(row[\"target\"], dtype=torch.float32))\n        mask[tidx] = 1.0\n\n        return image, y, mask\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-18T08:02:36.286541Z","iopub.execute_input":"2026-01-18T08:02:36.286804Z","iopub.status.idle":"2026-01-18T08:02:36.302308Z","shell.execute_reply.started":"2026-01-18T08:02:36.286782Z","shell.execute_reply":"2026-01-18T08:02:36.301809Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class GeM(nn.Module):\n    def __init__(self, p=3.0, eps=1e-6):\n        super().__init__()\n        self.p = nn.Parameter(torch.ones(1) * p)\n        self.eps = eps\n\n    def forward(self, x):\n        return torch.mean(\n            x.clamp(min=self.eps).pow(self.p),\n            dim=(-2, -1)\n        ).pow(1.0 / self.p)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-18T08:02:36.303923Z","iopub.execute_input":"2026-01-18T08:02:36.304113Z","iopub.status.idle":"2026-01-18T08:02:36.319150Z","shell.execute_reply.started":"2026-01-18T08:02:36.304095Z","shell.execute_reply":"2026-01-18T08:02:36.318658Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class BiomassModel(nn.Module):\n    def __init__(self, pretrained=True):\n        super().__init__()\n\n        resnet = models.resnet50(\n            weights=models.ResNet50_Weights.IMAGENET1K_V1 if pretrained else None\n        )\n\n        # ---- keep convolutional trunk only ----\n        self.stem = nn.Sequential(\n            resnet.conv1,\n            resnet.bn1,\n            resnet.relu,\n            resnet.maxpool,\n        )\n\n        self.layer1 = resnet.layer1\n        self.layer2 = resnet.layer2\n        self.layer3 = resnet.layer3\n        self.layer4 = resnet.layer4\n\n        self.pool = GeM()\n\n        self.feature_dim = 2048\n\n        self.heads = nn.ModuleList([\n            nn.Sequential(\n                nn.Linear(self.feature_dim, 1),\n                nn.Softplus()\n            )\n            for _ in range(5)\n        ])\n\n    def forward(self, x):\n        x = self.stem(x)\n        x = self.layer1(x)\n        x = self.layer2(x)\n        x = self.layer3(x)\n        x = self.layer4(x)     # [B, 2048, H, W]\n\n        x = self.pool(x)       # [B, 2048]\n\n        outs = [h(x) for h in self.heads]\n        return torch.cat(outs, dim=1)  # log1p space\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-18T08:02:36.320000Z","iopub.execute_input":"2026-01-18T08:02:36.320514Z","iopub.status.idle":"2026-01-18T08:02:36.333992Z","shell.execute_reply.started":"2026-01-18T08:02:36.320485Z","shell.execute_reply":"2026-01-18T08:02:36.333495Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def weighted_masked_mse(pred, target, mask, weights):\n    # pred, target: [B, 5]\n    # mask: [B, 5]\n    # weights: [5]\n\n    loss = (pred - target) ** 2\n    loss = loss * mask * weights.to(pred.device)\n\n    return loss.sum() / (mask * weights.to(pred.device)).sum()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-18T08:02:36.334861Z","iopub.execute_input":"2026-01-18T08:02:36.335084Z","iopub.status.idle":"2026-01-18T08:02:36.353895Z","shell.execute_reply.started":"2026-01-18T08:02:36.335065Z","shell.execute_reply":"2026-01-18T08:02:36.353387Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"DEVICE = \"cuda\" if torch.cuda.is_available() else \"cpu\"\n\nmodel = BiomassModel(pretrained=True).to(DEVICE)\n\nbase_lr = 3e-4\nwarmup_epochs = 2\n\noptimizer = AdamW(\n    model.parameters(),\n    lr=base_lr,\n    weight_decay=1e-4\n)\n\nfor g in optimizer.param_groups:\n    g[\"lr\"] = base_lr * 0.1\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-18T08:02:36.354721Z","iopub.execute_input":"2026-01-18T08:02:36.354976Z","iopub.status.idle":"2026-01-18T08:02:37.746958Z","shell.execute_reply.started":"2026-01-18T08:02:36.354955Z","shell.execute_reply":"2026-01-18T08:02:37.746389Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"@torch.no_grad()\ndef weighted_r2(model, loader):\n    model.eval()\n\n    y_true_log, y_pred_log, wts = [], [], []\n\n    for images, targets, masks in loader:\n        images = images.to(DEVICE)\n        preds = model(images).cpu()\n\n        targets = targets.cpu()\n        masks = masks.cpu()\n\n        for i in range(5):\n            mask_i = masks[:, i] == 1\n            if mask_i.any():\n                y_true_log.append(targets[mask_i, i])\n                y_pred_log.append(preds[mask_i, i])\n                wts.append(torch.full((mask_i.sum(),), TARGET_WEIGHTS[i]))\n\n    y_true_log = torch.cat(y_true_log)\n    y_pred_log = torch.cat(y_pred_log)\n    wts = torch.cat(wts)\n\n    # ---- inverse transform ----\n    y_true = torch.expm1(y_true_log)\n    y_pred = torch.expm1(y_pred_log)\n\n    y_true = torch.clamp(y_true, min=0.0)\n    y_pred = torch.clamp(y_pred, min=0.0)\n\n    y_bar = (wts * y_true).sum() / wts.sum()\n    ss_res = (wts * (y_true - y_pred) ** 2).sum()\n    ss_tot = (wts * (y_true - y_bar) ** 2).sum()\n\n    return 1.0 - ss_res / (ss_tot + 1e-6)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-18T08:02:37.747740Z","iopub.execute_input":"2026-01-18T08:02:37.747924Z","iopub.status.idle":"2026-01-18T08:02:37.754641Z","shell.execute_reply.started":"2026-01-18T08:02:37.747905Z","shell.execute_reply":"2026-01-18T08:02:37.754087Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_tfms = transforms.Compose([\n    transforms.Resize((384, 384)),\n    transforms.RandomHorizontalFlip(p=0.5),\n    transforms.ColorJitter(brightness=0.1, contrast=0.1),\n    transforms.ToTensor(),\n    transforms.Normalize(\n        mean=[0.485, 0.456, 0.406],\n        std=[0.229, 0.224, 0.225]\n    )\n])\n\nval_tfms = transforms.Compose([\n    transforms.Resize((384, 384)),\n    transforms.ToTensor(),\n    transforms.Normalize(\n        mean=[0.485, 0.456, 0.406],\n        std=[0.229, 0.224, 0.225]\n    )\n])\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-18T08:02:37.755595Z","iopub.execute_input":"2026-01-18T08:02:37.755875Z","iopub.status.idle":"2026-01-18T08:02:37.778683Z","shell.execute_reply.started":"2026-01-18T08:02:37.755826Z","shell.execute_reply":"2026-01-18T08:02:37.777954Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"CSV_PATH = \"/kaggle/input/csiro-biomass/train.csv\"\nIMAGE_DIR = \"/kaggle/input/csiro-biomass\"\nBATCH_SIZE = 16\nEPOCHS = 15\nNUM_WORKERS = 4\n\ndf = pd.read_csv(CSV_PATH)\ntrain_df, val_df = train_test_split(\n    df,\n    test_size=0.2,\n    random_state=42,\n    shuffle=True\n)\n\ntrain_df.to_csv(\"train_split.csv\", index=False)\nval_df.to_csv(\"val_split.csv\", index=False)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-18T08:02:37.779655Z","iopub.execute_input":"2026-01-18T08:02:37.780232Z","iopub.status.idle":"2026-01-18T08:02:37.860002Z","shell.execute_reply.started":"2026-01-18T08:02:37.780210Z","shell.execute_reply":"2026-01-18T08:02:37.859509Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_ds = BiomassDataset(\n    csv_path=\"train_split.csv\",\n    img_dir=IMAGE_DIR,\n    transform=train_tfms\n)\n\nval_ds = BiomassDataset(\n    csv_path=\"val_split.csv\",\n    img_dir=IMAGE_DIR,\n    transform=val_tfms\n)\n\ntrain_loader = DataLoader(\n    train_ds,\n    batch_size=BATCH_SIZE,\n    shuffle=True,\n    num_workers=NUM_WORKERS,\n    pin_memory=True\n)\n\nval_loader = DataLoader(\n    val_ds,\n    batch_size=BATCH_SIZE,\n    shuffle=False,\n    num_workers=NUM_WORKERS,\n    pin_memory=True\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-18T08:02:37.860844Z","iopub.execute_input":"2026-01-18T08:02:37.861405Z","iopub.status.idle":"2026-01-18T08:02:37.872751Z","shell.execute_reply.started":"2026-01-18T08:02:37.861382Z","shell.execute_reply":"2026-01-18T08:02:37.872047Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from torch.optim.lr_scheduler import ReduceLROnPlateau\n\nscheduler = ReduceLROnPlateau(\n    optimizer,\n    mode=\"min\",\n    factor=0.5,\n    patience=2,\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-18T08:02:37.873480Z","iopub.execute_input":"2026-01-18T08:02:37.873676Z","iopub.status.idle":"2026-01-18T08:02:37.888259Z","shell.execute_reply.started":"2026-01-18T08:02:37.873656Z","shell.execute_reply":"2026-01-18T08:02:37.887587Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"best_r2 = -1e9\nos.makedirs(\"checkpoints\", exist_ok=True)\n\nfor epoch in range(EPOCHS):\n\n    # ---- warmup ----\n    if epoch < warmup_epochs:\n        lr = base_lr * (epoch + 1) / warmup_epochs\n        for g in optimizer.param_groups:\n            g[\"lr\"] = lr\n\n    model.train()\n    total_loss = 0.0\n    total_w = 0.0\n\n    for images, targets, masks in train_loader:\n        images = images.to(DEVICE)\n        targets = targets.to(DEVICE)\n        masks = masks.to(DEVICE)\n\n        optimizer.zero_grad()\n        preds = model(images)\n\n        loss = weighted_masked_mse(\n            preds, targets, masks, TARGET_WEIGHTS\n        )\n        loss.backward()\n        optimizer.step()\n\n        total_loss += loss.item()\n\n    val_r2 = weighted_r2(model, val_loader)\n    old_lr = optimizer.param_groups[0]['lr']\n    scheduler.step(-val_r2)\n    new_lr = optimizer.param_groups[0]['lr']\n    if old_lr != new_lr:\n        print(f\"LR changed from {old_lr} to {new_lr}\")\n\n    if val_r2 > best_r2:\n        best_r2 = val_r2\n        torch.save(\n            {\n                \"epoch\": epoch,\n                \"model_state\": model.state_dict(),\n                \"val_r2\": val_r2,\n            },\n            \"checkpoints/best_model_new-v1.pt\"\n        )\n\n    print(\n        f\"Epoch {epoch+1:02d} | \"\n        f\"Train Loss: {total_loss:.4f} | \"\n        f\"Val weighted R²: {val_r2:.4f} | \"\n        f\"Best: {best_r2:.4f} | \"\n        f\"LR: {optimizer.param_groups[0]['lr']:.2e}\"\n    )\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-18T08:03:37.488480Z","iopub.execute_input":"2026-01-18T08:03:37.489197Z","iopub.status.idle":"2026-01-18T08:18:09.406759Z","shell.execute_reply.started":"2026-01-18T08:03:37.489163Z","shell.execute_reply":"2026-01-18T08:18:09.405887Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# from collections import defaultdict\n\n# # Load model from best checkpoint\n# checkpoint = torch.load(\"checkpoints/best_model.pt\", map_location=DEVICE)\n# model.load_state_dict(checkpoint[\"model_state\"])\n# model.to(DEVICE)\n# model.eval()\n\n# @torch.no_grad()\n# def compute_per_target_rmse(model, loader):\n#     # Dict mapping task → list of squared errors\n#     sq_errors = defaultdict(list)\n\n#     for images, targets, masks in loader:\n#         images = images.to(DEVICE)\n#         targets = targets.to(DEVICE)\n#         masks = masks.to(DEVICE)\n\n#         preds = model(images)\n\n#         # For each sample in batch\n#         for i in range(preds.shape[0]):\n#             # Find the index of the active target (mask == 1)\n#             task_idx = masks[i].argmax().item()\n\n#             pred_val = preds[i, task_idx].item()\n#             true_val = targets[i, task_idx].item()\n\n#             sq_err = (pred_val - true_val) ** 2\n#             sq_errors[TARGETS[task_idx]].append(sq_err)\n\n#     # Compute RMSE per task\n#     rmse_per_task = {}\n#     for task, errs in sq_errors.items():\n#         rmse_per_task[task] = float(np.sqrt(np.mean(errs)))\n\n#     return rmse_per_task\n\n# # Run it:\n# val_rmse_by_task = compute_per_target_rmse(model, val_loader)\n# print(\"Validation RMSE per target (log-space):\")\n# print(pd.Series(val_rmse_by_task))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-18T08:03:22.244759Z","iopub.status.idle":"2026-01-18T08:03:22.245054Z","shell.execute_reply.started":"2026-01-18T08:03:22.244930Z","shell.execute_reply":"2026-01-18T08:03:22.244947Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"ls ","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}