RStudio AI Blog: Clasificación de audio con antorcha

Variaciones sobre un tema

Clasificación de audio simple con Keras, Clasificación de audio con Keras: Mirando más de cerca las partes que no son de aprendizaje profundo, Clasificación de audio simple con antorcha: No, esta no es la primera publicación en este blog que presenta la clasificación de voz usando aprendizaje profundo. Con dos de esas publicaciones (las «aplicadas») comparte la configuración general, el tipo de arquitectura de aprendizaje profundo empleada y el conjunto de datos utilizado. Con el tercero, tiene en común el interés por las ideas y conceptos involucrados. Cada una de estas publicaciones tiene un enfoque diferente. ¿Deberías leer esta?

Bueno, por supuesto que no puedo decir «no», más aún porque aquí tiene una versión abreviada y resumida del capítulo sobre este tema en el próximo libro de CRC Press, Aprendizaje profundo y computación científica con R torch. A modo de comparación con la publicación anterior que utilizó torchescrito por el creador y mantenedor de torchaudioAthos Damiani, se han producido importantes avances en la torch ecosistema, el resultado final fue que el código se volvió mucho más fácil (especialmente en la parte de entrenamiento del modelo). Dicho esto, terminemos el preámbulo ya, ¡y sumérjase en el tema!

Inspeccionar los datos

usamos el comandos de voz conjunto de datos (Guardián (2018)) que viene con torchaudio. El conjunto de datos contiene grabaciones de treinta palabras diferentes de una o dos sílabas, pronunciadas por diferentes hablantes. Hay alrededor de 65.000 archivos de audio en total. Nuestra tarea será predecir, únicamente a partir del audio, cuál de las treinta palabras posibles se pronunció.

library(torch)
library(torchaudio)
library(luz)

ds <- speechcommand_dataset(
  root = "~/.torch-datasets", 
  url = "speech_commands_v0.01",
  download = TRUE
)

Comenzamos inspeccionando los datos.

[1]  "bed"    "bird"   "cat"    "dog"    "down"   "eight"
[7]  "five"   "four"   "go"     "happy"  "house"  "left"
[32] " marvin" "nine"   "no"     "off"    "on"     "one"
[19] "right"  "seven" "sheila" "six"    "stop"   "three"
[25]  "tree"   "two"    "up"     "wow"    "yes"    "zero" 

Al elegir una muestra al azar, vemos que la información que necesitaremos está contenida en cuatro propiedades: waveform, sample_rate, label_indexy label.

El primero, waveformserá nuestro predictor.

sample <- ds[2000]
dim(sample$waveform)
[1]     1 16000

Los valores de tensor individuales están centrados en cero y oscilan entre -1 y 1. Hay 16 000 de ellos, lo que refleja el hecho de que la grabación duró un segundo y se registró en (o los creadores del conjunto de datos la convirtieron) un velocidad de 16.000 muestras por segundo. Esta última información se almacena en sample$sample_rate:

[1] 16000

Todas las grabaciones han sido muestreadas al mismo ritmo. Su duración casi siempre es igual a un segundo; los – muy – pocos sonidos que son mínimamente más largos los podemos truncar con seguridad.

Finalmente, el objetivo se almacena, en forma de número entero, en sample$label_indexla palabra correspondiente está disponible en sample$label:

sample$label
sample$label_index
[1] "bird"
torch_tensor
2
[ CPULongType ]

¿Cómo se ve esta señal de audio?

library(ggplot2)

df <- data.frame(
  x = 1:length(sample$waveform[1]),
  y = as.numeric(sample$waveform[1])
  )

ggplot(df, aes(x = x, y = y)) +
  geom_line(size = 0.3) +
  ggtitle(
    paste0(
      "The spoken word "", sample$label, "": Sound wave"
    )
  ) +
  xlab("time") +
  ylab("amplitude") +
  theme_minimal()
La palabra hablada

Lo que vemos es una secuencia de amplitudes, que refleja la onda de sonido producida por alguien que dice «pájaro». Dicho de otra manera, aquí tenemos una serie temporal de «valores de sonoridad». Incluso para los expertos, adivinar cual palabra resultó en esas amplitudes es una tarea imposible. Aquí es donde entra en juego el conocimiento del dominio. Es posible que el experto no pueda aprovechar la señal. en esta representación; pero pueden conocer una manera de representarlo de manera más significativa.

