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.
Innanzitutto, caricherai ed eseguirai un modello preaddestrato in grado di riconoscere 20 comandi vocali. Poi, utilizzando il microfono, creerai e addestrerai una semplice rete neurale che riconosce i tuoi suoni e fa spostare il cursore a sinistra o a destra.
Questo codelab non tratterà la teoria alla base dei modelli di riconoscimento audio. Se ti interessa, consulta questo tutorial.
Abbiamo anche creato un glossario dei termini di machine learning che troverai in questo codelab.
Obiettivi didattici
- Come caricare un modello preaddestrato di riconoscimento dei comandi vocali
- Come eseguire 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 un altro browser moderno.
- Un editor di testo, in esecuzione localmente sul computer o sul web tramite Codepen o Glitch.
- Conoscenza di HTML, CSS, JavaScript e Chrome DevTools (o gli strumenti per sviluppatori del browser che preferisci).
- Una comprensione concettuale di alto livello delle reti neurali. Se hai bisogno di un'introduzione o di un ripasso, guarda questo video di 3blue1brown o questo video sul deep learning in JavaScript di Ashi Krishnan.
3. Caricare TensorFlow.js e il modello audio
Apri index.html in un editor e aggiungi questo contenuto:
<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 tag <script> importa il modello preaddestrato di comandi vocali. Il tag <div id="console"> verrà utilizzato per visualizzare l'output del modello.
4. Eseguire previsioni in tempo reale
Poi, 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. Tieni presente che questa procedura 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 dovrai 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 pazienta. Non appena il modello viene caricato, dovresti vedere una parola nella parte superiore della pagina. Il modello è stato addestrato per riconoscere i numeri da 0 a 9 e alcuni comandi aggiuntivi come "sinistra", "destra", "sì", "no" e così via.
Pronuncia una di queste parole. Il modello riconosce correttamente la parola? Prova a modificare probabilityThreshold, che controlla la frequenza con cui il modello viene attivato.Un valore di 0,75 significa che il modello verrà attivato quando è sicuro al 75% di aver sentito una determinata parola.
Per saperne di più sul modello di comandi vocali e sulla relativa API, consulta il file README.md su GitHub.
6. Raccogliere i dati
Per rendere il tutto più divertente, utilizziamo suoni brevi anziché parole intere per controllare il cursore.
Addestrerai un modello per riconoscere 3 comandi diversi: "Sinistra", "Destra" e "Rumore", che faranno spostare il cursore a sinistra o a destra. Il riconoscimento di "Rumore" (non è necessaria alcuna azione) è fondamentale nel rilevamento vocale, poiché vogliamo che il cursore reagisca solo quando produciamo il suono corretto e non quando parliamo e ci muoviamo in generale.
- Innanzitutto, dobbiamo raccogliere i dati. Aggiungi una semplice UI all'app inserendo 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 del codice
Questo codice potrebbe sembrare complicato all'inizio, quindi analizziamolo.
Abbiamo aggiunto tre pulsanti alla nostra UI con le etichette "Sinistra", "Destra" e "Rumore", che corrispondono ai tre comandi che vogliamo che il nostro modello riconosca. Quando si premono questi pulsanti, viene chiamata la funzione collect() appena aggiunta, che crea esempi di addestramento per il nostro modello.
collect() associa un'etichetta label all'output di recognizer.listen(). Poiché includeSpectrogram è impostato su true, recognizer.listen() fornisce lo spettrogramma grezzo (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 frame (circa 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 in genere numeri negativi elevati intorno a -100 e una deviazione di 10:
const mean = -100;
const std = 10;
return x.map(x => (x - mean) / std);
Infine, ogni esempio di addestramento avrà 2 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. Testare la raccolta dei dati
Apri index.html in un browser e dovresti vedere 3 pulsanti corrispondenti ai 3 comandi. Se lavori da un file locale, per accedere al microfono dovrai avviare un server web 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 coerente 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 ispezionare 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 di nuovo i dati quando testerai l'intera app.
8. Addestrare un modello
- Aggiungi un pulsante "Addestra" subito dopo il pulsante "Rumore" nel corpo di index.html:
<br/><br/>
<button id="train" onclick="train()">Train</button>
- 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;
}
- Chiama
buildModel()al caricamento dell'app:
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 "Addestra". Puoi testare l'addestramento raccogliendo di nuovo i dati e facendo clic su "Addestra" oppure puoi attendere il passaggio 10 per testare l'addestramento insieme alla previsione.
Analisi del codice
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 strati: uno strato convoluzionale che elabora i dati audio (rappresentati come uno spettrogramma), uno strato di max pooling, uno strato di appiattimento e uno strato denso che esegue il mapping alle 3 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 contenente 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 (circa 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 tra le probabilità previste (una probabilità per classe) e la probabilità del 100% nella classe effettiva e la probabilità dello 0% per tutte le altre classi. Forniamo anche accuracy come metrica da monitorare, che ci fornirà la percentuale di esempi corretti del modello dopo ogni epoca di addestramento.
Addestramento
L'addestramento viene eseguito 10 volte (epoche) sui dati utilizzando una dimensione del batch di 16 (elaborando 16 esempi alla volta) e mostra l'accuratezza corrente 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. Aggiornare il cursore in tempo reale
Ora che possiamo addestrare il nostro modello, aggiungiamo il codice per eseguire previsioni in tempo reale e spostare il cursore. Aggiungi questo codice subito dopo il pulsante "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 il seguente codice 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 del codice
Previsione in tempo reale
listen() ascolta il microfono ed esegue 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 che sommano a 1. Il tensore ha una dimensione esterna di 1 perché è 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.
Aggiornare il 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").
Eliminare i tensori
Per liberare 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 3 pulsanti corrispondenti ai 3 comandi. Ricorda di premere e tenere premuto ogni pulsante per 3-4 secondi durante la raccolta dei dati.
Una volta raccolti gli esempi, premi il pulsante "Addestra". Verrà avviato l'addestramento del modello e l'accuratezza del modello dovrebbe superare il 90%. Se non ottieni buone prestazioni del modello, prova a raccogliere altri dati.
Al termine dell'addestramento, premi il pulsante "Ascolta" per eseguire previsioni dal microfono e controllare il cursore.
Scopri altri tutorial all'indirizzo http://js.tensorflow.org/.