{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"gpu","dataSources":[{"sourceType":"competition","sourceId":91844,"databundleVersionId":11361821}],"dockerImageVersionId":31400,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# 单元格1：安装依赖和设置环境\nimport os\nimport numpy as np\nimport pandas as pd\nimport torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom torch.utils.data import Dataset, DataLoader, ConcatDataset\nfrom torch.cuda.amp import GradScaler, autocast\nimport timm\nimport librosa\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.metrics import accuracy_score, f1_score\nfrom tqdm import tqdm\nimport matplotlib.pyplot as plt\nimport warnings\nwarnings.filterwarnings('ignore')\n\n# 下载中文字体\nprint(\"下载中文字体...\")\nos.system(\"wget https://github.com/StellarCN/scp_zh/raw/master/fonts/SimHei.ttf -O /tmp/SimHei.ttf 2>/dev/null\")\nimport matplotlib\nmatplotlib.rcParams['font.family'] = 'sans-serif'\nmatplotlib.rcParams['font.sans-serif'] = ['SimHei']\nmatplotlib.rcParams['axes.unicode_minus'] = False\nprint(\"中文字体设置完成\")\n\nprint(\"环境准备完成\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-29T09:12:57.633859Z","iopub.execute_input":"2026-05-29T09:12:57.634736Z","iopub.status.idle":"2026-05-29T09:12:58.941773Z","shell.execute_reply.started":"2026-05-29T09:12:57.634684Z","shell.execute_reply":"2026-05-29T09:12:58.94091Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 单元格2：配置参数（已优化 Batch Size 与并行加载速度）\nimport os\nimport numpy as np\nimport torch\n\nclass Config:\n    BASE_PATH = \"/kaggle/input/competitions/birdclef-2025\"\n    train_audio_dir = f\"{BASE_PATH}/train_audio/\"\n    train_soundscapes_dir = f\"{BASE_PATH}/train_soundscapes/\"\n    train_csv = f\"{BASE_PATH}/train.csv\"\n    output_dir = \"/kaggle/working/output/\"\n    \n    model_name = \"tf_efficientnetv2_s\"\n    num_classes = 206\n    pretrained = True\n    \n    sr = 32000\n    duration = 5\n    n_mels = 128\n    fmin = 0\n    fmax = 16000\n    \n    # 💡 【提速核心1】：扩大批大小，榨干 GPU 算力。如果后面运行报错显存溢出(OOM)，改回 32 即可\n    batch_size = 64 \n    \n    # 💡 【提速核心2】：开启多线程与锁页内存，彻底解决 CPU 加载和音频转换慢的瓶颈\n    num_workers = 2     # Kaggle 环境双核 CPU 设为 2 效率最高\n    pin_memory = True   # 锁页内存，加速数据向显存搬运\n    \n    epochs_teacher = 20\n    epochs_student = 20\n    learning_rate = 1e-3\n    weight_decay = 1e-4\n    \n    focal_gamma = 2.0\n    pseudo_threshold = 0.7\n    use_amp = True       # 确保自动混合精度是开启的\n    seed = 42\n\ncfg = Config()\nos.makedirs(cfg.output_dir, exist_ok=True)\n\ndef set_seed(seed):\n    np.random.seed(seed)\n    torch.manual_seed(seed)\n    if torch.cuda.is_available():\n        torch.cuda.manual_seed_all(seed)\n\nset_seed(cfg.seed)\ndevice = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\nprint(f\"使用设备: {device}\")\nprint(f\"输出目录: {cfg.output_dir}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-29T09:12:58.943486Z","iopub.execute_input":"2026-05-29T09:12:58.943864Z","iopub.status.idle":"2026-05-29T09:12:58.954782Z","shell.execute_reply.started":"2026-05-29T09:12:58.943839Z","shell.execute_reply":"2026-05-29T09:12:58.953922Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 单元格3：加载和预处理数据\nprint(\"检查文件路径...\")\nif not os.path.exists(cfg.train_csv):\n    raise FileNotFoundError(f\"train.csv 未找到: {cfg.train_csv}\")\n\ntrain_df = pd.read_csv(cfg.train_csv)\nprint(f\"标注样本总数: {len(train_df)}\")\n\nspecies_list = sorted(train_df['primary_label'].unique())\nprint(f\"物种总数: {len(species_list)}\")\n\nspecies_to_idx = {species: i for i, species in enumerate(species_list)}\nidx_to_species = {i: species for species, i in species_to_idx.items()}\ncfg.num_classes = len(species_list)\n\ntrain_df['label'] = train_df['primary_label'].map(species_to_idx)\nclass_counts = train_df['primary_label'].value_counts().to_dict()\n\ntrain_labeled_df, val_df = train_test_split(\n    train_df, test_size=0.2, stratify=train_df['primary_label'], random_state=cfg.seed\n)\nprint(f\"训练样本: {len(train_labeled_df)}, 验证样本: {len(val_df)}\")\nprint(f\"类别分布 - 最少: {min(class_counts.values())}, 最多: {max(class_counts.values())}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-29T09:12:58.955929Z","iopub.execute_input":"2026-05-29T09:12:58.956283Z","iopub.status.idle":"2026-05-29T09:12:59.163078Z","shell.execute_reply.started":"2026-05-29T09:12:58.956261Z","shell.execute_reply":"2026-05-29T09:12:59.162359Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 单元格4：定义数据集类及加载器（已完美对接优化参数）\nimport os\nimport librosa\nimport numpy as np\nimport torch\nfrom torch.utils.data import Dataset, DataLoader\n\nclass BirdAudioDataset(Dataset):\n    def __init__(self, df, audio_dir, is_train=True):\n        self.df = df.reset_index(drop=True)\n        self.audio_dir = audio_dir\n        self.is_train = is_train\n        \n    def __len__(self):\n        return len(self.df)\n        \n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        filename = row['filename']\n        file_path = os.path.join(self.audio_dir, filename)\n        \n        # 动态根据当前配置计算时间步，防止尺寸不一致\n        time_steps = int(cfg.sr * cfg.duration / 512) + 1\n        default_spec = torch.zeros(1, cfg.n_mels, time_steps)\n        \n        try:\n            if not os.path.exists(file_path):\n                if self.is_train and 'label' in row:\n                    return default_spec, torch.tensor(row['label'], dtype=torch.long)\n                return default_spec, filename\n            \n            # 读取音频文件\n            audio, _ = librosa.load(file_path, sr=cfg.sr, duration=cfg.duration)\n            if len(audio) < cfg.sr * cfg.duration:\n                audio = np.pad(audio, (0, cfg.sr * cfg.duration - len(audio)))\n            else:\n                audio = audio[:cfg.sr * cfg.duration]\n            \n            # 提取梅尔频谱\n            mel_spec = librosa.feature.melspectrogram(\n                y=audio, sr=cfg.sr, n_mels=cfg.n_mels,\n                fmin=cfg.fmin, fmax=cfg.fmax,\n                n_fft=2048, hop_length=512, power=2.0\n            )\n            mel_spec_db = librosa.power_to_db(mel_spec, ref=np.max)\n            \n            # 归一化\n            mel_min, mel_max = mel_spec_db.min(), mel_spec_db.max()\n            if mel_max > mel_min:\n                mel_spec_db = (mel_spec_db - mel_min) / (mel_max - mel_min + 1e-8)\n            else:\n                mel_spec_db = np.zeros_like(mel_spec_db)\n            \n            spec = torch.from_numpy(mel_spec_db).float().unsqueeze(0)\n            \n            if self.is_train and 'label' in row:\n                return spec, torch.tensor(row['label'], dtype=torch.long)\n            return spec, filename\n            \n        except Exception as e:\n            if self.is_train and 'label' in row:\n                return default_spec, torch.tensor(row['label'], dtype=torch.long)\n            return default_spec, filename\n\n# 创建数据数据集实例\ntrain_dataset = BirdAudioDataset(train_labeled_df, cfg.train_audio_dir, is_train=True)\nval_dataset = BirdAudioDataset(val_df, cfg.train_audio_dir, is_train=True)\n\n# 💡 【提速完美对接】：确保 batch_size、num_workers、pin_memory 全部从全局 cfg 自动读取\ntrain_loader = DataLoader(\n    train_dataset, \n    batch_size=cfg.batch_size,        # 👈 自动继承改好的大 Batch Size (64)\n    shuffle=True, \n    num_workers=cfg.num_workers,      # 👈 使用我们在 Config 加好的全局参数\n    pin_memory=cfg.pin_memory,        # 👈 使用我们在 Config 加好的全局参数\n    drop_last=True                    # 💡 丢弃最后零头，防止不完整的末尾 batch 拉慢训练\n)\n\nval_loader = DataLoader(\n    val_dataset, \n    batch_size=cfg.batch_size, \n    shuffle=False, \n    num_workers=cfg.num_workers, \n    pin_memory=cfg.pin_memory\n)\n\nprint(f\"训练批次数: {len(train_loader)}, 验证批次数: {len(val_loader)}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-29T09:12:59.164125Z","iopub.execute_input":"2026-05-29T09:12:59.164436Z","iopub.status.idle":"2026-05-29T09:12:59.185689Z","shell.execute_reply.started":"2026-05-29T09:12:59.164411Z","shell.execute_reply":"2026-05-29T09:12:59.184733Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 单元格5：定义模型（修正为接受1通道输入）\ndef create_model(num_classes):\n    model = timm.create_model(cfg.model_name, pretrained=True, num_classes=num_classes)\n    \n    # 修改第一层卷积：从3通道改为1通道\n    old_conv = model.conv_stem\n    new_conv = nn.Conv2d(\n        in_channels=1,\n        out_channels=old_conv.out_channels,\n        kernel_size=old_conv.kernel_size,\n        stride=old_conv.stride,\n        padding=old_conv.padding,\n        bias=old_conv.bias is not None\n    )\n    \n    # 用原权重的均值初始化新卷积\n    with torch.no_grad():\n        if old_conv.weight.shape[1] == 3:\n            new_conv.weight[:, 0, :, :] = old_conv.weight.mean(dim=1)\n        else:\n            new_conv.weight.data = old_conv.weight.data.mean(dim=1, keepdim=True)\n    \n    model.conv_stem = new_conv\n    return model\n\n# Focal Loss\nclass FocalLoss(nn.Module):\n    def __init__(self, gamma=2.0):\n        super().__init__()\n        self.gamma = gamma\n    \n    def forward(self, inputs, targets):\n        ce_loss = nn.functional.cross_entropy(inputs, targets, reduction='none')\n        pt = torch.exp(-ce_loss)\n        focal_loss = (1 - pt) ** self.gamma * ce_loss\n        return focal_loss.mean()\n\nprint(\"模型定义完成\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-29T09:12:59.188064Z","iopub.execute_input":"2026-05-29T09:12:59.188459Z","iopub.status.idle":"2026-05-29T09:12:59.205577Z","shell.execute_reply.started":"2026-05-29T09:12:59.18843Z","shell.execute_reply":"2026-05-29T09:12:59.204735Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 单元格6：定义训练和验证函数\nimport torch\nfrom torch.cuda.amp import autocast\nfrom tqdm import tqdm\nfrom sklearn.metrics import accuracy_score, f1_score\n\ndef train_epoch(model, loader, optimizer, criterion, scaler):\n    model.train()\n    total_loss = 0\n    all_preds = []\n    all_targets = []\n    \n    pbar = tqdm(loader, desc=\"训练中\")\n    for specs, labels in pbar:\n        specs = specs.to(device)\n        labels = labels.to(device)\n        \n        optimizer.zero_grad()\n        \n        # 混合精度前向传播\n        with autocast(enabled=cfg.use_amp and device.type == 'cuda'):\n            outputs = model(specs)\n            loss = criterion(outputs, labels)\n        \n        # 混合精度反向传播与更新\n        scaler.scale(loss).backward()\n        scaler.step(optimizer)\n        scaler.update()\n        \n        total_loss += loss.item()\n        preds = torch.argmax(outputs, dim=1)\n        all_preds.extend(preds.cpu().numpy())\n        all_targets.extend(labels.cpu().numpy())\n        \n        pbar.set_postfix({'loss': f\"{loss.item():.4f}\"})\n    \n    acc = accuracy_score(all_targets, all_preds)\n    f1 = f1_score(all_targets, all_preds, average='macro', zero_division=0)\n    return total_loss / len(loader), acc, f1\n\ndef validate(model, loader, criterion):\n    model.eval()\n    total_loss = 0\n    all_preds = []\n    all_targets = []\n    \n    with torch.no_grad():\n        for specs, labels in tqdm(loader, desc=\"验证中\"):\n            specs = specs.to(device)\n            labels = labels.to(device)\n            \n            with autocast(enabled=cfg.use_amp and device.type == 'cuda'):\n                outputs = model(specs)\n                loss = criterion(outputs, labels)\n            \n            total_loss += loss.item()\n            preds = torch.argmax(outputs, dim=1)\n            all_preds.extend(preds.cpu().numpy())\n            all_targets.extend(labels.cpu().numpy())\n    \n    acc = accuracy_score(all_targets, all_preds)\n    f1 = f1_score(all_targets, all_preds, average='macro', zero_division=0)\n    return total_loss / len(loader), acc, f1\n\nprint(\"训练函数定义完成\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-29T09:12:59.206515Z","iopub.execute_input":"2026-05-29T09:12:59.206785Z","iopub.status.idle":"2026-05-29T09:12:59.227102Z","shell.execute_reply.started":"2026-05-29T09:12:59.206761Z","shell.execute_reply":"2026-05-29T09:12:59.226196Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 单元格7：阶段1 - 训练教师模型（监督基线）\nimport pandas as pd\nimport torch.optim as optim\nfrom torch.cuda.amp import GradScaler\n\nprint(\"=\"*50)\nprint(\"阶段1: 训练教师模型（监督学习基线）\")\nprint(\"=\"*50)\n\n# 创建并配置模型与核心组件\nteacher_model = create_model(cfg.num_classes).to(device)\noptimizer = optim.AdamW(teacher_model.parameters(), lr=cfg.learning_rate, weight_decay=cfg.weight_decay)\nscheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=cfg.epochs_teacher)\ncriterion = FocalLoss(gamma=cfg.focal_gamma).to(device)\nscaler = GradScaler(enabled=cfg.use_amp and device.type == 'cuda')\n\nteacher_metrics = []\nbest_macro_f1 = 0\n\nfor epoch in range(cfg.epochs_teacher):\n    print(f\"\\n第 {epoch+1}/{cfg.epochs_teacher} 轮\")\n    \n    # 核心长跑训练与验证\n    train_loss, train_acc, train_f1 = train_epoch(teacher_model, train_loader, optimizer, criterion, scaler)\n    val_loss, val_acc, val_f1 = validate(teacher_model, val_loader, criterion)\n    scheduler.step()\n    \n    teacher_metrics.append({\n        'epoch': epoch+1,\n        'train_loss': train_loss,\n        'train_acc': train_acc,\n        'train_macro_f1': train_f1,\n        'val_loss': val_loss,\n        'val_acc': val_acc,\n        'val_macro_f1': val_f1\n    })\n    \n    print(f\"训练 - Loss: {train_loss:.4f}, Acc: {train_acc:.4f}, Macro F1: {train_f1:.4f}\")\n    print(f\"验证 - Loss: {val_loss:.4f}, Acc: {val_acc:.4f}, Macro F1: {val_f1:.4f}\")\n    \n    # 自动保存效果最好的教师模型，用于后续半监督伪标签生成\n    if val_f1 > best_macro_f1:\n        best_macro_f1 = val_f1\n        torch.save(teacher_model.state_dict(), os.path.join(cfg.output_dir, 'best_teacher.pth'))\n        print(f\"✓ 保存最佳教师模型, Macro F1: {val_f1:.4f}\")\n\n# 保存教师模型整体训练指标\npd.DataFrame(teacher_metrics).to_csv(os.path.join(cfg.output_dir, 'teacher_metrics.csv'), index=False)\nprint(\"\\n教师模型训练完成，指标已成功保存！\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-29T09:12:59.228268Z","iopub.execute_input":"2026-05-29T09:12:59.228698Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 单元格8：阶段2 - 生成伪标签\nprint(\"=\"*50)\nprint(\"阶段2: 生成伪标签\")\nprint(\"=\"*50)\n\n# 检查无标签声景数据\nsoundscape_files = []\nif os.path.exists(cfg.train_soundscapes_dir):\n    soundscape_files = [f for f in os.listdir(cfg.train_soundscapes_dir) if f.endswith('.ogg')]\n    print(f\"找到 {len(soundscape_files)} 个无标签声景文件\")\n\npseudo_loader = None\nif len(soundscape_files) > 0:\n    pseudo_samples = []\n    teacher_model.eval()\n    process_count = min(200, len(soundscape_files))  # 先处理200个\n    \n    for fname in tqdm(soundscape_files[:process_count], desc=\"生成伪标签\"):\n        file_path = os.path.join(cfg.train_soundscapes_dir, fname)\n        try:\n            audio, _ = librosa.load(file_path, sr=cfg.sr, duration=cfg.duration)\n            if len(audio) < cfg.sr * cfg.duration:\n                audio = np.pad(audio, (0, cfg.sr * cfg.duration - len(audio)))\n            else:\n                audio = audio[:cfg.sr * cfg.duration]\n            \n            mel_spec = librosa.feature.melspectrogram(\n                y=audio, sr=cfg.sr, n_mels=cfg.n_mels,\n                fmin=cfg.fmin, fmax=cfg.fmax,\n                n_fft=2048, hop_length=512, power=2.0\n            )\n            mel_spec_db = librosa.power_to_db(mel_spec, ref=np.max)\n            mel_min, mel_max = mel_spec_db.min(), mel_spec_db.max()\n            if mel_max > mel_min:\n                mel_spec_db = (mel_spec_db - mel_min) / (mel_max - mel_min + 1e-8)\n            else:\n                mel_spec_db = np.zeros_like(mel_spec_db)\n            \n            spec = torch.from_numpy(mel_spec_db).float().unsqueeze(0).to(device)\n            \n            with torch.no_grad():\n                outputs = teacher_model(spec.unsqueeze(0))\n                probs = torch.softmax(outputs, dim=1)\n                max_prob, pred = torch.max(probs, dim=1)\n            \n            if max_prob.item() > cfg.pseudo_threshold:\n                pseudo_samples.append({\n                    'spec': spec.cpu(),\n                    'label': pred.item()\n                })\n        except Exception as e:\n            continue\n    \n    print(f\"生成 {len(pseudo_samples)} 个伪标签 (阈值={cfg.pseudo_threshold})\")\n    \n    if len(pseudo_samples) > 0:\n        class PseudoDataset(Dataset):\n            def __init__(self, samples):\n                self.samples = samples\n            def __len__(self):\n                return len(self.samples)\n            def __getitem__(self, idx):\n                s = self.samples[idx]\n                return s['spec'], torch.tensor(s['label'], dtype=torch.long)\n        \n        pseudo_dataset = PseudoDataset(pseudo_samples)\n        pseudo_loader = DataLoader(pseudo_dataset, batch_size=cfg.batch_size, shuffle=True, num_workers=2)\n        print(f\"伪标签数据集创建完成: {len(pseudo_dataset)} 个样本\")\n    else:\n        print(\"未生成任何伪标签，尝试降低阈值\")\nelse:\n    print(\"未找到声景数据，跳过伪标签生成\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 单元格9：阶段3 - 训练学生模型（使用伪标签）\nstudent_metrics = []\n\nif pseudo_loader is not None and len(pseudo_samples) > 0:\n    print(\"=\"*50)\n    print(\"阶段3: 训练学生模型（使用伪标签）\")\n    print(\"=\"*50)\n    \n    # 合并数据集\n    combined_dataset = ConcatDataset([train_dataset, pseudo_dataset])\n    combined_loader = DataLoader(combined_dataset, batch_size=cfg.batch_size, shuffle=True, num_workers=2)\n    print(f\"合并数据集: {len(train_dataset)} 标注 + {len(pseudo_dataset)} 伪标签 = {len(combined_dataset)} 总样本\")\n    \n    student_model = create_model(cfg.num_classes).to(device)\n    optimizer = optim.AdamW(student_model.parameters(), lr=cfg.learning_rate, weight_decay=cfg.weight_decay)\n    scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=cfg.epochs_student)\n    scaler = GradScaler(enabled=cfg.use_amp and device.type == 'cuda')\n    \n    best_macro_f1 = 0\n    \n    for epoch in range(cfg.epochs_student):\n        print(f\"\\n第 {epoch+1}/{cfg.epochs_student} 轮\")\n        \n        train_loss, train_acc, train_f1 = train_epoch(student_model, combined_loader, optimizer, criterion, scaler)\n        val_loss, val_acc, val_f1 = validate(student_model, val_loader, criterion)\n        scheduler.step()\n        \n        student_metrics.append({\n            'epoch': epoch+1,\n            'train_loss': train_loss,\n            'train_acc': train_acc,\n            'train_macro_f1': train_f1,\n            'val_loss': val_loss,\n            'val_acc': val_acc,\n            'val_macro_f1': val_f1\n        })\n        \n        print(f\"训练 - Loss: {train_loss:.4f}, Acc: {train_acc:.4f}, Macro F1: {train_f1:.4f}\")\n        print(f\"验证 - Loss: {val_loss:.4f}, Acc: {val_acc:.4f}, Macro F1: {val_f1:.4f}\")\n        \n        if val_f1 > best_macro_f1:\n            best_macro_f1 = val_f1\n            torch.save(student_model.state_dict(), os.path.join(cfg.output_dir, 'best_student.pth'))\n            print(f\"✓ 保存最佳学生模型, Macro F1: {val_f1:.4f}\")\n    \n    pd.DataFrame(student_metrics).to_csv(os.path.join(cfg.output_dir, 'student_metrics.csv'), index=False)\n    print(\"\\n学生模型训练完成\")\nelse:\n    print(\"\\n跳过阶段3: 无伪标签可用\")\n    student_metrics = teacher_metrics","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 单元格10：结果汇总和保存\nprint(\"=\"*50)\nprint(\"结果汇总\")\nprint(\"=\"*50)\n\n# 获取最佳结果\nteacher_best = teacher_metrics[np.argmax([m['val_macro_f1'] for m in teacher_metrics])]\nif student_metrics and len(student_metrics) > 0:\n    student_best = student_metrics[np.argmax([m['val_macro_f1'] for m in student_metrics])]\nelse:\n    student_best = teacher_best\n\n# 保存最终结果表格\nresults_df = pd.DataFrame({\n    '方法': ['监督基线', '半监督方法'],\n    '最佳验证准确率': [teacher_best['val_acc'], student_best['val_acc']],\n    '最佳Macro F1': [teacher_best['val_macro_f1'], student_best['val_macro_f1']]\n})\nresults_df.to_csv(os.path.join(cfg.output_dir, 'final_results.csv'), index=False)\n\nprint(\"\\n最终结果:\")\nprint(results_df.to_string(index=False))\nprint(f\"\\n输出目录: {cfg.output_dir}\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 单元格11：生成论文图表\nprint(\"=\"*50)\nprint(\"生成论文图表\")\nprint(\"=\"*50)\n\n# 图1：长尾分布\nplt.figure(figsize=(10, 5))\ncounts = sorted(class_counts.values(), reverse=True)\nplt.bar(range(len(counts)), counts, color='steelblue', alpha=0.8)\nplt.xlabel('物种排名', fontsize=12)\nplt.ylabel('样本数量', fontsize=12)\nplt.title('鸟类物种长尾分布图', fontsize=14)\nplt.yscale('log')\nplt.grid(True, alpha=0.3)\nplt.tight_layout()\nplt.savefig(os.path.join(cfg.output_dir, 'fig1_longtail.png'), dpi=300, bbox_inches='tight')\nplt.close()\nprint(\"✓ 图1: 长尾分布图\")\n\n# 图2：训练曲线对比\nif student_metrics != teacher_metrics:\n    fig, axes = plt.subplots(1, 2, figsize=(14, 5))\n    \n    axes[0].plot([m['epoch'] for m in teacher_metrics], [m['val_acc'] for m in teacher_metrics], \n                 'b-', label='教师模型', linewidth=2)\n    axes[0].plot([m['epoch'] for m in student_metrics], [m['val_acc'] for m in student_metrics], \n                 'r-', label='学生模型', linewidth=2)\n    axes[0].set_xlabel('训练轮次', fontsize=12)\n    axes[0].set_ylabel('验证准确率', fontsize=12)\n    axes[0].set_title('准确率对比', fontsize=14)\n    axes[0].legend()\n    axes[0].grid(True, alpha=0.3)\n    \n    axes[1].plot([m['epoch'] for m in teacher_metrics], [m['val_macro_f1'] for m in teacher_metrics], \n                 'b-', label='教师模型', linewidth=2)\n    axes[1].plot([m['epoch'] for m in student_metrics], [m['val_macro_f1'] for m in student_metrics], \n                 'r-', label='学生模型', linewidth=2)\n    axes[1].set_xlabel('训练轮次', fontsize=12)\n    axes[1].set_ylabel('Macro F1分数', fontsize=12)\n    axes[1].set_title('Macro F1对比', fontsize=14)\n    axes[1].legend()\n    axes[1].grid(True, alpha=0.3)\n    \n    plt.tight_layout()\n    plt.savefig(os.path.join(cfg.output_dir, 'fig2_training_curves.png'), dpi=300, bbox_inches='tight')\n    plt.close()\n    print(\"✓ 图2: 训练曲线对比图\")\n\n# 图3：损失曲线\nplt.figure(figsize=(10, 6))\nplt.plot([m['epoch'] for m in teacher_metrics], [m['train_loss'] for m in teacher_metrics], \n         'b-', label='教师训练损失', linewidth=2)\nplt.plot([m['epoch'] for m in teacher_metrics], [m['val_loss'] for m in teacher_metrics], \n         'b--', label='教师验证损失', linewidth=2)\nif student_metrics != teacher_metrics:\n    plt.plot([m['epoch'] for m in student_metrics], [m['train_loss'] for m in student_metrics], \n             'r-', label='学生训练损失', linewidth=2)\n    plt.plot([m['epoch'] for m in student_metrics], [m['val_loss'] for m in student_metrics], \n             'r--', label='学生验证损失', linewidth=2)\nplt.xlabel('训练轮次', fontsize=12)\nplt.ylabel('损失值', fontsize=12)\nplt.title('训练与验证损失曲线', fontsize=14)\nplt.legend()\nplt.grid(True, alpha=0.3)\nplt.tight_layout()\nplt.savefig(os.path.join(cfg.output_dir, 'fig3_loss_curves.png'), dpi=300, bbox_inches='tight')\nplt.close()\nprint(\"✓ 图3: 损失曲线图\")\n\n# 图4：结果对比柱状图\nfig, ax = plt.subplots(figsize=(8, 6))\nmethods = ['监督基线', '半监督方法']\nacc_values = [teacher_best['val_acc'], student_best['val_acc']]\nf1_values = [teacher_best['val_macro_f1'], student_best['val_macro_f1']]\n\nx = np.arange(len(methods))\nwidth = 0.35\n\nbars1 = ax.bar(x - width/2, acc_values, width, label='准确率', color='steelblue', alpha=0.8)\nbars2 = ax.bar(x + width/2, f1_values, width, label='Macro F1', color='coral', alpha=0.8)\n\nax.set_xlabel('方法', fontsize=12)\nax.set_ylabel('分数', fontsize=12)\nax.set_title('性能对比', fontsize=14)\nax.set_xticks(x)\nax.set_xticklabels(methods)\nax.legend()\nax.grid(True, alpha=0.3, axis='y')\n\nfor bar in bars1:\n    ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.01, \n            f'{bar.get_height():.4f}', ha='center', va='bottom', fontsize=10)\nfor bar in bars2:\n    ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.01,\n            f'{bar.get_height():.4f}', ha='center', va='bottom', fontsize=10)\n\nplt.tight_layout()\nplt.savefig(os.path.join(cfg.output_dir, 'fig4_comparison_bar.png'), dpi=300, bbox_inches='tight')\nplt.close()\nprint(\"✓ 图4: 结果对比柱状图\")\n\n# 图5：Top-20类别分布\nplt.figure(figsize=(12, 6))\ntop20_counts = dict(sorted(class_counts.items(), key=lambda x: x[1], reverse=True)[:20])\nspecies_names = list(top20_counts.keys())\nsample_counts = list(top20_counts.values())\n\nplt.barh(range(len(species_names)), sample_counts, color='steelblue', alpha=0.8)\nplt.yticks(range(len(species_names)), species_names, fontsize=9)\nplt.xlabel('样本数量', fontsize=12)\nplt.ylabel('物种', fontsize=12)\nplt.title('样本数量前20的物种分布', fontsize=14)\nplt.gca().invert_yaxis()\nplt.grid(True, alpha=0.3, axis='x')\nplt.tight_layout()\nplt.savefig(os.path.join(cfg.output_dir, 'fig5_top20.png'), dpi=300, bbox_inches='tight')\nplt.close()\nprint(\"✓ 图5: Top-20类别分布图\")\n\nprint(f\"\\n所有图表已保存到: {cfg.output_dir}\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 单元格12：打印实验统计信息\nprint(\"=\"*50)\nprint(\"实验统计信息\")\nprint(\"=\"*50)\n\nstats = {\n    '总物种数': cfg.num_classes,\n    '标注样本数': len(train_df),\n    '无标签声景数': len(soundscape_files) if soundscape_files else 0,\n    '教师最佳准确率': teacher_best['val_acc'],\n    '教师最佳Macro F1': teacher_best['val_macro_f1'],\n    '学生最佳准确率': student_best['val_acc'],\n    '学生最佳Macro F1': student_best['val_macro_f1'],\n    '准确率提升': student_best['val_acc'] - teacher_best['val_acc'],\n    'Macro F1提升': student_best['val_macro_f1'] - teacher_best['val_macro_f1']\n}\n\nstats_df = pd.DataFrame([stats])\nstats_df.to_csv(os.path.join(cfg.output_dir, 'experiment_statistics.csv'), index=False)\n\nprint(f\"总物种数: {cfg.num_classes}\")\nprint(f\"标注样本数: {len(train_df)}\")\nprint(f\"无标签声景数: {len(soundscape_files) if soundscape_files else 0}\")\nprint(\"-\"*30)\nprint(f\"教师模型（基线）:\")\nprint(f\"  最佳验证准确率: {teacher_best['val_acc']:.4f}\")\nprint(f\"  最佳Macro F1: {teacher_best['val_macro_f1']:.4f}\")\nprint(\"-\"*30)\nprint(f\"学生模型（半监督）:\")\nprint(f\"  最佳验证准确率: {student_best['val_acc']:.4f}\")\nprint(f\"  最佳Macro F1: {student_best['val_macro_f1']:.4f}\")\nprint(\"-\"*30)\nprint(f\"提升:\")\nprint(f\"  准确率: +{(student_best['val_acc'] - teacher_best['val_acc'])*100:.2f}%\")\nprint(f\"  Macro F1: +{(student_best['val_macro_f1'] - teacher_best['val_macro_f1']):.4f}\")\nprint(\"=\"*50)\nprint(\"\\n全部完成！\")","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}