Bei der Themenanalyse (engl. topic modeling) handelt es sich um ein im Vergleich mit dem in den vorausgehenden Kapiteln präsentierten Lexikon-Ansatz sehr viel jüngeres und deutlich anspruchsvolleres Verfahren, wenn es um die Anforderungen an Rechenleistung und Speicher Ihres Computers geht. Themenmodelle sind mathemathisch komplex und vollständig induktiv, d.h. das Modell setzt keinerlei Kenntnis des Inhalts voraus, was aber nicht bedeutet, dass solche Kenntnisse für die Validierung des Outputs nicht entscheidend sind. Die Beziehung von Themen zu Wörtern und Dokumenten wird in einem Themenmodell vollständig automatisiert hergestellt. Die bekannteste Implementation heißt Latent Dirichlet Allocation (kurz LDA) und wurde von den Computerlinguisten David Blei, Andrew Ng und Michael Jordan entwickelt. Während ich vorausgehende, einfachere Verfahren lediglich kurz beschrieben habe, ist es bei der Themenanalyse notwendig, sich eingehender mit den algoritihmischen Grundlagen des Ansatzes zu beschäftigen, wenn man diesen wirklich verstehen will. Für die Demonstration von Themenmodellen – und weitgehend auch für deren kompetente Anwendung – reicht es allerdings aus, wenn man diese ausprobiert und (und das ist sehr wichtig!) die Qualität der erzielten Ergebnisse systematisch überprüft, das Modell also umfassend validiert. Dies geschieht anhand einer Reihe von Verfahren, welche die Passung des Models etwa in Abhängigkeit zu Variablen wie der gewählten Themenzahl bewerten. Während auch die vorausgehenden Ansätze häufig Ergebnisse produzieren, die sorgfältig überprüft werden müssen, sind Topic Model-Ergebnisse besonders schwer vorhersagbar, weil sich die induktiven Wortverteilungsmuster, auf denen Themenmodelle basieren mitunter stark vom menschlichen Verständnis eines Themas unterscheiden.

Da Themenmodelle im Kontrast zu den bisher vorgestellten Methoden nicht Teil der Ausstattung von quanteda sind, nutzen wir folgend zwei neue Pakete für ihre Berechnung: topicmodels und stm. Das Paket topicmodels implementiert die beiden Verfahren Latent Dirichlet Allocation (LDA) und Correlated Topic Models (CTM), während STM auf einem ganz neuen Ansatz basiert, der zahlreiche Erweiterungen gegenüber LDA enthält. Hinzu kommt schließlich noch das Paket urltools, welches bei der Auswertung von Online-Nachrichtenbeiträgen nützlich sein wird, aber nicht direkt etwas mit Themenmodellen zu tun hat.

if(!require("quanteda")) {install.packages("quanteda"); library("quanteda")}
if(!require("tidyverse")) {install.packages("tidyverse"); library("tidyverse")}
if(!require("topicmodels")) {install.packages("topicmodels"); library("topicmodels")}
if(!require("ldatuning")) {install.packages("ldatuning"); library("ldatuning")}
if(!require("stm")) {install.packages("stm"); library("stm")}
if(!require("wordcloud")) {install.packages("wordcloud"); library("wordcloud")}
if(!require("urltools")) {install.packages("urltools"); library("urltools")}
theme_set(theme_bw())

Neben den genannten Paketen verwenden wir ausserdem noch die Bibliotheken ldatuning und wordcloud um Modellen zu optimieren, bzw. zu plotten.

Erste Erstellung eines groben LDA-Themenmodells

Wir beginnen mit einem sehr einfachen LDA-Themenmodell, welches wir anhand des Pakets topicmodels berechnen. Dieses Paket bietet zwar kaum Funktionen, anhand derer man das Modell näher inspizieren kann, aber ein Blick auf die Objektstruktur hilft diesbezüglich bereits, sofern man zumindest grob mit Themenmodellen vertraut ist. Am Ende diesen Abschnitts gehen wir nochmals darauf ein, wie man aus einem LDA-Modell die wichtigsten Metriken extrahiert.

