Music Genre Tree Clasification

# -*- coding: UTF-8 -*-
# Python3

from sklearn import tree
import numpy as np

import os
import json

DATASET: GTZAN genre collection

Audios disponibles en: The dataset consists of 1000 audio tracks each 30 seconds long. It contains 10 genres, each represented by 100 tracks. The tracks are all 22050Hz Mono 16-bit audio files in .wav format.

http://marsyas.info/downloads/datasets.html

Ground truth, géneros: blues, classical, country, disco, hiphop, jazz, metal, pop, reggae, roc (100 archivos por género)

Procesado con Essentia (http://essentia.upf.edu/documentation/)

Valores promedio (mean) de los siguientes descriptores
[ 
            'lowlevel.spectral_centroid',
            'lowlevel.dissonance',
            'lowlevel.mfcc',
            'lowlevel.mfcc_bands',
            'sfx.inharmonicity', 
            'rhythm.bpm
]
En el .json se agrega el género y un id único     

Descriptores MIR para cada audio (.au) almacenados en archivos .json (uno por cada audio)

Nota: No se entrena con todos, se excluyen los que luego se van a utilizar para predecir.

Ejemplo json

cat dataset/descriptores/blues.00001.json
{"lowlevel.dissonance.mean": "0.435817778111", "lowlevel.mfcc_bands.mean": "0.00309278", "sfx.inharmonicity.mean": "0.072731718421", "rhythm.bpm.mean": "67.7719039917", "lowlevel.spectral_centroid.mean": "2191.51098633", "lowlevel.mfcc.mean": "-695.122", "genre": "blues", "id": "blues.00001"}
files_dir = 'dataset/descriptores'
descriptors = [
                'lowlevel.dissonance.mean',
                'lowlevel.mfcc_bands.mean',
                'sfx.inharmonicity.mean',
                'rhythm.bpm.mean',    
                'lowlevel.spectral_centroid.mean',    
                'lowlevel.mfcc.mean',
                ]
#ls dataset/descriptores # list json files
#TODO: autonumerate
codify_genre = { 
    'blues': 0,
    'classical': 1,
    'country': 2,
    'disco': 3,
    'hiphop': 4,
    'jazz': 5,
    'metal': 6,
    'pop': 7,
    'reggae': 8,
    'rock': 9
}
undecode_genre = {v: k for k, v in codify_genre.items()}

def load_song_from_json(json_filename):
    data = json.load( open(json_filename,'r') )
    song_id = data.pop('id')
    song_genre = codify_genre[ str(data.pop('genre')) ]
    x = np.array( [float(v) for (k,v) in data.items()], dtype=float)
    return song_id, undecode_genre[song_genre], x

len_vector = 6 #amount of data value of descriptors
arr = np.empty((0,len_vector), float)
target_arr = np.empty((0,1), float)

for subdir, dirs, files in os.walk(files_dir):
    for f in files:
        if os.path.splitext(f)[1]==".json":
            data = json.load( open(files_dir + "/" + f,'r') )
            song_id = data.pop('id')
            song_genre = codify_genre[ str(data.pop('genre')) ]
            target_arr = np.append(target_arr, song_genre)
            x = np.array( [float(v) for (k,v) in data.items()], dtype=float)
            arr = np.vstack((arr, x))

Entrenamiento

ct = tree.DecisionTreeClassifier()
ct_entrenado = ct.fit( arr, target_arr ) #cargo los datos (entreno)

def genre_predict(ct_entrenado, x_arr):
    return undecode_genre[ ct_entrenado.predict( x_arr.reshape(1,-1) )[0] ]

### Pruebas de predicción

test_id, test_genre, test_arr = load_song_from_json( 'dataset/pruebas/pop.00010.json' )
test_genre, test_arr
('pop', array([  4.43471521e-01,   9.70065000e-03,   7.65194297e-02,
          1.06967133e+02,   2.11458838e+03,  -5.79939000e+02]))
print( "Género predecido: ", genre_predict(ct_entrenado, test_arr.reshape(1,-1)) )
Género predecido:  country
test_id, test_genre, test_arr = load_song_from_json( 'dataset/pruebas/country.00031.json' )
test_genre, test_arr
('country', array([  4.47215110e-01,   1.63228000e-03,   6.21108934e-02,
          1.10562599e+02,   2.25280249e+03,  -5.99665000e+02]))
print( "Género predecido: ", genre_predict(ct_entrenado, test_arr.reshape(1,-1)) )
Género predecido:  disco
# al agregar al entrenamiento con el género country, falla (sin ese, clasifica OK)
# al agregar hiphop vuelve a clasificar bien
test_id, test_genre, test_arr = load_song_from_json( 'dataset/pruebas/blues.00005.json' )
test_genre, test_arr
('blues', array([  4.13481325e-01,   5.61112000e-05,   5.49932234e-02,
          6.83032532e+01,   2.24271191e+03,  -6.62832000e+02]))
print( "Género predecido: ", genre_predict(ct_entrenado, test_arr.reshape(1,-1)) )
Género predecido:  country
# esta canción blues clasifica OK
test_id, test_genre, test_arr = load_song_from_json( 'dataset/pruebas/blues.00021.json' )
test_genre, test_arr
('blues', array([  4.01932120e-01,   1.25262000e-03,   5.18891178e-02,
          1.00242500e+02,   2.34314429e+03,  -7.47678000e+02]))
print( "Género predecido: ", genre_predict(ct_entrenado, test_arr.reshape(1,-1)) )
Género predecido:  blues
test_id, test_genre, test_arr = load_song_from_json( 'dataset/pruebas/reggae.00052.json' )
test_genre, test_arr
('reggae', array([  4.72415090e-01,   2.26356000e-02,   2.88092017e-01,
          9.96622391e+01,   2.15012451e+03,  -5.70954000e+02]))
print( "Género predecido: ", genre_predict(ct_entrenado, test_arr.reshape(1,-1)) )
Género predecido:  pop
test_id, test_genre, test_arr = load_song_from_json( 'dataset/pruebas/metal.00023.json' )
test_genre, test_arr
('metal', array([  4.86335456e-01,   1.36190000e-02,   1.09521285e-01,
          1.45000031e+02,   2.03619116e+03,  -5.16890000e+02]))
print( "Género predecido: ", genre_predict(ct_entrenado, test_arr.reshape(1,-1)) )
Género predecido:  metal

Algunas conclusiones

Entrenando con pocos géneros posibles clasificó bien los ejemplos tomados. Al entrenar con blues, classical y pop por ejemplo. Cuando a eso se le sumo el género country. Clasficó al tema 0005 de blues como de aquel género (country). Al agregar el entrenamiento hiphop, lo volvió a clasificar bien. Al agregar jazz, confunde pop.00010.json con blues…

En principio pareciera que confunde country con blues y/o pop. Lo cuál es razonable a nivel armonias (al escuchar).

Aunque son los que se observó en bibliografía que son los que se suelen utilizar, puede ser que los descriptores calculados no sean adecuados. Salvo en el BPM, tal vez sea necesario entrenar con todo el vector de valores, en lugar del valor promedio que puede no ser representativo de la canción.

Nota: los ejemplos con los que no fue entrenado el clasificador y se utilizaron para predecir, fueron tomados del mismo dataset de archivos de audio.

# from http://scikit-learn.org/stable/modules/tree.html
import graphviz

genres = list({k for k, v in codify_genre.items()})

dot_data = tree.export_graphviz(ct, out_file=None, 
                         feature_names=descriptors,  
                         class_names=genres,  
                         filled=True, rounded=True,  
                         special_characters=True)
graph = graphviz.Source(dot_data) 
graph.render("Géneros")
#graph # displays here
'Géneros.pdf'
graph

svg

Written on August 20, 2017