The full online-learning API. Design:
../design/adaptive-learner.md. Theory + honest gap:../theory/online-learning-fptl.md.
use adaptive_msm::{AdaptiveMsm, AdaptiveMsmConfig};
let config = AdaptiveMsmConfig::new()
.initial_c(1.0) // starting split/merge constant
.epsilon(0.5) // FPTL rate: Laplace scale = 1/epsilon AND the step
.c_bounds(0.01, 10.0) // keep c in (0, ∞) to stay in the metric regime
.window_size(16) // sliding window for the finite-difference gradient
.with_statistics() // enable telemetry (optional)
.seed(42); // fix the RNG (optional; entropy otherwise)
let mut learner = AdaptiveMsm::new(config);Every setter is chainable and clamps its argument (initial_c into
[min_c, max_c], epsilon to $\ge 0.001$, window_size to $\ge 1$). The
public fields (initial_c, epsilon, min_c, max_c, window_size,
track_statistics, seed) are readable directly.
Tuning note.
epsiloncouples exploration and step size (small$\epsilon$= big noise and small steps). Keep$\epsilon$near your data spacing and scale your cost signal to control the effective step — see../engineering/known-issues.md§B1 and theexamples/convergence.rs/examples/cookbook.rsparameterisations.
Two APIs for scoring, one for updating (with learner constructed as in §1):
let query = [1.0, 2.0, 3.0];
let target = [2.0, 3.0, 4.0];
// (a) predict: perturb c, return the MSM distance at the perturbed c.
let cost = learner.predict(&query, &target);
learner.observe(cost);
// (b) predict_deterministic: MSM distance at the CURRENT (unperturbed) c.
let eval = learner.predict_deterministic(&query, &target);
// (c) explore_c: draw ONE perturbed c for a contrastive round (see §3).
let c_tilde = learner.explore_c();observe(cost) records $(\tilde c, \text{cost})$ against the last explored
$\tilde c$, updates statistics, and — once the window holds $\ge 2$ points
— estimates the gradient and steps $c$. current_c(), round(), and
msm_config() report state; reset() returns to the initial configuration.
When one round needs several distances at the same explored $c$ (e.g. a
positive/negative pair whose difference is the loss), do not call predict
twice — it perturbs twice. Instead:
use adaptive_msm::MsmConfig;
// query `q`, a positive `pos` and a negative `neg` candidate:
let (q, pos, neg) = ([1.0, 2.0], [1.0, 2.0], [5.0, 6.0]);
let c = learner.explore_c(); // one draw
let cfg = MsmConfig::new(c); // one config
let d_pos = cfg.distance(&q, &pos);
let d_neg = cfg.distance(&q, &neg);
let loss = (d_pos - d_neg + 1.0).max(0.0); // e.g. a margin/triplet loss
learner.observe(loss); // one update for the whole roundWith .with_statistics(), learner.statistics() returns
Some(&AdaptiveMsmStatistics):
| Field | Meaning |
|---|---|
observations, total_cost, average_cost |
running counts / averages |
c_history, cost_history |
full trajectories (useful for plotting) |
current_gradient |
the most recent finite-difference gradient |
move_count, merge_count, split_count |
operation tallies (reserved for op-level telemetry) |
AdaptiveMsmBuilder constructs a learner and optionally pre-trains it on
(query, target, cost) samples:
use adaptive_msm::{AdaptiveMsmBuilder, AdaptiveMsmConfig};
let learner = AdaptiveMsmBuilder::new()
.config(AdaptiveMsmConfig::new().initial_c(1.5).seed(7))
.add_training_sample(vec![1.0, 2.0], vec![2.0, 3.0], 1.0)
.add_training_sample(vec![0.0, 1.0], vec![0.0, 1.0], 0.0)
.build(); // runs predict+observe over the samples
assert!(learner.round() >= 1);learner.regret_bound(max_cost, alphabet_size) returns the FPTL order
$\text{max_cost}\cdot\sqrt{T\ln\lvert\Sigma\rvert}$. It is a reference
value, not a guarantee this scalar learner attains it — see
../theory/online-learning-fptl.md §5 and
../formal/fptl-regret.md.
The examples/convergence.rs experiment shows $c$ converging to a known
minimizer with a $\approx 23\times$ loss reduction vs. a fixed baseline; the
examples/cookbook.rs teacher-recovery recovers a hidden $c^\star = 2.0$ as
$c \approx 2.07$. Both are analysed in
../scientific/ledger/2026-07-12-learner-convergence.md.