{"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"}],"dockerImageVersionId":31260,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import os\nimport pandas as pd\nimport numpy as np\nimport matplotlib.pyplot as plt\nimport seaborn as sns\nfrom PIL import Image\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.preprocessing import StandardScaler\nfrom sklearn.metrics import r2_score, mean_absolute_error\n\nimport torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom torch.utils.data import Dataset, DataLoader\nfrom torchvision import models, transforms\n\n# Set device to GPU\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nprint(f\"System ready using: {device}\")\n\n# Paths (Dynamic to handle Kaggle environment)\nDATA_DIR = \"/kaggle/input/csiro-biomass\" ","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2026-01-17T10:13:49.378292Z","iopub.execute_input":"2026-01-17T10:13:49.378882Z","iopub.status.idle":"2026-01-17T10:14:02.542395Z","shell.execute_reply.started":"2026-01-17T10:13:49.378853Z","shell.execute_reply":"2026-01-17T10:14:02.54157Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Load raw CSVs\ntrain_df = pd.read_csv(os.path.join(DATA_DIR, 'train.csv'))\n\n# Create a unique Image ID\ntrain_df['image_id'] = train_df['sample_id'].apply(lambda x: x.split('__')[0])\n\n# Pivot to get one row per image with all 5 targets\ntarget_cols = ['Dry_Green_g', 'Dry_Dead_g', 'Dry_Clover_g', 'GDM_g', 'Dry_Total_g']\ndf_pivoted = train_df.pivot_table(index='image_id', columns='target_name', values='target').reset_index()\n\n# Merge back the image path and sensor metadata (NDVI and Height)\nmeta_df = train_df.groupby('image_id')[['image_path', 'Pre_GSHH_NDVI', 'Height_Ave_cm']].first().reset_index()\nmaster_df = df_pivoted.merge(meta_df, on='image_id')\n\nprint(f\"Total Unique Samples: {len(master_df)}\")\nmaster_df.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-17T10:14:20.84799Z","iopub.execute_input":"2026-01-17T10:14:20.848676Z","iopub.status.idle":"2026-01-17T10:14:20.98697Z","shell.execute_reply.started":"2026-01-17T10:14:20.848646Z","shell.execute_reply":"2026-01-17T10:14:20.986363Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def run_eda(df):\n    plt.figure(figsize=(15, 5))\n    \n    # 1. Target Distribution\n    plt.subplot(1, 2, 1)\n    sns.histplot(df['Dry_Total_g'], kde=True, color='green')\n    plt.title('Distribution of Total Biomass (Target Variable)')\n    \n    # 2. Metadata correlation\n    plt.subplot(1, 2, 2)\n    correlation = df[['Pre_GSHH_NDVI', 'Height_Ave_cm', 'Dry_Total_g']].corr()\n    sns.heatmap(correlation, annot=True, cmap='YlGnBu')\n    plt.title('Sensor Correlation with Biomass')\n    \n    plt.tight_layout()\n    plt.show()\n\nrun_eda(master_df)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-17T10:14:59.131536Z","iopub.execute_input":"2026-01-17T10:14:59.132301Z","iopub.status.idle":"2026-01-17T10:14:59.710428Z","shell.execute_reply.started":"2026-01-17T10:14:59.132251Z","shell.execute_reply":"2026-01-17T10:14:59.709654Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 1. Fill missing sensor values with median\nmaster_df['Pre_GSHH_NDVI'] = master_df['Pre_GSHH_NDVI'].fillna(master_df['Pre_GSHH_NDVI'].median())\nmaster_df['Height_Ave_cm'] = master_df['Height_Ave_cm'].fillna(master_df['Height_Ave_cm'].median())\n\n# --- NEW: Feature Engineering for 80% Goal ---\nmaster_df['biomass_proxy'] = master_df['Pre_GSHH_NDVI'] * master_df['Height_Ave_cm']\n\n# 2. Scale sensor data (Updated to include 3 features)\nscaler = StandardScaler()\nmaster_df[['scaled_ndvi', 'scaled_height', 'scaled_proxy']] = scaler.fit_transform(\n    master_df[['Pre_GSHH_NDVI', 'Height_Ave_cm', 'biomass_proxy']]\n)\n\n# 3. Define professional transforms\nimg_transforms = transforms.Compose([\n    transforms.Resize((256, 256)), \n    transforms.CenterCrop((224, 224)),\n    transforms.RandomHorizontalFlip(),\n    transforms.RandomVerticalFlip(), \n    transforms.ColorJitter(brightness=0.2, contrast=0.2), \n    transforms.ToTensor(),\n    transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])\n])\n\nclass BiomassDataset(Dataset):\n    def __init__(self, df, transform=None):\n        self.df = df\n        self.transform = transform\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 = os.path.join(DATA_DIR, row['image_path'])\n        image = Image.open(img_path).convert('RGB')\n        \n        if self.transform:\n            image = self.transform(image)\n            \n        meta = torch.tensor([row['scaled_ndvi'], row['scaled_height'], row['scaled_proxy']], dtype=torch.float32)\n        \n        # --- CRITICAL CHANGE: Log Transformation ---\n        # This makes Clover (small numbers) as important as Total (large numbers)\n        targets_raw = row[target_cols].values.astype(np.float32)\n        targets = torch.tensor(np.log1p(targets_raw)) \n        \n        return image, meta, targets\n\nprint(\"SUCCESS: Feature Engineering Applied.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-17T10:15:04.702881Z","iopub.execute_input":"2026-01-17T10:15:04.703526Z","iopub.status.idle":"2026-01-17T10:15:04.717625Z","shell.execute_reply.started":"2026-01-17T10:15:04.703499Z","shell.execute_reply":"2026-01-17T10:15:04.716796Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import timm # High-performance library for EfficientNet\n\nclass BiomassFusionModel(nn.Module):\n    def __init__(self, num_targets=5):\n        super(BiomassFusionModel, self).__init__()\n        \n        # 1. Visual Branch (Upgraded to B3 for 80% accuracy goal)\n        # num_classes=0 and global_pool='avg' returns a clean feature vector\n        self.visual_branch = timm.create_model('efficientnet_b3', pretrained=True, num_classes=0, global_pool='avg')\n        \n        # EfficientNet-B3 produces 1536 features\n        n_visual_features = 1536 \n        \n        # 2. Metadata Branch (Processing NDVI and Height)\n        self.meta_branch = nn.Sequential(\n            nn.Linear(3, 64), # Increased width to 64 for more capacity\n            nn.SiLU(),\n            nn.Linear(64, 32),\n            nn.SiLU()\n        )\n        \n        # 3. Fusion Head (Combines 1536 image features + 32 meta features)\n        self.fusion_layer = nn.Sequential(\n            nn.Linear(n_visual_features + 32, 512),\n            nn.SiLU(),\n            nn.Dropout(0.3), \n            nn.Linear(512, 256),\n            nn.SiLU(),\n            nn.Linear(256, num_targets) \n        )\n\n    def forward(self, img, meta):\n        # Extract features from both branches\n        img_features = self.visual_branch(img)\n        meta_features = self.meta_branch(meta)\n        \n        # Concatenate (Fuse) the two paths\n        combined = torch.cat((img_features, meta_features), dim=1)\n        \n        # Predict the 5 biomass values\n        logits = self.fusion_layer(combined)\n        return logits\n\n# Initialize model\nmodel = BiomassFusionModel().to(device)\nprint(\"SUCCESS: Optimized Fusion Model (B3) Initialized and moved to GPU.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-17T10:15:08.499024Z","iopub.execute_input":"2026-01-17T10:15:08.499789Z","iopub.status.idle":"2026-01-17T10:15:15.592042Z","shell.execute_reply.started":"2026-01-17T10:15:08.49976Z","shell.execute_reply":"2026-01-17T10:15:15.591225Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Weights: [Green: 1, Dead: 5, Clover: 5, GDM: 1, Total: 1]\ncategory_weights = torch.tensor([1.0, 5.0, 5.0, 1.0, 1.0]).to(device)\n\ndef train_one_epoch(model, dataloader, optimizer, scheduler):\n    model.train()\n    total_loss = 0\n    for imgs, metas, targets in dataloader:\n        imgs, metas, targets = imgs.to(device), metas.to(device), targets.to(device)\n        \n        optimizer.zero_grad()\n        outputs = model(imgs, metas)\n        \n        # Calculate Weighted Squared Error\n        loss_val = (outputs - targets)**2\n        weighted_loss = (loss_val * category_weights).mean()\n        \n        weighted_loss.backward()\n        optimizer.step()\n        \n        # --- CRITICAL: Step the scheduler here (Per Batch) ---\n        scheduler.step() \n        \n        total_loss += weighted_loss.item()\n    return total_loss / len(dataloader)\n\n# Redefine optimizer for the new targets\noptimizer = optim.AdamW(model.parameters(), lr=1e-4, weight_decay=1e-2)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-17T10:15:21.177827Z","iopub.execute_input":"2026-01-17T10:15:21.178522Z","iopub.status.idle":"2026-01-17T10:15:21.185639Z","shell.execute_reply.started":"2026-01-17T10:15:21.178493Z","shell.execute_reply":"2026-01-17T10:15:21.185002Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from sklearn.metrics import r2_score\n\ndef evaluate_accuracy(model, dataloader):\n    model.eval()\n    all_preds = []\n    all_targets = []\n    \n    with torch.no_grad():\n        for imgs, metas, targets in dataloader:\n            imgs, metas = imgs.to(device), metas.to(device)\n            outputs = model(imgs, metas)\n            \n            # --- CRITICAL: Convert back from Log to Normal scale ---\n            all_preds.append(torch.expm1(outputs).cpu().numpy())\n            all_targets.append(torch.expm1(targets).cpu().numpy())\n            \n    y_pred = np.vstack(all_preds)\n    y_true = np.vstack(all_targets)\n    \n    # Calculate R2 for Total Biomass\n    total_r2 = r2_score(y_true[:, 4], y_pred[:, 4]) \n    return total_r2, y_pred, y_true\n\nprint(\"Evaluation function ready.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-17T10:15:28.08276Z","iopub.execute_input":"2026-01-17T10:15:28.083315Z","iopub.status.idle":"2026-01-17T10:15:28.089287Z","shell.execute_reply.started":"2026-01-17T10:15:28.083287Z","shell.execute_reply":"2026-01-17T10:15:28.088712Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 1. SPLIT THE DATA (80% Train, 20% Validation)\ntrain_df_split, val_df_split = train_test_split(master_df, test_size=0.20, random_state=42)\n\n# 2. INSTANTIATE THE DATASETS\ntrain_dataset = BiomassDataset(train_df_split, transform=img_transforms)\nval_dataset = BiomassDataset(val_df_split, transform=img_transforms)\n\n# 3. CREATE THE DATALOADERS (This defines 'train_loader')\ntrain_loader = DataLoader(train_dataset, batch_size=32, shuffle=True, num_workers=2)\nval_loader = DataLoader(val_dataset, batch_size=32, shuffle=False, num_workers=2)\n\nprint(f\"DataLoaders Ready: {len(train_dataset)} training samples.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-17T10:15:32.909375Z","iopub.execute_input":"2026-01-17T10:15:32.909676Z","iopub.status.idle":"2026-01-17T10:15:32.917332Z","shell.execute_reply.started":"2026-01-17T10:15:32.909649Z","shell.execute_reply":"2026-01-17T10:15:32.916717Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Setup Scheduler\nscheduler = optim.lr_scheduler.OneCycleLR(optimizer, max_lr=1e-3, \n                                          steps_per_epoch=len(train_loader), epochs=40)\nbest_r2 = -np.inf\n\nfor epoch in range(40):\n    # Train for one epoch (Pass scheduler here)\n    train_loss = train_one_epoch(model, train_loader, optimizer, scheduler)\n    \n    # Evaluate accuracy on the validation set\n    current_r2, preds, actuals = evaluate_accuracy(model, val_loader)\n    \n    print(f\"Epoch {epoch+1:02d} | Train Loss: {train_loss:.4f} | Val Total Biomass R2: {current_r2:.4f}\")\n    \n    if current_r2 > best_r2:\n        best_r2 = current_r2\n        torch.save(model.state_dict(), 'best_biomass_model.pth')\n        print(f\"--- New Best Model Saved! ---\")\n\nprint(f\"\\nFinal Best Total Biomass Accuracy: {best_r2*100:.2f}%\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-17T10:15:35.689802Z","iopub.execute_input":"2026-01-17T10:15:35.690311Z","iopub.status.idle":"2026-01-17T10:24:04.212051Z","shell.execute_reply.started":"2026-01-17T10:15:35.690282Z","shell.execute_reply":"2026-01-17T10:24:04.211229Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 1. Load your best 77.37% model first\nmodel.load_state_dict(torch.load('best_biomass_model.pth'))\nprint(\"Successfully loaded the 77.37% model for final polishing.\")\n\n# 2. Use a tiny learning rate\noptimizer_fine = optim.AdamW(model.parameters(), lr=1e-5, weight_decay=1e-2)\n\ndef fine_tune_weighted(model, train_loader, val_loader, epochs=10):\n    global best_r2\n    print(\"Starting Final Weighted Fine-Tuning...\")\n    \n    for epoch in range(epochs):\n        model.train()\n        train_loss = 0\n        for imgs, metas, targets in train_loader:\n            imgs, metas, targets = imgs.to(device), metas.to(device), targets.to(device)\n            \n            optimizer_fine.zero_grad()\n            outputs = model(imgs, metas)\n            \n            # Use the SAME weighted loss from your 77% run\n            loss_val = (outputs - targets)**2\n            weighted_loss = (loss_val * category_weights).mean()\n            \n            weighted_loss.backward()\n            optimizer_fine.step()\n            train_loss += weighted_loss.item()\n        \n        # Check accuracy\n        current_r2, _, _ = evaluate_accuracy(model, val_loader)\n        print(f\"Fine-Tune Epoch {epoch+1:02d} | Loss: {train_loss/len(train_loader):.4f} | Val R2: {current_r2:.4f}\")\n        \n        if current_r2 > best_r2:\n            best_r2 = current_r2\n            torch.save(model.state_dict(), 'best_biomass_model_final.pth')\n            print(f\"--- SUCCESS: Accuracy Improved to {best_r2*100:.2f}% ---\")\n\n# Run it for 10 epochs\nfine_tune_weighted(model, train_loader, val_loader)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-17T10:24:29.696715Z","iopub.execute_input":"2026-01-17T10:24:29.697062Z","iopub.status.idle":"2026-01-17T10:26:33.410686Z","shell.execute_reply.started":"2026-01-17T10:24:29.697028Z","shell.execute_reply":"2026-01-17T10:26:33.409949Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Load the final polished brain\nmodel.load_state_dict(torch.load('best_biomass_model_final.pth'))\nfinal_r2, predictions, actuals = evaluate_accuracy(model, val_loader)\n\nprint(f\"\\n--- FINAL VALIDATION REPORT ---\")\ncategories = ['Green', 'Dead', 'Clover', 'GDM', 'Total']\nfor i, cat in enumerate(categories):\n    cat_r2 = r2_score(actuals[:, i], predictions[:, i])\n    print(f\"{cat} Biomass R2: {cat_r2:.4f}\")\n\nprint(f\"\\nPROJECT FINAL SCORE: {final_r2*100:.2f}%\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-17T10:27:14.80376Z","iopub.execute_input":"2026-01-17T10:27:14.804103Z","iopub.status.idle":"2026-01-17T10:27:17.351298Z","shell.execute_reply.started":"2026-01-17T10:27:14.804072Z","shell.execute_reply":"2026-01-17T10:27:17.350504Z"}},"outputs":[],"execution_count":null}]}