← Back to list

Step-by-Step Guide to Implementing CTM in Python (or R)

If you’ve ever played around with topic modeling, chances are you’ve heard of LDA (Latent Dirichlet Allocation) — the classic algorithm…

Ujang Riswanto · 2025-11-10 00:02 · 0 claps · 12.3 min read
#step-by-step-guide #ctm #correlated-topic-model #topic-modeling #python-programming
Open on Medium ↗
Wiki topics: 💻 · Programming

Step-by-Step Guide to Implementing CTM in Python (or R)

Photo by Sigmund on Unsplash

Photo by Sigmund on Unsplash

If you’ve ever played around with topic modeling, chances are you’ve heard of LDA (Latent Dirichlet Allocation) — the classic algorithm that tries to figure out what topics are hiding in your documents. LDA is great… but it has one big limitation: it assumes that topics are independent of each other.

But real life doesn’t work that way, right? Imagine you’re analyzing news articles. If an article is about climate change, there’s a good chance it also touches on politics, energy policy, or even economics. These topics are connected. LDA can’t capture that relationship — but CTM (Correlated Topic Model) can.

So, what exactly is CTM? Think of CTM as LDA’s smarter cousin. It does the same basic thing (discovering hidden topics in a bunch of documents) but with an upgrade: it can learn which topics tend to appear together. That means it gives you more realistic insights about your text data.

Here’s why you might care about CTM:

  • Better insights: It doesn’t just spit out unrelated topics — it shows how they’re connected.
  • More accurate results: Especially useful if your data naturally has overlapping themes (like research papers, product reviews, or social media posts).
  • Richer storytelling: You can understand not just what people are talking about, but also how those conversations are connected.

In this guide, we’ll break CTM down step by step and show you how to implement it in Python (or R if that’s your vibe). By the end, you’ll have a working model that can uncover meaningful patterns in any collection of text.🚀

Prerequisites

Photo by Mohammad Rahmani on Unsplash

Photo by Mohammad Rahmani on Unsplash

Before we jump into code, let’s make sure you’ve got everything ready. Don’t worry — you don’t need a PhD in statistics to follow along, but a little background helps. Here’s your quick prep list:

🧠 What You Should Know

  • Basic Python or R: If you’ve ever run a Jupyter Notebook or an R script, you’re good.
  • Topic Modeling 101: Just a general idea of what topic modeling is (finding hidden themes in text). If you’ve used LDA before, even better!
  • Basic Data Cleaning: Things like removing stopwords, tokenizing text, and turning documents into a bag-of-words representation.

🛠️ Tools You’ll Need

For Python fans:

pip install contextualized-topic-models torch sklearn pandas

For R fans:

install.packages(c("topicmodels", "textmineR", "tidyverse"))

These libraries will handle everything from data preprocessing to training the CTM model.

📂 A Dataset to Play With

You can use:

  • The famous 20 Newsgroups dataset (available in sklearn.datasets)
  • A collection of tweets, reviews, or articles you scrape yourself
  • Any set of documents that has at least a few dozen text samples

Once you’ve got these basics ready, you’ll be all set to dive into the fun part: actually training a CTM model and discovering how topics in your data are connected.

Understanding the CTM Architecture

Photo by Kaleidico on Unsplash

Photo by Kaleidico on Unsplash

Okay, now that we’re ready to roll, let’s take a peek under the hood and see what makes CTM different from its older sibling, LDA.

🧩 The Big Idea

Both LDA and CTM try to do the same thing:

  • Take a bunch of documents
  • Discover hidden topics
  • Show which topics belong to which documents

But here’s where CTM gets clever — it uses a logistic normal distribution instead of a Dirichlet distribution.

If that sounds scary, think of it this way:

  • LDA is like putting each topic in its own little box, not talking to each other.
  • CTM is like drawing a mind map where topics are connected with lines. If “Climate Change” and “Energy Policy” are often found together, CTM notices that connection and reflects it in the model.

🔑 Why This Matters

  • More Realistic Results: Real-world conversations overlap — CTM captures those overlaps.
  • Better Clustering: You get richer topic groups and more meaningful document assignments.
  • Useful Insights: You can actually explore relationships between topics (not just the topics themselves).

🖼️ (Imagine This…)

Picture a Venn diagram:

  • Each circle is a topic.
  • LDA shows you circles that don’t overlap.
  • CTM shows you the overlaps — where two or more topics commonly appear together.

