{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.13","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":676526,"sourceType":"modelInstanceVersion","modelInstanceId":512947,"modelId":527586}],"dockerImageVersionId":31192,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import os\nimport gc\nimport warnings\nimport logging\nimport time\nimport math\nimport cv2\nfrom pathlib import Path\nimport numpy as np\nimport pandas as pd\nimport librosa\nimport soundfile as sf\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nimport timm\nfrom tqdm.auto import tqdm\nfrom glob import glob\nimport random\nimport itertools\nfrom typing import Union\nimport concurrent.futures\n\nwarnings.filterwarnings(\"ignore\")\nlogging.basicConfig(level=logging.ERROR)\n\n\n# Configuration\nclass CFG:\n    seed = 42\n    num_workers = 2\n    \n    # Path settings\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    submission_csv = '/kaggle/input/birdclef-2025/sample_submission.csv'\n    taxonomy_csv = '/kaggle/input/birdclef-2025/taxonomy.csv'\n    \n    # [Key] Modify this to your uploaded model path\n    # Assuming you uploaded best_model.pth to a dataset named birdclef-my-model\n    model_files = [\n        '/kaggle/input/sed-baseline/pytorch/default/1/best_model.pth' \n    ]\n \n    # Model parameters (Must match training)\n    model_name = 'efficientnet_b0'  \n    pretrained = False\n    in_channels = 1\n    \n    # Audio parameters (Must match training)\n    SR = 32000\n    target_duration = 5 # 5-second slice\n    \n    # MelSpectrogram parameters\n    n_fft = 1024\n    hop_length = 512\n    n_mels = 128\n    f_min = 50\n    f_max = 14000\n    target_shape = (256, 256)\n    \n    device = 'cuda' if torch.cuda.is_available() else 'cpu'\n\ncfg = CFG()\n\nprint(f\"Using device: {cfg.device}\")\nprint(f\"Loading taxonomy data...\")\n# If taxonomy is missing locally (e.g., during testing with only sample_submission), provide compatibility\nif os.path.exists(cfg.taxonomy_csv):\n    taxonomy_df = pd.read_csv(cfg.taxonomy_csv)\n    species_ids = taxonomy_df['primary_label'].tolist()\nelse:\n    # Fallback: read from sample_submission\n    ss = pd.read_csv(cfg.submission_csv)\n    species_ids = [c for c in ss.columns if c != 'row_id']\n\nnum_classes = len(species_ids)\nprint(f\"Number of classes: {num_classes}\")\n\n\n# Utilities\ndef set_seed(seed=42):\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.backends.cudnn.deterministic = True\n    torch.backends.cudnn.benchmark = False\n\nset_seed(cfg.seed)\n\n\n# Model Definition (Matching your training)\nclass BirdCLEFModel(nn.Module):\n    def __init__(self, cfg):\n        super().__init__()\n        self.cfg = cfg\n        \n        # Backbone network\n        self.backbone = timm.create_model(\n            cfg.model_name,\n            pretrained=False, # Cannot access internet during inference, set to False\n            in_chans=cfg.in_channels\n        )\n\n        # Replace classification head\n        if 'efficientnet' in cfg.model_name:\n            backbone_out = self.backbone.classifier.in_features\n            self.backbone.classifier = nn.Identity()\n        else:\n            backbone_out = self.backbone.num_features\n            self.backbone.reset_classifier(0, '')\n            \n        self.pooling = nn.AdaptiveAvgPool2d(1)\n        \n        # num_classes needs to be obtained from global variables or cfg\n        global num_classes\n        self.classifier = nn.Linear(backbone_out, num_classes)\n\n    def forward(self, x):\n        # x: (batch, 1, freq, time)\n        x = self.backbone(x)\n        \n        # Handle potential dict output from timm\n        if isinstance(x, dict):\n            x = x['features']\n            \n        # Global Average Pooling\n        if len(x.shape) == 4:\n            x = self.pooling(x)\n            x = x.view(x.size(0), -1)\n            \n        logits = self.classifier(x)\n        return logits\n\n\n# Feature Extraction (Librosa)\nclass LogMelFeatureExtractor:\n    def __init__(self, cfg):\n        self.cfg = cfg\n    \n    def __call__(self, audio_data):\n        # audio_data: numpy array (samples,)\n        mel_spec = librosa.feature.melspectrogram(\n            y=audio_data, \n            sr=self.cfg.SR, \n            n_fft=self.cfg.n_fft, \n            hop_length=self.cfg.hop_length, \n            n_mels=self.cfg.n_mels, \n            fmin=self.cfg.f_min, \n            fmax=self.cfg.f_max, \n            power=2.0\n        )\n        # Log Scale\n        mel_spec_db = librosa.power_to_db(mel_spec, ref=np.max)\n        \n        # Min-Max Normalization to [0, 1]\n        mel_spec_norm = (mel_spec_db - mel_spec_db.min()) / (mel_spec_db.max() - mel_spec_db.min() + 1e-8)\n        \n        # Resize to fixed shape (256, 256)\n        if mel_spec_norm.shape != self.cfg.target_shape:\n            mel_spec_norm = cv2.resize(mel_spec_norm, self.cfg.target_shape, interpolation=cv2.INTER_LINEAR)\n            \n        return mel_spec_norm.astype(np.float32)\n\n\n# Audio Loading & Slicing\ndef load_and_slice_audio(path, cfg):\n    \"\"\"\n    Load audio and slice into 5-second segments\n    \"\"\"\n    try:\n        audio, _ = librosa.load(path, sr=cfg.SR)\n    except Exception as e:\n        print(f\"Error loading {path}: {e}\")\n        return [], []\n\n    # Calculate required number of chunks\n    chunk_len = int(cfg.target_duration * cfg.SR)\n    # math.ceil for rounding up\n    num_chunks = math.ceil(len(audio) / chunk_len)\n    \n    # Pad length\n    target_len = num_chunks * chunk_len\n    if len(audio) < target_len:\n        audio = np.pad(audio, (0, target_len - len(audio)))\n        \n    segments = []\n    end_seconds = []\n    \n    for i in range(num_chunks):\n        seg = audio[i*chunk_len : (i+1)*chunk_len]\n        segments.append(seg)\n        end_seconds.append((i+1) * cfg.target_duration)\n        \n    return segments, end_seconds\n\n\n# Model Loading\ndef load_models(cfg):\n    models = []\n    model_files = cfg.model_files\n    \n    if not model_files:\n        print(f\"Warning: No model files found!\")\n        return models\n    \n    print(f\"Found a total of {len(model_files)} model files.\")\n    \n    for model_path in model_files:\n        if not os.path.exists(model_path):\n            print(f\"Path does not exist: {model_path}\")\n            continue\n            \n        try:\n            print(f\"Loading model: {model_path}\")\n            # [Key] weights_only=False to avoid CFG class errors\n            checkpoint = torch.load(model_path, map_location=torch.device(cfg.device), weights_only=False)\n            \n            # Initialize model\n            model = BirdCLEFModel(cfg)\n            \n            # Load weights\n            if 'model_state_dict' in checkpoint:\n                state_dict = checkpoint['model_state_dict']\n            else:\n                state_dict = checkpoint\n            \n            model.load_state_dict(state_dict)\n            model = model.to(cfg.device)\n            model.eval()\n            \n            # Half-precision acceleration (Optional, if GPU supports)\n            # model.half() \n            \n            models.append(model)\n        except Exception as e:\n            print(f\"Error loading model {model_path}: {e}\")\n            import traceback\n            traceback.print_exc()\n    \n    return models\n\n\n# Inference Function (Per File)\ndef predict_on_file(audio_path, models, cfg, feature_extractor):\n    \"\"\"\n    Process a single audio file\n    \"\"\"\n    audio_path = str(audio_path)\n    row_ids = []\n    predictions = []\n    soundscape_id = Path(audio_path).stem.split('.')[0] # Get filename part only\n\n    # 1. Load and slice audio\n    segments, seconds = load_and_slice_audio(audio_path, cfg)\n    \n    if len(segments) == 0:\n        return [], []\n\n    # 2. Pre-processing (Feature Extraction)\n    # Batch processing for speedup (but serial might be safer on CPU)\n    batch_imgs = []\n    for seg in segments:\n        spec = feature_extractor(seg) # (256, 256)\n        batch_imgs.append(spec)\n    \n    # Convert to Tensor: (Batch, 1, 256, 256)\n    batch_tensor = torch.tensor(np.array(batch_imgs), dtype=torch.float32).unsqueeze(1).to(cfg.device)\n    \n    # 3. Inference\n    file_preds = []\n    \n    # models is a list here (even if only one model) to facilitate future Ensemble\n    for model in models:\n        with torch.no_grad():\n            # (Batch, Num_Classes)\n            logits = model(batch_tensor)\n            probs = torch.sigmoid(logits).cpu().numpy()\n            file_preds.append(probs)\n    \n    # If multiple models, take the average\n    final_preds = np.mean(file_preds, axis=0) # (Batch, Num_Classes)\n    \n    # 4. Generate Row IDs\n    for i, sec in enumerate(seconds):\n        row_id = f\"{soundscape_id}_{int(sec)}\"\n        row_ids.append(row_id)\n        predictions.append(final_preds[i])\n        \n    return row_ids, predictions\n\n# Main Inference Loop\ndef run_inference(cfg, models):\n    # 1. Find test files\n    test_files = sorted(list(Path(cfg.test_soundscapes).glob('*.ogg')))\n    \n    # If no files in Commit stage, use dummy files or return directly\n    if len(test_files) == 0:\n        print(\"No test files found (Commit Stage). Using dummy files if available or skipping.\")\n        # To prevent empty errors, usually generate dummy submission\n        return [], []\n    \n    print(f\"Found {len(test_files)} test soundscapes\")\n\n    all_row_ids = []\n    all_predictions = []\n    \n    feature_extractor = LogMelFeatureExtractor(cfg)\n\n    # 2. Parallel processing\n    # Adjust max_workers based on CPU cores; audio processing is IO intensive + CPU intensive\n    with concurrent.futures.ThreadPoolExecutor(max_workers=cfg.num_workers) as executor:\n        results = list(\n            tqdm(\n                executor.map(\n                    predict_on_file,\n                    test_files,\n                    itertools.repeat(models),\n                    itertools.repeat(cfg),\n                    itertools.repeat(feature_extractor)\n                ),\n                total=len(test_files),\n                desc=\"Inferencing\"\n            )\n        )\n\n    # 3. Aggregate results\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\n# Submission & Smoothing\ndef create_submission(row_ids, predictions, species_ids, cfg):\n    print(\"Creating submission dataframe...\")\n    \n    if len(row_ids) == 0:\n        # Dummy submission creation\n        print(\"Generating dummy submission structure.\")\n        sample_sub = pd.read_csv(cfg.submission_csv)\n        sample_sub.to_csv(\"submission.csv\", index=False)\n        return\n\n    # Build DataFrame\n    # predictions is a list of arrays\n    preds_np = np.array(predictions)\n    \n    submission_df = pd.DataFrame(preds_np, columns=species_ids)\n    submission_df.insert(0, 'row_id', row_ids)\n\n    # Align column names (just in case)\n    if os.path.exists(cfg.submission_csv):\n        sample_sub = pd.read_csv(cfg.submission_csv)\n        # Fill missing columns\n        missing_cols = set(sample_sub.columns) - set(submission_df.columns)\n        for col in missing_cols:\n            submission_df[col] = 0.0\n        # Sort\n        submission_df = submission_df[sample_sub.columns]\n    \n    return submission_df\n\ndef smooth_submission(submission_path):\n    \"\"\"\n    Post-processing smoothing: If there are bird calls in the previous and next second, \n    the middle one likely has them too.\n    (0.8 * curr) + (0.2 * neighbor)\n    \"\"\"\n    print(\"Smoothing submission predictions...\")\n    if not os.path.exists(submission_path): return\n\n    sub = pd.read_csv(submission_path)\n    if len(sub) == 0: return\n\n    cols = sub.columns[1:]\n    # Extract soundscape filename as group key\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        idx = np.where(groups == group)[0]\n        # If file has only one slice, cannot smooth\n        if len(idx) <= 1: continue\n\n        sub_group = sub.iloc[idx].copy()\n        predictions = sub_group[cols].values.astype(float)\n        new_predictions = predictions.copy()\n        \n        # Special handling for start and end\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        \n        # Middle part processing\n        # curr * 0.6 + prev * 0.2 + next * 0.2\n        for i in range(1, predictions.shape[0]-1):\n            new_predictions[i] = (predictions[i-1] * 0.2) + (predictions[i] * 0.6) + (predictions[i+1] * 0.2)\n        \n        # Write back\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\n\n# Main Execution\ndef main():\n    start_time = time.time()\n    print(\"Starting BirdCLEF-2025 inference...\")\n\n    # 1. Load models\n    models = load_models(cfg)\n    \n    if not models:\n        print(\"No models found! Please check model paths.\")\n        # Generate an empty one to prevent errors\n        create_submission([], [], species_ids, cfg)\n        return\n    \n    print(f\"Model usage: {'Single model' if len(models) == 1 else f'Ensemble of {len(models)} models'}\")\n\n    # 2. Run inference\n    row_ids, predictions = run_inference(cfg, models)\n\n    # 3. Generate CSV\n    submission_df = create_submission(row_ids, predictions, species_ids, cfg)\n    \n    if submission_df is not None:\n        submission_path = 'submission.csv'\n        submission_df.to_csv(submission_path, index=False)\n        print(f\"Submission saved to {submission_path}\")\n\n        # 4. Smooth results\n        smooth_submission(submission_path)\n    \n    end_time = time.time()\n    print(f\"Inference completed in {(end_time - start_time)/60:.2f} minutes\")\n    \n    # Print first few lines to check\n    if os.path.exists(\"submission.csv\"):\n        print(pd.read_csv(\"submission.csv\").head())\n\nif __name__ == \"__main__\":\n    main()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-12T08:07:10.973355Z","iopub.execute_input":"2025-12-12T08:07:10.973677Z","iopub.status.idle":"2025-12-12T08:07:32.241839Z","shell.execute_reply.started":"2025-12-12T08:07:10.973651Z","shell.execute_reply":"2025-12-12T08:07:32.240451Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"pd.read_csv(\"submission.csv\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-12T08:07:32.243983Z","iopub.execute_input":"2025-12-12T08:07:32.244398Z","iopub.status.idle":"2025-12-12T08:07:32.290967Z","shell.execute_reply.started":"2025-12-12T08:07:32.244367Z","shell.execute_reply":"2025-12-12T08:07:32.289021Z"}},"outputs":[],"execution_count":null}]}