Hackathon OpenVINO : reconnaître la voix et les émotions sur Raspberry Pi

Du 30 novembre au 1er décembre à Nijni Novgorod a eu lieu Hackathon OpenVINO. Les participants ont été invités à créer un prototype de solution produit à l'aide de la boîte à outils Intel OpenVINO. Les organisateurs ont proposé une liste de sujets approximatifs qui pourraient être guidés lors du choix d'une tâche, mais la décision finale revenait aux équipes. De plus, l'utilisation de modèles non inclus dans le produit a été encouragée.

Hackathon OpenVINO : reconnaître la voix et les émotions sur Raspberry Pi

Dans cet article, nous vous expliquerons comment nous avons créé notre prototype du produit, avec lequel nous avons finalement pris la première place.

Plus de 10 équipes ont participé au hackathon. C'est bien que certains d'entre eux viennent d'autres régions. Le lieu du hackathon était le complexe « Kremlinsky sur Pochain », où des photographies anciennes de Nijni Novgorod étaient accrochées à l'intérieur, dans un entourage ! (Je vous rappelle qu'actuellement le bureau central d'Intel est situé à Nijni Novgorod). Les participants disposaient de 26 heures pour écrire du code et, à la fin, ils devaient présenter leur solution. Un autre avantage était la présence d'une session de démonstration pour s'assurer que tout ce qui était prévu était effectivement mis en œuvre et ne restait pas des idées dans la présentation. Merch, snacks, nourriture, tout était là aussi !

De plus, Intel a fourni en option des caméras, Raspberry PI, Neural Compute Stick 2.

Sélection de tâche

L’une des parties les plus difficiles de la préparation d’un hackathon de forme libre consiste à choisir un défi. Nous avons immédiatement décidé de proposer quelque chose qui n'était pas encore dans le produit, car l'annonce disait que c'était le bienvenu.

Ayant analysé modèles, qui sont inclus dans le produit dans la version actuelle, nous arrivons à la conclusion que la plupart d'entre eux résolvent divers problèmes de vision par ordinateur. De plus, il est très difficile de trouver un problème dans le domaine de la vision par ordinateur qui ne peut être résolu avec OpenVINO, et même s'il est possible d'en inventer un, il est difficile de trouver des modèles pré-entraînés dans le domaine public. Nous décidons d'aller dans une autre direction : vers le traitement et l'analyse de la parole. Considérons une tâche intéressante consistant à reconnaître les émotions de la parole. Il faut dire qu’OpenVINO dispose déjà d’un modèle qui détermine les émotions d’une personne en fonction de son visage, mais :

  • En théorie, il est possible de créer un algorithme combiné qui fonctionnera à la fois sur le son et sur l’image, ce qui devrait augmenter la précision.
  • Les caméras ont généralement un angle de vision étroit ; plusieurs caméras sont nécessaires pour couvrir une grande zone ; le son n'a pas une telle limitation.

Développons l'idée : prenons comme base l'idée du segment du commerce de détail. Vous pouvez mesurer la satisfaction des clients aux caisses des magasins. Si l'un des clients n'est pas satisfait du service et commence à hausser le ton, vous pouvez immédiatement appeler l'administrateur pour obtenir de l'aide.
Dans ce cas, nous devons ajouter la reconnaissance vocale humaine, cela nous permettra de distinguer les employés du magasin des clients et de fournir des analyses pour chaque individu. Bon, en plus, il sera possible d'analyser eux-mêmes le comportement des employés du magasin, d'évaluer l'ambiance dans l'équipe, ça sonne bien !

Nous formulons les exigences de notre solution :

  • Petite taille de l'appareil cible
  • Fonctionnement en temps réel
  • Bas prix
  • Évolutivité facile

En conséquence, nous sélectionnons Raspberry Pi 3 c comme périphérique cible Intel NCS2.

Ici, il est important de noter une caractéristique importante de NCS : il fonctionne mieux avec les architectures CNN standard, mais si vous devez exécuter un modèle avec des couches personnalisées, attendez-vous à une optimisation de bas niveau.

