Zu Anfang dieser Einführung habe ich die Techniken, um die es in diesem Kapitel geht, als weniger relevant für die sozialwissenschaftliche Forschung beschrieben, was damit zusammenhängt, dass ihr Nutzen für Sozialwissenschaftler üblicherweise eher indirekt ist, während den in diesem Abschnitt behandelten Verfahren in der Computerlinguistik in der Regel eine zentrale Rolle zukommt. Diese Charakterisierung sollte aber nicht den Eindruck erwecken, Tagging (die Bestimmung von Wortklassen), Parsing (die Bestimmung von Satz–Konstituenten) und Entitätenerkennung (oder engl. “named entity recognition”) hätten in dieser Einführung keinen Platz. Im Gegenteil, diese Methoden können maßgeblich zur Verbesserung von Resultaten aus den vorausgehenden Kapiteln beitragen, wie die folgenden Beispiele hoffentlich überzeugend belegen. Ein basales Verständnis von Sprache ist für sozialwissenschaftliche Inhaltsanalysen auch deshalb hilfreich, weil man sich bspw. nicht blind auf komplexe Verfahren wie Themenmodelle verlassen muss, wenn man eine Vorstellung davon hat, welche linguistischen Features besonders relevant dafür sind, welche Themen in einem Text eine Rolle spielen und welche sprachlichen Merkmale diese Themen aufweisen. Auch sind etwa syntaktische Informationen ausgesprochen wertvoll, wenn man Muster erkennen will, die sich mithilfe des “Bag of words”-Ansatzes nicht adäquat abbilden lassen.

Während R lange Zeit keine mit Python vergleichbare Bibliotheken vorweisen konnte, scheint dieses Problem inzwischen (endlich) weitgehend gelöst. Technisch arbeiten wir in diesem Abschnitt mit zwei Paketen: udpipe und spacyr. Letzteres zeichnet sich wie auch andere hier vorgestellte Pakete durch direkte Interoperabilität mit quanteda aus. Spacyr ist allerdings insofern ein etwas komplizierter Sonderfall, weil das Paket seinerseits eine Schnittstelle zu einer externen Ressource darstellt, und zwar zu Python. Weil es lange keine nativen R–Bibliotheken für das Tagging und Parsing von Texten gab, führt der Weg hierfür zwangsläufig über Java oder Python. Glücklicherweise steht seit Anfang 2019 mit udpipe ein solches Paket zur Verfügung, welches direkt auf C++ basiert, und weder Java noch Python erfordert.

if(!require("quanteda")) install.packages("quanteda")
if(!require("tidyverse")) install.packages("tidyverse")
if(!require("scales")) install.packages("scales")
if(!require("udpipe")) {install.packages("udpipe"); library("udpipe")}
if(!require("googlenlp")) {install.packages("googlenlp"); library("googlenlp")}
theme_set(theme_bw())

Erste Gehversuche mit Hilfe von udpipe

Nach der Paketinstallation laden wir das sogenannte Sprachmodell, welches udpipe für die Annotation verwenden soll (hier “german”), und testen es an einem einfachen Beispielsatz. Ein Überblick über sämtliche unterstützen Sprachen (derzeit 56, mit z.T. mehrereen Implementationen von Varietäten der gleichen Sprache) findet sich im Download-Archiv von LIDAT/CLARIN, Performancemetriken sowie weitere Informationen, welche Aufschluss über die Genauigkeit der Tagger/Parser geben auf der Seite des udpipe-Projekts.

if (!file.exists("verschiedenes/german-gsd-ud-2.3-181115.udpipe")) udpipe_download_model(language = "german", model_dir = "verschiedenes") # download model if it doesn't exist
sprachmodell.udpipe <- udpipe_load_model(file = "verschiedenes/german-gsd-ud-2.3-181115.udpipe")
beispiel <- udpipe_annotate(sprachmodell.udpipe, "Dies ist ein einfacher Beispielsatz.")
beispiel <- as.data.frame(beispiel, detailed = T)
beispiel

