Text Mining: Grundlagen

Andreas Warntjen

21.2.2021

Einführung

Mit Text Mining-Methoden lassen sich Informationen aus großen Textmengen generieren, etwa zu Themenschwerpunkten oder der Stimmung (sentiment analysis). Beispiele wären die Vorsortierung von Emails an ein Kundenservice-Center nach Thema (Beschwerde, Nachfrage zu einem Produkt, etc.) oder die Auswertung von Produktrezensionen (wie positiv wird das Produkt bewertet?). Da es sich bei Texten um unstrukturierte Daten handelt, sind für das Text Mining besondere Schritte bei der Datenaufbereitung notwendig. Die Datenaufbereitung umfasst typischerweise folgende Schritte:

Das folgende Beispiel analysiert die Antrittsreden (inaugural address) amerikanischer Präsidenten von 1789 (George Washington) bis 2017 (Trump). Der Fokus liegt auf der Datenaufbereitung, deshalb besteht die Analyse selber aus einem einfachen Vergleich von absoluten Häufigkeiten. Die Fragestellung ist, wie häufig in den Antrittsreden von Amerika bzw. der Verfassung die Rede ist.

Die Analyse erfolgt in Python mit Hilfe der Bibliothek NLTK.

Bibliotheken und Daten laden

Zunächst importieren wir die Bibliothekten nltk, re (für reguläre Ausdrücke), und pandas (für die Analyse). Die Bibliothek nltk beinhaltet eine Reihe von Beispieldatensätzen, darunter die Antrittsreden von US-Präsidenten (inaugural), die wir benutzen.

Meta-Daten

Texte sind typischerweise in einem Korpus zusammenhängender Texte organisiert. In unserem Beispiel also alle Antrittsreden von US-Präsidenten. Die einzelnen Texte sind als Textdateien abgelegt. Die Dateinamen enthalten Meta-Daten, nämlich das Jahr und den Nachnamen des Präsidenten, die wir auslesen möchten.

Auf die im Textkorpus inaugural enthaltenen Texte (bzw. Dateinamen) können wir mit der Methode fileids() zugreifen. Wir speichern diese Liste aller Antrittsreden in speeches.

Zeichenketten extrahieren (reguläre Ausdrücke)

In den Dateinamen ist das Jahr der Rede und der Name des Redners/Präsidenten enthalten. Diese Teile extrahieren wir mit regulären Ausdrücken.

Reguläre Ausdrücke (regular expressions) identifizieren Muster in Zeichenketten. Mit regulären Ausdrücken kann man nicht nur direkt nach einer bestimmten Zeichenfolge suchen (etwa "Obama"), sondern auch nach abstrakteren Mustern (4 Zahlen am Anfang eines Zeichenkette). In unserem Beispiel suchen wir nach einer vierstelligen Jahreszahl und wir könnten einfach die ersten vier Zeichen der Dateinamen nehmen. Ein etwas spezifischeres Muster ist es eine Zeichenkette zwischen bestimmten Zeichen zu suchen. Hier also etwa vier Zahlen (oder digits: "\d") vor einem Bindestrich. Wir extrahieren diese mit regulären Ausdrücken mit Hilfe der Funktion search() aus der Bibliothek re. Der erste Parameter ist der reguläre Ausdruck, der zweite der Text auf den es angewendet werden soll. "\d{4}" bedeutet, dass wir viermal eine Zahl (digit) suchen. Die Angabe "4" in eckigen Klammern ist ein quantifier: wie häufig soll dieser Ausdruck vorkommen (einmal? beliebig oft? eine bestimmte Anzahl?). Dieser gesamte Teil des regulären Ausdruck ist in runden Klammern und bildet damit eine Gruppe, die wir in einem nächsten Schritt mit group() extrahieren können. Prinzipiell kann man mehrere Bestandteile gleichzeitig extrahieren (mehrere jeweils geklammerte Teile eines regulären Ausdrucks), die dann mit der Nummer der Gruppe identifiziert werden müsste. Hier haben wir nur eine Gruppe (vier Zahlen).

Wir wollen die Jahreszahl nicht nur aus einer Zeichenkette (also einem Dateinamen) extrahieren wollen, sondern aus allen. Dazu könnte man eine For-Schleife benutzen. In Python gibt es das spezielle Konstrukt list comprehensions, die die gleiche Funktionalität liefern. Eine Funktion (hier: Extraktion eines Teils eines Zeichenkette mit re.search()) wird einzeln auf alle Elemente einer Liste angewandt und das Ergebnis wird als Liste zurückgelieft. Wir speichern die Liste der Jahreszahlen als _years_lst_.

