{"metadata":{"kernelspec":{"name":"python3","display_name":"Python 3","language":"python"},"language_info":{"name":"python","version":"3.12.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"gpu","dataSources":[{"sourceType":"competition","sourceId":112509,"databundleVersionId":14254895},{"sourceType":"datasetVersion","sourceId":14451508,"datasetId":9230593,"databundleVersionId":15271584},{"sourceType":"modelInstanceVersion","sourceId":782191,"databundleVersionId":16023001,"modelInstanceId":596810,"modelId":609071}],"dockerImageVersionId":31236,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true},"papermill":{"default_parameters":{},"duration":848.354627,"end_time":"2026-01-06T08:34:17.294730","environment_variables":{},"exception":null,"input_path":"__notebook__.ipynb","output_path":"__notebook__.ipynb","parameters":{},"start_time":"2026-01-06T08:20:08.940103","version":"2.6.0"}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"\n\nimport os\nimport sys\n\n# 必须在导入 transformers/tensorflow 之前设置\nos.environ[\"USE_TF\"] = \"0\"\nos.environ[\"PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION\"] = \"python\"\n\nimport gc\nimport random\nimport warnings\nimport numpy as np\nimport pandas as pd\nfrom tqdm import tqdm\nfrom PIL import Image\nfrom pathlib import Path\n\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import Dataset, DataLoader\nimport torchvision.transforms as T\nfrom transformers import AutoModel\n\n# Models\nimport lightgbm as lgb\nimport xgboost as xgb\n# import optuna  # 移除 Optuna\nfrom sklearn.linear_model import Ridge, LinearRegression\nfrom sklearn.preprocessing import StandardScaler, RobustScaler\nfrom sklearn.model_selection import KFold, RepeatedKFold\nfrom sklearn.multioutput import MultiOutputRegressor\nfrom sklearn.base import BaseEstimator, RegressorMixin, clone\nfrom sklearn.decomposition import PCA\n\n\n# 屏蔽警告\nwarnings.filterwarnings('ignore')\n# ====================================================\n# 配置 (Configuration)\n# ====================================================\nclass CFG:\n    DATA_PATH = '/kaggle/input/csiro-biomass'\n    train_path = os.path.join(DATA_PATH, 'train.csv')\n    test_path = os.path.join(DATA_PATH, 'test.csv')\n    \n    # DINOv2 模型路径 (支持 Kaggle 离线路径)\n    \"\"\"\n    dinov2_paths = [\n        'vit_7b_patch16_dinov3.lvd1689m',   \n    ]\n    \"\"\"\n    MODEL_DIR = '/kaggle/input/models/forrestxlz/timm-vit-large-patch16-dinov3-lvd1689m/pytorch/default/1/vit_large_patch16_dinov3.lvd1689m'\n    MODEL_NAME = f\"vit_large_patch16_dinov3.lvd1689m\"\n    \n    # 增大图片分辨率至 512 \n    img_size = 512 \n    IMG_SIZES = [256, 512, 768]   # 新增：同时提取 256 和 512\n    #IMG_SIZES = [16]   # 新增：同时提取 256 和 512\n    SPLIT_SCALES = (256,512,768)\n    # 新增：外层要集成的折数（只影响最外层的训练次数，不会重复做特征提取）\n    ENSEMBLE_FOLDS = [2,3,5]\n    # 分辨率增大后，显存占用增加，需减小 Batch Size\n    batch_size = 1\n    MID_BLOCKS = [22,23]   # 例如输入 35,37\n\n\n    USE_PCA = True\n    PCA_N_COMPONENTS = 355          # 每个 view 降到多少维\n    PCA_WHITEN = False              # 一般不需要\n    PCA_SVD_SOLVER = \"randomized\"   # 小样本大特征更快\n    \n    \n    seed = 42\n    n_fold = 4\n    \n    target_cols = ['Dry_Green_g', 'Dry_Dead_g', 'Dry_Clover_g', 'GDM_g', 'Dry_Total_g']\n    target_weights = [0.1, 0.1, 0.1, 0.2, 0.5]\n    \n    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n\n# ====================================================\n# 工具函数\n# ====================================================\ndef seed_everything(seed=42):\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\nseed_everything(CFG.seed)\n\n\n\ndef weighted_r2_metric(y_true, y_pred):\n    \"\"\"比赛评估指标: 加权 R2\"\"\"\n    weights = np.array(CFG.target_weights)\n    if y_true.ndim == 1: y_true = y_true.reshape(-1, 1)\n    if y_pred.ndim == 1: y_pred = y_pred.reshape(-1, 1)\n    \n    ss_res = np.sum(weights * np.sum((y_true - y_pred)**2, axis=0))\n    global_mean = np.average(np.mean(y_true, axis=0), weights=weights)\n    ss_tot = np.sum(weights * np.sum((y_true - global_mean)**2, axis=0))\n    return 1 - (ss_res / (ss_tot + 1e-8))\n\ndef post_process_biomass(df_preds):\n    \"\"\"物理约束后处理\"\"\"\n    ordered_cols = [\"Dry_Green_g\", \"Dry_Clover_g\", \"Dry_Dead_g\", \"GDM_g\", \"Dry_Total_g\"]\n    if not all(col in df_preds.columns for col in ordered_cols):\n        return df_preds\n    \n    Y = df_preds[ordered_cols].values.T\n    C = np.array([[1, 1, 0, -1, 0], [0, 0, 1, 1, -1]], dtype=float)\n    \n    try:\n        inv_CCt = np.linalg.inv(C @ C.T)\n    except np.linalg.LinAlgError:\n        inv_CCt = np.linalg.pinv(C @ C.T)\n    \n    P = np.eye(5) - C.T @ inv_CCt @ C\n    Y_reconciled = (P @ Y).T.clip(min=0)\n    \n    df_out = df_preds.copy()\n    df_out[ordered_cols] = Y_reconciled\n    \n    # 简单修正\n    df_out['GDM_g'] = df_out['Dry_Green_g'] + df_out['Dry_Clover_g']\n    df_out['Dry_Total_g'] = df_out['GDM_g'] + df_out['Dry_Dead_g']\n    return df_out\n\n# ====================================================\n# 数据加载与处理\n# ====================================================\ndef load_data():\n    train_df = pd.read_csv(CFG.train_path)\n    test_df = pd.read_csv(CFG.test_path)\n    \n    def parse_ids(df):\n        if 'sample_id' in df.columns:\n            df[['image_id', 'target_type']] = df['sample_id'].str.split('__', n=1, expand=True)\n        return df\n\n    train_df = parse_ids(train_df)\n    test_df = parse_ids(test_df)\n    \n    meta_cols = ['image_id', 'image_path']\n    train_pivot = train_df.pivot_table(index=meta_cols, columns='target_name', values='target').reset_index()\n    train_pivot.columns.name = None\n    \n    test_pivot = test_df[meta_cols].drop_duplicates().reset_index(drop=True)\n    \n    return train_pivot, test_pivot\n\n\ndef split_lr_pil(img: Image.Image, overlap: int = 0):\n    \"\"\"\n    把 PIL 图像左右切成两块，可选重叠 overlap 像素（建议 0 或 32/64）。\n    返回: left_img, right_img\n    \"\"\"\n    w, h = img.size\n    mid = w // 2\n\n    # clamp 防越界\n    l_right = min(w, mid + overlap)\n    r_left  = max(0, mid - overlap)\n\n    left  = img.crop((0, 0, l_right, h))\n    right = img.crop((r_left, 0, w, h))\n    return left, right\n\n\nclass BiomassDataset(Dataset):\n    def __init__(self, df, transform=None, split_lr=False, overlap=0):\n        self.image_paths = df['image_path'].values\n        self.transform = transform\n        self.split_lr = split_lr\n        self.overlap = overlap\n        \n    def __len__(self):\n        return len(self.image_paths)\n    \n    def __getitem__(self, idx):\n        path = os.path.join(CFG.DATA_PATH, self.image_paths[idx])\n        try:\n            image = Image.open(path).convert(\"RGB\")\n        except:\n            image = Image.new(\"RGB\", (256, 256))\n\n        if self.transform is None:\n            return image\n\n        if not self.split_lr:\n            return self.transform(image)\n\n        # split LR -> 2 个视图\n        left, right = split_lr_pil(image, overlap=self.overlap)\n        t_left  = self.transform(left)   # (3,H,W)\n        t_right = self.transform(right)  # (3,H,W)\n        return torch.stack([t_left, t_right], dim=0)  # (2,3,H,W)\n\n# ====================================================\n# 特征提取 (DINOv2)\n# ====================================================\ndef get_dinov2_model():\n    if AutoModel is not None:\n        for path in CFG.dinov2_paths:\n            if os.path.exists(path):\n                print(f\"Loading DINOv2 (Transformers) from: {path}\")\n                try:\n                    model = AutoModel.from_pretrained(path, local_files_only=True)\n                    return model.to(CFG.device).eval()\n                except Exception as e:\n                    print(f\"Failed to load from {path}: {e}\")\n\n    print(\"Trying to load via torch.hub (facebookresearch/dinov2)...\")\n    try:\n        torch.hub.set_dir('/kaggle/working/torch_hub_cache')\n        model = torch.hub.load('facebookresearch/dinov2', 'dinov2_vitl14', pretrained=True)\n        print(\"Successfully loaded DINOv2 via torch.hub!\")\n        return model.to(CFG.device).eval()\n    except Exception as e:\n        print(f\"torch.hub load failed: {e}\")\n        \n    if AutoModel is not None:\n        print(\"Warning: Local DINOv2 not found. Trying to download...\")\n        try:\n            model = AutoModel.from_pretrained('facebook/dinov2-large')\n            return model.to(CFG.device).eval()\n        except Exception as e:\n            print(f\"Transformers download failed: {e}\")\n\n    raise RuntimeError(\"Could not load DINOv2 model.\")\n\nfrom pathlib import Path\nimport torch\nimport timm\nfrom safetensors import safe_open\n\ndef load_timm_from_hf_dir_low_cpu(model_dir: str, model_name: str, device, dtype=torch.bfloat16):\n    model_dir = Path(model_dir)\n\n    # 0) 先在 CPU 上建模，并把参数 dtype 设为 bf16（关键）\n    model = timm.create_model(model_name, pretrained=False, num_classes=0, global_pool=\"\")\n    model = model.to(dtype=dtype)  # 让参数变成 bf16（在 load_state_dict 之前做）\n\n    # 1) 读权重文件（一般在 CPU 上读）\n    if (model_dir / \"model.safetensors\").exists():\n        from safetensors.torch import load_file\n        sd = load_file(str(model_dir / \"model.safetensors\"), device=\"cpu\")\n    else:\n        sd = torch.load(str(model_dir / \"pytorch_model.bin\"), map_location=\"cpu\")\n\n    # 2) 加载权重（会自动按 model 参数 dtype 写入 -> bf16）\n    incompat = model.load_state_dict(sd, strict=False)\n    print(\"missing:\", len(incompat.missing_keys), \"unexpected:\", len(incompat.unexpected_keys))\n\n    # 3) 移到目标设备\n    model = model.to(device).eval()\n    return model\n    \n\ndef debug_blocks_tokens(model, H=512, W=1024, block_indices=(31, 39)):\n    p0 = next(model.parameters())\n    x = torch.randn(1, 3, H, W, device=p0.device, dtype=p0.dtype)\n\n    feats = {}\n    handles = []\n    for idx in block_indices:\n        def _make_hook(i):\n            def _hook(_m, _inp, out):\n                feats[i] = out.detach()   # 关键：detach 成普通 tensor\n            return _hook\n        handles.append(model.blocks[idx].register_forward_hook(_make_hook(idx)))\n\n    with torch.no_grad():                 # 关键：不用 inference_mode\n        out_last = model.forward_features(x)\n\n    for h in handles:\n        h.remove()\n\n    print(\"forward_features(last) =\", tuple(out_last.shape), out_last.dtype)\n    for idx in block_indices:\n        t = feats[idx]\n        print(f\"block[{idx}] output =\", tuple(t.shape), t.dtype)\n\n    if 39 in feats and hasattr(model, \"norm\") and model.norm is not None:\n        diff = (out_last - model.norm(feats[39])).abs().mean().item()\n        print(\"mean(|out_last - norm(block[39])|) =\", diff)\n\n\ndef forward_with_blocks(model, imgs, block_indices, apply_final_norm_on_blocks=True):\n    feats = {}\n    handles = []\n\n    # 可选：去重但保留顺序\n    seen = set()\n    block_indices = [b for b in block_indices if not (b in seen or seen.add(b))]\n\n    for idx in block_indices:\n        def _make_hook(i):\n            def _hook(_m, _inp, out):\n                # 有些实现可能返回 tuple，这里做个兜底\n                if isinstance(out, (tuple, list)):\n                    out = out[0]\n                feats[i] = out.detach()\n            return _hook\n        handles.append(model.blocks[idx].register_forward_hook(_make_hook(idx)))\n\n    with torch.no_grad():\n        out_last = model.forward_features(imgs)\n\n    for h in handles:\n        h.remove()\n\n    if apply_final_norm_on_blocks and hasattr(model, \"norm\") and model.norm is not None:\n        for k in feats:\n            feats[k] = model.norm(feats[k])\n\n    return out_last, feats\n\n\n\ndef gem_pool_1d(x: torch.Tensor, p: float = 3.0, eps: float = 1e-6, dim: int = 1, signed: bool = True):\n    \"\"\"\n    x: (B, N, C), 在 dim=N 上 pooling\n    return: (B, C)\n    \"\"\"\n    if signed:\n        # 先做 signed 的幂平均，再开 p 次方根（带符号）\n        y = (torch.sign(x) * x.abs().clamp_min(eps).pow(p)).mean(dim=dim)  # (B, C)\n        return torch.sign(y) * y.abs().pow(1.0 / p)                        # (B, C)\n    else:\n        y = x.clamp_min(eps).pow(p).mean(dim=dim)                          # (B, C)\n        return y.pow(1.0 / p)                                              # (B, C)\n                                \n\n\n\ndef extract_features_multiscale(df, model, mid_blocks, img_sizes=(256, 512),\n                                split_scales=(768,), overlap=0):\n    # mid_blocks 统一成 list[int]，并去重保持顺序\n    if isinstance(mid_blocks, int):\n        mid_blocks = [mid_blocks]\n    seen = set()\n    mid_blocks = [b for b in list(mid_blocks) if not (b in seen or seen.add(b))]\n\n    print(f\"Extracting multi-scale features for {len(df)} images... scales={list(img_sizes)} mid_blocks={mid_blocks}\")\n\n    mean = [0.485, 0.456, 0.406]\n    std  = [0.229, 0.224, 0.225]\n\n    out = {}\n    prefix = getattr(model, \"num_prefix_tokens\", 1)\n\n    for sz in img_sizes:\n        do_split = (sz in split_scales)\n\n        print(f\"\\n[Scale {sz}] extracting last + mid blocks ... split_lr={do_split}\")\n\n        if do_split:\n            # 768：每半边 resize 到 (sz, sz)\n            transform = T.Compose([\n                T.Resize((sz, sz)),\n                T.ToTensor(),\n                T.Normalize(mean, std),\n            ])\n            ds = BiomassDataset(df, transform=transform, split_lr=True, overlap=overlap)\n        else:\n            # 256/512：保持原来的 (sz, 2sz)\n            transform = T.Compose([\n                T.Resize((sz, sz * 2)),\n                T.ToTensor(),\n                T.Normalize(mean, std),\n            ])\n            ds = BiomassDataset(df, transform=transform, split_lr=False)\n\n        ld = DataLoader(ds, batch_size=CFG.batch_size, shuffle=False, num_workers=0)\n\n        last_epoch = []\n        mid_epoch = {b: [] for b in mid_blocks}\n\n        for batch in tqdm(ld, desc=f\"Scale {sz}\"):\n            p = next(model.parameters())\n\n            with torch.inference_mode():\n                if batch.dim() == 5:\n                    # split 模式: batch = (B, 2, 3, sz, sz)\n                    B, K, C, H, W = batch.shape\n\n                    # 逐块 forward，降低峰值显存\n                    last_patches = []\n                    mid_patches = {b: [] for b in mid_blocks}\n\n                    for k in range(K):\n                        imgs = batch[:, k].to(device=p.device, non_blocking=True)\n                        if imgs.dtype != p.dtype:\n                            imgs = imgs.to(dtype=p.dtype)\n\n                        out_last_k, feats_k = forward_with_blocks(\n                            model,\n                            imgs,\n                            block_indices=mid_blocks,\n                            apply_final_norm_on_blocks=True\n                        )\n\n                        # 仅取 patch tokens（去掉 prefix）\n                        last_patches.append(out_last_k[:, prefix:, :])  # (B, Nk, C)\n\n                        for b in mid_blocks:\n                            mid_patches[b].append(feats_k[b][:, prefix:, :])  # (B, Nk, C)\n\n                    # 把左右两块的 patch tokens 拼起来：N 维度 concat\n                    last_patch = torch.cat(last_patches, dim=1)  # (B, N_total, C)\n\n                    last_avg = last_patch.mean(dim=1)\n                    last_max = last_patch.max(dim=1)[0]\n                    last_min = last_patch.min(dim=1)[0]\n                    last_gem = gem_pool_1d(last_patch, p=3.0, signed=True)\n\n                    emb_last = torch.cat([last_avg, last_max, last_min, last_gem], dim=1)  # (B, 4C)\n\n                    for b in mid_blocks:\n                        mid_patch = torch.cat(mid_patches[b], dim=1)  # (B, N_total, C)\n                        mid_avg = mid_patch.mean(dim=1)\n                        mid_max = mid_patch.max(dim=1)[0]\n                        mid_min = mid_patch.min(dim=1)[0]\n                        mid_gem = gem_pool_1d(mid_patch, p=3.0, signed=True)\n\n                        emb_mid = torch.cat([mid_avg, mid_max, mid_min, mid_gem], dim=1)  # (B, 4C)\n                        mid_epoch[b].append(emb_mid.detach().float().cpu().numpy())\n\n                else:\n                    # 非 split: batch = (B, 3, sz, 2sz)\n                    imgs = batch.to(device=p.device, non_blocking=True)\n                    if imgs.dtype != p.dtype:\n                        imgs = imgs.to(dtype=p.dtype)\n\n                    out_last, feats = forward_with_blocks(\n                        model,\n                        imgs,\n                        block_indices=mid_blocks,\n                        apply_final_norm_on_blocks=True\n                    )\n\n                    last_patch = out_last[:, prefix:, :]  # (B, N, C)\n                    last_avg = last_patch.mean(dim=1)\n                    last_max = last_patch.max(dim=1)[0]\n                    last_min = last_patch.min(dim=1)[0]\n                    last_gem = gem_pool_1d(last_patch, p=3.0, signed=True)\n\n                    emb_last = torch.cat([last_avg, last_max, last_min, last_gem], dim=1)  # (B, 4C)\n\n                    for b in mid_blocks:\n                        tok = feats[b]\n                        mid_patch = tok[:, prefix:, :]\n                        mid_avg = mid_patch.mean(dim=1)\n                        mid_max = mid_patch.max(dim=1)[0]\n                        mid_min = mid_patch.min(dim=1)[0]\n                        mid_gem = gem_pool_1d(mid_patch, p=3.0, signed=True)\n\n                        emb_mid = torch.cat([mid_avg, mid_max, mid_min, mid_gem], dim=1)  # (B, 4C)\n                        mid_epoch[b].append(emb_mid.detach().float().cpu().numpy())\n\n            last_epoch.append(emb_last.detach().float().cpu().numpy())\n\n        final_last = np.concatenate(last_epoch, axis=0)\n        out[f\"s{sz}_last\"] = pd.DataFrame(\n            final_last,\n            columns=[f\"s{sz}_last_feat_{i}\" for i in range(final_last.shape[1])]\n        )\n\n        for b in mid_blocks:\n            final_mid = np.concatenate(mid_epoch[b], axis=0)\n            out[f\"s{sz}_b{b}\"] = pd.DataFrame(\n                final_mid,\n                columns=[f\"s{sz}_b{b}_feat_{i}\" for i in range(final_mid.shape[1])]\n            )\n\n    return out\n\n\nclass MultiViewStackingTrainer:\n    \"\"\"\n    多 view stacking：\n      - 每个 view 有各自的 base models\n      - 训练时用同一个 KFold split，保证 OOF 对齐\n      - 输出：oof_preds[name], test_preds[name]，每个都是 (N, n_targets)\n    \"\"\"\n    def __init__(self, n_folds=5, seed=42, lgbm_params=None, xgb_params=None):\n        self.n_folds = n_folds\n        self.seed = seed\n        self.lgbm_params = lgbm_params\n        self.xgb_params = xgb_params\n        self.base_specs = []  # list of dicts: {\"name\":..., \"view\":..., \"model\":...}\n\n    def get_base_models(self):\n        ridge = Ridge(alpha=5.0, random_state=self.seed)\n\n        specs = []\n        for view in self.view_keys:  # 由 train_predict 注入\n            specs.append({\n                \"name\": f\"{view}_ridge\",\n                \"view\": view,\n                \"model\": MultiOutputRegressor(clone(ridge))\n            })\n\n        self.base_specs = specs\n        return specs\n\n    def train_predict(self, X_views: dict, y: np.ndarray, X_test_views: dict):\n        # 固定顺序，保证可复现\n        self.view_keys = sorted(X_views.keys())\n\n        any_view = self.view_keys[0]\n        X_for_split = X_views[any_view]\n\n        kf = KFold(n_splits=self.n_folds, shuffle=True, random_state=self.seed)\n\n        specs = self.get_base_models()\n        oof_preds = {s[\"name\"]: np.zeros_like(y) for s in specs}\n        test_preds = {s[\"name\"]: np.zeros((X_test_views[any_view].shape[0], y.shape[1])) for s in specs}\n\n        print(\"Training Base Models (Multi-View)...\")\n\n        for fold, (trn_idx, val_idx) in enumerate(kf.split(X_for_split, y)):\n            print(f\"  Fold {fold+1}/{self.n_folds}\")\n\n            for s in specs:\n                name, view, model = s[\"name\"], s[\"view\"], s[\"model\"]\n\n                X_tr = X_views[view][trn_idx]\n                X_val = X_views[view][val_idx]\n                y_tr = y[trn_idx]\n\n                model_fold = clone(model)\n                model_fold.fit(X_tr, y_tr)\n\n                val_pred = model_fold.predict(X_val)\n                val_pred = np.clip(val_pred, 0, None)\n                oof_preds[name][val_idx] = val_pred\n\n                test_pred = model_fold.predict(X_test_views[view])\n                test_pred = np.clip(test_pred, 0, None)\n                test_preds[name] += test_pred / self.n_folds\n\n        return oof_preds, test_preds\n\n    def fit_stacking(self, oof_preds: dict, y_true: np.ndarray, test_preds_dict: dict):\n        \"\"\"\n        meta 输入是所有 base 的 OOF 拼起来。\n        每个 target 单独训练一个 meta_model（和你原逻辑一致）。\n        \"\"\"\n        print(\"\\nTraining Meta Model (Stacking, Multi-View)...\")\n\n        # 固定 meta 特征列顺序，避免 dict 无序造成不稳定\n        base_names = sorted(oof_preds.keys())\n\n        n_targets = y_true.shape[1]\n        final_oof = np.zeros_like(y_true)\n        final_test_pred = np.zeros_like(next(iter(test_preds_dict.values())))\n\n        for i in range(n_targets):\n            X_meta_train = np.column_stack([oof_preds[n][:, i] for n in base_names])\n            X_meta_test  = np.column_stack([test_preds_dict[n][:, i] for n in base_names])\n            y_meta_train = y_true[:, i]\n\n            meta_model = Ridge(alpha=1.0, random_state=self.seed)\n            meta_model.fit(X_meta_train, y_meta_train)\n\n            final_oof[:, i] = meta_model.predict(X_meta_train)\n            final_test_pred[:, i] = meta_model.predict(X_meta_test)\n\n        final_oof = np.clip(final_oof, 0, None)\n        final_test_pred = np.clip(final_test_pred, 0, None)\n        return final_oof, final_test_pred\n\n\n\n# ====================================================\n# 主流程\n# ====================================================\ndef main():\n    print(\"Initializing Stacking Pipeline (LGBM + XGB + Ridge)...\")\n    \n    # 1. 数据准备\n    train_df, test_df = load_data()\n    print(\"删除前行数:\", train_df.shape[0])\n    target_index = train_df[train_df['image_id'] == 'ID230058600'].index\n    train_df.drop(target_index, inplace=True)\n    print(\"删除后行数:\", train_df.shape[0])\n    # 验证结果\n    print(\"删除后是否还存在目标行:\", 'ID230058600' in train_df['image_id'].values)\n    \n    dtype = torch.bfloat16 if CFG.device.type == \"cuda\" else torch.float32\n    model = load_timm_from_hf_dir_low_cpu(\n        model_dir=CFG.MODEL_DIR,\n        model_name=CFG.MODEL_NAME,\n        device=CFG.device,\n        dtype=dtype,\n    )\n    \n    #debug_blocks_tokens(model, H=CFG.img_size, W=CFG.img_size*2, block_indices=(31, 39))\n\n    X_train_views_df = extract_features_multiscale(\n        train_df, model,\n        mid_blocks=CFG.MID_BLOCKS,\n        img_sizes=CFG.IMG_SIZES,\n        #split_scales=(256, 512),   # 关键：改这里\n        split_scales=CFG.SPLIT_SCALES,   # 关键：改这里\n        overlap=0\n    )\n    \n    X_test_views_df = extract_features_multiscale(\n        test_df, model,\n        mid_blocks=CFG.MID_BLOCKS,\n        img_sizes=CFG.IMG_SIZES,\n        #split_scales=(256, 512),   # 关键：改这里\n        split_scales=CFG.SPLIT_SCALES,   # 关键：改这里\n        overlap=0\n    )\n\n\n    del model\n    torch.cuda.empty_cache()\n    gc.collect()\n\n    # 3) 预处理：每个 view 各自 scaler（逻辑正确且不会把不同 view 混在一起）\n    print(\"Preprocessing features (per-view)...\")\n    scalers = {}\n    pcas = {}  # 新增：保存每个 view 的 PCA\n    X_train_views = {}\n    X_test_views = {}\n    \n    FIT_ON_TRAIN_PLUS_TEST = True  # 你自己开关\n\n    for view in X_train_views_df.keys():\n        Xtr_raw = X_train_views_df[view].values\n        Xte_raw = X_test_views_df[view].values\n\n        scaler = RobustScaler()\n        if FIT_ON_TRAIN_PLUS_TEST:\n            X_all = np.vstack([Xtr_raw, Xte_raw])\n            scaler.fit(X_all)\n            Xtr = scaler.transform(Xtr_raw)\n            Xte = scaler.transform(Xte_raw)\n        else:\n            Xtr = scaler.fit_transform(Xtr_raw)\n            Xte = scaler.transform(Xte_raw)\n    \n        Xtr = Xtr.astype(np.float32, copy=False)\n        Xte = Xte.astype(np.float32, copy=False)\n    \n        if CFG.USE_PCA:\n            n_feats = Xtr.shape[1]\n            # 组件数建议仍按“训练集样本数”上限卡住，避免过度依赖 test 的维度上限\n            n_comp = min(CFG.PCA_N_COMPONENTS, Xtr.shape[0] - 1, n_feats)\n    \n            pca = PCA(\n                n_components=n_comp,\n                whiten=CFG.PCA_WHITEN,\n                svd_solver=CFG.PCA_SVD_SOLVER,\n                random_state=CFG.seed\n            )\n    \n            if False:\n                Z_all = np.vstack([Xtr, Xte])\n                pca.fit(Z_all)\n                Xtr = pca.transform(Xtr)\n                Xte = pca.transform(Xte)\n            else:\n                Xtr = pca.fit_transform(Xtr)\n                Xte = pca.transform(Xte)\n    \n        X_train_views[view] = Xtr\n        X_test_views[view] = Xte\n\n\n\n    Y_train = train_df[CFG.target_cols].values\n    Y_train_log = np.log1p(Y_train)\n\n    def run_one_nfold(n_folds: int):\n        # ========= Pipeline A: LOG 空间 =========\n        trainer_log = MultiViewStackingTrainer(n_folds=n_folds, seed=CFG.seed)\n\n        oof_preds_log, test_preds_log = trainer_log.train_predict(\n            X_train_views, Y_train_log, X_test_views\n        )\n        final_oof_log, final_test_pred_log = trainer_log.fit_stacking(\n            oof_preds_log, Y_train_log, test_preds_log\n        )\n\n        # 回到原空间\n        pipeA_oof_raw = np.expm1(final_oof_log)\n        pipeA_test_raw = np.expm1(final_test_pred_log)\n        pipeA_oof_raw = np.clip(pipeA_oof_raw, 0, None)\n        pipeA_test_raw = np.clip(pipeA_test_raw, 0, None)\n\n        # ========= Pipeline B: 原空间 =========\n        trainer_raw = MultiViewStackingTrainer(n_folds=n_folds, seed=CFG.seed)\n\n        oof_preds_raw, test_preds_raw = trainer_raw.train_predict(\n            X_train_views, Y_train, X_test_views\n        )\n        pipeB_oof_raw, pipeB_test_raw = trainer_raw.fit_stacking(\n            oof_preds_raw, Y_train, test_preds_raw\n        )\n        pipeB_oof_raw = np.clip(pipeB_oof_raw, 0, None)\n        pipeB_test_raw = np.clip(pipeB_test_raw, 0, None)\n\n        # ========= Level-2 输出做简单平均（保持你原逻辑）=========\n        final_oof = 0.5 * pipeA_oof_raw + 0.5 * pipeB_oof_raw\n        final_test_pred = 0.5 * pipeA_test_raw + 0.5 * pipeB_test_raw\n\n        return final_oof, final_test_pred\n\n    # ========= 外层：按 CFG.ENSEMBLE_FOLDS 跑多套，再平均集成 =========\n    oof_list = []\n    test_list = []\n\n    for nf in CFG.ENSEMBLE_FOLDS:\n        print(\"\\n==============================\")\n        print(f\" Outer Ensemble Run: n_folds={nf}\")\n        print(f\"==============================\")\n\n        oof_nf, test_nf = run_one_nfold(nf)\n\n        # 参考分（注意：仍然是 meta 在训练集上拟合得到的预测，会偏乐观）\n        score_nf = weighted_r2_metric(Y_train, oof_nf)\n        print(f\"n_folds={nf} Score(ref): {score_nf:.4f}\")\n\n        oof_list.append(oof_nf)\n        test_list.append(test_nf)\n\n    final_oof = np.mean(oof_list, axis=0)\n    final_test_pred = np.mean(test_list, axis=0)\n\n  \n    \n    # 评估（注意：你当前 fit_stacking 的 final_oof 是 meta 在训练集上的拟合预测，\n    # 会偏乐观；但你说不看 CV，这里就当个参考数）\n    score = weighted_r2_metric(Y_train, final_oof)\n    print(f\"\\nAveraged(Level-2 outputs) Score: {score:.4f}\")\n\n    # 5. 后处理与提交\n    print(\"Generating submission...\")\n    df_sub = test_df.copy()\n    df_sub[CFG.target_cols] = final_test_pred\n    df_sub = post_process_biomass(df_sub)\n    \n    sub_rows = []\n    for i, row in df_sub.iterrows():\n        img_id = row['image_id']\n        for col in CFG.target_cols:\n            sub_rows.append({\n                'sample_id': f\"{img_id}__{col}\",\n                'target': row[col]\n            })\n            \n    submission = pd.DataFrame(sub_rows)\n    submission.to_csv('submission.csv', index=False)\n    print(\"Saved to submission.csv\")\n\nif __name__ == '__main__':\n    main()","metadata":{"_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","papermill":{"duration":843.172553,"end_time":"2026-01-06T08:34:14.479586","exception":false,"start_time":"2026-01-06T08:20:11.307033","status":"completed"},"tags":[],"trusted":true,"execution":{"iopub.status.busy":"2026-03-11T12:40:57.827993Z","iopub.execute_input":"2026-03-11T12:40:57.828671Z","iopub.status.idle":"2026-03-11T12:43:18.829060Z","shell.execute_reply.started":"2026-03-11T12:40:57.828641Z","shell.execute_reply":"2026-03-11T12:43:18.828462Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}