Hackathon OpenVINO: recunoașterea vocii și a emoțiilor pe Raspberry Pi

30 noiembrie - 1 decembrie la Nijni Novgorod a avut loc Hackathon OpenVINO. Participanții au fost rugați să creeze un prototip al unei soluții de produs folosind setul de instrumente Intel OpenVINO. Organizatorii au propus o listă de subiecte aproximative după care ar putea fi ghidate atunci când alegem o sarcină, însă decizia finală a rămas la echipe. În plus, a fost încurajată utilizarea modelelor care nu sunt incluse în produs.

Hackathon OpenVINO: recunoașterea vocii și a emoțiilor pe Raspberry Pi

În acest articol vă vom spune despre modul în care am creat prototipul nostru de produs, cu care am ocupat în cele din urmă primul loc.

La hackathon au participat peste 10 echipe. E bine că unii dintre ei au venit din alte regiuni. Locul de desfășurare a hackatonului a fost complexul „Kremlinsky pe Pochain”, unde fotografii antice ale lui Nijni Novgorod erau agățate în interior, într-un anturaj! (Vă reamintesc că în acest moment sediul central al Intel este situat în Nijni Novgorod). Participanții au avut 26 de ore pentru a scrie cod, iar la final au trebuit să-și prezinte soluția. Un avantaj separat a fost prezența unei sesiuni demo pentru a ne asigura că tot ceea ce era planificat a fost efectiv implementat și nu a rămas idei în prezentare. Merch, gustări, mâncare, totul era și acolo!

În plus, Intel a furnizat opțional camere, Raspberry PI, Neural Compute Stick 2.

Selectarea sarcinilor

Una dintre cele mai dificile părți ale pregătirii pentru un hackathon în formă liberă este alegerea unei provocări. Am decis imediat să venim cu ceva care nu era încă în produs, deoarece anunțul spunea că acest lucru este foarte binevenit.

După ce a analizat model, care sunt incluse în produs în versiunea actuală, ajungem la concluzia că majoritatea rezolvă diverse probleme de vedere computerizată. Mai mult, este foarte greu să vii cu o problemă în domeniul viziunii computerizate care nu poate fi rezolvată folosind OpenVINO și chiar dacă una poate fi inventată, este greu să găsești modele pre-antrenate în domeniul public. Decidem să săpăm într-o altă direcție - spre procesarea vorbirii și analiză. Să luăm în considerare o sarcină interesantă de recunoaștere a emoțiilor din vorbire. Trebuie spus că OpenVINO are deja un model care determină emoțiile unei persoane pe baza feței sale, dar:

  • În teorie, este posibil să se creeze un algoritm combinat care va funcționa atât pe sunet, cât și pe imagine, ceea ce ar trebui să ofere o creștere a preciziei.
  • Camerele au de obicei un unghi de vizualizare îngust; este necesară mai mult de o cameră pentru a acoperi o zonă mare; sunetul nu are o astfel de limitare.

Să dezvoltăm ideea: să luăm ca bază ideea pentru segmentul de retail. Puteți măsura satisfacția clienților la casele din magazin. Dacă unul dintre clienți este nemulțumit de serviciu și începe să ridice tonul, puteți apela imediat administratorul pentru ajutor.
În acest caz, trebuie să adăugăm recunoașterea vocii umane, aceasta ne va permite să distingem angajații magazinului de clienți și să oferim analize pentru fiecare individ. Ei bine, în plus, se va putea analiza înșiși comportamentul angajaților magazinului, se va evalua atmosfera din echipă, sună bine!

Formulăm cerințele pentru soluția noastră:

  • Dimensiunea mică a dispozitivului țintă
  • Funcționare în timp real
  • Prețul scăzut
  • Scalabilitate ușoară

Ca rezultat, selectăm Raspberry Pi 3 c ca dispozitiv țintă Intel NCS 2.

Aici este important de remarcat o caracteristică importantă a NCS - funcționează cel mai bine cu arhitecturile CNN standard, dar dacă trebuie să rulați un model cu straturi personalizate pe el, atunci așteptați-vă la optimizare la nivel scăzut.

