Preprocessing audio samples with OpenSoundscape¶

Preprocessors in OpenSoundscape perform all of the preprocessing steps from loading a file from disk, up to providing a sample to the machine learning algorithm for training or prediction. They are designed to be flexible and customizable. These classes are used internally by classes such as opensoundscape.torch.models.cnn.CNN when (a) training a machine learning model in OpenSoundscape, or (b) making predictions with a machine learning model in OpenSoundscape.

Datasets are PyTorch’s way of handling a list of inputs to preprocess. In OpenSoundscape, there are two built-in classes (AudioFileDataset and AudioSplittingDataset) which use a Preprocessor to generate samples from a list of file paths.

While the CNN class in OpenSoundscape contains a default Preprocessor, you may want to modify or create your own Preprocessor depending on the specific way you wish to generate samples. Preprocessors are designed to be flexible and modular, so that each step of the preprocessing pipeline can be modified or removed. This notebook demonstrates:

• preparation of audio data to be used by a preprocessor
• how “Actions” are strung together in a Preprocessor to define how samples are generated
• modifying the parameters of actions
• turning Actions on and off
• modifying the order and contents of a Preprocessor
• use of the SpectrogramPreprocessor class, including examples of:
• modifying audio and spectrogram parameters
• changing the output image shape
• changing the output type
• turning augmentation on and off
• modifying augmentation parameters
• using the “overlay” augmentation
• writing custom preprocessors and actions

it also uses the Dataset classes to demonstrate - how to load one sample per file path - how to load long audio files as a series of shorter clips

First, import some packages.

[1]:

import warnings

[2]:

# Preprocessor classes are used to load, transform, and augment audio samples for use in a machine learing model
from opensoundscape.preprocess.preprocessors import SpectrogramPreprocessor
from opensoundscape.torch.datasets import AudioFileDataset, AudioSplittingDataset

# helper function for displaying a sample as an image
from opensoundscape.preprocess.utils import show_tensor, show_tensor_grid

#other utilities and packages
import torch
import pandas as pd
from pathlib import Path
import numpy as np
import random
import subprocess


Set up plotting

[3]:

#set up plotting
from matplotlib import pyplot as plt
plt.rcParams['figure.figsize']=[15,5] #for large visuals
%config InlineBackend.figure_format = 'retina'


Set manual seeds for pytorch and python. These ensure the training results are reproducible. You probably don’t want to do this when you actually train your model, but it’s useful for debugging.

[4]:

torch.manual_seed(0)
np.random.seed(0)
random.seed(0)


Preparing audio data¶

The Kitzes Lab has created a small labeled dataset of short clips of American Woodcock vocalizations. You have two options for obtaining the folder of data, called woodcock_labeled_data:

1. Run the following cell to download this small dataset. These commands require you to have tar installed on your computer, as they will download and unzip a compressed file in .tar.gz format.
2. Download a .zip version of the files by clicking here. You will have to unzip this folder and place the unzipped folder in the same folder that this notebook is in.

Note: Once you have the data, you do not need to run this cell again.

[5]:

subprocess.run(['curl','https://pitt.box.com/shared/static/79fi7d715dulcldsy6uogz02rsn5uesd.gz','-L', '-o','woodcock_labeled_data.tar.gz']) # Download the data
subprocess.run(["rm", "woodcock_labeled_data.tar.gz"]) # Remove the file after its contents are unzipped

  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0
0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0
100     7    0     7    0     0      7      0 --:--:-- --:--:-- --:--:--  7000
100 9499k  100 9499k    0     0   133k      0  0:01:11  0:01:11 --:--:--  115k

[5]:

CompletedProcess(args=['rm', 'woodcock_labeled_data.tar.gz'], returncode=0)


Load dataframe of files and labels¶

We need a dataframe with file paths in the index, so we manipulate the included one_hot_labels.csv slightly:

[6]:

# load one-hot labels dataframe

# prepend the folder location to the file paths
labels.index = pd.Series(labels.index).apply(lambda f: './woodcock_labeled_data/'+f)

#inspect

[6]:

present absent
file
./woodcock_labeled_data/d4c40b6066b489518f8da83af1ee4984.wav 1 0
./woodcock_labeled_data/79678c979ebb880d5ed6d56f26ba69ff.wav 1 0
./woodcock_labeled_data/49890077267b569e142440fa39b3041c.wav 1 0
./woodcock_labeled_data/0c453a87185d8c7ce05c5c5ac5d525dc.wav 1 0

Intro to Preprocessors¶

Preprocessors prepare samples for use by machine learning algorithms by performing a sequential procedure on each sample, like a recipe. The procedure is defined by a Pipeline which contains a sequential set of steps called Actions. There are 3 important characteristics of Preprocessors and Actions:

