whassan's picture
Deploy Sensorflow Studio
9762da0 verified
|
Raw History Blame Contribute Delete
25.8 kB
# seqeval — Efficient Sequential Regression Detection: Technical Design
**Goal.** Detect small model regressions (~2 percentage points on specific
classes/conditions) on every model update WITHOUT evaluating the full corpus,
with statistical validity under (a) sequential looks, (b) clustered
(non-independent) frames, (c) stratified non-proportional sampling, and
(d) many simultaneous hypotheses.
This document records the design *and the judgment calls*: which candidate
techniques were adopted, which were rejected, and why. The reference
implementation lives in this package and is exercised by `tests/test_seqeval/`.
---
## 1. Problem formulation
For each metric m (recall is the driver metric in v1) and each population
stratum h, let Δ_h = m_candidate(h) − m_baseline(h) measured on the same data.
With a **practical-significance margin** δ (default 0.005 = 0.5pp):
```
H0 (no material regression): Δ_h ≥ −δ
H1 (material regression): Δ_h < −δ
```
plus the separate equivalence-style claim used for PASS: reject `Δ_h ≤ −δ`
in favor of `Δ_h > −δ`.
**Absolute vs relative deltas.** The default driver is **absolute percentage
points**, for three reasons:
1. Safety cost is absolute: a 2pp recall drop means the same number of extra
missed pedestrians whether baseline recall was 0.98 or 0.80. Relative
deltas overstate changes on strong baselines (0.98→0.96 is "only −2%
relative") and understate them on weak ones.
2. Absolute deltas keep Bernoulli variance algebra and margin semantics simple
and uniform across strata; relative margins make the null hypothesis depend
on the (estimated) baseline value, contaminating the test with baseline
estimation error.
3. Absolute margins are what promotion policies elsewhere in this platform
already use (`megaeval.analysis.DEFAULT_PROMOTION_POLICY` is expressed in
absolute drops), so the gate composes with existing policy language.
Relative deltas are still *reported* in every evidence record (`rel_delta`)
because they are useful for human interpretation, especially for rare classes
with weak baselines. They are never the decision variable.
**Practical vs statistical significance.** With ~320k objects, a 0.05pp drop
can be "statistically significant" yet operationally meaningless. Hence the
margin δ: nothing is declared REGRESSION unless the evidence shows the drop
exceeds δ, and nothing is declared PASS unless the evidence shows any drop is
smaller than δ. Statistical significance against a point null of exactly zero
is deliberately not used anywhere.
## 2. The statistical unit: clusters, not frames
Frames/objects are not independent: objects in one container (scene/track)
share weather, lighting, scenario, scene-level hardness and — critically —
correlated model failures (one bad scene produces many misses). Treating
objects as iid understates variance by the design effect
```
DEFF = 1 + (m̄ − 1) · ICC (m̄ = size-weighted mean cluster size)
```
and every "significant" result computed that way is anti-conservative.
Adopted design:
* **Inference unit = cluster**: all sampled objects of a stratum in one
container. Per-cluster mean paired differences (bounded in [−1, 1]) are the
observations fed to the confidence sequences and e-processes. This is valid
under *arbitrary* within-cluster correlation — no correlation model is
assumed — and whole clusters are sampled so the unit is never split
(`planner.py` selects container groups, not objects).
* **Diagnostics, not corrections**: ICC (one-way ANOVA estimator), DEFF and
n_eff = n/DEFF are computed per node (`units.py`) and attached to every
evidence record. They are reporting artifacts; validity does not depend on
them because clustering is handled structurally.
* An interesting empirical note the machinery surfaces: pairing itself removes
most cluster correlation (the shared scene effect hits both models and
cancels in d_i), so ICC on paired differences is small (~0.07 in the worked
example, DEFF ≈ 1.2) even when ICC on raw outcomes is large. We keep the
cluster unit anyway — the residual correlation is real and free to handle.
* **Clustered bootstrap** (resampling clusters, not objects) is the prescribed
fallback for metrics that don't decompose into per-object outcomes (§3). It
is not needed for the v1 recall driver.
Rejected: variance-inflation-only corrections (divide n by an assumed DEFF≈2
and pretend units are iid). Rejected because ICC varies by stratum and model
pair; a fixed inflation is either wasteful or invalid, and it composes poorly
with anytime-valid methods whose guarantees are per-observation.
## 3. Paired evaluation as the core design
Both models are evaluated on the **same sampled units** (`paired.py`), and all
inference is on the per-unit difference d_i = c_i − b_i ∈ {−1, 0, +1}.
Why this is the single biggest efficiency lever: for two independent samples,
Var(m̂_c − m̂_b) ≈ 2·p(1−p)/n (~0.18/n at p=0.9). For paired evaluation,
Var(d) = ν − Δ², where ν = P(models disagree) — the discordance rate. Adjacent
model versions agree on almost everything (measured ν ≈ 0.03–0.06 in the
harness), so pairing cuts required samples by roughly 3–10×. The paired
design also removes the between-unit difficulty variance *and* the shared
scene effects (§2).
**McNemar correspondence.** For binary per-object outcomes the information in
the paired design is exactly the discordant counts: n01 (baseline right,
candidate wrong) vs n10 (the reverse) — McNemar's setup. We do not use the
classical McNemar *test* (it is fixed-n); instead the same discordant-pair
information drives (a) the sequential e-process on d_i and (b) the
Beta-Binomial posterior of §14, which is literally a Bayesian McNemar. The
counts n01/n10 are reported in attribution because they are the most
interpretable summary of "what changed".
**Which metrics get which treatment:**
| Metric | Per-object outcome? | Sequential treatment |
|---|---|---|
| recall (per-GT-object TP/FN) | yes, Bernoulli | confidence sequence / e-process on paired d_i ✔ (v1 driver) |
| precision (per-prediction) | yes, but the unit set differs per model (each model's own predictions); pair at the cluster level | CS on cluster-level paired precision differences |
| safety_recall, per-stratum recall | yes | same as recall ✔ |
| mean IoU (matched pairs) | bounded continuous per object | CS on paired IoU differences (empirical-Bernstein handles [0,1] naturally) |
| AP / mAP / AUC | **no** — rank statistics over the whole set; not a mean of per-object terms | **paired clustered bootstrap** at checkpoints (resample clusters, recompute both models' AP on the same resample, CI on the difference). No anytime-valid guarantee: schedule at *pre-registered* checkpoints (end of each budget stage) with alpha-spending across the (few, fixed) looks |
This split is a deliberate honesty boundary: Bernoulli-style anytime-valid
treatment is used exactly where a metric is a mean of bounded per-unit terms;
composite rank metrics get bootstrap at a small number of pre-committed looks
rather than pretending they support continuous monitoring.
## 4. Sampling design: hybrid Neyman + risk floors + safety minimums
Candidates considered:
* **Proportional allocation** — matches the population, but starves exactly
the strata we care about (pedestrian|night is ~5% of objects; a proportional
10k sample gives ~500 units, hopeless for 2pp detection). Rejected as the
primary rule.
* **Equal allocation** — maximal per-stratum power for fixed budget, but
wastes budget on huge easy strata and makes population-level estimates
needlessly noisy. Rejected.
* **Risk-weighted only** (as in `megaeval.sampling`) — right instinct, no
optimality story; can be arbitrarily far from variance-optimal. Rejected as
the *sole* rule, kept as a multiplier.
* **Neyman allocation** n_h ∝ N_h·σ_h — variance-optimal for the *population*
estimate, but per-stratum guarantees are incidental, and σ_h must not come
from candidate outcomes (bias firewall, §13). Adopted as the baseline using
a candidate-independent PRIOR variance profile (documented rates by
class/condition; a pilot baseline-only estimate would also qualify).
**Adopted hybrid** (`planner.py`), in order: Neyman shares from prior σ_h →
multiplicative risk weights (VRU ×1.5, night ×1.3) → mandatory minimum per
stratum (200) → pre-registered safety-critical strata get a floor sized for
the target MDE (2500 by default, §10) → truncate at N_h, select whole
clusters in a seeded shuffled order.
**Unbiasedness under oversampling.** Every sampled object carries the
Horvitz–Thompson weight w_h = N_h/n_h. Population and class-level estimators
are weight-corrected (weighted cluster means feed the overall/class/difficulty
nodes), so the classic stratified identity `E[Σ_h W_h p̂_h] = p` holds and
oversampling safety strata does not bias headline estimates. Per-stratum
(leaf) inference is self-weighting and needs no correction.
## 5. Multi-stage pipeline
```
sanity (≈500 units) → stratified screening (~15% of plan)
→ sequential confirmation (batched to 100% of plan, anytime-valid)
→ targeted escalation (reserve clusters, suspect/safety strata only)
```
* **Sanity**: fingerprints resolved, frozen plan hashed, smoke batch checks
for degenerate outputs (rate ∉ (0.2, 1], discordance ≥ 0.5 ⇒ abort — a
broken export should fail in seconds, not consume the budget).
* **Screening** flags "suspect" strata early (e-value ≥ 1.5 — soft evidence,
*never* a decision) to prioritize escalation later. Screening consumes the
same frozen plan; it does not select units based on outcomes (§13).
* **Sequential confirmation** processes the remaining plan in batches; all
decisions (REGRESSION / PASS) can fire at any batch boundary because the
underlying processes are anytime-valid.
* **Escalation** pulls from each stratum's pre-frozen reserve (shuffled at
plan time) for strata still undecided that are suspect or safety-primary,
up to a hard cap. Escalation to *full* evaluation of a stratum is the
terminal step of the same mechanism (the reserve is the rest of the
stratum).
## 6. Sequential methodology: anytime-valid confidence sequences, not CUSUM
**Why repeated fixed-sample CIs are wrong.** A 95% CI computed at n has 5%
miscoverage *at that single n*. Recompute after every batch and stop on the
first exclusion of the null and the type-I error inflates monotonically —
by the law of the iterated logarithm the statistic crosses any fixed-n
boundary infinitely often under the null, so "peek until significant" has
error probability → 1. Evaluation gates that check after every batch are
exactly this setting.
**Adopted: confidence sequences / e-processes** (`sequential.py`):
* Empirical-Bernstein confidence sequence (Waudby-Smith & Ramdas 2023,
predictable plug-in): a CS valid *simultaneously over all n*, so the gate
may look after every batch and stop whenever it likes. Empirical-Bernstein
rather than Hoeffding because paired differences have tiny variance
(ν ≪ 1/4) and EB width scales with the realized variance; a Hoeffding CS
would be ~3–4× wider here (Hoeffding rejected for power, kept as a mental
baseline).
* One-sided **betting e-processes** for the two directed nulls. The wealth
process K_t = Π(1 − λ_t(X_t − m)) is a nonnegative supermartingale under
H0: μ ≥ m for any predictable λ_t in the admissible range; Ville's
inequality gives P(∃t: K_t ≥ 1/α) ≤ α. Bet sizing = max(empirical-Bernstein
recipe, aGRAPA/approximate-Kelly λ ≈ drift/(σ̂² + drift²)), clipped —
sizing affects power only, never validity. E-values (not just CIs) are kept
because they compose: they feed e-BH in the hierarchy (§8) and multiply
across independent looks/runs if evidence is ever pooled.
* Batch-incremental updates, sticky decisions, per-batch trajectory recording
(`log e vs n` with the 1/α boundary) for the dashboard.
**Where CUSUM legitimately fits — and why not here.** CUSUM/Page tests are
designed for *change-point detection in an ongoing stream*: an unknown time
at which a monitored process shifts, with guarantees phrased as average run
length (ARL) to false alarm and detection delay. That is the right tool for
**production drift monitoring** (a deployed model's live metric degrading at
an unknown time — a reasonable future addition next to
`sensorflow/evaluation/regression.py`). It is the wrong tool for **gated A/B
evaluation**, where there is no change-point: the candidate is a fixed,
already-different model, the hypothesis is about a fixed mean difference, ARL
semantics don't map to per-release type-I error, and CUSUM provides neither
confidence intervals for effect size nor e-values for hierarchical multiple
testing. CUSUM is therefore explicitly rejected for the gate.
(Group-sequential designs — O'Brien-Fleming etc. — were also considered:
valid, but they require pre-committing to a fixed number of looks and a
maximum n, which fights the budget-staged, stop-any-time controller; the
anytime-valid framework strictly dominates for this use.)
## 7. Mandatory three-outcome decisions
Non-detection is NEVER equivalence. Each node ends in exactly one of:
* **REGRESSION** — the regression e-process crossed 1/α_allocated: confirmed
drop beyond the margin. Gate: block.
* **PASS** — the equivalence e-process crossed 1/α_pass: confirmed that any
drop is smaller than the margin (this is a one-sided equivalence /
non-inferiority claim, the sequential analogue of a TOST bound). Gate:
allow, but only when the overall node AND every pre-registered safety
primary individually PASS.
* **INSUFFICIENT_EVIDENCE** — budget exhausted first. Gate: expand within
budget if possible, else report with explicit language: *"not proven
equivalent — do not treat as a pass."* (`controller.INSUFFICIENT_LANGUAGE`
is exactly that sentence; the tiny-budget test asserts it.)
## 8. Hierarchical multiple testing
Testing ~28 nodes (overall, 6 classes, 18 class×condition strata, 3 difficulty
bands) at α each would give a family error of ~1−0.95²⁸ ≈ 76%. Naive
Bonferroni over everything destroys power for the one node that matters.
Adopted structure (`hierarchy.py`):
```
Level 1 overall α·0.30 single test
Safety pre-registered primaries α·0.30 split equally, tested individually
Level 2 class α·0.15 e-BH within level
Level 3 class × condition strata α·0.15 e-BH within level (non-safety)
Level 4 difficulty bands α·0.10 e-BH within level
```
Budgets sum to α, so the union bound gives family-wise control across levels;
within a level, **e-BH** (BH on e-values) controls FDR *under arbitrary
dependence* — mandatory here because levels re-aggregate the same units and
strata share scenes. e-BH composes with optional stopping because an
e-process value at any stopping time is a valid e-value (this is the reason
the implementation carries e-values and not just CSs).
**Gatekeeping and the masking trap.** Classic top-down gatekeeping ("test
children only where the parent rejected") was **rejected** for the regression
direction: a 2pp pedestrian-night drop inside a globally *improved* model is
invisible at level 1, and a gate that only descends on level-1 rejection would
never see it. Instead every level has its own reserved budget, and
**safety-critical strata are pre-registered primary hypotheses** with
dedicated alpha, tested individually and exempt from e-BH masking by
better-behaved siblings. The power test in the suite is exactly this case:
overall metric up ~0.3pp, pedestrian-night down 2pp — and the stratum node
must fire. Alpha propagation is top-down only in the *reporting* sense
(attribution descends into difficulty bands of regressed strata); decisions
never require a parent rejection.
## 9. Rare-class policy
* **Minimum counts**: every non-empty stratum gets ≥ 200 sampled objects
(or all of N_h if smaller) — below that, even 10pp effects are undetectable
and the honest answer is INSUFFICIENT_EVIDENCE, which the three-outcome
logic produces naturally.
* **Oversampling with weight correction**: rare + risky strata get risk
multipliers and floors (§4); HT weights keep population estimates unbiased.
* **Per-class thresholds**: the margin δ and alpha allocation are policy
fields; safety primaries effectively run at tighter per-node alpha with far
larger minimum samples. (A per-stratum δ_h map is a straightforward policy
extension; v1 keeps a single δ for legibility.)
* **Escalation**: rare strata that end INSUFFICIENT but suspect consume their
reserve up to the whole stratum. If a stratum is exhausted and still
undecided, the ledger says so explicitly (stopping_reason
`escalation_exhausted`) — the decision is a visible budget/detectability
statement, not silence.
## 10. Sample sizes and power (with the design effect)
Fixed-n planning numbers for a Bernoulli recall metric at baseline p₀ = 0.9,
one-sided α = 0.05, power 0.9 ((z₀.₉₅+z₀.₉)² ≈ 8.57). Paired variance uses
Var(d) ≈ ν − Δ² with measured-typical discordance ν ≈ 0.04 + Δ:
| detectable Δ | unpaired, per model arm | paired, iid units | paired, DEFF = 2 | paired, DEFF = 2, anytime (~×1.35) |
|---:|---:|---:|---:|---:|
| 1pp | 16,100 | 4,280 | 8,550 | ~11,500 |
| 2pp | 4,150 | 1,280 | 2,550 | ~3,450 |
| 5pp | 745 | 300 | 600 | ~810 |
| 10pp | 214 | 111 | 223 | ~300 |
Notes: (i) pairing beats doubling the corpus — the unpaired column is per
*arm*; (ii) the anytime-valid overhead (~30–40% at these scales, shrinking
with the e-process's adaptive bets) buys the right to stop at any time, which
in expectation *saves* samples whenever the effect is larger than the MDE
(the 10pp test stops ~15× before budget); (iii) measured DEFF on paired
differences was ≈1.2, not 2 — the table's DEFF=2 column is the conservative
planning figure requested, the ledger reports the realized one. The default
safety floor (2,500) is the 2pp row with the realized design effect plus
head-room.
## 11. Compute-budget staging
Default policy on a 320k corpus: plan ≈ 25k objects, staged
`[15%, 40%, 100%]` of plan (≈ 3.8k → 10k → 25k) + escalation reserve, i.e. a
10K→50K→250K→full ladder scaled to plan size. Advancement rules: advance
while any node is undecided and budget remains; stop immediately on any
confirmed REGRESSION (sticky, anytime-valid) or when overall + all safety
primaries PASS with nothing undecided; escalate only suspect/safety strata;
terminal state otherwise INSUFFICIENT_EVIDENCE with per-stratum sample
accounting in the ledger.
## 12. Prediction caching
Baseline predictions are computed once per (dataset fingerprint, model
fingerprint) and reused across every candidate update
(`paired.PredictionCache`, disk + memory). Fingerprints cover everything
predictions depend on — dataset identity/seed/size, model version + effects
config — so **invalidation is by construction**: any change produces a new
key; nothing stale can ever be served. A compute counter proves in tests that
the second candidate evaluation performs zero baseline recomputation. In a
real deployment the fingerprint inputs become dataset content hash + model
checkpoint digest + inference config (thresholds, NMS, quantization), and the
cache stores per-unit predictions rather than a success vector.
## 13. Sampling-bias prevention
* The plan is built by a function that **cannot see candidate outcomes** (no
such parameter exists), from population structure + a prior variance
profile only, then persisted with seed, config and a content hash over the
selected and reserve unit ids. The hash is recorded in every run's lineage;
two candidates with opposite outcomes provably receive byte-identical plans
(asserted in tests).
* Escalation draws from a reserve whose order was frozen at plan time, so
even adaptive *budget* decisions never become adaptive *selection*.
* **If outcome-adaptive sampling is ever added** (e.g. active sampling of
high-disagreement regions), estimates must be corrected with
inverse-propensity weights w_i = 1/π_i under recorded, everywhere-positive
selection probabilities π_i, and CS validity must be re-established for the
weighted observations (bounded w_i·d_i, e.g. via truncated propensities).
Without that correction, adaptive selection biases Δ̂ toward whatever the
selector chases. v1 deliberately does not do adaptive selection.
## 14. Bayesian complement
Alongside every frequentist decision, `sequential.py` computes a Beta-Binomial
posterior P(Δ < −δ | data) from the discordant pairs (θ = P(regression pair |
discordant) with Beta(1,1) prior; Δ = ν(1−2θ)). Operational comparison:
* *For* the posterior: a single monotone number ("99.99% probable this is a
real regression") that program managers read correctly without training;
natural for a decision-theoretic cost tradeoff.
* *Against* (as the gate): posteriors have no frequentist stopping guarantee
under continuous monitoring without committing to a prior + loss and
accepting their audit burden; e-values give the same "accumulating
evidence" narrative *with* an error guarantee any auditor can check
(e ≥ 1/α), and they compose across strata (e-BH) and across looks by
multiplication.
Verdict: **e-values gate, posterior explains.** Both are in the ledger.
## 15. Failure modes and mitigations
| Failure mode | Mitigation in this design |
|---|---|
| Peeking inflates type-I | everything anytime-valid; fixed-n intervals never gate (§6) |
| Correlated frames overstate n | cluster inference units; DEFF/n_eff in every record (§2) |
| Masking: overall improves, stratum regresses | reserved per-level alpha + pre-registered safety primaries (§8) |
| Multiplicity across strata | hierarchical budgets + e-BH within level (§8) |
| "No signal" read as "safe" | three-outcome logic; INSUFFICIENT carries "not proven equivalent" (§7) |
| Outcome-dependent sampling bias | frozen hashed plans, frozen reserves, no adaptive selection (§13) |
| Rare strata silently undecidable | floors, oversampling+weights, escalation, explicit exhaustion reason (§9) |
| Stale baseline predictions | fingerprint-keyed cache, invalidation by construction (§12) |
| Broken candidate export burns budget | sanity smoke stage aborts in ~500 units (§5) |
| Trivial-magnitude "significance" | practical margin δ in both null hypotheses (§1) |
| Irreproducible decisions | seeds + fingerprints + plan hash + config in lineage; end-to-end determinism asserted in tests |
| Metric doesn't decompose per object (mAP) | pre-registered checkpoint paired clustered bootstrap, not fake sequential (§3) |
## 16. Final recommendation (defaults shipped in `controller.DEFAULT_POLICY`)
Paired, cluster-unit, stratified evaluation against a frozen hybrid
Neyman/risk-floor plan; empirical-Bernstein confidence sequences + betting
e-processes with a 0.5pp practical margin; three-outcome decisions; hierarchical
alpha budget (30% overall / 30% safety primaries / 15% class / 15% stratum /
10% difficulty) with e-BH within levels; staged budget 15%→40%→100% of a ~25k
plan with targeted escalation; fingerprint-keyed baseline cache; Beta-Binomial
posterior as explanatory complement. CUSUM reserved for future production
drift monitoring, not release gating.
## 17. Worked example: planted 2pp pedestrian-night regression (real run)
Setup: population `320k` objects / 26,666 containers (megaeval synthetic,
seed 42). Baseline `model-v41`. Candidate `model-v42` with planted effects
{pedestrian|night: −2pp, global: +0.4pp} — the masking case. Default policy
(δ = 0.5pp, α = 0.05, plan target 24k). Ground truth measured on the full
corpus afterwards: overall Δ = **+0.28pp** (candidate looks *better*
headline-wise), pedestrian|night Δ = **−1.63pp** realized (N = 17,561,
baseline recall 0.801).
Run (deterministic, reproducible from lineage):
* Plan: 25,699 objects frozen across 18 strata; pedestrian|night allocated
2,501 (floor-driven; weight w = 7.02) + 15,060-object reserve.
* Decision: **REGRESSION** at 26,705 objects total = **8.4% of the corpus**
(~12× cheaper than a full paired evaluation), stopping reason
`regression_confirmed`, during escalation of the suspect safety primary.
* Pedestrian|night node at stop: n = 3,507 objects in 1,195 clusters
(ICC 0.065, DEFF 1.20, n_eff 2,923); Δ̂ = −1.94pp,
95% CS [−3.97pp, −0.48pp]; discordant pairs n01 = 110 vs n10 = 42;
log e-value **7.50** vs boundary log(1/α_safety) = **4.89**
(e ≈ 1,808 ≥ 133); Beta-Binomial P(Δ < −δ | data) = 0.99999.
* Evidence trajectory (n, log e): (129, 1.5) → (1002, 1.6) → (2501, 2.8) →
(3006, 4.9) → (3507, 7.5) — the dashboard plots exactly this against the
4.89 boundary.
* Overall node: Δ̂ = +0.03pp, CS [−0.51pp, +0.38pp] — correctly *not*
regressed and *not yet* provably equivalent; the hierarchy is what surfaced
the stratum despite the healthy headline.
* Ledger: 28 node records, each with the full §15-proof field set; lineage
carries plan hash, fingerprints, seed and the entire statistical config.
The same engine with a null candidate (no planted effect) across 24 Monte
Carlo repetitions produced **zero** false REGRESSION verdicts (α = 0.05), and
with a planted 10pp effect stopped at ~2.8k samples — 11% of plan, <1% of the
corpus (both asserted in `tests/test_seqeval/`).