Markov chain Monte Carlo (MCMC) is a sampling method widely used for computation of hierarchical models in Bayesian statistics. One of the examples, Gibbs sampler, is simple compared to others like Metropolis-Hastings algorithm when the full conditionals (i.e. one parameter conditioned on others) probabilities can be explicitly evaluated. In Latent Dirichlet Allocation (LDA), a collapsed Gibbs sampler (CGS) is used where one marginalize out the topic distribution parameter \(\theta\) and the topic-word distribution parameter \(\phi\) in [1] (or \(\theta, \beta\) in [2]), leaving the topic assignments \(\mathbf{z}\) to be sampled (and to be fully conditioned on).
We use the same dataset (AP corpus) tested in LDA with VEM, and maintain the notations:
Considering the algorithmic structure of CGS, the way we store the
documents is different from the form word id :occurrence in
the case of VEM. Instead, we directly store the words with repetitions
in each document, i.e. length(corpus$docs[[d]]$words) will
be equal to corpus$docs[[d]]$N. Stopwords provided by
stopwords-iso are also removed from each document in the
corpus.
# Read file
file <- file('data/ap.dat', open='r')
M <- 0 # Number of documents
V <- 0 # Size of vocabulary, set to be the max representing number of the words
docs <- list()
# List of stopwords
SW <- stopwords("en", source = "stopwords-iso")
addSW <- c('percent','monday','tuesday','wednesday','thursday','friday','saturday','sunday')
vocab = read.table('data/vocab.txt', stringsAsFactors=FALSE)
SW <- which(vocab$V1 %in% SW) - 1
# Iterate through the documents
while (length(doc <- readLines(file, n=1, warn=FALSE)) != 0) {
M <- M + 1
doc <- unlist(strsplit(doc, ' '))
nwords <- as.integer(doc[1]) # Number of distinct words
words <- c()
occur <- c()
sw.count <- 0
sw.total <- 0
N <- 0 # Word count of document
# Iterate through the words
for (n in 1:nwords) {
word.occur <- unlist(strsplit(doc[n+1], ':'))
if (word.occur[1] %in% SW) { # If the word is a stop word
sw.count <- sw.count + 1
sw.total <- sw.total + as.integer(word.occur[2])
}
else { # Or, the word is not a stop word
words <- c(words, rep(as.integer(word.occur[1]), as.integer(word.occur[2])))
occur <- c(occur, as.integer(word.occur[2]))
N <- N + occur[length(occur)]
if (words[length(words)] > V) {V <- words[length(words)]}
}
}
nwords <- nwords - sw.count
docs[[M]] <- list(words=words, occur=occur, nwords=nwords, N=N)
}
close(file)
# Generate corpus: list of documents as sublists
V <- V + 1 # Representing number starts with 0
corpus <- list(docs=docs, M=M, V=V)
The runtime for the gibbs sampler could be long (each sample
approximately \(2\) minutes). For
experimental purposes, we could eliminate the corpus to half by randomly
removing the documents. The following chunk will NOT be compiled
otherwise (eval=FALSE).
removing <- sample(1:M, as.integer(M * 0.75), replace=FALSE) # Halving the corpus
corpus$docs <- corpus$docs[-removing]
corpus$M <- M - as.integer(M * 0.75)
M <- corpus$M
The structure of LDA model has been described in “Latent Dirichlet Allocation (1)” so we only present some essential specifications here. The Dirichlet prior for each column of the topic-word matrix \(\phi\) is just the “smoothing” technique in [1]. Notice that here we are not rigorously considering \(w_n\) as a vector in \(\mathbb{R}^V\). For readability, for example, by \(\phi_{iw_n}\) we are referring to the \((i,j)\)-th of the matrix if the word \(w_n\) has index \(j\) in the vocabulary (i.e. \(w_n = \text{the j-th vector in canonical basis}\)). \[ \text{For} ~~ \phi_i \in \mathbb{R}^V, ~i=1,...,K: \quad \quad p(\phi_{i}) = \frac{\Gamma(V\beta)}{\Gamma(\beta)^V} \prod_{v=1}^V \phi_{iv}^{\beta-1}, \quad \quad p(w_n \mid z_n, \phi_i) = \phi_{iw_n}, ~\forall n \] \[ \text{For} ~~ \theta_d \in \mathbb{R}^K, ~d=1,...,M: \quad \quad p(\theta_d) = \frac{\Gamma(K\alpha)}{\Gamma(\alpha)^K} \prod_{i=1}^K \theta_{di}^{\alpha-1}, \quad \quad p(z_n \mid \theta_d) = \theta_{dz_n},~ \forall n \]
Thus following these definitions, we can obtain \(p(\mathbf{z}\mid\mathbf{w})\) by \(p(\mathbf{w}\mid\mathbf{z}) p(\mathbf{z})\). Denote \(n_i^{(v)}\) the number of times the word \(v \in \{1,...,V\}\) was assigned to topic \(i\), then by argumenting the Dirichlet distribution we have: \[ p(\mathbf{w}\mid\mathbf{z}) = \left(\frac{\Gamma(V\beta)}{\Gamma(\beta)^V}\right)^K \prod_{i=1}^K \int \prod_{v=1}^V \phi_{iv}^{\beta -1 + n_i^{(v)}} d\phi_i = \left(\frac{\Gamma(V\beta)}{\Gamma(\beta)^V}\right)^K \prod_{i=1}^K \frac{\prod_{v=1}^V \Gamma(n_i^{(v)} + \beta)}{\Gamma(n_i^{(\cdot)} + V\beta)} \] Also denote \(n_i^{(d)}\) the number of times some word from the document \(d\in \{1,...,M\}\) was assigned to topic \(i\), similarly we have: \[ p(\mathbf{z}) = \left(\frac{\Gamma(K\alpha)}{\Gamma(\alpha)^K}\right)^M \prod_{d=1}^M \int \prod_{i=1}^K \theta_{di}^{\alpha -1 + n_i^{(d)}} d\theta_d = \left(\frac{\Gamma(K\alpha)}{\Gamma(\alpha)^K}\right)^M \prod_{d=1}^M \frac{\prod_{i=1}^K \Gamma(n_i^{(d)} + \alpha)}{\Gamma(n_{~\cdot}^{(d)} + K\alpha)} \]
As mentioned by [2], the posterior \(p(\mathbf{z}\mid\mathbf{w})\) can be computed by \(\frac{p(\mathbf{w}, \mathbf{z})}{\sum_\mathbf{z} p(\mathbf{w}, \mathbf{z})}\) but the denominator is intractable. Gibbs sampling solves this problem.
The core of Gibbs sampling is the derivation of full conditional probabilities \(P(z_n = i \mid \mathbf{z}_{-n}, \mathbf{w})\) for each topic \(i\in \{1,...,K\}\). Through some manipulation, we can represent this in the form of two probabilities we derived above: \[ P(z_n = i \mid \mathbf{z}_{-n}, \mathbf{w}) = \frac{p(\mathbf{z}, \mathbf{w})}{p(\mathbf{z}_{-n}, \mathbf{w})} = \frac{p(\mathbf{z}) p(\mathbf{w}\mid\mathbf{z})}{p(\mathbf{z}_{-n}) p(\mathbf{w}_{-n}\mid \mathbf{z}_{-n}) p(w_n)} \propto \frac{p(\mathbf{z}) p(\mathbf{w}\mid\mathbf{z})}{p(\mathbf{z}_{-n}) p(\mathbf{w}_{-n}\mid \mathbf{z}_{-n})} \]
It suffices to calculate the product of two terms: \(\frac{p(\mathbf{z})}{p(\mathbf{z}_{-n})}\) and \(\frac{p(\mathbf{w}\mid\mathbf{z})}{p(\mathbf{w}_{-n}\mid \mathbf{z}_{-n})}\) for each topic \(i\). Since the product will then be considered as the weights for sampling the next state (topic) of the current word in MCMC. To simplify and derive an explicit expression for these two terms, we need the identity \(\Gamma(x+1) = x\Gamma(x)\).
Start with the first term. There should be \(\prod_{d=1}^M\) and \(\prod_{i=1}^K\) in the ratio below. However, if we assume the current word \(w_n\) is from document \(\tilde{d}\), and has been assigned to topic \(\tilde{i}\), then terms involving documents other than \(\tilde{d}\) and topics other than \(\tilde{i}\) will be eliminated. Hence the ratio can be simplified as follows: \[ \frac{p(\mathbf{z})}{p(\mathbf{z}_{-n})} = \frac{\Gamma(n_\tilde{i}^{(\tilde{d})} + \alpha)}{\Gamma(n_{~\cdot}^{(\tilde{d})} + K\alpha)} \cdot \frac{\Gamma(n_{~\cdot, -n}^{(\tilde{d})} + K\alpha)}{ \Gamma(n_{\tilde{i},-n}^{(\tilde{d})} + \alpha)} = \frac{n_{\tilde{i},-n}^{(\tilde{d})} + \alpha}{n_{~\cdot,-n}^{(\tilde{d})} + K\alpha} \]
The second term is similar. Assuming the current word is \(\tilde{v}\) in the vocabulary \(\{1,...,V\}\): \[ \frac{p(\mathbf{w}\mid\mathbf{z})}{p(\mathbf{w}_{-n}\mid \mathbf{z}_{-n})} = \frac{\Gamma(n_\tilde{i}^{(\tilde{v})} + \beta)}{\Gamma(n_{\tilde{i}}^{(\cdot)} + V\beta)} \cdot \frac{\Gamma(n_{\tilde{i}, -n}^{(\cdot)} + V\beta)}{ \Gamma(n_{\tilde{i},-n}^{(\tilde{v})} + \beta)} = \frac{n_{\tilde{i},-n}^{(\tilde{v})} + \beta}{n_{\tilde{i},-n}^{(\cdot)} + V\beta} \]
These derivation makes the definition of the full conditional probability in the Gibbs sampler concise in two lines:
# This is Eq [5] in Griffiths & Steyvers, 2004
# 'sample' function in R receive unnormalized weights
first.term <- (n.di[d, ] + alpha) / (sum(n.di[d,]) + K * alpha)
second.term <- (n.wi[w_dn, ] + beta) / (colSums(n.wi) + V * beta)
In order the check the convergence of the Gibbs sampler, we check the ‘perplexity’ index used in [1]. The following chunk is the function for computing perplexity given the word-topic and document-topic counts of the current iteration.
perplexity <- function(n.wi, n.di) {
ll <- 0
for (i in 1:K) {
ll <- ll + lgamma(V*beta) - V * lgamma(beta)
ll <- ll + sum(lgamma(n.wi[,i] + beta))
ll <- ll - lgamma(sum(n.wi[,i]) + V*beta)
}
for (d in 1:M) {
ll <- ll + lgamma(K*alpha) - K * lgamma(alpha)
ll <- ll + sum(lgamma(n.di[d,] + alpha))
ll <- ll - lgamma(sum(n.di[d,]) + K*alpha)
}
n.tokens <- sum(sapply(corpus$docs, function(x) x$N))
return(exp(- ll / n.tokens))
}
We follow the popular setting of \(\alpha=50/K\) and \(\beta=0.1\). The initial states are assigned randomly. Note that rigorously one should keep a track on the states of each iteration, so that afterwards the mean or mode will be taken for the parameter estimation. However, this involves computation using tensors or large arrays in R. For the sampling process to be carried out in a reasonable amount of time, we will use a single state after the burn-in period of MCMC. And actually this still produce great and accurate results in topic modelling, as we shall see in the model interpretation output.
# MCMC setup
set.seed(123)
K <- 30
N.iter <- 100 # Number of MCMC samples
beta <- 0.1
alpha <- 50/K
states <- lapply(corpus$docs, function(x) rep(0, x$N)) # Zeros according to doc lengths
n.wi <- matrix(0, V, K) # word-topic matrix
n.di <- matrix(0, M, K) # document-topic matrix
# Initial state
# Iterate through documents
for (d in 1:M) {
doc <- corpus$docs[[d]]
N <- doc$N
# Iterate through words
for (n in 1:N) {
w_dn <- doc$words[n] + 1 # Account for word number starting from 0
# States initialization
states[[d]][n] <- sample(1:K, 1)
# Counts initialization
i.alloc <- states[[d]][n]
# Count of word w being assigned to topic i throughout
n.wi[w_dn, i.alloc] <- n.wi[w_dn, i.alloc] + 1
}
# Count of some words from document d being assigned to topic i
for (i in 1:K) { n.di[d, i] <- sum(states[[d]] == i) }
}
# Initialise perplexity
perp <- c(perplexity(n.wi,n.di))
# Sampler main loop
for (t in 1:N.iter) {
# print(sprintf('Iteration: %d', t))
# Iterate through documents
for (d in 1:M) {
doc <- corpus$docs[[d]]
N <- doc$N
# Iterate through words
for (n in 1:N) {
w_dn <- doc$words[n] + 1
# Leave-current-word-out
i.old <- states[[d]][n] # From previous iteration
n.wi[w_dn, i.old] <- n.wi[w_dn, i.old] - 1
n.di[d, i.old] <- n.di[d, i.old] - 1
# MCMC walk
term1 <- (n.wi[w_dn, ] + beta) / (colSums(n.wi) + V * beta)
term2 <- (n.di[d, ] + alpha) / (sum(n.di[d,]) + K * alpha)
cond.prob.weight <- term1 * term2
i.new <- sample(1:K, 1, prob=cond.prob.weight)
# Update state
n.wi[w_dn, i.new] <- n.wi[w_dn, i.new] + 1
n.di[d, i.new] <- n.di[d, i.new] + 1
states[[d]][n] <- i.new
}
}
# Record perplexity of current iteration
perp <- c(perp, perplexity(n.wi,n.di))
}
We can examine the topics by looking at the top words (the words with the highest probability given the topic).
# Parameter estimation
phi <- t(t(n.wi + beta) / (colSums(n.wi) + V * beta)) # Size V*K, the transposes are important
theta <- (n.di + alpha) / (rowSums(n.di) + K * alpha) # Size M*K
# Topic with highest probability for each document
topic <- apply(theta, 1, which.max)
# Top words in each topic
top.n <- 20 # Top 20 words each topic being shown
word.id = matrix(0, top.n, K)
probs = matrix(0, top.n, K)
for (i in 1:K) {
topic.top = sort(phi[,i], decreasing=TRUE, index.return=TRUE)
word.id[,i] = topic.top$ix[1:top.n]
probs[,i] = topic.top$x[1:top.n]
}
topic.top.words = matrix(vocab[word.id], top.n, K)
# First row
kable(topic.top.words[,1:10], booktabs = TRUE) %>%
column_spec(1:10, border_left = T, border_right = T) %>%
add_header_above(paste0("Topic ", 1:10)) %>%
add_header_above(c("Topic top words, 1~10" = 10)) %>%
kable_styling()
| market | john | air | health | miles | company | news | government | people | oil |
| stock | film | officials | medical | water | corp | time | military | program | cents |
| york | record | plane | hospital | southern | business | television | president | tax | prices |
| exchange | york | flight | people | city | offer | tuesday | defense | programs | farmers |
| trading | book | force | aids | north | announced | monday | human | taxes | food |
| issues | magazine | aircraft | drug | coast | share | week | rights | cut | futures |
| stocks | died | eastern | study | northern | chairman | network | north | social | trade |
| wall | art | spokesman | dr | san | companies | wednesday | security | income | cent |
| prices | movie | airport | care | reported | executive | expected | force | service | farm |
| shares | war | navy | disease | damage | president | calls | national | country | production |
| rose | editor | airlines | treatment | river | vice | angeles | rebels | director | tons |
| jones | vietnam | base | center | county | chief | series | report | housing | market |
| investors | martin | accident | doctors | california | deal | broadcast | tuesday | private | agriculture |
| bond | newspaper | ship | heart | park | companys | cbs | reagan | cities | price |
| average | history | planes | blood | central | firm | los | dec | security | corn |
| week | william | service | patients | texas | sale | spokesman | country | poor | supplies |
| volume | director | pilots | cancer | homes | management | nbc | sept | land | july |
| board | wrote | airline | researchers | fair | sell | scheduled | june | homeless | gasoline |
| securities | published | travel | children | officials | agreement | thursday | secret | public | products |
| street | george | report | virus | people | industry | sunday | political | store | drought |
# Second row
kable(topic.top.words[,11:20], booktabs = TRUE) %>%
column_spec(1:10, border_left = T, border_right = T) %>%
add_header_above(paste0("Topic ", 11:20)) %>%
add_header_above(c("Topic top words, 11~20" = 10)) %>%
kable_styling()
| united | court | people | bush | money | soviet | family | school | south | space |
| bush | decision | government | dukakis | federal | party | children | students | government | project |
| soviet | rights | israel | campaign | bank | government | yearold | university | united | building |
| east | law | israeli | democratic | plan | political | wife | black | africa | mission |
| president | ruling | police | republican | financial | gorbachev | day | city | african | launch |
| west | aid | killed | president | pay | union | time | women | national | shuttle |
| agreement | supreme | capital | jackson | claims | communist | life | college | president | scientists |
| germany | judge | peace | election | debt | leader | son | white | north | day |
| german | ruled | china | presidential | fund | opposition | mother | education | nations | time |
| reagan | appeals | protest | vice | banks | minister | days | student | black | earth |
| europe | civil | jewish | george | loan | elections | parents | schools | panama | nasa |
| summit | lawyers | army | convention | funds | power | woman | board | korea | feet |
| administration | legal | official | candidates | savings | prime | child | public | noriega | center |
| aid | request | minister | support | estate | moscow | husband | community | korean | planned |
| trade | commission | violence | vote | assets | country | daughter | mayor | country | field |
| economic | nicaragua | security | voters | trust | parties | left | job | political | director |
| union | contra | demonstrators | candidate | insurance | leaders | living | hotel | fighting | venus |
| nations | abortion | reported | democrats | loans | central | father | professor | mandela | built |
| foreign | courts | palestinian | primary | cash | economic | months | teachers | remains | start |
| baker | appeal | west | political | week | gorbachevs | apartment | citys | people | ground |
# Third row
kable(topic.top.words[,21:30], booktabs = TRUE) %>%
column_spec(1:10, border_left = T, border_right = T) %>%
add_header_above(paste0("Topic ", 21:30)) %>%
add_header_above(c("Topic top words, 21~30" = 10)) %>%
kable_styling()
| late | house | meeting | prison | workers | percent | military | department | people | police |
| dollar | committee | officials | trial | union | sales | war | office | american | people |
| japan | congress | told | drug | plant | rate | iraq | federal | america | killed |
| japanese | senate | official | charges | contract | increase | troops | attorney | play | city |
| yen | rep | news | court | environmental | report | forces | investigation | lot | shot |
| london | budget | conference | convicted | strike | economy | kuwait | district | coming | authorities |
| thursday | chairman | talks | death | labor | months | iraqi | filed | british | died |
| gold | sen | president | charged | officials | month | united | justice | radio | night |
| trade | legislation | friday | judge | jobs | economic | gulf | government | tour | church |
| major | administration | reporters | attorney | employees | reported | iran | law | brown | death |
| bid | congressional | week | guilty | plants | department | saudi | documents | played | car |
| friday | vote | agency | charge | safety | rose | american | hearing | music | told |
| close | measure | met | accused | department | growth | soldiers | judge | audience | officers |
| west | policy | press | arrested | waste | rates | army | washington | life | victims |
| tokyo | approved | time | cocaine | pay | june | force | suit | looked | saturday |
| british | president | foreign | jury | industry | total | attack | service | fall | hospital |
| rates | action | day | crime | company | quarter | held | fraud | stage | injured |
| french | lawmakers | washington | murder | local | annual | arabia | indictment | religious | shooting |
| dealers | proposal | visit | sentence | energy | survey | hostages | lawsuit | hands | yearold |
| dollars | defense | efforts | arrest | cars | increased | crisis | assistant | playing | attack |