Skip to content

Representations

BaseRepresentation

Bases: BaseEstimator

The base representation model for fine-tuning topic representations.

Source code in bertopic\representation\_base.py
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
class BaseRepresentation(BaseEstimator):
    """The base representation model for fine-tuning topic representations."""

    def extract_topics(
        self,
        topic_model: "BERTopic",
        corpus: Corpus,
        topic_representations: dict[int, Keywords],
        c_tf_idf: csr_matrix,
        embeddings: np.ndarray | None = None,
    ) -> dict[int, TopicRepresentation]:
        """Extract topics.

        Each representation model that inherits this class will have
        its arguments automatically passed. Therefore, the representation
        model will only have access to the information about topics
        related to those arguments.

        Arguments:
            topic_model: The BERTopic model that is fitted until topic
                         representations are calculated.
            corpus: Documents and their metadata.
            topic_representations: A dictionary mapping topic IDs to their
                                   Keywords representations as calculated by c-TF-IDF.
            c_tf_idf: A c-TF-IDF representation that is typically
                      identical to `topic_model.c_tf_idf_` except for
                      dynamic, class-based, and hierarchical topic modeling
                      where it is calculated on a subset of the documents.
            embeddings: Pre-trained document embeddings.
        """
        return topic_model.topic_representations_

extract_topics(topic_model, corpus, topic_representations, c_tf_idf, embeddings=None)

Extract topics.

Each representation model that inherits this class will have its arguments automatically passed. Therefore, the representation model will only have access to the information about topics related to those arguments.

Parameters:

Name Type Description Default
topic_model BERTopic

The BERTopic model that is fitted until topic representations are calculated.

required
corpus Corpus

Documents and their metadata.

required
topic_representations dict[int, Keywords]

A dictionary mapping topic IDs to their Keywords representations as calculated by c-TF-IDF.

required
c_tf_idf csr_matrix

A c-TF-IDF representation that is typically identical to topic_model.c_tf_idf_ except for dynamic, class-based, and hierarchical topic modeling where it is calculated on a subset of the documents.

required
embeddings ndarray | None

Pre-trained document embeddings.

None
Source code in bertopic\representation\_base.py
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
def extract_topics(
    self,
    topic_model: "BERTopic",
    corpus: Corpus,
    topic_representations: dict[int, Keywords],
    c_tf_idf: csr_matrix,
    embeddings: np.ndarray | None = None,
) -> dict[int, TopicRepresentation]:
    """Extract topics.

    Each representation model that inherits this class will have
    its arguments automatically passed. Therefore, the representation
    model will only have access to the information about topics
    related to those arguments.

    Arguments:
        topic_model: The BERTopic model that is fitted until topic
                     representations are calculated.
        corpus: Documents and their metadata.
        topic_representations: A dictionary mapping topic IDs to their
                               Keywords representations as calculated by c-TF-IDF.
        c_tf_idf: A c-TF-IDF representation that is typically
                  identical to `topic_model.c_tf_idf_` except for
                  dynamic, class-based, and hierarchical topic modeling
                  where it is calculated on a subset of the documents.
        embeddings: Pre-trained document embeddings.
    """
    return topic_model.topic_representations_

Cohere

Bases: LLMRepresentation

Use the Cohere API to generate topic labels based on their generative model.

Find more about their models here: https://docs.cohere.ai/docs

Parameters:

Name Type Description Default
client

A cohere.ClientV2

required
model str

Model to use within Cohere, defaults to "command-r7b-12-2024".

'command-r7b-12-2024'
prompt str | None

The prompt to be used in the model. If no prompt is given, bertopic.representation._prompts.DEFAULT_CHAT_PROMPT is used instead. NOTE: Use "[KEYWORDS]" and "[DOCUMENTS]" in the prompt to decide where the keywords and documents need to be inserted.

None
system_prompt str | None

The system prompt to be used in the model. If no system prompt is given, bertopic.representation._prompts.DEFAULT_SYSTEM_PROMPT is used instead.

None
json_schema dict[str, Any] | bool

A dictionary representing the JSON schema to enforce structured output. If set to True, a default schema will be used (bertopic.representation._prompts.DEFAULT_JSON_SCHEMA). Uses Cohere's response_format with json_object type.

False
generator_kwargs dict[str, Any] | None

Kwargs passed to cohere.ClientV2.chat for fine-tuning the output.

None
delay_in_seconds float | None

The delay in seconds between consecutive prompts in order to prevent RateLimitErrors.

None
nr_docs int

The number of documents to pass to Cohere if a prompt with the ["DOCUMENTS"] tag is used.

4
diversity float | None

The diversity of documents to pass to Cohere. Accepts values between 0 and 1. A higher values results in passing more diverse documents whereas lower values passes more similar documents.

None
doc_length int | None

The maximum length of each document. If a document is longer, it will be truncated. If None, the entire document is passed.

None
tokenizer str | Callable | None

The tokenizer used to calculate to split the document into segments used to count the length of a document. * If tokenizer is 'char', then the document is split up into characters which are counted to adhere to doc_length * If tokenizer is 'whitespace', the document is split up into words separated by whitespaces. These words are counted and truncated depending on doc_length * If tokenizer is 'vectorizer', then the internal CountVectorizer is used to tokenize the document. These tokens are counted and truncated depending on doc_length * If tokenizer is a callable, then that callable is used to tokenize the document. These tokens are counted and truncated depending on doc_length

None

Usage:

To use this, you will need to install cohere first:

pip install cohere

Then, get yourself an API key and use Cohere's API as follows:

import cohere
from bertopic.representation import Cohere
from bertopic import BERTopic

# Create your representation model
co = cohere.ClientV2(my_api_key)
representation_model = Cohere(co)

# Use the representation model in BERTopic on top of the default pipeline
topic_model = BERTopic(representation_model=representation_model)

You can also use a custom prompt:

prompt = "I have the following documents: [DOCUMENTS]. What topic do they contain?"
representation_model = Cohere(co, prompt=prompt)

You can also use structured output with a JSON schema:

representation_model = Cohere(co, json_schema=True)
Source code in bertopic\representation\_cohere.py
 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
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
class Cohere(LLMRepresentation):
    """Use the Cohere API to generate topic labels based on their
    generative model.

    Find more about their models here:
    https://docs.cohere.ai/docs

    Arguments:
        client: A `cohere.ClientV2`
        model: Model to use within Cohere, defaults to `"command-r7b-12-2024"`.
        prompt: The prompt to be used in the model. If no prompt is given,
                `bertopic.representation._prompts.DEFAULT_CHAT_PROMPT` is used instead.
                NOTE: Use `"[KEYWORDS]"` and `"[DOCUMENTS]"` in the prompt
                to decide where the keywords and documents need to be
                inserted.
        system_prompt: The system prompt to be used in the model. If no system prompt is given,
                       `bertopic.representation._prompts.DEFAULT_SYSTEM_PROMPT` is used instead.
        json_schema: A dictionary representing the JSON schema to enforce structured output.
                     If set to True, a default schema will be used (`bertopic.representation._prompts.DEFAULT_JSON_SCHEMA`).
                     Uses Cohere's `response_format` with `json_object` type.
        generator_kwargs: Kwargs passed to `cohere.ClientV2.chat` for fine-tuning the output.
        delay_in_seconds: The delay in seconds between consecutive prompts
                                in order to prevent RateLimitErrors.
        nr_docs: The number of documents to pass to Cohere if a prompt
                 with the `["DOCUMENTS"]` tag is used.
        diversity: The diversity of documents to pass to Cohere.
                   Accepts values between 0 and 1. A higher
                   values results in passing more diverse documents
                   whereas lower values passes more similar documents.
        doc_length: The maximum length of each document. If a document is longer,
                    it will be truncated. If None, the entire document is passed.
        tokenizer: The tokenizer used to calculate to split the document into segments
                   used to count the length of a document.
                       * If tokenizer is 'char', then the document is split up
                         into characters which are counted to adhere to `doc_length`
                       * If tokenizer is 'whitespace', the document is split up
                         into words separated by whitespaces. These words are counted
                         and truncated depending on `doc_length`
                       * If tokenizer is 'vectorizer', then the internal CountVectorizer
                         is used to tokenize the document. These tokens are counted
                         and truncated depending on `doc_length`
                       * If tokenizer is a callable, then that callable is used to tokenize
                         the document. These tokens are counted and truncated depending
                         on `doc_length`

    Usage:

    To use this, you will need to install cohere first:

    `pip install cohere`

    Then, get yourself an API key and use Cohere's API as follows:

    ```python
    import cohere
    from bertopic.representation import Cohere
    from bertopic import BERTopic

    # Create your representation model
    co = cohere.ClientV2(my_api_key)
    representation_model = Cohere(co)

    # Use the representation model in BERTopic on top of the default pipeline
    topic_model = BERTopic(representation_model=representation_model)
    ```

    You can also use a custom prompt:

    ```python
    prompt = "I have the following documents: [DOCUMENTS]. What topic do they contain?"
    representation_model = Cohere(co, prompt=prompt)
    ```

    You can also use structured output with a JSON schema:

    ```python
    representation_model = Cohere(co, json_schema=True)
    ```
    """

    def __init__(
        self,
        client,
        model: str = "command-r7b-12-2024",
        prompt: str | None = None,
        system_prompt: str | None = None,
        delay_in_seconds: float | None = None,
        nr_docs: int = 4,
        diversity: float | None = None,
        doc_length: int | None = None,
        tokenizer: str | Callable | None = None,
        *,
        json_schema: dict[str, Any] | bool = False,
        generator_kwargs: dict[str, Any] | None = None,
    ):
        super().__init__(
            prompt=DEFAULT_JSON_PROMPT if json_schema else (prompt or DEFAULT_CHAT_PROMPT),
            nr_docs=nr_docs,
            diversity=diversity,
            doc_length=doc_length,
            tokenizer=tokenizer,
        )

        # Cohere specific parameters
        self.client = client
        self.model = model
        self.system_prompt = system_prompt if system_prompt is not None else DEFAULT_SYSTEM_PROMPT
        self.json_schema = DEFAULT_JSON_SCHEMA if json_schema is True else json_schema
        self.generator_kwargs = dict(generator_kwargs or {})
        self.delay_in_seconds = delay_in_seconds
        if self.json_schema:
            self.generator_kwargs["response_format"] = {"type": "json_object", "schema": self.json_schema}

    def _generate(self, prompt: str) -> str:
        """Send one prompt to Cohere's chat API."""
        if self.delay_in_seconds:
            time.sleep(self.delay_in_seconds)

        messages = [
            {"role": "system", "content": self.system_prompt},
            {"role": "user", "content": prompt},
        ]
        kwargs = {"model": self.model, "messages": messages, **self.generator_kwargs}
        return self.client.chat(**kwargs).message.content[0].text

KeyBERTInspired

Bases: BaseRepresentation

