{"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":"none","dataSources":[{"sourceType":"competition","sourceId":112509,"databundleVersionId":14254895},{"sourceType":"datasetVersion","sourceId":13617670,"datasetId":8654131,"databundleVersionId":14350959}],"dockerImageVersionId":31236,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import pandas as pd\nimport os\n\n# ================= Configuration =================\nclass CFG:\n    # 这里改为读取 test.csv (Kaggle 比赛的标准输入)\n    TEST_CSV_PATH = \"/kaggle/input/csiro-biomass/test.csv\"\n    OUTPUT_CSV = \"submission.csv\"\n\n# ================= Probe Values =================\n# 基于训练集均值 (Dry_Clover_g 强制为 0)\nPROBE_MAP = {\n    \"Dry_Green_g\": 20.8000,\n    \"Dry_Dead_g\":  7.9809,\n    \"GDM_g\":       27.1081,\n    \"Dry_Total_g\": 40.3000,\n    \"Dry_Clover_g\": 1.4235      # <--- 探测核心：强制置 0\n}\n\ndef run_probe_from_test_csv():\n    print(\"启动探测程序 (From test.csv)...\")\n    \n    # 1. 读取 test.csv\n    # 结构通常包含: image_path, target_name (可能还有 sample_id，但我们自己拼)\n    if not os.path.exists(CFG.TEST_CSV_PATH):\n        # 本地测试用的容错逻辑，实际比赛中路径肯定是存在的\n        print(f\"警告：未找到 {CFG.TEST_CSV_PATH}，尝试读取当前目录下的 test.csv\")\n        test_path = \"test.csv\"\n    else:\n        test_path = CFG.TEST_CSV_PATH\n        \n    df = pd.read_csv(test_path)\n    print(f\"读取 test.csv 成功，共 {len(df)} 行数据\")\n\n    # 2. 核心逻辑：手动拼接 ID 并填充\n    # 逻辑：从 image_path 提取文件名(去掉路径和后缀) + \"__\" + target_name\n    \n    sample_ids = []\n    targets = []\n    \n    for _, row in df.iterrows():\n        # A. 提取 Image ID\n        # 假设 path 是 \"test/ID1001187975.jpg\" -> 提取 \"ID1001187975\"\n        path = str(row['image_path'])\n        filename = os.path.basename(path)       # -> \"ID1001187975.jpg\"\n        image_id = os.path.splitext(filename)[0] # -> \"ID1001187975\"\n        \n        # B. 获取 Target Name\n        t_name = row['target_name']\n        \n        # C. 拼接 sample_id\n        # 标准格式: IDxxxxxxxxx__TargetName\n        s_id = f\"{image_id}__{t_name}\"\n        sample_ids.append(s_id)\n        \n        # D. 填充数值\n        # 如果是 Clover 填 0，否则填均值\n        val = PROBE_MAP.get(t_name, 0.0)\n        targets.append(val)\n\n    # 3. 构建提交 DataFrame\n    submission = pd.DataFrame({\n        \"sample_id\": sample_ids,\n        \"target\": targets\n    })\n\n    # 4. 保存\n    submission.to_csv(CFG.OUTPUT_CSV, index=False)\n    print(f\"探测文件已生成: {CFG.OUTPUT_CSV}\")\n    \n    # 预览检查\n    print(\"\\n--- 预览 (Check Clover = 0) ---\")\n    print(submission[submission['sample_id'].str.contains(\"Dry_Clover_g\")].head(3))\n    print(\"\\n--- 预览 (Check Others = Mean) ---\")\n    print(submission[~submission['sample_id'].str.contains(\"Dry_Clover_g\")].head(3))\n\nif __name__ == \"__main__\":\n    run_probe_from_test_csv()","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-12-28T12:35:24.847014Z","iopub.execute_input":"2025-12-28T12:35:24.847324Z","iopub.status.idle":"2025-12-28T12:35:26.205443Z","shell.execute_reply.started":"2025-12-28T12:35:24.847294Z","shell.execute_reply":"2025-12-28T12:35:26.204464Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# import pandas as pd\n# import os\n\n# # ================= Configuration =================\n# TEST_CSV_PATH = \"/kaggle/input/csiro-biomass/test.csv\"       # 你的测试集路径\n# OUTPUT_CSV = \"submission.csv\"\n\n# # 训练集计算出的精确全局加权均值\n# # Global Weighted Mean = sum(w * y) / sum(w)\n# GLOBAL_MEAN_VAL = 33.8458\n\n# def make_calibration_submission():\n#     print(f\"启动零点校准探测，填充值: {GLOBAL_MEAN_VAL}\")\n    \n#     # 1. 读取 test.csv 获取 ID\n#     if not os.path.exists(TEST_CSV_PATH):\n#         print(\"未找到 test.csv，请检查路径。\")\n#         return\n\n#     df = pd.read_csv(TEST_CSV_PATH)\n    \n#     # 2. 手动拼接 sample_id (稳健做法)\n#     # 格式: IDxxxxxxxxx__TargetName\n#     sample_ids = []\n    \n#     for _, row in df.iterrows():\n#         path = str(row['image_path'])\n#         # 提取文件名 ID (兼容 windows/linux 路径)\n#         filename = os.path.basename(path)\n#         image_id = os.path.splitext(filename)[0]\n        \n#         t_name = row['target_name']\n#         s_id = f\"{image_id}__{t_name}\"\n#         sample_ids.append(s_id)\n        \n#     # 3. 生成全常数预测\n#     submission = pd.DataFrame({\n#         \"sample_id\": sample_ids,\n#         \"target\": GLOBAL_MEAN_VAL  # 全员 33.8458\n#     })\n    \n#     # 4. 保存\n#     submission.to_csv(OUTPUT_CSV, index=False)\n#     print(f\"校准文件已生成: {OUTPUT_CSV}\")\n#     print(\"前 5 行预览:\")\n#     print(submission.head())\n\n# if __name__ == \"__main__\":\n#     make_calibration_submission()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-28T12:35:26.207192Z","iopub.execute_input":"2025-12-28T12:35:26.207574Z","iopub.status.idle":"2025-12-28T12:35:26.212366Z","shell.execute_reply.started":"2025-12-28T12:35:26.207546Z","shell.execute_reply":"2025-12-28T12:35:26.211473Z"},"jupyter":{"source_hidden":true}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# import os\n# import math\n# import warnings\n# import numpy as np\n# import pandas as pd\n# from dataclasses import dataclass\n# from tqdm import tqdm\n# import torch\n# import torch.nn as nn\n# import torch.nn.functional as F\n# from torch.utils.data import Dataset, DataLoader\n# import cv2\n# import albumentations as A\n# from albumentations.pytorch import ToTensorV2\n# import timm\n\n# # 忽略警告\n# warnings.filterwarnings('ignore')\n\n# # ==========================================\n# # 1. 配置与路径 (Configuration)\n# # ==========================================\n# @dataclass\n# class CFG:\n#     # --- [请修改路径] ---\n#     TEST_CSV = \"/kaggle/input/csiro-biomass/test.csv\"  \n#     IMAGE_ROOT = \"/kaggle/input/csiro-biomass\"        \n#     MODEL_DIR = \"/kaggle/input/try-model/pytorch/default/3\" \n#     OUTPUT_CSV = \"./submission.csv\"\n    \n#     # --- 模型设置 (需与训练一致) ---\n#     img_size: int = 384\n#     batch_size: int = 32\n#     num_workers: int = 4\n    \n#     backbone: str = \"convnext_tiny.fb_in22k_ft_in1k\"\n    \n#     n_folds: int = 4\n#     n_targets: int = 5            \n    \n#     target_order = [\"Dry_Green_g\", \"Dry_Dead_g\", \"Dry_Clover_g\", \"GDM_g\", \"Dry_Total_g\"]\n    \n#     # TTA: \"flip4\" (原图+3种翻转) 或 \"d4\" (8种变换) 或 \"none\"\n#     tta_mode: str = \"flip4\"       \n\n#     # 后处理：物理一致性融合\n#     enforce_consistency_infer: bool = True\n#     enforce_consistency_infer_gdm: bool = True \n\n# def device():\n#     return torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n\n# # ==========================================\n# # 2. 数据处理 (Data Processing)\n# # ==========================================\n# IMAGENET_MEAN = (0.485, 0.456, 0.406)\n# IMAGENET_STD  = (0.229, 0.224, 0.225)\n\n# def get_valid_transform(img_size: int):\n#     \"\"\"推理时的 Transform：Resize, Pad, Normalize\"\"\"\n#     return A.Compose([\n#         A.LongestMaxSize(max_size=img_size, interpolation=cv2.INTER_AREA),\n#         A.PadIfNeeded(img_size, img_size, border_mode=cv2.BORDER_REFLECT_101),\n#         A.Normalize(mean=IMAGENET_MEAN, std=IMAGENET_STD),\n#         ToTensorV2(),\n#     ])\n\n# class BiomassDataset(Dataset):\n#     def __init__(self, df, image_root=\"\", transform=None):\n#         self.df = df.reset_index(drop=True)\n#         self.image_root = image_root\n#         self.transform = transform\n\n#     def __len__(self):\n#         return len(self.df)\n\n#     def _read_image(self, path: str):\n#         # 兼容绝对路径和相对路径\n#         p = path if os.path.isabs(path) else os.path.join(self.image_root, path)\n#         img = cv2.imread(p, cv2.IMREAD_COLOR)\n#         if img is None:\n#             # 容错：如果图片缺失返回黑图（防止提交中断）\n#             return np.zeros((CFG.img_size, CFG.img_size, 3), dtype=np.uint8)\n            \n#         img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n#         # 注意：此处已移除 clean_image 调用\n#         return img\n\n#     def __getitem__(self, idx):\n#         row = self.df.iloc[idx]\n#         img_path = str(row[\"image_path\"])\n#         img = self._read_image(img_path)\n        \n#         if self.transform:\n#             img = self.transform(image=img)[\"image\"]\n\n#         return {\"image\": img, \"index\": idx}\n\n# # ==========================================\n# # 3. 模型定义 (Model)\n# # ==========================================\n# class BiomassNet(nn.Module):\n#     def __init__(self, backbone_name: str, n_targets: int = 5, feat_dim: int = 768):\n#         super().__init__()\n#         # 推理时不下载预训练权重，直接加载本地权重\n#         self.backbone = timm.create_model(\n#             backbone_name, pretrained=False, num_classes=0, global_pool=\"avg\"\n#         )\n#         feat_out = self.backbone.num_features\n#         self.head = nn.Sequential(\n#             nn.Linear(feat_out, feat_dim),\n#             nn.GELU(),\n#             nn.Dropout(0.1),\n#             nn.Linear(feat_dim, n_targets),\n#         )\n\n#     def forward(self, image):\n#         feat = self.backbone(image)\n#         raw = self.head(feat)         \n#         return F.softplus(raw) \n\n# # ==========================================\n# # 4. TTA 与 推理逻辑\n# # ==========================================\n# @torch.no_grad()\n# def tta_predict_log5(model, imgs, mode=\"flip4\"):\n#     \"\"\"\n#     Test Time Augmentation\n#     返回: [B, 5] log1p 预测值\n#     \"\"\"\n#     def _pred(x):\n#         return model(x)\n\n#     if mode == \"none\":\n#         return _pred(imgs)\n\n#     if mode == \"flip4\":\n#         xs = [\n#             imgs,\n#             torch.flip(imgs, dims=[3]),              # Horizontal\n#             torch.flip(imgs, dims=[2]),              # Vertical\n#             torch.flip(imgs, dims=[2, 3]),           # H + V\n#         ]\n#         ps = [_pred(x) for x in xs]\n#         return torch.stack(ps, dim=0).mean(0)\n\n#     if mode == \"d4\":\n#         ps = []\n#         x = imgs\n#         for k in range(4):\n#             ps.append(_pred(x))\n#             ps.append(_pred(torch.flip(x, dims=[3])))\n#             x = torch.rot90(x, k=1, dims=[2, 3])\n#         return torch.stack(ps, dim=0).mean(0)\n\n#     return _pred(imgs)\n\n# def main():\n#     print(f\"Starting Inference...\")\n#     dev = device()\n    \n#     if not os.path.exists(CFG.TEST_CSV):\n#         raise FileNotFoundError(f\"Test CSV not found at {CFG.TEST_CSV}\")\n        \n#     test_df = pd.read_csv(CFG.TEST_CSV)\n    \n#     # 提取唯一图片，去重预测\n#     unique_images = test_df[\"image_path\"].astype(str).unique().tolist()\n#     img2row = {p: i for i, p in enumerate(unique_images)}\n#     t2i = {t: i for i, t in enumerate(CFG.target_order)}\n    \n#     print(f\"Found {len(unique_images)} unique images.\")\n\n#     # 准备 DataLoader\n#     img_df = pd.DataFrame({\"image_path\": unique_images})\n#     ds = BiomassDataset(\n#         img_df,\n#         image_root=CFG.IMAGE_ROOT,\n#         transform=get_valid_transform(CFG.img_size)\n#     )\n#     loader = DataLoader(\n#         ds, \n#         batch_size=CFG.batch_size, \n#         shuffle=False,\n#         num_workers=CFG.num_workers, \n#         pin_memory=True\n#     )\n\n#     # 预测累加容器 (Log1p空间)\n#     y_log_sum = np.zeros((len(unique_images), CFG.n_targets), dtype=np.float32)\n#     models_found = 0\n\n#     # 遍历 Folds\n#     for fold in range(CFG.n_folds):\n#         ckpt_path = os.path.join(CFG.MODEL_DIR, f\"model_fold{fold}.pth\")\n#         if not os.path.exists(ckpt_path):\n#             print(f\"[Warning] Model file not found: {ckpt_path}, skipping...\")\n#             continue\n            \n#         print(f\"Loading fold {fold}: {ckpt_path}\")\n        \n#         # 初始化模型\n#         model = BiomassNet(CFG.backbone, n_targets=CFG.n_targets).to(dev)\n        \n#         # 加载权重\n#         # 你的训练代码中保存的是 {\"model\": model_ema.module.state_dict(), ...}\n#         # 所以这里直接加载 'model' 键即可得到 EMA 权重\n#         ckpt = torch.load(ckpt_path, map_location=dev)\n#         state_dict = ckpt[\"model\"] if \"model\" in ckpt else ckpt\n        \n#         # 移除可能的 'module.' 前缀 (DDP 训练残留)\n#         new_state_dict = {}\n#         for k, v in state_dict.items():\n#             if k.startswith(\"module.\"):\n#                 new_state_dict[k[7:]] = v\n#             else:\n#                 new_state_dict[k] = v\n                \n#         model.load_state_dict(new_state_dict, strict=True)\n#         model.eval()\n\n#         fold_preds = []\n#         with torch.no_grad():\n#             for batch in tqdm(loader, desc=f\"Fold {fold}\"):\n#                 imgs = batch[\"image\"].to(dev)\n#                 y_pred = tta_predict_log5(model, imgs, mode=CFG.tta_mode) \n#                 fold_preds.append(y_pred.cpu().numpy())\n        \n#         y_log_sum += np.concatenate(fold_preds, axis=0)\n#         models_found += 1\n        \n#         # 释放显存\n#         del model\n#         torch.cuda.empty_cache()\n\n#     if models_found == 0:\n#         raise RuntimeError(\"No models loaded! Check CFG.MODEL_DIR\")\n\n#     # 平均并还原 (Log1p -> Linear Grams)\n#     y_log_avg = y_log_sum / models_found\n#     y_lin = np.expm1(y_log_avg)            \n#     y_lin = np.maximum(y_lin, 0.0)         \n\n#     # 物理一致性校正\n#     if CFG.enforce_consistency_infer:\n#         print(\"Applying physics consistency enforcement...\")\n#         # 0:Green, 1:Dead, 2:Clover, 3:GDM, 4:Total\n        \n#         # Rule 1: Total ≈ GDM + Dead\n#         total_direct = y_lin[:, 4]\n#         total_sum    = y_lin[:, 3] + y_lin[:, 1] \n#         y_lin[:, 4]  = 0.5 * (total_direct + total_sum)\n    \n#         # Rule 2: GDM ≈ Green + Clover\n#         if CFG.enforce_consistency_infer_gdm:\n#             gdm_direct = y_lin[:, 3]\n#             gdm_sum    = y_lin[:, 0] + y_lin[:, 2] \n#             y_lin[:, 3] = 0.5 * (gdm_direct + gdm_sum)\n    \n#         y_lin = np.maximum(y_lin, 0.0)\n\n#     # 生成提交文件\n#     print(\"Generating submission file...\")\n#     out_vals = np.zeros(len(test_df), dtype=np.float32)\n    \n#     img_paths = test_df[\"image_path\"].astype(str).values\n#     target_names = test_df[\"target_name\"].astype(str).values\n    \n#     for r in tqdm(range(len(test_df)), desc=\"Mapping\"):\n#         p = img_paths[r]\n#         t = target_names[r]\n        \n#         if p in img2row and t in t2i:\n#             r_idx = img2row[p]\n#             c_idx = t2i[t]\n#             out_vals[r] = y_lin[r_idx, c_idx]\n#         else:\n#             out_vals[r] = 0.0\n\n#     sub = pd.DataFrame({\n#         \"sample_id\": test_df[\"sample_id\"], \n#         \"target\": out_vals\n#     })\n    \n#     sub.to_csv(CFG.OUTPUT_CSV, index=False, float_format=\"%.6f\")\n#     print(f\"Submission saved to: {CFG.OUTPUT_CSV}\")\n\n# if __name__ == \"__main__\":\n#     main()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-28T12:35:26.213705Z","iopub.execute_input":"2025-12-28T12:35:26.214173Z","iopub.status.idle":"2025-12-28T12:35:26.240503Z","shell.execute_reply.started":"2025-12-28T12:35:26.214130Z","shell.execute_reply":"2025-12-28T12:35:26.239564Z"},"jupyter":{"source_hidden":true}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"**Five Seeds**","metadata":{}},{"cell_type":"code","source":"# import os\n# import glob\n# import numpy as np\n# import pandas as pd\n# import cv2\n# import torch\n# import torch.nn as nn\n# import torch.nn.functional as F\n# from torch.utils.data import Dataset, DataLoader\n# import albumentations as A\n# from albumentations.pytorch import ToTensorV2\n# import timm\n# from tqdm import tqdm\n\n# # ==========================================\n# # 1. 配置与路径 (请根据你的Kaggle环境修改)\n# # ==========================================\n\n# # 测试集数据路径 (Kaggle 官方数据路径)\n# TEST_CSV = \"/kaggle/input/csiro-biomass/test.csv\"\n# IMAGE_ROOT = \"/kaggle/input/csiro-biomass\"\n\n# # ★★★ 重要：你上传的模型权重数据集路径 ★★★\n# # 例如: \"/kaggle/input/my-biomass-weights/\"\n# MODEL_WEIGHTS_DIR = \"/kaggle/input/try-model/pytorch/default/5\" \n\n# class CFG:\n#     # 图像与模型配置 (必须与训练时一致)\n#     img_size = 384\n#     backbone = \"convnext_tiny.fb_in22k_ft_in1k\"\n#     n_targets = 5\n#     batch_size = 32  # 推理时显存占用小，可以适当大一点\n#     num_workers = 4\n    \n#     target_order = [\"Dry_Green_g\",\"Dry_Dead_g\",\"Dry_Clover_g\",\"GDM_g\",\"Dry_Total_g\"]\n    \n#     # TTA 设置\n#     tta_mode = \"flip4\" # \"flip4\" 或 \"d4\"\n    \n#     # 物理一致性后处理\n#     enforce_consistency_infer = True\n#     enforce_consistency_infer_gdm = True\n    \n#     # 5 Seeds 设置\n#     seeds = [42, 1024, 2048,  43]\n#     n_folds = 4\n\n# def device():\n#     return torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n\n# # ==========================================\n# # 2. 数据处理与模型定义 (保持不变)\n# # ==========================================\n\n# def get_valid_transform(img_size: int):\n#     return A.Compose([\n#         A.LongestMaxSize(max_size=img_size, interpolation=cv2.INTER_AREA),\n#         A.PadIfNeeded(img_size, img_size, border_mode=cv2.BORDER_REFLECT_101),\n#         A.Normalize(mean=(0.485, 0.456, 0.406), std=(0.229, 0.224, 0.225)),\n#         ToTensorV2(),\n#     ])\n\n# class BiomassDataset(Dataset):\n#     def __init__(self, df, image_root=\"\", transform=None):\n#         self.df = df.reset_index(drop=True)\n#         self.image_root = image_root\n#         self.transform = transform\n        \n#     def __len__(self):\n#         return len(self.df)\n\n#     def _read_image(self, path: str):\n#         # 处理可能的相对路径\n#         p = path if os.path.isabs(path) else os.path.join(self.image_root, path)\n#         img = cv2.imread(p, cv2.IMREAD_COLOR)\n#         if img is None:\n#             # 简单的容错，防止某张图读取失败导致提交崩溃 (生成全黑图)\n#             print(f\"Warning: Cannot read {p}, using black image.\")\n#             return np.zeros((CFG.img_size, CFG.img_size, 3), dtype=np.uint8)\n#         img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n#         return img\n\n#     def __getitem__(self, idx):\n#         row = self.df.iloc[idx]\n#         img_path = str(row[\"image_path\"])\n#         img = self._read_image(img_path)\n        \n#         if self.transform:\n#             img = self.transform(image=img)[\"image\"]\n            \n#         return {\"image\": img, \"index\": idx}\n\n# class BiomassNet(nn.Module):\n#     def __init__(self, backbone_name: str, n_targets: int = 5, feat_dim: int = 768):\n#         super().__init__()\n#         self.backbone = timm.create_model(\n#             backbone_name, pretrained=False, num_classes=0, global_pool=\"avg\"\n#         )\n#         feat_out = self.backbone.num_features\n#         self.head = nn.Sequential(\n#             nn.Linear(feat_out, feat_dim),\n#             nn.GELU(),\n#             nn.Dropout(0.1),\n#             nn.Linear(feat_dim, n_targets),\n#         )\n\n#     def forward(self, image):\n#         feat = self.backbone(image)\n#         raw = self.head(feat)         \n#         return F.softplus(raw)\n\n# # ==========================================\n# # 3. TTA 与 推理逻辑\n# # ==========================================\n\n# @torch.no_grad()\n# def tta_predict_log5(model, imgs, mode=\"flip4\"):\n#     def _pred(x):\n#         return model(x)\n\n#     if mode == \"none\":\n#         return _pred(imgs)\n\n#     if mode == \"flip4\":\n#         xs = [\n#             imgs,\n#             torch.flip(imgs, dims=[3]),              # H\n#             torch.flip(imgs, dims=[2]),              # V\n#             torch.flip(imgs, dims=[2, 3]),           # HV\n#         ]\n#         ps = [_pred(x) for x in xs]\n#         return torch.stack(ps, dim=0).mean(0)\n    \n#     if mode == \"d4\":\n#         ps = []\n#         x = imgs\n#         for k in range(4):\n#             ps.append(_pred(x))\n#             ps.append(_pred(torch.flip(x, dims=[3])))\n#             x = torch.rot90(x, k=1, dims=[2, 3])\n#         return torch.stack(ps, dim=0).mean(0)\n        \n#     return _pred(imgs)\n\n# def main_inference():\n#     print(f\"Starting Inference on {device()}...\")\n    \n#     # 1. 准备测试数据\n#     if not os.path.exists(TEST_CSV):\n#         print(f\"Error: Test CSV not found at {TEST_CSV}\")\n#         return\n\n#     test_df = pd.read_csv(TEST_CSV)\n#     images = test_df[\"image_path\"].astype(str).unique().tolist()\n    \n#     # 映射辅助字典\n#     img2row = {p: i for i, p in enumerate(images)}\n#     t2i = {t: i for i, t in enumerate(CFG.target_order)}\n    \n#     # 创建 DataLoader (针对唯一图片进行推理，避免重复计算)\n#     img_df = pd.DataFrame({\"image_path\": images})\n#     ds = BiomassDataset(img_df, image_root=IMAGE_ROOT, transform=get_valid_transform(CFG.img_size))\n#     loader = DataLoader(ds, batch_size=CFG.batch_size, shuffle=False, num_workers=CFG.num_workers, pin_memory=True)\n    \n#     # 2. 遍历所有 Seeds 和 Folds 进行推理集成\n#     # 累计所有模型的 log1p 预测值\n#     y_log_sum = np.zeros((len(images), CFG.n_targets), dtype=np.float32)\n#     models_count = 0\n    \n#     dev = device()\n    \n#     for seed in CFG.seeds:\n#         for fold in range(CFG.n_folds):\n#             # 构造模型文件名，需要与训练代码保存的格式一致\n#             # 格式: model_seed42_fold0.pth\n#             ckpt_name = f\"model_seed{seed}_fold{fold}.pth\"\n#             ckpt_path = os.path.join(MODEL_WEIGHTS_DIR, ckpt_name)\n            \n#             if not os.path.exists(ckpt_path):\n#                 # 尝试不带下划线的命名 (容错)\n#                 ckpt_path_alt = os.path.join(MODEL_WEIGHTS_DIR, f\"model_seed{seed}fold{fold}.pth\")\n#                 if os.path.exists(ckpt_path_alt):\n#                     ckpt_path = ckpt_path_alt\n#                 else:\n#                     print(f\"⚠️ Warning: Model not found: {ckpt_name}. Skipping...\")\n#                     continue\n            \n#             print(f\"Load: {os.path.basename(ckpt_path)}\")\n            \n#             # 加载模型\n#             model = BiomassNet(CFG.backbone, n_targets=CFG.n_targets)\n#             ckpt = torch.load(ckpt_path, map_location=dev)\n#             model.load_state_dict(ckpt[\"model\"], strict=True)\n#             model.to(dev)\n#             model.eval()\n            \n#             # 推理\n#             preds = []\n#             with torch.no_grad():\n#                 for batch in tqdm(loader, desc=f\"S{seed}-F{fold}\", leave=False):\n#                     imgs = batch[\"image\"].to(dev)\n#                     y_pred = tta_predict_log5(model, imgs, mode=CFG.tta_mode)\n#                     preds.append(y_pred.detach().cpu().numpy())\n            \n#             # 累加\n#             preds = np.concatenate(preds, axis=0)\n#             y_log_sum += preds\n#             models_count += 1\n            \n#             # 清理显存\n#             del model, ckpt\n#             torch.cuda.empty_cache()\n\n#     if models_count == 0:\n#         print(\"❌ Error: No models loaded! Check MODEL_WEIGHTS_DIR.\")\n#         return\n\n#     print(f\"✅ Ensemble finished. Averaging over {models_count} models...\")\n    \n#     # 3. 后处理与生成提交文件\n#     y_log = y_log_sum / models_count\n#     y_log = np.maximum(y_log, 0.0)      # log1p 不应小于 0\n#     y_lin = np.expm1(y_log)             # 还原回 gram\n#     y_lin = np.maximum(y_lin, 0.0)\n    \n#     # 物理一致性修正\n#     if CFG.enforce_consistency_infer:\n#         # Total = (Direct + (GDM + Dead)) / 2\n#         total_direct = y_lin[:, 4]\n#         total_sum    = y_lin[:, 3] + y_lin[:, 1]\n#         y_lin[:, 4]  = 0.5 * (total_direct + total_sum)\n        \n#         if CFG.enforce_consistency_infer_gdm:\n#             # GDM = (Direct + (Green + Clover)) / 2\n#             gdm_direct = y_lin[:, 3]\n#             gdm_sum    = y_lin[:, 0] + y_lin[:, 2]\n#             y_lin[:, 3] = 0.5 * (gdm_direct + gdm_sum)\n        \n#         y_lin = np.maximum(y_lin, 0.0)\n\n#     # 填入 DataFrame\n#     out_vals = np.zeros(len(test_df), dtype=np.float32)\n#     for r in range(len(test_df)):\n#         img = str(test_df.at[r, \"image_path\"])\n#         tname = str(test_df.at[r, \"target_name\"])\n        \n#         if img in img2row:\n#             img_idx = img2row[img]\n#             col = t2i[tname]\n#             out_vals[r] = y_lin[img_idx, col]\n#         else:\n#             out_vals[r] = 0.0 # 理论上不应该发生\n\n#     sub = pd.DataFrame({\n#         \"sample_id\": test_df[\"sample_id\"].astype(str), \n#         \"target\": out_vals\n#     })\n    \n#     sub.to_csv(\"submission.csv\", index=False, float_format=\"%.6f\")\n#     print(\"🎉 Submission saved to 'submission.csv'\")\n#     print(sub.head())\n\n# if __name__ == \"__main__\":\n#     main_inference()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-28T12:35:26.241977Z","iopub.execute_input":"2025-12-28T12:35:26.242330Z","iopub.status.idle":"2025-12-28T12:35:26.266335Z","shell.execute_reply.started":"2025-12-28T12:35:26.242295Z","shell.execute_reply":"2025-12-28T12:35:26.265298Z"},"jupyter":{"source_hidden":true}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# import os\n# import sys\n# import cv2\n# import numpy as np\n# import pandas as pd\n# from dataclasses import dataclass\n# from tqdm import tqdm\n# import torch\n# import torch.nn as nn\n# import torch.nn.functional as F\n# from torch.utils.data import Dataset, DataLoader\n# import albumentations as A\n# from albumentations.pytorch import ToTensorV2\n# import timm\n\n# # ==========================================\n# # 1. 配置 (Configuration)\n# # ==========================================\n# MODEL_DIR = \"/kaggle/input/try-model/pytorch/default/9\"\n# TEST_CSV  = \"/kaggle/input/csiro-biomass/test.csv\"\n# IMAGE_ROOT = \"/kaggle/input/csiro-biomass\" \n# OUTPUT_CSV = \"submission.csv\"\n\n# @dataclass\n# class CFG:\n#     img_size: int = 384\n#     batch_size: int = 32\n#     num_workers: int = 2\n#     backbone: str = \"convnext_tiny.fb_in22k_ft_in1k\"\n#     n_targets: int = 5\n#     n_folds: int = 4\n    \n#     # [修改点 0] 指定推理的折数\n#     # 如果设为 None，则默认跑所有折并做 Ensemble (0, 1, 2, 3)\n#     # 如果设为具体的数字 (如 0, 1, 2, 3)，则只跑那一折\n#     target_fold: int = None  # <--- 修改这里来指定单折推理，例如 target_fold = 0\n    \n#     target_order = [\"Dry_Green_g\", \"Dry_Dead_g\", \"Dry_Clover_g\", \"GDM_g\", \"Dry_Total_g\"]\n    \n#     # [修改点 1] TTA 模式改为只做水平翻转\n#     tta_mode: str = \"hflip\" \n\n# def device():\n#     return torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n\n# # ==========================================\n# # 2. 数据处理 (Dataset)\n# # ==========================================\n# IMAGENET_MEAN = (0.485, 0.456, 0.406)\n# IMAGENET_STD  = (0.229, 0.224, 0.225)\n\n# def get_valid_transform(img_size: int):\n#     return A.Compose([\n#         A.LongestMaxSize(max_size=img_size, interpolation=cv2.INTER_AREA),\n#         A.PadIfNeeded(img_size, img_size, border_mode=cv2.BORDER_REFLECT_101),\n#         A.Normalize(mean=IMAGENET_MEAN, std=IMAGENET_STD),\n#         ToTensorV2(),\n#     ])\n\n# class BiomassDataset(Dataset):\n#     def __init__(self, df, image_root=\"\", transform=None):\n#         self.df = df.reset_index(drop=True)\n#         self.image_root = image_root\n#         self.transform = transform\n\n#     def __len__(self):\n#         return len(self.df)\n\n#     def _read_image(self, path: str):\n#         p = path if os.path.isabs(path) else os.path.join(self.image_root, path)\n#         img = cv2.imread(p, cv2.IMREAD_COLOR)\n#         if img is None:\n#             raise FileNotFoundError(f\"Image not found: {p}\")\n#         img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n#         return img\n\n#     def __getitem__(self, idx):\n#         row = self.df.iloc[idx]\n#         img = self._read_image(str(row[\"image_path\"]))\n#         if self.transform:\n#             img = self.transform(image=img)[\"image\"]\n#         return {\"image\": img, \"index\": idx}\n\n# # ==========================================\n# # 3. 模型 (Model)\n# # ==========================================\n# class BiomassNet(nn.Module):\n#     def __init__(self, backbone_name: str, n_targets: int = 5, feat_dim: int = 768):\n#         super().__init__()\n#         self.backbone = timm.create_model(backbone_name, pretrained=False, num_classes=0, global_pool=\"avg\")\n#         feat_out = self.backbone.num_features\n#         self.head = nn.Sequential(\n#             nn.Linear(feat_out, feat_dim),\n#             nn.GELU(),\n#             nn.Dropout(0.1),\n#             nn.Linear(feat_dim, n_targets),\n#         )\n\n#     def forward(self, image):\n#         feat = self.backbone(image)\n#         raw = self.head(feat)         \n#         return F.softplus(raw)\n\n# # ==========================================\n# # 4. TTA 逻辑\n# # ==========================================\n# @torch.no_grad()\n# def tta_predict_log5(model, imgs, mode=\"hflip\"):\n#     def _pred(x): return model(x)\n\n#     if mode == \"none\":\n#         return _pred(imgs)\n\n#     if mode == \"hflip\":\n#         img_flip = torch.flip(imgs, dims=[3])\n#         p1 = _pred(imgs)\n#         p2 = _pred(img_flip)\n#         return 0.5 * (p1 + p2)\n\n#     return _pred(imgs)\n\n# # ==========================================\n# # 5. 主函数\n# # ==========================================\n# def main():\n#     dev = device()\n#     test_df = pd.read_csv(TEST_CSV)\n    \n#     # 去重\n#     images = test_df[\"image_path\"].astype(str).unique().tolist()\n#     img2row = {p: i for i, p in enumerate(images)}\n#     t2i = {t: i for i, t in enumerate(CFG.target_order)}\n    \n#     img_df = pd.DataFrame({\"image_path\": images})\n#     ds = BiomassDataset(img_df, image_root=IMAGE_ROOT, transform=get_valid_transform(CFG.img_size))\n#     loader = DataLoader(ds, batch_size=CFG.batch_size, shuffle=False, num_workers=CFG.num_workers, pin_memory=True)\n\n#     # ------------------------------------------------------------------\n#     # [修改点 4] 确定要跑哪些 Fold\n#     # ------------------------------------------------------------------\n#     if CFG.target_fold is not None:\n#         print(f\"!!! Single Fold Inference Mode: Fold {CFG.target_fold} !!!\")\n#         folds_to_run = [CFG.target_fold]\n#         output_csv_name = f\"submission_fold{CFG.target_fold}.csv\" # 可选：改变输出文件名方便区分\n#     else:\n#         print(f\"!!! Ensemble Mode: Running all {CFG.n_folds} folds !!!\")\n#         folds_to_run = range(CFG.n_folds)\n#         output_csv_name = OUTPUT_CSV\n\n#     # 检查模型文件是否存在\n#     model_files = []\n#     for fold in folds_to_run:\n#         pth = os.path.join(MODEL_DIR, f\"model_fold{fold}.pth\")\n#         if os.path.exists(pth):\n#             model_files.append(pth)\n#         else:\n#             print(f\"Warning: Model file not found: {pth}\")\n\n#     if not model_files:\n#         print(\"No valid model files found!\")\n#         return\n\n#     # 开始推理\n#     y_log_sum = np.zeros((len(images), CFG.n_targets), dtype=np.float32)\n    \n#     print(f\"Running inference on {len(model_files)} models with TTA={CFG.tta_mode}...\")\n\n#     for ckpt_path in model_files:\n#         print(f\"Loading model: {ckpt_path}\")\n#         ckpt = torch.load(ckpt_path, map_location=dev)\n#         model = BiomassNet(CFG.backbone, n_targets=CFG.n_targets).to(dev)\n#         model.load_state_dict(ckpt[\"model\"], strict=True)\n#         model.eval()\n\n#         preds_fold = []\n#         with torch.no_grad():\n#             for batch in tqdm(loader, desc=f\"Infer {os.path.basename(ckpt_path)}\"):\n#                 imgs = batch[\"image\"].to(dev)\n#                 y_pred = tta_predict_log5(model, imgs, mode=CFG.tta_mode)\n#                 preds_fold.append(y_pred.detach().cpu().numpy())\n        \n#         y_log_sum += np.concatenate(preds_fold, axis=0)\n        \n#         # 释放显存\n#         del model, ckpt\n#         torch.cuda.empty_cache()\n\n#     # Average\n#     y_log = y_log_sum / len(model_files)\n#     y_lin = np.expm1(np.maximum(y_log, 0.0)) \n#     y_lin = np.maximum(y_lin, 0.0)\n    \n#     # ---------------------------------------------------------\n#     # 物理一致性 (保守策略)\n#     # ---------------------------------------------------------\n#     print(\"Applying optimized consistency...\")\n#     idx_gdm = CFG.target_order.index(\"GDM_g\")\n#     idx_green = CFG.target_order.index(\"Dry_Green_g\")\n#     idx_clover = CFG.target_order.index(\"Dry_Clover_g\")\n#     idx_total = CFG.target_order.index(\"Dry_Total_g\")\n#     idx_dead = CFG.target_order.index(\"Dry_Dead_g\")\n\n#     # 1. GDM 修正\n#     gdm_direct = y_lin[:, idx_gdm]\n#     gdm_sum = y_lin[:, idx_green] + y_lin[:, idx_clover]\n#     y_lin[:, idx_gdm] = 0.5 * gdm_direct + 0.5 * gdm_sum\n\n#     # 2. Total 修正 (保守权重)\n#     total_direct = y_lin[:, idx_total]\n#     total_sum = y_lin[:, idx_gdm] + y_lin[:, idx_dead]\n#     y_lin[:, idx_total] = 0.9 * total_direct + 0.1 * total_sum \n    \n#     # ---------------------------------------------------------\n\n#     # Mapping & Save\n#     out_vals = np.zeros(len(test_df), dtype=np.float32)\n#     for r in tqdm(range(len(test_df)), desc=\"Mapping\"):\n#         img = str(test_df.at[r, \"image_path\"])\n#         tname = str(test_df.at[r, \"target_name\"])\n#         out_vals[r] = y_lin[img2row[img], t2i[tname]]\n\n#     sub = pd.DataFrame({\"sample_id\": test_df[\"sample_id\"], \"target\": out_vals})\n    \n#     # 注意：Kaggle 提交必须叫 submission.csv，如果你是为了调试单折，可以改名；如果是为了提交，记得改回来\n#     # 这里我默认存为 submission.csv 以便你直接提交\n#     sub.to_csv(\"submission.csv\", index=False, float_format=\"%.6f\")\n#     print(f\"Saved submission.csv (Mode: {'Single Fold '+str(CFG.target_fold) if CFG.target_fold is not None else 'Ensemble'})\")\n\n# if __name__ == \"__main__\":\n#     main()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-28T12:35:26.268153Z","iopub.execute_input":"2025-12-28T12:35:26.268454Z","iopub.status.idle":"2025-12-28T12:35:26.292915Z","shell.execute_reply.started":"2025-12-28T12:35:26.268428Z","shell.execute_reply":"2025-12-28T12:35:26.291976Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"pd.read_csv(\"submission.csv\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-28T12:35:26.294140Z","iopub.execute_input":"2025-12-28T12:35:26.295310Z","iopub.status.idle":"2025-12-28T12:35:26.329118Z","shell.execute_reply.started":"2025-12-28T12:35:26.295266Z","shell.execute_reply":"2025-12-28T12:35:26.328268Z"}},"outputs":[],"execution_count":null}]}