Skip to content

Add batch querying support - #198

Open
CoolJosh0221 wants to merge 4 commits into
ntucllab:masterfrom
CoolJosh0221:batch-mode-upstream
Open

Add batch querying support#198
CoolJosh0221 wants to merge 4 commits into
ntucllab:masterfrom
CoolJosh0221:batch-mode-upstream

Conversation

@CoolJosh0221

Copy link
Copy Markdown
Contributor

Summary

  • Add QueryStrategy.make_query_batch(batch_size) with default score-based top-k selection.
  • Add Dataset.update_batch(entry_ids, labels) while preserving existing per-entry observer callback behavior.
  • Add batch-aware overrides for RandomSampling, CoreSet, and EpsilonUncertaintySampling.
  • Add DiversityWeightedMeta for diversity-aware batch selection over existing query strategies.
  • Document the batch API and add an end-to-end batch querying example.

Notes

  • make_query() keeps returning a single entry id; existing public single-query behavior is unchanged.
  • Dataset.update_batch() validates length, dimensionality, and duplicate entry ids before applying per-entry updates.
  • ALBL and VarianceReduction explicitly reject make_query_batch() because their existing semantics are not directly batch-compatible.

Tests

  • python -m unittest -v
    • 217 tests passed
  • MPLBACKEND=Agg MPLCONFIGDIR=/tmp/libact-mpl python examples/batch_query_plot.py
    • sequential uncertainty sampling: 120 training rounds for 120 labels
    • top-k batch uncertainty sampling: 12 training rounds for 120 labels
    • diversity batch uncertainty sampling: 12 training rounds for 120 labels

…WeightedMeta

Selecting N samples previously cost N training rounds, since make_query()
returns one entry id per call. This adds a batch path built on the
standardized _get_scores() contract (ntucllab#197), while keeping make_query()'s
single-int public contract and the per-entry (entry_id, label) observer
callback untouched.

- QueryStrategy.make_query_batch(batch_size): default = stable descending
  top-k of _get_scores(); returns np.ndarray of distinct entry ids, most
  preferred first. TypeError for non-integer batch_size; ValueError for
  batch_size < 1, an empty pool, or batch_size > n_unlabeled (no silent
  clamp). Ties break deterministically (stable sort), so
  make_query_batch(1) may differ from make_query() only at ties.

- Semantic overrides where top-k is unfaithful:
  - RandomSampling: uniform sampling without replacement.
  - CoreSet: true iterative k-center greedy (Sener & Savarese 2018) with
    a running min-distance vector; honors metric and transformer.
  - EpsilonUncertaintySampling: Binomial(batch_size, epsilon) exploration
    picks drawn from the complement of the top-uncertainty picks, so the
    batch is always exactly batch_size distinct ids.
  - ALBL and VarianceReduction: explicit NotImplementedError (inherently
    sequential / no per-sample scoring).

- DiversityWeightedMeta: wraps any score-based strategy so batches are
  not just top-k with near-duplicate redundancy. Greedy utility
  (1 - lmbda) * s_norm + lmbda * d_norm, where s_norm is a monotone
  min-max of the base scores (rank-faithful: never re-interprets score
  semantics, so confidence-flavored scores like HintSVM's are handled by
  construction) and d is the min distance to already-selected batch
  members. First pick = base argmax. Optional candidate_pool_size cap.

- Dataset.update_batch(entry_ids, labels): validates lengths, rejects
  duplicate ids, empty input is a no-op; applies labels through the
  existing per-entry update() path so observers (ALBL, QUIRE, QBC) see
  exactly the same incremental notification stream as sequential calls.

- CoreSet._get_scores: cdist -> sklearn pairwise_distances, fixing a
  latent crash on sparse feature matrices (identical dense results).

- Tests: 81 new (batch contract across all strategies, override
  semantics, diversity guarantee with a near-duplicate control fixture,
  rank-faithfulness with negative/adversarial-magnitude mocks, sparse
  inputs, update_batch equivalence with sequential updates for
  QUIRE/QBC/ALBL observers, error paths). Full suite: 217 passing.

- Docs: Sphinx entry, README batch-querying section, and
  examples/batch_query_plot.py (12 vs 120 training rounds for the same
  120-label budget on the diabetes dataset).
scikit-learn deprecated `multi_class` in 1.5 and removed it in 1.7, so
`LogisticRegression(..., multi_class="ovr")` now raises TypeError. On top of
that, `solver="liblinear"` no longer performs one-vs-rest for multiclass data
(n_classes >= 3): it raises and directs callers to OneVsRestClassifier.
Together these broke 24 tests under scikit-learn 1.8.

All changes are behavior-preserving:

- Drop `multi_class="ovr"` wherever it was paired with `solver="liblinear"`;
  liblinear only ever did one-vs-rest, so removing it is a no-op.
- Drop `'multi_class': 'multinomial'` from the default logreg_param of
  MaximumLossReductionMaximalConfidence; multinomial is now the default for
  the newton-cg solver.
- EER iris tests: wrap in
  SklearnProbaAdapter(OneVsRestClassifier(LogisticRegression(solver="liblinear")))
  to retain one-vs-rest. Verified to reproduce the exact recorded query
  sequences, so no assertion values changed.
- LogisticRegression / SklearnAdapter delegation tests: use the default solver
  with max_iter=1000 on both sides (the tests only assert wrapper-vs-sklearn
  equality, so the solver is incidental; max_iter avoids a ConvergenceWarning
  on unscaled iris).
- Update the CostSensitiveReferencePairEncoding docstring example.

Pre-existing issue independent of the batch-querying work; fails on master too.
217 passed (was 24 failed, 193 passed).
@ariapoy

ariapoy commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator

Discussions of Commit 3 "Remove linting".

  1. Commit 3 deletes .github/workflows/linting.yml entirely, which conflicts with the open Improve linting #192. The only pylint failure is in information_density.py:146 (E0606), a file this PR doesn't touch.
  2. @yangarbiter, @hsuantien, do you think it is okay to remove the linting config? Suggest reverting the deletion here and handling linting in Improve linting #192.

@ariapoy

ariapoy commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator

Update batch efficiency.

Goal. Making batch queries actually train once per batch needs an overridable batch-update hook on QueryStrategy
Scenario.
update_batch fires per-entry callbacks, so strategies that retrain inside update() (such as BALD, InformationDensity, EpsilonUncertaintySampling, QueryByCommittee) still retrain batch_size times per batch.
@CoolJosh0221 Could you double-check it? Thanks.

CoolJosh0221 and others added 2 commits August 5, 2026 10:18
BALD, QueryByCommittee, InformationDensity and
EpsilonUncertaintySampling retrain their models in the per-entry
update() hook, and Dataset.update_batch applied labels through that
per-entry path — so a batch of B labels triggered B full retrains,
of which only the last (after all labels are applied) can ever be
observed by the next query.

Dataset gains an on_update_batch observer channel: during
update_batch, batch-aware observers are notified exactly once with
the whole batch after all labels are applied, while callbacks
registered only via on_update still see the identical incremental
per-entry stream as sequential update() calls. QueryStrategy
registers on both channels; its default update_batch replays the
per-entry hook (preserving QUIRE/ALBL/HierarchicalSampling
bookkeeping semantics exactly), and the four model-retraining
strategies override it to train once on the fully updated dataset.

Full suite: 224 passing. examples/batch_query_plot.py now truly
performs 12 training rounds for 120 labels on the batch paths.
@CoolJosh0221
CoolJosh0221 force-pushed the batch-mode-upstream branch 2 times, most recently from c7ffeb5 to ce91b12 Compare August 5, 2026 02:46
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.

2 participants