How to: Texte mit Python analysieren Part 3

Wir beschäftigen uns in diesem Artikel wieder mit den Trump Tweets aus 2019 wie schon in den vorherigen Artikeln. Die sind nun schon deutlich älter. Die gezeigten Prinzipien lassen sich aber auch auf ziemlich alle anderen Datensätze anwenden. Ziel ist es zu analysieren, was für Themen Cluster in den Tweets stattgefunden haben. Ich werde die aufbereiteten Texte mit einfachen statistischen Mitteln für einen Cluster Algorithmus „interpretierbar“ machen und mehrere unterschiedliche Algorithmen miteinander vergleichen.

Es ist nun schon etwas länger her, dass ich einen Blog Beitrag verfasst habe. Es ist halt doch nicht so viel Zeit wie ich gedacht habe, um sich nebenbei mit solchen Themen zu beschäftigen. Aber die Geduld soll belohnt werden. Darum werden wir die Texte nun versuchen zu Clustern und an dem Beispiel 3 spannende Cluster Algorithmen vergleichen. K-Means, DBSCAN und HDBSCAN. Vorab, es gibt nicht den einen perfekten Algorithmus der immer passt. Neben diesen drei Algorithmen gibt es noch viele weitere die ebenso verwendet werden können.



Die bisherige Geschichte

Dies ist die Fortsetzung einer Artikelreihe, die ich bereits in 2020 gestartet habe. Der Start war eine grundlegende Einführung in Text Analyse und Machine Learning „How to: Texte mit Python analysieren Part 1“. Den zweiten Artikel habe ich im Frühjahr 2021 veröffentlicht. Hierbei ging es um eine erste explorative Untersuchung der Daten und Themen mit Advanced Analytics Methoden „How to: Texte mit Python analysieren Part 2“. Ich empfehle klar, dass ihr euch die einmal durchlest, da ich auf viele Dinge aufbaue die ich dort erklärt oder eingeführt habe. Nun ist wieder ordentlich Zeit ins Land gegangen und der Datensatz ist noch älter geworden. Damit ist die Relevanz der Daten inzwischen gegen Null tendierend aber ich will ja auch nur ein paar Prinzipien veranschaulichen. Darum hoffe ich, dass ihr trotzdem den Artikel lest und eventuell etwas mitnimmt für eure nächsten Text Mining und Analyse Projekte.

 



 

Let’s Start

Um mit unserer Datenanalyse zu beginnen müssen wir erstmal die wichtigsten Packages laden. Dazu zählen vor allem:

  • Pandas – Daten Analyse Package und stellt Dataframes zur Verfügung
  • Numpy – Support für Multi Dimensionale Arrays und effiziente Berechnung von Daten
  • Spacy – NLP Modul für den produktiven Einsatz
  • Re – Regexmodul von Python
  • Sklearn – Die bekannteste Machine Learning Library in Python
  • matplotlib – Eine umfangreiche Library zur Visualisierung von Daten
  • PCA – Principal Component Analyse ermöglicht die Dimensionsreduktion von Daten
  • HDBSCAN – Eine Library für einen zusätzlichen Cluster Algorithmus
import pandas as pd
import numpy as np
import re

import spacy

from sklearn.preprocessing import FunctionTransformer
from sklearn.pipeline import Pipeline
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.cluster import KMeans
from sklearn.metrics import silhouette_score

from sklearn.decomposition import PCA

import matplotlib.pyplot as plt

Nach dem grundsätzlichen Import unserer Packages definieren wir ein paar Funktionen. Diese sollen das Preprocessing der Texte ermöglichen und die Texte standardisieren. Im ersten Part laden wir ein Sprachmodell für Spacy. Da wir es mit englischen Texten zu tun haben laden wir entsprechend ein englisches Sprachmodell. Es gibt für viele Sprachen bereits Standard Sprachmodelle bei Spacy. Allerdings können auch eigene Sprachmodelle mit etwas Aufwand trainiert werden. Für unseren Use Case bleiben wir aber bei den Standard Sprachmodellen.

nlp = spacy.load('en_core_web_md')
spacy_stopwords = spacy.lang.en.stop_words.STOP_WORDS

Als nächstes benötigen wir eine Funktion die die Texte von Sonderzeichen bereinigt und standardisiert. Deshalb schreiben wir uns eine Preprocessor Funktion. Diese nutzt mehrere Reguläre Ausdrücke um die Sonderzeichen zu erkennen und entsprechend zu ersetzen. Für jeden Datensatz muss man so eine Funktion etwas anpassen. Dabei ist die Quelle der Daten wichtig zu berücksichtigen und die Zielstellung der Textanalyse. Wenn zum Beispiel die Stimmung eines Posts mithilfe einer Semantik Analyse untersucht werden soll, dann können Emoticons wichtige Informationen liefern. Bei einer inhaltlichen Analyse sind die Sonderzeichen aber eher störend.

