CoNLL-Datei und NLTK

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Antworten
MarcelF6
User
Beiträge: 226
Registriert: Samstag 3. März 2012, 21:30

Hallo miteinander

Ich möchte gerne aus einer CoNLL-Datei (s. unten) die Nomen extrahieren, welche ein Genitivattribut haben. Bisher habe ich es so gemacht, dass ich das File importiert habe, und dann Zeile für Zeile nach Gen-Attributen gesucht habe und dann jeweils das vorangehende Nomen extrahiert habe. Im Grunde hat es so funktioniert, war jedoch relativ langsam für grosse Dateien.
Da ich auf einem Forum gelesen habe, dass man mit NLTK viel eleganter CoNLL-Dateien handeln kann, wollte ich fragen, wie das geht? Leider gab es dazu kein Beispiel o.Ä.

Ein Ausschnitt meiner CoNLL-Datei sieht wie folgt aus:
1 Janine Janine N NE _|Nom|Sg 2 subj _ _
2 langweilte langweilen V VVFIN 3|Sg|Past|_ 0 root _ _
3 sich sie PRO PRF 3|_|_ 2 obja _ _
4 so so ADV ADV _ 5 adv _ _
5 sehr sehr ADV ADV _ 2 adv _ _
6 . . $. $. _ 0 root _ _

1 Es es PRO PPER 3|Sg|Neut|Nom 2 subj _ _
2 war sein V VAFIN 3|Sg|Past|Ind 0 root _ _
3 ein eine ART ART Indef|_|Nom|Sg 4 det _ _
4 Morgen Morgen N NN _|Nom|Sg 2 pred _ _
5 des die ART ART Def|Masc|Gen|Sg 6 det _ _
6 Montags Montag N NN Masc|Gen|Sg 4 gmod _ _
7 und und KON KON _ 2 kon _ _
8 sie sie PRO PPER 3|Sg|Fem|Nom 9 subj _ _
9 saß sitzen V VVFIN 3|Sg|Past|Ind 7 cj _ _
10 in in PREP APPR Dat 9 pp _ _
11 der die ART ART Def|Fem|Dat|Sg 12 det _ _
12 Stunde Stunde N NN Fem|Dat|Sg 10 pn _ _
13 der die ART ART Def|Fem|Gen|Sg 14 det _ _
14 Mathematik Mathematik N NN Fem|Gen|Sg 12 gmod _ _
15 . . $. $. _ 0 root _ _

1 Sie sie PRO PPER 3|Sg|Fem|Nom 2 subj _ _
2 kniff kneifen V VVFIN 3|Sg|Past|Ind 0 root _ _
3 sich sie PRO PRF 3|_|Dat 12 objd _ _
4 in in PREP APPR Acc 12 pp _ _
5 die die ART ART Def|_|Acc|_ 6 det _ _
6 Spitze Spitz N NN _|Acc|_ 4 pn _ _
7 der die ART ART Def|Masc|Gen|Pl 8 det _ _
8 Finger Finger N NN Masc|Gen|Pl 6 gmod _ _
9 um um PREP APPR _ 12 pp _ _
10 wach wach ADV ADJD Pos| 9 pn _ _
11 zu zu PTKZU PTKZU _ 12 part _ _
12 bleiben bleiben V VVINF _ 2 obji _ _
13 . . $. $. _ 0 root _ _

1 Das die ART ART Def|Neut|_|Sg 2 det _ _
2 Haus Haus N NN Neut|_|Sg 0 root _ _
3 des die ART ART Def|Masc|Gen|Sg 4 det _ _
4 Bürgermeisters Bürgermeister N NN Masc|Gen|Sg 2 gmod _ _
5 . . $. $. _ 0 root _ _

1 Der die ART ART Def|Masc|Nom|Sg 2 det _ _
2 Anstieg Anstieg N NN Masc|Nom|Sg 0 root _ _
3 der die ART ART Def|_|Gen|Pl 4 det _ _
4 Kosten Kosten N NN _|Gen|Pl 2 gmod _ _
5 . . $. $. _ 0 root _ _

1 Der die ART ART Def|Fem|_|Sg 2 det _ _
2 Eingliederung Eingliederung N NN Fem|_|Sg 0 root _ _
3 der die ART ART Def|Masc|Gen|Pl 4 det _ _
4 Spätaussiedler Spätaussiedler N NN Masc|Gen|Pl 2 gmod _ _
5 . . $. $. _ 0 root _ _

