by Benjamin 'kristall' Kierdorf | kristall@c-base.org*
cc some rights reserved by-sa (my e-mail is enough to satisfy the 'by')
@kristall:c-base.org (matrix)
@kristallpirat (@chaos.social, @bsky.social and @twitter)
* Der Autor ist cnowledge engineer des cience rings der c-base.
Dieser Artikel ist eine überarbeitete Neuauflage einer ursprünglichen Version von 2006.
Das Tokenizen1 deutscher Texte ist eine kleine Welt für sich. Dabei wirkt sich die Wahl des Schnittmusters erheblich auf die nachfolgenden Textanalysen aus. Nachfolgend werden einige mögliche Schnittmuster vorgestellt und andiskutiert. Dabei soll auch der Frage nachgegangen werden, ob die Behauptung von Schiller/Teufel/Stöckert/Thielen2 "Es ist bis jetzt (aus technischen Gründen) nicht möglich, Mehrwortlexeme als Ganzes zu taggen" (inzwischen) widerlegbar ist. Zuerst werden aber die notwendigen Begrifflichkeiten geklärt.
I. Tokens, reguläre Ausdrücke und Tokenizer¶
1. Das Tokenizen von Texten kann man sich bildlich so vorstellen, daß mit einer Schere aus einem Text (auf Papier) Wörter ausgeschnitten werden. Ein Token ist dann ein Schnipsel, auf dem ein Teil des Textes steht. Ein Text besteht demnach aus einer Liste von Tokens. Ein Token läßt sich leicht in Python3 mit Hilfe von Dictionaries4 implementieren.
class TextToken(dict):
def __init__(self, text):
self['Text'] = str(text)
class SubToken(dict):
def __init__(self, key, value):
self[key] = value
# Eine einfache "Tokens.py"-Datei.
Ein TextToken ist einfach ein Dictionary, in dem im Schlüssel Text der zu bearbeitende Text als String5 gespeichert ist. Ein SubToken ist ebenfalls ein Dictionary, dieser Tokentyp ist aber generischer.
2. Reguläre Ausdrücke sind spezielle Formeln, mit denen Sprachen definiert werden können.6 Sie sind mächtig genug, um damit alle endlichen Sprachen beschreiben zu können.7 Mit Hilfe dieser lassen sich verschiedene Schnittmuster für Texte definieren. Um die unterschiedlichen Möglichkeiten hierbei geht es in Kapitel II.
In Python werden reguläre Ausdrücke häufig in raw strings8 geschrieben (zB rA = r'\w+').9
3. Der Tokenizer ist sinnbildlich die Schere. Der Tokenizer beinhaltet eine Funktion tokenize() mit deren Hilfe ein Text in Tokens zerlegt werden kann.10 Die Implementation eines Tokenizers ist nicht so einfach, wie die eines Tokens, aber auch nicht wirklich schwierig.
import re, string
from Tokens import *
class Tokenizer(object):
def tokenize(self, TextToken, rA, schnipsel = 'Tokens', schnip = 'Token', add_spans = False, nummeriere = False):
tok = TextToken.copy()
tok[schnipsel] = []
rAc = re.compile(rA)
for Muster in rAc.finditer(TextToken['Text']):
tok[schnipsel].append(SubToken(schnip, tok['Text'][Muster.span()[0]:Muster.span()[1]]))
if add_spans: tok[schnipsel][-1]['Span'] = '@%s:%s' %(str(Muster.span()[0]), str(Muster.span()[1]))
if nummeriere: tok[schnipsel][-1]['Num'] = len(tok[schnipsel])-1
return tok
# Eine "Tokenizer.py"-Datei.
Die Funktion tokenize() hat dabei 2 obligatorische und 4 vorbelegte Aufrufsparameter.
a) Der TextToken stammt aus der Datei "Tokens.py" und enthält den zu zerlegenden Text als String in utf-811 (früher: Unicode). 'rA' ist der reguläre Ausdruck, der bestimmt was ein Schnipsel ist. Dazu mehr in Kapitel II.
b) Durch den Aufruf der Funktion wird zuerst der übergebene TextToken kopiert. Dieser ist ja ein Dictionary; in diesem wird ein neuer Schlüssel mit dem Wert von schnipsel angelegt. Unter diesem Schlüssel steht am Ende die Liste mit den Tokens. Danach wird der übergebene reguläre Ausdruck compiliert. In diesem regulären Ausdruck ist definiert was in einem Tokens stehen soll. rAc.finditer() ist ein Standardfunktion aus dem re-Modul.12 In dem 2-Tupel Muster.span() steht jeweils ein Indexpaar, welches den Start- und Endindex eines durch den regulären Ausdruck erkannten Tokens im Text enthält. Die schnipsel-Liste wird nun mit Hilfe von Slicing13 aus dem Text ausgeschnitten, wobei das jeweilige Ergebnis unter Verwendung des Schlüssels schnip in einen SubToken geschrieben wird. Wenn add_spans auf True gesetzt wird, dann wird in jedem SubToken zusätzlich der Schlüssel Span angelegt und die Indizes des Tokens im Text vermerkt. Ein setzen von nummeriere auf True sorgt dafür das in jedem SubToken ein Schlüssel Num angelegt wird, der als Wert den Index des SubTokens in der schnipsel-Liste enthält. Zum Schluß wird der kopierte und veränderte TextToken zurückgeben.
from Tokenizer import *
tokenizer = Tokenizer()
Text = 'Dies ist ein Beispielstext, der mit Hilfe eines Tokenizers zerlegt wird.'
rA = r'\S+' # Ein sehr einfacher regulärer Ausdruck: 'nicht Whitespace mindestens einmal'.
tokenizer.tokenize(TextToken(Text), rA)
tokenizer.tokenize(TextToken(Text), rA, add_spans = True, nummeriere = True)
Text[48:58]
TokenText = tokenizer.tokenize(TextToken(Text), rA, add_spans = True, nummeriere = True) # Den Output an einen Namen (hier: TokenText) binden
# Beispiele 01
II. Schnittmuster¶
Zur Darstellung der Treffer14 eines regulären Ausdrucks in einem Text (String) wird die folgende Funktion verwendet15:
def rA_show(pat, s):
print(re.compile(pat, re.M).sub(r"{\g<0>}", s.rstrip()))
# Die Funktion rA_show
1. Nach Whitespaces¶
a) Das wohl einfachste Schnittmuster für Text dürfte der reguläre Ausdruck r'\w+' sein.
rA_show(r'\w+', u'Dies ist ein Beispielstext, der -überraschenderweise- mit Hilfe eines Tokenizers zerlegt wird.')
# Beispiel 02
Der größte Nachteil dieses einfachen Ausdrucks liegt darin, daß Satzzeichen komplett ignoriert werden, der Text also unvollständig von den Tokens abgebildet wird. Daher ist dieses Schnittmuster ungeeignet um deutsche Texte sinnvoll zu parsen.
b) Erste geeignete Ergebnisse liefert der, bereits im obigen Beispiel verwendete,16 reguläre Ausdruck r'\S+'.
rA_show(r'\S+', u'Dies ist ein Beispielstext, der -überraschenderweise- mit Hilfe eines Tokenizers zerlegt wird.')
# Beispiel 03
Dieser Ausdruck beinhaltet zwar die Satzzeichen, diese werden aber nicht einzeln, sondern, da ihnen kein Whitespace17 voraus geht, nur mit dem vorausgehenden Wort zusammen, erkannt. Eine nachfolgende Analyse müsste also immer überprüfen, ob ein Token ein Satzzeichen enthält und diesen Fall dann gesondert abarbeiten. Dieses Schnittmuster ist also zwar geeignet deutsche Texte sinnvoll zu parsen, aber der Aufwand für die benötigte Satzzeichensonderbehandlung ist nicht erforderlich.
2. Wortorientiert¶
Weniger aufwändig ist es den regulären Ausdruck so anzupassen, daß Satzzeichen in einen eigenen Token kommen. Dies erreicht man mit dem Schnittmuster r'[^\w\s]|\w+.
rA_show(r'[^\w\s]|\w+', u'Dies ist ein Beispielstext, der -überraschenderweise- mit Hilfe eines Tokenizers zerlegt wird.')
# Beispiel 04
Auf den ersten Blick scheint dieser Ausdruck durchaus in der Lage deutsche Texte sinnvoll zu parsen. Er ist geeignet und wohl auch erforderlich, allerdings hat dieser Ausdruck auch erhebliche Schwächen.
rA_show(r'[^\w\s]|\w+', u'Wenn aber Mehrwortlexeme wie z.B. oder New York vorkommen, treten die Schwächen zu Tage.')
rA_show(r'[^\w\s]|\w+', u'Es gilt der Grundsatz: in dubio pro reo.')
# Beispiele 05
Die Tokenfolge {z}{.}{B}{.} ist dabei insofern problematisch, als daß die Punkte den Satz nicht beenden, sondern nur Hinweise auf eine Abkürzung darstellen. Wünschenswert wäre es, wenn die Zeichenfolge z.B. ein Token wäre (also {z.B.}). Auch bei Zusammensetzungen18 wie "New York" oder "in dubio pro reo" erscheint es wünschenswert19 diese in einen Token zu lesen (also {New York} statt {New} {York} und {in dubio pro reo} statt {in} {dubio} {pro} {reo}).
3. Mehrwortlexemorientiert¶
Um zu erreichen, daß Mehrwortlexeme20 in einem Token abgebildet werden, kann man nun auf die Idee kommen diese in den regulären Ausdruck als Ausnahmen aufzunehmen.
rA = r'z\.B\.|New\ York|[^\w\s]|\w+'
rA_show(rA, u'Wenn aber Mehrwortlexeme wie z.B. oder New York vorkommen, treten die Schwächen zu Tage.')
# Beispiel 06
Dabei spielt die Reihenfolge der Ausnahmen im regulären Ausdruck eine extrem wichtige Rolle. Wer ordentlich arbeiten möchte und die Ausnahmen alphabethisch sortiert anliefert, schafft sich Probleme, statt diese zu vermeiden.
rA = r'a\.|a\.a\.O\.|[^\w\s]+|\w+'
rA_show(rA, u'Denn die Abkürzung a.a.O. wird von diesem Ausdruck niemals erkannt.')
# Beispiel 07
Der Grund dafür liegt darin, daß der reguläre Ausdruck eine oder-Funktion ist; die meisten Compiler und Interpreter brechen eine solche Funktion nach dem ersten wahr ab, da die weiteren Wahrheitswerte keine Relevanz für das Ergebnis mehr haben. Und da "a\." vor "a\.a\.O\." im regulären Ausdruck steht, wird letzterer Ausdruck niemals erkannt. Die Lösung für dieses Problem liegt darin, die Ausnahmen nicht alphabethisch, sondern lang vor kurz und gegen das Alphabet sortiert in den regulären Ausdruck zu schreiben.
rA = r'a\.a\.O\.|a\.|[^\w\s]+|\w+'
rA_show(rA, u'Die Abkürzung a.a.O. wird von diesem Ausdruck erkannt.')
# Beispiel 08
Damit sind aber noch nicht alle Probleme, die durch die Aufnahme von Ausnahmen auftreten können, gelöst.
rA = r'reguläre\ Ausdruck|[^\w\s]+|\w+'
rA_show(rA, u'Denn dieser reguläre Ausdruck erkennt nicht alle Formen von regulären Ausdrücken.')
# Beispiel 09
Dies läßt sich aber auch erreichen:
rA = r'regulär(er|en|e)\ Ausdr(ü|u)c(ken|ks|ke|k)|[^\w\s]|\w+'
rA_show(rA, u'Dieser reguläre Ausdruck erkennt die meisten Formen von regulären Ausdrücken.')
# Beispiel 10
Problematisch ist es auch, wenn eine Ausnahme als Teilstring einer Nichtausnahme auftritt.
rA = r'SGB\ 2|[^\w\s]|\w+'
rA_show(rA, u'Hier wird nicht nur SGB 2 ausgeschnitten, sondern fälschlicherweise auch SGB 20 zerteilt.')
# Beispiel 11 (hypothetisch, bei 20 sind die Sozialgesetzbücher noch nicht angekommen)
Um diesen Effekt zu vermeiden sollte die negative lookahead assertion21 "(?!\w)" verwendet werden.
rA = r'SGB\ 2(?!\w)|[^\w\s]|\w+'
rA_show(rA, u'Hier wird nicht SGB 2 aus SGB 20 ausgeschnitten.' )
# Beispiel 12
In der folgenden Analyse ist darauf zu achten, das ein Mehrwortlexem auch das Zeichen für 'Satzende' enthalten kann.
Durch umfangreiche Ausnahmelisten wird der reguläre Ausdruck entsprechend lang, das aktuelle Schnittmuster des Autors ist über 72k Zeichen lang und erkennt über 3,25k Mehrwortlexeme aus dem juristischen Kontext. Dabei ist der Zeitbedarf akzeptabel, das 'Tokenizen' eines über 8,5k Wörter langen Textes dauert auf einem P2, 650 MHz, 256 MB RAM Notebook wenige Sekunden. Skalierungsprobleme traten bisher nicht auf.
III. Fazit¶
Mit etwas Aufwand läßt sich eine Ausnahmenliste, mit deren Hilfe das 'Schnittmuster' per Script erzeugt werden kann, pflegen. Aber der Aufwand lohnt sich, denn wenn Tokens auch Mehrwortlexeme enthalten, ist es möglich diese mit nur einem 'PosTag'22 zu versehen. Dies erleichtert die folgende Analyse erheblich. Wenn das 'Schnittmuster' auch html-Tags als einen Token behandelt, ist es möglich Webseiten mit juristischem Inhalt in einen 'Tokenstream' zu verwandeln und mit Hilfe der Tokens auf bestimmte Informationen auszuwerten. Die Behauptung23 Es ist bis jetzt (aus technischen Gründen) nicht möglich, Mehrwortlexeme als Ganzes zu taggen, ist folglich (inzwischen) widerlegbar.
Fußnoten¶
1 Normalerweise wohl Tokenisierung http://de.wikipedia.org/wiki/Tokenisierung (siehe auch http://en.wikipedia.org/wiki/Tokenization) der Autor bevorzugt aber den Begriff tokenizen.
2 Aus "Guidelines für das Tagging deutscher Textcorpora mit STTS" (August 1999) von Anne Schiller, Simone Teufel, Christine Stöckert und Christine Thielen, S. 9, zu finden unter https://www.ims.uni-stuttgart.de/documents/ressourcen/lexika/tagsets/stts-1999.pdf.
3 Siehe http://python.org/, siehe auch http://de.wikipedia.org/wiki/Python_%28Programmiersprache%29.
4 Siehe https://docs.python.org/3/tutorial/datastructures.html#dictionaries und https://docs.python.org/3/library/stdtypes.html#typesmapping.
5 In einer älteren Version dieses Dokuments, wurde noch die unicode() genutzt, aber in python3 ist der Unicode Support deutlich besser und integrierter, daher reicht es sicherzustellen, daß etwas ein String ist.
6 Uwe Schöning, "Theoretische Informatik - kurzgefaßt", 3. Auflage 1997, Spektrum Akademischer Verlag, Heidelberg, Berlin, S. 36 (ISBN 3-8274-0250-6).
7 Uwe Schöning, a.a.O., S. 37 .
8 Siehe dazu https://docs.python.org/3/reference/lexical_analysis.html#string-and-bytes-literals.
9 Im folgenden wird der Inhalt der Webseiten https://docs.python.org/3/library/re.html#regular-expression-syntax ff. als bekannt vorausgesetzt.
10 Bildlich also eine Funktion schneide().
11 Vgl. UTF-8 in der #SchrifttumsLinguistik Mindmap.
12 Siehe https://docs.python.org/3/library/re.html#re.finditer.
13 Siehe unter 3.1.2 auf https://docs.python.org/3/tutorial/introduction.html#tut-strings.
14 Oder auch: 'der erkannten Wörter'.
15 Public Domain Funktion aus "Text Processing in Python" von David Mertz (Volltext aufrufbar unter http://gnosis.cx/TPiP/) siehe Kapitel 3 http://gnosis.cx/TPiP/chap3.txt unter SECTION 0, leicht verändert.
Zum Public Domain Status siehe http://gnosis.cx/TPiP/intro.txt unter 'SECTION 5'.
16 Siehe Beispiele 01.
17 Whitespaces sind mehr als und andere Leerzeichen, siehe die als bekannt (vgl. Fußnote 9) vorausgesetzte Seite https://docs.python.org/3/library/re.html#regular-expression-syntax.
18 Sogenannte 'Mehrwortlexeme', vgl. dazu 20.
19 Vgl. Schiller/Teufel/Stöckert/Thielen a.a.O. (Siehe 2).
20 Aus zusammenhängenden Wortgruppen bestehende sprachliche Einheiten, Wolfgang Wahlster, "Natürlichsprachliche Systeme - Eine Einführung in die sprachorientierte KI-Forschung", S.17 unter 2.1, Volltext unter http://www.dfki.de/~wahlster/Publications/Natuerlichsprachliche_Systeme_Einfuehrung.pdf.
21 Erinnern Sie sich noch an die als bekannt vorausgesetzte Seite https://docs.python.org/3/library/re.html#regular-expression-syntax? (Vgl. 9).
22 Siehe http://en.wikipedia.org/wiki/Part-of-speech_tagging.
import time
print("Zuletzt geändert:", time.strftime("%d.%m.%y %H:%M:%S", time.localtime()))