• [1] A Preprocessor has a pipeline which defines a list of Actions to perform on each sample
• [2] Actions contain parameters that modify their behavior in the attribute .params. You can modify parameter values directly or use the action’s .set() method to change parameter values.
• [3] Preprocessing can be performed with or without augmentation. The Preprocessor’s .bypass_augmentations boolean variable will determine whether Actions in the pipeline with attribute .is_augmentation==True are performed or bypassed
• [3] SpecPreprocessor (the default Preprocessor class) loads audio in two distinct modes: (a) loading one sample per file, and (b) spliting files into clips, and creating a sample from each clip. You can see examples of each mode below (tldr: files are split into clips if the preprocessor’s .clip_times_df is a dataframe specifying desired clip sample paths and times). By default, OpenSoundscape’s CNN class loads one sample per file during training and splits files into clips during prediction.

In this notebook, you will see how to edit, add, remove, and bypass Actions in the pipeline to modify the Preprocessing procedure.

The CNN class in OpenSoundscape has an internal Preprocessor object which it use to generate samples during training, validation, and prediction. We can modify or overwrite the cnn model’s preprocessor object if we want to change how it generates samples.

The starting point for most preprocessors will be the SpecPreprocessor class, which loads audio files, creates spectrograms from the audio, performs various augmentations, and returns a pytorch Tensor.

Initialize preprocessor¶

We need to tell the preprocessor the duration (in seconds) of each sample it should create.

[7]:

pre = SpectrogramPreprocessor(sample_duration=2.0)


Initialize a Dataset¶

A Dataset pairs a set of samples (possibly including labels) with a Preprocessor

The Dataset draws samples from it’s .df attribute which must be a very specific dataframe:

• the index of the dataframe provides paths to audio samples
• the columns are the class names
• the values are 0 (absent/False) or 1 (present/True) for each sample and each class.

For example, we’ve set up the labels dataframe with files as the index and classes as the columns, so we can use it to make an instance of SpecPreprocessor:

[8]:

dataset = AudioFileDataset(labels,pre)


Generate a sample from a Dataset¶

We can ask a dataset for a specific sample using its numeric index, like accessing an element of a list. Each sample is a dictionary with two keys: ‘X’, the Tensor of the sample, and ‘y’, the Tensor of labels of the sample. The shape of ‘X’ is [channels, height, width] and the shape of ‘y’ is [number of classes].

[9]:

dataset[0] #loads and preprocesses the sample at row 0 of dataset.df

[9]:

{'X': tensor([[[ 0.0000,  0.0000,  0.0000,  ..., -0.3139, -0.4861, -0.4208],
[ 0.0000,  0.0000,  0.0000,  ..., -0.3800, -0.3729, -0.4864],
[ 0.0000,  0.0000,  0.0000,  ..., -0.4506, -0.3056, -0.5758],
...,
[ 0.0000,  0.0000,  0.0000,  ...,  0.4784,  0.4597,  0.3293],
[ 0.0000,  0.0000,  0.0000,  ...,  0.0000,  0.0000,  0.0000],
[ 0.0000,  0.0000,  0.0000,  ...,  0.0000,  0.0000,  0.0000]],

[[ 0.0000,  0.0000,  0.0000,  ..., -0.3006, -0.4739, -0.4238],
[ 0.0000,  0.0000,  0.0000,  ..., -0.4015, -0.3679, -0.4939],
[ 0.0000,  0.0000,  0.0000,  ..., -0.4587, -0.3104, -0.5777],
...,
[ 0.0000,  0.0000,  0.0000,  ...,  0.4656,  0.4625,  0.3283],
[ 0.0000,  0.0000,  0.0000,  ...,  0.0000,  0.0000,  0.0000],
[ 0.0000,  0.0000,  0.0000,  ...,  0.0000,  0.0000,  0.0000]],

[[ 0.0000,  0.0000,  0.0000,  ..., -0.3303, -0.4976, -0.3977],
[ 0.0000,  0.0000,  0.0000,  ..., -0.3992, -0.3732, -0.4985],
[ 0.0000,  0.0000,  0.0000,  ..., -0.4614, -0.2974, -0.5799],
...,
[ 0.0000,  0.0000,  0.0000,  ...,  0.4731,  0.4623,  0.3363],
[ 0.0000,  0.0000,  0.0000,  ...,  0.0000,  0.0000,  0.0000],
[ 0.0000,  0.0000,  0.0000,  ...,  0.0000,  0.0000,  0.0000]]]),
'y': tensor([1, 0])}


Visualize multiple samples¶

Using a helper function, we can easily visualze a set of samples on a grid. We highly recommend inspecting your preprocessed samples in this way before training or predicting with a machine learning model. By inspecting the samples, you can confirm that your labeled data is reasonable and that the preprocessing is representing your samples in a reasonable way.

