Kapitel 9: Rekurrente Netze

Updates dieser Seite:

Überblick

In diesem Kapitel geht es um Rekurrente Neuronale Netze (RNN). Das sind Netze, die Zyklen aufweisen und somit eine Art Gedächtnis haben, da sie Informationen aus der Vergangenheit verarbeiten können. Wir sehen uns RNN im Kontext von Textdaten an und müssen daher Konzepte der Sprachverabeitung (Natural Language Processing, NLP) anschauen. Anschließend lernen wir die grundlegende Architektur von RNN kennen und darauf aufbauend die Idee von Gated Units anhand von LSTM (Long Short-Term Memory) und GRUs (Gated Recurrent Units).

Konzepte

Bag of Words, Word Embeddings, Elman-Netze, Bidirektionale RNN, Long Short-Term Memory (LSTM), Gated Recurrent Units (GRU)

Datensatz

Name Daten Anz. Klassen Klassen Trainings-/Testdaten Ort
IMDB Texte (Filmreviews) 2 negativ (0), positiv (1) 25000/25000 Abschnitt 5

1 Sequentielle Daten

Bislang hatten wir es mit hauptsächlich mit Bilddaten zu tun. Bei Bildern hat man eine feste Eingabegröße, z.B. ein Bild mit 28x28 Pixeln. Da liegt es nahe, für jedes Pixel ein Neuron in der Eingabeschicht zu einzurichten, also 784 Neuronen für 28x28 Pixel.

Bei Sprach- oder Textdaten hat man zunächst mal das Problem, ein Wort zu repräsentieren. Aber auch wenn man jedes Wort als einen Zahlenvektor mit fixer Länge $n$ repräsentieren kann, hat man das Problem, dass ein Satz keine feste Länge hat. Wie soll man einen Satz in ein Neuronales Netz einspeisen?

Solche Daten wie Sätze nennt man sequentielle Daten. Weitere Beispiele sind:

Allen Beispielen ist gemein, dass es sich um eine Sequenz von Einzeldaten handelt und diese Einzeldaten wiederum eine feste Größe haben: ein Ton, ein Audiosample, ein 2D-Bild. Wir könnten also einem Neuronalen Netz eine Sequenz von Daten nacheinander zuführen. Das erfordert allerdings, dass ein NN sich an die vorigen Elemente erinnert. Unsere bisherigen Netze waren Feedforward-Netze (auch die CNN), weil ein Neuron von Schicht $l$ nur auf ein Neuron in einer höheren Schicht $k$, also mit $k > l$, zeigen konnte. Wir lernen jetzt Netze kennen, wo Verbindungen aus in niedrigere Schichten, also "zurück" zeigen können. Damit ist das Netz kein DAG mehr (directed acyclic graph), da diese Rückwärtsverbindungen Zyklen einführen. Dieser Typ Netz nennt sich Rekurrentes Neuronales Netz oder RNN.

1.1 Szenarien

Wir wissen jetzt, was sequentielle Daten sind, aber welchen Einsatzzwecke hat das Neuronale Netz? Hier unterscheiden wir die folgenden Typen:

Wir gehen alle Typen durch.

In dem Artikel The magic of LSTM neural networks sind einige der Beispiel schön illustriert.

Many-to-One

Many-to-One bedeutet: Eine Sequenz wird eingegeben und eine Kategorie wird ausgegeben.

Im Bereich Sentiment Analysis geht es um die Aufgabe, einen Text nach seiner Bedeutung bzw. Aussage zu klassifizieren. Im einfachsten Fall ist das eine binäre Klassifikation, z.B. ob eine Filmrezension positiv oder negativ ist wie beim sogenannten IMDB Dataset.

Ganz allgemein beschäftigt man sich bei der Textklassfikation mit der automatischen Kategorisierung von Texten. Zum Beispiel, ob eine e-Mail SPAM oder kein SPAM ist, oder in welche Kategorie eine Supportanfrage gehört, damit die Nachricht automatisch in die richtige Abteilung geleitet wird.

Bei der Speaker recognition/identification geht es darum, anhand eines Audiosignals festzustellen, wer dies gesprochen hat. Jeder mögliche Sprecher ist also eine Kategorie. Die binäre Variante wäre dann Speaker verification/authentication, wo es nur darum geht, ob ein Audiosignal von einem bestimmten Sprecher produziert wurde oder nicht.

Im Bereich Activity recognition geht es darum, aufgrund von Videodaten relevante Aktivitäten zu entdecken. Im Bereich öffentliche Sicherheit können das verdächtige Aktivitäten sein, die auf Gewalt oder Gefahr hindeuten. Im Smart-Home kann das ein Gestenbefehl des Benutzers sein. Oder es geht darum, eine Kollektion von Filmschnipseln mit Aktivitäten zu annotieren, so dass man besser darin suchen kann.

Many-to-Many

Many-to-Many bedeutet: eine Sequenz wird eingegeben und eine Sequenz wird ausgegeben. Das typische Beispiel ist Machine Translation, wo z.B. ein englischer Satz eingegeben wird und ein deutscher Satz ausgegeben wird. In diesem Fall ist die Länge der Sequenzen unterschiedlich.

Bei der Named Entity Recognition ist das Ziel, bei jedem Wort eine Aussage darüber zu treffen, ob es sich um einen Eigennamen handelt (z.B. um in einem Text Personen, Geschäfte oder Orte zu identifizieren). Das heißt, die Eingabesequenz ist ein Text und die Ausgabesequenz besteht aus Nullen und Einsen und beide Sequenzen sind gleich lang.

One-to-Many

One-to-Many bedeutet: aufgrund einer Eingabe mit fester Größe (z.B. eine Zufallszahl oder eine feste Anzahl von Anfangsnoten) wird eine Sequenz generiert. Dies kommt etwa im Bereich Musik, aber auch zum Generieren von Texten vor. Bei der Musik kann die Eingabe ein Genre sein, entsprechend wird neue Musik in diesem Stil "komponiert". Bei Texten kann es ein Autor wie "Shakespeare" als Eingabe sein und es wird ein entsprechender Text generiert.

Ein weitere Anwendung ist das Image Captioning: Hier ist die Eingabe ein Bild mit fester Größe und die Aufgabe ist, eine möglichst gute Bildbeschreibung (engl. caption) als Text zu generieren.

2 Textdaten

Wie kann man einen Text variabler Länge, z.B. den Satz "the cat sat on the mat", einem neuronalen Netz zuführen? Der nächste Satz könnte weniger oder mehr Wörter enthalten. Bei einem Netz müssen wir aber entscheiden, wie viele Input-Neuronen es gibt.

2.1 Wörter und Wortindex

Das führt zunächst zu der Frage, wie man ein einzelnes Wort wie "cat" oder "the" repräsentiert. Wir gehen von einem Vokabular $V$ von Strings mit fixer Größe $|V|$ aus, in dem jedes mögliche Wort mit einem eindeutigen Index (Zahl) gelistet ist (man spricht auch manchmal von einem Dictionary).

Die trivialste Möglichkeit ist, jedes Wort mit seinem Index im Vokabular $V$ zu repräsentieren, z.B. 5 für "the", 1 für "cat", 2 für "mat" usw. Dann wäre der Satz "The cat sat on the mat" eine Folge von Zahlen [5, 1, 4, 3, 5, 2].

Die Elemente von $V$ ("cat", "mat"...) nennt man auch Tokens.

Die Größe eines Vokabulars kann für kleine "Spielzeugsysteme" im Bereich 10000 oder 20000 liegen, für echte Systeme ist aber eher im Bereich 100000, 1 Million oder 100 Millionen realistisch.

Da die Größe von $V$ begrenzt ist, enthält $V$ in der Regel ein spezielles Token, das unbekannte Wörter repräsentiert. Dieses wird oft als <UNK> (für unknown) bezeichnet. Je nach Anwendung sind Tokens für Satzzeichen oder für das Satzende <EOS> enthalten, z.B. wenn man das Satzende vorhersagen möchte.

Das Problem mit dieser Repräsentation ist, dass Ähnlichkeit von Wörtern nicht einer numerischen Ähnlichkeit (Distanz bzw. Differenz) entspricht. Das Wort "cat" ist zum Beispiel rein zufällig sehr nah an "mat", obwohl es keine semantische Ähnlichkeit gibt. Wörter wie "dog" oder "tiger" sollten viel näher an "cat" sein. Diese Diskrepanz erschwert einem Neuronalen Netz die Arbeit und wird heutzutage im Bereich NN nicht verwendet.

