TAI BUI
Back to curriculum
BuildPython

Topic Modeling — LDA and BERTopic

LDA: documents are mixtures of topics, topics are distributions over words. BERTopic: documents cluster in embedding space, clusters are topics. Same goal, different decompositions.

TIME: ~45 minutesPREREQUISITES: Phase 5 · 02 (BoW + TF-IDF), Phase 5 · 03 (Word2Vec)

Learning Objectives

  • Explain LDA's generative model: each document as a mixture of topics, each topic as a distribution over words, and fit it with scikit-learn's LatentDirichletAllocation
  • Build a BERTopic pipeline (sentence-transformer embeddings, UMAP reduction, HDBSCAN clustering, class-based TF-IDF) and interpret its topic assignments, including the -1 outlier label
  • Evaluate topic quality with c_v coherence and topic diversity, backed by a qualitative read of each topic's top words
  • Choose between LDA, BERTopic, and alternatives like NMF and Top2Vec based on document length, compute budget, and whether you need mixed-membership topic distributions

The Problem

Say you're staring down 10,000 customer support tickets, 50,000 news articles, or 200,000 tweets. You need to know what the collection is about, but nobody labeled it, and you don't even know how many categories it should split into.

Topic modeling solves that without supervision: feed it a corpus, get back a small set of coherent topics and, for each document, a distribution over those topics.

Two algorithmic families dominate here. LDA, from 2003, treats each document as a mixture of latent topics and each topic as a distribution over words, with Bayesian inference doing the work underneath. It still ships in production wherever you need mixed-membership topic assignments and word-level probabilities you can actually explain to someone.

BERTopic, from 2020, takes a different route: encode documents with BERT, cut the dimensionality with UMAP, cluster with HDBSCAN, then pull topic words out with class-based TF-IDF. It wins on short text and social media, anywhere semantic similarity matters more than shared vocabulary. The tradeoff is that each document gets exactly one topic, which starts to hurt once your documents run long.

This lesson builds intuition for both, then tells you which one to reach for on a given corpus.

Warm-up0/3
1. You have 10,000 unlabeled support tickets and want to find the natural groupings in them without knowing the categories ahead of time. What kind of task is this?Warm-up
2. You plot a pile of short reviews so that reviews with similar meaning land close together in space. What would you expect reviews complaining about the same issue to do?Warm-up
3. In a bag-of-words count matrix built from a set of documents, what does each cell hold?Warm-up

The Concept

LDA mixture model vs BERTopic clustering

LDA generative story. Each topic is a distribution over words. Each document is a mixture of topics. To generate a word in a document, sample a topic from the document's mixture, then sample a word from that topic's distribution. Inference reverses this: given observed words, infer the topic distribution per document and the word distribution per topic. Collapsed Gibbs sampling or variational Bayes does the math.

