Natural Language Processing

Die computergestützte Verarbeitung natürlicher Sprache ist alles andere als trivial. Die Grundlagen dafür wurden über Jahrzehnte vor allem von der Computerlinguistik geschaffen. Die Sprachverarbeitung findet an vielen Stellen ihren Einsatz. Es können etwas grammatische Strukturen identifiziert und analysiert werden. Ein ganz wichtiger Anwendungsbereich liegt in der semantischen Analyse: der Computer soll Text nicht nur als Aneinanderreihung von Zeichen sehen, sondern den Inhalt eines Textes "verstehen". Ein typischer Anwendungsfall davon ist etwa die Named Entity Recognition, wo es darum geht, z.B. Personen- oder Ortsnennungen in einem Text zu finden.

Ein weiterer Bereich ist die Textklassifizierung, wo wir schon in den Bereich des künstlichen Intelligenz kommen. Eine klassische Anwendung dafür ist etwa die Identifizierung von Spam, die Klassifizierung nach Testsorten oder die Sentiment Analysis, bei der versucht wird, die "Stimmung" eines Textes (z.B. eines Online-Kommentars) zu verstehen.

NLP ist ein relativ komplexer Bereich, in dem statistische und wahrscheinlichkeitstheoritsche Aspekte aber auch Wissensrepräsentationen eine große Rolle spielen. Es gibt eine Reihe von Bibliotheken, die über Jahre für diesen Bereich geschrieben wurden. Für Python gibt es beispielsweise

SpaCy

Spacy steht als externe Bibliothek zur Verfügung. Sie kann einfach via

pip install spacy

installiert werden.

Für jede benötigte Sprache müssen dann noch sprachspezifische Dateien nach installiert werden. Für Deutsch geht das so:

python -m spacy download de

Eine Liste der unterstützten Sprachen finden Sie hier: https://spacy.io/models/

Sobald alles installiert ist, können wir erste Experimente starten.

Zuerst laden wir spacy und das (statistische) Modell für Deutsch:


In [ ]:
import spacy
nlp = spacy.load('de')

Dann laden wir den schon bekannten Roman von Jakob Wassermann:


In [ ]:
with open('../data/wassermann/der_mann_von_vierzig_jahren.txt') as fh:
    doc = nlp(fh.read())

Beim Erzeugen des Spacy-Dokuments wurden bereits allerlei Aktionen durchgeführt. Beispielsweise wurde der Text tokeniziert, also in einzelne Tokens (Wörter, wenn man so will) zerlegt. Die passiert sprachspezifisch und ist daher deutlich besser als unsere bisherigen Versuche mit split().


In [ ]:
for token in doc:
    print(token)

Wir können damit zum Beispiel die Zahl der Tokens zählen:


In [ ]:
sum(1 for token in doc)

Part-of-Speech-Tagging

spaCy speichert für jedes Token eine Reihe weitere Daten, die als Property angesprochen werden können:

  • den Text des Tokens
  • die lemmatisierte Form des Tokens
  • pos_: Part-of-speech-Tag (einfach)
    • ADJ - Adjektiv
    • ADP - Präposition
    • ADV - Adverb
    • AUX - Hilfsverb
    • CONJ - Bindewort
    • DET - Pronomen
    • INTJ - Ausruf
    • NOUN - Nomen
    • NUM - Zahl
    • PART - Partikel
    • PRON - Pronomen
    • PROPN - Eigenname
    • PUNCT - Satzzeichen
    • SCONJ - Untergeordnete Konjunktion
    • SPACE - Leerzeichen
    • VERB - Verb
    • X - Nicht-Wort
  • tag_: Part-of-speech-Tag detailiert (Details hier: https://spacy.io/api/annotation#pos-tagging)
  • dep_: Syntaktische Abhängigkeit (Beziehung zwischen Tokens) Details hier: https://spacy.io/api/annotation#section-dependency-parsing
  • shape_: Wortform: jedes Zeichen des Tokens wird so repräsentiert:
    • X: Großbuchstabe
    • x: Kleinbuchstabe
    • d: Ziffer
    • ...
  • is_alpha_: token is alpha character (d.h. ein normales Wort)
  • is_stop_: token ist Stopword

In [ ]:
# Gibt Token für Token mit POS-Daten aus. 
# Zum nächsten Token gelangen Sie durch Drücken der Enter-Taste
# Abbruch der Schleife durch Eingabe von 'exit' + Enter
for token in doc:
    print(('{}\n\tLemma: {}\n\tPOS: {}\n\tTAG: {}\n\tDEP: {}'
           '\n\tShape: {}\n\tis_alpha: {}\n\tis_stop: {}'.format(
        token.text, token.lemma_, token.pos_, token.tag_, token.dep_, 
               token.shape_, token.is_alpha, token.is_stop)))
    if input() == 'exit':
        break

Named Entities

Named Entity Recognition versucht Entitätsnamen (Personen, Orte, Firmen, Datumsangaben, Telefonnummern, Buchtitel, usw.) im Text zu identifizieren. Der Erfolg solcher Bemühungen hängt stark davon ab, wie gut das jeweilige Modell zur Textsorte (aber z.B. auch zur jeweiligen Zeit passt). Da spaCy vor allem auf zeitgenössische Texte mit einem Fokus auf Online-Text spezialisiert ist, ist das Ergebnis nicht berauschend, zeigt aber die grundlegende Idee. Die Ergebnisse sind rein statistisch und lassen sich trainieren. Auf die Schnelle habe ich leider kein gut trainiertes Modell für etwas antiquiertes literarisches Deutsch gefunden.

Mehr zu Named Entities in spaCy finden Sie hier: https://spacy.io/usage/linguistic-features#named-entities


In [ ]:
# Zeige Orte (LOC) es gibt noch: ORG, PERS, MISC
for ent in doc.ents:
    if ent.text.strip(): #  for some reason we get a lot of empty String Tokens
        if ent.label_ == 'LOC':
            print(ent.text)

Da Entity-Objekte auch die Positionsangaben innerhalb des Textes bereitstellen, kann man Entities bei Bedarf ziemlich leicht im Text taggen. Ich verwende dazu einen übersichtlicheren String:


In [ ]:
text = ('Anton Maier, geboren 1990, wohnt in Wien und mag Bücher. '
        'Seine Frau heißt Anna Huber und interessiert sich für Computer.'
        'Aufgewachsen ist er in Graz und sie in St. Pölten.')
doc = nlp(text)

In [ ]:
# Use custom tags for named entities
TAG_MAP = {
    'LOC': ('<place>', '</place>'),
    'PER': ('<person>', '</person>')
}

# stores the single parts which a joinded to a string in the end
buf = []
last_end = 0 # end position of the last entity found
for ent in doc.ents:
    # we are only interested in the tags defined in TAG_MAP, so if ent.label_
    # has no key in TAG_MAP, the tag will be an empty string
    start_tag, end_tag = TAG_MAP.get(ent.label_, ('', ''))
    # extract substring from end pos of last entity to start off this entity
    buf.append(text[last_end:ent.start_char])
    # add tags and entity string
    buf.append(start_tag + ent.text + end_tag)
    last_end = ent.end_char
# Add the remaining text after the last substring    
buf.append(text[last_end:])
print(''.join(buf))