[10]:

from opensoundscape.preprocess.utils import show_tensor_grid

pre = SpectrogramPreprocessor(sample_duration=2.0)
dataset = AudioFileDataset(labels,pre)

tensors = [dataset[i]['X'] for i in range(9)]
sample_labels = [dataset[i]['y'] for i in range(9)]

_ = show_tensor_grid(tensors,3,labels=sample_labels)

/Users/SML161/miniconda3/envs/opso_dev/lib/python3.9/site-packages/matplotlib_inline/config.py:68: DeprecationWarning: InlineBackend._figure_format_changed is deprecated in traitlets 4.1: use @observe and @unobserve instead.
def _figure_format_changed(self, name, old, new):
Clipping input data to the valid range for imshow with RGB data ([0..1] for floats or [0..255] for integers).
Clipping input data to the valid range for imshow with RGB data ([0..1] for floats or [0..255] for integers).
Clipping input data to the valid range for imshow with RGB data ([0..1] for floats or [0..255] for integers).


let’s repeat the exercise of inspecting preprocessed samples, this time without augmentation

[11]:

dataset.bypass_augmentations = True

tensors = [dataset[i]['X'] for i in range(9)]
sample_labels = [dataset[i]['y'] for i in range(9)]

_ = show_tensor_grid(tensors,3,labels=sample_labels)


during machine learning tasks with Pytorch, a DataLoader is often used on top of a Dataset to “batch” samples - that is, to prepare multiple samples at once. A batch returned by a DataLoader will have an extra leading dimension for both ‘X’ and ‘y’; for instance, a batch_size of 16 would produce ‘X’ withs shape [16, 3, 224, 224] for 3-channel 224x224 tensors and ‘y’ with shape [16, 5] if the labels contain 5 classes (columns). OpenSoundscape uses DataLoaders internally to create batches of samples during CNN training and prediction.

Subset samples from a Dataset¶

Preprocessors allow you to select a subset of samples using sample() and head() methods (like Pandas DataFrames).

(note that these methods subset files from the index, they do not subset individual clips from files)

[12]:

len(dataset)

[12]:

29


Select the first 10 samples (non-random)

[13]:

len(dataset.head(10))

[13]:

10


Randomly select an absolute number of samples

[14]:

len(dataset.sample(n=10))

[14]:

10


Randomly select a fraction of samples

[15]:

len(dataset.sample(frac=0.5))

[15]:

14


When preprocessing should result in many fixed-length samples per input file, instead of one sample per file, we use AudioSplittingDataset instead of AudioFileDataset. This dataset can be customized with parameters for:

• fractional overlap between consecutive samples
• how to handle remaining audio at the end of a file (if it is shorter than the desired sample duration)

The CNN.predict() function uses AudioSplittingDataset internally, so that the user can specify long audio file paths and get back predictions on fixed-length clips. (If one sample per file is desired, you can pass the argument split_files_into_clips=False to CNN.predict )

Here’s an example of how to use AudioSplittingDataset to create several samples from a long audio file:

(Note that you never have to manually create AudioSplittingDataset or AudioFileDataset objects to train and predict with the CNN class, they are created internally.)

[16]:

prediction_df = pd.DataFrame(index=['./woodcock_labeled_data/field_data/60s_field_data_sample_1.wav'])

[17]:

pre = SpectrogramPreprocessor(sample_duration=2.0)
splitting_dataset = AudioSplittingDataset(prediction_df,pre,overlap_fraction=0.5)
splitting_dataset.bypass_augmentations = True

#get the first 9 samples and plot them
tensors = [splitting_dataset[i]['X'] for i in range(9)]

_ = show_tensor_grid(tensors,3)


Pipelines and actions¶

Each Preprocessor class has a pipeline which is an ordered set of operations that are performed on each sample, in the form of a pandas.Series object. Each element of the series is an object of class Action (or one of its subclasses) and represents a transformation on the sample.

The preprocessor’s Pipeline is the ordered list of Actions that the preprocessor performs on each sample.

• The Pipeline is stored in the preprocessor.pipeline attribute.
• You can modify the contents or order of Preprocessor Actions by overwriting the preprocessor’s .pipeline attribute. When you modify this attribute, you must provide pd.Series with elements name:Action, where each Action is an instance of a class that sub-classes opensoundscape.preprocess.BaseAction.

Let’s Inspect the current pipeline of our preprocessor.

[18]:

# inspect the current pipeline (ordered sequence of Actions to take)
preprocessor = SpectrogramPreprocessor(sample_duration=2)
preprocessor.pipeline

[18]:

load_audio           Action calling <bound method Audio.from_file o...
random_trim_audio    Augmentation Action calling <function trim_aud...
trim_audio           Action calling <function trim_audio at 0x17d91...
to_spec              Action calling <bound method Spectrogram.from_...
bandpass             Action calling <function Spectrogram.bandpass ...
to_img               Action calling <function Spectrogram.to_image ...
time_mask            Augmentation Action calling <function time_mas...
frequency_mask       Augmentation Action calling <function frequenc...
add_noise            Augmentation Action calling <function tensor_a...
rescale              Action calling <function scale_tensor at 0x17d...
random_affine        Augmentation Action calling <function torch_ra...
dtype: object


Each element of the preprocessor’s pipeline (a pd.Series) contains a name (string) and an action (Action)

• Each Action takes a sample (and its labels), performs some transformation to them, and returns the sample (and its labels).

• You can generate an Action based on a function like this : Action(fn=my_function, other parameters…). The function you pass (my_function in this case) must expect the sample as the first argument. It can then take additional parameters. For instance, if we define the function:

def multiply(x,n):
return x*n


then we can create an action to multiply by 3 with action=Action(fn=multiply,n=3)

• Any customizable parameters for performing the Action are stored in a dictionary, .params. These parameters can be modified directly (e.g. Action.params.param1=value1) or using the Action’s .set() method (e.g. action.set(param=value, param2=value2, ...) )

• You can bypass an action in a pipeline by changing Action.bypass to True

• You can declare whether an Action is an augmentation (should not be performed if bypass_augmentation=True) using its .is_augmentation boolean attribute

Modifying Actions¶

View default parameters for an Action¶

the .params attribute of an Action is a pandas Series containing parameters that can be modified

[19]:

#since the pipeline is a series, we can access elements like pipeline.to_spec as well as pipeline['to_spec']
preprocessor.pipeline.to_spec.params

[19]:

window_type                 hann
window_samples              None
window_length_sec           None
overlap_samples             None
overlap_fraction            None
fft_size                    None
decibel_limits       (-100, -20)
dB_scale                    True
scaling                 spectrum
dtype: object


Modify Action parameters¶

we can modify parameters with the Action’s .set() method:

[20]:

preprocessor.pipeline.to_spec.set(dB_scale=False)


or by accessing the parameter directly (params is a pandas Series)

[21]:

preprocessor.pipeline.to_spec.params.window_samples = 512
preprocessor.pipeline.to_spec.params['overlap_fraction'] = 0.75

preprocessor.pipeline.to_spec.params

[21]:

window_type                 hann
window_samples               512
window_length_sec           None
overlap_samples             None
overlap_fraction            0.75
fft_size                    None
decibel_limits       (-100, -20)
dB_scale                   False
scaling                 spectrum
dtype: object


Bypass actions¶

Actions can be bypassed by changing the attribute .bypass=True. A bypassed action is never performed regardless of the .perform_augmentations attribute.

[22]:

preprocessor = SpectrogramPreprocessor(sample_duration=2.0)

#turn off augmentations other than noise

#printing the pipeline will show which actions are bypassed
preprocessor.pipeline

[22]:

load_audio           Action calling <bound method Audio.from_file o...
random_trim_audio    Augmentation Action calling <function trim_aud...
trim_audio           Action calling <function trim_audio at 0x17d91...
to_spec              Action calling <bound method Spectrogram.from_...
bandpass             Action calling <function Spectrogram.bandpass ...
to_img               Action calling <function Spectrogram.to_image ...
time_mask            ## Bypassed ## Augmentation Action calling <fu...
frequency_mask       ## Bypassed ## Augmentation Action calling <fu...
add_noise            ## Bypassed ## Augmentation Action calling <fu...
rescale              Action calling <function scale_tensor at 0x17d...
random_affine        Augmentation Action calling <function torch_ra...
dtype: object


create a Dataset with this preprocessor and our label dataframe

[23]:

dataset = AudioFileDataset(labels,preprocessor)

print('random affine off')
preprocessor.pipeline.random_affine.bypass = True
show_tensor(dataset[0]['X'],invert=True,transform_from_zero_centered=True)
plt.show()

print('random affine on')
preprocessor.pipeline.random_affine.bypass = False
show_tensor(dataset[0]['X'],invert=True,transform_from_zero_centered=True)

random affine off

random affine on


To view whether an individual Action in a pipeline is on or off, inspect its bypass attribute:

[24]:

# The AudioLoader Action that is still on

[24]:

False

[25]:

# The frequency_mask Action that we turned off

[25]:

True


Modifying the pipeline¶

Sometimes, you may want to change the order or composition of the Preprocessor’s pipeline. You can simply overwrite the .pipeline attribute, as long as it is a pandas Series of names:Actions

Example: return Spectrogram instead of Tensor¶

