Wir klassifizieren Dichter: Data-Analysis

Namaste! Dieser Artikel ist eine Fortsetzung. Wir bauen weiter an dem Dichter-Klassifikator. Zur Erinnerung: Damit ist ein spezielles Neural-Network gemeint. Es kennt Dichter. Johann Wolfgang von Goethe, Gotthold Ephraim Lessing, Friedrich Schiller und Hermann Hesse. Eine KI, die beliebige Texte lesen kann und dann sagt, wonach sie klingen. Wir liegen gut in der Zeit. Heute machen wir etwas Wichtiges: Data-Analysis.

Deep-Learning hat immer einen hohen Data-Science-Anteil. Im letzten Tutorial haben wir einen Textkorpus generiert. Dazu haben wir bei Project Gutenberg die Texte der Dichter runter geladen. Bevor wir die Texte fürs Training kodieren, gönnen wir uns noch einen Moment. Wir analysieren das gesamte Data-Set.

Data-Analysis ist ein wichtiger Teil von Deep-Learning.

In der Deep-Learning-Szene kennt man die Aussage „Deep-Learning ist 20% Neural-Networks und 80% Data-Science“. Ich tendiere zu „Ja, das stimmt“. Das deckt sich halt mit meinen Erfahrungswerten. Bevor die Daten zum Training in ein Neural-Network gefüttert werden können, vergeht ein wenig Zeit. Nungut, das kostet ordentlich Schweiß und gibt Schwielen an den Händen.

Was muss man eigentlich machen, bevor man ein Neural-Network für einen Use-Case implementieren kann? Eigentlich das ganze Data-Science-Wohlfühlpaket. Daten bekommen, Daten analysieren, Daten bereinigen, Daten aufbereiten, Daten kodieren. Das liegt in der Natur der Neural-Networks.

Bildlich gesprochen, sind Neural-Networks riesige Kästen in denen Zahlen sind. Man gibt oben andere Zahlen rein. Unten fallen neue Zahlen raus. Unterwegs passiert die sogenannte Forward-Propagation. Dort werden die Zahlen transformiert. Ein Requirement ist sofort klar: Unsere Daten müssen in ein Zahlenformat gebracht werden. Und unsaubere Zahlen sind nutzlos für Neural-Networks.

Es ist immer gut, ein ordentliches Gefühl für die Daten zu bekommen. Wie umfangreich sind die Daten? Welche Kennzahlen gibt es? Wie ist die Qualität? Data-Analysis. Zurück zum Anwendungsfall des Dichter-Klassifikators. Wir haben es mit Texten zu tun. Natural-Language-Processing. Dafür brauchen wir Spezial-Tools. Die gibt es frei verfügbar.

Wir benutzen spaCy.

spaCy. Das ist „Industrial-Strength Natural Language Processing“. Oder anders gesagt: Ein Blumenstrauß neuronaler Modelle für Tagging, Parsing und Entity-Recognition. Damit können wir ordentlich über unsere Texte bügeln und sie analysieren. spaCy kann viel mehr als wir brauchen. Das ist gut. Vielleicht brauchen wir später mehr. Heute interessiert uns nur der Tokenizer. Der zerlegt Texte in einzelne Komponenten. Tokens. Worte. Ein kleiner Blick in die Zukunft: Später brauchen wir wieder die Word-Embeddings. Die gibt es auch in spaCy.

Seit einer Weile beherrscht spaCy Deutsch. Das Projekt wohnt schließlich in Berlin. Das ist eine gute Motivation und ein Gewinn für uns. Da will man mal nicht so sein.

Wir fangen mit ein wenig Code an. Der Einstieg:

import os
import glob
import seaborn as sns
import matplotlib.pyplot as plt
import shutil
from pprint import pprint
import json
from wordcloud import WordCloud
import spacy
from nltk.corpus import stopwords
import string
from collections import Counter

print("Loading spacy-model...")
spacy_german = spacy.load('de')
german_stopwords = stopwords.words("german")
punctuations = string.punctuation

Hier machen wir drei Dinge. Wir laden das spaCy-Model für die deutsche Sprache. Danach besorgen wir uns die Stopwords. Die brauchen wir später zum Verschlanken des Textkorpus‘. Und wir schnappen uns die Punctuations. Wir wollen ja nicht mit Satzzeichen trainieren. Auch das ist Data-Analysis.