def preprocessor(text):
    if isinstance((text), (str)):
        text = re.sub('<[^>]*>', '', text)
        text = re.sub('[\d]+', '', text.lower())
        text = re.sub('"', '', text.lower())
        text = re.sub('https://twitter.com','',text.lower())
        text = re.sub('!pic.twitter.com','',text.lower())
        text = re.sub('pic.twitter.com','',text.lower())
        text = re.sub('\[.*?\]', '', text)
        text = re.sub('https?://\S+|www\.\S+', '', text)
        text = re.sub('<.*?>+', '', text)
        text = re.sub('\n', '', text)
        text = re.sub('\w*\d\w*', '', text)
        text = re.sub('\'','', text)
        return text
    else:
        pass

Zusätzlich zu dieser Funktion kommt nun noch eine Funktion die die Daten normalisiert. Also zum Beispiel Endungen abschneidet oder Wörter auf ihren Wortstamm zurückbildet. Spacy bietet hierbei einige nützliche Funktionen an. So bildet Spacy automatisch die lemma und stemma von Wörtern, wenn man einen Text einliest und die Wörter tokenisiert. Mit dem Aufruf „lemma_“ können diese für jedes token Objekt abgerufen werden. Wir gehen also für jeden Text einmal jedes Wort durch und ersetzen das Wort durch sein Lemma also seinen Wortstamm.

def normalizer(text):
    tokenizer = nlp.Defaults.create_tokenizer(nlp)
    tokens = tokenizer(text)
    lemma_list = []
    for token in tokens:
        if token.is_stop is False:
            token_preprocessed = preprocessor(token.lemma_)
            if token_preprocessed != '':
                 lemma_list.append(token_preprocessed)
    tokens = lemma_list
    return tokens

Als letztes schreiben wir uns noch eine Funktion die es uns ermöglicht die Daten in eine Pipeline zu übergeben. Diese prüft außerdem ab ob die Werte in einer Liste sind oder nicht und geht entsprechend damit um.

def pipelinize(function, active=True):
    def list_comprehend_a_function(list_or_series, active=True):
        if active:
            return [function(i) for i in list_or_series]
        else: # if it's not active, just pass it right back
            return list_or_series
    return FunctionTransformer(list_comprehend_a_function, validate=False, kw_args={'active':active})



Die Funktionen zusammenfügen und ausführen

So nun müssen wir unsere Daten ja nur noch einlesen und die Funktionen alle zusammen ausführen. Dies erfolgt ganz klassisch mithilfe von Pandas.

Die verwendete Datei könnt ihr dabei auf Kaggle finden.

df = pd.read_csv('./processed_data/output_overview_v1.csv')

und dem aufrufen unserer Funktionen bzw. dem erstellen unserer Text Processing Pipeline.

text = df["doc"]

spacy_estimators = [('tokenizer', pipelinize(normalizer)), ('preprocessor', pipelinize(preprocessor))]
spacy_pipe = Pipeline(spacy_estimators)
output = spacy_pipe.transform(text)

Dabei laden wir die Spalte mit den Texten in eine eigene Variable. Wir erzeugen dann eine pipeline mit unseren Funktionen und führen diese dann mit dem .transform() Befehl auf unsere Texte aus. Das Ergebnis können wir dann wieder als eigene Spalte in unserem Datensatz hinzufügen. Dabei sind nun die Stoppwörter entfernt worden, alle Buchstaben in Lowercase und von Sonderzeichen bereinigt. Mit diesem Ergebnis haben wir nun einen deutlich reduzierten Wortkorpus in unserem Datensatz. Allerdings benötigen wir für ein Clustering die Daten nicht nur in einem einheitlichem Format, sondern auch noch als Zahl.

Denn: Cluster Algorithmen versuchen in einem N-Dimensionalen Raum die Punkte die z.B. besonders nahe zusammenstehen in ein Cluster einzuordnen. Um die Wörter entsprechend in Zahlen umwandeln zu können wird der TF-IDF Vectorizer von SK-Learn sehr häufig verwendet.



Wörter in Vektoren transformieren

