TensorFlow.js - Riconoscimento audio tramite Transfer Learning

1. Introduzione

In questo codelab, creerai una rete di riconoscimento audio e la userai per controllare un dispositivo di scorrimento nel browser producendo dei suoni. Utilizzerai TensorFlow.js, una libreria di machine learning potente e flessibile per JavaScript.

Innanzitutto, caricherai ed eseguirai un modello preaddestrato in grado di riconoscere 20 comandi vocali. Quindi, utilizzando il microfono, creerai e addestrerai una semplice rete neurale che riconosce i tuoi suoni e sposta il cursore verso sinistra o verso destra.

Questo codelab non illustra la teoria alla base dei modelli di riconoscimento audio. Se vuoi saperne di più, dai un'occhiata a questo tutorial.

Abbiamo anche creato un glossario di termini relativi al machine learning che puoi trovare in questo codelab.

Obiettivi didattici

  • Come caricare un modello preaddestrato di riconoscimento dei comandi vocali
  • Come fare previsioni in tempo reale usando il microfono
  • Addestrare e utilizzare un modello di riconoscimento audio personalizzato con il microfono del browser

E ora iniziamo.

2. Requisiti

Per completare questo codelab, ti serviranno:

  1. Una versione recente di Chrome o di un altro browser moderno.
  2. Un editor di testo in esecuzione localmente sul tuo computer o sul web tramite un editor di testo come Codepen o Glitch.
  3. Conoscenza di HTML, CSS, JavaScript e Chrome DevTools (o gli strumenti di sviluppo del browser che preferisci).
  4. Una comprensione concettuale di alto livello delle reti neurali. Se hai bisogno di una presentazione o di un ripasso, guarda questo video di 3blue1brown o questo video sul deep learning in JavaScript di Ashi Krishnan.

3. Carica TensorFlow.js e il modello audio

Apri index.html in un editor e aggiungi questi contenuti:

<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>

Il primo tag <script> importa la libreria TensorFlow.js, mentre il secondo <script> importa il modello di comandi vocali preaddestrato. Per visualizzare l'output del modello verrà utilizzato il tag <div id="console">.

4. Previsione in tempo reale

Successivamente, apri/crea il file index.js in un editor di codice e includi il seguente codice:

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. Testa la previsione

Assicurati che il tuo dispositivo abbia un microfono. Vale la pena notare che questa operazione funziona anche su un cellulare. Per eseguire la pagina web, apri la pagina index.html in un browser. Se stai lavorando da un file locale, per accedere al microfono dovrai avviare un server web e usare http://localhost:port/.

Per avviare un semplice server web sulla porta 8000:

python -m SimpleHTTPServer

Il download del modello potrebbe richiedere del tempo, quindi ti chiediamo di avere pazienza. Non appena il modello viene caricato, dovresti vedere una parola nella parte superiore della pagina. Il modello è stato addestrato a riconoscere i numeri da 0 a 9 e alcuni comandi aggiuntivi come "left", "right", "yes", "no" e così via.

Pronuncia una di queste parole. Riesce a capire correttamente la tua parola? Usa probabilityThreshold che controlla la frequenza di attivazione del modello: 0,75 significa che il modello si attiverà quando ha una certezza superiore al 75% di sentire una determinata parola.

Per saperne di più sul modello Speech Commands e sulla relativa API, vedi il file README.md su GitHub.

6. Raccogliere i dati

Per rendere il tutto divertente, possiamo usare suoni brevi anziché intere parole per controllare il dispositivo di scorrimento.

In questo modo addestrerai un modello affinché riconosca tre comandi diversi: "Sinistra" e "Destra" e "Rumore" che consente di spostare il cursore verso sinistra o verso destra. Rilevamento di "Rumore" (nessuna azione richiesta) è fondamentale per il rilevamento vocale, in quanto vogliamo che il cursore reagisca solo quando viene prodotto il suono giusto e non quando in generale stiamo parlando e ci spostiamo.

  1. Per prima cosa dobbiamo raccogliere i dati. Aggiungi una UI semplice all'app inserendo questa informazione all'interno del tag <body> prima di <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. Aggiungi questo 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. Rimuovi predictWord() da app():
