{"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":[{"sourceType":"competition","sourceId":91844,"databundleVersionId":11361821},{"sourceType":"kernelVersion","sourceId":240685063}],"dockerImageVersionId":31040,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# Import Libraries","metadata":{}},{"cell_type":"code","source":"import os\nimport gc\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","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-05-19T21:41:46.593213Z","iopub.execute_input":"2025-05-19T21:41:46.593493Z","iopub.status.idle":"2025-05-19T21:41:46.597441Z","shell.execute_reply.started":"2025-05-19T21:41:46.593476Z","shell.execute_reply":"2025-05-19T21:41:46.596637Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Configuration","metadata":{}},{"cell_type":"code","source":"class CFG:\n    # Data Paths\n    test_soundscapes: str = \"/kaggle/input/birdclef-2025/test_soundscapes\"\n    submission_csv: str = \"/kaggle/input/birdclef-2025/sample_submission.csv\"\n    taxonomy_csv: str = \"/kaggle/input/birdclef-2025/taxonomy.csv\"\n    model_path: str = \"/kaggle/input/efficientnet-b0-pytorch-train\"\n\n    # Audio parameters\n    # Sampling rate for audio processing (samples per second).\n    FS: int = 32000  \n    # The duration of each audio segment to be processed in seconds.\n    WINDOW_SIZE: int = 5  \n\n    # Mel spectrogram parameters\n    # The number of FFT components used to compute the spectrogram.\n    N_FFT: int = 1024\n    # The number of samples between successive frames in the spectrogram.\n    HOP_LENGTH: int = 512\n    # The number of Mel bands to generate.\n    N_MELS: int = 128\n    # The minimum frequency (in Hz) to include in the Mel spectrogram.\n    FMIN: int = 50\n    # The maximum frequency (in Hz) to include in the Mel spectrogram.\n    FMAX: int = 14000\n    # The target shape (height, width) for the mel spectrogram image after resizing.\n    TARGET_SHAPE: tuple[int, int] = (256, 256)\n\n    # Model parameters\n    # The name of the base model architecture to use (e.g., \"efficientnet_b0\").\n    model_name: str = \"efficientnet_b0\"\n    # The number of input channels for the model (1 for grayscale/mel spectrogram).\n    in_channels: int = 1\n    # The device to use for inference (\"cuda\" for GPU, \"cpu\" for CPU).\n    device: str = \"cuda\" if torch.cuda.is_available() else \"cpu\"\n\n    # Inference parameters\n    # The number of audio segments to process in a single batch during inference.\n    batch_size: int = 16\n    # Flag to enable or disable Test-Time Augmentation (TTA).\n    use_tta: bool = False  \n    # The number of TTA variations to apply if use_tta is True.\n    tta_count: int = 3   \n    # The probability threshold to consider a species as detected in a segment.\n    threshold: int = 0.5\n\n    # Model selection\n    # Flag to use only models from specific folds if True. If False, all found models are used.\n    use_specific_folds: bool = False\n    # A list of fold numbers to use if use_specific_folds is True.\n    folds: tuple[int, int] = [0, 1]  # Used only if use_specific_folds is True\n\n    # Debugging\n    # Flag to enable debug mode. If True, only a small subset of test files is processed.\n    debug: bool = False\n    # The number of test files to process in debug mode.\n    debug_count: bool = 3\n\ncfg = CFG()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-19T21:41:47.744414Z","iopub.execute_input":"2025-05-19T21:41:47.744705Z","iopub.status.idle":"2025-05-19T21:41:47.754960Z","shell.execute_reply.started":"2025-05-19T21:41:47.744684Z","shell.execute_reply":"2025-05-19T21:41:47.754042Z"}},"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-19T21:41:53.338980Z","iopub.execute_input":"2025-05-19T21:41:53.339236Z","iopub.status.idle":"2025-05-19T21:41:53.347201Z","shell.execute_reply.started":"2025-05-19T21:41:53.339218Z","shell.execute_reply":"2025-05-19T21:41:53.346497Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class BirdCLEFModel(nn.Module):\n    \"\"\"\n    PyTorch model for bird song classification using a backbone architecture \n    (like EfficientNet) followed by a classifier.\n\n    Args:\n        cfg (CFG): Configuration object containing model and data parameters.\n        num_classes (int): The number of output classes (bird species).\n    \"\"\"\n\n    def __init__(self, cfg: CFG, num_classes: int):\n        super().__init__()\n        self.cfg = cfg\n        \n        self.backbone = timm.create_model(\n            cfg.model_name,\n            pretrained=False, # Set to False as we are loading weights later\n            in_chans=cfg.in_channels,\n            drop_rate=0.0,    \n            drop_path_rate=0.0\n        )\n\n        # Modify the classifier layer of the backbone based on its type\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            # For other timm models, get classifier features and reset it\n            backbone_out = self.backbone.get_classifier().in_features\n            self.backbone.reset_classifier(0, \"\")\n        \n        self.pooling = nn.AdaptiveAvgPool2d(1)\n        self.feat_dim = backbone_out\n        self.classifier = nn.Linear(backbone_out, num_classes)\n        \n    def forward(self, x: torch.Tensor) -> torch.Tensor:\n        \"\"\"\n        Forward pass of the model.\n\n        Args:\n            x (torch.Tensor): Input tensor (mel spectrogram), expected shape \n                              (batch_size, channels, height, width).\n\n        Returns:\n            torch.Tensor: Output logits from the classifier, shape (batch_size, num_classes).\n        \"\"\"\n\n        features = self.backbone(x)\n\n        # Handle potential dictionary output from some backbones\n        if isinstance(features, dict):\n            features = features['features']\n\n        # Apply pooling if the features are 4D (image-like)\n        if len(features.shape) == 4:\n            features = self.pooling(features)\n            features = features.view(features.size(0), -1) # Flatten the features\n        \n        logits = self.classifier(features)\n        return logits","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-19T21:47:20.783629Z","iopub.execute_input":"2025-05-19T21:47:20.783898Z","iopub.status.idle":"2025-05-19T21:47:20.790818Z","shell.execute_reply.started":"2025-05-19T21:47:20.783881Z","shell.execute_reply":"2025-05-19T21:47:20.789872Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Audio Processing Functions","metadata":{}},{"cell_type":"code","source":"def audio2melspec(audio_data: np.ndarray, cfg: CFG) -> np.ndarray:\n    \"\"\"Convert audio data to mel spectrogram\n\n    Args:\n        audio_data (np.ndarray): The input audio data as a NumPy array.\n        cfg (CFG): Configuration object containing mel spectrogram parameters\n             like FS, N_FFT, HOP_LENGTH, N_MELS, FMIN, FMAX.\n\n    Returns:\n        np.ndarray: The normalized mel spectrogram as a NumPy array.\n    \"\"\"\n\n    # Handle potential NaN values in the 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\n    # Compute the mel spectrogram\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 # Use power=2.0 for the power spectrogram\n    )\n\n    # Convert power spectrogram to decibels (dB)\n    mel_spec_db = librosa.power_to_db(mel_spec, ref=np.max)\n\n    # Normalize the dB mel spectrogram to a 0-1 range\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","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-19T21:47:22.750310Z","iopub.execute_input":"2025-05-19T21:47:22.750615Z","iopub.status.idle":"2025-05-19T21:47:22.755765Z","shell.execute_reply.started":"2025-05-19T21:47:22.750589Z","shell.execute_reply":"2025-05-19T21:47:22.755006Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def process_audio_segment(audio_data: np.ndarray, cfg: CFG) -> np.ndarray:\n    \"\"\"Process audio segment to get mel spectrogram\n\n    Pads the audio segment if it's shorter than the window size, converts it\n    to a mel spectrogram using audio2melspec, and resizes the spectrogram\n    to the target shape specified in the config.\n\n    Args:\n        audio_data (np.ndarray): The input audio data segment as a NumPy array.\n        cfg (CFG): Configuration object containing audio and mel spectrogram parameters\n             like FS, WINDOW_SIZE, TARGET_SHAPE.\n\n    Returns:\n        np.ndarray: The processed and resized mel spectrogram as a NumPy array \n                    of type np.float32.\n    \"\"\"\n\n    # Pad the audio data if its length is less than the required window size\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    # Convert the audio data segment to a mel spectrogram\n    mel_spec = audio2melspec(audio_data, cfg)\n    \n    # Resize the mel spectrogram if its shape does not match the target shape\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 the processed mel spectrogram, ensuring it's of type float32\n    return mel_spec.astype(np.float32)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-19T21:48:35.032164Z","iopub.execute_input":"2025-05-19T21:48:35.032834Z","iopub.status.idle":"2025-05-19T21:48:35.037857Z","shell.execute_reply.started":"2025-05-19T21:48:35.032813Z","shell.execute_reply":"2025-05-19T21:48:35.037062Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def find_model_files(cfg: CFG) -> list[str]:\n    \"\"\"\n    Find all .pth model files in the specified model directory\n\n    Args:\n        cfg: Configuration object containing audio and mel spectrogram parameters\n             like FS, WINDOW_SIZE, TARGET_SHAPE.\n\n    Returns:\n        list[str]: Paths of model files\n    \"\"\"\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","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-19T21:50:36.520507Z","iopub.execute_input":"2025-05-19T21:50:36.520750Z","iopub.status.idle":"2025-05-19T21:50:36.525470Z","shell.execute_reply.started":"2025-05-19T21:50:36.520733Z","shell.execute_reply":"2025-05-19T21:50:36.524722Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def load_models(cfg: CFG, num_classes: int) -> list[BirdCLEFModel]:\n    \"\"\"\n    Load all found model files and prepare them for ensemble\n\n    Searches for .pth model files in the directory specified by cfg.model_path.\n    If use_specific_folds is True in the config, it filters the models to\n    include only those from the specified folds. Each found model file is\n    loaded into a BirdCLEFModel instance, moved to the configured device,\n    and set to evaluation mode.\n\n    Args:\n        cfg (cfg): Configuration object containing model loading parameters\n             like model_path, use_specific_folds, folds, device.\n        num_classes (int): The number of output classes for the models.\n\n    Returns:\n        list[BirdCLEFModel]: A list of loaded BirdCLEFModel instances. Returns an empty list\n              if no models are found or loaded successfully.\n    \"\"\"\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            # Basic check for fold number in file path string\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    # Load models individually\n    for model_path in model_files:\n        try:\n            print(f\"Loading model: {model_path}\")\n            # Load the model checkpoint, mapping to the specified device\n            checkpoint = torch.load(model_path, map_location=torch.device(cfg.device), weights_only=False)\n\n            # Initialize the model architecture\n            model = BirdCLEFModel(cfg, num_classes)\n\n            # Load the state dictionary from the checkpoint\n            model.load_state_dict(checkpoint['model_state_dict'])\n\n            # Move the model to the specified device\n            model = model.to(cfg.device)\n\n            # Set the model to evaluation mode (disables dropout, batch normalization updates, etc.)\n            model.eval()\n\n            # Add the loaded model to the list\n            models.append(model)\n        except Exception as e:\n            # Print an error message if loading fails for a specific model\n            print(f\"Error loading model {model_path}: {e}\")\n\n    return models","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-19T21:52:46.204615Z","iopub.execute_input":"2025-05-19T21:52:46.204927Z","iopub.status.idle":"2025-05-19T21:52:46.212858Z","shell.execute_reply.started":"2025-05-19T21:52:46.204908Z","shell.execute_reply":"2025-05-19T21:52:46.212138Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def predict_on_spectrogram(audio_path: str, models: list[BirdCLEFModel], cfg: CFG, species_ids) -> tuple[int, np.ndarray]:\n    \"\"\"\n    Process a single audio file and predict species presence for each 5-second segment.\n\n    Loads an audio file, divides it into segments, processes each segment\n    into a mel spectrogram, applies TTA if enabled, passes the spectrogram(s)\n    through the model(s), and collects the predicted probabilities for each\n    species in each segment.\n\n    Args:\n        audio_path (str): The file path to the audio file (e.g., .ogg).\n        models (list[BirdCLEFModel]): A list of loaded BirdCLEFModel instances for inference\n                (can be a single model in a list or an ensemble).\n        cfg (CFG): Configuration object containing inference parameters\n             like FS, WINDOW_SIZE, use_tta, tta_count, device.\n        species_ids (list[int]): A list of species IDs corresponding to the model output indices.\n\n    Returns:\n        tuple: A tuple containing two elements:\n            - list: A list of row_ids (strings) for each segment.\n            - list: A list of NumPy arrays, where each array contains the \n                    predicted probabilities for all species for a segment.\n              Returns empty lists within the tuple if processing fails.\n    \"\"\"\n\n    predictions = []\n    row_ids = []\n    soundscape_id = Path(audio_path).stem\n    \n    try:\n        print(f\"Processing {soundscape_id}\")\n        # Load the full audio file\n        audio_data, _ = librosa.load(audio_path, sr=cfg.FS)\n\n        # Calculate the total number of full 5-second segments\n        total_segments = int(len(audio_data) / (cfg.FS * cfg.WINDOW_SIZE))\n\n        # Process each 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            # Determine the end time for the row_id\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            # Inference with or without TTA\n            if cfg.use_tta:\n                all_preds = []\n                \n                for tta_idx in range(cfg.tta_count):\n                    # Process segment and apply TTA\n                    mel_spec = process_audio_segment(segment_audio, cfg)\n                    mel_spec = apply_tta(mel_spec, tta_idx)\n\n                    # Prepare spectrogram for the model (add batch and channel dimensions)\n                    mel_spec = torch.tensor(mel_spec, dtype=torch.float32).unsqueeze(0).unsqueeze(0)\n                    mel_spec = mel_spec.to(cfg.device)\n\n                    # Perform inference with single model or ensemble for this TTA variation\n                    if len(models) == 1:\n                        with torch.no_grad():\n                            outputs = models[0](mel_spec)\n                            probs = torch.sigmoid(outputs).cpu().numpy().squeeze()\n                            all_preds.append(probs)\n                    else: # Ensemble\n                        segment_preds = []\n                        for model in models:\n                            with torch.no_grad():\n                                outputs = model(mel_spec)\n                                probs = torch.sigmoid(outputs).cpu().numpy().squeeze()\n                                segment_preds.append(probs)\n                        \n                        avg_preds = np.mean(segment_preds, axis=0)\n                        all_preds.append(avg_preds)\n\n                # Average predictions across all TTA variations\n                final_preds = np.mean(all_preds, axis=0)\n            else: # No TTA\n                mel_spec = process_audio_segment(segment_audio, cfg)\n\n                # Prepare spectrogram for the model\n                mel_spec = torch.tensor(mel_spec, dtype=torch.float32).unsqueeze(0).unsqueeze(0)\n                mel_spec = mel_spec.to(cfg.device)\n\n                # Perform inference with single model or ensemble\n                if len(models) == 1:\n                    with torch.no_grad():\n                        outputs = models[0](mel_spec)\n                        final_preds = torch.sigmoid(outputs).cpu().numpy().squeeze()\n                else: # Ensemble\n                    segment_preds = []\n                    for model in models:\n                        with torch.no_grad():\n                            outputs = model(mel_spec)\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    except Exception as e:\n        # Print an error if processing the audio file fails\n        print(f\"Error processing {audio_path}: {e}\")\n    \n    return row_ids, predictions","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-19T21:57:07.788067Z","iopub.execute_input":"2025-05-19T21:57:07.788342Z","iopub.status.idle":"2025-05-19T21:57:07.798918Z","shell.execute_reply.started":"2025-05-19T21:57:07.788324Z","shell.execute_reply":"2025-05-19T21:57:07.797620Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def apply_tta(spec: np.ndarray, tta_idx: int) -> np.ndarray:\n    \"\"\"\n    Apply test-time augmentation to a mel spectrogram.\n\n    Applies different transformations to the input spectrogram based on the \n    augmentation index. Supported transformations are original, horizontal flip\n    (time shift), and vertical flip (frequency shift).\n\n    Args:\n        spec (np.ndarray): The input mel spectrogram as a NumPy array.\n        tta_idx (int): The index specifying which TTA transformation to apply.\n                       - 0: Original (no transformation)\n                       - 1: Horizontal flip (time axis)\n                       - 2: Vertical flip (frequency axis)\n                       - Others: Original (no transformation)\n\n    Returns:\n        np.ndarray: The augmented mel spectrogram as a NumPy array.\n    \"\"\"\n\n    if tta_idx == 0:\n        # Original spectrogram\n        return spec\n    elif tta_idx == 1:\n        # Time shift (horizontal flip) by flipping along the second axis (columns)\n        return np.flip(spec, axis=1)\n    elif tta_idx == 2:\n        # Frequency shift (vertical flip) by flipping along the first axis (rows)\n        return np.flip(spec, axis=0)\n    else:\n        # Default to original if index is not recognized\n        return spec","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-19T21:57:59.815015Z","iopub.execute_input":"2025-05-19T21:57:59.815251Z","iopub.status.idle":"2025-05-19T21:57:59.820467Z","shell.execute_reply.started":"2025-05-19T21:57:59.815236Z","shell.execute_reply":"2025-05-19T21:57:59.819804Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def run_inference(cfg: CFG, models: list[BirdCLEFModel], species_ids: list[int]) -> tuple[int, np.ndarray]:\n    \"\"\"\n    Run inference on all test soundscapes.\n\n    Finds all audio files in the test soundscape directory, optionally limits\n    the number of files in debug mode, and calls predict_on_spectrogram for\n    each audio file to get predictions for all segments. Aggregates the\n    results from all files.\n\n    Args:\n        cfg: Configuration object containing inference parameters\n             like test_soundscapes, debug, debug_count.\n        models: A list of loaded BirdCLEFModel instances for inference.\n        species_ids: A list of species IDs used for predictions.\n\n    Returns:\n        tuple: A tuple containing two elements:\n            - list: A list of all row_ids from all processed segments \n                    across all test files.\n            - list: A list of NumPy arrays, where each array contains the \n                    predicted probabilities for all species for a segment. \n                    This list contains results from all segments of all files.\n    \"\"\"\n\n    # Find all audio files in the test soundscapes directory\n    test_files = list(Path(cfg.test_soundscapes).glob('*.ogg'))\n\n    # Apply debug mode if enabled\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    # Process each test audio file using tqdm for a progress bar\n    for audio_path in tqdm(test_files):\n        # Get predictions for all segments of the current audio file\n        row_ids, predictions = predict_on_spectrogram(str(audio_path), models, cfg, species_ids)\n\n        # Extend the master lists with results from the current file\n        all_row_ids.extend(row_ids)\n        all_predictions.extend(predictions)\n\n    # Return the aggregated row IDs and predictions\n    return all_row_ids, all_predictions","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-19T22:01:24.923455Z","iopub.execute_input":"2025-05-19T22:01:24.923696Z","iopub.status.idle":"2025-05-19T22:01:24.929340Z","shell.execute_reply.started":"2025-05-19T22:01:24.923682Z","shell.execute_reply":"2025-05-19T22:01:24.928543Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def create_submission(row_ids: list[int], predictions: list[np.ndarray], species_ids: list[int], cfg: CFG) -> pd.DataFrame:\n    \"\"\"\n    Create submission dataframe in the required format.\n\n    Constructs a pandas DataFrame from the row IDs and predictions.\n    It ensures that all species columns present in the sample submission file\n    are included, adding columns with 0.0 values for any missing species.\n    The columns are ordered according to the sample submission.\n\n    Args:\n        row_ids (list): A list of row_ids for each prediction segment.\n        predictions (list): A list of NumPy arrays, where each array contains the\n                             predicted probabilities for all species for a segment.\n        species_ids (list): A list of species IDs corresponding to the order of \n                            probabilities in the predictions arrays.\n        cfg: Configuration object containing the path to the sample submission CSV\n             (submission_csv).\n\n    Returns:\n        pd.DataFrame: A pandas DataFrame formatted for submission, with 'row_id'\n                      as the first column and species columns following.\n    \"\"\"\n\n    print(\"Creating submission dataframe...\")\n\n    # Create a dictionary to build the DataFrame\n    submission_dict = {\"row_id\": row_ids}\n\n    # Add columns for each species with their predicted probabilities\n    # Each 'pred' in predictions is a numpy array of probabilities corresponding to species_ids order\n    for i, species in enumerate(species_ids):\n        # Collect the prediction probability for the i-th species across all segments\n        submission_dict[species] = [pred[i] for pred in predictions]\n\n    # Create the initial DataFrame\n    submission_df = pd.DataFrame(submission_dict)\n\n    # Set \"row_id\" as the index temporarily for alignment with sample submission\n    submission_df.set_index(\"row_id\", inplace=True)\n\n    # Read the sample submission to get the required columns and order\n    sample_sub = pd.read_csv(cfg.submission_csv, index_col='row_id')\n\n    # Check for any species columns present in the sample submission but missing in our dataframe\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        # Add missing columns with default value 0.0\n        for col in missing_cols:\n            submission_df[col] = 0.0\n\n    # Reindex the submission dataframe to match the column order of the sample submission\n    submission_df = submission_df[sample_sub.columns]\n\n    # Reset the index to make 'row_id' a regular column again\n    submission_df = submission_df.reset_index()\n    \n    return submission_df","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-19T22:03:30.257598Z","iopub.execute_input":"2025-05-19T22:03:30.257863Z","iopub.status.idle":"2025-05-19T22:03:30.264274Z","shell.execute_reply.started":"2025-05-19T22:03:30.257848Z","shell.execute_reply":"2025-05-19T22:03:30.263284Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Run","metadata":{}},{"cell_type":"code","source":"print(\"Starting BirdCLEF-2025 inference...\")\nprint(f\"TTA enabled: {cfg.use_tta} (variations: {cfg.tta_count if cfg.use_tta else 0})\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-19T22:03:36.521487Z","iopub.execute_input":"2025-05-19T22:03:36.521729Z","iopub.status.idle":"2025-05-19T22:03:36.526245Z","shell.execute_reply.started":"2025-05-19T22:03:36.521714Z","shell.execute_reply":"2025-05-19T22:03:36.525491Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"models = load_models(cfg, num_classes)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-19T22:03:36.699387Z","iopub.execute_input":"2025-05-19T22:03:36.700131Z","iopub.status.idle":"2025-05-19T22:03:40.147106Z","shell.execute_reply.started":"2025-05-19T22:03:36.700113Z","shell.execute_reply":"2025-05-19T22:03:40.146361Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"if not models:\n    raise Exception(\"No models found! Please check model paths.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-19T22:03:40.148290Z","iopub.execute_input":"2025-05-19T22:03:40.148487Z","iopub.status.idle":"2025-05-19T22:03:40.151705Z","shell.execute_reply.started":"2025-05-19T22:03:40.148473Z","shell.execute_reply":"2025-05-19T22:03:40.151187Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(f\"Model usage: {'Single model' if len(models) == 1 else f'Ensemble of {len(models)} models'}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-19T22:03:40.152447Z","iopub.execute_input":"2025-05-19T22:03:40.152690Z","iopub.status.idle":"2025-05-19T22:03:40.169041Z","shell.execute_reply.started":"2025-05-19T22:03:40.152670Z","shell.execute_reply":"2025-05-19T22:03:40.168314Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"list(Path(cfg.test_soundscapes).glob(\"*\"))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-19T22:03:40.170700Z","iopub.execute_input":"2025-05-19T22:03:40.171157Z","iopub.status.idle":"2025-05-19T22:03:40.189658Z","shell.execute_reply.started":"2025-05-19T22:03:40.171133Z","shell.execute_reply":"2025-05-19T22:03:40.188869Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"row_ids, predictions = run_inference(cfg, models, species_ids)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-19T22:03:40.190473Z","iopub.execute_input":"2025-05-19T22:03:40.190681Z","iopub.status.idle":"2025-05-19T22:03:40.207721Z","shell.execute_reply.started":"2025-05-19T22:03:40.190666Z","shell.execute_reply":"2025-05-19T22:03:40.206527Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"submission_df = create_submission(row_ids, predictions, species_ids, cfg)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-19T22:03:40.208995Z","iopub.execute_input":"2025-05-19T22:03:40.209281Z","iopub.status.idle":"2025-05-19T22:03:40.244883Z","shell.execute_reply.started":"2025-05-19T22:03:40.209256Z","shell.execute_reply":"2025-05-19T22:03:40.244012Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Test data is private except when submitted, so the DataFrame is empty.\nsubmission_df.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-19T22:03:40.245739Z","iopub.execute_input":"2025-05-19T22:03:40.245963Z","iopub.status.idle":"2025-05-19T22:03:40.268022Z","shell.execute_reply.started":"2025-05-19T22:03:40.245948Z","shell.execute_reply":"2025-05-19T22:03:40.267208Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"submission_path = \"submission.csv\"\nsubmission_df.to_csv(submission_path, index=False)\nprint(f\"Submission saved to {submission_path}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-19T22:03:40.269810Z","iopub.execute_input":"2025-05-19T22:03:40.270031Z","iopub.status.idle":"2025-05-19T22:03:40.281067Z","shell.execute_reply.started":"2025-05-19T22:03:40.270017Z","shell.execute_reply":"2025-05-19T22:03:40.280076Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}