7  Zusätzliche Optionen: Lemmatisierung und POS-Tagging

Optional können wir die Daten auch automatisch lemmatisieren und mit POS-Tags versehen. Für die Fragestellung, der wir in unserem Beispiel nachgegangen sind, ist das zwar zunächst nicht unmittelbar relevant, es kann aber sehr nützlich sein, wenn wir quantitative Verfahren auf die Daten anwenden wollen. Dafür nutzen wir die spacyr-Programmbibliothek. Wenn Sie sie noch nicht installiert haben, müssen Sie sie zunächst mit

install.packages("spacyr")

installieren und zudem der Anleitung unter https://spacyr.quanteda.io/ folgen und über spacy_install() Spacy installieren. Außerdem müssen wir noch ein Modell fürs Deutsche installieren, weil spacy_install() zunächst nur das englische en_core_web_sm-Modell mitinstalliert. Die aktuell verfügbaren Modelle finden Sie hier, suchen Sie ggf. unter “Releases” nach “de_core”.

Wir verwenden hier das de_core_news_sm-Modell. sm steht für small, news weist darauf hin, dass es auf News-Daten trainiert wurde.

library(spacyr)
spacy_download_langmodel("de_core_news_sm")

Alternativ oder ergänzend können wir unsere Daten auch mit UDPipe annotieren. UDPipe bietet eine NLP-Schnittstelle für Universal Dependencies, ein Framework, das sich in den letzten Jahren als Standard für die sprachübergreifende Annotation von Daten vor allem im Bereich des syntaktischen Taggings entwickelt hat. Auch hier müssen Sie ggf. zunächst mit

install.packages("udpipe")

das entsprechende Paket installieren und anschließend die passenden Sprachmodelle herunterladen und laden.

library(udpipe)
udpipe_download_model("german-gsd")
udpipe_model <- udpipe_load_model("german-gsd-ud-2.5-191206.udpipe")

7.1 Liste mit Metadaten

Bevor wir weiterarbeiten, werten wir aber zunächst die Metadaten aus, die in den von Trafilatura gecrawlten Dateien enthalten sind – auch das haben wir zuvor in Chapter 5 manuell gemacht.

library(tidyverse)

# Liste aller Dateien
f <- list.files("materialien/netzpolitik_daten/", full.names = TRUE)

# über alle Dateien iterieren
for(i in 1:length(f)) {
  # Datei einlesen
  d <- readLines(f[i])
  
  # Header auslesen
  hdr <- grep("---", d[1:20])
  
  # Metadaten in Tabelle überführen
  cur_tbl <- separate_wider_delim(tibble(x = d[(hdr[1]+1):(hdr[2]-1)]), cols = x, delim =  ": ", names = c("type", "text"), too_many = "drop")
  
  # transponieren
  cur_tbl <- as_tibble(t(cur_tbl[,2])) %>% set_names(unname(as_vector(cur_tbl[,1])))
  
  # Dateinamen hinzufügen
  cur_tbl$file <- f[i]
  
  # zu Gesamttabelle hinzufügen
  if(i == 1) {
    all_tbl <- cur_tbl
  } else {
    all_tbl <- bind_rows(all_tbl, cur_tbl)
  }
  
  # Fortschritt anzeigen (bei Bedarf auskommentieren)
  # print(i)
}


# resultierende Tabelle
all_tbl |> DT::datatable()
1
brauchen wir für einige der unten angewandten Funktionen, u.a. die separate_wider_delim-Funktion
2
erstellt eine Liste mit den Dateien, die im nächsten Schritt eingelesen werden.
3
Mit `—` wird in den Trafilatura-Ergebnisdateien der Header mit Metadaten abgegrenzt. Für den unwahrscheinlichen Fall, dass irgendwo im Text noch der String `—` vorkommt, durchsuchen wir nur die ersten 20 Zeilen.
4
Dieser Code überführt die Metadaten-Zeilen in eine Tabelle, indem er da, wo in den Metadaten der erste Doppelpunkt steht, quasi eine Spaltengrenze setzt.
5
Hier wird die Tabelle transponiert, damit sie, wenn die nächsten Datenpunkte dazukommen, nicht “nach rechts” wächst, sondern “nach unten”.
6
Es ist immer nützlich, den Dateinamen in eine solche Tabelle mit aufzunehmen, damit man die richtige Datei mit den passenden Metadaten verknüpfen kann.
7
In dem Loop werden die Metadaten Datei für Datei extrahiert; es entsteht jeweils quasi eine eigene Tabelle (tbl_cur). Hier werden diese Einzeltabellen zu einer Gesamttabelle zusammengeführt.
8
Das kann für größere Loops nützlich sein, insbesondere zum Troubleshooting, weil man dann unmittelbar sieht, bei welcher Iteration es zu Problemen gekommen ist.

7.2 Tagging mit Spacy

Nun können wir die Daten lemmatisieren und POS-taggen; Spacy kann außerdem noch Tags für sog. Named Entities hinzufügen. Hier sehen wir zunächst für eine einzelne Datei, wie das geht:

# Datei einlesen
d <- readLines(f[1])

# Header finden
hdr <- grep("---", d[1:20])

# Header entfernen
d <- d[(hdr[2]+1):length(d)]

# parsen
d_parsed <- spacy_parse(d)

# Output
head(d_parsed, 10)
   doc_id sentence_id token_id                 token                 lemma
1   text1           1        1                    In                    in
2   text1           1        2                   der                   der
3   text1           1        3                 Ampel                 Ampel
4   text1           1        4                     -                     -
5   text1           1        5             Koalition             Koalition
6   text1           1        6                  gibt                  gibt
7   text1           1        7                    es                    es
8   text1           1        8                Streit                Streit
9   text1           1        9                     :                     :
10  text1           1       10 Bundesinnenministerin Bundesinnenministerin
     pos entity
1    ADP       
2    ADJ       
3  PROPN  ORG_B
4  PUNCT  ORG_I
5  PROPN  ORG_I
6   NOUN       
7  PROPN       
8  PROPN  GPE_B
9  PUNCT       
10 PROPN       

Mit einem einfachen Loop können wir dieses Verfahren natürlich nicht nur auf eine, sondern auf alle Dateien anwenden und diese dann auch exportieren.

for(i in 1:length(f)) {
  # Datei einlesen
  d <- readLines(f[i])
  
  # Header finden
  hdr <- grep("---", d[1:20])
  
  # Header entfernen
  d <- d[(hdr[2]+1):length(d)]
  
  # parsen
  tbl_cur <- spacy_parse(d)
  
  # exportieren
  
  write_csv(tbl_cur, paste0("materialien/netzpolitik_spacy_parsed/", gsub(".*/|\\.txt", "", f[i]), ".csv"))

  # Fortschritt tracken (ggf. auskommentieren)
  print(i)
}
Tip

Für alle, die mit der Corpus Workbench (CWB) arbeiten, lassen sich auf diese Weise auch mit kleinen Modifikationen des obigen Codes VRT-Dateien erstellen.

7.3 Tagging mit UDPipe

Auch mit UDPipe können wir POS-Tags erstellen, aber auch syntaktische Dependenzannotationen; darüber hinaus können wir die Daten im CoNNL-U-Format exportieren, das mit vielen Annotationsprogrammen (z.B. INCEpTION) kompatibel ist.

# über alle Dateien iterieren
for(i in 1:length(f)) {
  # Datei einlesen
  d <- readLines(f[i])
  
  # Header finden
  hdr <- grep("---", d[1:20])
  
  # Header entfernen
  d <- d[(hdr[2]+1):length(d)]
  
  # alles in eine Zeile
  d <- paste0(d, collapse = " ")
  
  # annotiertes Dokument erstellen
  d_annotated <- udpipe_annotate(object = udpipe_model, x = d, doc_id = gsub(".*/|\\.txt", "", f[1]))
  
  # annotertes Dokument exportieren
  d_annotated$conllu |> writeLines(paste0("materialien/netzpolitik_conllu/", gsub(".*/|\\.txt", "", f[i]), ".conllu"))
  
  # Fortschritt tracken (ggf. auskommentieren)
  # print(i)
}
1
Für die korrekte Erstellung des CoNLL-U-Outputs ist es wichtig, dass der Vektor, der als Input für udpipe_annotate verwendet wird, die Länge 1 hat. Das erreichen wir, indem wir mit dem paste-Argument den gesamten Text sozusagen in eine Zeile packen.

Die so entstehenden CoNLL-U-Dokumente können wir dann z.B. auch in INCEpTION zur weiteren Bearbeitung öffnen.

Der Screencast zeigt, wie in INCEpTION ein neues Projekt erstellt wird und dann CoNLL-U-Dateien eingelesen werden

Öffnen von CoNLL-U-Dateien in INCEpTION

7.4 Fazit

Insgesamt zeigt sich, dass viele der “Advanced”-Methoden tatsächlich “advanced” sind und im besten Fall etwas computationales Vorwissen erfordern – und zudem ein gewisses Maß an Frustrationstoleranz, da man bei der Verwendung solcher Methoden gerade als Anfänger:in oft mit kryptischen Fehlermeldungen zu kämpfen hat. Wir sehen aber auch, dass diese Methoden kein Hexenwerk sind, und dank Hilfeseiten wie StackOverflow und neuerdings auch Sprachmodellen wie ChatGPT, die sich beim Troubleshooting als hilfreich erweisen können, können solche Methoden heutzutage auch von weniger programmiererfahrenen Personen verwendet werden.

Was aus linguistischer Sicht am Ende zählt, ist die Qualität der Analyse, ob sie nun eher qualitativ oder eher quantitativ orientiert ausfällt. Hier konnten wir in diesem Tutorial natürlich nur an der Oberfläche kratzen, aber ich hoffe, gezeigt zu haben, dass die Arbeit mit selbst erstellten Korpora weniger Hürden mit sich bringt, als manche vielleicht zunächst befürchten mögen – gerade wenn man sich auf die “Quick&Dirty”-Methode beschränkt.