Repository navigation
Conversation
…meter `linear_regression` called `jnp.linalg.lstsq(..., rcond=None)`, whose cutoff is `max(n_rows, n_columns) * eps`. Fitting activations, `n_rows` is the sample count, so the cutoff grew with the dataset and discarded well-conditioned directions purely because the design was tall. Under float32 the effect is large at ordinary sizes: a 66930 x 321 design with condition number 5.3e3 gets a cutoff of 7.98e-3 and loses 140 of 321 directions. Truncating singular values is regularisation, so it is now a parameter rather than a hidden default, and the amount applied is reported: - `linear_regression` takes `rcond`, defaulting to `default_rcond(n_columns)` = `n_columns * eps`. The column count governs the conditioning of the solve; the row count does not. - Both solvers report `effective_rank`, `n_singular_values`, `rcond` and `condition_number`, so a caller can see truncation instead of having to run the SVD themselves. - `score_regression` scores fitted coefficients on held-out data, which every caller wanting an out-of-sample R2 previously reimplemented. `linear_regression` and `linear_regression_svd` were two implementations of one operation, so the shared preamble and the SVD solve are extracted rather than a third path added. `rcond` is threaded through the registry validator and `LinearRegressionAnalysis` so it is reachable from a config. The float32 solve agrees with float64 to ~1e-10 once the cutoff is explicit, so the defect is the cutoff and not the precision. Fixes #201 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DYtgMenFgbPzNoN1MqxCxG
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #201.
Summary
linear_regressioncalledjnp.linalg.lstsq(..., rcond=None). That cutoff ismax(n_rows, n_columns) * eps— scaled by the row count, so fitting activations on moresamples discards more of the design. Under float32 it bites at ordinary sizes.
The change makes the cutoff an explicit, row-count-independent parameter and reports how much
of the design survived it:
linear_regression(..., rcond=...), defaulting todefault_rcond(n_columns)=n_columns * eps. The column count governs the conditioning of the solve; the row countdoes not.
rcond=0.0gives an untruncated solve.effective_rank,n_singular_values,rcond,condition_number. Issue analysis.linear_regression: lstsq rcond=None silently truncates the design with a row-count-scaled cutoff, moving peak layers #201's point that "no summary a caller can see predicts how muchis discarded" was true; now one does.
score_regression(x, y, weights, coeffs, intercept)— a held-out scorer. The fitfunctions report in-sample metrics, and every caller wanting an out-of-sample R² was
reimplementing this with its own weighting convention.
rcondis threaded through_validate_linear_regression_kwargsandLinearRegressionAnalysis, so it is reachable from a config and not only from Python.Motivation
Measured independently of the issue, building the reported design from scratch (66,930 × 321,
float32, condition number 5.32e3 — a design that needs no truncation at all):
The cutoff arithmetic reproduced the filed numbers exactly. The harm did not at first — and
chasing that gap sharpened the diagnosis. Held-out R² loss is not a function of the
condition number; it is a function of where the target's signal sits in the spectrum. Holding
the design fixed and moving signal from high- to low-variance directions swept the gap:
The issue's measured 0.13 sits inside that range, and this explains its own observation that
the condition number predicts the damage only loosely (Spearman −0.68). Real activations put
belief signal in low-variance directions, which is why the bug bites there.
Cutoff, not precision. Confirmed separately: float32 with an explicit
rcondagrees withfloat64 to ~1e-10 at every alignment above. So this is an
rcondchange, not a dtype change.Reshape rather than append
linear_regressionandlinear_regression_svdwere two implementations of one operation —both standardize, weight, build a design and solve it; one via
lstsq, one via an explicitSVD with a threshold. Adding a third parameterised path would have been the easy change.
Instead:
_compute_regression_metrics) is extractedinto
_prepare_problem;linear_regressionnow solves through the same SVD +_compute_beta_from_svdthe SVDvariant already used, so there is one thresholding rule, not two;
fit_interceptresult-packing is extracted into_beta_arrays;_split_concat_resultsfiltered kwargs with a denylist (!= "rcond_values"), which anew parameter would have silently broken — it is now an allowlist.
Net diff is +291/−83 across four files, most of it docstrings and tests.
A reviewer should push back here if they would rather see the two public functions collapse
into one with the cutoff as a parameter. I kept both because
linear_regression_svd'scandidate search is a distinct operation with a distinct (and, as noted below, flawed)
selection rule, and merging them would have made this PR a signature break for existing
config users on top of a correctness fix.
Deliberately out of scope
A ridge with a validation-chosen penalty, which #201 argues a caller needs. Doing it
correctly in this module requires decisions this PR should not make silently: a ridge here
would penalise the intercept, and
standardize_featuresonly casts dtype — it does notcentre or scale — so the penalty would be feature-scale dependent. Shipping that is a
footgun, not a remedy. Filed separately rather than left as a paragraph.
linear_regression_svdselects its candidate on in-sample error, which cannot identify avalue that regularises: a cutoff only ever helps out-of-sample. I expected the search to be
outright degenerate (in-sample error is monotone in directions kept) and measured that it is
not — float32 roundoff gives in-sample error a genuine optimum, and it picked
1e-6over1e-15in both candidate orderings on a κ=2e8 design. So it is not broken, just selecting onthe wrong criterion. That is now stated in its docstring, pointing at
score_regressionforhonest selection, rather than changed under existing callers.
Breaking changes
None to signatures —
rcondis keyword-only with a default. Two behaviour changes callersshould know about:
but it is not one-signed: for a genuinely near-singular design the old accidental cutoff
was regularising, and removing it can make held-out fit worse.
effective_rankandcondition_numbernow make that visible, andrcondlets a caller choose it.effective_rank,n_singular_values,rcond,condition_number)appear in the returned metrics, and therefore in MLflow metric names under the existing
layer-key scheme.
Blast radius
Verified by grep against the actual refs, with positive controls (my first two greps ran
against branches where the directories did not exist and returned a confident, empty,
meaningless result):
simplex-research@a5d401baa:stage2/belief.py:110,targets/selfcheck.py:170and:380. Belief-regression R² reported fromstage2and thetargetsprogramme will move.selfcheck.pyexists partly to demonstrate this defect.ablate/subspaces.pyimportsget_robust_basis, which has its ownmax(matrix.shape) * epscutoff but applies it to the coefficient matrix, not arow-scaled design;
polar/report.mdand the READMEs are prose.eden-experiments@loop-belief.DESIGN.mdnames the module twice, both inplanning prose ("reuse, not rewrite" and an inventory). Every probe there uses
sklearn.linear_model.Ridge(1e-3)scored on a held-out split.simplexityitself beyond the registry wrappers in this diff.Testing
uv run --extra dev --extra pytorch --extra penzai --extra aws pytest -q→1052 passed, 2 skipped.
ruff format --checkclean.pyright→ 0 errors.Nine new tests. The headline regression test asserts recovered coefficients against a float64
reference on a tall well-conditioned design, and was run against the pre-fix module first:
The sharpest of them,
test_linear_regression_cutoff_is_independent_of_row_count, fits thesame column space at 2,000 / 20,000 / 60,000 rows and asserts the effective rank does not
change — which is the bug stated directly.
Also driven end-to-end through
LinearRegressionAnalysisrather than only the bare function,confirming
rcondreaches the solve from a config (default→ rank 9/9;rcond=1e-1→ rank3/9, R² 0.189) and that the validator refuses a negative, non-numeric, or wrongly-paired
value.
Suppressions
None added.
One note on the repo's gates
ruff check .exits 1 on this branch and onorigin/main— 3 errors (2× SIM117, 1×PT012) in
tests/generative_processes/test_data_prefetcher.py, which this PR does not touch(verified byte-identical to
origin/main). I left it alone because the PT012 finding needs ajudgement about that test's semantics — it raises inside its context manager deliberately, to
test cleanup — which does not belong bundled into a correctness fix. Filed separately.
Separately,
CLAUDE.md's documented commanduv run --extra dev --extra pytorch pyrightreports 20 errors and
pytestfails collection on 3 modules, purely from missingpenzaiand
awsextras. Adding--extra penzai --extra awsgives 0 pyright errors and a cleancollection. The documented command is under-specified.
🤖 Generated with Claude Code
https://claude.ai/code/session_01DYtgMenFgbPzNoN1MqxCxG