Richtig vorgekaut: So wird Text-Input für Machine Learning aufbereitet

Text-Preprocessing: Wie bereitet man Text für erfolgreiches Machine Learning vor?
Keine Kommentare

Die meisten Verfahren im Bereich des Machine Learnings (ML) arbeiten mit rein numerischem Input. Bei den meisten Arten von Daten, z B. Tabellen von Messwerten oder Bildern, ist die Umwandlung in die richtige Form offensichtlich. Aber wie können wir Machine Learning mit Buchstaben und Wörtern betreiben? Der Erfolg hängt vom richtigen Preprocessing ab.

Erfolgreiches Machine Learning ist meist zu 75 Prozent von richtigem Preprocessing abhängig. Da die richtige Repräsentation der Daten bei Text nicht eindeutig ist, sondern immer von der Zielsetzung und dem verwendeten Verfahren abhängt, hat das Preprocessing bei der Arbeit mit Text einen noch höheren Stellenwert als ohnehin schon. In diesem Artikel wollen wir uns der Frage widmen, wie wir dieses wichtige Preprocessing richtig umsetzen, und welche Textdarstellungen es gibt, um den größtmöglichen Erfolg mit unseren ML-Algorithmen zu erzielen.

Die meisten ML-Algorithmen, insbesondere bei Deep Learning (Schichten von neuronalen Netzen), benötigen Input in Form von Tensoren. Praktisch kann man bei der Entwicklung Tensoren mit mehrdimensionalen Arrays von Fließkommawerten gleichsetzen. Abbildung 12 und 3 zeigen Beispiele für Tensoren.

Abb. 1: Ein 1-D Tensor (Vektor), z. B. eine Zeile in einem Spreadsheet

Abb. 1: Ein 1-D Tensor (Vektor), z. B. eine Zeile in einem Spreadsheet

Abb. 2: Ein 2-D Tensor (Matrix), z. B. ein Schwarz-Weiß-Bild

Abb. 3: Ein 3-D Tensor, z. B. ein RGB-Bild

Abb. 3: Ein 3-D Tensor, z. B. ein RGB-Bild

Bei numerischen tabellarischen Daten oder Bildern liegt die Umwandlung auf der Hand. Schließlich haben die Daten prinzipiell schon die richtige Form. Die meisten ML-Verfahren erwarten aber nicht nur Tensoren, sondern auch, dass sie immer dieselbe Größe haben. Tabellarische Daten sind meist gleichförmig, die Datenpunkte haben dieselbe Anzahl Spalten (= dieselbe Dimension). Bilder lassen sich trivial vergrößern oder verkleinern und so in Matrizen fester Größe umwandeln. Die meisten Textdaten aber variieren in der Länge von Datum zu Datum, ähnlich wie Audio und Video, was eine zusätzliche Hürde in der Nutzung darstellt.

Klassisches NLP vs. Machine Learning

Dieser Artikel legt einen Fokus auf Machine-Learning-Verfahren und damit Verfahren, die generisch auf (fast) beliebigen Inputs arbeiten können, solange sie als Tensoren vorliegen. Es gibt aber auch eine Reihe von nützlichen Algorithmen, die mit rein regelbasierten Verfahren arbeiten. Die sind nicht auf die Darstellung von Text als Tensoren angewiesen und arbeiten oft auf Strings, z. B. Part-of-Speech-(POS-)Tagging, Lemmatisierung, Stemming, Ermittlung von Satzanfang und -ende, etc. Diese sind allerdings oft händisch durch langjährige, mühsame Arbeit ermittelt und verfeinert worden. Der Vorteil dieser Verfahren ist, dass sie erprobt und als fertige Bibliotheken verfügbar sind und so als Ergänzung zu ML-Verfahren oder zum Preprocessing eingesetzt werden können. Der Nachteil ist, dass sie in der Regel für jede neue Sprache mühsam händisch neu angepasst werden müssen. Nichtsdestotrotz ist es nützlich, sich einen Überblick über die Möglichkeiten zu verschaffen, die auch ohne ML zur Verfügung stehen.

Das Hauptproblem von Text ist also, dass die richtige numerische Darstellung nicht auf der Hand liegt. Der Grund dafür ist, dass es auch nach über sechzig Jahren KI-Forschung noch nicht gelungen ist, eine allgemeingültige Repräsentation von Wissen zu finden, mit der Algorithmen arbeiten können. Aber genau das ist ja Text: ein künstliches Mittel, mit dem Menschen Wissen außerhalb von Köpfen aufbewahren können. Das aber macht Text zugleich auch unglaublich reizvoll und nützlich für KI-Verfahren:

  • Es gibt viel davon (Internet, Bücher, firmeninterne Dokumentensammlungen …)
  • Er ist oft leicht verfügbar (Scraper, Datenbanken, einfacher File-Import)
  • Wissen ist enorm kompakt repräsentiert (Der String Katze lässt sich in 5 Byte oder weniger packen, ein Katzenbild hat schnell einige Dutzend oder Hunderte Kilobyte)

Trotz der Hürden lohnt es sich also herauszufinden, wie man Text geschickt in eine passende Form bringen kann, um die Vorteile von ML zu nutzen.

