TensorFlow.js: reconhecimento de áudio usando aprendizado por transferência

1. Introdução

Neste codelab, você vai criar uma rede de reconhecimento de áudio e usá-la para controlar um controle deslizante no navegador fazendo sons. Você vai usar o TensorFlow.js, uma biblioteca de machine learning avançada e flexível para JavaScript.

Primeiro, você vai carregar e executar um modelo pré-treinado que pode reconhecer 20 comandos de voz. Em seguida, usando o microfone, você vai criar e treinar uma rede neural simples que reconhece seus sons e faz o controle deslizante ir para a esquerda ou para a direita.

Este codelab não abordará a teoria por trás dos modelos de reconhecimento de áudio. Caso esteja curioso sobre isso, confira este tutorial.

Também criamos um glossário de termos de machine learning que você encontra neste codelab.

O que você vai aprender

  • Como carregar um modelo pré-treinado de reconhecimento de comandos de voz
  • Como fazer previsões em tempo real usando o microfone
  • Como treinar e usar um modelo de reconhecimento de áudio personalizado com o microfone do navegador

Vamos começar.

2. Requisitos

Para concluir este codelab, você vai precisar de:

  1. Uma versão moderna do Chrome ou de outro navegador mais recente
  2. Um editor de texto executado localmente na máquina ou na Web usando um software como o Codepen ou o Glitch
  3. Conhecimentos sobre HTML, CSS, JavaScript e Chrome DevTools (ou as DevTools do seu navegador preferido)
  4. Conhecimento conceitual de alto nível sobre redes neurais. Se você precisar de uma introdução ou revisão, assista a este vídeo da 3blue1brown ou este vídeo sobre aprendizado profundo em JavaScript de Ashi Krishnan (links em inglês)

3. Carregar o TensorFlow.js e o modelo de áudio

Abra index.html em um editor e adicione este conteúdo:

<html>
  <head>
    <script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs"></script>
    <script src="https://cdn.jsdelivr.net/npm/@tensorflow-models/speech-commands"></script>
  </head>
  <body>
    <div id="console"></div>
    <script src="index.js"></script>
  </body>
</html>

A primeira tag <script> importa a biblioteca do TensorFlow.js, e a segunda <script> importa o modelo de comandos de voz pré-treinado. A tag <div id="console"> será usada para mostrar a saída do modelo.

4. Previsão em tempo real

Em seguida, abra/crie o arquivo index.js em um editor de código e inclua o seguinte código:

let recognizer;

function predictWord() {
 // Array of words that the recognizer is trained to recognize.
 const words = recognizer.wordLabels();
 recognizer.listen(({scores}) => {
   // Turn scores into a list of (score,word) pairs.
   scores = Array.from(scores).map((s, i) => ({score: s, word: words[i]}));
   // Find the most probable word.
   scores.sort((s1, s2) => s2.score - s1.score);
   document.querySelector('#console').textContent = scores[0].word;
 }, {probabilityThreshold: 0.75});
}

async function app() {
 recognizer = speechCommands.create('BROWSER_FFT');
 await recognizer.ensureModelLoaded();
 predictWord();
}

app();

5. Testar a previsão

Verifique se o dispositivo tem um microfone. Vale lembrar que isso também funciona em um celular. Para executar a página da Web, abra index.html em um navegador. Se você estiver trabalhando em um arquivo local, para acessar o microfone, inicie um servidor da Web e use http://localhost:port/.

Para iniciar um servidor da Web simples na porta 8000:

python -m SimpleHTTPServer

O download do modelo pode levar um tempo. Assim que o modelo for carregado, uma palavra vai aparecer na parte de cima da página. O modelo foi treinado para reconhecer os números de 0 a 9 e alguns comandos adicionais, como "esquerda", "direita", "sim", "não" etc.

Fale uma dessas palavras. Ele acerta a palavra? Brinque com o probabilityThreshold, que controla a frequência com que o modelo é acionado.0,75 significa que o modelo será acionado quando tiver mais de 75% de confiança de que ouviu uma determinada palavra.

Para saber mais sobre o modelo de comandos de voz e a API dele, consulte o README.md no GitHub.

6. Coletar dados

Para deixar mais divertido, vamos usar sons curtos em vez de palavras inteiras para controlar o controle deslizante.

