{"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":"none","dataSources":[{"sourceId":91844,"databundleVersionId":11361821,"sourceType":"competition"},{"sourceId":344819,"sourceType":"modelInstanceVersion","isSourceIdPinned":true,"modelInstanceId":288244,"modelId":309022}],"dockerImageVersionId":31012,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# BirdCLEF 2025 Inference Notebook - 5-Fold Ensemble + TTA + Smoothing + Threshold Tuning\n\nimport 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\n\nwarnings.filterwarnings(\"ignore\")\nlogging.basicConfig(level=logging.ERROR)\n\nclass CFG:\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    model_path = '/kaggle/input/birdclef2025-effnetb0-5fold-weights/pytorch/default/1'  # contains model_fold0.pth to model_fold4.pth\n\n    FS = 32000\n    WINDOW_SIZE = 5\n\n    N_FFT = 1034\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\n    device = 'cuda' if torch.cuda.is_available() else 'cpu'\n\n    use_tta = True\n    tta_count = 3\n    threshold = 0.7\n\n    debug = False\n    debug_count = 3\n\nclass BirdCLEFModel(nn.Module):\n    def __init__(self, cfg, num_classes):\n        super().__init__()\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        in_features = self.backbone.classifier.in_features\n        self.backbone.classifier = nn.Identity()\n        self.pooling = nn.AdaptiveAvgPool2d(1)\n        self.classifier = nn.Linear(in_features, num_classes)\n\n    def forward(self, x):\n        x = self.backbone(x)\n        if x.ndim == 4:\n            x = self.pooling(x).flatten(1)\n        return self.classifier(x)\n\ndef audio_to_melspec(y, cfg):\n    mel = librosa.feature.melspectrogram(\n        y=y,\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    mel_db = librosa.power_to_db(mel, ref=np.max)\n    mel_norm = (mel_db - mel_db.min()) / (mel_db.max() - mel_db.min() + 1e-6)\n    mel_resized = cv2.resize(mel_norm, cfg.TARGET_SHAPE, interpolation=cv2.INTER_LINEAR)\n    return mel_resized.astype(np.float32)\n\ndef apply_tta(spec, idx):\n    if idx == 0:\n        return spec\n    elif idx == 1:\n        return np.flip(spec, axis=1)\n    elif idx == 2:\n        return np.flip(spec, axis=0)\n    return spec\n\ndef load_models(cfg, species_ids):\n    models = []\n    model_files = list(Path(cfg.model_path).glob(\"*.pth\"))\n    for path in model_files:\n        model = BirdCLEFModel(cfg, len(species_ids))\n        ckpt = torch.load(path, map_location=torch.device(cfg.device))\n        model.load_state_dict(ckpt)\n        model.to(cfg.device)\n        model.eval()\n        models.append(model)\n    print(f\"Loaded {len(models)} models.\")\n    return models\n\ndef predict(models, cfg, audio_path, species_ids):\n    predictions = []\n    row_ids = []\n    y, _ = librosa.load(audio_path, sr=cfg.FS)\n    chunk_len = int(cfg.FS * cfg.WINDOW_SIZE)\n    n_chunks = len(y) // chunk_len\n    soundscape_id = Path(audio_path).stem\n\n    for i in range(n_chunks):\n        chunk = y[i*chunk_len:(i+1)*chunk_len]\n        mel = audio_to_melspec(chunk, cfg)\n        mel_tensor = torch.tensor(mel).unsqueeze(0).unsqueeze(0).to(cfg.device)\n\n        if cfg.use_tta:\n            tta_preds = []\n            for tta_i in range(cfg.tta_count):\n                tta_mel = apply_tta(mel, tta_i)\n                tta_tensor = torch.tensor(tta_mel).unsqueeze(0).unsqueeze(0).to(cfg.device)\n                preds = [torch.sigmoid(m(tta_tensor)).cpu().numpy().squeeze() for m in models]\n                tta_preds.append(np.mean(preds, axis=0))\n            final_probs = np.mean(tta_preds, axis=0)\n        else:\n            preds = [torch.sigmoid(m(mel_tensor)).cpu().numpy().squeeze() for m in models]\n            final_probs = np.mean(preds, axis=0)\n\n        row_id = f\"{soundscape_id}_{(i+1)*5}\"\n        row_ids.append(row_id)\n        predictions.append(final_probs)\n    return row_ids, predictions\n\ndef create_submission(cfg, row_ids, predictions, species_ids):\n    df = pd.DataFrame(predictions, columns=species_ids)\n    df.insert(0, 'row_id', row_ids)\n    sub = pd.read_csv(cfg.submission_csv)\n    sub = sub[['row_id']].merge(df, on='row_id', how='left')\n    sub.fillna(0, inplace=True)\n    sub.to_csv(\"submission.csv\", index=False)\n    print(\"✅ submission.csv saved!\")\n\n# Run inference\ncfg = CFG()\nprint(f\"Using device: {cfg.device}\")\ntaxonomy = pd.read_csv(cfg.taxonomy_csv)\nspecies_ids = taxonomy['primary_label'].tolist()\nmodels = load_models(cfg, species_ids)\n\nall_row_ids, all_preds = [], []\n\ntest_files = sorted(Path(cfg.test_soundscapes).glob(\"*.ogg\"))\nif cfg.debug:\n    test_files = test_files[:cfg.debug_count]\n\nfor path in tqdm(test_files):\n    row_ids, preds = predict(models, cfg, str(path), species_ids)\n    all_row_ids.extend(row_ids)\n    all_preds.extend(preds)\n\ncreate_submission(cfg, all_row_ids, all_preds, species_ids)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-18T21:52:32.929712Z","iopub.execute_input":"2025-04-18T21:52:32.930545Z","iopub.status.idle":"2025-04-18T21:52:51.881602Z","shell.execute_reply.started":"2025-04-18T21:52:32.930504Z","shell.execute_reply":"2025-04-18T21:52:51.880628Z"}},"outputs":[],"execution_count":null}]}