Groups documents, or their sentences, clauses, or phrases (segment), into
topics by deterministic k-means on their Sentence-BERT embeddings. Supply
either a model to encode the text or a precomputed embedding matrix.
Topics are summarized with representative documents and class-based TF-IDF
terms. This is embedding-based topic discovery, not a probabilistic LDA model.
Usage
topics(
text,
n_topics,
column = NULL,
model = NULL,
embeddings = NULL,
batch_size = 32L,
iter_max = 100L,
n_terms = 10L,
n_representatives = 5L,
stop_words = default_stop_words(),
min_term_frequency = 1L,
min_token_length = 2L,
weighting = c("ctfidf", "bm25"),
reduce_frequent_words = FALSE,
stem = FALSE,
keep_embeddings = TRUE,
seeds = NULL,
seed_embeddings = NULL,
fixed_seeds = FALSE,
cores = 1L,
numbers = c("keep", "remove"),
roman_numerals = c("keep", "remove"),
section_numbers = c("keep", "remove"),
segment = c("document", "sentence", "clause", "phrase"),
max_tokens = NULL,
merge_below = 0L,
min_content = 0,
label = c("representative", "terms")
)Arguments
- text
Character vector containing one document per element, a data frame together with
column, or a preparedtopic_corpus().- n_topics
Number of semantic topics. Must be at least two.
- column
When
textis a data frame, the name of the column holding the documents to model. Every other column is carried along with each document in the fitted document table, and rows whose text is missing, blank, or a bibliographic placeholder ("[No abstract available]") are dropped. Precomputedembeddingsmay keep one row per supplied row; the dropped rows are removed from them too. LeaveNULLfor a character vector.- model
A loaded sbert_model, a pinned model name from
models(), orNULLfor the default model. Ignored whenembeddingsare supplied.- embeddings
Optional numeric matrix with one row per document; when supplied, no model is loaded or used.
- batch_size
Batch size passed to
encode()whenmodelis used.- iter_max
Maximum deterministic k-means iterations.
- n_terms
Maximum class-based TF-IDF terms returned per topic.
- n_representatives
Maximum representative documents per topic.
- stop_words
Character vector excluded from topic terms. Use
character()to disable stop-word filtering.- min_term_frequency
Minimum corpus-wide token frequency.
- min_token_length
Minimum Unicode character length for a topic token.
- weighting
Class-based term-weighting scheme.
"ctfidf"(default) usestf * log(1 + A / f_x);"bm25"uses the BM25 inverse-frequency varianttf * log(1 + (A - f_x + 0.5) / (f_x + 0.5)), which more aggressively down-weights terms shared across topics (Mendonca and Figueira 2025).- reduce_frequent_words
Whether to square-root the within-topic term frequency before weighting, damping very frequent words.
- stem
Whether to collapse inflected forms (for example
animalsandanimal) onto a shared Porter stem before scoring, displaying the most frequent surface form of each stem. Requires theSnowballCpackage.- keep_embeddings
Whether to retain normalized document embeddings in the returned object.
- seeds
Optional guided topics: a character vector (or list of character vectors, collapsed with spaces) of seed words or topic descriptions — one element per seeded topic. Seeds are embedded and become the first
length(seeds)cluster centroids; remaining topics (up ton_topics) are initialized away from the seeds. Seeded topics keep their position — topic i is seed i, never reordered by size — and a namedseedsvector names those topics' labels. With precomputedembeddings, supplyseed_embeddingsas well.- seed_embeddings
Optional matrix of seed embeddings (one row per seed, same dimension as the document embeddings); required when
seedsis used together with precomputedembeddings.- fixed_seeds
When
TRUE, seed centroids are frozen and documents are simply assigned to the nearest seed (zero-shot classification into the seeded topics;n_topicsmust equal the number of seeds, and topics may be empty). WhenFALSE(default), seeds only initialize the clustering and the data can move the centroids.- cores
Number of forked worker processes used to tokenize documents for term scoring. Default
1(serial). Values above one useparallel::mclapplyon Unix-alikes and fall back to serial on Windows or for small corpora; the tokenization — and therefore every result — is identical regardless of the count. Ignored when a preparedtopic_corpus()is supplied (its tokens are already computed).- numbers
How to treat purely numeric tokens (years, counts) in topic terms.
"keep"(default) keeps them;"remove"drops tokens made only of digits, while retaining alphanumerics such ascovid19.- roman_numerals
How to treat Roman-numeral tokens (chapter, section, and list markers such as
ii,iv,xii)."keep"(default) keeps them;"remove"drops canonical Roman numerals up to 100. The bound is deliberate: larger Roman numerals collide with common abbreviations that are also valid numerals (ml,mm,cc,ci,cv), which are always kept. A few small numerals that are also words (iv,vi,xl) are removed when this is on.- section_numbers
How to treat section, reference, and list numbering.
"keep"(default) keeps it;"remove"strips, before tokenizing, both multi-level indices (1.2.3,4.5.6.7— three or more dot-separated groups) and enumeration or list markers (1.,2.,figure 12.— a standalone one- or two-digit number followed by a period). Decimals (3.14), four-digit years (2020.), hyphenated numbers (covid-19.), and larger counts are left untouched, so genuine values survive even whennumbers = "keep". These filter only the topic terms; the document text and its embedding are untouched. To clean the source text itself — list markers, reference noise, junk characters — before encoding, seeclean_corpus().- segment
The unit the model is fitted on.
"document"(default) embeds each document whole."sentence","clause", or"phrase"first splits every document withsegment()at that level and fits the topics on the segments, so long documents are modeled in full instead of being truncated to the encoder's context window, and a document can span several topics. The fitted document table then has one row per segment with its parentdocument_idandsegmentposition;topic_sizes()counts by segment or by document,topic_gamma()recovers each document's topic mixture from the stored segments, and predict() segments new text the same way. A precomputedembeddingsmatrix must then have one row per segment, aligned withsegment(text, level = segment, ...).- max_tokens
Optional cap on segment length, passed to
segment(). When amodelis used, the budget counts that model's exact tokens; with precomputedembeddingsit counts words. Only with a segmented fit.- merge_below
Re-join segments shorter than this many words into their neighbor, passed to
segment(). Only with a segmented fit.- min_content
Minimum alphabetic-content ratio for a segment to be kept, passed to
segment(). Only with a segmented fit.- label
How each topic is named in
labelcolumns and plots."representative"(default) uses the topic's most central text, shortened to 60 characters;"terms"joins its three most distinctive words ("box / red / blue"). Namedseedsoverride either.
Value
An object of class sbert_topic_model: a list with
documentsOne row per modeled unit (document, or segment for a segmented fit):
document_id,document_name,segment(segmented fits only),text,topic,label, anddistance(cosine distance to the assigned centroid), followed by any carriedcolumndata.topicsOne row per topic:
topic,label,n_documents,proportion, andwithinss.terms,representativesThe tables returned by terms() and, ranked by distance, by
representatives().centersNumeric matrix of unit-length topic centroids, one row per topic.
embeddingsThe normalized unit embeddings, or
NULLwhenkeep_embeddings = FALSE.diagnosticsSums of squares (
totss,tot_withinss,betweenss),iterations, andalgorithm.model,settingsThe embedding model identity and every setting the fit was made with, which later verbs reuse.
Read it through its verbs rather than its fields: topic_sizes(),
terms(), representatives(),
summary(),
predict(), topic_membership(),
topic_gamma(), and plot().
Details
Each topic is named by its most central unit: the document or
segment nearest the topic's centroid, shortened to 60 characters. The
topics come from the meaning of whole texts, so a text names a topic
better than its top words do; label = "terms" names topics by their
three most distinctive words instead. Read more examples per topic with
representatives(), and its typical words with
terms(). Term scores use the class-based
TF-IDF weighting tf * log(1 + A / f_x), where tf is the
frequency of a term normalized within its topic, f_x is the term's
frequency across all topics, and A is the mean topic length (a real
number, matching Mendonca and Figueira (2025), Eq. 1). Set
weighting = "bm25" and/or reduce_frequent_words = TRUE for the BM25 and
square-root variants (their Eq. 2 to 4). The built-in tokenizer preserves
Unicode alphanumeric tokens and internal apostrophes, but does not perform
language-specific word segmentation for unspaced CJK text.
Errors
Raises an error when n_topics is below two or exceeds
the number of modeled units, when embeddings does not have one row per
unit, or when column is missing or not found in a data frame. With a
prepared topic_corpus(), passing model, embeddings, or a different
segment is an error.
References
Reimers, N. and Gurevych, I. (2019). Sentence-BERT: Sentence embeddings using Siamese BERT-networks. Proceedings of EMNLP-IJCNLP 2019, 3982–3992. doi:10.18653/v1/D19-1410
Grootendorst, M. (2022). BERTopic: Neural topic modeling with a class-based TF-IDF procedure. arXiv:2203.05794.
Mendonca, M. and Figueira, A. (2025). Modeling political discourse with Sentence-BERT and BERTopic. arXiv:2510.22904.
Lloyd, S. P. (1982). Least squares quantization in PCM. IEEE Transactions on Information Theory, 28(2), 129–137. doi:10.1109/TIT.1982.1056489
Examples
text <- c(
"Cats chase mice", "Dogs chase balls",
"Stocks and bonds trade", "Markets price shares"
)
embeddings <- rbind(c(1, 0), c(0.9, 0.1), c(0, 1), c(0.1, 0.9))
topic_model <- topics(text, 2, embeddings = embeddings)
topic_sizes(topic_model)
#> topic label n_documents proportion
#> 1 1 Cats chase mice 2 0.5
#> 2 2 Stocks and bonds trade 2 0.5
terms(topic_model, n = 3)
#> topic label term rank score frequency beta
#> 1 1 Cats chase mice chase 1 0.4620981 2 0.3333333
#> 2 1 Cats chase mice balls 2 0.3243184 1 0.1666667
#> 3 1 Cats chase mice cats 3 0.3243184 1 0.1666667
#> 4 2 Stocks and bonds trade bonds 1 0.3243184 1 0.1666667
#> 5 2 Stocks and bonds trade markets 2 0.3243184 1 0.1666667
#> 6 2 Stocks and bonds trade price 3 0.3243184 1 0.1666667
# Fit on sentences: each document is split first, and the segments are
# what the topics are made of (the embeddings here are one row per
# sentence, so no model download is needed).
documents <- c(
"Cats chase mice. Stocks and bonds trade.",
"Dogs chase balls. Markets price shares."
)
sentence_model <- topics(
documents, 2,
segment = "sentence",
embeddings = rbind(c(1, 0), c(0, 1), c(0.9, 0.1), c(0.1, 0.9))
)
representatives(sentence_model)
#> topic rank document_id segment text distance margin
#> 1 1 1 1 1 Cats chase mice. 0.001530237 0.9431694
#> 2 1 2 2 1 Dogs chase balls. 0.001530237 0.8332451
#> 3 2 1 1 2 Stocks and bonds trade. 0.001530237 0.9431694
#> 4 2 2 2 2 Markets price shares. 0.001530237 0.8332451
topic_sizes(sentence_model, by = "document")
#> topic label n_documents proportion
#> 1 1 Cats chase mice. 2 1
#> 2 2 Stocks and bonds trade. 2 1
topic_gamma(sentence_model)
#> document_id topic gamma n_segments
#> 1 1 1 0.5 2
#> 2 1 2 0.5 2
#> 3 2 1 0.5 2
#> 4 2 2 0.5 2