Você vai treinar um modelo para reconhecer três comandos diferentes: "Esquerda", "Direita" e "Ruído", que farão o controle deslizante se mover para a esquerda ou para a direita. Reconhecer "Ruído" (nenhuma ação necessária) é fundamental na detecção de fala, já que queremos que o controle deslizante reaja apenas quando produzimos o som certo, e não quando estamos falando e nos movimentando.

  1. Primeiro, precisamos coletar dados. Adicione uma interface simples ao app inserindo o seguinte na tag <body> antes de <div id="console">:
<button id="left" onmousedown="collect(0)" onmouseup="collect(null)">Left</button>
<button id="right" onmousedown="collect(1)" onmouseup="collect(null)">Right</button>
<button id="noise" onmousedown="collect(2)" onmouseup="collect(null)">Noise</button>
  1. Adicione isso a index.js:
// One frame is ~23ms of audio.
const NUM_FRAMES = 3;
let examples = [];

function collect(label) {
 if (recognizer.isListening()) {
   return recognizer.stopListening();
 }
 if (label == null) {
   return;
 }
 recognizer.listen(async ({spectrogram: {frameSize, data}}) => {
   let vals = normalize(data.subarray(-frameSize * NUM_FRAMES));
   examples.push({vals, label});
   document.querySelector('#console').textContent =
       `${examples.length} examples collected`;
 }, {
   overlapFactor: 0.999,
   includeSpectrogram: true,
   invokeCallbackOnNoiseAndUnknown: true
 });
}

function normalize(x) {
 const mean = -100;
 const std = 10;
 return x.map(x => (x - mean) / std);
}
  1. Remova predictWord() de app():
async function app() {
 recognizer = speechCommands.create('BROWSER_FFT');
 await recognizer.ensureModelLoaded();
 // predictWord() no longer called.
}

Detalhamento

Esse código pode ser confuso no início, então vamos explicar.

Adicionamos três botões à nossa interface rotulados como "Left", "Right" e "Noise", correspondentes aos três comandos que queremos que nosso modelo reconheça. Ao pressionar esses botões, a função collect() recém-adicionada é chamada, criando exemplos de treinamento para o modelo.

collect() associa um label à saída de recognizer.listen(). Como includeSpectrogram é verdadeiro,, recognizer.listen() fornece o espectrograma bruto (dados de frequência) de 1 segundo de áudio, dividido em 43 frames. Portanto, cada frame tem ~23 ms de áudio:

recognizer.listen(async ({spectrogram: {frameSize, data}}) => {
...
}, {includeSpectrogram: true});

Como queremos usar sons curtos em vez de palavras para controlar o controle deslizante, estamos considerando apenas os últimos três frames (~70 ms):

let vals = normalize(data.subarray(-frameSize * NUM_FRAMES));

Para evitar problemas numéricos, normalizamos os dados para ter uma média de 0 e um desvio padrão de 1. Nesse caso, os valores do espectrograma geralmente são grandes números negativos em torno de -100 e desvio de 10:

const mean = -100;
const std = 10;
return x.map(x => (x - mean) / std);

Por fim, cada exemplo de treinamento terá dois campos:

  • label****: 0, 1 e 2 para "Esquerda", "Direita" e "Ruído", respectivamente.
  • vals****: 696 números com as informações de frequência (espectrograma)

e armazenamos todos os dados na variável examples:

examples.push({vals, label});

7. Coleta de dados de teste

Abra index.html em um navegador. Você vai encontrar três botões correspondentes aos três comandos. Se você estiver trabalhando em um arquivo local, para acessar o microfone, inicie um servidor da Web e use http://localhost:port/.

Para iniciar um servidor da Web simples na porta 8000:

python -m SimpleHTTPServer

Para coletar exemplos de cada comando, faça um som consistente repetidamente (ou continuamente) enquanto pressiona e mantém pressionado cada botão por 3 a 4 segundos. Colete cerca de 150 exemplos para cada rótulo. Por exemplo, estale os dedos para "Esquerda", assobie para "Direita" e alterne entre silêncio e fala para "Ruído".

À medida que você coleta mais exemplos, o contador mostrado na página aumenta. Você também pode inspecionar os dados chamando console.log() na variável examples no console. Neste ponto, o objetivo é testar o processo de coleta de dados. Depois, você vai coletar dados novamente ao testar o app inteiro.

8. Treinar um modelo

  1. Adicione um botão Treinar logo após o botão Ruído no corpo em index.html:
