Posit AI Weblog: Classificação de áudio com tocha


Variações sobre um tema

Classificação de áudio simples com Keras, Classificação de áudio com Keras: analisando mais de perto as partes de aprendizagem não profunda, Classificação de áudio simples com tocha: Não, este não é o primeiro publish neste weblog que apresenta a classificação da fala usando aprendizagem profunda. Com duas dessas postagens (as “aplicadas”) ele compartilha a configuração geral, o tipo de arquitetura de aprendizagem profunda empregada e o conjunto de dados utilizado. Com o terceiro, tem em comum o interesse pelas ideias e conceitos envolvidos. Cada uma dessas postagens tem um foco diferente – você deveria ler esta?

Bem, é claro que não posso dizer “não” – ainda mais porque, aqui, você tem uma versão abreviada e condensada do capítulo sobre este tema no próximo livro da CRC Press, Aprendizado profundo e computação científica com R torch. A título de comparação com o publish anterior que utilizou torchescrito pelo criador e mantenedor do torchaudioAthos Damiani, desenvolvimentos significativos ocorreram no torch ecossistema, o resultado closing foi que o código ficou muito mais fácil (principalmente na parte de treinamento do modelo). Dito isso, vamos encerrar o preâmbulo e mergulhar no assunto!

Inspecionando os dados

Nós usamos o comandos de fala conjunto de dados (Diretor (2018)) que vem com torchaudio. O conjunto de dados contém gravações de trinta palavras diferentes de uma ou duas sílabas, pronunciadas por diferentes falantes. Existem cerca de 65.000 arquivos de áudio no whole. Nossa tarefa será prever, apenas a partir do áudio, qual das trinta palavras possíveis foi pronunciada.

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

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

Começamos inspecionando os dados.

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

Escolhendo uma amostra aleatoriamente, vemos que a informação que precisaremos está contida em quatro propriedades: waveform, sample_rate, label_indexe label.

O primeiro, waveformserá nosso preditor.

pattern <- ds(2000)
dim(pattern$waveform)
(1)     1 16000

Os valores dos tensores individuais são centralizados em zero e variam entre -1 e 1. Existem 16.000 deles, refletindo o fato de que a gravação durou um segundo e foi registrada em (ou foi convertida para, pelos criadores do conjunto de dados) em um taxa de 16.000 amostras por segundo. Estas últimas informações são armazenadas em pattern$sample_rate:

(1) 16000

Todas as gravações foram amostradas na mesma taxa. Sua duração quase sempre equivale a um segundo; os – muito – poucos sons que são minimamente mais longos podemos truncar com segurança.

Finalmente, o alvo é armazenado, na forma inteira, em pattern$label_indexa palavra correspondente estando disponível em pattern$label:

pattern$label
pattern$label_index
(1) "chicken"
torch_tensor
2
( CPULongType{} )

Qual é a “aparência” deste sinal de áudio?

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(dimension = 0.3) +
  ggtitle(
    paste0(
      "The spoken phrase "", pattern$label, "": Sound wave"
    )
  ) +
  xlab("time") +
  ylab("amplitude") +
  theme_minimal()
Posit AI Weblog: Classificação de áudio com tocha

O que vemos é uma sequência de amplitudes, refletindo a onda sonora produzida por alguém que diz “pássaro”. Em outras palavras, temos aqui uma série temporal de “valores de quantity”. Mesmo para especialistas, adivinhar qual palavra resultou nessas amplitudes é uma tarefa impossível. É aqui que entra o conhecimento do domínio. O especialista pode não ser capaz de aproveitar muito o sinal nesta representação; mas eles podem conhecer uma maneira de representá-lo de forma mais significativa.

Duas representações equivalentes

Think about que, em vez de ser uma sequência de amplitudes ao longo do tempo, a onda acima fosse representada de uma forma que não tivesse nenhuma informação sobre o tempo. A seguir, think about que pegamos essa representação e tentamos recuperar o sinal unique. Para que isso fosse possível, a nova representação teria de, de alguma forma, conter “tanta” informação quanto a onda da qual partimos. Que “o mesmo” é obtido a partir do Transformada de Fouriere consiste nas magnitudes e mudanças de fase dos diferentes frequências que compõem o sinal.

