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
| Change | What it does | Public LB |
|---|---|---|
| Per-subject whitening of the kNN video embedding | Subject-centred, PCA-whitened embedding for the graph | +0.003 |
| kNN temperature 0.5 | Softer neighbour weights | +0.001 |
| Whitened video-continuity link features | Per-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 sources | More candidate pairs from whitened similarity. Recall 0.930 to 0.965, exact successor 0.737 | inside the ensembles |
| ZCA video input to the window models | Per-subject ZCA of the 768-d frame features. Window F1 0.7253 to 0.7290 | +0.001 |
| Multi-run ensemble decode | Average 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
| Place | Public | Private |
|---|---|---|
| First (private) | 0.94538 | 0.94112 |
| Second (private) | 0.94355 | 0.93924 |
| Me | 0.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.