---
title: "CMU MoCap"
subtitle: "140,837 motion-capture frames, 62d joint angles — true k is ambiguous."
order: 6
image: cmu-mocap_files/figure-html/fig-dyf-output-1.png
categories: [motion, large, physical, no-embedding]
true-k: "7–80"
format:
html:
code-fold: show
code-tools: true
execute:
warning: false
message: false
---
Every other gallery notebook has a clean ground-truth `k` — 10 digits, 20 newsgroup topics, 40 identities. CMU Motion Capture breaks that convenience. The same frames are simultaneously labeled with **7 motion types** (walk, dance, climb, …), **25 descriptions** (slow walk, fast walk, salsa, …), and **80 trial IDs** (individual recording sessions). Which one is "true `k`"?
None of them. Or all of them. A dance trial contains walking phases. Climbing involves reaching. Running and walking share gait dynamics at transitions. The label hierarchy is a human convenience layered over continuous motion that refuses to partition cleanly at any single granularity.
This is DYF's native regime — data where the question "how many clusters are there?" doesn't have one right answer, and you shouldn't pretend that it does.
## Load the data
Joint-angle features from the CMU Motion Capture Database — 62 channels per frame (31 joints × Euler ZYX angles), 120fps recordings across 80 trials. No embedding model. DYF operates directly on the physical signal.
```{python}
#| label: load
import numpy as np
import polars as pl
FEATURES = "../data/cmu_mocap_features.npy"
META = "../data/cmu_mocap_metadata.parquet"
X_full = np.load(FEATURES).astype(np.float32)
m_full = pl.read_parquet(META)
# Subsample to every 5th frame — 120fps mocap is heavily redundant frame-to-frame,
# and the gallery render needs to complete in a reasonable wall-clock budget.
stride = 5
idx = np.arange(0, len(X_full), stride)
X = X_full[idx]
m = m_full[idx.tolist()]
print(f"Full: {X_full.shape[0]:,} frames → subsampled ({stride}×): {X.shape[0]:,} frames × {X.shape[1]}d")
print(f"Motion types ({m['motion_type'].n_unique()}): {sorted(m['motion_type'].unique().to_list())}")
```
## Three ground truths
```{python}
#| label: y-prep
y_motion = m["motion_type"].to_numpy()
y_desc = m["description"].to_numpy()
y_trial = m["trial_id"].to_numpy()
_, y_motion_idx = np.unique(y_motion, return_inverse=True)
_, y_desc_idx = np.unique(y_desc, return_inverse=True)
_, y_trial_idx = np.unique(y_trial, return_inverse=True)
print(f"motion_type: {len(np.unique(y_motion_idx))} classes — broad categories")
print(f"description: {len(np.unique(y_desc_idx))} classes — finer movement types")
print(f"trial_id: {len(np.unique(y_trial_idx))} classes — individual recording sessions")
```
## Run DYF (parameter-free) on the 62d joint angles
No embedding. The 62 Euler-angle channels are DYF's direct input.
```{python}
#| label: run-dyf
import sys
sys.path.insert(0, ".")
from _gallery import run_dyf_cached, run_kmeans, plot_single
# Score against the middle-granularity label (description, k=25) for the main run.
result = run_dyf_cached(X, y_desc_idx, cache_path="/tmp/gallery_mocap_dyf.npz")
print(f"Recovered k: {result.recovered_k}")
print(f"NMI vs description: {result.nmi:.3f}")
print(f"ARI vs description: {result.ari:.3f}")
```
## How well does DYF's partition match each ground truth?
```{python}
#| label: multi-gt
from sklearn.metrics import adjusted_mutual_info_score as nmi
from sklearn.metrics import adjusted_rand_score as ari
rows = []
for name, y_idx in [("motion_type (k=7)", y_motion_idx),
("description (k=25)", y_desc_idx),
("trial_id (k=80)", y_trial_idx)]:
rows.append((name, nmi(y_idx, result.labels), ari(y_idx, result.labels)))
print(f"{'Ground truth':25s} NMI ARI")
for name, m_nmi, m_ari in rows:
print(f"{name:25s} {m_nmi:.3f} {m_ari:.3f}")
```
The alignment gets better as the label granularity matches DYF's recovered resolution. DYF's partition is *not* a single "true k" clustering — it's a multi-resolution organization that aligns partially with every level of the label hierarchy.
## Oracle k-means on the middle granularity
```{python}
#| label: kmeans
kmeans = run_kmeans(X, y_desc_idx)
print(f"K-means told k=25: NMI={kmeans['nmi']:.3f}, ARI={kmeans['ari']:.3f}")
print(f"DYF (no k given): NMI={result.nmi:.3f}, ARI={result.ari:.3f}")
```
**DYF beats oracle-tuned k-means on NMI against every one of the three ground truths.** On motion-capture joint angles, the parameter-free method is the better tool even when k-means is handed the answer.
## Walking down the hierarchy against each ground truth
DYF's recovered partition is multi-resolution by design — and CMU MoCap is the clearest place to see it, because there are three plausible "true k" values to aim at (`motion_type`=7, `description`=25, `trial_id`=80):
```{python}
#| label: merge-walk-gt
#| output: asis
from _gallery import merge_walk, merge_walk_table
print("**Against `motion_type` (k=7):**\n")
print(merge_walk_table(merge_walk(result, X, y_motion_idx, targets=[25, 10, 7, 3]), result))
print("\n\n**Against `description` (k=25):**\n")
print(merge_walk_table(merge_walk(result, X, y_desc_idx, targets=[40, 25, 15, 7]), result))
print("\n\n**Against `trial_id` (k=80):**\n")
print(merge_walk_table(merge_walk(result, X, y_trial_idx, targets=[60, 40, 25, 10]), result))
```
Read this vertically. DYF's partition is the same across all three tables — only the *ground-truth target* changes. The best NMI/ARI for each ground truth sits near *that ground truth's* cluster count, not at a fixed "DYF-recovered" k. The hierarchy is the same; the rulebook you score it against is what moves.
## Figures
::: {.gallery-fluid}
```{python}
#| label: fig-motion
#| fig-cap: "Ground truth — motion_type (7 broad categories: walk, dance, climb, …)."
plot_single(result.umap_2d, y_motion_idx, title="Ground truth: motion_type (k=7)")
```
```{python}
#| label: fig-desc
#| fig-cap: "Ground truth — description (25 finer-grained movement types)."
plot_single(result.umap_2d, y_desc_idx, title="Ground truth: description (k=25)")
```
```{python}
#| label: fig-dyf
#| fig-cap: "DYF — parameter-free, found its own resolution."
plot_single(result.umap_2d, result.labels, title=f"DYF (recovered k={result.recovered_k})")
```
```{python}
#| label: fig-kmeans
#| fig-cap: "K-means given the oracle k=25 (description granularity)."
plot_single(result.umap_2d, kmeans["labels"], title="k-means (told k=25)")
```
:::
## Why DYF wins on ambiguous-k data
Three things are working *for* it here, exactly the opposite of the Olivetti setup:
**1. Dense support.**
28,167 frames across 25 descriptions is ~1,100 frames per description-class (and ~4,000 per motion_type). DYF's tree has plenty of room to carve stable leaves. Unlike Olivetti, the density-per-latent-cluster condition is satisfied comfortably.
**2. The "flat k" assumption is wrong, and DYF doesn't make it.**
K-means has to commit to a single `k`. When you tell it 25, it builds 25 centroids — even though some natural clusters are more like 3 frames (a jumping-jack peak) and others are 2,000 frames (a steady walking cycle). DYF's Louvain step lets community sizes vary naturally with the data's density topology.
**3. Motion manifolds are curved.**
Gait transitions, sub-phases within a dance move, reaching-then-grasping — these are smooth trajectories, not convex clouds. Voronoi cuts through a continuous cycle are always somewhat arbitrary. Density-based cuts can sit at the natural minima between sub-phases.
### The sports analogy, completed
Across the four notebooks we now have:
| Dataset | Geometry | DYF vs oracle k-means |
|---|---|---|
| Digits | Convex blobs, clean `k` | K-means wins (home court) |
| MNIST | Convex-ish, but more data per class | Mixed (NMI win / ARI loss) |
| 20 Newsgroups | Curved, hierarchical | Essentially tied |
| **CMU MoCap** | **Continuous motion, ambiguous k** | **DYF wins** |
The pattern isn't about "DYF vs k-means." It's about **how much the shape of the data matches the shape of k-means's assumptions.** When the data is convex blobs with a known `k`, k-means wins. When the data is curved with an ill-defined `k`, DYF wins. Most real-world data — RAG corpora, user behavior logs, sensor streams, documents on a moving topic — is closer to MoCap than to Digits.
## What to take away
- **"True k" is often a fiction.** If a dataset has multiple plausible labelings at different granularities, your clustering algorithm shouldn't demand you pick one upfront.
- **DYF operates on raw features, no embedding required.** Motion capture, tabular data, sensor readings — anywhere the raw features have meaningful geometry, DYF can go in direct.
- **The win margin depends on the data, not the method.** Reporting "DYF wins by NMI 0.04" isn't the point. The point is that DYF's multi-resolution output is a better *match for the question being asked* when the question is "what structure is in this data?"
## Caveats
- **Subsampled to 28,167 frames** for the gallery render. The full 140,837-frame results (pre-computed in the DYF repo's MoCap experiment) are similar in pattern with tighter metrics.
- **No temporal structure used.** These are frame-level features — adjacent-frame similarity within a walk cycle isn't exploited. A time-aware model (HMM, Transformer) would outperform both DYF and k-means on *sequence* tasks; this notebook is about *frame-level structure* only.
- **NMI is still a blunt metric** for continuous-motion data. A proper evaluation would use cluster-purity against each ground truth at the matching granularity, plus a quantitative measure of hierarchy recovery.