Vorlesungsskript 26

Bäume

Im Seminar Mathematische Grundlagen der Computerlinguistik haben Sie bereits Bäume kennengelernt. Bäume spielen in der (Computer)linguistik eine wichtige Rolle, u.a. deshalb, weil sie genutzt werden, um die syntaktische Struktur von Sätzen zu beschreiben. Sie haben Bäume als mathematische Objekte kennengelernt. In dieser Vorlesung wollen wir uns einige Möglichkeiten angucken, Bäume als Python-Datenstrukturen zu repräsentieren. So können wir dann Programme schreiben, die Bäume verarbeiten, z.B. Parser, die die syntaktische Struktur von Sätzen erkennen. Bei dieser Gelegenheit lernen wir, wie wir in Python unsere eigenen Datentypen definieren können.

Verschiedene Arten von Syntaxbäumen

Man unterscheidet zwischen Konstituentenbäumen und Dependenzbäumen. In Konstituentenbäumen entsprechen Blätter Wörtern und interne Knoten (d.h. Nicht-Blätter) entsprechen Konstituenten mit zwei oder mehr Wörtern. Direkte Dominanz zwischen zwei Knoten A und B bedeutet, dass B Teil der Konstituente ist, der A entspricht. Zum Beispiel:

Konstituentenbaum des Satzes “the cat chases peter”
Ein Konstituentenbaum (Quelle: Mathematische Grundlagen der Computerlinguistik)

In Dependenzbäumen gibt es dagegen keine Konstituenten. Alle Knoten entsprechen Wörtern. Direkte Dominanz zwischen zwei Knoten A und B bedeutet, dass das Wort B vom Wort A direkt abhängig ist. Man nennt B den Dependenten und A seinen Kopf. Oft haben die Kanten eines Dependenzbaums Etiketten, die angeben, was die genaue grammatische Relation zwischen Kopf und Dependent ist. Zum Beispiel:

Dependenzbaum des Satzes “the cat chases peter”
Ein Dependenzbaum (erzeugt mit displaCy)

Hier ist das Verb chases die Wurzel. Von ihm hängen zwei Wörter direkt ab: cat (das Subjekt, die grammatische Relation ist nsubj) und Peter (das direkte Objekt, die grammatische Relation ist dobj). Von cat ist wiederum ein Wort abhängig, nämlich The (das ist sein Determinierer, die grammatische Relation ist det).

Unterbäume

Als direkten Unterbaum (engl. immediate subtree) eines Baums T bezeichnen wir einen Baum U, dessen Wurzel rU eins der Kinder der Wurzel von T ist und der ansonsten aus allen Knoten besteht, die von rU dominiert werden, und aus den Kanten zwischen ihnen. Der oben abgebildete Konstituentenbaum hat zum Beispiel diese beiden direkten Unterbäume:

Konstituentenbaum der NP “the cat” Konstituentenbaum der VP “chases peter”
Die zwei direkten Unterbäume des o.a. Konstituentenbaumes

Bäume als Listen repräsentieren

Um Bäume als Python-Objekte zu repräsentieren, können wir z.B. Listen benutzen. Ein Baum wird dabei durch eine Liste dargestellt. Ihr erstes Element ist das Etikett der Wurzel. Jedes weitere Element steht für einen direkten Unterbaum, auf die gleiche Weise als Liste dargestellt. Der oben abgebildete Konstituentenbaum wird so durch die folgende Liste repräsentiert:

ctree = ['S',
            ['NP',
                ['D',
                    ['the']],
                ['N',
                    ['cat']]],
            ['VP',
                ['V',
                    ['chases']],
                ['NP',
                    ['EN',
                        ['peter']]]]]

Und der oben abgebildete Dependenzbaum so (wir ignorieren fürs Erste die grammatischen Relationen):

dtree = ['chases',
            ['cat',
                ['The']],
            ['Peter']]

Nachdem wir nun eine Python-Repräsentation für Bäume festgelegt haben, können wir Funktionen definieren, die Sachen mit Bäumen machen:

def leaf_labels(tree):
    """Returns an iterable of the labels of all leaves of the given tree.
    """
    if len(tree) == 1: # is leaf
        yield tree[0] # label
    else:
        for subtree in tree[1:]:
	    yield from leaf_labels(subtree)
>>> list(leaf_labels(ctree))
['the', 'cat', 'chases', 'peter']
>>> list(leaf_labels(dtree))
['The', 'Peter']
def preorder(tree):
    """Does a preorder traversal of the given tree.
    
    Returns an iterable containing the labels of all nodes in preorder, that
    is: the label of the root followed by the labels of all immediate subtrees,
    in preorder.
    """
    yield tree[0] # label
    for subtree in tree[1:]: # subtrees
        yield from preorder(subtree)
    
def postorder(tree):
    """Does a postorder traversal of the given tree.
    
    Returns an iterable containing the labels of all nodes in postorder, that
    is: the label of the root after the labels of all immediate subtrees,
    in postorder.
    """
    for subtree in tree[1:]: # subtrees
        yield from postorder(subtree)
    yield tree[0] # label
>>> list(preorder(ctree))
['S', 'NP', 'D', 'the', 'N', 'cat', 'VP', 'V', 'chases', 'NP', 'EN', 'peter']
>>> list(postorder(ctree))
['the', 'D', 'cat', 'N', 'NP', 'chases', 'V', 'peter', 'EN', 'NP', 'VP', 'S']
>>> list(preorder(dtree))
['chases', 'cat', 'The', 'Peter']
>>> list(postorder(dtree))
['The', 'cat', 'Peter', 'chases']

Geht es besser?

Bäume als (verschachtelte) Listen darzustellen wie oben funktioniert, aber die Lesbarkeit und Wartbarkeit sowohl dieser Darstellungen als auch der Funktionen, die darauf operieren, lässt noch zu wünschen übrig. Zum einen sieht man den Listen nicht unbedingt an, dass es sich um Baumdarstellungen handelt und nicht um irgendwelche beliebigen Listen. Klarer wäre es, wenn wir z.B. schreiben könnten:

ctree = Tree('S',
            Tree('NP',
                Tree('D',
                    Tree('the')),
                Tree('N',
                    Tree('cat'))),
            Tree('VP',
                Tree('V',
                    Tree('chases')),
                Tree('NP',
                    Tree('EN',
                        Tree('peter')))))
dtree = Tree('chases',
            Tree('cat',
                Tree('the')),
            Tree('Peter'))

Zum anderen: Um die Funktionen zu verstehen, muss man wissen, dass tree[0] das Etikett ist und tree[1:] die direkten Unterbäume. Dieser Aspekt unserer Darstellung ist relativ willkürlich; wir hätten z.B. das Etikett auch ans Ende der Liste setzen können oder zwischen Etikett und Unterbäumen noch ein Kantenetikett einfügen können (für Dependenzbäume). Wenn wir das Format unserer Bäume irgendwann einmal ändern, müssen alle diese Funktionen geändert werden, denn wenn wir versehentlich so eine Funktion auf einen Baum in einem anderen Format anwenden, kann es großes Chaos geben. Es wäre daher gut, die Darstellung und die Funktionen irgendwie zu „koppeln“, sodass man sicherer sein kann, dass die Funktion dem aktuellen Baumformat entspricht.

Es ist daher an der Zeit, uns nicht länger mit der Handvoll Standard-Datentypen (Listen usw.) zu begnügen, die Python zur Verfügung stellt. Es ist Zeit, unseren ersten eigenen Datentyp zu definieren, und zwar einen speziell für Bäume. Das ermöglicht dann eine klarere Darstellung und Kopplung von Darstellung und Funktionen.