Transferir el aprendizaje con YAMNet para la clasificación de sonido ambiental

Ver en TensorFlow.org Ejecutar en Google Colab Ver en GitHub Descargar libreta Ver modelo TF Hub

YAMNet es una red neuronal profunda preentrenada que puede predecir eventos de audio de 521 clases , como risas, ladridos o sirenas.

En este tutorial aprenderá a:

  • Cargue y use el modelo YAMNet para la inferencia.
  • Cree un nuevo modelo utilizando las incrustaciones de YAMNet para clasificar los sonidos de perros y gatos.
  • Evalúe y exporte su modelo.

Importar TensorFlow y otras bibliotecas

Comience instalando TensorFlow I/O , lo que le facilitará la carga de archivos de audio desde el disco.

pip install tensorflow_io
import os

from IPython import display
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd

import tensorflow as tf
import tensorflow_hub as hub
import tensorflow_io as tfio

Acerca de YAMNet

YAMNet es una red neuronal preentrenada que emplea la arquitectura de convolución separable en profundidad MobileNetV1 . Puede usar una forma de onda de audio como entrada y hacer predicciones independientes para cada uno de los 521 eventos de audio del corpus AudioSet .

Internamente, el modelo extrae "cuadros" de la señal de audio y procesa lotes de estos cuadros. Esta versión del modelo utiliza fotogramas de 0,96 segundos de duración y extrae un fotograma cada 0,48 segundos.

El modelo acepta una matriz 1-D float32 Tensor o NumPy que contiene una forma de onda de longitud arbitraria, representada como muestras de un solo canal (mono) de 16 kHz en el rango [-1.0, +1.0] . Este tutorial contiene código para ayudarlo a convertir archivos WAV al formato compatible.

El modelo devuelve 3 salidas, incluidos los puntajes de clase, las incrustaciones (que usará para el aprendizaje de transferencia) y el espectrograma log mel. Puede encontrar más detalles aquí .

Un uso específico de YAMNet es como un extractor de funciones de alto nivel: la salida de incrustación de 1024 dimensiones. Utilizará las características de entrada del modelo base (YAMNet) y las introducirá en su modelo menos profundo que consta de una capa oculta tf.keras.layers.Dense . Luego, entrenará la red en una pequeña cantidad de datos para la clasificación de audio sin requerir una gran cantidad de datos etiquetados y entrenamiento de extremo a extremo. (Esto es similar a la transferencia de aprendizaje para la clasificación de imágenes con TensorFlow Hub para obtener más información).

Primero, probará el modelo y verá los resultados de clasificar el audio. A continuación, construirá la canalización de preprocesamiento de datos.

Cargando YAMNet desde TensorFlow Hub

Vas a usar una YAMNet preentrenada de Tensorflow Hub para extraer las incrustaciones de los archivos de sonido.

Cargar un modelo desde TensorFlow Hub es sencillo: elija el modelo, copie su URL y use la función de load .

yamnet_model_handle = 'https://tfhub.dev/google/yamnet/1'
yamnet_model = hub.load(yamnet_model_handle)

Con el modelo cargado, puede seguir el tutorial de uso básico de YAMNet y descargar un archivo WAV de muestra para ejecutar la inferencia.

testing_wav_file_name = tf.keras.utils.get_file('miaow_16k.wav',
                                                'https://storage.googleapis.com/audioset/miaow_16k.wav',
                                                cache_dir='./',
                                                cache_subdir='test_data')

print(testing_wav_file_name)
Downloading data from https://storage.googleapis.com/audioset/miaow_16k.wav
221184/215546 [==============================] - 0s 0us/step
229376/215546 [===============================] - 0s 0us/step
./test_data/miaow_16k.wav

Necesitará una función para cargar archivos de audio, que también se usará más adelante cuando trabaje con los datos de entrenamiento. (Obtenga más información sobre la lectura de archivos de audio y sus etiquetas en Reconocimiento de audio simple .

# Utility functions for loading audio files and making sure the sample rate is correct.

@tf.function
def load_wav_16k_mono(filename):
    """ Load a WAV file, convert it to a float tensor, resample to 16 kHz single-channel audio. """
    file_contents = tf.io.read_file(filename)
    wav, sample_rate = tf.audio.decode_wav(
          file_contents,
          desired_channels=1)
    wav = tf.squeeze(wav, axis=-1)
    sample_rate = tf.cast(sample_rate, dtype=tf.int64)
    wav = tfio.audio.resample(wav, rate_in=sample_rate, rate_out=16000)
    return wav
testing_wav_data = load_wav_16k_mono(testing_wav_file_name)

_ = plt.plot(testing_wav_data)

# Play the audio file.
display.Audio(testing_wav_data,rate=16000)
2022-01-26 08:07:19.084427: W tensorflow_io/core/kernels/audio_video_mp3_kernels.cc:271] libmp3lame.so.0 or lame functions are not available
WARNING:tensorflow:Using a while_loop for converting IO>AudioResample
WARNING:tensorflow:Using a while_loop for converting IO>AudioResample

