Lab 8.4 — Vibration Anomaly Detection (Autoencoder)
← Course 2 syllabus · Bonus Module 8 · Prev: « Lab 8.3 · Next: Lab 8.5 »
Goal
Build a predictive-maintenance-style anomaly detector: train an autoencoder only on normal machine signals (vibration from an accelerometer, or a fan/motor audio proxy), then flag anomalies at run time when the reconstruction error exceeds a threshold. The skill built is the unsupervised edge-ML pattern that shows up in industrial and robotics firmware everywhere — you rarely have labeled fault data, so you model “normal” and detect deviation. You’ll pick the threshold properly from an ROC curve rather than eyeballing it, and run the detector on the edge boards. This ties the course’s statistical-DSP thread (random processes, spectral features) to a deployed, decision-making system.
Recommended reading
- Hayes Ch. 3 — discrete-time random processes: stationarity, autocorrelation, and the power spectrum of a random signal. A healthy machine is (roughly) a stationary process; a fault changes its statistics. → ../books/hayes-statistical-dsp/index.qmd
- Hayes Ch. 4 — signal modeling (AR/ARMA): the classical “model the normal, watch the residual” detector, which is the linear ancestor of the autoencoder residual you’ll use.
- Hayes Ch. 3, 8 — random-process classification and spectrum estimation with quantified error: what turns a raw accelerometer stream into trustworthy features. Carry over the error-bar results from Lab 6.4 when you quote a baseline spectrum.
- Course 1 Parts III–IV — probability: distributions of the reconstruction error, choosing a threshold as a tail probability / false-alarm rate, and (Lesson 41) the ROC as a likelihood-ratio trade-off. → ../course1/index.qmd#part-3
- Such Ch. 7–9 (Embedded AI, No Starch — see the reading key) — the IMU option’s supporting chapters: raw accelerometer/gyro preprocessing into orientation (Ch. 7), complementary/quaternion/Kalman sensor fusion (Ch. 8 — pairs with Lab 6.6), and ML on fused sensor streams (Ch. 9). Skip if you run the mic audio-proxy path.
Equipment & parts
- Host Mac (PyTorch) for training; ONNX export.
- Jetson Orin Nano and/or Raspberry Pi 5 for the on-device detector.
- A vibration/audio source — pick one:
- Audio proxy (simplest): a small fan or DC motor recorded through the USB/I²S mic. “Normal” = healthy running; “anomaly” = imbalance (a bit of tape on a blade), a stalled/loaded condition, or a bad bearing sound.
- Optional accelerometer/IMU: a BNO055 / ICM-20948 (I²C) mounted to the machine gives true 3-axis vibration; sample it over I²C on the board. This is the “real” predictive-maintenance sensor path.
- The ADS1115 reading an analog accelerometer or a piezo disc — low rate, fine for low-frequency mechanical vibration.
Wiring & bench setup
Audio-proxy path (simplest): no wiring — the USB mic from Lab 8.1 plugs into the board and sits close to (but clear of) the fan/motor. Power the motor from the WANPTEK on its own rail, never from the Pi/Jetson supply (back-EMF/inrush — see Safety).
Optional IMU path (BNO055 / ICM-20948, 3.3 V breakout): I²C on the 40-pin header — same header positions on the Pi 5 and the Jetson:
| IMU breakout | Purpose | 40-pin header |
|---|---|---|
| VIN/VDD | 3.3 V | pin 1 (3V3) |
| GND | ground | pin 9 (GND) |
| SDA | I²C data | pin 3 (SDA) |
| SCL | I²C clock | pin 5 (SCL) |
Mount the breakout rigidly to the machine (see Safety) and strain-relieve the harness so cable rattle doesn’t become “vibration”. Confirm the device with i2cdetect (BNO055 ≈ 0x28; ICM-20948 ≈ 0x68/0x69 — check your breakout): bus 1 on the Pi (i2cdetect -y 1); on the Jetson the header I²C enumerates as a different /dev/i2c-N (i2cdetect -l to find it).
The ADS1115 + piezo/analog-accelerometer option wires as in Lab 3.4 on the same SDA/SCL pins, with the clamp/divider from Safety on the piezo.
Safety & don’t-break-it
- Mechanical safety first. A spinning fan/motor with an intentional imbalance can throw the added mass. Secure the machine, keep fingers and leads clear of blades, and use a light, well-stuck imbalance mass. Eye protection if anything can fly off.
- Sensor mounting matters more than the model. A loosely mounted accelerometer measures its own rattle, not the machine. Rigidly couple it; keep the axis orientation fixed between the “normal” training data and the test data or the features shift and everything looks anomalous.
- I²C rules for the IMU/ADC: within-rails, common ground, correct address, and level-shift if the sensor is 5 V (Lab 3.5). Don’t exceed the ADS1115 input range with a piezo’s voltage spikes — piezos can output large transient voltages; clamp/divide.
- Motor back-EMF / inrush on the supply: drive the motor from a separate supply rail or add a flyback diode; don’t let motor transients reset the Pi/Jetson sharing the rail.
Project & environment setup
Mac (training) — the ~/train-venv from Lab 8.2 (torch, numpy, onnx, soundfile, matplotlib); the owner writes the featurizer and autoencoder training script.
Pi 5 — the audio-proxy path needs nothing beyond the ~/edge-venv from Labs 8.1–8.3 (onnxruntime, numpy, sounddevice, soundfile, and scipy — scipy.signal.welch/spectrogram for the band-energy/spectral features; owner writes the featurizer). For the IMU path add:
sudo apt install i2c-tools # i2cdetect
sudo raspi-config # Interface Options → I2C → enable (Pi only)
pip install smbus2 # you write the I²C sampling loopJetson Orin Nano — PyTorch (Jetson wheel) or a TensorRT engine as in Lab 8.2; i2c-tools installs from apt for the IMU path (header I²C is enabled by default).
Scripts (you write them) under labs/lab-8-4/edge/: collect.py (Parts A/B — record + featurize into windows), detect_stream.py (Part D — live score, threshold, debounce).
Where results go:
| Artifact | Path |
|---|---|
| Normal recordings / feature windows | labs/lab-8-4/captures/normal/ |
| Induced-fault recordings (never in training) | labs/lab-8-4/captures/anomaly/ |
| Exported autoencoder | labs/lab-8-4/edge/ae.onnx |
| Normal-error histogram (τ overlaid) + ROC (AUC, operating point) | labs/lab-8-4/edge/hist.png, labs/lab-8-4/edge/roc.png |
| Live-run detector log (scores, fires, false alarms) | labs/lab-8-4/edge/live-log.csv |
| Bench note | labs/lab-8-4/notes.md |
Background
Model the normal. Featurize a window of the signal the same way as the rest of the module — a log-mel or magnitude spectrogram, or a vector of band energies / RMS / spectral centroid, or (for an IMU) per-axis spectral features. Call the feature vector \(x \in \mathbb{R}^d\). Train an autoencoder \(x \mapsto \hat{x} = g_\theta(f_\theta(x))\) with a bottleneck, on normal data only, minimizing reconstruction error
\[ \mathcal{L}(\theta) \;=\; \frac{1}{d}\,\lVert x - \hat{x} \rVert_2^2 . \]
Because the bottleneck forces the network to learn the low-dimensional manifold of normal operation, it reconstructs normal windows well (small error) and fails to reconstruct patterns it never saw (large error). The per-window anomaly score is the reconstruction error
\[ e(x) \;=\; \lVert x - \hat{x} \rVert_2^2 , \]
and the detector fires when \(e(x) > \tau\).
Choosing the threshold \(\tau\). Fit the distribution of \(e\) on held-out normal data. A principled default sets \(\tau\) from a target false-alarm rate — e.g. mean-plus-\(k\)-sigma,
\[ \tau \;=\; \mu_e + k\,\sigma_e , \]
or a high empirical quantile (say the 99th percentile) of the normal-error distribution. With even a little labeled anomaly data, sweep \(\tau\) and plot the ROC curve (true-positive rate vs false-positive rate); pick the operating point from the cost of a missed fault vs a false alarm, and report the AUC as a threshold-independent quality measure. The ROC is exactly the likelihood-ratio trade-off from Course 1 Lesson 41 — each \(\tau\) is a likelihood-ratio cut.
Why an autoencoder over a classical AR residual. The Hayes Ch. 4 approach fits an AR/ARMA model to the normal signal and watches the prediction residual grow when the machine changes — a linear special case. The autoencoder generalizes it to nonlinear, multi-band structure, which matters for machines whose fault signature is a spectral-shape change, not just a power change.
Procedure
Part A — Collect normal data.
- Run the machine in its healthy state and record a long “normal” dataset across its real operating variation (warm-up, speed changes if any). Featurize into windows. Split into train / normal-validation.
Part B — Collect anomaly data.
- Induce faults you can create safely: imbalance (small mass on a blade), load/stall, a rough-bearing proxy. Record and featurize. Keep these out of training — they’re only for threshold selection and ROC.
Part C — Train on the host.
- Train the autoencoder on normal windows only; monitor the normal-validation reconstruction error (it should be low and stable). Export to ONNX.
# illustrative — owner writes/trains the real model
z = enc(x) # bottleneck
xhat = dec(z)
loss = F.mse_loss(xhat, x) # trained on NORMAL windows only
# at inference:
score = ((x - xhat)**2).mean(dim=1) # anomaly score e(x)Part D — Threshold, ROC, deploy.
- On normal-validation errors, compute \(\mu_e, \sigma_e\) and candidate \(\tau\); on the mixed normal+anomaly set, sweep \(\tau\) to build the ROC and pick the operating point. Record AUC.
- Deploy the encoder+decoder to the Pi 5 (onnxruntime) and Jetson (PyTorch/TensorRT). Stream live windows, compute \(e(x)\), apply \(\tau\) with a debounce (require \(N\) consecutive anomalous windows to fire — mechanical faults persist; single-window spikes are usually sensor noise).
- Log detection latency (feature + inference) and the live false-alarm rate over a clean run.
Deliverable & expected results
- The normal-error histogram with \(\tau\) overlaid; an ROC curve with AUC and the chosen operating point marked.
- A live-run log showing the detector staying quiet on normal operation and firing (after debounce) when you induce a fault.
| Quantity | Predicted / target | Measured |
|---|---|---|
| Mean normal reconstruction error \(\mu_e\) | small, stable | … |
| Threshold \(\tau = \mu_e + 3\sigma_e\) | (value) | … |
| False-alarm rate at chosen \(\tau\) | low (e.g. < 1%) | … |
| True-positive rate on induced faults | high | … |
| ROC AUC | → 1.0 (well-separated) | … |
| Detector latency, Pi 5 | (ms) | … |
| Detector latency, Jetson | (ms) | … |
Analysis & reconciliation
Interpret the normal-error histogram: a tight, low-variance distribution means the autoencoder learned the normal manifold well and \(\tau\) can sit close to the bulk; a heavy right tail on normal data means your feature or mounting is unstable and will cause false alarms. Read the ROC — if the anomaly and normal error distributions overlap (AUC well below 1), the fault doesn’t change the features you chose, so revisit the featurization (band energies vs full spectrogram, add an IMU axis). Justify the debounce from the physics: a real mechanical fault is persistent, so requiring consecutive anomalous windows trades a little detection latency for a large drop in false alarms — the same reasoning as contact debounce in firmware. Note the classical connection: your autoencoder residual is the nonlinear generalization of the Hayes Ch. 4 AR-model residual, and on a simple power-change fault the linear model might do just as well for far less compute — a point you quantify in Lab 8.5.
Going further
- Add the AR-residual detector (Hayes Ch. 4) as a classical baseline and compare its ROC to the autoencoder’s — find the fault type where the nonlinear model actually helps.
- Track the reconstruction error as a trend over time (a slow rise = incipient wear) instead of a hard threshold — the actual predictive-maintenance use case.
- If you used the audio proxy, redo it with a real IMU accelerometer and compare which sensor separates the fault better.
- Fuse multiple sensors (mic + IMU) into one feature vector and see whether the AUC improves.