Wir haben nichts auszusetzen.
„America, A plague in England's shame. Poor greatness, save our nightingale! Romeo, forgive me into myself, So thou no more but furnight, English peace.“ -Phasmid
Die Idee ist das Erstellen einer Selbstlernenden Künstlichen Intelligenz, welche gegebenes Textverhalten imitieren und selber Texte/Nachrichten verfassen kann. Hierfür muss eine Textquelle analysiert und ausgewertet werden können, Muster der Sprache erkannt und erlernt werden, sowie diese Anwendung im „unterrichten“ der KI finden.
Die Umsetzung des Projektes stellte sich als deutlich weniger geradlinig heraus, als Anfangs angenommen. Hierfür waren mehrere Faktoren verantwortlich. Einerseits ist der Umfang des Bereichs des Maschinellen Lernens, für welchen wir uns entschieden hatten, ein sehr großer, in welchen der Einstieg schwer fällt. Dies liegt unter Anderem daran, dass sich die Konzepte auf komplexe stochastische Modelle zurückführen lassen, deren Anwendung das Programm durchführt (Wie das funktioniert, versuchen wir später genauer zu erklären). Außerdem stellten sich mehrere unserer Ansätze als mehr oder minder ungeeignet heraus, sodass große Teile der Arbeit der ersten Wochen für sich genommen zwar funktionierten, in das fertige Projekt jedoch kaum einflossen. Auch unsere erste Idee einen „Chatbot“ zu erstellen, haben wir nur teilweise zum Ende geführt. Zwar kann unser Programm wunderbare Texte generieren und nach Trainingsstil zwischen Donald Trump und Shakespeare variieren, es kann jedoch (noch) nicht in Echtzeit auf andere Nachrichten reagieren etc.
Für das Verständnis des Projektes und des Projektcodes ist es wichtig einen kleinen Einblick in Maschinelles Lernen (ML) zu erhalten, welchen wir versuchen in aller Kürze hier zu vermitteln. Wie bereits erwähnt ist das Konzept des Maschinellen Lernens ein durchaus komplexes und nicht unbedingt intuitives, trotzdem wird es auch in den letzten Monaten und Jahren immer mehr zu einer Art Modewort innerhalb der Medien.
Keine Sorge so ist es (noch) nicht. Klar sollte erstmal werden, dass ein Computer nicht in derselben Weise lernen kann, wie es ein Mensch tut, weswegen man eventuell direkt den Term „Lernen“ als abstraktere Umschreibung sehen sollte. Vielmehr beschreibt dies die Fähigkeit eines Programmes gewisse Strukturen in Daten zu erkennen und anhand dieser stochastische Vorhersagen zu treffen. Die Genauigkeit dieser Vorhersagen wird meistens als Qualitätsmaß eines Machine Learning Programms gesehen. Anwendung findet diese Idee der Programmierung in immer vielfältigeren Bereichen, weswegen der „Medien-Hype“ vermutlich sogar gerechtfertigt ist, da so teilweise Lösungen zu Problemen gefunden werden können, bei denen wir überhaupt nicht wüssten, wie man eine explizite Aufgabenstellung an ein Programm stellen könnte. (Paradebeispiel hierfür ist die menschliche Sprache, da wir diese zwar verstehen und immer wieder benutzen, jedoch kaum Möglichkeiten haben dieses Konzept mit etwas Anderem als Sprache zu beschreiben.) Dies war ausschlaggebend, warum wir uns entschieden haben mit Machine Learning zu arbeiten.
Die meisten Machine Learning Programme basieren auf sog. Neuronalen Netzwerken, auch dieses wieder eine abstrakte Umschreibung entsprungen aus den Anfängen des maschinellen Lernens. Die Grundidee damals war, das menschliche Gehirn zu imitieren mithilfe von Verknüpfungen, welche denen in unserem Gehirn ähneln sollten. Anschaulich ist dieses Bild tatsächlich immer noch bis zu einem gewissen Level anwendbar.
Das Prinzip funktioniert folgendermaßen: Das neuronale Netz erhält einen Input. Dieses sind meistens vektorisierte Daten, das heißt die Ursprungsinformationen werden in Vektoren oder Matrizen bestehend aus Zahlen verpackt, damit diese verwendbar für das Programm werden. Dieser Input kann aus beliebig vielen „Features“ bestehen, welche dann das Format des Eingangs bestimmen, jedes Feature wird sozusagen ein Neuron. Diese werden mit der nächsten Schicht aus Neuronen verknüpft, sodass jeder Eingang mit jedem Neuron verbunden ist. Die Anzahl der Neuronen ist hier arbiträr und kann größer oder kleiner gemacht werden, abhängig beispielsweise davon, wie komplex das Problem, welches man untersuchen will, ist. Nun rät zu Beginn das Programm eine gewisse Abhängigkeit, zwischen der Input und der nächsten Schicht und übergibt davon abhängig einen Wert an jedes einzelne Neuron. Heruntergebrochen könnte man sagen, verschiedene Neuronen werden aufgrund dieser (zu Beginn geratenen) Abhängigkeiten aktiviert oder deaktiviert. Nun könnte man noch noch beliebig viele dieser Schichten übereinander stapeln (Wieder um die Komplexität der Erfassung des Programms zu erhöhen), der Einfachheit halber bleiben wir aber für die Erklärung bei nur einer sog. Hidden Layer. Nun werden erneut Verknüpfungen von der Hidden Layer zu unserem Output erzeugt, welcher dann wieder aufgrund (zu Anfangs geratener) Gewichtungen in gewisser Weise angesteuert wird und anhand dessen eine Vorhersage trifft. Und erst jetzt beginnt das eigentliche Lernen: Die Vorhersage wird mit dem erwarteten Output verglichen. So könnten wir zum Beispiel ein Programm zur Erkennung von Handschrift geschrieben haben und ein geschriebenes „a“ übergeben haben, jedoch die Vorhersage: „z“ erhalten (alles natürlich dann im vektorisierten Format). Offensichtlich ist unser Programm noch nicht perfekt, es hat aber ja auch noch nicht gelernt. Der Weg wie wir nun die Vorhersagen für die Zukunft verbessern können ist, dass wir basierend auf dem Unterschied zwischen Vorhersage und erwartetem Output alle Gewichtungen innerhalb des Netzwerkes minimal verändern und anschließend das Programm erneut, aber mit diesen Gewichtungen und anderen Trainingsdaten, laufen lassen. Nach vielen Iterationen verbessert sich das Modell und die Gewichtungen der einzelnen Neuronen stellen bestenfalls eine erkannte Struktur dar, welche sichere Vorhersagen treffen kann. Wichtig ist: Dies war nur eine kurze und vage Zusammenfassung. In Wirklichkeit haben die meisten Netze mehrere Millionen solcher Neuronen oder Knoten und benötigen sehr viele Iterationen zum Lernen, weswegen das Ausführen der Programme teils Stunden in Anspruch nehmen kann.
Wer noch genauer verstehen möchte, wie diese Netze funktionieren und wie beispielsweise die Gewichtungen berechnet werden oder welche Probleme beim Lernen auftreten können, sei beispielsweise die vier Videos umfassende Youtube Serie vom Youtube-Channel 3Blue1Brown empfohlen, welche knapp aber ohne ungenau zu sein das Thema umreißt. Zu finden ist diese unter: https://www.youtube.com/playlist?list=PLZHQObOWTQDNU6R1_67000Dx_ZCJB-3pi.
Doch nun endlich zum konkreten Projekt. Für unser Programm haben wir uns für ein GRU (Gated Recurrent Unit) Model entschieden. Dieses ist besonders nützlich für Daten, die mit Text zusammenhängen, da das GRU ein gewisses Gedächtnis besitzt und somit Umgebungen von beispielsweise Wörtern oder Buchstaben erkennen kann. Dies funktioniert indem der Output eines Zeitschrittes sowohl ausgegeben wird, als auch an den nächsten Zeitschritt weitergegeben werden kann. In unserem Model versuchen wir anhand von Buchstaben und deren Umgebung nachfolgende Buchstaben vorherzusagen und somit die Semantik und teilweise die Bedeutung des Ursprungstextes zu erfassen und diese in einem frisch generierten Text zu imitieren. Für den Input benötigen wir zuerst einen Ursprungstext, auf dessen Basis wir unser Model trainieren können. Wichtig für jegliche Form von maschinellem Lernen sind große Datenmengen, weswegen die einzige Bedingung an den Text eine nicht allzu geringe Zeichenmenge ist. Für das Testen des Modells haben wir beispielsweise die gesammelten Werke von Shakespeare oder einige Speeches von Donald Trump verwendet.
Um jedoch die Daten verarbeiten zu können teilen wir zuerst den Text in Sequenzen ein (z.B. 100 Zeichen lang). Die Trainingsdaten werden dann diese 100 Zeichen sein und der erwartete Output sind die gleichen Zeichen jedoch um ein Zeichen verschoben. Somit hat jedes Zeichen das Ziel, das darauf folgende Zeichen vorherzusagen. Um die Zeichen in unser Modell speisen zu können benötigen wir zuerst eine eindeutige vektorisierte Darstellung für jedes Zeichen, weswegen wir jedem individuellen Zeichen eine Zahl zwischen 0 und „Anzahl der unterschiedlichen Zeichen - 1“ zuweisen und anschließend unsere einzelnen Sequenzen in ein Array aus Zahlen umwandeln. Der erste Teil unseres Modells ist dann ein sog. Embedding. Dieses soll eine verbesserte vektorisierte Darstellung unser Eingabe erlernen und weitergeben, welche nicht nur die zugewiesene Zahl zu jedem Zeichen versteht sondern anhand der Umgebung der Zeichen eine verbesserte Darstellung der Sequenzen innerhalb unseres Modells erstellt. Die Ausgabe unseres Embeddings wird nun in das oben angesprochene GRU gegeben. Im obigen Bild zur GRU ist also, die Eingabe \(x_t\) (t = 0, 1, …, Länge der Sequenz) das t-te Zeichen der Sequenz und versucht das (t+1)-te Glied vorherzusagen (als Output \(o_x\)) und dies für alle Glieder der Sequenz. Ermittelt wird der Output anhand des Inputs von außen und dem Output des vorherigen GRUs, mithilfe einer Vergessens- und einer Updatefunktion. Die Vergessensfunktion sorgt dafür, dass alte Information von früheren Zeitschritten nicht zu stark gewichtet wird, wohingegen die Updatefunktion für das implementieren des neuen Inputs sorgt. Diese sind mathematisch wie folgt beschrieben: \[ \text{Update: } z_t = \sigma (W^{(z)}x_t+U^{(z)}h_{t-1}), \quad \text{Vergessen: } r_t = \sigma (W^{(r)}x_t+U^{(r)}h_{t-1}) \] wobei \(W\) für die Gewichtung des Inputs \( x_t \) steht und \(U\) für die Gewichtung des Inputs des vorherigen GRU Gates \( h_{t-1} \). Die \(\sigma\)-Funktion, sorgt hierbei dafür, dass das Ergebnis in einem einheitlichen Bereich liegt (zwischen 0 und 1). Hieraus wird dann die Ausgabe erzeugt, welche außerdem an die nächste GRU Zelle weitergegeben wird. Aus allen GRU Ausgaben erhalten wir dann mithilfe einer letzten Layer unsere endgültige Vorhersage als Vektor. Nachdem man dieses Modell dann lange genug trainiert hat und somit die Gewichtungen optimiert, können wir hiermit die Generierung eines Textes vornehmen. Hierbei beginnen wir mit einem sog. Seed, also einem Start-Text, auf welchem wir dann den Text aufbauen. Dieser wird dem Modell übergeben, welches hierauf basierend eine Ausgabe für den nächsten Buchstaben vorhersagt. Diese Vorhersage wird dann für den nächsten Buchstaben wieder dem Modell übergeben und dies wiederholen wir so oft, wie unsere Ausgabe lang sein soll. Wir erhalten so unsere Ausgabe und haben hiermit eine Textgenerierung vorgenommen. Spannenderweise schafft das Programm, obwohl es keine anderen Daten über die Texte oder Wissen über die Sprache erhält, Texte zu erzeugen, die echter Sprache erstaunlich ähneln und den Stil des Ursprungstextes erstaunlich gut treffen. Die generierten Texte treffen auch die Grammatik erstaunlich gut und sind semantisch in der nahen Wortumgebung sinnig. Einzig größere Zusammenhänge innerhalb des Textes kann das Programm kaum bis gar nicht erfassen, so ergeben die Sätze meist kontextuell keinen Sinn und wirken eher wie gute Imitation als Reproduktion.
Wir haben den Code in einem interaktiven Notebook über Google Colabatory erstellt. Unter folgendem Link findet ihr das ausführbare Programm, mit Erklärungen. Dafür einfach die Anweisungen zum Beginn des Notebooks befolgen. https://colab.research.google.com/drive/1DrhbVbUbDL8KCqUw-Jtut-PeBDiUIhCt Alternativ ist hier auch noch einmal eine downloadbare offline Version des Notebooks verfügbar, zusammen mit Modellparametern, welche sofort die Textgenerierung ohne neues trainieren ermöglichen: phasmid_textgenerierung.zip.
Zum Abschluss unseres Projektes sind wir sehr zufrieden mit dem Endergebnis. Besonders interessant war es sich in dieses doch sehr komplexe Thema einzuarbeiten und ein Grundverständnis für Maschinelles Lernen zu erhalten, welches vermutlich in der Zukunft noch wichtiger wird. Genau diese Komplexität war aber auch eines der größten Probleme mit unserem Projekt. Es hat sehr lange gedauert, bis wir tatsächlich mit unserem Projekt wirklich beginnen konnten. So haben wir zwar auch zu Beginn versucht einiges für unser Projekt zu erarbeiten, jedoch konnten wir davon im Endeffekt praktisch nichts für unser finales Produkt benutzen. Genau diese Realisation hat gegen Ende auch zu gewisser Frustration geführt. Ich denke dies liegt in der Natur von Projekten die mit Maschinellem Lernen zu tun haben und ist eventuell für einen solchen Rahmen eher ungeeignet, unter Anderem auch, da wir uns somit weniger mit „Programmieren“ beschäftigen konnten. Trotzdem ist es wirklich sehr beeindruckend, welche Ergebnisse man mit verhältnismäßig geringem Aufwand erhalten kann. Sehr hilfreich war außerdem die Entdeckung des Google Services „Google Colaboratory“, einem sog. Notebook welches die Möglichkeit gibt online Code zu schreiben und auszuführen. Was jedoch Google Colab ausgezeichnet ist die GPU-Acceleration. Wie bereits angedeutet benötigt Machine Learning eine ungeheure Menge an Rechenaufwand, sodass Programme oft mehrere Stunden laufen müssen bevor man, selbst bei überschaubaren Projekten wie unserem, Ergebnisse auswerten kann (Was sehr frustrierend ist, wenn es dann nicht so funktioniert, wie man es sich erhofft hätte…). Jedoch kann diese Rechenzeit drastisch verkürzt werden, wenn man eine dedizierte Grafikkarte (GPU) verwendet. Google Colab stellt nun die Möglichkeit zur Verfügung leistungsstarke Recheneinheiten und eben Grafikkarten von Google zu verwenden und somit die Programme innerhalb von Minuten auszuführen. Eine super Entdeckung, welche vor Allem gegen Ende hin enorm geholfen hat. Tatsächlich ist auch nur noch wenig an unserem Projekt in unseren Augen zu verbessern und wir haben das meiste erreicht, was wir uns zu Beginn vorgestellt hatten. Eine Weiterentwicklung könnte sein den Bot in Echtzeit Daten analysieren zu lassen und somit tatsächliches Sprachverhalten in bspw. einem Chat mimen zu lassen. Auch ein besseres Verständnis für Kontext und Bedeutung von Wörtern wäre spannend und man könnte eventuell anstatt auf Zeichenbasis eventuell auf Silben- oder Wortbasis arbeiten um dies zu erreichen. Trotzdem sind wir sehr zufrieden mit unserem Endprodukt und während ich hier schreibe, schreibt Phasmid bereits ein neues unentdecktes Theaterstück von Shakespeare…
Hier findet ihr unsere wöchentlichen Protokolle.
Das Programm funktioniert definitiv mit folgendem: