Echtzeit-Spracherkennung mit Deepspeech

Echtzeit - Spracherkennung mit Deepspeech, Sprachassistenten Alexa, Google Home, Google Cloud, Mycroft, Code, Software

Spracherkennung kann im Alltag eine ziemlich nützliche Funktion sein. So ist es damit möglich, Geräten wie z. B. einem Staubsaugroboter Befehle zu erteilen oder Lampen bequem vom Bett aus ein- und ausschalten. Sprachassistenten wie Amazons Alexa oder Google Home bieten darüber hinaus ein breites Spektrum an Funktionen. Die Bequemlichkeit bringt bisweilen aber auch Nachteile mit sich, die je nach persönlichen Ansichten ein Dealbreaker sein können.

Der aufgezeichnete Ton der Stimme wird für die Verarbeitung direkt an die Amazon bzw. Google Cloud gesendet und dort auch gespeichert. Dies wirft zwangsweise Fragen über den Datenschutz auf. Die genannten Unternehmen versichern zwar, die Privatsphäre der Nutzer zu wahren, dennoch haben viele Menschen dabei ein ungutes Gefühl. In der eigenen Wohnung mag die Ungewissheit noch vertretbar sein, im Büro oder Unternehmensumfeld allerdings nicht. Außerdem kann sich, je nach geplantem Einsatzort und -zweck, die Notwendigkeit einer stabilen und schnellen Internetverbindung nachteilig auswirken.

Aus meiner Sicht wäre daher eine Umwandlung von Sprache zu Text direkt auf dem Gerät des Nutzers erstrebenswert. Eine gute und zuverlässige Spracherkennung war bisher rechenaufwendig und wartungsintensiv. Da ein Sprachassistent energiesparend, gleichzeitig aber auch klein und günstig sein sollte, ist es naheliegend, dass die Umwandlung von Audio in Text üblicherweise auf Servern in Rechenzentren durchgeführt wird. In den letzten Jahren wurden mehrere Sprachassistenten auf Open-Source-Basis entwickelt, die ähnlich wie die Systeme von Amazon und Google verwendet werden können, aber mit etwas mehr Privatsphäre werben. Eines davon ist Mycroft.

Wie andere Sprachassistenten auch, läuft bei Mycroft der Großteil des Codes (darunter auch die Spracherkennung) auf Servern in der Cloud. Standardmäßig wird sogar Googles STT (Speech To Text) API verwendet. Die verbesserte Privatsphäre kommt daher, dass Audio-Streams mehrerer Benutzer zu Batches zusammengefasst und gemeinsam an google gesendet werden. Dies erschwert die Zuordnung zu einzelnen Nutzern. Mycroft ist modular aufgebaut und ermöglicht neben dem Hosten auf eigenen Servern auch das Austauschen seiner Module. Das Hosten einer eigenen Mycroft-Instanz ist im Moment aber am ehesten Unternehmen oder Technik-affinen Bastlern zumutbar und verfehlt daher meine Vorstellung eines vollständig lokal laufenden Sprachassistenten, der nur für Suchanfragen eine Internetverbindung benötigt.

Mycroft besteht, wie andere Sprachassistenten auch, aus fünf Teilen.

Schlüsselwort-Detektor

Dabei handelt es sich um eine vereinfachte Spracherkennungssoftware, die direkt auf dem Gerät des Nutzers läuft und das Mikrofon ununterbrochen abhört. Wird das Schlüsselwort erkannt, baut sich eine Verbindung zum Backend und die vom Mikrofon kommenden Daten werden gestreamt. Die hier eingesetzte Spracherkennung ist typischerweise eher rudimentär und wenig rechenintensiv. False positives führen aber dazu, dass ungewollt Tonaufnahmen gesendet werden.

Alle folgenden Punkte finden im Backend statt.

Speech To Text (STT) Engine

