Einführung in die maschinelle Sprachverarbeitung mit Python

Visual Studio Code und die Python-Konsole

Overview

Teaching: 30 min
Exercises: 0 min
Questions
  • Was ist eine IDE?

  • Wie kann man Python Code ausführen?

Objectives
  • Starten von VS Code und Öffnen eines Projektordners

  • Umgang mit der Python-Konsole

Visual Studio Code starten

Eine “Integrated Development Environment” (IDE) wie VS Code ist im Grunde ein Texteditor mit vielen Extra-Funktionen, die die Arbeit mit Quelltexten einfacher macht. Ein Python-Programm wird am Anfang typischerweise nur aus einer einzelnen Quelltext-Datei bestehen, aber mit zunehmender Komplexität wird die Funktionalität auf mehrere Quelltext-Dateien verteilt werden. Auch macht es für den Kurs Sinn, die Quelltexte gemeinsam in Ordnern abzuspeichern. Legen Sie z.B. in ihrem Benutzerordner einen Unterordner lehre und darunter einen Unterordner nlp an. Sie können den Ort des Projektordners frei wählen und je nach Betriebssystem befindet sich der Benutzerordner an anderer Stelle.

Starten Sie Visual Studio Code wie in den Setup-Schritten beschrieben. Danach sollten Sie neben dem Willkommensdialog im Zentrum auch links eine Seitenleiste sehen können.

VS Code IDE Oberfläche nach dem Start

In der Seitenleiste gibt es verschiedene Unterbereiche, zwischen denen Sie mit einem Klick auf das jeweilige Icon wechseln können. Als erstes sollten Sie den “Explorer” Unterbereich öffnen, in dem Sie auf das erste Icon oben links klicken. Öffnen sie danach den neu erstellen Projektordner in VS Code, in dem Sie in der Explorer-Seitenleiste “Open Folder” auswählen.

VS Code Open Folder

Alternativ können Sie einen Projektordner auch über den Menüeintrag “File -> Open Folder …” öffnen.

VS Code Open Folder via menu

Im Moment ist der Ordner leer, deswegen werden auch im Explorer keine Dateien angezeigt. Wenn Sie die Maus auf den Bereich mit dem Ordnernamen bewegen, erscheinen zwei kleine Icons mit denen Sie entweder eine neue Datei anlegen (erstes Icon) oder einen Unterordner erstellen können (zweites Icon).

Neue Datei anlegen in VS Code

Erstellen Sie eine Textdatei mit dem Namen Notizen.txt, in dem Sie auf das Symbol für die neue Datei klicken, den Dateinamen eingeben und (die Eingabetaste oder auch “Enter”) drücken.

Anlegen von Notizen.txt

In diese Textdatei können Sie jetzt einen beliebigen Text schreiben. Wie bei anderen Texteditoren auch zeigt VS Code an, dass die Datei geändert wurde. Wenn es ungespeicherte Änderungen gibt, wird oben neben dem Dateinamen ein runder Kreis eingeblendet. Speichern Sie die Änderungen mit dem Tastaturkürzel STRG+S oder den Menüeintrag “File -> Save”. Danach wird er Kreis zu einem Kreuz, mit dem sie das Texteditorfenster schließen können. Mit einem Klick auf den auf die Datei im der Explorer-Seitenleiste können Sie die Datei wieder öffnen. Wenn Sie den Projektordner im Dateimanager Ihres Systems öffnen, sollten Sie die neue Datei Notizen.txt sehen können.

Python-Konsole

Bisher haben wir noch nichts mit VS Code gemacht, das spezifisch für Python wäre. Python-Programme werden über das Programm python3 ausgeführt, das durch Miniconda mit installiert wurde. Um das python3 Programm zu starten, müssen Sie erste einmal ein Terminal öffnen. Sie könnten das System-Terminal starten, aber VS Code integriert bereits ein Terminal. Um ein neues Terminal in VS Code zu starten, klicken Sie in Menü auf “Terminal -> New Terminal”.

Starten eines Terminals in VS Code über das Menü

Daraufhin wird das Terminal im unteren Bereich angezeigt. Je nach Betriebsystem sieht das Terminal etwas anders aus, in MacOS und Linux wird eine Unix-Shell verwendet (z.B. “bash” oder “sh”). Wenn Sie den Befehl erneuert auswählen, wird ein neues Terminal gestartet, das alte ist dann aber immer noch da. Sie können über die “Dropdown” Box zwischen den verschiedenen Terminals hin und herschalten.

Wechseln des Terminals in VS Code

Falls Sie das Terminal verdeckt haben, können Sie diese Ansicht auch über den Menüeintrag “View -> Terminal” wieder anzeigen.

Windows Terminal-Auswahl

Windows hat die “Command Prompt” oder “Powershell” zur Auswahl. Es kann passieren, dass VS Code die Powershell startet. Für diesen Kurs ist die “Command Prompt” allerdings besser geeignet. In der gleichen Auswahl, in der Sie zwischen den offenen Terminals wechseln können, gibt es auch einen Eintrag “Select Default Shell”. Wählen Sie diesen Eintrag und klicken Sie in der darauf folgenden Auswahl auf “Command Prompt”.

Auswahl des Windows Terminals

Wählen Sie Command Prompt

Python ist auch nur ein Programm, dass über einen Terminal-Befehl gestartet werden kann. Damit die richtige Version von Python gefunden wird, müssen Sie zuerst die Miniconda-Umgebung aktivieren. Führen Sie dazu den folgenden Befehl aus:

conda activate

Nach Ausführen des Befehls wird am Anfang des Command-Prompt (base) stehen. Danach können Sie das Programm “python3” ausführen, um die interaktive Python-Konsole zu starten:

python3

Starten von Python im Terminal

In der Python-Konsole können einzelne Python-Befehle ausgeführt und getestet werden. Z.B. können Sie die folgende Zeichenketten in die Konsole eintipppen und die Eingabetaste drücken

print("Hello World")

Jeder einzelne Befehl, der mit der Eingabetaste abgeschlossen wird kann eine Ausgabe als Text produzieren, der in der Konsole angezeigt wird sobald der Befehl abgeschlossen ist. Hier z.B. ist die Ausgabe.

Hello World

Übung

Geben Sie einen Befehl ein, der die Zeichenkette Hallo Python in der Konsole anzeigt.

Lösung

print("Hallo Python")

Um die Python-Konsole wieder zu beenden, geben Sie den Befehl

exit()

ein. Daraufhin sind Sie wieder im normalen System-Terminal.

Beenden der Python-Konsole

Wenn Sie jetzt wieder python3 ausführen wollen, müssen sich nicht noch einmal die Conda-Umgebung aktivieren, da der Prompt (base) anzeigt, dass Conda immer noch aktiv ist. Falls Sie VS Code oder das Terminal beenden, müssen Sie die Conda-Umgebung eventuell neu aktivieren. Je nachdem wie Sie Miniconda installiert haben, kann es auch sein, das die Conda-Umgebung bereits aktiviert ist, wenn Sie ein beliebiges Terminal starten.

Quelltext-Dateien

Python-Programme besteht aber typischerweise aus vielen Anweisungen, die in einer oder mehreren Quelltext-Datei gebündelt werden. Ein typischer Arbeitsablauf kann also sein, in der Konsole eine Abfolgen von Befehlen auszuprobieren und diese dann in die Quelltext-Datei zu kopieren.

Erstellen Sie eine neue Datei mit dem Namen hello.py und öffnen Sie diese im Texteditor von VS Code. Die Dateiendung .py zeigt an, dass dies eine Python-Quelltextdatei ist. Fügen Sie dann folgende zwei Zeilen zu der Textdatei hinzu und speichern Sie Datei:

print("Hello World")
print("Hallo Python")

Sie können jetzt wieder das python3 Programm nutzen, um den Python-Code auszuführen. Anstatt aber jede Zeile interaktiv einzutippen, geben Sie den relativen Pfad zur Datei als Argument des Programmaufrufs an.

python3 hello.py

Python-Datei in Terminal ausführen

Herzlichen Glückwunsch, Sie haben ihr erstes eigenes Python-Programm ausgeführt! 🎉

Wenn eine Datei als Argument übergeben wird, wird python3 diese Anweisung für Anweisung ausführen und alle Ausgaben, die z.B. mit dem Python-Befehl print() gemacht werden ausgeben.

Hello World
Hallo Python

Kernpunkte

  • Einfache Ausdrücke können durch die interaktive Eingabe in der Python-Konsole ausgeführt werden.

  • Folgen von Anweisungen können in Quelltextdateien mit der Endung .py abgelegt und mit dem Programm python3 ausgeführt werden.


Einfache Ausdrücke, Datentypen und Variablen

Overview

Teaching: 50 min
Exercises: 0 min
Questions
  • Wie kann man einfache Ausdrücke in Python ausführen?

  • Was sind Datentypen und Variablen?

Objectives
  • Umgang mit der Python-Konsole

  • Datentypen, Werte und Variablen verstehen

Ganze Zahlen und Kommazahlen

In Programmen muss man immer wieder Dinge berechnen, zum Beispiel Häufigkeiten von Vorkommen von Wörtern. Python kann mit zwei Arten von Zahlen umgehen: ganzen Zahlen (Integer) und Kommazahlen (Float von „Gleitkommazahl“).

Zunächst wollen wir testen, wie die Python-Konsole auf Dateneingaben reagiert. Dafür tippen wir eine x-beliebige Zahl ein und drücken die Eingabetaste Return:

12

Der Python-Interpreter gibt nun folgende Ausgabe als Antwort:

12

Die Python-Konsole berechnet für uns den Wert 12. Das klingt nur so merkwürdig, weil es an ursprünglichen Wert 12 nichts mehr zu berechnen gibt, weshalb Ein- und Ausgabe identisch sind.

Trotzdem haben wir einen vollständigen Ausdruck (in Englisch „Expression“) an Python übergeben, den wir mit der Eingabetaste abgeschlossen haben.

12 lässt sich aber noch anders ausdrücken bzw. berechnen, z. B. durch

6+6

Die Python-Konsole gibt uns das Ergebnis der Operation „+“ zurück, angewendet auf 6 und 6.

12

Zum Verständnis: Wir haben der Python-Konsole das Input 6+6 gegeben und als Output 12 erhalten. Gleichzeitig haben wir der Operation + das Input (6,6) gegeben und als Output 12 erhalten.

Rechenoperationen