Dos representaciones equivalentes

Imagine que, en lugar de una secuencia de amplitudes a lo largo del tiempo, la onda anterior se representara de una manera que no tuviera ninguna información sobre el tiempo. A continuación, imagina que tomamos esa representación e intentamos recuperar la señal original. Para que eso sea posible, la nueva representación de alguna manera tendría que contener “tanta” información como la ola de la que partimos. Ese “igual” se obtiene de la Transformada de Fouriery consiste en las magnitudes y desfases de los diferentes frecuencias que componen la señal.

Entonces, ¿cómo se ve la versión transformada de Fourier de la onda de sonido del «pájaro»? Lo obtenemos llamando torch_fft_fft() (dónde fft significa Transformada Rápida de Fourier):

dft <- torch_fft_fft(sample$waveform)
dim(dft)
[1]     1 16000

La longitud de este tensor es la misma; sin embargo, sus valores no están en orden cronológico. En cambio, representan la Coeficientes de Fourier, correspondiente a las frecuencias contenidas en la señal. Cuanto mayor sea su magnitud, más contribuirán a la señal:

mag <- torch_abs(dft[1, ])

df <- data.frame(
  x = 1:(length(sample$waveform[1]) / 2),
  y = as.numeric(mag[1:8000])
)

ggplot(df, aes(x = x, y = y)) +
  geom_line(size = 0.3) +
  ggtitle(
    paste0(
      "The spoken word "",
      sample$label,
      "": Discrete Fourier Transform"
    )
  ) +
  xlab("frequency") +
  ylab("magnitude") +
  theme_minimal()
La palabra hablada

A partir de esta representación alternativa, podríamos volver a la onda de sonido original tomando las frecuencias presentes en la señal, ponderándolas según sus coeficientes y sumándolas. Pero en la clasificación de sonido, la información de tiempo seguramente debe importar; realmente no queremos tirarlo.

Combinando representaciones: El espectrograma

De hecho, lo que realmente nos ayudaría es una síntesis de ambas representaciones; una especie de «ten tu pastel y cómelo también». ¿Qué pasaría si pudiéramos dividir la señal en pequeños fragmentos y ejecutar la transformada de Fourier en cada uno de ellos? Como habrás adivinado a partir de esta introducción, esto es algo que podemos hacer; y la representación que crea se llama espectrograma.

Con un espectrograma, todavía mantenemos algo de información en el dominio del tiempo, algo, ya que hay una pérdida inevitable en la granularidad. Por otro lado, para cada uno de los segmentos de tiempo, conocemos su composición espectral. Sin embargo, hay un punto importante que destacar. Las resoluciones que obtenemos en tiempo versus en frecuencia, respectivamente, están inversamente relacionados. Si dividimos las señales en muchos fragmentos (llamados «ventanas»), la representación de frecuencia por ventana no será muy detallada. Por el contrario, si queremos obtener una mejor resolución en el dominio de la frecuencia, tenemos que elegir ventanas más largas, perdiendo así información sobre cómo varía la composición espectral con el tiempo. Lo que suena como un gran problema, y ​​en muchos casos lo será, no lo será para nosotros, como verá muy pronto.

Primero, sin embargo, creemos e inspeccionemos un espectrograma de este tipo para nuestra señal de ejemplo. En el siguiente fragmento de código, el tamaño de las ventanas superpuestas se elige para permitir una granularidad razonable tanto en el dominio del tiempo como en el de la frecuencia. Nos quedan sesenta y tres ventanas y, para cada ventana, obtenemos doscientos cincuenta y siete coeficientes:

fft_size <- 512
window_size <- 512
power <- 0.5

spectrogram <- transform_spectrogram(
  n_fft = fft_size,
  win_length = window_size,
  normalized = TRUE,
  power = power
)

spec <- spectrogram(sample$waveform)$squeeze()
dim(spec)
[1]   257 63

Podemos mostrar el espectrograma visualmente:

bins <- 1:dim(spec)[1]
freqs <- bins / (fft_size / 2 + 1) * sample$sample_rate 
log_freqs <- log10(freqs)