Il n'y a qu'une petite chose à faire : vous devez vous procurer un microphone. Un microphone USB ordinaire fera l'affaire, mais il n'aura pas fière allure avec le RPI. Mais même ici, la solution « se trouve littéralement à proximité ». Pour enregistrer la voix, nous décidons d'utiliser la carte Voice Bonnet du kit Kit vocal Google AIY, sur lequel se trouve un microphone stéréo filaire.

Téléchargez Raspbian depuis Référentiel de projets AIY et téléchargez-le sur une clé USB, testez que le microphone fonctionne à l'aide de la commande suivante (il enregistrera l'audio pendant 5 secondes et l'enregistrera dans un fichier) :

arecord -d 5 -r 16000 test.wav

Je dois immédiatement noter que le microphone est très sensible et capte bien le bruit. Pour résoudre ce problème, allons sur alsamixer, sélectionnons les appareils de capture et réduisons le niveau du signal d'entrée à 50-60 %.

Hackathon OpenVINO : reconnaître la voix et les émotions sur Raspberry Pi
On modifie le corps avec une lime et tout rentre, on peut même le fermer avec un couvercle

Ajout d'un bouton indicateur

En démontant l'AIY Voice Kit, on retient qu'il existe un bouton RGB dont le rétroéclairage peut être contrôlé par logiciel. Nous recherchons « Google AIY Led » et trouvons de la documentation : https://aiyprojects.readthedocs.io/en/latest/aiy.leds.html
Pourquoi ne pas utiliser ce bouton pour afficher l'émotion reconnue, nous n'avons que 7 classes, et le bouton a 8 couleurs, juste ce qu'il faut !

On connecte le bouton via GPIO à Voice Bonnet, charge les librairies nécessaires (elles sont déjà installées dans le kit de distribution des projets AIY)

from aiy.leds import Leds, Color
from aiy.leds import RgbLeds

Créons un dict dans lequel chaque émotion aura une couleur correspondante sous la forme d'un tuple RVB et un objet de la classe aiy.leds.Leds, à travers lequel nous mettrons à jour la couleur :

led_dict = {'neutral': (255, 255, 255), 'happy': (0, 255, 0), 'sad': (0, 255, 255), 'angry': (255, 0, 0), 'fearful': (0, 0, 0), 'disgusted':  (255, 0, 255), 'surprised':  (255, 255, 0)} 
leds = Leds()

Et enfin, après chaque nouvelle prédiction d'une émotion, nous mettrons à jour la couleur du bouton en fonction de celle-ci (par touche).

leds.update(Leds.rgb_on(led_dict.get(classes[prediction])))

Hackathon OpenVINO : reconnaître la voix et les émotions sur Raspberry Pi
Bouton, brûle !

Travailler avec la voix

Nous utiliserons pyaudio pour capturer le flux du microphone et webrtcvad pour filtrer le bruit et détecter la voix. De plus, nous créerons une file d'attente à laquelle nous ajouterons et supprimerons de manière asynchrone des extraits vocaux.

