{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceId":112509,"databundleVersionId":14254895,"sourceType":"competition"},{"sourceId":14373418,"sourceType":"datasetVersion","datasetId":8923696},{"sourceId":14542734,"sourceType":"datasetVersion","datasetId":8618425},{"sourceId":14571539,"sourceType":"datasetVersion","datasetId":9250969},{"sourceId":14633584,"sourceType":"datasetVersion","datasetId":9314440},{"sourceId":272104372,"sourceType":"kernelVersion"},{"sourceId":284954186,"sourceType":"kernelVersion"}],"dockerImageVersionId":31154,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"! uv pip install --system --no-index --find-links='/kaggle/input/latest-criro-whls/whls' 'timm==1.0.22'","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-28T06:19:57.525605Z","iopub.execute_input":"2026-01-28T06:19:57.525831Z","iopub.status.idle":"2026-01-28T06:20:41.707625Z","shell.execute_reply.started":"2026-01-28T06:19:57.525807Z","shell.execute_reply":"2026-01-28T06:20:41.706953Z"},"scrolled":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"%%writefile infer_v1.py\nfrom accelerate import Accelerator\nimport os\nimport pandas as pd\nimport numpy as np\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import Dataset, DataLoader\nimport albumentations as A\nfrom albumentations.pytorch import ToTensorV2\nimport timm\nimport cv2\nfrom tqdm import tqdm\nimport gc\n\n\nimport argparse\n\nparser = argparse.ArgumentParser(description='gamma inference')\nparser.add_argument('--model_dir', type=str, default = '/kaggle/input/qwen2.5/transformers/32b-instruct-awq/1')\nparser.add_argument('--model_name', type=str, default = 'vit_huge_plus_patch16_dinov3.lvd1689m')\nparser.add_argument('--output_file', type=str, default = 'submission.json')\nparser.add_argument('--image_size', type=int, default = 448)\n\n\nargs = parser.parse_args()\nprint(args)\n\n\nclass CFG:\n    BASE_PATH = '/kaggle/input/csiro-biomass'\n    TEST_CSV = os.path.join(BASE_PATH, 'test.csv')\n    TEST_IMAGE_DIR = os.path.join(BASE_PATH, 'test')\n    SUBMISSION_FILE = 'submission.csv'\n    \n\n    MODEL_DIR = '/kaggle/input/csiro-model-ckpt/1209_vit_huge_fold3_all_v1_70_9466/1209_vit_huge_fold3_all_v1_70_9466/model_fold-1_epoch30_0.9466.pth' # Giả sử 5 file .pth nằm cùng thư mục\n    MODEL_NAME = 'vit_huge_plus_patch16_dinov3.lvd1689m'\n    IMG_SIZE = 448\n\n    \n    DEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n    BATCH_SIZE = 1 \n    NUM_WORKERS = 1\n    N_FOLDS = 5\n    \n    # --- Mục tiêu & Loss (PHẢI TRÙNG KHỚP) ---\n    # 3 mục tiêu model đã dự đoán\n    TARGET_COLS = ['Dry_Green_g', 'Dry_Dead_g', 'Dry_Clover_g', 'GDM_g', 'Dry_Total_g']\n    \n    # 5 mục tiêu để nộp bài\n    ALL_TARGET_COLS = ['Dry_Green_g', 'Dry_Dead_g', 'Dry_Clover_g', 'GDM_g', 'Dry_Total_g']\n\n\nCFG.SUBMISSION_FILE = args.output_file\nCFG.MODEL_DIR = args.model_dir\nCFG.MODEL_NAME = args.model_name\nCFG.IMG_SIZE = args.image_size\n\n\n\n# ===============================================================\n# 2. 🏞️ AUGMENTATIONS (CHỈ DÙNG VALIDATION)\n# ===============================================================\nfrom albumentations import (\n    Compose, \n    Resize, \n    Normalize,\n    HorizontalFlip, \n    VerticalFlip\n)\n\n\ndef get_tta_transforms():\n \n    # -----------------\n    original_view = Compose([\n        \n        Normalize(\n            mean=[0.485, 0.456, 0.406],\n            std=[0.229, 0.224, 0.225]\n        ),\n        Resize(CFG.IMG_SIZE, CFG.IMG_SIZE),\n        ToTensorV2()\n    ])\n    \n    # -----------------\n    # View 2: Lật ngang (HFlip)\n    # -----------------\n    hflip_view = Compose([\n        HorizontalFlip(p=1.0), # Luôn luôn lật\n        Normalize(\n            mean=[0.485, 0.456, 0.406],\n            std=[0.229, 0.224, 0.225]\n        ),\n        Resize(CFG.IMG_SIZE, CFG.IMG_SIZE),\n        ToTensorV2()\n    ])\n    \n    # -----------------\n    # View 3: Lật dọc (VFlip)\n    # -----------------\n    vflip_view = Compose([\n        VerticalFlip(p=1.0), # Luôn luôn lật\n        Normalize(\n            mean=[0.485, 0.456, 0.406],\n            std=[0.229, 0.224, 0.225]\n        ),\n        Resize(CFG.IMG_SIZE, CFG.IMG_SIZE),\n        ToTensorV2()\n    ])\n    \n    return [original_view, hflip_view, vflip_view]\n\n\n\n\nclass TestBiomassDataset(Dataset):\n    \"\"\"\n    \"\"\"\n    def __init__(self, df, transform_pipeline, image_dir):\n        self.df = df\n        self.transforms = transform_pipeline \n        self.image_dir = image_dir\n        self.image_paths = df['image_path'].values\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        # 1. Lấy thông tin\n        img_path_suffix = self.image_paths[idx]\n        \n        # 2. Đọc ảnh gốc (2000x1000)\n        filename = os.path.basename(img_path_suffix)\n        full_path = os.path.join(self.image_dir, filename)\n        \n        image = cv2.imread(full_path)\n        if image is None:\n            print(f\"没有图片: {full_path}. 找到\")\n            image = np.zeros((1000, 2000, 3), dtype=np.uint8)\n        \n        image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)\n        \n        height, width, _ = image.shape\n        mid_point = width // 2\n        img_left = image[:, :mid_point]\n        img_right = image[:, mid_point:]\n        \n        img_left_tensor = self.transforms(image=img_left)['image']\n        img_right_tensor = self.transforms(image=img_right)['image']\n        \n        return idx, img_left_tensor, img_right_tensor\n\nclass BiomassModel(nn.Module):\n    \"\"\"\n    Kiến trúc mô hình (Hai luồng, Ba đầu ra)\n    PHẢI GIỐNG HỆT file training.\n    \"\"\"\n    def __init__(self, model_name, pretrained, n_targets=3):\n        super(BiomassModel, self).__init__()\n        \n        self.backbone = timm.create_model(\n            model_name,\n            pretrained=pretrained, # Sẽ là False khi inference\n            num_classes=0,\n            global_pool='avg'\n        )\n        \n        self.n_features = self.backbone.num_features\n        self.n_combined_features = self.n_features * 2\n        \n         # ---Dry_Green_g ---\n        self.head_green = nn.Sequential(\n            nn.Linear(self.n_combined_features, self.n_combined_features // 2),\n            nn.ReLU(),\n            nn.Dropout(0.3),\n            nn.Linear(self.n_combined_features // 2, 1)\n        )\n        \n        # Dry_Dead_g\n        self.head_dead = nn.Sequential(\n            nn.Linear(self.n_combined_features, self.n_combined_features // 2),\n            nn.ReLU(),\n            nn.Dropout(0.3),\n            nn.Linear(self.n_combined_features // 2, 1) \n        )\n\n        # Dry_Clover_g\n        self.head_clover = nn.Sequential(\n            nn.Linear(self.n_combined_features, self.n_combined_features // 2),\n            nn.ReLU(),\n            nn.Dropout(0.3),\n            nn.Linear(self.n_combined_features // 2, 1) \n        )\n\n        \n        \n        # --- GDM_g ---\n        self.head_gdm = nn.Sequential(\n            nn.Linear(self.n_combined_features, self.n_combined_features // 2),\n            nn.ReLU(),\n            nn.Dropout(0.3),\n            nn.Linear(self.n_combined_features // 2, 1)\n        )\n        # --- Dry_Total_g 头 ---\n        self.head_total = nn.Sequential(\n            nn.Linear(self.n_combined_features, self.n_combined_features // 2),\n            nn.ReLU(),\n            nn.Dropout(0.3),\n            nn.Linear(self.n_combined_features // 2, 1) \n        )\n\n    def forward(self, img_left, img_right):\n        features_left = self.backbone(img_left)\n        features_right = self.backbone(img_right)\n        combined = torch.cat([features_left, features_right], dim=1)\n        \n        # 4. 输出\n        out_green = self.head_green(combined)\n        out_dead = self.head_dead(combined)\n        out_clover = self.head_clover(combined)\n        out_gdm = self.head_gdm(combined)\n        out_total = self.head_total(combined)\n\n        out_clover = out_clover * 0.8\n        if out_dead > 20:\n            out_dead *= 1.1\n        elif out_dead < 10:\n            out_dead *= 0.9\n            \n        out_gdm = 0.5*out_gdm+ 0.5*(out_green+out_clover)\n        \n        pred_total1 = out_green + out_clover + out_dead\n        pred_total2 = out_gdm + out_dead\n        out_total = 0.5*out_total + 0.5*pred_total1 + 0.0*pred_total2\n        \n        return out_green, out_dead, out_clover, out_gdm, out_total\n\n\ndef post_process_biomass(df_preds):\n    \"\"\"\n    Enforces physical mass balance constraints on biomass predictions.\n    \n    Constraints enforced:\n    1. Dry_Green_g + Dry_Clover_g = GDM_g\n    2. GDM_g + Dry_Dead_g = Dry_Total_g\n    \n    Method:\n    Uses Orthogonal Projection. It finds the set of values that satisfy\n    the constraints while minimizing the Euclidean distance to the \n    original model predictions.\n    \n    Args:\n        df_preds (pd.DataFrame): DataFrame containing the 5 prediction columns.\n        \n    Returns:\n        pd.DataFrame: A new DataFrame with consistent, non-negative values.\n    \"\"\"\n    # 1. Define the specific order required for the math\n    # We treat the vector x as: [Green, Clover, Dead, GDM, Total]\n    ordered_cols = [\"Dry_Green_g\", \"Dry_Dead_g\", \"Dry_Clover_g\", \"GDM_g\", \"Dry_Total_g\"]\n    \n    # Check if columns exist\n    if not all(col in df_preds.columns for col in ordered_cols):\n        missing = [c for c in ordered_cols if c not in df_preds.columns]\n        raise ValueError(f\"Input DataFrame is missing columns: {missing}\")\n\n    # 2. Extract values in the specific order -> Shape (N_samples, 5)\n    Y = df_preds[ordered_cols].values.T  # Transpose to (5, N) for matrix math\n\n    # 3. Define the Constraint Matrix C\n    # We want Cx = 0\n    # Eq 1: 1*Green + 0*Dead + 1*Clover - 1*GDM + 0*Total = 0\n    # Eq 2: 0*Green + 1*Dead + 0*Clover + 1*GDM - 1*Total = 0\n    C = np.array([\n        [1, 0, 1, -1,  0],\n        [0, 1, 0,  1, -1]\n    ])\n\n    # 4. Calculate Projection Matrix P\n    # P = I - C^T * (C * C^T)^-1 * C\n    # This projects any vector onto the null space of C (the valid subspace)\n    C_T = C.T\n    inv_CCt = np.linalg.inv(C @ C_T)\n    P = np.eye(5) - C_T @ inv_CCt @ C\n\n    # 5. Apply Projection\n    # Y_new = P * Y\n    Y_reconciled = P @ Y\n\n    # 6. Transpose back to (N, 5)\n    Y_reconciled = Y_reconciled.T\n\n    # 7. Post-correction for negatives\n    # Projection can mathematically create negative values (e.g. if Total was predicted 0)\n    # We clip to 0. Note: This might slightly break the sum equality again, \n    # but exact equality with negatives is physically impossible anyway.\n    Y_reconciled = Y_reconciled.clip(min=0)\n\n    # 8. Create Output DataFrame\n    df_out = df_preds.copy()\n    df_out[ordered_cols] = Y_reconciled\n\n    return df_out\n\n@torch.no_grad()\ndef predict_one_view_accelerate(models_list, test_loader, accelerator):\n    device = accelerator.device\n\n    # 只在主进程收集最终拼接结果\n    idx_buf = []\n    total_buf, gdm_buf, green_buf, clover_buf, dead_buf = [], [], [], [], []\n\n    pbar = tqdm(test_loader, desc=\"  Predicting View\", leave=False, disable=not accelerator.is_main_process)\n\n    for batch in pbar:\n        idx, img_left, img_right = batch\n        idx = idx.to(device, non_blocking=True)\n        img_left = img_left.to(device, non_blocking=True)\n        img_right = img_right.to(device, non_blocking=True)\n\n        # ensemble\n        pred_total_list, pred_gdm_list, pred_green_list, pred_clover_list, pred_dead_list = [], [], [], [], []\n        with accelerator.autocast():\n            for model in models_list:\n                pred_green, pred_dead, pred_clover, pred_gdm, pred_total = model(img_left, img_right)\n                pred_total_list.append(pred_total)\n                pred_gdm_list.append(pred_gdm)\n                pred_green_list.append(pred_green)\n                pred_clover_list.append(pred_clover)\n                pred_dead_list.append(pred_dead)\n\n            avg_pred_total = torch.mean(torch.stack(pred_total_list), dim=0)\n            avg_pred_gdm = torch.mean(torch.stack(pred_gdm_list), dim=0)\n            avg_pred_green = torch.mean(torch.stack(pred_green_list), dim=0)\n            avg_pred_clover = torch.mean(torch.stack(pred_clover_list), dim=0)\n            avg_pred_dead = torch.mean(torch.stack(pred_dead_list), dim=0)\n\n        # ✅ 关键修改：跨进程 gather，并自动去掉末尾重复数据（用于 metrics/推理汇总）\n        g_idx = accelerator.gather_for_metrics(idx)\n        g_total = accelerator.gather_for_metrics(avg_pred_total)\n        g_gdm = accelerator.gather_for_metrics(avg_pred_gdm)\n        g_green = accelerator.gather_for_metrics(avg_pred_green)\n        g_clover = accelerator.gather_for_metrics(avg_pred_clover)\n        g_dead = accelerator.gather_for_metrics(avg_pred_dead)\n\n        if accelerator.is_main_process:\n            idx_buf.append(g_idx.detach().cpu())\n            total_buf.append(g_total.detach().cpu())\n            gdm_buf.append(g_gdm.detach().cpu())\n            green_buf.append(g_green.detach().cpu())\n            clover_buf.append(g_clover.detach().cpu())\n            dead_buf.append(g_dead.detach().cpu())\n\n    if not accelerator.is_main_process:\n        return None\n\n    idx_all = torch.cat(idx_buf, dim=0).numpy()\n    order = np.argsort(idx_all)\n\n    total_all = torch.cat(total_buf, dim=0).numpy().reshape(-1)[order]\n    gdm_all = torch.cat(gdm_buf, dim=0).numpy().reshape(-1)[order]\n    green_all = torch.cat(green_buf, dim=0).numpy().reshape(-1)[order]\n    clover_all = torch.cat(clover_buf, dim=0).numpy().reshape(-1)[order]\n    dead_all = torch.cat(dead_buf, dim=0).numpy().reshape(-1)[order]\n\n    return {\n        'total': total_all,\n        'gdm': gdm_all,\n        'green': green_all,\n        'clover': clover_all,\n        'dead': dead_all\n    }\n\n\ndef run_inference_with_tta():\n    \"\"\"\n    \"\"\"\n    # ✅ 关键修改：引入 accelerate\n    accelerator = Accelerator()\n    device = accelerator.device\n\n    accelerator.print(f\"\\n{'='*50}\")\n    accelerator.print(\"🚀 TTA (Accelerate multi-GPU) 🚀\")\n    accelerator.print(f\"{'='*50}\")\n\n    accelerator.print(f\"test file {CFG.TEST_CSV}...\")\n\n    \n    try:\n        test_df_long = pd.read_csv(CFG.TEST_CSV)\n        test_df_unique = test_df_long.drop_duplicates(subset=['image_path']).reset_index(drop=True)\n        print(f\"image  {len(test_df_unique)} .\")\n    except FileNotFoundError:\n        print(f\"没有发现 {CFG.TEST_CSV}\")\n        return None, None, None\n\n    accelerator.print(\"\\n加载模型...\")\n    models_list = []\n\n\n    model = BiomassModel(CFG.MODEL_NAME, pretrained=False)\n    model.load_state_dict(torch.load(CFG.MODEL_DIR, map_location='cpu'))\n\n    model.eval()\n    model.to(device)\n    models_list.append(model)\n\n\n    accelerator.print(f\"✓ 一共 {len(models_list)} 个模型.\")\n\n    tta_transforms = get_tta_transforms()\n    accelerator.print(f\"\\使用tta {len(tta_transforms)} TTA 个数...\")\n    \n    all_tta_view_preds = [] # List để lưu kết quả của mỗi view TTA\n\n    for i, tta_transform in enumerate(tta_transforms):\n        accelerator.print(f\"---  TTA View {i+1}/{len(tta_transforms)} ---\")\n        \n        # Tạo Dataset/Loader MỚI cho view TTA này\n        test_dataset = TestBiomassDataset(\n            df=test_df_unique,\n            transform_pipeline=tta_transform, # Truyền pipeline TTA\n            image_dir=CFG.TEST_IMAGE_DIR\n        )\n        test_loader = DataLoader(\n            test_dataset,\n            batch_size=CFG.BATCH_SIZE,\n            shuffle=False,\n            num_workers=CFG.NUM_WORKERS,\n            pin_memory=True\n        )\n        test_loader = accelerator.prepare(test_loader)\n        # Chạy ensemble 5-fold cho view này\n        view_preds_np = predict_one_view_accelerate(models_list, test_loader, accelerator)\n        if accelerator.is_main_process:\n            all_tta_view_preds.append(view_preds_np)\n            accelerator.print(f\"✓完成 TTA View {i+1}\")\n    if not accelerator.is_main_process:\n        return None, None, None\n    # --- 4. Ensemble (Lấy trung bình) kết quả TTA ---\n    accelerator.print(\"\\n处理 TTA views...\")\n    final_ensembled_preds = {\n        'total': np.mean([d['total'] for d in all_tta_view_preds], axis=0),\n        'gdm':   np.mean([d['gdm'] for d in all_tta_view_preds], axis=0),\n        'green': np.mean([d['green'] for d in all_tta_view_preds], axis=0),\n        'clover': np.mean([d['clover'] for d in all_tta_view_preds], axis=0),\n        'dead': np.mean([d['dead'] for d in all_tta_view_preds], axis=0)\n        \n    }\n    \n    accelerator.print(\"✓ 完成推理.\")\n    \n    del models_list, test_loader, test_dataset\n    gc.collect()\n    torch.cuda.empty_cache()\n    \n    return final_ensembled_preds, test_df_long, test_df_unique\n# ===============================================================\n# 6. ✍️ HÀM TẠO FILE SUBMISSION\n# ===============================================================\ndef create_submission(preds_np, test_df_long, test_df_unique):\n    \"\"\"\n    \"\"\"\n    if preds_np is None:\n        print(\"Bỏ qua tạo submission do lỗi ở trên.\")\n        return\n\n    print(\"\\nĐang hậu xử lý và tạo file submission...\")\n\n    # 1. Lấy 3 dự đoán đã ensemble\n    pred_total_final = preds_np['total']\n    pred_gdm_final = preds_np['gdm']\n    pred_green_final = preds_np['green']\n\n    pred_clover_final = preds_np['clover']\n    pred_dead_final = preds_np['dead']\n\n    # 1. 确保非负\n    pred_green_final = np.clip(pred_green_final, a_min=0, a_max=None)\n    pred_clover_final = np.clip(pred_clover_final, a_min=0, a_max=None)\n    pred_dead_final = np.clip(pred_dead_final, a_min=0, a_max=None)\n    \n\n\n    preds_wide_df = pd.DataFrame({\n        'image_path': test_df_unique['image_path'],\n        'Dry_Green_g': pred_green_final,\n        'Dry_Dead_g': pred_dead_final,\n        'Dry_Clover_g': pred_clover_final,\n        'GDM_g': pred_gdm_final,\n        'Dry_Total_g': pred_total_final\n    })\n    preds_wide_df = post_process_biomass(preds_wide_df)\n\n    preds_long_df = preds_wide_df.melt(\n        id_vars=['image_path'],\n        value_vars=CFG.ALL_TARGET_COLS, # \n        var_name='target_name',        # \n        value_name='target'            # \n    )\n\n    submission_df = pd.merge(\n        test_df_long[['sample_id', 'image_path', 'target_name']],\n        preds_long_df,\n        on=['image_path', 'target_name'],\n        how='left'\n    )\n\n    submission_df = submission_df[['sample_id', 'target']]\n    \n    # Lưu file\n    submission_df.to_csv(CFG.SUBMISSION_FILE, index=False)\n\n    print(f\"\\n🎉 HOÀN TẤT! Đã lưu file submission tại: {CFG.SUBMISSION_FILE}\")\n    print(submission_df.head())\n    print(submission_df.tail())\n    \nif __name__ == \"__main__\":\n    all_preds_np, df_long, df_unique = run_inference_with_tta()\n    # ✅ 关键修改：只在主进程写文件（避免多进程冲突）\n    accelerator = Accelerator()\n    if accelerator.is_main_process:\n        create_submission(all_preds_np, df_long, df_unique)","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2026-01-28T06:20:41.727591Z","iopub.execute_input":"2026-01-28T06:20:41.727824Z","iopub.status.idle":"2026-01-28T06:20:41.781893Z","shell.execute_reply.started":"2026-01-28T06:20:41.727799Z","shell.execute_reply":"2026-01-28T06:20:41.781382Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"%%writefile infer_v2.py\nfrom accelerate import Accelerator\nimport os\nimport pandas as pd\nimport numpy as np\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import Dataset, DataLoader\nimport albumentations as A\nfrom albumentations.pytorch import ToTensorV2\nimport timm\nimport cv2\nfrom tqdm import tqdm\nimport gc\n\nimport torch\nimport math\nimport torch.nn.functional as F\nimport torch.utils.checkpoint as checkpoint\nfrom timm.models.layers import DropPath, to_2tuple, trunc_normal_\n\nimport argparse\n\nparser = argparse.ArgumentParser(description='gamma inference')\nparser.add_argument('--model_dir', type=str, default = '/kaggle/input/qwen2.5/transformers/32b-instruct-awq/1')\nparser.add_argument('--model_name', type=str, default = 'vit_huge_plus_patch16_dinov3.lvd1689m')\nparser.add_argument('--output_file', type=str, default = 'submission.json')\nparser.add_argument('--image_size', type=int, default = 448)\n\n\nargs = parser.parse_args()\nprint(args)\n\n\nclass CFG:\n    BASE_PATH = '/kaggle/input/csiro-biomass'\n    TEST_CSV = os.path.join(BASE_PATH, 'test.csv')\n    TEST_IMAGE_DIR = os.path.join(BASE_PATH, 'test')\n    SUBMISSION_FILE = 'submission.csv'\n    \n\n    MODEL_DIR = '/kaggle/input/csiro-model-ckpt/1209_vit_huge_fold3_all_v1_70_9466/1209_vit_huge_fold3_all_v1_70_9466/model_fold-1_epoch30_0.9466.pth' # Giả sử 5 file .pth nằm cùng thư mục\n    MODEL_NAME = 'vit_huge_plus_patch16_dinov3.lvd1689m'\n    IMG_SIZE = 448\n\n    \n    DEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n    BATCH_SIZE = 1 \n    NUM_WORKERS = 1\n    N_FOLDS = 5\n    \n    # --- Mục tiêu & Loss (PHẢI TRÙNG KHỚP) ---\n    # 3 mục tiêu model đã dự đoán\n    TARGET_COLS = ['Dry_Green_g', 'Dry_Dead_g', 'Dry_Clover_g', 'GDM_g', 'Dry_Total_g']\n    \n    # 5 mục tiêu để nộp bài\n    ALL_TARGET_COLS = ['Dry_Green_g', 'Dry_Dead_g', 'Dry_Clover_g', 'GDM_g', 'Dry_Total_g']\n\n\nCFG.SUBMISSION_FILE = args.output_file\nCFG.MODEL_DIR = args.model_dir\nCFG.MODEL_NAME = args.model_name\nCFG.IMG_SIZE = args.image_size\n\n\nBORDERS_DICT = {\n    'Dry_Clover_g': [1.6e-05, 3.9, 10.5353, 20.6523, 37.5911, 71.7865],\n    'Dry_Dead_g': [1.6e-05, 6.1407, 13.1192, 23.277, 38.8581, 83.8407],\n    'Dry_Green_g': [1.6e-05, 13.4232, 27.0782, 45.5236, 79.834, 157.9836],\n    'Dry_Total_g': [1.6e-05, 23.4907, 41.1, 61.1, 96.8288, 185.7],\n    'GDM_g': [1.6e-05, 16.5143, 30.507, 49.5585, 81.0, 157.9836],\n}\n\n# ===============================================================\n# 2. 🏞️ AUGMENTATIONS (CHỈ DÙNG VALIDATION)\n# ===============================================================\nfrom albumentations import (\n    Compose, \n    Resize, \n    Normalize,\n    HorizontalFlip, \n    VerticalFlip\n)\n\ndef get_tta_transforms():\n    \"\"\"\n    Trả về một LIST các pipeline transform cho TTA.\n    Mỗi pipeline là một \"view\" khác nhau của ảnh.\n    \"\"\"\n \n    original_view = Compose([\n        \n        Normalize(\n            mean=[0.485, 0.456, 0.406],\n            std=[0.229, 0.224, 0.225]\n        ),\n        Resize(CFG.IMG_SIZE, CFG.IMG_SIZE),\n        ToTensorV2()\n    ])\n    \n    # -----------------\n    # View 2: Lật ngang (HFlip)\n    # -----------------\n    hflip_view = Compose([\n        HorizontalFlip(p=1.0), # Luôn luôn lật\n        Normalize(\n            mean=[0.485, 0.456, 0.406],\n            std=[0.229, 0.224, 0.225]\n        ),\n        Resize(CFG.IMG_SIZE, CFG.IMG_SIZE),\n        ToTensorV2()\n    ])\n    \n    # -----------------\n    # View 3: Lật dọc (VFlip)\n    # -----------------\n    vflip_view = Compose([\n        VerticalFlip(p=1.0), # Luôn luôn lật\n        Normalize(\n            mean=[0.485, 0.456, 0.406],\n            std=[0.229, 0.224, 0.225]\n        ),\n        Resize(CFG.IMG_SIZE, CFG.IMG_SIZE),\n        ToTensorV2()\n    ])\n    \n    return [original_view, hflip_view]\n\n\n\n\nclass TestBiomassDataset(Dataset):\n    \"\"\"\n    \"\"\"\n    def __init__(self, df, transform_pipeline, image_dir):\n        self.df = df\n        self.transforms = transform_pipeline \n        self.image_dir = image_dir\n        self.image_paths = df['image_path'].values\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        # 1. Lấy thông tin\n        img_path_suffix = self.image_paths[idx]\n        \n        # 2. Đọc ảnh gốc (2000x1000)\n        filename = os.path.basename(img_path_suffix)\n        full_path = os.path.join(self.image_dir, filename)\n        \n        image = cv2.imread(full_path)\n        if image is None:\n            print(f\"没有图片: {full_path}. 找到\")\n            image = np.zeros((1000, 2000, 3), dtype=np.uint8)\n        \n        image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)\n        \n        height, width, _ = image.shape\n        mid_point = width // 2\n        img_left = image[:, :mid_point]\n        img_right = image[:, mid_point:]\n        \n        img_left_tensor = self.transforms(image=img_left)['image']\n        img_right_tensor = self.transforms(image=img_right)['image']\n        \n        return idx, img_left_tensor, img_right_tensor\n\n\nclass FeatureInteractionModule(nn.Module):\n    \"\"\"特征交互模块，增强左右图像特征的关系建模\"\"\"\n    def __init__(self, hidden_dim):\n        super().__init__()\n        self.attention = nn.MultiheadAttention(\n            embed_dim=hidden_dim, \n            num_heads=4, \n            batch_first=True\n        )\n        self.norm = nn.LayerNorm(hidden_dim)\n        self.ffn = nn.Sequential(\n            nn.Linear(hidden_dim, hidden_dim * 2),\n            nn.ReLU(),\n            nn.Dropout(0.1),\n            nn.Linear(hidden_dim * 2, hidden_dim)\n        )\n        \n    def forward(self, left_feat, right_feat):\n        # 拼接左右特征 [B, 2, D]\n        combined = torch.stack([left_feat, right_feat], dim=1)\n        \n        # 自注意力交互\n        attended, _ = self.attention(combined, combined, combined)\n        attended = self.norm(attended + combined)\n        \n        # FFN\n        output = self.norm(attended + self.ffn(attended))\n        \n        # 分离左右特征\n        left_out = output[:, 0, :]\n        right_out = output[:, 1, :]\n        \n        return left_out, right_out\n\nclass BiomassModel(nn.Module):\n    \"\"\"\n    双流模型，三个专用输出头。\n    \n    1. 一个共享的主干网络（例如：ConvNeXt）。\n    2. 该主干网络分别应用于左图和右图。\n    3. 两个特征向量被拼接（Concatenate）在一起。\n    4. 合并后的向量被送入三个独立的 MLP \"头\"。\n\n\n    \"\"\"\n    def __init__(self, model_name, pretrained, n_targets=3):\n        super(BiomassModel, self).__init__()\n        \n        # 1. 加载共享主干网络\n        # num_classes=0: 移除原始分类层\n        # global_pool='avg': 添加全局平均池化层\n        #  以提取一维特征向量\n\n        self.backbone = timm.create_model(\n            model_name,\n            pretrained=pretrained,\n            num_classes=0,\n            global_pool='avg',\n\n        )\n  \n        \n        # 2. 从主干网络获取特征维度\n        # 例如：convnext_tiny 是 768，resnet50 是 2048\n        self.n_features = self.backbone.num_features\n        \n        # 3. 计算融合后的维度\n        # (features_left + features_right)\n        self.n_combined_features = self.n_features * 2\n        \n        # 4. 定义三个独立的MLP \"头\"\n        # 每个\"头\"是一个专用的小型神经网络\n\n        dropout = 0.3\n        # ---Dry_Green_g ---\n        self.head_green = nn.Sequential(\n            nn.Linear(self.n_combined_features, self.n_combined_features // 2),\n            nn.ReLU(),\n            nn.Dropout(dropout),\n            nn.Linear(self.n_combined_features//2, self.n_combined_features // 2),\n            nn.ReLU(),\n            nn.Dropout(dropout),\n            nn.Linear(self.n_combined_features // 2, 1)\n        )\n        self.cls_green = nn.Sequential(\n            nn.Linear(self.n_combined_features, self.n_combined_features // 2),\n            nn.ReLU(),\n            nn.Dropout(dropout),\n            nn.Linear(self.n_combined_features // 2, len(BORDERS_DICT['Dry_Green_g'])+1)\n        )\n        \n        # Dry_Dead_g\n        self.head_dead = nn.Sequential(\n            nn.Linear(self.n_combined_features, self.n_combined_features // 2),\n            nn.ReLU(),\n            nn.Dropout(dropout),\n            nn.Linear(self.n_combined_features//2, self.n_combined_features // 2),\n            nn.ReLU(),\n            nn.Dropout(dropout),\n            nn.Linear(self.n_combined_features // 2, 1)\n        )\n\n        self.cls_dead = nn.Sequential(\n            nn.Linear(self.n_combined_features, self.n_combined_features // 2),\n            nn.ReLU(),\n            nn.Dropout(dropout),\n            nn.Linear(self.n_combined_features // 2, len(BORDERS_DICT['Dry_Dead_g'])+1)\n        )\n        # Dry_Clover_g\n        self.head_clover = nn.Sequential(\n            nn.Linear(self.n_combined_features, self.n_combined_features // 2),\n            nn.ReLU(),\n            nn.Dropout(dropout),\n            nn.Linear(self.n_combined_features//2, self.n_combined_features // 2),\n            nn.ReLU(),\n            nn.Dropout(dropout),\n            nn.Linear(self.n_combined_features // 2, 1)\n        )\n        self.cls_clover = nn.Sequential(\n            nn.Linear(self.n_combined_features, self.n_combined_features // 2),\n            nn.ReLU(),\n            nn.Dropout(dropout),\n            nn.Linear(self.n_combined_features // 2, len(BORDERS_DICT['Dry_Clover_g'])+1)\n        )\n\n        \n        \n        # --- GDM_g ---\n        self.head_gdm = nn.Sequential(\n            nn.Linear(self.n_combined_features, self.n_combined_features // 2),\n            nn.ReLU(),\n            nn.Dropout(dropout),\n            nn.Linear(self.n_combined_features//2, self.n_combined_features // 2),\n            nn.ReLU(),\n            nn.Dropout(dropout),\n            nn.Linear(self.n_combined_features // 2, 1)\n        )\n        self.cls_gdm = nn.Sequential(\n            nn.Linear(self.n_combined_features, self.n_combined_features // 2),\n            nn.ReLU(),\n            nn.Dropout(dropout),\n            nn.Linear(self.n_combined_features // 2, len(BORDERS_DICT['GDM_g'])+1)\n        )\n\n        # --- Dry_Total_g 头 ---\n        self.head_total = nn.Sequential(\n            nn.Linear(self.n_combined_features, self.n_combined_features // 2),\n            nn.ReLU(),\n            nn.Dropout(dropout),\n            nn.Linear(self.n_combined_features//2, self.n_combined_features // 2),\n            nn.ReLU(),\n            nn.Dropout(dropout),\n            nn.Linear(self.n_combined_features // 2, 1)\n        )\n        self.total_cls = nn.Sequential(\n            nn.Linear(self.n_combined_features, self.n_combined_features // 2),\n            nn.ReLU(),\n            nn.Dropout(dropout),\n            nn.Linear(self.n_combined_features // 2, len(BORDERS_DICT['Dry_Total_g'])+1)\n        )\n\n        # 2. 特征交互模块\n        self.feature_interaction = FeatureInteractionModule(self.n_features)\n      \n\n    def forward(self, img_left, img_right):\n        # 1. 左图\n        features_left = self.backbone(img_left) # Shape: [batch, n_features]\n        # 2. 右图\n        features_right = self.backbone(img_right) # Shape: [batch, n_features]\n\n        # 2. 特征交互\n        features_left, features_right = self.feature_interaction(\n            features_left, features_right\n        )\n        \n        # 3. 特征融合\n        # \n        combined = torch.cat([features_left, features_right], dim=1) # Shape: [batch, n_combined_features]\n        # combined = self.shared_encoder(combined)\n        \n        # 4. 输出\n        out_green = self.head_green(combined)\n        out_dead = self.head_dead(combined)\n        out_clover = self.head_clover(combined)\n        out_gdm = self.head_gdm(combined)\n        out_total = self.head_total(combined)\n\n        out_clover = out_clover * 0.8\n        if out_dead > 20:\n            out_dead *= 1.1\n        elif out_dead < 10:\n            out_dead *= 0.9\n            \n        out_gdm = 0.5*out_gdm+ 0.5*(out_green+out_clover)\n        \n        pred_total1 = out_green + out_clover + out_dead\n        pred_total2 = out_gdm + out_dead\n        out_total = 0.5*out_total + 0.5*pred_total1 + 0.0*pred_total2\n        \n\n                \n        out_green_cls = self.cls_green(combined)\n        out_dead_cls = self.cls_dead(combined)\n        out_clover_cls = self.cls_clover(combined)\n        out_gdm_cls = self.cls_gdm(combined)\n        out_total_cls = self.total_cls(combined)\n        return (out_green, out_dead, out_clover, out_gdm, out_total), (out_green_cls, out_dead_cls, out_clover_cls, out_gdm_cls, out_total_cls)\n\n@torch.no_grad()\ndef predict_one_view_accelerate(models_list, test_loader, accelerator):\n    device = accelerator.device\n\n    # 只在主进程收集最终拼接结果\n    idx_buf = []\n    total_buf, gdm_buf, green_buf, clover_buf, dead_buf = [], [], [], [], []\n    prob_total_buf, prob_gdm_buf, prob_green_buf, prob_clover_buf, prob_dead_buf = [], [], [], [], []\n    \n\n    pbar = tqdm(test_loader, desc=\"  Predicting View\", leave=False, disable=not accelerator.is_main_process)\n\n    for batch in pbar:\n        idx, img_left, img_right = batch\n        idx = idx.to(device, non_blocking=True)\n        img_left = img_left.to(device, non_blocking=True)\n        img_right = img_right.to(device, non_blocking=True)\n\n        # ensemble\n        pred_total_list, pred_gdm_list, pred_green_list, pred_clover_list, pred_dead_list = [], [], [], [], []\n        prob_total_list, prob_gdm_list, prob_green_list, prob_clover_list, prob_dead_list = [], [], [], [], []\n\n        \n        with accelerator.autocast():\n            for model in models_list:\n                (pred_green, pred_dead, pred_clover, pred_gdm, pred_total), (pred_green_cls, pred_dead_cls, pred_clover_cls, pred_gdm_cls, pred_total_cls) = model(img_left, img_right)\n                \n                # pred_green, pred_dead, pred_clover, pred_gdm, pred_total = model(img_left, img_right)\n                pred_total_list.append(pred_total)\n                pred_gdm_list.append(pred_gdm)\n                pred_green_list.append(pred_green)\n                pred_clover_list.append(pred_clover)\n                pred_dead_list.append(pred_dead)\n\n            avg_pred_total = torch.mean(torch.stack(pred_total_list), dim=0)\n            avg_pred_gdm = torch.mean(torch.stack(pred_gdm_list), dim=0)\n            avg_pred_green = torch.mean(torch.stack(pred_green_list), dim=0)\n            avg_pred_clover = torch.mean(torch.stack(pred_clover_list), dim=0)\n            avg_pred_dead = torch.mean(torch.stack(pred_dead_list), dim=0)\n\n        # ✅ 关键修改：跨进程 gather，并自动去掉末尾重复数据（用于 metrics/推理汇总）\n        g_idx = accelerator.gather_for_metrics(idx)\n        g_total = accelerator.gather_for_metrics(avg_pred_total)\n        g_gdm = accelerator.gather_for_metrics(avg_pred_gdm)\n        g_green = accelerator.gather_for_metrics(avg_pred_green)\n        g_clover = accelerator.gather_for_metrics(avg_pred_clover)\n        g_dead = accelerator.gather_for_metrics(avg_pred_dead)\n\n\n        if accelerator.is_main_process:\n            idx_buf.append(g_idx.detach().cpu())\n            total_buf.append(g_total.detach().cpu())\n            gdm_buf.append(g_gdm.detach().cpu())\n            green_buf.append(g_green.detach().cpu())\n            clover_buf.append(g_clover.detach().cpu())\n            dead_buf.append(g_dead.detach().cpu())\n\n    if not accelerator.is_main_process:\n        return None\n\n    idx_all = torch.cat(idx_buf, dim=0).numpy()\n    order = np.argsort(idx_all)\n\n    total_all = torch.cat(total_buf, dim=0).numpy().reshape(-1)[order]\n    gdm_all = torch.cat(gdm_buf, dim=0).numpy().reshape(-1)[order]\n    green_all = torch.cat(green_buf, dim=0).numpy().reshape(-1)[order]\n    clover_all = torch.cat(clover_buf, dim=0).numpy().reshape(-1)[order]\n    dead_all = torch.cat(dead_buf, dim=0).numpy().reshape(-1)[order]\n\n    return {\n        'total': total_all,\n        'gdm': gdm_all,\n        'green': green_all,\n        'clover': clover_all,\n        'dead': dead_all,\n        \n    }\n\n\ndef run_inference_with_tta():\n    \"\"\"\n    \"\"\"\n    # ✅ 关键修改：引入 accelerate\n    accelerator = Accelerator()\n    device = accelerator.device\n\n    accelerator.print(f\"\\n{'='*50}\")\n    accelerator.print(\"🚀 TTA (Accelerate multi-GPU) 🚀\")\n    accelerator.print(f\"{'='*50}\")\n\n    accelerator.print(f\"test file {CFG.TEST_CSV}...\")\n\n    \n    try:\n        test_df_long = pd.read_csv(CFG.TEST_CSV)\n        test_df_unique = test_df_long.drop_duplicates(subset=['image_path']).reset_index(drop=True)\n        print(f\"image  {len(test_df_unique)} .\")\n    except FileNotFoundError:\n        print(f\"没有发现 {CFG.TEST_CSV}\")\n        return None, None, None\n\n    accelerator.print(\"\\n加载模型...\")\n    models_list = []\n\n\n    model = BiomassModel(CFG.MODEL_NAME, pretrained=False)\n    model.load_state_dict(torch.load(CFG.MODEL_DIR, map_location='cpu'))\n\n    model.eval()\n    model.to(device)\n    models_list.append(model)\n\n\n    accelerator.print(f\"✓ 一共 {len(models_list)} 个模型.\")\n\n    tta_transforms = get_tta_transforms()\n    accelerator.print(f\"\\使用tta {len(tta_transforms)} TTA 个数...\")\n    \n    all_tta_view_preds = [] # List để lưu kết quả của mỗi view TTA\n\n    for i, tta_transform in enumerate(tta_transforms):\n        accelerator.print(f\"---  TTA View {i+1}/{len(tta_transforms)} ---\")\n        \n        # Tạo Dataset/Loader MỚI cho view TTA này\n        test_dataset = TestBiomassDataset(\n            df=test_df_unique,\n            transform_pipeline=tta_transform, # Truyền pipeline TTA\n            image_dir=CFG.TEST_IMAGE_DIR\n        )\n        test_loader = DataLoader(\n            test_dataset,\n            batch_size=CFG.BATCH_SIZE,\n            shuffle=False,\n            num_workers=CFG.NUM_WORKERS,\n            pin_memory=True\n        )\n        test_loader = accelerator.prepare(test_loader)\n        # Chạy ensemble 5-fold cho view này\n        view_preds_np = predict_one_view_accelerate(models_list, test_loader, accelerator)\n        if accelerator.is_main_process:\n            all_tta_view_preds.append(view_preds_np)\n            accelerator.print(f\"✓完成 TTA View {i+1}\")\n    if not accelerator.is_main_process:\n        return None, None, None\n    # --- 4. Ensemble (Lấy trung bình) kết quả TTA ---\n    accelerator.print(\"\\n处理 TTA views...\")\n    final_ensembled_preds = {\n        'total': np.mean([d['total'] for d in all_tta_view_preds], axis=0),\n        'gdm':   np.mean([d['gdm'] for d in all_tta_view_preds], axis=0),\n        'green': np.mean([d['green'] for d in all_tta_view_preds], axis=0),\n        'clover': np.mean([d['clover'] for d in all_tta_view_preds], axis=0),\n        'dead': np.mean([d['dead'] for d in all_tta_view_preds], axis=0)\n        \n    }\n    \n    accelerator.print(\"✓ 完成推理.\")\n    \n    del models_list, test_loader, test_dataset\n    gc.collect()\n    torch.cuda.empty_cache()\n    \n    return final_ensembled_preds, test_df_long, test_df_unique\n\n\ndef post_process_biomass(df_preds):\n    \"\"\"\n    Enforces physical mass balance constraints on biomass predictions.\n    \n    Constraints enforced:\n    1. Dry_Green_g + Dry_Clover_g = GDM_g\n    2. GDM_g + Dry_Dead_g = Dry_Total_g\n    \n    Method:\n    Uses Orthogonal Projection. It finds the set of values that satisfy\n    the constraints while minimizing the Euclidean distance to the \n    original model predictions.\n    \n    Args:\n        df_preds (pd.DataFrame): DataFrame containing the 5 prediction columns.\n        \n    Returns:\n        pd.DataFrame: A new DataFrame with consistent, non-negative values.\n    \"\"\"\n    # 1. Define the specific order required for the math\n    # We treat the vector x as: [Green, Clover, Dead, GDM, Total]\n    ordered_cols = [\"Dry_Green_g\", \"Dry_Dead_g\", \"Dry_Clover_g\", \"GDM_g\", \"Dry_Total_g\"]\n    \n    # Check if columns exist\n    if not all(col in df_preds.columns for col in ordered_cols):\n        missing = [c for c in ordered_cols if c not in df_preds.columns]\n        raise ValueError(f\"Input DataFrame is missing columns: {missing}\")\n\n    # 2. Extract values in the specific order -> Shape (N_samples, 5)\n    Y = df_preds[ordered_cols].values.T  # Transpose to (5, N) for matrix math\n\n    # 3. Define the Constraint Matrix C\n    # We want Cx = 0\n    # Eq 1: 1*Green + 0*Dead + 1*Clover - 1*GDM + 0*Total = 0\n    # Eq 2: 0*Green + 1*Dead + 0*Clover + 1*GDM - 1*Total = 0\n    C = np.array([\n        [1, 0, 1, -1,  0],\n        [0, 1, 0,  1, -1]\n    ])\n\n    # 4. Calculate Projection Matrix P\n    # P = I - C^T * (C * C^T)^-1 * C\n    # This projects any vector onto the null space of C (the valid subspace)\n    C_T = C.T\n    inv_CCt = np.linalg.inv(C @ C_T)\n    P = np.eye(5) - C_T @ inv_CCt @ C\n\n    # 5. Apply Projection\n    # Y_new = P * Y\n    Y_reconciled = P @ Y\n\n    # 6. Transpose back to (N, 5)\n    Y_reconciled = Y_reconciled.T\n\n    # 7. Post-correction for negatives\n    # Projection can mathematically create negative values (e.g. if Total was predicted 0)\n    # We clip to 0. Note: This might slightly break the sum equality again, \n    # but exact equality with negatives is physically impossible anyway.\n    Y_reconciled = Y_reconciled.clip(min=0)\n\n    # 8. Create Output DataFrame\n    df_out = df_preds.copy()\n    df_out[ordered_cols] = Y_reconciled\n\n    return df_out\n\n# ===============================================================\n# 6. ✍️ HÀM TẠO FILE SUBMISSION\n# ===============================================================\ndef create_submission(preds_np, test_df_long, test_df_unique):\n    \"\"\"\n    \"\"\"\n    if preds_np is None:\n        print(\"Bỏ qua tạo submission do lỗi ở trên.\")\n        return\n\n    print(\"\\nĐang hậu xử lý và tạo file submission...\")\n\n    # 1. Lấy 3 dự đoán đã ensemble\n    pred_total_final = preds_np['total']\n    pred_gdm_final = preds_np['gdm']\n    pred_green_final = preds_np['green']\n\n    pred_clover_final = preds_np['clover']\n    pred_dead_final = preds_np['dead']\n\n    # 1. 确保非负\n    pred_green_final = np.clip(pred_green_final, a_min=0, a_max=None)\n    pred_clover_final = np.clip(pred_clover_final, a_min=0, a_max=None)\n    pred_dead_final = np.clip(pred_dead_final, a_min=0, a_max=None)\n    \n\n\n    preds_wide_df = pd.DataFrame({\n        'image_path': test_df_unique['image_path'],\n        'Dry_Green_g': pred_green_final,\n        'Dry_Dead_g': pred_dead_final,\n        'Dry_Clover_g': pred_clover_final,\n        'GDM_g': pred_gdm_final,\n        'Dry_Total_g': pred_total_final\n    })\n    preds_wide_df = post_process_biomass(preds_wide_df)\n\n    preds_long_df = preds_wide_df.melt(\n        id_vars=['image_path'],\n        value_vars=CFG.ALL_TARGET_COLS, # \n        var_name='target_name',        # \n        value_name='target'            # \n    )\n\n    submission_df = pd.merge(\n        test_df_long[['sample_id', 'image_path', 'target_name']],\n        preds_long_df,\n        on=['image_path', 'target_name'],\n        how='left'\n    )\n\n    submission_df = submission_df[['sample_id', 'target']]\n    \n    # Lưu file\n    submission_df.to_csv(CFG.SUBMISSION_FILE, index=False)\n\n    print(f\"\\n🎉 HOÀN TẤT! Đã lưu file submission tại: {CFG.SUBMISSION_FILE}\")\n    print(submission_df.head())\n    print(submission_df.tail())\n    \nif __name__ == \"__main__\":\n    all_preds_np, df_long, df_unique = run_inference_with_tta()\n    # ✅ 关键修改：只在主进程写文件（避免多进程冲突）\n    accelerator = Accelerator()\n    if accelerator.is_main_process:\n        create_submission(all_preds_np, df_long, df_unique)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-28T06:21:24.592448Z","iopub.execute_input":"2026-01-28T06:21:24.592909Z","iopub.status.idle":"2026-01-28T06:21:24.607507Z","shell.execute_reply.started":"2026-01-28T06:21:24.592880Z","shell.execute_reply":"2026-01-28T06:21:24.606903Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"%%writefile infer_v4.py\nfrom accelerate import Accelerator\nimport os\nimport pandas as pd\nimport numpy as np\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import Dataset, DataLoader\nimport albumentations as A\nfrom albumentations.pytorch import ToTensorV2\nimport timm\nimport cv2\nfrom tqdm import tqdm\nimport gc\n\nimport torch\nimport math\nimport torch.nn.functional as F\nimport torch.utils.checkpoint as checkpoint\nfrom timm.models.layers import DropPath, to_2tuple, trunc_normal_\n\nimport argparse\n\nparser = argparse.ArgumentParser(description='gamma inference')\nparser.add_argument('--model_dir', type=str, default = '/kaggle/input/qwen2.5/transformers/32b-instruct-awq/1')\nparser.add_argument('--model_name', type=str, default = 'vit_huge_plus_patch16_dinov3.lvd1689m')\nparser.add_argument('--output_file', type=str, default = 'submission.json')\nparser.add_argument('--image_size', type=int, default = 448)\n\n\nargs = parser.parse_args()\nprint(args)\n\n\nclass CFG:\n    BASE_PATH = '/kaggle/input/csiro-biomass'\n    TEST_CSV = os.path.join(BASE_PATH, 'test.csv')\n    TEST_IMAGE_DIR = os.path.join(BASE_PATH, 'test')\n    SUBMISSION_FILE = 'submission.csv'\n    \n\n    MODEL_DIR = '/kaggle/input/csiro-model-ckpt/1209_vit_huge_fold3_all_v1_70_9466/1209_vit_huge_fold3_all_v1_70_9466/model_fold-1_epoch30_0.9466.pth' # Giả sử 5 file .pth nằm cùng thư mục\n    MODEL_NAME = 'vit_huge_plus_patch16_dinov3.lvd1689m'\n    IMG_SIZE = 448\n\n    \n    DEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n    BATCH_SIZE = 1 \n    NUM_WORKERS = 1\n    N_FOLDS = 5\n    \n    # --- Mục tiêu & Loss (PHẢI TRÙNG KHỚP) ---\n    # 3 mục tiêu model đã dự đoán\n    TARGET_COLS = ['Dry_Green_g', 'Dry_Dead_g', 'Dry_Clover_g', 'GDM_g', 'Dry_Total_g']\n    \n    # 5 mục tiêu để nộp bài\n    ALL_TARGET_COLS = ['Dry_Green_g', 'Dry_Dead_g', 'Dry_Clover_g', 'GDM_g', 'Dry_Total_g']\n\n\nCFG.SUBMISSION_FILE = args.output_file\nCFG.MODEL_DIR = args.model_dir\nCFG.MODEL_NAME = args.model_name\nCFG.IMG_SIZE = args.image_size\n\n\nBORDERS_DICT = {\n    'Dry_Clover_g': [1.6e-05, 3.9, 10.5353, 20.6523, 37.5911, 71.7865],\n    'Dry_Dead_g': [1.6e-05, 6.1407, 13.1192, 23.277, 38.8581, 83.8407],\n    'Dry_Green_g': [1.6e-05, 13.4232, 27.0782, 45.5236, 79.834, 157.9836],\n    'Dry_Total_g': [1.6e-05, 23.4907, 41.1, 61.1, 96.8288, 185.7],\n    'GDM_g': [1.6e-05, 16.5143, 30.507, 49.5585, 81.0, 157.9836],\n}\n\n\n\n# ===============================================================\n# 2. 🏞️ AUGMENTATIONS (CHỈ DÙNG VALIDATION)\n# ===============================================================\nfrom albumentations import (\n    Compose, \n    Resize, \n    Normalize,\n    HorizontalFlip, \n    VerticalFlip\n)\n\ndef get_tta_transforms():\n    \"\"\"\n    Trả về một LIST các pipeline transform cho TTA.\n    Mỗi pipeline là một \"view\" khác nhau của ảnh.\n    \"\"\"\n    \n    # -----------------\n    # View 1: Ảnh gốc (Chỉ Resize + Normalize)\n    # -----------------\n    original_view = Compose([\n        \n        Normalize(\n            mean=[0.485, 0.456, 0.406],\n            std=[0.229, 0.224, 0.225]\n        ),\n        Resize(CFG.IMG_SIZE, CFG.IMG_SIZE),\n        ToTensorV2()\n    ])\n    \n    # -----------------\n    # View 2: Lật ngang (HFlip)\n    # -----------------\n    hflip_view = Compose([\n        HorizontalFlip(p=1.0), # Luôn luôn lật\n        Normalize(\n            mean=[0.485, 0.456, 0.406],\n            std=[0.229, 0.224, 0.225]\n        ),\n        Resize(CFG.IMG_SIZE, CFG.IMG_SIZE),\n        ToTensorV2()\n    ])\n    \n    # -----------------\n    # View 3: Lật dọc (VFlip)\n    # -----------------\n    vflip_view = Compose([\n        VerticalFlip(p=1.0), # Luôn luôn lật\n        Normalize(\n            mean=[0.485, 0.456, 0.406],\n            std=[0.229, 0.224, 0.225]\n        ),\n        Resize(CFG.IMG_SIZE, CFG.IMG_SIZE),\n        ToTensorV2()\n    ])\n    \n    return [original_view, hflip_view]\n\n\n\n\nclass TestBiomassDataset(Dataset):\n    \"\"\"\n    \"\"\"\n    def __init__(self, df, transform_pipeline, image_dir):\n        self.df = df\n        self.transforms = transform_pipeline \n        self.image_dir = image_dir\n        self.image_paths = df['image_path'].values\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        # 1. Lấy thông tin\n        img_path_suffix = self.image_paths[idx]\n        \n        # 2. Đọc ảnh gốc (2000x1000)\n        filename = os.path.basename(img_path_suffix)\n        full_path = os.path.join(self.image_dir, filename)\n        \n        image = cv2.imread(full_path)\n        if image is None:\n            print(f\"没有图片: {full_path}. 找到\")\n            image = np.zeros((1000, 2000, 3), dtype=np.uint8)\n        \n        image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)\n        \n        height, width, _ = image.shape\n        mid_point = width // 2\n        img_left = image[:, :mid_point]\n        img_right = image[:, mid_point:]\n        \n        img_left_tensor = self.transforms(image=img_left)['image']\n        img_right_tensor = self.transforms(image=img_right)['image']\n        \n        return idx, img_left_tensor, img_right_tensor\n\n\nclass FeatureInteractionModule(nn.Module):\n    \"\"\"特征交互模块，增强左右图像特征的关系建模\"\"\"\n    def __init__(self, hidden_dim):\n        super().__init__()\n        self.attention = nn.MultiheadAttention(\n            embed_dim=hidden_dim, \n            num_heads=4, \n            batch_first=True\n        )\n        self.norm = nn.LayerNorm(hidden_dim)\n        self.ffn = nn.Sequential(\n            nn.Linear(hidden_dim, hidden_dim * 2),\n            nn.ReLU(),\n            nn.Dropout(0.1),\n            nn.Linear(hidden_dim * 2, hidden_dim)\n        )\n        \n    def forward(self, left_feat, right_feat):\n        # 拼接左右特征 [B, 2, D]\n        combined = torch.stack([left_feat, right_feat], dim=1)\n        \n        # 自注意力交互\n        attended, _ = self.attention(combined, combined, combined)\n        attended = self.norm(attended + combined)\n        \n        # FFN\n        output = self.norm(attended + self.ffn(attended))\n        \n        # 分离左右特征\n        left_out = output[:, 0, :]\n        right_out = output[:, 1, :]\n        \n        return left_out, right_out\n\n\nclass BiomassModel(nn.Module):\n    \"\"\"\n    双流模型，三个专用输出头。\n    \n    1. 一个共享的主干网络（例如：ConvNeXt）。\n    2. 该主干网络分别应用于左图和右图。\n    3. 两个特征向量被拼接（Concatenate）在一起。\n    4. 合并后的向量被送入三个独立的 MLP \"头\"。\n\n\n    \"\"\"\n    def __init__(self, model_name, pretrained, n_targets=3):\n        super(BiomassModel, self).__init__()\n        \n        # 1. 加载共享主干网络\n        # num_classes=0: 移除原始分类层\n        # global_pool='avg': 添加全局平均池化层\n        #  以提取一维特征向量\n\n        self.backbone = timm.create_model(\n            model_name,\n            pretrained=pretrained,\n            num_classes=0,\n            global_pool='avg',\n\n        )\n\n  \n        \n        # 2. 从主干网络获取特征维度\n        # 例如：convnext_tiny 是 768，resnet50 是 2048\n        self.n_features = self.backbone.num_features\n        \n        # 3. 计算融合后的维度\n        # (features_left + features_right)\n        self.n_combined_features = self.n_features * 2\n        \n        # 4. 定义三个独立的MLP \"头\"\n        # 每个\"头\"是一个专用的小型神经网络\n\n        dropout = 0.3\n        # ---Dry_Green_g ---\n        self.head_green = nn.Sequential(\n            nn.Linear(self.n_combined_features, self.n_combined_features // 2),\n            nn.ReLU(),\n            nn.Dropout(dropout),\n            nn.Linear(self.n_combined_features//2, self.n_combined_features // 2),\n            nn.ReLU(),\n            nn.Dropout(dropout),\n            nn.Linear(self.n_combined_features // 2, 1)\n        )\n        self.cls_green = nn.Sequential(\n            nn.Linear(self.n_combined_features, self.n_combined_features // 2),\n            nn.ReLU(),\n            nn.Dropout(dropout),\n            nn.Linear(self.n_combined_features // 2, len(BORDERS_DICT['Dry_Green_g'])+1)\n        )\n        \n        # Dry_Dead_g\n        self.head_dead = nn.Sequential(\n            nn.Linear(self.n_combined_features, self.n_combined_features // 2),\n            nn.ReLU(),\n            nn.Dropout(dropout),\n            nn.Linear(self.n_combined_features//2, self.n_combined_features // 2),\n            nn.ReLU(),\n            nn.Dropout(dropout),\n            nn.Linear(self.n_combined_features // 2, 1)\n        )\n\n        self.cls_dead = nn.Sequential(\n            nn.Linear(self.n_combined_features, self.n_combined_features // 2),\n            nn.ReLU(),\n            nn.Dropout(dropout),\n            nn.Linear(self.n_combined_features // 2, len(BORDERS_DICT['Dry_Dead_g'])+1)\n        )\n        # Dry_Clover_g\n        self.head_clover = nn.Sequential(\n            nn.Linear(self.n_combined_features, self.n_combined_features // 2),\n            nn.ReLU(),\n            nn.Dropout(dropout),\n            nn.Linear(self.n_combined_features//2, self.n_combined_features // 2),\n            nn.ReLU(),\n            nn.Dropout(dropout),\n            nn.Linear(self.n_combined_features // 2, 1)\n        )\n        self.cls_clover = nn.Sequential(\n            nn.Linear(self.n_combined_features, self.n_combined_features // 2),\n            nn.ReLU(),\n            nn.Dropout(dropout),\n            nn.Linear(self.n_combined_features // 2, len(BORDERS_DICT['Dry_Clover_g'])+1)\n        )\n\n        \n        \n        # --- GDM_g ---\n        self.head_gdm = nn.Sequential(\n            nn.Linear(self.n_combined_features, self.n_combined_features // 2),\n            nn.ReLU(),\n            nn.Dropout(dropout),\n            nn.Linear(self.n_combined_features//2, self.n_combined_features // 2),\n            nn.ReLU(),\n            nn.Dropout(dropout),\n            nn.Linear(self.n_combined_features // 2, 1)\n        )\n        self.cls_gdm = nn.Sequential(\n            nn.Linear(self.n_combined_features, self.n_combined_features // 2),\n            nn.ReLU(),\n            nn.Dropout(dropout),\n            nn.Linear(self.n_combined_features // 2, len(BORDERS_DICT['GDM_g'])+1)\n        )\n\n        # --- Dry_Total_g 头 ---\n        self.head_total = nn.Sequential(\n            nn.Linear(self.n_combined_features, self.n_combined_features // 2),\n            nn.ReLU(),\n            nn.Dropout(dropout),\n            nn.Linear(self.n_combined_features//2, self.n_combined_features // 2),\n            nn.ReLU(),\n            nn.Dropout(dropout),\n            nn.Linear(self.n_combined_features // 2, 1)\n        )\n        self.total_cls = nn.Sequential(\n            nn.Linear(self.n_combined_features, self.n_combined_features // 2),\n            nn.ReLU(),\n            nn.Dropout(dropout),\n            nn.Linear(self.n_combined_features // 2, len(BORDERS_DICT['Dry_Total_g'])+1)\n        )\n\n        # 2. 特征交互模块\n        self.feature_interaction = FeatureInteractionModule(self.n_features)\n     \n        \n\n    def forward(self, img_left, img_right):\n        # 1. 左图\n        features_left = self.backbone(img_left) # Shape: [batch, n_features]\n        # 2. 右图\n        features_right = self.backbone(img_right) # Shape: [batch, n_features]\n\n        # 2. 特征交互\n        features_left, features_right = self.feature_interaction(\n            features_left, features_right\n        )\n        \n        # 3. 特征融合\n        # \n        combined = torch.cat([features_left, features_right], dim=1) # Shape: [batch, n_combined_features]\n        # combined = self.shared_encoder(combined)\n        \n        # 4. 输出\n        out_green = self.head_green(combined)\n        out_dead = self.head_dead(combined)\n        out_clover = self.head_clover(combined)\n        out_gdm = self.head_gdm(combined)\n        out_total = self.head_total(combined)\n\n        out_clover = out_clover * 0.8\n        if out_dead > 20:\n            out_dead *= 1.1\n        elif out_dead < 10:\n            out_dead *= 0.9\n            \n        out_gdm = 0.5*out_gdm+ 0.5*(out_green+out_clover)\n        \n        pred_total1 = out_green + out_clover + out_dead\n        pred_total2 = out_gdm + out_dead\n        out_total = 0.5*out_total + 0.5*pred_total1 + 0.0*pred_total2\n        \n\n                \n        out_green_cls = self.cls_green(combined)\n        out_dead_cls = self.cls_dead(combined)\n        out_clover_cls = self.cls_clover(combined)\n        out_gdm_cls = self.cls_gdm(combined)\n        out_total_cls = self.total_cls(combined)\n        return (out_green, out_dead, out_clover, out_gdm, out_total), (out_green_cls, out_dead_cls, out_clover_cls, out_gdm_cls, out_total_cls)\n\n\n@torch.no_grad()\ndef predict_one_view_accelerate(models_list, test_loader, accelerator):\n    device = accelerator.device\n\n    # 只在主进程收集最终拼接结果\n    idx_buf = []\n    total_buf, gdm_buf, green_buf, clover_buf, dead_buf = [], [], [], [], []\n    prob_total_buf, prob_gdm_buf, prob_green_buf, prob_clover_buf, prob_dead_buf = [], [], [], [], []\n    \n\n    pbar = tqdm(test_loader, desc=\"  Predicting View\", leave=False, disable=not accelerator.is_main_process)\n\n    for batch in pbar:\n        idx, img_left, img_right = batch\n        idx = idx.to(device, non_blocking=True)\n        img_left = img_left.to(device, non_blocking=True)\n        img_right = img_right.to(device, non_blocking=True)\n\n        # ensemble\n        pred_total_list, pred_gdm_list, pred_green_list, pred_clover_list, pred_dead_list = [], [], [], [], []\n        prob_total_list, prob_gdm_list, prob_green_list, prob_clover_list, prob_dead_list = [], [], [], [], []\n\n        \n        with accelerator.autocast():\n            for model in models_list:\n                (pred_green, pred_dead, pred_clover, pred_gdm, pred_total), (pred_green_cls, pred_dead_cls, pred_clover_cls, pred_gdm_cls, pred_total_cls) = model(img_left, img_right)\n                \n                # pred_green, pred_dead, pred_clover, pred_gdm, pred_total = model(img_left, img_right)\n                pred_total_list.append(pred_total)\n                pred_gdm_list.append(pred_gdm)\n                pred_green_list.append(pred_green)\n                pred_clover_list.append(pred_clover)\n                pred_dead_list.append(pred_dead)\n\n               \n\n            avg_pred_total = torch.mean(torch.stack(pred_total_list), dim=0)\n            avg_pred_gdm = torch.mean(torch.stack(pred_gdm_list), dim=0)\n            avg_pred_green = torch.mean(torch.stack(pred_green_list), dim=0)\n            avg_pred_clover = torch.mean(torch.stack(pred_clover_list), dim=0)\n            avg_pred_dead = torch.mean(torch.stack(pred_dead_list), dim=0)\n\n\n        # ✅ 关键修改：跨进程 gather，并自动去掉末尾重复数据（用于 metrics/推理汇总）\n        g_idx = accelerator.gather_for_metrics(idx)\n        g_total = accelerator.gather_for_metrics(avg_pred_total)\n        g_gdm = accelerator.gather_for_metrics(avg_pred_gdm)\n        g_green = accelerator.gather_for_metrics(avg_pred_green)\n        g_clover = accelerator.gather_for_metrics(avg_pred_clover)\n        g_dead = accelerator.gather_for_metrics(avg_pred_dead)\n        \n\n        if accelerator.is_main_process:\n            idx_buf.append(g_idx.detach().cpu())\n            total_buf.append(g_total.detach().cpu())\n            gdm_buf.append(g_gdm.detach().cpu())\n            green_buf.append(g_green.detach().cpu())\n            clover_buf.append(g_clover.detach().cpu())\n            dead_buf.append(g_dead.detach().cpu())\n\n    if not accelerator.is_main_process:\n        return None\n\n    idx_all = torch.cat(idx_buf, dim=0).numpy()\n    order = np.argsort(idx_all)\n\n    total_all = torch.cat(total_buf, dim=0).numpy().reshape(-1)[order]\n    gdm_all = torch.cat(gdm_buf, dim=0).numpy().reshape(-1)[order]\n    green_all = torch.cat(green_buf, dim=0).numpy().reshape(-1)[order]\n    clover_all = torch.cat(clover_buf, dim=0).numpy().reshape(-1)[order]\n    dead_all = torch.cat(dead_buf, dim=0).numpy().reshape(-1)[order]\n\n    return {\n        'total': total_all,\n        'gdm': gdm_all,\n        'green': green_all,\n        'clover': clover_all,\n        'dead': dead_all,\n        \n    }\n\n\ndef run_inference_with_tta():\n    \"\"\"\n    \"\"\"\n    # ✅ 关键修改：引入 accelerate\n    accelerator = Accelerator()\n    device = accelerator.device\n\n    accelerator.print(f\"\\n{'='*50}\")\n    accelerator.print(\"🚀 TTA (Accelerate multi-GPU) 🚀\")\n    accelerator.print(f\"{'='*50}\")\n\n    accelerator.print(f\"test file {CFG.TEST_CSV}...\")\n\n    \n    try:\n        test_df_long = pd.read_csv(CFG.TEST_CSV)\n        test_df_unique = test_df_long.drop_duplicates(subset=['image_path']).reset_index(drop=True)\n        print(f\"image  {len(test_df_unique)} .\")\n    except FileNotFoundError:\n        print(f\"没有发现 {CFG.TEST_CSV}\")\n        return None, None, None\n\n    accelerator.print(\"\\n加载模型...\")\n    models_list = []\n\n\n    model = BiomassModel(CFG.MODEL_NAME, pretrained=False)\n    model.load_state_dict(torch.load(CFG.MODEL_DIR, map_location='cpu'))\n\n    model.eval()\n    model.to(device)\n    models_list.append(model)\n\n\n    accelerator.print(f\"✓ 一共 {len(models_list)} 个模型.\")\n\n    tta_transforms = get_tta_transforms()\n    accelerator.print(f\"\\使用tta {len(tta_transforms)} TTA 个数...\")\n    \n    all_tta_view_preds = [] # List để lưu kết quả của mỗi view TTA\n\n    for i, tta_transform in enumerate(tta_transforms):\n        accelerator.print(f\"---  TTA View {i+1}/{len(tta_transforms)} ---\")\n        \n        # Tạo Dataset/Loader MỚI cho view TTA này\n        test_dataset = TestBiomassDataset(\n            df=test_df_unique,\n            transform_pipeline=tta_transform, # Truyền pipeline TTA\n            image_dir=CFG.TEST_IMAGE_DIR\n        )\n        test_loader = DataLoader(\n            test_dataset,\n            batch_size=CFG.BATCH_SIZE,\n            shuffle=False,\n            num_workers=CFG.NUM_WORKERS,\n            pin_memory=True\n        )\n        test_loader = accelerator.prepare(test_loader)\n        # Chạy ensemble 5-fold cho view này\n        view_preds_np = predict_one_view_accelerate(models_list, test_loader, accelerator)\n        if accelerator.is_main_process:\n            all_tta_view_preds.append(view_preds_np)\n            accelerator.print(f\"✓完成 TTA View {i+1}\")\n    if not accelerator.is_main_process:\n        return None, None, None\n    # --- 4. Ensemble (Lấy trung bình) kết quả TTA ---\n    accelerator.print(\"\\n处理 TTA views...\")\n    final_ensembled_preds = {\n        'total': np.mean([d['total'] for d in all_tta_view_preds], axis=0),\n        'gdm':   np.mean([d['gdm'] for d in all_tta_view_preds], axis=0),\n        'green': np.mean([d['green'] for d in all_tta_view_preds], axis=0),\n        'clover': np.mean([d['clover'] for d in all_tta_view_preds], axis=0),\n        'dead': np.mean([d['dead'] for d in all_tta_view_preds], axis=0)\n        \n    }\n    \n    accelerator.print(\"✓ 完成推理.\")\n    \n    del models_list, test_loader, test_dataset\n    gc.collect()\n    torch.cuda.empty_cache()\n    \n    return final_ensembled_preds, test_df_long, test_df_unique\n# ===============================================================\n# 6. ✍️ HÀM TẠO FILE SUBMISSION\n# ===============================================================\n\n\ndef post_process_biomass(df_preds):\n    \"\"\"\n    Enforces physical mass balance constraints on biomass predictions.\n    \n    Constraints enforced:\n    1. Dry_Green_g + Dry_Clover_g = GDM_g\n    2. GDM_g + Dry_Dead_g = Dry_Total_g\n    \n    Method:\n    Uses Orthogonal Projection. It finds the set of values that satisfy\n    the constraints while minimizing the Euclidean distance to the \n    original model predictions.\n    \n    Args:\n        df_preds (pd.DataFrame): DataFrame containing the 5 prediction columns.\n        \n    Returns:\n        pd.DataFrame: A new DataFrame with consistent, non-negative values.\n    \"\"\"\n    # 1. Define the specific order required for the math\n    # We treat the vector x as: [Green, Clover, Dead, GDM, Total]\n    ordered_cols = [\"Dry_Green_g\", \"Dry_Dead_g\", \"Dry_Clover_g\", \"GDM_g\", \"Dry_Total_g\"]\n    \n    # Check if columns exist\n    if not all(col in df_preds.columns for col in ordered_cols):\n        missing = [c for c in ordered_cols if c not in df_preds.columns]\n        raise ValueError(f\"Input DataFrame is missing columns: {missing}\")\n\n    # 2. Extract values in the specific order -> Shape (N_samples, 5)\n    Y = df_preds[ordered_cols].values.T  # Transpose to (5, N) for matrix math\n\n    # 3. Define the Constraint Matrix C\n    # We want Cx = 0\n    # Eq 1: 1*Green + 0*Dead + 1*Clover - 1*GDM + 0*Total = 0\n    # Eq 2: 0*Green + 1*Dead + 0*Clover + 1*GDM - 1*Total = 0\n    C = np.array([\n        [1, 0, 1, -1,  0],\n        [0, 1, 0,  1, -1]\n    ])\n\n    # This projects any vector onto the null space of C (the valid subspace)\n    C_T = C.T\n    inv_CCt = np.linalg.inv(C @ C_T)\n    P = np.eye(5) - C_T @ inv_CCt @ C\n\n    # 5. Apply Projection\n    # Y_new = P * Y\n    Y_reconciled = P @ Y\n\n    # 6. Transpose back to (N, 5)\n    Y_reconciled = Y_reconciled.T\n\n    Y_reconciled = Y_reconciled.clip(min=0)\n\n    # 8. Create Output DataFrame\n    df_out = df_preds.copy()\n    df_out[ordered_cols] = Y_reconciled\n\n    return df_out\n\n\ndef create_submission(preds_np, test_df_long, test_df_unique):\n    \"\"\"\n    \"\"\"\n    if preds_np is None:\n        print(\"Bỏ qua tạo submission do lỗi ở trên.\")\n        return\n\n    print(\"\\nĐang hậu xử lý và tạo file submission...\")\n\n    # 1. Lấy 3 dự đoán đã ensemble\n    pred_total_final = preds_np['total']\n    pred_gdm_final = preds_np['gdm']\n    pred_green_final = preds_np['green']\n\n    pred_clover_final = preds_np['clover']\n    pred_dead_final = preds_np['dead']\n\n    # 1. 确保非负\n    pred_green_final = np.clip(pred_green_final, a_min=0, a_max=None)\n    pred_clover_final = np.clip(pred_clover_final, a_min=0, a_max=None)\n    pred_dead_final = np.clip(pred_dead_final, a_min=0, a_max=None)\n    \n\n    preds_wide_df = pd.DataFrame({\n        'image_path': test_df_unique['image_path'],\n        'Dry_Green_g': pred_green_final,\n        'Dry_Dead_g': pred_dead_final,\n        'Dry_Clover_g': pred_clover_final,\n        'GDM_g': pred_gdm_final,\n        'Dry_Total_g': pred_total_final\n    })\n\n    preds_wide_df = post_process_biomass(preds_wide_df)\n\n    preds_long_df = preds_wide_df.melt(\n        id_vars=['image_path'],\n        value_vars=CFG.ALL_TARGET_COLS, # \n        var_name='target_name',        # \n        value_name='target'            # \n    )\n\n    submission_df = pd.merge(\n        test_df_long[['sample_id', 'image_path', 'target_name']],\n        preds_long_df,\n        on=['image_path', 'target_name'],\n        how='left'\n    )\n\n    submission_df = submission_df[['sample_id', 'target']]\n    \n    # Lưu file\n    submission_df.to_csv(CFG.SUBMISSION_FILE, index=False)\n\n    print(f\"\\n🎉 HOÀN TẤT! Đã lưu file submission tại: {CFG.SUBMISSION_FILE}\")\n    print(preds_wide_df.head())\n    print(preds_wide_df.tail())\n    \nif __name__ == \"__main__\":\n    all_preds_np, df_long, df_unique = run_inference_with_tta()\n    # ✅ 关键修改：只在主进程写文件（避免多进程冲突）\n    accelerator = Accelerator()\n    if accelerator.is_main_process:\n        create_submission(all_preds_np, df_long, df_unique)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-28T06:21:33.140238Z","iopub.execute_input":"2026-01-28T06:21:33.140488Z","iopub.status.idle":"2026-01-28T06:21:33.155178Z","shell.execute_reply.started":"2026-01-28T06:21:33.140472Z","shell.execute_reply":"2026-01-28T06:21:33.154450Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# https://www.kaggle.com/code/pingfan/infer-code-v3?scriptVersionId=288868442\n\n!accelerate launch --num_processes 2 infer_v1.py \\\n    --output_file sub1_1.csv \\\n    --model_name vit_huge_plus_patch16_dinov3.lvd1689m \\\n    --model_dir /kaggle/input/csiro-model-ckpt/1226_vit_huge_fold3_all_v1_seed42_new_9365/1226_vit_huge_fold3_all_v1_seed42_new_9365/model_fold-1_epoch30_0.9365.pth \\\n    --image_size 512\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-28T06:21:38.111336Z","iopub.execute_input":"2026-01-28T06:21:38.112009Z","iopub.status.idle":"2026-01-28T06:23:29.061599Z","shell.execute_reply.started":"2026-01-28T06:21:38.111982Z","shell.execute_reply":"2026-01-28T06:23:29.060899Z"},"scrolled":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# https://www.kaggle.com/code/pingfan/infer-code-v3?scriptVersionId=288511935\n\n!accelerate launch --num_processes 2 infer_v1.py \\\n    --output_file sub1_2.csv \\\n    --model_name vit_huge_plus_patch16_dinov3.lvd1689m \\\n    --model_dir /kaggle/input/csiro-model-ckpt/1226_vit_huge_fold3_0_v1_seed42_new_7742/1226_vit_huge_fold3_0_v1_seed42_new_7742/model_fold0_epoch30_0.7742.pth \\\n    --image_size 512","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-28T06:23:29.062897Z","iopub.execute_input":"2026-01-28T06:23:29.063154Z","iopub.status.idle":"2026-01-28T06:25:09.462997Z","shell.execute_reply.started":"2026-01-28T06:23:29.063100Z","shell.execute_reply":"2026-01-28T06:25:09.462332Z"},"scrolled":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!accelerate launch --num_processes 2 infer_v2.py \\\n    --output_file sub2_1.csv \\\n    --model_name vit_large_patch16_dinov3.lvd1689m \\\n    --model_dir /kaggle/input/csiro-model-ckpt2/0112_vit_large_cls_fold3_0_v4_sd42_7993_swa/0112_vit_large_cls_fold3_0_v4_sd42_7993_swa/last_swa.pth \\\n    --image_size 1024","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-28T06:25:09.463805Z","iopub.execute_input":"2026-01-28T06:25:09.463997Z","iopub.status.idle":"2026-01-28T06:26:38.386042Z","shell.execute_reply.started":"2026-01-28T06:25:09.463979Z","shell.execute_reply":"2026-01-28T06:26:38.385108Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!accelerate launch --num_processes 2 infer_v4.py \\\n    --output_file sub3_1.csv \\\n    --model_name vit_large_patch16_dinov3.lvd1689m \\\n    --model_dir /kaggle/input/csiro-2025-v3-wo-dead/last_swa.pth \\\n    --image_size 1024","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-28T06:26:38.388154Z","iopub.execute_input":"2026-01-28T06:26:38.388459Z","iopub.status.idle":"2026-01-28T06:28:00.520790Z","shell.execute_reply.started":"2026-01-28T06:26:38.388439Z","shell.execute_reply":"2026-01-28T06:28:00.519888Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pandas as pd\nimport copy\nsub1 = pd.read_csv('sub1_1.csv')\nsub2 = pd.read_csv('sub1_2.csv')\nsub3 = pd.read_csv('sub2_1.csv')\nsub4 = pd.read_csv('sub3_1.csv')\n\n\nsub = copy.deepcopy(sub1)\n\ntarget1 = (sub1['target'] + sub2['target'] ) / 2\ntarget2 = sub3['target']\ntarget3 = sub4['target']\n\n\n\nsub['target'] = (target1 + target2 + target3 ) / 3\nsub.to_csv('submission1.csv', index=False)\n\nsub.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-28T06:28:00.521722Z","iopub.execute_input":"2026-01-28T06:28:00.521937Z","iopub.status.idle":"2026-01-28T06:28:00.811773Z","shell.execute_reply.started":"2026-01-28T06:28:00.521918Z","shell.execute_reply":"2026-01-28T06:28:00.811179Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pandas  as pd\n\nfrom sklearn.model_selection import StratifiedKFold\nimport numpy as np\n\ndata_df = pd.read_csv('/kaggle/input/csiro-biomass/train.csv')\nprint(data_df.shape)\n\n\ndf_wide = data_df.pivot(\n    index=['image_path',],\n    columns='target_name',\n    values='target'\n).reset_index()\ndf_wide = df_wide.reset_index()\ndf_wide.columns.name = None \n\nprint(df_wide.shape)\nprint(df_wide.columns.tolist())\ndf_wide.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-28T06:28:00.812529Z","iopub.execute_input":"2026-01-28T06:28:00.812794Z","iopub.status.idle":"2026-01-28T06:28:01.353745Z","shell.execute_reply.started":"2026-01-28T06:28:00.812766Z","shell.execute_reply":"2026-01-28T06:28:01.352930Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"pseudo_df = pd.read_csv('/kaggle/working/submission1.csv')\npseudo_df['image_path'] = pseudo_df['sample_id'].apply(lambda x: \"test/\" + x.split('__')[0] + \".jpg\")\npseudo_df['target_name'] = pseudo_df['sample_id'].apply(lambda x: x.split('__')[1])\n\ndf_wide2 = pseudo_df.pivot(\n    index=['image_path'],\n    columns='target_name',\n    values='target'\n).reset_index()\ndf_wide2 = df_wide2.reset_index()\ndf_wide2.columns.name = None \n\nprint(df_wide2.shape)\nprint(df_wide2.columns.tolist())\ndf_wide2.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-28T06:28:01.354711Z","iopub.execute_input":"2026-01-28T06:28:01.354956Z","iopub.status.idle":"2026-01-28T06:28:01.371688Z","shell.execute_reply.started":"2026-01-28T06:28:01.354939Z","shell.execute_reply":"2026-01-28T06:28:01.370998Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df_wide2.tail()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-28T06:28:01.373048Z","iopub.execute_input":"2026-01-28T06:28:01.373320Z","iopub.status.idle":"2026-01-28T06:28:01.381671Z","shell.execute_reply.started":"2026-01-28T06:28:01.373303Z","shell.execute_reply":"2026-01-28T06:28:01.381008Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nif not os.getenv('KAGGLE_IS_COMPETITION_RERUN'):\n    print('dev')\n    df_wide.head(10).to_csv('train.csv', index=False)\n    all_df = pd.concat([df_wide, df_wide2], axis=0).reset_index(drop=True)\n    all_df.head(10).to_csv('all_data.csv', index=False)\n    \n    print(df_wide.shape, df_wide2.shape, all_df.shape)\nelse:\n    df_wide.to_csv('train.csv', index=False)\n    all_df = pd.concat([df_wide, df_wide2], axis=0).reset_index(drop=True)\n    all_df.to_csv('all_data.csv', index=False)\n    \n    print(df_wide.shape, df_wide2.shape, all_df.shape)\nall_df.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-28T06:28:01.382502Z","iopub.execute_input":"2026-01-28T06:28:01.382796Z","iopub.status.idle":"2026-01-28T06:28:01.401463Z","shell.execute_reply.started":"2026-01-28T06:28:01.382774Z","shell.execute_reply":"2026-01-28T06:28:01.400947Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"%%writefile train_base.py\n\nimport os\nimport pandas as pd\nimport numpy as np\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import Dataset, DataLoader\nimport albumentations as A\nfrom albumentations.pytorch import ToTensorV2\nimport timm  \nimport cv2  \nimport matplotlib.pyplot as plt\nimport torch.optim as optim\nfrom tqdm import tqdm\nimport time\nimport gc\nimport yaml\nimport argparse\nimport os\nimport random\nimport torch.distributed as dist\nimport shutil\nfrom albumentations import (\n    Compose, \n    Resize, \n    Normalize,\n    HorizontalFlip, \n    VerticalFlip,\n    RandomRotate90,  # Chỉ xoay 90, 180, 270\n    ColorJitter\n)\n\nfrom accelerate import Accelerator  # ✅ accelerate\nfrom accelerate.utils import DistributedType, ProjectConfiguration, set_seed\nimport math\n\ndef seed_everything(seed=None):\n    '''\n    固定seed\n    :param seed: int, 随机种子\n    '''\n    max_seed_value = np.iinfo(np.uint32).max\n    min_seed_value = np.iinfo(np.uint32).min\n\n    if (seed is None) or not (min_seed_value <= seed <= max_seed_value):\n        seed = random.randint(np.iinfo(np.uint32).min, np.iinfo(np.uint32).max)\n    os.environ[\"PYTHONHASHSEED\"] = str(seed)\n    random.seed(seed)\n    np.random.seed(seed)\n    torch.manual_seed(seed)\n    torch.cuda.manual_seed(seed)\n    torch.cuda.manual_seed_all(seed)\n    torch.backends.cudnn.benchmark = False\n    torch.backends.cudnn.deterministic = True\n    return seed\n\n\nTARGET_COLS = ['Dry_Green_g', 'Dry_Dead_g', 'Dry_Clover_g', 'GDM_g', 'Dry_Total_g']\nLOSS_WEIGHTS = {\n    'green_loss': 0.1,\n    'dead_loss': 0.1,\n    'clover_loss': 0.1,\n    'gdm_loss': 0.2,\n    'total_loss': 0.5,\n}\n# 评分目标（严格按照比赛顺序）\nALL_TARGET_COLS = ['Dry_Green_g', 'Dry_Dead_g', 'Dry_Clover_g', 'GDM_g', 'Dry_Total_g']\n\nR2_WEIGHTS = [0.1, 0.1, 0.1, 0.2, 0.5]\nfrom sklearn.metrics import r2_score\ndef calculate_competition_score(all_preds_3, all_targets_5):\n    \n    pred_total = all_preds_3['total']\n    pred_gdm = all_preds_3['gdm']\n    pred_green = all_preds_3['green']\n    pred_clover = all_preds_3['clover']\n    pred_dead = all_preds_3['dead']\n    \n    y_preds = np.stack([\n        pred_green,\n        pred_dead,\n        pred_clover,\n        pred_gdm,\n        pred_total\n    ], axis=1) # Shape: [N, 5]\n    \n    y_true = all_targets_5 # Shape: [N, 5]\n\n    r2_scores = r2_score(y_true, y_preds, multioutput='raw_values')\n    \n    weighted_r2_total = 0.0\n    for i, weight in enumerate(R2_WEIGHTS):\n        weighted_r2_total += r2_scores[i] * weight\n        \n    return weighted_r2_total\n\n    \n    \nclass BiomassDataset(Dataset):\n    \"\"\"\n    自定义数据集\n    \n    Sẽ trả về:\n    (img_left, img_right, train_targets (3), all_targets (5))\n    \"\"\"\n    def __init__(self, df, transforms_fn, image_dir, train_target_cols, all_target_cols):\n        self.df = df\n        \n        self.transforms_fn = transforms_fn\n        self.image_dir = image_dir\n        \n        self.image_paths = df['image_path'].values\n        self.train_targets = df[train_target_cols].values\n        self.all_targets = df[all_target_cols].values\n\n    def __len__(self):\n        return len(self.df)\n\n        \n    def __getitem__(self, idx):\n        # 1. Lấy thông tin\n        img_path_suffix = self.image_paths[idx]\n        train_target_vals = self.train_targets[idx]\n        all_target_vals = self.all_targets[idx]\n        \n        # 2. 读取原图 (2000x1000)。\n        # filename = os.path.basename(img_path_suffix)\n        full_path = os.path.join(self.image_dir, img_path_suffix)\n        image = cv2.imread(full_path)\n        if image is None:\n            raise FileNotFoundError(f\"无法读取图片: {full_path}\")\n        image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)\n        \n        # 3. （将图片）裁剪成 2 张图片（左和右）。\n        height, width, _ = image.shape\n        mid_point = width // 2\n        img_left = image[:, :mid_point]\n        img_right = image[:, mid_point:]\n        \n        # 4. （对两张图片）分别/独立地进行数据增强。\n        transforms = self.transforms_fn()\n        img_left_tensor = transforms(image=img_left)['image']\n        \n        # （为右图）创建新的处理流程（以实现独立增强）。\n        transforms_2 = self.transforms_fn()\n        img_right_tensor = transforms_2(image=img_right)['image']\n        \n        # 5. 获取目标（Targets）。\n        train_target_tensor = torch.tensor(train_target_vals, dtype=torch.float32)\n        all_targets_tensor = torch.tensor(all_target_vals, dtype=torch.float32)\n        \n        return img_left_tensor, img_right_tensor, train_target_tensor, all_targets_tensor\n\n\n\nclass WeightedBiomassLoss(nn.Module):\n    \"\"\"\n    计算3个独立的MSELoss并将它们相加，\n    权重来自配置参数(CFG)。\n    \"\"\"\n    def __init__(self, loss_weights_dict):\n        super(WeightedBiomassLoss, self).__init__()\n        \n        self.criterion = nn.SmoothL1Loss()\n        # self.criterion = nn.HuberLoss(delta=1.0)\n        \n        \n        # 权重\n        self.weights = loss_weights_dict\n        # self.constraint_weight = 0.1\n        # self.label_smoothing = 0.1\n\n        self.constraint_weight = 0.15  # 增加物理约束权重\n        self.correlation_weight = 0.1   # 新增：相关性损失权重\n\n\n    def forward(self, predictions, targets):\n        \"\"\"\n        # predictions: 来自模型的元组 (out_total, out_gdm, out_green)\n        # targets: 来自数据加载器的张量 [batch_size, 3]\n        \"\"\"\n        \n        # 1. 拆分预测结果\n        pred_green, pred_dead, pred_clover, pred_gdm, pred_total = predictions\n\n        true_green = targets[:, 0].unsqueeze(-1) # Shape [batch, 1]\n        true_dead   = targets[:, 1].unsqueeze(-1) # Shape [batch, 1]\n        true_clover = targets[:, 2].unsqueeze(-1) # Shape [batch, 1]\n        true_gdm = targets[:, 3].unsqueeze(-1) # Shape [batch, 1]\n        true_total = targets[:, 4].unsqueeze(-1) # Shape [batch, 1]\n        \n        \n        # 3. 计算3个独立的损失函数\n        loss_green = self.criterion(pred_green, true_green)\n        loss_dead = self.criterion(pred_dead, true_dead)\n        loss_clover = self.criterion(pred_clover, true_clover)\n        loss_gdm   = self.criterion(pred_gdm, true_gdm)\n        loss_total = self.criterion(pred_total, true_total)\n\n        # 4. 应用权重并求和\n        total_loss = (\n            self.weights['total_loss'] * loss_total +\n            self.weights['gdm_loss'] * loss_gdm +\n            self.weights['green_loss'] * loss_green + \n            self.weights['dead_loss'] * loss_dead +\n            self.weights['clover_loss'] * loss_clover\n        )\n        \n        return total_loss\n\n\n\nclass FeatureInteractionModule(nn.Module):\n    \"\"\"特征交互模块，增强左右图像特征的关系建模\"\"\"\n    def __init__(self, hidden_dim):\n        super().__init__()\n        self.attention = nn.MultiheadAttention(\n            embed_dim=hidden_dim, \n            num_heads=4, \n            batch_first=True\n        )\n        self.norm = nn.LayerNorm(hidden_dim)\n        self.ffn = nn.Sequential(\n            nn.Linear(hidden_dim, hidden_dim * 2),\n            nn.ReLU(),\n            nn.Dropout(0.1),\n            nn.Linear(hidden_dim * 2, hidden_dim)\n        )\n        \n    def forward(self, left_feat, right_feat):\n        # 拼接左右特征 [B, 2, D]\n        combined = torch.stack([left_feat, right_feat], dim=1)\n        \n        # 自注意力交互\n        attended, _ = self.attention(combined, combined, combined)\n        attended = self.norm(attended + combined)\n        \n        # FFN\n        output = self.norm(attended + self.ffn(attended))\n        \n        # 分离左右特征\n        left_out = output[:, 0, :]\n        right_out = output[:, 1, :]\n        \n        return left_out, right_out\n\n\nclass BiomassModel(nn.Module):\n    \"\"\"\n    双流模型，三个专用输出头。\n    \n    1. 一个共享的主干网络（例如：ConvNeXt）。\n    2. 该主干网络分别应用于左图和右图。\n    3. 两个特征向量被拼接（Concatenate）在一起。\n    4. 合并后的向量被送入三个独立的 MLP \"头\"。\n\n\n    \"\"\"\n    def __init__(self, model_name, pretrained, n_targets=3):\n        super(BiomassModel, self).__init__()\n        \n        # 1. 加载共享主干网络\n        # num_classes=0: 移除原始分类层\n        # global_pool='avg': 添加全局平均池化层\n        #  以提取一维特征向量\n\n        model_dir_dict = {\n            \"vit_large_patch16_dinov3.lvd1689m\": '/kaggle/input/opensource-models/models--timm--vit_large_patch16_dinov3.lvd1689m',\n            \"vit_base_patch16_dinov3.lvd1689m\": '/kaggle/input/opensource-models/models--timm--vit_base_patch16_dinov3.lvd1689m',\n            \"vit_huge_plus_patch16_dinov3.lvd1689m\": '/kaggle/input/opensource-models/models--timm--vit_huge_plus_patch16_dinov3.lvd1689m',\n        }\n        self.backbone = timm.create_model(\n            model_name,\n            pretrained=pretrained,\n            cache_dir=model_dir_dict[model_name],\n            num_classes=0,\n            global_pool='avg'\n        )\n        \n        # 2. 从主干网络获取特征维度\n        # 例如：convnext_tiny 是 768，resnet50 是 2048\n        self.n_features = self.backbone.num_features\n        \n        # 3. 计算融合后的维度\n        # (features_left + features_right)\n        self.n_combined_features = self.n_features * 2\n        \n        dropout = 0.3\n        # ---Dry_Green_g ---\n        self.head_green = nn.Sequential(\n            nn.Linear(self.n_combined_features, self.n_combined_features // 2),\n            nn.ReLU(),\n            nn.Dropout(dropout),\n            nn.Linear(self.n_combined_features // 2, 1)\n        )\n        \n        # Dry_Dead_g\n        self.head_dead = nn.Sequential(\n            nn.Linear(self.n_combined_features, self.n_combined_features // 2),\n            nn.ReLU(),\n            nn.Dropout(dropout),\n            nn.Linear(self.n_combined_features // 2, 1) \n        )\n\n        # Dry_Clover_g\n        self.head_clover = nn.Sequential(\n            nn.Linear(self.n_combined_features, self.n_combined_features // 2),\n            nn.ReLU(),\n            nn.Dropout(dropout),\n            nn.Linear(self.n_combined_features // 2, 1) \n        )\n\n        # --- GDM_g ---\n        self.head_gdm = nn.Sequential(\n            nn.Linear(self.n_combined_features, self.n_combined_features // 2),\n            nn.ReLU(),\n            nn.Dropout(dropout),\n            nn.Linear(self.n_combined_features // 2, 1)\n        )\n        # --- Dry_Total_g 头 ---\n        self.head_total = nn.Sequential(\n            nn.Linear(self.n_combined_features, self.n_combined_features // 2),\n            nn.ReLU(),\n            nn.Dropout(dropout),\n            nn.Linear(self.n_combined_features // 2, 1) \n        )\n\n        self.feature_interaction = FeatureInteractionModule(self.n_features)\n        \n\n    def forward(self, img_left, img_right):\n        # 1. 左图\n        features_left = self.backbone(img_left) # Shape: [batch, n_features]\n        \n        # 2. 右图\n        features_right = self.backbone(img_right) # Shape: [batch, n_features]\n\n        # 2. 特征交互\n        features_left, features_right = self.feature_interaction(\n            features_left, features_right\n        )\n\n        # 3. 特征融合\n        # \n        combined = torch.cat([features_left, features_right], dim=1) # Shape: [batch, n_combined_features]\n        \n        # 4. 输出\n        out_green = self.head_green(combined)\n        out_dead = self.head_dead(combined)\n        out_clover = self.head_clover(combined)\n        out_gdm = self.head_gdm(combined)\n        out_total = self.head_total(combined)\n\n\n        return out_green, out_dead, out_clover, out_gdm, out_total\n\n        \ndef get_train_transforms(args):\n    \"\"\"\n    为训练进行数据增强。\n    将独立应用于左图和右图。\n    \"\"\"\n    return Compose([\n        # 1. 几何增强（Geometric）\n        HorizontalFlip(p=0.5),\n        VerticalFlip(p=0.5),\n        RandomRotate90(p=0.5), # 自动旋转 90、180 或 270 度。\n\n        A.GaussNoise(p=0.3),  # 高斯噪声模拟图像质量变化\n        \n        A.RandomBrightnessContrast(\n            brightness_limit=0.2,\n            contrast_limit=0.2,\n            p=0.75\n        ),\n        A.HueSaturationValue(\n            hue_shift_limit=10,\n            sat_shift_limit=20,\n            val_shift_limit=20,\n            p=0.5\n        ),\n        A.CLAHE(clip_limit=2.0, tile_grid_size=(8, 8), p=0.3),  # 对比度限制自适应直方图均衡化\n        \n        \n\n        # 2. 色彩增强（Photometric）\n        ColorJitter(\n            brightness=0.2, \n            contrast=0.2, \n            saturation=0.2, \n            hue=0.1, \n            p=0.75\n        ),\n\n        # 3. 标准化（Normalize）\n        # ImageNet的标准mean/std值\n        Normalize(\n            mean=[0.485, 0.456, 0.406],\n            std=[0.229, 0.224, 0.225]\n        ),\n        \n        # 4. 压缩图像并转换为张量。\n        #（在增强后进行压缩以保证质量）\n        Resize(args.img_size, args.img_size),\n        ToTensorV2()\n    ])\n\ndef get_valid_transforms(args):\n    \"\"\"\n    仅对验证集进行标准化和图像压缩（不进行增强）。\n    \"\"\"\n    return Compose([\n        Normalize(\n            mean=[0.485, 0.456, 0.406],\n            std=[0.229, 0.224, 0.225]\n        ),\n        Resize(args.img_size, args.img_size),\n        ToTensorV2()\n    ])\n    \n\ndef train_one_epoch(model, loader, criterion, optimizer, accelerator: Accelerator):\n    model.train()\n    device = accelerator.device\n    epoch_loss = 0.0\n\n    pbar = tqdm(\n        range(math.ceil(len(loader) / args.gradient_accumulation_steps)), \n        desc=\"Training\", \n        leave=False, \n        disable=not accelerator.is_main_process\n        )\n\n\n    for (img_left, img_right, train_targets, _all_targets_ignored) in loader:\n        with accelerator.accumulate(model):\n            img_left = img_left.to(device)\n            img_right = img_right.to(device)\n            targets = train_targets.to(device) \n            with accelerator.autocast():\n                predictions = model(img_left, img_right)\n                loss = criterion(predictions, targets)\n            # predictions = model(img_left, img_right)\n            # loss = criterion(predictions, targets)\n            \n            accelerator.backward(loss)\n            accelerator.clip_grad_norm_(model.parameters(), 1.0)\n            optimizer.step()\n            # optimizer.zero_grad(set_to_none=args.set_grads_to_none)\n            optimizer.zero_grad()\n            \n            if accelerator.sync_gradients:\n                pbar.update(1)\n        epoch_loss += loss.item()\n        pbar.set_postfix(loss=f'{loss.item():.4f}') \n       \n    return epoch_loss / len(loader)\n\n\ndef validate_one_epoch(model, loader, criterion, accelerator):\n    model.eval()\n    device = accelerator.device\n    epoch_loss = 0.0\n\n    all_preds_3 = {'total': [], 'gdm': [], 'green': [], 'dead': [], \"clover\": []}\n    all_targets_list = []\n    \n    with torch.no_grad():\n        pbar = tqdm(loader, desc=\"Validating\", leave=False, disable=not accelerator.is_main_process)\n        for (img_left, img_right, train_targets, all_targets) in pbar:\n            \n            img_left = img_left.to(device)\n            img_right = img_right.to(device)\n            train_targets = train_targets.to(device) \n            \n            # 1. Forward pass\n            # out_green, out_dead, out_clover, out_gdm, out_total\n            pred_green, pred_dead, pred_clover, pred_gdm, pred_total = model(img_left, img_right)\n            \n            predictions_tuple = (pred_green, pred_dead, pred_clover, pred_gdm, pred_total)\n            loss = criterion(predictions_tuple, train_targets)\n            epoch_loss += loss.item()\n            \n            all_preds_3['total'].append(accelerator.gather_for_metrics(pred_total).cpu().numpy())\n            all_preds_3['gdm'].append(accelerator.gather_for_metrics(pred_gdm).cpu().numpy())\n            all_preds_3['green'].append(accelerator.gather_for_metrics(pred_green).cpu().numpy())\n            all_preds_3['dead'].append(accelerator.gather_for_metrics(pred_dead).cpu().numpy())\n            all_preds_3['clover'].append(accelerator.gather_for_metrics(pred_clover).cpu().numpy())\n            \n            all_targets_list.append(accelerator.gather_for_metrics(all_targets).cpu().numpy())\n\n    preds_dict_np = {\n        'total': np.concatenate(all_preds_3['total']).flatten(),\n        'gdm':   np.concatenate(all_preds_3['gdm']).flatten(),\n        'green': np.concatenate(all_preds_3['green']).flatten(),\n        'dead': np.concatenate(all_preds_3['dead']).flatten(),\n        'clover': np.concatenate(all_preds_3['clover']).flatten(),\n        \n    }\n    targets_np_5 = np.concatenate(all_targets_list) # Shape [N, 5]\n    \n    competition_score = calculate_competition_score(preds_dict_np, targets_np_5)\n\n    avg_epoch_loss = epoch_loss / len(loader)\n    \n    return avg_epoch_loss, competition_score\n\n    \ndef train(args):\n    # set the wandb project where this run will be logged\n    # os.environ[\"WANDB_PROJECT\"]=args.output_dir\n    from time import gmtime, strftime\n    start_time = time.time()\n    accelerator = Accelerator(\n        gradient_accumulation_steps=args.gradient_accumulation_steps,\n        mixed_precision=args.mixed_precision,\n    )\n\n    device = accelerator.device\n\n    if accelerator.is_main_process:\n        os.makedirs(args.output_dir, exist_ok=True)\n        shutil.copy2(\"train_base.py\", args.output_dir)\n        shutil.copy2(args.config, args.output_dir)\n\n    weight_dtype = torch.float32\n    if accelerator.mixed_precision == \"fp16\":\n        weight_dtype = torch.float16\n        args.mixed_precision = accelerator.mixed_precision\n    elif accelerator.mixed_precision == \"bf16\":\n        weight_dtype = torch.bfloat16\n        args.mixed_precision = accelerator.mixed_precision\n    \n    accelerator.print('all weight_dtype:', weight_dtype)\n    \n    # 加载数据\n    data_df = pd.read_csv(args.data_path)\n    all_data_df = pd.read_csv(args.all_data_path)\n    \n\n    train_dataset = BiomassDataset(\n        df=data_df, transforms_fn=lambda: get_train_transforms(args), image_dir=args.image_dir,\n        train_target_cols=TARGET_COLS, all_target_cols=ALL_TARGET_COLS\n    )\n    all_train_dataset = BiomassDataset(\n        df=all_data_df, transforms_fn=lambda: get_train_transforms(args), image_dir=args.image_dir,\n        train_target_cols=TARGET_COLS, all_target_cols=ALL_TARGET_COLS\n    )\n    accelerator.print('train:', data_df.shape)\n    accelerator.print('all train:', all_data_df.shape)\n\n\n    train_loader = DataLoader(\n        train_dataset, batch_size=args.batch_size, shuffle=True,\n        num_workers=args.num_workers, pin_memory=True\n    )\n\n    all_train_loader = DataLoader(\n        all_train_dataset, batch_size=args.batch_size, shuffle=True,\n        num_workers=args.num_workers, pin_memory=True\n    )\n\n    # 加载模型\n    accelerator.print(f\"加载模型 '{args.model_name}'...\")\n    model = BiomassModel(args.model_name, args.pretrained)\n\n    criterion = WeightedBiomassLoss(LOSS_WEIGHTS).to(device)\n    \n\n    import copy\n    for param in model.backbone.parameters():\n        param.requires_grad = False\n\n    optimizer = optim.Adam(\n        filter(lambda p: p.requires_grad, model.parameters()), \n        lr=args.learning_rate\n    )\n\n    scheduler = optim.lr_scheduler.ReduceLROnPlateau(\n        optimizer, mode='min', factor=0.1, patience=2 # Giảm patience\n    )\n\n    model, optimizer, train_loader, all_train_loader, scheduler = accelerator.prepare(\n        model, optimizer, train_loader, all_train_loader, scheduler\n    )\n\n    def unwrap_model(model):\n        return accelerator.unwrap_model(model)\n\n    \n    best_score = -float('inf') \n    pseudo_epoch = 10\n    for epoch in range(0, args.freeze_epochs):\n        accelerator.print(f\"\\nEpoch {epoch + 1}/{args.num_train_epochs + pseudo_epoch} (Stage 1)\")\n        if args.train_mode == 'mix':\n            train_loss = train_one_epoch(model, train_loader, criterion, optimizer, accelerator)\n        else:  \n            train_loss = train_one_epoch(model, all_train_loader, criterion, optimizer, accelerator)\n            # train_loss = train_one_epoch(model, train_loader, criterion, optimizer, accelerator)\n        \n        scheduler.step(train_loss)\n        \n        accelerator.print(f\"Epoch {epoch} - Train Loss: {train_loss:.4f} \")\n    \n\n    # ===== Stage 2: Fine-tune Entire Model =====\n    accelerator.print(f\"\\n--- Stage 2: Full Fine-tuning (Epoch {args.freeze_epochs+1}-{args.num_train_epochs}) ---\")\n\n    unwrapped = accelerator.unwrap_model(model)\n    for p in unwrapped.backbone.parameters():\n        p.requires_grad = True\n    \n    # optimizer = optim.Adam(\n    #     model.parameters(),\n    #     lr=args.finetune_lr\n    # )\n\n    optimizer = optim.AdamW(\n        [\n            {'params': unwrapped.backbone.parameters(), 'lr': args.finetune_lr},\n            {'params': [p for n, p in unwrapped.named_parameters() \n                        if 'backbone' not in n], 'lr': 1.e-4}\n            \n        ],\n        # model.parameters(),\n        lr=args.finetune_lr\n    )\n\n    \n    scheduler = optim.lr_scheduler.ReduceLROnPlateau(\n        optimizer, mode='min', factor=0.2, patience=3\n    )\n    model, optimizer, train_loader, all_train_loader, scheduler = accelerator.prepare(\n        model, optimizer, train_loader, all_train_loader, scheduler\n    )\n\n    for epoch in range(args.freeze_epochs, args.freeze_epochs + pseudo_epoch):\n        accelerator.print(f\"\\n--- Epoch {epoch + 1}/{args.num_train_epochs + pseudo_epoch}  (Stage 3 pseduo)\")\n     \n        train_loss = train_one_epoch(model, all_train_loader, criterion, optimizer, accelerator)\n        \n        scheduler.step(train_loss)\n        \n        accelerator.print(f\"Epoch {epoch} - Train Loss: {train_loss:.4f}\")\n        \n       \n    \n    for epoch in range(args.freeze_epochs + pseudo_epoch, args.num_train_epochs + pseudo_epoch):\n        accelerator.print(f\"\\n--- Epoch {epoch + 1}/{args.num_train_epochs + pseudo_epoch}  (Stage 3 pseduo)\")\n        train_loss = train_one_epoch(model, train_loader, criterion, optimizer, accelerator)\n\n        # model_copy = copy.deepcopy(model)\n        # model_copy = model_copy.half()\n\n        \n        # if args.train_mode == 'mix':\n        #     if epoch <= 20:\n        #         train_loss = train_one_epoch(model, train_loader, criterion, optimizer, accelerator)\n        #     else:\n        #         train_loss = train_one_epoch(model, all_train_loader, criterion, optimizer, accelerator)\n        # else:\n        #     train_loss = train_one_epoch(model, all_train_loader, criterion, optimizer, accelerator)\n        \n        scheduler.step(train_loss)\n        \n        accelerator.print(f\"Epoch {epoch} - Train Loss: {train_loss:.4f}\")\n        \n        accelerator.wait_for_everyone()\n        if accelerator.is_main_process:\n            to_save = accelerator.unwrap_model(model).state_dict()\n            # unwrapped_copy = unwrap_model(model_copy)\n            if epoch > 42:\n                torch.save(to_save, f'{args.output_dir}/model_epoch{epoch}.pth')\n                \n            # save_path = os.path.join(args.output_dir, \"model_last.pth\")\n            # accelerator.save(to_save, save_path)\n            \n    \n    \n    end_time = time.time()\n    accelerator.print(f\"\\n🎉 完成第{args.fold_to_run}折，消耗{(end_time - start_time)/60:.2f} 分钟.\")\n    accelerator.print(f\"最好结果cv: {best_score:.4f}\")\n    \n    # Dọn dẹp\n    del model, train_loader, train_dataset\n    gc.collect()\n    torch.cuda.empty_cache()\n    \n\n        \ndef load_config(config_file):\n    with open(config_file, 'r') as file:\n        config = yaml.safe_load(file)\n    return config\n\nif __name__ == \"__main__\":\n\n    parser = argparse.ArgumentParser(description='CSIRO - Image2Biomass Prediction')\n    parser.add_argument('--config', default='train_base.yaml', type=str, help='Path to the config file', required=False)\n    args = parser.parse_args()\n\n    config = load_config(args.config)\n\n    for key, value in config.items():\n        parser.add_argument(f'--{key}', default=value, type=type(value))\n    args = parser.parse_args()\n\n    seed_everything(args.seed)\n    \n    train(args)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-28T06:28:01.403815Z","iopub.execute_input":"2026-01-28T06:28:01.404051Z","iopub.status.idle":"2026-01-28T06:28:01.418871Z","shell.execute_reply.started":"2026-01-28T06:28:01.404034Z","shell.execute_reply":"2026-01-28T06:28:01.418096Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"%%writefile train_large1.yaml\n\ndata_path: /kaggle/working/train.csv\nall_data_path: /kaggle/working/all_data.csv\nimage_dir: /kaggle/input/csiro-biomass\nfold_to_run: 0\nbatch_size: 4\nimg_size: 448\nnum_workers: 4\nmodel_name: vit_large_patch16_dinov3.lvd1689m\npretrained: True\nlearning_rate: 1.e-4\nfinetune_lr: 1.e-5\nwarmup_ratio: 0.1\nwarmup_steps: 30\nnum_train_epochs: 40\nfreeze_epochs: 15\ngradient_accumulation_steps: 1\nlogging_steps: 10\neval_steps: 200\nsave_steps: 50\noutput_dir: output/ckpt\nseed: 42\ntrain_mode: mix\nmixed_precision: fp16\nckpt_file: /kaggle/input/csiro-model-ckpt/1208_vit_large_fold3_all_v1_lb67/1208_vit_large_fold3_all_v1_lb67/model_fold-1_epoch30_0.9119.pth","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-28T06:28:01.419590Z","iopub.execute_input":"2026-01-28T06:28:01.419917Z","iopub.status.idle":"2026-01-28T06:28:01.434191Z","shell.execute_reply.started":"2026-01-28T06:28:01.419902Z","shell.execute_reply":"2026-01-28T06:28:01.433466Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"%%writefile train_large2.yaml\n\ndata_path: /kaggle/working/train.csv\nall_data_path: /kaggle/working/all_data.csv\nimage_dir: /kaggle/input/csiro-biomass\nfold_to_run: 0\nbatch_size: 4\nimg_size: 448\nnum_workers: 4\nmodel_name: vit_large_patch16_dinov3.lvd1689m\npretrained: True\nlearning_rate: 1.e-4\nfinetune_lr: 1.e-5\nwarmup_ratio: 0.1\nwarmup_steps: 30\nnum_train_epochs: 40\nfreeze_epochs: 15\ngradient_accumulation_steps: 1\nlogging_steps: 10\neval_steps: 200\nsave_steps: 50\noutput_dir: output2/ckpt\nseed: 42\ntrain_mode: all\nmixed_precision: fp16\nckpt_file: /kaggle/input/csiro-model-ckpt/1208_vit_large_fold3_all_v1_lb67/1208_vit_large_fold3_all_v1_lb67/model_fold-1_epoch30_0.9119.pth","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-28T06:28:01.434971Z","iopub.execute_input":"2026-01-28T06:28:01.435257Z","iopub.status.idle":"2026-01-28T06:28:01.447688Z","shell.execute_reply.started":"2026-01-28T06:28:01.435243Z","shell.execute_reply":"2026-01-28T06:28:01.447173Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"%%writefile train_large1.sh\n\nset -ex\n\nMASTER_PORT=$(shuf -n 1 -i 10000-65535)\necho ${MASTER_PORT}\n\n    \nCUDA_VISIBLE_DEVICES=0 accelerate launch --num_processes 1  train_base.py \\\n    --config ./train_large1.yaml \\","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-28T06:28:01.448427Z","iopub.execute_input":"2026-01-28T06:28:01.448893Z","iopub.status.idle":"2026-01-28T06:28:01.459069Z","shell.execute_reply.started":"2026-01-28T06:28:01.448873Z","shell.execute_reply":"2026-01-28T06:28:01.458345Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"%%writefile train_large2.sh\n\nset -ex\n\nMASTER_PORT=$(shuf -n 1 -i 10000-65535)\necho ${MASTER_PORT}\n\n    \nCUDA_VISIBLE_DEVICES=1 accelerate launch --num_processes 1 train_base.py \\\n    --config ./train_large2.yaml \\","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-28T06:28:01.460021Z","iopub.execute_input":"2026-01-28T06:28:01.460260Z","iopub.status.idle":"2026-01-28T06:28:01.468843Z","shell.execute_reply.started":"2026-01-28T06:28:01.460238Z","shell.execute_reply":"2026-01-28T06:28:01.468362Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"%%writefile train1.sh\n\nsh train_large1.sh &\nsh train_large2.sh &\nwait \necho \"所有训练任务已完成\"","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-28T06:28:01.469578Z","iopub.execute_input":"2026-01-28T06:28:01.469851Z","iopub.status.idle":"2026-01-28T06:28:01.481155Z","shell.execute_reply.started":"2026-01-28T06:28:01.469836Z","shell.execute_reply":"2026-01-28T06:28:01.480458Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# !sh train_base.sh & sh train_base2.sh\n!sh train1.sh\n# !sh train_large1.sh","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-28T06:28:01.481786Z","iopub.execute_input":"2026-01-28T06:28:01.481991Z","iopub.status.idle":"2026-01-28T06:33:55.873100Z","shell.execute_reply.started":"2026-01-28T06:28:01.481976Z","shell.execute_reply":"2026-01-28T06:33:55.872370Z"},"scrolled":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### SWA","metadata":{}},{"cell_type":"code","source":"import torch\ndef get_swa(model_path, save_path):\n    \"\"\"\n    swa 滑动平均模型，一般在训练平稳阶段再使用 SWA\n    \"\"\"\n    model_path_list = [file for file in os.listdir(model_path) if '.ipynb_checkpoints' not in file and 'best_model' not in file and '.py' not in file and '.yaml' not in file and 'swa' not in file]\n\n    print(len(model_path_list))\n    \n    checkpoints = []\n    for _ckpt in model_path_list:\n        print(_ckpt)\n        # 使用 torch.load 加载 .bin 文件\n        checkpoint = torch.load(os.path.join(model_path, _ckpt), map_location='cpu')# ['state_dict']\n        \n        checkpoints.append(checkpoint)\n        \n    swa_check = {}\n    for checkpoint in checkpoints:\n        for key, item in checkpoint.items():\n            if key not in swa_check:\n                swa_check[key] = item / len(checkpoints)\n            else:\n                swa_check[key] = swa_check[key] + item / len(checkpoints)\n    tmp_ckpt = swa_check\n\n    torch.save(tmp_ckpt, os.path.join(save_path, \"last_swa.pth\"))\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-28T06:33:55.874062Z","iopub.execute_input":"2026-01-28T06:33:55.874269Z","iopub.status.idle":"2026-01-28T06:33:58.331528Z","shell.execute_reply.started":"2026-01-28T06:33:55.874250Z","shell.execute_reply":"2026-01-28T06:33:58.330648Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"get_swa('/kaggle/working/output/ckpt', '/kaggle/working/output')\n!rm -rf /kaggle/working/output/ckpt","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-28T06:33:58.332408Z","iopub.execute_input":"2026-01-28T06:33:58.332950Z","iopub.status.idle":"2026-01-28T06:34:31.917405Z","shell.execute_reply.started":"2026-01-28T06:33:58.332930Z","shell.execute_reply":"2026-01-28T06:34:31.916301Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"get_swa('/kaggle/working/output2/ckpt', '/kaggle/working/output2')\n!rm -rf /kaggle/working/output2/ckpt","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-28T06:34:31.918946Z","iopub.execute_input":"2026-01-28T06:34:31.919314Z","iopub.status.idle":"2026-01-28T06:35:13.294553Z","shell.execute_reply.started":"2026-01-28T06:34:31.919279Z","shell.execute_reply":"2026-01-28T06:35:13.293451Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### 推理","metadata":{}},{"cell_type":"code","source":"%%writefile infer_v3.py\nfrom accelerate import Accelerator\nimport os\nimport pandas as pd\nimport numpy as np\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import Dataset, DataLoader\nimport albumentations as A\nfrom albumentations.pytorch import ToTensorV2\nimport timm\nimport cv2\nfrom tqdm import tqdm\nimport gc\n\n\nimport argparse\n\nparser = argparse.ArgumentParser(description='gamma inference')\nparser.add_argument('--model_dir', type=str, default = '/kaggle/input/qwen2.5/transformers/32b-instruct-awq/1')\nparser.add_argument('--model_name', type=str, default = 'vit_huge_plus_patch16_dinov3.lvd1689m')\nparser.add_argument('--output_file', type=str, default = 'submission.json')\nparser.add_argument('--image_size', type=int, default = 448)\n\n\nargs = parser.parse_args()\nprint(args)\n\n\nclass CFG:\n    BASE_PATH = '/kaggle/input/csiro-biomass'\n    TEST_CSV = os.path.join(BASE_PATH, 'test.csv')\n    TEST_IMAGE_DIR = os.path.join(BASE_PATH, 'test')\n    SUBMISSION_FILE = 'submission.csv'\n    \n\n    MODEL_DIR = '/kaggle/input/csiro-model-ckpt/1209_vit_huge_fold3_all_v1_70_9466/1209_vit_huge_fold3_all_v1_70_9466/model_fold-1_epoch30_0.9466.pth' # Giả sử 5 file .pth nằm cùng thư mục\n    MODEL_NAME = 'vit_huge_plus_patch16_dinov3.lvd1689m'\n    IMG_SIZE = 448\n\n    \n    DEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n    BATCH_SIZE = 1 \n    NUM_WORKERS = 1\n    N_FOLDS = 5\n    \n    # --- Mục tiêu & Loss (PHẢI TRÙNG KHỚP) ---\n    # 3 mục tiêu model đã dự đoán\n    TARGET_COLS = ['Dry_Green_g', 'Dry_Dead_g', 'Dry_Clover_g', 'GDM_g', 'Dry_Total_g']\n    \n    # 5 mục tiêu để nộp bài\n    ALL_TARGET_COLS = ['Dry_Green_g', 'Dry_Dead_g', 'Dry_Clover_g', 'GDM_g', 'Dry_Total_g']\n\n\nCFG.SUBMISSION_FILE = args.output_file\nCFG.MODEL_DIR = args.model_dir\nCFG.MODEL_NAME = args.model_name\nCFG.IMG_SIZE = args.image_size\n\n\n\n# ===============================================================\n# 2. 🏞️ AUGMENTATIONS (CHỈ DÙNG VALIDATION)\n# ===============================================================\nfrom albumentations import (\n    Compose, \n    Resize, \n    Normalize,\n    HorizontalFlip, \n    VerticalFlip\n)\n\ndef get_tta_transforms():\n    \"\"\"\n    Trả về một LIST các pipeline transform cho TTA.\n    Mỗi pipeline là một \"view\" khác nhau của ảnh.\n    \"\"\"\n    \n    # -----------------\n    # View 1: Ảnh gốc (Chỉ Resize + Normalize)\n    # -----------------\n    original_view = Compose([\n        \n        Normalize(\n            mean=[0.485, 0.456, 0.406],\n            std=[0.229, 0.224, 0.225]\n        ),\n        Resize(CFG.IMG_SIZE, CFG.IMG_SIZE),\n        ToTensorV2()\n    ])\n    \n    # -----------------\n    # View 2: Lật ngang (HFlip)\n    # -----------------\n    hflip_view = Compose([\n        HorizontalFlip(p=1.0), # Luôn luôn lật\n        Normalize(\n            mean=[0.485, 0.456, 0.406],\n            std=[0.229, 0.224, 0.225]\n        ),\n        Resize(CFG.IMG_SIZE, CFG.IMG_SIZE),\n        ToTensorV2()\n    ])\n    \n    # -----------------\n    # View 3: Lật dọc (VFlip)\n    # -----------------\n    vflip_view = Compose([\n        VerticalFlip(p=1.0), # Luôn luôn lật\n        Normalize(\n            mean=[0.485, 0.456, 0.406],\n            std=[0.229, 0.224, 0.225]\n        ),\n        Resize(CFG.IMG_SIZE, CFG.IMG_SIZE),\n        ToTensorV2()\n    ])\n    \n    return [original_view, hflip_view, vflip_view]\n\n\n\n\n\nclass TestBiomassDataset(Dataset):\n    \"\"\"\n    \"\"\"\n    def __init__(self, df, transform_pipeline, image_dir):\n        self.df = df\n        self.transforms = transform_pipeline \n        self.image_dir = image_dir\n        self.image_paths = df['image_path'].values\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        # 1. Lấy thông tin\n        img_path_suffix = self.image_paths[idx]\n        \n        # 2. Đọc ảnh gốc (2000x1000)\n        filename = os.path.basename(img_path_suffix)\n        full_path = os.path.join(self.image_dir, filename)\n        \n        image = cv2.imread(full_path)\n        if image is None:\n            print(f\"没有图片: {full_path}. 找到\")\n            image = np.zeros((1000, 2000, 3), dtype=np.uint8)\n        \n        image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)\n        \n        height, width, _ = image.shape\n        mid_point = width // 2\n        img_left = image[:, :mid_point]\n        img_right = image[:, mid_point:]\n        \n        img_left_tensor = self.transforms(image=img_left)['image']\n        img_right_tensor = self.transforms(image=img_right)['image']\n        \n        return idx, img_left_tensor, img_right_tensor\n\nclass FeatureInteractionModule(nn.Module):\n    \"\"\"特征交互模块，增强左右图像特征的关系建模\"\"\"\n    def __init__(self, hidden_dim):\n        super().__init__()\n        self.attention = nn.MultiheadAttention(\n            embed_dim=hidden_dim, \n            num_heads=4, \n            batch_first=True\n        )\n        self.norm = nn.LayerNorm(hidden_dim)\n        self.ffn = nn.Sequential(\n            nn.Linear(hidden_dim, hidden_dim * 2),\n            nn.ReLU(),\n            nn.Dropout(0.1),\n            nn.Linear(hidden_dim * 2, hidden_dim)\n        )\n        \n    def forward(self, left_feat, right_feat):\n        # 拼接左右特征 [B, 2, D]\n        combined = torch.stack([left_feat, right_feat], dim=1)\n        \n        # 自注意力交互\n        attended, _ = self.attention(combined, combined, combined)\n        attended = self.norm(attended + combined)\n        \n        # FFN\n        output = self.norm(attended + self.ffn(attended))\n        \n        # 分离左右特征\n        left_out = output[:, 0, :]\n        right_out = output[:, 1, :]\n        \n        return left_out, right_out\n\n\nclass BiomassModel(nn.Module):\n    \"\"\"\n    Kiến trúc mô hình (Hai luồng, Ba đầu ra)\n    PHẢI GIỐNG HỆT file training.\n    \"\"\"\n    def __init__(self, model_name, pretrained, n_targets=3):\n        super(BiomassModel, self).__init__()\n        \n        self.backbone = timm.create_model(\n            model_name,\n            pretrained=pretrained, # Sẽ là False khi inference\n            num_classes=0,\n            global_pool='avg'\n        )\n        \n        self.n_features = self.backbone.num_features\n        self.n_combined_features = self.n_features * 2\n        \n         # ---Dry_Green_g ---\n        self.head_green = nn.Sequential(\n            nn.Linear(self.n_combined_features, self.n_combined_features // 2),\n            nn.ReLU(),\n            nn.Dropout(0.3),\n            nn.Linear(self.n_combined_features // 2, 1)\n        )\n        \n        # Dry_Dead_g\n        self.head_dead = nn.Sequential(\n            nn.Linear(self.n_combined_features, self.n_combined_features // 2),\n            nn.ReLU(),\n            nn.Dropout(0.3),\n            nn.Linear(self.n_combined_features // 2, 1) \n        )\n\n        # Dry_Clover_g\n        self.head_clover = nn.Sequential(\n            nn.Linear(self.n_combined_features, self.n_combined_features // 2),\n            nn.ReLU(),\n            nn.Dropout(0.3),\n            nn.Linear(self.n_combined_features // 2, 1) \n        )\n\n        \n        \n        # --- GDM_g ---\n        self.head_gdm = nn.Sequential(\n            nn.Linear(self.n_combined_features, self.n_combined_features // 2),\n            nn.ReLU(),\n            nn.Dropout(0.3),\n            nn.Linear(self.n_combined_features // 2, 1)\n        )\n        # --- Dry_Total_g 头 ---\n        self.head_total = nn.Sequential(\n            nn.Linear(self.n_combined_features, self.n_combined_features // 2),\n            nn.ReLU(),\n            nn.Dropout(0.3),\n            nn.Linear(self.n_combined_features // 2, 1) \n        )\n        self.feature_interaction = FeatureInteractionModule(self.n_features)\n\n    def forward(self, img_left, img_right):\n        features_left = self.backbone(img_left)\n        features_right = self.backbone(img_right)\n\n        # 2. 特征交互\n        features_left, features_right = self.feature_interaction(\n            features_left, features_right\n        )\n        \n        combined = torch.cat([features_left, features_right], dim=1)\n        \n        # 4. 输出\n        out_green = self.head_green(combined)\n        out_dead = self.head_dead(combined)\n        out_clover = self.head_clover(combined)\n        out_gdm = self.head_gdm(combined)\n        out_total = self.head_total(combined)\n\n        out_clover = out_clover * 0.8\n        if out_dead > 20:\n            out_dead *= 1.1\n        elif out_dead < 10:\n            out_dead *= 0.9\n            \n        out_gdm = 0.5*out_gdm+ 0.5*(out_green+out_clover)\n        \n        pred_total1 = out_green + out_clover + out_dead\n        pred_total2 = out_gdm + out_dead\n        out_total = 0.5*out_total + 0.5*pred_total1 + 0.0*pred_total2\n        \n        return out_green, out_dead, out_clover, out_gdm, out_total\n\n\n@torch.no_grad()\ndef predict_one_view_accelerate(models_list, test_loader, accelerator):\n    device = accelerator.device\n\n    # 只在主进程收集最终拼接结果\n    idx_buf = []\n    total_buf, gdm_buf, green_buf, clover_buf, dead_buf = [], [], [], [], []\n\n    pbar = tqdm(test_loader, desc=\"  Predicting View\", leave=False, disable=not accelerator.is_main_process)\n\n    for batch in pbar:\n        idx, img_left, img_right = batch\n        idx = idx.to(device, non_blocking=True)\n        img_left = img_left.to(device, non_blocking=True)\n        img_right = img_right.to(device, non_blocking=True)\n\n        # ensemble\n        pred_total_list, pred_gdm_list, pred_green_list, pred_clover_list, pred_dead_list = [], [], [], [], []\n        with accelerator.autocast():\n            for model in models_list:\n                pred_green, pred_dead, pred_clover, pred_gdm, pred_total = model(img_left, img_right)\n                pred_total_list.append(pred_total)\n                pred_gdm_list.append(pred_gdm)\n                pred_green_list.append(pred_green)\n                pred_clover_list.append(pred_clover)\n                pred_dead_list.append(pred_dead)\n\n            avg_pred_total = torch.mean(torch.stack(pred_total_list), dim=0)\n            avg_pred_gdm = torch.mean(torch.stack(pred_gdm_list), dim=0)\n            avg_pred_green = torch.mean(torch.stack(pred_green_list), dim=0)\n            avg_pred_clover = torch.mean(torch.stack(pred_clover_list), dim=0)\n            avg_pred_dead = torch.mean(torch.stack(pred_dead_list), dim=0)\n\n        # ✅ 关键修改：跨进程 gather，并自动去掉末尾重复数据（用于 metrics/推理汇总）\n        g_idx = accelerator.gather_for_metrics(idx)\n        g_total = accelerator.gather_for_metrics(avg_pred_total)\n        g_gdm = accelerator.gather_for_metrics(avg_pred_gdm)\n        g_green = accelerator.gather_for_metrics(avg_pred_green)\n        g_clover = accelerator.gather_for_metrics(avg_pred_clover)\n        g_dead = accelerator.gather_for_metrics(avg_pred_dead)\n\n        if accelerator.is_main_process:\n            idx_buf.append(g_idx.detach().cpu())\n            total_buf.append(g_total.detach().cpu())\n            gdm_buf.append(g_gdm.detach().cpu())\n            green_buf.append(g_green.detach().cpu())\n            clover_buf.append(g_clover.detach().cpu())\n            dead_buf.append(g_dead.detach().cpu())\n\n    if not accelerator.is_main_process:\n        return None\n\n    idx_all = torch.cat(idx_buf, dim=0).numpy()\n    order = np.argsort(idx_all)\n\n    total_all = torch.cat(total_buf, dim=0).numpy().reshape(-1)[order]\n    gdm_all = torch.cat(gdm_buf, dim=0).numpy().reshape(-1)[order]\n    green_all = torch.cat(green_buf, dim=0).numpy().reshape(-1)[order]\n    clover_all = torch.cat(clover_buf, dim=0).numpy().reshape(-1)[order]\n    dead_all = torch.cat(dead_buf, dim=0).numpy().reshape(-1)[order]\n\n    return {\n        'total': total_all,\n        'gdm': gdm_all,\n        'green': green_all,\n        'clover': clover_all,\n        'dead': dead_all\n    }\n\n\ndef run_inference_with_tta():\n    \"\"\"\n    \"\"\"\n    # ✅ 关键修改：引入 accelerate\n    accelerator = Accelerator()\n    device = accelerator.device\n\n    accelerator.print(f\"\\n{'='*50}\")\n    accelerator.print(\"🚀 TTA (Accelerate multi-GPU) 🚀\")\n    accelerator.print(f\"{'='*50}\")\n\n    accelerator.print(f\"test file {CFG.TEST_CSV}...\")\n\n    \n    try:\n        test_df_long = pd.read_csv(CFG.TEST_CSV)\n        test_df_unique = test_df_long.drop_duplicates(subset=['image_path']).reset_index(drop=True)\n        print(f\"image  {len(test_df_unique)} .\")\n    except FileNotFoundError:\n        print(f\"没有发现 {CFG.TEST_CSV}\")\n        return None, None, None\n\n    accelerator.print(\"\\n加载模型...\")\n    models_list = []\n\n\n    model = BiomassModel(CFG.MODEL_NAME, pretrained=False)\n    model.load_state_dict(torch.load(CFG.MODEL_DIR, map_location='cpu'))\n\n    model.eval()\n    model.to(device)\n    models_list.append(model)\n\n\n    accelerator.print(f\"✓ 一共 {len(models_list)} 个模型.\")\n\n    tta_transforms = get_tta_transforms()\n    accelerator.print(f\"\\使用tta {len(tta_transforms)} TTA 个数...\")\n    \n    all_tta_view_preds = [] # List để lưu kết quả của mỗi view TTA\n\n    for i, tta_transform in enumerate(tta_transforms):\n        accelerator.print(f\"---  TTA View {i+1}/{len(tta_transforms)} ---\")\n        \n        # Tạo Dataset/Loader MỚI cho view TTA này\n        test_dataset = TestBiomassDataset(\n            df=test_df_unique,\n            transform_pipeline=tta_transform, # Truyền pipeline TTA\n            image_dir=CFG.TEST_IMAGE_DIR\n        )\n        test_loader = DataLoader(\n            test_dataset,\n            batch_size=CFG.BATCH_SIZE,\n            shuffle=False,\n            num_workers=CFG.NUM_WORKERS,\n            pin_memory=True\n        )\n        test_loader = accelerator.prepare(test_loader)\n        # Chạy ensemble 5-fold cho view này\n        view_preds_np = predict_one_view_accelerate(models_list, test_loader, accelerator)\n        if accelerator.is_main_process:\n            all_tta_view_preds.append(view_preds_np)\n            accelerator.print(f\"✓完成 TTA View {i+1}\")\n    if not accelerator.is_main_process:\n        return None, None, None\n    # --- 4. Ensemble (Lấy trung bình) kết quả TTA ---\n    accelerator.print(\"\\n处理 TTA views...\")\n    final_ensembled_preds = {\n        'total': np.mean([d['total'] for d in all_tta_view_preds], axis=0),\n        'gdm':   np.mean([d['gdm'] for d in all_tta_view_preds], axis=0),\n        'green': np.mean([d['green'] for d in all_tta_view_preds], axis=0),\n        'clover': np.mean([d['clover'] for d in all_tta_view_preds], axis=0),\n        'dead': np.mean([d['dead'] for d in all_tta_view_preds], axis=0)\n        \n    }\n    \n    accelerator.print(\"✓ 完成推理.\")\n    \n    del models_list, test_loader, test_dataset\n    gc.collect()\n    torch.cuda.empty_cache()\n    \n    return final_ensembled_preds, test_df_long, test_df_unique\n\n\ndef post_process_biomass(df_preds):\n    \"\"\"\n    Enforces physical mass balance constraints on biomass predictions.\n    \n    Constraints enforced:\n    1. Dry_Green_g + Dry_Clover_g = GDM_g\n    2. GDM_g + Dry_Dead_g = Dry_Total_g\n    \n    Method:\n    Uses Orthogonal Projection. It finds the set of values that satisfy\n    the constraints while minimizing the Euclidean distance to the \n    original model predictions.\n    \n    Args:\n        df_preds (pd.DataFrame): DataFrame containing the 5 prediction columns.\n        \n    Returns:\n        pd.DataFrame: A new DataFrame with consistent, non-negative values.\n    \"\"\"\n    # 1. Define the specific order required for the math\n    # We treat the vector x as: [Green, Clover, Dead, GDM, Total]\n    ordered_cols = [\"Dry_Green_g\", \"Dry_Dead_g\", \"Dry_Clover_g\", \"GDM_g\", \"Dry_Total_g\"]\n    \n    # Check if columns exist\n    if not all(col in df_preds.columns for col in ordered_cols):\n        missing = [c for c in ordered_cols if c not in df_preds.columns]\n        raise ValueError(f\"Input DataFrame is missing columns: {missing}\")\n\n    # 2. Extract values in the specific order -> Shape (N_samples, 5)\n    Y = df_preds[ordered_cols].values.T  # Transpose to (5, N) for matrix math\n\n    # 3. Define the Constraint Matrix C\n    # We want Cx = 0\n    # Eq 1: 1*Green + 0*Dead + 1*Clover - 1*GDM + 0*Total = 0\n    # Eq 2: 0*Green + 1*Dead + 0*Clover + 1*GDM - 1*Total = 0\n    C = np.array([\n        [1, 0, 1, -1,  0],\n        [0, 1, 0,  1, -1]\n    ])\n\n    # 4. Calculate Projection Matrix P\n    # P = I - C^T * (C * C^T)^-1 * C\n    # This projects any vector onto the null space of C (the valid subspace)\n    C_T = C.T\n    inv_CCt = np.linalg.inv(C @ C_T)\n    P = np.eye(5) - C_T @ inv_CCt @ C\n\n    # 5. Apply Projection\n    # Y_new = P * Y\n    Y_reconciled = P @ Y\n\n    # 6. Transpose back to (N, 5)\n    Y_reconciled = Y_reconciled.T\n\n    # 7. Post-correction for negatives\n    # Projection can mathematically create negative values (e.g. if Total was predicted 0)\n    # We clip to 0. Note: This might slightly break the sum equality again, \n    # but exact equality with negatives is physically impossible anyway.\n    Y_reconciled = Y_reconciled.clip(min=0)\n\n    # 8. Create Output DataFrame\n    df_out = df_preds.copy()\n    df_out[ordered_cols] = Y_reconciled\n\n    return df_out\n\n\n# ===============================================================\n# 6. ✍️ HÀM TẠO FILE SUBMISSION\n# ===============================================================\ndef create_submission(preds_np, test_df_long, test_df_unique):\n    \"\"\"\n    \"\"\"\n    if preds_np is None:\n        print(\"Bỏ qua tạo submission do lỗi ở trên.\")\n        return\n\n    print(\"\\nĐang hậu xử lý và tạo file submission...\")\n\n    # 1. Lấy 3 dự đoán đã ensemble\n    pred_total_final = preds_np['total']\n    pred_gdm_final = preds_np['gdm']\n    pred_green_final = preds_np['green']\n\n    pred_clover_final = preds_np['clover']\n    pred_dead_final = preds_np['dead']\n\n    # 1. 确保非负\n    pred_green_final = np.clip(pred_green_final, a_min=0, a_max=None)\n    pred_clover_final = np.clip(pred_clover_final, a_min=0, a_max=None)\n    pred_dead_final = np.clip(pred_dead_final, a_min=0, a_max=None)\n    \n\n\n    preds_wide_df = pd.DataFrame({\n        'image_path': test_df_unique['image_path'],\n        'Dry_Green_g': pred_green_final,\n        'Dry_Dead_g': pred_dead_final,\n        'Dry_Clover_g': pred_clover_final,\n        'GDM_g': pred_gdm_final,\n        'Dry_Total_g': pred_total_final\n    })\n\n    preds_wide_df = post_process_biomass(preds_wide_df)\n\n    preds_long_df = preds_wide_df.melt(\n        id_vars=['image_path'],\n        value_vars=CFG.ALL_TARGET_COLS, # \n        var_name='target_name',        # \n        value_name='target'            # \n    )\n\n    submission_df = pd.merge(\n        test_df_long[['sample_id', 'image_path', 'target_name']],\n        preds_long_df,\n        on=['image_path', 'target_name'],\n        how='left'\n    )\n\n    submission_df = submission_df[['sample_id', 'target']]\n    \n    # Lưu file\n    submission_df.to_csv(CFG.SUBMISSION_FILE, index=False)\n\n    print(f\"\\n🎉 HOÀN TẤT! Đã lưu file submission tại: {CFG.SUBMISSION_FILE}\")\n    print(submission_df.head())\n    print(submission_df.tail())\n    \nif __name__ == \"__main__\":\n    all_preds_np, df_long, df_unique = run_inference_with_tta()\n    # ✅ 关键修改：只在主进程写文件（避免多进程冲突）\n    accelerator = Accelerator()\n    if accelerator.is_main_process:\n        create_submission(all_preds_np, df_long, df_unique)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-28T06:35:13.296279Z","iopub.execute_input":"2026-01-28T06:35:13.296556Z","iopub.status.idle":"2026-01-28T06:35:13.313147Z","shell.execute_reply.started":"2026-01-28T06:35:13.296532Z","shell.execute_reply":"2026-01-28T06:35:13.312186Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!accelerate launch --num_processes 2  infer_v3.py \\\n    --output_file submission_online1.csv \\\n    --model_name vit_large_patch16_dinov3.lvd1689m \\\n    --model_dir /kaggle/working/output/last_swa.pth \\\n    --image_size 448\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-28T06:35:13.313966Z","iopub.execute_input":"2026-01-28T06:35:13.314488Z","iopub.status.idle":"2026-01-28T06:36:33.609357Z","shell.execute_reply.started":"2026-01-28T06:35:13.314470Z","shell.execute_reply":"2026-01-28T06:36:33.608407Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!accelerate launch --num_processes 2  infer_v3.py \\\n    --output_file submission_online2.csv \\\n    --model_name vit_large_patch16_dinov3.lvd1689m \\\n    --model_dir /kaggle/working/output2/last_swa.pth \\\n    --image_size 448\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-28T06:36:33.610718Z","iopub.execute_input":"2026-01-28T06:36:33.611000Z","iopub.status.idle":"2026-01-28T06:37:49.378516Z","shell.execute_reply.started":"2026-01-28T06:36:33.610980Z","shell.execute_reply":"2026-01-28T06:37:49.377696Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!tail -n 10 all_data.csv","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-28T06:37:49.379619Z","iopub.execute_input":"2026-01-28T06:37:49.379950Z","iopub.status.idle":"2026-01-28T06:37:49.848853Z","shell.execute_reply.started":"2026-01-28T06:37:49.379918Z","shell.execute_reply":"2026-01-28T06:37:49.848086Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pandas as pd\nimport copy\nsub1 = pd.read_csv('submission_online1.csv')\nsub2 = pd.read_csv('submission_online2.csv')\n\n\nsub = copy.deepcopy(sub1)\n\nsub['target'] = (sub1['target'] + sub2['target'] ) / 2\nsub.to_csv('submission2.csv', index=False)\n\n\nimport pandas  as pd\n\nfrom sklearn.model_selection import StratifiedKFold\nimport numpy as np\n\ndata_df = pd.read_csv('/kaggle/input/csiro-biomass/train.csv')\nprint(data_df.shape)\n\n\ndf_wide = data_df.pivot(\n    index=['image_path',],\n    columns='target_name',\n    values='target'\n).reset_index()\ndf_wide = df_wide.reset_index()\ndf_wide.columns.name = None \n\nprint(df_wide.shape)\nprint(df_wide.columns.tolist())\n\npseudo_df = pd.read_csv('/kaggle/working/submission2.csv')\npseudo_df['image_path'] = pseudo_df['sample_id'].apply(lambda x: \"test/\" + x.split('__')[0] + \".jpg\")\npseudo_df['target_name'] = pseudo_df['sample_id'].apply(lambda x: x.split('__')[1])\n\ndf_wide2 = pseudo_df.pivot(\n    index=['image_path'],\n    columns='target_name',\n    values='target'\n).reset_index()\ndf_wide2 = df_wide2.reset_index()\ndf_wide2.columns.name = None \n\nprint(df_wide2.shape)\nprint(df_wide2.columns.tolist())\n\nimport os\nif not os.getenv('KAGGLE_IS_COMPETITION_RERUN'):\n    print('dev')\n    df_wide.head(10).to_csv('train.csv', index=False)\n    all_df = pd.concat([df_wide, df_wide2], axis=0).reset_index(drop=True)\n    all_df.head(10).to_csv('all_data.csv', index=False)\n    \n    print(df_wide.shape, df_wide2.shape, all_df.shape)\nelse:\n    df_wide.to_csv('train.csv', index=False)\n    all_df = pd.concat([df_wide, df_wide2], axis=0).reset_index(drop=True)\n    all_df.to_csv('all_data.csv', index=False)\n    \n    print(df_wide.shape, df_wide2.shape, all_df.shape)\nall_df.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-28T06:37:49.849855Z","iopub.execute_input":"2026-01-28T06:37:49.850165Z","iopub.status.idle":"2026-01-28T06:37:49.897988Z","shell.execute_reply.started":"2026-01-28T06:37:49.850115Z","shell.execute_reply":"2026-01-28T06:37:49.897345Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df_wide2.tail()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-28T06:37:49.898683Z","iopub.execute_input":"2026-01-28T06:37:49.898927Z","iopub.status.idle":"2026-01-28T06:37:49.906867Z","shell.execute_reply.started":"2026-01-28T06:37:49.898906Z","shell.execute_reply":"2026-01-28T06:37:49.906056Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"%%writefile train_base1.yaml\n\ndata_path: /kaggle/working/train.csv\nall_data_path: /kaggle/working/all_data.csv\nimage_dir: /kaggle/input/csiro-biomass\nfold_to_run: 0\nbatch_size: 4\nimg_size: 512\nnum_workers: 4\n# model_name: convnext_tiny\n# model_name: eva02_base_patch14_448.mim_in22k_ft_in1k\nmodel_name: vit_base_patch16_dinov3.lvd1689m\n# model_name: vit_huge_plus_patch16_dinov3.lvd1689m\npretrained: True\nlearning_rate: 1.e-4\nfinetune_lr: 1.e-5\nwarmup_ratio: 0.1\nwarmup_steps: 30\nnum_train_epochs: 40\nfreeze_epochs: 15\ngradient_accumulation_steps: 1\nlogging_steps: 10\neval_steps: 200\nsave_steps: 50\noutput_dir: output_base1/ckpt\nseed: 42\ntrain_mode: mix\nmixed_precision: fp16\nckpt_file: /kaggle/input/csiro-model-ckpt/1208_vit_large_fold3_all_v1_lb67/1208_vit_large_fold3_all_v1_lb67/model_fold-1_epoch30_0.9119.pth","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-28T06:37:49.907893Z","iopub.execute_input":"2026-01-28T06:37:49.908102Z","iopub.status.idle":"2026-01-28T06:37:49.919864Z","shell.execute_reply.started":"2026-01-28T06:37:49.908086Z","shell.execute_reply":"2026-01-28T06:37:49.918996Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"%%writefile train_base2.yaml\n\ndata_path: /kaggle/working/train.csv\nall_data_path: /kaggle/working/all_data.csv\nimage_dir: /kaggle/input/csiro-biomass\nfold_to_run: 0\nbatch_size: 4\nimg_size: 512\nnum_workers: 4\n# model_name: convnext_tiny\n# model_name: eva02_base_patch14_448.mim_in22k_ft_in1k\nmodel_name: vit_base_patch16_dinov3.lvd1689m\npretrained: True\nlearning_rate: 1.e-4\nfinetune_lr: 1.e-5\nwarmup_ratio: 0.1\nwarmup_steps: 30\nnum_train_epochs: 40\nfreeze_epochs: 15\ngradient_accumulation_steps: 1\nlogging_steps: 10\neval_steps: 200\nsave_steps: 50\noutput_dir: output_base2/ckpt\nseed: 42\ntrain_mode: all\nmixed_precision: fp16\nckpt_file: /kaggle/input/csiro-model-ckpt/1208_vit_large_fold3_all_v1_lb67/1208_vit_large_fold3_all_v1_lb67/model_fold-1_epoch30_0.9119.pth","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-28T06:37:49.920721Z","iopub.execute_input":"2026-01-28T06:37:49.921026Z","iopub.status.idle":"2026-01-28T06:37:49.933558Z","shell.execute_reply.started":"2026-01-28T06:37:49.921011Z","shell.execute_reply":"2026-01-28T06:37:49.932915Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"%%writefile train_base1.sh\n\nset -ex\n\nMASTER_PORT=$(shuf -n 1 -i 10000-65535)\necho ${MASTER_PORT}\n\n    \nCUDA_VISIBLE_DEVICES=0 accelerate launch --num_processes 1  train_base.py \\\n    --config ./train_base1.yaml \\","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-28T06:37:49.937331Z","iopub.execute_input":"2026-01-28T06:37:49.937598Z","iopub.status.idle":"2026-01-28T06:37:49.944345Z","shell.execute_reply.started":"2026-01-28T06:37:49.937572Z","shell.execute_reply":"2026-01-28T06:37:49.943626Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"%%writefile train_base2.sh\n\nset -ex\n\nMASTER_PORT=$(shuf -n 1 -i 10000-65535)\necho ${MASTER_PORT}\n\n    \nCUDA_VISIBLE_DEVICES=1 accelerate launch --num_processes 1  train_base.py \\\n    --config ./train_base2.yaml \\","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-28T06:37:49.945078Z","iopub.execute_input":"2026-01-28T06:37:49.945278Z","iopub.status.idle":"2026-01-28T06:37:49.955841Z","shell.execute_reply.started":"2026-01-28T06:37:49.945260Z","shell.execute_reply":"2026-01-28T06:37:49.955328Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"%%writefile train2.sh\n\nsh train_base1.sh &\nsh train_base2.sh &\nwait \necho \"所有训练任务已完成\"","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-28T06:37:49.956723Z","iopub.execute_input":"2026-01-28T06:37:49.957077Z","iopub.status.idle":"2026-01-28T06:37:49.969821Z","shell.execute_reply.started":"2026-01-28T06:37:49.957057Z","shell.execute_reply":"2026-01-28T06:37:49.969160Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!sh train2.sh","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-28T06:37:49.970467Z","iopub.execute_input":"2026-01-28T06:37:49.970635Z","iopub.status.idle":"2026-01-28T06:42:05.832907Z","shell.execute_reply.started":"2026-01-28T06:37:49.970622Z","shell.execute_reply":"2026-01-28T06:42:05.832160Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"get_swa('/kaggle/working/output_base1/ckpt', '/kaggle/working/output_base1')\n!rm -rf /kaggle/working/output_base1/ckpt","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-28T06:42:05.834703Z","iopub.execute_input":"2026-01-28T06:42:05.835357Z","iopub.status.idle":"2026-01-28T06:42:12.546109Z","shell.execute_reply.started":"2026-01-28T06:42:05.835334Z","shell.execute_reply":"2026-01-28T06:42:12.545265Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"get_swa('/kaggle/working/output_base2/ckpt', '/kaggle/working/output_base2')\n!rm -rf /kaggle/working/output_base2/ckpt","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-28T06:42:12.547254Z","iopub.execute_input":"2026-01-28T06:42:12.547530Z","iopub.status.idle":"2026-01-28T06:42:19.054993Z","shell.execute_reply.started":"2026-01-28T06:42:12.547506Z","shell.execute_reply":"2026-01-28T06:42:19.053933Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## final infer","metadata":{}},{"cell_type":"code","source":"!accelerate launch --num_processes 2  infer_v3.py \\\n    --output_file submission_base_online1.csv \\\n    --model_name vit_base_patch16_dinov3.lvd1689m \\\n    --model_dir /kaggle/working/output_base1/last_swa.pth \\\n    --image_size 512\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-28T06:42:19.056193Z","iopub.execute_input":"2026-01-28T06:42:19.056516Z","iopub.status.idle":"2026-01-28T06:43:27.144195Z","shell.execute_reply.started":"2026-01-28T06:42:19.056481Z","shell.execute_reply":"2026-01-28T06:43:27.143394Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!accelerate launch --num_processes 2  infer_v3.py \\\n    --output_file submission_base_online2.csv \\\n    --model_name vit_base_patch16_dinov3.lvd1689m \\\n    --model_dir /kaggle/working/output_base2/last_swa.pth \\\n    --image_size 512\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-28T06:43:27.145297Z","iopub.execute_input":"2026-01-28T06:43:27.145568Z","iopub.status.idle":"2026-01-28T06:44:34.487963Z","shell.execute_reply.started":"2026-01-28T06:43:27.145547Z","shell.execute_reply":"2026-01-28T06:44:34.487230Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pandas as pd\nimport copy\nsub1 = pd.read_csv('submission_online1.csv')\nsub2 = pd.read_csv('submission_online2.csv')\nsub3 = pd.read_csv('submission_base_online1.csv')\nsub4 = pd.read_csv('submission_base_online2.csv')\n\n\nsub = copy.deepcopy(sub1)\nsub.head()\n\n# 0.7 0.68\nsub['target'] = (sub1['target'] + sub2['target']) / 2 * 0.4 + (sub3['target'] + + sub4['target']) / 2 * 0.6\nsub.to_csv('submission.csv', index=False)\n\nsub.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-28T06:44:34.489039Z","iopub.execute_input":"2026-01-28T06:44:34.489304Z","iopub.status.idle":"2026-01-28T06:44:34.510561Z","shell.execute_reply.started":"2026-01-28T06:44:34.489282Z","shell.execute_reply":"2026-01-28T06:44:34.510015Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!head submission.csv","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-28T06:44:34.511231Z","iopub.execute_input":"2026-01-28T06:44:34.511468Z","iopub.status.idle":"2026-01-28T06:44:34.976671Z","shell.execute_reply.started":"2026-01-28T06:44:34.511450Z","shell.execute_reply":"2026-01-28T06:44:34.975961Z"}},"outputs":[],"execution_count":null}]}