{"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":[{"sourceType":"competition","sourceId":112509,"databundleVersionId":14254895},{"sourceType":"modelInstanceVersion","sourceId":862165,"databundleVersionId":17122045,"modelInstanceId":655426,"modelId":667385},{"sourceType":"modelInstanceVersion","sourceId":501,"databundleVersionId":4904669,"modelInstanceId":378,"modelId":34},{"sourceType":"modelInstanceVersion","sourceId":862173,"databundleVersionId":17122187,"modelInstanceId":655432,"modelId":667392}],"dockerImageVersionId":31328,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# This Python 3 environment comes with many helpful analytics libraries installed\n# It is defined by the kaggle/python Docker image: https://github.com/kaggle/docker-python\n# For example, here's several helpful packages to load\n\n# Input data files are available in the read-only \"../input/\" directory\nimport os\nimport os\nimport numpy as np\nimport pandas as pd\nfrom PIL import Image\n\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import Dataset, DataLoader\n\nimport torchvision.transforms as transforms\nimport torchvision.models as models\n\nDEVICE = torch.device(\"cuda\")\n\nBATCH_SIZE = 16\nEPOCHS = 40\n\nWEIGHTS_PATH = \"/kaggle/input/models/mdhhkviyeigsi/convnxt/pytorch/default/1/convnext_tiny-983f1562.pth\"\nDATA_DIR = \"/kaggle/input/competitions/csiro-biomass\"\nTRAIN_CSV = os.path.join(DATA_DIR, \"train.csv\")\nTEST_CSV = os.path.join(DATA_DIR, \"test.csv\")\n\n# You can write up to 20GB to the current directory (/kaggle/working/) that gets preserved as output when you create a version using \"Save & Run All\" \n# You can also write temporary files to /kaggle/temp/, but they won't be saved outside of the current session","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2026-05-10T18:46:49.076434Z","iopub.execute_input":"2026-05-10T18:46:49.076791Z","iopub.status.idle":"2026-05-10T18:46:49.082626Z","shell.execute_reply.started":"2026-05-10T18:46:49.076764Z","shell.execute_reply":"2026-05-10T18:46:49.081728Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class ConvNeXtRegressor(nn.Module):\n    def __init__(self, weights_path=None, output_dim=5):\n        super().__init__()\n\n        backbone = models.convnext_tiny(weights=None)\n\n        if weights_path is not None:\n            state_dict = torch.load(weights_path, map_location=\"cpu\")\n            backbone.load_state_dict(state_dict)\n            print(\"✅ Loaded pretrained weights\")\n\n        self.encoder = backbone.features\n\n        # OPTIONAL:\n        # freeze early layers only\n        for p in self.encoder.parameters():\n            p.requires_grad = False\n\n \n\n        self.pool = nn.AdaptiveAvgPool2d((1, 1))\n\n        self.feature_dim = 768\n\n        # -----------------------------------\n        # ADD ATTENTION LAYER HERE\n        # -----------------------------------\n        self.attn = nn.MultiheadAttention(\n            embed_dim=768,\n            num_heads=8,\n            batch_first=True\n        )\n\n        # NEW HEAD\n        self.head = nn.Sequential(\n            nn.Linear(768 * 2, 512),\n            nn.ReLU(),\n            nn.Dropout(0.3),\n\n            nn.Linear(512, 256),\n            nn.ReLU(),\n\n            nn.Linear(256, output_dim),\n            nn.ReLU()\n        )\n\n    def forward(self, left, right):\n\n        # extract features\n        f_left = self.pool(self.encoder(left)).flatten(1)\n        f_right = self.pool(self.encoder(right)).flatten(1)\n\n        # -----------------------------------\n        # STACK FEATURES FOR ATTENTION\n        # shape becomes: (B, 2, 768)\n        # -----------------------------------\n        x = torch.stack([f_left, f_right], dim=1)\n\n        # -----------------------------------\n        # APPLY ATTENTION\n        # -----------------------------------\n        attn_out, _ = self.attn(x, x, x)\n\n        # -----------------------------------\n        # FLATTEN BACK TO (B, 1536)\n        # -----------------------------------\n        x = attn_out.reshape(attn_out.size(0), -1)\n\n        return self.head(x)\n\nmodel = ConvNeXtRegressor(\n    weights_path=WEIGHTS_PATH,\n    output_dim=5\n).to(DEVICE)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-10T18:47:52.104730Z","iopub.execute_input":"2026-05-10T18:47:52.105564Z","iopub.status.idle":"2026-05-10T18:47:52.683421Z","shell.execute_reply.started":"2026-05-10T18:47:52.105529Z","shell.execute_reply":"2026-05-10T18:47:52.682806Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class ConvNeXtRegressor(nn.Module):\n    def __init__(self, weights_path=None, output_dim=5):\n        super().__init__()\n\n        backbone = models.convnext_tiny(weights=None)\n        if weights_path is not None:\n            state_dict = torch.load(weights_path, map_location=\"cpu\")\n            backbone.load_state_dict(state_dict)\n            print(\"✅ Loaded pretrained weights\")\n\n        self.encoder = backbone.features  # feature extractor\n\n        # DO NOT freeze everything\n        for p in self.encoder.parameters():\n            p.requires_grad = False\n\n        self.pool = nn.AdaptiveAvgPool2d((1, 1))\n\n        self.feature_dim = 768  # convnext_tiny output\n\n        self.head = nn.Sequential(\n            nn.Linear(self.feature_dim * 4, 512),\n            nn.ReLU(),\n            nn.Dropout(0.3),\n            nn.Linear(512, 256),\n            nn.ReLU(),\n            nn.Linear(256, output_dim),\n            nn.ReLU()\n        \n        )\n\n    def forward(self, left, right):\n        f_left = self.pool(self.encoder(left)).flatten(1)\n        f_right = self.pool(self.encoder(right)).flatten(1)\n        #x = torch.cat([f_left, f_right], dim=1)\n        x = torch.cat([\n         f_left,\n         f_right,\n         torch.abs(f_left - f_right),\n         f_left * f_right\n         ], dim=1)\n        return self.head(x)\n\n\nmodel = ConvNeXtRegressor(\n    weights_path=WEIGHTS_PATH,\n    output_dim=5\n).to(DEVICE)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-10T18:36:21.431278Z","iopub.execute_input":"2026-05-10T18:36:21.432052Z","iopub.status.idle":"2026-05-10T18:36:24.129664Z","shell.execute_reply.started":"2026-05-10T18:36:21.432016Z","shell.execute_reply":"2026-05-10T18:36:24.129044Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class BiomassDataset(Dataset):\n    def __init__(self, df, img_dir, transform=None, is_test=False):\n        self.df = df.reset_index(drop=True)\n        self.img_dir = img_dir\n        self.transform = transform\n        self.is_test = is_test\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n\n        img_path = os.path.join(self.img_dir, row[\"image_path\"])\n        image = Image.open(img_path).convert(\"RGB\")\n\n        img = np.array(image)\n        h, w, _ = img.shape\n        mid = w // 2\n\n        left = Image.fromarray(img[:, :mid])\n        right = Image.fromarray(img[:, mid:])\n\n        if self.transform:\n            left = self.transform(left)\n            right = self.transform(right)\n\n        if self.is_test:\n            sample_id = self.df.iloc[idx][\"sample_id\"]\n            sample_id = str(sample_id)\n\n            return left, right, sample_id\n\n        targets = torch.tensor([\n            row[\"Dry_Clover_g\"],\n            row[\"Dry_Dead_g\"],\n            row[\"Dry_Green_g\"],\n            row[\"Dry_Total_g\"],\n            row[\"GDM_g\"]\n        ], dtype=torch.float32)\n\n        return left, right, targets","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-10T18:48:01.681451Z","iopub.execute_input":"2026-05-10T18:48:01.681755Z","iopub.status.idle":"2026-05-10T18:48:01.690034Z","shell.execute_reply.started":"2026-05-10T18:48:01.681731Z","shell.execute_reply":"2026-05-10T18:48:01.689354Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"transform = transforms.Compose([\n    transforms.Resize((512, 512)),\n    #transforms.RandomHorizontalFlip(p=0.5),\n    transforms.RandomVerticalFlip(p=0.5),\n    #transforms.RandomGrayscale(p=0.2),\n    #transforms.GaussianBlur(kernel_size=(5, 9)),\n    #transforms.RandomRotation(degrees=90),\n    transforms.ColorJitter(brightness=0.3, contrast=0.3, saturation=0.3, hue=0.15),\n    transforms.ToTensor(),\n    transforms.Normalize((0.485, 0.456, 0.406),\n                         (0.229, 0.224, 0.225))\n])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-10T18:48:04.346750Z","iopub.execute_input":"2026-05-10T18:48:04.347017Z","iopub.status.idle":"2026-05-10T18:48:04.352121Z","shell.execute_reply.started":"2026-05-10T18:48:04.346995Z","shell.execute_reply":"2026-05-10T18:48:04.351516Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df = pd.read_csv(TRAIN_CSV)\n\ndf[\"base_id\"] = df[\"sample_id\"].apply(lambda x: x.split(\"__\")[0])\n\ndf_wide = df.pivot(index=\"base_id\", columns=\"target_name\", values=\"target\").reset_index()\nimg_paths = df.groupby(\"base_id\")[\"image_path\"].first().reset_index()\n\ndf = df_wide.merge(img_paths, on=\"base_id\")\ndf = df.rename(columns={\"base_id\": \"sample_id\"})\n\ndataset = BiomassDataset(df, DATA_DIR, transform)\n\ntrain_size = int(0.8 * len(dataset))\nval_size = len(dataset) - train_size\n\ntrain_ds, val_ds = torch.utils.data.random_split(dataset, [train_size, val_size])\n\ntrain_loader = DataLoader(train_ds, batch_size=BATCH_SIZE, shuffle=True)\nval_loader = DataLoader(val_ds, batch_size=BATCH_SIZE)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-10T18:48:06.134216Z","iopub.execute_input":"2026-05-10T18:48:06.134852Z","iopub.status.idle":"2026-05-10T18:48:06.194874Z","shell.execute_reply.started":"2026-05-10T18:48:06.134820Z","shell.execute_reply":"2026-05-10T18:48:06.194226Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"criterion = nn.SmoothL1Loss() \noptimizer = torch.optim.Adam(model.parameters(), lr=1e-3)\n\ndef r2_score(y_true, y_pred):\n    y_true = y_true.cpu().numpy()\n    y_pred = y_pred.cpu().numpy()\n    ss_res = ((y_true - y_pred) ** 2).sum()\n    ss_tot = ((y_true - y_true.mean()) ** 2).sum()\n    return 1 - ss_res / (ss_tot + 1e-8)\n\nfor epoch in range(EPOCHS):\n    model.train()\n    train_loss = 0\n\n    for left, right, targets in train_loader:\n        left, right, targets = left.to(DEVICE), right.to(DEVICE), targets.to(DEVICE)\n\n        preds = model(left, right)\n        loss = criterion(preds, targets)\n\n        optimizer.zero_grad()\n        loss.backward()\n        optimizer.step()\n\n        train_loss += loss.item()\n\n    # validation\n    model.eval()\n    preds_all, targets_all = [], []\n\n    with torch.no_grad():\n        for left, right, targets in val_loader:\n            left, right = left.to(DEVICE), right.to(DEVICE)\n            preds = model(left, right)\n\n            preds_all.append(preds.cpu())\n            targets_all.append(targets)\n\n    preds_all = torch.cat(preds_all)\n    targets_all = torch.cat(targets_all)\n\n    r2 = r2_score(targets_all, preds_all)\n\n    print(f\"Epoch {epoch+1}: Loss={train_loss:.4f}, Val R²={r2:.4f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-10T18:48:08.868531Z","iopub.execute_input":"2026-05-10T18:48:08.869112Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# -------------------------\n# Load test dataset\n# -------------------------\n# test_df = pd.read_csv(TEST_CSV)\n\n# # Extract base ID\n# test_df[\"base_id\"] = test_df[\"sample_id\"].apply(lambda x: x.split(\"__\")[0])\n\n# # Keep ONE row per image (cleanly)\n# test_df_unique = (\n#     test_df.groupby(\"base_id\", as_index=False)\n#     .agg({\n#         \"image_path\": \"first\"\n#     })\n# )\n\n# # Rename correctly\n# test_df_unique = test_df_unique.rename(columns={\"base_id\": \"sample_id\"})\n\n\n\n# test_dataset = BiomassDataset(test_df_unique, DATA_DIR, transform, is_test=True)\n\n# sample = test_dataset[0]\n# print(len(sample))\n# print(type(sample[2]))\n\n# test_loader = DataLoader(test_dataset, batch_size=BATCH_SIZE, shuffle=False)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-09T23:28:40.111823Z","iopub.execute_input":"2026-05-09T23:28:40.112740Z","iopub.status.idle":"2026-05-09T23:28:40.296807Z","shell.execute_reply.started":"2026-05-09T23:28:40.112708Z","shell.execute_reply":"2026-05-09T23:28:40.296034Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# -------------------------\n# Post-processing constants\n# -------------------------\n# MAX_CLOVER = 71.7865\n# MAX_DEAD = 83.8407\n# MAX_GREEN = 157.9836\n\n# predictions = []\n\n# model.eval()\n\n# with torch.no_grad():\n\n#     for left, right, sample_ids in test_loader:\n\n#         left = left.to(DEVICE)\n#         right = right.to(DEVICE)\n\n#         preds = model(left, right).cpu().numpy()\n\n# #         # ----------------------------------------\n# #         # POST-PROCESSING\n# #         # ----------------------------------------\n#         for i, sid in enumerate(sample_ids):\n\n#             sid = str(sid).strip()\n\n            # unpack predictions\n            # clover = preds[i][0]\n            # dead = preds[i][1]\n            # green = preds[i][2]\n            # total = preds[i][3]\n            # gdm = preds[i][4]\n\n            # # -------------------------\n            # # Clip predictions\n            # # -------------------------\n            # clover = np.clip(clover, 0, MAX_CLOVER)\n            # dead = np.clip(dead, 0, MAX_DEAD)\n            # green = np.clip(green, 0, MAX_GREEN)\n\n            # # Optional:\n            # total = max(total, 0)\n            # gdm = max(gdm, 0)\n\n\n            # predictions.append(\n            #     (\n            #         sid,\n            #         [clover, dead, green, total, gdm]\n            #     )\n            # )","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-09T23:28:43.564504Z","iopub.execute_input":"2026-05-09T23:28:43.565211Z","iopub.status.idle":"2026-05-09T23:28:45.654890Z","shell.execute_reply.started":"2026-05-09T23:28:43.565180Z","shell.execute_reply":"2026-05-09T23:28:45.654307Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# rows = []\n\n# target_names = [\n#     \"Dry_Clover_g\",\n#     \"Dry_Dead_g\",\n#     \"Dry_Green_g\",\n#     \"Dry_Total_g\",\n#     \"GDM_g\"\n# ]\n\n# for sid, pred in predictions:\n\n#     for t, value in zip(target_names, pred):\n\n#         rows.append({\n#             \"sample_id\": f\"{sid}__{t}\",\n#             \"target\": float(value)\n#         })\n\n# submission = pd.DataFrame(rows)\n\n# submission.to_csv(\"submission.csv\", index=False)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-09T23:29:20.528340Z","iopub.execute_input":"2026-05-09T23:29:20.529061Z","iopub.status.idle":"2026-05-09T23:29:20.535238Z","shell.execute_reply.started":"2026-05-09T23:29:20.529029Z","shell.execute_reply":"2026-05-09T23:29:20.534456Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"test_df = pd.read_csv(TEST_CSV)\n\n# Extract base ID\ntest_df[\"base_id\"] = test_df[\"sample_id\"].apply(lambda x: x.split(\"__\")[0])\n\n# Keep ONE row per image (cleanly)\ntest_df_unique = (\n    test_df.groupby(\"base_id\", as_index=False)\n    .agg({\n        \"image_path\": \"first\"\n    })\n)\n\n# Rename correctly\ntest_df_unique = test_df_unique.rename(columns={\"base_id\": \"sample_id\"})\n\n\n\ntest_dataset = BiomassDataset(test_df_unique, DATA_DIR, transform, is_test=True)\n\nsample = test_dataset[0]\nprint(len(sample))\nprint(type(sample[2]))\n\ntest_loader = DataLoader(test_dataset, batch_size=BATCH_SIZE, shuffle=False)\n\nmodel.eval()\n\npredictions = []\n\nwith torch.no_grad():\n    for left, right, sample_ids in test_loader:\n        left, right = left.to(DEVICE), right.to(DEVICE)\n\n        preds = model(left, right).cpu().numpy()\n\n        for sid, pred in zip(sample_ids, preds):\n            predictions.append((sid, pred))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-07T21:04:11.810881Z","iopub.execute_input":"2026-05-07T21:04:11.811134Z","iopub.status.idle":"2026-05-07T21:04:14.349230Z","shell.execute_reply.started":"2026-05-07T21:04:11.811110Z","shell.execute_reply":"2026-05-07T21:04:14.348343Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"rows = []\n\ntarget_names = [\n    \"Dry_Clover_g\",\n    \"Dry_Dead_g\",\n    \"Dry_Green_g\",\n    \"Dry_Total_g\",\n    \"GDM_g\"\n]\n\nfor sid, pred in predictions:\n    sid = str(sid).strip()  # extra safety\n\n    for t, value in zip(target_names, pred):\n        rows.append({\n            \"sample_id\": f\"{sid}__{t}\",\n            \"target\": float(value)\n        })\n\nsubmission = pd.DataFrame(rows)\nsubmission.to_csv(\"submission.csv\", index=False)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-07T21:04:14.350260Z","iopub.execute_input":"2026-05-07T21:04:14.350631Z","iopub.status.idle":"2026-05-07T21:04:14.363650Z","shell.execute_reply.started":"2026-05-07T21:04:14.350604Z","shell.execute_reply":"2026-05-07T21:04:14.362742Z"}},"outputs":[],"execution_count":null}]}