ESP32, intelligence artificielle... et ronflements

Oui, le titre est un peu abscons, je vous laisse découvrir...

Ma femme me dit "tu ronfles!", mais moi je dors, donc je ne peux pas savoir. D'où mon problème : comment savoir si je ronfle et à quels moments de la nuit ?

Je me suis dit que mon ami Arduino pouvait m'aider à trouver une réponse et je me suis lancé dans un projet de : DÉTECTEUR DE RONFLEMENT.

La première idée qui vient est d'analyser le son produit par un ronflement, de trouver des patterns fréquentiels caractéristiques. Une fois identifiés, il suffit de les détecter en phase opérationnelle et de lever une alarme.

D'où mon projet précédent d’analyseur de son. Sauf que... c'est pas si simple. Sur les suggestions d'Al1fch, j'ai installé Audacity et appris à m'en servir pour enregistrer les bruits nocturnes. Un tracé de spectrogramme (le spectre sonore en fonction du temps) montre qu'il est possible en effet de trouver des fréquences caractéristiques de phases de ronflement. Pour moi, elles sont autour de 5 à 8 kHz :

Vous voyez les petites traces bleues - violettes en haut du diagramme, vers 6:21:18 ou 6:21:24 ? C'est ça, un ronflement... Pas très fort, certes, mais suffisamment pour déranger Madame...

J'ai commencé par écrire un code pour chercher ces patterns et envoyer une information de détection sur Thingspeak, mais le résultat n'était pas à la hauteur des attentes : fausses alarmes, mauvaises détections. Bref plus aléatoire que convaincant... De plus, rien ne prouve que mes patterns seront les mêmes pour un autre ronfleur.

Alors je me suis dit : passons à autre chose. Le buzz-word du moment : l'intelligence artificielle ! C'est exactement l'outil idéal pour ce genre de besoin. Un programme apprend à reconnaître un pattern, en fait mieux que ça : une famille de patterns similaires. Une fois l'apprentissage fait, un second programme, bien plus simple, cherche les choses apprises parmi les données qu'il reçoit.

Bingo !

Petit tour d'horizon de l'IA : le principal framework (outil, in french) de développement de logiciels d'IA est basé sur la bibliothèque TensorFlow, avec la couche simplificatrice Keras par dessus. On trouve des centaines de tutos sur Internet pour en savoir plus et apprendre à s'en servir. De plus, je suis amené à l'utiliser au boulot, donc ça baigne.

Mais sur Arduino (et ses amis) : le constat est rapide. RIEN (ou presque). Quelques développements rapides sur ESP32 Cam pour une reconnaissance de visage, mais c'est à peu près tout.

Pour l'analyseur de son je n'avais pas trouvé de bibliothèque FFT qui me plaisait, j'ai donc décidé d'en porter une, écrite en C, que j'ai trouvée sur Internet. Et bien, autant faire la même chose pour l'IA !
J'ai donc cherché des bibliothèques d'IA en C, avec pas ou peu de dépendance, et si possible pas trop lourdes. En voici quelques unes :

Tinn (Tiny Neural Network) is a 200 line dependency free neural network library written in C99.

Je l'ai choisie car elle est légère, sans dépendance, le code est simple à comprendre, donc à adapter et le portage a été rapide. Juste une ou deux fonctions à adapter (générateur de nombres aléatoires - ça c'est vrai pour toutes les bibli, lecture et écriture de fichiers).

Comme j'étais déjà sur ESP32 pour l'analyseur de sons, et qu'une partie de ce code serait utilisée, autant rester sur ce SoC pour le reste. De plus, l'ESP32 dispose d'un espace fichiers (SPIFFS) et de pas mal de mémoire, dont les logiciels d'IA sont assez gourmands. Je pense que le porter sur un Arduino aurait été peu efficace.

Le décor étant planté, passons à la programmation.

D'abord quelques mots sur la structure d'une IA. Le modèle le plus utilisé (et celui des bibliothèques dont je parle plus haut) et le plus connu, c'est le perceptron. c'est une structure inspirée de celle du cerveau : des neurones organisés en couches sont reliés, et traitent les données, se transmttent leurs résultats jusqu'à la couche finale qui fournit la sortie du réseau. Si ça fonctionne bien, cette sortie correspond à un label, parmi ceux qui sont connus. Un petit schéma :

