Skip to contents

Most smallvis methods calculate interactions between every pair of observations. That makes them convenient for experimentation on small datasets, but their time and memory requirements grow quadratically. For larger datasets, t-SNE, SSNE, HSSNE, dynamic HSSNE, SNeRV, t-distributed elastic embedding (TEE), LargeVis, UMAP, t-UMAP, Jensen-Shannon SNE, Hellinger SNE, and chi-squared SNE can combine sparse neighbor affinities with Barnes-Hut repulsion.

Run Barnes-Hut t-SNE

This configuration uses approximate nearest neighbors, a sparse neighbor graph, and two native workers. Iris is small enough to run as a quick example; the configuration becomes useful when the dense calculation is too expensive.

set.seed(42)
bh_fit <- smallvis(
  iris,
  method = "tsne",
  bh = TRUE,
  inp_kernel = "knn",
  nn = "approximate",
  n_threads = 2,
  perplexity = 30,
  exaggeration_factor = 12,
  stop_lying_iter = 250,
  Y_init = "spca",
  epoch_callback = FALSE,
  verbose = FALSE
)
species <- as.integer(iris$Species)
plot(
  bh_fit,
  col = c("#0072B2", "#D55E00", "#009E73")[species],
  pch = species,
  xlab = "t-SNE 1",
  ylab = "t-SNE 2"
)

Barnes-Hut t-SNE embedding of iris, with species distinguished by color and plotting symbol.

Barnes-Hut requires two output dimensions. The default is theta = 0.5. Smaller values open more cells and require more work. Zero evaluates the repulsive sum exactly for the chosen input graph.

The example uses binary k-nearest-neighbor affinities. Use inp_kernel = "nngaussian" for Gaussian perplexity calibration over neighbors instead. The input kernel and Barnes-Hut arguments describe these controls.

You can also use method = list("hssne", alpha = 0.5) or method = list("tee", lambda = 0.01) with "knn" or "nngaussian" affinities. HSSNE changes the tail of the output kernel, and method = "dhssne" allows alpha to change at each epoch. TEE keeps the t-SNE kernel and uses a fixed repulsion multiplier. The same input kernels work with method = "ssne", method = "snerv", method = "largevis", method = "tumap", method = "jssne", method = "hlsne", and method = "chsne".

If you already have a neighbor graph, pass its idx and dist matrices in a list as neighbors = graph and leave nn unset. This works for dense and Barnes-Hut methods using "knn", "nngaussian", or "skd" affinities. With "nngaussian", a perplexity of 30 takes 90 non-self neighbors per row on a dataset with at least 91 observations. UMAP uses smooth kNN affinities (inp_kernel = "skd"), which are also available for t-UMAP. With perplexity = 30, "skd" takes 29 non-self neighbors per row. The saved graph supplies the affinities, so you can leave pca = FALSE and let Y_init = "spca" compute the starting coordinates.

Accelerate a dense calculation

For dense t-SNE, use_cpp = TRUE enables native calibration and gradient calculations. It retains the dense pairwise representation, so this speeds up a calculation without removing its quadratic memory requirement:

native_fit <- smallvis(
  iris,
  Y_init = "spca",
  perplexity = 30,
  use_cpp = TRUE,
  n_threads = 2,
  epoch_callback = FALSE,
  verbose = FALSE
)
dim(native_fit)
## [1] 150   2

Native acceleration covers selected calculations and methods. The specialized dense gradient kernels use two output dimensions; other dimensions use the general R calculations for those steps. use_cpp = TRUE therefore does not imply that every stage or every method runs in C++.

Choose a thread count

n_threads = 0 selects serial execution. Positive values control native worker threads in the supported stages, including neighbor search. More workers can help larger inputs, but setup costs, memory bandwidth, and work still performed in R limit the improvement. Numerical libraries can also have their own thread settings.

Start with a modest worker count and measure the complete call on your data. Changing from dense affinities to a sparse neighbor graph also changes the input problem, so compare those embeddings as different approximations rather than interpreting the runtime alone.