Es gibt folgende relevante Rechenoperationen in Python, die Operatoren sind in Klammern mit angebeben.

  1. Addition (+) und Subtraktion (-)
  2. Multiplikation (*), Division (/)
  3. ganzzahlige Division (//), Modulo (%)

+ und - werden zusätzlich als Vorzeichenoperatoren verwendet, wobei wir + in diesem Sinne nicht benutzen werden.

-12

Wie in der Mathematik, haben auch in Python * und / Vorrang vor + und -.

Übung

Was ist die Ausgabe dieses Ausdrucks und warum?

-4*2+10

Lösung

2

Zuerst wird -4*2 ausgewertet, was -8 ergibt. Danach wird mit +10 addiert und das Ergebnis 2 angezeigt.

Klammern können genutzt werden, um die Reihenfolge der Operatoren explizit anzugeben.

-4*(2+10)
-48

Wenn wir dividieren, erhalten wir als Rückgabewert keinen Integer, sondern einen Float:

3/4
0.75

Das gilt auch, wenn der Dividend durch den Divisor teilbar ist und eigentlich eine ganze Zahl anstatt einer Kommazahl zurückgegeben werden könnte.

-10/5
-2.0

Der Operator erzeugt also immer einen Float als Rückggabewert.

Übung

Was berechnen die Operatoren // und % ? Finden Sie es für die folgenden Ausdrücke heraus!

5//2
6//4
9//3
7%4
15%13
4%2

Lösung

  • 5//2 = 2
  • 6//4 = 1
  • 9//3 = 3
  • 7%4 = 3
  • 15%13 = 2
  • 4%2 = 0

Ganzzahlige Division (//) gibt uns das Ergebnis der Division ohne Nachkommastellen als Integer (nicht: Float) zurück. Modulo-Rechnung (%) hingegen gibt uns den Rest der ganzzahligen Division zurück. Das heißt immer dann, wenn a durch b teilbar ist, ist a%b gleich 0. Diese zwei Operationen können in verschiedenen Anwendungsfällen sehr nützlich sein.

Variablen

Wir programmieren (u. a.), um Probleme zu lösen und uns eigene Rechenarbeit zu ersparen. Für komplexe Berechnungen nützt uns die simple Berechnung allein wenig, wir müssen ihr Ergebnis auch zwischenspeichern, um es weiter verwenden zu können. Hierfür gibt es Variablen.

zahl1 = 5
zahl2 = 7

Wir haben 5 in der Variable mit dem Namen zahl1 gespeichert, 7 in zahl2. Die Zuweisung eines Wertes zu einer Variable wird auch assignment genannt.

Was passiert, wenn wir anschließend folgenden Befehl ausführen?

zahl1+zahl2

Python evaluiert die Werte von zahl1 und zahl2 und addiert diese.

12

Und auch dieses Ergebnis können wir wieder in einer Variable speichern:

ergebnis = zahl1+zahl2

Übung

Wie können wir nun das Ergebnis der Addition ansehen? Versuchen Sie es auf der Python-Konsole.

Lösung

Sie müssen den Variablennamen als Ausdruck in die Konsole eingeben:

ergebnis

Variablen können einen leeren Wert annehmen, in denen ihnen der spezielle Wert None zugewiesen wird.

ergebnis = None
ergebnis

Variablen vom Wert None erzeugen keine Ausgabe.

Frage(n)

Was passiert, wenn wir die folgende Zeilen nacheinander ausführen?

zahl1
Zahl2

Lösung

>>> zahl1
5
>>> Zahl2
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'Zahl2' is not defined

Was hat das zu bedeuten? Wie können wir das „reparieren“? Was erfahren wir über Variablennamen?

Einfache eingebaute Funktionen

Sogenannte Funktionen haben einen Namen, es können ein odere mehrere Argumente in Klammern angegeben werden und sie liefern einen Wert zurück (mehrere Argumente werden durch Komma getrennt).

Zum Beispiel liefert die eingebaute Funktion mit dem Namen type eine Information über den Typ eines Werts oder Variable.

type(zahl1)
<class 'int'>

Der Rückgabewert zeigt an, dass es sich um eine Ganze Zahl (Integer) handelt.

Im Gegensatz dazu, liefert

type(3.141)
<class 'float'>

einen Rückgabe wert für eine Kommazahl (Float) zurück.

Mit der eingebauten Funktion isinstance(wert, klasse) kann man überprüfen, ob ein Wert bzw. eine Variable einen bestimmten Typ hat. Der Rückgabewert ist dabei vom Typ bool, also ein Wahrheitswert, und kann True oder False sein.

isinstance(3, int)
True
isinstance(3.0, int)
False

Python ist eine dynamisch typisierte Programmiersprache. Das heißt, die gleiche Variable kann unterschiedliche Typen wie Float oder Integer zugewiesen bekommen. Mit type kann man den Typ herausfinden, aber es gibt auch Funktionen um explizit eine Variable zu einem anderen Typ zu überführen („casten“).

pi = int(3.141)
type(pi)
<class 'int'>
pi
3
float(pi)
3.0

Es noch gibt eine Vielzahl weiterer, bereits eingebauter Funktionen in Python. Eine Listen davon finden Sie unter https://docs.python.org/3.7/library/functions.html Einige davon benötigen keine Eingabeargumente, trotzdem müssen in diesem Fall die Klammern mit angegeben werden.

Kommentare und Dokumentation

Kommentieren Sie ihren eigenen Code und dokumentieren Sie somit dessen Funktionsweise. Das erleichtert anderen und auch Ihnen, den Code später noch zu verstehen. Außerdem können auf diese Weise Voraussetzungen Ihres Programms an die Eingabe und eine Darlegung der Form des Outputs dargelegt werden. Kommentare im Code beginnen Sie mit einer #.

Ein Beispiel:

# Das ist ein Kommentar
# This is a comment

Sprache in Python-Skripten

Es ist üblich Kommentare und andere Element des Quelltexts in Englisch zu halten, da sie so für ein breiteres Publikum verständlich sind. Im Rahmen dieses Kurses können Sie das aber ignorieren. Sie sollten lediglich sicherstellen, dass Ihre Mit-Studierenden und Ihr Dozent Sie verstehen.

Kernpunkte

  • Einfache Ausdrücke können durch die interaktive Eingabe in der Python-Konsole ausgeführt werden.

  • Datentypen sind Kategorien von Werten

  • Unterschiedliche Ausdrücke können Werte mit unterschiedliche Datentypen erzeugen

  • Variablen speichern Werte

  • Variablen muss ein Wert zugewiesen werden bevor sie genutzt werden können

  • Variablen können in Ausdrücken benutzt werden.

  • Variablen haben Namen und diese Namen unterscheiden Groß- und Kleinschreibung


Zeichenketten und einfache Skripte

Overview

Teaching: 110 min
Exercises: 0 min
Questions
  • Wie kann ich Zeichenketten in Python eingeben und darstellen?

  • Wie kann ich auf diese oder Teile davon zugreifen?

  • Was muss ich beim Ausführen von Python-Skripten beachten?

Objectives
  • Verstehen des Datentyps str Typs von Python

Zeichenketten als Wert definieren

Zeichenketten oder Strings sind Sequenzen von Zeichen die zur Repräsentation von Text verwendet werden. Sie werden in Python gekennzeichnet mit umschließenden ' oder ". Folgende Befehle sind also äquivalent:

text = 'Ein Brötchen kostet 25 Cent'
text = "Ein Brötchen kostet 25 Cent"

Variablen mit einem Textwert haben den Typ str:

type(text)
<class 'str'>

Man kann allerdings nicht einen String mit ' beginnen und dann mit " schließen, oder umgekehrt

text = 'Ein Brötchen kostet 25 Cent"
  File "<stdin>", line 1
    text = 'Ein Brötchen kostet 25 Cent"
                                        ^
SyntaxError: EOL while scanning string literal

Zugriff auf Teile von Strings

Jedem Zeichen in einem String ist ein Index zugewiesen, der seiner Position in der Zeichenkette entspricht. Gezählt wird ab 0 und Leerzeichen werden nicht übersprungen:

E i n   B r ö t c h e n   k o s t e t   2 5   C e n t
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26

Der in text gespeicherte String hat eine Länge von 27 Zeichen. Diese berechnen wir in Python mit der len Funktion:

len(text)

Es ist möglich auf Abschnitte bzw. einzelne Zeichen des Strings über den Index und eckige Klammern zuzugreifen. Probieren Sie es selbst:

text[6]

Zeichenkodierung in Python

Wir erkennen, dass Python keine Probleme mit Umlauten hat. Das liegt daran, dass in Python 3 (im Gegensatz zu Python 2) Strings standardmäßig in Unicode kodiert sind.

Frage(n)

Indexierung funktioniert auch mit negativen Zahlen:

text[-3]

Was erhalten Sie für den negativen index [-3], wie geht Python in solchen Fällen vor?

Lösung

'e'

Python gibt nicht das 3. Zeichen von vorne, sondern das 3. Zeichen vom Ende des Textes aus.

Frage(n)

Was passiert in folgendem Fall? Bevor sie es ausführen, was erwarten Sie?

text[30]

Lösung

Sie erhalten eine Fehlermeldung, dass der Index außerhalb des korrekten Bereichs für diesen Textwert war.

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
IndexError: string index out of range

Einen Abschnitt eines Strings (Substring) erhalten wir über einen Index [a:b] wobei a der Index des ersten Zeichens ist und b das Ende des Abschnitts beschreibt. a ist inklusiv, der a-te Buchstabe ist also selbst im Substring enthalten, b ist dagegen exklusiv, der b-te Buchstabe ist also nicht enthalten, nur der Buchstabe (mit Index b-1) davor.

text[0:3]
'Ein'

Frage(n)

Probieren Sie anhand eines Beispielstrings mit der Länge L folgende Dinge aus:

  • Was passiert, wenn Sie a, b oder beide Grenzen weglassen?
  • Was passiert, wenn b > Länge des Strings
  • Was passiert, wenn a > Länge des Strings
  • Was passiert, wenn a > b? Was wenn a == b?
  • Was passiert, wenn a > 0 und b < 0?
  • Was passiert, wenn a < 0 und b > 0?

Lösung

Wenn die erste Grenze a weggelassen wird, wird implizit das erste Zeichen (0) als Start angenommen.

text[:3]
'Ein'

Wenn dagegen die zweite Grenze b weggelassen wird, wird implizit das letzte Zeichen als Ende angenommen.

text[23:]
'Cent'

Das Weglassen beider Grenzen entspricht dem ganzen String, ohne Einschränkung für Beginn und Ende.

text[:]
'Ein Brötchen kostet 25 Cent'

Der End-Index wird immer implizit auf die Länge des Strings begrenzt. Ist der Größer, wird der String nur bis zum Ende des Strings zurück gegeben. Ist hingegen der Start-Index größer als die Länge des Texts, wird nur der leere String zurück gegeben.

text[4:123]
'Brötchen kostet 25 Cent'
text[123:]
''

Analog dazu, wird auch der leere String ausgegeben, wenn der Start-Index größer oder gleich dem End-Index ist.

text[123:4]
''
text[4:4]
''

Wenn der Start-Index a größer gleich 0 ist und der End-Index b negativ, wird der Teilstring vom Zeichen mit dem Index a bis zum b. Zeichen zählend vom Ende des Texts zurückgegeben.

text[4:-8]
'Brötchen kostet'

Das geht aber nur, solange der negative Wert nicht über den Anfang des mit a beginnenden Teilstrings “hinausragt”.

Wenn a < 0 und b > 0 ist, wird normalerweise eine leerer String zurückgegeben. Wenn das Ende b aber größer ist als die Länge des Strings, ist der Ausdruck äquivalent zu text[-a:] und es werden die letzten a Zeichen des Teilstrings ausgeben.

text[-4:10]
''
text[-4:100]
'Cent'

Wie Sie sehen, können negative Index sehr schnell sehr kompliziert und unerwartete Semantik mit sich bringen. Die Python-Dokumentation zu “Sequences” hat weitere Beispiele für komplexe Zugriffe über Indexes: https://docs.python.org/3.7/library/stdtypes.html?highlight=sequence#sequence-types-list-tuple-range. Solche komplexen Konstrukte werden ohne genaue Kenntnis der Python-Dokument sehr schnell sehr schwer verständlich und sollten vermieden werden.

Übung

Beschäftigen wir uns weiter mit dem oben erwähnten String, den wir in text gespeichert haben. Nehmen Sie an, Sie wollen die für Sie wesentliche Information aus Werbetexten wie „Ein Brötchen kostet 25 Cent“ extrahieren. Sie interessieren sich nur für das Produkt („Brötchen“) und den Preis („25 Cent“).

Vervollständigen Sie das folgende Codefragment so, dass diese Daten in den beiden Variablen what und howmuch gespeichert werden.

what = text
howmuch = text

Lösung

what = text[4:12]
howmuch = text[20:len(text)]

In der interaktiven Konsole kann man den Namen einer Variable als Befehl eingeben und bekommt deren Wert angezeigt. Sobald wir nicht mehr interaktiv in der Konsole, sondern mit Python-Skripte arbeiten, wird uns das nicht mehr weiterhelfen. Wir benötigen eine Funktion, mit der wir Variablen explizit ausgeben können.

Die Funktion print nimmt als Eingabe Daten vom Typ String und gibt diese auf der Konsole aus. Das einzige Argument der Funktion wird dabei in Klammern angegeben. Versuchen Sie es selbst:

print(what)

Sie können print auch mehrere Strings übergeben. Diese werden durch ein Zeichen (Standard: Leerzeichen) getrennt ausgegeben. Das Trennzeichen kann über den Parameter sep manipuliert werden. sep ist ein benannter Parameter, das heißt man gibt in den Klammern nicht nur den Wert an, sondern schreibt sep= davor.

print(what,howmuch)
Brötchen 25 Cent
print(what,howmuch,sep=";")
Brötchen;25 Cent

Ausführen mehrerer Anweisungen in einem Skript

Wie wir bereits im 1. Kapitel gesehen haben, können wir anstatt Anweisungen für ein Programm interaktiv in der Konsole auszuführen sie auch in eine Textdatei mit der Endung .py schreiben. Der Befehl python3 dateiname.py auf der Konsole führt diese Anweisungen dann hintereinander aus. Wird das Programm beendet, werden auch alle Variablen vergessen. Jeder Aufruf vom Python-Interpreter startet also immer ohne definierte Variablen, erst durch die einzelnen Ausführungsschritte im Skripts werden die Variablen definiert und mit Werten belegt. Bei der Entwicklung testet man oft kurze Code-Stücke in der interaktiven Konsole, die man dann in ein Skript überträgt.

Windows

Windows enthält standardmäßig kein Python und die Unterscheidung in Version 2 und 3 ist daher nicht so relevant. Nach einer systemweiten Installation und der Eintragung von der Python-Installation in die Systemvariable PATH oder mit conda activate kann man daher unter Windows mit dem Befehl python (also ohne die Versionsnummer „3“) auf der Kommandozeile starten.

Übung

Nutzen Sie einen Texteditor um ein Programm zu schreiben, dass folgende Anweisungen hintereinander ausführt und führen sie dies aus.

  • Berechne die Summe von 100 und 123
  • Gebe die Zeichenkette “100+123=???” wobei „???“ für das Ergebnis steht.

Lösung

# Dieser Text steht in der Datei summe.py
summe = 100+123
print("100+123",summe,sep="=")
python3 summe.py
100+123=223

Strings verketten

Wenn wir zwei Strings miteinander zu einem neuen String verbinden wollen (konkatenieren), so nutzen wir den „+“-Operator, den wir schon von den Zahlenoperationen kennen.

stem_1 = 'Haus'
stem_2 = 'boot'
comp = stem_1 + stem_2
print(comp)
Hausboot

Wollen wir eine Zeichenkette n mal vervielfachen, so können wir sie einfach mit n multiplizieren:

result = 'Nomen'*10
print(result)
NomenNomenNomenNomenNomenNomenNomenNomenNomenNomen

Strings können aus anderen Datentypen über die Konvertierungsfunktion str(val) erzeugt werden. Damit kann man z.B. auch einen String mit einer Zahl verketten

zahl = 13+13
result = "Die Zahl ist " + str(zahl)
print(result)
Die Zahl ist 26

String-Methoden

Wir wollen uns einen Überblick über die wichtigsten Methoden für String-Variablen ansehen. Aber was sind denn nun wieder Methoden? In dieser Episode nur so viel: Methoden sind Funktionen, die wir an einer Variable (genauer: einem Objekt) mit Hilfe eines . direkt aufrufen. Hier ein Beispiel:

var = "Huch"
print(var) # Funktion
var.strip() # Methode

strip()

Diese Methode ist z. B. wichtig bei der Bereinigung unserer Eingabe. Sie entfernt Leerzeichen, Zeilenumbrüche (\n in Linux), Tab-Einrückungszeichen (\t) und andere White-Space-Zeichen am Anfang und Ende des Strings. Sie verändert nicht die Variable, an der sie aufgerufen wird, sondern gibt einen neuen String zurück.

txt = "\tWas macht strip()? "
txt_no_whitespaces = txt.strip()
print("|" + txt + "|")
|       Was macht strip()? |
print("|" + txt_no_whitespaces + "|")
|Was macht strip()?|

find(seq) und rfind(seq)

Wenn wir eine Sequenz von 1 und mehr Zeichen in einem String suchen wollen, geben uns diese Methoden die Stelle (als Integer) im String zurück, an der die Sequenz beginnt. find(seq) gibt uns den ersten Treffer von links, rfind(seq) entsprechend von rechts. Ausgegeben wird die Stelle des ersten Zeichens der gesuchten Sequenz.

haystack = 'Heu Nadel Heu Heu Nadel Heu'
l = haystack.find('Nadel')
r = haystack.rfind('Nadel')
print(l,r)
4 18

Übung

Bearbeiten Sie den folgenden Abschnitt so, dass aus info nur der Text zwischen „Achtung“ und „egal“ ausgegeben (über die print Funktion) wird.

info = "bla bla bla Achtung Wichtig egal bla bla bla"
# Ihr Code:

Lösung

wichtig_l = info.find("Achtung") + len("Achtung")
wichtig_r = info.rfind("egal")
print(info[wichtig_l:wichtig_r].strip())
Wichtig

replace(is, willbe)

Mit dieser Methode wird eine Zeichensequenz in einem String an jeder Stelle ihres Vorkommens durch eine andere ersetzt. Das Ergebnis wird als neuer String zurückgegeben.

In folgendem Beispiel sollen aus dem Input-String „ß“ und Umlaute entfernt und durch Alternativen ersetzt werden:

messy_input = "Sie wohnen in der Hauptstraße. Das ist ungewöhnlich."
clean_input = messy_input.replace("ß","ss").replace("ö","oe").replace("ä","ae").replace("ü","ue")
print(clean_input)
Sie wohnen in der Hauptstrasse. Das ist ungewoehnlich.

Frage(n)

  • Warum kann replace einfach mehrmals hintereinander aufgerufen werden?
  • Was passiert, wenn ein String eine zu ersetzende Zeichensequenz nicht enthält?

Lösung

Die Methode replace liefert als Rückgabewert wieder einen neuen String zurück und auf Strings kann man die Methode replace ausführen. Man muss nicht zwingend den Rückgabewert in eine Variable zwischenspeichern, um ihn zu nutzen.

Wenn die gesuchte Zeichensequenz nicht vorkommt, wird der unveränderte Original-String zurückgegeben.

messy_input.replace("Berlin", "Brandenburg")
'Sie wohnen in der Hauptstraße. Das ist ungewöhnlich.'

Übung

Schreiben Sie einen „Übersetzer“, der den String german ins Englische übersetzt.

german = "Die Katze ist schwarz"
# Ihr Code
engl = 

Überlegen Sie kurz, ob dieses Vorgehen generell, also für eine beliebige deutsche Eingabe, sinnvoll ist.

Lösung

Wir können im Moment nur kontextunabhängig einzelne Strings ersetzen. Für das konkrete Beispiel reicht das schon, auch wenn wir natürlich kein Zugriff auf ein Lexikon oder syntaktische und semantische Informationen haben und nicht einmal Wörter erkennen oder sogar die Wortstellung umschreiben können.

german = "Die Katze ist schwarz"
engl = german.replace("Die", "The").replace("Katze", "cat").replace("ist", "is").replace("schwarz", "black")
print(engl)
The cat is black

startswith und endswith

Die beiden Funktionen überprüfen, ob ein String mit einem Prefix beginnt oder einem Suffix endet.

satz = "Ein Wort ist kein Satz."

print(satz.startswith("Ein"))
print(satz.endswith("!"))
True
False

Kernpunkte

  • Strings sind Sequenzen von Zeichen auf die über einen Index zugegriffen werden kann.

  • Es gibt Methoden, die Operationen auf diesen Zeichenketten ausführen, wie z.B. finden und ersetzen.

  • Variablen werden nur innerhalb eines Aufrufs des Python-Interpreters gespeichert.


Kontrollstrukturen und Listen

Overview

Teaching: 110 min
Exercises: 30 min
Questions
  • Wie können Sequenzen von Werten gespeichert werden?

  • Wie kann ich Code mehrfach ausführen?

  • Wie kann ich Code nur unter einer bestimmten Bedingung ausführen?

Objectives
  • Verständnis von wichtigen Hilfsdatentypen und Methoden für Kontrollstrukturen

  • Verstehen von Kontrollstrukturen wie Schleifen und Bedingungen

  • Ausdrücken von logischen Bedingungen

Listen

Eine Liste ist eine veränderbare Sequenz von Werte. Sie kann gleiche Elemente mehrmals enthalten. Zudem sind die einzelnen Elemente in einer festen Reihenfolge angeordnet, jedes Element hat einen Index, der dessen Platz in der Liste entspricht. Die Elemente einer Liste mit der Länge L können über die Indizes 0..L-1 angesprochen werden. Das kennen wir bereits von Strings.

l = [] # wir initialisieren eine leere Liste
l.append(1) # wir fügen der Liste Elemente hinzu
l.append("Piano")
l.append(1.42)
print(l)
[1, 'Piano', 1.42]

Länge einer Liste:

print(len(l))
3

Abruf eines einzelnen Elements:

print(l[0])
1

Abruf einer Unterliste:

print(l[0:len(l)-1])
[1, 'Piano']

Frage(n)

  • Welchen Typ hat l?
  • Welchen Typ hat l[0:2]?
  • Welchen Typ haben l[0],l[1] und l[2]?

Lösung

Zum beantworten müssen Sie die Funktion type() auf den jeweiligen Ausdrücken ausführen.

type(l)
type(l[0:2])
type(l[0])
type(l[1])
type(l[2])

Die ersten beiden Ausdrücke geben die gesamte Liste oder eine Teilliste zurück. und liefern als Rückgabe der type()-Funktion den Typ list für Listen in Python zurück. Wenn nur auf einzelne Element der Liste zurückgegriffen wird, wie in den letzten drei Aufrufen, wird der eigentliche Wert der an dieser Listenposition steht zurückgegeben. Damit liefert auch die type()-Funktion den Typ des Werts, hier also int, str und float, zurück.

Es können nicht nur neue Elemente hinzugefügt, sondern auch bestehende Elemente entfernt werden.

l.remove("Piano") # wir entfernen ein Element aus der Liste (nach Wert)
l.pop(0) # wir entfernen ein Element aus der Liste (nach Index)
print(l)
[1.42]

Es gibt auch weitere nützliche Methoden auf Listen.

Zählen der Anzahl der Elemente mit gegebenen Wert in einer Liste mit count:

a = [1,4,7,3,4,4]
a.count(4)
3

Umgekehrte Reihenfolge mit reverse:

a = [1,7,3,4]
a.reverse()
print(a)
[4, 3, 7, 1]

Sortierung mit sort:

a.sort()
print(a)
[1, 3, 4, 7]

Einfügen an angebenen Index (und nicht nur anhängen):

a.insert(2,10)
print(a)
[1, 3, 10, 4, 7]

Entfernen des letzten Elements mit pop() (ohne Argument):

e = a.pop()
print("popped", e, ", list is", a)
popped 7 , list is [1, 3, 10, 4]

Löschen des Inhalts einer Liste:

a.clear()
print(a)
[]

For-Schleifen

Mit dem Ausdruck for x in var: kann man den gleichen Code-Block auf jedem Element x der Liste var ausführen. Ein Code-Block sind Anweisungen, die mit der gleichen Anzahl Leerzeichen eingerückt sind. Der Code-Block wird also wiederholt.

l = [1, 2, 3, 4, 5]
for x in l:
  print("Element")
  print(x)
Element
1
Element
2
Element
3
Element
4
Element
5

Einrückung mit Leerzeichen oder Tabs?

Für die Einrückung eines Codeblocks können entweder Leerzeichen oder Tabs verwendet werden. Viele Texteditoren ersetzen eine Eingabe mit der Tab Taste auf der Tastatur automatisch mit einer bestimmten Anzahl Leerzeichen. Da Tabs ebenfalls als „Whitespace“ angezeigt werden, ist es schwierig diese auseinanderzuhalten. In alten Python-Versionen konnten Leerzeichen und Tabs im selben Skript gemischt werden, was zu Verwirrung und Fehlern bei unterschiedlich tief eingerückten Code-Blöcken geführt hat. In Python 3 ist das Mischen verboten.

Es bleibt natürlich die Frage, was besser ist. Darauf gibt es keine wirklich ultimative Antwort und diese Frage wird in entsprechenden Foren immer wieder stark diskutiert. Die „Style Guide for Python Code“ empfielt in neuem Code Leerzeichen zu verwenden: https://www.python.org/dev/peps/pep-0008/#tabs-or-spaces

Anstatt über eine Liste, können for-Schleifen auf sogenannten Iteratoren ausgeführt werden. Iteratoren sind Datenstrukturen, die basierend auf einem aktuellen Zustand einen neuen Zustand berechnen und zurückgeben können.

Die Funktion range(a,b) liefert einen Iterator zurück, der nacheinander alle Zahlen von a bis b (exklusive b) ausgibt.

Übung

Formulieren Sie for-Schleife von weiter oben so um, dass sie die range Funktion anstatt der Liste benutzt, aber die gleiche Ausgabe erzeugt.

Lösung

for x in range(1,6):
  print("Element")
  print(x)

Iteratoren haben gegenüber von Listen den Vorteil, dass sie nicht alle Element speichern müssen. Wenn wir z.B. alle Zahlen von 1 bis 1.000.000 aufaddieren wollen, würde das viel Speicher belegen, wenn wir eine Liste mit einer Million Zahlen anlegen müssten. Mit einem Iterator wird immer nur ein kleiner Zustand (die aktuelle Zahl und das Ende des Bereichs) gespeichert.

summe = 0
for i in range(1,1_000_001):
  summe = summe + i
print("Summe ist: " + str(summe))
Summe ist: 500000500000

Tausendertrennzeichen im Code

Um große Zahlen übersichtlicher eingeben zu können, ist seit Python 3.6 im Python-Code _ als Tausendertrennzeichen erlaubt.

Der Name der Schleifenvariable kann frei gewählt werden. Für Indexe hat sich oft i eingebürgert, aber sollte möglichst sprechende Namen für Variablen verwenden.

for hier_kann_wirklich_alles_stehen in range(0,11):
  print(hier_kann_wirklich_alles_stehen)
0
1
2
3
4
5
6
7
8
9
10

For-Schleifen und andere Kontrollstrukturen können verschachtelt werden, das heißt in einem for-Block können z.B. auch weitere for-Blöcke stehen. Die tiefere Verschachtelung eines Code-Blocks wird durch die größere Anzahl an Leerzeichen/Tabs ausgedrückt:

for i in range(1,4):
    for j in range(1,4):
        summe = i + j
        produkt = i * j
        print(i, "+", j, "=", summe)
        print(i, "*", j, "=", produkt)
1 + 1 = 2
1 * 1 = 1
1 + 2 = 3
1 * 2 = 2
1 + 3 = 4
1 * 3 = 3
2 + 1 = 3
2 * 1 = 2
2 + 2 = 4
2 * 2 = 4
2 + 3 = 5
2 * 3 = 6
3 + 1 = 4
3 * 1 = 3
3 + 2 = 5
3 * 2 = 6
3 + 3 = 6
3 * 3 = 9

Einfache logische Bedingungen und der Datentyp bool

Neben Strings und Zahlen hat Python auch einen Datentyp für Wahrheitswerte. Eine Variable mit Wahrheitswerten kann entweder den Wert True oder False haben.

stimmt = False
type(stimmt)
<class 'bool'>
stimmt = True
print(stimmt)
True

Man kann Wahrheitswerte durch den Vergleich von anderen Werte über bestimmte Operatoren erhalten. Z.B. kann man zwei Zeichenketten mit dem == Operator vergleichen, ob sie identisch sind:

"Haus" == "Haus"
True

Das Ergebnis eines Vergleichs kann auch wieder in einer Variablen gespeichert werden:

gleiches_haus = "Haus" == "Haus"
print(gleiches_haus)
True

== ist nicht gleich =

Der Vergleichsoperator == ist nicht mit dem Zuweisungsoperator = zu verwechseln. Letzerer weist einer Variable einen Wert zu und lieft nicht einen logischen Wert zurück. Z.B. ist folgender Ausdruck ein Fehler:

"Haus" = "Haus"
 File "<stdin>", line 1
SyntaxError: can't assign to literal

Bei einem Vergleich ist der Wert der Variablen oder des Ausdrucks entscheidend, nicht die Form. Z.B. liefern auch alle folgenden Vergleiche True zurück.

"Haus" == 'Haus'
var = 'Haus'
"Haus" == var
0.0000 == 0.0

Wertevergleiche sind exakt, dass heißt z.B. das bei Groß- und Kleinschreibung von Zeichenketten oder bei Whitespace (Leerzeichen, Tabs, Zeilenumbrüche etc.) unterschieden wird.

"Haus" == "haus"
False
"Haus" == "Haus "
False

Übung

Wie können die beiden Zeichenketten "Haus" und "Haus " mit True als Ergebnis verglichen werden?

Lösung

"Haus" == "Haus ".strip()

Analog zum == Operator gibt es den != Operator, der immer dann wahr ist wenn zwei Werte unterschiedlich sind.

4 != 1
True

Wenn-Dann-Blöcke

If-Ausdrücke

Wir haben nur einfache logische Bedingungen mit dem == Operator kennengelernt. Logische Bedingungen können genutzt werden, um Code-Blöcke basierend auf dem Wahrheitswert auszuführen, oder aber nicht.

x = 5
if x == 4:
  print("x ist vier!")

Dieses Programm erzeugt keine Ausgabe, weil die Bedingung nicht wahr ist. Wenn die Bedingung wahr ist, wird der Code-Block ausgeführt.

if x == 5:
  print("x ist fünf!")
x ist fünf!

Die Ein Code-Block kann aus mehr als einer Anweisung bestehen und nach dem eingerückten if-Block wird der orginale Code-Block weiter ausgeführt, egal ob die Bedingung wahr oder falsch war.

x = 5
if x == 4:
  x = x + 2
  print("x wahr vier, neuer Wert ist", x)
x = x - 1
print("x ist", x)
x ist 4
x = 5
if x == 5:
  x = x + 2
  print("x wahr fünf, neuer Wert ist", x)
x = x - 1
print("x ist", x)
x wahr fünf, neuer Wert ist 7
x ist 6

If-Else Ausdrücke

Diese Konstruktion erlaubt die Überprüfung einer Bedingung. Falls die Bedingung wahr ist, wird der Code-Block hinter der if-Anweisung ausgeführt, ansonsten der Block hinter der else: Anweisung:

x = 5
if x == 4:
  print("x ist vier")
else:
  print("x ist ein anderer Wert")
x ist ein anderer Wert

If-Elif(-Else)

Man kann beliebig viele Ausdrücke überprüfen, in dem man zusätzliche elif Bedingungen hinzufügt:

x = 3
if x == 1:
  print("x ist eins")
elif x == 2:
  print("x ist zwei")
elif x == 3:
  print("x ist drei")
elif x == 4:
  print("x ist vier")
x ist drei

Auch an diese Ausdrücke kann ein else angehängt werden:

x = 100
if x == 1:
  print("x ist eins")
elif x == 2:
  print("x ist zwei")
elif x == 3:
  print("x ist drei")
elif x == 4:
  print("x ist vier")
else:
  print("x ist ein anderer Wert")
x ist ein anderer Wert

Es wird immer der erste Ausdruck ausgewertet der wahr ist, die anderen werden ignoriert.

if 2==1:
  print("Mathematik kaputt")
elif 1==1:
  print("Mathematik geht")
elif 2==2:
  print("Mathematik geht immer noch")
else:
  print("Zustand von Mathematik unbekannt")
Mathematik geht

While-Schleifen

Man kann logische Bedingungen auch in While-Schleifen verwenden, um einen Block zu wiederholen, solange eine Bedingung erfüllt ist.

x = 0
while x != 10:
  print(x)
  x = x + 1 
0
1
2
3
4
5
6
7
8
9

Frage(n)

Was passiert wenn die Bedingung immer wahr ist, z.B. in:

while 1 == 1:
  print("Mathematik in Ordnung")

Lösung

Es wird eine sogenannte Endlosschleife erzeugt, die sich nicht mehr beendet. Sie können nur das gesamte Programm (also den Python-Interpreter) von außen beenden um die Ausführung der Schleife zu unterbrechen.

Programm im Terminal beenden

Falls Sie ein Programm im Terminal beenden wollen, können Sie Strg+C drücken.

Übung

Schreiben Sie ein kurzes Programm, das eine Liste aller Zahlen zwischen 1 und 999, die ohne Rest durch 13 teilbar sind, erstellt. Das Programm soll die Liste am Ende ausgeben.

Lösung

Mögliche Lösung:

zahl13 = []
for i in range(1,1000):
    if i % 13 == 0:
        zahl13.append(i)
print(zahl13)

Arithmetische Vergleichsoperatoren

Neben == und != gibt es für Zahlen noch weitere Operatoren:

print(2 < 2)
print(2 <= 2)
print(10 > 1)
print(10 >= 1)
False
True
True
True

Übung

Schreiben Sie ein kurzes Programm, das eine Liste aller Zahlen zwischen 1 und 999, die ohne Rest durch 13 teilbar sind, erstellt. Nutzen Sie dafür den < Operator! Diskutieren Sie die Vor- und Nachteile im Vergleich zur Ihrer Version weiter oben.

Lösung

zahl13 = []
x = 13
while x < 1000:
    zahl13.append(x)
    x = x + 13
print(zahl13)

Im Vergleich zur vorherigen Lösung muss noch mehr darauf geachtet werden, dass die Abbruchbedingung wirklich ausgelöst wird um keine Endlosschleife zu erstellen. Endlosschleifen sind mit for deutlich einfacher zu vermeiden. Allerdings ist diese Lösung womöglich performanter. In der for-Schleife wurde tausend mal überprüft, ob eine Zahl durch 13 teilbar ist. Diese Anweisung benötigt Ausführungszeit auf dem Prozessor. Die Anweisungen in der while-Schleife wird nur genau so oft ausgeführt, wie es Zahlen gibt die an die Liste angehängt werden müssen (also deutlich weniger als 1000 mal). Es ist in diesem Fall möglich, die Vorteile der for- mit denen der while-Schleife zu verbinden. Dazu muss bei der range() Funktion der optionale step Parameter mit angeben werden:

zahl13 = []
for i in range(13,1000, 13):
    zahl13.append(i)
print(zahl13)

Logische Formeln

Logische Ausdrücke können wie auch logische Formeln mit and, or und not verknüpft werden

a = True
b = False
implikation = not a or b

Klammern können genutzt werten um explizit die Reihenfolge der Vergleichsoperatoren anzugeben

if (1 != 1 and 1 == 4) or ((2+1) == 4):
  print("Mathematik schon wieder kaputt")
else:
  print("Mathematik geht noch")

Übung

Bilden Sie aus den gegebenen den folgenden drei Variablen den komplexen Wahrheitswert social_security. Dieser sagt aus, ob wir noch Kindergeld erhalten. Hier die genauen Bedingungen:

  • Wenn wir minderjährig sind, gibt es Kindergeld
  • Wenn wir nicht mehr minderjährig, aber jünger als 25 sind und studieren, gibt es Kindergeld
  • Wenn wir älter als 24 sind und ein FSJ (freiwilliges soziales Jahr) absolviert haben, bekommen wir Kindergeld (wir ignorieren, dass das zeitlich begrenzt ist)
age = 24
studying = False
fsj = True

social_security = # TODO: logischer Ausdruck

Lösung

age = 24
studying = False
fsj = True

social_security = (age < 18) or (age < 25 and studying) or (age > 24 and fsj)

Interaktive Eingabe

Wir kennen bereits print(var) um Informationen auszugeben. Das Gegenstück dazu die Funktion input(). Sie erlaubt einem Python-Skript, Texteingaben vom Nutzer entgegenzunehmen.

print("Hallo, meine Name ist Script!")
name = input("Wie heißen Sie? ")
print("Hallo " +  name + "!")

Dieses Script erzeugt erst die Ausgabe

Hallo, meine Name ist Script!
Wie heißen Sie? ▮

Das Argument von input() ist ein sogenannter “Prompt”, also eine Eingabeaufforderung an den Nutzer. Der Prompt wird ausgegeben und es wir solange gewartet, bis der Nutzer eine Eingabe gemacht und diese mit der Eingabetaste Return abgeschlossen hat. Die Funktion liefert die Eingabe als Typ String zurück. In diesem Beispiel wird die Rückgabe in eine Variable geschrieben und wieder ausgegeben.

Hallo, meine Name ist Script!
Wie heißen Sie? Thomas 
Hallo Thomas!

Übung

Schreiben Sie ein Skript das folgendes tut: Das Skript hat eine interne „magic number“, die Sie frei wählen können. Fragen Sie den Nutzer, wie die Nummer ist. Geben Sie ihm Feedback, ob die Zahl die er geraten hat größer oder kleiner als die „magic number“ ist. Wenn der Nutzer die Zahl erraten hat, gratulieren Sie ihm und beenden das Programm.

Lösung

Mögliche Lösung:

magic_number = 355
guessed = None
while guessed != magic_number:
    guessed = input("Bitte Nummer raten: ")
    guessed = int(guessed)
    if guessed < magic_number:
        print("Zu klein!")
    elif guessed > magic_number:
        print("Zu groß!")
print("Herzlichen Glückwunsch, Sie haben die Nummer erraten!")

Kernpunkte

  • Sequenzen von beliebigen Werten können in Listen gespeichert und abgerufen werden.

  • Logische Ausdrücke haben in Python den Datentyp bool und können unter anderem durch Vergleichsoperatoren erzeugt werden.

  • Mit Schleifen (for, while) und bedingt ausgeführten Code-Blöcken (if-elif-else) kann der Programmablauf flexibel auf die Eingabe reagieren und komplexere Berechnungen durchführen.


Eigene Funktionen und Module

Overview

Teaching: 60 min
Exercises: 0 min
Questions
  • Wie kann ich selbst eigene Funktionen definieren?

  • Wie können Funktionen aus externen Bibliotheken eingebunden werden?’

Objectives
  • Funktionalität von Programmen nachnutzen

Eigene Funktionen definieren

Bisher haben wir uns nur mit einfachen Anweisungen beschäftigt. Typischerweise wollen wir aber nicht immer wieder neu die gleichen Algorithmen an verschiedenen Stellen implementieren, sondern unser Programm soll eine komplexe Anweisung auf einmal ausführen. Dafür müssen wir unseren Code als eigene Funktion definieren:

def add_mult(a, b):
    output = a + b
    output = output + (a * b)
    return output

Eine Funktion besteht aus der Funktionsdefinition, die mit den Schlüssewort def eingeleitet und mit dem Doppelpunkt abgeschlossen wird. Darin steht der Name der Funktion (hier add_mult) und die Namen der Argumente, abgetrennt durch Komma. In dem Beispiel werden zwei Argumente a und b übergeben.

Der Funktionsdefinition folgt ein Code-Block, der die Funktion implementiert. Der Block ist hier mit der return Anweisung abgeschlossen, die die Funktion beendet und den Rückgabewert definiert.

Nachdem eine Funktion definiert ist, kann sie im Code aufgerufen werden, genauso wie die eingebauten Funktionen auch.

v = add_mult(1, 100)
print(v)

Übung

Schreiben Sie eine Funktion, die gegeben zweier Werte die Summe aller Zahlen zwischen dem ersten und dem zweiten Wert (inklusive) angibt. Rufen Sie diese Funktion für verschiedene Werte auf und geben Sie die Werte aus. Für z.B. die Argumente 5 und 13 soll die Funktion die Summe 5 + 6 + 7 + 8 + 9 + 10 + 12 + 13 = 81 berechnen.

Lösung

def complex_math(start, end):
    summe = 0
    for i in range(start, end+1):
        summe = summe + i
    return summe

print(complex_math(5,13))
print(complex_math(1,100))
print(complex_math(20,100))
print(complex_math(1,10))
81
5050
4860
55

Funktionen können Werte von verschiedenen Typen als Argument bekommen. Auch kann die gleiche Funktion verschiedene Typen von Werten zurückgeben und je nach Bedingungen verschiedene return Anweisungen ausführen (es wird aber immer nur die erste return Anweisung ausgeführt).

def fancy_add(val):
    if isinstance(val, str):
        return  val + " + 1"
    elif isinstance(val, int):
        return val + 1
    elif isinstance(val, float):
        return val + 1.0
    else:
        return 42

print(fancy_add(2))
print(fancy_add(2.0))
print(fancy_add("2"))
print(fancy_add(False))
3
3.0
2 + 1
1

Der Code nutzt die eingebaute Funktion isinstance um zu überprüfen, ob eine Variable einen gegebenen Typ hat.

Außerdem ist es möglich, dass eine Funktion mehr als einen Wert zurückgibt. In diesem Fall muss die return Anweisung alle Werte mit Komma getrennt auflisten. Die Zuweisung von mehreren Werten beim Aufruf einer Funktion erfolgt ebenfalls durch Auflistung mit Komma.

def get_str_info(text):
    l = len(text)
    c = text[0]
    return l, c

a, b = get_str_info("This is an arbitrary string, I swear")
36
T

Frage

Was bedeuten die Werte, die die Funktion zurückliefert?

In Python wird unterschieden zwischen positionalen Argumenten, die übergeben werden müssen und Keyword-Argumenten, die optional sind und für die ein Standard-Wert in der Funktions-Definitions angegeben werden kann, der genutzt wird wenn das Argument nicht übergeben wird. Hier ein Beispiel:

def print_log(log_text, with_exclamation_marks=False):
    if with_exclamation_marks:
        print('!!! '+log_text+' !!!')
    else:
        print(log_text)
    
print_log('Nothing happend.')
print_log('Alarm', with_exclamation_marks=True)
print_log('Alarm', True)
Nothing happend.
!!! Alarm !!!
!!! Alarm !!!

Der Standardwert für das Argument with_exclamation_marks ist hier False.

Module und der import Befehl

Eigene Module

Jede Python-Datei ist gleichzeitig eine sogenanntes Modul. Man z.B. kann man das folgende Python-Skript mit dem Namen poornlp.py abspeichern:

def get_str_info(text):
    l = len(text)
    c = text[0]
    return l, c

def is_noun(word):
    return word[0].isupper()

Dieses enthält jetzt die beiden Funktionen get_str_info und is_noun. Durch das Speichern in der Datei haben wir ein Modul gleichen Names (poornlp) erzeugt. Nun wollen wir diese Funktionen ja nachnutzen und nicht jedes mal in unsere Skripte kopieren. Zum Laden der Funktion in ein eigenes Skript oder in die interaktive Konsole kann man den import Befehl benutzen. Angenommen, das Skript mit dem Modul befindet sich im gleichen Ordner, dann kann man

import poornlp

aufrufen und bekommt Zugriff auf die beiden Funktionen über den Modulnamen, gefolgt von einem .:

poornlp.get_str_info("Das ist ein Text")

Möchte man eine bestimmte Funktion aus einem Modul importieren, geht das mit from ... import:

from poornlp import get_str_info

Danach ist es nicht mehr notwendig, für die importierte Funktion den Modulenamen anzugeben:

get_str_info("Das ist ein Text")

Wenn das Modul in einem Unterordner liegt, kann man ebenfalls durch import darauf zugreifen, muss aber wieder einen Punkt zwischen dem Elternmodul (dem Ordner) und dem Kindermodul angeben. Z.B. sei eine Ordnerstruktur mit einem Ordner mathmodules und zwei Python-Dateien in dem Ordner gegeben

mathmodules/
├── basic.py
└── fancy.py

basic.py:

def add(a,b):
    return a + b

fancy.py:

def add(val):
    if isinstance(val, str):
        return  val + " + 1"
    elif isinstance(val, int):
        return val + 1
    elif isinstance(val, float):
        return val + 1.0
    else:
        return 42

Der Import für basic sehe dann folgendermaßen aus:

import mathmodules.basic
mathmodules.basic.add(5,10)

Auch Untermodule müssen importiert werden, z.B. würde

mathmodules.fancy.add("A")

fehlschlagenn wenn nur mathmodules oder mathmodules.basic, aber nicht mathmodules.fancy importiert worden ist.

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: module 'mathmodules' has no attribute 'fancy'

Manchmal sind die Modulnamen sehr lang, dann kann man über import ... as ... abgekürzt werden

import mathmodules.basic as mb
mb.add(42,3.13)

Ähnliches geht auch, wenn man Funktionsnamen mit from ... import ... as ... importiert, was sogar eine Umbennung der Funktion erlaubt und hilft Namenskonflikte aufzulösen:

from mathmodules.fancy import add as fancy_add
fancy_add(42)

Standardbibliotheken und Installation von neuen Bibliotheken

Python besitzt neben den eingebauten Funktionen und Typen auch noch eine riesige sogenannte Standardbibliothek. Dies sind Module, die in allen Python-Installationen (für eine bestimmte Version von Python) immer vorhanden sind. Sie können also erwarten, dass Sie in jedem Skript diese Module importieren können.

Eine Auflistung und Dokumentation der Module der Standardbibliothek findet sich unter https://docs.python.org/3.7/library/index.html.

Reguläre Ausdrücke auf Strings mit Modul re

Ein Beispiel für eine sehr nützliches Module aus der Standardbibliothek ist das Modul für reguläre Ausdrücke (also Mustersuche) mit dem Namen re. Muster oder „Patterns“ müssen erst einmal erstellt („kompilliert“) werden und kann dann auf Strings angewendet werden.

import re
pattern = re.compile('Glü.+')
# gibt nicht None zurück, wenn der ganze String dem Pattern matched
is_match = pattern.match("Was für ein Glück")   
# gibt nicht None zurück, wenn ein Teilstring dem Pattern matched
found = pattern.search("Was für ein Glück")

# Überprüfe ob die jeweiligen Ergebnisse nicht None sind
if is_match:
    print('It is a full match!')
elif found:
    print('I found it somewhere!')
    print("Found string =", found.group(0))
I found it somewhere!
Found string = Glück

Auf der Dokumentation des Moduls re gibt es eine Einführung in die Syntax dieser Patterns und wie man nicht nur ja/nein Suchen sondern auch die Position des Treffers bekommen kann.

Installation neuer Pakete mit pip

Hilfreiche allgemeine Python-Pakete, können über den „Python Package Index“ unter https://pypi.org gefunden werden. Die Installation auf dem lokalen System erfolgt dann mit dem Kommandozeilentool pip.

Um pip nutzen zu können, müssen Sie es unter Umständen erst in Conda installieren. Führen Sie dazu in Ihrer System-Kommandozeile mit aktiviertem Conda (z.B. im VS Code Terminal), folgenden Befehl aus:

conda install pip

Wenn Sie die Installation bestätigen, können Sie danach mit pip list eine Liste aller bereits installierte Pakete aufrufen.

pip list
Package                Version   
---------------------- ----------
asn1crypto             1.2.0     
astroid                2.3.3     
autopep8               1.5.1     
certifi                2020.4.5.1
cffi                   1.13.0    
chardet                3.0.4     
conda                  4.8.3     
conda-package-handling 1.6.0     
cryptography           2.8       
idna                   2.8       
isort                  4.3.21    
lazy-object-proxy      1.4.3     
mccabe                 0.6.1     
pathtools              0.1.2     
pip                    20.0.2    
pycodestyle            2.5.0     
pycosat                0.6.3     
pycparser              2.19      
pylint                 2.4.4     
pyOpenSSL              19.0.0    
PySocks                1.7.1     
requests               2.22.0    
ruamel-yaml            0.15.46   
setuptools             41.4.0    
six                    1.12.0    
tqdm                   4.36.1    
treetaggerwrapper      2.3       
typed-ast              1.4.1     
urllib3                1.24.2    
watchdog               0.10.2    
wheel                  0.33.6    
wrapt                  1.11.2  

Conda vs. pip

Conda ist ähnlich wie pip auch ein Paketmanager, kann also zum Beispiel mit conda install Pakete nachinstallieren. Wir haben Conda benutzt um ein Basissystem mit Python 3.7 und pip zu installieren, im Folgenden werden wir aber ausschließlich pip benutzen. Welchen der Paketmanager Sie für Ihre eigenen Projekte nutzen wollen ist Geschmacksfrage. Conda kann teilweise mit Paketen, die Nicht-Python-Abhängigkeiten (wie zum Beispiel Compiler oder Bibliotheken für maschinelles Lernen) nutzen besser verwalten, es ist mehr vergleichbar mit einem Paketmanager für Betriebsysteme. Im Gegensatz dazu, ist der Fokus von pip auf reinen Python-Abhängigkeiten. Der Python Package Index (PyPI) als zentrale Anlaufstelle für pip-Pakete stellt enorm viele Pakete bereit und es ist auch einfach möglich, selbst Python-Pakete auf PyPI zu veröffentlichen. Neuere Versionen von Conda sind in der Lage, beim Paketmanagement mit pip zu kooperieren, conda list wird zum Beispiel auch die mit pip installiereten Pakete aufzeigen.

Wir wollen nun auf PyPI ein Paket finden, mit dem wir sogenanntes ASCII Art generieren wollen. Dazu benutzen wir auf https://pypi.org/ die Suche, z.B. nach „ascii art“. Nach etwas stöbern in der Trefferliste, finden wir zum Beispiel das Paket art: https://pypi.org/project/art/

Sie sollten sich immer genaue die Metadaten zum Paket, wie z.B. die Lizenz und Autorenschaft anschauen. PyPI Metadaten für das Projekt art

Mit dem Befehl

pip install art

in der System-Kommandozeile kann das Paket installiert werden.

Danach ist es über import art für eigene Python-Skripte oder auf der interaktiven Python-Konsole verfügbar:

import art

print(art.text2art("Python"))
 ____          _    _                   
|  _ \  _   _ | |_ | |__    ___   _ __  
| |_) || | | || __|| '_ \  / _ \ | '_ \ 
|  __/ | |_| || |_ | | | || (_) || | | |
|_|     \__, | \__||_| |_| \___/ |_| |_|
        |___/                           

Kernpunkte

  • Mit Funktionen können Teile von Programmen ausgegliedert und nachgenutzt werden.

  • Eigene Module können in Python-Dateien abgespeichert und mit import in anderen Python-Skripten nachgenutzt werden.

  • Mit pip können neue Pakete, die Module enthalten, installiert werden.


Levenshtein-Distanz

Overview

Teaching: 15 min
Exercises: 90 min
Questions
  • Wie kann man die Levenshtein-Distanz selbst in Python berechnen?

Objectives
  • Anwenden vom Wissen zur Organisation von Quelltext

  • Implementierung eines gegebenen Algorithmus

Nutzung von NLTK zur Berechnung der Levenshtein-Distanz

NLTK ist eine Python-Bibliothek zur Umsetzung vieler NLP-Standard-Aufgaben und auch über pip verfügbar. Mit aktivierter Conda-Umgebung kann es kann einfach im System-Terminal mit

pip install nltk

installiert werden.

Die Bibliothek ist in Untermodulen organisiert. Für Metriken wie die Levenshtein-Distanz, gibt es das Untermodul nltk.distance (Dokumentation)

NLTK mit

import nltk

in einem Python-Skript importiert werden.

Paketnamen und NLTK

Paketnamen in NLTK sind nicht unbedingt Modulnamen. So gibt es zwar das Paket nltk.metrics, aber nach dem Importieren ist der Name des Moduls nltk.distance. Das liegt an der etwas verwirrenden internen Modulstruktur von NLTK. Zudem gibt es ein anderes Modul mit dem Namen metrics, aber aus dem nltk.translate Paket und für Konflikte in den Modulnamen sorgt. Nutzen Sie einfach nltk.distance als Modulenamen, auch wenn die Dokumentation von nltk.metrics.distance spricht.

Die Levenshtein-Distanz ist als Funktion mit der Signatur edit_distance(s1, s2) implementiert.

nltk.distance.edit_distance("Geisterbahn", "Achterbahn")
4

Eigene Implementierung der Levenshtein-Distanz

Wie kann man nun selbst eine Funktion schreiben, die die Levenshtein-Distanz berechnet?

Die Beschreibung des Algorithmus ist folgende: Man berechnet die Distanz

Das allein gibt uns noch keine gute Beschreibung zur eigentlichen Umsetzung, deswegen versuchen wir, das Problem in kleinere Teilprobleme aufzuteilen, die einfacher zu fassen zu sind. Diese Teillösungen kombinieren wir dann zur ganzen Lösung. Es ist zwar notwendig, das Problem in Teilprobleme aufzuteilen, aber dadurch wird es auch schwieriger die Motivation der Teilprobleme und Lösungsansätze zu verstehen. Wenn Sie an einer Stelle nicht weiterkommen, schauen Sie sich die Lösung an und versuchen Sie diese zu verstehen. Am Ende der Übung wird es hoffentlich klarer, wie die einzelnen Teilprobleme zusammenhängen.

„vom längeren Wort zum kürzeren“

Übung

Überlegen Sie, was der Punkt „vom längeren Wort zum kürzeren“ für die eigentliche Berechnung bedeutet? Was sind die einfachsten Fälle für diesen Teil der Berechnung? Schreiben Sie eine Funktion für diese einfachen Fälle. Die Funktion soll überprüfen ob der Fall zutrifft und die Levenshtein-Distanz in diesem Fall zurückgeben.

Lösung

Der einfachste Fall ist, dass einer der beiden Strings leer ist. In diesem Fall ist die Länge des anderen Strings die Anzahl der Edit-Schritte (hier Anzahl der zu hinzuzufügenden Zeichen) und damit die Levenshtein-Distanz.

def levenshtein_distance_add(a, b):
    # Basisfall: leere Zeichenketten bei einem der beiden Strings
    # --> man muss die Zeichen des anderen Strings hinzufügen
    if len(a) == 0:
        return len(b)
    if len(b) == 0:
        return len(a)
    # Basisfall ist nicht eingetreten
    return None

„von jedem Buchstaben aus“

Übung

Was bedeutet der Punkt „von jedem Buchstaben aus“ für die eigentliche Berechnung? Was sind die einfachsten Fälle für diesen Teil der Berechnung? Wie viele Zeichen müssen minimal zwischen beiden Strings verglichen werden und wenn ja welche(s)? Schreiben Sie eine Funktion für diesen einfachen Fall der die Levenshtein-Distanz in diesem Fall zurückgibt.

Lösung

In dieser Funktion wollen wir immer nur ein Zeichen zwischen zwei Strings vergleichen. Es bietet sich an, das letzte Zeichen der Strings dafür zu nehmen. Wenn die letzten Zeichen der Strings gleich sind, ist keine Änderung notwendig und die Distanz ist 0, ansonsten ist sie 1 (da ja nur genau ein Zeichen geändert werden muss).

def levenshtein_distance_modify(a, b):
    if a[-1] == b[-1]:
        # Keine Änderung des letzten Zeichens notwendig
        return 0
    else:
        # Änderung des letzten Zeichens notwendig
        return 1

„und wählt dann das Minimum“

Frage(n)

Was bedeutet der Punkt „und wählt dann das Minimum“ für die eigentliche Berechnung? Minimum von was?

Lösung

Wir können bereits die Levenshtein-Distanz für bestimmte Basisfälle berechnen. Wenn wir die Levenshtein-Distanz für Teilstrings hätten, bei den wir bei einem der Strings ein Zeichen entfernen oder eines ändern, könnten wir die Levenshtein-Distanz für diese neuen Teilstrings berechnen. Die Levenshtein-Distanz plus die „Kosten“ für das Entfernen oder ändern des Zeichens ergibt die Distanz für beide Gesamtstrings. Wir können die Variante (also ob löschen von Zeichen oder Ändern), die am wenigsten Kosten verursacht auswählen und als Wert zurückgeben.

Rekursion

Für die Lösung der dieser Aufgabe müssen Sie sich mit rekursiven Funktionsaufrufen beschäftigen. Rekursion beim Programmieren ist ein schwieriges Thema, das gerade am Anfang schwer zu greifen ist. Grundsätzlich können Funktionen andere Funktionen aufrufen und bilden dabei eine Art Ausführungsbaum.

Wenn man z.B. im folgenden Beispiel die Funktion main aufruft, kann man anhand der Ausgabe die einzelnen Ausführungsschritte nachvollziehen.

def a():
  print("a")

def b():
  print("b")
  a()

def main():
  print("main")
  a()
  b()

main()
main
a
b
a

Erst wird main() aufgerufen, dass erst a() aufruft, dann ruft main() b() auf, was wieder selbst die Funktion a() aufruft.

  main
 /    \
a      b
        \
         a

Rekursive Funktionen rufen sich selbst auf, was zu potentiell unendlich tiefen Ausführungsbäumen führen kann. Es ist daher wichtig, dass eine rekursive Funktion immer eine Art Abbruchbedingung hat, bei der sie sich nicht weiter rekursiv selbst aufruft.

Übung

Schreiben Sie eine Funktion, die die Fälle vorher kombiniert und immer eine Levenshtein-Distanz für zwei gegebene Strings ausgibt. Diese Funktion soll sich rekursiv für immer kleiner werdende Teilzeichenketten selbst aufrufen und die das Minimum der Levenshtein-Distanz für diese Teilzeichenketten auswählen. Im Basisfall, dass einer der beiden String leer ist, wird die Rekursion abgebrochen.

Lösung

def levenshtein_distance(a, b):
    add_cost = levenshtein_distance_add(a, b)
    if add_cost != None:
        # Basisfall traf zu, der rekursive Aufruf wird abgebrochen
        return add_cost

    cost = levenshtein_distance_modify(a,b)

    # Gebe das Mimimum der Einzelfälle zurück:
    # - entferne letztes Zeichen von a
    # - entferne letztes Zeichen von b
    # - entferne letztes Zeichen von beiden Strings
    return min(
    levenshtein_distance(a[:-1], b) + 1, 
    levenshtein_distance(a, b[:-1]) + 1, 
    levenshtein_distance(a[:-1], b[:-1]) + cost
    )

Vollständige Implementierung (mit Rekursion)

Lösung

def levenshtein_distance_add(a, b):
    # Basisfall: leere Zeichenketten bei einem der beiden Strings
    # --> man muss die Zeichen des anderen Strings hinzufügen
    if len(a) == 0:
        return len(b)
    if len(b) == 0:
        return len(a)
    # Basisfall ist nicht eingetreten
    return None


def levenshtein_distance_modify(a, b):
    if a[-1] == b[-1]:
        # Keine Änderung des letzten Zeichens notwendig
        return 0
    else:
        # Änderung des letzten Zeichens notwendig
        return 1

def levenshtein_distance(a, b):
    cost = 0

    add_cost = levenshtein_distance_add(a, b)
    if add_cost != None:
        # Basisfall traf zu, der rekursive Aufruf wird abgebrochen
        return add_cost

    cost = levenshtein_distance_modify(a,b)

    # Gebe das Mimimum der Einzelfälle zurück:
    # - entferne letztes Zeichen von a
    # - entferne letztes Zeichen von b
    # - entferne letztes Zeichen von beiden Strings
    return min(
    levenshtein_distance(a[:-1], b) + 1, 
    levenshtein_distance(a, b[:-1]) + 1, 
    levenshtein_distance(a[:-1], b[:-1]) + cost
    )

Optimierung von rekursiven Funktionen(n)

Sie werden bemerken, dass die Funktion levenshtein_distance viel Zeit für die Berechnung der Distanz für längere Strings braucht. Beim rekursiven Aufruf kann es vorkommen, dass verschiedene Rekursionsschritte die Funktion mit den gleichen Parametern aufrufen, aber jeweils neu berechnen. Das ist sehr aufwendig, kann aber vereinfacht werden, in dem man Python anweist, Aufrufe von levenshtein_distance mit den gleichen Parametern zu cachen. Beim Caching wird die Funktion nur beim ersten Aufruf mit den gleichen Argumentwerten wirklich ausgeführt. Der Python-Interpreter merkt sich dann den Rückgabewert für diese Funktion und Argumente. Wenn wir die Funktion noch einmal mit den gleichen Argumenten aufgerufen, wird dieser Wert direkt zurückgeben. Um den Cache für den Funktionsaufruf zu aktivieren, müssen Sie nur @lru_cache(maxsize=128) (Dokumentation) als sogenannten „decorator“ vor die Funktionsdefinition schreiben. Damit werden die 128 zuletzt genutzten Rückgabewerte gecached („lru“ steht „last recently used“).

from functools import lru_cache

@lru_cache(maxsize=128)
def levenshtein_distance(a, b):
   cost = 0

   # [...] 

Kernpunkte

  • Man sollte versuchen größere Aufgaben in kleinere, einfache zu handhabene Teilaufgaben aufzuteilen

  • Funktionen sind nützlich, um diese kleineren Teilaufgaben abzubilden


Texte in NLTK

Overview

Teaching: 130 min
Exercises: 0 min
Questions
  • Wie werden Text in NLTK dargestellt?

  • Wie kann man auf diese Texte zugreifen?

  • Was kann man mit den Texten an einfachen statistischen Auswertungen durchführen?

Objectives
  • Laden der vorgefertigten NLTK Buch Beispieltexte

  • Einfache Auswertungen auf diesen Texten

Das NLTK besteht aus verschiedenen Modulen für verschiedene Aufgaben, z.B. nltk.corpora, nltk.parse oder nltk.stem. Eine Übersicht der Module finden Sie z.B. im Vorwort des NLTK-Buchs. Viele Beispiele sind auch aus dem NLTK-Buch übernommen, und es lohnt sich, als Nachbereitung die Kapitel 1 bis 3 durchzulesen.

Download von NLTK-Ressourcen

Es gibt zusätzlich eine Datenkollektion (Korpora und anderes), die für das NLTK-Buch zusammengestellt wurde. Um diese herunterzuladen, öffnen Sie eine Python-Konsole und führen Sie folgenden Code aus:

import nltk
nltk.download()

Es erscheint eine graphische Oberfläche, auf der Sie die „Collection“ „book“ auswählen müssen und dann über die Schaltfläche „Download“ herunterladen können. Danach können Sie die verschiedenen Korpora in Python importieren, in dem Sie

from nltk.book import *

ausführen.

*** 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

Nach dem Import sind die verschiedenen Korpora als Variablen mit dem Namen text1, text2 etc. vorhanden.

Frage(n)

Welchen Typ haben diese Variablen?

Lösung

Wir können den Typ einfach mit type(text1) überprüfen. Die Variablen mit den Texten haben nicht den Typ String, sondern den NLTK-eigenen Typ nltk.text.Text. Welche Methoden wir auf diesen Typ nutzen können, schauen wir uns im Rest des Tutorials an.

Texte als Sequenzen von Wörtern

Wir haben bereits Zeichenketten als Sequenzen von Zeichen betrachtet. In Korpora wird typischerweise bereits ein weiterer Vorverarbeitungsschritt vorausgesetzt, nämlich dass die Texte in Token unterteilt sind. Token sind typischerweise Wörter und Punktuationen. Ein Text ist dann eine Sequenz von Token und kann wie eine Liste interpretiert werden. Obwohl der Typ von NLTK-Texten nltk.text.Text ist, kann man die Variablen wie eine Liste von Zeichenketten nutzen.

Man kann sich zum Beispiel die ersten 10 Wörter eines Textes über die Listen-Indexierung ([n:m]) ausgeben lassen.

text4[0:10]
['Fellow', '-', 'Citizens', 'of', 'the', 'Senate', 'and', 'of', 'the', 'House']

Die Ausgabe ist wiederum von Typ list.

type(text4[0:10])
<class 'list'>

Suche in Texten

Wenn man nun nach „einen Wort in einem Text“ suchen möchte, kann das mehrere Dinge heißen:

Prüfen ob Element enthalten ist über den in Ausdruck

Mit dem Ausdruck wert in liste kann überprüft werden, ob ein Element in einer Liste vorhanden ist.

nachspeisen = ["Obst", "Quark", "Eis"]
if "Obst" in nachspeisen:
  print("Heute mal gesund.")
else:
  print("Esst mehr Obst!")

Da NLTK-Texte Listen von Zeichenketten sind, kann man mit in auch überprüfen, ob ein Wort im Text vorhanden ist.

Übung

Überprüfen Sie, ob irgendeiner Antrittsrede eines US-Präsidenten das Wort „fruit“ vorkommt.

Lösung

Ja, denn

"fruit" in text4

ergibt True.

Stelle finden, an dem ein Wort im Text vorkommt

Mit der Funktion text.index(wert) kann das erste Vorkommen eines Wortes im Text als Index der Liste gefunden werten

i = text4.index("fruit")
print(i)
print(text4[i-1], text4[i], text4[i+1])
13694
the fruit of

Zählen wie oft ein Wort im Text enthalten ist

Mit der Funktion text.count(wert) kann gezählt werden, wie oft ein Wort im Text vorkommt.

text4.count("fruit")
2

Komplexere Bedigungen abfragen

Man kann für jedes Wort des Textes eine Bedingung überprüfen, in dem man mit einer for-Schleife über die Wörter des Textes iteriert und für jedes Wort eine if-Abfrage ausführt.

from nltk.book import text4
for word in text4:
    if word.startswith("fr"):
        print(word)
*** 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
from
frequent
from
from
from
free
from
free
from
freemen
from
from
from
frankly
from
from
frontier
from
friends
from
from
fresh
from
from
from
free
[...]

Eleganter (und schneller) ist aber die Verwendung einer sogenannten „list comprehension“, einer aussagelogischen Abfrage:

[word for word in text4 if word.startswith("fr")]
['from', 'frequent', 'from', 'from', 'from', 'free', 'from', 'free', 'from', 'freemen', 'from', 'from', 'from', 'frankly', 'from', 'from', 'frontier', 'from', 'friends', 'from', 'from', 'fresh', 'from', 'from', 'from', 'free', 'fraud', 'fruits', 'from', 'free', 'from', 'friendly', 'friendly', 'friendship', 'friendship', 'fruitful', 'from', 'freely', 'from', 'free', 'free', 'from', 'from', 'from', 'from', 'frugal', 'from', 'free', 'from', 'friendship', 'from', 'freedom', 'freedom', 'freedom', 'from', 'from', 'freedom', 'from', 'from', 'friendship', 'from', 'from', 'frontiers', 'from', 'friendly', 'free', 'from', 'from', 'friends', 'from', 'freedom', 'freedom', 'from', 'friend', 'from', 'from', 'from', 'from', 'friendship', 'from', 'from', 'free', 'from', 'fruits', 'from', 'from', 'friendly', 'free', 'from', 'freedom', 'friendly', 'from', 'from', 'from', 'from', 'from', 'from', 'free', 'from', 'from', 'from', 'free', 'from', 'fraught', 'from', 'from', 'from', 'from', 'friendship', 'from', 'from', 'from', 'frontiers', 'from', 'from', 'free', 'from', 'from', 'fruit', 'from', 'friendly', 'free', 'from', 'from', 'from', 'from', 'frontiers', 'frontiers', 'from', 'from', 'front', 'from', 'from', 'from', 'from', 'from', 'from', 'from', 'from', 'friendship', 'free', 'from', 'from', 'from', 'from', 'from', 'from', 'from', 'free', 'from', 'from', 'from',
[...]

Der grundsätzliche Aufbau des Ausdrucks ist

[ausdruck for element in liste if bedingung]

Für jedes item in der liste wird der ausdruck ausgeführt, wenn die bedingung wahr ist. Das Ergebnis ist eine Liste aller Werte, die durch den Ausdruck generiert werden.

fr_woerter = [word for word in text4 if word.startswith("fr")]
print(fr_woerter)
print(type(fr_woerter))
[...]
'from', 'freedom', 'free', 'freedom', 'freedom', 'freedom', 'free', 'freedom', 'friendship', 'free', 'freedom', 'free', 'freedom', 'freedom', 'free', 'fragile', 'freedom', 'freedom', 'free', 'freedom', 'from', 'freedom', 'freedom', 'from', 'freedom', 'freedom', 'freedom', 'freedom', 'free', 'freedom', 'freedom', 'freedom', 'from', 'free', 'freedom', 'free', 'freedom', 'from', 'friend', 'from', 'friends', 'from', 'from', 'friend', 'freedom', 'freedom', 'from', 'free', 'free', 'from', 'freedoms', 'from', 'free', 'freedom', 'free', 'friendsâ', 'from', 'from', 'freedom', 'freedom', 'freedom', 'from', 'from', 'from', 'from', 'from', 'freedom', 'from', 'from', 'from', 'from', 'from', 'friendship', 'from', 'free', 'from', 'freedoms', 'from', 'from']
<class 'list'>

Ein Aufruf von print innerhalb der „list comprehension“ funktioniert daher in der interaktiven Konsole, die immer den Rückgabewert ausgibt, nur bedingt.

[print(word) for word in text4 if word.startswith("friends")]
friends
friendship
friendship
friendship
friendship
friends
friendship
friendship
friendship
friends
friendship
friendship
friendship
friends
friends
friendship
friendship
friendship
friends
friends
friends
friendship
friends
friends
friendship
friendship
friends
friends
friends
friends
friends
friends
friends
friendship
friendship
friends
friends
friendships
friends
friendship
friends
friends
friends
friends
friends
friends
friendship
friends
friends
friends
friends
friends
friendships
friends
friends
friendship
friends
friendsâ
friendship
Out[6]: 
[None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None]

„List comprehension“ Ausdrücke sind also am besten dazu geeignet, Listen zu generieren.

Man kann für die Bedingung auch reguläre Ausdrücke verwenden, wenn man das re Modul importiert und die re.search(pattern, word) Funktion verwendet.

Z.B. kann man nach allen Wörtern suchen, die mit „fruit“ anfangen, aber noch mehre Zeichen enthalten können:

import re
[w for w in text4 if re.search("fruit.*", w)]
fruits
fruitful
fruits
fruit
fruits
fruitful
fruitful
fruits
[...]

Oder z.B. die Suche nach allen Wörtern, die mit „end“ oder „begin“ enden:

[w   for w in text4 if re.search(".+(end|begin)$", w)]
recommend
depend
depend
comprehend
extend
extend
contend
tend
friend
contend
depend
depend
depend
extend
depend
[...]

Mengen

Neben Listen gibt es in Python auch Mengen, oder sogenannte Sets. Eine Menge enthält keine Duplikate, kann aber aus Listen mit der set(...) Funktion erstellt werden. Außerdem ist die Reihenfolge einer Menge nicht festgelegt.

original = ["Eis", "Obst", "Quark", "Eis"]
nachspeisen = set(original)
print(len(original), len(nachspeisen))
4 3

Im Vergleich zur originalen Liste wurde in der Menge das doppelte "Eis" entfernt und daher ist die Länge nur 3 anstatt 4.

print(nachspeisen)
{'Eis', 'Obst', 'Quark'}

Da Mengen keine Reihenfolge haben, werden neue Element mit der Funktion add(v) hinzugefügt. Mit remove(v) können sie entfernt werden.

l = set() # leere Menge
l.add(1) # wir fügen der Menge Elemente hinzu
l.add("Piano")
l.add(1.42)
l.add(1.43)
l.add(1.42)
print(l)
{1, 1.43, 1.42, 'Piano'}
l.remove("Piano")
print(l)
{1, 1.43, 1.42}

Die Funktionen in funktioniert auch auf Mengen:

1 in l
True

Ebenso gibt es die Funktion not in um zu überprüfen ob etwas nicht in der Menge enthalten ist:

13 not in l
True

Geschwindigkeit von Sets .vs Listen

Die Überprüfung, ob ein Wert Teil eines Sets ist kann deutlich schneller geschehen als in einer Liste. Während in einer Liste wirklich jedes Element angeschaut werden muss, gibt es für Sets Indexstrukturen, die annähernd konstant und unabhängig von der Länge der Liste die (Nicht)-Zugehörigkeit eines Werts überprüfen können. Insbesondere bei großen Datenmengen kann es also von Vorteil sein ein Set zu nutzen, soweit es semantisch Sinn macht.

Frage(n)

Ist der Ausdruck "1.42" in l für die Menge {1, 1.43, 1.42} wahr?

Lösung

Nein, denn "1.42" ist ein String und das Element in der Menge ist eine Ganzkommazahl. Elemente müssen vom Typ gleich sein um verglichen werden zu können. Mit Hilfe der Typumwandlungsfunktion (str(v),int(v),float(v), etc.) können solche Umwandlungen vorgenommen werden. Für einige Typen geschieht die Umwandlung automatisch, so ist z.B. 1.0 in l wahr, da ein Float zu einem Integer automatisch umgewandelt wird.

Wenn man Texte mit einer komplexen Bedingung durchsucht, kann man die daraus entstandene Liste zu einer Menge machen, um jeder Variante nur noch einmal vorkommen zu lassen:

import re
from nltk.book import text4

words = [w for w in text4 if re.search(".+(end|begin)$", w)]
words = set(words)
print(words)
{'comprehend', 'lend', 'recommend', 'tend', 'dividend', 'Godsend', 'attend', 'send', 'spend', 'rend', 'trend', 'friend', 'Reverend', 'apprehend', 'extend', 'contend', 'reverend', 'legend', 'intend', 'transcend', 'blend', 'depend', 'defend', 'superintend', 'pretend', 'bend'}

Konkordanz auf NLTK-Texten

Das NLTK hat eine eigene Konkordanzfunktion:

text4.concordance("fruit")
Displaying 2 of 2 matches:
as we do all the raw materials , the fruit of our own soil and industry , we ou
ce will grow greater and bear richer fruit with the coming years . No doubt thi

Damit können die gefunden Stellen gemeinsam mit ihren Kontext angezeigt werden. In diesem Fall ist der Kontext durch die Wörter/Zeichen links und rechts vom Treffer gegeben.

Die Konkordanzfunktion arbeitet auf tokenisiertem Text - sie kann deshalb nicht über mehrere Wörter suchen:

text4.concordance("fruit of")
no matches

liefert keine Matches, weil es das Token „fruit of“ nicht gibt!

Die Konkordanzfunktion funktioniert nur auf der Datenstruktur Text, die vom NLTK vorgegeben wird.

Übung

Wie können wir uns den Kontext für das Ergebnis einer Mustersuche ausgeben lassen?

Lösung

words = set([w for w in text4 if re.search(".+(end|begin)$", w)])
for w in words:
    text4.concordance(w)

Vorkommen eines Worts im Text

Wenn man eine Übersicht haben möchte, an welchen Stellen ein Wort im Text vorkommt, kann man einen sogenannten „dispersion plot“ nutzen:

text4.dispersion_plot(["citizens", "democracy", "freedom", "duties", "America"])

Um die Methode zu nutzen, muss das Paket “matplotlib” installiert sein.

pip install matplotlib

Einfache statistische Auswertungen

Wortfrequenzen

Um die Frequenz der Wörter für ein Korpus berechnen, kann man mit der Funktion FreqDist(text) ein neues Objekt anlegen, dass die Vorkommen zählt und die Möglichkeit gibt, auf verschiedene Aspekte der Frequenzanalyse zuzugreifen.

fdist1 = FreqDist(text1)
print(fdist1)
<FreqDist with 19317 samples and 260819 outcomes>

Mit der Methode .most_common(n) kann man sich jetzt die n häufigsten Wörter ausgeben lassen.

fdist1.most_common(50)
[(',', 18713), ('the', 13721), ('.', 6862), ('of', 6536), ('and', 6024), ('a', 4569), ('to', 4542), (';', 4072), ('in', 3916), ('that', 2982), ("'", 2684), ('-', 2552), ('his',2459), ('it', 2209), ('I', 2124), ('s', 1739), ('is', 1695), ('he', 1661), ('with', 1659), ('was', 1632), ('as', 1620), ('"', 1478), ('all', 1462), ('for', 1414), ('this', 1280), ('!', 1269), ('at', 1231), ('by', 1137), ('but', 1113), ('not', 1103), ('--', 1070), ('him', 1058), ('from', 1052), ('be', 1030), ('on', 1005), ('so', 918), ('whale', 906), ('one', 889), ('you', 841), ('had', 767), ('have', 760), ('there', 715), ('But', 705), ('or', 697), ('were', 680), ('now', 646), ('which', 640), ('?', 637), ('me', 627), ('like',624)]

Über einen Indexzugriff kann man auch die Häufigkeit eines gegebenen Wortes ausgeben:

fdist1['whale']
906

Man kann die Frequenzen auch wieder plotten lassen:

print(len(text1))
fdist1.plot(50)
260819

Wenn man die Häufigkeiten der häufigsten 50 Wörter aufaddiert kann man sehen, dass diese fast die Hälfte des Gesamttextes umfassen.

print(len(text1))
fdist1.plot(50, cumulative=True)
260819

Durchschnittliche Wortlänge

Man kann mit FreqDist() auch andere Dinge berechnen, wie z.B. die Verteilung der Wortlänge eines Textes. Dazu erstellen wir zuerst ein neue List, in der für jedes Wort des Textes die Länge eingetragen ist.

Übung

Erstellen Sie eine Liste mit den Wortlängen des Textes text1!

Lösung

l = [len(w) for w in text1]
print(l)
[1, 4, 4, 2, 6, 8, 4, 1, 9, 1, 1, 8, 2, 1, 4, 11, ...]

Jetzt können wir die Frequenz der verschiedenen Wortlängen direkt berechnen:

ldist = FreqDist(l)
print(ldist.most_common(10))
ldist.plot()
[(3, 50223), (1, 47933), (4, 42345), (2, 38513), (5, 26597), (6, 17111), (7, 14399), (8, 9966), (9, 6428), (10, 3528)]

Wir können auch direkt die häufigste Wortlänge mit der Methode .max() berechnen lassen:

ldist.max()
3

Kollokation

Kollokationen sind Sequenzen von ungewöhnlich häufig zusammen auftretenden Wörtern. Sequenzen von Wörtern, bei denen jedes einzelne Wort bereits häufig auftritt sind keine auffälligen Kollokationen, z.B. sind „the“ und „wine“ in einem Text vielleicht einzeln schon sehr häufig, deswegen ist klar, dass „the wine“ ebenfalls häufig auftritt und keine Kollokation. Dagegen ist „red wine“ eine, da die Sequenz der beiden Wörter häufiger auftritt als die einzelne Frequenz der beiden Wörter dies erwarten lassen würde.

Ein Beispiel für die Frequenz von „the“, „wine“ und „red“ in einem der Texte:

print(fdist1["the"])
print(fdist1["wine"])
print(fdist1["red"])
13721
12
36

Nun ist die Frage, wie man die Häufigkeit von „red wine“ finden kann. Als erstes muss man eine Liste von sogenannten Bigrammen erstellen, also alle Paare von Wörtern die jeweils als Sequenz im Text vorkommen. Bigramme können mit Methode bigrams(text) erstellt werden.

for b in bigrams(text1):
  print(b)
('Nantucket', 'commanders')
('commanders', ';')
(';', 'that')
('that', 'from')
('from', 'the')
('the', 'simple')
('simple', 'observation')
('observation', 'of')
('of', 'a')
[...]

Die einzelnen Bigramme sind vom Typ Tupel, den wir bisher noch nicht kennengelernt haben. Tupel sind ähnlich wie Listen, allerdings ist die Anzahl der Element fest und es können nicht neue Element angehängt oder bestehende gelöscht werden. Ein Tupel-Wert kann in Python mit Hilfe von Klammern angelegt werden.

t = ("red", "wine")
type(t)
print(t)
print(t[0])
print(t[1])
<class 'tuple'>
('red', 'wine')
'red'
'wine'

Listen von Bigrammen können nun ähnlich wie Listen von Wörtern behandelt werden, indem z.B. Frequenzlisten davon erstellt und ausgewertet werden.

bf = FreqDist(bigrams(text1))
print(bf.most_common(10))
print(bf[("red", "wine")])
print(bf[("the", "wine")])
[((',', 'and'), 2607), (('of', 'the'), 1847), (("'", 's'), 1737), (('in', 'the'), 1120), ((',', 'the'), 908), ((';', 'and'), 853), (('to', 'the'), 712), (('.', 'But'), 596), ((',', 'that'), 584), (('.', '"'), 557)]
1
0

Während „the“ häufiger vorkommt als „red“, gibt es kein einziges Bigramm „the wine“ in diesem Text, während „red wine“ einmal vorkommt. Damit ist „red wine“ eine Kollokation.

Nun kann man für einen Text direct mit der Methode .collocation_list() häufige Bigramme finden.

text4.collocation_list()
United States; fellow citizens; four years; years ago; Federal
Government; General Government; American people; Vice President; Old
World; Almighty God; Fellow citizens; Chief Magistrate; Chief Justice;
God bless; every citizen; Indian tribes; public debt; one another;
foreign nations; political parties
text8.collocation_list()
would like; medium build; social drinker; quiet nights; non smoker;
long term; age open; Would like; easy going; financially secure; fun
times; similar interests; Age open; weekends away; poss rship; well
presented; never married; single mum; permanent relationship; slim
build

Kernpunkte

  • Texte sind Sequenzen von Token und können wir Listen von Strings behandelt werden

  • Es ist in NLTK direkt möglich, Frequenzanalysen, Kollokationen und andere einfache statistische Auswertungen auf Texten durchzuführen.


Dateien lesen und schreiben

Overview

Teaching: 10 min
Exercises: 30 min
Questions
  • Wie können Dateien durch Python verarbeitet werden?

Objectives
  • Ein- und Auslesen von Dateien

Daten von der Festplatte laden und wieder speichern

Wenn wir unsere linguistische Forschung mit technischen Mitteln unterstützen wollen, kommen wir nicht umhin gespeicherte Sprachdaten zu laden und berechnete Auswertungen und/oder Aufbereitungen zu speichern. Unsere Berechnungen und Variablenbelegungen zur Laufzeit des Programmes sind im Hauptspeicher des Computers gespeichert. Dieser ist flüchtig. Persitenz bietet nur die Festplatte.

Lesen

Also, beginnen wir mit dem einfachen Laden. Hierfür benötigen wir open (schauen Sie für die Beschreibung der verwendeten Funktionen auch in die Python-Hilfe):

f1 = open('demofile.txt', 'r')
content1 = f1.read()
# ...

f2 = open('demofile.txt', 'r')
content2 = f2.readlines()
# ...

Frage(n)

  • Was passiert in jeder Zeile des Programmes?
  • Welchen Typ hat f und welchen Typ hat content1 bzw. content2?
  • Was fehlt noch? Warum ist das wichtig?
  • Was ist der Unterschied zwischen read und readlines?

Lösung

Die Funktion open öffnet die Datei im Lesemodus ("r" als Wert für den Modus-Parameter) und liest dann den Inhalt mit den Methoden read() bzw. readlines() in Variablen. read() liest die gesamte Datei als einen String und liefert einen Wert vom Typ str zurück. readlines() liest die Datei zeilenweise und liefert eine Liste mit Strings zurück. Es fehlt noch das Schließen der Dateien mit der Methode close().

f1.close()

Das Schließen von Datein ist notwendig, wenn die gleiche Datei von mehreren Programmen oder Programmteilen genutzt werden soll. Insbesondere beim mischen von Lesen und Schreiben in Dateien kann es zu Problemen kommen, wenn gleichzeitig zwei Dateivariablen offen sind.

Schreiben

Wenn wir schreiben wollen, dann geht das so:

f = open('outfile.txt', 'w')
content = 'blablabla'
f.write(content)
f.close()

Frage(n)

Existiert outfile.txt bereits? Was kann passieren, wenn wir f nicht schließen?

Lösung

Die Datei muss nicht bereits existieren, das Programm wird die Datei neu anlegen und überschreiben. Wenn f nicht geschlossen wird, wird zwar auch die Datei angelegt, aber es kann passieren, dass der Inhalt nicht oder nicht vollständig geschrieben wird.

Einen Datei manuell in allen möglichen Code-Pfaden zu schließen kann man oft vergessen, in Python gibt es daher einen speziellen with Block, der uns das vereinfacht:

with open("outfile.txt","a") as f
  f.write("hello world!\n")
  # wird nach diesem Block automatisch geschlossen

Tipps und Tricks

Wenn Sie vermeiden wollen, dass Sie eine Datei überschreiben, die es schon gibt, gibt es mehrere Mittel. Zum einen können Sie einen anderen Schreibmodus verwenden:

f = open('demofile.txt', 'x')
# ...
f.close()

Der Modus x schreibt in eine neue Datei. Existiert die Datei jedoch schon, wird ein sogenannter FileExistsError ausgegeben und das Programm beendet.

Das ist jedoch nicht die eleganteste Lösung. Solche Exceptions und Errors (mehr dazu in einer späteren Sitzung) sollten vermieden werden, wo es möglich ist.

Alternativ gibt es im Modul os (Submodul path) eine Funktion exists:

from os.path import exists
print(exists('demofile.txt'))

Übung

Wir wollen alle Nomen aus einer Geschichte und in eine Datei schreiben finden. Dafür brauchen wir

  • eine Liste von Wörtern, die wir aus einem String (der Geschichte) generieren
  • eine Definition von Nomen - wir gehen vereinfacht davon aus, dass alle großgeschriebenen Wörter außer am Satzanfang Nomen sind

Als Geschichte nehmen wir die Zusammenfassung des Inhalts von hier: http://de.wikipedia.org/wiki/Der_Hase_und_der_Igel

Lösung

# Unsere Funktion nimmt eine Liste als Parameter
def find_nouns(list_of_words):
    nouns = list()
    # Das erste Wort ist wahrscheinlich großgeschrieben, fällt aber aus unserer Definition raus
    for i in range(1, len(list_of_words)):
    	current_word = list_of_words[i]
    	if current_word[0].isupper():
            # list_of_words[i-1]: Das vorherige Wort
            if not list_of_words[i-1].endswith("."):
                nouns.append(current_word)
    return nouns


with open("hase_igel.txt") as f:
    story = f.read()
    words = story.split()
    nouns = find_nouns(words)

with open("hase_igel_nouns.txt", "w") as result:
    for noun in nouns:
        result.write(noun + ", ")

Kernpunkte

  • Mit den eingebauten Funktionen open können Dateien zum Lesen und Schreiben geöffnet werden, diese müssen immer wieder geschlossen werden.


NLTK und eigene Texte

Overview

Teaching: 50 min
Exercises: 10 min
Questions
  • Wie kann ich meine eigenen Texte in NLTK einbinden?

Objectives
  • Tokensieren von Texten

  • Auf Argumente der Kommandozeile zugreifen

Einlesen eines eigenen Texts in NLTK

Wir wollen einen Beispieltext als Text in NLTK laden. Dafür müssten wir als erstes eine Liste von allen Wörtern des Textes in der richtigen Reihenfolge erstellen. Das heißt, dass wenn wir den Text zeilenweise einlesen, die Zeichenketten auftrennen müssen. Eine einfache Methode dazu bietet die eingebaute split() Methode von Python. Diese Methode kann aber z.B. nicht mit Punktuation umgehen.

feedback = "Die positivste Reaktion, die man in Berlin bekommen kann, ist eine fehlende Beschwerde."
feedback.split()
['Die', 'positivste', 'Reaktion,', 'die', 'man', 'in', 'Berlin', 'bekommen', 'kann,', 'ist', 'eine', 'fehlende', 'Beschwerde.']

NLTK bietet eine Funktion word_tokenize(text), die diese Aufgabe besser löst.

from nltk import word_tokenize
word_tokenize(feedback)
['Die', 'positivste', 'Reaktion', ',', 'die', 'man', 'in', 'Berlin', 'bekommen', 'kann', ',', 'ist', 'eine', 'fehlende', 'Beschwerde', '.']

Die Funktion nltk.Text erstellt aus so einer Liste von Wörtern dann einen neuen Text, der genau so wie alle anderen Texte genutzt werden kann.

txt_de = nltk.Text(word_tokenize(feedback))
txt_de.concordance("Reaktion")
nltk.FreqDist(txt_de).plot()
Displaying 1 of 1 matches:
Die positivste Reaktion , die man in Berlin bekommen kann ,

Eine Möglichkeit, Text weiter zu verarbeiten ist das Tagging jedes Tokens nach Wortarten. Dazu erstellen wir erst einen Text aus einen einfachen englischen Beispielsatz.

txt_en = nltk.Text(word_tokenize("The most positive reaction you can get in Berlin is a missing complaint."))
txt_en.tokens
['The', 'most', 'positive', 'reaction', 'you', 'can', 'get', 'in', 'Berlin', 'is', 'a', 'missing', 'complaint', '.']

Die Funktion nltk.pos_tag(text) erlaubt jetzt das taggen eines englischen Texts:

nltk.pos_tag(txt_en)
[('The', 'DT'), ('most', 'RBS'), ('positive', 'JJ'), ('reaction', 'NN'), ('you', 'PRP'), ('can', 'MD'), ('get', 'VB'), ('in', 'IN'), ('Berlin', 'NNP'), ('is', 'VBZ'), ('a', 'DT'), ('missing', 'JJ'), ('complaint', 'NN'), ('.', '.')]

Das Ergebnis ist eine Liste von Tupeln, wobei das erste Element das Token ist und das zweite der Tag für die Wortklasse. Das hier verwendete Tagset ist as UPenn Tagset. Mit nltk.help.upenn_tagset(tag) kann man für eine einzelnes Tag die Beschreibung bekommen.

nltk.help.upenn_tagset('NN')
NN: noun, common, singular or mass
    common-carrier cabbage knuckle-duster Casino afghan shed thermostat
    investment slide humour falloff slick wind hyena override subhumanity
    machinist ...

Wir werden uns später damit beschäftigen, wie diese Tagger funktionieren. Tagsets können über den optionalen Parameter tagset ausgewählt werden, z.B. gibt es auch das „Universal POS Tagset

nltk.pos_tag(txt_en, tagset="universal")
[('The', 'DET'), ('most', 'ADV'), ('positive', 'ADJ'), ('reaction', 'NOUN'), ('you', 'PRON'), ('can', 'VERB'), ('get', 'VERB'), ('in', 'ADP'), ('Berlin', 'NOUN'), ('is', 'VERB'), ('a', 'DET'), ('missing', 'ADJ'), ('complaint', 'NOUN'), ('.', '.')]

Um den Tagger für eine andere Sprache zu nutzen, muss das Argument lang mit angeben werden. Als Argument muss der aus drei Buchstaben bestehende ISO 639 Code genutz werden, z.B. für Deutsch:

nltk.pos_tag(txt_de, tagset="universal", lang="deu")
NotImplementedError: Currently, NLTK pos_tag only supports English and Russian (i.e. lang='eng' or lang='rus')

In älteren Versionen von NLTK war ein sehr rudimentäre Tagger für Deutsch enthalten. Dieser wurde allerdings entfernt. Für das Beispiel hätte der alte POS-Tagger

[('Die', 'NOUN'), ('positivste', 'NOUN'), ('Reaktion', 'NOUN'), (',', '.'), ('die', 'NOUN'), ('man', 'NOUN'), ('in', 'ADP'), ('Berlin', 'NOUN'), ('bekommen', 'NOUN'), ('kann', 'VERB'), (',', '.'), ('ist', 'ADJ'), ('eine', 'NOUN'), ('fehlende', 'NOUN'), ('Beschwerde', 'NOUN'), ('.', '.')]

ausgegeben. Wir Sie sehen, war der Tagger für Deutsch ausbaufähig und seine Entfernung kein großer Verlust.

Kommandozeilenargumente

Wenn man ein Skript in der System-Konsole aufruft, kann man dem Aufruf zusätzliche per Leerzeichen getrennt Argumente übergeben.

python3 meinskript.py Argument1 NochEinArgument

Ähnlich wie bei einer Python-Funktion ist das sehr nützlich, um die gleiche Aufgabe bzw. den gleichen Algorithmus/Code auf unterschiedlichen Eingaben auszuführen. Wenn ein Skript eine Datei einliest, wäre das z.B. eine gute Möglichkeit den Dateipfad anzugeben, damit man das Skript auf verschieden Dateien ausführen kann ohne das Skript selbst fehleranfällig anpassen zu müssen.

Der Zugriff auf die Argumente erfolgt über die Liste argv aus dem Modul sys. Dazu müssen Sie das Modul erst importieren, und können dann auf die Listen-Variable zugreifen. Gegeben Sei das folgendes Skript in einer Datei cat.py.

import sys

print(sys.argv)

Wenn man das Skript nun mit verschiedenen Parametern auf dem System-Terminal ausführt ausführt, kann man sehen wie sich die Ausgabe verändert.

python3 cat.py
['cat.py']
python3 /home/nlp/cat.py
['/home/nlp/cat.py']

Das erste Element in der Liste sys.argv ist immer der Pfad, mit dem das Skript aufgerufen wurde, danach folgenden die anderen Argumente:

python3 cat.py Argument1 NochEinArgument
['cat.py', 'Argument1', 'NochEinArgument']

Wenn Sie z.B. das Skript cat.py abändern, können Sie den Inhalt einer beliebigen Datei, die als Argument übergeben wird, als Text ausgeben:

import sys

if len(sys.argv) < 2:
    print("No file given")
else:
    with open(sys.argv[1], 'r') as f:
        print(f.read())

Die Abfrage, ob die Argumentliste groß genug ist fängt den Fall ab, dass kein Argument angegeben worden ist. Sie können jetzt das Skript austesten:

python3 cat.py hase_igel.txt
Bei einer zufälligen Begegnung macht sich der Hase über die schiefen Beine des Igels lustig, woraufhin ihn dieser zu einem Wettrennen herausfordert,um den Einsatz eines goldenen „Lujedor“ (Louis d’or) und einer Flasche Branntwein. Bei der späteren Durchführung des Rennens auf einem Acker läuft der Igel nur beim Start ein paar Schritte, hat aber am Ende der Ackerfurche seine ihm zum Verwechseln ähnlich sehende Frau platziert. Als der siegesgewisse Hase heranstürmt, erhebt sich die Frau des Igels und ruft ihm zu: „Ick bün al hier!“ („Ich bin schon hier!“). Dem Hasen ist die Niederlage unbegreiflich, er verlangt Revanche und führt insgesamt 73 Läufe mit stets demselben Ergebnis durch. Beim 74. Rennen bricht er erschöpft zusammen und stirbt.
python3 cat.py
No file given
python3 cat.py nichtda.txt
Traceback (most recent call last):
  File "cat.py", line 6, in <module>
    with open(sys.argv[1], 'r') as f:
FileNotFoundError: [Errno 2] No such file or directory: 'nichtda.txt'

Übung

Schreiben Sie das Skript so um, dass es den Fehlerfall von nicht vorhanden Dateien auch abfängt.

Lösung

import sys
from os.path import exists

if len(sys.argv) < 2:
    print("No file given")
else:
    if exists(sys.argv[1]):
        with open(sys.argv[1], 'r') as f:
            print(f.read())
    else:
        print("Datei existiert nicht!")

python3 cat.py nichtda.txt
Datei existiert nicht!

Kernpunkte

  • Listen von Strings können mit nltk.Text() zu einem NLTK-Text umgewandelt werden.

  • Kommandozeilenargumente können über die Liste sys.argv abgerufen werden. Das erste Element ist immer der Aufrufpfad des Skripts.


POS-Tagger trainieren

Overview

Teaching: 0 min
Exercises: 0 min
Questions
  • Wie kann ein POS-Tagger in NLTK trainiert werden?

Objectives
  • Schritte des Lernverfahrens verstehen

RIDGES Korpus

Bitte laden Sie die Datei ridges8_gold.txt aus dem Moodle-Kurs herunter. Dieses ist ein Subset aus dem RIDGES Korpus mit manuell korrigierten POS-Tags im STTS Tagset

Die Datei besteht aus Sätzen, wobei jeder Satz in einer Zeile geschrieben ist. Tokens sind durch Leerzeichen abgetrennt und das POS-Tag ist mit / an jedes Token angeängt:

Die/ART Römer/NN nennen/VVFIN es/PPER Pers[('Politik', 'NN'), ('ist', 'VAFIN'), ('ein', 'ART'), ('schweres', 'ADJA'), ('Feld', 'NN'), ('.', '$.')]a/FM ./$.

Dieses Dateiformat kann direkt von NLTK durch den TaggedCorpusReader eingelesen werden.

RIDGES ist ein frei verfügbares Korpus bestehend aus deutsprachige Kräuterkundetexte aus historischen Sprachstufen. Wir wollen auf diesem Goldstandard einen Hidden-Markov-basierten POS-Tagger trainieren.

Dazu müssen wir erst einmal ein paar Module aus NLTK importieren.

import nltk
import nltk.corpus.reader.tagged as tagged
import nltk.tag.hmm as hmm

Dann laden wir die Datei mit dem TaggedCorpusReader. Es gibt noch weitere Korpusreader, die andere Formate verstehen. Das erste Argument ist das Arbeitsverzeichnis, das zweite eine Liste von Dateien.

gold_corpus = tagged.TaggedCorpusReader(".", ["ridges8_gold.txt"])

Mit der Methode tagged_words() kann man sich eine Liste aller Token und deren Tags ausgeben (als Tupel) lassen.

for w in gold_corpus.tagged_words():
    print(w)

Die Methode tagged_sents() liefert eine Liste von Sätzen zurück. Dabei ist jeder Satz wieder selbst eine Liste von Wörtern.

print(gold_corpus.tagged_sents()[5])

Nun legen wir ein neues HMM Trainings-Objekt mit Standard-Parametern an und trainieren es auf den getagged Sätzen.

trainer = hmm.HiddenMarkovModelTrainer()
model = trainer.train(gold_corpus.tagged_sents())

Die train() Funktion liefert ein Modell aus Objekt zurück. Dieses können wir jetzt nutzen, um eigene Sätze zu taggen.

print(model.tag(nltk.word_tokenize("Majoran ist frisch")))
print(model.tag(nltk.word_tokenize("Majoran ist cool")))
[('Majoran', 'NN'), ('ist', 'VAFIN'), ('frisch', 'ADJD')]
[('Majoran', 'NN'), ('ist', 'VAFIN'), ('cool', 'NN')]

Modelle können mit der Dill-Bibliothek in Dateien gespeichert und wieder geladen werden. Diese muss erst mit pip in einer Konsole mit aktivierter Conda-Umgebung installiert werden.

pip install dill

Dann kann man ein Modell folgendermaßen speichern:

import dill
dill.dump( model, open( "ridges.model", "wb" ) )
new_model = dill.load( open( "ridges.model", "rb" ))
new_model.tag(nltk.word_tokenize("Die Römer nennen es Majoran"))
[('Die', 'ART'), ('Römer', 'NN'), ('nennen', 'VVFIN'), ('es', 'PPER'), ('Majoran', 'NN')]

Tiger2 Korpus

Das Tiger2-Korpus basiert auf Zeitungstexten und wurde ebenfalls mit dem STTS annotiert. Laden Sie das Korpus “TIGER Corpus Release 2.2” in dem CONLL09-Format von der Tiger2-Webseite herunter: https://www.ims.uni-stuttgart.de/forschung/ressourcen/korpora/tiger/

Das CONLL-Format enthält Informationen über Dependenz-Strukturen, die wir im Moment nicht benötigen. Es kann aber analog zur Textdatei in NLTK mit dem nltk.corpus.ConllCorpusReader eingelesen werden, dabei muss mit angegeben werden welche Spalten als word oder pos genutzt und welche ignoriert werden sollen.

tiger_corpus = nltk.corpus.ConllCorpusReader('.', 'tiger_release_aug07.corrected.16012013.conll09',
                                     ['ignore', 'words', 'ignore', 'ignore', 'pos'],
                                     encoding='utf-8')

Nun kann wieder analog zum RIDGESS Korpus mit den annotierten Daten trainiert und getagged werden. Das Trainieren kann etwas dauern, da Tiger2 deutlich größer ist als der Ausschnitt aus dem RIDGES Korpus.

tiger_model = trainer.train(tiger_corpus.tagged_sents())
tiger_model.tag(nltk.word_tokenize("Politik ist ein schweres Feld."))
[('Politik', 'NN'), ('ist', 'VAFIN'), ('ein', 'ART'), ('schweres', 'ADJA'), ('Feld', 'NN'), ('.', '$.')]

Kernpunkte

  • Mit dem HiddenMarkovModelTrainer aus dem nltk.tag.hmm Modul können eigene HMM-basierte POS-Tagger trainiert werden

  • Mit der dill-Bibliothek können diese Modelle gespeichert und wieder geladen werden.


Textklassifzierung mit Tensorflow

Overview

Teaching: 0 min
Exercises: 0 min
Questions
  • Wie kann man mit Hilfe von neuronalen Netzen Texte klassifizieren?

Objectives
  • Tensorflow-API mit Python ansprechen

  • Daten für Tensorflow aus CSV Dateien einlesen und aufbereiten

Einführung

Textklassifizierung kann für verschiedene Bereiche eingesetzt werden, um Texte Kategorien zuzuweisen. Beispiele dafür sind

Bag-Of-Words

Textklassifikation hat den gesamten Text als Eingabe und die Klasse als Ausgabe. Neuronale Netzwerke haben aber Vektoren von Zahlen als Ein- und Ausgabe. Um die Ausgabe zu kodieren, kann man Vektoren nutzen, die die gleiche Länge wie die Anzahl der Klassen haben. Jeder Klasse wird dann ein Index des Ausgabe-Vektors zugeordnet. Nach Berechnung der Ausgabe des neuronalen Netzwerk ist dann die Klasse, die dem Index mit dem höchsten Wert im Vektor zugeordnet ist die erkannte Klasse. Wenn man zum Beispiel zwischen den Sprachen „Englisch“, „Deutsch“, „Französisch“ unterscheiden möchte, hat man 3 Klassen und damit Vektor der Länge drei. Wenn wir also zum Beispiel die Ausgabe (0.13, 0.99, 0.4) haben und Englisch dem ersten Index, Deutsch dem zweiten und Französisch dem dritten Index zugeordnet ist, dann wurde vom neuronalen Netz die Klasse „Deutsch“ vorhergesagt. Im Fall, dass man nur zwei Klassen hat (z. B. negativ und positiv) kann man auch nur einen einzelnen Wert aus Ausgabe nehmen, der dann 0 für die erste Klasse und 1 für die zweite Klasse ist.

Für die Eingabe, also den eigentlichen Text, müssen wir aber auch eine geeignete Vektordarstellung finden. Dazu nutzen wir in diesem Tutorial sogenannte Bag-Of-Words. Diese Darstellung reduziert den Text auf die Anzahl der genutzten Wörter aus einem vorher festgelegten Vokabular. Für jedes Wort aus dem Vokabular wir die Häufigkeit des Vorkommens bestimmt. Der Kontext oder die Position des Worts im Text geht dabei verloren.

Der Beispieltext

By the time the film ended, I not only disliked it, I despised it.

würde sich folgende Darstellung ergeben:

word #
by 1
the 2
time 1
film 1
ended 1
, 2
i 2
not 1
only 1
disliked 1
it 2
despised 1
. 1

Klassifzierung nach Sentiment

In diesem Tutorial wollen wir Texte nach ihrer Haltung (Sentiment) klassifizieren. Dieses Beispiel ist adaptiert von https://realpython.com/python-keras-text-classification/. Als Daten nutzen wir Kommentare aus der Internet Movie Database1. Bitte laden Sie den „Data Folder“ von der Homepage des Datasets herunter: https://archive.ics.uci.edu/ml/datasets/Sentiment+Labelled+Sentences und entpacken Sie die Datei sentiment labelled sentences.zip. Wir verwenden im Tutorial die Datei imdb_labelled.txt. In dieser Datei sind zeilenweise IMDB-Kommentare mit der Annotation versehen, ob diese negativ (0) oder positiv (1) sind.

Der Kommentar

A very, very, very slow-moving, aimless movie about a distressed, drifting young man.

wurde dabei manuell mit dem Ausgabewert 0 markiert (ist also negativ) während z. B. folgender Kommentar den Wert 1 für positiv bekommen hat:

The best scene in the movie was when Gerardo is trying to find a song that keeps running through his head.

Unsere Aufgabe ist es nun, diese beiden Klassen automatisch mit einem neuronalen Netz auf ungesehener Eingabe zu unterscheiden. Wir wollen also auf den Goldstandard-Daten der IMDB-Kommentare das Netzwerk trainieren und auf neuen Daten anwenden können. Dafür sind mehrere Schritte, von der Aufbereitung der Daten bis zur Definition der Netzwerkstruktur, dem eigentlichen Trainieren und Code zum Ausführen des trainierten Netzwerks notwendig.

Tensorflow (mit Keras API)

Für dieses Beispiel nutzen wir Tensorflow mit der Keras API. Tensorflow kann über pip installiert werden.

pip install tensorflow

Einlesen der CSV Dateien

Der erste Schritt ist das Einlesen der Trainingsdaten. Die Datei imdb_labelled.txt ist eine CSV- bzw. TSV-Datei: jede Zeile ist ein Datenpunkt und die Informationen in den Zeilen sind durch Tabs separiert (TSV steht für „Tab separated values“, „CSV“ dementsprechend für „Comma separated values“, wird aber oft als Oberbegriff für alle solche Formate unabhängig vom Trennzeichen verwendet). Wir definieren dazu die Funktion read_csv, die das Einlesen übernimmt und eine Liste von Sätzen/Kommentaren und eine Liste der Kategorien (also die „labels“) zurückgibt. Diese Funktion (und alle anderen Hilfsfunktionen) werden im Modul util definiert, müssen also in einer Datei mit dem Namen util.py gespeichert werden.

# Liest eine CSV-Datei mit Sätzen und Labeln (0 negativ, 1 positiv) ein
def read_csv(file):
    sentences = []
    labels = []
    
    with open(file, "r") as f:
        # Jede Zeile der Datei einlesen
        for line in f.readlines():
            # Zeile in zwei Teile spalten (durch Tab getrennt)
            splitted = line.split('\t')
            # Den Satzteil und das Score jeweils an die entsprechende Liste anhängen
            sentences.append(splitted[0])
            # Das Label muss zu einer Zahl konvertiert werden 
            labels.append(float(splitted[1]))
    # Beide Listen zurückgeben
    return (sentences, labels)

Erstellen eines Vokabulars

Für die Bag-Of-Words-Repräsentation benötigen wir ein Vokabular. Dazu implementieren wir folgene Funkion, die ein Vokabular der 999 häufigsten Wörter aus einer gegebenen Liste von Sätzen berechnet.

# Erstellt eine Zuordnung von den 999 häufigsten Wörtern zu einer ID
def create_vocabulary(sentences):
    # Erstelle eine Listen mit allen Wörtern
    # Ein Wort kann mehrfach vorkommen, ist aber immer klein geschrieben
    all_words = list()
    for s in sentences:
        for token in nltk.tokenize.word_tokenize(s):
            all_words.append(token.lower())
    # Erstelle  eine Frequenzliste aller Wörter
    fdist = nltk.FreqDist(all_words)
    
    # Erstelle eine Zuordnung von den 999 häufigsten Wörtern zu einer ID
    vocabulary = dict()
    # Die ID 0 wird freigelassen für unbekannte Wörter
    ident = 1
    for word, _ in fdist.most_common(999):
        vocabulary[word] = ident
        ident = ident + 1
    
    return vocabulary

Erstellen eines Bag-Of-Words für jeden Satz

Als nächsten Schritt definieren eine Funktion, die die eigentlichen Bag-Of-Words für alle Sätze gegeben des Vokabular berechnet. Da das Vokabular nicht vollständig ist, werden alle unbekannten Wörter mit der speziellen ID 0 kodiert.

# Erstellt eine Liste von Bag-Of-Words-Vektoren für eine Liste von Sätzen.
def create_bag_of_words(sentences, vocabulary):
    result = []
    for s in sentences:
        # Erstelle Liste mit nullen
        # Die Liste ist so lang wie es Einträge im Vokuabular gibt (plus 1 für die 0)
        # Jedes Listenelement entspricht der Anzahl der Token für die ID an dieser Position
        v = [0]*(len(vocabulary)+1)
        for token in nltk.tokenize.word_tokenize(s):
            token = token.lower()
            # Unbekannte Wörter bekommen die ID 0
            identifier = 0
            if token in vocabulary:
                # Schlage die ID für bekannte Wörter im Vokabular nach
                identifier = vocabulary[token]
            # Erhöhe die Anzahl der Wörter mit dieser ID um 1
            v[identifier] = v[identifier] + 1
        # Hänge den Bag-Of-Word Vektor für diesen Satz an das Ergebnis an
        result.append(v)
    # Gebe die Bag-Of-Word Vektoren als Liste zurück
    return result

Aufbereitung der Daten

Um beurteilen zu können, wie gut das neuronale Netz gelernt hat, trennen wir die Daten in eine Trainingsdatenmenge und eine Testdatenmenge auf. Die Performanz (also Anzahl korrekter Vorhersagen) wird dann auf den ungesehenen Testdaten berechnet.

Aus den Trainingsdaten berechnen wir das Vokabular: die Testdaten dürfen nicht ins Vokabular eingehen, da diese ja ungesehene neue Daten repräsentieren. Für beiden Datenmengen wird mit dem Vokabular für jeden Satz eine Bag-Of-Words Vektorrepräsentation erstellt.

import util
import dill

# CSV Datei einlesen
sentences, y = util.read_csv("imdb_labelled.txt")

# Aufspalten in Training und Testmengen
# 75% der Daten zum Training, 25% zum Testen
split_position = int(len(sentences) * 0.75)
# Trainingsdaten
sentences_train = sentences[:split_position]
y_train = y[:split_position]
# Testdaten
sentences_test = sentences[split_position:]
y_test = y[split_position:]

# Erstelle Vokabular aus den Traningsdaten 
# und transformiere alle Sätze in Bag-Of-Words Vektoren
vocabulary = util.create_vocabulary(sentences_train)
x_train = util.create_bag_of_words(sentences_train, vocabulary)
x_test = util.create_bag_of_words(sentences_test, vocabulary)

Trainieren eines neuronalen Netzes

Wir kommen jetzt zum eigentlichen Training. Dazu definieren wir ein Netzwerk mit der folgenden Struktur:

Struktur des neuronalen Netzes als Graph

Der Eingabe-Layer hat 1000 Neuronen, da unser Vokabular 999 Wörter plus das ungesehene Wort umfasst. Jedes Neuron im Eingabe-Layer bekommt die Anzahl des ihm zugeordneten Worts als Eingabe. Danach wird ein einfacher Hidden-Layer mit 10 Neuronen geschaltet. Die Ausgabe hat nur ein Neuron, das entweder den Wert 0 für negativ oder 1 für positiv annehmen soll.

Das folgende Skript nutzt nun die Tensorflow-API um dieses Netzwerk zu definieren und die passenden Gewichte für die bereits erstellten Trainingsdaten zu lernen. Tensorflow überprüft für die verschiedenen Iterationen der Gewichtsanpassungen mit Hilfe der Testdaten, wie gut das Netz bereits gelernt hat. Die trainierten Modelldaten (inklusive Vokabular) werden gespeichert

from tensorflow.keras.models import Sequential
from tensorflow.keras import layers

# Definiere ein Neuronales Netz mit einem Bag-Of-Words Vektor als Eingabe
# einem Hidden Layer und einer Ausgabe
model = Sequential()
model.add(layers.Dense(10, input_dim=len(x_train[0]), activation='relu'))
model.add(layers.Dense(1, activation='sigmoid'))

# Muss einmal "kompiliert" werden
model.compile(loss='mse', optimizer='adam', metrics=['accuracy'])
print(model.summary())

# "fit" trainiert das Modell auf den gegebenen Daten
model.fit(x_train, y_train, epochs=20, validation_data=(x_test, y_test), batch_size=10)
model.save("classifier.h5")
# Auch das Vokabular abspeichern, sonst können wir keine neuen Vektoren erstellen
dill.dump(vocabulary, open("vocabulary.dat", "wb"))

Ausgabe für ein paar Testdaten

Mit den gespeicherten Modelldaten können wir nun in einem neuen Skript eigene Eingaben ausprobieren.

import util
import dill
from tensorflow.keras.models import load_model

# Vokabular und Modell des neuronalen Netzes laden
vocabulary = dill.load(open("vocabulary.dat", "rb"))
model = load_model("classifier.h5")

# Beispieleingaben erzeugen
example_sentences = ["This movie is worse than anything else.",
                     "Best movie ever!",
                     "You can not imagine a better movie!",
                     "I would love to see this again. Bruce Wayne is great.",
                     "YAML SDFA FAFAFCA FAF AFAF"]
x = util.create_bag_of_words(example_sentences, vocabulary)

# Ausgabe vorhersagen
y = model.predict(x)
for i in range(0, len(y)):
    print(example_sentences[i], y[i])
This movie is worse than anything else. [0.10687903]
Best movie ever! [0.7610351]
You can not imagine a better movie! [0.09572864]
I would love to see this again. Bruce Wayne is great. [0.9571661]
YAML SDFA FAFAFCA FAF AFAF [0.5089223]
  1. Kotzias, Dimitrios, Misha Denil, Nando De Freitas, and Padhraic Smyth. 2015. “From Group to Individual Labels Using Deep Features.” In Proceedings of the 21th Acm Sigkdd International Conference on Knowledge Discovery and Data Mining, 597–606. ACM. 

Kernpunkte

  • Die Keras-API von Tensorflow erlaubt es relativ einfach neuronale Netze zu definieren, trainieren und anzuwenden

  • Datenaufbereitung und Umwandlung von Texten in Vektoren fester Länge macht einen großen Teil der Arbeit aus


Kollaboratives Arbeiten mit Git

Overview

Teaching: 0 min
Exercises: 0 min
Questions
  • Wie kann man gemeinsam Software entwickeln?

  • Welche Möglichkeiten gibt es den Quelltext zu teilen?

Objectives
  • Einfache Git-Kommandos ausführen

  • Projekte in GitLab anlegen und Dateien synchronisieren

Einführung und Terminologie

Das Arbeiten an Quelltexten ist typischerweise inkrementell. Das führt schnell dazu, dass man Überblick über die verschiedenen Versionen verliert. Beim Schreiben von Texten gibt es dann oft die Strategie, die Versionierung im Dateinamen auszudrücken (also z.B. “final.doc”, “final_rev2.doc”, final_final.doc”).1 Außerdem benötigen wir bei gemeinsamen Arbeiten in Projekten auch einen gemeinsamen Stand des Quelltexts, auf den alle Projektmitglieder zugreifen können.

Anstatt manuell eigene Versionierungs-Mechanismen zu erarbeiten, können wir auch bestehende Versionskontrollsysteme (VCS) nutzen. Dieses soll uns in die Lage versetzen

Inzwischen hat sich das Kommandozeilen-Programm Git als de-facto Standard Versionskontrollsystem für Quelltexte etabliert. Man kann es aber auch für andere Textdatei-lastige Projekte, wie zum Beispiel Korpusannotationsdateien nutzen.

Die Konzepte von Versionsverwaltungsprogrammen folgen einer eigenen Terminologie, die gerade am Anfang etwas überwältigend sein kann. Im Glossar der GitHub-Platform können Sie die wichtigsten Begriffe, die wir auch hier verwenden werden, nachschlagen. Dazu gehören unter anderem,

Tools und Plattformen

Um gemeinsam Quelltexte zu bearbeiten, benötigen wir zwei Dinge:

Wir werden die grafische Oberfläche GitHub Desktop (nicht zu verwechseln mit der Online-Platform GitHub.com, aber von der gleichen Firma) nutzen, die Sie von https://desktop.github.com/ für Ihr Betriebsystem herunterladen können. Für Linux werden inoffizielle Installationspakete unter https://github.com/shiftkey/desktop/releases/ bereitgestellt. Gleichzeitig werden wir auch immer für jede Aktion in GitHub Desktop den entsprechenden Git-Kommandozeilen-Aufruf zeigen. GitHub Desktop bringt seine eigene Git-Installation mit. Um die Kommandozeilenbefehle nachzuvollziehen, müssen Sie Git für die Kommandozeile von https://git-scm.com/downloads installieren. Unter Linux ist git typischerweise schon in den Paketquellen vorhanden und kann zum Beispiel mit sudo apt install git unter Ubuntu installiert werden.

Eine populäre Platform für Git-basierte Projekte ist GitHub.com, die zum Konzern Microsoft gehört. Diese erlaubt inzwischen auch das Erstellen von nicht-öffentlichen Projekten und kann daher auch für eigene Studien-Projekte geeignet sein. Daneben gibt es aber auch Alternativen wie GitLab, das einerseits eine zentrale Plattform analog zu GitHub.com ist, deren Software man aber auch auf eigenen Servern installieren und betreiben kann. Die HU-Berlin betreibt unter https://scm.cms.hu-berlin.de/ eine solche GitLab-Instanz auf ihren eigenen Servern, auf der Sie sich mit Ihrem CMS-Acccount einloggen können. Diese verschiedenen Plattformen haben oft ähnliche Funkionen, wie zum Beispiel Ticketsysteme zum Verwalten von Bugs und TODOs. In diesem Tutorial werden wir die GitLab-Instanz der HU nutzen, aber das Prinzip ist für die anderen Plattformen gleich.

Anlegen eines neuen lokalen Git-Repositories

Git arbeitet auf sogenannten Repositories, die alle Dateien und die komplette Versionshistorie beinhalten. Ein Git-Repository besteht aus einem lokalen Ordner mit den zu versionierenden Dateien und einem speziellen versteckten Unterordner .git.

Zum Anlegen eines Git-Reppositories, starten Sie GitHub Desktop. Beim ersten Start wird das Programm Sie auffordern, einen Account auf GitHub.com anzulegen, da wir das GitLab der HU nutzen werden, können Sie diesen Schritt bedenkenlos überspringen.

Skip GitHub Account

Trotzdem benötigt Git die Angabe eines Namens und einer E-Mail-Adresse. Diese Informationen werden in der Versionshistorie als Felder für den Autor angezeigt. Beide Angaben werden nur lokal gespeichert und nicht verifiziert, weder der Name noch die E-Mail-Adresse müssen stimmen.

Konfiguration des Namens

Wählen Sie im Startbildschirm von GitHub Desktop jetzt „Create a new Repository on your hard drive…“ aus, um ein neues Repository als Ordner in einem beliebigen Pfad anzulegen.

Neues Repository initialisieren

Falls bereits ein Repository offen ist, können Sie neue Repositories auch das Menü und “File -> New repository” anlegen. Auf der Kommandozeile würde man erst einen neuen Ordner erstellen, in diesem mit cd wechseln und dann git init ausführen, um das gleiche Ergebnis zu erreichen.

mkdir nlp
cd nlp
git init
Leeres Git-Repository in /home/thomas/Dokumente/studium/nlp/.git/ initialisiert

Diesen Ordner, oder Arbeitskopie oder “working tree” genannt, können Sie nun wie einen normalen Ordner im Dateisystem benutzen. Sie können Ihn auch in Visual Studio Code öffnen, um Dateien anzulegen oder zu editieren. GitHub Desktop bietet einem sogar einen Link an, um das Repository direkt zu in Visual Studio Code zu öffnen

Öffnen in VS Code

Dateien hinzufügen und ändern

Legen Sie jetzt eine neue Python-Datei mit dem Namen main.py im Repository, zum Beispiel über Visual Studio Code an.

name = input("Wie heißen Sie: ")
print("Hallo " + name)

Im Gegensatz zu Programmen wie Dropbox oder der HU Box müssen Änderungen an den Dateien explizit markiert und in einem Commit zusammengefasst werden. In GitHub Desktop markiert man dazu die geänderten Dateien, füllt eine Commit-Nachricht aus und comitted die Änderungen. Dieser Commit ist dann Teil der History des Repositories. Wenn alle lokal geänderten bzw. hinzugefügten Dateien Teil des Commits waren, wird GitHub Desktop “No local changes” anzeigen.

Datei hinzufügen

Auf der Kommandozeile werden Änderungen mit git add <datei> vorgemerkt und dann mit git commit -m Nachricht comitted.

git add main.py
git commit -m "Initial Script"

Git verwaltet Änderungen zeilenweise. Sobald eine Datei geändert wird, zeigt GitHub Desktop die geänderten Zeilen an. Ändern Sie main.py so, dass am Ende der Ausgabe ein Ausrufezeichen steht, speichern Sie die Datei ab und GitHub Desktop wird die lokalen Änderungen anzeigen.

Lokale Änderungen

Äquivalent dazu ist der Aufruf von git diff auf der Kommandozeile.

git diff
diff --git a/main.py b/main.py
index f0b5686..936e410 100644
--- a/main.py
+++ b/main.py
@@ -1,2 +1,2 @@
 name = input("Wie heißen Sie: ")
-print("Hallo " + name)
+print("Hallo " + name + "!")

Markieren Sie auch die neuen Änderungen und committen Sie diese mit einer Aussagekräftigen Commit-Nachricht, zum Beispiel “Added more excitement when welcoming the user.”. In dem Tab “History” listet GitHub Desktop alle Commits mit ihren Nachrichten, dem Autor und den geänderten Zeilen auf.

History und Diff

Jeder Commit hat dabei eine eindeutige ID, in dem Screenshot ist es zum Beispiel d34119c für den Commit, in dem das Ausrufezeichen hinzugefügt wurde (unter dem Titel oben rechts im Bild). Diese ID wird bei Ihnen anders sein, da der Nutzername, die E-Mail, Zeitstempel und alle bisherigen Commits in die Berechnung dieser ID eingehen. Wenn zwei Commit die gleiche ID haben können Sie sich sicher sein, dass Sie die gleiche Version des Repositories mit dem gleichen Dateiinhalt haben.

Auf der Kommandozeile listet git log alle Commits und ihre ID auf.

git log
commit d34119cd81d054d02b87dedd6dbff14ef65e93b8 (HEAD -> master)
Author: Thomas Krause <krauseto@hu-berlin.de>
Date:   Tue Jun 9 16:54:04 2020 +0200

    Added more excitement when welcoming the user.

commit f22fe3b7787a10c18039e907474d0cef03785705
Author: Thomas Krause <krauseto@hu-berlin.de>
Date:   Tue Jun 9 16:49:00 2020 +0200

    Initial script
    
    The script just asks the user for its name and prints a welcome message.

commit 561e8c6b06e757868fd31bafa619e7c0b43c01e8
Author: Thomas Krause <krauseto@hu-berlin.de>
Date:   Tue Jun 9 14:36:46 2020 +0200

    Initial commit

Im Gegensatz zu GitHub Desktop listet git log die komplette ID und nicht nur die ersten Zeichen auf. Zur eindeutigen Identifikation des Commits reichen aber meist die ersten 7 Zeichen.

Sie können lokale Änderungen auch rückgängig machen anstatt Sie zu committed. Wenn Sie zum Beispiel das Ausrufezeichen in ein Fragezeichen ändern und main.py speichern, wird die Änderungen in GitHub Desktop angezeigt. Sie können dann mit der rechten Maustaste auf die geänderte Datei unter “Changes” klicken und “Discard changes…” auswählen.

Discard Changes

Dieser Befehl ändert die Datei auf der Festplatte, weswegen ein extra Bestätigungsdialog erscheint. Die Kommanzeilenvariante basiert auf dem Befehl git checkout <version> <file>, der Dateien in bestimmten Versionen aus dem Repository wiederherstellt. Wir verwenden die spezielle Meta-Version “HEAD”, die für den letzten lokalen Commit steht.

git checkout HEAD main.py
1 Pfad von 80b993b aktualisiert

Synchronisieren von Repositories

Anlegen eines neuen Projekts in GitLab

Um Quelltexte zu teilen, muss man ein Repository auf einem Server anlegen. Für das GitLab der HU loggen Sie sich zuerst unter https://scm.cms.hu-berlin.de/ ein und drücken Sie dann die grüne “New Project” Schaltfläche.

New Project in GitLab

Füllen Sie danach das Eingabeformular für neue Projekte mit dem Namen und einer optionalen Beschreibung aus. Sie können einstellen, wer das Projekt sehen kann. Stellen Sie sicher, dass “Initialize repository with a README” nicht ausgewählt ist.

New Project Formular

Pushen der Änderungen

Das erstelle Repository ist komplett leer, beinhaltet also keine Commits. Um die lokalen Commits zum entfernten Repository hinzuzufügen, muss es als sogenanntes Remote-Repository registriert werden. Dazu klicken Sie zuerst auf die blaue “Clone” Schaltfläche und kopieren Sie die URL unter “Clone with HTTPS”.

Clone URL

Das Registrieren des Remote-Repositories geht leider nicht im GitHub Desktop Client2, deswegen müssen wir auf die Kommandozeile ausweichen und im Repository-Ordner als aktuellem Verzeichnis folgenden Befehl ausführen:

git remote add origin https://scm.cms.hu-berlin.de/krauseto/python-kurs-test.git

Dieser Befehl fügt ein neues Remote-Repository mit dem Namen “origin” und gegebenen URL hinzu (diese muss natürlich ihre eigene URL, nicht die aus dem Beispiel sein).

Sie müssen das Remote-Repository nur einmal hinzufügen. Danach können Sie auch weiter in GitHub Desktop arbeiten. Der erste Schritt ist, alle lokalen Commits in das entfernte Repository mit “Publish changes” zu übertragen.

Push in GitHub Desktop

Ein Neuladen der Projekt-Seite in GitLab sollte nun die Dateien des Repositories anzeigen.

Projektübersicht GitLab

Auf der Kommandozeilen kann man alle lokalen Commits mit

git push origin master

“origin” ist der vorher vergebene Kurzname für das Remote-Repository und “master” der sogenannte Branch, der übertragen werden soll. Wir werden später Branches kennenlernen, im Moment müssen Sie aber nur wissen, dass der Standard-Branch “master” heißt.

Hinzufügen andere Entwickler

Auf ein Remote-Repository können mehrere Nutzer zugreifen. Fügen Sie Ihre Projektmitglieder unter “Settings -> Members” hinzu. Um mit am Quelltext arbeiten zu können, muss mindestens die Rolle “Developer” zugewiesen werden.

Projektmitglieder hinzufügen

Klonen eines neuen Repositories

Im Projekt werden Sie vermutlich nur ein Repository nutzen. Einer der Team-Mitglieder erstellt dieses Repository und die anderen müssen es dann auf Ihren Rechner übertragen. Auf der Kommandozeile würde man dazu ein neues Verzeichnis erstellen und mit git clone und der URL, die man über die Schaltfläche “Clone” erhalten hat ein neues lokales Repository als Kopie erstellen. Als weiteres optionales Argument kann man den Namen der neuen Arbeitskopie (also des Ordners) angeben.

cd ~/Dokumente/studium/
git clone https://scm.cms.hu-berlin.de/krauseto/python-kurs-test.git nlp-clone
cd nlp-clone

Über das Menü “File -> Clone repository” kann man im GitHub Desktop ebenfalls ein bestehendes Projekt klonen. In der Projektübersicht links oben kann man dann zwischen den verschiedenen Arbeitskopien die in GitHub Desktop registriert sind wechseln.

Klonen in GitHub Desktop

Änderungen vom Remote-Repository übernehmen

Wenn andere Entwickler Änderungen comitten und per “push” übertragen, können wir zwar die Änderungen in der GitLab Projektansicht sehen, aber haben den neuesten Stand der Dateien nicht lokal in unserem eigenen Repository. Das Holen der Änderungen vom entfernten Repository ist in Git eine explizite Aktion. Um zu emulieren, dass ein anderes Mitglied committed und dann gepushed hat, klicken wir in der Projektansicht von GitLab auf die Datei “main.py” und editieren sie direkt in GitLab.

Änderung der Datei in GitLab

Diese entfernten Änderungen können Sie dann in GitHub Desktop erst “fetchen” und dann “pullen”. Der Schritt “Fetch origin” überträgt die Commits. Der Pfeil mit der “1” zeigt an, dass ein entfernten Commit bereitsteht und angewendet werden kann. Die zweite Aktion “Pull origin with rebase” aktualisiert dann die Dateien im Arbeitsordner auf den aktuelleren Commit des entfernten Repositories anstatt des älteren lokalen Commits. GitHub Desktop fetch/pull Die entsprechenden Kommandozeilenbefehle sind entsprechend:

git fetch origin
git pull origin master

Solange das Remote-Repository keine neueren Commits hat, kann man nun neue lokale Commits wieder mit “push” zum Remote-Repository hochladen. Falls es neuere Commits gibt, kann man diese zuerst “fetchen” und dann “pullen”. Legen Sie zum Beispiel eine neue Datei README.md mit dem einer Projektbeschreibung in Markdown an.

# Beispiel-Seminarprojekt

TODO: Beschreibung des Vorhabens

## Setup

Installieren Sie alle benötigten Pakete mit:

```
pip install nltk
```

Mit dem “Add”, “Commit, und “Push” können Sie diese Änderungen wieder hochladen. Dieser Zyklus aus Pullen und Pushen wiederholen Sie für jede Änderung.

Add/commit/push cycle

Wann sollte man pullen und pushen?

Pullen und pushen Sie möglichst häufig, damit Ihr lokaler Stand nicht zu sehr von dem der anderen Projektmitglieder abweicht. Bevor Sie zum Beispiel anfangen lokal die Quelltexte zu bearbeiten, sollten Sie vorher einmal “Pull” ausgeführt haben. Committen Sie auch kleine Änderungen und pushen Sie sie gleich nach dem Commit.

Branches

Bisher haben wir nur Änderungen (also Commits) nur linear aufeinander aufgebaut. Auf Commit A folgt Commit B etc. Zum Beispiel würden die Commit-Befehle

git add file1.txt
git commit -m "commit A"
git add file2.txt
git commit -m "commit B"
git add file1.txt
git commit -m "commit C"

zu einer linearen Commit-Historie führen:

master branch
      |
      + commit A
      |
      + commit B
      | 
      + commit C
      |
     ...

Ein Branch ist dabei die Sammlung einer linearen Abfolge von Commits. Git erlaubt das Abzweigen eines neuen Branches von einer bestehenden Commit-Abfolge. Nach dem Abzeigen mit git branch <Name neuer Branch> muss man die lokale Arbeitskopie auf diesen Branch umstellen git checkout <Name neuer Branch> und kann dann unabhängig vom Original-Branch commiten, bis man die beiden Branches wieder zusammenführt, in dem man zuerst die Arbeitskopie wieder auf den master-Branch mit git checkout master umschalten und dann den neuen Branch merged git merge <Name neuer Branch>. zusammenführt.

       master branch
            |
            |\
            | \
            |   bugfix-123 branch
   commit A +   |
            |   |
            |   + commit B
            |   + commit C
            | /
            |/ 
            + merging branch

“master” enthält nach dem Mergen alle Commits aus beiden Branches.

GitHub Desktop erlaubt ebenfalls das abzweigen und zusammenführen neuer Branches sowie das Umschalten der Arbeitskopie auf den Stand eines jeweiligen Branches.

Add Branch GitHub Desktop

Auf welchem Branch die Arbeitskopie aktuelle eingestellt ist, wird oben unter “Current Branch” angzeigt. In dem Branch können Sie nun neue Commits hinzufügen. Um Sie zu mergen, wechseln Sie erst zurück auf den “master” branch, klicken Sie dann auf die Branch-Auswahl Schaltfläche und wählen Sie “Choose a branch to merge into master”. Wählen Sie den Branch aus und bestätigen Sie das mergen. Die neuen Commits sollten Sie danach direkt auch wieder pushen.

Merge Branch in GitHub Desktop

Sie können auch Branches auf dem entfernten Repository mit “Publish” und “Push” hochladen.

Weiterführende Themen

GitHub Desktop Hilfe

Git hat sehr viele Funkionen und gerade der Umgang mit Branches kann am Anfang schwierig sein. Die Online-Hilfe von GitHub Desktop beschreibt viele typische Git-Arbeitsschritte, darunter auch die hier vorgestellten: https://help.github.com/en/desktop/contributing-to-projects.

Konflikte

Besonders wichtig ist der Umgang mit Konflikten. Wenn zwei Entwickler an verschiedenen Dateien oder verschieden Zeilen der gleichen Datei arbeiten, wird Git beim mergen die Versionen einfach zusammenführen. Bei Änderungen an der gleichen Datei in der gleichen Zeile wird es aber zu so einem sogenannten Konflikt kommen. Diesen müssen Sie manuell auflösen (also bestimmen welche Zeile übernommen wird und welche nicht). Neuere Versionen von GitHub Desktop unterstützen Sie bei der Auflösung des Commits. Wenn ein Konflikt ensteht, können Sie entweder auswählen welcher der beiden Versionen (aus Ihrem Branch oder dem entfernten) genommen werden soll oder Sie können die Datei in Visual Studio Code öffnen lassen.

Konflikt in GitHub Code öffnen

Die Datei mit dem Konflikt enthält beide Zeilen und die Markierungen <<<<<<< am Konflikt-Anfang ======= für die Grenze zwischen der einen und der anderen Variante und “»»»>” für das Ende des Bereichs mit dem Konflikt.

<<<<<<< HEAD
pip install nltk gensim art
=======
pip install nltk gensim tensorflow
>>>>>>> Added tensorflow to dependencies

Passen Sie die Datei manuell mit den gewünschten Änderungen an und stellen Sie sicher, die Zeilen mit den Konflikt-Markern dabei entfernt zu haben. Also zum Beispiel durch die zusammengeführte Zeile, die beide neuen Abhängigkeiten enthält.

pip install nltk gensim art tensorflow

Sobald Sie die Konfliktmarker entfernt haben, wird GitHub Desktop erlauben das Mergen fortzusetzen.

Fortsetzen Merge GitHub Desktop

Mehr über Git Kommandozeile

Um sehr tief in die Bedienung von Git auf der Kommandozeile einzusteigen, können Sie die Lektionen zu Git vom Software Carpentry Projekt durcharbeiten: http://swcarpentry.github.io/git-novice/.

  1. Zu den Problemen die durch „Versionen“ im Dateinamen entstehen können, sei auch die PHD Comics Episode dazu empfohlen. 

  2. GitHub möchte mit seinem Produkt natürlich die eigene Plattform unterstützen, deswegen ist es kein Problem unter “Publish Repository” ein GitHub.com Repository auszuwählen, aber leider nicht von anderen Plattformen wie dem HU GitLab. 

Kernpunkte

  • Git ist eine Software zum Verwalten der Quelltexte, um Git herum gibt es Plattformen wie GitLab oder GitHub, die als zentraler Hub für das Projekt dienen

  • Man kann Git als Kommandozeilenprogramm nutzen, aber auch grafische Oberflächen wie „GitHub Desktop“