Wofür lässt sich der Output des PoS-Taggings konkret nutzen? Im folgenden Beispiel laden wir das bereits in Kapitel 5 eingesetzte Korpus aus der Onlineausgabe der Wochenzeitung “Die Zeit”" und filtern den Output anschließend so, dass wir (a) nur Nomen (mit upos == “NOUN”), dem Genus Femininum bzw. Maskulinum (mit “Gender=Fem” und “Gender=Masc”) und der Endung “-in” bzw. “-er” erhalten. Dieses etwas hemdsärmelige Verfahren liefert uns Begriffspaare wie etwa “Politikerin” / “Politiker” zurück und erlaubt so eine grobe Schätzung des Anteils weiblicher Berufsbezeichnungen gegenüber generisch-maskulinen Bezeichnungen (in der Zeit etwa 1:10). Dabei sind allerdings sowohl falsch-positive Ergebnisse enthalten (“Putin”, “Kilometer”), als auch Fälle, in denen man nicht von einer stilistischen Wahlentscheidung sprechen kann (“Bundeskanzlerin”). Jedenfalls gibt das Beispiel bereits einen ersten Vorgeschmack darauf, welche Möglichkeiten sich aufgrund der zusätzlichen Informationen, welche eine PoS-Tagger liefert, für die sozialwissenschaftliche Analyse bieten, wenn man nur etwas kreativ wird.

load("daten/zeit/zeit.sample.korpus.RData")
zeit.sample <- texts(zeit.korpus)
zeit.pos <- udpipe_annotate(sprachmodell.udpipe, zeit.sample, doc_id = docnames(zeit.korpus), tagger = "default", parser = "none")
zeit.pos.df <- as.data.frame(zeit.pos)
nomen.fem <- filter(zeit.pos.df, upos == "NOUN" & str_detect(feats, "Gender=Fem") & str_detect(lemma, "in$"))
nomen.masc <- filter(zeit.pos.df, upos == "NOUN" & str_detect(feats, "Gender=Masc") & str_detect(lemma, "er$"))
as.data.frame(head(sort(table(nomen.fem$lemma, dnn = list("Term")), decreasing = T), 10), responseName = "Frequenz")
as.data.frame(head(sort(table(nomen.masc$lemma, dnn = list("Term")), decreasing = T), 10), responseName = "Frequenz")

Weitergehende Analyse mittels spacyr

Wir wenden uns nun dem Paket spacyr zu, das entgegen udpipe auch noch einen weiteren Vorteil bietet: es erkennt auch sog. benannted Entitäten (“named entities”). Wir installieren zunächst das Paket, bzw. laden dieses sofern bereits vorhanden. Die Installation von spacyr ist vergleichsweise aufwändig, weil zusätzlich eine aktuelle Version von Python benötigt wird, und kann mitunter eine Zeit in Anspruch nehmen. Zusästzliche benötigen wir zudem auch noch ein Sprachmodul für Deutsch, welches Tagging, Parsing und Entitätenerkennung in dieser Sprache ermöglicht. Nach dem Laden erfolgt schließlich die Initialisierung von spacyr, welche jeweils auf die zu benutzende Sprache abgestimmt sein muss.

if (!require("spacyr")) {
  install.packages("spacyr")
  library("spacyr")
  spacy_install()
  spacy_download_langmodel("de")
}
## Loading required package: spacyr

Nun können wir das Paket einsetzen, um etwa die Wortklassen in einem Korpus zu bestimmen. Zunächst laden wir wieder das Korpus mit Beiträgen Schweizer Tageszeitungen zum Thema Finanzkrise. Wir beginnen mit einem einfachen Überblick des Outputs von spacyr. Die Diagnostik-Meldungen aus Python können wir dabei ignorieren.

load("daten/cosmas/finanzkrise/finanzkrise.korpus.RData")
korpus.finanzkrise.sample <- corpus_sample(korpus.finanzkrise, size = 1000)
spacy_initialize(mode = "de")
## spacy python option is already set, spacyr will use:
##  condaenv = "spacy_condaenv"
## successfully initialized (spaCy Version: 2.0.16, language model: de)
## (python options: type = "condaenv", value = "spacy_condaenv")
finanzkrise.pos <- spacy_parse(korpus.finanzkrise.sample, lemma = F, entity = T, dependency = T)
spacy_finalize()
head(finanzkrise.pos, 100)

