Hackathon OpenVINO: rozpoznawanie głosu i emocji na Raspberry Pi

30 listopada - 1 grudnia w Niżnym Nowogrodzie odbył się Hackaton OpenVINO. Uczestnicy zostali poproszeni o stworzenie prototypu rozwiązania produktowego z wykorzystaniem zestawu narzędzi Intel OpenVINO. Organizatorzy zaproponowali listę przybliżonych tematów, którymi można było się kierować przy wyborze zadania, ale ostateczna decyzja pozostała w gestii zespołów. Ponadto zachęcano do stosowania modeli, które nie są zawarte w produkcie.

Hackathon OpenVINO: rozpoznawanie głosu i emocji na Raspberry Pi

W tym artykule opowiemy Wam o tym, jak stworzyliśmy nasz prototyp produktu, z którym ostatecznie zajęliśmy pierwsze miejsce.

W hackatonie wzięło udział ponad 10 drużyn. Miło, że część z nich przyjechała z innych regionów. Miejscem hackathonu był kompleks „Kremlinski na Połajnie”, w którym w otoczeniu wisiały starożytne fotografie Niżnego Nowogrodu! (Przypominam, że w tej chwili centrala Intela znajduje się w Niżnym Nowogrodzie). Uczestnicy mieli 26 godzin na napisanie kodu, a na koniec musieli zaprezentować swoje rozwiązanie. Osobną zaletą była obecność sesji demonstracyjnej, która pozwalała mieć pewność, że wszystko, co zaplanowano, zostało faktycznie wdrożone i nie pozostało pomysłami w prezentacji. Towary, przekąski, jedzenie, wszystko też tam było!

Dodatkowo Intel opcjonalnie dostarczał kamery, Raspberry PI, Neural Compute Stick 2.

Wybór zadań

Jedną z najtrudniejszych części przygotowań do hackatonu swobodnego jest wybór wyzwania. Od razu postanowiliśmy wymyślić coś, czego jeszcze nie było w produkcie, ponieważ w ogłoszeniu było napisane, że jest to bardzo mile widziane.

Po przeanalizowaniu modele, które są zawarte w produkcie w aktualnej wersji, dochodzimy do wniosku, że większość z nich rozwiązuje różne problemy z widzeniem komputerowym. Co więcej, bardzo trudno jest wymyślić problem z zakresu widzenia komputerowego, którego nie da się rozwiązać za pomocą OpenVINO, a nawet jeśli da się go wymyślić, to trudno znaleźć w domenie publicznej gotowe modele. Decydujemy się obrać inny kierunek – w stronę przetwarzania mowy i analityki. Rozważmy ciekawe zadanie rozpoznawania emocji na podstawie mowy. Trzeba powiedzieć, że OpenVINO ma już model, który określa emocje człowieka na podstawie jego twarzy, ale:

  • Teoretycznie możliwe jest stworzenie kombinowanego algorytmu, który będzie działał zarówno na dźwięk, jak i na obraz, co powinno dać wzrost dokładności.
  • Kamery mają zazwyczaj wąski kąt widzenia, do pokrycia dużego obszaru wymagana jest więcej niż jedna kamera, a dźwięk nie ma takiego ograniczenia.

Rozwijajmy pomysł: za podstawę weźmy pomysł na segment detaliczny. Możesz mierzyć satysfakcję klientów przy kasach sklepowych. Jeśli któryś z klientów będzie niezadowolony z obsługi i zacznie podnosić ton, możesz od razu wezwać administratora po pomoc.
W tym przypadku musimy dodać rozpoznawanie ludzkiego głosu, pozwoli nam to odróżnić pracowników sklepu od klientów i zapewnić analitykę dla każdego z osobna. No cóż, w dodatku będzie można przeanalizować zachowanie samych pracowników sklepu, ocenić atmosferę w zespole, brzmi nieźle!

Formułujemy wymagania dla naszego rozwiązania:

  • Mały rozmiar urządzenia docelowego
  • Działanie w czasie rzeczywistym
  • Niska cena
  • Łatwa skalowalność

W rezultacie jako urządzenie docelowe wybieramy Raspberry Pi 3 c IntelNCS2.

W tym miejscu należy zwrócić uwagę na jedną ważną cechę NCS - najlepiej działa ze standardowymi architekturami CNN, ale jeśli chcesz uruchomić model z niestandardowymi warstwami, spodziewaj się optymalizacji na niskim poziomie.

