1. Einführung
In diesem Codelab erstellen Sie ein Netzwerk zur Audioerkennung und verwenden es, um einen Schieberegler im Browser durch Geräusche zu steuern. Sie verwenden TensorFlow.js, eine leistungsstarke und flexible Bibliothek für maschinelles Lernen in JavaScript.
Zuerst laden und führen Sie ein vortrainiertes Modell aus, das 20 Sprachbefehle erkennen kann. Anschließend erstellen und trainieren Sie mit Ihrem Mikrofon ein einfaches neuronales Netzwerk, das Ihre Geräusche erkennt und den Schieberegler nach links oder rechts bewegt.
In diesem Codelab wird nicht auf die Theorie hinter Audioerkennungsmodellen eingegangen. Wenn Sie mehr darüber erfahren möchten, sehen Sie sich diese Anleitung an.
Wir haben auch ein Glossar mit Begriffen zum maschinellen Lernen erstellt, die in diesem Codelab verwendet werden.
Lerninhalte
- Vortrainiertes Modell zur Spracherkennung von Befehlen laden
- Echtzeitvorhersagen mit dem Mikrofon erstellen
- Benutzerdefiniertes Modell für die Audioerkennung mit dem Browsermikrofon trainieren und verwenden
Fangen wir also an.
2. Voraussetzungen
Für dieses Codelab benötigen Sie Folgendes:
- Eine aktuelle Version von Chrome oder einem anderen modernen Browser.
- Ein Texteditor, der entweder lokal auf Ihrem Computer oder im Web über Codepen oder Glitch ausgeführt wird.
- Kenntnisse von HTML, CSS, JavaScript und den Chrome-Entwicklertools (oder den Entwicklertools Ihres bevorzugten Browsers).
- Ein grundlegendes konzeptionelles Verständnis von neuronalen Netzwerken. Wenn Sie eine Einführung oder Auffrischung benötigen, können Sie sich dieses Video von 3blue1brown oder dieses Video zu Deep Learning in JavaScript von Ashi Krishnan ansehen.
3. TensorFlow.js und das Audio-Modell laden
Öffnen Sie index.html in einem Editor und fügen Sie diesen Inhalt hinzu:
<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>
Mit dem ersten <script>-Tag wird die TensorFlow.js-Bibliothek importiert und mit dem zweiten das vortrainierte Speech Commands-Modell.<script> Mit dem Tag <div id="console"> wird die Ausgabe des Modells angezeigt.
4. In Echtzeit vorhersagen
Öffnen oder erstellen Sie als Nächstes die Datei index.js in einem Code-Editor und fügen Sie den folgenden Code ein:
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. Vorhersage testen
Achten Sie darauf, dass Ihr Gerät über ein Mikrofon verfügt. Das funktioniert übrigens auch auf einem Smartphone. Öffnen Sie index.html in einem Browser, um die Webseite auszuführen. Wenn Sie mit einer lokalen Datei arbeiten, müssen Sie einen Webserver starten und http://localhost:port/ verwenden, um auf das Mikrofon zuzugreifen.
So starten Sie einen einfachen Webserver auf Port 8000:
python -m SimpleHTTPServer
Das Herunterladen des Modells kann etwas dauern. Sobald das Modell geladen ist, sollte oben auf der Seite ein Wort angezeigt werden. Das Modell wurde darauf trainiert, die Zahlen 0 bis 9 und einige zusätzliche Befehle wie „links“, „rechts“, „ja“ und „nein“ zu erkennen.
Sprechen Sie eines dieser Wörter. Wird das Wort richtig erkannt? Mit dem probabilityThreshold können Sie festlegen, wie oft das Modell ausgelöst wird.Bei einem Wert von 0,75 wird das Modell ausgelöst, wenn es mit einer Wahrscheinlichkeit von mehr als 75% ein bestimmtes Wort hört.
Weitere Informationen zum Speech Commands-Modell und seiner API finden Sie in der README.md auf GitHub.
6. Daten erheben
Damit es Spaß macht, verwenden wir kurze Töne anstelle von ganzen Wörtern, um den Schieberegler zu steuern.
Sie trainieren ein Modell, das drei verschiedene Befehle erkennen soll: „Links“, „Rechts“ und „Rauschen“. Dadurch soll der Schieberegler nach links oder rechts bewegt werden. Die Erkennung von „Rauschen“ (keine Aktion erforderlich) ist bei der Spracherkennung von entscheidender Bedeutung, da der Schieberegler nur reagieren soll, wenn wir den richtigen Klang erzeugen, und nicht, wenn wir sprechen und uns bewegen.
- Zuerst müssen wir Daten erheben. Fügen Sie der App eine einfache Benutzeroberfläche hinzu, indem Sie Folgendes innerhalb des
<body>-Tags vor dem<div id="console">einfügen:
<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>
- Fügen Sie Folgendes zu
index.jshinzu:
// 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);
}
- Entfernen Sie
predictWord()ausapp():
async function app() {
recognizer = speechCommands.create('BROWSER_FFT');
await recognizer.ensureModelLoaded();
// predictWord() no longer called.
}
Details zur Mutation
Dieser Code kann anfangs etwas verwirrend sein. Deshalb sehen wir uns ihn genauer an.
Wir haben der Benutzeroberfläche drei Schaltflächen mit den Beschriftungen „Links“, „Rechts“ und „Rauschen“ hinzugefügt, die den drei Befehlen entsprechen, die unser Modell erkennen soll. Durch Drücken dieser Schaltflächen wird die neu hinzugefügte collect()-Funktion aufgerufen, mit der Trainingsbeispiele für unser Modell erstellt werden.
Mit collect() wird eine label mit der Ausgabe von recognizer.listen() verknüpft. Da includeSpectrogram wahr, ist, gibt recognizer.listen() das Rohspektrogramm (Frequenzdaten) für 1 Sekunde Audio an, unterteilt in 43 Frames. Jeder Frame entspricht also etwa 23 ms Audio:
recognizer.listen(async ({spectrogram: {frameSize, data}}) => {
...
}, {includeSpectrogram: true});
Da wir kurze Töne anstelle von Wörtern verwenden möchten, um den Schieberegler zu steuern, berücksichtigen wir nur die letzten drei Frames (ca. 70 ms):
let vals = normalize(data.subarray(-frameSize * NUM_FRAMES));
Um numerische Probleme zu vermeiden, normalisieren wir die Daten so, dass sie einen Durchschnitt von 0 und eine Standardabweichung von 1 haben. In diesem Fall sind die Spektrogrammwerte in der Regel große negative Zahlen um -100 und die Abweichung beträgt 10:
const mean = -100;
const std = 10;
return x.map(x => (x - mean) / std);
Jedes Trainingsbeispiel hat zwei Felder:
label****: 0, 1 und 2 für „Links“, „Rechts“ und „Rauschen“.vals****: 696 Zahlen mit den Frequenzinformationen (Spektrogramm)
Alle Daten werden in der Variablen examples gespeichert:
examples.push({vals, label});
7. Erhebung von Testdaten
Öffnen Sie index.html in einem Browser. Es sollten drei Schaltflächen für die drei Befehle angezeigt werden. Wenn Sie mit einer lokalen Datei arbeiten, müssen Sie einen Webserver starten und http://localhost:port/ verwenden, um auf das Mikrofon zuzugreifen.
So starten Sie einen einfachen Webserver auf Port 8000:
python -m SimpleHTTPServer
Um Beispiele für jeden Befehl zu sammeln, machen Sie wiederholt (oder kontinuierlich) ein gleichmäßiges Geräusch, während Sie jede Taste gedrückt halten (3–4 Sekunden). Sie sollten etwa 150 Beispiele für jedes Label sammeln. Wir können beispielsweise mit den Fingern schnipsen, um „Links“ zu signalisieren, pfeifen, um „Rechts“ zu signalisieren, und zwischen Stille und Sprechen wechseln, um „Geräusch“ zu signalisieren.
Wenn Sie weitere Beispiele sammeln, sollte der Zähler auf der Seite ansteigen. Sie können die Daten auch prüfen, indem Sie in der Konsole console.log() für die Variable examples aufrufen. An diesem Punkt geht es darum, den Prozess der Datenerhebung zu testen. Später werden Sie Daten noch einmal erfassen, wenn Sie die gesamte App testen.
8. Modell trainieren
- Fügen Sie direkt nach der Schaltfläche Noise (Rauschen) im Body in index.html die Schaltfläche Train (Trainieren) ein:
<br/><br/>
<button id="train" onclick="train()">Train</button>
- Fügen Sie den vorhandenen Code in index.js ein:
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;
}
- Rufen Sie
buildModel()beim Laden der App auf:
async function app() {
recognizer = speechCommands.create('BROWSER_FFT');
await recognizer.ensureModelLoaded();
// Add this line.
buildModel();
}
Wenn Sie die App jetzt aktualisieren, sehen Sie die neue Schaltfläche Trainieren. Sie können das Training testen, indem Sie Daten noch einmal erfassen und auf „Trainieren“ klicken. Alternativ können Sie bis Schritt 10 warten, um das Training zusammen mit der Vorhersage zu testen.
Aufschlüsselung
Im Wesentlichen führen wir zwei Schritte aus: buildModel() definiert die Modellarchitektur und train() trainiert das Modell mit den erfassten Daten.
Modellarchitektur
Das Modell hat vier Ebenen: eine Convolutional-Ebene, die die Audiodaten (als Spektrogramm dargestellt) verarbeitet, eine Max-Pool-Ebene, eine Flatten-Ebene und eine Dense-Ebene, die den drei Aktionen zugeordnet wird:
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'}));
Die Eingabeform des Modells ist [NUM_FRAMES, 232, 1]. Jeder Frame besteht aus 23 ms Audio mit 232 Zahlen, die verschiedenen Frequenzen entsprechen. Die Zahl 232 wurde gewählt, weil sie der Anzahl der Frequenzbereiche entspricht, die zum Erfassen der menschlichen Stimme erforderlich sind. In diesem Codelab verwenden wir Samples mit einer Länge von 3 Frames (ca. 70 ms), da wir Geräusche machen, anstatt ganze Wörter zu sprechen, um den Schieberegler zu steuern.
Wir kompilieren unser Modell, um es für das Training vorzubereiten:
const optimizer = tf.train.adam(0.01);
model.compile({
optimizer,
loss: 'categoricalCrossentropy',
metrics: ['accuracy']
});
Wir verwenden den Adam-Optimierer, einen gängigen Optimierer, der im Deep Learning eingesetzt wird, und categoricalCrossEntropy für den Verlust, die Standardverlustfunktion für die Klassifizierung. Kurz gesagt: Sie misst, wie weit die vorhergesagten Wahrscheinlichkeiten (eine Wahrscheinlichkeit pro Klasse) davon entfernt sind, eine Wahrscheinlichkeit von 100% in der richtigen Klasse und eine Wahrscheinlichkeit von 0% für alle anderen Klassen zu haben. Wir stellen auch accuracy als Messwert zum Monitoring bereit. Dieser gibt den Prozentsatz der Beispiele an, die das Modell nach jeder Trainingsepoche richtig klassifiziert.
Schulung
Beim Training werden die Daten zehnmal (Epochen) mit einer Batchgröße von 16 durchlaufen (es werden jeweils 16 Beispiele verarbeitet). Die aktuelle Genauigkeit wird in der Benutzeroberfläche angezeigt:
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. Schieberegler in Echtzeit aktualisieren
Jetzt, da wir unser Modell trainieren können, fügen wir Code hinzu, um Vorhersagen in Echtzeit zu treffen und den Schieberegler zu bewegen. Fügen Sie diesen Code direkt nach dem Button Train in index.html ein:
<br/><br/>
<button id="listen" onclick="listen()">Listen</button>
<input type="range" id="output" min="0" max="10" step="0.1">
Und Folgendes 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
});
}
Aufschlüsselung
Echtzeitvorhersage
listen() hört auf das Mikrofon und erstellt Echtzeitvorhersagen. Der Code ähnelt der Methode collect(), die das Rohspektrogramm normalisiert und alle Frames bis auf die letzten NUM_FRAMES verwirft. Der einzige Unterschied besteht darin, dass wir auch das trainierte Modell aufrufen, um eine Vorhersage zu erhalten:
const probs = model.predict(input);
const predLabel = probs.argMax(1);
await moveSlider(predLabel);
Die Ausgabe von model.predict(input) ist ein Tensor der Form [1, numClasses], der eine Wahrscheinlichkeitsverteilung für die Anzahl der Klassen darstellt. Einfacher ausgedrückt: Dies ist nur eine Reihe von Konfidenzwerten für jede der möglichen Ausgabeklassen, die sich zu 1 summieren. Der Tensor hat eine äußere Dimension von 1, da dies die Größe des Batches ist (ein einzelnes Beispiel).
Um die Wahrscheinlichkeitsverteilung in eine einzelne Ganzzahl umzuwandeln, die die wahrscheinlichste Klasse darstellt, rufen wir probs.argMax(1)auf, das den Klassenindex mit der höchsten Wahrscheinlichkeit zurückgibt. Wir übergeben „1“ als Achsenparameter, da wir argMax für die letzte Dimension, numClasses, berechnen möchten.
Schieberegler aktualisieren
moveSlider() verringert den Wert des Schiebereglers, wenn das Label 0 („Links“) ist, erhöht ihn, wenn das Label 1 („Rechts“) ist, und ignoriert ihn, wenn das Label 2 („Rauschen“) ist.
Tensoren verwerfen
Um den GPU-Speicher zu bereinigen, ist es wichtig, dass wir tf.dispose() für Ausgabetensoren manuell aufrufen. Die Alternative zur manuellen tf.dispose()-Funktion besteht darin, Funktionsaufrufe in eine tf.tidy()-Funktion einzuschließen. Dies kann jedoch nicht mit asynchronen Funktionen verwendet werden.
tf.dispose([input, probs, predLabel]);
10. Endgültige App testen
Öffnen Sie index.html in Ihrem Browser und erheben Sie Daten wie im vorherigen Abschnitt mit den drei Schaltflächen, die den drei Befehlen entsprechen. Denken Sie daran, jede Taste beim Erheben von Daten 3–4 Sekunden lang gedrückt zu halten.
Wenn Sie Beispiele gesammelt haben, drücken Sie die Schaltfläche Trainieren. Dadurch wird das Training des Modells gestartet und die Genauigkeit des Modells sollte über 90 % liegen. Wenn Sie keine gute Modellleistung erzielen, sollten Sie versuchen, mehr Daten zu erfassen.
Wenn das Training abgeschlossen ist, drücken Sie die Schaltfläche Zuhören, um Vorhersagen über das Mikrofon zu treffen und den Schieberegler zu steuern.
Weitere Anleitungen finden Sie unter http://js.tensorflow.org/.