Vom 30. November bis 1. Dezember fand in Nischni Nowgorod. Den Teilnehmern wurde die Aufgabe gestellt, einen Prototyp eines Produktlösungs mit dem Intel OpenVINO Toolkit zu erstellen. Die Organisatoren boten eine Liste von Themen an, die als Orientierung bei der Auswahl der Aufgabe dienen konnten, aber die endgültige Entscheidung lag bei den Teams. Außerdem wurde die Verwendung von Modellen, die nicht im Produkt enthalten sind, gefördert.

Im Artikel erzählen wir, wie wir unseren Produktprototyp erstellt haben, mit dem wir letztendlich den ersten Platz belegten.
Am Hackathon nahmen über 10 Teams teil. Es freut mich, dass einige von ihnen aus anderen Regionen kamen. Der Veranstaltungsort des Hackathons war das 'Kremlevskiy auf Pochaine', wo alte Fotografien von Nischni Nowgorod aufgehängt waren - sehr stimmungsvoll! (Zur Erinnerung: Das zentrale Büro von Intel befindet sich derzeit genau in Nischni Nowgorod). Den Teilnehmern standen 26 Stunden zur Verfügung, um ihren Code zu schreiben, und am Ende mussten sie ihre Lösung präsentieren. Ein zusätzlicher Pluspunkt war die Durchführung 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 - das gab es auch!
Darüber hinaus stellte Intel auf Wunsch Kameras, Raspberry PI, Neural Compute Stick 2 zur Verfügung.
Aufgabenwahl
Eine der schwierigsten Aufgaben bei der Vorbereitung auf einen Hackathon mit freier Themenwahl ist die Wahl der Aufgabe. Wir haben sofort beschlossen, etwas zu entwickeln, das im Produkt noch nicht vorhanden ist, da in der Ankündigung gesagt wurde, dass dies ausdrücklich begrüßt wird.
Nach einer Analyse , die im aktuellen Release des Produkts enthalten sind, kommen wir zu dem Schluss, dass die meisten von ihnen verschiedene Aufgaben der Computer Vision lösen. Es ist ziemlich schwierig, eine Aufgabe aus dem Bereich der Computer Vision zu finden, die sich nicht mit OpenVINO lösen lässt. Und falls so eine Aufgabe überhaupt existiert, ist es schwer, vortrainierte Modelle in der öffentlichen Domain zu finden. Wir sollten daher auch in eine andere Richtung forschen – in Richtung der Sprachverarbeitung und -analyse. Ein interessantes Problem, das wir betrachten, ist die Emotionserkennung aus Sprache. Es sei gesagt, dass OpenVINO bereits ein Modell hat, das die Emotionen eines Menschen anhand seines Gesichts erkennt, aber:
- Theoretisch könnte man einen kombinierten Algorithmus erstellen, der sowohl mit Geräuschen als auch mit Bildern arbeitet, was zu einer Verbesserung der Genauigkeit führen sollte.
- Kameras haben normalerweise einen engen Blickwinkel; um ein größeres Gebiet abzudecken, sind mehrere Kameras erforderlich, während der Ton diese Einschränkung nicht hat.
Wir entwickeln die Idee weiter: Lassen Sie uns die Idee für den Einzelhandelssektor als Grundlage nehmen. Man könnte die Zufriedenheit der Kunden an den Kassen der Geschäfte messen. Wenn ein Kunde mit dem Service unzufrieden ist und seine Stimme erhebt, könnte man sofort einen Administrator zur Hilfe rufen.
In diesem Fall sollten wir die Spracherkennung hinzufügen, um zwischen den Mitarbeitern des Geschäfts und den Kunden zu unterscheiden und Analysen für jede einzelne Person bereitzustellen. Außerdem können wir das Verhalten der Mitarbeiter im Geschäft analysieren und die Atmosphäre im Team bewerten; das klingt vielversprechend!
Wir formulieren die Anforderungen an unsere Lösung:
- Kleine Größe des Zielgeräts
- Echtzeitbetrieb
- Niedriger Preis
- Einfache Skalierbarkeit
Letztlich wählen wir das Zielgerät Raspberry Pi 3 mit .
Hier ist es wichtig, eine Besonderheit des NCS zu erwähnen — es funktioniert am besten mit standardmäßigen CNN-Architekturen. Wenn Sie jedoch ein Modell mit benutzerdefinierten Schichten auf ihm ausführen müssen, erwarten Sie ̶n̶i̶ed̶r̶i̶g̶e̶ ̶o̶p̶t̶i̶m̶i̶e̶r̶u̶ng ̶. der tiefgreifenden Optimierung.
Das letzte Detail: Ein Mikrofon muss beschafft werden. Ein gewöhnliches USB-Mikrofon ist ausreichend, obwohl es nicht gut zum RPI passt. Aber auch hier liegt die Lösung buchstäblich „nebenan“. Für die Sprachaufnahme beschließen wir, die Voice Bonnet-Platine aus dem Set zu verwenden , auf der ein verlötetes Stereo-Mikrofon vorhanden ist.
Wir laden Raspbian aus Laden Sie diese auf einen USB-Stick und testen Sie, ob das Mikrofon mit dem folgenden Befehl funktioniert (dies wird Audio mit einer Länge von 5 Sekunden aufnehmen und in einer Datei speichern):
arecord -d 5 -r 16000 test.wavIch möchte gleich hinzufügen, dass das Mikrofon sehr empfindlich ist und Geräusche gut aufnimmt. Um dies zu beheben, gehen wir in alsamixer, wählen Capture-Geräte aus und reduzieren den Eingangspegel auf 50-60 %.

