OpenVINO Hackathon: Sprache und Emotionen auf Raspberry Pi erkennen

Vom 30. November bis 1. Dezember fand in Nischni Nowgorod der OpenVINO-Hackathonstatt. Den Teilnehmern wurde angeboten, einen Prototyp für eine Produktlösung unter Verwendung des Intel OpenVINO Toolkits zu entwickeln. Die Organisatoren hatten eine Liste mit Beispielthemen vorgeschlagen, an denen man sich bei der Themenwahl orientieren konnte, aber die endgültige Entscheidung lag bei den Teams. Zudem wurde die Nutzung von Modellen, die nicht im Produkt enthalten sind, gefördert.

OpenVINO Hackathon: Sprache und Emotionen auf Raspberry Pi erkennen

In diesem Artikel erzählen wir, wie wir unseren Prototyp entwickelt haben, mit dem wir schließlich den ersten Platz belegten.

Am Hackathon nahmen über 10 Teams teil. Es war erfreulich, dass einige von ihnen aus anderen Regionen angereist sind. Der Veranstaltungsort des Hackathons wurde im Komplex „Kreml an der Pochaina“ ausgewählt, wo alte Fotografien von Nischni Nowgorod ausgestellt waren – sehr stimmig! (Ich erinnere daran, dass sich das zentrale Büro von Intel derzeit genau in Nischni Nowgorod befindet). Den Teilnehmern standen 26 Stunden für die Programmierung zur Verfügung, und am Ende war eine Präsentation ihrer Lösung erforderlich. Ein weiterer Pluspunkt war die Möglichkeit einer Demosession, um sicherzustellen, dass alles, was geplant war, tatsächlich umgesetzt wurde und nicht nur Ideen in der Präsentation blieben. Merchandise, Snacks, Essen – auch das war vorhanden!

Darüber hinaus stellte Intel auf Wunsch Kameras, Raspberry PI und den Neural Compute Stick 2 zur Verfügung.

Aufgabenwahl

Eine der schwierigsten Teile der Vorbereitung auf den Hackathon mit freier Themenwahl ist die Wahl der Aufgaben. Wir haben sofort beschlossen, etwas zu entwickeln, was es im Produkt noch nicht gibt, da in der Ankündigung gesagt wurde, dass dies sehr begrüßt wird.

Nach der Analyse Modelle, die im aktuellen Release des Produkts enthalten sind, kommen wir zu dem Schluss, dass die meisten von ihnen verschiedene Aufgaben im Bereich Computer Vision lösen. Es ist sehr schwierig, eine Herausforderung im Bereich Computer Vision zu finden, die sich nicht mit OpenVINO lösen lässt, und selbst wenn man so etwas finden könnte, ist es schwierig, vortrainierte Modelle im offenen Zugang zu finden. Daher haben wir beschlossen, auch in eine andere Richtung zu gehen – in Richtung Sprachverarbeitung und -analyse. Wir betrachten ein interessantes Problem bei der Emotionserkennung aus der Sprache. Es muss gesagt werden, dass OpenVINO bereits ein Modell enthält, das die Emotionen eines Menschen anhand seines Gesichtes erkennt, aber:

  • Theoretisch könnte man einen kombinierten Algorithmus entwickeln, der sowohl auf Klang als auch auf Bild arbeitet, was eine Verbesserung der Genauigkeit zur Folge haben sollte.
  • Kameras haben in der Regel einen schmalen Sichtwinkel, um einen größeren Bereich abzudecken, benötigt man mehr als eine Kamera, der Ton hat keine solchen Einschränkungen.

Wir entwickeln die Idee weiter: Wir nehmen die Idee für den Einzelhandel als Grundlage. Man kann die Zufriedenheit der Kunden an den Kassen der Geschäfte bestimmen. Wenn ein Kunde mit dem Service unzufrieden ist und anfängt, lauter zu werden – kann man sofort den Manager um Hilfe rufen.
In diesem Fall sollte die Spracherkennung hinzugefügt werden, um die Mitarbeiter von den Kunden unterscheiden zu können und Analysen für jede einzelne Person zu liefern. Darüber hinaus kann das Verhalten der Mitarbeiter im Geschäft analysiert werden, um die Atmosphäre im Team zu bewerten. Das klingt gut!

