Wir klassifizieren Dichter: Textkorpus-Download

Namaste! Wir habe heute etwas besonderes vor. Dieser Post ist der erste aus einer kleinen Serie. Ein Anfang. Vor uns liegt ein Dichter-Klassifikator. Was steckt dahinter? Ganz einfach. Ein Neural-Network, das beliebige Texte einschätzt. Klingt ein Text eher nach Johann Wolfgang von Goethe, Gotthold Ephraim Lessing, Friedrich Schiller oder Hermann Hesse? Das Neural-Network weiß es. Und das lernt es aus einem Textkorpus.

Ein solcher Klassifikator ist ein typisches Anwendungsbeispiel für Deep-Learning. Er ist mit wenigen Code-Zeilen implementiert. Eine Sache hat das mit allen oder zumindest den meisten Deep-Learning-Excercises gemeinsam: Der Aufwand ist 80% Data-Science und 20% Deep-Learning.

Welche Schritte haben wir eigentlich?

Deep-Learning in der Praxis fängt immer mit Daten an und endet mit dem Deploy eines Neural-Networks in eine Zielumgebung. Allgemein sind bei beinahe jedem Deep-Learning-Task diese Schritte nötig:

  1. Daten auftreiben: Die Trainingsdaten werden erschlossen. Das kann ein Download sein. Oder ein Abzug aus einer Datenbank. Kann, muss aber nicht.
  2. Datenanalyse: Womit haben wir es eigentlich zu tun? Wie umfangreich sind die Daten? Wie sind sie verteilt? Wie sauber sind sie? All diese Fragen und noch mehr werden in diesem Schritt beantwortet.
  3. Preprocessing: Die Daten liegen vor, können aber in ihrer Rohform nicht zum Training eines Neural-Networks benutzt werden. Sie sind nicht bereinigt, nicht angereichert und nicht fürs Deep-Learning kodiert. Das alles machen wir hier.
  4. Neural-Network-Training: Das ist der geduldige Schritt. Die Netzwerkarchitektur wird samt Hyperparametern definiert. Danach geht das Lernen los. Das kann auch mal ein paar Tage dauern.
  5. Deploy: Das Netz soll als Micro-Service bereitgestellt werden? Es soll vielleicht auf Smartphones ausgeliefert werden? Das übernehmen wir hier.

Zurück zu unserem Projekt. Wir wollen Texte klassifizieren. Typisch Natural-Language-Processing. Das ist neben dem Image-Processing der zweitgrößte Use-Case von Deep-Learning. Wir erinnern uns: Der zugrundeliegende Datensatz heißt im Natural-Language-Processing Textkorpus. Und dieser wird uns nun begleiten.

Project Gutenberg. Hier holen wir uns unseren Textkorpus.

Project Gutenberg hat etwa 54K frei-verfügbare eBooks. Dort kann man schonmal ein paar Stunden verbringen und sich amtlich eindecken. Als Mensch kommt man gut zurecht. Die Sache ist aber komplizierter, wenn man ein Python-Skript ist. Und wir wollen ein Skript haben. Schließlich sollen viele Dokumente automatisch geladen werden. So etwas macht kein Mensch mehr von Hand. Und leider ist die Project-Gutenberg-API ist nicht ganz so prickelnd. Wir rennen also in die ersten Probleme. Das hält uns aber nicht auf.

Zum Glück gibt es die gutenberg-http, eine vereinfachte API für Project Gutenberg. Die nutzen wir gleich. Eigentlich brauchen wir nur zwei Dinge. Erstens die Möglichkeit, alle Werke eines Autors zu suchen. Und zweitens die Möglichkeit einzelne Werke runter zu laden.

Erst die Ids der Texte. Dann die Texte selbst.

Die Text-Ids aller Werke eines Autors zu ermitteln ist eine Suche. Das ist sowas von einfach. So einfach, dass wir das schnell über die Bash ausprobieren:

curl "https://gutenbergapi.org/search/author eq Schiller, Friedrich and language eq de"

Das liefert einige Text-Ids, über die wir einzeln die Texte beziehen werden. Das kann unter Umständen eine lange Liste werden. Bei Schiller ist es grad nich überschaubar:

"texts":[{"text_id":6496},{"text_id":6498},{"text_id":7939},{"text_id":6499},{"text_id":6503},{"text_id":6504},{"text_id":6505},{"text_id":6383},{"text_id":31216},{"text_id":6549},{"text_id":6518},{"text_id":6649},{"text_id":47804},{"text_id":6525}]}%