Como é, então, a versão transformada de Fourier da onda sonora do “pássaro”? Nós o obtemos ligando torch_fft_fft() (onde fft significa Transformada Rápida de Fourier):

dft <- torch_fft_fft(pattern$waveform)
dim(dft)
(1)     1 16000

O comprimento deste tensor é o mesmo; no entanto, seus valores não estão em ordem cronológica. Em vez disso, eles representam o Coeficientes de Fouriercorrespondendo às frequências contidas no sinal. Quanto maior a sua magnitude, mais contribuem para o sinal:

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(dimension = 0.3) +
  ggtitle(
    paste0(
      "The spoken phrase "",
      pattern$label,
      "": Discrete Fourier Rework"
    )
  ) +
  xlab("frequency") +
  ylab("magnitude") +
  theme_minimal()
A palavra falada “pássaro”, na representação no domínio da frequência.

A partir desta representação alternativa, poderíamos voltar à onda sonora unique pegando nas frequências presentes no sinal, ponderando-as de acordo com os seus coeficientes e somando-as. Mas na classificação correta, as informações de tempo certamente devem ser importantes; nós realmente não queremos jogá-lo fora.

Combinando representações: o espectrograma

Na verdade, o que realmente nos ajudaria é uma síntese de ambas as representações; algum tipo de “pegue seu bolo e coma também”. E se pudéssemos dividir o sinal em pequenos pedaços e executar a Transformada de Fourier em cada um deles? Como você deve ter adivinhado nesta preparação, isso é realmente algo que podemos fazer; e a representação que ele cria é chamada de espectrograma.

Com um espectrograma, ainda mantemos algumas informações no domínio do tempo – algumas, já que há uma perda inevitável de granularidade. Por outro lado, para cada um dos segmentos de tempo, aprendemos sobre a sua composição espectral. Há um ponto importante a ser destacado, no entanto. As resoluções que recebemos tempo versus em freqüênciarespectivamente, são inversamente relacionados. Se dividirmos os sinais em vários pedaços (chamados de “janelas”), a representação de frequência por janela não será muito refinada. Por outro lado, se quisermos obter uma melhor resolução no domínio da frequência, temos que escolher janelas mais longas, perdendo assim informação sobre como a composição espectral varia ao longo do tempo. O que parece ser um grande problema – e em muitos casos será – não será um problema para nós, como você verá muito em breve.

Porém, primeiro vamos criar e inspecionar esse espectrograma para nosso sinal de exemplo. No trecho de código a seguir, o tamanho das janelas – sobrepostas – é escolhido de modo a permitir granularidade razoável tanto no domínio do tempo quanto no domínio da frequência. Ficamos com sessenta e três janelas e, para cada janela, obtemos duzentos e cinquenta e sete 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 exibir o 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")
)
fundamental <- paste0("Spectrogram, window dimension = ", window_size)
sub <- "Magnitude (sq. root)"
mtext(facet = 3, line = 2, at = 0, adj = 0, cex = 1.3, fundamental)
mtext(facet = 3, line = 1, at = 0, adj = 0, cex = 1, sub)
A palavra falada “pássaro”: Espectrograma.

Sabemos que perdemos alguma resolução tanto no tempo quanto na frequência. Porém, exibindo a raiz quadrada das magnitudes dos coeficientes – e, portanto, aumentando a sensibilidade – ainda conseguimos obter um resultado razoável. (Com o viridis esquema de cores, tons de onda longa indicam coeficientes de maior valor; os de ondas curtas, o oposto.)

Finalmente, voltemos à questão essential. Se esta representação, por necessidade, é um compromisso – por que, então, quereríamos empregá-la? É aqui que adotamos a perspectiva do aprendizado profundo. O espectrograma é uma representação bidimensional: uma imagem. Com as imagens, temos acesso a um rico reservatório de técnicas e arquiteturas: entre todas as áreas em que o deep studying teve sucesso, o reconhecimento de imagens ainda se destaca. Em breve você verá que para esta tarefa nem mesmo são necessárias arquiteturas sofisticadas; um convnet simples fará um trabalho muito bom.

Treinando uma rede neural em espectrogramas

Começamos criando um torch::dataset() que, a partir do unique speechcommand_dataset()calcula um espectrograma para cada amostra.