Außerdem haben wir mit dieser Repräsentation noch nicht das Problem gelöst, einen Satz mit beliebiger Länge in ein Netz mit fixer Anzahl von Input-Neuronen einzuspeisen. Die "naive" Idee, einfach sehr viele Input-Neuronen zu definieren (z.B. 100 Neuronen) und die nicht benutzten Neuronen (bei 5 Wörtern wären das die restlichen 95 Neuronen) mit 0 zu belegen, erweist sich in der Praxis als nicht nutzbar.

2.2 One-Hot Encoding und Bag of Words

Wir können unsere Wörter mit dem bekannten One-Hot-Encoding repräsentieren. Ein Wort $w$ mit Index $i$ wird als Vektor $v = (0, 0, \ldots, 0, 1, 0, \ldots, 0)$ der Länge $|V|$ repräsentiert. Vektor $v$ hat genau eine 1 an der Stelle $i$ und sonst Nullen. Man nennt solche Vektoren auch sparse.

Das Wort "cat" könnte so aussehen - bei einem sehr kleinen Vokabular mit $|V|=5$:

$$\left( \begin{array}{c} 1 \\ 0 \\ 0 \\ 0 \\ 0 \\ 0 \end{array} \right)$$

Schauen wir uns ein Beispiel für dieses kleine Vokabular an:

Wenn wir die Wörter einzeln einem Neuronalen Netz zuführen würden, hätten wir folgende Inputs für den Satz "the cat sat on the mat":

Wenn wir für den ganzen Satz einen einzigen Output generieren möchten - z.B. SPAM oder Nicht-SPAM - dann haben wir hier ein Problem, denn wir generieren ja fünf Outputs. Außerdem ist jeder Output unabhängig von den anderen Outputs, d.h. man kann Zusammenhänge zwischen den Wörtern nicht abbilden.

Wie kodieren wir also einen ganzen Satz als Input für ein Netz mit fixer Eingabegröße? Ein Satz kann schließlich unterschiedlich lang sein.

Ein häufiger Ansatz ist es, einfach alle Wortvektoren zu addieren, so dass man immer einen Summenvektor der Länge $|V|$ erhält. Diese Repräsentation nennt man Bag of Words (ein "Sack voll Wörter"). Im Grunde spiegelt dieser Vektor die Häufigkeitsverteilung aller Wörter, die in dem Satz vorkommen, wider:

$$ \mbox{the cat sat on the mat} \quad\Rightarrow\quad \left( \begin{array}{c} 1 \\ 1 \\ 1 \\ 1 \\ 1 \\ 2 \end{array} \right) $$

(Dieser Bag-of-Words-Vektor ist in der Regel auch "sparse", d.h. es gibt z.B. 10000 Stellen, von denen nur einige wenige nicht gleich Null sind.)

Man beachte, dass die Information der Wortreihenfolge (wo steht welches Wort im Satz) verloren geht. Für viele einfache Anwendungen reicht es aber schon aus, nur das Vorkommen bestimmter Wörter zu verarbeiten, unabhängig davon, wie genau diese angeordnet sind. Man denke an SPAM-Detection oder eine einfache Kategorisierung von Textdokumenten. Man kann sich vorstellen, dass hier bestimmte Signalwörter eine größere Rolle spielen als die Reihenfolge (man spricht auch von Keyword Spotting). Umgekehrt gibt es natürlich viele Beispiele, wo die Reihenfolge der Wörter ausschlaggebend für die Bedeutung ist. Typische Beispiel sind Negationen, zum Beispiel "du, nicht ich" versus "nicht du, ich" (beide haben eine identische Bag-of-Words-Repräsentation).

2.3 Word Embeddings (Wort-Feature-Vektoren)

Die dritte Möglichkeit sind Word Embeddings. Das ist sozusagen eine Zwischenlösung zwischen den beiden Möglichkeiten oben. Wir geben eine beliebige Anzahl von Features $m$ vor, z.B. $m=2$, und möchten für jedes Wort eine Repräsenation als Vektor der Länge 2 finden. Die Elemente dieses Feature-Vektors beschränken sich nicht auf 0 und 1, sondern können beliebige Dezimalzahlen sein. Wichtig ist, dass die Länge $m$ der Word-Embedding-Vektoren deutlich geringer ist als die Größe des Vokabulars $|V|$, zum Beispiel $m=30$ bei einem Vokabular von $|V| = 17000$.

Hier ein Beispiel:

Natürlich gilt auch hier das Argument von Option 2: die Bedeutungsnähe von Wörtern sollte eine Entsprechung in der numerischen Nähe der Vektoren haben.

Jetzt werden diese Repräsentation aber gelernt (s.u.) und die Ähnlichkeiten stellen sich tatsächlich im Laufe des Lernens ein. Am Ende des Lernprozesses hat man eine "Tabelle" wie oben, wo man für jedes Wort die 4-dimensionale Kodierung (Einbettung) als Feature-Vektor der Länge $m$ ablesen kann.

Jede Dimension in der Abbildung oben nennt man Feature und man kann sich das als einen semantischen Aspekt des Worts vorstellen, der mit einem Wert zwischen 0 und 1 (oder einer anderen Skalierung) belegt ist. Mögliche Aspekte sind "belebt-unbelebt". Es kann auch eine Formeigenschaft wie "kurz-lang" oder eine Bewegungseigenschaft wie "schnell-langsam" oder ein Persönlichkeitsmerkmal wie "introvertiert-extrovertiert" sein. Da die Embeddings gelernt werden, richtet sich die Repräsentation nach den Daten und dem Ziel der Klassifikationsaufgabe.

Beim Word Embedding unterscheidet man also zwischen Größe des Vokabulars $|V|$ und der Anzahl der Features $m$. In realen Anwendungen ist die Größe des Vokabulars z.B. 100000 und die Anzahl der Features im Bereich 100 oder 30.

Lernen von Word Embeddings (Neuronales Sprachmodell)

Bengio et al. (2003) haben eine Methode vorgestellt, um Wort-Feature-Vektoren zu lernen. Man spricht auch von Neuronalen Sprachmodellen (Neural Language Model). Die Grundidee ist es, ein Zwei-Schichten-Netz auf die Aufgabe zu trainieren, das jeweils nächste Wort in einem Satz vorherzusagen auf Basis der letzten $n$ Wörter, wobei $n$ fix ist und z.B. 2 oder 3 beträgt.

Zur Erklärung der Aufgabe: Bei dem Satz

the cat sat on the mat

möchte man also, dass das Modell folgende Vorhersagen macht:

the cat -> sat
cat sat -> on
sat on -> the
on the -> mat

Im Beispiel haben wir $n=2$ angenommen. Dieses Problem löst man normalerweise mit Hilfe von Statistik, indem man einfach die Häufigkeit von Wortfolgen zählt und damit die bedingte Wahrscheinlichkeit $P(w_3 | w_1, w_2)$ schätzt.

Wir sehen diese Aufgabe aber nur als Mittel zum Zweck. Der Einfachheit halber reduzieren wir das Problem auf die Vorhersage des nächsten Worts auf Basis des vorangegangenen Worts, also für $n=1$. Jetzt betrachten wir ein Netz, das als erste Schicht ein Mapping von One-Hot-Vektoren (Länge $|V|$) auf einen Feature-Vektor, der die Länge $m$ hat. Beispielhafte Werte sind $|V| = 17000$ und $m=30$ (aus Bengio et al., 2003).

Das Mapping von One-Hot-Vektor zu Feature-Vektor kann man mit einer Matrix $E$ der Größe $m \times |V|$ darstellen. Bei Eingabe eines Worts $x_o$ in One-Hot-Enkodierung berechnet man durch Matrizenmultiplikation den Feature-Vektor $x_f$:

$$ x_f = E \, x_o $$

Wir können uns jetzt ein Netz konstruieren mit $|V|$ Eingabeneuronen und $m$ Neuronen in der zweiten Schicht. Die dritte Schicht würde das nächste Wort vorhersagen, muss also $|V|$ Ausgabeneuronen mit Softmax-Aktivierung haben.

Man kann sich die Netzarchitektur so vorstellen:

Es zeigt sich, dass sich durch das Trainieren eines solchen Netzes sehr gute Embeddings in der Matrix $E$ ergeben. Nachteil ist die hohe Anzahl der Parameter.

