Skip to content

Topic Modeling & BERTopic

Topic modeling answers a deceptively simple question: given thousands of documents, what are they about? It is unsupervised β€” no labels, no predefined categories β€” making it the text-world sibling of clustering. Applications: mining customer reviews and support tickets, organizing news archives, monitoring social media, exploring scientific literature.

This lesson introduces the classical approach (LDA) and then BERTopic β€” a modern pipeline assembled almost entirely from techniques you have already learned in this course.

Classical topic modeling: LDA

Latent Dirichlet Allocation (Blei, Ng & Jordan, 2003) is a probabilistic generative model built on bag-of-words counts. It assumes each document was "written" by a random process:

  1. each topic is a probability distribution over the vocabulary (topic "sports": high probability for game, team, score...);
  2. each document is a mixture of topics (70% sports, 30% finance);
  3. every word in a document is generated by first sampling a topic from the document's mixture, then sampling a word from that topic.

Fitting LDA inverts the process: given only the observed words, infer the topic–word distributions and the document–topic mixtures.

from sklearn.feature_extraction.text import CountVectorizer
from sklearn.decomposition import LatentDirichletAllocation

X = CountVectorizer(max_df=0.9, min_df=5, stop_words='english').fit_transform(docs)
lda = LatentDirichletAllocation(n_components=10, random_state=0).fit(X)

LDA served the field for two decades, but it inherits every limitation of bag-of-words:

  • word order and context are ignored; synonyms are unrelated dimensions;
  • the number of topics \(k\) must be fixed in advance;
  • short texts (tweets, ticket titles) give very sparse counts β€” LDA struggles;
  • topics are often hard to interpret without heavy preprocessing (stop-word lists, stemming, tuning).

BERTopic: topic modeling on embeddings

BERTopic (Grootendorst, 2022) replaces the generative story with a geometric one: embed documents so that semantic similarity is spatial proximity, then find the dense regions. The pipeline is a composition of the last three lessons:

flowchart LR
    A[Documents] --> B["1. Sentence embeddings<br><small>SBERT β€” Text Representation</small>"]
    B --> C["2. Reduce dimensions<br><small>UMAP β€” Dimensionality Reduction</small>"]
    C --> D["3. Cluster<br><small>HDBSCAN β€” Clustering</small>"]
    D --> E["4. Describe topics<br><small>c-TF-IDF β€” this lesson</small>"]

Step 1 β€” Embed. Each document becomes a dense vector via a sentence-transformer. Documents about refunds and money back land close together even with disjoint vocabulary β€” the decisive advantage over LDA.

Step 2 β€” Reduce. Embeddings have 384–768 dimensions; density-based clustering suffers there (the curse of dimensionality). UMAP compresses to ~5 dimensions while preserving neighborhood structure.

Step 3 β€” Cluster. HDBSCAN finds clusters of varying shape and density β€” and crucially, it does not force every document into a topic: documents that fit nowhere become outliers (topic βˆ’1) instead of polluting real topics. The number of topics emerges from the data; you never set \(k\).

Step 4 β€” Describe. Each cluster needs a human-readable label. BERTopic concatenates all documents of a cluster into one pseudo-document and applies class-based TF-IDF:

\[ \text{c-TF-IDF}(t, c) = \underbrace{\text{tf}(t, c)}_{\text{freq. of } t \text{ in class } c} \times \log\!\Big(1 + \frac{A}{\text{tf}(t)}\Big) \]

where \(A\) is the average number of words per class and \(\text{tf}(t)\) is the frequency of \(t\) across all classes. It is the TF-IDF you know applied at the cluster level: words frequent in this topic but rare in others rank highest β€” those become the topic's keywords.

Why this design is worth studying

BERTopic is a case study in composition: four classical components, each replaceable (swap SBERT for any embedder, HDBSCAN for k-means, c-TF-IDF for another labeler), assembled into a state-of-the-art system. Understanding the parts β€” which you now do β€” means you can tune, debug, and extend the whole.

A basic worked example

BERTopic is not in this site's build environment (embedding models are heavyweight), so run this locally or in Colab β€” a companion notebook is provided below.

# pip install bertopic
from bertopic import BERTopic
from sklearn.datasets import fetch_20newsgroups

docs = fetch_20newsgroups(subset='all', remove=('headers', 'footers', 'quotes')).data

topic_model = BERTopic(language='english', verbose=True)
topics, probs = topic_model.fit_transform(docs)

topic_model.get_topic_info().head(10)

Typical output β€” topics discovered with no supervision, no preset \(k\):

Topic  Count  Name
-1     6789   -1_the_of_to_and          ← outliers (no topic)
 0      589   0_game_team_hockey_play
 1      541   1_god_jesus_bible_faith
 2      480   2_car_engine_dealer_miles
 3      432   3_key_encryption_chip_clipper
 ...

Inspecting and using the model:

topic_model.get_topic(0)                     # top c-TF-IDF words of topic 0
topic_model.find_topics("space exploration") # search topics semantically
topic_model.transform(["My car needs new brakes"])  # assign topics to new docs

# Built-in interactive visualizations (Plotly)
topic_model.visualize_topics()      # inter-topic distance map
topic_model.visualize_barchart()    # top words per topic
topic_model.visualize_heatmap()     # topic similarity matrix

Practical tips

  • Reduce outliers: a large topic βˆ’1 is normal; topic_model.reduce_outliers(docs, topics) reassigns them to the nearest topic if desired.
  • Control topic granularity with HDBSCAN's min_cluster_size (via min_topic_size): larger β†’ fewer, broader topics. Or merge after fitting: topic_model.reduce_topics(docs, nr_topics=20).
  • Better keywords: pass a CountVectorizer(stop_words='english', ngram_range=(1,2)) to improve c-TF-IDF labels without touching the clustering.
  • Reproducibility: UMAP is stochastic β€” set umap_model=UMAP(random_state=42) for repeatable topics.
  • Portuguese / multilingual corpora: BERTopic(language='multilingual') selects a multilingual sentence-transformer β€” it works well on Brazilian Portuguese text.

LDA vs BERTopic

LDA (2003) BERTopic (2022)
Representation bag-of-words counts contextual sentence embeddings
Synonyms/context invisible captured by the embedder
Number of topics fixed in advance (k) emerges from density (HDBSCAN)
Outliers forced into topics explicit topic βˆ’1
Short texts weak (sparse counts) strong
Interpretability topic–word probabilities c-TF-IDF keywords + visualizations
Cost cheap, CPU embedding step needs a model (GPU helps)

Companion notebook

Download the notebook and run it in Colab or locally (pip install bertopic):

bertopic_example.ipynb


Quiz