Here’s an example where we replace the pipeline with one that just loads audio and converts it to a Spectrogram, returning a Spectrogram instead of a Tensor:

[26]:

#initialize a preprocessor
preprocessor = SpectrogramPreprocessor(2.0)
print('original pipeline:')
[print(p) for p in pre.pipeline]

#overwrite the pipeline with a slice of the original pipeline
print('\nnew pipeline:')
preprocessor.pipeline = preprocessor.pipeline[0:4]
[print(p) for p in preprocessor.pipeline]

print('\nWe now have a preprocessor that returns Spectrograms instead of Tensors:')
dataset = AudioFileDataset(labels,preprocessor)
print(f"Type of returned sample: {type(dataset[0]['X'])}")
dataset[0]['X'].plot()

original pipeline:
Action calling <bound method Audio.from_file of <class 'opensoundscape.audio.Audio'>>
Augmentation Action calling <function trim_audio at 0x17d913dc0>
Action calling <function trim_audio at 0x17d913dc0>
Action calling <bound method Spectrogram.from_audio of <class 'opensoundscape.spectrogram.Spectrogram'>>
Action calling <function Spectrogram.bandpass at 0x16c174ca0>
Action calling <function Spectrogram.to_image at 0x16c174f70>
Augmentation Action calling <function time_mask at 0x17d931550>
Augmentation Action calling <function frequency_mask at 0x17d9315e0>
Augmentation Action calling <function tensor_add_noise at 0x17d931670>
Action calling <function scale_tensor at 0x17d9314c0>
Augmentation Action calling <function torch_random_affine at 0x17d9313a0>

new pipeline:
Action calling <bound method Audio.from_file of <class 'opensoundscape.audio.Audio'>>
Augmentation Action calling <function trim_audio at 0x17d913dc0>
Action calling <function trim_audio at 0x17d913dc0>
Action calling <bound method Spectrogram.from_audio of <class 'opensoundscape.spectrogram.Spectrogram'>>

We now have a preprocessor that returns Spectrograms instead of Tensors:
Type of returned sample: <class 'opensoundscape.spectrogram.Spectrogram'>


Customizing preprocessing to achieve better machine learning outcomes¶

The right choice of preprocessing depends heavily on the characteristics of the sounds you wish to study. The best way to tune preprocessing parameters is to visually inspect samples created by your preprocessing procedure and tweak parameters to achieve visual clarity of the sounds of interest in your samples. We find these heuristics to be a good starting point:

• The duration of a sample should be approximately 2-5x the duration of the target sound. For instance, a very short nocturnal flight call lasting 0.1 seconds might be best visualized with a 0.3 second sample_duration. Meahwhile, a 10-second bout of ruffed grouse drumming might deserve a 20 second sample_duartion.
• The frequency range of a sample should be wider than the target sound, but not by more than 1 order of magnitude. For instance, sounds that are low-pitched will be more clearly visualized when bandpassing a spectrogram to the low frequencies. If you use a 0-10,000 Hz spectrogram for a 500 Hz target sound, your target sound will only occupy a small fraction of your sample.
• Spectrogram parameters should be matched to the temporal or spectral features of the target sound. Modify the Spectorgram’s window_samples to achieve high enough time resolution (lower value of window_samples) or frequency resolution (higher value of window_samples) to see features of your target sound clearly on the resulting sample. For example, a rapid trill with a pulse repetition rate of 50 Hz will only be distinctive on a spectrogram if the Spectrogram windows are less than 1/(50*2) = 0.01 seconds in duration. On the other hand, visualizing a distinctive harmonic “ladder” structure of a nasal sound might require long spectrogram windows which will increase frequency resolution.

Augmentations are Actions that are only performed during training, not during prediction. These actions manipulate the sample in some randomized way, so that each time the same sample is provided to the model as training data, the actual values of the sample are different. This prevents over-training of a model on a training set and effectively increases the size of a training dataset. In general, you can expect that a basic set of augmentations (such as those included by default in the SpecPreprocessor and CNN classes) will be necessary to train a useful machine learning model. In particular, “overlay” augmentations which blend together multiple samples often increase the generalizability (transferability) of a model. You might choose to use audio from your target system (for instance, field recordings at your study site) to make the training data look more similar to the data that the model will be applied to.

Below are various examples of how to modify parameters of the Actions to achieve different preprocessing outcomes.

Modify the sample rate¶

Resample all loaded audio to a specified rate during the load_audio action

[27]:

pre = SpectrogramPreprocessor(sample_duration=2)



Modify spectrogram window length and overlap¶

(see Spectrogram.from_audio() for detailed documentation)

[28]:

dataset = AudioFileDataset(labels,SpectrogramPreprocessor(sample_duration=2))
dataset.bypass_augmentations=True

