TensorFlow.js – Audioerkennung mit Lerntransfer

1. Einführung

In diesem Codelab bauen Sie ein Audioerkennungsnetzwerk ein und steuern damit einen Schieberegler im Browser, indem Sie Töne erstellen. Sie verwenden TensorFlow.js, eine leistungsstarke und flexible ML-Bibliothek für JavaScript.

Zuerst laden Sie ein vortrainiertes Modell, das 20 Sprachbefehle erkennen kann, und führen es aus. Dann erstellen und trainieren Sie mit Ihrem Mikrofon ein einfaches neuronales Netzwerk, das Ihre Töne erkennt und den Schieberegler nach links oder rechts bewegt.

In diesem Codelab geht es nicht um die Theorie hinter Audioerkennungsmodellen. Wenn Sie mehr darüber erfahren möchten, sehen Sie sich diese Anleitung an.

Außerdem haben wir ein Glossar mit Begriffen zum maschinellen Lernen erstellt, die Sie in diesem Codelab finden.

Lerninhalte

  • Vortrainiertes Spracherkennungsmodell laden
  • So treffen Sie mit dem Mikrofon Echtzeitvorhersagen
  • Benutzerdefiniertes Audioerkennungsmodell mit dem Mikrofon des Browsers trainieren und verwenden

Fangen wir also an.

2. Voraussetzungen

Für dieses Codelab benötigen Sie Folgendes:

  1. Eine aktuelle Version von Chrome oder einem anderen aktuellen Browser
  2. Einen Texteditor, der entweder lokal auf Ihrem Computer oder im Web über Codepen oder Glitch ausgeführt wird
  3. Kenntnisse in HTML, CSS, JavaScript und Chrome-Entwicklertools bzw. den Entwicklertools Ihrer bevorzugten Browser
  4. Ein 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 Audiomodell laden

Öffnen Sie index.html in einem Editor und fügen Sie den folgenden 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>

Das erste <script>-Tag importiert die TensorFlow.js-Bibliothek und das zweite <script>-Tag importiert das vortrainierte Speech Commands-Modell. Das Tag <div id="console"> wird verwendet, um die Ausgabe des Modells anzuzeigen.

4. Vorhersagen in Echtzeit

Ö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

Vergewissern Sie sich, dass Ihr Gerät über ein Mikrofon verfügt. Beachten Sie, dass dies auch auf einem Mobiltelefon funktioniert. Ö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 eine Weile dauern. Bitte haben Sie etwas Geduld. Sobald das Modell geladen wurde, sollte oben auf der Seite ein Wort angezeigt werden. Das Modell wurde so trainiert, dass es die Zahlen von 0 bis 9 und einige zusätzliche Befehle wie „left“, „right“, „yes“, „no“ usw. erkennt.

Sprechen Sie eines dieser Wörter. Ist dein Wort richtig? Spielen Sie mit dem probabilityThreshold, das steuert, wie oft das Modell ausgelöst wird.0,75 bedeutet, dass das Modell ausgelöst wird, wenn es mit mehr als 75% Wahrscheinlichkeit ein bestimmtes Wort hört.

Weitere Informationen zum Speech Commands-Modell und zu seiner API finden Sie in der README.md auf GitHub.

6. Daten erheben

Damit es noch mehr Spaß macht, verwenden wir kurze Töne anstelle von ganzen Wörtern, um den Schieberegler zu steuern.

Sie trainieren ein Modell so, dass es drei verschiedene Befehle erkennt: „Links“ und „Rechts“. und „Rauschen“ sodass sich der Schieberegler nach links oder rechts bewegt. „Rauschen“ erkennen (keine Aktion erforderlich) ist für die Spracherkennung entscheidend, da der Schieberegler nur dann reagieren soll, wenn wir den richtigen Ton erzeugen, und nicht, wenn wir allgemein sprechen und uns bewegen.

  1. Zuerst müssen wir Daten sammeln. Fügen Sie der App eine einfache UI hinzu, indem Sie Folgendes in das <body>-Tag vor <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>
  1. Zu index.js hinzufügen:
// 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. Entfernen Sie predictWord() aus app():
async function app() {
 recognizer = speechCommands.create('BROWSER_FFT');
 await recognizer.ensureModelLoaded();
 // predictWord() no longer called.
}

Details zur Mutation

Sehen wir uns diesen Code einmal genauer an.

Wir haben unserer Benutzeroberfläche drei Schaltflächen mit den Bezeichnungen „Left“, „Right“ und „Noise“ 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, die Trainingsbeispiele für das Modell erstellt.

collect() verknüpft eine label mit der Ausgabe von recognizer.listen(). Da includeSpectrogram „wahr“ ist,, gibt recognizer.listen() das Rohspektrogramm (Frequenzdaten) für 1 Sekunde Audio, aufgeteilt in 43 Frames, an, sodass jeder Frame ca. 23 ms Audio enthält:

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