Key LDA output:

  • doc_topic: matrix (n_docs, n_topics), each row sums to 1 (document's topic mixture).
  • topic_word: matrix (n_topics, vocab_size), each row sums to 1 (topic's word distribution).

BERTopic pipeline. Where LDA fits a generative model over words, BERTopic finds topics by clustering: documents that land near each other in embedding space get grouped into one topic, and the cluster's most distinctive words become that topic's label.

  1. Encode each document with a sentence transformer (e.g., all-MiniLM-L6-v2). 384-dim vectors.
  2. Reduce dimensionality with UMAP to ~5 dimensions. BERT embeddings are too high-dim for clustering.
  3. Cluster with HDBSCAN. Density-based, produces variable-size clusters and an "outlier" label.
  4. For each cluster, compute class-based TF-IDF (TF-IDF run per cluster instead of per document) over the cluster's documents to extract top words.

Output is one topic per document (plus a -1 outlier label). Optionally, a soft membership via HDBSCAN's probability vector.

Check: The Concept0/10
1. In LDA, what is a topic?Define
2. What does mixed membership mean in a topic model such as LDA?Define
3. What role does UMAP play in the BERTopic pipeline?Define
4. What does HDBSCAN produce that suits it to BERTopic?Define
5. What is class-based TF-IDF in the BERTopic pipeline?Define
6. In LDA output, what does one row of the doc_topic matrix hold?Define
7. In LDA's generative story, how is each word in a document produced?Explain
8. What sequence of steps does BERTopic use to turn documents into topics?Explain
9. Why does BERTopic reduce the embeddings to about 5 dimensions before clustering?Explain
10. Why does a default BERTopic result give each document exactly one topic?Explain

Build It

Step 1: LDA via scikit-learn

python
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.decomposition import LatentDirichletAllocation
import numpy as np
 
 
def fit_lda(documents, n_topics=5, max_features=1000):
    cv = CountVectorizer(
        max_features=max_features,
        stop_words="english",
        min_df=2,
        max_df=0.9,
    )
    X = cv.fit_transform(documents)
    lda = LatentDirichletAllocation(
        n_components=n_topics,
        random_state=42,
        max_iter=50,
        learning_method="online",
    )
    doc_topic = lda.fit_transform(X)
    feature_names = cv.get_feature_names_out()
    return lda, cv, doc_topic, feature_names
 
 
def print_top_words(lda, feature_names, n_top=10):
    for idx, topic in enumerate(lda.components_):
        top_idx = np.argsort(-topic)[:n_top]
        words = [feature_names[i] for i in top_idx]
        print(f"topic {idx}: {' '.join(words)}")

Worth noticing: stopwords are stripped out, min_df and max_df filter rare and ubiquitous terms, and this uses CountVectorizer, not TfidfVectorizer, because LDA's model expects raw counts, not weighted ones.

Step 2: BERTopic (production)

python
from bertopic import BERTopic
 
topic_model = BERTopic(
    embedding_model="sentence-transformers/all-MiniLM-L6-v2",
    min_topic_size=15,
    verbose=True,
)
 
topics, probs = topic_model.fit_transform(documents)
info = topic_model.get_topic_info()
print(info.head(20))
valid_topics = info[info["Topic"] != -1]["Topic"].tolist()
for topic_id in valid_topics[:5]:
    print(f"topic {topic_id}: {topic_model.get_topic(topic_id)[:10]}")

The filter on Topic != -1 drops BERTopic's outlier bucket (documents HDBSCAN could not cluster). min_topic_size controls HDBSCAN's minimum cluster size; BERTopic's library default is 10. This example sets it to 15 explicitly for the lesson's scale. For corpora over 10,000 documents, increase to 50 or 100.

Step 3: evaluation

Both methods output topic words. The question is whether those words cohere.

  • Topic coherence (c_v). Combines NPMI (normalized pointwise mutual information) of top-word pairs over sliding-window contexts, aggregates the scores into topic vectors, and compares those vectors via cosine similarity. Higher is better. Use gensim.models.CoherenceModel with coherence="c_v".
  • Topic diversity. Fraction of unique words across all topics' top words. Higher is better (topics do not overlap).
  • Qualitative inspection. Read the top words of each topic. Do they name a real thing? Human judgment is still the last line of defense.
Check: Build It0/11
1. What does c_v coherence measure for a topic?Define
2. What does the topic diversity metric measure?Define
3. Why does scikit-learn's LDA expect raw counts (CountVectorizer), not TF-IDF?Explain
4. Why read each topic's top words by hand even when c_v and diversity look good?Explain
5. You run fit_lda from the lesson on 200 documents with its default arguments. What shapes do doc_topic and lda.components_ have?Apply
6. In fit_lda, a word appears in only one document of the corpus. With min_df=2, what happens to it?Apply
7. In print_top_words, n_top is 2, a topic's weights are [0.05, 0.40, 0.10, 0.25], and feature_names is ['bank', 'loan', 'river', 'rate']. What is printed after 'topic 0:'?Apply
8. In the BERTopic example, info["Topic"] holds [-1, 0, 1, 2]. What does valid_topics contain after the filter?Apply
9. You apply the BERTopic call to a corpus of 50,000 documents. Which change to min_topic_size matches the lesson's guidance?Apply
10. BERTopic on a small corpus labels most documents -1. Which setting should you check first?Analyze
11. A model's c_v is high, yet the same words appear in the top ten of most topics. Which metric exposes this?Analyze

When to pick which

SituationPick
Short text (tweets, reviews, headlines)BERTopic
Long documents with topic mixturesLDA
No GPU / limited computeLDA or NMF
Need document-level multi-topic distributionsLDA
LLM integration for topic labelingBERTopic (direct support)
Resource-constrained edge deploymentLDA
Max semantic coherenceBERTopic

The biggest practical consideration is document length. BERT embeddings truncate; LDA counts work on whatever length. For documents longer than the embedding model's context, either chunk + aggregate or use LDA.

Check: When to pick which0/5
1. When is LDA usually a better fit than BERTopic?Explain
2. Why does BERTopic typically win on short text such as tweets and headlines?Explain
3. A team runs BERTopic on very long reports and finds that the topics reflect only the start of each document. What is the likely cause and fix?Analyze
4. You must topic 200,000 tweets, you have a GPU, and you want an LLM to name each topic. Which approach fits best?Design
5. You have legal contracts far longer than any embedding context, each covering several themes, no GPU, and stakeholders who want per-document theme proportions. What do you pick?Design

Use It

The 2026 stack:

  • BERTopic. Default for short text and anything where semantics matter.
  • gensim.models.LdaModel. Classic LDA for production, mature, battle-tested.
  • sklearn.decomposition.LatentDirichletAllocation. Easy LDA for experiments.
  • NMF. Non-negative matrix factorization. Fast alternative to LDA, comparable quality on short text.
  • Top2Vec. Similar design to BERTopic. Smaller community but good on some benchmarks.
  • FASTopic. Newer, faster than BERTopic on very large corpora.
  • LLM-based labeling. Run any clustering, then prompt a model to name each cluster.
Check: Use It0/1
1. What is NMF in the lesson's topic-modeling stack?Define

Ship It

Save as outputs/skill-topic-picker.md:

markdown
---
name: topic-picker
description: Pick LDA or BERTopic for a corpus. Specify library, knobs, evaluation.
version: 1.0.0
phase: 5
lesson: 15
tags: [nlp, topic-modeling]
---
 
Given a corpus description (document count, avg length, domain, language, compute budget), output:
 
1. Algorithm. LDA / NMF / BERTopic / Top2Vec / FASTopic. One-sentence reason.
2. Configuration. Number of topics: `recommended = max(5, round(sqrt(n_docs)))`, clamped to 200 for corpora under 40,000 docs; permit >200 only when the corpus is genuinely large (>40k) and note the increased compute cost. `min_df` / `max_df` filters and embedding model for neural approaches also belong here.
3. Evaluation. Topic coherence (c_v) via `gensim.models.CoherenceModel`, topic diversity, and a 20-sample human read.
4. Failure mode to probe. For LDA, "junk topics" absorbing stopwords and frequent terms. For BERTopic, the -1 outlier cluster swallowing ambiguous documents.
 
Refuse BERTopic on documents longer than the embedding model's context window without a chunking strategy. Refuse LDA on very short text (tweets, reviews under 10 tokens) as coherence collapses. Flag any n_topics choice below 5 as likely wrong; flag >200 on corpora under 40k docs as likely over-splitting.
Check: Ship It0/4
1. Which failure mode should you probe for when you run LDA?Explain
2. A corpus has 2,500 documents. What topic count does the topic-picker formula max(5, round(sqrt(n_docs))) recommend?Apply
3. LDA on a set of tweets under 10 tokens gives incoherent topics. What is the best diagnosis?Analyze
4. You are handing a topic model to a client and need an evaluation plan that catches both statistical and meaningless topics. Which plan fits?Design
Author's Note

I've shipped both of these more than once, and the failure mode that costs the most time isn't picking the wrong algorithm. It's trusting the topic count you started with. Run LDA with n_topics=10 because ten felt like a reasonable guess, and you'll get ten topics back whether or not ten is the right number. The model has no opinion on whether it under-split or over-split; it just fits whatever you handed it. Sweep the count, plot coherence against it, and let the curve tell you where it actually bends. Don't eyeball a number up front and call it done.

The other one to watch: BERTopic's min_topic_size decides how much of your corpus quietly lands in the -1 bucket. Set it too high on a small corpus and half your documents come back "unclustered," which looks like a bug in your embeddings when it's really just a knob left at a value tuned for someone else's dataset.

Exercises

  1. Easy. Fit LDA with 5 topics on the 20 Newsgroups dataset. Print top 10 words per topic. Label each topic by hand. Did the algorithm find the real categories?
  2. Medium. Fit BERTopic on the same 20 Newsgroups subset. Compare the number of topics found, top words, and qualitative coherence against LDA. Which surfaces the real categories more cleanly?
  3. Hard. Compute c_v coherence for both LDA and BERTopic on your corpus. Run each with 5, 10, 20, 50 topics. Plot coherence vs topic count. Report which method is more stable across topic counts.

Key Terms

TermWhat people sayWhat it actually means
TopicA thing the corpus is aboutA probability distribution over words (LDA) or a cluster of similar documents (BERTopic).
Mixed membershipDoc is multiple topicsLDA assigns each document a distribution over all topics.
UMAPDimensionality reductionManifold learning that preserves local structure; used in BERTopic.
HDBSCANDensity clusteringFinds variable-size clusters; produces "noise" label (-1) for outliers.
c_v coherenceTopic quality metricAverage pointwise mutual information of top topic words within sliding windows.

Further Reading

Code

Ships With