Skip to content
All projects

Machine learning2025Published

Classifying gym exercises from wrist-worn IMU data

A CNN-LSTM that reads windows of wrist motion-sensor data and names the gym exercise, with 90% accuracy on sets it never saw in training. The cleaning and the leakage-aware split around the model are what make that number worth trusting.

Role
Independent ML project
Date
October 2025
Data
Gym Workout IMU, a public Kaggle dataset
Stack
Python, PyTorch, scikit-learn, Pandas, NumPy
Scope
Preprocessing, split design, model, evaluation
  • Python
  • PyTorch
  • CNN-LSTM
  • Time series
  • IMU
  • Wearables
  • scikit-learn
  • Pandas
  • +1
Problem
Turn raw recordings from a wrist-worn IMU, one CSV file per gym set, into the name of the exercise being performed.
Approach
Clean and window every recording, split by whole set, fit the scaler on training data only, weight the loss by class and train a 1D CNN followed by LSTM layers.
Outcome
90% accuracy on held-out sets. The confusion matrix shows errors between biomechanically similar exercises, pointing to where more data is needed.

Each set is one multichannel recording with one label

The data comes from Gym Workout IMU, a public dataset on Kaggle. Each CSV file holds one set recorded by a wrist-worn device. Its motion channels include rotation rate, gravity, linear acceleration and quaternion orientation, aligned by timestamp, and the label is the exercise.

That makes it multivariate time-series classification: given a window of wrist motion, say which exercise it comes from. It is the building block for fitness tracking and form analysis.

A wrist sensor only measures the wrist. Exercises that move it along similar paths produce similar signals, and that is where the hard cases are.

Four steps turn a raw recording into model-ready windows

Every CSV goes through the same preprocessing. Smoothing and downsampling reduce noise and compute; fixed-length windows give the network inputs of a single shape.

  1. 01

    Trim

    • Drop the noisy first and last seconds of the set
  2. 02

    Repair

    • Fix or drop missing values (NaNs)
  3. 03

    Smooth and downsample

    • Rolling averages
    • Stride-based downsampling
  4. 04

    Window

    • Fixed length, e.g. 300 timesteps
    • Each window keeps its set's label

The scaler is left out of this stage on purpose: it learns means and variances, so it is fit after the split, on training data only.

Whole sets, not windows, are the unit of the split

Windows cut from the same set share the same person, the same session and the same sensor placement. Shuffle all the windows and split them at random, and almost every test window has near-identical neighbors in the training data. The score then rewards remembering a set rather than recognizing a new one.

So the split happens at file level and is stratified: each set goes whole to training, validation or test, and no set contributes windows to more than one of them.

  • Train
  • Test

Random window split

  1. Set 1 · Exercise A
  2. Set 2 · Exercise A
  3. Set 3 · Exercise A
  4. Set 4 · Exercise B
  5. Set 5 · Exercise B
  6. Set 6 · Exercise B

Almost every test window has siblings from the same set in training, so the score partly rewards memory.

Split by set (used here)

  1. Set 1 · Exercise A
  2. Set 2 · Exercise A
  3. Set 3 · Exercise A
  4. Set 4 · Exercise B
  5. Set 5 · Exercise B
  6. Set 6 · Exercise B

Test sets are never seen in training or by the scaler, so the score measures recognition of a new set.

Schematic, not the real data sizes: six sets of eight windows, validation omitted for clarity. The real split also holds out whole sets for validation.

Three smaller decisions keep the number honest

  1. 01

    Fit the scaler on training data only

    A StandardScaler learns means and variances. Fit on all the data, it would let test statistics shape the training inputs, so it is fit on the training split and applied unchanged to validation and test.

  2. 02

    Weight the loss by class

    Some exercises are under-represented. Class weights in the cross-entropy loss make mistakes on rare classes cost more, so the model cannot score well by neglecting them.

    With weighting on, training curves were stable and precision and recall were balanced.

  3. 03

    Judge per class, not by one number

    A classification report and a confusion matrix show which exercises fail and what they are mistaken for. Overall accuracy hides both.

Convolutions find local patterns, the LSTM follows them over time

The architecture is a standard hybrid and a solid default for wearable IMU data.

  1. 01

    Input

    • One window
    • Timesteps × channels
  2. 02

    1D CNN blocks

    • Local features in short stretches of signal
  3. 03

    LSTM layers

    • How those features evolve across the window
  4. 04

    Dense head

    • One score per exercise class

Built in PyTorch and trained with class-weighted cross-entropy.

90% on unseen sets, with errors where the movements look alike

accuracy on held-out sets
90%
With class weighting and full preprocessing
sets shared between training and test
0
The split is by file, not by window

Held-out data is whole CSV files, one set each, kept out of training and out of the scaler fit.

On held-out sets the model separates the common exercises well. The confusion matrix shows where it still fails: between biomechanically similar movements, which leave similar traces at the wrist. Those errors are useful, because they point to the exercises that need more data.

One limit to state plainly: holding out sets is not the same as holding out people. The 90% estimates performance on new sets. Performance on a brand-new user still has to be measured, with more subjects and a split by person.

Next: more people, rep counting and a lighter model

  • Collect more users and exercise classes, and test on people the model has never seen.
  • Add rep counting on top of the class predictions for richer workout analytics.
  • Prototype a lighter on-device model for real-time feedback.
  • Compare GRU and Transformer variants, and add augmentation such as jitter and time-warping.

Next case study

Improving a Grey Wolf Optimizer for job shop scheduling

Recorded 15-minute SOL window: the Binance ask (blue, left axis, 78.4 to 78.9) plotted against the Polymarket Up bid (orange, right axis, 0 to 0.9) over 900 seconds

Market systems2025–2026Stopped

Polymarket trading systems

Prediction-market research: a dataset builder, a 15-minute bot and a Rust + Python HFT recorder.

Get in touch

Working on something similar? I am happy to compare notes.