{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceId":112509,"databundleVersionId":14254895,"sourceType":"competition"},{"sourceId":13818387,"sourceType":"datasetVersion","datasetId":8758997},{"sourceId":4535,"sourceType":"modelInstanceVersion","modelInstanceId":3327,"modelId":986}],"dockerImageVersionId":31192,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import os\nos.environ['PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION'] = 'python'\n\nimport copy\nimport random\nimport time\nfrom pathlib import Path\nfrom typing import Dict, List, Tuple\n\nimport numpy as np\nimport pandas as pd\nimport json\nfrom PIL import Image\nimport matplotlib.pyplot as plt\n\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch import optim\nfrom torch.utils.data import Dataset, DataLoader\nfrom torch.amp import autocast, GradScaler\nimport torchvision.transforms as transforms\nfrom torch.optim.lr_scheduler import CosineAnnealingLR\n\nfrom transformers import AutoImageProcessor, AutoModel\n\nfrom sklearn.metrics import r2_score\nfrom sklearn.model_selection import StratifiedGroupKFold\nfrom sklearn.preprocessing import LabelEncoder","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-11-21T14:52:23.910740Z","iopub.execute_input":"2025-11-21T14:52:23.910901Z","iopub.status.idle":"2025-11-21T14:52:51.338025Z","shell.execute_reply.started":"2025-11-21T14:52:23.910885Z","shell.execute_reply":"2025-11-21T14:52:51.337353Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def seed_everything(seed: int = 2025):\n    \"\"\"设置随机种子\"\"\"\n    random.seed(seed)\n    np.random.seed(seed)\n    torch.manual_seed(seed)\n    torch.cuda.manual_seed_all(seed)\n    os.environ['PYTHONHASHSEED'] = str(seed)","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-11-21T14:52:51.339525Z","iopub.execute_input":"2025-11-21T14:52:51.340068Z","iopub.status.idle":"2025-11-21T14:52:51.343970Z","shell.execute_reply.started":"2025-11-21T14:52:51.340048Z","shell.execute_reply":"2025-11-21T14:52:51.343148Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class BiomassConfig:\n\"\"\"配置参数类\"\"\"\n\n    def __init__(self):\n        # 基础设置\n        self.seed = 91792  # 最优划分种子\n        self.device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n    \n        # 数据路径\n        self.train_csv = '/kaggle/input/csiro-biomass/train.csv'\n        self.train1_xlsx = '/kaggle/input/train1/train1.xlsx'\n        self.teacher_pred_xlsx = '/kaggle/input/train1/teacher_prediction5.xlsx'\n        self.test_csv = '/kaggle/input/csiro-biomass/test.csv'\n        self.root_image_dir = '/kaggle/input/csiro-biomass'\n    \n        # 模型路径\n        self.model_path = '/kaggle/input/dinov2/pytorch/large/1/'\n    \n        # 训练参数\n        self.batch_size = 16  # 减小batch size以适应ViT\n        self.num_epochs = 40\n        self.fc_lr = 1e-2\n        self.weight_decay = 1e-4\n        self.num_workers = 4\n        \n        # loss权重\n        self.loss_weights = [1.0, 1.0, 1.0, 5.0, 2.0]\n        self.beta = 0.2  # 物理约束损失系数\n    \n        # 图像处理 - 适应ViT输入尺寸\n        self.hidden_dim = 1024\n        self.image_size = 224  # ViT标准输入尺寸\n        self.normalize_mean = [0.485, 0.456, 0.406]\n        self.normalize_std = [0.229, 0.224, 0.225]\n    \n        # 模型参数\n        self.backbone = 'dinov2_vit_large'\n        self.num_targets = 5\n    \n        # 教师预测损失系数与温度\n        self.T = 3.0\n        self.alpha = 0.8\n        \n        # 交叉验证\n        self.n_folds = 5\n        self.use_cross_validation = True\n    \n        # 混合精度训练\n        self.use_amp = True\n    \n        # TTA (测试时增强)\n        self.use_tta = True\n        self.tta_scales = [1.0]\n        self.tta_flip = True  # 水平翻转\n        self.tta_vflip = True # 垂直翻转\n    \n        # 输出权重\n        self.target_weights = [0.1, 0.1, 0.1, 0.5, 0.2]  # Green, Clover, Dead, Total, GDM\n    \n        # 输出目录\n        self.output_dir = '.'\n        os.makedirs(self.output_dir, exist_ok=True)","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-11-21T14:52:51.344516Z","iopub.execute_input":"2025-11-21T14:52:51.344767Z","iopub.status.idle":"2025-11-21T14:52:51.363200Z","shell.execute_reply.started":"2025-11-21T14:52:51.344729Z","shell.execute_reply":"2025-11-21T14:52:51.362522Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class FeatureExtractor:\n    \"\"\"特征提取器\"\"\"\n    \n    def __init__(self, config: BiomassConfig, df_imgs: pd.DataFrame, image_root: str):\n        self.config = config\n        self.device = self.config.device\n        self.df = df_imgs\n        self.root = Path(image_root)\n        self.image_paths = [self.root/row for row in self.df]\n        self.backbone = AutoModel.from_pretrained(self.config.model_path).to(self.device)\n        \n        # 完全冻结backbone\n        for param in self.backbone.parameters():\n            param.requires_grad = False\n        self.backbone.eval()\n        \n        # 图像预处理\n        self.transform = transforms.Compose([\n            transforms.Resize((self.config.image_size, self.config.image_size)),\n            transforms.ToTensor(),\n            transforms.Normalize(mean=self.config.normalize_mean, \n                               std=self.config.normalize_std)\n        ])\n            \n    def extract_features(self):\n        \"\"\"批量提取图像特征\"\"\"\n        print(\"Feature extraction start...\")\n        features_list = []\n        \n        with torch.no_grad():\n            for i in range(0, len(self.image_paths), self.config.batch_size):\n                batch_paths = self.image_paths[i:i+self.config.batch_size]\n                batch_images = []\n                \n                # 加载和预处理图像\n                for path in batch_paths:\n                    image = Image.open(path).convert('RGB')\n                    image = self.transform(image)\n                    batch_images.append(image)\n                \n                # 堆叠批次\n                batch_tensor = torch.stack(batch_images).to(self.device)\n                \n                # 提取特征\n                outputs = self.backbone(batch_tensor)\n                \n                # 使用[CLS] token的特征\n                if hasattr(outputs, 'last_hidden_state'):\n                    features = outputs.last_hidden_state[:, 0, :]\n                elif hasattr(outputs, 'pooler_output'):\n                    features = outputs.pooler_output\n                else:\n                    features = outputs.last_hidden_state.mean(dim=1)\n                \n                features_list.append(features.cpu())\n        \n        all_features = torch.cat(features_list, dim=0)\n        print(\"Feature extraction done.\")\n        return all_features","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-21T14:52:51.363878Z","iopub.execute_input":"2025-11-21T14:52:51.364138Z","iopub.status.idle":"2025-11-21T14:52:51.381360Z","shell.execute_reply.started":"2025-11-21T14:52:51.364121Z","shell.execute_reply":"2025-11-21T14:52:51.380769Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class BiomassDataset(Dataset):\n    \"\"\"生物量数据集类\"\"\"\n\n    def __init__(self, features: torch.Tensor, y_data: torch.Tensor = None, \n                 y_data_soft: torch.Tensor = None, is_train: bool = True,\n                 image_paths: list = None, indices: list = None):\n        \"\"\"\n        Args:\n            features: 预提取的特征张量 [num_samples, hidden_dim]\n            y_data: 硬标签\n            y_data_soft: 软标签  \n            is_train: 是否为训练模式\n            image_paths: 图像路径列表\n            indices: 指定使用的索引\n        \"\"\"\n        self.is_train = is_train\n        self.image_paths = image_paths\n        self.indices = indices\n        \n        # 如果指定了索引，只使用这些索引对应的特征和标签\n        if self.indices is not None:\n            self.features = features[self.indices]\n            self.y_data = y_data[self.indices] if y_data is not None else None\n            self.y_data_soft = y_data_soft[self.indices] if y_data_soft is not None else None\n            self.image_paths = [image_paths[i] for i in self.indices] if image_paths is not None else None\n        else:\n            self.features = features\n            self.y_data = y_data\n            self.y_data_soft = y_data_soft\n\n    def __len__(self):\n        return len(self.features)\n\n    def __getitem__(self, idx):\n        feature = self.features[idx]\n        \n        if self.is_train and self.y_data is not None and self.y_data_soft is not None:\n            label = self.y_data[idx]\n            label_soft = self.y_data_soft[idx]\n            return feature, label, label_soft\n        elif self.is_train and self.y_data is not None and self.y_data_soft is None:\n            label = self.y_data[idx]\n            return feature, label\n        else:\n            image_path = self.image_paths[idx] if self.image_paths is not None else \"\"\n            return feature, image_path","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-11-21T14:52:51.381999Z","iopub.execute_input":"2025-11-21T14:52:51.382225Z","iopub.status.idle":"2025-11-21T14:52:51.397827Z","shell.execute_reply.started":"2025-11-21T14:52:51.382209Z","shell.execute_reply":"2025-11-21T14:52:51.397196Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class TestDataset(Dataset):\n    \"\"\"测试数据集类\"\"\"\n\n    def __init__(self, features: torch.Tensor, df_imgs: pd.DataFrame = None):\n        \"\"\"\n        Args:\n            features: 预提取的特征张量 [num_samples, hidden_dim]\n            df_imgs: 数据框（可选，用于获取image_path）\n        \"\"\"\n        self.features = features\n        self.df = df_imgs.reset_index(drop=True) if df_imgs is not None else None\n\n    def __len__(self):\n        return len(self.features)\n\n    def __getitem__(self, idx):\n        feature = self.features[idx]\n        if self.df is not None:\n            row = self.df.iloc[idx]\n            return feature, row['image_path']\n        else:\n            return feature","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-11-21T14:52:51.398419Z","iopub.execute_input":"2025-11-21T14:52:51.398712Z","iopub.status.idle":"2025-11-21T14:52:51.414268Z","shell.execute_reply.started":"2025-11-21T14:52:51.398690Z","shell.execute_reply":"2025-11-21T14:52:51.413607Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class FeatureAttention(nn.Module):\n    \"\"\"一个适用于一维特征的注意力模块，类似于SEBlock\"\"\"\n    def __init__(self, channel, reduction=16):\n        super().__init__()\n        self.fc = nn.Sequential(\n            nn.Linear(channel, channel // reduction, bias=False),\n            nn.GELU(),\n            nn.Linear(channel // reduction, channel, bias=False),\n            nn.Sigmoid()\n        )\n\n    def forward(self, x):\n        # 为每个样本学习一个通道权重\n        weights = self.fc(x)\n        return x * weights\n        \nclass BiomassModel(nn.Module):\n    \"\"\"生物量预测模型\"\"\"\n\n    def __init__(self, config: BiomassConfig):\n        super().__init__()\n        self.config = config\n        self.device = config.device\n        self.fc = self._build_fc_layer()\n        self.img_attention = FeatureAttention(self.config.hidden_dim)\n        \n        print(f\"Model initialized with {sum(p.numel() for p in self.parameters()):,} trainable parameters\")\n\n    def _build_fc_layer(self):\n        hidden_dim1 = 512\n        hidden_dim2 = 256\n        hidden_dim3 = 128\n        dropout_rate = 0.4\n        \n        fc_layers = nn.Sequential(\n            # 第一层\n            nn.Linear(self.config.hidden_dim, hidden_dim1),\n            nn.BatchNorm1d(hidden_dim1),\n            nn.GELU(),\n            nn.Dropout(dropout_rate),\n            \n            # 第二层\n            nn.Linear(hidden_dim1, hidden_dim2),\n            nn.BatchNorm1d(hidden_dim2),\n            nn.GELU(),\n            nn.Dropout(dropout_rate),\n            \n            # 第三层\n            nn.Linear(hidden_dim2, hidden_dim3),\n            nn.BatchNorm1d(hidden_dim3),\n            nn.GELU(),\n            nn.Dropout(dropout_rate * 0.5),\n            \n            # 输出层\n            nn.Linear(hidden_dim3, self.config.num_targets)\n        )\n        \n        # 初始化权重\n        self._init_fc_weights(fc_layers)\n        return fc_layers\n\n    def _init_fc_weights(self, model):\n        for m in model.modules():\n            if isinstance(m, nn.Linear):\n                nn.init.xavier_uniform_(m.weight)\n                if m.bias is not None:\n                    nn.init.constant_(m.bias, 0)\n            elif isinstance(m, nn.BatchNorm1d):\n                nn.init.constant_(m.weight, 1)\n                nn.init.constant_(m.bias, 0)\n\n    def forward(self, x):\n        attended_img_features = self.img_attention(x)\n        output = self.fc(attended_img_features)\n        output = F.relu(output)\n        \n        return output","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-21T14:52:51.416361Z","iopub.execute_input":"2025-11-21T14:52:51.416616Z","iopub.status.idle":"2025-11-21T14:52:51.433229Z","shell.execute_reply.started":"2025-11-21T14:52:51.416590Z","shell.execute_reply":"2025-11-21T14:52:51.432465Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class WeightedMSELoss(nn.Module):\n    \"\"\"加权MSE\"\"\"\n    \n    def __init__(self, config: BiomassConfig):\n        super().__init__()\n        self.config = config\n        self.weights = torch.tensor(self.config.loss_weights)\n\n    def forward(self, input, target):\n        mse_per_dim = (input - target) ** 2\n\n        if self.weights.device != input.device:\n            self.weights = self.weights.to(input.device)\n\n        weighted_mse = mse_per_dim * self.weights.view(1, -1)\n\n        return weighted_mse.mean()","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-11-21T14:52:51.433902Z","iopub.execute_input":"2025-11-21T14:52:51.434137Z","iopub.status.idle":"2025-11-21T14:52:51.444110Z","shell.execute_reply.started":"2025-11-21T14:52:51.434120Z","shell.execute_reply":"2025-11-21T14:52:51.443557Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class BiomassTrainer:\n    \"\"\"生物量训练器\"\"\"\n\n    def __init__(self, config: BiomassConfig):\n        self.config = config\n        self.device = config.device\n        self.scaler = GradScaler('cuda') if config.use_amp else None\n        self.log_path = f'./log_{config.backbone}_fc_{config.num_epochs}.txt'\n        self.features = None\n        self.train_loader = None # 用于训练集回测\n        self.target = None # 用于训练集回测\n        self.models = []\n        self.model_r2 = []\n\n        # 训练历史记录\n        self.history = {\n            'train_loss': [], 'val_loss': [], 'r2_scores': [],\n            'r2_total': [], 'epoch_time': [], 'fold': []\n        }\n\n        seed_everything(config.seed)\n\n    def prepare_data(self) -> Tuple[torch.Tensor, DataLoader]:\n        \"\"\"准备数据\"\"\"\n        # 读取训练数据\n        data = pd.read_csv(self.config.train_csv)\n        df_img = data['image_path'].drop_duplicates().reset_index(drop=True)\n        data1 = pd.read_excel(self.config.train1_xlsx)\n        data_soft = pd.read_excel(self.config.teacher_pred_xlsx)\n        y_data = torch.reshape(\n            torch.tensor(data.iloc[:, -1]),\n            (int(len(data) / 5), 5)\n        )\n        self.target = y_data.numpy()\n        \n        y_data_soft = torch.reshape(\n            torch.tensor(data_soft.iloc[:, -1]),\n            (int(len(data_soft) / 5), 5)\n        )\n        state_encoder = LabelEncoder()\n        date_encoder = LabelEncoder()\n    \n        y = torch.tensor(state_encoder.fit_transform(data1['State'])).to(torch.int)\n        groups = torch.tensor(date_encoder.fit_transform(data1['Sampling_Date'])).to(torch.int)\n\n        feature_extractor = FeatureExtractor(self.config, df_img, self.config.root_image_dir)\n        self.features = feature_extractor.extract_features()\n\n        # 创建数据集和数据加载器\n        train_dataset = BiomassDataset(\n            features=self.features,\n            y_data=y_data,\n            y_data_soft=y_data_soft,\n            is_train=True,\n        )\n        \n        train_loader = DataLoader(\n            train_dataset,\n            batch_size=self.config.batch_size,\n            shuffle=True,\n            num_workers=self.config.num_workers\n        )\n\n        self.train_loader = DataLoader(\n            train_dataset,\n            batch_size=self.config.batch_size,\n            shuffle=False,\n            num_workers=self.config.num_workers\n        )\n\n        return y_data, y_data_soft, y, groups, train_loader\n\n    def create_model_and_optimizer(self) -> Tuple[nn.Module, optim.Optimizer]:\n        \"\"\"创建模型和优化器\"\"\"\n        model = BiomassModel(self.config).to(self.device)\n\n        # 设置不同的学习率\n        # 分离backbone参数和分类头参数\n        backbone_params = []\n        fc_params = []\n        \n        for name, param in model.named_parameters():\n            fc_params.append(param)\n\n        optimizer = optim.AdamW([\n            {'params': fc_params, 'lr': self.config.fc_lr, 'weight_decay': self.config.weight_decay}\n        ])\n\n        scheduler = CosineAnnealingLR(optimizer, T_max=15)\n\n        return model, optimizer, scheduler\n\n    def calculate_metrics(self, predictions: np.ndarray, targets: np.ndarray) -> Tuple[List[float], float]:\n        \"\"\"计算评估指标\"\"\"\n        # 计算每个目标的R²，仅用于观察拟合效果\n        r2_scores = []\n        weights = self.config.target_weights\n        num_targets = self.config.num_targets\n\n        predictions = np.log1p(predictions)\n        targets = np.log1p(targets)\n\n        for i in range(5):\n            r2 = r2_score(targets[:, i], predictions[:, i])\n            r2_scores.append(r2)\n\n        # 计算加权总分\n        SS_res = sum([sum(weights[i]*((predictions[:, i]-targets[:, i])**2)) for i in range(num_targets)])\n        weighted_y_sum = sum([sum(weights[i]*targets[:, i]) for i in range(num_targets)]) / len(targets)\n        SS_tot = sum([sum(weights[i]*((targets[:, i]-weighted_y_sum)**2)) for i in range(num_targets)])\n        r2_total = 1 - SS_res / SS_tot\n\n        return r2_scores, r2_total\n\n    def train_epoch(self, model: nn.Module, train_loader: DataLoader,\n                    optimizer: optim.Optimizer, criterion: nn.Module) -> float:\n        \"\"\"训练一个epoch\"\"\"\n        model.train()\n        total_loss = 0\n        num_batches = 0\n        \n        # 确定是否使用scaler（仅在CUDA + AMP时使用）\n        use_scaler = self.config.use_amp and self.device.type == 'cuda'\n        \n        for batch_x, batch_y, batch_y_soft in train_loader:\n            batch_x = batch_x.to(self.device)\n            batch_y = batch_y.to(self.device)\n            batch_y_soft = batch_y_soft.to(self.device)\n            \n            optimizer.zero_grad(set_to_none=True)\n            \n            autocast_device_type = 'cuda' if self.device.type == 'cuda' else 'cpu'\n            with autocast(device_type=autocast_device_type, enabled=self.config.use_amp):\n                output = model(batch_x)\n                loss_main = criterion(output, batch_y.float())\n                loss_physics1 = nn.MSELoss()(output[:, 3], output[:, 0]+output[:, 1]+output[:, 2])\n                loss_physics2 = nn.MSELoss()(output[:, 4], output[:, 0]+output[:, 2])\n                loss_soft = criterion(output / self.config.T, (batch_y_soft / self.config.T).float())\n                \n                loss = loss_main + self.config.alpha * loss_soft + self.config.beta * (loss_physics1 + loss_physics2)\n\n            if use_scaler:\n                # CUDA + AMP路径\n                self.scaler.scale(loss).backward()\n                self.scaler.unscale_(optimizer)\n                torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)\n                self.scaler.step(optimizer)\n                self.scaler.update()\n            else:\n                # CPU / MPS / AMP=False路径\n                loss.backward()\n                torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)\n                optimizer.step()\n            \n            total_loss += loss.item()\n            num_batches += 1\n        \n        return total_loss / max(1, num_batches)\n\n    @torch.no_grad()\n    def validate(self, model: nn.Module, data_loader: DataLoader) -> Tuple[float, List[float], float]:\n        \"\"\"验证模型\"\"\"\n        model.eval()\n        all_predictions = []\n        all_targets = []\n        all_targets_soft = []\n\n        for batch_x, batch_y, batch_y_soft in data_loader:\n            batch_x = batch_x.to(self.device)\n            output = model(batch_x)\n\n            all_predictions.append(output.cpu().numpy())\n            all_targets.append(batch_y.cpu().numpy())\n            all_targets_soft.append(batch_y_soft.cpu().numpy())\n\n        all_predictions = np.concatenate(all_predictions, axis=0)\n        all_targets = np.concatenate(all_targets, axis=0)\n        all_targets_soft = np.concatenate(all_targets_soft, axis=0)\n        \n        r2_scores, r2_total = self.calculate_metrics(all_predictions, all_targets)\n        loss_main = ((all_predictions - all_targets) ** 2 * self.config.loss_weights).mean()\n        loss_physics1 = ((all_predictions[:,3] - all_predictions[:,0] - \n                          all_predictions[:,1] - all_predictions[:,2]) ** 2).mean()\n        loss_physics2 = ((all_predictions[:,4] - all_predictions[:,0] - all_predictions[:,2]) ** 2).mean()\n        loss_soft = (((all_predictions - all_targets_soft) / self.config.T) ** 2 * self.config.loss_weights).mean()\n        loss = loss_main + self.config.alpha * loss_soft + self.config.beta * (loss_physics1 + loss_physics2)\n\n        return loss, r2_scores, r2_total\n\n    def cross_validation(self, y_data: torch.Tensor, y_data_soft: torch.Tensor, y: torch.Tensor, \n                         groups: np.array) -> Dict:\n        \"\"\"执行交叉验证\"\"\"\n        print(\"Starting Cross-Validation...\")\n\n        # 创建分组\n        n_samples = len(y_data)\n\n        sgkf = StratifiedGroupKFold(n_splits=self.config.n_folds, shuffle=True)\n        sgkf.get_n_splits(np.arange(n_samples), y)\n        fold_results = {}\n\n        for fold, (train_idx, val_idx) in enumerate(sgkf.split(np.arange(n_samples), y, groups)):\n            print(f\"\\n=== Fold {fold + 1}/{self.config.n_folds} ===\")\n                        \n            train_dataset = BiomassDataset(\n                features=self.features,\n                y_data=y_data,\n                y_data_soft=y_data_soft,\n                is_train=True,\n                indices=train_idx.tolist()\n            )\n            val_dataset = BiomassDataset(\n                features=self.features,\n                y_data=y_data,\n                y_data_soft=y_data_soft,\n                is_train=True,\n                indices=val_idx.tolist()\n            )\n\n            train_loader = DataLoader(\n                train_dataset, batch_size=self.config.batch_size, shuffle=True,\n                num_workers=self.config.num_workers\n            )\n            val_loader = DataLoader(\n                val_dataset, batch_size=self.config.batch_size, shuffle=False,\n                num_workers=self.config.num_workers\n            )\n\n            # 训练模型\n            fold_history, model, optimizer = self.train_single_fold(fold, train_loader, val_loader)\n            fold_results[fold+1] = fold_history\n\n            # 保存模型\n            model_path = os.path.join(\n                self.config.output_dir, f'fc_fold_{fold+1}.pth'\n            )\n            torch.save({\n                'model_state_dict': model.state_dict(),\n                'optimizer_state_dict': optimizer.state_dict()\n            }, model_path)\n        \n        with open(self.log_path, 'w') as f:\n            json.dump(fold_results, f, ensure_ascii=False, indent=2)\n        return fold_results\n\n    def train_single_fold(self, fold: int, train_loader: DataLoader,\n                          val_loader: DataLoader = None) -> Dict:\n        \"\"\"训练单个折痕\"\"\"\n        model, optimizer, scheduler = self.create_model_and_optimizer()\n        criterion = WeightedMSELoss(self.config)\n\n        fold_history = {\n            'train_loss': [], 'val_loss': [], 'r2_scores': [],\n            'r2_total': [], 'epoch_time': [], 'best_val_loss': 2e3,\n            'best_epoch': 0, 'best_r2': -1\n        }\n\n        best_model_state = None\n\n        for epoch in range(1, self.config.num_epochs + 1):\n            start_time = time.time()\n            scheduler.step()\n\n            # 训练\n            train_loss = self.train_epoch(model, train_loader, optimizer, criterion)\n\n            # 验证\n            if val_loader is not None:\n                val_loss, r2_scores, r2_total = self.validate(model, val_loader)\n            else:\n                val_loss, r2_scores, r2_total = self.validate(model, train_loader)\n\n            epoch_time = time.time() - start_time\n\n            # 记录历史\n            fold_history['train_loss'].append(train_loss)\n            fold_history['val_loss'].append(val_loss)\n            fold_history['r2_scores'].append(r2_scores)\n            fold_history['r2_total'].append(r2_total)\n            fold_history['epoch_time'].append(epoch_time)\n\n            if val_loss < fold_history['best_val_loss']:\n                fold_history['best_val_loss'] = val_loss\n                fold_history['best_r2'] = r2_total\n                fold_history['best_epoch'] = epoch\n                # 保存模型状态字典（深拷贝）\n                best_model_state = copy.deepcopy(model.state_dict())\n                print(f\"🎯 Fold {fold+1} - 新的最佳模型! val loss: {val_loss:.4f}, r2: {r2_total:.4f} (Epoch {epoch})\")\n\n            print(f\"Fold {fold+1}, Epoch {epoch}/{self.config.num_epochs}: \"\n                  f\"Train Loss: {train_loss:.4f}, Val Loss: {val_loss:.4f}, \"\n                  f\"R²: {[round(r, 4) for r in r2_scores]}, \"\n                  f\"R² Total: {r2_total:.4f}, Time: {epoch_time:.2f}s\")\n\n        if best_model_state is not None:\n            model.load_state_dict(best_model_state)\n            print(f\"✅ Fold {fold+1} - 已加载最佳模型 (Epoch {fold_history['best_epoch']}, val loss: {fold_history['best_val_loss']:.4f}, r2: {fold_history['best_r2']:.4f})\")\n        else:\n            print(f\"⚠️ Fold {fold+1} - 警告: 未找到最佳模型，使用最终epoch的模型\")\n\n        self.models.append(model)\n        self.model_r2.append(fold_history['best_r2'])\n        return fold_history, model, optimizer\n\n    def train(self) -> Dict:\n        \"\"\"训练主函数\"\"\"\n        y_data, y_data_soft, y, groups, train_loader = self.prepare_data()\n\n        if self.config.use_cross_validation:\n            return self.cross_validation(y_data, y_data_soft, y, groups)\n        else:\n            return self.train_single_fold(0, train_loader)\n\n    def predict_with_tta(self, model: nn.Module, dataloader: DataLoader) -> np.ndarray:\n        \"\"\"使用TTA进行预测\"\"\"\n        model.eval()\n        all_predictions = []\n\n        with torch.no_grad():\n            for batch in dataloader:\n                if isinstance(batch, list):\n                    batch_x = batch[0].to(self.device)\n                else:\n                    batch_x = batch.to(self.device)\n\n                # 多尺度 + 翻转 TTA\n                tta_predictions = []\n\n                for scale in self.config.tta_scales:\n                    # 缩放\n                    if scale != 1.0:\n                        new_size = int(self.config.image_size * scale)\n                        batch_scaled = F.interpolate(\n                            batch_x, size=(new_size, new_size),\n                            mode='bilinear', align_corners=False\n                        )\n                        # 中心裁剪\n                        batch_scaled = transforms.CenterCrop(\n                            self.config.image_size\n                        )(batch_scaled)\n                    else:\n                        batch_scaled = batch_x\n\n                    # 原始预测\n                    pred = model(batch_scaled)\n                    tta_predictions.append(pred)\n\n                    # 水平翻转\n                    if self.config.tta_flip:\n                        batch_flipped = torch.flip(batch_scaled, dims=[3])\n                        pred_flipped = model(batch_flipped)\n                        tta_predictions.append(pred_flipped)\n\n                    # 垂直翻转\n                    if self.config.tta_vflip:  \n                        batch_vflipped = torch.flip(batch_scaled, dims=[2])\n                        pred_vflipped = model(batch_vflipped)\n                        tta_predictions.append(pred_vflipped)\n\n                # 平均所有TTA预测\n                final_pred = torch.stack(tta_predictions).mean(dim=0)\n                all_predictions.append(final_pred.cpu().numpy())\n\n        return np.concatenate(all_predictions, axis=0)\n\n    def predict(self, test_loader: DataLoader, model_paths: List[str] = None) -> np.ndarray:\n        \"\"\"预测函数\"\"\"\n        all_predictions = []\n        lowest_r2_idx = np.argmin(self.model_r2)\n        \n        for i, model in enumerate(self.models):\n            if True:\n                if self.config.use_tta:\n                    predictions = self.predict_with_tta(model, test_loader)\n                else:\n                    model.eval()\n                    predictions = []\n                    with torch.no_grad():\n                        for batch in test_loader:\n                            if isinstance(batch, list):\n                                batch_x = batch[0].to(self.device)\n                            else:\n                                batch_x = batch.to(self.device)\n                            output = model(batch_x)\n                            predictions.append(output.cpu().numpy())\n                    predictions = np.concatenate(predictions, axis=0)\n    \n                all_predictions.append(predictions)\n            else:\n                pass\n                                    \n        # 平均所有模型的预测\n        if all_predictions:\n            return np.mean(all_predictions, axis=0)\n        else:\n            raise ValueError(\"No valid models found for prediction\")\n\n    def plot_training_history(self, history: Dict, save_path: str = None):\n        \"\"\"绘制训练历史（示例，以第1折为例）\"\"\"\n        fig, axes = plt.subplots(2, 2, figsize=(15, 10))\n\n        # 训练损失\n        axes[0, 0].plot(history[1].get('train_loss', []))\n        axes[0, 0].set_title('Training Loss')\n        axes[0, 0].set_xlabel('Epoch')\n        axes[0, 0].set_ylabel('Loss')\n\n        # 验证损失\n        axes[0, 1].plot(history[1].get('val_loss', []))\n        axes[0, 1].set_title('Validation Loss')\n        axes[0, 1].set_xlabel('Epoch')\n        axes[0, 1].set_ylabel('Loss')\n\n        # R²总分\n        axes[1, 0].plot(history[1].get('r2_total', []))\n        axes[1, 0].set_title('R² Total Score')\n        axes[1, 0].set_xlabel('Epoch')\n        axes[1, 0].set_ylabel('R²')\n\n        #  epoch时间\n        axes[1, 1].plot(history[1].get('epoch_time', []))\n        axes[1, 1].set_title('Epoch Time')\n        axes[1, 1].set_xlabel('Epoch')\n        axes[1, 1].set_ylabel('Time (s)')\n\n        plt.tight_layout()\n\n        if save_path:\n            plt.savefig(save_path)\n        plt.show()","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-11-21T14:52:51.444901Z","iopub.execute_input":"2025-11-21T14:52:51.445199Z","iopub.status.idle":"2025-11-21T14:52:51.485837Z","shell.execute_reply.started":"2025-11-21T14:52:51.445169Z","shell.execute_reply":"2025-11-21T14:52:51.485091Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 创建配置\nconfig = BiomassConfig()\n\n# 创建训练器\ntrainer = BiomassTrainer(config)","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-11-21T14:52:51.486534Z","iopub.execute_input":"2025-11-21T14:52:51.486789Z","iopub.status.idle":"2025-11-21T14:52:51.506051Z","shell.execute_reply.started":"2025-11-21T14:52:51.486772Z","shell.execute_reply":"2025-11-21T14:52:51.505214Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 训练模型\nhistory = trainer.train()","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-11-21T14:52:51.506617Z","iopub.execute_input":"2025-11-21T14:52:51.506860Z","iopub.status.idle":"2025-11-21T14:56:31.549360Z","shell.execute_reply.started":"2025-11-21T14:52:51.506842Z","shell.execute_reply":"2025-11-21T14:56:31.548488Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 绘制训练历史并保存\ntrainer.plot_training_history(history)","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-11-21T14:56:31.550451Z","iopub.execute_input":"2025-11-21T14:56:31.550764Z","iopub.status.idle":"2025-11-21T14:56:32.238754Z","shell.execute_reply.started":"2025-11-21T14:56:31.550715Z","shell.execute_reply":"2025-11-21T14:56:32.237938Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 训练数据回测\ntl = trainer.train_loader\ntg = trainer.target\npred_retest = trainer.predict(tl)\nr2_scores, r2_total = trainer.calculate_metrics(pred_retest, tg)\n\nprint(f\"训练数据回测结果：r2_scores:{[round(r,4) for r in r2_scores]}, r2_total:{r2_total:.4f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-21T14:56:32.239468Z","iopub.execute_input":"2025-11-21T14:56:32.239729Z","iopub.status.idle":"2025-11-21T14:56:33.411078Z","shell.execute_reply.started":"2025-11-21T14:56:32.239702Z","shell.execute_reply":"2025-11-21T14:56:33.410121Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 测试数据预测\ndf_test = pd.read_csv(config.test_csv)\ndf_imgs = df_test['image_path'].drop_duplicates().reset_index(drop=True)\n\nfeature_extractor = FeatureExtractor(config, df_imgs, config.root_image_dir)\nfeatures = feature_extractor.extract_features()\ntest_dataset = TestDataset(features)\ntest_loader = DataLoader(test_dataset, batch_size=config.batch_size,\n                         shuffle=False, num_workers=config.num_workers)\n\npredictions = trainer.predict(test_loader)","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-11-21T14:56:33.412298Z","iopub.execute_input":"2025-11-21T14:56:33.412895Z","iopub.status.idle":"2025-11-21T14:56:35.725165Z","shell.execute_reply.started":"2025-11-21T14:56:33.412867Z","shell.execute_reply":"2025-11-21T14:56:35.724018Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 提交文件\nflattened_predictions = predictions.flatten().tolist()\ndf_test['target'] = flattened_predictions\ndf_test.drop(columns=['image_path', 'target_name'], inplace=True)\ndf_test.to_csv('submission.csv', index=False)\n\nprint(\"Training and prediction completed!\")","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-11-21T14:56:35.726488Z","iopub.execute_input":"2025-11-21T14:56:35.727164Z","iopub.status.idle":"2025-11-21T14:56:35.743957Z","shell.execute_reply.started":"2025-11-21T14:56:35.727136Z","shell.execute_reply":"2025-11-21T14:56:35.743187Z"}},"outputs":[],"execution_count":null}]}