Hier passiert die eigentliche Umwandlung der aufgenommenen Sprache in Text. Traditionell handelt es sich dabei um eine recht komplexe Software, die phonetische Merkmale aus einer Audioaufnahme extrahiert und Buchstabenfolgen oder Wörter auf Basis von Sprachmodellen generiert. Klassische STT-Engines werden aufwändig von Hand auf eine bestimmte Sprache und einen bestimmten Kontext justiert und benötigen viel Expertise. Mittlerweile werden sie vermehrt durch robustere und auf KI-basierte Spracherkennung abgelöst. Mehr dazu weiter unten.

Intent Parser

Der Intent Parser versucht, die Absicht des Benutzers aus der Aneinanderreihung von Wörtern abzuleiten und diese an ein dazu passendes "Skill" weiterzuleiten.

Skills

Ein Skill führt den Intent des Benutzers aus und liefert gegebenenfalls eine Antwort. Es gibt beispielsweise ein Skill, das den Wetterbericht liefert oder eines, das die Lichter
betätigt.

Text To Speech (TTS)

Die erzeugte Antwort wird durch Sprachsynthese wieder in ein akustisches Signal umgewandelt. Wie bei Speech to Text gibt es auch hier verschiedenen Ansätze, die sich hinsichtlich Qualität und Rechenaufwand deutlich unterscheiden.

Da ich die Umwandlung von Sprache in Text als den kritischsten und zugleich auch als interessantesten Teil betrachte, habe ich mich entschlossen, damit etwas herumzuspielen. Möglicherweise beschäftige ich mich in näherer Zukunft in weiteren Blogbeiträgen noch mit den anderen Punkten.

Mozillas Deepspeech

Bei Deepspeech handelt es sich um eine Implementierung eines auf KI-basierten Speech-To-Text-Systems von Mozilla. Die Implementierung basiert auf dem Paper „Deep Speech: Scaling up end-to-endspeech recognition „. Dabei handelt es sich um ein sogenanntes „Recurrent Neural Network“ also ein Neuronales Netzwerk mit Rückkopplungen, die als eine Art Speicher dienen. Damit kann das Netzwerk Eingabedaten über einen längeren Zeitraum betrachten. Deepspeech hat den Vorteil, dass es im Vergleich zu klassischen Spracherkennungssystemen vergleichsweise einfach aufgebaut ist und dazu auch noch wesentlich robuster gegenüber Hintergrund- bzw. Störgeräuschen ist. Der verwendete Algorithmus benötigt keine Kenntnisse der Phonetik von Sprachen oder andere manuelle Anpassungen. Er muss lediglich mit ausreichend vielen Beispieldaten versorgt und trainiert werden. Genau darin besteht gleichzeitig auch seine Schwäche, denn für eine robuste Spracherkennung braucht es hunderte Stunden an Sprachaufzeichnungen und die dazugehörigen Texte.

Mozilla versucht mithilfe des Common-Voice-Projekts genügend Sprachaufzeichnungen zu sammeln, um neuronale Netze wie Deepspeech trainieren zu können. Wie zu erwarten stehen momentan hauptsächlich englische Daten zur Verfügung. Deshalb haben die zur Verfügung gestellten vortrainierten Modelle die Tendenz, englische Sprecher mit amerikanischen Akzent deutlich besser zu verstehen. Nichts desto trotz kann mit eigenen Daten trainiert werden: entweder von Grund auf neu oder auf Basis der vortrainierten englischen Modelle. Empfehlenswert ist dies vor allem, wenn man Spracherkennung für eine sehr spezifische Anwendung mit nur wenigen zu erkennenden Wörtern benötigt. Damit lässt sich die Fehlerrate deutlich reduzieren. Will man Deepspeech selbst trainieren, muss man berücksichtigen, dass man ohne eine schnelle GPU, die CUDA unterstützt, nicht weit kommen wird. Für die Inferenz, d. h. für die reine Nutzung des Netzwerks genügt eine normale CPU.

Bis vor kurzem hat Deepspeech, das intern auf Googles Tensorflow aufsetzt, noch große Mengen an Arbeitsspeicher benötigt. Das war neben der zu großen Rechenlast der Hauptgrund, warum es für eingebettete Systeme und kleine Single-Board-Computer wie dem Raspberry Pi nicht in Frage kam. Mittlerweile wird Deepspeech auch in einer Tensorflow Lite Variante angeboten. Diese Variante enthält ein auf 16bit oder 8bit Ganzzahlen quantisiertes Modell, das dadurch auf leistungsschwacher Hardware deutlich performanter und speichersparender ausgeführt werden kann. Damit wird es auf einem Raspberry Pi 4 erstmals möglich, Sprache mithilfe von Deepspeech in Echtzeit in Text umzuwandeln.