First Things First: die richtige Nutzung von Unicode

Um mit Text arbeiten zu können, muss er zerschnitten werden, die Schnipsel verglichen, Sonderzeichen und Umlaute einheitlich dargestellt und die Länge von Textelementen muss verglichen werden. Das ist natürlich trivial – wir nehmen einfach für alle Texte Unicode, dann geht alles wie von Zauberhand!

Leider ist es in der Praxis nicht so leicht. Das Problem ist, dass die Kodierung eines Texts in Unicode nicht eindeutig ist, auch wenn es bei der Darstellung des Texts auf dem Bildschirm so aussieht. Insbesondere bei der Arbeit mit OCR-Software und wenn der Input aus Dokumenten stark unterschiedlicher Herkunft besteht, ergeben sich häufig Probleme wie die folgenden (und das ist nur eine Auswahl):

  • Umlaute und Akzente können als ein Code-Point (z. B. U+00FC LATIN SMALL LETTER U WITH DIAERESIS) oder als zwei Code-Points (z. B. U+0075 LATIN SMALL LETTER U und U+0308 COMBINING DIARESIS) repräsentiert werden.
  • Auch scheinbar eindeutige Zeichen wie der Buchstabe A können durch verschiedene Code-Points dargestellt werden, es gibt mehr als zehn Unicodekodierungen für das lateinische Alphabet (a–z, A–Z) und die arabischen Ziffern (0–9).
  • Es gibt mehr als zwanzig Arten von Leerzeichen. Zieht man das nicht in Betracht und zerlegt Text einfach anhand von Standardleerzeichen (U+0020), kann es sein, dass Wörter aneinander kleben bleiben.
  • Manchmal können mehrere Buchstaben zusammen durch einen einzelnen Code-Point, eine Ligatur, repräsentiert werden. Das führt dann dazu, dass der String „Affe“ die Länge drei hat (weil das Doppel-F durch ein Zeichen dargestellt wird) und der String „Af f e“ die Länge vier.
  • Je nach Alter des Inputs kann es auch sein, dass Code-Points „deprecated“ sind, das heißt, sie wurden durch neue Code-Points an anderer Stelle ersetzt.

Grund hierfür ist, dass Unicode über Jahre gewachsen ist und Sprache, Text und Druck komplizierte Materien sind. Für die meisten dieser Besonderheiten gibt es gute und logische Begründungen. Nichtsdestotrotz ist derartiger Input Gift für erfolgreiches ML. Zum Glück gibt es hierfür eine Lösung: Normalisierung.

Normalisierung bedeutet, dass Daten, bei denen mehrere äquivalente Darstellungen möglich sind, nach vereinbarten Regeln in eine eindeutige Form gebracht werden, um sie besser vergleichen zu können. Die Polynome x³ + x + 4x² und 4x² +x³ + x beschreiben zum Beispiel dieselbe Funktion. Damit das auf den ersten Blick klar wird, werden Polynome oft nach aufsteigendem Exponenten normalisiert, also ergibt sich für beide Formeln x + 4x² + x³. Nun wird auch optisch oder durch einfaches Vergleichen Zeichen-für-Zeichen klar, dass es sich um dieselbe Formel handelt.

Im Fall von Unicode bedeutet Normalisierung, dass nach umfangreichen und komplizierten Regeln Ketten von Code-Points neu angeordnet und Code-Points teilweise ersetzt werden, damit Strings mit gleichem semantischem Inhalt auch wirklich die gleichen Code-Point-Sequenzen enthalten. Hier gibt es je nach Anwendungsfall unterschiedliche Anforderungen an die Normalisierung. Deshalb sieht der Unicodestandard vier Arten der Normalisierung (vier Normalformen) vor, die jeweils andere Ergebnisse liefern. Bei ML-Preprocessing ist es absolut essenziell, dass alle beteiligten Komponenten die gleiche Normalisierung für Text verwenden. Idealerweise wird diese Normalisierung einmalig zu Beginn des Preprocessings durchgeführt, sodass spätere Komponenten einfach von normalisiertem Text ausgehen können. Für die meisten ML-Anwendungen ist die NFKC-Normalform passend. Vereinfacht ausgedrückt verwandelt NFKC „merkwürdige“ Leerzeichen, ersetzt Ligaturen und Symbole wie „km“ durch die tatsächlichen einzelnen Buchstaben und sorgt für eine einheitliche Darstellung von Akzenten und Umlauten. Alle Standardnormalisierungsmethoden stehen in jeder Programmiersprache mit Unicodesupport bequem als Bibliothek zur Verfügung. Aufgrund der Komplexität des Themas ist aufs Schärfste davon abzuraten, Unicodenormalisierung selbst zu implementieren.

