Cascading Classifiers
The idea: filter fast, decide carefully
A cascading classifier chains a sequence of classifiers of increasing complexity and cost, where each stage's job is to quickly reject the 'easy negatives' and pass only the harder, ambiguous cases on to the next, more expensive stage. The classic example is the Viola-Jones face detector: the first stage is a tiny, cheap classifier that rejects roughly 50% of non-face image windows in a handful of operations; only surviving windows proceed to progressively heavier stages. By the final stage, only a small fraction of the original candidates remain, so the expensive, accurate classifier only ever runs where it's actually needed.
Why this beats running one big model on everything
If 95% of inputs are trivially negative, running a large, accurate model on all of them is mostly wasted computation. A cascade inverts the cost structure: cheap stages handle the bulk of traffic, and compute is spent proportionally to difficulty. This is both a latency optimization (critical for real-time systems like object detection or fraud screening) and often an accuracy lever too — a specialized early stage can be tuned for extremely high recall (never miss a true positive) even at the cost of precision, because later stages clean up the false positives it lets through.
Designing the stage thresholds
Each stage is tuned to a very high recall / moderate precision operating point — for example, 'reject this window only if you're virtually certain it's negative.' This means the overall cascade recall is the product of each stage's recall ($R_{total} = \prod_i R_i$), which is why individual stages must be tuned extremely conservatively: even a 99% per-stage recall compounds to meaningfully lost recall across 10+ stages. Precision, by contrast, improves multiplicatively in the right direction — each stage only tightens the surviving set, so overall precision rises as candidates pass through more filters.
import numpy as np
def cascade_predict(x, stages, thresholds):
"""stages: list of (cheap -> expensive) scoring functions.
thresholds: reject if score below threshold at each stage."""
for score_fn, thresh in zip(stages, thresholds):
score = score_fn(x)
if score < thresh:
return 0, "rejected_early" # fast negative, skip remaining stages
return 1, "passed_all_stages"
# Example: 3-stage cascade with increasing cost/accuracy
stage1 = lambda x: np.mean(x) # trivial heuristic, near-zero cost
stage2 = lambda x: np.dot(x, np.ones_like(x)) / len(x) # linear model
stage3 = lambda x: 1.0 if np.std(x) > 0.5 else 0.0 # "expensive" final model
thresholds = [0.1, 0.3, 0.5]
label, reason = cascade_predict(np.array([0.9, 0.8, 0.95]), [stage1, stage2, stage3], thresholds)
print(label, reason)
Cascades as an operating-point composition trick
| Property | Single large model | Cascading classifiers |
|---|---|---|
| Average compute per input | Fixed, high | Low — scales with difficulty |
| Worst-case latency | Predictable, high | Predictable, high only for hard cases |
| Tuning complexity | One model to tune | Per-stage thresholds to tune jointly |
| Typical use case | Offline batch scoring | Real-time detection, high-volume filtering |
What's next?
Cascades combine classifiers sequentially, trading early speed for later accuracy. A complementary family of techniques combines classifiers in parallel instead — training several models independently and merging their opinions — which trades compute for a different kind of robustness, and is where ensemble methods like stacking and voting pick up the thread.
I build these systems professionally.
Whether it's a RAG pipeline, analytics migration, or AI workflow — let's talk.