Skip to content

maroon: PCA and k-means over Lagoon arrays (Hoon) - #90

Merged
sigilante merged 1 commit into
mainfrom
sigilante/maroon-models
Sep 21, 2026
Merged

sigilante merged 1 commit into
mainfrom
sigilante/maroon-models

Conversation

@sigilante

Copy link
Copy Markdown
Collaborator

Step 3 of the foundation work list in #86: the first classical models. Hoon only, no jet hints.

Stacked on #88 (base sigilante/saloon-linalg), which is itself on #87 — +pca uses #88's +svd, and everything else uses #87's axis arms. Review those first; this diff is only the maroon/ files.

This is the classical-ML line. The 2024 tinygrad autodiff port in /lib/tinygrad is a separate track (#85) and is untouched here.

/lib/maroon

A dataset is an n x d %i754 ray, one row per sample. Labels and indices are %uint bloq-6 rays — exactly what +argmin-dim returns — so the pieces compose without reshaping.

arm what it does
+pca, +pca-transform components from the SVD of the centred data
+kmeans Lloyd's algorithm from given centroids
+kmeans-pp, +kmeans-fit k-means++ seeding, and seeding + Lloyd
+assign, +update the two halves of a Lloyd step
+cov, +center, +col-mean, +inertia the supporting statistics
+row, +set-row row access, since a dataset is row-major

Everything is written in terms of whole-array Lagoon operations (+cdist-sq, +argmin-dim, +mean-dim, +mmul, +broadcast-to) rather than per-element loops, so the jets those arms eventually grow carry the models with them. +update is the one exception — it gathers rows per cluster.

Three decisions worth flagging:

  • PCA decomposes the centred data, not the covariance. Forming Xc^T*Xc squares the condition number, and Saloon's one-sided Jacobi already returns singular values in descending order, which is the order PCA wants. +cov is still there for callers who want the matrix itself.
  • Lloyd's iteration is split from the seeding. +kmeans takes its starting centroids and is deterministic; +kmeans-pp is the random part and threads an /lib/rand generator explicitly. That split is what makes this testable — the iteration is asserted exactly, the seeding only for properties that hold whatever it draws.
  • +kmeans stops at the exact fixed point of the assignment rather than a tolerance, and reports the iteration count so a caller can tell convergence from exhaustion. An empty cluster keeps its previous centroid.

Tests: 40 arms

maroon-pca.hoon (19) and maroon-kmeans.hoon (21).

Exact, compared as whole rays: column means and centring; a covariance that is exactly representable at ddof=0; a dataset on the x-axis whose first principal component is e1 exactly (its centred columns are already orthogonal, so Jacobi performs no rotation at all); cluster means that are exact halves; +assign tie-breaking to the lower centroid; an empty cluster keeping its centroid; inertia; maxit=0; and the crash paths.

Approximate, against scikit-learn: the rotated 45-degree case, its explained variance (13.333333333333336), and two-component extraction. Note the explained variance is approximate even on exact data, because it comes back through Saloon's Newton-iteration square root — a true variance of 1 returns as 0.9999999999999998, so that expectation is a tolerance rather than bits.

The seeding tests assert what must hold regardless of the draw: same seed → same centroids, seeds are distinct samples drawn from the data, and +kmeans-fit recovers a well-separated partition (compared up to label permutation, since cluster numbering is arbitrary).

maroon/tools/ml_check.py re-derives all of it — 23 checks, 0 failures — mirroring each operation in fractions.Fraction to prove the exact cases never round, with numpy/sklearn for the rest. It also corrected the +pca doccord example I first wrote.

Verification

Fresh fakeship at hoon-135/zuse-408 on a vere built from urbit/vere#1057 (plus the @rq fix from #89), %num desk carrying lagoon, saloon, maroon and the libmath/librand deps:

python3 maroon/tools/ml_check.py      # 23 checks, 0 failures
-test /=num=/tests/lib/maroon-pca     # 19 tests, ok=%.y
-test /=num=/tests/lib/maroon-kmeans  # 21 tests, ok=%.y
-test /=num=/tests/lib                # 402 OK / 4 failed  (was 362 / 4 before this PR)

+40 OK, no change to the failures: the 4 are the pre-existing saloon-unum posit transcendentals, which fail on every runtime including vere 4.6.

Housekeeping

The desk was missing symlinks for libs it pulls in transitively (complex, unum, rand, i754rand, sur/rand) — added, per the convention that everything external to maroon is a symlink. All 14 now resolve.

Not in this PR

Jets; GMM, spectral clustering and kNN; and regression, which wants QR first. Per #86 the next step is Cholesky/QR plus linear and ridge regression.

Refs #86.

🤖 Generated with Claude Code

https://claude.ai/code/session_01TgBnKsUPYzkoPZonjePnaq

Step 3 of #86's foundation list, stacked on #88 (whose +svd
backs +pca) and #87 (whose axis arms back everything else).  Hoon only, no jet
hints.  This is the classical-ML line; the tinygrad autodiff port in
/lib/tinygrad is a separate track (#85) and is untouched.

/lib/maroon is new.  A dataset is an n x d %i754 ray, one row per sample;
labels and indices are %uint bloq-6 rays, exactly what +argmin-dim returns, so
the pieces compose without reshaping.  Arms are written in terms of whole-array
Lagoon operations (+cdist-sq, +argmin-dim, +mean-dim, +mmul, +broadcast-to)
rather than per-element loops, so the jets those arms grow will carry the
models with them; +update, which gathers rows per cluster, is the exception.

  +pca / +pca-transform  principal components from the SVD of the CENTRED data
                         rather than an eigendecomposition of the covariance,
                         which would square the condition number.  Saloon's
                         one-sided Jacobi already sorts descending, which is
                         the order PCA wants.
  +kmeans                Lloyd's algorithm from given centroids: deterministic,
                         stopping at the exact fixed point of the assignment
                         (not a tolerance) and reporting the iteration count.
  +kmeans-pp             k-means++ seeding via /lib/i754rand's alias table,
                         threading the generator explicitly so a seed
                         reproduces its centroids.  +kmeans-fit is the two.
  +assign +update        the two halves of a Lloyd step; an empty cluster keeps
                         its previous centroid.
  +cov +center +col-mean +inertia +row +set-row

Splitting the deterministic Lloyd iteration from the random seeding is what
makes this testable: the iteration is asserted exactly, the seeding only for
the properties that hold whatever it draws.

Tests: 40 arms.  The exact cases compare whole rays -- cluster means that are
exact halves, a dataset whose first principal component is e1 exactly (its
centred columns are already orthogonal, so Jacobi performs no rotation), an
exactly representable covariance at ddof=0.  The approximate ones compare
against scikit-learn within a tolerance; the explained VARIANCE is always
approximate even on exact data, since it returns through Saloon's
Newton-iteration square root (a true variance of 1 comes back as
0.9999999999999998).  maroon/tools/ml_check.py re-derives all of it (23
checks) with exact rational arithmetic plus numpy/sklearn references, and
corrected the +pca doccord example I first wrote.

Also symlinks the libs the desk was missing (complex, unum, rand, i754rand,
sur/rand), per the convention that everything external to maroon is a symlink.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TgBnKsUPYzkoPZonjePnaq
@sigilante
sigilante changed the base branch from sigilante/saloon-linalg to main September 21, 2026 23:10
@sigilante
sigilante merged commit 047df07 into main Sep 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant