{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.13","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"},{"sourceId":13708417,"sourceType":"datasetVersion","datasetId":8720648},{"sourceId":13730523,"sourceType":"datasetVersion","datasetId":8735832}],"dockerImageVersionId":31192,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import os\nimport torch\nimport timm\nimport numpy as np\nimport pandas as pd\nfrom torch.utils.data import Dataset, DataLoader\nfrom torchvision import transforms\nfrom PIL import Image\nimport json\n\ndevice = \"cuda\" if torch.cuda.is_available() else \"cpu\"\nprint(\"Running on:\", device)\n\nwith open(\"/kaggle/input/teacher-student-ensemble/meta_cols.json\", \"r\") as f:\n    meta_cols = json.load(f)\n\nmeta_dim = len(meta_cols)\nprint(f\"Total Metal cols:{meta_dim}\")\n# =========================================================\n# Model Definitions (same as training, but pretrained=False)\n# =========================================================\n\nclass TeacherImgMeta(torch.nn.Module):\n    def __init__(self, backbone=\"tf_efficientnet_b4_ns\", meta_dim=64, out_dim=5):\n        super().__init__()\n        self.backbone = timm.create_model(backbone, pretrained=False, num_classes=0)\n        in_features = self.backbone.num_features\n\n        self.meta = torch.nn.Sequential(\n            torch.nn.Linear(meta_dim,128), torch.nn.ReLU(),\n            torch.nn.LayerNorm(128),\n            torch.nn.Linear(128,128), torch.nn.ReLU()\n        )\n\n        self.head = torch.nn.Sequential(\n            torch.nn.Linear(in_features+128,512), torch.nn.ReLU(),\n            torch.nn.Linear(512,out_dim)\n        )\n\n        self.softplus = torch.nn.Softplus()\n\n    def forward(self,img,meta):\n        x = self.backbone(img)\n        m = self.meta(meta)\n        return self.softplus(self.head(torch.cat([x,m],dim=1)))\n\nclass StudentImgOnly(torch.nn.Module):\n    def __init__(self, backbone=\"tf_efficientnet_b4_ns\", out_dim=5):\n        super().__init__()\n        self.backbone = timm.create_model(backbone, pretrained=False, num_classes=0)\n        in_features = self.backbone.num_features\n\n        self.head = torch.nn.Sequential(\n            torch.nn.Linear(in_features,512), torch.nn.ReLU(),\n            torch.nn.Linear(512,out_dim)\n        )\n        self.softplus = torch.nn.Softplus()\n\n    def forward(self,img):\n        x = self.backbone(img)\n        return self.softplus(self.head(x))\n\n# =========================================================\n# Load Saved Weights (uploaded Dataset)\n# =========================================================\n\nteacher = TeacherImgMeta(meta_dim=meta_dim).to(device)\nstudent = StudentImgOnly().to(device)\n\nteacher.load_state_dict(torch.load(\"/kaggle/input/teacher-student-ensemble/best_teacher.pth\", map_location=device))\nstudent.load_state_dict(torch.load(\"/kaggle/input/teacher-student-ensemble/best_student.pth\", map_location=device))\n\nteacher.eval()\nstudent.eval()\n\nprint(\"Models loaded successfully.\")\n\n# =========================================================\n# Prepare Test Data\n# =========================================================\n\ntest_dir = \"/kaggle/input/csiro-biomass/test/\"\ntest_files = sorted([f for f in os.listdir(test_dir) if f.endswith(\".jpg\")])\n\ntest_df = pd.DataFrame({\n    \"image_path\": [os.path.join(test_dir,f) for f in test_files],\n    \"image_id\": [os.path.splitext(f)[0] for f in test_files]\n})\n\ntfms = transforms.Compose([\n    transforms.Resize((384,384)),\n    transforms.ToTensor()\n])\n\nclass TestDataset(Dataset):\n    def __init__(self, df, transform):\n        self.df = df\n        self.tfm = transform\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        r = self.df.iloc[idx]\n        img = Image.open(r.image_path).convert(\"RGB\")\n        img = self.tfm(img)\n        return img, r.image_id\n\ndl = DataLoader(TestDataset(test_df, tfms), batch_size=16, shuffle=False)\n\n# =========================================================\n# Ensemble Inference\n# =========================================================\nalpha = 0.6   # student's weight\ntargets = [\"Dry_Green_g\",\"Dry_Dead_g\",\"Dry_Clover_g\",\"GDM_g\",\"Dry_Total_g\"]\n\nrows = []\n\nwith torch.no_grad():\n    for imgs, ids in dl:\n        imgs = imgs.to(device)\n        meta_zero = torch.zeros(imgs.size(0), meta_dim).to(device)\n\n        t_pred = teacher(imgs, meta_zero)\n        s_pred = student(imgs)\n\n        pred = alpha*s_pred + (1-alpha)*t_pred\n        pred = pred.cpu().numpy()\n\n        for img_id, p in zip(ids, pred):\n            for t, val in zip(targets, p):\n                rows.append({\n                    \"sample_id\": f\"{img_id}__{t}\",\n                    \"target\": float(np.expm1(val))  # reverse log1p\n                })\n\nsub = pd.DataFrame(rows)\nsub.to_csv(\"submission.csv\", index=False)\n\nprint(\"submission.csv created.\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-14T10:33:23.196700Z","iopub.execute_input":"2025-11-14T10:33:23.197149Z","iopub.status.idle":"2025-11-14T10:33:37.171376Z","shell.execute_reply.started":"2025-11-14T10:33:23.197126Z","shell.execute_reply":"2025-11-14T10:33:37.170735Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"sub.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-14T10:33:41.042297Z","iopub.execute_input":"2025-11-14T10:33:41.042872Z","iopub.status.idle":"2025-11-14T10:33:41.061818Z","shell.execute_reply.started":"2025-11-14T10:33:41.042850Z","shell.execute_reply":"2025-11-14T10:33:41.061013Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# import pandas as pd\n# import torch\n# import timm\n# from PIL import Image\n# from torchvision import transforms\n\n# # load sample submission\n# sub = pd.read_csv(\"/kaggle/input/csiro-biomass/sample_submission.csv\")\n\n# # extract image IDs\n# sub[\"image_id\"] = sub[\"sample_id\"].str.split(\"__\").str[0]\n# sub[\"target_name\"] = sub[\"sample_id\"].str.split(\"__\").str[1]\n\n# # load models\n# teacher = TeacherImgMeta(...)\n# teacher.load_state_dict(torch.load(\"/kaggle/input/your-model-weights/best_teacher.pth\"))\n\n# student = StudentImgOnly(...)\n# student.load_state_dict(torch.load(\"/kaggle/input/your-model-weights/best_student.pth\"))\n\n# teacher.eval(); student.eval()\n\n# test_tfms = transforms.Compose([\n#     transforms.Resize((384,384)),\n#     transforms.ToTensor()\n# ])\n\n# preds = []\n\n# for idx, row in sub.iterrows():\n#     img_path = f\"/kaggle/input/csiro-biomass/test-images/{row['image_id']}.jpg\"\n#     img = Image.open(img_path).convert(\"RGB\")\n#     img = test_tfms(img).unsqueeze(0).cuda()\n\n#     meta_zero = torch.zeros(1, len(meta_cols)).cuda()\n\n#     t_pred = teacher(img, meta_zero)\n#     s_pred = student(img)\n\n#     blended = 0.6 * s_pred + 0.4 * t_pred\n#     blended = blended.cpu().numpy()[0]\n\n#     # map target name to correct index\n#     target_idx = targets.index(row[\"target_name\"])\n\n#     preds.append(np.expm1(blended[target_idx]))\n\n# sub[\"target\"] = preds\n# sub[[\"sample_id\",\"target\"]].to_csv(\"submission.csv\", index=False)\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# # =========================================================\n# # 0. Imports\n# # =========================================================\n# import os\n# import glob\n# import numpy as np\n# import pandas as pd\n\n# import torch\n# import torch.nn as nn\n# from torch.utils.data import Dataset, DataLoader\n\n# from sklearn.model_selection import GroupKFold\n# from sklearn.preprocessing import StandardScaler\n\n# from PIL import Image\n# import timm\n# from torchvision import transforms\n\n# # =========================================================\n# # 1. Load train_fe.csv (long format: one row per (image, target_name))\n# # =========================================================\n# train_csv_path = \"/kaggle/input/biomass-data/train_fe.csv\"\n# df_long = pd.read_csv(train_csv_path)\n# print(\"Loaded long train:\", df_long.shape)\n\n# # =========================================================\n# # 2. Pivot long -> wide (5 target columns)\n# #    index = image_path, columns = target_name, values = target\n# # =========================================================\n# df_targets = df_long.pivot_table(\n#     index=\"image_path\",\n#     columns=\"target_name\",\n#     values=\"target\",\n#     aggfunc=\"first\"\n# ).reset_index()\n\n# print(\"Wide target table:\", df_targets.shape)\n\n# # =========================================================\n# # 3. Extract metadata (first row per image_path)\n# # =========================================================\n# meta_cols_all = [\n#     c for c in df_long.columns\n#     if c not in [\"target_name\", \"target\"]\n# ]\n\n# df_meta = df_long[meta_cols_all].groupby(\"image_path\").first().reset_index()\n# print(\"Meta table:\", df_meta.shape)\n\n# # =========================================================\n# # 4. Merge metadata + wide targets\n# # =========================================================\n# df = df_meta.merge(df_targets, on=\"image_path\", how=\"inner\")\n# print(\"Merged df:\", df.shape)\n\n# # =========================================================\n# # 5. image_id and log targets\n# # =========================================================\n# df[\"image_id\"] = df[\"image_path\"].apply(\n#     lambda x: os.path.splitext(os.path.basename(str(x)))[0]\n# )\n\n# targets = [\"Dry_Green_g\", \"Dry_Dead_g\", \"Dry_Clover_g\", \"GDM_g\", \"Dry_Total_g\"]\n# for t in targets:\n#     df[f\"{t}_log\"] = np.log1p(df[t])\n\n# print(\"Log targets added. Columns now:\", df.columns.tolist())\n\n# # =========================================================\n# # 6. Drop columns not used as metadata features\n# # =========================================================\n# drop_cols = [\n#     \"sample_id\", \"Sampling_Date\", \"Species\",\n#     \"target_name\", \"target\", \"target_log\",\n#     \"Dry_Green_g\", \"Dry_Dead_g\", \"Dry_Clover_g\", \"GDM_g\", \"Dry_Total_g\",\n#     \"Month\", \"Year\", \"NDVI_sq\"\n# ]\n\n# df = df.drop(columns=drop_cols, errors=\"ignore\")\n# print(\"After drops:\", df.shape)\n\n# # =========================================================\n# # 7. Build metadata feature list: numeric + one-hot categorical\n# # =========================================================\n# num_cols = [\n#     \"Pre_GSHH_NDVI\",\"Height_Ave_cm\",\"month_sin\",\"month_cos\",\"NDVI_x_Height\",\n#     \"n_species\",\"species_richness\",\"frac_grass\",\"frac_legume\",\"frac_weeds\",\n#     \"is_monoculture\",\"shannon_diversity\"\n# ]\n\n# cat_cols = [\n#     c for c in df.columns\n#     if c.startswith((\"State_\", \"dom_\", \"top_\"))\n# ]\n\n# meta_cols = num_cols + cat_cols\n# META_DIM = len(meta_cols)\n# print(\"Total metadata features:\", META_DIM)\n\n# # Ensure numeric dtype for all meta cols\n# for c in meta_cols:\n#     df[c] = pd.to_numeric(df[c], errors=\"coerce\").fillna(0)\n\n# # Scale numeric features\n# scaler = StandardScaler()\n# df[num_cols] = scaler.fit_transform(df[num_cols])\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\n# =========================================================\n# 8. Split using GroupKFold (keep all targets of same image together)\n# =========================================================\ngroups = df[\"image_id\"]\ngkf = GroupKFold(n_splits=5)\ntrain_idx, val_idx = list(gkf.split(df, groups=groups))[0]\n\ndf_train = df.iloc[train_idx].reset_index(drop=True)\ndf_val   = df.iloc[val_idx].reset_index(drop=True)\n\nprint(\"Train:\", df_train.shape, \"Val:\", df_val.shape)\n\n# =========================================================\n# 9. Dataset classes\n# =========================================================\nclass BiomassDataset(Dataset):\n    def __init__(self, df, meta_cols, img_root, transform=None):\n        self.df = df\n        self.meta_cols = meta_cols\n        self.img_root = img_root\n        self.transform = transform\n        self.targets = [\n            \"Dry_Green_g_log\",\"Dry_Dead_g_log\",\"Dry_Clover_g_log\",\n            \"GDM_g_log\",\"Dry_Total_g_log\"\n        ]\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        sample = self.df.iloc[idx]\n        img_path = os.path.join(self.img_root, sample[\"image_path\"])\n\n        try:\n            img = Image.open(img_path).convert(\"RGB\")\n        except:\n            # fallback: black image if something is wrong\n            img = Image.new(\"RGB\", (384,384), (0,0,0))\n\n        if self.transform:\n            img = self.transform(img)\n\n        meta = torch.tensor(sample[self.meta_cols].to_numpy(dtype=np.float32))\n        y = torch.tensor(sample[self.targets].to_numpy(dtype=np.float32))\n\n        return img, meta, y, sample[\"image_id\"]\n\nclass TestImageDataset(Dataset):\n    def __init__(self, df, transform=None):\n        self.df = df\n        self.transform = transform\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n\n        try:\n            img = Image.open(row[\"image_path\"]).convert(\"RGB\")\n        except:\n            img = Image.new(\"RGB\", (384,384), (0,0,0))\n\n        if self.transform:\n            img = self.transform(img)\n\n        return img, row[\"image_id\"]\n\n# =========================================================\n# 10. Transforms and DataLoaders\n# =========================================================\ntrain_tfms = transforms.Compose([\n    transforms.Resize((384,384)),\n    transforms.RandomHorizontalFlip(),\n    transforms.RandomVerticalFlip(),\n    transforms.ColorJitter(0.2,0.2,0.2,0.1),\n    transforms.ToTensor()\n])\n\nval_tfms = transforms.Compose([\n    transforms.Resize((384,384)),\n    transforms.ToTensor()\n])\n\nimg_root = \"/kaggle/input/csiro-biomass/\"\n\ntrain_ds = BiomassDataset(df_train, meta_cols, img_root, train_tfms)\nval_ds   = BiomassDataset(df_val,   meta_cols, img_root, val_tfms)\n\ntrain_dl = DataLoader(train_ds, batch_size=8, shuffle=True, num_workers=4)\nval_dl   = DataLoader(val_ds, batch_size=8, shuffle=False, num_workers=4)\n\n# =========================================================\n# 11. Models: Teacher (img+meta), Student (img-only)\n# =========================================================\nclass TeacherImgMeta(nn.Module):\n    def __init__(self, backbone=\"tf_efficientnet_b4_ns\", meta_dim=64, out_dim=5):\n        super().__init__()\n        self.backbone = timm.create_model(backbone, pretrained=True, num_classes=0)\n        in_features = self.backbone.num_features\n\n        self.meta = nn.Sequential(\n            nn.Linear(meta_dim, 128), nn.ReLU(),\n            nn.LayerNorm(128),\n            nn.Linear(128, 128), nn.ReLU()\n        )\n\n        self.head = nn.Sequential(\n            nn.Linear(in_features + 128, 512), nn.ReLU(),\n            nn.Linear(512, out_dim)\n        )\n\n        self.softplus = nn.Softplus()\n\n    def forward(self, img, meta):\n        x = self.backbone(img)\n        m = self.meta(meta)\n        h = torch.cat([x, m], dim=1)\n        return self.softplus(self.head(h))\n\nclass StudentImgOnly(nn.Module):\n    def __init__(self, backbone=\"tf_efficientnet_b4_ns\", out_dim=5):\n        super().__init__()\n        self.backbone = timm.create_model(backbone, pretrained=True, num_classes=0)\n        in_features = self.backbone.num_features\n\n        self.head = nn.Sequential(\n            nn.Linear(in_features, 512), nn.ReLU(),\n            nn.Linear(512, out_dim)\n        )\n        self.softplus = nn.Softplus()\n\n    def forward(self, img):\n        x = self.backbone(img)\n        return self.softplus(self.head(x))\n\n# =========================================================\n# 12. Train Teacher (30 epochs, early stopping, save best)\n# =========================================================\ndevice = \"cuda\" if torch.cuda.is_available() else \"cpu\"\nteacher = TeacherImgMeta(meta_dim=META_DIM).to(device)\nopt = torch.optim.AdamW(teacher.parameters(), lr=1e-4)\n\ntarget_weights = np.array([0.1,0.1,0.1,0.2,0.5], dtype=np.float64)\ntarget_weights = target_weights / target_weights.sum()\nw_torch = torch.tensor(target_weights, device=device)\n\nEPOCHS = 30\npatience = 5\nbest_r2 = -np.inf\npatience_counter = 0\n\nfor epoch in range(EPOCHS):\n    teacher.train()\n    train_loss = 0.0\n\n    for imgs, meta, y, _ in train_dl:\n        imgs, meta, y = imgs.to(device), meta.to(device), y.to(device)\n\n        preds = teacher(imgs, meta)\n        loss = ((preds - y)**2 * w_torch).mean()\n\n        opt.zero_grad()\n        loss.backward()\n        opt.step()\n\n        train_loss += loss.item()\n\n    # validation\n    teacher.eval()\n    preds_list, y_list = [], []\n\n    with torch.no_grad():\n        for imgs, meta, y, _ in val_dl:\n            imgs, meta = imgs.to(device), meta.to(device)\n            preds = teacher(imgs, meta)\n            preds_list.append(preds.cpu().numpy().astype(np.float64))\n            y_list.append(y.numpy().astype(np.float64))\n\n    yp = np.concatenate(preds_list, axis=0).reshape(-1, 5)\n    yt = np.concatenate(y_list,   axis=0).reshape(-1, 5)\n\n    w = target_weights.reshape(1, -1)\n    W = np.broadcast_to(w, yt.shape)\n\n    mu = np.sum(W * yt) / np.sum(W)\n    rss = np.sum(W * (yt - yp)**2)\n    tss = np.sum(W * (yt - mu)**2)\n    r2 = 1.0 - rss / tss\n\n    print(f\"[Teacher] Epoch {epoch+1:02d} | \"\n          f\"Train Loss {train_loss/len(train_dl):.4f} | Val R² {r2:.5f}\")\n\n    if r2 > best_r2:\n        best_r2 = r2\n        patience_counter = 0\n        torch.save(teacher.state_dict(), \"/kaggle/working/best_teacher.pth\")\n        print(f\"   ✔ New best Teacher R²={best_r2:.5f} (saved)\")\n    else:\n        patience_counter += 1\n        print(f\"   Patience {patience_counter}/{patience}\")\n        if patience_counter >= patience:\n            print(\"   ⛔ Early stopping Teacher.\")\n            break\n\nprint(f\"\\nBest Teacher Val R² = {best_r2:.5f}\")\n\n# =========================================================\n# 13. Student Distillation Training\n# =========================================================\n# reload best teacher (for safety)\nteacher = TeacherImgMeta(meta_dim=META_DIM).to(device)\nteacher.load_state_dict(torch.load(\"/kaggle/working/best_teacher.pth\", map_location=device))\nteacher.eval()\nfor p in teacher.parameters():\n    p.requires_grad = False\n\nstudent = StudentImgOnly().to(device)\nstudent_opt = torch.optim.AdamW(student.parameters(), lr=1e-4)\n\nEPOCHS = 30\npatience = 5\nbest_r2 = -np.inf\npatience_counter = 0\n\nfor epoch in range(EPOCHS):\n    student.train()\n    epoch_loss = 0.0\n\n    for imgs, meta, y, _ in train_dl:\n        imgs, meta, y = imgs.to(device), meta.to(device), y.to(device)\n\n        with torch.no_grad():\n            teacher_pred = teacher(imgs, meta)\n\n        student_pred = student(imgs)\n\n        distill_loss = ((student_pred - teacher_pred)**2 * w_torch).mean()\n        target_loss  = ((student_pred - y)**2 * w_torch).mean()\n        loss = 0.7 * distill_loss + 0.3 * target_loss\n\n        student_opt.zero_grad()\n        loss.backward()\n        student_opt.step()\n\n        epoch_loss += loss.item()\n\n    # validation\n    student.eval()\n    preds_list, y_list = [], []\n\n    with torch.no_grad():\n        for imgs, meta, y, _ in val_dl:\n            imgs = imgs.to(device)\n            student_pred = student(imgs).cpu().numpy().astype(np.float64)\n            preds_list.append(student_pred)\n            y_list.append(y.numpy().astype(np.float64))\n\n    yp = np.concatenate(preds_list, axis=0).reshape(-1, 5)\n    yt = np.concatenate(y_list,   axis=0).reshape(-1, 5)\n\n    w = target_weights.reshape(1, -1)\n    W = np.broadcast_to(w, yt.shape)\n\n    mu = np.sum(W * yt) / np.sum(W)\n    rss = np.sum(W * (yt - yp)**2)\n    tss = np.sum(W * (yt - mu)**2)\n    r2 = 1.0 - rss / tss\n\n    print(f\"[Student] Epoch {epoch+1:02d} | \"\n          f\"Loss {epoch_loss/len(train_dl):.4f} | Val R² {r2:.5f}\")\n\n    if r2 > best_r2:\n        best_r2 = r2\n        patience_counter = 0\n        torch.save(student.state_dict(), \"/kaggle/working/best_student.pth\")\n        print(f\"   ✔ New best Student R²={best_r2:.5f} (saved)\")\n    else:\n        patience_counter += 1\n        print(f\"   Patience {patience_counter}/{patience}\")\n        if patience_counter >= patience:\n            print(\"   ⛔ Early stopping Student.\")\n            break\n\nprint(f\"\\nBest Student Val R² = {best_r2:.5f}\")\n\n# =========================================================\n# 14. Ensemble Validation Check (optional sanity)\n# =========================================================\nteacher = TeacherImgMeta(meta_dim=META_DIM).to(device)\nteacher.load_state_dict(torch.load(\"/kaggle/working/best_teacher.pth\", map_location=device))\nteacher.eval()\n\nstudent = StudentImgOnly().to(device)\nstudent.load_state_dict(torch.load(\"/kaggle/working/best_student.pth\", map_location=device))\nstudent.eval()\n\nalpha = 0.6  # weight on student\n\npreds_list, y_list = [], []\nwith torch.no_grad():\n    for imgs, meta, y, _ in val_dl:\n        imgs, meta = imgs.to(device), meta.to(device)\n        tp = teacher(imgs, meta)\n        sp = student(imgs)\n        blended = alpha * sp + (1 - alpha) * tp\n        preds_list.append(blended.cpu().numpy().astype(np.float64))\n        y_list.append(y.numpy().astype(np.float64))\n\nyp = np.concatenate(preds_list, axis=0).reshape(-1,5)\nyt = np.concatenate(y_list,   axis=0).reshape(-1,5)\nw = target_weights.reshape(1,-1)\nW = np.broadcast_to(w, yt.shape)\n\nmu = np.sum(W * yt) / np.sum(W)\nrss = np.sum(W * (yt - yp)**2)\ntss = np.sum(W * (yt - mu)**2)\nr2_ens = 1.0 - rss / tss\nprint(\"Ensemble Validation R²:\", r2_ens)\n\n# =========================================================\n# 15. Test set: load images, run ensemble, make submission\n# =========================================================\ntest_dir = \"/kaggle/input/csiro-biomass/test/\"\ntest_files = sorted(glob.glob(os.path.join(test_dir, \"*.jpg\")))\nprint(\"Found test images:\", len(test_files))\n\ntest_df = pd.DataFrame({\n    \"image_path\": test_files,\n    \"image_id\": [os.path.splitext(os.path.basename(f))[0] for f in test_files]\n})\n\ntest_tfms = transforms.Compose([\n    transforms.Resize((384,384)),\n    transforms.ToTensor()\n])\n\ntest_ds = TestImageDataset(test_df, transform=test_tfms)\ntest_dl = DataLoader(test_ds, batch_size=8, shuffle=False, num_workers=4)\n\nteacher.eval()\nstudent.eval()\n\ntest_preds = []\nwith torch.no_grad():\n    for imgs, img_ids in test_dl:\n        imgs = imgs.to(device)\n        # fake metadata as zeros (same dim as training)\n        meta_zero = torch.zeros(imgs.size(0), META_DIM, device=device)\n\n        tp = teacher(imgs, meta_zero)\n        sp = student(imgs)\n\n        blended = alpha * sp + (1 - alpha) * tp\n        blended = blended.cpu().numpy()\n\n        for i, img_id in enumerate(img_ids):\n            test_preds.append((img_id, blended[i]))\n\n# build submission in original target space\nrows = []\ntargets_log = [\n    \"Dry_Green_g_log\",\"Dry_Dead_g_log\",\"Dry_Clover_g_log\",\n    \"GDM_g_log\",\"Dry_Total_g_log\"\n]\ntarget_names = [t.replace(\"_log\",\"\") for t in targets_log]\n\nfor img_id, pred in test_preds:\n    for t_name, val in zip(target_names, pred):\n        rows.append({\n            \"sample_id\": f\"{img_id}__{t_name}\",\n            \"target\": float(np.expm1(val))  # inverse log1p\n        })\n\nsub = pd.DataFrame(rows)\nsub.to_csv(\"/kaggle/working/submission.csv\", index=False)\nprint(sub.head())\nprint(\"Saved submission.csv and model weights in /kaggle/working\")\n\n#!ls -lah /kaggle/working/\n","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-11-13T17:29:00.325782Z","iopub.execute_input":"2025-11-13T17:29:00.326421Z","iopub.status.idle":"2025-11-13T17:40:06.879285Z","shell.execute_reply.started":"2025-11-13T17:29:00.326399Z","shell.execute_reply":"2025-11-13T17:40:06.878062Z"}},"outputs":[],"execution_count":null}]}