12  Sprachverarbeitung

Wir begeben uns in die Welt der Sprachverarbeitung (Natural Language Processing, NLP).Im Vergleich zu unseren bisherigen Daten (z.B. Hauspreise oder Pixel von Bildern) handelt es sich bei Sprachdaten um sequentielle Daten. Wir arbeiten mit Wörtern und Sätzen unterschiedlicher Länge. Wir lernen zunächst, wie man Wörter und Sätze repräsentiert und welche Vorverarbeitungsschritte man unternehmen muss.

Konzepte in diesem Kapitel

Vokabular, Token, Tokenizer, Wortindex, One-Hot-Encoding, Bag of Words, Word Embeddings, Sprachmodell, Unigramm, Bigramm, Neuronales Sprachmodell

Datensatz

Name Daten Klassen Trainings-/Testdaten Ort
IMDB Text (Filmreviews) negativ (0), positiv (1) 25000/25000 12.3.4

12.1 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. Solche Daten variabler Länge nennt man sequentielle Daten. Sie unterscheiden sich fundamental von Daten mit fixer Größe, z.B. Bildern mit der Auflösung 28x28.

Das führt zunächst zu der Frage, wie man ein einzelnes Wort wie “cat” oder “the” repräsentiert. Wir nehmen dabei an, dass es eine feste Menge von möglichen Wörtern gibt, also ein Vokabular \(V\). Mit \(|V|\) bezeichnen wir die Anzahl der Wörter in \(V\).

12.1.1 Wörter, Vokabular, Wortindex und Token

Wenn wir ein Vokabular \(V\) haben, dann ist 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. In unserem Beispiel sind es Wörter, aber in der Realität werden oft Wort-Teile genommen, so dass z.B. “Haustür” in die Tokens “Haus” und “Tür” zerlegt wird oder die Pluralform “Türen” in die Tokens “Tür” und “-en”.

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.

12.1.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 \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 \\ 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).

12.2 Sprachmodelle

Bevor wir uns mit Word Embeddings beschäftigen, müssen wir einen Exkurs unternehmen und lernen, was Sprachmodelle (engl. language models) sind. Sprachmodelle sind seit GPT-3 und ChatGPT - welche auch manchmal als large language models (LLM) bezeichnet werden - auch in nicht-technischen Bereichen ein Begriff.

12.2.1 Was sind Sprachmodelle?

Ein Sprachmodell ist ein Modell, das Vorhersagen treffen kann. Die Eingabe besteht aus einer Reihe von Wörtern. Ein Beispiel wäre:

Ich lese ein

Die Ausgabe ist das wahrscheinlichste nächste Wort. Bei unseren Beispiel könnte dies das folgende Wort sein:

Buch

Es könnte auch eine Reihe von möglichen Wörtern mit jeweils einer Wahrscheinlichkeit sein:

Buch, 0.82
Rezept, 0.10
Dokument, 0.05
Pamphlet, 0.03

Sprachmodelle sind sprach-spezifisch, d.h. für das Deutsche benötigt man offensichtlich ein anderes Modell als fürs Englische.

Der ursprüngliche Einsatz von Sprachmodellen war die Spracherkennung. Spracherkennung heißt: Gesprochene Sprachen wird übersetzt in geschriebene Wörter, also Text. Der Input besteht also aus einer Sequenz von Audiodaten (in Form einer Datei oder eines Streams), der Output besteht aus einer Sequenz von Wörtern (= Strings). Spracherkennung ist sehr fehleranfällig, besonders wenn schnell und/oder unartikuliert gesprochen wird.

Vergleichen Sie:

  • Ich kann nicht mehr / Ich kann ich Meer
  • Ich mag mit dem Rad fahren / Ich Mark mitten Rat Farn

Sprachmodelle können hier schnell Klärung schaffen, da viele Wortkombinationen grammatikalisch oder semantisch ausgeschlossen werden können.

12.2.2 Wie baut man ein Sprachmodell?

Die ersten Sprachmodelle waren probabilistischer Natur. Das Problem, das nächste Wort eines Teilsatzes vorherzusagen, wurde zunächst auf folgende Aufgabe reduziert:

Gegeben eine bestimmte Anzahl vorheriger Wörter, welches ist das wahrscheinlichste nächste Wort?

Der triviale Fall ist, dass man keine vorherigen Wörter betrachtet. Dann sagt man im Grunde immer das häufigste Wort voraus. Man würde dies aus einer Tabelle mit den Häufigkeiten aller Wörter ablesen können. Man nennt dieses triviale und offensichtlich wenig sinnvolle Modell auch ein Unigramm-Modell.

Betrachten wir jetzt immer genau ein vorheriges Wort, dann können wir ebenfalls eine Tabelle erstellen. Diesmal betrachten wir im Grunde Wortpaare \((v, w)\), wobei \(v\) das vorherige Wort und \(w\) das nachfolgende Wort ist. So ein Wortpaar nennt man auch ein Bigramm. Jetzt möchten wir zählen, wie oft \(w\) auf \(v\) folgt. Das nennt man die absolute Häufigkeit. Wenn wir diese Zahl durch die Anzahl der Vorkommen von \(w\) teilen, erhalten wir die relative Häufigkeit. Die relative Häufigkeit von \((v, w)\) entspricht der bedingten Wahrscheinlichkeit:

\[ P(v | w) \]

Sehen wir uns ein Beispiel an, wo wir nur die folgenden 8 Beispielsätze haben.

  • Der Hund spielt im Park.
  • Das Kind spielt gern im Park.
  • Der Hund und das Kind spielen.
  • Das Kind und der Hund laufen.
  • Der Hund läuft schnell im Park.
  • Das Kind spielt gern mit dem Hund.
  • Im Park spielen das Kind und der Hund.
  • Der Hund läuft gern.

Wir ersetzen ein paar Wörter durch Tokens:

  • Satzzeichen wie der Punkt werden entfernt
  • Artikel (“der”, “das”, “dem”) werden entfernt
  • Alle Wörter werden klein geschrieben
  • die Verbformen “laufen” und “läuft” werden durch laufen ersetzt, die Verbformen “spielen” und “spielt” durch spielen

Außerdem stellen wir die speziellen Tokens START und END an Anfang und Ende. Die Sätze sehen jetzt so aus:

  • START hund spielen im park ENDE
  • START kind spielen gern im park ENDE
  • START hund und kind spielen ENDE
  • START kind und hund laufen ENDE
  • START hund laufen schnell im park ENDE
  • START kind spielen gern mit hund ENDE
  • START im park spielen kind und hund ENDE
  • START hund laufen gern ENDE

Wir können jetzt eine Matrix erstellen, wo jede Zeile das vorherige Wort repräsentiert und jede Spalte das folgende Wort.

ENDE START gern hund im kind laufen mit park spielen und
ENDE 0 0 0 0 0 0 0 0 0 0 0
START 0 0 0 3 1 3 0 0 0 0 0
gern 1 0 0 0 1 0 0 1 0 0 0
hund 2 0 0 0 0 0 2 0 0 1 1
im 0 0 0 0 0 0 0 0 3 0 0
kind 0 0 0 0 0 0 0 0 0 3 2
laufen 1 0 1 0 0 0 0 0 0 0 0
mit 0 0 0 1 0 0 0 0 0 0 0
park 2 0 0 0 0 0 0 0 0 1 0
spielen 1 0 2 0 1 1 0 0 0 0 0
und 0 0 0 2 0 1 0 0 0 0 0

Die erste Zeile der Matrix zeigt, welche Tokens nach dem Token “ENDE” kommen. Es kommt natürlich nie ein weiteres Token, daher steht dort überall eine Null. Die zweite Zeile zeigt, welches Wort meist am Anfang steht. Da sind “hund” und “kind” die Spitzenreiter, da sie jeweils 3x am Anfang eines Satzes stehen. Man nennt diese Zahlen auch die absoluten Häufigkeiten. Die relative Häufigkeit kann man berechnen, wenn man jede Zahl durch die Zeilensumme teilt. In der Zeile “START” ist die Zeilensumme 7. Also ergeben sich folgende relative Häufigkeiten, die gleichzeitig eine Schätzung der bedingten Wahrscheinlichkeit ist:

\[ \begin{align} P(\mbox{START} | \mbox{hund}) &= \frac{3}{7} = 0.429 \\ P(\mbox{START} | \mbox{im}) &= \frac{1}{7} = 0.143 \\ P(\mbox{START} | \mbox{kind}) &= \frac{3}{7} = 0.429 \end{align} \]

12.2.3 Wer tiefer einsteigen möchte…

…dem sei ein Video von Andrej Karpathy ans Herz gelegt, wo er in Python sehr elegant ein Bigram-Modell für Buchstabenfolgen baut: The spelled-out intro to language modeling: building makemore

12.3 Word Embeddings

Mittlerweile hat es sich zum Standard entwickelt, sogenannte Word Embeddings zu verwenden, um Wörter zu repräsentieren. Das ist sozusagen eine Zwischenlösung zwischen den beiden Möglichkeiten, die wir bislang betrachtet haben (Wortindex bzw. One-Hot-Vektor). Bei einem Vokabular \(V\) mit 20.000 oder 100.000 Wörtern sind One-Hot-Vektoren sehr ineffizient, da diese Vektoren hauptsächlich aus sehr vielen Nullen bestehen. Stattdessen wollen wir pro Wort einen Vektor mit deutlich weniger Einträgen, wo aber alle Elemente (Features) tatsächlich mit Werten belegt sind.

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 Werte 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| = 20000\).

Hier ein Beispiel mit \(m=2\):

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. 100.000 und die Anzahl der Features im Bereich 30 bis 700.

12.3.1 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. (2003).

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.

12.3.2 Tokenizer

Ein Tokenizer ist ein Programm, das Texte in logische Einheiten zerlegt, z.B. in Wörter. Diese Einheiten nennt man dann Tokens. Der Begriff kommt sowohl aus dem Bereich natürlicher Sprachen (NLP) als auch aus dem Bereich Computersprachen/Compilerbau. In unserem Fall sind die Tokens alle Wörter, wobei Groß-/Kleinschreibung ignoriert wird. Satzzeichen sind keine Tokens und werden ignoriert.

Ein Tokenizer wird auf einer Menge von Texten “trainiert” mit fit_on_words. Der Tokenizer baut einen Wortindex auf: jedes Wort (= Token) wird mit einer eindeutigen Zahl assoziiert.

s1 = 'Harry ging nach Berlin, ich nicht.'
s2 = 'Ich ging nach Hause?'

texte = [s1, s2]

Man gibt dem Tokenizer i.d.R. über num_words eine Grenze \(M\) mit, wie viele Tokens aufgenommen werden sollen. Es werden dann nur die \(M\) häufigsten Tokens verwendet (im Wortindex sind dennoch alle Wörter vertreten, aber später in den Sequenzen nicht mehr).

from tensorflow.keras.preprocessing.text import Tokenizer

tok = Tokenizer(num_words=4)
tok.fit_on_texts(texte)

tok.word_index
{'ging': 1,
 'nach': 2,
 'ich': 3,
 'harry': 4,
 'berlin': 5,
 'nicht': 6,
 'hause': 7}

Hier sehen wir, dass “Ich” und “nicht” repräsentiert werden, aber nicht “nicht”, weil dieses Wort nicht unter den 4 häufigsten Wörtern war.

tok.texts_to_sequences(['Ich ging nicht dorthin.'])
[[3, 1]]

Jetzt können wir das als Bag-of-words-Repräsentation ausgeben. Dies ist ein Vektor der Länge 4. Nur bei solchen Indizes, bei denen das entsprechende Wort im Satz vorkommt, steht eine Zahl (z.B. 1), bei allen anderen Null.

Im Modus binary steht bei den vorkommeneden Wörtern eine 1, egal wie häufig sie vorkommen. Hier sehen wir zwei Einträge für “ich” und “ging”.

tok.texts_to_matrix(['Ich ging nicht dorthin, aber er ging.'], mode='binary')
array([[0., 1., 0., 1.]])

Im Modus count steht die Häufigkeit der Vorkommen als absolute Häufigkeit (hier die 2 für “ging”).