Die tabellarische Auflistung ist bereits hinlänglich bekannt, neu sind aber die Felder pos (Wortart), head_token_id (Hauptwort), dep_rel (syntaktische Abhängigkeitsbeziehung) und entity (Art der Entität), welche durch die linguistische Analyse hinzugekommen sind.

Ein praktisches Beispiel zeigt konkrete Vorteile der Annotation von Wortart und Satzkonstituente: Wir ermitteln die frequentesten Satzsubjekte im Finanzkrise-Korpus, also solche Nomen, die das Hauptwort eines Satzes darstellen. Dies gibt Aufschluss über die Akteure die im Korpus eine Rolle spielen, auch wenn es sich beim Subjekt um eine rein syntaktische Kategorie handelt.

subjekte <- finanzkrise.pos %>% 
  filter(pos == "NOUN", dep_rel == "sb") %>% 
  mutate(Wort = str_to_lower(token)) %>% 
  group_by(Wort) %>% 
  summarise(Frequenz = n()) %>% 
  arrange(desc(Frequenz)) %>% 
  mutate(Rang = row_number()) %>% 
  filter(Rang <= 25)
ggplot(subjekte, aes(reorder(Wort, Rang), Frequenz)) + geom_bar(stat = "identity") + theme(axis.text.x = element_text(angle = 45, hjust = 1)) + xlab("") + ggtitle("Frequenteste Satzsubjekte im Finanzkrise-Korpus")

Wie das Ergebnis zeigt, lassen sich im Gegensatz zu einer einfachen Frequenzliste aller Wörter im Korpus viel leichter Rückschlüsse über wichtige Akteure anstellen.

Wir wenden uns nun einem anderen Beispiel zu, nämlich den Trump-Clinton-Tweets. Hier interessieren uns besonders Adjektive, und ganz spezifisch, welche für wen der beiden Kandidaten besonders charakteristisch sind. Zunächst müssel wir spacy erneut initialisieren, und zwar um die Sprache von Deutsch auf Englisch umzustellen. Anschließend wenden wir wieder die Funktion spacy_parse an. Wir beschränken uns hier darauf, die Wortart zu bestimmen.

load("daten/twitter/trumpclinton.korpus.RData")
spacy_initialize(mode = "en")
## Python space is already attached.  If you want to switch to a different Python, please restart R.
## successfully initialized (spaCy Version: 2.0.16, language model: en)
## (python options: type = "condaenv", value = "spacy_condaenv")
twitter.pos <- spacy_parse(korpus, lemma = F, entity = F)
spacy_finalize()
head(twitter.pos, 100)

Nun vergleichen wir anhand einer zuvor aus der Gesamtliste aller verwendeten Adjektive ausgewählten Gruppe von Adjektiven, wie charakteristische diese jeweils für Donald Trump und Hillary Clinton sind.

adjektive.trumpclinton <- scan("daten/twitter/adjektive.trumpclinton.txt", what = "char", sep = "\n", quiet = T)
adjektive <- twitter.pos %>% 
  mutate(Wort = str_to_lower(token)) %>% 
  filter(pos == "ADJ", Wort %in% adjektive.trumpclinton) %>% 
  left_join(korpus.stats, by = c("doc_id" = "Text")) %>% 
  group_by(Wort, Kandidat) %>% 
  summarise(Frequenz = n()) %>% 
  complete(Wort, Kandidat) %>%
  replace(., is.na(.), 0) %>% 
  arrange(Wort, Kandidat)
## Warning: Column `doc_id`/`Text` joining character vector and factor,
## coercing into character vector
adjektive.diff <- data.frame(Wort = unique(adjektive$Wort), Differenz = adjektive$Frequenz[adjektive$Kandidat == "Clinton"] - adjektive$Frequenz[adjektive$Kandidat == "Trump"]) %>% 
  mutate(Differenz.S = as.vector(scale(Differenz, center = 0))) %>% 
  arrange(Differenz.S)

ggplot(adjektive.diff, aes(reorder(Wort, Differenz.S), Differenz.S)) + geom_bar(stat = "identity") + theme(axis.text.x = element_text(angle = 90,  hjust = 1, vjust = 0.5)) + xlab("") + ggtitle("Adjektive Trump vs. Clinton (mit 'great', 'big' enfernt)") + ylab("Trump                                                               Clinton") + coord_flip()