1 Ein ein ART ART Indef|_|_|Sg 2 det _ _
2 Drittel Drittel N NN _|_|Sg 0 root _ _
3 der die ART ART Def|_|Gen|Pl 4 det _ _
4 Kosten Kosten N NN _|Gen|Pl 2 gmod _ _
5 . . $. $. _ 0 root _ _

1 Eine eine ART ART Indef|Fem|_|Sg 2 det _ _
2 Dame Dame N NN Fem|_|Sg 0 root _ _
3 eines eine ART ART Indef|_|Gen|Sg 5 det _ _
4 gewissen gewiss ADJA ADJA Pos|_|Gen|Sg|_| 5 attr _ _
5 Alters Alter N NN _|Gen|Sg 2 gmod _ _
6 . . $. $. _ 0 root _ _

1 Das die ART ART Def|Neut|_|Sg 2 det _ _
2 Glück Glück N NN Neut|_|Sg 0 root _ _
3 der die ART ART Def|Fem|Gen|Sg 4 det _ _
4 Zufriedenheit Zufriedenheit N NN Fem|Gen|Sg 2 gmod _ _
5 . . $. $. _ 0 root _ _
Vielen Dank für die Hilfe! :)
nezzcarth
User
Beiträge: 1633
Registriert: Samstag 16. April 2011, 12:47

Die CoNLL-Dateien, die ich bislang gesehen habe, waren von der Struktur her TSV-Dateien (s.a.: http://anthology.aclweb.org/W/W06/W06-2920.pdf). Die kannst du zum Beispiel mit dem CSV-Modul aus der Standardbibliothek einlesen und verarbeiten. Wenn es NLTK sein soll: Hattest du das hier schon mal angesehen?
MarcelF6
User
Beiträge: 226
Registriert: Samstag 3. März 2012, 21:30

Vielen Dank für die Tipps.

Ich habe es nun mal auf die "dirty"-Variante probiert, siehe:

Code: Alles auswählen

nouns = set()
conll = []

f = open('test.CoNLL', 'rU')
text = f.read()

for line in text.split("\n"):
    conll.append(line)

for index in conll:
    if len(index.split()) > 5 and "Gen" in index.split()[5]:
        print index.split()[1]
Dabei ist "test.CoNLL" genau die Datei, welche ich hier schon als Beispiel gezeigt habe.
Mein Problem ist nun eigentlich mehr, dass ich ja an den Nomen interessiert bin, welche ein Genitivattribut haben (also interessieren vor allem die Köpfe). Mit meiner Variante oben extrahiere ich zwar völlig korrekt alles, was im Genitiv steht. Aber das sind ja noch nicht die Köpfe.
--> Ist das immer so, dass die Köpe eine Stelle vor den Genitiven stehen (ok, das ist mehr eine sprachwissenschaftliche und keine programmiertechnische Frage ;) ).

--> Falls ja: wie kann ich da die Köpfe extrahieren? Einfach den Index (erste Zahl pro Zeile) minus 1, oder gäbe es da eine schönere Lösung? :)

Danke bestens für jeglichen Tipp! :)
Zuletzt geändert von Anonymous am Freitag 19. Mai 2017, 12:27, insgesamt 1-mal geändert.
Grund: Quelltext in Python-Codebox-Tags gesetzt.
BlackJack

@MarcelF6: Dreckig passt als Beschreibung. :-)

Die Datei wird nicht wieder geschlossen.

``text.split('\n')`` hat als Ergebnis eine Liste. Und Du gehst da jedes Element einzeln durch und hängst die alle an eine anfangs leere Liste an. Am Ende hast Du dann die gleiche Liste die der `split()`-Aufruf sowieso schon geliefert hat.

Statt alles als eine grosse Zeichenkette einzulesen und die dann an den Zeilenenden zu zerlegen, hätte man auch gleich eine Liste mit Zeilen aus dem Dateiobjekt erstellen können. Einfach `list()` mit dem Dateiobjekt aufrufen, denn das ist ja iterierbar und liefert dabei pro Schritt eine Zeile.

In der letzten Schleife nennst Du die Zeilen `index`, was ziemlich verwirrend ist, denn das ist ja gar kein Index.

Und *in* der Schleife wird dann die Zeile bis zu dreimal an Whitespace aufgeteilt. Dabei kommt jedes mal das gleiche heraus, es macht also keinen Sinn das wirklich drei mal zu machen, etwas mit dem Ergebnis zu tun, und es dann gleich wieder zu verwerfen.

Zur Struktur der Datei hat nezzcarth einen Link zu einem PDF gepostet wo auf Seite drei der PDF-Datei (Seitennummer im Dokument: 151) das Format beschrieben zu sein scheint. Also auch was die einzelnen Felder einer Zeile bedeuten. Ich habe kurz drüber geschaut und es sieht so aus, als wenn ein Feld als Referenz auf den Kopf existiert, also die ID der Zeile im aktuellen Satz enthält, auf die sich die aktuelle ID bezieht.

Für den Test im 'FEATS'-Feld würde ich übrigens am '|' aufteilen und dann erst testen. Dann ist das Programm robust dagegen falls man auf ein Feature testen möchte das als Teilzeichenkette eines anderen Features vorkommen kann.

Generelles Vorgehen ist hier wie immer das Problem auf Teilprobleme runter zu brechen und Funktionen zu schreiben die Teilprobleme lösen. Eine ”Achse” der sinnvollen Aufteilung sind Wiederholungen gleicher Tätigkeiten. So eine Datei mit Deinem Ziel auszuwerten, bedeutet alle Sätze auszuwerten. Das heisst *einen* Satz auszuwerten, wäre ein Teil den man in einer Funktion erledigen könnte. Wenn man die hat, kann man sie einfach auf alle Sätze nacheinander anwenden. Vorher braucht man eine Funktion welche die Eingabedaten satzweise aufteilt.

Einen Satz auswerten, bedeutet die einzelnen Token auszuwerten. Also braucht man eine Funktion die einen Satz in eine passende Datenstruktur überführt, zum Beispiel eine Liste mit Daten die ein Token repräsentieren. Dafür würde sich ein Wörterbuch anbieten das einen Eintrag pro Spalte hat oder ein `collections.namedtuple()`. Eine Liste ginge auch, aber dann ist der Quelltext voller magischer Zahlen für die Indexzugriffe.

Dann geht man die Liste mit den Token durch, schaut ob's ein 'Gen'-Feature enthält, und auf welchen Kopf verwiesen wird. Das Token mit dem Kopf kann man dann aus der Liste holen und die gewünschten Daten daraus sammeln, ausgeben, speichern, zurückgeben, whatever.

Hier mal Dein dreckiger Code gewaschen und gebügelt (ungetestet):

Code: Alles auswählen

    with open('test.CoNLL', 'rU') as lines:
        for line in lines:
            row = line.split()
            # `row` has either zero or ten elements (see PDF).
            if row and 'Gen' in row[5].split('|'):
                print row[1]
nezzcarth
User
Beiträge: 1633
Registriert: Samstag 16. April 2011, 12:47

Dieses CoNLL-Format ist halt nicht ganz einheitlich, was die Bedeutung der Spalten angeht. Um das genau zu wissen, müsste man herausfinden, wie die Daten erzeugt wurden.
MarcelF6 hat geschrieben: --> Ist das immer so, dass die Köpe eine Stelle vor den Genitiven stehen (ok, das ist mehr eine sprachwissenschaftliche und keine programmiertechnische Frage ;) ).

--> Falls ja: wie kann ich da die Köpfe extrahieren? Einfach den Index (erste Zahl pro Zeile) minus 1, oder gäbe es da eine schönere Lösung? :)
Nein, das ist nicht immer so. Allerdings hast du hier ja schon Daten, die scheinbar nach einem Dependenzmodell analysiert wurden. Der kompliziertere Teil der Arbeit ist also bereits erledigt und du musst nur noch die passenden Elemente aus dem Baum heraussuchen.

Ergänzend/aufbauend auf den sehr hilfreichen Hinweisen von BlackJack folgende Spielerei, die eine Variante zeigt, wie man mit solchen Daten umgehen könnte:

Code: Alles auswählen

In [1]: import csv

In [2]: names = ('ID', 'FORM', 'LEMMA', 'CPOSTAG', 'POSTAG', 'FEATS', 'HEAD', 'DEPREL', 'PHEAD', 'PDEPREL')

In [3]: with open('test.conll') as f:
   ...:     reader = csv.DictReader(f, fieldnames=names, delimiter='\t')
   ...:     sentences = [sentence for sentence in reader]
   ...:     

In [4]: with open('dependencies.dot', 'w+') as f:
   ...:     print('digraph dependencies {', file=f)
   ...:     for sentence in sentences:
   ...:         print('{id} [label="{form}"];'.format(id=sentence['ID'], form=sentence['FORM']), file=f)
   ...:         print('{id} -> {head} [label="{deprel}"];'.format(id=sentence['ID'], head=sentence['HEAD'], deprel=sentence['DEPREL']), file=f)
   ...:     print('}', file=f)

Code: Alles auswählen

$ dot -Tpng dependencies.dot > dependencies.png
Ausgabe:
https://mfile.io/mqu1Vt

An dem Diagramm kannst du auch erkennen, wie du die gesuchten Nomina finden kannst.
MarcelF6
User
Beiträge: 226
Registriert: Samstag 3. März 2012, 21:30

Hallo BlackJack und nezzcarth :)

Vielen Dank für eure verständlichen und erklärenden Worte! Ich habe mittlerweile Teile von euren Vorschlägen integriert - was mich aber noch wunder nimmt ist die Andeutung des letzten Beitrags: Wenn man das Diagramm sieht, ist klar, welches der Kopf des Genitivatrributs ist. Wie aber kann man direkt (und formal, d.h. automatisiert) bestimmen, welches der Kopf ist?

Vielen Dank für eure Tipps! :)
MarcelF6
User
Beiträge: 226
Registriert: Samstag 3. März 2012, 21:30

* Ich korrigiere mich gleich selbst: Bei den Dependenz-Daten kann man die Zahlen am Schluss betrachten. Beispiel:
5 die die ART ART Def|_|Acc|_ 6 det _ _
6 Spitze Spitz N NN _|Acc|_ 4 pn _ _
7 der die ART ART Def|Masc|Gen|Pl 8 det _ _
8 Finger Finger N NN Masc|Gen|Pl 6 gmod _ _
Hier wäre ja "Spitze" der Kopf; das heisst man kann wie folgt vorgehen:
1.) Nach Nomen + Genitiv suchen
2.) hintere Zahl auslesen
3.) die zuletzt vorkommende Zahl suchen und dann die nächstfolgende Zeile mit einem Nomen auslesen
4.) Diese Zeile ist dann der Kopf

Was meint ihr zu diesem Algorithmus?
Ist der in Ordnung so? (Ich frage mich nur gerade, ob es eventuell Fälle mit 2 Nomen als Köpfe gibt...denke aber, dass dies nicht der Fall ist (oder?))

Sorry, wieder eine eher sprachliche Frage ;) Vielen Dank für eure Tipps! :)
nezzcarth
User
Beiträge: 1633
Registriert: Samstag 16. April 2011, 12:47

MarcelF6 hat geschrieben: Was meint ihr zu diesem Algorithmus?
Ist der in Ordnung so? (Ich frage mich nur gerade, ob es eventuell Fälle mit 2 Nomen als Köpfe gibt...denke aber, dass dies nicht der Fall ist (oder?))
Schau dir die Daten, die du hast, doch noch mal genauer an. ;) Vielleicht übersehe ich etwas, aber ich habe den Eindruck, du machst es dir zu kompliziert. Mein Beispiel war eigentlich bewusst so gewählt, dass da genau die Felder vorkommen, die du meiner Meinung nach brauchst. Die Daten wurden ja scheinbar von einem Parser anhand einer Depedenzgrammatik analysiert und die Relation eines Elements zu seinem Kopf ist bereits in einer der Spalten kodiert. Wenn du versuchst, die Genitivattribute topologisch und über die Wortklasse zu bestimmen, ist das fehleranfälliger und du verwendest die Informationen, die du bereits hast gar nicht.

Eigentlich sollte es doch so gehen: Such die Wörter, die in der Spalte, die die Dependenzrelation angibt, als Genitivattribute gekennzeichnet sind, nimm dann den Wert, der im Feld für den Kopf steht und such dir das Wort mit der passenden ID heraus -- fertig. Wenn man möchte, kann man sich jetzt ggf. noch ein paar Gedanken dazu machen, wie man die Daten so verpackt, dass das elegant machbar ist. Bei dieser Vorgehensweise kannst du die (nicht ganz einfache) Frage nach mehreren Köpfen ignorieren; wenn das Grammatikmodell, das dem Parser zugrunde liegt, diese vorsieht, sollten sie im entsprechenden Feld gekennzeichnet sein, wenn nicht, dann würde ich sagen, dass diese für dich in dem Kontext nicht relevant sind.
MarcelF6
User
Beiträge: 226
Registriert: Samstag 3. März 2012, 21:30

Ok, ich habe es im Grunde nicht viel unterschiedlicher gemacht. Aber du hättest es also "unkomplizierter" hingebracht?

Hier meine aktuelle Version, wobei sentences eine vom csv-DictReader erstellte list und nouns ein Counter-Objekt ist:

Code: Alles auswählen

i = 0
for lines in sentences:
    if lines['POSTAG'] == 'NN' and 'Gen' in lines['FEATS']:
        dep = lines['HEAD']
        while sentences[i-1]['HEAD'] != dep:
            i = i-1
        while sentences[i]['POSTAG'] != 'NN':
            i = i+1
        else:
            nouns.update(sentences[i]['FORM'].split())
    i = i+1
Zuletzt geändert von Anonymous am Dienstag 23. Mai 2017, 07:23, insgesamt 1-mal geändert.
Grund: Quelltext in Python-Codebox-Tags gesetzt.
BlackJack

@MarcelF6: Ich wüsste jetzt auf Anhieb nicht einmal ob das mit dem `i` was Du da machst, auch tatsächlich immer funktioniert. Ist auf jeden Fall ziemlich ineffizient und verwirrend. Und das kommt wohl hautpsächlich daher das Du das Problem und die Daten nicht ordentlich aufteilst. Und die Namen sind falsch. `sentences` enthält keine Sequenz von Sätzen, sondern eine Sequenz von Token von allen Sätzen. Und `lines` steht für *ein* Token, was man dem Namen so gar nicht ansieht.

Die erste Aufteilung der Daten wäre tatsächlich nach Sätzen, so das der Name `sentences` nicht mehr falsch ist, so das man jeden Satz einzeln betrachten kann. Dann kann man auch direkt die Verweise auf andere Token aus den Daten verwenden (-1 weil die IDs bei 1 anfangen).
nezzcarth
User
Beiträge: 1633
Registriert: Samstag 16. April 2011, 12:47

@BlackJack: Mit den Sätzen hast du recht, das war in meinem Beispiel auch falsch :( Danke für den Hinweis.

@MarcelF6: Weshalb versuchst du denn, die Gentivattribute selbst irgendwie über die POS-Tags und die Wortstellung zu bestimmen? In den Eingabedaten sind sie doch, wie gesagt, explizit markiert und du müsstest einfach nur den entsprechenden Wert in der Spalte, die die Art der Dependenzrelation angibt (die Kantenbeschriftung im gezeigten Diagramm) auslesen.
MarcelF6
User
Beiträge: 226
Registriert: Samstag 3. März 2012, 21:30

Darf ich fragen, wie ihr das Problem gelöst hättet?
(Ich habe das Pojekt mittlerweile zwar schon abgegeben, aber zugegeben: meine Lösung schaut schon nicht optimal aus...)
nezzcarth
User
Beiträge: 1633
Registriert: Samstag 16. April 2011, 12:47

Als Schnelllösung für einen einzigen Satz (also den Sonderfall einer Datei, die nur einen Satz enthält) und um zu verdeutlichen, wie ich das mit den Köpfen meinte, schwebte mir so etwas vor:

Code: Alles auswählen

In [1]: import csv

In [2]: names = ('ID', 'FORM', 'LEMMA', 'CPOSTAG', 'POSTAG', 'FEATS', 'HEAD', 'DEPREL', 'PHEAD', 'PDEPREL')

In [3]: with open('test.conll') as f:
   ...:     reader = csv.DictReader(f, fieldnames=names, delimiter='\t')
   ...:     tokens = [token for token in reader]
   ...:     

In [4]: attributes = [token for token in tokens if token['DEPREL'] == 'gmod']

In [5]: heads = [tokens[int(attribute['HEAD'])-1] for attribute in attributes]

In [6]: print(', '.join(str(head['FORM']) for head in heads))
Morgen, Stunde
Für eine Projektaufgabe brauchst du natürlich noch deutlich mehr. Da würde ich vermutlich dann auch nicht das CSV-Modul nehmen, sondern einen eigenen Parser basteln, der ganze Dateien einlesen und in adäquaten Datenstrukturen ablegen kann.
Antworten