Interessant an dem Ansatz ist auch, dass es bei dem beschriebenen Netz nicht um die eigentliche Aufgabe (nächstes Wort vorhersagen) geht, sondern um die Matrix $E$ als Word Embedding. Die Aufgabe der Wortvorhersage lässt sich mit anderen Mechanismen tatsächlich besser lösen.

Einem aktuelleren Ansatz namens Word2Vec (Mikolov et al. 2013), entwickelt von Google, liegt die Idee zugrunde, dass nicht aufeinander folgende Wörter betrachtet werden, sondern beliebige Wörter. In einem Satz wird also ein Wort zufällig herausgepickt, welches vorhergesagt werden soll (Target), und ein weiteres zufälliges Wort (oder mehrere) in dem Satz wird als "Hinweis" ausgewählt (Kontext). Dieses Verfahren ist nochmal effizienter als das von Bengio et al.

Wir haben hier die beiden Verfahren der Neuronalen Sprachmodelle und Word2Vec natürlich nur sehr grob skizziert. Wichtig ist, dass eine Embedding-Schicht eine One-Hot-Repräsentation eines Worts (z.B. ein Vektor der Länge 10000 bei einem Vokabular von 10000 Wörtern) in einen wesentlich kompakteren Feature-Vektor, z.B. mit Länge 32, umwandelt.

2.4 Keras und die IMDB-Datenbank

Schauen wir uns die Prinzipien von One-Hot-Encoding mit einem Beispiel an. Der IMDB-Datensatz enthält Filmrezensionen. Für jede Rezension gibt es einen Label, der besagt, ob die Rezension positiv oder negativ war. Man kann den Datensatz also für binäre Klassifikation verwenden.

Beim Laden kann man die Größe des Vokabulars angeben. Wir geben hier zum Testen 20 an, d.h. nur die 20 häufigsten Wörter werden ins Vokabular aufgenommen, dass entsprechend die Größe 20 hat.

Der Datensatz enthält Sätze als Zahlenreihen. Die "2" steht für ein Wort, das nicht im Vokabular enthalten ist.

Ein Wortindex erlaubt uns zu sehen, welches Wort zu welcher Zahl gehört.

Wir schauen uns den ersten Satz in den Trainingsdaten an und konvertieren die Wörter zu einer One-Hot-Encoding.

Um den Bag-of-Words-Vektor zu berechnen, addieren wir alle Vektoren, und erhalte so einen fixen Input der Länge 20. Mit diesem Input könnte man ein konventionelles Feedforard-Netz trainieren.

Wir haben hier noch keine Word Embeddings gesehen. Wir werden aber Embedding-Schichten demnächst kennen lernen, wenn wir uns wieder Keras zuwenden.

Verständnisfrage

Was kommt heraus, wenn Sie Achse 1 verwenden?

np.sum(s, axis=1)

Überlegen Sie erst, bevor Sie es ausprobieren.

3 Rekurrente Neuronale Netze (RNN)

Nachdem wir wissen, mit welchen Daten wir arbeiten werden, wenden wir uns den Neuronalen Netzen zu. Wir erweitern unsere Feedforward-Netze (FNN) um rückwärts gerichtete (rekurrente) Verbindungen.

Schauen wir uns nochmal ein FNN mit einer versteckten Schicht an:

Jetzt soll unser Netzwerk z.B. in einem Satz $s = (w_1, \ldots, w_n)$ immer das nächste Wort vorhersagen. Wir geben also ein Wort $w_t$ ein, z.B. in One-Hot-Encoding, und die Ausgabe ist das hypothetische Wort $\hat{w}_{t+1}$.

Vergleichen Sie diese Sätze. Das eingeklammerte Wort soll von unserem Netz vorhergesagt werden:

Er holte [sein] Buch

Sie holte[ihr] Buch

Wenn wir nur das Wort "holte" in unser Netz eingeben, ist es unmöglich, eine "richtige" Entscheidung zu treffen. Dem Netz braucht mehr Kontext, in diesem Fall würde ein weiteres Wort reichen. Notwendiger Kontext kann auch über Satzgrenzen hinweg reichen.

Peter trat ein. Dann setzte [er] sich.

Maria trat ein. Dann setzte [sie] sich.

Damit wir diesen Kontext erfassen, benötigen wir eine Art Gedächtnis für unsere Netze. Genau darum geht es bei RNN. Es kann auch sein, dass der notwendige Kontext in der Zukunft liegt. Vergleiche:

Er holte sein [Auto] und fuhr nach München.

Er holte sein [Tagebuch] und schrieb los.

Bevor wir uns RNN ansehen, erinnern wir uns an die Formeln für Forward Propagation (in Vektorschreibweise):

$$ \begin{align} a^{(1)} &:= x \tag{Eingabeschicht}\\[3mm] z^{(2)} &:= W^{(1)}\, a^{(1)} + b^{(1)}\tag{Roheingabe}\\[3mm] a^{(2)} &:= g(z^{(2)}) \tag{Aktivierung}\\[3mm] \hat{y} &:= a^{(3)}\tag{Ausgabe} \end{align} $$

Wir beziehen uns im Folgenden auf diese Formeln und modifizieren sie für RNN.

3.1 Elman-Netz

Bei einem einfachen RNN wie dem sogenannten Elman-Netz (Elman 1990) wird die Ausgabe der versteckten Schicht $a^{(2)}$ bei der nächsten Eingabe wieder mit aufgenommen. Natürlich ist diese zusätzliche Eingabe (zusammen mit Vektor $x$) auch über Gewichte reguliert, die wir als Matrix $U$ bezeichnen.

Zum Begriff Elman-Netz: Ein Elman-Netz hat sogenannte Kontextneuronen. Die Zwischenschicht wird in diese Kontextschicht kopiert und im nächsten Schritt gehen die Neuronen dieser Kontextschicht wieder in die versteckte Schicht ein. Dies ist äquivalent zu dem Netz, das wir hier vorstellen. Bei der Gelegenheit: ein Jordan-Netz (Jordan 1990) ist wie ein Elman-Netz mit dem Unterschied, dass der Output $\hat{y}$ in die Kontextschicht kopiert wird.

In der Abbildung unten sind nur die Verbindungen von $a_1^{(2)}$ eingezeichnet, für die anderen zwei Neuronen gilt gleiches.

Wir vereinfachen die Schichtdarstellung. Die Zwischenschicht wird als Box dargestellt, $z$ und $a$ sind Vektoren. Die Tatsache, dass $a$ wieder eingespeist wird, wird durch den roten Pfeil dargestellt. Es handelt sich bei dem Netz jetzt nicht mehr um einen zyklenfreien Graphen (DAG).

Um zu klären, wann genau $a$ wieder eingespeist wird, führen wir einen Index $t$ für den Zeitschritt ein. Jetzt sehen wir, dass zum Zeitpunkt $t$ das Ergebnis der Zwischenschicht zum Zeitpunkt $t-1$ eingespeist wird.

Wir formalisieren wir das? Zunächst mal führen wir den Parameter $t$ für den Zeitschritt ein. In der Schicht 2 addieren bei den $a^{(2)}_{[t]}$ einfach die eigene Eingabe vom vorigen Zeitschritt, also $a^{(2)}_{[t-1]}$. Dazu haben wir eine eigene Gewichtsmatrix $U$. Sie finden das hier unter "Roheingabe" (die Matrix $W^{(1)}$ schreiben wir als $W$).

$$ \begin{align} z_{[t]} &:= W\, x_{[t]} + U \,a_{[t-1]} + b \tag{Roheingabe}\\[3mm] a_{[t]} &:= g(z_{[t]}) \tag{Aktivierung}\\[3mm] \hat{y}_{[t]} &:= a_{[t]}\tag{Ausgabe} \end{align} $$

Wenn $n$ die Größe der Eingabe $x$ bezeichnet und $n_2$ die Anzahl der Neuronen in der versteckten Schicht, dann ist $W$ eine $n_2 \times n$-Matrix und $U$ eine $n_2\times n_2$-Matrix.

Betrachten wir uns die Roheingabe:

$$ z_{[t]} := W\, x_{[t]} + U \,a_{[t-1]} + b $$

Wir definieren jetzt einen Operator, der Matrizen konkateniert (also zusammenfügt). Wenn die Matrizen $A$ und $B$ die gleiche Zeilenzahl haben, dann ist

$$ \left[ A ; B \right] $$

die zusammengesetzte Matrix, die sich ergibt, wenn man die Elemente von $B$ direkt rechts neben die Elemente von $A$ stellt. Ein Beispiel:

$$ \left[ \begin{pmatrix} a & b \\ c & d \end{pmatrix} ; \begin{pmatrix} e & f \\ g & h \end{pmatrix} \right] = \begin{pmatrix} a & b & e & f \\ c & d & g & h \end{pmatrix} $$

Der Operator funktioniert auch in der Vertikalen, dann muss die Spaltenzahl übereinstimmen:

$$ \left[\begin{array}{c} \left( \begin{array}{c} a \\ b \end{array}\right) \\[1mm] \left( \begin{array}{c} c \\ d \end{array}\right) \end{array} \right] = \left( \begin{array}{c} a \\ b \\ c\\d\end{array}\right) $$

Mit Hilfe der Konkatenation kann man die Formel für $z_{[t]}$ so formulieren (beide Matrizen haben Zeilenzahl $n_2$):

$$ z_{[t]} := \left[ W ; U \right] \, \left[\begin{array}{c} x_{[t]} \\[1mm] \,a_{[t-1]} \end{array} \right] + b $$

Die obige Darstellung erlaubt es uns, die Matrix $\left[ W ; U \right]$ als die eine Gewichtsmatrix der versteckten Schicht aufzufassen.

Eine alternative Darstellung "wickelt" das Verhalten über die Zeit auf in mehrere Netzwerke zu unterschiedlichen Zeiten $t$. Wir werden uns auch an diese Darstellung halten.

3.2 RNN-Architekturen

Das Elman-Netz, das wir oben gesehen haben, ist ein Beispiel für eine Many-to-Many-Architektur, wo die Länge der Eingabesequenz, die wir als $N_x$ bezeichnen, genauso groß ist wie die Länge der Ausgabesequenz $N_y$, also $N_x = N_y$.

Schauen wir uns noch weitere Architektur-Varianten an.

Bei der Many-to-One-Architektur wird eine Sequenz mit einem Klassenlabel versehen, z.B. bei der Sentiment Analysis (Filmreviews in positiv/negativ kategorisieren). Die Eingabe $x_{[1]}, x_{[2]}, \ldots$ besteht in diesem Fall aus Wörtern, die Ausgabe $\hat{y}$ ist eine Zahl $\in [0, 1]$ für die Wahrscheinlichkeit, dass ein Review positiv ist.

Bei der One-to-Many-Architektur will man Sequenzen generieren, z.B. ein Musikstück aus ein paar Anfangsnoten oder einen Text im Stile eines Autoren (der Autor ist die Eingabeklasse). Hier ist die Eingabe $x$ zum Beispiel eine Note und die Ausgabe $\hat{y}_{[1]}, \hat{y}_{[1]}, \ldots$ eine Sequenz von Noten.

Bei der Many-to-Many-Architektur gibt es noch die Encoder-Decoder-Variante. Dort ist die Eingabelänge $N_x$ nicht gleich Ausgabelänge $N_y$ ist, was typischerweise in der maschinellen Übersetzung der Fall ist.

Das funktioniert so, dass erst die Eingabe mit einem Netz - dem Encoder-Netz - verarbeitet wird. Das ist praktisch ein Many-to-One-Netz. Die Ausgabe des Encoder-Netzes wird dem Decoder-Netz übergeben, praktisch ein One-to-Many-Netz, das daraus die Ausgabe generiert.

Hier sind sowohl die Eingabe als auch die Ausgabe Sequenzen von Wörtern.

Wie gesagt: Diese Variante ist die typische Wahl für Übersetzungssysteme und überall, wo Eingabe und Ausgabe unterschiedlich lang sind.

3.3 RNN mit mehreren Schichten

Es ist naheliegend, ein RNN mit mehr als einer Zwischenschicht zu versehen. Hier sehen wir ein Drei-Schicht-RNN (wenn wir die Eingabeschicht mitzählen):

Bei FNN haben wir gesehen, dass man extrem effizient arbeiten kann, wenn man für einen kompletten "Batch" von Trainingsbeispielen (z.B. 64 Stück) zunächst die Zwischenschicht berechnet (Schicht 2) und anschließend für den Batch den Output (Schicht 3) berechnet.

Bei einem rekurrenten Netz muss man aufpassen. Nehmen wir an, wir haben einen Batch mit zwei Trainingsbeispielen, wobei wir die Inputsequenzen $(b_{[1]}, b_{[2]}, \ldots, b_{[n]})$ und $(c_{[1]}, c_{[2]}, \ldots, c_{[m]})$ nennen.

Bei der Batch-Verarbeitung werden die Werte $b_{[1]}$ und $c_{[1]}$ parallel verarbeitet, indem der Output der Schicht 2 berechnet wird. Bei der Berechnung von Schicht 3 kann aber parallel mit der Verarbeitung von $b_{[2]}$ und $c_{[2]}$ begonnen werden.

In der folgenden Abbildung sehen Sie die effiziente Berechnung einer Inputsequenz $(x_{[1]},x_{[2]},x_{[3]})$ in einem 3-Schicht-Netz. Sie sehen, dass vier Schritte nötig sind (Schritt = Berechnung innerhalb einer Schicht).

Im Grunde funktioniert ein rekurrentes Netz wie ein "vertieftes" Netz. Im abgebildeten Beispiel haben wir ein 3-Schicht-Netz, wo normalerweise zwei Schichten berechnet werden müssen (Schicht 2 und 3). Wenn wir eine Inputsequenz der Länge 3 haben, berechnen wir aber effektiv 4 Schichten. Bei einer Inputsequenz der Länge 200 (durchaus üblich bei Texten), entspricht dies also einem Netz der Tiefe 201 (wenn das eigentlich Netz drei Schichten hat).

