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.
Pour commencer, 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 allez créer et entraîner un réseau de neurones simple qui reconnaît vos sons et déplace le curseur vers la gauche ou vers la droite.
Nous n'aborderons pas les bases théoriques sur lesquelles reposent les modèles de reconnaissance audio. Si vous souhaitez en savoir plus à ce sujet, consultez ce tutoriel.
Nous avons également créé un glossaire des termes de 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
Pour suivre cet atelier de programmation, vous aurez besoin :
- Une version récente de Chrome ou d'un autre navigateur moderne.
- Un éditeur de texte s'exécutant localement sur votre ordinateur ou sur le Web via Codepen ou Glitch, par exemple
- Bonne 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é)
- Bonne compréhension du concept de réseau de neurones Si vous avez besoin d'une présentation ou d'un rappel, vous pouvez regarder 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 tag <script> importe le modèle Speech Commands pré-entraîné. La balise <div id="console"> sera utilisée pour afficher le résultat 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. Sachez que cela fonctionne également sur les téléphones mobiles. Pour exécuter la page Web, ouvrez 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 patienter. Dès que le modèle se charge, un mot doit s'afficher en haut de la page. Le modèle a été entraîné à reconnaître les chiffres de 0 à 9 et quelques commandes supplémentaires telles que "gauche", "droite", "oui", "non", etc.
Prononcez l'un de ces mots. Le mot est-il correctement retranscrit ? Jouez avec le probabilityThreshold, qui contrôle la fréquence à laquelle le modèle se déclenche.Par exemple, une valeur de 0,75 signifie que le modèle se déclenchera lorsqu'il sera sûr à plus de 75 % d'entendre 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 rendre l'expérience amusante, utilisons des sons courts au lieu de mots entiers pour contrôler le curseur.
Vous allez entraîner un modèle à reconnaître trois commandes différentes : "Gauche", "Droite" et "Bruit", qui feront bouger le curseur vers la gauche ou la droite. Il est essentiel de reconnaître le "bruit" (aucune action n'est requise) dans la détection vocale, car nous voulons que le curseur ne réagisse que lorsque nous produisons le bon son, et non lorsque nous parlons et bougeons en général.
- Nous devons d'abord collecter des données. Ajoutez une interface utilisateur simple à l'application en ajoutant ce qui suit 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>
- 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);
}
- Supprimez
predictWord()deapp().
async function app() {
recognizer = speechCommands.create('BROWSER_FFT');
await recognizer.ensureModelLoaded();
// predictWord() no longer called.
}
Répartir
Ce code peut sembler un peu fastidieux au premier abord. Examinons-le de plus près.
Nous avons ajouté trois boutons à notre UI, intitulés "Left" (Gauche), "Right" (Droite) et "Noise" (Bruit), qui correspondent aux trois commandes que nous voulons que notre modèle reconnaisse. Lorsque vous appuyez sur ces boutons, la fonction collect() que nous venons d'ajouter est appelée, ce qui crée des exemples d'entraînement pour notre modèle.
collect() associe un label à la sortie de recognizer.listen(). Étant donné que includeSpectrogram est défini sur "true",, recognizer.listen() fournit le spectrogramme brut (données de fréquence) pour une seconde d'audio, divisée en 43 frames. Chaque frame correspond donc à environ 23 ms d'audio :
recognizer.listen(async ({spectrogram: {frameSize, data}}) => {
...
}, {includeSpectrogram: true});
Comme nous voulons utiliser des sons courts au lieu de mots pour contrôler le curseur, nous ne prenons en compte que les trois dernières images (~70 ms) :
let vals = normalize(data.subarray(-frameSize * NUM_FRAMES));
Pour éviter les problèmes numériques, nous normalisons les données afin qu'elles aient 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 autour de -100, avec une déviation 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)
Nous stockons toutes les données dans la variable examples :
examples.push({vals, label});
7. Collecte de données de test
Ouvrez index.html dans un navigateur. Vous devriez voir trois boutons correspondant aux trois commandes. 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
Pour collecter des exemples pour chaque commande, émettez un son régulier de manière répétée (ou continue) tout en appuyant de manière prolongée sur chaque bouton pendant trois à quatre secondes. Vous devez collecter environ 150 exemples pour chaque libellé. Par exemple, nous pouvons faire claquer des doigts pour "Gauche", siffler pour "Droite" et alterner entre silence et parole pour "Bruit".
Au fur et à mesure que vous collectez des exemples, le compteur affiché sur la page devrait augmenter. N'hésitez pas à inspecter 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 de données. Vous collecterez à nouveau des données lorsque vous testerez l'ensemble de l'application.
8. Entraîner un modèle
- Ajoutez un bouton Train juste après le bouton Noise dans le corps de index.html :
<br/><br/>
<button id="train" onclick="train()">Train</button>
- Ajoutez le code suivant au code existant dans 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;
}
- 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, vous verrez un nouveau bouton Train (Entraîner). Vous pouvez tester l'entraînement en collectant à nouveau des données et en cliquant sur "Entraîner", ou vous pouvez attendre l'étape 10 pour tester l'entraînement en même temps que la prédiction.
En détail
De manière générale, nous effectuons deux opérations : 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 de convolution qui traite les données audio (représentées sous forme de spectrogramme), une couche de mise en commun maximale, une couche d'aplatissement 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 frame 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équence nécessaires pour capturer la voix humaine). Dans cet atelier de programmation, nous utilisons des échantillons de trois images (~70 ms) puisque nous produisons des sons au lieu de prononcer des mots entiers pour contrôler le curseur.
Nous compilons notre modèle pour le préparer à 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é dans l'apprentissage profond, et categoricalCrossEntropy pour la perte, la fonction de perte standard utilisée pour la classification. En bref, elle mesure l'écart entre les probabilités prédites (une probabilité par classe) et la probabilité de 100 % dans la classe réelle, et la probabilité de 0 % pour toutes les autres classes. Nous fournissons également accuracy comme métrique à surveiller, ce qui nous donnera le pourcentage d'exemples que le modèle obtient correctement après chaque époque d'entraînement.
Formation
L'entraînement s'effectue 10 fois (époques) sur les données à l'aide d'une taille de lot de 16 (traitement de 16 exemples à la fois) et affiche la précision 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éplacer le curseur. Ajoutez ce code juste après le bouton Train (Entraîner) dans index.html :
<br/><br/>
<button id="listen" onclick="listen()">Listen</button>
<input type="range" id="output" min="0" max="10" step="0.1">
Et le code suivant 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 génère des prédictions en temps réel. Le code est très semblable à la méthode collect(), qui normalise le spectrogramme brut et supprime tous les frames, sauf les NUM_FRAMES derniers. 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és sur le nombre de classes. Plus simplement, il s'agit d'un ensemble de degrés de confiance pour chacune des classes de sortie possibles, dont la somme est égale à 1. Le Tensor a une dimension externe de 1, car il s'agit de la taille du lot (un seul exemple).
Pour convertir la distribution de probabilité en un seul 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 "1" en tant que paramètre d'axe, car nous voulons calculer argMax sur la dernière dimension, numClasses.
Mettre à jour le curseur
moveSlider() diminue la valeur du curseur si le libellé est 0 ("Gauche") , l'augmente si le libellé est 1 ("Droite") et l'ignore si le libellé est 2 ("Bruit").
Supprimer des Tensors
Pour nettoyer la mémoire du GPU, il est important d'appeler manuellement tf.dispose() sur les Tensors de sortie. L'alternative à tf.dispose() manuel consiste à encapsuler les appels de fonction dans un tf.tidy(), mais cela ne peut pas être utilisé avec les fonctions asynchrones.
tf.dispose([input, probs, predLabel]);
10. Tester l'application finale
Ouvrez index.html dans votre navigateur et collectez des données comme dans la section précédente avec les trois boutons correspondant aux trois commandes. N'oubliez pas d'appuyer de manière prolongée sur chaque bouton 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. Vous devriez constater que sa précision dépasse les 90 %. Si vous n'obtenez pas de bonnes performances de modèle, essayez de collecter davantage de données.
Une fois l'entraînement terminé, appuyez sur le bouton Écouter pour faire des prédictions à partir du micro et contrôler le curseur.
Pour en savoir plus, consultez http://js.tensorflow.org/.