¡El Día de la Comunidad de ML es el 9 de noviembre! Únase a nosotros para recibir actualizaciones de TensorFlow, JAX, y más Más información

Reconocimiento de audio simple: reconocimiento de palabras clave

Ver en TensorFlow.org Ejecutar en Google Colab Ver fuente en GitHub Descargar cuaderno

Este tutorial le mostrará cómo construir una red básica de reconocimiento de voz que reconoce diez palabras diferentes. Es importante saber que los sistemas reales de reconocimiento de voz y audio son mucho más complejos, pero al igual que MNIST para imágenes, debería brindarle una comprensión básica de las técnicas involucradas. Una vez que haya completado este tutorial, tendrá un modelo que intenta clasificar un clip de audio de un segundo como "abajo", "ir", "izquierda", "no", "derecha", "detener", "arriba " y si".

Configuración

Importe los módulos y las dependencias necesarios.

import os
import pathlib

import matplotlib.pyplot as plt
import numpy as np
import seaborn as sns
import tensorflow as tf

from tensorflow.keras.layers.experimental import preprocessing
from tensorflow.keras import layers
from tensorflow.keras import models
from IPython import display


# Set seed for experiment reproducibility
seed = 42
tf.random.set_seed(seed)
np.random.seed(seed)

Importar el conjunto de datos Speech Commands

Que voy a escribir un guión para descargar una parte del conjunto de datos comandos de voz . El conjunto de datos original consta de más de 105.000 archivos de audio WAV de personas que dicen treinta palabras diferentes. Estos datos fueron recopilados por Google y publicados bajo una licencia CC BY.

Utilizará una parte del conjunto de datos para ahorrar tiempo con la carga de datos. Extraer el mini_speech_commands.zip y cargarlo en el uso de la tf.data API.

data_dir = pathlib.Path('data/mini_speech_commands')
if not data_dir.exists():
  tf.keras.utils.get_file(
      'mini_speech_commands.zip',
      origin="http://storage.googleapis.com/download.tensorflow.org/data/mini_speech_commands.zip",
      extract=True,
      cache_dir='.', cache_subdir='data')
Downloading data from http://storage.googleapis.com/download.tensorflow.org/data/mini_speech_commands.zip
182083584/182082353 [==============================] - 1s 0us/step
182091776/182082353 [==============================] - 1s 0us/step

Verifique las estadísticas básicas sobre el conjunto de datos.

commands = np.array(tf.io.gfile.listdir(str(data_dir)))
commands = commands[commands != 'README.md']
print('Commands:', commands)
Commands: ['no' 'yes' 'left' 'right' 'up' 'stop' 'go' 'down']

Extraiga los archivos de audio en una lista y mezcle.

filenames = tf.io.gfile.glob(str(data_dir) + '/*/*')
filenames = tf.random.shuffle(filenames)
num_samples = len(filenames)
print('Number of total examples:', num_samples)
print('Number of examples per label:',
      len(tf.io.gfile.listdir(str(data_dir/commands[0]))))
print('Example file tensor:', filenames[0])
Number of total examples: 8000
Number of examples per label: 1000
Example file tensor: tf.Tensor(b'data/mini_speech_commands/right/a8e25ebb_nohash_0.wav', shape=(), dtype=string)

Divida los archivos en conjuntos de entrenamiento, validación y prueba utilizando una proporción de 80:10:10, respectivamente.

train_files = filenames[:6400]
val_files = filenames[6400: 6400 + 800]
test_files = filenames[-800:]

print('Training set size', len(train_files))
print('Validation set size', len(val_files))
print('Test set size', len(test_files))
Training set size 6400
Validation set size 800
Test set size 800

Leer archivos de audio y sus etiquetas

El archivo de audio se leerá inicialmente como un archivo binario, que querrá convertir en un tensor numérico.

Para cargar un archivo de audio, que va a utilizar tf.audio.decode_wav , que devuelve el audio WAV codificados como de tensor y la frecuencia de muestreo.

Un archivo WAV contiene datos de series de tiempo con un número determinado de muestras por segundo. Cada muestra representa la amplitud de la señal de audio en ese momento específico. En un sistema de 16 bits, al igual que los archivos de mini_speech_commands , los valores oscilan entre -32768 y 32767. La frecuencia de muestreo para este conjunto de datos es de 16 kHz. Nota que tf.audio.decode_wav se normalizar los valores para el rango [-1,0, 1,0].

def decode_audio(audio_binary):
  audio, _ = tf.audio.decode_wav(audio_binary)
  return tf.squeeze(audio, axis=-1)

La etiqueta de cada archivo WAV es su directorio principal.

def get_label(file_path):
  parts = tf.strings.split(file_path, os.path.sep)

  # Note: You'll use indexing here instead of tuple unpacking to enable this 
  # to work in a TensorFlow graph.
  return parts[-2]

Definamos un método que tomará el nombre del archivo WAV y generará una tupla que contenga el audio y las etiquetas para el entrenamiento supervisado.

def get_waveform_and_label(file_path):
  label = get_label(file_path)
  audio_binary = tf.io.read_file(file_path)
  waveform = decode_audio(audio_binary)
  return waveform, label

Ahora se aplique process_path para construir su conjunto de entrenamiento para extraer los pares de etiqueta de audio y comprobar los resultados. Construirá los conjuntos de validación y prueba utilizando un procedimiento similar más adelante.

AUTOTUNE = tf.data.AUTOTUNE
files_ds = tf.data.Dataset.from_tensor_slices(train_files)
waveform_ds = files_ds.map(get_waveform_and_label, num_parallel_calls=AUTOTUNE)

Examinemos algunas formas de onda de audio con sus etiquetas correspondientes.

rows = 3
cols = 3
n = rows*cols
fig, axes = plt.subplots(rows, cols, figsize=(10, 12))
for i, (audio, label) in enumerate(waveform_ds.take(n)):
  r = i // cols
  c = i % cols
  ax = axes[r][c]
  ax.plot(audio.numpy())
  ax.set_yticks(np.arange(-1.2, 1.2, 0.2))
  label = label.numpy().decode('utf-8')
  ax.set_title(label)

plt.show()

png

Espectrograma

Convertirá la forma de onda en un espectrograma, que muestra los cambios de frecuencia a lo largo del tiempo y se puede representar como una imagen 2D. Esto se puede hacer aplicando la transformada de Fourier de corta duración (STFT) para convertir el audio en el dominio de tiempo-frecuencia.

Una transformada de Fourier ( tf.signal.fft ) convierte una señal a sus frecuencias componentes, pero pierde toda la información del tiempo. La STFT ( tf.signal.stft ) divide la señal en las ventanas de tiempo y se ejecuta una transformada de Fourier en cada ventana, conservando algo de información de tiempo, y devolver un tensor 2D que se puede ejecutar en circunvoluciones estándar.

STFT produce una matriz de números complejos que representan la magnitud y la fase. Sin embargo, lo único que necesita la magnitud de este tutorial, que se puede derivar mediante la aplicación de tf.abs en la salida del tf.signal.stft .

Elija frame_length y frame_step parámetros de tal manera que el espectrograma "imagen" generada es casi cuadrada. Para obtener más información sobre la elección STFT parámetros, puede hacer referencia a este vídeo en el procesamiento de señales de audio.

También desea que las formas de onda tengan la misma longitud, de modo que cuando la convierta en una imagen de espectrograma, los resultados tengan dimensiones similares. Esto se puede hacer simplemente rellenando con cero los clips de audio que tengan una duración inferior a un segundo.