Source code in bertopic\representation\_keybert.py
 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
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
class KeyBERTInspired(BaseRepresentation):
    def __init__(
        self,
        top_n_words: int = 10,
        nr_repr_docs: int = 5,
        nr_samples: int = 500,
        nr_candidate_words: int = 100,
        random_state: int = 42,
    ):
        """Use a KeyBERT-like model to fine-tune the topic representations.

        The algorithm follows KeyBERT but does some optimization in
        order to speed up inference.

        The steps are as follows. First, we extract the top n representative
        documents per topic. To extract the representative documents, we
        randomly sample a number of candidate documents per cluster
        which is controlled by the `nr_samples` parameter. Then,
        the top n representative documents  are extracted by calculating
        the c-TF-IDF representation for the  candidate documents and finding,
        through cosine similarity, which are closest to the topic c-TF-IDF representation.
        Next, the top n words per topic are extracted based on their
        c-TF-IDF representation, which is controlled by the `nr_repr_docs`
        parameter.

        Then, we extract the embeddings for words and representative documents
        and create topic embeddings by averaging the representative documents.
        Finally, the most similar words to each topic are extracted by
        calculating the cosine similarity between word and topic embeddings.

        Arguments:
            top_n_words: The top n words to extract per topic.
            nr_repr_docs: The number of representative documents to extract per cluster.
            nr_samples: The number of candidate documents to extract per cluster.
            nr_candidate_words: The number of candidate words per cluster.
            random_state: The random state for randomly sampling candidate documents.

        Usage:

        ```python
        from bertopic.representation import KeyBERTInspired
        from bertopic import BERTopic

        # Create your representation model
        representation_model = KeyBERTInspired()

        # Use the representation model in BERTopic on top of the default pipeline
        topic_model = BERTopic(representation_model=representation_model)
        ```
        """
        self.top_n_words = top_n_words
        self.nr_repr_docs = nr_repr_docs
        self.nr_samples = nr_samples
        self.nr_candidate_words = nr_candidate_words
        self.random_state = random_state

    def extract_topics(
        self,
        topic_model: "BERTopic",
        corpus: Corpus,
        topic_representations: dict[int, Keywords],
        c_tf_idf: csr_matrix,
        embeddings: np.ndarray = None,
    ) -> dict[int, Keywords]:
        """Extract topics.

        Arguments:
            topic_model: A BERTopic model
            corpus: The input documents including (calculated) embeddings
            topic_representations: The candidate topic representations
            c_tf_idf: The topic c-TF-IDF representation
            embeddings: Pre-trained document embeddings (not used)

        Returns:
            updated_topics: Updated topic representations
        """
        # We extract the top n representative documents per class
        _, representative_docs, repr_doc_indices, _ = topic_model._extract_representative_docs(
            c_tf_idf=c_tf_idf,
            corpus=corpus,
            nr_samples=self.nr_samples,
            nr_repr_docs=self.nr_repr_docs,
        )

        # We extract the top n words per class
        topic_representations = self._extract_candidate_words(
            topic_model=topic_model, c_tf_idf=c_tf_idf, topic_representations=topic_representations
        )

        # We calculate the similarity between word and document embeddings and create
        # topic embeddings from the representative document embeddings
        sim_matrix, vocab = self._extract_embeddings(
            topic_model=topic_model,
            topic_representations=topic_representations,
            representative_docs=representative_docs,
            repr_doc_indices=repr_doc_indices,
        )
        # Find the best matching words based on the similarity matrix for each topic
        updated_topics = self._extract_top_words(
            vocab=vocab, topic_representations=topic_representations, sim_matrix=sim_matrix
        )

        return updated_topics

    def _extract_candidate_words(
        self,
        topic_model: "BERTopic",
        c_tf_idf: csr_matrix,
        topic_representations: dict[int, Keywords],
    ) -> dict[int, Keywords]:
        """For each topic, extract candidate words based on the c-TF-IDF
        representation.

        Arguments:
            topic_model: A BERTopic model
            c_tf_idf: The topic c-TF-IDF representation
            topic_representations: The top words per topic

        Returns:
            topics: The `self.top_n_words` per topic
        """
        labels = [int(label) for label in sorted(list(topic_representations.keys()))]

        # Scikit-Learn Deprecation: get_feature_names is deprecated in 1.0
        # and will be removed in 1.2. Please use get_feature_names_out instead.
        if version.parse(sklearn_version) >= version.parse("1.0.0"):
            words = topic_model.vectorizer_model.get_feature_names_out()
        else:
            words = topic_model.vectorizer_model.get_feature_names()

        indices = topic_model._top_n_idx_sparse(c_tf_idf, self.nr_candidate_words)
        scores = topic_model._top_n_values_sparse(c_tf_idf, indices)
        sorted_indices = np.argsort(scores, 1)
        indices = np.take_along_axis(indices, sorted_indices, axis=1)
        scores = np.take_along_axis(scores, sorted_indices, axis=1)

        # Get top 30 words per topic based on c-TF-IDF score
        updated_topic_representations = {
            label: Keywords(
                [
                    (words[word_index], score) if word_index is not None and score > 0 else ("", 0.00001)
                    for word_index, score in zip(indices[index][::-1], scores[index][::-1])
                ]
            )
            for index, label in enumerate(labels)
        }

        return updated_topic_representations

    def _extract_embeddings(
        self,
        topic_model: "BERTopic",
        topic_representations: dict[int, Keywords],
        representative_docs: list[str],
        repr_doc_indices: list[list[int]],
    ) -> tuple[np.ndarray, list[str]]:
        """Extract the representative document embeddings and create topic embeddings.
        Then extract word embeddings and calculate the cosine similarity between topic
        embeddings and the word embeddings. Topic embeddings are the average of
        representative document embeddings.

        Arguments:
            topic_model: A BERTopic model
            topic_representations: The top words per topic
            representative_docs: A flat list of representative documents
            repr_doc_indices: The indices of representative documents
                              that belong to each topic

        Returns:
            sim: The similarity matrix between word and topic embeddings
            vocab: The complete vocabulary of input documents
        """
        # Calculate representative document embeddings
        repr_embeddings = topic_model.embedding_model.embed_documents(representative_docs, verbose=False)

        topic_embeddings = [np.mean(repr_embeddings[i[0] : i[-1] + 1], axis=0) for i in repr_doc_indices]

        # Calculate word embeddings and extract best matching with updated topic_embeddings
        # vocab = list(set([word for words in topic_repr.words for word in words]))
        vocab = list(set([word for keywords in topic_representations.values() for word in keywords.words]))
        word_embeddings = topic_model.embedding_model.embed_words(vocab, verbose=False)
        sim = cosine_similarity(topic_embeddings, word_embeddings)

        return sim, vocab

    def _extract_top_words(
        self,
        vocab: list[str],
        topic_representations: dict[int, Keywords],
        sim_matrix: np.ndarray,
    ) -> dict[int, Keywords]:
        """Extract the top n words per topic based on the
        similarity matrix between topics and words.

        Arguments:
            vocab: The complete vocabulary of input documents
            topic_representations: The top words per topic
            sim_matrix: The similarity matrix between word and topic embeddings

        Returns:
            updated_topics: The updated topic representations
        """
        labels = [int(label) for label in sorted(list(topic_representations.keys()))]
        updated_topics = {}
        for i, topic in enumerate(labels):
            indices = [vocab.index(word) for word in topic_representations[topic].words]
            values = sim_matrix[:, indices][i]
            word_indices = np.argsort(values)[-self.top_n_words :][::-1]
            updated_topics[topic] = Keywords([(vocab[indices[idx]], values[idx]) for idx in word_indices])

        return updated_topics

__init__(top_n_words=10, nr_repr_docs=5, nr_samples=500, nr_candidate_words=100, random_state=42)

Use a KeyBERT-like model to fine-tune the topic representations.

The algorithm follows KeyBERT but does some optimization in order to speed up inference.

The steps are as follows. First, we extract the top n representative documents per topic. To extract the representative documents, we randomly sample a number of candidate documents per cluster which is controlled by the nr_samples parameter. Then, the top n representative documents are extracted by calculating the c-TF-IDF representation for the candidate documents and finding, through cosine similarity, which are closest to the topic c-TF-IDF representation. Next, the top n words per topic are extracted based on their c-TF-IDF representation, which is controlled by the nr_repr_docs parameter.

Then, we extract the embeddings for words and representative documents and create topic embeddings by averaging the representative documents. Finally, the most similar words to each topic are extracted by calculating the cosine similarity between word and topic embeddings.

Parameters:

Name Type Description Default
top_n_words int

The top n words to extract per topic.

10
nr_repr_docs int

The number of representative documents to extract per cluster.

5
nr_samples int

The number of candidate documents to extract per cluster.

500
nr_candidate_words int

The number of candidate words per cluster.

100
random_state int

The random state for randomly sampling candidate documents.

42

Usage:

from bertopic.representation import KeyBERTInspired
from bertopic import BERTopic

# Create your representation model
representation_model = KeyBERTInspired()

# Use the representation model in BERTopic on top of the default pipeline
topic_model = BERTopic(representation_model=representation_model)
Source code in bertopic\representation\_keybert.py
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
def __init__(
    self,
    top_n_words: int = 10,
    nr_repr_docs: int = 5,
    nr_samples: int = 500,
    nr_candidate_words: int = 100,
    random_state: int = 42,
):
    """Use a KeyBERT-like model to fine-tune the topic representations.

    The algorithm follows KeyBERT but does some optimization in
    order to speed up inference.

    The steps are as follows. First, we extract the top n representative
    documents per topic. To extract the representative documents, we
    randomly sample a number of candidate documents per cluster
    which is controlled by the `nr_samples` parameter. Then,
    the top n representative documents  are extracted by calculating
    the c-TF-IDF representation for the  candidate documents and finding,
    through cosine similarity, which are closest to the topic c-TF-IDF representation.
    Next, the top n words per topic are extracted based on their
    c-TF-IDF representation, which is controlled by the `nr_repr_docs`
    parameter.

    Then, we extract the embeddings for words and representative documents
    and create topic embeddings by averaging the representative documents.
    Finally, the most similar words to each topic are extracted by
    calculating the cosine similarity between word and topic embeddings.

    Arguments:
        top_n_words: The top n words to extract per topic.
        nr_repr_docs: The number of representative documents to extract per cluster.
        nr_samples: The number of candidate documents to extract per cluster.
        nr_candidate_words: The number of candidate words per cluster.
        random_state: The random state for randomly sampling candidate documents.

    Usage:

    ```python
    from bertopic.representation import KeyBERTInspired
    from bertopic import BERTopic

    # Create your representation model
    representation_model = KeyBERTInspired()

    # Use the representation model in BERTopic on top of the default pipeline
    topic_model = BERTopic(representation_model=representation_model)
    ```
    """
    self.top_n_words = top_n_words
    self.nr_repr_docs = nr_repr_docs
    self.nr_samples = nr_samples
    self.nr_candidate_words = nr_candidate_words
    self.random_state = random_state

extract_topics(topic_model, corpus, topic_representations, c_tf_idf, embeddings=None)

Extract topics.

Parameters:

Name Type Description Default
topic_model BERTopic

A BERTopic model

required
corpus Corpus

The input documents including (calculated) embeddings

required
topic_representations dict[int, Keywords]

The candidate topic representations

required
c_tf_idf csr_matrix

The topic c-TF-IDF representation

required
embeddings ndarray

Pre-trained document embeddings (not used)

None

Returns:

Name Type Description
updated_topics dict[int, Keywords]

Updated topic representations

Source code in bertopic\representation\_keybert.py
 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
112
113
114
115
116
117
118
119
120
def extract_topics(
    self,
    topic_model: "BERTopic",
    corpus: Corpus,
    topic_representations: dict[int, Keywords],
    c_tf_idf: csr_matrix,
    embeddings: np.ndarray = None,
) -> dict[int, Keywords]:
    """Extract topics.

    Arguments:
        topic_model: A BERTopic model
        corpus: The input documents including (calculated) embeddings
        topic_representations: The candidate topic representations
        c_tf_idf: The topic c-TF-IDF representation
        embeddings: Pre-trained document embeddings (not used)

    Returns:
        updated_topics: Updated topic representations
    """
    # We extract the top n representative documents per class
    _, representative_docs, repr_doc_indices, _ = topic_model._extract_representative_docs(
        c_tf_idf=c_tf_idf,
        corpus=corpus,
        nr_samples=self.nr_samples,
        nr_repr_docs=self.nr_repr_docs,
    )

    # We extract the top n words per class
    topic_representations = self._extract_candidate_words(
        topic_model=topic_model, c_tf_idf=c_tf_idf, topic_representations=topic_representations
    )

    # We calculate the similarity between word and document embeddings and create
    # topic embeddings from the representative document embeddings
    sim_matrix, vocab = self._extract_embeddings(
        topic_model=topic_model,
        topic_representations=topic_representations,
        representative_docs=representative_docs,
        repr_doc_indices=repr_doc_indices,
    )
    # Find the best matching words based on the similarity matrix for each topic
    updated_topics = self._extract_top_words(
        vocab=vocab, topic_representations=topic_representations, sim_matrix=sim_matrix
    )

    return updated_topics

LangChain

Bases: LLMRepresentation

Using a LangChain chat model to generate topic labels.

Accepts any LangChain chat model (e.g. from init_chat_model, ChatOpenAI, ChatAnthropic, etc.). For structured output, uses model.with_structured_output() which automatically selects the best method (provider-native or tool-calling fallback).

Parameters:

Name Type Description Default
model

A LangChain chat model instance (e.g. ChatOpenAI(...), init_chat_model("gpt-6-luna"), etc.).

required
prompt str | None

The prompt to be used in the model. If no prompt is given, bertopic.representation._prompts.DEFAULT_CHAT_PROMPT is used instead. NOTE: Use "[KEYWORDS]" and "[DOCUMENTS]" in the prompt to decide where the keywords and documents need to be inserted.

None
system_prompt str | None

The system prompt to be used in the model. If no system prompt is given, bertopic.representation._prompts.DEFAULT_SYSTEM_PROMPT is used instead.

None
json_schema dict[str, Any] | bool

A dictionary representing the JSON schema to enforce structured output. If set to True, a default schema will be used (bertopic.representation._prompts.DEFAULT_JSON_SCHEMA). Uses LangChain's with_structured_output() for provider-native or tool-calling structured output.

False
generator_kwargs dict[str, Any]

Kwargs passed to model.invoke() for fine-tuning the output.

{}
nr_docs int

The number of documents to pass to the model if a prompt with the ["DOCUMENTS"] tag is used.

4
diversity float | None

The diversity of documents to pass to the model. Accepts values between 0 and 1. A higher values results in passing more diverse documents whereas lower values passes more similar documents.

None
doc_length int | None

The maximum length of each document. If a document is longer, it will be truncated. If None, the entire document is passed.

None
tokenizer str | Callable | None

The tokenizer used to calculate to split the document into segments used to count the length of a document. * If tokenizer is 'char', then the document is split up into characters which are counted to adhere to doc_length * If tokenizer is 'whitespace', the document is split up into words separated by whitespaces. These words are counted and truncated depending on doc_length * If tokenizer is 'vectorizer', then the internal CountVectorizer is used to tokenize the document. These tokens are counted and truncated depending on doc_length * If tokenizer is a callable, then that callable is used to tokenize the document. These tokens are counted and truncated depending on doc_length

None

Usage:

To use this, you will need to install the langchain package first. Additionally, you will need a provider package for your chosen model:

pip install langchain langchain-openai

Then, you can use the LangChain representation model as follows:

from langchain.chat_models import init_chat_model
from bertopic.representation import LangChain
from bertopic import BERTopic

# Create your representation model
model = init_chat_model("gpt-6-luna")
representation_model = LangChain(model)

# Use the representation model in BERTopic on top of the default pipeline
topic_model = BERTopic(representation_model=representation_model)

You can also use a custom prompt:

prompt = "I have the following documents: [DOCUMENTS]. What topic do they contain?"
representation_model = LangChain(model, prompt=prompt)

You can also use structured output with a JSON schema:

representation_model = LangChain(model, json_schema=True)
Source code in bertopic\representation\_langchain.py
 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
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
class LangChain(LLMRepresentation):
    """Using a LangChain chat model to generate topic labels.

    Accepts any LangChain chat model (e.g. from `init_chat_model`, `ChatOpenAI`,
    `ChatAnthropic`, etc.). For structured output, uses `model.with_structured_output()`
    which automatically selects the best method (provider-native or tool-calling fallback).

    Arguments:
        model: A LangChain chat model instance (e.g. `ChatOpenAI(...)`,
               `init_chat_model("gpt-6-luna")`, etc.).
        prompt: The prompt to be used in the model. If no prompt is given,
                `bertopic.representation._prompts.DEFAULT_CHAT_PROMPT` is used instead.
                NOTE: Use `"[KEYWORDS]"` and `"[DOCUMENTS]"` in the prompt
                to decide where the keywords and documents need to be
                inserted.
        system_prompt: The system prompt to be used in the model. If no system prompt is given,
                       `bertopic.representation._prompts.DEFAULT_SYSTEM_PROMPT` is used instead.
        json_schema: A dictionary representing the JSON schema to enforce structured output.
                     If set to True, a default schema will be used (`bertopic.representation._prompts.DEFAULT_JSON_SCHEMA`).
                     Uses LangChain's `with_structured_output()` for provider-native or
                     tool-calling structured output.
        generator_kwargs: Kwargs passed to `model.invoke()` for fine-tuning the output.
        nr_docs: The number of documents to pass to the model if a prompt
                 with the `["DOCUMENTS"]` tag is used.
        diversity: The diversity of documents to pass to the model.
                   Accepts values between 0 and 1. A higher
                   values results in passing more diverse documents
                   whereas lower values passes more similar documents.
        doc_length: The maximum length of each document. If a document is longer,
                    it will be truncated. If None, the entire document is passed.
        tokenizer: The tokenizer used to calculate to split the document into segments
                   used to count the length of a document.
                       * If tokenizer is 'char', then the document is split up
                         into characters which are counted to adhere to `doc_length`
                       * If tokenizer is 'whitespace', the document is split up
                         into words separated by whitespaces. These words are counted
                         and truncated depending on `doc_length`
                       * If tokenizer is 'vectorizer', then the internal CountVectorizer
                         is used to tokenize the document. These tokens are counted
                         and truncated depending on `doc_length`
                       * If tokenizer is a callable, then that callable is used to tokenize
                         the document. These tokens are counted and truncated depending
                         on `doc_length`

    Usage:

    To use this, you will need to install the langchain package first.
    Additionally, you will need a provider package for your chosen model:

    `pip install langchain langchain-openai`

    Then, you can use the LangChain representation model as follows:

    ```python
    from langchain.chat_models import init_chat_model
    from bertopic.representation import LangChain
    from bertopic import BERTopic

    # Create your representation model
    model = init_chat_model("gpt-6-luna")
    representation_model = LangChain(model)

    # Use the representation model in BERTopic on top of the default pipeline
    topic_model = BERTopic(representation_model=representation_model)
    ```

    You can also use a custom prompt:

    ```python
    prompt = "I have the following documents: [DOCUMENTS]. What topic do they contain?"
    representation_model = LangChain(model, prompt=prompt)
    ```

    You can also use structured output with a JSON schema:

    ```python
    representation_model = LangChain(model, json_schema=True)
    ```
    """

    def __init__(
        self,
        model,
        prompt: str | None = None,
        nr_docs: int = 4,
        diversity: float | None = None,
        doc_length: int | None = None,
        tokenizer: str | Callable | None = None,
        *,
        system_prompt: str | None = None,
        json_schema: dict[str, Any] | bool = False,
        generator_kwargs: dict[str, Any] = {},
    ):
        super().__init__(
            prompt=DEFAULT_JSON_PROMPT if json_schema else (prompt or DEFAULT_CHAT_PROMPT),
            nr_docs=nr_docs,
            diversity=diversity,
            doc_length=doc_length,
            tokenizer=tokenizer,
        )

        # LangChain specific parameters
        self.model = model
        self.system_prompt = system_prompt if system_prompt is not None else DEFAULT_SYSTEM_PROMPT
        self.json_schema = DEFAULT_JSON_SCHEMA if json_schema is True else json_schema
        self.generator_kwargs = generator_kwargs

    def _generate(self, prompt: str) -> str:
        """Send one prompt to the LangChain chat model."""
        messages = [
            {"role": "system", "content": self.system_prompt},
            {"role": "user", "content": prompt},
        ]

        # A JSON schema is answered with a dict, or with a pydantic model when the schema is one
        if self.json_schema:
            structured_model = self.model.with_structured_output(self.json_schema)
            response = structured_model.invoke(messages, **self.generator_kwargs)
            return json.dumps(response if isinstance(response, dict) else response.model_dump(mode="json"))

        # Some models answer with a list of blocks, such as reasoning followed by text
        response = self.model.invoke(messages, **self.generator_kwargs)
        return response.content if isinstance(response.content, str) else response.text

LiteLLM

Bases: LLMRepresentation

Using the LiteLLM API to generate topic labels.

For an overview of models see:
https://docs.litellm.ai/docs/providers

Arguments:
    model: Model to use. Defaults to OpenAI's "gpt-6-luna".
    generator_kwargs: Kwargs passed to `litellm.completion`.
    prompt: The prompt to be used in the model. If no prompt is given,
            `bertopic.representation._prompts.DEFAULT_CHAT_PROMPT` is used instead.
            NOTE: Use `"[KEYWORDS]"` and `"[DOCUMENTS]"` in the prompt
            to decide where the keywords and documents need to be
            inserted.
    system_prompt: The system prompt to be used in the model. If no system prompt is given,
                   `bertopic.representation._prompts.DEFAULT_SYSTEM_PROMPT` is used instead.
    json_schema: A dictionary representing the JSON schema to enforce structured output.
                 If set to True, a default schema will be used (`bertopic.representation._prompts.DEFAULT_JSON_SCHEMA`).
                 Uses LiteLLM's `response_format` with `json_schema` type.
    delay_in_seconds: The delay in seconds between consecutive prompts
                      in order to prevent RateLimitErrors.
    exponential_backoff: Retry requests with a random exponential backoff.
                         A short sleep is used when a rate limit error is hit,
                         then the requests is retried. Increase the sleep length
                         if errors are hit until 10 unsuccesfull requests.
                         If True, overrides `delay_in_seconds`.
    nr_docs: The number of documents to pass to LiteLLM if a prompt
             with the `["DOCUMENTS"]` tag is used.
    diversity: The diversity of documents to pass to LiteLLM.
               Accepts values between 0 and 1. A higher
               values results in passing more diverse documents
               whereas lower values passes more similar documents.
    doc_length: The maximum length of each document. If a document is longer,
                it will be truncated. If None, the entire document is passed.
    tokenizer: The tokenizer used to calculate to split the document into segments
               used to count the length of a document.
                   * If tokenizer is 'char', then the document is split up
                     into characters which are counted to adhere to `doc_length`
                   * If tokenizer is 'whitespace', the document is split up
                     into words separated by whitespaces. These words are counted
                     and truncated depending on `doc_length`
                   * If tokenizer is 'vectorizer', then the internal CountVectorizer
                     is used to tokenize the document. These tokens are counted
                     and truncated depending on `doc_length`
                   * If tokenizer is a callable, then that callable is used to tokenize
                     the document. These tokens are counted and truncated depending
                     on `doc_length`

Usage:

To use this, you will need to install the litellm package first:

`pip install litellm`

Then, get yourself an API key of any provider (for instance OpenAI) and use it as follows:

```python
import os
from bertopic.representation import LiteLLM
from bertopic import BERTopic

# set ENV variables
os.environ["OPENAI_API_KEY"] = "your-openai-key"

# Create your representation model
representation_model = LiteLLM(model="gpt-6-luna")

# Use the representation model in BERTopic on top of the default pipeline
topic_model = BERTopic(representation_model=representation_model)
```

You can also use a custom prompt:

```python
prompt = "I have the following documents: [DOCUMENTS]

These documents are about the following topic: '" representation_model = LiteLLM(model="gpt-6-luna", prompt=prompt) You can also use structured output with a JSON schema:python representation_model = LiteLLM(model="gpt-6-luna", json_schema=True) ```

Source code in bertopic\representation\_litellm.py
 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
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
class LiteLLM(LLMRepresentation):
    """Using the LiteLLM API to generate topic labels.

    For an overview of models see:
    https://docs.litellm.ai/docs/providers

    Arguments:
        model: Model to use. Defaults to OpenAI's "gpt-6-luna".
        generator_kwargs: Kwargs passed to `litellm.completion`.
        prompt: The prompt to be used in the model. If no prompt is given,
                `bertopic.representation._prompts.DEFAULT_CHAT_PROMPT` is used instead.
                NOTE: Use `"[KEYWORDS]"` and `"[DOCUMENTS]"` in the prompt
                to decide where the keywords and documents need to be
                inserted.
        system_prompt: The system prompt to be used in the model. If no system prompt is given,
                       `bertopic.representation._prompts.DEFAULT_SYSTEM_PROMPT` is used instead.
        json_schema: A dictionary representing the JSON schema to enforce structured output.
                     If set to True, a default schema will be used (`bertopic.representation._prompts.DEFAULT_JSON_SCHEMA`).
                     Uses LiteLLM's `response_format` with `json_schema` type.
        delay_in_seconds: The delay in seconds between consecutive prompts
                          in order to prevent RateLimitErrors.
        exponential_backoff: Retry requests with a random exponential backoff.
                             A short sleep is used when a rate limit error is hit,
                             then the requests is retried. Increase the sleep length
                             if errors are hit until 10 unsuccesfull requests.
                             If True, overrides `delay_in_seconds`.
        nr_docs: The number of documents to pass to LiteLLM if a prompt
                 with the `["DOCUMENTS"]` tag is used.
        diversity: The diversity of documents to pass to LiteLLM.
                   Accepts values between 0 and 1. A higher
                   values results in passing more diverse documents
                   whereas lower values passes more similar documents.
        doc_length: The maximum length of each document. If a document is longer,
                    it will be truncated. If None, the entire document is passed.
        tokenizer: The tokenizer used to calculate to split the document into segments
                   used to count the length of a document.
                       * If tokenizer is 'char', then the document is split up
                         into characters which are counted to adhere to `doc_length`
                       * If tokenizer is 'whitespace', the document is split up
                         into words separated by whitespaces. These words are counted
                         and truncated depending on `doc_length`
                       * If tokenizer is 'vectorizer', then the internal CountVectorizer
                         is used to tokenize the document. These tokens are counted
                         and truncated depending on `doc_length`
                       * If tokenizer is a callable, then that callable is used to tokenize
                         the document. These tokens are counted and truncated depending
                         on `doc_length`

    Usage:

    To use this, you will need to install the litellm package first:

    `pip install litellm`

    Then, get yourself an API key of any provider (for instance OpenAI) and use it as follows:

    ```python
    import os
    from bertopic.representation import LiteLLM
    from bertopic import BERTopic

    # set ENV variables
    os.environ["OPENAI_API_KEY"] = "your-openai-key"

    # Create your representation model
    representation_model = LiteLLM(model="gpt-6-luna")

    # Use the representation model in BERTopic on top of the default pipeline
    topic_model = BERTopic(representation_model=representation_model)
    ```

    You can also use a custom prompt:

    ```python
    prompt = "I have the following documents: [DOCUMENTS] \nThese documents are about the following topic: '"
    representation_model = LiteLLM(model="gpt-6-luna", prompt=prompt)
    ```

    You can also use structured output with a JSON schema:

    ```python
    representation_model = LiteLLM(model="gpt-6-luna", json_schema=True)
    ```
    """  # noqa: D301

    def __init__(
        self,
        model: str = "gpt-6-luna",
        prompt: str | None = None,
        generator_kwargs: Mapping[str, Any] | None = None,
        delay_in_seconds: float | None = None,
        exponential_backoff: bool = False,
        nr_docs: int = 4,
        diversity: float | None = None,
        doc_length: int | None = None,
        tokenizer: str | Callable | None = None,
        *,
        system_prompt: str | None = None,
        json_schema: Mapping[str, Any] | bool = False,
    ):
        super().__init__(
            prompt=DEFAULT_JSON_PROMPT if json_schema else (prompt or DEFAULT_CHAT_PROMPT),
            nr_docs=nr_docs,
            diversity=diversity,
            doc_length=doc_length,
            tokenizer=tokenizer,
        )

        # LiteLLM specific parameters
        self.model = model
        self.system_prompt = DEFAULT_SYSTEM_PROMPT if system_prompt is None else system_prompt
        self.json_schema = DEFAULT_JSON_SCHEMA if json_schema is True else json_schema
        self.generator_kwargs = dict(generator_kwargs or {})
        self.delay_in_seconds = delay_in_seconds
        self.exponential_backoff = exponential_backoff
        if self.json_schema:
            self.generator_kwargs["response_format"] = {
                "type": "json_schema",
                "json_schema": {"name": "Topic", "schema": self.json_schema},
            }

    def _generate(self, prompt: str) -> str:
        """Send one prompt through LiteLLM, retrying rate limits when asked to."""
        if self.delay_in_seconds:
            time.sleep(self.delay_in_seconds)

        messages = [
            {"role": "system", "content": self.system_prompt},
            {"role": "user", "content": prompt},
        ]
        kwargs = {"model": self.model, "messages": messages, **self.generator_kwargs}
        response = (
            chat_completions_with_backoff(**kwargs) if self.exponential_backoff else completion(**kwargs)
        )
        return response["choices"][0]["message"]["content"]

LlamaCPP

Bases: LLMRepresentation

A llama.cpp implementation to use as a representation model.

Parameters:

Name Type Description Default
model str | Llama

Either a string pointing towards a local LLM or a llama_cpp.Llama object.

required
prompt str | None

The prompt to be used in the model. If no prompt is given, bertopic.representation._prompts.DEFAULT_CHAT_PROMPT is used instead. NOTE: Use "[KEYWORDS]" and "[DOCUMENTS]" in the prompt to decide where the keywords and documents need to be inserted.

None
system_prompt str | None

The system prompt to be used in the model. If no system prompt is given, bertopic.representation._prompts.DEFAULT_SYSTEM_PROMPT is used instead.

None
json_schema dict[str, Any] | bool

A dictionary representing the JSON schema to enforce structured output. If set to True, a default schema will be used (bertopic.representation._prompts.DEFAULT_JSON_SCHEMA). Uses llama.cpp's response_format with json_object type.

False
generator_kwargs dict[str, Any] | None

Kwargs passed to llama_cpp.Llama.create_chat_completion for fine-tuning the output.

None
nr_docs int

The number of documents to pass to the model if a prompt with the ["DOCUMENTS"] tag is used.

4
diversity float | None

The diversity of documents to pass to the model. Accepts values between 0 and 1. A higher values results in passing more diverse documents whereas lower values passes more similar documents.

None
doc_length int | None

The maximum length of each document. If a document is longer, it will be truncated. If None, the entire document is passed.

None
tokenizer str | Callable | None

The tokenizer used to calculate to split the document into segments used to count the length of a document. * If tokenizer is 'char', then the document is split up into characters which are counted to adhere to doc_length * If tokenizer is 'whitespace', the document is split up into words separated by whitespaces. These words are counted and truncated depending on doc_length * If tokenizer is 'vectorizer', then the internal CountVectorizer is used to tokenize the document. These tokens are counted and truncated depending on doc_length * If tokenizer is a callable, then that callable is used to tokenize the document. These tokens are counted and truncated depending on doc_length

None

Usage:

To use a llama.cpp, first download the LLM:

wget https://huggingface.co/ggml-org/gemma-4-E2B-it-GGUF/resolve/main/gemma-4-E2B-it-Q4_0.gguf

Then, we can now use the model the model with BERTopic in just a couple of lines:

from bertopic import BERTopic
from bertopic.representation import LlamaCPP

# Use llama.cpp to load in a 4-bit quantized version of Gemma 4 E2B
representation_model = LlamaCPP("gemma-4-E2B-it-Q4_0.gguf")

# Create our BERTopic model
topic_model = BERTopic(representation_model=representation_model, verbose=True)

If you want to have more control over the LLMs parameters, you can run it like so:

from bertopic import BERTopic
from bertopic.representation import LlamaCPP
from llama_cpp import Llama

# Use llama.cpp to load in a 4-bit quantized version of Gemma 4 E2B
llm = Llama(model_path="gemma-4-E2B-it-Q4_0.gguf", n_gpu_layers=-1, n_ctx=16384)
representation_model = LlamaCPP(llm)

# Create our BERTopic model
topic_model = BERTopic(representation_model=representation_model, verbose=True)

You can also use structured output with a JSON schema:

representation_model = LlamaCPP("gemma-4-E2B-it-Q4_0.gguf", json_schema=True)
Source code in bertopic\representation\_llamacpp.py
 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
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
class LlamaCPP(LLMRepresentation):
    """A llama.cpp implementation to use as a representation model.

    Arguments:
        model: Either a string pointing towards a local LLM or a
                `llama_cpp.Llama` object.
        prompt: The prompt to be used in the model. If no prompt is given,
                `bertopic.representation._prompts.DEFAULT_CHAT_PROMPT` is used instead.
                NOTE: Use `"[KEYWORDS]"` and `"[DOCUMENTS]"` in the prompt
                to decide where the keywords and documents need to be
                inserted.
        system_prompt: The system prompt to be used in the model. If no system prompt is given,
                       `bertopic.representation._prompts.DEFAULT_SYSTEM_PROMPT` is used instead.
        json_schema: A dictionary representing the JSON schema to enforce structured output.
                     If set to True, a default schema will be used (`bertopic.representation._prompts.DEFAULT_JSON_SCHEMA`).
                     Uses llama.cpp's `response_format` with `json_object` type.
        generator_kwargs: Kwargs passed to `llama_cpp.Llama.create_chat_completion`
                          for fine-tuning the output.
        nr_docs: The number of documents to pass to the model if a prompt
                 with the `["DOCUMENTS"]` tag is used.
        diversity: The diversity of documents to pass to the model.
                   Accepts values between 0 and 1. A higher
                   values results in passing more diverse documents
                   whereas lower values passes more similar documents.
        doc_length: The maximum length of each document. If a document is longer,
                    it will be truncated. If None, the entire document is passed.
        tokenizer: The tokenizer used to calculate to split the document into segments
                   used to count the length of a document.
                       * If tokenizer is 'char', then the document is split up
                         into characters which are counted to adhere to `doc_length`
                       * If tokenizer is 'whitespace', the document is split up
                         into words separated by whitespaces. These words are counted
                         and truncated depending on `doc_length`
                       * If tokenizer is 'vectorizer', then the internal CountVectorizer
                         is used to tokenize the document. These tokens are counted
                         and truncated depending on `doc_length`
                       * If tokenizer is a callable, then that callable is used to tokenize
                         the document. These tokens are counted and truncated depending
                         on `doc_length`

    Usage:

    To use a llama.cpp, first download the LLM:

    ```bash
    wget https://huggingface.co/ggml-org/gemma-4-E2B-it-GGUF/resolve/main/gemma-4-E2B-it-Q4_0.gguf
    ```

    Then, we can now use the model the model with BERTopic in just a couple of lines:

    ```python
    from bertopic import BERTopic
    from bertopic.representation import LlamaCPP

    # Use llama.cpp to load in a 4-bit quantized version of Gemma 4 E2B
    representation_model = LlamaCPP("gemma-4-E2B-it-Q4_0.gguf")

    # Create our BERTopic model
    topic_model = BERTopic(representation_model=representation_model, verbose=True)
    ```

    If you want to have more control over the LLMs parameters, you can run it like so:

    ```python
    from bertopic import BERTopic
    from bertopic.representation import LlamaCPP
    from llama_cpp import Llama

    # Use llama.cpp to load in a 4-bit quantized version of Gemma 4 E2B
    llm = Llama(model_path="gemma-4-E2B-it-Q4_0.gguf", n_gpu_layers=-1, n_ctx=16384)
    representation_model = LlamaCPP(llm)

    # Create our BERTopic model
    topic_model = BERTopic(representation_model=representation_model, verbose=True)
    ```

    You can also use structured output with a JSON schema:

    ```python
    representation_model = LlamaCPP("gemma-4-E2B-it-Q4_0.gguf", json_schema=True)
    ```
    """

    def __init__(
        self,
        model: str | Llama,
        prompt: str | None = None,
        system_prompt: str | None = None,
        generator_kwargs: dict[str, Any] | None = None,
        nr_docs: int = 4,
        diversity: float | None = None,
        doc_length: int | None = None,
        tokenizer: str | Callable | None = None,
        *,
        json_schema: dict[str, Any] | bool = False,
    ):
        super().__init__(
            prompt=DEFAULT_JSON_PROMPT if json_schema else (prompt or DEFAULT_CHAT_PROMPT),
            nr_docs=nr_docs,
            diversity=diversity,
            doc_length=doc_length,
            tokenizer=tokenizer,
        )

        # Llama.cpp specific parameters
        if isinstance(model, str):
            # A local file is loaded with its own chat template, and room for the prompt's documents
            self.model = Llama(model_path=model, n_gpu_layers=-1, n_ctx=16384)
        elif isinstance(model, Llama):
            self.model = model
        else:
            raise ValueError(
                "Make sure that the model that you"
                "pass is either a string referring to a"
                "local LLM or a ` llama_cpp.Llama` object."
            )
        self.system_prompt = system_prompt if system_prompt is not None else DEFAULT_SYSTEM_PROMPT
        self.json_schema = DEFAULT_JSON_SCHEMA if json_schema is True else json_schema
        self.generator_kwargs = dict(generator_kwargs or {})
        if self.json_schema:
            self.generator_kwargs["response_format"] = {"type": "json_object", "schema": self.json_schema}

    def _generate(self, prompt: str) -> str:
        """Send one prompt to the llama.cpp model."""
        messages = [
            {"role": "system", "content": self.system_prompt},
            {"role": "user", "content": prompt},
        ]
        kwargs = {"messages": messages, **self.generator_kwargs}
        return self.model.create_chat_completion(**kwargs)["choices"][0]["message"]["content"]

MaximalMarginalRelevance

Bases: BaseRepresentation

Calculate Maximal Marginal Relevance (MMR) between candidate keywords and the document.

MMR considers the similarity of keywords/keyphrases with the document, along with the similarity of already selected keywords and keyphrases. This results in a selection of keywords that maximize their within diversity with respect to the document.

Parameters:

Name Type Description Default
diversity float

How diverse the select keywords/keyphrases are. Values range between 0 and 1 with 0 being not diverse at all and 1 being most diverse.

0.1
top_n_words int

The number of keywords/keyhprases to return

10

Usage:

from bertopic.representation import MaximalMarginalRelevance
from bertopic import BERTopic

# Create your representation model
representation_model = MaximalMarginalRelevance(diversity=0.3)

# Use the representation model in BERTopic on top of the default pipeline
topic_model = BERTopic(representation_model=representation_model)
Source code in bertopic\representation\_mmr.py
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
class MaximalMarginalRelevance(BaseRepresentation):
    """Calculate Maximal Marginal Relevance (MMR)
    between candidate keywords and the document.

    MMR considers the similarity of keywords/keyphrases with the
    document, along with the similarity of already selected
    keywords and keyphrases. This results in a selection of keywords
    that maximize their within diversity with respect to the document.

    Arguments:
        diversity: How diverse the select keywords/keyphrases are.
                    Values range between 0 and 1 with 0 being not diverse at all
                    and 1 being most diverse.
        top_n_words: The number of keywords/keyhprases to return

    Usage:

    ```python
    from bertopic.representation import MaximalMarginalRelevance
    from bertopic import BERTopic

    # Create your representation model
    representation_model = MaximalMarginalRelevance(diversity=0.3)

    # Use the representation model in BERTopic on top of the default pipeline
    topic_model = BERTopic(representation_model=representation_model)
    ```
    """

    def __init__(self, diversity: float = 0.1, top_n_words: int = 10):
        self.diversity = diversity
        self.top_n_words = top_n_words

    def extract_topics(
        self,
        topic_model: "BERTopic",
        corpus: Corpus,
        topic_representations: dict[int, Keywords],
        c_tf_idf: csr_matrix,
        embeddings: np.ndarray = None,
    ) -> dict[int, Keywords]:
        """Extract topic representations.

        Arguments:
            topic_model: The BERTopic model
            corpus: The input documents including (calculated) embeddings
            topic_representations: The candidate topic representations
            c_tf_idf: The topic c-TF-IDF representation
            embeddings: Pre-trained document embeddings (not used)

        Returns:
            updated_topics: Updated topic representations
        """
        if topic_model.embedding_model is None:
            warnings.warn(
                "MaximalMarginalRelevance can only be used if BERTopic was instantiated "
                "with the `embedding_model` parameter."
            )
            return {
                topic: Keywords(keywords.top_n(self.top_n_words))
                for topic, keywords in topic_representations.items()
            }

        updated_topics = {}
        for topic, keywords in topic_representations.items():
            words = keywords.words
            word_embeddings = topic_model.embedding_model.embed_documents(words, verbose=False)
            topic_embedding = topic_model.embedding_model.embed_documents(
                [" ".join(words)], verbose=False
            ).reshape(1, -1)
            selected_words = mmr(
                topic_embedding,
                word_embeddings,
                words,
                self.diversity,
                self.top_n_words,
            )
            # Filter the original keywords to keep only the selected words, preserving their scores
            updated_topics[topic] = Keywords(
                [(word, score) for word, score in keywords.data if word in selected_words]
            )
        return updated_topics

extract_topics(topic_model, corpus, topic_representations, c_tf_idf, embeddings=None)

Extract topic representations.

Parameters:

Name Type Description Default
topic_model BERTopic

The BERTopic model

required
corpus Corpus

The input documents including (calculated) embeddings

required
topic_representations dict[int, Keywords]

The candidate topic representations

required
c_tf_idf csr_matrix

The topic c-TF-IDF representation

required
embeddings ndarray

Pre-trained document embeddings (not used)

None

Returns:

Name Type Description
updated_topics dict[int, Keywords]

Updated topic representations

Source code in bertopic\representation\_mmr.py
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
def extract_topics(
    self,
    topic_model: "BERTopic",
    corpus: Corpus,
    topic_representations: dict[int, Keywords],
    c_tf_idf: csr_matrix,
    embeddings: np.ndarray = None,
) -> dict[int, Keywords]:
    """Extract topic representations.

    Arguments:
        topic_model: The BERTopic model
        corpus: The input documents including (calculated) embeddings
        topic_representations: The candidate topic representations
        c_tf_idf: The topic c-TF-IDF representation
        embeddings: Pre-trained document embeddings (not used)

    Returns:
        updated_topics: Updated topic representations
    """
    if topic_model.embedding_model is None:
        warnings.warn(
            "MaximalMarginalRelevance can only be used if BERTopic was instantiated "
            "with the `embedding_model` parameter."
        )
        return {
            topic: Keywords(keywords.top_n(self.top_n_words))
            for topic, keywords in topic_representations.items()
        }

    updated_topics = {}
    for topic, keywords in topic_representations.items():
        words = keywords.words
        word_embeddings = topic_model.embedding_model.embed_documents(words, verbose=False)
        topic_embedding = topic_model.embedding_model.embed_documents(
            [" ".join(words)], verbose=False
        ).reshape(1, -1)
        selected_words = mmr(
            topic_embedding,
            word_embeddings,
            words,
            self.diversity,
            self.top_n_words,
        )
        # Filter the original keywords to keep only the selected words, preserving their scores
        updated_topics[topic] = Keywords(
            [(word, score) for word, score in keywords.data if word in selected_words]
        )
    return updated_topics

MultiModalRepresentation

Bases: TextConverter

Represent topics by their media, and describe that media so c-TF-IDF has words.

A topic that holds photographs and voice notes is one topic, so it gets one representation carrying both. The same sample of representative media serves twice: it is described in text, which is what gives a media-only corpus any keywords at all, and it is kept as the topic's Media representation. Each modality also gets one summary that stands for the topic: a collage of its images, a sheet of its videos' middle frames, and a montage of the first seconds of its audio.

Describing every item would be prohibitive on a corpus of millions, so only nr_repr_media items per topic are described. MMR picks them near each topic's centroid while discarding near-duplicates.

A modality without its own model is described by model. Note that a model name is loaded into that modality's own task, so one name rarely covers both captioning and transcription; a callable can, since it decides for itself. Models are loaded on first use, so a modality absent from the corpus costs nothing.

Parameters:

Name Type Description Default
model str | Pipeline | Callable | None

The model that describes media in words, for any modality without its own. Either the name of a model to load into a transformers pipeline, an already built pipeline, or any callable taking a list of items and returning one string each. Without one, topics are represented by their media alone.

None
image_model str | Pipeline | Callable | None

The model that describes only images.

None
audio_model str | Pipeline | Callable | None

The model that transcribes only audio.

None
video_model str | Pipeline | Callable | None

The model that describes only video.

None
nr_repr_media int

Number of representative media items to describe per topic.

9
nr_frames int

Number of frames to sample from each video, since one still cannot stand for a clip.

3
image_height int