async function app() {
 recognizer = speechCommands.create('BROWSER_FFT');
 await recognizer.ensureModelLoaded();
 // predictWord() no longer called.
}

Approfondimento

Questo codice all'inizio potrebbe essere complesso, quindi analizziamolo.

Abbiamo aggiunto alla UI tre pulsanti denominati "Sinistra", "Destra" e "Rumore", corrispondenti ai tre comandi che vogliamo riconoscere nel modello. La pressione di questi pulsanti chiama la funzione collect() appena aggiunta, che crea esempi di addestramento per il nostro modello.

collect() associa un label all'output di recognizer.listen(). Poiché il valore includeSpectrogram è vero,, recognizer.listen() fornisce lo spettrogramma non elaborato (dati di frequenza) per 1 secondo di audio, suddiviso in 43 fotogrammi, quindi ogni fotogramma corrisponde a circa 23 ms di audio:

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

Dal momento che vogliamo usare suoni brevi invece delle parole per controllare il cursore, prendiamo in considerazione solo gli ultimi 3 frame (~70 ms):

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

Per evitare problemi numerici, normalizziamo i dati in modo che abbiano una media di 0 e una deviazione standard di 1. In questo caso, i valori dello spettrogramma sono solitamente grandi numeri negativi intorno a -100 e deviazione di 10:

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

Infine, ogni esempio di addestramento avrà due campi:

  • label**** 0, 1 e 2 per "Sinistra", "Destra" e "Rumore" rispettivamente.
  • vals**** 696 numeri che contengono le informazioni sulla frequenza (spettrogramma)

e memorizziamo tutti i dati nella variabile examples:

examples.push({vals, label});

7. Testa la raccolta dei dati

Apri la pagina index.html in un browser, dovresti vedere tre pulsanti corrispondenti ai tre comandi. Se stai lavorando da un file locale, per accedere al microfono dovrai avviare un server web e usare http://localhost:port/.

Per avviare un semplice server web sulla porta 8000:

python -m SimpleHTTPServer

Per raccogliere esempi per ogni comando, crea un suono coerente ripetutamente (o continuo) tenendo premuto ciascun pulsante per 3-4 secondi. Dovresti raccogliere circa 150 esempi per ogni etichetta. Ad esempio, possiamo schiacciare le dita per "Sinistra", fischiettare per "Destra" e alternare tra silenziamento e parlare per "Rumore".

Man mano che raccogli altri esempi, il contatore mostrato nella pagina dovrebbe aumentare. Puoi anche esaminare i dati chiamando console.log() sulla variabile examples nella console. A questo punto, l'obiettivo è testare il processo di raccolta dei dati. In seguito, raccoglierai nuovamente i dati quando testerai l'intera app.

8. Addestra un modello

  1. Aggiungi un "Addestramento" subito dopo il pulsante "Rumore" nel corpo in index.html:
<br/><br/>
<button id="train" onclick="train()">Train</button>
  1. Aggiungi il seguente codice al codice esistente in 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. Chiama buildModel() quando viene caricata l'app:
async function app() {
 recognizer = speechCommands.create('BROWSER_FFT');
 await recognizer.ensureModelLoaded();
 // Add this line.
 buildModel();
}

A questo punto, aggiornando l'app vedrai un nuovo pulsante "Addestra" . Puoi testare l'addestramento raccogliendo nuovamente i dati e facendo clic su "Addestra" oppure puoi attendere il passaggio 10 per testare l'addestramento insieme alla previsione.

Approfondimento

A livello generale, stiamo facendo due cose: buildModel() definisce l'architettura del modello e train() addestra il modello utilizzando i dati raccolti.

Architettura del modello