Folgende Checkliste dient als Richtlinie für das initiale Low-Level-Preprocessing von Text. Je nach Anwendungsfall müssen einzelne Punkte weggelassen oder ergänzt werden, aber die folgenden Schritte stellen einen sinnvollen Standard für die meisten Probleme dar:

  1. Eingehende Daten aus anderen Encodings in einheitliches Unicode-Encoding (z. B. UTF-8) konvertieren
  2. NFKC-Normalisierung anwenden
  3. Nicht benötigte Code-Points (Punktierung, Emojis, Pfeile, …) durch Leerzeichen (U+0020) ersetzen
  4. Zifferngruppen entweder durch Leerzeichen oder durch ein spezielles Keyword („DIGITS“, „ZIPCODE“, „TELEPHONENO“) ersetzen; alternativ alle Ziffern durch 0 ersetzen; so reduziert sich später die Anzahl der verschiedenen Tokens erheblich, aber Ziffernfolgen mit semantischer Bedeutung können immer noch erkannt werden
  5. Oft ist es sinnvoll, den gesamten Text in Kleinbuchstaben zu konvertieren
  6. Konsekutive Folgen von Leerzeichen durch ein einziges Leerzeichen ersetzen

Diese Schritte sollten idealerweise auf Code-Points, nicht auf chars durchgeführt werden (Kasten: „char vs. Buchstabe vs. Code-Point vs. Glyphe“).

char vs. Buchstabe vs. Code-Point vs. Glyphe

1 char = 1 Buchstabe. Einfach, oder? Sobald man sich umfassend mit Text-Preprocessing beschäftigt, lohnt es sich, einmal einen Blick auf die Details von Schriftsystemen zu werfen.

Ein Zeichen auf dem Bildschirm nennt man eine Glyphe. Das genaue Aussehen einer Glyphe wird durch den verwendeten Schriftsatz (Font) bestimmt. Bei europäischen Schriftsystemen ist eine Glyphe meist identisch mit einem Buchstaben, aber im Fall einer Ligatur wie „ff“ statt „f f” ist es auch möglich, dass eine Glyphe mehrere Buchstaben umfasst. Im Chinesischen repräsentiert eine Glyphe meist ein ganzes Wort. Ein Emoji wäre z. B. auch eine Glyphe.

Glyphen setzen sich aus einem oder mehreren Graphemen zusammen. Ein Graphem ist das kleinste bedeutungsunterscheidende Element einer Schriftsprache. Bei einfachen Buchstaben wie „A“ ist ein Buchstabe ein Graphem, das eine Glyphe ist. Akzente und andere Betonungszeichen oder Teile asiatischer Schriftzeichen kann man auch als Graphem auffassen. Je nach akademischer Sichtweise ist z. B. ein „ü“ ein einzelnes Graphem oder besteht aus den Graphemen „u“ und „¨“ (weshalb es beide Möglichkeiten gibt, einen Umlaut in Unicode zu repräsentieren).

Die Mission von Unicode war, alle Grapheme aller Schriftsprachen repräsentieren zu können. Oft ist es gelungen, allerdings nicht immer, sodass es besser ist, bei Unicodezeichen weder von Buchstaben, noch von Glyphen oder Graphemen zu sprechen, sondern von Code-Points.

Zu Zeiten der ersten Version von Unicode dachte man noch, dass 16 Bits reichen würden, um alle notwendigen Code-Points zu kodieren, daher haben populäre Sprachen aus den 90er-Jahren wie Java und C# chars der Größe 16 Bits. Für die meisten Texte aus lateinischen Schriftsystemen stimmt das, sodass man als Entwickler*in aus Westeuropa oft fälschlicherweise annimmt, dass ein char einem Code-Point bzw. einem Buchstaben oder einer Glyphe entspricht.

Bei späteren Versionen von Unicode wurde schnell klar, dass man mehr als 16 Bits braucht, um die Vielfalt menschlicher Schriftsysteme korrekt darzustellen. Daher kann es sein, dass ein Code-Point (z. B. ein Emoji) in Java durch zwei chars dargestellt wird. Je nach Größe eines chars können es mehr oder weniger sein. Es gilt also:

  • Einer oder mehrere chars ergeben einen Code-Point
  • Einer oder mehrere Code-Points ergeben eine Glyphe
  • Eine Glyphe entspricht einem oder mehreren Buchstaben

Es empfiehlt sich also, bei der Arbeit mit Unicode möglichst schnell auf Code-Point-Ebene zu gehen und sich darüber zu informieren, wie die eigene Programmiersprache mit diesem Problem umgeht.

Abstraktionsebenen der Textrepräsentation

Dank Unicodenormalisierung und Preprocessing auf Zeichenebene steht am Ende des ersten Preprocessing-Schritts ein „ordentlicher“ Unicodestring, der eindeutig verglichen, in Tokens zerlegt und weiterverarbeitet werden kann. Doch wie verwandelt man diesen String nun in einen Tensor, sodass ein ML-Algorithmus daraus etwas lernen kann? Prinzipiell kann man Text auf vier Abstraktionsebenen in Tensoren verwandeln:

  1. Auf Zeichenebene: Jeder Datenpunkt ist ein Zeichen oder eine fixe Anzahl von Zeichen (Buchstaben-N-Gramm), ein Dokument wird zu einer Sequenz von Datenpunkten. Die Länge der Sequenz entspricht der Anzahl der Zeichen.
  2. Auf Wortebene: Jeder Datenpunkt ist ein Wort oder eine fixe Anzahl von Wörtern (Wort-N-Gramm), ein Dokument wird zu einer Sequenz von Datenpunkten. Die Länge der Sequenz entspricht der Anzahl der Wörter.
  3. Auf Satzebene: Jeder Datenpunkt ist ein Satz, ein Dokument wird zu einer Sequenz von Datenpunkten. Die Länge der Sequenz entspricht der Anzahl von Sätzen.
  4. Auf Dokumentebene: Jeder Datenpunkt ist ein komplettes Dokument.

Die Größe der Datenpunkte wird fix gewählt. Das bedeutet aber immer noch, dass für die Darstellungsebenen 1, 2 und 3 eine variable Länge von Datenpunkten für einen Text existiert. Hierfür gibt es zwei Lösungsansätze:

  • Die Sequenzen werden auf eine fixe Länge forciert, entweder durch Abschneiden ab einer gewissen Länge oder durch Auffüllen mit speziellen Nulldatenpunkten.
  • Die Auswahl der möglichen ML-Verfahren wird auf sequenzbasierte Verfahren wie RNNs beschränkt.

Die Darstellung von Text durch Buchstaben oder Buchstaben-N-Gramme (Ansatz 1) ist hauptsächlich nützlich für die Spracherkennung (Ist der Text deutsch oder englisch?) oder für andere Domänen, in denen zwar Strings als Input vorliegen, aber keinen natürlichen Text enthalten, wie z. B. die Analyse von DNA-Daten. Um den Umfang dieses Artikels nicht zu sprengen, lassen wir diesen Ansatz daher außen vor.

Die Betrachtung von ganzen Sätzen als ein Datenpunkt (Ansatz 3) ist nicht sehr häufig, da man ein Satzende einfach als das Spezialwort „SATZENDE“ kodieren kann und so die Information über die Unterteilung des Dokumentes bewahrt und einfach Wortsequenzen benutzen kann. Alternativ kann man Sätze als Mini-Dokumente betrachten und die gleichen Techniken wie für Ansatz 4 verwenden.

Aus diesen Gründen werden wir uns nun auf die Ansätze 2 und 4 konzentrieren, also Sequenzen von Wörtern und Darstellungen ganzer Dokumente.

Wörterbücher

Auch nach Auswahl einer Abstraktionsebene für die Darstellung eines Texts ist eine Darstellung als Tensor noch immer nicht offensichtlich. Eine Wortsequenz ist immer noch eine Liste von Strings, und wie ein Dokument beliebiger Länge in einen Tensor fester Größe umgewandelt werden soll, ist auch nicht eindeutig.

Um weiterarbeiten zu können, fehlt noch ein entscheidender Schritt: die Erstellung von Wörterbüchern (Dictionaries). In ihrer einfachsten Form ordnen Wörterbücher jedem Wort eine eindeutige ID zu. Diese IDs können beliebig erstellt werden, meist werden die IDs einfach beim ersten Auftauchen eines Worts im Korpus (der Korpus ist die Gesamtmenge aller Dokumente, aus denen gelernt wird) fortlaufend vergeben. Aus dem Satz „Das ist das Haus vom Nikolaus“ wird also das Wörterbuch „das“ → 0, „ist” → 1, „haus” → 2, etc. Wie zu sehen ist, erzeugt das zweite Auftauchen des Worts „das“ keinen neuen Eintrag im Wörterbuch. Wörterbücher werden beim Erstellen einfach mittels Hash Maps mit dem Wort als Key und der ID als Value implementiert.

Der Begriff „Wörterbuch“ ist allerdings etwas irreführend: Je nach Anwendungsfall kann es z. B. sinnvoll sein, nicht ganzen Wörtern IDs zuzuordnen, sondern Buchstabensequenzen fixer Länge (Buchstaben-N-Gramme) oder Tupeln von Wörtern (Wort-N-Gramme). Für Buchstaben-N-Gramme der Länge N = 5 sähe das beispielsweise so aus: „das␣i“ → 0, „as␣is“ → 1, „s␣ist“ → 2, „␣ist␣“. → 3, etc. Ein solches Wörterbuch wird also durch scannen des Korpus mit einem Fenster fester Breite erstellt. Das ist notwendig, wenn wir wie zuvor aufgeführt auf Buchstabenebene arbeiten wollen.

N-Gramme aus Wörtern werden ähnlich gebildet. Für N = 2 ergibt sich z. B. folgendes Wörterbuch: „das ist“ → 0, „ist das” → 1, „das haus” → 2, etc. Das kann für manche Anwendungsfälle, wie die Dokumentenklassifikation auf sehr speziellen Kategorien, die Qualität erhöhen, hat aber den erheblichen Nachteil, dass die Größe des Wörterbuchs exponentiell zu N wächst. In der folgenden Betrachtung wollen wir uns aber auf Wörterbücher aus einzelnen Wörtern konzentrieren, da die in der Praxis die größte Rolle spielen.

Wichtig beim Verständnis von Wörterbüchern ist, dass sich die Größe nach dem Erstellen des Wörterbuchs nicht mehr ändert. Das Erstellen ist ein weiterer Preprocessing-Schritt, ML-Verfahren werden danach auf Wörterbüchern mit konstanter Größe und konstantem Inhalt trainiert. Die Größe des Wörterbuchs hat großen Einfluss auf die Anzahl der Dimensionen, mit denen sich ein Lernalgorithmus herumschlagen muss. Wird jedes Wort übernommen, kann die Größe schnell einige Hunderttausend Einträge erreichen, da jede Variation eines Worts („gehen“, „ging“, „gehst“, …) im Wörterbuch landet. Wörter, die nicht in das Wörterbuch übernommen werden, werden im Input durch das Spezialwort „UNKNOWN“ oder „UNK“ ersetzt. Um die Größe eines Wörterbuchs zu reduzieren, gibt es eine Reihe von Möglichkeiten:

  • Nur Wörter übernehmen, die eine Mindestanzahl von Vorkommen im Korpus haben
  • Die Wörterbuchgröße vorher fix wählen (z. B. 50 000) und nur die häufigsten Wörter übernehmen
  • Stemming: Wörter auf ihren Stamm zurückführen („gehen“ → gehen, „ging“ → gehen, „gehst“ → gehen, „Häuser“ → Haus etc.). Das wird durch spezielle, sprachabhängige und regelbasierte Algorithmen erreicht
  • Stoppworte filtern: Stoppworte (Stop Words) sind Hilfsworte, die grammatisch nötig, aber für gröbere Aufgaben wie die Textklassifizierung unnötig sind („der“, „die“, „das“, „an“, „bei“, „auf“, …); Stoppworte werden meist mithilfe fester Listen ausgefiltert, die für jede Sprache zuhauf online zu finden sind
  • Zwischen Groß- und Kleinschreibung unterscheiden oder nicht: Meist wird der Korpus komplett in Kleinbuchstaben umgewandelt, um die Größe des Wörterbuchs zu verringern; manchmal kann es aber sinnvoll sein, Groß- und Kleinschreibung beizubehalten
  • Wörter nach tf-idf (Term Frequency/Inverse Document Frequency) Score filtern und nur Wörter mit einem hohen tf-idf Wert übernehmen
  • Einsatz von speziellen Signalwörtern, die ganze Gruppen von Wörtern repräsentieren; so kann man z. B. alle Vornamen in einem Text durch das Wort „FIRSTNAME“ ersetzen, das sonst im Korpus nicht vorkommt; gleiches kann man mit Nachnamen, Postleitzahlen, Straßennamen, Städtenamen etc. machen; hierfür bedarf es zuvor des Einsatzes eines NER-Taggers (Named Entity Recognition), fester Namenslisten oder speziell angepasster Regular Expressions
  • Die richtige Größe eines Wörterbuches hängt stark vom Anwendungsfall und vom verwendeten Lernalgorithmus ab und muss experimentell bestimmt werden; übliche Größen liegen zwischen 10 000 und 500 000 Einträgen.

Textrepräsentation durch Bag of Words

Durch die Zuordnung von Wörtern zu fortlaufenden IDs durch Wörterbücher können wir nun Dokumente variabler Länge durch Tensoren fixer Größe darstellen: Unsere Tensoren haben Dimension 1 (sind also Vektoren) und die gleiche Länge wie das Wörterbuch. Jedem Dokument ordnen wir nun einen Vektor zu, in dem jeder Eintrag für das Vorkommen eines Wortes steht. Wir schmeißen also die Reihenfolge der Wörter komplett aus dem Fenster und notieren im Vektor des Dokuments nur, ob das Wort vorkommt. Wenn das Wort „Haus“ z. B. die ID 1024 hat, dann hat ein Dokument, in dem das Wort „Haus“ vorkommt, an der Stelle 1024 einen Wert ungleich null. Alle Wörter, die nicht vorkommen, werden auf null gesetzt. Es gibt verschiedene Möglichkeiten, den Wert für ein vorkommendes Wort zu bestimmen. Die häufigsten sind:

  • Hot-Cold-Encoding oder auch Bernoulli-Vektor: eine 1 für jedes vorkommende Wort, unabhängig von der Häufigkeit
  • Frequenzvektor: Anzahl der Vorkommen des Wortes

Für manche Lernverfahren wird danach die Länge des Vektors normalisiert. Unabhängig davon, wie genau ein solcher Dokumentenvektor erzeugt wird, spricht man immer von einem Bag-of-Words (BOW) und bezeichnet den Vektor als BOW-Vektor oder einfach nur BOW. Ein Beispiel findet sich im Kasten „BOW-Beispiel“.

BOW-Beispiel

Text: das ist das haus vom nikolaus
Wörterbuch: {das → 0, der → 1, die → 2, ist → 3, haus → 4,
vom → 5, nikolaus → 6, apfel → 7, birne → 8}
Bernoulli-Vektor: [1, 0, 0, 1, 1, 1, 1, 0, 0]
Frequenzvektor: [2, 0, 0, 1, 1, 1, 1, 0, 0]

