{"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"codemirror_mode":{"name":"ipython","version":3},"file_extension":".py","mimetype":"text/x-python","name":"python","nbconvert_exporter":"python","pygments_lexer":"ipython3","version":"3.11.11"},"kaggle":{"accelerator":"none","dataSources":[{"sourceId":91844,"databundleVersionId":11361821,"sourceType":"competition"},{"sourceId":11948763,"sourceType":"datasetVersion","datasetId":7130272},{"sourceId":12056622,"sourceType":"datasetVersion","datasetId":7588147},{"sourceId":12065622,"sourceType":"datasetVersion","datasetId":7594423},{"sourceId":12065630,"sourceType":"datasetVersion","datasetId":7594429},{"sourceId":12065636,"sourceType":"datasetVersion","datasetId":7594434}],"dockerImageVersionId":31040,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false},"papermill":{"default_parameters":{},"duration":102.399335,"end_time":"2025-06-05T05:36:23.345163","environment_variables":{},"exception":null,"input_path":"__notebook__.ipynb","output_path":"__notebook__.ipynb","parameters":{},"start_time":"2025-06-05T05:34:40.945828","version":"2.6.0"}},"nbformat_minor":4,"nbformat":4,"cells":[{"id":"83e81c7e","cell_type":"code","source":"!pip install -qqq onnxruntime --no-index --find-links=file:/kaggle/input/onnxruntime","metadata":{"execution":{"iopub.execute_input":"2025-06-05T05:34:46.220293Z","iopub.status.busy":"2025-06-05T05:34:46.219967Z","iopub.status.idle":"2025-06-05T05:34:53.393713Z","shell.execute_reply":"2025-06-05T05:34:53.392240Z"},"papermill":{"duration":7.180459,"end_time":"2025-06-05T05:34:53.396358","exception":false,"start_time":"2025-06-05T05:34:46.215899","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"d673faa9","cell_type":"code","source":"import numpy as np\nfrom typing import Union\nimport os\nimport gc\nimport time\nimport pandas as pd\nimport torch\nimport torchaudio\nimport torchaudio.transforms as AT\nimport concurrent.futures\nfrom tqdm import tqdm\nimport onnxruntime as ort\n\n# Configuration\nTEST_AUDIO_DIR_DEFAULT = '/kaggle/input/birdclef-2025/test_soundscapes/'\nTRAIN_AUDIO_DIR_DEBUG = '/kaggle/input/birdclef-2025/train_soundscapes/'\nTAXONOMY_FILE = \"/kaggle/input/birdclef-2025/taxonomy.csv\"\nSUBMISSION_FILE = \"submission.csv\"\nSUBMISSION_FILE_PSD = \"submission_psd.csv\"\nSUBMISSION_FILE_PSD2 = \"submission_psd2.csv\"\nSUBMISSION_FILE_PSD3 = \"submission_psd3.csv\"\n\n# Model paths for both types - changed to .onnx extension\nMODEL_PATHS_EF3_PSD = [f'/kaggle/input/new15sec-onnx-v2/ef3_{i}.onnx' for i in range(1,2)]\nMODEL_PATHS_SER_PSD = [f'/kaggle/input/new15sec-onnx-v2/ser_{i}.onnx' for i in range(1,2)]\nMODEL_PATHS_NF_PSD = [f'/kaggle/input/new15sec-onnx-v2/nfnet_{i}.onnx' for i in range(1,2)]\n\nMODEL_PATHS_FB_PSD2 = [f'/kaggle/input/new122sec-onnx-v2/fbnet_{i}.onnx' for i in range(1,2)]\nMODEL_PATHS_NF_PSD2 = [f'/kaggle/input/new20sec-onnx/ef3_{i}.onnx' for i in range(1,2)]\n\nMODEL_PATHS_FB_PSD3 = [f'/kaggle/input/new30secvis-onnx/fbnet_{i}.onnx' for i in range(1,2)]\nMODEL_PATHS_NF_PSD3 = [f'/kaggle/input/new30secvis-onnx/nfnet_{i}.onnx' for i in range(1,2)]\n\nPSD_MODEL_PATHS = MODEL_PATHS_EF3_PSD + MODEL_PATHS_SER_PSD + MODEL_PATHS_NF_PSD\nPSD2_MODEL_PATHS = MODEL_PATHS_FB_PSD2 + MODEL_PATHS_NF_PSD2\nPSD3_MODEL_PATHS = MODEL_PATHS_FB_PSD3 + MODEL_PATHS_NF_PSD3\n\n# Debugging\nDEBUG_MODE = False  # Set to True to enable debug mode\nDEBUG_START_NUM = 5\nDEBUG_NUM_FILES = 8\n\n# Audio Parameters\nWAV_SEC = 5\nSAMPLE_RATE = 32000\nINFER_DURATION_SEC = 5\nTRAIN_DURATION_SEC = 10\n\n# Mel Spectrogram Parameters\nN_FFT = 2048\nWIN_LENGTH = 2048\nHOP_LENGTH = 512\nF_MIN = 20\nF_MAX = 16000\nN_MELS = 256\nMEL_CENTER = True\nMEL_PAD_MODE = \"reflect\"\nMEL_POWER = 2.0\nMEL_NORM = 'slaney'\nMEL_SCALE = \"htk\"\n\n# Model Parameters\nBASE_MODEL_NAME_1 = 'seresnext26t_32x4d'\nBASE_MODEL_NAME_2 = 'tf_efficientnetv2_b3.in21k'\nBASE_MODEL_NAME_3 = 'eca_nfnet_l0'\nPRETRAINED_MODELS = False\nIN_CHANNELS_TIMM = 1\nNUM_CLASSES = 206 # Derived from taxonomy.csv, but useful to have as a constant for AttBlockV2\n\n# Inference Parameters\nTTA_DELTA = 2\nAPPLY_POWER_TOP_K = 30\nAPPLY_POWER_EXPONENT = 2\nMAX_WORKERS_THREADPOOL = 5\n\n# End Configuration\n\ndef apply_power_to_low_ranked_cols(\n    p: np.ndarray,\n    top_k: int = APPLY_POWER_TOP_K,\n    exponent: Union[int, float] = APPLY_POWER_EXPONENT,\n    inplace: bool = True\n) -> np.ndarray:\n    if not inplace:\n        p = p.copy()\n\n    tail_cols = np.argsort(-p.max(axis=0))[top_k:]\n\n    p[:, tail_cols] = p[:, tail_cols] ** exponent\n    return p\n\namount_to_add = {32: 0.05, 58: 0.05, 5: 0.05, 1: 0.05, 7: 0.065, 2: 0.065, 17: 0.065, 44: 0.065, 52: 0.1, 48: 0.1, 50: 0.1, 57: 0.1, 27: 0.1, 33: 0.1, 49: 0.1, 20: 0.1, 26: 0.1, 23: 0.1, 24: 0.1, 11: 0.1, 0: 0.1, 10: 0.1, 39: 0.1}\nSHAPE = (12, 206)\namount_array = np.zeros(SHAPE, dtype=float)\nfor idx, amount in amount_to_add.items():\n    if idx < SHAPE[1]:  # Check bounds\n        amount_array[:, idx] = amount\ndef add_amounts_to_top_predictions(predictions, amount_array=amount_array, top_k=5):\n\n    predictions = predictions.astype(float)  # Ensure float type for addition\n    \n    # Get top k indices for all rows at once\n    top_k_indices = np.argsort(predictions, axis=1)[:, -top_k:]\n    \n    # Create a boolean mask for top k positions\n    top_k_mask = np.zeros_like(predictions, dtype=bool)\n    row_indices = np.arange(predictions.shape[0])[:, np.newaxis]\n    top_k_mask[row_indices, top_k_indices] = True\n    # Apply amounts only where mask is True (top k positions)\n    mask_and_amount = top_k_mask & (amount_array > 0)\n    predictions[mask_and_amount] += amount_array[mask_and_amount]\n        \n    return predictions\ntest_audio_dir = TEST_AUDIO_DIR_DEFAULT\nfile_list = [f for f in sorted(os.listdir(test_audio_dir))]\nfile_list = [file.split('.')[0] for file in file_list if file.endswith('.ogg')]\n\ndebug = DEBUG_MODE\nif len(file_list) == 0 or debug: # Simplified debug check\n    debug = True # Ensure debug is true if file_list was initially empty\n    test_audio_dir = TRAIN_AUDIO_DIR_DEBUG\n    file_list = [f for f in sorted(os.listdir(test_audio_dir))]\n    file_list = [file.split('.')[0] for file in file_list if file.endswith('.ogg')]\n    if DEBUG_NUM_FILES > 0 : # Allow running on all files in debug if DEBUG_NUM_FILES is 0 or less\n        file_list = file_list[DEBUG_START_NUM : DEBUG_START_NUM + DEBUG_NUM_FILES]\n\nprint('Debug mode:', debug)\nprint('Number of test soundscapes:', len(file_list))\n\nmin_segment = SAMPLE_RATE * WAV_SEC # Retained as it was explicitly defined\ntaxonomy_df = pd.read_csv(TAXONOMY_FILE)\nclass_labels = sorted(taxonomy_df['primary_label'].unique().tolist())\n# Ensure NUM_CLASSES matches the actual number of labels\nif NUM_CLASSES != len(class_labels):\n    print(f\"Warning: NUM_CLASSES ({NUM_CLASSES}) does not match number of labels from taxonomy ({len(class_labels)}). Using length from taxonomy.\")\n\nmel_spectrogram = AT.MelSpectrogram(\n    sample_rate=SAMPLE_RATE,\n    n_fft=N_FFT,\n    win_length=WIN_LENGTH,\n    hop_length=HOP_LENGTH,\n    center=MEL_CENTER,\n    f_min=F_MIN,\n    f_max=F_MAX,\n    pad_mode=MEL_PAD_MODE,\n    power=MEL_POWER,\n    norm=MEL_NORM,\n    n_mels=N_MELS,\n    mel_scale=MEL_SCALE,\n)\n\ndef normalize_std(spec, eps=1e-6):\n    mean = torch.mean(spec)\n    std = torch.std(spec)\n    return torch.where(std == 0, spec-mean, (spec - mean) / (std+eps))\n\ndef audio_to_mel(filepath=None):\n    waveform, sr = torchaudio.load(filepath, backend=\"soundfile\") # sr should be SAMPLE_RATE\n    if sr != SAMPLE_RATE:\n        # Resample if necessary, though problem context implies data is prepared\n        print(f\"Warning: Sample rate of {filepath} is {sr}, not {SAMPLE_RATE}. Resampling not implemented here.\")\n    len_wav = waveform.shape[1]\n    waveform = waveform[0,:].reshape(1, len_wav)\n    PREDS = []\n    # The loop processes 12 segments of 5 seconds each from a 60-second audio file\n    # Each segment is 5 * SAMPLE_RATE frames\n    for i in range(12): # Assuming 60s audio, 12 * 5s chunks\n        waveform2 = waveform[:, i * SAMPLE_RATE * WAV_SEC : (i * SAMPLE_RATE * WAV_SEC) + (SAMPLE_RATE * WAV_SEC)]\n        melspec = mel_spectrogram(waveform2)\n        melspec = torch.log(melspec+1e-6)\n        melspec = normalize_std(melspec)\n        melspec = torch.unsqueeze(melspec, dim=0)\n\n        PREDS.append(melspec)\n    return torch.vstack(PREDS)\n\n# Load ONNX models\npsd_models = []\npsd_model_types = []\n\nfor path in PSD_MODEL_PATHS:\n    model_type = 'PSD'\n    print(f\"{path}  - {model_type}\")\n    \n    # Load ONNX model\n    ort_session = ort.InferenceSession(path, providers=['CPUExecutionProvider'])\n    \n    psd_models.append(ort_session)\n    psd_model_types.append(model_type)\n\npsd2_models = []\npsd2_model_types = []\n\nfor path in PSD2_MODEL_PATHS:\n    model_type = 'PSD2'\n    print(f\"{path}  - {model_type}\")\n    \n    # Load ONNX model\n    ort_session = ort.InferenceSession(path, providers=['CPUExecutionProvider'])\n    \n    psd2_models.append(ort_session)\n    psd2_model_types.append(model_type)\n\npsd3_models = []\npsd3_model_types = []\n\nfor path in PSD3_MODEL_PATHS:\n    model_type = 'PSD3'\n    print(f\"{path}  - {model_type}\")\n    \n    # Load ONNX model\n    ort_session = ort.InferenceSession(path, providers=['CPUExecutionProvider'])\n    \n    psd3_models.append(ort_session)\n    psd3_model_types.append(model_type)\n\ndef prediction_combined(afile):\n    global pred_psd, pred_psd2, pred_psd3\n    path = test_audio_dir + afile + '.ogg'\n    \n    sig = audio_to_mel(path)\n    \n    # Process PSD models\n    psd_outputs = None\n    for model_idx, (ort_session, model_type) in enumerate(zip(psd_models, psd_model_types)):\n        \n        # Convert PyTorch tensor to numpy for ONNX\n        sig_np = sig.detach().cpu().numpy().astype(np.float32)\n        \n        # Get input name from ONNX model\n        input_name = ort_session.get_inputs()[0].name\n        \n        # Run ONNX inference\n        ort_outputs = ort_session.run(None, {input_name: sig_np})\n        p = ort_outputs[0]  # Assuming first output is the predictions\n        \n        # Apply sigmoid if needed (depends on ONNX model export)\n        # p = 1 / (1 + np.exp(-p))  # sigmoid function if needed\n        \n        if p.ndim > 2:\n            p = p.squeeze()\n        p = add_amounts_to_top_predictions(p)\n        p = apply_power_to_low_ranked_cols(p, top_k=APPLY_POWER_TOP_K, exponent=APPLY_POWER_EXPONENT)\n        if psd_outputs is None: \n            psd_outputs = p\n        else: \n            psd_outputs += p\n\n    psd_outputs /= len(psd_models)\n    \n    # Process PSD2 models\n    psd2_outputs = None\n    for model_idx, (ort_session, model_type) in enumerate(zip(psd2_models, psd2_model_types)):\n        \n        # Convert PyTorch tensor to numpy for ONNX\n        sig_np = sig.detach().cpu().numpy().astype(np.float32)\n        \n        # Get input name from ONNX model\n        input_name = ort_session.get_inputs()[0].name\n        \n        # Run ONNX inference\n        ort_outputs = ort_session.run(None, {input_name: sig_np})\n        p = ort_outputs[0]  # Assuming first output is the predictions\n        \n        # Apply sigmoid if needed (depends on ONNX model export)\n        # p = 1 / (1 + np.exp(-p))  # sigmoid function if needed\n        \n        if p.ndim > 2:\n            p = p.squeeze()\n        p = add_amounts_to_top_predictions(p)       \n        p = apply_power_to_low_ranked_cols(p, top_k=APPLY_POWER_TOP_K, exponent=APPLY_POWER_EXPONENT)\n        if psd2_outputs is None: \n            psd2_outputs = p\n        else: \n            psd2_outputs += p\n\n    psd2_outputs /= len(psd2_models)\n    \n    # Process PSD3 models\n    psd3_outputs = None\n    for model_idx, (ort_session, model_type) in enumerate(zip(psd3_models, psd3_model_types)):\n        \n        # Convert PyTorch tensor to numpy for ONNX\n        sig_np = sig.detach().cpu().numpy().astype(np.float32)\n        \n        # Get input name from ONNX model\n        input_name = ort_session.get_inputs()[0].name\n        \n        # Run ONNX inference\n        ort_outputs = ort_session.run(None, {input_name: sig_np})\n        p = ort_outputs[0]  # Assuming first output is the predictions\n        \n        # Apply sigmoid if needed (depends on ONNX model export)\n        # p = 1 / (1 + np.exp(-p))  # sigmoid function if needed\n        \n        if p.ndim > 2:\n            p = p.squeeze()\n        p = add_amounts_to_top_predictions(p)\n        p = apply_power_to_low_ranked_cols(p, top_k=APPLY_POWER_TOP_K, exponent=APPLY_POWER_EXPONENT)\n        if psd3_outputs is None: \n            psd3_outputs = p\n        else: \n            psd3_outputs += p\n\n    psd3_outputs /= len(psd3_models)\n    \n    # Store results for all 3 model types\n    chunks = [[] for i in range(12)]\n    for i in range(len(chunks)):\n        chunk_end_time = (i + 1) * 5\n        row_id = afile + '_' + str(chunk_end_time)\n        \n        # Store PSD results\n        pred_psd['row_id'].append(row_id)\n        bird_no = 0\n        for bird in class_labels:\n            pred_psd[bird].append(psd_outputs[i,bird_no])\n            bird_no += 1\n            \n        # Store PSD2 results\n        pred_psd2['row_id'].append(row_id)\n        bird_no = 0\n        for bird in class_labels:\n            pred_psd2[bird].append(psd2_outputs[i,bird_no])\n            bird_no += 1\n            \n        # Store PSD3 results\n        pred_psd3['row_id'].append(row_id)\n        bird_no = 0\n        for bird in class_labels:\n            pred_psd3[bird].append(psd3_outputs[i,bird_no])\n            bird_no += 1\n            \n    gc.collect()\n\n# Initialize prediction dictionaries\npred_psd = {'row_id': []}\nfor species_code in class_labels:\n    pred_psd[species_code] = []\n\npred_psd2 = {'row_id': []}\nfor species_code in class_labels:\n    pred_psd2[species_code] = []\n\npred_psd3 = {'row_id': []}\nfor species_code in class_labels:\n    pred_psd3[species_code] = []\n\n# Process all models in single ThreadPoolExecutor\nprint(\"Processing PSD, PSD2, and PSD3 models...\")\nstart = time.time()\nwith concurrent.futures.ThreadPoolExecutor(max_workers=MAX_WORKERS_THREADPOOL) as executor:\n    _ = list(executor.map(prediction_combined, tqdm(file_list, desc=\"Processing audio files\")))\nend_t = time.time()\n\nif debug == True:\n    print(\"Total processing time:\", 700*(end_t - start)/60/DEBUG_NUM_FILES if DEBUG_NUM_FILES > 0 else \"DEBUG_NUM_FILES is 0, cannot calculate rate\")\n\n# Save and process PSD results\nresults_psd = pd.DataFrame(pred_psd, columns = ['row_id'] + class_labels)\nresults_psd.to_csv(SUBMISSION_FILE_PSD, index=False)\n\n# Apply temporal smoothing to PSD results\nsub_psd = pd.read_csv(SUBMISSION_FILE_PSD)\ncols = sub_psd.columns[1:]\ngroups = sub_psd['row_id'].str.rsplit('_', n=1).str[0]\ngroups = groups.values\nfor group in np.unique(groups):\n    current_group_mask = (groups == group)\n    predictions = sub_psd.loc[current_group_mask, cols].values\n    new_predictions = predictions.copy()\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    new_predictions[0] = (predictions[0] * 0.8) + (predictions[1] * 0.2)\n    new_predictions[-1] = (predictions[-1] * 0.8) + (predictions[-2] * 0.2)\n    sub_psd.loc[current_group_mask, cols] = new_predictions\nsub_psd.to_csv(SUBMISSION_FILE_PSD, index=False)\n\n# Save and process PSD2 results\nresults_psd2 = pd.DataFrame(pred_psd2, columns = ['row_id'] + class_labels)\nresults_psd2.to_csv(SUBMISSION_FILE_PSD2, index=False)\n\n# Apply temporal smoothing to PSD2 results\nsub_psd2 = pd.read_csv(SUBMISSION_FILE_PSD2)\ncols = sub_psd2.columns[1:]\ngroups = sub_psd2['row_id'].str.rsplit('_', n=1).str[0]\ngroups = groups.values\nfor group in np.unique(groups):\n    current_group_mask = (groups == group)\n    predictions = sub_psd2.loc[current_group_mask, cols].values\n    new_predictions = predictions.copy()\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    new_predictions[0] = (predictions[0] * 0.8) + (predictions[1] * 0.2)\n    new_predictions[-1] = (predictions[-1] * 0.8) + (predictions[-2] * 0.2)\n    sub_psd2.loc[current_group_mask, cols] = new_predictions\nsub_psd2.to_csv(SUBMISSION_FILE_PSD2, index=False)\n\n# Save and process PSD3 results\nresults_psd3 = pd.DataFrame(pred_psd3, columns = ['row_id'] + class_labels)\nresults_psd3.to_csv(SUBMISSION_FILE_PSD3, index=False)\n\n# Apply temporal smoothing to PSD3 results\nsub_psd3 = pd.read_csv(SUBMISSION_FILE_PSD3)\ncols = sub_psd3.columns[1:]\ngroups = sub_psd3['row_id'].str.rsplit('_', n=1).str[0]\ngroups = groups.values\nfor group in np.unique(groups):\n    current_group_mask = (groups == group)\n    predictions = sub_psd3.loc[current_group_mask, cols].values\n    new_predictions = predictions.copy()\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    new_predictions[0] = (predictions[0] * 0.8) + (predictions[1] * 0.2)\n    new_predictions[-1] = (predictions[-1] * 0.8) + (predictions[-2] * 0.2)\n    sub_psd3.loc[current_group_mask, cols] = new_predictions\nsub_psd3.to_csv(SUBMISSION_FILE_PSD3, index=False)\n\n# Average the three submissions using rank averaging\nprint(\"Averaging PSD, PSD2, and PSD3 submissions...\")\nsub_psd = pd.read_csv(SUBMISSION_FILE_PSD)\nsub_psd2 = pd.read_csv(SUBMISSION_FILE_PSD2)\nsub_psd3 = pd.read_csv(SUBMISSION_FILE_PSD3)\n\n# Ensure all dataframes have the same row_id order\nsub_psd = sub_psd.sort_values('row_id').reset_index(drop=True)\nsub_psd2 = sub_psd2.sort_values('row_id').reset_index(drop=True)\nsub_psd3 = sub_psd3.sort_values('row_id').reset_index(drop=True)\n\n# Convert to ranks and average (excluding row_id column)\nsub_final = sub_psd.copy()\ncols = sub_final.columns[1:]  # All columns except row_id\nsub_psd[cols] = sub_psd[cols].rank(axis=0, pct=True)\nsub_psd2[cols] = sub_psd2[cols].rank(axis=0, pct=True)\nsub_psd3[cols] = sub_psd3[cols].rank(axis=0, pct=True)\n\n# Weighted average of the three models\nsub_final[cols] = 0.5*sub_psd[cols] + 0.25*sub_psd2[cols] + 0.25*sub_psd3[cols]\n\nsub_final.to_csv(SUBMISSION_FILE, index=False)\nprint(f\"Final averaged submission saved to {SUBMISSION_FILE}\")\nprint(\"Used weights: PSD=0.5, PSD2=0.25, PSD3=0.25\") ","metadata":{"execution":{"iopub.execute_input":"2025-06-05T05:34:53.403604Z","iopub.status.busy":"2025-06-05T05:34:53.403263Z","iopub.status.idle":"2025-06-05T05:36:19.795198Z","shell.execute_reply":"2025-06-05T05:36:19.793934Z"},"papermill":{"duration":86.397815,"end_time":"2025-06-05T05:36:19.796778","exception":false,"start_time":"2025-06-05T05:34:53.398963","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"d2f78bae","cell_type":"code","source":"sub_final.head()","metadata":{"execution":{"iopub.execute_input":"2025-06-05T05:36:19.803505Z","iopub.status.busy":"2025-06-05T05:36:19.803172Z","iopub.status.idle":"2025-06-05T05:36:19.841176Z","shell.execute_reply":"2025-06-05T05:36:19.840140Z"},"papermill":{"duration":0.043201,"end_time":"2025-06-05T05:36:19.842777","exception":false,"start_time":"2025-06-05T05:36:19.799576","status":"completed"},"tags":[]},"outputs":[],"execution_count":null}]}