Skip to content

random_state is never applied: GaussianMixture and ClusterAutoK are not reproducible #122

Description

@Marius1311

Summary

cc.tl.GaussianMixture accepts random_state, stores it, and never uses it. The attribute is assigned at tl/_gmm.py:97 but is not among the arguments forwarded to super().__init__() at lines 86–95:

super().__init__(
    num_components=n_clusters,
    covariance_type=covariance_type,
    init_strategy=init_strategy,
    init_means=init_means,
    convergence_tolerance=convergence_tolerance,
    covariance_regularization=covariance_regularization,
    batch_size=batch_size,
    trainer_params=trainer_params,
)
self.n_clusters = n_clusters
self.random_state = random_state   # <- stored, then unused

Nothing else in the package reads it (grep -rn random_state src/cellcharter/ finds only the assignment, the Cluster pass-through at _gmm.py:279, and ClusterAutoK computing i + random_state at tl/_autok.py:123 before handing it to this constructor).

This means ClusterAutoK's per-run variation is not seeded either — runs differ only through ambient global RNG state, so best_k is not reproducible.

Why it can't simply be forwarded

torchgmm.bayes.GaussianMixture.__init__ takes no seed argument at all:

num_components, covariance_type, init_strategy, init_means,
convergence_tolerance, covariance_regularization, batch_size, trainer_params

and grep -rn "seed_everything\|manual_seed\|random_state" torchgmm/bayes/gmm/ torchgmm/base/ is empty. So the fix needs explicit seeding of the torch global RNG (kmeans init is what consumes it), not a passthrough.

Reproduction

Self-contained, no data download:

import numpy as np, anndata as ad
from sklearn.datasets import make_blobs
import cellcharter as cc

TRAINER = {"accelerator": "cpu", "enable_progress_bar": False, "enable_model_summary": False}
X, _ = make_blobs(n_samples=1500, n_features=10, centers=8, cluster_std=3.0, random_state=0)
X = X.astype(np.float32)

def fit_labels(seed):
    m = cc.tl.GaussianMixture(n_clusters=8, random_state=seed, trainer_params=dict(TRAINER))
    m.fit(X)
    return np.asarray(m.predict(X))

a, b = fit_labels(0), fit_labels(0)
print("identical:", np.array_equal(a, b), "| differing cells:", int((a != b).sum()), "/", len(a))

adata = ad.AnnData(X=np.zeros((X.shape[0], 2), dtype=np.float32))
adata.obsm["X_rep"] = X
for _ in range(3):
    autok = cc.tl.ClusterAutoK(
        n_clusters=(2, 10), model_class=cc.tl.GaussianMixture,
        model_params={"random_state": 0, "trainer_params": dict(TRAINER)}, max_runs=3,
    )
    autok.fit(adata, use_rep="X_rep")
    print("best_k:", autok.best_k)

Output:

identical: False | differing cells: 1184 / 1500
best_k: 6
best_k: 9
best_k: 9

Two fits with the same random_state disagree on 79% of cells, and ClusterAutoK reports a different best_k on identical repeated runs.

Candidate fix

Seeding before the fit does restore determinism — verified with the same script:

from pytorch_lightning import seed_everything

def fit_seeded(seed):
    seed_everything(seed, workers=True, verbose=False)
    m = cc.tl.GaussianMixture(n_clusters=8, random_state=seed, trainer_params=dict(TRAINER))
    m.fit(X)
    return np.asarray(m.predict(X))
identical labels (same seed):      True
identical labels (different seed): False

i.e. same seed reproduces exactly, different seeds still explore — which is what ClusterAutoK's repeated runs need.

Calling seed_everything(self.random_state) inside GaussianMixture.fit would fix both classes at once. Worth noting it mutates global RNG state as a side effect; a narrower alternative is a torch.random.fork_rng() block around the fit, seeded from random_state.

One knock-on: ClusterAutoK derives each run's seed as i + random_state (_autok.py:123), so once seeding is live, two ClusterAutoK instances whose random_state differ by less than max_runs would share fits. Deriving per-run seeds from numpy.random.SeedSequence([random_state, i]) avoids that overlap.

Happy to open a PR if the seed_everything-in-fit direction looks right to you.

Environment

cellcharter 0.3.7
torchgmm    0.1.4
torch       2.13.0+cpu
python      3.12.13

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions