![]() | ![]() | ![]() | ![]() |
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".
Preparar
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 de Speech Commands
Escribirás un script para descargar una parte del conjunto de datos Speech Commands . 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, y usted puede ayudar a mejorarlos contribuyendo con cinco minutos de su propia voz .
Utilizará una parte del conjunto de datos para ahorrar tiempo con la carga de datos. Extraiga el mini_speech_commands.zip
y cárguelo usando la API tf.data
.
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 [==============================] - 2s 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: ['down' 'yes' 'left' 'right' 'no' 'go' 'stop' 'up']
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/yes/fde2dee7_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, usará tf.audio.decode_wav
, que devuelve el audio codificado en WAV como un 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, como los archivos en mini_speech_commands
, los valores oscilan entre -32768 y 32767. La frecuencia de muestreo para este conjunto de datos es de 16 kHz. Tenga en cuenta que tf.audio.decode_wav
normalizará los valores en 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 aplicará process_path
para construir su conjunto de entrenamiento para extraer los pares de etiquetas de audio y verificar los resultados. Construirá los conjuntos de validación y prueba mediante 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()
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 de tiempo. El STFT ( tf.signal.stft
) divide la señal en ventanas de tiempo y ejecuta una transformada de Fourier en cada ventana, conservando algo de información de tiempo y devolviendo un tensor 2D en el que puede ejecutar convoluciones estándar.
STFT produce una matriz de números complejos que representan magnitud y fase. Sin embargo, solo necesitará la magnitud para este tutorial, que puede derivarse aplicando tf.abs
en la salida de tf.signal.stft
.
Elija los parámetros frame_length
y frame_step
modo que la "imagen" del espectrograma generada sea casi cuadrada. Para obtener más información sobre la elección de los parámetros STFT, puede consultar este video sobre el procesamiento de la señal 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: yes Waveform shape: (13375,) 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).
log_spec = np.log(spectrogram.T)
height = log_spec.shape[0]
X = np.arange(16000, step=height + 1)
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()
/tmpfs/src/tf_docs_env/lib/python3.6/site-packages/ipykernel_launcher.py:4: RuntimeWarning: divide by zero encountered in log after removing the cwd from sys.path. /tmpfs/src/tf_docs_env/lib/python3.6/site-packages/ipykernel_launcher.py:8: 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.
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()
/tmpfs/src/tf_docs_env/lib/python3.6/site-packages/ipykernel_launcher.py:4: RuntimeWarning: divide by zero encountered in log after removing the cwd from sys.path. /tmpfs/src/tf_docs_env/lib/python3.6/site-packages/ipykernel_launcher.py:8: 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.
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 entrenamiento 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)
Agregue operaciones de cache()
y prefetch()
conjuntos de datos para reducir la latencia de lectura mientras entrena el 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:
- Una capa de cambio de
Resizing
para reducir la resolución de la entrada y permitir que el modelo se entrene 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 capa de Normalization
, su método de adapt
primero necesitaría ser llamado en los datos de entrenamiento para calcular estadísticas agregadas (es decir, desviación estándar y media).
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 [==============================] - 16s 47ms/step - loss: 1.9142 - accuracy: 0.2855 - val_loss: 1.2969 - val_accuracy: 0.5938 Epoch 2/10 100/100 [==============================] - 0s 4ms/step - loss: 1.2597 - accuracy: 0.5581 - val_loss: 0.8918 - val_accuracy: 0.7237 Epoch 3/10 100/100 [==============================] - 0s 4ms/step - loss: 0.9525 - accuracy: 0.6647 - val_loss: 0.7742 - val_accuracy: 0.7337 Epoch 4/10 100/100 [==============================] - 0s 4ms/step - loss: 0.7793 - accuracy: 0.7288 - val_loss: 0.6514 - val_accuracy: 0.7850 Epoch 5/10 100/100 [==============================] - 0s 4ms/step - loss: 0.6715 - accuracy: 0.7639 - val_loss: 0.5986 - val_accuracy: 0.8000 Epoch 6/10 100/100 [==============================] - 0s 4ms/step - loss: 0.5647 - accuracy: 0.7968 - val_loss: 0.5901 - val_accuracy: 0.8075 Epoch 7/10 100/100 [==============================] - 0s 4ms/step - loss: 0.5122 - accuracy: 0.8093 - val_loss: 0.5526 - val_accuracy: 0.8087 Epoch 8/10 100/100 [==============================] - 0s 4ms/step - loss: 0.4780 - accuracy: 0.8298 - val_loss: 0.5166 - val_accuracy: 0.8163 Epoch 9/10 100/100 [==============================] - 0s 4ms/step - loss: 0.4216 - accuracy: 0.8544 - val_loss: 0.4806 - val_accuracy: 0.8438 Epoch 10/10 100/100 [==============================] - 0s 4ms/step - loss: 0.3976 - accuracy: 0.8510 - val_loss: 0.4848 - val_accuracy: 0.8388
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()
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 del 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()
Ejecutar inferencia en un archivo de audio
Finalmente, verifique la salida de 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()
Puede ver que su modelo reconoció muy claramente el comando de audio como "no".
Próximos pasos
Este tutorial mostró cómo se puede hacer una clasificación de audio simple mediante una red neuronal convolucional con TensorFlow y Python.
Para aprender a usar el aprendizaje por transferencia para la clasificación de audio, consulte el tutorial Clasificación de sonido con YAMNet .
Para crear su propia aplicación web interactiva para la clasificación de audio, considere tomar TensorFlow.js - Reconocimiento de audio usando el laboratorio de código de aprendizaje por transferencia .
TensorFlow también tiene soporte adicional para la preparación y el aumento de datos de audio para ayudarlo con sus propios proyectos basados en audio.