Există un singur lucru mic de făcut: trebuie să-ți iei un microfon. Un microfon USB obișnuit va funcționa, dar nu va arăta bine împreună cu RPI. Dar chiar și aici soluția literalmente „se află în apropiere”. Pentru a înregistra vocea, decidem să folosim placa Voice Bonnet din kit Kit de voce Google AIY, pe care se află un microfon stereo cu fir.

Descărcați Raspbian de pe Depozitul de proiecte AIY și încărcați-l pe o unitate flash, testați dacă microfonul funcționează folosind următoarea comandă (va înregistra audio timp de 5 secunde și îl va salva într-un fișier):

arecord -d 5 -r 16000 test.wav

Ar trebui să remarc imediat că microfonul este foarte sensibil și captează bine zgomotul. Pentru a remedia acest lucru, să mergem la alsamixer, să selectăm Dispozitive de captură și să reducem nivelul semnalului de intrare la 50-60%.

Hackathon OpenVINO: recunoașterea vocii și a emoțiilor pe Raspberry Pi
Modificam corpul cu o pila si totul se potriveste, il puteti inchide chiar si cu un capac

Adăugarea unui buton indicator

În timp ce demontam kit-ul AIY Voice, ne amintim că există un buton RGB, a cărui iluminare de fundal poate fi controlată prin software. Căutăm „Google AIY Led” și găsim documentație: https://aiyprojects.readthedocs.io/en/latest/aiy.leds.html
De ce să nu folosiți acest buton pentru a afișa emoția recunoscută, avem doar 7 clase, iar butonul are 8 culori, doar suficient!

Conectăm butonul prin GPIO la Voice Bonnet, încărcăm bibliotecile necesare (sunt deja instalate în kitul de distribuție din proiectele AIY)

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

Să creăm un dict în care fiecare emoție va avea o culoare corespunzătoare sub forma unui tuplu RGB și un obiect din clasa aiy.leds.Leds, prin care vom actualiza culoarea:

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

Și în sfârșit, după fiecare nouă predicție a unei emoții, vom actualiza culoarea butonului în conformitate cu aceasta (prin cheie).

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

Hackathon OpenVINO: recunoașterea vocii și a emoțiilor pe Raspberry Pi
Buton, arde!

Lucrul cu vocea

Vom folosi pyaudio pentru a capta fluxul de la microfon și webrtcvad pentru a filtra zgomotul și a detecta vocea. În plus, vom crea o coadă la care vom adăuga și elimina în mod asincron fragmente vocale.

Deoarece webrtcvad are o limitare a dimensiunii fragmentului furnizat - trebuie să fie egal cu 10/20/30ms, iar antrenamentul modelului pentru recunoașterea emoțiilor (după cum vom afla mai târziu) a fost efectuat pe un set de date de 48 kHz, vom captura bucăți de dimensiune 48000×20ms/1000×1(mono)=960 bytes. Webrtcvad va returna True/False pentru fiecare dintre aceste bucăți, ceea ce corespunde prezenței sau absenței unui vot în bloc.

Să implementăm următoarea logică:

  • Vom adăuga la listă acele bucăți în care există un vot; dacă nu există vot, atunci vom crește contorul de bucăți goale.
  • Dacă contorul de bucăți goale este >=30 (600 ms), atunci ne uităm la dimensiunea listei de bucăți acumulate; dacă este >250, atunci îl adăugăm la coadă; dacă nu, considerăm că lungimea a înregistrării nu este suficient pentru a-l alimenta modelului pentru a identifica vorbitorul.
  • Dacă contorul de bucăți goale este încă < 30, iar dimensiunea listei de bucăți acumulate depășește 300, atunci vom adăuga fragmentul la coadă pentru o predicție mai precisă. (deoarece emoțiile tind să se schimbe în timp)

 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 = []

Este timpul să căutați modele pre-antrenate în domeniul public, mergeți la github, Google, dar amintiți-vă că avem o limitare asupra arhitecturii folosite. Aceasta este o parte destul de dificilă, deoarece trebuie să testați modelele pe datele dvs. de intrare și, în plus, să le convertiți în formatul intern OpenVINO - IR (Reprezentare intermediară). Am încercat vreo 5-7 soluții diferite de la github, iar dacă modelul de recunoaștere a emoțiilor a funcționat imediat, atunci cu recunoașterea vocii a trebuit să așteptăm mai mult - folosesc arhitecturi mai complexe.