This means your analysis moves from just “what topics exist?” to “how do these topics relate to each other?”

Dataset Preparation

Photo by Mika Baumeister on Unsplash

Photo by Mika Baumeister on Unsplash

Alright, time to give CTM something to chew on. Before we train a model, we need to clean up our text data and turn it into a format CTM can understand.

🧹 Step 1: Clean the Text

Whether you’re working with news articles, tweets, or product reviews, you usually want to:

  • Lowercase everything ("The""the")
  • Remove stopwords ("and", "the", "of", etc.)
  • Get rid of weird symbols or HTML tags
  • (Optional) Lemmatize or stem words (turn "running""run")

🐍 Python Example

import pandas as pd
from sklearn.datasets import fetch_20newsgroups
from sklearn.feature_extraction.text import CountVectorizer
import re

# Load sample dataset
newsgroups = fetch_20newsgroups(subset='train')
docs = newsgroups.data

# Simple cleaning function
def clean_text(text):
    text = text.lower()
    text = re.sub(r'[^a-z\s]', '', text)  # remove numbers/punctuation
    return text

cleaned_docs = [clean_text(doc) for doc in docs]

# Create Bag-of-Words (BoW) representation
vectorizer = CountVectorizer(stop_words='english', max_features=5000)
bow_matrix = vectorizer.fit_transform(cleaned_docs)

print("Shape of BoW Matrix:", bow_matrix.shape)

This gives you a document-term matrix (DTM) — basically a big table where each row is a document and each column is a word.

📊 R Example

library(tidyverse)
library(tidytext)

# Example dataframe
docs <- data.frame(text = c("Climate change is real!",
                            "Energy policy affects the economy.",
                            "Politics and environment are connected."))

# Clean and tokenize
cleaned_docs <- docs %>%
  mutate(text = str_to_lower(text)) %>%
  unnest_tokens(word, text) %>%
  anti_join(stop_words)

# Create document-term matrix
dtm <- cleaned_docs %>%
  count(row_number(), word) %>%
  cast_dtm(row_number, word, n)

dtm

Now you have a clean, numeric representation of your text — exactly what CTM needs to find hidden topics.

💡 Pro Tip: Contextual Embeddings

If you want supercharged results, you can also generate embeddings (e.g., using BERT or SentenceTransformers) to capture semantic meaning beyond just raw word counts. Some Python CTM libraries even support this out of the box.

Implementing CTM in Python

Photo by Safar Safarov on Unsplash

Photo by Safar Safarov on Unsplash

Now for the fun part — actually training a Correlated Topic Model! We’ll use the contextualized-topic-models library in Python, which makes CTM surprisingly easy to work with.

🛠️ Step 1: Install the Library

Run this in your terminal:

pip install contextualized-topic-models torch sklearn pandas

This installs the CTM library, PyTorch (needed under the hood), and some helpers for data prep.

🐍 Step 2: Prepare the Data

If you followed Section #4, you already have a Bag-of-Words (BoW) matrix and cleaned text. Let’s transform them into the format CTM needs:

from contextualized_topic_models.utils.preprocessing import WhiteSpacePreprocessing

# Example small corpus
documents = [
    "Climate change is affecting weather patterns",
    "Energy policy impacts economy and climate",
    "Politics and environment are deeply connected",
    "Renewable energy is the future",
]

# Preprocess
sp = WhiteSpacePreprocessing(documents)
preprocessed_documents, unpreprocessed_corpus, vocab = sp.preprocess()

🏋️ Step 3: Train the CTM Model

from contextualized_topic_models.models.ctm import CombinedTM
from contextualized_topic_models.utils.data_preparation import TopicModelDataPreparation

tp = TopicModelDataPreparation()

training_dataset = tp.fit(text_for_contextual=unpreprocessed_corpus,
                          text_for_bow=preprocessed_documents)

ctm = CombinedTM(bow_size=len(vocab),
                 contextual_size=768,  # for BERT embeddings
                 n_components=5,       # number of topics
                 num_epochs=20)

ctm.fit(training_dataset)  # Train the model

Here’s what’s happening:

  • bow_size: number of unique words in your vocabulary
  • contextual_size: size of embeddings (768 for BERT)
  • n_components: number of topics you want CTM to find
  • num_epochs: how many passes over the dataset (20 is a good start)

🔎 Step 4: Inspect the Topics

