Skip to content

Topic Distribution Learning

While in most scenarios you can store an entire document-topic matrix in memory, this is not always the case, especially with extremely large datasets. Distribution learners in Turftopic are exactly developed for this reason.

With a distribution learner, you can pass document-topic matrices per batch, and update its parameters, while slowly learning the true distribution of topics in the dataset with uncertainty.

To use distribution learners you should install conjugate-models:

pip install turftopic[conjugate]

Example

import numpy as np
import pandas as pd

from sklearn.datasets import fetch_20newsgroups
from turftopic import SensTopic
from turftopic.distribution_learning import GaussianDistributionLearner

ds = fetch_20newsgroups(remove=("headers", "footers", "quotes"), subset="all")
corpus = ds.data

batch_size = 2000
model = SensTopic(random_state=42)
# Initializing the distribution learner
distribution_learner = GaussianDistributionLearner()
# batch fitting over the dataset
for batch_start in range(0, len(corpus), batch_size):
    batch_end = batch_start + batch_size
    # Calculating doc_topic_matrix for current batch
    batch_doc_topic_matrix = model.partial_fit_transform(
        corpus[batch_start:batch_end],
        merge_method="asymmetric_mean",
    )
    # Updating the posteriors
    distribution_learner.update(batch_doc_topic_matrix)

# `pip install plotly` if you want to plot
distribution_learner.plot_topic_distribution(model.topic_names)
Topic distribution learned by the GaussianDistributionLearner.

API Reference

turftopic.distribution_learning.GaussianDistributionLearner

Learns posterior distribution of the mean and variance of a bunch of (Non-multivariate) Gaussian distributions using Bayesian updating.

This is very useful for when you cannot keep a dataset in memory and want to learn the importance of topics in the dataset from batches with uncertainty.

Source code in turftopic/distribution_learning.py
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
class GaussianDistributionLearner:
    """Learns posterior distribution of the mean and variance of a bunch of
    (Non-multivariate) Gaussian distributions using Bayesian updating.

    This is very useful for when you cannot keep a dataset in memory and
    want to learn the importance of topics in the dataset from batches with uncertainty.
    """

    def __init__(self, mu=0, alpha=1, beta=1, nu=1):
        self.mu = mu
        self.alpha = alpha
        self.beta = beta
        self.nu = nu
        self.posteriors = []

    def init_prior(self):
        return NormalInverseGamma(
            mu=self.mu, alpha=self.alpha, beta=self.beta, nu=self.nu
        )

    def update(self, batch_doc_topic):
        """Updates posterior distributions based on the incoming batch."""
        for i_topic, dt in enumerate(batch_doc_topic.T):
            if i_topic >= len(self.posteriors):
                self.posteriors.append(self.init_prior())
            prior = self.posteriors[i_topic]
            posterior = normal(
                x_total=np.sum(dt),
                x2_total=np.sum(np.square(dt)),
                n=dt.shape[0],
                prior=prior,
            )
            self.posteriors[i_topic] = posterior

    def sample_means(self, n_datapoints=100, random_state=None):
        """Samples means from each of the learned posteriors.

        Parameters
        ----------
        n_datapoints: int, default 100
            Number of datapoints to sample from the posterior.
        random_state: int or None, default None
            Random seed to use for sampling.

        Returns
        -------
        ndarray of shape (n_topics, n_datapoints)
            Posterior samples for each topic.
        """
        out = []
        for posterior in self.posteriors:
            out.append(posterior.sample_mean(size=n_datapoints))
        return np.stack(out)

    def plot_topic_distribution(
        self, topic_names: list[str] | None = None, sort_topics=True
    ):
        try:
            import plotly.graph_objects as go
            import plotly.express as px
        except (ImportError, ModuleNotFoundError) as e:
            raise ModuleNotFoundError(
                "Please install plotly if you intend to use plots in Turftopic."
            ) from e
        fig = go.Figure()
        if topic_names is None:
            topic_names = [f"Topic {i}" for i in range(len(self.posteriors))]
        if len(topic_names) != len(self.posteriors):
            raise ValueError(
                "The number of posteriors learned by the distribution learner is not the same as the number of topic names given."
            )
        mus = self.sample_means()
        y = mus.mean(axis=1)
        se = np.std(mus, axis=1)
        topic_colors = list(
            itertools.islice(
                itertools.cycle(px.colors.qualitative.Dark24),
                len(self.posteriors),
            )
        )
        if sort_topics:
            order = np.argsort(y)
        else:
            order = np.arange(len(self.posteriors))
        for i_topic in order:
            fig.add_bar(
                y0=topic_names[i_topic],
                x=[y[i_topic]],
                error_x=dict(
                    type="data",
                    array=[se[i_topic]],
                    visible=True,
                ),
                showlegend=False,
                name=topic_names[i_topic],
                marker=dict(
                    line=dict(color=topic_colors[i_topic], width=2),
                    color="white",
                ),
            )
        fig.update_layout(
            template="plotly_white",
            font=dict(family="Roboto Mono", color="black", size=10),
        )
        return fig

sample_means(n_datapoints=100, random_state=None)

Samples means from each of the learned posteriors.

Parameters:

Name Type Description Default
n_datapoints

Number of datapoints to sample from the posterior.

100
random_state

Random seed to use for sampling.

None

Returns:

Type Description
ndarray of shape (n_topics, n_datapoints)

Posterior samples for each topic.

Source code in turftopic/distribution_learning.py
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
def sample_means(self, n_datapoints=100, random_state=None):
    """Samples means from each of the learned posteriors.

    Parameters
    ----------
    n_datapoints: int, default 100
        Number of datapoints to sample from the posterior.
    random_state: int or None, default None
        Random seed to use for sampling.

    Returns
    -------
    ndarray of shape (n_topics, n_datapoints)
        Posterior samples for each topic.
    """
    out = []
    for posterior in self.posteriors:
        out.append(posterior.sample_mean(size=n_datapoints))
    return np.stack(out)

update(batch_doc_topic)

Updates posterior distributions based on the incoming batch.

Source code in turftopic/distribution_learning.py
27
28
29
30
31
32
33
34
35
36
37
38
39
def update(self, batch_doc_topic):
    """Updates posterior distributions based on the incoming batch."""
    for i_topic, dt in enumerate(batch_doc_topic.T):
        if i_topic >= len(self.posteriors):
            self.posteriors.append(self.init_prior())
        prior = self.posteriors[i_topic]
        posterior = normal(
            x_total=np.sum(dt),
            x2_total=np.sum(np.square(dt)),
            n=dt.shape[0],
            prior=prior,
        )
        self.posteriors[i_topic] = posterior