Saaim Abdullah
ProjectsNovember 15, 2025

Autoencoder compression for edge devices

Built with
  • Python
  • TensorFlow
  • TensorFlow Lite Micro
  • Flask
  • AWS
  • Amazon EC2
  • Amazon S3
  • ESP32
On a remote sensor the binding constraint is not compute, it is bandwidth. Sending raw signal is what drains the budget, so the question is how much of the signal can be reconstructed after squeezing it through a model small enough to run on the microcontroller itself.
  • Microcontroller-class hardware. Kilobytes of RAM, no room for a general-purpose runtime, and a model that has to fit in flash alongside the application.
  • Reconstruction has to stay faithful. Compression that loses the signal defeats the point of collecting it.
  • The budget is power, not time. Encoding has to cost less energy than transmitting the bytes it saves, or the whole exercise is negative.
The target is an ESP32 class device, which is what fixes the third constraint in place: the encoder has to share flash with the application, so the model budget is whatever is left over rather than whatever the model wants. An autoencoder is trained offline on representative sensor data. The encoder half is converted to TensorFlow Lite Micro and flashed to the device, where it turns each reading window into a small latent vector that goes over the radio. The decoder half sits behind a Flask API on AWS EC2 and reconstructs the original signal server-side, with S3 holding encoded payloads. The asymmetry is the entire design. The constrained side only ever runs the cheap half.
On an ESP32-class device, a 128-value sensor window runs through the encoder half of the autoencoder, converted to TensorFlow Lite Micro, integer-quantised and occupying 86 KB of flash. It emits a 16-float latent vector, a 7.8:1 reduction, which crosses the radio link to a Flask decoder on EC2 that reconstructs the signal at 0.0038 MSE, with S3 holding the encoded payloads.
The split that is the whole design: the device runs only the encoder, and the half that costs memory runs where memory is free.
Sizing the latent vector, and getting it wrong first. The first latent size I tried was too aggressive. It fit comfortably and reconstructed badly, in a way that was easy to miss because average error looked acceptable while the sharp transitions in the signal, the part actually worth transmitting, were being smoothed away. Mean error is the wrong lens for this: it rewards a model that reproduces the boring middle of the distribution. Widening the latent vector fixed the reconstruction and moved the problem to memory, which is where the quantisation work came in. The final encoder is integer-quantised and sized to leave headroom in flash for the application rather than consuming everything available. Measured:
  • Compression ratio: 7.8:1 versus raw readings
  • Reconstruction error: 0.0038 MSE at that ratio
  • Latent vector: 16 floats, down from 128 raw values
  • Encoder footprint on-device: 86 KB
Lossy learned compression over lossless encoding. The autoencoder reaches 7.8:1 by accepting reconstruction error a lossless codec never introduces. The cost is that the decoded signal is an approximation and its error tolerance has to be stated, not assumed. The benefit is a compression ratio lossless approaches cannot reach on this signal. Splitting the model across the boundary. The encoder runs on-device through TensorFlow Lite Micro; the decoder runs server-side on EC2. The cost is that the two halves must stay version-compatible across deployments. The benefit is that the device carries only the half that must be small. An autoencoder rather than a general-purpose compressor. Generic compression is domain-blind. An autoencoder trained on this sensor's actual distribution learns what is signal and what is noise for this data specifically, and reaches a much smaller representation at equal fidelity. The cost is that the model is useless on a different sensor, which is an acceptable trade for a fixed deployment. Quantised encoder over a smaller float model. TF Lite Micro wants integer arithmetic, and quantising a well-sized model beat shrinking a float one at the same footprint. I tuned latent size against reconstruction error alone. The metric that actually decides whether this project pays for itself is energy per encode measured against energy saved per transmission, and I never measured it. A model that compresses 7.8:1 while costing more power to run than the radio saves is a net loss, and I cannot currently prove it is not. That measurement comes first next time. I would also have chosen the error metric before the latent size rather than after. Mean squared error was the default, and it is the metric that hid the first failure: it rewarded a model that smoothed away exactly the sharp transitions the sensor exists to capture. I found that by looking at reconstructions, not by measuring. Picking a metric that weights transitions properly would have made the first attempt fail loudly instead of quietly, which is the whole point of having a metric.

Want to dig into this one?

I’m open to full-time backend, platform and data engineering roles, and happy to walk through any decision on this page in more detail than it deserves.
Get in touch