<- All posts

I finished 5th of 232 on a Kaggle wearables challenge.

The public leaderboard said 7th. The private one said 5th, and every team near the top lost points on the way. Here is what moved my score, the leak that looked like a gain, and what I'd try next.

I finished 5th of 232 teams in the 3rd WEAR Dataset Challenge @HASCA 2026 on Kaggle, with a macro-F1 of 0.93137 on the final private leaderboard. Only the top three places won prizes, so I didn't. There is no workshop paper or invitation either. This is a write-up of the method, because the interesting parts were not the ones I expected.

The task

Four test subjects each did 18 exercises while wearing IMUs and being filmed. Their recordings are cut into non-overlapping one-second tiles and shuffled. Every tile carries one random limb of IMU (50 samples by 3 axes) and 15 video frames. You label each tile with one of 18 exercises or "null". The metric is macro-F1 over those 19 classes. The private score uses about half the tiles; the public score uses the other half.

The twist is that the tiles are shuffled but not independent. A tile's neighbours in time are somewhere in the pile, and finding them is most of the game.

Where I started

I didn't build this from scratch. The pipeline is a fork of goodpjw2008's public Kaggle notebook (public score 0.929), which builds on woominyo's Timeline+Graph notebook and honghanhhh's timeline links. In outline: window models label each tile, a learned scorer guesses which tile follows which, Hungarian matching turns those guesses into chains, a graph propagates labels along the chains and over video neighbours, and a count calibration step pushes each subject's totals towards sensible numbers.

My reproduction of it scored 0.92497 on the public board. I worked through the experiments in sessions with Claude on rented cloud machines. Everything below is a change on top of that base.

What moved the score

ChangeWhat it doesPublic LB
Per-subject whitening of the kNN video embeddingSubject-centred, PCA-whitened embedding for the graph+0.003
kNN temperature 0.5Softer neighbour weights+0.001
Whitened video-continuity link featuresPer-subject-whitened cosine between the end of tile i and the start of tile j, as extra link features. Exact-successor rate 0.659 to 0.717+0.004
Whitened candidate sourcesMore candidate pairs from whitened similarity. Recall 0.930 to 0.965, exact successor 0.737inside the ensembles
ZCA video input to the window modelsPer-subject ZCA of the 768-d frame features. Window F1 0.7253 to 0.7290+0.001
Multi-run ensemble decodeAverage log-probabilities over runs, union their matchings, decode once+0.002

Together that took the public score from 0.92497 to 0.93825. The common thread: video features are compared across one subject's tiles, so centring and whitening them per subject (rather than across everyone) is what helped.

The leak that looked like a gain

My first version of the whitening used a covariance fitted on all 22 training subjects. It raised my out-of-fold score by 0.003 and lowered the leaderboard score by 0.003.

The out-of-fold score wasn't nested. The fold being scored had contributed to the whitening, so the number was meaningless. Per-subject whitening uses no labels and is allowed on test subjects, and it fixed the problem. The lesson I'm keeping: anything fitted on all training subjects before out-of-fold scoring makes that score untrustworthy, however good it looks.

Dead ends

About 25 ideas were flat or worse. The ones I'd warn a friend about:

  • A cross-encoder link model, and second-order (trigram) link prediction.
  • HMM and chain decoders. Even with the true links, an HMM only reached about 0.929.
  • Learned kNN embeddings, and variants of the window models.
  • A post-decode LightGBM corrector, and count corrections.
  • TSP-solver links: more links, no gain in the decode.
  • A second pseudo-label round: +0.0003.
  • Large majority votes over many runs. They always scored worse on the leaderboard than my best ensemble, even when their out-of-fold score was higher (0.9409 against 0.9386).

The shake-up

PlacePublicPrivate
First (private)0.945380.94112
Second (private)0.943550.93924
Me0.93825 (7th)0.93137 (5th)

Every top team dropped. I lost 0.007 and moved up two places, to 5th. The gaps at the end were 0.0013 to third place and 0.0098 to first. Differences of 60 to 130 tiles between my ensembles moved the public score by about 0.002, which is noise. A single borderline bout (one exercise in one test subject, about 95 tiles) could swing the score by 0.005.

I rejected configurations that flipped that bout to "null", even when out-of-fold liked them. One such setting gained 0.002 out-of-fold and lost 0.005 on the public board. Trusting the leaderboard over my own validation was uncomfortable, but it held up.

What I'd try next

  • A stronger refiner along the matched chains. With the true chain order it would be worth about +0.018; mine gains +0.003.
  • A pairwise re-ranker over the whitened space for the link scorer, whose exact-successor rate is stuck at 0.737.
  • Using the recording-session structure to set counts.

To be plain about it: this was AI-assisted, built on public notebooks, and outside the prize places. The score is real, and the method notes above are what I'd want to have read before starting.

See the leaderboard

The final standings are public on Kaggle. There is no public repository or notebook for my version.