Zunächst laden wir wieder das Sherlock-Holmes Korpus, allerdings dieses Mal in einer besonderen Variante. Die nachstehend verwendete Version unterteil das Korpus in 174 Dokumente, die jeweils aus 40 Sätzen bestehen. Diesen Schritt haben wir bereits zuvor mithilfe der Funktion corpus_reshape durchgeführt. Das Ergebnis sind 10-17 “Texte” pro Roman, also in etwa so, als seien die Romane in Kapitel unterteilt, was sie in der von uns verwendeten Fassung nicht sind.

Wieso dieser Aufwand? Für die LDA-Analyse ist die Anzahl von nur 12 (relativ langen) Texten insgesamt ungünstiger als diese Aufteilung, auch wenn die arbiträre Unterteilung nach der Anzahl der Sätze weniger gut funktioniert, als dies sinngebenden Kapite tun würden. Hier ein Überblick über das refakturierte Korpus.

load("daten/sherlock/sherlock.absaetze.RData")
as.data.frame(korpus.stats)

Auch der nächste Schritt ist inzwischen schon hinreichend bekannt: Wieder einmal berechnen wir eine DFM und entfernen Zahlem, Symbole und englische Standard-Stoppwörter. In einem zweiten Schritt entfernen wir solche Begriffe die nur ein einziges Mal vorkommen, sowie solche, die häufiger als 75x vorkommen. Der Befehl dfm_trim erlaubt durchaus komplexere Parameter wie die Begriffshäufigkeit relativ zur Termfrequenz oder Dokumentfrequenz insgesamt, aber an dieser Stelle reicht uns diese einfache Filterung.

meine.dfm <- dfm(korpus, remove_numbers = TRUE, remove_punct = TRUE, remove_symbols = TRUE, remove = c(stopwords("english"), "sherlock", "holmes"))
meine.dfm.trim <- dfm_trim(meine.dfm, min_termfreq = 2, max_termfreq = 75)
meine.dfm.trim
## Document-feature matrix of: 174 documents, 4,226 features (96.3% sparse).

Nun folgt die eigentliche Modellierung der Themen. Wir legen zunächst arbiträr eine Themenanzahl von k = 10 fest. Die Anzahl der Themen ist grundsätzliche variabel und wird anhand unterschiedlicher Faktoren bestimmt (dazu später noch etwas mehr). Dann konvertieren wir mit dem bereits bekannten Befehl convert die quanteda-DFM in ein Format, welches das Paket topicmodels versteht. Während die bisher verwendeten Befehle aus quanteda kamen, ist der Befehl LDA dem Paket topicmodels entnommen.

anzahl.themen <- 10
dfm2topicmodels <- convert(meine.dfm.trim, to = "topicmodels")
lda.modell <- LDA(dfm2topicmodels, anzahl.themen)
lda.modell
## A LDA_VEM topic model with 10 topics.

Nachdem das eigentliche Modell berecht wurde, können wir uns nun zwei zentrale Bestandteile des Modells ausgeben lassen: Die Begriffe, die besonders stark mit jedem der Themen verknüpft sind (mit dem Befehl terms) …

as.data.frame(terms(lda.modell, 10))

…und die Dokumente, in denen die Themen besonders stark vertreten sind (mit dem Befehl topics).

data.frame(Thema = topics(lda.modell))

Was sehen wir hier genau? Die erste Tabelle zeigt für jedes der zehn Themen die zehn am stärksten mit dem jeweiligen Thema verknüpften Begriffe. Die zweite Tabelle zeigt wiederum für jeden Text das Thema mit dem höchsten Anteil. Wie schon erläutert, sind “Texte” in diesem Fall eigentlich Absätze aus einzelnen Romanen, also bezeichnet “01_02” den zweiten Absatz von A Scandal in Bohemia.

Für Begiffe und Texte gilt gleichermaßen, dass alle Themen in einer gewissen Stärke mit allen Begriffen/Texten verknüpft sind, nur interessieren uns üblicherweise lediglich Assoziationen einer bestimmten Stärke.

Welche quantitative Verteilung ergibt sich hieraus? Dies lässt sich leicht ermitteln, wenn man die Themen-Vorkommnisse für einen Roman durch die Gesamtanzahl der Abschnitte teilt.

