t-SNE & UMAP Math Basics
These methods turn high-dimensional neighbours into 2D neighbours by matching who is close to whom, so clusters survive the flattening while far distances do not.
Why Does This Exist?
High-dimensional data cannot be plotted. Gene vectors, image features, and text embeddings live in hundreds of dimensions, and linear principal component analysis can only rotate and project. On curved manifolds it crushes distinct groups into the middle, the crowding problem: there is not enough room in 2D for every far pair to stay far.
t-SNE and UMAP exist for one job: visualization that keeps neighbours. They convert distances into neighbour probabilities up high and into layout probabilities down low, then move the 2D points until the two agree. The dimensionality reduction hub covers the linear parents; this page covers the neighbour-matching pair and what their maps do not promise.
The one idea you need first
A neighbour probability turns a distance into a chance of being picked as a neighbour: close means likely, far means unlikely. t-SNE uses a Gaussian up high, so probability falls fast with squared distance, and a heavy-tailed t-distribution down low, so moderate 2D distances still count. UMAP uses a fuzzy-graph version of the same idea with cross-entropy. The loss that compares the two distributions is Kullback-Leibler divergence.
Think of It Like This
Uncrumpling a marked paper ball without tearing it
Crumple a paper ball with ink marks on it, then flatten it on a desk without tearing. Marks that touched on the ball should sit together on the desk. Marks from opposite sides of the ball will land somewhere, but their desk distance means little about the ball.
t-SNE and UMAP are that flattening as springs: strong springs pull likely neighbours together, weak repulsion spreads everything else just enough to fit. The analogy stops at sizes: on your desk a big blob means more ink, while in t-SNE blob size and gap width are layout accidents, not measurements.
How It Actually Works
Both methods run three steps: affinities up high, affinities down low, then a match.
1. High-dimensional affinities. t-SNE centres a Gaussian on each point and converts the distance to point into a conditional probability with bandwidth :
Here is the high-dimensional distance, sets the local neighbourhood width chosen by perplexity, and the denominator normalizes over all others. UMAP replaces this with fuzzy simplicial weights, but the role is identical: near means large weight.
2. Low-dimensional affinities. t-SNE lays out 2D points and scores their distances with a Student t-distribution with one degree of freedom:
Here is the 2D map distance. The heavy tail is the crowding fix: moderately far map neighbours still attract, so points need not crush into the centre to satisfy far high-dimensional pairs.
3. Match by gradient descent. t-SNE minimizes the Kullback-Leibler divergence between and . UMAP minimizes fuzzy cross-entropy without global normalization, which runs faster and keeps more global shape. Both optimize the 2D positions directly, so there is no reusable function for new points without refitting or a parametric extension.
Worked example
One pair at distance with . High-dimensional unnormalized weight: . Low-dimensional unnormalized weight: . The same pair reads 0.135 up high and 0.2 down low: the heavy tail lifts the 2D score, which is exactly why the map can place this pair at distance 2 without paying the Gaussian penalty. Normalization over all pairs turns these weights into the and above; the optimizer then nudges the 2D distance until they agree.
Code
The two kernels side by side for the worked distance:
import numpy as np
def high_dim_affinity(d_ij: float, sigma: float) -> float: """Unnormalized Gaussian neighbour weight used up high by t-SNE.""" return float(np.exp(-(d_ij**2) / (2 * sigma**2)))
def low_dim_affinity(d_ij: float) -> float: """Unnormalized t-distribution weight used down low by t-SNE.""" return float(1 / (1 + d_ij**2))
dist = 2.0print(round(high_dim_affinity(dist, 1.0), 3)) # -> 0.135print(round(low_dim_affinity(dist), 3)) # -> 0.2The prints match the hand computation: and .
Watch Out For
Reading cluster sizes and gaps as measurements
In a t-SNE plot, blob diameter and the whitespace between blobs carry almost no meaning. The loss preserves local neighbourhood order, not global metric distances, and perplexity plus random initialization reshapes gaps freely. The symptom is a story about one cluster being twice another or two groups drifting apart over runs. Compare membership, never ruler distances, and rerun with several seeds before believing a gap.
Tuning perplexity until the map looks nice and stopping
Perplexity sets each , hence the neighbourhood size the Gaussian sees. Too small fragments real clusters into confetti; too large merges distinct manifolds into one blob. The symptom is structure that appears and vanishes as the dial turns. Sweep it (for example 5, 30, 50 on thousands of points), check that claimed clusters survive the sweep and survive in the high-dimensional -nearest graph, and report the value used.
The Quick Version
- t-SNE models high-dimensional neighbours with Gaussians and 2D neighbours with a heavy-tailed t-distribution.
- It places points by minimizing KL divergence between those two neighbourhood distributions.
- UMAP keeps the neighbour idea on fuzzy graphs with cross-entropy, running faster and holding more global shape.
- A pair at distance 2 reads 0.135 up high but 0.2 down low, which is the crowding fix in numbers.
- Neither map preserves sizes or gaps: read who sits with whom, not how far blobs sit apart.