Repository navigation
Conversation
pyRATS called np.random.seed(42) at seven places, so every fit reset the caller's global random state (after fit_transform, np.random was left freshly seeded with 42). The draws now come from local numpy.random.RandomState generators seeded with the same value: every fit draws the same numbers in the same order as before, the results are unchanged bit for bit, and NumPy's global state is left alone. - lpca: the ARPACK start vector of every local SVD, reseeded per point as before. - kpca: the seed is now an argument of the function (still 42). - The noise seeds of the intermediate views, the shuffle that picks the far-off points (n_repel > 0) and the start vector of the spectral initialization share one generator per fit, reseeded where the global one was, so the shuffle continues the same stream as before. - Tear coloring: the start vector of eigsh in every component. - The two reseeds with no draw after them (compute_final_embedding, spectrum_of_laplacian_from_neighbors) are removed. - Param's noise draws use a local generator with the same seed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
RATS(random_state=42) exposes the seed that pyRATS fixed internally. The default, 42, is the value used so far, so results do not change unless a different seed is passed. Following scikit-learn, random_state accepts an int, None (a new seed from NumPy's global generator at every fit) or a RandomState instance; a numpy.random.Generator is accepted too. The value is resolved to one int per fit, which seeds every random draw of the fit: the start vectors of the local PCA (ARPACK), kernel PCA, the spectral initialization and the tear coloring, k-means when n_forced_clusters > 1, and the choice of far-off points when n_repel > 0. The seed reaches the loky workers of the local PCA as an argument, so results do not depend on n_jobs. compute_color_of_pts_on_tear reuses the seed of the last fit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The same seed gives the same embedding and another seed another one; the default is 42; a RandomState or Generator supplies one seed per fit, and None draws it from NumPy's global generator; the seed reaches the worker processes of the local PCA (n_jobs=1 and n_jobs=2 agree bit for bit); the tear coloring uses the seed of the fit; NumPy's global random state is untouched by a fit; invalid values raise ValueError. The data are generated in the file (no optional dependencies) and small: the 13 tests take about 17 s on one core. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ci.yml ran only tests/test_e2e.py; this adds a step for tests/test_random_state.py (about 20 s). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Explains random_state and its default, says that pyRATS leaves NumPy's global random state alone, and shows how to measure how much an embedding moves between seeds after a rigid alignment. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.
During my visit to Devika Narain's lab I needed to run each fit with several seeds to report how robust the embeddings are.
pyRATS fixes its random seed internally: seven calls to
np.random.seed(42), plusrandom_state=42inKernelPCAandKMeans. This PR exposes that seed as a parameter,RATS(random_state=42).e3283f1).np.random.seed.np.randomfreshly seeded with 42, so any unseeded random draw made afterwards by the calling code came out the same in every run. This is the only change in behaviour: code that relied on it should seed NumPy itself.API
random_state : int, RandomState instance, Generator or None, default=42, with scikit-learn's semantics:42(default)NoneRandomStateorGeneratorThe default is 42 rather than scikit-learn's
None, so that results, and the regression CI, stay deterministic.Generatoris accepted although scikit-learn does not accept it yet, since the value is reduced to one int per fit anyway.What changes
The seed is resolved once at the start of
fit_transformand passed explicitly to every place that draws random numbers:lpcakernel=None)kpcaKernelPCA(random_state=...)kernelRATS._fit_nbrhd_graphKMeans(random_state=...)n_forced_clusters > 1spectral_alignmenteigshglobal_init_algo_name="spectral"compute_far_off_pointsn_repel > 0compute_spectral_color_of_pts_on_teareigshin every tear componentcompute_color_of_pts_on_tearRandomStateper fit. It is reseeded wherever the global generator was, so the shuffle continues the same stream as before.n_jobs.compute_color_of_pts_on_tearuses the seed of the last fit, so the gluing colors match that fit.np.random.seed(42)calls with no draw after them (compute_final_embedding,spectrum_of_laplacian_from_neighbors) are removed.Param's noise draws, active only whennoise_varis set, use a local generator with the same seed.Tests and verification
tests/test_random_state.py: 13 tests, about 17 s on one core, with small data generated in the file. They check that:RandomState,GeneratorandNonesupply one seed per fit;n_jobs=1andn_jobs=2agree bit for bit;ValueError.ci.yml: one more step runs them. Drop it if you prefer.The default is unchanged. I compared this branch with
e3283f1, bit for bit (np.array_equal), on these runs:tests/scripts/generate_snapshots.py);n_forced_clusters=2,n_repel > 0with one and with two clusters,min_cluster_size=1, the fast CI configuration and a torn Klein bottle with its tear coloring.In all 68 comparisons,
y, the tear colors,Utilde,C,n_Utilde_Utilde,Utildegandcwere identical. Two repeated runs ofe3283f1were also identical to each other, so the comparison is not blurred by run-to-run noise. The only combination that fails,n_forced_clusters=2with the spectral initialization, fails the same way in both versions (see the side notes below).Both versions ran in the same environment: Linux, Python 3.13, NumPy 2.5.4, SciPy 1.18.1 and scikit-learn 1.9.1, with 3 cores for the large fits. In every fit, the old code left NumPy's global generator reseeded and the new code left it untouched.
Seeds other than 42. With
random_state=0andrandom_state=1, the branch gives bit for bit what the old code gave when its internal 42 was swapped for those seeds by patchingnp.random.seedin every process. Fits made that way can be repeated with the new parameter.Example: how much does an embedding depend on the seed?
A different seed always changes the bits. Most often the two embeddings differ only by a rotation or a reflection, but not always. These numbers come from my machine; the details may differ with another BLAS:
small_spherewithhole(1 986 points), default parameters: seed 0 gives the embedding of seed 42 up to a rotation or a reflection. Seeds 1 and 2 give another embedding, 14% of the diameter away (median per point, after a rigid alignment).A single seed would not show the first case. The README now has a short section with this snippet:
Two side notes, not changed here
n_forced_clusters > 1andglobal_init_algo_name="spectral", the fit fails withTypeError: 'tuple' object does not support item assignment.spectral_alignmentreturns a tuple(y, Z), andcompute_init_embeddingassigns it toyand indexes it for the next cluster. I am happy to send a separate fix.CONTRIBUTING.mdgives8e27b77as the regression baseline, whileci.ymlusese862055.Prepared with the help of Claude Code; reviewed and tested by me.