{"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":11828260,"sourceType":"datasetVersion","datasetId":7430593},{"sourceId":11867185,"sourceType":"datasetVersion","datasetId":7457365},{"sourceId":11870659,"sourceType":"datasetVersion","datasetId":7459867},{"sourceId":11948763,"sourceType":"datasetVersion","datasetId":7130272},{"sourceId":235777618,"sourceType":"kernelVersion"},{"sourceId":354509,"sourceType":"modelInstanceVersion","modelInstanceId":295701,"modelId":316310},{"sourceId":354624,"sourceType":"modelInstanceVersion","modelInstanceId":295797,"modelId":316405}],"dockerImageVersionId":31040,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# 🥉 BirdCLEF 2025 - Bronze Medal Inference Pipeline (Private LB 0.893)\n\n### 📌 Overview\nThis notebook presents a **multi-stage ensemble inference** pipeline for BirdCLEF 2025, based on high-performing public solutions. It achieves a **Private LB score of 0.893**, earning a **Bronze Medal**.\n\n### 🧠 Inference Flow","metadata":{}},{"cell_type":"markdown","source":"\nEach stage progressively refines predictions, enhancing robustness and efficiency under time constraints.\n\n### 🔗 References & Credits\n- 🚀 [OpenVINO multi-threaded TTA baseline (kurisew)](https://www.kaggle.com/code/kurisew/lb0-855-openvino-multithread-tta-ensemble-infer)\n- ⚙️ [Alternative blend order with ConvNeXtV2 (hideyukizushi)](https://www.kaggle.com/code/hideyukizushi/bird25-weightedblend-nfnet-convnextv2-lb-860)\n- 🧩 [Single SED model (i2nfinit3y)](https://www.kaggle.com/code/i2nfinit3y/bird2025-single-sed-model-inference-lb-0-857)\n- 🧪 [Post-processing w/ power adjustment (myso1987)](https://www.kaggle.com/code/myso1987/post-processing-with-power-adjustment-for-low-rank)\n\n### 🛠 Notes\n- The ensemble **order matters**: our final submission uses a custom order that gives better stability (0.874 on LB) within ~**1h 5min** runtime.\n- There's still **performance overhead** in the 3-fold SED phase due to mel-spectrogram generation. Switching from `torchaudio` to `librosa` may improve runtime efficiency.\n\n### 💡 Tip\nAvoid hardcoding taxonomy labels; use `taxonomy.csv` for consistent index mapping across models.\n\n","metadata":{}},{"cell_type":"markdown","source":"note: You can get better score on LB, just by adjusting hyperparameters. (0.88 or higher) But I think it will overfit the LB and that's why I call this is a baseline.","metadata":{}},{"cell_type":"markdown","source":"By watching last year of this competition, shakeup is a great problem. I want to find a way to avoid this. But there is no test data for us to make reliable CV. Really hoping we will get few test files next year.","metadata":{}},{"cell_type":"code","source":"! python -m pip install --no-index --find-links=../input/openvino-wheels -r ../input/openvino-wheels/requirements.txt","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport gc\nimport warnings\nimport logging\nimport time\nimport math\nimport cv2\nfrom pathlib import Path\nimport joblib\nimport openvino.runtime as ov\nimport numpy as np\nimport pandas as pd\nimport librosa\nimport soundfile as sf\nfrom soundfile import SoundFile\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nimport torchaudio.transforms as AT\nfrom contextlib import contextmanager\nimport scipy\nfrom torch.cuda.amp import autocast, GradScaler\nimport timm\nfrom tqdm.auto import tqdm\nfrom glob import glob\nimport torchaudio\nimport random\nimport itertools\nfrom typing import Union\n\nimport concurrent.futures\n\nwarnings.filterwarnings(\"ignore\")\nlogging.basicConfig(level=logging.ERROR)\n\n\nclass CFG:\n    seed = 42\n    print_freq = 100\n    num_workers = 4\n    # single : 3-fold = sub1 : sub2 \n    ensemble_weights = [0.65, 0.15, 0.2]\n    stage = 'train_bce'\n    model_weights = [1]\n    train_datadir = '/kaggle/input/birdclef-2025/train_audio/'\n    train_csv = '/kaggle/input/birdclef-2025/train.csv'\n    test_soundscapes = '/kaggle/input/birdclef-2025/test_soundscapes/'\n    train_soundscapes = '/kaggle/input/birdclef-2025/train_soundscapes/'\n    submission_csv = '/kaggle/input/birdclef-2025/sample_submission.csv'\n    taxonomy_csv = '/kaggle/input/birdclef-2025/taxonomy.csv'\n    model_files = ['/kaggle/input/bird2025-sed-ckpt/sedmodel.pth'\n                   ]\n    openvino_path = ['/kaggle/input/efficientnet_b0/pytorch/openvino/1/efficientnet_b0.xml',\n                    '/kaggle/input/regnety_008/pytorch/default/1/regnety_008.xml',]\n    model_name = 'seresnext26t_32x4d'\n    pretrained = False\n    in_channels = 1\n    FS = 32000\n    WINDOW_SIZE = 5\n    # Mel spectrogram parameters\n    N_FFT = 1024\n    HOP_LENGTH = 256\n    N_MELS = 128\n    FMIN = 48\n    FMAX = 15000\n    TARGET_SHAPE = (256, 256)\n    threshold = 0.75\n    use_tta = False\n    tta_using_threshold = 100\n    tta_count = 6\n    channel_change_start = 100\n    channel_change_end = 100\n    SR = 32000\n    target_duration = 5\n    train_duration = 10\n\n    device = 'cpu'\n    debug = False\n    debug_num = 1\n\ncfg = CFG()\n\nprint(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}\")\n\ndef set_seed(seed=42):\n    \"\"\"\n    Set seed for reproducibility\n    \"\"\"\n    random.seed(seed)\n    os.environ[\"PYTHONHASHSEED\"] = str(seed)\n    np.random.seed(seed)\n    torch.manual_seed(seed)\n    torch.cuda.manual_seed(seed)\n    torch.cuda.manual_seed_all(seed)\n    torch.backends.cudnn.deterministic = True\n    torch.backends.cudnn.benchmark = False\n\nset_seed(cfg.seed)\n\nclass AttBlockV2(nn.Module):\n    def __init__(self, in_features: int, out_features: int, activation=\"linear\"):\n        super().__init__()\n\n        self.activation = activation\n        self.att = nn.Conv1d(\n            in_channels=in_features,\n            out_channels=out_features,\n            kernel_size=1,\n            stride=1,\n            padding=0,\n            bias=True,\n        )\n        self.cla = nn.Conv1d(\n            in_channels=in_features,\n            out_channels=out_features,\n            kernel_size=1,\n            stride=1,\n            padding=0,\n            bias=True,\n        )\n\n        self.init_weights()\n\n    def init_weights(self):\n        init_layer(self.att)\n        init_layer(self.cla)\n\n    def forward(self, x):\n        # x: (n_samples, n_in, n_time)\n        norm_att = torch.softmax(torch.tanh(self.att(x)), dim=-1)\n        cla = self.nonlinear_transform(self.cla(x))\n        x = torch.sum(norm_att * cla, dim=2)\n        return x, norm_att, cla\n\n    def nonlinear_transform(self, x):\n        if self.activation == \"linear\":\n            return x\n        elif self.activation == \"sigmoid\":\n            return torch.sigmoid(x)\n\n\ndef init_layer(layer):\n    nn.init.xavier_uniform_(layer.weight)\n\n    if hasattr(layer, \"bias\"):\n        if layer.bias is not None:\n            layer.bias.data.fill_(0.0)\n\ndef init_bn(bn):\n    bn.bias.data.fill_(0.0)\n    bn.weight.data.fill_(1.0)\n\n\nclass BirdCLEFModel(nn.Module):\n    def __init__(self, cfg):\n        super().__init__()\n        self.cfg = cfg\n\n        taxonomy_df = pd.read_csv('/kaggle/input/birdclef-2025/taxonomy.csv')\n        self.num_classes = len(taxonomy_df)\n\n        self.bn0 = nn.BatchNorm2d(cfg['n_mels'])\n\n        self.backbone = timm.create_model(\n            cfg['model_name'],\n            pretrained=False,\n            in_chans=cfg['in_channels'],\n            drop_rate=0.2,\n            drop_path_rate=0.2,\n        )\n\n        layers = list(self.backbone.children())[:-2]\n        self.encoder = nn.Sequential(*layers)\n\n        if \"efficientnet\" in self.cfg['model_name']:\n            backbone_out = self.backbone.classifier.in_features\n        elif \"eca\" in self.cfg['model_name']:\n            backbone_out = self.backbone.head.fc.in_features\n        elif \"res\" in self.cfg['model_name']:\n            backbone_out = self.backbone.fc.in_features\n        else:\n            backbone_out = self.backbone.num_features\n\n        self.fc1 = nn.Linear(backbone_out, backbone_out, bias=True)\n        self.att_block = AttBlockV2(backbone_out, self.num_classes, activation=\"sigmoid\")\n\n        self.melspec_transform = torchaudio.transforms.MelSpectrogram(\n            sample_rate=self.cfg['SR'],\n            hop_length=self.cfg['hop_length'],\n            n_mels=self.cfg['n_mels'],\n            f_min=self.cfg['f_min'],\n            f_max=self.cfg['f_max'],\n            n_fft=self.cfg['n_fft'],\n            pad_mode=\"constant\",\n            norm=\"slaney\",\n            onesided=True,\n            mel_scale=\"htk\",\n        )\n        if self.cfg['device'] == \"cuda\":\n            self.melspec_transform = self.melspec_transform.cuda()\n        else:\n            self.melspec_transform = self.melspec_transform.cpu()\n\n        self.db_transform = torchaudio.transforms.AmplitudeToDB(\n            stype=\"power\", top_db=80\n        )\n\n    def extract_feature(self, x):\n        x = x.permute((0, 1, 3, 2))\n        frames_num = x.shape[2]\n\n        x = x.transpose(1, 3)\n        x = self.bn0(x)\n        x = x.transpose(1, 3)\n\n        # if self.training:\n        #    x = self.spec_augmenter(x)\n\n        x = x.transpose(2, 3)\n        # (batch_size, channels, freq, frames)\n        x = self.encoder(x)\n\n        # (batch_size, channels, frames)\n        x = torch.mean(x, dim=2)\n\n        # channel smoothing\n        x1 = F.max_pool1d(x, kernel_size=3, stride=1, padding=1)\n        x2 = F.avg_pool1d(x, kernel_size=3, stride=1, padding=1)\n        x = x1 + x2\n\n        x = F.dropout(x, p=0.5, training=self.training)\n        x = x.transpose(1, 2)\n        x = F.relu_(self.fc1(x))\n        x = x.transpose(1, 2)\n        x = F.dropout(x, p=0.5, training=self.training)\n        return x, frames_num\n\n    @torch.cuda.amp.autocast(enabled=False)\n    def transform_to_spec(self, audio):\n\n        audio = audio.float()\n\n        spec = self.melspec_transform(audio)\n        spec = self.db_transform(spec)\n\n        if self.cfg['normal'] == 80:\n            spec = (spec + 80) / 80\n        elif self.cfg['normal'] == 255:\n            spec = spec / 255\n        else:\n            raise NotImplementedError\n\n        return spec\n\n    def forward(self, x):\n\n        with torch.no_grad():\n            x = self.transform_to_spec(x)\n\n        x, frames_num = self.extract_feature(x)\n\n        (clipwise_output, norm_att, segmentwise_output) = self.att_block(x)\n        logit = torch.sum(norm_att * self.att_block.cla(x), dim=2)\n        segmentwise_logit = self.att_block.cla(x).transpose(1, 2)\n        segmentwise_output = segmentwise_output.transpose(1, 2)\n\n        return torch.logit(clipwise_output)\n\n    def infer(self, x, tta_delta=2):\n        with torch.no_grad():\n            x = self.transform_to_spec(x)\n        x, _ = self.extract_feature(x)\n        time_att = torch.tanh(self.att_block.att(x))\n        feat_time = x.size(-1)\n        start = (\n                feat_time / 2 - feat_time * (self.cfg['infer_duration'] / self.cfg['duration_train']) / 2\n        )\n        end = start + feat_time * (self.cfg['infer_duration'] / self.cfg['duration_train'])\n        start = int(start)\n        end = int(end)\n        pred = self.attention_infer(start, end, x, time_att)\n\n        start_minus = max(0, start - tta_delta)\n        end_minus = end - tta_delta\n        pred_minus = self.attention_infer(start_minus, end_minus, x, time_att)\n\n        start_plus = start + tta_delta\n        end_plus = min(feat_time, end + tta_delta)\n        pred_plus = self.attention_infer(start_plus, end_plus, x, time_att)\n\n        pred = 0.5 * pred + 0.25 * pred_minus + 0.25 * pred_plus\n        return pred\n\n    def attention_infer(self, start, end, x, time_att):\n        feat = x[:, :, start:end]\n        # att = torch.softmax(time_att[:, :, start:end], dim=-1)\n        #             print(feat_time, start, end)\n        #             print(att_a.sum(), att.sum(), time_att.shape)\n        framewise_pred = torch.sigmoid(self.att_block.cla(feat))\n        framewise_pred_max = framewise_pred.max(dim=2)[0]\n        # clipwise_output = torch.sum(framewise_pred * att, dim=-1)\n        # logits = torch.sum(\n        #    self.att_block.cla(feat) * att,\n        #    dim=-1,\n        # )\n\n        # return clipwise_output\n        return framewise_pred_max\n\n\ndef load_sample(path, cfg):\n    audio, orig_sr = sf.read(path, dtype=\"float32\")\n    seconds = []\n    audio_length = cfg.SR * cfg.target_duration\n    step = audio_length\n    for i in range(audio_length, len(audio) + step, step):\n        start = max(0, i - audio_length)\n        end = start + audio_length\n        if end > len(audio):\n            pass\n        else:\n            seconds.append(int(end / cfg.SR))\n\n    audio = np.concatenate([audio, audio, audio])\n    audios = []\n    for i, second in enumerate(seconds):\n        end_seconds = int(second)\n        start_seconds = int(end_seconds - cfg.target_duration)\n\n        end_index = int(cfg.SR * (end_seconds + (cfg.train_duration - cfg.target_duration) / 2)) + len(audio) // 3\n        start_index = int(cfg.SR * (start_seconds - (cfg.train_duration - cfg.target_duration) / 2)) + len(audio) // 3\n        end_pad = int(cfg.SR * (cfg.train_duration - cfg.target_duration) / 2)\n        start_pad = int(cfg.SR * (cfg.train_duration - cfg.target_duration) / 2)\n        y = audio[start_index:end_index].astype(np.float32)\n        if i == 0:\n            y[:start_pad] = 0\n        elif i == (len(seconds) - 1):\n            y[-end_pad:] = 0\n        audios.append(y)\n\n    return audios\n\n\ndef sigmoid(x):\n    s = 1 / (1 + np.exp(-x))\n    return s\n\n\ndef find_model_files(cfg):\n    \"\"\"\n    Find all .pth model files in the specified model directory\n    \"\"\"\n    model_files = []\n\n    model_dir = Path(cfg.model_path)\n\n    for path in model_dir.glob('**/*.pth'):\n        model_files.append(str(path))\n\n    return model_files\n\n\ndef load_models(cfg, num_classes):\n    \"\"\"\n    Load all found model files and prepare them for ensemble\n    \"\"\"\n    models = []\n\n    # model_files = find_model_files(cfg)\n    model_files = cfg.model_files\n\n    if not model_files:\n        print(f\"Warning: No model files found under {cfg.model_path}!\")\n        return models\n\n    print(f\"Found a total of {len(model_files)} model files.\")\n\n    for i, model_path in enumerate(model_files):\n        try:\n            print(f\"Loading model: {model_path}\")\n            checkpoint = torch.load(model_path, map_location=torch.device(cfg.device), weights_only=False)\n            cfg_temp = checkpoint['cfg']\n            cfg_temp['device'] = cfg.device\n\n            model = BirdCLEFModel(cfg_temp)\n            model.load_state_dict(checkpoint['model_state_dict'])\n            model = model.to(cfg.device)\n            model.eval()\n            model.zero_grad()\n            model.half().float()\n\n            models.append(model)\n        except Exception as e:\n            print(f\"Error loading model {model_path}: {e}\")\n\n    return models\n\n\ndef predict_on_spectrogram(audio_path, models, cfg, species_ids):\n    \"\"\"Process a single audio file and predict species presence for each 5-second segment\"\"\"\n    audio_path = str(audio_path)\n    predictions = []\n    row_ids = []\n    soundscape_id = Path(audio_path).stem\n\n    print(f\"Processing {soundscape_id}\")\n    audio_data = load_sample(audio_path, cfg)\n    for segment_idx, audio_input in enumerate(audio_data):\n\n        end_time_sec = (segment_idx + 1) * cfg.target_duration\n        row_id = f\"{soundscape_id}_{end_time_sec}\"\n        row_ids.append(row_id)\n\n        mel_spec = torch.tensor(audio_input, dtype=torch.float32).unsqueeze(0).unsqueeze(0)\n        mel_spec = mel_spec.to(cfg.device)\n\n        if len(models) == 1:\n            with torch.no_grad():\n                outputs = models[0].infer(mel_spec)\n                final_preds = outputs.squeeze()\n                # final_preds = torch.sigmoid(outputs).cpu().numpy().squeeze()\n\n        else:\n            segment_preds = []\n            for model in models:\n                with torch.no_grad():\n                    outputs = model.infer(mel_spec)\n                    probs = outputs.squeeze()\n                    # probs = torch.sigmoid(outputs).cpu().numpy().squeeze()\n                    segment_preds.append(probs)\n\n            final_preds = np.mean(segment_preds, axis=0)\n\n        predictions.append(final_preds)\n\n    predictions = np.stack(predictions, axis=0)\n\n    return row_ids, predictions\n\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    if cfg.debug:\n        test_files = sorted(glob(str(Path(cfg.train_soundscapes) / '*.ogg')))[:cfg.debug_num]\n\n    print(f\"Found {len(test_files)} test soundscapes\")\n\n    all_row_ids = []\n    all_predictions = []\n\n    with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor:\n        results = list(\n            executor.map(\n                predict_on_spectrogram,\n                test_files,\n                itertools.repeat(models),\n                itertools.repeat(cfg),\n                itertools.repeat(species_ids)\n            )\n        )\n\n    for rids, preds in results:\n        all_row_ids.extend(rids)\n        all_predictions.extend(preds)\n\n    return all_row_ids, all_predictions\n\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\n    submission_df.set_index('row_id', inplace=True)\n\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\n    submission_df = submission_df.reset_index()\n\n    return submission_df\n\n\ndef smooth_submission(submission_path):\n    \"\"\"\n    Post-process the submission CSV by smoothing predictions to enforce temporal consistency.\n\n    For each soundscape (grouped by the file name part of 'row_id'), each row's predictions\n    are averaged with those of its neighbors using defined weights.\n\n    :param submission_path: Path to the submission CSV file.\n    \"\"\"\n    print(\"Smoothing submission predictions...\")\n    sub = pd.read_csv(submission_path)\n    cols = sub.columns[1:]\n    # Extract group names by splitting row_id on the last underscore\n    groups = sub['row_id'].str.rsplit('_', n=1).str[0].values\n    unique_groups = np.unique(groups)\n\n    for group in unique_groups:\n        # Get indices for the current group\n        idx = np.where(groups == group)[0]\n        sub_group = sub.iloc[idx].copy()\n        predictions = sub_group[cols].values\n        new_predictions = predictions.copy()\n\n        if predictions.shape[0] > 1:\n            # Smooth the predictions using neighboring segments\n            new_predictions[0] = (predictions[0] * 0.8) + (predictions[1] * 0.2)\n            new_predictions[-1] = (predictions[-1] * 0.8) + (predictions[-2] * 0.2)\n            for i in range(1, predictions.shape[0] - 1):\n                new_predictions[i] = (predictions[i - 1] * 0.15) + (predictions[i] * 0.7) + (predictions[i + 1] * 0.15)\n        # Replace the smoothed values in the submission dataframe\n        sub.iloc[idx, 1:] = new_predictions\n\n    sub.to_csv(submission_path, index=False)\n    print(f\"Smoothed submission saved to {submission_path}\")\n\ndef apply_power_to_low_ranked_cols(\n    p: np.ndarray,\n    top_k: int = 30,\n    exponent: Union[int, float] = 2,\n    inplace: bool = True\n) -> np.ndarray:\n    if not inplace:\n        p = p.copy()\n\n    # Identify columns whose max value ranks below `top_k`\n    tail_cols = np.argsort(-p.max(axis=0))[top_k:]\n\n    # Apply the power transformation to those columns\n    p[:, tail_cols] = p[:, tail_cols] ** exponent\n    return p\n\nwav_sec = 5\nsample_rate = 32000\nmin_segment = sample_rate * wav_sec\n\nn_fft = 1024\nwin_length = 1024\nhop_length = 512\nf_min = 50\nf_max = 16000\nn_mels = 128\n\nmel_spectrogram = AT.MelSpectrogram(\n        sample_rate=sample_rate,\n        n_fft=n_fft,\n        win_length=win_length,\n        hop_length=hop_length,\n        center=True,\n        f_min=f_min,\n        f_max=f_max,\n        pad_mode=\"reflect\",\n        power=2.0,\n        norm='slaney',\n        n_mels=n_mels,\n        mel_scale=\"htk\",\n        # normalized=True\n    )\n\ndef audio_to_mel(filepath=None):\n    waveform, sample_rate = torchaudio.load(filepath, backend=\"soundfile\")\n    len_wav = waveform.shape[1]\n    waveform = waveform[0, :].reshape(1, len_wav)  # stereo->mono mono->mono\n    PREDS = []\n    for i in range(12):\n        waveform2 = waveform[:, i * sample_rate * 5:i * sample_rate * 5 + sample_rate * 5]\n        melspec = mel_spectrogram(waveform2)\n        melspec = torch.log(melspec + 1e-6)\n        melspec = normalize_std(melspec)\n        melspec = torch.unsqueeze(melspec, dim=0)\n\n        PREDS.append(melspec)\n    return torch.vstack(PREDS)\n\ndef init_weights(model):\n    classname = model.__class__.__name__\n    if classname.find(\"Conv2d\") != -1:\n        nn.init.xavier_uniform_(model.weight, gain=np.sqrt(2))\n        model.bias.data.fill_(0)\n    elif classname.find(\"BatchNorm\") != -1:\n        model.weight.data.normal_(1.0, 0.02)\n        model.bias.data.fill_(0)\n    elif classname.find(\"GRU\") != -1:\n        for weight in model.parameters():\n            if len(weight.size()) > 1:\n                nn.init.orghogonal_(weight.data)\n    elif classname.find(\"Linear\") != -1:\n        model.weight.data.normal_(0, 0.01)\n        model.bias.data.zero_()\n\n\ndef interpolate(x, ratio):\n    (batch_size, time_steps, classes_num) = x.shape\n    upsampled = x[:, :, None, :].repeat(1, 1, ratio, 1)\n    upsampled = upsampled.reshape(batch_size, time_steps * ratio, classes_num)\n    return upsampled\n\n\ndef pad_framewise_output(framewise_output, frames_num):\n    output = F.interpolate(\n        framewise_output.unsqueeze(1),\n        size=(frames_num, framewise_output.size(2)),\n        align_corners=True,\n        mode=\"bilinear\").squeeze(1)\n\n    return output\n\n\nclass AttBlockV2(nn.Module):\n    def __init__(self,\n                 in_features: int,\n                 out_features: int,\n                 activation=\"linear\"):\n        super().__init__()\n\n        self.activation = activation\n        self.att = nn.Conv1d(\n            in_channels=in_features,\n            out_channels=out_features,\n            kernel_size=1,\n            stride=1,\n            padding=0,\n            bias=True)\n        self.cla = nn.Conv1d(\n            in_channels=in_features,\n            out_channels=out_features,\n            kernel_size=1,\n            stride=1,\n            padding=0,\n            bias=True)\n\n        self.init_weights()\n\n    def init_weights(self):\n        init_layer(self.att)\n        init_layer(self.cla)\n\n    def forward(self, x):\n        norm_att = torch.softmax(torch.tanh(self.att(x)), dim=-1)\n        cla = self.nonlinear_transform(self.cla(x))\n        x = torch.sum(norm_att * cla, dim=2)\n        return x, norm_att, cla\n\n    def nonlinear_transform(self, x):\n        if self.activation == 'linear':\n            return x\n        elif self.activation == 'sigmoid':\n            return torch.sigmoid(x)\n\n\nclass TimmSED(nn.Module):\n    def __init__(self, base_model_name: str, pretrained=False, num_classes=24, in_channels=1, n_mels=24):\n        super().__init__()\n\n        self.bn0 = nn.BatchNorm2d(n_mels)\n\n        base_model = timm.create_model(\n            base_model_name, pretrained=pretrained, in_chans=in_channels)\n        layers = list(base_model.children())[:-2]\n        self.encoder = nn.Sequential(*layers)\n\n        in_features = base_model.num_features\n\n        self.fc1 = nn.Linear(in_features, in_features, bias=True)\n        self.att_block2 = AttBlockV2(\n            in_features, num_classes, activation=\"sigmoid\")\n\n        self.init_weight()\n\n    def init_weight(self):\n        init_bn(self.bn0)\n        init_layer(self.fc1)\n\n    def forward(self, input_data):\n        x = input_data.transpose(2, 3)\n        x = torch.cat((x, x, x), 1)\n\n        x = x.transpose(2, 3)\n\n        x = self.encoder(x)\n\n        x = torch.mean(x, dim=2)\n\n        x1 = F.max_pool1d(x, kernel_size=3, stride=1, padding=1)\n        x2 = F.avg_pool1d(x, kernel_size=3, stride=1, padding=1)\n        x = x1 + x2\n\n        x = x.transpose(1, 2)\n        x = F.relu_(self.fc1(x))\n        x = x.transpose(1, 2)\n\n        (clipwise_output, norm_att, segmentwise_output) = self.att_block2(x)\n        logit = torch.sum(norm_att * self.att_block2.cla(x), dim=2)\n\n        output_dict = {\n            'logit': logit,\n        }\n\n        return output_dict\n\ndef normalize_std(spec, eps=1e-6):\n        mean = torch.mean(spec)\n        std = torch.std(spec)\n        return torch.where(std == 0, spec - mean, (spec - mean) / (std + eps))\n\ndef audio2melspec(audio_data, cfg):\n    \"\"\"Convert audio data to mel spectrogram\"\"\"\n    if np.isnan(audio_data).any():\n        series = pd.Series(audio_data)\n        audio_data = series.interpolate(method='linear', limit_direction='both').to_numpy()\n    audio_data = librosa.effects.preemphasis(audio_data)\n    if cfg.threshold == 0.5:\n        mel_spec = librosa.feature.melspectrogram(\n            y=audio_data,\n            sr=cfg.SR,\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    else:\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        )\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 audio_pad(audio_data, target_len, fs):\n    while 2 * len(audio_data) <= target_len:\n        audio_data += audio_data.copy()\n    if len(audio_data) >= target_len:\n        return audio_data[:target_len]  # truncate if too long\n\n    audio_data = audio_data.copy()\n    needed = target_len - len(audio_data)\n\n    audio_data += audio_data[:needed]\n    return audio_data\n\n\ndef process_audio_segment(audio_data, cfg):\n    \"\"\"Process audio segment to get mel spectrogram and nan mask\"\"\"\n    if len(audio_data) < cfg.FS * cfg.WINDOW_SIZE:\n        audio_data = audio_pad(audio_data, cfg.FS * cfg.WINDOW_SIZE, cfg.FS)\n    mel_spec = audio2melspec(audio_data, cfg)  # shape: [n_mels, time_frames]\n    # Resize spectrogram to the target shape if necessary.\n    if mel_spec.shape != cfg.TARGET_SHAPE:\n        mel_spec = cv2.resize(mel_spec, cfg.TARGET_SHAPE, interpolation=cv2.INTER_LINEAR)\n    return mel_spec.astype(np.float32)\n\nfrom concurrent.futures import ThreadPoolExecutor, as_completed\n\n\ndef predict_on_spectrogram_openvino(audio_path, models, cfg, species_ids):\n    predictions = []\n    row_ids = []\n    soundscape_id = Path(audio_path).stem\n    model_weights = cfg.model_weights\n    model_weights = np.array(model_weights).reshape(-1, 1)\n    try:\n        print(f\"Processing {soundscape_id}\")\n        audio_data, _ = librosa.load(audio_path, sr=cfg.FS)\n        total_segments = int(len(audio_data) / (cfg.FS * cfg.WINDOW_SIZE))\n\n        for segment_idx in range(total_segments):\n            start_sample = segment_idx * cfg.FS * cfg.WINDOW_SIZE\n            end_sample = start_sample + cfg.FS * cfg.WINDOW_SIZE\n            segment_audio = audio_data[start_sample:end_sample]\n\n            end_time_sec = (segment_idx + 1) * cfg.WINDOW_SIZE\n            row_id = f\"{soundscape_id}_{end_time_sec}\"\n            row_ids.append(row_id)\n\n            futures = []\n            mel_spec = process_audio_segment(segment_audio, cfg)\n\n            with ThreadPoolExecutor(max_workers=cfg.num_workers) as executor:\n                for model_idx, compiled_model in enumerate(models):\n                    if cfg.use_tta and model_idx >= cfg.tta_using_threshold:\n                        for tta_idx in range(cfg.tta_count):\n                            mel_spec_tta = apply_tta(mel_spec, tta_idx)\n                            mel_tensor = torch.tensor(mel_spec_tta, dtype=torch.float32).unsqueeze(0).unsqueeze(\n                                0)  # shape: [1, 1, H, W]\n                            if cfg.channel_change_start <= model_idx <= cfg.channel_change_end and mel_tensor.shape[\n                                1] == 1:\n                                mel_tensor = mel_tensor.repeat(1, 3, 1, 1)\n                            futures.append(\n                                executor.submit(async_run_inference, compiled_model, mel_tensor.numpy())\n                            )\n                    else:\n                        mel_tensor = torch.tensor(mel_spec, dtype=torch.float32).unsqueeze(0).unsqueeze(\n                            0)  # shape: [1, 1, H, W]\n\n                        if cfg.channel_change_start <= model_idx <= cfg.channel_change_end and mel_tensor.shape[1] == 1:\n                            mel_tensor = mel_tensor.repeat(1, 3, 1, 1)\n\n                        futures.append(\n                            executor.submit(async_run_inference, compiled_model, mel_tensor.numpy())\n                        )\n                all_preds = []\n                for future in as_completed(futures):\n                    probs = future.result()\n                    all_preds.append(probs)\n                    \n                '''all_preds = np.stack(all_preds)  # shape: (num_models, num_classes)\n                final_preds = np.sum(all_preds * model_weights, axis=0)\n                predictions.append(final_preds)'''\n                \n                final_preds = np.mean(all_preds, axis=0)\n                predictions.append(final_preds)\n\n    except Exception as e:\n        print(f\"Error processing {audio_path}: {e}\")\n\n    return row_ids, predictions\n\ndef async_run_inference(compiled_model, input_numpy):\n    \"\"\"每个线程独立推理\"\"\"\n    infer_request = compiled_model.create_infer_request()\n    input_tensor = compiled_model.inputs[0]\n    outputs = infer_request.infer({input_tensor: input_numpy})\n    output_tensor = outputs[compiled_model.outputs[0]]\n    probs = torch.sigmoid(torch.tensor(output_tensor)).cpu().numpy().squeeze()\n    return probs\n\n\ndef apply_tta(mel, tta_idx):\n    if tta_idx == 0:\n        return mel\n    elif tta_idx == 1:\n        # 时间滚动\n        shift = mel.shape[1] // 10\n        return np.roll(mel, shift=shift, axis=1)\n    elif tta_idx == 2:\n        # 添加噪声\n        noise = np.random.normal(0, 0.01, mel.shape)\n        return mel + noise\n    elif tta_idx == 3:\n        # 时间遮挡（SpecAugment）\n        mel_mask = mel.copy()\n        t = mel.shape[1]\n        mask_width = t // 10\n        start = np.random.randint(0, t - mask_width)\n        mel_mask[:, start:start + mask_width] = 0\n        return mel_mask\n    elif tta_idx == 4:\n        # 翻转 + 增强对比度\n        flip = np.flip(mel, axis=1)\n        contrast = np.clip((flip - flip.mean()) * 1.2 + flip.mean(), 0, None)\n        return contrast\n    elif tta_idx == 5:\n        # 频率遮挡（SpecAugment）\n        mel_mask = mel.copy()\n        f = mel.shape[0]\n        mask_height = f // 10\n        start = np.random.randint(0, f - mask_height)\n        mel_mask[start:start + mask_height, :] = 0\n        return mel_mask\n\ndef run_inference_openvino(cfg, models, species_ids):\n    \"\"\"Run inference on all test soundscapes\"\"\"\n    if cfg.debug:\n        files_list = [f for f in sorted(os.listdir(cfg.train_soundscapes))]\n        files_list = [cfg.train_soundscapes + file.split('.')[0] + '.ogg' for file in files_list if file.endswith('.ogg')]\n        test_files = files_list[0:cfg.debug_num]\n    else:\n        test_audio_dir = cfg.test_soundscapes\n        file_list = [f for f in sorted(os.listdir(test_audio_dir))]\n        test_files = [test_audio_dir + file.split('.')[0] + '.ogg' for file in file_list if file.endswith('.ogg')]\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_id, prediction = predict_on_spectrogram_openvino(audio_path, models, cfg, species_ids)\n        all_row_ids.extend(row_id)\n        all_predictions.extend(prediction)\n    return all_row_ids, all_predictions\n    \ndef load_models_openvino(cfg):\n    models = []\n    core = ov.Core()\n    models_path = cfg.openvino_path\n    print(f'load {len(models_path)} models {models_path}')\n    for model_path in models_path:\n        try:\n            print(f\"モデルをロード中: {model_path}\")\n            openvino_model = core.read_model(model=model_path)\n            compiled_model = core.compile_model(openvino_model, device_name=\"CPU\")\n            models.append(compiled_model)  # 注意：保存 compiled_model，不是 infer_request\n        except Exception as e:\n            print(f\"モデル{model_path}のロード中にエラーが発生しました: {e}\")\n    return models\n    \ndef main(pred, file_list):\n    print(\"Starting BirdCLEF-2025 inference...\")\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    # single sed\n    print(f\"Model usage: {'Single model' if len(models) == 1 else f'Ensemble of {len(models)} models'}\")\n    start_time = time.time()\n    row_ids, predictions = run_inference(cfg, models, species_ids)\n    end_time = time.time()\n    print(\"inference time maybe:\",700 * (end_time - start_time) / cfg.debug_num, \"average_time:\",(end_time - start_time) / cfg.debug_num)\n    single_df = create_submission(row_ids, predictions, species_ids, cfg)\n    # openvino\n    models = load_models_openvino(cfg)\n    print(f\"Model usage: {'Single model' if len(models) == 1 else f'Ensemble of {len(models)} models'}\")\n    start_time = time.time()\n    row_ids, predictions = run_inference_openvino(cfg, models, species_ids)\n    end_time = time.time()\n    print(\"inference time maybe:\",700 * (end_time - start_time) / cfg.debug_num, \"average_time:\",(end_time - start_time) / cfg.debug_num)\n    openvino_df = create_submission(row_ids, predictions, species_ids, cfg)\n    del models, row_ids, predictions\n    gc.collect()\n    class_labels = sorted(os.listdir('/kaggle/input/birdclef-2025/train_audio/'))\n    for species_code in class_labels:\n        pred[species_code] = []\n    # 3-fold sed\n    start = time.time()\n    with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:\n        _ = list(executor.map(prediction, file_list))\n    end_t = time.time()\n    \n    if cfg.debug == True:\n        print(\"inference time maybe:\",700 * (end_t - start) / cfg.debug_num, \"average_time:\",(end_t - start) / cfg.debug_num)\n    \n    fold_df = pd.DataFrame(pred, columns = ['row_id'] + class_labels)\n    s1 = 'submission001.csv'\n    s2 = 'submission002.csv'\n    s3 = 'submission003.csv'\n    single_df.to_csv(s1, index=False)\n    openvino_df.to_csv(s2, index=False)\n    fold_df.to_csv(s3, index=False)\n\n    weight = cfg.ensemble_weights\n    \n    try:\n        submission_path_blended = 'submission.csv'\n        print(f\"Blending submissions with weights {weight}...\")\n        \n        result = single_df.set_index('row_id').multiply(weight[0]).add(\n            openvino_df.set_index('row_id').multiply(weight[1]), fill_value=0).add(\n            fold_df.set_index('row_id').multiply(weight[2]), fill_value=0).reset_index()\n        \n        result.to_csv(submission_path_blended, index=False)\n        print(f\"Blended submission saved to {submission_path_blended}\")\n\n        # Apply smoothing to the final blended submission\n        smooth_submission(submission_path_blended)\n\n    except FileNotFoundError as e:\n        print(f\"Error blending submissions: {e}. Ensure submission001.csv and submission002.csv exist.\")\n    except Exception as e:\n        print(f\"An error occurred during submission blending or smoothing: {e}\")\n\nif __name__ == \"__main__\":\n    pred = {'row_id': []}\n    test_audio_dir = cfg.test_soundscapes\n    file_list = [f for f in sorted(os.listdir(test_audio_dir))]\n    file_list = [file.split('.')[0] for file in file_list if file.endswith('.ogg')]\n    \n    if cfg.debug:\n        test_audio_dir = cfg.train_soundscapes\n        file_list = [f for f in sorted(os.listdir(test_audio_dir))]\n        file_list = [file.split('.')[0] for file in file_list if file.endswith('.ogg')]\n        file_list = file_list[0:cfg.debug_num]\n    n_mels = 128\n    class_labels = sorted(os.listdir('/kaggle/input/birdclef-2025/train_audio'))\n    base_model_name='eca_nfnet_l0'\n    pretrained=False\n    in_channels=3\n    \n    MODELS = [f'/kaggle/input/birdclef-2025-sed-models-p/sed{i}.pth' for i in range(3)]\n    print(MODELS)\n    models = []\n    for path in MODELS:\n        model = TimmSED(base_model_name=base_model_name,\n                   pretrained=pretrained,\n                   num_classes=len(class_labels),\n                   in_channels=in_channels,\n                   n_mels=n_mels);\n        model.load_state_dict(torch.load(path, weights_only=True, map_location=torch.device('cpu')))\n        model.eval();\n        models.append(model)\n    def prediction(afile):\n            global pred\n            path = test_audio_dir + afile + '.ogg'\n            with torch.inference_mode():\n                sig = audio_to_mel(path)\n                outputs = None\n                for model in models:\n                    model.eval()\n                    p = model(sig)\n                    p = torch.sigmoid(p['logit']).detach().cpu().numpy()\n                    p = apply_power_to_low_ranked_cols(p, top_k=30, exponent=2)\n                    if outputs is None:\n                        outputs = p\n                    else:\n                        outputs += p\n        \n                outputs /= len(models)\n                chunks = [[] for i in range(12)]\n                for i in range(len(chunks)):\n                    chunk_end_time = (i + 1) * 5\n                    row_id = afile + '_' + str(chunk_end_time)\n                    pred['row_id'].append(row_id)\n                    bird_no = 0\n                    for bird in class_labels:\n                        pred[bird].append(outputs[i, bird_no])\n                        bird_no += 1\n                gc.collect()\n    main(pred, file_list)\n    ","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}