Für die Liste der Präsidenten extrahieren wir alle Buchstaben zwischen dem Bindestrich und dem Punkt vor dem Dateipräfix (".txt"). Oben haben wir "\d" genutzt, um nach beliebigen Zahlen (digits) zu suchen. Hier nutzen wir eine Definition der Gruppe von Zeichen nach denen wir suchen, nämlich alle Zeichen von A-Z (groß geschrieben) und a-z (klein geschrieben) - also alle Buchstaben. Im Gegensatz zur Jahreszahl wissen wir nicht, wie viele Buchstaben wir suchen. Die Namen der Präsidenten sind unterschiedlich lang. Deshalb nehmen wir als quantifier "*" (beliebig viele). Die Eingrenzung durch den "." ist hier etwas redundant (der Punkt ist kein Buchstabe). Man kann auf mehreren Webseiten reguläre Ausdrücke ausprobieren (z.B. https://regex101.com/). Das Ergebnis (Nachname der Präsidenten) speichern wir in der Liste _presidents_lst_.

Die Reden

Auf die Rohversion der Reden greifen wir mit der Methode raw(), indem wir den Dateinamen angeben. Wir gucken uns als Beispiel die ersten 500 Zeichen der Antrittsrede von Präsident Obama im Jahr 2013 an.

Der Text enthält noch Steuerungszeichen ("\n\n" für einen Zeilenumbruch). Außerdem gibt ein Encoding-Problem ("â\x80\x94" statt " - "). Für diese eine Rede können wir es direkt austauschen.

Normalisierung

Wir wollen im Folgenden nicht zwischen Groß- und Kleinschreibung unterscheiden - also etwa zwischen "We" (am Satzanfang) und "we" (im Satz). Deshalb ändern wir den String komplett auf Kleinschreibung.

Tokenization

Viele Text Mining-Methoden beruhen allein auf der Häufigkeit von Wörter (bag of words), nicht deren Reihenfolge. In unserem Beispiel wollen wir ebenfalls nur die Häufigkeit von Wörtern zählen. Dazu müssen wir den Text (eine lange Zeichenkette) in eine Liste von einzelnen Wörtern umwandeln. Wenn uns etwa die Anzahl der Wörter pro Satz interessieren würde, dann müssten wir zusätzlich den Text in Sätze unterteilen. Die Unterteilung einer Zeichenkette in seine Elemente (Sätze, Wörter) nennt sich tokenization. Mit der Methode word_tokenize() der Bibliothek nltk konvertieren wir die Obama-Rede (als Zeichenkette) in eine Liste einzelner Wörter.

Irrelevante Wörter entfernen

Viele dieser Wörter sind Satzzeichen, die wir nicht berücksichtigen möchten. Gleiches gilt für häufige Wörter ("Stoppwörter"; stop words), wie etwa Artikel. NLTK hat eine Liste häufiger Wörter, die wir um Satzzeichen ergänzen. Wir greifen darauf mit stopwords.words() zu und geben an, dass wir Stoppwörter für die englische Sprache benötigen.

Es gibt 179 Stoppwörter in der NLTK-Liste

Dann suchen wir nach Satzzeichen - also Elementen unserer Liste, die nur aus einem Zeichen bestehen. Dazu benutzen wir wieder eine list comprehension, diesmal ergänzt um eine Filter-Klausel (if). Der Befehl set() reduziert die Liste auf die Menge eindeutiger Zeichen, damit entfernen wir redundante Nennungen des gleichen Elements.

Die Satzzeichen, die wir so gefunden haben, nehmen wir in die Liste _stop_words_to_add_ auf und fügen sie zu der NTLK-Stoppwörterliste hinzu.

Die Gesamtliste (_stop_words_extended_) beinhaltet 184 Elemente (Wörter und Satzzeichen). Mit Hilfe einer list comprehension entfernen wir diese Stoppwörter aus der Liste der Wörter der Obama-Rede. Das Ergebnis ist _Obama_2013_clean_.

Worthäufigkeiten zählen

Mit FreqDist() können wir ein Lexikon mit der Häufigkeit der Wörter anlegen.

Das Wort "us" (was man auch als Stoppwort betrachten könnte) ist mit 21 Nennungen das häufigste Wort. "america" kommt 8 mal vor. Wir können auch direkt auf die Häufigkeit einzelner Wörter zugreifen.

Hilfsfunktion zur Textaufbereitung

Die Aufbereitung des Textes, die wir an einem Beispiel demonstriert haben, können wir in einer Funktion zusammenfügen, um sie dann auf alle Texte anzuwenden.

Unsere neue Hilfsfunktion erlaubt es uns, die Aufbereitung und Analyse der Obama-Rede mit nur drei Zeilen für eine weitere Rede durchzuführen. Wir können beispielsweise gucken, wie häufig George Washington in 1789 das Wort "America" benutzt hat.

Erstaunlicherweise hat George Washington kein einziges mal den Namen "America" benutzt. Tatsächlich spricht er an zwei Stellen von dem amerikanischen Volk ("American people"), aber eben nicht von dem Land selbst ("America").

Analyse ohne Stemming

Wir können nun eine erste Antwort auf unsere Frage geben: wie häufig wurden die Wörter "America" und "Constitution". Dazu lassen wir in einer For-Schleife alle Texte durch unsere Hilfsfunktion zur Textbereinigung laufen und zählen anschließend die Nennung von "America" bzw. "Constitution". Die Häufigkeit pro Rede speichern wir jeweils in Listen (_america_frq_lst_ bzw. _constitution_frq_lst_) ab.

Die Listen mit den Worthäufigkeiten sowie die Listen mit den Meta-Daten (Jahr und Präsident) fügen wir nun zu einem pandas data frame zusammen.

"America" kam im Durchschnitt 3,7 mal vor, im Schnitt marginal häufiger als das Wort "Constitution" (3.6). Bei der höchsten Anzahl an Nennungen liegt "constitution" mit 36 Nennungen klar vorne (America: 21).

Um zu analysieren, wie sehr sich die absoluten Häufigkeiten im Zeitverlauf geändert haben, können wir die Worthäufigkeiten als Liniendiagramm visualisieren.

Die Verfassung ("constitution") wurde zu Beginn der Republik und in der Mitte des 19. Jahrhunderts häuf erwähnt, im 20. und 21. Jahrhundert hingegen selten. Referenzen auf das Land gab es anfangs selten, aber im 20 und 21. Jahrhundert häufig.

Analyse mit Stemming

Stemming/Lemmatisierung

Wir hatten oben gesehen, dass George Washington in seiner ersten Antrittsrede das Wort "America" kein einziges mal benutzt hat aber "American" zwei mal. Für unsere Analyse mag dieser Unterschied (Landesname als Substantiv oder Adjektiv) egal sein - wir möchten nur wissen, wie häufig es Verweise auf das Land oder seine Menschen, etc. gab. Grammatikalische Unterschiede zwischen Wörter (Singular vs. Plural, unterschiedliche Endungen auf Grund grammatikalischer Regeln, etc.) sind beim Text Mining meistens nicht sehr relevant, weil es uns primär um die Bedeutung von Wörtern geht.

Um Wörter auf ihren Wortstamm bzw. auf ihre grundlegende Bedeutung zurückzuführen gibt es zwei grundlegende Techniken:

Wir probieren zwei Formen von Stemming (Porter Stemmer, Snowball Stemmer) und eine Implementierung von Lemmatisierung aus. Unser erstes Beispiel sind Variationen des Wortes "constitution" (Substantiv im Singular, Substantiv im Plural, Adjektiv).

Lemmatisierung beseitigt den Unterschied zwischen Singular und Plural, unterscheidet aber zwischen der Wortart (Substantiv, Adjektiv). Die Stemmer führen alle Variationen auf "constitut" zurück. Für unsere Zwecke ist die zweite Variante sinnvoller.

Mit (normalisierten/klein geschriebenen) Variationen von "America" gelingt es keinem Stemmer bzw. Lemmatizer alle Wörter auf ein Wort(-stamm) zu reduzieren.

Auch ein Vorziehen dieses Bearbeitungsschritts vor die Normalisierung (Umwandlung in Kleinschreibung) würde keinen Unterschied machen.

Deshalb tauschen wir die "Synonyme" einfach direkt mit "america" aus.

Am Beispiel der Obama-Rede von 2013 sehen wir den Effekt davon.

Es gab insgesamt 18 Nennungen von "America", "American" oder "Americans".

Mit unserer Hilfsfunktion replace_synonyms_america() und einer Liste von "Synonymen" (für unsere Zwecke) des Wortes "America" kommen wir zu dem gleichen Ergebnis.

Wir möchten also sowohl ein Stemming als auch einen Austausch von Synonymen vornehmen. Dazu definieren wir noch eine Hilfsfunktion stem_text().

Analyse

Jetzt können wir in einer kurzen For-Schleife alle Bearbeitungsschritte (Bereinigung, Austausch Synonyme, Stemming) für alle Texte durchführen und Verweise auf das Land und die Verfassung zählen.

Für die Datenanalyse generieren wir wieder einen pandas data frame.

Die Häufigkeiten sind jetzt - nachdem wir grammatikalische Unterschiede ignorieren - höher. Im Durchschnitt gibt es häufiger Verweise auf das Land (oder seine Menschen, ...) als auf die Verfassung.

Während des Kalten Krieges gab es eine steigende Tendenz von Referenzen auf "Amerika". Diese Anzahl ging nach dem Ende des Kalten Krieges für eine WEile zurück und ist zuletzt (letzte Rede: Trump 2017) wieder gestiegen.

Weitere Informationen

https://www.tutorialspoint.com/natural_language_toolkit/natural_language_toolkit_stemming_lemmatization.htm

https://towardsdatascience.com/text-preprocessing-with-nltk-9de5de891658