{"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":31260,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import pandas as pd\n\nimport torch\nfrom torch.utils.data import Dataset\nfrom PIL import Image\nimport pandas as pd\nimport numpy as np\nimport os\n\nfrom torchvision import transforms, models\nfrom torch.utils.data import DataLoader\n\nimport torch.nn as nn\nimport torch.nn.functional as F","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2026-01-18T08:34:00.150294Z","iopub.execute_input":"2026-01-18T08:34:00.150978Z","iopub.status.idle":"2026-01-18T08:34:00.155359Z","shell.execute_reply.started":"2026-01-18T08:34:00.150952Z","shell.execute_reply":"2026-01-18T08:34:00.154530Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"device = \"cuda\" if torch.cuda.is_available() else \"cpu\"","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-18T08:34:00.156651Z","iopub.execute_input":"2026-01-18T08:34:00.156851Z","iopub.status.idle":"2026-01-18T08:34:00.169962Z","shell.execute_reply.started":"2026-01-18T08:34:00.156834Z","shell.execute_reply":"2026-01-18T08:34:00.169353Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"MAIN_DIR = r\"/kaggle/input/csiro-biomass\"\nTRAIN_DIR = f\"{MAIN_DIR}/train\"\nTEST_DIR = f\"{MAIN_DIR}/test\"\n\nTRAIN_CSV = f\"{MAIN_DIR}/train.csv\"\nTEST_CSV = f\"{MAIN_DIR}/test.csv\"\nSUBMISSION_CSV = f\"{MAIN_DIR}/sample_submission.csv\"","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-18T08:34:00.170993Z","iopub.execute_input":"2026-01-18T08:34:00.171307Z","iopub.status.idle":"2026-01-18T08:34:00.181906Z","shell.execute_reply.started":"2026-01-18T08:34:00.171280Z","shell.execute_reply":"2026-01-18T08:34:00.181270Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Target Columns","metadata":{}},{"cell_type":"code","source":"TARGET_COLS = [\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-18T08:34:00.200056Z","iopub.execute_input":"2026-01-18T08:34:00.200313Z","iopub.status.idle":"2026-01-18T08:34:00.203850Z","shell.execute_reply.started":"2026-01-18T08:34:00.200293Z","shell.execute_reply":"2026-01-18T08:34:00.203107Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Input Per Sample\n\n| Component | Source          |\n| --------- | --------------- |\n| Image     | `image_path`    |\n| NDVI      | `Pre_GSH_NDVI`  |\n| Height    | `Height_Ave_cm` |\n| Species   | categorical     |\n| State     | categorical     |\n\nWe’ll encode:\n- numeric → float tensor\n- categorical → integer indices (Embedding ready)","metadata":{}},{"cell_type":"markdown","source":"# Step 1: Label Encoders for Categorical Columns","metadata":{}},{"cell_type":"code","source":"\ndef build_category_maps(csv_path):\n    df = pd.read_csv(csv_path)\n\n    state_map = {v: i for i, v in enumerate(sorted(df[\"State\"].dropna().unique()))}\n    species_map = {v: i for i, v in enumerate(sorted(df[\"Species\"].dropna().unique()))}\n\n    return state_map, species_map\n\n\nstate_map, species_map = build_category_maps(TRAIN_CSV)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-18T08:34:00.205353Z","iopub.execute_input":"2026-01-18T08:34:00.205629Z","iopub.status.idle":"2026-01-18T08:34:00.223068Z","shell.execute_reply.started":"2026-01-18T08:34:00.205600Z","shell.execute_reply":"2026-01-18T08:34:00.222534Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# tep 2 — Dataset Class\n\nThis dataset:\n\n- groups rows by image\n- loads image\n- returns (image, tabular_features, targets)","metadata":{}},{"cell_type":"code","source":"class PastureDataset(Dataset):\n    def __init__(\n        self,\n        csv_path,\n        image_root,\n        state_map,\n        species_map,\n        transform=None,\n        is_train=True,\n        normalize_targets=False,\n        target_mean=None,\n        target_std=None,\n    ):\n\n        \"\"\"\n        csv_path     : train.csv or test.csv\n        image_root   : base folder for images\n        state_map    : dict mapping state -> index\n        species_map  : dict mapping species -> index\n        transform    : torchvision transforms\n        is_train     : whether targets exist\n        \"\"\"\n        self.df = pd.read_csv(csv_path)\n        self.image_root = image_root\n        self.transform = transform\n        self.is_train = is_train\n        self.state_map = state_map\n        self.species_map = species_map\n        self.normalize_targets = normalize_targets\n        self.target_mean = target_mean\n        self.target_std = target_std\n\n\n        # -------------------------\n        # Group rows by image\n        # -------------------------\n        self.groups = self.df.groupby(\"image_path\")\n\n        # List of unique image paths\n        self.image_keys = list(self.groups.groups.keys())\n\n    def __len__(self):\n        return len(self.image_keys)\n\n    def _load_image(self, image_path):\n        full_path = os.path.join(self.image_root, image_path)\n        img = Image.open(full_path).convert(\"RGB\")\n        return img\n\n    def __getitem__(self, idx):\n        image_path = self.image_keys[idx]\n        rows = self.groups.get_group(image_path)\n\n        # -------------------------\n        # Load image\n        # -------------------------\n        img = self._load_image(image_path)\n        if self.transform:\n            img = self.transform(img)\n\n        # -------------------------\n        # Tabular features\n        # (same for all 5 rows)\n        # -------------------------\n        row0 = rows.iloc[0]\n\n        if \"Pre_GSHH_NDVI\" in row0:\n            ndvi = float(row0[\"Pre_GSHH_NDVI\"])\n        else:\n            ndvi = 0.0   # fallback for test set\n\n        # Height\n        if \"Height_Ave_cm\" in row0:\n            height = float(row0[\"Height_Ave_cm\"])\n        else:\n            height = 0.0\n\n        # State\n        if \"State\" in row0:\n            state_id = self.state_map[row0[\"State\"]]\n        else:\n            state_id = 0\n            \n        # Species\n        if \"Species\" in row0:\n            species_id = self.species_map[row0[\"Species\"]]\n        else:\n            species_id = 0\n\n        tabular = torch.tensor(\n            [ndvi, height, state_id, species_id],\n            dtype=torch.float32,\n        )\n\n        # -------------------------\n        # Targets (only for training)\n        # -------------------------\n        if self.is_train:\n            targets = np.zeros(len(TARGET_COLS), dtype=np.float32)\n\n            for i, target_name in enumerate(TARGET_COLS):\n                value = rows.loc[\n                    rows[\"target_name\"] == target_name, \"target\"\n                ].values[0]\n                targets[i] = value\n\n            targets = torch.tensor(targets, dtype=torch.float32)\n            if self.normalize_targets:\n                targets = (targets - self.target_mean) / self.target_std\n\n            return img, tabular, targets\n            \n\n        else:\n            # For test set (no targets)\n            return img, tabular, image_path\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-18T08:34:00.223901Z","iopub.execute_input":"2026-01-18T08:34:00.224156Z","iopub.status.idle":"2026-01-18T08:34:00.234880Z","shell.execute_reply.started":"2026-01-18T08:34:00.224131Z","shell.execute_reply":"2026-01-18T08:34:00.234245Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from torch.utils.data import DataLoader\nimport torch\n\ndef compute_target_stats(dataset):\n    ys = []\n\n    for _, _, y in DataLoader(dataset, batch_size=64):\n        ys.append(y)\n\n    ys = torch.cat(ys, dim=0)\n\n    mean = ys.mean(dim=0)\n    std  = ys.std(dim=0) + 1e-6\n\n    return mean, std\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-18T08:34:00.236152Z","iopub.execute_input":"2026-01-18T08:34:00.236458Z","iopub.status.idle":"2026-01-18T08:34:00.250198Z","shell.execute_reply.started":"2026-01-18T08:34:00.236436Z","shell.execute_reply":"2026-01-18T08:34:00.249624Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_tfms = transforms.Compose([\n    transforms.Resize((224, 224)),\n    transforms.ToTensor(),\n])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-18T08:34:00.269361Z","iopub.execute_input":"2026-01-18T08:34:00.269584Z","iopub.status.idle":"2026-01-18T08:34:00.273820Z","shell.execute_reply.started":"2026-01-18T08:34:00.269565Z","shell.execute_reply":"2026-01-18T08:34:00.273257Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"temp_train_ds = PastureDataset(\n    csv_path=TRAIN_CSV,\n    image_root=MAIN_DIR,\n    state_map=state_map,\n    species_map=species_map,\n    transform=train_tfms,\n    is_train=True,\n    normalize_targets=False,   # IMPORTANT\n)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-18T08:34:00.275184Z","iopub.execute_input":"2026-01-18T08:34:00.275518Z","iopub.status.idle":"2026-01-18T08:34:00.298868Z","shell.execute_reply.started":"2026-01-18T08:34:00.275497Z","shell.execute_reply":"2026-01-18T08:34:00.298118Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"target_mean, target_std = compute_target_stats(temp_train_ds)\n\nprint(\"Target Mean:\", target_mean)\nprint(\"Target Std :\", target_std)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-18T08:34:00.299803Z","iopub.execute_input":"2026-01-18T08:34:00.300076Z","iopub.status.idle":"2026-01-18T08:34:18.916363Z","shell.execute_reply.started":"2026-01-18T08:34:00.300046Z","shell.execute_reply":"2026-01-18T08:34:18.915480Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Step 3 — Instantiate Datasets","metadata":{}},{"cell_type":"code","source":"train_ds = PastureDataset(\n    csv_path=TRAIN_CSV,\n    image_root=MAIN_DIR,\n    state_map=state_map,\n    species_map=species_map,\n    transform=train_tfms,\n    is_train=True,\n    normalize_targets=True,\n    target_mean=target_mean,\n    target_std=target_std,\n)\n\n\ntest_ds = PastureDataset(\n    csv_path=TEST_CSV,\n    image_root=MAIN_DIR,\n    state_map=state_map,\n    species_map=species_map,\n    transform=train_tfms,\n    is_train=False,\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-18T08:34:18.917247Z","iopub.execute_input":"2026-01-18T08:34:18.917473Z","iopub.status.idle":"2026-01-18T08:34:18.933592Z","shell.execute_reply.started":"2026-01-18T08:34:18.917452Z","shell.execute_reply":"2026-01-18T08:34:18.933087Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Step 4 — DataLoader","metadata":{}},{"cell_type":"code","source":"\n\ntrain_loader = DataLoader(\n    train_ds,\n    batch_size=16,\n    shuffle=True,\n    num_workers=2,\n    pin_memory=True,\n)\n\ntest_loader = DataLoader(\n    test_ds,\n    batch_size=16,\n    shuffle=False,\n    num_workers = 2\n)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-18T08:34:18.939602Z","iopub.execute_input":"2026-01-18T08:34:18.939843Z","iopub.status.idle":"2026-01-18T08:34:18.950523Z","shell.execute_reply.started":"2026-01-18T08:34:18.939817Z","shell.execute_reply":"2026-01-18T08:34:18.949953Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Quick Sanity Check","metadata":{}},{"cell_type":"code","source":"img, tab, y = train_ds[0]\n\nprint(\"Image:\", img.shape)\nprint(\"Tabular:\", tab)\nprint(\"Targets:\", y)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-18T08:34:18.951304Z","iopub.execute_input":"2026-01-18T08:34:18.951577Z","iopub.status.idle":"2026-01-18T08:34:19.021694Z","shell.execute_reply.started":"2026-01-18T08:34:18.951550Z","shell.execute_reply":"2026-01-18T08:34:19.021007Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Model Architecture Overview\n```\nImage (3×224×224)\n   ↓\nCNN Backbone (ResNet / EfficientNet)\n   ↓\nImage Embedding (D_img)\n\nTabular Input [ndvi, height, state_id, species_id]\n   ↓\nEmbeddings for categorical features\n   ↓\nMLP for numeric features\n   ↓\nTabular Embedding (D_tab)\n\n[Image Embedding || Tabular Embedding]\n   ↓\nFusion MLP\n   ↓\nOutput (5 regression values)\n\n```\n\n# Dimensions\n\n| Component         | Dim  |\n| ----------------- | ---- |\n| Image embedding   | 512  |\n| NDVI + Height     | 2    |\n| State embedding   | 8    |\n| Species embedding | 16   |\n| Tabular embedding | ~64  |\n| Fusion vector     | ~576 |\n| Output            | 5    |\n","metadata":{}},{"cell_type":"markdown","source":"# Step 5: Tabular Encoder","metadata":{}},{"cell_type":"code","source":"class TabularEncoder(nn.Module):\n    def __init__(\n        self,\n        num_states,\n        num_species,\n        state_emb_dim=8,\n        species_emb_dim=16,\n        hidden_dim=64,\n    ):\n        super().__init__()\n\n        self.state_emb = nn.Embedding(num_states, state_emb_dim)\n        self.species_emb = nn.Embedding(num_species, species_emb_dim)\n\n        # NDVI + Height = 2 numeric features\n        numeric_dim = 2\n\n        input_dim = numeric_dim + state_emb_dim + species_emb_dim\n\n        self.mlp = nn.Sequential(\n            nn.Linear(input_dim, hidden_dim),\n            nn.BatchNorm1d(hidden_dim),\n            nn.ReLU(),\n        )\n\n        self.out_dim = hidden_dim\n\n    def forward(self, x):\n        \"\"\"\n        x shape: [B, 4]\n        [ndvi, height, state_id, species_id]\n        \"\"\"\n\n        ndvi = x[:, 0:1]\n        height = x[:, 1:2]\n        state_id = x[:, 2].long()\n        species_id = x[:, 3].long()\n\n        state_emb = self.state_emb(state_id)\n        species_emb = self.species_emb(species_id)\n\n        z = torch.cat([ndvi, height, state_emb, species_emb], dim=1)\n        z = self.mlp(z)\n\n        return z\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-18T08:34:19.022535Z","iopub.execute_input":"2026-01-18T08:34:19.022723Z","iopub.status.idle":"2026-01-18T08:34:19.029518Z","shell.execute_reply.started":"2026-01-18T08:34:19.022706Z","shell.execute_reply":"2026-01-18T08:34:19.028747Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Step 6: Image Encoder\n\nWe will use pretrained ResNet18","metadata":{}},{"cell_type":"code","source":"import torch\nimport torch.nn as nn\nfrom torchvision.models import resnet18\n\n\nclass ImageEncoder(nn.Module):\n    def __init__(self, pretrained=True):\n        super().__init__()\n\n        if pretrained:\n            from torchvision.models import ResNet18_Weights\n            weights = ResNet18_Weights.DEFAULT\n        else:\n            weights = None\n\n        backbone = resnet18(weights=weights)\n\n        # Remove classification head\n        self.features = nn.Sequential(*list(backbone.children())[:-1])\n        self.out_dim = backbone.fc.in_features   # 512\n\n    def forward(self, x):\n        \"\"\"\n        x: [B, 3, H, W]\n        \"\"\"\n        x = self.features(x)        # [B, 512, 1, 1]\n        x = x.flatten(1)            # [B, 512]\n        return x\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-18T08:34:19.030497Z","iopub.execute_input":"2026-01-18T08:34:19.030777Z","iopub.status.idle":"2026-01-18T08:34:19.045527Z","shell.execute_reply.started":"2026-01-18T08:34:19.030749Z","shell.execute_reply":"2026-01-18T08:34:19.044843Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Step 7: Fusion Model","metadata":{}},{"cell_type":"code","source":"class PastureNet(nn.Module):\n    def __init__(self, num_states, num_species, pretrained=True):\n        super().__init__()\n\n        # Only ONE image encoder initialization\n        self.image_encoder = ImageEncoder(pretrained=pretrained)\n\n        self.tabular_encoder = TabularEncoder(\n            num_states=num_states,\n            num_species=num_species,\n        )\n\n        fusion_dim = (\n            self.image_encoder.out_dim +\n            self.tabular_encoder.out_dim\n        )\n\n        self.regressor = nn.Sequential(\n            nn.Linear(fusion_dim, 256),\n            nn.ReLU(),\n            nn.Dropout(0.3),\n            nn.Linear(256, 128),\n            nn.ReLU(),\n            nn.Linear(128, 5)\n        )\n\n    def forward(self, image, tabular):\n        img_feat = self.image_encoder(image)\n        tab_feat = self.tabular_encoder(tabular)\n        fused = torch.cat([img_feat, tab_feat], dim=1)\n        return self.regressor(fused)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-18T08:34:19.046481Z","iopub.execute_input":"2026-01-18T08:34:19.046861Z","iopub.status.idle":"2026-01-18T08:34:19.058593Z","shell.execute_reply.started":"2026-01-18T08:34:19.046840Z","shell.execute_reply":"2026-01-18T08:34:19.057897Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"device = \"cuda\" if torch.cuda.is_available() else \"cpu\"\n\nmodel = PastureNet(\n    num_states=len(state_map),\n    num_species=len(species_map),\n    pretrained=False,    # download allowed\n).to(device)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-18T08:34:19.059770Z","iopub.execute_input":"2026-01-18T08:34:19.060460Z","iopub.status.idle":"2026-01-18T08:34:19.219448Z","shell.execute_reply.started":"2026-01-18T08:34:19.060439Z","shell.execute_reply":"2026-01-18T08:34:19.218675Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Sanity Check","metadata":{}},{"cell_type":"code","source":"B = 2\n\nimg = torch.randn(B, 3, 224, 224).to(device)\n\nndvi = torch.rand(B, 1).to(device) * 1.0         # [0,1]\nheight = torch.rand(B, 1).to(device) * 50.0     # realistic height\n\nstate_id = torch.randint(\n    0, len(state_map), (B, 1), device=device\n)\n\nspecies_id = torch.randint(\n    0, len(species_map), (B, 1), device=device\n)\n\ntab = torch.cat([ndvi, height, state_id.float(), species_id.float()], dim=1)\n\ny = model(img, tab)\nprint(y.shape)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-18T08:34:19.220611Z","iopub.execute_input":"2026-01-18T08:34:19.221199Z","iopub.status.idle":"2026-01-18T08:34:19.234367Z","shell.execute_reply.started":"2026-01-18T08:34:19.221174Z","shell.execute_reply":"2026-01-18T08:34:19.233807Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Step 8 — Weighted Loss (Matches Kaggle 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\ndef weighted_mse_loss(pred, target):\n    \"\"\"\n    pred, target: [B, 5]\n    \"\"\"\n    mse = (pred - target) ** 2\n    weighted = mse * TARGET_WEIGHTS\n    return weighted.mean()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-18T08:34:19.235244Z","iopub.execute_input":"2026-01-18T08:34:19.235838Z","iopub.status.idle":"2026-01-18T08:34:19.239628Z","shell.execute_reply.started":"2026-01-18T08:34:19.235816Z","shell.execute_reply":"2026-01-18T08:34:19.238957Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Step 9:Optimizer","metadata":{}},{"cell_type":"code","source":"optimizer = torch.optim.AdamW(\n    model.parameters(),\n    lr=2e-4,\n    weight_decay=1e-4,\n)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-18T08:34:19.242057Z","iopub.execute_input":"2026-01-18T08:34:19.242350Z","iopub.status.idle":"2026-01-18T08:34:19.254393Z","shell.execute_reply.started":"2026-01-18T08:34:19.242330Z","shell.execute_reply":"2026-01-18T08:34:19.253721Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"@torch.no_grad()\ndef weighted_r2_score(preds, targets):\n    \"\"\"\n    preds, targets: [N, 5]\n    \"\"\"\n    weights = TARGET_WEIGHTS.cpu().numpy()\n\n    y = targets.cpu().numpy().reshape(-1)\n    y_hat = preds.cpu().numpy().reshape(-1)\n\n    # Repeat weights for all samples\n    w = np.tile(weights, len(targets))\n\n    y_bar = np.sum(w * y) / np.sum(w)\n\n    rss = np.sum(w * (y - y_hat) ** 2)\n    tss = np.sum(w * (y - y_bar) ** 2)\n\n    return 1.0 - rss / (tss + 1e-8)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-18T08:34:19.255242Z","iopub.execute_input":"2026-01-18T08:34:19.255505Z","iopub.status.idle":"2026-01-18T08:34:19.264848Z","shell.execute_reply.started":"2026-01-18T08:34:19.255478Z","shell.execute_reply":"2026-01-18T08:34:19.264131Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from torch.amp import autocast, GradScaler\nfrom tqdm import tqdm\n\nscaler = GradScaler()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-18T08:34:19.266393Z","iopub.execute_input":"2026-01-18T08:34:19.266647Z","iopub.status.idle":"2026-01-18T08:34:19.280994Z","shell.execute_reply.started":"2026-01-18T08:34:19.266628Z","shell.execute_reply":"2026-01-18T08:34:19.280426Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from torch.amp import autocast, GradScaler\n\nscaler = GradScaler()\n\ndef train_one_epoch(model, loader, optimizer):\n    model.train()\n    running_loss = 0.0\n\n    for imgs, tabs, targets in loader:\n        imgs = imgs.to(device)\n        tabs = tabs.to(device)\n        targets = targets.to(device)\n\n        optimizer.zero_grad()\n\n        with autocast(\"cuda\"):\n            preds = model(imgs, tabs)\n            loss = weighted_mse_loss(preds, targets)\n\n        scaler.scale(loss).backward()\n        scaler.step(optimizer)\n        scaler.update()\n\n        running_loss += loss.item() * imgs.size(0)\n\n    return running_loss / len(loader.dataset)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-18T08:34:19.281815Z","iopub.execute_input":"2026-01-18T08:34:19.282039Z","iopub.status.idle":"2026-01-18T08:34:19.295527Z","shell.execute_reply.started":"2026-01-18T08:34:19.282019Z","shell.execute_reply":"2026-01-18T08:34:19.294851Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"@torch.no_grad()\ndef validate(model, loader):\n    model.eval()\n\n    all_preds = []\n    all_targets = []\n\n    for imgs, tabs, targets in tqdm(loader, leave=False):\n        imgs = imgs.to(device)\n        tabs = tabs.to(device)\n        targets = targets.to(device)\n\n        preds = model(imgs, tabs)\n\n        preds = preds.cpu() * target_std + target_mean\n        targets = targets.cpu() * target_std + target_mean\n        \n        all_preds.append(preds)\n        all_targets.append(targets)\n\n\n    all_preds = torch.cat(all_preds)\n    all_targets = torch.cat(all_targets)\n\n    r2 = weighted_r2_score(all_preds, all_targets)\n    return r2","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-18T08:34:19.296395Z","iopub.execute_input":"2026-01-18T08:34:19.296730Z","iopub.status.idle":"2026-01-18T08:34:19.308262Z","shell.execute_reply.started":"2026-01-18T08:34:19.296700Z","shell.execute_reply":"2026-01-18T08:34:19.307714Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from torch.utils.data import random_split\n\nval_size = int(0.15 * len(train_ds))\ntrain_size = len(train_ds) - val_size\n\ntrain_subset, val_subset = random_split(\n    train_ds,\n    [train_size, val_size],\n    generator=torch.Generator().manual_seed(42),\n)\n\ntrain_loader = DataLoader(train_subset, batch_size=32, shuffle=True, num_workers=2)\nval_loader   = DataLoader(val_subset, batch_size=32, shuffle=False, num_workers=2)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-18T08:34:19.309046Z","iopub.execute_input":"2026-01-18T08:34:19.309315Z","iopub.status.idle":"2026-01-18T08:34:19.322381Z","shell.execute_reply.started":"2026-01-18T08:34:19.309293Z","shell.execute_reply":"2026-01-18T08:34:19.321715Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"EPOCHS = 30\nbest_r2 = -1e9","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-18T08:34:19.323187Z","iopub.execute_input":"2026-01-18T08:34:19.323436Z","iopub.status.idle":"2026-01-18T08:34:19.335978Z","shell.execute_reply.started":"2026-01-18T08:34:19.323404Z","shell.execute_reply":"2026-01-18T08:34:19.335472Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"for epoch in range(EPOCHS):\n    train_loss = train_one_epoch(model, train_loader, optimizer)\n    val_r2 = validate(model, val_loader)\n\n    print(\n        f\"Epoch [{epoch+1}/{EPOCHS}] | \"\n        f\"Train Loss: {train_loss:.4f} | \"\n        f\"Val Weighted R2: {val_r2:.4f}\"\n    )\n\n    if val_r2 > best_r2:\n        best_r2 = val_r2\n        torch.save(model.state_dict(), \"best_model.pth\")\n        print(\"Best model saved!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-18T08:34:19.336797Z","iopub.execute_input":"2026-01-18T08:34:19.337040Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_cols = set(pd.read_csv(TRAIN_CSV).columns)\ntest_cols  = set(pd.read_csv(TEST_CSV).columns)\n\nprint(\"Missing in test:\", train_cols - test_cols)\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"model = PastureNet(\n    num_states=len(state_map),\n    num_species=len(species_map),\n    pretrained=False,   # no internet\n).to(device)\n\nmodel.load_state_dict(torch.load(\"best_model.pth\", map_location=device))\nmodel.eval()","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"all_preds = {}\n# key: image_path\n# value: prediction vector (5,)\n\nwith torch.no_grad():\n    for imgs, tabs, image_paths in test_loader:\n        imgs = imgs.to(device)\n        tabs = tabs.to(device)\n\n        preds = model(imgs, tabs)          # normalized\n        preds = preds.cpu()\n\n        # De-normalize\n        preds = preds * target_std + target_mean\n\n        for img_path, pred_vec in zip(image_paths, preds):\n            all_preds[img_path] = pred_vec.numpy()\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"test_df = pd.read_csv(TEST_CSV)\n\nprint(test_df.head())\nprint(\"Rows:\", len(test_df))\nprint(\"Unique images:\", test_df[\"image_path\"].nunique())","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"submission_rows = []\n\nfor _, row in test_df.iterrows():\n    img_path = row[\"image_path\"]\n    target_name = row[\"target_name\"]\n    sample_id = row[\"sample_id\"]\n\n    pred_vec = all_preds[img_path]\n    target_index = TARGET_COLS.index(target_name)\n\n    value = float(pred_vec[target_index])\n\n    submission_rows.append({\n        \"sample_id\": sample_id,\n        \"target\": value,\n    })\n\nsubmission_df = pd.DataFrame(submission_rows)\n\nprint(submission_df.head())\nprint(\"Total rows:\", len(submission_df))","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"submission_path = \"/kaggle/working/submission.csv\"\nsubmission_df.to_csv(submission_path, index=False)\n\nprint(\"Saved to:\", submission_path)","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}