lda.themen.absaetze <- data.frame(korpus.stats, Thema = topics(lda.modell)) %>%
  add_count(Roman, Thema) %>%
  group_by(Roman) %>% 
  mutate(Anteil = n/sum(n)) %>% 
  ungroup() %>% 
  mutate(Thema = paste0("Thema ", sprintf("%02d", Thema))) %>% 
  mutate(Roman = as_factor(Roman))
ggplot(lda.themen.absaetze, aes(Roman, Anteil, color = Thema, fill = Thema)) + geom_bar(stat="identity") + ggtitle("LDA-Themen in den Sherlock Holmes-Romanen") + xlab("") + ylab("Themen-Anteil (%)") + theme(axis.text.x = element_text(angle = 45, hjust = 1))

Es fällt sofort ins Auge, das mehrere Roman gewissermaßen ihr eigenes Thema besitzen, was angesichts der Merkmale des Roman-Genres durchaus nachvollziehbar ist. Es existieren aber auch solche Themen, die in mehreren Romanen vorkommen und eher allgemeiner Natur sind, oder die Charaktere beeinhalten, die in einer Vielzahl von Sherlock Holmes-Erzählungen auftreten.

Die Verteilung von Themen in Dokumenten wird in diesem Beispiel etwas anders als sonst berechnet, weil wir diesen Schritt mithilfe von Funktionen aus dplyr gewissermaßen händisch durchführen. Die “normale” Berechnung der Beziehung zwischen Begriffen und Themen bzw. Dokumenten und Themen erfolgt über die Extraktion der Variablen beta und gamma, die im LDA-Modell bereits enthalten sind (die Struktur des Modells kann mit dem Standard-R-Befehl str genauer untersucht werden). Das Ergebnis ist jeweils ein Data Frame, den man natürlich auch plotten kann. Die Variablen V1-VX bezeichnen hier die Themen, während die Zeilen die Begriffe bzw. die Dokumente beinhalten. Die Zahlewerte beschreiben die Wahrscheinlichkeit der Assoziation eines Begriffs mit einem Thema, bzw. der Anteil eines Themas an einem Dokument.

head(as.data.frame(t(lda.modell@beta), row.names = lda.modell@terms)) # Begriffe > Themen
head(as.data.frame(lda.modell@gamma, row.names = lda.modell@documents)) # Dokumente > Themen

Was haben wir durch das Modell erfahren? Zunächst einmal existieren Themen, welche im Wesentlichen die Handlung des jeweiligen Romans wiedergeben. Dies ist angesichts des vergleichsweise kleinen Samples nicht weiter verwunderlich – ein deutlich größeres Korpus würde uns hier besserer Ergebnisse liefern. Andererseits sind aber auch Themen erkennbar, die nicht an einen einzelnen Roman gebunden sind, sondern in mehreren Romanen vorkommen. Trotzdem ist das Genre – Romane des selben Autors zum (grob) gleichen Thema – nicht wirklich ideal für eine stichhaltige Analyse mit LDA geeignet. Das nächste Beispiel eines LDA-Modells für Nachrichtentexte macht dies anschaulich.

LDA-Themenpassung bestimmen mit LDAtuning

Bevor wir uns diesem Beispiel zuwenden, nehmen wir aber noch eine Heuristik für die Ermittlung des idealen k (also der Themenanzahl) in den Blick. Statt einfach eine ärbiträtre Zahl festzulegen ist es sinnvoll, die Passung unterschiedlicher Einstellungen zu testen. Dies erleichtert das Paket LDAtuning, in das eine Reihe unterschiedlicher Metriken für die Bestimmung einer guten Themenanzahl auf Grundlage statistischer Faktoren integriert sind. Achtung: diese Berechnung ist deshalb sehr aufwändig, weil für alle Schritte ein eigenes Modell gerechnet wird (also in diesem Beispiel 15 einzelne Modelle), was vor allem bei größeren Datensätzen leicht mehrere Tage dauern kann.