Hinweis zu Keras: Oft haben wir es mit Many-to-One-Problemen zu tun, d.h. eine Inputsequenz wird auf ein Label abgebildet, so dass nur das letzte $\hat{y}$ relevant ist und alle vorigen Outputs ignoriert werden. Bei obigen Netz sieht man aber, dass die Output der Zwischenschicht $a^{(2)}$ durchaus in jedem Zeitschritt benötigt werden (im Gegensatz zu den $a^{(3)} = \hat{y}$. Daher muss man in Keras bei den nicht-finalen rekurrenten Schichten angeben return_sequences=True (Default ist False). Nur dann werden alle $a^{(2)}$ weitergereicht.

3.4 Backpropagation Through Time (BPTT)

Wie werden in einem RNN die Gewichte gelernt? Die Lernmethode ist tatsächlich nur eine leichte Modifikation von Backpropagation (BP) und nennt sich Backpropagation Through Time oder BPTT (Werbos 1974, 1990; Williams & Zipser 1989a, 1989b).

Wir skizzieren BPTT anhand eines Many-to-Many-RNN, wo zu jedem Zeitschritt eine Ausgabe produziert wird und mit einer korrekten Ausgabe verglichen werden kann. Hier nochmal als beispielhafte Abbildung:

Interpretation eines RNN als tieferes Netz

Dass BPTT nicht so verschieden von Backpropagation sein kann, wird vielleicht klar, wenn man sich ein RNN in der "aufgewickelten" Darstellung vorstellt, wie wir es beim Thema "RNN mit mehreren Schichten" getan haben. Dort haben wir gesehen, dass man sich ein RNN nach mehreren Zeitschritten als ein "normales" Netz mit vielen Schichten vorstellen kann.

In der folgenden Abbildung sehen wir ein RNN mit zwei Zeitschritten. Die linke Darstellung zeigt die "aufgewickelte" Version. Die Zahlen zeigen die Reihenfolge der Abarbeitung durch die zwei Schichten und über die zwei Zeitschritte. Die rechte Darstellung zeigt die Interpretation als "tieferes" Netz. Der zweite Zeitschritt wird als Zusatzschichten desselben Netzes angesehen.

Zielfunktion

Auch bei BPTT muss man eine Zielfunktion definieren.

Wir definieren zunächst $J_{[t]}$ (ohne Regularisierung) für einen einzelnen Zeitschritt $t$.

$$ J_{[t]} = - \sum_{i=1}^m \left[ y_{i, [t]} \; log(\hat{y}_{i, [t]}) + (1 - y_{i, [t]}) \; log(1 - \hat{y}_{i, [t]})\right] $$

Wir haben oben über die Komponenten $i$ der Vektoren $y$ (korrekte Ausgabe) und $\hat{y}$ (errechnete Ausgabe) summiert.

Jetzt summieren wird noch über die ganze Sequenz (z.B. Wörter), d.h. wir summieren über alle Zeitschritte $t$:

$$ J = \sum_{t=1}^{N_x} J_{[t]} $$

Die Fehlerfunktion ist fast identisch mit unserer Fehlerfunktion im herkömmlichen Backpropagation.

Backpropagation

Jetzt erinnern Sie sich sicher, dass Backpropagation im ersten Schritt einen Fehlerwert $\delta$ für jedes Neuron berechnet (ausgenommen Eingabeneuronen).

$$ \delta^{(l)} = \begin{cases} \hat{y} - y & \mbox{wenn}\quad l = L\\[3mm] (W^{(l)})^T \; \delta^{l+1} \odot a^{(l)} \odot (1 - a^{(l)})& \mbox{wenn}\quad l \in \{2, \ldots, L-1\} \end{cases} $$

Im zweiten Schritt wird für jedes Gewicht $w$ ein Änderungswert $\Delta w$ berechnet (wo die Fehlerwerte $\delta$ mit eingehen).

$$ \Delta w_{i,j}^{(l)} = - \frac{\partial J}{\partial w_{i,j}^{(l)}} = - a_j^{(l)} \: \delta_i^{(l+1)} $$

Hier sehen wir ein RNN mit den ersten zwei Zeitschritten, also "aufgewickelt". Im ersten Schritt wurden Fehlerwerte $\delta$ berechnet. In der Abbildung handelt es sich um Vektoren. Im Gegensatz zur herkömmlichen BP hängt das $\delta_{[1]}^{(2)}$ auch vom Delta im nächsten Zeitschritt ab.

Die (herkömmliche) Formel zur Berechnung eines $\delta$ (nicht an der Ausgabeschicht)

$$ \delta^{(l)}_{[t]} = (W^{(l)}_{[t]})^T \; \delta^{l+1}_{[t]} \odot a^{(l)}_{[t]} \odot (1 - a^{(l)}_{[t]}) $$

wird zu folgender Formel:

$$ \delta^{(l)}_{[t]} = ([W^{(l)}_{[t]};U^{(l)}_{[t+1]}])^T \; \left[ \begin{array}{c} \delta^{l+1}_{[t]}\\ \delta^{l}_{[t+1]}\end{array}\right] \odot \left[ \begin{array}{c} a^{(l)}_{[t]} \\ a^{(l)}_{[t]}\end{array}\right] \odot \left[ \begin{array}{c} (1 - a^{(l)}_{[t]}) \\ (1 - a^{(l)}_{[t]}) \end{array}\right] $$

An dieser Formel ist wichtig, dass $\delta^{(l)}_{[t]}$ von dem "zukünftigen" $\delta^{(l)}_{[t+1]}$ abhängt.

Wenn Sie sich die weiteren Zeitschritte vorstellen, verstehen Sie, dass hier Fehlerwerte "durch die Zeit" transportiert werden.

Im zweiten Schritt werden die $\Delta w$ mit Hilfe der $\delta$-Werte berechnet. Beim normalen BP:

$$ \begin{align} \Delta w_{i,j}^{(2)} &= - \frac{\partial J}{\partial w_{i,j}^{(2)}} = - a_j^{(2)} \: \delta_i^{(3)} \\[2mm] \Delta w_{i,j}^{(1)} &= - \frac{\partial J}{\partial w_{i,j}^{(1)}} = - a_j^{(1)} \: \delta_i^{(2)} \end{align} $$

In Vektorschreibweise (und mit Zeitschritt):

$$ \begin{align} \Delta W^{(2)}_{[t]} &= - \delta^{(3)}_{[t]} \: (a^{(2)}_{[t]})^T\\[4mm] \Delta W^{(1)}_{[t]} &= - \delta^{(2)}_{[t]} \: (a^{(1)}_{[t]})^T = - \delta^{(2)}_{[t]} \: (x_{[t]})^T \tag{*} \end{align} $$

Im Unterschied zu BP müssen wie bei BPTT Formel (*) ändern: die $\delta^{(2)}$-Werte gehen in die Änderung von sowohl $W^{(1)}$ als auch $U$ ein. Wir ersetzen also Formel (*) durch (**).

$$ [\Delta W^{(1)}_{[t]}; \Delta U_{[t]}] = - \delta^{(2)}_{[t]} \: \left[ \begin{array}{c} x_{[t]} \\[2mm] a^{(2)}_{t-1} \end{array} \right]^T \tag{**} $$

Verarbeitung über eine Sequenz

Jetzt müssen wir uns noch einmal vergegenwärtigen, dass wir im Training immer eine Sequenz (z.B. von Wörtern) eingeben, also $x_{[1]}, x_{[2]}, x_{[3]}, \ldots, x_{[N_x]}$. Bei der (Vorwärts-)Verarbeitung einer solchen Sequenz bleiben die Gewichte in unseren Matrizen $W^l$ und $U$ natürlich fix. Bei Backpropagation (BPTT) berechnen wir aber Anpassungswerte - also $\Delta W^l_{[t]}$ und $\Delta U_{[t]}$ - für jeden Zeitschritt. Das Update summiert erst alle Deltas und mittelt über alle Zeitschritte, bevor die Gewichte angepasst werden.

Vanishing Gradient

Das wichtigste Konzept hinter Backpropagation (und auch hinter BPTT) ist der Gradient, denn dieser ist für die Gewichtsänderung verantwortlich:

$$ \Delta w_{i,j}^{(l)} = - \frac{\partial J}{\partial w_{i,j}^{(l)}} = - a_j^{(l)} \: \delta_i^{(l+1)} $$

Der Gradient wiederum nutzt die Deltawerte und diese hängen teils von einer langen Kette von vorher berechneten Deltawerten ab. Bei vielen Multiplikationen hat man immer das Problem, dass Werte, die nur leicht von $1$ abweichen, also etwas kleiner oder größer sind, das Gesamtprodukt sehr schnell gegen Null geht oder sehr groß wird.

Oft ist es so, dass die Werte zu klein werden, so dass sie keinerlei Einfluss mehr auf die Gewichte haben. Man spricht hier vom Vanishing Gradient Problem, das wir schon beim ResNet angesprochen haben (analog spricht man vom Exploding Gradient Problem bei Werten $>1$). Es verwundert auch nicht, dass dieses Problem sowohl bei vielen Schichten und eben auch bei langen Sequenzen auftaucht, denn die beiden Situationen sind - wie wir oben gesehen haben - äquivalent.

Ein schöner Artikel dazu ist Recurrent Neural Networks Tutorial, Part 3 – Backpropagation Through Time and Vanishing Gradients von Denny Britz (2015).

3.5 Bidirektionale RNN (BRNN)

Das vorgestellte Elman-Netz erlaubt es, dass zum Zeitpunkt $t$ Informationen von vorherigen Eingaben $1, \ldots, t-1$ mit berücksichtigt werden können. In sprachlichen Äußerungen kommt aber auch oft vor, dass der zukünftige Kontext $t+1, \ldots, T_x$ eine Rolle spielt.

Will man hier das eingeklammerte Wort vorhersagen, spielt jeweils das fett gesetzte Wort eine Rolle, das in der Zukunft liegt:

Er holte sein [Auto] und fuhr nach München.

Er holte sein [Tagebuch] und schrieb los.

Ein Bidirektionales RNN (BRNN) leitet die Aktivierung der versteckten Schicht $a_{[t]}$ nicht nur vorwärts weiter, sondern auch rückwärts. Das setzt natürlich voraus, dass der komplette Input (z.b. der vollständige Satz oder ein komplettes Dokument) vollständig durchlaufen werden kann. Das BRNN vollzieht bei der Forward Propagation zwei Durchläufe.

Wir sehen uns das anhand eines einfachen Beispiels mit drei Zeitschritten an (z.B. drei Wörter). Wir führen mit der Eingabe $x_{[1]},x_{[2]},x_{[3]}$ zunächst die normale Forward Propagation durch und speisen jeweils $a_{[t-1]}$ mit in die versteckte Schicht ein.

Jetzt rechnen wir rückwärts. Wir berechnen eine zweite Aktivierung $\tilde{a}_{[t]}$ basierend auf der Eingabe $x_{[t]}$ und dem zukünftigen Kontext $\tilde{a}_{[t+1]}$, wobei der initiale Kontext $\tilde{a}_{[3]} := a_{[3]}$. Man beachte, dass wir trotz der Rückwärtsverarbeitung immer noch von Forward Propagation sprechen, da wir hier kein Lernen durchführen, sondern Aktivierungen berechnen. Am Ende dieses Durchlaufs haben wir in der Zwischenschicht für jeden Zeitschritt zwei Aktivierungsvektoren $a_{[t]}$ und $\tilde{a}_{[t]}$ (für die Berechnung von $\tilde{a}_{[t]}$ benötigt man auch ein separates $\tilde{z}_{[t]}$, das hier nicht dargestellt ist):

Die beiden Aktivierungen enthalten jeweils den Kontext aus der Vergangenheit ($a$) und der Zukunft ($\tilde{a}$). Für die Berechnung des Outputs werden beide Aktivierungen benutzt, indem die zwei Vektoren aneinander gehängt werden. Wir haben hier auch eine entsprechend dimensionierte Gewichtsmatrix. Das dargestellte Szenario ist many-to-many. Analog würde es für einen einzelnen Output (many-to-one) funktionieren (man lasse einfach $\hat{y}_{[1]}, \hat{y}_{[2]}$ weg).

Beim Lernen können wir Backpropagation Through Time (BPTT) entsprechend erweitern. Einmal wird BPTT wie bereits beschrieben angewendet (s.o.). Anschließend wird BPTT für die Rückrichtung angewendet. Sie können sich dazu vorstellen, dass man $a$ und $\tilde{a}$ getrennt behandelt. Die Gewichtsmatrizen kann man ebenfalls in zwei Teile $W^{(l)}$ und $U^{(l)}$ trennen und bei BPTT entsprechend anpassen.

4 RNN mit Gated Units

Ein RNN realisiert mit den rekurrenten Verbindungen eine Art Gedächtnis, da die Aktivierung in Zeitschritt $t$ in die Verarbeitung im nächsten Zeitschritt $t+1$ mit eingeht, wie man hier nochmal sieht:

Man kann sich - auch ohne Kenntnis des Vanishing Gradient Problems - vorstellen, dass Abhängigkeiten über mehrere Zeitschritte schwierig zu lernen sind, weil Wichtiges verblasst und Unwichtiges mitgeschleppt wird.

Die Grundidee von "Gated Units" wie GRU oder LSTM ist es, diesen Informationskanal der rekurrenten Verbindungen (in der Abb. rot) zu verwalten bzw. zu steuern: Irrelevante Informationen sollten gelöscht/unterdrückt werden können und relevante Information sollten effizient enkodiert werden können.

4.1 GRU: Gated Recurrent Units

Das Konzept der Gated Recurrent Units (GRU) stammt aus dem Jahr 2014 im Zuge des Gated Recursive CNN aus der Arbeitsgruppe von Yoshua Bengio, Universität Montréal (Chung et al. 2014, Cho et al. 2014). Es ist somit deutlich jünger als das LSTM-Konzept, das bereits 1997 vorgestellt wurde (Hochreiter, Schmidhuber, 1997). Da GRUs etwas einfacher aufgebaut sind, schauen wir sie uns als erstes an (wer in das Originalpaper reinschauen möchte: ich empfehle Chung et al. 2014).

Wir stellen uns zunächst ein NN mit nur einer versteckten Schicht vor. Die Aktivierung der versteckten Schicht $a_{[t]}$ zum Zeitpunkt $t$ kann man als Kontext für das Netz zum Zeitpunkt $t+1$ verstehen. Die Grundidee ist jetzt, dieses Signal $a_{[t]}$ mit Hilfe von sogenannten Gates zu steuern: Man filtert Irrelevantes heraus, man verstärkt wichtige Aspekte. Dazu wird ein Kandidat $\tilde{a}$ berechnet, der einen neuen möglichen Kontext repräsentiert, und anschließend wird gesteuert, inwieweit dieser Kandidat verwendet wird oder ob der alte Kontext weiterverwendet wird.

Beim GRU gibt es zwei Gates:

Diese beiden Vektoren werden mit eigenen Gewichten angelernt.

Wir definieren zunächst die zwei Gates:

$$ \begin{align} r &:= \sigma\, (W_r \left[\begin{array}{c} a_{[t-1]}\\ x_{[t]}\end{array}\right] + b_r) \tag{Reset}\\[2mm] u &:= \sigma\, (W_u \left[\begin{array}{c} a_{[t-1]}\\ x_{[t]}\end{array}\right] + b_u) \tag{Update} \end{align} $$

Hier bezieht sich $\sigma$ (griechischer Kleinbuchstabe Sigma) auf die alt bekannte logistische Funktion (Sigmoid-Funktion), die den Wertebereich $[0, 1]$ hat.

$$ \sigma(z) = \frac{1}{1+e^{-z}} $$

Der resultierenden Vektoren können also Komponenten auslöschen (mit 0) oder erhalten (mit 1) oder mit Zwischenwerten abschwächen.

Schematisch kann man den Zusammenhang so darstellen:

Jetzt können wir den Kandidaten $\tilde{a}$ für einen neuen Kontext berechnen:

$$ \tilde{a} := \tanh (W_a \left[\begin{array}{c} r \odot a_{[t-1]}\\ x_{[t]}\end{array}\right] + b_a) \tag{Kandidat} $$

Um $\tilde{a}$ zu berechnen wird erst der alte Zustand $a_{[t-1]}$ mit Hilfe des Reset-Gates $r$ gefiltert (mit dem Hadamard-Operator $\odot$). Der Name deutet darauf hin, dass einzelne Komponenten auf Null gesetzt (Reset) oder zumindest abgeschwächt werden können. Dann wird der gefilterte Zustand mit der Eingabe $x_{[t]}$ und den (gelernten) Gewichten verarbeitet.

Die Idee hinter dem möglichen neuen Kontext $\tilde{a}$ ist, wichtige Informationen zu akzentuieren, indem irrelevante Informationen gelöscht werden. Man könnte sich z.B. einen Genus (männlich/weiblich) oder eine Numerusinformation (singular/plural) vorstellen, die am Satzende beim Verb wichtig ist. Aktivierungsfunktion ist hier der $\tanh$, der in den Bereich $[-1, 1]$ abbildet.

Im letzten Schritt wird aber noch über das Update-Gate $u$ entschieden, ob dieser neue Zustand $\tilde{a}$ verwendet wird bzw. in welchem Umfang oder ob der alte Kontext $a_{[t-1]}$ weiterverwendet wird.

$$ a_{[t]} := u \odot \tilde{a} + (1 - u) \odot a_{[t-1]} $$

Da Update-Gate $u$ ein Vektor mit kontinuierlichen Werten $\in [0, 1]$ ist, werden die beiden Vektoren komplementär gemischt (z.B. 30% zu 70%), man nennt das eine Linearkombination. Wichtig ist hier, dass der alte Vektor $a_{[t-1]}$ weitergereicht werden könnte, sollte er wichtige Informationen enthalten. Anderfalls wird der neu erstellte Kontextvektor $\tilde{a}$ verwendet (wobei auch dieser auf dem alten Kontext beruht, aber dieser vorher gefiltert wurde). Das Update-Gate gibt dem Netz aber die Möglichkeit den neuen Kandidaten komplett zu ignorieren.

Die Ausgabe $a_{[t]}$ ist dann der neue Kontext und die Eingabe für den nächsten Zeitschritt. Zusätzlich kann es sein, dass noch weitere Schichten vorhanden sind, dann ist das auch der Input für die nächste Schicht. Es kann auch sein, dass dies bereits die Ausgabeschicht ist, dann ist $\hat{y}_{[t]} = a_{[t]}$, eventuell wird vorher noch die Softmax-Funktion angewandt.

Darstellungen von GRU-Netzen finden Sie auch in den Artikeln Illustrated Guide to LSTM’s and GRU’s: A step by step explanation von Michael Phi und Understanding GRU Networks von Simeon Kostadinov.

4.2 LSTM: Long Short-Term Memory

Das LSTM-Netz wurde bereits 1997 von Sepp Hochreiter und Jürgen Schmidhuber an der TU München entwickelt (Hochreiter & Schmidhuber, 1997, Schmidhuber 2015). Man kann die oben vorgestellten GRU-Einheiten auch als vereinfachte Version von LSTM-Einheiten betrachten.

Die zwei Hauptunterschiede sind:

In einem LSTM haben wir die drei Gates: Update, Forget und Output:

$$ \begin{align} u &:= \sigma\, (W_u \left[\begin{array}{c} a_{[t-1]}\\ x_{[t]}\end{array}\right] + b_u) \tag{Update}\\[2mm] f &:= \sigma\, (W_f \left[\begin{array}{c} a_{[t-1]}\\ x_{[t]}\end{array}\right] + b_f) \tag{Forget}\\[2mm] o &:= \sigma\, (W_o \left[\begin{array}{c} a_{[t-1]}\\ x_{[t]}\end{array}\right] + b_o) \tag{Output} \end{align} $$

Wir haben auch im LSTM eine Kandidatenzelle für einen neuen Kontext, die wir $\tilde{c}$ nennen. Hier wird der $a$-Wert nicht (wie beim GRU) gefiltert.

$$ \tilde{c} := \tanh (W_c \left[\begin{array}{c} a_{[t-1]}\\ x_{[t]}\end{array}\right] + b_c) \tag{Kandidat} $$

Beim LSTM hat jede Schicht einen Kontextvektor $c_{[t]}$, der im nächsten Zeitschritt wieder eingespeist wird. Im LSTM wird zunächst ein Kandidat $\tilde{c}$ berechnet, wie oben gezeigt. Ob das neue $\tilde{c}$ weitergeleitet wird oder das alte $c_{[t-1]}$, bestimmen das Update-Gate und das Forget-Gate:

$$ c_{[t]} := u \odot \tilde{c} + f \odot c_{[t-1]} $$

Man beachte den Unterschied zum GRU: statt einer Linearkombination ($u$ und $1-u$) werden hier zwei Vektoren, $u$ und $f$ verwendet. Das heißt, zwei separate Werte bestimmen die Anteile von altem und neuem Kandidaten. Das $f$ bestimmt, wie viel vom alten Wert vergessen werden soll.

Die Aktivierung der Schicht - also $a_{[t]}$ - bestimmt das Output-Gate $o$, indem es den Kontext "filtert":

$$ a_{[t]} = o \odot c_{[t]} $$

Aus der Aktivierung ergibt sich auch wie gewohnt die Ausgabe $\hat{y}_{[t]}$.

Hier sehen wir jetzt die vollständige LSTM-Schicht:

Der wichtige Unterschied ist ja offensichtlich die Trennung von $c_{[t]}$ und $a_{[t]}$. In der Abbildung sieht man, dass das $a_{[t]}$ im Grunde eine "gefilterte" Version von $c_{[t]}$ ist. Man könnte das so verstehen, dass man mit $c_{[t]}$ einen "globaleren" Kontext hat, den man über lange Zeit behält, wohingegen Informationen, die direkt für die aktuelle Ausgabe $\hat{y}_{[t]}$ relevant sind, mit dem Output-Gate herausgefiltert werden können, ohne den globalen Kontext zu "stören".

Schauen Sie sich gern auch den viel zitierten Blogartikel Understanding LSTM Networks von Christopher Olah oder den Illustrated Guide to LSTM’s and GRU’s von Michael Phi an.

5 Keras: Sentiment Analysis mit RNN

Interessant ist vielleicht, dass der Erfinder von Keras, François Chollet, das Projekt auch deshalb ins Leben rief, weil es seinerzeit keine guten Softwareframeworks für RNNs gab. Insofern sind RNNs wie LSTM und GRU von Keras besonders gut abgedeckt.

Um RNNs auszuprobieren, sehen wir uns den Datensatz IMDB (Internet Movie Database) an. Dort liegen textuelle Rezensionen von Filmen vor, die in einer binären Klassifikationsaufgabe in positiv und negativ kategorisiert werden sollen. Die Daten enthalten natürlich die Labels. Da es sich bei positiv/negativ hier um eine subjektiv Einschätzung handelt, fällt dieser Typ Klassifikationsaufgabe unter den Begriff Sentiment Analysis. Weitere Szenarien, die unter diesen Begriff fallen, sind z.B. die Einschätzung von emotionalen oder politischen Äußerungen. Dies ist insbesondere im Kontext von sozialen Medien, z.B. auch beim Thema Hate Speech oder politscher Manipulation relevant.

Für diese Aufgabenstellung benötigen wir eine Many-to-One-Konstruktion, da der Satz (Sequenz) komplett eingespeist wird und ein einziger Output (positiv/negativ) erwartet wird.

Wir werden hier die Netz-Typen Elman-Netz, GRU und LSTM betrachten. Außerdem sehen wir uns an, wie wir Bidirektionalität einbauen können.

Lesenswert sind der Keras-Guide Working with RNNs und die Keras-APIs der rekurrenten Schichten.

Parameter

Zunächst einige Parameter für alle Netze.

Hilfsfunktion

Eine Hilfsfunktion zur Visualisierung der Trainingshistorie:

5.1 Daten: IMDB-Datensatz

Wir lesen die Daten ein, die mit Keras ausgeliefert werden.

Bei der Funktion load_data können wir mit num_words das Vokabular beschränken, hier beschränken uns auf 10000 (häufigsten) Wörter.

Die Struktur der Daten ist eigentümlich. Es handelt sich um einen NumPy-Array von Listen.

Schauen wir uns die ersten zwei Elemente der Trainingsdaten an:

Wir sehen zwei Python-Listen unterschiedlicher Länge. Jede Liste entspricht einem Text, jede Zahl entspricht einem Wort. Wir schauen uns an, wie lang die ersten 5 Listen sind.

Offensichtlich sind alle Texte unterschiedlich lang.

Jetzt sehen wir uns die Label an:

Die Funktion pad_sequences schneiden die Reviews auf eine fixe Länge von 500 Wörtern. Ist ein Text kürzer, wird er mit Nullen aufgefüllt (deshalb "pad").

Wir sehen uns an, wie das funktioniert hat.

Und führen noch einen Sanity Check durch: Wie lang sind die ersten 5 Listen?

Die x-Daten (= Rezensionen) liegen jetzt als Vektoren der Länge 500 vor. Jeder Vektor enthält die Indexzahlen der Wörter in entsprechenden Reihenfolge des Textes.

Die Netze werden eine entsprechende Eingabeschicht haben, wo ein Embedding durch Lernen erzeugt wird.

5.2 Elman-Netz (SimpleRNN)

Wir beginnen mit einem einfachen RNN, dem Elman-Netz, das in Keras SimpleRNN heißt. Die erste Schicht ist vom Typ Embedding. Diese Schicht lernt eine Embedding-Matrix, die die Eingabe eines One-Hot-Vektors der Länge 10000 auf einen embedded Vektor der Länge 32 abbildet. Die 32 wird als Parameter übergeben und spiegelt den Grad der "Kompression" eines Worts wider.

Unsere Zwischenschicht enthält 32 Neuronen. Also Ausgabeaktivierung wählen wir die logistische Funktion.

Siehe auch: https://keras.io/api/layers/recurrent_layers/simple_rnn

Training

Trainingsverlauf

Ergebnisse

Das einfache RNN erzielt ca. 84% Accuracy auf den Testdaten und zwar nach der 4. Epoche.

5.3 Bidirektionales RNN

In Keras wird Bidirektionalität durch einen "Wrapper" erreicht. So kann man beliebige rekurrente Schichten in bidirektionale Schichten ummünzen (auch z.B. LSTM oder GRU).

Siehe auch:

Training

Trainingsverlauf

Ergebnisse

Das BRNN-Netz erzielt als ca. 86.0% Accuracy auf den Testdaten und in der 10. Epoche.

5.4 LSTM

Wir tauschen jetzt einfach die Schicht SimpleRNN durch eine Schicht LSTM aus.

Siehe auch: https://keras.io/api/layers/recurrent_layers/lstm

Training

Trainingsverlauf

Ergebnisse

Das LSTM-Netz erzielt als ca. 88.2% Accuracy auf den Testdaten und zwar im Grunde schon nach der 1. Epoche.

5.5 GRU

Zu guter letzt noch ein Netz mit GRU-Schicht.

Siehe auch: https://keras.io/api/layers/recurrent_layers/gru

Training

Trainingsverlauf

Ergebnisse

Das GRU-Netz erzielt als ca. 88.6% Accuracy auf den Testdaten und zwar im Grunde schon nach der 2. Epoche.

5.6 Bidirektionales GRU

Da unser GRU so gut abgeschnitten hat, probieren wir die bidirektionale Variante des GRU.

Training

Trainingsverlauf

Ergebnisse

Das Bidirektionale GRU-Netz erzielt als ca. 89.5% Accuracy auf den Testdaten in der 8. Epoche.

5.7 Performanzvergleich

Unsere Ergebnisse sind wie folgt. Für wirklich seriöse Resultate müsste man viele Durchläufe durchführen und die Werte dann mitteln. Wir zeigen hier die höchste erzielte Accuracy auf den Testdaten und die jeweilige Epoche, in der das Ergebnis auftrat. Beachten Sie also, dass kleinere Unterschiede mit hoher Wahrscheinlichkeit auf statistische Schwankungen zurückzuführen sind. Man "sieht" hier also höchstens zwei Klassen von Ergebnissen, einmal um 85/86% und einmal um 88/89%.

Netz Acc Testdaten Epoche
SimpleRNN 85.3 4
Bidirektionales RNN 86.0 10
LSTM 88.4 2
GRU 89.4 7
Bidirektionales GRU 89.5 8

Die Ergebnisse spiegeln die Aussagen aus der Literatur wider: LSTM und GRU werden oft als gleichwertig betrachtet und nur bei ganz spezifischen Aufgaben scheinen Vor- und Nachteile sichtbar zu werden. LSTM/GRU sind aber in der Regel dem einfachen RNN klar überlegen. Bidirektionalität scheint weder beim einfachen RNN noch beim GRU einen echten Vorteil zu bringen. Auch das scheint plausibel, da man für einen kompletten Text eine Klasse sucht und daher ohnehin alle Wörter berücksichtigt werden. Bidirektionalität ist vielleicht eher für Many-to-many-Szenarien geeignet.

In Understanding LSTM Networks sagt Christopher Olah, dass die Unterschiede zwischen den RNN-Varianten wie LSTM und GRU eher gering seien:

Which of these variants is best? Do the differences matter? Greff, et al. (2015) do a nice comparison of popular variants, finding that they’re all about the same.

Unten im Literaturverzeichnis finden Sie die Referenz als Greff et al. (2017). Es ist das gleiche Paper wie oben, nur dass es schon 2015 online erschien, aber erst 2017 in der Fachzeitschrift. Es sei auch nochmal auf Schmidhuber (2015) verwiesen, wo auf die Performanz verschiedener RNN eingegangen wird.

6 Weiterführende Themen

6.1 Vortrainierte Word Embeddings (Transfer Learning)

Wir haben das Thema Transfer Learning in Kapitel 7 erläutert. In der Sprachverarbeitung liegt es nahe, word embeddings auf großen vorhandenen Datensätzen zu lernen und dann wiederzuverwenden, anstatt sie für jedes Problem neu zu lernen. Umgekehrt kann es natürlich für bestimmten Probleme auch eine gute Strategie sein, die word embeddings ganz neu zu lernen.

Ein oft verwendetes Modell nennt sich GloVe (für Global Vectors), entwickelt von Pennington, Socher und Manning (2014) von der Stanford-Universität. Sie finden Erläuterungen und vortrainierte Modelle auf der GloVe-Projektseite.

Modelle, die durch die bereits erwähnte Word2vec-Methode (Mikolov et al. 2013) gewonnen wurden, stellt Google auf der Projektseite bereit.

Von Facebook gibt es Modell unter dem Titel fastText (siehe Joulin et al. 2017). Auch hier finden Sie Erklärungen und Modelle auf der fastText-Projektseite.

6.2 Attention/Transformer

Ein jüngerer Durchbruch auf dem Gebiet der Sprachverarbeitung - insbesondere der maschinellen Übersetzung - mit Neuronalen Netzen gelang mit Hilfe eines Mechanismus, den man Attention nennt (siehe Paper "Attention Is All You Need" von Google-Forschern Vaswani et al. 2017). Wohingegen LSTM/GRU-Netze die komplette Historie berücksichtigt, wird mit Hilfe von Attention auf bestimmte Teile eines Satzes fokussiert. So lässt sich z.B. die Problematik unterschiedlicher Wortstellungen in verschiedenen Sprachen leichter abbilden, z.B. wenn man das deutsche

schwarze Katze

ins Französische übersetzt, wo das Adjektiv hinter das Nomen gestellt ist:

chat noir

Typisch ist auch die Lücke zwischen Hilfsverb und Verb im Deutschen:

Ich bin drei Stunden gelaufen

Im Gegensatz dazu im Englischen:

I was walking for three hours

Betrachten wir das zweite Beispiel. Hier würden wir den englischen Satz generieren wollen. Das Problem ist, dass das dritte Wort im Inputsatz "drei" wenig mit dem dritten Wort im Zielsatz ("walking") zu tun hat. Attention steuert den Grad der Relevanz zwischen Inputsatz und Outputsatz. Im Beispiel wäre beim dritten Wort wünschenswert, dass das Wort "gelaufen" eine hohe Attention bekommt.

Neuronale Netze, die Attention verwenden, nennt man auch Transformer. Der wichtigste Vorteil von Transformern ist, dass Netze auf großen Textdatensätzen trainieren und speichern kann, um diese vortrainierten Netze für speziellen Probleme wiederzuverwenden. Auch dies ist eine Form von Transfer Learning (s.o.).

Ein sehr schöner Vortrag von Leo Dirac (2019) erläutert sowohl LSTM als auch Attention und Transformer und warum er der Meinung ist, dass LSTM nicht mehr gebraucht werden: LSTM is dead. Long Live Transformers!

7 Literatur

Bengio, Yoshua; Ducharme, Réjean; Vincent, Pascal & Janvin, Christian (2003) A Neural Probabilistic Language Model. In: Journal of Machine Learning Research, 3, pp. 1137–1155.

Cho, Kyunghyun; van Merriënboer, Bart; Bahdanau, Dzmitry; Bengio, Yoshua (2014) On the Properties of Neural Machine Translation: Encoder–Decoder Approaches. In: Proceedings of SSST-8, Eighth Workshop on Syntax, Semantics and Structure in Statistical Translation, pp. 103–111.

Chung, Junyoung; Gulcehre, Caglar; Cho, Kyunghyun; Bengio, Yoshua (2014) Empirical evaluation of gated recurrent neural networks on sequence modeling. In: NIPS 2014 Workshop on Deep Learning.

Elman, Jeffrey L. (1990) Finding Structure in Time. In: Cognitive Science 14, pp. 179–211.

Greff, Klaus; Srivastava, Rupesh K.; Koutník, Jan; Steunebrink, Bas R.; Schmidhuber, Jürgen (2017) LSTM: A Search Space Odyssey. In IEEE Transactions on Neural Networks and Learning Systems, 28: 10, pp. 2222 - 2232.

Hochreiter, Sepp (1991) Untersuchungen zu dynamischen neuronalen Netzen, Diplomarbeit, Institut für Informatik, Technische Universität München.

Hochreiter, Sepp; Schmidhuber, Jürgen (1997) Long Short-Term Memory. In: Neural Computation 9: 8, pp. 1735–1780.

Joulin, Armand; Grave, Edouard; Bojanowski, Piotr; Mikolov, Tomas (2017) Bag of Tricks for Efficient Text Classification. In: Proceedings of the 15th Conference of the European Chapter of the Association for Computational Linguistics. arXiv

Jordan, Michael I. (1990) Attractor dynamics and parallelism in a connectionist sequential machine. In: Artificial neural networks: concept learning, IEEE Press, pp. 112–127.

Mikolov, Tomas; Sutskever, Ilya; Chen, Kai; Corrado, Greg S.; Dean, Jeff (2013) Distributed representations of words and phrases and their compositionality. In: Proceedings of the 26th International Conference on Neural Information Processing Systems (NIPS’13), pp. 3111–3119.

Pennington, Jeffrey; Socher, Richard; Manning, Christopher D. (2014) GloVe: Global Vectors for Word Representation. In: Proceedings of the 2014 Conference on Empirical Methods in Natural Language Processing (EMNLP).

Schmidhuber, Jürgen (2015) Deep learning in neural networks: An overview. In: Neural Networks, Vol. 61, pp. 85-117.

Vaswani, Ashish; Shazeer, Noam; Parmar, Niki; Uszkoreit, Jakob; Jones, Llion; Gomez, Aidan N.; Kaiser, Lukasz; Polosukhin, Illia (2017) Attention is All You Need. In: NIPS.

Werbos, Paul (1974) Beyond Regression: New Tools for Prediction and Analysis in the Behavioral Sciences, PhD Thesis, Applied Mathematics, Harvard University, Boston, MA.

Werbos, Paul (1990) Backpropagation Through Time: What It Does and How to Do It. In: Proceedings of the IEEE, 78: 10, pp. 1550–1560.

Williams, Ronald J.; Zipser, David (1989a) Experimental analysis of the real-time recurrent learning algorithm. In: Connection Science, 1(1), pp. 87-111.

Williams, Ronald J.; Zipser, David (1989b) A learning algorithm for continually running fully recurrent neural networks. In: Neural Computation, 1(2), pp. 270-280.