Like my articles? Feel free to vote for me
as ML Writer of the year here
.
El análisis de sonido es una tarea desafiante, asociada a varias aplicaciones modernas, como análisis de voz, recuperación de información musical, reconocimiento de locutores, análisis de comportamiento y análisis de escenas auditivas para monitoreo de seguridad, salud y medio ambiente. Este artículo proporciona una breve introducción a los conceptos básicos de extracción de características de audio , clasificación y segmentación de sonido, con ejemplos de demostración en aplicaciones como clasificación de géneros musicales, agrupación de altavoces, clasificación de eventos de audio y detección de actividad de voz.
Se proporcionan ejemplos de Python en todos los casos, principalmente a través de la biblioteca . Todos los ejemplos también se proporcionan en repositorio de github.
Con respecto a las metodologías de ML involucradas, este artículo se centra en las funciones de audio hechas a mano y los clasificadores estadísticos tradicionales como SVM. Los métodos de audio profundo se seguirán en un artículo futuro, ya que el presente artículo trata más sobre aprender a extraer características de audio que tengan sentido para sus clasificadores, incluso cuando tenga algunas decenas de muestras de entrenamiento.Antes de profundizar en el reconocimiento de audio, el lector debe conocer los conceptos básicos del manejo de audio y la representación de señales: definición de sonido, muestreo, cuantificación, frecuencia de muestreo, resolución de muestreo y los conceptos básicos de representación de frecuencia. Estos temas se tratan en este artículo.
Por lo tanto, ya debería saber que una señal de audio está representada por una secuencia de muestras con una "resolución de muestra" dada (generalmente 16 bits = 2 bytes por muestra) y con una frecuencia de muestreo particular (por ejemplo, 16 KHz = 16000 muestras por segundo).
Ahora podemos continuar con el siguiente paso: usar estas muestras para analizar los sonidos correspondientes. Por "analizar" podemos referirnos a cualquier cosa, desde: reconocer entre diferentes tipos de sonidos, segmentar una señal de audio en partes homogéneas (por ejemplo, dividir los segmentos sonoros de los no sonoros en una señal de voz) o agrupar archivos de sonido en función de su similitud de contenido.
En todos los casos, primero debemos encontrar una manera de pasar de las muestras de datos de audio voluminosas y de bajo nivel a una representación de nivel superior del contenido de audio. Este es el propósito de la extracción de características (FE) , la tarea más común e importante en todas las aplicaciones de aprendizaje automático y reconocimiento de patrones.
FE se trata de extraer un conjunto de características que son informativas con respecto a las propiedades deseadas de los datos originales. En nuestro caso, estamos interesados en extraer características de audio que sean capaces de discriminar entre diferentes clases de audio, es decir, diferentes hablantes, eventos, emociones o géneros musicales, dependiendo del subdominio de la aplicación.El concepto más importante de la extracción de características de audio es el uso de ventanas a corto plazo (o marcos ): esto simplemente significa que la señal de audio se divide en ventanas (o marcos ) a corto plazo. Los marcos pueden superponerse opcionalmente.
La duración de las tramas suele oscilar entre 10 y 100 milisegundos, según la aplicación y los tipos de señales. Para el caso de no superposición, el paso del procedimiento de ventana es igual a la longitud de la ventana (también llamado "tamaño").
Si, por otro lado, paso < tamaño, entonces los marcos se superponen: por ejemplo, un paso de 10 ms para un tamaño de ventana de 40 ms significa una superposición del 75%. Por lo general, también se aplica una función de ventana (como hamming) a cada cuadro. Para cada fotograma (sea N el número total de fotogramas), extraemos un conjunto de características de audio (a corto plazo). Cuando las características se extraen directamente de los valores de la muestra de audio, se denominan dominio del tiempo. Si las características se calculan sobre los valores de FFT, se denominan características de dominio de frecuencia. Finalmente, las características cepstrales (como los ) son características que se basan en el cepstrum.Como ejemplo, supongamos que solo extraemos la energía de la señal (la media de los cuadrados de las muestras de audio) y el centroide espectral (el centroide de la magnitud de la FFT). Esto significa que, durante este procedimiento de trama, la señal se representa mediante una secuencia de vectores de características bidimensionales a corto plazo. (o dos secuencias de funciones de igual duración, si lo desea).
Entonces, ¿cómo podemos usar estas secuencias de tamaño arbitrario para analizar la señal respectiva? Imagina que quieres construir un clasificador para discriminar entre dos clases de audio, por ejemplo, habla y silencio. Tus datos de entrenamiento iniciales son archivos de audio y las etiquetas de clase correspondientes (una etiqueta de clase por archivo de audio completo ). Si estos archivos tienen la misma duración, las secuencias de vectores de características a corto plazo correspondientes tendrán la misma longitud. ¿Qué sucede, sin embargo, en el caso general de duraciones arbitrarias?
Un enfoque más común seguido en el análisis de audio tradicional es extraer un conjunto de estadísticas de características por segmento de tamaño fijo . Las estadísticas a nivel de segmento extraídas sobre las secuencias de características a corto plazo son las representaciones para cada segmento de tamaño fijo. La representación final de la señal puede ser el promedio a largo plazo de las estadísticas del segmento.
... las estadísticas de características del segmento son la forma más sencilla de hacerloComo ejemplo, considere una señal de audio de 2,5 segundos. Seleccionamos una ventana a corto plazo de 50 ms y un segmento de 1 segundo. De acuerdo con lo anterior, las secuencias de energía y centroide espectral se extraerán para cada segmento de 1 segundo. La longitud de las secuencias N será igual a 1 / 0.050 = 20. Luego, se extraen los μ y σ de cada secuencia para cada segmento de 1 segundo, como las estadísticas de características del segmento. Estos finalmente se promedian a largo plazo, lo que da como resultado la representación final de la señal. (Tenga en cuenta que el último segmento tiene una longitud de 0,5, por lo que las estadísticas se extraen en un segmento más corto)
El ejemplo para leer un archivo de audio WAV y extraer secuencias de funciones a corto plazo y trazar la secuencia de energía (solo una de las funciones). Consulte los comentarios en línea para obtener una explicación, junto con estas dos notas:
read_audio_file()
devuelve la frecuencia de muestreo (Fs) del archivo de audio y una matriz NumPy de las muestras de audio sin procesar. Para obtener la duración en segundos, simplemente hay que dividir el número de muestras por Fs ShortTermFeatures.feature_extraction()
La función devuelve (a) una matriz de características a corto plazo de 68 x 20, donde 68 es la cantidad de características a corto plazo implementadas en la biblioteca y 20 es la cantidad de fotogramas que caben en los segmentos de 1 segundo (se usa 1 segundo como ventana intermedia en el ejemplo) (b) una lista de 68 cadenas de caracteres que contienen los nombres de cada función implementada en la biblioteca. # Example 1: short-term feature extraction from pyAudioAnalysis import ShortTermFeatures as aF from pyAudioAnalysis import audioBasicIO as aIO import numpy as np import plotly.graph_objs as go import plotly import IPython # read audio data from file # (returns sampling freq and signal as a numpy array) fs, s = aIO.read_audio_file( "data/object.wav" ) # play the initial and the generated files in notebook: IPython.display.display(IPython.display.Audio( "data/object.wav" )) # print duration in seconds: duration = len(s) / float(fs) print( f'duration = {duration} seconds' ) # extract short-term features using a 50msec non-overlapping windows win, step = 0.050 , 0.050 [f, fn] = aF.feature_extraction(s, fs, int(fs * win), int(fs * step)) print( f' {f.shape[ 1 ]} frames, {f.shape[ 0 ]} short-term features' ) print( 'Feature names:' ) for i, nam in enumerate(fn): print( f' {i} : {nam} ' ) # plot short-term energy # create time axis in seconds time = np.arange( 0 , duration - step, win) # get the feature whose name is 'energy' energy = f[fn.index( 'energy' ), :] mylayout = go.Layout(yaxis=dict(title= "frame energy value" ), xaxis=dict(title= "time (sec)" )) plotly.offline.iplot(go.Figure(data=[go.Scatter(x=time, y=energy)], layout=mylayout))
duration = 1.03 seconds 20 frames, 68 short-term features Feature names: 0 :zcr 1 :energy 2 :energy_entropy 3 :spectral_centroid 4 :spectral_spread 5 :spectral_entropy 6 :spectral_flux 7 :spectral_rolloff 8 :mfcc_1 ... 31 :chroma_11 32 :chroma_12 33 :chroma_std 34 :delta zcr 35 :delta energy ... 66 :delta chroma_12 67 :delta chroma_std
El ejemplo 2 demuestra la característica a corto plazo del centroide espectral. El centroide espectral es simplemente el centroide de la magnitud FFT, normalizado en el rango de frecuencia [0, Fs/2] (por ejemplo, si el centroide espectral = 0,5, esto es igual a Fs/4 medido en Hz).
# Example 2: short-term feature extraction: # spectral centroid of two speakers from pyAudioAnalysis import ShortTermFeatures as aF from pyAudioAnalysis import audioBasicIO as aIO import numpy as np import plotly.graph_objs as go import plotly import IPython # read audio data from file # (returns sampling freq and signal as a numpy array) fs, s = aIO.read_audio_file( "data/trump_bugs.wav" ) # play the initial and the generated files in notebook: IPython.display.display(IPython.display.Audio( "data/trump_bugs.wav" )) # print duration in seconds: duration = len(s) / float(fs) print( f'duration = {duration} seconds' ) # extract short-term features using a 50msec non-overlapping windows win, step = 0.050 , 0.050 [f, fn] = aF.feature_extraction(s, fs, int(fs * win), int(fs * step)) print( f' {f.shape[ 1 ]} frames, {f.shape[ 0 ]} short-term features' ) # plot short-term energy # create time axis in seconds time = np.arange( 0 , duration - step, win) # get the feature whose name is 'energy' energy = f[fn.index( 'spectral_centroid' ), :] mylayout = go.Layout(yaxis=dict(title= "spectral_centroid value" ), xaxis=dict(title= "time (sec)" )) plotly.offline.iplot(go.Figure(data=[go.Scatter(x=time, y=energy)], layout=mylayout))
En total, se extraen 34 funciones a corto plazo en , para cada cuadro, y ShortTermFeatures.feature_extraction() La función también (opcionalmente) extrae las características delta respectivas. En ese caso, el número total de características extraídas para cada cuadro a corto plazo es 68. La lista completa y la descripción de las características a corto plazo se pueden encontrar en el de la biblioteca y en .
Los dos primeros ejemplos utilizaron la función
ShortTermFeatures.feature_extraction()
para extraer 68 características por cuadro a corto plazo. Como se describe en la sección anterior, en muchos casos, como la clasificación a nivel de segmento, también extraemos estadísticas a nivel de segmento. Esto se logra a través de la MidTermFeatures.mid_feature_extraction()
función, como se muestra en el Ejemplo 3 : # Example 3: segment-level feature extraction from pyAudioAnalysis import MidTermFeatures as aF from pyAudioAnalysis import audioBasicIO as aIO # read audio data from file # (returns sampling freq and signal as a numpy array) fs, s = aIO.read_audio_file( "data/trump_bugs.wav" ) # get mid-term (segment) feature statistics # and respective short-term features: mt, st, mt_n = aF.mid_feature_extraction(s, fs, 1 * fs, 1 * fs, 0.05 * fs, 0.05 * fs) print( f'signal duration {len(s)/fs} seconds' ) print( f' {st.shape[ 1 ]} {st.shape[ 0 ]} -D short-term feature vectors extracted' ) print( f' {mt.shape[ 1 ]} {mt.shape[ 0 ]} -D segment feature statistic vectors extracted' ) print( 'mid-term feature names' ) for i, mi in enumerate(mt_n): print( f' {i} : {mi} ' )
signal duration 3.812625 seconds 76 68 -D short-term feature vectors extracted 4 136 -D segment feature statistic vectors extracted mid-term feature names 0 :zcr_mean 1 :energy_mean 2 :energy_entropy_mean 3 :spectral_centroid_mean 4 :spectral_spread_mean 5 :spectral_entropy_mean 6 :spectral_flux_mean 7 :spectral_rolloff_mean 8 :mfcc_1_mean ... 131 :delta chroma_9_std 132 :delta chroma_10_std 133 :delta chroma_11_std 134 :delta chroma_12_std 135 :delta chroma_std_std
MidTermFeatures.mid_feature_extraction()
extrae 2 estadísticas, a saber, la media y la estándar de cada secuencia de características a corto plazo, utilizando el tamaño de ventana de "mediano plazo" (segmento) proporcionado de 1 segundo para el ejemplo anterior. Dado que la duración de la señal es de 3,8 segundos, y el paso y el tamaño de la ventana a medio plazo es de 1 segundo, esperamos que se creen 4 segmentos a medio plazo y para cada uno de ellos se calculará un vector de estadísticas de características. Además, estas estadísticas de segmento se calculan sobre las secuencias de características a corto plazo de 3,8/0,05 = 76 fotogramas a corto plazo. Además, tenga en cuenta que los nombres de características a mediano plazo también contienen la estadística del segmento, por ejemplo, zcr_mean es la media de la característica a corto plazo con tasa de cruce por cero.Los primeros 3 ejemplos mostraron cómo podemos extraer características a corto plazo y estadísticas de características a mediano plazo (segmento). Función
MidTermFeatures.directory_feature_extraction()
extrae características de audio para todos los archivos en la carpeta proporcionada, de modo que estos datos se puedan usar para entrenar un clasificador, etc.Así que en realidad llama
MidTermFeatures.mid_feature_extraction()
para cada archivo WAV y realiza un promedio a largo plazo para pasar de vectores estadísticos de características de segmento a un solo vector de características. Además, esta función es capaz de extraer dos características relacionadas con el ritmo de la música que se adjuntan en las estadísticas del segmento promediado. Como ejemplo, supongamos que queremos analizar una canción de 120 segundos, con una ventana (y paso) a corto plazo de 50 mseg y una ventana (segmento) a medio plazo y paso de 1 segundo.Los siguientes pasos ocurrirán durante el
MidTermFeatures.directory_feature_extraction()
llamar:El ejemplo 4 demuestra el uso de
MidTermFeatures.directory_feature_extraction()
para extraer características a nivel de archivo (promedios de estadísticas de características de segmento) para 20 muestras de música de 2 segundos (archivos WAV separados) de dos categorías de géneros musicales, a saber, música clásica y heavy metal. Para cada uno de los segmentos de canciones de 2 segundos MidTermFeatures.directory_feature_extraction()
extrae el vector de características 138-D, como se describe anteriormente. Luego seleccionamos trazar 2 de estas características, usando diferentes colores para las dos clases de audio (clásico y metal): # Example4: plot 2 features for 10 2-second samples # from classical and 10 from metal music from pyAudioAnalysis import MidTermFeatures as aF import os import numpy as np import plotly.graph_objs as go import plotly dirs = [ "data/music/classical" , "data/music/metal" ] class_names = [os.path.basename(d) for d in dirs] m_win, m_step, s_win, s_step = 1 , 1 , 0.1 , 0.05 # segment-level feature extraction: features = [] for d in dirs: # get feature matrix for each directory (class) f, files, fn = aF.directory_feature_extraction(d, m_win, m_step, s_win, s_step) features.append(f) # (each element of the features list contains a # (samples x segment features) = (10 x 138) feature matrix) print(features[ 0 ].shape, features[ 1 ].shape) # select 2 features and create feature matrices for the two classes: f1 = np.array([features[ 0 ][:, fn.index( 'spectral_centroid_mean' )], features[ 0 ][:, fn.index( 'energy_entropy_mean' )]]) f2 = np.array([features[ 1 ][:, fn.index( 'spectral_centroid_mean' )], features[ 1 ][:, fn.index( 'energy_entropy_mean' )]]) # plot 2D features plots = [go.Scatter(x=f1[ 0 , :], y=f1[ 1 , :], name=class_names[ 0 ], mode= 'markers' ), go.Scatter(x=f2[ 0 , :], y=f2[ 1 , :], name=class_names[ 1 ], mode= 'markers' )] mylayout = go.Layout(xaxis=dict(title= "spectral_centroid_mean" ), yaxis=dict(title= "energy_entropy_mean" )) plotly.offline.iplot(go.Figure(data=plots, layout=mylayout))
En el ejemplo de la Sección anterior, hemos visto cómo se diferencian dos características para dos clases de géneros musicales, a partir de respectivos archivos WAV organizados en dos carpetas. El ejemplo 5 muestra cómo se pueden usar las mismas características para entrenar un clasificador SVM simple: cada punto de una cuadrícula en el espacio de características 2D se clasifica luego en cualquiera de las dos clases. Esta es una forma de visualizar la superficie de decisión del clasificador.
# Example5: plot 2 features for 10 2-second samples # from classical and 10 from metal music. # also train an SVM classifier and draw the respective # decision surfaces from pyAudioAnalysis import MidTermFeatures as aF import os import numpy as np from sklearn.svm import SVC import plotly.graph_objs as go import plotly dirs = [ "data/music/classical" , "data/music/metal" ] class_names = [os.path.basename(d) for d in dirs] m_win, m_step, s_win, s_step = 1 , 1 , 0.1 , 0.05 # segment-level feature extraction: features = [] for d in dirs: # get feature matrix for each directory (class) f, files, fn = aF.directory_feature_extraction(d, m_win, m_step, s_win, s_step) features.append(f) # select 2 features and create feature matrices for the two classes: f1 = np.array([features[ 0 ][:, fn.index( 'spectral_centroid_mean' )], features[ 0 ][:, fn.index( 'energy_entropy_mean' )]]) f2 = np.array([features[ 1 ][:, fn.index( 'spectral_centroid_mean' )], features[ 1 ][:, fn.index( 'energy_entropy_mean' )]]) # plot 2D features p1 = go.Scatter(x=f1[ 0 , :], y=f1[ 1 , :], name=class_names[ 0 ], marker=dict(size= 10 ,color= 'rgba(255, 182, 193, .9)' ), mode= 'markers' ) p2 = go.Scatter(x=f2[ 0 , :], y=f2[ 1 , :], name=class_names[ 1 ], marker=dict(size= 10 ,color= 'rgba(100, 100, 220, .9)' ), mode= 'markers' ) mylayout = go.Layout(xaxis=dict(title= "spectral_centroid_mean" ), yaxis=dict(title= "energy_entropy_mean" )) y = np.concatenate((np.zeros(f1.shape[ 1 ]), np.ones(f2.shape[ 1 ]))) f = np.concatenate((f1.T, f2.T), axis = 0 ) # train the svm classifier cl = SVC(kernel= 'rbf' , C= 20 ) cl.fit(f, y) # apply the trained model on the points of a grid x_ = np.arange(f[:, 0 ].min(), f[:, 0 ].max(), 0.002 ) y_ = np.arange(f[:, 1 ].min(), f[:, 1 ].max(), 0.002 ) xx, yy = np.meshgrid(x_, y_) Z = cl.predict(np.c_[xx.ravel(), yy.ravel()]).reshape(xx.shape) / 2 # and visualize the grid on the same plot (decision surfaces) cs = go.Heatmap(x=x_, y=y_, z=Z, showscale= False , colorscale= [[ 0 , 'rgba(255, 182, 193, .3)' ], [ 1 , 'rgba(100, 100, 220, .3)' ]]) mylayout = go.Layout(xaxis=dict(title= "spectral_centroid_mean" ), yaxis=dict(title= "energy_entropy_mean" )) plotly.offline.iplot(go.Figure(data=[p1, p2, cs], layout=mylayout))
Alternativamente, proporciona una funcionalidad envuelta que incluye tanto la extracción de características como el entrenamiento del clasificador. esto se hace en
audioTrainTest.extract_features_and_train()
función, que(a) función de primeras llamadas
MidTermFeatures.multi_directory_feature_extraction()
, que llama MidTermFeatures.directory_feature_extraction()
, para extraer matrices de características para todas las carpetas dadas de archivos de audio (asumiendo que cada carpeta corresponde a una clase) (b) genera matrices X e y, también conocidas como matriz de características para la tarea de clasificación y las respectivas etiquetas de clase. (c) evalúa el clasificador para diferentes parámetros (por ejemplo, C si se seleccionan clasificadores SVM) (d) devuelve los resultados de evaluación impresos y guarda el mejor modelo en un archivo binario (para que lo use otra función para realizar pruebas, como se muestra más adelante) El siguiente ejemplo entrena un clasificador SVM para la tarea de clasificación de música clásica/metal: # Example6: use pyAudioAnalysis wrapper # to extract feature and train SVM classifier # for 20 music (10 classical/10 metal) song samples from pyAudioAnalysis.audioTrainTest import extract_features_and_train mt, st = 1.0 , 0.05 dirs = [ "data/music/classical" , "data/music/metal" ] extract_features_and_train(dirs, mt, mt, st, st, "svm_rbf" , "svm_classical_metal" )
classical metal OVERALL C PRE REC f1 PRE REC f1 ACC f1 0.001 79.4 81.0 80.2 80.6 79.0 79.8 80.0 80.0 0.010 77.2 78.0 77.6 77.8 77.0 77.4 77.5 77.5 0.500 76.5 75.0 75.8 75.5 77.0 76.2 76.0 76.0 1.000 88.2 75.0 81.1 78.3 90.0 83.7 82.5 82.4 5.000 100.0 83.0 90.7 85.5 100.0 92.2 91.5 91.4 best f1 best Acc 10.000 100.0 78.0 87.6 82.0 100.0 90.1 89.0 88.9 20.000 100.0 75.0 85.7 80.0 100.0 88.9 87.5 87.3 Confusion Matrix: cla met cla 41.50 8.50 met 0.00 50.00 Selected params: 5.00000
audioTrainTest.extract_features_and_train() de pyAudioAnalysis lib, es un contenedor que (a) lee todos los archivos de audio organizados en una lista de carpetas y extrae estadísticas de características promediadas a largo plazo (b) luego entrena un clasificador asumiendo que los nombres de las carpetas representan clases de audio.
El modelo entrenado se guardará en
svm_classical_metal
(último argumento de la función), junto con los parámetros de extracción de características (tamaños y pasos de ventana a corto plazo y de segmento). Tenga en cuenta que también se crea otro archivo llamado svm_classical_metalMEANS
, que almacena los parámetros de normalización, es decir, la media y el estándar utilizados para normalizar las funciones de audio antes del entrenamiento y la prueba. Finalmente, aparte de las SVM, el contenedor es compatible con la mayoría de los clasificadores de scikit-learn, como árboles de decisión y aumento de gradiente.Por lo tanto, hemos entrenado un clasificador de audio para distinguir entre dos clases de audio (clásico y metal) en función de los promedios de las estadísticas de características, como se describió anteriormente. Ahora veamos cómo podemos usar el modelo entrenado para predecir la clase de un archivo de audio desconocido . Con este fin, vamos a utilizar pyAudioAnalysis'
audioTrainTest.file_classification()
como se muestra en el Ejemplo 7 : # Example7: use trained model from Example6 # to classify an unknown sample (song) from pyAudioAnalysis import audioTrainTest as aT files_to_test = [ "data/music/test/classical.00095.au.wav" , "data/music/test/metal.00004.au.wav" , "data/music/test/rock.00037.au.wav" ] for f in files_to_test: print( f' {f} :' ) c, p, p_nam = aT.file_classification(f, "svm_classical_metal" , "svm_rbf" ) print( f'P( {p_nam[ 0 ]} = {p[ 0 ]} )' ) print( f'P( {p_nam[ 1 ]} = {p[ 1 ]} )' ) print()
data/music/test/classical .00095 .au.wav: P(classical= 0.63654 ) P(metal= 0.3634828441433037 ) data/music/test/metal .00004 .au.wav: P(classical= 0.15576 ) P(metal= 0.82566 ) data/music/test/rock .00037 .au.wav: P(classical= 0.2757302369241449 ) P(metal= 0.7242697630758552 )
audioTrainTest.file_classification() obtiene el modelo entrenado y la ruta de un archivo de audio desconocido y presenta extracción y predicción del clasificador para el archivo desconocido, devolviendo la clase ganadora (prevista), las clases posteriores y los nombres de las clases respectivas.
El ejemplo anterior mostró cómo podemos aplicar el clasificador de audio entrenado a un archivo de audio desconocido para predecir su etiqueta de audio. Además de eso, pyAudioAnalysis proporciona la función
audioTrainTest.evaluate_model_for_folders()
, que acepta una lista de carpetas, asumiendo que sus nombres base son nombres de clase (como hacemos durante el entrenamiento), y aplica de forma repetitiva un clasificador previamente entrenado en los archivos de audio de cada carpeta. Al final, genera métricas de rendimiento como la matriz de confusión y las curvas ROC: # Example8: use trained model from Example6 # to classify audio files organized in folders # and evaluate the predictions, assuming that # foldernames = classes names as during training from pyAudioAnalysis import audioTrainTest as aT aT.evaluate_model_for_folders([ "data/music/test/classical" , "data/music/test/metal" ], "svm_classical_metal" , "svm_rbf" , "classical" )
da como resultado las siguientes cifras de la matriz de confusión, precisión/recuperación/f1 por clase, y curva de precisión/recuperación y curva ROC para una "clase de interés" (aquí hemos proporcionado clásica). Tenga en cuenta que las subparcelas 3 y 4 evalúan el clasificador como un detector de la clase "clásica" (último argumento). Por ejemplo, el último gráfico muestra la tasa de verdaderos positivos frente a la tasa de falsos positivos, y esto se logra simulando el umbral de la parte posterior de la clase de interés (clásica): a medida que aumenta el umbral de probabilidad, aumentan las tasas de verdaderos positivos y falsos negativos. , la pregunta es: ¿qué tan "empinado" es el aumento de la tasa positiva verdadera? Puede encontrar más información sobre la curva ROC . La curva de precisión/recuperación es equivalente a ROC, pero muestra ambas métricas en el mismo gráfico en el eje y para diferentes umbrales de la parte posterior que se muestra en el eje x (más información ).
En nuestro ejemplo, podemos ver que para un umbral de probabilidad de, digamos, 0.6 podemos tener una Precisión del 100 % con alrededor del 80 % de Recall para clásico: esto significa que todos los archivos detectados serán de hecho clásicos, mientras que estaremos "perdiendo" casi 1 de cada 5 canciones "clásicas" como metal. Otra nota importante aquí es que no hay un "mejor" punto de operación del clasificador, eso depende de la aplicación individual .
En este artículo, demostramos cómo se puede usar la regresión para detectar el tono de un segmento de canto coral, sin usar ningún enfoque de procesamiento de señales (por ejemplo, el método de autocorrelación). Con este fin, hemos utilizado parte del , que es un conjunto de grabaciones acapella con las respectivas anotaciones de tono . Aquí, hemos seleccionado usar este conjunto de datos para producir anotaciones de tono a nivel de segmento: dividimos las grabaciones de canto en segmentos pequeños (0,5 segundos) y para cada segmento, calculamos la media y la desviación estándar del tono (que proporciona el conjunto de datos). ). Estas dos métricas son f0_mean y f0_std respectivamente y son los dos valores de regresión objetivo que se muestran en el siguiente código. A continuación puede escuchar una muestra de 0,5 segundos con una f0 baja y una desviación de f0 baja:
Obtener segmentos de 0,5 segundos del de señas corales conduce a miles de muestras, pero para fines de demostración de este artículo, hemos utilizado alrededor de 120 muestras de entrenamiento y 120 de prueba, disponibles en
data/regression/f0/segments_train
y data/regression/f0/segments_train folders
del repositorio de github. Nuevamente, para demostrar el entrenamiento y las pruebas del modelo de regresión, estamos usando que trata la segmentación de audio de la siguiente manera:En nuestro ejemplo,
data/regression/f0/segments_train
contiene 120 archivos WAV y dos archivos CSV de 120 líneas llamados f0.csv y f0_std.csv. Cada CSV corresponde a una tarea de regresión separada y cada línea del CSV corresponde a la verdad básica del archivo de audio respectivo. Para entrenar, evaluar y guardar los dos modelos de regresión para nuestro ejemplo, se usa el siguiente código: # Example9: # Train two linear SVM regression models # that map song segments to pitch and pitch deviation # The following function searches for .csv files in the # input folder. For each csv of the format <filename>,<value> # a separate regresion model is trained from pyAudioAnalysis import audioTrainTest as aT aT.feature_extraction_train_regression( "data/regression/f0/segments_train" , 0.5 , 0.5 , 0.05 , 0.05 , "svm" , "singing" , False )
Ya que
data/regression/f0/segments_train
contiene dos CSV, a saber f0.csv
y f0_std.csv
, el código anterior da como resultado dos modelos: singing_f0
y singing_f0_std
( singing
El prefijo se proporciona como un séptimo argumento en la función anterior y se usa para todos los modelos entrenados).Asimismo, este es el resultado del proceso de evaluación ejecutado internamente por la
audioTrainTest.feature_extraction_train_regression()
función: Analyzing file 1 of 120 : data/regression/f0/segments_train/CSD_ER_alto_1.wav_segments_263 .1595 .wav Analyzing file 2 of 120 : data/regression/f0/segments_train/CSD_ER_alto_1.wav_segments_264 .957 .wav Analyzing file 3 of 120 : data/regression/f0/segments_train/CSD_ER_alto_1.wav_segments_301 .632 .wav Analyzing file 4 of 120 : data/regression/f0/segments_train/CSD_ER_alto_1.wav_segments_328 .748 .wav Analyzing file 5 of 120 : data/regression/f0/segments_train/CSD_ER_alto_1.wav_segments_331 .2835 .wav ... ... ... Analyzing file 119 of 120 : data/regression/f0/segments_train/CSD_ND_alto_4.wav_segments_383 .483 .wav Analyzing file 120 of 120 : data/regression/f0/segments_train/CSD_ND_alto_4.wav_segments_394 .315 .wav Feature extraction complexity ratio: 44.7 x realtime Regression task f0_std Param MSE T-MSE R-MSE 0.0010 736.98 10.46 661.43 0.0050 585.38 9.64 573.52 0.0100 522.73 9.17 539.87 0.0500 529.10 7.41 657.36 0.1000 379.13 6.73 541.03 0.2500 361.75 5.09 585.60 0.5000 323.20 3.88 522.12 best 1.0000 386.30 2.58 590.08 5.0000 782.14 0.99 548.65 10.0000 1140.95 0.47 529.20 Selected params: 0.50000 Regression task f0 Param MSE T-MSE R-MSE 0.0010 3103.83 44.65 3121.97 0.0050 2772.07 41.38 3098.40 0.0100 2293.79 37.57 2935.42 0.0500 1206.49 19.69 2999.49 0.1000 1012.29 13.94 3115.49 0.2500 839.82 8.64 3147.30 0.5000 758.04 5.62 2917.62 1.0000 689.12 3.53 3087.71 best 5.0000 892.52 1.07 3061.10 10.0000 1158.60 0.47 2889.27 Selected params: 1.00000
Ahora, una vez que los dos modelos de regresión se entrenan, evalúan y guardan, podemos usarlos para asignar cualquier segmento de audio a f0 o f0_std. Example10 demuestra cómo hacer esto usando
audioTrainTest.file_regression()
: # Example10 # load trained regression model for f0 and apply it to a folder # of WAV files and evaluate (use csv file with ground truths) import glob import csv import os import numpy as np import plotly.graph_objs as go import plotly from pyAudioAnalysis import audioTrainTest as aT # read all files in testing folder: wav_files_to_test = glob.glob( "data/regression/f0/segments_test/*.wav" ) ground_truths = {} with open( 'data/regression/f0/segments_test/f0.csv' , 'r' ) as file: reader = csv.reader(file, delimiter = ',' ) for row in reader: ground_truths[row[ 0 ]] = float(row[ 1 ]) estimated_val, gt_val = [], [] for w in wav_files_to_test: # for each audio file # get the estimates for all regression models starting with "singing" values, tasks = aT.file_regression(w, "singing" , "svm" ) # check if there is ground truth available for the current file if os.path.basename(w) in ground_truths: # ... and append ground truth and estimated values # for the f0 task estimated_val.append(values[tasks.index( 'f0' )]) gt_val.append(ground_truths[os.path.basename(w)]) # compute mean square error: mse = ((np.array(estimated_val) - np.array(gt_val))** 2 ).mean() print( f'Testing MSE= {mse} ' ) # plot real vs predicted results p = go.Scatter(x=gt_val, y=estimated_val, mode= 'markers' ) mylayout = go.Layout(xaxis=dict(title= "f0 real" ), yaxis=dict(title= "f0 predicted" ), showlegend= False ) plotly.offline.iplot(go.Figure(data=[p, go.Scatter(x=[min(gt_val+ estimated_val), max(gt_val+ estimated_val)], y=[min(gt_val+ estimated_val), max(gt_val+ estimated_val)])], layout=mylayout))
En este ejemplo, demostramos cómo
audioTrainTest.file_regression()
se puede utilizar para un conjunto de archivos de un conjunto de datos de prueba. Tenga en cuenta que esta función devuelve decisiones y nombres de tareas para todas las tareas de regresión disponibles que comienzan con el prefijo proporcionado (en nuestro caso, "cantando"). Los resultados se muestran a continuación: podemos ver que los valores real y predicho están bastante cerca para la tarea f0. Testing MSE=492.74
audioTrainTest.feature_extraction_train_regression() lee una carpeta de archivos WAV y asume que cada CSV de formato (<ruta>,<valor>) es un archivo de regresión real. Luego extrae características de audio, entrena y guarda el número respectivo de modelos usando un prefijo (proporcionado también como argumento). audioTrainTest.file_regression() lee los modelos guardados y devuelve resultados de regresión previstos para todas las tareas.
Hasta ahora, hemos visto cómo entrenar modelos supervisados que asignan estadísticas de características de audio a nivel de segmento a etiquetas de clase (clasificación de audio) o objetivos de valor real (regresión de audio). Además, hemos visto cómo utilizar estos modelos para predecir la etiqueta de un archivo de audio desconocido, por ejemplo, una expresión verbal o una canción completa o un segmento de una canción. En todos estos casos, la suposición seguida fue que las señales de audio desconocidas pertenecían a una sola etiqueta . Por ejemplo, una canción pertenece a un género particular, un segmento de canto tiene un valor de tono particular y una expresión verbal tiene una emoción particular. Sin embargo, en las aplicaciones del mundo real, hay muchos casos en los que las señales de audio no son segmentos de contenido homogéneo, sino flujos de audio complejos que contienen muchos segmentos sucesivos de diferentes etiquetas de contenido. Una grabación de un diálogo del mundo real, por ejemplo, es una secuencia de etiquetas de identidades o emociones del hablante.
Las grabaciones del mundo real no son segmentos de contenido homogéneo sino secuencias de segmentos de diferentes etiquetas.
Por esa razón, la segmentación de audio es un paso importante del análisis de audio y se trata de segmentar una grabación de audio larga en una secuencia de segmentos que son de contenido homogéneo. La definición de homogeneidad es relativa al dominio de la aplicación: si, por ejemplo, estamos interesados en el reconocimiento del hablante, un segmento se considera homogéneo si pertenece al mismo hablante.
En el Ejemplo 6, habíamos entrenado un modelo que clasifica segmentos de música desconocidos en "metal" y "clásico" (el modelo se guardó en el archivo
svm_classical_metal
). Usemos este modelo para segmentar una grabación de 30 segundos que contiene partes de metal y clásicas (no superpuestas). Esta grabación se almacena en data/music/metal_classical_mix.wav
del código del artículo. También, data/music/metal_classical_mix.segment
contiene el respectivo archivo de anotación de verdad en tierra con el formato <start_segment_sec>\t<end_segment_sec>\t<segment_label>. Este es el archivo de verdad del terreno: 0 7.5 classical 7.5 15 metal 15 19 classical 19 29 metal
La funcionalidad de segmentación supervisada de ventana fija se implementa en función
audioSegmentation.mid_term_file_classification()
, como se muestra en el Ejemplo 11 : # Example 11 # Supervised audio segmentation example: # - Apply model "svm_classical_metal" to achieve fix-sized, supervised audio segmentation # on file data/music/metal_classical_mix.wav # - Function audioSegmentation.mid_term_file_classification() uses pretrained model and applies # the mid-term step that has been used when training the model (1 sec in our case as shown in Example6) # - data/music/metal_classical_mix.segments contains the ground truth of the audio file from pyAudioAnalysis.audioSegmentation import mid_term_file_classification, labels_to_segments from pyAudioAnalysis.audioTrainTest import load_model labels, class_names, _, _ = mid_term_file_classification( "data/music/metal_classical_mix.wav" , "svm_classical_metal" , "svm_rbf" , True , "data/music/metal_classical_mix.segments" ) print( "\nFix-sized segments:" ) for il, l in enumerate(labels): print( f'fix-sized segment {il} : {class_names[int(l)]} ' ) # load the parameters of the model (actually we just want the mt_step here): cl, m, s, m_classes, mt_win, mt_step, s_win, s_step, c_beat = load_model( "svm_classical_metal" ) # print "merged" segments (use labels_to_segments()) print( "\nSegments:" ) segs, c = labels_to_segments(labels, mt_step) for iS, seg in enumerate(segs): print( f'segment {iS} {seg[ 0 ]} sec - {seg[ 1 ]} sec: {class_names[int(c[iS])]} ' )
audioSegmentation.mid_term_file_classification()
devuelve una lista de identificadores de etiqueta (uno para cada ventana de segmento de tamaño fijo), una lista de nombres de clase y la matriz de precisión y confusión (si también se proporciona la verdad básica, como en el ejemplo anterior). los labels
list corresponde a segmentos de tamaño fijo de longitud igual al paso de segmento utilizado durante el entrenamiento del modelo (1 segundo en el ejemplo anterior, según el Ejemplo 6). Por eso usamos audioTrainTest.load_model()
, para cargar la ventana del segmento directamente desde el archivo del modelo. También, usamos audioSegmentation.labels_to_segments()
para generar la lista de segmentos finales, basada en la ruta de fusión simple (es decir, concatenar segmentos sucesivos de 1 segundo que tienen la misma etiqueta). El resultado del código anterior es el siguiente (el rojo corresponde a la realidad del terreno y el azul a las etiquetas de los segmentos previstos): Overall Accuracy: 0.79 Fix-sized segments: fix-sized segment 0 : classical fix-sized segment 1 : classical fix-sized segment 2 : classical fix-sized segment 3 : classical fix-sized segment 4 : classical fix-sized segment 5 : classical fix-sized segment 6 : classical fix-sized segment 7 : metal fix-sized segment 8 : metal fix-sized segment 9 : metal fix-sized segment 10 : metal fix-sized segment 11 : metal fix-sized segment 12 : classical fix-sized segment 13 : metal fix-sized segment 14 : metal fix-sized segment 15 : classical fix-sized segment 16 : classical fix-sized segment 17 : classical fix-sized segment 18 : metal fix-sized segment 19 : metal fix-sized segment 20 : classical fix-sized segment 21 : metal fix-sized segment 22 : classical fix-sized segment 23 : classical fix-sized segment 24 : metal fix-sized segment 25 : metal fix-sized segment 26 : metal fix-sized segment 27 : metal fix-sized segment 28 : metal fix-sized segment 29 : metal Segments: segment 0 0.0 sec - 7.0 sec: classical segment 1 7.0 sec - 12.0 sec: metal segment 2 12.0 sec - 13.0 sec: classical segment 3 13.0 sec - 15.0 sec: metal segment 4 15.0 sec - 18.0 sec: classical segment 5 18.0 sec - 20.0 sec: metal segment 6 20.0 sec - 21.0 sec: classical segment 7 21.0 sec - 22.0 sec: metal segment 8 22.0 sec - 24.0 sec: classical segment 9 24.0 sec - 29.0 sec: metal
audioSegmentation.labels_to_segments()
devuelve la información más "compacta" y útil para el "usuario" final. Además, tenga en cuenta que los errores de segmentación son:(a) debido a errores de clasificación del clasificador de segmentos (p. ej., los segmentos 22 y 23 se clasifican erróneamente como clásicos cuando su verdadera etiqueta es metal o
(b) debido a problemas de resolución de tiempo : por ejemplo, de acuerdo con la realidad básica, el primer segmento de música clásica termina en 7,5 segundos, mientras que nuestro modelo se aplica cada 1 segundo, por lo que lo mejor que logrará esta metodología de ventana fija es reconocer música clásica hasta 7 u 8 seg. Obviamente, esto se puede manejar a través de un paso más pequeño en la ventana del segmento (es decir, introduciendo una superposición de segmento), sin embargo, esto será con un aumento significativo en las demandas computacionales (se realizarán más predicciones a nivel de segmento).
Segmentación musical
La extracción de partes estructurales de una pista de música es un caso de uso típico en el que se puede utilizar el análisis de audio no supervisado. Dado que obviamente es bastante difícil tener un clasificador que distinga entre partes de canciones, podemos responder a la pregunta: ¿puedes agrupar segmentos de canciones para que los segmentos del mismo grupo suenen como si pertenecieran a la misma parte de una canción? En el siguiente ejemplo, "Billie Jean" de M. Jackson se utiliza como entrada para el proceso de extracción de características a nivel de segmento descrito anteriormente y se aplica un agrupamiento simple de k-medias en las secuencias de vectores de características resultantes. Luego, los segmentos de cada grupo se concatenan en una grabación artificial y se guardan en archivos de audio. Cada "grabación de grupo" artificial muestra cómo se pueden agrupar las partes de la canción y si esta agrupación tiene algún sentido en términos de estructura musical. El código se muestra en el Ejemplo 12 :
# Example 12: Unsupervised Music Segmentation # # This example groups of song segments to clusters of similar content import os, sklearn.cluster from pyAudioAnalysis.MidTermFeatures import mid_feature_extraction as mT from pyAudioAnalysis.audioBasicIO import read_audio_file, stereo_to_mono from pyAudioAnalysis.audioSegmentation import labels_to_segments from pyAudioAnalysis.audioTrainTest import normalize_features import numpy as np import scipy.io.wavfile as wavfile import IPython # read signal and get normalized segment feature statistics: input_file = "data/music/billie_jean.wav" fs, x = read_audio_file(input_file) mt_size, mt_step, st_win = 5 , 0.5 , 0.1 [mt_feats, st_feats, _] = mT(x, fs, mt_size * fs, mt_step * fs, round(fs * st_win), round(fs * st_win * 0.5 )) (mt_feats_norm, MEAN, STD) = normalize_features([mt_feats.T]) mt_feats_norm = mt_feats_norm[ 0 ].T # perform clustering n_clusters = 5 x_clusters = [np.zeros((fs, )) for i in range(n_clusters)] k_means = sklearn.cluster.KMeans(n_clusters=n_clusters) k_means.fit(mt_feats_norm.T) cls = k_means.labels_ # save clusters to concatenated wav files segs, c = labels_to_segments(cls, mt_step) # convert flags to segment limits for sp in range(n_clusters): count_cl = 0 for i in range(len(c)): # for each segment in each cluster (>2 secs long) if c[i] == sp and segs[i, 1 ]-segs[i, 0 ] > 2 : count_cl += 1 # get the signal and append it to the cluster's signal (followed by some silence) cur_x = x[int(segs[i, 0 ] * fs): int(segs[i, 1 ] * fs)] x_clusters[sp] = np.append(x_clusters[sp], cur_x) x_clusters[sp] = np.append(x_clusters[sp], np.zeros((fs,))) # write cluster's signal into a WAV file print( f'cluster {sp} : {count_cl} segments {len(x_clusters[sp])/float(fs)} sec total dur' ) wavfile.write( f'cluster_ {sp} .wav' , fs, np.int16(x_clusters[sp])) IPython.display.display(IPython.display.Audio( f'cluster_ {sp} .wav' ))
Este es claramente el estribillo de la canción, repetido dos veces (aunque la segunda vez es mucho más larga ya que incluye más repeticiones sucesivas y un pequeño solo)
El grupo 2 tiene un solo segmento que corresponde a la introducción de la canción.
Cluster 3 es el pre-estribillo de la canción.
El cuarto grupo contiene segmentos de los versos de la canción (si excluye el segmento pequeño al principio). El quinto grupo no se muestra ya que solo incluía un segmento muy corto casi silencioso al comienzo de la canción. En todos los casos, los grupos representaban (con algunos errores, por supuesto) componentes estructurales de la canción, incluso usando este enfoque muy simple y sin hacer uso de ningún conocimiento supervisado "externo", aparte de características similares, pueden significar contenido musical similar.
Los grupos de segmentos de canciones pueden corresponder a elementos estructurales de canciones si se utilizan las funciones de audio apropiadas.Por último, tenga en cuenta que ejecutar el código anterior puede dar como resultado el mismo agrupamiento pero con un orden diferente de ID de clúster (y, por lo tanto, orden en los archivos de audio resultantes). Esto probablemente se deba a la semilla aleatoria k-means.
Diarización del hablante
Esta es la tarea que, dada una grabación de voz desconocida, responde a la pregunta: "¿quién habla cuándo?". En aras de la simplicidad, supongamos que ya conocemos el número de hablantes en la grabación. ¿Cuál es la forma más sencilla de resolver esta tarea? Obviamente, primero extraiga características de audio a nivel de segmento y luego realice algún tipo de agrupación, con la esperanza de que las agrupaciones resultantes correspondan a las identificaciones de los altavoces. En el siguiente ejemplo (13), usamos exactamente la misma canalización que la que se siguió en el Ejemplo 12, donde agrupamos una canción en sus partes estructurales. Sólo hemos cambiado el tamaño de la ventana del segmento a 2 seg con un paso de 0,1 seg y una ventana de corto plazo más pequeña (50 mseg), ya que las señales de voz se caracterizan, en general, por cambios más rápidos en sus atributos principales, debido a la existencia de fonemas muy diferentes, algunos de los cuales duran apenas unos segundos (en cambio, la nota musical dura varios mseg, incluso en las músicas más rápidas). Entonces, el Ejemplo 13 usa la misma lógica de agrupamiento de vectores de funciones de audio. Esta vez, la señal de entrada es una señal de voz con 4 altavoces (esto se sabe de antemano), por lo que establecemos nuestro tamaño de grupo de kmeans en 4: import os, sklearn.cluster from pyAudioAnalysis.MidTermFeatures import mid_feature_extraction as mT from pyAudioAnalysis.audioBasicIO import read_audio_file, stereo_to_mono from pyAudioAnalysis.audioSegmentation import labels_to_segments from pyAudioAnalysis.audioTrainTest import normalize_features import numpy as np import scipy.io.wavfile as wavfile import IPython # read signal and get normalized segment feature statistics: input_file = "data/diarization_example.wav" fs, x = read_audio_file(input_file) mt_size, mt_step, st_win = 2 , 0.1 , 0.05 [mt_feats, st_feats, _] = mT(x, fs, mt_size * fs, mt_step * fs, round(fs * st_win), round(fs * st_win * 0.5 )) (mt_feats_norm, MEAN, STD) = normalize_features([mt_feats.T]) mt_feats_norm = mt_feats_norm[ 0 ].T # perform clustering n_clusters = 4 x_clusters = [np.zeros((fs, )) for i in range(n_clusters)] k_means = sklearn.cluster.KMeans(n_clusters=n_clusters) k_means.fit(mt_feats_norm.T) cls = k_means.labels_ # save clusters to concatenated wav files segs, c = labels_to_segments(cls, mt_step) # convert flags to segment limits for sp in range(n_clusters): count_cl = 0 for i in range(len(c)): # for each segment in each cluster (>2 secs long) if c[i] == sp and segs[i, 1 ]-segs[i, 0 ] > 2 : count_cl += 1 # get the signal and append it to the cluster's signal (followed by some silence) cur_x = x[int(segs[i, 0 ] * fs): int(segs[i, 1 ] * fs)] x_clusters[sp] = np.append(x_clusters[sp], cur_x) x_clusters[sp] = np.append(x_clusters[sp], np.zeros((fs,))) # write cluster's signal into a WAV file print( f'speaker {sp} : {count_cl} segments {len(x_clusters[sp])/float(fs)} sec total dur' ) wavfile.write( f'diarization_cluster_ {sp} .wav' , fs, np.int16(x_clusters[sp])) IPython.display.display(IPython.display.Audio( f'diarization_cluster_ {sp} .wav' ))