Ne concentrăm pe următoarele:

În continuare vom vorbi despre conversia modelelor, începând cu teorie. OpenVINO include mai multe module:

  • Deschideți Model Zoo, modele din care ar putea fi utilizate și incluse în produsul dvs
  • Model Optimzer, datorită căruia puteți converti un model din diverse formate de cadru (Tensorflow, ONNX etc) în formatul de reprezentare intermediară, cu care vom lucra în continuare
  • Inference Engine vă permite să rulați modele în format IR pe procesoare Intel, cipuri Myriad și acceleratoare Neural Compute Stick
  • Cea mai eficientă versiune de OpenCV (cu suport pentru Inference Engine)
    Fiecare model în format IR este descris de două fișiere: .xml și .bin.
    Modelele sunt convertite în format IR prin Model Optimizer, după cum urmează:

    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 vă permite să selectați formatul de date cu care va funcționa modelul. FP32, FP16, INT8 sunt acceptate. Alegerea tipului optim de date poate oferi o creștere bună a performanței.
    --input_shape indică dimensiunea datelor de intrare. Capacitatea de a o schimba dinamic pare să fie prezentă în API-ul C++, dar nu am săpat atât de departe și am rezolvat-o pur și simplu pentru unul dintre modele.
    Apoi, să încercăm să încărcăm modelul deja convertit în format IR prin modulul DNN în OpenCV și să-l transmitem către acesta.

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

    Ultima linie în acest caz vă permite să redirecționați calculele către Neural Compute Stick, calculele de bază sunt efectuate pe procesor, dar în cazul Raspberry Pi acest lucru nu va funcționa, veți avea nevoie de un stick.

    În continuare, logica este următoarea: împărțim audio-ul nostru în ferestre de o anumită dimensiune (pentru noi este de 0.4 s), convertim fiecare dintre aceste ferestre în MFCC, pe care apoi le alimentarem în rețea:

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

    În continuare, să luăm cea mai comună clasă pentru toate ferestrele. O soluție simplă, dar pentru un hackathon nu trebuie să vii cu ceva prea abstrus, doar dacă ai timp. Mai avem mult de lucru, așa că să trecem mai departe - ne vom ocupa de recunoașterea vocii. Este necesar să se realizeze un fel de bază de date în care să fie stocate spectrogramele vocilor preînregistrate. Deoarece a mai rămas puțin timp, vom rezolva această problemă cât de bine putem.

    Și anume, creăm un script pentru înregistrarea unui fragment de voce (funcționează în același mod ca cel descris mai sus, doar atunci când este întrerupt de la tastatură va salva vocea într-un fișier).

    Sa incercam:

    python3 voice_db/record_voice.py test.wav

    Înregistrăm vocile mai multor persoane (în cazul nostru, trei membri ai echipei)
    Apoi, pentru fiecare voce înregistrată, efectuăm o transformare Fourier rapidă, obținem o spectrogramă și o salvăm ca matrice numpy (.npy):

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

    Mai multe detalii in dosar create_base.py
    Ca rezultat, atunci când rulăm scriptul principal, vom obține încorporații din aceste spectrograme chiar de la început:

    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)

    După ce primim încorporarea din segmentul sunat, vom putea determina cui îi aparține luând distanța cosinus de la trecere la toate vocile din baza de date (cu cât mai mici, cu atât mai probabil) - pentru demo se stabilește pragul până la 0.3):

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

    În cele din urmă, aș dori să remarc că viteza de inferență a fost rapidă și a făcut posibilă adăugarea a încă 1-2 modele (pentru o probă de 7 secunde a fost nevoie de 2.5 pentru inferență). Nu am mai avut timp să adăugăm modele noi și ne-am concentrat pe scrierea unui prototip al aplicației web.

    aplicatie web

    Un punct important: luăm cu noi un router de acasă și ne instalăm rețeaua locală, ajută la conectarea dispozitivului și a laptopurilor prin rețea.

    Backend-ul este un canal de mesaje end-to-end între front și Raspberry Pi, bazat pe tehnologia websocket (protocol http over tcp).

    Prima etapă este de a primi informații procesate de la raspberry, adică predictori împachetați în json, care sunt salvate în baza de date la jumătatea drumului lor, astfel încât să poată fi generate statistici despre mediul emoțional al utilizatorului pentru perioada respectivă. Acest pachet este apoi trimis către front-end, care utilizează abonamentul și primește pachete de la punctul final de websocket. Întregul mecanism backend este construit în limbajul golang; a fost ales pentru că este potrivit pentru sarcini asincrone, pe care goroutine le gestionează bine.
    La accesarea endpoint-ului, utilizatorul este înregistrat și intrat în structură, apoi este primit mesajul său. Atât utilizatorul, cât și mesajul sunt introduse într-un hub comun, din care mesajele sunt deja trimise mai departe (în fața abonată), iar dacă utilizatorul închide conexiunea (zmeura sau front), atunci abonamentul îi este anulat și este eliminat din hubul.

    Hackathon OpenVINO: recunoașterea vocii și a emoțiilor pe Raspberry Pi
    Așteptăm o conexiune din spate

    Front-end este o aplicație web scrisă în JavaScript care utilizează biblioteca React pentru a accelera și simplifica procesul de dezvoltare. Scopul acestei aplicații este de a vizualiza datele obținute folosind algoritmi care rulează pe partea de back-end și direct pe Raspberry Pi. Pagina are rutare secțională implementată folosind react-router, dar pagina principală de interes este pagina principală, unde un flux continuu de date este primit în timp real de la server folosind tehnologia WebSocket. Raspberry Pi detectează o voce, determină dacă aceasta aparține unei anumite persoane din baza de date înregistrată și trimite o listă de probabilități clientului. Clientul afișează cele mai recente date relevante, afișează avatarul persoanei care a vorbit cel mai probabil în microfon, precum și emoția cu care pronunță cuvintele.

    Hackathon OpenVINO: recunoașterea vocii și a emoțiilor pe Raspberry Pi
    Pagina de pornire cu previziuni actualizate

    Concluzie

    Nu a fost posibil să finalizăm totul așa cum era planificat, pur și simplu nu aveam timp, așa că speranța principală era în demo, că totul va funcționa. În prezentare au vorbit despre cum funcționează totul, ce modele au luat, ce probleme au întâmpinat. Urmează partea demonstrativă - experții s-au plimbat prin cameră în ordine aleatorie și s-au apropiat de fiecare echipă pentru a se uita la prototipul de lucru. Ne-au pus și ele întrebări, fiecare și-a răspuns la partea sa, au părăsit web-ul pe laptop și totul a funcționat într-adevăr conform așteptărilor.

    Permiteți-mi să notez că costul total al soluției noastre a fost de 150 USD:

    • Raspberry Pi 3 ~ 35 USD
    • Google AIY Voice Bonnet (puteți primi o taxă pentru difuzor) ~ 15 USD
    • Intel NCS 2 ~ 100$

    Cum să îmbunătățiți:

    • Utilizați înregistrarea de la client - cereți să citiți textul care este generat aleatoriu
    • Adăugați încă câteva modele: puteți determina sexul și vârsta prin voce
    • Voci separate care sună simultan (diarizare)

    Repertoriu: https://github.com/vladimirwest/OpenEMO

    Hackathon OpenVINO: recunoașterea vocii și a emoțiilor pe Raspberry Pi
    Suntem obosiți, dar fericiți

    În încheiere, aș dori să le mulțumesc organizatorilor și participanților. Printre proiectele altor echipe, personal ne-a plăcut soluția de monitorizare a locurilor de parcare gratuite. Pentru noi, a fost o experiență extraordinar de grozavă de imersiune în produs și dezvoltare. Sper că în regiuni vor avea loc din ce în ce mai multe evenimente interesante, inclusiv pe teme de IA.

Sursa: www.habr.com

Adauga un comentariu