{"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,"isSourceIdPinned":false,"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":"\n   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, Subset\nfrom torchvision import transforms, models\nfrom sklearn.model_selection import KFold\n\n# ====================================================\n# Configuration\n# ====================================================\nSUBMISSION_DESCRIPTION = \"5-Fold Ensemble | EfficientNet-B4 | Weighted MSE Loss | 20 Epochs. Reverted to MSE for R2 alignment.\"\n\nclass Config:\n    seed = 42\n    img_size = 512\n    batch_size = 8\n    epochs = 20\n    lr = 1e-4\n    n_folds = 5\n    folds_to_train = [0, 1, 2, 3, 4]\n    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n    \n    # Offline Weights Config\n    resnet_weights_path = '/kaggle/input/efficientnet-b4/efficientnet_b4.pth' \n    \n    # Auto-discovery for weights\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}\") \n    \n    # Data Root\n    if os.path.exists('/kaggle/input'):\n        data_root = '/kaggle/input/csiro-biomass'\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\n        \nprint(f\"Using device: {Config.device}\")\nprint(f\"Data Root: {Config.data_root}\")\nprint(f\"Config: {SUBMISSION_DESCRIPTION}\")\n\n# ====================================================\n# Seed Everything\n# ====================================================\ndef seed_everything(seed=42):\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(Config.seed)\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        self.df = pd.read_csv(csv_file)\n        \n        if self.mode == 'train':\n            # Extract unique image IDs\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            # Ensure consistent order\n            self.data = self.data.sort_values('image_id').reset_index(drop=True)\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\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', (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.RandomRotation(15),\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\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        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            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        in_features = self.backbone.classifier[1].in_features\n        self.backbone.classifier = nn.Sequential(\n            nn.Dropout(p=0.4, inplace=True),\n            nn.Linear(in_features, output_base_dim)\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)\n\n# ====================================================\n# Losses\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\n# ====================================================\n# Training Loop (K-Fold)\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!\")\n        return []\n\n    full_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    # K-Fold\n    kf = KFold(n_splits=Config.n_folds, shuffle=True, random_state=Config.seed)\n    \n    trained_models = []\n    \n    for fold, (train_idx, val_idx) in enumerate(kf.split(full_dataset)):\n        if fold not in Config.folds_to_train:\n            continue\n            \n        print(f\"\\n{'='*20} Fold {fold+1}/{Config.n_folds} {'='*20}\")\n        \n        train_sub = Subset(full_dataset, train_idx)\n        val_sub = Subset(full_dataset, val_idx)\n        \n        train_loader = DataLoader(train_sub, batch_size=Config.batch_size, shuffle=True, num_workers=2)\n        val_loader = DataLoader(val_sub, 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_wts = copy.deepcopy(model.state_dict())\n        \n        torch.cuda.empty_cache()\n        \n        for epoch in range(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                # Reverted to MSE\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 = train_loss / len(train_sub)\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            avg_val = val_loss / len(val_sub)\n            \n            print(f\"Epoch {epoch+1} | Train: {avg_train:.4f} | Val: {avg_val:.4f}\")\n            \n            if avg_val < best_loss:\n                best_loss = avg_val\n                best_wts = copy.deepcopy(model.state_dict())\n                \n            scheduler.step()\n            \n        save_name = f'model_fold{fold}.pth'\n        torch.save(best_wts, save_name)\n        print(f\"Fold {fold} Best Loss: {best_loss:.4f} - Saved to {save_name}\")\n        \n        model.load_state_dict(best_wts)\n        trained_models.append(model)\n        \n        del model, optimizer, scheduler\n        torch.cuda.empty_cache()\n        \n    return trained_models\n\n# ====================================================\n# Ensemble Inference\n# ====================================================\ndef inference_ensemble():\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    \n    # Load all models found\n    models_list = []\n    print(\"Loading models for ensemble...\")\n    for i in range(Config.n_folds):\n        path = f'model_fold{i}.pth'\n        if os.path.exists(path):\n            print(f\"Loading {path}...\")\n            m = BiomassModel(pretrained=False).to(Config.device)\n            m.load_state_dict(torch.load(path, map_location=Config.device))\n            m.eval()\n            models_list.append(m)\n            \n    if not models_list:\n        print(\"No models found! Cannot run inference.\")\n        return\n\n    print(f\"Ensembling {len(models_list)} models with TTA...\")\n    results = []\n    target_names = ['Dry_Green_g', 'Dry_Clover_g', 'Dry_Dead_g', 'GDM_g', 'Dry_Total_g']\n    \n    with torch.no_grad():\n        for imgs, ids in test_loader:\n            imgs = imgs.to(Config.device)\n            \n            # TTA Variations\n            imgs_flip_h = torch.flip(imgs, [3])\n            imgs_flip_v = torch.flip(imgs, [2])\n            \n            batch_preds = []\n            \n            # For each model in ensemble\n            for m in models_list:\n                p1 = m(imgs)\n                p2 = m(imgs_flip_h)\n                p3 = m(imgs_flip_v)\n                # Average TTA for this model\n                p_avg = (p1 + p2 + p3) / 3.0\n                batch_preds.append(p_avg)\n            \n            # Average across all models in ensemble\n            final_pred = torch.stack(batch_preds).mean(dim=0)\n            final_pred = final_pred.cpu().numpy()\n            \n            for i in range(len(ids)):\n                img_id = ids[i]\n                vals = final_pred[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(\"Ensemble submission saved to submission.csv!\")\n\nif __name__ == '__main__':\n    # 1. Train 5 Folds\n    train_loop()\n    \n    # 2. Inference Ensemble\n    inference_ensemble()\n\n\n\n    \n   \n       \n           \n            \n         \n            \n          \n        \n        \n\n\n    \n","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-12-24T17:20:35.178441Z","iopub.execute_input":"2025-12-24T17:20:35.178757Z","iopub.status.idle":"2025-12-24T18:03:01.797104Z","shell.execute_reply.started":"2025-12-24T17:20:35.178724Z","shell.execute_reply":"2025-12-24T18:03:01.796130Z"}},"outputs":[],"execution_count":null}]}