png

Cargue el mapeo de clases

Es importante cargar los nombres de clase que YAMNet puede reconocer. El archivo de mapeo está presente en yamnet_model.class_map_path() en formato CSV.

class_map_path = yamnet_model.class_map_path().numpy().decode('utf-8')
class_names =list(pd.read_csv(class_map_path)['display_name'])

for name in class_names[:20]:
  print(name)
print('...')
Speech
Child speech, kid speaking
Conversation
Narration, monologue
Babbling
Speech synthesizer
Shout
Bellow
Whoop
Yell
Children shouting
Screaming
Whispering
Laughter
Baby laughter
Giggle
Snicker
Belly laugh
Chuckle, chortle
Crying, sobbing
...

Ejecutar inferencia

YAMNet proporciona puntajes de clase a nivel de marco (es decir, 521 puntajes para cada marco). Para determinar las predicciones a nivel de clip, las puntuaciones se pueden agregar por clase a través de fotogramas (p. ej., utilizando la agregación media o máxima). Esto se hace a continuación mediante scores_np.mean(axis=0) . Finalmente, para encontrar la clase con la puntuación más alta a nivel de clip, se toma el máximo de las 521 puntuaciones agregadas.

scores, embeddings, spectrogram = yamnet_model(testing_wav_data)
class_scores = tf.reduce_mean(scores, axis=0)
top_class = tf.argmax(class_scores)
inferred_class = class_names[top_class]

print(f'The main sound is: {inferred_class}')
print(f'The embeddings shape: {embeddings.shape}')
The main sound is: Animal
The embeddings shape: (13, 1024)

Conjunto de datos ESC-50

El conjunto de datos ESC-50 ( Piczak, 2015 ) es una colección etiquetada de 2000 grabaciones de audio ambientales de cinco segundos de duración. El conjunto de datos consta de 50 clases, con 40 ejemplos por clase.

Descargue el conjunto de datos y extráigalo.

_ = tf.keras.utils.get_file('esc-50.zip',
                        'https://github.com/karoldvl/ESC-50/archive/master.zip',
                        cache_dir='./',
                        cache_subdir='datasets',
                        extract=True)
Downloading data from https://github.com/karoldvl/ESC-50/archive/master.zip
645103616/Unknown - 47s 0us/step

Explora los datos

Los metadatos de cada archivo se especifican en el archivo csv en ./datasets/ESC-50-master/meta/esc50.csv

y todos los archivos de audio están en ./datasets/ESC-50-master/audio/

Creará un DataFrame de pandas con el mapeo y lo usará para tener una vista más clara de los datos.

esc50_csv = './datasets/ESC-50-master/meta/esc50.csv'
base_data_path = './datasets/ESC-50-master/audio/'

pd_data = pd.read_csv(esc50_csv)
pd_data.head()

Filtrar los datos

Ahora que los datos están almacenados en DataFrame , aplique algunas transformaciones:

  • Filtre las filas y use solo las clases seleccionadas: dog y cat . Si desea utilizar otras clases, aquí es donde puede elegirlas.
  • Modifique el nombre del archivo para tener la ruta completa. Esto facilitará la carga más adelante.
  • Cambie los objetivos para que estén dentro de un rango específico. En este ejemplo, dog permanecerá en 0 , pero cat se convertirá en 1 en lugar de su valor original de 5 .
my_classes = ['dog', 'cat']
map_class_to_id = {'dog':0, 'cat':1}

filtered_pd = pd_data[pd_data.category.isin(my_classes)]

class_id = filtered_pd['category'].apply(lambda name: map_class_to_id[name])
filtered_pd = filtered_pd.assign(target=class_id)