Da wir zum Steuern des Schiebereglers kurze Töne anstelle von Wörtern verwenden möchten, berücksichtigen wir nur die letzten 3 Frames (~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 eine Abweichung von 10:

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

Schließlich hat jedes Trainingsbeispiel zwei Felder:

  • label****: 0, 1 und 2 für „Links“ und „Rechts“ und „Rauschen“ .
  • vals****: 696 Zahlen mit Frequenzinformationen (Spektrogramm)

Alle Daten werden in der Variablen examples gespeichert:

examples.push({vals, label});

7. Datenerhebung testen

Öffnen Sie index.html in einem Browser. Sie sollten drei Schaltflächen sehen, die den drei Befehlen entsprechen. 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

Wenn Sie Beispiele für jeden Befehl erfassen möchten, erzeugen Sie wiederholt (oder fortlaufend) einen gleichmäßigen Ton, während Sie die jeweilige Taste 3–4 Sekunden lang gedrückt halten. Sie sollten ca. 150 Beispiele für jedes Label erfassen. Beispielsweise können wir für „Links“ die Finger schnippeln, für „Rechts“ pfeifen und abwechselnd zwischen „Lautlos“ und „Geräusche“ sprechen.

Je mehr Beispiele Sie erfassen, desto höher sollte sich der auf der Seite angezeigte Zähler erhöhen. Sie können die Daten auch überprüfen, indem Sie in der Console die Datei console.log() für die Variable examples aufrufen. An dieser Stelle besteht das Ziel darin, den Datenerfassungsprozess zu testen. Später sammeln Sie dann noch einmal Daten, wenn Sie die gesamte App testen.

8. Modell trainieren

  1. Fügen Sie einen Zug hinzu. direkt nach „Noise“ (Rauschen) im Text der Datei index.html::
<br/><br/>
<button id="train" onclick="train()">Train</button>
  1. Fügen Sie dem vorhandenen Code in index.js Folgendes hinzu:
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. Rufen Sie buildModel() auf, wenn die App geladen wird:
async function app() {
 recognizer = speechCommands.create('BROWSER_FFT');
 await recognizer.ensureModelLoaded();
 // Add this line.
 buildModel();
}

Wenn Sie die App aktualisieren, sehen Sie jetzt den neuen Namen Train (Trainieren) Schaltfläche. Sie können das Training testen, indem Sie Daten noch einmal erfassen und auf „Trainieren“ klicken, oder bis Schritt 10 warten, um das Training zusammen mit der Vorhersage zu testen.

Zusammenfassung

Grundsätzlich führen wir zwei Schritte aus: buildModel() definiert die Modellarchitektur und train() trainiert das Modell anhand der erfassten Daten.

Modellarchitektur

Das Modell hat vier Ebenen: eine Faltungsschicht, die die Audiodaten verarbeitet (dargestellt als Spektrogramm), eine maximale Poolebene, eine flache Ebene und eine dichte Ebene, die den drei Aktionen zugeordnet ist:

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], wobei jeder Frame 23 ms Audio mit 232 Zahlen umfasst, die verschiedenen Frequenzen entsprechen. 232 wurde ausgewählt, weil dies die Anzahl der Häufigkeits-Buckets ist, die zum Erfassen der menschlichen Stimme erforderlich sind. In diesem Codelab verwenden wir Samples, die 3 Frames lang sind (ca. 70 ms Sample), da wir zum Steuern des Schiebereglers Töne erzeugen, anstatt ganze Wörter auszusprechen.

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 das Adam-Optimierungstool, ein gängiges Optimierungstool für Deep Learning, und categoricalCrossEntropy für Verluste, die Standardverlustfunktion für die Klassifizierung. Kurz gesagt, sie misst, wie weit die vorhergesagten Wahrscheinlichkeiten (eine Wahrscheinlichkeit pro Klasse) von einer Wahrscheinlichkeit von 100% in der wahren Klasse und einer Wahrscheinlichkeit von 0% für alle anderen Klassen entfernt sind. Wir stellen auch accuracy als zu überwachenden Messwert bereit, der uns den Prozentsatz der Beispiele angibt, die das Modell nach jeder Trainingsphase korrekt erhält.

Training

Das Training durchläuft die Daten zehnmal (Epochen) mit einer Batchgröße von 16 (gleichzeitig werden 16 Beispiele verarbeitet) und zeigt die aktuelle Genauigkeit in der Benutzeroberfläche:

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

Nachdem wir unser Modell trainiert haben, fügen wir nun Code hinzu, um Vorhersagen in Echtzeit zu treffen, und bewegen den Schieberegler. Fügen Sie dies direkt nach Zug ein. Schaltfläche in index.html:

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

Zusammenfassung

Vorhersagen in Echtzeit

listen() hört das Mikrofon ab und macht Vorhersagen in Echtzeit. Der Code ist der Methode collect() sehr ähnlich, mit der das Rohspektrogramm normalisiert und alle bis auf die letzten NUM_FRAMES Frames gelöscht werden. 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 über die Anzahl der Klassen darstellt. Einfacher ausgedrückt ist dies nur ein Satz von Konfidenzwerten für jede der möglichen Ausgabeklassen, die in der Summe 1 ergeben. Tensor hat die äußere Dimension 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 eine „1“ als Achsenparameter verwenden, da wir den 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 bei dem Label 1 („Rechts“) und ignoriert ihn, wenn das Label 2 („Rauschen“) ist.

Tensoren entfernen

Zum Bereinigen des GPU-Arbeitsspeichers ist es wichtig, dass wir tf.dispose() manuell für Ausgabe-Tensoren aufrufen. Die Alternative zum manuellen tf.dispose() ist die Zusammenfassung von Funktionsaufrufen in eine tf.tidy(). Dies kann jedoch nicht mit asynchronen Funktionen verwendet werden.

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

10. Fertige App testen

Öffnen Sie die Datei index.html in Ihrem Browser und erfassen Sie wie im vorherigen Abschnitt Daten mit den drei Schaltflächen, die den drei Befehlen entsprechen. Denke daran, jede Taste 3 bis 4 Sekunden lang gedrückt zu halten, während Daten erfasst werden.

Wenn Sie die Beispiele erfasst haben, klicken Sie auf die Schaltfläche Trainieren. Dadurch wird mit dem Training des Modells begonnen und die Genauigkeit des Modells sollte über 90 % liegen. Wenn Sie keine gute Modellleistung erzielen, versuchen Sie, mehr Daten zu erheben.

Wenn das Training abgeschlossen ist, klicken Sie auf 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/.