ldatuning.metriken <- FindTopicsNumber(dfm2topicmodels, topics = seq(from = 2, to = 15, by = 1), metrics = c("Griffiths2004", "CaoJuan2009", "Arun2010", "Deveaud2014"), method = "Gibbs", control = list(seed = 77), mc.cores = 2L, verbose = TRUE
)
## fit models... done.
## calculate metrics:
##   Griffiths2004... done.
##   CaoJuan2009... done.
##   Arun2010... done.
##   Deveaud2014... done.

Auch für unsere Sherlock Holmes-LDA lassen sich diese Metriken grafisch darstellen.

FindTopicsNumber_plot(ldatuning.metriken)

Die Ergebnisse sind etwas uneinheitlich, was primär damit zusammenhängt, dass die Daten keine ganz ideale Grundlage für ein Themenmodell mittels LDA darstellen. Zwei Metriken (Arun et al, 2010 und Griffiths & Steyvers, 2004) werden konsequent besser und haben ihren Idealpunkt vermutlich bei k > 15, während die beiden anderen (Cao et al, 2009 und Deveaud, San Juan & Bellot, 2014) fluktuieren bzw. abfallen.

Ein LDA-Themenmodell für das Zeit-Nachrichtenkorpus

Wie sieht eine ähnliche Analyse für ein etwas repräsentativeres und heterogeneres Korpus aus? Um diese Frage zu beantworten, laden wir ein bisher noch unverwendetes Korpus, bestehend aus 377 Beiträgen aus der Wochenzeitung Die Zeit, die zwischen 2011 und 2016 veröffentlicht wurden. Diese wurden anhand des Pakets rzeit2 erhoben, welches in Kapitel 8 noch etwas genauer besprochen wird. Zunächste werfen wir wie immer einen Blick auf die Metadaten.

load("daten/zeit/zeit.sample.korpus.RData")
as.data.frame(zeit.korpus.stats)

Relevant für die spätere Verwendung sind unter anderem die URLs der Beiträge, aber auch das Veröffentlichungsdatum. Die folgenden Schritte unterscheiden sich zunächst nicht von denen für die Analyse des Sherlock Holmes-Korpus.

meine.dfm <- dfm(zeit.korpus, remove_numbers = TRUE, remove_punct = TRUE, remove_symbols = TRUE, remove = stopwords("german"))
meine.dfm.trim <-  dfm_trim(meine.dfm, min_docfreq = 3, max_docfreq = 65)
meine.dfm.trim
## Document-feature matrix of: 377 documents, 5,260 features (97.9% sparse).

Unsere DFM ist um Vergleich zum ersten Beispiel aber deutlich ergiebiger, weil die Relation von Dokumenten zu Begriffen eine andere ist (d.h. es gibt eine größere Zahl an inhaltlich relevanten Begriffen, welche ungleicher über die Dokumente verteilt sind). Haben wir zuvor eine minimale und maximale Termfrequenz bei der Reduzierung der DFM festgelegt, verwenden wir nun die Dokumentfrequenz als Kriterium.

Wir modellieren nun 15 statt 10 Themen, was allerdings immer noch eine relativ geringen Anzahl darstellt. Wie der Zuschnitt der Themen erkennen lässt, ist eine größeres k i.d.R. durchaus sinnvoll.

anzahl.themen <- 15
dfm2topicmodels <- convert(meine.dfm.trim, to = "topicmodels")
lda.modell <- LDA(dfm2topicmodels, anzahl.themen)
lda.modell
## A LDA_VEM topic model with 15 topics.

Auch hier lassen sich wieder die Schlüsselbegriffe und -themen extrahieren.

as.data.frame(terms(lda.modell, 10))
data.frame(Thema = topics(lda.modell))

Das Bild ist trotz des recht kleinen Samples und einiger Interferenzen deutlich klarer, als beim Holmes-Beispiel, was daran liegt, dass das Korpus thematisch deutlich heterogener ist. Unterschiede zwischen Ressorts wie Aussenpolitik und Sport, aber auch zwischen konkreten Themenfeldern wie der Euro- und Flüchtlingskrise, der Energiewende, oder der Vorratsdatenspeicherung, sind klar erkennbar.

