-
Notifications
You must be signed in to change notification settings - Fork 7
/
Copy pathner.Rmd
199 lines (154 loc) · 18.1 KB
/
ner.Rmd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
---
title: "Automatisierte Inhaltsanalyse mit R"
author: "Cornelius Puschmann"
subtitle: Tagging, Parsing und Entitätenerkennung
output: html_notebook
---
<!---
Todos
* deepl-übersetzung hinzufügen
* ...
* ...
-->
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](https://cran.r-project.org/package=udpipe) und [spacyr](https://github.com/quanteda/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.
```{r Installation und Laden der benötigten R-Bibliotheken, message = FALSE}
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_minimal())
```
#### 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](https://lindat.mff.cuni.cz/repository/xmlui/handle/11234/1-2898), Performancemetriken sowie weitere Informationen, welche Aufschluss über die Genauigkeit der Tagger/Parser geben auf der [Seite des udpipe-Projekts](http://ufal.mff.cuni.cz/udpipe/models#universal_dependencies_23_models).
```{r Laden des udpipe Sprachmodells für deutsch und Annotation eines Beispielsatzes}
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.
```{r Laden und Annotation des Zeit-Korpus mit udpipe}
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.
```{r Installation des Spezialpakets spacyr}
if (!require("spacyr")) {
install.packages("spacyr")
library("spacyr")
spacy_install()
spacy_download_langmodel("de")
}
```
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.
```{r Laden und Parsen des Finanzkrise-Korpus}
load("daten/cosmas/finanzkrise/finanzkrise.korpus.RData")
korpus.finanzkrise.sample <- corpus_sample(korpus.finanzkrise, size = 1000)
spacy_initialize(mode = "de")
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.
```{r Frequenteste Satzsubjekte im Finanzkrise-Korpus ermitteln}
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](https://www.rdocumentation.org/packages/spacyr/versions/1.0/topics/spacy_parse) an. Wir beschränken uns hier darauf, die Wortart zu bestimmen.
```{r Laden und Parsen des Twitter-Korpus}
load("daten/twitter/trumpclinton.RData")
spacy_initialize(mode = "en")
twitter.pos <- corpus(daten.twitter.trumpclinton, docid_field = "id", text_field = "text", unique_docnames = F) %>%
spacy_parse(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.
```{r Typische Adjektive bei Donald Trump und Hillary Clinton}
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)
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()
```
Gerade die Adjektive machen die Unterschiede zwischen den Kandidaten bezüglich des politischen Stils sehr deutlich, und nicht nur deshalb, weil Trump deutlich häufiger negative Adjektive verwendet als Clinton.
Zum Schluss wechseln wir noch einmal den Ansatz und schauen uns die Erwähnung von Entitäten im UN-Korpus -- in diesem Fall Organisationen -- etwas genauer an. Die Entitätenerkennung von spacyr ist alles andere als fehlerfrei -- man sollte die Ergebnisse auf jeden Fall genau inspizieren. Dennoch lassen sich durchaus interessante Ergebnisse erzielen. Die Frequenzliste enthält vor allem Dopplungen ("The United Nations" vs. "United Nations") die sich aber relativ leicht entfernen lassen. So lässt sich etwa ein Lexikon generieren, welches Organizationen bspw. nach Organisationstyp oder nach geographischem Fokus einordnen könnte.
```{r Entitäten im UN-Korpus}
load("daten/un/un.korpus.RData")
korpus.un.sample <- corpus_sample(korpus.un, size = 100)
spacy_initialize(mode = "en")
un.parsed <- spacy_parse(korpus.un.sample, pos = F, lemma = F)
un.entities <- entity_extract(un.parsed)
spacy_finalize()
organizations <- un.entities$entity[un.entities$entity_type=="ORG"] %>%
str_remove("\n|\t") %>%
str_replace_all("_+", " ") %>%
str_trim(.) %>%
str_to_title(.) %>%
table(.) %>%
sort(., decreasing = T)
head(organizations[names(organizations)!=""], 25)
```
#### Annotation mit Hilfe des Paketes googlenlp und der Cloud Natural Language API
Die Methoden der (teil)automatisierten Textanalyse, die in dieser Einführung im Mittelpunkt stehen, sind bisher anhand von Funktionen und Paketen vorgestellt worden, die man selbst auf dem eigenen Rechner installieren, ausführen und mit der notwendigen Expertise auch einsehen und verändern kann. Ob es sich nun um solche Verfahren handelt, die zum Funktionsumfang von quanteda gehören, um jene, die über ein zusätzliche Pakete wie etwa RTextTools, topicmodels, stm oder spacyr realisiert werden, oder um einfache ad-hoc-Verfahren, die man anhand vorhandenerer Funktionen selbst zusammenstellen kann, die Lösung wurde bisher immer auf Grundlage von Paketen erzielt, über die man als R-Nutzer selbst vollständige Kontrolle hat.
In diesem Abschnitt geht es um Programmierschittestellen (application programming interfaces oder APIs), also um Schnittstellen, die den Zugang zu Daten, aber auch die Anwendung bestimmter computergestützter Analyseverfahren über das Internet ermöglichen. Zunächst ist am Zugang zu Daten über APIs nichts auszusätzen -- statt mühevoll Parser zu schreiben, die aus Webseiten oder uneinheitlich formatierten PDF-Dokumenten strukturierte Daten herausholen verwendet man oft lieber eine gut dokumentierte API, die ordentliche Daten in genau der Form liefert, wie man sie haben möchte. APIs gibt es in ganz unterschiedlichen Formen und für unterschiedliche Zielgruppen. Da aber häufig gerade bei kommerziellen Social Media-Plattformen die Entwicklung von Apps durch Drittentwickler der eigentliche Grund für das Vorhandensein einer API ist, sind nicht unbedingt alle APIs ideal für die Untersuchung sozialwissenschaftlicher Fragegestellungen geeignet.
Folgend verwenden wir das Paket [googlenlp](https://github.com/BrianWeinstein/googlenlp), welches die Dienste von [Googles Cloud Natural Language API](https://cloud.google.com/natural-language/) in R zugänglich macht. Um diese Dienste nutzen zu können ist eine Anmeldung auf der [Google Cloud Platform](https://cloud.google.com/) notwendig, sowie die Registrierung eines Projekts und die Freischaltung der eigentlichen API, die nur eine von sehr (!) vielen Diensten darstellt, die Google auf diesem Weg anbietet. Zudem müssen Zahlungsmethoden hinterlegt werden für den Fall, dass ein Projekt eine größere Anzahl von Anfragen generiert. Alle großen Internet-Unternehmen bieten solche Dienste an, die in der Regel im geringen Umfang kostenlos nutzbar sind. Für die Google Cloud Platform kann man sich [hier](https://cloud.google.com/) kostenlos registrieren, näheres zur Cloud Natural Lanuage API findet sich [hier](https://cloud.google.com/natural-language/). Wichtigster Schritt ist nach der Registrierung eines Projekts und der API-Freischaltung das Kopieren eines API-Schlüssels, welchen man braucht, um Anfragen an Google schicken zu können.
Nachdem wir den Schlüssel aus einer Textdatei geladen haben, schicken wir einen Beispielsatz an die API, welcher mittels [annotate_text()]() annotiert wird (die Funktion fasst hierbei die Funktionen Tagging, Parsing, Entitätenerkennung, Spracherkennung sowie Sentimentanalyse zusammen).
Zunächst schauen wir uns das Ergebnis des PoS-Taggings und Syntax-Parsings an.
```{r Tagging und Parsing mittels googlenlp}
gl.apikey <- scan("verschiedenes/googlenlp_api.key", what = "char", quiet = T)
set_api_key(gl.apikey)
gl.annotation <- annotate_text("Dies ist ein einfacher Satz, analysiert mit den Diensten von Google.")
gl.annotation$tokens
```
In einem zweiten Schritt nehmen wir den Entitäten-Auswertung in den Blick, welche die Anfrage gleich mitgeliefert hat.
```{r Entitätenerkennung mittels googlenlp}
gl.annotation$entities
```
Um nicht bei diesem etwas dürftigen Beispiel zu bleiben, wenden wir die Entitätenerkennung auf die Zeit-Daten an, die auch vielfältig genug sind, um die Qualität der Annotation zu demonstrieren.
```{r Entitätenerkennung der Zeit-Daten mittels googlenlp}
load("daten/zeit/zeit.sample.korpus.RData")
zeit.sample <- sample(texts(zeit.korpus), size = 5)
entitaeten <- tibble(doc_id = character(0), name = character(0), entity_type = character(0), mid = character(0), wikipedia_url = character(0), salience = double(0), content = character(0), beginOffset = integer(0), mentions_type = character(0))
for (i in 1:5) {
print(paste0("Analyzing document ", names(zeit.sample[i]), "..."))
entitaeten <- bind_rows(entitaeten, data.frame(doc_id = names(zeit.sample[i]), analyze_entities(zeit.sample[i])$entities, stringsAsFactors = F))
}
entitaeten
```
Wie man sieht, liefert die Google Cloud Natural Language API auch für deutsch gute Ergebnisse. Wir gehen hier nicht weiter auf die Facetten Sentimentanalyse, Sprachbestimmung, sowie automatische Übersetzung der Google-Dienste ein, aber auch diese sind durchaus nützlich für inhaltsanalytische Projekte.
Zusammenfassend sind die Funktionen Tagging, Parsing und Entitätenerkennung nützliche Hilfswerkzeuge für die sozialwissenschaftliche Untersuchung von Textinhalten. Der vielleicht zentralste Vorteil besteht in der Möglichkeit dieser Ansätze, gezielt Wortklassen zu identifizieren die für die Analyse relevant sind (i.d.R. die Inhaltswortklassen Nomen, Verben und Adjektive). Ein weiterer Nutzen besteht darin, sehr basale Akteursbeziehungen zu identifizieren (Parsing) und eine verbesserte Erkennung von Akteuren, Orten und abstrakten Konzepten zu erreichen (Entitätenerkennung). Das die letztgenannten Verfahren gerade für deutschsprachige Inhalte nicht unbedingt immer besonders gute Ergebnisse liefern, hat schlicht damit zu tun, dass es für deutschsprachige Inhalte immer noch vergleichsweise weniger exakte Modelle existieren als etwas für Englisch.