sbert 0.5.2
Added
clean_corpus(), one place for the text-level cleaning that should happen before encoding, so the same clean text feeds the encoder, the topic terms, and the representatives. It repairs junk and strips non-content markup — HTML entities and tags, URLs, DOIs, email addresses, bracketed numeric citations ([12]), page references (p. 1,pp. 15-30), curly quotes, dashes, bullets, control, zero-width, BOM and replacement characters, and non-breaking spaces — then strips list markers (1.,(i),(a),(109), and bare Roman-numeral section markers such asIV.and a leadingV.) and section/reference numbering (1.2.3, and alphanumeric indices such asII.2andAA.2), applies any customremovepatterns (the extension point for domain-specific noise the generic cleaners leave, for exampleremove = "OJ No L?\\s*\\d+"), optionally removes numeric and Roman-numeral tokens, and drops documents below amin_contentalphabetic-density floor — citation lists, reference footnotes, number tables — carrying a data frame’s other columns along so metadata stays aligned. Supporting helpersstrip_list_markers()andcontent_ratio()are exported too.content_ratio()is the fraction of a string’s non-space characters that are letters, a domain-agnostic measure of prose versus reference noise (real sentences score near 1; “OJ No L 297, 24.11.1979, p. 1.” scores about 0.25), andmin_contentjudges it on the repaired text before the number strips so a stripped reference cannot slip through. Because Sentence-BERT tolerates a little noise, this targets broken text and non-content rows rather than scrubbing every token.The
numbers,roman_numerals, andsection_numbersterm filters stay ontopics(),select_topics(),topic_corpus(), andterms(): they tidy the topic labels without touching the document text or its embedding, which is the point of keeping them separate fromclean_corpus(). Text-level cleaning that would change the embedding lives inclean_corpus().representatives()and a fitted model’s built-in representatives now return distinct texts. A representative list of the same string repeated is never useful, and duplicates are common once documents are segmented.segment()splits a run-together sentence boundary — a lower-case letter, a period, then an upper-case letter, as in “results.The next” — rather than treating it as an acronym; genuine acronyms (“U.S.”) and decimals (“3.14”) are left intact.topic_gamma()now accepts either raw documents (segmented internally, as before) or the data frame returned bysegment(), used as-is. Passing your own segmentation keeps every segment option —level,max_tokens,merge_below— insegment()and means a suppliedembeddingsmatrix always lines up with the segments, instead of having to match an internal re-segmentation. The two paths give identicalgammawhen the segmentation matches.segment()gainsmax_tokens, a cap so no segment overruns an encoder’s context window and is silently truncated. An over-long segment is re-split at the finest logical boundaries — clause hinges,;,:,-, and commas — and the pieces are packed back up to the budget, so splits land on punctuation wherever possible. A run with no such boundary is chopped further, but even then the break is placed just before a function word (a coordinator, preposition, article, or relative pronoun) near the budget edge rather than mid-phrase — so “the schools” is kept together instead of stranding “the”. By default the budget counts whitespace words (offline, deterministic). Passing a loadedmodelcounts that model’s exact sub-word tokens instead, somax_tokenscan be the model’s real limit; token-counted segmentation runs serially since the tokenizer is not forked.NULL(default) leaves segments uncapped, byte-identical to before.-
encode()gains acacheargument: a path to a content-addressed embedding store. Each document is keyed by a SHA-256 digest of its own text together with the model identity, revision, dimension, maximum length, pooling method andnormalizesetting, so only documents with no matching entry are encoded and the file is updated with whatever was newly computed. Corpora are usually edited rather than replaced, and encoding is about 97% of the cost of a topic workflow, so reuse dominates: re-running an unchanged 3,847-abstract corpus cost 0.07 s against 36.59 s (515x, outputidentical()), and re-running it after editing 38 documents cost 0.41 s (89x) with unchanged rows identical to the previous run and changed rows identical to a fresh encode. Reordering or subsetting a cached corpus encodes nothing at all. Duplicate documents within one call are encoded once and expanded by position — 295 of the 3,847 bundledcovidabstracts are duplicates.The key deliberately excludes
batch_sizeandsort_by_length, which move embeddings by around 1e-7 through batch composition alone; cached values are therefore stable to whatever computed them first rather than bit-identical to a fresh encode under different batching. A cache that cannot be read, or was written for a different embedding dimension, is discarded with a warning and rebuilt rather than trusted, and writes go to a temporary file and are renamed so an interrupted save cannot leave a half-written cache behind. load_model()acceptsthreads = "auto", which asks the platform for its performance-core count instead of its logical one. ONNX inference is about 88% of a batch and the only threaded part of the package, but it scales with performance cores only. Measured over three repetitions at 200, 600 and 1,500 documents on an Apple M4 (4 performance + 6 efficiency), median speedups against a single thread were 1.22x at two threads, 1.29x at four, 1.22x at six and 1.18x at eight; four threads was fastest at both larger sizes (1.34x at 1,500 documents), after which the efficiency cores contribute contention rather than throughput."auto"selects that knee and is capped at 8 so a library never seizes a whole machine uninvited. Raising the thread count has no numerical consequence: output wasidentical()in all 45 runs. The default stays1so results remain reproducible across machines and within the two-core limit that check environments impose. Note that threaded timings are noisy — run-to-run spread reached 26% at the smallest size against under 3% single-threaded — so a single timing run is not sufficient evidence for tuning.-
Mask-aware mean pooling is about 6.3x faster, and
encode()about 1.18x faster end to end, with identical output on platforms whosesum()androwsum()accumulate in the same precision.pool()built a second 24 MB copy of every batch withsweep()and then summed it withapply(., c(1, 3), sum), an R-level loop over a 3-D array. Because an R array is column-major and its first two margins are adjacent, the batch re-dimensions to(batch * sequence) x hiddenwithout moving any data, so masking becomes one recycled multiply and the per-document sum becomes a single C-levelrowsum(). Verified asidentical()against the previous formulation across 60,000+ shape, mask, method and normalization combinations and on real ONNX output.One caveat, and it is a real one: R’s
sum()accumulates into a long double whilerowsum()accumulates into a double. On builds with extended-precision long doubles — common x86-64 Linux and Windows builds, where.Machine$sizeof.longdoubleis 16 — the two can therefore differ in the last bits on inputs with heavy cancellation. Equality was confirmed on arm64 macOS, wheresizeof.longdoubleis 8 and the question does not arise; on extended-precision builds expect agreement to around 1e-16 rather than to the bit. Embedding values of that magnitude do not change any topic assignment.The element-scanning part of
pool()’s validation is now skipped on the internal encoding path, where the mask has already been validated by the tokenizer step. The structural checks are kept — dimension agreement is O(1) and its removal would have let a wrongly shaped ONNX output recycle silently into the wrong number of rows. Finiteness is now checked on the pooled result rather than the raw batch: the same guarantee at roughly 1/256 the cost, since any non-finite token value must surface there (an unmaskedInfsums toInf, a masked one becomesInf * 0 = NaN).pool()itself is unchanged for direct callers and still validates in full. encode()andtopic_gamma()gainsort_by_length(defaultFALSE). Every sequence in a batch is padded to the longest member, so a batch costs the model its maximum length rather than its mean. Grouping inputs of similar length into the same batch shrinks that padding: at clause level 60.1% of all token work is padding in input order against 1.6% when sorted, measured at 1.89x faster fortopic_gamma(level = "clause")and 1.08x for whole documents, which vary less in length. The permutation is inverted before returning, so row order always matches the input. Off by default because regrouping changes batch composition, which moves embeddings by about 1e-7 — the same trade-off already documented fordedupe_segments. On the bundledcovidcorpus that drift changed nogammavalue and no document’s dominant topic, but it is opt-in rather than assumed.topics(),select_topics(),topic_corpus(), andterms()gainroman_numeralsandsection_numbers, two independent token filters alongsidenumbers.roman_numerals = "remove"drops chapter/section/list markers written as Roman numerals (ii,iv,xii), bounded at 100 so that common abbreviations that are also valid numerals —ml,mm,cc,ci,cv— are always kept.section_numbers = "remove"strips both multi-level section and reference indices (1.2.3,4.5.6.7) and enumeration or list markers (1.,2.,figure 12.— a standalone one- or two-digit number followed by a period) before tokenizing; decimals, four-digit years (2020.), hyphenated numbers (covid-19.), and larger counts survive, so genuine values are kept even undernumbers = "keep". Both default to"keep"(byte-identical to before) and are recorded on the model socoherence()stays consistent.topics(),select_topics(), andtopic_corpus()gain anumbersargument."keep"(default, unchanged) keeps numeric tokens;"remove"drops tokens made only of digits — years, counts — from topic terms while retaining alphanumerics such ascovid19. The setting is recorded on the model socoherence()stays consistent. Custom stop words already compose through thestop_wordsargument of these verbs andkeywords();stop_words(add =)keeps the defaults plus your terms, a bare vector replaces the list, andcharacter()disables filtering. Documented with examples.The topic tokenizer is about 1.5x faster: a single-pass base-R
strsplitsplit replaces the previousgregexpr+regmatchespair. Output is byte-identical (same token grammar, verified across Unicode, apostrophe, and empty-document edge cases) and no dependency is added. This speeds uptopics(),select_topics(),topic_corpus(), andcoherence(), and stacks with thecoresargument.segment()andtopic_gamma()gain acoresargument. Splitting documents into sentences or clauses is the dominant non-encoding cost of sentence-leveltopic_gamma(), and it is per-document independent, so forking it across cores gives a several-fold speed-up (about 4-5x on eight cores) with byte-identical output. Encoding is parallelized separately viaload_model(threads =).topic_gamma()gainsdedupe_segments(defaultFALSE). Real corpora often repeat many segments (boilerplate sentences, short clauses); enabling this encodes each distinct segment once and expands by position, roughly halving the encoding cost on such corpora. Off by default because encoding a smaller batched set can shift a rare borderline assignment at the ~1e-7 level; with the staticpotion-base-8Mmodel the result is identical.topics(),select_topics(),topic_corpus(), andcoherence()gain acoresargument for parallel tokenization;select_topics()also fits its independent candidates in parallel. Because document tokenization and per-candidate fitting are deterministic and order-preserving, results are byte-identical for any core count —coreschanges only speed, never output. Parallelism usesparallel::mclapplyon Unix-alikes and falls back to serial on Windows and for small corpora; the default iscores = 1. On an 8-core machine a five-count sweep on 15,000 documents drops from about 12 s to 7 s.Added
topic_corpus(): prepare a corpus once — embedding every document and tokenizing it for term scoring — and reuse it across many models.topics()andselect_topics()accept the prepared corpus in place of raw text and skip both the encoding and the tokenization, which previously repeated on every call. A sweep or hand-written loop over topic counts now costs one tokenization (and one encoding) plus the per-model clustering, rather than repeating the whole corpus pass per model; the speedup grows with the number of models fitted. Results are byte-identical to preparing the corpus inside each call, andselect_topics()now shares one prepared corpus across all candidates internally.select_topics()no longer re-tokenizes the corpus insidecoherence()on every candidate — the dominant cost of a sweep.coherence()gains an optionaltoken_listsargument so the shared corpus tokenization is reused; scores are byte-identical. Combined with the corpus reuse above, a five-count sweep on 15,000 documents dropped from about 164 s to 12 s (~13x) with no change to any result.topics()(andselect_topics(), which calls it per candidate) no longer exhausts memory or stalls on large corpora. The farthest-point centroid initializer was rewritten from an O(n_topics^2 * n * d) recompute-everything pass — which repeatedly allocated full document-by-dimension matrices and could crash the R session — to an incremental O(n_topics * n * d) pass that keeps a running nearest-centroid distance (about 24x faster at 20k documents, with a fraction of the peak memory). The expensive up-front distinct-row dedup was dropped in favour of an exact in-loop check, and per-document centroid cosines are now vectorized. Topic assignments, terms, and representatives are bit-identical to previous versions.Added the
coviddataset: 4,170 COVID-19 research abstracts (2020-2024) on education, children, schools, and society, each with a publication year. Editorial notices (retractions, corrections, errata) are removed during preparation; 323 records carry a"[No abstract available]"placeholder and 3,671 abstracts are distinct. A companion pkgdown article, “Topic Modeling COVID-19 Research Abstracts”, walks the full sweep-fit-read workflow.Added
plot(topic_model, type = "representatives"), a per-topic ranked text list of the centroid-nearest documents (with their cosine similarity to the centroid), ann_representativesargument, andtype = "fit", a per-topic report placing all three term views (count, TF-IDF, beta) alongside the representative documents, one row per topic. Term bar panels now annotate each bar with its value.plot(type = "terms")gains abyargument selecting one or more of"score"(class-based TF-IDF, the default),"beta"(generative word probability), and"frequency"(within-topic count); several metrics draw one row per topic with a column each. A newtopicsargument restricts"terms","representatives", and"fit"to chosen topic numbers, andper_topic = TRUEdraws a separate figure for each topic instead of one gridded figure.README updated for the 0.5 API: data-frame input to
topics(), the retained-model sweep withfitted(), andterms()retuning without a refit. Adds a pointer to the tutorial vignette.Repaired README prose damaged by the 0.5.0 rename, where the English words “membership” and “diversity” had been rewritten as function names.
Corrected the model count in the README from thirteen to fourteen.
Named the
n_topicsargument in the README’sreduce_topics()example, and stopped shadowingtopic_hierarchy()with a variable of the same name.
sbert 0.5.1
-
R CMD check --as-cranis clean: 0 errors, 0 warnings, 0 notes. - Documented the
columnargument oftopics(). - Repaired a malformed
\eqnmacro in theterms()documentation. - Qualified
graphics::par()in the topic-hierarchy plot method. - Declared
withrin Suggests; the session tests use it. - Excluded
.DS_Store,TODO.mdand knitr caches from the package build.
sbert 0.5.0
Breaking: the sbert_ prefix is gone
Every exported function lost its prefix. The package has never been released, so the old names were removed outright rather than deprecated:
sbert_topics() is now topics(), sbert_encode() encode(), sbert_segment() segment(), sbert_blend() blend(), sbert_keywords() keywords(), sbert_dedupe() dedupe(), sbert_coherence() coherence(), sbert_select_topics() select_topics(), sbert_representatives() representatives(), sbert_load_model() load_model(), sbert_models() models(), and so on for the cache and runtime verbs.
Six names are qualified because the bare form collides with a package this audience is likely to have attached — igraph, quanteda, purrr:
-
sbert_membership()->topic_membership() -
sbert_diversity()->topic_diversity() -
sbert_hierarchy()->topic_hierarchy() -
sbert_similarity()->topic_similarity() -
sbert_reduce()->reduce_topics() -
sbert_stopwords()->stop_words()
Two are qualified because the bare form would mask base R: sbert_gamma() -> topic_gamma(), sbert_palette() -> topic_palette().
Class names keep the prefix (sbert_topic_model, sbert_model), so S3 methods read terms.sbert_topic_model().
New: terms() retunes without refitting
Topic terms depend only on the fitted assignments and the document text, so every term setting can now be changed without repeating the clustering or the encoding:
terms(fit, n = 12, weighting = "bm25", stem = TRUE)
terms(fit, n = 8, sort_by = "beta") # words a topic uses most
terms(fit, n = 8) # words that distinguish itsort_by chooses between the class-based weight (distinctive terms, the default) and raw p(term | topic). n is applied after ordering. The returned frame carries beta, which absorbs the removed sbert_beta().
Verbs take your data frame and keep what they compute
-
topics()accepts a data frame pluscolumn =naming the text column. Remaining columns ride along into$documents. Rows whose text is missing, blank, or a[NO ABSTRACT AVAILABLE]placeholder are dropped once, and any suppliedembeddingsare subset to match. -
keep_embeddingsnow defaults toTRUE.topic_membership(),reduce_topics()andplot(type = "map")previously failed on a freshly fitted model and told the user to refit — which meant re-encoding. -
labelis carried into$documents,$termsand$representatives, so joining it back by hand is never necessary. -
representatives(fit)defaults to the fitted corpus and its stored embeddings.
select_topics() keeps every model it fits
The sweep no longer discards the models it built. fitted(sweep, n_topics =) returns one without refitting, and the result gains print, plot and as.data.frame methods while remaining a plain data frame with the same columns.
Other
- New tutorial vignette, “Topic Modeling: A Tutorial”, running end to end on the bundled
feedback_translationsdata with precomputed embeddings, so it builds without any download. - Removed
sbert_beta()(now thebetacolumn ofterms()). - Fixed two documentation blocks that could not run: an example passed a cache directory where a model name belongs, and the README referenced an unassigned
modelobject.
sbert 0.4.0
Static embeddings (instant-speed tier)
- New pinned model
potion-base-8M(Model2Vec static embeddings, MIT): encoding is a token-lookup and mean in pure base R — no ONNX session — which makes it roughly two orders of magnitude faster than transformer models (10,000 sentences in under a second) at a modest quality cost. Loaded through the same verbs (sbert_model_download(),sbert_load_model("potion-base-8M"),sbert_encode()); the safetensors weight matrix is read with a minimal base-R reader and verified by SHA-256 like every other artifact.sbert_models(detail = TRUE)gains anenginecolumn (“onnx” or “static”). Numerical parity with Pythonmodel2vec: max abs diff 3.3e-08.
Seeded (guided) topics
-
sbert_topics()gainsseeds,seed_embeddings, andfixed_seeds: supply seed words or topic descriptions and they become the first cluster centroids. Seeded topics keep their position (topic i is seed i, never reordered by size) and take the seed’s name as their label whenseedsis named. Remaining topics up ton_topicsare initialized deterministically away from the seeds. Withfixed_seeds = TRUEthe centroids never move — documents are zero-shot assigned to the nearest seed, and seeded topics may legitimately be empty.
Analysis utilities
- New
sbert_keywords(): embedding-based keyword and phrase extraction (the KeyBERT design) — each document’s own unigrams and n-grams are embedded with the same model, ranked by cosine similarity to the document, and selected by maximal marginal relevance (diversityargument) so keywords are relevant without being redundant. -
sbert_stopwords()gainsaddandremove: exclude a corpus-wide vocabulary from topic terms and keyword candidates, or un-list built-in entries. - New
sbert_select_topics(): fits the model across candidate topic counts on one shared embedding matrix and returns coherence, diversity, and between-topic variance share per count — granularity chosen by numbers, not habit. - New
sbert_hierarchy(): deterministic agglomerative merge tree of the topic centroids (cosine distance) with a tidy merge table, labeled dendrogram plot, and readable print. - New
sbert_reduce(): cuts the hierarchy at a target count and rebuilds a fullsbert_topic_model(merged assignments, recomputed centroids, terms, labels, sizes, representatives) — every downstream verb works on the reduced model unchanged.
Context-blended segment embeddings
- New
sbert_blend(): context-aware embeddings for segments — each segment keepsalphaof its context-orthogonal residual and inherits the rest of its direction from the parent document embedding, renormalized. Blending the residual (rather than the raw segment vector) avoids the near-collinearity of a segment with its own document. Accepts asbert_segment()data frame plus the original documents (encoding both), or precomputed embedding matrices. Output drops intosbert_topics(),sbert_representatives(), andpredict()unchanged. This is the package’s supported way to “mix” granularities: per unit, never by pooling units of different levels into one training set.
Representative selection and segmentation
- New
sbert_representatives(): evidence selection by distinctiveness margin (distance to the second-best centroid minus distance to the own centroid) instead of raw closeness, which systematically favors long units.rank = "distance"restores the old behavior. -
"ETC."removed from the default abbreviation gazetteer: sentence boundaries only fire at period-space-word, and mid-sentence “etc.” always carries trailing punctuation, so protecting it could only glue true sentence ends together.
Multi-model registry
Added six modern embedding models to the registry:
bge-small-en-v1.5andbge-base-en-v1.5(BAAI, CLS pooling),multilingual-e5-small(100+ languages, automatic"query: "prefix),nomic-embed-text-v1.5andjina-embeddings-v2-small-en(8,192-token context), andmxbai-embed-large-v1(1,024 dimensions). The encoder now supports CLS pooling (sbert_pool(method = "cls")) and per-model input prefixes (sbert_encode(prefix = ...)overrides the pinned default). Parity vs Python: at or below 2.9e-7 for the BGE/E5/nomic models, 8e-8 cross-runtime for jina; mxbai agrees to cosine 0.9996 with its float16-distributed reference (the pinned ONNX is the full-precision float32 artifact).Added a registry of pinned, SHA-256-verified models. Besides the default
all-MiniLM-L6-v2, the package now supports six further models:all-MiniLM-L12-v2(deeper English MiniLM),paraphrase-MiniLM-L3-v2(the smallest and fastest, 69 MB),multi-qa-MiniLM-L6-cos-v1(semantic search, 512-token inputs),paraphrase-multilingual-MiniLM-L12-v2(384 dimensions, 50+ languages),all-mpnet-base-v2(768 dimensions, higher-quality English), andparaphrase-multilingual-mpnet-base-v2(768 dimensions, 50+ languages).sbert_models()lists them tidily. Every model remains locked to an immutable revision with byte-size and SHA-256 verification, and every MiniLM addition was verified numerically identical to PythonSentenceTransformersoutput (max difference below 3e-7).sbert_model_download()andsbert_load_model()now take the model name as their first argument (sbert_load_model("all-mpnet-base-v2"));sbert_model_status()andsbert_model_remove()gained a trailingmodelargument. Calls that passedcache_dirpositionally tosbert_model_download()/sbert_load_model()must now name it.The encoder adapts to each model’s input signature: MPNet- and XLM-RoBERTa-based graphs receive two ONNX inputs (no
token_type_ids) and are padded with their own pad token; every model uses its published maximum sequence length.
Generic loader
- Added
sbert_load_custom(): loads any public Hugging Face repository that ships an ONNX encoder export and atokenizer.json, with trust-on-first-use pinning (revision, byte sizes, and SHA-256 recorded in a local manifest on first download and verified on every later load). Pooling, maximum length, and the pad token are auto-detected from the repository’s Sentence-Transformers configuration and tokenizer; the input signature and embedding dimension are read from the ONNX graph itself. Returns a regularsbert_model, so every downstream verb works unchanged. Requires thejsonlitepackage (Suggests). Unlike registry models, no parity certificate is implied.
One-verb usage
- Every verb that takes a model now accepts a pinned model name or nothing at all:
sbert_encode(text)uses the defaultall-MiniLM-L6-v2,sbert_encode(text, model = "bge-small-en-v1.5")switches by name, andsbert_topics(),predict(), andsbert_gamma()resolve models the same way. Loaded models are kept in a session cache and reused. - Missing models are never downloaded silently: interactive sessions get a one-time yes/no prompt (size and license shown); scripts must opt in via
sbert_model_download()oroptions(sbert.download = TRUE). - Attaching the package now prints a one-line orientation message with the model count, the default model, and the
sbert_models()menu pointer.
Corpus verbs
- Added
sbert_dedupe(): collapses a corpus to its distinct non-blank texts (first-appearance order) with occurrence counts, so repeated templates cannot dominate the clustering geometry while frequencies remain available as weights. - Added
sbert_topic_sizes(): topic sizes on the distinct and weighted scales in one call; the gap betweenproportionandweighted_sharemeasures how template-driven each topic is.
Inferential layer
- Added
predict()forsbert_topic_model: assigns unseen documents to the nearest fitted centroid under cosine distance, returning a tidy data frame with topics, labels, and distances. - Added
sbert_membership(): fuzzy-c-means soft topic probabilities on the stored centroids with asharpnessfuzzifier (default 1.15; the textbook value 2 degenerates toward uniform membership in high-dimensional embedding spaces). The within-document topic ranking is sharpness-invariant. - Added
sbert_beta(): the generative multinomialp(term | topic)from within-topic token counts with optional Laplace smoothing — the frequent- words view that complements (and must not be confused with) the discriminative class-based TF-IDF terms. - Added
sbert_gamma(): per-document topic distributions obtained by segmenting each document withsbert_segment()and assigning each segment to its nearest centroid — parameter-free mixed membership.
sbert 0.3.0
- Added the
feedback_translationsdataset: 8,757 multilingual AI-generated mathematics feedback messages from the Levebee educational application paired with their English translations (8,005 distinct translations, template repetition preserved), giving every example and vignette a realistic offline corpus for the embedding, segmentation, and topic-modeling workflow. - Added
sbert_segment(), deterministic rule-based text segmentation at sentence, clause (default), or phrase granularity. Sentence boundaries are guarded by a word-boundary-anchored, case-insensitive abbreviation gazetteer (sbert_abbreviations()), a decimal-number guard, and a parenthetical guard; clause level adds splits at semicolons, colons, spaced dashes, and subordinating hinges while keeping comma enumerations whole. Returns a tidy one-row-per-segment data frame and preserves letter case. Promoted from the benchmarked analysis module (F1 0.999 on realistic abbreviation-rich abstracts versus 0.80-0.82 for ICU and NLTK Punkt references).
sbert 0.2.0
- Fixed topic tokenization to normalize the curly apostrophe (U+2019) to the straight apostrophe, so contractions such as
it'sandit’sare one token. - Added optional Porter stemming to
sbert_topics()(stem = TRUE, usingSnowballC) that collapses inflected forms while displaying the most frequent surface form of each stem, giving cleaner, deduplicated topic terms. - Made NPMI coherence exact at the co-occurrence limits (terms that always or never co-occur now score 1 and -1 instead of a non-finite value).
- Added intrinsic topic-model evaluation:
sbert_coherence()(UMass and NPMI) andsbert_diversity(), with asummary()method that reports cluster separation, coherence, and diversity alongside a tidy per-topic table. - Added deterministic base-graphics visualizations via a
plot()method for topic models (type = "sizes","terms","map") and thesbert_palette()colour helper. The document map uses classical MDS on cosine distance. - Added
weighting = "bm25"andreduce_frequent_wordsoptions tosbert_topics(), implementing the BM25 and square-root class-based TF-IDF variants of Mendonca and Figueira (2025). - Fixed the class-based TF-IDF average topic length to be real-valued rather than integer-truncated, matching the published formula (Eq. 1).
sbert 0.1.0
- Added Python-free
all-MiniLM-L6-v2inference throughtokandonnxr. - Added revision-pinned, SHA-256-verified model download and cache management.
- Added mask-aware mean pooling, L2 normalization, and cosine similarity.
- Added deterministic semantic topic modeling with representative documents, Unicode tokenization, and BERTopic-style class TF-IDF term summaries.
- Added offline unit tests and optional official-model numerical parity tests.
