Data Collection & Dataset Design¶
Edge AI models fail in the field far more often from bad data than from bad architectures. A model trained on studio-quality recordings and clean lab photos meets a noisy kitchen and a badly-lit hallway in production, and the accuracy gap between "test set" and "real world" is almost always a dataset design problem. This module covers the discipline of building a dataset that actually represents deployment conditions: sampling strategy, labeling consistency, splitting correctly, and measuring class balance and drift before you ever start training.
The core failure mode: test accuracy that lies¶
The single most common edge-AI dataset mistake is a leaky split — training and test data drawn from the same recording session, same speaker, same lighting, same background. A model can score 98% on such a test set and 60% in deployment, because the test set never asked it to generalize across the things that actually vary in the field (different rooms, different people, different days, different hardware units).
import numpy as np
def session_based_split(samples, session_ids, test_sessions):
"""Split by SESSION, not by random shuffle, so no session's audio/images
leak between train and test. samples: list-like. session_ids: parallel
array of session identifiers. test_sessions: set of session ids held out."""
session_ids = np.asarray(session_ids)
test_mask = np.isin(session_ids, list(test_sessions))
train_idx = np.where(~test_mask)[0]
test_idx = np.where(test_mask)[0]
return train_idx, test_idx
# Example: 5 recording sessions, hold out session 4 entirely for test.
sessions = np.array([0, 0, 1, 1, 2, 2, 3, 3, 4, 4])
train_idx, test_idx = session_based_split(range(len(sessions)), sessions, {4})
print("train:", train_idx, "test:", test_idx)
A random shuffle of the same 10 samples could easily put both halves of session 4 on different sides of the split — the model "memorizes" that session's background noise and looks great on a test point drawn from the same recording. Splitting by session (or by speaker, by device unit, by day) closes that leak.
Measuring class balance and coverage¶
Before training, quantify two things: how balanced the classes are, and how much condition diversity (lighting, background noise, distance from sensor, orientation) each class actually covers.
def class_balance_report(labels):
labels = np.asarray(labels)
classes, counts = np.unique(labels, return_counts=True)
total = len(labels)
report = []
for c, n in zip(classes, counts):
report.append((c, n, n / total))
imbalance = counts.max() / counts.min()
return report, imbalance
def condition_coverage(metadata, condition_key):
"""metadata: list of dicts, one per sample, each with e.g.
{'label': 'person', 'lighting': 'dim', 'distance_m': 2.5}.
Returns per-label counts of each observed condition value."""
from collections import defaultdict
coverage = defaultdict(lambda: defaultdict(int))
for m in metadata:
coverage[m["label"]][m[condition_key]] += 1
return {k: dict(v) for k, v in coverage.items()}
meta = [
{"label": "person", "lighting": "bright"},
{"label": "person", "lighting": "bright"},
{"label": "person", "lighting": "dim"},
{"label": "no_person", "lighting": "bright"},
{"label": "no_person", "lighting": "bright"},
{"label": "no_person", "lighting": "bright"},
]
print(condition_coverage(meta, "lighting"))
# {'person': {'bright': 2, 'dim': 1}, 'no_person': {'bright': 3}}
That toy example already reveals a real bug: the no_person class has zero
dim lighting examples. A model trained on it may learn "dim = person" by
accident, simply because dim conditions never appeared in the negative
class during training.
Negative/background class design¶
For detection-style tasks (wake word, person detection, anomaly detection), the negative class is usually harder to design well than the positive class, because "everything that isn't the target" is enormous and easy to under-sample. Two negative-class mistakes recur constantly:
- Silence-only negatives (for audio): a model trained against pure silence as its only negative learns to detect "any sound" rather than the specific keyword, and false-triggers on TV, traffic, or conversation.
- Empty-background-only negatives (for vision): a model trained only against an empty room learns "any change in the frame," not "the specific object," and false-triggers on shadows, pets, or camera noise.
The fix is the same in both domains: deliberately collect hard
negatives — near-miss audio (similar-sounding words, background speech)
or near-miss images (other objects, partial occlusions, similar-looking
non-targets) — and verify their presence with condition_coverage-style
checks before training.
Data augmentation: multiplying coverage without multiplying collection¶
Augmentation is cheap coverage expansion, not a replacement for it — it can simulate variation you already have some real examples of, but it cannot invent variation you've never observed at all.
def augment_audio_snr(clean_signal, noise_signal, target_snr_db, rng=None):
"""Mix clean_signal with noise_signal at a target signal-to-noise ratio,
simulating a noisier deployment environment from clean recordings."""
rng = rng or np.random.default_rng()
sig_power = np.mean(clean_signal ** 2)
noise_power = np.mean(noise_signal ** 2)
target_noise_power = sig_power / (10 ** (target_snr_db / 10))
scale = np.sqrt(target_noise_power / (noise_power + 1e-12))
return clean_signal + scale * noise_signal
def augment_image_brightness(image, factor_range=(0.6, 1.4), rng=None):
"""image: float array in [0, 1]. Randomly scales brightness and clips."""
rng = rng or np.random.default_rng()
factor = rng.uniform(*factor_range)
return np.clip(image * factor, 0.0, 1.0)
A dataset with 10 real "dim lighting" photos, brightness-augmented into 200 variants, still only encodes 10 distinct scenes — augmentation multiplies apparent volume but not the number of independent real-world samples behind it. Treat augmented counts and real-collection counts as separate numbers in your coverage report.
Labeling consistency¶
Multi-person or multi-session labeling drifts. A condition_coverage-style
audit is cheap for conditions you tagged deliberately, but labeling
mistakes (mislabeled class, inconsistent boundary decisions — "is this
still a wake word if it's whispered?") need spot-checking:
def flag_label_disagreement(labels_a, labels_b, sample_ids):
"""Compare two independent labelings of the same samples (e.g. two
annotators, or a re-label pass) and report disagreement rate."""
labels_a, labels_b = np.asarray(labels_a), np.asarray(labels_b)
disagree = labels_a != labels_b
rate = disagree.mean()
disagreeing_ids = np.asarray(sample_ids)[disagree]
return rate, disagreeing_ids
A disagreement rate above a few percent between two labelers on the same sample set usually means the labeling instructions are ambiguous, not that one labeler is careless — fix the definition before collecting more data against it.
Edge-AI tradeoffs¶
Collection volume vs. condition diversity. 10,000 samples from one room teach a model far less than 1,000 samples spread across ten rooms, ten lighting conditions, and multiple hardware units — diversity of conditions usually beats raw count for field robustness.
Real hard negatives vs. synthetic augmentation. Augmentation is nearly free but can't manufacture a genuinely novel confusable case; a small budget of real hard-negative collection (10-20 minutes of a housemate talking near the wake-word mic) is often worth more than hours of synthetic noise-mixing.
Session-based splits vs. more training data. Holding out entire sessions/speakers/devices for testing means less data available to train on, but produces a test accuracy number that actually predicts field performance — a smaller number you can trust beats a larger number that lies.
Labeling speed vs. labeling consistency. A single fast labeler is
internally consistent but may encode one person's blind spots system-wide;
cross-checking with flag_label_disagreement costs time but catches
definitional drift before it poisons the whole dataset.
Cheat sheet¶
| Practice | Why |
|---|---|
| Split by session/speaker/device | prevents leakage that inflates test accuracy |
class_balance_report |
catches skewed class counts before training |
condition_coverage per class |
catches missing conditions (e.g. no dim negatives) |
| Collect real hard negatives | prevents the model learning the wrong signal |
| Track augmented vs. real counts separately | augmentation ≠ new information |
flag_label_disagreement |
catches ambiguous labeling instructions early |
| Re-check coverage after every collection round | datasets grow unevenly by default |
How It Actually Works¶
Why a leaky split inflates accuracy through the model's actual learned weights, not through evaluation bookkeeping. A neural network trained on samples from session 4 alongside samples from sessions 0–3 will, during gradient descent, happily fit any feature that reduces training loss — including session-specific artifacts like a particular room's background hum, a specific microphone's frequency response, or JPEG compression quirks from one camera unit. If a test sample from session 4 is held out by simple random shuffling, its label is technically unseen but its session-identity features were seen thousands of times during training on other session-4 samples — so the model's high test score partly reflects memorized session fingerprints rather than the target concept. Splitting by session removes every one of those fingerprints from the training set entirely, forcing the measured accuracy to depend only on features that generalize across sessions — which is the only kind of accuracy that predicts field performance.
Why SNR-targeted noise mixing is a power-ratio computation, not a
volume slider. augment_audio_snr computes sig_power = mean(clean²)
and noise_power = mean(noise²) (both are proportional to the physical
power of each signal), then solves for the scale factor that makes the
ratio of the two, in decibels, equal the target: SNR_dB = 10·log10(
sig_power / (scale²·noise_power)). Solving that equation for scale
gives exactly the sqrt(target_noise_power / noise_power) line in the
code. This is why the function scales by a square root, not a linear
factor — power scales with the square of amplitude, so achieving a
specific power ratio requires taking a square root of the corresponding
power ratio to get the amplitude ratio.
Why augmentation multiplying "apparent" dataset size doesn't multiply
information content. Each of the 10 real dim-lit photos encodes one
specific real-world scene: one exact arrangement of light sources,
surfaces, and shadows. Applying augment_image_brightness to it explores
one narrow, deterministic transformation (global brightness scaling) of
that same scene — it cannot synthesize the genuinely novel spatial
information a photo of a different dim room would contain (different
shadow shapes, different reflective surfaces, different noise patterns
from a different sensor). In information-theoretic terms, augmentation
increases the training set's support along dimensions the transform
covers (brightness) while leaving its support along uncovered dimensions
(scene geometry, sensor noise characteristics) exactly as narrow as the
original 10 samples — which is precisely why coverage reports must track
real and augmented counts separately rather than treating a bigger dataset
number as automatically more capable of generalizing.
Exercise¶
- Implement
session_based_splitand, using 4 synthetic sessions of different sizes, show that a random shuffle split places samples from the same session on both sides while the session-based split never does. - Build a
metadatalist of at least 20 synthetic samples across 2 classes and 3 conditions (e.g. lighting or noise level), deliberately leaving one class/condition combination empty, then confirmcondition_coveragesurfaces the gap. - Implement
augment_audio_snron a synthetic sine-wave "clean" signal plus synthetic white-noise, and verify the achieved SNR (recompute it from the mixed signal) is close to yourtarget_snr_dbrequest. - Design (in prose) a hard-negative collection plan for a real deployment scenario of your choice (a wake word for a specific room, or a person-detector for a specific camera position) — list at least 5 concrete hard-negative conditions you'd deliberately record.