Skip to content
This repository has been archived by the owner on Nov 17, 2023. It is now read-only.

[MXNET-1210 ] Gluon Audio - Example #13325

Merged
merged 21 commits into from
Dec 1, 2018
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions example/gluon/urban_sounds/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# Urban Sounds classification in MXNet
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Link your design doc

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Classification


This example provides an end-to-end pipeline for a common datahack competition - Urban Sounds Classification Example.
Below is the link to the competition:
https://datahack.analyticsvidhya.com/contest/practice-problem-urban-sound-classification/

After logging in, the data set can be downloaded.
The details of the dataset and the link to download it are given below:


Urban Sounds Dataset:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: make header

## Description
The dataset contains 8732 wav files which are audio samples(<= 4s)) of street sounds like engine_idling, car_horn, children_playing, dog_barking and so on.
The task is to classify these audio samples into one of the 10 labels.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: please add list of available labels here as well

To be able to run this example:

1. `pip install -r ./requirements.txt`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would mention that we need to go back to the directory: cd ../
would just leave it to pip install requirements.txt


This step installs the required libraries to run the example.
The main dependency that is required is: Librosa.
The version used to test the example is: `0.6.2`
For more details, refer here:
*https://librosa.github.io/librosa/install.html*

2. Download the dataset(train.zip, test.zip) required for this example from the location:
https://drive.google.com/drive/folders/0By0bAi7hOBAFUHVXd1JCN3MwTEU
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you move this to a public S3 bucket instead?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use https://registry.opendata.aws/ this page to onboard your dataset onto a public S3 bucket - I would highly recommend doing so as the 1. Google drive link is external and we don't use it to store data in production
2. S3 buckets are more reliable and the example would not have any issues related to data availability in the future


3. Extract both the zip archives into the **current directory** - after unzipping you would get 2 new folders namely,\
**Train** and **Test** and two csv files - **train.csv**, **test.csv**

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please add comment about how the folder structure should look like for more clarity

4. Apache MXNet is installed on the machine. For instructions, go to the link: **https://mxnet.incubator.apache.org/install/**



For information on the current design of how the AudioFolderDataset is implemented, refer below:
**https://cwiki.apache.org/confluence/display/MXNET/Gluon+-+Audio**

## Usage

For training:

- arguments
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Arguments

- train : The folder/directory that contains the audio(wav) files locally. Default = "./Train"
- csv: The file name of the csv file that contains audio file name to label mapping. Default = "train.csv"
- epochs : Number of epochs to train the model. Default = 30
- batch_size : The batch size for training. Default = 32


###### default setting
```
python train.py
```
or

###### manual setting
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please elaborate more here - you can instead say - To pass training data run this command

```
python train.py --train ./Train --csv train.csv --batch_size 32 --epochs 30
```

For prediction:

- arguments
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Arguments

- pred : The folder/directory that contains the audio(wav) files which are to be classified. Default = "./Test"


###### default setting
```
python predict.py
```
or

###### manual setting
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as above

```
python train.py --pred ./Test
```
178 changes: 178 additions & 0 deletions example/gluon/urban_sounds/datasets.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.

# coding: utf-8
# pylint: disable=
""" Audio Dataset container."""
__all__ = ['AudioFolderDataset']

import os
import warnings
from itertools import islice
import csv
from mxnet.gluon.data import Dataset
from mxnet import ndarray as nd
try:
import librosa
except ImportError as e:
raise ImportError("librosa dependency could not be resolved or \
imported, could not load audio onto the numpy array. pip install librosa")



