{"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":true,"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\nwarnings.filterwarnings(\"ignore\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-27T01:23:40.662598Z","iopub.execute_input":"2025-12-27T01:23:40.663219Z","iopub.status.idle":"2025-12-27T01:23:40.668366Z","shell.execute_reply.started":"2025-12-27T01:23:40.663193Z","shell.execute_reply":"2025-12-27T01:23:40.667593Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Configuration & Reproducibility","metadata":{}},{"cell_type":"code","source":"SEED = 42\nBASE_PATH = \"/kaggle/input/csiro-biomass\"\nIMG_SIZE = 224\nBATCH_SIZE = 16\nNUM_EPOCHS = 20\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-27T01:23:40.669546Z","iopub.execute_input":"2025-12-27T01:23:40.669769Z","iopub.status.idle":"2025-12-27T01:23:40.689463Z","shell.execute_reply.started":"2025-12-27T01:23:40.669751Z","shell.execute_reply":"2025-12-27T01:23:40.688780Z"}},"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":"2025-12-27T01:23:40.690789Z","iopub.execute_input":"2025-12-27T01:23:40.691003Z","iopub.status.idle":"2025-12-27T01:23:40.723692Z","shell.execute_reply.started":"2025-12-27T01:23:40.690985Z","shell.execute_reply":"2025-12-27T01:23:40.722897Z"}},"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    df,\n    test_size=0.2,\n    random_state=SEED,\n    shuffle=True\n)\n\nscaler = StandardScaler()\nscaler.fit(train_df[TARGET_COLUMNS].values)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-27T01:23:40.724676Z","iopub.execute_input":"2025-12-27T01:23:40.724952Z","iopub.status.idle":"2025-12-27T01:23:40.735889Z","shell.execute_reply.started":"2025-12-27T01:23:40.724925Z","shell.execute_reply":"2025-12-27T01:23:40.733533Z"}},"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        img_path = os.path.join(self.image_dir, row.image_path)\n        image = Image.open(img_path).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":"2025-12-27T01:23:40.737966Z","iopub.execute_input":"2025-12-27T01:23:40.738181Z","iopub.status.idle":"2025-12-27T01:23:40.754943Z","shell.execute_reply.started":"2025-12-27T01:23:40.738161Z","shell.execute_reply":"2025-12-27T01:23:40.754294Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# DataLoaders","metadata":{}},{"cell_type":"code","source":"transform = 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\ntrain_dataset = BiomassDataset(\n    train_df,\n    os.path.join(BASE_PATH, \"train\"),\n    transform,\n    scaler\n)\n\nval_dataset = BiomassDataset(\n    val_df,\n    os.path.join(BASE_PATH, \"train\"),\n    transform,\n    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":"2025-12-27T01:23:40.755801Z","iopub.execute_input":"2025-12-27T01:23:40.756246Z","iopub.status.idle":"2025-12-27T01:23:40.778144Z","shell.execute_reply.started":"2025-12-27T01:23:40.756225Z","shell.execute_reply":"2025-12-27T01:23:40.777516Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Weighted Loss  ","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\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","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-27T01:23:40.778883Z","iopub.execute_input":"2025-12-27T01:23:40.779053Z","iopub.status.idle":"2025-12-27T01:23:40.791961Z","shell.execute_reply.started":"2025-12-27T01:23:40.779036Z","shell.execute_reply":"2025-12-27T01:23:40.791253Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Weighted R² Metric","metadata":{}},{"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    return (1 - (weights * ss_res).sum() /\n            ((weights * ss_tot).sum() + 1e-8)).item()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-27T01:23:40.792889Z","iopub.execute_input":"2025-12-27T01:23:40.793098Z","iopub.status.idle":"2025-12-27T01:23:40.805239Z","shell.execute_reply.started":"2025-12-27T01:23:40.793081Z","shell.execute_reply":"2025-12-27T01:23:40.804659Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# CNN with Variable Kernel Size","metadata":{}},{"cell_type":"code","source":"class CNNWithKernelSize(nn.Module):\n    def __init__(self, kernel_size=3, num_filters=256, num_blocks=3, num_outputs=5):\n        super().__init__()\n\n        layers = []\n        in_channels = 3\n        padding = kernel_size // 2\n\n        for _ in range(num_blocks):\n            layers.append(\n                nn.Conv2d(\n                    in_channels,\n                    num_filters,\n                    kernel_size=kernel_size,\n                    padding=padding\n                )\n            )\n            layers.append(nn.BatchNorm2d(num_filters))\n            layers.append(nn.ReLU(inplace=True))\n            layers.append(nn.MaxPool2d(2))\n            in_channels = num_filters\n\n        layers.append(nn.AdaptiveAvgPool2d(1))\n\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","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-27T01:23:40.806829Z","iopub.execute_input":"2025-12-27T01:23:40.807013Z","iopub.status.idle":"2025-12-27T01:23:40.821740Z","shell.execute_reply.started":"2025-12-27T01:23:40.806998Z","shell.execute_reply":"2025-12-27T01:23:40.821201Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# One-Epoch Train / Eval 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 = images.to(DEVICE)\n            targets = targets.to(DEVICE)\n\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            bsz = images.size(0)\n            total_loss += loss.item() * bsz\n            total_r2 += weighted_r2_score(\n                targets, preds, TARGET_WEIGHTS\n            ) * bsz\n            n += bsz\n\n    return total_loss / n, total_r2 / n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-27T01:23:40.822434Z","iopub.execute_input":"2025-12-27T01:23:40.822628Z","iopub.status.idle":"2025-12-27T01:23:40.834304Z","shell.execute_reply.started":"2025-12-27T01:23:40.822611Z","shell.execute_reply":"2025-12-27T01:23:40.833751Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Kernel Size Experiment [3, 5, 7]","metadata":{}},{"cell_type":"code","source":"kernel_options = [3, 5, 7]\nresults = {}\n\nfor k in kernel_options:\n    print(f\"\\n==============================\")\n    print(f\" Training CNN with kernel {k}x{k}\")\n    print(f\"==============================\")\n\n    model = CNNWithKernelSize(\n        kernel_size=k,\n        num_filters=256,\n        num_blocks=3,\n        num_outputs=len(TARGET_COLUMNS)\n    ).to(DEVICE)\n\n    optimizer = optim.Adam(\n        model.parameters(),\n        lr=LEARNING_RATE,\n        weight_decay=WEIGHT_DECAY\n    )\n\n    train_losses, val_losses = [], []\n    train_r2s, val_r2s = [], []\n    best_val_r2 = -1e9\n\n    # -------- Training epochs --------\n    for epoch in range(NUM_EPOCHS):\n        train_loss, train_r2 = run_epoch(\n            model, train_loader, optimizer\n        )\n        val_loss, val_r2 = run_epoch(\n            model, val_loader\n        )\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        best_val_r2 = max(best_val_r2, val_r2)\n\n        print(\n            f\"Epoch [{epoch+1}/{NUM_EPOCHS}] | \"\n            f\"Train Loss: {train_loss:.4f} | \"\n            f\"Val Loss: {val_loss:.4f} | \"\n            f\"Train R²: {train_r2:.4f} | \"\n            f\"Val R²: {val_r2:.4f}\"\n        )\n\n    # -------- Store averaged metrics --------\n    results[k] = {\n        \"best_val_r2\": best_val_r2,\n        \"avg_train_loss\": float(np.mean(train_losses)),\n        \"avg_val_loss\": float(np.mean(val_losses)),\n        \"avg_train_r2\": float(np.mean(train_r2s)),\n        \"avg_val_r2\": float(np.mean(val_r2s)),\n        \"train_losses\": train_losses,\n        \"val_losses\": val_losses,\n        \"train_r2s\": train_r2s,\n        \"val_r2s\": val_r2s\n    }\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-27T01:23:40.835278Z","iopub.execute_input":"2025-12-27T01:23:40.835722Z","iopub.status.idle":"2025-12-27T01:23:42.909190Z","shell.execute_reply.started":"2025-12-27T01:23:40.835703Z","shell.execute_reply":"2025-12-27T01:23:42.908271Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Summary Table","metadata":{}},{"cell_type":"code","source":"summary_df = pd.DataFrame([\n    {\n        \"Kernel\": f\"{k}x{k}\",\n        \"Avg Train Loss\": results[k][\"avg_train_loss\"],\n        \"Avg Val Loss\": results[k][\"avg_val_loss\"],\n        \"Avg Train R2\": results[k][\"avg_train_r2\"],\n        \"Avg Val R2\": results[k][\"avg_val_r2\"],\n        \"Best Val R2\": results[k][\"best_val_r2\"]\n    }\n    for k in kernel_options\n]).sort_values(\"Avg Val R2\", ascending=False)\n\ndisplay(summary_df)\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Plot Validation R²","metadata":{}},{"cell_type":"code","source":"plt.figure(figsize=(16, 6))\nfor k in kernel_options:\n    plt.plot(results[k][\"val_r2s\"], label=f\"Kernel {k}×{k}\")\nplt.title(\"Validation Weighted R² vs Epoch\")\nplt.xlabel(\"Epoch\")\nplt.ylabel(\"Weighted R²\")\nplt.legend()\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-27T01:23:42.910558Z","iopub.status.idle":"2025-12-27T01:23:42.910845Z","shell.execute_reply.started":"2025-12-27T01:23:42.910714Z","shell.execute_reply":"2025-12-27T01:23:42.910731Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Plot Validation Loss","metadata":{}},{"cell_type":"code","source":"plt.figure(figsize=(16, 6))\nfor k in kernel_options:\n    plt.plot(results[k][\"val_losses\"], label=f\"Kernel {k}×{k}\")\nplt.title(\"Validation Loss vs Epoch\")\nplt.xlabel(\"Epoch\")\nplt.ylabel(\"Loss\")\nplt.legend()\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-27T01:23:42.911753Z","iopub.status.idle":"2025-12-27T01:23:42.912324Z","shell.execute_reply.started":"2025-12-27T01:23:42.912186Z","shell.execute_reply":"2025-12-27T01:23:42.912202Z"}},"outputs":[],"execution_count":null}]}