{"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"}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# Model training\n\nIn this notebook, we will train our first model and apply this model to a soundscape. We will keep the amount of training samples, species and soundscapes to a minimum to keep the execution time as short as possible. Remember, this is only a sample implementation, feel free to explore your own workflow.\n\nThese are the steps that we will cover:\n\n\n* select audio files we want to use for training  \n* extract spectrograms from those files and save them in a working directory  \n* load selected samples into a large in-memory dataset  \n* build a simple beginners CNN  \n* train the model  \n* apply the model to a selected soundscape and look at the results \n\n# 1. Settings and imports\n\nLet’s begin with imports and a few basic settings.","metadata":{}},{"cell_type":"code","source":"import os\n\nimport warnings\nwarnings.filterwarnings(action='ignore')\n\nimport pandas as pd\nimport librosa\nimport numpy as np\n\nfrom sklearn.utils import shuffle\nfrom PIL import Image\nfrom tqdm import tqdm\nimport matplotlib.pyplot as plt\n\nimport tensorflow as tf\n\n# Global vars\nRANDOM_SEED = 1337\nSAMPLE_RATE = 32000\nSIGNAL_LENGTH = 5 # seconds\nSPEC_SHAPE = (48, 128) # height x width\nFMIN = 500\nFMAX = 12500\nMAX_AUDIO_FILES = 1500","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","execution":{"iopub.status.busy":"2021-06-07T19:05:33.797269Z","iopub.execute_input":"2021-06-07T19:05:33.797591Z","iopub.status.idle":"2021-06-07T19:05:33.804475Z","shell.execute_reply.started":"2021-06-07T19:05:33.797563Z","shell.execute_reply":"2021-06-07T19:05:33.80349Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# 2. Data preparation\n\nThe training data for this competition contains tens of thousands of audio files for 397 species. That’s way too much for this tutorial, so we will limit our species selection to species that have at least 200 recordings with a rating of 4 or better.","metadata":{}},{"cell_type":"code","source":"# Code adapted from: \n# https://www.kaggle.com/frlemarchand/bird-song-classification-using-an-efficientnet\n# Make sure to check out the entire notebook.\n\n# Load metadata file\ntrain = pd.read_csv('../input/birdclef-2021/train_metadata.csv',)\n\n# Limit the number of training samples and classes\n# First, only use high quality samples\ntrain = train.query('rating>=4')\n\n# Second, assume that birds with the most training samples are also the most common\n# A species needs at least 200 recordings with a rating above 4 to be considered common\nbirds_count = {}\nfor bird_species, count in zip(train.primary_label.unique(), \n                               train.groupby('primary_label')['primary_label'].count().values):\n    birds_count[bird_species] = count\nmost_represented_birds = [key for key,value in birds_count.items() if value >= 200] \n\nTRAIN = train.query('primary_label in @most_represented_birds')\nLABELS = sorted(TRAIN.primary_label.unique())\n\n# Let's see how many species and samples we have left\nprint('NUMBER OF SPECIES IN TRAIN DATA:', len(LABELS))\nprint('NUMBER OF SAMPLES IN TRAIN DATA:', len(TRAIN))\nprint('LABELS:', most_represented_birds)","metadata":{"execution":{"iopub.status.busy":"2021-06-07T19:05:33.806559Z","iopub.execute_input":"2021-06-07T19:05:33.806897Z","iopub.status.idle":"2021-06-07T19:05:34.084304Z","shell.execute_reply.started":"2021-06-07T19:05:33.80686Z","shell.execute_reply":"2021-06-07T19:05:34.083361Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Ok, that leaves us with 27 species and 8,548 audio files. The species list includes very common species like the House Sparrow (houspa), Blue Jay (blujay), or Song Sparrow (sonspa). This is not a bad selection to start experimenting.\n\n# 3. Extract training samples\n\nWe need to define a function that extracts spectrograms for a given audio file. This function needs to load a file with Librosa (we only use the first 15 seconds in this tutorial), extract mel spectrograms and save each spectrogram as PNG image in a working directory for later access.","metadata":{}},{"cell_type":"code","source":"# Shuffle the training data and limit the number of audio files to MAX_AUDIO_FILES\nTRAIN = shuffle(TRAIN, random_state=RANDOM_SEED)[:MAX_AUDIO_FILES]\n\n# Define a function that splits an audio file, \n# extracts spectrograms and saves them in a working directory\ndef get_spectrograms(filepath, primary_label, output_dir):\n    \n    # Open the file with librosa (limited to the first 15 seconds)\n    sig, rate = librosa.load(filepath, sr=SAMPLE_RATE, offset=None, duration=15)\n    \n    # Split signal into five second chunks\n    sig_splits = []\n    for i in range(0, len(sig), int(SIGNAL_LENGTH * SAMPLE_RATE)):\n        split = sig[i:i + int(SIGNAL_LENGTH * SAMPLE_RATE)]\n\n        # End of signal?\n        if len(split) < int(SIGNAL_LENGTH * SAMPLE_RATE):\n            break\n        \n        sig_splits.append(split)\n        \n    # Extract mel spectrograms for each audio chunk\n    s_cnt = 0\n    saved_samples = []\n    for chunk in sig_splits:\n        \n        hop_length = int(SIGNAL_LENGTH * SAMPLE_RATE / (SPEC_SHAPE[1] - 1))\n        mel_spec = librosa.feature.melspectrogram(y=chunk, \n                                                  sr=SAMPLE_RATE, \n                                                  n_fft=1024, \n                                                  hop_length=hop_length, \n                                                  n_mels=SPEC_SHAPE[0], \n                                                  fmin=FMIN, \n                                                  fmax=FMAX)\n    \n        mel_spec = librosa.power_to_db(mel_spec, ref=np.max) \n        \n        # Normalize\n        mel_spec -= mel_spec.min()\n        mel_spec /= mel_spec.max()\n        \n        # Save as image file\n        save_dir = os.path.join(output_dir, primary_label)\n        if not os.path.exists(save_dir):\n            os.makedirs(save_dir)\n        save_path = os.path.join(save_dir, filepath.rsplit(os.sep, 1)[-1].rsplit('.', 1)[0] + \n                                 '_' + str(s_cnt) + '.png')\n        im = Image.fromarray(mel_spec * 255.0).convert(\"L\")\n        im.save(save_path)\n        \n        saved_samples.append(save_path)\n        s_cnt += 1\n        \n        \n    return saved_samples\n\nprint('FINAL NUMBER OF AUDIO FILES IN TRAINING DATA:', len(TRAIN))","metadata":{"execution":{"iopub.status.busy":"2021-06-07T19:05:34.0862Z","iopub.execute_input":"2021-06-07T19:05:34.086717Z","iopub.status.idle":"2021-06-07T19:05:34.103506Z","shell.execute_reply.started":"2021-06-07T19:05:34.08668Z","shell.execute_reply":"2021-06-07T19:05:34.102491Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Ok, we have 1,500 audio files that cover 27 species, let's extract spectrograms (this might take a while).","metadata":{}},{"cell_type":"code","source":"# Parse audio files and extract training samples\ninput_dir = '../input/birdclef-2021/train_short_audio/'\noutput_dir = '../working/melspectrogram_dataset/'\nsamples = []\nwith tqdm(total=len(TRAIN)) as pbar:\n    for idx, row in TRAIN.iterrows():\n        pbar.update(1)\n        \n        if row.primary_label in most_represented_birds:\n            audio_file_path = os.path.join(input_dir, row.primary_label, row.filename)\n            samples += get_spectrograms(audio_file_path, row.primary_label, output_dir)\n            \nTRAIN_SPECS = shuffle(samples, random_state=RANDOM_SEED)\nprint('SUCCESSFULLY EXTRACTED {} SPECTROGRAMS'.format(len(TRAIN_SPECS)))","metadata":{"execution":{"iopub.status.busy":"2021-06-07T19:05:34.106029Z","iopub.execute_input":"2021-06-07T19:05:34.106755Z","iopub.status.idle":"2021-06-07T19:07:03.44395Z","shell.execute_reply.started":"2021-06-07T19:05:34.106719Z","shell.execute_reply":"2021-06-07T19:07:03.442958Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Alright, we got 4,157 training spectrograms. That's roughly 150 for each species which is not too bad.\n\nLet's make sure the spectrograms look right and show the first 12.","metadata":{}},{"cell_type":"code","source":"# Plot the first 12 spectrograms of TRAIN_SPECS\nplt.figure(figsize=(15, 7))\nfor i in range(12):\n    spec = Image.open(TRAIN_SPECS[i])\n    plt.subplot(3, 4, i + 1)\n    plt.title(TRAIN_SPECS[i].split(os.sep)[-1])\n    plt.imshow(spec, origin='lower')","metadata":{"execution":{"iopub.status.busy":"2021-06-07T19:07:03.445723Z","iopub.execute_input":"2021-06-07T19:07:03.446385Z","iopub.status.idle":"2021-06-07T19:07:04.811265Z","shell.execute_reply.started":"2021-06-07T19:07:03.446341Z","shell.execute_reply":"2021-06-07T19:07:04.810359Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Nice! These are good samples. Notice how some of them only contain a fraction of a bird call? That's an issue we won't deal with in this tutorial. We will simply ignore the fact that samples might not contain any bird sounds.\n\n# 4. Load training samples\n\nFor now, our spectrograms reside in a working directory. If we want to train a model, we have to load them into memory. Yet, with potentially hundreds of thousands of extracted spectrograms, an in-memory dataset is not a good idea. But for now, loading samples from disk and combining them into a large NumPy array is fine. It’s the easiest way to use these data for training with Keras.","metadata":{}},{"cell_type":"code","source":"# Parse all samples and add spectrograms into train data, primary_labels into label data\ntrain_specs, train_labels = [], []\nwith tqdm(total=len(TRAIN_SPECS)) as pbar:\n    for path in TRAIN_SPECS:\n        pbar.update(1)\n\n        # Open image\n        spec = Image.open(path)\n\n        # Convert to numpy array\n        spec = np.array(spec, dtype='float32')\n        \n        # Normalize between 0.0 and 1.0\n        # and exclude samples with nan \n        spec -= spec.min()\n        spec /= spec.max()\n        if not spec.max() == 1.0 or not spec.min() == 0.0:\n            continue\n\n        # Add channel axis to 2D array\n        spec = np.expand_dims(spec, -1)\n\n        # Add new dimension for batch size\n        spec = np.expand_dims(spec, 0)\n\n        # Add to train data\n        if len(train_specs) == 0:\n            train_specs = spec\n        else:\n            train_specs = np.vstack((train_specs, spec))\n\n        # Add to label data\n        target = np.zeros((len(LABELS)), dtype='float32')\n        bird = path.split(os.sep)[-2]\n        target[LABELS.index(bird)] = 1.0\n        if len(train_labels) == 0:\n            train_labels = target\n        else:\n            train_labels = np.vstack((train_labels, target))","metadata":{"execution":{"iopub.status.busy":"2021-06-07T19:07:04.81268Z","iopub.execute_input":"2021-06-07T19:07:04.813246Z","iopub.status.idle":"2021-06-07T19:08:23.426463Z","shell.execute_reply.started":"2021-06-07T19:07:04.813195Z","shell.execute_reply":"2021-06-07T19:08:23.425352Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# 5. Build a simple model\n\nAlright, our dataset is ready, now we need to define a model architecture. In this tutorial, we’ll use a very simplistic, AlexNet-like design with four convolutional layers and three dense layers. It might make sense to choose an off-the-shelve TF model that was pre-trained on audio data, but we would need to adjust the inputs (i.e., the resolution of our spectrograms) to fit the external model. So we keep it simple and build our own model.","metadata":{}},{"cell_type":"code","source":"# Make sure your experiments are reproducible\ntf.random.set_seed(RANDOM_SEED)\n\n# Build a simple model as a sequence of  convolutional blocks.\n# Each block has the sequence CONV --> RELU --> BNORM --> MAXPOOL.\n# Finally, perform global average pooling and add 2 dense layers.\n# The last layer is our classification layer and is softmax activated.\n# (Well it's a multi-label task so sigmoid might actually be a better choice)\nmodel = tf.keras.Sequential([\n    \n    # First conv block\n    tf.keras.layers.Conv2D(16, (3, 3), activation='relu', \n                           input_shape=(SPEC_SHAPE[0], SPEC_SHAPE[1], 1)),\n    tf.keras.layers.BatchNormalization(),\n    tf.keras.layers.MaxPooling2D((2, 2)),\n    \n    # Second conv block\n    tf.keras.layers.Conv2D(32, (3, 3), activation='relu'),\n    tf.keras.layers.BatchNormalization(),\n    tf.keras.layers.MaxPooling2D((2, 2)), \n    \n    # Third conv block\n    tf.keras.layers.Conv2D(64, (3, 3), activation='relu'),\n    tf.keras.layers.BatchNormalization(),\n    tf.keras.layers.MaxPooling2D((2, 2)), \n    \n    # Fourth conv block\n    tf.keras.layers.Conv2D(128, (3, 3), activation='relu'),\n    tf.keras.layers.BatchNormalization(),\n    tf.keras.layers.MaxPooling2D((2, 2)),\n    \n    # Global pooling instead of flatten()\n    tf.keras.layers.GlobalAveragePooling2D(), \n    \n    # Dense block\n    tf.keras.layers.Dense(256, activation='relu'),   \n    tf.keras.layers.Dropout(0.5),  \n    tf.keras.layers.Dense(256, activation='relu'),   \n    tf.keras.layers.Dropout(0.5),\n    \n    # Classification layer\n    tf.keras.layers.Dense(len(LABELS), activation='softmax')\n])\nprint('MODEL HAS {} PARAMETERS.'.format(model.count_params()))","metadata":{"execution":{"iopub.status.busy":"2021-06-07T19:08:23.427864Z","iopub.execute_input":"2021-06-07T19:08:23.428208Z","iopub.status.idle":"2021-06-07T19:08:23.554729Z","shell.execute_reply.started":"2021-06-07T19:08:23.428171Z","shell.execute_reply":"2021-06-07T19:08:23.553784Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"This is not a huge CNN, it only has ~200,000 parameters. Yet, we also only have a very small dataset with just 27 classes.\n\nNext, we need to specify an optimzer, initial learning rate, a loss function and a metric.","metadata":{}},{"cell_type":"code","source":"# Compile the model and specify optimizer, loss and metric\nmodel.compile(optimizer=tf.keras.optimizers.Adam(lr=0.001),\n              loss=tf.keras.losses.CategoricalCrossentropy(label_smoothing=0.01),\n              metrics=['accuracy'])","metadata":{"execution":{"iopub.status.busy":"2021-06-07T19:08:23.555963Z","iopub.execute_input":"2021-06-07T19:08:23.55646Z","iopub.status.idle":"2021-06-07T19:08:23.568076Z","shell.execute_reply.started":"2021-06-07T19:08:23.556422Z","shell.execute_reply":"2021-06-07T19:08:23.567278Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Callbacks make our life easier, the three that we're adding will take care of saving the best checkpoint, they will reduce the learning rate whenever the training process stalls, and they will stop the training if the model is overfitting.","metadata":{}},{"cell_type":"code","source":"# Add callbacks to reduce the learning rate if needed, early stopping, and checkpoint saving\ncallbacks = [tf.keras.callbacks.ReduceLROnPlateau(monitor='val_loss', \n                                                  patience=2, \n                                                  verbose=1, \n                                                  factor=0.5),\n             tf.keras.callbacks.EarlyStopping(monitor='val_loss', \n                                              verbose=1,\n                                              patience=5),\n             tf.keras.callbacks.ModelCheckpoint(filepath='best_model.h5', \n                                                monitor='val_loss',\n                                                verbose=0,\n                                                save_best_only=True)]","metadata":{"execution":{"iopub.status.busy":"2021-06-07T19:08:23.569604Z","iopub.execute_input":"2021-06-07T19:08:23.570135Z","iopub.status.idle":"2021-06-07T19:08:23.577409Z","shell.execute_reply.started":"2021-06-07T19:08:23.570098Z","shell.execute_reply":"2021-06-07T19:08:23.576544Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Here we go, everything is in place, let's train a model. We'll use 20% of our training data for validation and we'll stop after 25 epochs.","metadata":{}},{"cell_type":"code","source":"# Let's train the model for a few epochs\nmodel.fit(train_specs, \n          train_labels,\n          batch_size=32,\n          validation_split=0.2,\n          callbacks=callbacks,\n          epochs=25)","metadata":{"execution":{"iopub.status.busy":"2021-06-07T19:08:23.578857Z","iopub.execute_input":"2021-06-07T19:08:23.579284Z","iopub.status.idle":"2021-06-07T19:08:42.602398Z","shell.execute_reply.started":"2021-06-07T19:08:23.579245Z","shell.execute_reply":"2021-06-07T19:08:42.601673Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Not too bad, we got into the 60s of our validation accuracy. But remember, we're training on focal recordings and validate on focal recordings. The scores might not tell us much about how well we will perform on soundscape data.\n\nWe'll have to check ourselves, luckily, we have some validation soundscapes.\n\n# 6. Soundscape analysis\n\nIn this tutorial, we will simply pick a soundscape from the training data, but the overall process can easily be automated and then applied to all soundscape files. And again, we have to load a file with Librosa, extract spectrograms for 5-second chunks, pass each chunk through the model and eventually assign a label to the 5-second audio chunk.\n\nLet's use a soundscape that actually contains some of the species that we trained our model for. The file \"28933_SSW_20170408.ogg\" seems to contain a lot of Song Sparrow (sonspa) vocalizations, let's try this one then.","metadata":{}},{"cell_type":"code","source":"# Load the best checkpoint\nmodel = tf.keras.models.load_model('best_model.h5')\n\n# First, get a list of soundscape files to process.\n# We'll use the test_soundscape directory if it contains \"ogg\" files\n# (which it only does when submitting the notebook), \n# otherwise we'll use the train_soundscape folder to make predictions.\ndef list_files(path):\n    return [os.path.join(path, f) for f in os.listdir(path) if f.rsplit('.', 1)[-1] in ['ogg']]\n\nprint(os.listdir('../input/birdclef-2021/test_soundscapes'))\nprint(os.listdir('../input/birdclef-2021/test_soundscapes')[-1])\n\ntest_audio = list_files('../input/birdclef-2021/test_soundscapes')\nif len(test_audio) == 0:\n    print(\"EVAL TEST SET NOT FOUND\")\n    print(\"USING TRAIN SET INSTEAD\")\n    test_audio = [list_files('../input/birdclef-2021/train_soundscapes')[0]]\n    if len(test_audio) == 0:\n        print(\"TRAIN SET NOT FOUND\")\n\nelse:\n    print(\"Found test set!\")\n\nprint('{} FILES IN SET.'.format(len(test_audio)))\n\n# Load the best checkpoint\nmodel = tf.keras.models.load_model('best_model.h5')\n\n# This is where we will store our results\npred = {'row_id': [], 'birds': []}\n\n# Analyze each soundscape recording\nfor i, path in enumerate(test_audio):\n    print(\"Soundscape {}/{}\".format(i, len(test_audio)))\n\n    # Open file with Librosa\n    sig, rate = librosa.load(path, sr=SAMPLE_RATE)\n    print(\"Length of file: {} seconds\".format(len(sig)/SAMPLE_RATE))\n\n    # Split file into 5-second chunks\n    # Can be made into function\n    sig_splits = []\n    for i in range(0, len(sig), int(SIGNAL_LENGTH * SAMPLE_RATE)):\n        split = sig[i:i + int(SIGNAL_LENGTH * SAMPLE_RATE)]\n\n        # End of signal?\n        if len(split) < int(SIGNAL_LENGTH * SAMPLE_RATE):\n            break\n\n        sig_splits.append(split)\n\n    expected_splits = len(sig) / SAMPLE_RATE / 5\n    print(\"Made {} splits (Expected is {})\".format(len(sig_splits), expected_splits))\n\n    # Extract spectrogram for each chunk\n    seconds, scnt = 0, 0\n    for i, chunk in enumerate(sig_splits):\n        if i % 30 == 0:\n            print(\"chunk {} / {}\".format(i, len(sig_splits)))\n        # Keep track of the end time of each chunk\n        seconds += 5\n\n        # Get the spectrogram\n        hop_length = int(SIGNAL_LENGTH * SAMPLE_RATE / (SPEC_SHAPE[1] - 1))\n        mel_spec = librosa.feature.melspectrogram(y=chunk, \n                                                  sr=SAMPLE_RATE, \n                                                  n_fft=1024, \n                                                  hop_length=hop_length, \n                                                  n_mels=SPEC_SHAPE[0], \n                                                  fmin=FMIN, \n                                                  fmax=FMAX)\n\n        mel_spec = librosa.power_to_db(mel_spec, ref=np.max) \n\n        # Normalize to match the value range we used during training.\n        # That's something you should always double check!\n        mel_spec -= mel_spec.min()\n        mel_spec /= mel_spec.max()\n\n        # Add channel axis to 2D array\n        mel_spec = np.expand_dims(mel_spec, -1)\n\n        # Add new dimension for batch size\n        mel_spec = np.expand_dims(mel_spec, 0)\n\n        # Predict on spectrogram\n        p = model.predict(mel_spec)[0]\n\n        # Get highest scoring species\n        idx = p.argmax()\n        #print(\"idx: {}\".format(idx))\n        species = LABELS[idx]\n        score = p[idx]\n\n        # Prepare submission entry\n        # Get row_id and birds and store result\n        # (maybe using a post-filter based on location)\n        fileinfo = path.split(os.sep)[-1].rsplit('.', 1)[0].split('_') # From BirdCLEF2021: Sample submission\n        row_id = fileinfo[0] + '_'  + fileinfo[1] + '_'  + str(seconds) # From BirdCLEF2021: Sample submission\n\n        # From BirdCLEF2021: Model Training\n        #pred['row_id'].append(path.split(os.sep)[-1].rsplit('_', 1)[0] + \n        #                  '_' + str(seconds)) \n        pred['row_id'].append(row_id) # From BirdCLEF2021: Sample submission\n\n        # Decide if it's a \"nocall\" or a species by applying a threshold\n        if score > 0.25:\n            pred['birds'].append(species)\n            scnt += 1\n        else:\n            pred['birds'].append('nocall')\n\n    results = pd.DataFrame(pred, columns = ['row_id', 'birds'])\n    \n# Convert our results to csv\nresults.to_csv(\"../working/submission.csv\", index=False)\nresults.to_csv(\"submission.csv\", index=False)","metadata":{"execution":{"iopub.status.busy":"2021-06-07T19:08:42.60539Z","iopub.execute_input":"2021-06-07T19:08:42.605659Z","iopub.status.idle":"2021-06-07T19:08:48.442571Z","shell.execute_reply.started":"2021-06-07T19:08:42.605632Z","shell.execute_reply":"2021-06-07T19:08:48.441733Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Ok, we found a few bird species with a score above the threshold. Let's look at the results and see how well we're actually doing.","metadata":{}},{"cell_type":"markdown","source":"Ok, that's not too bad. We actually got some of these Song Sparrow (sonspa) vocalizations. Well, and we missed others... We also didn't detect the Northern Cardinal (norcar) and Red-winged Blackbird (rewbla) even though we had them in our training data.\n\nThis is a good example for the difficulties we're facing when analyzing soundscapes. Focal recordings as training data can be misleading and soundscapes have much higher noise levels (and also contain very faint bird calls).\n\nNow it's your turn to find better strategies to cope with this shift in acoustic domains. Please don't hesitate to leave a comment or start a new forum thread if you have any questions.","metadata":{}}]}