Wir feilen das Gehäuse nach und alles passt, sogar der Deckel kann wieder geschlossen werden.
Wir fügen einen Indikator-Knopf hinzu.
Während wir das AIY Voice Kit zerlegen, fällt uns ein, dass es einen RGB-Knopf gibt, dessen Beleuchtung programmatisch gesteuert werden kann. Wir suchen nach "Google AIY Led" und finden die Dokumentation:
Warum nicht diesen Knopf verwenden, um die erkannte Emotion anzuzeigen? Wir haben insgesamt 7 Klassen und der Knopf hat 8 Farben, das reicht genau aus!
Wir verbinden den Knopf über GPIO mit dem Voice Bonnet und laden die benötigten Bibliotheken, die bereits im AIY Projects-Distributionspaket installiert sind:
from aiy.leds import Leds, Color
from aiy.leds import RgbLedsWir erstellen ein dict, in dem jede Emotion einer Farbe in Form eines RGB-Tupels und einem 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 Emotion Vorhersage, werden wir die Farbe des Knopfes entsprechend ihr aktualisieren (basierend auf dem Schlüssel).
leds.update(Leds.rgb_on(led_dict.get(classes[prediction])))
Lichtknopf, leuchte!
Wir arbeiten mit Stimme
Wir werden pyaudio verwenden, um den Mikrofonstream aufzuzeichnen, und webrtcvad, um Rauschen zu filtern und die Stimme zu erkennen. Außerdem erstellen wir eine Warteschlange, in die wir Audioausschnitte asynchron hinzufügen und abrufen.
Da webrtcvad eine Begrenzung 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 weiter sehen werden) wurde mit einem 48 kHz-Datensatz durchgeführt, werden wir Fragmente der Größe 48000×20 ms/1000×1 (mono) = 960 Byte erfassen. Webrtcvad wird für jedes dieser Fragmente True/False zurückgeben, was das Vorhandensein oder Fehlen von Stimme im Fragment kennzeichnet.
Wir implementieren folgende Logik:
- Wir fügen der Liste die Fragmente hinzu, in denen Stimme vorhanden ist; falls keine Stimme vorhanden ist, inkrementieren wir den Zähler leerer Fragmente.
- Wenn der Zähler für leere Chunks >=30 (600 ms) ist, überprüfen wir die Größe der Liste der angesammelten Chunks. Wenn sie >250 ist, fügen wir sie in die Warteschlange ein. Andernfalls gehen wir davon aus, dass die Länge des Aufzeichnung nicht ausreicht, um sie dem Modell zur Identifizierung des Sprechers vorzulegen.
- Sollte der Zähler für leere Chunks weiterhin < 30 sein, und die Größe der Liste der angesammelten Chunks 300 überschreiten, fügen wir den Auszug der Warteschlange für eine genauere Vorhersage hinzu (da Emotionen im Laufe der Zeit variieren können).
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, googeln, aber denken daran, dass wir eine Einschränkung hinsichtlich der verwendeten Architektur haben. Das ist ein ziemlich komplexer Teil, da wir die Modelle mit unseren Eingabedaten testen müssen und außerdem in das interne Format von OpenVINO – IR (Intermediate Representation) konvertieren müssen. Wir haben etwa 5-7 verschiedene Lösungen von GitHub ausprobiert, und während das Modell zur Emotionserkennung sofort funktionierte, dauerte es beim Sprachverständnis etwas länger – hier werden komplexere Architekturen verwendet.
Wir entscheiden uns für folgende:
- Emotionen aus der Stimme –
Es funktioniert nach folgendem Prinzip: Audiodateien werden in Fragmente einer bestimmten Größe zerlegt, für jedes dieser Fragmente extrahieren wir und reichen diese dann in ein CNN ein. - Spracherkennung –
Hier arbeiten wir anstelle von MFCC mit Spektrogrammen. Nach der FFT leiten wir das Signal in ein CNN, wo wir am Ausgang eine vektorielle Darstellung der Stimme erhalten.
Im Folgenden werden wir über die Konvertierung von Modellen sprechen, beginnend mit der Theorie. OpenVINO besteht aus mehreren Modulen:
- Open Model Zoo, aus dem Modelle verwendet und in Ihr Produkt integriert werden können.
- Der Model Optimizer ermöglicht die Konvertierung von Modellen aus verschiedenen Framework-Formaten (Tensorflow, ONNX usw.) in das Intermediate Representation-Format, mit dem wir anschließend arbeiten werden.
- Die Inference Engine erlaubt 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 die Inference Engine).
Jedes Modell im IR-Format wird durch zwei Dateien beschrieben: .xml und .bin.
Modelle werden über den Model Optimizer in das IR-Format wie folgt 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_typeermöglicht die Auswahl des Datentyps, mit dem das Modell arbeiten wird. Unterstützt werden FP32, FP16, INT8. Die Wahl des optimalen Datentyps kann die Leistung erheblich steigern.
--input_shapegibt die Dimensionen der Eingabedaten an. Es scheint, dass die Möglichkeit, diese dynamisch zu ändern, im C++ API vorhanden ist, aber wir wollten nicht so tief graben und haben einfach für eines der Modelle eine feste Größe hinterlegt.
Als nächstes versuchen wir, das bereits in IR konvertierte Modell über das DNN-Modul in OpenCV zu laden und einen Forward-Durchlauf darauf durchzufü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, die Berechnungen auf den Neural Compute Stick umzuleiten. Standardmäßig erfolgen die Berechnungen auf der CPU, aber bei einem Raspberry Pi funktioniert das nicht, da wird der Stick benötigt.
Die Logik geht weiter: Wir teilen unser Audio in Fenster einer bestimmten Größe (in unserem Fall 0,4 s) auf, jedes dieser Fenster wird in MFCC umgewandelt, die dann in das Netzwerk eingegeben werden:
emotionsNet.setInput(MFCC_from_window) result = emotionsNet.forward()Dann wählen wir die am häufigsten vorkommende Klasse für alle Fenster aus. Eine einfache Lösung, aber für einen Hackathon braucht man nicht etwas zu Ausgefeiltes zu erfinden, es sei denn, man hat Zeit. Wir haben noch viel Arbeit vor uns, also machen wir weiter – beschäftigen uns mit der Spracherkennung. Wir müssen eine Art Datenbank erstellen, in der die Spektrogramme der vorher aufgenommenen Stimmen gespeichert werden. Da die Zeit knapp ist, lösen wir dieses Problem so gut wir können.
Genauer gesagt, erstellen wir ein Skript zur Aufnahme eines Sprachabschnitts (es funktioniert wie oben beschrieben, aber beim Unterbrechen über die Tastatur wird die Stimme in einer Datei gespeichert).
Lass es uns versuchen.:
python3 voice_db/record_voice.py test.wavWir nehmen die Stimmen mehrerer Personen auf (in unserem Fall drei Teammitglieder).
Anschließend führen wir für jede aufgezeichnete Stimme eine schnelle Fourier-Transformation durch, erhalten das Spektrum 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 Details 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)Nachdem wir das Embedding aus dem gespielten Abschnitt erhalten haben, können wir bestimmen, zu wem es gehört, indem wir die Kosinus-Distanz 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)Ich möchte abschließend erwähnen, dass die Inferenzgeschwindigkeit schnell war und es erlaubte, 1-2 weitere Modelle hinzuzufügen (bei einer Länge des Samples von 7 Sekunden benötigten wir 2,5 Sekunden für die Inferenz). Neue Modelle konnten wir bereits nicht mehr hinzufügen und konzentrierten uns auf das Schreiben eines Prototyps der Webanwendung.
Webanwendung
Wichtiger Punkt: Wir bringen unseren Router von zu Hause mit und richten unser lokales Netzwerk ein, um Geräte und Laptops im Netzwerk zu verbinden.
Der Backend besteht aus einem durchgehenden Nachrichtenkanal zwischen dem Frontend und dem Raspberry Pi, basierend auf der WebSocket-Technologie (HTTP über TCP-Protokoll).
Der erste Schritt besteht darin, die verarbeiteten Informationen vom Raspberry Pi zu erhalten, das heißt die in JSON verpackten Vorhersagen, die auf ihrem Weg in einer Datenbank gespeichert werden, um Statistiken über das emotionale Verhalten des Nutzers über einen Zeitraum zu erstellen. Anschließend wird dieses Paket an das Frontend gesendet, das Abonnements nutzt, um Pakete von dem WebSocket-Endpunkt zu empfangen. Der gesamte Backend-Mechanismus ist in der Programmiersprache Go реализiert, da diese gut für asynchrone Aufgaben geeignet ist, bei denen Goroutinen besonders effektiv sind.
Bei Zugriff auf den Endpoint wird der Benutzer registriert und in die Struktur aufgenommen, danach erfolgt der Empfang seiner Nachricht. Sowohl der Benutzer als auch die Nachricht werden in einen gemeinsamen Hub eingefügt, von dem aus die Nachrichten weitergeleitet werden (an das abonnierte Frontend). Wenn der Benutzer die Verbindung trennt (Raspberry oder Frontend), wird sein Abonnement widerrufen und er wird aus dem Hub entfernt.
Wir warten auf die Verbindung mit dem BackendDas Frontend ist eine Webanwendung, die mit JavaScript unter Verwendung der React-Bibliothek entwickelt wurde, um den Entwicklungsprozess zu beschleunigen und zu vereinfachen. Das Ziel dieser Anwendung ist es, Daten zu visualisieren, die durch Algorithmen auf der Backend-Seite und direkt auf dem Raspberry Pi generiert werden. Die Seite verfügt über eine Abschnittsführung, die mit react-router implementiert wurde, wobei das Hauptinteresse der Startseite gilt, die in Echtzeit einen kontinuierlichen Datenstrom vom Server über WebSocket erhält. Der Raspberry Pi erkennt Stimmen, identifiziert Personen aus einer registrierten Datenbank und sendet dem Client eine Liste von Wahrscheinlichkeiten. Der Client zeigt die neuesten relevanten Daten an, zeigt das Profilbild der Person, die mit der größten Wahrscheinlichkeit ins Mikrofon gesprochen hat, sowie die Emotion, mit der die Worte ausgesprochen werden.

