"The demo scored 94. Then a $2 accelerometer came loose in the field and the model, never having been asked, confidently kept scoring 94 on a flat line."
An Unpleasantly Surprised AI Agent
Prerequisites
This section assumes the leakage-safe splitting discipline of Section 65.2 and the operational-metric framing of Section 65.1. It draws on the physical fault mechanisms (bias, drift, saturation, quantization) catalogued in Chapter 2, and it evaluates the missing-modality architectures built in Chapter 50 without re-deriving them. The concern here is strictly evaluation: how to measure degradation under sensor failure, not how to engineer the redundancy that resists it (that is Chapter 68).
The Big Picture
A model trained and tested on pristine, fully-populated sensor frames is being graded on an exam it will never sit in production. Real fleets lose channels: a connector corrodes, a Bluetooth link drops a packet, an IMU saturates during impact, a thermistor freezes at its last reading. A clean-data leaderboard number tells you nothing about what happens then, and the failure is usually silent: the model still emits a confident prediction over garbage. This section makes robustness a first-class evaluated quantity. You will define a fault taxonomy, inject faults only at test time under a leakage-safe protocol, and summarize the result not as one accuracy but as a degradation curve with a worst-case floor. The goal is to know, before deployment, exactly how gracefully your system fails.
A taxonomy of sensor faults worth injecting
"Missing sensor" is only the simplest failure. A robustness evaluation that tests dropout alone will bless a model that then collapses on a stuck thermistor. The point of a taxonomy is coverage: you inject a representative fault from each family and report each separately, because the failure modes are mechanistically distinct and a model can be immune to one while catastrophically brittle to another. Six families cover the overwhelming majority of field incidents.
Dropout (missing) is a channel going absent: no samples, or a sentinel like NaN. Stuck-at (frozen) is the sensor repeating its last value, a flat line that a mean-imputer will happily accept as real. Bias and drift shift the signal by a constant or a slow ramp, the calibration failure of Chapter 2; the values stay in-range and plausible, which is what makes drift so dangerous to detect. Saturation and clipping pin the signal at a rail when the true value exceeds the sensor's range. Noise inflation multiplies the sensor's variance, modeling a failing amplifier or loose ground. Latency and desynchronization delay one stream relative to the others, breaking the temporal alignment that fusion depends on (Chapter 3). Table 65.5.1 pairs each family with its field cause and, critically, the imputation strategy it defeats, since a robustness gain that only survives your favorite imputer is not a gain.
| Fault family | Signal effect | Typical field cause | Defeats |
|---|---|---|---|
| Dropout | absent / NaN | connector loss, packet drop | models with no missing-mask input |
| Stuck-at | flat repeat of last value | frozen firmware, dead ADC | mean / forward-fill imputation |
| Bias / drift | in-range constant or ramp shift | decalibration, thermal drift | range and NaN checks |
| Saturation | clipped at rail | true value exceeds range | normalization by fixed scale |
| Noise inflation | variance × k | failing amp, loose ground | low-pass smoothing alone |
| Latency / desync | time-shifted stream | clock skew, buffering | early-fusion concatenation |
Key Insight
The most dangerous faults are the ones that stay in-range. A dropped channel announces itself as NaN and any competent pipeline can branch on it. A drifting bias or a stuck-at line produces perfectly plausible numbers, so the model has no structural signal that anything is wrong and neither does a naive range check. Consequently a robustness evaluation must weight the silent faults (stuck-at, drift, saturation) at least as heavily as the loud one (dropout). Reporting only dropout robustness is the single most common way a benchmark overstates field resilience.
The leakage-safe fault-injection protocol
Where you inject the fault decides whether you are measuring robustness or fooling yourself. The rule is that faults are a property of the test-time input, never of the labels, and never leaked into tuning. Three protocol choices make this rigorous. First, keep the evaluation set's ground truth exactly as it was on clean data: the correct activity, heart rate, or object is unchanged by the fact that one sensor broke, so the label must not move. What degrades is the input, and therefore the score. Second, if you train with fault augmentation (a legitimate and usually effective defense), the augmentation distribution and its random seeds must be disjoint from the evaluation faults. Tuning the dropout rate on the same seed you report against is the temporal cousin of the leakage sins in Section 65.2: it manufactures a robustness number the field will not honor. Third, hold the fault seed fixed across models so that model A and model B face byte-identical corrupted inputs; otherwise you are comparing luck, not robustness.
With the protocol fixed, the summary statistic is a degradation curve, not a single number. For a fault parameterized by severity \(s\) (dropout probability, drift magnitude, noise multiplier), sweep \(s\) from clean to catastrophic and plot the operational metric \(M(s)\). Two scalars condense the curve. The retained performance at a chosen operating severity is \[ R(s) = \frac{M(s)}{M(0)}, \] and the area under the degradation curve integrates resilience across the whole sweep, $$ \mathrm{AUDC} = \frac{1}{M(0)}\int_0^{s_{\max}} M(s)\,\mathrm{d}s. $$ Report both, plus the worst-case single-channel ablation: drop each sensor in turn and record the largest drop. That worst-case floor is what a safety case (Chapter 68) actually cares about, because the field will find your weakest channel whether or not your average looks healthy.
In Practice: a wrist wearable that leaned on one sensor
A sleep-staging wearable fused PPG, accelerometer, and skin-temperature streams and reported 0.81 macro-F1 on a subject-disjoint test set. Robustness evaluation told a different story. Under the taxonomy of Table 65.5.1, dropping the accelerometer cost only 3 F1 points, but a stuck-at fault on the PPG channel (the optical sensor losing skin contact and repeating its last sample, a nightly occurrence when the band loosens) cost 22 points, and the model never flagged low confidence, since a flat PPG line is a valid-looking input. Mean imputation had trained the model to trust the PPG channel implicitly. The worst-case single-channel ablation, not the average, exposed the fragility. The team added PPG-contact masking and stuck-at augmentation during training; the stuck-at penalty fell to 6 points and, as importantly, the model's abstention rate (Chapter 18) now rose when contact was lost instead of emitting a confident wrong stage.
Building the harness
A robustness harness is a fault-injection layer wrapped around your existing evaluation loop plus a curve summarizer. Listing 65.5.1 shows the core: a dictionary of fault functions keyed by family, a sweep over severities that re-scores the clean-label test set on corrupted inputs, and the two scalars from above. The design point is that the labels object is passed through untouched; only X is corrupted, which is what keeps the protocol honest.
import numpy as np
def dropout(X, s, rng): # s = fraction of channels zeroed per frame
M = rng.random(X.shape[:2]) < s
Xc = X.copy(); Xc[M] = 0.0
return Xc, (~M).astype(np.float32) # also emit the missing-mask
def stuck_at(X, s, rng): # s = fraction of channels frozen
Xc = X.copy()
for c in np.where(rng.random(X.shape[1]) < s)[0]:
Xc[:, c, :] = Xc[:, c, :1] # repeat first sample forever
return Xc, np.ones(X.shape[:2], np.float32)
def drift(X, s, rng): # s = bias in signal std units
return X + s * X.std(), np.ones(X.shape[:2], np.float32)
FAULTS = {"dropout": dropout, "stuck_at": stuck_at, "drift": drift}
def degradation_curve(model, X, y, fault, sevs, seed=0):
rng = np.random.default_rng(seed) # fixed seed: same across models
scores = []
for s in sevs:
Xc, mask = FAULTS[fault](X, s, rng)
scores.append(model.score(Xc, mask, y)) # labels y never corrupted
scores = np.array(scores)
audc = np.trapz(scores, sevs) / (scores[0] * (sevs[-1] - sevs[0]))
return scores, scores / scores[0], audc # M(s), retained R(s), AUDC
X and emits a missing-mask; the labels y pass through unchanged. A fixed seed guarantees every model faces identical corruption, and the returned triple gives the raw curve, retained performance, and area under the degradation curve.As Listing 65.5.1 emphasizes, dropout returns a missing-mask alongside the corrupted tensor. Whether a model can consume that mask is itself part of what you are evaluating: a fusion model built for missing-modality inputs (Chapter 50) reads it and reweights; a naive early-fusion concatenator ignores it and drifts. Running the same sweep on both, with the fixed seed, is exactly the apples-to-apples comparison the protocol demands.
The Right Tool
Hand-rolling a full corruption suite (a dozen fault functions, severity sweeps, per-severity reseeding, curve integration, and worst-case ablation over every channel) runs to a few hundred lines. Established perturbation-benchmark libraries collapse the boilerplate to a compose-and-run:
from robustness_suite import CorruptionSuite # e.g. an audiomentations /
suite = CorruptionSuite( # imagecorruptions-style API
faults=["dropout", "stuck_at", "drift", "gain", "desync"],
severities=5, seed=0)
report = suite.evaluate(model, test_loader) # curves + AUDC + worst-case
Listing 65.5.2 cuts the harness to four lines, but the taxonomy in Table 65.5.1 is still your responsibility: a suite that omits stuck-at because your camera-centric library never needed it will silently under-test a wearable.
Exercise
Take a multi-channel model you have trained (HAR, biosignal, or a small fusion model). Using the harness in Listing 65.5.1, produce a degradation curve for dropout, stuck-at, and drift, and compute AUDC for each. Then run the worst-case single-channel ablation and identify your most load-bearing sensor. Retrain with augmentation on only that fault family, using a seed disjoint from evaluation, and re-measure. Report which fault improved, which regressed, and whether the model's confidence tracked the degradation or stayed flatly overconfident.
Self-Check
1. Why must the ground-truth labels stay unchanged when you inject a sensor fault into the test input?
2. A team reports strong dropout robustness. Name two fault families from Table 65.5.1 that this number tells you nothing about, and explain why they are more dangerous to miss.
3. What specifically goes wrong if you tune your training-time augmentation severity on the same random seed you later evaluate against?
What's Next
In Section 65.6, we turn to the other half of the sleep-wearable story: even a robust model is untrustworthy if its confidence lies. We will measure calibration and uncertainty directly, so that when a sensor fails the reported probability drops truthfully instead of staying pinned at a confident, wrong value.