Wie ähnlich sind sich die Themen untereinander? Diese Frage ist einerseits deshalb interessant, weil sich so das Modell besser validieren lässt, und andererseits, weil sich aus der Entdeckung von homogenen Clustern möglicherweise Themenbündel ableiten lassen, die für die weitere Analyse relevant sind. Das folgende Plot zeigt die Ähnlichkeit der Themen auf der Grundlage von Wortverteilungen. Realisiert wird diese Ähnlichkeitsberechnung mit den Befehlen dist und hclust welche zum Standardumfang von R gehören. Anschließend wird das Ergebnis mit dem R-nativen plot-Befehl dargestellt – ggplot liefert hier kein wesentlich schöneres Ergebnis.

Das Resultat ist insofern vielversprechend, als dass die außen- und innenpolitischen Themen jeweils einen Cluster ergeben. Auch sonst lassen sich Gemeinsamkeiten identifizieren, die durchaus plausibel erscheinen, auch wenn gerade die Beziehung von Gesellschafts- und Kulturthemen zum Teil nur schwer nachvollziehbar ist.

lda.themen.aehnlichkeit <- as.data.frame(lda.modell@beta) %>% 
  scale() %>% 
  dist(method = "euclidean") %>% 
  hclust(method = "ward.D2")
par(mar = c(0, 4, 4, 2))
plot(lda.themen.aehnlichkeit, main = "LDA-Themenähnlichkeit nach Features", xlab = "", sub = "")

Wie verhalten sich die ermittelten Themen zu einer externen Variable, wie etwa dem Ressort? Die Beiträge aus der Zeit können anhand ihrer URL in Ressorts differenziert werden, etwa http://www.zeit.de/politik/deutschland/2011-01/gorch-fock-ushuaia in politik (nach dem sehr einfachen Muster zeit.de/Ressort). Wir erwarten, dass eine klare Beziehung zwischen den Themen und den Ressorts existiert, denn auch wenn es natürlich Überlappungen gibt, sollte sich bspw. der Bereich Sport vom Bereich Politik unterscheiden lassen.

lda.themen.artikel <- merge(docvars(meine.dfm.trim), as.data.frame(lda.modell@gamma, row.names = lda.modell@documents), by = "row.names", sort = F)
lda.themen.artikel <- mutate(lda.themen.artikel, Ressort = str_split(url_parse(lda.themen.artikel$href)$path, pattern = "/", simplify = T)[,1]) %>%
  gather(Thema, Prozent, V1:V15) %>%
  mutate(Thema = paste0("Thema ", sprintf("%02d", as.numeric(str_sub(Thema, start = 2))))) %>% 
  group_by(Ressort) %>% 
  mutate(Row.names = paste0(Ressort, row_number())) %>% 
  mutate(Prozent = Prozent/sum(Prozent)) %>% 
  ungroup() %>% 
  rename(Dokument = Row.names) %>% 
  filter(!Ressort %in% c("2011", "administratives", "aktuelles", "auto", "campus", "community", "lebensart", "studium", "zeit-magazin"))
ggplot(lda.themen.artikel, aes(Ressort, Prozent, color = Thema, fill = Thema)) + geom_bar(stat = "identity") + ggtitle("LDA-Themen im Zeit-Nachrichtekorpus nach Ressort") + xlab("") + ylab("Themen-Anteil (%)") + theme(axis.text.x = element_text(angle = 45, hjust = 1))

Da das Plot in diesem Fall vermutlich etwas schlechter interpretierbar ist, als es die “nakten” Zahlen sind, anbei auch noch die Themen-Ressort-Verteilung als einfache Tabelle. Wie beim ersten Beispiel zum Sherlock Holmes-Korpus re-aggregieren wir die Themenanteile hier, d.h. die Gesamtanteile aller Themen an einem Ressort summieren sich auf 100%.

lda.themen.verteilung <- lda.themen.artikel %>% 
  group_by(Thema, Ressort) %>% 
  summarise(Prozent = sum(Prozent)*100) %>% 
  arrange(Ressort, desc(Prozent))
lda.themen.verteilung

Auch hier gilt, dass die unterstellte Beziehung zwischen Thema und Ressort klar erkennbar ist. Logisch ist auch, dass spezialisierte Ressorts wie Karriere thematisch homogener sind, als breite Bereiche wie Politik, Wirtschaft oder Gesellschaft, die eine Vielzahl von Themen und Beitragstypen beeinhalten. Unterschiede ergeben sich aber auch aus der extrem ungleichen Größenverteilung der Ressorts (Politik hat mit über 30% einen sehr großen Anteil am Gesamtkorpus).

