TensorFlow.js – Reconnaissance audio à l'aide de l'apprentissage par transfert

1. Introduction

Dans cet atelier de programmation, vous allez créer un réseau de reconnaissance audio et l'utiliser pour contrôler un curseur dans le navigateur en émettant des sons. Vous allez utiliser TensorFlow.js, une bibliothèque de machine learning puissante et flexible pour JavaScript.

Tout d'abord, vous allez charger et exécuter un modèle pré-entraîné capable de reconnaître 20 commandes vocales. Ensuite, à l'aide de votre micro, vous créerez et entraînerez un réseau de neurones simple qui reconnaîtra les sons et fera tourner le curseur vers la gauche ou vers la droite.

Cet atelier de programmation ne détaille pas la théorie sous-jacente des modèles de reconnaissance audio. Si cela vous intéresse, consultez ce tutoriel.

Nous avons également créé un glossaire des termes liés au machine learning que vous trouverez dans cet atelier de programmation.

Points abordés

  • Charger un modèle de reconnaissance de commandes vocales pré-entraîné
  • Effectuer des prédictions en temps réel à l'aide du micro
  • Entraîner et utiliser un modèle de reconnaissance audio personnalisé à l'aide du micro du navigateur

Alors, c'est parti !

2. Conditions requises

Voici les conditions à remplir pour effectuer cet atelier de programmation:

  1. Une version récente de Chrome ou un autre navigateur récent.
  2. Un éditeur de texte s'exécutant localement sur votre ordinateur ou sur le Web via un outil tel que Codepen ou Glitch
  3. Connaissance des langages HTML, CSS et JavaScript, ainsi que des outils pour les développeurs Chrome (ou des outils de développement de votre navigateur préféré)
  4. Une compréhension conceptuelle de haut niveau des réseaux de neurones. Si vous avez besoin d'une introduction ou d'un rappel, regardez cette vidéo de 3blue1brown ou cette vidéo sur le deep learning en JavaScript d'Ashi Krishnan.

3. Charger TensorFlow.js et le modèle audio

Ouvrez index.html dans un éditeur et ajoutez le contenu suivant:

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

Le premier tag <script> importe la bibliothèque TensorFlow.js, et le second <script> importe le modèle de commandes vocales pré-entraîné. Le tag <div id="console"> permet d'afficher la sortie du modèle.

4. Prédire en temps réel

Ensuite, ouvrez/créez le fichier index.js dans un éditeur de code et incluez le code suivant:

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. Tester la prédiction

Assurez-vous que votre appareil est équipé d'un micro. Notez que cette fonctionnalité fonctionne également sur les téléphones mobiles. Pour exécuter la page Web, ouvrez le fichier index.html dans un navigateur. Si vous travaillez à partir d'un fichier local, vous devrez démarrer un serveur Web et utiliser http://localhost:port/ pour accéder au micro.

Pour démarrer un serveur Web simple sur le port 8000:

python -m SimpleHTTPServer

Le téléchargement du modèle peut prendre un certain temps. Veuillez donc patienter. Dès que le modèle est chargé, vous devez voir un mot en haut de la page. Le modèle a été entraîné à reconnaître les nombres de 0 à 9 et quelques commandes supplémentaires telles que "left", "right", "yes", "no", etc.

Prononcez l'un de ces mots. Comprend-il vos mots correctement ? Jouez avec probabilityThreshold, qui contrôle la fréquence de déclenchement du modèle.0,75 signifie que le modèle se déclenchera lorsqu'il a plus de 75% de certitude qu'il entend un mot donné.

Pour en savoir plus sur le modèle Speech Commands et son API, consultez le fichier README.md sur GitHub.

6. Collecter des données

Pour le rendre amusant, utilisez des sons courts plutôt que des mots entiers pour contrôler le curseur.

