Topic models: LDA and NMF
Build LDA's plate model, understand Gibbs sampling as the inference engine, and use NMF as a deterministic matrix-factorization alternative.
A document is a mixture of topics; each topic is a distribution over words. Topic models discover these latent themes from a corpus without supervision, grouping words that tend to appear together and assigning each document a mixture of those groups.
The two canonical approaches are Latent Dirichlet Allocation (LDA), a probabilistic graphical model, and Non-negative Matrix Factorization (NMF), a deterministic matrix decomposition[2].
Latent Dirichlet Allocation
The generative story
LDA assumes documents are generated by the following process[1]:
- For each document , draw a topic mixture .
- For each of the topics , draw a word distribution .
- For each word position in document : a. Draw a topic assignment . b. Draw a word .
The Dirichlet priors and control sparsity: small produces documents that concentrate on few topics; small produces topics that concentrate on few words.
Plate notation
Shaded nodes () are observed; all others are latent.
Inference: collapsed Gibbs sampling
Exact posterior inference is intractable; the standard approach is collapsed Gibbs sampling, which integrates out and analytically and samples only the topic assignments .
The conditional posterior for a single assignment (given all others):
where is the number of words in document assigned to topic (excluding position ), and is the number of times word is assigned to topic (excluding position ).
Each iteration cycles over every word in the corpus and resamples its topic assignment. After burn-in, the averaged counts estimate and .
Reading the output
After convergence:
- : distribution over vocabulary for topic . The top-10 words characterize the topic.
- : distribution over topics for document .
import numpy as np
from sklearn.decomposition import LatentDirichletAllocation
from sklearn.feature_extraction.text import CountVectorizer
docs = [
"The president signed the new healthcare bill into law today.",
"Stock markets rose sharply on strong earnings reports.",
"Scientists discover a new species of deep-sea fish.",
"Senate debates immigration reform and border security funding.",
"Tech company reports record profits driven by cloud services.",
"Marine biologists study coral reef bleaching in the Pacific.",
]
vectorizer = CountVectorizer(stop_words="english")
X = vectorizer.fit_transform(docs)
vocab = vectorizer.get_feature_names_out()
lda = LatentDirichletAllocation(n_components=3, random_state=0, max_iter=100)
lda.fit(X)
for k, phi in enumerate(lda.components_):
top_words = [vocab[i] for i in phi.argsort()[-5:][::-1]]
print(f"Topic {k}: {', '.join(top_words)}")Non-negative Matrix Factorization
NMF factorizes the document-term matrix into two non-negative factors:
- : the word distribution for topic (analogous to in LDA).
- : the topic weight vector for document (analogous to ).
Non-negativity forces additive, parts-based decompositions: topics can only combine positively, which produces interpretable components.
Objective: minimize the Frobenius reconstruction error with a multiplicative update:
Multiplicative update rules (Lee and Seung, 2001):
Division and multiplication are elementwise. These updates guarantee non-negativity is preserved and the objective never increases.
from sklearn.decomposition import NMF
nmf = NMF(n_components=3, random_state=0, max_iter=500)
W = nmf.fit_transform(X)
H = nmf.components_
for k in range(3):
top_words = [vocab[i] for i in H[k].argsort()[-5:][::-1]]
print(f"NMF topic {k}: {', '.join(top_words)}")LDA vs NMF
| Property | LDA | NMF |
|---|---|---|
| Framework | Probabilistic graphical model | Matrix factorization |
| Inference | MCMC (Gibbs) or variational EM | Multiplicative updates |
| Interpretability | Principled uncertainty | Simpler, faster |
| Topic coherence | Often better | Comparable on short docs |
| Scalability | Slower | Faster |
Exercises
Exercise 1. In collapsed Gibbs sampling, the unnormalized resampling weight for assigning word in document to topic is the product of a document-topic affinity and a topic-word affinity. A corpus has topics, vocabulary words, hyperparameters , . Excluding the current word, document has counts , (so ); the current word has topic counts , , with topic totals , . Compute the unnormalized weight for each topic and the resulting probability of topic 0.
Solution
Step 1. Apply the formula for topic 0. Document term: . Word term: .
Step 2. For topic 1: document term , word term .
Step 3. Normalize:
The word is almost certainly assigned to topic 0: both the document (already dominated by topic 0) and the topic-word counts (the word has been seen in topic 0, never in topic 1) pull the same way, and the small priors leave only a vanishing chance for topic 1.
Exercise 2. NMF minimizes with the multiplicative update . Show that if , , and start non-negative, the update keeps non-negative, and that a fixed point of the update satisfies the stationarity condition wherever .
Solution
Step 1. The update multiplies each entry by the ratio . With and , the numerator is a product of non-negative matrices, so it is non-negative; likewise the denominator . A non-negative times a non-negative ratio stays non-negative, so non-negativity is preserved at every iteration.
Step 2. At a fixed point, the update leaves unchanged: .
Step 3. Wherever , divide both sides by to get , i.e. . This is exactly the first-order stationarity (KKT) condition: the gradient of the Frobenius objective with respect to is , which vanishes on the support of at a fixed point.
Exercise 3 (code). Implement NMF with the multiplicative updates in pure numpy on a small non-negative document-term matrix. Run 200 iterations and confirm the factors stay non-negative and the Frobenius reconstruction error decreases monotonically.
Solution
import numpy as np
rng = np.random.default_rng(0)
D, V, K = 6, 8, 3
# A non-negative document-term matrix built from latent parts so structure exists
W_true = rng.random((D, K))
H_true = rng.random((K, V))
X = W_true @ H_true
W = rng.random((D, K)) + 1e-3
H = rng.random((K, V)) + 1e-3
eps = 1e-9
errors = []
for _ in range(200):
H = H * (W.T @ X) / (W.T @ W @ H + eps)
W = W * (X @ H.T) / (W @ H @ H.T + eps)
errors.append(np.linalg.norm(X - W @ H))
errors = np.array(errors)
assert (W >= 0).all() and (H >= 0).all() # non-negativity preserved
# Error is non-increasing (allow a tiny numerical slack from the eps guard)
assert (np.diff(errors) <= 1e-6).all()
assert errors[-1] < errors[0]
print("start err:", round(errors[0], 4), " end err:", round(errors[-1], 6))The multiplicative updates keep both factors in the non-negative orthant and drive the reconstruction error down step by step, the empirical face of the monotone-decrease guarantee.
Both produce interpretable topics when is in the range 10-100 and documents are reasonably long. Modern LLMs have largely replaced topic models for semantic search (embeddings from E2 are more expressive), but topic models remain useful for exploratory analysis at scale with no labeled data.
References
- [1]Blei, D. M., Ng, A. Y., & Jordan, M. I. (2003). "Latent Dirichlet allocation". JMLR, 3, 993-1022. The generative model and variational inference for LDA.↩
- [2]Jurafsky, D., & Martin, J. H. (2024). Speech and Language Processing (3rd ed. draft). Topic models and vector-space document representations.↩