Startseite mit aktualisierten VorhersagenFazit
Wir konnten leider nicht alles wie geplant fertigstellen; einfach gesagt, die Zeit hat nicht gereicht. Daher lag unsere Haupthoffnung auf der Demo, dass alles funktionieren würde. In der Präsentation haben wir erklärt, wie alles aufgebaut ist, welche Modelle wir gewählt haben und mit welchen Problemen wir konfrontiert waren. Danach folgte der Demo-Teil – die Experten gingen in zufälliger Reihenfolge durch den Raum und schauten sich jedes Team an, um den funktionierenden Prototypen zu begutachten. Sie stellten Fragen an uns, jeder beantwortete sie in seinem Bereich. Auf dem Laptop hatten wir das Web-Interface offen, und es funktionierte tatsächlich, wie erwartet.
Ich möchte anmerken, dass die Gesamtkosten unserer Lösung 150 $ betrugen:
- Raspberry Pi 3 ~ 35 $
- Google AIY Voice Bonnet (man kann auch das respeaker-Board nehmen) ~ 15 $
- Intel NCS 2 ~ 100 $
Wie verbessern:
- Kundenregistrierung nutzen – den Text vorlesen lassen, den wir zufällig generieren
- Einige weitere Modelle hinzufügen: man kann durch den Stimme Geschlecht und Alter erkennen
- Gleichzeitig erklingende Stimmen trennen (Diarisierung)
Repository:

Erschöpft, aber glücklich sind wirAbschließend möchte ich den Organisatoren und Teilnehmern danken. Von den Projekten anderer Teams hat uns besonders die Lösung zur Überwachung freier Parkplätze gefallen. Für uns war es eine unglaublich großartige Erfahrung, in das Produkt und die Entwicklung einzutauchen. Ich hoffe, dass in den Regionen immer mehr interessante Veranstaltungen stattfinden werden, auch im Bereich KI.
Quelle: habr.com