Erster Test

Deepspeech wurde intern mithilfe von Googles Tensorflow Framework umgesetzt. Die Bibliothek kann allerdings ohne direkte Nutzung der Tensorflow-APIs verwendet werden. Mozilla stellt Bindings für C, .Net, Java, Javascript und Python bereit. Um möglichst schnell zu ersten Ergebnissen zu kommen, habe ich natürlich die Python-API verwendet 😉

Installation

Die Verwendung von Pipenv kann ich nur empfehlen. Pipenv vereint die Erstellung von Python-Enviroments mit dem Installieren von Paketen. Das verwendete Pipfile und Pipfile.lock verbessern zudem auch das Abhängigkeitsmanagement gegenüber einer klassischen requirements.txt deutlich. Pipenv erstellt automatisch ein Python-Virtual-Enviroment für den gerade aktiven Ordner, sollte noch keins vorhanden sein. Mit pipenv run können Programme innerhalb des Enviroments gestartet werden. Mit pipenv shell kann eine Shell gestartet werden. Pipfile und Pipfile.lock funktionieren ähnlich wie die von npm verwendeten package.json und package-lock.json aus der Javascript-Welt.

Deepspeech gibt es in zwei Varianten: die klassische Tensorflow-Variante, die auch zum trainieren mit eigenen Daten verwendet werden kann und die resourcensparende Tensorflow Lite-Variante, die nur zur Inferenz genutzt werden kann. Erstere kann mit

pipenv install deepspeech

installiert werden. Die Lite-Variante kann man mit

pipenv install deepspeech-tflite

installieren. Für das Raspberry Pi gibt es nur die Tensorflow Lite-Variante, die dort aber Deepspeech (ohne Lite) heißt.

Mein Ziel war es, meine Stimme in Echtzeit als Text auf der Konsole auszugeben. Um von Python aus Sprache aufzeichnen zu können, verwende ich PyAudio. Installieren kann man es mit:

pipenv install pyaudio

Zuguterletzt wird noch Numpy benötigt.

pipenv install numpy

Aufzeichnung der Sprache

Für das Aufzeichnen der Sprache verwende ich PyAudio. PyAudio unterstützt sowohl das synchrone als auch das asynchrone Lesen und Schreiben von Audio-Streams. Da die Umwandlung der Sprache in Echtzeit stattfinden soll, d. h. gleichzeitig wie die Audio-Aufzeichnung, können wir uns keine blockierenden Lesefunktionen leisten und werden deshalb die asynchrone API nutzen. Dazu muss bei der Initialisierung des Streams eine Callback-Funktion angegeben werden, die in einem separaten Thread periodisch für eintreffende Puffer aufgerufen wird. Wie groß der Puffer gesetzt werden soll, hängt von mehreren Faktoren ab. Ein kleiner Puffer führt dazu, dass die Callback-Funktion öfter aufgerufen wird und wir weniger lange Zeit haben, die Daten zu verarbeiten. Da wir die Daten aber sowieso im Haupt-Thread verarbeiten und die Verarbeitung auf der eingesetzten Hardware meist schneller als Echtzeit verläuft, spielt das keine Rolle. Eine viel größere rolle spielt das Deepspeech im Moment wie es aussieht, mit längeren zusammengehörenden Audioschnipseln besser klarkommt. Der Nachteil eines großen Puffers ist, dass sich damit die Latenz deutlich erhöht. Nach etwas Herumprobieren habe ich mich auf einen Puffer mit einer Größe von 1/10 der Abtastrate festgelegt. Das entspricht 10ms. Wichtig zu beachten ist, dass das Modell mit einer bestimmten Abtastrate trainiert wurde und diese auch erwartet. Man muss daher entweder direkt mit dieser Rate aufzeichnen oder andernfalls das Signal vorher konvertieren.

