Repository navigation
0.3.0: better small-data accuracy, calibration, active learning, serving, sklearn API, and a hardened test suite - #68
Merged
Conversation
Packaging: - Move all metadata into a PEP 621 [project] table in pyproject.toml and drop setup.py and requirements.txt; version is read from adaptive_classifier.__version__ so it lives in one place - Require Python >=3.10 (3.8/3.9 are EOL) and add 3.11-3.13 classifiers - Drop unused core dependencies (tqdm, setuptools, wheel); tqdm and the other script-only packages move to a `scripts` extra - CI: test on Python 3.10-3.13, use actions v4/v5 with pip caching, install via the `test` extra, build releases with `python -m build` Docs: - Add docs/deployment.md: multilingual base models, the saved file layout, the exact inference algorithm, and C#/JS notes - Add examples/portable_inference.py, a NumPy + onnxruntime reference that reproduces predict() without PyTorch or FAISS; verified against AdaptiveClassifier.predict for mean and cls pooling Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- save()/push_to_hub() now persist the resolved pooling mode ('mean' or
'cls') instead of the literal 'auto', so reloads and non-Python runtimes
no longer depend on a Hub lookup of the base model's config
- save() also writes the tokenizer files (tokenizer.json etc.) so a saved
directory is self-contained; a failure to write them only warns
- Add tests/test_portable_export.py (offline, tiny local BERT) covering
persisted pooling, tokenizer output, legacy-config handling, reload
round-trips, and parity of examples/portable_inference.py with predict()
- Update docs/deployment.md and the reference script: the two manual steps
are no longer needed for newly saved classifiers
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- forget(labels) removes whole classes from memory, label maps, training history and the neural head (keeping other classes' learned weights); clear_memory(labels) now delegates to it, fixing a gap where cleared classes still showed up in predictions through the head - remove_examples(texts, label=None, retrain=True) removes examples by text, recomputes prototypes, drops emptied classes and optionally fine-tunes the head - ood_score()/is_ood() score a text by its distance to the nearest prototype relative to that class's radius; radii are saved with the model so reloaded classifiers keep their spread - predict() gains abstain_below and abstain_ood, both off by default - New config keys ood_threshold and ood_min_radius - Share the offline tiny-BERT fixture in tests/conftest.py and add tests/test_forget_and_ood.py (30 tests) - Document the new methods in docs/API.md and the README Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- The integration job now runs `pytest tests/ -m integration` instead of a single named file, so integration-marked tests in any file are executed (together with the matrix job this covers the whole suite) - Cache the Hugging Face model directory in both jobs - Add a 45 minute timeout to the matrix job - Add a weekly schedule and workflow_dispatch so Hub or dependency breakage is caught without waiting for a PR - Cancel superseded runs on the same PR Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- New SklearnAdaptiveClassifier (fit, partial_fit, predict, predict_proba, predict_log_proba, score, classes_) wrapping AdaptiveClassifier, so it works in Pipeline, cross_val_score and GridSearchCV - Strings or integers as labels, returned as given; input validation for X and y; partial_fit accepts new classes at any call - The fitted AdaptiveClassifier is exposed as classifier_ for save, forget, remove_examples and OOD detection - tests/test_sklearn_api.py (23 offline tests) including clone, CV, Pipeline and GridSearchCV - Document in the README and docs/API.md Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…-mode tests CI - New `models` job downloads the test models once (with retries) via scripts/prefetch_test_models.py and caches them; the test jobs restore the cache and run with HF_HUB_OFFLINE=1. The previous run hit the Hub's 1000-requests-per-5-minutes limit (HTTP 429) because four matrix legs shared one quota; every failure, including the ONNX ones, was a 429 - Matrix reduced to the ends of the supported range, Python 3.10 and 3.13 - tests/test_ci_models.py fails if a test names a model the prefetch list does not cover Library fixes found by the new tests - Non-string labels (e.g. ints) are rejected: they were accepted, then reloaded from disk as a duplicated mix of int and str classes - None as a text or label, and mixed label types, raise a clear ValueError before any state changes - Negative or non-integer k raises ValueError (k=-1 silently dropped the last prediction) - Loading a directory without config.json says so instead of reporting an invalid Hub repo id Tests - tests/test_robustness.py: accuracy on separable synthetic data at several sizes, no catastrophic forgetting, class imbalance, label noise, determinism, order independence, and property-based checks (hypothesis) that predictions are always a valid ordered distribution - tests/test_failure_modes.py: hostile and malformed input, damaged or missing files, Hub failures falling back to PyTorch, and randomized sequences of add/forget/remove checked for internal consistency - A strict expected failure records a known weakness: with fewer than 10 examples per class the default blend lets an undertrained neural head outvote correct prototypes (~75-92% training accuracy on separable data) - Mutation-checked: 12 deliberate faults in the library, all caught Also: version 0.3.0, CHANGELOG.md, hypothesis added to the test extra. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The models job failed on bert-base-uncased and distilbert-base-uncased: the Hub's download-token endpoint returns 404 for the short alias names, on every retry. Canonical ids (distilbert/distilbert-base-cased, google-bert/bert-large-cased) downloaded fine. - Tests and the prefetch list use google-bert/bert-base-uncased and distilbert/distilbert-base-uncased (same models) - tests/test_ci_models.py rejects alias ids so they cannot return - The prefetch script no longer retries HTTP 401/403/404, which are permanent, so such a failure shows up in seconds instead of minutes Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…cores, ramp the head in The default blend scored 0.55-0.73 on perfectly separable synthetic data with fewer than 10 examples per class, where prototypes alone scored 0.96-1.0. Experiments on synthetic Gaussian, XOR and incremental-class data traced it to three causes, all fixed (this is a default-behaviour change, hence the 0.3.0 minor bump): - The neural head was badly undertrained: ~10 optimiser steps and an early stop at the first loss plateau. It could not learn a 2-D XOR from 120 examples (0.49-0.70). It now trains for at least `head_steps` (300) steps with a cosine-decayed `head_learning_rate` (0.003) and solves XOR (1.00) - Prototype scores were softmax(exp(-d)), which barely separated the nearest class from the rest (~0.45 vs 0.21), so a confident head always outvoted them. They are now softmax(-d / prototype_temperature), 0.25 - Classes below new_class_example_threshold used a fixed 0.3 prototype / 0.7 head split. The head's weight now ramps up from zero with the number of examples, so there is no accuracy cliff at the threshold. Setting both new_class_* weights restores a fixed split Backward compatibility: classifiers saved by an earlier version keep the legacy scoring when loaded (prototype_temperature absent -> None). Also updates the portable reference implementation and the deployment guide to the new scoring math, the README section on predictions, the changelog, and the real-model tests that encoded the old behaviour (blend weights, and a 0.85-0.95 confidence window that pinned the old blend). Tests: the known-weakness expected failure is now a regression test, plus XOR (only the head can solve it, and prototypes alone cannot), head-only and head_steps tests, ramp continuity and configuration, scoring sharpness, legacy-save compatibility, and portable-reference parity for every scoring branch. Mutation-checked: ten deliberate reversions, all caught. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- calibrate(texts, labels) fits one temperature on held-out labelled data (minimising negative log-likelihood), so a reported 0.9 means about 90%. predict, predict_batch and abstain_below then use calibrated probabilities; the winning class never changes. On synthetic overlapping classes held-out ECE drops from 0.20-0.34 to 0.02-0.03 - predict_set(text, alpha) returns a split-conformal set that contains the true label with probability >= 1 - alpha (never empty) - calibration_report() gives accuracy, ECE and NLL for checking on a separate test set - Calibration is saved with the model and discarded when the class set changes (a new class, forget) - Fix: predict_batch scored only k classes before blending, so with more than k classes its scores differed from predict(); it now scores all classes and trims to k - Pure maths lives in adaptive_classifier.calibration tests/test_calibration.py (45 tests): the maths against known answers (recovering a known miscalibration, conformal order statistics, ECE), and the classifier on separate train / calibration / test splits. Mutation- checked with 14 deliberate faults, all caught. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- suggest_labels(texts, n, strategy, diverse) ranks unlabeled texts by how useful a label would be: margin, entropy, least_confidence, or ood (to find classes the model has not seen). diverse=True discounts texts similar to earlier picks so near-duplicates are not all chosen - drift_report(texts) scores a window of incoming texts with ood_score and runs a one-sided binomial test of the out-of-distribution share against an expected rate; returns the rate, mean score, p-value and a verdict tests/test_active_learning.py (38 tests): each strategy's ranking is checked against an independent recomputation from predict(), boundary texts outrank clear ones, the ood strategy finds a hidden class, diverse selection avoids twins, and drift is flagged for shifted streams but not for in-distribution ones. Mutation-checked with 12 deliberate faults; two initially survived and exposed missing tests, now added. Honest note kept in the docs: on heavily overlapping synthetic classes uncertainty sampling did not beat random labelling (0.67 vs 0.69), so no efficiency claim is made. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…rfile
Thread safety (a bug fix that serving needs)
- Predicting while another thread added or forgot classes crashed with
"selected index k out of range", and concurrent add_examples calls
corrupted each other's gradient computation ("modified by an inplace
operation"). Both reproduced reliably. Every public operation now holds a
per-instance reentrant lock, so one classifier can be shared between
threads. The stress tests fail 5 of 5 runs when the lock is removed
Serving
- adaptive_classifier.serving: create_app(classifier) builds a FastAPI app
(/health, /info, /predict, /predict_batch, /predict_set, /ood, and
/examples, /forget, /remove_examples when updates are allowed);
python -m adaptive_classifier.serving ./model runs it
- Updates are off by default; --allow-updates enables them, an API key
(bearer token, constant-time compare) protects them, --save-dir persists
them. Request size limits, validation (422) and classifier errors (400)
- apredict / apredict_batch / aadd_examples run in a worker thread so they
do not block an event loop
- New serve extra; fastapi and httpx added to the test extra
- docker/Dockerfile (CPU torch, non-root, health check) and docs/serving.md.
The Dockerfile was not built: there was no Docker daemon available
Self-contained saves
- Loading a saved directory used to fetch the tokenizer from the base
model's repository even though save() writes it alongside the model, so
an ONNX-saved classifier could not start without Hub access. The saved
tokenizer is now used (and fetched from the Hub repo with the other
files), for both the ONNX and PyTorch paths
tests/test_concurrency.py (11) and tests/test_serving.py (43), plus tokenizer
loading tests. Mutation-checked: 15 deliberate faults, all caught.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The serving CLI tests monkeypatch uvicorn.run, so they need uvicorn installed. CI failed with four "No module named 'uvicorn'" errors (406 other tests passed); it worked locally only because uvicorn had been installed by hand. An audit of every third-party import in tests/ confirms nothing else is missing from a clean install of the test extra. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
scripts/benchmark_fewshot.py compares adaptive-classifier (default and prototypes-only), logistic regression on the same frozen embeddings, and SetFit, all on one shared encoder with the same stratified few-shot sample and test subset. Per run it records accuracy, macro-F1, calibration error, train time, test-set prediction time, median single-query latency, and the cost of adding one more labelled example (SetFit has no incremental update, so its training time is reported and marked as a full retrain). Samples are nested across shot counts so results are comparable. Commands: run, report (Markdown, best per column in bold), check (streams one example from each split to verify dataset ids and columns before a long run) and plan (validates workflow inputs so they are never interpolated into a shell). .github/workflows/benchmark.yml runs it by hand: a plan job validates inputs and checks the datasets, one parallel job per dataset runs the benchmark, and a summary job merges the results into the run summary. docs/benchmark.md explains the methods and, deliberately, the limits of what the numbers show. Found while verifying against the real Hub: PolyAI/banking77 is a loading script that current `datasets` releases refuse to run, so the benchmark uses the parquet copy mteb/banking77. SetFit also wrote ~260 MB of checkpoints into the working directory; they now go to a temporary directory (and checkpoints/ is git-ignored). tests/test_benchmark_harness.py (69 tests, all offline): sampling, metrics, aggregation, the report, input validation including shell-injection attempts, dataset verification against a stubbed `datasets`, every method run end to end on a tiny local model, and the command line. SetFit runs end to end when it is installed. The workflow's own shell steps were executed from the YAML with a simulated Actions environment. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
tests/test_ci_models.py failed in CI because the harness tests pass "sentence-transformers/all-MiniLM-L6-v2" to plan() as input data, and the guard reads any model-like string in tests/ as a model the suite needs offline. Nothing downloads it. Use "acme/encoder-small" for the dummy value instead of adding a model to the prefetch list for no reason. All other tests in the run passed (476 passed on Python 3.13). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
codelion
force-pushed
the
claude/zen-hawking-qj5dfq
branch
from
October 8, 2026 12:45
00dc1f1 to
83a799e
Compare
The benchmark harness stays as internal tooling, but the library's public documentation is about continual learning and does not need to advertise a comparison with another method. Also neutralise two comments that named it. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Saves from before 0.2.0 store neither prototype_temperature nor the new-class weights. The loader already pinned the temperature to the legacy scoring, but a missing new_class_prototype_weight / new_class_neural_weight picked up the new default (a ramp), so a class with fewer than 10 examples was blended (0.85, 0.15) instead of the (0.3, 0.7) it had in 0.2.0 and predictions changed. Found by loading one stripped-down save under both versions. Missing weights are now pinned to the old fixed split; saves that stored their own weights keep them. The classifiers published in the adaptive-classifier Hugging Face org were not affected (they all have 100 examples per class): 10 of them give identical predictions under 0.2.0 and 0.3.0 (225 predictions, no changed labels, largest score difference 3e-8). Co-Authored-By: Claude Sonnet 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.
Summary
Version 0.3.0. It bundles accuracy fixes (which change default predictions), new features for continual learning in production, packaging and CI work, and a much larger test suite.
CHANGELOG.mdhas the full list; this is the shape of it.Default behaviour changes (predictions change)
With fewer than 10 examples per class the default config scored 0.55-0.73 on perfectly separable synthetic data, where prototypes alone scored 0.96-1.0. Experiments traced it to three causes, all fixed:
head_steps(300) steps and solves it (1.00).softmax(exp(-d)): nearest class ~0.45 vs ~0.21), so a confident head always outvoted them. Nowsoftmax(-d / prototype_temperature), default 0.25.Classifiers saved by an earlier version keep their old scoring when loaded.
prototype_temperature=Noneand fixednew_class_*weights restore the old behaviour.Trade-offs: a class added with only a few examples is recognised less readily than before, because the old split biased the head toward new classes at the expense of existing ones.
add_examplesis slower on small memories (roughly 1-2 s vs ~0.1 s);head_stepsis configurable.Real-data check: 0.2.0 vs 0.3.0 defaults, adding classes one at a time
MiniLM encoder,
ag_news(start with 2 classes) andemotion(start with 3), 16 examples per starting class, remaining classes added one at a time, 3 seeds, 600 test examples, identical few-shot examples in both runs. It is a trade-off, not a pure win:Pooled over the 12 configurations, final accuracy improves by 0.05 on average (better in 8 of 12). So: clearly better when new classes arrive with few examples (far less forgetting, much better overall accuracy) at the price of the new class being under-recognised; neutral overall when classes arrive well-populated, with somewhat more old-class loss. With 3 seeds and 2 datasets this is suggestive, not conclusive.
New features
calibrate()(temperature scaling, held-out ECE 0.20-0.34 -> 0.02-0.03 on synthetic overlapping classes),calibration_report(), and conformalpredict_set().suggest_labels()(margin / entropy / least_confidence / ood, optional diversity) anddrift_report(). Uncertainty sampling did not beat random labelling on heavily overlapping synthetic classes (0.67 vs 0.69); the docs say so.forget(),remove_examples(),ood_score()/is_ood(), andpredict(..., abstain_below=, abstain_ood=).SklearnAdaptiveClassifier(fit,partial_fit,predict_proba, works inPipeline/cross_val_score/GridSearchCV).python -m adaptive_classifier.serving ./model(FastAPI; updates off by default and API-key protected), async methods,docker/Dockerfile,docs/serving.md.docs/deployment.md) andexamples/portable_inference.py, a NumPy + onnxruntime reference that reproducespredict()without PyTorch or FAISS. A saved directory now includes the tokenizer and the resolved pooling mode and loads without the Hub.Bugs fixed (found by the new tests)
add_examplescalls corrupted each other. The public operations ofAdaptiveClassifiernow hold a per-instance lock.add_examplesnow rejects non-string labels (breaking for anyone passing ints;str(label)fixes it).predict(k=-1)silently dropped the last result;predict_batch(k=...)scored onlykclasses so its scores differed frompredict;clear_memory(labels=...)left classes in the label map and head; loading a directory withoutconfig.jsonreported an invalid Hub repo id.Packaging and CI
pyproject.toml(setup.pyandrequirements.txtremoved), Python >= 3.10,test/serve/scriptsextras,tqdmno longer a core dependency.Testing
predict()for every scoring branch.adaptive-classifierHugging Face org (saved before 0.2.0) give identical predictions under 0.2.0 and 0.3.0: 225 predictions, no changed labels, largest score difference 3e-8.0.85 < confidence < 0.95window intest_reported_confidence_drop.py, which now asserts a consistent blend that is not the saturated pure-neural value.Known gaps
docker/Dockerfilehas not been built or run (no Docker daemon was available when it was written). The server it runs is covered by tests.ood_threshold(1.25) was chosen on synthetic data.MultiLabelAdaptiveClassifier's own methods (such aspredict_multilabel) are not covered by the new lock. Strategic mode is not covered by the portable reference or by the new accuracy and calibration tests.🤖 Generated with Claude Code