The height of the resulting collage and frame sheet.

600
image_squares bool

Whether to crop each image in a collage or frame sheet to fill a square, rather than keep it whole in a cell of the images' typical shape.

False
prompt str

What to ask the model for, when it is a pipeline rather than a callable.

'Describe this image in one short sentence.'
batch_size int

The number of items to describe at a time.

4

Usage:

from bertopic import BERTopic
from bertopic.representation import MultiModalRepresentation

# Media is an additional way of looking at a topic, so it is passed as an aspect
representation_model = {
    "Media": MultiModalRepresentation(
        "HuggingFaceTB/SmolVLM-256M-Instruct", audio_model="openai/whisper-base"
    )
}
topic_model = BERTopic(representation_model=representation_model)
Source code in bertopic\representation\_multimodal.py
 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
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
class MultiModalRepresentation(TextConverter):
    """Represent topics by their media, and describe that media so c-TF-IDF has words.

    A topic that holds photographs and voice notes is one topic, so it gets one
    representation carrying both. The same sample of representative media serves twice:
    it is described in text, which is what gives a media-only corpus any keywords at all,
    and it is kept as the topic's `Media` representation. Each modality also gets one
    summary that stands for the topic: a collage of its images, a sheet of its videos'
    middle frames, and a montage of the first seconds of its audio.

    Describing every item would be prohibitive on a corpus of millions, so only
    `nr_repr_media` items per topic are described. MMR picks them near each topic's
    centroid while discarding near-duplicates.

    A modality without its own model is described by `model`. Note that a model *name*
    is loaded into that modality's own task, so one name rarely covers both captioning
    and transcription; a callable can, since it decides for itself. Models are loaded on
    first use, so a modality absent from the corpus costs nothing.

    Arguments:
        model: The model that describes media in words, for any modality without its own.
               Either the name of a model to load into a `transformers` pipeline, an
               already built pipeline, or any callable taking a list of items and
               returning one string each. Without one, topics are represented by their
               media alone.
        image_model: The model that describes only images.
        audio_model: The model that transcribes only audio.
        video_model: The model that describes only video.
        nr_repr_media: Number of representative media items to describe per topic.
        nr_frames: Number of frames to sample from each video, since one still cannot
                   stand for a clip.
        image_height: The height of the resulting collage and frame sheet.
        image_squares: Whether to crop each image in a collage or frame sheet to fill a square,
                       rather than keep it whole in a cell of the images' typical shape.
        prompt: What to ask the model for, when it is a pipeline rather than a callable.
        batch_size: The number of items to describe at a time.

    Usage:

    ```python
    from bertopic import BERTopic
    from bertopic.representation import MultiModalRepresentation

    # Media is an additional way of looking at a topic, so it is passed as an aspect
    representation_model = {
        "Media": MultiModalRepresentation(
            "HuggingFaceTB/SmolVLM-256M-Instruct", audio_model="openai/whisper-base"
        )
    }
    topic_model = BERTopic(representation_model=representation_model)
    ```
    """

    modalities: ClassVar[set[Modality]] = {Modality.IMAGE, Modality.AUDIO, Modality.VIDEO}

    def __init__(
        self,
        model: str | Pipeline | Callable | None = None,
        image_model: str | Pipeline | Callable | None = None,
        audio_model: str | Pipeline | Callable | None = None,
        video_model: str | Pipeline | Callable | None = None,
        nr_repr_media: int = 9,
        nr_frames: int = 3,
        image_height: int = 600,
        image_squares: bool = False,
        prompt: str = "Describe this image in one short sentence.",
        batch_size: int = 4,
    ):
        # A modality without its own model is described by the main one
        self.models = {}
        for modality, given in (
            (Modality.IMAGE, image_model),
            (Modality.AUDIO, audio_model),
            (Modality.VIDEO, video_model),
        ):
            given = model if given is None else given
            if given is not None and not isinstance(given, str) and not callable(given):
                raise ValueError(
                    "Please pass the name of a model, a transformers pipeline, or a callable "
                    "taking a list of items and returning one description each. For example:"
                    "MultiModalRepresentation('HuggingFaceTB/SmolVLM-256M-Instruct')"
                )
            self.models[modality] = given

        self.pipelines = {}
        self.nr_repr_media = nr_repr_media
        self.nr_frames = nr_frames
        self.image_height = image_height
        self.image_squares = image_squares
        self.prompt = prompt
        self.batch_size = batch_size

    def to_text(self, corpus: Corpus) -> Corpus:
        """Describe a sample of each topic's media, writing into those rows' text channel.

        The corpus keeps its shape. Only sampled rows without text gain some, so documents,
        captions the user gave, and modalities this model does not handle are left exactly
        as they were, and several converters can run one after another without overwriting
        each other.
        """
        documents = list(corpus.documents)
        for modality in self.modalities:
            if self.models[modality] is None:
                continue

            # Describe a sample of every topic at once, so the model batches them. A row that
            # already has text, such as a captioned image, keeps the words it was given
            rows = [
                row
                for topic in corpus.topic_ids()
                for row in self._sample(corpus, topic, modality)
                if not corpus.documents[row]
            ]
            if not rows:
                continue

            described = self._convert_media_to_text([corpus.media[row] for row in rows], modality)
            for row, description in zip(rows, described):
                documents[row] = description

        corpus.documents = documents
        return corpus

    def extract_topics(
        self,
        topic_model: "BERTopic",
        corpus: Corpus,
        topic_representations: dict[int, Keywords],
        c_tf_idf: csr_matrix,
        embeddings: np.ndarray = None,
    ) -> dict[int, Media]:
        """Collect each topic's representative media into a single representation.

        Arguments:
            topic_model: A BERTopic model
            corpus: The input documents including (calculated) embeddings
            topic_representations: The candidate topic representations
            c_tf_idf: The topic c-TF-IDF representation (unused, for API compatibility)
            embeddings: Pre-trained document embeddings (unused, for API compatibility)

        Returns:
            A `Media` representation per topic, carrying its media and their descriptions,
            and empty for a topic without media so every topic has the same columns
        """
        representations = {}
        for topic in tqdm(sorted(topic_representations), disable=not topic_model.verbose):
            # The same rows `to_text` described, since MMR over unchanged embeddings repeats. Sorted,
            # since a set's order changes between runs and a topic should list its media the same way
            rows = {modality: self._sample(corpus, topic, modality) for modality in sorted(self.modalities)}
            rows = {modality: found for modality, found in rows.items() if found}

            items = {modality: [corpus.media[row] for row in found] for modality, found in rows.items()}
            representations[topic] = Media(
                items=items,
                summaries=self._summarize(items),
                captions=[
                    corpus.documents[row] for found in rows.values() for row in found if corpus.documents[row]
                ],
            )

        return representations

    def _sample(self, corpus: Corpus, topic: int, modality: Modality) -> list[int]:
        """The rows of one modality that best represent one topic, without near-duplicates."""
        rows = np.array(
            [
                index
                for index, (value, assigned) in enumerate(zip(corpus.modality, corpus.topics))
                if value == modality and assigned == topic
            ]
        )
        if not len(rows):
            return []

        # Pick a diverse sample near the topic's centroid
        centroid = corpus.embeddings[rows].mean(axis=0).reshape(1, -1)
        return list(
            mmr(
                centroid,
                corpus.embeddings[rows],
                rows,
                top_n=min(self.nr_repr_media, len(rows)),
                diversity=0.1,
            )
        )

    def _convert_media_to_text(self, items: list, modality: Modality) -> list[str]:
        """Describe media of one modality, with the model that handles it.

        Arguments:
            items: A list of media items, all of the same modality.
            modality: Which modality the items are.

        Returns:
            List of descriptions
        """
        model = self._model_for(modality)
        if not isinstance(model, Pipeline):
            return model(items)

        if modality == Modality.AUDIO:
            transcripts = model(items, batch_size=self.batch_size, chunk_length_s=30, ignore_warning=True)
            return [spoken["text"].strip() for spoken in transcripts]

        if modality == Modality.IMAGE:
            images = [self._open(image) for image in items]
            captions = self._caption(images, model)
            for image in images:
                image.close()
            return captions

        # A video is described by describing the frames sampled from it, joined back up
        frames = [frame for video in items for frame in self._frames(video, self.nr_frames)]
        captions = self._caption(frames, model)
        return [
            " ".join(captions[start : start + self.nr_frames])
            for start in range(0, len(captions), self.nr_frames)
        ]

    def _caption(self, images: list, model: Pipeline) -> list[str]:
        """Caption opened images through a vision-language pipeline."""
        # One chat turn per image, which is how `image-text-to-text` is served
        messages = [
            [
                {
                    "role": "user",
                    "content": [{"type": "image", "image": image}, {"type": "text", "text": self.prompt}],
                }
            ]
            for image in images
        ]

        # A caption, not an essay: c-TF-IDF wants the words rather than the prose
        outputs = model(text=messages, max_new_tokens=30, batch_size=self.batch_size)
        return [output[0]["generated_text"][-1]["content"].strip() for output in outputs]

    def _summarize(self, items: dict[Modality, list]) -> dict[Modality, Any]:
        """One object per modality that stands for the topic, the way a label does for text."""
        summaries = {}
        if Modality.IMAGE in items:
            summaries[Modality.IMAGE] = self._collage(items[Modality.IMAGE])

        # A sheet of each clip's middle frame, tiled the way a collage tiles images
        if Modality.VIDEO in items:
            frames = [self._frames(video, nr_frames=1)[0] for video in items[Modality.VIDEO]]
            summaries[Modality.VIDEO] = self._collage(frames)

        if Modality.AUDIO in items:
            summaries[Modality.AUDIO] = self._montage(items[Modality.AUDIO])
        return summaries

    def _collage(self, images: list) -> Image.Image:
        """Tile a topic's images three to a row, so one picture stands for the topic.

        Every image gets a cell of the same size, so a short last row stays short rather
        than being stretched to the full width. Cells have the images' typical shape and
        hold each image whole, or are squares the images are cropped to fill.
        """
        opened = [self._open(image) for image in images]
        columns = min(3, len(opened))
        rows = math.ceil(len(opened) / columns)

        # Together the rows are `image_height` tall
        height = self.image_height // rows
        shape = 1 if self.image_squares else statistics.median(image.width / image.height for image in opened)
        cell = (round(height * shape), height)

        collage = Image.new("RGB", (cell[0] * columns, cell[1] * rows), "white")
        for index, image in enumerate(opened):
            if self.image_squares:
                tile = ImageOps.fit(image.convert("RGB"), cell)
            else:
                tile = ImageOps.pad(image.convert("RGB"), cell, color="white")
            collage.paste(tile, ((index % columns) * cell[0], (index // columns) * cell[1]))

        for image in opened:
            image.close()
        return collage

    def _model_for(self, modality: Modality):
        """Load a modality's model on first use, so an absent modality costs nothing."""
        model = self.models[modality]
        if not isinstance(model, str):
            return model

        # Image and video share a task, so one pipeline serves both when the name matches
        key = (TASKS[modality], model)
        if key not in self.pipelines:
            self.pipelines[key] = pipeline(key[0], model=key[1])
        return self.pipelines[key]

    @staticmethod
    def _frames(video, nr_frames: int) -> list:
        """Take the middle frame of equal stretches of a clip, clear of its black ends."""
        # Imported here because decoding video is the only thing that needs it, and video is
        # an extra: describing images and audio must work without it installed
        from torchcodec.decoders import VideoDecoder

        decoder = VideoDecoder(video)
        stretch = decoder.metadata.num_frames / nr_frames
        indices = [int((index + 0.5) * stretch) for index in range(nr_frames)]
        return [Image.fromarray(decoder[index].permute(1, 2, 0).numpy()) for index in indices]

    def _montage(self, clips: list) -> bytes:
        """The opening seconds of each clip back to back, written as one WAV file."""
        waveforms = [self._read_audio(clip) if isinstance(clip, str) else clip for clip in clips]
        montage = np.concatenate([waveform[: MONTAGE_SECONDS * SAMPLING_RATE] for waveform in waveforms])

        # Sixteen-bit mono, which any player reads and the standard library can write
        buffer = io.BytesIO()
        with wave.open(buffer, "wb") as file:
            file.setnchannels(1)
            file.setsampwidth(2)
            file.setframerate(SAMPLING_RATE)
            file.writeframes((np.clip(montage, -1, 1) * 32767).astype(np.int16).tobytes())
        return buffer.getvalue()

    @staticmethod
    def _open(image):
        """Open an image from a path, or copy one that is already loaded."""
        return Image.open(image) if isinstance(image, str) else image.copy()

    @staticmethod
    def _read_audio(clip: str) -> np.ndarray:
        """Decode the opening of a clip from its path, at the rate a bare array is taken to have."""
        # Imported here for the same reason as `torchcodec`: audio is an extra
        import librosa

        # Only what a montage keeps, since a clip may be an hour-long recording
        return librosa.load(clip, sr=SAMPLING_RATE, duration=MONTAGE_SECONDS)[0]

extract_topics(topic_model, corpus, topic_representations, c_tf_idf, embeddings=None)

Collect each topic's representative media into a single representation.

Parameters:

Name Type Description Default
topic_model BERTopic

A BERTopic model

required
corpus Corpus

The input documents including (calculated) embeddings

required
topic_representations dict[int, Keywords]

The candidate topic representations

required
c_tf_idf csr_matrix

The topic c-TF-IDF representation (unused, for API compatibility)

required
embeddings ndarray

Pre-trained document embeddings (unused, for API compatibility)

None

Returns:

Type Description
dict[int, Media]

A Media representation per topic, carrying its media and their descriptions,

dict[int, Media]

and empty for a topic without media so every topic has the same columns

Source code in bertopic\representation\_multimodal.py
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
def extract_topics(
    self,
    topic_model: "BERTopic",
    corpus: Corpus,
    topic_representations: dict[int, Keywords],
    c_tf_idf: csr_matrix,
    embeddings: np.ndarray = None,
) -> dict[int, Media]:
    """Collect each topic's representative media into a single representation.

    Arguments:
        topic_model: A BERTopic model
        corpus: The input documents including (calculated) embeddings
        topic_representations: The candidate topic representations
        c_tf_idf: The topic c-TF-IDF representation (unused, for API compatibility)
        embeddings: Pre-trained document embeddings (unused, for API compatibility)

    Returns:
        A `Media` representation per topic, carrying its media and their descriptions,
        and empty for a topic without media so every topic has the same columns
    """
    representations = {}
    for topic in tqdm(sorted(topic_representations), disable=not topic_model.verbose):
        # The same rows `to_text` described, since MMR over unchanged embeddings repeats. Sorted,
        # since a set's order changes between runs and a topic should list its media the same way
        rows = {modality: self._sample(corpus, topic, modality) for modality in sorted(self.modalities)}
        rows = {modality: found for modality, found in rows.items() if found}

        items = {modality: [corpus.media[row] for row in found] for modality, found in rows.items()}
        representations[topic] = Media(
            items=items,
            summaries=self._summarize(items),
            captions=[
                corpus.documents[row] for found in rows.values() for row in found if corpus.documents[row]
            ],
        )

    return representations

to_text(corpus)

Describe a sample of each topic's media, writing into those rows' text channel.

The corpus keeps its shape. Only sampled rows without text gain some, so documents, captions the user gave, and modalities this model does not handle are left exactly as they were, and several converters can run one after another without overwriting each other.

Source code in bertopic\representation\_multimodal.py
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
def to_text(self, corpus: Corpus) -> Corpus:
    """Describe a sample of each topic's media, writing into those rows' text channel.

    The corpus keeps its shape. Only sampled rows without text gain some, so documents,
    captions the user gave, and modalities this model does not handle are left exactly
    as they were, and several converters can run one after another without overwriting
    each other.
    """
    documents = list(corpus.documents)
    for modality in self.modalities:
        if self.models[modality] is None:
            continue

        # Describe a sample of every topic at once, so the model batches them. A row that
        # already has text, such as a captioned image, keeps the words it was given
        rows = [
            row
            for topic in corpus.topic_ids()
            for row in self._sample(corpus, topic, modality)
            if not corpus.documents[row]
        ]
        if not rows:
            continue

        described = self._convert_media_to_text([corpus.media[row] for row in rows], modality)
        for row, description in zip(rows, described):
            documents[row] = description

    corpus.documents = documents
    return corpus

Ollama

Bases: LLMRepresentation

Using the Ollama API to generate topic labels based on a local LLM.

Parameters:

Name Type Description Default
model str

Model to use within Ollama.

required
prompt str | None

The prompt to be used in the model. If no prompt is given, bertopic.representation._prompts.DEFAULT_CHAT_PROMPT is used instead. NOTE: Use "[KEYWORDS]" and "[DOCUMENTS]" in the prompt to decide where the keywords and documents need to be inserted.

None
system_prompt str | None

The system prompt to be used in the model. If no system prompt is given, bertopic.representation._prompts.DEFAULT_SYSTEM_PROMPT is used instead.

None
json_schema dict[str, Any] | bool

A dictionary representing the JSON schema to enforce structured output. If set to True, a default schema will be used (bertopic.representation._prompts.DEFAULT_JSON_SCHEMA).

False
generator_kwargs dict[str, Any] | None

Kwargs passed to ollama.chat for fine-tuning the output.

None
nr_docs int

The number of documents to pass to Ollama if a prompt with the ["DOCUMENTS"] tag is used.

4
diversity float | None

The diversity of documents to pass to Ollama. Accepts values between 0 and 1. A higher values results in passing more diverse documents whereas lower values passes more similar documents.

None
doc_length int | None

The maximum length of each document. If a document is longer, it will be truncated. If None, the entire document is passed.

None
tokenizer str | Callable | None

The tokenizer used to calculate to split the document into segments used to count the length of a document. * If tokenizer is 'char', then the document is split up into characters which are counted to adhere to doc_length * If tokenizer is 'whitespace', the document is split up into words separated by whitespaces. These words are counted and truncated depending on doc_length * If tokenizer is 'vectorizer', then the internal CountVectorizer is used to tokenize the document. These tokens are counted and truncated depending on doc_length * If tokenizer is a callable, then that callable is used to tokenize the document. These tokens are counted and truncated depending on doc_length

None

Usage:

To use this, you will need to install the ollama package first:

pip install ollama

Then, you can use the Ollama representation model as follows:

from bertopic.representation import Ollama
from bertopic import BERTopic

# Create your representation model
representation_model = Ollama("gemma4")

# Use the representation model in BERTopic on top of the default pipeline
topic_model = BERTopic(representation_model=representation_model)

You can also use a custom prompt:

prompt = "I have the following documents: [DOCUMENTS] \nThese documents are about the following topic: '"
representation_model = Ollama("gemma4", prompt=prompt)
Source code in bertopic\representation\_ollama.py
 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 Ollama(LLMRepresentation):
    r"""Using the Ollama API to generate topic labels based on a local LLM.

    Arguments:
        model: Model to use within Ollama.
        prompt: The prompt to be used in the model. If no prompt is given,
                `bertopic.representation._prompts.DEFAULT_CHAT_PROMPT` is used instead.
                NOTE: Use `"[KEYWORDS]"` and `"[DOCUMENTS]"` in the prompt
                to decide where the keywords and documents need to be
                inserted.
        system_prompt: The system prompt to be used in the model. If no system prompt is given,
                       `bertopic.representation._prompts.DEFAULT_SYSTEM_PROMPT` is used instead.
        json_schema: A dictionary representing the JSON schema to enforce structured output.
                     If set to True, a default schema will be used (`bertopic.representation._prompts.DEFAULT_JSON_SCHEMA`).
        generator_kwargs: Kwargs passed to `ollama.chat` for fine-tuning the output.
        nr_docs: The number of documents to pass to Ollama if a prompt
                 with the `["DOCUMENTS"]` tag is used.
        diversity: The diversity of documents to pass to Ollama.
                   Accepts values between 0 and 1. A higher
                   values results in passing more diverse documents
                   whereas lower values passes more similar documents.
        doc_length: The maximum length of each document. If a document is longer,
                    it will be truncated. If None, the entire document is passed.
        tokenizer: The tokenizer used to calculate to split the document into segments
                   used to count the length of a document.
                       * If tokenizer is 'char', then the document is split up
                         into characters which are counted to adhere to `doc_length`
                       * If tokenizer is 'whitespace', the document is split up
                         into words separated by whitespaces. These words are counted
                         and truncated depending on `doc_length`
                       * If tokenizer is 'vectorizer', then the internal CountVectorizer
                         is used to tokenize the document. These tokens are counted
                         and truncated depending on `doc_length`
                       * If tokenizer is a callable, then that callable is used to tokenize
                         the document. These tokens are counted and truncated depending
                         on `doc_length`

    Usage:

    To use this, you will need to install the ollama package first:

    `pip install ollama`

    Then, you can use the Ollama representation model as follows:

    ```python
    from bertopic.representation import Ollama
    from bertopic import BERTopic

    # Create your representation model
    representation_model = Ollama("gemma4")

    # Use the representation model in BERTopic on top of the default pipeline
    topic_model = BERTopic(representation_model=representation_model)
    ```

    You can also use a custom prompt:

    ```python
    prompt = "I have the following documents: [DOCUMENTS] \nThese documents are about the following topic: '"
    representation_model = Ollama("gemma4", prompt=prompt)
    ```
    """

    def __init__(
        self,
        model: str,
        prompt: str | None = None,
        system_prompt: str | None = None,
        json_schema: dict[str, Any] | bool = False,
        generator_kwargs: dict[str, Any] | None = None,
        nr_docs: int = 4,
        diversity: float | None = None,
        doc_length: int | None = None,
        tokenizer: str | Callable | None = None,
    ):
        super().__init__(
            prompt=DEFAULT_JSON_PROMPT if json_schema else (prompt or DEFAULT_CHAT_PROMPT),
            nr_docs=nr_docs,
            diversity=diversity,
            doc_length=doc_length,
            tokenizer=tokenizer,
        )

        # Ollama specific parameters
        self.model = model
        self.system_prompt = DEFAULT_SYSTEM_PROMPT if system_prompt is None else system_prompt
        self.json_schema = DEFAULT_JSON_SCHEMA if json_schema is True else json_schema
        self.generator_kwargs = dict(generator_kwargs or {})
        if self.json_schema:
            self.generator_kwargs["format"] = self.json_schema

    def _generate(self, prompt: str) -> str:
        """Send one prompt to the local Ollama server."""
        messages = [
            {"role": "system", "content": self.system_prompt},
            {"role": "user", "content": prompt},
        ]
        kwargs = {"model": self.model, "messages": messages, **self.generator_kwargs}
        return chat(**kwargs).message.content

OpenAI

Bases: LLMRepresentation

Using the OpenAI Chat Completions API to generate topic labels.

Uses OpenAI's Chat Completions API, which OpenAI-compatible servers such as vLLM, LM Studio and Ollama serve as well, with Structured Outputs for schema-adherent JSON generation.

For an overview see: https://developers.openai.com/api/docs/guides/text https://developers.openai.com/api/docs/guides/structured-outputs

Parameters:

Name Type Description Default
client

A openai.OpenAI client

required
model str

Model to use within OpenAI, defaults to "gpt-6-luna".

'gpt-6-luna'
prompt str | None

The prompt to be used in the model. If no prompt is given, bertopic.representation._prompts.DEFAULT_CHAT_PROMPT is used instead. NOTE: Use "[KEYWORDS]" and "[DOCUMENTS]" in the prompt to decide where the keywords and documents need to be inserted.

None
system_prompt str | None

The system prompt to be used in the model. If no system prompt is given, bertopic.representation._prompts.DEFAULT_SYSTEM_PROMPT is used instead.

None
json_schema dict[str, Any] | bool

A dictionary representing the JSON schema to enforce structured output. If set to True, a default schema will be used (bertopic.representation._prompts.DEFAULT_JSON_SCHEMA). Uses OpenAI's Structured Outputs via the response_format parameter with json_schema type for strict schema adherence.

False
generator_kwargs dict[str, Any] | None

Kwargs passed to client.chat.completions.create for fine-tuning the output.

None
delay_in_seconds float | None

The delay in seconds between consecutive prompts in order to prevent RateLimitErrors.

None
exponential_backoff bool

Retry requests with a random exponential backoff. A short sleep is used when a rate limit error is hit, then the requests is retried. Increase the sleep length if errors are hit until 10 unsuccessful requests. If True, overrides delay_in_seconds.

False
nr_docs int

The number of documents to pass to OpenAI if a prompt with the ["DOCUMENTS"] tag is used.

4
diversity float | None

The diversity of documents to pass to OpenAI. Accepts values between 0 and 1. A higher values results in passing more diverse documents whereas lower values passes more similar documents.

None
doc_length int | None

The maximum length of each document. If a document is longer, it will be truncated. If None, the entire document is passed.

None
tokenizer str | Callable | None

The tokenizer used to calculate to split the document into segments used to count the length of a document. * If tokenizer is 'char', then the document is split up into characters which are counted to adhere to doc_length * If tokenizer is 'whitespace', the document is split up into words separated by whitespaces. These words are counted and truncated depending on doc_length * If tokenizer is 'vectorizer', then the internal CountVectorizer is used to tokenize the document. These tokens are counted and truncated depending on doc_length * If tokenizer is a callable, then that callable is used to tokenize the document. These tokens are counted and truncated depending on doc_length

None

Usage:

To use this, you will need to install the openai package first:

pip install openai

Then, get yourself an API key and use OpenAI's API as follows:

import openai
from bertopic.representation import OpenAI
from bertopic import BERTopic

# Create your representation model
client = openai.OpenAI(api_key=MY_API_KEY)
representation_model = OpenAI(client)

# Use the representation model in BERTopic on top of the default pipeline
topic_model = BERTopic(representation_model=representation_model)

You can also use a custom prompt:

prompt = "I have the following documents: [DOCUMENTS] \nThese documents are about the following topic: '"
representation_model = OpenAI(client, prompt=prompt)

You can also use structured output with a JSON schema:

representation_model = OpenAI(client, json_schema=True)
Source code in bertopic\representation\_openai.py
 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
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
class OpenAI(LLMRepresentation):
    r"""Using the OpenAI Chat Completions API to generate topic labels.

    Uses OpenAI's Chat Completions API, which OpenAI-compatible servers such as vLLM,
    LM Studio and Ollama serve as well, with Structured Outputs for schema-adherent
    JSON generation.

    For an overview see:
    https://developers.openai.com/api/docs/guides/text
    https://developers.openai.com/api/docs/guides/structured-outputs

    Arguments:
        client: A `openai.OpenAI` client
        model: Model to use within OpenAI, defaults to `"gpt-6-luna"`.
        prompt: The prompt to be used in the model. If no prompt is given,
                `bertopic.representation._prompts.DEFAULT_CHAT_PROMPT` is used instead.
                NOTE: Use `"[KEYWORDS]"` and `"[DOCUMENTS]"` in the prompt
                to decide where the keywords and documents need to be
                inserted.
        system_prompt: The system prompt to be used in the model. If no system prompt is given,
                       `bertopic.representation._prompts.DEFAULT_SYSTEM_PROMPT` is used instead.
        json_schema: A dictionary representing the JSON schema to enforce structured output.
                     If set to True, a default schema will be used (`bertopic.representation._prompts.DEFAULT_JSON_SCHEMA`).
                     Uses OpenAI's Structured Outputs via the `response_format` parameter with
                     `json_schema` type for strict schema adherence.
        generator_kwargs: Kwargs passed to `client.chat.completions.create`
                          for fine-tuning the output.
        delay_in_seconds: The delay in seconds between consecutive prompts
                          in order to prevent RateLimitErrors.
        exponential_backoff: Retry requests with a random exponential backoff.
                             A short sleep is used when a rate limit error is hit,
                             then the requests is retried. Increase the sleep length
                             if errors are hit until 10 unsuccessful requests.
                             If True, overrides `delay_in_seconds`.
        nr_docs: The number of documents to pass to OpenAI if a prompt
                 with the `["DOCUMENTS"]` tag is used.
        diversity: The diversity of documents to pass to OpenAI.
                   Accepts values between 0 and 1. A higher
                   values results in passing more diverse documents
                   whereas lower values passes more similar documents.
        doc_length: The maximum length of each document. If a document is longer,
                    it will be truncated. If None, the entire document is passed.
        tokenizer: The tokenizer used to calculate to split the document into segments
                   used to count the length of a document.
                       * If tokenizer is 'char', then the document is split up
                         into characters which are counted to adhere to `doc_length`
                       * If tokenizer is 'whitespace', the document is split up
                         into words separated by whitespaces. These words are counted
                         and truncated depending on `doc_length`
                       * If tokenizer is 'vectorizer', then the internal CountVectorizer
                         is used to tokenize the document. These tokens are counted
                         and truncated depending on `doc_length`
                       * If tokenizer is a callable, then that callable is used to tokenize
                         the document. These tokens are counted and truncated depending
                         on `doc_length`

    Usage:

    To use this, you will need to install the openai package first:

    `pip install openai`

    Then, get yourself an API key and use OpenAI's API as follows:

    ```python
    import openai
    from bertopic.representation import OpenAI
    from bertopic import BERTopic

    # Create your representation model
    client = openai.OpenAI(api_key=MY_API_KEY)
    representation_model = OpenAI(client)

    # Use the representation model in BERTopic on top of the default pipeline
    topic_model = BERTopic(representation_model=representation_model)
    ```

    You can also use a custom prompt:

    ```python
    prompt = "I have the following documents: [DOCUMENTS] \nThese documents are about the following topic: '"
    representation_model = OpenAI(client, prompt=prompt)
    ```

    You can also use structured output with a JSON schema:

    ```python
    representation_model = OpenAI(client, json_schema=True)
    ```
    """

    def __init__(
        self,
        client,
        model: str = "gpt-6-luna",
        prompt: str | None = None,
        system_prompt: str | None = None,
        generator_kwargs: dict[str, Any] | None = None,
        delay_in_seconds: float | None = None,
        exponential_backoff: bool = False,
        nr_docs: int = 4,
        diversity: float | None = None,
        doc_length: int | None = None,
        tokenizer: str | Callable | None = None,
        *,
        json_schema: dict[str, Any] | bool = False,
    ):
        super().__init__(
            prompt=DEFAULT_JSON_PROMPT if json_schema else (prompt or DEFAULT_CHAT_PROMPT),
            nr_docs=nr_docs,
            diversity=diversity,
            doc_length=doc_length,
            tokenizer=tokenizer,
        )

        # OpenAI specific parameters
        self.client = client
        self.model = model
        self.system_prompt = system_prompt if system_prompt is not None else DEFAULT_SYSTEM_PROMPT
        self.json_schema = DEFAULT_JSON_SCHEMA if json_schema is True else json_schema
        self.delay_in_seconds = delay_in_seconds
        self.exponential_backoff = exponential_backoff
        self.generator_kwargs = dict(generator_kwargs or {})
        if self.json_schema:
            # OpenAI's strict mode only accepts schemas that allow no other properties
            self.generator_kwargs["response_format"] = {
                "type": "json_schema",
                "json_schema": {
                    "name": "Topic",
                    "schema": {"additionalProperties": False, **self.json_schema},
                    "strict": True,
                },
            }

    def _generate(self, prompt: str) -> str | None:
        """Send one prompt through OpenAI's Chat Completions API."""
        if self.delay_in_seconds:
            time.sleep(self.delay_in_seconds)

        messages = [
            {"role": "system", "content": self.system_prompt},
            {"role": "user", "content": prompt},
        ]
        kwargs = {"model": self.model, "messages": messages, **self.generator_kwargs}
        try:
            if self.exponential_backoff:
                response = chat_completions_with_backoff(self.client, **kwargs)
            else:
                response = self.client.chat.completions.create(**kwargs)
        except openai.BadRequestError as error:
            # A prompt OpenAI refuses for its content gets no label, rather than stopping the fit (#2342)
            if error.code == "content_filter":
                return None
            raise
        return response.choices[0].message.content

PartOfSpeech

Bases: BaseRepresentation

Extract Topic Keywords based on their Part-of-Speech.

DEFAULT_PATTERNS = [ [{'POS': 'ADJ'}, {'POS': 'NOUN'}], [{'POS': 'NOUN'}], [{'POS': 'ADJ'}] ]

From candidate topics, as extracted with c-TF-IDF, find documents that contain keywords found in the candidate topics. These candidate documents then serve as the representative set of documents from which the Spacy model can extract a set of candidate keywords for each topic.

These candidate keywords are first judged by whether they fall within the DEFAULT_PATTERNS or the user-defined pattern. Then, the resulting keywords are sorted by their respective c-TF-IDF values.

Parameters:

Name Type Description Default
model str | Language

The Spacy model to use

'en_core_web_sm'
top_n_words int

The top n words to extract

10
pos_patterns list[str] | None

Patterns for Spacy to use. See https://spacy.io/usage/rule-based-matching

None

Usage:

from bertopic.representation import PartOfSpeech
from bertopic import BERTopic

# Create your representation model
representation_model = PartOfSpeech("en_core_web_sm")

# Use the representation model in BERTopic on top of the default pipeline
topic_model = BERTopic(representation_model=representation_model)

You can define custom POS patterns to be extracted:

pos_patterns = [
            [{'POS': 'ADJ'}, {'POS': 'NOUN'}],
            [{'POS': 'NOUN'}], [{'POS': 'ADJ'}]
]
representation_model = PartOfSpeech("en_core_web_sm", pos_patterns=pos_patterns)
Source code in bertopic\representation\_pos.py
 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
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
class PartOfSpeech(BaseRepresentation):
    """Extract Topic Keywords based on their Part-of-Speech.

    DEFAULT_PATTERNS = [
                [{'POS': 'ADJ'}, {'POS': 'NOUN'}],
                [{'POS': 'NOUN'}],
                [{'POS': 'ADJ'}]
    ]

    From candidate topics, as extracted with c-TF-IDF,
    find documents that contain keywords found in the
    candidate topics. These candidate documents then
    serve as the representative set of documents from
    which the Spacy model can extract a set of candidate
    keywords for each topic.

    These candidate keywords are first judged by whether
    they fall within the DEFAULT_PATTERNS or the user-defined
    pattern. Then, the resulting keywords are sorted by
    their respective c-TF-IDF values.

    Arguments:
        model: The Spacy model to use
        top_n_words: The top n words to extract
        pos_patterns: Patterns for Spacy to use.
                      See https://spacy.io/usage/rule-based-matching

    Usage:

    ```python
    from bertopic.representation import PartOfSpeech
    from bertopic import BERTopic

    # Create your representation model
    representation_model = PartOfSpeech("en_core_web_sm")

    # Use the representation model in BERTopic on top of the default pipeline
    topic_model = BERTopic(representation_model=representation_model)
    ```

    You can define custom POS patterns to be extracted:

    ```python
    pos_patterns = [
                [{'POS': 'ADJ'}, {'POS': 'NOUN'}],
                [{'POS': 'NOUN'}], [{'POS': 'ADJ'}]
    ]
    representation_model = PartOfSpeech("en_core_web_sm", pos_patterns=pos_patterns)
    ```
    """

    def __init__(
        self,
        model: str | Language = "en_core_web_sm",
        top_n_words: int = 10,
        pos_patterns: list[str] | None = None,
    ):
        if isinstance(model, str):
            self.model = spacy.load(model)
        elif isinstance(model, Language):
            self.model = model
        else:
            raise ValueError(
                "Make sure that the Spacy model that you"
                "pass is either a string referring to a"
                "Spacy model or a Spacy nlp object."
            )

        self.top_n_words = top_n_words

        if pos_patterns is None:
            self.pos_patterns = [
                [{"POS": "ADJ"}, {"POS": "NOUN"}],
                [{"POS": "NOUN"}],
                [{"POS": "ADJ"}],
            ]
        else:
            self.pos_patterns = pos_patterns

    def extract_topics(
        self,
        topic_model: "BERTopic",
        corpus: Corpus,
        topic_representations: dict[int, Keywords],
        c_tf_idf: csr_matrix,
        embeddings: np.ndarray = None,
    ) -> dict[int, Keywords]:
        """Extract topics.

        Arguments:
            topic_model: A BERTopic model
            corpus: The input documents including (calculated) embeddings
            topic_representations: The candidate topic representations
            c_tf_idf: The topic c-TF-IDF representation, with a row per topic in `topic_representations`
            embeddings: Not used

        Returns:
            updated_topics: Updated topic representations
        """
        matcher = Matcher(self.model.vocab)
        matcher.add("Pattern", self.pos_patterns)

        # Build a lookup of documents per topic for efficient filtering
        docs_by_topic = {}
        for doc, topic_id in zip(corpus.documents, corpus.topics):
            docs_by_topic.setdefault(topic_id, []).append(doc)

        candidate_topics = {}
        for topic, keywords_repr in topic_representations.items():
            keywords = keywords_repr.words

            # Extract candidate documents
            candidate_documents = []
            topic_docs = docs_by_topic.get(topic, [])
            for keyword in keywords:
                selection = [doc for doc in topic_docs if keyword in doc]
                candidate_documents.extend(selection[:2])
            candidate_documents = list(set(candidate_documents))

            # Extract keywords
            docs_pipeline = self.model.pipe(candidate_documents)
            updated_keywords = []
            for doc in docs_pipeline:
                matches = matcher(doc)
                for _, start, end in matches:
                    updated_keywords.append(doc[start:end].text)
            candidate_topics[topic] = list(set(updated_keywords))

        # Scikit-Learn Deprecation: get_feature_names is deprecated in 1.0
        # and will be removed in 1.2. Please use get_feature_names_out instead.
        if version.parse(sklearn_version) >= version.parse("1.0.0"):
            words = list(topic_model.vectorizer_model.get_feature_names_out())
        else:
            words = list(topic_model.vectorizer_model.get_feature_names())

        # Match updated keywords with c-TF-IDF values
        words_lookup = dict(zip(words, range(len(words))))
        updated_topics = {}

        for index, (topic, candidate_keywords) in enumerate(candidate_topics.items()):
            word_indices = np.sort(
                [words_lookup.get(keyword) for keyword in candidate_keywords if keyword in words_lookup]
            )
            vals = c_tf_idf[:, word_indices][index]
            indices = np.argsort(np.array(vals.todense().reshape(1, -1))[0])[-self.top_n_words :][::-1]
            vals = np.sort(np.array(vals.todense().reshape(1, -1))[0])[-self.top_n_words :][::-1]
            topic_words = [(words[word_indices[index]], val) for index, val in zip(indices, vals)]
            if len(topic_words) < self.top_n_words:
                topic_words += [("", 0) for _ in range(self.top_n_words - len(topic_words))]
            updated_topics[topic] = Keywords(topic_words)

        return updated_topics

extract_topics(topic_model, corpus, topic_representations, c_tf_idf, embeddings=None)

Extract topics.

Parameters:

Name Type Description Default
topic_model BERTopic

A BERTopic model

required
corpus Corpus

The input documents including (calculated) embeddings

required
topic_representations dict[int, Keywords]

The candidate topic representations

required
c_tf_idf csr_matrix

The topic c-TF-IDF representation, with a row per topic in topic_representations

required
embeddings ndarray

Not used

None

Returns:

Name Type Description
updated_topics dict[int, Keywords]

Updated topic representations

Source code in bertopic\representation\_pos.py
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
def extract_topics(
    self,
    topic_model: "BERTopic",
    corpus: Corpus,
    topic_representations: dict[int, Keywords],
    c_tf_idf: csr_matrix,
    embeddings: np.ndarray = None,
) -> dict[int, Keywords]:
    """Extract topics.

    Arguments:
        topic_model: A BERTopic model
        corpus: The input documents including (calculated) embeddings
        topic_representations: The candidate topic representations
        c_tf_idf: The topic c-TF-IDF representation, with a row per topic in `topic_representations`
        embeddings: Not used

    Returns:
        updated_topics: Updated topic representations
    """
    matcher = Matcher(self.model.vocab)
    matcher.add("Pattern", self.pos_patterns)

    # Build a lookup of documents per topic for efficient filtering
    docs_by_topic = {}
    for doc, topic_id in zip(corpus.documents, corpus.topics):
        docs_by_topic.setdefault(topic_id, []).append(doc)

    candidate_topics = {}
    for topic, keywords_repr in topic_representations.items():
        keywords = keywords_repr.words

        # Extract candidate documents
        candidate_documents = []
        topic_docs = docs_by_topic.get(topic, [])
        for keyword in keywords:
            selection = [doc for doc in topic_docs if keyword in doc]
            candidate_documents.extend(selection[:2])
        candidate_documents = list(set(candidate_documents))

        # Extract keywords
        docs_pipeline = self.model.pipe(candidate_documents)
        updated_keywords = []
        for doc in docs_pipeline:
            matches = matcher(doc)
            for _, start, end in matches:
                updated_keywords.append(doc[start:end].text)
        candidate_topics[topic] = list(set(updated_keywords))

    # Scikit-Learn Deprecation: get_feature_names is deprecated in 1.0
    # and will be removed in 1.2. Please use get_feature_names_out instead.
    if version.parse(sklearn_version) >= version.parse("1.0.0"):
        words = list(topic_model.vectorizer_model.get_feature_names_out())
    else:
        words = list(topic_model.vectorizer_model.get_feature_names())

    # Match updated keywords with c-TF-IDF values
    words_lookup = dict(zip(words, range(len(words))))
    updated_topics = {}

    for index, (topic, candidate_keywords) in enumerate(candidate_topics.items()):
        word_indices = np.sort(
            [words_lookup.get(keyword) for keyword in candidate_keywords if keyword in words_lookup]
        )
        vals = c_tf_idf[:, word_indices][index]
        indices = np.argsort(np.array(vals.todense().reshape(1, -1))[0])[-self.top_n_words :][::-1]
        vals = np.sort(np.array(vals.todense().reshape(1, -1))[0])[-self.top_n_words :][::-1]
        topic_words = [(words[word_indices[index]], val) for index, val in zip(indices, vals)]
        if len(topic_words) < self.top_n_words:
            topic_words += [("", 0) for _ in range(self.top_n_words - len(topic_words))]
        updated_topics[topic] = Keywords(topic_words)

    return updated_topics

TextGeneration

Bases: LLMRepresentation

Text generation with transformers.

Parameters:

Name Type Description Default
model Union[str, pipeline]

A transformers pipeline that should be initialized as "text-generation", for example pipeline('text-generation', model='google/gemma-4-E2B-it'). If a string is passed, a "text-generation" pipeline is created for it. A model with a chat template, as instruction-tuned models have, is sent the prompt as a chat message, so its template is applied and only its answer is kept.

required
prompt str | None

The prompt to be used in the model. If no prompt is given, bertopic.representation._prompts.DEFAULT_CHAT_PROMPT is used for a model with a chat template, and DEFAULT_COMPLETION_PROMPT for any other model. NOTE: Use "[KEYWORDS]" and "[DOCUMENTS]" in the prompt to decide where the keywords and documents need to be inserted.

None
pipeline_kwargs dict[str, Any]

Kwargs that you can pass to the transformers.pipeline when it is called.

{}
random_state int

A random state to be passed to transformers.set_seed

42
nr_docs int

The number of documents to pass to the model if a prompt with the ["DOCUMENTS"] tag is used.

4
diversity float | None

The diversity of documents to pass to the model. Accepts values between 0 and 1. A higher values results in passing more diverse documents whereas lower values passes more similar documents.

None
doc_length int | None

The maximum length of each document. If a document is longer, it will be truncated. If None, the entire document is passed.

None
tokenizer str | Callable | None

The tokenizer used to calculate to split the document into segments used to count the length of a document. * If tokenizer is 'char', then the document is split up into characters which are counted to adhere to doc_length * If tokenizer is 'whitespace', the document is split up into words separated by whitespaces. These words are counted and truncated depending on doc_length * If tokenizer is 'vectorizer', then the internal CountVectorizer is used to tokenize the document. These tokens are counted and truncated depending on doc_length * If tokenizer is a callable, then that callable is used to tokenize the document. These tokens are counted and truncated depending on doc_length

None

Usage:

To use a model from the Hugging Face Hub:

from bertopic.representation import TextGeneration
from bertopic import BERTopic

# Create your representation model
representation_model = TextGeneration('google/gemma-4-E2B-it')

# Use the representation model in BERTopic on top of the default pipeline
topic_model = BERTopic(representation_model=representation_model)

You can use a custom prompt and decide where the keywords should be inserted by using the [KEYWORDS] or documents with the [DOCUMENTS] tag:

from transformers import pipeline
from bertopic.representation import TextGeneration

prompt = (
    "I have a topic described by the following keywords: [KEYWORDS]. "
    "Give a short label for this topic, and nothing else."
)

# Create your representation model
generator = pipeline('text-generation', model='google/gemma-4-E2B-it')
representation_model = TextGeneration(generator, prompt=prompt)
Source code in bertopic\representation\_textgeneration.py
  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
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
class TextGeneration(LLMRepresentation):
    """Text generation with transformers.

    Arguments:
        model: A transformers pipeline that should be initialized as "text-generation",
               for example `pipeline('text-generation', model='google/gemma-4-E2B-it')`. If a string
               is passed, a "text-generation" pipeline is created for it. A model with
               a chat template, as instruction-tuned models have, is sent the prompt as
               a chat message, so its template is applied and only its answer is kept.
        prompt: The prompt to be used in the model. If no prompt is given,
                `bertopic.representation._prompts.DEFAULT_CHAT_PROMPT` is used for a model
                with a chat template, and `DEFAULT_COMPLETION_PROMPT` for any other model.
                NOTE: Use `"[KEYWORDS]"` and `"[DOCUMENTS]"` in the prompt
                to decide where the keywords and documents need to be
                inserted.
        pipeline_kwargs: Kwargs that you can pass to the transformers.pipeline
                         when it is called.
        random_state: A random state to be passed to `transformers.set_seed`
        nr_docs: The number of documents to pass to the model if a prompt
                 with the `["DOCUMENTS"]` tag is used.
        diversity: The diversity of documents to pass to the model.
                   Accepts values between 0 and 1. A higher
                   values results in passing more diverse documents
                   whereas lower values passes more similar documents.
        doc_length: The maximum length of each document. If a document is longer,
                    it will be truncated. If None, the entire document is passed.
        tokenizer: The tokenizer used to calculate to split the document into segments
                   used to count the length of a document.
                       * If tokenizer is 'char', then the document is split up
                         into characters which are counted to adhere to `doc_length`
                       * If tokenizer is 'whitespace', the document is split up
                         into words separated by whitespaces. These words are counted
                         and truncated depending on `doc_length`
                       * If tokenizer is 'vectorizer', then the internal CountVectorizer
                         is used to tokenize the document. These tokens are counted
                         and truncated depending on `doc_length`
                       * If tokenizer is a callable, then that callable is used to tokenize
                         the document. These tokens are counted and truncated depending
                         on `doc_length`

    Usage:

    To use a model from the Hugging Face Hub:

    ```python
    from bertopic.representation import TextGeneration
    from bertopic import BERTopic

    # Create your representation model
    representation_model = TextGeneration('google/gemma-4-E2B-it')

    # Use the representation model in BERTopic on top of the default pipeline
    topic_model = BERTopic(representation_model=representation_model)
    ```

    You can use a custom prompt and decide where the keywords should
    be inserted by using the `[KEYWORDS]` or documents with the `[DOCUMENTS]` tag:

    ```python
    from transformers import pipeline
    from bertopic.representation import TextGeneration

    prompt = (
        "I have a topic described by the following keywords: [KEYWORDS]. "
        "Give a short label for this topic, and nothing else."
    )

    # Create your representation model
    generator = pipeline('text-generation', model='google/gemma-4-E2B-it')
    representation_model = TextGeneration(generator, prompt=prompt)
    ```
    """

    def __init__(
        self,
        model: Union[str, pipeline],
        prompt: str | None = None,
        pipeline_kwargs: dict[str, Any] = {},
        random_state: int = 42,
        nr_docs: int = 4,
        diversity: float | None = None,
        doc_length: int | None = None,
        tokenizer: str | Callable | None = None,
    ):
        super().__init__(
            prompt=prompt,
            nr_docs=nr_docs,
            diversity=diversity,
            doc_length=doc_length,
            tokenizer=tokenizer,
        )

        # Transformer specific parameters
        self.random_state = random_state
        set_seed(random_state)

        if isinstance(model, str):
            self.model = pipeline("text-generation", model=model)
        elif isinstance(model, Pipeline):
            self.model = model
        else:
            raise ValueError(
                "Make sure that the HF model that you"
                "pass is either a string referring to a"
                "HF model or a `transformers.pipeline` object."
            )
        self.pipeline_kwargs = pipeline_kwargs

        # A base model without a chat template completes a short prompt instead, as on 0.17
        if prompt is None and not self.model.tokenizer.chat_template:
            self.prompt = DEFAULT_COMPLETION_PROMPT

    def _generate(self, prompt: str) -> str | None:
        """Ask the transformers pipeline, as a chat when the model has a chat template."""
        # A chat model answers a conversation, which the pipeline writes in the model's own chat template
        if self.model.tokenizer.chat_template:
            messages = [{"role": "user", "content": prompt}]

            # The answer is a message only within the full conversation, and an empty one has no content
            output = self.model(messages, **{**self.pipeline_kwargs, "return_full_text": True})
            return output[0]["generated_text"][-1].get("content")

        # Any other model continues the prompt
        return self.model(prompt, **self.pipeline_kwargs)[0]["generated_text"].replace(prompt, "")

ZeroShotClassification

Bases: BaseRepresentation

Zero-shot Classification on topic keywords with candidate labels.

Parameters:

Name Type Description Default
candidate_topics list[str]

A list of labels to assign to the topics if they exceed min_prob

required
model str

A transformers pipeline that should be initialized as "zero-shot-classification". For example, pipeline("zero-shot-classification", model="facebook/bart-large-mnli")

'facebook/bart-large-mnli'
pipeline_kwargs dict[str, Any]

Kwargs that you can pass to the transformers.pipeline when it is called. NOTE: Use {"multi_label": True} to extract multiple labels for each topic.

{}
min_prob float

The minimum probability to assign a candidate label to a topic

0.8

Usage:

from bertopic.representation import ZeroShotClassification
from bertopic import BERTopic

# Create your representation model
candidate_topics = ["space and nasa", "bicycles", "sports"]
representation_model = ZeroShotClassification(candidate_topics, model="facebook/bart-large-mnli")

# Use the representation model in BERTopic on top of the default pipeline
topic_model = BERTopic(representation_model=representation_model)
Source code in bertopic\representation\_zeroshot.py
 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
112
113
114
115
class ZeroShotClassification(BaseRepresentation):
    """Zero-shot Classification on topic keywords with candidate labels.

    Arguments:
        candidate_topics: A list of labels to assign to the topics if they
                          exceed `min_prob`
        model: A transformers pipeline that should be initialized as
               "zero-shot-classification". For example,
               `pipeline("zero-shot-classification", model="facebook/bart-large-mnli")`
        pipeline_kwargs: Kwargs that you can pass to the transformers.pipeline
                         when it is called. NOTE: Use `{"multi_label": True}`
                         to extract multiple labels for each topic.
        min_prob: The minimum probability to assign a candidate label to a topic

    Usage:

    ```python
    from bertopic.representation import ZeroShotClassification
    from bertopic import BERTopic

    # Create your representation model
    candidate_topics = ["space and nasa", "bicycles", "sports"]
    representation_model = ZeroShotClassification(candidate_topics, model="facebook/bart-large-mnli")

    # Use the representation model in BERTopic on top of the default pipeline
    topic_model = BERTopic(representation_model=representation_model)
    ```
    """

    def __init__(
        self,
        candidate_topics: list[str],
        model: str = "facebook/bart-large-mnli",
        pipeline_kwargs: dict[str, Any] = {},
        min_prob: float = 0.8,
    ):
        self.candidate_topics = candidate_topics
        if isinstance(model, str):
            self.model = pipeline("zero-shot-classification", model=model)
        elif isinstance(model, Pipeline):
            self.model = model
        else:
            raise ValueError(
                "Make sure that the HF model that you"
                "pass is either a string referring to a"
                "HF model or a `transformers.pipeline` object."
            )
        self.pipeline_kwargs = pipeline_kwargs
        self.min_prob = min_prob

    def extract_topics(
        self,
        topic_model: "BERTopic",
        corpus: Corpus,
        topic_representations: dict[int, Keywords],
        c_tf_idf: csr_matrix,
        embeddings: np.ndarray = None,
    ) -> dict[int, Keywords]:
        """Extract topics.

        Arguments:
            topic_model: Not used
            corpus: Not used
            topic_representations: The candidate topic representations
            c_tf_idf: Not used
            embeddings: Not used

        Returns:
            updated_topics: Updated topic representations
        """
        # Classify topics
        topic_descriptions = [
            " ".join(topic_representations[topic].words) for topic in topic_representations.keys()
        ]
        classifications = self.model(topic_descriptions, self.candidate_topics, **self.pipeline_kwargs)

        # Extract labels
        updated_topics = {}
        for topic, classification in zip(topic_representations.keys(), classifications):
            topic_description = []

            # Multi-label assignment
            if self.pipeline_kwargs.get("multi_label"):
                for label, score in zip(classification["labels"], classification["scores"]):
                    if score > self.min_prob:
                        topic_description.append((label, score))

            # Single label assignment
            elif classification["scores"][0] > self.min_prob:
                topic_description = [(classification["labels"][0], classification["scores"][0])]

            # Fall back to original representation if no labels passed the threshold
            if len(topic_description) == 0:
                updated_topics[topic] = topic_representations[topic]
            else:
                if len(topic_description) < 10:
                    topic_description += [("", 0) for _ in range(10 - len(topic_description))]
                updated_topics[topic] = Keywords(topic_description)

        return updated_topics

extract_topics(topic_model, corpus, topic_representations, c_tf_idf, embeddings=None)

Extract topics.

Parameters:

Name Type Description Default
topic_model BERTopic

Not used

required
corpus Corpus

Not used

required
topic_representations dict[int, Keywords]

The candidate topic representations

required
c_tf_idf csr_matrix

Not used

required
embeddings ndarray

Not used

None

Returns:

Name Type Description
updated_topics dict[int, Keywords]

Updated topic representations

Source code in bertopic\representation\_zeroshot.py
 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
112
113
114
115
def extract_topics(
    self,
    topic_model: "BERTopic",
    corpus: Corpus,
    topic_representations: dict[int, Keywords],
    c_tf_idf: csr_matrix,
    embeddings: np.ndarray = None,
) -> dict[int, Keywords]:
    """Extract topics.

    Arguments:
        topic_model: Not used
        corpus: Not used
        topic_representations: The candidate topic representations
        c_tf_idf: Not used
        embeddings: Not used

    Returns:
        updated_topics: Updated topic representations
    """
    # Classify topics
    topic_descriptions = [
        " ".join(topic_representations[topic].words) for topic in topic_representations.keys()
    ]
    classifications = self.model(topic_descriptions, self.candidate_topics, **self.pipeline_kwargs)

    # Extract labels
    updated_topics = {}
    for topic, classification in zip(topic_representations.keys(), classifications):
        topic_description = []

        # Multi-label assignment
        if self.pipeline_kwargs.get("multi_label"):
            for label, score in zip(classification["labels"], classification["scores"]):
                if score > self.min_prob:
                    topic_description.append((label, score))

        # Single label assignment
        elif classification["scores"][0] > self.min_prob:
            topic_description = [(classification["labels"][0], classification["scores"][0])]

        # Fall back to original representation if no labels passed the threshold
        if len(topic_description) == 0:
            updated_topics[topic] = topic_representations[topic]
        else:
            if len(topic_description) < 10:
                topic_description += [("", 0) for _ in range(10 - len(topic_description))]
            updated_topics[topic] = Keywords(topic_description)

    return updated_topics