Der TF IDF Vectorizer transformiert Wörter in Vektoren. TF-IDF steht dabei für Text Frequency und Inverse Document Frequency. Also vereinfacht gesagt. Es wird geprüft wie häufig kommt das Wort im aktuellen Text vor und wie häufig kommt es im gesamten Dokument (Alle Texte im Datensatz) vor. Dabei wird die Bedeutung für den Inhalt des aktuellen Textes gemessen. Wenn ein Wort besonders häufig im aktuellen Text vorkommt, allerdings nicht im gesamten Datensatz, so lässt sich eine hohe Bedeutung für den Inhalt des Textes ableiten. Wenn es auch besonders häufig in anderen Texten vorkommt, dann wird es für den Inhalt nicht viel Information bereitstellen. Davon sind vor allem Wörter wie zum Beispiel „und“, „darum“, „deshalb“ und so weiter betroffen. Der TF-IDF Vectorizer lässt sich folgendermaßen anwenden:

vectorizer = TfidfVectorizer()
X = vectorizer.fit_transform(df['processed_txt'])

Dabei kann der TF-IDF Vectorizer noch weitere Parameter bekommen wie z.B. die Anzahl der maximalen Features oder die maximale / minimale Document Frequency die ein Feature aufweisen muss. Am besten schaut ihr euch für ein besseres Verständnis die Dokumentation des TF_IDF Vectorizers auf SK-Learn an.

Das Ergebnis ist ein Array in der Größe des ursprünglichen Datensatzes und mit einem Wert für jedes Wort im Text. Und auf diesem Datensatz können wir nun endlich ein Clustering anwenden. Ich benutze in der Regel immer den K-Means Algorithmus von SK-Learn für das Clustering.

true_k = 3
kmeans = KMeans(n_clusters=true_k, init='k-means++', max_iter=300, n_init=10)
kmeans.fit(X)

prediction = kmeans.predict(X)
# Cluster in das Dataframe hinzufügen
df['k_means_pred'] = prediction

Da der K-Means Algorithmus die Anzahl der Cluster, die in dem Datensatz zu erwarten sind, vorgegeben bekommen muss habe ich diese ihm mit 3 definiert. X ist unser Array, welches an das K-Means Objekt übergeben wird mit „.fit()“ und mit „.predict()“ wird das Cluster für die Datensätze zurückgegeben. Das Ergebnis wird dann wieder in unserem Dataframe gespeichert. Damit haben wir nun unsere Texte in Cluster eingeteilt. Aber.. fehlt da nicht noch was? Woher wissen wir denn jetzt was für Cluster es gibt und was der Inhalt dieser Cluster ist?


Themen der Cluster zuordnen

Für die Themen Zuordnung müssen wir nun die Top Features je Cluster ermitteln und ausgeben. Da K-Means die Cluster mithilfe von Abständen zu einem Cluster Zentrum ermittelt sind die Top Features entsprechend die Wörter, die besonders nahe an dem Clusterzentrum liegen. Dazu lassen wir uns von dem Vectorizer die Namen der Features ausgeben und sortieren diese den Top 10 Features nach der Entfernung zum jeweiligen Clusterzentrum zu.

order_centroids = kmeans.cluster_centers_.argsort()[:, ::-1]
terms = vectorizer.get_feature_names()
clust = []

for i in range(true_k):
    print("Cluster %d:" % i),
    for ind in order_centroids[i, :10]:
        clust.append([i, terms[ind]])
        print(' %s' % terms[ind]),

In dem angehängten Bild kann man sich die Top Features anzeigen lassen. Dabei wird schnell deutlich, dass es ein Cluster gibt für das Thema Impeachment und ein Cluster, dass sich um die Themen Grenzen und China drehen.

Top Cluster Features

Das Problem bei dem K-Means Algorithmus ist die erforderliche Vorgabe der Anzahl der Cluster. Wie man diese Anzahl ermitteln kann werde ich in einem späteren Beitrag nochmal beschreiben. Außerdem werden alle Datensätze zwingend einem Cluster zugeordnet. Daten die eigentlich in keines der Cluster gehören werden trotzdem zugeordnet. Dies liegt im Algorithmus bedingt, welcher kein „Rauschen“ kennt. Dafür gibt es allerdings dichtebasierte Verfahren wie der DBSCAN oder auch HDBSCAN. Hierzu werde ich auch nochmal einen Beitrag erstellen.

Ich hoffe ich konnte euch ein paar Ansätze zeigen, wie man mithilfe von Machine Learning große Textmengen so aufbereitet, dass sich sinnvolle Informationen extrahieren lassen. Lasst mich gerne auch Wissen wenn ihr Fehler findet oder etwas zu ungenau beschrieben ist.