Mit einem solchen in der Größe fixen Tensor können für die Klassifikation von Dokumenten nun alle Klassifikationsverfahren eingesetzt werden, die Vektoren fester Größe als Input verarbeiten können, z. B. lineare Modelle, logistische Regression, SVM, neuronale Netze, Random Forests und viele mehr. Hiermit kann man unter anderem Spam Detection betreiben oder herausfinden, ob es sich bei einem Dokument um eine Rechnung oder um einen Vertrag handelt.

Hier beginnt nun das typische Trial-and-Error-Verfahren eines ML-Projekts: Es können viele verschiedene Varianten der Vorverarbeitung ausprobiert werden:

  • Filtern und Mapping verschiedener Unicode-Zeichen (mit/ohne Ziffern, mit/ohne Emojis, Groß- und Kleinschreibung beibehalten/ausschalten etc.)
  • mit/ohne Stemming
  • mit/ohne Stoppwort-Filterung
  • verschiedene Mindestvorkommen, damit ein Wort ins Wörterbuch übernommen wird
  • Bernoulli-BOW-Vektoren oder Frequenz-BOW-Vektoren
  • Normalisierung der Wortvektoren an/aus

Durch das variable Preprocessing ergibt sich also eine Vielzahl neuer Hyperparameter zusätzlich zu den Hyperparametern des jeweiligen Lernalgorithmus. Wirklich wichtig für die praktische Arbeit ist, dass dieses Preprocessing für die Qualität des Modells oft entscheidender ist als der verwendete Algorithmus und dessen Hyperparameter.

Textrepräsentation durch Wortsequenzen

Durch BOW-Vektoren lassen sich erstaunlich genaue Klassifizierer bauen. Aber sie haben einen entscheidenden Nachteil: Aufgaben, die eine feinere Problemstellung haben, bei der es auf die genaue Wortanordnung und komplexere Semantik ankommt, können so nicht gelöst werden. Der BOW-Vektor hat ja sämtlichen Kontext vergessen, oft sind durch Stoppwort-Filterung wichtige Details verloren gegangen.

iJS React Cheat Sheet

Free: React Cheat Sheet

You want to improve your knowledge in React or just need some kind of memory aid? We have the right thing for you: the iJS React Cheat Sheet (written by Joel Lord). Now you will always know how to React!

Alternativ zu einem BOW-Vektor können wir aber auch einen Tensor erstellen, der einfach die Sequenz der IDs der vorkommenden Wörter in der richtigen Reihenfolge enthält. Füllt man diesen bis zu einer gewissen Länge mit einem konstanten Wert auf, bzw. schneidet ihn ab einer gewissen Länge ab, hat man sogar einen Tensor fester Größe. Alternativ kann man die Auswahl von Lernverfahren auf Verfahren einschränken, die einen Input dynamischer Länge erlauben, wie Recurrent Neural Networks (RNN). Ein Beispiel dafür findet sich im Kasten „RNN-Beispiel“.

RNN-Beispiel

Text: das ist das haus vom nikolaus
Wörterbuch: {das → 0, der → 1, die → 2, ist → 3, haus → 4,
vom → 5, nikolaus → 6, apfel → 7, birne → 8}
ID Sequenz fester Länge (acht Wörter): [0, 3, 0, 4, 5, 6, -1, -1]

Leider ist eine solche Darstellung in der Praxis nutzlos. Da die IDs rein zufällig zustande kommen, tragen die Zahlen keinerlei Bedeutung. Eine 1024 kann für „Haus“ stehen, eine 1025 für „Wackelpudding“. Da in den meisten Verfahren eine numerische Nähe oft auch für Nähe im Ergebnis sorgt, lernt ein ML-Algorithmus nur in absoluten Ausnahmefällen etwas aus einer Sequenz von zufälligen IDs und braucht dann unverhältnismäßig viele Daten und Zeit.

Um also die Wortsequenzen nutzbar zu machen, müssen die IDs durch eine immer noch numerische aber aussagekräftigere Darstellung ersetzt werden, die Informationen über die Semantik des Wortes bewahrt. Eine mögliche Darstellung nennt sich Einbettung (Embedding). Eine Einbettung kann man sich wie die Werte einer Figur in einem Rollenspiel vorstellen: Man definiert verschiedene Eigenschaften wie Stärke, Ausdauer, Intelligenz etc. und vergibt für sie numerische Werte (z. B. 0-10). Listet man alle Eigenschaften übereinander, ergibt das einen Vektor, der die Figur beschreibt. Mit solchen Eigenschaftsvektoren kann man nun Figuren miteinander vergleichen. Liegen Werte an einem bestimmten Index nahe beieinander, sind die Figuren sich bezüglich dieser Eigenschaft ähnlich. Das gleiche können wir nun mit beliebigen Wörtern machen. Das Wort „Katze“ könnte man wie in Abbildung 4 gezeigt einordnen.

Abb. 4: Das Prinzip einer Einbettung anhand eines konstruierten Beispiels für das Wort „Katze“

Abb. 4: Das Prinzip einer Einbettung anhand eines konstruierten Beispiels für das Wort „Katze“

