{"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":"gpu","dataSources":[{"sourceId":112509,"databundleVersionId":14254895,"sourceType":"competition"}],"dockerImageVersionId":31236,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# Imports","metadata":{}},{"cell_type":"code","source":"import os\nimport warnings\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nfrom PIL import Image\n\nimport torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom torch.utils.data import Dataset, DataLoader\nfrom torchvision import transforms\n\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.preprocessing import StandardScaler\nfrom tqdm import tqdm\nimport joblib\n\nwarnings.filterwarnings(\"ignore\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-09T22:11:59.948857Z","iopub.execute_input":"2026-01-09T22:11:59.949155Z","iopub.status.idle":"2026-01-09T22:11:59.954215Z","shell.execute_reply.started":"2026-01-09T22:11:59.949123Z","shell.execute_reply":"2026-01-09T22:11:59.953367Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Config & Reproducibility","metadata":{}},{"cell_type":"code","source":"SEED = 42\nBASE_PATH = \"/kaggle/input/csiro-biomass\"\nIMG_SIZE = 224\nBATCH_SIZE = 16\nNUM_EPOCHS = 50\nLEARNING_RATE = 1e-3\nWEIGHT_DECAY = 1e-4\n\nDEVICE = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n\nnp.random.seed(SEED)\ntorch.manual_seed(SEED)\ntorch.cuda.manual_seed_all(SEED)\ntorch.backends.cudnn.deterministic = True\ntorch.backends.cudnn.benchmark = False\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-09T22:11:59.955679Z","iopub.execute_input":"2026-01-09T22:11:59.956142Z","iopub.status.idle":"2026-01-09T22:11:59.973002Z","shell.execute_reply.started":"2026-01-09T22:11:59.956122Z","shell.execute_reply":"2026-01-09T22:11:59.972447Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Load & Pivot Dataset","metadata":{}},{"cell_type":"code","source":"df = pd.read_csv(os.path.join(BASE_PATH, \"train.csv\"))\n\ndf = df.pivot_table(\n    index=\"image_path\",\n    columns=\"target_name\",\n    values=\"target\"\n).reset_index()\n\ndf.fillna(0, inplace=True)\n\nTARGET_COLUMNS = [\n    'Dry_Green_g',\n    'Dry_Dead_g',\n    'Dry_Clover_g',\n    'GDM_g',\n    'Dry_Total_g'\n]\n\nprint(\"Dataset shape:\", df.shape)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-09T22:11:59.973712Z","iopub.execute_input":"2026-01-09T22:11:59.973965Z","iopub.status.idle":"2026-01-09T22:12:00.009680Z","shell.execute_reply.started":"2026-01-09T22:11:59.973947Z","shell.execute_reply":"2026-01-09T22:12:00.009159Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Train / Validation Split & Scaling","metadata":{}},{"cell_type":"code","source":"train_df, val_df = train_test_split(\n    df,\n    test_size=0.2,\n    random_state=SEED,\n    shuffle=True\n)\n\ntarget_scaler = StandardScaler()\ntarget_scaler.fit(train_df[TARGET_COLUMNS].values)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-09T22:12:00.010894Z","iopub.execute_input":"2026-01-09T22:12:00.011096Z","iopub.status.idle":"2026-01-09T22:12:00.019773Z","shell.execute_reply.started":"2026-01-09T22:12:00.011078Z","shell.execute_reply":"2026-01-09T22:12:00.019205Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Dataset Class","metadata":{}},{"cell_type":"code","source":"class BiomassDataset(Dataset):\n    def __init__(self, df, image_dir, transform=None, scaler=None):\n        self.df = df.copy()\n        self.df[\"image_path\"] = self.df[\"image_path\"].apply(os.path.basename)\n        self.samples = list(self.df.itertuples(index=False))\n        self.image_dir = image_dir\n        self.transform = transform\n        self.scaler = scaler\n\n    def __len__(self):\n        return len(self.samples)\n\n    def __getitem__(self, idx):\n        row = self.samples[idx]\n        image = Image.open(\n            os.path.join(self.image_dir, row.image_path)\n        ).convert(\"RGB\")\n\n        if self.transform:\n            image = self.transform(image)\n\n        target = np.array(\n            [getattr(row, t) for t in TARGET_COLUMNS],\n            dtype=np.float32\n        )\n\n        if self.scaler:\n            target = self.scaler.transform(target.reshape(1, -1))[0]\n\n        return image, torch.tensor(target)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-09T22:12:00.020629Z","iopub.execute_input":"2026-01-09T22:12:00.020853Z","iopub.status.idle":"2026-01-09T22:12:00.034986Z","shell.execute_reply.started":"2026-01-09T22:12:00.020835Z","shell.execute_reply":"2026-01-09T22:12:00.034266Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Transforms (Medium Augmentation)","metadata":{}},{"cell_type":"code","source":"train_transforms = transforms.Compose([\n    transforms.Resize((IMG_SIZE, IMG_SIZE)),\n    transforms.RandomHorizontalFlip(p=0.5),\n    transforms.RandomRotation(10),\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\nval_transforms = transforms.Compose([\n    transforms.Resize((IMG_SIZE, IMG_SIZE)),\n    transforms.ToTensor(),\n    transforms.Normalize([0.485,0.456,0.406],[0.229,0.224,0.225])\n])\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-09T22:12:00.035858Z","iopub.execute_input":"2026-01-09T22:12:00.036259Z","iopub.status.idle":"2026-01-09T22:12:00.048198Z","shell.execute_reply.started":"2026-01-09T22:12:00.036239Z","shell.execute_reply":"2026-01-09T22:12:00.047600Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# DataLoaders","metadata":{}},{"cell_type":"code","source":"train_dataset = BiomassDataset(\n    train_df,\n    os.path.join(BASE_PATH, \"train\"),\n    train_transforms,\n    target_scaler\n)\n\nval_dataset = BiomassDataset(\n    val_df,\n    os.path.join(BASE_PATH, \"train\"),\n    val_transforms,\n    target_scaler\n)\n\ntrain_loader = DataLoader(\n    train_dataset,\n    batch_size=BATCH_SIZE,\n    shuffle=True,\n    num_workers=2\n)\n\nval_loader = DataLoader(\n    val_dataset,\n    batch_size=BATCH_SIZE,\n    shuffle=False,\n    num_workers=2\n)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-09T22:12:00.048926Z","iopub.execute_input":"2026-01-09T22:12:00.049150Z","iopub.status.idle":"2026-01-09T22:12:00.067830Z","shell.execute_reply.started":"2026-01-09T22:12:00.049123Z","shell.execute_reply":"2026-01-09T22:12:00.067184Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Weighted Loss & Metric","metadata":{}},{"cell_type":"code","source":"TARGET_WEIGHTS = torch.tensor([0.1,0.1,0.1,0.2,0.5], device=DEVICE)\n\nclass WeightedMSELoss(nn.Module):\n    def __init__(self, weights):\n        super().__init__()\n        self.register_buffer(\"weights\", weights)\n\n    def forward(self, preds, targets):\n        return ((preds - targets) ** 2 * self.weights).mean()\n\ncriterion = WeightedMSELoss(TARGET_WEIGHTS)\n\ndef weighted_r2_score(y_true, y_pred, weights):\n    mean_y = y_true.mean(0, keepdim=True)\n    ss_res = ((y_true - y_pred) ** 2).sum(0)\n    ss_tot = ((y_true - mean_y) ** 2).sum(0)\n    return (1 - (weights * ss_res).sum() / ((weights * ss_tot).sum() + 1e-8)).item()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-09T22:12:00.068533Z","iopub.execute_input":"2026-01-09T22:12:00.068706Z","iopub.status.idle":"2026-01-09T22:12:00.077674Z","shell.execute_reply.started":"2026-01-09T22:12:00.068691Z","shell.execute_reply":"2026-01-09T22:12:00.077109Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Optimized CNN Architecture","metadata":{}},{"cell_type":"code","source":"class OptimizedCNN(nn.Module):\n    def __init__(self, num_outputs=5):\n        super().__init__()\n        layers = []\n        in_channels = 3\n        num_filters = 256\n        \n        # EXPERIMENT WINNER: Kernel Size 3x3 (Best Val R2: 0.238753)\n        kernel_size = 3\n        padding = kernel_size // 2\n        \n        # EXPERIMENT WINNER: Dropout 0.6 (Best Val R2: 0.260552 with wd 0.0)\n        dropout_p = 0.6\n\n        # EXPERIMENT WINNER: 3 Blocks (Strong performance, balancing depth and loss)\n        for _ in range(3):\n            layers += [\n                nn.Conv2d(in_channels, num_filters, kernel_size, padding=padding),\n                nn.BatchNorm2d(num_filters),\n                nn.ReLU(inplace=True),\n                nn.MaxPool2d(2),\n                nn.Dropout(dropout_p)\n            ]\n            in_channels = num_filters\n\n        layers.append(nn.AdaptiveAvgPool2d(1))\n        self.features = nn.Sequential(*layers)\n        self.regressor = nn.Linear(num_filters, num_outputs)\n\n    def forward(self, x):\n        x = self.features(x)\n        return self.regressor(x.flatten(1))\n\n# --- TRAINING CONFIGURATION ---\n# EXPERIMENT WINNER: Weight Decay 0.0 (per the dropout_0.6_wd_0.0 config)\nmodel = OptimizedCNN(num_outputs=5)\noptimizer = torch.optim.Adam(\n    model.parameters(), \n    lr=0.001, \n    weight_decay=0.0  # Set to 0.0 based on your best R2 result\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-09T22:12:00.084871Z","iopub.execute_input":"2026-01-09T22:12:00.085081Z","iopub.status.idle":"2026-01-09T22:12:00.106199Z","shell.execute_reply.started":"2026-01-09T22:12:00.085063Z","shell.execute_reply":"2026-01-09T22:12:00.105632Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Epoch Function","metadata":{}},{"cell_type":"code","source":"def run_epoch(model, loader, optimizer=None):\n    training = optimizer is not None\n    model.train() if training else model.eval()\n\n    total_loss, total_r2, n = 0.0, 0.0, 0\n\n    with torch.set_grad_enabled(training):\n        for images, targets in loader:\n            images, targets = images.to(DEVICE), targets.to(DEVICE)\n            preds = model(images)\n            loss = criterion(preds, targets)\n\n            if training:\n                optimizer.zero_grad()\n                loss.backward()\n                optimizer.step()\n\n            b = images.size(0)\n            total_loss += loss.item() * b\n            total_r2 += weighted_r2_score(targets, preds, TARGET_WEIGHTS) * b\n            n += b\n\n    return total_loss / n, total_r2 / n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-09T22:12:00.107831Z","iopub.execute_input":"2026-01-09T22:12:00.108069Z","iopub.status.idle":"2026-01-09T22:12:00.113490Z","shell.execute_reply.started":"2026-01-09T22:12:00.108051Z","shell.execute_reply":"2026-01-09T22:12:00.112790Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Training Loop + Metrics + Best Model Saving","metadata":{}},{"cell_type":"code","source":"model = OptimizedCNN(num_outputs=len(TARGET_COLUMNS)).to(DEVICE)\noptimizer = optim.Adam(\n    model.parameters(),\n    lr=LEARNING_RATE,\n    weight_decay=WEIGHT_DECAY\n)\n\ntrain_losses, val_losses = [], []\ntrain_r2s, val_r2s = [], []\n\nbest_val_r2 = -1e9\n\nfor epoch in range(NUM_EPOCHS):\n    tr_l, tr_r2 = run_epoch(model, train_loader, optimizer)\n    va_l, va_r2 = run_epoch(model, val_loader)\n\n    train_losses.append(tr_l)\n    val_losses.append(va_l)\n    train_r2s.append(tr_r2)\n    val_r2s.append(va_r2)\n\n    if va_r2 > best_val_r2:\n        best_val_r2 = va_r2\n        torch.save(model.state_dict(), \"best_model_weights.pth\")\n        joblib.dump(target_scaler, \"target_scaler.pkl\")\n\n    print(\n        f\"Epoch {epoch+1}/{NUM_EPOCHS} | \"\n        f\"Train R²: {tr_r2:.4f} | Val R²: {va_r2:.4f}\"\n    )\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-09T22:12:00.114432Z","iopub.execute_input":"2026-01-09T22:12:00.114771Z","iopub.status.idle":"2026-01-09T22:21:19.245323Z","shell.execute_reply.started":"2026-01-09T22:12:00.114742Z","shell.execute_reply":"2026-01-09T22:21:19.244473Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Average Metrics","metadata":{}},{"cell_type":"code","source":"print(\"\\n===== FINAL AVERAGES =====\")\nprint(\"Avg Train Loss:\", np.mean(train_losses))\nprint(\"Avg Val Loss:\", np.mean(val_losses))\nprint(\"Avg Train R²:\", np.mean(train_r2s))\nprint(\"Avg Val R²:\", np.mean(val_r2s))\nprint(\"Best Val R²:\", best_val_r2)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-09T22:21:19.246581Z","iopub.execute_input":"2026-01-09T22:21:19.246828Z","iopub.status.idle":"2026-01-09T22:21:19.252120Z","shell.execute_reply.started":"2026-01-09T22:21:19.246801Z","shell.execute_reply":"2026-01-09T22:21:19.251459Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Plots","metadata":{}},{"cell_type":"code","source":"plt.figure(figsize=(14,5))\nplt.subplot(1,2,1)\nplt.plot(val_r2s, label=\"Val R²\")\nplt.title(\"Validation R²\")\nplt.legend()\n\nplt.subplot(1,2,2)\nplt.plot(val_losses, label=\"Val Loss\")\nplt.title(\"Validation Loss\")\nplt.legend()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-09T22:21:19.253764Z","iopub.execute_input":"2026-01-09T22:21:19.253959Z","iopub.status.idle":"2026-01-09T22:21:19.496494Z","shell.execute_reply.started":"2026-01-09T22:21:19.253943Z","shell.execute_reply":"2026-01-09T22:21:19.495801Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Inference","metadata":{}},{"cell_type":"markdown","source":"## Device & Paths","metadata":{}},{"cell_type":"code","source":"DEVICE = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nBASE_PATH = \"/kaggle/input/csiro-biomass\"\nTEST_DIR = f\"{BASE_PATH}/test\"\nMODEL_PATH = \"best_model_weights.pth\"\nSCALER_PATH = \"target_scaler.pkl\"","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-09T22:21:19.497223Z","iopub.execute_input":"2026-01-09T22:21:19.497447Z","iopub.status.idle":"2026-01-09T22:21:19.501343Z","shell.execute_reply.started":"2026-01-09T22:21:19.497427Z","shell.execute_reply":"2026-01-09T22:21:19.500700Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Target Columns (Must Match Training)","metadata":{}},{"cell_type":"code","source":"TARGET_COLUMNS = [\n    'Dry_Green_g',\n    'Dry_Dead_g',\n    'Dry_Clover_g',\n    'GDM_g',\n    'Dry_Total_g'\n]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-09T22:21:19.502075Z","iopub.execute_input":"2026-01-09T22:21:19.502259Z","iopub.status.idle":"2026-01-09T22:21:19.514386Z","shell.execute_reply.started":"2026-01-09T22:21:19.502242Z","shell.execute_reply":"2026-01-09T22:21:19.513684Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Load Model Weights & Scaler","metadata":{}},{"cell_type":"code","source":"best_model = OptimizedCNN(num_outputs=len(TARGET_COLUMNS)).to(DEVICE)\nbest_model.load_state_dict(\n    torch.load(MODEL_PATH, map_location=DEVICE)\n)\nbest_model.eval()\n\ntarget_scaler = joblib.load(SCALER_PATH)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-09T22:21:19.515246Z","iopub.execute_input":"2026-01-09T22:21:19.515590Z","iopub.status.idle":"2026-01-09T22:21:19.550760Z","shell.execute_reply.started":"2026-01-09T22:21:19.515563Z","shell.execute_reply":"2026-01-09T22:21:19.550063Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Image Transforms (Validation / Test Only)","metadata":{}},{"cell_type":"code","source":"image_transforms = transforms.Compose([\n    transforms.Resize((224, 224)),\n    transforms.ToTensor(),\n    transforms.Normalize(\n        mean=[0.485, 0.456, 0.406],\n        std=[0.229, 0.224, 0.225]\n    )\n])\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-09T22:21:19.551647Z","iopub.execute_input":"2026-01-09T22:21:19.552108Z","iopub.status.idle":"2026-01-09T22:21:19.555773Z","shell.execute_reply.started":"2026-01-09T22:21:19.552087Z","shell.execute_reply":"2026-01-09T22:21:19.555078Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Test-Time Inference","metadata":{}},{"cell_type":"code","source":"submission_rows = []\n\nwith torch.no_grad():\n    for file in tqdm(sorted(os.listdir(TEST_DIR))):\n        image = Image.open(os.path.join(TEST_DIR, file)).convert(\"RGB\")\n        tensor = image_transforms(image).unsqueeze(0).to(DEVICE)\n\n        preds_scaled = best_model(tensor).cpu().numpy()\n        preds = target_scaler.inverse_transform(preds_scaled)[0]\n\n        image_id = file.replace(\".jpg\", \"\")\n\n        for name, value in zip(TARGET_COLUMNS, preds):\n            submission_rows.append({\n                \"sample_id\": f\"{image_id}__{name}\",\n                \"target\": float(max(0.0, value))  # clamp negatives\n            })\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-09T22:21:19.556631Z","iopub.execute_input":"2026-01-09T22:21:19.556896Z","iopub.status.idle":"2026-01-09T22:21:19.794621Z","shell.execute_reply.started":"2026-01-09T22:21:19.556870Z","shell.execute_reply":"2026-01-09T22:21:19.793930Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Save Submission File","metadata":{}},{"cell_type":"code","source":"submission_df = pd.DataFrame(submission_rows)\nsubmission_df.to_csv(\"submission.csv\", index=False)\n\nsubmission_df.head()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-09T22:21:19.795487Z","iopub.execute_input":"2026-01-09T22:21:19.795743Z","iopub.status.idle":"2026-01-09T22:21:19.819053Z","shell.execute_reply.started":"2026-01-09T22:21:19.795723Z","shell.execute_reply":"2026-01-09T22:21:19.818378Z"}},"outputs":[],"execution_count":null}]}