audio_stream = audio.open(rate=model.sampleRate(),
                              channels=1,
                              format=audio.get_format_from_width(
                                  SAMPLE_WIDTH, unsigned=False),
                              input_device_index=index,
                              input=True,
                              frames_per_buffer=buffer_size,
                              stream_callback=audio_callback)

Die Callback-Funktion bekommt einen Puffer der Länge frame_count sowie Timing-Informationen und verschiedene Flags übergeben, die wir hier aber ignorieren können. Als Status geben wir paContinue zurück und sagen damit, dass die Aufzeichnung nicht gestoppt werden soll.

def audio_callback(in_data, frame_count, time_info, status_flags):
    buffer_queue.put(np.frombuffer(in_data, dtype='int16'))
    return (None, pyaudio.paContinue)

Da unser Callback in einem anderen Thread als die Spracherkennung ausgeführt wird und die Puffer in einer anderen Rate verarbeitet (Consumer), als sie von PyAudio geliefert werden (Producer), übergeben wir den Puffer über eine Thread-sichere FIFO-Queue. Praktischerweise stellt uns Pythons umfangreiche Standardbibliothek eine solche zur Verfügung.

    i = 0
    while audio_stream.is_active():
        stream.feedAudioContent(buffer_queue.get())
        if i % num_iterations == 0:
            text = stream.intermediateDecode()
            if text.find('stop') >= 0:
                break
            print(text)
        i += 1

    print(stream.finishStream())
    audio_stream.close()

Im Haupt-Thread werden die Puffer aus der Queue genommen und dem Deepspeech-Stream „gefüttert“. Alle 2 Sekunden wird ein Zwischenergebnis berechnet und ausgegeben. Wird das Word „stop“ erkannt, wird die Spracherkennung beendet und das finale Ergebnis präsentiert. Neben der Decodierung als einfachen String gibt es auch die Möglichkeit, Metadaten wie z. B. den Zeitpunkt des Auftretens eines erkannten Worts zu erhalten. Darüber hinaus besteht die Möglichkeit, mehrere Kandidaten eines Transkripts und dessen Wahrscheinlichkeit zu erhalten.

Parametrisierung

Deepspeech bietet einige Einstellungsmöglichkeiten, mit denen man experimentieren kann. Um zusammengehörende Buchstabenfolgen zu finden, wird CTC (Connectionist Temporal Classification) verwendet. Optional ist es möglich, einen zusätzlichen externen sprachabhängigen Scorer zu verwenden, der ermittelt, mit welcher Wahrscheinlichkeit eine zusammenhängende Buchstabenfolge ein bestimmtes Wort einer Sprache ergibt und dann das wahrscheinlichste Wort auswählt. Der Scorer kann die Zuverlässigkeit der Spracherkennung nochmal deutlich erhöhen. Mozilla bietet einen zu ihren Trainingsdaten passenden Scorer an, den ich für meine Tests verwendet habe. Für einen Sprachassistenten à la Alexa benötigt man einen Scorer, der unzählige Wörter und deren relative Wahrscheinlichkeit in der jeweiligen Sprache enthält. Für sehr spezifische Einsatzzwecke ist ein Scorer mit einer auf diesen Einsatzzweck beschränkten Wortauswahl am zuverlässigsten. Aber auch ohne explizites Sprachmodell sind die Tarnscripte beeindruckend gut.

Ein eher kritischer Parameter ist die „Beam Width“, die von der CTC-Klassifikation verwendet wird. Um ehrlich zu sein weiß ich nicht zu 100%, was genau die „Beam Width“ beschreibt, aber größere Werte führen laut Dokumentation zu besseren Ergebnissen auf Kosten der Rechenzeit. Ursprünglich habe ich den Wert unverändert gelassen, was dazu führte, dass nur einzelne Lautfetzen erkannt wurden. Nach etwas Experimentieren und einer kurzen Recherche in der Dokumentation konnte ich mit einem Wert von 500 erste Erfolge erzielen.

Der gesamte Beispielcode kann hier gefunden werden.

Zurück
Zurück

Die 6 häufigsten Fehler bei Softwareprojekten