{"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,"isSourceIdPinned":false,"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\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-26T16:09:21.882888Z","iopub.execute_input":"2025-12-26T16:09:21.883795Z","iopub.status.idle":"2025-12-26T16:09:21.888447Z","shell.execute_reply.started":"2025-12-26T16:09:21.883757Z","shell.execute_reply":"2025-12-26T16:09:21.887725Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"warnings.filterwarnings(\"ignore\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-26T16:09:28.661312Z","iopub.execute_input":"2025-12-26T16:09:28.661839Z","iopub.status.idle":"2025-12-26T16:09:28.665317Z","shell.execute_reply.started":"2025-12-26T16:09:28.661812Z","shell.execute_reply":"2025-12-26T16:09:28.664570Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Configuration & Reproducibility","metadata":{}},{"cell_type":"code","source":"SEED = 42\nBASE_PATH = \"/kaggle/input/csiro-biomass\"\n\nIMG_SIZE = 224\nBATCH_SIZE = 16\nNUM_EPOCHS = 50\n\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":"2025-12-26T16:09:43.610583Z","iopub.execute_input":"2025-12-26T16:09:43.611475Z","iopub.status.idle":"2025-12-26T16:09:43.617445Z","shell.execute_reply.started":"2025-12-26T16:09:43.611437Z","shell.execute_reply":"2025-12-26T16:09:43.616874Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Load & Reshape Training Data","metadata":{}},{"cell_type":"code","source":"train_csv = pd.read_csv(f\"{BASE_PATH}/train.csv\")\n\ntrain_df = train_csv.pivot_table(\n    index=\"image_path\",\n    columns=\"target_name\",\n    values=\"target\"\n).reset_index()\n\ntrain_df.fillna(0, inplace=True)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-26T16:09:59.529305Z","iopub.execute_input":"2025-12-26T16:09:59.529595Z","iopub.status.idle":"2025-12-26T16:09:59.545888Z","shell.execute_reply.started":"2025-12-26T16:09:59.529572Z","shell.execute_reply":"2025-12-26T16:09:59.545129Z"}},"outputs":[],"execution_count":null},{"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]\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-26T16:10:07.200286Z","iopub.execute_input":"2025-12-26T16:10:07.200575Z","iopub.status.idle":"2025-12-26T16:10:07.204496Z","shell.execute_reply.started":"2025-12-26T16:10:07.200549Z","shell.execute_reply":"2025-12-26T16:10:07.203738Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Train / Validation Split & Target Scaling","metadata":{}},{"cell_type":"code","source":"train_df, val_df = train_test_split(\n    train_df, test_size=0.2, random_state=SEED, shuffle=True\n)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-26T16:10:30.854204Z","iopub.execute_input":"2025-12-26T16:10:30.854860Z","iopub.status.idle":"2025-12-26T16:10:30.859552Z","shell.execute_reply.started":"2025-12-26T16:10:30.854834Z","shell.execute_reply":"2025-12-26T16:10:30.858843Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"target_scaler = StandardScaler()\ntarget_scaler.fit(train_df[TARGET_COLUMNS].values)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-26T16:10:35.923208Z","iopub.execute_input":"2025-12-26T16:10:35.923502Z","iopub.status.idle":"2025-12-26T16:10:35.931068Z","shell.execute_reply.started":"2025-12-26T16:10:35.923479Z","shell.execute_reply":"2025-12-26T16:10:35.930521Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Dataset Definition","metadata":{}},{"cell_type":"code","source":"class BiomassDataset(Dataset):\n    def __init__(self, dataframe, image_dir, transform, scaler=None):\n        self.df = dataframe.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\n        image = Image.open(\n            os.path.join(self.image_dir, row.image_path)\n        ).convert(\"RGB\")\n        image = self.transform(image)\n\n        targets = np.array(\n            [getattr(row, c) for c in TARGET_COLUMNS],\n            dtype=np.float32\n        )\n\n        if self.scaler:\n            targets = self.scaler.transform(targets.reshape(1, -1))[0]\n\n        return image, torch.tensor(targets)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-26T16:10:53.461505Z","iopub.execute_input":"2025-12-26T16:10:53.462061Z","iopub.status.idle":"2025-12-26T16:10:53.467957Z","shell.execute_reply.started":"2025-12-26T16:10:53.462034Z","shell.execute_reply":"2025-12-26T16:10:53.467226Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Transforms & DataLoaders","metadata":{}},{"cell_type":"code","source":"image_transforms = transforms.Compose([\n    transforms.Resize((IMG_SIZE, IMG_SIZE)),\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":"2025-12-26T16:11:17.269274Z","iopub.execute_input":"2025-12-26T16:11:17.270162Z","iopub.status.idle":"2025-12-26T16:11:17.274359Z","shell.execute_reply.started":"2025-12-26T16:11:17.270127Z","shell.execute_reply":"2025-12-26T16:11:17.273737Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_loader = DataLoader(\n    BiomassDataset(train_df, f\"{BASE_PATH}/train\", image_transforms, target_scaler),\n    batch_size=BATCH_SIZE, shuffle=True, num_workers=2\n)\n\nval_loader = DataLoader(\n    BiomassDataset(val_df, f\"{BASE_PATH}/train\", image_transforms, target_scaler),\n    batch_size=BATCH_SIZE, shuffle=False, num_workers=2\n)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-26T16:11:24.871049Z","iopub.execute_input":"2025-12-26T16:11:24.871603Z","iopub.status.idle":"2025-12-26T16:11:24.878388Z","shell.execute_reply.started":"2025-12-26T16:11:24.871577Z","shell.execute_reply":"2025-12-26T16:11:24.877655Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# CNN Regression Model","metadata":{}},{"cell_type":"code","source":"class BiomassCNN(nn.Module):\n    def __init__(self):\n        super().__init__()\n\n        self.features = nn.Sequential(\n            nn.Conv2d(3, 32, 3, padding=1),\n            nn.ReLU(),\n            nn.MaxPool2d(2),\n\n            nn.Conv2d(32, 64, 3, padding=1),\n            nn.ReLU(),\n            nn.MaxPool2d(2),\n\n            nn.Conv2d(64, 128, 3, padding=1),\n            nn.ReLU(),\n            nn.AdaptiveAvgPool2d(1)\n        )\n\n        self.regressor = nn.Linear(128, len(TARGET_COLUMNS))\n\n    def forward(self, x):\n        x = self.features(x)\n        return self.regressor(x.flatten(1))\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-26T16:11:39.849447Z","iopub.execute_input":"2025-12-26T16:11:39.849705Z","iopub.status.idle":"2025-12-26T16:11:39.854776Z","shell.execute_reply.started":"2025-12-26T16:11:39.849685Z","shell.execute_reply":"2025-12-26T16:11:39.854222Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"model = BiomassCNN().to(DEVICE)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-26T16:11:47.232131Z","iopub.execute_input":"2025-12-26T16:11:47.232411Z","iopub.status.idle":"2025-12-26T16:11:47.238414Z","shell.execute_reply.started":"2025-12-26T16:11:47.232391Z","shell.execute_reply":"2025-12-26T16:11:47.237841Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Loss Function, Optimizer & Metric","metadata":{}},{"cell_type":"code","source":"TARGET_WEIGHTS = torch.tensor(\n    [0.1, 0.1, 0.1, 0.2, 0.5],\n    device=DEVICE\n)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-26T16:12:07.603458Z","iopub.execute_input":"2025-12-26T16:12:07.603953Z","iopub.status.idle":"2025-12-26T16:12:07.607769Z","shell.execute_reply.started":"2025-12-26T16:12:07.603928Z","shell.execute_reply":"2025-12-26T16:12:07.606954Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class WeightedMSELoss(nn.Module):\n    def __init__(self, weights):\n        super().__init__()\n        self.register_buffer(\"weights\", weights)\n\n    def forward(self, predictions, targets):\n        return ((predictions - targets) ** 2 * self.weights).mean()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-26T16:12:12.792471Z","iopub.execute_input":"2025-12-26T16:12:12.793038Z","iopub.status.idle":"2025-12-26T16:12:12.796973Z","shell.execute_reply.started":"2025-12-26T16:12:12.793011Z","shell.execute_reply":"2025-12-26T16:12:12.796302Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"criterion = WeightedMSELoss(TARGET_WEIGHTS)\noptimizer = optim.Adam(\n    model.parameters(),\n    lr=LEARNING_RATE,\n    weight_decay=WEIGHT_DECAY\n)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-26T16:12:19.432675Z","iopub.execute_input":"2025-12-26T16:12:19.432935Z","iopub.status.idle":"2025-12-26T16:12:19.438019Z","shell.execute_reply.started":"2025-12-26T16:12:19.432914Z","shell.execute_reply":"2025-12-26T16:12:19.437228Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def 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    r2 = 1 - (weights * ss_res).sum() / ((weights * ss_tot).sum() + 1e-8)\n    return r2.item()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-26T16:12:24.752265Z","iopub.execute_input":"2025-12-26T16:12:24.752963Z","iopub.status.idle":"2025-12-26T16:12:24.756984Z","shell.execute_reply.started":"2025-12-26T16:12:24.752939Z","shell.execute_reply":"2025-12-26T16:12:24.756240Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Training / Validation Epoch Function","metadata":{}},{"cell_type":"code","source":"def run_epoch(model, dataloader, optimizer=None):\n    training = optimizer is not None\n    model.train() if training else model.eval()\n\n    total_loss, total_r2, count = 0.0, 0.0, 0\n\n    with torch.set_grad_enabled(training):\n        for images, targets in dataloader:\n            images = images.to(DEVICE)\n            targets = targets.to(DEVICE)\n\n            predictions = model(images)\n            loss = criterion(predictions, targets)\n\n            if training:\n                optimizer.zero_grad()\n                loss.backward()\n                optimizer.step()\n\n            batch_size = images.size(0)\n            total_loss += loss.item() * batch_size\n            total_r2 += weighted_r2_score(targets, predictions, TARGET_WEIGHTS) * batch_size\n            count += batch_size\n\n    return total_loss / count, total_r2 / count\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-26T16:12:44.411026Z","iopub.execute_input":"2025-12-26T16:12:44.411568Z","iopub.status.idle":"2025-12-26T16:12:44.416815Z","shell.execute_reply.started":"2025-12-26T16:12:44.411541Z","shell.execute_reply":"2025-12-26T16:12:44.416153Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Training Loop with History Tracking","metadata":{}},{"cell_type":"code","source":"train_losses, val_losses = [], []\ntrain_r2s, val_r2s = [], []\n\nbest_val_r2 = -1e9\n\nfor epoch in range(NUM_EPOCHS):\n    train_loss, train_r2 = run_epoch(model, train_loader, optimizer)\n    val_loss, val_r2 = run_epoch(model, val_loader)\n\n    train_losses.append(train_loss)\n    val_losses.append(val_loss)\n    train_r2s.append(train_r2)\n    val_r2s.append(val_r2)\n\n    if val_r2 > best_val_r2:\n        best_val_r2 = val_r2\n        torch.save(model.state_dict(), \"best_model.pth\")\n\n    print(\n        f\"Epoch {epoch+1}/{NUM_EPOCHS} | \"\n        f\"Train Loss: {train_loss:.4f}, R²: {train_r2:.4f} | \"\n        f\"Val Loss: {val_loss:.4f}, R²: {val_r2:.4f}\"\n    )\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-26T16:13:02.191534Z","iopub.execute_input":"2025-12-26T16:13:02.192145Z","iopub.status.idle":"2025-12-26T16:13:46.994297Z","shell.execute_reply.started":"2025-12-26T16:13:02.192119Z","shell.execute_reply":"2025-12-26T16:13:46.993418Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Training Curves (Loss & R²)","metadata":{}},{"cell_type":"code","source":"plt.figure(figsize=(14,4))\n\nplt.subplot(1,2,1)\nplt.plot(train_losses, label=\"Train Loss\")\nplt.plot(val_losses, label=\"Val Loss\")\nplt.legend()\nplt.title(\"Loss Curve\")\n\nplt.subplot(1,2,2)\nplt.plot(train_r2s, label=\"Train R²\")\nplt.plot(val_r2s, label=\"Val R²\")\nplt.legend()\nplt.title(\"Weighted R² Curve\")\n\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-26T16:13:46.995910Z","iopub.execute_input":"2025-12-26T16:13:46.996161Z","iopub.status.idle":"2025-12-26T16:13:47.258063Z","shell.execute_reply.started":"2025-12-26T16:13:46.996135Z","shell.execute_reply":"2025-12-26T16:13:47.257335Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Load Best Model","metadata":{}},{"cell_type":"code","source":"best_model = BiomassCNN().to(DEVICE)\nbest_model.load_state_dict(torch.load(\"best_model.pth\", map_location=DEVICE))\nbest_model.eval()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-26T16:13:47.259091Z","iopub.execute_input":"2025-12-26T16:13:47.259759Z","iopub.status.idle":"2025-12-26T16:13:47.272145Z","shell.execute_reply.started":"2025-12-26T16:13:47.259735Z","shell.execute_reply":"2025-12-26T16:13:47.271577Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Test Inference & Submission","metadata":{}},{"cell_type":"code","source":"test_dir = f\"{BASE_PATH}/test\"\nsubmission_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        for name, value in zip(TARGET_COLUMNS, preds):\n            submission_rows.append({\n                \"sample_id\": f\"{image_id}__{name}\",\n                \"target\": max(0, value)\n            })\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-26T16:13:52.223569Z","iopub.execute_input":"2025-12-26T16:13:52.224221Z","iopub.status.idle":"2025-12-26T16:13:52.324355Z","shell.execute_reply.started":"2025-12-26T16:13:52.224184Z","shell.execute_reply":"2025-12-26T16:13:52.323377Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"submission_df = pd.DataFrame(submission_rows)\nsubmission_df.head()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-26T16:14:05.123390Z","iopub.execute_input":"2025-12-26T16:14:05.123937Z","iopub.status.idle":"2025-12-26T16:14:05.144550Z","shell.execute_reply.started":"2025-12-26T16:14:05.123913Z","shell.execute_reply":"2025-12-26T16:14:05.144001Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"submission_df.to_csv(\"submission.csv\", index=False)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-26T16:14:13.643563Z","iopub.execute_input":"2025-12-26T16:14:13.643848Z","iopub.status.idle":"2025-12-26T16:14:13.652851Z","shell.execute_reply.started":"2025-12-26T16:14:13.643825Z","shell.execute_reply":"2025-12-26T16:14:13.652230Z"}},"outputs":[],"execution_count":null}]}