{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.10.12","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":11617590,"sourceType":"datasetVersion","datasetId":7287967}],"dockerImageVersionId":30918,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# **BirdCLEF 2025 Inference Notebook**\nThis notebook runs inference on BirdCLEF 2025 test soundscapes and generates a submission file. It supports both single model inference and ensemble inference with multiple models. You can find the pre-processing and training processes in the following notebooks:\n\n- [Transforming Audio-to-Mel Spec. | BirdCLEF'25](https://www.kaggle.com/code/kadircandrisolu/transforming-audio-to-mel-spec-birdclef-25)  \n- [EfficientNet B0 Pytorch [Train] | BirdCLEF'25](https://www.kaggle.com/code/kadircandrisolu/efficientnet-b0-pytorch-train-birdclef-25)\n\n**Features**\n- Audio Preprocessing\n- Test-Time Augmentation (TTA)","metadata":{}},{"cell_type":"code","source":"import 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\nimport matplotlib.pyplot as plt\n\nwarnings.filterwarnings(\"ignore\")\nlogging.basicConfig(level=logging.ERROR)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-02T09:28:49.950563Z","iopub.execute_input":"2025-05-02T09:28:49.951084Z","iopub.status.idle":"2025-05-02T09:28:49.959646Z","shell.execute_reply.started":"2025-05-02T09:28:49.951048Z","shell.execute_reply":"2025-05-02T09:28:49.958457Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class CFG:\n \n    test_soundscapes = '/kaggle/input/birdclef-2025/test_soundscapes'\n    # test_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    \n    model_path = '/kaggle/input/vit-one-fold-patch8'\n    \n    # Audio parameters\n    FS = 32000  \n    WINDOW_SIZE = 5  \n    \n    # Mel spectrogram parameters\n    N_FFT = 1024\n    HOP_LENGTH = 512\n    N_MELS = 128\n    FMIN = 50\n    FMAX = 14000\n    TARGET_SHAPE = (224, 224)\n\n    model_name = 'vit_small_patch8_224.dino'\n    # output_dim = 768\n    output_dim = 384\n    \n    in_channels = 1\n    device = 'cpu'  \n    \n    # Inference parameters\n    batch_size = 16\n    use_tta = False  \n    tta_count = 3   \n    threshold = 0.5\n    \n    use_specific_folds = True  # If False, use all found models\n    folds = [0]  # Used only if use_specific_folds is True\n    \n    debug = False\n    debug_count = 2\n\ncfg = CFG()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-02T09:28:49.961901Z","iopub.execute_input":"2025-05-02T09:28:49.962536Z","iopub.status.idle":"2025-05-02T09:28:49.980153Z","shell.execute_reply.started":"2025-05-02T09:28:49.962496Z","shell.execute_reply":"2025-05-02T09:28:49.979027Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(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}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-02T09:28:49.982554Z","iopub.execute_input":"2025-05-02T09:28:49.982980Z","iopub.status.idle":"2025-05-02T09:28:50.011766Z","shell.execute_reply.started":"2025-05-02T09:28:49.982942Z","shell.execute_reply":"2025-05-02T09:28:50.010402Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class BirdCLEFModel(nn.Module):\n    def __init__(self, cfg, num_classes):\n        super().__init__()\n        self.cfg = cfg\n        \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        \n        if 'efficientnet' in cfg.model_name:\n            backbone_out = self.backbone.classifier.in_features\n            self.backbone.classifier = nn.Identity()\n        elif 'resnet' in cfg.model_name:\n            backbone_out = self.backbone.fc.in_features\n            self.backbone.fc = nn.Identity()\n        else:\n            # backbone_out = self.backbone.get_classifier().in_features\n            self.backbone.reset_classifier(0, '')\n        \n        self.pooling = nn.AdaptiveAvgPool2d(1)\n            \n        # self.feat_dim = backbone_out\n        \n        # self.classifier = nn.Linear(backbone_out, num_classes)\n        self.classifier = nn.Linear(cfg.output_dim, num_classes)\n        \n    def forward(self, x):\n        features = self.backbone(x)\n        \n        if isinstance(features, dict):\n            features = features['features']\n            \n        if len(features.shape) == 4:\n            features = self.pooling(features)\n            features = features.view(features.size(0), -1)\n\n        logits = self.classifier(features)\n        return logits\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-02T09:28:50.013756Z","iopub.execute_input":"2025-05-02T09:28:50.014427Z","iopub.status.idle":"2025-05-02T09:28:50.033262Z","shell.execute_reply.started":"2025-05-02T09:28:50.014366Z","shell.execute_reply":"2025-05-02T09:28:50.031856Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def audio2melspec(audio_data, cfg):\n    \"\"\"Convert audio data to mel spectrogram\"\"\"\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\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\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 process_audio_segment(audio_data, cfg):\n    \"\"\"Process audio segment to get mel spectrogram\"\"\"\n    if len(audio_data) < cfg.FS * cfg.WINDOW_SIZE:\n        audio_data = np.pad(audio_data, \n                          (0, cfg.FS * cfg.WINDOW_SIZE - len(audio_data)), \n                          mode='constant')\n    \n    mel_spec = audio2melspec(audio_data, cfg)\n    \n    # Resize if needed\n    if mel_spec.shape != cfg.TARGET_SHAPE:\n        mel_spec = cv2.resize(mel_spec, cfg.TARGET_SHAPE, interpolation=cv2.INTER_LINEAR)\n    \n    return mel_spec.astype(np.float32)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-02T09:28:50.034470Z","iopub.execute_input":"2025-05-02T09:28:50.034865Z","iopub.status.idle":"2025-05-02T09:28:50.059078Z","shell.execute_reply.started":"2025-05-02T09:28:50.034822Z","shell.execute_reply":"2025-05-02T09:28:50.057959Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def 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\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    \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    if cfg.use_specific_folds:\n        filtered_files = []\n        for fold in cfg.folds:\n            fold_files = [f for f in model_files if f\"fold{fold}\" in f]\n            filtered_files.extend(fold_files)\n        model_files = filtered_files\n        print(f\"Using {len(model_files)} model files for the specified folds ({cfg.folds}).\")\n    \n    for model_path in model_files:\n        try:\n            print(f\"Loading model: {model_path}\")\n            checkpoint = torch.load(model_path, map_location=torch.device(cfg.device))\n\n            model = BirdCLEFModel(cfg, num_classes)\n            model.load_state_dict(checkpoint['model_state_dict'])\n            model = model.to(cfg.device)\n            model.eval()\n            \n            models.append(model)\n        except Exception as e:\n            print(f\"Error loading model {model_path}: {e}\")\n    \n    return models\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    predictions = [] # This list will store one (206,) prediction vector for each 5-second segment\n    row_ids = []\n    soundscape_id = Path(audio_path).stem\n\n    try:\n        print(f\"Processing {soundscape_id}\")\n        audio_data, _ = librosa.load(audio_path, sr=cfg.FS)\n\n        # Calculate total 5-second segments\n        total_segments = int(len(audio_data) / (cfg.FS * cfg.WINDOW_SIZE))\n\n        # Iterate through each 5-second segment\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            # Process the 5-second segment\n            if cfg.use_tta:\n                all_tta_step_predictions = [] # List to hold (197, 206) arrays from TTA\n\n                for tta_idx in range(cfg.tta_count):\n                    mel_spec = process_audio_segment(segment_audio, cfg)\n                    mel_spec = apply_tta(mel_spec, tta_idx)\n\n                    # Add batch and channel dimensions\n                    mel_spec_tensor = torch.tensor(mel_spec, dtype=torch.float32).unsqueeze(0).unsqueeze(0)\n                    mel_spec_tensor = mel_spec_tensor.to(cfg.device)\n\n                    if len(models) == 1:\n                        with torch.no_grad():\n                            outputs = models[0](mel_spec_tensor) # Output shape (1, 197, 206)\n                            # Squeeze batch dim (0), result is (197, 206) -> predictions for all 197 steps within this 5s segment\n                            step_predictions = torch.sigmoid(outputs).cpu().numpy().squeeze(0)\n                            all_tta_step_predictions.append(step_predictions)\n                    else: # Multiple models\n                        segment_model_step_predictions = [] # List to hold (197, 206) arrays from different models\n                        for model in models:\n                            with torch.no_grad():\n                                outputs = model(mel_spec_tensor) # Output shape (1, 197, 206)\n                                # Squeeze batch dim (0), result is (197, 206)\n                                step_predictions = torch.sigmoid(outputs).cpu().numpy().squeeze(0)\n                                segment_model_step_predictions.append(step_predictions)\n\n                        # Average across models (axis 0), result is (197, 206)\n                        avg_model_step_predictions = np.mean(segment_model_step_predictions, axis=0)\n                        all_tta_step_predictions.append(avg_model_step_predictions)\n\n                # Average across TTA (axis 0), result is (197, 206)\n                combined_step_predictions = np.mean(all_tta_step_predictions, axis=0)\n\n                # --- FIX APPLIED HERE ---\n                # Aggregate the 197 time step predictions (axis 0) into a single vector (size 206) for this 5-sec segment\n                segment_prediction = np.mean(combined_step_predictions, axis=0) # Shape becomes (206,)\n                # --- END FIX ---\n\n            else: # No TTA\n                mel_spec = process_audio_segment(segment_audio, cfg)\n\n                mel_spec_tensor = torch.tensor(mel_spec, dtype=torch.float32).unsqueeze(0).unsqueeze(0)\n                mel_spec_tensor = mel_spec_tensor.to(cfg.device)\n\n                # mel_spec_np = mel_spec_tensor.squeeze().cpu().numpy()\n                # # Plot the mel spectrogram\n                # plt.figure(figsize=(5, 5))\n                # plt.imshow(mel_spec_np)\n                # plt.show()\n\n                if len(models) == 1:\n                    with torch.no_grad():\n                        outputs = models[0](mel_spec_tensor) # Output shape (1, 197, 206)\n                        # Squeeze batch dim (0), result is (197, 206)\n                        step_predictions = torch.sigmoid(outputs).cpu().numpy().squeeze(0)\n\n                else: # Multiple models\n                    segment_model_step_predictions = []\n                    for model in models:\n                        with torch.no_grad():\n                            outputs = model(mel_spec_tensor) # Output shape (1, 197, 206)\n                            # Squeeze batch dim (0), result is (197, 206)\n                            step_predictions = torch.sigmoid(outputs).cpu().numpy().squeeze(0)\n                            segment_model_step_predictions.append(step_predictions)\n\n                    # Average across models (axis 0), result is (197, 206)\n                    step_predictions = np.mean(segment_model_step_predictions, axis=0)\n\n                # --- FIX APPLIED HERE ---\n                # Aggregate the 197 time step predictions (axis 0) into a single vector (size 206) for this 5-sec segment\n                segment_prediction = np.mean(step_predictions, axis=0) # Shape becomes (206,)\n                # --- END FIX ---\n\n\n            # Append the single prediction vector (size 206) for this 5-second segment\n            predictions.append(segment_prediction)\n\n    except Exception as e:\n        print(f\"Error processing {audio_path}: {e}\")\n        # Depending on your needs, you might want to append a placeholder\n        # prediction (e.g., zeros) if an error occurs, to keep dimensions consistent.\n        # For this fix, we assume successful processing or the error is handled upstream.\n\n\n    # predict_on_spectrogram now returns a list of (206,) vectors, one for each row_id\n    return row_ids, predictions","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-02T09:28:50.060356Z","iopub.execute_input":"2025-05-02T09:28:50.061456Z","iopub.status.idle":"2025-05-02T09:28:50.085163Z","shell.execute_reply.started":"2025-05-02T09:28:50.061416Z","shell.execute_reply":"2025-05-02T09:28:50.084010Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def apply_tta(spec, tta_idx):\n    \"\"\"Apply test-time augmentation\"\"\"\n    if tta_idx == 0:\n        # Original spectrogram\n        return spec\n    elif tta_idx == 1:\n        # Time shift (horizontal flip)\n        return np.flip(spec, axis=1)\n    elif tta_idx == 2:\n        # Frequency shift (vertical flip)\n        return np.flip(spec, axis=0)\n    else:\n        return spec\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    \n    if cfg.debug:\n        print(f\"Debug mode enabled, using only {cfg.debug_count} files\")\n        test_files = test_files[:cfg.debug_count]\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_ids, predictions = predict_on_spectrogram(str(audio_path), models, cfg, species_ids)\n        all_row_ids.extend(row_ids)\n        all_predictions.extend(predictions)\n    \n    return all_row_ids, all_predictions\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","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-02T09:28:50.086443Z","iopub.execute_input":"2025-05-02T09:28:50.086881Z","iopub.status.idle":"2025-05-02T09:28:50.107666Z","shell.execute_reply.started":"2025-05-02T09:28:50.086839Z","shell.execute_reply":"2025-05-02T09:28:50.106466Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def main():\n    start_time = time.time()\n    print(\"Starting BirdCLEF-2025 inference...\")\n    print(f\"TTA enabled: {cfg.use_tta} (variations: {cfg.tta_count if cfg.use_tta else 0})\")\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    \n    print(f\"Model usage: {'Single model' if len(models) == 1 else f'Ensemble of {len(models)} models'}\")\n\n    row_ids, predictions = run_inference(cfg, models, species_ids)\n    submission_df = create_submission(row_ids, predictions, species_ids, cfg)\n\n    submission_path = 'submission.csv'\n    submission_df.to_csv(submission_path, index=False)\n    print(f\"Submission saved to {submission_path}\")\n    \n    end_time = time.time()\n    print(f\"Inference completed in {(end_time - start_time)/60:.2f} minutes\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-02T09:28:50.110371Z","iopub.execute_input":"2025-05-02T09:28:50.110793Z","iopub.status.idle":"2025-05-02T09:28:50.132781Z","shell.execute_reply.started":"2025-05-02T09:28:50.110767Z","shell.execute_reply":"2025-05-02T09:28:50.131466Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"if __name__ == \"__main__\":\n    main()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-02T09:28:50.134344Z","iopub.execute_input":"2025-05-02T09:28:50.134922Z","iopub.status.idle":"2025-05-02T09:28:51.171792Z","shell.execute_reply.started":"2025-05-02T09:28:50.134882Z","shell.execute_reply":"2025-05-02T09:28:51.170417Z"}},"outputs":[],"execution_count":null}]}