{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","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":"gpu","dataSources":[{"sourceId":112509,"databundleVersionId":14254895,"isSourceIdPinned":false,"sourceType":"competition"},{"sourceId":13636070,"sourceType":"datasetVersion","datasetId":8667402},{"sourceId":46407609,"sourceType":"kernelVersion"}],"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import os\nimport gc\nimport glob\nimport sys\nimport math\nimport random\nimport numpy as np\nimport pandas as pd\nfrom tqdm.auto import tqdm\nfrom sklearn.model_selection import GroupKFold\nimport torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom torch.utils.data import Dataset, DataLoader\nfrom torch.optim.lr_scheduler import OneCycleLR\nimport albumentations as A\nfrom albumentations.pytorch import ToTensorV2\nimport timm\nimport cv2\nimport warnings\n\nwarnings.filterwarnings('ignore')\n\n# ====================================================\n# CONFIGURATION\n# ====================================================\nclass Config:\n    seed = 42\n    # We use B3 or B4. B4 is stronger but requires more GPU memory.\n    model_name = 'tf_efficientnet_b4_ns' \n    img_size = 384\n    batch_size = 16 \n    epochs = 10 \n    lr = 5e-4\n    weight_decay = 1e-6\n    num_folds = 5\n    num_workers = 2 \n    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n    \n    # Competition targets\n    target_cols = ['Dry_Green_g', 'Dry_Dead_g', 'Dry_Clover_g', 'GDM_g', 'Dry_Total_g']\n    # Weights: Total is 50% of the score, GDM is 20%\n    target_weights = [0.1, 0.1, 0.1, 0.2, 0.5] \n\nCONFIG = Config()\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(CONFIG.seed)\n\n# ====================================================\n# DATA PREPROCESSING (Wide Format)\n# ====================================================\ndef get_data():\n    data_dir = '/kaggle/input/csiro-biomass'\n    train_raw = pd.read_csv(f'{data_dir}/train.csv')\n    test_raw = pd.read_csv(f'{data_dir}/test.csv')\n    \n    # 1. Pivot Train Data: Convert long format to one row per image\n    train_df = train_raw.pivot_table(\n        index='image_path', \n        columns='target_name', \n        values='target'\n    ).reset_index()\n    \n    # 2. Add Metadata back (Crucial for GroupKFold on Dates)\n    meta_df = train_raw[['image_path', 'Sampling_Date']].drop_duplicates()\n    train_df = train_df.merge(meta_df, on='image_path', how='left')\n    train_df = train_df[['image_path', 'Sampling_Date'] + CONFIG.target_cols]\n    \n    # 3. Fix Paths\n    train_df['image_path'] = train_df['image_path'].apply(lambda x: f'{data_dir}/{x}')\n    \n    # 4. Prepare Test Data\n    test_df = test_raw[['image_path']].drop_duplicates().reset_index(drop=True)\n    test_df['image_path'] = test_df['image_path'].apply(lambda x: f'{data_dir}/{x}')\n    \n    return train_df, test_df, test_raw\n\ntrain_df, test_df, submission_template = get_data()\n\n# ====================================================\n# DATASET\n# ====================================================\nclass BiomassDataset(Dataset):\n    def __init__(self, df, transforms=None, is_train=True):\n        self.df = df\n        self.transforms = transforms\n        self.is_train = is_train\n        self.image_paths = df['image_path'].values\n        if self.is_train:\n            self.targets = df[CONFIG.target_cols].values\n            \n    def __len__(self):\n        return len(self.df)\n    \n    def __getitem__(self, idx):\n        path = self.image_paths[idx]\n        image = cv2.imread(path)\n        if image is None:\n            image = np.zeros((CONFIG.img_size, CONFIG.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.is_train:\n            target = self.targets[idx]\n            # Log1p transform to handle outliers/skew\n            target = np.log1p(target)\n            return image, torch.tensor(target, dtype=torch.float32)\n        return image\n\ndef get_transforms(data):\n    if data == 'train':\n        return A.Compose([\n            A.Resize(CONFIG.img_size, CONFIG.img_size),\n            A.HorizontalFlip(p=0.5),\n            A.VerticalFlip(p=0.5),\n            A.Rotate(limit=30, p=0.5),\n            A.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2, p=0.5),\n            A.Normalize(),\n            ToTensorV2(),\n        ])\n    elif data == 'valid':\n        return A.Compose([\n            A.Resize(CONFIG.img_size, CONFIG.img_size),\n            A.Normalize(),\n            ToTensorV2(),\n        ])\n\n# ====================================================\n# OFFLINE-SAFE MODEL\n# ====================================================\nclass BiomassModel(nn.Module):\n    def __init__(self, model_name):\n        super().__init__()\n        \n        # 1. Initialize model WITHOUT pretrained weights (prevents internet error)\n        self.backbone = timm.create_model(model_name, pretrained=False, num_classes=0)\n        \n        # 2. Manually load weights from the attached dataset\n        self.load_offline_weights(model_name)\n        \n        self.n_features = self.backbone.num_features\n        self.head = nn.Sequential(\n            nn.Linear(self.n_features, 512),\n            nn.BatchNorm1d(512),\n            nn.SiLU(),\n            nn.Dropout(0.3),\n            nn.Linear(512, 5) # 5 targets\n        )\n        \n    def load_offline_weights(self, model_name):\n        # Look for weights in common dataset paths\n        search_paths = [\n            f'/kaggle/input/timm-efficientnet/{model_name}*.pth',\n            f'/kaggle/input/efficientnet-pytorch/{model_name}*.pth',\n            f'/kaggle/input/**/*{model_name}*.pth' # Wildcard search\n        ]\n        \n        found_path = None\n        for pattern in search_paths:\n            files = glob.glob(pattern, recursive=True)\n            if files:\n                found_path = files[0]\n                break\n        \n        if found_path:\n            print(f\"Loading offline weights from: {found_path}\")\n            checkpoint = torch.load(found_path, map_location='cpu')\n            \n            # Handle different checkpoint formats\n            if 'state_dict' in checkpoint: state_dict = checkpoint['state_dict']\n            elif 'model' in checkpoint: state_dict = checkpoint['model']\n            else: state_dict = checkpoint\n            \n            # Remove prefixes if necessary\n            state_dict = {k.replace('backbone.', ''): v for k, v in state_dict.items()}\n            \n            # Load weights (strict=False allows loading backbone even if head differs)\n            self.backbone.load_state_dict(state_dict, strict=False)\n        else:\n            print(f\"!! WARNING: No weights found for {model_name}. Training from scratch.\")\n\n    def forward(self, x):\n        features = self.backbone(x)\n        output = self.head(features)\n        return output\n\n# ====================================================\n# LOSS & TRAINING\n# ====================================================\nclass WeightedMSELoss(nn.Module):\n    def __init__(self, weights):\n        super().__init__()\n        self.weights = torch.tensor(weights).to(CONFIG.device)\n    \n    def forward(self, preds, targets):\n        # MSE per sample per target\n        loss = (preds - targets) ** 2\n        # Average over batch\n        loss = torch.mean(loss, dim=0)\n        # Weighted sum\n        return torch.sum(loss * self.weights)\n\ndef train_epoch(model, loader, optimizer, criterion, device):\n    model.train()\n    running_loss = 0.0\n    scaler = torch.cuda.amp.GradScaler() # Mixed Precision for speed\n    \n    for images, targets in tqdm(loader, leave=False):\n        images, targets = images.to(device), targets.to(device)\n        \n        optimizer.zero_grad()\n        with torch.cuda.amp.autocast():\n            outputs = model(images)\n            loss = criterion(outputs, targets)\n            \n        scaler.scale(loss).backward()\n        scaler.step(optimizer)\n        scaler.update()\n        \n        running_loss += loss.item()\n        \n    return running_loss / len(loader)\n\n@torch.no_grad()\ndef validate(model, loader, criterion, device):\n    model.eval()\n    running_loss = 0.0\n    preds = []\n    \n    for images, targets in loader:\n        images, targets = images.to(device), targets.to(device)\n        outputs = model(images)\n        loss = criterion(outputs, targets)\n        running_loss += loss.item()\n        preds.append(outputs.cpu().numpy())\n        \n    return running_loss / len(loader), np.concatenate(preds)\n\n# ====================================================\n# MAIN RUNNER\n# ====================================================\nif __name__ == '__main__':\n    \n    # GroupKFold to prevent date leakage\n    gkf = GroupKFold(n_splits=CONFIG.num_folds)\n    folds = list(gkf.split(train_df, groups=train_df['Sampling_Date']))\n    \n    # Containers for results\n    test_preds_accum = np.zeros((len(test_df), 5))\n    \n    # Prepare Test Loader ONCE\n    test_ds = BiomassDataset(test_df, get_transforms('valid'), is_train=False)\n    test_loader = DataLoader(test_ds, batch_size=CONFIG.batch_size, shuffle=False, num_workers=CONFIG.num_workers)\n    \n    for fold, (train_idx, val_idx) in enumerate(folds):\n        print(f'\\n=== Fold {fold+1}/{CONFIG.num_folds} ===')\n        \n        # Split Data\n        train_sub = train_df.iloc[train_idx].reset_index(drop=True)\n        val_sub = train_df.iloc[val_idx].reset_index(drop=True)\n        \n        train_loader = DataLoader(BiomassDataset(train_sub, get_transforms('train')), \n                                  batch_size=CONFIG.batch_size, shuffle=True, num_workers=CONFIG.num_workers, drop_last=True)\n        val_loader = DataLoader(BiomassDataset(val_sub, get_transforms('valid')), \n                                batch_size=CONFIG.batch_size, shuffle=False, num_workers=CONFIG.num_workers)\n        \n        # Init Model\n        model = BiomassModel(CONFIG.model_name).to(CONFIG.device)\n        \n        optimizer = optim.AdamW(model.parameters(), lr=CONFIG.lr, weight_decay=CONFIG.weight_decay)\n        criterion = WeightedMSELoss(CONFIG.target_weights)\n        \n        # OneCycleLR is often faster/better for convergence\n        scheduler = OneCycleLR(optimizer, max_lr=CONFIG.lr, steps_per_epoch=len(train_loader), epochs=CONFIG.epochs)\n        \n        best_loss = np.inf\n        best_model_path = f'best_model_fold_{fold}.pth'\n        \n        # Train Loop\n        for epoch in range(CONFIG.epochs):\n            train_loss = train_epoch(model, train_loader, optimizer, criterion, CONFIG.device)\n            val_loss, _ = validate(model, val_loader, criterion, CONFIG.device)\n            scheduler.step()\n            \n            print(f'Epoch {epoch+1}: Train Loss {train_loss:.4f} | Val Loss {val_loss:.4f}')\n            \n            if val_loss < best_loss:\n                best_loss = val_loss\n                torch.save(model.state_dict(), best_model_path)\n        \n        # Inference on Test Set immediately to save disk/ram\n        model.load_state_dict(torch.load(best_model_path))\n        model.eval()\n        \n        fold_preds = []\n        with torch.no_grad():\n            for images in tqdm(test_loader, desc=f'Infer Fold {fold+1}'):\n                images = images.to(CONFIG.device)\n                # TTA: Average Original + Flip\n                p1 = model(images)\n                p2 = model(torch.flip(images, [3]))\n                fold_preds.append(((p1 + p2) / 2).cpu().numpy())\n                \n        test_preds_accum += np.concatenate(fold_preds) / CONFIG.num_folds\n        \n        # Cleanup\n        del model, optimizer, train_loader, val_loader\n        gc.collect()\n        torch.cuda.empty_cache()\n\n    # ====================================================\n    # SUBMISSION\n    # ====================================================\n    print(\"\\nGenerating Submission...\")\n    \n    # Inverse Log Transform\n    final_preds = np.expm1(test_preds_accum)\n    \n    # Map results back to submission format\n    pred_map = {}\n    for i, row in test_df.iterrows():\n        pred_map[row['image_path']] = final_preds[i]\n        \n    submission = submission_template.copy()\n    \n    def fill_target(row):\n        path = f\"/kaggle/input/csiro-biomass/{row['image_path']}\"\n        if path in pred_map:\n            preds = pred_map[path]\n            target_idx = CONFIG.target_cols.index(row['target_name'])\n            return max(0, preds[target_idx]) # Ensure positive biomass\n        return 0.0\n        \n    submission['target'] = submission.apply(fill_target, axis=1)\n    submission = submission[['sample_id', 'target']]\n    submission.to_csv('submission.csv', index=False)\n    \n    print(\"Success! submission.csv created.\")","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-11-19T16:00:44.137685Z","iopub.execute_input":"2025-11-19T16:00:44.138399Z","iopub.status.idle":"2025-11-19T16:21:35.957849Z","shell.execute_reply.started":"2025-11-19T16:00:44.138374Z","shell.execute_reply":"2025-11-19T16:21:35.957173Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}