Vous allez entraîner un modèle à reconnaître trois commandes différentes : "Gauche" et "Droite". et "Bruit" ce qui fera déplacer le curseur vers la gauche ou vers la droite. Détecter du "bruit" (aucune action requise) est essentielle pour la détection vocale, car nous voulons que le curseur réagisse uniquement lorsque nous produisons le bon son, et non lorsque nous parlons et nous bougeons en général.

  1. Nous devons d'abord collecter des données. Ajoutez une UI simple à l'application en ajoutant ceci dans la balise <body> avant <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. Ajoutez ceci à 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. Supprimez predictWord() de app().
async function app() {
 recognizer = speechCommands.create('BROWSER_FFT');
 await recognizer.ensureModelLoaded();
 // predictWord() no longer called.
}

Répartir

Ce code peut être difficile au premier abord, alors analysons-le.

Nous avons ajouté à l'interface utilisateur trois boutons intitulés "Left", "Right" et "Noise", correspondant aux trois commandes que le modèle doit reconnaître. En appuyant sur ces boutons, vous appelez la nouvelle fonction collect(), qui crée des exemples d'entraînement pour notre modèle.

collect() associe un label à la sortie de recognizer.listen(). Comme includeSpectrogram est vrai,, recognizer.listen() donne le spectrogramme brut (données de fréquence) pour une seconde de son, divisé en 43 trames, de sorte que chaque trame représente environ 23 ms de son:

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

Étant donné que nous souhaitons utiliser des sons courts plutôt que des mots pour contrôler le curseur, nous ne prenons en compte que les trois dernières images (environ 70 ms):

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

Et pour éviter les problèmes numériques, nous normalisons les données pour obtenir une moyenne de 0 et un écart type de 1. Dans ce cas, les valeurs du spectrogramme sont généralement de grands nombres négatifs proches de -100 et un écart de 10:

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

Enfin, chaque exemple d'entraînement comporte deux champs:

  • label**** : 0, 1 et 2 pour "Gauche", "Droite" et "Bruit" respectivement.
  • vals**** : 696 nombres contenant les informations de fréquence (spectrogramme)

Toutes les données sont stockées dans la variable examples:

examples.push({vals, label});

7. Tester la collecte de données

Ouvrez le fichier index.html dans un navigateur. Vous devriez voir trois boutons correspondant aux trois commandes. Si vous travaillez à partir d'un fichier local, vous devez démarrer un serveur Web et utiliser http://localhost:port/ pour accéder au micro.

Pour démarrer un serveur Web simple sur le port 8000:

python -m SimpleHTTPServer

Pour recueillir des exemples pour chaque commande, émettez un son constant de façon répétée (ou continue) tout en appuyant sur chaque bouton pendant 3 à 4 secondes. Vous devez collecter environ 150 exemples pour chaque étiquette. Par exemple, nous pouvons claquer des doigts pour "Gauche", siffler pour "Droite", et alterner entre le silence et la voix pour "Bruit".

Plus vous collectez d'exemples, plus le compteur affiché sur la page augmente. N'hésitez pas à inspecter également les données en appelant console.log() sur la variable examples dans la console. À ce stade, l'objectif est de tester le processus de collecte des données. Plus tard, vous collecterez à nouveau des données lorsque vous testerez l'ensemble de l'application.

8. Entraîner un modèle

  1. Ajoutez un train. juste après la notification Noise dans le corps du fichier index.html: :
<br/><br/>
<button id="train" onclick="train()">Train</button>
  1. Ajoutez ce qui suit au code existant dans le fichier 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. Appelez buildModel() lorsque l'application se charge:
async function app() {
 recognizer = speechCommands.create('BROWSER_FFT');
 await recognizer.ensureModelLoaded();
 // Add this line.
 buildModel();
}

À ce stade, si vous actualisez l'application, une nouvelle fenêtre Train (Entraînement) s'affiche. . Vous pouvez tester l'entraînement en collectant à nouveau les données et en cliquant sur "Entraîner", ou attendre l'étape 10 pour tester l'entraînement et la prédiction.

En détail

De manière générale, nous faisons deux choses: buildModel() définit l'architecture du modèle et train() entraîne le modèle à l'aide des données collectées.

Architecture du modèle