def get_spectrogram(waveform):
  # Padding for files with less than 16000 samples
  zero_padding = tf.zeros([16000] - tf.shape(waveform), dtype=tf.float32)

  # Concatenate audio with padding so that all audio clips will be of the 
  # same length
  waveform = tf.cast(waveform, tf.float32)
  equal_length = tf.concat([waveform, zero_padding], 0)
  spectrogram = tf.signal.stft(
      equal_length, frame_length=255, frame_step=128)

  spectrogram = tf.abs(spectrogram)

  return spectrogram

A continuación, explorará los datos. Compare la forma de onda, el espectrograma y el audio real de un ejemplo del conjunto de datos.

for waveform, label in waveform_ds.take(1):
  label = label.numpy().decode('utf-8')
  spectrogram = get_spectrogram(waveform)

print('Label:', label)
print('Waveform shape:', waveform.shape)
print('Spectrogram shape:', spectrogram.shape)
print('Audio playback')
display.display(display.Audio(waveform, rate=16000))
Label: right
Waveform shape: (16000,)
Spectrogram shape: (124, 129)
Audio playback

def plot_spectrogram(spectrogram, ax):
  # Convert to frequencies to log scale and transpose so that the time is
  # represented in the x-axis (columns). An epsilon is added to avoid log of zero.
  log_spec = np.log(spectrogram.T+np.finfo(float).eps)
  height = log_spec.shape[0]
  width = log_spec.shape[1]
  X = np.linspace(0, np.size(spectrogram), num=width, dtype=int)
  Y = range(height)
  ax.pcolormesh(X, Y, log_spec)


fig, axes = plt.subplots(2, figsize=(12, 8))
timescale = np.arange(waveform.shape[0])
axes[0].plot(timescale, waveform.numpy())
axes[0].set_title('Waveform')
axes[0].set_xlim([0, 16000])
plot_spectrogram(spectrogram.numpy(), axes[1])
axes[1].set_title('Spectrogram')
plt.show()
/home/kbuilder/.local/lib/python3.7/site-packages/ipykernel_launcher.py:9: MatplotlibDeprecationWarning: shading='flat' when X and Y have the same dimensions as C is deprecated since 3.3.  Either specify the corners of the quadrilaterals with X and Y, or pass shading='auto', 'nearest' or 'gouraud', or set rcParams['pcolor.shading'].  This will become an error two minor releases later.
  if __name__ == '__main__':

png

Ahora transforme el conjunto de datos de forma de onda para tener imágenes de espectrograma y sus etiquetas correspondientes como ID enteros.

def get_spectrogram_and_label_id(audio, label):
  spectrogram = get_spectrogram(audio)
  spectrogram = tf.expand_dims(spectrogram, -1)
  label_id = tf.argmax(label == commands)
  return spectrogram, label_id
spectrogram_ds = waveform_ds.map(
    get_spectrogram_and_label_id, num_parallel_calls=AUTOTUNE)

Examine las "imágenes" del espectrograma en busca de diferentes muestras del conjunto de datos.

rows = 3
cols = 3
n = rows*cols
fig, axes = plt.subplots(rows, cols, figsize=(10, 10))
for i, (spectrogram, label_id) in enumerate(spectrogram_ds.take(n)):
  r = i // cols
  c = i % cols
  ax = axes[r][c]
  plot_spectrogram(np.squeeze(spectrogram.numpy()), ax)
  ax.set_title(commands[label_id.numpy()])
  ax.axis('off')

plt.show()
/home/kbuilder/.local/lib/python3.7/site-packages/ipykernel_launcher.py:9: MatplotlibDeprecationWarning: shading='flat' when X and Y have the same dimensions as C is deprecated since 3.3.  Either specify the corners of the quadrilaterals with X and Y, or pass shading='auto', 'nearest' or 'gouraud', or set rcParams['pcolor.shading'].  This will become an error two minor releases later.
  if __name__ == '__main__':

png

Construye y entrena el modelo

Ahora puede construir y entrenar su modelo. Pero antes de hacer eso, deberá repetir el procesamiento previo del conjunto de capacitación en los conjuntos de validación y prueba.

