-
Notifications
You must be signed in to change notification settings - Fork 0
Extending SAMLB
Four things people usually add: a framework, an adapter for another library, a dataset, or a C++ learner. The full contributor workflow — setup, tests, style, PR checklist — is in CONTRIBUTING.md; this page is the design side.
Implement three methods and the whole benchmark works on your method.
# samlb/framework/classification/my_method/model.py
from __future__ import annotations
from typing import Any, Dict
from samlb.framework.base import BaseStreamFramework
class MyStreamingAutoML(BaseStreamFramework):
"""One-line description of the search strategy."""
def __init__(self, seed: int = 42, exploration_window: int = 1000, budget: int = 10):
self.seed = seed
self.exploration_window = exploration_window
self.budget = budget
self._init_state()
def predict_one(self, x: Dict[str, float]) -> Any:
return self._best.predict_one(x)
def learn_one(self, x: Dict[str, float], y: Any) -> None:
self._best.learn_one(x, y)
self._maybe_explore(x, y)
def reset(self) -> None:
self._init_state()Then register it in samlb/framework/classification/__init__.py (or
regression/).
Five things the benchmark assumes:
-
reset()really resets. It is called before every run; anything it leaves behind leaks across datasets and seeds. The test for this is that two runs of the same stream, with areset()between, produce identical numbers. -
predict_onenever learns. Prequential evaluation depends on the prediction happening first. -
Nonemeans "not ready". Return it during warm-up; the instance is counted but not scored. Do not return a fake label instead. -
self.seedis honoured.BenchmarkSuitesets it before each run and expects a different seed to produce a different — but reproducible — search. -
Exceptions are survivable but visible. A raising run is caught and
reported as
ERRORin the table, not silently dropped.
Do not write Python inner loops. Compose the native learners instead:
from samlb.framework.base import (
HoeffdingTreeClassifier, SelectKBest, StandardScaler,
)
from samlb.metrics import ADWIN, WindowMetric
candidate = StandardScaler() | SelectKBest(k=10) | HoeffdingTreeClassifier()
fresh = candidate.clone() # untrained copy, same hyperparametersclone() on any estimator or pipeline gives a fresh untrained copy — that is
the primitive most search strategies are built from. WindowMetric scores a
candidate on its recent behaviour rather than its lifetime average, and ADWIN
/ EDDM tell you when the stream moved.
A framework that searches its own private pool cannot be fairly compared with
the others. Draw from shared_config instead:
from samlb.framework.classification.shared_config import (
SHARED_CLASSIFIER_INSTANCES, SHARED_HYPERPARAMETERS, SHARED_PREPROCESSORS,
)If your method needs a differently shaped view of the pool, add a
ClassificationConfig method that translates it — that is what
asml_config_dict(), autoclass_config_dict() and eaml_param_grid() do.
Follow samlb/framework/adapters/:
class MyLibClassifier(BaseStreamFramework):
def __init__(self, model, name=None, seed=42):
if not _has_module("mylib"):
raise ImportError("... install with `pip install mylib`")
...
@staticmethod
def is_available() -> bool:
return _has_module("mylib")Four rules, learned from the River and CapyMOA ones:
-
Import lazily. A missing optional backend must not break
import samlb.framework.adapters. -
Expose
is_available(). Suites should skip a backend, not crash on it. - Never train the caller's object. Clone it, or rebuild from a factory, so one adapter survives a whole sweep.
- Translate honestly. If the library speaks a different vocabulary — class indices, instance objects, a fixed schema — the adapter converts, and converts back on the way out.
Add the dependency as an extra in pyproject.toml, and guard the tests with
@pytest.mark.skipif(not X.is_available(), ...) on synthetic data so they run
anywhere.
See Datasets — write an .npz with X, y, feature_names,
target_name, drop it in samlb/datasets/<task>/, and it is discovered
automatically. Keep the natural order.
- Header and source under
_cpp/<area>/. - Add the
.cpptoCPP_SOURCESinCMakeLists.txt. - Bind it in
_cpp/bindings/pybind_module.cpp. - Wrap it in
samlb/framework/base/_cpp_wrappers.pyand export it fromsamlb/framework/base/__init__.py. - Validate against a reference implementation (River or MOA) before claiming parity.
Two constraints: the build deliberately avoids -ffast-math (it breaks the
-inf sentinels the learners rely on), and a learner must be deterministic
given its seed.
Remember to rebuild — an editable install does not recompile on its own:
cmake -S . -B build/local -DCMAKE_BUILD_TYPE=Release
cmake --build build/local -j
cp build/local/_samlb_core.*.so samlb/def test_reset_makes_runs_reproducible():
data = list(stream("electricity", task="classification", max_samples=500))
model = MyStreamingAutoML(seed=42)
def prequential():
hits = 0
for x, y in data:
hits += model.predict_one(x) == y
model.learn_one(x, y)
return hits
first = prequential()
model.reset()
assert prequential() == firstThat single test catches the most common bug in a new framework: state that
survives reset() and quietly inflates every result after the first.