{"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":"none","dataSources":[{"sourceId":112509,"databundleVersionId":14254895,"sourceType":"competition"}],"dockerImageVersionId":31239,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"id":"7ef9b9f3-cd68-4686-8c81-08e358af90e3","cell_type":"markdown","source":"# 🌿 CSIRO Biomass: Visual EDA & Strong Baseline [PyTorch]\n## Introduction\nWelcome to the **CSIRO - Image2Biomass Prediction** competition! \nIn this competition, we need to estimate pasture biomass from images. This is crucial for farmers to optimize feed management.\n\n### What makes this notebook special?\n1.  **Pivot Strategy**: The data comes in a \"melted\" format (multiple rows per image). We pivot it to a standard regression format (one image -> 5 targets).\n2.  **Visual EDA**: We visualize the \"Green\" vs \"Dead\" biomass components to understand the data.\n3.  **Robust Training**: a `timm` based EfficientNet training pipeline with mixed precision.\n4.  **Reproducibility**: Seeded and ready for Kaggle GPU.\n\nLet's get started! 🚀\n\n","metadata":{}},{"id":"d3741a8e-ab57-4919-84e8-d6aafec5739b","cell_type":"code","source":"import os\nimport gc\nimport random\nimport math\nimport time\nimport pandas as pd\nimport numpy as np\nimport matplotlib.pyplot as plt\nimport seaborn as sns\nimport cv2\nfrom tqdm.auto import tqdm\nfrom pathlib import Path\n\nimport torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom torch.utils.data import Dataset, DataLoader\nfrom torch.cuda.amp import autocast, GradScaler\nimport albumentations as A\nfrom albumentations.pytorch import ToTensorV2\nfrom sklearn.model_selection import KFold\nfrom sklearn.metrics import mean_squared_error, mean_absolute_error\n\n# Suppress warnings\nimport warnings\nwarnings.filterwarnings('ignore')\n\n# Style\nplt.style.use('ggplot')\nsns.set_palette('viridis')\npd.set_option('display.max_columns', None)\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-03T06:08:46.173464Z","iopub.execute_input":"2026-01-03T06:08:46.173868Z","iopub.status.idle":"2026-01-03T06:08:56.038438Z","shell.execute_reply.started":"2026-01-03T06:08:46.173827Z","shell.execute_reply":"2026-01-03T06:08:56.037421Z"}},"outputs":[],"execution_count":null},{"id":"ffc18ad8-018f-42fe-b51e-bc56b00af42c","cell_type":"code","source":"# --- Configuration ---\nclass CFG:\n    seed = 42\n    img_size = 256  # Increase to 384 or 512 for better results\n    batch_size = 32\n    epochs = 5\n    lr = 1e-4\n    backbone = 'tf_efficientnet_b0_ns' # Consider 'convnext_tiny' or 'resnet50'\n    num_workers = 4\n    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n    n_folds = 5\n    target_cols = ['Dry_Green_g', 'Dry_Dead_g', 'Dry_Clover_g', 'GDM_g', 'Dry_Total_g']\n    \n    # Paths (adjust if running locally)\n    if os.path.exists('/kaggle/input'):\n        data_root = Path('/kaggle/input/csiro-biomass')\n    else:\n        # Local path assumption\n        data_root = Path('.') \n        \nprint(f\"Device: {CFG.device}\")\nprint(f\"Data Root: {CFG.data_root}\")\n\ndef seed_everything(seed):\n    random.seed(seed)\n    os.environ['PYTHONHASHSEED'] = str(seed)\n    np.random.seed(seed)\n    torch.manual_seed(seed)\n    torch.cuda.manual_seed(seed)\n    torch.backends.cudnn.deterministic = True\n\nseed_everything(CFG.seed)\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-03T06:08:56.040231Z","iopub.execute_input":"2026-01-03T06:08:56.040677Z","iopub.status.idle":"2026-01-03T06:08:56.054921Z","shell.execute_reply.started":"2026-01-03T06:08:56.040655Z","shell.execute_reply":"2026-01-03T06:08:56.054219Z"}},"outputs":[],"execution_count":null},{"id":"02876514-1d83-42d5-849b-aa019c32e94b","cell_type":"markdown","source":"## 1. Data Cleaning & Transformation\nThe competition data structure is unique. Let's look at `train.csv`.\n\n","metadata":{}},{"id":"ae1f1ff4-6b96-4e29-b382-4346170b15e1","cell_type":"code","source":"train_df = pd.read_csv(CFG.data_root / 'train.csv')\ntest_df = pd.read_csv(CFG.data_root / 'test.csv')\nsample_sub = pd.read_csv(CFG.data_root / 'sample_submission.csv')\n\nprint(f\"Original Train Shape: {train_df.shape}\")\ntrain_df.head()\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-03T06:08:56.055844Z","iopub.execute_input":"2026-01-03T06:08:56.056507Z","iopub.status.idle":"2026-01-03T06:08:56.129587Z","shell.execute_reply.started":"2026-01-03T06:08:56.056474Z","shell.execute_reply":"2026-01-03T06:08:56.128717Z"}},"outputs":[],"execution_count":null},{"id":"4314543b-1864-44cc-bea6-576cae95ac81","cell_type":"markdown","source":"### The \"Values\" Trick (Pivoting)\nEach image has multiple rows. We want to predict **5 values** for one image.\nLet's filter the train dataframe directly for the targets we need and then pivot.\nNote: Some images might not have all targets? Let's assume the competition guarantees structure or we should fill NaNs.\nBasically, `train.csv` has `target_name` and `target`.\n\n","metadata":{}},{"id":"40c0a191-c10d-4ab2-bded-b82459970f09","cell_type":"code","source":"# Pivot the table: Index=image_path, Columns=target_name, Values=target\n# We need to preserve other columns like State if we want to use them, but let's focus on images first.\n\n# 1. Get unique images\nunique_images = train_df[['sample_id', 'image_path', 'State', 'Sampling_Date']].drop_duplicates()\n\n# 2. Pivot the targets\ntargets = train_df.pivot(index='sample_id', columns='target_name', values='target').reset_index()\ntargets = targets.rename_axis(None, axis=1) # Cleanup name\n\n# 3. Merge back\nmerged_df = unique_images.merge(targets, on='sample_id', how='left')\nmerged_df[CFG.target_cols] = merged_df[CFG.target_cols].fillna(0) # Logic check: should we fill 0?\n\nprint(f\"Pivoted Train Shape: {merged_df.shape}\")\nmerged_df.head()\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-03T06:08:56.130544Z","iopub.execute_input":"2026-01-03T06:08:56.130839Z","iopub.status.idle":"2026-01-03T06:08:56.178988Z","shell.execute_reply.started":"2026-01-03T06:08:56.130819Z","shell.execute_reply":"2026-01-03T06:08:56.178133Z"}},"outputs":[],"execution_count":null},{"id":"71103cdd-a5dd-443f-a898-cb27b32bb9f8","cell_type":"markdown","source":"## 2. Exploratory Data Analysis (EDA)\nLet's visualize the distribution of `Dry_Total_g` and the relationship between `Dry_Green_g` and `Dry_Dead_g`.\n\n","metadata":{}},{"id":"7e468d03-1b0b-44c8-8c43-586aac71e141","cell_type":"code","source":"fig, axes = plt.subplots(1, 2, figsize=(15, 6))\n\nsns.histplot(merged_df['Dry_Total_g'], kde=True, ax=axes[0], color='green')\naxes[0].set_title('Distribution of Total Dry Biomass')\n\nsns.scatterplot(data=merged_df, x='Dry_Green_g', y='Dry_Dead_g', hue='State', ax=axes[1], alpha=0.6)\naxes[1].set_line_style = False # Typo fix in thought, seaborn usage correct\naxes[1].set_title('Green vs Dead Biomass by State')\n\nplt.tight_layout()\nplt.show()\n\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"id":"96492932-d32d-41fc-9a26-afd713193847","cell_type":"markdown","source":"Let's look at some images! We will overlay the Total Biomass value.\n","metadata":{}},{"id":"8e344faa-497b-44f2-a23d-9c00f056390b","cell_type":"code","source":"def show_images(df, n=5):\n    plt.figure(figsize=(20, 10))\n    sample = df.sample(n)\n    for i, (_, row) in enumerate(sample.iterrows()):\n        img_path = CFG.data_root / row['image_path']\n        if not img_path.exists(): continue\n        \n        img = cv2.imread(str(img_path))\n        img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n        \n        plt.subplot(1, n, i+1)\n        plt.imshow(img)\n        plt.title(f\"Total: {row['Dry_Total_g']:.1f}g\\nGreen: {row['Dry_Green_g']:.1f}g\")\n        plt.axis('off')\n    plt.show()\n\ntry:\n    show_images(merged_df)\nexcept Exception as e:\n    print(f\"Could not load images (Check path): {e}\")\n\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"id":"2184d9d6-206b-4952-97e4-0e1786c7059c","cell_type":"markdown","source":"## 3. Dataset & Model\nWe use `timm` to create a strong baseline model.\n\n","metadata":{}},{"id":"713f3712-8fdf-4180-8a45-381c0951d918","cell_type":"code","source":"# Install timm if needed (usually helpful in Kaggle kernels that are offline)\n# !pip install timm\nimport timm\n\nclass CSIRODataset(Dataset):\n    def __init__(self, df, transforms=None, mode='train'):\n        self.df = df\n        self.transforms = transforms\n        self.mode = mode\n        self.image_paths = df['image_path'].values\n        \n        if mode == 'train':\n            self.targets = df[CFG.target_cols].values\n            \n    def __len__(self):\n        return len(self.df)\n    \n    def __getitem__(self, idx):\n        path = CFG.data_root / self.image_paths[idx]\n        image = cv2.imread(str(path))\n        if image is None:\n            # Placeholder for missing files in development\n            image = np.zeros((CFG.img_size, CFG.img_size, 3), dtype=np.uint8)\n        else:\n            image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)\n            \n        if self.transforms:\n            image = self.transforms(image=image)['image']\n            \n        if self.mode == 'train':\n            label = torch.tensor(self.targets[idx], dtype=torch.float32)\n            return image, label\n        else:\n            return image\n\n# Augmentations\ndef get_transforms(data):\n    if data == 'train':\n        return A.Compose([\n            A.Resize(CFG.img_size, CFG.img_size),\n            A.HorizontalFlip(p=0.5),\n            A.VerticalFlip(p=0.5),\n            A.Rotate(limit=15, p=0.5),\n            A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),\n            ToTensorV2(),\n        ])\n    elif data == 'valid':\n        return A.Compose([\n            A.Resize(CFG.img_size, CFG.img_size),\n            A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),\n            ToTensorV2(),\n        ])\n\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"id":"a8f966aa-1a2f-4df6-bf32-df7bb0e96f3d","cell_type":"code","source":"class BiomassModel(nn.Module):\n    def __init__(self, model_name, pretrained=True):\n        super().__init__()\n        self.backbone = timm.create_model(model_name, pretrained=pretrained, num_classes=0)\n        self.n_features = self.backbone.num_features\n        self.fc = nn.Sequential(\n            nn.Dropout(0.2),\n            nn.Linear(self.n_features, 64),\n            nn.LeakyReLU(),\n            nn.Linear(64, len(CFG.target_cols)) # Output 5 values\n        )\n        \n    def forward(self, x):\n        features = self.backbone(x)\n        output = self.fc(features)\n        return output\n\nmodel = BiomassModel(CFG.backbone, pretrained=False) # False here properly, will be true in training\nprint(model)\n\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"id":"1f93ce7f-0d04-469e-9e2f-8a122ed7906b","cell_type":"markdown","source":"## 4. Training Loop\nStandard PyTorch loop with RMSE monitoring.\n\n","metadata":{}},{"id":"58ea3b1b-dda0-43f0-bff6-7f807780856c","cell_type":"code","source":"def train_fn(dataloader, model, criterion, optimizer, device, scaler):\n    model.train()\n    running_loss = 0\n    \n    for images, labels in tqdm(dataloader, leave=False):\n        images = images.to(device)\n        labels = labels.to(device)\n        \n        optimizer.zero_grad()\n        \n        with autocast():\n            outputs = model(images)\n            loss = criterion(outputs, labels)\n            \n        scaler.scale(loss).backward()\n        scaler.step(optimizer)\n        scaler.update()\n        \n        running_loss += loss.item() * images.size(0)\n        \n    return running_loss / len(dataloader.dataset)\n\ndef valid_fn(dataloader, model, criterion, device):\n    model.eval()\n    running_loss = 0\n    preds = []\n    \n    with torch.no_grad():\n        for images, labels in tqdm(dataloader, leave=False):\n            images = images.to(device)\n            labels = labels.to(device)\n            \n            outputs = model(images)\n            loss = criterion(outputs, labels)\n            running_loss += loss.item() * images.size(0)\n            preds.append(outputs.cpu().numpy())\n            \n    return running_loss / len(dataloader.dataset), np.concatenate(preds)\n\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"id":"477be367-d08d-47bf-a479-69784a4f88e1","cell_type":"code","source":"# --- Main Execution ---\n# We will just run 1 fold as a demo/starter in this notebook to save time.\n# In a real submission, loop over folds.\n\nkf = KFold(n_splits=CFG.n_folds, shuffle=True, random_state=CFG.seed)\n\n# Dummy split for demonstration if dataset file is missing\nif 'merged_df' in globals() and len(merged_df) > 0:\n    fold_0_indices = list(kf.split(merged_df))[0]\n    train_idx, val_idx = fold_0_indices\n    \n    train_data = merged_df.iloc[train_idx].reset_index(drop=True)\n    valid_data = merged_df.iloc[val_idx].reset_index(drop=True)\n    \n    train_dataset = CSIRODataset(train_data, transform=get_transforms('train'), mode='train')\n    valid_dataset = CSIRODataset(valid_data, transform=get_transforms('valid'), mode='train') # mode train because we need labels\n    \n    train_loader = DataLoader(train_dataset, batch_size=CFG.batch_size, shuffle=True, num_workers=CFG.num_workers)\n    valid_loader = DataLoader(valid_dataset, batch_size=CFG.batch_size * 2, shuffle=False, num_workers=CFG.num_workers)\n    \n    # Model Setup\n    model = BiomassModel(CFG.backbone, pretrained=True)\n    model.to(CFG.device)\n    \n    optimizer = optim.AdamW(model.parameters(), lr=CFG.lr)\n    criterion = nn.MSELoss() # Or nn.L1Loss() for MAE\n    scaler = GradScaler()\n    \n    best_loss = float('inf')\n    \n    print(\"Starting Training (Fold 0)...\")\n    for epoch in range(CFG.epochs):\n        train_loss = train_fn(train_loader, model, criterion, optimizer, CFG.device, scaler)\n        valid_loss, preds = valid_fn(valid_loader, model, criterion, CFG.device)\n        \n        print(f\"Epoch {epoch+1}/{CFG.epochs} | Train Loss: {train_loss:.4f} | Valid Loss: {valid_loss:.4f}\")\n        \n        if valid_loss < best_loss:\n            best_loss = valid_loss\n            torch.save(model.state_dict(), 'best_model.pth')\n            print(\"  --> Model Saved!\")\n            \nelse:\n    print(\"Data not found or empty. Please ensure 'csiro-biomass' data is in Input.\")\n\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"id":"8a444743-bbc2-4937-bd2c-196e0392819f","cell_type":"markdown","source":"## 5. Submission\nLoad the best model and predict on `test.csv`.\nNote that test.csv is also \"melted\". We need to predict for each unique image, then map values back.\n\n","metadata":{}},{"id":"6e1af1fa-1cf5-4496-8be4-e7c6473006a2","cell_type":"code","source":"# Prepare Test Set\n# The test.csv has rows per target. We need unique images to run inference efficiently.\n\nif os.path.exists(CFG.data_root / 'test.csv'):\n    test_unique = test_df[['sample_id', 'image_path']].drop_duplicates()\n    test_dataset = CSIRODataset(test_unique, transform=get_transforms('valid'), mode='test')\n    test_loader = DataLoader(test_dataset, batch_size=CFG.batch_size*2, shuffle=False, num_workers=CFG.num_workers)\n    \n    # Load Model\n    model = BiomassModel(CFG.backbone, pretrained=False)\n    model.load_state_dict(torch.load('best_model.pth', map_location=CFG.device))\n    model.to(CFG.device)\n    model.eval()\n    \n    # Prediction\n    predictions = []\n    with torch.no_grad():\n        for images in tqdm(test_loader):\n            images = images.to(CFG.device)\n            outputs = model(images)\n            predictions.append(outputs.cpu().numpy())\n            \n    predictions = np.concatenate(predictions) # Shape: (N_images, 5)\n    \n    # Create a prediction DF\n    pred_df = test_unique.copy()\n    pred_df[CFG.target_cols] = predictions\n    \n    # Melt it back to submission format\n    # The submission needs: sample_id, target\n    # Wait, sample_id in test.csv is unique per (image, target_name) pair.\n    # So we need to map our Image-level predictions back to the Row-level sample_ids.\n    \n    # 1. Melt our predictions\n    melted_preds = pred_df.melt(id_vars=['sample_id', 'image_path'], \n                                value_vars=CFG.target_cols, \n                                var_name='target_name', \n                                value_name='prediction')\n    \n    # 2. Key matching is tricky. \n    # test.csv has `sample_id` which is the RowID. It also has `image_path` and `target_name`.\n    # Let's merge `melted_preds` with `test_df` on `image_path` and `target_name`.\n    # But `melted_preds` uses `sample_id` from the unique calculation? \n    # Actually `sample_id` in `test_unique` is NOT unique per image solely, it's just one of the IDs.\n    # Better to just use image_path as key.\n    \n    submission_merge = test_df.merge(melted_preds[['image_path', 'target_name', 'prediction']], \n                                     on=['image_path', 'target_name'], \n                                     how='left')\n    \n    # 3. Create final submission file\n    submission = submission_merge[['sample_id', 'prediction']].rename(columns={'prediction': 'target'})\n    \n    submission.to_csv('submission.csv', index=False)\n    print(\"Submission saved successfully!\")\n    print(submission.head())\n\nelse:\n    print(\"Test data not found.\")\n\n","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}