Jetzt die einzelnen Texte zu bekommen, ist ein Kinderspiel. Ganz RESTful geht das über diese URL:

curl "https://gutenbergapi.org/texts/6496/body"

Das wiederum liefert ein riesiges JSON-Dokument:

{"body":"\r\nThe Project Gutenberg EBook of Die Braut von Messina\r\nby Johann Christoph Friedrich von Schiller...

Ich habe das mal abgekürzt. Den ganzen Text zu lesen ist heute out-of-scope. Das Buch ist eher was für eine kalte Winternacht.

An diesem Punkt beherrschen wir die API ausreichend. Da kann man bestimmt noch mehr machen. Wir können von beliebigen Autoren alle Texte beziehen. Für heute reicht das. Das Ganze müssen wir nur noch in Python umsetzen. Also ein wenig URL-Magie machen und den eingebauten Python-HTTP-Client nutzen. Das ist soweit die Theorie.

Jetzt laden wir den Textkorpus in Python… Zum Glück wird der HTTP-Client mitgeliefert.

Texte zu beziehen soll idealerweise automatisch von statten gehen. Dazu legen wir als erstes die Autoren fest. Denn ihre Texte werden unseren Textkorpus bilden. Dafür definieren wir ein einfaches Array. Mehr Autoren gewünscht? Kein Problem. Die Liste kann ohne Probleme erweitert werden. Legen wir also los:

def main(args=None):
    authors = []
    authors.append("Goethe, Johann Wolfgang von")
    authors.append("Schiller, Friedrich")
    authors.append("Lessing, Gotthold Ephraim")
    authors.append("Hesse, Hermann")
    corpus_path = "corpus"
    download_works_by_authors(authors, corpus_path)

Die Methode download_works_by_authors ist jetzt richtig interessant. Dort laden wir Texte der Autoren über eine Schleife herunter. Besonders ist, dass jeder Autor einen eigenen Ordner auf der Festplatte bekommt. Ein kleines Detail für später… Hier ist die Methode:

def download_works_by_authors(authors, corpus_path="corpus"):
    # Make sure that there is a proper corpus-folder.
    if os.path.exists(corpus_path) and remove_corpus is True:
        shutil.rmtree(corpus_path)
    if not os.path.exists(corpus_path):
        os.makedirs(corpus_path)

    for author in authors:
        print("Downloading all works of author", author)

        # Create a folder for the autor.
        text_collection_path = os.path.join(corpus_path, author)
        if not os.path.exists(text_collection_path):
            os.makedirs(text_collection_path)

        # Getting all text-ids.
        text_ids = get_all_textids_from_author(author)
        print("Loading", len(text_ids), "texts...")

        # Downloading texts.
        for text_id in text_ids:
            download_text_with_id(text_id, text_collection_path)

get_all_textids_from_author macht von urllib Gebrauch. Das ist ein kleiner HTTP-Client mit dem wir uns über die Suche die Text-Ids besorgen:

def get_all_textids_from_author(author):
    # Generate URL for querying all of the author's texts.
    list_of_works_url_string = "author eq " + author + "and language eq de"
    list_of_works_url_string = urllib.parse.quote(list_of_works_url_string)
    list_of_works_url_string = "https://gutenbergapi.org/search/" + list_of_works_url_string
    print("URL", list_of_works_url_string)

    # Get all text-ids.
    with urllib.request.urlopen(list_of_works_url_string) as list_of_works_url:
        json_data = json.loads(list_of_works_url.read().decode())

        # Get text-ids.
        text_ids = jsonpath.jsonpath(json_data, '$..text_id')
        return text_ids

Mit jsonpath ermitteln wir die Text-Ids direkt und lassen die ganze Dekoration weg. Die recht tiefe JSON-Datenstruktur wird zu einem schlichten Array. Danach laden wir die Texte einzeln mit download_text_with_id:

def download_text_with_id(text_id, text_collection_path):
    print("Downloading text with id", text_id)

    # Load the body.
    text_body_url_string = "https://gutenbergapi.org/texts/" + str(text_id) + "/body"
    print("URL", text_body_url_string)
    text_body_file_name = str(text_id) + ".body.txt"
    text_body_file_path = os.path.join(text_collection_path, text_body_file_name)
    print("File", text_body_file_path)
    if not os.path.exists(text_body_file_path):
        with urllib.request.urlopen(text_body_url_string) as text_body_url:
            text_body = json.loads(text_body_url.read().decode())
            text_body = text_body["body"]
            text_body = clean_up_body(text_body)
            with open(text_body_file_path, "w") as text_file:
                text_file.write(text_body)

Diese Methode schreibt brav Text-Dateien auf die Festplatte. Doch was macht hier clean_up_body? Leider ist das Data-Set ein wenig unsauber. Es beinhaltet nutzlose Daten. Um die kümmern wir uns nun. Zielgerade!

Data-Science. Wir müssen das Data-Set bereinigen.

Wenn wir uns Die Braut von Messina ansehen, fällt uns sofort etwas auf (gerne jetzt dem Link folgen). Es gibt einen Header und einen Footer. Jeweils mit Informationen, die nicht direkt zum Text gehören. Etwa Rechtliches und Kleingedrucktes. So wie hier:

The Project Gutenberg EBook of Die Braut von Messina
by Johann Christoph Friedrich von Schiller

Copyright laws are changing all over the world. Be sure to check the
copyright laws for your country before downloading or redistributing
this or any other Project Gutenberg eBook.

This header should be the first thing seen when viewing this Project
Gutenberg file.  Please do not remove it.  Do not change or edit the
header without written permission.

[...]

Natürlich sind solche Infos generell wichtig. Doch wollen wir unser Neural-Network darauf trainieren? Nein. Diese Informationen brauchen wir nicht. Also müssen wir sauber machen. Wie? Wir wollen ja keine Workforce mit dem Clean-Up beauftragen. Deswegen schneiden wir Header und Footer ab. So gut es geht.

Es gibt zwei verschiedene Varianten von überflüssigen Daten. Zumindest habe ich das so gesehen. Wir werden gewisse Regelmäßigkeiten in ihrer Struktur ausnutzen. Denn Anfang und Ende sind gut markiert. Wir suchen diese Marker und schneiden Header und Footer nacheinander raus. Genau so:

def clean_up_body(text_body):
    # Variant 1.
    start_string =  "START OF"
    start_string_index = text_body.find(start_string)
    end_string = "END OF"
    end_string_index = text_body.find(end_string)
    if start_string_index != -1:
        text_body = text_body[start_string_index:end_string_index]
        text_body = text_body[text_body.find("***") + 3:]
        return text_body

    # Variant 2.
    small_print_end_string = "*END*"
    small_print_end_string_index = text_body.rfind(small_print_end_string)
    if small_print_end_string_index != -1:
        text_body = text_body[small_print_end_string_index + len(small_print_end_string):]
        return text_body

    raise Exception("Text could not be cleaned up.")

Ja klar. Ich gebe es zu. Es können noch weitere Varianten von überflüssigen Daten existieren. Daran gemahnt die Exception in der letzten Zeile. Die Implementierung überlasse gerne ich dem geneigten Leser.

Damit sind wir durch. Das Modul, welches den Corpus aus dem Internet bezieht, ist fertig. Der ganze Quelltext ist bei GitHub zu finden. Jetzt heißt es „Ausführen!“:

python download_corpus.py

Der Textkorpus liegt auf der Festplatte. Doch das ist nur der Anfang!

Wir schauen jetzt auf die Festplatte und freuen uns über die Daten. So oder so ähnlich sollte das jetzt aussehen:

Der Textkorpus auf der Festplatte.
So ist der Textkorpus auf der Festplatte abgelegt.

Jeder Autor hat einen eigenen Unterordner. In jedem Unterordner liegen bereinigte Text-Dateien. Das ist bewusst so organisiert. Die Daten liegen klassifiziert vor. Das wird später das Deep-Learning einfacher machen.

Deep-Learning fängt immer mit den Daten an. Wir haben uns entschieden, als Exercise Daten von Project Gutenberg zu holen. Es gibt eine dafür wunderbare API eines Drittanbieters. Wir mussten diese nur ansprechen. Leider waren die Daten ein wenig unsauber. Wir haben uns aber darum gekümmert. Und ich sage: Diesen Tod muss man im Deep-Learning häufiger sterben. Das macht aber nichts.

Wie geht es weiter? Bevor wir in das Preprocessing der Daten einsteigen oder das Training anwerfen machen wir zunächst eine Analyse. Dazu aber später mehr. Es wird weitere Artikel geben. Danke fürs tapfere Lesen!

Hinterlasse einen Kommentar

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