{"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","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":14533802,"sourceType":"datasetVersion","datasetId":9280423},{"sourceId":282128585,"sourceType":"kernelVersion"}],"dockerImageVersionId":31193,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# CSIRO Biomass Prediction - Inference Notebook\n\nThis notebook performs inference using trained models, loading one model at a time to conserve memory.","metadata":{}},{"cell_type":"code","source":"!pip uninstall -y timm\n!pip install -q --no-deps /kaggle/input/wheels-csiro/timm-1.0.22-py3-none-any.whl","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-18T05:21:52.181221Z","iopub.execute_input":"2026-01-18T05:21:52.181520Z","iopub.status.idle":"2026-01-18T05:21:57.063198Z","shell.execute_reply.started":"2026-01-18T05:21:52.181498Z","shell.execute_reply":"2026-01-18T05:21:57.062349Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport random\nfrom pathlib import Path\nfrom typing import List, Tuple, Dict, Optional\n\nimport numpy as np\nimport pandas as pd\nfrom PIL import Image\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.utils.data import Dataset, DataLoader\nimport torchvision.transforms as T\nimport timm\nfrom tqdm import tqdm\nfrom sklearn.preprocessing import LabelEncoder, StandardScaler\nimport gc\n\nSEED = 42\ndef seed_everything(seed=SEED):\n    random.seed(seed)\n    np.random.seed(seed)\n    torch.manual_seed(seed)\n    torch.cuda.manual_seed_all(seed)\n    os.environ[\"PYTHONHASHSEED\"] = str(seed)\n\nseed_everything()\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nprint(\"Current Device:\", device)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-18T05:21:57.064770Z","iopub.execute_input":"2026-01-18T05:21:57.065071Z","iopub.status.idle":"2026-01-18T05:22:07.729726Z","shell.execute_reply.started":"2026-01-18T05:21:57.065046Z","shell.execute_reply":"2026-01-18T05:22:07.729054Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class CFG:\n    TARGETS = [\"Dry_Green_g\", \"Dry_Dead_g\", \"Dry_Clover_g\", \"GDM_g\", \"Dry_Total_g\"]\n    MAX_SPECIES_LEN = 5\n    DATA_DIR = Path(\"/kaggle/input/csiro-biomass\")\n    MODEL_DIR = Path(\"/kaggle/input/biomass-model-dinov3\")\n    IMG_SIZE = 512\n    N_FOLDS = 4\n    BACKBONE = \"vit_huge_plus_patch16_dinov3.lvd1689m\"\n    BATCH_SIZE = 2  # Can increase if memory allows\n    \n    # TTA configuration\n    USE_TTA = False","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-18T05:22:07.730799Z","iopub.execute_input":"2026-01-18T05:22:07.731219Z","iopub.status.idle":"2026-01-18T05:22:07.735444Z","shell.execute_reply.started":"2026-01-18T05:22:07.731199Z","shell.execute_reply":"2026-01-18T05:22:07.734775Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Load data\ntrain_df = pd.read_csv(CFG.DATA_DIR / \"train.csv\")\ntest_df = pd.read_csv(CFG.DATA_DIR / \"test.csv\")\n\nprint(f\"Train rows: {len(train_df)}\")\nprint(f\"Test rows: {len(test_df)}\")\nprint(\"\\nTest data preview:\")\ntest_df.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-18T05:22:07.736405Z","iopub.execute_input":"2026-01-18T05:22:07.736911Z","iopub.status.idle":"2026-01-18T05:22:07.784905Z","shell.execute_reply.started":"2026-01-18T05:22:07.736891Z","shell.execute_reply":"2026-01-18T05:22:07.784319Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Prepare Test Data with Metadata","metadata":{}},{"cell_type":"code","source":"test_wide = test_df[[\"image_path\"]].drop_duplicates().reset_index(drop=True)\n\nprint(f\"Test data prepared: {len(test_wide)} samples\")\ntest_wide.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-18T05:22:07.786375Z","iopub.execute_input":"2026-01-18T05:22:07.786621Z","iopub.status.idle":"2026-01-18T05:22:07.803660Z","shell.execute_reply.started":"2026-01-18T05:22:07.786602Z","shell.execute_reply":"2026-01-18T05:22:07.803052Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Define Model Architecture","metadata":{}},{"cell_type":"code","source":"class LocalMambaBlock(nn.Module):\n    \"\"\"Lightweight Mamba-like block\"\"\"\n\n    def __init__(self, dim: int, kernel_size: int = 5, dropout: float = 0.1):\n        super().__init__()\n        self.norm = nn.LayerNorm(dim)\n        self.dwconv = nn.Conv1d(dim, dim, kernel_size=kernel_size,\n                                 padding=kernel_size // 2, groups=dim)\n        self.gate = nn.Linear(dim, dim)\n        self.proj = nn.Linear(dim, dim)\n        self.drop = nn.Dropout(dropout)\n\n    def forward(self, x: torch.Tensor) -> torch.Tensor:\n        shortcut = x\n        x = self.norm(x)\n        g = torch.sigmoid(self.gate(x))\n        x = x * g\n        x = x.transpose(1, 2)\n        x = self.dwconv(x)\n        x = x.transpose(1, 2)\n        x = self.proj(x)\n        x = self.drop(x)\n        return shortcut + x\n\n\nclass BiomassModel(nn.Module):\n    \"\"\"DINOv3 + Mamba Fusion + Multi-Head Regression\"\"\"\n\n    def __init__(self, model_name: str, pretrained: bool = True,\n                 backbone_path: Optional[Path] = None):\n        super().__init__()\n        self.model_name = model_name\n        self.backbone_path = backbone_path\n\n        self.backbone = timm.create_model(\n            model_name, pretrained=pretrained, num_classes=0, global_pool=''\n        )\n        nf = self.backbone.num_features\n\n        self.fusion = nn.Sequential(\n            LocalMambaBlock(nf, kernel_size=5, dropout=0.1),\n            LocalMambaBlock(nf, kernel_size=5, dropout=0.1)\n        )\n        self.pool = nn.AdaptiveAvgPool1d(1)\n\n        self.head_green = nn.Sequential(\n            nn.Linear(nf, nf // 2), nn.GELU(), nn.Dropout(0.2),\n            nn.Linear(nf // 2, 1), nn.Softplus()\n        )\n        self.head_dead = nn.Sequential(\n            nn.Linear(nf, nf // 2), nn.GELU(), nn.Dropout(0.2),\n            nn.Linear(nf // 2, 1), nn.Softplus()\n        )\n        self.head_clover = nn.Sequential(\n            nn.Linear(nf, nf // 2), nn.GELU(), nn.Dropout(0.2),\n            nn.Linear(nf // 2, 1), nn.Softplus()\n        )\n\n    def forward(self, x):\n        # x is a tuple (left, right)\n        if isinstance(x, tuple):\n            left, right = x\n        else:\n            raise ValueError(\"Input must be a tuple of (left, right)\")\n\n        x_l = self.backbone(left)\n        x_r = self.backbone(right)\n        x_cat = torch.cat([x_l, x_r], dim=1)\n        x_fused = self.fusion(x_cat)\n        x_pool = self.pool(x_fused.transpose(1, 2)).flatten(1)\n\n        green = self.head_green(x_pool)\n        dead = self.head_dead(x_pool)\n        clover = self.head_clover(x_pool)\n        gdm = green + clover\n        total = gdm + dead\n\n        # Return as a single tensor (batch, 5)\n        return torch.cat([green, dead, clover, gdm, total], dim=1)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-18T05:22:07.804358Z","iopub.execute_input":"2026-01-18T05:22:07.804568Z","iopub.status.idle":"2026-01-18T05:22:07.818563Z","shell.execute_reply.started":"2026-01-18T05:22:07.804550Z","shell.execute_reply":"2026-01-18T05:22:07.817841Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Define Test Dataset and DataLoader","metadata":{}},{"cell_type":"code","source":"class PastureImageTestDataset(Dataset):\n    \"\"\"Dataset for test images with metadata\"\"\"\n    def __init__(self, df, image_root, transform=None):\n        self.df = df.reset_index(drop=True)\n        self.image_root = image_root\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        # Load image\n        img_path = self.image_root / row[\"image_path\"]\n        img = Image.open(img_path).convert(\"RGB\")\n        w, h = img.size  # expect (2000, 1000)\n\n        # Split in the middle (deterministic for test)\n        left = img.crop((0, 0, h, h))        # (0, 0, 1000, 1000)\n        right = img.crop((w - h, 0, w, h))   # (1000, 0, 2000, 1000)\n\n        if self.transform:\n            img1 = self.transform(left)\n            img2 = self.transform(right)\n\n        # Return images and row info\n        return (img1, img2), row.to_dict()\n\n\n# Define transforms\ntest_tfms = 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\n\ndef get_tta_transforms():\n    \"\"\"Returns a list of transform pipelines for TTA during inference.\"\"\"\n    base_transforms = [\n        T.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])\n    ]\n\n    # View 1: Original\n    original_view = T.Compose([\n        T.Resize((CFG.IMG_SIZE, CFG.IMG_SIZE)),\n        T.ToTensor(),\n        *base_transforms\n    ])\n\n    # View 2: Horizontal Flip\n    hflip_view = T.Compose([\n        T.Resize((CFG.IMG_SIZE, CFG.IMG_SIZE)),\n        T.RandomHorizontalFlip(p=1.0),\n        T.ToTensor(),\n        *base_transforms\n    ])\n\n    # View 3: Vertical Flip\n    vflip_view = T.Compose([\n        T.Resize((CFG.IMG_SIZE, CFG.IMG_SIZE)),\n        T.RandomVerticalFlip(p=1.0),\n        T.ToTensor(),\n        *base_transforms\n    ])\n\n    # View 4: Rotate90\n    vflip_view = T.Compose([\n        T.Resize((CFG.IMG_SIZE, CFG.IMG_SIZE)),\n        T.Lambda(lambda img: T.functional.rotate(img, angle=90)),\n        T.ToTensor(),\n        *base_transforms\n    ])\n\n    return [original_view, hflip_view, vflip_view]\n\n\ndef collate_fn_test(batch):\n    \"\"\"Custom collate function for test data\"\"\"\n    imgs1, imgs2 = [], []\n    row_infos = []\n\n    for (img1, img2), row_info in batch:\n        imgs1.append(img1)\n        imgs2.append(img2)\n        row_infos.append(row_info)\n\n    imgs1 = torch.stack(imgs1)\n    imgs2 = torch.stack(imgs2)\n\n    return (imgs1, imgs2), row_infos\n\n\nprint(\"Test dataset and transforms defined\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-18T05:22:10.679124Z","iopub.execute_input":"2026-01-18T05:22:10.679626Z","iopub.status.idle":"2026-01-18T05:22:10.689749Z","shell.execute_reply.started":"2026-01-18T05:22:10.679604Z","shell.execute_reply":"2026-01-18T05:22:10.689001Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Inference Function","metadata":{}},{"cell_type":"code","source":"def predict_with_model(model, loader, use_tta=False):\n    \"\"\"\n    Make predictions with a single model.\n    \n    Args:\n        model: Trained model\n        test_loader: DataLoader for test data\n        use_tta: Whether to use test-time augmentation\n    \n    Returns:\n        predictions: numpy array of shape (n_samples, 5) with all 5 targets\n    \"\"\"\n    model.eval()\n    # Standard inference without TTA\n    \n    preds_all = []\n    with torch.no_grad():\n        for (imgs1, imgs2), _ in tqdm(test_loader, desc=\"Inference\"):\n            imgs1, imgs2 = imgs1.to(device), imgs2.to(device)\n            with torch.amp.autocast('cuda'):\n                pred = model((imgs1, imgs2))\n\n            preds_all.append(pred.cpu().numpy())\n    \n    return np.vstack(preds_all)\n\n\n\nprint(\"Inference function defined\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-18T05:22:19.742715Z","iopub.execute_input":"2026-01-18T05:22:19.743037Z","iopub.status.idle":"2026-01-18T05:22:19.748886Z","shell.execute_reply.started":"2026-01-18T05:22:19.743013Z","shell.execute_reply":"2026-01-18T05:22:19.748106Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Run Inference with Model Ensemble\n\nLoad one model at a time, make predictions, and ensemble the results.","metadata":{}},{"cell_type":"code","source":"# Create test dataset and dataloader\ntest_dataset = PastureImageTestDataset(\n    test_wide,\n    CFG.DATA_DIR,\n    test_tfms\n)\n\ntest_loader = DataLoader(\n    test_dataset,\n    batch_size=CFG.BATCH_SIZE,\n    shuffle=False,\n    num_workers=2,\n    pin_memory=True,\n    collate_fn=collate_fn_test\n)\n\nprint(f\"Test loader created with {len(test_dataset)} samples\")\n\n# Storage for ensemble predictions\nall_fold_predictions = []\n\n# Loop through each fold\nfor fold in range(0, CFG.N_FOLDS):\n    print(f\"\\n{'='*60}\")\n    print(f\"Processing Fold {fold}\")\n    print(f\"{'='*60}\")\n\n    model_file = (\n        \"biomass_model_easy_fold1_0.6228.pth\" if fold == 1 else\n        \"biomass_model_easy_fold3.pth\" if fold == 3 else\n        \"x\"\n    )\n    model_path = CFG.MODEL_DIR / model_file\n    if not os.path.exists(model_path):\n        continue\n    print(f\"Loading model from {model_path}...\")\n    \n    model = BiomassModel(\n        model_name=CFG.BACKBONE,\n        pretrained=False\n    )\n    \n    # Load weights\n    state_dict = torch.load(model_path, map_location='cpu')\n    \n    # Handle DataParallel state dict\n    if list(state_dict.keys())[0].startswith('module.'):\n        # Remove 'module.' prefix\n        state_dict = {k.replace('module.', ''): v for k, v in state_dict.items()}\n    \n    model.load_state_dict(state_dict)\n    model = model.to(device)\n    \n    # Make predictions\n    fold_predictions = predict_with_model(\n        model, test_loader, use_tta=CFG.USE_TTA\n    )\n    weight = (\n        0.75 if fold == 1 else\n        1.25 if fold == 3 else\n        1.0\n    )\n    all_fold_predictions.append(weight * fold_predictions)\n    \n    print(f\"\\nFold {fold} predictions shape: {fold_predictions.shape}\")\n    print(f\"Predictions stats: min={fold_predictions.min():.4f}, max={fold_predictions.max():.4f}, mean={fold_predictions.mean():.4f}\")\n    \n    # Clean up to free memory\n    del model\n    del state_dict\n    torch.cuda.empty_cache()\n    gc.collect()\n    print(f\"\\nMemory cleaned after fold {fold}\")\n\nprint(f\"\\n{'='*60}\")\nprint(f\"All folds processed: {len(all_fold_predictions)} models\")\nprint(f\"{'='*60}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-18T05:22:24.507563Z","iopub.execute_input":"2026-01-18T05:22:24.507862Z","iopub.status.idle":"2026-01-18T05:23:23.807163Z","shell.execute_reply.started":"2026-01-18T05:22:24.507840Z","shell.execute_reply":"2026-01-18T05:23:23.806423Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Ensemble Predictions","metadata":{}},{"cell_type":"code","source":"# Average predictions across all folds\nif len(all_fold_predictions) == 0:\n    raise ValueError(\"No model predictions were generated. Check if model files exist.\")\n\nensemble_predictions = np.mean(all_fold_predictions, axis=0)\n\nprint(f\"Ensemble predictions shape: {ensemble_predictions.shape}\")\nprint(f\"Ensemble stats: min={ensemble_predictions.min():.4f}, max={ensemble_predictions.max():.4f}, mean={ensemble_predictions.mean():.4f}\")\n\n# Show predictions for first sample\nprint(\"\\nFirst sample predictions:\")\nfor i, target in enumerate(CFG.TARGETS):\n    print(f\"  {target}: {ensemble_predictions[0, i]:.4f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-18T05:24:26.132922Z","iopub.execute_input":"2026-01-18T05:24:26.133661Z","iopub.status.idle":"2026-01-18T05:24:26.139396Z","shell.execute_reply.started":"2026-01-18T05:24:26.133635Z","shell.execute_reply":"2026-01-18T05:24:26.138431Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Create Submission File","metadata":{}},{"cell_type":"code","source":"# Create submission dataframe\n# Each test image should have 5 rows (one for each target)\n\nsubmission_rows = []\n\nfor idx, row in test_wide.iterrows():\n    image_id = row['image_path'].split('/')[-1].replace('.jpg', '')\n    \n    for target_idx, target_name in enumerate(CFG.TARGETS):\n        sample_id = f\"{image_id}__{target_name}\"\n        prediction = ensemble_predictions[idx, target_idx]\n        if target_name == \"Dry_Clover_g\":\n            prediction = prediction * 0.8\n        elif target_name == \"Dry_Dead_g\":\n            if prediction > 20:\n                prediction *= 1.1\n            elif prediction < 10:\n                prediction *= 0.9\n        \n        submission_rows.append({\n            'sample_id': sample_id,\n            'target': prediction\n        })\n\nsubmission_df = pd.DataFrame(submission_rows)\nsubmission_path = \"submission.csv\"\nsubmission_df.to_csv(submission_path, index=False)\nprint(f\"\\nSubmission saved to {submission_path}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-18T05:24:33.592267Z","iopub.execute_input":"2026-01-18T05:24:33.592552Z","iopub.status.idle":"2026-01-18T05:24:33.606141Z","shell.execute_reply.started":"2026-01-18T05:24:33.592529Z","shell.execute_reply":"2026-01-18T05:24:33.605326Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Display statistics\nprint(\"\\nSubmission statistics by target:\")\nfor target in CFG.TARGETS:\n    target_rows = submission_df[submission_df['sample_id'].str.contains(target)]\n    print(f\"  {target}:\")\n    print(f\"    Count: {len(target_rows)}\")\n    print(f\"    Min: {target_rows['target'].min():.4f}\")\n    print(f\"    Max: {target_rows['target'].max():.4f}\")\n    print(f\"    Mean: {target_rows['target'].mean():.4f}\")\n    print(f\"    Median: {target_rows['target'].median():.4f}\")\n\nsubmission_df.head(10)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-18T05:24:35.904540Z","iopub.execute_input":"2026-01-18T05:24:35.904877Z","iopub.status.idle":"2026-01-18T05:24:35.923115Z","shell.execute_reply.started":"2026-01-18T05:24:35.904858Z","shell.execute_reply":"2026-01-18T05:24:35.922321Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}