1. Introduzione
In questo codelab creerai una rete di riconoscimento audio e la utilizzerai per controllare un cursore nel browser emettendo suoni. Utilizzerai TensorFlow.js, una libreria di machine learning potente e flessibile per JavaScript.
Per prima cosa, caricherai ed eseguirai un modello preaddestrato in grado di riconoscere 20 comandi vocali. Poi, usando il microfono, creerai e addestrerai una semplice rete neurale che riconosce i tuoi suoni e fa spostare il cursore verso sinistra o destra.
Questo codelab non tratterà la teoria alla base dei modelli di riconoscimento audio. Se ti incuriosisce, dai un'occhiata a questo tutorial.
Abbiamo anche creato un glossario dei termini di machine learning che trovi in questo codelab.
Obiettivi didattici
- Come caricare un modello di riconoscimento dei comandi vocali preaddestrato
- Come fare previsioni in tempo reale utilizzando il microfono
- Come addestrare e utilizzare un modello di riconoscimento audio personalizzato utilizzando il microfono del browser
E ora iniziamo.
2. Requisiti
Per completare questo codelab, ti serviranno:
- Una versione recente di Chrome o di un altro browser moderno.
- Un editor di testo, eseguito localmente sul computer o sul web tramite Codepen o Glitch.
- Conoscenza di HTML, CSS, JavaScript e Chrome DevTools (o degli strumenti di sviluppo dei browser che preferisci).
- Una comprensione concettuale di alto livello delle reti neurali. Se hai bisogno di un'introduzione o di un ripasso, ti consigliamo di guardare 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 Speech Commands preaddestrato. Il tag <div id="console"> verrà utilizzato per visualizzare l'output del modello.
4. Previsioni 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. Testare la previsione
Assicurati che il dispositivo abbia un microfono. È importante notare che questa operazione funziona anche su un cellulare. Per eseguire la pagina web, apri index.html in un browser. Se lavori da un file locale, per accedere al microfono devi avviare un server web e utilizzare http://localhost:port/.
Per avviare un semplice server web sulla porta 8000:
python -m SimpleHTTPServer
Il download del modello potrebbe richiedere un po' di 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 "sinistra", "destra", "sì", "no" e così via.
Pronuncia una di queste parole. Riesce a capire correttamente la parola? Gioca con il valore probabilityThreshold, che controlla la frequenza con cui il modello si attiva.0,75 significa che il modello si attiverà quando avrà una confidenza superiore al 75% di sentire una determinata parola.
Per saperne di più sul modello Speech Commands e sulla relativa API, consulta il file README.md su GitHub.
6. Raccogli i dati
Per renderlo più divertente, utilizziamo suoni brevi anziché parole intere per controllare il cursore.
Addestrerai un modello per riconoscere tre diversi comandi: "Sinistra", "Destra" e "Rumore", che faranno spostare il cursore verso sinistra o destra. Il riconoscimento del "rumore" (nessuna azione necessaria) è fondamentale nel rilevamento del parlato, poiché vogliamo che il cursore reagisca solo quando produciamo il suono giusto e non quando parliamo e ci muoviamo in generale.
- Innanzitutto, dobbiamo raccogliere i dati. Aggiungi un'interfaccia utente semplice all'app aggiungendo questo codice 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>
- Aggiungi questo codice 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);
}
- Rimuovi
predictWord()daapp():
async function app() {
recognizer = speechCommands.create('BROWSER_FFT');
await recognizer.ensureModelLoaded();
// predictWord() no longer called.
}
Analisi
All'inizio questo codice potrebbe sembrare complicato, quindi analizziamolo passo passo.
Abbiamo aggiunto tre pulsanti alla nostra UI con le etichette "Sinistra", "Destra" e "Rumore", corrispondenti ai tre comandi che vogliamo che il nostro modello riconosca. Se premi questi pulsanti, viene chiamata la funzione collect() appena aggiunta, che crea esempi di addestramento per il nostro modello.
collect() associa un label all'output di recognizer.listen(). Poiché includeSpectrogram è true, recognizer.listen() fornisce lo spettrogramma non elaborato (dati di frequenza) per 1 secondo di audio, suddiviso in 43 frame, quindi ogni frame corrisponde a circa 23 ms di audio:
recognizer.listen(async ({spectrogram: {frameSize, data}}) => {
...
}, {includeSpectrogram: true});
Poiché vogliamo utilizzare suoni brevi anziché parole per controllare il cursore, prendiamo in considerazione solo gli ultimi 3 fotogrammi (~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 generalmente numeri negativi elevati 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 contenenti le informazioni sulla frequenza (spettrogramma)
e archiviamo tutti i dati nella variabile examples:
examples.push({vals, label});
7. Raccolta dei dati di test
Apri index.html in un browser e dovresti vedere tre pulsanti corrispondenti ai tre comandi. Se lavori da un file locale, per accedere al microfono devi avviare un web server e utilizzare http://localhost:port/.
Per avviare un semplice server web sulla porta 8000:
python -m SimpleHTTPServer
Per raccogliere esempi per ogni comando, emetti un suono costante ripetutamente (o continuamente) mentre premi e tieni premuto ogni pulsante per 3-4 secondi. Dovresti raccogliere circa 150 esempi per ogni etichetta. Ad esempio, possiamo schioccare le dita per "Sinistra", fischiare per "Destra" e alternare silenzio e parlato per "Rumore".
Man mano che raccogli altri esempi, il contatore visualizzato 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 un secondo momento raccoglierai nuovamente i dati durante il test dell'intera app.
8. Addestra un modello
- Aggiungi un pulsante "Treno" subito dopo il pulsante "Rumore" nel corpo di index.html:
<br/><br/>
<button id="train" onclick="train()">Train</button>
- Aggiungi quanto segue 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;
}
- Chiama
buildModel()quando l'app si carica:
async function app() {
recognizer = speechCommands.create('BROWSER_FFT');
await recognizer.ensureModelLoaded();
// Add this line.
buildModel();
}
A questo punto, se aggiorni l'app, vedrai un nuovo pulsante "Allenamento". 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.
Analisi
A livello generale, eseguiamo due operazioni: buildModel() definisce l'architettura del modello e train() addestra il modello utilizzando i dati raccolti.
Architettura del modello
Il modello ha 4 livelli: un livello convoluzionale che elabora i dati audio (rappresentati come spettrogramma), un livello di pooling massimo, un livello di flattening e un livello denso che mappa le 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 è costituito da 23 ms di audio contenenti 232 numeri che corrispondono a frequenze diverse (232 è stato scelto perché è la quantità di bucket di frequenza necessari per acquisire la voce umana). In questo codelab, utilizziamo campioni di 3 frame (~70 ms) poiché emettiamo suoni anziché pronunciare parole intere per controllare il cursore.
Compiliamo il modello per prepararlo all'addestramento:
const optimizer = tf.train.adam(0.01);
model.compile({
optimizer,
loss: 'categoricalCrossentropy',
metrics: ['accuracy']
});
Utilizziamo l'ottimizzatore Adam, un ottimizzatore comune utilizzato nel deep learning, e categoricalCrossEntropy per la perdita, la funzione di perdita standard utilizzata per la classificazione. In breve, misura la distanza delle probabilità previste (una probabilità per classe) dalla probabilità del 100% nella classe vera e dalla probabilità dello 0% per tutte le altre classi. Forniamo anche accuracy come metrica da monitorare, che ci darà la percentuale di esempi che il modello ottiene corretti dopo ogni epoca di addestramento.
Addestramento
L'addestramento viene eseguito 10 volte (epoche) sui dati utilizzando una dimensione batch di 16 (elaborando 16 esempi alla volta) e mostra l'accuratezza attuale nell'interfaccia utente:
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. Aggiornare il cursore in tempo reale
Ora che possiamo addestrare il modello, aggiungiamo il codice per fare previsioni in tempo reale e spostare il cursore. Aggiungi questo codice subito dopo il pulsante "Train" (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
});
}
Analisi
Previsione in tempo reale
listen() ascolta il microfono ed effettua previsioni in tempo reale. Il codice è molto simile al metodo collect(), che normalizza lo spettrogramma grezzo ed elimina 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, si tratta di un insieme di confidenze per ciascuna delle possibili classi di output la cui somma è pari a 1. Il tensore 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 "1" come parametro dell'asse perché vogliamo calcolare argMax sull'ultima dimensione, numClasses.
Aggiornamento del cursore
moveSlider() diminuisce il valore del cursore se l'etichetta è 0 ("Sinistra") , lo aumenta se l'etichetta è 1 ("Destra") e lo ignora se l'etichetta è 2 ("Rumore").
Eliminazione dei tensori
Per pulire la memoria della GPU, è importante chiamare manualmente tf.dispose() sui tensori di output. L'alternativa a tf.dispose() manuale è racchiudere le chiamate di funzione in un tf.tidy(), ma questo non può essere utilizzato con le funzioni asincrone.
tf.dispose([input, probs, predLabel]);
10. Testare l'app finale
Apri index.html nel browser e raccogli i dati come hai fatto nella sezione precedente con i tre pulsanti corrispondenti ai tre comandi. Ricordati di tenere premuto ogni tasto per 3-4 secondi durante la raccolta dei dati.
Una volta raccolti gli esempi, premi il pulsante "Addestra". In questo modo inizierà l'addestramento del modello e dovresti notare che la sua accuratezza supera il 90%. Se non ottieni un buon rendimento del modello, prova a raccogliere più dati.
Al termine dell'addestramento, premi il pulsante "Ascolta" per fare previsioni dal microfono e controllare il cursore.
Scopri altri tutorial all'indirizzo http://js.tensorflow.org/.