#install.packages("tidyverse)
#install.packages("RCurl")
#install.packages("quanteda")
#install.packages("stm")
#install.packages("reshape2")
library("tidyverse")
library("RCurl")
library("quanteda")
library("stm")
library("reshape2")
Sitzung 4: Topic Modeling
1. Pakete laden und Daten einlesen
Zunächst installieren alle Pakete, die wir für diese Sitzung brauchten (z.B. tidyverse
). Ihr braucht install.packages()
nur, wenn ihr die Pakete im Methodencafe noch nicht installiert hattet.
Neu hinzugekommen seit dem Methodencafe ist das stminsights
-Paket, das ihr neu installieren müsstest.
Wenn das bei euch nicht klappt, ist das kein Problem - wir brauchen das Paket nur für eine zusätzliche Analyse am Ende.
install.packages("stminsights")
library("stminsights")
Nun lesen wir die Daten wieder ein und führen die bereits erlernten Preprocessing-Schritte, inkl. der Transformation in eine Document-Feature-Matrix, aus:
# Daten laden
<- getURL("https://raw.githubusercontent.com/valeriehase/textasdata-ms/main/data/data_tvseries.csv")
url <- read.csv2(text = url)
data
# Preprocessing
<- tokens(data$Description,
tokens what = "word", #Tokenisierung, hier zu Wörtern als Analyseeinheit
remove_punct = TRUE, #Entfernung von Satzzeichen
remove_numbers = TRUE) %>% #Entfernung von Zahlen
# Kleinschreibung
tokens_tolower() %>%
# Entfernung von Stoppwörtern
tokens_remove(stopwords("english")) %>%
# Stemming
tokens_wordstem()
# Text-as-Data Repräsentation als Document-Feature-Matrix
<- tokens %>%
dfm dfm() %>%
# Relative pruning
dfm_trim( min_docfreq = 0.005,
max_docfreq = 0.99,
docfreq_type = "prop",
verbose = TRUE)
Jetzt sind wir bereit für das Topic Modeling!
2. Anzahl Themen K
Relevante Entscheidungen für ein Topic-Modeling umfassen u.a. die Anzahl von Themen K
, die das Modell identifizieren soll (mit anderen Aspekten, wie der Wahl des Algorithmus oder weiterer Hyperparameter, beschäftigen wir uns heute nicht).
Zunächst müssen wir unsere dfm
in ein stm
-Objekt umwandeln, damit das stm
-Paket, das wir für die Berechnung des Topic Models brauchen, das Datenformat versteht.
# DFM in STM-Objekt umwandeln
<- convert(dfm, to = "stm") dfm_stm
2.1 Statistischer Fit
Ein Kriterium für die Entscheidung bzgl. K kann die der statistische Fit sein.
Mit der Funktion searchK()
aus dem stm
-Paket berechnen wir den statistischen Fit hier beispielhaft für Modelle mit 4 vs. 6 Themen.
Das Argument verbose
, das wir hier auf TRUE
setzen, führt dazu, dass uns R alle iterativen Berechnungen ausgibt. Wir können also nachverfolgen, was der Computer gerade berechnet.
# dfm_stm$documents: Welche Dokumente nutzen wir?
# dfm_stm$vocab: Welche Features nutzen wir?
<- searchK(dfm_stm$documents, dfm_stm$vocab,
stat_fit K = c(4,6), verbose = TRUE)
Um das Ergebnis zu interpretieren, ploten wir die Daten. Hierfür nutzen wir das Packet ggplot2
aus dem tidyverse
-Universum.
# Wir speichern die Ergebnisse im Objekt "Plot" ab
<- data.frame("K" = c(4, 6),
plot
#Kohärenz: Je höher, desto besser
"Coherence" = unlist(stat_fit$results$semcoh),
#Perplexität: Je niedriger, desto besser
"Perplexity" = unlist(stat_fit$results$heldout))
# Wir wandeln das Format zu einem "long format" um
<- melt(plot, id = c("K"))
plot
# Plot erstellen
ggplot(plot, aes(K, value, color = variable)) +
geom_line(linewidth = 1.5, show.legend = FALSE) +
scale_x_continuous(breaks = c(4, 6)) +
facet_wrap(~ variable, scales = "free_y") +
labs(x = "Anzahl Themen K",
title = "Statistischer Fit für Modelle mit K = 4 und K = 6")
Als nächstes schauen wir auf die Interpretierbarkeit der Themen!
2.2 Inhaltliche Interpretierbarkeit
Eine weitere Grundlage, um sich für die Anzahl der Themen zu entscheiden, sind Top Features und Top Documents: Welches Modell ist im Vergleich besser interpretierbar?
Dafür müssen zunächst die einzelnen Modelle mit K = 4 bzw. K = 6 berechnet werden.
# Model mit K = 4 berechnen
<- stm(documents = dfm_stm$documents,
model_4K vocab = dfm_stm$vocab,
K = 4)
# Model mit K = 6 berechnen
<- stm(documents = dfm_stm$documents,
model_6K vocab = dfm_stm$vocab,
K = 6)
2.2.1 Top Features
Schauen wir uns an, wie interpretierbar die Modelle auf Basis ihrer Features wirken:
Um die Top Features für jedes Modell zu berechnen, nutzen wir die labelTopics()
-Funktion aus dem stm
-Paket.
Als Argumente bestimmen wir ein Modell sowie über das n
-Argument, das nur die zehn wichtigsten Features ausgegeben werden sollen. Für den anschliessenden Vergleich speichern wir die Top Features im Objekt topics_4
bzw. topics_6
ab:
# Top Features für K = 4
<- labelTopics(model_4K, n = 10)
topics_4K
# Nur Top-10 Features nach Frex-Gewichtung, welche besser interpretierbar ist
# Gewichtet Features nach Kohärenz und Exklusivität
<- data.frame("features" = t(topics_4K$frex))
topics_4K
# Benennung & Ausgabe
colnames(topics_4K) <- paste("Topics", c(1:4))
topics_4K
Topics 1 Topics 2 Topics 3 Topics 4
1 crime find life mysteri
2 human seri live discov
3 detect struggl new boy
4 adventur chang friend lead
5 crimin name citi agent
6 case everi school angel
7 solv polit york dark
8 use head togeth mother
9 futur seem high surviv
10 alien embark person children
# Top Features für K = 6
<- labelTopics(model_6K, n = 10)
topics_6K
# Nur Top-10 Features nach Frex-Gewichtung, welche besser interpretierbar ist
# Gewichtet Features nach Kohärenz und Exklusivität
<- data.frame("features" = t(topics_6K$frex))
topics_6
#Benennung & Ausgabe
colnames(topics_6) <- paste("Topics", c(1:6))
topics_6
Topics 1 Topics 2 Topics 3 Topics 4 Topics 5 Topics 6
1 polic best vampir human comedi american
2 crime school famili alien show war
3 detect new teen earth togeth serial
4 profession boy brother planet various dream
5 investig hero differ futur work killer
6 stori york son struggl anim plan
7 person mother epic fight name cia
8 case return demon galaxi featur turn
9 special high world space everyth drug
10 polit troubl navig last lead begin
Die Top Features geben bereits einen ersten Eindruck, worum es sich inhaltlich bei den Topics handelt.
2.2.1 Top Documents
Zusätzlich helfen die Top Documents eines Themas, dieses zu interpretieren. Um diese zu finden, nutzen wir die findThoughts()
-Funktion des stm
-Pakets.
Als Argumente werden wieder das jeweilige Modell, die Serienbeschreibungen aus unserem Dataframe via daten_df$Description
, das zu untersuchende Topic, hier Topic 1, und die Anzahl der zu identifizierenden Top Documents, hier drei, eingegeben.
Schauen wir Topic 1 an:
findThoughts(model_4K, data$Description, topics = 1, n = 3)
Topic 1:
Detective Jane Rizzoli and Chief Medical Examiner Dr. Maura Isles team up to solve crimes in Boston.
An elite unit, led by an ex-homicide cop, which is linked to the Miami-Dade Police Department, uses all possible resources and skills at their disposal to solve complex crimes in cross-cultural Miami.
The further adventures in time and space of the alien adventurer known as the Doctor and his companions from planet Earth.
Die Top Documents zeigen, ähnlich wie die Top Features, dass Topic 1 Krimi-Serien zusammenfasst. Für eine Evaluierung der Themen müssen dann für beide Modelle und alle jeweiligen Themen die Top Documents ausgegeben und die Ergebnisse verglichen werden.
2.3. Rank-1 Metrik
Mit Hilfe der Rank-1 Metrik lässt sich zuletzt zeigen, wie oft ein Thema als Hauptthema vorkommt. Hier wird jedem Dokument ein einziges Hauptthema zugeordnet - nämlich das, für welches das Dokument laut der Document-Topic-Matrix die höchste bedingte Wahrscheinlichkeit hat.
So liessen sich Themen identifizieren, die z. B. wenig prävalent und damit ggf. irrelevant sind. Auch lässt sich nachverfolgen, wie sich Themen über Modelle mit z. B. mehr K in mehrere Themen “aufsplitten”.
Im ersten Schritt berechnen wir die Document-Topic-Matrix, in der für jeden Dokument zusammengefasst wird, welches Thema wie gut zum Dokument passt. Das machen wir mit der make.dt()
Funktion aus dem stm
-Paket.
<- make.dt(model_4K)
theta_4K <- make.dt(model_6K)
theta_6K
#Schauen wir uns kurz beispielhaft die Matrix an:
%>%
theta_4K head()
docnum Topic1 Topic2 Topic3 Topic4
<int> <num> <num> <num> <num>
1: 1 0.2663954 0.2620603 0.2840283 0.1875160
2: 2 0.3234499 0.1736692 0.2082024 0.2946784
3: 3 0.1401247 0.1727312 0.1448235 0.5423207
4: 4 0.1237850 0.1137826 0.6349453 0.1274870
5: 5 0.1694126 0.1476420 0.3017674 0.3811779
6: 6 0.2748651 0.1791878 0.2271607 0.3187864
Jetzt nehmen wir diese Matrix und ordnen jedem Dokument das Thema mit der höchsten Prävalenz zu.
# Zuerst erstellen wir zwei leere Spalten in unserem Dataframe data
<- data %>%
data
# Leere Variable für Hauptthema, wird später "aufgefüllt"
mutate(Rank1_K4 = NA,
Rank1_K6 = NA)
# Berechnung von Rank-1 Metrik
for (i in 1:nrow(data)){ # Schleife: Für jede nachfolgende Zeile...
# Bestimme Hauptthema für K = 4
# Wähle alle Spalten der Document-Topic-Matrix aus (ohne die erste, die nur doc_id enthält)
<- theta_4K[i,-1]
column
# Bestimmung des Hauptthemas (Spalte mit dem höchsten Wert)
<- colnames(column)[which(column == max(column))]
maintopic
# Zuweisung des Hauptthemas zur entsprechenden Zeile
$Rank1_K4[i] <- maintopic
datarm(column, maintopic)
# Bestimme Hauptthema für K = 6
# Wähle alle Spalten der Document-Topic-Matrix aus (ohne die erste, die nur doc_id enthält)
<- theta_6K[i,-1]
column
# Bestimmung des Hauptthemas (Spalte mit dem höchsten Wert)
<- colnames(column)[which(column == max(column))]
maintopic
# Zuweisung des Hauptthemas zur entsprechenden Zeile
$Rank1_K6[i] <- maintopic
datarm(column, maintopic)
}
Schauen wir uns an, wie häufig jedes Thema bei K = 4 vorkommt!
# Erzeugung einer Häufigkeitstabelle für Rank-1 Themen bei K = 4
%>%
data
# absolute Anzahl jedes Themas
count(Rank1_K4) %>%
# Ausgabe in Prozent (perc)
mutate(perc = prop.table(n)*100,
perc = round(perc, 2))
Rank1_K4 n perc
1 Topic1 190 21.11
2 Topic2 40 4.44
3 Topic3 311 34.56
4 Topic4 359 39.89
Und jetzt das gleiche für K = 6
# Erzeugung einer Häufigkeitstabelle für Rank-1 Themen bei K = 6
%>%
data
# absolute Anzahl jedes Themas
count(Rank1_K6) %>%
# Ausgabe in Prozent (perc)
mutate(perc = prop.table(n)*100,
perc = round(perc, 2))
Rank1_K6 n perc
1 Topic1 147 16.33
2 Topic2 116 12.89
3 Topic3 189 21.00
4 Topic4 197 21.89
5 Topic5 139 15.44
6 Topic6 112 12.44
Die Ergebnisse zeigen, dass bei K = 6 die Verteilung der Dokumente wesentlich gleichmäßiger ausfällt.
Für eine endgültige Entscheidung von K sollten diese Metriken für deutlich mehr als zwei Ks ausgegeben werden.
In diesem Fall deuten die ersten Ergebnisse darauf hin, dass ein Modell mit 6 Themen den TV-Seriendatensatz ggf. besser abbildet als ein Modell mit 4 Themen.
3. Analyse
3.1 Einfluss unabhängiger Variablen
Für eine beispielhafte Analyse nehmen wir nun an, dass wir uns für K = 6 Themen entschieden haben. Mit dem stm
-Paket können wir als weiteren Analyseschritt eine unabhängige Variable einzubeziehen, um die Prävalenz (prevalence
-Argument) oder den Inhalt (content
-Argument) von Themen zu modellieren:
Gehen wir als Arbeitshypothese davon aus, dass das Jahr, in dem eine Serie zuerst ausgestrahlt wurde, einen Einfluss auf das dort beschriebene Thema hat.
Beispielsweise könnte es sein, dass unterschiedliche Jahre von unterschiedlichen “Genres” geprägt wurden.
Zunächst nutzen wir wieder reguläre Ausdrücke, um das Erstausstrahlungs-Jahr aller Serien zu identifizieren. Bitte beachtet dabei, dass wir fehlende Werte einfach mit dem Mittelwert von Start_Year
ersetzen. Das würde man normalerweise nicht machen, aber fehlende Werte für unabhängige Variablen sind im stm
-Paket höllisch schwierig zu handeln - das tun wir uns heute nicht an.
<- data %>%
data
# Wir entfernen alle nicht-numerische Zeichen, um "-" zu entfernen
mutate(Year_Start = gsub("[^0-9]", "", Year),
# Wir beschränken uns nur auf die ersten 4 Jahre
Year_Start = substr(Year_Start, 1, 4),
# Wir verwandeln das ganze in eine numerische Variable
Year_Start = as.numeric(Year_Start),
#Wir ersetzen fehlende Werte mit dem Mittelwert (2010)
Year_Start = replace(Year_Start,
is.na(Year_Start),
2010))
#Ausgabe der ersten Zeilen
%>%
data
#Reduktion auf weniger Variablen
select(Title, Year, Year_Start) %>%
#Ausgabe der ersten Zeilen
head()
Title Year Year_Start
1 1. Game of Thrones 2011–2019 2011
2 2. Breaking Bad 2008–2013 2008
3 3. Stranger Things 2016–2025 2016
4 4. Friends 1994–2004 1994
5 5. The Walking Dead 2010–2022 2010
6 6. Sherlock 2010–2017 2010
Dann nehmen wir die Variable Year_Start
als unabhängige Variable in unser Modell auf, um die Prävalenz von Themen zu modellieren:
# Wir lassen das angepasste Modell laufen
<- stm(documents = dfm_stm$documents,
model_6K_year vocab = dfm_stm$vocab,
K = 6,
prevalence = ~ Year_Start, #neu!
data = data)
Jetzt können wir mit der Funktion estimateEffect()
aus dem stm
-Paket berechnen, inwiefern das Erscheinungsjahr einer Serie einen Einfluss auf das dort behandelte Thema hat:
<- estimateEffect(formula = ~ Year_Start,
effect stmobj = model_6K_year,
metadata = data)
Diesen Effekt können wir nun auch grafisch darstellen. Bsp. wollen wir wissen: Hat das Erscheinungsjahr einen Effekt darauf, wie häufig Serien z.B. Drama (Topic 2) vs. Sci-Fi (Topic 4) darstellen?
Schauen wir uns kurz die Themen an:
%>%
topics_6 select(`Topics 2`, `Topics 4`)
Topics 2 Topics 4
1 best human
2 school alien
3 new earth
4 boy planet
5 hero futur
6 york struggl
7 mother fight
8 return galaxi
9 high space
10 troubl last
Wir plotten die Ergebnisse und sehen - es scheint Unterschiede über die Zeit hinweg zu geben!
plot(effect, "Year_Start",
method = "continuous",
topics = c(2,4),
model = model_6K_year)
3.2 Visualisierung des Topic Models
Um unser Modell noch zu verstehen, können wir es zudem mit dem stminsights
-Paket visualisieren. Dafür…
- erstellen wir zunächst das Objekt
out
, indem alle wichtigen Infos gespeichert sind. Dieses benötigt dasstminsights
-Paket. - starten wir dann die zugehörige Shiny-App mit
run_stminsights()
- laden dort anschliessend unser R-Environment hoch.
Das R-Environment findet ihr unter Sitzung 4 zum Download (hier), solltest ihr bis hierhin Probleme gehabt haben, den Code auszuführen.
# Wir erstellen das Objekt out, in dem alle wichtigen Infos gespeichert sind
# Das stminsights-Paket benötigt dieses Format
<- list(documents = dfm_stm$documents,
out vocab = dfm_stm$vocab,
meta = dfm_stm$meta)
# Wir speichern das Environment ab, um es hochzuladen
save.image("Sitzung4.RDATA")
# Wir lassen die Shiny App laufen
run_stminsights()
Aufgabe 1 📌
Aufgabe 1.1 (Basis)
Könnt ihr testen, wie sich das Modell verändert, wenn wir mit K = 10 Serien arbeiten? Wird es besser oder schlechter interpretierbar?
# dfm_stm$documents: Welche Dokumente nutzen wir?
# dfm_stm$vocab: Welche Features nutzen wir?
<- searchK(dfm_stm$documents, dfm_stm$vocab,
stat_fit K = c(4, 6, 10))
# Wir speichern die Ergebnisse im Objekt "Plot" ab
<- data.frame("K" = c(4, 6, 10),
plot "Coherence" = unlist(stat_fit$results$semcoh),
"Perplexity" = unlist(stat_fit$results$heldout))
# Wir wandeln das Format zu einem "long format" um
<- melt(plot, id = c("K"))
plot
# Plot erstellen
ggplot(plot, aes(K, value, color = variable)) +
geom_line(linewidth = 1.5, show.legend = FALSE) +
scale_x_continuous(breaks = c(4, 6, 10)) +
facet_wrap(~ variable, scales = "free_y") +
labs(x = "Anzahl Themen K",
title = "Statistischer Fit für Modelle mit K = 4, K = 6 und K = 10")
# Model mit K = 10 berechnen
<- stm(documents = dfm_stm$documents,
model_10K vocab = dfm_stm$vocab,
K = 10,
verbose = F)
# Top Features
#für K = 10
<- labelTopics(model_10K, n = 10)
topics_10K <- data.frame("features" = t(topics_10K$frex))
topics_10 colnames(topics_10) <- paste("Topics", c(1:10))
topics_10
Aufgabe 1.2 (Fortgeschritten)
Könnt ihr mittels des Datensatzes zu Horoskopen testen, ob Zwillinge und Wassermänner andere Themen in ihren Horoskopen vorhergesagt kriegen?
# Daten laden
<- getURL("https://raw.githubusercontent.com/valeriehase/textasdata-ms/main/data/data_horoscope.csv")
url <- read.csv2(text = url)
data
# Reduktion auf Daten von Zwillingen und Wassermännern
<- data %>%
data filter(Signs %in% c("AQUARIUS", "GEMINI"))
# Preprocessing
<- tokens(data$Horoscope,
tokens what = "word", #Tokenisierung, hier zu Wörtern als Analyseeinheit
remove_punct = TRUE, #Entfernung von Satzzeichen
remove_numbers = TRUE) %>% #Entfernung von Zahlen
# Kleinschreibung
tokens_tolower() %>%
# Entfernung von Stoppwörtern
tokens_remove(stopwords("english")) %>%
# Stemming
tokens_wordstem()
# Text-as-Data Repräsentation als Document-Feature-Matrix
<- tokens %>%
dfm dfm() %>%
# Relative pruning
dfm_trim(min_docfreq = 0.005,
max_docfreq = 0.99,
docfreq_type = "prop",
verbose = TRUE)
# DFM in STM-Objekt umwandeln
<- quanteda::convert(dfm, to = "stm")
dfm_stm
# Berechnung eines Modells, hier probeweise mit K = 5 Themen
# Wir nutzen das Sternzeichen als unabhängige Variable
<- stm(documents = dfm_stm$documents,
model_horoscope vocab = dfm_stm$vocab,
K = 4,
prevalence = ~ Signs,
data = data,
verbose = F)
# Ein erster Blick in die Top Features
<- labelTopics(model_horoscope, n = 10)
topics_horoscope <- data.frame("features" = t(topics_horoscope$frex))
topics_horoscope colnames(topics_horoscope) <- paste("Topics", c(1:4))
topics_horoscope
Topics 1 Topics 2 Topics 3 Topics 4
1 person feel though rather
2 financi peopl success keep
3 matter accept just go
4 improv like get may
5 well work friend sure
6 relationship give let quick
7 life ideal strong difficulti
8 now although far busi
9 care aquarian next way
10 situat practic away eye
#Schauen wir uns den Effekt an
<- estimateEffect(formula = ~ Signs,
effect stmobj = model_horoscope,
metadata = data)
# Und visualisieren wir den Unterschied
plot(effect, "Signs",
method = "pointestimate",
topics = c(4),
model = model_horoscope)