class AudioFolderDataset(Dataset):
"""A dataset for loading Audio files stored in a folder structure like::

root/children_playing/0.wav
root/siren/23.wav
root/drilling/26.wav
root/dog_barking/42.wav
OR
Files(wav) and a csv file that has file name and associated label

Parameters
----------
root : str
Path to root directory.
transform : callable, default None
A function that takes data and label and transforms them
train_csv: str, default None
train_csv should be populated by the training csv filename
file_format: str, default '.wav'
The format of the audio files(.wav)
skip_header: boolean, default False
While reading from csv file, whether to skip at the start of the file to avoid reading in header


Attributes
----------
synsets : list
List of class names. `synsets[i]` is the name for the `i`th label
items : list of tuples
List of all audio in (filename, label) pairs.

"""
def __init__(self, root, train_csv=None, file_format='.wav', skip_header=False):
if not librosa:
warnings.warn("pip install librosa to continue.")
raise RuntimeError("Librosa not installed. Run pip install librosa and retry this step.")
self._root = os.path.expanduser(root)
self._exts = ['.wav']
self._format = file_format
self._train_csv = train_csv
if file_format.lower() not in self._exts:
raise RuntimeError("format {} not supported currently.".format(file_format))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Format {}..."

skip_rows = 0
if skip_header:
skip_rows = 1
self._list_audio_files(self._root, skip_rows=skip_rows)


def _list_audio_files(self, root, skip_rows=0):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In previous PR, I had a concern for calling this skip_rows. May be better name is skip_header?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point Sandeep! I have addressed this.

"""Populates synsets - a map of index to label for the data items.
Populates the data in the dataset, making tuples of (data, label)
"""
self.synsets = []
self.items = []
if not self._train_csv:
# The audio files are organized in folder structure with
# directory name as label and audios in them
self._folder_structure(root)
else:
# train_csv contains mapping between filename and label
self._csv_labelled_dataset(root, skip_rows=skip_rows)

#Generating the synset.txt file now
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: add space between # & Generating

if not os.path.exists("./synset.txt"):
with open("./synset.txt", "w") as synsets_file:
for item in self.synsets:
synsets_file.write(item+os.linesep)
print("Synsets is generated as synset.txt")
else:
warnings.warn("Synset file already exists in the current directory! Not generating synset.txt.")


def _folder_structure(self, root):
for folder in sorted(os.listdir(root)):
path = os.path.join(root, folder)
if not os.path.isdir(path):
warnings.warn('Ignoring {}, which is not a directory.'.format(path))
continue
label = len(self.synsets)
self.synsets.append(folder)
for filename in sorted(os.listdir(path)):
file_name = os.path.join(path, filename)
ext = os.path.splitext(file_name)[1]
if ext.lower() not in self._exts:
warnings.warn('Ignoring {} of type {}. Only support {}'\
.format(filename, ext, ', '.join(self._exts)))
continue
self.items.append((file_name, label))


def _csv_labelled_dataset(self, root, skip_rows=0):
with open(self._train_csv, "r") as traincsv:
for line in islice(csv.reader(traincsv), skip_rows, None):
filename = os.path.join(root, line[0])
label = line[1].strip()
if label not in self.synsets:
self.synsets.append(label)
if self._format not in filename:
filename = filename+self._format
self.items.append((filename, nd.array([self.synsets.index(label)]).reshape((1,))))


def __getitem__(self, idx):
"""Retrieve the item (data, label) stored at idx in items"""
filename, label = self.items[idx]
# resampling_type is passed as kaiser_fast for a better performance
X1, _ = librosa.load(filename, res_type='kaiser_fast')
return nd.array(X1), label


def __len__(self):
"""Retrieves the number of items in the dataset"""
return len(self.items)


def transform_first(self, fn, lazy=False):
"""Returns a new dataset with the first element of each sample
transformed by the transformer function `fn`.

This is useful, for example, when you only want to transform data
while keeping label as is.
lazy=False is passed to transform_first for dataset so that all tramsforms could be performed in
one shot and not during training. This is a performance consideration.

Parameters
----------
fn : callable
A transformer function that takes the first element of a sample
as input and returns the transformed element.
lazy : bool, default True
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: default False

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. corrected this.

If False, transforms all samples at once. Otherwise,
transforms each sample on demand. Note that if `fn`
is stochastic, you must set lazy to True or you will
get the same result on all epochs.

Returns
-------
Dataset
The transformed dataset.

"""
return super(AudioFolderDataset, self).transform_first(fn, lazy=False)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return super(AudioFolderDataset, self).transform_first(fn, lazy=False)
return super(AudioFolderDataset, self).transform_first(fn, lazy=lazy)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Passing lazy from the arguments now.