Jest tylko jedna mała rzecz do zrobienia: musisz zaopatrzyć się w mikrofon. Zwykły mikrofon USB wystarczy, ale w połączeniu z RPI nie będzie dobrze wyglądać. Ale nawet tutaj rozwiązanie dosłownie „leży w pobliżu”. Do nagrywania głosu decydujemy się wykorzystać znajdującą się w zestawie płytkę Voice Bonnet Zestaw głosowy Google AIY, na którym znajduje się przewodowy mikrofon stereofoniczny.

Pobierz Raspbian z Repozytorium projektów AIY i wgraj go na pendrive, przetestuj działanie mikrofonu za pomocą poniższego polecenia (nagra dźwięk o długości 5 sekund i zapisze go do pliku):

arecord -d 5 -r 16000 test.wav

Od razu zaznaczę, że mikrofon jest bardzo czuły i dobrze zbiera hałas. Aby to naprawić, przejdźmy do alsamixer, wybierz Przechwytuj urządzenia i zmniejsz poziom sygnału wejściowego do 50-60%.

Hackathon OpenVINO: rozpoznawanie głosu i emocji na Raspberry Pi
Modyfikujemy korpus pilnikiem i wszystko pasuje, można nawet zamknąć pokrywką

Dodanie przycisku wskaźnika

Rozbierając AIY Voice Kit pamiętamy, że znajduje się tam przycisk RGB, którego podświetleniem można sterować programowo. Wyszukujemy „Google AIY Led” i znajdujemy dokumentację: https://aiyprojects.readthedocs.io/en/latest/aiy.leds.html
Dlaczego nie użyć tego przycisku do wyświetlenia rozpoznanej emocji, mamy tylko 7 klas, a przycisk ma 8 kolorów, w zupełności wystarczy!

Łączymy przycisk poprzez GPIO z Voice Bonnet, ładujemy niezbędne biblioteki (są już zainstalowane w pakiecie dystrybucyjnym z projektów AIY)

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

Stwórzmy dyktat, w którym każda emocja będzie miała odpowiedni kolor w postaci krotki RGB i obiektu klasy aiy.leds.Leds, za pomocą którego zaktualizujemy kolor:

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 na koniec, po każdej nowej prognozie emocji, zaktualizujemy kolor przycisku zgodnie z nią (według klucza).

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

Hackathon OpenVINO: rozpoznawanie głosu i emocji na Raspberry Pi
Przycisk, spal!

Praca z głosem

Użyjemy pyaudio do przechwytywania strumienia z mikrofonu i webrtcvad do filtrowania szumów i wykrywania głosu. Dodatkowo utworzymy kolejkę, do której będziemy asynchronicznie dodawać i usuwać fragmenty głosu.

Ponieważ webrtcvad ma ograniczenie wielkości dostarczanego fragmentu - musi on wynosić 10/20/30ms, a szkolenie modelu rozpoznawania emocji (o czym dowiemy się później) zostało przeprowadzone na zbiorze danych 48 kHz, będziemy przechwytuje fragmenty o rozmiarze 48000×20ms/1000×1(mono)=960 bajtów. Webrtcvad zwróci wartość True/False dla każdej z tych porcji, co odpowiada obecności lub braku głosu w tej porcji.

Zastosujmy następującą logikę:

  • Do listy dodamy te kawałki, na które jest głosowanie, a jeśli nie ma głosowania, to zwiększamy licznik pustych fragmentów.
  • Jeśli licznik pustych porcji wynosi >=30 (600 ms), to patrzymy na wielkość listy zgromadzonych porcji; jeśli jest >250, to dodajemy ją do kolejki; jeśli nie, uważamy, że długość zapisu nie wystarczy, aby przekazać go modelowi w celu zidentyfikowania mówiącego.
  • Jeśli licznik pustych porcji nadal wynosi < 30, a rozmiar listy skumulowanych porcji przekracza 300, wówczas dodamy fragment do kolejki w celu dokładniejszej predykcji. (ponieważ emocje mają tendencję do zmiany się w czasie)

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

Czas poszukać wstępnie wytrenowanych modeli w domenie publicznej, wejść na github, Google, ale pamiętajmy, że mamy ograniczenia co do zastosowanej architektury. To dość trudna część, gdyż trzeba przetestować modele na danych wejściowych, a dodatkowo przekonwertować je do wewnętrznego formatu OpenVINO – IR (ang. Intermediate Representation). Wypróbowaliśmy około 5-7 różnych rozwiązań z githuba i jeśli model rozpoznawania emocji zadziałał od razu, to z rozpoznawaniem głosu trzeba było poczekać dłużej - korzystają z bardziej złożonych architektur.