STM-Themenmodelle auf das UN-Korpus anwenden

In einem dritten Schritt wenden wir uns nun einem Themenmodell-Ansatz zu, der speziell für sozialwissenschaftliche Anwendungen entwickelt wurde, und der zahlreiche Zusatzfunktionen gegenüber LDA bietet: Structured Topic Models oder STM. Wieder verwenden wir zunächst quanteda und “überreichen” dann eine DFM an das Paket stm, welches das eigentliche Modell rechnet. Erneut verwenden wir das UN General Debate Corpus aus dem vorherigen Kapitel.

Eine sehr gute Einführung in STM liefert dieser Artikel von Molly Roberts und Kollegen. Der hier verwendete Code folgt stark den Beispielen aus dem Artikel, auch wenn wir anderen Daten verwenden.

Zunächst laden wir wieder die vorbereiteten UN-Korpus-Daten.

load("daten/un/un.korpus.RData")
head(korpus.un.stats, 100)

Dann berechnen wir – Sie ahnen es bereits – eine DFM unter Ausschluss von Zahlen, Interpunktion, Symbolen, und Stoppwörtern, und reduzieren diese wieder, in diesem Fall besonders großzügig, indem wir Wörter die in weniger als 7.5% und mehr als 90% aller Dokumente vorkommen, entfernen. Dies hat mit der Größe des Korpus zu tun, die es uns erlaubt, den Inhalt stark zu destillieren, ohne wirklich relevante Informationen zu verlieren. Gerade in diesem Fall wird die Berechnung des Modells extrem langsam, wenn wir die Daten nicht effektiv reduzieren, und dabei bleibt zudem auch noch “Lärm” zurück, welcher die Analyse wesentlich erschwert.

meine.dfm.un <- dfm(korpus.un, remove_numbers = TRUE, remove_punct = TRUE, remove_symbols = TRUE, remove = stopwords("english"))
meine.dfm.un.trim <- dfm_trim(meine.dfm.un, min_docfreq = 0.075, max_docfreq = 0.90, docfreq_type = "prop") # min 7.5% / max 90%
meine.dfm.un.trim
## Document-feature matrix of: 7,897 documents, 2,479 features (77.3% sparse).

Nun können wir das STM-Modell rechnen, was zunächst ähnlich abläuft wie bei dem LDA-Themenmodell. Wir legen eine Themenanzahl von k = 40 Themen fest und konvertieren dann mittels convert die DFM aus quanteda in eine Form, die das Paket stm versteht.

Da die Berechnung eines STM-Modells mit einer größeren Anzahl von Themen für ein umfangreiches Korpus wie das UN General Debate Corpus deutlich länger dauert, als dies in den vorangehenden Beispiele der Fall ist, laden wir im folgenden Codeblock ein bereits gerechnetes Model mit load(“daten/un/un.stm.RData”). Wer sich die STM-Berechnung in Aktion anschauen möchte, muss nur die Zeile mit dem Funktionsaufruf von stm auskommentieren, also das “#” entfernen.

Schließlich erstellen wir eine Tabelle, welche uns die wichtigsten Schlüsselwörter für jedes Thema (X1-X40) anzeigt.

anzahl.themen <- 40
dfm2stm <- convert(meine.dfm.un.trim, to = "stm")
#modell.stm <- stm(dfm2stm$documents, dfm2stm$vocab, K = anzahl.themen, data = dfm2stm$meta, init.type = "Spectral")
load("daten/un/un.stm.RData")
as.data.frame(t(labelTopics(modell.stm, n = 10)$prob))

Wer eine visuelle Darstellung bevorzugt (und zudem das Paket wordcloud installiert hat), kann auch die STM-Themen anschaulich als Wortwolke plotten.

par(mar=c(0.5, 0.5, 0.5, 0.5))
cloud(modell.stm, topic = 1, scale = c(2.25,.5))

cloud(modell.stm, topic = 3, scale = c(2.25,.5))