33 changes: 33 additions & 0 deletions example/gluon/urban_sounds/model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.

"""This module builds a model an MLP with a configurable output layer( number of units in the last layer).
Users can pass any number of units in the last layer. SInce this dataset has 10 labels,
the default value of num_labels = 10
"""
import mxnet as mx
from mxnet import gluon

# Defining a neural network with number of labels
def get_net(num_labels=10):
net = gluon.nn.Sequential()
with net.name_scope():
net.add(gluon.nn.Dense(256, activation="relu")) # 1st layer (256 nodes)
net.add(gluon.nn.Dense(256, activation="relu")) # 2nd hidden layer ( 256 nodes )
net.add(gluon.nn.Dense(num_labels))
net.collect_params().initialize(mx.init.Xavier())
return net
89 changes: 89 additions & 0 deletions example/gluon/urban_sounds/predict.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
""" Prediction module for Urban Sounds Classification
"""
import os
import warnings
import mxnet as mx
from mxnet import nd
from transforms import MFCC
from model import get_net
try:
import librosa
except ImportError:
raise ImportError("Librosa is not installed! please run the following command pip install librosa.")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please run the following command pip install librosa


Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[add a line]

def predict(prediction_dir='./Test'):
"""The function is used to run predictions on the audio files in the directory `pred_directory`.

Parameters
----------
net:
The model that has been trained.
prediction_dir: string, default ./Test
The directory that contains the audio files on which predictions are to be made

"""

if not os.path.exists(prediction_dir):
warnings.warn("The directory on which predictions are to be made is not found!")
return

if len(os.listdir(prediction_dir)) == 0:
warnings.warn("The directory on which predictions are to be made is empty! Exiting...")
return

# Loading synsets
if not os.path.exists('./synset.txt'):
warnings.warn("The synset or labels for the dataset do not exist. Please run the training script first.")
return

with open("./synset.txt", "r") as f:
synset = [l.rstrip() for l in f]
net = get_net(len(synset))
print("Trying to load the model with the saved parameters...")
if not os.path.exists("./net.params"):
warnings.warn("The model does not have any saved parameters... Cannot proceed! Train the model first")
return

net.load_parameters("./net.params")
file_names = os.listdir(prediction_dir)
full_file_names = [os.path.join(prediction_dir, item) for item in file_names]
mfcc = MFCC()
print("\nStarting predictions for audio files in ", prediction_dir, " ....\n")
for filename in full_file_names:
# Argument kaiser_fast to res_type is faster than 'kaiser_best'. To reduce the load time, passing kaiser_fast.
X1, _ = librosa.load(filename, res_type='kaiser_fast')
transformed_test_data = mfcc(mx.nd.array(X1))
output = net(transformed_test_data.reshape((1, -1)))
prediction = nd.argmax(output, axis=1)
print(filename, " -> ", synset[(int)(prediction.asscalar())])


if __name__ == '__main__':
try:
import argparse
parser = argparse.ArgumentParser(description="Urban Sounds clsssification example - MXNet")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MXNet Gluon

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. Corrected this.

parser.add_argument('--pred', '-p', help="Enter the folder path that contains your audio files", type=str)
args = parser.parse_args()
pred_dir = args.pred

except ImportError:
warnings.warn("Argparse module not installed! passing default arguments.")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be good, if you can add requirements.txt file in this example folder. Have a step in readme to install pre-requisites using this requirements.txt

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you could also provide a setup script to install all dependencies

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. I have added a requirements.txt file and a step in README for installing pre-requisites.

pred_dir = './Test'
predict(prediction_dir=pred_dir)
print("Urban sounds classification Prediction DONE!")
2 changes: 2 additions & 0 deletions example/gluon/urban_sounds/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
librosa>=0.6.2 # librosa is a library that is used to load the audio(wav) files and provides capabilities of feature extraction.
argparse # used for parsing arguments
Loading