spectrogram_dataset <- dataset(
  inherit = speechcommand_dataset,
  initialize = perform(...,
                        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 = perform(i) {
    merchandise <- tremendous$.getitem(i)

    x <- merchandise$waveform
    # be sure all samples have the identical size (57)
    # shorter ones might be padded,
    # longer ones might 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 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
    listing(x = x, y = y)
  }
)

Na lista de parâmetros para spectrogram_dataset()observação energycom um valor padrão de 2. Este é o valor que, salvo indicação em contrário, torchde transform_spectrogram() assumirá que energy deveria ter. Nestas circunstâncias, os valores que compõem o espectrograma são as magnitudes quadradas dos coeficientes de Fourier. Usando energyvocê pode alterar o padrão e especificar, por exemplo, se deseja valores absolutos (energy = 1), qualquer outro valor positivo (como 0.5aquele que usamos acima para exibir um exemplo concreto) – ou ambas as partes actual e imaginária dos coeficientes (energy = NULL).

Em termos de exibição, é claro, a representação completa e complexa é inconveniente; o gráfico do espectrograma precisaria de uma dimensão adicional. Mas podemos muito bem perguntar-nos se uma rede neural poderia lucrar com a informação adicional contida no número complexo “inteiro”. Afinal, ao reduzir para magnitudes perdemos as mudanças de fase dos coeficientes individuais, que podem conter informações úteis. Na verdade, meus testes mostraram que sim; o uso de valores complexos resultou em maior precisão de classificação.

Vamos ver o que ganhamos 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

Temos 257 coeficientes para 101 janelas; e cada coeficiente é representado por suas partes reais e imaginárias.

Em seguida, dividimos os dados e instanciamos o dataset() e dataloader() objetos.

train_ids <- pattern(
  1:size(ds),
  dimension = 0.6 * size(ds)
)
valid_ids <- pattern(
  setdiff(
    1:size(ds),
    train_ids
  ),
  dimension = 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

O modelo é uma rede simples, com dropout e normalização em lote. As partes actual e imaginária dos coeficientes de Fourier são passadas para o modelo inicial nn_conv2d() como dois separados canais.

mannequin <- nn_module(
  initialize = perform() {
    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 = perform(x) {
    x <- self$options(x)$squeeze()
    x <- self$classifier(x)
    x
  }
)

A seguir determinamos uma taxa de aprendizagem adequada:

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

rates_and_losses <- mannequin %>%
  lr_finder(train_dl)
rates_and_losses %>% plot()
Localizador de taxa de aprendizagem, executado no modelo de espectrograma complexo.

Com base no gráfico, decidi usar 0,01 como taxa máxima de aprendizado. O treinamento durou quarenta épocas.

fitted <- mannequin %>%
  match(train_dl,
    epochs = 50, valid_data = valid_dl,
    callbacks = listing(
      luz_callback_early_stopping(persistence = 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 o modelo de espectrograma complexo.

Vamos verificar as precisões reais.

"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

Com trinta courses para distinguir, uma precisão closing do conjunto de validação de ~0,94 parece um resultado muito decente!

Podemos confirmar isso no conjunto de teste:

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

Uma questão interessante é quais palavras são confundidas com mais frequência. (Claro, ainda mais interessante é como as probabilidades de erro estão relacionadas às características dos espectrogramas – mas isso, temos que deixar para o verdadeiro especialistas do domínio. Uma boa maneira de exibir a matriz de confusão é criar um gráfico aluvial. Vemos as previsões, à esquerda, “fluindo para” os slots de destino. (Os pares de previsão-alvo menos frequentes que um milésimo da cardinalidade do conjunto de testes estão ocultos.)

Gráfico aluvial para a configuração do espectrograma complexo.

Conclusão

É isso por hoje! Nas próximas semanas, espere mais postagens baseadas no conteúdo do livro CRC que será lançado em breve, Aprendizado profundo e computação científica com R torch. Obrigado por ler!

Foto de Alex Lauzon sobre Remover respingo

Diretor, Pete. 2018. “Comandos de fala: UM Conjunto de dados para reconhecimento de fala com vocabulário limitado.” CoRR abs/1804.03209. http://arxiv.org/abs/1804.03209.

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *