{"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":[{"sourceId":91844,"databundleVersionId":11361821,"sourceType":"competition"},{"sourceId":11558331,"sourceType":"datasetVersion","datasetId":7242517},{"sourceId":762,"sourceType":"modelInstanceVersion","modelInstanceId":629,"modelId":52},{"sourceId":356425,"sourceType":"modelInstanceVersion","modelInstanceId":297166,"modelId":317778}],"dockerImageVersionId":31011,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"This notebook is inspired by the code from: https://medium.com/@danya.kosmin/meowtalk-how-to-train-yamnet-audio-classification-model-for-mobile-devices-4f228cf5650c\n\nAdditionally, see https://www.tensorflow.org/tutorials/audio/transfer_learning_audio#import_tensorflow_and_other_libraries","metadata":{}},{"cell_type":"markdown","source":"# Import libraries","metadata":{}},{"cell_type":"code","source":"import os\nimport csv\nimport numpy as np\nimport pandas as pd\nimport tensorflow as tf\nimport tensorflow_hub as hub\nimport matplotlib.pyplot as plt\nfrom tqdm import tqdm\nimport librosa\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.metrics import roc_auc_score, precision_recall_curve, average_precision_score\nfrom tensorflow.keras import layers, models\nimport shutil","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-27T07:34:01.590061Z","iopub.execute_input":"2025-04-27T07:34:01.592002Z","iopub.status.idle":"2025-04-27T07:34:01.603056Z","shell.execute_reply.started":"2025-04-27T07:34:01.591929Z","shell.execute_reply":"2025-04-27T07:34:01.601983Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Set random seeds for reproducibility\nSEED = 42\ntf.random.set_seed(SEED)\nnp.random.seed(SEED)\n\nBATCH_SIZE = 32\nEPOCHS = 20\nINPUT_SHAPE = (1024,)  # YAMNet embedding size\nSAMPLE_RATE = 16000  # YAMNet requires 16kHz audio\n\nDATA_DIR = '/kaggle/input/birdclef-2025/train_audio'\nOUTPUT_DIR = '/kaggle/working/'\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-27T07:34:01.605223Z","iopub.execute_input":"2025-04-27T07:34:01.605634Z","iopub.status.idle":"2025-04-27T07:34:01.644272Z","shell.execute_reply.started":"2025-04-27T07:34:01.605601Z","shell.execute_reply":"2025-04-27T07:34:01.643155Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Handling Audio","metadata":{}},{"cell_type":"code","source":"def load_audio_file(file_path, sample_rate=16000):\n    audio, file_sr = librosa.load(file_path, sr=sample_rate, mono=True)\n    audio = audio.astype(np.float32)\n    return audio\n\ndef segment_audio(audio, sample_rate=16000):\n    segment_length = int(5 * sample_rate) # 5 seconds * sample rate = number of samples for the segment\n    segments = []\n    \n    # Pad audio if needed\n    if len(audio) % segment_length != 0:\n        padding = np.zeros(segment_length - (len(audio) % segment_length), dtype=np.float32)\n        audio = np.concatenate([audio, padding])\n    \n    # Split into segments\n    for i in range(0, len(audio), segment_length):\n        segment = audio[i:i+segment_length]\n        # Only include segments that have actual audio (not just silence)\n        if np.abs(segment).max() > 0.01:  # Really small threshold\n            segments.append(segment)\n    \n    return segments\n\ndef extract_yamnet_embeddings(audio_data, yamnet_model):\n    scores, embeddings, spectrogram  = yamnet_model(audio_data)\n    \n    # YamNet returns a result per 0.96s (basically 1s) of audio\n    # As we are working with 5s windows, we have to take the mean of the results\n    return tf.reduce_mean(embeddings, axis=0)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-27T07:34:01.645037Z","iopub.execute_input":"2025-04-27T07:34:01.645302Z","iopub.status.idle":"2025-04-27T07:34:01.668286Z","shell.execute_reply.started":"2025-04-27T07:34:01.645284Z","shell.execute_reply":"2025-04-27T07:34:01.667287Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def prepare_dataset(data_dir):\n    all_files = []\n    species_sets = []  # List of sets containing species in each recording\n    \n    species_folders = [d for d in os.listdir(data_dir) if os.path.isdir(os.path.join(data_dir, d))]\n    \n    for species in species_folders:\n        species_path = os.path.join(data_dir, species)\n        audio_files = [os.path.join(species_path, f) for f in os.listdir(species_path) \n                       if f.endswith('.wav') or f.endswith('.mp3') or f.endswith('.ogg')]\n        \n        all_files.extend(audio_files)\n        species_sets.extend([{species}] * len(audio_files))\n    \n    # Create dataframe\n    df = pd.DataFrame({\n        'file_path': all_files,\n        'species_set': species_sets\n    })\n    \n    # Get all unique species\n    all_species = set()\n    for species_set in df['species_set']:\n        all_species.update(species_set)\n    all_species = sorted(list(all_species))\n    \n    # Create mapping dictionaries\n    species_to_idx = {species: i for i, species in enumerate(all_species)}\n    idx_to_species = {i: species for i, species in enumerate(all_species)}\n    \n    # Save mapping for later use\n    with open(os.path.join(OUTPUT_DIR, 'species_mapping.csv'), 'w', newline='') as f:\n        writer = csv.writer(f)\n        writer.writerow(['species', 'index'])\n        for species, idx in species_to_idx.items():\n            writer.writerow([species, idx])\n    \n    return df, species_to_idx, idx_to_species # Returns dataframe with file paths and labels.\n\n# Extract YAMNet embeddings for 5-second segments of all audio files, and make multi-hot encoded labels\ndef create_embeddings_dataset(df, yamnet_model, species_to_idx):\n\n    all_embeddings = []\n    all_labels = []\n    segment_file_mapping = []  # Track which file each segment came from\n    \n    for i, row in tqdm(df.iterrows(), total=len(df), desc=\"Extracting embeddings\"):\n        try:\n            # Load audio\n            audio = load_audio_file(row['file_path'])\n            \n            # Segment audio into 5-second chunks\n            segments = segment_audio(audio, SAMPLE_RATE)\n            \n            # Skip files with no valid segments\n            if not segments:\n                continue\n                \n            # Process each segment\n            for segment in segments:\n                # Extract embedding\n                embedding = extract_yamnet_embeddings(segment, yamnet_model)\n                all_embeddings.append(embedding.numpy())\n                \n                # Create multi-hot encoded label vector (for multi-label classification)\n                label_vector = np.zeros(len(species_to_idx))\n                for species in row['species_set']:\n                    label_vector[species_to_idx[species]] = 1\n                all_labels.append(label_vector)\n                \n                # Track which file this segment came from\n                segment_file_mapping.append(row['file_path'])\n        \n        except Exception as e:\n            tqdm.write(f\"Error processing {row['file_path']}: {e}\")\n            continue\n    \n    # Convert to numpy arrays\n    embeddings_array = np.array(all_embeddings)\n    labels_array = np.array(all_labels)\n    \n    # Create dataframe with segment info\n    segments_df = pd.DataFrame({\n        'file_path': segment_file_mapping\n    })\n    \n    # Save embeddings and labels to disk\n    np.save(os.path.join(OUTPUT_DIR, 'embeddings.npy'), embeddings_array)\n    np.save(os.path.join(OUTPUT_DIR, 'labels.npy'), labels_array)\n    segments_df.to_csv(os.path.join(OUTPUT_DIR, 'segments.csv'), index=False)\n    \n    return embeddings_array, labels_array, segments_df # Returns embeddings and multi-hot encoded labels.","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-27T07:34:01.669326Z","iopub.execute_input":"2025-04-27T07:34:01.669658Z","iopub.status.idle":"2025-04-27T07:34:01.694209Z","shell.execute_reply.started":"2025-04-27T07:34:01.669632Z","shell.execute_reply":"2025-04-27T07:34:01.692775Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Model (Helper) Function","metadata":{}},{"cell_type":"code","source":"def build_classifier_model(num_classes):\n    model = models.Sequential([\n        layers.Input(shape=INPUT_SHAPE), # Input from YamNet\n        layers.Dense(512, activation='relu'),\n        layers.Dropout(0.3),\n        layers.Dense(256, activation='relu'),\n        layers.Dropout(0.3),\n        layers.Dense(num_classes, activation='sigmoid')  # Sigmoid for probability per class\n    ])\n    \n    model.compile(\n        optimizer=tf.keras.optimizers.Adam(learning_rate=0.001),\n        loss='binary_crossentropy',  # For multi-label classification\n        metrics=['accuracy', tf.keras.metrics.AUC()]  # Include AUC metric\n    )\n    \n    return model\n\n\ndef train_and_evaluate_model(X_train, y_train, X_val, y_val, num_classes):\n    model = build_classifier_model(num_classes)\n    \n    # Create callbacks\n    early_stopping = tf.keras.callbacks.EarlyStopping(\n        monitor='val_loss',\n        patience=5,\n        restore_best_weights=True\n    )\n    \n    model_checkpoint = tf.keras.callbacks.ModelCheckpoint(\n        filepath=os.path.join(OUTPUT_DIR, 'best_model.keras'),\n        monitor='val_auc',\n        save_best_only=True\n    )\n    \n    # Train model\n    history = model.fit(\n        X_train, y_train,\n        validation_data=(X_val, y_val),\n        epochs=EPOCHS,\n        batch_size=BATCH_SIZE,\n        callbacks=[early_stopping, model_checkpoint]\n    )\n    \n    # Evaluate model\n    val_loss, val_accuracy, val_auc = model.evaluate(X_val, y_val)\n    print(f\"Validation loss: {val_loss:.4f}\")\n    print(f\"Validation accuracy: {val_accuracy:.4f}\")\n    print(f\"Validation AUC: {val_auc:.4f}\")\n   \n    return model\n\ndef evaluate_model(model, X_val, y_val, idx_to_species):\n    y_pred = model.predict(X_val)\n    \n    # Calculate per-class AUC\n    n_classes = y_val.shape[1]\n    auc_scores = []\n    \n    for i in range(n_classes):\n        # Only calculate AUC if there are positive examples\n        if np.sum(y_val[:, i]) > 0:\n            class_auc = roc_auc_score(y_val[:, i], y_pred[:, i])\n            auc_scores.append((idx_to_species[i], class_auc))\n            \n            # Plot precision-recall curve for this class\n            precision, recall, _ = precision_recall_curve(y_val[:, i], y_pred[:, i])\n            average_precision = average_precision_score(y_val[:, i], y_pred[:, i])\n    \n    # Calculate macro average AUC (the competition metric)\n    macro_auc = np.mean([auc for _, auc in auc_scores])\n    print(f\"Macro-averaged ROC-AUC: {macro_auc:.4f}\")\n    \n    # Save detailed AUC scores\n    auc_scores.sort(key=lambda x: x[1], reverse=True)\n    with open(os.path.join(OUTPUT_DIR, 'auc_scores.csv'), 'w', newline='') as f:\n        writer = csv.writer(f)\n        writer.writerow(['Species', 'AUC'])\n        for species, auc in auc_scores:\n            writer.writerow([species, auc])\n    \n    return macro_auc","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-27T07:34:01.696670Z","iopub.execute_input":"2025-04-27T07:34:01.697087Z","iopub.status.idle":"2025-04-27T07:34:01.723788Z","shell.execute_reply.started":"2025-04-27T07:34:01.697046Z","shell.execute_reply":"2025-04-27T07:34:01.722663Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Create inference model that processes audio in 5-second windows\ndef create_inference_model(yamnet_model, classifier_model):\n    class BirdSegmentClassifier(tf.keras.Model):\n        def __init__(self, yamnet_model, classifier_model):\n            super(BirdSegmentClassifier, self).__init__()\n            self.yamnet_model = yamnet_model\n            self.classifier_model = classifier_model\n            self.sample_rate = 16000\n        \n        def process_audio(self, audio):\n            # Segment the audio into 5-second chunks\n            segments = segment_audio(audio)\n            \n            all_predictions = []\n            \n            for segment in segments:\n                # Convert segment to tensor with correct shape for YamNet\n                segment_tensor = tf.convert_to_tensor(segment, dtype=tf.float32)\n                \n                # Extract embedding using YamNet\n                embedding = extract_yamnet_embeddings(segment_tensor, self.yamnet_model)\n                \n                # Reshape for classifier input (add batch dimension)\n                embedding = tf.expand_dims(embedding, 0)\n                \n                # Get predictions from our classifier\n                predictions = self.classifier_model(embedding)\n                all_predictions.append(predictions[0])\n            \n            # If we have segments, stack predictions for all segments\n            if all_predictions:\n                return tf.stack(all_predictions)\n            else:\n                # Return zeros if no valid segments\n                return tf.zeros((1, self.classifier_model.output_shape[-1]))\n        \n        def call(self, inputs):\n            return self.process_audio(inputs)\n    \n    inference_model = BirdSegmentClassifier(yamnet_model, classifier_model)\n    return inference_model\n\n# Function to prepare submission format\n# See https://www.kaggle.com/code/stefankahl/birdclef-2025-sample-submission\n# and https://www.kaggle.com/competitions/birdclef-2025/discussion/573921\ndef prepare_submission(test_audio_dir, inference_model, idx_to_species):\n    # Get sample submission to ensure format consistency\n    submission = pd.read_csv('/kaggle/input/birdclef-2025/sample_submission.csv')\n    species_columns = [col for col in submission.columns if col != 'row_id']\n    print(f\"Submission has {len(species_columns)} species columns\")\n    \n    all_results = []\n    \n    for file in tqdm(os.listdir(test_audio_dir), desc=\"Processing test files\"):\n        # Safety mechanism for README in the folder\n        if not file.endswith('.ogg'):\n            continue\n        \n        # Extract soundscape ID from filename (soundscape_xxxxx.ogg)\n        soundscape_id = file.split('_')[1].split('.')[0]\n        # print(f\"Soundscape ID: {soundscape_id}\")\n        \n        # Load audio at 16kHz, YamNet is trained on 16kHz\n        audio = load_audio_file(os.path.join(test_audio_dir, file))\n        sr = 16000\n        \n        # Process each 5-second segment\n        for i in range(0, 60, 5):  # 60 seconds max per file\n            start_sample = i * sr\n            end_sample = (i + 5) * sr\n            \n            if start_sample >= len(audio):\n                break\n                \n            chunk = audio[start_sample:end_sample]\n            \n            # Ensure chunk is the right length and pad if it is too short\n            if len(chunk) < 5 * sr:\n                chunk = np.pad(chunk, (0, 5 * sr - len(chunk)), 'constant')\n                                \n            embedding = extract_yamnet_embeddings(tf.convert_to_tensor(chunk, dtype=tf.float32), \n                                                 inference_model.yamnet_model)\n            \n            predictions = inference_model.classifier_model(tf.expand_dims(embedding, 0))[0].numpy()\n            \n            # Create row_id according to expected format\n            row_id = f'soundscape_{soundscape_id}_{i+5}'\n            row_data = {'row_id': row_id}\n            \n            # Dictionary for mapping species to their prediction values\n            species_predictions = {}\n            for class_idx, prob in enumerate(predictions):\n                if class_idx in idx_to_species:\n                    species = idx_to_species[class_idx]\n                    species_predictions[species] = float(prob)\n            \n            for col in species_columns:\n                row_data[col] = species_predictions.get(col, 0.0)\n            \n            all_results.append(row_data)\n                \n    \n    print(f\"\\nTotal results: {len(all_results)}\")\n    \n    # Create submission DataFrame with all necessary columns\n    if all_results:\n        final_submission = pd.DataFrame(all_results)\n        \n        # Make sure all columns from sample submission are present\n        for col in submission.columns:\n            if col not in final_submission.columns:\n                final_submission[col] = 0.0\n        \n        # Ensure the right order\n        final_submission = final_submission[submission.columns]\n        \n        # Save submission\n        final_submission.to_csv('submission.csv', index=False)\n        print(f\"Final submission shape: {final_submission.shape}\")\n    else:\n        final_submission = pd.DataFrame(columns=submission.columns)\n        final_submission.to_csv('submission.csv', index=False)\n        print(\"Created CSV with only headers.\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-27T07:36:46.636415Z","iopub.execute_input":"2025-04-27T07:36:46.636721Z","iopub.status.idle":"2025-04-27T07:36:46.653425Z","shell.execute_reply.started":"2025-04-27T07:36:46.636701Z","shell.execute_reply":"2025-04-27T07:36:46.652007Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Prediction functions","metadata":{}},{"cell_type":"code","source":"print(\"Loading YamNet\\n\")\nyamnet_model = hub.load('/kaggle/input/yamnet/tensorflow2/yamnet/1')\n\n# Check if we already have pre-computed embeddings, this is only for ensuring the submission/prediction\n# is of the correct format.\n# In the inference notebook we'll only do loading of these files and the model\nembedding_file = \"/kaggle/input/yamnet-training-files-needed/embeddings.npy\"\nlabels_file = \"/kaggle/input/yamnet-training-files-needed/labels.npy\"\nsegments_file = \"/kaggle/input/yamnet-training-files-needed/segments.csv\"\n\nprint(\"Loading pre-computed embeddings\\n\")\nembeddings = np.load(embedding_file)\nlabels = np.load(labels_file)\nsegments_df = pd.read_csv(segments_file)\n\n# Load species mappings\nspecies_mapping = pd.read_csv(\"/kaggle/input/yamnet-training-files-needed/species_mapping.csv\")\nspecies_to_idx = dict(zip(species_mapping['species'], species_mapping['index']))\nidx_to_species = dict(zip(species_mapping['index'], species_mapping['species']))\n\nprint(\"Loading model\\n\")\nclassifier_model = models.load_model(\"/kaggle/input/yamnetclassification-head/tensorflow2/default/1/best_model.keras\")\ninference_model = create_inference_model(yamnet_model, classifier_model)\n\n\nprint(\"Creating submission\\n\")\nprepare_submission(\"/kaggle/input/birdclef-2025/test_soundscapes\", inference_model, idx_to_species)\nprint(\"Done\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-27T07:36:53.009493Z","iopub.execute_input":"2025-04-27T07:36:53.009809Z","iopub.status.idle":"2025-04-27T07:37:28.981091Z","shell.execute_reply.started":"2025-04-27T07:36:53.009787Z","shell.execute_reply":"2025-04-27T07:37:28.980054Z"}},"outputs":[],"execution_count":null}]}