Wir gehen das ganze Data-Set durch und sammeln statistische Informationen. Data-Analysis in einer großen Schleife.

Jetzt haben wir die Werkzeuge in der Hand und legen los. Was haben wir vor? Wir erinnern uns. Unser Korpus liegt auf der Festplatte in einem Ordner:

Der Korpus auf der Festplatte.

Jeder Dichter hat einen Unterordner. Und jeder Unterordner enthält die Texte, jeweils als eine txt-Datei. Diese Unterordner gehen wir durch und sammeln Daten über die einzelnen Autoren. Und am Ende machen wir eine Gesamtstatistik. Legen wir also los:

def main(args=None):
    corpus_path = "corpus"
    analysis_path = "analysis"
    analyze_corpus(corpus_path, analysis_path)


def analyze_corpus(corpus_path="corpus", analysis_path="analysis"):
    print("Analyzing corpus...")

    # Make sure that there is a proper folder for analysis.
    if os.path.exists(analysis_path) and remove_analysis is True:
        shutil.rmtree(analysis_path)
    if not os.path.exists(analysis_path):
        os.makedirs(analysis_path)

    # Get all authors from the corpus and process them individually.
    authors = [element for element in os.listdir(corpus_path) if not os.path.isfile(os.path.join(corpus_path, element))]
    counts = []
    for author in authors:
        # Process an author. Get full string, individual tokens and token count.
        all_text, all_tokens, all_count = process_author(corpus_path, author)

        # Render the word cloud.
        word_cloud_name = author + ".wordcloud.png"
        word_cloud_path = os.path.join(analysis_path, word_cloud_name)
        render_word_cloud(all_text, word_cloud_path)

        # Render the word distribution.
        most_frequent_words_name = author + ".most_frequent_words.png"
        most_frequent_words_path = os.path.join(analysis_path, most_frequent_words_name)
        render_most_frequent_words(author, all_tokens, most_frequent_words_path)

        # Overall count.
        counts.append(all_count)

    # Render corpus distribution.
    corpus_distribution_name = "corpus_distribution.png"
    corpus_distribution_path = os.path.join(analysis_path, corpus_distribution_name)
    render_corpus_distribution(authors, counts, corpus_distribution_path)

Das ist eine verdammt große Schleife. Da sie aber noch auf einen Screen passt, dürfte sie Clean-Code sein. Wahre Magie liegt in den Methoden process_author, render_word_cloud, render_most_frequent_words und render_corpus_distribution. Time to explain!

Kleiner Exkurs. Wir arbeiten mit Tokens. Das sind atomare Textbausteine.

spaCy hat einen Tokenizer eingebaut. Das ist ein feines Stück Software-Ingenieurskunst. Wir Menschen haben keine Probleme damit, Texte in ihre atomaren Textbausteine zu zerlegen. Computer haben es schwieriger. Wer jemals was über Compilerbau gelernt hat, weiß was ich meine.

Trotzdem! Ein kleines Beispiel hat schon so manche Neugier befriedigt. Hier ist ein Quelltext, der den Tokenizer anspricht:

import spacy
 
spacy_german = spacy.load('de')
doc = spacy_german("Namaste. Ich bin Tristan. Euer AI-Guru.")
all_tokens = [token.text for token in doc]
print(all_tokens)

In Zeile vier wird der Text zerlegt. Es fällt unten ein verarbeitetes Dokument raus. Hier kann man über die Tokens iterieren. Ein Token hat eine Text-Information. Die sehen wir gleich. Dass ein Token noch mehr Daten hat, verrate ich nur nebenbei. Damit kann man noch viel machen. Doch wie sehen die Tokens aus? So:

['Namaste', '.', 'Ich', 'bin', 'Tristan', '.', 'Euer', 'AI-Guru', '.']

Das war einfach. Eine kleine Bemerkung am Rande. Eher ein Reminder… Es gibt eine hohe Kunst der Data-Science. Damit meine ich das Aufbereiten. Abhängig vom Use-Case werden die Tokens noch transformiert. In manchen Fällen werden sogar Tokens aussortiert und überhaupt nicht benutzt. Das sei aber nur erwähnt. Wir müssen jetzt die Autoren durchgehen.

Zurück zu den Autoren. Die Tokens-Pipeline.