Les bibliothèques indiquées plus haut mettent presque toutes en oeuvre un perceptron simple, avec une seule couche cachée. On parle de couche cachée, car seules les couches d'entrée et de sortie sont "visibles" pour l'utilisateur : le première reçoit les données, la dernière fournit le résultat.

Comme je l'ai dit plus haut, une IA fonctionne en deux phases :

  • Apprentissage : optimisation d'une fonction pour interpoler les fameux patterns parmi les données fournies (aussi appelées 'dataset'). Ces données sont souvent affectées d'un label, qui permet des les classifier (voiture, chat, ronflement, ou pas)
  • Inférence : recherche de ces patterns parmi les données reçues (en temps réel par exemple dans le cas d'acquisition de sons), en utilisant la fonction construite en phase d'apprentissage. La valeur de sortie de cette fonction appliquée aux données d'entrée correspond à ce qu'on demande à l'IA : une décision.

Ce que je viens de décrire, avec mes mots (on trouve de bien meilleures descriptions sur internet) c'est un apprentissage supervisé. Les données initiales (dataset) sont accompagnées des labels qui indiquent à l'IA comment les classifier.

En appliquant ça à mon cas, les données seront une base de sons et les labels diront s'il s'agit d'un ronflement ou pas. Là, c'est simple le label c'est 1 ou 0, un booléen. Ronfle ou ronfle pas.

Par contre, quelles données sonores sont pertinentes ? A priori, on pense au spectre sonore, c'est pourquoi j'ai mon analyseur. Mes premiers tests ont montré que ce n'est pas suffisant, il y a beaucoup de fausses détections (mais ce sont des premiers tests, donc mon logiciel ou ma façon de l’utiliser n'était peut-être pas bonne). J'ai donc ajouté une donnée supplémentaire : l'amplitude du son.

Je vous passe les détails, mais cette petite modification a fait fonctionner le détecteur ! :smiley:

Revenons un peu en arrière : le perceptron a besoin de données pour fonctionner, pour apprendre. J'ai donc écrit un premier code pour acquérir ces données et créer une base d'apprentissage.

CODE NUMÉRO 1 : Acquisition_ESP32
Les fichiers sont fournis plus bas :

  • Acquisition_ESP32.ino
  • functions.h
  • params.h

Je préviens : comme d'habitude, c'est programmé comme un cochon.

Pour le faire fonctionner, il faut :

  • un ESP32
  • un écran OLED 128x64
  • un bouton poussoir
  • un microphone, j'utilise un max4466