topics = ctm.get_topic_lists(5)  # top 5 words per topic
for idx, topic in enumerate(topics):
    print(f"Topic {idx+1}: {topic}")

You’ll see something like:

Topic 1: ['climate', 'energy', 'policy', 'economy', 'future']
Topic 2: ['politics', 'environment', 'connected', 'impacts', 'change']
...

Boom 💥 — CTM just uncovered the dominant topics and their most important words.

🎨 Step 5: (Optional) Visualize the Topics

You can visualize results with word clouds or bar plots for extra flair:

from wordcloud import WordCloud
import matplotlib.pyplot as plt

for idx, topic in enumerate(topics):
    text = " ".join(topic)
    wc = WordCloud(width=400, height=200).generate(text)
    plt.figure()
    plt.imshow(wc, interpolation="bilinear")
    plt.axis("off")
    plt.title(f"Topic {idx+1}")
    plt.show()

And that’s it — you’ve got a working CTM model in Python! 🎉 From here, you can use the model to assign topics to new documents or explore how topics are correlated.

Implementing CTM in R

Photo by Christopher Gower on Unsplash

Photo by Christopher Gower on Unsplash

If you prefer the R ecosystem, good news: you can train a Correlated Topic Model (CTM) with the tried-and-true {topicmodels} package. We’ll keep things simple: build a document-term matrix (DTM), fit CTM, then inspect and visualize the results.

Install & Load Libraries

install.packages(c("topicmodels", "tidyverse", "tidytext", "tm"))
# If you like, quanteda also works for DTM creation:
# install.packages("quanteda")

library(topicmodels)
library(tidyverse)
library(tidytext)
library(tm)

Prepare a Document–Term Matrix (DTM)

You can use your own corpus (CSV, JSON, etc.). Below is a minimal example that cleans text and builds a DTM with {tm}. (Feel free to swap {tm} with {quanteda} if that’s your usual toolbox.)

texts <- c(
  "Climate change is affecting weather patterns.",
  "Energy policy impacts the economy and the climate.",
  "Politics and environment are connected.",
  "Renewable energy is the future of our planet."
)

# Build a corpus
corp <- VCorpus(VectorSource(texts))

# Basic cleaning
corp <- corp %>%
  tm_map(content_transformer(tolower)) %>%
  tm_map(removePunctuation) %>%
  tm_map(removeNumbers) %>%
  tm_map(removeWords, stopwords("en")) %>%
  tm_map(stripWhitespace)

# Create DTM (you can add bounds to trim very rare/common terms)
dtm <- DocumentTermMatrix(corp,
                          control = list(wordLengths = c(3, Inf)))
# Optional: sparsity trimming to keep things tidy
dtm <- removeSparseTerms(dtm, 0.99)

dim(dtm)

Tip: For real projects, consider additional steps: custom stopword lists, stemming/lemmatization, and pruning ultra-rare terms for stability.

Train the CTM Model

topicmodels::CTM() fits a correlated topic model using a logistic-normal prior. Set the number of topics (k) and a few control options. Start small (e.g., 5–10 topics) and adjust later.

set.seed(42)

k <- 5  # number of topics

ctm_model <- CTM(
  dtm,
  k = k,
  control = list(
    seed = 42,
    # Variational parameters (tweak as needed)
    var = list(tol = 1e-6, iter.max = 100),
    em  = list(tol = 1e-4, iter.max = 100),
    initialize = "random"  # or "kmeans"
  )
)

nspect Topics & Document Assignments

Top words per topic

top_terms <- terms(ctm_model, 10)  # top 10 words per topic
top_terms

Topic distribution for each document

post <- posterior(ctm_model)
doc_topic <- post$topics   # rows = docs, cols = topics; rows sum to ~1

round(doc_topic, 3)

Quick Visualization (Bar Plot of a Topic)

Let’s turn one topic’s top words into a simple chart with {ggplot2}.

library(ggplot2)

# Convert 'terms()' matrix into a tidy tibble for one topic
topic_id <- 1
topic_words <- top_terms[, topic_id]

# Get per-word probabilities (beta) from the posterior
beta <- post$terms[, topic_id]  # probability of word given topic

viz_df <- tibble(
  term = names(beta),
  beta = as.numeric(beta)
) %>%
  filter(term %in% topic_words) %>%
  arrange(desc(beta)) %>%
  slice_head(n = 10)