print('default parameters:')
show_tensor(dataset[0]['X'],invert=True,transform_from_zero_centered=True)
plt.show()

print('high time resolution, low frequency resolution:')
dataset.preprocessor.pipeline.to_spec.set(window_samples=64)

show_tensor(dataset[0]['X'],invert=True,transform_from_zero_centered=True)

default parameters:

high time resolution, low frequency resolution:


Bandpass spectrograms¶

Trim spectrograms to a specified frequency range:

[29]:

dataset = AudioFileDataset(labels, SpectrogramPreprocessor(2.0))

print('default parameters:')
show_tensor(dataset[0]['X'],invert=True,transform_from_zero_centered=True)

print('bandpassed to 2-4 kHz:')
dataset.preprocessor.pipeline.bandpass.set(min_f=2000,max_f=4000)
show_tensor(dataset[0]['X'],invert=True,transform_from_zero_centered=True)

Clipping input data to the valid range for imshow with RGB data ([0..1] for floats or [0..255] for integers).

default parameters:
bandpassed to 2-4 kHz:


Change the output shape¶

Change the shape of the output sample - note that the shape argument expects (height, width), not (width, height)

[30]:

dataset = AudioFileDataset(labels, SpectrogramPreprocessor(2.0))

dataset.preprocessor.pipeline.to_img.set(shape=[500,1000])
show_tensor(dataset[0]['X'],invert=True,transform_from_zero_centered=True)

Clipping input data to the valid range for imshow with RGB data ([0..1] for floats or [0..255] for integers).


Turn all augmentation on or off¶

augmentation is controlled by the preprocessor.bypass_augmentation boolean (aka True/False) variable. By default, augmentations are performed. A CNN will internally manipulate this attribute to perform augmentations during training but not during validation or prediction.

[31]:

dataset = AudioFileDataset(labels, SpectrogramPreprocessor(2.0))

dataset.bypass_augmentations = True
show_tensor(dataset[0]['X'],invert=True,transform_from_zero_centered=True)

[32]:

dataset.bypass_augmentations = False
show_tensor(dataset[0]['X'],invert=True,transform_from_zero_centered=True)

Clipping input data to the valid range for imshow with RGB data ([0..1] for floats or [0..255] for integers).


Modify augmentation parameters¶

SpectrogramPreprocessor includes several augmentations with customizable parameters. Here we provide a couple of illustrative examples - see any action’s documentation for details on how to use its parameters.

[33]:

#initialize a preprocessor
preprocessor = SpectrogramPreprocessor(2.0)

#turn off augmentations other than overlay
preprocessor.pipeline.random_affine.bypass=True

# allow up to 20 horizontal masks, each spanning up to 0.1x the height of the image.

#preprocess the same sample 4 times
dataset = AudioFileDataset(labels,preprocessor)
tensors = [dataset[0]['X'] for i in range(4)]
fig = show_tensor_grid(tensors,2)
plt.show()


turn off frequency mask and turn on gaussian noise

[34]:

dataset.preprocessor.pipeline.add_noise.bypass = False

# increase the intensity of gaussian noise added to the image
show_tensor(dataset[0]['X'],invert=True,transform_from_zero_centered=True)

Clipping input data to the valid range for imshow with RGB data ([0..1] for floats or [0..255] for integers).


remove an action by its name¶

[35]:

preprocessor.remove_action('add_noise')
preprocessor.pipeline

[35]:

load_audio           Action calling <bound method Audio.from_file o...
random_trim_audio    Augmentation Action calling <function trim_aud...
trim_audio           Action calling <function trim_audio at 0x17d91...
to_spec              Action calling <bound method Spectrogram.from_...
bandpass             Action calling <function Spectrogram.bandpass ...
to_img               Action calling <function Spectrogram.to_image ...
time_mask            ## Bypassed ## Augmentation Action calling <fu...
frequency_mask       ## Bypassed ## Augmentation Action calling <fu...
rescale              Action calling <function scale_tensor at 0x17d...
random_affine        ## Bypassed ## Augmentation Action calling <fu...
dtype: object


add an action at a specific position¶

specify the action in the pipeline you want to insert before or after

[36]:

from opensoundscape.preprocess.actions import Action, tensor_add_noise

preprocessor.insert_action(
after_key='to_img', #where to put it (can also use before_key=...)
)

[37]:

preprocessor.pipeline

[37]:

load_audio           Action calling <bound method Audio.from_file o...
random_trim_audio    Augmentation Action calling <function trim_aud...
trim_audio           Action calling <function trim_audio at 0x17d91...
to_spec              Action calling <bound method Spectrogram.from_...
bandpass             Action calling <function Spectrogram.bandpass ...
to_img               Action calling <function Spectrogram.to_image ...
time_mask            ## Bypassed ## Augmentation Action calling <fu...
frequency_mask       ## Bypassed ## Augmentation Action calling <fu...
rescale              Action calling <function scale_tensor at 0x17d...
random_affine        ## Bypassed ## Augmentation Action calling <fu...
dtype: object


