perf: BLAS projection, per-width caching, chunked fits, and accuracy fixes for ExtremeLearningMachine#48
Merged
Merged
Conversation
Replace the naive numba matmul kernel in ExtremeLearningMachine with a BLAS-backed float32 projection, sample and cache the random projection per input width, and solve large fits (>65536 rows) from a chunked augmented Gram matrix so the hidden matrix is never materialized. The per-width cache also fixes an out-of-bounds read: one instance refit with more input features than its first fit (which happens for every missingness pattern inside the imputers) read past the projection matrix and produced non-finite predictions. Measured: standalone fit 8.7->3.3 ms (20k rows, 35->100 features), imputer run with ELM regressor 0.67->0.46 s, 500k-row fit+predict 0.32 s / 204 MB -> 0.18 s / 29 MB peak. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tion MultivariateImputer refits a fresh model per missingness pattern on a small, pattern-specific row subset (often tens of rows). A fixed hidden width regardless of that subset size turned ExtremeLearningMachine's internal ridge fit into a severely underdetermined problem, causing high variance and frequent underperformance versus FastRidge. Scale the random projection by 1/sqrt(n_input_features) so alpha's effective strength no longer drifts with the number of predictor columns per pattern, and cap the hidden width to n_samples // min_samples_per_feature so it never outgrows the data it's fit on.
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.
Summary
Rewrites
ExtremeLearningMachinefor speed and memory, fixes a correctness bug that corrupted its output when used as the imputer regressor, and fixes a second, separate accuracy issue that made it systematically underperform (and sometimes actively hurt) compared toFastRidgein typical imputation workloads:np.matmul+ in-place bias/ReLU on float32 (numba dependency removed fromelm.py).fitpreviously initialized the projection matrix once and reused it forever. InsideMultivariateImputer, the same instance is refit on column subsets of varying widths (one per missingness pattern); refits with more input features read past the projection matrix (numba, no bounds check) and produced non-finite imputations, while narrower refits silently used a truncated projection. Projections are now sampled lazily per input width and cached, seeded identically — reproducible across instances, and identical values to before for the first width.set_paramsinvalidates the cache whenn_features/random_statechange.fitaccumulates the augmented Gram matrix[H, y, 1]chunk by chunk and solves via the existingfit_ridge_from_gram;predictprojects into a reusable chunk buffer. The full(n_samples, n_features)hidden matrix is never materialized.MultivariateImputerrefits a fresh model per missingness pattern on a small, pattern-specific row subset (often tens of rows). Two compounding issues madeExtremeLearningMachinea poor fit for that regime: (1) projection weights/bias were drawn~N(0,1)with no fan-in scaling, so pre-activation variance — and therefore the effective strength ofalpha— grew with the number of predictor columns per pattern; (2) the hidden width (n_features, default 100) was fixed regardless of how many samples a given pattern's fit had, turning the internal ridge fit into a severely underdetermined (parameters > samples) problem. Weights/bias are now scaled by1/sqrt(n_input_features), and a newmin_samples_per_featureparameter (default 5) caps the hidden width ton_samples // min_samples_per_featureat fit time (set to0to opt out and keep the old fixed-width behavior).Measurements
Same machine, warm runs (local
perf/elm_benchmark.py):MultivariateImputer(regressor=ELM), 10k × 60 panel, 3 targetsAccuracy:
ExtremeLearningMachinevsFastRidgeas the imputer regressorBenchmarked via
MultivariateImputer(regressor=...)across 10 seeds, 10% MAR missingness (mean r² on numeric targets):*constructed so every column is a nonlinear transform of shared latent factors — the one case ELM should structurally beat a linear model, kept to confirm the fix doesn't just regress ELM toward
FastRidgeeverywhere.Root cause on Titanic: per-pattern fits use a median of 38 rows against a 100-unit hidden layer (parameters > samples by 2.6x), which is what produced the negative r² before the fix. On typical tabular/time-series data
FastRidgeremains the stronger default;ExtremeLearningMachineis best reserved for imputation contexts with known nonlinear structure and enough samples per pattern.Tests
min_samples_per_feature=0, predict/fit width consistency after capping, and fan-in variance stability across input widths.Docs:
docs/api.rstautodocsExtremeLearningMachinevia:members:, so the updated docstring (documenting fan-in scaling andmin_samples_per_feature) is already reflected on the API page — verified with a clean localsphinx-build(no warnings).Note: imputation results with an ELM regressor will differ from previous releases whenever pattern widths varied — the old outputs were garbage (out-of-bounds reads) or badly overfit (fixed hidden width vs. pattern sample size), so no parity is intended.
🤖 Generated with Claude Code