Erster Blick auf Universal Dependencies – Teil 2

In Teil 1 habe ich Universal Dependencies vorgestellt und einige der Konzepte der Baumbanken gestreift. Wir werden uns jetzt die deutsche Baumbank gründlicher angucken. Ihr könnt die Datei ud-treebanks-v2.0.tgz hier herunterladen. Nach dem Auspacken seht ihr mehrere Ordner, die UD plus die Sprache heißen. In dem UD_German-Ordner findet ihr die Datei. Sie heißt de-ud-train.conllu. Sie enthält die Trainingsdaten, mit denen ein Algorithmus des maschinellen Lernens trainiert werden kann, um die Merkmale der deutschen Sätze (die Struktur, der Worttyp, usw.) zu erkennen. Lasst uns anfangen.

Bücherregale gefüllt mit Büchern in einer Bibliothek.
Library von Kevin Wong unter BY 2.0.
Aus dem Original ausgeschnitten und Farbeffekte angewandt.

Universal Dependencies parsen

Das CoNLL-U-Dateiformat enthält Sätze, die in individuelle Wörter und Mehrfachwortelemente eingeteilt sind. Ein Beispiel eines Mehrfachwortelements ist „vom“ („von dem“ / „von einem“). Einige Wörter (auch Tokens genannt) stellen Zahlen und Symbole wie Kommas, Punkte, Ausrufezeichen und sogar Emoticons dar.

Hier ist ein Beispiel des Formats:

CoNNL-U (Leerzeichen statt Tabs) Gist

# sent_id = train-s2
# text = Die Kosten sind definitiv auch im Rahmen.
#id     form            lemma           upostag         xpostag         feats                                                                   head    deprel          deps    misc
1       Die             der             DET             ART             Case=Nom|Definite=Def|Gender=Fem|Number=Sing|PronType=Art               2       det             _       _
2       Kosten          Kosten          NOUN            NN              Case=Nom|Gender=Fem|Number=Sing                                         3       nsubj:pass      _       _
3       sind            sein            VERB            VAFIN           Mood=Ind|Number=Sing|Person=3|Tense=Pres|VerbForm=Fin                   0       root            _       _
4       definitiv       definitiv       ADV             ADJD            _                                                                       3       advmod          _       _
5       auch            auch            ADV             ADV             _                                                                       3       advmod          _       _
6-7     im              _               _               _               _                                                                       _       _               _       _
6       in              in              ADP             APPR            _                                                                       8       case            _       _
7       dem             der             DET             ART             Case=Dat|Definite=Def|Gender=Masc,Neut|Number=Sing|PronType=Art         8       det             _       _
8       Rahmen          Rahmen          NOUN            NN              Case=Dat|Gender=Masc,Neut|Number=Sing                                   3       obl             _       SpaceAfter=No
9       .               .               PUNCT           $.              _                                                                       3       punct           _       _

Viele Dinge passieren in diesem Beispiel, aber das Format ist ziemlich einfach. Es ist ein Zeilen-basiertes Format. Jede Zeile hat mehrere Spalten. Hier ist eine visuelle Darstellung der oben genannten Daten:

Visuelle Darstellung des Satzes „Die Kosten sind definitiv auch im Rahmen.“ mit Lemmata und POS-Tags des CoNLL-U-Formats unten.
Abbildung 1: Visuelle Darstellung des Satzes „Die Kosten sind definitiv auch im Rahmen.“ mit Lemmata und POS-Tags des CoNLL-U-Formats unten.

Die erste Spalte ist die ID des Wortes. Normalerwiese haben Wörter 1, 2, 3, usw. Mehrfachwortelemente haben ID’s wie z.B. 6-7 („im“), die in 6 („in“) und 7 („dem“) aufgeteilt werden.

Die zweite Spalte ist die Wortform; wie das Wort genau im Satz geschrieben wird. Das dritte Spalte ist das Lemma, d.h. die reduzierte Form des Wortes ohne Deklinationen, das ich in der letzten Post erwähnt habe.

Die vierte Spalte ist das universelle POS-Tag, das den Typ des Wortes in der Universal-Dependencies-Terminologie zeigt. Dieses Tag ist sprachenübergreifend. Die fünfte Spalte ist das lokale POS-Tag, das den sprachspezifischen Typ des Wortes zeigt. Da sich Sprachen voneinander unterscheiden, wird das lokale POS-Tag auch zwischen verschiedenen Sprachen unterschieden. Das universelle POS-Tag versucht jedoch einen generalisierten Worttyp zu erfassen. Einer der Absichten des Universal-Dependencies-Projektes ist eine für alle Sprachen universell anwendbare Liste der Worttypen und Relationen zu publizieren.

Die verbleibenden Spalten annotieren mehrere andere Wortmerkmale und verknüpfen die Wörter eines Satzes mit anderen Wörtern. Die Wortrelationen erlauben ein besseres Verständnis der Satzstruktur.

Um das CoNLL-U-Format zu parsen, habe ich ein kleines Node.js-Modul entwickelt. Ihr könnt es als conllu-stream auf NPM herunterladen. Dadurch können die Dateien schnell geparst und in der Daten angesehen werden. Hier ist ein Beispiel:

Node.js Gist

var conllu = require('conllu-stream');
var fs     = require('fs');

fs.createReadStream('ud-treebanks-v2.0/UD_German/de-ud-train.conllu')
    .pipe(conllu())
    .on('data', sentence => {
        console.log(sentence.features.sent_id, sentence.toString());
    });

Es drückt alle Sätze aus, insgesamt 14118.

Grundsätzliche Statistiken

Um einen Eindruck von der Baumbank zu bekommen, kann man ein einfaches Programm schreiben, das Statistiken der Daten sammelt. Lasst uns das Histogram der Wortformen und Lemmata berechnen:

Node.js Gist

var _      = require('lodash');
var conllu = require('conllu-stream');
var fs     = require('fs');

// Function to print value as percent (nicely).
function percent(value) {
    value *= 100;
    return  isNaN(value) ? '' :
            value > 10   ? value.toPrecision(3)+'%' :
            value > 1    ? value.toPrecision(2)+'%' :
                           value.toPrecision(1)+'%';
}

// Function to calculate and display histogram.
// It first calculates the histogram of the primary `key`.
// For each key it calculates a secondary histogram of the seconday `linkKey`.
function displayHistogram(words, key, linkKey) {

    // Calculate `key` histogram of words, sorted by the frequenzy.
    var grouped   = _.groupBy(words, key);
    var histogram = _(grouped)
        .mapValues('length')
        .toPairs()
        .sortBy([ 1, 0 ])
        .reverse()
        .value();

    // Get top-10 and bottom-10 parts of the histogram.
    var top       = histogram.slice(0, 10);
    var bottom    = histogram.slice(-10);

    // Combine top and bottom parts to display.
    var entries   = top.concat([['--']]).concat(bottom);

    // For each entry, calculate top-6 of secondary `linkKey` histogram.
    entries.forEach(entry => {
        entry[2] = percent(entry[1] / words.length);
        var linked = _.map(grouped[entry[0]], linkKey);
        entry[3] =
            _(linked)
            .groupBy()
            .mapValues('length')
            .toPairs()
            .sortBy([ 1, 0 ])
            .reverse()
            // Show percent values for each item.
            .map(item => `${item[0]} (${percent(item[1]/linked.length)})`)
            .slice(0, 6)
            .join(', ');
    });

    // Display table of results.
    console.log('-- %s --', key);
    console.log();
    console.log(entries.map(entry => entry.join('\t')).join('\n'));
    console.log();
    console.log('#words     :', words.length);
    console.log('#histogram :', histogram.length);
    console.log();

}


// Array to store all word objects we encounter.
var words = [];

// Parse CoNLL-U file.
fs.createReadStream('ud-treebanks-v2.0/UD_German/de-ud-train.conllu')
    .pipe(conllu())
    .on('data', sentence => {
        // Collect all words and lemmas in lowercase (except punctuation/numbers).
        sentence.getSequence()
            .filter(word => [ 'PUNCT', 'NUM' ].indexOf(word.upostag) === -1)
            .forEach(word => {
                // Using lowercase so "Haus" and "haus" are counted together.
                word.form  = word.form.toLowerCase();
                word.lemma = word.lemma.toLowerCase();
                words.push(word);
            });
    })
    .on('end', () => {
        // Calculate and display histograms of words vs. lemmas and vice versa.
        displayHistogram(words, 'form', 'lemma');
        displayHistogram(words, 'lemma', 'form');
        console.log('-- done --');
    });

Die Resultate werden in den folgenden Tabellen angezeigt:

Tabelle 1: Die am häufigsten und am seltensten vorkommenden Wortformen mitsamt der entsprechenden Lemmata.
# Form Frequency Lemmas
1 der 4,1% der98,7%, d1,3%
2 die 3,2% der99,2%, d0,7%, die0,03%
3 in 3,2% in100%, er0,03%
4 und 2,9% und100%
5 dem 2,5% der99,8%, d0,2%
6 von 1,5% von100%
7 zu 1,3% zu100%
8 den 1,1% der99,5%, d0,5%
9 das 1,0% der98,7%, d1,3%
10 mit 0,9% mit100%
46337 1,2 0,0004% 1,2100%
46338 08 0,0004% 08100%
46339 07/10/11 0,0004% 07/10/11100%
46340 020c 0,0004% 020c100%
46341 .limitierte 0,0004% .limitierte100%
46342 .einzelne 0,0004% .einzelne100%
46343 .. 0,0004% ..100%
46344 . 0,0004% .100%
46345 ) 0,0004% )100%
46346 'm 0,0004% 'm100%
Tabelle 2: Die am häufigsten und am seltensten vorkommenden Lemmata mitsamt der entsprechenden Wortformen (klein geschrieben).
# Lemma Frequency Forms
1 der 12,9% der31,1%, die24,9%, dem19,4%, den8,9%, das8,0%, des6,8%, …
2 in 3,2% in100%
3 und 2,9% und100%
4 sein 2,6% ist32,8%, war21,0%, sind12,3%, seine7,1%, sein6,1%, seiner5,0%, …
5 ein 2,3% eine30,4%, ein29,8%, einer13,0%, einen10,7%, einem10,7%, eines5,3%, …
6 von 1,5% von99,7%, v.0,3%
7 werden 1,5% wurde43,9%, werden19,6%, wird18,3%, wurden12,5%, worden2,8%, würde1,0%, …
8 zu 1,3% zu100%
9 er 1,1% er86,9%, ihm7,2%, ihn5,9%, in0,08%
10 mit 0,9% mit100%
38755 1,2 0,0004% 1,2100%
38756 08 0,0004% 08100%
38757 07/10/11 0,0004% 07/10/11100%
38758 020c 0,0004% 020c100%
38759 .limitierte 0,0004% .limitierte100%
38760 .einzelne 0,0004% .einzelne100%
38761 .. 0,0004% ..100%
38762 . 0,0004% .100%
38763 ) 0,0004% )100%
38764 'm 0,0004% 'm100%

Es gibt einige interessante Beobachtungen. Artikel wie der/dem/die/den/das/des sind alleine für 12,9% aller Wörter verantwortlich. Es scheint auch so, als wären die Leute, die die Lemmata annotierten, sich nicht einig darüber, ob sie das Lemma der oder d verwenden sollten. In diesem Fall wäre der die gebräuchlichere Variante. In einigen Fällen könnten die Lemmata falsch zugeordnet sein, z.B., ist „in“ in 0,03% aller Fälle als er markiert. In den Rohdaten sieht es wie ein Tippfehler aus. Es sollte „ihn“ heißen, was genau mit dem Lemma er verknüpft ist. Wenn wir in den Tabellen für die am seltensten vorkommenden Wortformen und Lemmata anschauen, bemerken wir Kalenderdaten, Codes oder falsch markierte Zahlen oder Interpunktionen. Wir haben absichtlich Zahlen (NUM) ausgelassen, aber einige Zahlen sind als Eigennamen (PROPN) oder Nomen (NOUN) markiert. Im Falle von „.limitierte“ und „.einzelne“ sieht es aus, als hätte ein fehlendes Leerzeichen nach dem Punkt dazu geführt, dass sie als Wörter statt neue Sätze markiert werden. Bei Datensätzes wie diesem, die manuell von Leuten gebaut werden und auf Textkorpura basieren, die selber Tippfehler enthalten, sind Inkonsistenzen zu erwarten.

Als eine letzte Übung können wir alle Mehrfachwortelemente identifizieren und auflisten. Hier ist der Code:

Node.js Gist

var _      = require('lodash');
var conllu = require('conllu-stream');
var fs     = require('fs');

var multiwords = [];

fs.createReadStream('ud-treebanks-v2.0/UD_German/de-ud-train.conllu')
    .pipe(conllu())
    .on('data', sentence => {
        // Collect all words and lemmas in lowercase.
        sentence.structure.multiwords
            .map(id => sentence.tokens[id])
            .forEach(multiword => {
                // Get expanded form of the multiword.
                var expansion =
                    _.range(multiword.position, multiword.endPosition+1)
                    .map(id => sentence.tokens[''+id].form)
                    .join(' ');

                // Store multiword and its expansion.
                multiwords.push(multiword.form.toLowerCase() +
                    '\t-->\t' + expansion.toLowerCase());
            });
    })
    .on('end', () => {
        // Calculate and show histogram sorted by frequency.
        console.log(
            _(multiwords)
            .groupBy()
            .mapValues('length')
            .toPairs()
            .sortBy([ 1, 0 ])
            .reverse()
            .map(row => row.join('\t\t'))
            .join('\n')
        );
    });

Hier sind die Resultate des Programmes:

Tabelle 3: Kurzformen und ihre Frequenz aus der deutschen Universal-Dependencies-Baumbank.
# Kurzform Erweiterte Form Frequenz
1 im in dem 51,2%
2 zum zu dem 14,6%
3 zur zu der 11,9%
4 am an dem 10,7%
5 vom von dem 5,91%
6 beim bei dem 3,71%
7 ins in das 1,61%
8 ans an das 0,19%
9 ums um das 0,17%
10 aufs auf das 0,064%
11 übers über das 0,021%
12 am an dem an dem* 0,021%

Das ist alles für heute. Im nächsten Eintrag schauen wir uns Wiktionary und die Vor- und Nachteile dieses Datensatzes im Vergleich zu Universal Dependencies an. Wenn ihr mit den Daten selber herumspielen möchtet, könnt ihr euch das Gist ansehen, wo ich alle Code-Schnipsel geteilt habe.