Einleitung

"Tools" (Handwerkzeug-Collage) Copyright: Bernd Klein

„Tools“ (Handwerkzeug-Collage)
Copyright: Bernd Klein

 

In diesem Beitrag beschäftigen wir uns mit der Verarbeitung von geschriebenen Texten in natürlicher Sprache, also beispielsweise Romanen. Dazu benutzen wir das Python-Modul NLTK. NLTK ist eine Abkürzung für Natural Language Toolkit. Wie der englische Name vermuten lässt, stellt uns dieses Modul Werkzeuge (toolkit), – also z.B. Klassen mit Funktionen und Methoden, – zur Verfügung, mit denen sich Probleme und Aufgabenstellungen der Computerlinguistik lösen lassen. NLTK wurde bereits im Jahr 2001 an der University of Pennsylvania entwickelt und wird von Edward Loper, Steven Bird und Ewan Klein geleitet.

Installation

Das NLTK-Modul ist nicht standardmäßig installiert, d.h. bevor Sie die Beispiele des Blogs praktisch nachvollziehen können, müssen Sie das Modul installieren. Unter Debian, Ubuntu, Mint oder andern Debian/Ubuntu-basierten Systemen ist dies sehr einfach. In einer Shell das folgende Kommando abschicken:

sudo apt-get install python-nltk

Für alle anderen Betriebssysteme inklusive Microsoft Windows folgen Sie bitte den Anweisungen auf der NLTK-Download-Site.
Leider gibt es den NLTK noch nicht in einer offziellen Python3 Version, weshalb dieser Beitrag Python2 voraussetzt!
Nachdem der NLTK installiert wurde, können wir NLTK wie gewohnt in Python importieren.

lingu@babel:~$ python
Python 2.7.3 (default, Apr 10 2013, 05:09:49)
[GCC 4.7.2] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import nltk
>>>

Als erstes müssen wir die NLTK-Daten herunterladen. Zu diesem Zweck rufen wird den NLTK-Downloader nach dem nltk-Import wie folgt auf:

>>> nltk.download()

Danach sollte folgendes Fenster erscheinen:

 

Fenster des NLTK, Screendump

Fenster des NLTK, Screendump

 

 

Für diese Einführung genügt es die Zeile mit dem Identifier „book“ und dem Namen „Everything used in the NLTK Book“ herunterzuladen. Also diese Zeile markieren und „Download“ starten.

Erste Schritte mit dem NLTK

Wenn wir den NLTK installiert und die „book“-Daten heruntergeladen haben, können wir folgenden Import ausführen:

>>> from nltk.book import *
*** Introductory Examples for the NLTK Book ***
Loading text1, ..., text9 and sent1, ..., sent9
Type the name of the text or sentence to view it.
Type: 'texts()' or 'sents()' to list the materials.
text1: Moby Dick by Herman Melville 1851
text2: Sense and Sensibility by Jane Austen 1811
text3: The Book of Genesis
text4: Inaugural Address Corpus
text5: Chat Corpus
text6: Monty Python and the Holy Grail
text7: Wall Street Journal
text8: Personals Corpus
text9: The Man Who Was Thursday by G . K . Chesterton 1908
>>>

Die Ausgabe verrät uns, dass wir verschiedene Bücher und Texte geladen haben. So z.B. den Roman „Moby Dick“ von Herman Melville und, was nicht fehlen darf, das Skript zu „Monty Python and the Holy Grail“. Zu den einzelnen Texten können wir auch weitere Informationen erhalten, wenn wir den entsprechenden Namen des Textes aufrufen:

>>> text1
<Text: Moby Dick by Herman Melville 1851>
>>> text3
<Text: The Book of Genesis>
>>> text6
<Text: Monty Python and the Holy Grail>
>>>

Informationen erhalten

Wir möchten nun Informationen über unsere Texte gewinnen. Eine Frage ist beispielsweise „Wie groß ist unser Text“. Größe drückt sich natürlich vor allem in der Anzahl der Wörter aus:

>>> len(text1)
260819
>>>