full_path = filtered_pd['filename'].apply(lambda row: os.path.join(base_data_path, row))
filtered_pd = filtered_pd.assign(filename=full_path)

filtered_pd.head(10)

Cargue los archivos de audio y recupere incrustaciones

Aquí aplicará load_wav_16k_mono y preparará los datos WAV para el modelo.

Al extraer incrustaciones de los datos WAV, obtiene una matriz de forma (N, 1024) donde N es la cantidad de fotogramas que encontró YAMNet (uno por cada 0,48 segundos de audio).

Su modelo usará cada cuadro como una entrada. Por lo tanto, debe crear una nueva columna que tenga un marco por fila. También debe expandir las etiquetas y la columna de fold para reflejar correctamente estas nuevas filas.

La columna de fold expandida mantiene los valores originales. No puede mezclar cuadros porque, al realizar las divisiones, podría terminar teniendo partes del mismo audio en diferentes divisiones, lo que haría que sus pasos de validación y prueba fueran menos efectivos.

filenames = filtered_pd['filename']
targets = filtered_pd['target']
folds = filtered_pd['fold']

main_ds = tf.data.Dataset.from_tensor_slices((filenames, targets, folds))
main_ds.element_spec
(TensorSpec(shape=(), dtype=tf.string, name=None),
 TensorSpec(shape=(), dtype=tf.int64, name=None),
 TensorSpec(shape=(), dtype=tf.int64, name=None))
def load_wav_for_map(filename, label, fold):
  return load_wav_16k_mono(filename), label, fold

main_ds = main_ds.map(load_wav_for_map)
main_ds.element_spec
WARNING:tensorflow:Using a while_loop for converting IO>AudioResample
WARNING:tensorflow:Using a while_loop for converting IO>AudioResample
(TensorSpec(shape=<unknown>, dtype=tf.float32, name=None),
 TensorSpec(shape=(), dtype=tf.int64, name=None),
 TensorSpec(shape=(), dtype=tf.int64, name=None))
# applies the embedding extraction model to a wav data
def extract_embedding(wav_data, label, fold):
  ''' run YAMNet to extract embedding from the wav data '''
  scores, embeddings, spectrogram = yamnet_model(wav_data)
  num_embeddings = tf.shape(embeddings)[0]
  return (embeddings,
            tf.repeat(label, num_embeddings),
            tf.repeat(fold, num_embeddings))

# extract embedding
main_ds = main_ds.map(extract_embedding).unbatch()
main_ds.element_spec
(TensorSpec(shape=(1024,), dtype=tf.float32, name=None),
 TensorSpec(shape=(), dtype=tf.int64, name=None),
 TensorSpec(shape=(), dtype=tf.int64, name=None))

dividir los datos

Utilizará la columna de fold para dividir el conjunto de datos en conjuntos de entrenamiento, validación y prueba.

ESC-50 está organizado en cinco fold de validación cruzada de tamaño uniforme, de modo que los clips de la misma fuente original siempre están en el mismo fold ; obtenga más información en el documento ESC: Conjunto de datos para clasificación de sonido ambiental .

El último paso es eliminar la columna de fold del conjunto de datos, ya que no la usará durante el entrenamiento.

cached_ds = main_ds.cache()
train_ds = cached_ds.filter(lambda embedding, label, fold: fold < 4)
val_ds = cached_ds.filter(lambda embedding, label, fold: fold == 4)
test_ds = cached_ds.filter(lambda embedding, label, fold: fold == 5)

# remove the folds column now that it's not needed anymore
remove_fold_column = lambda embedding, label, fold: (embedding, label)

train_ds = train_ds.map(remove_fold_column)
val_ds = val_ds.map(remove_fold_column)
test_ds = test_ds.map(remove_fold_column)

train_ds = train_ds.cache().shuffle(1000).batch(32).prefetch(tf.data.AUTOTUNE)
val_ds = val_ds.cache().batch(32).prefetch(tf.data.AUTOTUNE)
test_ds = test_ds.cache().batch(32).prefetch(tf.data.AUTOTUNE)

Crea tu modelo

¡Hiciste la mayor parte del trabajo! A continuación, defina un modelo secuencial muy simple con una capa oculta y dos salidas para reconocer gatos y perros a partir de sonidos.

my_model = tf.keras.Sequential([
    tf.keras.layers.Input(shape=(1024), dtype=tf.float32,
                          name='input_embedding'),
    tf.keras.layers.Dense(512, activation='relu'),
    tf.keras.layers.Dense(len(my_classes))
], name='my_model')