it will complain if you use a non-unique index

[38]:

from opensoundscape.preprocess.actions import Action, tensor_add_noise

try:
preprocessor.insert_action(
action_index='add_noise_NEW', #using the same name as a currentaction will lead to an AssertionError
after_key='to_img', #where to put it (can also use before_key=...)
)
except AssertionError:
print("raised Assertion Error, as expected")

raised Assertion Error, as expected


Overlay augmentation¶

Overlay is a powerful Action that allows additional samples to be overlayed or blended with the original sample.

The additional samples are chosen from the overlay_df that is provided to the preprocessor when it is initialized. The index of the overlay_df must be paths to audio files. The dataframe can be simply an index containing audio files with no other columns, or it can have the same columns as the sample dataframe for the preprocessor.

Samples for overlays are chosen based on their class labels, according to the parameter overlay_class:

• None - Randomly select any file from overlay_df
• "different" - Select a random file from overlay_df containing none of the classes this file contains
• specific class name - always choose files from this class

By default, the overlay Action does not change the labels of the sample it modifies. However, if you wish to add the labels from overlayed samples to the original sample’s labels, you can set update_labels=True (see example below).

[39]:

#initialize a preprocessor and provide a dataframe with samples to use as overlays
preprocessor = SpectrogramPreprocessor(2.0, overlay_df=labels)

#remove augmentations other than overlay
preprocessor.remove_action(name)


Modify overlay_weight¶

Let’s change overlay_weight to

To demonstrate this, let’s show what happens if we overlay samples from the “negative” class, resulting in the final sample having a higher or lower signal-to-noise ratio. By default, the overlay Action chooses a random file from the overlay dataframe. Instead, choose a sample from the class called "present" using the overlay_class parameter.

[40]:

preprocessor.pipeline.overlay.set(overlay_class='present')
tensors = []
overlay_weights = [0.01, 0.4, 0.6, 0.8]
for w in overlay_weights:
preprocessor.pipeline.overlay.set(overlay_weight=w)
dataset = AudioFileDataset(labels,preprocessor)
np.random.seed(0) #get the same overlay every time
tensors.append(dataset[2]['X'])
_ = show_tensor_grid(tensors, 2, labels=overlay_weights)


Overlay samples from a specific class¶

As demonstrated above, you can choose a specific class to choose samples from. Here, instead, we choose samples from the “absent” class.

[41]:

dataset.preprocessor.pipeline.overlay.set(
overlay_class='absent',
overlay_weight=0.4
)
show_tensor(dataset[0]['X'],invert=True,transform_from_zero_centered=True)


Overlaying samples from any class¶

By default, or by specifying overlay_class=None, the overlay sample is chosen randomly from the overlay_df with no restrictions.

[42]:

dataset.preprocessor.pipeline.overlay.set(overlay_class=None)
show_tensor(dataset[0]['X'],invert=True,transform_from_zero_centered=True)


Overlaying samples from a “different” class¶

The 'different' option for overlay_class chooses a sample to overlay that has non-overlapping labels with the original sample.

In the case of this example, this has the same effect as drawing samples from the "negative" class a demonstrated above. In multi-class examples, this would draw from any of the samples not labeled with the class(es) of the original sample.

We’ll again use overlay_weight=0.8 to exaggerate the importance of the overlayed sample (80%) compared to the original sample (20%).

[43]:

dataset.preprocessor.pipeline.overlay.set(update_labels=False,overlay_class='different',overlay_weight=0.8)
show_tensor(dataset[0]['X'],invert=True,transform_from_zero_centered=True)


Updating labels¶

By default, the overlay Action does not change the labels of the sample it modifies.

For instance, if the overlayed sample has labels [1,0] and the original sample has labels [0,1], the default behavior will return a sample with labels [0,1] not [1,1].

If you wish to add the labels from overlayed samples to the original sample’s labels, you can set update_labels=True.

[44]:

print('default: labels do not update')
dataset.preprocessor.pipeline.overlay.set(update_labels=False,overlay_class='different')
print(f"\t resulting labels: {dataset[0]['y'].numpy()}")

print('Using update_labels=True')
dataset.preprocessor.pipeline.overlay.set(update_labels=True,overlay_class='different')
print(f"\t resulting labels: {dataset[0]['y'].numpy()}")

default: labels do not update
resulting labels: [1 0]
Using update_labels=True
resulting labels: [1 1]