Das bedeutet, dass „Mobby Dick“ aus 260819 Wörtern besteht. Allerdings verstecken sich bei den „Wörtern“ auch Satzzeichen, wie „!“, „,“, „.“ und so weiter. Dabei handelt es sich aber nicht um verschiedene Wörter, sondern Vorkommen. So enthält der Text beispielsweise fünf Mal das Wort ‚unconsciously‘. Die Anzahl der verschiedenen Wörter erhalten wir mittels „normalem“ Python, also ohne NLTK. Dazu müssen wir lediglich „text1“ in eine Menge mittels der Funktion „set“ wandeln:

>>> words = set(text1)
>>> len(words)
19317
>>> # removing words containing special characters
...
>>> words = [word for word in words if word.isalpha()]
>>> len(words)
19032
>>> words = set(text1)
>>> non_words = [word for word in words if not word.isalpha()]
>>> non_words
['99', '91', '90', '97', 'L1', '1750', ').--', '103', ...]
>>> len(non_words)
285
>>>

 

(Achtung: die drei Punkte „…“ in der obigen Ausgabe von non_words wurden eingefügt für gelöschte Einträge aus non_words)

Lexikalische Diversität

Nun kann man auch ganz einfach die Diversität für Mobby Dick oder die anderen Texte berechnen. Die Diversität ist ein Maß für die Sprachvielfalt. Sie ist definiert als Quotient der „verschiedenen Wörter“ dividiert durch die „Gesamtanzahl von Wörtern“ eines Textes.
Im folgenden berechnen wir die lexikalische Diversität von „Mobby Dick“:

>>> words = list(text1)
>>> words = [word for word in words if word.isalpha()]
>>> diff_words = set(words)
>>> diversity = len(diff_words) / float(len(words))
>>> diversity
0.08715842114663333
>>>

 

Das folgende Programm berechnet die lexikalische Diversität für alle 9 Texte. Wir möchten nicht weiter auf das Programm eingehen, da das Verständnis dieses Programmes für das Folgende nicht notwendig ist:

from __future__ import division
from nltk.book import *

for t in range(1,10):
    name = "text" + str(t)
    words = list(eval(name))
    # removing words containing or consisting of special characters
    words = [word for word in words if word.isalpha()]

    diff_words = set(words)

    num_words = len(words)
    num_diff_words = len(diff_words)

    diversity = num_diff_words / num_words

    print(name + ":", eval(name))
    print("different words:    {0:8d}".format(num_diff_words))
    print("words:              {0:8d}".format(num_words))
    print("lexical diversity:  {0:8.2f}".format(diversity))

Das Programm liefert die folgende Ausgabe. Wir haben die Ausgaben durch den Import weggelassen:

('text1:', &lt;Text: Moby Dick by Herman Melville 1851&gt;)
different words:       19032
words:                218361
lexical diversity:      0.09
('text2:', &lt;Text: Sense and Sensibility by Jane Austen 1811&gt;)
different words:        6713
words:                120733
lexical diversity:      0.06
('text3:', &lt;Text: The Book of Genesis&gt;)
different words:        2776
words:                 38495
lexical diversity:      0.07
('text4:', &lt;Text: Inaugural Address Corpus&gt;)
different words:        9652
words:                132124
lexical diversity:      0.07
('text5:', &lt;Text: Chat Corpus&gt;)
different words:        5285
words:                 34297
lexical diversity:      0.15
('text6:', &lt;Text: Monty Python and the Holy Grail&gt;)
different words:        2110
words:                 11450
lexical diversity:      0.18
('text7:', &lt;Text: Wall Street Journal&gt;)
different words:       10117
words:                 75185
lexical diversity:      0.13
('text8:', &lt;Text: Personals Corpus&gt;)
different words:         996
words:                  3397
lexical diversity:      0.29
('text9:', &lt;Text: The Man Who Was Thursday by G . K . Chesterton 1908&gt;)
different words:        6758
words:                 587243>
lexical diversity:      0.12

Mit NLTK zum Romancier …

Auch wenn NLTK nicht das hält, was die Überschrift verspricht, so sind die Ergebnisse irgendwie doch beeindruckend. Mit dem NLTK-Modul können wir Zufallstexte im Stil eines vorhandenen Romans oder anderen Textes erzeugen. Da diese nur mit 3-Grammen berechnet werden, also keine semantischen Methoden oder Grammatikchecks einfließen, überzeugt das Ergebnis natürlich nicht durch literarische Brillianz. Einen Zufallstext im Stil der Vorlage erzeugen wir, wenn wir die Methode generate() auf einem Text aufrufen:

>>> text1.generate()
[ Moby Dick !" cried stationary Elijah , that while the dogged crew
eyed askance , and many random inquiries , I shall be my lawyer ,
executor , and more than once did worship , and being now in keen
pursuit of them belonged . When the proper time arrived , and standing
beside the pin , he will frequently offer to view his ship , and
turning towards Stubb , " what all this fuss I have never yet was he
?-- not killed !" cried Ahab , furious with this job -- coming . "
Queequeg ,"
>>> text1.generate(50)
[ Moby Dick had been betrayed . At times there are skeleton
authorities you can possibly be humorously grounded upon some
unearthly reminiscence ;-- even so is the stoneless grave of
Bulkington . Let not the first cry , no man must eventually lower ,
backward sloping part of the
>>>

Wie wir sehen können, lässt sich generate() mit oder ohne Argument aufrufen. Das Argument „length“ steht standardmäßig auf 100, d.h. 100 Wörter.

… oder zum Politiker

Besser klappt generate() übrigens, wenn man die Methode auf die „Inaugural Address“-Reden der amerikanischen Präsidenten ansetzt:

>>> text4.generate()
Building ngram index...
Fellow - Countrymen : This scourge will stop . And sometimes our
differences with civility , courage and patriotism have been called
for a society must rise up united and independent expression of their
continued presence and be unmindful that our own , it is -- the
Indians located within our control . Present excitement will at all ,
the unconquerable valor of our Federal system of government can more
auspiciously commence . By making every citizen , by defending all its
members , but with unequaled alacrity . No mind can comprehend them .
In pleading our just cause

Vielleicht könnten ja deutsche Politikerinnen und Politiker diese Technik zur Vorbereitung Ihrer Reden nutzen? Oder sollten Sie etwa bereits den NLTK seit Jahren benutzen? Einiges spricht dafür 🙂

Als Übung können Sie generate() auf die anderen Texte, wie beispielsweise auf „The Book of Genesis“, anwenden.

Die häufigsten und seltensten Wörter

Mit der Methode FreqDist() können wir die Vorkommenshäufigkeit der Wörter in einem Text ermitteln. Im folgenden berechnen wir die Häufigkeiten für die Genesis:

>>> fdist1 = FreqDist(text3)
>>> frequencies = fdist1.items()
>>> frequencies[:30]
[(u',', 3681), (u'and', 2428), (u'the', 2411), (u'of', 1358), (u'.', 1315),
(u'And', 1250), (u'his', 651), (u'he', 648), (u'to', 611), (u';', 605), 
(u'unto', 590), (u'in', 588), (u'that', 509), (u'I', 484), (u'said', 476),
(u'him', 387), (u'a', 342), (u'my', 325), (u'was', 317), (u'for', 297), 
(u'it', 290), (u'with', 289), (u'me', 282), (u'thou', 272), (u"'", 268), 
(u'is', 267), (u'thy', 267), (u's', 263), (u'thee', 257), (u'be', 254)]
>>> frequencies[-30:]
[(u'wickedly', 1), (u'widowhood', 1), (u'wild', 1), (u'winged', 1), 
(u'winter', 1), (u'wit', 1), (u'withered', 1), (u'withhold', 1), 
(u'wittingly', 1), (u'wiv', 1), (u'wolf', 1), (u'wombs', 1), 
(u'womenservan', 1), (u'wondering', 1), (u'wor', 1), (u'worse', 1), 
(u'worship', 1), (u'worthy', 1), (u'wotteth', 1), (u'wounding', 1), 
(u'wrapped', 1), (u'wrestlings', 1), (u'wrong', 1), (u'wrought', 1), 
(u'y', 1), (u'yearn', 1), (u'yielded', 1), (u'yoke', 1), (u'yonder', 1), (u'younge', 1)]
>>>

Frequencies[:30] gibt uns die 30 häufigsten Wörter zurück und frequencies[-30:] die 30 am seltensten vorkommenden Wörter des Textes. Wollen wir nur die Öwrter als Strings, also ohne die Häufigkeit, können wir sie wie folgt ausgeben:

>>> [str(w[0]) for w in frequencies[:30]]
[',', 'and', 'the', 'of', '.', 'And', 'his', 'he', 'to', ';', 'unto', 'in',
'that', 'I', 'said', 'him', 'a', 'my', 'was', 'for', 'it', 'with', 'me', 
'thou', "'", 'is', 'thy', 's', 'thee', 'be']
>>> [str(w[0]) for w in frequencies[-30:]]
['wickedly', 'widowhood', 'wild', 'winged', 'winter', 'wit', 'withered',
'withhold', 'wittingly', 'wiv', 'wolf', 'wombs', 'womenservan', 'wondering',
'wor', 'worse', 'worship', 'worthy', 'wotteth', 'wounding', 'wrapped',
'wrestlings', 'wrong', 'wrought', 'y', 'yearn', 'yielded', 'yoke', 'yonder',
'younge']
>>>

Wir können uns auch eine kummulative Verteilungsfunktion grafisch anzeigen lassen. Dazu können wir die plot()-Methode auf dem fdist1-Objekt anwenden. Dazu muss jedoch das Modul matplotlib installiert sein!

>>> fdist1.plot(50, cumulative=True)

Damit erhalten wir folgendes Diagram:

Kummulative Verteilungsfunktion

Kummulative Verteilungsfunktion

 

Textklassifikation mit NLTK

Die Aufgabe der Textklassifikation besteht darin, Dokumente (Texte) in Abhängigkeit ihres semantischen Inhaltes einer oder mehreren Kategorien bzw. Klassen zuzuordnen. Die Zuordnung von E-Mail zu Ham (gute E-Mails) und Spam (naja, da brauch‘ man wohl nichts dazu zu sagen) ist beispielsweise so eine Klassifikationsaufgabe. In der Lernphase labelled ein Mensch die E-Mails mit den Labels „ham“ und „spam“. Nach der Lernphase kann das Programm dann selbständig eingehende E-Mails in Ham und Spam aufteilen. Das folgende Diagram zeigt, wie die Lernphase und die Vorhersage-Phase funktionieren:

Lern- und Testphase eines Naive-Bayes-Klassifikators Grafik erzeugt von Bernd Klein, siehe http://www.python-kurs.eu/text_klassifikation_einfuehrung.php

Lern- und Testphase eines Naive-Bayes-Klassifikators Grafik erzeugt von Bernd Klein, siehe http://www.python-kurs.eu/text_klassifikation_einfuehrung.php

 

Man unterscheidet eine Lernphase und eine Klassifikationsphase. Die Lernphase kann man grob in drei Arten unterteilen:

  • Die notwendige Information für das Lernen, also die korrekte Labelung der Dokumente, erfolgt beim überwachten Lernen durch „Eingriff“ eines externen Mechanismus, üblicherweise menschliches Feedback.
  • Unter Halb-überwachtem-Lernen (semi-supervised learning) versteht man ein Mischung aus überwachtem und nicht überwachtem Lernen.

nicht überwachtes Lernen (automatisches Lernen) erfolgt komplett ohne Einwirkung von Außen.

Klassifikation von Vornamen

Im folgenden wollen wir nun einen Klassifikator mittels NLTK trainieren, der dann englische Vornamen in male („männlich“) und female („weiblich“) klassifizieren kann. Woran erkennen wir, ob Namen männlich oder weiblich sind? Charlotte, Amelia, Aria, Sophie und Sophia sind zweifelsfrei weiblich, während Oliver, Henry, Benjamin und Lucas sind genauso eindeutig männlich.

Bisher haben wir alle Beispiele direkt in der interaktiven Python-Shell ausgeführt. Diesmal schreiben wir jedoch ein kleines Modul, was wir dann in der Python-Shell importieren werden.

import nltk
import random

def gender_features(word):
    return {'last_letter': word[-1]}

def classify(name):
    return classifier.classify(gender_features(name))

male_names     = nltk.corpus.names.words('male.txt')
female_names   = nltk.corpus.names.words('female.txt')
labelled_names = ([(name, 'male') for name in male_names] + 
[(name, 'female') for name in female_names])

random.shuffle(labelled_names)

featuresets = [(gender_features(n), g) for (n,g) in labelled_names]
train_set  = featuresets[500:]
test_set   = featuresets[:500]

classifier = nltk.NaiveBayesClassifier.train(train_set)

Mit der Funktion gender_features extrahieren wir die für den Klassifikator nötigen Merkmale. Wie wir sehen, liefert unsere Funktion ein Dictionary zurück. Wir können also auch weitere Merkmale hinzufügen. Nun wollen wir unseren Klassifikator testen.

>>> from names_classifier import *
>>> for name in ["Charlotte", "Amelia", "Benjamin", "Lucas", "Obama"]:
...      print(name, classify(name))
... 
('Charlotte', 'female')
('Amelia', 'female')
('Benjamin', 'male')
('Lucas', 'male')
('Obama', 'female')
>>> print(nltk.classify.accuracy(classifier, test_set))
0.764
>>> classifier.show_most_informative_features(7)
Most Informative Features
             last_letter = 'a'            female : male   =     34.4 : 1.0
             last_letter = 'k'              male : female =     31.2 : 1.0
             last_letter = 'f'              male : female =     15.9 : 1.0
             last_letter = 'p'              male : female =     11.8 : 1.0
             last_letter = 'v'              male : female =     10.5 : 1.0
             last_letter = 'd'              male : female =      9.8 : 1.0
             last_letter = 'm'              male : female =      8.5 : 1.0
>>>

Unsere Präzision beträgt nur 0.764. Auch aus der Tatsache, dass Obama zur Frau gemacht wurde, können wir schließen, dass noch Optimierungspotential besteht.

Wir fügen ein weiteres Merkmal hinzu. Den Suffix der letzten zwei Buchstaben:

def gender_features(word):
    return {'last_letter': word[-1], "last_two": word[-2:]}

Wir können erkennen, dass dann die Präzision (Accuracy) steigt, d.h. unsere Optimierung war – zumindest was unsere zur Verfügung stehende Test- und Lernmenge betrifft – erfolgreich gewesen. Bei den besten sieben Merkmalen taucht nur noch zweimal der letzte Buchstabe auf und erst auf Platz 5 zum ersten Mal.

>>> from names_classifier import *
>>> print(nltk.classify.accuracy(classifier, test_set))
0.782
>>> classifier.show_most_informative_features(7)
Most Informative Features
                last_two = 'na'           female : male   =     97.2 : 1.0
                last_two = 'la'           female : male   =     73.6 : 1.0
                last_two = 'ia'           female : male   =     39.7 : 1.0
                last_two = 'ra'           female : male   =     37.2 : 1.0
             last_letter = 'a'            female : male   =     35.3 : 1.0
             last_letter = 'k'              male : female =     32.4 : 1.0
                last_two = 'rt'             male : female =     31.8 : 1.0
>>>

Allerdings wird Präsident Obama von unserem Klassifikator leider immer noch als eine Frau eingestuft.

>>> classify('Obama')
'female'
>>>

 Weitergehende Lektüre

Bibliothek des Court of Appeal der Provinz Antario in Toronto Copyright: Bernd Klein

Bibliothek des Court of Appeal der Provinz Antario in Toronto
Copyright: Bernd Klein

Der NLTK wurde primär für Lehrzwecke entwickelt und so ist es nicht verwunderlich, dass es zu diesem Modul umfangreiche Dokumentationen gibt. Allerdings nur in englischer Sprache. Zum einen ist das ausgezeichnete Lehrbuch „Natural Language Processing with Python — Analyzing Text with the Natural Language Toolkit“ von Steven Bird, Ewan Klein, and Edward Loper frei im Internet zugänglich. Außerdem gibt es ein weiteres gut aufgebautes Buch von Jacob Perkins mit dem Titel Python Text Processing with NLTK 2.0 Cookbook. Beide Bücher gibt es natürlich auch in qualitativ hochwertigen Druckausgaben!
In Deutsch finden Sie von mir eine allgemeine Einführung in die Text-Klassifikation sowie eine Implementierung der Textklassifikation in Python. Diesem Gebiet habe ich auch ein umfangreiches Kapitel in meinem Buch Einführung in Python3 gewidmet.

Beim Kursveranstalter Bodenseo biete ich auch einen speziellen Kurs zur Computerlinguistik an, in dem ich auch intensiv auf NLTK eingehe: Python, Textverarbeitung, Textklassifikation. Dieser Kurs wird auch in englischer Sprache angeboten: Python Text Processing Course.

 

Ihr

Bernd Klein

Klein, Einführung in Python 3, 2.A.

Mehr zum Thema Python 3 lesen Sie im neuen Buch von Bernd Klein Einführung in Python 3.