Hackathon OpenVINO: riconosciamo la voce e le emozioni su Raspberry Pi

Dal 30 novembre al 1 dicembre si è svolto a Nizhny Novgorod l'hackathon OpenVINO. Ai partecipanti è stato chiesto di creare un prototipo di soluzione di prodotto utilizzando Intel OpenVINO toolkit. Gli organizzatori hanno fornito un elenco di temi indicativi su cui orientarsi nella scelta della sfida, ma la decisione finale è rimasta a carico dei team. Inoltre, è stato incoraggiato l'uso di modelli che non rientrano nel prodotto.

Hackathon OpenVINO: riconosciamo la voce e le emozioni su Raspberry Pi

Nell'articolo parleremo di come abbiamo creato il nostro prototipo di prodotto, con cui alla fine abbiamo vinto il primo posto.

Al hackathon hanno partecipato oltre 10 team. È bello che alcuni di loro siano venuti da altre regioni. La sede del hackathon è stata scelta nel complesso “Kremlevsky na Pochaine”, dove erano appese fotografie storiche di Nizhny Novgorod, creando un'atmosfera suggestiva! (ricordo che attualmente la sede centrale dell'azienda Intel si trova proprio a Nizhny Novgorod). I partecipanti avevano a disposizione 26 ore per scrivere il codice, al termine delle quali dovevano presentare la propria soluzione. Un ulteriore vantaggio era la presenza di una demo session per assicurarsi che tutto ciò che era stato concepito fosse realmente implementato e non rimanesse solo un'idea nella presentazione. C'era anche merce, snack e cibo!

Inoltre, l'azienda Intel forniva, su richiesta, telecamere, Raspberry PI e Neural Compute Stick 2.

Scelta del compito

Una delle parti più difficili nella preparazione di un hackathon con tema libero è la scelta del compito. Ci si è subito messi a pensare a qualcosa che nel prodotto non esiste ancora, poiché nell'annuncio era stato specificato che questo è fortemente incoraggiato.

Dopo aver analizzato modelli, che rientrano nel prodotto nella versione attuale, giungiamo alla conclusione che la maggior parte di essi risolve vari problemi di visione artificiale. È molto difficile pensare a un problema nel campo della visione artificiale che non possa essere risolto utilizzando OpenVINO, e se esiste, è difficile trovare modelli pre-addestrati disponibili pubblicamente. Possiamo anche esplorare un'altra direzione — l'elaborazione e l'analisi del linguaggio. Stiamo considerando un'interessante sfida nel riconoscimento delle emozioni dalla voce. Va detto che in OpenVINO esiste già un modello che determina le emozioni di una persona attraverso il volto, ma:

  • In teoria, si potrebbe creare un algoritmo combinato che funzioni sia con il suono che con l'immagine, il che dovrebbe portare a un aumento della precisione.
  • Le telecamere di solito hanno un angolo di visione ristretto; per coprire una grande area, serve più di una telecamera, mentre il suono non ha tale limitazione.

Sviluppiamo l'idea: prendiamo come riferimento il settore retail. Si può determinare la soddisfazione del cliente alle casse dei negozi. Se uno dei clienti è insoddisfatto del servizio e inizia a alzare la voce, si può subito chiamare un amministratore per assistenza.
In questo caso, è necessario aggiungere il riconoscimento vocale umano, in modo da poter differenziare i dipendenti del negozio dai clienti e fornire analisi per ciascun individuo. Inoltre, sarà possibile analizzare il comportamento stesso dei dipendenti del negozio e valutare l'atmosfera nel team, sembra interessante!

Definiamo i requisiti per la nostra soluzione:

  • Dimensioni ridotte del dispositivo target
  • Funzionamento in tempo reale
  • Prezzo contenuto
  • Facile scalabilità

Alla fine, scegliamo come dispositivo target il Raspberry Pi 3 con Intel NCS 2.

È importante sottolineare una caratteristica fondamentale del NCS: funziona al meglio con architetture CNN standard; se necessiti di eseguire un modello con strati personalizzati, preparati a quelle che saranno ottimizzazioni a basso livello.

Ora rimane poco da fare: dobbiamo procurare un microfono. Va bene anche un normale microfono USB, anche se non si abbinerà bene con RPI. Ma anche qui la soluzione è praticamente a portata di mano. Per la registrazione della voce, decidiamo di utilizzare la scheda Voice Bonnet dal kit Google AIY Voice Kit, che ha un microfono stereo saldato.

Scarichiamo Raspbian da repository AIY projects e salviamo su una penna USB, testiamo che il microfono funzioni con il seguente comando (registrerà audio per 5 secondi e lo salverà in un file):

arecord -d 5 -r 16000 test.wav

Voglio sottolineare che il microfono è molto sensibile e cattura bene i rumori. Per migliorare questo, entreremo in alsamixer, selezioneremo i dispositivi di cattura e ridurremo il livello del segnale di ingresso al 50-60%.

Hackathon OpenVINO: riconosciamo la voce e le emozioni su Raspberry Pi
Modifichiamo il case con una limetta e tutto entra, possiamo anche chiudere il coperchio.

