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

1. Introdução

Neste codelab, você criará uma rede de reconhecimento de áudio e a usará para controlar um controle deslizante no navegador emitindo sons. Você 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 fala. Depois, 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 direita.

Este codelab não vai 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 podem ser encontrados neste codelab.

O que você vai aprender

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

Vamos começar.

2. Requisitos

Para concluir este codelab, você vai precisar do seguinte:

  1. Uma versão recente do Chrome ou de outro navegador moderno.
  2. Um editor de texto executado localmente na sua máquina ou na Web com algo como o Codepen ou o Glitch.
  3. Conhecimento sobre HTML, CSS, JavaScript e Chrome DevTools (ou as DevTools do seu navegador preferido).
  4. Uma compreensão conceitual de alto nível das 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. Prever 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 ressaltar 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, será necessário iniciar um servidor da Web e usar http://localhost:port/ para acessar o microfone.

Para iniciar um servidor da Web simples na porta 8000:

python -m SimpleHTTPServer

O download do modelo pode demorar um pouco, então tenha paciência. Uma palavra vai aparecer no topo da página assim que o modelo for carregado. 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. A palavra está correta? Teste o probabilityThreshold, que controla a frequência de disparo do modelo.0,75 significa que o modelo será disparado quando tiver mais de 75% de confiança de que ouve 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 ficar 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" o que move o controle deslizante para a esquerda ou direita. Como reconhecer "ruído" (nenhuma ação necessária) é fundamental na detecção de fala, porque queremos que o controle deslizante reaja somente quando produzimos o som certo, e não quando estamos falando de maneira geral e nos movimentando.

  1. Primeiro, precisamos coletar dados. Adicione uma interface simples ao app dentro da 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 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

Este código pode parecer complicado no início, então vamos analisá-lo.

Adicionamos três botões à nossa interface, chamados "Esquerda", "Direita" e "Ruído", correspondentes aos três comandos que queremos que o modelo reconheça. Pressionar esses botões chama a função collect() recém-adicionada, que cria exemplos de treinamento para o modelo.

collect() associa um label à saída de recognizer.listen(). Como includeSpectrogram é verdadeiro,, o recognizer.listen() fornece o espectrograma bruto (dados de frequência) para 1 segundo de áudio, dividido em 43 frames, de modo que cada frame tenha aproximadamente 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, consideramos apenas os últimos três frames (aproximadamente 70 ms):

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

Para evitar problemas numéricos, normalizamos os dados para uma média de 0 e um desvio padrão de 1. Nesse caso, os valores do espectrograma são geralmente 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 para ver três botões correspondentes aos três comandos. Se você estiver trabalhando em um arquivo local, será necessário iniciar um servidor da Web e usar http://localhost:port/ para acessar o microfone.

Para iniciar um servidor da Web simples na porta 8000:

python -m SimpleHTTPServer

Para coletar exemplos de cada comando, emita um som consistente repetidamente (ou continuamente) enquanto pressione e segure cada botão por três a quatro segundos. Você deve coletar cerca de 150 exemplos para cada rótulo. Por exemplo, podemos estalar os dedos para "Esquerda", assobiar para "Direita" e alternar entre silêncio e falar para "ruído".

Conforme você coleta mais exemplos, o contador mostrado na página deve aumentar. 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. Mais tarde, você vai coletar dados novamente quando estiver testando o app todo.

8. Treinar um modelo

  1. Adicione um "Trem". logo após "Noise" no corpo da tag index.html:
<br/><br/>
<button id="train" onclick="train()">Train</button>
  1. Adicione o seguinte ao código que está 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 momento, se você atualizar o aplicativo, verá uma nova mensagem "Train" . 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 junto com a previsão.

Detalhes

De modo geral, estamos fazendo duas coisas: buildModel() define a arquitetura do modelo e train() treina o modelo usando os dados coletados.

Arquitetura de modelos

O modelo tem quatro camadas: uma convolucional que processa os dados de áudio (representada como um espectrograma), uma camada máxima do pool, uma camada nivelada 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 contendo 232 números que correspondem a frequências diferentes (232 foi escolhido porque é a quantidade de intervalos de frequência necessários para capturar a voz humana). Neste codelab, estamos usando amostras com três frames (amostras de aproximadamente 70 ms), já que estamos fazendo sons em vez de falar palavras inteiras para controlar o controle deslizante.

Compilamos o modelo para prepará-lo para treinamento:

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

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

Treinamento

O treinamento é executado 10 vezes (períodos) sobre os 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 logo após "Train" 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
 });
}

Detalhes

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 NUM_FRAMES, exceto os últimos. 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 forma mais simples, isso é apenas um conjunto de confianças para cada uma das possíveis classes de saída 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. Passamos um "1" como o parâmetro do eixo porque queremos calcular o argMax sobre a última dimensão, numClasses.

Como atualizar o controle deslizante

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

Descarte de tensores

Para limpar a memória da GPU, é importante chamar manualmente tf.dispose() nos tensores de saída. A alternativa ao tf.dispose() manual é unir 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 seu navegador e colete dados como você fez na seção anterior, com os três botões que correspondem aos três comandos. Lembre-se de manter cada botão pressionado por três a quatro segundos enquanto coleta dados.

Depois de coletar exemplos, pressione o botão Treinar. Isso vai começar a treinar o modelo, e a acurácia vai ficar acima de 90%. Se o desempenho do modelo não for bom, tente coletar mais dados.

Quando o treinamento terminar, pressione o botão " Listen" para fazer previsões pelo microfone e controlar o controle deslizante.

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