Il modello ha 4 livelli: uno convoluzionale che elabora i dati audio (rappresentato come uno spettrogramma), uno massimo del pool, uno flatten e uno denso che mappa alle tre azioni:

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'}));

La forma di input del modello è [NUM_FRAMES, 232, 1], dove ogni frame è di 23 ms di audio e contiene 232 numeri che corrispondono a frequenze diverse (232 sono stati scelti perché è la quantità di bucket di frequenza necessari per acquisire la voce umana). In questo codelab, utilizziamo campioni di 3 frame (con circa 70 ms di campioni) perché stiamo producendo suoni invece di pronunciare parole intere per controllare il cursore.

Compiliamo il nostro modello per prepararlo per l'addestramento:

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

Usiamo l'ottimizzatore Adam, uno strumento comune utilizzato nel deep learning, e categoricalCrossEntropy per la perdita, la funzione di perdita standard utilizzata per la classificazione. In breve, misura quanto le probabilità previste (una probabilità per classe) siano rispetto al 100% di probabilità nella classe vera e allo 0% di probabilità per tutte le altre classi. Forniamo anche accuracy come metrica da monitorare, che ci fornirà la percentuale di esempi che il modello raggiunge dopo ogni epoca di addestramento.

Addestramento

L'addestramento esegue 10 volte (epoche) sui dati utilizzando una dimensione del batch di 16 (elaborando 16 esempi alla volta) e mostra la precisione attuale nell'UI:

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. Aggiorna il cursore in tempo reale.

Ora che possiamo addestrare il modello, aggiungiamo il codice per fare previsioni in tempo reale e spostiamo il cursore. Aggiungilo subito dopo "Addestra" in index.html:

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

E quanto segue in 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
 });
}

Approfondimento

Previsione in tempo reale

listen() ascolta il microfono e fa previsioni in tempo reale. Il codice è molto simile al metodo collect(), che normalizza lo spettrogramma non elaborato e scarta tutti i frame tranne gli ultimi NUM_FRAMES. L'unica differenza è che chiamiamo anche il modello addestrato per ottenere una previsione:

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

L'output di model.predict(input)è un tensore di forma [1, numClasses] che rappresenta una distribuzione di probabilità sul numero di classi. Più semplicemente, questo è solo un insieme di confidenza per ciascuna delle possibili classi di output, che sommano a 1. Il Tensor ha una dimensione esterna pari a 1 perché questa è la dimensione del batch (un singolo esempio).

Per convertire la distribuzione di probabilità in un singolo numero intero che rappresenta la classe più probabile, chiamiamo probs.argMax(1)che restituisce l'indice della classe con la probabilità più alta. Passiamo l'"1" come parametro dell'asse perché vogliamo calcolare il argMax nell'ultima dimensione, numClasses.

Aggiornare il cursore

moveSlider() diminuisce il valore del dispositivo di scorrimento se l'etichetta è 0 ("Sinistra") , lo aumenta se l'etichetta è 1 ("Destra") e ignora se l'etichetta è 2 ("Rumore").

Disporre i tensori

Per liberare la memoria GPU, è importante chiamare manualmente tf.dispose() sui tensori di output. L'alternativa a tf.dispose() manuale sta eseguendo il wrapping delle chiamate di funzione in un tf.tidy(), ma questo metodo non può essere utilizzato con le funzioni asincrone.

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

10. Testa l'app finale

Apri index.html nel tuo browser e raccogli i dati come hai fatto nella sezione precedente con i tre pulsanti corrispondenti ai tre comandi. Ricorda di tenere premuto ciascun pulsante per 3-4 secondi durante la raccolta dei dati.

Dopo aver raccolto gli esempi, premi il pulsante "Addestra". In questo modo verrà avviato l'addestramento del modello e dovresti vedere che la precisione del modello supera il 90%. Se le prestazioni del modello non sono buone, prova a raccogliere più dati.

Al termine dell'addestramento, premi il pulsante "Ascolta" per fare previsioni dal microfono e controlla il cursore.

Per altri tutorial, visita http://js.tensorflow.org/.