Aggiungiamo un pulsante indicatore.

Durante lo smontaggio dell'AIY Voice Kit, ricordo che c'è un pulsante RGB la cui retroilluminazione può essere controllata programmaticamente. 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, mentre il pulsante ha 8 colori, proprio sufficienti!

Colleghiamo il pulsante tramite GPIO al Voice Bonnet, carichiamo le librerie necessarie (sono già installate nella distribuzione di AIY projects).

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

Creeremo un dizionario nel quale ogni emozione sarà associata a un colore sotto forma di Tupla RGB e a 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()

Infine, dopo ogni nuova previsione dell'emozione, aggiorneremo il colore del pulsante secondo essa (in base alla chiave).

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

Hackathon OpenVINO: riconosciamo la voce e le emozioni su Raspberry Pi
Pulsante, accendi!

Lavoriamo con la voce

Utilizzeremo pyaudio per catturare il flusso dal microfono e webrtcvad per filtrare il rumore e rilevare la voce. Inoltre, creeremo una coda in cui aggiungere e prelevare in modo asincrono frammenti vocali.

Poiché webrtcvad ha una limitazione sulla dimensione del frammento fornito — deve essere di 10/20/30 ms, e l'addestramento del modello per riconoscere le emozioni (come scopriremo più avanti) è stato effettuato su un dataset a 48 kHz, cattureremo chunk di dimensioni 48000×20 ms/1000×1 (mono) = 960 byte. Webrtcvad restituirà True/False per ciascuno di questi chunk, che indica la presenza o l'assenza di voce nel chunk.

Implementeremo la seguente logica:

  • Aggiungeremo a list quei chunk in cui c'è voce; se non c'è voce, incrementeremo il contatore dei chunk vuoti.
  • Se il contatore dei chunk vuoti è >=30 (600 ms), controlliamo la dimensione della lista dei chunk accumulati; se è >250, allora lo aggiungiamo in coda, altrimenti consideriamo che la lunghezza della registrazione non sia sufficiente per inviarla al modello per l'identificazione del parlante.
  • Se il contatore dei chunk vuoti è ancora < 30 e la dimensione della lista dei chunk accumulati ha superato 300, aggiungeremo il frammento in coda per una previsione più accurata. (poiché nel tempo le emozioni tendono a cambiare)

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

È tempo di cercare modelli pre-addestrati disponibili pubblicamente: andiamo su GitHub, facciamo una ricerca su Google, ma ricordiamo che abbiamo delle limitazioni riguardo all'architettura da utilizzare. Questa è una parte piuttosto complessa, poiché dobbiamo testare i modelli con i nostri dati di input e, inoltre, convertirli nel formato interno OpenVINO — IR (Rappresentazione Intermedia). Abbiamo provato circa 5-7 soluzioni diverse da GitHub e, sebbene il modello per il riconoscimento delle emozioni funzionasse subito, per il riconoscimento vocale abbiamo dovuto investire più tempo — qui si utilizzano architetture più complesse.

