{"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":4535,"sourceType":"modelInstanceVersion","isSourceIdPinned":false,"modelInstanceId":3327,"modelId":986}],"dockerImageVersionId":31236,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# version_optimized.py\n# 基于 DINOv2 + LightGBM 的高效生物量预测方案\n# 核心思路：利用冻结的 SOTA 视觉大模型提取特征，使用 GBDT 进行快速回归\n\nimport os\nimport sys\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\nimport lightgbm as lgb\nfrom sklearn.preprocessing import StandardScaler\nfrom sklearn.model_selection import KFold\nfrom sklearn.multioutput import MultiOutputRegressor\n\n# 屏蔽警告\nwarnings.filterwarnings('ignore')\nos.environ[\"USE_TF\"] = \"0\"\n\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    dinov2_paths = [\n        './dinov2_large',                                            # 本地路径\n        '/kaggle/input/dinov2/pytorch/large/1',                      # Kaggle 常用路径\n        '/kaggle/input/facebook-dinov2-large'                        # 备选路径\n    ]\n    \n    img_size = 224 # DINOv2 标准输入尺寸\n    batch_size = 32 # 特征提取时的批次大小\n    \n    seed = 42\n    n_fold = 5\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\ndef weighted_r2_metric(y_true, y_pred):\n    \"\"\"比赛评估指标: 加权 R2\"\"\"\n    weights = np.array(CFG.target_weights)\n    # 确保维度匹配\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    \"\"\"加载并转换数据为宽表格式\"\"\"\n    train_df = pd.read_csv(CFG.train_path)\n    test_df = pd.read_csv(CFG.test_path)\n    \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    # 透视表转换\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\nclass BiomassDataset(Dataset):\n    def __init__(self, df, transform=None):\n        self.image_paths = df['image_path'].values\n        self.transform = transform\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:\n            image = self.transform(image)\n        return image\n\n# ====================================================\n# 特征提取 (DINOv2)\n# ====================================================\ndef get_dinov2_model():\n    \"\"\"加载 DINOv2 模型 (优先尝试 Transformers，失败则回退到 torch.hub)\"\"\"\n    \n    # 1. 尝试从本地路径加载 (Transformers)\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                    print(\"Trying fallback methods...\")\n\n    # 2. 尝试 torch.hub 加载 (无需 transformers 库，更稳定)\n    print(\"Trying to load via torch.hub (facebookresearch/dinov2)...\")\n    try:\n        # 优先尝试本地缓存\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    # 3. 最后尝试在线下载 (Transformers)\n    if AutoModel is not None:\n        print(\"Warning: Local DINOv2 not found. Trying to download (transformers)...\")\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. Please check paths or internet connection.\")\n\ndef extract_features(df, model):\n    \"\"\"批量提取特征 (Batch Processing + TTA)\"\"\"\n    print(f\"Extracting features for {len(df)} images...\")\n    \n    # DINOv2 标准化参数\n    mean = [0.485, 0.456, 0.406]\n    std = [0.229, 0.224, 0.225]\n    \n    transforms_list = [\n        T.Compose([\n            T.Resize(256), T.CenterCrop(224), \n            T.ToTensor(), T.Normalize(mean, std)\n        ]),\n        T.Compose([\n            T.Resize(256), T.CenterCrop(224), \n            T.RandomHorizontalFlip(p=1.0), \n            T.ToTensor(), T.Normalize(mean, std)\n        ])\n    ]\n    \n    # 判断模型类型 (Transformers vs TorchHub)\n    is_transformer_model = hasattr(model, 'last_hidden_state') or hasattr(model, 'embeddings')\n    # TorchHub 模型直接返回 output tensor，或者 class token + patch tokens\n    # 我们需要根据实际输出做适配\n    \n    feats_tta = []\n    \n    for i, transform in enumerate(transforms_list):\n        ds = BiomassDataset(df, transform=transform)\n        ld = DataLoader(ds, batch_size=CFG.batch_size, shuffle=False, num_workers=2)\n        \n        feats_epoch = []\n        with torch.no_grad():\n            for imgs in tqdm(ld, desc=f\"TTA Round {i+1}\"):\n                imgs = imgs.to(CFG.device)\n                \n                if is_transformer_model:\n                    # Transformers 库模型\n                    outputs = model(imgs)\n                    # last_hidden_state: (B, L, D), ignore CLS (idx 0)\n                    emb = outputs.last_hidden_state[:, 1:, :].mean(dim=1)\n                else:\n                    # TorchHub 模型 (forward_features 返回 dict 或 tensor)\n                    # DINOv2 repo forward_features 返回 {'x_norm_clstoken': ..., 'x_norm_patchtokens': ...}\n                    outputs = model.forward_features(imgs)\n                    if isinstance(outputs, dict):\n                        # 取 patch tokens 平均\n                        patch_tokens = outputs['x_norm_patchtokens']\n                        emb = patch_tokens.mean(dim=1)\n                    else:\n                        # 某些版本可能直接返回\n                        emb = outputs.mean(dim=1) \n                        \n                feats_epoch.append(emb.cpu().numpy())\n        \n        feats_tta.append(np.concatenate(feats_epoch))\n    \n    # 平均 TTA 结果\n    final_feats = np.mean(feats_tta, axis=0)\n    \n    # 转换为 DataFrame\n    feat_cols = [f\"feat_{i}\" for i in range(final_feats.shape[1])]\n    return pd.DataFrame(final_feats, columns=feat_cols)\n\n# ====================================================\n# 主流程\n# ====================================================\ndef main():\n    print(\"Initializing optimized pipeline...\")\n    \n    # 1. 数据准备\n    train_df, test_df = load_data()\n    print(f\"Train shape: {train_df.shape}, Test shape: {test_df.shape}\")\n    \n    # 2. 特征提取\n    model = get_dinov2_model()\n    \n    # 检查是否已经有提取好的特征缓存 (可选，这里略过)\n    X_train = extract_features(train_df, model)\n    X_test = extract_features(test_df, model)\n    \n    # 释放显存\n    del model\n    torch.cuda.empty_cache()\n    gc.collect()\n    \n    # 3. 预处理 (标准化 + Log变换)\n    print(\"Preprocessing features...\")\n    scaler = StandardScaler()\n    X_train_scaled = scaler.fit_transform(X_train)\n    X_test_scaled = scaler.transform(X_test)\n    \n    Y_train = train_df[CFG.target_cols].values\n    Y_train_log = np.log1p(Y_train) # Log1p 变换\n    \n    # 4. 训练与验证 (LightGBM)\n    print(\"Training LightGBM models...\")\n    \n    # LightGBM 参数 (参考 lzx_version1)\n    lgbm_params = {\n        'n_estimators': 200,\n        'learning_rate': 0.05,\n        'num_leaves': 31,\n        'random_state': CFG.seed,\n        'n_jobs': -1,\n        'verbose': -1\n    }\n    \n    kf = KFold(n_splits=CFG.n_fold, shuffle=True, random_state=CFG.seed)\n    \n    oof_preds = np.zeros(Y_train.shape)\n    test_preds = np.zeros((len(test_df), 5))\n    \n    # 对每个目标单独训练 (MultiOutputRegressor 逻辑的手动实现，便于监控)\n    models = []\n    \n    # 这里我们使用 MultiOutputRegressor 简化代码\n    estimator = MultiOutputRegressor(lgb.LGBMRegressor(**lgbm_params))\n    \n    # 交叉验证\n    fold_scores = []\n    \n    for fold, (train_idx, val_idx) in enumerate(kf.split(X_train_scaled, Y_train_log)):\n        X_tr, X_val = X_train_scaled[train_idx], X_train_scaled[val_idx]\n        y_tr, y_val = Y_train_log[train_idx], Y_train_log[val_idx]\n        \n        estimator.fit(X_tr, y_tr)\n        \n        val_pred_log = estimator.predict(X_val)\n        val_pred = np.expm1(val_pred_log).clip(min=0) # 还原\n        \n        oof_preds[val_idx] = val_pred\n        y_val_orig = np.expm1(y_val)\n        \n        score = weighted_r2_metric(y_val_orig, val_pred)\n        fold_scores.append(score)\n        print(f\"Fold {fold+1} Weighted R2: {score:.4f}\")\n        \n        # 对测试集推理 (平均)\n        test_pred_log = estimator.predict(X_test_scaled)\n        test_preds += np.expm1(test_pred_log).clip(min=0) / CFG.n_fold\n        \n    print(f\"\\nCV Average Score: {np.mean(fold_scores):.4f}\")\n    print(f\"OOF Score: {weighted_r2_metric(Y_train, oof_preds):.4f}\")\n    \n    # 5. 后处理与提交\n    print(\"Generating submission...\")\n    \n    # 构造预测 DataFrame\n    df_sub = test_df.copy()\n    df_sub[CFG.target_cols] = test_preds\n    \n    # 物理约束修正\n    df_sub = post_process_biomass(df_sub)\n    \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()\n","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2026-01-05T02:50:09.037881Z","iopub.execute_input":"2026-01-05T02:50:09.038679Z","iopub.status.idle":"2026-01-05T02:51:23.475632Z","shell.execute_reply.started":"2026-01-05T02:50:09.038642Z","shell.execute_reply":"2026-01-05T02:51:23.474693Z"}},"outputs":[],"execution_count":null}]}