frames <- 1:(dim(spec)[2])
seconds <- (frames / dim(spec)[2]) *
  (dim(sample$waveform$squeeze())[1] / sample$sample_rate)

image(x = as.numeric(seconds),
      y = log_freqs,
      z = t(as.matrix(spec)),
      ylab = 'log frequency [Hz]',
      xlab = 'time [s]',
      col = hcl.colors(12, palette = "viridis")
)
main <- paste0("Spectrogram, window size = ", window_size)
sub <- "Magnitude (square root)"
mtext(side = 3, line = 2, at = 0, adj = 0, cex = 1.3, main)
mtext(side = 3, line = 1, at = 0, adj = 0, cex = 1, sub)
La palabra hablada “pájaro”: Espectrograma.

Sabemos que hemos perdido algo de resolución tanto en tiempo como en frecuencia. Sin embargo, al mostrar la raíz cuadrada de las magnitudes de los coeficientes y, por lo tanto, mejorar la sensibilidad, pudimos obtener un resultado razonable. (Con el viridis esquema de color, los tonos de onda larga indican coeficientes de mayor valor; los de onda corta, lo contrario.)

Finalmente, volvamos a la pregunta crucial. Si esta representación, por necesidad, es un compromiso, ¿por qué, entonces, querríamos emplearla? Aquí es donde tomamos la perspectiva del aprendizaje profundo. El espectrograma es una representación bidimensional: una imagen. Con las imágenes, tenemos acceso a un rico reservorio de técnicas y arquitecturas: entre todas las áreas en las que el aprendizaje profundo ha tenido éxito, el reconocimiento de imágenes aún se destaca. Pronto, verá que para esta tarea, ni siquiera se necesitan arquitecturas sofisticadas; una convnet directa hará un muy buen trabajo.

Entrenamiento de una red neuronal en espectrogramas

Empezamos creando un torch::dataset() que, partiendo del original speechcommand_dataset()calcula un espectrograma para cada muestra.

spectrogram_dataset <- dataset(
  inherit = speechcommand_dataset,
  initialize = function(...,
                        pad_to = 16000,
                        sampling_rate = 16000,
                        n_fft = 512,
                        window_size_seconds = 0.03,
                        window_stride_seconds = 0.01,
                        power = 2) ,
  .getitem = function(i) 
)

En la lista de parámetros a spectrogram_dataset()Nota powercon un valor por defecto de 2. Este es el valor que, a menos que se indique lo contrario, torch‘s transform_spectrogram() asumirá que power debería tener. En estas circunstancias, los valores que componen el espectrograma son las magnitudes al cuadrado de los coeficientes de Fourier. Usando powerpuede cambiar el valor predeterminado y especificar, por ejemplo, si desea valores absolutos (power = 1), cualquier otro valor positivo (como 0.5el que usamos arriba para mostrar un ejemplo concreto) – o tanto la parte real como la imaginaria de los coeficientes (power = NULL).

Desde el punto de vista de la visualización, por supuesto, la representación compleja completa es un inconveniente; la gráfica del espectrograma necesitaría una dimensión adicional. Pero bien podemos preguntarnos si una red neuronal podría beneficiarse de la información adicional contenida en el número complejo «completo». Después de todo, al reducir a magnitudes perdemos los cambios de fase de los coeficientes individuales, que podrían contener información utilizable. De hecho, mis pruebas mostraron que hizo; el uso de los valores complejos resultó en una mayor precisión de clasificación.

Veamos qué obtenemos de spectrogram_dataset():

ds <- spectrogram_dataset(
  root = "~/.torch-datasets",
  url = "speech_commands_v0.01",
  download = TRUE,
  power = NULL
)

dim(ds[1]$x)
[1]   2 257 101

Tenemos 257 coeficientes para 101 ventanas; y cada coeficiente está representado por sus partes real e imaginaria.

A continuación, dividimos los datos e instanciamos el dataset() y dataloader() objetos.

train_ids <- sample(
  1:length(ds),
  size = 0.6 * length(ds)
)
valid_ids <- sample(
  setdiff(
    1:length(ds),
    train_ids
  ),
  size = 0.2 * length(ds)
)
test_ids <- setdiff(
  1:length(ds),
  union(train_ids, valid_ids)
)

