{"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":[{"sourceId":112509,"databundleVersionId":14254895,"sourceType":"competition"},{"sourceId":13617670,"sourceType":"datasetVersion","datasetId":8654131},{"sourceId":14219637,"sourceType":"datasetVersion","datasetId":8654086},{"sourceId":699221,"sourceType":"modelInstanceVersion","isSourceIdPinned":true,"modelInstanceId":474801,"modelId":490703},{"sourceId":699339,"sourceType":"modelInstanceVersion","isSourceIdPinned":true,"modelInstanceId":474801,"modelId":490703},{"sourceId":699510,"sourceType":"modelInstanceVersion","isSourceIdPinned":true,"modelInstanceId":474801,"modelId":490703},{"sourceId":699560,"sourceType":"modelInstanceVersion","isSourceIdPinned":true,"modelInstanceId":474801,"modelId":490703},{"sourceId":699618,"sourceType":"modelInstanceVersion","isSourceIdPinned":true,"modelInstanceId":474801,"modelId":490703},{"sourceId":699675,"sourceType":"modelInstanceVersion","isSourceIdPinned":true,"modelInstanceId":474801,"modelId":490703},{"sourceId":699746,"sourceType":"modelInstanceVersion","isSourceIdPinned":true,"modelInstanceId":474801,"modelId":490703},{"sourceId":699753,"sourceType":"modelInstanceVersion","isSourceIdPinned":true,"modelInstanceId":474801,"modelId":490703}],"dockerImageVersionId":31236,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# import os\n# import random\n# import numpy as np\n# import pandas as pd\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# from dataclasses import dataclass\n# from tqdm import tqdm\n\n# # ====================================================\n# # Configuration\n# # ====================================================\n# @dataclass\n# class CFG:\n#     seed: int = 42\n#     # 推理时使用的 Fold 数量，请确保您的 models 文件夹中有对应数量的模型权重 (model_fold0.pth, model_fold1.pth ...)\n#     n_folds: int = 4\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#     target_order = [\"Dry_Green_g\", \"Dry_Dead_g\", \"Dry_Clover_g\", \"GDM_g\", \"Dry_Total_g\"]\n    \n#     # 模型输出的参数配置\n#     n_params: int = 3\n#     param_order = [\"Total_log\", \"r_gdm_over_total\", \"s_clover_over_gdm\"]\n    \n#     # TTA 设置\n#     tta_mode: str = \"flip4\"  # \"flip4\" 或 \"d4\"\n\n# def device():\n#     return torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n\n# def set_seed(seed=42):\n#     random.seed(seed)\n#     np.random.seed(seed)\n#     torch.manual_seed(seed)\n#     torch.cuda.manual_seed_all(seed)\n\n# # ====================================================\n# # Transforms\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# # ====================================================\n# # Dataset\n# # ====================================================\n# class BiomassDataset(Dataset):\n#     def __init__(self, df, image_root=\"\", transform=None, target_cols=None):\n#         self.df = df.reset_index(drop=True)\n#         self.image_root = image_root\n#         self.transform = transform\n#         self.target_cols = target_cols or CFG.target_order\n#         # 推理时如果 test.csv 没有目标列，has_target 会为 False\n#         self.has_target = all(c in self.df.columns for c in self.target_cols)\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: Image not found: {p}\")\n#             img = np.zeros((CFG.img_size, CFG.img_size, 3), dtype=np.uint8)\n#         else:\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        \n#         if self.transform:\n#             img = self.transform(image=img)[\"image\"]\n\n#         # 推理模式下，如果没有标签，返回空 tensor\n#         if self.has_target:\n#             y = np.log1p(row[self.target_cols].astype(np.float32).values)\n#             y = torch.from_numpy(y.astype(np.float32))\n#         else:\n#             y = torch.empty(0, dtype=torch.float32)\n\n#         return {\"image\": img, \"y\": y, \"index\": idx}\n\n# # ====================================================\n# # Model & Logic\n# # ====================================================\n# class BiomassNet(nn.Module):\n#     def __init__(self, backbone_name: str, n_targets: int = 3, feat_dim: int = 768):\n#         super().__init__()\n#         # 推理时 pretrained=False，避免联网下载权重，权重将从本地 checkpoint 加载\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#         return self.head(feat)\n\n# # 核心逻辑：将模型预测的 3 个参数转换为 5 个 biomass 目标值\n# PARAM2I = {name: i for i, name in enumerate(CFG.param_order)}\n\n# def params_to_log_targets(p):\n#     raw_total = p[:, PARAM2I[\"Total_log\"]]\n#     raw_r     = p[:, PARAM2I[\"r_gdm_over_total\"]]\n#     raw_s     = p[:, PARAM2I[\"s_clover_over_gdm\"]]\n\n#     total_log = F.softplus(raw_total)      # log1p(Total)\n#     total_g   = torch.expm1(total_log)\n\n#     r = torch.sigmoid(raw_r)               # GDM/Total ratio\n#     s = torch.sigmoid(raw_s)               # Clover/GDM ratio\n\n#     gdm_g    = total_g * r\n#     dead_g   = total_g - gdm_g\n#     clover_g = gdm_g * s\n#     green_g  = gdm_g - clover_g\n\n#     # 数值安全截断\n#     green_g  = torch.clamp(green_g,  min=0.0)\n#     dead_g   = torch.clamp(dead_g,   min=0.0)\n#     clover_g = torch.clamp(clover_g, min=0.0)\n#     gdm_g    = torch.clamp(gdm_g,    min=0.0)\n\n#     # 返回 log1p 空间的值\n#     return torch.stack([\n#         torch.log1p(green_g),              # Dry_Green_g\n#         torch.log1p(dead_g),               # Dry_Dead_g\n#         torch.log1p(clover_g),             # Dry_Clover_g\n#         torch.log1p(gdm_g),                # GDM_g\n#         total_log                          # Dry_Total_g\n#     ], dim=1)\n\n# # TTA (Test Time Augmentation) 预测函数\n# def tta_predict_log5(model, imgs, mode=\"flip4\"):\n#     preds = []\n\n#     def _pred(x):\n#         p = model(x)                      \n#         return params_to_log_targets(p)   \n\n#     if mode == \"flip4\":\n#         preds.append(_pred(imgs))\n#         preds.append(_pred(torch.flip(imgs, dims=[3])))      # hflip\n#         preds.append(_pred(torch.flip(imgs, dims=[2])))      # vflip\n#         preds.append(_pred(torch.flip(imgs, dims=[2,3])))    # hvflip\n#         return torch.stack(preds, 0).mean(0)\n\n#     elif mode == \"d4\":\n#         for k in [0,1,2,3]:\n#             x = torch.rot90(imgs, k, dims=[2,3])\n#             preds.append(_pred(x))\n#             preds.append(_pred(torch.flip(x, dims=[3])))\n#         return torch.stack(preds, 0).mean(0)\n#     else:\n#         # 不做 TTA\n#         return _pred(imgs)\n\n# # ====================================================\n# # Inference Function\n# # ====================================================\n# def infer_and_submit(test_csv_path, image_root_path, models_dir_path, output_csv_path):\n#     print(f\"Loading test data from: {test_csv_path}\")\n#     test_df = pd.read_csv(test_csv_path)\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    \n#     # 目标列名到索引的映射\n#     t2i = {t: i for i, t in enumerate(CFG.target_order)}\n\n#     # 创建只包含唯一图片的 DataFrame 用于 Dataset\n#     img_df = pd.DataFrame({\"image_path\": unique_images})\n    \n#     ds = BiomassDataset(\n#         img_df,\n#         image_root=image_root_path,\n#         transform=get_valid_transform(CFG.img_size),\n#         target_cols=CFG.target_order # 实际上不需要 target，但保持接口一致\n#     )\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#     dev = device()\n    \n#     # 用于存储所有 Fold 的预测结果总和 (log空间)\n#     y_log_sum = np.zeros((len(unique_images), len(CFG.target_order)), dtype=np.float32)\n    \n#     # 遍历所有 Fold 加载模型并预测\n#     models_found = 0\n#     for fold in range(CFG.n_folds):\n#         ckpt_path = os.path.join(models_dir_path, f\"model_fold{fold}.pth\")\n#         if not os.path.exists(ckpt_path):\n#             print(f\"Warning: Model file not found: {ckpt_path}, skipping this fold.\")\n#             continue\n            \n#         print(f\"Predicting with model fold {fold}...\")\n        \n#         # 初始化模型架构\n#         model = BiomassNet(CFG.backbone, n_targets=CFG.n_params).to(dev)\n        \n#         # 加载权重\n#         ckpt = torch.load(ckpt_path, map_location=dev)\n#         # 兼容 key 可能在 'model' 字段下或者直接是 state_dict 的情况\n#         state_dict = ckpt[\"model\"] if \"model\" in ckpt else ckpt\n#         model.load_state_dict(state_dict, strict=True)\n        \n#         model.eval()\n#         models_found += 1\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#                 # 使用 TTA 进行预测，输出 shape [B, 5]\n#                 y_pred = tta_predict_log5(model, imgs, mode=CFG.tta_mode)\n#                 fold_preds.append(y_pred.detach().cpu().numpy())\n        \n#         y_log_sum += np.concatenate(fold_preds, axis=0)\n\n#     if models_found == 0:\n#         raise FileNotFoundError(\"No model weights found in the specified directory!\")\n\n#     # 计算平均值 (log空间)\n#     y_log_avg = y_log_sum / models_found\n#     y_log_avg = np.maximum(y_log_avg, 0.0) # 确保非负\n\n#     # 转换回线性空间 (g)\n#     y_lin = np.expm1(y_log_avg)\n#     y_lin = np.maximum(y_lin, 0.0)\n\n#     # 将预测结果填回 test_df (根据 sample_id 要求的特定 target)\n#     print(\"Generating submission file...\")\n#     out_vals = np.zeros(len(test_df), dtype=np.float32)\n    \n#     for r in tqdm(range(len(test_df)), desc=\"Mapping predictions\"):\n#         img_path = str(test_df.at[r, \"image_path\"])\n#         target_name = str(test_df.at[r, \"target_name\"])\n        \n#         # 找到该图片在 predictions 中的索引\n#         img_idx = img2row[img_path]\n#         # 找到目标变量在 predictions 中的列索引\n#         col_idx = t2i[target_name]\n        \n#         out_vals[r] = y_lin[img_idx, col_idx]\n\n#     # 创建提交文件\n#     submission = pd.DataFrame({\n#         \"sample_id\": test_df[\"sample_id\"].astype(str),\n#         \"target\": out_vals\n#     })\n    \n#     submission.to_csv(output_csv_path, index=False, float_format=\"%.6f\")\n#     print(f\"Successfully saved submission to: {output_csv_path}\")\n\n# # ====================================================\n# # Main Execution\n# # ====================================================\n# if __name__ == \"__main__\":\n#     # ----------------------------------------------------\n#     # TODO: 请在此处修改为您的实际数据路径\n#     # ----------------------------------------------------\n    \n#     # 测试集 CSV 路径 (test.csv)\n#     MY_TEST_CSV = \"/kaggle/input/csiro-biomass/test.csv\"\n    \n#     # 图片根目录 (如果是相对路径，将基于此目录拼接 test.csv 中的 image_path)\n#     MY_IMAGE_ROOT = \"/kaggle/input/csiro-biomass\"\n    \n#     # 存放模型权重 (.pth 文件) 的文件夹\n#     MY_MODELS_DIR = \"/kaggle/input/image/pytorch/default/11\"\n    \n#     # 输出文件路径\n#     MY_OUTPUT_CSV = \"submission.csv\"\n    \n#     # ----------------------------------------------------\n    \n#     # 检查路径是否存在 (可选)\n#     if not os.path.exists(MY_TEST_CSV):\n#         print(f\"Error: Test CSV not found at {MY_TEST_CSV}\")\n#         # 这里你可以选择抛出异常或使用假数据测试\n    \n#     # 运行推理\n#     try:\n#         set_seed(CFG.seed)\n#         infer_and_submit(\n#             test_csv_path=MY_TEST_CSV,\n#             image_root_path=MY_IMAGE_ROOT,\n#             models_dir_path=MY_MODELS_DIR,\n#             output_csv_path=MY_OUTPUT_CSV\n#         )\n#     except Exception as e:\n#         print(f\"An error occurred during inference: {e}\")","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# import os\n# import cv2\n# import math\n# import random\n# import numpy as np\n# import pandas as pd\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# from tqdm import tqdm\n# import albumentations as A\n# from albumentations.pytorch import ToTensorV2\n# import timm\n\n# # ================= Configuration =================\n# class CFG:\n#     # --- 路径设置 (请根据你的Kaggle环境修改) ---\n#     # 测试集 CSV 路径\n#     TEST_CSV_PATH = \"/kaggle/input/csiro-biomass/test.csv\" \n#     # 图片文件夹根目录 (test.csv 中的路径是相对路径，如 test/img.jpg)\n#     IMAGE_ROOT = \"/kaggle/input/csiro-biomass\" \n#     # 存放训练好模型(.pth)的文件夹路径\n#     MODEL_DIR = \"/kaggle/input/image/pytorch/default/17\" \n#     # 输出文件名\n#     OUTPUT_CSV = \"submission.csv\"\n\n#     # --- 模型设置 (必须与训练时一致) ---\n#     img_size = 384\n#     n_folds = 4  # 训练了几个 fold 就填几个\n#     batch_size = 32\n#     num_workers = 4\n#     backbone = \"convnext_tiny.fb_in22k_ft_in1k\"\n    \n#     # 目标顺序 (必须与训练时一致)\n#     target_order = [\"Dry_Green_g\", \"Dry_Dead_g\", \"Dry_Clover_g\", \"GDM_g\", \"Dry_Total_g\"]\n#     n_targets = 5\n    \n#     # 推理策略\n#     tta_mode = \"flip4\"  # \"flip4\" (4次翻转集成) 或 \"none\"\n#     enforce_consistency = True # 是否启用物理一致性修正 (Total = GDM + Dead)\n\n# # ================= Utils & Transforms =================\n# def device():\n#     return torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\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# # ================= Dataset =================\n# class BiomassInferenceDataset(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: Image not found {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, \"image_path\": img_path}\n\n# # ================= Model =================\n# class BiomassNet(nn.Module):\n#     \"\"\"\n#     必须与训练代码完全一致\n#     \"\"\"\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)         # [B,5] log1p >= 0\n\n# # ================= TTA Logic =================\n# @torch.no_grad()\n# def tta_predict_log5(model, imgs, mode=\"flip4\"):\n#     \"\"\"\n#     TTA 推理，返回 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#         # 原图, 水平翻, 垂直翻, 双翻\n#         xs = [\n#             imgs,\n#             torch.flip(imgs, dims=[3]), \n#             torch.flip(imgs, dims=[2]), \n#             torch.flip(imgs, dims=[2, 3]), \n#         ]\n#         ps = [_pred(x) for x in xs]\n#         return torch.stack(ps, dim=0).mean(0)\n\n#     return _pred(imgs)\n\n# # ================= Main Inference Function =================\n# def run_inference():\n#     print(f\"Starting Inference...\")\n#     print(f\"Device: {device()}\")\n    \n#     # 1. 读取 Test CSV\n#     raw_test_df = pd.read_csv(CFG.TEST_CSV_PATH)\n    \n#     # 2. 去重：只对唯一的图片进行推理，而不是对每行 Target 推理\n#     unique_img_df = raw_test_df.drop_duplicates(subset=[\"image_path\"]).reset_index(drop=True)\n#     print(f\"Unique images to predict: {len(unique_img_df)}\")\n\n#     # 3. 准备 DataLoader\n#     ds = BiomassInferenceDataset(\n#         unique_img_df,\n#         image_root=CFG.IMAGE_ROOT,\n#         transform=get_valid_transform(CFG.img_size)\n#     )\n#     loader = DataLoader(ds, batch_size=CFG.batch_size, shuffle=False, \n#                         num_workers=CFG.num_workers, pin_memory=True)\n\n#     # 4. 循环 Folds 进行集成\n#     # 存储累加的 log1p 预测结果 [N_images, 5]\n#     final_log_preds = np.zeros((len(unique_img_df), CFG.n_targets), dtype=np.float32)\n    \n#     dev = device()\n    \n#     model_count = 0\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\"Checkpoint not found: {ckpt_path}, skipping...\")\n#             continue\n            \n#         print(f\"Loading Model Fold {fold} from {ckpt_path}...\")\n        \n#         # 加载模型\n#         model = BiomassNet(CFG.backbone, n_targets=CFG.n_targets)\n#         state_dict = torch.load(ckpt_path, map_location=dev)[\"model\"]\n#         model.load_state_dict(state_dict)\n#         model.to(dev)\n#         model.eval()\n        \n#         # 推理当前 Fold\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#                 # 使用 TTA 预测\n#                 y_pred = tta_predict_log5(model, imgs, mode=CFG.tta_mode)\n#                 fold_preds.append(y_pred.cpu().numpy())\n        \n#         fold_preds = np.concatenate(fold_preds, axis=0)\n#         final_log_preds += fold_preds\n#         model_count += 1\n        \n#         # 清理显存\n#         del model\n#         torch.cuda.empty_cache()\n\n#     if model_count == 0:\n#         raise ValueError(\"No models loaded! Check CFG.MODEL_DIR.\")\n\n#     # 5. 平均 log1p 结果并转换回线性空间\n#     avg_log_preds = final_log_preds / model_count\n#     # log1p -> linear (expm1), 并确保非负\n#     final_lin_preds = np.expm1(avg_log_preds)\n#     final_lin_preds = np.maximum(final_lin_preds, 0.0)\n\n#     # 6. (可选) 物理一致性修正\n#     if CFG.enforce_consistency:\n#         print(\"Applying physics consistency check...\")\n#         # target_order: [Green, Dead, Clover, GDM, Total]\n#         # indices: 0=Green, 1=Dead, 2=Clover, 3=GDM, 4=Total\n        \n#         # Total ≈ GDM + Dead\n#         # GDM   ≈ Green + Clover\n        \n#         # 修正 Total: 预测值 vs (GDM+Dead) 的平均\n#         pred_total = final_lin_preds[:, 4]\n#         sum_total  = final_lin_preds[:, 3] + final_lin_preds[:, 1]\n#         final_lin_preds[:, 4] = 0.5 * (pred_total + sum_total)\n        \n#         # (可选) 修正 GDM: 预测值 vs (Green+Clover) 的平均\n#         pred_gdm = final_lin_preds[:, 3]\n#         sum_gdm  = final_lin_preds[:, 0] + final_lin_preds[:, 2]\n#         final_lin_preds[:, 3] = 0.5 * (pred_gdm + sum_gdm)\n        \n#         final_lin_preds = np.maximum(final_lin_preds, 0.0)\n\n#     # 7. 映射结果回原始 Test CSV 格式\n#     # 创建查找字典: image_path -> [pred_array_5]\n#     img_to_pred = {}\n#     paths = unique_img_df[\"image_path\"].values\n#     for i, path in enumerate(paths):\n#         img_to_pred[path] = final_lin_preds[i]\n\n#     # 目标列名到索引的映射\n#     target_map = {name: i for i, name in enumerate(CFG.target_order)}\n    \n#     # 填充结果\n#     submission_scores = []\n#     for i, row in tqdm(raw_test_df.iterrows(), total=len(raw_test_df), desc=\"Formatting Submission\"):\n#         img_path = row[\"image_path\"]\n#         t_name = row[\"target_name\"]\n        \n#         if img_path in img_to_pred and t_name in target_map:\n#             pred_idx = target_map[t_name]\n#             val = img_to_pred[img_path][pred_idx]\n#             submission_scores.append(val)\n#         else:\n#             # 异常情况填0\n#             submission_scores.append(0.0)\n            \n#     # 8. 保存\n#     submission = pd.DataFrame({\n#         \"sample_id\": raw_test_df[\"sample_id\"],\n#         \"target\": submission_scores\n#     })\n    \n#     submission.to_csv(CFG.OUTPUT_CSV, index=False)\n#     print(f\"Submission saved to {CFG.OUTPUT_CSV}\")\n#     print(submission.head())\n\n# if __name__ == \"__main__\":\n#     run_inference()","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# import os\n# import cv2\n# import math\n# import random\n# import numpy as np\n# import pandas as pd\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# from tqdm import tqdm\n# import albumentations as A\n# from albumentations.pytorch import ToTensorV2\n# import timm\n\n# # ================= Configuration =================\n# class CFG:\n#     # --- 1. 路径设置 (请根据你的环境修改) ---\n#     # 测试集 CSV 路径 (Kaggle通常是 /kaggle/input/csiro-biomass/test.csv)\n#     TEST_CSV_PATH = \"/kaggle/input/csiro-biomass/test.csv\" \n    \n#     # 图片文件夹根目录 (test.csv 中的路径通常是相对路径，如 test/img.jpg)\n#     IMAGE_ROOT = \"/kaggle/input/csiro-biomass\" \n    \n#     # 存放训练好模型(.pth)的文件夹路径\n#     MODEL_DIR = \"/kaggle/input/image/pytorch/default/20\" \n    \n#     # 输出文件名\n#     OUTPUT_CSV = \"submission.csv\"\n\n#     # --- 2. 核心设置：指定推理的 Fold ---\n#     # 列表里写几就跑第几折。\n#     # 单折推理示例: [0]\n#     # 全折集成示例: [0, 1, 2, 3]\n#     folds_to_use = [0,1,2,3]  \n\n#     # --- 3. 模型参数 (必须与训练时完全一致) ---\n#     img_size = 384\n#     batch_size = 32   # 推理时不反向传播，显存占用小，可以稍微大点\n#     num_workers = 2\n#     backbone = \"convnext_tiny.fb_in22k_ft_in1k\"\n    \n#     # 目标顺序 (必须与训练时一致)\n#     target_order = [\"Dry_Green_g\", \"Dry_Dead_g\", \"Dry_Clover_g\", \"GDM_g\", \"Dry_Total_g\"]\n#     n_targets = 5\n    \n#     # --- 4. 推理策略 ---\n#     # TTA模式: \"flip4\" (原图+水平翻+垂直翻+双翻) 或 \"none\"\n#     tta_mode = \"flip4\"  \n#     # 是否启用物理一致性修正 (Total = GDM + Dead)\n#     enforce_consistency = True \n\n# # ================= Utils & Transforms =================\n# def device():\n#     return torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\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# # ================= Dataset =================\n# class BiomassInferenceDataset(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: Image not found {p}, using black placeholder.\")\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#         # 返回图片和路径，用于后续匹配结果\n#         return {\"image\": img, \"image_path\": img_path}\n\n# # ================= Model =================\n# class BiomassNet(nn.Module):\n#     \"\"\"\n#     必须与训练代码完全一致\n#     \"\"\"\n#     def __init__(self, backbone_name: str, n_targets: int = 5, feat_dim: int = 768):\n#         super().__init__()\n#         # 推理时 pretrained=False，因为我们要加载自己的权重\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)         # [B,5] log1p >= 0\n\n# # ================= TTA Logic =================\n# @torch.no_grad()\n# def tta_predict_log5(model, imgs, mode=\"flip4\"):\n#     \"\"\"\n#     TTA 推理，返回 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#         # 原图, 水平翻, 垂直翻, 双翻\n#         xs = [\n#             imgs,\n#             torch.flip(imgs, dims=[3]), \n#             torch.flip(imgs, dims=[2]), \n#             torch.flip(imgs, dims=[2, 3]), \n#         ]\n#         ps = [_pred(x) for x in xs]\n#         # 平均 4 个视角的 log1p 预测\n#         return torch.stack(ps, dim=0).mean(0)\n\n#     return _pred(imgs)\n\n# # ================= Main Inference Function =================\n# def run_inference():\n#     print(f\"Starting Inference...\")\n#     print(f\"Device: {device()}\")\n#     print(f\"Folds to use: {CFG.folds_to_use}\")\n    \n#     # 1. 读取 Test CSV\n#     if not os.path.exists(CFG.TEST_CSV_PATH):\n#         raise FileNotFoundError(f\"Test CSV not found at {CFG.TEST_CSV_PATH}\")\n        \n#     raw_test_df = pd.read_csv(CFG.TEST_CSV_PATH)\n    \n#     # 2. 去重：只对唯一的图片进行推理，而不是对每行 Target 推理\n#     unique_img_df = raw_test_df.drop_duplicates(subset=[\"image_path\"]).reset_index(drop=True)\n#     print(f\"Total rows: {len(raw_test_df)}, Unique images: {len(unique_img_df)}\")\n\n#     # 3. 准备 DataLoader\n#     ds = BiomassInferenceDataset(\n#         unique_img_df,\n#         image_root=CFG.IMAGE_ROOT,\n#         transform=get_valid_transform(CFG.img_size)\n#     )\n#     loader = DataLoader(ds, batch_size=CFG.batch_size, shuffle=False, \n#                         num_workers=CFG.num_workers, pin_memory=True)\n\n#     # 4. 循环 Folds 进行集成\n#     # 存储累加的 log1p 预测结果 [N_images, 5]\n#     final_log_preds = np.zeros((len(unique_img_df), CFG.n_targets), dtype=np.float32)\n    \n#     dev = device()\n#     model_count = 0\n    \n#     # 只遍历配置中指定的 Fold\n#     for fold in CFG.folds_to_use:\n#         ckpt_path = os.path.join(CFG.MODEL_DIR, f\"model_fold{fold}.pth\")\n        \n#         if not os.path.exists(ckpt_path):\n#             print(f\"❌ Error: Checkpoint not found: {ckpt_path}\")\n#             # 如果是单折推理且文件不存在，直接报错停止\n#             if len(CFG.folds_to_use) == 1:\n#                 raise FileNotFoundError(f\"Model file missing: {ckpt_path}\")\n#             continue\n            \n#         print(f\"Loading Model Fold {fold} from {ckpt_path}...\")\n        \n#         # 初始化模型\n#         model = BiomassNet(CFG.backbone, n_targets=CFG.n_targets)\n        \n#         # 加载权重\n#         try:\n#             state_dict = torch.load(ckpt_path, map_location=dev)\n#             # 兼容保存时 key 为 \"model\" 的情况，也兼容直接保存 state_dict 的情况\n#             if \"model\" in state_dict:\n#                 state_dict = state_dict[\"model\"]\n#             model.load_state_dict(state_dict)\n#         except Exception as e:\n#             print(f\"❌ Error loading state dict: {e}\")\n#             continue\n\n#         model.to(dev)\n#         model.eval()\n        \n#         # 推理当前 Fold\n#         fold_preds = []\n#         with torch.no_grad():\n#             for batch in tqdm(loader, desc=f\"Infer Fold {fold}\"):\n#                 imgs = batch[\"image\"].to(dev)\n#                 # 使用 TTA 预测\n#                 y_pred = tta_predict_log5(model, imgs, mode=CFG.tta_mode)\n#                 fold_preds.append(y_pred.cpu().numpy())\n        \n#         # 拼接并在 CPU 上累加\n#         fold_preds = np.concatenate(fold_preds, axis=0)\n#         final_log_preds += fold_preds\n#         model_count += 1\n        \n#         # 清理显存\n#         del model\n#         torch.cuda.empty_cache()\n\n#     if model_count == 0:\n#         raise ValueError(\"No models loaded! Check CFG.MODEL_DIR and CFG.folds_to_use.\")\n\n#     # 5. 平均 log1p 结果并转换回线性空间\n#     print(f\"Averaging predictions from {model_count} models...\")\n#     avg_log_preds = final_log_preds / model_count\n    \n#     # log1p -> linear (expm1), 并确保非负\n#     final_lin_preds = np.expm1(avg_log_preds)\n#     final_lin_preds = np.maximum(final_lin_preds, 0.0)\n\n#     # 6. (可选) 物理一致性修正\n#     if CFG.enforce_consistency:\n#         print(\"Applying physics consistency check...\")\n#         # target_order: [Green, Dead, Clover, GDM, Total]\n#         # 索引: 0=Green, 1=Dead, 2=Clover, 3=GDM, 4=Total\n        \n#         # 规则1: Total ≈ GDM + Dead\n#         pred_total = final_lin_preds[:, 4]\n#         sum_total  = final_lin_preds[:, 3] + final_lin_preds[:, 1]\n#         final_lin_preds[:, 4] = 0.5 * (pred_total + sum_total)\n        \n#         # 规则2: GDM ≈ Green + Clover (通常 GDM 会比 component sum 大一点点，这里简单平均)\n#         pred_gdm = final_lin_preds[:, 3]\n#         sum_gdm  = final_lin_preds[:, 0] + final_lin_preds[:, 2]\n#         final_lin_preds[:, 3] = 0.5 * (pred_gdm + sum_gdm)\n        \n#         final_lin_preds = np.maximum(final_lin_preds, 0.0)\n\n#     # 7. 映射结果回原始 Test CSV 格式\n#     # 创建查找字典: image_path -> [pred_array_5]\n#     img_to_pred = {}\n#     paths = unique_img_df[\"image_path\"].values\n#     for i, path in enumerate(paths):\n#         img_to_pred[path] = final_lin_preds[i]\n\n#     # 目标列名到索引的映射\n#     target_map = {name: i for i, name in enumerate(CFG.target_order)}\n    \n#     # 填充结果\n#     submission_scores = []\n#     for i, row in tqdm(raw_test_df.iterrows(), total=len(raw_test_df), desc=\"Formatting Submission\"):\n#         img_path = row[\"image_path\"]\n#         t_name = row[\"target_name\"]\n        \n#         if img_path in img_to_pred and t_name in target_map:\n#             pred_idx = target_map[t_name]\n#             val = img_to_pred[img_path][pred_idx]\n#             submission_scores.append(val)\n#         else:\n#             # 异常情况填0\n#             submission_scores.append(0.0)\n            \n#     # 8. 保存\n#     submission = pd.DataFrame({\n#         \"sample_id\": raw_test_df[\"sample_id\"],\n#         \"target\": submission_scores\n#     })\n    \n#     submission.to_csv(CFG.OUTPUT_CSV, index=False)\n#     print(f\"Submission saved to {CFG.OUTPUT_CSV}\")\n#     print(submission.head())\n\n# if __name__ == \"__main__\":\n#     run_inference()","metadata":{"trusted":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 = 3  # <--- 修改这里来指定单折推理，例如 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},"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# 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# from dataclasses import dataclass\n\n# # ==========================================\n# # 1. 配置与路径 (请根据 Kaggle 环境修改此处)\n# # ==========================================\n\n# # 比赛数据通常挂载在 /kaggle/input/csiro-biomass/ 下\n# # 请确保您上传的模型数据集路径正确，例如 /kaggle/input/my-biomass-models/\n# PATH_TO_TEST_CSV  = \"/kaggle/input/csiro-biomass/test.csv\"\n# PATH_TO_IMAGE_DIR = \"/kaggle/input/csiro-biomass\"  # 图片根目录\n# PATH_TO_MODEL_DIR = \"/kaggle/input/image/pytorch/default/21\"  # 存放 model_fold0.pth 等权重的目录\n# OUTPUT_CSV_PATH   = \"submission.csv\"\n\n# # 如果您不想在推理时读取训练集 CSV，请在此处填入训练集 5 个目标的均值 (Dry_Green_g, Dry_Dead_g, ... etc)\n# # 格式参考: {'Dry_Green_g': 12.3, ...}。如果留空且不提供训练 CSV，均值校准步骤将被跳过。\n# TRAIN_MEANS_DICT = None \n# # PATH_TO_TRAIN_CSV = \"/kaggle/input/csiro-biomass/data/train_wide_with_time_clusters.csv\" # 备用：如果需要实时计算\n\n# @dataclass\n# class CFG:\n#     seed: int = 42\n#     n_folds: int = 4            # 这里的折数要对应您保存的模型数量\n#     img_size: int = 384\n#     batch_size: int = 32        # 推理时显存允许可调大\n#     num_workers: int = 2\n    \n#     backbone: str = \"convnext_tiny.fb_in22k_ft_in1k\"\n    \n#     # 目标顺序必须与训练时一致\n#     target_order = [\"Dry_Green_g\", \"Dry_Dead_g\", \"Dry_Clover_g\", \"GDM_g\", \"Dry_Total_g\"]\n    \n#     # 模型输出维度 (log1p 空间的 3 个分量: Green, Dead, Clover)\n#     n_targets = 3\n    \n#     # 推理时是否做物理一致性融合\n#     enforce_consistency_infer = False\n#     enforce_consistency_infer_gdm = False\n    \n#     # TTA 设置: \"flip4\" (原图+3种翻转) 或 \"d4\" (8种变换) 或 \"none\"\n#     tta_mode: str = \"flip4\"\n\n# def device():\n#     return torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n\n# # ==========================================\n# # 2. 数据处理与增强\n# # ==========================================\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, target_cols=None):\n#         self.df = df.reset_index(drop=True)\n#         self.image_root = image_root\n#         self.transform = transform\n#         self.target_cols = target_cols or CFG.target_order\n#         # 推理模式下通常没有标签，如果有则加载\n#         self.has_target = all(c in self.df.columns for c in self.target_cols)\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#             # Kaggle 推理时如果找不到图，可以用全黑图代替避免报错退出，或者抛出异常\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_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#         if self.has_target:\n#             y = np.log1p(row[self.target_cols].astype(np.float32).values)\n#             y = torch.from_numpy(y.astype(np.float32))\n#         else:\n#             y = torch.empty(0, dtype=torch.float32)\n\n#         return {\"image\": img, \"y\": y, \"index\": idx, \"image_path\": img_path}\n\n# # ==========================================\n# # 3. 模型定义 (必须与训练代码一致)\n# # ==========================================\n\n# class BiomassNet(nn.Module):\n#     def __init__(self, backbone_name: str, n_targets: int = 3, 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),   # 输出 3 个分量\n#         )\n\n#     def forward(self, image):\n#         feat = self.backbone(image)\n#         raw3 = self.head(feat)                 # [B, 3]\n#         comp_log = F.softplus(raw3)            # log1p 空间，保证 >=0\n\n#         green_log = comp_log[:, 0]\n#         dead_log  = comp_log[:, 1]\n#         clov_log  = comp_log[:, 2]\n\n#         # -> 还原到线性空间 (g)\n#         green_g = torch.expm1(green_log)\n#         dead_g  = torch.expm1(dead_log)\n#         clov_g  = torch.expm1(clov_log)\n\n#         # -> 用确定性公式生成 GDM / Total（线性空间）\n#         gdm_g   = green_g + clov_g\n#         total_g = green_g + dead_g + clov_g\n\n#         # -> 再映射回 log1p 空间（与 dataset y 对齐）\n#         gdm_log   = torch.log1p(gdm_g)\n#         total_log = torch.log1p(total_g)\n\n#         pred_log5 = torch.stack(\n#             [green_log, dead_log, clov_log, gdm_log, total_log], dim=1\n#         )  # [B, 5]\n#         return pred_log5\n\n# # ==========================================\n# # 4. TTA 与 推理逻辑\n# # ==========================================\n\n# @torch.no_grad()\n# def tta_predict_log5(model, imgs, mode=\"flip4\"):\n#     \"\"\"\n#     返回: [B,5] log1p predictions\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#     dev = device()\n#     print(f\"Inference Device: {dev}\")\n    \n#     # 1. 读取测试数据\n#     test_df = pd.read_csv(PATH_TO_TEST_CSV)\n#     print(f\"Test data shape: {test_df.shape}\")\n    \n#     # 2. 准备数据集 (去重 image_path 以加速推理)\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#     img_df = pd.DataFrame({\"image_path\": unique_images})\n#     ds = BiomassDataset(\n#         img_df,\n#         image_root=PATH_TO_IMAGE_DIR,\n#         transform=get_valid_transform(CFG.img_size),\n#         target_cols=CFG.target_order \n#     )\n#     loader = DataLoader(ds, batch_size=CFG.batch_size, shuffle=False,\n#                         num_workers=CFG.num_workers, pin_memory=True)\n    \n#     # 3. 初始化结果矩阵 (Log空间累加)\n#     y_log_sum = np.zeros((len(unique_images), len(CFG.target_order)), dtype=np.float32)\n    \n#     # 4. 循环加载 Fold 模型并推理\n#     # 注意：如果您的模型文件名不同（如不是 model_fold0.pth），请修改此处\n#     valid_folds = 0\n#     for fold in range(CFG.n_folds):\n#         ckpt_path = os.path.join(PATH_TO_MODEL_DIR, f\"model_fold{fold}.pth\")\n#         if not os.path.exists(ckpt_path):\n#             print(f\"Warning: Model not found at {ckpt_path}, skipping fold {fold}\")\n#             continue\n            \n#         print(f\"Loading model fold {fold} from {ckpt_path} ...\")\n#         ckpt = torch.load(ckpt_path, map_location=dev)\n        \n#         model = BiomassNet(CFG.backbone, n_targets=CFG.n_targets).to(dev)\n#         # 加载权重，注意处理 key 匹配\n#         state_dict = ckpt[\"model\"] if \"model\" in ckpt else ckpt\n#         model.load_state_dict(state_dict, strict=True)\n#         model.eval()\n        \n#         preds_fold = []\n#         with torch.no_grad():\n#             for batch in tqdm(loader, desc=f\"Infer Fold {fold}\"):\n#                 imgs = batch[\"image\"].to(dev)\n#                 # 使用 TTA 预测\n#                 y_pred = tta_predict_log5(model, imgs, mode=CFG.tta_mode)\n#                 preds_fold.append(y_pred.detach().cpu().numpy())\n                \n#         preds_fold = np.concatenate(preds_fold, axis=0)\n#         y_log_sum += preds_fold\n#         valid_folds += 1\n        \n#         # 清理显存\n#         del model, ckpt\n#         torch.cuda.empty_cache()\n\n#     if valid_folds == 0:\n#         raise RuntimeError(\"No models loaded! Check PATH_TO_MODEL_DIR.\")\n\n#     # 5. 平均并转换回线性空间\n#     y_log = y_log_sum / valid_folds\n#     y_log = np.maximum(y_log, 0.0) # 修正可能的负值\n#     y_lin = np.expm1(y_log)        # [N, 5] grams\n#     y_lin = np.maximum(y_lin, 0.0)\n    \n#     # 6. (可选) 均值校准\n#     # 如果您有训练集均值，可以在这里做一些后处理校准，或者物理一致性处理前做\n#     # 这里省略读取 CSV 的步骤，假定已包含在模型学习到的分布中或通过后处理修正\n    \n#     # 7. 物理一致性修正 (Physics Consistency)\n#     if CFG.enforce_consistency_infer:\n#         print(\"Enforcing Physics Consistency...\")\n#         # 索引\n#         idx_green = CFG.target_order.index(\"Dry_Green_g\")\n#         idx_dead  = CFG.target_order.index(\"Dry_Dead_g\")\n#         idx_clover= CFG.target_order.index(\"Dry_Clover_g\")\n#         idx_gdm   = CFG.target_order.index(\"GDM_g\")\n#         idx_total = CFG.target_order.index(\"Dry_Total_g\")\n        \n#         # A. 校准 GDM = (Predicted_GDM + (Green + Clover)) / 2\n#         if CFG.enforce_consistency_infer_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#         # B. 校准 Total = (Predicted_Total + (GDM + Dead)) / 2\n#         total_direct = y_lin[:, idx_total]\n#         total_sum    = y_lin[:, idx_gdm] + y_lin[:, idx_dead]\n#         y_lin[:, idx_total] = 0.5 * total_direct + 0.5 * total_sum\n        \n#         y_lin = np.maximum(y_lin, 0.0)\n\n#     # 8. 生成提交文件\n#     print(\"Generating submission file...\")\n#     # 比赛格式要求: sample_id, target\n#     # test.csv 格式: sample_id, image_path, target_name\n    \n#     out_vals = np.zeros(len(test_df), dtype=np.float32)\n    \n#     # 使用 numpy 向量化操作加速 (如果顺序一致) 或者 循环填值\n#     # 这里为了安全起见，依然使用 map 映射\n#     for r in tqdm(range(len(test_df)), desc=\"Mapping results\"):\n#         img_p = str(test_df.at[r, \"image_path\"])\n#         t_name = str(test_df.at[r, \"target_name\"])\n        \n#         if img_p in img2row and t_name in t2i:\n#             r_idx = img2row[img_p]\n#             c_idx = t2i[t_name]\n#             out_vals[r] = y_lin[r_idx, c_idx]\n#         else:\n#             # 异常情况处理\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(OUTPUT_CSV_PATH, index=False, float_format=\"%.6f\")\n#     print(f\"Submission saved to {OUTPUT_CSV_PATH}\")\n#     print(sub.head())\n\n# if __name__ == \"__main__\":\n#     main()","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# import os\n# import sys\n# import cv2\n# import gc\n# import random\n# import numpy as np\n# import pandas as pd\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# from dataclasses import dataclass\n\n# # ==========================================\n# # 1. 配置与路径 (Kaggle Environment)\n# # ==========================================\n\n# # 请根据实际挂载路径修改\n# # 假设比赛数据在 /kaggle/input/csiro-biomass/\n# PATH_TO_TEST_CSV  = \"/kaggle/input/csiro-biomass/test.csv\"\n# PATH_TO_IMAGE_DIR = \"/kaggle/input/csiro-biomass\" \n\n# # 假设模型权重上传到了 /kaggle/input/my-biomass-models/\n# # 文件夹内应包含 model_fold0.pth, model_fold1.pth 等\n# PATH_TO_MODEL_DIR = \"/kaggle/input/image/pytorch/default/22\" \n\n# OUTPUT_CSV_PATH   = \"submission.csv\"\n\n# @dataclass\n# class CFG:\n#     seed: int = 42\n    \n#     # --- 控制运行哪些折 (例如 [0] 或 [0, 1, 2, 3]) ---\n#     folds_to_run = [2]\n    \n#     img_size: int = 384\n#     batch_size: int = 32\n#     num_workers: int = 2\n    \n#     # 必须与训练时的 backbone 一致\n#     backbone: str = \"convnext_tiny.fb_in22k_ft_in1k\"\n    \n#     # 3头架构 (Green, Dead, Clover) -> 5头输出\n#     n_targets = 3  \n    \n#     # TTA 模式: \"flip4\", \"d4\", \"none\"\n#     tta_mode: str = \"flip4\"\n    \n#     # 物理一致性参数\n#     phys_eps: float = 1e-6\n    \n#     target_order = [\"Dry_Green_g\", \"Dry_Dead_g\", \"Dry_Clover_g\", \"GDM_g\", \"Dry_Total_g\"]\n\n# def device():\n#     return torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n\n# def set_seed(seed=42):\n#     random.seed(seed)\n#     np.random.seed(seed)\n#     torch.manual_seed(seed)\n#     torch.cuda.manual_seed_all(seed)\n\n# # ==========================================\n# # 2. 核心逻辑函数 (源自 Baseline)\n# # ==========================================\n\n# def compose_log5_from_log3(comp_log3: torch.Tensor, eps: float = 1e-6) -> torch.Tensor:\n#     \"\"\"\n#     将模型预测的 3 个分量 (Green, Dead, Clover) 转换为 5 个目标\n#     保证:\n#       GDM   = Green + Clover\n#       Total = Green + Dead + Clover\n#     \"\"\"\n#     green_log, dead_log, clov_log = comp_log3.unbind(dim=1)\n\n#     green_g = torch.expm1(green_log).clamp_min(0.0)\n#     dead_g  = torch.expm1(dead_log).clamp_min(0.0)\n#     clov_g  = torch.expm1(clov_log).clamp_min(0.0)\n\n#     # 物理合成\n#     gdm_log   = torch.log1p(green_g + clov_g + eps)\n#     total_log = torch.log1p(green_g + dead_g + clov_g + eps)\n\n#     pred_log5 = torch.stack([green_log, dead_log, clov_log, gdm_log, total_log], dim=1)\n#     return pred_log5\n\n# # ==========================================\n# # 3. 数据处理\n# # ==========================================\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, target_cols=None):\n#         self.df = df.reset_index(drop=True)\n#         self.image_root = image_root\n#         self.transform = transform\n#         self.target_cols = target_cols or CFG.target_order\n#         # 检查是否包含标签列 (推理时通常为 False)\n#         self.has_target = all(c in self.df.columns for c in self.target_cols)\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#             # 容错处理：如果图片读取失败，生成全黑图防止报错中断\n#             print(f\"Warning: Image not found {p}, using blank.\")\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#         if self.has_target:\n#             y = np.log1p(row[self.target_cols].astype(np.float32).values)\n#             y = torch.from_numpy(y.astype(np.float32))\n#         else:\n#             y = torch.empty(0, dtype=torch.float32)\n\n#         return {\"image\": img, \"y\": y, \"index\": idx, \"image_path\": img_path}\n\n# # ==========================================\n# # 4. 模型定义 (必须与训练一致)\n# # ==========================================\n\n# class BiomassNet(nn.Module):\n#     def __init__(self, backbone_name: str, n_targets: int = 3, feat_dim: int = 768):\n#         super().__init__()\n#         # 此时无需 pretrained=True，因为我们会加载权重\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\n#         # 只输出 3 个分量：Green / Dead / Clover（log1p 空间）\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_targets=3\n#         )\n\n#     def forward(self, image):\n#         feat = self.backbone(image)\n#         raw3 = self.head(feat)           # [B,3] raw\n#         comp_log3 = F.softplus(raw3)     # [B,3] -> log1p 分量（>=0）\n#         return comp_log3                 # 只返回 3 头\n\n# # ==========================================\n# # 5. TTA 与 推理流程\n# # ==========================================\n\n# @torch.no_grad()\n# def tta_predict_log5(model, imgs, mode=\"flip4\", eps=1e-6):\n#     \"\"\"\n#     TTA 包装函数：输入图像，输出 [B, 5] 的 log1p 预测值\n#     \"\"\"\n#     def _pred(x):\n#         # 1. 模型预测 3 个分量 [B, 3]\n#         comp3 = model(x)\n#         # 2. 转换为 5 个物理一致的目标 [B, 5]\n#         return compose_log5_from_log3(comp3, eps=eps)\n\n#     if mode == \"none\":\n#         return _pred(imgs)\n\n#     if mode == \"flip4\":\n#         x0 = imgs\n#         x1 = torch.flip(imgs, dims=[3])     # H Flip\n#         x2 = torch.flip(imgs, dims=[2])     # V Flip\n#         x3 = torch.flip(imgs, dims=[2, 3])  # H+V Flip\n#         ps = [_pred(x) for x in [x0, x1, x2, x3]]\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#     set_seed(CFG.seed)\n#     dev = device()\n#     print(f\"Device: {dev}\")\n#     print(f\"Folds to run: {CFG.folds_to_run}\")\n\n#     # 1. 读取测试表\n#     if not os.path.exists(PATH_TO_TEST_CSV):\n#         raise FileNotFoundError(f\"Test CSV not found: {PATH_TO_TEST_CSV}\")\n    \n#     test_df = pd.read_csv(PATH_TO_TEST_CSV)\n#     print(f\"Test samples: {len(test_df)}\")\n\n#     # 2. 准备数据集 (按 image_path 去重以加速)\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#     img_df = pd.DataFrame({\"image_path\": unique_images})\n#     ds = BiomassDataset(\n#         img_df,\n#         image_root=PATH_TO_IMAGE_DIR,\n#         transform=get_valid_transform(CFG.img_size)\n#     )\n#     loader = DataLoader(ds, batch_size=CFG.batch_size, shuffle=False, \n#                         num_workers=CFG.num_workers, pin_memory=True)\n\n#     # 3. 结果累加器 (Log空间)\n#     y_log_sum = np.zeros((len(unique_images), len(CFG.target_order)), dtype=np.float32)\n#     valid_folds = 0\n\n#     # 4. 循环推理指定折\n#     for fold in CFG.folds_to_run:\n#         ckpt_path = os.path.join(PATH_TO_MODEL_DIR, f\"model_fold{fold}.pth\")\n#         if not os.path.exists(ckpt_path):\n#             print(f\"[Warning] Fold {fold} model not found at {ckpt_path}. Skipping.\")\n#             continue\n        \n#         print(f\"Loading Fold {fold}: {ckpt_path}\")\n#         ckpt = torch.load(ckpt_path, map_location=dev)\n        \n#         model = BiomassNet(CFG.backbone, n_targets=CFG.n_targets).to(dev)\n        \n#         # 处理权重键名 (有时候会有 \"model.\" 前缀或包裹在 \"model\" key 中)\n#         state_dict = ckpt[\"model\"] if \"model\" in ckpt else ckpt\n#         # 简单清洗 key 如果需要 (例如去 DDP 前缀)\n#         new_state_dict = {}\n#         for k, v in state_dict.items():\n#             k = k.replace(\"module.\", \"\")\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\"Infer Fold {fold}\"):\n#                 imgs = batch[\"image\"].to(dev)\n#                 # TTA 预测 -> 返回 [B, 5] log1p\n#                 pred = tta_predict_log5(model, imgs, mode=CFG.tta_mode, eps=CFG.phys_eps)\n#                 fold_preds.append(pred.cpu().numpy())\n        \n#         fold_preds = np.concatenate(fold_preds, axis=0) # [N_images, 5]\n#         y_log_sum += fold_preds\n#         valid_folds += 1\n        \n#         del model, ckpt, state_dict\n#         torch.cuda.empty_cache()\n#         gc.collect()\n\n#     if valid_folds == 0:\n#         raise RuntimeError(\"No valid models found! Check paths.\")\n\n#     # 5. 平均并还原到线性空间\n#     y_log_avg = y_log_sum / valid_folds\n#     y_lin_avg = np.expm1(y_log_avg) # 还原为 grams\n#     y_lin_avg = np.maximum(y_lin_avg, 0.0)\n\n#     # 6. 生成提交 CSV\n#     print(\"Mapping results to submission format...\")\n#     out_vals = np.zeros(len(test_df), dtype=np.float32)\n    \n#     for r in range(len(test_df)):\n#         img_p = str(test_df.at[r, \"image_path\"])\n#         t_name = str(test_df.at[r, \"target_name\"])\n        \n#         if img_p in img2row and t_name in t2i:\n#             r_idx = img2row[img_p]\n#             c_idx = t2i[t_name]\n#             out_vals[r] = y_lin_avg[r_idx, c_idx]\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(OUTPUT_CSV_PATH, index=False, float_format=\"%.6f\")\n#     print(f\"Submission saved to {OUTPUT_CSV_PATH}\")\n#     print(sub.head())\n\n# if __name__ == \"__main__\":\n#     main()","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# import os\n# import sys\n# import gc\n# import cv2\n# import math\n# import torch\n# import numpy as np\n# import pandas as pd\n# import torch.nn as nn\n# import albumentations as A\n# import torch.nn.functional as F\n# from torch.utils.data import Dataset, DataLoader\n# from albumentations.pytorch import ToTensorV2\n# from tqdm import tqdm\n# import timm\n\n# # ==========================================\n# # 1. 配置与路径 (USER CONFIG)\n# # ==========================================\n# class CFG:\n#     # 你的模型架构名称（必须与训练时完全一致）\n#     backbone = \"convnext_tiny.fb_in22k_ft_in1k\" \n    \n#     img_size = 384\n#     batch_size = 32\n#     num_workers = 2  # Kaggle 环境下设为 2 通常比 0 快\n#     seed = 42\n    \n#     # 目标列定义（顺序必须与训练一致）\n#     target_order = [\"Dry_Green_g\", \"Dry_Dead_g\", \"Dry_Clover_g\", \"GDM_g\", \"Dry_Total_g\"]\n#     n_targets = 5\n    \n#     # TTA 设置: \"none\", \"flip4\" (H/V flip), \"d4\"\n#     tta_mode = \"flip4\"\n    \n#     # 推理时是否做一致性融合 (Total = (Direct + Sum)/2)\n#     enforce_consistency_infer = True\n#     enforce_consistency_infer_gdm = True\n\n# # --- ⚠️ 请修改这里的路径 ⚠️ ---\n# PATHS = {\n#     # 你的模型权重所在的 Dataset 路径 (例如: /kaggle/input/my-biomass-models/)\n#     \"MODEL_DIR\": \"/kaggle/input/image/pytorch/default/24\", \n    \n#     # 官方测试集图片路径\n#     \"IMAGE_ROOT\": \"/kaggle/input/csiro-biomass\",\n    \n#     # 官方测试集 CSV 路径\n#     \"TEST_CSV\": \"/kaggle/input/csiro-biomass/test.csv\",\n    \n#     # 输出文件路径\n#     \"SUBMISSION_CSV\": \"submission.csv\"\n# }\n\n# # 自动检测设备\n# device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n# print(f\"Using device: {device}\")\n\n# # ==========================================\n# # 2. 数据处理与增强 (与训练保持一致)\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#         # 兼容绝对路径和相对路径\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: Image not found {p}, creating 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 = self._read_image(str(row[\"image_path\"]))\n        \n#         if self.transform:\n#             img = self.transform(image=img)[\"image\"]\n            \n#         return {\"image\": img}\n\n# # ==========================================\n# # 3. 模型定义 (必须与训练完全一致)\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#         # ⚠️ 关键修改: pretrained=False\n#         # 在提交时必须设为 False，因为没有网络下载权重\n#         # 权重会通过 load_state_dict 加载\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#     \"\"\"\n#     def _pred(x):\n#         return model(x)\n\n#     if mode == \"none\":\n#         return _pred(imgs)\n\n#     if mode == \"flip4\":\n#         # 原图 + 水平翻转 + 垂直翻转 + 水平垂直翻转\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 inference():\n#     # 1. 准备测试数据\n#     test_df = pd.read_csv(PATHS[\"TEST_CSV\"])\n    \n#     # 提取唯一的图片列表，避免重复预测（test.csv 是长格式，一张图对应多行）\n#     unique_images = test_df[\"image_path\"].astype(str).unique().tolist()\n#     img_df = pd.DataFrame({\"image_path\": unique_images})\n    \n#     # 建立映射: image_path -> index\n#     img2idx = {p: i for i, p in enumerate(unique_images)}\n#     # 建立映射: target_name -> column index (0-4)\n#     target2col = {t: i for i, t in enumerate(CFG.target_order)}\n    \n#     ds = BiomassDataset(\n#         img_df, \n#         image_root=PATHS[\"IMAGE_ROOT\"], \n#         transform=get_valid_transform(CFG.img_size)\n#     )\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#     # 存储 Log 空间的结果累加\n#     y_log_sum = np.zeros((len(unique_images), CFG.n_targets), dtype=np.float32)\n#     models_found = 0\n    \n#     # 2. 遍历所有 Fold 模型进行集成\n#     # 假设你保存的文件名为 model_fold0.pth 到 model_fold3.pth\n#     for fold in range(5): # 如果你是4折，这里改为 range(4)\n#         ckpt_path = os.path.join(PATHS[\"MODEL_DIR\"], f\"model_fold{fold}.pth\")\n        \n#         if not os.path.exists(ckpt_path):\n#             print(f\"⚠️ Model not found: {ckpt_path}, skipping...\")\n#             continue\n            \n#         print(f\"Load fold {fold} from {ckpt_path}...\")\n        \n#         # 初始化模型\n#         model = BiomassNet(CFG.backbone, n_targets=CFG.n_targets)\n#         state_dict = torch.load(ckpt_path, map_location=device)[\"model\"]\n#         model.load_state_dict(state_dict)\n#         model.to(device)\n#         model.eval()\n        \n#         preds = []\n#         with torch.no_grad():\n#             for batch in tqdm(loader, desc=f\"Fold {fold}\"):\n#                 imgs = batch[\"image\"].to(device)\n#                 # TTA 预测\n#                 p = tta_predict_log5(model, imgs, mode=CFG.tta_mode)\n#                 preds.append(p.cpu().numpy())\n                \n#         y_log_sum += np.concatenate(preds, axis=0)\n#         models_found += 1\n        \n#         # 清理显存\n#         del model\n#         gc.collect()\n#         torch.cuda.empty_cache()\n\n#     if models_found == 0:\n#         raise FileNotFoundError(\"No models found! Check PATHS['MODEL_DIR']\")\n        \n#     print(f\"Ensembled {models_found} models.\")\n    \n#     # 3. 平均与后处理\n#     y_log_avg = y_log_sum / models_found\n#     y_log_avg = np.maximum(y_log_avg, 0.0) # 防止负值\n    \n#     # 转换回线性空间 (Log1p -> Expm1)\n#     y_lin = np.expm1(y_log_avg)\n#     y_lin = np.maximum(y_lin, 0.0)\n    \n#     # --- 物理一致性修正 (Consistency) ---\n#     if CFG.enforce_consistency_infer:\n#         print(\"Applying Physics Consistency (Total)...\")\n#         # Total ≈ (Predicted_Total + (GDM + Dead)) / 2\n#         # Index: 0:Green, 1:Dead, 2:Clover, 3:GDM, 4:Total\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#             print(\"Applying Physics Consistency (GDM)...\")\n#             # GDM ≈ (Predicted_GDM + (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#     # 4. 生成提交文件\n#     # 比赛要求长格式: sample_id, target\n#     submission_rows = []\n    \n#     print(\"Generating submission file...\")\n#     for i, row in tqdm(test_df.iterrows(), total=len(test_df)):\n#         img_path = str(row[\"image_path\"])\n#         target_name = str(row[\"target_name\"])\n        \n#         # 查找图片对应的索引\n#         img_idx = img2idx[img_path]\n#         # 查找目标对应的列索引\n#         col_idx = target2col[target_name]\n        \n#         # 获取预测值\n#         pred_val = y_lin[img_idx, col_idx]\n        \n#         submission_rows.append({\n#             \"sample_id\": str(row[\"sample_id\"]),\n#             \"target\": float(pred_val)\n#         })\n        \n#     sub_df = pd.DataFrame(submission_rows)\n#     sub_df.to_csv(PATHS[\"SUBMISSION_CSV\"], index=False)\n#     print(f\"✅ Submission saved to {PATHS['SUBMISSION_CSV']}\")\n#     print(sub_df.head())\n\n# if __name__ == \"__main__\":\n#     try:\n#         inference()\n#     except Exception as e:\n#         print(f\"❌ Error during inference: {e}\")\n#         # 打印详细错误方便调试\n#         import traceback\n#         traceback.print_exc()","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport sys\nimport gc\nimport cv2\nimport math\nimport torch\nimport numpy as np\nimport pandas as pd\nimport torch.nn as nn\nimport albumentations as A\nimport torch.nn.functional as F\nfrom torch.utils.data import Dataset, DataLoader\nfrom albumentations.pytorch import ToTensorV2\nfrom tqdm import tqdm\nimport timm\n\n# ==========================================\n# 1. 配置与路径 (USER CONFIG)\n# ==========================================\nclass CFG:\n    # 必须与训练时的 backbone 一致\n    backbone = \"convnext_tiny.fb_in22k_ft_in1k\" \n    \n    img_size = 384\n    batch_size = 32\n    num_workers = 2\n    seed = 42\n    \n    # 目标列定义\n    target_order = [\"Dry_Green_g\", \"Dry_Dead_g\", \"Dry_Clover_g\", \"GDM_g\", \"Dry_Total_g\"]\n    n_targets = 5\n    \n    # TTA 模式 (保持与训练一致): \"flip4\"\n    tta_mode = \"flip4\"\n    \n    # 物理一致性后处理开关\n    enforce_consistency_infer = True\n    enforce_consistency_infer_gdm = True\n\n# --- ⚠️ 请根据你的 Kaggle Dataset 修改这里 ⚠️ ---\nPATHS = {\n    # 你的模型权重文件夹 (例如: /kaggle/input/my-biomass-models/)\n    \"MODEL_DIR\": \"/kaggle/input/image/pytorch/default/28\", \n    \n    # 官方测试集图片路径\n    \"IMAGE_ROOT\": \"/kaggle/input/csiro-biomass\",\n    \n    # 官方测试集 CSV 路径\n    \"TEST_CSV\": \"/kaggle/input/csiro-biomass/test.csv\",\n    \n    # 输出文件路径\n    \"SUBMISSION_CSV\": \"submission.csv\"\n}\n\n# 自动检测设备\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nprint(f\"Using device: {device}\")\n\n# ==========================================\n# 2. 数据处理 (只保留验证集变换)\n# ==========================================\n# Mixup/Cutmix 是训练时的 Batch 操作，推理时只需要标准的 Resize 和 Normalize\nIMAGENET_MEAN = (0.485, 0.456, 0.406)\nIMAGENET_STD  = (0.229, 0.224, 0.225)\n\ndef 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\nclass 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            # 容错处理\n            print(f\"Warning: Image not found {p}, using black placeholder.\")\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 = self._read_image(str(row[\"image_path\"]))\n        \n        if self.transform:\n            img = self.transform(image=img)[\"image\"]\n            \n        return {\"image\": img}\n\n# ==========================================\n# 3. 模型定义 (pretrained=False)\n# ==========================================\nclass BiomassNet(nn.Module):\n    def __init__(self, backbone_name: str, n_targets: int = 5, feat_dim: int = 768):\n        super().__init__()\n        # ⚠️ 关键: 推理时设为 False，避免联网下载\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 (Test Time Augmentation)\n# ==========================================\n@torch.no_grad()\ndef tta_predict_log5(model, imgs, mode=\"flip4\"):\n    \"\"\"\n    这里不做 Mixup/Cutmix，只做几何变换 (Flip/Rotate)\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    # 如果你在训练里写了 d4，这里也可以支持\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# ==========================================\n# 5. 主推理逻辑\n# ==========================================\ndef inference():\n    # 1. 准备测试数据\n    test_df = pd.read_csv(PATHS[\"TEST_CSV\"])\n    \n    # 提取唯一图片 (去重)\n    unique_images = test_df[\"image_path\"].astype(str).unique().tolist()\n    img_df = pd.DataFrame({\"image_path\": unique_images})\n    \n    # 建立映射索引\n    img2idx = {p: i for i, p in enumerate(unique_images)}\n    target2col = {t: i for i, t in enumerate(CFG.target_order)}\n    \n    ds = BiomassDataset(\n        img_df, \n        image_root=PATHS[\"IMAGE_ROOT\"], \n        transform=get_valid_transform(CFG.img_size)\n    )\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    # 存储预测结果 (Log space)\n    y_log_sum = np.zeros((len(unique_images), CFG.n_targets), dtype=np.float32)\n    models_found = 0\n    \n    # 2. 遍历 Fold 模型 (集成)\n    # 假设你有 4 折 (0,1,2,3)\n    # 如果你跑了5折，请把 range(4) 改为 range(5)\n    for fold in range(4): \n        ckpt_path = os.path.join(PATHS[\"MODEL_DIR\"], f\"model_fold{fold}.pth\")\n        \n        if not os.path.exists(ckpt_path):\n            print(f\"⚠️ Model not found: {ckpt_path}, skipping...\")\n            continue\n            \n        print(f\"Loading fold {fold} from {ckpt_path}...\")\n        \n        # 加载模型\n        model = BiomassNet(CFG.backbone, n_targets=CFG.n_targets)\n        state_dict = torch.load(ckpt_path, map_location=device)[\"model\"]\n        model.load_state_dict(state_dict)\n        model.to(device)\n        model.eval()\n        \n        preds = []\n        with torch.no_grad():\n            for batch in tqdm(loader, desc=f\"Infer Fold {fold}\"):\n                imgs = batch[\"image\"].to(device)\n                # 执行 TTA 预测\n                p = tta_predict_log5(model, imgs, mode=CFG.tta_mode)\n                preds.append(p.cpu().numpy())\n                \n        y_log_sum += np.concatenate(preds, axis=0)\n        models_found += 1\n        \n        # 释放显存\n        del model\n        gc.collect()\n        torch.cuda.empty_cache()\n\n    if models_found == 0:\n        raise FileNotFoundError(f\"No models found in {PATHS['MODEL_DIR']}!\")\n        \n    print(f\"Ensembled {models_found} models.\")\n    \n    # 3. 后处理 (Log -> Linear -> Consistency)\n    y_log_avg = y_log_sum / models_found\n    y_log_avg = np.maximum(y_log_avg, 0.0) # 修正负值\n    \n    # 还原到克数空间 (expm1)\n    y_lin = np.expm1(y_log_avg)\n    y_lin = np.maximum(y_lin, 0.0)\n    \n    # --- 物理一致性修正 (Physics Consistency) ---\n    if CFG.enforce_consistency_infer:\n        print(\"Applying Physics Consistency (Total)...\")\n        # Total ≈ (Direct + (GDM + Dead)) / 2\n        # Index: 0:Green, 1:Dead, 2:Clover, 3:GDM, 4:Total\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            print(\"Applying Physics Consistency (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    # 4. 生成提交文件 (长格式)\n    print(\"Generating submission file...\")\n    submission_rows = []\n    \n    for i, row in tqdm(test_df.iterrows(), total=len(test_df)):\n        img_path = str(row[\"image_path\"])\n        target_name = str(row[\"target_name\"])\n        \n        img_idx = img2idx[img_path]\n        col_idx = target2col[target_name]\n        \n        pred_val = y_lin[img_idx, col_idx]\n        \n        submission_rows.append({\n            \"sample_id\": str(row[\"sample_id\"]),\n            \"target\": float(pred_val)\n        })\n        \n    sub_df = pd.DataFrame(submission_rows)\n    sub_df.to_csv(PATHS[\"SUBMISSION_CSV\"], index=False)\n    print(f\"✅ Submission saved to {PATHS['SUBMISSION_CSV']}\")\n    print(sub_df.head())\n\nif __name__ == \"__main__\":\n    try:\n        inference()\n    except Exception as e:\n        print(f\"❌ Error: {e}\")\n        import traceback\n        traceback.print_exc()","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"pd.read_csv(\"submission.csv\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# import matplotlib.pyplot as plt\n# import numpy as np\n# import cv2\n# import albumentations as A\n# from albumentations.pytorch import ToTensorV2\n# import os\n\n# # === 你的配置 ===\n# IMAGE_ROOT = \"/kaggle/input/csiro-biomass\" # 修改为你实际的图片路径\n# TEST_CSV_PATH = \"/kaggle/input/csiro-biomass/test.csv\"\n# # ===============\n\n# def check_pipeline():\n#     # 1. 读取 CSV\n#     if not os.path.exists(TEST_CSV_PATH):\n#         print(\"找不到 test.csv\")\n#         return\n#     df = pd.read_csv(TEST_CSV_PATH)\n    \n#     # 取第一张图\n#     img_rel_path = df.iloc[0]['image_path']\n#     full_path = os.path.join(IMAGE_ROOT, img_rel_path)\n    \n#     print(f\"尝试读取: {full_path}\")\n    \n#     # 2. 模拟读取流程\n#     img_bgr = cv2.imread(full_path)\n#     if img_bgr is None:\n#         print(f\"❌ 严重错误: 无法读取图片！请检查 IMAGE_ROOT 设置。\")\n#         print(f\"当前设置的根目录: {os.path.abspath(IMAGE_ROOT)}\")\n#         return\n    \n#     # 转 RGB\n#     img_rgb = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB)\n    \n#     # 3. 模拟预处理 (Transform)\n#     transform = A.Compose([\n#         A.Resize(384, 384),\n#         A.Normalize(mean=(0.485,0.456,0.406), std=(0.229,0.224,0.225)),\n#         ToTensorV2(),\n#     ])\n    \n#     augmented = transform(image=img_rgb)[\"image\"] # Tensor [3, H, W]\n    \n#     # 4. 逆归一化以便显示 (Denormalize)\n#     # 因为输入给模型的图经过了 Normalize，人眼看是花的，我们需要还原回来检查是否正确\n#     mean = np.array([0.485, 0.456, 0.406])\n#     std = np.array([0.229, 0.224, 0.225])\n    \n#     img_show = augmented.permute(1, 2, 0).cpu().numpy() # [H, W, 3]\n#     img_show = (img_show * std + mean) * 255.0\n#     img_show = np.clip(img_show, 0, 255).astype(np.uint8)\n    \n#     # 5. 绘图对比\n#     plt.figure(figsize=(10, 5))\n    \n#     plt.subplot(1, 2, 1)\n#     plt.imshow(img_rgb)\n#     plt.title(\"原始读取 (RGB)\")\n#     plt.axis(\"off\")\n    \n#     plt.subplot(1, 2, 2)\n#     plt.imshow(img_show)\n#     plt.title(\"模型输入 (预处理后还原)\")\n#     plt.axis(\"off\")\n    \n#     plt.show()\n#     print(\"✅ 如果右边的图看起来和左边基本一样（只有尺寸变化），说明数据管线没问题。\")\n\n# check_pipeline()","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}