Welche Daten sammeln wir pro Autor? Die wesentlichsten. Wir besorgen uns alle Tokens und deren Zahl. Das macht eine Aussage darüber, welche Wörter wie häufig benutzt werden. Das geht in Richtung Stilistik. Unterschiedliche Dichter haben unterschiedliche Stile. Das ist unter anderem das Vokabular und das Wie der Wort-Aneinandereihung.

Uns interessieren definitiv alle Tokens. Vor der Bereinigung schauen wir uns an, was wir mit einzelnen Autoren im Quelltext machen:

def process_author(corpus_path, author):
    all_text = ""
    all_tokens = []
    all_count = 0

    # Get all documents from author and get data.
    glob_path = os.path.join(corpus_path, author, "*.body.txt")
    body_file_paths = glob.glob(glob_path)
    for body_file_path in body_file_paths:
        print(body_file_path)
        with open(body_file_path) as body_file:
            body = body_file.read()
            text, tokens = clean_up_text(body)
            all_text += text
            all_tokens.extend(tokens)
            all_count += len(tokens)

    return all_text, all_tokens, all_count

Relativ einfach. Relativ. Jeder Autor hat einen eigenen Unterordner. In dem suchen wir alle Text-Dateien. Die hat uns ja der Downloader aus dem vorigen Blog-Eintrag so schön platziert. Aus jeder Datei extrahieren wir den Gesamt-Text (für später) und alle Tokens (sowieso für später). Und weil wir so frei sind, nehmen wir gleich die Token-Anzahl mit (später!).

Sehr interessant ist hier die Methode clean_up_text. Sie wurde schon mehrfach angekündigt. Quelltext-Prinzessin. Damit zerlegen wir die einzelnen Texte in Tokens und bereinigen sie. Denn es gilt unser Mantra: Die Qualität einer Artificial-Intelligence ist immer stark abhängig von der Trainingsmenge. Diese sollte idealerweise von hoher Qualität und in ihrer Größe optimiert sein.

Unsere Texte wirken schmutzig. Da müssen wir was tun.

Ja. „Schmutzig“ ist übertrieben. Mit Absicht. Oben ist uns schon aufgefallen, dass Satzzeichen – der Brite nennt diese „Punctuations“ – zu den Tokens zählen. Satzzeichen sind toll. Sie sind aber für uns Computerlinguisten maximal überflüssig. Wir setzen sie vor die Tür. Und es gibt noch ein paar Wörter, die rausgeworfen werden: Stopwords. Das sind Wörter, die in Textkorpora so oft vorkommen, dass sie eigentlich keinen Beitrag zur Semantik leisten. Und wir brauchen auch die Pronomen nicht. Die können auch gehen. Wir kümmern uns drum:

def clean_up_text(text):
    doc = spacy_german(text)

    # Go to lower-case.
    tokens = [tok.lemma_.lower().strip() for tok in doc if tok.lemma_ != '-PRON-']

    # Remove stopwords.
    tokens = [tok for tok in tokens if tok not in german_stopwords and tok not in punctuations]

    text = ' '.join(tokens)
    return text, tokens

Kleine Methode, große Wirkung. Zuerst wird der Text in seine Tokens zerlegt. Wie immer setzen wir sie in Lowercase – das spart wieder ein wenig. Und wir lassen alles gehen, was ein Pronomen, ein Stopword oder ein Satzzeichen ist. Das Bereinigte geben wir dann einfach zurück.

Ein kurzes Beispiel? Aber gern! Hier ist der Code:

text, tokens = clean_up_text("Namaste. Ich bin Tristan. Euer AI-Guru.")
print(tokens)

Das liefert:

['namaste', 'tristan', 'ai-guru']

Nice! Das reicht eigentlich schon. Damit können wir uns an die Analyse selbst setzen. Wir haben pro Autor die Texte auf Token-Basis aufbereitet. Well done. Jetzt machen wir das schön bunt. Wirklich!

Word-Clouds sind angenehm für die Augen. Außerdem sind sie aussagekräftig.

Wordclouds oder Tagclouds visualisieren Wörter und ihre Relevanz. Die Relevanz ist die Häufigkeit. Man sieht auf den ersten Blick, welche Wörter wie wichtig sind. Der Mensch ist doch ein Augentier.

