{"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":44224,"databundleVersionId":5188730,"sourceType":"competition"}],"dockerImageVersionId":31193,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# ============================================================\n# Cell 1: Install & Imports & Config\n# ============================================================\n!pip install timm noisereduce --quiet\n\nimport os\nimport math\nimport random\nimport time\nimport warnings\n\nwarnings.filterwarnings(\"ignore\")\n\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\n\nimport librosa\nimport librosa.display\n\nfrom scipy.signal import butter, filtfilt\n\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import Dataset, DataLoader\nimport timm\nfrom tqdm import tqdm\n\ntry:\n    import noisereduce as nr\n    HAS_NR = True\nexcept ImportError:\n    HAS_NR = False\n    print(\"noisereduce not installed, will skip noise reduction.\")\n\nclass CFG:\n    seed = 42\n    sample_rate = 32000\n    n_mels = 256\n    fmin = 80\n    fmax = 15000\n    duration = 10.0        # seconds\n    train_batch_size = 24\n    valid_batch_size = 24\n    # traning ephoch\n    epochs = 15            # 为了在一天内跑完，短一点即可\n    lr = 3e-4\n    model_name = \"tf_efficientnet_b2\"\n    num_workers = 4\n    device = \"cuda\" if torch.cuda.is_available() else \"cpu\"\n    use_subset = True     # 只用子集，适合课程项目\n    subset_n = 16941       # 使用多少条样本（可以调）\n    train_metadata_path = \"/kaggle/input/birdclef-2023/train_metadata.csv\"\n    audio_dir = \"/kaggle/input/birdclef-2023/train_audio\"\n    model_save_path = \"/kaggle/working/tf_efficientnet_b2.pth\"\n\n\ndef set_seed(seed=42):\n    random.seed(seed)\n    np.random.seed(seed)\n    torch.manual_seed(seed)\n    torch.cuda.manual_seed_all(seed)\n\n\nset_seed(CFG.seed)\nprint(\"Using device:\", CFG.device)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-18T08:28:13.965834Z","iopub.execute_input":"2025-11-18T08:28:13.966105Z","iopub.status.idle":"2025-11-18T08:29:49.186840Z","shell.execute_reply.started":"2025-11-18T08:28:13.966082Z","shell.execute_reply":"2025-11-18T08:29:49.185969Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# Cell 2: Preprocessing (bandpass + noise reduction + mel)\n# 相当于 preprocessing.py\n# ============================================================\n\ndef bandpass_filter(data, sr, low=1000, high=12000, order=4):\n    \"\"\"简单带通滤波，突出鸟叫频段\"\"\"\n    nyq = 0.5 * sr\n    low_norm = low / nyq\n    high_norm = high / nyq\n    b, a = butter(order, [low_norm, high_norm], btype='band')\n    filtered = filtfilt(b, a, data)\n    return filtered\n\n\ndef apply_noise_reduction(audio, sr):\n    \"\"\"如果有 noisereduce，则做降噪，否则原样返回\"\"\"\n    if HAS_NR:\n        return nr.reduce_noise(y=audio, sr=sr)\n    else:\n        return audio\n\n\ndef load_audio(path, sr):\n    audio, _ = librosa.load(path, sr=sr, mono=True)\n    return audio\n\n\ndef crop_or_pad(audio, sr, duration, random_start=True):\n    \"\"\"裁剪或填充到固定时长 duration 秒\"\"\"\n    target_len = int(sr * duration)\n    if len(audio) < target_len:\n        pad_len = target_len - len(audio)\n        audio = np.concatenate([audio, np.zeros(pad_len, dtype=audio.dtype)])\n    elif len(audio) > target_len:\n        if random_start:\n            start = np.random.randint(0, len(audio) - target_len + 1)\n        else:\n            start = 0\n        audio = audio[start:start + target_len]\n    return audio\n\n\ndef audio_to_mel(audio, sr, n_mels, fmin, fmax):\n    \"\"\"audio -> log-mel spectrogram\"\"\"\n    mel = librosa.feature.melspectrogram(\n        y=audio,\n        sr=sr,\n        n_mels=n_mels,\n        fmin=fmin,\n        fmax=fmax\n    )\n    mel_db = librosa.power_to_db(mel, ref=np.max)\n    # 标准化有助于训练\n    mean = mel_db.mean()\n    std = mel_db.std() + 1e-6\n    mel_db = (mel_db - mean) / std\n    return mel_db.astype(np.float32)\n\n\ndef preprocess_audio_segment(audio, sr):\n    \"\"\"完整预处理：带通滤波 + 降噪 + mel\"\"\"\n    audio = bandpass_filter(audio, sr)\n    audio = apply_noise_reduction(audio, sr)\n    mel = audio_to_mel(audio, sr, CFG.n_mels, CFG.fmin, CFG.fmax)\n    return mel","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-18T08:29:49.188085Z","iopub.execute_input":"2025-11-18T08:29:49.188325Z","iopub.status.idle":"2025-11-18T08:29:49.196910Z","shell.execute_reply.started":"2025-11-18T08:29:49.188302Z","shell.execute_reply":"2025-11-18T08:29:49.196251Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# Cell 3: Dataset & Label Encoding\n# 相当于 dataset.py\n# ============================================================\n\nmetadata = pd.read_csv(CFG.train_metadata_path)\nprint(\"Total rows in metadata:\", len(metadata))\n\n# 为了课程项目，一般只用部分数据（比如 subset_n 条）\nif CFG.use_subset:\n    # 只保留 rating >= 3 的较高质量样本再采样\n    filtered = metadata[metadata['rating'] >= 3.0]\n    if len(filtered) >= CFG.subset_n:\n        metadata = filtered.sample(CFG.subset_n, random_state=CFG.seed)\n    else:\n        metadata = filtered\n    metadata = metadata.reset_index(drop=True)\n    print(\"Using subset size:\", len(metadata))\n\n# label 编码\nprimary_labels = metadata['primary_label'].unique()\nprimary_labels = np.sort(primary_labels)\nlabel2id = {label: i for i, label in enumerate(primary_labels)}\nid2label = {i: label for label, i in label2id.items()}\nnum_classes = len(primary_labels)\nprint(\"Num classes:\", num_classes)\n\n\nclass BirdClefDataset(Dataset):\n    def __init__(self, df, audio_dir, sr, duration, is_train=True):\n        self.df = df.reset_index(drop=True)\n        self.audio_dir = audio_dir\n        self.sr = sr\n        self.duration = duration\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        label = row['primary_label']\n        label_id = label2id[label]\n\n        # audio path：train_audio/primary_label/filename\n        filename = row['filename']\n        # species_dir = os.path.join(self.audio_dir, label)\n        # file_path = os.path.join(species_dir, filename)\n        # 如果 filename 已经自带子目录，不要重复加入 primary_label\n        if \"/\" in filename:\n            file_path = os.path.join(self.audio_dir, filename)\n        else:\n            file_path = os.path.join(self.audio_dir, label, filename)\n\n        # 读取音频\n        audio = load_audio(file_path, self.sr)\n        # 裁剪/填充到固定时长\n        audio = crop_or_pad(audio, self.sr, self.duration, random_start=self.is_train)\n        # 预处理 -> mel\n        mel = preprocess_audio_segment(audio, self.sr)  # (n_mels, time)\n\n        # 转成 tensor，增加 channel 维度\n        mel_tensor = torch.tensor(mel).unsqueeze(0)  # (1, n_mels, time)\n\n        # one-hot 多标签向量（这里只用 primary_label）\n        target = np.zeros(num_classes, dtype=np.float32)\n        target[label_id] = 1.0\n        target_tensor = torch.tensor(target)\n\n        return mel_tensor, target_tensor\n\n\n# train/valid 划分\nfrom sklearn.model_selection import train_test_split\n\ntrain_df, valid_df = train_test_split(\n    metadata,\n    test_size=0.2,\n    random_state=CFG.seed\n)\n\ntrain_ds = BirdClefDataset(train_df, CFG.audio_dir, CFG.sample_rate, CFG.duration, is_train=True)\nvalid_ds = BirdClefDataset(valid_df, CFG.audio_dir, CFG.sample_rate, CFG.duration, is_train=False)\n\ntrain_loader = DataLoader(\n    train_ds,\n    batch_size=CFG.train_batch_size,\n    shuffle=True,\n    num_workers=CFG.num_workers,\n    pin_memory=True\n)\n\nvalid_loader = DataLoader(\n    valid_ds,\n    batch_size=CFG.valid_batch_size,\n    shuffle=False,\n    num_workers=CFG.num_workers,\n    pin_memory=True\n)\n\nprint(\"Train batches:\", len(train_loader), \"Valid batches:\", len(valid_loader))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-18T08:29:49.197662Z","iopub.execute_input":"2025-11-18T08:29:49.197916Z","iopub.status.idle":"2025-11-18T08:29:50.376117Z","shell.execute_reply.started":"2025-11-18T08:29:49.197893Z","shell.execute_reply":"2025-11-18T08:29:50.375261Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# Cell 4: EfficientNet Model\n# 相当于 model.py\n# ============================================================\n\nclass BirdCLEFModel(nn.Module):\n    def __init__(self, model_name, num_classes, pretrained=True):\n        super().__init__()\n        self.backbone = timm.create_model(\n            model_name,\n            pretrained=pretrained,\n            in_chans=3   # 我们会把单通道 mel repeat 成 3 通道\n        )\n        if hasattr(self.backbone, \"get_classifier\"):\n            in_features = self.backbone.get_classifier().in_features\n            self.backbone.reset_classifier(0)\n        else:\n            # fallback\n            in_features = self.backbone.num_features\n            self.backbone.classifier = nn.Identity()\n        self.classifier = nn.Linear(in_features, num_classes)\n\n    def forward(self, x):\n        # x: (B, 1, H, W) -> (B, 3, H, W)\n        if x.shape[1] == 1:\n            x = x.repeat(1, 3, 1, 1)\n        feats = self.backbone(x)\n        logits = self.classifier(feats)\n        return logits\n\n\nmodel = BirdCLEFModel(CFG.model_name, num_classes)\nmodel.to(CFG.device)\nprint(\"Model created.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-18T08:29:50.378061Z","iopub.execute_input":"2025-11-18T08:29:50.378343Z","iopub.status.idle":"2025-11-18T08:29:52.334042Z","shell.execute_reply.started":"2025-11-18T08:29:50.378321Z","shell.execute_reply":"2025-11-18T08:29:52.333369Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# Cell 5: Training Loop\n# 相当于 train.py\n# ============================================================\n\ncriterion = nn.BCEWithLogitsLoss()\noptimizer = torch.optim.Adam(model.parameters(), lr=CFG.lr)\nscheduler = torch.optim.lr_scheduler.CosineAnnealingLR(\n    optimizer, T_max=CFG.epochs\n)\n\n\ndef train_one_epoch(model, loader, optimizer, criterion, device):\n    model.train()\n    running_loss = 0.0\n    for mel, target in tqdm(loader, desc=\"Train\", leave=False):\n        mel = mel.to(device)       # (B, 1, n_mels, time)\n        target = target.to(device) # (B, num_classes)\n\n        optimizer.zero_grad()\n        logits = model(mel)\n        loss = criterion(logits, target)\n        loss.backward()\n        optimizer.step()\n\n        running_loss += loss.item() * mel.size(0)\n    epoch_loss = running_loss / len(loader.dataset)\n    return epoch_loss\n\n\ndef validate_one_epoch(model, loader, criterion, device):\n    model.eval()\n    running_loss = 0.0\n    preds_all = []\n    targets_all = []\n\n    with torch.no_grad():\n        for mel, target in tqdm(loader, desc=\"Valid\", leave=False):\n            mel = mel.to(device)\n            target = target.to(device)\n\n            logits = model(mel)\n            loss = criterion(logits, target)\n\n            running_loss += loss.item() * mel.size(0)\n            preds_all.append(torch.sigmoid(logits).cpu().numpy())\n            targets_all.append(target.cpu().numpy())\n\n    epoch_loss = running_loss / len(loader.dataset)\n    preds_all = np.concatenate(preds_all, axis=0)\n    targets_all = np.concatenate(targets_all, axis=0)\n\n    # 简单计算一个平均 accuracy-like 指标：取 argmax\n    pred_labels = preds_all.argmax(axis=1)\n    true_labels = targets_all.argmax(axis=1)\n    acc = (pred_labels == true_labels).mean()\n\n    return epoch_loss, acc\n\n\nbest_val_loss = np.inf\nhistory = {\n    \"train_loss\": [],\n    \"valid_loss\": [],\n    \"valid_acc\": []\n}\n\nfor epoch in range(1, CFG.epochs + 1):\n    print(f\"Epoch {epoch}/{CFG.epochs}\")\n    train_loss = train_one_epoch(model, train_loader, optimizer, criterion, CFG.device)\n    valid_loss, valid_acc = validate_one_epoch(model, valid_loader, criterion, CFG.device)\n    scheduler.step()\n\n    history[\"train_loss\"].append(train_loss)\n    history[\"valid_loss\"].append(valid_loss)\n    history[\"valid_acc\"].append(valid_acc)\n\n    print(f\"  train_loss: {train_loss:.4f}  valid_loss: {valid_loss:.4f}  valid_acc: {valid_acc:.4f}\")\n\n    if valid_loss < best_val_loss:\n        best_val_loss = valid_loss\n        torch.save(model.state_dict(), CFG.model_save_path)\n        print(f\"  Saved best model to {CFG.model_save_path}\")\n\nprint(\"Training done. Best val loss:\", best_val_loss)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-18T08:29:52.334714Z","iopub.execute_input":"2025-11-18T08:29:52.334911Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# Cell 6: Simple Visualization (for your report)\n# ============================================================\n\nplt.figure(figsize=(8,4))\nplt.plot(history[\"train_loss\"], label=\"train_loss\")\nplt.plot(history[\"valid_loss\"], label=\"valid_loss\")\nplt.xlabel(\"Epoch\")\nplt.ylabel(\"Loss\")\nplt.legend()\nplt.title(\"Training & Validation Loss\")\nplt.show()\n\nplt.figure(figsize=(6,4))\nplt.plot(history[\"valid_acc\"], marker=\"o\")\nplt.xlabel(\"Epoch\")\nplt.ylabel(\"Valid Acc (argmax)\")\nplt.title(\"Validation Accuracy-like metric\")\nplt.show()","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# Cell 7: Inference Helper\n# 相当于 inference.py\n# ============================================================\n\ndef load_trained_model(model_path):\n    model = BirdCLEFModel(CFG.model_name, num_classes)\n    sd = torch.load(model_path, map_location=CFG.device)\n    model.load_state_dict(sd)\n    model.to(CFG.device)\n    model.eval()\n    return model\n\n\ndef predict_on_file(model, file_path, window_sec=5.0, threshold=0.5):\n    \"\"\"\n    对一条长音频做滑窗预测，返回每个窗口的 top class 及概率\n    \"\"\"\n    sr = CFG.sample_rate\n    audio = load_audio(file_path, sr)\n    results = []\n\n    win_len = int(window_sec * sr)\n    if len(audio) < win_len:\n        audio = np.concatenate([audio, np.zeros(win_len - len(audio))])\n\n    # 每个窗口移动 window_sec 秒（可以改成有重叠）\n    for start in range(0, len(audio) - win_len + 1, win_len):\n        seg = audio[start:start + win_len]\n        mel = preprocess_audio_segment(seg, sr)  # (n_mels, time)\n        mel_tensor = torch.tensor(mel).unsqueeze(0).unsqueeze(0)  # (1,1,H,W)\n        mel_tensor = mel_tensor.to(CFG.device)\n\n        with torch.no_grad():\n            logits = model(mel_tensor)\n            probs = torch.sigmoid(logits).cpu().numpy()[0]\n\n        top_idx = probs.argmax()\n        top_prob = probs[top_idx]\n        label_name = id2label[top_idx]\n\n        if top_prob >= threshold:\n            results.append({\n                \"start_sec\": start / sr,\n                \"end_sec\": (start + win_len) / sr,\n                \"label\": label_name,\n                \"prob\": float(top_prob)\n            })\n\n    return results","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# Cell 8: Example inference on one training file\n# ============================================================\n\n# 加载最佳模型\nbest_model = load_trained_model(CFG.model_save_path)\n\n# 从 valid_df 中随便拿一条样本来测试\nsample_row = valid_df.iloc[0]\nsample_label = sample_row[\"primary_label\"]\nsample_filename = sample_row[\"filename\"]\nif \"/\" in sample_filename:\n    # filename 带子目录，例如 \"gyhspa1/XC610092.ogg\"\n    sample_path = os.path.join(CFG.audio_dir, sample_filename)\nelse:\n    sample_path = os.path.join(CFG.audio_dir, sample_label, sample_filename)\nprint(\"Sample file:\", sample_path)\n\npred_results = predict_on_file(best_model, sample_path, window_sec=CFG.duration, threshold=0.3)\nprint(\"Prediction windows:\")\nfor r in pred_results:\n    print(r)\n\nwith torch.no_grad():\n    mel = preprocess_audio_segment(load_audio(sample_path, CFG.sample_rate), CFG.sample_rate)\n    mel_tensor = torch.tensor(mel).unsqueeze(0).unsqueeze(0).to(CFG.device)\n    logits = best_model(mel_tensor)\n    probs = torch.sigmoid(logits).cpu().numpy()[0]\n\ntop_idx = probs.argmax()\nprint(\"Top class:\", id2label[top_idx])\nprint(\"Top prob:\", probs[top_idx])","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def predict_on_file(model, file_path, window_sec=5.0, threshold=0.1):\n    sr = CFG.sample_rate\n    audio = load_audio(file_path, sr)\n    results = []\n\n    win_len = int(window_sec * sr)\n    if len(audio) < win_len:\n        audio = np.concatenate([audio, np.zeros(win_len - len(audio))])\n\n    for start in range(0, len(audio) - win_len + 1, win_len):\n        seg = audio[start:start + win_len]\n\n        mel = preprocess_audio_segment(seg, sr)\n        mel_tensor = torch.tensor(mel).unsqueeze(0).unsqueeze(0).to(CFG.device)\n\n        with torch.no_grad():\n            logits = model(mel_tensor)\n            probs = torch.sigmoid(logits).cpu().numpy()[0]\n\n        top_idx = probs.argmax()\n        top_prob = float(probs[top_idx])\n        label_name = id2label[top_idx]\n\n        results.append({\n            \"start_sec\": start / sr,\n            \"end_sec\": (start + win_len) / sr,\n            \"label\": label_name,\n            \"prob\": top_prob,\n            \"is_bird\": top_prob >= threshold\n        })\n\n    return results\n\npred_results = predict_on_file(best_model, sample_path, threshold=0.05)\nfor r in pred_results:\n    print(r)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# Cell 9: Multi-window prediction + visualization\n# ============================================================\n\nimport matplotlib.pyplot as plt\nimport numpy as np\n\ndef visualize_prediction_windows(model, file_path, window_sec=5.0):\n    sr = CFG.sample_rate\n    audio = load_audio(file_path, sr)\n\n    win_len = int(window_sec * sr)\n    results = []\n    probs_all = []\n    labels_all = []\n\n    # 分窗预测\n    for start in range(0, len(audio) - win_len + 1, win_len):\n        seg = audio[start:start + win_len]\n        mel = preprocess_audio_segment(seg, sr)\n        mel_tensor = torch.tensor(mel).unsqueeze(0).unsqueeze(0).to(CFG.device)\n\n        with torch.no_grad():\n            logits = model(mel_tensor)\n            probs = torch.sigmoid(logits).cpu().numpy()[0]\n\n        top_idx = probs.argmax()\n        top_label = id2label[top_idx]\n        top_prob = float(probs[top_idx])\n\n        results.append((start/sr, (start+win_len)/sr, top_label, top_prob))\n        probs_all.append(top_prob)\n        labels_all.append(top_label)\n\n    # 打印表格\n    print(\"=== Window Predictions Table ===\")\n    for r in results:\n        print(f\"{r[0]:5.1f}s - {r[1]:5.1f}s | {r[2]:10s} | prob={r[3]:.3f}\")\n\n    # ---- 图 1：概率折线图 ----\n    plt.figure(figsize=(14,5))\n    plt.plot(probs_all, marker='o')\n    plt.title(\"Window-level Top1 Probability over Time\", fontsize=15)\n    plt.xlabel(\"Window Index\")\n    plt.ylabel(\"Probability\")\n    plt.grid(True)\n    plt.show()\n\n    # ---- 图 2：Label 时间线图 ----\n    plt.figure(figsize=(14,3))\n    plt.plot(labels_all, marker='o')\n    plt.title(\"Window-level Predicted Labels\", fontsize=15)\n    plt.xlabel(\"Window Index\")\n    plt.ylabel(\"Label\")\n    plt.xticks(rotation=45)\n    plt.grid(True)\n    plt.show()\n\n    return results","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# Cell 10: Run multi-window visualization on sample audio\n# ============================================================\n\n# 随机从验证集取一个例子\nsample_row = valid_df.sample(1).iloc[0]\nsample_label = sample_row[\"primary_label\"]\nsample_filename = sample_row[\"filename\"]\n\nif \"/\" in sample_filename:\n    sample_path = os.path.join(CFG.audio_dir, sample_filename)\nelse:\n    sample_path = os.path.join(CFG.audio_dir, sample_label, sample_filename)\n\nprint(\"Testing file:\", sample_path)\n\n# 运行可视化\nviz_results = visualize_prediction_windows(best_model, sample_path, window_sec=CFG.duration)\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# Cell 11: Predict multiple audio files and produce a summary table\n# ============================================================\n\ndef predict_multiple_samples(model, df, num_samples=5):\n    rows = df.sample(num_samples)\n    table = []\n\n    for _, row in rows.iterrows():\n        label = row[\"primary_label\"]\n        filename = row[\"filename\"]\n\n        if \"/\" in filename:\n            path = os.path.join(CFG.audio_dir, filename)\n        else:\n            path = os.path.join(CFG.audio_dir, label, filename)\n\n        # 单次推理\n        audio = load_audio(path, CFG.sample_rate)\n        mel = preprocess_audio_segment(audio, CFG.sample_rate)\n        mel_tensor = torch.tensor(mel).unsqueeze(0).unsqueeze(0).to(CFG.device)\n\n        with torch.no_grad():\n            logits = model(mel_tensor)\n            probs = torch.sigmoid(logits).cpu().numpy()[0]\n\n        top_idx = probs.argmax()\n        pred_label = id2label[top_idx]\n        pred_prob = float(probs[top_idx])\n\n        table.append([filename, label, pred_label, pred_prob])\n\n    df_table = pd.DataFrame(table,\n                            columns=[\"filename\", \"true_label\", \"pred_label\", \"prob\"])\n    return df_table\n\n# 运行它\nsummary_table = predict_multiple_samples(best_model, valid_df, num_samples=10)\nsummary_table\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# Cell 12: Plot Mel Spectrogram for sample file\n# ============================================================\n\naudio = load_audio(sample_path, CFG.sample_rate)\nmel = preprocess_audio_segment(audio, CFG.sample_rate)\n\nplt.figure(figsize=(12,4))\nlibrosa.display.specshow(mel, sr=CFG.sample_rate, hop_length=512, x_axis='time', y_axis='mel')\nplt.colorbar(label='dB')\nplt.title(\"Mel Spectrogram of Sample Audio\")\nplt.show()\n","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}