{"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"name":"python","version":"3.12.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceType":"competition","sourceId":70203,"databundleVersionId":8068726},{"sourceType":"modelInstanceVersion","sourceId":516989,"databundleVersionId":13353982,"modelInstanceId":404337,"modelId":319},{"sourceType":"kernelVersion","sourceId":315119457}],"dockerImageVersionId":31329,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# BirdCLEF 2024: MLP Head Submission\nThis notebook uses the **Residual MLP Head** trained on Day 5. It runs on **Perch v2** embeddings and handles the full 182-species vocabulary.","metadata":{}},{"cell_type":"code","source":"import os\nimport pandas as pd\nimport numpy as np\nimport tensorflow as tf\nimport torch\nimport torch.nn as nn\nimport soundfile as sf\nfrom pathlib import Path\nfrom tqdm.auto import tqdm\n\nINPUT_DIR = Path('/kaggle/input')\nTEST_AUDIO_DIR = INPUT_DIR / 'competitions/birdclef-2024/test_soundscapes'\n\ntest_files = list(TEST_AUDIO_DIR.glob('*.ogg'))\nif not test_files:\n    TEST_AUDIO_DIR = INPUT_DIR / 'competitions/birdclef-2024/unlabeled_soundscapes'\n    test_files = list(TEST_AUDIO_DIR.glob('*.ogg'))[:2]\n\ntrain_df = pd.read_csv(INPUT_DIR / 'competitions/birdclef-2024/train_metadata.csv')\nTARGET_SPECIES = sorted(train_df['primary_label'].unique())\nN_CLASSES = len(TARGET_SPECIES)\nDEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 1. Robust Perch v2 Loading","metadata":{}},{"cell_type":"code","source":"try:\n    pb_path = next(INPUT_DIR.rglob('saved_model.pb'))\n    perch_model = tf.saved_model.load(str(pb_path.parent))\n    if 'serving_default' in perch_model.signatures:\n        perch_fn = perch_model.signatures['serving_default']\n    elif hasattr(perch_model, 'infer_tf'):\n        perch_fn = perch_model.infer_tf\n    else:\n        perch_fn = perch_model\n    print(\"Perch v2 loaded successfully.\")\nexcept Exception as e:\n    print(f\"Perch loading error: {e}\")\n    perch_fn = None\n\ndef get_embeddings(audio_chunk):\n    # Ensure chunk is 32kHz and wrapped in a batch dimension\n    x = tf.constant(audio_chunk[None, :], dtype=tf.float32)\n    \n    try:\n        # 1. Try positional argument (Standard for most TF versions)\n        out = perch_fn(x)\n    except Exception:\n        try:\n            # 2. Try keyword 'inputs' (Common for Perch v2 'serving_default')\n            out = perch_fn(inputs=x)\n        except Exception:\n            # 3. Try keyword 'input' (Some older Perch exports)\n            out = perch_fn(input=x)\n            \n    # Parse the output (it might be a dict or a raw tensor)\n    if isinstance(out, dict):\n        if 'embedding' in out: return out['embedding'].numpy()[0]\n        if 'label' in out: return out['label'].numpy()[0] # Fallback\n    return out.numpy()[0]\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 2. MLP Head Definition & Weight Loading","metadata":{}},{"cell_type":"code","source":"class ResidualBlock(nn.Module):\n    def __init__(self, dim, dropout=0.3):\n        super().__init__()\n        self.net = nn.Sequential(nn.Linear(dim, dim), nn.LayerNorm(dim), nn.GELU(), nn.Dropout(dropout))\n    def forward(self, x): return x + self.net(x)\n\nclass MLPHead(nn.Module):\n    def __init__(self, in_dim=1536, hidden_dim=512, n_classes=182, dropout=0.3):\n        super().__init__()\n        self.input_layer = nn.Sequential(nn.Linear(in_dim, hidden_dim), nn.LayerNorm(hidden_dim), nn.GELU())\n        self.res_block = ResidualBlock(hidden_dim, dropout)\n        self.output_layer = nn.Linear(hidden_dim, n_classes)\n        self.temperature = nn.Parameter(torch.ones(n_classes))\n    def forward(self, x, apply_calibration=True):\n        x = self.input_layer(x)\n        x = self.res_block(x)\n        logits = self.output_layer(x)\n        if apply_calibration:\n            t = torch.nn.functional.softplus(self.temperature) + 1e-4\n            logits = logits / t\n        return logits\n\nmodel = MLPHead(n_classes=N_CLASSES).to(DEVICE)\n# --- Fixed Weight Loading ---\n# --- Failsafe Weight Loading ---\nimport glob\n\n# Search for any file named 'mlp_head_final.pth' anywhere in /kaggle/input\npotential_paths = glob.glob('/kaggle/input/**/mlp_head_final.pth', recursive=True)\n\nif potential_paths:\n    actual_path = potential_paths[0]\n    try:\n        model.load_state_dict(torch.load(actual_path, map_location=DEVICE))\n        model.eval()\n        print(f\"✅ SUCCESS: Automatically found and loaded weights from: {actual_path}\")\n    except Exception as e:\n        print(f\"❌ Error loading MLP weights: {e}\")\nelse:\n    print(\"❌ STILL NOT FOUND. Listing all files in /kaggle/input to help you:\")\n    for root, dirs, files in os.walk('/kaggle/input'):\n        for file in files:\n            if file.endswith('.pth'):\n                print(os.path.join(root, file))\n\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 3. Inference Loop","metadata":{}},{"cell_type":"code","source":"# --- OPTIMIZED BATCHED INFERENCE ---\nmodel = model.to('cpu') # Keep MLP on CPU to save VRAM\nmodel.eval()\n\nall_preds = []\nfor file_path in tqdm(test_files):\n    audio_id = file_path.stem\n    try:\n        y, _ = sf.read(file_path, dtype='float32')\n        if len(y.shape) > 1: y = y.mean(axis=1)\n        \n        # 1. PREPARE ALL CHUNKS FOR THE FILE\n        chunks = []\n        time_ends = []\n        for i in range(0, len(y), CHUNK_LEN):\n            time_end = ((i // CHUNK_LEN) + 1) * 5\n            if time_end > 240: break\n            chunk = y[i : i + CHUNK_LEN]\n            if len(chunk) < CHUNK_LEN: chunk = np.pad(chunk, (0, CHUNK_LEN - len(chunk)))\n            chunks.append(chunk)\n            time_ends.append(time_end)\n        \n        # 2. BATCH INFERENCE (Perch on GPU)\n        # We send the entire file (48 chunks) as a single batch to the GPU\n        batch_x = tf.constant(np.array(chunks), dtype=tf.float32)\n                # --- Fixed Perch Call ---\n        if 'serving_default' in str(perch_fn):\n            out = perch_fn(inputs=batch_x)\n        else:\n            out = perch_fn(batch_x)\n\n        \n        if isinstance(out, dict):\n            embs = out['embedding'].numpy()\n        else:\n            embs = out.numpy()\n            \n        # 3. MLP HEAD (on CPU)\n        with torch.no_grad():\n            x_torch = torch.tensor(embs) # Process the whole batch of embeddings at once\n            probs = torch.sigmoid(model(x_torch)).numpy()\n            \n        # 4. STORE RESULTS\n        for j, time_end in enumerate(time_ends):\n            row = {'row_id': f\"{audio_id}_{time_end}\"}\n            for k, sp in enumerate(TARGET_SPECIES):\n                row[sp] = probs[j, k]\n            all_preds.append(row)\n            \n        # Clear TF memory every file to prevent the 9-minute OOM crash\n        tf.keras.backend.clear_session()\n            \n    except Exception as e:\n        print(f\"Error on {audio_id}: {e}\")\n        for t in range(5, 245, 5):\n            all_preds.append({'row_id': f\"{audio_id}_{t}\", **{sp: 0.0 for sp in TARGET_SPECIES}})\n\n# Final Save\nsub_df = pd.DataFrame(all_preds)\nsub_df.to_csv('submission.csv', index=False)\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}