<br/><br/>
<button id="train" onclick="train()">Train</button>
  1. Adicione o seguinte ao código atual em index.js:
const INPUT_SHAPE = [NUM_FRAMES, 232, 1];
let model;

async function train() {
 toggleButtons(false);
 const ys = tf.oneHot(examples.map(e => e.label), 3);
 const xsShape = [examples.length, ...INPUT_SHAPE];
 const xs = tf.tensor(flatten(examples.map(e => e.vals)), xsShape);

 await model.fit(xs, ys, {
   batchSize: 16,
   epochs: 10,
   callbacks: {
     onEpochEnd: (epoch, logs) => {
       document.querySelector('#console').textContent =
           `Accuracy: ${(logs.acc * 100).toFixed(1)}% Epoch: ${epoch + 1}`;
     }
   }
 });
 tf.dispose([xs, ys]);
 toggleButtons(true);
}

function buildModel() {
 model = tf.sequential();
 model.add(tf.layers.depthwiseConv2d({
   depthMultiplier: 8,
   kernelSize: [NUM_FRAMES, 3],
   activation: 'relu',
   inputShape: INPUT_SHAPE
 }));
 model.add(tf.layers.maxPooling2d({poolSize: [1, 2], strides: [2, 2]}));
 model.add(tf.layers.flatten());
 model.add(tf.layers.dense({units: 3, activation: 'softmax'}));
 const optimizer = tf.train.adam(0.01);
 model.compile({
   optimizer,
   loss: 'categoricalCrossentropy',
   metrics: ['accuracy']
 });
}

function toggleButtons(enable) {
 document.querySelectorAll('button').forEach(b => b.disabled = !enable);
}

function flatten(tensors) {
 const size = tensors[0].length;
 const result = new Float32Array(tensors.length * size);
 tensors.forEach((arr, i) => result.set(arr, i * size));
 return result;
}
  1. Chame buildModel() quando o app for carregado:
async function app() {
 recognizer = speechCommands.create('BROWSER_FFT');
 await recognizer.ensureModelLoaded();
 // Add this line.
 buildModel();
}

Nesse ponto, se você atualizar o app, vai aparecer um novo botão Treinar. Para testar o treinamento, colete os dados novamente e clique em "Treinar". Também é possível esperar até a etapa 10 para testar o treinamento e a previsão.

Arrasando na pista

Em um nível mais alto, fazemos duas coisas: buildModel() define a arquitetura do modelo e train() treina o modelo usando os dados coletados.

Arquitetura do modelo

O modelo tem quatro camadas: uma camada convolucional que processa os dados de áudio (representados como um espectrograma), uma camada de agrupamento máximo, uma camada de achatamento e uma camada densa que mapeia as três ações:

model = tf.sequential();
 model.add(tf.layers.depthwiseConv2d({
   depthMultiplier: 8,
   kernelSize: [NUM_FRAMES, 3],
   activation: 'relu',
   inputShape: INPUT_SHAPE
 }));
 model.add(tf.layers.maxPooling2d({poolSize: [1, 2], strides: [2, 2]}));
 model.add(tf.layers.flatten());
 model.add(tf.layers.dense({units: 3, activation: 'softmax'}));

O formato de entrada do modelo é [NUM_FRAMES, 232, 1], em que cada frame tem 23 ms de áudio com 232 números que correspondem a diferentes frequências. O número 232 foi escolhido porque é a quantidade de intervalos de frequência necessária para capturar a voz humana. Neste codelab, usamos amostras de três frames (~70 ms) porque estamos fazendo sons em vez de falar palavras inteiras para controlar o controle deslizante.

Compilamos nosso modelo para prepará-lo para o treinamento:

const optimizer = tf.train.adam(0.01);
 model.compile({
   optimizer,
   loss: 'categoricalCrossentropy',
   metrics: ['accuracy']
 });

Usamos o otimizador Adam, um otimizador comum usado no aprendizado profundo, e categoricalCrossEntropy para perda, a função de perda padrão usada para classificação. Em resumo, ela mede o quanto as probabilidades previstas (uma por classe) estão distantes de ter 100% de probabilidade na classe verdadeira e 0% de probabilidade para todas as outras classes. Também fornecemos accuracy como uma métrica de monitoramento, que nos dá a porcentagem de exemplos que o modelo acerta após cada época de treinamento.

