{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"#!/usr/bin/env python\n# coding: utf-8\n#from the official birdclef code page, adapted to my own pipeline\n\nimport os\nimport glob\nfrom types import SimpleNamespace\n\nimport numpy as np\nimport pandas as pd\nimport soundfile as sf\nimport torch\nimport torch.nn as nn\nimport torchaudio\nimport torchvision.models as models\n\n\n\nCHECKPOINT = '/kaggle/input/datasets/kylenkomo/r50-finetune/r50_finetune.pth'\nCOMP_DIR = '/kaggle/input/competitions/birdclef-2023'\nTEST_DIR = os.path.join(COMP_DIR, 'test_soundscapes')\nOUTPUT_CSV = 'submission.csv'\n\nSEGMENT_SECS = 5.0        # the competition scores every 5 seconds\nBATCH_SIZE = 16           # segments per forward pass\n\n# used only if a key is missing from the checkpoint's stored config\nDEFAULTS = dict(sr=32_000, num_classes=264, backbone='resnet18', dropout=0.4,\n                n_mels=128, n_fft=2048, hop_length=512, f_min=40, f_max=15_000,\n                top_db=100.0, img_size=224, freq_mask=24, time_mask=24,\n                n_masks=2, loss_type='ce')\n\n\nclass MelSpecTransform(nn.Module):\n    \"\"\"audio -> normalised 3-channel spectrogram. dB clamping and normalisation are per sample\"\"\"\n\n    def __init__(self, cfg):\n        super().__init__()\n        self.cfg = cfg\n        self.spec = torchaudio.transforms.Spectrogram(\n            n_fft=cfg.n_fft, win_length=cfg.n_fft,\n            hop_length=cfg.hop_length, power=None)\n        \n        self.mel_scale = torchaudio.transforms.MelScale(\n            n_mels=cfg.n_mels, sample_rate=cfg.sr,\n            f_min=cfg.f_min, f_max=cfg.f_max, n_stft=cfg.n_fft // 2 + 1)\n        self.n_frames = int(cfg.chunk_secs * cfg.sr) // cfg.hop_length + 1\n\n    def _to_db(self, power):\n        db = 10.0 * torch.log10(power.clamp(min=1e-10))\n        if self.cfg.top_db is not None:\n            peak = db.amax(dim=(-2, -1), keepdim=True)\n            db = torch.maximum(db, peak - self.cfg.top_db)\n        return db\n\n    @torch.no_grad()\n    def forward(self, audio):\n        cfg = self.cfg\n        z = self.spec(audio)      # (B, F, T) complex\n\n        if z.shape[-1] > self.n_frames:\n            z = z[..., :self.n_frames]\n        elif z.shape[-1] < self.n_frames:\n            z = nn.functional.pad(z, (0, self.n_frames - z.shape[-1]))\n\n        mel = self._to_db(self.mel_scale(z.abs().pow(2)))\n        mel = mel.unsqueeze(1)       # (B, 1, mel, T)\n\n        #resize the spec to 224x224\n        mel = nn.functional.interpolate(\n            mel, size=(cfg.img_size, cfg.img_size),\n            mode='bilinear', align_corners=False)\n\n        #per sample normalisation\n        mean = mel.mean(dim=(1, 2, 3), keepdim=True)\n        std = mel.std(dim=(1, 2, 3), keepdim=True) + 1e-6\n        mel = (mel - mean) / std\n\n        return mel.repeat(1, 3, 1, 1)\n\n\n\ndef build_model(cfg):\n    \"\"\"build the model used for scoring\"\"\"\n    if cfg.backbone == 'resnet18':\n        model = models.resnet18(weights=None)\n        in_features = model.fc.in_features\n        model.fc = nn.Sequential(nn.Dropout(p=cfg.dropout),\n                                 nn.Linear(in_features, cfg.num_classes))\n    elif cfg.backbone == 'resnet50':\n        model = models.resnet50(weights=None)\n        in_features = model.fc.in_features\n        model.fc = nn.Sequential(nn.Dropout(p=cfg.dropout),\n                                 nn.Linear(in_features, cfg.num_classes))\n    elif cfg.backbone == 'efficientnet_b0':\n        model = models.efficientnet_b0(weights=None)\n        in_features = model.classifier[1].in_features\n        model.classifier = nn.Sequential(nn.Dropout(p=cfg.dropout),\n                                         nn.Linear(in_features, cfg.num_classes))\n    else:\n        raise ValueError(f'unknown backbone: {cfg.backbone}')\n    return model\n\n\n\n# inference\ndef load_config(checkpoint):\n    \"\"\"rebuild the training config from the checkpoint, filling any gaps. used for the spec params\"\"\"\n    stored = checkpoint.get('cfg', {}) if isinstance(checkpoint, dict) else {}\n    merged = dict(DEFAULTS)\n    \n    for key, value in stored.items():\n        merged[key] = tuple(value) if isinstance(value, list) else value\n    merged['chunk_secs'] = SEGMENT_SECS      # the window the competition scores\n    cfg = SimpleNamespace(**merged)\n    \n    print(f'backbone={cfg.backbone} n_mels={cfg.n_mels} n_fft={cfg.n_fft} '\n          f'hop={cfg.hop_length} loss={cfg.loss_type} classes={cfg.num_classes}')\n    if not stored:\n        print('WARNING: no config in checkpoint, using DEFAULTS')\n    return cfg\n\n\ndef load_model(cfg, checkpoint, device):\n    \"\"\"load the weights and what not from CHECKPOINT\"\"\"\n    model = build_model(cfg)\n    state = checkpoint.get('model_state_dict', checkpoint) \\\n        if isinstance(checkpoint, dict) else checkpoint\n    state = {k.replace('module.', '', 1): v for k, v in state.items()}\n    model.load_state_dict(state, strict=True)\n    model.to(device).eval()\n    return model\n\n\ndef species_columns(comp_dir, num_classes):\n    sample = os.path.join(comp_dir, 'sample_submission.csv')\n    if os.path.exists(sample):\n        cols = [c for c in pd.read_csv(sample, nrows=1).columns if c != 'row_id']\n        if len(cols) == num_classes:\n            return cols\n            \n    # fallback: exactly how training built label2idx\n    meta = pd.read_csv(os.path.join(comp_dir, 'train_metadata.csv'))\n    return sorted(meta['primary_label'].unique())\n\n\ndef segment_soundscape(path, sr, segment_len):\n    \"\"\"read a soundscape and yield (end_second, segment), zero padding the tail\"\"\"\n    audio, file_sr = sf.read(path, dtype='float32', always_2d=False)\n    if audio.ndim > 1:\n        audio = audio.mean(axis=1)\n    if file_sr != sr:\n        import librosa\n        audio = librosa.resample(audio, orig_sr=file_sr, target_sr=sr)\n\n    n_segments = int(np.ceil(len(audio) / segment_len))\n    for i in range(n_segments):\n        seg = audio[i * segment_len:(i + 1) * segment_len]\n        if len(seg) < segment_len:\n            padded = np.zeros(segment_len, dtype='float32')\n            padded[:len(seg)] = seg\n            seg = padded\n        yield (i + 1) * int(SEGMENT_SECS), seg\n\n\n@torch.no_grad()\ndef predict_file(path, model, mel_transform, cfg, device, segment_len):\n    end_times, batch, rows = [], [], []\n\n    def flush():\n        if not batch:\n            return\n        wav = torch.from_numpy(np.stack(batch)).to(device)\n        logits = model(mel_transform(wav)).float()\n        probs = (torch.sigmoid(logits) if cfg.loss_type == 'bce'\n                 else torch.softmax(logits, dim=1))\n        rows.append(probs.cpu().numpy())\n        batch.clear()\n\n    for end_time, seg in segment_soundscape(path, cfg.sr, segment_len):\n        end_times.append(end_time)\n        batch.append(seg)\n        if len(batch) == BATCH_SIZE:\n            flush()\n    flush()\n\n    return end_times, np.concatenate(rows, axis=0)\n\n\ndef main():\n    #submisison mustn't use the gpu or the internet\n    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n    torch.set_num_threads(os.cpu_count() or 4)\n    print(f'device: {device}')\n\n    #load the trained model's best checkpoint and the configs for that model run\n    checkpoint = torch.load(CHECKPOINT, map_location='cpu', weights_only=False)\n    cfg = load_config(checkpoint)\n    model = load_model(cfg, checkpoint, device)\n\n    #create the spectrogram transform and get the length for each chunk\n    mel_transform = MelSpecTransform(cfg).to(device)\n    segment_len = int(SEGMENT_SECS * cfg.sr)\n\n    columns = species_columns(COMP_DIR, cfg.num_classes)\n    assert len(columns) == cfg.num_classes, \\\n        f'{len(columns)} submission columns vs {cfg.num_classes} model outputs'\n\n    test_files = sorted(glob.glob(os.path.join(TEST_DIR, '*.ogg')))\n    if not test_files:\n        # during commit the hidden test set is absent, so write a valid empty file\n        # so the notebook saves. the real soundscapes appear at rescoring.\n        print('no test soundscapes found, writing header only')\n        pd.DataFrame(columns=['row_id'] + columns).to_csv(OUTPUT_CSV, index=False)\n        return\n    print(f'{len(test_files)} soundscapes to score')\n\n    row_ids, all_probs = [], []\n    for path in test_files:\n        stem = os.path.splitext(os.path.basename(path))[0]\n        end_times, probs = predict_file(path, model, mel_transform, cfg,\n                                        device, segment_len)\n        row_ids.extend(f'{stem}_{t}' for t in end_times)\n        all_probs.append(probs)\n        print(f'{stem}: {len(end_times)} segments')\n\n    submission = pd.DataFrame(np.concatenate(all_probs, axis=0), columns=columns)\n    submission.insert(0, 'row_id', row_ids)\n    submission.to_csv(OUTPUT_CSV, index=False)\n    print(f'wrote {OUTPUT_CSV}: {submission.shape[0]} rows, '\n          f'{submission.shape[1] - 1} species')\n\n\nmain()","metadata":{"_uuid":"18f617b8-12e0-4e32-b539-7e43512b1388","_cell_guid":"05e4ccea-69d7-4b86-9bb8-01f459ff02ef","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2026-10-18T17:58:34.201094Z","iopub.execute_input":"2026-10-18T17:58:34.201454Z","iopub.status.idle":"2026-10-18T17:58:49.691225Z","shell.execute_reply.started":"2026-10-18T17:58:34.201411Z","shell.execute_reply":"2026-10-18T17:58:49.690373Z"}},"outputs":[],"execution_count":null}]}