Ci concentriamo sui seguenti:

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

  • Open Model Zoo, i modelli del quale possono essere utilizzati e inclusi nel proprio prodotto
  • Model Optimizer, che consente di riconvertire il modello da vari formati di framework (Tensorflow, ONNX, ecc.) in formato Intermediate Representation, con il quale lavoreremo successivamente.
  • Inference Engine permette di eseguire modelli nel formato IR su processori Intel, chip Myriad e acceleratori Neural Compute Stick.
  • La versione più efficace di OpenCV (con supporto per Inference Engine).
    Ogni modello nel formato IR è descritto da due file: .xml e .bin.
    I modelli vengono convertiti nel formato IR tramite Model Optimizer nel seguente modo:
    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 permette di scegliere il formato dei dati con cui il modello lavorerà. Sono supportati FP32, FP16, INT8. La scelta del tipo di dati ottimale può portare a un buon incremento delle prestazioni.
    --input_shape indica la dimensione delle informazioni in ingresso. La possibilità di modificarla dinamicamente sembra essere presente nell'API C++, ma non ci siamo avventurati così lontano e per uno dei modelli l'abbiamo semplicemente fissata.
    Successivamente, proveremo a caricare il modello già convertito nel formato IR tramite il modulo DNN in OpenCV e faremo un forward su di 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 permette di reindirizzare i calcoli su Neural Compute Stick, di base i calcoli vengono eseguiti sulla CPU, ma nel caso di Raspberry Pi non funzionerà, sarà necessario un stick.

    La logica successiva è la seguente: suddivideremo il nostro audio in finestre di dimensioni definite (nel nostro caso 0.4s), ciascuna di queste finestre sarà convertita in MFCC, che poi passeremo alla rete:

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

    Poi prenderemo la classe più frequente per tutte le finestre. È una soluzione semplice, ma per un hackathon non è necessario inventare qualcosa di troppo complicato, a meno che non ci sia tempo. Abbiamo ancora molto lavoro, quindi andiamo avanti — ci occupiamo del riconoscimento vocale. Dobbiamo creare una sorta di database in cui siano memorizzate le spettrogrammi delle voci registrate in precedenza. Poiché il tempo è scarso, affrontiamo questo problema come possiamo.

    Cioè, stiamo creando uno script per registrare un estratto della voce (funziona come descritto sopra, solo che quando viene 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 la fast fourier transform, otteniamo la spettrogramma e salviamo come array numpy (.npy):

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

    Ulteriori dettagli nel file create_base.py
    Alla fine, quando eseguiamo lo script principale, otteniamo all'inizio gli embedding da 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 ottenuto l'embedding del segmento che ha suonato, possiamo determinare a chi appartiene, prendendo la distanza coseno dal frammento a tutte le voci nella base (più è piccola, più è probabile) — per la demo abbiamo impostato una soglia di 0.3):

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

    Infine, segnalo che la velocità di inferenza è stata rapida e ha permesso di aggiungere un'altra 1-2 modelli (per un campione della durata di 7 secondi l'inferenza durava 2.5). Non siamo riusciti ad aggiungere nuovi modelli e ci siamo concentrati sulla scrittura del prototipo dell'app web.

    App web

    Punto importante: portiamo con noi il router da casa e configuriamo la nostra rete locale, utile per connettere dispositivi e laptop in rete.

    Il backend funge da canale di comunicazione tra il frontend e il Raspberry Pi, basato sulla tecnologia websocket ( protocollo http su tcp).

    La prima fase consiste nell'ottenere informazioni elaborate dal Raspberry Pi, ovvero predizioni imballate in JSON, che a metà del loro percorso vengono salvate nel database per poter formare statistiche sullo stato emotivo dell'utente nel tempo. Successivamente, questo pacchetto viene inviato al frontend, che utilizza iscrizioni per ricevere pacchetti dall'endpoint websocket. L'intero meccanismo del backend è costruito nel linguaggio Golang, scelto perché adatto a compiti asincroni, per cui le goroutine funzionano molto bene.
    Quando si accede all'endpoint, l'utente viene registrato e inserito nella struttura, quindi avviene la ricezione del suo messaggio. Sia l'utente che il messaggio vengono inseriti in un hub comune, da cui i messaggi vengono poi inviati ulteriormente (al front-end sottoscritto); se l'utente chiude la connessione (Raspberry o front-end), la sua sottoscrizione viene annullata e viene rimosso dall'hub.

    Hackathon OpenVINO: riconosciamo la voce e le emozioni su Raspberry Pi
    In attesa di connessione con il backend

    Il front-end è un'applicazione web scritta in JavaScript, utilizzando la libreria React per accelerare e semplificare il processo di sviluppo. L'obiettivo di questa applicazione è la visualizzazione dei dati ottenuti tramite algoritmi eseguiti sul lato back-end e direttamente su Raspberry Pi. La pagina presenta il routing delle sezioni, implementato tramite react-router, ma l'interesse principale è per la home page, dove arriva un flusso continuo di dati dal server in tempo reale tramite tecnologia WebSocket. Raspberry Pi rileva la voce, determina a quale persona registrata appartiene e invia un elenco di probabilità al client. Il client visualizza gli ultimi dati aggiornati, mostra l'avatar della persona che ha maggiore probabilità di aver parlato al microfono, così come l'emozione con cui pronuncia le parole.

    Hackathon OpenVINO: riconosciamo la voce e le emozioni su Raspberry Pi
    La home page con previsioni aggiornabili

    Conclusione

    Non siamo riusciti a completare tutto come previsto, banalmente non ci siamo riusciti in tempo, quindi la nostra principale speranza era sul demo, che tutto avrebbe funzionato. Durante la presentazione hanno spiegato come è strutturato tutto, quali modelli hanno utilizzato e quali problemi hanno affrontato. Successivamente c'è stata una parte di demo: gli esperti si muovevano per la sala in modo casuale e si avvicinavano a ciascun team per vedere il prototipo funzionante. Facevano domande anche a noi, ognuno rispondeva per la propria parte, sul laptop abbiamo lasciato il web, e tutto funzionava davvero come ci si aspettava.

    Segnalo che il costo totale della nostra soluzione è stato di 150$:

    • Raspberry Pi 3 ~ 35$
    • Google AIY Voice Bonnet (puoi utilizzare la scheda respeaker) ~ 15$
    • Intel NCS 2 ~ 100$

    Come migliorare:

    • Utilizzare la registrazione dal client — chiedere di leggere il testo che generiamo casualmente
    • Aggiungere ancora alcuni modelli: si può determinare sesso e età dalla voce
    • Separare voci che suonano contemporaneamente (diarizzazione)

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

    Hackathon OpenVINO: riconosciamo la voce e le emozioni su Raspberry Pi
    Stanchi, ma felici

    In conclusione, vogliamo ringraziare gli organizzatori e i partecipanti. Tra i progetti di altre squadre, a noi è piaciuta particolarmente la soluzione per il monitoraggio dei posti auto disponibili. È stata per noi un'esperienza davvero fantastica nel immergerci nel prodotto e nella sua sviluppo. Speriamo che in altre regioni si tengano sempre più eventi interessanti, compresi quelli legati all'AI.

Fonte: habr.com

Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server 🔥 Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server | ProHoster