Eine solche Einbettung händisch zu erstellen, ist allerdings nicht praktikabel. Selbst bei kleinen Wörterbüchern würde dies tausende Stunden manueller Kleinstarbeit brauchen. Auch wäre es enorm aufwändig, die genauen Kriterien für jedes Attribut zu spezifizieren. Zum Glück gibt es Möglichkeiten, solche Einbettungen automatisch erlernen zu lassen, z. B. durch den Word2Vec-Algorithmus (Abb. 5). Dieser nutzt einen bestehenden Textkorpus, wie die deutsche Wikipedia, zusammen mit einem einfachen neuronalen Netz um Einbettungen zu erlernen. Dazu wird zunächst eine Dimension für den Einbettungsvektor gewählt, übliche Werte liegen zwischen 50 und 500. Dann wird für jedes Wort im Wörterbuch der Einbettungsvektor zufällig initialisiert.

Abb. 5: Einbettung zu Beginn des Lernens: die Positionen der Wörter sind noch sinnlos und zufällig

Abb. 5: Einbettung zu Beginn des Lernens: die Positionen der Wörter sind noch sinnlos und zufällig

Nun wird der Textkorpus mehrere Male mit einem Wortfenster fester Breite durchlaufen, das immer die gleiche Anzahl benachbarter Wörter selektiert, z. B. fünf. Das neuronale Netz bekommt die Aufgabe, aus den Einbettungen der links und rechts stehenden Wörter die Einbettung des mittleren Wortes vorherzusagen. Anhand der Größe des Fehlers der Vorhersage werden die Werte für die Einbettungen stückweise angepasst, ähnlich eines normalen Gradientenabstiegs beim Training eines neuronalen Netzes. Abbildung 6 zeigt die Einbettung zweier Wörter, die sich während des Lernens entwickeln: Schritt für Schritt entwickeln zwei Achsen eine Bedeutung.

Abb. 6: Einbettung zweier Wörter, die sich während des Lesens entwickeln

Abb. 6: Einbettung zweier Wörter, die sich während des Lesens entwickeln

Wichtig für das Verständnis der Einbettungen ist, dass die Eigenschaften, die sich in den Einbettungen anhand von Geraden in einem hochdimensionalen Raum anordnen, nicht zuvor definiert oder eingestellt werden. Die Einbettungen für verschiedene Worte ordnen sich durch das Training von selbst so an, dass sich daraus Achsen mit semantischer Bedeutung ergeben. Diese Bedeutung ist allerdings nur einem menschlichen Nutzer bewusst und auch erst nach genauer Analyse. Ein KI-Verfahren, das Einbettungsvektoren nutzt, hat kein Wissen um die Bedeutung von Wörtern.

Obwohl Einbettungen eigentlich nur ein Vorverarbeitungsschritt für andere Verfahren sind, lassen sich die Einbettungen von Wörtern selbst bereits nutzen, um einfache logische Rätsel zu lösen, beispielsweise um Wortzugehörigkeiten festzustellen. Da die Einbettungen von Zahlwörtern von denen von Farbwörten abweicht, kann man durch einfache Abstandsberechnung herausfinden, welches Wort nicht dazu gehört (Abb. 7). Da sich die Eigenschaften von Wörtern entlang gerader Achsen organisieren, kann dies mit einfacher Vektorrechnung erreicht werden. Indem man beispielsweise die Differenz von „China“ und „Peking” zu „Italien” addiert, kann man herausfinden, dass „Rom“ die Hauptstadt von Italien ist. (Abb. 8)

Abb. 7: Einbettung von Wörtern zum Lösen logischer Rätsel

Abb. 7: Einbettung von Wörtern zum Lösen logischer Rätsel

Abb. 8: Berechnung einer Hauptstadt

Abb. 8: Berechnung einer Hauptstadt

Dabei werden die Wörter einfach durch ihre Einbettungsvektoren ersetzt und Addition und Subtraktion auf den Vektoren durchgeführt. Um das Ergebnis zu erhalten, wird einfach nach dem Einbettungsvektor gesucht, der dem Ergebnis am nächsten ist, und dessen Wort aus dem Wörterbuch ausgelesen (Abb. 9).

Abb. 9: Durch Addition und Subtraktion kann man die Eigenschaft „Mann“ durch die Eigenschaft „Frau“ ersetzen und erhält das richtige Ergebnis

Abb. 9: Durch Addition und Subtraktion kann man die Eigenschaft „Mann“ durch die Eigenschaft „Frau“ ersetzen und erhält das richtige Ergebnis

Zurück zu unseren Wortsequenzen: Die IDs in diesen Sequenzen werden nun durch die Einbettungsvektoren der jeweiligen Wörter ersetzt. Das Wörterbuch verweist nun also nicht mehr von Wörtern auf IDs, sondern von Wörtern auf Einbettungsvektoren (in der Praxis werden die Einbettungsvektoren oft in einer großen Matrix gespeichert und das Wörterbuch verweist auf den Index der Spalte). Mit sequenzbasierten Verfahren wie RNNs können nun Klassifizierer, Regression, Tagging oder auch komplexere Anwendungen wie Übersetzer mit Sequenzen von Einbettungsvektoren trainiert werden.