def preprocess_dataset(files):
  files_ds = tf.data.Dataset.from_tensor_slices(files)
  output_ds = files_ds.map(get_waveform_and_label, num_parallel_calls=AUTOTUNE)
  output_ds = output_ds.map(
      get_spectrogram_and_label_id,  num_parallel_calls=AUTOTUNE)
  return output_ds
train_ds = spectrogram_ds
val_ds = preprocess_dataset(val_files)
test_ds = preprocess_dataset(test_files)

Agrupe los conjuntos de capacitación y validación para la capacitación de modelos.

batch_size = 64
train_ds = train_ds.batch(batch_size)
val_ds = val_ds.batch(batch_size)

Agregar conjunto de datos cache() y prefetch() las operaciones para reducir la latencia de lectura durante el entrenamiento del modelo.

train_ds = train_ds.cache().prefetch(AUTOTUNE)
val_ds = val_ds.cache().prefetch(AUTOTUNE)

Para el modelo, utilizará una red neuronal convolucional simple (CNN), ya que ha transformado los archivos de audio en imágenes de espectrograma. El modelo también tiene las siguientes capas de preprocesamiento adicionales:

  • Un Resizing capa para disminuir la resolución de la entrada para que el modelo de entrenar más rápido.
  • A Normalization capa para normalizar cada píxel de la imagen basada en su desviación media y estándar.

Para la Normalization capa, su adapt necesitaría primer método a ser llamado en la formación de datos con el fin de calcular las estadísticas de agregados (es decir, media y desviación estándar).

for spectrogram, _ in spectrogram_ds.take(1):
  input_shape = spectrogram.shape
print('Input shape:', input_shape)
num_labels = len(commands)

norm_layer = preprocessing.Normalization()
norm_layer.adapt(spectrogram_ds.map(lambda x, _: x))

model = models.Sequential([
    layers.Input(shape=input_shape),
    preprocessing.Resizing(32, 32), 
    norm_layer,
    layers.Conv2D(32, 3, activation='relu'),
    layers.Conv2D(64, 3, activation='relu'),
    layers.MaxPooling2D(),
    layers.Dropout(0.25),
    layers.Flatten(),
    layers.Dense(128, activation='relu'),
    layers.Dropout(0.5),
    layers.Dense(num_labels),
])

