Skip to content

Make active-learning runs reproducible under a fixed seed#200

Open
CoolJosh0221 wants to merge 3 commits into
ntucllab:masterfrom
CoolJosh0221:seeding-reproducible
Open

Make active-learning runs reproducible under a fixed seed#200
CoolJosh0221 wants to merge 3 commits into
ntucllab:masterfrom
CoolJosh0221:seeding-reproducible

Conversation

@CoolJosh0221

Copy link
Copy Markdown
Contributor

A passed random_state now fully determines a run's query sequence. Audited every randomness source (strategy tie-breaking, QBC/BALD bootstrapping, KMeans-based strategies, model init, labeler, dataset utilities, and the HintSVM/variance-reduction C extensions — which contain no RNG calls). Four sites were not governed by the passed seed:

  • IdealLabeler.label called module-level np.random.choice (the global NumPy random state) and had no random_state parameter — now accepts one.
  • Dataset.labeled_uniform_sample (np.random.choice) and import_scipy_mat (np.random.shuffle) drew from the global NumPy random state — both now accept an optional random_state.
  • BALD's base_model committee is built with sklearn.clone, which copies the base estimator's random_state verbatim. So BALD's seed controlled the bootstrap bags but not each clone's internal fit randomness: a stochastic base (e.g. RandomForestClassifier(random_state=None)) stayed non-reproducible, while seeding the base with a fixed int made all clones identical. Each clone now receives a distinct deterministic seed derived from BALD's random_state_.

New libact.utils.check_random_state centralizes the rule for the labeler/dataset sites: None returns the global NumPy random state, so unseeded behavior is byte-identical to before; an int or RandomState returns a seeded instance. seed_random_state also accepts numpy integer scalars.

Tests

test_seed_reproducibility.py: same seed ⇒ identical query sequence across 14 query strategies (16 same-seed cases, incl. QBC bootstrapping, BALD via both models= and stochastic base_model=, ε-greedy, KMeans-based, ALBL), plus an IdealLabeler loop over conflicting-duplicate rows and dataset-utility seeding. Mutation-checked: reverting any of the four fixes fails its test. Unseeded behavior unchanged; full suite green.

Scope

The three global-RNG sites were already reproducible via np.random.seed() (as the existing tests do); this mostly makes a passed random_state sufficient and composable, and adds a test guard so the property can't silently regress. BALD is the exception — a committee that is both reproducible and diverse from a passed seed wasn't achievable before.

Audit of every randomness entry point (strategy tie-breaking, QBC/BALD
bootstrap bagging, diversity/clustering selectors, model init paths,
labeler, dataset utilities, and the C extensions - which are RNG-free)
found three places not controlled by a passed seed:

- IdealLabeler.label drew from the global np.random with no way to seed
  it; it now accepts random_state and breaks ties between duplicate
  features with conflicting labels reproducibly.
- Dataset.labeled_uniform_sample sampled via the global np.random; it
  now accepts an optional random_state.
- import_scipy_mat shuffled entries (and hence all positional entry
  ids) via the global np.random; it now accepts an optional
  random_state.

A new libact.utils.check_random_state helper centralizes the seeding
rule for these call sites: None keeps the global numpy random state so
unseeded behavior is byte-identical to before, while an int or
RandomState fully determines the outcome. seed_random_state now also
accepts numpy integer scalars as seeds.

Adds test_seed_reproducibility.py: two full active-learning runs with
the same seed must produce identical query sequences across 15
strategies (including QBC bootstrapping, BALD ensembles, epsilon-greedy
exploration, clustering-based and C-extension-backed strategies, and
ALBL), plus a canonical loop with an IdealLabeler over
conflicting-duplicate data, and seeding tests for the dataset
utilities. Mutation-checked: re-introducing global-RNG draws in
IdealLabeler or the QBC bootstrap sampler makes the new tests fail.
BALD's base_model + n_models path manufactures its committee via
clone(), so a stochastic base estimator (e.g. RandomForestClassifier)
left the run non-reproducible even when a seed was passed to BALD, and
seeding the base estimator directly collapsed committee diversity to
identical members.

BALD now derives a distinct deterministic child seed per clone from its
random_state_ (mirroring scikit-learn's ensemble behavior), and
SklearnAdapter/SklearnProbaAdapter.clone() gained an optional
random_state that reseeds the cloned estimator when it exposes one.
No-arg clone() is unchanged; estimators without a random_state
parameter and custom clone() signatures are handled gracefully.

Adds test_bald_base_model_stochastic covering the stochastic
base_model path; it fails against the previous unseeded clone
construction.
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