{"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":13865614,"sourceType":"datasetVersion","datasetId":8833965},{"sourceId":658191,"sourceType":"modelInstanceVersion","isSourceIdPinned":false,"modelInstanceId":497610,"modelId":512933}],"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import numpy as np\nimport pandas as pd\nfrom pathlib import Path\nimport warnings\nwarnings.filterwarnings('ignore')\nimport torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom torch.utils.data import Dataset, DataLoader\nimport torchvision.transforms as transforms\nfrom PIL import Image\nfrom sklearn.model_selection import GroupKFold\nfrom tqdm.auto import tqdm\nimport os","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================================\n# CONFIGURATION - OPTIMIZED FOR PRODUCTION\n# ============================================================================\nclass Config:\n    DATA_DIR = Path('/kaggle/input/csiro-biomass')\n    TRAIN_CSV = DATA_DIR / 'train.csv'\n    TEST_CSV = DATA_DIR / 'test.csv'\n    \n    IMG_SIZE = 224\n    BATCH_SIZE = 8\n    N_FOLDS = 4\n    EPOCHS = 20\n    LR = 1e-5  # More stable learning rate\n    \n    TARGETS = ['Dry_Total_g', 'Dry_Green_g', 'Dry_Dead_g', 'Dry_Clover_g', 'GDM_g']\n    TARGET_WEIGHTS = {\n        'Dry_Total_g': 0.5,\n        'Dry_Green_g': 0.1,\n        'Dry_Dead_g': 0.1,\n        'Dry_Clover_g': 0.1,\n        'GDM_g': 0.2\n    }\n    \n    # Default metadata values for test inference (from training statistics)\n    DEFAULT_META = {\n        'Pre_GSHH_NDVI': 0.65,  # Training mean\n        'Height_Ave_cm': 25.0,  # Training mean\n        'month': 6,            # Average month\n        'State': 'Vic'         # Most common state\n    }\n    \n    DEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n    SEED = 42\n    NUM_WORKERS = 0\n\ndef set_seed(seed=42):\n    np.random.seed(seed)\n    torch.manual_seed(seed)\n    torch.cuda.manual_seed_all(seed)\n\nset_seed(Config.SEED)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================================\n# DATA LOADING WITH ROBUST METADATA HANDLING\n# ============================================================================\ndef load_and_prepare_data():\n    \"\"\"Load training data and compute metadata statistics for test inference\"\"\"\n    df = pd.read_csv(Config.TRAIN_CSV)\n    df['base_sample_id'] = df['sample_id'].str.split('__').str[0]\n    \n    # Pivot to wide format\n    df_wide = df.pivot(\n        index=['base_sample_id', 'image_path', 'Sampling_Date', 'State', \n               'Species', 'Pre_GSHH_NDVI', 'Height_Ave_cm'],\n        columns='target_name',\n        values='target'\n    ).reset_index()\n    \n    df_wide.rename(columns={'base_sample_id': 'sample_id'}, inplace=True)\n    df_wide = df_wide.dropna(subset=Config.TARGETS)\n    \n    # Compute training statistics for test inference fallbacks\n    Config.DEFAULT_META['Pre_GSHH_NDVI'] = df_wide['Pre_GSHH_NDVI'].mean()\n    Config.DEFAULT_META['Height_Ave_cm'] = df_wide['Height_Ave_cm'].mean()\n    Config.DEFAULT_META['month'] = pd.to_datetime(df_wide['Sampling_Date']).dt.month.mean()\n    Config.DEFAULT_META['State'] = df_wide['State'].mode()[0]\n    \n    # Create location groups\n    df_wide['sampling_date'] = pd.to_datetime(df_wide['Sampling_Date'])\n    df_wide['month'] = df_wide['sampling_date'].dt.month\n    \n    for col in ['Pre_GSHH_NDVI', 'Height_Ave_cm']:\n        try:\n            df_wide[f'{col}_tertile'] = pd.qcut(df_wide[col], q=3, labels=['low', 'mid', 'high'], duplicates='drop')\n        except:\n            df_wide[f'{col}_tertile'] = pd.cut(df_wide[col], bins=3, labels=['low', 'mid', 'high'])\n    \n    df_wide['location_group'] = (\n        df_wide['State'].astype(str) + '_' + \n        df_wide['Species'].astype(str) + '_' +\n        df_wide['Pre_GSHH_NDVI_tertile'].astype(str) + '_' +\n        df_wide['Height_Ave_cm_tertile'].astype(str)\n    ).str.replace(' ', '_')\n    \n    # GroupKFold setup\n    gkf = GroupKFold(n_splits=Config.N_FOLDS)\n    df_wide['fold'] = -1\n    for fold, (_, val_idx) in enumerate(gkf.split(df_wide, groups=df_wide['location_group'])):\n        df_wide.loc[df_wide.index[val_idx], 'fold'] = fold\n    \n    return df_wide","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================================\n# ROBUST DATASET CLASS\n# ============================================================================\nclass BiomassDataset(Dataset):\n    def __init__(self, dataframe, transform=None, is_test=False):\n        self.df = dataframe.reset_index(drop=True)\n        self.transform = transform\n        self.is_test = is_test\n        self.states = ['Tas', 'Vic', 'NSW', 'WA']\n        \n    def __len__(self):\n        return len(self.df)\n    \n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        img_path = Config.DATA_DIR / row['image_path']\n        \n        try:\n            img = Image.open(img_path).convert('RGB')\n        except Exception as e:\n            print(f\"Warning: Could not load image {img_path}: {e}\")\n            img = Image.new('RGB', (Config.IMG_SIZE, Config.IMG_SIZE), (128, 128, 128))\n        \n        if self.transform:\n            img = self.transform(img)\n        \n        if self.is_test:\n            # For test inference - create dummy targets\n            targets = torch.zeros(len(Config.TARGETS), dtype=torch.float32)\n            meta = self._extract_metadata(row, is_test=True)\n            return img, meta, targets, row['sample_id']\n        else:\n            # For training\n            targets = torch.tensor(np.log1p([row[t] for t in Config.TARGETS]), dtype=torch.float32)\n            meta = self._extract_metadata(row, is_test=False)\n            return img, meta, targets\n    def _extract_metadata(self, row, is_test=False):\n        \"\"\"Safely extract metadata with fallbacks for test data\"\"\"\n        try:\n            ndvi = float(row['Pre_GSHH_NDVI'])\n        except (KeyError, TypeError):\n            ndvi = Config.DEFAULT_META['Pre_GSHH_NDVI']\n        \n        try:\n            height = float(row['Height_Ave_cm'])\n        except (KeyError, TypeError):\n            height = Config.DEFAULT_META['Height_Ave_cm']\n        \n        try:\n            month = int(row['month'])\n        except (KeyError, TypeError, ValueError):\n            month = int(Config.DEFAULT_META['month'])\n        \n        try:\n            state = str(row['State'])\n        except (KeyError, TypeError):\n            state = Config.DEFAULT_META['State']\n        \n        # Meta: NDVI, Height, Month (12), State (4)\n        meta = [ndvi, height]\n        meta += [1.0 if month == i else 0.0 for i in range(1, 13)]\n        meta += [1.0 if state == s else 0.0 for s in self.states]\n        \n        return torch.tensor(meta, dtype=torch.float32)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================================\n# MODEL ARCHITECTURE - OFFLINE CONVNEXT\n# ============================================================================\nclass ConvNeXtBiomassModel(nn.Module):\n    def __init__(self, num_targets=5, n_meta=18):\n        super().__init__()\n        from timm import create_model\n        \n        # Set TORCH_HOME for offline loading\n        os.environ['TORCH_HOME'] = '/kaggle/input/torchvision-models-offline'\n        \n        self.cnn = create_model(\n            'convnext_large',\n            pretrained=False,\n            num_classes=0,\n            global_pool='avg'\n        )\n        \n        # Load local weights with robust error handling\n        try:\n            weights_path = '/kaggle/input/convnext-large/transformers/default/1/convnext_large.pth'\n            state_dict = torch.load(weights_path, map_location='cpu', weights_only=False)\n            \n            if 'model' in state_dict:\n                state_dict = state_dict['model']\n            elif any(k.startswith('module.') for k in state_dict.keys()):\n                state_dict = {k.replace('module.', ''): v for k, v in state_dict.items()}\n            \n            self.cnn.load_state_dict(state_dict, strict=False)\n            print(\"✓ Successfully loaded ConvNeXt-Large weights\")\n        except Exception as e:\n            print(f\"Warning: Could not load pre-trained weights: {e}\")\n            print(\"Training from scratch with random initialization\")\n        \n        # Metadata network\n        self.meta_net = nn.Sequential(\n            nn.Linear(n_meta, 64),\n            nn.ReLU(),\n            nn.Dropout(0.4),\n            nn.Linear(64, 32),\n            nn.ReLU()\n        )\n        # Prediction head\n        self.head = nn.Sequential(\n            nn.Linear(1536 + 32, 128),\n            nn.ReLU(),\n            nn.Dropout(0.5),\n            nn.Linear(128, num_targets)\n        )\n    \n    def forward(self, img, meta):\n        img_feat = self.cnn(img)\n        meta_feat = self.meta_net(meta)\n        x = torch.cat([img_feat, meta_feat], dim=1)\n        return self.head(x)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\n    \n\n\n\n        \n\n\n# ============================================================================\n# TRAINING WITH STABILITY ENHANCEMENTS\n# ============================================================================\ndef train_with_tqdm(model, train_dl, optimizer, criterion, scheduler, device):\n    model.train()\n    total_loss = 0.0\n    pbar = tqdm(train_dl, desc=\"Training\", leave=True)\n    \n    for imgs, meta, tgts in pbar:\n        imgs, meta, tgts = imgs.to(device), meta.to(device), tgts.to(device)\n        optimizer.zero_grad()\n        out = model(imgs, meta)\n        loss = criterion(out, tgts)\n        loss.backward()\n        torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)\n        optimizer.step()\n        \n        total_loss += loss.item()\n        avg_loss = total_loss / (pbar.n + 1)\n        pbar.set_postfix({'Loss': f\"{avg_loss:.4f}\"})\n    \n    if scheduler is not None:\n        scheduler.step(total_loss / len(train_dl))\n    \n    return total_loss / len(train_dl)\n\ndef validate_with_tqdm(model, val_dl, device):\n    model.eval()\n    all_preds, all_tgts, all_weights = [], [], []\n    pbar = tqdm(val_dl, desc=\"Validating\", leave=True)\n    \n    with torch.no_grad():\n        for imgs, meta, tgts in pbar:\n            imgs, meta = imgs.to(device), meta.to(device)\n            preds_log = model(imgs, meta).cpu().numpy()\n            tgts = tgts.numpy()\n            \n            # Convert from log(1+y) to raw values\n            preds = np.expm1(preds_log)\n            tgts = np.expm1(tgts)\n            \n            # Flatten predictions and targets\n            for i in range(len(preds)):\n                for j, target in enumerate(Config.TARGETS):\n                    all_preds.append(preds[i, j])\n                    all_tgts.append(tgts[i, j])\n                    all_weights.append(Config.TARGET_WEIGHTS[target])\n    \n    # Calculate weighted R²\n    all_preds = np.array(all_preds)\n    all_tgts = np.array(all_tgts)\n    all_weights = np.array(all_weights)\n    \n    weighted_mean = np.average(all_tgts, weights=all_weights)\n    ss_res = np.sum(all_weights * (all_tgts - all_preds) ** 2)\n    ss_tot = np.sum(all_weights * (all_tgts - weighted_mean) ** 2)\n    \n    if ss_tot < 1e-8:\n        r2 = 0.0\n    else:\n        r2 = 1 - (ss_res / ss_tot)\n    \n    mae = np.mean(np.abs(all_tgts - all_preds))\n    return r2, mae\n\n# ============================================================================\n# MAIN TRAINING PIPELINE\n# ============================================================================\ndef train_all_folds(df_wide):\n    \"\"\"Train all 4 folds with robust error handling\"\"\"\n    fold_scores = []\n    best_models = []\n    \n    for fold in range(Config.N_FOLDS):\n        print(f\"\\n{'='*80}\")\n        print(f\"=== TRAINING FOLD {fold} ===\")\n        print(f\"{'='*80}\")\n        \n        train_df = df_wide[df_wide['fold'] != fold].copy()\n        val_df = df_wide[df_wide['fold'] == fold].copy()\n        \n        # Datasets and dataloaders\n        train_ds = BiomassDataset(train_df, transform=train_transform)\n        val_ds = BiomassDataset(val_df, transform=val_transform)\n        \n        train_dl = DataLoader(train_ds, batch_size=Config.BATCH_SIZE, shuffle=True, \n                             num_workers=Config.NUM_WORKERS, pin_memory=True)\n        val_dl = DataLoader(val_ds, batch_size=Config.BATCH_SIZE*2, shuffle=False,\n                           num_workers=Config.NUM_WORKERS, pin_memory=True)\n        \n        # Model and optimizer\n        model = ConvNeXtBiomassModel(num_targets=len(Config.TARGETS), n_meta=18).to(Config.DEVICE)\n        optimizer = optim.AdamW(model.parameters(), lr=Config.LR, weight_decay=5e-4)\n        criterion = nn.MSELoss()\n        \n        # Fixed: Removed verbose parameter which is not supported in this PyTorch version\n        scheduler = optim.lr_scheduler.ReduceLROnPlateau(optimizer, mode='min', factor=0.5, \n                                                        patience=3)\n        \n        best_r2 = -1e9\n        best_epoch = 0\n        \n        for epoch in range(1, Config.EPOCHS + 1):\n            train_loss = train_with_tqdm(model, train_dl, optimizer, criterion, scheduler, Config.DEVICE)\n            val_r2, val_mae = validate_with_tqdm(model, val_dl, Config.DEVICE)\n            \n            print(f\"\\nFold {fold} | Epoch {epoch}/{Config.EPOCHS}\")\n            print(f\"  Train Loss: {train_loss:.4f}\")\n            print(f\"  Val R²: {val_r2:.4f} | MAE: {val_mae:.2f}g\")\n            \n            if val_r2 > best_r2:\n                best_r2 = val_r2\n                best_epoch = epoch\n                torch.save({\n                    'model_state_dict': model.state_dict(),\n                    'fold': fold,\n                    'best_r2': best_r2\n                }, f'best_model_fold{fold}.pth')\n                print(f\"  ✓ Saved best model (R²={best_r2:.4f})\")\n        \n        print(f\"\\nFold {fold} training complete | Best Val R²: {best_r2:.4f} (epoch {best_epoch})\")\n        fold_scores.append(best_r2)\n        best_models.append(model)\n    \n    return fold_scores\n\n# ============================================================================\n# ROBUST INFERENCE WITH PERFORMANCE-WEIGHTED ENSEMBLE\n# ============================================================================\ndef generate_ensemble_submission(fold_scores):\n    \"\"\"Generate final submission using performance-weighted ensemble\"\"\"\n    print(f\"\\n{'='*80}\")\n    print(\"🔍 GENERATING ENSEMBLE SUBMISSION\")\n    print(f\"{'='*80}\")\n    \n    # Load test data\n    test_df = pd.read_csv(Config.TEST_CSV)\n    test_df['image_id'] = test_df['sample_id'].str.split('__').str[0]\n    \n    # Get unique images\n    unique_images = test_df['image_id'].unique()\n    print(f\"Processing {len(unique_images)} unique images\")\n    \n    # Normalize fold scores for weighting\n    fold_weights = np.array(fold_scores) / sum(fold_scores)\n    print(f\"Fold weights based on validation performance: {fold_weights.round(3)}\")\n    \n    # Prepare result storage\n    all_preds = []\n    failed_images = []\n    \n    # Create test dataset for metadata extraction\n    test_meta_df = test_df.drop_duplicates('image_id').copy()\n    test_dataset = BiomassDataset(test_meta_df, transform=val_transform, is_test=True)\n    \n    # Process each image\n    for idx, image_id in enumerate(tqdm(unique_images, desc=\"Ensemble Prediction\")):\n        try:\n            # Get image and metadata from dataset\n            img, meta, _, _ = test_dataset[idx]\n            img_tensor = img.unsqueeze(0).to(Config.DEVICE)\n            meta_tensor = meta.unsqueeze(0).to(Config.DEVICE)\n            \n            # Get predictions from all folds\n            fold_preds = []\n            for fold in range(Config.N_FOLDS):\n                try:\n                    model = ConvNeXtBiomassModel(num_targets=5, n_meta=18).to(Config.DEVICE)\n                    ckpt = torch.load(f'best_model_fold{fold}.pth', map_location=Config.DEVICE, weights_only=False)\n                    model.load_state_dict(ckpt['model_state_dict'])\n                    model.eval()\n                    \n                    with torch.no_grad():\n                        pred_log = model(img_tensor, meta_tensor).cpu().numpy().flatten()\n                        pred = np.expm1(pred_log)\n                        fold_preds.append(pred)\n                except Exception as e:\n                    print(f\"Warning: Fold {fold} failed for image {image_id}: {e}\")\n                    # Use zeros as fallback\n                    fold_preds.append(np.zeros(5))\n            \n            # Performance-weighted average\n            weighted_pred = np.zeros(5)\n            for w, pred in zip(fold_weights, fold_preds):\n                weighted_pred += w * pred\n            \n            # Apply physical constraints and clipping\n            weighted_pred = np.clip(weighted_pred, 0.0, 300.0)\n            \n            # Ensure logical biomass relationships\n            # Dry_Total_g should be >= sum of components\n            components_sum = weighted_pred[1] + weighted_pred[2] + weighted_pred[3]  # Green + Dead + Clover\n            if weighted_pred[0] < components_sum:\n                weighted_pred[0] = components_sum * 1.1  # Add small buffer\n            \n            # GDM_g should be similar to Green + Dead\n            green_dead_sum = weighted_pred[1] + weighted_pred[2]\n            weighted_pred[4] = max(weighted_pred[4], green_dead_sum * 0.9)  # GDM_g\n            \n            # Add predictions for all 5 targets\n            for i, target in enumerate(Config.TARGETS):\n                all_preds.append({\n                    'sample_id': f\"{image_id}__{target}\",\n                    'target': float(weighted_pred[i])\n                })\n                \n        except Exception as e:\n            print(f\"❌ Failed to process image {image_id}: {e}\")\n            failed_images.append(image_id)\n            # Add default predictions for failed images\n            for target in Config.TARGETS:\n                all_preds.append({\n                    'sample_id': f\"{image_id}__{target}\",\n                    'target': 0.0\n                })\n    \n    # Create and save submission\n    submission = pd.DataFrame(all_preds)\n    \n    # Verify submission format\n    expected_rows = len(unique_images) * 5\n    if len(submission) != expected_rows:\n        print(f\"⚠️ Warning: Submission has {len(submission)} rows, expected {expected_rows}\")\n    \n    submission.to_csv(\"submission.csv\", index=False)\n    \n    print(f\"\\n{'='*80}\")\n    print(\"✅ SUBMISSION COMPLETE\")\n    print(f\"{'='*80}\")\n    print(f\"Total predictions: {len(submission)}\")\n    print(f\"Failed images: {len(failed_images)}\")\n    print(\"\\nFirst 10 rows of submission:\")\n    print(submission.head(10))\n    \n    return submission\n\n# ============================================================================\n# MAIN EXECUTION\n# ============================================================================\nif __name__ == \"__main__\":\n    # Data transformations\n    train_transform = transforms.Compose([\n        transforms.Resize((Config.IMG_SIZE, Config.IMG_SIZE)),\n        transforms.RandomHorizontalFlip(p=0.5),\n        transforms.RandomRotation(degrees=10),\n        transforms.ColorJitter(brightness=0.2, contrast=0.2, 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    \n    val_transform = transforms.Compose([\n        transforms.Resize((Config.IMG_SIZE, Config.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    # Load and prepare data\n    print(f\"\\n{'='*80}\")\n    print(\"📊 LOADING AND PREPARING DATA\")\n    print(f\"{'='*80}\")\n    df_wide = load_and_prepare_data()\n    print(f\"Training samples: {len(df_wide)}\")\n    print(f\"Location groups: {df_wide['location_group'].nunique()}\")\n    print(f\"Default metadata values for test inference:\")\n    for key, value in Config.DEFAULT_META.items():\n        print(f\"  {key}: {value}\")\n    \n    # Train all folds\n    print(f\"\\n{'='*80}\")\n    print(\"🚀 STARTING 4-FOLD TRAINING\")\n    print(f\"{'='*80}\")\n    fold_scores = train_all_folds(df_wide)\n    \n    # Generate submission\n    submission = generate_ensemble_submission(fold_scores)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-29T16:48:37.808093Z","iopub.execute_input":"2025-11-29T16:48:37.808647Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}