my_model.summary()
Model: "my_model"
_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 dense (Dense)               (None, 512)               524800    
                                                                 
 dense_1 (Dense)             (None, 2)                 1026      
                                                                 
=================================================================
Total params: 525,826
Trainable params: 525,826
Non-trainable params: 0
_________________________________________________________________
my_model.compile(loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True),
                 optimizer="adam",
                 metrics=['accuracy'])

callback = tf.keras.callbacks.EarlyStopping(monitor='loss',
                                            patience=3,
                                            restore_best_weights=True)
history = my_model.fit(train_ds,
                       epochs=20,
                       validation_data=val_ds,
                       callbacks=callback)
Epoch 1/20
15/15 [==============================] - 6s 49ms/step - loss: 0.7811 - accuracy: 0.8229 - val_loss: 0.4866 - val_accuracy: 0.9125
Epoch 2/20
15/15 [==============================] - 0s 18ms/step - loss: 0.3385 - accuracy: 0.8938 - val_loss: 0.2185 - val_accuracy: 0.8813
Epoch 3/20
15/15 [==============================] - 0s 18ms/step - loss: 0.3091 - accuracy: 0.9021 - val_loss: 0.4290 - val_accuracy: 0.8813
Epoch 4/20
15/15 [==============================] - 0s 18ms/step - loss: 0.5354 - accuracy: 0.9062 - val_loss: 0.2074 - val_accuracy: 0.9125
Epoch 5/20
15/15 [==============================] - 0s 18ms/step - loss: 0.4651 - accuracy: 0.9333 - val_loss: 0.6857 - val_accuracy: 0.8813
Epoch 6/20
15/15 [==============================] - 0s 18ms/step - loss: 0.2489 - accuracy: 0.9167 - val_loss: 0.3640 - val_accuracy: 0.8750
Epoch 7/20
15/15 [==============================] - 0s 17ms/step - loss: 0.2020 - accuracy: 0.9292 - val_loss: 0.2158 - val_accuracy: 0.9125
Epoch 8/20
15/15 [==============================] - 0s 16ms/step - loss: 0.4550 - accuracy: 0.9208 - val_loss: 0.9893 - val_accuracy: 0.8750
Epoch 9/20
15/15 [==============================] - 0s 17ms/step - loss: 0.3434 - accuracy: 0.9354 - val_loss: 0.2670 - val_accuracy: 0.8813
Epoch 10/20
15/15 [==============================] - 0s 17ms/step - loss: 0.2864 - accuracy: 0.9208 - val_loss: 0.5122 - val_accuracy: 0.8813

Ejecutemos el método de evaluate en los datos de prueba solo para asegurarnos de que no haya sobreajuste.

loss, accuracy = my_model.evaluate(test_ds)

print("Loss: ", loss)
print("Accuracy: ", accuracy)
5/5 [==============================] - 0s 9ms/step - loss: 0.2526 - accuracy: 0.9000
Loss:  0.25257644057273865
Accuracy:  0.8999999761581421

¡Lo hiciste!

Prueba tu modelo

A continuación, pruebe su modelo en la incrustación de la prueba anterior usando solo YAMNet.

scores, embeddings, spectrogram = yamnet_model(testing_wav_data)
result = my_model(embeddings).numpy()

inferred_class = my_classes[result.mean(axis=0).argmax()]
print(f'The main sound is: {inferred_class}')
The main sound is: cat

Guarde un modelo que pueda tomar directamente un archivo WAV como entrada

Su modelo funciona cuando le da las incrustaciones como entrada.

En un escenario del mundo real, querrá usar datos de audio como entrada directa.

Para hacer eso, combinará YAMNet con su modelo en un solo modelo que puede exportar para otras aplicaciones.

Para facilitar el uso del resultado del modelo, la capa final será una operación reduce_mean . Cuando use este modelo para servir (sobre el cual aprenderá más adelante en el tutorial), necesitará el nombre de la capa final. Si no define uno, TensorFlow definirá automáticamente uno incremental que dificulta la prueba, ya que seguirá cambiando cada vez que entrene el modelo. Al usar una operación de TensorFlow sin procesar, no puede asignarle un nombre. Para solucionar este problema, creará una capa personalizada que aplique reduce_mean y la llamará 'classifier' .