batch_size <- 128

train_ds <- dataset_subset(ds, indices = train_ids)
train_dl <- dataloader(
  train_ds,
  batch_size = batch_size, shuffle = TRUE
)

valid_ds <- dataset_subset(ds, indices = valid_ids)
valid_dl <- dataloader(
  valid_ds,
  batch_size = batch_size
)

test_ds <- dataset_subset(ds, indices = test_ids)
test_dl <- dataloader(test_ds, batch_size = 64)

b <- train_dl %>%
  dataloader_make_iter() %>%
  dataloader_next()

dim(b$x)
[1] 128   2 257 101

El modelo es una convnet sencilla, con abandono y normalización por lotes. Las partes real e imaginaria de los coeficientes de Fourier se pasan a los valores iniciales del modelo. nn_conv2d() como dos separados canales.

model <- nn_module(
  initialize = function() ,
  forward = function(x) 
)

A continuación determinamos una tasa de aprendizaje adecuada:

model <- model %>%
  setup(
    loss = nn_cross_entropy_loss(),
    optimizer = optim_adam,
    metrics = list(luz_metric_accuracy())
  )

rates_and_losses <- model %>%
  lr_finder(train_dl)
rates_and_losses %>% plot()
Buscador de tasa de aprendizaje, ejecutar en el modelo de espectrograma complejo.

Basado en la gráfica, decidí usar 0.01 como tasa de aprendizaje máxima. El entrenamiento se prolongó durante cuarenta épocas.

fitted <- model %>%
  fit(train_dl,
    epochs = 50, valid_data = valid_dl,
    callbacks = list(
      luz_callback_early_stopping(patience = 3),
      luz_callback_lr_scheduler(
        lr_one_cycle,
        max_lr = 1e-2,
        epochs = 50,
        steps_per_epoch = length(train_dl),
        call_on = "on_batch_end"
      ),
      luz_callback_model_checkpoint(path = "models_complex/"),
      luz_callback_csv_logger("logs_complex.csv")
    ),
    verbose = TRUE
  )

plot(fitted)
Ajuste del modelo de espectrograma complejo.

Vamos a comprobar las precisiones reales.

"epoch","set","loss","acc"
1,"train",3.09768574611813,0.12396992171405
1,"valid",2.52993751740923,0.284378862793572
2,"train",2.26747255972008,0.333642356819118
2,"valid",1.66693911248562,0.540791100123609
3,"train",1.62294889937818,0.518464153275649
3,"valid",1.11740599192825,0.704882571075402
...
...
38,"train",0.18717994078312,0.943809229501442
38,"valid",0.23587799138006,0.936418417799753
39,"train",0.19338578602993,0.942882159044087
39,"valid",0.230597475945365,0.939431396786156
40,"train",0.190593419024368,0.942727647301195
40,"valid",0.243536252455384,0.936186650185414

Con treinta clases para distinguir entre ellas, ¡una precisión final del conjunto de validación de ~0.94 parece un resultado muy decente!

Podemos confirmar esto en el conjunto de prueba:

evaluate(fitted, test_dl)
loss: 0.2373
acc: 0.9324

Una pregunta interesante es qué palabras se confunden más a menudo. (Por supuesto, aún más interesante es cómo las probabilidades de error se relacionan con las características de los espectrogramas, pero esto, tenemos que dejarlo al verdadero expertos en dominios. Una buena forma de mostrar la matriz de confusión es crear un gráfico aluvial. Vemos las predicciones, a la izquierda, «fluir hacia» las ranuras de destino. (Los pares de predicción objetivo menos frecuentes que una milésima parte de la cardinalidad del conjunto de prueba están ocultos).

Gráfica aluvial para la configuración del espectrograma complejo.

Envolver

¡Es todo por hoy! En las próximas semanas, espere más publicaciones basadas en el contenido del libro CRC que aparecerá pronto, Aprendizaje profundo y computación científica con R torch. ¡Gracias por leer!

Foto por alex lauzon en Unsplash

Guardián, Pete. 2018. “Comandos de voz: A Conjunto de datos para reconocimiento de voz de vocabulario limitado”. CoRR abs/1804.03209. http://arxiv.org/abs/1804.03209.

Fuente del artículo

Deja un comentario