In dieser Erläuterung haben wir die Erstellung der Einbettungen als einen separaten Preprocessing-Schritt betrachtet. Trainiert man neuronale Netze, kann es aber von Vorteil sein, die Einbettungen und deren Training selbst zum Teil des neuronalen Netzes zu machen, mit dem man an dem eigentlichen Problem arbeitet. Der Vorteil ist, dass dann der gesamte Gradient, der für jede Trainingsinstanz berechnet wird, das Netz selbst und die Einbettung in einem Schritt anpasst. Das führt oft dazu, dass die Einbettung sich problemspezifisch anpasst. In der Analyse von Verträgen wird sich so zum Beispiel keine Achse für die Flauschigkeit von Haustieren herausbilden und die Einbettung wird auf relevante Worteigenschaften fokussiert. Ebenso ist es oft möglich, Einbettungen mit niedrigerer Dimension zu nutzen, was zu kompakteren Netzen und schnellerer Konvergenz führt.

Weitere Anwendungen für Einbettungen

Im vorigen Beispiel wurden Einbettungsvektoren immer als Sequenz genutzt, was die Anzahl der verfügbaren Lernverfahren stark einschränkt. Aber man kann Einbettungsvektoren auch zur Erstellung von BOW-artigen Vektoren für Dokumente nutzen: Dazu werden einfach alle Einbettungsvektoren der im Dokument enthaltenen Wörter aufaddiert und der resultierende Vektor wieder normiert. Die resultierenden Vektoren sind wesentlich kleiner als die klassischen BOW-Vektoren, die ja hauptsächlich aus Nullen bestehen. Trotzdem können sie genauso wie diese für Klassifizierung, Clustering oder Ähnliches benutzt werden. Welcher Ansatz bessere Ergebnisse bringt, hängt stark vom Anwendungsfall ab und kann konkret nur durch Ausprobieren festgestellt werden. Generell ist ein solcher Durchschnittsvektor vor allem bei kürzeren Dokumenten oder einzelnen Sätzen nützlich.

Einbettungen sind nicht nur für Wörter anwendbar, sie sind generell gute Werkzeuge, wenn Eingabedaten, die sonst durch große One-Hot-Vektoren kodiert werden müssten, in der Dimension reduziert werden sollen. Also immer dann, wenn ein Eingabewert nicht numerisch ist, sondern ein Element aus einer festen aber großen Menge ist. Beispiele können Produktbezeichner, Chemikalien, Orte oder ähnliches sein. Alles, was ansonsten einer ID zugeordnet werden würde, die für sich aber keine Bedeutung hat.

Ein weiteres wichtiges Beispiel für Einbettungen ist der Doc2Vec-Algorithmus, der analog zum Word2Vec-Algorithmus Einbettungen für ganze Dokumente statt für Wörter ermittelt. Hier entsteht die Einbettung nicht einfach als Durchschnitt einzelner Worteinbettungen, sondern wird für jedes Dokument im Korpus individuell ermittelt.

Fazit

Preprocessing ist ein enorm wichtiger Schritt in jedem ML-Projekt. Im Falle von Textinput ist dies ganz besonders der Fall. Bei umfangreicheren Projekten ist es essenziell, eine variable Input-Pipeline aufzubauen und viele verschiedene Arten und Parametrierungen des Preprocessing auszuprobieren und die Ergebnisse zu vergleichen. Am besten ist es, die Preprocessing-Parameter als weitere Hyperparameter des ML-Lernalgorithmus aufzufassen und genauso zu behandeln. Absolut essenziell ist eine saubere Unicode-Normalisierung.

Nimmt man sich aber die notwendige Zeit und arbeitet bei diesen essenziellen ersten Schritten sauber, wird man mit einer flexiblen Input-Pipeline belohnt, mit der sich viele verschiedenartige ML-Verfahren ausprobieren und vergleichen lassen. Wichtig ist: Egal, wie pfiffig der ML-Lernalgorithmus ist, wenn gravierende Fehler beim Preprocessing gemacht werden oder die gewählte Darstellung des Textes für das Problem unpassend ist, werden die Ergebnisse bestenfalls ernüchternd ausfallen. Und wie bei allen ML-Projekten gilt, dass man mit sauberem Preprocessing und dem einfachsten möglichen Algorithmus anfangen sollte, um eine Baseline zu etablieren, damit man am Ende einschätzen kann, ob das komplexe und spannende State-of-the-Art-Verfahren überhaupt eine Verbesserung bringt.

Entwickler Magazin

Entwickler Magazin abonnierenDieser Artikel ist im Entwickler Magazin erschienen.

Natürlich können Sie das Entwickler Magazin über den entwickler.kiosk auch digital im Browser oder auf Ihren Android- und iOS-Devices lesen. In unserem Shop ist das Entwickler Magazin ferner im Abonnement oder als Einzelheft erhältlich.

Unsere Redaktion empfiehlt:

Relevante Beiträge

Hinterlasse einen Kommentar

Hinterlasse den ersten Kommentar!

avatar
400
  Subscribe  
Benachrichtige mich zu:
X
- Gib Deinen Standort ein -
- or -