{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.10.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[{"sourceId":91844,"databundleVersionId":11361821,"sourceType":"competition"},{"sourceId":11590046,"sourceType":"datasetVersion","datasetId":7267557}],"dockerImageVersionId":30918,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"<h1 style=\"color: #6cb4e4;  text-align: center;  padding: 0.25em;  border-top: solid 2.5px #6cb4e4;  border-bottom: solid 2.5px #6cb4e4;  background: -webkit-repeating-linear-gradient(-45deg, #f0f8ff, #f0f8ff 3px,#e9f4ff 3px, #e9f4ff 7px);  background: repeating-linear-gradient(-45deg, #f0f8ff, #f0f8ff 3px,#e9f4ff 3px, #e9f4ff 7px);height:45px;\">\n<b>\nOnly Submission(LoadLocalTrainModel)\n</b></h1> ","metadata":{}},{"cell_type":"markdown","source":"### **ℹ️INFO**\n* This notebook is an inference notebook.\n* that performed a unique LocalTrain based on the great Train/Inference published by the Kadircan İdrisoğlu.\n    * [PP] https://www.kaggle.com/code/kadircandrisolu/transforming-audio-to-mel-spec-birdclef-25\n    * [TRAIN] https://www.kaggle.com/code/kadircandrisolu/efficientnet-b0-pytorch-train-birdclef-25\n    * [INF] https://www.kaggle.com/code/kadircandrisolu/efficientnet-b0-pytorch-inference-birdclef-25\n\n### **ℹ️2025/04/28 MyLocalTrainResult**\n* trained using FocalLossBCE, which was used in the previous competition, BirdCLEF 2024 8th place solution. The results were good.\n    ```\n    0.9652\n    0.9605\n    0.9607\n    0.9626\n    [OOF]0.9622\n    ```","metadata":{}},{"cell_type":"code","source":"import os\nimport gc\nimport warnings\nimport logging\nimport time\nimport math\nimport cv2\nfrom pathlib import Path\n\nimport numpy as np\nimport pandas as pd\nimport librosa\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nimport timm\nfrom tqdm.auto import tqdm\nimport torchvision\nwarnings.filterwarnings(\"ignore\")\nlogging.basicConfig(level=logging.ERROR)\n\nimport re\nfrom torch.quantization import quantize_dynamic","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-27T16:50:45.291518Z","iopub.execute_input":"2025-05-27T16:50:45.292659Z","iopub.status.idle":"2025-05-27T16:51:01.725640Z","shell.execute_reply.started":"2025-05-27T16:50:45.292584Z","shell.execute_reply":"2025-05-27T16:51:01.724156Z"},"_kg_hide-input":true,"_kg_hide-output":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\"\"\" \n    FocalLossBCE Use Example\n\"\"\"\nclass FocalLossBCE(torch.nn.Module):\n    def __init__(\n            self,\n            alpha: float = 0.25,\n            gamma: float = 2,\n            reduction: str = \"mean\",\n            bce_weight: float = 0.6,\n            focal_weight: float = 1.4,\n    ):\n        super().__init__()\n        self.alpha = alpha\n        self.gamma = gamma\n        self.reduction = reduction\n        self.bce = torch.nn.BCEWithLogitsLoss(reduction=reduction)\n        self.bce_weight = bce_weight\n        self.focal_weight = focal_weight\n\n    def forward(self, logits, targets):\n        focall_loss = torchvision.ops.focal_loss.sigmoid_focal_loss(\n            inputs=logits,\n            targets=targets,\n            alpha=self.alpha,\n            gamma=self.gamma,\n            reduction=self.reduction,\n        )\n        bce_loss = self.bce(logits, targets)\n        return self.bce_weight * bce_loss + self.focal_weight * focall_loss\n\ndef get_criterion(cfg):\n    return FocalLossBCE()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-27T16:51:01.728624Z","iopub.execute_input":"2025-05-27T16:51:01.729109Z","iopub.status.idle":"2025-05-27T16:51:01.737098Z","shell.execute_reply.started":"2025-05-27T16:51:01.729068Z","shell.execute_reply":"2025-05-27T16:51:01.735748Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"<h1 style=\"color: #6cb4e4;  text-align: center;  padding: 0.25em;  border-top: solid 2.5px #6cb4e4;  border-bottom: solid 2.5px #6cb4e4;  background: -webkit-repeating-linear-gradient(-45deg, #f0f8ff, #f0f8ff 3px,#e9f4ff 3px, #e9f4ff 7px);  background: repeating-linear-gradient(-45deg, #f0f8ff, #f0f8ff 3px,#e9f4ff 3px, #e9f4ff 7px);height:45px;\">\n<b>\nInference Pipeline\n</b></h1> ","metadata":{}},{"cell_type":"markdown","source":"## **》》》Env**","metadata":{}},{"cell_type":"code","source":"class CFG:\n    # ------------------------------------------- #\n    # [IMPORTANT]\n    # * Melspectrogram & Audio Params\n    # ------------------------------------------- #\n    N_FFT = 2048\n    HOP_LENGTH = 128\n    N_MELS = 512\n    FMIN = 20\n    FMAX = 16000\n    TARGET_SHAPE = (256,256)\n    FS = 32000  \n    WINDOW_SIZE = 5\n\n    # ------------------------------------------- #\n    # * Model def\n    # ------------------------------------------- #\n    model_path = '/kaggle/input/pub-bird25-b-422-ppv15-v2-s-focallossbce'\n    model_name = 'tf_efficientnetv2_s.in21k_ft_in1k'\n    use_specific_folds = False\n    folds = [0,1,2,3]\n    in_channels = 1\n    device = 'cpu'  \n\n    # datasets\n    test_soundscapes = '/kaggle/input/birdclef-2025/test_soundscapes'\n    submission_csv = '/kaggle/input/birdclef-2025/sample_submission.csv'\n    taxonomy_csv = '/kaggle/input/birdclef-2025/taxonomy.csv'\n    \n    # Inference parameters\n    batch_size = 16\n    use_tta = False  \n    tta_count = 3\n    threshold = 0.5\n\n    # util\n    debug = False\n    debug_count = 3\n\ncfg = CFG()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-27T16:51:01.739304Z","iopub.execute_input":"2025-05-27T16:51:01.740030Z","iopub.status.idle":"2025-05-27T16:51:01.765886Z","shell.execute_reply.started":"2025-05-27T16:51:01.739928Z","shell.execute_reply":"2025-05-27T16:51:01.764237Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(f\"Using device: {cfg.device}\")\nprint(f\"Loading taxonomy data...\")\ntaxonomy_df = pd.read_csv(cfg.taxonomy_csv)\nspecies_ids = taxonomy_df['primary_label'].tolist()\nnum_classes = len(species_ids)\nprint(f\"Number of classes: {num_classes}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-27T16:51:01.767463Z","iopub.execute_input":"2025-05-27T16:51:01.768029Z","iopub.status.idle":"2025-05-27T16:51:01.824603Z","shell.execute_reply.started":"2025-05-27T16:51:01.767998Z","shell.execute_reply":"2025-05-27T16:51:01.823164Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## **》》》Model**","metadata":{}},{"cell_type":"code","source":"class BirdCLEFModel(nn.Module):\n    def __init__(self, cfg, num_classes):\n        super().__init__()\n        self.cfg = cfg\n        \n        self.backbone = timm.create_model(\n            cfg.model_name,\n            pretrained=False,  \n            in_chans=cfg.in_channels,\n            drop_rate=0.0,    \n            drop_path_rate=0.0\n        )\n        \n        backbone_out = self.backbone.classifier.in_features\n        self.backbone.classifier = nn.Identity()\n        self.pooling = nn.AdaptiveAvgPool2d(1)\n        self.feat_dim = backbone_out\n        self.classifier = nn.Linear(backbone_out, num_classes)\n        \n    def forward(self, x):\n        features = self.backbone(x)\n        if isinstance(features, dict):\n            features = features['features']\n        if len(features.shape) == 4:\n            features = self.pooling(features)\n            features = features.view(features.size(0), -1)\n\n        logits = self.classifier(features)\n        return logits","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-27T16:51:01.826907Z","iopub.execute_input":"2025-05-27T16:51:01.827238Z","iopub.status.idle":"2025-05-27T16:51:01.834825Z","shell.execute_reply.started":"2025-05-27T16:51:01.827212Z","shell.execute_reply":"2025-05-27T16:51:01.833324Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## **》》》Melspectrogram**","metadata":{}},{"cell_type":"code","source":"def audio2melspec(audio_data, cfg):\n    \"\"\"Convert audio data to mel spectrogram\"\"\"\n    if np.isnan(audio_data).any():\n        mean_signal = np.nanmean(audio_data)\n        audio_data = np.nan_to_num(audio_data, nan=mean_signal)\n\n    mel_spec = librosa.feature.melspectrogram(\n        y=audio_data,\n        sr=cfg.FS,\n        n_fft=cfg.N_FFT,\n        hop_length=cfg.HOP_LENGTH,\n        n_mels=cfg.N_MELS,\n        fmin=cfg.FMIN,\n        fmax=cfg.FMAX,\n        power=2.0,\n        pad_mode=\"reflect\",\n        norm='slaney',\n        htk=True,\n        center=True,\n    )\n\n    mel_spec_db = librosa.power_to_db(mel_spec, ref=np.max)\n    mel_spec_norm = (mel_spec_db - mel_spec_db.min()) / (mel_spec_db.max() - mel_spec_db.min() + 1e-8)\n    \n    return mel_spec_norm\n\ndef process_audio_segment(audio_data, cfg):\n    \"\"\"Process audio segment to get mel spectrogram\"\"\"\n    if len(audio_data) < cfg.FS * cfg.WINDOW_SIZE:\n        audio_data = np.pad(audio_data, \n                          (0, cfg.FS * cfg.WINDOW_SIZE - len(audio_data)), \n                          mode='constant')\n    \n    mel_spec = audio2melspec(audio_data, cfg)\n    \n    if mel_spec.shape != cfg.TARGET_SHAPE:\n        mel_spec = cv2.resize(mel_spec, cfg.TARGET_SHAPE, interpolation=cv2.INTER_LINEAR)\n        \n    return mel_spec.astype(np.float32)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-27T16:51:01.837129Z","iopub.execute_input":"2025-05-27T16:51:01.837947Z","iopub.status.idle":"2025-05-27T16:51:01.867300Z","shell.execute_reply.started":"2025-05-27T16:51:01.837897Z","shell.execute_reply":"2025-05-27T16:51:01.866149Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def find_model_files(cfg):\n    \"\"\"\n    Recursively collect all .pth files under cfg.model_path\n    \"\"\"\n    model_dir   = Path(cfg.model_path)\n    model_files = [str(p) for p in model_dir.glob(\"**/*.pth\")]\n    return model_files\n\n\ndef load_models(cfg, num_classes):\n    \"\"\"\n    Load checkpoints, compile with TorchScript, apply fp16 dynamic quantization,\n    and return a list of (model, weight) tuples for weighted ensembling.\n    \"\"\"\n    import re                                # <<< changed\n    from torch.quantization import quantize_dynamic\n\n    models      = []\n    model_files = find_model_files(cfg)\n\n    if not model_files:\n        print(f\"[WARN] No .pth found under {cfg.model_path}\")\n        return models\n\n    print(f\"Found {len(model_files)} model files.\")\n\n    # ---------- optional: only keep specified folds ----------\n    if cfg.use_specific_folds:\n        selected = []\n        for f in cfg.folds:\n            selected += [m for m in model_files if f\"fold{f}\" in m]\n        model_files = selected\n        print(f\"Using {len(model_files)} files for folds {cfg.folds}\")\n\n    # ---------- load each ckpt ----------\n    float_pat = re.compile(r\"(\\d+\\.\\d{2,4})\")      # <<< changed (两到四位小数)\n    for mp in model_files:\n        try:\n            print(f\"Loading {mp}\")\n            ckpt  = torch.load(mp, map_location=torch.device(cfg.device))\n\n            net   = BirdCLEFModel(cfg, num_classes)\n            net.load_state_dict(ckpt[\"model_state_dict\"])\n            net   = net.to(cfg.device).eval()\n\n            # ---- TorchScript + freeze ----\n            with torch.inference_mode():\n                dummy    = torch.rand(1, cfg.in_channels, *cfg.TARGET_SHAPE)\n                scripted = torch.jit.trace(net, dummy)\n                scripted = torch.jit.freeze(scripted)\n\n            # ---- fp16 dynamic quantisation (Linear layers) ----\n            scripted = quantize_dynamic(\n                scripted,\n                {torch.nn.Linear},\n                dtype=torch.float16                     # <<< changed\n            )\n\n            # ---- parse ckpt score as weight ----\n            m = float_pat.search(mp)\n            weight = float(m.group(1)) if m else 1.0   # fallback=1.0\n\n            models.append((scripted, weight))\n\n        except Exception as e:\n            print(f\"[ERR] {mp}: {e}\")\n\n    return models\n\n\n\ndef predict_on_spectrogram(audio_path, models, cfg, species_ids):\n    \"\"\"\n    Inference for one .ogg; supports TTA & weighted ensemble.\n    Returns row_ids, predictions (list of np.array(num_classes))\n    \"\"\"\n    predictions, row_ids = [], []\n    soundscape_id        = Path(audio_path).stem\n\n    try:\n        print(f\"Processing {soundscape_id}\")\n        audio_data, _  = librosa.load(audio_path, sr=cfg.FS)\n        total_segments = len(audio_data) // (cfg.FS * cfg.WINDOW_SIZE)\n\n        # --------- take model weights once (np array) ---------\n        #w_arr = np.array([w for _, w in models], dtype=np.float32)   # <<< changed\n        w_arr = np.exp( np.array([w for _, w in models], np.float32) )\n        w_arr /= w_arr.sum()                                         # 归一化\n\n        for seg_idx in range(total_segments):\n            ss = seg_idx * cfg.FS * cfg.WINDOW_SIZE\n            es = ss +      cfg.FS * cfg.WINDOW_SIZE\n            segment_audio = audio_data[ss:es]\n\n            row_ids.append(f\"{soundscape_id}_{(seg_idx+1)*cfg.WINDOW_SIZE}\")\n\n            # ---------- helper: forward pass over all models ----------\n            # def _forward(mel):\n            #     mel = torch.tensor(mel, dtype=torch.float32\n            #                        ).unsqueeze(0).unsqueeze(0).to(cfg.device)\n            #     out_list = []\n            #     for mdl, _ in models:\n            #         with torch.no_grad():\n            #             out_list.append(torch.sigmoid(mdl(mel)\n            #                             ).cpu().numpy().squeeze())\n            #     return np.vstack(out_list)                             # (N, C)\n            def _forward(mel):\n                mel = torch.tensor(mel, dtype=torch.float32).unsqueeze(0).unsqueeze(0\n                       ).to(cfg.device)\n                out_list = []\n                for mdl, _ in models:\n                    with torch.no_grad():\n                        out_list.append(mdl(mel).cpu().numpy().squeeze())   # ← 不再 sigmoid\n                return np.vstack(out_list)          # shape (N_models, C) logits\n\n            # ---------- make predictions ----------\n            if cfg.use_tta:\n                tta_stack = []\n                for tta_i in range(cfg.tta_count):\n                    mel = apply_tta(process_audio_segment(segment_audio, cfg),\n                                    tta_i)\n                    tta_stack.append(_forward(mel))\n                preds_raw = np.mean(tta_stack, axis=0)                 # (N, C)\n            else:\n                mel       = process_audio_segment(segment_audio, cfg)\n                preds_raw = _forward(mel)                              # (N, C)\n\n            # ---------- weighted average ----------\n            # if preds_raw.shape[0] == 1:\n            #     final_preds = preds_raw[0]\n            # else:\n            #     final_preds = np.average(preds_raw, axis=0, weights=w_arr)  # <<< changed\n\n            # predictions.append(final_preds)\n\n\n    # ---------- weighted logit average ----------\n            if preds_raw.shape[0] == 1:                  # 只有 1 个模型\n                final_logits = preds_raw[0]              # (C,)\n            else:\n                # a) 取 ckpt 分数\n                scores  = np.array([w for _, w in models], dtype=np.float32)\n                # b) 指数化放大 -> softmax 权重\n                exp_w   = np.exp(scores * 50)            # 50 可调；越大差距越显著\n                w_arr   = exp_w / exp_w.sum()            # 权重和 = 1\n                # c) 加权平均 logits\n                final_logits = np.average(preds_raw, axis=0, weights=w_arr)\n            \n            # d) 一次性 sigmoid 变概率\n            final_preds = 1 / (1 + np.exp(-final_logits))\n            predictions.append(final_preds)\n\n\n    except Exception as e:\n        print(f\"[ERR] processing {audio_path}: {e}\")\n\n    return row_ids, predictions\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-27T16:51:01.868135Z","iopub.execute_input":"2025-05-27T16:51:01.868556Z","iopub.status.idle":"2025-05-27T16:51:01.897615Z","shell.execute_reply.started":"2025-05-27T16:51:01.868516Z","shell.execute_reply":"2025-05-27T16:51:01.896133Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def apply_tta(spec, tta_idx):\n    \"\"\"Apply test-time augmentation\"\"\"\n    if tta_idx == 0:\n        # Original spectrogram\n        return spec\n    elif tta_idx == 1:\n        # Time shift (horizontal flip)\n        return np.flip(spec, axis=1)\n    elif tta_idx == 2:\n        # Frequency shift (vertical flip)\n        return np.flip(spec, axis=0)\n    else:\n        return spec\n\ndef run_inference(cfg, models, species_ids):\n    \"\"\"Run inference on all test soundscapes\"\"\"\n    test_files = list(Path(cfg.test_soundscapes).glob('*.ogg'))\n    \n    if cfg.debug:\n        print(f\"Debug mode enabled, using only {cfg.debug_count} files\")\n        test_files = test_files[:cfg.debug_count]\n    \n    print(f\"Found {len(test_files)} test soundscapes\")\n\n    all_row_ids = []\n    all_predictions = []\n\n    for audio_path in tqdm(test_files):\n        row_ids, predictions = predict_on_spectrogram(str(audio_path), models, cfg, species_ids)\n        all_row_ids.extend(row_ids)\n        all_predictions.extend(predictions)\n    \n    return all_row_ids, all_predictions\n\ndef create_submission(row_ids, predictions, species_ids, cfg):\n    \"\"\"Create submission dataframe\"\"\"\n    print(\"Creating submission dataframe...\")\n\n    submission_dict = {'row_id': row_ids}\n    \n    for i, species in enumerate(species_ids):\n        submission_dict[species] = [pred[i] for pred in predictions]\n\n    submission_df = pd.DataFrame(submission_dict)\n    submission_df.set_index('row_id', inplace=True)\n    sample_sub = pd.read_csv(cfg.submission_csv, index_col='row_id')\n\n    missing_cols = set(sample_sub.columns) - set(submission_df.columns)\n    if missing_cols:\n        print(f\"Warning: Missing {len(missing_cols)} species columns in submission\")\n        for col in missing_cols:\n            submission_df[col] = 0.0\n\n    submission_df = submission_df[sample_sub.columns]\n    submission_df = submission_df.reset_index()\n    \n    return submission_df\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-27T16:51:01.898819Z","iopub.execute_input":"2025-05-27T16:51:01.899174Z","iopub.status.idle":"2025-05-27T16:51:01.921809Z","shell.execute_reply.started":"2025-05-27T16:51:01.899148Z","shell.execute_reply":"2025-05-27T16:51:01.920816Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def main():\n    start_time = time.time()\n    print(\"Starting BirdCLEF-2025 inference...\")\n    print(f\"TTA enabled: {cfg.use_tta} (variations: {cfg.tta_count if cfg.use_tta else 0})\")\n\n    models = load_models(cfg, num_classes)\n    \n    if not models:\n        print(\"No models found! Please check model paths.\")\n        return\n    \n    print(f\"Model usage: {'Single model' if len(models) == 1 else f'Ensemble of {len(models)} models'}\")\n\n    row_ids, predictions = run_inference(cfg, models, species_ids)\n    submission_df = create_submission(row_ids, predictions, species_ids, cfg)\n    submission_path = 'submission.csv'\n    submission_df.to_csv(submission_path, index=False)\n    print(f\"Submission saved to {submission_path}\")\n    \n    end_time = time.time()\n    print(f\"Inference completed in {(end_time - start_time)/60:.2f} minutes\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-27T16:51:01.923194Z","iopub.execute_input":"2025-05-27T16:51:01.923732Z","iopub.status.idle":"2025-05-27T16:51:01.952220Z","shell.execute_reply.started":"2025-05-27T16:51:01.923666Z","shell.execute_reply":"2025-05-27T16:51:01.951121Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"<h1 style=\"color: #6cb4e4;  text-align: center;  padding: 0.25em;  border-top: solid 2.5px #6cb4e4;  border-bottom: solid 2.5px #6cb4e4;  background: -webkit-repeating-linear-gradient(-45deg, #f0f8ff, #f0f8ff 3px,#e9f4ff 3px, #e9f4ff 7px);  background: repeating-linear-gradient(-45deg, #f0f8ff, #f0f8ff 3px,#e9f4ff 3px, #e9f4ff 7px);height:45px;\">\n<b>\nCreate Submission\n</b></h1> ","metadata":{}},{"cell_type":"code","source":"if __name__ == \"__main__\":\n    main()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-27T16:51:01.953888Z","iopub.execute_input":"2025-05-27T16:51:01.954314Z","iopub.status.idle":"2025-05-27T16:51:50.611939Z","shell.execute_reply.started":"2025-05-27T16:51:01.954275Z","shell.execute_reply":"2025-05-27T16:51:50.610067Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"sub = pd.read_csv('submission.csv')\ncols = sub.columns[1:]\ngroups = sub['row_id'].str.rsplit('_', n=1).str[0]\ngroups = groups.values\nfor group in np.unique(groups):\n    sub_group = sub[group == groups]\n    predictions = sub_group[cols].values\n    new_predictions = predictions.copy()\n    for i in range(1, predictions.shape[0]-1):\n        new_predictions[i] = (predictions[i-1] * 0.2) + (predictions[i] * 0.6) + (predictions[i+1] * 0.2)\n    new_predictions[0] = (predictions[0] * 0.9) + (predictions[1] * 0.1)\n    new_predictions[-1] = (predictions[-1] * 0.9) + (predictions[-2] * 0.1)\n    sub_group[cols] = new_predictions\n    sub[group == groups] = sub_group\nsub.to_csv(\"submission.csv\", index=False)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-27T16:51:50.613570Z","iopub.execute_input":"2025-05-27T16:51:50.614028Z","iopub.status.idle":"2025-05-27T16:51:50.648152Z","shell.execute_reply.started":"2025-05-27T16:51:50.613988Z","shell.execute_reply":"2025-05-27T16:51:50.646951Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =========================================================\n#   BirdCLEF – 模型输出相关性快速诊断\n#   抽样音频 → 收集各模型 Sigmoid 概率 → 皮尔逊系数矩阵\n#   运行环境：CPU / TorchScript / 动态量化均兼容\n# =========================================================\n\nimport os, random, time, gc, math, re\nimport numpy as np\nimport pandas as pd\nimport librosa\nimport torch\nfrom pathlib import Path\nfrom tqdm import tqdm\nfrom scipy.stats import pearsonr\n\n# -------------------- 1. 准备模型 --------------------\ntaxonomy_df  = pd.read_csv(CFG.taxonomy_csv)\nspecies_ids  = taxonomy_df[\"primary_label\"].tolist()\nnum_classes  = len(species_ids)\n\nmodels = load_models(CFG, num_classes)\nif len(models) < 2:\n    raise RuntimeError(\"至少需要两个 ckpt 才能计算相关系数！\")\n\nprint(f\"[INFO] Loaded {len(models)} ckpts for correlation check\\n\")\n\n# -------------------- 2. 选取音频 --------------------\nTEST_DIR   = Path(CFG.test_soundscapes)                            # 测试目录（通常空）\nTRAIN_DIR  = Path(\"/kaggle/input/birdclef-2025/train_audio\")        # 训练录音根目录\nCUSTOM_DIR = None   # ← 若有自定义验证集目录，可写 Path(\"/path/to/ogg_dir\")\n\ndef gather_ogg(d: Path):\n    return list(d.rglob(\"*.ogg\")) if d.exists() else []\n\naudio_candidates = gather_ogg(TEST_DIR) or gather_ogg(TRAIN_DIR) \\\n                   or (gather_ogg(CUSTOM_DIR) if CUSTOM_DIR else [])\n\nif not audio_candidates:\n    raise FileNotFoundError(\"在 TEST_DIR / TRAIN_DIR 中都未找到任何 .ogg 音频！\")\n\nrandom.shuffle(audio_candidates)\nsample_audio = audio_candidates[:5]                                # 抽 5 个文件\nprint(\"[INFO] Using audio files:\")\nfor p in sample_audio:\n    print(\"   \", p)\nprint()\n\n# -------------------- 3. 收集 logits --------------------\ndef collect_logits(models, audio_files, max_segments_per_file=20):\n    \"\"\"返回 ndarray: (N_models, N_samples, N_classes)\"\"\"\n    logits_lists = [[] for _ in models]\n\n    for audio_path in tqdm(audio_files, desc=\"Collecting segments\"):\n        y, _ = librosa.load(audio_path, sr=CFG.FS)\n        tot_seg = len(y) // (CFG.FS * CFG.WINDOW_SIZE)\n        seg_ids = list(range(tot_seg))\n        random.shuffle(seg_ids)\n        seg_ids = seg_ids[:max_segments_per_file]\n\n        for sidx in seg_ids:\n            ss = sidx * CFG.FS * CFG.WINDOW_SIZE\n            es = ss +       CFG.FS * CFG.WINDOW_SIZE\n            seg_audio = y[ss:es]\n\n            mel = process_audio_segment(seg_audio, CFG)\n            mel = torch.tensor(mel, dtype=torch.float32\n                               ).unsqueeze(0).unsqueeze(0).to(CFG.device)\n\n            for (mdl, _), store in zip(models, logits_lists):\n                with torch.no_grad():\n                    store.append(torch.sigmoid(mdl(mel)\n                                   ).cpu().numpy().squeeze())\n\n    return np.stack([np.stack(v) for v in logits_lists])           # (N_models,N_samples,C)\n\nlogits_all = collect_logits(models, sample_audio, max_segments_per_file=20)\n\n# -------------------- 4. 打印相关系数 --------------------\nprint(\"\\n=== Pearson correlation between model outputs ===\")\nn_models = logits_all.shape[0]\nfor i in range(n_models):\n    for j in range(i + 1, n_models):\n        r = pearsonr(logits_all[i].ravel(), logits_all[j].ravel())[0]\n        print(f\"corr(model {i}, model {j}) = {r:.4f}\")\nprint(\"=================================================\\n\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-27T16:51:50.649314Z","iopub.execute_input":"2025-05-27T16:51:50.649719Z","iopub.status.idle":"2025-05-27T16:53:50.269714Z","shell.execute_reply.started":"2025-05-27T16:51:50.649659Z","shell.execute_reply":"2025-05-27T16:53:50.268626Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}