{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.11","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceId":91844,"databundleVersionId":11361821,"sourceType":"competition"}],"dockerImageVersionId":31012,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import os\nimport logging\nimport random\nimport gc\nimport time\nimport cv2\nimport math\nimport warnings\nfrom pathlib import Path\n\nimport numpy as np\nimport pandas as pd\nfrom sklearn.model_selection import StratifiedKFold\nfrom sklearn.metrics import roc_auc_score\nimport librosa\n\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nimport torch.optim as optim\nfrom torch.optim import lr_scheduler\nfrom torch.utils.data import Dataset, DataLoader\n\nimport matplotlib.pyplot as plt\nimport seaborn as sns\nfrom tqdm.auto import tqdm\n\nimport timm\n\nwarnings.filterwarnings(\"ignore\")\nlogging.basicConfig(level=logging.ERROR)","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-05-13T07:06:49.755500Z","iopub.execute_input":"2025-05-13T07:06:49.755780Z","iopub.status.idle":"2025-05-13T07:07:03.143357Z","shell.execute_reply.started":"2025-05-13T07:06:49.755760Z","shell.execute_reply":"2025-05-13T07:07:03.142744Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\nclass CFG:\n\n    train_audio_path = '/kaggle/input/birdclef-2025/train_audio/' # 你的训练音频文件存放路径\n    train_metadata_csv = '/kaggle/input/birdclef-2025/train.csv' # 包含音频文件名和对应鸟类标签的CSV文件路径\n\n    # 2. 物种分类文件 (用于获取类别总数和标签映射)\n    taxonomy_csv = '/kaggle/input/birdclef-2025/taxonomy.csv'\n\n    # 3. 你训练好的模型的保存路径 (检查点)\n    output_model_dir = '/kaggle/working/models/' # 或者其他任何你有写入权限的目录\n                                                 # 每一折或每一个epoch的模型都会保存在这里\n  # --- 音频和梅尔频谱图参数 (通常和推理时保持一致) ---\n    FS = 32000  \n    WINDOW_SIZE = 5  \n    \n    N_FFT = 1024\n    HOP_LENGTH = 64\n    N_MELS = 136\n    FMIN = 20\n    FMAX = 16000\n    TARGET_SHAPE = (256, 256)\n    # --- 模型架构 ---\n    model_name = 'efficientnet_b0'\n    in_channels = 1 # 输入通道数 (1代表单通道梅尔频谱图)\n    # 是否使用 ImageNet 预训练权重作为你骨干网络的起点\n    # 这对于迁移学习通常是有益的\n    pretrained_on_imagenet = True # 设置为 True 来加载 ImageNet 权重到骨干网络\n    \n    # --- 训练超参数 ---\n    # 1. 通用设置\n    device = 'cuda' if torch.cuda.is_available() else 'cpu' # 如果有GPU则使用GPU\n    seed = 42               # 用于保证实验可复现的随机种子\n    num_epochs = 10         # 总训练轮次\n    train_batch_size = 32   # 训练批次大小\n    valid_batch_size = 64   # 验证批次大小 (通常可以设大一些，因为验证时没有反向传播)\n\n    # 2. 优化器\n    optimizer_name = 'AdamW' # 例如: 'Adam', 'AdamW', 'SGD'\n    learning_rate = 1e-3     # 学习率\n    weight_decay = 1e-5      # 权重衰减 (用于像 AdamW 这样的优化器)\n\n    # 3. 学习率调度器 (可选，但通常有帮助)\n    scheduler_name = 'CosineAnnealingLR' # 例如: 'StepLR', 'ReduceLROnPlateau', 'CosineAnnealingLR'\n    lr_scheduler_params = {  # 学习率调度器的具体参数\n        'T_max': num_epochs, # 对于 CosineAnnealingLR\n        'eta_min': 1e-6      # 对于 CosineAnnealingLR\n    }\n    # 或者对于 StepLR: {'step_size': 10, 'gamma': 0.1}\n    if num_epochs != 50: # 如果 num_epochs 被修改，需要确保 T_max 也更新\n        lr_scheduler_params['T_max'] = num_epochs\n    # 4. 损失函数\n    loss_fn_name = 'CrossEntropyLoss' # 如果你的标签是每个样本一个鸟类整数ID，就用这个\n                                      # 如果一个声音里可能有多种鸟 (多标签)，可能用 'BCEWithLogitsLoss'\n    # --- 数据处理与验证 ---\n    num_workers = 2         # DataLoader 使用的工作进程数\n    # K折交叉验证 (在竞赛中很常见)\n    n_folds = 5             # 总共分几折\n    current_fold_to_train = 0 # 当前训练的是第几折 (从0到 n_folds-1)\n\n    # --- 数据增强 (可选, 用于训练数据) ---\n    # 你可以在这里定义音频数据增强的参数，例如:\n    # use_noise_injection = True  # 是否使用噪声注入\n    # noise_level = 0.005         # 噪声水平\n    # use_random_shift = True     # 是否使用随机时间平移\n    # 等等...\n\n    # --- 日志与模型保存 ---\n    print_freq_epochs = 1   # 每隔多少轮打印一次训练日志\n    save_best_model_only = True # 只保存验证集上表现最好的模型\n\n    # --- 用于调试 (只处理一小部分数据) ---\n    debug_mode = False\n    debug_subset_size = 100 # 如果 debug_mode 为 True, 使用的样本数量\n\ncfg = CFG()\n\ncfg.lr_scheduler_params = {\n    'T_max': cfg.num_epochs,\n    'eta_min': 1e-6\n}\n\nprint(f\"--- CFG 实例化后 ---\")\nprint(f\"torch.cuda.is_available(): {torch.cuda.is_available()}\")\nprint(f\"cfg.device 设置为: {cfg.device}\")\n# 下面这两行先注释掉，因为 model 还没定义\n# print(f\"模型已移动到设备: {cfg.device}\")\n# print(f\"训练将在设备: {cfg.device} 上进行\")\nprint(f\"梅尔频谱图参数: N_FFT={cfg.N_FFT}, HOP_LENGTH={cfg.HOP_LENGTH}, N_MELS={cfg.N_MELS}\")\nprint(f\"目标图像形状: {cfg.TARGET_SHAPE}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-13T07:07:03.144632Z","iopub.execute_input":"2025-05-13T07:07:03.144928Z","iopub.status.idle":"2025-05-13T07:07:03.245420Z","shell.execute_reply.started":"2025-05-13T07:07:03.144883Z","shell.execute_reply":"2025-05-13T07:07:03.244658Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def set_seed(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    torch.cuda.manual_seed_all(seed) # if use multi-GPU","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-13T07:07:03.246325Z","iopub.execute_input":"2025-05-13T07:07:03.246666Z","iopub.status.idle":"2025-05-13T07:07:03.271689Z","shell.execute_reply.started":"2025-05-13T07:07:03.246646Z","shell.execute_reply":"2025-05-13T07:07:03.270820Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"set_seed(cfg.seed)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-13T07:07:03.273755Z","iopub.execute_input":"2025-05-13T07:07:03.274019Z","iopub.status.idle":"2025-05-13T07:07:03.290332Z","shell.execute_reply.started":"2025-05-13T07:07:03.274000Z","shell.execute_reply":"2025-05-13T07:07:03.289574Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 创建模型保存目录 (如果不存在)\nos.makedirs(cfg.output_model_dir, exist_ok=True)\nprint(f\"模型将保存在: {cfg.output_model_dir}\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-13T07:07:03.291247Z","iopub.execute_input":"2025-05-13T07:07:03.291503Z","iopub.status.idle":"2025-05-13T07:07:03.296237Z","shell.execute_reply.started":"2025-05-13T07:07:03.291480Z","shell.execute_reply":"2025-05-13T07:07:03.295580Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# --- 步骤 2: 数据准备 ---\n\n# 2.1. 加载元数据和物种分类信息，确定 num_classes\nprint(\"\\n--- 开始数据准备 ---\")\ntry:\n    train_df = pd.read_csv(cfg.train_metadata_csv)\n    print(f\"成功加载训练元数据: {cfg.train_metadata_csv}, 形状: {train_df.shape}\")\nexcept FileNotFoundError:\n    print(f\"错误: 训练元数据文件未找到于 {cfg.train_metadata_csv}\")\n    # 在这里你可以停止执行，或者创建一个空的DataFrame来避免后续代码出错（但不推荐）\n    # exit() # 或者 raise FileNotFoundError\n    train_df = pd.DataFrame() # 仅为避免直接报错\n\ntry:\n    taxonomy_df = pd.read_csv(cfg.taxonomy_csv)\n    print(f\"成功加载物种分类文件: {cfg.taxonomy_csv}, 形状: {taxonomy_df.shape}\")\nexcept FileNotFoundError:\n    print(f\"错误: 物种分类文件未找到于 {cfg.taxonomy_csv}\")\n    taxonomy_df = pd.DataFrame() # 仅为避免直接报错\n\nif not train_df.empty and not taxonomy_df.empty and 'primary_label' in taxonomy_df.columns:\n    unique_labels = sorted(taxonomy_df['primary_label'].unique())\n    num_classes = len(unique_labels)\n    print(f\"总共的鸟类类别数量 (num_classes): {num_classes}\")\n\n    label_to_int = {label: i for i, label in enumerate(unique_labels)}\n    int_to_label = {i: label for i, label in enumerate(unique_labels)}\n\n    if 'primary_label' in train_df.columns:\n        train_df['label_id'] = train_df['primary_label'].map(label_to_int)\n        train_df.dropna(subset=['label_id'], inplace=True) # 移除没有对应 label_id 的行\n        train_df['label_id'] = train_df['label_id'].astype(int)\n        print(f\"处理后 train_df 中有效数据条数: {len(train_df)}\")\n    else:\n        print(f\"警告: train_df 中缺少 'primary_label' 列。\")\nelse:\n    print(\"错误或警告: 无法确定 num_classes，因为元数据或物种分类文件加载失败或缺少关键列。\")\n    num_classes = -1 # 表示错误状态\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-13T07:07:03.296959Z","iopub.execute_input":"2025-05-13T07:07:03.297131Z","iopub.status.idle":"2025-05-13T07:07:03.505870Z","shell.execute_reply.started":"2025-05-13T07:07:03.297116Z","shell.execute_reply":"2025-05-13T07:07:03.505215Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 2.2. K 折交叉验证切分\nif not train_df.empty and 'label_id' in train_df.columns:\n    skf = StratifiedKFold(n_splits=cfg.n_folds, shuffle=True, random_state=cfg.seed)\n    train_df['fold'] = -1 # 初始化fold列\n    for fold_num, (train_idx, val_idx) in enumerate(skf.split(train_df, train_df['label_id'])):\n        train_df.loc[val_idx, 'fold'] = fold_num\n    train_df['fold'] = train_df['fold'].astype(int)\n    print(f\"数据已划分为 {cfg.n_folds} 折。\")\n\n    current_train_df = train_df[train_df.fold != cfg.current_fold_to_train].reset_index(drop=True)\n    current_valid_df = train_df[train_df.fold == cfg.current_fold_to_train].reset_index(drop=True)\n    print(f\"当前训练折: {cfg.current_fold_to_train}\")\n    print(f\"训练集大小: {len(current_train_df)}, 验证集大小: {len(current_valid_df)}\")\nelse:\n    print(\"警告: 由于 train_df 为空或缺少 'label_id'，无法进行 K 折切分。\")\n    # 创建空的DataFrame以避免后续代码出错，但训练无法进行\n    current_train_df = pd.DataFrame()\n    current_valid_df = pd.DataFrame()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-13T07:07:03.506682Z","iopub.execute_input":"2025-05-13T07:07:03.506996Z","iopub.status.idle":"2025-05-13T07:07:03.539868Z","shell.execute_reply.started":"2025-05-13T07:07:03.506972Z","shell.execute_reply":"2025-05-13T07:07:03.539073Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 2.3. 创建自定义 PyTorch Dataset 类 (这里的代码和我上次提供的一样)\nclass BirdSoundDataset(Dataset):\n    def __init__(self, df, cfg, audio_path_base, num_classes_fallback, augmentations=None, is_training=True): # 添加 num_classes_fallback\n        self.df = df\n        self.cfg = cfg\n        self.audio_path_base = audio_path_base\n        self.augmentations = augmentations\n        self.is_training = is_training\n        if not df.empty and 'filename' in df.columns and 'label_id' in df.columns:\n            self.filenames = df['filename'].values\n            self.labels = df['label_id'].values\n        else: # 处理空的或不完整的DataFrame\n            self.filenames = []\n            self.labels = []\n        self.num_classes_fallback = num_classes_fallback # 用于标签是-1的情况\n\n    def __len__(self):\n        return len(self.filenames) # 使用 self.filenames 的长度\n\n    def __getitem__(self, idx):\n        if self.filenames.size == 0: # 如果没有文件名，返回占位符\n            dummy_spec = np.zeros(self.cfg.TARGET_SHAPE, dtype=np.float32)\n            # 对于标签，如果类别数已知，我们可以返回一个有效的类别（比如0），或者一个特殊值\n            # 这里返回一个在类别范围内的值，或者如果num_classes_fallback是-1，就返回0\n            dummy_label_val = 0 if self.num_classes_fallback == -1 else (self.num_classes_fallback -1 if self.num_classes_fallback > 0 else 0)\n            return torch.tensor(dummy_spec).unsqueeze(0), torch.tensor(dummy_label_val, dtype=torch.long)\n\n        filename = self.filenames[idx]\n        audio_file_path = os.path.join(self.audio_path_base, filename)\n\n        try:\n            y, sr = librosa.load(audio_file_path, sr=self.cfg.FS, mono=True)\n        except Exception as e:\n            print(f\"错误: 无法加载音频文件 {audio_file_path}: {e}\")\n            dummy_spec = np.zeros(self.cfg.TARGET_SHAPE, dtype=np.float32)\n            # 返回一个在类别范围内的标签，或者如果 num_classes_fallback 是-1，就返回0\n            # 避免标签是-1导致 CrossEntropyLoss 出错\n            error_label_val = 0 if self.num_classes_fallback == -1 else (self.num_classes_fallback -1 if self.num_classes_fallback > 0 else 0)\n            return torch.tensor(dummy_spec).unsqueeze(0), torch.tensor(error_label_val, dtype=torch.long)\n\n        target_samples = int(self.cfg.WINDOW_SIZE * self.cfg.FS)\n        current_samples = len(y)\n\n        if current_samples > target_samples:\n            if self.is_training:\n                start = random.randint(0, current_samples - target_samples)\n            else:\n                start = (current_samples - target_samples) // 2\n            y_segment = y[start : start + target_samples]\n        elif current_samples < target_samples:\n            y_segment = np.pad(y, (0, target_samples - current_samples), 'constant')\n        else:\n            y_segment = y\n\n        # (可选音频增强)\n        # if self.augmentations:\n        # y_segment = self.augmentations(samples=y_segment, sample_rate=self.cfg.FS)\n\n        melspec = librosa.feature.melspectrogram(\n            y=y_segment, sr=self.cfg.FS, n_fft=self.cfg.N_FFT,\n            hop_length=self.cfg.HOP_LENGTH, n_mels=self.cfg.N_MELS,\n            fmin=self.cfg.FMIN, fmax=self.cfg.FMAX\n        )\n        melspec_db = librosa.power_to_db(melspec, ref=np.max)\n        norm_melspec = (melspec_db - melspec_db.min()) / (melspec_db.max() - melspec_db.min() + 1e-8)\n\n        if norm_melspec.shape != self.cfg.TARGET_SHAPE:\n            resized_melspec = cv2.resize(norm_melspec, self.cfg.TARGET_SHAPE, interpolation=cv2.INTER_LINEAR)\n        else:\n            resized_melspec = norm_melspec\n\n        image = np.expand_dims(resized_melspec, axis=0)\n        label = torch.tensor(self.labels[idx], dtype=torch.long)\n        image_tensor = torch.tensor(image, dtype=torch.float32)\n\n        return image_tensor, label","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-13T07:07:03.540920Z","iopub.execute_input":"2025-05-13T07:07:03.541209Z","iopub.status.idle":"2025-05-13T07:07:03.553836Z","shell.execute_reply.started":"2025-05-13T07:07:03.541189Z","shell.execute_reply":"2025-05-13T07:07:03.553232Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 实例化 Dataset\n# 确保 current_train_df 和 current_valid_df 不是空的，并且 num_classes 是有效值\nif not current_train_df.empty and not current_valid_df.empty and num_classes > 0:\n    train_dataset = BirdSoundDataset(current_train_df, cfg, cfg.train_audio_path, num_classes, is_training=True)\n    valid_dataset = BirdSoundDataset(current_valid_df, cfg, cfg.train_audio_path, num_classes, is_training=False)\n    print(\"Dataset 实例化成功。\")\nelse:\n    print(\"警告: 由于数据帧为空或num_classes无效，Dataset 可能未正确实例化或为空。\")\n    # 创建空的Dataset避免后续代码直接报错，但训练无法进行\n    train_dataset = BirdSoundDataset(pd.DataFrame(columns=['filename', 'label_id']), cfg, cfg.train_audio_path, num_classes if num_classes > 0 else 1, is_training=True)\n    valid_dataset = BirdSoundDataset(pd.DataFrame(columns=['filename', 'label_id']), cfg, cfg.train_audio_path, num_classes if num_classes > 0 else 1, is_training=False)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-13T07:07:03.554456Z","iopub.execute_input":"2025-05-13T07:07:03.554633Z","iopub.status.idle":"2025-05-13T07:07:03.571295Z","shell.execute_reply.started":"2025-05-13T07:07:03.554611Z","shell.execute_reply":"2025-05-13T07:07:03.570620Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 2.4. 创建 PyTorch DataLoader\nif len(train_dataset) > 0 and len(valid_dataset) > 0 : # 确保dataset不是空的\n    train_loader = DataLoader(\n        train_dataset, batch_size=cfg.train_batch_size, shuffle=True,\n        num_workers=cfg.num_workers, pin_memory=True, drop_last=True\n    )\n    valid_loader = DataLoader(\n        valid_dataset, batch_size=cfg.valid_batch_size, shuffle=False,\n        num_workers=cfg.num_workers, pin_memory=True, drop_last=False\n    )\n    print(\"DataLoader 实例化成功。\")\nelse:\n    print(\"警告: 由于 Dataset 为空，无法创建 DataLoader。训练无法进行。\")\n\nprint(\"\\n--- 数据准备阶段结束 ---\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-13T07:07:03.573611Z","iopub.execute_input":"2025-05-13T07:07:03.573804Z","iopub.status.idle":"2025-05-13T07:07:03.588882Z","shell.execute_reply.started":"2025-05-13T07:07:03.573789Z","shell.execute_reply":"2025-05-13T07:07:03.588289Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# --- 步骤 3: 模型定义 ---\nprint(\"\\n--- 开始模型定义 ---\")\n\n# 确保 num_classes 是在数据准备步骤中正确计算出来的\n# 如果你是在不同的单元格中运行，需要确保 num_classes 在当前作用域是可访问的\n# 例如，它可以是全局变量，或者你从上一个单元格的输出中获取它。\n# 这里我们假设 num_classes 变量已经存在并且包含了正确的鸟类类别数量。\nif 'num_classes' not in globals() or num_classes <= 0:\n    print(\"错误: 'num_classes' 未定义或无效。请确保在数据准备步骤中已正确计算。\")\n    # 你可能需要从上一个单元格重新获取或设置它，例如：\n    # if not taxonomy_df.empty and 'primary_label' in taxonomy_df.columns:\n    #     unique_labels = sorted(taxonomy_df['primary_label'].unique())\n    #     num_classes = len(unique_labels)\n    # else:\n    #     num_classes = 264 # 或者一个默认的回退值，但这不推荐用于实际训练\n    #     print(f\"警告: 使用回退的 num_classes = {num_classes}\")\n    # 为了继续，我们假设一个值，但实际中你需要确保它是正确的\n    if 'num_classes' not in globals() or num_classes <= 0: # 再次检查\n        if not isinstance(num_classes, int) or num_classes <=0 : # 确保 num_classes 是正整数\n             print(\"错误：num_classes 不是一个有效的正整数。无法继续定义模型。\")\n             # exit() # 如果在脚本中，可以退出\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-13T07:07:03.589492Z","iopub.execute_input":"2025-05-13T07:07:03.589648Z","iopub.status.idle":"2025-05-13T07:07:03.605761Z","shell.execute_reply.started":"2025-05-13T07:07:03.589636Z","shell.execute_reply":"2025-05-13T07:07:03.605235Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class BirdSoundModelTrain(nn.Module):\n    def __init__(self, cfg_config, num_classes_model, pretrained_imagenet=True):\n        super().__init__()\n        self.cfg = cfg_config # 可以直接用 cfg，避免命名冲突\n        self.num_classes = num_classes_model\n\n        # 1. 创建骨干网络 (Backbone)\n        #    使用 cfg.model_name, cfg.in_channels\n        #    pretrained_imagenet 参数决定是否加载 ImageNet 权重\n        self.backbone = timm.create_model(\n            self.cfg.model_name,\n            pretrained=pretrained_imagenet, # 使用传入的参数\n            in_chans=self.cfg.in_channels,\n            drop_rate=0.0,       # 通常在训练初期或微调时，可以从0开始，或根据需要调整\n            drop_path_rate=0.0   # 同上\n            # num_classes=0 # 另一种移除原始分类头的方式，timm会返回不带分类头的特征\n        )\n\n        # 2. 获取骨干网络的输出特征维度并移除/替换原始分类器\n        #    这里的逻辑和你推理脚本中的类似\n        if 'efficientnet' in self.cfg.model_name:\n            backbone_out_features = self.backbone.classifier.in_features\n            self.backbone.classifier = nn.Identity() # 替换为空操作层\n        elif 'resnet' in self.cfg.model_name or 'resnext' in self.cfg.model_name:\n            backbone_out_features = self.backbone.fc.in_features\n            self.backbone.fc = nn.Identity()\n        # 为其他可能的timm模型添加通用处理 (如果需要)\n        # 例如，对于 Vision Transformer (vit) 系列:\n        # elif 'vit' in self.cfg.model_name:\n        #     backbone_out_features = self.backbone.head.in_features\n        #     self.backbone.head = nn.Identity()\n        else:\n            # 尝试一个更通用的方法获取分类器特征数并移除分类器\n            try:\n                backbone_out_features = self.backbone.get_classifier().in_features\n                self.backbone.reset_classifier(0, '') # num_classes=0, global_pool=''\n            except AttributeError:\n                # 如果上面的方法失败，可能需要针对特定模型系列添加处理\n                # 或者检查timm模型是如何命名的分类层\n                print(f\"警告: 无法自动确定模型 '{self.cfg.model_name}' 的分类器输出特征数或移除分类器。\")\n                print(f\"请检查timm库中该模型的结构，或手动指定backbone_out_features。\")\n                # 你可能需要硬编码backbone_out_features，例如对于EfficientNet-B0是1280\n                if self.cfg.model_name == 'efficientnet_b0':\n                    backbone_out_features = 1280 # EfficientNet-B0 的特征数\n                    if hasattr(self.backbone, 'classifier'):\n                         self.backbone.classifier = nn.Identity()\n                    else:\n                         print(f\"错误：EfficientNet-B0 模型没有 'classifier' 属性。\")\n                else:\n                    raise ValueError(f\"无法处理模型 {self.cfg.model_name} 的分类头，请手动适配。\")\n\n\n        # 3. 添加全局平均池化层 (或其它池化方式)\n        self.pooling = nn.AdaptiveAvgPool2d(output_size=1)\n        # AdaptiveAvgPool2d(1) 会将 HxW 的特征图转换为 1x1\n\n        # 4. 添加新的分类器 (全连接层)\n        self.classifier = nn.Linear(backbone_out_features, self.num_classes)\n\n    def forward(self, x):\n        print(f\"Input x shape: {x.shape}\")\n        features = self.backbone(x)\n        print(f\"After backbone, features shape: {features.shape}\")\n\n        # 检查 features 是否已经是 2D (N, C)\n        if features.ndim == 2: # 例如 (batch_size, num_features)\n            # 如果已经是2D，说明timm模型内部可能已经做了池化和展平\n            # 此时，我们不需要再进行 pooling 操作\n            flattened_features = features\n            print(f\"Features were 2D, using as flattened_features. Shape: {flattened_features.shape}\")\n        elif features.ndim == 4: # (batch_size, num_features, H_feat, W_feat)\n            pooled_features = self.pooling(features)\n            print(f\"After pooling, pooled_features shape: {pooled_features.shape}\")\n\n            flattened_features = torch.flatten(pooled_features, start_dim=1)\n            print(f\"After flatten, flattened_features shape: {flattened_features.shape}\")\n        else:\n            raise ValueError(f\"Unexpected features dimension: {features.ndim}. Shape: {features.shape}\")\n\n        # 在这里检查 flattened_features 的形状是否是 (batch_size, 1280)\n        if flattened_features.shape[1] != self.classifier.in_features: # self.classifier.in_features 应该是 1280\n            print(f\"CRITICAL WARNING: flattened_features.shape[1] ({flattened_features.shape[1]}) \"\n                  f\"does not match self.classifier.in_features ({self.classifier.in_features})!\")\n            # 这里可以引发错误或尝试调整，但最好是找出根本原因\n\n        logits = self.classifier(flattened_features)\n        print(f\"Output logits shape: {logits.shape}\")\n        return logits\n        \n# 实例化模型\n# 确保 num_classes 是有效的\nif 'num_classes' in globals() and isinstance(num_classes, int) and num_classes > 0:\n    model = BirdSoundModelTrain(cfg_config=cfg,\n                                num_classes_model=num_classes,\n                                pretrained_imagenet=cfg.pretrained_on_imagenet)\n    model.to(cfg.device) # 将模型移动到指定设备\n    print(f\"模型 {cfg.model_name} 已成功定义并移动到设备: {cfg.device}\")\n    # (可选) 打印模型结构，检查是否符合预期\n    # print(model)\nelse:\n    print(\"错误: 由于 num_classes 无效，模型未实例化。请返回数据准备步骤检查。\")\n\n\n# (可选) 测试模型是否能处理一个伪造的输入批次\nif 'model' in globals() and 'train_loader' in globals() and len(train_loader) > 0 :\n    try:\n        print(\"\\n测试模型前向传播...\")\n        dummy_images, dummy_labels = next(iter(train_loader)) # 从 DataLoader 取一个批次\n        dummy_images = dummy_images.to(cfg.device)\n        dummy_labels = dummy_labels.to(cfg.device)\n\n        print(f\"伪造输入图像形状: {dummy_images.shape}\") # 应该是 (batch_size, in_channels, H, W)\n        print(f\"伪造输入标签形状: {dummy_labels.shape}\") # 应该是 (batch_size)\n\n        with torch.no_grad(): # 在测试时不计算梯度\n            model.eval() # 设置为评估模式 (主要影响 Dropout, BatchNorm 等)\n            output_logits = model(dummy_images)\n            model.train() # 切换回训练模式\n\n        print(f\"模型输出 (logits) 的形状: {output_logits.shape}\") # 应该是 (batch_size, num_classes)\n        assert output_logits.shape == (cfg.train_batch_size, num_classes), \"模型输出形状与预期不符！\"\n        print(\"模型前向传播测试成功！\")\n    except Exception as e:\n        print(f\"模型前向传播测试失败: {e}\")\n        print(\"请检查：\")\n        print(\"1. DataLoader 是否能正确输出数据。\")\n        print(\"2. 模型定义的输入输出维度是否正确。\")\n        print(f\"3. num_classes ({num_classes if 'num_classes' in globals() else '未定义'}) 是否与 DataLoader 中的标签对应。\")\n\nprint(\"\\n--- 模型定义阶段结束 ---\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-13T07:07:03.606786Z","iopub.execute_input":"2025-05-13T07:07:03.607491Z","iopub.status.idle":"2025-05-13T07:07:26.678534Z","shell.execute_reply.started":"2025-05-13T07:07:03.607467Z","shell.execute_reply":"2025-05-13T07:07:26.677424Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# --- 步骤 4: 训练组件设置 ---\nprint(\"\\n--- 开始设置训练组件 ---\")\n\n# 1. 定义损失函数\n# 根据 CFG 中的 loss_fn_name\nif cfg.loss_fn_name.lower() == 'crossentropyloss':\n    # CrossEntropyLoss 适用于多分类问题，当模型输出原始 logits 且标签是类别索引时。\n    # 它内部已经包含了 Softmax 操作。\n    criterion = nn.CrossEntropyLoss()\n    print(f\"损失函数已设置: nn.CrossEntropyLoss\")\nelif cfg.loss_fn_name.lower() == 'bcewithlogitsloss':\n    # BCEWithLogitsLoss 适用于多标签分类问题，或者二分类问题。\n    # 模型输出原始 logits，标签是 one-hot 编码或者每个类别对应一个0或1的值。\n    criterion = nn.BCEWithLogitsLoss()\n    print(f\"损失函数已设置: nn.BCEWithLogitsLoss\")\nelse:\n    raise ValueError(f\"不支持的损失函数: {cfg.loss_fn_name}. 请在CFG中选择 'CrossEntropyLoss' 或 'BCEWithLogitsLoss'，或在此处添加更多选项。\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-13T07:07:26.679926Z","iopub.execute_input":"2025-05-13T07:07:26.680254Z","iopub.status.idle":"2025-05-13T07:07:26.687195Z","shell.execute_reply.started":"2025-05-13T07:07:26.680219Z","shell.execute_reply":"2025-05-13T07:07:26.686316Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 2. 定义优化器\n# 根据 CFG 中的 optimizer_name 和相关参数\n# model.parameters() 告诉优化器需要更新哪些参数\nif cfg.optimizer_name.lower() == 'adamw':\n    optimizer = optim.AdamW(model.parameters(),\n                            lr=cfg.learning_rate,\n                            weight_decay=cfg.weight_decay)\n    print(f\"优化器已设置: AdamW, 学习率: {cfg.learning_rate}, 权重衰减: {cfg.weight_decay}\")\nelif cfg.optimizer_name.lower() == 'adam':\n    optimizer = optim.Adam(model.parameters(),\n                           lr=cfg.learning_rate,\n                           weight_decay=cfg.weight_decay) # Adam 也可以设置 weight_decay\n    print(f\"优化器已设置: Adam, 学习率: {cfg.learning_rate}, 权重衰减: {cfg.weight_decay}\")\nelif cfg.optimizer_name.lower() == 'sgd':\n    optimizer = optim.SGD(model.parameters(),\n                          lr=cfg.learning_rate,\n                          momentum=0.9, # SGD 通常需要 momentum\n                          weight_decay=cfg.weight_decay)\n    print(f\"优化器已设置: SGD, 学习率: {cfg.learning_rate}, Momentum: 0.9, 权重衰减: {cfg.weight_decay}\")\nelse:\n    raise ValueError(f\"不支持的优化器: {cfg.optimizer_name}. 请在CFG中选择 'AdamW', 'Adam', 'SGD'，或在此处添加更多选项。\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-13T07:07:26.688176Z","iopub.execute_input":"2025-05-13T07:07:26.688479Z","iopub.status.idle":"2025-05-13T07:07:26.710934Z","shell.execute_reply.started":"2025-05-13T07:07:26.688450Z","shell.execute_reply":"2025-05-13T07:07:26.710159Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 3. 定义学习率调度器 (可选)\n# 根据 CFG 中的 scheduler_name 和 lr_scheduler_params\nscheduler = None # 初始化为 None\nif cfg.scheduler_name: # 只有在 CFG 中指定了 scheduler_name 才创建\n    if cfg.scheduler_name.lower() == 'cosineannealinglr':\n        # 确保 cfg.lr_scheduler_params['T_max'] 和 cfg.lr_scheduler_params['eta_min'] 已正确设置\n        # 在我们之前的CFG实例化后，我们已经处理了 T_max = cfg.num_epochs\n        scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer,\n                                                         T_max=int(cfg.lr_scheduler_params['T_max']), # T_max应为整数\n                                                         eta_min=cfg.lr_scheduler_params['eta_min'])\n        print(f\"学习率调度器已设置: CosineAnnealingLR, T_max: {cfg.lr_scheduler_params['T_max']}, eta_min: {cfg.lr_scheduler_params['eta_min']}\")\n    elif cfg.scheduler_name.lower() == 'steplr':\n        scheduler = optim.lr_scheduler.StepLR(optimizer,\n                                              step_size=int(cfg.lr_scheduler_params['step_size']), # step_size应为整数\n                                              gamma=cfg.lr_scheduler_params['gamma'])\n        print(f\"学习率调度器已设置: StepLR, step_size: {cfg.lr_scheduler_params['step_size']}, gamma: {cfg.lr_scheduler_params['gamma']}\")\n    elif cfg.scheduler_name.lower() == 'reducelronplateau':\n        scheduler = optim.lr_scheduler.ReduceLROnPlateau(optimizer,\n                                                         mode='min', # 通常监控验证集损失，所以是 'min'\n                                                         factor=0.1,\n                                                         patience=5, # 5个epoch验证集损失没有改善则降低学习率\n                                                         verbose=True)\n        print(f\"学习率调度器已设置: ReduceLROnPlateau\")\n    # 你可以在这里添加更多调度器的选项\n    else:\n        print(f\"警告: 不支持的学习率调度器 '{cfg.scheduler_name}'。将不使用调度器。\")\nelse:\n    print(\"未在CFG中指定学习率调度器 (scheduler_name 为空或None)。将不使用调度器。\")\n\nprint(\"\\n--- 训练组件设置阶段结束 ---\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-13T07:07:26.711951Z","iopub.execute_input":"2025-05-13T07:07:26.712542Z","iopub.status.idle":"2025-05-13T07:07:26.730924Z","shell.execute_reply.started":"2025-05-13T07:07:26.712514Z","shell.execute_reply":"2025-05-13T07:07:26.730069Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# --- 验证训练组件是否已正确设置 ---\nprint(\"\\n--- 开始验证训练组件 ---\")\n\n# 标志变量，表示所有检查是否通过\nall_checks_passed = True\n\n# 1. 检查 cfg 对象\ntry:\n    if 'cfg' in globals() and cfg is not None:\n        print(f\"✅ cfg: 已定义。模型名称: {cfg.model_name}, 设备: {cfg.device}, Epochs: {cfg.num_epochs}\")\n        # 可以添加更多 cfg 属性的检查\n        if not hasattr(cfg, 'output_model_dir') or not cfg.output_model_dir:\n            print(\"    ⚠️ 警告: cfg.output_model_dir 未设置或为空，模型可能无法保存。\")\n            # all_checks_passed = False # 可以选择是否因此失败\n    else:\n        print(\"❌ cfg: 未定义或为 None。\")\n        all_checks_passed = False\nexcept Exception as e:\n    print(f\"❌ cfg: 检查时发生错误 - {e}\")\n    all_checks_passed = False\n\n# 2. 检查 model 对象\ntry:\n    if 'model' in globals() and isinstance(model, torch.nn.Module):\n        # 检查模型是否在正确的设备上 (这是一个启发式检查，不完全可靠)\n        # 更可靠的方式是检查模型参数的设备\n        model_device = next(model.parameters()).device\n        print(f\"✅ model: 已定义，类型: {type(model).__name__}, 所在设备: {model_device}\")\n        if str(model_device) != str(cfg.device): # 转换为字符串比较以防设备对象类型不同\n             print(f\"    ⚠️ 警告: 模型设备 ({model_device}) 与 CFG设备 ({cfg.device}) 不符。确保已执行 model.to(cfg.device)。\")\n             # all_checks_passed = False # 可以选择是否因此失败\n    else:\n        print(\"❌ model: 未定义或不是 torch.nn.Module 的实例。\")\n        all_checks_passed = False\nexcept Exception as e:\n    print(f\"❌ model: 检查时发生错误 - {e}\")\n    all_checks_passed = False\n\n# 3. 检查 criterion 对象\ntry:\n    if 'criterion' in globals() and isinstance(criterion, torch.nn.modules.loss._Loss):\n        print(f\"✅ criterion: 已定义，类型: {type(criterion).__name__}\")\n    else:\n        print(\"❌ criterion: 未定义或不是损失函数的有效实例。\")\n        all_checks_passed = False\nexcept Exception as e:\n    print(f\"❌ criterion: 检查时发生错误 - {e}\")\n    all_checks_passed = False\n\n# 4. 检查 optimizer 对象\ntry:\n    if 'optimizer' in globals() and isinstance(optimizer, torch.optim.Optimizer):\n        print(f\"✅ optimizer: 已定义，类型: {type(optimizer).__name__}\")\n        # 检查优化器是否关联了模型参数\n        if not optimizer.param_groups or not optimizer.param_groups[0]['params']:\n            print(\"    ⚠️ 警告: 优化器似乎没有关联任何模型参数。\")\n            all_checks_passed = False\n    else:\n        print(\"❌ optimizer: 未定义或不是优化器的有效实例。\")\n        all_checks_passed = False\nexcept Exception as e:\n    print(f\"❌ optimizer: 检查时发生错误 - {e}\")\n    all_checks_passed = False\n    \n# 在验证 scheduler 的代码块中\n\ntry:\n    if 'scheduler' in globals(): # 检查变量是否存在\n        if scheduler is None and (not hasattr(cfg, 'scheduler_name') or not cfg.scheduler_name):\n            print(f\"✅ scheduler: 为 None (符合预期，因为 CFG 中未指定 scheduler_name)。\")\n        elif scheduler is not None:\n            # 直接打印 scheduler 的类型，并检查它是否具有 step 方法\n            print(f\"✅ scheduler: 已定义，实际类型: {type(scheduler).__name__}\")\n            if not hasattr(scheduler, 'step'):\n                print(f\"    ⚠️ 警告: scheduler 对象 ({type(scheduler).__name__}) 缺少 'step' 方法，可能不是一个有效的学习率调度器。\")\n                all_checks_passed = False\n            # 你仍然可以保留 isinstance 检查，但如果它持续报错，上面的 hasattr 检查更实用\n            # if not isinstance(scheduler, torch.optim.lr_scheduler._LRScheduler):\n            #     print(f\"    ℹ️ 信息: isinstance(scheduler, torch.optim.lr_scheduler._LRScheduler) 返回 False，但只要有 'step' 方法通常就可用。\")\n\n        # ... (else if scheduler is None 但 cfg.scheduler_name 已指定 的逻辑保持不变) ...\n    # ... (except 和其他逻辑保持不变) ...\nexcept Exception as e:\n    print(f\"❌ scheduler: 检查时发生错误 - {e}\")\n    all_checks_passed = False\n\n\n# 6. 检查 train_loader 和 valid_loader 对象\ndef check_dataloader(loader_name, loader_instance, batch_size_cfg):\n    global all_checks_passed # 允许修改外部的 all_checks_passed\n    try:\n        if loader_name in globals() and isinstance(loader_instance, torch.utils.data.DataLoader):\n            print(f\"✅ {loader_name}: 已定义，类型: DataLoader, Batch Size: {loader_instance.batch_size}\")\n            if loader_instance.batch_size != batch_size_cfg:\n                print(f\"    ⚠️ 警告: {loader_name} 的 batch_size ({loader_instance.batch_size}) 与 CFG ({batch_size_cfg}) 不符。\")\n            if len(loader_instance) == 0:\n                print(f\"    ⚠️ 警告: {loader_name} 为空 (长度为0)，无法进行训练/验证。请检查 Dataset 是否正确加载数据。\")\n                all_checks_passed = False # 通常 DataLoader 为空是严重问题\n        else:\n            print(f\"❌ {loader_name}: 未定义或不是 DataLoader 的有效实例。\")\n            all_checks_passed = False\n    except Exception as e:\n        print(f\"❌ {loader_name}: 检查时发生错误 - {e}\")\n        all_checks_passed = False\n\nif 'train_loader' in globals(): check_dataloader('train_loader', train_loader, cfg.train_batch_size)\nelse: print(\"❌ train_loader: 未定义。\"); all_checks_passed = False\n\nif 'valid_loader' in globals(): check_dataloader('valid_loader', valid_loader, cfg.valid_batch_size)\nelse: print(\"❌ valid_loader: 未定义。\"); all_checks_passed = False\n\n\n# 7. 检查 num_classes\ntry:\n    if 'num_classes' in globals() and isinstance(num_classes, int) and num_classes > 0:\n        print(f\"✅ num_classes: 已定义，值为: {num_classes}\")\n    else:\n        print(f\"❌ num_classes: 未定义，不是整数，或值无效 (当前值: {num_classes if 'num_classes' in globals() else '未定义'})。\")\n        all_checks_passed = False\nexcept Exception as e:\n    print(f\"❌ num_classes: 检查时发生错误 - {e}\")\n    all_checks_passed = False\n\n# 8. 检查 int_to_label (可选)\ntry:\n    if 'int_to_label' in globals() and isinstance(int_to_label, dict):\n        print(f\"✅ int_to_label: 已定义 (可选)，类型: dict, 包含 {len(int_to_label)} 个条目。\")\n        if 'num_classes' in globals() and isinstance(num_classes, int) and num_classes > 0 and len(int_to_label) != num_classes:\n             print(f\"    ⚠️ 警告: int_to_label 的条目数 ({len(int_to_label)}) 与 num_classes ({num_classes}) 不符。\")\n    elif 'int_to_label' not in globals():\n        print(f\"ℹ️ int_to_label: 未定义 (此项为可选，用于日志可读性)。\")\n    else:\n        print(f\"⚠️ int_to_label: 已定义但不是字典类型 (类型: {type(int_to_label).__name__})。\")\n\nexcept Exception as e:\n    print(f\"❌ int_to_label: 检查时发生错误 - {e}\")\n    # all_checks_passed = False # 因为是可选的，所以不因此失败\n\nprint(\"\\n--- 验证结束 ---\")\nif all_checks_passed:\n    print(\"👍 所有关键组件看起来都已正确设置！可以准备开始训练循环了。\")\nelse:\n    print(\"🔥 注意：一个或多个关键组件未正确设置或存在警告。请检查上面的错误/警告信息，并在开始训练前修复它们。\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-13T07:07:26.731883Z","iopub.execute_input":"2025-05-13T07:07:26.732194Z","iopub.status.idle":"2025-05-13T07:07:26.756590Z","shell.execute_reply.started":"2025-05-13T07:07:26.732167Z","shell.execute_reply":"2025-05-13T07:07:26.755825Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import time # 用于记录训练时间\nimport torch # 确保torch已导入\nfrom tqdm.auto import tqdm # 进度条\nimport os # 用于文件路径操作\nimport copy # 用于深拷贝模型权重 (保存最佳模型时)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-13T07:07:26.757522Z","iopub.execute_input":"2025-05-13T07:07:26.757974Z","iopub.status.idle":"2025-05-13T07:07:26.778564Z","shell.execute_reply.started":"2025-05-13T07:07:26.757949Z","shell.execute_reply":"2025-05-13T07:07:26.777723Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# --- 步骤 5: 训练循环 ---\nprint(\"\\n--- 开始训练循环 ---\")\n\n# 用于记录每个epoch的平均损失和准确率 (或其他指标)\nhistory = {\n    'train_loss': [],\n    'train_acc': [],\n    'valid_loss': [],\n    'valid_acc': [],\n    'lr': []\n}","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-13T07:07:26.779494Z","iopub.execute_input":"2025-05-13T07:07:26.780343Z","iopub.status.idle":"2025-05-13T07:07:26.795327Z","shell.execute_reply.started":"2025-05-13T07:07:26.780313Z","shell.execute_reply":"2025-05-13T07:07:26.794550Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"best_valid_acc = 0.0 # 用于追踪最佳验证准确率\nbest_epoch = -1\nbest_model_weights = None # 用于保存最佳模型的权重\n\n# 确保模型在正确的设备上\nmodel.to(cfg.device)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-13T07:07:26.796147Z","iopub.execute_input":"2025-05-13T07:07:26.796430Z","iopub.status.idle":"2025-05-13T07:07:26.821480Z","shell.execute_reply.started":"2025-05-13T07:07:26.796407Z","shell.execute_reply":"2025-05-13T07:07:26.820711Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"for epoch in range(cfg.num_epochs):\n    epoch_start_time = time.time()\n    print(f\"\\nEpoch {epoch+1}/{cfg.num_epochs}\")\n    print(\"-\" * 30)\n\n    # --- 训练阶段 ---\n    model.train()  # 设置模型为训练模式 (启用 Dropout, BatchNorm 更新等)\n    running_train_loss = 0.0\n    correct_train_preds = 0\n    total_train_samples = 0\n\n    # 使用 tqdm 创建训练进度条\n    train_pbar = tqdm(train_loader, desc=f\"Training Epoch {epoch+1}\", leave=False)\n\n    for batch_idx, (images, labels) in enumerate(train_pbar):\n        images = images.to(cfg.device)\n        labels = labels.to(cfg.device)\n\n        # 1. 清零梯度\n        optimizer.zero_grad()\n\n        # 2. 前向传播\n        outputs = model(images)  # 模型输出 logits\n\n        # 3. 计算损失\n        loss = criterion(outputs, labels)\n\n        # 4. 反向传播\n        loss.backward()\n\n        # 5. 更新权重\n        optimizer.step()\n\n        # 统计损失和准确率\n        running_train_loss += loss.item() * images.size(0) # loss.item()是当前batch的平均loss\n        _, predicted_classes = torch.max(outputs, 1) # 获取预测类别 (概率最高的那个)\n        correct_train_preds += (predicted_classes == labels).sum().item()\n        total_train_samples += labels.size(0)\n\n        # 更新进度条描述 (可选)\n        if batch_idx % 20 == 0: # 每20个batch更新一次\n            train_pbar.set_postfix({\n                'Loss': f\"{loss.item():.4f}\",\n                'Acc': f\"{(predicted_classes == labels).sum().item() / labels.size(0):.4f}\"\n            })\n\n    epoch_train_loss = running_train_loss / total_train_samples\n    epoch_train_acc = correct_train_preds / total_train_samples\n    history['train_loss'].append(epoch_train_loss)\n    history['train_acc'].append(epoch_train_acc)\n\n    # --- 验证阶段 ---\n    model.eval()   # 设置模型为评估模式 (禁用 Dropout, BatchNorm 使用运行时的统计数据)\n    running_valid_loss = 0.0\n    correct_valid_preds = 0\n    total_valid_samples = 0\n\n    valid_pbar = tqdm(valid_loader, desc=f\"Validating Epoch {epoch+1}\", leave=False)\n\n    with torch.no_grad(): # 在验证阶段不计算梯度，节省内存和计算\n        for images, labels in valid_pbar:\n            images = images.to(cfg.device)\n            labels = labels.to(cfg.device)\n\n            outputs = model(images)\n            loss = criterion(outputs, labels)\n\n            running_valid_loss += loss.item() * images.size(0)\n            _, predicted_classes = torch.max(outputs, 1)\n            correct_valid_preds += (predicted_classes == labels).sum().item()\n            total_valid_samples += labels.size(0)\n\n            if batch_idx % 20 == 0:\n                 valid_pbar.set_postfix({\n                    'Loss': f\"{loss.item():.4f}\",\n                    'Acc': f\"{(predicted_classes == labels).sum().item() / labels.size(0):.4f}\"\n                })\n\n\n    epoch_valid_loss = running_valid_loss / total_valid_samples\n    epoch_valid_acc = correct_valid_preds / total_valid_samples\n    history['valid_loss'].append(epoch_valid_loss)\n    history['valid_acc'].append(epoch_valid_acc)\n\n    # 记录当前学习率\n    current_lr = optimizer.param_groups[0]['lr']\n    history['lr'].append(current_lr)\n\n    # --- 学习率调度器步骤 ---\n    if scheduler:\n        if isinstance(scheduler, torch.optim.lr_scheduler.ReduceLROnPlateau):\n            scheduler.step(epoch_valid_loss) # ReduceLROnPlateau 需要监控一个指标\n        else:\n            scheduler.step() # 其他大多数调度器在 epoch 结束时 step\n\n    # --- 打印当前 Epoch 的结果 ---\n    epoch_duration = time.time() - epoch_start_time\n    if (epoch + 1) % cfg.print_freq_epochs == 0: # 根据 CFG 中的频率打印\n        print(f\"Epoch {epoch+1}/{cfg.num_epochs} - \"\n              f\"Duration: {epoch_duration:.2f}s - \"\n              f\"LR: {current_lr:.1e}\")\n        print(f\"  Train Loss: {epoch_train_loss:.4f}, Train Acc: {epoch_train_acc:.4f}\")\n        print(f\"  Valid Loss: {epoch_valid_loss:.4f}, Valid Acc: {epoch_valid_acc:.4f}\")\n\n    # --- 模型保存 ---\n    # 检查是否是当前最佳模型 (基于验证准确率)\n    if epoch_valid_acc > best_valid_acc:\n        best_valid_acc = epoch_valid_acc\n        best_epoch = epoch + 1\n        # 保存最佳模型的权重 (使用深拷贝以防后续model被修改)\n        best_model_weights = copy.deepcopy(model.state_dict())\n        print(f\"🎉 New best model found at Epoch {best_epoch} with Valid Acc: {best_valid_acc:.4f}\")\n\n        # 如果只保存最佳模型，则在这里保存\n        if cfg.save_best_model_only:\n            save_path = os.path.join(cfg.output_model_dir,\n                                     f\"{cfg.model_name}_fold{cfg.current_fold_to_train}_best_acc.pth\")\n            torch.save({\n                'epoch': best_epoch,\n                'model_state_dict': best_model_weights,\n                'optimizer_state_dict': optimizer.state_dict(),\n                'scheduler_state_dict': scheduler.state_dict() if scheduler else None,\n                'best_valid_acc': best_valid_acc,\n                'config': vars(cfg) # 保存配置信息 (将 CFG 对象转为字典)\n            }, save_path)\n            print(f\"   Best model (so far) saved to: {save_path}\")\n\n    # (可选) 如果不是只保存最佳模型，也可以每隔一定epoch保存一次，或者每个epoch都保存\n    # if not cfg.save_best_model_only:\n    #     if (epoch + 1) % 5 == 0: # 例如每5个epoch保存一次\n    #         save_path = os.path.join(cfg.output_model_dir,\n    #                                  f\"{cfg.model_name}_fold{cfg.current_fold_to_train}_epoch{epoch+1}.pth\")\n    #         torch.save({\n    #             'epoch': epoch + 1,\n    #             'model_state_dict': model.state_dict(),\n    #             # ... (其他你想保存的信息)\n    #         }, save_path)\n    #         print(f\"   Model at epoch {epoch+1} saved to: {save_path}\")\n\n# --- 训练循环结束 ---\nprint(\"\\n--- 训练循环已结束 ---\")\nprint(f\"最佳验证准确率: {best_valid_acc:.4f} 在第 {best_epoch} 轮达到。\")\nif cfg.save_best_model_only and best_model_weights is not None:\n    print(f\"最佳模型已保存在 {cfg.output_model_dir} 目录下。\")\nelif best_model_weights is None:\n    print(\"警告: 未找到或保存任何最佳模型，可能是因为验证准确率没有提升或save_best_model_only为False且未实现其他保存策略。\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-13T07:07:26.822527Z","iopub.execute_input":"2025-05-13T07:07:26.822789Z","iopub.status.idle":"2025-05-13T07:28:04.114297Z","shell.execute_reply.started":"2025-05-13T07:07:26.822754Z","shell.execute_reply":"2025-05-13T07:28:04.113165Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"plt.figure(figsize=(15, 5))\n\nplt.subplot(1, 3, 1)\nplt.plot(history['train_loss'], label='Train Loss')\nplt.plot(history['valid_loss'], label='Valid Loss')\nplt.title('Loss vs. Epochs')\nplt.xlabel('Epochs')\nplt.ylabel('Loss')\nplt.legend()\n\nplt.subplot(1, 3, 2)\nplt.plot(history['train_acc'], label='Train Accuracy')\nplt.plot(history['valid_acc'], label='Valid Accuracy')\nplt.title('Accuracy vs. Epochs')\nplt.xlabel('Epochs')\nplt.ylabel('Accuracy')\nplt.legend()\n\nplt.subplot(1, 3, 3)\nplt.plot(history['lr'], label='Learning Rate')\nplt.title('Learning Rate vs. Epochs')\nplt.xlabel('Epochs')\nplt.ylabel('Learning Rate')\nplt.legend()\n\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-13T07:31:51.905846Z","iopub.execute_input":"2025-05-13T07:31:51.906687Z","iopub.status.idle":"2025-05-13T07:31:52.543998Z","shell.execute_reply.started":"2025-05-13T07:31:51.906650Z","shell.execute_reply":"2025-05-13T07:31:52.543199Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os, torch\nmodel_dir = \"/kaggle/working/models\"\nos.makedirs(model_dir, exist_ok=True)\n\nmodel_path = os.path.join(model_dir, \"efficientnet_b0_fold0_best_acc.pth\")\ntorch.save(model.state_dict(), model_path)      # 只保存权重\n# 或者\n# torch.save(model, model_path)                # 保存整模型，体积更大\nprint(\"模型已写入:\", model_path)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-13T07:41:15.166368Z","iopub.execute_input":"2025-05-13T07:41:15.167177Z","iopub.status.idle":"2025-05-13T07:41:15.251483Z","shell.execute_reply.started":"2025-05-13T07:41:15.167153Z","shell.execute_reply":"2025-05-13T07:41:15.250636Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\nclass CFG:\n\n    train_audio_path = '/kaggle/input/birdclef-2025/train_audio/' # 你的训练音频文件存放路径\n    train_metadata_csv = '/kaggle/input/birdclef-2025/train.csv' # 包含音频文件名和对应鸟类标签的CSV文件路径\n\n    # 2. 物种分类文件 (用于获取类别总数和标签映射)\n    taxonomy_csv = '/kaggle/input/birdclef-2025/taxonomy.csv'\n\n    # 3. 你训练好的模型的保存路径 (检查点)\n    output_model_dir = '/kaggle/working/models/' # 或者其他任何你有写入权限的目录\n                                                 # 每一折或每一个epoch的模型都会保存在这里\n  # --- 音频和梅尔频谱图参数 (通常和推理时保持一致) ---\n    FS = 32000  \n    WINDOW_SIZE = 5  \n    \n    N_FFT = 1024\n    HOP_LENGTH = 64\n    N_MELS = 136\n    FMIN = 20\n    FMAX = 16000\n    TARGET_SHAPE = (256, 256)\n    # --- 模型架构 ---\n    model_name = 'efficientnet_b0'\n    in_channels = 1 # 输入通道数 (1代表单通道梅尔频谱图)\n    # 是否使用 ImageNet 预训练权重作为你骨干网络的起点\n    # 这对于迁移学习通常是有益的\n    pretrained_on_imagenet = True # 设置为 True 来加载 ImageNet 权重到骨干网络\n    \n    # --- 训练超参数 ---\n    # 1. 通用设置\n    device = 'cuda' if torch.cuda.is_available() else 'cpu' # 如果有GPU则使用GPU\n    seed = 42               # 用于保证实验可复现的随机种子\n    num_epochs = 1         # 总训练轮次\n    train_batch_size = 32   # 训练批次大小\n    valid_batch_size = 64   # 验证批次大小 (通常可以设大一些，因为验证时没有反向传播)\n\n    # 2. 优化器\n    optimizer_name = 'AdamW' # 例如: 'Adam', 'AdamW', 'SGD'\n    learning_rate = 1e-3     # 学习率\n    weight_decay = 1e-5      # 权重衰减 (用于像 AdamW 这样的优化器)\n\n    # 3. 学习率调度器 (可选，但通常有帮助)\n    scheduler_name = 'CosineAnnealingLR' # 例如: 'StepLR', 'ReduceLROnPlateau', 'CosineAnnealingLR'\n    lr_scheduler_params = {  # 学习率调度器的具体参数\n        'T_max': num_epochs, # 对于 CosineAnnealingLR\n        'eta_min': 1e-6      # 对于 CosineAnnealingLR\n    }\n    # 或者对于 StepLR: {'step_size': 10, 'gamma': 0.1}\n    if num_epochs != 50: # 如果 num_epochs 被修改，需要确保 T_max 也更新\n        lr_scheduler_params['T_max'] = num_epochs\n    # 4. 损失函数\n    loss_fn_name = 'CrossEntropyLoss' # 如果你的标签是每个样本一个鸟类整数ID，就用这个\n                                      # 如果一个声音里可能有多种鸟 (多标签)，可能用 'BCEWithLogitsLoss'\n    # --- 数据处理与验证 ---\n    num_workers = 2         # DataLoader 使用的工作进程数\n    # K折交叉验证 (在竞赛中很常见)\n    n_folds = 5             # 总共分几折\n    current_fold_to_train = 0 # 当前训练的是第几折 (从0到 n_folds-1)\n\n    # --- 数据增强 (可选, 用于训练数据) ---\n    # 你可以在这里定义音频数据增强的参数，例如:\n    # use_noise_injection = True  # 是否使用噪声注入\n    # noise_level = 0.005         # 噪声水平\n    # use_random_shift = True     # 是否使用随机时间平移\n    # 等等...\n\n    # --- 日志与模型保存 ---\n    print_freq_epochs = 1   # 每隔多少轮打印一次训练日志\n    save_best_model_only = True # 只保存验证集上表现最好的模型\n\n    # --- 用于调试 (只处理一小部分数据) ---\n    debug_mode = False\n    debug_subset_size = 100 # 如果 debug_mode 为 True, 使用的样本数量\n\ncfg = CFG()\n\ncfg.lr_scheduler_params = {\n    'T_max': cfg.num_epochs,\n    'eta_min': 1e-6\n}\n\nprint(f\"--- CFG 实例化后 ---\")\nprint(f\"torch.cuda.is_available(): {torch.cuda.is_available()}\")\nprint(f\"cfg.device 设置为: {cfg.device}\")\n# 下面这两行先注释掉，因为 model 还没定义\n# print(f\"模型已移动到设备: {cfg.device}\")\n# print(f\"训练将在设备: {cfg.device} 上进行\")\nprint(f\"梅尔频谱图参数: N_FFT={cfg.N_FFT}, HOP_LENGTH={cfg.HOP_LENGTH}, N_MELS={cfg.N_MELS}\")\nprint(f\"目标图像形状: {cfg.TARGET_SHAPE}\")","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}