OpenVINO hackathon: riconoscere voce ed emozioni su Raspberry Pi

Dal 30 novembre al 1 dicembre si è tenuto a Nizhny Novgorod OpenVINO hackathon. Ai partecipanti è stato chiesto di creare un prototipo di una soluzione di prodotto utilizzando il toolkit Intel OpenVINO. Gli organizzatori hanno proposto un elenco di argomenti approssimativi da cui si potrebbe orientarsi nella scelta di un compito, ma la decisione finale è rimasta alle squadre. Inoltre, è stato incoraggiato l'uso di modelli non inclusi nel prodotto.

OpenVINO hackathon: riconoscere voce ed emozioni su Raspberry Pi

In questo articolo ti parleremo di come abbiamo creato il nostro prototipo del prodotto, con il quale alla fine ci siamo classificati al primo posto.

All’hackathon hanno partecipato più di 10 squadre. È bello che alcuni di loro provenissero da altre regioni. La sede dell'hackathon è stata il complesso "Kremlinsky on Pochain", dove all'interno erano appese antiche fotografie di Nizhny Novgorod, in un entourage! (Ti ricordo che al momento l'ufficio centrale di Intel si trova a Nizhny Novgorod). Ai partecipanti venivano concesse 26 ore per scrivere il codice e alla fine dovevano presentare la loro soluzione. Un vantaggio a parte è stata la presenza di una sessione demo per assicurarsi che tutto ciò che era stato pianificato fosse effettivamente implementato e non rimanessero idee nella presentazione. Merchandising, snack, cibo, c'era anche tutto!

Inoltre, Intel ha fornito opzionalmente fotocamere, Raspberry PI, Neural Compute Stick 2.

Selezione del compito

Una delle parti più difficili della preparazione per un hackathon in formato libero è scegliere una sfida. Abbiamo subito deciso di inventare qualcosa che non era ancora presente nel prodotto, poiché l'annuncio diceva che sarebbe stato molto gradito.

Dopo aver analizzato modello, che sono inclusi nel prodotto nella versione attuale, giungiamo alla conclusione che la maggior parte di essi risolve vari problemi di visione artificiale. Inoltre, è molto difficile trovare un problema nel campo della visione artificiale che non possa essere risolto utilizzando OpenVINO, e anche se è possibile inventarne uno, è difficile trovare modelli pre-addestrati di pubblico dominio. Decidiamo di scavare in un'altra direzione: verso l'elaborazione e l'analisi del parlato. Consideriamo un compito interessante di riconoscere le emozioni dal discorso. C’è da dire che OpenVINO ha già un modello che determina le emozioni di una persona in base al suo volto, ma:

  • In teoria, è possibile creare un algoritmo combinato che funzionerà sia sul suono che sull'immagine, il che dovrebbe aumentare la precisione.
  • Le telecamere di solito hanno un angolo di visione ristretto; sono necessarie più telecamere per coprire una vasta area; il suono non ha tale limitazione.

Sviluppiamo l'idea: prendiamo come base l'idea per il segmento vendita al dettaglio. Puoi misurare la soddisfazione del cliente alle casse del negozio. Se uno dei clienti è insoddisfatto del servizio e inizia ad alzare il tono, puoi chiamare immediatamente l'amministratore per chiedere aiuto.
In questo caso, dobbiamo aggiungere il riconoscimento vocale umano, questo ci consentirà di distinguere i dipendenti del negozio dai clienti e fornire analisi per ciascun individuo. Ebbene, inoltre, sarà possibile analizzare il comportamento degli stessi dipendenti del negozio, valutare l'atmosfera nella squadra, suona bene!

Formuliamo i requisiti per la nostra soluzione:

  • Piccole dimensioni del dispositivo di destinazione
  • Operazione in tempo reale
  • Prezzo basso
  • Facile scalabilità

Di conseguenza, selezioniamo Raspberry Pi 3 c come dispositivo di destinazione IntelNCS2.

Qui è importante notare una caratteristica importante di NCS: funziona meglio con le architetture CNN standard, ma se è necessario eseguire un modello con livelli personalizzati su di esso, aspettarsi un'ottimizzazione di basso livello.

C'è solo una piccola cosa da fare: devi procurarti un microfono. Andrà bene un normale microfono USB, ma non starà bene insieme all'RPI. Ma anche qui la soluzione “è letteralmente vicina”. Per registrare la voce decidiamo di utilizzare la scheda Voice Bonnet del kit Kit vocale Google AIY, sul quale è presente un microfono stereo cablato.

Scarica Raspbian da Archivio dei progetti AIY e caricalo su un'unità flash, verifica che il microfono funzioni utilizzando il seguente comando (registrerà l'audio per 5 secondi e lo salverà in un file):

