30.7 C
Colombia
lunes, julio 7, 2025

Weblog de Posit AI: clasificación de audio con antorcha


Variaciones sobre un tema

Clasificación de audio sencilla con Keras, Clasificación de audio con Keras: una mirada más cercana a las partes que no son de aprendizaje profundo, Clasificación de audio sencilla con linterna.: No, esta no es la primera publicación en este weblog que presenta la clasificación del habla mediante el aprendizaje profundo. Con dos de esas publicaciones (las “aplicadas”) comparte la configuración basic, 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 concepts y conceptos involucrados. Cada una de estas publicaciones tiene un enfoque diferente. ¿Deberías leer esta?

Bueno, por supuesto que no puedo decir “no”, especialmente porque aquí tenéis una versión abreviada y condensada 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 el publish anterior que utilizaba torchescrito por el creador y mantenedor de torchaudioAthos Damiani, se han producido importantes avances en la torch ecosistema, el resultado closing fue que el código se volvió mucho más fácil (especialmente en la parte de entrenamiento del modelo). Dicho esto, ¡terminemos ya con el preámbulo y profundicemos en el tema!

Inspeccionando 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. En complete, hay alrededor de 65.000 archivos de audio. Nuestra tarea será predecir, únicamente a partir del audio, cuál de las treinta palabras posibles fue pronunciada.

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

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

Empezamos inspeccionando los datos.

[1]  "mattress"    "chook"   "cat"    "canine"    "down"   "eight"
[7]  "5"   "4"   "go"     "glad"  "home"  "left"
[32] " marvin" "9"   "no"     "off"    "on"     "one"
[19] "proper"  "seven" "sheila" "six"    "cease"   "three"
[25]  "tree"   "two"    "up"     "wow"    "sure"    "zero" 

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

La primera, waveformserá nuestro predictor.

pattern <- ds[2000]
dim(pattern$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 fue registrada (o convertida por los creadores del conjunto de datos) en una velocidad de 16.000 muestras por segundo. Esta última información se almacena en pattern$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 pattern$label_indexestando disponible la palabra correspondiente en pattern$label:

pattern$label
pattern$label_index
[1] "chook"
torch_tensor
2
[ CPULongType{} ]

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

library(ggplot2)

df <- information.body(
  x = 1:size(pattern$waveform[1]),
  y = as.numeric(pattern$waveform[1])
  )

ggplot(df, aes(x = x, y = y)) +
  geom_line(measurement = 0.3) +
  ggtitle(
    paste0(
      "The spoken phrase "", pattern$label, "": Sound wave"
    )
  ) +
  xlab("time") +
  ylab("amplitude") +
  theme_minimal()
La palabra hablada “pájaro”, en representación en el dominio del tiempo.

Lo que vemos es una secuencia de amplitudes que reflejan la onda sonora producida por alguien que cube “pájaro”. Dicho de otra manera, tenemos aquí una serie temporal de “valores de sonoridad”. Incluso para los expertos, adivinar cual La palabra resultante 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 mucho la señal. en esta representación; pero es posible que conozcan una manera de representarlo de manera más significativa.

Dos representaciones equivalentes

Think about que en lugar de ser una secuencia de amplitudes a lo largo del tiempo, la onda anterior estuviera representada de una manera que no tuviera ninguna información sobre el tiempo. A continuación, imaginemos que tomamos esa representación e intentamos recuperar la señal authentic. Para que eso sea posible, la nueva representación tendría que contener de algún modo “tanta” información como la ola de la que partimos. Ese “tanto” se obtiene de la Transformada de Fouriery consta de las magnitudes y cambios de fase de los diferentes frecuencias que componen la señal.

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

dft <- torch_fft_fft(pattern$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 el coeficientes de Fouriercorrespondiente a las frecuencias contenidas en la señal. Cuanto mayor es su magnitud, más contribuyen a la señal:

magazine <- torch_abs(dft[1, ])

df <- information.body(
  x = 1:(size(pattern$waveform[1]) / 2),
  y = as.numeric(magazine[1:8000])
)

ggplot(df, aes(x = x, y = y)) +
  geom_line(measurement = 0.3) +
  ggtitle(
    paste0(
      "The spoken phrase "",
      pattern$label,
      "": Discrete Fourier Remodel"
    )
  ) +
  xlab("frequency") +
  ylab("magnitude") +
  theme_minimal()
La palabra hablada “pájaro”, en representación en el dominio de la frecuencia.

A partir de esta representación alternativa, podríamos volver a la onda sonora authentic tomando las frecuencias presentes en la señal, ponderándolas según sus coeficientes y sumándolas. Pero en una clasificación sólida, la información sobre el 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 “toma tu pastel y cómelo también”. ¿Qué pasaría si pudiéramos dividir la señal en pequeños trozos y ejecutar la Transformada de Fourier en cada uno de ellos? Como habrás adivinado a partir de este avance, esto es algo que podemos hacer; y la representación que crea se llama espectrograma.

Con un espectrograma, todavía conservamos cierta información en el dominio del tiempo, parte, ya que hay una pérdida inevitable de granularidad. Por otro lado, para cada uno de los segmentos temporales conocemos su composición espectral. Sin embargo, hay un punto importante que destacar. Las resoluciones que tomamos tiempo versus en frecuenciarespectivamente, 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 parece un gran problema (y en muchos casos lo será) no lo será para nosotros, como veremos muy pronto.

Pero primero, creemos e inspeccionemos dicho espectrograma para nuestra señal de ejemplo. En el siguiente fragmento de código, el tamaño de las ventanas (superpuestas) se elige de manera que permita 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
energy <- 0.5

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

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

Podemos visualizar el espectrograma visualmente:

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

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

picture(x = as.numeric(seconds),
      y = log_freqs,
      z = t(as.matrix(spec)),
      ylab = 'log frequency [Hz]',
      xlab = 'time [s]',
      col = hcl.colours(12, palette = "viridis")
)
principal <- paste0("Spectrogram, window measurement = ", window_size)
sub <- "Magnitude (sq. root)"
mtext(aspect = 3, line = 2, at = 0, adj = 0, cex = 1.3, principal)
mtext(aspect = 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 tanto, mejorar la sensibilidad), pudimos obtener un resultado razonable. (Con el viridis combinación de colores, los tonos de onda larga indican coeficientes de mayor valor; los de onda corta, todo lo contrario.)

Finalmente, volvamos a la pregunta essential. Si esta representación, por necesidad, es un compromiso, ¿por qué entonces querríamos emplearla? Aquí es donde adoptamos la perspectiva del aprendizaje profundo. El espectrograma es una representación bidimensional: una imagen. Con las imágenes, tenemos acceso a una rica reserva 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 destaca. Pronto verás que para esta tarea ni siquiera se necesitan arquitecturas sofisticadas; un convnet sencillo hará un muy buen trabajo.

Entrenamiento de una crimson neuronal en espectrogramas.

Empezamos creando un torch::dataset() que, a partir del authentic speechcommand_dataset()calcula un espectrograma para cada muestra.

spectrogram_dataset <- dataset(
  inherit = speechcommand_dataset,
  initialize = operate(...,
                        pad_to = 16000,
                        sampling_rate = 16000,
                        n_fft = 512,
                        window_size_seconds = 0.03,
                        window_stride_seconds = 0.01,
                        energy = 2) {
    self$pad_to <- pad_to
    self$window_size_samples <- sampling_rate *
      window_size_seconds
    self$window_stride_samples <- sampling_rate *
      window_stride_seconds
    self$energy <- energy
    self$spectrogram <- transform_spectrogram(
        n_fft = n_fft,
        win_length = self$window_size_samples,
        hop_length = self$window_stride_samples,
        normalized = TRUE,
        energy = self$energy
      )
    tremendous$initialize(...)
  },
  .getitem = operate(i) {
    merchandise <- tremendous$.getitem(i)

    x <- merchandise$waveform
    # ensure that all samples have the identical size (57)
    # shorter ones will probably be padded,
    # longer ones will probably be truncated
    x <- nnf_pad(x, pad = c(0, self$pad_to - dim(x)[2]))
    x <- x %>% self$spectrogram()

    if (is.null(self$energy)) {
      # on this case, there may be a further dimension, in place 4,
      # that we wish to seem in entrance
      # (as a second channel)
      x <- x$squeeze()$permute(c(3, 1, 2))
    }

    y <- merchandise$label_index
    checklist(x = x, y = y)
  }
)

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

En cuanto a la visualización, por supuesto, la representación completa y compleja es inconveniente; el gráfico del espectrograma necesitaría una dimensión adicional. Pero bien podemos preguntarnos si una crimson neuronal podría beneficiarse de la información adicional contenida en el número complejo “completo”. Al fin y al cabo, al reducir a magnitudes perdemos los desfases de los coeficientes individuales, que podrían contener información utilizable. De hecho, mis pruebas demostraron que así period; El uso de valores complejos dio como resultado una mayor precisión de clasificación.

Veamos de qué obtenemos spectrogram_dataset():

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

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

Tenemos 257 coeficientes para 101 ventanas; y cada coeficiente está representado por su parte actual e imaginaria.

A continuación, dividimos los datos y creamos una instancia del dataset() y dataloader() objetos.

train_ids <- pattern(
  1:size(ds),
  measurement = 0.6 * size(ds)
)
valid_ids <- pattern(
  setdiff(
    1:size(ds),
    train_ids
  ),
  measurement = 0.2 * size(ds)
)
test_ids <- setdiff(
  1:size(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 conversión sencilla, con abandono y normalización por lotes. Las partes actual e imaginaria de los coeficientes de Fourier se pasan a la inicial del modelo. nn_conv2d() como dos separados canales.

mannequin <- nn_module(
  initialize = operate() {
    self$options <- nn_sequential(
      nn_conv2d(2, 32, kernel_size = 3),
      nn_batch_norm2d(32),
      nn_relu(),
      nn_max_pool2d(kernel_size = 2),
      nn_dropout2d(p = 0.2),
      nn_conv2d(32, 64, kernel_size = 3),
      nn_batch_norm2d(64),
      nn_relu(),
      nn_max_pool2d(kernel_size = 2),
      nn_dropout2d(p = 0.2),
      nn_conv2d(64, 128, kernel_size = 3),
      nn_batch_norm2d(128),
      nn_relu(),
      nn_max_pool2d(kernel_size = 2),
      nn_dropout2d(p = 0.2),
      nn_conv2d(128, 256, kernel_size = 3),
      nn_batch_norm2d(256),
      nn_relu(),
      nn_max_pool2d(kernel_size = 2),
      nn_dropout2d(p = 0.2),
      nn_conv2d(256, 512, kernel_size = 3),
      nn_batch_norm2d(512),
      nn_relu(),
      nn_adaptive_avg_pool2d(c(1, 1)),
      nn_dropout2d(p = 0.2)
    )

    self$classifier <- nn_sequential(
      nn_linear(512, 512),
      nn_batch_norm1d(512),
      nn_relu(),
      nn_dropout(p = 0.5),
      nn_linear(512, 30)
    )
  },
  ahead = operate(x) {
    x <- self$options(x)$squeeze()
    x <- self$classifier(x)
    x
  }
)

A continuación determinamos una tasa de aprendizaje adecuada:

mannequin <- mannequin %>%
  setup(
    loss = nn_cross_entropy_loss(),
    optimizer = optim_adam,
    metrics = checklist(luz_metric_accuracy())
  )

rates_and_losses <- mannequin %>%
  lr_finder(train_dl)
rates_and_losses %>% plot()
Buscador de tasa de aprendizaje, ejecútelo en el modelo de espectrograma complejo.

Según la trama, decidí utilizar 0,01 como tasa de aprendizaje máxima. El entrenamiento continuó durante cuarenta épocas.

fitted <- mannequin %>%
  match(train_dl,
    epochs = 50, valid_data = valid_dl,
    callbacks = checklist(
      luz_callback_early_stopping(endurance = 3),
      luz_callback_lr_scheduler(
        lr_one_cycle,
        max_lr = 1e-2,
        epochs = 50,
        steps_per_epoch = size(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)
Ajustando el modelo de espectrograma complejo.

Comprobemos las precisiones reales.

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

Con treinta clases para distinguir, una precisión closing del conjunto de validación de ~0,94 parece un resultado muy decente.

Podemos confirmar esto en el equipo de prueba:

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

Una pregunta interesante es qué palabras se confunden con más frecuencia. (Por supuesto, aún más interesante es cómo se relacionan las probabilidades de error con las características de los espectrogramas, pero esto tenemos que dejarlo para el verdadero expertos en el dominio. Una buena forma de mostrar la matriz de confusión es crear un gráfico aluvial. Vemos que las predicciones, a la izquierda, “fluyen hacia” los espacios objetivo. (Los pares objetivo-predicción menos frecuentes que una milésima parte de la cardinalidad del conjunto de pruebas están ocultos).

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

Resumen

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

Foto por alex lauzon en desempaquetar

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

Related Articles

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Latest Articles