Que fait ce premier code ? Il acquiert les sons, calcule leur spectre fréquentiel, et stocke ce spectre avec un label (1 ou 0 si ronflement ou pas) dans un fichier dans le SPIFFS. Plus précisément (les instructions sont indiquées sur l'écran OLED) :

  • Un petit écran d'accueil
  • Si un fichier est déjà présent : appuyez sur le bouton pour l'effacer. Sinon, les données seront ajoutées en fin du fichier existant.
  • Après on passe à des phases d'enregistrement, alternant ronflement et silence. Appuyez sur le bouton pour lancer une phase.
  • D'abord : ronflement. Lorsque l'écran le demande, faites un bruit de ronflement devant le micro, si possible pendant 3 secondes (la LED bleue s'allume). Pendant cette durée, l'ESP32 acquiert, traite et stocke 10 ensembles de données labellisés (label 1).
  • Ensuite : silence (plutôt bruit de fond). Rien à faire, juste aucun son à faire pendant les 3 secondes suivantes, pendant lesquelles l'ESP32 crée 10 jeux de données sans ronflement (label 0).
  • Et on reboucle sur une nouvelle phase d'acquisition. Pour arrêter, il suffit de débrancher l'ESP32.

L'écran indique à chaque phase d'enregistrement le nombre de phases passées (en bas à droite). Pas la peine d'en faire plus de 25 à 30, car le programme d'IA ne pourra pas en traiter plus (limitation mémoire).

Si vous voulez voir le contenu du fichier de dataset, j'ai écrit un petit programme qui affiche sur la console Arduino le contenu des fichiers présents dans le SPIFFS : Dump_SPIFFS

Le programme d'acquisition calcule la FFT du son enregistré sur 16 bandes de fréquences. Ce paramètre peut être changé (fichier params.h), mais inutile d'aller à plus de 32 pour la même raison que précédemment (mémoire).

Acquisition_ESP32.ino (2.09 KB)

functions.h (9.99 KB)

params.h (602 Bytes)

Dump_SPIFFS.ino (1.3 KB)

CODE NUMÉRO 2 : Learning_ESP32
Maintenant, le perceptron proprement dit. Le code est bien plus long car il comporte les deux bibliothèques que j'ai portées : FFT et TINN. Il y a un boulot à faire pour en faire de vraies bibliothèques Arduino et simplifier leur utilisation. Mais je n'ai jamais fait, je n'en ai pas l'expérience... Donc c'est un gros code, réparti sur 6 fichiers.

Que fait le code ?

  • D'abord un petit écran d'accueil.
  • Ensuite, si un fichier de réseau de neurones est présent, il propose de la charger. Ça évite de refaire l'apprentissage, qui comporte des éléments aléatoires : refaire un nouvel apprentissage, c'est refaire un nouveau réseau, qui peut avoir des performances inférieures à celui que vous avez déjà fait. Appuyez sur le bouton pour charger le réseau existant.
  • Sinon, on passe à la phase d'apprentissage : je détaillerai ça plus loin. Le code boucle sur la routine d'entraînement du réseau, jusqu'à obtenir des performances inférieures à un seuil ou arriver au nombre maximum d'itérations.
  • On sauve le réseau dans un fichier dans le SPIFFS
  • Puis on affiche les performances (taux d'erreur obtenu sur la base d'apprentissage)
  • Et on passe à la phase d'écoute et de détection : les sons acquis par le micro sont passés dans le réseau et s'il détecte un ronflement, la LED bleue s'allume et l'écran l'affiche.

J'ajouterai plus tard un envoi de détection sur Thingspeak pour logger les résultats.

Quelques détails :

Les paramètres du code (fichier params.h) : j'espère que c'est parlant

  • // FFT parameters
  • #define SAMPLES 256
  • #define MAX_FREQ 20 // kHz
  • // Attenuation and threshold for display
  • #define COEF 30
  • #define ATT 500.0f
  • // Perceptron parameters
  • #define RATIO 0.8f // ratio of training data vs. testing
  • #define EPOCHS 6000 // number of training epochs
  • #define NHID 40 // number of hidden layers
  • #define BATCH 40 // number of data used for training in each epoch
  • #define LR 1.0f // initial learning rate
  • #define ANNEAL 0.9999f // rate of change of learning rate
  • #define MAXERR 0.001f // stop training if error is less than this
  • #define DETECT 0.9f // detection threshold

La FFT est faite sur 256 valeurs de son (SAMPLES), mesurées au rythme de 20000 par seconde (MAX_FREQ). La fréquence maximale exploitable du calcul est donc de 10kHz.
COEF sert pour l'affichage du spectre sur l'écran pendant l'acquisition en phase d'écoute.
ATT est un coefficient diviseur pour fournir au réseau les données issues du dataset. Les données brutes sont les valeurs moyennes du spectre sonore sur les bandes de fréquences de la mesure, ce sont des entiers. Mais j'ai remarqué que l'apprentissage fonctionne mal s'il est fait sur ces données brutes, pouvant varier de 0 à plus de 500. Donc, je les divise par ATT pour les ramener dans un intervalle qui semble mieux convenir au programme.

Les paramètres du perceptron sont explicites, j'espère. Nombre maximal d'itération d'apprentissage (EPOCHS) et seuil d'arrêt (MAXERR), nombre de neurones de la couche cachée (NHID), taux d'apprentissage initial (LR) et sa variation à chaque itération (LR devient LR * ANNEAL)

Mélange des données
Le dataset contient des groupes de 20 jeux de données : 10 de ronflements, 10 de silence. Ces données sont mélangées après avoir été lues afin de ne pas biaiser l'apprentissage par cette régularité.

La séparation du dataset en données d'entraînement et de test
Le fonctionnement habituel d'un apprentissage est de séparer le dataset en deux parties :
Un jeu de données pour l'apprentissage pur
Un jeu de données de test : elles servent à vérifier que la phase d'apprentissage ne s'est pas spécialisée que sur les données d'apprentissage, mais qu'il est capable de généraliser à des données inconnues. Le paramètre RATIO permet de faire cette séparation (après le mélange) : 0.8 veut dire que 80% des données sont pour l'apprentissage et 20% pour le test.
Ceci n'était pas prévu dans le code TINN d'origine, j'ai dû l'implémenter.

Les batchs
Il n'est pas efficace de faire l'apprentissage sur l'ensemble des données à chaque itération, car cela a aussi tendance à sur-spécialiser le réseau. On divise alors les données en "batches", un sous ensemble du jeu de données, différent à chaque étape, qui sert à chaque itération de l'apprentissage. Comme ça, chaque itération est faite sur des données différentes et on limite le phénomène d'overfitting. Le paramètre BATCH contient le nombre de données du batch. Ces données sont tirées au hasard dans le jeu d'apprentissage à chaque itération.
Ceci n'était pas prévu dans le code TINN d'origine, j'ai dû l'implémenter.

Learning_ESP32.ino (3.85 KB)

init.h (4.44 KB)

params.h (1.04 KB)

sound_functions.h (8.07 KB)

Tinn.h (6.54 KB)

train_test.h (6.1 KB)

Je vous entend déjà : "et alors, ça marche ou pas ?"

Eh bien, j'ai été assez heureusement surpris du résultat. Ça marche plutôt bien et l'apprentissage est très rapide.

Voici les résultats de l'apprentissage, tels qu'affichés par la console (le code ayant une part d'aléatoire, deux exécutions donneront des résultats différents, notamment au niveau du temps d'apprentissage) :

Found one network file : press button to load
OK, let us make a new network
Reading dataset: Done
Read 500 samples of 16 frequency bands

Training... on 40 data
Epoch 0 Error 13.234% Learning rate 1.000
Epoch 300 Error 1.781% Learning rate 0.970
Epoch 600 Error 0.615% Learning rate 0.942
Epoch 900 Error 3.799% Learning rate 0.914
Epoch 1200 Error 1.919% Learning rate 0.887
Epoch 1500 Error 0.108% Learning rate 0.861
Epoch 1800 Error 0.030% Learning rate 0.835
Epoch 2100 Error 0.647% Learning rate 0.811
Epoch 2400 Error 2.602% Learning rate 0.787
Epoch 2700 Error 0.275% Learning rate 0.763
Epoch 3000 Error 0.085% Learning rate 0.741
Epoch 3023 Error 0.000% Learning rate 0.739
Training done in 28828 ms
Saving network on SPIFFS

Testing on unknown data...
54 Expected : 0 Prediction : 0.338 NOK
60 Expected : 0 Prediction : 0.221 NOK
2 errors over 100 samples : error rate 2.00%

Testing over the training set :
19 errors on 400 samples : error rate 4.75%

Testing over the entire dataset :
21 errors on 500 samples : error rate 4.20%

Le dataset contient 500 jeux de données (25 mesures de 20 échantillons)
Le réseau est créé et entraîné sur des batches de 40 données. Le taux d'erreur diminue et varie autour de 1 à 2% (données fournies par TINN) et le taux d'apprentissage diminue au fur et à mesure des itérations.
L'apprentissage stoppe au bout de 3023 étapes, lorsque le taux d'erreur passe sous le seuil (MAXERR 0.001f ) pour une durée d'apprentissage de 28 secondes.

Le code affiche les résultats d'apprentissage sur le dataset:

  • 2 erreurs sur le jeu de test de 100 données
  • 19 erreurs sur le jeu de 400 données d'apprentissage
  • Total : 21 erreurs sur 500 (4.2%)

Puis on passe en phase d'écoute. Le micro est près de mon clavier : pendant que je tape ce texte, le code ne détecte rien, pas de fausse alarme. Si je parle à proximité du micro : pas de fausse alarme. Si je siffle pareil, sauf si je siffle un peu fort : là ça me fait une détection.

Enfin, si j'imite un bruit de ronflement ou de respiration forte, il détecte un ronflement et me fait une alarme :

Score : 1.00 DETECTION
Score : 0.99 DETECTION
Score : 1.00 DETECTION
Score : 0.91 DETECTION
Score : 1.00 DETECTION
Score : 1.00 DETECTION

Bien sûr, c'est perfectible : on peut jouer avec les paramètres, la taille du dataset, le nombre de fréquences (en refaisant le dataset), etc. C'est juste un démonstrateur, qui j'espère vous donnera envie de tester vous même.

Cet ensemble de code doit aussi être adaptable à la reconnaissance de quelques mots clés: OUI, NON par exemple. A tester...

Enfin, il faudrait mettre toutes ces fonctions sous forme d'une bibliothèque et la développer en ajoutant des "fonctions d'activation". TINN utilise seulement la sigmoïde, mais d'autres plus récentes sont aussi efficaces (RELU par exemple : Rectified linear unit)

Ma femme me dit "tu ronfles!", mais moi je dors, donc je ne peux pas savoir. D'où mon problème : comment savoir si je ronfle et à quels moments de la nuit ?

Si tu veux que ta mesure soit valable il te faut un microphone étalon, car comment prouver sinon que tu ne ronfles pas ?
Avec un certificat de mesure du microphone, la contestation ne sera pas possible :smiling_imp:

Sans me vanter, l'étalon c'est moi... 8)
Après, le cas d'application est plus un prétexte pour m'amuser sur le sujet de l'IA.

lesept stallone :wink:

lesept ouvre la voie !!

MERCI pour cet éclairage !!

Je pensais que, pour nos ESP32 "la" porte d'entrée était TensorFlow Lite sur ESP32, ta solution te permet sans doute de coller de manière plus efficace à ton besoin

Bonjour

Je me permets une suggestion :

  • realiser l'acquisition des données sur la base "arduino"
  • remonter les données d'apprentissage dans un pc
  • utiliser tensorflow pour réaliser l'apprentissage
  • recuperer les poids du réseau obtenu apres optimisation
  • reimporter ces poids dans arduino sous forme d'une fct qui effectue "juste" les produits nécessaires a l'execution feedforward du réseau

Ceci permets de bénéficier de la puissance d'optimisation de tensorflow
C'est juste un principe et la réalisation pratique n'est pas forcément si simple. Sauf a développer des passerelles, le principe reste assez lourd aussi
Peu expérimenté sur arduino (et sur tensorflow aussi d'ailleurs ;o) je pense que ma proposition ne vaut que pour des réseaux de neurones assez limités

Salutations

Bonjour,

en tout cas belle démonstration et donnant plein d'idées d'utilisation (hors ronflement). J'ai une question, bon, ton dispositif fonctionne et détecte avec peu de fausses alarmes. Quelle est la suite ? Le seau d'eau au dessus du lit ? L'émission d'un bruit ? d'une vibration ?

Si j'ai bien compris, tu discrimines via une analyse spectrale effectuée par FFT. Il y a une autre transformation (souvent associée à la FFT) c'est la transformation de Karhunen-Loève ou transformation en composantes principales. Elle permet, à partir d'un certain nombre de séquences de données, de redistribuer l'information en fonction du poids des corrélations entre les séquences. Par exemple, le ronflement proprement dit est d'une séquence à l'autre plutôt semblable, il est pollué par les bruits ambiants dont le spectre (le cas du sifflement) est suffisamment étendu pour corréler avec le ronflement. Par TKL, les données vont être rangées par corrélation, donc un premier canal qui va contenir quasiment que le ronflement, un second canal qui est plus ou moins la dérivée du premier, les suivants de plus en plus bruités pour finir par du bruit blanc. On vire tout, sauf le premier canal et on revient éventuellement dans l'espace de départ pat TKL inversée.

On a donc tout viré (grincement du lit, bruits divers pour ne garder que le signal répétitif). C'est un peu comme une FFT qui range l'info par poids spectral mais qui a su éliminer ce qui n'était pas répétitif.

On l'a beaucoup utilisé en imagerie (surtout satellite) et dans mon labo pour traiter des images en microscopie tunnel (images souvent polluées par des perturbations mécaniques) enfin, pour étudier les anomalies dans les battements du cœur, battements fortement pollués par tous les bruits organiques ambiants.

Pour la petite histoire, la transmission de l'info, de la rétine au cerveau semble utiliser la TKL pour limiter la masse de données à transmettre.

En tout cas, je ne parlerai pas de ton projet à ma femme...

Merci Lacuzon, j'ai entendu parler de la PCA sans jamais l'utiliser. Je vais me renseigner

Au passage un lien vers un instructables