Le modèle comporte quatre couches: une couche convolutive qui traite les données audio (représentées sous forme de spectrogramme), une couche de pool maximal, une couche aplatie et une couche dense qui correspond aux trois actions:

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 forme d'entrée du modèle est [NUM_FRAMES, 232, 1], où chaque trame correspond à 23 ms d'audio contenant 232 nombres correspondant à différentes fréquences (232 a été choisi, car il s'agit du nombre de buckets de fréquences nécessaires pour capturer la voix humaine). Dans cet atelier de programmation, nous utilisons des échantillons de trois frames (échantillons d'environ 70 ms), car nous prononçons des sons au lieu de prononcer des mots entiers pour contrôler le curseur.

Nous compilons notre modèle pour le préparer pour l'entraînement:

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

Nous utilisons l'optimiseur Adam, un optimiseur courant utilisé en deep learning, et categoricalCrossEntropy pour la perte, la fonction de perte standard utilisée pour la classification. En bref, elle mesure dans quelle mesure les probabilités prédites (une probabilité par classe) se situent entre une probabilité de 100% dans la classe réelle et une probabilité de 0% pour toutes les autres classes. Nous fournissons également accuracy comme métrique à surveiller, ce qui nous donne le pourcentage d'exemples que le modèle est correct après chaque époque d'entraînement.

Entraînement

L'entraînement passe 10 fois (époques) sur les données en utilisant une taille de lot de 16 (traitement de 16 exemples à la fois) et affiche la justesse actuelle dans l'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. Mettre à jour le curseur en temps réel

Maintenant que nous pouvons entraîner notre modèle, ajoutons du code pour effectuer des prédictions en temps réel et déplacez le curseur. Ajoutez cet élément juste après la ligne Train (Train). dans le fichier index.html:

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

Et ce qui suit dans 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
 });
}

En détail

Prédiction en temps réel

listen() écoute le micro et effectue des prédictions en temps réel. Le code est très semblable à la méthode collect(), qui normalise le spectrogramme brut et supprime toutes les images, à l'exception des dernières NUM_FRAMES. La seule différence est que nous appelons également le modèle entraîné pour obtenir une prédiction:

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

La sortie de model.predict(input) est un Tensor de forme [1, numClasses] représentant une distribution de probabilité sur le nombre de classes. Pour faire simple, il s'agit simplement d'un ensemble de indices de confiance pour chacune des classes de sortie possibles dont la somme est égale à 1. La dimension extérieure du Tensor est de 1, car il s'agit de la taille du lot (un seul exemple).

Pour convertir la distribution de probabilité en un entier représentant la classe la plus probable, nous appelons probs.argMax(1), qui renvoie l'index de classe avec la probabilité la plus élevée. Nous transmettons un "1" comme paramètre d'axe, car nous voulons calculer la argMax sur la dernière dimension, numClasses.

Modifier le curseur

moveSlider() diminue la valeur du curseur si le libellé est 0 ("Gauche") , l'augmente si le libellé est 1 ("Droite") et ignore si le libellé est 2 ("Bruit").

Supprimer des Tensors

Pour nettoyer la mémoire GPU, il est important d'appeler manuellement tf.dispose() sur les Tensors de sortie. L'alternative au tf.dispose() manuel consiste à encapsuler les appels de fonction dans un tf.tidy(), mais cela ne peut pas être utilisé avec des fonctions asynchrones.

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

10. Tester l'application finale

Ouvrez le fichier index.html dans votre navigateur et collectez les données comme vous l'avez fait dans la section précédente, à l'aide des trois boutons correspondant aux trois commandes. N'oubliez pas d'appuyer sur chaque bouton et de le maintenir enfoncé pendant trois à quatre secondes lorsque vous collectez des données.

Une fois que vous avez collecté des exemples, appuyez sur le bouton Entraîner. L'entraînement du modèle commence, et vous devriez constater que la justesse de ce dernier dépasse 90%. Si les performances du modèle ne sont pas satisfaisantes, essayez de collecter plus de données.

Une fois l'entraînement terminé, appuyez sur le bouton Listen (Écouter) pour faire des prédictions à partir du micro et contrôlez le curseur.

Découvrez d'autres tutoriels sur http://js.tensorflow.org/.