{"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":"none","dataSources":[{"sourceId":112509,"databundleVersionId":14254895,"sourceType":"competition"},{"sourceId":287883194,"sourceType":"kernelVersion"}],"dockerImageVersionId":31236,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# CSIRO Pasture Biomass — Inference Notebook\n\nThis notebook is a **continuation of the Training Notebook**. Here, we focus on **making predictions** on the test set using the models trained previously and then submit the results\n\nYou can explore the training process in detail by visiting the **[Training Notebook](https://www.kaggle.com/code/chikonzeroselemani/csir0-gettingstarted-training)**.\n\n---\n\n## Overview of the Approach\n\nWe follow a **simple and effective workflow**:\n\n- **Ensemble of best models from K-Folds**: For each fold during training, the **best-performing model** was saved. These models are combined at inference by **averaging their predictions** to improve accuracy and stability.    \n- **Transformations & normalization**: Images are resized, converted to tensors, and normalized to match the input format used during training.  \n- **Submission-ready output**: Predictions are mapped back to `sample_id` and formatted exactly as required for the competition submission.","metadata":{}},{"cell_type":"markdown","source":"# 1.  Imports and setup\n\nThis cell contains all the required libraries for the notebook,\n","metadata":{}},{"cell_type":"code","source":"import os\nimport gc\nfrom glob import glob\nimport pandas as pd\nimport torch\nimport timm\nfrom torch import nn\nfrom torch.utils.data import Dataset, DataLoader\nfrom torchvision.transforms import Compose, Resize, Normalize, ToTensor\nfrom PIL import Image\nfrom tqdm import tqdm\n\ndevice = \"cuda\" if torch.cuda.is_available() else \"cpu\"\nprint(\"Device:\", device)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-22T12:30:11.667167Z","iopub.execute_input":"2025-12-22T12:30:11.668419Z","iopub.status.idle":"2025-12-22T12:30:11.680944Z","shell.execute_reply.started":"2025-12-22T12:30:11.668349Z","shell.execute_reply":"2025-12-22T12:30:11.679583Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# 2. Model Definition\n\nThis defines the ConvNeXtRegressor using the ConvNeXt Tiny backbone. \n","metadata":{}},{"cell_type":"code","source":"class ConvNeXtRegressor(nn.Module):\n    def __init__(self, num_targets=5):\n        super().__init__()\n\n        self.backbone = timm.create_model(\n            \"convnext_tiny\",\n            pretrained=False,         \n            features_only=False\n        )\n        self.backbone.head = nn.Identity()\n\n        self.pool = nn.AdaptiveAvgPool2d(1)\n        self.head = nn.Linear(768, num_targets)\n\n    def forward(self, x):\n        x = self.backbone.forward_features(x)\n        x = self.pool(x).flatten(1)\n        return self.head(x)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-22T12:30:11.683150Z","iopub.execute_input":"2025-12-22T12:30:11.683762Z","iopub.status.idle":"2025-12-22T12:30:11.708894Z","shell.execute_reply.started":"2025-12-22T12:30:11.683703Z","shell.execute_reply":"2025-12-22T12:30:11.707717Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# 3. Load  Models\n\nHere, we load the best model from each K-Fold saved during training. All models are set to evaluation mode and moved to the selected device. These models will later be ensembled by averaging their predictions for robust inference.\n","metadata":{}},{"cell_type":"code","source":"models_paths = sorted(\n    [p for p in os.listdir(\"/kaggle/input/csir0-gettingstarted\") if p.endswith(\".pth\")]\n)\nmodels_paths = [os.path.join(\"/kaggle/input/csir0-gettingstarted\", p) for p in models_paths]\n\nmodels = []\nfor path in models_paths:\n    model = ConvNeXtRegressor(num_targets=5)\n    state_dict = torch.load(path, map_location=device)\n    model.load_state_dict(state_dict)\n    model.to(device)\n    model.eval()\n    models.append(model)\n\nprint(f\"Loaded {len(models)} models\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-22T12:30:11.710015Z","iopub.execute_input":"2025-12-22T12:30:11.710341Z","iopub.status.idle":"2025-12-22T12:30:14.989968Z","shell.execute_reply.started":"2025-12-22T12:30:11.710305Z","shell.execute_reply":"2025-12-22T12:30:14.988615Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# 4. Prepare Test Data\n\nWe load the test CSV, map each target name to its corresponding index (matching the training order), and convert image paths to absolute paths. The data is then grouped by image so that all targets for a single image can be processed together during inference.\n","metadata":{}},{"cell_type":"code","source":"root_dir = \"/kaggle/input/csiro-biomass\"\ntest_df = pd.read_csv(os.path.join(root_dir, \"test.csv\"))\n\n# Map target name \ntarget_name_to_idx = {\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\n\ntest_df[\"target_idx\"] = test_df[\"target_name\"].map(target_name_to_idx)\n\n# Convert image paths to absolute paths\ntest_df[\"image_path\"] = test_df[\"image_path\"].apply(\n    lambda x: os.path.join(root_dir, x)\n)\n\n# Group by image \ngroup_df = test_df.groupby(\"image_path\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-22T12:30:14.991821Z","iopub.execute_input":"2025-12-22T12:30:14.992119Z","iopub.status.idle":"2025-12-22T12:30:15.003947Z","shell.execute_reply.started":"2025-12-22T12:30:14.992091Z","shell.execute_reply":"2025-12-22T12:30:15.002674Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# 5. Test Dataset & DataLoader\n\nWe define a custom dataset to handle grouped test images, apply the same transformations used during training, and create a DataLoader to efficiently feed batches of images for inference.\n","metadata":{}},{"cell_type":"code","source":"class TestDataset(Dataset):\n    def __init__(self, grouped_df):\n        self.items = list(grouped_df)\n\n    def __len__(self):\n        return len(self.items)\n\n    def __getitem__(self, idx):\n        img_path, df = self.items[idx]\n        img = Image.open(img_path).convert(\"RGB\")\n        return img, df\n\ntransform = Compose([\n    Resize((224, 224)),\n    ToTensor(),\n    Normalize([0.485, 0.456, 0.406],\n              [0.229, 0.224, 0.225])\n])\n\ndef collate_fn(batch):\n    imgs = [transform(b[0]) for b in batch]\n    dfs  = [b[1] for b in batch]\n    return torch.stack(imgs), dfs\n\ntest_loader = DataLoader(\n    TestDataset(group_df),\n    batch_size=16,\n    shuffle=False,\n    collate_fn=collate_fn\n)\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-22T12:30:15.005115Z","iopub.execute_input":"2025-12-22T12:30:15.005652Z","iopub.status.idle":"2025-12-22T12:30:15.027622Z","shell.execute_reply.started":"2025-12-22T12:30:15.005617Z","shell.execute_reply":"2025-12-22T12:30:15.026486Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# 6. Inference & Predictions\n\nWe perform inference by passing each batch of test images through all saved K-Fold models, averaging their predictions to form an ensemble. Predictions are then mapped back to each `(image, target)` pair and stored for submission.\n","metadata":{}},{"cell_type":"code","source":"preds = []\n\nwith torch.no_grad():\n    for imgs, df_batch in tqdm(test_loader):\n        imgs = imgs.to(device)\n\n        # Ensemble prediction\n        preds_sum = torch.zeros((imgs.size(0), 5), device=device)\n        for model in models:\n            preds_sum += model(imgs)\n\n        preds_avg = preds_sum / len(models)\n\n        # Map predictions back to original test rows\n        for i, df in enumerate(df_batch):\n            batch_pred = preds_avg[i] \n\n            for _, row in df.iterrows():\n                preds.append({\n                    \"sample_id\": row[\"sample_id\"],               \n                    \"target\": float(torch.relu(batch_pred[row[\"target_idx\"]]))\n                })\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-22T12:30:15.029035Z","iopub.execute_input":"2025-12-22T12:30:15.029443Z","iopub.status.idle":"2025-12-22T12:30:15.883801Z","shell.execute_reply.started":"2025-12-22T12:30:15.029402Z","shell.execute_reply":"2025-12-22T12:30:15.882765Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# 7. Save Submission","metadata":{}},{"cell_type":"code","source":"submission = pd.DataFrame(preds, columns=[\"sample_id\", \"target\"])\nsubmission.to_csv(\"submission.csv\", index=False)\n\nprint(\"Submission saved:\", submission.shape)\nsubmission.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-22T12:30:15.885295Z","iopub.execute_input":"2025-12-22T12:30:15.885764Z","iopub.status.idle":"2025-12-22T12:30:15.902804Z","shell.execute_reply.started":"2025-12-22T12:30:15.885720Z","shell.execute_reply":"2025-12-22T12:30:15.901405Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"If you found this notebook helpful, clear, or inspiring, please consider giving it an upvote\n","metadata":{}}]}