cloud(modell.stm, topic = 7, scale = c(2.25,.5))

cloud(modell.stm, topic = 9, scale = c(2.25,.5))

Praktisch an STM-Modellen ist unter anderem, dass das Paket mit dem Befehl plot.STM ähnlich wie quanteda bereits weitere eigene Plot-Typen integriert, die bestimmte Bestandteile des Modells (zu Themen, Begriffen, Dokumenten) darstellen können, ohne dass man dies selbst in R umsetzen müsste.

Die folgenden vier Plots zeigen (a) den jeweilige Themenanteil am Korpus insgesamt, (b) ein Histogramm der Themenanteile innerhalb der Dokumente, (c) zentrale Begriffe zu vier verwandten Themen, sowie (d) den Kontrast zwischen zwei verwandten Themen.

plot(modell.stm, type = "summary", text.cex = 0.5, main = "Themenanteile am Korpus insgesamt", xlab = "geschätzter Themenanteil")

plot(modell.stm, type = "hist", topics = sample(1:anzahl.themen, size = 9), main = "Histogramme der Anteile einzelner Themen")

plot(modell.stm, type = "labels", topics = c(5, 12, 16, 21), main = "Themenbegriffe")

plot(modell.stm, type = "perspectives", topics = c(16,21), main = "Themenkontrast")

Als nächstes berechnen wir die Prävalenz der Themen über die Zeit. Dafür wird die Funktion estimateEffect verwendet, die ebenfalls zur Ausstattung von stm gehört und eine Regression der geschätzten Themenanteile rechnet. Im Unterschied zu der Möglichkeit, die wir auch im LDA-Beispiel für die Bestimmung von Themenanteilen hatten (wo eine Berechnung der Anteile nach Zeit ja auch kein Hindernis dargestellt hätte), können mit estimateEffekt auch Kovariaten berücksichtigt werden, was die Genauigkeit des Modells erheblich steigern kann. Zudem erhalten wir ein lokales Konfidenzintervall zu unserer Schätzung.

modell.stm.labels <- labelTopics(modell.stm, 1:anzahl.themen)
dfm2stm$meta$datum <- as.numeric(dfm2stm$meta$year)
modell.stm.effekt <- estimateEffect(1:anzahl.themen ~ country + s(year), modell.stm, meta = dfm2stm$meta)

Wir plotten nun die Themenprävalenz für neun ausgewählte Themen. Als Labels sind hier zur besseren Anschaulichkeit gleich die wichtigsten Schlüsselbegriffe gewählt worden; die etwas unansehnliche Schleifenstruktur wird notwendig, um eine Vielzahl von Themen direkt vergleichen zu können.

par(mfrow=c(3,3))
for (i in 1:9)
{
  plot(modell.stm.effekt, "year", method = "continuous", topics = i, main = paste0(modell.stm.labels$prob[i,1:3], collapse = ", "), ylab = "", printlegend = F)
}