tok.texts_to_matrix(['Ich ging nicht dorthin, aber er ging.'], mode='count')
array([[0., 2., 0., 1.]])

Im Modus freq wird die relative Häufigkeit genommen, d.h. die Zahl der Vorkommen von Wort \(w\) wird durch die Gezamtzahl aller vorkommenden Wörter geteilt (hier einmal 2/3 für “ging” und einmal 1/3 für “ich”).

tok.texts_to_matrix(['Ich ging nicht dorthin, aber er ging.'], mode='freq')
array([[0.        , 0.66666667, 0.        , 0.33333333]])

12.3.3 Padding bei Sequenzen

Um eine fixe Sequenzlänge zu erzwingen, gibt es die Keras-Funktion pad_sequences.

Eine Sequenz ist hier eine Reihe von Zahlen. Wir stellen zwei Sequenzen mit Hilfe unseres Tokenizers her.

seq = tok.texts_to_sequences(texte)
seq
[[1, 2, 3], [3, 1, 2]]

Hier kürzen wir die Sequenzen auf Länge 2.

from tensorflow.keras.preprocessing import sequence

sequence.pad_sequences(seq, maxlen=2)
array([[2, 3],
       [1, 2]], dtype=int32)

Hier erweitern wir die Sequenzen, so dass sie Länge 5 haben.

sequence.pad_sequences(seq, maxlen=5)
array([[0, 0, 1, 2, 3],
       [0, 0, 3, 1, 2]], dtype=int32)

Eine gute Darstellung findet man in dem Artikel Data Preparation for Variable Length Input Sequences.

12.3.4 IMDB-Datensatz in Keras

IMDB-Datensatz

Der IMDB-Datensatz enthält 25.000 Filmrezensionen (IMDb steht für Internet Movie Database). Für jede Rezension besteht aus einem Text und hat einen Label, der besagt, ob die Rezension positiv oder negativ war. Man kann den Datensatz also für binäre Klassifikation verwenden (man nennt diese Art Klassifikation aus sentiment classification). Die Wörter sind mit Indexzahlen kodiert. Die Indexzahl ergibt sich aus der Häufigkeit eines Worts. Das Wort mit Index 5 ist also das 5. häufigste Wort in der Datenbank. Siehe auch die Keras-Doku. Wir zeigen in Abschnitt 13.3.1 nochmal eine praktische Anwendung.

Schauen wir uns die Prinzipien von One-Hot-Encoding am Beispiel des IMDB-Datensatzes an.

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.

from tensorflow.keras.datasets import imdb
from tensorflow.keras.preprocessing import sequence

(train_x, train_y), (test_x, test_y) = imdb.load_data(num_words=20)

print(f'{train_x.shape}, {test_x.shape}')
(25000,), (25000,)

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

train_x[0][:20]
[1, 14, 2, 16, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 4, 2, 2, 2, 5, 2]

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

[(w, i) for w, i in imdb.get_word_index().items()][:10]
[('fawn', 34701),
 ('tsukino', 52006),
 ('nunnery', 52007),
 ('sonja', 16816),
 ('vani', 63951),
 ('woods', 1408),
 ('spiders', 16115),
 ('hanging', 2345),
 ('woody', 2289),
 ('trawling', 52008)]

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

import numpy as np
from tensorflow.keras.utils import to_categorical

s = to_categorical(train_x[0], num_classes=20)
print(len(s))
s
218
array([[0., 1., 0., ..., 0., 0., 0.],
       [0., 0., 0., ..., 0., 0., 0.],
       [0., 0., 1., ..., 0., 0., 0.],
       ...,
       [0., 0., 0., ..., 0., 0., 1.],
       [0., 0., 1., ..., 0., 0., 0.],
       [0., 0., 1., ..., 0., 0., 0.]], dtype=float32)

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.

np.sum(s, axis=0)
array([  0.,   1., 149.,   0.,  15.,   9.,   3.,   2.,   3.,   1.,   0.,
         0.,   6.,   3.,   3.,   4.,  11.,   3.,   3.,   2.],
      dtype=float32)

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.