{"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":"nvidiaTeslaT4","dataSources":[{"sourceId":112509,"databundleVersionId":14254895,"sourceType":"competition"},{"sourceId":14282053,"sourceType":"datasetVersion","datasetId":9115674}],"dockerImageVersionId":31236,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"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# ====================================================\n# Configuration\n# ====================================================\nSUBMISSION_DESCRIPTION = \"EfficientNet-B4 Model @ 512px with TTA. Predicts Green, Clover, and Dead components independently. Trained with Weighted MSE Loss.\"\n\nclass Config:\n    seed = 42\n    img_size = 512  # Upgraded to 512\n    batch_size = 8 # Reduced to 8 to avoid OOM with EfficientNet-B4 @ 512px\n    epochs = 15     # increased slightly\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 on Kaggle!\n    resnet_weights_path = '/kaggle/input/efficientnet-b4/efficientnet_b4.pth' \n    \n    # Auto-discovery for weights (in case of naming differences)\n    if os.path.exists('/kaggle/input'):\n        found_weights = False\n        for dirname, _, filenames in os.walk('/kaggle/input'):\n            if 'efficientnet_b4.pth' in filenames:\n                resnet_weights_path = os.path.join(dirname, 'efficientnet_b4.pth')\n                print(f\"Auto-discovered weights at: {resnet_weights_path}\")\n                found_weights = True\n                break\n        if not found_weights:\n            print(f\"Warning: Weights not found at default path {resnet_weights_path} and auto-discovery failed.\") \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        # Local path\n        data_root = 'csiro-biomass'\n        \nprint(f\"Using device: {Config.device}\")\nprint(f\"Data Root: {Config.data_root}\")\nprint(f\"Generatng submission for: {SUBMISSION_DESCRIPTION}\")\n\n# ====================================================\n# Dataset\n# ====================================================\nclass 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        # Robust image loading\n        try:\n            image = Image.open(img_name).convert('RGB')\n        except Exception as e:\n            # Fallback for missing images in test set if any\n            image = Image.new('RGB', (Config.img_size, Config.img_size))\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=512, 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, saturation=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        ])\n\n# ====================================================\n# Model (EfficientNet-B4)\n# ====================================================\nclass BiomassModel(nn.Module):\n    def __init__(self, output_base_dim=3, pretrained=True, weights_path=None):\n        super(BiomassModel, self).__init__()\n        \n        # Load backbone: EfficientNet-B4\n        if weights_path and os.path.exists(weights_path):\n            print(f\"Loading weights from {weights_path}\")\n            self.backbone = models.efficientnet_b4(weights=None)\n            self.backbone.load_state_dict(torch.load(weights_path))\n        else:\n            # If internet is ON or running locally without custom weights path\n            weights = models.EfficientNet_B4_Weights.DEFAULT if pretrained else None\n            try:\n                self.backbone = models.efficientnet_b4(weights=weights)\n            except:\n                print(\"Warning: Download failed. Using random weights.\")\n                self.backbone = models.efficientnet_b4(weights=None)\n        \n        # EfficientNet classifier replacement\n        # The classifier is a Sequential block: [Dropout, Linear]\n        # We need to replace it to output 3 base components\n        in_features = self.backbone.classifier[1].in_features\n        \n        self.backbone.classifier = nn.Sequential(\n            nn.Dropout(p=0.4, inplace=True),\n            nn.Linear(in_features, output_base_dim)\n        )\n        \n        self.output_act = nn.Softplus()\n        \n    def forward(self, x):\n        # EfficientNet forward returns logits directly\n        base_preds = self.output_act(self.backbone(x))\n        \n        green = base_preds[:, 0].unsqueeze(1)\n        clover = base_preds[:, 1].unsqueeze(1)\n        dead = base_preds[:, 2].unsqueeze(1)\n        \n        gdm = green + clover\n        total = gdm + dead\n        \n        # Return 5 components\n        return torch.cat([green, clover, dead, gdm, total], dim=1)\n\n# ====================================================\n# Loss & Training\n# ====================================================\ndef 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    if not os.path.exists(train_csv):\n        print(\"Train CSV not found. Skipping training (assuming inference only mode or wrong path).\")\n        return None\n\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], generator=torch.Generator().manual_seed(Config.seed))\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    torch.cuda.empty_cache() # Clear memory before training\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        avg_train_loss = train_loss / len(train_ds)\n        print(f\"Train Loss: {avg_train_loss:.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            print(\"Saved Best Model\")\n            \n        scheduler.step()\n    \n    print(\"Training Complete.\")\n    model.load_state_dict(best_model_wts)\n    return model\n\n# ====================================================\n# Inference with TTA\n# ====================================================\ndef inference(model):\n    test_csv = os.path.join(Config.data_root, 'test.csv')\n    if not os.path.exists(test_csv):\n        print(\"Test CSV not found.\")\n        return\n\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    \n    print(\"Starting inference with TTA (Original + HFlip + VFlip)...\")\n    \n    with torch.no_grad():\n        for imgs, ids in test_loader:\n            imgs = imgs.to(Config.device)\n            \n            # TTA 1: Original\n            out1 = model(imgs)\n            \n            # TTA 2: Horizontal Flip\n            out2 = model(torch.flip(imgs, [3]))\n            \n            # TTA 3: Vertical Flip\n            out3 = model(torch.flip(imgs, [2]))\n            \n            # Average\n            outputs = (out1 + out2 + out3) / 3.0\n            outputs = outputs.cpu().numpy()\n            \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                    \n    sub_df = pd.DataFrame(results)\n    sub_df.to_csv('submission.csv', index=False)\n    print(\"Submission saved to submission.csv!\")\n\nif __name__ == '__main__':\n    # Train\n    model = train_loop()\n    if model:\n        # Inference\n        inference(model)\n","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-12-24T14:51:35.408707Z","iopub.execute_input":"2025-12-24T14:51:35.409016Z","iopub.status.idle":"2025-12-24T14:57:45.021480Z","shell.execute_reply.started":"2025-12-24T14:51:35.408985Z","shell.execute_reply":"2025-12-24T14:57:45.020677Z"}},"outputs":[],"execution_count":null}]}