Embedding cost
The question: how well does this projection do at what a particular method was trying to do?
These three are the objective functions of Sammon mapping, curvilinear component analysis and neighbour retrieval visualiser. Each weights distance error differently, and the weighting is the interesting part.
The measures
Section titled “The measures”Writing for the distance between two points in the data and for the distance in the projection, all three penalise the same error and differ only in what they multiply it by:
▲ higher is better · ▼ lower is better · △▽ hollow: unbounded, so the number is comparable only between projections of the same data.
| measure | weights errors by | range | |
|---|---|---|---|
sammonStress | ▽ | — short distances matter most | |
curvilinearStress | ▽ | , a kernel on the projected distance | |
nerv | ▽ | a trade-off between missing and false neighbours, set by |
The numerators are the published sums; the denominators are sickle’s, and make the
values comparable across datasets. Both value fields are the ratio, so a number
quoted here will not match a bare transcription of the classical formula.
curvilinearStress also returns raw, the unnormalised numerator.
Sammon skips pairs at , which would divide by zero.
The difference between the two is worth stating plainly, and it is visible in which distance the weight depends on. Sammon weights by , the original distance: getting a genuinely-close pair wrong is expensive. CCA weights by , the projected one: an error is only expensive if the points ended up drawn near each other. That makes CCA tolerant of tearing a manifold apart — which is the point, since a tear is often the only way to flatten something.
Using them
Section titled “Using them”Sammon comes with the default pass. CCA needs its kernel width, NeRV is its own pass:
const a = analyze(hd, ld, { ccaLambda: 1 });
sammonStress(a.embedding).value; // 0.0238curvilinearStress(a.embedding).value; // 0.5316
const p = nervPass(hd, ld, { lambda: 0.5, perplexity: 30 });nerv(p).value;An empty scatterplot.
- Sammon stress
- —
- parameters
- none
- points
- —
- cost
O(1)
Stress that weights short distances most heavily.
ccaKernel picks . The default "exponential" is , a
smooth decay; "step" is the original bounded kernel, for
and 0 beyond.
NeRV, and what λ does
Section titled “NeRV, and what λ does”NeRV frames the projection as an information retrieval problem: a viewer looking at the plot retrieves neighbours, and can make two kinds of mistake.
- — only false neighbours are penalised. Precision, .
- — only missing neighbours are penalised. Recall, . This is what SNE and t-SNE minimise.
- — the default here, both equally.
nerv() returns recall and precision alongside the combined value, so you can see
which side the cost is coming from rather than only the total.
The per-point is fitted in the high-dimensional space by bisection to match the requested perplexity, then reused for the projection — the paper’s formulation, and the reason this is the slowest pass in the library.
Per-point values
Section titled “Per-point values”sammonStress and curvilinearStress have localKind: "sum" — the entries are
contributions in the measure’s own units and add up to the total.
Both measures are lower is better and both entries are quantities of error, so a
large entry does mean more of the distortion sits on that point: it is worth looking
at. The converse is where these arrays mislead. A small entry says only that this
measure is not charging for that point, and the weighting decides what counts.
Sammon weights by , so a point flung a long way breaks pairs that were
already distant and is charged little; CCA weights by , so it forgives
a tear outright. On a projection with six points teleported into the wrong class,
curvilinearStress bills them 24× the average point and sammonStress only 4× —
and Sammon ranks three of those six below average, because the pairs they broke
were long ones.
The reading that survives both: a large entry is a lead, a small one is not an
all-clear. Reading a score has the
general form of this, for share as well as sum.
NeRV’s recall and precision are scalars — the mean per-point and
over the dataset, not per-point arrays. What it does give per point is
local, with localKind: "mean": each entry is that point’s own
, so the entries average to
the reported value and say which points are being misrepresented. The split by
direction is only available in aggregate. The pass object also carries sigma, the
fitted per-point bandwidth.
sammonStress and curvilinearStress are read-outs. NeRV is with a
bisection per point on top, and benefits most from nervAsync.
Verification
Section titled “Verification”All three are checked against the published definition, not against a reference implementation — there is no canonical one to compare against. Each is verified against an independent naive transcription of the formula in the test suite, and NeRV’s σ search is checked to produce exactly the requested perplexity. This is the second verification tier; see Verification.