{"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":"nvidiaTeslaT4","dataSources":[{"sourceId":91844,"databundleVersionId":11361821,"sourceType":"competition"},{"sourceId":11060723,"sourceType":"datasetVersion","datasetId":6891568}],"dockerImageVersionId":30918,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import re\nimport os\nimport gc\nimport timm\nimport time\nimport torch\nimport wandb\nimport folium\nimport librosa\nimport torchaudio\nimport librosa.display\nimport concurrent.futures\n\nimport numpy as np\nimport pandas as pd\nfrom glob import glob\nfrom pathlib import Path\nfrom tqdm import tqdm\n\nimport matplotlib.pyplot as plt\nimport seaborn as sns\n\nimport torch.nn as nn\nimport plotly.express as px\nfrom torchvision import models\nfrom IPython.display import Audio\nfrom shapely.geometry import Point\nimport torchaudio.transforms as AT\nfrom folium.plugins import FastMarkerCluster\n\nimport warnings\nwarnings.filterwarnings('ignore')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-05T16:46:13.172975Z","iopub.execute_input":"2025-04-05T16:46:13.173292Z","iopub.status.idle":"2025-04-05T16:46:13.178869Z","shell.execute_reply.started":"2025-04-05T16:46:13.173265Z","shell.execute_reply":"2025-04-05T16:46:13.177936Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Feature Extraction","metadata":{"execution":{"iopub.status.busy":"2025-03-21T15:09:13.388769Z","iopub.status.idle":"2025-03-21T15:09:13.389097Z","shell.execute_reply":"2025-03-21T15:09:13.388939Z"}}},{"cell_type":"markdown","source":"**Note**\n\n- `tiny_noise:` we add tiny noise in our waveform to reduce overfitting by forcing it to learning general patterns instead of memorizing specific waveforms\n- `waveform in 5 sec chunks:` Instead of analyzing everything at once, we split each waveform in 5sec chunks which is useful in task like sound classification of different species, where shorter audio snippets are more informative.\n- `unsqueeze(melspec, dim=0):` [ num_mel_bands x num_time_frames ] **→** [ 1 x num_mel_bands x num_time_frames ]\n  - Many machine learning models expect inputs to have a batch dimension.\n  - If this spectrogram is fed into a convolutional neural network (CNN), it needs a channel dimension.\nThink of it like an image:\n\n    - Grayscale Image → [1 x Height x Width]\n    - Spectrogram → [1 x Mel Bands x Time Frames]\n\n  - The 1 acts like a single-channel image (just like a black-and-white photo).\n- `torch.vstack(PREDS):` It organizes the data in a batch-like format for deep learning models.The model can process multiple segments simultaneously.Helps in sequence-based tasks like bird call detection over time.\n","metadata":{}},{"cell_type":"markdown","source":"### Process that USED\n\n1. Loads an audio file and converts it to mono.\n2. Normalizes the audio and adds a small amount of noise.\n3. Splits the audio into 5-second chunks.\n4. Converts each chunk into a Mel spectrogram.\n5. Applies logarithm and normalization for better model training.\n6. Stacks all spectrograms into a single output tensor.","metadata":{}},{"cell_type":"code","source":"class CFG:\n    def __init__(self):\n        self.test_soundscapes = '/kaggle/input/birdclef-2025/test_soundscapes'\n        self.submission_csv = '/kaggle/input/birdclef-2025/sample_submission.csv'\n        self.taxonomy_csv = '/kaggle/input/birdclef-2025/taxonomy.csv'\n        self.model_path = '/kaggle/input/birdclef25-effnetb0-starter-weight'  \n\n        # Audio Parameters\n        self.SR = 32000\n        self.WINDOW_SIZE = 5\n        self.N_FFT = 1000\n        self.HOP_LENGTH = 512\n        self.N_MELS = 144\n        self.FMIN = 50\n        self.FMAX = 14000\n        self.TARGET_SHAPE = (256, 256)\n\n        # Model Parameters\n        self.model_name = 'efficientnet_b0'\n        self.in_channels = 1\n        self.device = 'cpu'\n\n        # Inference parameters\n        self.batch_size = 16\n        self.use_tta = False\n        self.tta_count = 3\n        self.threshold = 0.5\n\n        self.use_specific_folds = False\n        self.folds = [0, 1]\n\n        # Debug\n        self.debug = False\n        self.debug_count = 3\n\ncfg = CFG()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-05T16:46:28.763056Z","iopub.execute_input":"2025-04-05T16:46:28.763302Z","iopub.status.idle":"2025-04-05T16:46:28.773527Z","shell.execute_reply.started":"2025-04-05T16:46:28.763282Z","shell.execute_reply":"2025-04-05T16:46:28.772562Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"cfg.model_path","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-05T16:46:28.774541Z","iopub.execute_input":"2025-04-05T16:46:28.774776Z","iopub.status.idle":"2025-04-05T16:46:28.785363Z","shell.execute_reply.started":"2025-04-05T16:46:28.774743Z","shell.execute_reply":"2025-04-05T16:46:28.784715Z"}},"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-04-05T16:46:28.786273Z","iopub.execute_input":"2025-04-05T16:46:28.786563Z","iopub.status.idle":"2025-04-05T16:46:28.799689Z","shell.execute_reply.started":"2025-04-05T16:46:28.786536Z","shell.execute_reply":"2025-04-05T16:46:28.798930Z"}},"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, # No need for pretrained Weights\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\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        self.feat_dim= backbone_out\n        self.classifier = nn.Linear(backbone_out, 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","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-05T16:46:28.800542Z","iopub.execute_input":"2025-04-05T16:46:28.800817Z","iopub.status.idle":"2025-04-05T16:46:28.807513Z","shell.execute_reply.started":"2025-04-05T16:46:28.800789Z","shell.execute_reply":"2025-04-05T16:46:28.806783Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def audio2melspec(audio_data, cfg):\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.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    )\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    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.SR * cfg.WINDOW_SIZE:\n        # PADDING \n        audio_data = np.pad(audio_data, (0, cfg.SR*cfg.WINDOW_SIZE-len(audio_data)), mode='constant')\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    return mel_spec.astype(np.float32)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-05T16:46:28.808284Z","iopub.execute_input":"2025-04-05T16:46:28.808517Z","iopub.status.idle":"2025-04-05T16:46:28.824567Z","shell.execute_reply.started":"2025-04-05T16:46:28.808499Z","shell.execute_reply":"2025-04-05T16:46:28.823795Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"The function `librosa.load()` is used to load an audio file into a NumPy array, making it easy to process audio signals for machine learning, deep learning, or analysis.","metadata":{}},{"cell_type":"code","source":"def apply_tta(spec, tta_idx):\n    '''Apply test-time augmentation'''\n    if tta_idx == 0:\n        return spec\n    elif tta_idx == 1:\n        return np.flip(spec, axis=1) # horizontal flip => time shift \n    elif tta_idx == 2:\n        return np.flip(spec, axis=0)  # vertical flip => frequency shift\n    else:\n        return spec","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-05T16:46:28.825271Z","iopub.execute_input":"2025-04-05T16:46:28.825575Z","iopub.status.idle":"2025-04-05T16:46:28.838587Z","shell.execute_reply.started":"2025-04-05T16:46:28.825547Z","shell.execute_reply":"2025-04-05T16:46:28.837989Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def find_model_files(cfg):\n    '''Find all .pth model files in the specified model directory'''\n    model_files = []\n\n    # convert the model directory to a path object\n    model_dir = Path(cfg.model_path)\n\n    # find all .pth files\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    '''Load all found model files and prepare them for ensemble'''\n    models = []\n\n    # find model files\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    # Load each model files \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            model = BirdCLEFModel(cfg, num_classes)\n            model.load_state_dict(checkpoint['model_state_dict'])\n            model = model.to(cfg.device)\n            model.eval()\n            models.append(model)\n        except Exception as e:\n            print(f'Error Loading model {model_path}: {e}')\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 = []\n    row_ids = []\n    soundscape_id = Path(audio_path).stem\n    \n    try:\n        print(f'Processing {soundscape_id}')\n        audio_data, _ = libros.load(audio_path, sr=cfg.SR)\n\n        total_segments = len(audio_data)//(cfg.SR*cfg.WINDOW_SIZE)\n\n        for segment_idx in range(total_segments):\n            # Extract current 5-second segment\n            st_sample = segment_idx * cfg.SR + cfg.WINDOW_SIZE\n            end_sample= st_sample + cfg.SR + cfg.WINDOW_SIZE\n            segment_audio = audio_data[st_sample:end_sample]\n\n            # Calculate end time in seconds for row_id\n            end_time_sec = (segment_idx+1)*cfg.WINDOW_SIZE\n\n            row_id = f'{soundscape_id}_{end_time_sec}'\n            row_ids.append(row_id)\n\n            # Process the audio segment and get mel spectrogram\n            if cfg.use_tta:\n                # Use test-time augmentation\n                all_preds = []\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                    # convert to tensor and 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                    # Handle single model case without ensemble \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:\n                        # Get prediction from each models for 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                        # Average predictions from all models\n                        avg_preds = np.mean(segment_preds, axis=0)\n                        all_preds.append(avg_preds)\n\n                    # Average TTA predictions\n                    final_preds = np.mean(all_preds, axis=0)\n\n            else:\n                # No TTA -> just use original spectrogram\n                mel_spec = process_audio_segment(segment_audio, cfg)\n                mel_spec = torch.tensor(mel_spec, dtype=torch.float32).unsqueeze(0).unsqueeze(0)\n                mel_spec = mel_spec.to(cfg.device)\n\n                # Handle single model case without 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\n                else:\n                    # Get predictions from each model for ensemble\n                    segment_preds = []\n                    for model in models:\n                        with torch.no_grad():\n                            outputs = model(mel_spec)\n                            prob = 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(f'Error processing {audio_path}: {e}')\n\n    return row_ids, predictions","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-05T16:46:28.839156Z","iopub.execute_input":"2025-04-05T16:46:28.839421Z","iopub.status.idle":"2025-04-05T16:46:28.855783Z","shell.execute_reply.started":"2025-04-05T16:46:28.839381Z","shell.execute_reply":"2025-04-05T16:46:28.855030Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def run_inference(cfg, models, species_ids):\n    '''Run inference on all test soundscapes'''\n    # Get list of 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    # Initialize lists for predictions\n    all_row_ids = []\n    all_predictions = []\n\n    # Process each soundscape\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","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-05T16:46:28.856516Z","iopub.execute_input":"2025-04-05T16:46:28.856757Z","iopub.status.idle":"2025-04-05T16:46:28.870944Z","shell.execute_reply.started":"2025-04-05T16:46:28.856727Z","shell.execute_reply":"2025-04-05T16:46:28.869885Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def 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    # Add predicitions for each species\n    for i, species in enumerate(species_ids):\n        submission_dict[species] = [pred[i] for pred in predictions]\n\n    # Create dataframe\n    submission_df = pd.DataFrame(submission_dict)\n    # Set row_id as index\n    submission_df.set_index('row_id', inplace=True)\n    # Verify the submission format against sample submission\n    sample_sub = pd.read_csv(cfg.submission_csv, index_col='row_id')\n\n    # Check if all species columns are present\n    missing_cols = set(sample_sub.columns)-set(submission_df.columns)\n\n    if missing_cols:\n        print(f'Warning: Missing {len(missing_cols)} species columns in submission')\n\n        # Add missing columns with zeros\n        for col in missing_cols:\n            submission_df[col]=0.0\n\n     # Ensure columns are in the same order as sample submission\n    submission_df = submission_df[sample_sub.columns]\n    \n    # Reset the index to include row_id as a column\n    submission_df = submission_df.reset_index()\n    \n    return submission_df","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-05T16:46:28.872059Z","iopub.execute_input":"2025-04-05T16:46:28.872385Z","iopub.status.idle":"2025-04-05T16:46:28.882188Z","shell.execute_reply.started":"2025-04-05T16:46:28.872331Z","shell.execute_reply":"2025-04-05T16:46:28.881285Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def main():\n    start_time = time.time()\n    print(f'Starting BirdCLEF-2025 inference...')\n    print(f'TTA Enabled: {cfg.use_tta} (variations: {cfg.tta_count if cfg.use_tta else 0})')\n\n    # Load models \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    # Run inference on test soundscapes\n    row_ids, predictions = run_inference(cfg, models, species_ids)\n\n     # Create submission dataframe\n    submission_df = create_submission(row_ids, predictions, species_ids, cfg)\n    \n    # Save submission file\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-04-05T16:46:28.883308Z","iopub.execute_input":"2025-04-05T16:46:28.883664Z","iopub.status.idle":"2025-04-05T16:46:28.900865Z","shell.execute_reply.started":"2025-04-05T16:46:28.883636Z","shell.execute_reply":"2025-04-05T16:46:28.899810Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"if __name__ == \"__main__\":\n    main()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-05T16:46:28.901774Z","iopub.execute_input":"2025-04-05T16:46:28.902049Z","iopub.status.idle":"2025-04-05T16:46:29.112898Z","shell.execute_reply.started":"2025-04-05T16:46:28.902022Z","shell.execute_reply":"2025-04-05T16:46:29.112011Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}