class ReduceMeanLayer(tf.keras.layers.Layer):
  def __init__(self, axis=0, **kwargs):
    super(ReduceMeanLayer, self).__init__(**kwargs)
    self.axis = axis

  def call(self, input):
    return tf.math.reduce_mean(input, axis=self.axis)
saved_model_path = './dogs_and_cats_yamnet'

input_segment = tf.keras.layers.Input(shape=(), dtype=tf.float32, name='audio')
embedding_extraction_layer = hub.KerasLayer(yamnet_model_handle,
                                            trainable=False, name='yamnet')
_, embeddings_output, _ = embedding_extraction_layer(input_segment)
serving_outputs = my_model(embeddings_output)
serving_outputs = ReduceMeanLayer(axis=0, name='classifier')(serving_outputs)
serving_model = tf.keras.Model(input_segment, serving_outputs)
serving_model.save(saved_model_path, include_optimizer=False)
WARNING:tensorflow:Compiled the loaded model, but the compiled metrics have yet to be built. `model.compile_metrics` will be empty until you train or evaluate the model.
WARNING:tensorflow:Compiled the loaded model, but the compiled metrics have yet to be built. `model.compile_metrics` will be empty until you train or evaluate the model.
2022-01-26 08:08:33.807036: W tensorflow/python/util/util.cc:368] Sets are not currently considered sequences, but this may change in the future, so consider avoiding using them.
INFO:tensorflow:Assets written to: ./dogs_and_cats_yamnet/assets
INFO:tensorflow:Assets written to: ./dogs_and_cats_yamnet/assets
tf.keras.utils.plot_model(serving_model)

png

Cargue su modelo guardado para verificar que funciona como se esperaba.

reloaded_model = tf.saved_model.load(saved_model_path)

Y para la prueba final: dados algunos datos de sonido, ¿tu modelo devuelve el resultado correcto?

reloaded_results = reloaded_model(testing_wav_data)
cat_or_dog = my_classes[tf.argmax(reloaded_results)]
print(f'The main sound is: {cat_or_dog}')
The main sound is: cat

Si desea probar su nuevo modelo en una configuración de servicio, puede usar la firma 'serving_default'.

serving_results = reloaded_model.signatures['serving_default'](testing_wav_data)
cat_or_dog = my_classes[tf.argmax(serving_results['classifier'])]
print(f'The main sound is: {cat_or_dog}')
The main sound is: cat

(Opcional) Algunas pruebas más

El modelo está listo.

Comparémoslo con YAMNet en el conjunto de datos de prueba.

test_pd = filtered_pd.loc[filtered_pd['fold'] == 5]
row = test_pd.sample(1)
filename = row['filename'].item()
print(filename)
waveform = load_wav_16k_mono(filename)
print(f'Waveform values: {waveform}')
_ = plt.plot(waveform)

display.Audio(waveform, rate=16000)
./datasets/ESC-50-master/audio/5-214759-A-5.wav
WARNING:tensorflow:Using a while_loop for converting IO>AudioResample
WARNING:tensorflow:Using a while_loop for converting IO>AudioResample
Waveform values: [ 3.2084468e-09 -7.7704687e-09 -1.2222010e-08 ...  2.2788899e-02
  1.0315948e-02 -3.4766860e-02]

png

# Run the model, check the output.
scores, embeddings, spectrogram = yamnet_model(waveform)
class_scores = tf.reduce_mean(scores, axis=0)
top_class = tf.argmax(class_scores)
inferred_class = class_names[top_class]
top_score = class_scores[top_class]
print(f'[YAMNet] The main sound is: {inferred_class} ({top_score})')

reloaded_results = reloaded_model(waveform)
your_top_class = tf.argmax(reloaded_results)
your_inferred_class = my_classes[your_top_class]
class_probabilities = tf.nn.softmax(reloaded_results, axis=-1)
your_top_score = class_probabilities[your_top_class]
print(f'[Your model] The main sound is: {your_inferred_class} ({your_top_score})')
[YAMNet] The main sound is: Silence (0.500638484954834)
[Your model] The main sound is: cat (0.9981643557548523)

Próximos pasos

Ha creado un modelo que puede clasificar los sonidos de perros o gatos. Con la misma idea y un conjunto de datos diferente, puede intentar, por ejemplo, construir un identificador acústico de pájaros basado en su canto.

¡Comparta su proyecto con el equipo de TensorFlow en las redes sociales!