<jupyter_start><jupyter_text>Text als Sprache Als *Natural Language Processing* bezeichnet man die computergestützte Verarbeitung von natürlichen Sprachen, also von von Menschen gesprochenen Sprachen im Gegensatz zu Programmiersprachen. Computer sind gut darin, Programmiersprachen mit ihren starren Regeln zu verstehen, aber menschliche Sprachen mit ihren Unregelmäßigkeiten stellen immer noch eine Herausforderung dar. Die Wissenschaft, die sich mit diesen Problemen beschäftigt, ist die Computerlinguistik.Methoden der Computerlinguistik sind mittlerweile für eine Vielzahl von Anwendungsbereichen zentral. So sind etwa Suchmaschinen darauf trainiert, nicht nur den exakten Text einer Suchanfrage zu finden, sondern auch andere Flexionsformen. Eine Google-Suche nach „Parties in Hamburg“ findet so z.B. auch die Seite [Nachtleben & Party – hamburg.de](http://www.hamburg.de/nachtleben-party/). Dazu muss die Suchmaschine wissen, dass »Party« der Singular von »Parties« ist, und dass »in« als Präposition für den gesuchten Inhalt nicht zwingend relevant ist.Für die Analyse von Texten sind insbesondere zwei Verfahren interessant: *Generalisierung* und *Selektion*.**Generalisierung**Mit Generalisierung ist die Abbildung von verschiedenen Varianten auf eine gemeinsame, abstraktere Form gemeint. Im obigen Beispiel als die Abbildung der beiden unterschiedlichen Wortformen »Party« und »Parties« auf eine gemeinsame Grundform »Party«. Ziel der Generalisierung in der Textverarbeitung ist es, semantisch gleiche (oder zumindest sehr ähnliche) Einheiten auch dann zu identifizieren, wenn sie in unterschiedlichen Formen auftauchen.Dies kann nicht nur Wortformen betreffen, sondern z.B. auch die Bezeichnungen von Personen. In einem Text kann etwa wechselnd von »<NAME>«, »<NAME>«, »<NAME>« oder auch nur »die Bundeskanzlerin« die Rede sein.**Selektion**Mit Selektion ist gemeint, für die Analyse relevante Informationen aus der Gesamtmenge an Informationen zu extrahieren. Im obigen Beispiel wäre dies, »in« für die Suche zu ignorieren. Je nach Analsestrategie kann es sehr unterschiedlich sein, welche Informationen relevant sind und welche nicht. Der Ausgangspunkt für NLP ist dabei fast immer der reine Text, ohne Formatierung oder ähnliches. Als Beispiel soll der Anfang einer Rede der deutschen Bundeskanzlerin <NAME> dienen.<jupyter_code>import os

filepath = os.path.join('..', 'Daten', 'Rede_Jugend_forscht.txt')

with open(filepath) as infile:
    rede = infile.read()
print(rede[0:200])<jupyter_output><NAME>,
<NAME>, <NAME>,
meine Damen und Herren,
aber besonders: liebe Preisträgerinnen und Preisträger von „Jugend forscht“,

ich heiße Sie alle ganz herzlich im Bundeskanzleramt<jupyter_text>Wenn wir mit Texten arbeiten, sind wir meist aber nicht am Text als Ganzem interessiert, sondern an den Wörtern, aus denen er besteht. Wörter sind in den europäischen Sprachen meist durch Leerzeichen getrennt. So sehen die ersten Wörter nach der Anrede aus aus, wenn man den Text entsprechend aufspaltet:<jupyter_code>sample = rede[148:254]

print(' -- '.join(sample.split()))<jupyter_output>ich -- heiße -- Sie -- alle -- ganz -- herzlich -- im -- Bundeskanzleramt -- willkommen. -- Dieses -- Jahr -- ist -- ein -- ganz -- besonderes -- Jahr:<jupyter_text>Hier sieht man schon einige Probleme. Insbesondere werden hier die Satzzeichen noch beibehalten. Die verfälschen aber das Ergebnis, denn das Wort heißt ja nicht „willkommen**.**“, sondern einfach „willkommen“. Schon bei so einfachen Aufgaben wie dem Aufspalten kann es also nützlich sein, etwas ausgefeiltere Werkzeuge heranzuziehen. Einen einfachen Einstieg bietet dabei das Modul `TextBlob`, für das es auch eine auf die deutsche Sprache ausgelegte Version gibt:<jupyter_code>from textblob_de import TextBlobDE as TextBlob
blob = TextBlob(sample)
print(' -- '.join(blob.words))<jupyter_output>ich -- heiße -- Sie -- alle -- ganz -- herzlich -- im -- Bundeskanzleramt -- willkommen -- Dieses -- Jahr -- ist -- ein -- ganz -- besonderes -- Jahr<jupyter_text>Hier wird der Text mit einem geeigneteren Algorithmus, der auch Satzzeichen berücksichtigt, in Wörter aufgespalten.`TextBlob` stellt noch eine Reihe weiterer Methoden aus der Sprachverarbeitung bereit. So kann man etwa Worte auf ihre Grundformen zurückführen. Gerade bei stark flektierenden Sprachen wie dem Deutschen ist das oft nützlich. Diesen Schritt nennt man in der Computerlinguistik Lemmatisierung.<jupyter_code>base_forms = blob.words.lemmatize()

print(' -- '.join(base_forms))<jupyter_output>ich -- heeissen -- Sie -- all -- ganz -- herzlich -- im -- Bundeskanzleramt -- willkomm -- Dies -- Jahr -- sein -- ein -- ganz -- besonder -- Jahr<jupyter_text>Man kann sehen, dass der Schritt vergleichsweise fehleranfällig ist. Relevant für die Analyse, wie häufig ein bestimmtes Wort (und nicht eine bestimmte, flektierte Wortform) auftaucht, ist aber, dass eine gemeinsame Form gefunden wird, etwa »all« für »alle, alles, allen …« oder »sein« für »ist, sind, waren, …«.Insgesamt ist der Lemmatisierungsalgorithmus von `TextBlob` im vergleich zu anderen Werkzeugen eher schwach. Zur Demonstration und aufgrund der einfachen Handhabbarkeit reicht er hier aus. In Anwendungsfällen, in denen eine hohe Qualität der Verarbeitung wichtig ist, muss man aber ggf. nach besseren Werkzeugen suchen.Für weitere Analysen kann man auch die Grammatik eines Texts analysieren. Ein grundlegender Schritt ist dabei die Bestimming der Wortart (Substantiv, Verb, etc.), englisch »Part of Speech«. Hierfür werden in der Linguistik meist bestimmte Kürzel verwendet, die von `TextBlob` sind auf [dieser Seite](http://www.clips.ua.ac.be/pages/mbsp-tags) aufgelistet.<jupyter_code>print(' -- '.join(['{}/{}'.format(word, tag) for word, tag in blob.pos_tags]))<jupyter_output>ich/PRP -- heiße/VB -- Sie/PRP -- alle/DT -- ganz/RB -- herzlich/JJ -- im/IN -- Bundeskanzleramt/NN -- willkommen/JJ -- Dieses/DT -- Jahr/NN -- ist/VB -- ein/DT -- ganz/RB -- besonderes/JJ -- Jahr/NN<jupyter_text>Dies kann etwa dazu verwendet werden, für bestimmte Anwendungen nur bestimmte Wortarten zu berücksichtigen. Falls etwa nur die Substantive interessieren, lassen sie sich aufgrund der PoS-Information herausfiltern.Um Informationen wie Lemma und Wortart nicht einzeln erzeugen zu müssen, kann man diese mit der `parse()`-Funktion auch in einem Durchlauf erzeugen. Da der Standard-Parser keine Lemmata erzeugt, müssen diese zunächst explizit aktiviert werden.<jupyter_code>from textblob_de import PatternParser
lemma_parser = PatternParser(lemmata=True)
blob = TextBlob(sample, parser=lemma_parser)
parse = blob.parse()
parse<jupyter_output><empty_output><jupyter_text>Diese Form ist auf Anhieb nicht sehr gut lesbar. Es gibt auch eine tabellarische Darstellung, die mit dem Parameter `pprint` aktiviert werden kann:<jupyter_code>blob2 = TextBlob(sample, parser=PatternParser(lemmata=True, pprint=True))
blob2.parse()<jupyter_output>WORD   TAG    CHUNK    ROLE   ID     PNP    LEMMA              
                                                                           
             ich   PRP    NP       -      -      -      ich                
           heiße   VB     VP       -      -      -      heeißen            
             Sie   PRP    NP       -      -      -      sie                
            alle   DT     -        -      -      -      all                
            ganz   RB     ADJP     -      -      -      ganz               
        herzlich   JJ     ADJP ^   -      -      -      herzlich           
              im   IN     PP       -      -      PNP    im                 
Bundeskanzleramt   NN     NP       -      -      PNP    bundeskanzleramt   
      willkommen   JJ     ADJP     -      -      -      willkomm           
               .   .      -        -      -      -      .                  
          Dieses   DT     NP       -      -      -      dies               
            [...]<jupyter_text>Für die Weiterverarbeitung ist es hilfreich, für jedes Wort auf Informationen wie Wortart, Lemma etc. zugreifen zu können. Dafür kann die erste Form relativ leicht zerlegt werden. Dort werden alle Informationen zu einem Wort durch Schrägstriche getrennt angegeben. Dies ist eine übliche Konvention in der Linguistik. Die Bedeutung der einzelnen Elemente kann man sich anzeigen lassen:<jupyter_code>parse.tags<jupyter_output><empty_output><jupyter_text>Um die Informationen für jedes Wort (*token*) leicht zugänglich zu machen, kann man in Python die Klasse `namedtuple()` nutzen, mit der man einzelne Felder in einer Liste (nichts anderes ist ein sogenanntes „tuple“ im Grunde) benennen kann. Die Idee ist hierbei, statt `token[4]` für das fünfte Tag zu einem Wort einfach `token.lemma` schreiben zu können.Da die Namen von Eigenschaften in Python kein '-' enthalten dürfen, wird es zunächst durch einen Unterstrich ersetzt. Auf der Grundlage können wir eine neue Datenstruktur (Klasse) erzeugen, die einen einfachen Zugriff auf die einzelnen Informationen erlaubt.<jupyter_code>from collections import namedtuple
fieldnames = ['word', 'part_of_speech', 'chunk', 'preposition', 'lemma']
Token = namedtuple('Token', fieldnames)

# Beispiel: „vor“
Token('vor', 'IN', 'B-PP', 'B-PNP', 'vor')<jupyter_output><empty_output><jupyter_text>Ein kleiner Python-Exkurs: Der Klasse `Token` müssen die Informationen als einzelne Argumente übergeben werden. Das ist dann ein Problem, wenn die Informationen als Liste in einer einzelnen Variable vorliegen.<jupyter_code>fields = ['vor', 'IN', 'B-PP', 'B-PNP', 'vor']
Token(fields)<jupyter_output><empty_output><jupyter_text>Die Klasse hätte fünf Argumente erwartet, hat aber nur eines, nämlich die Liste mit fünf Elementen, erhalten. (Ein Argument wird immer intern verwendet, daher spricht die Fehlermeldung von 6 und 2 statt von 5 und 1.) Mit `*` lassen sich aber Listen statt einzelner Argumente übergeben.<jupyter_code>Token(*fields)<jupyter_output><empty_output><jupyter_text>Dies kann man sich nun zunutze machen, um aus dem »Parse« des Textes eine besser zu verarbeitende Datenstruktur zu gewinnen. Da einzelne Worte durch Leerzeichen und die einzelnen Informationen pro Wort durch Schrägstriche getrennt sind, muss der Parse nur zweimal aufgespalten werden. Die dadurch gewonnenen Einzelinformationen pro Wort werden in der Token-Klasse gespeichert.<jupyter_code>tokens = parse.split(' ')
tokens[0:5]<jupyter_output><empty_output><jupyter_text>Im zweiten Schritt werden die Einzelinformationen der Tokens erneut aufgespalten.Damit immer genau die 5 Informationsfelder aufgespalten werden, aber nicht versehentlich mehr, wird der Parameter `maxsplit` übergeben. Dies ist nur in Randfällen, wie z.B. »2012/13« relevant.<jupyter_code>tokens = [Token(*token.split('/', maxsplit=4)) for token in tokens]
tokens[0:5]<jupyter_output><empty_output><jupyter_text>Relevant wird diese Informationen nun, wenn man sie für die weitere Verarbeitung nutzt. Aufgrund dieser Information kann man die oben angesprochenen Verfahren *Generalisierung* und *Selektion* umsetzen: Als Generalisierung dient hier die Lemmatisierung, die Selektion erfolgt auf Basis der Wortarten.Eine Funktion kann so einen Text nehmen, in Wörter zerlegen, diese auf ihre Grundform zurückführen, und dann nach Wortart filtern.<jupyter_code>def lemmatize_and_filter(text, tags):
    """
    Tokenisierung, lemmatisiert und filtert einen Text.
    
    Der erste Parameter `text` ist dabei ein unverarbeiteter *string*.
    
    Der zweite Paramter `tags` ist eine Liste von Part-of-Speech-Tags, die in der Ausgabe
    berücksichtigt werden sollen. Beispiel: ['NN', 'VB', 'JJ']
    
    """
    # Tokenisierung
    blob = TextBlob(text, parser=lemma_parser)
    parse = blob.parse()
    tokens = [Token(*token.split('/', maxsplit=4)) for token in parse.split(' ')]
    # Generalisierung und Selektion
    result = []
    for token in tokens:
        pos = token.part_of_speech[0:2]
        # Filtern
        if pos in tags:
            if pos == 'NN':
                # Substantive immer groß schreiben
                result.append(token.lemma.title())
            else:
                restult.append(token.lemma)
    return result
print(' -- '.join(lemmatize_and_filter(rede, ['NN'])[0:20]))<jupyter_output>Herr -- Kock -- Kollegin -- Frau -- Wanka -- Damen -- Herren -- Preisträgerinnen -- Preisträger -- Forscht -- Bundeskanzleramt -- Jahr -- Jahr -- Preis -- – -- Ihrem -- Leben -- Rolle -- Gespielt -- –<jupyter_text>Das Verfahren funktioniert in weiten Teilen, aber alle Methoden der Computerlinguistik sind mit einer gewissen Fehlerrate behaftet. Gute Verfahren haben dabei eine Genauigkeit von über 90% – was in der Gesamtschau immer noch eine Menge Fehler sind. In diesem Fall sieht man, dass der Wortfinde-Algorithmus nicht mit Gedankenstrichen umgehen kann. Hier würde eine entsprechende Vorbereitung des Textes helfen.Ein anderer Ansatz kann sein, nicht über die Wortarten, sondern über eine vorgegebene Liste an Wörtern zu filtern. Je nach Anwendungszweck kann eine solche Stoppwortliste länger oder kürzer sein. Ihr Ziel ist es, nicht bedeutungstragende Wörter herauszufiltern. Dies können neben Partikeln auch Hilfsverben und unspezifische Adjektive und Adverben (z.B. „sehr“) sein.<jupyter_code>path = os.path.join('..', 'Daten', 'stopwords.txt')
with open(path) as stopwordfile:
    stopwords_de = stopwordfile.read().splitlines()

from string import punctuation  # !"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~
punctuation += '»«›‹„“”‚‘’–'  # additional quotation marks

def lemmatize_and_filter2(text, stopwords):
    blob = TextBlob(text, parser=lemma_parser)
    parse = blob.parse()
    tokens = [Token(*token.split('/', maxsplit=4)) for token in parse.split(' ')]
    result = []
    for token in tokens:
        pos = token.part_of_speech[0:2]
        word = token.word
        if not word.lower() in stopwords and not word.isdigit() and not word in punctuation:
            if pos == 'NN':
                result.append(token.lemma.title())
            else:
                result.append(token.lemma)
    return result

print(' -- '.join(lemmatize_and_filter2(rede, stopwords_de)[0:20]))<jupyter_output>lieb -- Herr -- Kock -- lieb -- Kollegin -- Wanka -- Damen -- Herren -- lieb -- Preisträgerinnen -- Preisträger -- „jugend -- Forscht -- heeißen -- herzlich -- Bundeskanzleramt -- willkomm -- Jahr -- besonder -- Jahr
{'fraction_non_alphanumeric': 0.08397743889701274, 'fraction_numerical': 0.003133486526007938, 'mean_word_length': 3.5884984025559103, 'pattern_counts': {'":': 0, '<': 61, '<?xml version=': 0, '>': 61, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 11, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '15', 'dir': 'jupyter-structured-clean-dedup', 'id': '497611', 'license': 'CC-BY-4.0', 'n_tokens_mistral': 4543, 'n_tokens_neox': 4650, 'n_words': 1575, 'path': '03_Sprache/Text als Sprache.ipynb', 'repo_name': 'frederik-elwert/UniLu_HS2015_Textanalyse', 'size': '14418'}