M9 refinement: operate geometry descriptor in mean-centered (isotropic) embedding space
The nomic-embed-text space over the corpus is strongly anisotropic (mean pairwise cosine ~0.55), which compresses cosine-based domain separation almost to nothing so the design-doc s5 operators (distance/overlap/Wasserstein) cannot discriminate. Subtracting the global mean of the normalized embeddings restores isotropy (mean pairwise cosine ~0) and sharpens the operators. - add GeoMeanCache (engram_geo_mean_build / _maybe_refresh / _vec / _free): a store-derived centering offset over the embed-eligible set, cached and refreshed on significant drift; lives in geometry.c, not the store. - engram_geometry_descriptor gains an optional global_mean: when supplied the centroid, per-member cosine distance, and co-registration run in centered space (GM=zeros reproduces the legacy raw path exactly). - co-registration choice (b): the ANN query stays in raw unit space (index unchanged) since centering is a rigid translation that ~preserves neighborhood membership; only the descriptor statistics move to the centered frame. Covariance/axes/radius are translation-invariant and therefore unchanged. - test: synthetic ground-truth suite stays green (PERF + ASan/UBSan), plus new centered/raw/mean-cache assertions. - add bench_discrimination.c (env-gated, read-only, skips in CI): on a copy of the real store the two-domain overlap operator drops 1.13 -> 0.008 and cross-centroid cosine 0.899 -> 0.003 after centering, Euclid distance unchanged (translation-invariant control). No change to activation/retrieval behavior; wiring geometry into retrieval is a separate, behavior-changing cutover.
This commit is contained in:
@@ -50,7 +50,18 @@ typedef struct {
|
||||
int dim;
|
||||
/* ── anchor ── */
|
||||
char* hub_id; /* highest-centrality member: the relational hub */
|
||||
float* centroid; /* v̄ ∈ R^dim: mean of L2-normalized member embs */
|
||||
float* centroid; /* v̄ ∈ R^dim: mean of the member embeddings in the
|
||||
* frame the descriptor operated in. When centered
|
||||
* (global_mean != NULL) this is the CENTERED
|
||||
* centroid (mean of L2-normalized embs minus the
|
||||
* global mean): the neighborhood's location in the
|
||||
* isotropic/whitened frame. Add global_mean back to
|
||||
* recover the raw prototype point. When uncentered
|
||||
* it is the raw mean of L2-normalized member embs. */
|
||||
float* global_mean; /* the centering offset actually applied (dim floats),
|
||||
* or NULL if the descriptor ran in raw space. The §5
|
||||
* operators (distance/overlap/Wasserstein) are only
|
||||
* discriminative in the centered frame — see notes. */
|
||||
/* ── shape (compact covariance): top principal axes + extents ── */
|
||||
int n_axes;
|
||||
GeoAxis* axes; /* orientation + extents of the ellipsoid */
|
||||
@@ -84,6 +95,39 @@ typedef struct {
|
||||
* kcore_k=0 (auto), top_axes=8, max_members=400. */
|
||||
void engram_geo_default_params(GeoParams* p);
|
||||
|
||||
/* ── Global-mean cache (mean-centering / whitening the anisotropic emb space) ──
|
||||
* The nomic-embed-text space over the engram corpus is strongly ANISOTROPIC:
|
||||
* every embedding sits in a narrow cone (mean pairwise cosine ~0.55), which
|
||||
* compresses cosine-based domain separation almost to nothing. Subtracting the
|
||||
* GLOBAL MEAN of the (L2-normalized) embeddings recenters the cloud on the
|
||||
* origin (mean pairwise cosine -> ~0), restoring isotropy so the §5 operators
|
||||
* discriminate. The mean is a store-level derived quantity, like the ANN index:
|
||||
* built once from the paged store, cached, and refreshed when the embedded set
|
||||
* drifts. It lives here (not in the store) so this stays a contained, read-only
|
||||
* addition; a runtime owns one GeoMeanCache per open store alongside its VIndex. */
|
||||
typedef struct GeoMeanCache GeoMeanCache;
|
||||
|
||||
/* Scan every live node in `store` and compute the mean of the L2-normalized
|
||||
* embeddings over the embed-eligible set (nodes carrying an emb vector; the
|
||||
* unembedded telemetry/system nodes are skipped). Returns a malloc'd cache, or
|
||||
* NULL on error / no embedded nodes. The offset vector is NOT renormalized — it
|
||||
* is a translation, applied by subtraction. */
|
||||
GeoMeanCache* engram_geo_mean_build(EngramPagedStore* store);
|
||||
|
||||
/* The cached offset (dim floats) — pass to engram_geometry_descriptor as
|
||||
* global_mean. Valid until the cache is freed/refreshed. */
|
||||
const float* engram_geo_mean_vec(const GeoMeanCache* c);
|
||||
int engram_geo_mean_dim(const GeoMeanCache* c);
|
||||
uint64_t engram_geo_mean_count(const GeoMeanCache* c); /* #embedded nodes used */
|
||||
|
||||
/* Recompute the mean IN PLACE iff the embedded-node count has drifted by more
|
||||
* than `frac` (e.g. 0.10 = 10%) since the cache was built — "recompute on
|
||||
* significant change". Returns 1 if it rebuilt, 0 if unchanged, <0 on error. */
|
||||
int engram_geo_mean_maybe_refresh(GeoMeanCache* c, EngramPagedStore* store,
|
||||
double frac);
|
||||
|
||||
void engram_geo_mean_free(GeoMeanCache* c);
|
||||
|
||||
/* Compute the geometry descriptor of the neighborhood grown from seed_ids.
|
||||
* READ-ONLY over store + vindex.
|
||||
* store — an opened store (borrowed; not modified).
|
||||
@@ -92,13 +136,25 @@ void engram_geo_default_params(GeoParams* p);
|
||||
* (vids[node_id] == store id). Required iff vindex != NULL.
|
||||
* n_vids — length of vids.
|
||||
* params — NULL to use engram_geo_default_params.
|
||||
* global_mean — optional centering offset (dim floats, from engram_geo_mean_*).
|
||||
* When non-NULL the SEMANTIC geometry is computed in mean-centered
|
||||
* (isotropic) space: every normalized member emb has global_mean
|
||||
* subtracted before the centroid / cosine-distance / co-registration
|
||||
* math, so those operators discriminate. NULL = raw space (legacy).
|
||||
* NOTE: the ANN neighbor query still runs in RAW unit-vector space —
|
||||
* centering is a rigid translation that ~preserves neighborhood
|
||||
* MEMBERSHIP, so the index needs no rebuild; only the descriptor
|
||||
* STATISTICS move to the centered frame (co-registration choice (b)).
|
||||
* The eigen/covariance shape (axes, radius) is translation-invariant
|
||||
* and therefore identical in either frame.
|
||||
* Returns a malloc'd descriptor (free with engram_geo_free), or NULL on error
|
||||
* (no seeds resolvable, OOM). */
|
||||
GeoDescriptor* engram_geometry_descriptor(
|
||||
EngramPagedStore* store, VIndex* vindex,
|
||||
char** vids, int n_vids,
|
||||
const char* const* seed_ids, size_t n_seeds,
|
||||
const GeoParams* params);
|
||||
const GeoParams* params,
|
||||
const float* global_mean);
|
||||
|
||||
void engram_geo_free(GeoDescriptor* g);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user