{"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":11788973,"sourceType":"datasetVersion","datasetId":7402222},{"sourceId":407000,"sourceType":"modelInstanceVersion","modelInstanceId":332560,"modelId":353493}],"dockerImageVersionId":31040,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"#!/usr/bin/env python3\n\"\"\"\ninfer_b2_kaggle.py  ·  BirdCLEF-2025\n=====================================================================\nEfficientNet-B2  ·  128-mel / 16 ms-hop inference\n---------------------------------------------------------------------\nThis script performs inference for BirdCLEF-2025 using a B2-style\npipeline with log-mel spectrograms and blended window predictions.\n=====================================================================\n\"\"\"\n\n# Dummy CFG class for compatibility with checkpoints\nclass CFG:\n    pass\n\n# ─── USER-ADJUSTABLE CONSTANTS ─────────────────────────────────────────\nBLEND_W_5S   = 0.90     # 5s window weight (main window)\nBLEND_W_7P5S = 0.10     # 7.5s window weight (centered window)\nWINDOW_SIZE  = 5        # seconds per inference window\nIMG_SIZE     = 256      # spectrogram image size\nSR           = 32_000   # sample rate\nN_FFT        = 2048\nHOP          = 512\nMELS         = 128\nEPS          = 1e-8\n# ----------------------------------------------------------------------\n\nimport os, random, math, pathlib, warnings, sys, importlib.util\nimport numpy as np, pandas as pd, torch, torch.nn.functional as F\nimport soundfile as sf, librosa\nfrom scipy.signal import butter, filtfilt\nimport cv2\n\n# Optional: offline noisereduce\nnr_path = \"/kaggle/input/noisereduce/noisereduce/noisereduce/noisereduce.py\"\nspec = importlib.util.spec_from_file_location(\n    \"noisereduce\", nr_path,\n    submodule_search_locations=[os.path.dirname(nr_path)])\nnr = importlib.util.module_from_spec(spec); sys.modules[\"noisereduce\"] = nr\nspec.loader.exec_module(nr)\n\ntorch.set_num_threads(os.cpu_count())\n\n# Kaggle paths\nWEIGHTS_DIR = \"/kaggle/input/allb2pths/pytorch/default/1\"\nROOT_DIR      = \"/kaggle/input/birdclef-2025\"\nTEST_DIR      = f\"{ROOT_DIR}/test_soundscapes\"\nTRAIN_SC      = f\"{ROOT_DIR}/train_soundscapes\"\nSAMPLE_CSV    = f\"{ROOT_DIR}/sample_submission.csv\"\nTAXONOMY_CSV  = f\"{ROOT_DIR}/taxonomy.csv\"\n\nDEVICE = torch.device(\"cpu\")\nTARGET_SHAPE = (IMG_SIZE, IMG_SIZE)\n\n# ─── EfficientNet-B2 Backbone definition ───────────────────────────────\nimport timm\nimport torch.nn as nn\n\nclass EffNetB2Backbone(nn.Module):\n    \"\"\"\n    EfficientNet-B2 backbone for BirdCLEF-2025 (customized for 1-channel input).\n    \"\"\"\n    def __init__(self, num_classes):\n        super().__init__()\n        self.backbone = timm.create_model(\n            \"regnety_008\", pretrained=False, in_chans=1,\n            drop_rate=0.0, drop_path_rate=0.0\n        )\n        backbone_out = self.backbone.get_classifier().in_features\n        self.backbone.reset_classifier(0, '')\n        self.pooling = nn.AdaptiveAvgPool2d(1)\n        self.classifier = nn.Linear(backbone_out, num_classes)\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        logits = self.classifier(features)\n        return logits\n\n# ─── Model Load (robust: only loads state dict, never unpickles CFG) ─────────\ntaxonomy_df = pd.read_csv(TAXONOMY_CSV)\nCLASS_NAMES = taxonomy_df['primary_label'].tolist()\nmodel_files = list(pathlib.Path(WEIGHTS_DIR).glob('**/*.pth'))\n\nmodels = []\nfor model_path in model_files:\n    checkpoint = torch.load(model_path, map_location=DEVICE, weights_only=False)\n    if isinstance(checkpoint, dict):\n        if 'model_state_dict' in checkpoint:\n            state_dict = checkpoint['model_state_dict']\n        elif 'state_dict' in checkpoint:\n            state_dict = checkpoint['state_dict']\n        else:\n            state_dict = checkpoint\n    else:\n        state_dict = checkpoint\n    model = EffNetB2Backbone(len(CLASS_NAMES))\n    model.load_state_dict(state_dict, strict=False)\n    model = model.to(DEVICE)\n    model.eval()\n    models.append(model)\n\nprint(f\"✓ EfficientNet-B2 backbone ready | {len(models)} model(s) loaded\")\n\n# ─── log-mel spectrogram extraction ────────────────────────────────────\ndef b2_melspec(audio_data):\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    mel_spec = librosa.feature.melspectrogram(\n        y=audio_data,\n        sr=SR,\n        n_fft=N_FFT,\n        hop_length=HOP,\n        n_mels=MELS,\n        fmin=50,\n        fmax=14000,\n        power=2.0\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() + EPS)\n    return mel_spec_norm\n\ndef process_audio_segment(audio_data, window_size=5):\n    target_len = int(SR * window_size)\n    if len(audio_data) < target_len:\n        audio_data = np.pad(audio_data, (0, target_len - len(audio_data)), mode='constant')\n    mel_spec = b2_melspec(audio_data)\n    if mel_spec.shape != TARGET_SHAPE:\n        mel_spec = cv2.resize(mel_spec, TARGET_SHAPE, interpolation=cv2.INTER_LINEAR)\n    return mel_spec.astype(np.float32)\n\ndef list_audio(folder): return list(folder.glob(\"*.ogg\")) + list(folder.glob(\"*.wav\"))\n\n# ─── Per-file inference ────────────────────────────────────────────────\ndef rows_for_file(fp: str):\n    y, _ = librosa.load(fp, sr=SR)\n    total_segments = int(len(y) / (SR * WINDOW_SIZE))\n    stem = pathlib.Path(fp).stem\n    rows = []\n\n    for segment_idx in range(total_segments):\n        start_sample = segment_idx * SR * WINDOW_SIZE\n        end_sample = start_sample + SR * WINDOW_SIZE\n        segment_audio = y[start_sample:end_sample]\n\n        # 7.5s window, centered\n        center_sample = (start_sample + end_sample) // 2\n        half_window = int(3.75 * SR)\n        win7_start = max(0, center_sample - half_window)\n        win7_end = min(len(y), center_sample + half_window)\n        segment_audio_7p5 = y[win7_start:win7_end]\n        if len(segment_audio_7p5) < 7.5 * SR:\n            segment_audio_7p5 = np.pad(segment_audio_7p5, (0, int(7.5 * SR) - len(segment_audio_7p5)), mode='constant')\n\n        # 5s prediction\n        mel_spec_5s = process_audio_segment(segment_audio, window_size=5)\n        mel_spec_tensor_5s = torch.tensor(mel_spec_5s, dtype=torch.float32).unsqueeze(0).unsqueeze(0).to(DEVICE)\n\n        # 7.5s prediction\n        mel_spec_7p5 = process_audio_segment(segment_audio_7p5, window_size=7.5)\n        mel_spec_tensor_7p5 = torch.tensor(mel_spec_7p5, dtype=torch.float32).unsqueeze(0).unsqueeze(0).to(DEVICE)\n\n        def infer(mel_spec_tensor):\n            if len(models) == 1:\n                with torch.no_grad():\n                    outputs = models[0](mel_spec_tensor)\n                    return torch.sigmoid(outputs).cpu().numpy().squeeze()\n            else:\n                segment_preds = []\n                for model in models:\n                    with torch.no_grad():\n                        outputs = model(mel_spec_tensor)\n                        probs = torch.sigmoid(outputs).cpu().numpy().squeeze()\n                        segment_preds.append(probs)\n                return np.mean(segment_preds, axis=0)\n\n        pred_5s = infer(mel_spec_tensor_5s)\n        pred_7p5 = infer(mel_spec_tensor_7p5)\n\n        # Blend predictions\n        blended_pred = BLEND_W_5S * pred_5s + BLEND_W_7P5S * pred_7p5\n\n        # Ensure always iterable\n        bp = blended_pred\n        if np.isscalar(bp):\n            bp = [bp]\n        else:\n            bp = bp.tolist()\n        rows.append([f\"{stem}_{(segment_idx+1)*WINDOW_SIZE}\", *bp])\n\n    return rows\n\n# ─── drive the script ---------------------------------------------------\ntest_files = list_audio(pathlib.Path(TEST_DIR))\nif not test_files:\n    warnings.warn(\"No test soundscapes; sampling train.\")\n    test_files = random.sample(list_audio(pathlib.Path(TRAIN_SC)), 2)\n\nresults = []\nfor fp in sorted(map(str, test_files)):\n    results.extend(rows_for_file(fp))\n\npd.DataFrame(results, columns=[\"row_id\"] + CLASS_NAMES)\\\n  .to_csv(\"/kaggle/working/submission.csv\", index=False)\nprint(f\"✓ submission.csv with {len(results)} rows\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-27T03:13:48.293853Z","iopub.execute_input":"2025-05-27T03:13:48.294362Z","iopub.status.idle":"2025-05-27T03:14:10.842876Z","shell.execute_reply.started":"2025-05-27T03:13:48.294329Z","shell.execute_reply":"2025-05-27T03:14:10.841348Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}