Variaciones sobre un tema
Clasificación de audio easy 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 easy con antorcha: No, esta no es la primera publicación de este weblog que presenta la clasificación del habla mediante el aprendizaje profundo. Con dos de esas publicaciones (las “aplicadas”) comparte la configuración normal, 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”, 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ó torch
escrito por el creador y mantenedor de torchaudio
Athos Damiani, se han producido importantes avances en la torch
ecosistema, el resultado remaining 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 complete. Nuestra tarea será predecir, únicamente a partir del audio, cuál de las treinta palabras posibles se pronunció.
Comenzamos inspeccionando los datos.
(1) "mattress" "hen" "cat" "canine" "down" "eight"
(7) "5" "4" "go" "blissful" "home" "left"
(32) " marvin" "9" "no" "off" "on" "one"
(19) "proper" "seven" "sheila" "six" "cease" "three"
(25) "tree" "two" "up" "wow" "sure" "zero"
Al elegir una muestra al azar, vemos que la información que necesitaremos está contenida en cuatro propiedades: waveform
, sample_rate
, label_index
y label
.
El primero, waveform
será 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 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 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_index
la palabra correspondiente está disponible en pattern$label
:
pattern$label
pattern$label_index
(1) "hen"
torch_tensor
2
( CPULongType{} )
¿Cómo se ve esta señal de audio?
library(ggplot2)
df <- knowledge.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()

Lo que vemos es una secuencia de amplitudes, que refleja la onda de sonido producida por alguien que cube “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
Think about 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 unique. 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()
(donde 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 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:
magazine <- torch_abs(dft(1, ))
df <- knowledge.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()

A partir de esta representación alternativa, podríamos volver a la onda de sonido unique 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 hora 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
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 mostrar 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")
)
primary <- paste0("Spectrogram, window measurement = ", window_size)
sub <- "Magnitude (sq. root)"
mtext(aspect = 3, line = 2, at = 0, adj = 0, cex = 1.3, primary)
mtext(aspect = 3, line = 1, at = 0, adj = 0, cex = 1, sub)

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 colour, los tonos de onda larga indican coeficientes de mayor valor; los de onda corta, 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 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 pink neuronal en espectrogramas
Empezamos creando un torch::dataset()
que, partiendo del unique 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 shall be padded,
# longer ones shall 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's an extra dimension, in place 4,
# that we need 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 a spectrogram_dataset()
Nota energy
con un valor por defecto 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 energy
puede cambiar el valor predeterminado y especificar, por ejemplo, si desea valores absolutos (energy = 1
), cualquier otro valor positivo (como 0.5
el que usamos arriba para mostrar un ejemplo concreto) – o tanto la parte actual como la imaginaria de los coeficientes (energy = 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 pink 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 demostraron que sí; 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",
obtain = TRUE,
energy = NULL
)
dim(ds(1)$x)
(1) 2 257 101
Tenemos 257 coeficientes para 101 ventanas; y cada coeficiente está representado por sus partes actual e imaginaria.
A continuación, dividimos los datos e instanciamos el 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 convnet sencilla, con abandono y normalización por lotes. Las partes actual e imaginaria de los coeficientes de Fourier se pasan a los valores iniciales 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:

Basado en la gráfica, decidí usar 0.01 como tasa de aprendizaje máxima. El entrenamiento se prolongó 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)

Vamos a comprobar 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 entre ellas, ¡una precisión remaining del conjunto de validación de ~0.94 parece un resultado muy decente!
Podemos confirmar esto en el conjunto de prueba:
consider(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).

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.