{"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":"none","dataSources":[{"sourceId":112509,"databundleVersionId":14254895,"sourceType":"competition"}],"dockerImageVersionId":31153,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"\nimport os, sys, math, time, gc, random\nfrom pathlib import Path\nimport warnings\nwarnings.filterwarnings(\"ignore\")\n\n\nimport numpy as np\nimport pandas as pd\nfrom PIL import Image\n\n\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import Dataset, DataLoader\nimport torch.optim as optim\nfrom torch.cuda.amp import autocast, GradScaler\n\n\nimport torchvision.transforms as T\nimport timm  # strong backbone library; if missing: pip install timm\n\n\nfrom sklearn.model_selection import GroupKFold, KFold\nfrom sklearn.linear_model import Ridge\nfrom sklearn.metrics import r2_score\n\n\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nprint(\"Device:\", device)\n","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-11-02T10:59:37.053881Z","iopub.execute_input":"2025-11-02T10:59:37.054210Z","iopub.status.idle":"2025-11-02T10:59:55.589389Z","shell.execute_reply.started":"2025-11-02T10:59:37.054185Z","shell.execute_reply":"2025-11-02T10:59:55.588429Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\nINPUT = Path(\"/kaggle/input/csiro-biomass\")  \nTRAIN_CSV = INPUT / \"train.csv\"\nTEST_CSV = INPUT / \"test.csv\"\nTRAIN_IMG_ROOT = INPUT / \"train\"  \nTEST_IMG_ROOT = INPUT / \"test\"     \nSAMPLE_SUB = INPUT / \"sample_submission.csv\"\n\n\nOUT = Path(\"/kaggle/working\")\nOUT.mkdir(parents=True, exist_ok=True)\n\n\nfor p in [TRAIN_CSV, TEST_CSV, SAMPLE_SUB]:\n    print(p, \"exists?\", p.exists())\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-02T10:59:55.591593Z","iopub.execute_input":"2025-11-02T10:59:55.592034Z","iopub.status.idle":"2025-11-02T10:59:55.601281Z","shell.execute_reply.started":"2025-11-02T10:59:55.592011Z","shell.execute_reply":"2025-11-02T10:59:55.600123Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\nCFG = {\n    \"seed\": 42,\n    \"img_size\": 384,           # 384 or 512 are common choices\n    \"batch_size\": 12,         # tune by GPU memory; lower if OOM\n    \"num_workers\": 2,\n    \"epochs\": 8,              # full training typically 10-25; start small during dev\n    \"lr\": 3e-4,\n    \"weight_decay\": 1e-2,\n    \"backbone\": \"tf_efficientnet_b3_ns\",  # timm model name (strong baseline)\n    \"pretrained\": True,\n    \"n_folds\": 5,\n    \"tta\": True,              # toggle test-time augmentation\n    \"mixup\": False,           # optional MixUp/CutMix for regression (careful)\n    \"use_ndvi\": True,         # attempt to use Pre_GSHH_NDVI column if present (per-image value)\n    \"device\": device\n}\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-02T10:59:55.602365Z","iopub.execute_input":"2025-11-02T10:59:55.602664Z","iopub.status.idle":"2025-11-02T10:59:55.632592Z","shell.execute_reply.started":"2025-11-02T10:59:55.602643Z","shell.execute_reply":"2025-11-02T10:59:55.631628Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\ndef seed_everything(seed=42):\n    random.seed(seed)\n    np.random.seed(seed)\n    torch.manual_seed(seed)\n    if torch.cuda.is_available(): \n        torch.cuda.manual_seed_all(seed)\n    torch.backends.cudnn.deterministic = True\n    torch.backends.cudnn.benchmark = False\n\nseed_everything(CFG[\"seed\"])\n\ndef save_checkpoint(model, path):\n    torch.save(model.state_dict(), path)\n\ndef load_checkpoint(model, path, map_location=\"cpu\"):\n    model.load_state_dict(torch.load(path, map_location=map_location))\n    return model\n\ndef weighted_r2(y_true, y_pred):\n    # y_true, y_pred: shape (N,5)\n    # compute R^2 per target then weighted sum per competition weights\n    weights = np.array([0.1,0.1,0.1,0.2,0.5])\n    r2s = []\n    for i in range(y_true.shape[1]):\n        ss_res = np.sum((y_true[:,i] - y_pred[:,i])**2)\n        ss_tot = np.sum((y_true[:,i] - np.mean(y_true[:,i]))**2)\n        r2 = 1 - ss_res / (ss_tot + 1e-12)\n        r2s.append(r2)\n    r2s = np.array(r2s)\n    return r2s, float((r2s * weights).sum())\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-02T10:59:55.633257Z","iopub.execute_input":"2025-11-02T10:59:55.633496Z","iopub.status.idle":"2025-11-02T10:59:55.651661Z","shell.execute_reply.started":"2025-11-02T10:59:55.633476Z","shell.execute_reply":"2025-11-02T10:59:55.650557Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\ntrain_df = pd.read_csv(TRAIN_CSV)\ntest_df = pd.read_csv(TEST_CSV)\nsample_sub = pd.read_csv(SAMPLE_SUB)\n\nprint(\"train shape (long):\", train_df.shape)\ntrain_df.head(6)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-02T10:59:55.652604Z","iopub.execute_input":"2025-11-02T10:59:55.652873Z","iopub.status.idle":"2025-11-02T10:59:55.718024Z","shell.execute_reply.started":"2025-11-02T10:59:55.652852Z","shell.execute_reply":"2025-11-02T10:59:55.717064Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\ndef extract_image_id(path_str):\n    # examples: images/ID1098771283.jpg  or train/ID...jpg\n    return Path(path_str).stem\n\ntrain_df['image_id'] = train_df['image_path'].apply(extract_image_id)\ntest_df['image_id'] = test_df['image_path'].apply(extract_image_id)\n\n# pivot targets per image\npivot = train_df.pivot(index=['image_id','image_path','Sampling_Date','State','Species','Pre_GSHH_NDVI','Height_Ave_cm'],\n                       columns='target_name', values='target').reset_index()\n\n# rename columns if needed\npivot = pivot.rename_axis(None, axis=1)  # remove name\n    \n    \nprint(\"pivot shape (wide):\", pivot.shape)\npivot.head(3)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-02T10:59:55.719014Z","iopub.execute_input":"2025-11-02T10:59:55.719344Z","iopub.status.idle":"2025-11-02T10:59:55.780305Z","shell.execute_reply.started":"2025-11-02T10:59:55.719321Z","shell.execute_reply":"2025-11-02T10:59:55.779401Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\ndisplay(pivot[['Dry_Green_g','Dry_Dead_g','Dry_Clover_g','GDM_g','Dry_Total_g']].describe().T)\nprint(\"NDVI present?\", 'Pre_GSHH_NDVI' in pivot.columns)\nprint(\"Any NaN targets?\", pivot[['Dry_Green_g','Dry_Dead_g','Dry_Clover_g','GDM_g','Dry_Total_g']].isnull().any().any())\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-02T10:59:55.783719Z","iopub.execute_input":"2025-11-02T10:59:55.784069Z","iopub.status.idle":"2025-11-02T10:59:55.817955Z","shell.execute_reply.started":"2025-11-02T10:59:55.784048Z","shell.execute_reply":"2025-11-02T10:59:55.816958Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\npivot['fold'] = -1\n# try to group by Sampling_Date if it has many unique values, else use State\nif pivot['Sampling_Date'].nunique() >= CFG['n_folds']:\n    gkf = GroupKFold(n_splits=CFG['n_folds'])\n    groups = pivot['Sampling_Date'].values\nelse:\n    print(\"Using State for grouping folds\")\n    gkf = GroupKFold(n_splits=CFG['n_folds'])\n    if 'State' in pivot.columns:\n        groups = pivot['State'].values\n    else:\n        groups = pivot['image_id'].values  # fallback\n\nfor fold, (tr_idx, val_idx) in enumerate(gkf.split(pivot, pivot[['Dry_Green_g']], groups)):\n    pivot.loc[val_idx, 'fold'] = fold\n\npivot['fold'].value_counts()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-02T10:59:55.818757Z","iopub.execute_input":"2025-11-02T10:59:55.819097Z","iopub.status.idle":"2025-11-02T10:59:55.839093Z","shell.execute_reply.started":"2025-11-02T10:59:55.819065Z","shell.execute_reply":"2025-11-02T10:59:55.838038Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\nTARGET_COLS = ['Dry_Green_g','Dry_Dead_g','Dry_Clover_g','GDM_g','Dry_Total_g']\n\nfrom torchvision import transforms\nclass CSIRO_Dataset(Dataset):\n    def __init__(self, df, img_root, transforms=None, is_test=False, use_ndvi=False):\n        self.df = df.reset_index(drop=True)\n        self.img_root = Path(img_root)\n        self.transforms = transforms\n        self.is_test = is_test\n        self.use_ndvi = use_ndvi\n\n    def __len__(self): \n        return len(self.df)\n\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        image_id = row['image_id']\n        # locate file (image_path stored in the original train.csv/test.csv)\n        # fallback to constructs path from image_id\n        # ensure extension jpg\n        # Many train image paths are like images/IDxxx.jpg or train/ID.jpg\n        candidates = [\n            self.img_root / f\"{image_id}.jpg\",\n            self.img_root / f\"{image_id}.jpeg\",\n            self.img_root / f\"{image_id}.png\"\n        ]\n        img_path = None\n        for c in candidates:\n            if c.exists():\n                img_path = c\n                break\n        if img_path is None:\n            # sometimes the path in pivot includes a directory; try using stored image_path column if exists\n            if 'image_path' in row:\n                p = Path(row['image_path'])\n                # try relative to INPUT\n                cand = INPUT / p\n                if cand.exists():\n                    img_path = cand\n        if img_path is None:\n            raise FileNotFoundError(f\"Image not found for {image_id}\")\n\n        img = Image.open(img_path).convert(\"RGB\")\n        if self.transforms:\n            img = self.transforms(img)  # should be tensor CxHxW\n\n        out = {\"image\": img, \"image_id\": image_id}\n        if self.use_ndvi:\n            ndvi = row.get('Pre_GSHH_NDVI', np.nan)\n            if np.isnan(ndvi):\n                ndvi = 0.0\n            out[\"ndvi\"] = torch.tensor([float(ndvi)], dtype=torch.float32)\n\n        if not self.is_test:\n            targets = row[TARGET_COLS].values.astype(np.float32)\n            out[\"targets\"] = torch.tensor(targets, dtype=torch.float32)\n\n        return out\n\n# Transforms\ntrain_transforms = T.Compose([\n    T.RandomResizedCrop((CFG['img_size'], CFG['img_size']), scale=(0.6,1.0)),\n    T.RandomHorizontalFlip(),\n    T.RandomVerticalFlip(),\n    T.ColorJitter(0.2,0.2,0.2,0.1),\n    T.ToTensor(),\n    T.Normalize(mean=[0.485,0.456,0.406], std=[0.229,0.224,0.225]),\n])\n\nvalid_transforms = T.Compose([\n    T.Resize((CFG['img_size'], CFG['img_size'])),\n    T.ToTensor(),\n    T.Normalize(mean=[0.485,0.456,0.406], std=[0.229,0.224,0.225]),\n])\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-02T10:59:55.840274Z","iopub.execute_input":"2025-11-02T10:59:55.840631Z","iopub.status.idle":"2025-11-02T10:59:55.856336Z","shell.execute_reply.started":"2025-11-02T10:59:55.840603Z","shell.execute_reply":"2025-11-02T10:59:55.855314Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\nclass BiomassModel(nn.Module):\n    def __init__(self, backbone_name=CFG['backbone'], pretrained=True, use_ndvi=False, fc_dim=512, out_dim=5):\n        super().__init__()\n        self.backbone = timm.create_model(backbone_name, pretrained=pretrained, num_classes=0, global_pool='avg')\n        in_feats = self.backbone.num_features\n        self.use_ndvi = use_ndvi\n        if use_ndvi:\n            self.ndvi_proj = nn.Sequential(nn.Linear(1, 32), nn.ReLU())\n            in_feats = in_feats + 32\n        self.head = nn.Sequential(\n            nn.Linear(in_feats, fc_dim),\n            nn.ReLU(),\n            nn.BatchNorm1d(fc_dim),\n            nn.Dropout(0.3),\n            nn.Linear(fc_dim, out_dim)\n        )\n\n    def forward(self, x, ndvi=None):\n        bs = x.size(0)\n        feat = self.backbone(x)  # shape (bs, num_features)\n        if self.use_ndvi:\n            nd = self.ndvi_proj(ndvi)  # (bs, 32)\n            feat = torch.cat([feat, nd], dim=1)\n        out = self.head(feat)\n        return out\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-02T10:59:55.857300Z","iopub.execute_input":"2025-11-02T10:59:55.857569Z","iopub.status.idle":"2025-11-02T10:59:55.878993Z","shell.execute_reply.started":"2025-11-02T10:59:55.857545Z","shell.execute_reply":"2025-11-02T10:59:55.877926Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\nscaler = GradScaler()\n\ndef train_one_epoch(model, loader, optimizer, criterion, device, accum_steps=1):\n    model.train()\n    running_loss = 0.0\n    n = 0\n    optimizer.zero_grad()\n    for batch in loader:\n        images = batch[\"image\"].to(device)\n        targets = batch[\"targets\"].to(device)\n        ndvi = batch.get(\"ndvi\", None)\n        if ndvi is not None:\n            ndvi = ndvi.to(device)\n\n        with autocast():\n            preds = model(images, ndvi) if ndvi is not None else model(images)\n            loss = criterion(preds, targets)\n            loss = loss / accum_steps\n\n        scaler.scale(loss).backward()\n        if (n + 1) % accum_steps == 0:\n            scaler.step(optimizer)\n            scaler.update()\n            optimizer.zero_grad()\n\n        running_loss += float(loss.item() * images.size(0) * accum_steps)\n        n += 1\n\n    return running_loss / (len(loader.dataset))\n\ndef valid_one_epoch(model, loader, criterion, device):\n    model.eval()\n    running_loss = 0.0\n    preds_all = []\n    targs_all = []\n    ids_all = []\n    with torch.no_grad():\n        for batch in loader:\n            images = batch[\"image\"].to(device)\n            targets = batch[\"targets\"].to(device)\n            ndvi = batch.get(\"ndvi\", None)\n            if ndvi is not None:\n                ndvi = ndvi.to(device)\n            with autocast():\n                outputs = model(images, ndvi) if ndvi is not None else model(images)\n                loss = criterion(outputs, targets)\n            running_loss += float(loss.item() * images.size(0))\n            preds_all.append(outputs.cpu().numpy())\n            targs_all.append(targets.cpu().numpy())\n            ids_all.extend(batch[\"image_id\"])\n    preds_all = np.concatenate(preds_all, axis=0)\n    targs_all = np.concatenate(targs_all, axis=0)\n    return running_loss / len(loader.dataset), preds_all, targs_all, ids_all\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-02T10:59:55.880030Z","iopub.execute_input":"2025-11-02T10:59:55.880838Z","iopub.status.idle":"2025-11-02T10:59:55.903673Z","shell.execute_reply.started":"2025-11-02T10:59:55.880813Z","shell.execute_reply":"2025-11-02T10:59:55.902739Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\ntarget_means = pivot[TARGET_COLS].values.mean(axis=0)\ntarget_stds = pivot[TARGET_COLS].values.std(axis=0) + 1e-6\nprint(\"target means:\", target_means)\nprint(\"target stds:\", target_stds)\n\ndef scale_targets(y):\n    return (y - target_means) / target_stds\n\ndef descale_targets(y):\n    return (y * target_stds) + target_means\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-02T10:59:55.904765Z","iopub.execute_input":"2025-11-02T10:59:55.905043Z","iopub.status.idle":"2025-11-02T10:59:55.927171Z","shell.execute_reply.started":"2025-11-02T10:59:55.905022Z","shell.execute_reply":"2025-11-02T10:59:55.926222Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# === Preparations used inside the loop ===\ntarget_means_np = pivot[TARGET_COLS].values.mean(axis=0).astype(np.float32)\ntarget_stds_np  = pivot[TARGET_COLS].values.std(axis=0).astype(np.float32) + 1e-6\n\n# Torch tensors on device for criterion\ntorch_target_means = torch.tensor(target_means_np, dtype=torch.float32, device=CFG['device'])\ntorch_target_stds  = torch.tensor(target_stds_np, dtype=torch.float32, device=CFG['device'])\n\ndef descale_targets(preds_scaled_np):\n    return (preds_scaled_np * target_stds_np) + target_means_np\n\noof_preds = np.zeros((len(pivot), 5), dtype=np.float32)\noof_ids = pivot['image_id'].values\nmodel_paths = []\n\n# === CV loop (offline safe) ===\nfor fold in range(CFG['n_folds']):\n    print(f\"\\n====== Fold {fold} / {CFG['n_folds']-1} ======\")\n    train_idx = pivot[pivot['fold'] != fold].index\n    valid_idx = pivot[pivot['fold'] == fold].index\n    train_df_fold = pivot.loc[train_idx].reset_index(drop=True)\n    valid_df_fold = pivot.loc[valid_idx].reset_index(drop=True)\n\n    train_ds = CSIRO_Dataset(train_df_fold, TRAIN_IMG_ROOT, transforms=train_transforms, is_test=False, use_ndvi=CFG['use_ndvi'])\n    valid_ds = CSIRO_Dataset(valid_df_fold, TRAIN_IMG_ROOT, transforms=valid_transforms, is_test=False, use_ndvi=CFG['use_ndvi'])\n\n    train_loader = DataLoader(\n        train_ds,\n        batch_size=CFG['batch_size'],\n        shuffle=True,\n        num_workers=CFG['num_workers'],\n        pin_memory=True,\n        drop_last=True\n    )\n    valid_loader = DataLoader(\n        valid_ds,\n        batch_size=CFG['batch_size'],\n        shuffle=False,\n        num_workers=CFG['num_workers'],\n        pin_memory=True\n    )\n\n    # === MODEL: ensure offline ===\n    # pretrained=False to avoid downloading from internet\n    model = BiomassModel(\n        backbone_name=CFG['backbone'],\n        pretrained=False,       # ❗ offline mode\n        use_ndvi=CFG['use_ndvi']\n    ).to(CFG['device'])\n\n    # === Loss ===\n    def criterion(preds, targets):\n        scaled_targets = (targets - torch_target_means) / torch_target_stds\n        return nn.MSELoss()(preds, scaled_targets)\n\n    optimizer = optim.AdamW(model.parameters(), lr=CFG['lr'], weight_decay=CFG['weight_decay'])\n    scheduler = optim.lr_scheduler.ReduceLROnPlateau(optimizer, mode='min', factor=0.5, patience=2, verbose=True)\n\n    best_score = -999\n    best_path = OUT / f\"model_fold{fold}.pth\"\n\n    # === Training Loop ===\n    for epoch in range(CFG['epochs']):\n        t0 = time.time()\n        train_loss = train_one_epoch(model, train_loader, optimizer, criterion, CFG['device'])\n        val_loss, preds_val_scaled, targs_val, val_ids = valid_one_epoch(model, valid_loader, criterion, CFG['device'])\n\n        preds_val_scaled = preds_val_scaled.cpu().numpy() if isinstance(preds_val_scaled, torch.Tensor) else preds_val_scaled\n        targs_val = targs_val.cpu().numpy() if isinstance(targs_val, torch.Tensor) else targs_val\n\n        preds_val = descale_targets(preds_val_scaled)\n        r2s, weighted = weighted_r2(targs_val, preds_val)\n\n        print(f\"Fold {fold} Epoch {epoch+1} - train_loss {train_loss:.4f} val_loss {val_loss:.4f} weighted_R2 {weighted:.5f} per-target {r2s}\")\n        scheduler.step(val_loss)\n        print(\"Epoch time:\", time.time() - t0)\n\n        if weighted > best_score:\n            best_score = weighted\n            save_checkpoint(model, best_path)\n            print(\"Saved best:\", best_path)\n\n    model_paths.append(str(best_path))\n\n    # === Offline inference using saved model ===\n    model = BiomassModel(backbone_name=CFG['backbone'], pretrained=False, use_ndvi=CFG['use_ndvi']).to(CFG['device'])\n    load_checkpoint(model, best_path, map_location=CFG['device'])\n    model.eval()\n\n    preds_val_all = []\n    with torch.no_grad():\n        val_loader_for_oof = DataLoader(valid_ds, batch_size=CFG['batch_size'], shuffle=False, num_workers=CFG['num_workers'], pin_memory=True)\n        for batch in val_loader_for_oof:\n            imgs = batch['image'].to(device=CFG['device'], dtype=torch.float32)\n            ndvi = batch.get('ndvi', None)\n            if ndvi is not None:\n                ndvi = ndvi.to(device=CFG['device'], dtype=torch.float32)\n            out = model(imgs, ndvi) if ndvi is not None else model(imgs)\n            preds_val_all.append(out.cpu().numpy())\n\n    preds_val_all = np.concatenate(preds_val_all, axis=0)\n    preds_val_all = descale_targets(preds_val_all)\n    oof_preds[valid_idx] = preds_val_all\n\n# === Final Offline OOF Evaluation ===\ntrue_all = pivot[TARGET_COLS].values.astype(np.float32)\nr2s, wscore = weighted_r2(true_all, oof_preds)\nprint(\"OOF per-target R2s:\", r2s)\nprint(\"OOF weighted R2:\", wscore)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-02T11:03:32.140926Z","iopub.execute_input":"2025-11-02T11:03:32.141309Z","iopub.status.idle":"2025-11-02T14:05:24.491057Z","shell.execute_reply.started":"2025-11-02T11:03:32.141278Z","shell.execute_reply":"2025-11-02T14:05:24.488441Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\n\ndef safe_load_checkpoint(model, path, map_location=\"cpu\"):\n    \"\"\"Load checkpoint safely, ignoring unexpected or mismatched keys.\"\"\"\n    checkpoint = torch.load(path, map_location=map_location)\n    model_state = model.state_dict()\n\n    # Filter out unexpected keys (like ndvi_proj.*)\n    filtered_checkpoint = {k: v for k, v in checkpoint.items() if k in model_state and v.shape == model_state[k].shape}\n    missing_keys = [k for k in model_state.keys() if k not in filtered_checkpoint]\n    unexpected_keys = [k for k in checkpoint.keys() if k not in model_state]\n\n    print(f\"Loading {len(filtered_checkpoint)} keys. Skipped {len(missing_keys)} missing and {len(unexpected_keys)} unexpected keys.\")\n    model_state.update(filtered_checkpoint)\n    model.load_state_dict(model_state)\n    return model\n\ntest_df_simple = test_df[['image_id', 'image_path']].drop_duplicates().reset_index(drop=True)\ntest_ds = CSIRO_Dataset(\n    test_df_simple,\n    TEST_IMG_ROOT,\n    transforms=valid_transforms,\n    is_test=True,\n    use_ndvi=False  # keep as False since test has no NDVI\n)\n\ntest_loader = DataLoader(\n    test_ds,\n    batch_size=CFG['batch_size'],\n    shuffle=False,\n    num_workers=CFG['num_workers'],\n    pin_memory=True\n)\n\npreds_ensemble = np.zeros((len(test_ds), 5), dtype=np.float32)\n\nfor mp in model_paths:\n    print(f\"\\n🧠 Inferencing with: {mp}\")\n    model = BiomassModel(backbone_name=CFG['backbone'], pretrained=False, use_ndvi=False).to(device)\n    model = safe_load_checkpoint(model, mp, map_location=device)\n    model.eval()\n\n    preds_fold = []\n    with torch.no_grad():\n        for batch in test_loader:\n            imgs = batch['image'].to(device)\n            out = model(imgs).cpu().numpy()\n\n            if CFG['tta']:\n                imgs_h = torch.flip(imgs, dims=[3])\n                imgs_v = torch.flip(imgs, dims=[2])\n                out_h = model(imgs_h).cpu().numpy()\n                out_v = model(imgs_v).cpu().numpy()\n                out = (out + out_h + out_v) / 3.0\n\n            preds_fold.append(out)\n\n    preds_fold = np.concatenate(preds_fold, axis=0)\n    preds_fold = descale_targets(preds_fold)\n    preds_ensemble += preds_fold / len(model_paths)\n\nprint(\"\\n✅ Test preds shape:\", preds_ensemble.shape)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-02T14:27:48.088607Z","iopub.execute_input":"2025-11-02T14:27:48.091122Z","iopub.status.idle":"2025-11-02T14:27:58.822778Z","shell.execute_reply.started":"2025-11-02T14:27:48.091058Z","shell.execute_reply":"2025-11-02T14:27:58.821180Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\nimg_to_idx = {row['image_id']: i for i,row in test_df_simple.iterrows()}\n\nrows = []\ntargets = ['Dry_Green_g','Dry_Dead_g','Dry_Clover_g','GDM_g','Dry_Total_g']\nfor _, r in sample_sub.iterrows():\n    sample_id = r['sample_id']  # sample_id format: IDxxxx__TargetName\n    # sample_submission already contains sample_id; we need to fill target value.\n    # but safer: parse sample_id to get image_id & target_name\n    img_and_target = sample_id.split(\"__\")\n    image_id = img_and_target[0]\n    tname = img_and_target[1]\n    idx = img_to_idx.get(image_id, None)\n    if idx is None:\n        # fallback: try extracting from path\n        # but ideally this shouldn't happen if all test rows match\n        val = 0.0\n    else:\n        tindex = targets.index(tname)\n        val = float(preds_ensemble[idx, tindex])\n        # clip\n        if val < 0:\n            val = 0.0\n    rows.append([sample_id, val])\n\nsub = pd.DataFrame(rows, columns=[\"sample_id\",\"target\"])\nsub.to_csv(OUT / \"submission.csv\", index=False)\nprint(\"Saved submission:\", OUT / \"submission.csv\")\nsub.head(10)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-02T14:28:04.591623Z","iopub.execute_input":"2025-11-02T14:28:04.593186Z","iopub.status.idle":"2025-11-02T14:28:04.631891Z","shell.execute_reply.started":"2025-11-02T14:28:04.593129Z","shell.execute_reply":"2025-11-02T14:28:04.630345Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\noof_df = pivot[['image_id']].copy()\nfor i, t in enumerate(TARGET_COLS):\n    oof_df[f\"oof_{t}\"] = oof_preds[:, i]\noof_df.to_csv(OUT / \"oof_predictions.csv\", index=False)\nprint(\"Saved OOF preds:\", OUT / \"oof_predictions.csv\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-02T14:28:09.389786Z","iopub.execute_input":"2025-11-02T14:28:09.390151Z","iopub.status.idle":"2025-11-02T14:28:09.406419Z","shell.execute_reply.started":"2025-11-02T14:28:09.390129Z","shell.execute_reply":"2025-11-02T14:28:09.405308Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\n\ndef create_pseudo_labeled_df(test_preds_array, test_df_simple, threshold=0.05):\n    \"\"\"\n    test_preds_array: shape (n_test_images, 5)\n    test_df_simple: dataframe with image_id & image_path\n    threshold: placeholder for selecting confident samples (you should design a rule)\n    \"\"\"\n    # naive rule: use all test preds (not recommended); better: use low-variance samples across ensemble\n    pseudo_rows = []\n    for i, image_id in enumerate(test_df_simple['image_id']):\n        # convert to long-format rows per target\n        row = {\"image_id\": image_id}\n        for j, tname in enumerate(TARGET_COLS):\n            row[tname] = float(test_preds_array[i, j])\n        pseudo_rows.append(row)\n    pseudo_df = pd.DataFrame(pseudo_rows)\n    # merge required metadata columns (Sampling_Date, State, etc.) are unknown for test -> leave blank or infer\n    return pseudo_df\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-02T14:28:16.483981Z","iopub.execute_input":"2025-11-02T14:28:16.484345Z","iopub.status.idle":"2025-11-02T14:28:16.492865Z","shell.execute_reply.started":"2025-11-02T14:28:16.484324Z","shell.execute_reply":"2025-11-02T14:28:16.491275Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\n\ndef safe_load_checkpoint(model, path, map_location=\"cpu\"):\n    \"\"\"Load checkpoint safely — ignores NDVI & mismatched layers.\"\"\"\n    checkpoint = torch.load(path, map_location=map_location)\n    model_state = model.state_dict()\n\n    filtered_checkpoint = {k: v for k, v in checkpoint.items()\n                           if k in model_state and v.shape == model_state[k].shape}\n\n    missing_keys = [k for k in model_state.keys() if k not in filtered_checkpoint]\n    unexpected_keys = [k for k in checkpoint.keys() if k not in model_state]\n\n    print(f\"✅ Loaded {len(filtered_checkpoint)} layers. Skipped {len(missing_keys)} missing and {len(unexpected_keys)} unexpected keys.\")\n    model_state.update(filtered_checkpoint)\n    model.load_state_dict(model_state)\n    return model\n\n\n# Dataset setup\ntest_df_simple = test_df[['image_id', 'image_path']].drop_duplicates().reset_index(drop=True)\ntest_ds = CSIRO_Dataset(\n    test_df_simple,\n    TEST_IMG_ROOT,\n    transforms=valid_transforms,\n    is_test=True,\n    use_ndvi=False\n)\ntest_loader = DataLoader(\n    test_ds,\n    batch_size=min(64, CFG['batch_size'] * 2),  # larger batch for faster inference\n    shuffle=False,\n    num_workers=min(4, CFG['num_workers']),\n    pin_memory=True\n)\n\npreds_ensemble = np.zeros((len(test_ds), 5), dtype=np.float32)\n\n# Disable TTA for speed (you can enable if needed)\nCFG['tta'] = False  \n\nfor mp in model_paths:\n    print(f\"\\n🚀 Inference using: {mp}\")\n    model = BiomassModel(backbone_name=CFG['backbone'], pretrained=False, use_ndvi=False).to(device)\n    model = safe_load_checkpoint(model, mp, map_location=device)\n    model.eval()\n\n    preds_fold = []\n    with torch.no_grad():\n        # Enable mixed precision for faster inference\n        with torch.cuda.amp.autocast():\n            for batch in test_loader:\n                imgs = batch['image'].to(device, non_blocking=True)\n                out = model(imgs).cpu().numpy()\n\n                # Optional TTA (currently disabled)\n                if CFG['tta']:\n                    imgs_h = torch.flip(imgs, dims=[3])\n                    imgs_v = torch.flip(imgs, dims=[2])\n                    out_h = model(imgs_h).cpu().numpy()\n                    out_v = model(imgs_v).cpu().numpy()\n                    out = (out + out_h + out_v) / 3.0\n\n                preds_fold.append(out)\n\n    preds_fold = np.concatenate(preds_fold, axis=0)\n    preds_fold = descale_targets(preds_fold)\n    preds_ensemble += preds_fold / len(model_paths)\n\nprint(\"\\n✅ Done! Test predictions shape:\", preds_ensemble.shape)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-02T14:28:20.933954Z","iopub.execute_input":"2025-11-02T14:28:20.934351Z","iopub.status.idle":"2025-11-02T14:28:28.932890Z","shell.execute_reply.started":"2025-11-02T14:28:20.934327Z","shell.execute_reply":"2025-11-02T14:28:28.931426Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"submission = test_df_simple.copy()\nsubmission[TARGET_COLS] = preds_ensemble\nsubmission.to_csv(\"submission.csv\", index=False)\nprint(\"✅ submission.csv saved!\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-02T14:28:33.530059Z","iopub.execute_input":"2025-11-02T14:28:33.531297Z","iopub.status.idle":"2025-11-02T14:28:33.545193Z","shell.execute_reply.started":"2025-11-02T14:28:33.531256Z","shell.execute_reply":"2025-11-02T14:28:33.543945Z"}},"outputs":[],"execution_count":null}]}