Wir formulieren die Anforderungen an unsere Lösung:

  • Kleine Größe des Zielgeräts
  • Echtzeitbetrieb
  • Niedriger Preis
  • Einfache Skalierbarkeit

Letztendlich wählen wir als Zielgerät Raspberry Pi 3 mit Intel NCS 2.

Hier ist eine wichtige Eigenschaft des NCS zu erwähnen – er funktioniert am besten mit standardmäßigen CNN-Architekturen. Wenn jedoch ein Modell mit benutzerdefinierten Schichten auf ihm ausgeführt werden soll, erwarten Sie eine tiefgreifende Low-Level-Optimierung.

Jetzt fehlt nur noch eines: Wir müssen ein Mikrofon beschaffen. Ein gewöhnliches USB-Mikrofon eignet sich, auch wenn es nicht gut zum RPI passt. Aber auch hier liegt die Lösung praktisch „zum Greifen nah“. Für die Sprachaufnahme entscheiden wir uns für die Voice Bonnet-Platine aus dem Set Google AIY Voice Kit, auf der ein eingebautes Stereo-Mikrofon vorhanden ist.

Wir laden Raspbian aus dem AIY-Projekte-Repository herunter und übertragen es auf einen USB-Stick. Wir testen, ob das Mikrofon mit dem folgenden Befehl funktioniert (dies nimmt Audio für 5 Sekunden auf und speichert es in einer Datei):

arecord -d 5 -r 16000 test.wav

Ich möchte anmerken, dass das Mikrofon sehr empfindlich ist und gut Geräusche aufnimmt. Um dies zu beheben, gehen wir in den alsamixer, wählen Aufnahmegeräte aus und senken das Eingangssignal auf 50-60%.

OpenVINO Hackathon: Sprache und Emotionen auf Raspberry Pi erkennen
Wir feilen das Gehäuse und alles passt hinein, man kann sogar den Deckel schließen.

Wir fügen eine Status-Taste hinzu.

Während wir das AIY Voice Kit zerlegen, erinnern wir uns, dass es dort eine RGB-Taste gibt, deren Beleuchtung programmatisch gesteuert werden kann. Wir suchen nach "Google AIY Led" und finden die Dokumentation: https://aiyprojects.readthedocs.io/en/latest/aiy.leds.html
Warum nicht diese Taste zur Anzeige der erkannten Emotionen verwenden? Wir haben nur 7 Klassen, aber die Taste hat 8 Farben, das reicht aus!

Wir schließen die Taste über GPIO an das Voice Bonnet an und laden die erforderlichen Bibliotheken (diese sind bereits im AIY Projects-Distribution installiert).

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

Wir erstellen ein dict, in dem jeder Emotion eine Farbe in Form eines RGB-Tuples und ein Objekt der Klasse aiy.leds.Leds zugeordnet wird, über das wir die Farbe aktualisieren werden:

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

Und schließlich, nach jeder neuen Vorhersage der Emotion, werden wir die Farbe der Taste entsprechend aktualisieren (basierend auf dem Schlüssel).

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

OpenVINO Hackathon: Sprache und Emotionen auf Raspberry Pi erkennen
Knopf, leuchte!

Wir arbeiten mit Stimme

Wir werden pyaudio verwenden, um den Stream vom Mikrofon aufzunehmen, und webrtcvad zur Rauschfilterung und Sprachdetektion. Außerdem erstellen wir eine Warteschlange, in die wir asynchron Sprachsegmente einfügen und entnehmen werden.