Python-like gibt es dafür schon einen passenden Import. Den müssen wir nur anwerfen. Wir nehmen dafür den Gesamttext eines Autors in die Hand und geben ihn in die Wolke. Und zwar so:

def render_word_cloud(all_text, word_cloud_path):
    wordcloud = WordCloud(width=800, height=500,
                      random_state=21, max_font_size=110).generate(all_text)
    figure = plt.figure(figsize=(15, 12))
    plt.imshow(wordcloud, interpolation="bilinear")
    plt.axis('off')
    figure.savefig(word_cloud_path)
    print("Written word-cloud to", word_cloud_path)

Aus unserem Korpus werden mehrere Wolken generiert. Hier ist die vom Dichterfürst Goethe:
Wordcloud von Goethe.

Sicher. Wordclouds sind nur eine Möglichkeit, eine visuelle Aussage über Texte zu machen. Der eingefleischte Statistiker wünscht sich an dieser Stelle Balkendiagramme. Zu Befehl!

Ohne Balkendiagramme gehen wir heute nicht auseinander. Data-Analysis traditionell.

Mit matplotlib arbeitet jeder. Zum Rendern von Diagrammen ist dieses Modul eines der stärksten. Wir bedienen uns und malen Balkendiagramme. Wie bei den Wordclouds arbeiten wir pro Autor mit dem Gesamttext. Nur dieses Mal mit seinen Tokens. Um sie zu zählen, benutzen wir einen Counter. Den gibt es schon als importierbares Modul:

def render_most_frequent_words(author, tokens, most_frequent_words_path):
    counts = Counter(tokens)

    common_words = [word[0] for word in counts.most_common(25)]
    common_counts = [word[1] for word in counts.most_common(25)]

    figure = plt.figure(figsize=(15, 12))
    sns.barplot(x=common_words, y=common_counts)
    plt.title("Most Common Words used by " + author)
    figure.savefig(most_frequent_words_path)
    print("Written most frequent words to", most_frequent_words_path)

Der Counter ermittelt pro Token seine Anzahl im Korpus. Damit können wir direkt ein hübsches Bild machen. So sieht es für Goethe aus:
Wortverteilung von Goethe.

Wir sind auf der Zielgeraden. Es ist fast geschafft. Jetzt müssen wir nur noch die Dichter in Relation setzen.

Der große, statistische Showdown der Data-Analysis steht an. Wir vergleichen die Autoren.

Deine Augen sind müde. Meine Finger sind es auch. Trotzdem reißen wir uns zusammen und kümmern uns um den Vergleich. Mit Code:

def render_corpus_distribution(authors, counts, corpus_distribution_path):
    sns_plot = sns.barplot(x=authors, y=counts)
    sns_plot.get_figure().savefig(corpus_distribution_path)
    print("Written corpus-distribution to", corpus_distribution_path)

Diese Methode empfängt die Namen der Autoren und ihre individuellen Wortanzahlen. Damit können wir erkennen, wie der Gesamtkorpus gewichtet ist. Das Bild ist dieses:
Wortverteilungen der einzelnen Autoren.

And the winner is: Goethe! Für mich war von Anfang an klar, dass der größte der vier Dichter gewinnt. Was sagt uns das Bild? Jedes Neural-Network, dass mit diesem Textkorpus trainiert wird, wird unausgewogen sein. Wer viel zu viel Goethe liest, wird überall Goethe wieder erkennen. Bias. Das wegzupuffern ist eine andere Aufgabe.

Summary.

Der Weg zu einem Neural-Network in Produktion fängt immer mit einem Datensatz an. Den haben wir im letzten Blog-Eintrag runtergeladen. Heute haben wir ihn analysiert. Das ist ein typischer Schritt im Deep-Learning.

Warum machen wir man das? Ganz klar. Wir brauchen eine Gefühl für Quantität, Qualität und Zusammensetzung unserer Daten. Eine schlechte Qualität wird im Training unweigerlich ein schlechtes Modell erzeugen. Und das will niemand. Besonders wir nicht.

Eine kleine Vorschau sei schon erlaubt. Als nächstes werden wir uns an das Training machen! Bis dahin bitte noch ein wenig Geduld.

Und wie immer ist der ganze Quelltext bei Github. Vielen Dank fürs Lesen!

Hinterlasse einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.