ggplot(viz_df, aes(x = reorder(term, beta), y = beta)) +
  geom_col() +
  coord_flip() +
  labs(
    title = paste("Top Words for Topic", topic_id),
    x = NULL, y = "P(term | topic)"
  )

Want interactive visuals? Try {LDAvis} (works with LDA out-of-the-box; for CTM you’ll need a small adapter), or export tables with {DT} for easy browsing.

Practical Tips

  • Choosing k (number of topics): Try several values (e.g., 5, 10, 15) and compare interpretability and metrics (see next section on evaluation).
  • Stability matters: Set a seed for reproducibility and keep preprocessing consistent.
  • Speed & memory: Large corpora with many terms can be heavy. Prune vocabulary and consider batching your corpus.

That’s it — you’ve trained CTM in R! 🎉

Model Evaluation

Photo by Sigmund on Unsplash

Photo by Sigmund on Unsplash

Okay, you’ve trained your CTM — but how do you know if it’s good? We don’t just want pretty word clouds; we want topics that actually make sense. Here’s how to measure and improve your model’s quality.

🎯 What We Care About

  • Topic Coherence: Do the top words in a topic actually go together in a meaningful way?
  • Perplexity: A measure of how well the model predicts unseen data (lower is better).
  • Human Interpretability: Honestly, the simplest check: do the topics make sense to you or your domain experts?

🐍 Python: Evaluating CTM

1. Coherence Score

The contextualized-topic-models library has built-in helpers to compute coherence.

from contextualized_topic_models.evaluation.measures import CoherenceNPMI

# Evaluate coherence
coherence = CoherenceNPMI(ctm.get_topic_word_matrix(),
                          tp.bow_embeddings,
                          tp.id2token)

print("Coherence (NPMI):", coherence.score())

A higher NPMI score means your topics are more semantically coherent.

2. Perplexity (Optional)

Some libraries expose perplexity for CTM, but often coherence + human review are enough. If you care, you can measure held-out likelihood using a split dataset.

3. Human Sanity Check

Print top words and skim through them:

for idx, topic in enumerate(ctm.get_topic_lists(10)):
    print(f"Topic {idx+1}: {', '.join(topic)}")

If they read like random word salad, it might be time to tweak preprocessing or number of topics.

📊 R: Evaluating CTM

1. Log-Likelihood & Perplexity

The {topicmodels} package makes this super easy:

logLik(ctm_model)      # Log-likelihood
perplexity(ctm_model)  # Perplexity (lower = better)

Compare across different k values (e.g., 5, 10, 15 topics) and choose the one with the lowest perplexity and most interpretable topics.

2. Topic Coherence (Custom)

You can roll your own simple coherence metric by counting how often top words co-occur in the same documents. Packages like {textmineR} also have built-in CalcProbCoherence():

library(textmineR)

phi <- as.matrix(posterior(ctm_model)$terms)  # word-topic matrix
coherence <- CalcProbCoherence(phi = phi, dtm = dtm, M = 10)

mean(coherence)

This gives you an average coherence score across all topics.

🔧 Tips for Improving Results

  • Tune Number of Topics: Too few → overly broad topics. Too many → redundant or noisy topics.
  • Better Preprocessing: Remove irrelevant words, add domain-specific stopwords, and lemmatize.
  • Play With Initialization: Random vs. k-means can lead to different topic quality.
  • Use Contextual Embeddings: In Python, combining BoW with embeddings often leads to much cleaner topics.

With a good evaluation process, you can iterate until your CTM produces meaningful, human-readable topics that reveal real insights from your data.

Advanced Tips

Photo by ThisisEngineering on Unsplash

Photo by ThisisEngineering on Unsplash

Once you’ve got CTM up and running, you can take it to the next level. Here’s how to make your model faster, smarter, and more useful in real-world projects.

🧠 Use Contextual Embeddings (Game-Changer!)

Plain bag-of-words is good, but embeddings are better. By combining BoW with embeddings (like BERT or SentenceTransformers), CTM can capture semantic meaning — not just raw word counts.

Python Example: Using BERT Embeddings

from contextualized_topic_models.utils.preprocessing import WhiteSpacePreprocessing
from contextualized_topic_models.utils.data_preparation import TopicModelDataPreparation
from sentence_transformers import SentenceTransformer

# Load pre-trained sentence transformer
bert = SentenceTransformer("all-MiniLM-L6-v2")