arecord -d 5 -r 16000 test.wav

Devo subito notare che il microfono è molto sensibile e capta bene il rumore. Per risolvere questo problema, andiamo su alsamixer, selezioniamo Dispositivi di acquisizione e riduciamo il livello del segnale di ingresso al 50-60%.

OpenVINO hackathon: riconoscere voce ed emozioni su Raspberry Pi
Modifichiamo il corpo con una lima e tutto si adatta, puoi anche chiuderlo con un coperchio

Aggiunta di un pulsante indicatore

Smontando l'AIY Voice Kit, ricordiamo che è presente un pulsante RGB, la cui retroilluminazione può essere controllata via software. Cerchiamo “Google AIY Led” e troviamo la documentazione: https://aiyprojects.readthedocs.io/en/latest/aiy.leds.html
Perché non utilizzare questo pulsante per visualizzare l'emozione riconosciuta, abbiamo solo 7 classi e il pulsante ha 8 colori, quanto basta!

Colleghiamo il pulsante tramite GPIO a Voice Bonnet, carichiamo le librerie necessarie (sono già installate nel kit di distribuzione dai progetti AIY)

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

Creiamo un dict in cui ogni emozione avrà un colore corrispondente sotto forma di una tupla RGB e un oggetto della classe aiy.leds.Leds, attraverso il quale aggiorneremo il colore:

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

E infine, dopo ogni nuova previsione di un'emozione, aggiorneremo il colore del pulsante in base ad essa (tramite tasto).

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

OpenVINO hackathon: riconoscere voce ed emozioni su Raspberry Pi
Bottone, brucia!

Lavorare con la voce

Utilizzeremo pyaudio per acquisire il flusso dal microfono e webrtcvad per filtrare il rumore e rilevare la voce. Inoltre, creeremo una coda alla quale aggiungeremo e rimuoveremo in modo asincrono brani vocali.

Poiché webrtcvad ha una limitazione sulla dimensione del frammento fornito - deve essere pari a 10/20/30ms, e l'addestramento del modello per il riconoscimento delle emozioni (come apprenderemo in seguito) è stato effettuato su un set di dati a 48kHz, lo faremo cattura blocchi di dimensioni 48000×20ms/1000×1(mono)=960 byte. Webrtcvad restituirà True/False per ciascuno di questi blocchi, che corrisponde alla presenza o all'assenza di un voto nel blocco.

Implementiamo la seguente logica:

  • Aggiungeremo alla lista i pezzi in cui c'è un voto; se non c'è nessun voto, incrementeremo il contatore dei pezzi vuoti.
  • Se il contatore dei pezzi vuoti è >=30 (600 ms), allora guardiamo la dimensione della lista dei pezzi accumulati; se è >250, allora lo aggiungiamo alla coda; in caso contrario, consideriamo che la lunghezza del record non è sufficiente per darlo in pasto al modello per identificare chi parla.
  • Se il contatore dei blocchi vuoti è ancora < 30 e la dimensione dell'elenco dei blocchi accumulati supera 300, aggiungeremo il frammento alla coda per una previsione più accurata. (perché le emozioni tendono a cambiare nel tempo)

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