Da webrtcvad eine Beschränkung für die Größe des übergebenen Fragments hat — es muss 10/20/30 ms betragen, und das Training des Modells zur Emotionserkennung (wie wir später erfahren werden) fand mit einem Dataset von 48 kHz statt, werden wir Chunks mit einer Größe von 48000×20ms/1000×1(mono)=960 Bytes erfassen. Webrtcvad wird für jedes dieser Chunks True/False zurückgeben, was das Vorhandensein oder Fehlen von Sprache im Chunk entspricht.

Wir implementieren folgende Logik:

  • Wir werden in die Liste die Chunks hinzufügen, in denen Sprache vorhanden ist, und wenn keine Sprache vorhanden ist, inkrementieren wir den Zähler der leeren Chunks.
  • Wenn der Zähler der leeren Chunks >=30 (600 ms) ist, schauen wir uns die Größe der Liste der gesammelten Chunks an. Wenn sie >250 ist, fügen wir sie zur Warteschlange hinzu, wenn nicht, betrachten wir die Aufnahmedauer als nicht ausreichend, um sie dem Modell zur Identifikation des Sprechers vorzulegen.
  • Wenn der Zähler der leeren Chunks jedoch weiterhin < 30 ist und die Größe der Liste der gesammelten Chunks 300 überschreitet, fügen wir das Segment zur Warteschlange für eine genauere Vorhersage hinzu. (da Emotionen mit der Zeit dazu neigen, sich zu ändern)

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

Es ist an der Zeit, im Internet nach vortrainierten Modellen zu suchen. Wir gehen zu GitHub, suchen bei Google, aber denken daran, dass wir eine Einschränkung hinsichtlich der verwendeten Architektur haben. Das ist ein ziemlich komplizierter Teil, weil man die Modelle mit seinen eigenen Eingabedaten testen muss und außerdem die Konvertierung ins interne OpenVINO-Format IR (Intermediate Representation) erforderlich ist. Wir haben etwa 5-7 verschiedene Lösungen von GitHub ausprobiert, und während das Modell zur Emotionserkennung sofort funktionierte, dauerte es bei der Spracherkennung etwas länger — da kommen komplexere Architekturen zum Einsatz.

Wir konzentrieren uns auf die folgenden:

  • Emotionen aus der Stimme — https://github.com/alexmuhr/Voice_Emotion
    Es funktioniert nach folgendem Prinzip: Audio wird in Segmente bestimmter Größe zerlegt, für jedes dieser Segmente extrahieren wir MFCC und reichen sie dann als Eingabe an die CNN weiter
  • Spracherkennung — https://github.com/linhdvu14/vggvox-speaker-identification
    Hier arbeiten wir anstelle von MFCC mit dem Spektrogramm. Nach der FFT reicht man das Signal in die CNN ein, wo wir als Ausgabe eine vektorielle Darstellung der Stimme erhalten.