Treinamento

O treinamento passa 10 vezes (épocas) pelos dados usando um tamanho de lote de 16 (processando 16 exemplos por vez) e mostra a acurácia atual na interface:

await model.fit(xs, ys, {
   batchSize: 16,
   epochs: 10,
   callbacks: {
     onEpochEnd: (epoch, logs) => {
       document.querySelector('#console').textContent =
           `Accuracy: ${(logs.acc * 100).toFixed(1)}% Epoch: ${epoch + 1}`;
     }
   }
 });

9. Atualizar o controle deslizante em tempo real

Agora que podemos treinar nosso modelo, vamos adicionar código para fazer previsões em tempo real e mover o controle deslizante. Adicione isso logo após o botão Treinar em index.html:

<br/><br/>
<button id="listen" onclick="listen()">Listen</button>
<input type="range" id="output" min="0" max="10" step="0.1">

E o seguinte em index.js:

async function moveSlider(labelTensor) {
 const label = (await labelTensor.data())[0];
 document.getElementById('console').textContent = label;
 if (label == 2) {
   return;
 }
 let delta = 0.1;
 const prevValue = +document.getElementById('output').value;
 document.getElementById('output').value =
     prevValue + (label === 0 ? -delta : delta);
}

function listen() {
 if (recognizer.isListening()) {
   recognizer.stopListening();
   toggleButtons(true);
   document.getElementById('listen').textContent = 'Listen';
   return;
 }
 toggleButtons(false);
 document.getElementById('listen').textContent = 'Stop';
 document.getElementById('listen').disabled = false;

 recognizer.listen(async ({spectrogram: {frameSize, data}}) => {
   const vals = normalize(data.subarray(-frameSize * NUM_FRAMES));
   const input = tf.tensor(vals, [1, ...INPUT_SHAPE]);
   const probs = model.predict(input);
   const predLabel = probs.argMax(1);
   await moveSlider(predLabel);
   tf.dispose([input, probs, predLabel]);
 }, {
   overlapFactor: 0.999,
   includeSpectrogram: true,
   invokeCallbackOnNoiseAndUnknown: true
 });
}

Arrasando na pista

Previsão em tempo real

O listen() ouve o microfone e faz previsões em tempo real. O código é muito semelhante ao método collect(), que normaliza o espectrograma bruto e descarta todos os frames, exceto os últimos NUM_FRAMES. A única diferença é que também chamamos o modelo treinado para receber uma previsão:

const probs = model.predict(input);
const predLabel = probs.argMax(1);
await moveSlider(predLabel);

A saída de model.predict(input) é um tensor de forma [1, numClasses] que representa uma distribuição de probabilidade sobre o número de classes. De maneira mais simples, esse é apenas um conjunto de níveis de confiança para cada uma das classes de saída possíveis, que somam 1. O tensor tem uma dimensão externa de 1 porque esse é o tamanho do lote (um único exemplo).

Para converter a distribuição de probabilidade em um único número inteiro que representa a classe mais provável, chamamos probs.argMax(1), que retorna o índice da classe com a maior probabilidade. Transmitimos "1" como o parâmetro de eixo porque queremos calcular o argMax na última dimensão, numClasses.

Como atualizar o controle deslizante

moveSlider() diminui o valor do controle deslizante se o marcador for 0 ("Esquerda") , aumenta se o marcador for 1 ("Direita") e ignora se o marcador for 2 ("Ruído").

Descarte de tensores

Para limpar a memória da GPU, é importante chamar manualmente tf.dispose() em tensores de saída. A alternativa ao tf.dispose() manual é encapsular chamadas de função em um tf.tidy(), mas isso não pode ser usado com funções assíncronas.

   tf.dispose([input, probs, predLabel]);

10. Testar o app final

Abra index.html no navegador e colete dados como fez na seção anterior com os três botões correspondentes aos três comandos. Não se esqueça de tocar e pressionar cada botão por 3 a 4 segundos ao coletar dados.

Depois de coletar exemplos, clique no botão Treinar. Isso vai iniciar o treinamento do modelo, e a acurácia dele vai passar de 90%. Se você não conseguir um bom desempenho do modelo, tente coletar mais dados.

Quando o treinamento terminar, pressione o botão Ouvir para fazer previsões com o microfone e controlar o controle deslizante.

Confira mais tutoriais em http://js.tensorflow.org/.