{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"pygments_lexer":"ipython3","nbconvert_exporter":"python","version":"3.6.4","file_extension":".py","codemirror_mode":{"name":"ipython","version":3},"name":"python","mimetype":"text/x-python"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceId":91844,"databundleVersionId":11361821,"isSourceIdPinned":false,"sourceType":"competition"},{"sourceId":11712113,"sourceType":"datasetVersion","datasetId":7351744}],"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import os\nimport gc\nimport warnings\nimport logging\nimport time\nimport math\nimport cv2\nimport random\nfrom pathlib import Path\n\nimport numpy as np\nimport pandas as pd\nimport librosa\nimport torch\nimport torchvision.transforms as transforms\nimport torch.nn as nn\nimport torch.nn.functional as F\nimport timm\nfrom tqdm.auto import tqdm\n\nwarnings.filterwarnings(\"ignore\")\nlogging.basicConfig(level=logging.ERROR)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ## Configuration\n\n# %%\nclass CFG:\n    # --- Paths ---\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    model_path = '/kaggle/input/my-birdclef25-trained-models-v1' \n\n\n    # --- Model --- \n    model_name = 'efficientnet_b0'\n    in_channels = 3\n\n    # --- Audio / Mel / Resize --- \n    FS = 32000\n    WINDOW_SIZE = 5\n    N_FFT = 2048\n    HOP_LENGTH = 512\n    WIN_LENGTH = 2048\n    N_MELS = 128\n    FMIN = 20\n    FMAX = 16000\n    TARGET_SHAPE = (256, 256)\n\n    # --- Inference ---\n    device = 'cpu'\n    batch_size = 12 \n    use_tta = False\n    tta_count = 3\n\n    # --- Model Selection ---\n    use_specific_folds = False\n\n    # --- Debug ---\n    debug = True  \n    debug_limit = 1 \n\ncfg = CFG()\nif cfg.debug:\n    print(\"!!!!!!!!!!!!!! DEBUG MODE IS ON !!!!!!!!!!!!!!\")\n    cfg.batch_size = 1","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ## Setup\n\n# %%\nprint(f\"--- Inference Configuration ---\")\nprint(f\"Device: {cfg.device}\")\nprint(f\"Model Path: {cfg.model_path}\")\nprint(f\"Using TTA: {cfg.use_tta} ({cfg.tta_count if cfg.use_tta else 0} variations)\")\nprint(f\"Using specific folds: {cfg.use_specific_folds} ({cfg.folds if cfg.use_specific_folds and hasattr(cfg, 'folds') else 'All found'})\")\nprint(f\"Mel Params: FS={cfg.FS}, N_FFT={cfg.N_FFT}, HOP={cfg.HOP_LENGTH}, N_MELS={cfg.N_MELS}\")\nprint(f\"Target Shape: {cfg.TARGET_SHAPE}\")\nprint(f\"Batch Size (for inference): {cfg.batch_size}\")\nprint(f\"-----------------------------\")\n\nprint(f\"Loading taxonomy data...\")\ntaxonomy_df = pd.read_csv(cfg.taxonomy_csv)\ncfg.species_ids = taxonomy_df['primary_label'].tolist()\ncfg.num_classes = len(cfg.species_ids)\nprint(f\"Number of classes: {cfg.num_classes}\")\n\nnormalize = transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ## Model Definition \n\n# %%\nclass BirdCLEFModel(nn.Module):\n    def __init__(self, cfg, num_classes):\n        super().__init__()\n        self.cfg = cfg\n        self.backbone = timm.create_model(\n            cfg.model_name, pretrained=False, in_chans=cfg.in_channels,\n            drop_rate=0.0, drop_path_rate=0.0\n        )\n        if hasattr(self.backbone, 'get_classifier'):\n             backbone_out = self.backbone.get_classifier().in_features\n        elif hasattr(self.backbone, 'head') and hasattr(self.backbone.head, 'in_features'):\n             backbone_out = self.backbone.head.in_features\n        elif hasattr(self.backbone, 'fc') and hasattr(self.backbone.fc, 'in_features'):\n            backbone_out = self.backbone.fc.in_features\n        elif hasattr(self.backbone, 'classifier') and hasattr(self.backbone.classifier, 'in_features'):\n            backbone_out = self.backbone.classifier.in_features\n        else:\n            try: backbone_out = self.backbone.num_features\n            except AttributeError: raise ValueError(f\"Cannot determine out feats for {cfg.model_name}\")\n        if hasattr(self.backbone, 'reset_classifier'): self.backbone.reset_classifier(0, '')\n        elif hasattr(self.backbone, 'head'): self.backbone.head = nn.Identity()\n        elif hasattr(self.backbone, 'fc'): self.backbone.fc = nn.Identity()\n        elif hasattr(self.backbone, 'classifier'): self.backbone.classifier = nn.Identity()\n        self.pooling = nn.AdaptiveAvgPool2d(1)\n        self.dropout = nn.Dropout(p=0.3) # p không quan trọng khi eval\n        self.classifier = nn.Linear(backbone_out, num_classes)\n    def forward(self, x):\n        features = self.backbone(x)\n        if isinstance(features, dict):\n             features = features.get('features', features.get('head_output', next(iter(features.values()))))\n        if len(features.shape) == 4:\n            features = self.pooling(features)\n            features = features.view(features.size(0), -1)\n        # Dropout không áp dụng khi model.eval()\n        logits = self.classifier(features)\n        return logits","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ## Audio Processing and Inference Functions\n\n# %%\ndef 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 if not np.isnan(mean_signal) else 0.0)\n    mel_spec = librosa.feature.melspectrogram(\n        y=audio_data, sr=cfg.FS, n_fft=cfg.N_FFT, hop_length=cfg.HOP_LENGTH,\n        win_length=cfg.WIN_LENGTH, n_mels=cfg.N_MELS, fmin=cfg.FMIN, fmax=cfg.FMAX,\n        power=2.0, center=True, pad_mode=\"reflect\"\n    )\n    mel_spec_db = librosa.power_to_db(mel_spec, ref=np.max)\n    if mel_spec_db.max() == mel_spec_db.min():\n        target_time_steps = int(np.floor((cfg.WINDOW_SIZE * cfg.FS) / cfg.HOP_LENGTH) + 1)\n        return np.zeros((cfg.N_MELS, target_time_steps), dtype=np.float32)\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    target_samples = int(cfg.WINDOW_SIZE * cfg.FS)\n    if len(audio_data) < target_samples:\n        audio_data = np.pad(audio_data, (0, target_samples - len(audio_data)), mode='constant')\n    elif len(audio_data) > target_samples:\n        audio_data = audio_data[:target_samples]\n    mel_spec_norm = audio2melspec(audio_data, cfg)\n    if mel_spec_norm.shape != cfg.TARGET_SHAPE:\n         if mel_spec_norm.shape[1] == 0 or mel_spec_norm.shape[0] == 0 :\n              final_spec = np.zeros(cfg.TARGET_SHAPE, dtype=np.float32)\n              # ### DEBUG ###\n              # print(f\"    DEBUG (process_audio_segment): Zero-width/height spec encountered, returning zeros. Original shape: {mel_spec_norm.shape}\")\n         else:\n              final_spec = cv2.resize(mel_spec_norm, (cfg.TARGET_SHAPE[1], cfg.TARGET_SHAPE[0]), interpolation=cv2.INTER_LINEAR)\n    else:\n        final_spec = mel_spec_norm\n    return final_spec.astype(np.float32)\n\ndef apply_tta(spec, tta_idx):\n    if tta_idx == 0: return spec\n    elif tta_idx == 1: return np.flip(spec, axis=1).copy()\n    elif tta_idx == 2: return np.flip(spec, axis=0).copy()\n    else: return spec\n\n# %%\ndef find_model_files(cfg):\n    model_dir = Path(cfg.model_path)\n    all_files = list(model_dir.glob('**/*.pth'))\n    print(f\"DEBUG (find_model_files): Found {len(all_files)} .pth files in {cfg.model_path}: {all_files}\")\n\n    if not all_files: return []\n    if cfg.use_specific_folds and hasattr(cfg, 'folds'):\n        model_files = [f for f in all_files if any(f\"fold{fold}\" in f.name for fold in cfg.folds)]\n        print(f\"DEBUG (find_model_files): Using {len(model_files)} models for folds: {cfg.folds}: {model_files}\")\n    else:\n        model_files = all_files\n        print(f\"DEBUG (find_model_files): Using all {len(model_files)} found models: {model_files}\")\n    return [str(f) for f in model_files]\n\ndef load_models(cfg):\n    models = []\n    model_files = find_model_files(cfg)\n    if not model_files:\n        print(f\"ERROR: No model files found or selected in {cfg.model_path}. Exiting.\")\n        return None\n    for model_path_str in model_files:\n        try:\n            print(f\"Loading model: {model_path_str}...\")\n            checkpoint = torch.load(model_path_str, map_location=torch.device(cfg.device))\n            num_classes_model = cfg.num_classes\n            if 'cfg' in checkpoint and 'num_classes' in checkpoint['cfg']:\n                 loaded_cfg_num_classes = checkpoint['cfg']['num_classes']\n                 if loaded_cfg_num_classes != cfg.num_classes:\n                      print(f\"  WARNING: num_classes in checkpoint ({loaded_cfg_num_classes}) differs from current taxonomy ({cfg.num_classes}). Using current taxonomy value.\")\n                 # Kiểm tra các tham số quan trọng khác từ checkpoint nếu có\n                 if 'in_channels' in checkpoint['cfg'] and checkpoint['cfg']['in_channels'] != cfg.in_channels:\n                     print(f\"  ERROR: Mismatch in_channels! Checkpoint: {checkpoint['cfg']['in_channels']}, Current CFG: {cfg.in_channels}\")\n                     continue # Bỏ qua model này\n            model = BirdCLEFModel(cfg, num_classes_model)\n            if 'model_state_dict' in checkpoint:\n                model.load_state_dict(checkpoint['model_state_dict'])\n            else:\n                 model.load_state_dict(checkpoint)\n                 print(\"  Warning: Loaded state_dict directly.\")\n            model = model.to(cfg.device)\n            model.eval()\n            models.append(model)\n            print(f\"  Model loaded successfully from {model_path_str}.\")\n        except Exception as e:\n            print(f\"  ERROR loading model {model_path_str}: {e}\")\n    if not models:\n         print(\"ERROR: Failed to load ANY models.\")\n         return None\n    return models\n\n# %%\ndef predict_soundscape(audio_path, models, cfg):\n    soundscape_id = Path(audio_path).stem\n    # ### DEBUG ###\n    print(f\"DEBUG (predict_soundscape): Processing file: {soundscape_id}\")\n\n    try:\n        audio_data, sr = librosa.load(audio_path, sr=cfg.FS)\n        if sr != cfg.FS:\n            print(f\"  WARNING: Audio sr {sr} differs from cfg.FS {cfg.FS}. Resampling not done here, ensure consistency.\")\n\n        total_duration = len(audio_data) / cfg.FS\n        num_segments = math.ceil(total_duration / cfg.WINDOW_SIZE)\n        # ### DEBUG ###\n        print(f\"  DEBUG: Audio duration: {total_duration:.2f}s, Expected segments: {num_segments}\")\n\n        if num_segments == 0:\n            print(f\"  WARNING: No segments for {soundscape_id}, duration {total_duration}s too short.\")\n            return [], []\n\n        segment_specs_list = []\n        row_ids_list = []\n\n        for i in range(num_segments):\n            start_time = i * cfg.WINDOW_SIZE\n            end_time = start_time + cfg.WINDOW_SIZE\n            start_sample = int(start_time * cfg.FS)\n            end_sample = int(end_time * cfg.FS)\n            segment_audio = audio_data[start_sample:end_sample]\n\n            spec = process_audio_segment(segment_audio, cfg)\n            segment_specs_list.append(spec)\n            row_ids_list.append(f\"{soundscape_id}_{(i + 1) * cfg.WINDOW_SIZE}\")\n            if i < 2 and cfg.debug : # Chỉ in 2 segment đầu khi debug\n                print(f\"    DEBUG (segment {i}): Spec shape: {spec.shape}, min: {spec.min():.2f}, max: {spec.max():.2f}, mean: {spec.mean():.2f}\")\n\n\n        num_valid_segments = len(segment_specs_list)\n        if num_valid_segments == 0:\n            print(f\"  WARNING: No valid spectrograms generated for {soundscape_id}\")\n            return [], []\n\n        all_segment_preds = np.zeros((num_valid_segments, cfg.num_classes), dtype=np.float32)\n\n        with torch.no_grad():\n            for i in range(0, num_valid_segments, cfg.batch_size):\n                batch_start_idx = i\n                batch_end_idx = min(i + cfg.batch_size, num_valid_segments)\n                current_batch_size = batch_end_idx - batch_start_idx\n                # ### DEBUG ###\n                # print(f\"    DEBUG: Batch {i//cfg.batch_size + 1}, indices [{batch_start_idx}:{batch_end_idx}], size: {current_batch_size}\")\n\n                batch_specs_np = np.array(segment_specs_list[batch_start_idx:batch_end_idx])\n                batch_tensors = torch.tensor(batch_specs_np, dtype=torch.float32).unsqueeze(1).repeat(1, 3, 1, 1)\n                batch_tensors = normalize(batch_tensors)\n                batch_tensors = batch_tensors.to(cfg.device)\n                # ### DEBUG ###\n                if i == 0 and cfg.debug:\n                    print(f\"      DEBUG (batch 0 tensor): Shape: {batch_tensors.shape}, min: {batch_tensors.min():.2f}, max: {batch_tensors.max():.2f}, mean: {batch_tensors.mean():.2f}\")\n\n\n                batch_ensemble_preds = []\n                for model_idx, model in enumerate(models):\n                    outputs = model(batch_tensors)\n                    probs = torch.sigmoid(outputs).cpu().numpy()\n                    batch_ensemble_preds.append(probs)\n                    # ### DEBUG ###\n                    if i == 0 and model_idx == 0 and cfg.debug:\n                        print(f\"        DEBUG (model {model_idx}, batch 0 logits): Shape: {outputs.shape}, min: {outputs.min():.2f}, max: {outputs.max():.2f}, mean: {outputs.mean():.2f}\")\n                        print(f\"        DEBUG (model {model_idx}, batch 0 probs): Shape: {probs.shape}, min: {probs.min():.4f}, max: {probs.max():.4f}, mean: {probs.mean():.4f}, sum_per_sample_avg: {probs.sum(axis=1).mean():.2f}\")\n\n\n                if batch_ensemble_preds:\n                    batch_avg_preds = np.mean(batch_ensemble_preds, axis=0)\n                    all_segment_preds[batch_start_idx:batch_end_idx] = batch_avg_preds\n                    # ### DEBUG ###\n                    if i == 0 and cfg.debug:\n                         print(f\"      DEBUG (batch 0 ensemble_avg_preds): Shape: {batch_avg_preds.shape}, min: {batch_avg_preds.min():.4f}, max: {batch_avg_preds.max():.4f}, mean: {batch_avg_preds.mean():.4f}\")\n                else:\n                    print(f\"    WARNING: No predictions from ensemble for batch {i//cfg.batch_size + 1}\")\n\n\n            # TTA \n            if cfg.use_tta:\n                pass\n\n        predictions_list = list(all_segment_preds)\n        return row_ids_list, predictions_list\n\n    except Exception as e:\n        print(f\"ERROR processing {audio_path}: {e}\")\n        import traceback\n        traceback.print_exc()\n        return [], []\n\n\n# %%\ndef run_inference(cfg, models):\n    test_dir = Path(cfg.test_soundscapes)\n    if not test_dir.exists():\n         print(f\"ERROR: Test soundscapes directory not found at {cfg.test_soundscapes}\")\n         return [], []\n    test_files = sorted(list(test_dir.glob('*.ogg')))\n    if not test_files:\n         print(f\"WARNING: No .ogg files found in {cfg.test_soundscapes}.\")\n         sample_sub = pd.read_csv(cfg.submission_csv)\n         return sample_sub['row_id'].tolist(), [np.zeros(cfg.num_classes) for _ in range(len(sample_sub))]\n    if cfg.debug:\n        print(f\"DEBUG mode: Processing first {cfg.debug_limit} files: {[f.name for f in test_files[:cfg.debug_limit]]}\")\n        test_files = test_files[:cfg.debug_limit]\n    print(f\"Found {len(test_files)} test soundscape files to process.\")\n    all_row_ids = []\n    all_predictions = []\n    for audio_path_obj in tqdm(test_files, desc=\"Inferencing Soundscapes\"):\n        row_ids, preds = predict_soundscape(str(audio_path_obj), models, cfg)\n        all_row_ids.extend(row_ids)\n        all_predictions.extend(preds)\n    return all_row_ids, all_predictions\n\n# %%\ndef create_submission(row_ids, predictions, species_ids, cfg):\n    print(\"Creating submission dataframe...\")\n    # ### DEBUG ###\n    print(f\"  DEBUG (create_submission): len(row_ids): {len(row_ids)}, len(predictions): {len(predictions)}\")\n    if predictions:\n        print(f\"  DEBUG (create_submission): Shape of first prediction: {predictions[0].shape if len(predictions[0].shape) > 0 else 'scalar or empty'}\")\n\n\n    if not row_ids or not predictions or len(row_ids) != len(predictions):\n        print(\"  WARNING: Mismatch in row_ids/predictions or empty. Creating submission based on sample.\")\n        submission_df = pd.read_csv(cfg.submission_csv)\n        for col in submission_df.columns:\n            if col != 'row_id': submission_df[col] = 0.0\n        return submission_df\n\n    submission_dict = {'row_id': row_ids}\n    try:\n        predictions_array = np.array(predictions)\n        if predictions_array.ndim == 1 and predictions_array.shape[0] == len(row_ids) and len(row_ids)>0 and predictions_array.shape[0] > 0 and isinstance(predictions[0], (float, int)):\n            print(\"  ERROR: Predictions seem to be a list of scalars, not arrays of probabilities per class.\")\n            sample_sub = pd.read_csv(cfg.submission_csv)\n            sample_sub.iloc[:, 1:] = 0.0\n            return sample_sub\n\n        if predictions_array.shape[1] != len(species_ids):\n             print(f\"  ERROR: Num predicted columns ({predictions_array.shape[1]}) != num species ({len(species_ids)}).\")\n             sample_sub = pd.read_csv(cfg.submission_csv)\n             sample_sub.iloc[:, 1:] = 0.0\n             return sample_sub\n        for i, species in enumerate(species_ids):\n            submission_dict[species] = predictions_array[:, i]\n    except Exception as e:\n        print(f\"  ERROR converting predictions to array or assigning columns: {e}\")\n        sample_sub = pd.read_csv(cfg.submission_csv)\n        sample_sub.iloc[:, 1:] = 0.0\n        return sample_sub\n\n\n    submission_df = pd.DataFrame(submission_dict)\n    # ### DEBUG ###\n    print(f\"  DEBUG (create_submission): Initial submission_df head:\\n{submission_df.head()}\")\n    print(f\"  DEBUG (create_submission): Initial submission_df describe:\\n{submission_df.describe()}\")\n\n\n    try:\n        sample_sub = pd.read_csv(cfg.submission_csv)\n        # ### DEBUG ###\n        print(f\"  DEBUG (create_submission): sample_submission.csv head:\\n{sample_sub.head()}\")\n        final_df = pd.merge(sample_sub[['row_id']], submission_df, on='row_id', how='left').fillna(0.0)\n        final_df = final_df[['row_id'] + cfg.species_ids] \n        # ### DEBUG ###\n        print(f\"  DEBUG (create_submission): final_df (after merge) head:\\n{final_df.head()}\")\n        print(f\"  DEBUG (create_submission): final_df (after merge) describe:\\n{final_df.describe()}\")\n\n\n    except Exception as e:\n        print(f\"  ERROR merging with sample submission: {e}. Returning raw predictions df.\")\n        final_df = submission_df\n\n    print(f\"Submission dataframe created with {len(final_df)} rows.\")\n    # ### DEBUG ###\n    numeric_cols = final_df.select_dtypes(include=np.number).columns\n    for col in numeric_cols:\n        if final_df[col].min() < 0 or final_df[col].max() > 1:\n            print(f\"    WARNING: Column {col} has values outside [0,1]. Min: {final_df[col].min()}, Max: {final_df[col].max()}\")\n    if final_df[numeric_cols].isnull().any().any():\n        print(f\"    WARNING: Submission contains NaN values!\")\n\n    return final_df","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ## Main Execution\n\n# %%\ndef main():\n    overall_start_time = time.time()\n    print(\"--- Starting BirdCLEF 2025 Inference (with DEBUG) ---\")\n\n    print(\"\\n--- Loading Models ---\")\n    models = load_models(cfg)\n    if models is None or not models:\n        print(\"Failed to load models. Exiting.\")\n        # Tạo submission rỗng nếu không load được model\n        sample_sub = pd.read_csv(cfg.submission_csv)\n        sample_sub.iloc[:, 1:] = 0.0 # Điền 0 cho tất cả các loài\n        sample_sub.to_csv('submission.csv', index=False)\n        print(\"Created an empty submission.csv as no models were loaded.\")\n        return\n    print(f\"Successfully loaded {len(models)} model(s).\")\n\n    print(\"\\n--- Running Inference ---\")\n    row_ids, predictions = run_inference(cfg, models)\n\n    print(\"\\n--- Creating Submission ---\")\n    submission_df = create_submission(row_ids, predictions, cfg.species_ids, cfg)\n\n    submission_path = 'submission.csv'\n    try:\n        submission_df.to_csv(submission_path, index=False)\n        print(f\"\\nSubmission file saved successfully to: {submission_path}\")\n        # ### DEBUG ###\n        print(f\"Final Submission head:\\n{submission_df.head()}\")\n        if cfg.debug and not submission_df.empty:\n            print(f\"Final Submission describe:\\n{submission_df.describe().transpose().head(10)}\") # Chỉ in 10 loài đầu\n            # Kiểm tra một vài row_id cụ thể nếu bạn biết\n            # if 'soundscape_X_Y' in submission_df['row_id'].values:\n            #     print(submission_df[submission_df['row_id'] == 'soundscape_X_Y'])\n\n    except Exception as e:\n        print(f\"ERROR saving submission file: {e}\")\n\n    overall_end_time = time.time()\n    print(f\"\\n--- Inference Finished ---\")\n    print(f\"Total time: {(overall_end_time - overall_start_time)/60:.2f} minutes\")\n\nif __name__ == \"__main__\":\n    gc.collect()\n    if cfg.device == 'cuda': \n        torch.cuda.empty_cache()\n    main()\n","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}