# Get embeddings
embeddings = bert.encode(unpreprocessed_corpus)

tp = TopicModelDataPreparation()
training_dataset = tp.fit(
    text_for_contextual=unpreprocessed_corpus,
    text_for_bow=preprocessed_documents,
    contextual_embeddings=embeddings
)

🔥 Result: Topics become more human-readable and less noisy.

⚡ Speed Up Training

  • Use GPU: If you have a CUDA-compatible GPU, PyTorch will train much faster.
  • Trim Vocabulary: Remove extremely rare terms to make BoW smaller.
  • Mini-Batching: For very large datasets, split training data into batches.

🎛️ Hyperparameter Tuning

A few knobs to turn for better results:

  • **n_components (number of topics):** Try a range (e.g., 5, 10, 15, 20) and pick what makes sense for your domain.
  • **num_epochs:** If topics look messy, increase training epochs (but watch out for overfitting).
  • Regularization Strength: Some CTM libraries let you control topic sparsity — tweak this for crisper topics.

🔍 Post-Processing Tricks

  • Merge Overlapping Topics: If two topics look almost identical, you can combine them manually.
  • Label Topics Automatically: Use keyword matching or embeddings to assign human-readable names (e.g., “Climate Policy” instead of Topic 3).
  • Filter Out Junk Topics: Sometimes you’ll get a “miscellaneous” topic with random words — drop it if it doesn’t add value.

💡 Real-World Use Cases

  • Customer Feedback Analysis: Group related complaints/features together.
  • Research Papers: Find emerging themes and how they relate.
  • News Monitoring: Track topic trends and overlaps across different outlets.

By adding these enhancements, you’ll move from “just running a model” to building a serious, production-ready topic analysis pipeline.

Common Pitfalls & Troubleshooting

Photo by Danial Igdery on Unsplash

Photo by Danial Igdery on Unsplash

Even with a great library, CTM can sometimes misbehave. Here are the most common issues and how to fix them:

🥴 Pitfall 1: Topics Don’t Make Sense

  • Symptom: Top words look random or too similar across topics.
  • Fix:
  • Add more cleaning (remove domain-specific stopwords).
  • Try fewer/more topics (n_components).
  • Use contextual embeddings for better semantic clustering.

🥱 Pitfall 2: Model is Too Slow

  • Symptom: Training takes forever on a large corpus.
  • Fix:
  • Trim vocabulary (drop super-rare words).
  • Use GPU acceleration if available.
  • Reduce number of epochs (start small, scale up only if needed).

🌀 Pitfall 3: Overlapping or Redundant Topics

  • Symptom: Two or more topics look almost identical.
  • Fix:
  • Reduce number of topics.
  • Post-process and merge similar topics manually.
  • Check that your corpus isn’t too narrow (may not have enough variation).

❌ Pitfall 4: Convergence Warnings (in R)

  • Symptom: CTM doesn’t converge or throws warnings.
  • Fix:
  • Increase iter.max in CTM() control settings.
  • Try initialize = "kmeans" instead of random.
  • Remove extremely sparse terms to stabilize training.

Conclusion

Congrats — you’ve just learned how to build a Correlated Topic Model (CTM) from scratch in Python and R! 🎉

Here’s what we covered:

  • What CTM is and why it’s an upgrade over LDA
  • How to clean and prepare your dataset
  • Step-by-step implementation in Python (and R!)
  • How to evaluate, tune, and visualize your results
  • Advanced tips to make your topics more meaningful

CTM is powerful because it doesn’t just tell you what topics exist — it shows you how they relate to each other. That’s huge for storytelling, research, and data-driven decision-making.

So go ahead: grab a dataset, run CTM, and see what hidden patterns you can uncover. And remember — the best models are the ones that not only crunch numbers but actually make sense to humans.👨‍💻


메타데이터
post_id
d40ce8949155
slug
step-by-step-guide-to-implementing-ctm-in-python-or-r-d40ce8949155
url
https://medium.com/@ujangriswanto08/step-by-step-guide-to-implementing-ctm-in-python-or-r-d40ce8949155
canonical_url
https://medium.com/@ujangriswanto08/step-by-step-guide-to-implementing-ctm-in-python-or-r-d40ce8949155
author_url
https://medium.com/@ujangriswanto08
status
ok
fetched_at
2026-06-20 20:29:01