Die Ergebnisse geben Grund zu der Annahme, dass sich mit STM-Themenmodellen tatsächlich interessante Trends identifizieren lassen. Zunächst sind die Ergebnisse zum Teil konfirmatorisch: Wir würden erwarten, dass ein Thema zu sowjetischen Nuklearwaffen (hier Thema #9) mit dem Ende der Sowjetunion stark abfällt. Dass das Thema nicht ganz verschwindet hat zum einen damit zu tun, dass es weiterhin Erwähnung findet, aber auch damit, dass es Berührung mit anderen Themen (etwa neuen russischen Atomwaffen) hat. Kein Themenmodell kann solche Differenzierungen perfekt vornehmen, weil sich manche Themen begrifflich schlicht zu sehr ähneln, auch wenn ein Mensch den Unterschied problemlos erkennnen würde. Wir stellen ausserdem fest, dass bestimmte Themen (im Sinne des Modells) einzelne historische Ereignisse beinhalten, zum Teil auch in Kombination, etwa der Libanonkrieg und die erste Intifada (Thema #2), der zweite Kongokrieg (Thema #5), oder die stufenweise Realisierung der europäischen Währungsunion (Thema #7). Andere Themen sind im Vergleich “zeitloser”, wobei manche sich in ihrem Niveau über die Zeit kaum verändern (pazifische Inselstaaten, Thema #4) und andere saisonal wiederkehren (atomare Abrüstung, Thema #6). Interessant ist der Abfall bei nationalen (sozialistischen) Unabhängigkeitsbewegungen, die in den 1970ern noch eine sichtbare Rolle spielten (Thema #3), oder Diskurse über die Reform der UN und des Weltsicherheitsrats (Thema #1).

Wie sind die Unterschiede bei dem angezeigten Konfidenzintervall zu interpretieren? Themen wie die Reform des Weltsicherheitsrates oder der europäischen Zusammenarbeit sind lexikalisch klarer identifzierbar als die (vermutlich recht variablen) Diskurse um die Interessen von Inselstaaten. Themen mit einem sehr klaren zeitlichen Profil sind i.d.R. zuverlässiger identifizierbar, als solche, die über lange Zeiträume hinweg auftreten.

Ideale STM-Themenanzahl bestimmen

Auch für ein STM-Modell lässt sich die (statistisch) ideale Anzahl von Themen bestimmen. Zunächst laden wir hierfür wieder den uninformativen (aber kleinen) Sherlock Holmes-Datensatz. Im folgenden Codeabschnitt wird dieser in eine DFM umgewandelt und anschließend konvertiert (diese Schritte sind identisch mit denen am Anfang des Kapitels).

load("daten/sherlock/sherlock.absaetze.RData")
meine.dfm <- dfm(korpus, remove_numbers = TRUE, remove_punct = TRUE, remove_symbols = TRUE, remove = c(stopwords("english"), "sherlock", "holmes"))
meine.dfm.trim <- dfm_trim(meine.dfm, min_termfreq = 2, max_termfreq = 75)
dfm2stm <- convert(meine.dfm.trim, to = "stm")

Nun wenden wir analog zur Verwendung von LDAtuning die Funktion searchK an, die direkt aus dem Funktionsumfang von stm kommt. Auch diese Funktion probiert alle Einstellung nacheinander durch, d.h. man muss Zeit mitbringen, um sie anzuwenden, insbesondere bei einem größeren Korpus als dem im Beispiel verwendeten. Analog zur obigen Berechnung des eigentlichen STM-Modells habe ich um Zeit zu sparen auch hier das Diagnose-Ergebnis bereits als RData-Datei gespeichert, die gleich geplottet werden kann. Die Funktionsweise ist analog zu den in LDAtuning verwendeten Verfahren auch hier die Maximierung bzw. Minimierung der Kennwerte mit steigendem k. Auch hier gilt, dass die statistischen Verfahren keinerlei unmittelbaren Aufschluss darüber bieten, wie einleuchtend die Themen für menschliche Leser notwendigerweise sind.

load("daten/sherlock/sherlock.stm.idealK.RData")
#mein.stm.idealK <- searchK(dfm2stm$documents, dfm2stm$vocab, K = seq(4, 20, by = 2), max.em.its = 75)
plot(mein.stm.idealK)

Abschließend lässt sich festhalten, dass Themenmodelle ein nützliches Werkzeit der automatisierten Inhaltsanalyse sind, und zwar sowohl dann, wenn man einen großen Datenbestand explorativ erschließen will, also auch, wenn es um die gezielte Ermittlung von systematischen Zusammenhängen des Themenvorkommens mit andere Variablen geht. Allerdings sind Themenmodelle auch keine Wunderwaffe. Sind bestimmte Voraussetzungen wie Mindestgröße und -vielfalt des Korpus (und zwar auf der Ebene von Wörtern und Dokumenten und ihrer Relation zu einander) nicht erfüllt, oder lassen sich keine schlüssigen Muster aus den Wortverteilungen ableiten, erhält man ein wenig schlüssiges Modell. Auch ist eben alles mit einem klaren Fussabdruck aus Worthäufigkeiten ein Thema im Sinne des Themenmodells, auch wenn es sich nicht um ein Thema in der menschlichen Interpretation handelt. Gerade bei Themenmodellen gilt also das für die automatisierte Inhaltsanalyse weithin erprobte Motto Validieren, Validieren, Validieren.