È ora di cercare modelli pre-addestrati di pubblico dominio, vai su github, Google, ma ricorda che abbiamo una limitazione sull'architettura utilizzata. Questa è una parte piuttosto difficile, perché devi testare i modelli sui tuoi dati di input e, inoltre, convertirli nel formato interno di OpenVINO - IR (Intermediate Representation). Abbiamo provato circa 5-7 soluzioni diverse da Github e se il modello per riconoscere le emozioni ha funzionato immediatamente, con il riconoscimento vocale abbiamo dovuto aspettare più a lungo: utilizzano architetture più complesse.

Ci concentriamo su quanto segue:

Successivamente parleremo della conversione dei modelli, iniziando dalla teoria. OpenVINO comprende diversi moduli:

  • Apri Model Zoo, i cui modelli potrebbero essere utilizzati e inclusi nel tuo prodotto
  • Model Optimzer, grazie al quale è possibile convertire un modello da vari formati framework (Tensorflow, ONNX ecc.) nel formato di rappresentazione intermedia, con il quale lavoreremo ulteriormente
  • Inference Engine consente di eseguire modelli in formato IR su processori Intel, chip Myriad e acceleratori Neural Compute Stick
  • La versione più efficiente di OpenCV (con supporto Inference Engine)
    Ogni modello in formato IR è descritto da due file: .xml e .bin.
    I modelli vengono convertiti in formato IR tramite Model Optimizer come segue:

    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 consente di selezionare il formato dati con cui funzionerà il modello. Sono supportati FP32, FP16, INT8. La scelta del tipo di dati ottimale può dare un buon incremento delle prestazioni.
    --input_shape indica la dimensione dei dati di input. La possibilità di modificarlo dinamicamente sembra essere presente nell'API C++, ma non abbiamo approfondito il problema e l'abbiamo semplicemente risolto per uno dei modelli.
    Successivamente, proviamo a caricare il modello già convertito in formato IR tramite il modulo DNN in OpenCV e ad inoltrarlo ad esso.

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

    L'ultima riga in questo caso ti permette di reindirizzare i calcoli al Neural Compute Stick, i calcoli di base vengono eseguiti sul processore, ma nel caso del Raspberry Pi questo non funzionerà, avrai bisogno di uno stick.

    Successivamente, la logica è la seguente: dividiamo il nostro audio in finestre di una certa dimensione (per noi è 0.4 s), convertiamo ciascuna di queste finestre in MFCC, che poi inseriamo nella griglia:

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

    Successivamente, prendiamo la classe più comune per tutte le finestre. Una soluzione semplice, ma per un hackathon non è necessario inventare qualcosa di troppo astruso, solo se si ha tempo. Abbiamo ancora molto lavoro da fare, quindi andiamo avanti: ci occuperemo del riconoscimento vocale. È necessario creare una sorta di database in cui archiviare gli spettrogrammi delle voci preregistrate. Poiché manca poco tempo, risolveremo il problema nel miglior modo possibile.

    Creiamo cioè uno script per registrare un brano vocale (funziona allo stesso modo descritto sopra, solo se interrotto dalla tastiera salverà la voce in un file).

    Proviamo:

    python3 voice_db/record_voice.py test.wav

    Registriamo le voci di più persone (nel nostro caso, tre membri del team)
    Successivamente, per ogni voce registrata eseguiamo una trasformata veloce di Fourier, otteniamo uno spettrogramma e lo salviamo come array numpy (.npy):

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

    Maggiori dettagli nel dossier create_base.py
    Di conseguenza, quando eseguiamo lo script principale, otterremo fin dall'inizio gli incorporamenti di questi spettrogrammi:

    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)

    Dopo aver ricevuto l'incorporamento del segmento sondato, potremo determinare a chi appartiene prendendo la distanza coseno dal passaggio a tutte le voci nel database (più piccolo, più probabile) - per la demo impostiamo la soglia a 0.3):

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

    Alla fine, vorrei sottolineare che la velocità di inferenza è stata elevata e ha permesso di aggiungere 1-2 modelli in più (per un campione di 7 secondi ne sono voluti 2.5 per l'inferenza). Non abbiamo più avuto tempo per aggiungere nuovi modelli e ci siamo concentrati sulla scrittura di un prototipo dell'applicazione web.

    applicazione web

    Un punto importante: portiamo con noi un router da casa e configuriamo la nostra rete locale, aiuta a connettere il dispositivo e i laptop tramite la rete.

    Il backend è un canale di messaggi end-to-end tra il front e Raspberry Pi, basato sulla tecnologia websocket (protocollo http su tcp).

    La prima fase consiste nel ricevere informazioni elaborate da raspberry, ovvero predittori impacchettati in json, che vengono salvati nel database a metà del loro viaggio in modo che si possano generare statistiche sul background emotivo dell’utente per quel periodo. Questo pacchetto viene quindi inviato al frontend, che utilizza l'abbonamento e riceve pacchetti dall'endpoint websocket. L'intero meccanismo di backend è costruito nel linguaggio golang; è stato scelto perché è adatto per attività asincrone, che le goroutine gestiscono bene.
    Quando si accede all'endpoint, l'utente viene registrato ed inserito nella struttura, quindi viene ricevuto il suo messaggio. Sia l'utente che il messaggio vengono inseriti in un hub comune, dal quale i messaggi vengono già inviati ulteriormente (al fronte sottoscritto) e se l'utente chiude la connessione (raspberry o front), il suo abbonamento viene annullato e viene rimosso da il mozzo.

    OpenVINO hackathon: riconoscere voce ed emozioni su Raspberry Pi
    Stiamo aspettando una connessione dal retro

    Il front-end è un'applicazione web scritta in JavaScript utilizzando la libreria React per accelerare e semplificare il processo di sviluppo. Lo scopo di questa applicazione è visualizzare i dati ottenuti utilizzando algoritmi in esecuzione sul lato back-end e direttamente sul Raspberry Pi. La pagina ha un routing sezionale implementato utilizzando react-router, ma la pagina principale di interesse è la pagina principale, dove un flusso continuo di dati viene ricevuto in tempo reale dal server utilizzando la tecnologia WebSocket. Raspberry Pi rileva una voce, determina se appartiene a una persona specifica dal database registrato e invia un elenco di probabilità al client. Il client visualizza gli ultimi dati rilevanti, mostra l'avatar della persona che molto probabilmente ha parlato al microfono, nonché l'emozione con cui pronuncia le parole.

    OpenVINO hackathon: riconoscere voce ed emozioni su Raspberry Pi
    Home page con pronostici aggiornati

    conclusione

    Non è stato possibile completare tutto come previsto, semplicemente non avevamo tempo, quindi la speranza principale era nella demo, che tutto funzionasse. Nella presentazione hanno parlato di come funziona il tutto, quali modelli hanno preso, quali problemi hanno riscontrato. Successivamente è stata la parte dimostrativa: gli esperti hanno camminato per la stanza in ordine casuale e si sono avvicinati a ciascun team per osservare il prototipo funzionante. Anche a noi hanno fatto domande, ognuno ha risposto la sua parte, hanno lasciato il web sul portatile, e tutto ha funzionato davvero come previsto.

    Vorrei notare che il costo totale della nostra soluzione era di $ 150:

    • Lampone Pi 3 ~ $ 35
    • Google AIY Voice Bonnet (puoi richiedere una tariffa per il respeaker) ~ 15 $
    • Intel NCS 2 ~ 100 $

    Come migliorare:

    • Utilizza la registrazione dal client: chiedi di leggere il testo generato in modo casuale
    • Aggiungi qualche altro modello: puoi determinare sesso ed età con la voce
    • Separare le voci che suonano simultaneamente (diarizzazione)

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

    OpenVINO hackathon: riconoscere voce ed emozioni su Raspberry Pi
    Stanchi ma felici siamo

    In conclusione desidero ringraziare gli organizzatori e i partecipanti. Tra i progetti di altri team, personalmente ci è piaciuta la soluzione per il monitoraggio dei parcheggi liberi. Per noi è stata un'esperienza davvero fantastica di immersione nel prodotto e nello sviluppo. Spero che nelle regioni si svolgano eventi sempre più interessanti, anche sui temi dell'IA.

Fonte: habr.com

Aggiungi un commento