This example is a single-target problem: the two classes represent “woodcock absent” and “woodcock present.” Because the labels are mutually exclusive, labels [1,1] do not make sense. So, for this single-target problem, we would not want to use update_labels=True, and it would probably make most sense to only overlay absent recordings, e.g., overlay_class='absent'.

Creating a new Preprocessor class¶

If you have a specific augmentation routine you want to perform, you may want to create your own Preprocessor class rather than modifying an existing one.

Your subclass might add a different set of Actions, define a different pipeline, or even override the __getitem__ method of BasePreprocessor.

Here’s an example of a customized preprocessor that subclasses AudioToSpectrogramPreprocessor and creates a pipeline that depends on the magic_parameter input.

[45]:

from opensoundscape.preprocess.actions import Action, tensor_add_noise
class MyPreprocessor(SpectrogramPreprocessor):
"""Child of AudioToSpectrogramPreprocessor with weird augmentation routine"""

def __init__(
self,
magic_parameter,
sample_duration,
return_labels=True,
out_shape=[224, 224,1],
):

super(MyPreprocessor, self).__init__(
sample_duration=sample_duration,
out_shape=out_shape,
)

for i in range(magic_parameter):
self.insert_action(f'noise_{i}',action)

[46]:

dataset = AudioFileDataset(labels, MyPreprocessor(sample_duration=2.0, magic_parameter=1))
show_tensor(dataset[0]['X'],invert=False)

[47]:

dataset = AudioFileDataset(labels, MyPreprocessor(sample_duration=2.0, magic_parameter=4))
show_tensor(dataset[0]['X'],invert=False)


Defining new Actions¶

You can usually define a new action simply by passing a method to Action(). However, you can also write a subclass of Action for more advanced use cases - this is necessary if the action needs inputs other than the sample, such as labels.

using additional input in an Action¶

The following additional variables can be requested by an action, and will be passed from the pipeline when the action is run:

"_path": audio file path
"_labels": row of pd.DataFrame with 0/1 labels for each class (pd.Series)
"_start_time": start time of clip within longer audio file, if splitting long files into clips during preprocessing
"_sample_duration": sample_duration of clip in seconds
"_pipeline": a copy of the preprocessor's pipeline itself

[48]:

from opensoundscape.preprocess.actions import Action

def my_action_fn(x, _labels,threshold=0.1):
if _labels[0]==1:
samples = np.array([0 if np.abs(s)<threshold else s for s in audio.samples])
x = Audio(samples, audio.sample_rate)
return x

class AudioGate(Action):
"""Replace audio samples below a threshold with 0, but only if label[0]==1

Audio in, Audio out

Args:
threshold: sample values below this will become 0
"""

def __init__(self, **kwargs):
super(AudioGate, self).__init__(my_action_fn,extra_args=['_labels'],**kwargs)


Test it out:

[49]:

from opensoundscape.audio import Audio

gate_action = AudioGate(threshold=0.2)

print('histogram of samples')
audio = Audio.from_file('./woodcock_labeled_data/01c5d0c90bd4652f308fd9c73feb1bf5.wav')
_ = plt.hist(audio.samples,bins=100)
plt.semilogy()
plt.show()

print('histogram of samples after audio gate')
audio_gated = gate_action.go(audio,_labels={0:1,1:0})
_ = plt.hist(audio_gated.samples,bins=100)
plt.semilogy()
plt.show()

print('histogram of samples after audio gate, when labels[0]==0')
audio_gated = gate_action.go(audio,_labels={0:0,1:1})
_ = plt.hist(audio_gated.samples,bins=100)
plt.semilogy()

histogram of samples

histogram of samples after audio gate

histogram of samples after audio gate, when labels[0]==0

[49]:

[]


Add custom Action to a preprocessor¶

For instance, if you want to use your custom Action while training a cnn, you can add it to the cnn.preprocessor’s pipeline.

In this example, we put the custom AudioGate action before the to_spec action.

[50]:

gate_action = AudioGate(threshold=0.2)
preprocessor.insert_action(
action_index='custom_audio_gate', #give it a name
action=gate_action,
before_key='to_spec', #where to put it (can also use before_key=...)
)

[51]:

preprocessor.pipeline

[51]:

load_audio           Action calling <bound method Audio.from_file o...
random_trim_audio    Augmentation Action calling <function trim_aud...
trim_audio           Action calling <function trim_audio at 0x17d91...
custom_audio_gate    Action calling <function my_action_fn at 0x17d...
to_spec              Action calling <bound method Spectrogram.from_...
bandpass             Action calling <function Spectrogram.bandpass ...
to_img               Action calling <function Spectrogram.to_image ...
overlay              Augmentation Action calling <function overlay ...
rescale              Action calling <function scale_tensor at 0x17d...
dtype: object


Clean up files created during this tutorial:

[52]:

import shutil
shutil.rmtree('./woodcock_labeled_data')