model.summary()
Input shape: (124, 129, 1)
Model: "sequential"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
resizing (Resizing)          (None, 32, 32, 1)         0         
_________________________________________________________________
normalization (Normalization (None, 32, 32, 1)         3         
_________________________________________________________________
conv2d (Conv2D)              (None, 30, 30, 32)        320       
_________________________________________________________________
conv2d_1 (Conv2D)            (None, 28, 28, 64)        18496     
_________________________________________________________________
max_pooling2d (MaxPooling2D) (None, 14, 14, 64)        0         
_________________________________________________________________
dropout (Dropout)            (None, 14, 14, 64)        0         
_________________________________________________________________
flatten (Flatten)            (None, 12544)             0         
_________________________________________________________________
dense (Dense)                (None, 128)               1605760   
_________________________________________________________________
dropout_1 (Dropout)          (None, 128)               0         
_________________________________________________________________
dense_1 (Dense)              (None, 8)                 1032      
=================================================================
Total params: 1,625,611
Trainable params: 1,625,608
Non-trainable params: 3
_________________________________________________________________
model.compile(
    optimizer=tf.keras.optimizers.Adam(),
    loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True),
    metrics=['accuracy'],
)
EPOCHS = 10
history = model.fit(
    train_ds, 
    validation_data=val_ds,  
    epochs=EPOCHS,
    callbacks=tf.keras.callbacks.EarlyStopping(verbose=1, patience=2),
)
Epoch 1/10
100/100 [==============================] - 13s 40ms/step - loss: 1.7503 - accuracy: 0.3659 - val_loss: 1.3361 - val_accuracy: 0.5600
Epoch 2/10
100/100 [==============================] - 0s 4ms/step - loss: 1.1948 - accuracy: 0.5809 - val_loss: 0.9340 - val_accuracy: 0.7225
Epoch 3/10
100/100 [==============================] - 0s 4ms/step - loss: 0.9253 - accuracy: 0.6773 - val_loss: 0.7738 - val_accuracy: 0.7588
Epoch 4/10
100/100 [==============================] - 0s 4ms/step - loss: 0.7543 - accuracy: 0.7341 - val_loss: 0.6993 - val_accuracy: 0.7650
Epoch 5/10
100/100 [==============================] - 0s 4ms/step - loss: 0.6473 - accuracy: 0.7719 - val_loss: 0.6500 - val_accuracy: 0.7800
Epoch 6/10
100/100 [==============================] - 0s 4ms/step - loss: 0.5746 - accuracy: 0.7977 - val_loss: 0.6049 - val_accuracy: 0.7975
Epoch 7/10
100/100 [==============================] - 0s 4ms/step - loss: 0.5181 - accuracy: 0.8155 - val_loss: 0.5967 - val_accuracy: 0.8200
Epoch 8/10
100/100 [==============================] - 0s 4ms/step - loss: 0.4493 - accuracy: 0.8402 - val_loss: 0.5678 - val_accuracy: 0.8100
Epoch 9/10
100/100 [==============================] - 0s 4ms/step - loss: 0.4292 - accuracy: 0.8491 - val_loss: 0.5531 - val_accuracy: 0.8175
Epoch 10/10
100/100 [==============================] - 0s 4ms/step - loss: 0.3914 - accuracy: 0.8598 - val_loss: 0.5204 - val_accuracy: 0.8275

Revisemos las curvas de pérdida de entrenamiento y validación para ver cómo ha mejorado su modelo durante el entrenamiento.

metrics = history.history
plt.plot(history.epoch, metrics['loss'], metrics['val_loss'])
plt.legend(['loss', 'val_loss'])
plt.show()

png

Evaluar el rendimiento del equipo de prueba

Ejecutemos el modelo en el conjunto de prueba y verifiquemos el rendimiento.

test_audio = []
test_labels = []

for audio, label in test_ds:
  test_audio.append(audio.numpy())
  test_labels.append(label.numpy())

test_audio = np.array(test_audio)
test_labels = np.array(test_labels)
y_pred = np.argmax(model.predict(test_audio), axis=1)
y_true = test_labels

test_acc = sum(y_pred == y_true) / len(y_true)
print(f'Test set accuracy: {test_acc:.0%}')
Test set accuracy: 84%

Mostrar una matriz de confusión

Una matriz de confusión es útil para ver qué tan bien funcionó el modelo en cada uno de los comandos en el conjunto de prueba.

confusion_mtx = tf.math.confusion_matrix(y_true, y_pred) 
plt.figure(figsize=(10, 8))
sns.heatmap(confusion_mtx, xticklabels=commands, yticklabels=commands, 
            annot=True, fmt='g')
plt.xlabel('Prediction')
plt.ylabel('Label')
plt.show()

png

Ejecutar inferencia en un archivo de audio

Finalmente, verifique la salida de la predicción del modelo usando un archivo de audio de entrada de alguien que diga "no". ¿Qué tan bien funciona su modelo?

sample_file = data_dir/'no/01bb6a2a_nohash_0.wav'

sample_ds = preprocess_dataset([str(sample_file)])

for spectrogram, label in sample_ds.batch(1):
  prediction = model(spectrogram)
  plt.bar(commands, tf.nn.softmax(prediction[0]))
  plt.title(f'Predictions for "{commands[label[0]]}"')
  plt.show()

png

Puede ver que su modelo reconoció claramente el comando de audio como "no".

Próximos pasos

Este tutorial mostró cómo se puede realizar una clasificación de audio simple mediante una red neuronal convolucional con TensorFlow y Python.