{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceId":112509,"databundleVersionId":14254895,"isSourceIdPinned":false,"sourceType":"competition"},{"sourceId":14164178,"sourceType":"datasetVersion","datasetId":9025870}],"dockerImageVersionId":31192,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"使用的dzk问的ai的分层的方法+网络上下载的模型(修改后的)","metadata":{}},{"cell_type":"code","source":"# -------------------- Imports & Warning Handling --------------------\nimport os\nimport math\nimport gc\nimport random\nfrom pathlib import Path\nfrom typing import List, Dict, Tuple, Optional\n\nimport warnings\nwarnings.filterwarnings('ignore')\n\nimport numpy as np\nimport pandas as pd\nfrom PIL import Image\n\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.utils.data import Dataset, DataLoader\nfrom torch.cuda.amp import autocast, GradScaler\nimport torchvision.transforms as T\nimport timm\n\nfrom sklearn.model_selection import GroupKFold, StratifiedKFold\nfrom sklearn.preprocessing import LabelEncoder\nfrom tqdm.auto import tqdm\n\n# -------------------- Config (CFG) --------------------\nclass CFG:\n    seed = 42\n    img_size = 384\n    batch_size = 16\n    num_workers = 0\n    epochs = 12\n    lr = 1e-4\n    backbone = 'convnext_base'\n    log_transform = True\n    img_dir_train = '/kaggle/input/csiro-biomass'\n    img_dir_test = '/kaggle/input/csiro-biomass'\n    train_long_csv = '/kaggle/input/csiro-biomass/train.csv'\n    test_meta_csv = '/kaggle/input/csiro-biomass/test.csv'\n    sample_submission = '/kaggle/input/csiro-biomass/sample_submission.csv'\n    exp_dir = '/kaggle/working/exp_multimodal_pro_plus'\n    pretrained_weight_path = '/kaggle/input/d/doudoudoudouba/my-pretrained-weights/convnext_base_pretrained.pth'\n    \n    device = 'cuda' if torch.cuda.is_available() else 'cpu'\n    target_cols = ['Dry_Green_g','Dry_Dead_g','Dry_Clover_g','GDM_g','Dry_Total_g']\n    target_weights = {'Dry_Green_g':0.1,'Dry_Dead_g':0.1,'Dry_Clover_g':0.1,'GDM_g':0.2,'Dry_Total_g':0.5}\n    tabular_cols = ['Pre_GSHH_NDVI', 'Height_Ave_cm']\n    n_folds = 4\n    ema_decay = 0.9999\n    tta = True\n    \n    # ========== 新增：分层抽样配置 ==========\n    # 各站点抽样比例（训练集/验证集）\n    site_sample_ratio = {\n        'Tas': 0.7,   # 训练70%，验证30%\n        'NSW': 0.7,   # 训练70%，验证30%\n        'WA': 0.85,   # 训练85%，验证15%（小样本特殊处理）\n        'Vic': 0.7,   # 训练70%，验证30%\n        'Unknown': 0.7 # 未知站点默认比例------- ----------------可以删除,并没有unknown站点454646456465465\n    }\n    # WA站点需要合并的物种（小样本处理）\n    wa_species_merge = {\n        'SubcloverLosa': 'Subclover',#subclover-->clover\n        'SubcloverDalkeith': 'Subclover',\n        'SubcloverYarloop': 'Subclover'\n    }\n    # 小样本组阈值（样本数<20合并为Other）\n    min_group_size = 20\n\nos.makedirs(CFG.exp_dir, exist_ok=True)\n\n# ========== 新增：物种合并函数（针对WA站点） ==========\ndef merge_wa_species(df: pd.DataFrame) -> pd.DataFrame:\n    \"\"\"合并WA站点的相似物种，减少小分组数量\"\"\"\n    df = df.copy()\n    # 填充物种缺失值\n    df['Species'] = df['Species'].fillna('Unknown')#-------不必要的0--------00454654564654654\n    # 仅处理WA站点的物种合并\n    wa_mask = df['State'] == 'WA'\n    df.loc[wa_mask, 'Species'] = df.loc[wa_mask, 'Species'].replace(CFG.wa_species_merge)\n    return df\n\n# ========== 重构：三级分层组生成函数（替代原create_stratified_labels） ==========\ndef create_custom_strat_groups(df: pd.DataFrame) -> pd.DataFrame:\n    \"\"\"\n    生成三级分层组：State（一级）→Target（二级）→Species（三级）\n    处理小样本组合并，返回带分层组标签的DataFrame\n    \"\"\"\n    df = df.copy()\n    \n    # 1. 基础预处理：填充缺失值\n    df['State'] = df['State'].fillna('Unknown').astype(str)\n    df['Species'] = df['Species'].fillna('Unknown').astype(str)\n    # 合并WA站点相似物种\n    df = merge_wa_species(df)\n    \n    # 2. 生成三级分层组标签（State-Target-Species）\n    # 由于数据是宽表（每个image_id一行，包含所有target），需要为每个image_id关联所有target\n    # 先复制数据，为每个target生成一行\n    df_expand = []\n    for idx, row in df.iterrows():\n        for target in CFG.target_cols:#target_cols = ['Dry_Green_g','Dry_Dead_g','Dry_Clover_g','GDM_g','Dry_Total_g']\n            row_copy = row.copy()\n            row_copy['target_name'] = target\n            # 生成三级组标签\n            state = row_copy['State']\n            species = row_copy['Species']\n            row_copy['strat_group'] = f\"{state}__{target}__{species}\"\n            df_expand.append(row_copy)\n    df_expand = pd.DataFrame(df_expand)#转dataframe\n    \n    # 3. 小样本组合并（样本数<20的组合并为Other）\n    group_counts = df_expand['strat_group'].value_counts()\n    small_groups = group_counts[group_counts < CFG.min_group_size].index.tolist()\n    \n    def replace_small_group(group):\n        if group in small_groups:\n            # 拆分组名，保留State+Target，替换Species为Other\n            state, target, _ = group.split('__')\n            return f\"{state}__{target}__Other\"\n        return group\n    \n    df_expand['strat_group'] = df_expand['strat_group'].apply(replace_small_group)\n    \n    # 4. 为每个image_id聚合分层组（一个image_id对应多个target，取所有组的集合）\n    img_group_map = df_expand.groupby('image_id')['strat_group'].apply(list).to_dict()\n    # 为原df添加分层组列（用于后续抽样）\n    df['strat_groups'] = df['image_id'].map(img_group_map)\n    \n    # 5. 生成最终分层标签（用于SKlearn处理）\n    # 为每个唯一的组组合分配数字标签\n    df['strat_group_str'] = df['strat_groups'].apply(lambda x: '|'.join(sorted(x)))\n    le = LabelEncoder()\n    df['strat_label'] = le.fit_transform(df['strat_group_str'])\n    \n    return df\n\n# ========== 新增：自定义分层抽样函数 ==========\ndef custom_stratified_split(df: pd.DataFrame, fold: int, seed: int) -> Tuple[np.ndarray, np.ndarray]:\n    \"\"\"\n    按三级分层逻辑进行抽样：\n    1. 按State拆分大组\n    2. 每个State内按strat_group抽样（适配站点专属比例）\n    3. 确保训练/验证集各维度分布与原始一致\n    \"\"\"\n    random.seed(seed + fold)  # 不同fold使用不同种子，保证划分多样性\n    tr_idx = []\n    val_idx = []\n    \n    # 按State分组处理\n    for state, state_df in df.groupby('State'):\n        # 获取当前站点的抽样比例\n        sample_ratio = CFG.site_sample_ratio.get(state, 0.7)\n        \n        # 按strat_group分组抽样\n        for group, group_df in state_df.groupby('strat_group_str'):\n            group_idx = group_df.index.tolist()\n            random.shuffle(group_idx)\n            \n            # 计算抽样数量\n            train_size = int(len(group_idx) * sample_ratio)\n            # 兜底：确保训练/验证集至少有1个样本\n            train_size = max(1, min(train_size, len(group_idx)-1))\n            \n            # 划分训练/验证索引\n            tr_idx.extend(group_idx[:train_size])\n            val_idx.extend(group_idx[train_size:])\n    \n    # 转换为numpy数组\n    tr_idx = np.array(tr_idx)\n    val_idx = np.array(val_idx)\n    \n    return tr_idx, val_idx\n\n# ========== 增强：分布验证函数（验证三个核心维度） ==========\ndef validate_3d_distribution(df: pd.DataFrame, tr_idx: np.ndarray, val_idx: np.ndarray, fold: int):\n    \"\"\"验证站点、目标变量、物种三个维度的分布一致性\"\"\"\n    tr_df = df.iloc[tr_idx]\n    val_df = df.iloc[val_idx]\n    \n    print(f\"\\n===== 第 {fold} 折 三维分布验证 =====\")\n    \n    # 1. 站点（State）分布验证\n    print(\"--- 站点分布 ---\")\n    total_state = df['State'].value_counts(normalize=True) * 100\n    tr_state = tr_df['State'].value_counts(normalize=True) * 100\n    val_state = val_df['State'].value_counts(normalize=True) * 100\n    for state in total_state.index:\n        total_pct = total_state.get(state, 0)\n        tr_pct = tr_state.get(state, 0)\n        val_pct = val_state.get(state, 0)\n        tr_diff = abs(tr_pct - total_pct)\n        val_diff = abs(val_pct - total_pct)\n        print(f\"{state}: 原始{total_pct:.1f}% | 训练{tr_pct:.1f}%（偏差{tr_diff:.1f}%） | 验证{val_pct:.1f}%（偏差{val_diff:.1f}%）\")\n    \n    # 2. 物种（Species）分布验证\n    print(\"\\n--- 物种分布 ---\")\n    total_species = df['Species'].value_counts(normalize=True) * 100\n    tr_species = tr_df['Species'].value_counts(normalize=True) * 100\n    val_species = val_df['Species'].value_counts(normalize=True) * 100\n    # 只显示前10个主要物种\n    top_species = total_species.head(10).index\n    for species in top_species:\n        total_pct = total_species.get(species, 0)\n        tr_pct = tr_species.get(species, 0)\n        val_pct = val_species.get(species, 0)\n        tr_diff = abs(tr_pct - total_pct)\n        val_diff = abs(val_pct - total_pct)\n        print(f\"{species}: 原始{total_pct:.1f}% | 训练{tr_pct:.1f}%（偏差{tr_diff:.1f}%） | 验证{val_pct:.1f}%（偏差{val_diff:.1f}%）\")\n    \n    # 3. 目标变量分布验证（Dry_Total_g为核心）\n    print(\"\\n--- 核心目标变量（Dry_Total_g）分布 ---\")\n    for col in ['Dry_Total_g']:\n        total_mean = df[col].mean()\n        tr_mean = tr_df[col].mean()\n        val_mean = val_df[col].mean()\n        tr_diff = abs(tr_mean - total_mean) / total_mean * 100\n        val_diff = abs(val_mean - total_mean) / total_mean * 100\n        print(f\"{col}: 原始均值{total_mean:.2f} | 训练均值{tr_mean:.2f}（偏差{tr_diff:.1f}%） | 验证均值{val_mean:.2f}（偏差{val_diff:.1f}%）\")\n\n# 原seed_everything函数保持不变\ndef seed_everything(seed=CFG.seed):\n    random.seed(seed)\n    np.random.seed(seed)\n    torch.manual_seed(seed)\n    torch.cuda.manual_seed_all(seed)\n\nseed_everything()\n\n# 原long_to_wide函数保持不变\ndef long_to_wide(long_df: pd.DataFrame) -> pd.DataFrame:\n    df = long_df.copy()\n    if 'sample_id' not in df.columns or 'target' not in df.columns:\n        raise ValueError('训练集CSV必须包含sample_id和target列')\n    df[['image_id','tname']] = df['sample_id'].str.rsplit('__', n=1, expand=True)\n    wide = df.pivot(index='image_id', columns='tname', values='target').reset_index()\n    for t in CFG.target_cols:\n        if t not in wide.columns:\n            wide[t] = np.nan\n    return wide[['image_id'] + CFG.target_cols]\n\n# 原weighted_r2_strict函数保持不变\ndef weighted_r2_strict(y_true: np.ndarray, y_pred: np.ndarray) -> float:\n    n, m = y_true.shape\n    assert m == len(CFG.target_cols)\n    w_vec = np.array([CFG.target_weights[t] for t in CFG.target_cols], dtype=np.float64)\n    w = np.tile(w_vec, n)\n    y_true_f = y_true.reshape(-1)\n    y_pred_f = y_pred.reshape(-1)\n    y_bar_w = np.sum(w * y_true_f) / np.sum(w)\n    ss_res = np.sum(w * (y_true_f - y_pred_f) **2)\n    ss_tot = np.sum(w * (y_true_f - y_bar_w)** 2)\n    return float(1.0 - ss_res / ss_tot)\n\n# 原TabularProcessor类保持不变\nclass TabularProcessor:\n    def __init__(self, num_cols: List[str]=None, cat_cols: List[str]=None):\n        self.num_cols = num_cols or []\n        self.cat_cols = cat_cols or []\n        self.num_stats = {}\n        self.cat_maps = {}\n\n    def fit(self, df: pd.DataFrame):\n        if not self.num_cols and not self.cat_cols:\n            for c in df.columns:\n                if c in ['image_id','image_path'] + CFG.target_cols + ['State', 'Species']:\n                    continue\n                if pd.api.types.is_numeric_dtype(df[c]):\n                    self.num_cols.append(c)\n                else:\n                    self.cat_cols.append(c)\n        for c in self.num_cols:\n            col = pd.to_numeric(df[c], errors='coerce')\n            mean = float(col.mean(skipna=True)) if not col.dropna().empty else 0.0\n            std = float(col.std(skipna=True)) if not col.dropna().empty else 1.0\n            self.num_stats[c] = {'mean': mean, 'std': std}\n        for c in self.cat_cols:\n            vals = df[c].fillna('NA').astype(str).unique().tolist()\n            self.cat_maps[c] = {v:i+1 for i,v in enumerate(vals)}\n\n    def transform_row(self, row: pd.Series) -> Tuple[np.ndarray, np.ndarray]:\n        nums = []\n        for c in self.num_cols:\n            v = row.get(c, np.nan)\n            try:\n                v = float(v)\n            except Exception:\n                v = np.nan\n            if np.isnan(v):\n                v = self.num_stats[c]['mean']\n            mean = self.num_stats[c]['mean']\n            std = self.num_stats[c]['std'] if self.num_stats[c]['std']!=0 else 1.0\n            nums.append((v - mean) / std)\n        cats = []\n        for c in self.cat_cols:\n            v = str(row.get(c, 'NA'))\n            idx = self.cat_maps[c].get(v, 0)\n            cats.append(idx)\n        return np.array(nums, dtype=np.float32), np.array(cats, dtype=np.int64)\n\n    def transform_df_numeric(self, df: pd.DataFrame) -> np.ndarray:\n        arr = np.zeros((len(df), len(self.num_cols)), dtype=np.float32)\n        for i, (_, row) in enumerate(df.iterrows()):\n            nums, _ = self.transform_row(row)\n            if len(nums)>0:\n                arr[i] = nums\n        return arr\n\n# 原PastureDataset类保持不变\nclass PastureDataset(Dataset):\n    def __init__(self, df: pd.DataFrame, img_dir: str, tab_processor: TabularProcessor=None, transforms=None, is_test=False):\n        self.df = df.reset_index(drop=True)\n        self.img_dir = img_dir\n        self.tab_processor = tab_processor\n        self.transforms = transforms\n        self.is_test = is_test\n\n    def __len__(self):\n        return len(self.df)\n\n    def load_image(self, path: str):\n        return Image.open(path).convert('RGB')\n\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        img_path = row.get('image_path', f\"{row['image_id']}.jpg\")\n        full_path = os.path.join(self.img_dir, img_path)\n        img = self.load_image(full_path)\n        if self.transforms is not None:\n            img = self.transforms(img)\n        tab_num = None\n        if self.tab_processor is not None:\n            nums, _ = self.tab_processor.transform_row(row)\n            tab_num = torch.from_numpy(nums)\n        if self.is_test:\n            return img, tab_num, row['image_id']\n        targets = row[CFG.target_cols].values.astype(np.float32)\n        return img, tab_num, torch.from_numpy(targets)\n\n# 原图像变换保持不变\ntrain_transforms = T.Compose([\n    T.Resize((CFG.img_size, CFG.img_size)),\n    T.RandomHorizontalFlip(p=0.5),\n    T.RandomVerticalFlip(p=0.15),\n    T.RandomApply([T.ColorJitter(0.12,0.12,0.12,0.04)], p=0.6),\n    T.RandomRotation(8),\n    T.ToTensor(),\n    T.Normalize(mean=[0.485,0.456,0.406], std=[0.229,0.224,0.225])\n])\n\nvalid_transforms = T.Compose([\n    T.Resize((CFG.img_size, CFG.img_size)),\n    T.ToTensor(),\n    T.Normalize(mean=[0.485,0.456,0.406], std=[0.229,0.224,0.225])\n])\n\n# 原TabularEncoder类保持不变\nclass TabularEncoder(nn.Module):\n    def __init__(self, num_in:int):\n        super().__init__()\n        if num_in > 0:\n            self.net = nn.Sequential(\n                nn.Linear(num_in, 128),\n                nn.ReLU(),\n                nn.BatchNorm1d(128),\n                nn.Dropout(0.1),\n                nn.Linear(128, 128),\n                nn.ReLU()\n            )\n        else:\n            self.net = None\n\n    def forward(self, x):\n        if self.net is None:\n            return None\n        return self.net(x)\n\n# 原MultimodalModel类保持不变\nclass MultimodalModel(nn.Module):\n    def __init__(self, backbone_name=CFG.backbone, pretrained=True, num_tab=1, n_targets=5, head_dim=512, dropout=0.3):\n        super().__init__()\n        self.backbone = timm.create_model(\n            backbone_name,\n            pretrained=False,\n            num_classes=0,\n            global_pool='avg'\n        )\n\n        if pretrained and os.path.exists(CFG.pretrained_weight_path):\n            pretrained_dict = torch.load(CFG.pretrained_weight_path, map_location=CFG.device)\n            pretrained_dict = {k.replace('module.', ''): v for k, v in pretrained_dict.items()}\n            backbone_dict = self.backbone.state_dict()\n            pretrained_dict = {k: v for k, v in pretrained_dict.items() if k in backbone_dict}\n            self.backbone.load_state_dict(pretrained_dict, strict=False)\n            print(f\"成功加载自定义预训练权重: {CFG.pretrained_weight_path}\")\n        elif pretrained:\n            self.backbone = timm.create_model(backbone_name, pretrained=True, num_classes=0, global_pool='avg')\n            print(\"未找到自定义预训练权重，加载timm官方权重\")\n\n        feat_dim = self.backbone.num_features\n        self.tab_enc = TabularEncoder(num_tab) if num_tab > 0 else None\n        if self.tab_enc is not None:\n            fusion_dim = feat_dim + 128\n        else:\n            fusion_dim = feat_dim\n        self.head = nn.Sequential(\n            nn.Linear(fusion_dim, head_dim),\n            nn.ReLU(),\n            nn.Dropout(dropout),\n            nn.Linear(head_dim, n_targets)\n        )\n\n    def forward(self, img, tab=None):\n        f = self.backbone(img)\n        if self.tab_enc is not None and tab is not None:\n            tabf = self.tab_enc(tab)\n            f = torch.cat([f, tabf], dim=1)\n        out = self.head(f)\n        return out\n\n# 原ModelEMA类保持不变\nclass ModelEMA:\n    def __init__(self, model: nn.Module, decay=CFG.ema_decay):\n        self.ema = self._clone_model(model).to(CFG.device)\n        self.decay = decay\n        self.num_updates = 0\n        for p in self.ema.parameters():\n            p.requires_grad_(False)\n\n    def _clone_model(self, model):\n        m = type(model)(\n            backbone_name=CFG.backbone,\n            pretrained=False,\n            num_tab=len(CFG.tabular_cols),\n            n_targets=len(CFG.target_cols)\n        )\n        m.load_state_dict(model.state_dict())\n        return m\n\n    def update(self, model):\n        self.num_updates += 1\n        decay = min(self.decay, (1 + self.num_updates) / (10 + self.num_updates))\n        msd = model.state_dict()\n        for k, v in self.ema.state_dict().items():\n            if v.dtype.is_floating_point:\n                v.copy_(v * decay + msd[k].detach().to(CFG.device) * (1. - decay))\n\n    def state_dict(self):\n        return self.ema.state_dict()\n\n# 原train_one_epoch函数保持不变\ndef train_one_epoch(model, loader, optimizer, loss_fn, device, scaler: GradScaler, ema: Optional[ModelEMA]=None):\n    model.train()\n    running_loss = 0.0\n    pbar = tqdm(loader, leave=False)\n    for imgs, tabs, targets in pbar:\n        imgs = imgs.to(device, non_blocking=True)\n        tabs = tabs.to(device, non_blocking=True) if tabs is not None else None\n        targets = targets.to(device, non_blocking=True)\n        \n        optimizer.zero_grad()\n        with autocast():\n            preds = model(imgs, tabs)\n            loss = loss_fn(preds, targets)\n        \n        scaler.scale(loss).backward()\n        scaler.step(optimizer)\n        scaler.update()\n        \n        if ema is not None:\n            ema.update(model)\n        \n        running_loss += loss.item() * imgs.size(0)\n        pbar.set_description(f\"tr_loss {running_loss/((pbar.n+1)*CFG.batch_size):.4f}\")\n    \n    return running_loss / len(loader.dataset)\n\n# 原valid_epoch函数保持不变\ndef valid_epoch(model, loader, device):\n    model.eval()\n    preds_list = []\n    targs_list = []\n    with torch.no_grad():\n        for imgs, tabs, targets in tqdm(loader, leave=False):\n            imgs = imgs.to(device, non_blocking=True)\n            tabs = tabs.to(device, non_blocking=True) if tabs is not None else None\n            p = model(imgs, tabs).cpu().numpy()\n            preds_list.append(p)\n            targs_list.append(targets.numpy())\n    \n    preds = np.vstack(preds_list)\n    targs = np.vstack(targs_list)\n    score = weighted_r2_strict(targs, preds)\n    return score, preds, targs\n\n# ========== 修改：prepare_dataframes函数（新增State和Species列处理） ==========\ndef prepare_dataframes():\n    \"\"\"准备训练数据和元数据（新增State和Species列处理）\"\"\"\n    # 读取长表训练数据并转换为宽表\n    long_train = pd.read_csv(CFG.train_long_csv)\n    wide = long_to_wide(long_train)\n    \n    # 从训练集中提取元数据（新增Species列）\n    train_csv_all = pd.read_csv(CFG.train_long_csv)\n    train_csv_all[['image_id','tname']] = train_csv_all['sample_id'].str.rsplit('__', n=1, expand=True)\n\n    # 填充缺失值\n    train_csv_all['State'] = train_csv_all['State'].fillna('Unknown').astype(str)\n    train_csv_all['Species'] = train_csv_all['Species'].fillna('Unknown').astype(str)\n    # WA站点物种合并\n    train_csv_all = merge_wa_species(train_csv_all)\n    \n    # 动态构建元数据列（新增Species）\n    possible_meta_cols = ['image_path', 'State', 'Species'] + CFG.tabular_cols\n    existing_meta_cols = [col for col in possible_meta_cols if col in train_csv_all.columns]\n    \n    # 提取唯一的元数据信息\n    if 'image_id' in train_csv_all.columns:\n        meta_train = train_csv_all[['image_id'] + existing_meta_cols].drop_duplicates(subset=['image_id'])\n    else:\n        meta_train = wide[['image_id']].copy()\n    \n    # 合并宽表目标值和元数据\n    df = wide.merge(meta_train, on='image_id', how='left')\n    \n    # 确保必要列存在\n    if 'image_path' not in df.columns:\n        df['image_path'] = df['image_id'].astype(str) + '.jpg'\n    for col in CFG.tabular_cols:\n        if col not in df.columns:\n            df[col] = 0.0\n    # 确保State和Species列存在\n    if 'State' not in df.columns:\n        df['State'] = 'Unknown'\n    if 'Species' not in df.columns:\n        df['Species'] = 'Unknown'\n    \n    return df\n\n# ========== 修改：run_cv函数（替换为自定义分层抽样） ==========\ndef run_cv(df: pd.DataFrame):\n    \"\"\"运行K折交叉验证（使用自定义三级分层抽样）\"\"\"\n    device = CFG.device\n    tab_proc = TabularProcessor(num_cols=CFG.tabular_cols, cat_cols=[])\n    tab_proc.fit(df)\n\n    # ========== 核心修改：生成三级分层组 ==========\n    df = create_custom_strat_groups(df)\n    oof_preds = np.zeros((len(df), len(CFG.target_cols)))\n\n    # 遍历每个折叠（使用自定义分层抽样）\n    for fold in range(CFG.n_folds):\n        print(f\"--- 第 {fold} 折 ---\")\n        # 自定义分层抽样\n        tr_idx, val_idx = custom_stratified_split(df, fold, CFG.seed)\n        # 验证三维分布\n        validate_3d_distribution(df, tr_idx, val_idx, fold)\n        \n        train_df = df.iloc[tr_idx].reset_index(drop=True)\n        val_df = df.iloc[val_idx].reset_index(drop=True)\n\n        # 目标值对数转换（原逻辑不变）\n        if CFG.log_transform:\n            train_df_tr = train_df.copy()\n            train_df_tr[CFG.target_cols] = np.log1p(train_df_tr[CFG.target_cols].values)\n        else:\n            train_df_tr = train_df.copy()\n\n        # 创建数据集和数据加载器（原逻辑不变）\n        train_ds = PastureDataset(\n            train_df_tr, CFG.img_dir_train,\n            tab_processor=tab_proc,\n            transforms=train_transforms,\n            is_test=False\n        )\n        val_ds = PastureDataset(\n            val_df, CFG.img_dir_train,\n            tab_processor=tab_proc,\n            transforms=valid_transforms,\n            is_test=False\n        )\n\n        train_loader = DataLoader(\n            train_ds, batch_size=CFG.batch_size,\n            shuffle=True, num_workers=CFG.num_workers,\n            pin_memory=True, drop_last=True\n        )\n        val_loader = DataLoader(\n            val_ds, batch_size=CFG.batch_size,\n            shuffle=False, num_workers=CFG.num_workers,\n            pin_memory=True\n        )\n\n        # 初始化模型（原逻辑不变）\n        model = MultimodalModel(\n            backbone_name=CFG.backbone,\n            pretrained=True,\n            num_tab=len(tab_proc.num_cols),\n            n_targets=len(CFG.target_cols)\n        ).to(device)\n        optimizer = torch.optim.AdamW(\n            model.parameters(),\n            lr=CFG.lr,\n            weight_decay=1e-5\n        )\n        scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(\n            optimizer, T_max=CFG.epochs\n        )\n        scaler = GradScaler()\n        ema = ModelEMA(model, decay=CFG.ema_decay)\n\n        loss_fn = nn.MSELoss()\n        best_score = -999\n        best_path = None\n\n        # 训练循环（原逻辑不变）\n        for epoch in range(1, CFG.epochs+1):\n            tr_loss = train_one_epoch(model, train_loader, optimizer, loss_fn, device, scaler, ema=ema)\n            scheduler.step()\n\n            # 验证主模型\n            val_score_model, preds_model_trans, _ = valid_epoch(model, val_loader, device)\n            if CFG.log_transform:\n                preds_model = np.expm1(preds_model_trans)\n            else:\n                preds_model = preds_model_trans\n            val_score_model = weighted_r2_strict(val_df[CFG.target_cols].values.astype(np.float32), preds_model)\n\n            # 验证EMA模型\n            ema.ema.to(device)\n            ema_preds = []\n            with torch.no_grad():\n                for imgs, tabs, targets in val_loader:\n                    imgs = imgs.to(device)\n                    tabs = tabs.to(device) if tabs is not None else None\n                    p = ema.ema(imgs, tabs).cpu().numpy()\n                    ema_preds.append(p)\n            ema_preds = np.vstack(ema_preds)\n            if CFG.log_transform:\n                ema_preds_inv = np.expm1(ema_preds)\n            else:\n                ema_preds_inv = ema_preds\n            ema_score = weighted_r2_strict(val_df[CFG.target_cols].values.astype(np.float32), ema_preds_inv)\n\n            print(f\"Epoch {epoch} | 训练损失: {tr_loss:.4f} | 模型R²: {val_score_model:.5f} | EMA R²: {ema_score:.5f}\")\n\n            if ema_score > best_score:\n                best_score = ema_score\n                best_path = os.path.join(CFG.exp_dir, f'model_fold{fold}_ema.pth')\n                torch.save(ema.state_dict(), best_path)\n\n        print(f\"第 {fold} 折完成，最佳EMA R²: {best_score:.5f}\")\n\n        # 加载最佳模型生成OOF预测（原逻辑不变）\n        state = torch.load(best_path, map_location=device)\n        inf_model = MultimodalModel(\n            backbone_name=CFG.backbone,\n            pretrained=False,\n            num_tab=len(tab_proc.num_cols),\n            n_targets=len(CFG.target_cols)\n        ).to(device)\n        inf_model.load_state_dict(state)\n        inf_model.eval()\n\n        preds_list = []\n        with torch.no_grad():\n            inf_loader = DataLoader(val_ds, batch_size=CFG.batch_size, shuffle=False, num_workers=CFG.num_workers)\n            for imgs, tabs, _ in inf_loader:\n                imgs = imgs.to(device)\n                tabs = tabs.to(device) if tabs is not None else None\n                p = inf_model(imgs, tabs).cpu().numpy()\n                preds_list.append(p)\n        preds = np.vstack(preds_list)\n        if CFG.log_transform:\n            preds = np.expm1(preds)\n        oof_preds[val_idx] = preds\n\n        # 清理内存\n        del model, inf_model, ema\n        gc.collect()\n        torch.cuda.empty_cache()\n\n    # 计算整体OOF分数\n    overall = weighted_r2_strict(df[CFG.target_cols].values.astype(np.float32), oof_preds)\n    print(f'所有折叠OOF加权R²: {overall:.5f}')\n    np.save(os.path.join(CFG.exp_dir, 'oof_preds.npy'), oof_preds)\n    return overall\n\n# 原tta_inference函数保持不变\ndef tta_inference(model_path: str, test_meta_csv: str, out_csv: str):\n    device = CFG.device\n    \n    train_df = prepare_dataframes()\n    tab_proc = TabularProcessor(num_cols=CFG.tabular_cols, cat_cols=[])\n    tab_proc.fit(train_df)\n    \n    meta_long = pd.read_csv(test_meta_csv)\n    meta_long[['image_id', 'target_name_tmp']] = meta_long['sample_id'].str.rsplit('__', n=1, expand=True)\n    cols_to_keep = ['image_id', 'image_path'] + CFG.tabular_cols\n    existing_cols = [col for col in cols_to_keep if col in meta_long.columns]\n    meta_wide = meta_long[existing_cols].drop_duplicates(subset=['image_id']).reset_index(drop=True)\n\n    for col in CFG.tabular_cols:\n        if col not in meta_wide.columns:\n            meta_wide[col] = 0.0\n            \n    meta = meta_wide.copy()\n\n    tab_proc = TabularProcessor(num_cols=CFG.tabular_cols)\n    train_long = pd.read_csv(CFG.train_long_csv)\n    wide_train = long_to_wide(train_long)\n\n    train_long[['image_id', 'target_name_tmp']] = train_long['sample_id'].str.rsplit('__', n=1, expand=True)\n\n    train_long['State'] = train_long['State'].fillna('Unknown').astype(str)\n    le = LabelEncoder()\n    train_long['State'] = le.fit_transform(train_long['State'])\n    \n    if os.path.exists(CFG.train_long_csv):\n        try:\n            train_all = pd.read_csv(CFG.train_long_csv)\n            train_all[['image_id', 'target_name_tmp']] = train_all['sample_id'].str.rsplit('__', n=1, expand=True)\n\n            train_all['State'] = train_all['State'].fillna('Unknown').astype(str)\n            le = LabelEncoder()\n            train_all['State'] = le.fit_transform(train_all['State'])\n            \n            required_meta_cols = ['image_id', 'State'] + CFG.tabular_cols\n            existing_meta_cols = [col for col in required_meta_cols if col in train_all.columns]\n            \n            if len(existing_meta_cols) > 1:\n                meta_train = train_all[['image_id'] + existing_meta_cols].drop_duplicates(subset=['image_id'])\n                wide_train = wide_train.merge(meta_train, on='image_id', how='left')\n        except Exception:\n            pass\n            \n    for col in CFG.tabular_cols:\n        if col not in wide_train.columns:\n            wide_train[col] = 0.0\n            \n    tab_proc.fit(wide_train)\n\n    model = MultimodalModel(\n        backbone_name=CFG.backbone,\n        pretrained=False,\n        num_tab=len(tab_proc.num_cols),\n        n_targets=len(CFG.target_cols)\n    ).to(device)\n    state = torch.load(model_path, map_location=device)\n    model.load_state_dict(state)\n    model.eval()\n\n    ds = PastureDataset(\n        meta, CFG.img_dir_test,\n        tab_processor=tab_proc,\n        transforms=valid_transforms,\n        is_test=True\n    )\n    loader = DataLoader(\n        ds, batch_size=CFG.batch_size,\n        shuffle=False, num_workers=CFG.num_workers\n    )\n\n    preds_list = []\n    with torch.no_grad():\n        for imgs, tabs, image_ids in tqdm(loader):\n            imgs = imgs.to(device)\n            tabs = tabs.to(device) if tabs is not None else None\n            p1 = model(imgs, tabs).cpu().numpy()\n            if CFG.tta:\n                imgs_flipped = torch.flip(imgs, dims=[3])\n                p2 = model(imgs_flipped, tabs).cpu().numpy()\n                p = (p1 + p2) / 2.0\n            else:\n                p = p1\n            preds_list.append(p)\n    \n    preds = np.vstack(preds_list)\n    if CFG.log_transform:\n        preds = np.expm1(preds)\n\n    rows = []\n    for i, r in meta.reset_index(drop=True).iterrows():\n        image_id = r['image_id']\n        for j, t in enumerate(CFG.target_cols):\n            rows.append({'sample_id': f'{image_id}__{t}', 'target': float(preds[i,j])})\n    sub = pd.DataFrame(rows)\n    sub.to_csv(out_csv, index=False)\n    print(f'提交文件已保存至: {out_csv}')\n\n# 原main函数保持不变\nif __name__ == '__main__':\n    print(f'使用设备: {CFG.device}')\n    test_model = MultimodalModel(pretrained=True).to(CFG.device)\n    first_conv_weight = test_model.backbone.stem[0].weight.data.mean().item()\n    print(f\"Backbone第一层卷积权重均值: {first_conv_weight:.6f}\")\n\n    df = prepare_dataframes()\n    print(f'数据准备完成，形状: {df.shape}')\n\n    overall = run_cv(df)\n    print(f'OOF分数: {overall:.5f}')\n\n    model_files = sorted(Path(CFG.exp_dir).glob('model_fold*_ema.pth'))\n    if len(model_files) == 0:\n        print('未找到模型文件，无法进行推理')\n    else:\n        model_path = str(model_files[-1])\n        out_csv = '/kaggle/working/submission.csv'\n        tta_inference(model_path, CFG.test_meta_csv, out_csv)\n        print('完成，提交文件已保存')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-18T02:41:43.209940Z","iopub.execute_input":"2025-12-18T02:41:43.210704Z","execution_failed":"2025-12-18T02:42:40.282Z"}},"outputs":[],"execution_count":null}]}