{"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":"nvidiaTeslaT4","dataSources":[{"sourceId":112509,"databundleVersionId":14254895,"sourceType":"competition"},{"sourceId":14262431,"sourceType":"datasetVersion","datasetId":9100975}],"dockerImageVersionId":31240,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# CSIRO - Image2Biomass Prediction\n\nThis notebook implements a ResNet50-based model to predict pasture biomass from top-view images.\nIt predicts 3 base components (`Green`, `Clover`, `Dead`) and derives `GDM` and `Total` to ensure physical consistency.\n\n**Offline Mode Notes**:\n- If Internet is OFF (Kaggle Submission), you must add valid ResNet50 weights as a dataset.\n- Update `Config.resnet_weights_path` with the path to the `.pth` file.","metadata":{}},{"cell_type":"code","source":"import os\nimport copy\nimport time\nimport numpy as np\nimport pandas as pd\nfrom PIL import Image\nimport torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom torch.utils.data import Dataset, DataLoader, random_split\nfrom torchvision import transforms, models\n\n# Configuration\nclass Config:\n    seed = 42\n    img_size = 224 # Increase to 512 for better results if GPU allows\n    batch_size = 16\n    epochs = 10\n    lr = 1e-4\n    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n    \n    # Offline Weights Config\n    # Update this to match your uploaded dataset path!\n    # Example: '/kaggle/input/my-resnet50/resnet50.pth'\n    resnet_weights_path = '/kaggle/input/resnet50/resnet50.pth' \n    \n    # Check if running on Kaggle\n    if os.path.exists('/kaggle/input'):\n        data_root = '/kaggle/input/csiro-biomass'\n        # Auto-discovery\n        for dirname, _, filenames in os.walk('/kaggle/input'):\n            if 'train.csv' in filenames:\n                data_root = dirname.replace('/train.csv', '')\n                break\n    else:\n        data_root = 'csiro-biomass' # Local path\n        \nprint(f\"Using device: {Config.device}\")\nprint(f\"Data Root: {Config.data_root}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-23T01:30:20.558439Z","iopub.execute_input":"2025-12-23T01:30:20.558644Z","iopub.status.idle":"2025-12-23T01:30:30.030202Z","shell.execute_reply.started":"2025-12-23T01:30:20.558614Z","shell.execute_reply":"2025-12-23T01:30:30.029361Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 1. Dataset","metadata":{}},{"cell_type":"code","source":"class BiomassDataset(Dataset):\n    TARGET_ORDER = ['Dry_Green_g', 'Dry_Clover_g', 'Dry_Dead_g', 'GDM_g', 'Dry_Total_g']\n    \n    def __init__(self, csv_file, root_dir, mode='train', transform=None):\n        self.root_dir = root_dir\n        self.mode = mode\n        self.transform = transform\n        \n        self.df = pd.read_csv(csv_file)\n        \n        if self.mode == 'train':\n            self.df['image_id'] = self.df['sample_id'].apply(lambda x: x.split('__')[0])\n            self.data = self.df.pivot_table(\n                index=['image_id', 'image_path'], \n                columns='target_name', \n                values='target'\n            ).reset_index()\n            \n            for t in self.TARGET_ORDER:\n                if t not in self.data.columns:\n                    raise ValueError(f\"Missing target column: {t}\")\n            \n            self.targets = self.data[self.TARGET_ORDER].values.astype(np.float32)\n            self.image_paths = self.data['image_path'].values\n            self.sample_ids = self.data['image_id'].values\n            \n        else: # test mode\n            self.df['image_id'] = self.df['sample_id'].apply(lambda x: x.split('__')[0])\n            self.data = self.df.drop_duplicates(subset=['image_id'])\n            self.image_paths = self.data['image_path'].values\n            self.sample_ids = self.data['image_id'].values\n\n    def __len__(self):\n        return len(self.image_paths)\n\n    def __getitem__(self, idx):\n        img_name = os.path.join(self.root_dir, self.image_paths[idx])\n        try:\n            image = Image.open(img_name).convert('RGB')\n        except Exception as e:\n            image = Image.new('RGB', (512, 512))\n            \n        if self.transform:\n            image = self.transform(image)\n        \n        image_id = self.sample_ids[idx]\n        if self.mode == 'train':\n            targets = self.targets[idx]\n            return image, torch.tensor(targets), image_id\n        else:\n            return image, image_id\n\ndef get_transforms(img_size=224, mode='train'):\n    if mode == 'train':\n        return transforms.Compose([\n            transforms.Resize((img_size, img_size)),\n            transforms.RandomHorizontalFlip(),\n            transforms.RandomVerticalFlip(),\n            transforms.ColorJitter(brightness=0.1, contrast=0.1),\n            transforms.ToTensor(),\n            transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])\n        ])\n    else:\n        return transforms.Compose([\n            transforms.Resize((img_size, img_size)),\n            transforms.ToTensor(),\n            transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])\n        ])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-23T01:30:30.032025Z","iopub.execute_input":"2025-12-23T01:30:30.032359Z","iopub.status.idle":"2025-12-23T01:30:30.042855Z","shell.execute_reply.started":"2025-12-23T01:30:30.032340Z","shell.execute_reply":"2025-12-23T01:30:30.041980Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 2. Model","metadata":{}},{"cell_type":"code","source":"class BiomassModel(nn.Module):\n    def __init__(self, output_base_dim=3, pretrained=True, weights_path=None):\n        super(BiomassModel, self).__init__()\n        \n        # Weights Logic\n        if weights_path and os.path.exists(weights_path):\n            print(f\"Loading weights from {weights_path}\")\n            self.backbone = models.resnet50(weights=None)\n            self.backbone.load_state_dict(torch.load(weights_path))\n        else:\n            weights = models.ResNet50_Weights.DEFAULT if pretrained else None\n            try:\n                self.backbone = models.resnet50(weights=weights)\n            except:\n                # Random weights fallback if path invalid and internet off\n                print(\"Warning: Download failed. Using random weights.\")\n                self.backbone = models.resnet50(weights=None)\n        \n        in_features = self.backbone.fc.in_features\n        self.backbone.fc = nn.Sequential(\n            nn.Linear(in_features, 512),\n            nn.ReLU(),\n            nn.Dropout(0.2),\n            nn.Linear(512, output_base_dim)\n        )\n        \n        self.output_act = nn.Softplus()\n        \n    def forward(self, x):\n        base_preds = self.output_act(self.backbone(x))\n        green = base_preds[:, 0].unsqueeze(1)\n        clover = base_preds[:, 1].unsqueeze(1)\n        dead = base_preds[:, 2].unsqueeze(1)\n        gdm = green + clover\n        total = gdm + dead\n        return torch.cat([green, clover, dead, gdm, total], dim=1)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-23T01:30:30.043717Z","iopub.execute_input":"2025-12-23T01:30:30.044039Z","iopub.status.idle":"2025-12-23T01:30:30.068725Z","shell.execute_reply.started":"2025-12-23T01:30:30.044013Z","shell.execute_reply":"2025-12-23T01:30:30.068072Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 3. Training","metadata":{}},{"cell_type":"code","source":"def weighted_mse_loss(input, target, weights):\n    w = weights.view(1, -1).to(input.device)\n    se = (input - target) ** 2\n    wse = w * se\n    return wse.sum(dim=1).mean()\n\ndef train_loop():\n    loss_weights = torch.tensor([0.1, 0.1, 0.1, 0.2, 0.5])\n\n    train_csv = os.path.join(Config.data_root, 'train.csv')\n    dataset = BiomassDataset(\n        csv_file=train_csv,\n        root_dir=Config.data_root,\n        mode='train',\n        transform=get_transforms(Config.img_size, 'train')\n    )\n    \n    val_size = int(len(dataset) * 0.2)\n    train_size = len(dataset) - val_size\n    train_ds, val_ds = random_split(dataset, [train_size, val_size])\n    \n    train_loader = DataLoader(train_ds, batch_size=Config.batch_size, shuffle=True, num_workers=2)\n    val_loader = DataLoader(val_ds, batch_size=Config.batch_size, shuffle=False, num_workers=2)\n    \n    model = BiomassModel(pretrained=True, weights_path=Config.resnet_weights_path).to(Config.device)\n    optimizer = optim.AdamW(model.parameters(), lr=Config.lr)\n    scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=Config.epochs)\n    \n    best_loss = float('inf')\n    best_model_wts = copy.deepcopy(model.state_dict())\n    \n    for epoch in range(Config.epochs):\n        print(f\"Epoch {epoch+1}/{Config.epochs}\")\n        model.train()\n        train_loss = 0.0\n        for imgs, tars, _ in train_loader:\n            imgs, tars = imgs.to(Config.device), tars.to(Config.device)\n            optimizer.zero_grad()\n            outputs = model(imgs)\n            loss = weighted_mse_loss(outputs, tars, loss_weights)\n            loss.backward()\n            optimizer.step()\n            train_loss += loss.item() * imgs.size(0)\n        \n        print(f\"Train Loss: {train_loss / len(train_ds):.4f}\")\n        \n        model.eval()\n        val_loss = 0.0\n        with torch.no_grad():\n            for imgs, tars, _ in val_loader:\n                imgs, tars = imgs.to(Config.device), tars.to(Config.device)\n                outputs = model(imgs)\n                loss = weighted_mse_loss(outputs, tars, loss_weights)\n                val_loss += loss.item() * imgs.size(0)\n        \n        epoch_val_loss = val_loss / len(val_ds)\n        print(f\"Val Loss: {epoch_val_loss:.4f}\")\n        \n        if epoch_val_loss < best_loss:\n            best_loss = epoch_val_loss\n            best_model_wts = copy.deepcopy(model.state_dict())\n            torch.save(best_model_wts, 'best_model.pth')\n        scheduler.step()\n    \n    print(\"Training Complete.\")\n    model.load_state_dict(best_model_wts)\n    return model\n\nif __name__ == '__main__':\n    model = train_loop()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-23T01:30:30.069451Z","iopub.execute_input":"2025-12-23T01:30:30.069680Z","iopub.status.idle":"2025-12-23T01:32:24.614306Z","shell.execute_reply.started":"2025-12-23T01:30:30.069655Z","shell.execute_reply":"2025-12-23T01:32:24.613541Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 4. Inference","metadata":{}},{"cell_type":"code","source":"def inference(model):\n    test_csv = os.path.join(Config.data_root, 'test.csv')\n    test_ds = BiomassDataset(\n        csv_file=test_csv,\n        root_dir=Config.data_root,\n        mode='test',\n        transform=get_transforms(Config.img_size, 'test')\n    )\n    test_loader = DataLoader(test_ds, batch_size=Config.batch_size, shuffle=False, num_workers=2)\n    model.eval()\n    results = []\n    target_names = ['Dry_Green_g', 'Dry_Clover_g', 'Dry_Dead_g', 'GDM_g', 'Dry_Total_g']\n    with torch.no_grad():\n        for imgs, ids in test_loader:\n            imgs = imgs.to(Config.device)\n            outputs = model(imgs)\n            outputs = outputs.cpu().numpy()\n            for i in range(len(ids)):\n                img_id = ids[i]\n                vals = outputs[i]\n                for j, tname in enumerate(target_names):\n                    results.append({'sample_id': f\"{img_id}__{tname}\", 'target': vals[j]})\n    sub_df = pd.DataFrame(results)\n    sub_df.to_csv('submission.csv', index=False)\n    print(\"Submission saved!\")\n\ninference(model)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-23T01:32:24.615900Z","iopub.execute_input":"2025-12-23T01:32:24.616272Z","iopub.status.idle":"2025-12-23T01:32:24.934838Z","shell.execute_reply.started":"2025-12-23T01:32:24.616244Z","shell.execute_reply":"2025-12-23T01:32:24.934004Z"}},"outputs":[],"execution_count":null}]}