Nun wird es um die Konvertierung von Modellen gehen, wir beginnen mit der Theorie. OpenVINO umfasst mehrere Module:

  • Open Model Zoo, aus dem man Modelle verwenden und in sein Produkt integrieren konnte
  • Model Optimizer, mit dem man Modelle aus verschiedenen Framework-Formaten (Tensorflow, ONNX usw.) ins Format Intermediate Representation konvertieren kann, mit dem wir dann weiterarbeiten werden
  • Inference Engine ermöglicht das Ausführen von Modellen im IR-Format auf Intel-Prozessoren, Myriad-Chips und Neural Compute Stick-Beschleunigern
  • Die effizienteste Version von OpenCV (mit Unterstützung für Inference Engine)
    Jedes Modell im IR-Format wird durch zwei Dateien beschrieben: .xml und .bin.
    Modelle werden durch den Model Optimizer wie folgt in das IR-Format konvertiert:
    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 ermöglicht die Auswahl des Datenformats, mit dem das Modell arbeitet. Unterstützt werden FP32, FP16, INT8. Die Wahl des optimalen Datentyps kann eine gute Leistungssteigerung bringen.
    --input_shape gibt die Dimension der Eingabedaten an. Die Möglichkeit, dies dynamisch zu ändern, scheint im C++ API vorhanden zu sein, aber wir sind nicht so weit gegangen und haben es für eines der Modelle einfach fixiert.
    Als nächstes werden wir das bereits konvertierte Modell im IR-Format über das DNN-Modul in OpenCV laden und eine Vorwärtsausführung darauf durchführen.

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

    Die letzte Zeile in diesem Fall ermöglicht es, Berechnungen auf den Neural Compute Stick umzuleiten, die Berechnungen werden im Basismodell auf dem Prozessor ausgeführt, aber im Fall von Raspberry Pi wird das nicht funktionieren, ein Stick ist erforderlich.

    Die Logik ist wie folgt: Wir teilen unser Audio in Fenster einer bestimmten Größe (in unserem Fall 0,4 s), jedes dieser Fenster wird in MFCC umgewandelt, die wir dann an das Netzwerk übergeben:

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

    Danach nehmen wir die häufigste Klasse für alle Fenster. Eine einfache Lösung, aber für einen Hackathon ist es nicht nötig, sich etwas zu kompliziertes auszudenken, es sei denn, es bleibt Zeit. Wir haben noch viel zu tun, also machen wir weiter und beschäftigen uns mit der Spracherkennung. Wir müssen eine Art Datenbank erstellen, in der die Spektrogramme vorab aufgezeichneter Stimmen gespeichert werden. Da nur wenig Zeit bleibt, lösen wir diese Frage so gut wir können.

    Genauer gesagt erstellen wir ein Skript zur Aufnahme eines Ausschnitts der Stimme (es funktioniert genauso wie oben beschrieben, nur dass es beim Abbrechen mit der Tastatur die Stimme in eine Datei speichert).

    Wir probieren es aus:

    python3 voice_db/record_voice.py test.wav

    Wir nehmen die Stimmen mehrerer Personen auf (in unserem Fall drei Mitglieder des Teams)
    Anschließend führen wir für jede aufgezeichnete Stimme eine schnelle Fourier-Transformation durch, erhalten das Spektrogramm und speichern es als numpy array (.npy):

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

    Weitere Informationen finden Sie in der Datei create_base.py
    Wenn wir das Hauptskript ausführen, erhalten wir zu Beginn die Embeddings aus diesen Spektrogrammen:

    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)

    Nach dem Empfang der Einbettung aus dem gesendeten Abschnitt können wir bestimmen, zu wem er gehört, indem wir den cosinus distance vom Abschnitt zu allen Stimmen in der Datenbank nehmen (je kleiner, desto wahrscheinlicher) — für die Demo haben wir einen Schwellenwert von 0.3 festgelegt:

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

    Am Ende möchte ich erwähnen, dass die Inferenzgeschwindigkeit schnell war und es uns ermöglichte, noch 1-2 Modelle hinzuzufügen (für ein 7-sekündiges Sample benötigten wir 2.5 Sekunden zur Inferenz). Neue Modelle konnten wir jedoch nicht mehr rechtzeitig hinzufügen und konzentrierten uns auf die Erstellung des Prototyps der Webanwendung.

    Webanwendung

    Wichtiger Punkt: Wir bringen einen Router von zu Hause mit und richten unser eigenes Netzwerk ein, das hilft, das Gerät und die Laptops im Netzwerk zu verbinden.

    Das Backend stellt einen durchgängigen Nachrichtenkanal zwischen dem Frontend und dem Raspberry Pi dar, der auf der Websocket-Technologie (http über tcp-Protokoll) basiert.

    Der erste Schritt besteht darin, die verarbeiteten Informationen vom Raspberry zu erhalten, also die in JSON verpackten Vorhersagen, die auf halbem Weg in einer Datenbank gespeichert werden, um Statistiken über den emotionalen Hintergrund des Benutzers über einen bestimmten Zeitraum zu erstellen. Dieses Paket wird dann an das Frontend gesendet, das Abonnements verwendet und Pakete vom Websocket-Endpunkt erhält. Der gesamte Mechanismus des Backends ist in der Programmiersprache Golang aufgebaut, weil diese gut für asynchrone Aufgaben geeignet ist, mit denen Goroutinen gut umgehen können.
    Beim Zugriff auf den Endpunkt wird der Benutzer registriert und in eine Struktur aufgenommen, dann wird seine Nachricht empfangen. Sowohl der Benutzer als auch die Nachricht werden in einen gemeinsamen Hub eingetragen, aus dem die Nachrichten dann weitergeleitet werden (an das abonnierte Frontend). Schließt der Benutzer die Verbindung (Raspberry oder Frontend), wird sein Abonnement storniert und er wird aus dem Hub entfernt.

    OpenVINO Hackathon: Sprache und Emotionen auf Raspberry Pi erkennen
    Wir erwarten eine Verbindung zum Backend

    Das Front-end ist eine Webanwendung, die in JavaScript unter Verwendung der React-Bibliothek geschrieben wurde, um den Entwicklungsprozess zu beschleunigen und zu vereinfachen. Ziel dieser Anwendung ist die Visualisierung von Daten, die mithilfe von Algorithmen auf der Back-end-Seite und direkt auf dem Raspberry Pi verarbeitet werden. Die Seite hat eine Navigation durch Abschnitte, die mit react-router umgesetzt wurde, aber das Hauptinteresse gilt der Hauptseite, auf der in Echtzeit ein kontinuierlicher Datenstrom vom Server über WebSocket-Technologie empfangen wird. Der Raspberry Pi erkennt die Stimme, bestimmt die Zugehörigkeit zu einer bestimmten Person aus der registrierten Datenbank und sendet dem Client eine Liste von Wahrscheinlichkeiten. Der Client zeigt die neuesten aktuellen Daten an, zeigt das Avatarbild der Person, die höchstwahrscheinlich in das Mikrofon gesprochen hat, sowie die Emotion an, mit der sie spricht.

    OpenVINO Hackathon: Sprache und Emotionen auf Raspberry Pi erkennen
    Hauptseite mit aktualisierten Vorhersagen

    Fazit

    Es hat nicht alles so geklappt, wie wir es uns vorgestellt hatten, einfach weil die Zeit nicht reichte, deshalb lag unsere Haupt Hoffnung auf der Demo, dass alles funktionieren würde. In der Präsentation wurde erklärt, wie alles strukturiert ist, welche Modelle wir verwendet haben und mit welchen Problemen wir konfrontiert waren. Danach gab es einen Demoteil – die Experten gingen in zufälliger Reihenfolge durch den Raum und schauten bei jedem Team vorbei, um den funktionierenden Prototyp anzusehen. Sie stellten auch uns Fragen, jeder antwortete in seinem Teil, auf dem Laptop ließen wir das Web und es funktionierte tatsächlich alles wie erwartet.

    Ich möchte darauf hinweisen, dass die Gesamtkosten unserer Lösung 150 $ betrugen:

    • Raspberry Pi 3 ~ 35 $
    • Google AIY Voice Bonnet (man kann auch die Respeaker-Platine verwenden) ~ 15 $
    • Intel NCS 2 ~ 100 $

    Wie man es verbessern kann:

    • Kundenregistrierung verwenden – darum bitten, einen zufällig generierten Text vorzulesen
    • Einige weitere Modelle hinzufügen: an der Stimme kann man Geschlecht und Alter bestimmen
    • Gleichzeitig klingende Stimmen unterscheiden (Diarisation)

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

    OpenVINO Hackathon: Sprache und Emotionen auf Raspberry Pi erkennen
    Erschöpft, aber glücklich sind wir

    Abschließend möchten wir den Organisatoren und Teilnehmern danken. Aus den Projekten anderer Teams hat uns persönlich die Lösung zur Überwachung freier Parkplätze gut gefallen. Für uns war das eine unglaublich tolle Erfahrung in das Produkt und die Entwicklung einzutauchen. Ich hoffe, dass in den Regionen immer mehr interessante Veranstaltungen stattfinden, auch zu Themen der KI.

Quelle: habr.com

60GB SSD 8Gb DDR4