Étant donné que webrtcvad a une limitation sur la taille du fragment fourni - il doit être égal à 10/20/30 ms, et que la formation du modèle de reconnaissance des émotions (comme nous l'apprendrons plus tard) a été réalisée sur un ensemble de données à 48 kHz, nous allons capturez des morceaux de taille 48000 20 × 1000 ms/1 960 × XNUMX (mono) = XNUMX octets. Webrtcvad renverra Vrai/Faux pour chacun de ces chunks, ce qui correspond à la présence ou à l'absence d'un vote dans le chunk.

Implémentons la logique suivante :

  • Nous ajouterons à la liste les fragments pour lesquels il y a un vote ; s'il n'y a pas de vote, alors nous incrémenterons le compteur des fragments vides.
  • Si le compteur de chunks vides est >=30 (600 ms), alors on regarde la taille de la liste des chunks accumulés ; s'il est >250, alors on l'ajoute à la file d'attente ; sinon, on considère que la longueur de l'enregistrement ne suffit pas à le transmettre au modèle pour identifier l'orateur.
  • Si le compteur de fragments vides est toujours < 30 et que la taille de la liste des fragments accumulés dépasse 300, alors nous ajouterons le fragment à la file d'attente pour une prédiction plus précise. (car les émotions ont tendance à changer avec le temps)

 def to_queue(frames):
    d = np.frombuffer(b''.join(frames), dtype=np.int16)
    return d

framesQueue = queue.Queue()
def framesThreadBody():
    CHUNK = 960
    FORMAT = pyaudio.paInt16
    CHANNELS = 1
    RATE = 48000

    p = pyaudio.PyAudio()
    vad = webrtcvad.Vad()
    vad.set_mode(2)
    stream = p.open(format=FORMAT,
                channels=CHANNELS,
                rate=RATE,
                input=True,
                frames_per_buffer=CHUNK)
    false_counter = 0
    audio_frame = []
    while process:
        data = stream.read(CHUNK)
        if not vad.is_speech(data, RATE):
            false_counter += 1
            if false_counter >= 30:
                if len(audio_frame) > 250:              
                    framesQueue.put(to_queue(audio_frame,timestamp_start))
                    audio_frame = []
                    false_counter = 0

        if vad.is_speech(data, RATE):
            false_counter = 0
            audio_frame.append(data)
            if len(audio_frame) > 300:                
                    framesQueue.put(to_queue(audio_frame,timestamp_start))
                    audio_frame = []

Il est temps de rechercher des modèles pré-entraînés dans le domaine public, allez sur github, Google, mais n'oubliez pas que nous avons une limitation sur l'architecture utilisée. C'est une partie assez difficile, car vous devez tester les modèles sur vos données d'entrée, et en plus, les convertir au format interne d'OpenVINO - IR (Intermediate Representation). Nous avons essayé environ 5 à 7 solutions différentes de github, et si le modèle de reconnaissance des émotions fonctionnait immédiatement, alors avec la reconnaissance vocale, nous avons dû attendre plus longtemps - ils utilisent des architectures plus complexes.

Nous nous concentrons sur les éléments suivants :

  • Émotions de la voix - https://github.com/alexmuhr/Voice_Emotion
    Cela fonctionne selon le principe suivant : l'audio est découpé en passages d'une certaine taille, pour chacun de ces passages nous sélectionnons MFCC puis soumettez-les en tant qu'entrée à CNN
  • Reconnaissance vocale - https://github.com/linhdvu14/vggvox-speaker-identification
    Ici, au lieu de MFCC, nous travaillons avec un spectrogramme, après FFT, nous transmettons le signal à CNN, où en sortie nous obtenons une représentation vectorielle de la voix.

Nous parlerons ensuite de la conversion de modèles, en commençant par la théorie. OpenVINO comprend plusieurs modules :

  • Open Model Zoo, dont les modèles pourraient être utilisés et inclus dans votre produit
  • Model Optimzer, grâce auquel vous pouvez convertir un modèle de différents formats de framework (Tensorflow, ONNX, etc.) au format de représentation intermédiaire, avec lequel nous travaillerons plus loin
  • Inference Engine vous permet d'exécuter des modèles au format IR sur des processeurs Intel, des puces Myriad et des accélérateurs Neural Compute Stick
  • La version la plus efficace d'OpenCV (avec prise en charge du moteur d'inférence)
    Chaque modèle au format IR est décrit par deux fichiers : .xml et .bin.
    Les modèles sont convertis au format IR via Model Optimizer comme suit :

    python /opt/intel/openvino/deployment_tools/model_optimizer/mo_tf.py --input_model speaker.hdf5.pb --data_type=FP16 --input_shape [1,512,1000,1]

    --data_type vous permet de sélectionner le format de données avec lequel le modèle fonctionnera. FP32, FP16, INT8 sont pris en charge. Choisir le type de données optimal peut donner une bonne amélioration des performances.
    --input_shape indique la dimension des données d’entrée. La possibilité de le modifier dynamiquement semble être présente dans l'API C++, mais nous n'avons pas creusé jusque-là et l'avons simplement corrigé pour l'un des modèles.
    Essayons ensuite de charger le modèle déjà converti au format IR via le module DNN dans OpenCV et de le lui transmettre.

    import cv2 as cv
    emotionsNet = cv.dnn.readNet('emotions_model.bin',
                              'emotions_model.xml')
    emotionsNet.setPreferableTarget(cv.dnn.DNN_TARGET_MYRIAD)

    La dernière ligne dans ce cas permet de rediriger les calculs vers le Neural Compute Stick, les calculs de base sont effectués sur le processeur, mais dans le cas du Raspberry Pi cela ne fonctionnera pas, vous aurez besoin d'un stick.

    Ensuite, la logique est la suivante : nous divisons notre audio en fenêtres d'une certaine taille (pour nous c'est 0.4 s), nous convertissons chacune de ces fenêtres en MFCC, que nous alimentons ensuite dans la grille :

    emotionsNet.setInput(MFCC_from_window)
    result = emotionsNet.forward()

    Prenons ensuite la classe la plus courante pour toutes les fenêtres. Une solution simple, mais pour un hackathon, vous n'avez pas besoin de proposer quelque chose de trop abstrus, seulement si vous avez le temps. Nous avons encore beaucoup de travail à faire, alors passons à autre chose : nous nous occuperons de la reconnaissance vocale. Il est nécessaire de créer une sorte de base de données dans laquelle seraient stockés des spectrogrammes de voix préenregistrées. Comme il nous reste peu de temps, nous résoudrons ce problème du mieux que nous pouvons.

    À savoir, nous créons un script pour enregistrer un extrait vocal (cela fonctionne de la même manière que décrit ci-dessus, uniquement lorsqu'il est interrompu depuis le clavier, il enregistrera la voix dans un fichier).

    Essayons:

    python3 voice_db/record_voice.py test.wav

    Nous enregistrons les voix de plusieurs personnes (dans notre cas, trois membres de l'équipe)
    Ensuite, pour chaque voix enregistrée, nous effectuons une transformation de Fourier rapide, obtenons un spectrogramme et l'enregistrons sous forme de tableau numpy (.npy) :

    for file in glob.glob("voice_db/*.wav"):
            spec = get_fft_spectrum(file)
            np.save(file[:-4] + '.npy', spec)

    Plus de détails dans le dossier create_base.py
    En conséquence, lorsque nous exécuterons le script principal, nous obtiendrons des intégrations de ces spectrogrammes au tout début :

    for file in glob.glob("voice_db/*.npy"):
        spec = np.load(file)
        spec = spec.astype('float32')
        spec_reshaped = spec.reshape(1, 1, spec.shape[0], spec.shape[1])
        srNet.setInput(spec_reshaped)
        pred = srNet.forward()
        emb = np.squeeze(pred)

    Après avoir reçu l'intégration du segment sonore, nous pourrons déterminer à qui il appartient en prenant la distance cosinusoïdale du passage à toutes les voix de la base de données (la plus petite, la plus probable) - pour la démo, nous fixons le seuil à 0.3) :

            dist_list = cdist(emb, enroll_embs, metric="cosine")
            distances = pd.DataFrame(dist_list, columns = df.speaker)

    Au final, je voudrais noter que la vitesse d'inférence était rapide et permettait d'ajouter 1 à 2 modèles supplémentaires (pour un échantillon de 7 secondes, il en fallait 2.5 pour l'inférence). Nous n'avons plus eu le temps d'ajouter de nouveaux modèles et nous nous sommes concentrés sur l'écriture d'un prototype de l'application web.

    application Web

    Un point important : nous prenons un routeur avec nous depuis chez nous et configurons notre réseau local, cela permet de connecter l'appareil et les ordinateurs portables via le réseau.

    Le backend est un canal de messages de bout en bout entre le front et le Raspberry Pi, basé sur la technologie websocket (protocole http sur tcp).

    La première étape consiste à recevoir des informations traitées par Raspberry, c'est-à-dire des prédicteurs emballés en JSON, qui sont enregistrés dans la base de données à mi-chemin de leur parcours afin que des statistiques puissent être générées sur le contexte émotionnel de l'utilisateur pour la période. Ce paquet est ensuite envoyé au frontend, qui utilise l'abonnement et reçoit les paquets du point de terminaison Websocket. L'ensemble du mécanisme backend est construit dans le langage Golang ; il a été choisi car il est bien adapté aux tâches asynchrones, que les goroutines gèrent bien.
    Lors de l'accès au point final, l'utilisateur est enregistré et entré dans la structure, puis son message est reçu. L'utilisateur et le message sont entrés dans un hub commun, à partir duquel les messages sont déjà envoyés plus loin (vers le front abonné), et si l'utilisateur ferme la connexion (framboise ou front), alors son abonnement est annulé et il est supprimé de le moyeu.

    Hackathon OpenVINO : reconnaître la voix et les émotions sur Raspberry Pi
    Nous attendons une connexion par l'arrière

    Front-end est une application Web écrite en JavaScript utilisant la bibliothèque React pour accélérer et simplifier le processus de développement. Le but de cette application est de visualiser les données obtenues à l'aide d'algorithmes exécutés côté back-end et directement sur le Raspberry Pi. La page a un routage sectionnel implémenté à l'aide de React-Router, mais la page principale d'intérêt est la page principale, où un flux continu de données est reçu en temps réel du serveur à l'aide de la technologie WebSocket. Raspberry Pi détecte une voix, détermine si elle appartient à une personne spécifique à partir de la base de données enregistrée et envoie une liste de probabilités au client. Le client affiche les dernières données pertinentes, affiche l'avatar de la personne qui a probablement parlé dans le microphone, ainsi que l'émotion avec laquelle il prononce les mots.

    Hackathon OpenVINO : reconnaître la voix et les émotions sur Raspberry Pi
    Page d'accueil avec des prévisions mises à jour

    Conclusion

    Il n’était pas possible de tout terminer comme prévu, nous n’avions tout simplement pas le temps, donc le principal espoir était dans la démo, que tout fonctionnerait. Dans la présentation, ils ont expliqué comment tout fonctionnait, quels modèles ils avaient pris, quels problèmes ils avaient rencontrés. Vient ensuite la partie démonstration : des experts se promènent dans la salle dans un ordre aléatoire et se rapprochent de chaque équipe pour examiner le prototype fonctionnel. Ils nous ont aussi posé des questions, chacun a répondu à sa question, ils ont laissé le Web sur leur ordinateur portable et tout a vraiment fonctionné comme prévu.

    Permettez-moi de noter que le coût total de notre solution était de 150 $ :

    • Framboise Pi 3 ~ 35 $
    • Google AIY Voice Bonnet (vous pouvez prendre des frais de respeaker) ~ 15$
    • Intel NCS2 ~ 100$

    Comment améliorer:

    • Utiliser l'inscription du client - demander à lire le texte généré aléatoirement
    • Ajoutez quelques modèles supplémentaires : vous pouvez déterminer le sexe et l'âge par la voix
    • Voix séparées simultanément (diarisation)

    Dépôt: https://github.com/vladimirwest/OpenEMO

    Hackathon OpenVINO : reconnaître la voix et les émotions sur Raspberry Pi
    Fatigués mais heureux nous sommes

    En conclusion, je voudrais remercier les organisateurs et les participants. Parmi les projets d'autres équipes, nous avons personnellement apprécié la solution de surveillance des places de stationnement gratuites. Pour nous, ce fut une expérience extrêmement cool d’immersion dans le produit et son développement. J'espère que de plus en plus d'événements intéressants auront lieu dans les régions, notamment sur des sujets liés à l'IA.

Source: habr.com

Ajouter un commentaire