{"metadata":{"kernelspec":{"name":"python3","display_name":"Python 3","language":"python"},"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":13887419,"sourceType":"datasetVersion","datasetId":8847701},{"sourceId":13945951,"sourceType":"datasetVersion","datasetId":8888519},{"sourceId":13995156,"sourceType":"datasetVersion","datasetId":8919059},{"sourceId":14018229,"sourceType":"datasetVersion","datasetId":8929818},{"sourceId":14232842,"sourceType":"datasetVersion","datasetId":9080129},{"sourceId":14359583,"sourceType":"datasetVersion","datasetId":9169266},{"sourceId":4537,"sourceType":"modelInstanceVersion","modelInstanceId":3329,"modelId":986},{"sourceId":268942,"sourceType":"modelInstanceVersion","modelInstanceId":230141,"modelId":251887},{"sourceId":663314,"sourceType":"modelInstanceVersion","modelInstanceId":471723,"modelId":487624},{"sourceId":686541,"sourceType":"modelInstanceVersion","modelInstanceId":507446,"modelId":522154}],"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"## Config","metadata":{}},{"cell_type":"code","source":"import os","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-05T01:21:44.028126Z","iopub.execute_input":"2026-01-05T01:21:44.028891Z","iopub.status.idle":"2026-01-05T01:21:44.032704Z","shell.execute_reply.started":"2026-01-05T01:21:44.028852Z","shell.execute_reply":"2026-01-05T01:21:44.032008Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 环境设置\n\nTRAIN = False  # submission时只用跑推理，设为 False\n\nDEBUG = os.environ.get('KAGGLE_KERNEL_RUN_TYPE', '') == 'Interactive'  # 交互式环境下会少跑一些epoch，用于快速跑通流程，方便调试\nLOCAL = os.environ.get('KAGGLE_KERNEL_RUN_TYPE', '') == ''  # 用于在本地开发，觉得代码ok后通过 `kaggle k push` 命令提交到 kaggle 平台\n\nif LOCAL:\n    os.environ['HF_ENDPOINT'] = 'https://hf-mirror.com'","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-05T01:21:44.03407Z","iopub.execute_input":"2026-01-05T01:21:44.034287Z","iopub.status.idle":"2026-01-05T01:21:44.049641Z","shell.execute_reply.started":"2026-01-05T01:21:44.034271Z","shell.execute_reply":"2026-01-05T01:21:44.048912Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 训练设置\n\n## CV\nCV_STRATEGY = 'random'  # groupby_Sampling_Date\nNFOLD = 5\nKFOLD_SEED = 42\n\n## Model\nMODEL_NAME = 'vit_large_patch16_dinov3_qkvb'  # convnext_base\n\n## Training Hyper Params\nLR = 1e-3","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-05T01:21:44.050382Z","iopub.execute_input":"2026-01-05T01:21:44.050648Z","iopub.status.idle":"2026-01-05T01:21:44.06495Z","shell.execute_reply.started":"2026-01-05T01:21:44.050625Z","shell.execute_reply":"2026-01-05T01:21:44.064381Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Load Data","metadata":{"execution":{"iopub.execute_input":"2025-11-21T05:07:31.762158Z","iopub.status.busy":"2025-11-21T05:07:31.760963Z","iopub.status.idle":"2025-11-21T05:07:31.766349Z","shell.execute_reply":"2025-11-21T05:07:31.765388Z","shell.execute_reply.started":"2025-11-21T05:07:31.762125Z"}}},{"cell_type":"code","source":"from PIL import Image\nimport pandas as pd\nimport numpy as np\nimport matplotlib.pyplot as plt\n%matplotlib inline\n\nfrom sklearn.model_selection import KFold, GroupKFold, StratifiedGroupKFold\nfrom tqdm.auto import tqdm\ntqdm.pandas()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-05T01:21:44.066504Z","iopub.execute_input":"2026-01-05T01:21:44.066769Z","iopub.status.idle":"2026-01-05T01:21:45.229415Z","shell.execute_reply.started":"2026-01-05T01:21:44.066753Z","shell.execute_reply":"2026-01-05T01:21:45.228857Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"DATA_ROOT = '../input/' if LOCAL else '/kaggle/input/csiro-biomass/'\ntrain_df = pd.read_csv(f'{DATA_ROOT}/train.csv')\ntrain_df.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-05T01:21:45.230134Z","iopub.execute_input":"2026-01-05T01:21:45.230475Z","iopub.status.idle":"2026-01-05T01:21:45.283936Z","shell.execute_reply.started":"2026-01-05T01:21:45.230456Z","shell.execute_reply":"2026-01-05T01:21:45.283193Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_df[['sample_id_prefix', 'sample_id_suffix']] = train_df.sample_id.str.split('__', expand=True)\n(train_df.sample_id_suffix == train_df.target_name).all()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-05T01:21:45.284778Z","iopub.execute_input":"2026-01-05T01:21:45.285033Z","iopub.status.idle":"2026-01-05T01:21:45.298467Z","shell.execute_reply.started":"2026-01-05T01:21:45.285007Z","shell.execute_reply":"2026-01-05T01:21:45.297586Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"cols = ['sample_id_prefix', 'image_path', 'Sampling_Date', 'State', 'Species', 'Pre_GSHH_NDVI', 'Height_Ave_cm']\nagg_train_df = train_df.groupby(cols).apply(lambda df: df.set_index('target_name').target)\nagg_train_df.reset_index(inplace=True)\nagg_train_df.columns.name = None\n\nagg_train_df['image'] = agg_train_df.image_path.progress_apply(\n    lambda path: Image.open(DATA_ROOT + path).convert('RGB')\n)\n\nagg_train_df.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-05T01:21:45.299405Z","iopub.execute_input":"2026-01-05T01:21:45.299776Z","iopub.status.idle":"2026-01-05T01:22:09.528454Z","shell.execute_reply.started":"2026-01-05T01:21:45.299751Z","shell.execute_reply":"2026-01-05T01:22:09.527641Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"agg_train_df['image_size'] = agg_train_df.image.apply(lambda x: x.size)\nagg_train_df['image_size'].value_counts()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-05T01:22:09.529621Z","iopub.execute_input":"2026-01-05T01:22:09.52996Z","iopub.status.idle":"2026-01-05T01:22:09.542152Z","shell.execute_reply.started":"2026-01-05T01:22:09.529935Z","shell.execute_reply":"2026-01-05T01:22:09.541051Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"np.isclose(\n    agg_train_df[['Dry_Green_g', 'Dry_Clover_g']].sum(axis=1),\n    agg_train_df['GDM_g'],\n    atol=1e-04\n).mean()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-05T01:22:09.543217Z","iopub.execute_input":"2026-01-05T01:22:09.54356Z","iopub.status.idle":"2026-01-05T01:22:09.560021Z","shell.execute_reply.started":"2026-01-05T01:22:09.543526Z","shell.execute_reply":"2026-01-05T01:22:09.5594Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"np.isclose(\n    agg_train_df[['GDM_g', 'Dry_Dead_g']].sum(axis=1),\n    agg_train_df['Dry_Total_g'],\n    atol=1e-04\n).mean()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-05T01:22:09.563019Z","iopub.execute_input":"2026-01-05T01:22:09.563315Z","iopub.status.idle":"2026-01-05T01:22:09.577158Z","shell.execute_reply.started":"2026-01-05T01:22:09.563298Z","shell.execute_reply":"2026-01-05T01:22:09.576587Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"plt.figure(figsize=(16, 4))\nplt.subplot(1, 3, 1)\nagg_train_df.Dry_Green_g.plot(kind='hist')\n_ = plt.title('Dry_Green_g')\n\nplt.subplot(1, 3, 2)\nagg_train_df.Dry_Clover_g.plot(kind='hist')\n_ = plt.title('Dry_Clover_g')\n\nplt.subplot(1, 3, 3)\nagg_train_df.Dry_Dead_g.plot(kind='hist')\n_ = plt.title('Dry_Dead_g')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-05T01:22:09.577915Z","iopub.execute_input":"2026-01-05T01:22:09.578097Z","iopub.status.idle":"2026-01-05T01:22:10.323373Z","shell.execute_reply.started":"2026-01-05T01:22:09.578074Z","shell.execute_reply":"2026-01-05T01:22:10.322714Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## CV Strategy","metadata":{}},{"cell_type":"code","source":"agg_train_df['Sampling_Date_Month'] = agg_train_df.Sampling_Date.apply(lambda x: x.split('/')[1].strip())\nKFoldClass = StratifiedGroupKFold if CV_STRATEGY == 'groupby_Sampling_Date' else KFold\nkfold = KFoldClass(n_splits=NFOLD, shuffle=True, random_state=KFOLD_SEED)\n\nagg_train_df['fold'] = None\nfor i, (trn_idx, val_idx) in enumerate(kfold.split(agg_train_df.index, y=agg_train_df.State, groups=agg_train_df.Sampling_Date)):\n    agg_train_df.loc[val_idx, 'fold'] = i\n    \n    trn_df = agg_train_df[agg_train_df.fold != i]\n    val_df = agg_train_df[agg_train_df.fold == i]\n    \n    trn_months = sorted(int(x) for x in trn_df.Sampling_Date_Month.unique())\n    val_months = sorted(int(x) for x in val_df.Sampling_Date_Month.unique())\n    print(f'trn({trn_df.shape[0]}) -> val({val_df.shape[0]}): {trn_months} -> {val_months}')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-05T01:22:10.324118Z","iopub.execute_input":"2026-01-05T01:22:10.324351Z","iopub.status.idle":"2026-01-05T01:22:10.344256Z","shell.execute_reply.started":"2026-01-05T01:22:10.324325Z","shell.execute_reply":"2026-01-05T01:22:10.34356Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## DataLoader","metadata":{}},{"cell_type":"code","source":"from torch.utils.data import Dataset, DataLoader\nfrom torchvision import transforms","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-05T01:22:10.345146Z","iopub.execute_input":"2026-01-05T01:22:10.345342Z","iopub.status.idle":"2026-01-05T01:22:19.445676Z","shell.execute_reply.started":"2026-01-05T01:22:10.345327Z","shell.execute_reply":"2026-01-05T01:22:19.445013Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class RegressionDataset(Dataset):\n    def __init__(self, data, transform=None, vertical_split=True):\n        self.data = data\n        self.transform = transform\n        self.vertical_split = vertical_split\n\n    def __len__(self):\n        return self.data.shape[0]\n\n    def __getitem__(self, idx):\n        item = self.data.iloc[idx]\n        image = item.image\n        targets = [item['Dry_Green_g'], item['Dry_Clover_g'], item['Dry_Dead_g']]\n        \n        if self.vertical_split:\n            # 垂直均分成左右两张图片\n            width, height = image.size\n            mid_point = width // 2\n            left_image = image.crop((0, 0, mid_point, height))\n            right_image = image.crop((mid_point, 0, width, height))\n            \n            if self.transform:\n                left_image = self.transform(left_image)\n                right_image = self.transform(right_image)\n            \n            return left_image, right_image, targets\n        \n        else:\n            if self.transform:\n                image = self.transform(image)\n\n            return image, targets\n\n\ndef create_dataloader(data, target_image_size=(256, 256), batch_size=32, shuffle=True, aug=True, tta_transform=None):    \n    if aug:\n        transform = transforms.Compose([\n            transforms.Resize(target_image_size),\n            transforms.RandomHorizontalFlip(p=0.5),\n            transforms.RandomVerticalFlip(p=0.5),\n            transforms.RandomApply([transforms.RandomRotation([90, 90])], p=0.5),\n            transforms.ColorJitter(brightness=0.1, contrast=0.1, saturation=0.1, hue=0.05),\n            transforms.ToTensor(),\n            transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])\n        ])\n    \n    else:\n        if tta_transform:\n            transform = transforms.Compose([\n                transforms.Resize(target_image_size),\n                tta_transform,\n                transforms.ToTensor(),\n                transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])\n            ])\n        else:\n            transform = transforms.Compose([\n                transforms.Resize(target_image_size),\n\n                transforms.ToTensor(),\n                transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])\n            ])\n\n    dataset = RegressionDataset(data, transform=transform)\n    print('dataset size:', len(dataset))\n    dataloader = DataLoader(dataset, batch_size=batch_size, shuffle=shuffle, num_workers=4)\n    return dataloader","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-05T01:22:19.446437Z","iopub.execute_input":"2026-01-05T01:22:19.446915Z","iopub.status.idle":"2026-01-05T01:22:19.45628Z","shell.execute_reply.started":"2026-01-05T01:22:19.446889Z","shell.execute_reply":"2026-01-05T01:22:19.455603Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def get_tta_dataloaders(data, target_image_size, batch_size):\n    res = []\n    for transform in [None, transforms.RandomHorizontalFlip(p=1.0), transforms.RandomVerticalFlip(p=1.0), transforms.RandomRotation([90, 90])]:\n        res.append(\n            create_dataloader(data, target_image_size, batch_size, shuffle=False, aug=False, tta_transform=transform)\n        )\n    return res","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-05T01:22:19.457056Z","iopub.execute_input":"2026-01-05T01:22:19.457396Z","iopub.status.idle":"2026-01-05T01:22:19.483148Z","shell.execute_reply.started":"2026-01-05T01:22:19.457372Z","shell.execute_reply":"2026-01-05T01:22:19.482599Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Model","metadata":{}},{"cell_type":"code","source":"import timm\nimport torch\nimport torch.nn as nn","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-05T01:22:19.483839Z","iopub.execute_input":"2026-01-05T01:22:19.48407Z","iopub.status.idle":"2026-01-05T01:22:23.021328Z","shell.execute_reply.started":"2026-01-05T01:22:19.484049Z","shell.execute_reply":"2026-01-05T01:22:23.020758Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"model = timm.create_model(MODEL_NAME, pretrained=False, num_classes=3)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-05T01:22:23.022092Z","iopub.execute_input":"2026-01-05T01:22:23.022333Z","iopub.status.idle":"2026-01-05T01:22:27.684214Z","shell.execute_reply.started":"2026-01-05T01:22:23.022307Z","shell.execute_reply":"2026-01-05T01:22:27.683313Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"model.pretrained_cfg\nTARGET_IMAGE_SIZE = model.pretrained_cfg['input_size'][1:]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-05T01:22:27.685212Z","iopub.execute_input":"2026-01-05T01:22:27.685505Z","iopub.status.idle":"2026-01-05T01:22:27.689441Z","shell.execute_reply.started":"2026-01-05T01:22:27.685475Z","shell.execute_reply":"2026-01-05T01:22:27.688626Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class MultiTargetRegressor(nn.Module):\n    def __init__(self, model_name, pretrained=True, num_classes=3, dropout=0.0, freeze_backbone=False):\n        super().__init__()\n        self.backbone = timm.create_model(model_name, pretrained=pretrained, num_classes=0, global_pool='avg')\n        self.dropout = nn.Dropout(dropout)\n        \n        def make_head():\n            return nn.Sequential(\n                nn.Linear(self.backbone.num_features * 2, 8),\n                nn.ReLU(inplace=True),\n                nn.Dropout(dropout),\n                nn.Linear(8, 1),\n            )\n        \n        self.head_green = make_head()\n        self.head_clover = make_head()\n        self.head_dead = make_head()\n        \n        self.softplus = nn.Softplus(beta=1.0)\n    \n        if freeze_backbone:\n            for p in self.backbone.parameters():\n                p.requires_grad = False\n    \n    def forward(self, left_img, right_img):\n        left_feat = self.backbone(left_img)\n        right_feat = self.backbone(right_img)\n        combined = torch.cat([left_feat, right_feat], dim=1)\n        \n        green = self.softplus(self.head_green(combined))    # Bx1\n        clover = self.softplus(self.head_clover(combined))  # Bx1\n        dead = self.softplus(self.head_dead(combined))   # Bx1\n    \n        logits = torch.cat([green, clover, dead], dim=1)  # Bx3\n        return logits","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-05T01:22:27.6902Z","iopub.execute_input":"2026-01-05T01:22:27.691156Z","iopub.status.idle":"2026-01-05T01:22:27.709803Z","shell.execute_reply.started":"2026-01-05T01:22:27.691136Z","shell.execute_reply":"2026-01-05T01:22:27.709256Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Train","metadata":{}},{"cell_type":"code","source":"import torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom torch.optim import AdamW\nfrom transformers import get_cosine_schedule_with_warmup","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-05T01:22:27.710508Z","iopub.execute_input":"2026-01-05T01:22:27.710979Z","iopub.status.idle":"2026-01-05T01:22:30.75214Z","shell.execute_reply.started":"2026-01-05T01:22:27.710953Z","shell.execute_reply":"2026-01-05T01:22:30.751561Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ======== Weighted R² ========\ndef weighted_r2_score(y_true: np.ndarray, y_pred: np.ndarray):\n    \"\"\"\n    y_true, y_pred: shape (N, 5): Green/Clover/Dead/GDM/Total\n    \"\"\"\n    weights = np.array([0.1, 0.1, 0.1, 0.2, 0.5])\n    r2_scores = []\n    for i in range(5):\n        y_t = y_true[:, i]\n        y_p = y_pred[:, i]\n        ss_res = np.sum((y_t - y_p) ** 2)\n        ss_tot = np.sum((y_t - np.mean(y_t)) ** 2)\n        r2 = 1 - ss_res / ss_tot if ss_tot > 0 else 0.0\n        r2_scores.append(r2)\n    r2_scores = np.array(r2_scores)\n    weighted_r2 = np.sum(r2_scores * weights) / np.sum(weights)\n    return weighted_r2, r2_scores","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-05T01:22:30.752914Z","iopub.execute_input":"2026-01-05T01:22:30.753383Z","iopub.status.idle":"2026-01-05T01:22:30.758524Z","shell.execute_reply.started":"2026-01-05T01:22:30.753356Z","shell.execute_reply":"2026-01-05T01:22:30.757814Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def calc_metric(outputs, targets):\n    '''\n        outputs/targets: shape (N, 3): Green/Clover/Dead\n    '''\n    y_true = np.column_stack((\n        targets,\n        targets[:, :2].sum(axis=1),\n        targets.sum(axis=1),\n    ))\n    \n    y_pred = np.column_stack((\n        outputs,\n        outputs[:, :2].sum(axis=1),\n        outputs.sum(axis=1),\n    ))\n    \n    weighted_r2, r2_scores = weighted_r2_score(y_true, y_pred)\n    return weighted_r2, r2_scores","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-05T01:22:30.759287Z","iopub.execute_input":"2026-01-05T01:22:30.759502Z","iopub.status.idle":"2026-01-05T01:22:30.782974Z","shell.execute_reply.started":"2026-01-05T01:22:30.759485Z","shell.execute_reply":"2026-01-05T01:22:30.782486Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def train_epoch(model, dataloader, criterion, optimizer, scheduler, device):\n    model.train()\n    total_loss = 0\n\n    for images, targets in dataloader:\n        images = images.to(device)\n        targets = torch.stack(targets).T.float().to(device)\n\n        optimizer.zero_grad()\n        outputs = model(images)\n        loss = criterion(outputs, targets)\n        loss.backward()\n        optimizer.step()\n        scheduler.step()\n\n        total_loss += loss.item()\n\n    return total_loss / len(dataloader)\n\n\ndef validate(model, dataloader, criterion, device):\n    model.eval()\n    total_loss = 0\n    all_outputs = []\n    all_targets = []\n\n    with torch.no_grad():\n        for images, targets in dataloader:\n            images = images.to(device)\n            targets = torch.stack(targets).T.float().to(device)\n\n            outputs = model(images)\n            loss = criterion(outputs, targets)\n            total_loss += loss.item()\n\n            all_outputs.append(outputs.detach().cpu())\n            all_targets.append(targets.detach().cpu())\n\n    outputs = torch.cat(all_outputs).numpy()\n    targets = torch.cat(all_targets).numpy()\n    \n    return total_loss / len(dataloader), outputs, targets\n    \n    # weighted_r2, r2_scores = calc_metric(outputs, targets)\n    # return total_loss / len(dataloader), weighted_r2, r2_scores\n\n\ndef tta_validate(model, dataloaders, criterion, device):\n    if not isinstance(dataloaders, list):\n        dataloaders = [dataloaders]\n    \n    all_loss = []\n    all_outputs = []\n    all_targets = []\n    for dataloader in dataloaders:\n        loss, outputs, targets = validate(model, dataloader, criterion, device)\n        all_loss.append(loss)\n        all_outputs.append(outputs)\n        all_targets.append(targets)\n    \n    avg_loss = np.mean(all_loss)\n    avg_outputs = np.mean(all_outputs, axis=0)\n    avg_targets = np.mean(all_targets, axis=0)\n    \n    weighted_r2, r2_scores = calc_metric(avg_outputs, avg_targets)\n    \n    return avg_loss, weighted_r2, r2_scores","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-05T01:22:30.783668Z","iopub.execute_input":"2026-01-05T01:22:30.783924Z","iopub.status.idle":"2026-01-05T01:22:30.80232Z","shell.execute_reply.started":"2026-01-05T01:22:30.783895Z","shell.execute_reply":"2026-01-05T01:22:30.801721Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def train_fold(data, fold):\n    device = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')\n    \n    # Hyperparameters\n    batch_size = 64\n    lr = LR\n    patience = 10\n    num_epochs = 5 if DEBUG else 100\n    warmup_ratio = 0.05\n\n    # data\n    train_loader = create_dataloader(data[data.fold != fold], TARGET_IMAGE_SIZE, batch_size, shuffle=True, aug=True)\n    # val_loader = create_dataloader(data[data.fold == fold], TARGET_IMAGE_SIZE, batch_size, shuffle=False, aug=False)\n    val_loaders = get_tta_dataloaders(data[data.fold == fold], TARGET_IMAGE_SIZE, batch_size)\n\n    # model, loss, optimizer\n    model = timm.create_model(MODEL_NAME, pretrained=True, num_classes=3)\n    model.to(device)\n\n    criterion = nn.SmoothL1Loss()  # nn.MSELoss()\n    optimizer = AdamW(model.parameters(), lr=lr)\n\n    num_training_steps = num_epochs * len(train_loader)\n    warmup_steps = int(warmup_ratio * num_training_steps)\n    \n    # scheduler = optim.lr_scheduler.ReduceLROnPlateau(optimizer, mode='max', factor=0.5, patience=patience//2)\n    scheduler = get_cosine_schedule_with_warmup(optimizer, num_warmup_steps=warmup_steps, num_training_steps=num_training_steps)\n\n    # Training loop\n    history = []\n    best_score = -float('inf')\n    # best_loss = float('inf')\n    epochs_without_improvement = 0\n    \n    for epoch in range(num_epochs):\n        train_loss = train_epoch(model, train_loader, criterion, optimizer, scheduler, device)\n        # val_loss, weighted_r2, r2_scores = validate(model, val_loader, criterion, device)\n        val_loss, weighted_r2, r2_scores = tta_validate(model, val_loaders, criterion, device)\n\n        if epoch % 10 == 0:\n            print(f\"Epoch [{epoch}/{num_epochs}]: Train Loss: {train_loss:.4f}, Val Loss: {val_loss:.4f}, weighted_r2: {weighted_r2:.4f}, lr: {optimizer.param_groups[0]['lr']}\")\n        \n        history.append({\n            'train_loss': train_loss,\n            'val_loss': val_loss,\n            'lr': optimizer.param_groups[0]['lr'],\n            'weighted_r2': weighted_r2,\n            'r2_scores': r2_scores,\n        })\n        \n        # 早停\n        if weighted_r2 > best_score:\n        # if val_loss < best_loss:\n            best_loss = val_loss\n            best_score = weighted_r2\n            epochs_without_improvement = 0\n            # 保存最佳模型\n            torch.save(model.state_dict(), f'{OUTPUT_DIR}/best_mode_fold{fold}.pth')\n        \n        else:\n            epochs_without_improvement += 1\n        \n        if epochs_without_improvement >= patience:\n            print(f\"早停: epoch={epoch}, {patience} 个 epoch 无改善\")\n            break\n\n    print(f\"\\nTraining completed. Best weighted_r2: {best_score:.4f}\")\n    \n    return history, best_score","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-05T01:22:30.803152Z","iopub.execute_input":"2026-01-05T01:22:30.803433Z","iopub.status.idle":"2026-01-05T01:22:30.8275Z","shell.execute_reply.started":"2026-01-05T01:22:30.803414Z","shell.execute_reply":"2026-01-05T01:22:30.827017Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"OUTPUT_DIR = 'trained_models/'\nif not os.path.exists(OUTPUT_DIR):\n    os.mkdir(OUTPUT_DIR)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-05T01:22:30.828218Z","iopub.execute_input":"2026-01-05T01:22:30.828478Z","iopub.status.idle":"2026-01-05T01:22:30.846799Z","shell.execute_reply.started":"2026-01-05T01:22:30.828455Z","shell.execute_reply":"2026-01-05T01:22:30.846278Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"if TRAIN:\n    all_best_score = []\n\n    for i in range(2 if DEBUG else NFOLD):\n        print(f'### fold={i}')\n        history, best_score = train_fold(agg_train_df, fold=i)\n        all_best_score.append(best_score)\n        history = pd.DataFrame(history)\n\n        history.to_json(\n            f'{OUTPUT_DIR}/history_fold{i}.jsonl',\n            orient='records',\n            lines=True,\n            force_ascii=False,\n        )\n\n        # plot\n        plt.figure(figsize=(16, 4))\n\n        plt.subplot(1, 3, 1)\n        plt.title('LR')\n        plt.plot(history.lr)\n\n        plt.subplot(1, 3, 2)\n        plt.title('Loss')\n        plt.plot(history.train_loss, label='train')\n        plt.plot(history.val_loss, label='val')\n        plt.legend()\n\n        plt.subplot(1, 3, 3)\n        plt.title('weighted_r2')\n        plt.plot(history.weighted_r2)\n        plt.show()\n    \n    print('Avg CV:', np.mean(all_best_score))","metadata":{"scrolled":true,"trusted":true,"execution":{"iopub.status.busy":"2026-01-05T01:22:30.84737Z","iopub.execute_input":"2026-01-05T01:22:30.847654Z","iopub.status.idle":"2026-01-05T01:22:30.863367Z","shell.execute_reply.started":"2026-01-05T01:22:30.847631Z","shell.execute_reply":"2026-01-05T01:22:30.862663Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Inference","metadata":{}},{"cell_type":"code","source":"import os\nfrom pathlib import Path","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-05T01:22:30.864205Z","iopub.execute_input":"2026-01-05T01:22:30.864901Z","iopub.status.idle":"2026-01-05T01:22:30.87966Z","shell.execute_reply.started":"2026-01-05T01:22:30.864877Z","shell.execute_reply":"2026-01-05T01:22:30.879083Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def get_lastest_saved_models(model_root='/kaggle/input/csiro-simple-output/pytorch/default/'):    \n    latest = 1\n    for version in os.listdir(model_root):\n        try:\n            version = int(version)\n        except:\n            continue\n\n        if version > latest:\n            latest = version\n\n    return f'{model_root}/{latest}/trained_models/'","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-05T01:22:30.883403Z","iopub.execute_input":"2026-01-05T01:22:30.883775Z","iopub.status.idle":"2026-01-05T01:22:30.897002Z","shell.execute_reply.started":"2026-01-05T01:22:30.883758Z","shell.execute_reply":"2026-01-05T01:22:30.896448Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"SAVED_MODELS = './trained_models/' if TRAIN else get_lastest_saved_models('/kaggle/input/csiro-local-trained/pytorch/default/')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-05T01:22:30.897802Z","iopub.execute_input":"2026-01-05T01:22:30.898217Z","iopub.status.idle":"2026-01-05T01:22:30.918518Z","shell.execute_reply.started":"2026-01-05T01:22:30.8982Z","shell.execute_reply":"2026-01-05T01:22:30.918041Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def predict(model, dataloader, device):\n    model.to(device)\n    model.eval()\n\n    all_outputs = []\n    with torch.no_grad():\n        for left_images, right_images, targets in dataloader:\n            left_images = left_images.to(device)\n            right_images = right_images.to(device)\n    \n            outputs = model(left_images, right_images)\n            all_outputs.append(outputs.detach().cpu())\n    \n    outputs = torch.cat(all_outputs).numpy()\n    return outputs","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-05T01:22:30.91927Z","iopub.execute_input":"2026-01-05T01:22:30.919549Z","iopub.status.idle":"2026-01-05T01:22:30.936767Z","shell.execute_reply.started":"2026-01-05T01:22:30.919524Z","shell.execute_reply":"2026-01-05T01:22:30.936093Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def tta_predict(model, dataloaders, device):\n    all_outputs = []\n    for dataloader in dataloaders:\n        outputs = predict(model, dataloader, device)\n        all_outputs.append(outputs)\n    avg_outputs = np.mean(all_outputs, axis=0)\n    return avg_outputs","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-05T01:22:30.937513Z","iopub.execute_input":"2026-01-05T01:22:30.937883Z","iopub.status.idle":"2026-01-05T01:22:30.954539Z","shell.execute_reply.started":"2026-01-05T01:22:30.937867Z","shell.execute_reply":"2026-01-05T01:22:30.954044Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def kfold_predict(dataloaders):\n    device = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')\n    \n    all_preds = []\n    for model_file in Path(SAVED_MODELS).glob('*.pth'):\n        # model = timm.create_model(MODEL_NAME, pretrained=False, num_classes=3)\n        fold_idx = int(model_file.name.split('.')[0].split('fold')[1])\n        if fold_idx >= NFOLD: continue\n        print(model_file.name)\n        model = MultiTargetRegressor(MODEL_NAME, pretrained=False, num_classes=3)\n        model.load_state_dict(torch.load(model_file))\n\n        preds = tta_predict(model, dataloaders, device)\n        all_preds.append(preds)\n\n    avg_preds = np.mean(all_preds, axis=0)\n    return avg_preds","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-05T01:22:30.955304Z","iopub.execute_input":"2026-01-05T01:22:30.955531Z","iopub.status.idle":"2026-01-05T01:22:30.971209Z","shell.execute_reply.started":"2026-01-05T01:22:30.955505Z","shell.execute_reply":"2026-01-05T01:22:30.970503Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"test_df = pd.read_csv(DATA_ROOT + 'test.csv')\n\ntest_df['target'] = 0.0\ntest_df[['sample_id_prefix', 'sample_id_suffix']] = test_df.sample_id.str.split('__', expand=True)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-05T01:22:30.971951Z","iopub.execute_input":"2026-01-05T01:22:30.972126Z","iopub.status.idle":"2026-01-05T01:22:31.001993Z","shell.execute_reply.started":"2026-01-05T01:22:30.972103Z","shell.execute_reply":"2026-01-05T01:22:31.001283Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"cols = ['sample_id_prefix', 'image_path']\nagg_test_df = test_df.groupby(cols).apply(lambda df: df.set_index('target_name').target)\nagg_test_df.reset_index(inplace=True)\nagg_test_df.columns.name = None\n\nagg_test_df['image'] = agg_test_df.image_path.progress_apply(\n    lambda path: Image.open(DATA_ROOT + path).convert('RGB')\n)\n\nagg_test_df.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-05T01:22:31.002759Z","iopub.execute_input":"2026-01-05T01:22:31.003043Z","iopub.status.idle":"2026-01-05T01:22:31.195011Z","shell.execute_reply.started":"2026-01-05T01:22:31.00302Z","shell.execute_reply":"2026-01-05T01:22:31.194394Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"test_loader = get_tta_dataloaders(agg_test_df, TARGET_IMAGE_SIZE, 64)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-05T01:22:31.195709Z","iopub.execute_input":"2026-01-05T01:22:31.196023Z","iopub.status.idle":"2026-01-05T01:22:31.201379Z","shell.execute_reply.started":"2026-01-05T01:22:31.196004Z","shell.execute_reply":"2026-01-05T01:22:31.200824Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"preds = kfold_predict(test_loader)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-05T01:22:31.202046Z","iopub.execute_input":"2026-01-05T01:22:31.202283Z","iopub.status.idle":"2026-01-05T01:23:57.642091Z","shell.execute_reply.started":"2026-01-05T01:22:31.202259Z","shell.execute_reply":"2026-01-05T01:23:57.641192Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"agg_test_df[['Dry_Green_g', 'Dry_Clover_g', 'Dry_Dead_g']] = preds\n\ncols_pred = ['Dry_Green_g', 'Dry_Clover_g', 'Dry_Dead_g']\nagg_test_df[cols_pred] = agg_test_df[cols_pred].clip(lower=0)\n\nagg_test_df['GDM_g'] = agg_test_df.Dry_Green_g + agg_test_df.Dry_Clover_g\nagg_test_df['Dry_Total_g'] = agg_test_df.GDM_g + agg_test_df.Dry_Dead_g\n\nagg_test_df = agg_test_df.rename(columns={'sample_id_prefix': 'base_id'})\ncols_to_save = ['base_id', 'Dry_Total_g', 'GDM_g', 'Dry_Green_g', 'Dry_Dead_g', 'Dry_Clover_g']\n\nagg_test_df[cols_to_save].to_csv('vit_wide.csv', index=False)\nprint(\"✅ Saved with non-negative constraints.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-05T01:23:57.65859Z","iopub.execute_input":"2026-01-05T01:23:57.658899Z","iopub.status.idle":"2026-01-05T01:23:57.679131Z","shell.execute_reply.started":"2026-01-05T01:23:57.658872Z","shell.execute_reply":"2026-01-05T01:23:57.678487Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!head vit_wide.csv","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-05T01:23:57.679919Z","iopub.execute_input":"2026-01-05T01:23:57.680185Z","iopub.status.idle":"2026-01-05T01:23:57.9507Z","shell.execute_reply.started":"2026-01-05T01:23:57.680168Z","shell.execute_reply":"2026-01-05T01:23:57.949775Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport sys\nimport glob\nimport gc  # 垃圾回收\nfrom pathlib import Path\n\nimport logging\nimport argparse\nimport json\nimport math\nimport random\nfrom copy import deepcopy\nfrom tqdm.auto import tqdm  # 进度条\n\nimport pandas as pd\nimport numpy as np\n\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nimport torchvision.transforms as transforms  # torchvision 变换\nfrom torch.utils.data import Dataset, DataLoader\n\nimport cv2\nfrom PIL import Image\nimport timm  # 图像模型库\nimport albumentations as A  # 图像增强\nfrom albumentations.pytorch import ToTensorV2  # 转Tensor\n\nfrom transformers import (\n    AutoProcessor,\n    AutoImageProcessor,\n    AutoModel,\n    Siglip2Model,\n    Siglip2ImageProcessor,\n    SiglipModel,\n    SiglipImageProcessor,\n    AutoTokenizer\n)\n\nfrom sklearn.model_selection import KFold, GroupKFold\nfrom sklearn.linear_model import LinearRegression, Ridge, Lasso, BayesianRidge\nfrom sklearn.svm import SVR, LinearSVR\nfrom sklearn.ensemble import (\n    GradientBoostingRegressor,\n    HistGradientBoostingRegressor,\n    ExtraTreesRegressor\n)\nfrom sklearn.decomposition import PCA\nfrom sklearn import preprocessing\nfrom sklearn.dummy import DummyRegressor\n\nfrom xgboost import XGBRegressor\nfrom lightgbm import LGBMRegressor\nfrom catboost import CatBoostRegressor\n\nfrom dataclasses import dataclass, field\nfrom typing import List, Tuple, Dict, Optional\n\nimport matplotlib.pyplot as plt\n\n# ====================================================================================\n# [Global Setup] Shared Configuration & Utils\n# ====================================================================================\ndef seed_everything(seed=43):\n    random.seed(seed)\n    os.environ['PYTHONHASHSEED'] = str(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.deterministic = True\n    torch.backends.cudnn.benchmark = False\n    print(f\"🔒 Seed set to {seed}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-05T01:23:57.952073Z","iopub.execute_input":"2026-01-05T01:23:57.952399Z","iopub.status.idle":"2026-01-05T01:24:28.717395Z","shell.execute_reply.started":"2026-01-05T01:23:57.952365Z","shell.execute_reply":"2026-01-05T01:24:28.716612Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =============================================================================\n# CSIRO Image2Biomass - v4 CrossPVT T2T Mamba Inference (Self-contained)\n# =============================================================================\nLOGGER = logging.getLogger(\"csiro_infer_v4\")\nif not LOGGER.handlers:\n    LOGGER.addHandler(logging.StreamHandler())\nLOGGER.setLevel(logging.INFO)\n\n@dataclass\nclass TrainCFG:\n    dropout: float = 0.1\n    hidden_ratio: float = 0.35\n\n    # DINO backbone 相关\n    dino_candidates: Tuple[str, ...] = (\n        \"vit_base_patch14_dinov2\",\n        \"vit_base_patch14_reg4_dinov2\",\n        \"vit_small_patch14_dinov2\",\n    )\n    small_grid: Tuple[int, int] = (4, 4)\n    big_grid: Tuple[int, int] = (2, 2)\n    t2t_depth: int = 2\n    cross_layers: int = 2\n    cross_heads: int = 6\n\n    # Pyramid + Mamba\n    pyramid_dims: Tuple[int, int, int] = (384, 512, 640)\n    mobilevit_heads: int = 4\n    mobilevit_depth: int = 2\n    sra_heads: int = 8\n    sra_ratio: int = 2\n    mamba_depth: int = 3\n    mamba_kernel: int = 5\n    aux_head: bool = True\n    aux_loss_weight: float = 0.4\n\n    # 目标列（用于辅助头 / 5 目标打包）\n    ALL_TARGET_COLS: Tuple[str, ...] = (\n        \"Dry_Green_g\",\n        \"Dry_Dead_g\",\n        \"Dry_Clover_g\",\n        \"GDM_g\",\n        \"Dry_Total_g\",\n    )\n\n\nCFG = TrainCFG()\n\n\ndef update_cfg_from_checkpoint(cfg_dict: dict):\n    \"\"\"\n    用 checkpoint 中保存的 cfg 覆盖当前 CFG 的同名字段，\n    确保推理结构与训练完全一致。\n    \"\"\"\n    global CFG\n    if not cfg_dict:\n        return\n    for k, v in cfg_dict.items():\n        if hasattr(CFG, k):\n            setattr(CFG, k, v)\n\n\n# =============================================================================\n# Model blocks（与 train.py 保持一致的简化实现）\n# =============================================================================\nclass FeedForward(nn.Module):\n    def __init__(self, dim, mlp_ratio=4.0, dropout=0.0):\n        super().__init__()\n        hid = int(dim * mlp_ratio)\n        self.net = nn.Sequential(\n            nn.Linear(dim, hid),\n            nn.GELU(),\n            nn.Dropout(dropout),\n            nn.Linear(hid, dim),\n            nn.Dropout(dropout),\n        )\n\n    def forward(self, x):\n        return self.net(x)\n\n\nclass AttentionBlock(nn.Module):\n    def __init__(self, dim, heads=8, dropout=0.0, mlp_ratio=4.0):\n        super().__init__()\n        self.norm1 = nn.LayerNorm(dim)\n        self.attn = nn.MultiheadAttention(dim, heads, dropout=dropout, batch_first=True)\n        self.norm2 = nn.LayerNorm(dim)\n        self.ff = FeedForward(dim, mlp_ratio=mlp_ratio, dropout=dropout)\n\n    def forward(self, x):\n        h = self.norm1(x)\n        attn_out, _ = self.attn(h, h, h, need_weights=False)\n        x = x + attn_out\n        x = x + self.ff(self.norm2(x))\n        return x\n\n\nclass MobileViTBlock(nn.Module):\n    \"\"\"\n    轻量 MobileViT：局部 CNN + 小型 Transformer（token 化和 fold back）\n    \"\"\"\n\n    def __init__(self, dim, heads=4, depth=2, patch=(2, 2), dropout=0.0):\n        super().__init__()\n        self.local = nn.Sequential(\n            nn.Conv2d(dim, dim, 3, padding=1, groups=dim),\n            nn.Conv2d(dim, dim, 1),\n            nn.GELU(),\n        )\n        self.patch = patch\n        self.transformer = nn.ModuleList(\n            [AttentionBlock(dim, heads=heads, dropout=dropout, mlp_ratio=2.0) for _ in range(depth)]\n        )\n        self.fuse = nn.Conv2d(dim * 2, dim, kernel_size=1)\n\n    def forward(self, x: torch.Tensor):\n        local_feat = self.local(x)\n        B, C, H, W = local_feat.shape\n        ph, pw = self.patch\n        new_h = math.ceil(H / ph) * ph\n        new_w = math.ceil(W / pw) * pw\n        if new_h != H or new_w != W:\n            local_feat = F.interpolate(local_feat, size=(new_h, new_w), mode=\"bilinear\", align_corners=False)\n            H, W = new_h, new_w\n\n        tokens = local_feat.unfold(2, ph, ph).unfold(3, pw, pw)  # B,C,nh,nw,ph,pw\n        tokens = tokens.contiguous().view(B, C, -1, ph, pw)\n        tokens = tokens.permute(0, 2, 3, 4, 1).reshape(B, -1, C)\n\n        for blk in self.transformer:\n            tokens = blk(tokens)\n\n        feat = tokens.view(B, -1, ph * pw, C).permute(0, 3, 1, 2)\n        nh = H // ph\n        nw = W // pw\n        feat = feat.view(B, C, nh, nw, ph, pw).permute(0, 1, 2, 4, 3, 5)\n        feat = feat.reshape(B, C, H, W)\n\n        if feat.shape[-2:] != x.shape[-2:]:\n            feat = F.interpolate(feat, size=x.shape[-2:], mode=\"bilinear\", align_corners=False)\n\n        out = self.fuse(torch.cat([x, feat], dim=1))\n        return out\n\n\nclass SpatialReductionAttention(nn.Module):\n    def __init__(self, dim, heads=8, sr_ratio=2, dropout=0.0):\n        super().__init__()\n        self.heads = heads\n        self.scale = (dim // heads) ** -0.5\n        self.q = nn.Linear(dim, dim)\n        self.kv = nn.Linear(dim, dim * 2)\n        self.sr_ratio = sr_ratio\n        if sr_ratio > 1:\n            self.sr = nn.Conv2d(dim, dim, kernel_size=sr_ratio, stride=sr_ratio)\n            self.norm = nn.LayerNorm(dim)\n        else:\n            self.sr = None\n        self.proj = nn.Linear(dim, dim)\n        self.drop = nn.Dropout(dropout)\n\n    def forward(self, x, hw: Tuple[int, int]):\n        B, N, C = x.shape\n        q = self.q(x).reshape(B, N, self.heads, C // self.heads).permute(0, 2, 1, 3)\n\n        if self.sr is not None:\n            H, W = hw\n            feat = x.transpose(1, 2).reshape(B, C, H, W)\n            feat = self.sr(feat)\n            feat = feat.reshape(B, C, -1).transpose(1, 2)\n            feat = self.norm(feat)\n        else:\n            feat = x\n\n        kv = self.kv(feat)\n        k, v = kv.chunk(2, dim=-1)\n        k = k.reshape(B, -1, self.heads, C // self.heads).permute(0, 2, 3, 1)\n        v = v.reshape(B, -1, self.heads, C // self.heads).permute(0, 2, 1, 3)\n\n        attn = torch.matmul(q, k) * self.scale\n        attn = attn.softmax(dim=-1)\n        attn = self.drop(attn)\n        out = torch.matmul(attn, v).permute(0, 2, 1, 3).reshape(B, N, C)\n        out = self.proj(out)\n        return out\n\n\nclass PVTBlock(nn.Module):\n    def __init__(self, dim, heads=8, sr_ratio=2, dropout=0.0, mlp_ratio=4.0):\n        super().__init__()\n        self.norm1 = nn.LayerNorm(dim)\n        self.sra = SpatialReductionAttention(dim, heads=heads, sr_ratio=sr_ratio, dropout=dropout)\n        self.norm2 = nn.LayerNorm(dim)\n        self.ff = FeedForward(dim, mlp_ratio=mlp_ratio, dropout=dropout)\n\n    def forward(self, x, hw: Tuple[int, int]):\n        x = x + self.sra(self.norm1(x), hw)\n        x = x + self.ff(self.norm2(x))\n        return x\n\n\nclass LocalMambaBlock(nn.Module):\n\n    def __init__(self, dim, kernel_size=5, dropout=0.0):\n        super().__init__()\n        self.norm = nn.LayerNorm(dim)\n        self.dwconv = nn.Conv1d(dim, dim, kernel_size=kernel_size, padding=kernel_size // 2, groups=dim)\n        self.gate = nn.Linear(dim, dim)\n        self.proj = nn.Linear(dim, dim)\n        self.drop = nn.Dropout(dropout)\n\n    def forward(self, x):\n        shortcut = x\n        x = self.norm(x)\n        g = torch.sigmoid(self.gate(x))\n        x = (x * g).transpose(1, 2)  # B, C, N\n        x = self.dwconv(x).transpose(1, 2)\n        x = self.proj(x)\n        x = self.drop(x)\n        return shortcut + x\n\n\nclass T2TRetokenizer(nn.Module):\n    \"\"\"\n    将 4x4 tile token 做局部 attention + 下采样到 2x2\n    \"\"\"\n\n    def __init__(self, dim, depth=2, heads=4, dropout=0.0):\n        super().__init__()\n        self.blocks = nn.ModuleList(\n            [AttentionBlock(dim, heads=heads, dropout=dropout, mlp_ratio=2.0) for _ in range(depth)]\n        )\n\n    def forward(self, tokens: torch.Tensor, grid_hw: Tuple[int, int]):\n        B, T, C = tokens.shape\n        H, W = grid_hw\n        feat_map = tokens.transpose(1, 2).reshape(B, C, H, W)\n        seq = feat_map.flatten(2).transpose(1, 2)\n        for blk in self.blocks:\n            seq = blk(seq)\n        seq_map = seq.transpose(1, 2).reshape(B, C, H, W)\n        pooled = F.adaptive_avg_pool2d(seq_map, (2, 2))\n        retokens = pooled.flatten(2).transpose(1, 2)\n        return retokens, seq_map\n\n\nclass CrossScaleFusion(nn.Module):\n    def __init__(self, dim, heads=6, dropout=0.0, layers=2):\n        super().__init__()\n        self.layers_s = nn.ModuleList(\n            [AttentionBlock(dim, heads=heads, dropout=dropout, mlp_ratio=2.0) for _ in range(layers)]\n        )\n        self.layers_b = nn.ModuleList(\n            [AttentionBlock(dim, heads=heads, dropout=dropout, mlp_ratio=2.0) for _ in range(layers)]\n        )\n        self.cross_s = nn.ModuleList(\n            [\n                nn.MultiheadAttention(dim, heads, dropout=dropout, batch_first=True, kdim=dim, vdim=dim)\n                for _ in range(layers)\n            ]\n        )\n        self.cross_b = nn.ModuleList(\n            [\n                nn.MultiheadAttention(dim, heads, dropout=dropout, batch_first=True, kdim=dim, vdim=dim)\n                for _ in range(layers)\n            ]\n        )\n        self.norm_s = nn.LayerNorm(dim)\n        self.norm_b = nn.LayerNorm(dim)\n\n    def forward(self, tok_s: torch.Tensor, tok_b: torch.Tensor):\n        B, Ts, C = tok_s.shape\n        Tb = tok_b.shape[1]\n        cls_s = tok_s.new_zeros(B, 1, C)\n        cls_b = tok_b.new_zeros(B, 1, C)\n        tok_s = torch.cat([cls_s, tok_s], dim=1)\n        tok_b = torch.cat([cls_b, tok_b], dim=1)\n\n        for ls, lb, cs, cb in zip(self.layers_s, self.layers_b, self.cross_s, self.cross_b):\n            tok_s = ls(tok_s)\n            tok_b = lb(tok_b)\n            q_s = self.norm_s(tok_s[:, :1])\n            q_b = self.norm_b(tok_b[:, :1])\n            cls_s_upd, _ = cs(\n                q_s,\n                torch.cat([tok_b, q_b], dim=1),\n                torch.cat([tok_b, q_b], dim=1),\n                need_weights=False,\n            )\n            cls_b_upd, _ = cb(\n                q_b,\n                torch.cat([tok_s, q_s], dim=1),\n                torch.cat([tok_s, q_s], dim=1),\n                need_weights=False,\n            )\n            tok_s = torch.cat([tok_s[:, :1] + cls_s_upd, tok_s[:, 1:]], dim=1)\n            tok_b = torch.cat([tok_b[:, :1] + cls_b_upd, tok_b[:, 1:]], dim=1)\n\n        tokens = torch.cat([tok_s[:, :1], tok_b[:, :1], tok_s[:, 1:], tok_b[:, 1:]], dim=1)\n        return tokens  # shape ~ (B, 2 + Ts + Tb, C)\n\nclass TileEncoder(nn.Module):\n    def __init__(self, backbone: nn.Module, input_res: int):\n        super().__init__()\n        self.backbone = backbone\n        self.input_res = input_res\n\n    def forward(self, x: torch.Tensor, grid: Tuple[int, int]):\n        B, C, H, W = x.shape\n        r, c = grid\n        hs = torch.linspace(0, H, steps=r + 1, device=x.device).round().long()\n        ws = torch.linspace(0, W, steps=c + 1, device=x.device).round().long()\n        tiles = []\n        for i in range(r):\n            for j in range(c):\n                rs, re = hs[i].item(), hs[i + 1].item()\n                cs, ce = ws[j].item(), ws[j + 1].item()\n                xt = x[:, :, rs:re, cs:ce]\n                if xt.shape[-2:] != (self.input_res, self.input_res):\n                    xt = F.interpolate(xt, size=(self.input_res, self.input_res), mode=\"bilinear\", align_corners=False)\n                tiles.append(xt)\n        tiles = torch.stack(tiles, dim=1)  # (B, T, C, H, W)\n        flat = tiles.view(-1, C, self.input_res, self.input_res)\n        feats = self.backbone(flat)\n        feats = feats.view(B, -1, feats.shape[-1])\n        return feats\n\n\nclass PyramidMixer(nn.Module):\n    def __init__(\n        self,\n        dim_in: int,\n        dims: Tuple[int, int, int],\n        mobilevit_heads: int = 4,\n        mobilevit_depth: int = 2,\n        sra_heads: int = 6,\n        sra_ratio: int = 2,\n        mamba_depth: int = 3,\n        mamba_kernel: int = 5,\n        dropout: float = 0.0,\n    ):\n        super().__init__()\n        c1, c2, c3 = dims\n        self.proj1 = nn.Linear(dim_in, c1)\n        self.mobilevit = MobileViTBlock(c1, heads=mobilevit_heads, depth=mobilevit_depth, dropout=dropout)\n        self.proj2 = nn.Linear(c1, c2)\n        self.pvt = PVTBlock(c2, heads=sra_heads, sr_ratio=sra_ratio, dropout=dropout, mlp_ratio=3.0)\n        self.mamba_local = LocalMambaBlock(c2, kernel_size=mamba_kernel, dropout=dropout)\n        self.proj3 = nn.Linear(c2, c3)\n        self.mamba_global = nn.ModuleList(\n            [LocalMambaBlock(c3, kernel_size=mamba_kernel, dropout=dropout) for _ in range(mamba_depth)]\n        )\n        self.final_attn = AttentionBlock(c3, heads=min(8, c3 // 64 + 1), dropout=dropout, mlp_ratio=2.0)\n\n    def _tokens_to_map(self, tokens: torch.Tensor, target_hw: Tuple[int, int]):\n        B, N, C = tokens.shape\n        H, W = target_hw\n        need = H * W\n        if N < need:\n            pad = tokens.new_zeros(B, need - N, C)\n            tokens = torch.cat([tokens, pad], dim=1)\n        tokens = tokens[:, :need, :]\n        feat_map = tokens.transpose(1, 2).reshape(B, C, H, W)\n        return feat_map\n\n    @staticmethod\n    def _fit_hw(n_tokens: int) -> Tuple[int, int]:\n        \"\"\"选择一个接近方形、满足 h*w>=n_tokens 的网格。\"\"\"\n        h = int(math.sqrt(n_tokens))\n        w = h\n        while h * w < n_tokens:\n            w += 1\n            if h * w < n_tokens:\n                h += 1\n        return h, w\n\n    def forward(self, tokens: torch.Tensor):\n        # 约 10 tokens -> 3x4 map\n        B, N, C = tokens.shape\n        map_hw = (3, 4)\n        feat_map = self._tokens_to_map(tokens, map_hw)\n\n        t1 = self.proj1(tokens)\n        m1 = self._tokens_to_map(t1, map_hw)\n        m1 = self.mobilevit(m1)\n        t1_out = m1.flatten(2).transpose(1, 2)[:, :N]\n\n        # Stage2: 下采样 token 数量（平均池化）\n        t2 = self.proj2(t1_out)\n        new_len = max(4, N // 2)\n        t2 = t2[:, :new_len] + F.adaptive_avg_pool1d(t2.transpose(1, 2), new_len).transpose(1, 2)\n        hw2 = self._fit_hw(t2.size(1))\n        if t2.size(1) < hw2[0] * hw2[1]:\n            pad = t2.new_zeros(B, hw2[0] * hw2[1] - t2.size(1), t2.size(2))\n            t2 = torch.cat([t2, pad], dim=1)\n        t2 = self.pvt(t2, hw2)\n        t2 = self.mamba_local(t2)\n\n        # Stage3: 全局\n        t3 = self.proj3(t2)\n        pooled = torch.stack([t3.mean(dim=1), t3.max(dim=1).values], dim=1)  # (B,2,C)\n        t3 = pooled\n        for blk in self.mamba_global:\n            t3 = blk(t3)\n        t3 = self.final_attn(t3)\n        global_feat = t3.mean(dim=1)\n        return global_feat, {\"stage1_map\": m1.detach(), \"stage2_tokens\": t2.detach(), \"stage3_tokens\": t3.detach()}\n\n\nclass CrossPVT_T2T_MambaDINO(nn.Module):\n    def __init__(self, dropout: float = 0.1, hidden_ratio: float = 0.35):\n        super().__init__()\n        self.backbone, self.feat_dim, self.backbone_name, self.input_res = self._build_dino_backbone()\n        self.tile_encoder = TileEncoder(self.backbone, self.input_res)\n        self.t2t = T2TRetokenizer(self.feat_dim, depth=CFG.t2t_depth, heads=CFG.cross_heads, dropout=dropout)\n        self.cross = CrossScaleFusion(\n            self.feat_dim, heads=CFG.cross_heads, dropout=dropout, layers=CFG.cross_layers\n        )\n        self.pyramid = PyramidMixer(\n            dim_in=self.feat_dim,\n            dims=CFG.pyramid_dims,\n            mobilevit_heads=CFG.mobilevit_heads,\n            mobilevit_depth=CFG.mobilevit_depth,\n            sra_heads=CFG.sra_heads,\n            sra_ratio=CFG.sra_ratio,\n            mamba_depth=CFG.mamba_depth,\n            mamba_kernel=CFG.mamba_kernel,\n            dropout=dropout,\n        )\n\n        combined = CFG.pyramid_dims[-1] * 2\n        self.combined_dim = combined\n        hidden = max(32, int(combined * hidden_ratio))\n\n        def head():\n            return nn.Sequential(\n                nn.Linear(combined, hidden),\n                nn.GELU(),\n                nn.Dropout(dropout),\n                nn.Linear(hidden, 1),\n            )\n\n        self.head_green = head()\n        self.head_clover = head()\n        self.head_dead = head()\n        self.score_head = nn.Sequential(nn.LayerNorm(combined), nn.Linear(combined, 1))\n        self.aux_head = (\n            nn.Sequential(nn.LayerNorm(CFG.pyramid_dims[1]), nn.Linear(CFG.pyramid_dims[1], 5))\n            if CFG.aux_head\n            else None\n        )\n        self.softplus = nn.Softplus(beta=1.0)\n\n        self.cross_gate_left = nn.Linear(CFG.pyramid_dims[-1], CFG.pyramid_dims[-1])\n        self.cross_gate_right = nn.Linear(CFG.pyramid_dims[-1], CFG.pyramid_dims[-1])\n\n    def _build_dino_backbone(self):\n        \"\"\"\n        只创建骨干结构，不加载预训练权重（pretrained=False），\n        避免在 Kaggle 无网环境下下载；权重由 checkpoint 提供。\n        \"\"\"\n        last_err = None\n        for name in CFG.dino_candidates:\n            for gp in [\"token\", \"avg\", \"__default__\"]:\n                try:\n                    if gp == \"__default__\":\n                        m = timm.create_model(name, pretrained=False, num_classes=0)\n                        gp_str = \"default\"\n                    else:\n                        m = timm.create_model(name, pretrained=False, num_classes=0, global_pool=gp)\n                        gp_str = gp\n                    feat = m.num_features\n                    input_res = self._infer_input_res(m)\n                    LOGGER.info(\n                        f\"✅ 使用 DINO 主干: {name} | global_pool={gp_str} | \"\n                        f\"feat_dim={feat} | input_res={input_res}\"\n                    )\n                    if hasattr(m, \"set_grad_checkpointing\"):\n                        m.set_grad_checkpointing(True)\n                    return m, feat, name, int(input_res)\n                except Exception as e:\n                    last_err = e\n                    continue\n        raise RuntimeError(f\"无法创建任何 DINO 主干。最后错误: {last_err}\")\n\n    @staticmethod\n    def _infer_input_res(m) -> int:\n        if hasattr(m, \"patch_embed\") and hasattr(m.patch_embed, \"img_size\"):\n            isz = m.patch_embed.img_size\n            return int(isz if isinstance(isz, (int, float)) else isz[0])\n        if hasattr(m, \"img_size\"):\n            isz = m.img_size\n            return int(isz if isinstance(isz, (int, float)) else isz[0])\n        dc = getattr(m, \"default_cfg\", {}) or {}\n        ins = dc.get(\"input_size\", None)\n        if ins:\n            if isinstance(ins, (tuple, list)) and len(ins) >= 2:\n                return int(ins[1])\n            return int(ins if isinstance(ins, (int, float)) else 224)\n        return 518\n\n    def _half_forward(self, x_half: torch.Tensor):\n        tiles_small = self.tile_encoder(x_half, CFG.small_grid)\n        tiles_big = self.tile_encoder(x_half, CFG.big_grid)\n        t2, stage1_map = self.t2t(tiles_small, CFG.small_grid)\n        fused = self.cross(t2, tiles_big)\n        feat, feat_maps = self.pyramid(fused)\n        feat_maps[\"stage1_map\"] = stage1_map\n        return feat, feat_maps\n\n    def _merge_heads(self, f_l: torch.Tensor, f_r: torch.Tensor):\n        g_l = torch.sigmoid(self.cross_gate_left(f_r))\n        g_r = torch.sigmoid(self.cross_gate_right(f_l))\n        f_l = f_l * g_l\n        f_r = f_r * g_r\n        f = torch.cat([f_l, f_r], dim=1)\n        green_pos = self.softplus(self.head_green(f))\n        clover_pos = self.softplus(self.head_clover(f))\n        dead_pos = self.softplus(self.head_dead(f))\n        gdm = green_pos + clover_pos\n        total = gdm + dead_pos\n        return total, gdm, green_pos, f\n\n    def _param_device_dtype(self):\n        try:\n            ref = next(self.parameters())\n            return ref.device, ref.dtype\n        except StopIteration:\n            return torch.device(\"cpu\"), torch.float32\n\n    def _empty_forward_output(self, device=None, dtype=None, return_features: bool = False):\n        if device is None or dtype is None:\n            device_p, dtype_p = self._param_device_dtype()\n            if device is None:\n                device = device_p\n            if dtype is None:\n                dtype = dtype_p\n\n        zero = torch.zeros(0, 1, device=device, dtype=dtype)\n        out = {\n            \"total\": zero,\n            \"gdm\": zero,\n            \"green\": zero,\n            \"score_feat\": torch.zeros(0, self.combined_dim, device=device, dtype=dtype),\n        }\n        if self.aux_head is not None:\n            out[\"aux\"] = torch.zeros(0, len(CFG.ALL_TARGET_COLS), device=device, dtype=dtype)\n        if return_features:\n            out[\"feature_maps\"] = {}\n        return out\n\n    def forward(self, *inputs, x_left=None, x_right=None, return_features: bool = False):\n        # 兼容 DataParallel 传参方式（单 tensor / 元组）\n        if inputs:\n            if len(inputs) == 1:\n                first = inputs[0]\n                if isinstance(first, (tuple, list)):\n                    if len(first) >= 1:\n                        x_left = first[0]\n                    if len(first) >= 2:\n                        x_right = first[1]\n                else:\n                    x_left = first\n            else:\n                x_left = inputs[0]\n                x_right = inputs[1]\n\n        if x_left is None:\n            return self._empty_forward_output(return_features=return_features)\n        if isinstance(x_left, torch.Tensor) and x_left.shape[0] == 0:\n            return self._empty_forward_output(return_features=return_features)\n\n        if x_right is None:\n            if isinstance(x_left, torch.Tensor):\n                if x_left.shape[1] % 2 != 0:\n                    raise ValueError(\"无法从单个张量推断左右分支，请显式提供 x_right。\")\n                x_left, x_right = torch.chunk(x_left, 2, dim=1)\n            else:\n                raise ValueError(\"缺少 x_right 输入。\")\n\n        feat_l, feats_l = self._half_forward(x_left)\n        feat_r, feats_r = self._half_forward(x_right)\n        total, gdm, green, f_concat = self._merge_heads(feat_l, feat_r)\n\n        out = {\n            \"total\": total,\n            \"gdm\": gdm,\n            \"green\": green,\n            \"score_feat\": f_concat,\n        }\n        if self.aux_head is not None:\n            aux_tokens = torch.cat([feats_l[\"stage2_tokens\"], feats_r[\"stage2_tokens\"]], dim=1)\n            aux_pred = self.softplus(self.aux_head(aux_tokens.mean(dim=1)))\n            out[\"aux\"] = aux_pred  # 顺序与 CFG.ALL_TARGET_COLS 对齐\n        if return_features:\n            out[\"feature_maps\"] = {\n                \"stage1_left\": feats_l.get(\"stage1_map\"),\n                \"stage1_right\": feats_r.get(\"stage1_map\"),\n                \"stage3_left\": feats_l.get(\"stage3_tokens\"),\n                \"stage3_right\": feats_r.get(\"stage3_tokens\"),\n            }\n        return out\n\n\n# =============================================================================\n# 推理配置 (Modified)\n# =============================================================================\nclass INF_CFG:\n    # 数据路径（Kaggle 默认，本地测试可修改）\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\n    # [修改] 实验目录（你的权重所在目录）\n    EXPERIMENT_DIR = \"/kaggle/input/csiro/pytorch/default/12\"\n\n    # Checkpoint 路径（5-fold）\n    CKPT_PATTERN_FOLD_X = os.path.join(\n        EXPERIMENT_DIR, \"fold_{fold}\", \"checkpoints\", \"best_wr2.pt\"\n    )\n    CKPT_PATTERN_FOLDX = os.path.join(\n        EXPERIMENT_DIR, \"fold{fold}\", \"checkpoints\", \"best_wr2.pt\"\n    )\n    N_FOLDS = 5\n\n    # [修改] 输出文件名称\n    SUBMISSION_FILE = \"preds_dino_wide.csv\"\n\n    # 推理设置\n    DEVICE = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n    BATCH_SIZE = 1\n    NUM_WORKERS = 0\n    MIXED_PRECISION = True\n\n    # TTA 设置\n    USE_TTA = True\n    TTA_TRANSFORMS = [\"original\", \"hflip\", \"vflip\"]\n\n    # 目标列顺序（与训练一致）\n    ALL_TARGET_COLS = [\"Dry_Green_g\", \"Dry_Dead_g\", \"Dry_Clover_g\", \"GDM_g\", \"Dry_Total_g\"]\n\n\nprint(f\"Device: {INF_CFG.DEVICE}\")\nprint(f\"Experiment Dir: {INF_CFG.EXPERIMENT_DIR}\")\n\n\n# =============================================================================\n# 数据集\n# =============================================================================\nclass TestBiomassDataset(Dataset):\n    \"\"\"测试数据集：左右两路输入\"\"\"\n\n    def __init__(self, df: pd.DataFrame, transform, image_dir: str):\n        self.df = df.reset_index(drop=True)\n        self.transform = transform\n        self.image_dir = image_dir\n        self.paths = self.df[\"image_path\"].values\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        filename = os.path.basename(self.paths[idx])\n        full_path = os.path.join(self.image_dir, filename)\n\n        img = cv2.imread(full_path)\n        if img is None:\n            # 容错：若读图失败，用黑图占位\n            img = np.zeros((1000, 2000, 3), dtype=np.uint8)\n        else:\n            img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n\n        # 左右切半（与训练一致）\n        h, w, _ = img.shape\n        mid = w // 2\n        left = img[:, :mid]\n        right = img[:, mid:]\n\n        left_t = self.transform(image=left)[\"image\"]\n        right_t = self.transform(image=right)[\"image\"]\n\n        return left_t, right_t\n\n\n# =============================================================================\n# TTA 变换\n# =============================================================================\ndef get_tta_transforms(img_size: int) -> List[A.Compose]:\n    \"\"\"生成 TTA 变换列表\"\"\"\n    base = [\n        A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),\n        ToTensorV2(),\n    ]\n\n    transforms = []\n\n    # 原图\n    transforms.append(\n        A.Compose([\n            A.Resize(img_size, img_size, interpolation=cv2.INTER_AREA),\n            *base,\n        ])\n    )\n\n    # 水平翻转\n    transforms.append(\n        A.Compose([\n            A.HorizontalFlip(p=1.0),\n            A.Resize(img_size, img_size, interpolation=cv2.INTER_AREA),\n            *base,\n        ])\n    )\n\n    # 垂直翻转\n    transforms.append(\n        A.Compose([\n            A.VerticalFlip(p=1.0),\n            A.Resize(img_size, img_size, interpolation=cv2.INTER_AREA),\n            *base,\n        ])\n    )\n\n    return transforms\n\n\n# =============================================================================\n# 权重加载\n# =============================================================================\ndef strip_module_prefix(state_dict: dict) -> dict:\n    \"\"\"移除 DataParallel 的 module. 前缀\"\"\"\n    if not state_dict:\n        return state_dict\n\n    keys = list(state_dict.keys())\n    if all(k.startswith(\"module.\") for k in keys):\n        return {k[len(\"module.\"):]: v for k, v in state_dict.items()}\n    return state_dict\n\n\ndef load_checkpoint(path: str) -> dict:\n    \"\"\"加载 checkpoint\"\"\"\n    if not os.path.exists(path):\n        raise FileNotFoundError(f\"Checkpoint not found: {path}\")\n\n    try:\n        state = torch.load(path, map_location=\"cpu\", weights_only=False)\n    except TypeError:\n        state = torch.load(path, map_location=\"cpu\")\n\n    return state\n\n\ndef load_model_from_checkpoint(ckpt_path: str) -> nn.Module:\n    \"\"\"从 checkpoint 加载模型\"\"\"\n    print(f\"\\n加载 checkpoint: {os.path.basename(ckpt_path)}\")\n\n    state = load_checkpoint(ckpt_path)\n\n    # 用 checkpoint 中的 cfg 覆盖当前 CFG（保证结构对应）\n    cfg_dict = state.get(\"cfg\", {})\n    update_cfg_from_checkpoint(cfg_dict)\n\n    dropout = cfg_dict.get(\"dropout\", CFG.dropout)\n    hidden_ratio = cfg_dict.get(\"hidden_ratio\", CFG.hidden_ratio)\n\n    # 创建模型（结构与训练一致）\n    model = CrossPVT_T2T_MambaDINO(dropout=dropout, hidden_ratio=hidden_ratio)\n\n    # 提取模型状态\n    model_state = state.get(\"model_state\")\n    if model_state is None:\n        # 如果 checkpoint 直接是 state_dict\n        model_state = state\n\n    # 移除 module. 前缀\n    model_state = strip_module_prefix(model_state)\n\n    # 加载权重\n    missing_keys, unexpected_keys = model.load_state_dict(model_state, strict=False)\n\n    if missing_keys:\n        print(f\"  ⚠️  缺失的键: {len(missing_keys)} 个\")\n        if len(missing_keys) <= 10:\n            for k in missing_keys[:10]:\n                print(f\"    - {k}\")\n\n    if unexpected_keys:\n        print(f\"  ⚠️  意外的键: {len(unexpected_keys)} 个\")\n        if len(unexpected_keys) <= 10:\n            for k in unexpected_keys[:10]:\n                print(f\"    - {k}\")\n\n    model.to(INF_CFG.DEVICE)\n    model.eval()\n\n    # 获取输入分辨率\n    input_res = getattr(model, \"input_res\", 518)\n    backbone_name = getattr(model, \"backbone_name\", \"unknown\")\n\n    print(f\"  ✓ 模型加载成功 | backbone={backbone_name} | input_res={input_res}\")\n\n    return model\n\n\n# =============================================================================\n# 推理\n# =============================================================================\ndef pack5_targets(total: torch.Tensor, gdm: torch.Tensor, green: torch.Tensor) -> torch.Tensor:\n    \"\"\"将 total, gdm, green 打包为 5 个目标\"\"\"\n    clover = gdm - green\n    dead = total - gdm\n    return torch.cat([green, dead, clover, gdm, total], dim=1)\n\n\n@torch.no_grad()\ndef predict_one_view(models: List[nn.Module], loader: DataLoader) -> np.ndarray:\n    \"\"\"对单个 TTA 视角进行预测\"\"\"\n    preds_list = []\n    amp_dtype = \"cuda\" if INF_CFG.DEVICE.type == \"cuda\" else \"cpu\"\n\n    for xl, xr in tqdm(loader, desc=\"  Predicting\", leave=False):\n        xl = xl.to(INF_CFG.DEVICE, non_blocking=True)\n        xr = xr.to(INF_CFG.DEVICE, non_blocking=True)\n\n        # 拼接为单 tensor（与训练时的 DataParallel 调用方式一致）\n        x_cat = torch.cat([xl, xr], dim=1)\n\n        per_model_preds = []\n\n        with torch.amp.autocast(amp_dtype, enabled=INF_CFG.MIXED_PRECISION):\n            for model in models:\n                out = model(x_cat, return_features=False)\n\n                total = out[\"total\"]\n                gdm = out[\"gdm\"]\n                green = out[\"green\"]\n\n                # 打包为 5 个目标\n                five = pack5_targets(total, gdm, green)\n\n                # 非负约束\n                five = torch.clamp(five, min=0.0)\n\n                per_model_preds.append(five.float().cpu())\n\n        # 5-fold ensemble 平均\n        stacked = torch.mean(torch.stack(per_model_preds, dim=0), dim=0)\n        preds_list.append(stacked.numpy())\n\n    return np.concatenate(preds_list, axis=0)\n\n\ndef run_inference(test_df: pd.DataFrame, image_dir: str) -> np.ndarray:\n    \"\"\"运行完整推理流程（5-fold ensemble + TTA）\"\"\"\n    print(\"\\n\" + \"=\" * 80)\n    print(\"开始推理\")\n    print(\"=\" * 80)\n\n    # 加载所有 fold 的模型\n    print(\"\\n加载模型 (5-fold)...\")\n    models = []\n    input_res = None\n\n    for fold in range(INF_CFG.N_FOLDS):\n        ckpt_path = INF_CFG.CKPT_PATTERN_FOLD_X.format(fold=fold)\n        if not os.path.exists(ckpt_path):\n            ckpt_path = INF_CFG.CKPT_PATTERN_FOLDX.format(fold=fold)\n\n        if not os.path.exists(ckpt_path):\n            print(f\"  ⚠️  Fold {fold} checkpoint 不存在，尝试路径:\")\n            print(f\"    - {INF_CFG.CKPT_PATTERN_FOLD_X.format(fold=fold)}\")\n            print(f\"    - {INF_CFG.CKPT_PATTERN_FOLDX.format(fold=fold)}\")\n            continue\n\n        model = load_model_from_checkpoint(ckpt_path)\n        models.append(model)\n\n        if input_res is None:\n            input_res = getattr(model, \"input_res\", 518)\n            print(f\"  输入分辨率: {input_res}\")\n\n    if len(models) == 0:\n        print(\"\\n❌ 错误：没有找到任何可用的 checkpoint！\")\n        print(f\"   请检查实验目录: {INF_CFG.EXPERIMENT_DIR}\")\n        for fold in range(INF_CFG.N_FOLDS):\n            print(f\"     - {INF_CFG.CKPT_PATTERN_FOLD_X.format(fold=fold)}\")\n            print(f\"     - {INF_CFG.CKPT_PATTERN_FOLDX.format(fold=fold)}\")\n        raise RuntimeError(\"没有找到任何可用的 checkpoint！\")\n\n    print(f\"\\n✓ 成功加载 {len(models)} 个模型\")\n\n    # TTA 推理\n    if INF_CFG.USE_TTA:\n        tta_transforms = get_tta_transforms(input_res)\n        print(f\"\\n使用 TTA: {len(tta_transforms)} 个视角\")\n\n        per_view_preds = []\n\n        for i, transform in enumerate(tta_transforms):\n            view_name = INF_CFG.TTA_TRANSFORMS[i] if i < len(INF_CFG.TTA_TRANSFORMS) else f\"view_{i+1}\"\n            print(f\"\\n--- TTA 视角 {i+1}/{len(tta_transforms)}: {view_name} ---\")\n\n            ds = TestBiomassDataset(test_df, transform, image_dir)\n            dl = DataLoader(\n                ds,\n                batch_size=INF_CFG.BATCH_SIZE,\n                shuffle=False,\n                num_workers=INF_CFG.NUM_WORKERS,\n                pin_memory=True,\n            )\n\n            view_pred = predict_one_view(models, dl)\n            per_view_preds.append(view_pred)\n\n        final_pred = np.mean(per_view_preds, axis=0)\n        print(f\"\\n✓ TTA 完成，最终预测形状: {final_pred.shape}\")\n    else:\n        transform = get_tta_transforms(input_res)[0]\n        ds = TestBiomassDataset(test_df, transform, image_dir)\n        dl = DataLoader(\n            ds,\n            batch_size=INF_CFG.BATCH_SIZE,\n            shuffle=False,\n            num_workers=INF_CFG.NUM_WORKERS,\n            pin_memory=True,\n        )\n        final_pred = predict_one_view(models, dl)\n\n    return final_pred\n\n\n# =============================================================================\n# 生成提交文件 (Modified for Wide Format)\n# =============================================================================\ndef create_submission(final_pred: np.ndarray, test_long: pd.DataFrame, test_unique: pd.DataFrame) -> pd.DataFrame:\n    \"\"\"生成宽格式输出文件 (用于集成)\"\"\"\n    print(\"\\n\" + \"=\" * 80)\n    print(\"生成集成用宽格式文件\")\n    print(\"=\" * 80)\n\n    # 提取各目标 (对应 pack5_targets 的顺序: green, dead, clover, gdm, total)\n    green = final_pred[:, 0]\n    dead = final_pred[:, 1]\n    clover = final_pred[:, 2]\n    gdm = final_pred[:, 3]\n    total = final_pred[:, 4]\n\n    # 非负 & NaN/Inf 处理\n    def clean(x):\n        x = np.nan_to_num(x, nan=0.0, posinf=0.0, neginf=0.0)\n        return np.maximum(0, x)\n\n    green, dead, clover, gdm, total = map(clean, [green, dead, clover, gdm, total])\n\n    # 构造 Wide DataFrame\n    wide = pd.DataFrame(\n        {\n            \"image_path\": test_unique[\"image_path\"],\n            \"Dry_Green_g\": green,\n            \"Dry_Dead_g\": dead,\n            \"Dry_Clover_g\": clover,\n            \"GDM_g\": gdm,\n            \"Dry_Total_g\": total,\n        }\n    )\n\n    # 提取 base_id 用于后续集成索引\n    wide['base_id'] = wide['image_path'].apply(lambda x: os.path.splitext(os.path.basename(x))[0])\n\n    # 调整列顺序，确保 base_id 在前，且包含所有必要的列\n    output_cols = ['base_id', 'Dry_Total_g', 'GDM_g', 'Dry_Green_g', 'Dry_Dead_g', 'Dry_Clover_g']\n    final_df = wide[output_cols]\n\n    # 保存文件\n    final_df.to_csv(INF_CFG.SUBMISSION_FILE, index=False)\n\n    print(f\"\\n✓ 宽格式文件已保存: {INF_CFG.SUBMISSION_FILE}\")\n    print(f\"  行数: {len(final_df)}\")\n    print(f\"\\n前 5 行预览:\")\n    print(final_df.head(5).to_string())\n    return final_df\n\n\n# =============================================================================\n# 主函数（使用 parse_known_args 避免 SystemExit:2）\n# =============================================================================\ndef parse_args():\n    parser = argparse.ArgumentParser(description=\"CSIRO v4 CrossPVT T2T Mamba Inference\")\n\n    parser.add_argument(\n        \"--test-csv\",\n        type=str,\n        default=None,\n        help=\"测试集 CSV 路径（默认: INF_CFG.TEST_CSV）\",\n    )\n    parser.add_argument(\n        \"--test-image-dir\",\n        type=str,\n        default=None,\n        help=\"测试图像目录（默认: INF_CFG.TEST_IMAGE_DIR）\",\n    )\n    parser.add_argument(\n        \"--experiment-dir\",\n        type=str,\n        default=None,\n        help=\"实验目录（checkpoint 所在位置，默认: INF_CFG.EXPERIMENT_DIR）\",\n    )\n    parser.add_argument(\n        \"--output\",\n        type=str,\n        default=None,\n        help=\"输出文件路径（默认: INF_CFG.SUBMISSION_FILE）\",\n    )\n    parser.add_argument(\n        \"--batch-size\",\n        type=int,\n        default=None,\n        help=\"批次大小（默认: INF_CFG.BATCH_SIZE）\",\n    )\n    parser.add_argument(\n        \"--no-tta\",\n        action=\"store_true\",\n        help=\"禁用 TTA\",\n    )\n\n    # 关键修改：用 parse_known_args 吞掉 Kaggle/Colab 多余参数（如 -f /tmp/xxx.json）\n    args, _ = parser.parse_known_args()\n    return args\n\n\ndef main():\n    # [新增] 设置随机种子\n    seed_everything(43)\n\n    args = parse_args()\n\n    # 更新配置\n    if args.test_csv:\n        INF_CFG.TEST_CSV = args.test_csv\n    if args.test_image_dir:\n        INF_CFG.TEST_IMAGE_DIR = args.test_image_dir\n    if args.experiment_dir:\n        INF_CFG.EXPERIMENT_DIR = args.experiment_dir\n        INF_CFG.CKPT_PATTERN_FOLD_X = os.path.join(\n            INF_CFG.EXPERIMENT_DIR, \"fold_{fold}\", \"checkpoints\", \"best_wr2.pt\"\n        )\n        INF_CFG.CKPT_PATTERN_FOLDX = os.path.join(\n            INF_CFG.EXPERIMENT_DIR, \"fold{fold}\", \"checkpoints\", \"best_wr2.pt\"\n        )\n    if args.output:\n        INF_CFG.SUBMISSION_FILE = args.output\n    if args.batch_size:\n        INF_CFG.BATCH_SIZE = args.batch_size\n    if args.no_tta:\n        INF_CFG.USE_TTA = False\n\n    print(\"=\" * 80)\n    print(\"CSIRO Image2Biomass - v4 CrossPVT T2T Mamba Inference\")\n    print(\"=\" * 80)\n    print(f\"测试 CSV: {INF_CFG.TEST_CSV}\")\n    print(f\"测试图像目录: {INF_CFG.TEST_IMAGE_DIR}\")\n    print(f\"实验目录: {INF_CFG.EXPERIMENT_DIR}\")\n    print(f\"输出文件: {INF_CFG.SUBMISSION_FILE}\")\n    print(f\"批次大小: {INF_CFG.BATCH_SIZE}\")\n    print(f\"使用 TTA: {INF_CFG.USE_TTA}\")\n\n    # 加载测试数据\n    print(\"\\n加载测试数据...\")\n    if not os.path.exists(INF_CFG.TEST_CSV):\n        raise FileNotFoundError(f\"测试 CSV 不存在: {INF_CFG.TEST_CSV}\")\n\n    test_long = pd.read_csv(INF_CFG.TEST_CSV)\n    test_unique = test_long.drop_duplicates(subset=[\"image_path\"]).reset_index(drop=True)\n    print(f\"✓ 找到 {len(test_unique)} 张独立测试图像\")\n    print(f\"  总测试样本数: {len(test_long)}\")\n\n    # 运行推理\n    final_pred = run_inference(test_unique, INF_CFG.TEST_IMAGE_DIR)\n\n    # 生成宽格式提交文件\n    submission = create_submission(final_pred, test_long, test_unique)\n\n    print(\"\\n\" + \"=\" * 80)\n    print(\"推理完成！\")\n    print(\"=\" * 80)\n\n    # 清理\n    gc.collect()\n    if torch.cuda.is_available():\n        torch.cuda.empty_cache()\n\n\nif __name__ == \"__main__\":\n    main()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-05T01:24:28.718974Z","iopub.execute_input":"2026-01-05T01:24:28.719736Z","iopub.status.idle":"2026-01-05T01:26:05.326542Z","shell.execute_reply.started":"2026-01-05T01:24:28.719714Z","shell.execute_reply":"2026-01-05T01:26:05.325621Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# import os\n# import cv2\n# import torch\n# import timm\n# import numpy as np\n# import pandas as pd\n# from tqdm import tqdm\n# import torch.nn as nn\n# import albumentations as A\n# from albumentations.pytorch import ToTensorV2\n\n# # 定义设备（补充缺失的device定义，确保代码可运行）\n# device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n\n# class Config:\n#     # Data\n#     data_dir = '/kaggle/input/csiro-biomass/test'\n\n#     # Model\n#     model_path = '/kaggle/input/d/seddiktrk/eva02-biomass-regression/best_model_fold_4.pth'\n\n#     # TTA\n#     img_size = 448\n\n#     # Inference\n#     smooth_factor = 0.9050\n#     dry_clover_min = 1.25\n#     dry_dead_minimum = 1.00\n    \n# config = Config()\n\n# class BiomassModel(nn.Module):\n        \n#     def regression_head(self, name, in_features: int, dropout: float):\n#         self.name = name\n#         return nn.Sequential(\n#             nn.Linear(in_features, in_features // 2),\n#             nn.ReLU(),\n#             nn.Dropout(dropout),\n#             nn.Linear(in_features // 2, 1)\n#         )\n    \n#     def __init__(self, model_name, dropout = 0.0):\n#         super().__init__()\n                \n#         # Shared backbone\n#         self.backbone = timm.create_model(\n#             model_name,\n#             pretrained=False,  \n#             num_classes=0, \n#             global_pool='avg'  \n#         )\n#         n_features = self.backbone.num_features\n#         n_features *= 2\n        \n#         self.head_total = self.regression_head('dry_total', n_features, dropout)\n#         self.head_gdm = self.regression_head('dry_gdm', n_features, dropout)\n#         self.head_green = self.regression_head('dry_green', n_features, dropout)\n\n    \n#     def forward(self, img_left, img_right):\n#         # Extract image features\n#         fl = self.backbone(img_left)\n#         fr = self.backbone(img_right)\n#         img_feat = torch.cat([fl, fr], dim=1)\n        \n#         dry_total = self.head_total(img_feat)\n#         gdm = self.head_gdm(img_feat)\n#         dry_green = self.head_green(img_feat)\n        \n#         return dry_total, gdm, dry_green\n\n# ckpt = torch.load(config.model_path, map_location = device)\n\n# model_name = ckpt['model_name']\n# val_score = ckpt['val_r2_score']\n# state_dict = ckpt['model_state_dict']\n\n# # Create Model + Load Weights\n# eva_model = BiomassModel(model_name = model_name)\n# eva_model.load_state_dict(state_dict)\n# eva_model.eval()\n# eva_model.to(device)\n\n# num_params = sum(p.numel() for p in eva_model.parameters())\n# print(f\"✅ Loaded model with r² val_score: {val_score:.5f} and N° of params: {num_params}\")\n\n# def get_tta_batch(image_path, tta_transforms):\n#     image = cv2.imread(image_path)\n#     image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)\n    \n#     height, width, _ = image.shape\n#     mid_point = width // 2\n#     iml_np, imr_np = image[:, :mid_point], image[:, mid_point:]\n        \n#     iml, imr = [], []\n#     for transform in tta_transforms:\n#         iml.append(transform(image=iml_np)['image'])\n#         imr.append(transform(image=imr_np)['image'])\n\n#     iml = torch.stack(iml, dim=0).to(device) # (6, 3 ,img_size, img_size)\n#     imr = torch.stack(imr, dim=0).to(device)\n#     return iml, imr\n\n# def get_tta_transformations(img_size = 448):\n#     transform = [\n#         A.Resize(img_size, img_size),\n#         A.Normalize(mean=[0.485, 0.456, 0.406], \n#                     std=[0.229, 0.224, 0.225]),\n#         ToTensorV2()\n#     ]\n    \n#     tta_transforms = [\n#         A.Compose([*transform]),\n#         A.Compose([A.HorizontalFlip(p=1.0), *transform]),\n#         A.Compose([A.VerticalFlip(p=1.0), *transform]),\n#         A.Compose([A.Rotate(90, p=1.0), *transform]),\n#         A.Compose([A.Rotate(180, p=1.0), *transform]),\n#         A.Compose([A.Rotate(270, p=1.0), *transform]),\n#     ]\n#     return tta_transforms\n\n# image_paths = [os.path.join(config.data_dir, f) for f in os.listdir(config.data_dir)]\n# tta_transforms = get_tta_transformations(img_size = config.img_size)\n\n# # ======================== 核心修改部分开始 ========================\n# # 改为字典存储，方便构建宽格式DataFrame\n# submission_dict = {}\n\n# for image_path in tqdm(image_paths):\n#     iml, imr = get_tta_batch(image_path, tta_transforms)\n    \n#     with torch.inference_mode():\n#         dry_total, gdm, dry_green = eva_model(iml, imr)\n\n#     preds = torch.cat([dry_total, gdm, dry_green], dim=1).cpu().numpy() # (num_transforms , 3)\n#     preds = np.maximum(preds, 0)\n#     preds = preds.mean(axis=0)\n    \n#     dry_total, gdm, dry_green = preds  \n#     dry_clover = gdm - dry_green\n#     dry_dead = dry_total - gdm\n\n#     dry_clover = np.clip(gdm - dry_green, 0, config.smooth_factor * gdm)\n#     dry_dead = np.clip(dry_total - gdm, 0, config.smooth_factor * dry_total)\n    \n#     # If clover is very small (< 1.25g), set to 0\n#     dry_clover = np.where(dry_clover < config.dry_clover_min, 0, dry_clover)\n#     # If dead is very small (< 1g), set to 0\n#     dry_dead = np.where(dry_dead < config.dry_dead_minimum, 0, dry_dead)\n\n#     # 获取base_id（去掉后缀的图片ID）\n#     base_id = os.path.splitext(os.path.basename(image_path))[0]\n#     # 存储当前图片的所有预测结果\n#     submission_dict[base_id] = {\n#         'Dry_Clover_g': dry_clover,\n#         'Dry_Dead_g': dry_dead,\n#         'Dry_Green_g': dry_green,\n#         'Dry_Total_g': dry_total,\n#         'GDM_g': gdm\n#     }\n\n# # 转换为宽格式DataFrame（适配集成代码）\n# submission = pd.DataFrame.from_dict(submission_dict, orient='index').reset_index()\n# submission.rename(columns={'index': 'base_id'}, inplace=True)\n\n# # 保存CSV（格式与集成代码要求一致）\n# submission.to_csv('preds_eva02_wide.csv', index=False)\n# display(submission)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-05T01:26:05.327566Z","iopub.execute_input":"2026-01-05T01:26:05.32791Z","iopub.status.idle":"2026-01-05T01:26:05.334429Z","shell.execute_reply.started":"2026-01-05T01:26:05.327889Z","shell.execute_reply":"2026-01-05T01:26:05.333623Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport cv2\nimport timm\nimport torch\nimport numpy as np\nimport pandas as pd\nimport torch.nn as nn\nimport albumentations as A\nimport torch.nn.functional as F\n\nfrom tqdm import tqdm\nfrom albumentations.pytorch import ToTensorV2\nfrom torch.utils.data import Dataset, DataLoader\n\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nbatch_size = 4\ndropout = 0.30\nhidden_ratio = 0.25\ngrid = (2, 2)\n\ntest_csv = \"/kaggle/input/csiro-biomass/test.csv\"\ntest_img_dir = \"/kaggle/input/csiro-biomass/test\"\n\nmodel_dir = \"/kaggle/input/csiro-mvp-models\"\nmodel_paths = [os.path.join(model_dir, f\"model{i}.pth\") for i in range(1, 11)]\nexisting_models = [path for path in model_paths if os.path.exists(path)]\n\nCKPTS_A = existing_models[:5]\nCKPTS_B = existing_models[5:]\nW_A, W_B = 0.95, 0.075\n\ntargets = [\"Dry_Green_g\", \"Dry_Dead_g\", \"Dry_Clover_g\", \"GDM_g\", \"Dry_Total_g\"]\n# 集成代码要求的列顺序（关键）\nensemble_targets = ['Dry_Total_g', 'GDM_g', 'Dry_Green_g', 'Dry_Dead_g', 'Dry_Clover_g']\n\nbackbones = [\n    \"vit_base_patch14_reg4_dinov2\",\n    \"vit_base_patch14_reg4_dinov3\",\n    \"vit_base_patch14_dinov3\",\n]\n\nclass BiomassDataset(Dataset):\n    def __init__(self, df, transform, img_dir):\n        self.df = df.reset_index(drop=True)\n        self.transform = transform\n        self.img_dir = img_dir\n        self.paths = self.df[\"image_path\"].values\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        filename = os.path.basename(self.paths[idx])\n        full_path = os.path.join(self.img_dir, filename)\n        img = cv2.imread(full_path)\n        \n        if img is None:\n            img = np.zeros((1000, 2000, 3), dtype=np.uint8)\n        else:\n            img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n\n        h, w, _ = img.shape\n        mid = w // 2\n        left_img = img[:, :mid]\n        right_img = img[:, mid:]\n\n        left_tensor = self.transform(image=left_img)[\"image\"]\n        right_tensor = self.transform(image=right_img)[\"image\"]\n        \n        return left_tensor, right_tensor\n\ndef get_input_size(model):\n    if hasattr(model, \"patch_embed\") and hasattr(model.patch_embed, \"img_size\"):\n        size = model.patch_embed.img_size\n        return int(size if isinstance(size, (int, float)) else size[0])\n    \n    if hasattr(model, \"img_size\"):\n        size = model.img_size\n        return int(size if isinstance(size, (int, float)) else size[0])\n    \n    cfg = getattr(model, \"default_cfg\", {}) or {}\n    input_size = cfg.get(\"input_size\", None)\n    \n    if input_size:\n        if isinstance(input_size, (tuple, list)) and len(input_size) >= 2:\n            return int(input_size[1])\n        return int(input_size if isinstance(input_size, (int, float)) else 224)\n    \n    arch = cfg.get(\"architecture\", \"\") or str(type(model))\n    return 518 if \"dinov2\" in arch.lower() or \"dinov3\" in arch.lower() else 224\n\ndef build_backbone(name):\n    model = timm.create_model(name, pretrained=False, num_classes=0)\n    features = model.num_features\n    input_size = get_input_size(model)\n    return model, features, input_size\n\nclass BaseDINO(nn.Module):\n    def __init__(self, backbone_name):\n        super().__init__()\n        self.backbone, feat_dim, input_size = build_backbone(backbone_name)\n        self.input_size = int(input_size)\n        self.feat_dim = feat_dim\n        self.combined_dim = feat_dim * 2\n        hidden_size = max(8, int(self.combined_dim * hidden_ratio))\n\n        def make_head():\n            return nn.Sequential(\n                nn.Linear(self.combined_dim, hidden_size),\n                nn.ReLU(inplace=True),\n                nn.Dropout(dropout),\n                nn.Linear(hidden_size, 1),\n            )\n\n        self.head_green = make_head()\n        self.head_clover = make_head()\n        self.head_dead = make_head()\n        self.softplus = nn.Softplus(beta=1.0)\n\n    def merge_features(self, left_feat, right_feat):\n        combined = torch.cat([left_feat, right_feat], dim=1)\n        green = self.softplus(self.head_green(combined))\n        clover = self.softplus(self.head_clover(combined))\n        dead = self.softplus(self.head_dead(combined))\n        gdm = green + clover\n        total = gdm + dead\n        return total, gdm, green\n\nclass PlainDINO(BaseDINO):\n    def forward(self, left_img, right_img):\n        left_feat = self.backbone(left_img)\n        right_feat = self.backbone(right_img)\n        return self.merge_features(left_feat, right_feat)\n\ndef split_dimension(length, parts):\n    step = length // parts\n    segments = []\n    start = 0\n    \n    for _ in range(parts - 1):\n        segments.append((start, start + step))\n        start += step\n    \n    segments.append((start, length))\n    return segments\n\nclass TiledDINO(BaseDINO):\n    def __init__(self, backbone_name):\n        super().__init__(backbone_name)\n        self.grid = grid\n\n    def process_tiles(self, x):\n        B, C, H, W = x.shape\n        rows, cols = self.grid\n        row_segments = split_dimension(H, rows)\n        col_segments = split_dimension(W, cols)\n        features = []\n        \n        for (rs, re) in row_segments:\n            for (cs, ce) in col_segments:\n                tile = x[:, :, rs:re, cs:ce]\n                if tile.shape[-2:] != (self.input_size, self.input_size):\n                    tile = F.interpolate(tile, size=(self.input_size, self.input_size), mode=\"bilinear\")\n                feat = self.backbone(tile)\n                features.append(feat)\n        \n        features = torch.stack(features, dim=0).permute(1, 0, 2)\n        return features.mean(dim=1)\n\n    def forward(self, left_img, right_img):\n        left_feat = self.process_tiles(left_img)\n        right_feat = self.process_tiles(right_img)\n        return self.merge_features(left_feat, right_feat)\n\nclass FiLM(nn.Module):\n    def __init__(self, feat_dim):\n        super().__init__()\n        hidden = max(32, feat_dim // 2)\n        self.mlp = nn.Sequential(\n            nn.Linear(feat_dim, hidden), nn.ReLU(inplace=True), nn.Linear(hidden, feat_dim * 2)\n        )\n\n    def forward(self, context):\n        gamma_beta = self.mlp(context)\n        return torch.chunk(gamma_beta, 2, dim=1)\n\nclass TiledFiLMDINO(BaseDINO):\n    def __init__(self, backbone_name):\n        super().__init__(backbone_name)\n        self.grid = grid\n        self.film_left = FiLM(self.feat_dim)\n        self.film_right = FiLM(self.feat_dim)\n\n    def extract_tile_features(self, x):\n        B, C, H, W = x.shape\n        rows, cols = self.grid\n        row_segments = split_dimension(H, rows)\n        col_segments = split_dimension(W, cols)\n        features = []\n        \n        for (rs, re) in row_segments:\n            for (cs, ce) in col_segments:\n                tile = x[:, :, rs:re, cs:ce]\n                if tile.shape[-2:] != (self.input_size, self.input_size):\n                    tile = F.interpolate(tile, size=(self.input_size, self.input_size), mode=\"bilinear\")\n                feat = self.backbone(tile)\n                features.append(feat)\n        \n        return torch.stack(features, dim=0).permute(1, 0, 2)\n\n    def process_stream(self, x, film_layer):\n        tiles = self.extract_tile_features(x)\n        context = tiles.mean(dim=1)\n        gamma, beta = film_layer(context)\n        modulated = tiles * (1 + gamma.unsqueeze(1)) + beta.unsqueeze(1)\n        return modulated.mean(dim=1)\n\n    def forward(self, left_img, right_img):\n        left_feat = self.process_stream(left_img, self.film_left)\n        right_feat = self.process_stream(right_img, self.film_right)\n        return self.merge_features(left_feat, right_feat)\n\ndef clean_state_dict(state_dict):\n    if not state_dict:\n        return state_dict\n    \n    cleaned_dict = {}\n    \n    for k, v in state_dict.items():\n        if k.startswith(\"module.\"):\n            k = k[7:]\n        \n        if k.startswith(\"student.\"):\n            k = k[8:]\n        \n        skip_prefixes = (\"txt_enc.\", \"img_proj.\", \"txt_film\", \"teacher.\", \"momentum_teacher.\")\n        if any(k.startswith(prefix) for prefix in skip_prefixes):\n            continue\n            \n        cleaned_dict[k] = v\n    \n    return cleaned_dict\n\ndef load_model(checkpoint_path):\n    print(f\"Loading model: {checkpoint_path}\")\n    \n    if not os.path.exists(checkpoint_path):\n        raise FileNotFoundError(f\"Checkpoint not found: {checkpoint_path}\")\n    \n    try:\n        raw_state = torch.load(checkpoint_path, map_location=device, weights_only=False)\n    except Exception as e:\n        print(f\"Error loading {checkpoint_path}: {e}\")\n        return None\n    \n    if isinstance(raw_state, dict):\n        if 'state_dict' in raw_state:\n            state_dict = raw_state['state_dict']\n        elif 'model' in raw_state:\n            state_dict = raw_state['model']\n        else:\n            state_dict = raw_state\n    \n    state_dict = clean_state_dict(state_dict)\n    \n    if not state_dict:\n        print(f\"Empty state_dict for {checkpoint_path}\")\n        return None\n    \n    for backbone in backbones:\n        try:\n            model = TiledFiLMDINO(backbone)\n            result = model.load_state_dict(state_dict, strict=False)\n            \n            missing = [k for k in result.missing_keys if not k.startswith('backbone.pos_embed')]\n            \n            if len(missing) == 0:\n                print(f\"Loaded with backbone: {backbone}\")\n                model.to(device)\n                model.eval()\n                return model\n                \n        except Exception as e:\n            continue\n    \n    return None\n\ndef get_tta_transforms(img_size):\n    norm = [A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), ToTensorV2()]\n    return [\n        A.Compose([A.Resize(img_size, img_size), *norm]),\n        A.Compose([A.HorizontalFlip(p=1.0), A.Resize(img_size, img_size), *norm]),\n        A.Compose([A.VerticalFlip(p=1.0), A.Resize(img_size, img_size), *norm]),\n        A.Compose([A.RandomRotate90(p=1.0), A.Resize(img_size, img_size), *norm]),\n    ]\n\n@torch.no_grad()\ndef predict_one_view(models, loader):\n    preds = []\n    use_amp = device.type == \"cuda\"\n    \n    for left_imgs, right_imgs in tqdm(loader, desc=\"Infer\", leave=False):\n        left_imgs = left_imgs.to(device, non_blocking=True)\n        right_imgs = right_imgs.to(device, non_blocking=True)\n        batch_preds = []\n        \n        with torch.amp.autocast(\"cuda\", enabled=use_amp):\n            for model in models:\n                total, gdm, green = model(left_imgs, right_imgs)\n                dead = torch.clamp(total - gdm, min=0.0)\n                clover = torch.clamp(gdm - green, min=0.0)\n                pred = torch.cat([green, dead, clover, gdm, total], dim=1)\n                batch_preds.append(pred.clamp(0.05, 400.0).cpu())\n        \n        preds.append(torch.stack(batch_preds).mean(dim=0).numpy())\n    \n    return np.concatenate(preds)\n\ndef run_inference_for_ckpts(checkpoint_paths, df, img_dir):\n    models = []\n    \n    for ckpt_path in checkpoint_paths:\n        model = load_model(ckpt_path)\n        if model is not None:\n            models.append(model)\n    \n    if not models:\n        raise ValueError(f\"No models loaded from {checkpoint_paths}\")\n    \n    input_size = models[0].input_size\n    \n    tta_preds = []\n    for transform in get_tta_transforms(input_size):\n        ds = BiomassDataset(df, transform, img_dir)\n        dl = DataLoader(ds, batch_size=batch_size, shuffle=False, num_workers=0, pin_memory=True)\n        tta_preds.append(predict_one_view(models, dl))\n    \n    return np.mean(tta_preds, axis=0)\n\n# ======================== 核心修改：生成集成所需的wide格式文件 ========================\ndef generate_ensemble_wide_file(final_preds, unique_df, output_filename=\"preds_dino3_wide.csv\"):\n    \"\"\"\n    生成集成代码所需的wide格式CSV文件\n    :param final_preds: 模型预测结果 (n_samples, 5)\n    :param unique_df: 去重后的test_df (按image_path)\n    :param output_filename: 输出文件名（适配集成代码的命名）\n    \"\"\"\n    # 1. 构建wide格式DataFrame\n    wide_df = pd.DataFrame({\n        \"base_id\": unique_df[\"image_path\"].apply(lambda x: os.path.splitext(os.path.basename(x))[0]).astype(str),  # base_id使用image_path作为唯一标识\n        \"Dry_Green_g\": np.clip(final_preds[:, 0], 0.05, 400.0),\n        \"Dry_Dead_g\": np.clip(final_preds[:, 1], 0.05, 400.0),\n        \"Dry_Clover_g\": np.clip(final_preds[:, 2], 0.05, 400.0),\n        \"GDM_g\": np.clip(final_preds[:, 3], 0.05, 400.0),\n        \"Dry_Total_g\": np.clip(final_preds[:, 4], 0.05, 400.0),\n    })\n    \n    # 2. 按集成代码要求的列顺序重新排列\n    wide_df = wide_df[[\"base_id\"] + ensemble_targets]\n    \n    # 3. 去重并保存（防止重复的image_path）\n    wide_df = wide_df.drop_duplicates(subset=[\"base_id\"]).reset_index(drop=True)\n    wide_df.to_csv(output_filename, index=False)\n    \n    print(f\"✅ 生成集成适配的wide格式文件: {output_filename}\")\n    print(f\"📊 文件形状: {wide_df.shape}, 预览前5行:\")\n    print(wide_df.head())\n    \n    return wide_df\n\ndef main():\n    test_df = pd.read_csv(test_csv)\n    unique_df = test_df.drop_duplicates(\"image_path\").reset_index(drop=True)\n    \n    # 原有推理逻辑完全保留\n    pred_a = run_inference_for_ckpts(CKPTS_A, unique_df, test_img_dir)\n    pred_b = run_inference_for_ckpts(CKPTS_B, unique_df, test_img_dir)\n    final_preds = W_A * pred_a + W_B * pred_b\n    \n    # 替换原有create_submission，生成集成适配的wide格式文件\n    generate_ensemble_wide_file(final_preds, unique_df, \"preds_dino3_wide.csv\")\n\nif __name__ == \"__main__\":\n    main()\n    print(pd.read_csv(\"preds_dino3_wide.csv\").iloc[0].to_dict())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-05T01:26:05.335584Z","iopub.execute_input":"2026-01-05T01:26:05.335925Z","iopub.status.idle":"2026-01-05T01:27:17.166345Z","shell.execute_reply.started":"2026-01-05T01:26:05.335899Z","shell.execute_reply":"2026-01-05T01:27:17.165592Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def flush():\n    import gc\n    gc.collect()\n    if torch.cuda.is_available():\n        torch.cuda.empty_cache()\n        torch.cuda.reset_peak_memory_stats()\n\nseed_everything(42)\n\n@dataclass\nclass Config:\n    # Data paths\n    DATA_PATH: Path = Path(\"/kaggle/input/csiro-biomass/\")\n    TRAIN_DATA_PATH: Path = DATA_PATH/'train'\n    TEST_DATA_PATH: Path = DATA_PATH/'test'\n\n    device = \"cuda\" if torch.cuda.is_available() else \"cpu\"\n    seed = 42\n\ncfg = Config()\nseed_everything(cfg.seed)\n\ndef pivot_table(df: pd.DataFrame)->pd.DataFrame:\n\n    if 'target' in df.columns.tolist():\n        df_pt = pd.pivot_table(\n            df, \n            values='target', \n            index=['image_path', 'Sampling_Date', 'State', 'Species', 'Pre_GSHH_NDVI', 'Height_Ave_cm'], \n            columns='target_name', \n            aggfunc='mean'\n        ).reset_index()\n    else:\n        df['target'] = 0\n        df_pt = pd.pivot_table(\n            df, \n            values='target', \n            index='image_path', \n            columns='target_name', \n            aggfunc='mean'\n        ).reset_index()\n    return df_pt\n\n# train_df = pd.read_csv(cfg.DATA_PATH/'train.csv')\ntest_df = pd.read_csv(cfg.DATA_PATH/'test.csv')\n# train_df = pivot_table(df=train_df)\ntest_df = pivot_table(df=test_df)\n\n# train_df['image_path'] = train_df['image_path'].apply(lambda p: str(cfg.DATA_PATH / p))\ntest_df['image_path'] = test_df['image_path'].apply(lambda p: str(cfg.DATA_PATH / p))\n# test_df.head()\n\ntrain_df = pd.read_csv(\"/kaggle/input/csiro-datasplit/csiro_data_split.csv\")\n\ndef melt_table(df: pd.DataFrame) -> pd.DataFrame:\n    TARGET_NAMES = ['Dry_Clover_g', 'Dry_Dead_g', 'Dry_Green_g', 'Dry_Total_g', 'GDM_g']\n    melted = df.melt(\n        id_vars='image_path',\n        value_vars=TARGET_NAMES,\n        var_name='target_name',\n        value_name='target'\n    )\n    melted['sample_id'] = (\n        melted['image_path']\n        .str.replace(r'^.*/', '', regex=True)  # remove folder path, keep filename\n        .str.replace('.jpg', '', regex=False)  # remove extension\n        + '__' + melted['target_name']\n    )\n    \n    return melted[['sample_id', 'image_path', 'target_name', 'target']]\n\n# t1 = melt_table(test_df)\n# t1\ndef split_image(image, patch_size=520, overlap=16):\n    h, w, c = image.shape\n    stride = patch_size - overlap\n    \n    patches = []\n    coords  = []   # (y1, x1, y2, x2)\n    \n    for y in range(0, h, stride):\n        for x in range(0, w, stride):\n            y1 = y\n            x1 = x\n            y2 = y + patch_size\n            x2 = x + patch_size\n            \n            # Pad last patch if needed (very rare with your fixed 1000×2000)\n            patch = image[y1:y2, x1:x2, :]\n            if patch.shape[0] < patch_size or patch.shape[1] < patch_size:\n                pad_h = patch_size - patch.shape[0]\n                pad_w = patch_size - patch.shape[1]\n                patch = np.pad(patch, ((0,pad_h), (0,pad_w), (0,0)), mode='reflect')\n            \n            patches.append(patch)\n            coords.append((y1, x1, y2, x2))\n    \n    return patches, coords\n    \ndef get_model(model_path: str, device: str = 'cpu'):\n    model = AutoModel.from_pretrained(\n        model_path,\n        local_files_only=True\n    )\n    processor = AutoImageProcessor.from_pretrained(model_path)\n    return model.eval().to(device), processor\n\ndino_path = \"/kaggle/input/dinov2/pytorch/giant/1\"\nsiglip_path = \"/kaggle/input/google-siglip-so400m-patch14-384/transformers/default/1\"\n\ndef compute_embeddings(model_path, df, patch_size=520):\n    device = 'cuda' if torch.cuda.is_available() else 'cpu'\n    print(device)\n\n    model, processor = get_model(\n        model_path=model_path, device=device\n    )\n\n    IMAGE_PATHS = []\n    EMBEDDINGS = []\n\n    for i, row in tqdm(df.iterrows(), total=len(df)):\n        img_path = row['image_path']\n        img = cv2.imread(img_path, cv2.IMREAD_COLOR)\n        patches, coords = split_image(img, patch_size=patch_size)\n        images = [Image.fromarray(p).convert(\"RGB\") for p in patches]\n\n        inputs = processor(images=images, return_tensors=\"pt\").to(model.device)\n        with torch.no_grad():\n            if 'siglip' in model_path:\n                features = model.get_image_features(**inputs)\n            elif 'dino' in model_path:\n                features = model(**inputs).pooler_output\n                # patches = model(**inputs).last_hidden_state\n                # features = patches[:, 0, :]\n            else:\n                raise Exception(\"Model should be dino or siglip\")\n        embeds = features.mean(dim=0).detach().cpu().numpy()\n        EMBEDDINGS.append(embeds)\n        IMAGE_PATHS.append(img_path)\n\n    embeddings = np.stack(EMBEDDINGS, axis=0)\n    n_features = embeddings.shape[1]\n    emb_columns = [f\"emb{i+1}\" for i in range(n_features)]\n    emb_df = pd.DataFrame(embeddings, columns=emb_columns)\n    emb_df['image_path'] = IMAGE_PATHS\n    df_final = df.merge(emb_df, on='image_path', how='left')\n    flush()\n    return df_final \n\n# train_siglip_df = compute_embeddings(model_path=siglip_path, df=train_df, patch_size=520)\ntrain_siglip_df = train_df.copy()\ntest_siglip_df = compute_embeddings(model_path=siglip_path, df=test_df, patch_size=520)\n\nflush()\n\ndef generate_semantic_features(image_embeddings, model_path=siglip_path):\n    \"\"\"\n    Generates 'Concept Scores' by averaging synonyms and calculating biological ratios.\n    \"\"\"\n    print(f\"Loading SigLIP Text Encoder from {model_path}...\")\n    device = 'cuda' if torch.cuda.is_available() else 'cpu'\n    \n    try:\n        model = AutoModel.from_pretrained(model_path).to(device)\n        tokenizer = AutoTokenizer.from_pretrained(model_path)\n    except Exception as e:\n        print(f\"Error loading model: {e}\")\n        return None\n\n    # 1. Define Concept Ensembles (Grouping synonyms reduces noise)\n    concept_groups = {\n        # Quantity Anchors\n        \"bare\": [\"bare soil\", \"dirt ground\", \"sparse vegetation\", \"exposed earth\"],\n        \"sparse\": [\"low density pasture\", \"thin grass\", \"short clipped grass\"],\n        \"medium\": [\"average pasture cover\", \"medium height grass\", \"grazed pasture\"],\n        \"dense\": [\"dense tall pasture\", \"thick grassy volume\", \"high biomass\", \"overgrown vegetation\"],\n        \n        # State Anchors\n        \"green\": [\"lush green vibrant pasture\", \"photosynthesizing leaves\", \"fresh growth\"],\n        \"dead\": [\"dry brown dead grass\", \"yellow straw\", \"senesced material\", \"standing hay\"],\n        \n        # Species Anchors\n        \"clover\": [\"white clover\", \"trifolium repens\", \"broadleaf legume\", \"clover flowers\"],\n        \"grass\": [\"ryegrass\", \"blade-like leaves\", \"fescue\", \"grassy sward\"],\n        \"weeds\": [\"broadleaf weeds\", \"thistles\", \"non-pasture vegetation\"]\n    }\n    \n    # 2. Encode and Average Prompts for each Concept\n    concept_vectors = {}\n    with torch.no_grad():\n        for name, prompts in concept_groups.items():\n            inputs = tokenizer(prompts, padding=\"max_length\", return_tensors=\"pt\").to(device)\n            emb = model.get_text_features(**inputs)\n            emb = emb / emb.norm(p=2, dim=-1, keepdim=True)\n            # Average the embeddings of synonyms to get a stable \"Concept Vector\"\n            concept_vectors[name] = emb.mean(dim=0, keepdim=True)\n\n    # 3. Compute Concept Scores\n    if isinstance(image_embeddings, np.ndarray):\n        img_tensor = torch.tensor(image_embeddings, dtype=torch.float32).to(device)\n    else:\n        img_tensor = image_embeddings.to(device)\n    img_tensor = img_tensor / img_tensor.norm(p=2, dim=-1, keepdim=True)\n\n    scores = {}\n    for name, vec in concept_vectors.items():\n        # Dot product\n        scores[name] = torch.matmul(img_tensor, vec.T).cpu().numpy().flatten()\n    \n    # 4. Feature Engineering: Explicit Ratios\n    # These help models distinguish between \"High Biomass Dead\" vs \"High Biomass Green\"\n    \n    # Convert dict to DataFrame for easy math\n    df_scores = pd.DataFrame(scores)\n    \n    # A. Greenness Ratio: Green / (Green + Dead)\n    df_scores['ratio_greenness'] = df_scores['green'] / (df_scores['green'] + df_scores['dead'] + 1e-6)\n    \n    # B. Legume Fraction: Clover / (Clover + Grass)\n    df_scores['ratio_clover'] = df_scores['clover'] / (df_scores['clover'] + df_scores['grass'] + 1e-6)\n    \n    # C. Vegetation Cover: (Dense + Medium) / (Bare + Sparse)\n    df_scores['ratio_cover'] = (df_scores['dense'] + df_scores['medium']) / (df_scores['bare'] + df_scores['sparse'] + 1e-6)\n    \n    # D. \"Volume\": Max of density anchors\n    df_scores['max_density'] = df_scores[['bare', 'sparse', 'medium', 'dense']].max(axis=1)\n\n    print(f\"Generated {df_scores.shape[1]} semantic features (Ensembles + Ratios).\")\n    return df_scores.values\n\nimport numpy as np\nfrom sklearn.base import BaseEstimator, TransformerMixin\nfrom sklearn.preprocessing import StandardScaler\nfrom sklearn.decomposition import PCA\nfrom sklearn.cross_decomposition import PLSRegression\nfrom sklearn.mixture import GaussianMixture\nfrom sklearn.linear_model import BayesianRidge\nfrom sklearn.metrics.pairwise import cosine_similarity\n\n\nclass SupervisedEmbeddingEngine(BaseEstimator, TransformerMixin):\n    def __init__(self, \n                 n_pca=0.98,  # Slightly higher to keep texture details\n                 n_pls=8,     # Keep 8, it worked well\n                 n_gmm=5,     \n                 random_state=42):\n        \n        self.n_pca = n_pca\n        self.n_pls = n_pls\n        self.n_gmm = n_gmm\n        self.random_state = random_state\n        \n        self.scaler = StandardScaler()\n        self.pca = PCA(n_components=n_pca, random_state=random_state)\n        self.pls = PLSRegression(n_components=n_pls, scale=False)\n        self.gmm = GaussianMixture(n_components=n_gmm, covariance_type='diag', random_state=random_state)\n\n        self.pls_fitted_ = False\n\n    def fit(self, X, y=None, X_semantic=None):\n        # 1. Standard Scaling on IMAGE embeddings only\n        X_scaled = self.scaler.fit_transform(X)\n        \n        # 2. Fit Unsupervised on IMAGES\n        self.pca.fit(X_scaled)\n        self.gmm.fit(X_scaled)\n        \n        # 3. Fit PLS on IMAGES (Supervised)\n        if y is not None:\n            y_clean = y.values if hasattr(y, 'values') else y\n            self.pls.fit(X_scaled, y_clean)\n            self.pls_fitted_ = True\n        \n        return self\n\n    def transform(self, X, X_semantic=None):\n        X_scaled = self.scaler.transform(X)\n        return self._generate_features(X_scaled, X_semantic)\n\n    def _generate_features(self, X_scaled, X_semantic=None):\n        features = []\n        \n        # A. PCA (Texture/Structure from Images)\n        f_pca = self.pca.transform(X_scaled)\n        features.append(f_pca)\n        \n        # B. PLS (Biomass-correlated signals from Images)\n        if self.pls_fitted_:\n            f_pls = self.pls.transform(X_scaled)\n            features.append(f_pls)\n        \n        # C. GMM (Cluster probs)\n        f_gmm = self.gmm.predict_proba(X_scaled)\n        features.append(f_gmm)\n        \n        # D. Semantic Features (LATE FUSION)\n        # We append them raw. They are already high-level signals.\n        if X_semantic is not None:\n            # Normalize semantic scores relative to themselves to match scale of PCA/PLS\n            sem_norm = (X_semantic - np.mean(X_semantic, axis=0)) / (np.std(X_semantic, axis=0) + 1e-6)\n            features.append(sem_norm)\n\n        return np.hstack(features)\n\n\nCOLUMNS = train_df.filter(like=\"emb\").columns\nTARGET_NAMES = ['Dry_Clover_g', 'Dry_Dead_g', 'Dry_Green_g', 'Dry_Total_g', 'GDM_g']\nweights = {\n    'Dry_Green_g': 0.1,\n    'Dry_Dead_g': 0.1,\n    'Dry_Clover_g': 0.1,\n    'GDM_g': 0.2,\n    'Dry_Total_g': 0.5,\n}\n\nTARGET_MAX = {\n    \"Dry_Clover_g\": 71.7865,\n    \"Dry_Dead_g\": 83.8407,\n    \"Dry_Green_g\": 157.9836,\n    \"Dry_Total_g\": 185.70,\n    \"GDM_g\": 157.9836,\n}\n\ndef competition_metric(y_true, y_pred) -> float:\n    y_weighted = 0\n    for l, label in enumerate(TARGET_NAMES):\n        y_weighted = y_weighted + y_true[:, l].mean() * weights[label]\n\n    ss_res = 0\n    ss_tot = 0\n    for l, label in enumerate(TARGET_NAMES):\n        ss_res = ss_res + ((y_true[:, l] - y_pred[:, l])**2).mean() * weights[label]\n        ss_tot = ss_tot + ((y_true[:, l] - y_weighted)**2).mean() * weights[label]\n\n    return 1 - ss_res / ss_tot\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 = [\n        \"Dry_Green_g\", \n        \"Dry_Clover_g\", \n        \"Dry_Dead_g\", \n        \"GDM_g\", \n        \"Dry_Total_g\"\n    ]\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    C = np.array([\n        [1, 1, 0, -1,  0],\n        [0, 0, 1,  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    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 compare_results(oof, train_data):\n    y_oof_df = pd.DataFrame(oof, columns=TARGET_NAMES) # ensure columns match\n    # 2. Check Score BEFORE Processing\n    raw_score = competition_metric(train_data[TARGET_NAMES].values, y_oof_df.values)\n    print(f\"Raw CV Score: {raw_score:.6f}\")\n    \n    # 3. Apply Post-Processing\n    y_oof_proc = post_process_biomass(y_oof_df)\n    \n    # 4. Check Score AFTER Processing\n    proc_score = competition_metric(train_data[TARGET_NAMES].values, y_oof_proc.values)\n    print(f\"Processed CV Score: {proc_score:.6f}\")\n    \n    print(f\"Improvement: {raw_score - proc_score:.6f}\")\n\nX_all_emb = np.vstack([\n    train_siglip_df[COLUMNS].values, \n    test_siglip_df[COLUMNS].values\n])\n\n# Generate Semantic Probes (Using the function defined in Part 1)\n# Make sure SIGLIP_PATH is correct for your environment\nprint(\"Generating Semantic Features via SigLIP Text Encoder...\")\ntry:\n    all_semantic_scores = generate_semantic_features(X_all_emb, model_path=siglip_path)\n    \n    # Split back into Train and Test\n    n_train = len(train_siglip_df)\n    sem_train_full = all_semantic_scores[:n_train]\n    sem_test_full = all_semantic_scores[n_train:]\n    print(f\"Semantic Features Generated. Train: {sem_train_full.shape}, Test: {sem_test_full.shape}\")\n    \nexcept Exception as e:\n    print(f\"Skipping Semantic Features due to error: {e}\")\n    # Fallback to None if model path is wrong or memory fails\n    sem_train_full = None\n    sem_test_full = None\n\n# --- STEP 2: Updated Cross-Validation Function ---\ndef cross_validate(model, train_data, test_data, feature_engine, \n                   semantic_train=None, semantic_test=None, # <--- NEW ARGS\n                   target_transform='max', seed=42):\n\n    n_splits = train_data['fold'].nunique()\n    # Setup Targets\n    target_max_arr = np.array([TARGET_MAX[t] for t in TARGET_NAMES], dtype=float)\n    y_true = train_data[TARGET_NAMES]\n    \n    # Setup Storage\n    y_pred = pd.DataFrame(0.0, index=train_data.index, columns=TARGET_NAMES)\n    y_pred_test = np.zeros([len(test_data), len(TARGET_NAMES)], dtype=float)\n\n    for fold in range(n_splits):\n        seed_everything(seed*(seed//2 + fold))\n        # Create masks\n        train_mask = train_data['fold'] != fold\n        valid_mask = train_data['fold'] == fold\n        val_idx = train_data[valid_mask].index\n\n        # Raw Inputs (Embeddings)\n        X_train_raw = train_data[train_mask][COLUMNS].values\n        X_valid_raw = train_data[valid_mask][COLUMNS].values\n        X_test_raw = test_data[COLUMNS].values\n        \n        # Semantic Inputs (Slicing)\n        # We handle the case where semantic features might be None\n        sem_train_fold = semantic_train[train_mask] if semantic_train is not None else None\n        sem_valid_fold = semantic_train[valid_mask] if semantic_train is not None else None\n        \n        # Raw Targets\n        y_train = train_data[train_mask][TARGET_NAMES].values\n        y_valid = train_data[valid_mask][TARGET_NAMES].values\n\n        # ===========================\n        # 1) TRANSFORM TARGETS\n        # ===========================\n        if target_transform == 'log':\n            y_train_proc = np.log1p(y_train)\n        elif target_transform == 'max':\n            y_train_proc = y_train / target_max_arr\n        else:\n            y_train_proc = y_train\n\n        # ==========================================\n        # 2) FEATURE ENGINEERING\n        # ==========================================\n        engine = deepcopy(feature_engine)\n        \n        # FIT: Now passes y (for PLS/RFE) and Semantic Features\n        engine.fit(X_train_raw, y=y_train_proc, X_semantic=sem_train_fold)\n        \n        # TRANSFORM: Pass Semantic Features\n        x_train_eng = engine.transform(X_train_raw, X_semantic=sem_train_fold)\n        x_valid_eng = engine.transform(X_valid_raw, X_semantic=sem_valid_fold)\n        # For test, we use the full test semantic set\n        x_test_eng = engine.transform(X_test_raw, X_semantic=semantic_test)\n        \n        # ==========================================\n        # 3) TRAIN & PREDICT\n        # ==========================================\n        fold_valid_pred = np.zeros_like(y_valid)\n        fold_test_pred = np.zeros([len(test_data), len(TARGET_NAMES)])\n\n        for k in range(len(TARGET_NAMES)):\n            regr = deepcopy(model)\n            \n            # Fit model\n            regr.fit(x_train_eng, y_train_proc[:, k])\n            \n            # Predict\n            pred_valid_raw = regr.predict(x_valid_eng)\n            pred_test_raw = regr.predict(x_test_eng)\n            \n            # ===========================\n            # 4) INVERSE TRANSFORM\n            # ===========================\n            if target_transform == 'log':\n                pred_valid_inv = np.expm1(pred_valid_raw)\n                pred_test_inv = np.expm1(pred_test_raw)\n            elif target_transform == 'max':\n                pred_valid_inv = (pred_valid_raw * target_max_arr[k])\n                pred_test_inv = (pred_test_raw * target_max_arr[k])\n            else:\n                pred_valid_inv = pred_valid_raw\n                pred_test_inv = pred_test_raw\n\n            fold_valid_pred[:, k] = pred_valid_inv\n            fold_test_pred[:, k] = pred_test_inv\n\n        # Store results\n        y_pred.loc[val_idx] = fold_valid_pred\n        y_pred_test += fold_test_pred / n_splits\n        \n        if fold == 0:\n            print(f\"  [Fold 0 Info] Target: {target_transform}, Feats: {x_train_eng.shape}\")\n\n    full_cv = competition_metric(y_true.values, y_pred.values)\n    print(f\"Full CV Score: {full_cv:.6f}\")\n    \n    return y_pred.values, y_pred_test\n\n# --- STEP 3: Run Models ---\n\n# Initialize the NEW Supervised Engine\nfeat_engine = SupervisedEmbeddingEngine(\n    n_pca=0.80,\n    n_pls=8,             # Supervised signals\n    n_gmm=6,             # Soft clusters\n)\n\nprint(\"\\n###### GradientBoosting Regressor #######\")\noof_gb, pred_test_gb = cross_validate(\n    GradientBoostingRegressor(random_state=43), \n    train_siglip_df, test_siglip_df, \n    feature_engine=feat_engine,\n    semantic_train=sem_train_full,\n    semantic_test=sem_test_full\n)\ncompare_results(oof_gb, train_siglip_df)\n\nprint(\"\\n###### Hist Gradient Boosting Regressor ######\")\noof_hb, pred_test_hb = cross_validate(\n    HistGradientBoostingRegressor(random_state=43), \n    train_siglip_df, test_siglip_df, \n    feature_engine=feat_engine,\n    semantic_train=sem_train_full,\n    semantic_test=sem_test_full\n)\ncompare_results(oof_hb, train_siglip_df)\n\nprint(\"\\n##### CAT Regressor ######\")\noof_cat, pred_test_cat = cross_validate(\n    CatBoostRegressor(verbose=0,random_state=43), \n    train_siglip_df, test_siglip_df, \n    feature_engine=feat_engine,\n    semantic_train=sem_train_full,\n    semantic_test=sem_test_full\n)\ncompare_results(oof_cat, train_siglip_df)\n\nprint(\"\\n######## LGBM #######\")\noof_lgbm, pred_test_lgbm = cross_validate(\n    LGBMRegressor(verbose=-1,random_state=43), \n    train_siglip_df, test_siglip_df, \n    feature_engine=feat_engine, \n    semantic_train=sem_train_full,\n    semantic_test=sem_test_full,\n    target_transform='max')\ncompare_results(oof_lgbm, train_siglip_df)\n\npred_test = (\n    pred_test_hb\n    + pred_test_gb\n    + pred_test_cat\n    + pred_test_lgbm\n) / 4\n\ntest_df[TARGET_NAMES] = pred_test\ntest_df = post_process_biomass(test_df)\n\ntest_df['base_id'] = test_df['image_path'].apply(lambda p: Path(p).stem)\ncols_to_save = ['base_id'] + TARGET_NAMES\nsave_name = \"preds_siglip_engine_wide.csv\" \n\ntest_df[cols_to_save].to_csv(save_name, index=False)\n\nprint(f\"✅ Successfully saved Wide-Format predictions to {save_name}\")\nprint(f\"   Shape: {test_df[cols_to_save].shape}\")\nprint(test_df[cols_to_save].head())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-05T01:27:17.167404Z","iopub.execute_input":"2026-01-05T01:27:17.167673Z","iopub.status.idle":"2026-01-05T01:30:12.527243Z","shell.execute_reply.started":"2026-01-05T01:27:17.167654Z","shell.execute_reply":"2026-01-05T01:30:12.526485Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# # ====================================================================================\n# # [Part 5 Optimized] Robust Ensemble & Orthogonal Projection-proved\n# # ====================================================================================\n# import pandas as pd\n# import numpy as np\n# import os\n# import glob\n# import warnings\n\n# # 忽略 pandas 的 FutureWarning\n# warnings.filterwarnings('ignore')\n\n# def robust_ensemble_inference():\n#     print(\"🏁 Starting Robust Final Ensemble (With Projection)...\")\n    \n#     # 🌟 统一使用 targets 变量\n#     targets = ['Dry_Total_g', 'GDM_g', 'Dry_Green_g', 'Dry_Dead_g', 'Dry_Clover_g']\n    \n#     # 1. 定义文件和初始权重 (Config)\n#     model_configs = {\n#         'DINOMamba': {'file': 'preds_dino_wide.csv',          'weight': 0.23}, #0.66\n#         #'EVA':       {'file': 'preds_eva02_wide.csv',         'weight': 0.15},  #0.65\n#         'D3':        {'file': 'preds_dino3_wide.csv',         'weight': 0.19}, #0.65\n#         'SigEngine': {'file': 'preds_siglip_engine_wide.csv', 'weight': 0.35}, #0.67\n#         'vit':       {'file': 'vit_wide.csv',                 'weight': 0.23}  #0.66 \n#     }\n    \n#     loaded_dfs = {}\n#     valid_weights = {}\n    \n#     # 2. 安全加载数据 & 3. 加载并“标准化”每个模型\n#     print(\"  Loading and Expanding predictions...\")\n#     for name, cfg in model_configs.items():\n#         fpath = cfg['file']\n        \n#         # 简单容错：找不到就去工作目录找\n#         if not os.path.exists(fpath):\n#             alt = os.path.join(\"/kaggle/working\", fpath)\n#             if os.path.exists(alt): fpath = alt\n\n#         if os.path.exists(fpath):\n#             df = pd.read_csv(fpath)\n#             df['base_id'] = df['base_id'].astype(str)\n#             df = df.drop_duplicates(subset=['base_id']).set_index('base_id')\n            \n#             # ====================================================\n#             # 🌟 核心逻辑：3列变5列 (标准化)\n#             # ====================================================\n            \n#             # 1. 基础清洗\n#             for c in ['Dry_Total_g', 'GDM_g', 'Dry_Green_g']:\n#                 if c in df.columns: df[c] = df[c].clip(lower=0)\n\n#             # 2. 如果缺少 Dead (针对 DINO 模型)，通过减法补全\n#             if 'Dry_Dead_g' not in df.columns:\n#                 print(f\"    🔧 Extending {name}: Calculating Dead...\")\n#                 safe_gdm = np.minimum(df['GDM_g'], df['Dry_Total_g'])\n#                 df['Dry_Dead_g'] = df['Dry_Total_g'] - safe_gdm\n            \n#             # 3. 如果缺少 Clover (针对 DINO 模型)，通过减法补全\n#             if 'Dry_Clover_g' not in df.columns:\n#                 print(f\"    🔧 Extending {name}: Calculating Clover...\")\n#                 safe_green = np.minimum(df['Dry_Green_g'], df['GDM_g'])\n#                 df['Dry_Clover_g'] = df['GDM_g'] - safe_green\n\n#             # 4. 提取对齐后的 5 列\n#             for t in targets:\n#                 if t not in df.columns: df[t] = 0.0 # 防爆\n#             df = df[targets]\n            \n#             loaded_dfs[name] = df\n#             valid_weights[name] = cfg['weight']\n#             print(f\"    ✅ Loaded {name:<10} | Shape: {df.shape} (All 5 cols ready)\")\n#         else:\n#             print(f\"    ⚠️ File not found: {fpath}\")\n\n#     # 3. 权重归一化\n#     total_w = sum(valid_weights.values())\n#     if total_w == 0: return\n#     final_weights = {k: v / total_w for k, v in valid_weights.items()}\n    \n#     # 4. 对齐索引\n#     base_model = max(loaded_dfs, key=lambda k: len(loaded_dfs[k]))\n#     all_ids = loaded_dfs[base_model].index\n#     print(f\"\\n  Alignment Base: {base_model} ({len(all_ids)} IDs)\")\n\n#     # 5. 加权融合 (Weighted Averaging)\n#     # 🌟 修正：这里变量名从 core_cols 改回 targets\n#     print(\"  ⚖️ Weighted Averaging on 5 Columns...\")\n#     ensemble_df = pd.DataFrame(index=all_ids, columns=targets)\n#     ensemble_df[:] = 0\n    \n#     for col in targets:\n#         weighted_sum = 0\n#         for name, weight in final_weights.items():\n#             series = loaded_dfs[name][col].reindex(all_ids).fillna(0)\n#             weighted_sum += series * weight\n#         ensemble_df[col] = weighted_sum\n\n#     # ====================================================\n#     # 🌟 核心升级：Step 6 改用正交投影 (Top方案的核心)\n#     # ====================================================\n#     print(\"  🔧 Applying Orthogonal Projection (Smart Constraints)...\")\n    \n#     def apply_projection(df):\n#         # 必须严格按照矩阵定义的顺序提取数据\n#         ordered_cols = [\"Dry_Green_g\", \"Dry_Clover_g\", \"Dry_Dead_g\", \"GDM_g\", \"Dry_Total_g\"]\n#         # 转置为 (5, N)\n#         Y = df[ordered_cols].values.T\n        \n#         # 定义物理约束矩阵 C (Cx = 0)\n#         # Row 1: Green + Clover - GDM = 0\n#         # Row 2: Dead + GDM - Total = 0\n#         C = np.array([\n#             [1, 1, 0, -1,  0],  \n#             [0, 0, 1,  1, -1]   \n#         ])\n        \n#         # 计算投影矩阵 P = I - C^T (C C^T)^-1 C\n#         # 这一步将预测值投影到符合物理定律的最近平面上\n#         P = np.eye(5) - C.T @ np.linalg.inv(C @ C.T) @ C\n        \n#         # 执行修正\n#         Y_new = P @ Y\n        \n#         # 转回 DataFrame\n#         return pd.DataFrame(Y_new.T, columns=ordered_cols, index=df.index)\n\n#     # 1. 先确保非负\n#     ensemble_df[targets] = ensemble_df[targets].clip(lower=0)\n    \n#     # 2. 执行投影修正\n#     corrected_df = apply_projection(ensemble_df)\n    \n#     # 3. 投影后可能会产生微小的负数（数学误差），再次截断\n#     corrected_df[targets] = corrected_df[targets].clip(lower=0)\n    \n#     # 将修正结果覆盖回去\n#     ensemble_df = corrected_df\n\n#     # 7. 生成提交文件\n#     print(\"  📝 Generating submission file...\")\n#     submission_rows = []\n#     targets_order = ['Dry_Clover_g', 'Dry_Dead_g', 'Dry_Green_g', 'Dry_Total_g', 'GDM_g']\n    \n#     for base_id, row in ensemble_df.iterrows():\n#         for t in targets_order:\n#             submission_rows.append({\n#                 'sample_id': f\"{base_id}__{t}\",\n#                 'target': float(row[t])\n#             })\n            \n#     sub_df = pd.DataFrame(submission_rows)\n#     sub_df.to_csv(\"submission.csv\", index=False)\n#     print(f\"🎉 FINAL SUBMISSION GENERATED: submission.csv\")\n#     print(f\"   Shape: {sub_df.shape}\")\n#     print(\"\\nPreview:\")\n#     print(sub_df.head(10))\n\n# if __name__ == \"__main__\":\n#     robust_ensemble_inference()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-05T01:30:12.528262Z","iopub.execute_input":"2026-01-05T01:30:12.528558Z","iopub.status.idle":"2026-01-05T01:30:12.536509Z","shell.execute_reply.started":"2026-01-05T01:30:12.528537Z","shell.execute_reply":"2026-01-05T01:30:12.535622Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ====================================================================================\n# [Part 5 Targeted] Arithmetic Ensemble -> Projection -> Dead Scaling\n# ====================================================================================\nimport pandas as pd\nimport numpy as np\nimport os\nimport glob\nimport warnings\n\n# 忽略 pandas 的 FutureWarning\nwarnings.filterwarnings('ignore')\n\ndef targeted_ensemble_inference():\n    print(\"🏁 Starting Targeted Ensemble (Arithmetic -> Projection -> Scale Dead)...\")\n    \n    targets = ['Dry_Total_g', 'GDM_g', 'Dry_Green_g', 'Dry_Dead_g', 'Dry_Clover_g']\n    \n    # 1. 定义文件和权重 (保持之前的最优配置)\n    model_configs = {\n        'DINOMamba': {'file': 'preds_dino_wide.csv',          'weight': 0.23}, \n        'D3':        {'file': 'preds_dino3_wide.csv',         'weight': 0.19}, \n        'SigEngine': {'file': 'preds_siglip_engine_wide.csv', 'weight': 0.35}, \n        'vit':       {'file': 'vit_wide.csv',                 'weight': 0.23}  \n    }\n    \n    # 特定变量的缩放配置\n    DEAD_SCALE_FACTOR = 0.98\n\n    loaded_dfs = {}\n    valid_weights = {}\n    \n    print(\"  Loading and Expanding predictions...\")\n    for name, cfg in model_configs.items():\n        fpath = cfg['file']\n        if not os.path.exists(fpath):\n            alt = os.path.join(\"/kaggle/working\", fpath)\n            if os.path.exists(alt): fpath = alt\n\n        if os.path.exists(fpath):\n            df = pd.read_csv(fpath)\n            df['base_id'] = df['base_id'].astype(str)\n            df = df.drop_duplicates(subset=['base_id']).set_index('base_id')\n            \n            # 基础清洗\n            for c in ['Dry_Total_g', 'GDM_g', 'Dry_Green_g']:\n                if c in df.columns: df[c] = df[c].clip(lower=0)\n\n            # 补全逻辑\n            if 'Dry_Dead_g' not in df.columns:\n                safe_gdm = np.minimum(df['GDM_g'], df['Dry_Total_g'])\n                df['Dry_Dead_g'] = df['Dry_Total_g'] - safe_gdm\n            \n            if 'Dry_Clover_g' not in df.columns:\n                safe_green = np.minimum(df['Dry_Green_g'], df['GDM_g'])\n                df['Dry_Clover_g'] = df['GDM_g'] - safe_green\n\n            # 对齐列\n            for t in targets:\n                if t not in df.columns: df[t] = 0.0\n            df = df[targets]\n            \n            loaded_dfs[name] = df\n            valid_weights[name] = cfg['weight']\n            print(f\"    ✅ Loaded {name:<10} | Shape: {df.shape}\")\n        else:\n            print(f\"    ⚠️ File not found: {fpath}\")\n\n    # 权重归一化\n    total_w = sum(valid_weights.values())\n    if total_w == 0: return\n    final_weights = {k: v / total_w for k, v in valid_weights.items()}\n    print(f\"\\n  Normalized Weights: {final_weights}\")\n    \n    # 对齐索引\n    base_model = max(loaded_dfs, key=lambda k: len(loaded_dfs[k]))\n    all_ids = loaded_dfs[base_model].index\n    \n    # ====================================================\n    # Step 1: 算术平均 (Arithmetic Mean)\n    # ====================================================\n    print(\"  ⚖️ Weighted Arithmetic Averaging...\")\n    ensemble_df = pd.DataFrame(index=all_ids, columns=targets)\n    ensemble_df[:] = 0\n    \n    for col in targets:\n        weighted_sum = 0\n        for name, weight in final_weights.items():\n            series = loaded_dfs[name][col].reindex(all_ids).fillna(0).clip(lower=0)\n            weighted_sum += series * weight\n        ensemble_df[col] = weighted_sum\n\n    # ====================================================\n    # Step 2: 正交投影修正 (Orthogonal Projection)\n    # ====================================================\n    print(\"  🔧 Applying Orthogonal Projection (Enforcing Constraints)...\")\n    \n    def apply_projection(df):\n        ordered_cols = [\"Dry_Green_g\", \"Dry_Clover_g\", \"Dry_Dead_g\", \"GDM_g\", \"Dry_Total_g\"]\n        Y = df[ordered_cols].values.T\n        \n        # 物理约束矩阵\n        C = np.array([\n            [1, 1, 0, -1,  0],  \n            [0, 0, 1,  1, -1]   \n        ])\n        \n        P = np.eye(5) - C.T @ np.linalg.inv(C @ C.T) @ C\n        Y_new = P @ Y\n        \n        return pd.DataFrame(Y_new.T, columns=ordered_cols, index=df.index)\n\n    # 执行投影\n    corrected_df = apply_projection(ensemble_df)\n    corrected_df[targets] = corrected_df[targets].clip(lower=0)\n    \n    # ====================================================\n    # Step 3: 特定变量缩放 (Targeted Scaling) - 在投影之后\n    # ====================================================\n    print(f\"  📉 Applying Post-Projection Scaling: Dry_Dead_g * {DEAD_SCALE_FACTOR}\")\n    \n    # 只针对 Dry_Dead_g 进行缩放\n    corrected_df['Dry_Dead_g'] = corrected_df['Dry_Dead_g'] * DEAD_SCALE_FACTOR\n    \n    # 这一步之后，GDM + Dead = Total 的约束会被轻微破坏，\n    # 但我们接受这一点，以换取更准确的 Dead 预测。\n    \n    ensemble_df = corrected_df[targets]\n\n    # 生成提交\n    print(\"  📝 Generating submission file...\")\n    submission_rows = []\n    targets_order = ['Dry_Clover_g', 'Dry_Dead_g', 'Dry_Green_g', 'Dry_Total_g', 'GDM_g']\n    \n    for base_id, row in ensemble_df.iterrows():\n        for t in targets_order:\n            submission_rows.append({\n                'sample_id': f\"{base_id}__{t}\",\n                'target': float(row[t])\n            })\n            \n    sub_df = pd.DataFrame(submission_rows)\n    sub_df.to_csv(\"submission.csv\", index=False)\n    print(f\"🎉 FINAL SUBMISSION GENERATED: submission.csv\")\n    print(f\"   Shape: {sub_df.shape}\")\n    print(sub_df.head(10))\n\nif __name__ == \"__main__\":\n    targeted_ensemble_inference()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-05T01:50:46.826345Z","iopub.execute_input":"2026-01-05T01:50:46.82723Z","iopub.status.idle":"2026-01-05T01:50:46.885146Z","shell.execute_reply.started":"2026-01-05T01:50:46.827206Z","shell.execute_reply":"2026-01-05T01:50:46.884496Z"}},"outputs":[],"execution_count":null}]}