{"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,"sourceType":"competition"},{"sourceId":1803569,"sourceType":"datasetVersion","datasetId":1071580},{"sourceId":13746359,"sourceType":"datasetVersion","datasetId":8746989},{"sourceId":13892472,"sourceType":"datasetVersion","datasetId":8618494},{"sourceId":14031129,"sourceType":"datasetVersion","datasetId":8667879},{"sourceId":14086906,"sourceType":"datasetVersion","datasetId":8969108},{"sourceId":185608,"sourceType":"modelInstanceVersion","modelInstanceId":158235,"modelId":164716},{"sourceId":630489,"sourceType":"modelInstanceVersion","modelInstanceId":471723,"modelId":487624},{"sourceId":638209,"sourceType":"modelInstanceVersion","modelInstanceId":471723,"modelId":487624}],"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# ===================================================================\n#       --- V50.2: CSIRO \"PROTOCOL BETA\" (FULLY REPAIRED) ---\n#       Status: Syntax Fixed, Logic Completed, Runnable\n# ===================================================================\nimport pandas as pd\nimport numpy as np\nimport os\nimport timm\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.utils.data import Dataset, DataLoader\nfrom PIL import Image\nfrom torchvision import transforms\nfrom tqdm import tqdm\nfrom sklearn.model_selection import GroupKFold\nimport warnings\nimport gc\n\nwarnings.filterwarnings('ignore')\nprint(\"Initializing V50.2: Fully Repaired Pipeline...\")\n\n# --- CONFIGURATION ---\nclass CFG:\n    BASE_PATH = '/kaggle/input/csiro-biomass'\n    TRAIN_CSV_PATH = os.path.join(BASE_PATH, 'train.csv')\n    TEST_CSV_PATH = os.path.join(BASE_PATH, 'test.csv')\n    \n    # Models: Teacher (ConvNext) -> Student (EfficientNet)\n    TEACHER_BACKBONE = 'convnext_tiny.fb_in22k' \n    STUDENT_BACKBONE = 'efficientnet_b0' \n    \n    IMAGE_SIZE = 384 \n    BATCH_SIZE = 16\n    EPOCHS = 12 \n    LR = 2e-4\n    DEVICE = 'cuda' if torch.cuda.is_available() else 'cpu'\n    N_SPLITS = 5\n    SEED = 42\n    \n    # Loss Weights\n    W_BIOMASS = 0.6\n    W_DISTILL = 0.3\n    W_AUX = 0.1\n\ndef set_seed(seed=42):\n    torch.manual_seed(seed)\n    torch.cuda.manual_seed(seed)\n    np.random.seed(seed)\n    os.environ['PYTHONHASHSEED'] = str(seed)\n\nset_seed(CFG.SEED)\n\n# ===================================================================\n#   --- HELPER: GLOBAL WEIGHTED R2 METRIC ---\n# ===================================================================\ndef global_weighted_r2_score(y_true, y_pred):\n    # Weights: Clover, Dead, Green, GDM, Total\n    weights = np.array([0.1, 0.1, 0.1, 0.2, 0.5]) \n    \n    y_true_flat = y_true.ravel()\n    y_pred_flat = y_pred.ravel()\n    \n    weights_flat = np.tile(weights, (y_true.shape[0], 1)).ravel()\n    \n    y_w_mean = np.average(y_true_flat, weights=weights_flat)\n    ss_res = np.sum(weights_flat * (y_true_flat - y_pred_flat)**2)\n    ss_tot = np.sum(weights_flat * (y_true_flat - y_w_mean)**2)\n    \n    if ss_tot == 0: return 0.0\n    r2 = 1 - (ss_res / ss_tot)\n    return r2\n\n# ===================================================================\n#   --- STEP 1: DATA PROCESSING ---\n# ===================================================================\n# Fixed: Defined target columns explicitly\nTARGET_COLS = ['Dry_Clover_g', 'Dry_Dead_g', 'Dry_Green_g', 'GDM_g', 'Dry_Total_g']\n\nprint(\"Loading and Pivoting Data...\")\ntrain_df = pd.read_csv(CFG.TRAIN_CSV_PATH)\n\n# Fixed: Filled missing index columns for pivot_table\ntrain_df = train_df.pivot_table(\n    index=['image_path', 'Sampling_Date', 'State', 'Species', 'Pre_GSHH_NDVI', 'Height_Ave_cm'], \n    columns='target_name', \n    values='target'\n).reset_index()\n\n# Clean up column names (flatten pivot index)\ntrain_df.columns.name = None\n\ntrain_df['full_path'] = train_df['image_path'].apply(lambda x: os.path.join(CFG.BASE_PATH, x))\n\n# Create robust Group Key\ntrain_df['group_key'] = train_df['Sampling_Date'].astype(str) + \"_\" + train_df['State'].astype(str)\n\n# Precompute Targets & Ratios\n# Logic: \n# 1. Total Biomass (Log Transformed)\n# 2. GDM Ratio (Green+Clover / Total)\n# 3. Clover Ratio (Clover / Green+Clover)\n\n# Ensure no NaN/Infinite\nfor col in TARGET_COLS:\n    train_df[col] = train_df[col].fillna(0)\n\n# Total\ntrain_df['log_total'] = np.log1p(train_df['Dry_Total_g'])\n\n# Ratio 1: GDM / Total (How much is \"Green Dry Matter\" vs Dead)\ntrain_df['gdm_ratio'] = train_df['GDM_g'] / (train_df['Dry_Total_g'] + 1e-6)\n\n# Ratio 2: Clover / GDM (How much of the live stuff is Clover)\ntrain_df['clover_ratio'] = train_df['Dry_Clover_g'] / (train_df['GDM_g'] + 1e-6)\n\ntrain_df['gdm_ratio'] = train_df['gdm_ratio'].clip(0, 1)\ntrain_df['clover_ratio'] = train_df['clover_ratio'].clip(0, 1)\n\n# Load Test\ntest_df = pd.read_csv(CFG.TEST_CSV_PATH)\ntest_meta = test_df[['image_path']].drop_duplicates().reset_index(drop=True)\ntest_meta['full_path'] = test_meta['image_path'].apply(lambda x: os.path.join(CFG.BASE_PATH, x))\n\nprint(f\"Train Shape: {train_df.shape}, Test Images: {len(test_meta)}\")\n\n# ===================================================================\n#   --- STEP 2: DATASET WITH METADATA ---\n# ===================================================================\nclass BiomassDataset(Dataset):\n    def __init__(self, df, transform=None, is_test=False):\n        self.paths = df['full_path'].values\n        self.transform = transform\n        self.is_test = is_test\n        \n        if not is_test:\n            # Targets: LogTotal, GDM_Ratio, Clover_Ratio\n            self.targets = df[['log_total', 'gdm_ratio', 'clover_ratio']].values.astype(np.float32)\n            \n            # Fixed: Correctly selecting metadata columns\n            # Using NDVI and Height as auxiliary inputs/targets\n            self.meta_targets = df[['Pre_GSHH_NDVI', 'Height_Ave_cm']].fillna(0).values.astype(np.float32)\n            self.meta_targets[:, 1] = self.meta_targets[:, 1] / 100.0 # Normalize height\n            \n            self.meta_inputs = self.meta_targets.copy() \n            \n    def __len__(self): return len(self.paths)\n    \n    def __getitem__(self, idx):\n        try:\n            img = Image.open(self.paths[idx]).convert('RGB')\n        except:\n            # Fallback for corrupt images\n            img = Image.new('RGB', (CFG.IMAGE_SIZE, CFG.IMAGE_SIZE), (0,0,0))\n            \n        if self.transform: img = self.transform(img)\n        \n        if self.is_test: return img\n        \n        return img, torch.tensor(self.meta_inputs[idx]), torch.tensor(self.targets[idx]), torch.tensor(self.meta_targets[idx])\n\n# ===================================================================\n#   --- STEP 3: MODELS ---\n# ===================================================================\n\n\nclass TeacherModel(nn.Module):\n    def __init__(self):\n        super().__init__()\n        self.backbone = timm.create_model(CFG.TEACHER_BACKBONE, pretrained=True, num_classes=0)\n        # Meta encoder takes 2 inputs (NDVI, Height)\n        self.meta_enc = nn.Sequential(nn.Linear(2, 64), nn.BatchNorm1d(64), nn.ReLU(), nn.Dropout(0.2))\n        \n        self.head = nn.Sequential(\n            nn.Linear(self.backbone.num_features + 64, 256),\n            nn.SiLU(),\n            nn.Dropout(0.2),\n            nn.Linear(256, 3) \n        )\n        \n    def forward(self, img, meta):\n        feats = self.backbone(img)\n        meta_feats = self.meta_enc(meta)\n        combined = torch.cat([feats, meta_feats], dim=1)\n        output = self.head(combined)\n        \n        # Enforce constraints: Total > 0, Ratios in [0, 1]\n        total = F.relu(output[:, 0]).unsqueeze(1)\n        ratios = torch.sigmoid(output[:, 1:])\n        return torch.cat([total, ratios], dim=1), feats \n\nclass StudentModel(nn.Module):\n    def __init__(self):\n        super().__init__()\n        self.backbone = timm.create_model(CFG.STUDENT_BACKBONE, pretrained=True, num_classes=0)\n        self.head = nn.Sequential(nn.Linear(self.backbone.num_features, 256), nn.SiLU(), nn.Linear(256, 3))\n        # Aux Head to predict metadata from image\n        self.aux_head = nn.Sequential(nn.Linear(self.backbone.num_features, 128), nn.ReLU(), nn.Linear(128, 2))\n        \n    def forward(self, img):\n        feats = self.backbone(img)\n        output = self.head(feats)\n        aux_out = self.aux_head(feats)\n        \n        total = F.relu(output[:, 0]).unsqueeze(1)\n        ratios = torch.sigmoid(output[:, 1:])\n        return torch.cat([total, ratios], dim=1), aux_out, feats\n\n# ===================================================================\n#   --- STEP 4: TRAINING ENGINE ---\n# ===================================================================\ndef train_epoch(teacher, student, loader, opt, scheduler, epoch):\n    student.train()\n    teacher.eval()\n    avg_loss = 0\n    criterion_mse = nn.MSELoss()\n    \n    pbar = tqdm(loader, desc=f\"Epoch {epoch+1} Train\")\n    \n    for img, meta_in, target, meta_target in pbar:\n        img = img.to(CFG.DEVICE)\n        meta_in = meta_in.to(CFG.DEVICE)\n        target = target.to(CFG.DEVICE)\n        meta_target = meta_target.to(CFG.DEVICE)\n        \n        # Teacher Inference (No Grad)\n        with torch.no_grad():\n            t_pred, t_feats = teacher(img, meta_in)\n            \n        # Student Training\n        s_pred, s_aux, s_feats = student(img)\n        \n        # 1. Main Task Loss\n        loss_bio = (0.7 * criterion_mse(s_pred[:, 0], target[:, 0]) + \n                    0.15 * criterion_mse(s_pred[:, 1], target[:, 1]) + \n                    0.15 * criterion_mse(s_pred[:, 2], target[:, 2]))\n        \n        # 2. Distillation Loss (Force Student to mimic Teacher)\n        loss_distill = criterion_mse(s_pred, t_pred)\n        \n        # 3. Aux Loss\n        loss_aux = criterion_mse(s_aux, meta_target)\n        \n        loss = (CFG.W_BIOMASS * loss_bio) + (CFG.W_DISTILL * loss_distill) + (CFG.W_AUX * loss_aux)\n        \n        opt.zero_grad()\n        loss.backward()\n        opt.step()\n        scheduler.step()\n        \n        avg_loss += loss.item()\n        pbar.set_postfix({'Loss': f\"{loss.item():.4f}\"})\n        \n    return avg_loss / len(loader)\n\ndef validate(model, loader):\n    model.eval()\n    # Fixed: Initialized lists properly\n    preds = []\n    truths = []\n    \n    with torch.no_grad():\n        for img, _, target, _ in loader:\n            img = img.to(CFG.DEVICE)\n            pred, _, _ = model(img)\n            \n            pred = pred.cpu().numpy()\n            target = target.cpu().numpy()\n            \n            # --- RECONSTRUCT PREDICTIONS ---\n            # Output 0: Log Total -> Total\n            p_total = np.expm1(pred[:, 0])\n            # Output 1: GDM Ratio\n            p_gdm_r = pred[:, 1]\n            # Output 2: Clover Ratio\n            p_clover_r = pred[:, 2]\n            \n            # Math:\n            p_gdm = p_total * p_gdm_r\n            p_clover = p_gdm * p_clover_r\n            p_green = p_gdm - p_clover\n            p_dead = p_total - p_gdm\n            \n            # Order: Clover, Dead, Green, GDM, Total (Matches Target Cols)\n            p_vec = np.stack([p_clover, p_dead, p_green, p_gdm, p_total], axis=1)\n            preds.append(np.maximum(0, p_vec))\n            \n            # --- RECONSTRUCT GROUND TRUTH ---\n            t_total = np.expm1(target[:, 0])\n            t_gdm_r = target[:, 1]\n            t_clover_r = target[:, 2]\n            \n            t_gdm = t_total * t_gdm_r\n            t_clover = t_gdm * t_clover_r\n            t_green = t_gdm - t_clover\n            t_dead = t_total - t_gdm\n            \n            t_vec = np.stack([t_clover, t_dead, t_green, t_gdm, t_total], axis=1)\n            truths.append(t_vec)\n            \n    preds = np.concatenate(preds)\n    truths = np.concatenate(truths)\n    \n    score = global_weighted_r2_score(truths, preds)\n    return score, preds\n\n# ===================================================================\n#   --- MAIN EXECUTION ---\n# ===================================================================\n# Fixed: Filled empty Transforms\ntrain_tfm = transforms.Compose([\n    transforms.Resize((CFG.IMAGE_SIZE, CFG.IMAGE_SIZE)),\n    transforms.RandomHorizontalFlip(),\n    transforms.RandomVerticalFlip(),\n    transforms.ToTensor(),\n    transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])\n])\n\nval_tfm = transforms.Compose([\n    transforms.Resize((CFG.IMAGE_SIZE, CFG.IMAGE_SIZE)),\n    transforms.ToTensor(),\n    transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])\n])\n\ngkf = GroupKFold(n_splits=CFG.N_SPLITS)\noof_preds_container = np.zeros((len(train_df), 5))\ntest_preds_container = np.zeros((len(test_meta), 5))\n\ntest_ds = BiomassDataset(test_meta, transform=val_tfm, is_test=True)\ntest_loader = DataLoader(test_ds, batch_size=CFG.BATCH_SIZE, shuffle=False, num_workers=2)\n\nfor fold, (train_idx, val_idx) in enumerate(gkf.split(train_df, groups=train_df['group_key'])):\n    print(f\"\\n>>> Starting Fold {fold+1} <<<\")\n    \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, train_tfm), batch_size=CFG.BATCH_SIZE, shuffle=True, num_workers=2)\n    val_loader = DataLoader(BiomassDataset(val_sub, val_tfm), batch_size=CFG.BATCH_SIZE, shuffle=False, num_workers=2)\n    \n    teacher = TeacherModel().to(CFG.DEVICE)\n    student = StudentModel().to(CFG.DEVICE)\n    \n    optimizer = torch.optim.AdamW(student.parameters(), lr=CFG.LR, weight_decay=1e-4)\n    scheduler = torch.optim.lr_scheduler.OneCycleLR(optimizer, max_lr=CFG.LR, \n                                                    steps_per_epoch=len(train_loader), epochs=CFG.EPOCHS)\n    \n    best_score = -np.inf\n    best_weights = None\n    \n    for ep in range(CFG.EPOCHS):\n        loss = train_epoch(teacher, student, train_loader, optimizer, scheduler, ep)\n        score, val_preds_raw = validate(student, val_loader)\n        \n        print(f\"Ep {ep+1} | Loss: {loss:.4f} | Val Global R2: {score:.4f}\")\n        \n        if score > best_score:\n            best_score = score\n            best_weights = student.state_dict()\n            oof_preds_container[val_idx] = val_preds_raw\n            \n    # Inference on Test with Best Weights\n    student.load_state_dict(best_weights)\n    student.eval()\n    \n    # Fixed: Initialized list properly\n    fold_test_preds = []\n    with torch.no_grad():\n        for img in tqdm(test_loader, desc=\"Test Inference\"):\n            img = img.to(CFG.DEVICE)\n            pred, _, _ = student(img)\n            pred = pred.cpu().numpy()\n            \n            p_total = np.expm1(pred[:, 0])\n            p_gdm_r = pred[:, 1]\n            p_clover_r = pred[:, 2]\n            \n            p_gdm = p_total * p_gdm_r\n            p_clover = p_gdm * p_clover_r\n            p_green = p_gdm - p_clover\n            p_dead = p_total - p_gdm\n            \n            p_vec = np.stack([p_clover, p_dead, p_green, p_gdm, p_total], axis=1)\n            fold_test_preds.append(np.maximum(0, p_vec))\n            \n    test_preds_container += np.concatenate(fold_test_preds) / CFG.N_SPLITS\n    \n    del teacher, student, optimizer\n    torch.cuda.empty_cache()\n    gc.collect()\n\n# ===================================================================\n#   --- STEP 5: SUBMISSION GENERATION ---\n# ===================================================================\n# Fixed: Filled Missing Variables\ncols = ['Dry_Clover_g', 'Dry_Dead_g', 'Dry_Green_g', 'GDM_g', 'Dry_Total_g']\nsub_rows = []\n\ntest_paths = test_meta['image_path'].values\nfor i, path in enumerate(test_paths):\n    img_id = os.path.splitext(os.path.basename(path))[0]\n    preds = test_preds_container[i]\n    \n    for j, col in enumerate(cols):\n        sub_rows.append({\n            'sample_id': f\"{img_id}__{col}\",\n            'target': float(preds[j])\n        })\n\nsubmission = pd.DataFrame(sub_rows)\nsubmission.to_csv('submission.csv', index=False)\nprint(\"Submission Generated Successfully.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-14T16:45:28.262930Z","iopub.execute_input":"2025-12-14T16:45:28.263477Z"}},"outputs":[],"execution_count":null}]}