Koncentrujemy się na następujących kwestiach:

Następnie porozmawiamy o konwertowaniu modeli, zaczynając od teorii. OpenVINO zawiera kilka modułów:

  • Open Model Zoo, z których modele można wykorzystać i włączyć do swojego produktu
  • Model Optimzer, dzięki któremu możesz konwertować model z różnych formatów frameworków (Tensorflow, ONNX itp.) do formatu Intermediate Representation, z którym będziemy dalej pracować
  • Inference Engine umożliwia uruchamianie modeli w formacie IR na procesorach Intel, chipach Myriad i akceleratorach Neural Compute Stick
  • Najbardziej wydajna wersja OpenCV (z obsługą Inference Engine)
    Każdy model w formacie IR opisany jest dwoma plikami: .xml i .bin.
    Modele są konwertowane do formatu IR za pomocą narzędzia Model Optimizer w następujący sposób:

    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 pozwala wybrać format danych, z jakim będzie współpracował model. Obsługiwane są FP32, FP16, INT8. Wybór optymalnego typu danych może znacznie zwiększyć wydajność.
    --input_shape wskazuje wymiar danych wejściowych. Możliwość dynamicznej zmiany wydaje się być obecna w API C++, ale nie sięgaliśmy aż tak daleko i po prostu naprawiliśmy to dla jednego z modeli.
    Następnie spróbujmy załadować już przekonwertowany model w formacie IR poprzez moduł DNN do OpenCV i przesłać go do niego.

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

    Ostatnia linijka w tym przypadku pozwala na przekierowanie obliczeń do Neural Compute Stick, podstawowe obliczenia wykonywane są na procesorze, jednak w przypadku Raspberry Pi to nie zadziała, potrzebny będzie kij.

    Następnie logika jest następująca: dzielimy nasze audio na okna o określonej wielkości (u nas jest to 0.4 s), każde z tych okien konwertujemy na MFCC, które następnie wrzucamy do siatki:

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

    Następnie weźmy najczęstszą klasę dla wszystkich okien. Proste rozwiązanie, ale na hackaton nie musisz wymyślać czegoś zbyt zawiłego, tylko jeśli masz czas. Przed nami jeszcze sporo pracy, więc przejdźmy dalej – zajmiemy się rozpoznawaniem głosu. Konieczne jest stworzenie pewnego rodzaju bazy danych, w której przechowywane będą spektrogramy wcześniej zarejestrowanych głosów. Ponieważ pozostało niewiele czasu, rozwiążemy ten problem najlepiej jak potrafimy.

    Mianowicie tworzymy skrypt umożliwiający nagranie fragmentu głosu (działa to analogicznie jak opisano powyżej, jedynie przerwane z klawiatury zapisze głos do pliku).

    Spróbujmy:

    python3 voice_db/record_voice.py test.wav

    Nagrywamy głosy kilku osób (w naszym przypadku trzech członków zespołu)
    Następnie dla każdego nagranego głosu wykonujemy szybką transformatę Fouriera, uzyskujemy spektrogram i zapisujemy go jako tablicę numpy (.npy):

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

    Więcej szczegółów w pliku create_base.py
    W rezultacie, gdy uruchomimy główny skrypt, już na początku otrzymamy osady z tych spektrogramów:

    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)

    Po otrzymaniu osadzania z dźwięcznego segmentu będziemy mogli określić do kogo on należy, biorąc cosinusową odległość od przejścia do wszystkich głosów w bazie (im mniejsza, tym bardziej prawdopodobne) - dla demo ustalamy próg do 0.3):

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

    Na koniec chciałbym zauważyć, że szybkość wnioskowania była duża i umożliwiła dodanie 1-2 modeli więcej (dla próbki o długości 7 sekund wnioskowanie zajęło 2.5). Nie mieliśmy już czasu na dodawanie nowych modeli i skupiliśmy się na pisaniu prototypu aplikacji webowej.

    Aplikacja internetowa

    Ważna kwestia: zabieramy ze sobą router z domu i konfigurujemy naszą sieć lokalną, pomaga to w podłączeniu urządzenia i laptopów przez sieć.

    Backend to kompleksowy kanał komunikatów pomiędzy frontem a Raspberry Pi, oparty na technologii websocket (protokół http przez tcp).

    Pierwszym etapem jest otrzymanie przetworzonej informacji z Raspberry, czyli predyktorów spakowanych w formacie json, które w połowie podróży zapisywane są w bazie danych, dzięki czemu można wygenerować statystyki dotyczące tła emocjonalnego użytkownika za dany okres. Pakiet ten jest następnie wysyłany do frontendu, który korzysta z subskrypcji i odbiera pakiety z punktu końcowego websocket. Cały mechanizm backendowy jest zbudowany w języku golang, wybrano go dlatego, że dobrze radzi sobie z zadaniami asynchronicznymi, z którymi dobrze radzą sobie goroutines.
    Podczas uzyskiwania dostępu do punktu końcowego użytkownik jest rejestrowany i wprowadzany do struktury, po czym odbierany jest jego komunikat. Zarówno użytkownik, jak i wiadomość są wprowadzane do wspólnego huba, z którego wiadomości są już wysyłane dalej (na subskrybowany front), a jeśli użytkownik zamknie połączenie (malinowe lub frontowe), to jego subskrypcja zostaje anulowana i zostaje usunięty z Centrum.

    Hackathon OpenVINO: rozpoznawanie głosu i emocji na Raspberry Pi
    Czekamy na połączenie od tyłu

    Front-end to aplikacja internetowa napisana w języku JavaScript wykorzystująca bibliotekę React w celu przyspieszenia i uproszczenia procesu rozwoju. Celem tej aplikacji jest wizualizacja danych uzyskanych za pomocą algorytmów działających po stronie back-endu oraz bezpośrednio na Raspberry Pi. Strona ma routing sekcyjny zaimplementowany przy użyciu routera React-Router, ale stroną główną, która nas interesuje, jest strona główna, na której odbierany jest ciągły strumień danych w czasie rzeczywistym z serwera za pomocą technologii WebSocket. Raspberry Pi wykrywa głos, z zarejestrowanej bazy określa, czy należy on do konkretnej osoby, i wysyła do klienta listę prawdopodobieństw. Klient wyświetla najnowsze istotne dane, wyświetla awatar osoby, która najprawdopodobniej przemówiła do mikrofonu, a także emocję, z jaką wymawia słowa.

    Hackathon OpenVINO: rozpoznawanie głosu i emocji na Raspberry Pi
    Strona główna ze zaktualizowanymi prognozami

    wniosek

    Nie udało się ukończyć wszystkiego zgodnie z planem, po prostu nie mieliśmy czasu, więc główna nadzieja była w wersji demonstracyjnej, że wszystko będzie działać. W prezentacji opowiadali o tym jak wszystko działa, jakie modele wzięli, z jakimi problemami się spotkali. Następnie odbyła się część demonstracyjna – eksperci chodzili po sali w losowej kolejności i podchodzili do każdego zespołu, aby obejrzeć działający prototyp. Zadawali nam też pytania, wszyscy odpowiadali na swoje pytania, opuszczali sieć na laptopie i wszystko naprawdę działało zgodnie z oczekiwaniami.

    Dodam, że całkowity koszt naszego rozwiązania wyniósł 150 dolarów:

    • Raspberry Pi 3 ~ 35 dolarów
    • Google AIY Voice Bonnet (można pobrać opłatę za respeakera) ~ 15 $
    • Intel NCS 2 ~ 100 dolarów

    Jak polepszyć:

    • Skorzystaj z rejestracji od klienta - poproś o przeczytanie losowo generowanego tekstu
    • Dodaj jeszcze kilka modeli: możesz określić płeć i wiek za pomocą głosu
    • Oddzielne, jednocześnie brzmiące głosy (diaryzacja)

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

    Hackathon OpenVINO: rozpoznawanie głosu i emocji na Raspberry Pi
    Jesteśmy zmęczeni, ale szczęśliwi

    Na zakończenie chciałbym podziękować organizatorom i uczestnikom. Spośród projektów innych zespołów, nam osobiście spodobało się rozwiązanie monitorowania wolnych miejsc parkingowych. Dla nas było to szalenie fajne doświadczenie zanurzenia się w produkcie i jego rozwoju. Mam nadzieję, że w regionach będzie odbywać się coraz więcej ciekawych wydarzeń, w tym poświęconych tematyce AI.

Źródło: www.habr.com

Dodaj komentarz